全书导航
大模型之路:从图灵、感知机到 ChatGPT · 卷 3

第 17 章:神经网络到底在算什么

本章问题:神经网络是不是一个复杂函数?


前两卷讲完了神经网络的历史和深度学习的崛起,但有一个问题一直没有正面展开:一个神经网络,到底在做什么数学运算?这一章用最短的路径回答它。

17.1 从一句话开始

在前两卷中,我们反复提到了"神经网络"这个词。感知机是一个神经元,AlexNet 是一大群神经元组成的网络,反向传播负责调整所有神经元之间的连接。

但有一点一直没有正面展开:一个神经网络,到底在做什么数学运算?

本章用最短的路径回答这个问题。不需要先修任何机器学习的知识——你只要知道什么是乘法、加法,以及函数的概念。

答案很简单,简单到你可能不信:

神经网络就是一个函数。一个把输入向量变成输出向量的参数化函数。


17.2 网络 = 函数

初中学函数:y = f(x),给一个 x,得到一个 y。

神经网络做的是一模一样的事。只不过:

  • 输入 x 不是一个数,而是一组数——一个向量。比如一张 28×28 的灰度图片,可以展平成 784 个数。
  • 输出 y 也可能是一组数。比如 10 个数,分别代表图片是 0 到 9 每个数字的概率。
  • 函数 f 内部不只是一个数学公式,而是由很多"层"叠成的一条流水线。

用更技术一点的语言说:神经网络是一个由一系列可微分的运算层组成的函数。 输入流过每一层,逐步被变换,最终变成输出。

"可微分"这个词是关键,但我们放到后面再展开——它只意味着一件事:你可以算出"每个参数对最终错误贡献了多少",从而能调整它。反向传播(第 8 章)做的事就是这个。


17.3 一层到底长什么样

神经网络的"层"有很多种,但最基础、出现在几乎每一个网络里的,是全连接层,也叫线性层

它是一个矩阵乘法加一个向量加法:

output = input @ weight + bias

如果一个层接收 784 个输入,输出 128 个数,那么——

  • input 是一个长度为 784 的向量
  • weight 是一个 784×128 的矩阵(每一个输入到每一个输出的连接上有一个权重)
  • bias 是一个长度为 128 的向量(每个输出神经元有一个偏置值)
  • output 是一个长度为 128 的向量

每一个输出值是怎么算出来的?

output[j] = input[0]×W[0][j] + input[1]×W[1][j] + ... + input[783]×W[783][j] + b[j]

就是把所有输入做一个加权求和,再加上一个偏移量。这不就是感知机做的事吗?(第 3 章)

没错。一个全连接层的每一个输出神经元,就是一个感知机。

整个神经网络,就是很多层这样的计算叠在一起。但这里有一个容易被忽略的问题——如果每一层都只是"加权求和",叠多少层都没意义。为什么?因为多次线性变换的叠加,在数学上等价于一次线性变换——增加层数没有增加表达能力。

所以需要一个关键组件,插在每一层的加权求和之后——


17.4 激活函数:让直线弯曲

如果每一层都是加权求和,那么无论你叠多少层,整个网络在数学上都只能表示一个线性函数——只能画直线。

现实世界的规律很少是线性的。"如果天阴,带伞概率增加"——线性。"如果天阴且刚被淋过,带伞概率大幅增加"——两个因素产生了交互,不再是简单的线性叠加。

所以需要在每一次加权求和之后插入一个非线性函数。

在数学上,非线性意味着——输出相对于输入的变化,在不同的输入位置会有不同的变化率。它打破了"总变化是各因素变化的简单加权和"的限制。

这个非线性函数被称为激活函数

最经典的激活函数是 ReLU(Rectified Linear Unit):

ReLU(x) = max(0, x)

输入为正?原样通过。输入为负?输出 0。

它可以被直观理解为一个门的角色——对于每一个神经元输出,ReLU 会说:"如果你算出的那个加权和是负的,那算了,别传了(输出 0);如果是正的,全量传给下一层。"

