详情

首页手游攻略 AI 深度学习基础

AI 深度学习基础

佚名 2026-08-07 10:47:04

深度学习不是什么神秘的黑科技,它本质上是一个数学函数拟合器——给它大量输入输出数据,它自动找出它们之间的复杂关系。

举个例子:你给它一百万张猫的图片,每张图片对应"猫"这个标签。深度学习自动找出什么样的像素组合看起来像猫。

再比如:你给它十亿句人类语言,每句话的下一个词作为目标。深度学习自动学会给定上文,下一个词最可能是什么。

深度学习的核心优势是:随着数据量增加,它的效果持续提升 。这是传统机器学习做不到的。

在这一章,我们会从零开始,一步步拆解深度学习的每个组件:

  1. 神经网络的数学基础
  2. 前向传播的计算过程
  3. 激活函数的作用
  4. 损失函数的设计
  5. 反向传播的原理
  6. 优化器的选择
  7. 正则化技术
  8. 归一化方法
  9. 学习率调度

最后,我们用 PyTorch 实现一个完整的多层感知机(MLP),把所有知识点串联起来。

这一章会涉及一些数学,但不用担心——我们会用直觉解释每一个公式,而不是让你死记硬背。重点是理解为什么这样设计,而不是怎么计算。

神经网络数学基础

深度学习建立在三个数学分支之上:线性代数、微积分、概率论。

你不需要成为数学专家,但需要理解几个核心概念。

线性代数:矩阵乘法

神经网络最基本的运算就是矩阵乘法。

为什么用矩阵?因为它能简洁地表示"多个输入经过多个神经元变换"这个过程。

先看一个直观的例子:

实例

# ============================================

# 矩阵乘法的直观理解

# ============================================

import numpy as np

# 假设输入是一个 3 维向量:[身高, 体重, 年龄]

# 单位分别是:厘米、公斤、岁

x = np.array([175,70,25])# 输入向量,形状 (3,)

print(f"输入 x: {x}")

print(f"输入形状: {x.shape}")

# 权重矩阵 W:2 个神经元,每个神经元接收 3 个输入

# 形状是 (输出维度, 输入维度) = (2, 3)

W = np.array([

[0.1,0.2,0.3],# 第 1 个神经元的权重

[0.4,0.5,0.6],# 第 2 个神经元的权重

])

print(f"n权重矩阵 W:n{W}")

print(f"权重形状: {W.shape}")

# 偏置 b:每个神经元一个偏置

b = np.array([0.1,0.2])# 形状 (2,)

print(f"n偏置 b: {b}")

print(f"偏置形状: {b.shape}")

# 矩阵乘法:y = W · x + b

# 注意:numpy 的 @ 运算符表示矩阵乘法

y = W @ x + b

print(f"n输出 y: {y}")

print(f"输出形状: {y.shape}")

# 手动计算验证一下

y1 =0.1 * 175 + 0.2 * 70 + 0.3 * 25 + 0.1# 第 1 个神经元

y2 =0.4 * 175 + 0.5 * 70 + 0.6 * 25 + 0.2# 第 2 个神经元

print(f"n手动计算验证: [{y1}, {y2}]")

矩阵乘法的几何意义是"线性变换"——它可以旋转、缩放、拉伸向量空间。

但只有线性变换是不够的。

如果每一层都只是矩阵乘法,那么无论网络有多深,整个网络等价于一个单层网络。因为多个线性变换的组合仍然是线性变换。

这就是为什么我们需要激活函数——引入非线性。

微积分:导数与链式法则

训练神经网络的核心是"梯度下降"——找到让损失最小的参数方向。

梯度就是"多元函数的导数"——它告诉我们,每个参数变化一点,损失会变化多少。

先看一个简单的例子:

实例

# ============================================

# 导数的直观理解

# ============================================

def f(x):

"""一个简单的函数:f(x) = x²"""

return x 2

def numerical_derivative(f, x, h=1e-6):

"""数值导数:用微小增量 h 近似计算导数

导数定义:f'(x) = lim(h→0) [f(x+h) - f(x)] / h

"""

return(f(x + h) - f(x)) / h

# 在 x=3 处计算导数

x =3.0

df_dx = numerical_derivative(f, x)

print(f"f({x}) = {f(x)}")

print(f"f'({x}) ≈ {df_dx}")

print(f"解析解(精确值):2 * {x} = {2 * x}")

# 理解导数的意义:

# 导数 6 表示:在 x=3 处,x 增加 1,f(x) 大约增加 6

x_new = x + 0.01

f_new = f(x_new)

print(f"nx 从 {x} 增加到 {x_new}")

print(f"f(x) 从 {f(x)} 变为 {f_new}")

print(f"实际增加:{f_new - f(x)}")

print(f"导数预测:{df_dx * 0.01}")

对于多元函数,我们需要计算每个变量的偏导数,然后把它们组合成一个向量,这就是梯度。

链式法则是反向传播的数学基础。它让我们能够"层层拆解"复合函数的导数。

实例

# ============================================

# 链式法则的直观理解

# ============================================

# 考虑复合函数:y = f(g(x))

# 其中:g(x) = x², f(z) = z³

# 那么:y = (x²)³ = x⁶

def g(x):

return x 2

def f(z):

return z 3

def y(x):

return f(g(x))

# 手动计算导数(链式法则):

# dy/dx = df/dz * dz/dx = 3*z² * 2*x = 3*(x²)² * 2*x = 6*x⁵

x =2.0

z = g(x)# z = 4

dy_dx_chain =3 * (z 2) * 2 * x # 链式法则计算

print(f"链式法则计算:dy/dx = {dy_dx_chain}")

# 数值导数验证

def numerical_derivative_y(x, h=1e-6):

return(y(x + h) - y(x)) / h

dy_dx_numerical = numerical_derivative_y(x)

print(f"数值导数验证:dy/dx ≈ {dy_dx_numerical}")

# 解析解:6*x^5 = 6*32 = 192

print(f"解析解:6 * {x}^5 = {6 * (x 5)}")

链式法则的核心思想是: 把复杂函数拆成简单函数,分别求导再相乘

反向传播就是链式法则在神经网络中的应用——从输出开始,一层一层往回传梯度。

概率论:条件概率

分类问题中,神经网络输出的往往是"概率分布"——给定输入,每个类别的概率是多少。

条件概率 P(Y|X) 表示"在已知 X 发生的情况下,Y 发生的概率"。

举个例子:P(下雨|乌云密布) 表示"看到乌云密布时,下雨的概率"。

在深度学习中,我们用神经网络来估计这个条件概率:

实例

# ============================================

# 用神经网络输出概率分布

# ============================================

import numpy as np

