<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>梯度消失 on Zewang's Blog</title><link>https://zewang0217.github.io/tags/%E6%A2%AF%E5%BA%A6%E6%B6%88%E5%A4%B1/</link><description>Recent content in 梯度消失 on Zewang's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Sat, 18 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zewang0217.github.io/tags/%E6%A2%AF%E5%BA%A6%E6%B6%88%E5%A4%B1/index.xml" rel="self" type="application/rss+xml"/><item><title>makemore Part 3：激活函数、梯度消失与 Batch Normalization</title><link>https://zewang0217.github.io/p/makemore-part3-activations-batchnorm/</link><pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate><guid>https://zewang0217.github.io/p/makemore-part3-activations-batchnorm/</guid><description>&lt;h1 id="makemore-part-3激活函数梯度消失与-batch-normalization"&gt;makemore Part 3：激活函数、梯度消失与 Batch Normalization
&lt;/h1&gt;
 &lt;blockquote&gt;
 &lt;p&gt;资料源：Andrej Karpathy - Building makemore Part 3: Activations &amp;amp; Gradients, BatchNorm（2026-07-18）
前篇：&lt;a class="link" href="https://zewang0217.github.io/post/makemore-mlp/" &gt;makemore Part 2：MLP 语言模型&lt;/a&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="part-2-的遗留问题"&gt;Part 2 的遗留问题
&lt;/h2&gt;&lt;p&gt;Part 2 我们按照 Bengio 2003 的论文，实现了一个 MLP 字符级语言模型——输入几个历史字符的 embedding，拼接后过一层隐藏层（tanh），最后用 softmax 预测下一个字符。&lt;/p&gt;
&lt;p&gt;当时我们初始化模型后直接开始训练，损失确实在下降，模型也确实在工作。但我们&lt;strong&gt;没有检查的一件事&lt;/strong&gt;是：网络各层的激活值和梯度分布是什么样的？它们健康吗？&lt;/p&gt;
&lt;p&gt;答案是：&lt;strong&gt;大概率不健康&lt;/strong&gt;。这就是 Part 3 要解决的问题。&lt;/p&gt;
&lt;p&gt;回顾一下 Part 2 的 MLP 架构：&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="144px" data-flex-grow="60" height="3300" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://zewang0217.github.io/p/makemore-part3-activations-batchnorm/mlp-architecture.png" srcset="https://zewang0217.github.io/p/makemore-part3-activations-batchnorm/mlp-architecture_hu_601e899fd11c25b7.png 800w, https://zewang0217.github.io/p/makemore-part3-activations-batchnorm/mlp-architecture_hu_e0e438d297cb1995.png 1600w, https://zewang0217.github.io/p/makemore-part3-activations-batchnorm/mlp-architecture.png 1980w" width="1980"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图：makemore MLP 架构。&lt;/strong&gt; 输入 3 个字符 → Embedding 表查稠密向量 → 拼接 → 隐藏层（线性变换 + tanh）→ 输出层（线性变换 + softmax）→ 交叉熵损失。&lt;/p&gt;
&lt;h2 id="一个理想初始化的标准"&gt;一个理想初始化的标准
&lt;/h2&gt;&lt;p&gt;当我们初始化一个神经网络时，我们希望&lt;strong&gt;输出层的概率分布在训练初期接近 uniform&lt;/strong&gt;。对于字符级语言模型（27 个字符），这意味着 softmax 输出的每个类别的概率应该在 1/27 ≈ 3.7% 左右，对应的交叉熵损失应该是 &lt;code&gt;-ln(1/27) ≈ 3.3&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果初始损失远低于 3.3，说明模型一开始就&amp;quot;太自信&amp;quot;了，这通常意味着某些 logits 异常大；如果远高于 3.3，说明输出分布太分散。&lt;/p&gt;
&lt;p&gt;但保持初始输出的均匀分布只是第一步。真正的问题是：&lt;strong&gt;随着网络加深，激活值的分布会逐层漂移&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="tanh-饱和与梯度消失"&gt;Tanh 饱和与梯度消失
&lt;/h2&gt;&lt;p&gt;Tanh 函数的值域是 (-1, 1)，它有一个关键特性：&lt;strong&gt;当输入绝对值很大时（&amp;gt;2 或 &amp;lt;-2），tanh 进入饱和区，输出几乎不变，梯度趋近于 0&lt;/strong&gt;。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tanh(x) 的梯度 = 1 - tanh(x)²
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;当 x=0 时，梯度 = 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;当 x=2 时，梯度 ≈ 0.07
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;当 x=3 时，梯度 ≈ 0.01
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;当 x=5 时，梯度 ≈ 0.0002
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;问题出在&lt;strong&gt;多层累积&lt;/strong&gt;上。假设你有一层线性变换 &lt;code&gt;h = Wx + b&lt;/code&gt;，然后过 tanh。如果 W 的初始值太大，h 的方差会很大，导致 tanh 输入进入饱和区。这一层退化为&amp;quot;要么 1 要么 -1&amp;quot;的开关，梯度几乎为零。&lt;/p&gt;
&lt;p&gt;更糟的是，梯度会随着层数&lt;strong&gt;指数级衰减&lt;/strong&gt;：每一层把梯度缩小一点，N 层之后就消失了。这就是&lt;strong&gt;梯度消失&lt;/strong&gt;——深层网络的经典问题。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;梯度消失不是&amp;quot;训练变慢&amp;quot;，而是&amp;quot;底层几乎不再学习&amp;quot;。前面几层的权重基本不动，只有最后几层在变化。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="权重初始化的改进"&gt;权重初始化的改进
&lt;/h3&gt;&lt;p&gt;一个直接的思路：&lt;strong&gt;控制权重初始化的大小，让各层激活值的方差保持稳定&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Kaiming He 等人发现：对于使用 ReLU 的网络，权重应该从 &lt;code&gt;N(0, 2/fan_in)&lt;/code&gt; 初始化；对于 tanh/Sigmoid，Xavier/Glorot 初始化建议用 &lt;code&gt;N(0, 1/fan_in)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为什么是 &lt;code&gt;1/fan_in&lt;/code&gt;？考虑一个简单的线性层 &lt;code&gt;y = Wx&lt;/code&gt;，其中 x 的均值为 0，方差为 1。W 有 fan_in 个输入。y 的方差大约是 &lt;code&gt;fan_in * Var(W)&lt;/code&gt;。要 y 的方差保持为 1，需要 &lt;code&gt;Var(W) = 1/fan_in&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;用 Kaiming 初始化后，各层激活值的分布确实更稳定了——tanh 的输入不会一上来就飞到饱和区，梯度也能正常流通。&lt;/p&gt;
&lt;p&gt;但 Kaiming 初始化不是银弹。它只是一个&lt;strong&gt;静态的修补&lt;/strong&gt;——当你确定网络结构、深度、激活函数后，算一个合适的初始值。一旦网络深度变化、层类型变化（Linear → CNN → Attention），你得重新算。&lt;/p&gt;
&lt;p&gt;有没有更动态、更通用的方案？&lt;/p&gt;
&lt;h2 id="batch-normalization2015"&gt;Batch Normalization（2015）
&lt;/h2&gt;&lt;p&gt;Batch Normalization（BN）是 Google 在 2015 年提出的，一篇论文彻底改变了深层网络的训练方式。&lt;/p&gt;
&lt;h3 id="bn-的核心思想"&gt;BN 的核心思想
&lt;/h3&gt;&lt;p&gt;每一层之前，显式地把激活值拉回到&lt;strong&gt;均值为 0、方差为 1 的标准正态分布&lt;/strong&gt;，然后再送给下一层。&lt;/p&gt;
&lt;p&gt;具体来说，对于一个 batch 的激活值 &lt;code&gt;x&lt;/code&gt;（维度是 &lt;code&gt;batch_size × features&lt;/code&gt;）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;μ&lt;/span&gt;&lt;span class="n"&gt;_B&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# batch 均值&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;σ²&lt;/span&gt;&lt;span class="n"&gt;_B&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# batch 方差&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;x̂&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;μ&lt;/span&gt;&lt;span class="n"&gt;_B&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="err"&gt;√&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;σ²&lt;/span&gt;&lt;span class="n"&gt;_B&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;ε&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 归一化&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;γ&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x̂&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;β&lt;/span&gt; &lt;span class="c1"&gt;# 缩放 + 平移（可学习）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;关键设计：&lt;strong&gt;归一化之后又加了一个可学习的缩放和平移&lt;/strong&gt;。为什么？&lt;/p&gt;
&lt;p&gt;如果不加这一层，强制输出为 N(0,1) 可能限制了网络的表达能力——网络可能想学到一个非标准正态的分布。&lt;code&gt;γ&lt;/code&gt; 和 &lt;code&gt;β&lt;/code&gt; 给了网络&amp;quot;反悔&amp;quot;的能力：如果网络发现 N(0,1) 不好，可以学回原来的分布。&lt;/p&gt;
&lt;p&gt;实际上，&lt;code&gt;γ&lt;/code&gt; 初始化为 1，&lt;code&gt;β&lt;/code&gt; 初始化为 0，所以 BN 一开始做的是纯归一化，然后网络在训练过程中慢慢调整 &lt;code&gt;γ&lt;/code&gt; 和 &lt;code&gt;β&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="bn-解决了什么问题"&gt;BN 解决了什么问题？
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;梯度消失&lt;/strong&gt;：tanh 的输入被拉到 0 附近，永远在 tanh 的线性区域工作（梯度最大），不会再进入饱和区。这使得我们可以训练更深的网络，&lt;strong&gt;而不需要小心翼翼地调初始化&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对初始化不敏感&lt;/strong&gt;：有了 BN，用什么样的初始化差别不大。BN 会自动修正激活值的分布。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;允许更大的学习率&lt;/strong&gt;：激活值分布稳定 → 梯度分布稳定 → 可以用更大的学习率加速训练。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="bn-的正则化效果副效应"&gt;BN 的正则化效果（副效应）
&lt;/h3&gt;&lt;p&gt;这是一个&amp;quot;意外收获&amp;quot;：&lt;strong&gt;BN 有正则化效果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;原因：每个样本在一个 batch 中的归一化依赖于同一 batch 中的其他样本。具体用哪些样本来算均值/方差，每个 epoch 都不同（因为 shuffle）。这相当于在训练过程中引入了噪声——同一个样本每次经过 BN 时的&amp;quot;语境&amp;quot;不同。&lt;/p&gt;
&lt;p&gt;这种噪声让网络不容易过拟合到特定样本的精确特征。类似于 Dropout，但来源不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dropout&lt;/strong&gt;：显式随机丢弃神经元&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BN 的正则化&lt;/strong&gt;：隐式通过 batch 统计量的随机性引入&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也是为什么很多任务中，去掉 BN 后需要加 Dropout 或其他正则化手段才能维持同样的泛化性能。&lt;/p&gt;
&lt;h3 id="bn-的耦合问题"&gt;BN 的耦合问题
&lt;/h3&gt;&lt;p&gt;BN 有一个不易察觉的代价：&lt;strong&gt;样本耦合&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;没有 BN 时，每个样本独立处理，batch 只是效率优化。有了 BN，算 mean/std 依赖整个 batch——样本 A 的输出耦合了样本 B、C、D。同一样本在不同 batch 里输出不同（因为&amp;quot;同桌&amp;quot;不同），梯度也变得耦合。&lt;/p&gt;
&lt;p&gt;这就是为什么后来的人更倾向用 Layer Normalization（不做 batch 维度的归一化，只做特征维度的归一化，每个样本独立）。&lt;/p&gt;
&lt;h3 id="训练-vs-推理bn-的两副面孔"&gt;训练 vs 推理：BN 的两副面孔
&lt;/h3&gt;&lt;p&gt;训练时，BN 使用&lt;strong&gt;当前 batch 的均值/方差&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;推理时，没有 batch 的概念（可能一次只来一个样本），不能再用 batch 统计量。所以 BN 在训练过程中维护&lt;strong&gt;running mean&lt;/strong&gt; 和 &lt;strong&gt;running variance&lt;/strong&gt;——对所有 batch 的均值/方差做指数移动平均（EMA）。&lt;/p&gt;
&lt;p&gt;推理时直接用 running 统计量，不再依赖 batch 大小。类比：训练时每次考试按这班同学的成绩来评分，推理时用过去一学期的平均分标准评分。&lt;/p&gt;
&lt;h2 id="训练过程的健康检查"&gt;训练过程的健康检查
&lt;/h2&gt;&lt;p&gt;Karpathy 在代码中做了大量的&lt;strong&gt;可视化&lt;/strong&gt;——这是理解网络是否在健康训练的核心手段：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;前向传播的激活值直方图&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对每一层，在 forward 之后画出激活值的分布&lt;/li&gt;
&lt;li&gt;健康的分布（钟形）：接近 N(0,1)，没有大片饱和区域&lt;/li&gt;
&lt;li&gt;不健康的分布：集中在 ±1 附近（tanh 饱和），或者方差发散&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;反向传播的梯度直方图&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;各层梯度应该在相似的尺度上&lt;/li&gt;
&lt;li&gt;如果第一层的梯度比最后一层小 1000 倍，梯度消失了&lt;/li&gt;
&lt;li&gt;如果第一层的梯度比最后一层大 1000 倍，梯度爆炸了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;调整 - 直方图变化 - 诊断的链条&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发现 W 太大 → 所有层激活值直方图两头高中间空（饱和）→ 梯度≈0&lt;/li&gt;
&lt;li&gt;把 W 乘 0.1 → 激活值缩回中间 → 钟形恢复 → 梯度开始流通&lt;/li&gt;
&lt;li&gt;加 BN → 所有层自动稳定 → 不需要手动试&lt;/li&gt;
&lt;li&gt;看梯度直方图 → 检查各层梯度是否同数量级&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;手动调 W → 看直方图 → 再调 → 再看&amp;hellip; 这个试错循环就是 BN 和 Kaiming 初始化要替代的。&lt;/p&gt;
&lt;h3 id="一个令人困惑的发现"&gt;一个令人困惑的发现
&lt;/h3&gt;&lt;p&gt;在 MLP 中，&lt;strong&gt;最后一层（输出层）的梯度往往比前面的隐藏层大几个数量级&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;原因：输出层直接连接着 softmax + 交叉熵损失。损失函数对输出层 logits 的梯度就是 &lt;code&gt;(softmax - target)&lt;/code&gt;，这是一个直接的误差信号。而前面层的梯度需要经过多层 tanh 的链式法则，每一层放大或缩小梯度，最终传到前面的梯度被大幅衰减。&lt;/p&gt;
&lt;p&gt;这说明：即使有了 BN，输出层和隐藏层之间的梯度尺度仍然不匹配。需要&lt;strong&gt;对最后一层用更小的学习率&lt;/strong&gt;，或者给不同的层设置不同的学习率。&lt;/p&gt;
&lt;h2 id="从-bn-到更多归一化层"&gt;从 BN 到更多归一化层
&lt;/h2&gt;&lt;p&gt;BN 工作得很好，但它有一个局限：&lt;strong&gt;依赖 batch 维度&lt;/strong&gt;。当 batch size 很小（比如 2 或 4）时，batch 统计量不稳定；当 batch size = 1 时（在线学习），BN 完全无法工作。&lt;/p&gt;
&lt;p&gt;这引出了一系列后续工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Layer Normalization（LN）&lt;/strong&gt;：对每个样本的所有特征做归一化，不依赖 batch。Transformer 用的就是 LN&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instance Normalization（IN）&lt;/strong&gt;：对每个样本的每个通道做归一化。风格迁移常用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Group Normalization（GN）&lt;/strong&gt;：介于 LN 和 IN 之间，把特征分成几组做归一化。适用于 batch size 小的场景&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;一个有趣的历史事实：LN 比 BN 早几个月被提出，但 BN 的影响力远大于 LN，因为 BN 的论文把故事讲得更好——它抓住了&amp;quot;internal covariate shift&amp;quot;这个痛点。直到 Transformer 出现，LN 才重新被重视。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="小结"&gt;小结
&lt;/h2&gt;&lt;p&gt;Part 3 的核心教训是：&lt;strong&gt;不要假设你的网络在健康地训练&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;三个层层递进的方案：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;方案&lt;/th&gt;
 &lt;th&gt;思路&lt;/th&gt;
 &lt;th&gt;局限&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;手动调初始化&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;缩小 W 的 std，让 tanh 落在线性区&lt;/td&gt;
 &lt;td&gt;深度/层类型变了得重新试&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Kaiming/Xavier 初始化&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;根据 fan_in 算合适的 std&lt;/td&gt;
 &lt;td&gt;静态的，不动态适应&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Batch Normalization&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;每层动态归一化 + 可学习缩放平移&lt;/td&gt;
 &lt;td&gt;依赖 batch，耦合样本&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;本质：&lt;/strong&gt; 激活值和梯度的统计特性决定网络能不能正常训练。调初始化、做 BN、做 ResNet，归根结底都在做同一件事——让激活值分布稳定，让梯度顺畅流通。&lt;/p&gt;
