从 y = ax + b 到神经网络:为什么 AI 可以被看作函数逼近

0 阅读26分钟

神经网络理解成“复杂函数逼近”,不是说图像识别、翻译、文本生成背后都藏着一个像 y = 2x + 1 这样漂亮的公式。它更像是在给初学者搭一个入口:只要一个任务能被描述成“给定输入,产生输出”,我们就可以先把它看成某种映射,再讨论这个映射如何由数据学出来。

这个入口很朴素,却足够有力。因为神经网络的第一层直觉正是:不要试图手写所有规则,而是构造一个带大量可学习参数的函数,让数据一点点塑造它。

图像分类、机器翻译和语言模型都可以被看成输入到函数再到输出的映射
图 1:先把智能任务统一写成输入、函数、输出。

先把任务写成 x 到 y 的映射

函数在这里不是高中数学里那种必须写成显式公式的对象,而是一个更宽泛的说法:输入和输出之间存在某种对应关系。这个关系可能清楚,也可能模糊;可能输出一个确定值,也可能输出一组概率;可能输入是一张图片,也可能输入是一串 token

图像分类就是一个典型例子。一张彩色图片在计算机里不是“猫”或者“狗”,而是一个由像素值组成的数组。假设图片大小是 224 x 224,每个像素有 RGB 三个通道,那么输入 x 可以看成一个形状为 224 x 224 x 3 的数字张量。模型 f 接收这个张量,输出的不是一句“我觉得是猫”,而通常是一个概率向量,例如“猫 0.91,狗 0.04,狐狸 0.02,其他类别合计 0.03”。从函数视角看,这就是 f(图像像素) = 类别概率分布

机器翻译也是映射,只是输入输出不再是固定长度的数字表格。输入可能是“我喜欢机器学习”,输出可能是“I enjoy machine learning”。这个函数的难点不在于查词典,而在于它要处理语序、上下文、省略和表达习惯。“我请你吃饭”和“我请你帮忙”里的“请”不是同一种翻译方式,模型必须根据上下文决定输出。

语言模型更适合用概率映射来理解。给定前文 token 序列,例如“今天天气很”,模型不会直接从空气里抽出唯一答案。它会输出下一个 token 的概率分布 的概率可能很高,糟糕 也有机会出现。模型每次生成一个 token,再把新 token 放回上下文里继续预测下一个 token。看起来像是在写句子,本质上是一连串 前文 -> 下一个 token 概率分布 的函数调用。

这个视角能把很多任务放在同一个框架里。输入 x 进入一个带参数的函数 f(x; θ),输出预测值 ŷ。这里的 θ 表示模型里所有可学习参数,ŷ 表示模型当前给出的答案。训练时,数据集会提供目标答案 y,模型则不断调整 θ,让 ŷ 更接近 y

手写规则为什么会撞上墙

早期人工智能有一条自然路线:人先总结规则,再让机器照着执行。这个方法在边界明确、规则稳定的任务里很好用。比如发票金额校验、表单字段验证、棋类游戏中的合法走法判断,都可以写出比较可靠的规则。

图像和自然语言的问题在于,变化空间太大。你可以写一条规则说“猫有尖耳朵”,但猫可以趴着、跳着、蜷成一团,也可能被沙发挡住半张脸。你可以继续补规则,“猫通常有胡须”“猫的眼睛比较圆”“猫的尾巴细长”,很快又会遇到反例:有些图片只露出猫背,有些猫在暗光里,有些毛绒玩具看起来也像猫。规则越写越多,系统却越来越脆弱。

自然语言更麻烦,因为词语含义会被上下文改写。“苹果很甜”里的苹果是水果,“苹果发布了新芯片”里的苹果是公司;“这电影不差”可能是温和肯定,也可能是在委婉批评;“你可真行”在不同语气里甚至能表达相反意思。规则系统如果想覆盖所有同义、歧义、省略和语境,就会像给一张漏水的网不断打补丁。

神经网络的做法不是把这些规则一条条写出来。它先给出一个足够灵活的函数结构,再用大量样本调整参数。人不告诉模型“猫的耳朵角度必须在多少度之间”,而是给它看许多猫和非猫的图片,让训练过程自己找到哪些模式对区分类别有用。

