本文主要使用Qwen2ForSequenceClassification实现文本分类任务。
文章首发于我的知乎:https://zhuanlan.zhihu.com/p/17468021019
一、实验结果和结论
这几个月,在大模型分类场景做了很多实验,攒了一点小小经验。
1、短文本
1)query情感分类,一般不如BERT
ps:结论和,https://segmentfault.com/a/1190000044485544#item-13,基本一致
2、长文本
1)通话ASR转译长文本,BERT截断512不如LLM
- LLM没有截断(如果都阶段512,可能效果差不多)
- 没有对比,BERT进行文本滑动窗口的版本
2)Base v.s. Instruct
- 数据量小时,Base微调不如Instruct(Instruct模型有对齐税,但是微调数据量小时,效果还是比Base没见过指令微调样本的好)
3)SFT v.s. LoRA - 数据量小时(总样本10K以下,每个标签需要视情况而定),SFT微调不如LoRA(SFT调参成本也更大)
3、分类场景的提升方案
1)生成式微调独有
- 混合同领域相似数据类型不同业务数据,可以提升若干点
- 数据分布不能差异太大,特别是文本长度,否则混入这种数据反而会让效果下滑(一个平均长度1.2K,一个平均长度5k)
- 混入比例(接近2:1,不同场景需自行尝试)
- 混入顺序(我使用的是随机采样,没验证是否分开先后训练顺序是否有影响)
- 优化提示词(提示词中,增加各类别标签的精要描述;短文本可尝试few-shot)
2)分类头微调 + 生成式微调
- 数据量大时(10K以上,平均每个标签样本充足),尝试微调Base,而不是微调Instruct
- 数据增强:尝试无标注数据上跑的伪标签样本(提示词抽的标签 + 微调后的模型抽的标签)
- 数据量大时,尝试SFT
- LoRA微调时,加入LLM的embedding层(未验证过)
- 尝试蒸馏更大模型到小模型(痛点:大模型难调参,训练成本更高,部署上线还是得小模型)
- 尝试LoRA的变体
- 尝试调参(试过optuna自动搜索,效果也不太好;一般就调lr, epoch, rank)
3)重量级
- Base模型,领域数据增量预训练后,再进行指令微调
- 方案待验证
- 这边尝试了在Qwen2-7B-Instruct的领域数据指令微调后的模型,微调效果反而比直接微调Qwen2-7B-Instruct效果差些。由于不清楚该模型训练步骤细节,所以原因尚不明确)
- 若验证成功,其好处是训出来的基座在各个领域任务上的微调都能提点
- 方案待验证
第一优先级,还是搞数据。其次,才是尝试各种方案的加加减减。
4、注意点
-
学习率
- 训练的参数量越大,学习率适当要调小
-
标签噪声
- 样本标注错误,需要在错误分析时进行剔除和校正
-
分类业务规则
- 复杂场景,需要提前确定好完备的标注规则,避免返工(那些模型可以做,那么模型不能做)
二、文本分类-从BERT到LLM
Qwen2ForSequenceClassification和BERTForSequenceClassification,逻辑上是一致的。都是在模型的输出层,加上一个Linear层,用来完成分类任务。
之前在,BERT上做的所有改动,都可以迁移到LLM上。譬如,BERT-CRF。
和BERTForSequenceClassification一样。
BertForSequenceClassification是一个已经实现好的用来进行文本分类的类,一般用来进行文本分类任务。
通过num_labels传递分类的类别数,从构造函数可以看出这个类大致由3部分组成,1个是Bert,1个是Dropout,1个是用于分类的线性分类器Linear。
class BertForSequenceClassification(BertPreTrainedModel):
def __init__(self, config):
super(BertForSequenceClassification, self).__init__(config)
self.num_labels = config.num_labels
python
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(config.hidden_size, self.config.num_labels)
self.init_weights()
Bert用于提取文本特征进行Embedding,Dropout防止过拟合,Linear是一个弱分类器,进行分类,如果需要用更复杂的网络结构进行分类可以参考它进行改写。
forward()函数里面已经定义了损失函数,训练时可以不用自己额外实现,返回值包括4个内容
def forward(...):
...
if labels is not None:
if self.num_labels == 1:
# We are doing regression
loss_fct = MSELoss()
loss = loss_fct(logits.view(-1), labels.view(-1))
else:
loss_fct = CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
outputs = (loss,) + outputs
return outputs # (loss), logits, (hidden_states), (attentions)
接下来。看看Qwen2ForSequenceClassification。
Qwen2ForSequenceClassification(
(model): Qwen2Model(
(embed_tokens): Embedding(151936, 1024, padding_idx=151643)
(layers): ModuleList(
(0-23): 24 x Qwen2DecoderLayer(
(self_attn): Qwen2SdpaAttention(
(q_proj): Linear(in_features=1024, out_features=1024, bias=True)
(k_proj): Linear(in_features=1024, out_features=1024, bias=True)
(v_proj): Linear(in_features=1024, out_features=1024, bias=True)
(o_proj): Linear(in_features=1024, out_features=1024, bias=False)
(rotary_emb): Qwen2RotaryEmbedding()
)
(mlp): Qwen2MLP(
(gate_proj): Linear(in_features=1024, out_features=2816, bias=False)
(up_proj): Linear(in_features=1024, out_features=2816, bias=False)
(down_proj): Linear(in_features=2816, out_features=1024, bias=False)
(act_fn): SiLU()
)
(input_layernorm): Qwen2RMSNorm()
(post_attention_layernorm): Qwen2RMSNorm()
)
)
(norm): Qwen2RMSNorm()
)
(score): Linear(in_features=1024, out_features=3, bias=False)
)
Qwen2官方代码实现,内置三种模式:
- single_label_classification 单标签分类
- 损失为CrossEntropyLoss
- 取单标签对应logit,算负对数似然
- multi_label_classification 多标签分类
- 损失为BCEWithLogitsLoss
- 标签为muilti-hot, 预测logits计算sigmoid,实际取对应维度标签Logit,损失求和
- regression 回归
- 损失为MSELoss
- 默认为单维度回归(回归可以作为奖励模型,预测打分)
这3种模式的输入标签不同。
class Qwen2ForSequenceClassification(Qwen2PreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.num_labels = config.num_labels
self.model = Qwen2Model(config)
self.score = nn.Linear(config.hidden_size

5399

被折叠的 条评论
为什么被折叠?



