深度学习与生成式AI理论

本文是《人工智能训练师基础》系列的第三章独立扩展版。原系列把数据结构、算法、人工智能基础、机器学习、深度学习、生成式AI、伦理安全七大模块挤在一篇里,理论细节常常只能点到为止。本章单独拆出来讲透——深度学习与生成式AI是整个理论体系的核心内容,也是后续实操模块(模型微调、训练任务)的理论根基,所以这一章得啃下来。原文只讲了思路的部分,这次我会用具体数字带你一步步算一遍,把”听懂了”变成”真正会算”。

第三章 深度学习与生成式人工智能理论:从神经网络到大模型

深度学习与生成式AI是核心重点内容,虽然原清单只把”人工智能基础”列为重点模块,但DL和生成式AI的理论部分会和其它模块交叉,且是后续实操模块的理论根基,必须讲清核心。

激活函数:给网络加非线性

没有激活函数,多层网络无论堆多深都等价于一个线性变换,等于白搭。激活函数就是把线性结果”掰弯”的非线性函数。

Sigmoid 把任意实数压到 (0,1) 区间,输出可以解释成概率,所以二分类输出层常用它。但它有两个臭名昭著的毛病:一是梯度消失,当输入很大或很小时导数接近0,反向传播时梯度一层层衰减下去;二是输出非零中心,会让下一层梯度总往一个方向走,收敛慢。现在基本只在输出层用,隐藏层早就不用了。

ReLU 是当前隐藏层的绝对主力,公式就是 max(0, x),正区间导数恒为1,彻底解决了正区间的梯度消失问题,计算还特别快。它为什么能缓解梯度消失?因为只要神经元活着(输入大于0),梯度就能一路畅通地传回去,不会被层层相乘稀释掉。ReLU 的缺点是”神经元死亡”——如果某个神经元输入长期小于0,它的梯度永远是0。补救办法是 LeakyReLU,负区间给一个很小的斜率,让神经元还有一线生机。

Softmax 不是单值激活,而是把一个向量”归一化”成概率分布,所有分量之和为1,多分类输出层标配。

记住一句话:隐藏层默认 ReLU,二分类输出用 Sigmoid,多分类输出用 Softmax。

动手算一算:三种激活函数的输出和梯度

光记公式容易混,咱们拿一个具体输入走一遍。设输入 x = 2.0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import math

# 输入
x = 2.0

# ReLU: max(0, x),正区间导数=1,负区间导数=0
relu_out = max(0.0, x) # 2.0
relu_grad = 1.0 if x > 0 else 0.0 # 1.0

# Sigmoid: 1/(1+e^-x),导数 = s*(1-s)
s = 1.0 / (1.0 + math.exp(-x)) # ≈ 0.8808
sigmoid_grad = s * (1 - s) # ≈ 0.1050

# Tanh: (e^x - e^-x)/(e^x + e^-x),导数 = 1 - t^2
t = math.tanh(x) # ≈ 0.9640
tanh_grad = 1 - t * t # ≈ 0.0707

几个关键观察你跟着对一遍:

  • ReLU 输出 2.0、梯度 1.0——梯度原封不动传回去,这就是”只要活着梯度就畅通”。
  • Sigmoid 输出 0.8808、梯度只有 0.1050,已经损失近九成。再看 x = -3.0 时 Sigmoid 输出 ≈ 0.0474、梯度 ≈ 0.0452,离 0 已经很近了——这就是”输入很大或很小时梯度趋零”。
  • Tanh 在 x=2.0 时输出 0.9640、梯度 0.0707,比 Sigmoid 略好但仍衰减明显。

记住一个数字感觉:Sigmoid 导数最大值在 x=0 处,刚好是 0.25。也就是说哪怕在最理想情况下,每经过一层 Sigmoid,梯度最多只剩四分之一,连乘十层就只剩 0.25 的 10 次方。这就是下一节梯度消失的根骨。

损失函数与优化器:网络怎么学

损失函数衡量”预测值离真实值有多远”。回归问题用 MSE 均方误差,分类问题用交叉熵,它衡量两个概率分布的差异,配合 Softmax 时梯度形式非常简洁(预测概率减真实概率),收敛快。

反向传播是训练的核心机制,本质是用链式法则从输出层往输入层逐层算梯度。算出梯度后,优化器决定怎么用这些梯度更新参数。

最基础的是 SGD 随机梯度下降,参数往梯度的反方向走一步,步长由学习率控制。Momentum 加了”动量”,把历史梯度做指数加权平均,相当于给小球加了惯性,能冲过小坑、减少震荡。RMSprop 给每个参数单独维护一个学习率,按梯度平方的滑动平均自适应缩放。Adam 是 Momentum 和 RMSprop 的结合体,同时维护梯度的一阶矩和二阶矩的滑动平均并做偏差修正,是目前最常用的”默认选择”,收敛快、调参少。实际应用中如果遇到”哪个优化器对稀疏梯度友好、自适应学习率”的问题,重点候选就是 Adam 和 RMSprop。

学习率不是越小越好——太小收敛慢,太大会在最优点附近震荡甚至发散。实战中常用学习率调度,比如余弦退火、Warmup。

动手算一算:反向传播的链式法则

光说”链式法则从输出往输入逐层算梯度”很抽象,咱们建一个最小网络手算一遍。

网络结构:输入 x → 隐层 z1 = w1·x + b1,激活 a1 = σ(z1) → 输出层 z2 = w2·a1 + b2,激活 y = σ(z2) → 损失 L = 0.5·(y - t)²。设 x=1.0, w1=0.5, b1=0, w2=0.5, b2=0, 真实标签 t=1.0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import math

def sigmoid(z):
return 1.0 / (1.0 + math.exp(-z))

x, w1, b1, w2, b2, t = 1.0, 0.5, 0.0, 0.0, 1.0
# 注意 b2=0, w2=0.5, 上面那行写错了, 这里修正
w2 = 0.5

