Python实现带负采样Word2Vec时梯度报错及训练不收敛问题排查
问题根因与修正方案
1. tensor have no grad_fn 报错与requires_grad_()的错误用法
你在forward返回值上手动调用.requires_grad_()是完全错误的操作,这个操作没有解决根本问题,反而直接切断了反向传播的梯度通路,是后续模型训练不动的核心原因之一。
这个报错的本质是你的前向传播代码意外切断了自动微分的计算图:
nn.Module中定义的nn.Linear层的权重,默认已经开启了requires_grad=True,只要前向过程全程使用PyTorch支持自动微分的张量操作,最终输出的预测张量会自动绑定grad_fn,不需要手动给任何张量开梯度。- 你在循环中写的
results.append(torch.tensor([forward_result[out_index] for out_index in output_indices_batch[i]]))存在严重问题:用torch.tensor()包裹从已有计算图张量中取出的值生成新张量,会直接斩断新张量和前面线性层权重的计算关联,新生成的是独立的无计算历史的叶子张量,自然没有grad_fn。 - 你手动给最终输出加
.requires_grad_(),相当于告诉autograd“这个张量是独立的叶子节点,梯度只算到这里就行”,反向传播时梯度根本不会传到前面的输入、输出层权重,参数全程不会更新。
2. 训练时损失、准确率无变化的其他诱因
除了上述计算图断裂的问题,你的代码还有几个硬伤直接导致训练失效:
- 训练循环缩进错误:你的
for epoch循环体里只有model.train()一行代码,后面的数据集生成、遍历dataloader、反向传播、参数更新、指标计算全在循环外部,不管设置多少个epoch,实际只会执行一次训练流程。 - 逐样本Python循环效率极低:你用for循环逐样本生成one-hot、逐样本索引得分,不仅速度慢,也更容易出现意外断图的问题,应该使用批量化的张量操作实现。
- 数值类型与计算逻辑错误:
BCEWithLogitsLoss要求输入标签为浮点型,你直接传入整型标签会出现隐式类型问题;计算平均损失时你直接累加带计算图的损失张量、且除以的步数值少算1(i_step从0开始计数),会导致显存泄漏、损失值计算错误。 - 指标累加逻辑问题:你直接累加张量类型的正确样本数,会长期持有计算图引用浪费显存,应该转成普通Python数值再累加。
修正后的可运行代码
模型前向传播修正
class Word2VecNegativeSamples(nn.Module): def __init__(self, num_tokens, wordvec_dim=10): super(Word2VecNegativeSamples, self).__init__() self.input = nn.Linear(num_tokens, wordvec_dim, bias=False) self.output = nn.Linear(wordvec_dim, num_tokens, bias=False) self.num_tokens = num_tokens def forward(self, input_index_batch, output_indices_batch): ''' 实现带负采样的前向传播 参数: input_index_batch: 整型张量,形状(batch_size,),批次内中心词索引 output_indices_batch: 整型张量,形状(batch_size, num_negative_samples+1),每个样本对应的正样本+负样本索引 返回: predictions: 浮点张量,形状(batch_size, num_negative_samples+1),对应位置的词得分 ''' batch_size = input_index_batch.shape[0] # 批量生成one-hot编码,替代逐样本循环 input_one_hot = torch.zeros(batch_size, self.num_tokens, device=input_index_batch.device) input_one_hot.scatter_(1, input_index_batch.unsqueeze(1), 1.0) # 计算中心词嵌入 center_embed = self.input(input_one_hot) # 计算所有词的得分 all_word_scores = self.output(center_embed) # 用gather取对应正/负样本位置的得分,不会切断计算图 predictions = torch.gather(all_word_scores, 1, output_indices_batch) return predictions
训练循环修正
def train_neg_sample(model, dataset, train_loader, optimizer, scheduler, num_epochs): loss_fn = nn.BCEWithLogitsLoss() loss_history = [] train_history = [] for epoch in range(num_epochs): model.train() dataset.generate_dataset() loss_accum = 0 correct_samples = 0 total_samples = 0 for i_step, (inp, out, lab) in enumerate(train_loader): optimizer.zero_grad() prediction = model(inp, out) # 标签转float匹配BCEWithLogitsLoss输入要求 loss_value = loss_fn(prediction, lab.float()) loss_value.backward() optimizer.step() # 计算准确率:正样本在每个样本组的第0位,取预测得分最高的位置判断 pred_pos = torch.argmax(prediction, dim=1) correct_samples += torch.sum(pred_pos == 0).item() total_samples += lab.shape[0] # 取item()转普通数值累加,避免持有计算图浪费显存 loss_accum += loss_value.item() scheduler.step() # i_step从0开始,总步数为i_step+1 ave_loss = loss_accum / (i_step + 1) train_accuracy = correct_samples / total_samples loss_history.append(ave_loss) train_history.append(train_accuracy) print(f"Epoch#: {epoch}, Average loss: {ave_loss:.6f}, Train accuracy: {train_accuracy:.6f}") return loss_history, train_history
额外优化建议
- 用
nn.Embedding替代nn.Linear+手动生成one-hot的实现,不需要构造巨大的one-hot张量,训练速度会提升数倍,本质上两者的计算结果是完全等价的。 - 注意保证模型、输入张量、损失函数在同一个设备(CPU/GPU)上,避免设备不匹配报错。
- 负采样训练时标签的构造要保证每个样本组的第0位是正样本(标签为1),其余位置是负样本(标签为0),和你现在的准确率判断逻辑对齐。
内容的提问来源于stack exchange,提问作者Даниил Гурин
相关产品推荐
相关产品推荐