def softmax(x):

"""Softmax 函数:把任意实数转换成概率分布

输出所有值的和为 1,每个值在 [0, 1] 之间

"""

# 减去最大值防止数值溢出

exp_x = np.exp(x - np.max(x))

return exp_x / np.sum(exp_x)

# 假设神经网络最后一层输出了 3 个"分数"(logits)

# 分别对应"猫"、"狗"、"鸟"

logits = np.array([2.0,1.0,0.5])

print(f"神经网络输出(logits):{logits}")

# 用 Softmax 转换成概率

probabilities = softmax(logits)

print(f"概率分布:{probabilities}")

print(f"概率和:{np.sum(probabilities)}")

# 解读:

# P(猫|输入图片) ≈ 0.67

# P(狗|输入图片) ≈ 0.24

# P(鸟|输入图片) ≈ 0.09

print("n类别概率:")

print(f" 猫: {probabilities[0]:.2%}")

print(f" 狗: {probabilities[1]:.2%}")

print(f" 鸟: {probabilities[2]:.2%}")

三个数学基础的核心要点:

数学分支核心概念在深度学习中的用途
线性代数矩阵乘法、向量、张量表示网络结构,高效计算前向传播
微积分导数、链式法则、梯度反向传播,更新参数
概率论条件概率、概率分布建模不确定性,设计损失函数

前向传播

前向传播就是输入数据"流过"神经网络,从输入层到隐藏层再到输出层的计算过程。

计算图

把神经网络表示成计算图,能让前向传播和反向传播的逻辑更清晰。

计算图由节点(运算)和边(数据流)组成。

一个简单的两层神经网络的计算图:

实例

# ============================================

# 用计算图理解前向传播

# ============================================

import numpy as np

def relu(x):

"""ReLU 激活函数:max(0, x)"""

return np.maximum(0, x)

# 一个简单的两层神经网络

# 输入层 (2) → 隐藏层 (3) → 输出层 (2)

# 输入:x = [x1, x2]

x = np.array([1.0,2.0])

print(f"输入 x: {x}")

# 第一层:输入 → 隐藏层

W1 = np.array([

[0.1,0.2],# 第 1 个隐藏神经元

[0.3,0.4],# 第 2 个隐藏神经元

[0.5,0.6],# 第 3 个隐藏神经元

])

b1 = np.array([0.1,0.2,0.3])

# 第二层:隐藏层 → 输出层

W2 = np.array([

[0.1,0.2,0.3],# 第 1 个输出神经元

[0.4,0.5,0.6],# 第 2 个输出神经元

])

b2 = np.array([0.1,0.2])

# 前向传播计算图

# 步骤 1: 隐藏层线性变换

z1 = W1 @ x + b1

print(f"n隐藏层线性变换 z1: {z1}")

# 步骤 2: 隐藏层激活函数

a1 = relu(z1)

print(f"隐藏层激活后 a1: {a1}")

# 步骤 3: 输出层线性变换

z2 = W2 @ a1 + b2

print(f"输出层线性变换 z2: {z2}")

# 步骤 4: 输出层激活(如果是分类问题,通常用 Softmax)

a2 = softmax(z2)# 使用前面定义的 softmax 函数

print(f"输出层激活后 a2: {a2}")

计算图的好处是:每一步都很清晰,反向传播时只要按照相反的顺序计算梯度即可。

矩阵乘法视角下的神经网络

从矩阵乘法的角度看,神经网络就是一连串的"线性变换 + 非线性激活"。

每一层可以抽象成:

aᵢ = activation(Wᵢ · aᵢ₋₁ + bᵢ)

其中 a₀ 就是输入 x。

这个公式简洁但强大——它能表示从简单的逻辑回归到复杂的 Transformer 的几乎所有神经网络结构。

注意矩阵乘法的维度匹配:如果输入向量是 n 维,下一层有 m 个神经元,那么权重矩阵 W 的形状一定是 m×n。这样 W·x 之后得到的是 m 维向量。

激活函数的作用

前面提到过:没有激活函数,深层网络等价于单层网络。

用一个简单的例子证明这一点:

实例

# ============================================

# 为什么需要激活函数

# ============================================

import numpy as np

# 假设有一个两层网络,但都没有激活函数

# 输入

x = np.array([1.0,2.0])

# 第一层权重和偏置

W1 = np.array([[0.1,0.2],[0.3,0.4]])

b1 = np.array([0.1,0.2])

# 第二层权重和偏置

W2 = np.array([[0.5,0.6],[0.7,0.8]])

b2 = np.array([0.3,0.4])

# 两层分别计算

z1 = W1 @ x + b1

z2 = W2 @ z1 + b2

print(f"两层分别计算结果: {z2}")

# 合并成一层计算

# 数学上可以证明:z2 = (W2·W1)·x + (W2·b1 + b2)

W_combined = W2 @ W1

b_combined = W2 @ b1 + b2

z_combined = W_combined @ x + b_combined

print(f"合并成一层计算结果: {z_combined}")

# 结果完全一样!

print(f"n两个结果相等: {np.allclose(z2, z_combined)}")

print("结论:没有激活函数,深层网络 = 单层网络")

激活函数就是那个让深层网络有意义的关键组件——它引入非线性,让网络能够学习复杂的模式。

激活函数详解

激活函数决定了一个神经元的输出如何响应输入。

好的激活函数应该满足几个条件:

  1. 非线性:这是必须的
  2. 可微:这样才能计算梯度
  3. 计算简单:前向和反向传播都要快
  4. 不会饱和:梯度不会消失或爆炸

我们来看看最常用的几个激活函数。

Sigmoid:饱和与梯度消失

Sigmoid 是最早使用的激活函数之一。它把任意实数压缩到 (0, 1) 之间。

公式:σ(x) = 1 / (1 + e⁻ˣ)

实例

# ============================================

# Sigmoid 激活函数

# ============================================

import numpy as np

import matplotlib.pyplotas plt

def sigmoid(x):

"""Sigmoid 函数"""

return1 / (1 + np.exp(-x))

def sigmoid_derivative(x):

"""Sigmoid 的导数:σ'(x) = σ(x) * (1 - σ(x))"""

s = sigmoid(x)

return s * (1 - s)

# 测试一些值

x_values =[-10, -5, -2,0,2,5,10]

print("x | sigmoid(x) | sigmoid'(x)")

print("-" * 40)

for x in x_values:

s = sigmoid(x)

d = sigmoid_derivative(x)

print(f"{x:4} | {s:10.6f} | {d:12.6f}")

# 观察:当 x 很大或很小时,导数接近 0

# 这就是"梯度消失"问题

x_big =10.0

print(f"n当 x = {x_big} 时:")

