当前位置: 首页 > news >正文

深度学习进阶:自然语言处理|4.1.2 QA|grads 列表与省略号 [...] 详解

grads 列表、[0]、[…] 与 Embedding 梯度清零


1.self.grads[0]是什么?

classMatMul:def__init__(self,W):self.params=[W]self.grads=[np.zeros_like(W)]

paramsgrads是一一对应的列表:

self.params = [ W ] → params[0] 是 W self.grads = [ dW ] → grads[0] 是 W 的梯度槽

如果一层有两个参数,例如全连接层的Wb

self.params=[W,b]self.grads=[dW,db]

对应关系就是:

params[0] = W → grads[0] = dW params[1] = b → grads[1] = db

所以[0]没有特殊含义,只是“取第 0 个参数对应的梯度”。


2. 为什么用[...],而不是直接赋值?

核心区别:

grads[0] = dW → 让 grads[0] 指向一个新数组 grads[0][...] = dW → 把 dW 的值写进原数组,数组对象不变

优化器通常会提前拿到梯度数组的引用。如果你换掉数组,优化器还指向旧数组;如果你原地改数组,优化器能看到新梯度。

实际代码验证:普通赋值会让引用断开

importnumpyasnp grads=[np.zeros((3,2))]optimizer_grad_ref=grads[0]# 模拟优化器提前保存梯度引用old_id=id(grads[0])dW_new=np.array([[1.,1.],[0.,0.],[1.,1.]])grads[0]=dW_new# 普通赋值:换成新数组print("grads[0] 还是旧数组吗?",id(grads[0])==old_id)print("optimizer 还指向旧数组吗?",id(optimizer_grad_ref)==old_id)print("optimizer 看到的梯度:\n",optimizer_grad_ref)print("grads[0] 当前内容:\n",grads[0])

输出:

grads[0] 还是旧数组吗? False optimizer 还指向旧数组吗? True optimizer 看到的梯度: [[0. 0.] [0. 0.] [0. 0.]] grads[0] 当前内容: [[1. 1.] [0. 0.] [1. 1.]]

图解:

普通赋值后: optimizer_grad_ref ──→ 旧数组 [[0,0],[0,0],[0,0]] grads[0] ───────────→ 新数组 [[1,1],[0,0],[1,1]]

结论:grads[0]有新梯度,但优化器还看着旧的零数组。

实际代码验证:原地赋值不会让引用断开

importnumpyasnp grads=[np.zeros((3,2))]optimizer_grad_ref=grads[0]old_id=id(grads[0])dW_new=np.array([[1.,1.],[0.,0.],[1.,1.]])grads[0][...]=dW_new# 原地赋值:不换数组,只改内容print("grads[0] 还是旧数组吗?",id(grads[0])==old_id)print("optimizer 还指向旧数组吗?",id(optimizer_grad_ref)==old_id)print("optimizer 看到的梯度:\n",optimizer_grad_ref)print("grads[0] 当前内容:\n",grads[0])

输出:

grads[0] 还是旧数组吗? True optimizer 还指向旧数组吗? True optimizer 看到的梯度: [[1. 1.] [0. 0.] [1. 1.]] grads[0] 当前内容: [[1. 1.] [0. 0.] [1. 1.]]

图解:

原地赋值后: optimizer_grad_ref ─┐ ├──→ 同一个数组,内容变成 [[1,1],[0,0],[1,1]] grads[0] ───────────┘

结论:[...]的价值是保持数组对象不变,只更新里面的数据


3. Embedding 层为什么先dW[...] = 0

Embedding 的反向传播代码:

defbackward(self,dout):dW,=self.grads dW[...]=0dW[self.idx]=dout# 不太好的方式returnNone

dW[...] = 0清掉的是上一轮 mini-batch 留在 dW 里的旧梯度;当前梯度还在dout里,并没有被清掉。

设:

W.shape = (5, 3) dW.shape = (5, 3)

上一轮反向传播后,dW里可能残留:

词 ID dW 0 [0, 0, 0] 1 [1, 1, 1] ← 上一轮残留 2 [0, 0, 0] 3 [3, 3, 3] ← 上一轮残留 4 [0, 0, 0]

本轮只有词 ID2出现:

idx=[2]dout=[[9,9,9]]

如果不清零,直接写入本轮梯度:

