使用AutoModelForSequenceClassification做多分类时模型无学习效果的排查
你的模型出现完全不学习、所有样本预测为同一类的情况,主要是训练循环里的几个致命错误导致的,还有标签处理的小细节需要修正,我们逐个拆解:
1. 训练循环开头的无效优化步骤(最关键问题)
你在每个epoch的最开头就执行了:
optimizer.step() lr_scheduler.step() optimizer.zero_grad()
这完全是错误的操作!此时你还没有处理任何batch、没有计算任何损失和梯度,就直接更新了模型参数——这不仅是无意义的参数变更,还会彻底打乱优化器和学习率调度器的状态,直接导致模型无法正常学习。必须删掉这三行代码。
2. 标签数据类型错误
你在预处理时把标签转成了普通的torch.Tensor:
encoding['labels'] = torch.Tensor(examples['labels'])
而nn.CrossEntropyLoss要求标签必须是整数类型的LongTensor(因为它要作为类别索引使用),但torch.Tensor默认生成的是FloatTensor。这种类型不匹配不会直接报错,但会导致梯度传递异常,模型无法学到有效信息。你需要改成:
encoding['labels'] = torch.LongTensor(examples['labels'])
3. 损失值的平均计算逻辑错误
你当前计算训练/验证损失的方式是:
training_loss.append(epoch_loss/num_training_steps) validation_loss.append(epoch_val_loss/num_val_steps)
但num_training_steps是整个训练周期的总batch数(NUM_EPOCHS * len(train_dataloader)),而epoch_loss只是当前一个epoch的总损失。正确的做法是除以当前epoch的batch数量,也就是:
training_loss.append(epoch_loss / len(train_dataloader)) validation_loss.append(epoch_val_loss / len(valid_dataloader))
否则你的损失值会被错误地缩小几十倍(因为你设置了NUM_EPOCHS=30),导致你完全无法正确判断模型的学习状态。
4. 学习率调度器的执行问题
你在每个batch里已经正确执行了lr_scheduler.step(),但之前epoch开头的额外lr_scheduler.step()会导致调度器提前走完所有预设步数,学习率过早降到极低水平,模型无法继续更新。删掉开头的无效步骤后,这个问题就会解决。
另外补充一点:BERT-base模型的常规学习率范围是2e-5到5e-5,你设置的1e-4偏高一丢丢——小数据集可能没问题,但大数据集容易出现训练震荡,你可以尝试降到5e-5看看效果。
5. 关于独热编码的疑问:不需要!
你的任务是多分类(每个样本属于唯一类别),problem_type="multi_class_classification"搭配LabelEncoder生成的整数标签是完全正确的,CrossEntropyLoss就是专门处理这种场景的损失函数。只有当你的任务是**多标签分类(每个样本可以属于多个类别)**时,才需要使用独热编码和BCEWithLogitsLoss。
修正后的核心训练循环
把开头的无效步骤删掉,修正后的循环代码如下:
for epoch in tqdm(range(1, NUM_EPOCHS+1), total = NUM_EPOCHS, position = 0, leave = True): epoch_loss = 0 epoch_val_loss = 0 model.train() for batch in tqdm(train_dataloader, total = len(train_dataloader),position = 0, leave = True): batch = {k: v.to(device) for k, v in batch.items()} outputs = model(**batch) loss = criterion(outputs.logits, batch['labels']) loss.backward() epoch_loss += loss.item() optimizer.step() lr_scheduler.step() optimizer.zero_grad() model.eval() with torch.no_grad(): for batch in tqdm(valid_dataloader, total = len(valid_dataloader), position = 0, leave = True): batch = {k : v.to(device) for k, v in batch.items()} outputs = model(**batch) val_loss = criterion(outputs.logits, batch['labels']) epoch_val_loss += val_loss.item() # 修正损失平均逻辑 training_loss.append(epoch_loss / len(train_dataloader)) validation_loss.append(epoch_val_loss / len(valid_dataloader)) pd.DataFrame({'train':training_loss,'valid':validation_loss}).to_csv("SMALL_BERT"+'_loss.csv') print(f'Epoch {epoch+0:03}: | Training Loss: {training_loss[-1]:.5f} | Validation Loss: {validation_loss[-1]:.5f}')
额外建议
- 可以在训练/验证阶段加入准确率计算,比如在验证时统计模型预测类别与真实标签的匹配率,这样能更直观地观察模型是否在学习。
- 检查一下当前数据集的类别分布,如果某个类别占比极高(比如超过90%),模型可能会倾向于预测这个类别,但你提到小数据集没问题,所以这个可能性较低,不过还是可以确认一下。
内容的提问来源于stack exchange,提问作者gankmeplease