ReLU 的巧妙之处在于——它的计算极其简单(就一个比较),但在足够多层叠加后,能产生任意复杂的非线性分界。而且它的导数简单(正区域是常数 1),深层网络中梯度不会指数级地消失。

就这样:加权求和(线性层)→ 激活函数(ReLU)→ 再加权求和 → 再激活 → …… 这就是绝大多数神经网络的基本节奏。


17.5 参数:网络中"可以学习"的东西

到目前为止提到的 weight 矩阵和 bias 向量——这些数值,就是神经网络的参数

在 AlexNet 中有约 60,000,000 个参数。在 GPT-3 中有 175,000,000,000 个参数。无论规模如何,所有的参数在数学上只有一个职责——它们是乘法中的因子和加法中的加数。

训练,就是找到一组参数值,使得网络在训练数据上的表现尽可能地好。参数值本身没有"含义"——weight[3][7] = 0.02 不意味着任何人类能解读的东西。它的意义只有在一个位置——它参与的运算链是否能最终让输出更接近正确答案。

这是"从连接主义到表示学习"那条主线在微观层面的落脚点:智能不是被写在参数里的;它是在参数与输入交互的计算过程中涌现出来的。


17.6 损失:告诉网络"你错得多离谱"

要训练网络,需要先定义什么是"好"。

损失函数(loss function)就是用来做这件事的:输入是网络的预测值和真实答案,输出是一个数——错得越离谱,这个数越大。

对于一个分类任务——输入一张猫的照片,网络应该输出"猫"——最常用的损失函数是交叉熵损失(cross-entropy loss)。它的直觉很简单:如果网络对正确答案赋予高概率,损失就小;如果它自信地选了错的,损失就大到离谱。

数学上,交叉熵衡量的是两个概率分布之间的距离——网络的预测分布和"真正的分布"(标准答案的那个)有多接近。当预测分布完全集中在标准答案所在那个类别上时,交叉熵损失最小。

对于回归任务——输入一套房子的信息,网络应该输出预估房价——最经典的损失是均方误差(MSE):(预测值 - 真实值)² 的平均。

无论具体的损失函数长什么样,它们都服务于同一个目的:把"做得好"和"做得差"变成两个可以直接比较的数。


17.7 训练:反复的"试—错—调"

有了网络(函数)和损失(验收标准),训练就是一个重复三步骤的循环:

对于每一批训练样本:  1. 前向传播:输入一批数据 → 网络给出预测  2. 计算损失:对比预测和真实答案 → 得到一个 loss 值  3. 反向传播:用梯度算出每个参数对 loss 的贡献 → 往减少 loss 的方向微调参数

第一步是"试",第二步是"打分",第三步是"调"。重复几千次、几万次、几百万次之后,参数逐渐移动到低损失区域——网络的预测越来越准。

在代码里,这三步通常被隐藏在三行调用后面:

python
output = model(input)           # 前向传播loss = criterion(output, target) # 计算损失loss.backward()                  # 反向传播optimizer.step()                  # 更新参数

但这四行背后对应着本书前面 16 章讲过的每一个核心概念。它们不是魔法——它们是函数求值、矩阵乘法、链式法则的直接应用。


17.8 最小代码:手写一个两层网络

下面的代码用 NumPy 实现一个完整的两层神经网络,在经典的鸢尾花数据集(Iris)上训练。总共约 50 行,包含前向传播、反向传播和训练循环。

