ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深度学习第二课:nn.Linear线性层代码逐行解读与实战

深度学习第二课:nn.Linear线性层代码逐行解读与实战 很多自学深度学习的人在第二节课就会迎来第一道坎课程或教程甩过来一段带nn.Linear的代码配一句这是最基础的线性层大家自己看看。我见过太多人卡在这一步——函数名每个字母都认识代码也能跑通但就是说不清它内部发生了什么weight为什么要定义成那个形状forward里为什么要这样乘训练循环里那五行代码为什么缺一不可这篇文章就把深度学习第二节课之 linear 代码解读这件事做完整从数学上它在算什么、到 PyTorch 源码里它怎么写、再到训练闭环怎么转起来、最后用手写版穿透包装一次讲清楚。正在跟深度学习入门课程的朋友或者跑过几个 Demo 但对linear心里没底的同学都可以拿这篇当第二节课的配套笔记来用。1. 为什么第二节课的主角只能是 Linear1.1 linear 在深度学习里到底指什么先统一一下概念。深度学习语境里的linear在 PyTorch 中的实体就是torch.nn.Linear通常翻译成线性层或者全连接层。它的数学定义非常朴素单个样本y Wx b一批样本Y XW^T b注意 PyTorch 文档里的实际约定是output input weight.T bias也就是说权重矩阵W的形状是(out_features, in_features)输入从右边乘上来。很多初学者对照数学公式 y Wx 去看代码会懵就是因为这个转置约定。这个细节后面我会专门展开。为什么所有入门课都在第二节课安排它因为linear就是神经网络里最基础的构件。一个感知机就是一个linear加一个阶跃激活一个多层感知机就是好几个linear叠起来CNN 最后的分类层通常是linearTransformer 里做自注意力时的 Q、K、V 投影本质上也全是linear。你把linear看透了后面见到的绝大多数复杂结构都是一堆不同形状的线性变换加上非线性激活在组合跳舞。1.2 课程设计视角这段代码是浓缩的知识枢纽站在带新人的角度第二节课安排 linear 是非常聪明的设计。第一节课讲环境、讲张量学习者刚掌握的数据类型、形状、matmul这些概念在其他地方很难一口气串成闭环但通过 linear 可以全部串起来你得处理输入形状这就用到了张量的shape概念你得理解weight和bias这就引出了参数的概念你把模型放进训练循环里计算损失这就引出了前向传播和损失函数你要更新参数这就必须解释梯度、backward、优化器。换句话说学 linear 的这节课实际上是整个深度学习课程里第一个最小完整闭环。如果第二节课去讲 CNN 或 Transformer信息量会直接爆炸学习者会被卷积核尺寸、padding、多头注意力这些概念淹没。而 linear 足够简单简单到你可以把全部注意力放在一段代码是怎么从数据变成模型的这件事本身。我一直跟学员说第二节课的 linear 代码不是让你背 API而是给你一把扳手把深度学习到底怎么让程序学会东西这个黑盒子撬开一条缝。下面我们就从源码角度把这把扳手的结构看仔细。2. nn.Linear 源码逐行解读一行代码背后的四件套2.1 先建立直观感受用一个小例子看穿它直接看代码。创建一个 4 维输入、3 维输出的线性层然后打印关键信息import torch import torch.nn as nn linear nn.Linear(in_features4, out_features3) print(linear.weight.shape) # torch.Size([3, 4]) print(linear.bias.shape) # torch.Size([3]) x torch.randn(2, 4) y linear(x) print(y.shape) # torch.Size([2, 3])这段代码的输出非常有信息量。in_features4, out_features3模型会把输入的最后一位从 4 变成 3。weight的形状是[3, 4]行数是输出维度列数是输入维度。很多人第一次看到这个形状会疑惑为什么不是[4, 3]原因就是前面说的 PyTorch 约定output x weight.T bias。x 是(2, 4)weight.T是(4, 3)乘出来正好是(2, 3)。从矩阵乘法角度可以算一笔账一个形状为(batch, in_features)的输入乘上形状为(in_features, out_features)的转置权重最后得到(batch, out_features)。这个形状链条是理解所有全连接层的基础。2.2 init 里的三件事参数、初始化、偏置开关nn.Linear在__init__阶段做的事情本质上就是创建两个参数对象并做初始化。我在这里给出一个简化但不失真义的版本class Linear(nn.Module): def __init__(self, in_features, out_features, biasTrue): super().__init__() self.in_features in_features self.out_features out_features self.weight nn.Parameter(torch.empty(out_features, in_features)) if bias: self.bias nn.Parameter(torch.empty(out_features)) else: self.register_parameter(bias, None) self.reset_parameters() def reset_parameters(self): nn.init.kaiming_uniform_(self.weight, amath.sqrt(5)) if self.bias is not None: fan_in, _ nn.init._calculate_fan_in_and_fan_out(self.weight) bound 1 / math.sqrt(fan_in) if fan_in 0 else 0 nn.init.uniform_(self.bias, -bound, bound)逐行看。self.weight nn.Parameter(torch.empty(out_features, in_features))表示创建一个随机初值的参数张量。注意这里的两个形状参数顺序很容易踩坑函数签名是in_features在前但张量形状是out_features在前。Parameter是 PyTorch 专门为可训练参数设计的类型它和普通 Tensor 最大的区别是具备requires_gradTrue会被优化器自动识别和更新。如果你把我的个人经验记一下凡是在模型里用Parameter包裹的量就是神经网络需要通过训练去学习的旋钮。初始化也很关键。想象一下如果weight全部初始化为 0那么每个样本的预测输出都会是 bias 的重复值而且梯度传播时每层信号全是零深层网络根本学不动。这就像所有人都站在原地无论你往哪个方向喊没有人能确定该往哪走。所以 PyTorch 默认用 Kaiming 均匀分布来初始化权重范围由输入维度fan_in决定目的是让输出方差维持在可控量级避免信号在前向传播中指数级放大或衰减。Bias 的初始化范围则简单很多取1 / sqrt(fan_in)基本是一个以 0 为中心的小量。biasTrue是默认开关。你可以传入biasFalse得到一个不带偏置的纯线性变换。什么时候需要关掉 bias比如在对输入做归一化之前、或者下游已经带截距项的时候关闭 bias 能减少不必要的自由度也能避免一些数值上的冗余。但对于绝大多数入门场景保持默认就好。2.3 forward 里到底算了一次什么运算创建完模型调用linear(x)时实际执行的是nn.Module.__call__内部逻辑最终会进到F.linear(x, weight, bias)。核心计算一行就能说清output x weight.T bias我来手动复现一遍验证结果完全一致manual x linear.weight.T linear.bias print(torch.allclose(manual, y)) # True这个 True 很有意义它说明nn.Linear本身没有任何神秘力量就是一次矩阵乘法加一次广播加法。bias的形状是[3]而输出是[2, 3]PyTorch 会把 bias 按行广播每一行都加上同一个偏置向量。这个广播机制是张量库的通用能力你在后续接触卷积、注意力机制时也会频繁遇到。还有一个常被忽略的细节nn.Linear的 forward 对非最后维度是完全无感的。输入可以是(N, T, 4)输出就是(N, T, 3)中间每一行独立做线性变换参数共享。这个特性让 Linear 能处理序列、时间步等多维数据也是后面理解 Transformer 结构的重要伏笔。3. 形状陷阱在 batch 维度上翻车的次数比想象的要多3.1 从单个向量到一批数据的维度跃迁初学者最大的痛苦来源之一就是维度。单个样本进入nn.Linear(4, 3)时x 的形状假设是(4,)输出是(3,)。这个纯向量形式在数学上很干净但在真实训练里几乎不会这样用——深度学习训练几乎总是采用 mini-batch 方式一次喂给模型一整批样本。于是 x 的真实形状变成(N, 4)输出变成(N, 3)。多出来的第一维就是 batch 维它告诉 PyTorch这次计算有 N 个样本GPU 可以并行处理这些样本。很多新手手写数据时会犯一个经典错误造数据时忘了把单个样本变成二维的(N, in_features)。比如x torch.linspace(-1, 1, 100) model nn.Linear(1, 1) y model(x) # 看起来能跑其实 x 是 (100,) 而不是 (100, 1)这个例子因为输入和输出都是 1 维代码往往能跑通但x被当成 100 个独立样本、每个样本 0 维实际上 PyTorch 会把它当作(100,)即 100 个样本每个样本in_features1不是这样PyTorch 支持(*, in_features)输入所以(100,)会被视为 100 个独立的标量样本正好输出(100,)。这个行为在标量场景下歪打正着但一旦in_features 1你就必须显式地保留特征维度。一个稳定的习惯任何时刻都让输入保持显式的 batch 维。用reshape(-1, in_features)或者.unsqueeze(0)给单个样本增加 batch 维。别偷懒这个习惯能帮你躲掉后面一堆雷。3.2 三个最常见的形状报错实例我在带学员过程中反复遇到三类的 shape 报错。逐个拆开看。第一个是mat1 and mat2 shapes cannot be multiplied。比如x torch.randn(4, 2) linear nn.Linear(4, 3) y linear(x) # RuntimeError: mat1 and mat2 shapes cannot be multiplied (4x2 and 4x3)这个报错的本质是你输入的最后维度是 2但weight的转置形状是(in_features4, out_features3)也就是期望 x 的最后一维是 4。排查时先看linear.weight.shape再看输入x.shape确保两者匹配。第二个常见错误是卷积特征图没有展平就喂给 Linear。CNN 输出的特征图形状通常是(N, C, H, W)而 Linear 只认最后一位。直接塞进去会报维度不匹配。正确做法是先.flatten(1)把(N, C, H, W)展平成(N, C*H*W)再接 Linear。这一步是 CNN 分类头里最常见的衔接方式。第三个更隐蔽batch 维和特征维搞反。比如有的同学手写数据集时用了(features, samples)这种转置结构然后再丢进 Linear导致模型学到的东西全部错位。深度学习中数据的标准布局是(batch, features)如果你从别处拿到的是(features, batch)记得先转置。3.3 一张形状速查表整理一张常用的形状对照表建议收藏排查时直接对号入座。输入形状模型参数输出形状说明(4,)Linear(4, 3)(3,)单个样本合法但少见(N, 4)Linear(4, 3)(N, 3)标准 mini-batch(N, T, 4)Linear(4, 3)(N, T, 3)序列输入每个时间步独立做线性变换(N, C, H, W)直接接 Linear 会报错先.flatten(1)CNN 特征图需要展平后接入当你哪天在一个复杂的网络里看到维度不匹配的报错不要慌。从报错堆栈里找到出错的 Linear 行打印一下它的 weight.shape 和输入张量的 shape对照这张表基本就能定位。4. 把 Linear 放进训练循环最小的可运行代码逐段拆解4.1 完整示例拟合一条直线学 Linear 不能只看它自己孤零零地算一次前向必须放进训练循环里动起来。下面这个例子是我上课时最爱用的 demo完整、可运行、而且能直观看到参数变化。import torch import torch.nn as nn # 1. 造数据真实规律是 y 2x 1再加一点噪声 torch.manual_seed(0) x torch.linspace(-1, 1, 128).reshape(-1, 1) y 2 * x 1 0.05 * torch.randn_like(x) # 2. 定义模型、损失函数、优化器 model nn.Linear(1, 1) loss_fn nn.MSELoss() optimizer torch.optim.SGD(model.parameters(), lr0.05) # 3. 训练循环 for epoch in range(500): pred model(x) loss loss_fn(pred, y) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 100 0: print(fepoch {epoch:3d}, loss: {loss.item():.4f}) print(f最终学到的参数: weight{model.weight.item():.3f}, bias{model.bias.item():.3f})你跑完会看到 loss 从接近 1 一路掉到非常接近 0最终学到的weight逼近 2bias逼近 1。注意模型并不知道真实规律是 y 2x 1它是完全从数据里猜出来的。这就是深度学习最核心的逻辑给数据、给一个带参数的模型、给一个衡量差距的损失函数然后用梯度下降不断调节参数让损失变小。4.2 训练循环里那五行代码的各自分工训练循环看起来只有五行但每一行都缺一不可。我拆开讲第一行pred model(x)是前向传播。数据和模型进来预测结果出去。此时模型内部只是做了一次矩阵乘法加 bias没有任何学习发生。第二行loss loss_fn(pred, y)是计算损失。MSELoss即均方误差把预测值和真实值的差距量化为一个标量。这个标量代表现在模型有多不准。第三行optimizer.zero_grad()是清空梯度。这个操作是新手最容易忽略的坑。PyTorch 的设计里梯度默认是累加的每次backward()会把新算出来的梯度加到旧的.grad上。如果你不在每个 batch 开始前清零梯度就会累加得越来越大参数更新方向也会被历史梯度污染训练表现会非常诡异。第四行loss.backward()是反向传播。PyTorch 会从 loss 出发沿着计算图自动求出每个参数对 loss 的导数存放到weight.grad和bias.grad里。这是整个循环里魔法含量最高的部分但本质就是一个链式法则的自动实现。第五行optimizer.step()是更新参数。优化器根据.grad和设定的学习率把参数往梯度下降的方向挪一步。SGD 的更新公式就是w w - lr * grad。这一步做完模型才真正学了一点东西。下面这张表方便你记忆代码行作用如果漏掉pred model(x)前向计算什么都没有无法开始loss loss_fn(pred, y)衡量预测和真实差距没有更新的目标optimizer.zero_grad()清空累积梯度梯度累加更新异常loss.backward()计算每个参数的梯度参数不动optimizer.step()按梯度更新参数模型永远不学习4.3 看 loss 下降过程训练到底在调什么很多人看训练过程只看 loss 数字但不太理解背后的直觉。我常用一个下山类比想象你在山脊上蒙眼走路目标是走到山脚下。每一小步的方向由脚下的坡度决定——梯度就是那个坡度学习率就是你迈步的幅度。loss 相当于你离地面的高度step()就是朝下坡方向走一步。从这个类比能自然推出两个看训练的技巧第一学习率过大会导致步子迈太大。你可能一脚跨过山脚跑到对面的山坡上再跨回来如此反复loss 不降反升甚至发散成nan。学习率过小则像在原地走碎步很久才前进一点。对于入门任务lr0.05或0.01通常是比较安全的起点。第二训练结束时别只盯着 loss把模型参数打出来看。上面例子跑完weight应该非常接近 2.0、bias接近 1.0。这就是学到知识的最直接证明——知识被编码在这两个数值里了。很多初学者觉得神经网络学到的东西很抽象其实最朴素的场景下学到的就是这一组数字而已一点都不神秘。5. 手写一个 Linear反向传播不是黑魔法5.1 用 numpy 手写 forwardnn.Linear再好用如果只看它永远隔着一层。我建议你至少手写一次最基本的线性模型把forward和梯度更新都自己实现一遍这能让你对深度学习建立起底层代码不过是数学运算的直觉。先写前向import numpy as np def linear_forward(X, w, b): return X w b这个函数做的事情和F.linear完全一样不考虑转置的区别这里我直接用单个权重向量。给定输入矩阵X和权重w、偏置b输出就是矩阵乘加偏置。没有其他隐藏逻辑。5.2 手写梯度推导与训练循环现在考虑一个最简单的单变量场景数据点来自 y 2x 1我们想让一个w和一个b去逼近它。损失函数使用 MSEloss mean((y_pred - y_true)^2)对w求梯度用链式法则dloss/dw (2 / N) * sum((y_pred - y_true) * x)dloss/db (2 / N) * sum(y_pred - y_true)这就是反向传播在这个最小模型上的全部内容。把它写进训练循环x np.linspace(-1, 1, 128) y_true 2 * x 1 0.05 * np.random.randn(128) w, b 0.0, 0.0 lr 0.05 for epoch in range(500): y_pred w * x b loss np.mean((y_pred - y_true) ** 2) grad_w 2.0 * np.mean((y_pred - y_true) * x) grad_b 2.0 * np.mean(y_pred - y_true) w - lr * grad_w b - lr * grad_b if epoch % 100 0: print(fepoch {epoch:3d}, loss: {loss:.4f}) print(f手写训练结果: w{w:.3f}, b{b:.3f})你会发现这套手写循环和前面 PyTorch 的训练循环在逻辑上完全同构算预测、算 loss、算梯度、更新参数。区别只在于 PyTorch 用autograd自动完成了grad_w和grad_b的推导。这也是为什么我一直强调反向传播不是黑魔法——在最小模型上它就是一个链式法则几行代码就能写清楚。5.3 所以 autograd 到底替你省了什么看完手写版你应该能回答为什么实际项目还是用 nn.Linear这个问题了。手写梯度在一个参数、两个参数时还能忍但当模型变成两层、三层w变成矩阵梯度推导会指数级复杂化。更别提像 ResNet、Transformer 那样动辄几十上百层手推梯度在工程上完全不可行。autograd做的事情是在前向传播时记录计算图把每个张量的操作历史存成grad_fn调用backward()时从 loss 出发沿图反向走一遍自动计算出每个Parameter对应的梯度。这是深度学习框架最核心的引擎之一但它的基础仍然是你在手写版里见过的那条链式法则。所以学习路线应该是手写一遍一维线性回归理解梯度下降的本质再切回nn.Linear把注意力放在模型设计、数据处理和训练策略上。两条腿走路你既能看懂底层又不会被底层绑住手脚。6. 新手最爱踩的几个 linear 相关坑以及我的查错顺序6.1 坑一in_features 和实际输入对不上这个坑出现的频率最高。报错信息五花八门但拆到底几乎都是同一个问题linear.weight.shape的列数也就是in_features和输入张量最后一维不相等。我自己排查这类问题时的固定顺序是看报错最底下的RuntimeError找到报错对应的Linear层到底在模型哪一处打印该层的weight.shape打印送入该层的张量shape确认最后一维是否对齐。很多人的问题不是不会改而是不看报错里的文件和行号。PyTorch 的报错已经把所有线索都给你了养成从下往上读的习惯排查效率会高很多。6.2 坑二batch 维被暴力 reshape 破坏有一次学员写了这样的预处理代码x torch.randn(64, 4) # 64 个样本每个 4 维 x x.reshape(-1, 2) # 变成了 128 个样本每个 2 维然后喂给Linear(4, 3)直接报错。他把reshape当成万能工具箱结果把数据布局完全打乱。reshape会自动推导维度但你必须明确每一维的语义。处理输入数据的正确姿势是先确认原始数据的真实含义再决定如何变形图省事随便-1是灾难的开端。我建议在任何预处理脚本里每做一次 shape 变换就打印一次新形状并且写一行注释例如# 经过这步变成 (batch, features)。这个习惯能帮你追回无数时间。6.3 坑三记不清 weight 到底是 [3,4] 还是 [4,3]这个问题我在课上问过无数次几乎每次都有人答反。其实记忆方法很简单out_features决定 weight 的行数——你想输出几个数它就几行in_features决定 weight 的列数——你喂进来几个数它就几列。所以Linear(4, 3)的 weight 是[3, 4]。很多教材里的公式是y WxW 形状天然是(out, in)和 PyTorch 是吻合的差别只在代码实现里weight是否需要转置。你只要记住第一个参数是输入维度、第二个是输出维度就够用了然后每次用的时候打印一下shape确认而不是靠脑内虚拟运行。这个习惯能帮你规避掉这个坑的绝大部分变体。6.4 坑四loss 不降反升先怀疑学习率如果训练循环代码没问题但 loss 一直震荡甚至变成nan十有八九是学习率太大。很多人会下意识去调网络结构其实这种症状最应该先调的就是lr。我的排查顺序是把学习率降低为原来的十分之一看 loss 曲线是否平滑如果还是发散检查输入数据是否有极端值考虑归一化如果 loss 为nan检查数据中是否有无穷值或糟糕的初始值最后才考虑别的模型结构问题。作为参考我在入门任务里一般从lr0.05起步效果不佳就降到0.01再不行就0.001。等你有经验后再引入学习率调度器也不迟。6.5 把第二节课学到的拆解法用到下一个模块关于 linear 的代码解读到这里核心内容就讲完了。但你真正要带走的不只是 linear 本身而是一套解读任何代码模块的方法先看数学定义、再看参数形状、跑一次前向验证、放进训练循环里观察变化、最后手写一遍穿透黑盒子。顺着这个思路你下一步可以用同样的方法去学nn.ReLU、nn.Sequential、nn.MSELoss。找一个简单的二分类问题把 Linear 和 ReLU 叠成一个两层小网络亲手试一试加激活和不加激活训练结果有什么本质区别这个实验做完你就亲手验证了非线性为什么是神经网络能力的来源。第二节课的门就算真正迈过去了。
返回列表