print(f" sigmoid(x) = {sigmoid(x_big):.10f} (几乎是 1)")

print(f" sigmoid'(x) = {sigmoid_derivative(x_big):.10f} (几乎是 0)")

print("梯度消失了!反向传播时梯度传不回去。")

Sigmoid 的问题很明显:

  1. 当 |x| > 6 时,梯度几乎为 0,梯度消失
  2. 输出不是以 0 为中心,这会影响优化
  3. 计算指数比较慢

由于这些问题,Sigmoid 在现代深度学习中已经很少在隐藏层使用了。

ReLU 与变体

ReLU(Rectified Linear Unit)是现在最常用的激活函数。

公式:ReLU(x) = max(0, x)

简单但非常有效。

实例

# ============================================

# ReLU 及其变体

# ============================================

import numpy as np

def relu(x):

"""ReLU:max(0, x)"""

return np.maximum(0, x)

def relu_derivative(x):

"""ReLU 的导数:x > 0 时为 1,否则为 0"""

return(x >0).astype(float)

def leaky_relu(x, alpha=0.01):

"""Leaky ReLU:x > 0 时为 x,否则为 alpha*x"""

return np.where(x >0, x, alpha * x)

def gelu(x):

"""GELU:Gaussian Error Linear Unit

这是 Transformer 中常用的激活函数

近似公式:0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715*x^3)))

"""

cdf =0.5 * (1.0 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * np.power(x,3))))

return x * cdf

# 测试

x_values =[-5, -2, -1,0,1,2,5]

print("x | ReLU | Leaky | GELU")

print("-" * 45)

for x in x_values:

r = relu(x)

lr = leaky_relu(x)

g = gelu(x)

print(f"{x:4} | {r:4.2f} | {lr:5.2f} | {g:5.2f}")

# ReLU 的优势:

print("nReLU 的优势:")

print("1. 计算简单:只需要 max 操作")

print("2. 不会饱和(正区间):梯度始终是 1")

print("3. 稀疏性:部分神经元输出为 0,增加模型鲁棒性")

# ReLU 的问题:"死亡 ReLU"

print("nReLU 的问题:死亡 ReLU")

print("如果一个神经元的输入总是负的,那么:")

print(" - 输出始终为 0")

print(" - 梯度始终为 0")

print(" - 参数永远不会更新")

print(" - 这个神经元就'死'了")

几种常见激活函数的对比:

激活函数公式优点缺点适用场景
Sigmoid1/(1+e⁻ˣ)输出在 (0,1),可解释为概率梯度消失、非零中心、计算慢二分类输出层(已很少用于隐藏层)
ReLUmax(0, x)计算快、不饱和、稀疏性死亡 ReLU、输出非零中心大多数隐藏层(默认选择)
Leaky ReLUmax(αx, x)解决死亡 ReLU 问题α 需要手动选择ReLU 失效时的替代
GELUx·Φ(x)平滑、在 Transformer 中效果好计算稍复杂Transformer、BERT、GPT 等
Swish/SwiGLUx·σ(βx)在深层网络中优于 ReLU计算复杂PaLM、LLaMA 等大模型

选择原则

激活函数的选择建议:

  1. 隐藏层:优先用 ReLU,简单有效
  2. 如果遇到死亡 ReLU:试试 Leaky ReLU 或 GELU
  3. Transformer:GELU 或 SwiGLU 是标准选择
  4. 二分类输出层:Sigmoid(输出概率)
  5. 多分类输出层:Softmax(输出概率分布)
  6. 回归输出层:不加激活(输出任意实数)

不要过度纠结激活函数的选择。通常 ReLU 就足够好了。当你确认 ReLU 是瓶颈时,再换其他的也不迟。

损失函数

损失函数衡量"模型的预测"和"真实答案"之间的差距。

训练的目标就是让这个差距尽可能小。

交叉熵损失(分类)

分类问题最常用的损失函数是交叉熵损失(Cross-Entropy Loss)。

它的直观含义是:"模型预测的概率分布和真实分布之间有多大差异"。

实例

# ============================================

# 交叉熵损失

# ============================================

import numpy as np

def cross_entropy_loss(probs, target_one_hot):

"""交叉熵损失

probs: 模型预测的概率分布,形状 (batch_size, num_classes)

target_one_hot: 真实标签的 one-hot 编码,形状同上

"""

# 加上 epsilon 防止 log(0)

epsilon =1e-12

probs = np.clip(probs, epsilon,1.0 - epsilon)

# 交叉熵 = -sum(target * log(pred))

return -np.sum(target_one_hot * np.log(probs)) / len(probs)

def softmax(x):

"""Softmax 函数"""

exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))

return exp_x / np.sum(exp_x, axis=-1, keepdims=True)

# 例子:三分类问题,类别是"猫"、"狗"、"鸟"

# 假设真实标签是"猫"(索引 0),one-hot 编码

target = np.array([[1,0,0]])# 正确答案是第 0 类

# 场景 1:模型很自信地预测"猫"

logits1 = np.array([[5.0,0.5,0.1]])# 猫的分数最高

probs1 = softmax(logits1)

loss1 = cross_entropy_loss(probs1, target)

print("场景 1:模型自信预测正确")

print(f" 概率分布: {probs1[0]}")

print(f" 损失: {loss1:.6f} (很小)")

# 场景 2:模型不确定

logits2 = np.array([[1.0,0.9,0.8]])# 三个类别分数差不多

probs2 = softmax(logits2)

loss2 = cross_entropy_loss(probs2, target)

print("n场景 2:模型不确定")

print(f" 概率分布: {probs2[0]}")

print(f" 损失: {loss2:.6f} (中等)")

# 场景 3:模型预测错误

logits3 = np.array([[0.1,5.0,0.5]])# 狗的分数最高

probs3 = softmax(logits3)

loss3 = cross_entropy_loss(probs3, target)

print("n场景 3:模型预测错误")

print(f" 概率分布: {probs3[0]}")

print(f" 损失: {loss3:.6f} (很大)")

交叉熵损失的一个简化版本:当标签是类别索引而不是 one-hot 编码时,可以用更高效的计算方式:

loss = -log(probs[target_class])

这就是为什么在 PyTorch 中,CrossEntropyLoss 直接接受类别索引作为标签。

MSE 损失(回归)

回归问题(预测连续值)最常用的损失是均方误差(Mean Squared Error,MSE)。

公式:MSE = mean((pred - target)²)

实例

# ============================================

# MSE 损失

# ============================================

import numpy as np

def mse_loss(pred, target):

"""均方误差损失"""

return np.mean((pred - target)2)

def mae_loss(pred, target):

"""平均绝对误差损失"""

