lc:98、51
熬夜看小说起晚了。晚上看了一下claudecode在国内怎么配置的问题,不得不说还真是八仙过海各显神通,什么法子都来了。CC官方防大陆也和防火一样生怕 claudecode在大陆能用。
配了一个小时差不多弄完了,然后把平时写日记的typora换成了obsidian并加上了claudecode插件。
开始做第一个项目:
Fine-tuning Qwen-1.5B with LoRA for Chinese Sentiment Analysis
用Qwen是因为它的中文能力较强,虽然说事实上它支持超过29种语言的多语言。符合我们用lora锦上添花的过程,而且它显存友好,部署起来并不会特别吃力。
| 特性 | Qwen | BERT |
|---|---|---|
| 架构 | Decoder-Only | Encoder-Only |
| 注意力 | Causal (单向) | Bidirectional (双向) |
| 预训练目标 | 自回归生成 | 掩码语言建模 |
| 分类时使用 | 最后一个 token | [CLS] token |
在GPT之前分类任务主流就是用bert,由谷歌2018年提出。
Decoder-Only 模型做分类时,通常取序列最后一个位置的隐藏状态,通过一个线性分类头输出类别概率。和生成式的区别仅仅是最后过的头不同,lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS, # 序列分类任务
...
)
不看视频,自己动起手来才发现github的autoxxx还是有点花样的,能够自己帮你处理很多结构。
比如说Qwen2.5-1.5B本来是文本生成的模型,如果用AutoModelForSequenceClassification去下载它,模型结构会自动调整:
AutoModelForSequenceClassification (分类任务)
| 层级 | 组件名称 | 状态 |
|---|---|---|
| 1 | Qwen Transformer | 预训练好的(和上面一样) |
| 2 | Score Head (2) | 随机初始化的(num_labels) |
AutoModelForCausalLM (生成任务)
| 层级 | 组件名称 | 状态 |
|---|---|---|
| 1 | Qwen Transformer | 预训练好的 |
| 2 | LM Head (151936) | 预训练好的(词表大小) |
关键区别:
- 分类任务:最后一层是 Score Head,只输出 2 个分数(对应"好"/"坏")
- 生成任务:最后一层是 LM Head,输出 151936 个分数(词表中的每个词)
分类任务就不是从超大词表里取出预测的下一个结果了,而是像从两个词的词表中抽一个,也就是"好"或者"坏"。
当然,tokenizer还是要的,虽然最后不用根据它把数据映射回真实词,直接用0、1二分类就行,但是Embedding还是要用tokenizer把数据变成词向量。
今天最后一个工作,理解一下数据预处理。
原始文本
{"text": "这家酒店环境非常好,服务态度也很棒!", "label": 1}
{"text": "服务态度太差了,再也不会来了。", "label": 0}
考虑到我们需要的是连贯的文字和可理解的内容。乱码、多余空格、重复标点,都是"脏"的数据,需要去除,但是内容量过于巨大,不可能一个一个看。所以采用下面的操作
def clean_text(text: str) -> str:
# 1. 统一空白字符
text = re.sub(r'\s+', ' ', text) # 多个空格/换行 → 单个空格
# 2. 移除控制字符
text = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', text) # 删除不可见字符
# 3. 标点符号去重
text = re.sub(r'([。!?,、;:])\1+', r'\1', text) # "好好好!!!" → "好好好!"
# 4. 去除首尾空白
text = text.strip()
第一个传参位置寻找要匹配的字符,第二个位置是替换的结果,第三个位置传入的是文本变量,除此之外还可以传入count,表示最多替换次数,不传则替换所有。flags,正则表达式标志,比如忽略大小写之类的操作可以由它指定。
分词处理
模型不能直接理解文字,需要转换成数字
def preprocess_function(examples, tokenizer, max_length=256):
# 1. 清洗文本
texts = [clean_text(text) for text in examples["text"]]
# 2. 分词:文字 → 数字
tokenized = tokenizer(
texts,
padding="max_length", # 填充到固定长度
truncation=True, # 超长就截断
max_length=max_length, # 最大长度256
return_tensors=None, # 返回Python列表
)
以 "这家酒店很好" 为例:
词汇切分: "这家酒店很好" → ["这家", "酒店", "很", "好"]
转换ID: ["这家", "酒店", "很", "好"] → [1234, 5678, 9012, 3456]
添加特殊token: [101, 1234, 5678, 9012, 3456, 102] (101=开始,102=结束),4个转换的token,加俩特殊token
填充: 如果不足256长度,用0填充到256
结果包含:
input_ids:[101, 1234, 5678, 9012, 3456, 102, 0, 0, ...](256个数字)attention_mask:[1, 1, 1, 1, 1, 1, 0, 0, ...](1=真实内容,0=填充)- labels:
1(情感标签)
接着用一个函数
def create_tokenized_dataset(dataset, tokenizer, max_length=256):
# 定义处理函数
def tokenize_fn(examples):
return preprocess_function(examples, tokenizer, max_length)
# 并行处理所有数据
tokenized_dataset = dataset.map(
tokenize_fn,
batched=True, # 批量处理,更快
num_proc=4, # 4个进程并行
remove_columns=[...], # 删除原始text列,只保留数字
desc="Tokenizing dataset",
)
批量调用刚刚的预处理过程即可,dataset.map封装好了最后的组装环节,直接输入各个参数即可。
完整流程
原始数据:
{"text": "这家酒店很好", "label": 1}
↓ clean_text
"这家酒店很好" (清洗后)
↓ tokenizer
{
"input_ids": [101, 1234, 5678, 9012, 3456, 102, 0, 0, ..., 0], # 256个
"attention_mask": [1, 1, 1, 1, 1, 1, 0, 0, ..., 0], # 256个
"labels": 1
}
↓ 输入模型训练
Why 这么处理
- 统一格式: 所有句子都变成256长度的数字数组
- 批量处理: 可以一次性处理多个样本,提高效率
- 模型理解: 模型只能处理数字,不能直接理解文字
- 内存对齐: 固定长度便于GPU并行计算
今天先到这吧,等着晚点看cs2的2026年的森载大战了。
转码学习day11
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法