&lt;p&gt;Batch Normalization 是 makemore 系列的一个重要转折点——解决了&amp;quot;训练深不深得下去&amp;quot;的问题，为后面 Part 4（Backprop Ninja）和 Part 5（WaveNet）的复杂网络铺平了道路。&lt;/p&gt;
&lt;h2 id="思考与延伸"&gt;思考与延伸
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;理解激活函数的关键&lt;/strong&gt;：tanh 把直线掰弯，让神经元从&amp;quot;音量旋钮&amp;quot;变成&amp;quot;软开关&amp;quot;。没有激活函数，叠多少层都等价于一层。&lt;/p&gt;

 &lt;/blockquote&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;梯度消失的直觉&lt;/strong&gt;：不是&amp;quot;训练变慢&amp;quot;，是&amp;quot;前面层在空转&amp;quot;。梯度传不到的地方，参数不更新，深层网络的有效深度比层数浅得多。&lt;/p&gt;

 &lt;/blockquote&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;BN 的&amp;quot;反悔权&amp;quot;设计很巧妙&lt;/strong&gt;：先强制归一化到 N(0,1)，再用 γ 和 β 允许网络学回去。初始化=1 和 0，所以一开始做纯归一化，需要时再偏离。&lt;/p&gt;

 &lt;/blockquote&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;BN 的耦合是它最不直观的代价&lt;/strong&gt;：样本 A 的输出取决于 batch 里的 B、C、D——&amp;ldquo;同桌&amp;quot;不同，输出不同。这让调试变得复杂，也是后来 LN 被重用的原因。&lt;/p&gt;

 &lt;/blockquote&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;直方图可视化是核心调试手段&lt;/strong&gt;：一层层看激活值和梯度的分布，而不是只看最终 loss。各层梯度在同一数量级 → 健康；越靠近输入层越小 → 梯度消失。&lt;/p&gt;

 &lt;/blockquote&gt;</description></item></channel></rss>