深度学习项目做了不少,从图像分类到文本生成,最后真正让我把整个体系串起来的,反而是最不起眼的那个概念:线性表示。我见过太多“跑通了代码却不懂原理”的新人,他们卡住的节点出奇一致——不知道层与层之间的维度怎么算、不知道为什么要有激活函数、更不清楚所谓“端到端学习”学的到底是什么。这些问题追到根上,都指向同一个数学对象:线性映射。
这个项目就是围绕“线性表示”这条主线,带你亲手从零实现一套完整的小型深度学习系统,包括纯 NumPy 手写的线性回归、全连接神经网络,以及对应的 PyTorch 版本。它不是什么高深的理论推演,而是接地气的实操项目,适合刚学完 Python 基础、准备踏入深度学习大门的初学者,也适合那些已经能跑通现成模型、但总觉得地基不牢的人。整个项目用一台普通 CPU 电脑就能完成,根本不需要折腾 GPU 服务器——这反而是很多人忽略的入门优势。
1. 项目概述与设计思路
做这个项目之前,我先说清楚一件事:为什么“线性表示”值得单独拿出来当做一个深度学习项目来做,而不是直接上手画个 CNN 或者跑个 Transformer。
1.1 什么是线性表示
线性表示,说人话就是:用一条直线或者一个超平面去描述输入和输出之间的关系。在机器学习语境下,它指的是把输入特征 ( x ) 通过一个线性变换映射到输出空间,数学形式就一行:( y = Wx + b )。其中 ( W ) 是权重矩阵,( b ) 是偏置向量。
在深度学习里,这个式子无处不在。全连接层就是个典型的线性映射,卷积层在提取完局部特征之后,最终也要通过一个线性分类头输出结果;Transformer 里的 Q、K、V 投影矩阵,本质上是三个不同的线性变换;就连大语言模型里那些看起来“智能”的注意力机制,拆到最底层,核心操作依然是矩阵乘法——也就是对输入向量做加权线性组合。
我经常用一个生活类比帮新人理解线性表示:它就像“按斤称重计价”。单价固定,你买两斤就是一斤的两倍价格,不存在量大优惠、量小加价这种弯弯绕绕。非线性则是“阶梯计价”,买得越多单价越低,甚至还有满减券——这种规则就复杂多了,也正是这种复杂性,让神经网络有了逼近各种稀奇古怪函数的能力。
1.2 为什么拿线性表示当深度学习项目的切入点
这个项目选“线性表示”,不是因为线性模型本身有多厉害,而是因为它是最小、最完整的深度学习切片。我总结过三个理由。
第一,它的计算路径短,梯度可以手工推导。一个线性层的向前传播只有乘法和加法,反向传播只有链式法则的两次乘积。这意味着你可以在白纸上把每一个梯度公式写出来,再对照代码逐行验证。这种“看得见、摸得着”的掌控感,是直接调 PyTorch 接口永远给不了你的。
第二,它是一切深度学习模型的公共底座。你可以把任何复杂网络看成三件事的组合:线性变换、非线性激活、损失函数。卷积是局部共享的线性变换,注意力是对输入做动态加权的线性组合,哪怕是残差连接,也是把输入“原样加上”的线性操作。把线性表示搞明白了,后面学什么结构都轻松。
第三,它的环境要求极低。这个项目的全部代码跑在 CPU 上毫无压力,几十秒就能完成训练。我见过太多新手一上来就折腾 GPU 驱动、CUDA 版本、云服务器扣费,结果环境配了三天,模型一行没跑。先把基础项目在本地跑通,把心思放在原理上,才是正路。
1.3 项目目标与技术路线
整个项目分三步走:先用 NumPy 手写一个线性回归,理解最朴素的前向传播、损失计算、梯度下降;再用手写的方式实现一个多输出线性层,装上 Softmax 和交叉熵,做分类任务;最后引入隐藏层和 ReLU 激活函数,把网络从线性升级成非线性,亲手验证“深度”到底带来了什么。
完成这三步之后,你会发现几条硬通货级别的认知:
- 能独立推导并实现线性层的前向传播和反向传播;
- 明确掌握张量维度变化规则:输入形状 ([N, in_features]) 经过权重 ([in_features, out_features]) 变换,得到 ([N, out_features]);
- 能解释为什么线性层堆叠一百层还是“线性”,以及激活函数在其中扮演的角色;
- 能自主完成从线性回归到多层感知机的代码演进,而不是复制粘贴。
这些能力,比背十遍“神经网络是万能逼近器”有用得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数学模型与原理解读
这一章我不打算堆公式,而是把线性表示背后那些“为什么”讲透。你要是能把这一章的直觉建立起来,后面不管学什么模型,都不会再觉得它是黑盒。
2.1 从线性回归到线性层:一次优雅的升级
线性回归是最简单的监督学习模型,形式是 ( y = wx + b ),输入是一个标量或者向量,输出是一个标量。深度学习里的线性层,其实就是把多个线性回归“并排”放在一起:每个输出神经元都有一组独立的权重和偏置,它们共享同一个输入,但各自产生一个输出。
用矩阵写出来更清晰:( Y = XW + b )。假设输入 ( X ) 的形状是 ([N, in_features]),权重 ( W ) 的形状是 ([in_features, out_features]),那么输出 ( Y ) 的形状就是 ([N, out_features])。这里的 ( N ) 是样本数量(batch size),( in_features ) 是每个样本的特征维度,( out_features ) 是我们要输出的维度。
这个矩阵乘法的几何意义非常直观:一个线性变换,本质上是把输入空间做了旋转和缩放。如果 ( W ) 是方阵,你可以把它的行列式理解为“体积缩放系数”,把它的特征向量理解为“变换后保持不变的方向”。偏置 ( b ) 则承担了平移的作用——没有偏置,所有变换都必须经过原点,表达力会大打折扣。
2.2 线性层堆叠的致命缺陷
很多初学者会想:既然一个线性层能做线性回归,那我把十个线性层串起来,是不是就能拟合曲线了?答案是:不能。
原因在于线性变换的复合仍然是线性变换。我们算一下:第一层 ( h = W_1x + b_1 ),第二层 ( y = W_2h + b_2 ),整体展开就是 ( y = (W_2W_1)x + (W_2b_1 + b_2) )。你看,两个权重矩阵相乘得到一个合并矩阵,两个偏置向量合并成一个新偏置——无论堆多少层,最终表达式还是 ( y = Wx + b ) 的形式。换句话说,深层线性网络的表达能力,和一个单层线性网络完全等价。
这是个非常重要的结论:深度学习之所以“深”,前提是每一层后面都跟了非线性激活函数。没有激活函数,深度就是个数学上的伪命题。
2.3 激活函数:打破线性牢笼的开关
激活函数的作用,是在每一层线性变换之后,逐元素地施加一个非线性映射。最常用的是 ReLU:( f(z) = \max(0, z) ),负数全部截断,正数原样输出。
ReLU 虽然看起来很简单,但正是这个“截断”操作,让整个网络变成了分段线性函数。你可以把它想象成一个个开关:对于不同的输入区域,网络会选择不同的线性路径。当一个样本落在正区间,就走这条线;落在负区间,就走另一条线。大量这样的开关组合在一起,就能拼出任意复杂的决策边界。
这里有一个数学保证叫万能逼近定理:只要隐藏层神经元数量足够多,激活函数是非线性的(比如 ReLU),一个三层前馈网络就能以任意精度逼近任意连续函数。虽然这个定理没有告诉你多少神经元才算“足够”,但它在理论上解释了为什么神经网络有资格被称为“通用模型”。
补充一个实操细节:ReLU 在负区间的梯度恒为 0,可能导致神经元“死亡”(一旦权重更新后输入落在负区间,此后梯度永远是 0,神经元再也无法激活)。实战中如果发现 ReLU 网络大量神经元输出恒为 0,可以考虑换成 Leaky ReLU,它给负区间一个很小的斜率(比如 0.01),让梯度能够回流。
2.4 损失函数与梯度下降的直觉
模型有了表达力,还得有一个“好坏的度量标准”。回归任务最常用均方误差:( L = \frac{1}{N} \sum (y_{pred} - y_{true})^2 )。分类任务常用交叉熵,它衡量的是预测概率分布和真实标签分布之间的差距。
梯度下降的过程,可以类比成在山谷里找最低点:你不知道全局最低处在哪里,但你能感觉到当前位置哪个方向是下坡。权重沿着梯度的反方向更新一小步,loss 就会降低一点点。学习率就是这个“步长”——太大容易跳过最低点甚至震荡发散,太小则半天挪不动窝。
我见过太多新手在调参时像无头苍蝇一样乱试。这里给一个非常管用的直觉:如果你的 loss 曲线是锯齿状上下乱跳,先尝试把学习率除以 10;如果 loss 曲线平缓下降但训练时间太长,尝试把学习率乘以 2,同时观察是否出现发散。一个合适的初始学习率,在回归任务上通常在 0.001 到 0.01 之间,分类任务稍低一些。
3. 核心实现与实操过程
理论知识讲完,开始动手。这部分我给出完整的可运行代码,并且每一步都说明“我为什么这么写”,而不是让你复制完就完事。
3.1 环境准备:CPU 就够了
这个项目只需要三样东西:
- Python 3.8 及以上版本;
- NumPy,用于手写模型和数据处理;
- PyTorch(CPU 版即可),用于第二阶段的框架实现。
安装命令很简单,在终端里执行:
bash复制pip install numpy
pip install torch --index-url https://download.pytorch.org/whl/cpu
如果你之前已经装过 GPU 版 PyTorch,也不用卸载,CPU 训练照样能跑。后面所有代码我都保证在纯 CPU 环境下运行时间不超过一分钟,放心复制。
3.2 步骤一:纯 NumPy 实现线性回归
我们先构造一个人工数据集:真实关系是 ( y = 2x + 1 ),加上一些高斯噪声,模拟现实中的观测误差。
python复制import numpy as np
np.random.seed(42)
x = np.random.rand(100, 1) * 10 # 输入特征,范围 0~10
true_w, true_b = 2.0, 1.0
y = true_w * x + true_b + np.random.randn(100, 1) * 0.5 # 加噪声
# 定义模型参数和训练超参数
w = np.random.randn(1, 1) * 0.01
b = np.zeros((1, 1))
lr = 0.005
for epoch in range(1000):
# 前向传播:计算预测值
y_pred = x @ w + b
# 计算损失(均方误差)
loss = np.mean((y_pred - y) ** 2)
# 反向传播:手动推导梯度
grad_y_pred = 2 * (y_pred - y) / len(x)
grad_w = x.T @ grad_y_pred
grad_b = np.sum(grad_y_pred, axis=0, keepdims=True)
# 参数更新
w -= lr * grad_w
b -= lr * grad_b
if epoch % 100 == 0:
print(f"epoch {epoch:3d} loss {loss:.6f}")
print(f"训练得到的 w = {w.item():.4f}, b = {b.item():.4f}")
训练结束后,你应该会看到 w 逼近 2.0,b 逼近 1.0,loss 从几十降到零点几。这里有两个细节值得你关注。
第一个是权重初始化。我把 w 初始化为 0.01 倍的随机数,而不是直接 random.randn,原因在于初始权重过大会导致一开始的预测离真实值太远,梯度巨大,训练极不稳定。第二个细节是梯度公式里的 ( 2/N ),这是对均方误差求导的结果,千万别漏掉,否则学习率的表现会跟你预期差一倍。
如果你跑出来的 loss 不降反升,先检查是不是学习率设大了。这个数据集的 x 范围是 0 到 10,所以梯度本身就偏大,0.01 以上就很容易发散。我上面特意用 0.005,就是提前帮你避坑。
3.3 步骤二:用 PyTorch 搭建线性分类器
回归只是热身。现在我们把任务升级成二分类,数据集也不再是手工构造的线性直线,而是两个可分离的二维点簇。这次我们使用 PyTorch 的 nn.Linear 层,体会一下框架的便利。
python复制import torch
import torch.nn as nn
import torch.optim as optim
# 构造线性可分的数据
torch.manual_seed(42)
n = 200
class0 = torch.randn(n, 2) + torch.tensor([-2.0, -2.0])
class1 = torch.randn(n, 2) + torch.tensor([2.0, 2.0])
X = torch.cat([class0, class1], dim=0)
y = torch.cat([torch.zeros(n, 1), torch.ones(n, 1)], dim=0)
# 打乱顺序
perm = torch.randperm(len(X))
X, y = X[perm], y[perm]
# 定义一个单层线性分类器
model = nn.Linear(2, 1)
loss_fn = nn.BCEWithLogitsLoss()
optimizer = optim.SGD(model.parameters(), lr=0.05)
for epoch in range(500):
optimizer.zero_grad()
logits = model(X)
loss = loss_fn(logits, y)
loss.backward()
optimizer.step()
if epoch % 50 == 0:
print(f"epoch {epoch:3d} loss {loss.item():.4f}")
# 评估准确率
with torch.no_grad():
preds = (torch.sigmoid(model(X)) > 0.5).float()
acc = (preds == y).float().mean().item()
print(f"准确率: {acc:.4f}")
这里的核心是一个被很多人忽略的点:nn.Linear(2, 1) 这个层的输入维度是 2,因为我们的数据是二维平面上的点;输出维度是 1,因为二分类只需要一个 logit。训练时我们用的是 BCEWithLogitsLoss,它把 sigmoid 激活和交叉熵损失合并在了一起,数值上更稳定。如果你手工分开写 sigmoid 再算 BCE,反而会遇到梯度震荡的问题。
数据是可线性分离的,所以这个单层模型在 500 个 epoch 后准确率能到 100%。你可以在 Notebook 里画一条决策边界:满足 ( W_1 x_1 + W_2 x_2 + b = 0 ) 的直线。你会发现这条直线正好把两类点从中间切开。
3.4 步骤三:异或问题——线性模型的阿喀琉斯之踵
这里我要做整个项目中最有说服力的一个实验:异或(XOR)分类。异或问题的数据长这样:输入是两个二值特征,输出是它们的异或结果。你一眼就能看出来,在二维平面上这四个点分布在两个对角区域,没有任何一条直线能把两类点分开。
线性模型在这个任务上必然失败,这不是调参能解决的,而是表达能力的局限。我们先用一个线性分类器试试,它会学到一条“最不烂”的直线,准确率撑死 75%。再用一个带隐藏层的多层感知机(MLP),加上 ReLU 激活函数,准确率可以轻松到达 100%。
python复制import torch
import torch.nn as nn
import torch.optim as optim
# 构造异或数据:四个点,两个标签
X = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=torch.float32)
y = torch.tensor([[0], [1], [1], [0]], dtype=torch.float32)
# 线性模型(对比用)
model_linear = nn.Linear(2, 1)
optimizer_linear = optim.SGD(model_linear.parameters(), lr=0.1)
loss_fn = nn.BCEWithLogitsLoss()
for epoch in range(2000):
optimizer_linear.zero_grad()
loss = loss_fn(model_linear(X), y)
loss.backward()
optimizer_linear.step()
with torch.no_grad():
preds_linear = (torch.sigmoid(model_linear(X)) > 0.5).float()
acc_linear = (preds_linear == y).float().mean().item()
print(f"线性模型在异或数据上的准确率: {acc_linear:.4f}")
上面这个模型的准确率大概率是 0.75——它只能靠猜,把四个点中的三个分对,剩下一个永远分错。接下来是 MLP 版本:
python复制class MLP(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(2, 4) # 隐藏层:4 个神经元
self.relu = nn.ReLU()
self.fc2 = nn.Linear(4, 1) # 输出层
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
model_mlp = MLP()
optimizer_mlp = optim.SGD(model_mlp.parameters(), lr=0.1)
for epoch in range(2000):
optimizer_mlp.zero_grad()
loss = loss_fn(model_mlp(X), y)
loss.backward()
optimizer_mlp.step()
with torch.no_grad():
preds_mlp = (torch.sigmoid(model_mlp(X)) > 0.5).float()
acc_mlp = (preds_mlp == y).float().mean().item()
print(f"MLP 在异或数据上的准确率: {acc_mlp:.4f}")
这个实验只要跑一遍,你对“深度”和“非线性”的理解就会推进一大截。线性模型不是“不够好”,而是它的假设空间根本不含能解决异或问题的函数;加了 4 个隐藏神经元和 ReLU 之后,模型有能力在四个点之间“掰”出非线性的决策边界。
实操中你还会观察到一个现象:MLP 的 loss 曲线下降得比线性模型“曲折”,有时会先跳到某个高点再迅速下降。这是因为 ReLU 的存在让损失函数变成非凸的,梯度下降山谷里有多个局部最低点。这时候不用慌,多跑几次或者调小学习率,通常能稳定下来。
3.5 维度变换与初始化:两个必须刻进骨子里的习惯
这个项目做下来,你会频繁跟“维度”打交道。我强烈建议你在每个 Linear 层前后各打一行 shape:
python复制print(f"输入: {x.shape}")
h = self.fc1(x)
print(f"隐藏层输出: {h.shape}")
无数新人报错 “mat1 and mat2 shapes cannot be multiplied”,根源都是维度没对齐。其实规则非常死板:线性层要求输入的最后一位等于权重矩阵的行数(in_features),变换后输出最后一位等于权重矩阵的列数(out_features)。前面有多少个 batch 维度,输出就保留多少个。
权重初始化同样关键。PyTorch 的 nn.Linear 默认采用 Kaiming 初始化,它是为了让前向传播的方差在各层之间保持稳定,避免梯度消失或爆炸。手写模型的时候,你用 0.01 倍随机数通常够了;但层数一旦超过三四层,就得认真考虑 Xavier 或 Kaiming 初始化——否则网络在反向传播时梯度一层层乘下去,到最前面几层要么小到消失,要么大到溢出,训练根本走不动。
4. 常见问题与排查技巧
这部分我直接把你可能踩的坑摆出来,每个问题都给出原因、排查思路和解决方案。
4.1 模型不收敛:loss 变成 NaN 或持续停留在高位
最典型的场景是手写线性回归时,loss 在某个 epoch 之后突然变成 NaN。原因几乎只有一个:学习率太大,导致参数更新幅度过大,梯度爆炸,数值溢出。排查方式很简单:把训练过程中的权重打印出来,如果发现 w 每隔几个 epoch 就成倍暴涨,基本可以确定是学习率问题。
解决方法是先把学习率除以 10 重跑一遍。如果还不行,检查你的输入数据是否需要归一化。比如我前面手写回归时 x 的范围是 0 到 10,梯度量级大概在几十,学习率 0.005 勉强能稳住;如果 x 范围是 0 到 1000,同样学习率下早就炸了。一个通用的数据预处理习惯:把输入标准化到均值为 0、方差为 1,或者缩放到 [0, 1] 区间,能大幅提升训练稳定性。
4.2 loss 不断下降但测试准确率很低
如果你做过训练集和测试集的划分,会发现自己遇到的是“过拟合”。尤其是数据量小、模型隐含层神经元多的时候,网络完全有能力把训练样本“背下来”,但对新样本毫无泛化能力。
解决思路分三路并行:增加训练数据是王道,数据不够就做数据增广(比如图像任务的翻转、裁剪、加噪声);降低模型复杂度,比如减少隐藏层神经元数量;加正则化,常见的 L2 正则在 PyTorch 里就是优化器的 weight_decay 参数,一般设置 1e-4 到 1e-2 之间。还有一个代价极低的技巧是早停:每训练完一个 epoch,用验证集算一次损失,连续几次不降就提前结束训练。
4.3 线性模型在非线性问题上“无声的失败”
很多新手拿到一个非线性分类任务,上来就搭个 nn.Linear,发现准确率死活上不去 80%,就开始疯狂调学习率、换优化器、加大迭代次数——全是徒劳。线性模型的假设空间是固定的,它只能学到一个线性决策边界。这个问题不是“训练不到位”,而是“模型根本做不到”。
排查方法有两种。第一种是把训练数据的分布画出来,看两类点是否能用一条直线隔开;第二种是直接把模型换成 MLP(加一层隐藏层 + ReLU),如果准确率瞬间上来,那么罪魁祸首就是模型复杂度不够。记住:先确认模型表达力和任务是否匹配,再谈调参。
4.4 维度报错:一个高效的 debug 顺序
“shape 不匹配”是初学者遇到最多的报错。我的建议是,不要盯着红色错误信息干瞪眼,而是建立一个系统化的排查顺序:
- 在 forward 函数的第一行打印输入 x.shape;
- 查看模型定义里每个 Linear 层的 in_features/out_features;
- 确认数据集的标签形状是否跟输出层一致;
- 如果是分类任务,检查标签是整数还是 one-hot,交叉熵损失对格式有严格的要求;
- 在 loss.backward() 之前打印一次 logits.shape。
通常五步走完,问题就暴露了。我见过最搞笑的报错,是有人把标签 y 的形状搞成了 [N, 1],而模型输出是 [N],导致交叉熵计算时维度广播出错——这种情况打印一下两个张量的 shape,三秒钟就能定位。
4.5 梯度消失:从线性到深层网络的第一道坎
当你把网络从一层扩展到五层之后,即使有 ReLU 激活,也可能遇到训练缓慢、前期几层权重几乎不更新的情况。原因在于链式法则的连乘效应:梯度从最后一层往前传,每一层都要乘上一个小于 1 的因子,层数一多,前面的梯度就趋近于 0。
万金油手段有四个:使用 ReLU 而不是 Sigmoid(Sigmoid 导数的最大值只有 0.25,连乘几层就没了);给每层做 Batch Normalization;使用残差连接(把输入直接加到输出上,让梯度有一条“高速公路”);合理初始化。对一个入门项目来说,优先保证激活函数选对,其次加 BN,通常就能解决。
5. 进阶方向与应用场景
线性表示这个项目做完之后,你可以顺着几条线继续深挖。这里我给出最值得走的四个方向,它们每一个都能直接对接真实工作需求。
5.1 从线性表示到潜在空间:PCA、自编码器与词向量
线性表示的思想在无监督学习里同样响亮。PCA(主成分分析)本质上是寻找一组正交基,让数据在这组基上的投影方差最大——它找到的就是数据的最佳“线性表示”。很多高维数据经过 PCA 降到二维后,聚类结构一目了然,这就是线性表示降维的威力。
更进一步,自编码器用神经网络把数据压缩到一个低维潜在向量,再从这个向量恢复原始输入。它的中间层就是一种“非线性”的潜在表示。而词向量(比如 word2vec)之所以能实现“king - man + woman ≈ queen”这种惊艳的语义运算,也是因为模型学习到的词嵌入空间具有线性结构——语义关系被编码成了向量之间的线性位移。
5.2 Transformer 与注意力机制中的线性本质
很多人学 Transformer 时被多头注意力绕晕,但如果你把线性表示搞透了,会发现注意力机制的骨架就是线性操作:每个 token 的向量通过三个不同的线性层投影成 Query、Key、Value;注意力分数是 Query 与 Key 的内积;加权求和本质是对 Value 向量做线性组合。所谓“注意力”,不过是在不同的线性子空间里做加权平均。
还有一个特别值得关注的方向:LoRA(Low-Rank Adaptation)微调。大模型微调时不更新全部参数,而是学习两个低秩矩阵 ( A ) 和 ( B ),用 ( BA ) 作为权重增量的近似。为什么低秩就够了?因为大量实验表明,模型在特定任务上的参数更新往往集中在某个低维子空间里——换句话说,任务差异可以用很少的参数维度来表达,这本身就是线性表示理论的胜利。
5.3 图像识别与目标检测里的线性应用
回到热搜词里出现频率很高的“深度学习图像识别”。你可以留意一个细节:几乎所有主流的图像分类模型,比如 ResNet、VGG、EfficientNet,最后几层都是全局平均池化接一个全连接层——这个全连接层就是一个线性分类器。前面的卷积层负责学习非线性特征提取器,最后一步用线性表示做类别划分。这种“非线性特征 + 线性分类头”的设计,是视觉领域经过大量实践沉淀下来的一套范式。
目标检测里的边界框回归分支,本质上也是回归任务,输出的四个数(中心点坐标和宽高)就是线性层的输出,只不过网络结构把特征共享给了分类分支和回归分支。理解了线性表示,你再看那些检测模型的 decode 部分会豁然开朗。
5.4 深度强化学习与线性表示的隐秘联系
最后提一个很多人没意识到的点:强化学习里的价值函数近似,早期用的就是线性函数。经典方法如线性 Q-learning,用特征向量乘以权重来逼近状态价值。后面深度强化学习把近似器换成了神经网络,本质上是用非线性函数替代线性函数,去拟合更复杂的 Q 函数。但很多基础理论和初始化策略仍然保留着线性时代的影子。
如果你之后要研究强化学习,会发现“线性表示”在策略梯度、时序差分这些概念里反复出现。把它作为地基打牢,学那些看似高深的内容时会省下大量时间。
这个项目做到这里,其实已经把深度学习的骨架摸了一遍。我个人经验是:每次接触一个新结构,不管是 CNN、LSTM 还是 Transformer,都要问自己一句“它的线性部分在哪、非线性部分在哪”。这个问题想明白,任何论文里的网络结构图在你的眼里都会变得透明。最后再分享一个我长期保留的小习惯:每次搭新模型,我都会先用全连接层在小规模数据上把流程完整跑通,确认梯度、损失、评估指标都没有问题,再替换成复杂的结构层。这个习惯让我避开了至少一半的 debug 时间,也希望它能帮到你。