错误结果: 词 ID dW 0 [0, 0, 0] 1 [1, 1, 1] ← 错:旧梯度还在 2 [9, 9, 9] ← 对:本轮梯度 3 [3, 3, 3] ← 错:旧梯度还在 4 [0, 0, 0]

正确流程是先清零,再写入:

dW[...] = 0 词 ID dW 0 [0, 0, 0] 1 [0, 0, 0] 2 [0, 0, 0] 3 [0, 0, 0] 4 [0, 0, 0] 然后 dW[idx] = dout 词 ID dW 0 [0, 0, 0] 1 [0, 0, 0] 2 [9, 9, 9] ← 本轮梯度 3 [0, 0, 0] 4 [0, 0, 0]

所以dW[...] = 0不是覆盖本轮梯度,而是先擦掉旧缓存。


4. 为什么还要创建和W一样大的dW

Embedding 层前向传播只取出W的几行:

out=W[idx]

所以反向传播时,理论上也只需要更新这几行:

W 是大矩阵: 词 ID W 0 [...] 1 [...] 2 [...] ← 本轮用到,需要更新 3 [...] 4 [...] ← 本轮用到,需要更新

因此更节省的表示方式其实是:

需要更新的行号:idx = [2, 4] 这些行的梯度: dout = [[...], [...]]

也就是说,不一定非要创建一个和W一样大的完整dW

完整 dW: 词 ID dW 0 [0, 0, 0] 1 [0, 0, 0] 2 [a, a, a] ← 有用 3 [0, 0, 0] 4 [b, b, b] ← 有用

其中大部分行都是 0,真正有用的只有idx对应的几行。

但书中这里仍然创建完整dW,是为了兼容已经实现好的优化器:

optimizer.update(params,grads)

优化器默认认为:

params[0] 是完整的 W grads[0] 也是和 W 形状相同的完整 dW

所以当前写法牺牲了一点效率,换来和已有训练框架的统一接口。

一句话:Embedding 的梯度本质上是稀疏的,只需要idx + dout;但为了适配通用 Optimizer,代码把它展开成完整的dW


5. 真正会覆盖梯度的问题:dW[self.idx] = dout

dW[...] = 0是必要的;真正“不太好”的是:

dW[self.idx]=dout

覆盖只会出现在一个条件下:同一次backward()里,idx中有重复的词 ID

例如一个 mini-batch 里取了 3 个词:

idx = [2, 2, 4]

含义是:

第 1 个样本用了词 ID 2 第 2 个样本也用了词 ID 2 ← 重复 第 3 个样本用了词 ID 4

这种情况很常见,比如一句话里同一个词出现多次,或者一个 batch 的不同句子都出现了同一个词。

如果idx没有重复,例如:

idx = [1, 2, 4]

那么dW[self.idx] = dout不会发生覆盖,因为每个dout都写入不同的行。

实际代码验证:重复词 ID 才会覆盖

