一句话解释
神经网络包含layers/modules,代表了对数据的操作。 任何一个module都是nn.Module的子类。一个神经网络也是一个“包含多个modules”的module。
最小例子
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
# from torchvision import datasets, transforms
device = torch.accelerator.current_accelerator().type if torch.accelerator.is_available() else "cpu"
print(f"Using device: {device}")
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28*28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10)
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
model = NeuralNetwork().to(device)
print(model)
X = torch.rand(1, 28, 28, device=device)
logits = model(X)
print(logits)
pred_probab = nn.Softmax(dim=1)(logits)
print(pred_probab)
y_pred = pred_probab.argmax(1)
print(f"Predicted class: {y_pred}")
注意:forward不能直接用,传入data的时候会自动执行。
(repytorch) alpaca@Mac repytorch % python tutorials/official_basics/04_build-model/build-model.py
Using device: mps
NeuralNetwork(
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear_relu_stack): Sequential(
(0): Linear(in_features=784, out_features=512, bias=True)
(1): ReLU()
(2): Linear(in_features=512, out_features=512, bias=True)
(3): ReLU()
(4): Linear(in_features=512, out_features=10, bias=True)
)
)
tensor([[-0.0046, -0.0692, 0.0140, 0.0731, 0.0015, -0.0604, 0.0042, -0.0252,
-0.0017, -0.0183]], device='mps:0', grad_fn=<LinearBackward0>)
tensor([[0.1003, 0.0941, 0.1022, 0.1084, 0.1009, 0.0949, 0.1012, 0.0983, 0.1006,
0.0990]], device='mps:0', grad_fn=<SoftmaxBackward0>)
Predicted class: tensor([3], device='mps:0')
这段代码完成的是一次前向传播:输入图片数据,经过各层计算,得到类别分数、概率和预测类别。
先注意:模型还没有训练,输入也是随机数,所以输出的类别 3 暂时没有识别意义。 它只是说明整条计算流程能够运行。
整个过程中,张量形状这样变化:
| 步骤 | 输出形状 | 含义 |
|---|---|---|
X |
(1, 28, 28) |
1 个样本,每个有 28×28 个数 |
Flatten |
(1, 784) |
每个样本展开为 784 个数 |
Linear(784, 512) |
(1, 512) |
每个样本得到 512 个特征 |
ReLU |
(1, 512) |
逐元素处理,形状不变 |
Linear(512, 512) |
(1, 512) |
进一步变换特征 |
ReLU |
(1, 512) |
形状不变 |
Linear(512, 10) |
(1, 10) |
每个样本得到 10 个类别分数 |
Softmax(dim=1) |
(1, 10) |
将类别分数转换成概率形式 |
argmax(1) |
(1,) |
每个样本得到一个预测类别编号 |
| 先从输入看: |
X = torch.rand(1, 28, 28, device=device)
torch.rand() 创建一个形状为 (1, 28, 28) 的张量,里面是 [0, 1) 范围内的均匀随机浮点数,并直接放到指定设备上。这里就是 MPS。torch.rand 文档
这相当于生成一张随机噪声“图片”,用于测试模型。
这里第一个 1 被模型代码当作 batch size,也就是样本数量:
(1, 28, 28)
↑ ↑ ↑
数量 高度 宽度
它没有显式写出通道维。实际 FashionMNIST 经 DataLoader 组成 batch 后,通常是 (N, 1, 28, 28);这个模型的 Flatten 也能处理这种输入。
Questions
flatten是什么?
self.flatten = nn.Flatten()
Flatten 就是“展平”:将多个维度合并成一个维度,保留数值及其逻辑顺序。
例如:
输入,shape=(1, 2, 2):
[
[
[1, 2],
[3, 4]
]
]
Flatten 后,shape=(1, 4):
[
[1, 2, 3, 4]
]
nn.Flatten() 默认是:
nn.Flatten(start_dim=1, end_dim=-1)
表示从第 1 维到最后一维全部合并,保留第 0 维。因此:
(1, 28, 28) → (1, 784)
如果输入 32 张图片:
(32, 1, 28, 28) → (32, 784)
它不会把不同样本混在一起,也没有需要训练的参数。Flatten 文档
nn.Sequential()是什么?
nn.Sequential() 把多个网络层串成一条流水线:
self.linear_relu_stack = nn.Sequential(
nn.Linear(784, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10),
)
执行:
logits = self.linear_relu_stack(x)
就会按定义顺序计算:
x → Linear → ReLU → Linear → ReLU → Linear → logits
前一层的输出自动成为后一层的输入。它还会把这些子层注册到模型中,方便统一管理参数、迁移设备。Sequential 文档
nn.Linear()是什么?
打印出的:
(0): Linear(...)
(1): ReLU()
...
(4): Linear(...)
其中 0~4 是容器里的层编号。
其中的 nn.Linear() 是全连接层:
nn.Linear(784, 512)
含义是:把每个样本的 784 个输入特征,通过可学习的加权组合,转换成 512 个输出特征。 每个输出都类似这样计算:
某个输出 = w₁×输入₁ + w₂×输入₂ + … + w₇₈₄×输入₇₈₄ + b
w是权重;b是偏置;- 不同输出有各自的权重和偏置。 因此,512 个输出不是简单地从 784 个输入中选出 512 个,而是计算出 512 个新的组合特征。 批量计算可以表示为:
Y = X @ W.T + b
这层的参数形状是:
layer.weight.shape # (512, 784)
layer.bias.shape # (512,)
权重和偏置在创建网络时初始化,之后通过训练更新。打印中的 bias=True 表示这一层使用偏置。Linear 文档
512 是设计者选择的隐藏层宽度;最后一层的 10 对应 FashionMNIST 的 10 个类别。
nn.ReLU()呢?
nn.ReLU() 则对每个元素执行:
ReLU(x) = max(0, x)
例如:
输入:[-2, -0.5, 0, 1, 3]
输出:[ 0, 0, 0, 1, 3]
负数变为零,正数保持不变,张量形状不变。
它为网络引入非线性。如果只连续堆叠 Linear 层,整体仍然可以合并成一个仿射变换;加入 ReLU 后,网络才能表达更复杂的输入与输出关系。ReLU 文档
这些层在 __init__() 中创建,而实际的数据流向由 forward() 定义:
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
调用:
logits = model(X)
时,PyTorch 会执行这段前向计算。
softmax具体是怎样计算的?
Softmax 的计算过程是:先对每个类别分数取指数,再除以所有类别指数值的总和,得到总和为 1 的概率分布。
假设模型输出的 logits 为:
logits = torch.tensor([2.0, 1.0, 0.0])
对于第 $$i$$ 个类别,Softmax 的公式是: $$ p_i=\frac{e^{z_i}}{\sum_{j=1}^{C}e^{z_j}} $$ 其中,$$z_i$$ 是该类别的 logit,$$C$$ 是类别数,$$e\approx2.71828$$ 具体计算如下:
- 对每个分数取指数: $$ [e^2,e^1,e^0]\approx[7.389,2.718,1] $$
- 将指数值相加: $$ S=7.389+2.718+1=11.107 $$
- 每个指数值除以总和: $$ \left[ \frac{7.389}{11.107}, \frac{2.718}{11.107}, \frac{1}{11.107} \right] \approx[0.6652,0.2447,0.0900] $$ 结果对应: | 类别 | 原始 logit | Softmax 概率 | |—|—:|—:| | 0 | 2.0 | 约 66.52% | | 1 | 1.0 | 约 24.47% | | 2 | 0.0 | 约 9.00% |
这些概率相加约等于 1,微小差异来自显示时的四舍五入。
在 PyTorch 中:
import torch
logits = torch.tensor([2.0, 1.0, 0.0])
probabilities = torch.softmax(logits, dim=0)
print(probabilities)
# tensor([0.6652, 0.2447, 0.0900])
为什么先取指数? 因为 logits 可以是负数,而指数会将它们转换成正数,并保留大小顺序:原来分数最大的类别,转换后概率仍然最大。
指数还让分数差距影响概率比例:
$$
\frac{p_i}{p_j}=e^{z_i-z_j}
$$
例如两个类别的 logit 相差 1,它们的 Softmax 概率之比就是 $$e^1\approx2.718$$
之前的代码中:
logits.shape
# (1, 10)
pred_probab = nn.Softmax(dim=1)(logits)
dim=1 表示沿着类别维度计算。对于每张图片,分别对它的 10 个分数执行“取指数、求和、相除”:
类别维 dim=1
─────────────────→
样本0 [10个分数] → [10个概率,总和为1]
样本1 [10个分数] → [10个概率,总和为1]
……
输出形状保持不变,仍是 (样本数量, 类别数量)。
实际数值计算时,常使用一个等价形式,避免指数值过大:
$$
p_i=\frac{e^{z_i-m}}{\sum_j e^{z_j-m}},
\qquad m=\max_j z_j
$$
也就是先减去最大的 logit。例如:
[1002, 1001, 1000]
↓ 减去1002
[ 0, -1, -2]
两组的 Softmax 结果完全相同,但后一组计算指数时更稳定。原因是共同的因子 $$e^{-m}$$ 会在分子、分母中约掉。日常代码直接使用 torch.softmax() 或 nn.Softmax() 即可。
为什么要用softmax?
Softmax 适合你现在这种“多个类别中选一个”的任务:它把模型输出的任意实数分数,转换为一个概率分布,方便解释预测,并配合损失函数训练模型。
它主要有以下几个优点。
-
把任意分数转换成合法的概率形式。 logits 可以有正数、负数,也不要求总和为 1:
logits:[2.0, 1.0, 0.0]Softmax 后:
概率:[0.6652, 0.2447, 0.0900]每个值都非负,总和为 1,可以用来表达模型对各类别的概率分配。Softmax 官方文档 单纯将 logits 除以总和无法保证这一点,因为可能出现负数,甚至总和为零。
-
让互斥类别之间形成竞争。 FashionMNIST 中,一张图片只有一个正确类别,所以可以把总共
100%的概率分配给 10 个类别。 在其他 logits 不变时,提高某个类别的分数,会提高它的概率,同时降低其他类别的概率。 这适合“猫、狗、鸟三选一”。如果任务允许多个标签同时成立,例如“戴眼镜”和“戴帽子”,通常使用逐标签的 Sigmoid,不要求所有标签概率加起来为 1。 -
可微,可以配合反向传播训练。 如果直接取最大值所在的类别:
predicted_class = logits.argmax()得到的只有离散编号,无法通过这个编号给模型提供通常的梯度信号。 Softmax 则会随着 logits 平滑变化,可以参与梯度计算。尤其与交叉熵结合时,梯度形式非常简洁。 对于一个样本,设:
- (z_i):第 (i) 类的 logit;
- (p_i):Softmax 概率;
- (y_i):one-hot 目标值。 那么普通交叉熵损失满足: [ \boxed{\frac{\partial L}{\partial z_i}=p_i-y_i} ] 例如,正确类别是第 0 类:
预测概率:[0.2, 0.7, 0.1] 正确目标:[1.0, 0.0, 0.0] 梯度: [-0.8, 0.7, 0.1]梯度下降会推动正确类别的分数提高,错误类别的分数降低。这里的 one-hot 用于说明数学公式;PyTorch 可以直接接收整数类别标签。
-
只关注类别分数之间的相对差异。
[2, 1, 0] [102, 101, 100]两组分数的 Softmax 结果相同。给所有类别增加同一个值,不会改变概率;类别之间的差距才影响结果。 这符合分类的需求:我们关心模型更偏向哪个类别,以及偏向多少。 不过,只想知道预测类别时,不需要计算 Softmax。因为它保留分数的大小顺序:
logits.argmax(dim=1)
与:
torch.softmax(logits, dim=1).argmax(dim=1)
结果相同。 在你的代码里,可以根据用途分别写:
logits = model(X)
# 获取预测类别
y_pred = logits.argmax(dim=1)
# 需要查看各类别概率时
probabilities = torch.softmax(logits, dim=1)
# 训练时,直接把 logits 交给交叉熵
loss = nn.CrossEntropyLoss()(logits, labels)
CrossEntropyLoss 已经包含等价于 LogSoftmax + NLLLoss 的计算,因此训练时不用先手动执行 Softmax。交叉熵官方文档
另外,Softmax 输出的 0.9 表示模型给该类别分配了 90% 的概率;它是否与真实正确率一致,还取决于模型训练和概率校准情况。
logits和preb_probab的tensor第一项为什么是一个只有1项的二维数组,而不直接用一位数组?第三项的grad_fn又是什么含义?
tensor([[-0.0046, -0.0692, ..., -0.0183]],
device='mps:0',
grad_fn=<LinearBackward0>)
其中:
[[...]]是实际数值;device=...是设备信息;grad_fn=...是自动求导信息。 后面两项都是张量的描述信息,并不是张量中额外的元素。 这里的[[...]]表示形状为(1, 10):1 行、10 列,共有 10 个元素。外层长度为 1,表示这里只有一个样本:
[
[样本0的10个类别分数]
]
如果输入 3 个样本:
X = torch.rand(3, 28, 28, device=device)
输出就会是:
[
[样本0的10个类别分数],
[样本1的10个类别分数],
[样本2的10个类别分数]
]
形状为 (3, 10)。保留 batch 维,可以让同一套代码处理一个或多个样本。
如果只是想查看单个样本的一维结果,可以写:
print(logits[0]) # shape=(10,)
# 或
print(logits.squeeze(0))
之后的:
pred_probab = nn.Softmax(dim=1)(logits)
可以拆成:
softmax = nn.Softmax(dim=1) # 创建操作
pred_probab = softmax(logits) # 应用到 logits
dim=1 是类别维,因此它分别把每行的 10 个分数转换成总和为 1 的概率形式。
你的结果中,最大概率是:
0.1084 → 索引3 → 约10.84%
于是:
y_pred = pred_probab.argmax(1)
# tensor([3], device='mps:0')
argmax(1) 沿每一行找最大值的位置,每个样本返回一个类别编号。因此一个样本得到形状为 (1,) 的结果。需要 Python 整数时可以用:
y_pred.item()
# 3
这不是说模型达到了 10.84% 的准确率,而是它给当前输入的类别 3 分配了约 10.84% 的概率。由于模型未经训练、输入又是噪声,这些值暂时只用于理解计算过程。
最后,grad_fn 表示产生这个张量的操作所对应的反向传播节点:
logits:
grad_fn=<LinearBackward0>
↑
最后一步来自 Linear
pred_probab:
grad_fn=<SoftmaxBackward0>
↑
最后一步来自 Softmax
PyTorch 记录这些连接,是为了之后执行:
loss.backward()
时,沿计算图反向计算模型参数的梯度。自动求导教程
它并不表示已经计算好了梯度。即使 X 默认不要求梯度,Linear 中的权重和偏置默认需要梯度,所以输出仍然会携带计算历史。
如果只想做推理、不记录求导过程,可以写:
with torch.no_grad():
logits = model(X)
pred_probab = nn.Softmax(dim=1)(logits)
这次生成的结果就不会带有 grad_fn。
全链接层就是nn.Linear()吗?隐藏层,池化层又是什么呢?关于模型的各种“层”你有没有更详细的解释?
是的,PyTorch 中通常用 nn.Linear() 实现全连接层(fully connected layer,也叫 Dense 层)。
你后面的问题涉及两件事:网络由哪些计算模块组成,以及为什么这些简单模块组合起来就能表达复杂关系。
先把几个名称区分开:
| 名称 | 描述什么 |
|---|---|
| 输入层、隐藏层、输出层 | 这一层在网络中的位置 |
| 全连接层、卷积层、池化层 | 这一层执行什么操作 |
| 激活函数 | 如何给计算加入非线性 |
所以,“隐藏层”和“全连接层”可以同时描述同一层:它位于网络中间,并且执行全连接计算。 你的网络可以这样理解:
输入:784 个像素值
↓
隐藏层:Linear(784, 512) + ReLU
↓
隐藏层:Linear(512, 512) + ReLU
↓
输出层:Linear(512, 10)
↓
10 个类别的 logits
“隐藏”是因为这些中间特征没有直接提供的标准答案。训练数据只告诉模型“输入是什么、正确类别是什么”,中间的 512 个特征由训练过程自己学习,并不是人为规定“第一个代表袖子,第二个代表领口”。 按常见的计数方式,这是一个有两个隐藏层、三个全连接层的网络。PyTorch 打印时会把 ReLU 等模块也单独列出,所以“模块数量”和口头所说的“网络层数”可能不同。 全连接层的“全连接”,指的是:每一个输出,都有连接到全部输入的权重。 例如:
nn.Linear(3, 2)
有三个输入、两个输出:
y₁ = w₁₁x₁ + w₁₂x₂ + w₁₃x₃ + b₁
y₂ = w₂₁x₁ + w₂₂x₂ + w₂₃x₃ + b₂
这里有 6 个权重、2 个偏置。训练会调整这些参数。
严格说,带偏置的 Wx+b 叫“仿射变换”;深度学习里通常仍把这种模块称为 Linear 层。nn.Linear 文档
其他常见层,可以先按它们的工作来认识:
| 层或模块 | 主要作用 | 通常有可学习参数吗? |
|---|---|---|
全连接层 Linear |
综合全部输入特征,生成新的特征 | 有 |
卷积层 Conv2d |
用共享的小窗口扫描图像,提取局部特征 | 有 |
池化层 MaxPool2d、AvgPool2d |
汇总局部区域,常用于降低空间尺寸 | 没有 |
激活层 ReLU、GELU 等 |
引入非线性 | 多数没有 |
归一化层 BatchNorm、LayerNorm |
调整中间特征的数值分布,帮助训练 | 默认通常有缩放、偏移参数 |
Dropout |
训练时随机将部分特征置零,减少过拟合 | 没有 |
Flatten |
合并维度,调整形状 | 没有 |
Embedding |
把词元等离散编号转换成特征向量 | 有 |
| 注意力模块 | 根据内容计算不同位置之间如何交换信息 | 通常有 |
这些模块都是处理 Tensor 的不同方式;并非每一层都需要学习参数。PyTorch nn 模块目录 |
||
| 池化层可以用一个小例子理解。假设某个通道的特征图是: |
1 2 │ 3 4
5 6 │ 7 8
───────┼───────
9 10 │ 11 12
13 14 │ 15 16
执行:
nn.MaxPool2d(kernel_size=2, stride=2)
每个 2×2 小区域只保留最大值:
6 8
14 16
空间尺寸从 4×4 变成 2×2。对于:
(N, C, 28, 28)
同样的操作会得到:
(N, C, 14, 14)
每个样本、每个通道分别处理,通道数量不变。AvgPool2d 则取小区域的平均值。池化会丢失部分细节,换来更小的特征图和更少的后续计算;是否使用取决于任务。MaxPool2d 文档
为什么非线性要用ReLU表达,是单纯看创造者的喜好吗?这一类是不是有一类专有名词来统一称呼这类函数?这类函数在使用时如何选择?
再来看非线性。ReLU 属于一大类叫作**激活函数(activation functions)**的函数,选择它有实际理由,并不是单纯的个人喜好。 常见激活函数包括:
| 激活函数 | 大致行为 | 常见用途或特点 |
|---|---|---|
| ReLU | max(0, x) |
简单、计算便宜,常作为基础网络的起点 |
| LeakyReLU | 负数乘一个小系数,正数不变 | 让负数区域也保留梯度 |
| GELU | 平滑地调节输入 | 常用于 Transformer 中的前馈网络 |
| SiLU / Swish | x × sigmoid(x) |
一种平滑激活函数 |
| Sigmoid | 将数值映射到 (0, 1) |
概率输出、门控 |
| Tanh | 将数值映射到 (-1, 1) |
有界特征、某些循环网络 |
PyTorch 提供了这些激活函数;例如 TransformerEncoderLayer 支持选择 ReLU 或 GELU。激活函数目录,TransformerEncoderLayer 文档 |
||
| 选择时主要考虑三个因素: |
- 能否容易训练:反向传播时,梯度是否容易变得太小或太大。
- 计算成本:训练和推理是否足够快。
- 实际效果:在验证集上是否提高了效果。 ReLU 的优点很直观:
x > 0:输出等于 x,导数为 1
x < 0:输出为 0,导数为 0
正数区域不会像 Sigmoid 的饱和区域那样,把局部梯度压得非常小。不过 ReLU 也有缺点:如果某个单元长期对所有输入都处于负数区域,它可能一直输出零、得不到更新。这是考虑 LeakyReLU 等替代方案的一个原因。ReLU 的使用也有长期的实验研究基础。Deep Sparse Rectifier Neural Networks 对你目前的 FashionMNIST 网络,先用 ReLU 很合适。掌握完整训练流程后,可以保持其他设置一致,比较 ReLU、GELU 等在验证集上的表现。如果使用现成的预训练模型,通常沿用其原有激活函数。 输出层则需要结合任务与损失函数决定。例如你现在使用的多分类模型:
nn.Linear(512, 10)
直接输出 logits 即可,训练时交给 CrossEntropyLoss;需要展示概率时,再使用 Softmax。不要给每个输出层都习惯性地加 ReLU,否则会限制它能够输出的数值。
为什么不用更复杂的函数构造模型?而是只用(某个输出 = w₁×输入₁ + w₂×输入₂ + … + w₇₈₄×输入₇₈₄ + b)的形式?更复杂的函数不会能表示更丰富的特征吗?
网络整体已经是复杂函数,即使单个模块很简单。
你的模型实际上计算的是:
$$
h_1=\operatorname{ReLU}(W_1x+b_1)
$$
$$
h_2=\operatorname{ReLU}(W_2h_1+b_2)
$$
$$
\text{logits}=W_3h_2+b_3
$$
把它们合在一起就是:
$$
f(x)=W_3\operatorname{ReLU}
\left(W_2\operatorname{ReLU}(W_1x+b_1)+b_2\right)+b_3
$$
它已经超出了单个 Wx+b 的表达能力。
如果没有激活函数,两层确实可以合并:
$$
W_2(W_1x+b_1)+b_2
=(W_2W_1)x+(W_2b_1+b_2)
$$
结果仍然只是一个仿射变换。加入 ReLU 后,不同输入会使不同单元开启或关闭,网络会在不同区域采用不同的计算关系。
甚至只用三个 ReLU,就能构造一个三角形函数。令:
r(x) = max(0, x)
构造:
f(x) = r(x+1) - 2r(x) + r(x-1)
得到:
x |
-2 | -1 | -0.5 | 0 | 0.5 | 1 | 2 |
|---|---|---|---|---|---|---|---|
f(x) |
0 | 0 | 0.5 | 1 | 0.5 | 0 | 0 |
| 这可以用下面的结构实现: |
Linear(1, 3) → ReLU → Linear(3, 1)
不同的权重和偏置,可以改变这些折点的位置、斜率和高度。组合更多这样的单元,就能构造很多段折线,在有限区间内逼近 x²、正弦曲线等复杂函数。组成模型的函数简单,并不意味着组合后的模型简单。
当然,也可以直接使用平方、指数、对数或其他函数。实际模型中确实存在这些运算,例如 Sigmoid 使用指数,交叉熵使用对数。只是函数公式更复杂,不会自动保证模型更好:
exp(x)在输入较大时增长很快,数值和梯度可能过大。log(x)要求输入为正,而Wx+b可能产生负数。- 高次幂的导数如
10x⁹,可能在某些范围很小、另一些范围很大,让优化更困难。 - 某些复杂运算会增加计算成本,却未必改善验证集效果。
加权求和有大量可学习参数,又能高效地用矩阵乘法实现;搭配合适的非线性,通常能取得表达能力、训练难度和计算成本之间的良好平衡。
如果问题本身明确包含某种规律,例如物理过程中的周期关系、指数衰减,那么把
sin()、exp()等结构加入模型也可能很有价值。选择模型结构的依据,是它能否学到任务需要的关系,并且稳定、高效地训练出来。
可以用微分思想来理解神经网络吗?
可以理解为用微分思想的一种延伸吗?只要分的够小就全都能当作直线处理?然后就能进行线性回归?这方面有没有什么证明推导,我是说有没有一门专门的分支是用来做神经网络方向数学推导的,从而可以得到严谨的证明?
可以用“用许多小段直线逼近曲线”来建立直觉。它与微分中的局部线性近似有关,但更直接对应的数学方向是函数逼近、分段线性插值和样条理论。
不过,“然后就能进行线性回归”还需要区分:整体是分段线性的,不代表训练整个网络就是线性回归。
先看它和微分的联系。如果函数在 $$x_0$$ 处可微,那么在附近:
$$
f(x_0+\Delta x)
\approx f(x_0)+f’(x_0)\Delta x
$$
这就是用切线近似曲线。
而你的 Linear + ReLU 网络具有一个更具体的性质:在激活状态保持不变的每个区域内,它本身就精确地是一个仿射函数。 一维情况下是许多直线段,高维情况下是许多区域内的仿射映射。不同区域拼起来,整体就能表现出复杂的非线性。神经网络逼近综述
这里讨论的是经过 Softmax 之前的网络输出;再接上 Softmax 后,概率输出通常就不再是分段线性的了。
你问“有没有推导”,可以从一个能完整算出来的例子开始:用折线逼近 $$f(x)=x^2$$。
假设只研究区间 $$[0,1]$$,把它分成 $$N$$ 个等长的小区间,每段长度:
$$ h=\frac{1}{N} $$
在任意一段 $$[u,v]$$ 上,用连接两端点 $$(u,u^2)$$、$$(v,v^2)$$ 的直线近似。这条直线的表达式是:
$$ \ell(x)=(u+v)x-uv $$
因为代入 $$x=u$$ 和 $$x=v$$,分别得到 $$u^2$$ 和 $$v^2$$。
直线与原函数的误差是:
$$ \ell(x)-x^2 =(x-u)(v-x) $$
由于 $$x$$ 位于 $$[u,v]$$ 中,误差在中点最大,所以:
$$ 0\le \ell(x)-x^2 \le \frac{(v-u)^2}{4} =\frac{h^2}{4} $$
整个区间上的最大误差因此满足:
$$ \boxed{ \max_{x\in[0,1]}|s(x)-x^2| \le \frac{1}{4N^2} } $$
这里 $$s(x)$$ 是拼接出来的折线函数。例如:
| 分段数 $$N$$ | 最大误差上界 |
|---|---|
| 10 | 0.0025 |
| 100 | 0.000025 |
| 1000 | 0.00000025 |
这就严格证明了:对于这个函数,分段越细,折线可以逼近得越准确。
接下来还要证明:ReLU 网络能够表示这些折线。
令:
$$ r(x)=\operatorname{ReLU}(x)=\max(0,x) $$
那么:
$$ c,r(x-t) $$
有一个很方便的作用:
- 在 $$x<t$$ 时,贡献为零;
- 在 $$x>t$$ 时,给函数的斜率增加 $$c$$。
所以可以把 $$t$$ 看作折点,把 $$c$$ 看作“经过这个折点后,斜率改变多少”。
一维连续分段线性函数可以写成:
$$ s(x)=a+bx+\sum_{k=1}^{K}c_k\operatorname{ReLU}(x-t_k) $$
其中:
$$ a+bx $$ 给出起始直线; $$ t_k $$ 决定折点位置; $$ c_k $$ 决定各折点处的斜率变化。
这正是“仿射变换 → ReLU → 加权求和”的结构。表达式中的 $$x$$ 也可以用:
$$ x=\operatorname{ReLU}(x)-\operatorname{ReLU}(-x) $$
表示,因此整个函数确实能由普通 ReLU 网络实现。
于是得到了一条完整的证明路线:
函数可以被折线准确逼近
↓
折线可以由 ReLU 网络精确表示
↓
ReLU 网络可以准确逼近该函数
这个思路还能推广。对于闭区间上的连续函数,即使它不处处可微,也能通过足够细的线性插值做到任意精度的逼近。这里起作用的数学性质是“一致连续性”,所以并不一定要依赖导数。
但“所有函数、所有位置都能这样逼近”就说得太宽了。例如,连续的 ReLU 网络不能在包含跳跃点的整个区间上,以任意小的最大误差逼近一个跳跃函数;必须明确函数类型、研究区域以及误差如何衡量。
至于线性回归,关键在于:哪些东西固定,哪些东西需要学习。
如果先固定这些特征:
$$ \phi_k(x)=\operatorname{ReLU}(x-t_k) $$
只学习最后的系数:
$$ \hat y=a+\sum_k c_k\phi_k(x) $$
那么配合平方误差,这确实可以作为对固定特征做线性回归。它对原始输入 $$x$$ 可以是非线性的,但对待学习系数 $$a,c_k$$ 是线性的。
普通神经网络还会学习: $$ \operatorname{ReLU}(w_kx+b_k) $$ 中的 $$w_k,b_k$$,甚至多层之间所有的参数。这意味着网络连“使用哪些特征、在哪里改变计算关系”都在学习,整个优化问题通常是非凸的。 另外,你正在学习的是分类任务,使用交叉熵损失,也不能直接称为线性回归。 你说的那种专门做严谨推导的方向,确实存在,通常叫: 深度学习理论(Deep Learning Theory)或深度学习的数学基础(Mathematical Foundations of Deep Learning)。 它结合了多个数学分支,最核心的三个问题是:
| 研究问题 | 对应方向 | 典型问题 |
|---|---|---|
| 网络能表示什么? | 逼近理论、表达能力理论 | 多大网络才能把误差降到某个程度?深度有什么作用? |
| 参数能否学出来? | 优化理论 | 梯度下降是否收敛?需要哪些条件? |
| 学出来后能否处理新数据? | 统计学习理论、泛化理论 | 需要多少样本?为什么会过拟合? |
| 这些方向都有严格定理和证明,但结论通常带有明确条件;目前也没有一个定理能完整解释所有实际深度网络的表现。深度学习数学理论综述 | ||
| 其中,与你现在的疑问最直接相关的是通用逼近定理(Universal Approximation Theorem)。一个适用于 ReLU 网络的版本可以表述为: |
对于紧集 $$K$$ 上的连续函数 $$f$$,以及任意误差要求 $$\varepsilon>0$$,存在一个足够宽的、带偏置和线性输出层的单隐藏层 ReLU 网络 $$F$$,使得
$$ \sup_{x\in K}|F(x)-f(x)|<\varepsilon. $$
它证明的是“存在这样的网络”。训练算法能否找到这些参数、需要多少计算、能否从有限样本推广到新数据,还需要另外的理论。《Deep Learning》第 6 章
按你现在的学习进度,可以沿着这条顺序深入:
- 线性代数、多元微积分、概率与优化基础:《Mathematics for Machine Learning》。
- 前馈网络、反向传播和通用逼近:《Deep Learning》第 6 章。MIT课本
- 网络规模与逼近误差的严格证明:Yarotsky:Error bounds for approximations with deep ReLU networks,这篇会明显更偏数学研究。
仍不确定的问题
上述数学理论
Conversation
评论
欢迎交流这篇文章。评论公开保存在 GitHub Discussions,发表前需要登录 GitHub。