← 全部文章

技术笔记 · Note

Build Model

搭建一个基础模型需要哪些步骤,有哪些注意事项

6,118 字

一句话解释

神经网络包含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$$ 具体计算如下:

  1. 对每个分数取指数: $$ [e^2,e^1,e^0]\approx[7.389,2.718,1] $$
  2. 将指数值相加: $$ S=7.389+2.718+1=11.107 $$
  3. 每个指数值除以总和: $$ \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 适合你现在这种“多个类别中选一个”的任务:它把模型输出的任意实数分数,转换为一个概率分布,方便解释预测,并配合损失函数训练模型。

它主要有以下几个优点。

  1. 把任意分数转换成合法的概率形式。 logits 可以有正数、负数,也不要求总和为 1:

    logits:[2.0, 1.0, 0.0]

    Softmax 后:

    概率:[0.6652, 0.2447, 0.0900]

    每个值都非负,总和为 1,可以用来表达模型对各类别的概率分配。Softmax 官方文档 单纯将 logits 除以总和无法保证这一点,因为可能出现负数,甚至总和为零。

  2. 让互斥类别之间形成竞争。 FashionMNIST 中,一张图片只有一个正确类别,所以可以把总共 100% 的概率分配给 10 个类别。 在其他 logits 不变时,提高某个类别的分数,会提高它的概率,同时降低其他类别的概率。 这适合“猫、狗、鸟三选一”。如果任务允许多个标签同时成立,例如“戴眼镜”和“戴帽子”,通常使用逐标签的 Sigmoid,不要求所有标签概率加起来为 1。

  3. 可微,可以配合反向传播训练。 如果直接取最大值所在的类别:

    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 可以直接接收整数类别标签。

  4. 只关注类别分数之间的相对差异。

    [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 章

按你现在的学习进度,可以沿着这条顺序深入:

  1. 线性代数、多元微积分、概率与优化基础:《Mathematics for Machine Learning》。
  2. 前馈网络、反向传播和通用逼近:《Deep Learning》第 6 章。MIT课本
  3. 网络规模与逼近误差的严格证明:Yarotsky:Error bounds for approximations with deep ReLU networks,这篇会明显更偏数学研究。

仍不确定的问题

上述数学理论

View the source Markdown on GitHub ↗

Conversation

评论

欢迎交流这篇文章。评论公开保存在 GitHub Discussions,发表前需要登录 GitHub。

快速去一个地方

⌘ / Ctrl + K 打开 · Esc 关闭