这并不意味着模型“自动理解了世界”。更准确的说法是,它从训练数据里学习了输入和输出之间的统计关系。当训练数据覆盖得足够好,模型就可能在新样本上表现不错;当新样本离训练分布很远,模型也会露出短板。

从 y = ax + b 看懂可学习参数

神经网络的参数听起来抽象,可以先从最简单的线性函数看起:

y = ax + b

这里的 x 是输入,y 是输出,ab 是参数。a 控制输入对输出的影响方向和幅度,也就是直线的倾斜程度;b 控制整条线的上下平移。如果 a 是正数,x 越大,y 通常越大;如果 a 是负数,x 越大,y 反而越小;如果 b 变大,同样的 x 会得到更高的 y。在神经网络里,类似 a 这样的参数通常会被叫作权重

拿“气温预测冰淇淋销量”举例。x 可以是当天最高气温,y 可以是门店卖出的冰淇淋数量。天气越热,销量通常越高,所以 a 大概率是正数。即使气温是 0 度,门店也可能卖出少量冰淇淋,因此 b 可以理解成某种基础销量。真实世界当然不会这么整齐,周末、门店位置、促销活动都会影响结果,但这个例子足够说明参数的意义:函数形状由参数决定。

线性函数 y = ax + b 中 a 控制斜率,b 控制平移
图 2:a 改变直线倾斜程度,b 改变直线位置。

训练的第一层直觉也在这里出现。传统程序由人写死 ab,而机器学习让数据推动它们变化。模型一开始随便猜一组参数,用这组参数做预测,发现预测和真实答案之间有误差,就沿着能让误差变小的方向微调参数。反复多次以后,那条线会逐渐贴近数据的趋势。

当输入不止一个数时,可以先把它理解成“多个输入各自乘上一个系数,再加起来”:

y = a1x1 + a2x2 + ... + anxn + b

这里的 x1x2xn 是不同输入特征,a1a2an 是对应的可学习系数,b 仍然负责整体平移。图像、文本、语音进入模型以后,都会先被表示成数字向量或张量。线性层的工作就是把这些数字重新加权、组合、平移,生成下一层要处理的新表示。

神经元是一小段可调的计算流程

一个神经元可以看成一小段可调计算。它先把多个输入按不同系数加起来,再把这个中间结果交给下一步处理:

z = a1x1 + a2x2 + ... + anxn + b

这个神经元本身并不神秘,它只是一个带参数的小函数。许多神经元并排放在一起,就得到一层;多层堆叠起来,就得到一个神经网络。先记住这一点:神经网络不是一条公式突然变聪明,而是很多个小函数接力,把输入一步步改写成更容易判断的表示。

这时模型不再只有一个 a 和一个 b,而是有很多可学习系数和偏置。第一层可能把原始输入变成一些低级特征,后面的层继续组合这些特征,形成更抽象的表示。在图像任务中,早期层可能对边缘、方向、颜色块比较敏感,中间层可能组合出纹理或局部形状,后面层才把这些线索汇总成“像猫”或“不像猫”的判断。这个描述是直觉层面的,不是说每一层都能被人清楚命名,但它有助于理解“多层”为什么有意义。

在文本任务里,类似过程发生在 token 表示上。模型先把 token 变成向量,再通过多层计算让向量不断吸收上下文信息。比如“苹果”这个 token,刚进入模型时只是一个初始向量;经过上下文处理后,它在“苹果很甜”和“苹果发布芯片”里的表示会朝不同方向变化。

先让直线弯起来,再理解隐藏层和激活函数

猫狗分类里经常会遇到这种情况:只看两个特征时,一条直线分不开两类样本。模型需要先制造一些中间判断,再把这些判断组合起来,最后形成弯曲的分类边界。

假设我们给图片提了两个很粗糙的特征:x1 表示耳朵尖不尖,x2 表示脸部轮廓圆不圆。有些猫耳朵尖、脸也圆,很容易判断;有些狗也有尖耳朵,某些猫的脸又不明显圆。一条直线只能做一次“一边是猫,一边是狗”的切分,遇到这种交错分布就会别扭。

猫狗分类中,单条直线难以分开交错样本,引入隐藏变量后边界可以弯曲
图 3:样本交错时,单条直线会卡住;隐藏变量让模型能拼出更灵活的边界。

