深度学习入门语法知识点完全指南(PyTorch 版)

本教程面向机器学习零基础读者,围绕「深度学习基本原理 → PyTorch 框架 → 八大应用实例」的顺序展开。每个知识点都会讲清楚:它是什么、为什么要学、在企业里怎么用、代码怎么写。所有代码均为可运行的 PyTorch 入门示例,逐行注释。


目录


第一部分:深度学习基本原理与基本概念

1.1 什么是深度学习

通俗理解

想象你教一个小孩认猫。你不用告诉他「猫有尖耳朵、有胡须、瞳孔是竖的」这些规则,你只需要给他看 1000 张猫的照片和 1000 张不是猫的照片,他自己就会总结出「猫长什么样」的规律。

深度学习就是这样一种方法:不靠人工写规则,而是让机器从大量数据中自己「学」出规律。

「深度」指的是神经网络的层数多。层数越多,能学到的特征越抽象:

第1层:学到边缘、颜色块          (低级特征)
第2层:学到眼睛、耳朵、纹理      (中级特征)
第3层:学到猫脸、猫身的完整轮廓   (高级特征)

为什么要学它

  • 传统机器学习需要人工「特征工程」(手工提取数据中有用的信息),费时费力且依赖专家经验。
  • 深度学习可以自动提取特征,在图像、语音、自然语言处理等复杂任务上效果远超传统方法。

企业中的应用

领域具体应用
互联网推荐系统(抖音、淘宝的"猜你喜欢")、搜索排序
金融风控反欺诈、信用评分、量化交易
自动驾驶行人检测、车道线识别、交通标志识别
医疗CT/MRI 影像诊断、药物分子筛选
制造业产品缺陷检测(AOI)、设备故障预测
语音语音助手(小爱同学)、会议实时转写

1.2 神经元与神经网络

1.2.1 单个神经元(感知机)

一个神经元做的事情其实非常简单,就是三步:

输入 x1, x2, x3
  → 各乘以权重 w1, w2, w3(表示各输入的重要程度)
  → 求和,再加上偏置 b(调节门槛)
  → 通过激活函数 f(决定要不要"激活"输出)
  → 输出 y

数学表达:

y = f(w1*x1 + w2*x2 + w3*x3 + b)

通俗例子:你决定要不要出门跑步,考虑三个因素:

  • 天气好(x1)、时间充裕(x2)、身体不累(x3)
  • 你对每个因素的重视程度就是权重 w:比如你最看重天气(w1=0.5),其次时间(w2=0.3),身体(w3=0.2)
  • 偏置 b 是你的「门槛」:门槛高说明你很挑剔,条件都很好才出门
  • 激活函数 f 是最终决策:综合分超过某个值就出门(输出 1),否则不出门(输出 0)

1.2.2 神经网络

把很多神经元按层组织起来,就构成神经网络:

输入层(接收数据)
   ↓
隐藏层1(第1组神经元)  ← 提取初级特征
   ↓
隐藏层2(第2组神经元)  ← 提取高级特征
   ↓
输出层(给出预测结果)

为什么要学这个知识点:后面所有的模型(CNN、GAN、ResNet、孪生网络)都只是「神经元 + 层」的不同组合方式。理解了神经元,就理解了所有模型的积木块。


1.3 激活函数(Activation Function)

是什么

激活函数是加在神经元输出端的一个「非线性开关」。如果没有激活函数,无论网络叠多少层,效果都等价于一层线性变换(因为线性函数套线性函数还是线性函数),网络就「深」不起来了。

通俗例子

激活函数就像公司的「审批流程」:

  • 普通员工(输入)提出申请(加权求和)
  • 经理(激活函数)判断:「申请力度够不够?」
    • Sigmoid:经理很温和,给一个 0 到 1 之间的「通过率」(0.8 表示 80% 通过)
    • ReLU:经理很干脆,「负数一律不批(输出 0),正数原样放行」
    • Tanh:经理的评分范围是 -1 到 1

三种最常用激活函数对比

激活函数公式输出范围特点常用场景
Sigmoid1/(1+e^(-x))(0, 1)输出可当概率;深层网络中梯度消失二分类输出层
Tanh(ex-e(-x))/(ex+e(-x))(-1, 1)以 0 为中心,比 Sigmoid 好RNN 隐藏层
ReLUmax(0, x)[0, +∞)计算快,缓解梯度消失隐藏层首选

入门代码

import torch  # 导入 PyTorch 库,这是深度学习最主流的框架之一
import torch.nn as nn  # 导入神经网络模块,里面封装了激活函数、层等组件

# 创建一个模拟的神经元输出数据:3个样本,每个样本1个数值
x = torch.tensor([-2.0, -0.5, 1.0, 3.0])  # tensor 就是 PyTorch 里的"多维数组"

# ---------- Sigmoid 激活函数 ----------
sigmoid = nn.Sigmoid()          # 实例化 Sigmoid 激活函数对象
print("Sigmoid输出:", sigmoid(x))  # 任何负数→趋近0,正数→趋近1,例如 -2→0.12

# ---------- Tanh 激活函数 ----------
tanh = nn.Tanh()                # 实例化 Tanh 激活函数对象
print("Tanh输出:", tanh(x))      # 输出范围 -1 到 1,例如 -2→-0.96

# ---------- ReLU 激活函数 ----------
relu = nn.ReLU()                # 实例化 ReLU 激活函数对象
print("ReLU输出:", relu(x))      # 负数全部变成 0,正数保持不变,例如 [-2]→0

企业中的使用:几乎所有公司训练深度学习模型时,隐藏层默认使用 ReLU(或其变体 LeakyReLU、GELU),只在二分类任务的最后一层用 Sigmoid。这是行业通用惯例。


1.4 损失函数(Loss Function)

是什么

损失函数衡量「模型预测的答案」和「正确答案」之间差多少。损失越小,模型越准。训练的目标就是让损失变小。

通俗例子

损失函数就像考试的「扣分标准」:

  • 你答 85 分,标准答案 90 分 → 扣 5 分(损失=5)
  • 训练过程就是不断做题、对答案、想办法少扣分

常用损失函数

损失函数任务类型通俗解释
nn.MSELoss(均方误差)回归(预测数值)预测值与真实值之差的平方的平均,对大误差惩罚重
nn.CrossEntropyLoss(交叉熵)多分类衡量预测的概率分布与真实分布的差异
nn.BCELoss(二元交叉熵)二分类专门用于输出为 0/1 概率的场景
nn.L1Loss回归差的绝对值的平均,对异常值比 MSE 更稳健

入门代码

import torch
import torch.nn as nn

# ---------- 均方误差损失(回归任务用) ----------
mse_loss = nn.MSELoss()  # 创建均方误差损失函数
pred = torch.tensor([2.5, 0.0, 2.1])   # 模型的预测值
real = torch.tensor([3.0, -0.5, 2.0])  # 真实值(标准答案)
loss = mse_loss(pred, real)            # 计算损失:((2.5-3)^2+(0-(-0.5))^2+(2.1-2)^2)/3
print("MSE损失:", loss.item())  # .item() 把张量转成普通 Python 数字,方便打印

# ---------- 交叉熵损失(分类任务用) ----------
ce_loss = nn.CrossEntropyLoss()  # 创建交叉熵损失函数
# logits 是模型最后一层的原始输出(未经过 softmax 的"得分")
logits = torch.tensor([[2.0, 1.0, 0.1]])  # 1个样本,3个类别的得分
target = torch.tensor([0])                # 真实类别是第 0 类
print("交叉熵损失:", ce_loss(logits, target).item())  # 得分最高的类别恰好是正确答案,损失较小

为什么要学:选错损失函数,模型训练方向就错了。比如做房价预测(回归)却用交叉熵损失,模型根本学不到东西。


1.5 优化器与梯度下降(Optimizer & Gradient Descent)

是什么

知道了损失(差距)之后,怎么缩小差距?——调整网络中的权重 w 和偏置 b。优化器就是负责「怎么调」的工具。

核心思想是梯度下降:

  1. 计算损失对每个权重的「梯度」(可以理解为:这个权重往哪个方向稍微动一点,损失会下降最快)
  2. 沿着梯度的反方向小步修正权重
  3. 反复迭代,损失逐渐下降

通俗例子

想象你蒙着眼睛在山上,想走到谷底(损失最小的地方):

  • 梯度就是脚下「最陡的下坡方向」
  • 学习率(Learning Rate)就是「每一步走多大」
    • 步子太小 → 走得极慢,训练耗时太长
    • 步子太大 → 一步跨过谷底,在山谷两壁来回跳,永远到不了底
  • 优化器是「走路策略」:SGD 是老老实实一步步走;Adam 是自适应调整步幅的智能登山杖