# 前向
z1 = w1 * x + b1 # 0.5
a1 = sigmoid(z1) # ≈ 0.6225
z2 = w2 * a1 + b2 # ≈ 0.3113
y = sigmoid(z2) # ≈ 0.5772
L = 0.5 * (y - t) ** 2 # ≈ 0.0893

# 反向(链式法则从 L 一路展开到 w1)
dL_dy = y - t # -0.4228
dy_dz2 = y * (1 - y) # ≈ 0.2440
dL_dz2 = dL_dy * dy_dz2 # ≈ -0.1032

dL_dw2 = dL_dz2 * a1 # ≈ -0.0643 ← 输出层权重梯度
dL_da1 = dL_dz2 * w2 # ≈ -0.0516
da1_dz1 = a1 * (1 - a1) # ≈ 0.2350
dL_dz1 = dL_da1 * da1_dz1 # ≈ -0.0121
dL_dw1 = dL_dz1 * x # ≈ -0.0121 ← 隐藏层权重梯度

观察一下两个梯度的大小:w2 拿到 -0.0643,w1 拿到 -0.0121。浅层 w1 的梯度只有输出层的五分之一左右——这就是梯度在反向过程中被层层稀释的直观体现。如果再用 Sigmoid 把导数压到 0.25,连个十层八层,浅层梯度直接归零。

如果用学习率 η=0.1 更新:w2_new ≈ 0.5 - 0.1×(-0.0643) = 0.5064;w1_new ≈ 0.5 - 0.1×(-0.0121) = 0.5012。这就是一次梯度下降的完整闭环。

动手算一算:Sigmoid 导数连乘如何让梯度归零

接着上面的观察,咱们专门演示梯度消失。Sigmoid 导数最大值是 0.25(出现在 x=0 处),其他位置更小。假设一个 10 层网络,每层都用 Sigmoid,反向传播时梯度连乘:

1
2
3
4
# 假设每层都恰好工作在 Sigmoid 导数最大处 0.25
single_grad = 0.25
layers = 10
total_grad = single_grad ** layers # ≈ 9.54e-7

10 层之后梯度只剩 0.00000095,参数几乎不动了。这就是为什么深层网络早期训不动。把激活换成 ReLU 后,正区间导数恒为 1:

1
2
relu_grad = 1.0
total_grad_relu = relu_grad ** 10 # 1.0,完全不衰减

对比一目了然:ReLU 让梯度能原封不动地传回浅层,这就是它成为隐藏层默认选择的根骨。

CNN:卷积如何”看”图

CNN 解决了 MLP 参数爆炸的问题,靠的是两个核心思想:局部连接和权值共享。

卷积层用一个小小的卷积核(比如3×3)在整张图上滑动,每次只看局部一块,这就是局部连接。更妙的是,同一个卷积核在滑动过程中权重不变——这就是”权值共享”,可以形象理解为”用同一个滤镜扫整张图”。一张图里无论猫耳朵在左上角还是右下角,同一个滤镜都能识别出来,这叫平移不变性。这样一来,3×3 的核只有9个参数,远比全连接层省。

卷积层有几个关键超参数:卷积核大小、步长 stride、填充 padding。输出尺寸公式记一下:输出边长 = (输入边长 - 卷积核大小 + 2×填充) / 步长 + 1,向下取整。当步长为2、不填充时,输出尺寸会减半,相当于做了下采样。

池化层就是降采样,最常用的是最大池化 MaxPool,在窗口里取最大值,作用是缩小特征图、降低计算量、增加平移鲁棒性,没有参数。

批归一化 BatchNorm 是个里程碑式技巧:给每一层的输入做标准化,减去均值除以标准差,再用两个可学习参数缩放和偏移。好处是让每层输入分布稳定、允许用更大学习率、加速收敛。一句话总结:BatchNorm 就是给每层输入做标准化。注意它在训练和推理时行为不同——训练用 batch 统计量,推理用全量滑动平均。它和 LayerNorm 别混——BatchNorm 沿 batch 维度归一化(适合 CV),LayerNorm 沿特征维度归一化(适合 NLP/Transformer)。

经典架构发展脉络必须记清楚。LeNet 是 LeCun 在1998年搞的,第一个成功的 CNN,识别手写数字。AlexNet 在2012年 ImageNet 比赛一战成名,首次用 ReLU、Dropout、GPU 训练【需网络查询确认:AlexNet 具体年份与作者】。VGG 探索了”用堆叠小卷积核代替大卷积核”的思想,两个3×3卷积的感受野等于一个5×5,但参数更少、非线性更多。ResNet 是何恺明的代表作,提出了残差连接,让网络可以堆到152层甚至更深而不退化——核心是让输入直接跳过几层加到输出上,网络学的是”残差”而非”恒等映射”,解决了深层网络退化问题。Inception 走的是”多尺度并行卷积”路线。

动手算一算:一次卷积操作的全过程

光说”卷积核在图上滑动”不直观,咱们手动跑一遍。设输入是 3×3 矩阵,卷积核 2×2,步长 stride=1,不填充 padding=0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import numpy as np

# 输入图(3x3)
x = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])

# 卷积核(2x2)
kernel = np.array([[1, 0],
[0, 1]])

stride = 1
H, W = x.shape
kH, kW = kernel.shape

# 输出尺寸 = (输入边长 - 卷积核大小 + 2*padding) / 步长 + 1
out_h = (H - kH) // stride + 1 # (3-2)//1 + 1 = 2
out_w = (W - kW) // stride + 1 # 2
out = np.zeros((out_h, out_w))

# 在每个锚点做对应元素相乘再求和
for i in range(out_h):
for j in range(out_w):
patch = x[i*stride:i*stride+kH, j*stride:j*stride+kW]
out[i, j] = np.sum(patch * kernel)

print(out)
# [[ 6. 8.]
# [12. 14.]]