python
import numpy as npfrom sklearn.datasets import load_irisfrom sklearn.model_selection import train_test_split# 1. 准备数据iris = load_iris()X = iris.data.astype(np.float32)  # (150, 4): 花萼/花瓣的长宽y_raw = iris.target                # 0, 1, 2 三类# 将标签转为 one-hot: 0→[1,0,0], 1→[0,1,0], 2→[0,0,1]y = np.eye(3)[y_raw].astype(np.float32)X_train, X_test, y_train, y_test = train_test_split(    X, y, test_size=0.3, random_state=42)# 2. 初始化参数np.random.seed(42)W1 = np.random.randn(4, 16) * 0.1   # (4 → 16)b1 = np.zeros((1, 16))W2 = np.random.randn(16, 3) * 0.1   # (16 → 3)b2 = np.zeros((1, 3))# 3. 训练for epoch in range(5000):    # ----- 前向传播 -----    z1 = X_train @ W1 + b1          # 线性层 1    a1 = np.maximum(0, z1)           # ReLU 激活    z2 = a1 @ W2 + b2                # 线性层 2    # softmax: 把输出转为概率分布    exp_z = np.exp(z2 - np.max(z2, axis=1, keepdims=True))    probs = exp_z / np.sum(exp_z, axis=1, keepdims=True)    # ----- 计算损失(交叉熵) -----    loss = -np.mean(np.sum(y_train * np.log(probs + 1e-8), axis=1))    # ----- 反向传播 -----    d_z2 = probs - y_train                          # 交叉熵 + softmax 的梯度合体    d_W2 = a1.T @ d_z2 / len(X_train)    d_b2 = np.sum(d_z2, axis=0, keepdims=True) / len(X_train)    d_a1 = d_z2 @ W2.T    d_z1 = d_a1 * (z1 > 0).astype(np.float32)       # ReLU 反向: 正→1, 负→0    d_W1 = X_train.T @ d_z1 / len(X_train)    d_b1 = np.sum(d_z1, axis=0, keepdims=True) / len(X_train)    # ----- 参数更新(梯度下降) -----    lr = 0.01    W1 -= lr * d_W1; b1 -= lr * d_b1    W2 -= lr * d_W2; b2 -= lr * d_b2    if epoch % 1000 == 0:        # 评估测试准确率        a1_t = np.maximum(0, X_test @ W1 + b1)        z2_t = a1_t @ W2 + b2        acc = np.mean(np.argmax(z2_t, axis=1) == np.argmax(y_test, axis=1))        print(f"epoch {epoch:4d} | loss {loss:.4f} | test acc {acc:.2%}")

这段代码放到 Jupyter Notebook 里可以分步运行,观察 loss 从 ~1.1 降到 ~0.1,测试准确率从 ~30% 一路升到 ~95%。所有东西加起来 50 行,没有框架、没有黑箱——这就是神经网络最基本的训练全貌。


17.9 本章地图

text
问题:神经网络是不是一个复杂函数?回答:是——一个由多层矩阵乘法和非线性激活函数叠加而成的参数化函数。核心组件:线性层(矩阵乘法 + 偏置)、激活函数(ReLU 等)、损失函数、梯度。训练循环:前向传播(试) → 计算损失(打分) → 反向传播(算出每个参数的调整方向) → 更新参数(调)。最小代码:50 行 NumPy 实现了一个两层网络在 Iris 数据集上达到 95% 准确率。今天:GPT 的 Transformer block 也是由线性层和激活函数堆叠而成的——最基本的"层"没有变,变了的是规模、结构和训练方式。

17.10 本章结语:所有东西不过乘法和加法

在你被大模型的神秘感笼罩前,这一章想传递的就一件事:

神经网络在最底层什么也没有——只有乘法、加法、和一个"正数通过负数截断"的非线性变换。

当别人用不可名状的方式谈"AI 魔法"时,你可以记住这里有一个非常平凡的底层事实:一个 175,000,000,000 参数的巨型网络,每一个参数在你训练它的过程中被移动的那一点点,都是通过链式法则算出来的精确调整量。

魔法感褪去后,留下来的是工程和数学——两者都不神秘,但都值得被理解。

下一章,我们把反向传播从直觉版升级为计算版——用纸笔算一遍一个最简单的两层网络里梯度是怎样反向流动的。那是整个训练逻辑在微观上最清楚的展现。

SECTION §02 · ENGAGE

Discussion

留言区 · GitHub-powered comments via Giscus