PyTorch情感分析FFNN模型零准确率问题求助
电影评论情感分析FFNN模型准确率始终为0的问题排查
我基于电影评论数据集,用PyTorch构建了二分类情感分析的前馈神经网络(FFNN),任务仅区分正类(1)和负类(0)。模型流程为:生成词嵌入→对词嵌入取均值→经过两层网络与激活函数→通过sigmoid得到预测概率。但模型能正常运行,准确率却始终为0。
已尝试的解决方案:
- 更换损失函数
- 调整模型结构顺序
- 修改嵌入维度
相关代码
模型代码
from collections import OrderedDict import torch from typing import List import torch.nn as nn class FeedForwardNeuralNetClassifier(nn.Module): """ 前馈神经网络情感分类器。 """ def __init__(self, vocab_size, emb_dim, n_hidden_units): """ 在__init__函数中定义FFNN的模块。 :param vocab_size: 词汇表大小 :param emb_dim: 嵌入向量维度 :param n_hidden_units: 隐藏层单元维度 """ super(FeedForwardNeuralNetClassifier, self).__init__() self.vocab_size = vocab_size self.emb_dim = emb_dim self.n_hidden_units = n_hidden_units # 使用nn.Embedding实现随机初始化的词嵌入矩阵 # 尺寸应为(vocab_size x emb_dim) self.word_embeddings = nn.Embedding(self.vocab_size, self.emb_dim) # 定义模型剩余部分 self.layer1 = nn.Linear(self.emb_dim, self.n_hidden_units) self.layer2 = nn.Linear(self.n_hidden_units, 1) self.activation = nn.ReLU() self.sigmoid = nn.Sigmoid() def forward(self, batch_inputs: torch.Tensor, batch_lengths: torch.Tensor) -> torch.Tensor: """ 前向传播函数,定义FFNN在给定一批输入及实际句子长度(填充前)时的运行逻辑 :param batch_inputs: 尺寸为(n_examples, max_sent_length_in_this_batch)的torch.Tensor对象,即索引化的输入 :param batch_lengths: 尺寸为(n_examples)的torch.Tensor对象,描述每个样本的实际句子长度(填充前) :return FFNN的logits(即sigmoid之前的未归一化隐藏单元),形状为(n_examples) """ # 查找词嵌入 embeddings = self.word_embeddings(batch_inputs) # 逐元素平均层 averaged = torch.mean(embeddings, dim=1) # 隐藏层ReLU激活 hidden = self.activation(self.layer1(averaged)) # 输出层 return self.layer2(hidden) def batch_predict(self, batch_inputs: torch.Tensor, batch_lengths: torch.Tensor) -> List[int]: """ 对一批输入进行预测。该函数可直接调用forward(将输入传入FFNN并返回输出logits) :param batch_inputs: 尺寸为(n_examples, max_sent_length_in_this_batch)的torch.Tensor对象,即索引化的输入 :param batch_lengths: 尺寸为(n_examples)的torch.Tensor对象,描述每个样本的实际句子长度(填充前) :return: 该批数据的预测类别列表,0代表负类,1代表正类 """ output = self.forward(batch_inputs, batch_lengths) # Sigmoid激活 probability = self.sigmoid(output) # 将logits转换为预测标签 predicted_labels = (probability > 0.5).int().tolist() return predicted_labels
FFNN实例化代码
model = FeedForwardNeuralNetClassifier(vocab_size=len(vocab), emb_dim=300, n_hidden_units=300) # FeedForwardNeuralNetClassifier( # (word_embeddings): Embedding(4818, 300) # (layer1): Linear(in_features=300, out_features=300, bias=True) # (layer2): Linear(in_features=300, out_features=1, bias=True) # (activation): ReLU() # (sigmoid): Sigmoid() # ) device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") print(device) model = model.to(device) optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
训练代码
import time BATCH_SIZE=32 N_EPOCHS=20 # 为训练数据创建批次迭代器 batch_iterator = SentimentExampleBatchIterator( train_exs, batch_size=BATCH_SIZE, PAD_idx=PAD_IDX, shuffle=True) # 损失函数 loss_func = nn.BCEWithLogitsLoss() # 训练过程 best_epoch = -1 best_acc = -1 start_time = time.time() for epoch in range(N_EPOCHS): print("第%i轮训练" % epoch) batch_iterator.refresh() # 为当前轮次初始化新的迭代器 model.train() # 开启训练模式 batch_loss = 0.0 batch_example_count = 0 batch_data = batch_iterator.get_next_batch() while batch_data is not None: batch_inputs, batch_lengths, batch_labels = batch_data # 将数据移至指定设备 batch_inputs = batch_inputs.to(device) batch_lengths = batch_lengths.to(device) batch_labels = batch_labels.unsqueeze(1) batch_labels = batch_labels.to(device) # 清除当前批次的梯度 optimizer.zero_grad() # 调用模型并计算损失 outputs = model(batch_inputs, batch_lengths).float() batch_labels = batch_labels.float() loss = loss_func(outputs, batch_labels) # 记录损失和样本数量,用于统计 batch_example_count += len(batch_labels) batch_loss += loss.item() * len(batch_labels) # 基于损失进行反向传播 loss.backward() # 更新模型参数 optimizer.step() # 获取下一批数据 batch_data = batch_iterator.get_next_batch() print("平均损失: %.5f" % (batch_loss / batch_example_count)) # 在验证集上评估 model.eval() # 开启评估模式 acc, _, _, _ = evaluate(model, dev_exs, return_metrics=True) if acc > best_acc: best_acc = acc best_epoch = epoch print("第%d轮训练取得新的最佳准确率%.3f!" % (best_epoch, best_acc)) # 保存当前最佳模型参数 print("将最佳模型 checkpoint 保存为best_model.ckpt!") torch.save(model.state_dict(), "best_model.ckpt") print("已耗时: %s" % time.strftime("%Hh%Mm%Ss", time.gmtime(time.time()-start_time))) print("-" * 10) print("训练结束! 最佳准确率%.3f出现在第%d轮训练。" % (best_acc, best_epoch)) # 加载验证集上的最佳checkpoint model.load_state_dict(torch.load("best_model.ckpt"))
问题排查与修复建议
1. 词嵌入平均未忽略PAD token
当前模型对整个序列的词嵌入取均值,包含填充的PAD token。PAD token的随机初始化嵌入会稀释有效词的特征信息,导致模型无法学习到有用的情感特征。
修复代码:在forward函数中替换原平均逻辑,基于batch_lengths生成掩码,仅对有效token的嵌入取平均:
# 替换原averaged = torch.mean(embeddings, dim=1) mask = (batch_inputs != PAD_IDX).unsqueeze(-1).float() sum_embeddings = torch.sum(embeddings * mask, dim=1) # 避免样本长度为0时除以0 lengths = batch_lengths.clamp(min=1).unsqueeze(-1).float() averaged = sum_embeddings / lengths
2. PAD token嵌入未做特殊处理
PAD token的嵌入不需要参与学习,建议将其固定为0向量,避免干扰模型训练:
# 在模型__init__中添加 self.word_embeddings = nn.Embedding(self.vocab_size, self.emb_dim) # 固定PAD token嵌入为0 self.word_embeddings.weight.data[PAD_IDX] = torch.zeros(self.emb_dim) # 禁止PAD token嵌入的梯度更新 self.word_embeddings.weight.requires_grad = True self.word_embeddings.weight.register_hook(lambda grad: grad.index_fill_(0, torch.tensor(PAD_IDX, device=grad.device), 0.0))
3. 数据与训练流程检查
- 确认训练/验证集的标签分布是否均衡,若某一类占比极高,模型可能会偏向预测多数类导致准确率为0
- 检查
SentimentExampleBatchIterator是否正确生成输入、长度和标签,确保标签没有被错误统一为0或1 - 在训练过程中打印部分预测结果,确认模型是否始终输出同一类别,以此判断是模型学习问题还是数据问题
4. 评估函数验证
检查evaluate函数是否正确调用batch_predict,且标签匹配逻辑无误,避免因评估逻辑错误导致准确率计算异常
内容的提问来源于stack exchange,提问作者nojibe
相关产品推荐
相关产品推荐