一个自然想法是:别急着直接输出“猫”或“狗”,先引入几个新的中间变量。比如 h1 专门检测“耳朵尖到一定程度了吗”,h2 专门检测“脸部轮廓像不像猫”,h3 专门检测“是否出现胡须和鼻口区域的组合纹理”。这些 h 不在原始数据里,也不是人工标注的答案,而是模型自己在中间层里学出来的信号。

这些中间变量所在的层,就叫隐藏层。它“隐藏”的意思不是神秘,而是它不直接对应输入和标签:训练数据里只有图片和“猫/狗”答案,没有告诉模型 h1h2h3 应该长什么样。模型只能通过最终分类误差,倒推这些中间变量怎么设置更有用。

不过,只引入中间变量还不够。如果 h1 = a1x + b1h2 = a2h1 + b2,最后 y = a3h2 + b3,把式子展开后仍然会变成:

y = ax + b

也就是说,很多层“纯线性计算”叠在一起,最后仍然等价于一条直线。模型看起来变深了,边界却没有真正弯起来。

真正让边界发生弯折的是中间那一步“点亮或压住”的处理。比如某个中间变量算出 z 后,不把 z 原样传下去,而是做一次截断:

h = max(0, z)

如果 z 小于 0,h 就变成 0;如果 z 大于 0,h 才保留下来。它像一个小开关:证据不够时先不发声,证据足够时再把信号传给下一层。这个把中间结果重新处理的函数,就叫激活函数ReLU 是最常见的激活函数之一,sigmoid 会把数值压到 0 到 1 之间,tanh 会把数值压到 -1 到 1 之间。名字可以晚点记,先抓住作用:它让隐藏层不只是搬运数字,而是能把输入空间折出拐点。

输入特征先经过线性组合得到 z,再经过激活函数得到隐藏信号 h
图 4:隐藏层先把输入拆成多个中间问题,激活函数像开关一样决定证据是否继续传递。

可以把分类边界想成模型在输入空间里画的一条线或一个面。单个线性模型只能画直线或平面。现实数据经常不是这样分布的:一类点可能围成圆环,另一类点可能藏在中间;一类句子可能由多个远距离词共同决定情感,不能只看某个关键词。线性层负责搬运和组合信息,隐藏层负责制造中间信号,激活函数负责给这些信号加上“开关”和“拐点”。三者合在一起,神经网络才有能力逼近复杂函数。

把 PyTorch 示例拆成一次可观察的实验

这个例子仍然使用二维点分类。点落在圆内记为 1,圆外记为 0。选择圆形边界不是为了炫技,而是为了制造一个清楚的对照:一层线性模型只能画直线,多层模型加上 ReLU 才能把边界折成近似圆形。

第一步先造数据。每个样本只有两个输入特征,所以它可以画在平面上;标签由半径决定,所以真正的分界线是一圈圆。

import torch

torch.manual_seed(7)

n = 512
x = torch.rand(n, 2) * 2 - 1

radius_squared = x[:, 0] ** 2 + x[:, 1] ** 2
y = (radius_squared < 0.42).float().unsqueeze(1)

这里的 x 是输入,形状是 [512, 2]y 是目标答案,形状是 [512, 1]。如果点接近原点,它更可能在圆内;如果点远离原点,它就在圆外。这个规则人知道,但模型不知道,模型只能通过训练样本去逼近这个映射。

第二步定义模型。nn.Linear(2, 16) 把二维坐标变成 16 个隐藏变量,nn.ReLU() 决定哪些隐藏信号继续往后传。

from torch import nn

model = nn.Sequential(
    nn.Linear(2, 16),
    nn.ReLU(),
    nn.Linear(16, 16),
    nn.ReLU(),
    nn.Linear(16, 1),
)

这段结构可以读成一个函数链条:二维输入先被投影成 16 个中间信号,ReLU 把一部分信号压成 0,后面的线性层继续组合这些信号。最后一层输出一个 logit。logit 不是概率,它可以是任意实数;经过 torch.sigmoid 之后,才会变成 0 到 1 之间的概率。

第三步再训练。损失函数负责衡量“预测和答案差多远”,优化器负责根据梯度改参数。

loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.03)