逐个位置给你看:

  • 左上角 (0,0) 看到子矩阵 [[1,2],[4,5]],跟核 [[1,0],[0,1]] 对应相乘再求和:1×1 + 2×0 + 4×0 + 5×1 = 6。
  • 右上角 (0,1) 子矩阵 [[2,3],[5,6]]:2×1 + 3×0 + 5×0 + 6×1 = 8。
  • 左下角 (1,0) 子矩阵 [[4,5],[7,8]]:4 + 8 = 12。
  • 右下角 (1,1) 子矩阵 [[5,6],[8,9]]:5 + 9 = 14。

输出就是 [[6, 8], [12, 14]]。这个核的效果相当于”取对角线元素之和”——你滑动它扫过整张图,效果就像用同一块滤镜扫整张图,这就是”权值共享”的直观含义。如果换成步长 stride=2,输出尺寸变成 (3-2)//2 + 1 = 1,只剩一个值,相当于做了一次下采样。

动手算一算:MaxPool 池化操作

池化比卷积还简单。设输入 4×4 矩阵,MaxPool 窗口 2×2,步长 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np

x = np.array([[1, 3, 2, 4],
[5, 6, 7, 8],
[9, 4, 3, 2],
[1, 2, 5, 6]])

pool = 2
stride = 2
out_h = (x.shape[0] - pool) // stride + 1 # 2
out_w = (x.shape[1] - pool) // stride + 1 # 2
out = np.zeros((out_h, out_w))

for i in range(out_h):
for j in range(out_w):
patch = x[i*stride:i*stride+pool, j*stride:j*stride+pool]
out[i, j] = patch.max()

print(out)
# [[6. 8.]
# [9. 6.]]

四个窗口逐个看:左上 [[1,3],[5,6]] 取最大 6;右上 [[2,4],[7,8]] 取最大 8;左下 [[9,4],[1,2]] 取最大 9;右下 [[3,2],[5,6]] 取最大 6。整个过程没有可学习参数,纯粹是”挑最大的那个”——作用是缩小特征图、降低计算量,还能让特征对小幅度平移更鲁棒(移动一两格最大值往往不变)。

动手算一算:BatchNorm 标准化、缩放、偏移

BatchNorm 听起来玄乎,其实就三步:减均值、除标准差、再做缩放和偏移。设一个 batch 有 4 个样本的某个特征值:x = [2.0, 4.0, 6.0, 8.0],可学习参数 γ=2.0、β=1.0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np

x = np.array([2.0, 4.0, 6.0, 8.0])
gamma, beta = 2.0, 1.0

# 1. 求 batch 内均值和方差
mu = x.mean() # 5.0
var = x.var() # 5.0
sigma = np.sqrt(var + 1e-8) # ≈ 2.2361(加 epsilon 防除零)

# 2. 标准化
x_hat = (x - mu) / sigma
# ≈ [-1.3416, -0.4472, 0.4472, 1.3416]

# 3. 缩放和偏移
out = gamma * x_hat + beta
# ≈ [-1.6832, 0.1056, 1.8944, 3.6832]

走一遍数字感觉:

  • 原始数据 [2,4,6,8] 分布在 2 到 8 之间。
  • 减均值 5.0 后变成 [-3,-1,1,3],再除以标准差 2.2361 变成 [-1.34,-0.45,0.45,1.34]——这就是”标准正态分布”的形状,均值 0、方差 1。
  • 最后乘 γ=2 加 β=1,输出 [-1.68, 0.11, 1.89, 3.68]——分布形状不变,但被拉伸和平移了。

γ 和 β 是网络自己学的:如果它发现”原始分布其实更好”,可以把 γ 学成 1、β 学成 0,等于跳过 BatchNorm;如果发现”标准化后丢掉了某些尺度信息”,可以用 γ 拉回来。所以 BatchNorm 不是死板的归一化,而是”先归一化再让模型自己决定要不要复原”——这就是它的精妙之处。

动手算一算:残差连接让梯度直达浅层

ResNet 的残差连接 y = x + F(x) 看着就一行公式,但它解决了一个大问题。咱们看一个对比,假设有 3 层网络,每层梯度衰减因子是 0.1:

1
2
3
4
5
# 不用残差:梯度逐层连乘
no_res_grad = 0.1 * 0.1 * 0.1 # 0.001,浅层几乎学不动

# 用残差:每层梯度 = 1 + ∂F/∂x,至少为 1
with_res_grad = (1 + 0.1) * (1 + 0.1) * (1 + 0.1) # ≈ 1.331

不用残差时浅层梯度只有 0.001,参数几乎不动;加上残差后梯度至少是 1,浅层照样能更新——这就是”让网络可以堆到 152 层还不退化”的核心机制。

再用一个具体的前向数值感受一下”学残差”和”学恒等映射”的区别。设输入 x=1.0,浅层 F(x) 初始输出 0.01(接近 0):

  • 不用残差:y = F(x) = 0.01,模型要直接学出”输出 1.0”,难度大。
  • 用残差:y = x + F(x) = 1.0 + 0.01 = 1.01,模型只要让 F(x) 趋近 0 就等于恒等映射,学习目标变成”学残差”,难度大幅降低。

这就是 ResNet 的双重好处:前向上”学恒等映射很容易”,后向上”梯度能直达浅层”。

RNN与LSTM:处理序列的记忆机器

RNN 专门处理序列数据(文本、语音、时序),核心思想是”当前输出不仅依赖当前输入,还依赖上一时刻的隐状态”。隐状态就像网络的短期记忆。但原始 RNN 有个致命问题:长程依赖,序列一长,反向传播时梯度要么消失要么爆炸。

LSTM 用精巧的”门控”机制解决了这个问题。它有三个门:遗忘门、输入门、输出门,外加一个细胞状态 cell state。可以用”记忆的写/读/遗忘”来类比:遗忘门决定从长期记忆里忘掉什么,输入门决定把新信息写进长期记忆多少,输出门决定基于当前记忆输出什么。细胞状态是一条贯穿全程的”传送带”,信息可以几乎无损地流过去,梯度也能顺畅回流,这就解决了长程依赖。