常用优化器

优化器特点使用建议
SGD随机梯度下降,经典稳定,可配合动量CV 大任务常用
Adam自适应学习率,收敛快,调参省心入门首选,90% 场景适用
AdamWAdam + 权重衰减修正Transformer 类模型标配

入门代码

import torch
import torch.nn as nn

# 定义一个最简单的模型:y = w*x + b(一次函数)
model = nn.Linear(in_features=1, out_features=1)  # 1个输入1个输出的线性层,内部自动有 w 和 b

# 创建优化器:Adam 优化器,lr 是学习率(步长)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# model.parameters() 返回模型所有需要学习的参数(w 和 b),优化器只更新这些参数

# 定义损失函数
loss_fn = nn.MSELoss()

# 造训练数据:y = 2x + 1
x_train = torch.tensor([[1.0], [2.0], [3.0], [4.0]])  # 输入,4个样本
y_train = torch.tensor([[3.0], [5.0], [7.0], [9.0]])  # 对应的真实输出

# ---------- 训练循环(这是深度学习最核心的代码结构!) ----------
for step in range(500):           # 迭代 500 次(每个循环叫一个 step)
    y_pred = model(x_train)       # 第1步:前向传播——用当前参数做预测
    loss = loss_fn(y_pred, y_train)  # 第2步:计算损失——预测和真实差多少

    optimizer.zero_grad()  # 第3步:清空上一轮的梯度(PyTorch 默认累加梯度,必须清零)
    loss.backward()        # 第4步:反向传播——自动计算每个参数的梯度
    optimizer.step()       # 第5步:更新参数——优化器根据梯度调整 w 和 b

    if step % 100 == 0:    # 每 100 步打印一次,观察损失是否在下降
        print(f"第{step}步, 损失: {loss.item():.4f}")

# 训练完成后看看学到了什么
w = model.weight.item()  # 取出学到的权重,理想值应接近 2
b = model.bias.item()    # 取出学到的偏置,理想值应接近 1
print(f"学到的函数: y = {w:.2f}x + {b:.2f}")

重点记忆:深度学习的训练循环永远是这五步:

预测 → 算损失 → 清零梯度 → 反向传播 → 更新参数

企业中的使用:任何公司的模型训练代码,核心都是这个循环。区别只在于数据怎么读、模型怎么定义、循环跑多少次。


1.6 前向传播与反向传播

通俗理解

  • 前向传播(Forward):数据从输入层流向输出层,一步步算出预测结果。就像工厂流水线,原料(数据)经过每道工序(层),产出成品(预测)。
  • 反向传播(Backward):拿到预测和答案的差距后,从输出层往回倒推,计算「每道工序对误差贡献了多少」,即每个参数的梯度。就像出了次品后,质量员回头追责:是哪道工序的问题?

为什么要学

loss.backward() 这一行代码背后就是反向传播。理解它,你就明白为什么:

  • 必须在 backward() 之前先 zero_grad()(不清零会把历次梯度叠加)
  • 图不能随便「断掉」(比如对需要梯度的张量做了 .numpy() 转换,链路就断了,报错 “Tensor that requires grad”)

1.7 训练相关的核心概念

概念定义通俗解释典型取值
样本(Sample)一条数据一张图片 / 一条用户记录—
特征(Feature)描述样本的属性图片的像素值、用户的年龄收入—
标签(Label)正确答案这张图是猫还是狗—
批次(Batch Size)一次喂给模型多少样本一次做 32 道题再统一对答案16 / 32 / 64 / 128
轮次(Epoch)完整过一遍全部训练数据把整本习题集做一遍10~100
学习率(LR)每步参数更新的幅度上山每步迈多大0.001 / 0.0001
迭代(Iteration/Step)一次参数更新做完一个 batch 并更新一次参数总数 = 样本数/batch × epoch

为什么要理解 Batch Size

  • 太小(如 2):每次看的样本太少,梯度不稳定,训练像喝醉了走路;但显存占用小。
  • 太大(如 1024):梯度稳定,但显存可能爆炸(OOM),且可能陷入尖锐极小值,泛化差。
  • 经验值:图像任务常用 32 或 64,先试 32,显存不够就减半。

通俗例子:评价一家餐厅好不好吃——

  • batch=1:只听 1 个客人的评价就改菜单(容易被极端评价带偏)
  • batch=32:听 32 个客人的平均评价再改(稳妥)
  • batch=全部数据:把所有历史评价全听一遍再改(最准但太慢)

1.8 过拟合与欠拟合

是什么

状态训练集表现测试集表现通俗解释
欠拟合差差学得太少,连例题都没学会(死记硬背都不够)
过拟合很好差把训练数据背下来了,换道新题就不会(高分低能)
理想状态好好学到了真正的规律

通俗例子:

  • 欠拟合 = 学生上课睡觉,考试啥也不会
  • 过拟合 = 学生把练习册的每道题和答案硬背下来,练习册满分,但考试题一变就不会了
  • 我们要的是「举一反三」的能力

解决过拟合的常用手段(企业实战必备)

手段做法代码体现
数据增强翻转、旋转、裁剪图片,扩充数据量torchvision.transforms
Dropout训练时随机关掉一部分神经元,防止单元"抱团作弊"nn.Dropout(0.5)
L2 正则化(权重衰减)惩罚过大的权重optimizer = Adam(..., weight_decay=1e-4)
早停(Early Stopping)验证集损失连续上升就停止训练手写循环判断
增加数据收集更多真实数据数据工程

入门代码:Dropout 与 weight_decay

import torch
import torch.nn as nn

# 定义一个带 Dropout 的小网络
model = nn.Sequential(          # Sequential 表示"按顺序堆叠各层"
    nn.Linear(10, 64),          # 全连接层:10维输入 → 64维输出
    nn.ReLU(),                  # ReLU 激活
    nn.Dropout(0.5),            # Dropout:训练时随机让 50% 的神经元输出置0,防止过拟合
    nn.Linear(64, 1)            # 输出层:64维 → 1维(回归预测一个值)
)

# 重要:训练模式和评估模式行为不同!
model.train()   # 切换到训练模式:Dropout 生效(随机关神经元)
y1 = model(torch.randn(4, 10))  # 每次前向传播,结果都可能不同

model.eval()    # 切换到评估/测试模式:Dropout 关闭,所有神经元都工作
with torch.no_grad():  # 测试时不需要计算梯度,用 no_grad 包住省内存且加速
    y2 = model(torch.randn(4, 10))

# weight_decay(L2正则化):在优化器里加一个参数即可
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)

注意:训练时用 model.train(),测试时必须 model.eval(),这是新手最容易犯的错误之一。忘了切换会导致测试结果不稳定。


1.9 训练集、验证集、测试集

数据集用途比例通俗解释
训练集让模型学习70%平时做的练习题
验证集调整超参数(学习率、层数等)、早停判断15%模拟考
测试集最终评估模型真实水平15%高考真题(只用一次)

企业中的使用:数据划分通常按时间切分(比如用 1~10 月数据训练、11 月验证、12 月测试),因为真实场景是「用过去预测未来」。随机打乱划分在时序场景会造成「数据泄露」,线上效果虚高。


第二部分:PyTorch 框架核心语法

2.0 为什么选 PyTorch

  • 语法像 Python:定义模型、写训练循环都非常自然,学习曲线平缓
  • 动态图:可以随时打印中间结果调试(TensorFlow 1.x 静态图就很难调)
  • 生态成熟:学术界主流(大部分论文开源代码都是 PyTorch),工业界大量使用(Meta、特斯拉、国内大厂)
  • GPU 加速简单:一行代码把数据和模型搬到 GPU

2.1 张量(Tensor)——PyTorch 的基本数据类型

是什么

张量就是「多维数组」,和 NumPy 的 ndarray 几乎一样,区别是:张量可以在 GPU 上计算,且支持自动求导。

维度名称例子
0 维标量torch.tensor(3.14) —— 一个数
1 维向量torch.tensor([1, 2, 3]) —— 一列数
2 维矩阵torch.tensor([[1,2],[3,4]]) —— 表格
3 维3D 张量一张彩色图片:(通道, 高, 宽)
4 维4D 张量一批图片:(批次, 通道, 高, 宽)

通俗例子:张量就像快递仓库的货架编号系统——0 维是单件商品,1 维是一排货架,2 维是一面货架墙,3 维是一个仓库,4 维是全国多个仓库的统一库存表。

入门代码

import torch
import numpy as np