return np.mean(np.abs(pred - target))

# 例子:预测房价(单位:万元)

# 真实房价

target = np.array([100,200,150])

# 场景 1:预测很准

pred1 = np.array([102,195,148])

mse1 = mse_loss(pred1, target)

mae1 = mae_loss(pred1, target)

print("场景 1:预测准确")

print(f" 预测: {pred1}")

print(f" 真实: {target}")

print(f" MSE: {mse1:.2f}")

print(f" MAE: {mae1:.2f}")

# 场景 2:预测有一个大误差

pred2 = np.array([102,280,148])# 第二个房子预测高了 80 万

mse2 = mse_loss(pred2, target)

mae2 = mae_loss(pred2, target)

print("n场景 2:有一个大误差")

print(f" 预测: {pred2}")

print(f" 真实: {target}")

print(f" MSE: {mse2:.2f} (被大误差放大很多)")

print(f" MAE: {mae2:.2f}")

# MSE vs MAE

print("nMSE vs MAE:")

print("- MSE 对大误差更敏感(因为平方)")

print("- MAE 对异常值更鲁棒")

print("- 一般情况下用 MSE 更多")

语言模型的预测下一词损失

大语言模型(如 GPT)的训练目标很简单:给定上文,预测下一个词。

这本质上是一个多分类问题——词汇表中的每个词都是一个类别。

实例

# ============================================

# 语言模型的损失函数

# ============================================

import numpy as np

def language_model_loss(logits, targets, vocab_size):

"""语言模型损失

本质上是对每个位置的交叉熵损失

"""

batch_size, seq_len, _ = logits.shape

total_loss =0.0

# 对序列中的每个位置计算损失

for i inrange(seq_len):

# 这个位置的预测分数

step_logits = logits[:, i, :]

# 转换成概率

step_probs = softmax(step_logits)

# 目标词的索引

step_target = targets[:, i]

# 只取目标词的概率

for b inrange(batch_size):

target_idx = step_target[b]

target_prob = step_probs[b, target_idx]

# 损失 = -log(p)

total_loss += -np.log(target_prob + 1e-12)

return total_loss / (batch_size * seq_len)

# 一个简单的例子

vocab_size =10000# 假设词汇表大小是 1 万

batch_size =2

seq_len =3

# 模型输出:(batch_size, seq_len, vocab_size)

logits = np.random.randn(batch_size, seq_len, vocab_size)

# 真实目标:每个位置应该预测的词索引

targets = np.array([

[123,456,789],# 第一个序列的目标词

[234,567,890],# 第二个序列的目标词

])

loss = language_model_loss(logits, targets, vocab_size)

print(f"语言模型损失: {loss:.4f}")

print("n这个损失的含义:")

print("- 对序列中的每个位置")

print("- 预测下一个词")

print("- 计算所有位置的平均交叉熵")

常用损失函数总结:

任务类型损失函数输出层激活标签格式
二分类Binary Cross-EntropySigmoid0 或 1
多分类Cross-EntropySoftmax类别索引
多标签分类Binary Cross-EntropySigmoid多个 0/1
回归MSE / MAE连续值
语言模型Cross-EntropySoftmax词索引

反向传播

反向传播(Backpropagation)是训练神经网络的核心算法。

它的目标是:计算损失函数对每个参数的梯度,然后用梯度下降更新参数。

链式法则推导

我们用一个简单的例子,一步步推导反向传播。

实例

# ============================================

# 手动实现反向传播

# ============================================

import numpy as np

def relu(x):

return np.maximum(0, x)

def relu_derivative(x):

return(x >0).astype(float)

def mse_loss(pred, target):

return np.mean((pred - target)2)

# 一个最简单的神经网络:一层线性变换

# y = w * x + b

# 损失 = (y - target)^2

# 输入和目标

x = np.array(2.0)

target = np.array(7.0)

# 初始化参数

w = np.array(1.0)

b = np.array(0.0)

print(f"初始参数: w = {w}, b = {b}")

print(f"目标: {target}")

# ============================================

# 第一步:前向传播

# ============================================

y = w * x + b

loss =(y - target)2

print(f"n前向传播:")

print(f" y = {y}")

print(f" loss = {loss}")

# ============================================

# 第二步:反向传播(手动计算梯度)

# ============================================

# 我们需要计算:d_loss/d_w, d_loss/d_b

# 用链式法则一步步来:

# loss = (y - target)^2

# d_loss/d_y = 2 * (y - target)

d_loss_d_y =2 * (y - target)

# y = w * x + b

# d_y/d_w = x

# d_y/d_b = 1

d_y_d_w = x

d_y_d_b =1.0

# 链式法则:

# d_loss/d_w = d_loss/d_y * d_y/d_w

# d_loss/d_b = d_loss/d_y * d_y/d_b

d_loss_d_w = d_loss_d_y * d_y_d_w

d_loss_d_b = d_loss_d_y * d_y_d_b

print(f"n反向传播:")

print(f" d_loss/d_y = {d_loss_d_y}")

print(f" d_loss/d_w = {d_loss_d_w}")

print(f" d_loss/d_b = {d_loss_d_b}")

# ============================================

# 第三步:梯度下降更新参数

# ============================================

learning_rate =0.1

w_new = w - learning_rate * d_loss_d_w

b_new = b - learning_rate * d_loss_d_b

print(f"n参数更新:")

print(f" w: {w} -> {w_new}")

print(f" b: {b} -> {b_new}")

# 验证:用新参数前向传播

y_new = w_new * x + b_new

loss_new =(y_new - target)2

print(f"n验证:")

print(f" 旧预测: {y}, 旧损失: {loss}")

print(f" 新预测: {y_new}, 新损失: {loss_new}")

print(f" 损失下降了!")

这个例子虽然简单,但包含了反向传播的所有核心思想:

  1. 前向传播:计算所有中间变量
  2. 反向传播:从输出开始,用链式法则层层计算梯度
  3. 参数更新:用梯度下降更新参数

计算图中的梯度流动

对于多层神经网络,梯度在计算图中反向流动。

每一层的梯度依赖于后一层传来的梯度。

实例

# ============================================

# 两层网络的反向传播

# ============================================

import numpy as np

def relu(x):

return np.maximum(0, x)

def relu_derivative(x):

return(x >0).astype(float)

# 一个两层网络

# 输入 (2) → 隐藏层 (3) → 输出 (1)

# 数据

x = np.array([1.0,2.0])# 输入

target = np.array([5.0])# 目标输出

# 参数初始化

W1 = np.array([[0.1,0.2],[0.3,0.4],[0.5,0.6]])# (3, 2)

b1 = np.array([0.1,0.2,0.3])# (3,)