for step in range(1000):
    logits = model(x)
    loss = loss_fn(logits, y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if step % 200 == 0:
        print(f"step={step:4d}, loss={loss.item():.4f}")

训练循环展示了参数如何被数据推动。model(x)前向传播loss_fn(logits, y) 把预测误差变成一个数,loss.backward()反向传播,计算每个参数对误差的影响,optimizer.step() 根据这些影响更新参数。读这段代码时,不必先记住所有 PyTorch 细节,先看清楚一件事:模型不是被人手写出圆形边界,而是在误差压力下把参数调到能近似圆形边界的位置。

最后可以拿几个点试一下模型当前学到的函数。

with torch.no_grad():
    test_points = torch.tensor([
        [0.0, 0.0],
        [0.4, 0.2],
        [0.9, 0.9],
        [-0.8, 0.1],
    ])
    probabilities = torch.sigmoid(model(test_points))
    print(probabilities)

如果把模型改成只有一层 nn.Linear(2, 1),它只能学到一条直线边界。面对“圆内 vs 圆外”这种数据,它无论怎么调参数都会别扭。加入隐藏层和 ReLU 后,模型可以先生成一批中间变量,再用这些变量拼出近似圆形的区域。这就是“函数逼近”的具体样子:模型不是拿到一个完美圆公式,而是在参数空间里找到了一个能把训练数据分开的近似映射。

如果希望把这次实验真正跑起来,可以把下面的完整脚本保存为 circle_boundary_experiment.py。仓库里也保留了一份同名示例脚本:examples/circle_boundary_experiment.py。它不依赖数据集下载,运行后会打印训练过程中的 loss、accuracy,以及几个测试点被判为“圆内”的概率。

import torch
from torch import nn

# 1. 固定随机种子,保证每次运行时随机生成的数据和初始参数尽量一致。
#    这不是训练必须条件,但写教程和调试时很有用,因为读者能复现相近的输出。
torch.manual_seed(7)


def make_circle_data(sample_count=512, radius_squared_threshold=0.42):
    """生成一个二维圆形分类数据集。

    每个样本是平面上的一个点 (x1, x2)。如果点到原点的距离足够近,
    它就属于圆内类别 1;否则属于圆外类别 0。这里用“半径平方”判断,
    是为了避免额外开平方,让规则更直接。
    """

    # torch.rand(sample_count, 2) 会生成 [0, 1) 区间的随机数。
    # 乘以 2 再减 1 后,坐标范围变成 [-1, 1),点会散布在一个正方形里。
    x = torch.rand(sample_count, 2) * 2 - 1

    # 半径平方 r^2 = x1^2 + x2^2。圆形边界的本质就藏在这个非线性关系里。
    radius_squared = x[:, 0] ** 2 + x[:, 1] ** 2

    # 小于阈值的点标为 1,大于等于阈值的点标为 0。
    # unsqueeze(1) 把形状从 [sample_count] 变成 [sample_count, 1],
    # 这样它能和模型输出的 logits 对齐。
    y = (radius_squared < radius_squared_threshold).float().unsqueeze(1)
    return x, y


class CircleNet(nn.Module):
    """一个很小的多层感知机,用来学习圆形分类边界。"""

    def __init__(self):
        super().__init__()

        # 第一层把二维坐标变成 16 个隐藏变量。
        # 可以把这 16 个变量理解成 16 个“中间问题”,例如:
        # 点是否在左上方、是否靠近某个方向、是否触发某条局部边界。
        self.net = nn.Sequential(
            nn.Linear(2, 16),

            # ReLU 是激活函数。它会把负数压成 0,正数保留下来。
            # 没有这一步,多层线性层叠起来仍然等价于一层线性层,边界很难弯起来。
            nn.ReLU(),

            # 第二层继续组合上一层产生的中间变量,让局部边界可以拼成更复杂的形状。
            nn.Linear(16, 16),
            nn.ReLU(),

            # 最后一层输出一个 logit。logit 不是概率,可以是任意实数;
            # 后面用 sigmoid 才会把它转换成“属于圆内”的概率。
            nn.Linear(16, 1),
        )

    def forward(self, x):
        # PyTorch 会在调用 model(x) 时自动进入 forward。
        return self.net(x)


def accuracy_from_logits(logits, y):
    """把 logit 转成 0/1 预测,并计算分类准确率。"""

    # sigmoid(logit) 得到 0 到 1 之间的概率。
    probabilities = torch.sigmoid(logits)

    # 二分类里常用 0.5 作为阈值:概率 >= 0.5 判为 1,否则判为 0。
    predictions = (probabilities >= 0.5).float()

    # predictions 和 y 形状相同,逐个比较后取平均,就是准确率。
    return (predictions == y).float().mean().item()


def main():
    # 2. 生成训练数据。这里没有 train/test 划分,是为了让示例集中在函数逼近本身。
    #    如果要写成正式实验,应该再生成一份独立测试集。
    x, y = make_circle_data()

    # 3. 创建模型。此时参数是随机初始化的,所以刚开始的预测通常接近乱猜。
    model = CircleNet()

    # 4. BCEWithLogitsLoss 会把 sigmoid 和二分类交叉熵合在一起算。
    #    这样比先 sigmoid 再算 BCELoss 更稳定,也更符合 PyTorch 的常见写法。
    loss_fn = nn.BCEWithLogitsLoss()

    # 5. Adam 优化器根据梯度更新所有可学习参数。
    #    lr 控制每次更新走多远;这个小实验里 0.03 收敛较快。
    optimizer = torch.optim.Adam(model.parameters(), lr=0.03)

    # 6. 训练循环。每一轮都包含前向传播、计算损失、反向传播、更新参数四步。
    for step in range(1001):
        # 前向传播:把所有二维点交给模型,得到每个点对应的 logit。
        logits = model(x)

        # 损失函数:把模型输出和真实标签比较,得到一个需要尽量变小的数。
        loss = loss_fn(logits, y)

        # 清空上一轮留下的梯度。PyTorch 默认会累加梯度,所以每轮训练前要归零。
        optimizer.zero_grad()

        # 反向传播:计算每个参数对当前 loss 的影响方向和大小。
        loss.backward()

        # 参数更新:优化器根据梯度调整权重和偏置。
        optimizer.step()

        if step % 200 == 0:
            # 打印中间结果,方便观察模型是不是在真正学习。
            accuracy = accuracy_from_logits(logits, y)
            print(f"step={step:4d} loss={loss.item():.4f} accuracy={accuracy:.3f}")

    # 7. 用几个手工指定的点检查模型学到的函数。
    #    前两个点靠近圆心,应该更像圆内;后两个点远离圆心,应该更像圆外。
    test_points = torch.tensor([
        [0.0, 0.0],
        [0.4, 0.2],
        [0.9, 0.9],
        [-0.8, 0.1],
    ])

    # 推理阶段不需要梯度,torch.no_grad() 可以减少额外开销。
    with torch.no_grad():
        probabilities = torch.sigmoid(model(test_points))

    print("\n测试点被判为“圆内”的概率:")
    for point, probability in zip(test_points, probabilities):
        x1, x2 = point.tolist()
        print(f"point=({x1:+.1f}, {x2:+.1f}) probability={probability.item():.3f}")


if __name__ == "__main__":
    main()

可视化扩展:用 MNIST 看模型如何学会识别数字

二维点分类适合讲清楚“边界为什么会弯”,MNIST 更适合把同一套函数视角落到手写数字识别上。MNIST 的输入是一张 28 x 28 灰度图,展开后就是 784 个像素值;输出是 0 到 9 十个类别的概率分布。

这个扩展现在不是静态曲线页,而是一个在线交互演示。读者在页面画一个数字,浏览器会把笔画裁剪、缩放、居中成 28 x 28 的灰度输入,然后用内嵌参数做一次本地前向传播,最后输出预测数字和 0 到 9 的概率条。

可视化页面:在线体验手写数字识别演示
演示页面已经内嵌模型参数、样式和交互逻辑,打开链接即可直接使用。

演示里使用的是一个小型 784 -> 96 -> 10 ReLU 神经网络,在 MNIST 测试集上的准确率约为 98.54%。它足够小,可以完整嵌入单个页面,同时比线性 softmax 分类器更能容忍笔画粗细、轻微偏移和不太标准的手写形态。读者打开页面后,识别过程会在浏览器本地完成,手写内容只参与本地计算,不会发送到服务器。

从函数逼近的角度看,MNIST 仍然是同一件事:模型接收 784 个像素值,通过一组可学习参数输出十个数字类别的概率。页面里那张小的 28 x 28 预览图很重要,它提醒读者模型看到的不是“人脑里的数字”,而是一个像素向量。手写得太靠边、太小、太潦草时,预处理后的向量会偏离 MNIST 训练分布,预测也会跟着变差。

函数视角有用,但别把它用过头

函数逼近是入门深度学习的好入口,因为它把任务、模型、参数和训练统一到了一个清楚框架里。可是这个视角也有边界。

首先,神经网络不是在寻找一个永恒正确的显式公式。很多任务没有单一确定答案,尤其是语言生成。同一句开头可以接出不同风格、不同事实密度、不同情绪的文本。模型输出概率分布,比输出唯一答案更贴近实际机制。温度、采样策略、top-k、top-p 这些推理参数会继续影响最终生成的文字。

其次,模型学到的函数依赖训练数据分布。一个猫狗分类器如果只见过白天、高清、正面拍摄的宠物照片,到了夜间监控、医学影像或者卡通图片上就可能失效。不是函数突然坏了,而是输入离它熟悉的区域太远。很多模型错误,本质上都是“训练时支持的映射”被拿到陌生分布上使用。

再次,函数视角还不能解释深度学习的全部问题。表示学习关心模型内部如何组织信息,概率建模关心不确定性如何表达,优化理论关心为什么巨大的参数空间还能被训练到可用状态。理解 Transformer、大语言模型和多模态模型时,这些视角都会继续出现。

所以,“神经网络是函数近似器”应该被当成第一张地图,而不是整本地图册。它帮助我们知道模型在做什么:接收输入,经过参数化计算,输出预测。后面还要继续追问:参数怎样被学出来,数据怎样影响模型,模型为什么会泛化,又为什么会犯错。

下一步:参数从哪里来

这篇文章把神经网络的第一直觉落在了“用可学习函数替代手写规则”上。线性层提供可调参数,隐藏层制造中间变量,激活函数让这些变量产生拐点,多层结构再把简单计算组合成复杂映射。图像分类、机器翻译和语言模型虽然形态不同,都可以先被写成 x -> f(x; θ) -> y

接下来真正的问题是训练。既然参数决定了函数形状,模型就必须知道当前形状哪里不好、应该朝哪个方向改。这个问题会把我们带到损失函数、梯度下降和反向传播:神经网络不是凭感觉变聪明,而是在误差的指引下一步步改参数。

名词说明

名词解释在本文中的作用
神经网络由可学习参数、层和非线性计算组成的函数族。本文讨论的主要模型形式。
复杂函数逼近用参数化模型学习输入到输出的近似映射,而不是手写完整规则。解释 AI 任务为什么能被统一成函数视角。
Token文本被模型处理前切成的离散片段,可以是字、词、子词或符号。说明语言模型的输入输出单位。
概率分布对多个可能结果分别给出概率的一组数值。解释分类和语言生成为什么不是只吐出唯一答案。
参数模型中通过训练更新的数值,常记为 θ决定函数形状,是训练要调整的对象。
权重控制某个输入信号影响方向和幅度的参数。y = ax + b 类比神经网络里的可调系数。
偏置在线性计算中控制整体平移的参数。解释为什么同样输入可以被整体抬高或压低。
线性层对输入做加权求和和平移的网络层。神经网络最基本的信息组合单元。
神经元接收多个输入、做线性组合并传递结果的小计算单元。帮助读者把网络拆成可理解的局部计算。
隐藏层位于输入和输出之间、不直接对应标签的中间层。负责制造模型自己学出的中间信号。
激活函数对线性结果做非线性处理的函数。让多层网络能形成弯曲边界。
ReLU常见激活函数,定义为 max(0, z)作为“证据不足归零,证据足够保留”的例子。
Logit转成概率之前的原始模型输出,可以是任意实数。解释 PyTorch 二分类示例里最后一层的输出。
损失函数衡量预测和真实答案差距的函数。为参数更新提供可优化目标。
优化器根据梯度和更新策略修改参数的算法。让模型沿着误差变小的方向移动。
前向传播输入按模型结构一路计算到输出和损失的过程。对应训练循环里的 model(x)
反向传播从损失出发反向计算参数梯度的方法。对应训练循环里的 loss.backward()
训练分布训练样本覆盖的数据范围和统计规律。说明模型为什么在陌生输入上可能失效。