GRU 是 LSTM 的简化版,把三个门简化成两个(更新门和重置门),参数更少、训练更快,效果在很多任务上和 LSTM 接近。

双向 RNN 让序列的每个位置都能看到前后文信息,适合离线任务。序列到序列 Seq2Seq 是”编码器-解码器”结构,典型应用是机器翻译,后来有了注意力机制来缓解定长向量的信息损失瓶颈,再后来注意力机制演化成了 Transformer。

动手算一算:LSTM 三个门怎么开门

LSTM 公式背起来很烦,咱们用一个最小数字示例把流程走通。设当前输入 x_t = 0.5,上一时刻隐状态 h_{t-1} = 0.3,上一时刻细胞状态 C_{t-1} = 0.7。为了聚焦在”门怎么开”这件事上,假设 W·[h_{t-1}, x_t] + b 已经被算好成一个标量(实际是向量,原理一样)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import math

def sigmoid(z):
return 1.0 / (1.0 + math.exp(-z))

def tanh(z):
return math.tanh(z)

# 假设各门的"预激活值"已经算好(即 W·[h_{t-1}, x_t] + b 的结果)
a_f = 1.0 # 遗忘门预激活
a_i = -0.5 # 输入门预激活
a_C = 0.5 # 候选细胞预激活
a_o = 0.8 # 输出门预激活

C_prev = 0.7 # 上一时刻细胞状态

# 1. 三个门 + 候选细胞
f_t = sigmoid(a_f) # ≈ 0.7311 ← 遗忘门:开 73%,决定保留多少旧记忆
i_t = sigmoid(a_i) # ≈ 0.3775 ← 输入门:开 38%,决定写入多少新信息
C_bar = tanh(a_C) # ≈ 0.4621 ← 候选新内容
o_t = sigmoid(a_o) # ≈ 0.6899 ← 输出门:开 69%,决定输出多少

# 2. 更新细胞状态:旧记忆保留一部分 + 写入一部分新内容
C_t = f_t * C_prev + i_t * C_bar
# = 0.7311 * 0.7 + 0.3775 * 0.4621
# ≈ 0.5118 + 0.1744
# ≈ 0.6862

# 3. 输出隐状态:基于当前细胞状态,由输出门控制
h_t = o_t * tanh(C_t)
# = 0.6899 * tanh(0.6862)
# ≈ 0.6899 * 0.5956
# ≈ 0.4109

跟着数字走一遍你就能看清门控的本质:

  • 遗忘门开 0.73,意味着旧细胞状态 0.7 被保留了约 73%(贡献 0.5118)。
  • 输入门只开 0.38,意味着新候选内容 0.4621 只写入了 38%(贡献 0.1744)。
  • 两者相加得到新细胞状态 0.6862——和旧的 0.7 接近,因为遗忘门开得大、输入门开得小,”短期记忆基本没换”。
  • 输出门开 0.69,把细胞状态过 tanh 压一下再放出去 69%,得到新隐状态 0.4109。

关键直觉:细胞状态 C_t 是一条”传送带”,门只是在控制”留多少、写多少、说多少”。反向传播时梯度沿 C_t 这条传送带回流,门控只起”乘上一个 0~1 的系数”的作用,不会像 RNN 那样反复乘权重导致爆炸或消失——这就是 LSTM 解决长程依赖的根骨。

正则化与优化策略

Dropout 是隐藏层正则化的经典做法:训练时随机让一部分神经元”掉线”(输出置0),推理时全部启用但权重缩放。直观理解是每次训练的是不同子网络,相当于模型集成,强迫网络不要过度依赖个别神经元。BatchNorm 除了稳定分布本身也有正则化效果。L1 正则化产生稀疏权重做特征选择,L2 正则化让权重整体变小更平滑。早停 Early Stopping 是性价比最高的正则化:在验证集损失不再下降反而上升时停止训练。数据增强对图像特别有用——翻转、旋转、裁剪等。

优化策略这边还有权重初始化:不能全置0,也不能随机太大。Xavier 初始化适合 Sigmoid 和 Tanh,He 初始化(何恺明)适合 ReLU 族。迁移学习是”站在巨人肩膀上”——拿一个在大数据集上预训练好的模型,冻结底层或用小学习率微调顶层,在自己小数据集上往往能拿到不错的效果。

强化学习速览

核心框架是马尔可夫决策过程 MDP,由状态 S、动作 A、奖励 R、转移概率 P、折扣因子 γ 组成。”马尔可夫”的含义是”未来只依赖当前状态,与历史无关”。

Q-Learning 是值函数方法,维护一个 Q 表 Q(s,a),更新公式是经典的贝尔曼方程。问题是 Q 表只能处理离散小状态空间。DQN 用神经网络逼近 Q 函数,引入经验回放和目标网络两个技巧【需网络查询确认:DQN 论文年份】。策略梯度 Policy Gradient 直接参数化策略,适合连续动作空间但方差大。Actor-Critic 把值函数和策略函数结合起来,是现代强化学习的主流框架。PPO(近端策略优化)是 OpenAI 力推的算法,对策略更新幅度做裁剪限制,是 RLHF 里最常用的对齐算法之一,必须记住它和 RLHF 的关系。

生成模型家族:AE/VAE/GAN/扩散

生成模型的目标是”学数据分布,然后能采样生成新样本”。这家族有四大主流。

自编码器 AE 是个”压缩-还原”网络,编码器把输入压成低维向量,解码器再还原回去,学到的低维向量叫潜在表示。但 AE 本身不能直接生成新样本——因为潜在空间没有结构。

VAE 补上了这个缺口,它不学确定的潜在向量而是学一个分布,损失除了重建项还有 KL 散度项,这样潜在空间是连续平滑的,随便采点都能生成合理样本。VAE 生成相对模糊但训练稳定。