W2 = np.array([[0.7,0.8,0.9]])# (1, 3)

b2 = np.array([0.4])# (1,)

print("初始参数已设置")

# ============================================

# 前向传播

# ============================================

print("n=== 前向传播 ===")

# 第一层

z1 = W1 @ x + b1 # (3,)

a1 = relu(z1)# (3,)

print(f"z1 = {z1}")

print(f"a1 = {a1}")

# 第二层

z2 = W2 @ a1 + b2 # (1,)

a2 = z2 # 回归任务,输出层不加激活

print(f"z2 = {z2}")

print(f"a2 = {a2}")

# 损失

loss = np.mean((a2 - target)2)

print(f"loss = {loss}")

# ============================================

# 反向传播

# ============================================

print("n=== 反向传播 ===")

# 输出层梯度

d_loss_d_a2 =2 * (a2 - target)# (1,)

d_a2_d_z2 =1.0# 没有激活函数

d_loss_d_z2 = d_loss_d_a2 * d_a2_d_z2 # (1,)

print(f"d_loss_d_z2 = {d_loss_d_z2}")

# 第二层参数梯度

d_z2_d_W2 = a1 # (3,),因为 z2 = W2@a1 + b2

d_z2_d_b2 =1.0# (1,)

d_z2_d_a1 = W2[0]# (3,)

# 计算参数梯度

d_loss_d_W2 = d_loss_d_z2.reshape(-1,1)@ d_z2_d_W2.reshape(1, -1)# (1, 3)

d_loss_d_b2 = d_loss_d_z2 * d_z2_d_b2 # (1,)

print(f"d_loss_d_W2 = {d_loss_d_W2}")

print(f"d_loss_d_b2 = {d_loss_d_b2}")

# 传到隐藏层的梯度

d_loss_d_a1 = d_loss_d_z2 @ d_z2_d_a1 # (3,)

print(f"d_loss_d_a1 = {d_loss_d_a1}")

# 隐藏层梯度

d_a1_d_z1 = relu_derivative(z1)# (3,)

d_loss_d_z1 = d_loss_d_a1 * d_a1_d_z1 # (3,)

print(f"d_loss_d_z1 = {d_loss_d_z1}")

# 第一层参数梯度

d_z1_d_W1 = x # (2,)

d_z1_d_b1 =1.0# (3,)

# 计算参数梯度

d_loss_d_W1 = d_loss_d_z1.reshape(-1,1)@ d_z1_d_W1.reshape(1, -1)# (3, 2)

d_loss_d_b1 = d_loss_d_z1 * d_z1_d_b1 # (3,)

print(f"d_loss_d_W1 = {d_loss_d_W1}")

print(f"d_loss_d_b1 = {d_loss_d_b1}")

# ============================================

# 参数更新

# ============================================

print("n=== 参数更新 ===")

learning_rate =0.01

W1_new = W1 - learning_rate * d_loss_d_W1

b1_new = b1 - learning_rate * d_loss_d_b1

W2_new = W2 - learning_rate * d_loss_d_W2

b2_new = b2 - learning_rate * d_loss_d_b2

print("参数已更新")

这个例子展示了反向传播的完整流程。

核心规律是:

  1. 对于线性层 y = Wx + b:dL/dW = (dL/dy)·xᵀ,dL/db = dL/dy
  2. 对于激活函数 y = f(z):dL/dz = dL/dy ⊙ f'(z)(⊙ 是逐元素相乘)
  3. 梯度从后往前传,每一层都用链式法则"积累"梯度

好消息是:现代深度学习框架(如 PyTorch、TensorFlow)会自动为你计算反向传播。你只需要定义前向传播,框架会用自动微分(autograd)搞定所有梯度计算。

梯度下降优化器

计算出梯度后,我们需要用优化器来更新参数。

最简单的优化器是随机梯度下降(SGD),但还有很多更高级的优化器。

SGD:随机梯度下降

公式:θ = θ - η·∇L(θ)

其中 η 是学习率。

实例

# ============================================

# SGD 优化器

# ============================================

import numpy as np

def f(x):

"""我们要最小化的函数:f(x) = x²"""

return x 2

def f_grad(x):

"""梯度:f'(x) = 2x"""

return2 * x

# SGD 优化

x =10.0# 初始值

learning_rate =0.1# 学习率

steps =20

print(f"初始 x = {x}, f(x) = {f(x)}")

print("-" * 40)

for step inrange(steps):

grad = f_grad(x)

x = x - learning_rate * grad # SGD 更新

print(f"Step {step+1}: x = {x:.6f}, f(x) = {f(x):.6f}")

print("-" * 40)

print(f"最终 x = {x:.6f}, f(x) = {f(x):.6f}")

print("理论最优:x = 0, f(x) = 0")

SGD 简单但有缺点:

  1. 对所有参数使用相同的学习率
  2. 收敛可能很慢
  3. 容易卡在局部最优或鞍点
  4. 对噪声敏感

Momentum:动量加速

Momentum 引入了"速度"的概念——让梯度更新带有惯性。

公式:

v = β·v + (1-β)·∇L(θ)θ = θ - η·v

β 通常设为 0.9。

实例

# ============================================

# Momentum 优化器

# ============================================

import numpy as np

def f(x):

"""目标函数"""

return x 2

def f_grad(x):

"""梯度"""

return2 * x

def sgd_optimize(x_init, learning_rate, steps):

"""纯 SGD"""

x = x_init

history =[x]

for _ inrange(steps):

grad = f_grad(x)

x = x - learning_rate * grad

history.append(x)

return np.array(history)

def momentum_optimize(x_init, learning_rate, beta, steps):

"""Momentum"""

x = x_init

v =0.0# 初始速度

history =[x]

for _ inrange(steps):

grad = f_grad(x)

v = beta * v + (1 - beta) * grad

x = x - learning_rate * v

history.append(x)

return np.array(history)

# 对比

x_init =10.0

learning_rate =0.1

beta =0.9

steps =20

sgd_history = sgd_optimize(x_init, learning_rate, steps)

momentum_history = momentum_optimize(x_init, learning_rate, beta, steps)

print("Step | SGD | Momentum")

print("-" * 35)

for i inrange(steps + 1):

print(f"{i:4} | {sgd_history[i]:9.6f} | {momentum_history[i]:9.6f}")

print("n观察:Momentum 收敛更快!")

print("因为它累积了过去的梯度,有惯性。")

Adam:自适应学习率

Adam(Adaptive Moment Estimation)是目前最常用的优化器之一。

它结合了 Momentum(一阶矩)和 RMSProp(二阶矩)的思想,为每个参数维护自适应的学习率。

公式:

m = β₁·m + (1-β₁)·∇L(θ)v = β₂·v + (1-β₂)·(∇L(θ))²m̂ = m / (1-β₁ᵗ)v̂ = v / (1-β₂ᵗ)θ = θ - η·m̂ / (√v̂ + ε)

超参数默认值:β₁=0.9, β₂=0.999, ε=1e-8, η=1e-3。

实例

# ============================================

# Adam 优化器

# ============================================

import numpy as np

def adam_optimize(x_init, learning_rate, beta1, beta2, epsilon, steps):

"""Adam 优化"""

x = x_init

m =0.0# 一阶矩

v =0.0# 二阶矩

history =[x]

for t inrange(1, steps + 1):

grad =2 * x # f(x) = x² 的梯度

# 更新一阶和二阶矩

m = beta1 * m + (1 - beta1) * grad

v = beta2 * v + (1 - beta2) * (grad 2)

# 偏差修正

m_hat = m / (1 - beta1 t)

v_hat = v / (1 - beta2 t)

# 参数更新

x = x - learning_rate * m_hat / (np.sqrt(v_hat) + epsilon)

history.append(x)

return np.array(history)

# 参数

x_init =10.0

learning_rate =0.5

beta1 =0.9

beta2 =0.999

epsilon =1e-8

steps =20

# 优化

history = adam_optimize(x_init, learning_rate, beta1, beta2, epsilon, steps)

print("Adam 优化过程:")

print("Step | x | f(x)")

print("-" * 35)

for i inrange(steps + 1):

print(f"{i:4} | {history[i]:9.6f} | {history[i]2:9.6f}")

AdamW:权重衰减修正

AdamW 是 Adam 的一个改进版本——它把权重衰减(Weight Decay)从梯度中分离出来。

这在正则化中很有用。

优化器对比:

优化器公式优点缺点适用场景
SGDθ = θ - η·∇L简单、稳定收敛慢、对学习率敏感数据量大、调参经验丰富
SGD + Momentumv = βv + (1-β)∇L

θ = θ - ηv

加速收敛、减少振荡需要调 β一般比纯 SGD 好
Adam自适应学习率收敛快、需要较少调参可能泛化不如 SGD大多数场景(默认选择)
AdamWAdam + 权重衰减更好的正则化需要调权重衰减系数Transformer、大模型

优化器选择建议:先试 Adam,学习率设为 1e-3(3e-4 是更稳妥的选择)。如果效果不满意,再尝试调参或换其他优化器。

正则化技术

正则化的目标是防止过拟合——让模型在未见过的数据上也能表现良好。

L1/L2 正则化

L1 和 L2 正则化是最基本的正则化方法。

它们在损失函数中加上对参数的惩罚:

L_total = L + λ·L_reg

L1 正则化(Lasso):L_reg = |w|

L2 正则化(Ridge):L_reg = w²

实例

# ============================================

# L1/L2 正则化

# ============================================

import numpy as np

def l1_regularization(weights, lambda_l1):

"""L1 正则化:返回正则化项和梯度"""

reg = lambda_l1 * np.sum(np.abs(weights))

grad = lambda_l1 * np.sign(weights)

return reg, grad

def l2_regularization(weights, lambda_l2):

"""L2 正则化:返回正则化项和梯度"""

reg =0.5 * lambda_l2 * np.sum(weights 2)

grad = lambda_l2 * weights

return reg, grad

# 例子

weights = np.array([1.0, -2.0,3.0,0.5])

print(f"权重: {weights}")

lambda_l1 =0.1

lambda_l2 =0.1

l1_reg, l1_grad = l1_regularization(weights, lambda_l1)

print(f"nL1 正则化项: {l1_reg:.4f}")

print(f"L1 梯度: {l1_grad}")

l2_reg, l2_grad = l2_regularization(weights, lambda_l2)

print(f"nL2 正则化项: {l2_reg:.4f}")

print(f"L2 梯度: {l2_grad}")

print("nL1 vs L2:")

print("- L1:倾向于产生稀疏解(很多参数变 0),可做特征选择")

print("- L2:倾向于让所有参数都很小,但不会变 0")

print("- 深度学习中 L2 更常用(或用 AdamW 的权重衰减)")

Dropout

Dropout 是一个简单但有效的正则化技术:训练时随机"关掉"一部分神经元。

这迫使模型学习更鲁棒的特征——它不能依赖任何特定的神经元。

实例

# ============================================

# Dropout

# ============================================

import numpy as np

def dropout_forward(x, dropout_rate, training=True):

"""Dropout 前向传播"""

ifnot training:

# 测试时不 dropout,直接返回

return x

# 生成 mask:随机选择一部分神经元置 0

mask =(np.random.rand(*x.shape)> dropout_rate).astype(float)

# 缩放:测试时不需要缩放,所以训练时要除以 (1-dropout_rate)

scale =1.0 / (1.0 - dropout_rate)

# 应用 dropout

out = x * mask * scale

return out, mask

# 测试

x = np.array([1.0,2.0,3.0,4.0,5.0])

dropout_rate =0.4

print(f"输入 x: {x}")

print(f"Dropout 率: {dropout_rate}")

# 多次运行,观察随机性

print("n多次 Dropout 结果:")

for i inrange(3):

out, mask = dropout_forward(x, dropout_rate, training=True)

print(f" 第 {i+1} 次: mask={mask}, out={out}")

# 测试模式

out_test = dropout_forward(x, dropout_rate, training=False)

print(f"n测试模式(不 Dropout): {out_test}")

数据增强

数据增强是最有效的正则化方法之一——通过变换现有数据来创造更多训练样本。

对于图像,常用的数据增强有:翻转、旋转、缩放、裁剪、颜色抖动等。

对于文本,常用的数据增强有:同义词替换、随机插入、随机删除、回译等。

正则化技术总结:

方法原理优点缺点
L2 正则化惩罚大的权重简单、稳定效果有限
Dropout随机关掉神经元简单有效减慢训练
数据增强增加训练数据多样性效果显著需要领域知识
早停(Early Stopping)验证集不提升就停止简单有效需要监控验证集
批归一化归一化层输入加速训练、正则化增加计算开销

批归一化 vs 层归一化

归一化技术可以加速训练,让优化更稳定。

内部协变量偏移

训练深度网络时,每一层的输入分布会随着前一层参数的变化而变化。这就是"内部协变量偏移"(Internal Covariate Shift)。

这会导致训练变慢,因为每一层都要不断适应新的分布。

归一化技术的目标就是:让每一层的输入分布保持稳定。

批归一化(Batch Normalization)

批归一化(BatchNorm)在 batch 维度上进行归一化。

公式:

μ = 1/N · sum(xᵢ)σ² = 1/N · sum((xᵢ - μ)²)x̂ᵢ = (xᵢ - μ) / √(σ² + ε)yᵢ = γ·x̂ᵢ + β

其中 γ 和 β 是可学习参数。

实例

# ============================================

# 批归一化

# ============================================

import numpy as np

def batch_norm(x, gamma, beta, epsilon=1e-5, training=True, running_mean=None, running_var=None, momentum=0.1):

"""批归一化

x: 输入,形状 (batch_size, features)

gamma: 缩放参数

beta: 偏移参数

"""

if training:

# 训练模式:用当前 batch 的统计量

batch_mean = np.mean(x, axis=0)

batch_var = np.var(x, axis=0)

# 更新 running 统计量(用于测试时)

if running_mean isnotNoneand running_var isnotNone:

running_mean[:]= momentum * running_mean + (1 - momentum) * batch_mean

running_var[:]= momentum * running_var + (1 - momentum) * batch_var

# 归一化

x_normalized =(x - batch_mean) / np.sqrt(batch_var + epsilon)

else:

# 测试模式:用 running 统计量

x_normalized =(x - running_mean) / np.sqrt(running_var + epsilon)

# 缩放和偏移

out = gamma * x_normalized + beta

return out

# 测试

batch_size =4

features =3

# 一个 batch 的数据

x = np.array([

[1.0,2.0,3.0],

[2.0,3.0,4.0],

[3.0,4.0,5.0],

[4.0,5.0,6.0],

])

print(f"输入 x:n{x}")

# 初始参数

gamma = np.ones(features)# 初始不缩放

beta = np.zeros(features)# 初始不偏移

print(f"ngamma: {gamma}")

print(f"beta: {beta}")

# 应用批归一化

out = batch_norm(x, gamma, beta, training=True)

print(f"n批归一化后:n{out}")

# 验证:每个特征的均值应该接近 0,方差接近 1

print(f"n归一化后的均值: {np.mean(out, axis=0)}")

print(f"归一化后的方差: {np.var(out, axis=0)}")

批归一化的优点:

  1. 加速训练
  2. 允许更大的学习率
  3. 减少对初始化的敏感
  4. 有轻微的正则化效果

批归一化的缺点:

  1. 依赖 batch 大小——batch 太小效果不好
  2. 训练和测试时行为不同
  3. 不适用于序列模型(如 RNN、Transformer)

层归一化(Layer Normalization)

层归一化(LayerNorm)在特征维度上进行归一化,而不是 batch 维度。

这让它不依赖 batch 大小,非常适合序列模型。

实例

# ============================================

# 层归一化

# ============================================

import numpy as np

def layer_norm(x, gamma, beta, epsilon=1e-5):

"""层归一化

x: 输入,形状 (batch_size, features) 或 (batch_size, seq_len, features)

"""

# 在最后一个维度(特征维度)上计算均值和方差

mean = np.mean(x, axis=-1, keepdims=True)

var = np.var(x, axis=-1, keepdims=True)

# 归一化

x_normalized =(x - mean) / np.sqrt(var + epsilon)

# 缩放和偏移

out = gamma * x_normalized + beta

return out

# 测试

batch_size =2

seq_len =3

features =4

# 模拟 Transformer 中的输入:(batch_size, seq_len, features)

x = np.array([

[[1.0,2.0,3.0,4.0],

[2.0,3.0,4.0,5.0],

[3.0,4.0,5.0,6.0]],

[[4.0,5.0,6.0,7.0],

[5.0,6.0,7.0,8.0],

[6.0,7.0,8.0,9.0]],

])

print(f"输入 x 形状: {x.shape}")

# 初始参数

gamma = np.ones(features)

beta = np.zeros(features)

# 应用层归一化

out = layer_norm(x, gamma, beta)

print(f"层归一化后形状: {out.shape}")

# 验证:每个位置的均值接近 0,方差接近 1

mean_check = np.mean(out, axis=-1)

var_check = np.var(out, axis=-1)

print(f"n每个位置的均值:n{mean_check}")

print(f"每个位置的方差:n{var_check}")

两种归一化方法的对比:

方法归一化维度依赖 batch适用场景代表模型
BatchNormbatch 维度CNN、固定 batch 大小ResNet、VGG
LayerNorm特征维度Transformer、RNN、变长序列GPT、BERT、LLaMA

对于 Transformer 和大语言模型,LayerNorm 是标准选择。它在每个位置独立计算统计量,不依赖 batch 大小,也不需要维护 running 统计量。

学习率调度

学习率是最重要的超参数之一——太大不收敛,太小收敛太慢。

学习率调度(Learning Rate Scheduling)就是在训练过程中动态调整学习率。

预热(Warmup)

训练初期,先用小学习率"预热",让模型稳定下来,然后再提升到目标学习率。

这对 Transformer 尤其重要。

余弦退火

余弦退火(Cosine Annealing)让学习率按照余弦函数的形状逐渐下降。

实例

# ============================================

# 学习率调度

# ============================================

import numpy as np

def warmup_schedule(step, warmup_steps, base_lr):

"""线性预热调度"""

if step < warmup_steps:

return base_lr * (step + 1) / warmup_steps

return base_lr

def cosine_annealing_schedule(step, total_steps, base_lr, min_lr=0.0):

"""余弦退火调度"""

return min_lr + 0.5 * (base_lr - min_lr) * (1 + np.cos(np.pi * step / total_steps))

def warmup_cosine_schedule(step, warmup_steps, total_steps, base_lr, min_lr=0.0):

"""预热 + 余弦退火"""

if step < warmup_steps:

return base_lr * (step + 1) / warmup_steps

return min_lr + 0.5 * (base_lr - min_lr) * (1 + np.cos(np.pi * (step - warmup_steps) / (total_steps - warmup_steps)))

# 参数

total_steps =100

warmup_steps =10

base_lr =1e-3

min_lr =1e-5

print("不同学习率调度:")

print("Step | Warmup | Cosine | Warmup+Cosine")

print("-" * 55)

for step inrange(0, total_steps + 1,10):

lr_warmup = warmup_schedule(step, warmup_steps, base_lr)

lr_cosine = cosine_annealing_schedule(step, total_steps, base_lr, min_lr)

lr_combined = warmup_cosine_schedule(step, warmup_steps, total_steps, base_lr, min_lr)

print(f"{step:4} | {lr_warmup:.6f} | {lr_cosine:.6f} | {lr_combined:.6f}")

学习率对训练的影响

学习率的影响:

  1. 太大:训练不稳定,可能发散
  2. 太小:收敛慢,可能卡在局部最优
  3. 合适:快速稳定收敛

一个常用的策略是"学习率范围测试"(Learning Rate Finder):先用从小到大的学习率训练几步,看哪个学习率下损失下降最快。

学习率调度方法总结:

调度方法特点适用场景
固定学习率最简单小模型、简单任务
Step Decay每隔几步降一次CNN 等
Cosine Annealing平滑下降大多数场景
Warmup + Cosine先升后降Transformer、大模型(推荐)
Reduce on Plateau验证集不提升就降需要监控验证集

实战:用 PyTorch 从零实现 MLP

现在我们把前面所有知识点整合起来,用 PyTorch 实现一个完整的多层感知机(MLP),并在一个简单的分类任务上训练它。

实例

# ============================================

# PyTorch 实现 MLP 完整训练流程

# ============================================

import torch

import torch.nnas nn

import torch.optimas optim

from torch.utils.dataimport Dataset, DataLoader

import numpy as np

# ============================================

# 1. 准备数据

# ============================================

class SimpleDataset(Dataset):

"""一个简单的二分类数据集"""

def__init__(self, n_samples=1000, n_features=10):

# 生成合成数据

np.random.seed(42)

self.X= np.random.randn(n_samples, n_features).astype(np.float32)

# 简单的分类规则:y = 1 if sum(x[:5]) > 0 else 0

self.y=(np.sum(self.X[:, :5], axis=1)>0).astype(np.int64)

def__len__(self):

returnlen(self.X)

def__getitem__(self, idx):

return torch.tensor(self.X[idx]), torch.tensor(self.y[idx])

# 创建数据集和数据加载器

train_dataset = SimpleDataset(n_samples=800)

val_dataset = SimpleDataset(n_samples=200)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

print(f"训练集大小: {len(train_dataset)}")

print(f"验证集大小: {len(val_dataset)}")

# ============================================

# 2. 定义模型

# ============================================

class MLP(nn.Module):

"""多层感知机"""

def__init__(self, input_dim, hidden_dims, output_dim, dropout_rate=0.1):

super().__init__()

layers =[]

prev_dim = input_dim

# 隐藏层

for hidden_dim in hidden_dims:

layers.append(nn.Linear(prev_dim, hidden_dim))

layers.append(nn.ReLU())

layers.append(nn.LayerNorm(hidden_dim))# 层归一化

layers.append(nn.Dropout(dropout_rate))# Dropout

prev_dim = hidden_dim

# 输出层

layers.append(nn.Linear(prev_dim, output_dim))

self.model= nn.Sequential(*layers)

def forward(self, x):

returnself.model(x)

# 创建模型

model = MLP(

input_dim=10,

hidden_dims=[64,32],

output_dim=2,

dropout_rate=0.1

)

print("n模型结构:")

print(model)

# ============================================

# 3. 定义损失函数和优化器

# ============================================

criterion = nn.CrossEntropyLoss()# 交叉熵损失

optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)# AdamW 优化器

# 学习率调度:预热 + 余弦退火

total_epochs =20

warmup_epochs =2

total_steps =len(train_loader) * total_epochs

warmup_steps =len(train_loader) * warmup_epochs

def lr_lambda(step):

if step < warmup_steps:

return(step + 1) / warmup_steps

else:

progress =(step - warmup_steps) / (total_steps - warmup_steps)

return0.5 * (1 + np.cos(np.pi * progress))

scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lr_lambda)

print("n训练配置:")

print(f" Epochs: {total_epochs}")

print(f" Warmup epochs: {warmup_epochs}")

print(f" Total steps: {total_steps}")

print(f" Warmup steps: {warmup_steps}")

# ============================================

# 4. 训练循环

# ============================================

print("n开始训练...")

print("-" * 50)

global_step =0

best_val_acc =0.0

for epoch inrange(total_epochs):

# 训练阶段

model.train()

train_loss =0.0

train_correct =0

train_total =0

for batch_X, batch_y in train_loader:

# 前向传播

logits = model(batch_X)

loss = criterion(logits, batch_y)

# 反向传播

optimizer.zero_grad()

loss.backward()

optimizer.step()

scheduler.step()

# 统计

train_loss += loss.item()

predicted = torch.argmax(logits, dim=1)

train_total += batch_y.size(0)

train_correct +=(predicted == batch_y).sum().item()

global_step +=1

train_loss /=len(train_loader)

train_acc = train_correct / train_total

# 验证阶段

model.eval()

val_loss =0.0

val_correct =0

val_total =0

with torch.no_grad():

for batch_X, batch_y in val_loader:

logits = model(batch_X)

loss = criterion(logits, batch_y)

val_loss += loss.item()

predicted = torch.argmax(logits, dim=1)

val_total += batch_y.size(0)

val_correct +=(predicted == batch_y).sum().item()

val_loss /=len(val_loader)

val_acc = val_correct / val_total

# 保存最好的模型

if val_acc > best_val_acc:

best_val_acc = val_acc

# 这里可以保存模型 checkpoint

# 打印日志

current_lr = optimizer.param_groups[0]['lr']

print(f"Epoch {epoch+1:2d} | lr: {current_lr:.6f} | "

f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | "

f"Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}")

print("-" * 50)

print(f"训练完成!最好验证准确率: {best_val_acc:.4f}")

# ============================================

# 5. 推理示例

# ============================================

print("n推理示例:")

model.eval()

# 几个测试样本

test_samples =[

np.array([1.0,1.0,1.0,1.0,1.0,0.0,0.0,0.0,0.0,0.0], dtype=np.float32),# 应该是 1

np.array([-1.0, -1.0, -1.0, -1.0, -1.0,0.0,0.0,0.0,0.0,0.0], dtype=np.float32),# 应该是 0

]

for i, sample inenumerate(test_samples):

with torch.no_grad():

input_tensor = torch.tensor(sample).unsqueeze(0)

logits = model(input_tensor)

probs = torch.softmax(logits, dim=1)

predicted_class = torch.argmax(probs, dim=1).item()

print(f" 样本 {i+1}: {sample[:5]}...")

print(f" 预测类别: {predicted_class}")

print(f" 概率: 类0={probs[0,0]:.4f}, 类1={probs[0,1]:.4f}")

这个例子包含了深度学习训练的所有标准组件:

  1. 数据处理:Dataset、DataLoader
  2. 模型定义:MLP 带 LayerNorm 和 Dropout
  3. 损失函数:CrossEntropyLoss
  4. 优化器:AdamW
  5. 学习率调度:Warmup + Cosine
  6. 训练循环:训练阶段 + 验证阶段
  7. 模型保存和推理

这个流程可以直接推广到更复杂的任务和模型。

相关资讯
点击查看更多
游戏推荐
推荐专题
热门阅读
推荐下载