# ---------- 创建张量的多种方式 ----------
a = torch.tensor([1.0, 2.0, 3.0])        # 方式1:直接从 Python 列表创建
b = torch.zeros(2, 3)                    # 方式2:创建 2行3列 的全 0 张量(常用来初始化)
c = torch.ones(2, 3)                     # 方式3:创建全 1 张量
d = torch.randn(2, 3)                    # 方式4:创建服从标准正态分布的随机张量(最常用来初始化权重)
e = torch.arange(0, 10, 2)               # 方式5:创建等差数列 [0, 2, 4, 6, 8]

# ---------- 查看张量的形状(非常重要!调试全靠它) ----------
print(a.shape)       # torch.Size([3])       —— 1维,3个元素
print(d.shape)       # torch.Size([2, 3])    —— 2维,2行3列
print(d.dtype)       # torch.float32         —— 数据类型,深度学习默认 float32
print(d.device)      # cpu                   —— 张量在哪个设备上(CPU 或 GPU)

# ---------- 张量运算(和 NumPy 类似) ----------
x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
y = torch.tensor([[5.0, 6.0], [7.0, 8.0]])

print(x + y)         # 逐元素相加
print(x * y)         # 逐元素相乘(注意:不是矩阵乘法!)
print(x @ y)         # 矩阵乘法(@ 运算符),深度学习中最常用的运算
print(x.sum())       # 求所有元素之和
print(x.mean())      # 求平均值

# ---------- 形状变换(view / reshape,非常常用) ----------
z = torch.arange(6)          # 一维:[0, 1, 2, 3, 4, 5]
print(z.view(2, 3))          # 变成 2行3列:[[0,1,2],[3,4,5]]
print(z.view(3, -1))         # -1 表示"这一维你自己算",变成 3行2列

# ---------- 张量 ↔ NumPy 互转 ----------
np_arr = z.numpy()           # 张量转 NumPy 数组(注意:要求张量不在 GPU 上且不求导)
back = torch.from_numpy(np_arr)  # NumPy 数组转张量

企业中的使用

  • 数据管道:原始数据(图片、文本)→ 预处理 → 转成 Tensor → 喂给模型。一切数据最终都变成张量。
  • 调试:模型形状不匹配是新手第一大报错来源,print(tensor.shape) 是最高频的调试手段。

2.2 GPU 加速(CUDA)

是什么

GPU(显卡)有成千上万个小核心,特别擅长「大量简单并行计算」——恰好神经网络的核心运算就是海量矩阵乘法。同样的训练任务,GPU 可能比 CPU 快 10~100 倍。

通俗例子

  • CPU 像一位数学教授:啥都会,但一次只能算一道题
  • GPU 像 1000 个小学生:每人只会算乘加,但 1000 人同时算——做矩阵运算这种"大量简单题"时完胜教授

入门代码

import torch

# 检查是否有可用的 GPU(NVIDIA 显卡 + 装了 CUDA 版 PyTorch)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("当前使用设备:", device)  # 有显卡打印 cuda:0,没有打印 cpu

# 把张量搬到 GPU(如果有)
x = torch.randn(3, 3).to(device)   # .to(device) 是最通用的搬运写法
y = torch.randn(3, 3).to(device)
z = x @ y                          # 矩阵乘法在 GPU 上执行,速度飞快

# 把结果搬回 CPU(比如要打印或存盘时)
print(z.cpu())                     # .cpu() 搬回 CPU

# 模型也必须搬到 GPU
# model = model.to(device)        # 训练前调用一次
# 然后每个 batch 的数据也要搬:images = images.to(device)

关键规则:模型和数据必须在同一个设备上!模型在 GPU、数据在 CPU 会报错。企业开发中训练脚本开头固定写 device = torch.device("cuda" if torch.cuda.is_available() else "cpu"),保证代码在有无显卡的机器上都能跑。


2.3 自动求导(autograd)

是什么

PyTorch 最强大的能力之一:自动计算梯度。你只管写前向计算,梯度由 loss.backward() 自动算出。这背后是「计算图」技术——PyTorch 会记录你对张量做过的每一步运算,反向传播时沿着这张图回溯求导。

通俗例子

就像 Excel 的公式追踪:=B2+C2 依赖于 B2 和 C2,当你改 B2 时 Excel 自动重算。autograd 就是给张量运算建立「依赖关系图」,然后自动做链式求导。

入门代码

import torch

# requires_grad=True 告诉 PyTorch:"这个张量需要求梯度,请记录它的运算过程"
x = torch.tensor(2.0, requires_grad=True)
w = torch.tensor(3.0, requires_grad=True)
b = torch.tensor(1.0, requires_grad=True)

# 前向计算:y = w*x + b = 3*2+1 = 7
y = w * x + b

# 反向传播:自动计算 y 对每个 requires_grad 张量的梯度
y.backward()

# 查看梯度:dy/dx = w = 3,dy/dw = x = 2,dy/db = 1
print(x.grad)  # tensor(3.)
print(w.grad)  # tensor(2.)
print(b.grad)  # tensor(1.)

常见注意事项

# 1. 测试/推理时关掉求导,省显存提速
with torch.no_grad():
    prediction = model(test_data)   # 此过程不会构建计算图

# 2. 如果想冻结某些层(不训练它们),关掉其参数的梯度
for param in model.feature_extractor.parameters():
    param.requires_grad = False   # 这些参数不会被优化器更新(迁移学习常用)

# 3. 梯度是累加的!每个循环必须清零
optimizer.zero_grad()

2.4 nn.Module——定义模型的标准方式

是什么

nn.Module 是 PyTorch 里所有模型的「母类」。你自己定义的模型必须继承它,并实现两个方法:

  • __init__:定义模型用到的层(在「装修图纸」上列出所有房间)
  • forward:定义数据怎么流过这些层(定义从进门到出门的路线)

通俗例子

把模型想成一台榨汁机:

  • __init__ = 确定机器有哪些零件(刀片、电机、滤网)
  • forward = 定义水果进去后怎么被加工(先榨汁、再过滤、再出杯)
  • 调用 model(x) = 把水果放进去,机器自动按 forward 的流程加工(PyTorch 会自动调用 forward,你从不手动写 model.forward(x))

入门代码

import torch
import torch.nn as nn

# 定义一个自定义模型:必须继承 nn.Module
class MyNet(nn.Module):
    def __init__(self):
        super().__init__()  # 必须先调用父类的初始化方法(固定写法,别忘!)
        # 在这里定义模型需要的层
        self.layer1 = nn.Linear(784, 256)  # 全连接层:784维输入 → 256维输出
        self.relu = nn.ReLU()              # 激活函数
        self.layer2 = nn.Linear(256, 10)   # 输出层:256维 → 10个类别

    def forward(self, x):
        # 在这里定义数据的流动路径(前向传播)
        x = self.layer1(x)   # 数据先经过第一层
        x = self.relu(x)     # 再过激活函数
        x = self.layer2(x)   # 最后过输出层得到 10 个得分
        return x             # 返回模型输出

# 使用模型
model = MyNet()                          # 实例化模型
x = torch.randn(4, 784)                  # 模拟 4 张展平的 28x28 图片(4个样本,每个784维)
out = model(x)                           # 前向传播(自动调用 forward)
print(out.shape)                         # torch.Size([4, 10]) —— 4个样本,每个10个类别得分

# 查看模型结构
print(model)

# 查看模型参数量(企业中汇报模型规模时用)
total_params = sum(p.numel() for p in model.parameters())
print(f"总参数量: {total_params}")  # 784*256+256 + 256*10+10 = 203530

等价的简洁写法(nn.Sequential)

如果模型就是「一层层顺序堆叠」,可以用 nn.Sequential 快速搭建,不用手写 forward:

model = nn.Sequential(          # 按顺序依次执行括号里的层
    nn.Linear(784, 256),        # 第1层
    nn.ReLU(),                  # 第2层:激活
    nn.Linear(256, 10)          # 第3层:输出
)
out = model(torch.randn(4, 784))  # 直接调用即可

2.5 Dataset 与 DataLoader——数据加载

是什么

  • Dataset:定义「数据集长什么样」——总共多少条数据、第 i 条数据是什么
  • DataLoader:定义「怎么取数据」——按批次取、要不要打乱顺序、用几个子进程并行读

为什么要学

企业里的数据集动辄几十万、几百万条,不可能一次性塞进内存/显存。DataLoader 实现了「按批次流式加载」,还能并行读数据(num_workers),让 GPU 不至于等数据。

通俗例子

  • Dataset = 菜谱(定义有哪些菜、每道菜怎么做)
  • DataLoader = 传菜员(每次端 32 道菜上桌,还会随机打乱上菜顺序防止你猜到下一道)

入门代码

import torch
from torch.utils.data import Dataset, DataLoader

# ---------- 第1步:自定义数据集类,必须继承 Dataset ----------
class MyDataset(Dataset):
    def __init__(self):
        # 造一些假数据:100 个样本,每个 10 维特征,标签是 0 或 1
        self.features = torch.randn(100, 10)   # 100行10列的特征矩阵
        self.labels = torch.randint(0, 2, (100,))  # 100个随机 0/1 标签

    def __len__(self):
        # 返回数据集的总样本数(DataLoader 用它来知道有多少数据)
        return len(self.labels)

    def __getitem__(self, idx):
        # 根据下标 idx 返回第 idx 条数据(特征 + 标签)
        # DataLoader 每次取数据都会调用这个方法
        return self.features[idx], self.labels[idx]

# ---------- 第2步:用 DataLoader 包装数据集 ----------
dataset = MyDataset()  # 创建数据集实例

dataloader = DataLoader(
    dataset,          # 传入数据集
    batch_size=16,    # 每批取 16 个样本
    shuffle=True,     # 每个 epoch 打乱数据顺序(训练集必须 True,防止模型学到假的顺序规律)
    num_workers=0     # 用几个子进程读数据,Windows 下调试可设 0,Linux 生产环境设 4~8
)

# ---------- 第3步:在训练循环中使用 ----------
for epoch in range(3):               # 训练 3 轮
    for batch_x, batch_y in dataloader:   # 每次取出一个批次的数据
        print(f"批次特征形状: {batch_x.shape}, 批次标签形状: {batch_y.shape}")
        # batch_x.shape = [16, 10],batch_y.shape = [16]
        # 在这里把 batch_x 喂给模型训练即可
    print(f"--- 第 {epoch+1} 轮结束 ---")

企业中的使用

真实项目中,Dataset 的 __getitem__ 里通常做的是「读一张图片 → 数据增强(随机翻转、裁剪)→ 转张量」。DataLoader 的 num_workers 和 pin_memory=True 是提升训练速度的标配参数。


2.6 模型保存与加载

为什么重要

企业里模型训练一次要几小时甚至几天,训练好的模型必须保存下来,部署到服务器上提供预测服务。

入门代码

import torch
import torch.nn as nn

model = nn.Linear(10, 1)  # 假设这是我们训练好的模型

# ---------- 保存模型 ----------
# 推荐方式:只保存模型参数(字典),文件小且灵活
torch.save(model.state_dict(), "model_weights.pth")  # state_dict 是参数字典 {层名: 权重张量}

# ---------- 加载模型 ----------
new_model = nn.Linear(10, 1)                  # 先创建一个结构完全一样的空模型
new_model.load_state_dict(torch.load("model_weights.pth"))  # 把保存的参数填进去
new_model.eval()                               # 别忘了切换到评估模式

# ---------- 保存整个模型(不推荐但方便) ----------
torch.save(model, "whole_model.pth")           # 保存结构+参数
loaded = torch.load("whole_model.pth")         # 直接加载出可用模型

注意:load_state_dict 要求模型结构和保存时完全一致,所以企业里通常把「模型定义类」放在单独的文件里,训练脚本和部署脚本共同引用,保证结构统一。


第三部分:应用实例


实例 1:回归分析(线性回归)

任务说明

回归分析:预测一个连续数值。例如根据房屋面积预测房价、根据广告投入预测销售额、根据温度湿度预测用电量。

为什么用神经网络做回归

简单线性回归用最小二乘法就能解,但当特征多、关系复杂(非线性)时,神经网络能自动拟合复杂规律。这是深度学习的 Hello World。

企业应用

  • 金融:预测股票价格区间、预测贷款金额
  • 电商:预测商品未来 7 天销量(用于备货)
  • 能源:预测电网负荷,安排发电计划

完整代码(预测房价:面积 → 价格)

import torch                    # 导入 PyTorch 主库
import torch.nn as nn           # 导入神经网络模块(层、损失函数)
import matplotlib.pyplot as plt # 导入绘图库,用来可视化训练效果

# ============ 1. 准备数据 ============
# 真实世界规律假设:房价 = 0.05 * 面积 + 30(单位:万),面积300平的房子约45万
x_train = torch.tensor([[50], [60], [70], [80], [90], [100], [110], [120]], dtype=torch.float32)  # 面积特征,8个样本
y_train = torch.tensor([[32.5], [33.0], [33.5], [34.0], [34.5], [35.0], [35.5], [36.0]], dtype=torch.float32)  # 对应房价

# ============ 2. 定义模型 ============
model = nn.Linear(in_features=1, out_features=1)
# 线性层 y = w*x + b,1个输入(面积)1个输出(房价),w 和 b 自动初始化

# ============ 3. 定义损失函数和优化器 ============
loss_fn = nn.MSELoss()                       # 均方误差:回归任务的标准损失
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)  # Adam 优化器,学习率 0.01

# ============ 4. 训练循环 ============
epochs = 1000           # 训练 1000 轮
losses = []             # 记录每轮损失,用于画图

for epoch in range(epochs):
    y_pred = model(x_train)              # 前向传播:用当前模型预测房价
    loss = loss_fn(y_pred, y_train)      # 计算预测值与真实值的均方误差

    optimizer.zero_grad()                # 清空上一轮梯度(PyTorch 梯度会累加,必须清零)
    loss.backward()                      # 反向传播:计算 w 和 b 的梯度
    optimizer.step()                     # 根据梯度更新 w 和 b

    losses.append(loss.item())           # 记录当前损失值
    if (epoch + 1) % 200 == 0:           # 每 200 轮打印一次进度
        print(f"第 {epoch+1} 轮, 损失: {loss.item():.4f}")

# ============ 5. 查看训练结果 ============
w = model.weight.item()   # 取出学到的权重(理想值 ≈ 0.05)
b = model.bias.item()     # 取出学到的偏置(理想值 ≈ 30)
print(f"学到的公式: 房价 = {w:.4f} × 面积 + {b:.2f}")

# ============ 6. 用模型做预测 ============
model.eval()                       # 切换到评估模式
with torch.no_grad():              # 预测时关闭梯度计算,节省内存
    test = torch.tensor([[200]], dtype=torch.float32)  # 预测 200 平米的房子
    pred = model(test)             # 前向传播得到预测价
    print(f"200平米房子预测价格: {pred.item():.2f} 万")  # .item() 取出 Python 数字

# ============ 7. 可视化损失下降曲线 ============
plt.plot(losses)                  # 画损失曲线
plt.xlabel("训练轮次")            # x轴标签
plt.ylabel("MSE 损失")           # y轴标签
plt.title("训练损失下降曲线")     # 标题
plt.show()                        # 显示图像

学完这个实例你掌握了

训练循环的完整五步(预测→算损失→清零→反向→更新),这是后面所有实例的共同骨架。


实例 2:Logistic 回归(二分类)

任务说明

Logistic 回归虽然名字带"回归",实际是二分类算法:输出结果是「属于某一类的概率」,范围 0~1。

判断逻辑:

输出概率 ≥ 0.5 → 判为正类(比如"会购买")
输出概率 < 0.5 → 判为负类(比如"不购买")

与线性回归的关键区别

线性回归Logistic 回归
输出任意实数(-∞, +∞)概率(0, 1)
最后一层激活无Sigmoid
损失函数MSEBCELoss(二元交叉熵)

为什么必须用 Sigmoid:线性回归的输出可能超出 0~1(比如 -3 或 7),没法当概率用。Sigmoid 把任意实数「压」进 0~1 区间:

Sigmoid(-3) ≈ 0.047   (低概率)
Sigmoid(0)  = 0.5     (临界点)
Sigmoid(7)  ≈ 0.999   (高概率)

为什么用 BCE 而不是 MSE:交叉熵在预测错得离谱时惩罚极重(梯度大,学得快),预测接近时惩罚轻。MSE 在配合 Sigmoid 时容易梯度消失,训练变慢。这是理论和实践的共同结论。

企业应用

  • 金融风控:判断一笔交易是否欺诈(是/否)
  • 电商:判断用户是否会购买某商品(是/否)
  • HR:判断简历是否通过初筛(是/否)

完整代码(根据考试成绩判断是否录取)

import torch
import torch.nn as nn

# ============ 1. 准备数据 ============
# 两个特征:笔试成绩(x1)、面试成绩(x2);标签:1=录取,0=不录取
x_train = torch.tensor([
    [85, 80], [90, 88], [78, 82], [95, 92],   # 前4个:高分,录取(标签1)
    [50, 55], [45, 60], [55, 48], [40, 52]    # 后4个:低分,不录取(标签0)
], dtype=torch.float32)

y_train = torch.tensor([[1], [1], [1], [1], [0], [0], [0], [0]], dtype=torch.float32)
# 注意:BCELoss 要求标签也是 float 类型,且形状为 (样本数, 1)

# ============ 2. 定义 Logistic 回归模型 ============
class LogisticRegression(nn.Module):
    def __init__(self):
        super().__init__()                      # 调用父类初始化(固定写法)
        self.linear = nn.Linear(2, 1)           # 线性层:2个输入特征 → 1个输出
        self.sigmoid = nn.Sigmoid()             # Sigmoid 激活:把输出压缩到 0~1 之间当概率

    def forward(self, x):
        out = self.linear(x)                    # 先做线性变换 z = w1*x1 + w2*x2 + b
        out = self.sigmoid(out)                 # 再过 Sigmoid 得到概率
        return out                              # 返回属于类别1的概率

model = LogisticRegression()  # 实例化模型

# ============ 3. 定义损失函数和优化器 ============
loss_fn = nn.BCELoss()                          # 二元交叉熵损失(二分类专用)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)  # Adam 优化器

# ============ 4. 训练 ============
for epoch in range(1000):
    y_pred = model(x_train)                     # 前向传播,得到每个样本的录取概率
    loss = loss_fn(y_pred, y_train)             # 计算二元交叉熵损失

    optimizer.zero_grad()                       # 清空梯度
    loss.backward()                             # 反向传播
    optimizer.step()                            # 更新参数

    if (epoch + 1) % 200 == 0:
        print(f"第 {epoch+1} 轮, 损失: {loss.item():.4f}")

# ============ 5. 预测新数据 ============
model.eval()                                    # 切换评估模式
with torch.no_grad():                           # 关闭梯度计算
    new_students = torch.tensor([[75, 70], [48, 50]], dtype=torch.float32)  # 2个新学生
    probs = model(new_students)                 # 得到录取概率
    preds = (probs >= 0.5).float()              # 概率≥0.5 判为录取(1),否则不录取(0)

    for i in range(len(new_students)):
        print(f"学生成绩{new_students[i].tolist()}, 录取概率: {probs[i].item():.2f}, "
              f"预测结果: {'录取' if preds[i].item() == 1 else '不录取'}")

学完这个实例你掌握了

  • 分类任务的输出层设计(Sigmoid + BCE)
  • model.eval() 和 torch.no_grad() 的标准用法
  • 概率转类别标签的写法 (probs >= 0.5).float()

实例 3:自动编码解码器(AutoEncoder)

是什么

自动编码器是一种无监督学习模型(不需要标签!),结构分两部分:

原始数据(784维) →【编码器】→ 低维特征(32维) →【解码器】→ 重建数据(784维)
  • 编码器(Encoder):把高维数据压缩成低维的「精华表示」
  • 解码器(Decoder):把精华还原成原始数据
  • 训练目标:让「输入」和「输出」越像越好(自己学自己,所以叫自编码)

通俗例子

  • 编码器 = 你读完一本书后写的读书笔记(把 300 页压缩成 3 页精华)
  • 解码器 = 根据笔记复述出这本书的主要内容
  • 训练 = 练习「读→记→复述」,直到复述和原书高度一致
  • 如果笔记只有 3 页却能复述出 300 页书的精髓,说明笔记抓住了最本质的信息

为什么要学它 / 有什么用

用途解释
降维784 维压缩到 32 维,可视化、加速后续任务
去噪输入带噪声的图片,输出干净图片(去噪自编码器)
异常检测正常数据能很好地重建,异常数据重建误差大 → 报警(工业质检、金融反欺诈)
数据生成在低维空间采样再解码,生成新样本
特征提取编码器输出的低维向量可作为其他模型的输入特征

企业应用

  • 工业:设备振动信号的异常检测(重建误差突然变大 = 设备可能故障)
  • 金融:交易行为压缩编码,发现异常交易模式
  • 推荐系统:用自编码器学习用户/物品的低维表示

完整代码(MNIST 手写数字的压缩与重建)

import torch
import torch.nn as nn

# ============ 1. 定义自编码器 ============
class AutoEncoder(nn.Module):
    def __init__(self):
        super().__init__()                              # 调用父类初始化

        # ---------- 编码器:784 → 128 → 64 → 32(逐步压缩) ----------
        self.encoder = nn.Sequential(
            nn.Linear(784, 128),                        # 28x28=784 像素 → 128 维
            nn.ReLU(),                                  # 激活函数引入非线性
            nn.Linear(128, 64),                         # 128 → 64
            nn.ReLU(),                                  # 激活
            nn.Linear(64, 32)                           # 64 → 32,这就是"精华表示"(瓶颈层)
        )

        # ---------- 解码器:32 → 64 → 128 → 784(逐步还原) ----------
        self.decoder = nn.Sequential(
            nn.Linear(32, 64),                          # 32 → 64
            nn.ReLU(),                                  # 激活
            nn.Linear(64, 128),                         # 64 → 128
            nn.ReLU(),                                  # 激活
            nn.Linear(128, 784),                        # 128 → 784,还原成图片大小
            nn.Sigmoid()                                # Sigmoid:让每个像素输出在 0~1 之间
        )                                               # (MNIST 像素归一化到了 0~1)

    def forward(self, x):
        encoded = self.encoder(x)                       # 第1步:压缩
        decoded = self.decoder(encoded)                 # 第2步:还原
        return decoded                                  # 返回重建后的数据

model = AutoEncoder()       # 实例化模型
print(model)                # 打印模型结构

# ============ 2. 准备模拟数据(真实项目请用 MNIST 数据集) ============
# 模拟 64 个手写数字图片样本,每张 784 像素(已展平)
dummy_images = torch.rand(64, 784)   # 随机生成 0~1 之间的像素值,模拟归一化后的图片

# ============ 3. 训练自编码器 ============
loss_fn = nn.MSELoss()                              # 重建损失:输入和输出的均方误差
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)  # Adam 优化器

for epoch in range(20):
    reconstructed = model(dummy_images)             # 前向传播:压缩再还原
    loss = loss_fn(reconstructed, dummy_images)     # 损失 = 重建图与原图的差距
    # 注意!自编码器的"标签"就是输入本身,所以第二个参数是 dummy_images

    optimizer.zero_grad()                           # 清空梯度
    loss.backward()                                 # 反向传播
    optimizer.step()                                # 更新参数

    print(f"第 {epoch+1} 轮, 重建损失: {loss.item():.4f}")  # 损失越小,重建越像

# ============ 4. 使用:提取低维特征 ============
model.eval()
with torch.no_grad():
    new_img = torch.rand(1, 784)                    # 一张新的"图片"
    code = model.encoder(new_img)                   # 只过编码器 → 得到 32 维精华表示
    print(f"原始维度: {new_img.shape}, 压缩后维度: {code.shape}")  # [1,784] → [1,32]
    # 这 32 维向量可以直接用于后续的分类、检索、聚类任务

学完这个实例你掌握了

  • nn.Sequential 的嵌套使用(编码器/解码器两个子模块)
  • 无监督学习的训练方式(标签就是输入本身)
  • 瓶颈层(bottleneck)的设计思想:压缩迫使模型学本质特征

实例 4:卷积神经网络(CNN)

是什么

CNN 是处理图像的专用神经网络。之前用全连接层处理图片有个大问题:一张 224×224×3 的图片展开成向量有 15 万维,参数量爆炸,且丢失了空间结构。

CNN 的两大法宝:

  1. 卷积层(Convolution):用小的「滤波器(卷积核)」在图片上滑动,提取局部特征(边缘、纹理、角点)
  2. 池化层(Pooling):缩小特征图尺寸,减少计算量,保留关键信息

通俗例子

卷积就像拿着一个「特征放大镜」在图片上从左到右、从上到下扫描:

  • 放大镜(卷积核)专门对「竖线敏感」→ 扫过图片时,有竖线的位置响应值高
  • 多个不同的放大镜 → 分别检测横线、斜线、颜色块……
  • 逐层组合:边缘 → 纹理 → 眼睛鼻子 → 整张猫脸

池化就像把高分辨率照片缩小成缩略图:尺寸小了,但一眼还是能看出照片内容。

为什么要学

CNN 是图像识别、目标检测、图像分割的基石。人脸识别、车牌识别、医学影像诊断、工业缺陷检测……全是 CNN 或其变体。

核心语法知识点

卷积层参数详解
nn.Conv2d(
    in_channels=3,        # 输入通道数:彩色图=3(RGB),灰度图=1
    out_channels=16,      # 输出通道数 = 卷积核个数(16 个卷积核提取 16 种特征)
    kernel_size=3,        # 卷积核大小 3x3(最常用)
    stride=1,             # 步长:卷积核每次滑动 1 个像素
    padding=1             # 填充:在图片边缘补 1 圈 0,防止越界并控制输出尺寸
)

输出尺寸计算公式(面试常考):

输出尺寸 = (输入尺寸 - 卷积核大小 + 2×padding) / stride + 1
例:(32 - 3 + 2×1)/1 + 1 = 32   (尺寸不变)
池化层
nn.MaxPool2d(kernel_size=2, stride=2)  # 最大池化:2x2 区域取最大值,尺寸减半
# 例:32x32 的特征图 → 16x16

企业应用

  • 手机人脸解锁、支付刷脸
  • 自动驾驶的摄像头感知
  • 工业 AOI(自动光学检测):检测 PCB 板、玻璃面板缺陷
  • 医疗:肺结节检测、病理切片分析

完整代码(MNIST 手写数字识别)

import torch
import torch.nn as nn

# ============ 1. 定义 CNN 模型 ============
class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()                              # 调用父类初始化

        # ---------- 特征提取部分:卷积 + 池化 ----------
        self.features = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=3, padding=1),
            # 卷积层:1个输入通道(灰度图)→16个卷积核,3x3卷积核,padding=1 保持尺寸
            # 输入 [batch, 1, 28, 28] → 输出 [batch, 16, 28, 28]

            nn.ReLU(),                                  # 激活函数
            nn.MaxPool2d(2, 2),                         # 最大池化:28x28 → 14x14

            nn.Conv2d(16, 32, kernel_size=3, padding=1),
            # 第二层卷积:16通道→32通道,进一步提取高级特征
            # 输入 [batch, 16, 14, 14] → 输出 [batch, 32, 14, 14]

            nn.ReLU(),                                  # 激活
            nn.MaxPool2d(2, 2)                          # 池化:14x14 → 7x7
        )

        # ---------- 分类部分:全连接层 ----------
        self.classifier = nn.Sequential(
            nn.Linear(32 * 7 * 7, 128),                 # 展平后 32*7*7=1568 维 → 128 维
            nn.ReLU(),                                  # 激活
            nn.Linear(128, 10)                          # 128 → 10(10 个数字类别)
        )

    def forward(self, x):
        x = self.features(x)                            # 第1步:卷积层提取特征
        x = x.view(x.size(0), -1)                       # 第2步:展平!把 [batch,32,7,7] 变成 [batch,1568]
        # x.size(0) 是 batch 大小,-1 表示其余维度自动合并
        x = self.classifier(x)                          # 第3步:全连接层分类
        return x                                        # 返回 10 个类别的得分

model = SimpleCNN()   # 实例化模型
print(model)          # 打印结构,观察每层的输出尺寸

# ============ 2. 准备模拟数据 ============
# 模拟一个批次:32 张 1x28x28 的灰度图片
images = torch.randn(32, 1, 28, 28)     # [batch=32, 通道=1, 高=28, 宽=28]
labels = torch.randint(0, 10, (32,))    # 32 个标签,取值 0~9

# ============ 3. 训练一步(完整流程) ============
loss_fn = nn.CrossEntropyLoss()                         # 多分类用交叉熵损失
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

outputs = model(images)                                 # 前向传播:[32, 1, 28, 28] → [32, 10]
loss = loss_fn(outputs, labels)                         # 计算损失

optimizer.zero_grad()                                   # 清空梯度
loss.backward()                                         # 反向传播
optimizer.step()                                        # 更新参数

print(f"损失: {loss.item():.4f}")

# ============ 4. 形状变化总结(重点理解!) ============
print("""
形状变化过程:
输入:     [32, 1, 28, 28]
卷积1+池化: [32, 16, 14, 14]
卷积2+池化: [32, 32, 7, 7]
展平:      [32, 1568]
全连接:    [32, 10]  ← 每个样本 10 个类别得分
""")

学完这个实例你掌握了

  • 卷积层/池化层的作用和参数含义
  • 展平(view)操作:卷积特征图接入全连接层前必须展平
  • 图片数据的标准形状 [batch, channel, height, width]
  • CrossEntropyLoss 多分类的用法(注意:它内部自带 Softmax,模型最后一层不要再加 Sigmoid/Softmax!)

实例 5:生成式对抗网络(GAN)

是什么

GAN 由两个神经网络「对抗博弈」组成:

  • 生成器(Generator):凭空造假(从随机噪声生成假图片),目标是「以假乱真」
  • 判别器(Discriminator):鉴定真伪(判断输入图片是真图还是生成器造的假图),目标是「火眼金睛」

两者相互对抗、共同进步:

生成器拼命把假图做得更像真图  ←→  判别器拼命提高鉴别能力
最终达到纳什均衡:生成器造的图和真图几乎无法区分

通俗例子

造假币者 vs 验钞师的军备竞赛:

  • 造假者(生成器)不断改进印刷技术
  • 验钞师(判别器)不断学习新的防伪特征
  • 起初造假很粗糙,验钞师一眼识破;造假者根据"哪里被识破"改进;如此往复
  • 最终造假技术登峰造极(当然现实中这是违法的,GAN 用来做合法的生成任务)

为什么要学 / 有什么用

应用说明
图像生成生成不存在的人脸(thispersondoesnotexist.com)、风景、动漫
图像翻译照片→油画风格、白天→黑夜、马→斑马(CycleGAN)
数据增强医疗影像等稀缺数据场景,生成合成样本扩充训练集
超分辨率低清图→高清图(老照片修复)
图像修复(Inpainting)把图片中缺失/遮挡的部分补全

企业应用

  • 电商:自动生成商品展示图、虚拟模特换装
  • 游戏/影视:自动生成场景素材、角色概念图
  • 医疗:生成合成医学影像辅助诊断训练
  • 安防:训练数据生成(如各种光照下的行人样本)

核心语法知识点

训练技巧(GAN 的训练比普通网络难,这些技巧是实践总结):

技巧做法原因
标签平滑真实标签用 0.9 而非 1.0防止判别器过于自信导致生成器梯度消失
输入归一化图片像素归一到 [-1, 1]配合 Tanh 激活,训练更稳定
不用 DropoutGAN 里通常用 BatchNormDropout 造成随机性,影响对抗平衡
学习率生成器和判别器都用较小的 lr防止一方进步太快导致崩溃

完整代码(生成 28×28 手写数字风格图片)

import torch
import torch.nn as nn

# ============ 1. 生成器:噪声 → 假图片 ============
class Generator(nn.Module):
    def __init__(self, noise_dim=100):
        super().__init__()                              # 调用父类初始化
        self.net = nn.Sequential(
            nn.Linear(noise_dim, 256),                  # 100 维随机噪声 → 256 维
            nn.ReLU(),                                  # 激活
            nn.Linear(256, 512),                        # 256 → 512
            nn.ReLU(),                                  # 激活
            nn.Linear(512, 784),                        # 512 → 784(28x28=784 像素)
            nn.Tanh()                                   # Tanh:输出范围 -1~1
        )

    def forward(self, z):
        # z 是随机噪声 [batch, 100]
        img = self.net(z)                               # 经过网络生成图片展平向量
        return img.view(img.size(0), 1, 28, 28)         # reshape 成图片形状 [batch, 1, 28, 28]

# ============ 2. 判别器:图片 → 真假概率 ============
class Discriminator(nn.Module):
    def __init__(self):
        super().__init__()                              # 调用父类初始化
        self.net = nn.Sequential(
            nn.Linear(784, 512),                        # 784 像素 → 512
            nn.LeakyReLU(0.2),                          # LeakyReLU:负数区保留小梯度(GAN 常用)
            nn.Linear(512, 256),                        # 512 → 256
            nn.LeakyReLU(0.2),                          # 激活
            nn.Linear(256, 1),                          # 256 → 1
            nn.Sigmoid()                                # Sigmoid:输出"是真图"的概率 0~1
        )

    def forward(self, img):
        # img 是图片 [batch, 1, 28, 28]
        x = img.view(img.size(0), -1)                   # 展平成 [batch, 784]
        return self.net(x)                              # 返回真伪概率 [batch, 1]

# ============ 3. 创建两个网络和优化器 ============
noise_dim = 100                                         # 噪声向量维度
G = Generator(noise_dim)                                # 创建生成器
D = Discriminator()                                     # 创建判别器

# 两个网络要分别优化!它们的目标是相反的
optimizer_G = torch.optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizer_D = torch.optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999))
# betas=(0.5, 0.999) 是 GAN 的经验参数,比默认值训练更稳定

# 二元交叉熵损失(判别真假就是二分类)
loss_fn = nn.BCELoss()

# ============ 4. 训练循环(交替训练两个网络) ============
batch_size = 64                                         # 批次大小

for epoch in range(50):
    # ---- 模拟一批真实图片(真实项目用 MNIST 数据集) ----
    real_imgs = (torch.rand(batch_size, 1, 28, 28) * 2 - 1)  # 模拟真实图片,像素范围 [-1, 1]
    real_labels = torch.ones(batch_size, 1) * 0.9           # 真实标签 0.9(标签平滑技巧!不用 1.0)
    fake_labels = torch.zeros(batch_size, 1)                # 假图标签 0

    # ========== 训练判别器(先让判别器变强) ==========
    z = torch.randn(batch_size, noise_dim)                  # 生成随机噪声
    fake_imgs = G(z)                                        # 生成器造一批假图(注意:不更新 G!)

    d_loss_real = loss_fn(D(real_imgs), real_labels)        # 判别器看真图的损失:应该判为真
    d_loss_fake = loss_fn(D(fake_imgs.detach()), fake_labels) # 判别器看假图的损失:应该判为假
    # .detach() 切断假图与生成器的梯度联系,训练判别器时不更新生成器!

    d_loss = d_loss_real + d_loss_fake                      # 判别器总损失
    optimizer_D.zero_grad()                                 # 清空判别器梯度
    d_loss.backward()                                       # 反向传播
    optimizer_D.step()                                      # 只更新判别器参数

    # ========== 训练生成器(让生成器学会骗过判别器) ==========
    z = torch.randn(batch_size, noise_dim)                  # 再生成一批新噪声
    fake_imgs = G(z)                                        # 生成新假图

    g_loss = loss_fn(D(fake_imgs), real_labels)             # 生成器的目标:假图被判别器判为"真"(标签用0.9)
    optimizer_G.zero_grad()                                 # 清空生成器梯度
    g_loss.backward()                                       # 反向传播
    optimizer_G.step()                                      # 只更新生成器参数

    if (epoch + 1) % 10 == 0:
        print(f"第 {epoch+1} 轮, 判别器损失: {d_loss.item():.4f}, 生成器损失: {g_loss.item():.4f}")

# ============ 5. 用训练好的生成器生成图片 ============
G.eval()
with torch.no_grad():
    z = torch.randn(1, noise_dim)                           # 一组随机噪声
    generated_img = G(z)                                    # 生成一张假图片 [1, 1, 28, 28]
    print(f"生成图片的形状: {generated_img.shape}")          # [1, 1, 28, 28]

学完这个实例你掌握了

  • 两个网络交替训练的结构(先 D 后 G)
  • .detach() 的作用(切断梯度流,防止误更新生成器)
  • 标签平滑、LeakyReLU、betas 参数等 GAN 训练技巧

实例 6:残差神经网络(ResNet)

是什么

ResNet 的核心创新是残差连接(Skip Connection / Shortcut):把输入直接「跳过」若干层加到输出上。

普通网络:  输入 x → [层1] → [层2] → 输出 F(x)
ResNet:   输入 x → [层1] → [层2] → 输出 F(x) + x   ← x 走"捷径"直接加过来

为什么需要它(通俗理解)

理论上网络越深越好,但实践中发现:超过 20 层后,网络越深效果反而越差(不是过拟合,训练集误差都降不下去)。

原因:深层网络中梯度要经过几十上百层的连乘,传到浅层时已经接近 0(梯度消失),浅层参数几乎不更新。

残差连接的妙处:让网络学习「输入和输出的差(残差)」而不是直接学输出:

  • 通俗例子:老师让学生「把作文从 60 分改到 90 分」,任务很难;但如果改成「在原文基础上改进 30 分的地方」,学生就知道只关注不足之处。
  • 数学上,输出 = F(x) + x,反向传播时梯度可以通过 +x 这条「高速公路」直接传回浅层,绕过中间层的梯度消失问题。

为什么要学:ResNet 是现代深度学习的奠基性架构。今天的 ViT、目标检测、分割网络都广泛使用残差连接。学会它等于学会了深度网络的「通用积木」。

企业应用

  • 图像识别服务(几乎所有工业级视觉模型的骨干网络都是 ResNet 或其变体)
  • 人脸识别(ArcFace 等基于 ResNet 骨干)
  • 医学影像分析、遥感图像分类
  • 作为迁移学习的预训练骨干(torchvision.models.resnet50 直接下载 ImageNet 预训练权重)

核心语法知识点:残差块的实现

import torch
import torch.nn as nn

# ============ 1. 定义残差块(ResNet 的基本积木) ============
class ResidualBlock(nn.Module):
    def __init__(self, channels):
        super().__init__()                              # 调用父类初始化
        self.conv1 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
        # 卷积层1:通道数不变(3x3卷积,padding=1 保持尺寸)
        self.bn1 = nn.BatchNorm2d(channels)             # 批归一化:稳定训练、加速收敛(ResNet 标配)
        self.relu = nn.ReLU()                           # 激活函数

        self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
        # 卷积层2:继续提取特征
        self.bn2 = nn.BatchNorm2d(channels)             # 批归一化

    def forward(self, x):
        identity = x                                    # 保存输入副本!这就是"捷径"里的 x

        out = self.conv1(x)                             # 主路径:卷积1
        out = self.bn1(out)                             # 归一化
        out = self.relu(out)                            # 激活

        out = self.conv2(out)                           # 主路径:卷积2
        out = self.bn2(out)                             # 归一化

        out = out + identity                            # ★★★ 核心:残差相加!主路径输出 + 捷径输入
        out = self.relu(out)                            # 相加后再激活
        return out

# ============ 2. 用残差块搭建一个小型 ResNet ============
class MiniResNet(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()                              # 调用父类初始化
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, padding=1)  # 入口卷积:1→16通道
        self.bn1 = nn.BatchNorm2d(16)                   # 归一化
        self.relu = nn.ReLU()                           # 激活

        self.layer1 = ResidualBlock(16)                 # 残差块1(16通道)
        self.layer2 = ResidualBlock(16)                 # 残差块2(16通道)

        self.pool = nn.AdaptiveAvgPool2d(1)             # 自适应平均池化:任意尺寸 → 1x1
        self.fc = nn.Linear(16, num_classes)            # 分类头:16维 → 10类

    def forward(self, x):
        out = self.relu(self.bn1(self.conv1(x)))        # 入口处理
        out = self.layer1(out)                          # 过残差块1
        out = self.layer2(out)                          # 过残差块2
        out = self.pool(out)                            # 全局池化:[batch,16,H,W] → [batch,16,1,1]
        out = out.view(out.size(0), -1)                 # 展平:[batch, 16]
        out = self.fc(out)                              # 分类
        return out

model = MiniResNet(num_classes=10)   # 实例化模型

# ============ 3. 验证前向传播 ============
x = torch.randn(8, 1, 28, 28)        # 8 张 1x28x28 灰度图
out = model(x)                        # 前向传播
print(f"输入形状: {x.shape}, 输出形状: {out.shape}")  # [8,1,28,28] → [8,10]

# ============ 4. 企业实战:直接用 torchvision 的预训练 ResNet ============
# from torchvision import models
# resnet50 = models.resnet50(pretrained=True)  # 加载 ImageNet 预训练权重
# # 迁移学习:冻结骨干,只训练分类头
# for param in resnet50.parameters():
#     param.requires_grad = False    # 冻结所有层(不训练)
# resnet50.fc = nn.Linear(2048, 5)   # 替换最后的全连接层为自己的5分类任务
# # 这样只需要很少的数据和时间,就能得到高精度模型——这是企业最常用的做法

学完这个实例你掌握了

  • identity = x + out = out + identity 的残差模式
  • BatchNorm2d(批归一化)的使用
  • AdaptiveAvgPool2d:不管输入多大,统一池化到指定尺寸(极大增强网络对输入尺寸的适应性)
  • 迁移学习的工业级做法(冻结骨干 + 替换分类头)

实例 7:孪生神经网络(Siamese Network)

是什么

孪生神经网络由两个共享权重的相同子网络组成:

输入A → [神经网络] → 嵌入向量A ┐
                              ├→ 计算距离/相似度 → 输出"像不像"
输入B → [神经网络] → 嵌入向量B ┘
              ↑
        两边用的是同一套权重("孪生"的含义)
  • 它回答的不是「这是什么」,而是「这两个像不像」
  • 输出是相似度分数(0~1 或距离值)

通俗例子

双胞胎鉴定:找两位鉴定员(其实是同一个人,用同一套标准)分别给两个人画像打分,然后比较两份「画像特征」的相似度。因为两份画像出自同一标准,比较才公平有效。

为什么要学 / 有什么用

普通分类网络有个致命限制:类别数固定。新增一个类别就要重新训练。

孪生网络学到的是「相似度度量」,天然适合类别数不固定、样本少的场景:

应用说明
人脸识别(验证)判断两张照片是不是同一个人(人脸解锁、支付验证)
签名验证判断签名是否本人所写
少样本学习每类只有几个样本也能工作(对比新样本与已知样本)
重复检测商品去重、文章查重、嫌疑人追踪
推荐系统判断用户兴趣与商品的匹配度

核心语法知识点

对比损失(Contrastive Loss)

孪生网络常用对比损失训练,思想是:

  • 相似的样本对(同一个人)→ 两向量距离应该小
  • 不相似的样本对(不同人)→ 两向量距离应该大(至少大于 margin)
# 对比损失公式(margin = 1 时)
# 相似对: loss = 距离²
# 不相似对: loss = max(0, margin - 距离)²   ← 距离超过 margin 就不惩罚了
共享权重的实现方式

关键语法:同一个 nn.Linear 对象被调用两次,就是共享权重。PyTorch 中同一个层对象的参数只有一份,两次调用用的是同一份权重。

完整代码(判断两个输入是否属于同一类别)

import torch
import torch.nn as nn
import torch.nn.functional as F  # 导入函数式接口(用 F.pairwise_distance 等)

# ============ 1. 定义基网络(特征提取器) ============
class BaseNetwork(nn.Module):
    def __init__(self, input_dim=10, embedding_dim=8):
        super().__init__()                              # 调用父类初始化
        self.net = nn.Sequential(
            nn.Linear(input_dim, 32),                   # 10维输入 → 32
            nn.ReLU(),                                  # 激活
            nn.Linear(32, embedding_dim)                # 32 → 8(8维嵌入向量)
        )

    def forward(self, x):
        return self.net(x)                              # 返回 8 维嵌入向量

# ============ 2. 定义孪生网络 ============
class SiameseNetwork(nn.Module):
    def __init__(self):
        super().__init__()                              # 调用父类初始化
        self.base_network = BaseNetwork()               # ★ 只创建一次基网络!
        # 两个输入都用这同一个网络处理 → 权重完全共享

    def forward(self, x1, x2):
        out1 = self.base_network(x1)                    # 输入A 经过基网络 → 嵌入向量A
        out2 = self.base_network(x2)                    # 输入B 经过同一个基网络 → 嵌入向量B
        # 注意:是同一个 self.base_network!这就是"孪生"的实现方式
        return out1, out2

# ============ 3. 定义对比损失 ============
class ContrastiveLoss(nn.Module):
    def __init__(self, margin=1.0):
        super().__init__()                              # 调用父类初始化
        self.margin = margin                            # margin:不相似样本对的最小距离要求

    def forward(self, out1, out2, label):
        # label=1 表示相似(同类),label=0 表示不相似(不同类)
        distance = F.pairwise_distance(out1, out2)      # 计算两个嵌入向量的欧氏距离

        # 对比损失公式
        # 相似对(label=1):loss = 距离²,希望距离趋近0
        # 不相似对(label=0):loss = max(0, margin-距离)²,希望距离至少为 margin
        loss_similar = label * torch.pow(distance, 2)                     # 相似部分的损失
        loss_dissimilar = (1 - label) * torch.pow(
            torch.clamp(self.margin - distance, min=0.0), 2)              # 不相似部分的损失
        # torch.clamp(x, min=0) 把负数截断为 0:距离超过 margin 就不惩罚

        return (loss_similar + loss_dissimilar).mean()   # 取批次平均损失

# ============ 4. 创建模型、损失、优化器 ============
model = SiameseNetwork()                                # 创建孪生网络
loss_fn = ContrastiveLoss(margin=1.0)                   # 对比损失
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)  # Adam 优化器

# ============ 5. 准备训练数据(成对数据 + 相似标签) ============
batch_size = 16
x1 = torch.randn(batch_size, 10)                        # 第一组样本(10维特征)
x2 = torch.randn(batch_size, 10)                        # 第二组样本
pair_labels = torch.randint(0, 2, (batch_size, 1)).float()  # 每对的相似标签:1=同类,0=不同类

# ============ 6. 训练 ============
for epoch in range(100):
    out1, out2 = model(x1, x2)                          # 前向传播:得到两个嵌入向量
    loss = loss_fn(out1, out2, pair_labels)             # 计算对比损失

    optimizer.zero_grad()                               # 清空梯度
    loss.backward()                                     # 反向传播
    optimizer.step()                                    # 更新参数

    if (epoch + 1) % 20 == 0:
        print(f"第 {epoch+1} 轮, 对比损失: {loss.item():.4f}")

# ============ 7. 推理:判断两个新样本是否相似 ============
model.eval()
with torch.no_grad():
    sample_a = torch.randn(1, 10)                       # 新样本 A
    sample_b = torch.randn(1, 10)                       # 新样本 B
    emb_a, emb_b = model(sample_a, sample_b)            # 提取嵌入向量
    dist = F.pairwise_distance(emb_a, emb_b).item()     # 计算距离
    print(f"两样本嵌入距离: {dist:.4f}")                 # 距离小→相似,距离大→不相似
    print(f"判断结果: {'相似' if dist < 0.5 else '不相似'}")

企业应用示例:人脸识别验证流程

用户刷脸 → 摄像头拍照 → 基网络提取人脸嵌入向量(512维)
                              ↓
                     与数据库中该用户的注册照片向量计算距离
                              ↓
              距离 < 阈值 → 验证通过(是本人);否则 → 拒绝

这正是手机人脸解锁、机场人脸核验背后的技术原理。

学完这个实例你掌握了

  • 权重共享的实现(同一个层对象调用两次)
  • 对比损失的原理和代码
  • 「度量学习」思想:学相似度而非学分类,解决类别数不固定的问题

附录:常见报错与调试技巧

A.1 新手高频报错速查表

报错信息原因解决办法
RuntimeError: Expected all tensors to be on the same device模型和数据不在同一设备数据也 .to(device)
RuntimeError: mat1 and mat2 shapes cannot be multiplied全连接层输入维度不对检查 view/reshape 展平后的维度
RuntimeError: Target size is different from input size标签形状和输出形状不匹配检查标签形状,如 (batch,) vs (batch,1)
RuntimeError: element 0 of tensors does not require grad对不需要梯度的张量调用了 backward输入设 requires_grad=True 或检查计算图是否被 .detach()/.numpy() 切断
size mismatch for ... bias加载权重时模型结构不一致保证定义的层参数和保存时完全一致
CUDA out of memory (OOM)显存不够减小 batch_size、减小图片尺寸、用 torch.no_grad()

A.2 通用调试三板斧

# 1. 打印形状(解决 80% 的问题)
print(x.shape)

# 2. 打印数值范围(检查数据是否归一化)
print(x.min(), x.max(), x.mean())

# 3. 小批量过拟合测试(验证模型有学习能力)
# 取 8 个样本训练 100 轮,如果损失降不下去,说明模型/损失/数据有 bug

A.3 学习路径建议

1. 线性回归(实例1)        ← 掌握训练循环五步
2. Logistic 回归(实例2)    ← 掌握分类输出设计
3. CNN(实例4)              ← 掌握图像处理核心
4. AutoEncoder(实例3)      ← 理解无监督学习
5. ResNet(实例6)           ← 理解深层网络与迁移学习
6. Siamese(实例7)          ← 理解度量学习
7. GAN(实例5)              ← 最难,建议最后学

A.4 核心知识速记卡片

训练循环五步(一切模型通用):

y_pred = model(x)              # 前向传播
loss = loss_fn(y_pred, y)      # 计算损失
optimizer.zero_grad()          # 清空梯度
loss.backward()                # 反向传播
optimizer.step()               # 更新参数

三件套配置(一切任务通用):

任务类型输出层激活损失函数
回归无(或无激活)MSELoss
二分类SigmoidBCELoss
多分类无(CrossEntropy 自带 Softmax)CrossEntropyLoss

两个模式切换(千万别忘):

model.train()   # 训练时
model.eval()    # 测试/推理时(配合 torch.no_grad())

总结:深度学习的语法骨架其实很小——张量运算 + nn.Module 定义模型 + 训练循环五步 + DataLoader 喂数据。复杂的是不同网络结构的设计思想(卷积提取局部特征、残差解决深层退化、GAN 对抗生成、孪生度量相似度)。建议读者把每个示例代码亲手运行、修改参数观察变化,比只看不练有效十倍。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