importnumpyasnp dW=np.zeros((5,3))idx=np.array([2,2,4])dout=np.array([[1.,1.,1.],# 第一次给词 ID 2 的梯度[2.,2.,2.],# 第二次给词 ID 2 的梯度[4.,4.,4.]])# 给词 ID 4 的梯度dW[idx]=doutprint(dW)

输出:

[[0. 0. 0.] [0. 0. 0.] [2. 2. 2.] [0. 0. 0.] [4. 4. 4.]]

词 ID2出现了两次:

第一次:dW[2] = [1, 1, 1] 第二次:dW[2] = [2, 2, 2] ← 覆盖第一次

但正确结果应该是:

dW[2] = [1, 1, 1] + [2, 2, 2] = [3, 3, 3]

正确写法:np.add.at

importnumpyasnp dW=np.zeros((5,3))idx=np.array([2,2,4])dout=np.array([[1.,1.,1.],[2.,2.,2.],[4.,4.,4.]])np.add.at(dW,idx,dout)print(dW)

输出:

[[0. 0. 0.] [0. 0. 0.] [3. 3. 3.] [0. 0. 0.] [4. 4. 4.]]

图解:

idx = [2, 2, 4] [1,1,1] ─┐ ├──→ dW[2] = [3,3,3] [2,2,2] ─┘ [4,4,4] ───→ dW[4] = [4,4,4]

6. 为什么重复词梯度是相加,不是求平均?

假设词 ID2是“猫”:

句子:猫 喜欢 猫 idx = [2, 5, 2]

Embedding 前向传播中,两个“猫”都使用同一行参数W[2]

第 1 个“猫” → W[2] 第 3 个“猫” → W[2]

如果反向传播传回来:

第 1 个“猫”的梯度:[1, 1, 1] 第 3 个“猫”的梯度:[2, 2, 2]

那么W[2]收到的总梯度是:

W[2] 的梯度 = [1, 1, 1] + [2, 2, 2] = [3, 3, 3]

原因很简单:同一行参数W[2]被用了两次,就通过两个位置影响 loss;两个位置的影响要合并,合并方式是相加。

如果求平均:

([1, 1, 1] + [2, 2, 2]) / 2 = [1.5, 1.5, 1.5]

这不是默认反向传播规则,而是额外的“按出现次数缩放”策略。

什么时候会平均?当模型公式里本来就写了平均,例如:

句子向量 = (猫 + 喜欢 + 猫) / 3

这时/3会进入传回 Embedding 层的dout,Embedding 层仍然只负责把同一个词 ID 的多份梯度相加。

一句话:重复词梯度默认相加;如果要按词频平均,应该由模型公式、loss 计算或优化策略决定,而不是在np.add.at这里自动除以次数。


7. 核心结论

Embedding 层更稳妥的写法是:

defbackward(self,dout):dW,=self.grads dW[...]=0np.add.at(dW,self.idx,dout)returnNone

对应三件事:

dW, = self.grads → 取出 W 对应的梯度槽 dW[...] = 0 → 原地清空旧梯度,数组对象不变 np.add.at(dW, self.idx, dout) → 把本轮梯度累加到对应词 ID,重复词不会被覆盖

一句话:[...]解决“引用不断开/旧梯度清零”的问题;np.add.at解决“重复词梯度累加”的问题。

http://www.jsqmd.com/news/885973/

相关文章:

  • 如何快速实现Windows游戏控制器虚拟化:ViGEmBus完整使用指南
  • 夏季血压“正常”了,能停药吗?别让好心办坏事
  • LongLLMLingua 核心原理:对比困惑度实现提示词压缩
  • 航空发动机叶片三维扫描-诺斯顿
  • Flory-Huggins参数与机器学习结合:聚合物耐化学性预测模型构建与应用
  • 告别MQTT.fx!用STM32+ESP8266直连新版OneNET,手把手教你从零配置JSON数据上传
  • ZMJS,把 JavaScript 解释器放进 SAP ABAP 应用服务器之后,很多扩展思路会变得不一样
  • 39 - Go 信号捕获与处理:优雅退出、进程控制
  • 告别AWCC臃肿:AlienFX Tools终极轻量级控制方案深度评测
  • 谈美---朱光潜前20页
  • 15个靶场如何构建渗透测试能力成长路径
  • 【Linux:文件】Linux 动静态库详解:动态链接与动态库加载深度解析
  • 如何突破百度网盘下载限制:Python解析工具完整指南
  • Ubuntu经常安装软件
  • 【安全加固】Claude Code v2.1.149 发布:堵截 PowerShell 越权路径漏洞,账单明细精准透视
  • Redis三大缓存异常问题
  • 机器学习势函数在辐射损伤模拟中的性能评估与优化策略
  • 白嫖$100直充券,3款Search MCP让你的AI Agent更聪明!
  • 为什么这个免费工具能快速修复你的重要视频文件:完整实战指南
  • 相贯曲线自动焊接轨迹规划与轨迹控制技术【附代码】
  • 2026 太原装修公司十佳榜单重磅发布!口碑实力双优,装修选对不踩坑 - 资讯快报
  • 5分钟学会BlenderKit:让你在Blender里拥有一个永不枯竭的创意资源库
  • 2026广州增城注册公司怎么选?本地老创业者实测5家靠谱财税,避坑不踩雷 - 资讯快报
  • [Dify实战] 从 Docker Compose 起步,怎么先搭出一个可验证的 Dify 本地环境?
  • 小白友好:OpenClaw Windows 一键部署教程(含安装包)
  • 【常规维护】Claude Code v2.1.150 发布:聚焦内部基础设施演进
  • 调试手记:通过正点原子飞控源码理解PID串级调参与内外环频率匹配问题
  • 2026年北京朝阳搬家公司多维度精选推荐四家正规公司 - 余小铁
  • 2026广州高企认定机构哪家靠谱?主流代办服务商场景适配测评清单 - 资讯快报
  • DMA Buffer Cache同步的批处理优化及高通平台的实践