GAN 走的是完全不同的路子:生成器与判别器对抗博弈。经典比喻就是”造假者 vs 警察”——生成器造假钞试图骗过判别器,判别器努力分辨真假,两者在博弈中一起变强。GAN 生成的图比 VAE 锐利,但训练难、容易模式崩溃(只生成少数几种样本)。

扩散模型 Diffusion 是这两年图像生成的王者,Stable Diffusion、DALL-E 2 都是它的徒子徒孙。核心思想简单到让人意外:加噪再去噪。前向过程逐步给原图加高斯噪声加到纯噪声,反向过程训练一个网络学会从噪声逐步去噪还原成图。训练时网络预测每一步要去除的噪声,推理时从纯噪声开始迭代去噪生成图。优点是训练稳定、生成质量高,缺点是采样慢。Stable Diffusion 还把扩散过程搬到潜在空间(用 VAE 先压缩),大幅降低计算量。

动手算一算:扩散模型前向加噪与反向去噪

扩散模型公式背起来头疼,咱们用 3 步加噪走一遍。设原图 x_0 = 1.0(一个像素值,简化为一维),每步的”保留系数” α_t 和”噪声系数” √(1-α_t) 都是预设好的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import math
import numpy as np

x0 = 1.0
alpha = [0.9, 0.8, 0.7] # 每步保留比例
noise = [0.5, -0.3, 0.8] # 假设采样到的高斯噪声(实际是随机的)

# 前向过程:x_t = √α_t · x_{t-1} + √(1-α_t) · ε_t
x_prev = x0
print(f"x_0 = {x_prev}")
for t in range(3):
eps = noise[t]
x_t = math.sqrt(alpha[t]) * x_prev + math.sqrt(1 - alpha[t]) * eps
print(f"x_{t+1} = √{alpha[t]}·x_{t} + √{1-alpha[t]}·ε = "
f"{math.sqrt(alpha[t]):.4f}·{x_prev:.4f} + "
f"{math.sqrt(1-alpha[t]):.4f}·{eps} = {x_t:.4f}")
x_prev = x_t

# 输出:
# x_0 = 1.0
# x_1 = 0.9487·1.0000 + 0.3162·0.5 = 1.1068
# x_2 = 0.8944·1.1068 + 0.4472·-0.3 = 0.8557
# x_3 = 0.8367·0.8557 + 0.5477·0.8 = 1.1542

三步之后原图 1.0 被噪声搅成了 1.1542,已经看不出原来的样子。再多加几步,就趋近于纯高斯噪声了——这就是前向过程”加噪到纯噪声”。

反向去噪是训练的核心目标。网络 ε_θ 接收当前的 x_t 和步数 t,预测”这一步加进去的噪声是什么”。看一个反向单步的简化计算:

1
2
3
4
5
6
7
8
9
10
11
# 反向过程:已知 x_3,假设网络预测出第 3 步噪声 ε_θ = 0.7(真实是 0.8)
x3 = 1.1542
eps_pred = 0.7 # 网络预测的噪声
alpha_t = 0.7

# 简化的反向更新公式
x2_pred = (x3 - math.sqrt(1 - alpha_t) * eps_pred) / math.sqrt(alpha_t)
# = (1.1542 - 0.5477 * 0.7) / 0.8367
# = 0.7708 / 0.8367
# ≈ 0.9213
print(f"x_2 预测值 = {x2_pred:.4f}, 真实 x_2 = 0.8557")

网络预测的噪声 0.7 跟真实的 0.8 有点偏差,所以预测出来的 x_2 = 0.9213 和真实的 0.8557 也不完全一样。训练的目标就是让 ε_θ 的预测越来越准,这样从纯噪声开始一步步反推,就能还原出清晰的图。推理时从 x_T(纯噪声)开始,反复执行这个反向步骤 T 次,最后得到生成的图——所以扩散模型推理慢是因为要迭代很多次。

Transformer:大模型的基石

Transformer 是 2017 年 Google 论文《Attention is All You Need》提出的,彻底颠覆了 RNN 统治 NLP 的格局【需网络查询确认:论文具体年份与作者】。它的核心是自注意力机制,让序列中每个位置都能直接和所有位置交互,可以高度并行。

自注意力怎么算?用一句话概括:每个词跟所有词算相关性。具体来说,对每个词的 embedding 做三次线性变换得到 Query、Key、Value 三个向量,然后用 Query 和所有 Key 做点积得到相关性分数,除以根号 dk 缩放,过 Softmax 归一化成权重,再用这些权重对 Value 加权求和。整个过程就是”我用 Query 去问所有人,谁的 Key 和我匹配度高就多取谁的 Value”。

多头注意力是自注意力的升级版:把 Q/K/V 分成多组,每组独立做注意力,最后拼起来再投影。好处是不同头可以关注不同子空间的关系——有的头关注语法、有的关注语义、有的关注长距离依赖。

位置编码是因为 Transformer 本身没有位置感知能力(注意力是顺序无关的),需要额外把位置信息加进去。原始论文用正弦余弦函数生成固定编码,也有可学习位置编码(BERT 用)、相对位置编码(T5 用)。后来有 ALiBi、RoPE 旋转位置编码等改进。

Transformer 整体结构是编码器-解码器堆叠。后来 GPT 只取解码器部分,BERT 只取编码器部分,T5 保留完整结构,这就引出了三条路线。

动手算一算:自注意力的完整计算流程

自注意力公式 Attention(Q, K, V) = softmax(Q·K^T / √dk)·V。咱们用 3 个词、每个词 2 维向量走完整流程。为了聚焦在注意力机制本身,假设 Q=K=V 都等于输入 embedding(实际中会有 W_Q、W_K、W_V 三个权重矩阵做线性变换):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import numpy as np
import math

# 三个词的 2 维 embedding
x = np.array([[1.0, 0.0], # 词1
[0.0, 1.0], # 词2
[1.0, 1.0]]) # 词3
Q = K = V = x
dk = 2

