01 - 核心概念
在深入具体算法之前,我们需要建立一些共同的基础。这一章将通过对话的形式,帮助你理解后续所有算法的共同语言。
目录
什么是神经网络?
对话 1: 从拟合函数说起
🤔 学生:我在很多地方看到”神经网络”,但它到底是什么?
💡 导师:让我换个角度问你——你觉得 y = 2x + 3 是什么?
🤔 学生:这是个线性函数,给定 x 就能算出 y。
💡 导师:没错!现在如果我给你 100 个 (x, y) 的点,让你猜背后的函数是什么,你会怎么做?
🤔 学生:我会画出这些点,看看它们的趋势,可能是条直线,也可能是曲线…
💡 导师:这就是函数拟合!神经网络本质上就是一个超级灵活的函数——你给它输入输出的例子,它调整内部参数,最终能模仿这个输入输出关系。只不过它能拟合的函数复杂得多:图像→标签、文本→下一个词、棋盘→最佳走法…
🤔 学生:所以神经网络 = 可以学习的复杂函数?
💡 导师:可以这么理解。而”神经元”就是这个复杂函数的基本单元。
✨ 关键洞察:
- 神经网络 = 参数化的函数
f(x; θ),其中θ是可学习的参数 - 训练 = 调整
θ让函数拟合数据 - “网络”是因为由很多简单函数组合而成
对话 2: 为什么需要多层?
🤔 学生:既然神经网络是函数,为什么需要多层?一层不够吗?
💡 导师:想象你要识别照片中的猫。一层神经网络能做什么?
🤔 学生:嗯…直接从像素判断是不是猫?
💡 导师:对,但这很难。像素值是”左上角第10个点是红色120”这样的低级信息,而”是否是猫”是高级概念。中间跨度太大了。
🤔 学生:那多层有什么帮助?
💡 导师:多层可以逐步抽象:
- 第1层:从像素检测边缘和纹理
- 第2层:从边缘组合成耳朵、眼睛等局部特征
- 第3层:从局部特征组合成”猫脸”这样的整体概念
- 最后一层:判断是否是猫
每层都在前一层的基础上构建更抽象的表示。这叫层次化特征学习。
🤔 学生:所以深度学习的”深度”就是指层数多?
💡 导师:正是!深度学习 = 深层神经网络 + 大数据训练。深度让网络能表达更复杂的函数。
✨ 关键洞察:
- 单层网络只能表达简单的线性组合
- 多层网络通过逐层抽象,能表达复杂的非线性关系
- 深度学习 = 深层网络 + 数据 + 计算
对话 3: 激活函数的作用
🤔 学生:我看到神经网络里有 ReLU、Sigmoid 这些,它们是什么?
💡 导师:想象你在搭积木。如果每块积木都是方形的,你能搭出圆形吗?
🤔 学生:搭不出,因为方形只能组成方形的组合…
💡 导师:对!这就是为什么需要激活函数。如果神经网络只做线性变换(矩阵乘法、加法),那多层网络其实等价于一层——线性叠线性还是线性。
🤔 学生:所以激活函数是为了引入非线性?
💡 导师:完全正确!激活函数就像给线性变换”加个弯”,让网络能拟合曲线。常见的激活函数:
- ReLU (Rectified Linear Unit):
f(x) = max(0, x)—— 负数变0,正数不变 - Sigmoid:
f(x) = 1 / (1 + e^(-x))—— 把任意值压缩到 0-1 之间 - Tanh:
f(x) = (e^x - e^(-x)) / (e^x + e^(-x))—— 压缩到 -1 到 1
🤔 学生:为什么 ReLU 这么简单却很常用?
💡 导师:因为它梯度友好——导数不是0就是1,不会出现梯度消失问题(后面会讲)。而且计算超快。
✨ 关键洞察:
- 激活函数引入非线性,让网络能拟合复杂函数
- 没有激活函数,多层网络退化为单层
- ReLU 因其简单和梯度特性成为主流
张量与矩阵运算
对话 4: 为什么用矩阵而不是循环?
🤔 学生:神经网络的代码里到处都是矩阵乘法,为什么不用 for 循环?
💡 导师:假设你要计算100个学生每人3门课的总分,你会怎么做?
🤔 学生:用 for 循环遍历每个学生,加起来他们的3门成绩…
💡 导师:可以。但如果我告诉你,把所有成绩排成一个 100×3 的表格,然后让每一行乘以 [1, 1, 1] 就能一次性得到所有人的总分呢?
🤔 学生:这就是矩阵乘法?确实快很多!
💡 导师:没错。神经网络的计算本质上是批量运算——100张图片一起处理比一张张处理快得多。而矩阵乘法让GPU可以并行计算所有元素。
🤔 学生:所以矩阵运算 = 批量运算 = 并行化?
💡 导师:是的。这也是为什么神经网络代码看起来很”数学”——不是为了装X,而是为了性能。
✨ 关键洞察:
- 矩阵运算 = 向量化 = 批量计算
- GPU 擅长并行的矩阵运算
- 用循环写神经网络会慢100倍以上
对话 5: 张量的维度含义
🤔 学生:我经常看到 (batch, seq_len, d_model) 这样的形状,每个维度代表什么?
💡 导师:把张量想象成一摞纸。每个维度告诉你某个层级的”数量”:
- batch: 一次处理多少个样本(比如32张图片)
- seq_len: 序列长度(比如一句话有20个词)
- d_model: 特征维度(每个词用256维向量表示)
所以 (32, 20, 256) 的张量表示:32个样本,每个20个词,每个词是256维向量。
🤔 学生:为什么要batch一起处理?一个一个来不行吗?
💡 导师:技术上可以,但batch有两个好处:
- 并行化:GPU 一次计算32个样本和计算1个样本时间差不多
- 梯度稳定:多个样本的梯度平均起来,更能代表整体趋势
🤔 学生:那batch size是不是越大越好?
💡 导师:不是。太大会:
- 内存爆炸
- 梯度更新频率降低(收敛慢)
- 可能错过局部最优解
通常选32/64/128这样的2的幂次(对GPU友好)。
✨ 关键洞察:
- 张量维度 = 数据的组织方式
- batch 维度是为了并行化和梯度稳定
- 形状匹配是神经网络调试的常见问题
前向传播与反向传播
对话 6: 前向传播 —— 数据如何流动
🤔 学生:什么是”前向传播”?
💡 导师:就是输入 → 网络 → 输出的计算过程。比如你有一个3层网络:
输入 x
↓ 第1层: W1 @ x + b1
↓ 激活函数: ReLU
↓ 第2层: W2 @ h1 + b2
↓ 激活函数: ReLU
↓ 第3层: W3 @ h2 + b3
↓ 输出 y
从上往下计算一遍,就是前向传播。
🤔 学生:这看起来很简单,为什么要特意起个名字?
💡 导师:因为训练时还需要反向传播——沿着相反方向计算梯度。前向传播要保存中间结果(h1, h2),供反向传播使用。
✨ 关键洞察:
- 前向传播 = 按网络结构计算输出
- 需要保存中间激活值,供反向传播使用
- 推理(inference)时只需前向传播
对话 7: 反向传播 —— 梯度如何计算
🤔 学生:反向传播是什么?听起来很复杂…
💡 导师:想象你爬山迷路了,想知道”往哪个方向走能下降最快”。你会怎么做?
🤔 学生:往四周看看,哪边最陡就往哪边走。
💡 导师:没错!梯度就是”最陡的方向”。反向传播就是计算”损失函数相对于每个参数的梯度”的高效算法。
🤔 学生:为什么叫”反向”?
💡 导师:因为计算顺序和前向传播相反。前向传播是 输入→输出,反向传播是 输出→输入:
前向: x → h1 → h2 → y → loss
反向: ∂loss/∂y → ∂loss/∂h2 → ∂loss/∂h1 → ∂loss/∂x
利用链式法则,把复杂的导数分解成每一层的局部导数的乘积。
🤔 学生:链式法则是什么?
💡 导师:高中学过的求导法则:如果 z = f(y) 且 y = g(x),那么 dz/dx = (dz/dy) * (dy/dx)。
神经网络就是很长的函数链,反向传播就是应用链式法则,从后往前乘。
✨ 关键洞察:
- 反向传播 = 用链式法则高效计算梯度
- 梯度告诉我们”改变哪个参数对减少损失最有效”
- 自动微分框架(PyTorch/TensorFlow)帮我们自动做这件事
损失函数与梯度下降
对话 8: 什么是”好”的预测?
🤔 学生:训练时怎么知道模型预测得好不好?
💡 导师:需要一个评分标准。比如预测房价,如果预测100万但实际是90万,差了10万。如果预测80万,差了10万。哪个更好?
🤔 学生:好像一样糟糕?都差了10万。
💡 导师:对!所以我们用平方误差: (预测值 - 真实值)²。100万的误差是 10² = 100,80万的误差也是 10² = 100。
但如果预测95万,误差是 5² = 25——平方让”接近真实值”的预测获得更高分。
🤔 学生:所以损失函数就是”预测和真实值的差距”?
💡 导师:是的。常见的损失函数:
- 均方误差(MSE):用于回归任务,
(y_pred - y_true)² - 交叉熵(Cross-Entropy):用于分类任务,衡量预测概率分布和真实分布的差异
- Huber Loss:MSE和绝对值的混合,对异常值更鲁棒
训练的目标就是最小化损失函数。
✨ 关键洞察:
- 损失函数量化”模型有多错”
- 不同任务用不同损失函数
- 训练 = 调整参数让损失下降
对话 9: 梯度下降的直觉
🤔 学生:知道损失之后,怎么改进模型?
💡 导师:回到爬山的例子。你站在山坡上,想走到最低点。你会:
- 看看四周,哪个方向最陡(计算梯度)
- 往那个方向走一小步(更新参数)
- 重复,直到到达谷底
这就是梯度下降!
🤔 学生:为什么要”走一小步”而不是直接跳到最低点?
💡 导师:因为我们不知道最低点在哪。梯度只告诉你”当前位置”的最陡方向,走远了可能方向就变了。
这”一小步”的大小叫学习率(learning rate)。太大会越过最低点震荡,太小会收敛很慢。
🤔 学生:那怎么选学习率?
💡 导师:经验和实验。通常从0.001或0.01开始尝试。现代优化器(如Adam)会自动调整学习率,我们后面会讲到。
✨ 关键洞察:
- 梯度下降 = 沿梯度反方向逐步调整参数
- 学习率控制每次调整的步长
- 训练 = 多次梯度下降迭代
对话 10: 为什么梯度指向”上坡”方向?
🤔 学生:你说梯度是”最陡的方向”,但我们想让损失下降,为什么要沿梯度反方向走?
💡 导师:好问题!梯度的数学定义是”函数增长最快的方向”。
想象损失是海拔,梯度指向”往哪走海拔升得最快”,那么梯度的反方向就是”往哪走海拔降得最快”。
🤔 学生:所以参数更新公式是 θ = θ - lr * gradient,这个减号就是”反方向”?
💡 导师:完全正确!这个公式就是梯度下降的核心:
# 伪代码
for step in range(num_steps):
loss = compute_loss(model, data)
gradient = compute_gradient(loss, model.params)
model.params = model.params - learning_rate * gradient✨ 关键洞察:
- 梯度 = 函数增长最快的方向
- 梯度下降 = 沿梯度反方向走
- 参数更新 =
θ ← θ - lr * ∇θ
过拟合与泛化
对话 11: 记忆 vs 理解
🤔 学生:如果模型在训练数据上表现完美,是不是就训练好了?
💡 导师:不一定。想象你准备考试,是背答案还是理解知识?
🤔 学生:理解当然更好,但背答案考试也能拿高分啊?
💡 导师:如果考的题和练习题一模一样,背答案确实可以。但如果题目稍微变化呢?比如把数字换了?
🤔 学生:那就完了…背答案的人答不出来。
💡 导师:这就是过拟合(overfitting)!模型把训练数据记住了,但没有学到背后的规律,遇到新数据就失效。
好的模型应该泛化(generalize)——在没见过的数据上也表现好。
✨ 关键洞察:
- 过拟合 = 模型记住训练数据,但不能泛化
- 泛化能力 = 模型在新数据上的表现
- 训练集高分 ≠ 模型好,要看测试集
对话 12: 训练集、验证集、测试集
🤔 学生:怎么知道模型有没有过拟合?
💡 导师:把数据分成三份:
- 训练集(train set):用来训练模型(调整参数)
- 验证集(validation set):用来调超参数(学习率、层数等)
- 测试集(test set):最后评估模型,绝对不能在训练中看到
如果训练集准确率95%,但验证集只有70%,说明过拟合了。
🤔 学生:为什么要分三份?训练集和测试集不够吗?
💡 导师:因为你训练时会根据验证集表现调整模型(比如”验证集效果不好,加一层试试”)。这样验证集的信息也”泄漏”到模型里了。
测试集必须是完全没见过的,才能真实反映泛化能力。
🤔 学生:比例怎么分?
💡 导师:常见的是 70%训练 / 15%验证 / 15%测试,或 80% / 10% / 10%。数据很多时,验证集和测试集可以少一点。
✨ 关键洞察:
- 三份数据各有用途,不能混用
- 验证集用来调超参数,测试集用来最终评估
- 测试集一定要保持”干净”,不能影响训练决策
对话 13: 正则化的直觉
🤔 学生:怎么防止过拟合?
💡 导师:想象你在拟合一些点。用一个10次多项式,你可以让曲线完美穿过每个点,但曲线会非常扭曲。如果用2次多项式,虽然不能完美拟合,但曲线更平滑,泛化更好。
这就是**正则化(regularization)**的思想:限制模型复杂度,防止它过度拟合训练数据。
🤔 学生:具体怎么做?
💡 导师:常见方法:
- L2正则化(权重衰减):在损失函数中加入
λ * Σ(w²),惩罚过大的权重 - Dropout:训练时随机”丢弃”一些神经元,防止神经元之间过度依赖
- Early Stopping:验证集效果不再提升时,停止训练
- 数据增强:通过旋转、裁剪等方式”制造”更多训练数据
🤔 学生:为什么惩罚大权重能防止过拟合?
💡 导师:大权重意味着模型对某些特征过度敏感。输入稍微变化,输出就剧烈波动。小权重让模型更”保守”,更鲁棒。
✨ 关键洞察:
- 正则化 = 限制模型复杂度
- 不是让模型在训练集上完美,而是在训练集和验证集间平衡
- Dropout、权重衰减是常用正则化技术
为什么需要这么多算法?
对话 14: 没有免费的午餐
🤔 学生:既然神经网络这么强大,为什么还有这么多不同的算法?不能一个算法解决所有问题吗?
💡 导师:这涉及到机器学习的一个基本定理——没有免费的午餐(No Free Lunch):不存在对所有问题都最优的算法。
🤔 学生:为什么?
💡 导师:不同任务有不同的特点:
- 图像:空间局部性(相邻像素相关)→ CNN 利用这个先验
- 文本:序列依赖性(前面的词影响后面)→ RNN/Transformer 处理序列
- 时间序列:长程依赖和因果性 → SSM/Mamba 线性时间建模
每个算法都是针对某类问题的**归纳偏置(inductive bias)**设计的。
✨ 关键洞察:
- 没有万能算法,只有适合特定任务的算法
- 算法设计 = 把对问题的理解编码成网络结构
- 通用性和效率往往是权衡
对话 15: 算法演化的三条主线
🤔 学生:这么多算法,有没有什么脉络可以梳理?
💡 导师:可以从三个维度理解算法的演化:
1. 建模能力的提升
简单MLP → CNN(空间结构) → RNN(序列) → Transformer(全局注意力) → SSM(线性时间)
每一步都在解决前一代的局限。
2. 训练技术的改进
SGD → Momentum → Adam → LoRA → QLoRA
让模型训练更快、更稳定、更经济。
3. 推理效率的优化
标准Attention → Flash Attention → KV-Cache → PagedAttention → Quantization
让模型部署更快、更省内存。
🤔 学生:所以学习顺序应该按照这个演化路线?
💡 导师:对!理解”为什么A不够好 → 所以有了B”比孤立地学每个算法重要得多。这也是本教程的组织逻辑。
✨ 关键洞察:
- 算法不是孤立的,而是相互演化的
- 理解”问题 → 解决方案”的脉络比记住细节重要
- 本教程按演化主线组织,而非简单罗列
最小神经网络示例
让我们用代码把前面的概念串起来。下面是一个最小的2层神经网络,展示前向传播、损失计算、反向传播的完整流程。
import math
# === 数据 ===
# 简单的"异或"问题: 输入两个0/1,输出它们的异或值
X = [[0, 0], [0, 1], [1, 0], [1, 1]] # 4个样本
y = [0, 1, 1, 0] # 对应标签
# === 参数初始化 ===
# 2层网络: 输入2维 → 隐藏层2维 → 输出1维
# 权重随机初始化(小的随机数)
W1 = [[0.5, -0.3], [0.2, 0.4]] # 2x2 隐藏层权重
b1 = [0.1, -0.1] # 隐藏层偏置
W2 = [[0.6], [-0.5]] # 2x1 输出层权重
b2 = [0.0] # 输出层偏置
# === 激活函数 ===
def sigmoid(x):
"""Sigmoid激活: 输出0-1之间"""
return 1 / (1 + math.exp(-x))
def sigmoid_derivative(x):
"""Sigmoid的导数: s(x) * (1 - s(x))"""
s = sigmoid(x)
return s * (1 - s)
# === 前向传播 ===
def forward(x):
"""
x: 输入 [2维]
返回: 输出值, 以及中间激活(供反向传播使用)
"""
# 第1层: z1 = W1 @ x + b1
z1 = [W1[i][0]*x[0] + W1[i][1]*x[1] + b1[i] for i in range(2)]
# 激活: h1 = sigmoid(z1)
h1 = [sigmoid(z) for z in z1]
# 第2层: z2 = W2 @ h1 + b2
z2 = [W2[0][0]*h1[0] + W2[1][0]*h1[1] + b2[0]]
# 激活: output = sigmoid(z2)
output = sigmoid(z2[0])
return output, (z1, h1, z2) # 返回输出和中间值
# === 损失函数 ===
def loss(pred, true):
"""均方误差"""
return 0.5 * (pred - true) ** 2
# === 反向传播(手工版,展示原理) ===
def backward(x, y_true, lr=0.1):
"""
计算梯度并更新参数
lr: 学习率
"""
global W1, b1, W2, b2
# 前向传播,保存中间值
y_pred, (z1, h1, z2) = forward(x)
# 计算损失对输出的梯度
d_loss = y_pred - y_true # ∂loss/∂output
# 反向传播第2层
d_z2 = d_loss * sigmoid_derivative(z2[0]) # 链式法则
d_W2 = [[d_z2 * h1[i]] for i in range(2)] # ∂loss/∂W2
d_b2 = [d_z2] # ∂loss/∂b2
d_h1 = [W2[i][0] * d_z2 for i in range(2)] # 传播到h1
# 反向传播第1层
d_z1 = [d_h1[i] * sigmoid_derivative(z1[i]) for i in range(2)]
d_W1 = [[d_z1[i] * x[j] for j in range(2)] for i in range(2)]
d_b1 = d_z1
# 梯度下降更新参数
W2 = [[W2[i][j] - lr * d_W2[i][j] for j in range(1)] for i in range(2)]
b2 = [b2[i] - lr * d_b2[i] for i in range(1)]
W1 = [[W1[i][j] - lr * d_W1[i][j] for j in range(2)] for i in range(2)]
b1 = [b1[i] - lr * d_b1[i] for i in range(2)]
return loss(y_pred, y_true)
# === 训练 ===
print("训练开始...")
for epoch in range(1000):
total_loss = 0
for i in range(4):
l = backward(X[i], y[i], lr=0.5) # 学习率0.5
total_loss += l
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {total_loss:.4f}")
# === 测试 ===
print("\n测试结果:")
for i in range(4):
pred, _ = forward(X[i])
print(f"输入: {X[i]} → 预测: {pred:.4f}, 真实: {y[i]}")运行这段代码,你会看到:
- 损失逐渐下降(模型在学习)
- 最终预测接近真实值(泛化能力)
代码中的关键点:
forward(): 实现前向传播,返回中间激活值backward(): 手工计算梯度(真实项目用自动微分)- 参数更新:
θ = θ - lr * gradient - 训练循环: 多次遍历数据,逐步优化
本章小结
我们建立了后续所有算法的共同基础:
✅ 神经网络 = 可学习的参数化函数
✅ 深度 = 逐层抽象,表达复杂关系
✅ 激活函数 = 引入非线性
✅ 张量 = 批量数据的高效表示
✅ 前向传播 = 计算输出
✅ 反向传播 = 计算梯度(链式法则)
✅ 损失函数 = 量化”有多错”
✅ 梯度下降 = 沿梯度反方向调整参数
✅ 过拟合 = 记忆vs理解,需要正则化
✅ 泛化 = 模型在新数据上的表现
下一站
现在你已经掌握了机器学习的”通用语言”,是时候进入真正的算法世界了!
👉 02-基础轨道-Transformer之旅 - 从 Tokenizer 到 GPT 的完整链条,理解现代语言模型的核心
或者,如果你对其他主题更感兴趣: