
1. 从一行代码说起nn.Linear 到底做了一次什么运算先看一段再常见不过的代码import torch import torch.nn as nn linear nn.Linear(4, 3) x torch.randn(2, 4) y linear(x) print(y.shape) # torch.Size([2, 3])很多初学者一眼扫过去知道这是“全连接层”知道输入维度 4、输出维度 3数据进去转一圈变成另一个形状。但如果追问一句这层网络在数学上到底对输入的数据做了什么不少人就开始含糊了。这恰恰是理解 PyTorch 基础框架的关键分水岭。nn.Linear是 PyTorch 里最基础、最常用的网络层之一它不只是“把维度变一下”的黑盒子而是一个完整的数学变换一个线性映射加上一个平移。从数学角度拆清楚它的来龙去脉后面理解卷积、RNN、Transformer 里的 QKV 投影都会轻松很多。这篇文章就从纯数学角度把nn.Linear从里到外拆一遍包含它的矩阵运算形式为什么长这样、权重和偏置分别扮演什么几何角色、反向传播的梯度怎么推导、默认初始化背后的方差逻辑以及实际使用中几个最容易踩的坑。适合正在学 PyTorch 基础框架的初学者也适合想要把“会调 API”升级成“懂原理”的开发者。2. Linear 的数学本质一个线性映射的完整表达2.1 先看公式y xW^T b 而不是 y WxPyTorch 官方的nn.Linear文档里写着它对输入做如下变换$$ y x A^T b $$注意这里的写法是 $x A^T$不是 $Ax$。这是新手最容易困惑的地方。原因在于 PyTorch 约定输入 $x$ 的形状是 $(batch_size, in_features)$也就是行向量在后的布局。为了跟这种布局配套权重矩阵的形态也要反过来设计。定义一个nn.Linear(in_features, out_features)时内部的权重矩阵weight的形状是 $(out_features, in_features)$。每行对应一个输出神经元每列对应一个输入特征。对输入 $x$ 做变换时实际上是把 $x$ 跟weight的每一行做内积得到一个标量所有行算完就拼出输出向量。写成矩阵形式就是$$ y (W^T)^T x^T $$绕来绕去容易晕直接记结论weight的形状是 $(out, in)$前向传播计算的是x weight.T也就是输入矩阵和权重矩阵的转置做矩阵乘法最后加上偏置bias形状是 $(out,)$。代码验证一下import torch import torch.nn as nn torch.manual_seed(42) linear nn.Linear(4, 3) x torch.randn(2, 4) y1 linear(x) # 手动实现相同的运算 y2 x linear.weight.T linear.bias print(torch.allclose(y1, y2)) # Trueallclose返回True说明手动算的结果和nn.Linear内部运算完全一致。很多人在面试或者笔试时被问到“nn.Linear 的公式是什么”如果只说 y Wx b严格来说不算错但跟 PyTorch 的实际实现有一层转置的差别。弄清楚这一层后面看源码、改自定义层都会顺手很多。2.2 权重的几何含义缩放和旋转偏置的含义平移从线性代数的角度看一个不带偏置的线性变换 $y xW^T$本质上是把输入向量从一个向量空间映射到另一个向量空间。在这个过程中一个向量可能被拉伸、压缩、旋转或者投影但有一个性质永远不变直线还是直线平行关系保持不变。这就是“线性”两个字的数学含义。把输入从 $d_{in}$ 维映射到 $d_{out}$ 维如果 $d_{in} d_{out}$相当于把高维空间的信息“压缩”到低维空间必然丢失一部分信息反过来如果 $d_{in} d_{out}$则是在向高维空间做嵌入原始数据获得了更丰富的表达能力。这也是为什么 Transformer 里的 FFN前馈网络先把维度放大好几倍再缩回来——先扩展表达空间再压缩提炼特征。偏置 $b$ 的角色更像是一次“整体挪动”。在二维平面上$y Wx$ 描述的是过原点的直线或者平面而加上 $b$ 之后这个平面就可以脱离原点在空间中任意平移。没有偏置的线性层输出永远过原点这在很多场景下是一个不必要的限制。打个比方$W$ 决定了一个方向上的“斜率”$b$ 决定了“截距”。3. 从一条数据到一批数据nn.Linear 的批量矩阵运算3.1 单样本视角向量内积的堆叠先看单个输入样本形状为 $(in_features,)$。经过线性层时实际发生的是$$ y_j \sum_{i1}^{in_features} W_{j,i} \cdot x_i b_j $$第 $j$ 个输出神经元的值等于输入向量的每个分量跟权重矩阵第 $j$ 行对应分量相乘后求和最后加上该神经元的偏置。这个“相乘后求和”就是向量内积。也就是说nn.Linear的每一个输出神经元本质上是在对输入向量做一次“加权求和”。这也解释了为什么线性层也被叫做“全连接层”输出端的每个神经元都跟输入端的每一个分量有连接每个连接上有一个可学习的权重。假设输入维度是 784比如一张展平的 MNIST 图像输出维度是 10那么这一层就有 $784 \times 10 7840$ 个权重参数加上 10 个偏置总共 7850 个可学习参数。理解了这个数字怎么来的就不会再对“模型有多少参数”这个问题感到无从下手。3.2 批量视角矩阵乘法的自然延伸实际训练时不会一次只喂一条数据而是把多条数据拼成一个 batch形状为 $(batch_size, in_features)$。此时nn.Linear的运算变成$$ Y X W^T b $$其中 $X$ 的形状是 $(batch, in)$$W^T$ 的形状是 $(in, out)$矩阵乘法得到 $(batch, out)$ 的输出偏置 $b$ 通过广播机制加到每一行上。这就是矩阵乘法的强大之处不管 batch 里有 1 条数据还是 256 条数据运算形式完全不变只是多了一个“行”的维度。底层的 BLAS 库会对这种密集矩阵乘法做高度优化这也是为什么深度学习框架都倾向于把计算组织成矩阵乘法的形式而不是写一层 for 循环逐个样本逐个神经元地算——那样效率差几个数量级。有个细节值得注意偏置 $b$ 在批量运算中是对每一行都加上同一个向量这看起来像是“偷懒”的写法但恰恰是这个共享偏置的设计让模型的参数数量不会随着 batch 增大而膨胀。每个输出神经元只有一个偏置跟输入多少条数据无关。3.3 维度检查in_features 和 out_features 该怎么定nn.Linear的两个核心参数in_features和out_features分别对应输入向量的维度和输出向量的维度。这个映射关系决定了网络的整体结构也是模型设计中最常需要动脑的地方。常见的设定方式大概分三类连续压缩比如把 784 维的图像特征逐步压缩到 256、128、10用于分类任务特征扩展比如把 512 维的语义向量先映射到 2048 维再用非线性激活处理提升表达空间维度不变比如 Transformer 里的自注意力投影输入输出维度保持一致方便残差连接。一个最常见的报错是输入维度不匹配RuntimeError: mat1 and mat2 shapes cannot be multiplied (2x3 and 4x5)意思是传入的数据最后一维是 3但模型期望的是 4。遇到这种错误别急着改代码先检查输入数据经过前面的层之后最后一维到底变成了多少再决定怎么调整。4. 反向传播的数学推导Linear 的梯度从哪来4.1 对权重 W 的梯度训练神经网络靠的是反向传播而反向传播的基础是链式法则。对一个线性层来说假设损失函数为 $L$前向传播是$$ y x W^T b $$反向传播时上游传回来的梯度是 $\frac{\partial L}{\partial y}$形状跟 $y$ 一致。基于链式法则对权重矩阵的梯度为$$ \frac{\partial L}{\partial W} \left(\frac{\partial L}{\partial y}\right)^T x $$这里也牵扯转置的细节$\frac{\partial L}{\partial y}$ 的形状是 $(batch, out)$$x$ 的形状是 $(batch, in)$要让两个矩阵乘起来得到 $(out, in)$ 的梯度形状就需要把前者转置一下。PyTorch 的autograd引擎会自动完成这套计算但理解推导过程对排查梯度问题很重要。比如你想手动实现一个自定义层或者在调试时想确认梯度是否正确就可以用torch.autograd.grad手动算一次梯度跟前向传播的数值做对比验证。4.2 对偏置 b 的梯度偏置的梯度更简单。因为偏置是逐元素相加梯度就是上游梯度在 batch 维度上的求和$$ \frac{\partial L}{\partial b} \sum_{i1}^{batch_size} \frac{\partial L}{\partial y_i} $$也就是说把上游梯度的每一行加起来得到一个 $(out,)$ 的向量。这也是为什么偏置的梯度往往比权重的梯度小一个数量级——它的更新只依赖上游梯度的“总和”而权重更新依赖的是上游梯度与输入的内积。4.3 对输入 x 的梯度为什么 Linear 可以级联堆叠线性层对输入的梯度是$$ \frac{\partial L}{\partial x} \frac{\partial L}{\partial y} W $$注意这里不需要转置W因为 $\frac{\partial L}{\partial y}$ 形状是 $(batch, out)$$W$ 形状是 $(out, in)$直接相乘得到 $(batch, in)$。这保证了梯度能够穿过线性层继续向前传播让多层网络的反向传播成为可能。有一个很实用的推论线性层的梯度计算不涉及任何非线性操作所以它不会导致梯度消失或梯度爆炸。真正引发梯度问题的是它后面的激活函数比如 sigmoid 在饱和区的导数趋近于 0或者深度堆叠导致的连乘效应。明白了这一点在排查“梯度消失”问题时就能更快锁定真正的病灶。5. 初始化背后的数学PyTorch 默认帮我们做了什么5.1 kaiming_uniform_ 与均匀分布创建一个nn.Linear(4, 3)之后它的权重和偏置不会随机乱来PyTorch 会自动用特定规则初始化。官方对Linear的默认初始化方式是kaiming_uniform_(self.weight, amath.sqrt(5))这个初始化方法来自于何恺明团队提出的 Kaiming 初始化专门针对 ReLU 类激活函数设计。它的核心思想是让每一层输出的方差保持在一个合理范围避免信号在深层网络中越传越小或者越放越大。具体到nn.Linear的实现权重参数 $W$ 会在区间 $[-\sqrt{k}, \sqrt{k}]$ 内均匀采样其中$$ k \frac{1}{in_features} $$这里稍作解释为了让 $y xW^T b$ 的输出方差与输入方差保持一致在无激活函数的情况下需要权重的方差约为 $1 / in_features$。均匀分布 $U[-a, a]$ 的方差是 $a^2 / 3$所以令 $a^2 / 3 1 / in_features$解出 $a \sqrt{3 / in_features}$再乘上 Kaiming 初始化里的增益系数 $\sqrt{2/(1a^2)}$其中 $a$ 是 ReLU 的负斜率化简后就有了 PyTorch 源码里的那个公式。不深究每一步数学化简的话记住一句核心结论默认初始化已经为大多数场景调好了方差尺度不要轻易改除非你明确知道自己在做什么。5.2 bias 的初始化为什么不直接用零nn.Linear的偏置默认也是用均匀分布初始化的采样区间是$$ [-\frac{1}{\sqrt{in_features}}, \frac{1}{\sqrt{in_features}}] $$很多框架包括某些自定义实现习惯把偏置初始化为 0因为从数学上讲即使全部初始化为 0只要权重不为 0模型依然能正常训练。PyTorch 选择非零初始化的原因更多是为了配合 Kaiming 初始化里对输出方差的推导——偏置也参与前向传播如果偏置初始化为 0权重分担的方差会略大虽然影响很小但不完美。实操中如果遇到模型训练初期 loss 不下降的情况检查一下初始化是不是被改过是个不错的排查方向。5.3 什么时候需要手动改初始化默认初始化适用于大部分情况但也有例外使用了 sigmoid/tanh 激活这时候适合用 Xavier 初始化因为 Kaiming 是针对 ReLU 类设计的做了预训练迁移加载预训练权重时PyTorch 会直接覆盖随机初始化这种情况下默认初始化就无关紧要了最后一层输出层某些场景下希望初始输出分布更均匀可以手动把最后一层权重调小一点比如乘以 0.01这在强化学习的策略网络里很常见。6. 从 Linear 出发维度变换的实际场景与应用逻辑6.1 分类任务从特征空间到类别分数最典型的使用场景是分类任务的输出层。假设有一个图像分类模型特征提取部分输出一个 512 维的向量类别有 10 个那么最后一层就是nn.Linear(512, 10)。这一层的输出通常被叫做 logits也就是“未归一化的对数概率”。数学上它做的是把 512 维的特征向量分别跟 10 个类别的权重向量做内积得到一个 10 维的分数向量。哪个类别的分数高模型就更倾向于认为输入属于哪个类别。注意这里的输出不是概率如果要转成概率后面需要接softmax$$ p_i \frac{e^{y_i}}{\sum_j e^{y_j}} $$sigmoid和softmax的区别也值得说一句sigmoid对每个输出独立处理适合多标签分类softmax在输出之间做竞争适合单标签多分类。跟nn.Linear配合时的选择取决于任务定义。6.2 MLP 堆叠线性层之间不能没有激活函数理论上多个线性层堆叠在一起数学上仍然等价于一个线性层。因为线性变换的复合还是线性变换$$ W_2 (W_1 x b_1) b_2 (W_2 W_1) x (W_2 b_1 b_2) $$所以如果在两个nn.Linear之间不插入非线性激活函数那堆 10 层跟堆 1 层没有本质区别模型表达能力不会增加。这就是为什么一个典型的 MLP 模块长这样nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 64), nn.ReLU(), nn.Linear(64, 10) )ReLU的作用是引入非线性让网络能够拟合复杂函数。这个设计哲学贯穿整个深度学习线性变换负责改变数据的形状和坐标系非线性激活负责“掰弯”直线让模型具备更强的表达能力。6.3 Transformer 中的 Linear三个投影矩阵其实都是它如果你对 Transformer 有了解会发现里面的W_q、W_k、W_v以及 FFN 里的两个全连接层全部都是nn.Linear的实例。输入序列经过三个不同的线性投影分别得到 query、key、value然后做注意力计算。注意力机制里的核心公式$$ Attention(Q, K, V) softmax\left(\frac{QK^T}{\sqrt{d_k}}\right) V $$这里的 $Q$、$K$、$V$ 都是从原始输入通过三个独立的nn.Linear映射得到的。从数学角度理解线性投影把输入向量映射到三个不同的“语义空间”让模型可以从不同角度对序列中的信息进行检索和聚合。理解了nn.Linear在 Transformer 里的角色之后再回头看“为什么深度学习框架的核心是矩阵乘法”这个问题答案就非常清晰了无论是全连接层、卷积的 im2col 实现还是注意力机制里的投影和相似度计算底层全都归结为矩阵乘法而nn.Linear是最直观的那块积木。7. 常见问题与排查技巧从维度报错到梯度异常7.1 维度不匹配导致的报错这是出现频率最高的问题。常见场景有两个一是输入维度设置错误。模型定义时写了nn.Linear(784, 256)但实际输入的数据最后一维是 512矩阵相乘时直接报 shape 不匹配。排查方式很简单在报错信息里看mat1和mat2的形状mat1是你输入经过前面层之后的实际形状mat2是线性层期望的形状两者最后一维必须一致。二是输入形状超过 2 维。nn.Linear对多维输入的处理是除了最后一维其他维度全部当作 batch 维度。比如输入形状是 $(batch, seq_len, hidden_size)$线性层会对每个时间步独立做变换输出形状变成 $(batch, seq_len, out_features)$。这个行为在某些场景下很省事但也可能在查看梯度形状时让人困惑。如果确实只想变换高维向量的某一维可以先 reshape 再进线性层。7.2 模型参数量怎么算算模型的参数量是量化模型大小的基本功。一个nn.Linear(in, out)的参数数量是$$ in \times out out $$其中 $in \times out$ 是权重参数$out$ 是偏置参数。比如nn.Linear(768, 768)参数量是 $768 \times 768 768 591,360$大约 59 万。BERT-base 里有几十个这样的层所以模型动辄上亿参数也就好理解了。7.3 梯度异常NaN 或者不更新实际训练中如果发现某个线性层的梯度变成 NaN原因多半是输入里出现了无穷大值或者学习率设置过大导致权重更新幅度过大。排查思路检查输入数据是否经过归一化有没有异常值检查 loss 是否在某个 step 突然变成 NaN如果是大概率是学习率问题用torch.isnan检查权重和梯度的数值for name, param in model.named_parameters(): if torch.isnan(param).any(): print(fNaN in {name})另一个容易被忽略的点是requires_grad可能被意外设置成了False导致参数在训练过程中不更新。排查时可以用param.grad是不是None来快速判断。7.4 一个实用小工具查看每层的输出形状搭建模型时不确定每层输出的形状可以用一个简单的自定义函数打印出来def print_shape(model, input_tensor): x input_tensor for name, layer in model.named_children(): x layer(x) print(f{name}: {x.shape}) return x这比在报错之后反复猜维度要高效得多。尤其是在写多层网络时每定义一个模块就顺手打印一次形状能省下大量调试时间。8. 我对 nn.Linear 的一些个人经验从第一次接触 PyTorch 到现在nn.Linear可能是用过的所有 API 里出现频率最高的一个。但它也恰恰是我觉得最值得多花时间理解的概念之一原因很简单整个深度学习训练过程中大量的计算都围绕矩阵乘法展开而nn.Linear就是最纯粹的那个起点。有几个小经验值得分享。第一调试模型时如果损失不下降不要急着调学习率或换优化器先用一个单 batch 的数据试一下模型能不能跑通前向和反向传播看看梯度是不是正常。很多时候问题出在数据维度或者nn.Linear的输入输出设置上而不是优化算法。第二在自定义网络层时尽量手动计算一遍输入输出维度和参数量再跟torchsummary或者model.parameters()的结果对一下。这一步能帮你建立对模型结构的直觉而不是盲目地“搭积木”。第三理解nn.Linear的数学原理之后后续接触nn.Conv2d卷积层时你会发现它们在数学上有很多共通之处都是线性变换都涉及权重和偏置只是卷积在空间维度上做了参数共享和局部连接。有了这个认识学习新概念的速度会快很多。最后想补充一点PyTorch 官方文档里对nn.Linear的解释非常简洁但如果你想更深入地看它的实现细节直接去阅读源码是最直观的路径。torch/nn/modules/linear.py这个文件只有几十行几秒钟就能读完里面包含了weight、bias的声明以及forward的定义看一遍基本就对整个模块了如指掌了。