# 第1步:Q · K^T,得到 3x3 的相关性得分矩阵
scores = Q @ K.T
# [[1, 0, 1],
# [0, 1, 1],
# [1, 1, 2]]

# 第2步:除以 √dk 缩放(防止点积过大导致 softmax 进入饱和区)
scaled = scores / math.sqrt(dk)
# [[0.7071, 0, 0.7071],
# [0, 0.7071, 0.7071],
# [0.7071, 0.7071, 1.4142]]

# 第3步:对每行做 softmax,归一化成权重
def softmax(rows):
e = np.exp(rows - rows.max(axis=1, keepdims=True)) # 减最大值防溢出
return e / e.sum(axis=1, keepdims=True)

weights = softmax(scaled)
# [[0.4013, 0.1978, 0.4013],
# [0.1978, 0.4013, 0.4013],
# [0.2482, 0.2482, 0.5036]]

# 第4步:用权重对 V 加权求和,得到每个词的输出向量
out = weights @ V
# 词1输出: 0.4013·[1,0] + 0.1978·[0,1] + 0.4013·[1,1] = [0.8026, 0.5991]
# 词2输出: 0.1978·[1,0] + 0.4013·[0,1] + 0.4013·[1,1] = [0.5991, 0.8026]
# 词3输出: 0.2482·[1,0] + 0.2482·[0,1] + 0.5036·[1,1] = [0.7518, 0.7518]

四个步骤都给你串起来了,几个直觉点你跟着对一遍:

  • 第 1 步 Q·K^T 是”两两算相关性”。词3 跟自己点积是 2(最大),因为它向量最长;词1 和词2 互不相关(点积 0)。
  • 第 2 步除以 √dk=√2≈1.414。为什么要缩放?因为 dk 越大,点积本身数值越大,softmax 会进入”几乎 one-hot”的饱和区,梯度趋零。除一下把数值压回合理范围。
  • 第 3 步 softmax 把每行归一成”注意力权重”,所有权重和为 1,可以理解为”我把注意力按这个比例分给每个词”。
  • 第 4 步加权求和。词1 的输出 [0.8026, 0.5991] 基本上是”自己 + 词3”,因为权重 0.4013 给自己、0.4013 给词3——这就是”谁的 Key 跟我匹配就多取谁的 Value”。

这就是自注意力的全部家底,多头注意力就是”把这个过程重复 h 次,每次用不同的 W_Q/W_K/W_V,最后拼起来再投影”。

GPT与BERT:两条路线

虽然都基于 Transformer,但 GPT 和 BERT 的设计哲学完全相反,必须分清。

GPT 走的是自回归路线:用 Transformer 解码器,从左到右逐词预测下一个词,训练目标就是”语言建模”。它生成时一个词一个词往外蹦,天然适合生成任务。优点是生成流畅、能做零样本少样本,缺点是只能看到左边的上下文。

BERT 走的是双向编码路线:用 Transformer 编码器,训练时用掩码语言建模 MLM——随机遮住一些词,让模型基于上下文(左右双向)预测被遮的词。BERT 能看到完整上下文,理解能力强,做分类、问答效果很好,但天生不擅长生成。

简单记忆:GPT 自回归、单向、擅长生成;BERT 双向编码、擅长理解。T5 走中间路线,用编码器-解码器统一所有任务为”文本到文本”格式。LLaMA 是 Meta 开源的一系列大模型,结构上基本沿用 GPT 的解码器路线,是开源大模型的标杆【需网络查询确认:LLaMA 各版本发布时间和参数规模】。

多模态模型

CLIP 是 OpenAI 提出的,思想很优雅:用两个编码器分别编码图像和文本,让”匹配的图文对”在向量空间里靠近,”不匹配的”远离。CLIP 的图像编码器后来被无数多模态模型当”眼睛”用。BLIP 和 BLIP-2 引入 Q-Former 这个小网络,把冻结的图像编码器和冻结的大语言模型连起来【需网络查询确认:BLIP-2 发布年份】。LLaVA 是把 CLIP 图像编码器的输出通过一个投影层接到 LLaMA 上,让大模型能”看图说话”。GPT-4V 是 OpenAI 在 GPT-4 基础上加的多模态版本。

高效微调技术:LoRA为何这么火

大模型动辄几十亿几百亿参数,全量微调显存和算力都吃不消。于是诞生了一系列”参数高效微调”PEFT 方法。

LoRA 是当下最火的方法,核心思想:冻结原模型,只训练低秩小矩阵。原模型某层权重 W 是 d×d 的大矩阵,LoRA 不直接动 W,而是学两个小矩阵 A(d×r) 和 B(r×d) 的乘积加到 W 上,r 远小于 d(比如8或16)。这样可训练参数从 d² 降到 2dr。推理时可以把 BA 合并到 W 里,零额外开销。为什么有效?因为微调时权重更新 ΔW 本身就是低秩的。LoRA 的优势是参数少、效果接近全量、可以插拔复用。

Adapter Tuning 是在 Transformer 每层里插入小瓶颈模块,只训练这些小模块,参数量小但会增加推理延迟。Prefix Tuning 是在每个注意力层的 KV 前面拼上一些可学习的”虚拟前缀” token,只训练这些前缀。Prompt Tuning 更轻量,只在输入 embedding 层加可学习的 prompt token,参数最少但小模型上效果一般。P-Tuning v2 是 Prefix Tuning 的改进版,把可学习 prompt 加到每一层,对中小模型也有效。

实际应用中如果遇到”哪个方法推理无额外开销”的问题,答案是 LoRA(可合并权重);遇到”哪个最轻量只动输入”,是 Prompt Tuning;遇到”为什么 LoRA 火”,因为它参数少、效果好、可插拔、推理零开销。

动手算一算:LoRA 低秩分解省了多少参数

LoRA 的精髓在 d² → 2dr 这个参数缩减。先用一个小矩阵把”乘积加到 W 上”这件事走通,再单独看大模型场景下的参数节省比例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import numpy as np

# 假设原模型某层权重 W 是 4x4(被冻结,不参与训练)
W = np.array([[1.0, 0.5, 0.2, 0.1],
[0.3, 0.8, 0.4, 0.2],
[0.1, 0.2, 0.9, 0.3],
[0.0, 0.1, 0.2, 0.7]])

# LoRA 分解:A 是 4x2, B 是 2x4,秩 r=2
A = np.array([[1.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[0.0, 0.0]]) # 4x2,8 个参数

B = np.array([[1.0, 2.0, 0.0, 1.0],
[0.0, 1.0, 1.0, 0.0]]) # 2x4,8 个参数

# 计算 ΔW = B @ A(注意维度:B 是 r×d,A 是 d×r,乘积是 d×d 跟 W 同形状)
delta_W = A @ B
# 手算 B @ A 的第 1 行:
# 1*1 + 0*0 = 1, 1*2 + 0*1 = 2, 1*0 + 0*1 = 0, 1*1 + 0*0 = 1
# 第 2 行:
# 0*1 + 1*0 = 0, 0*2 + 1*1 = 1, 0*0 + 1*1 = 1, 0*1 + 1*0 = 0
# 第 3 行:
# 1+0 = 1, 2+1 = 3, 0+1 = 1, 1+0 = 1
# 第 4 行:
# 全 0
# 所以 delta_W =
# [[1, 2, 0, 1],
# [0, 1, 1, 0],
# [1, 3, 1, 1],
# [0, 0, 0, 0]]

# 前向:用 W' = W + ΔW 替换原权重,等于在原模型基础上加了一个"低秩修正"
W_new = W + delta_W

看清楚乘法过程之后,咱们再算一下参数节省比例。这个例子 d=4、r=2,原参数 16 个,LoRA 参数 8+8=16 个,反而没省——这告诉你”低秩分解只有在 d 远大于 r 时才划算”。换个真实规模:

1
2
3
4
5
6
7
8
9
# 大模型实际场景:d=4096, r=8
d, r = 4096, 8

full_params = d * d # 16,777,216(约 1677 万)
lora_params = d * r + r * d # 65,536(约 6.5 万)
ratio = lora_params / full_params # ≈ 0.0039,即 0.39%
print(f"全量微调参数: {full_params:,}")
print(f"LoRA 参数: {lora_params:,}")
print(f"节省比例: {(1 - ratio) * 100:.2f}%")

1677 万参数被压到 6.5 万,节省 99.6%。这就是 LoRA 让普通消费级显卡也能微调大模型的根骨。

再看”推理零开销”这件事:训练时 W 和 ΔW=BA 是分开存的,但推理前可以把 ΔW 直接加到 W 里得到 W’,之后只用 W’ 做前向,跟原来完全一样快——这就是”推理零额外开销”的来源。Adapter 就做不到这点,因为它在每一层都插了额外的小网络,前向计算量永久增加。

训练方法:SFT、RLHF与DPO

把一个预训练基座模型变成好用的对话助手,要经过几个阶段的训练。

预训练阶段是大规模无监督地学语言知识,用海量文本做下一个词预测,得到基座模型。这一步烧钱烧卡,但模型还不会”听指令”。

SFT 监督微调是第一步对齐:用”指令-回答”对的高质量数据微调模型,让它学会按指令格式回答。SFT 是几乎所有对齐流程的起点。

RLHF 基于人类反馈的强化学习是 OpenAI 在 InstructGPT/ChatGPT 里发扬光大的方法【需网络查询确认:InstructGPT 论文年份】。流程分三步:先 SFT 得到初始策略,再训练一个奖励模型 RM(用人类对多个回答的偏好排序数据),最后用 RM 的打分当奖励,用 PPO 算法优化策略模型让回答得分更高。RLHF 效果惊艳但流程复杂、训练不稳定、要同时维护四个模型(策略、参考、奖励、价值)。

DPO 直接偏好优化是后来居上的简化方案,核心洞察是:在特定数学假设下,可以直接用偏好数据(A比B好)通过一个简洁的损失函数优化策略模型,省掉了显式的奖励模型和 RL 训练循环。DPO 训练更稳定、更简单,效果接近甚至超过 RLHF。

三者关系一句话总结:SFT 是基础对齐,RLHF 用强化学习+奖励模型做偏好优化,DPO 用监督学习方式直接做偏好优化、绕开 RL 的复杂性。一般流程是 SFT 先做,再用 RLHF 或 DPO 二选一做偏好对齐。

提示工程与RAG

提示工程不训练模型,靠设计输入提示来激发模型能力。零样本 Zero-shot 是直接给模型任务不加示例。少样本 Few-shot 是在提示里给几个”输入-输出”示例,让模型模仿模式。链式思维 CoT 是让模型”一步步思考”再给答案的技巧,提示里加”Let’s think step by step”或给带推理过程的示例,能显著提升数学、逻辑等复杂推理任务的准确率。

RAG 检索增强生成是把外部知识检索和大模型生成结合的范式。流程是:用户提问→把问题编码成向量→在知识库(向量数据库)里检索相关文档片段→把检索结果拼进提示→大模型基于检索内容生成答案。它解决的是大模型”知识过期”和”幻觉”问题。RAG 比微调更适合知识频繁更新的场景,成本也低。关键区别:RAG vs 微调——知识频繁更新选 RAG(不用重训),需要改变模型行为风格选微调。RAG 不能让模型学会新技能,只能让它查到新知识。

评估指标与工具库

评估指标按任务分要记清楚。文本生成用 BLEU 和 ROUGE。BLEU 看生成文本里 n-gram 有多少出现在参考文本里,侧重精确率,机器翻译常用。ROUGE 反过来,看参考文本里 n-gram 有多少被生成文本覆盖,侧重召回率,文本摘要常用。别记反。多模态对齐用 CLIP Score,衡量图像和文本匹配程度。大语言模型综合能力有 MMLU(英文多任务理解基准,覆盖57个学科)和 C-Eval(中文综合评估基准)。

工具库这块要认识这几个名字和它们的分工。Hugging Face Transformers 是事实上的模型库标准,提供几千个预训练模型的加载、推理、微调 API。PEFT 是 Hugging Face 出的参数高效微调库,封装了 LoRA、Adapter、Prefix Tuning 等方法。DeepSpeed 是微软的大模型训练加速库,提供 ZeRO 优化(把优化器状态、梯度、参数分片到多卡)。Accelerate 也是 Hugging Face 的,简化多卡、混合精度、分布式训练的样板代码。TRL 是 Transformers Reinforcement Learning 库,专门做 RLHF、DPO 等对齐训练。别把 DeepSpeed 和 Accelerate 混,也别把 PEFT 和 TRL 混。

动手算一算:BLEU 与 ROUGE 的 n-gram 匹配

BLEU 和 ROUGE 容易记反,咱们用同一个例子把两个都算一遍。设参考文本 R = “我 喜欢 吃 苹果”,生成文本 G = “我 喜欢 苹果”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from collections import Counter

ref = ["我", "喜欢", "吃", "苹果"]
gen = ["我", "喜欢", "苹果"]

def ngrams(tokens, n):
return [tuple(tokens[i:i+n]) for i in range(len(tokens) - n + 1)]

def clipped_match(gen_ngrams, ref_ngrams):
"""截断计数:每个 n-gram 出现次数取 min(生成, 参考)"""
gen_c = Counter(gen_ngrams)
ref_c = Counter(ref_ngrams)
matched = sum(min(cnt, ref_c[ng]) for ng, cnt in gen_c.items())
return matched, len(gen_ngrams)

# 1-gram (unigram)
g1, r1 = ngrams(gen, 1), ngrams(ref, 1)
m1, total_g1 = clipped_match(g1, r1) # 匹配 3, 生成共 3 个
bleu1 = m1 / total_g1 # 3/3 = 1.0
rouge1_recall = m1 / len(r1) # 3/4 = 0.75

# 2-gram (bigram)
g2, r2 = ngrams(gen, 2), ngrams(ref, 2)
m2, total_g2 = clipped_match(g2, r2) # 匹配 1 ("我 喜欢"), 生成共 2 个
bleu2 = m2 / total_g2 # 1/2 = 0.5
rouge2_recall = m2 / len(r2) # 1/3 ≈ 0.333

print(f"BLEU-1 (精确率) = {bleu1:.3f}") # 1.000
print(f"ROUGE-1 (召回率) = {rouge1_recall:.3f}") # 0.750
print(f"BLEU-2 (精确率) = {bleu2:.3f}") # 0.500
print(f"ROUGE-2 (召回率) = {rouge2_recall:.3f}") # 0.333

跟着数字对一遍:

  • 1-gram 层面:生成的 [“我”,”喜欢”,”苹果”] 三个词都在参考里出现,匹配 3 个。
    • BLEU-1 = 匹配数 / 生成总数 = 3/3 = 1.0(”生成里有多少命中参考”——精确率视角,翻译常用)。
    • ROUGE-1 = 匹配数 / 参考总数 = 3/4 = 0.75(”参考里有多少被生成覆盖”——召回率视角,摘要常用)。
  • 2-gram 层面:生成的 2-gram 是 [“我 喜欢”, “喜欢 苹果”],参考的 2-gram 是 [“我 喜欢”, “喜欢 吃”, “吃 苹果”]。只有 “我 喜欢” 命中,匹配 1 个。
    • BLEU-2 = 1/2 = 0.5。
    • ROUGE-2 = 1/3 ≈ 0.333。

关键直觉就一句话:BLEU 用”生成文本做分母”(精确率),ROUGE 用”参考文本做分母”(召回率)。翻译用 BLEU,摘要用 ROUGE,就这两个对应关系。

深度学习与生成式AI重点回顾

激活函数:隐藏层默认 ReLU 不是 Sigmoid,ReLU 缓解梯度消失是因为正区间梯度恒为1;Softmax 只用于多分类输出层。卷积:权值共享是”同一个滤镜扫整张图”;池化没有参数;输出尺寸公式别忘了加2倍 padding。BatchNorm:给每层输入做标准化,训练用 batch 统计、推理用滑动平均;它和 LayerNorm 别混。ResNet:残差连接学的是”残差”不是”恒等映射”,解决的是退化问题。LSTM 三个门:遗忘门忘、输入门写、输出门读,细胞状态是传送带;GRU 是简化版三个门变两个。GAN:生成器和判别器对抗博弈不是合作;模式崩溃是只生成少数样本。扩散模型:加噪再去噪,训练预测噪声、推理从纯噪声迭代去噪;Stable Diffusion 在潜在空间扩散才省算力。Transformer:自注意力是每个词跟所有词算相关性,用 Q/K/V;多头是分多组并行算;位置编码是因为注意力本身无位置感知。GPT vs BERT:自回归单向生成 vs 双向编码理解,这个对比很重要。LoRA:冻结原模型只训练低秩小矩阵,可合并权重所以推理零开销;Adapter 会增加推理延迟;Prompt Tuning 最轻量只动输入。SFT/RLHF/DPO:SFT 是监督微调打基础,RLHF 用奖励模型+PPO 做偏好优化(复杂不稳定),DPO 直接用偏好数据做监督式优化(简单稳定),一般先 SFT 再二选一对齐。PPO 是 RLHF 里最常用的对齐算法。提示工程:零样本无示例、少样本有示例、CoT 让模型逐步推理;RAG 是检索+生成解决幻觉,不是微调。评估指标:BLEU 重精确率用于翻译,ROUGE 重召回率用于摘要,别记反;CLIP Score 评图文匹配;MMLU 是英文综合、C-Eval 是中文综合。