机器学习教程,从入门到精通,深度学习入门语法知识点完全指南(PyTorch 版)(12)
深度学习入门语法知识点完全指南(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
三种最常用激活函数对比
| 激活函数 | 公式 | 输出范围 | 特点 | 常用场景 |
|---|---|---|---|---|
| Sigmoid | 1/(1+e^(-x)) | (0, 1) | 输出可当概率;深层网络中梯度消失 | 二分类输出层 |
| Tanh | (ex-e(-x))/(ex+e(-x)) | (-1, 1) | 以 0 为中心,比 Sigmoid 好 | RNN 隐藏层 |
| ReLU | max(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。优化器就是负责「怎么调」的工具。
核心思想是梯度下降:
- 计算损失对每个权重的「梯度」(可以理解为:这个权重往哪个方向稍微动一点,损失会下降最快)
- 沿着梯度的反方向小步修正权重
- 反复迭代,损失逐渐下降
通俗例子
想象你蒙着眼睛在山上,想走到谷底(损失最小的地方):
- 梯度就是脚下「最陡的下坡方向」
- 学习率(Learning Rate)就是「每一步走多大」
- 步子太小 → 走得极慢,训练耗时太长
- 步子太大 → 一步跨过谷底,在山谷两壁来回跳,永远到不了底
- 优化器是「走路策略」:SGD 是老老实实一步步走;Adam 是自适应调整步幅的智能登山杖
常用优化器
| 优化器 | 特点 | 使用建议 |
|---|---|---|
SGD | 随机梯度下降,经典稳定,可配合动量 | CV 大任务常用 |
Adam | 自适应学习率,收敛快,调参省心 | 入门首选,90% 场景适用 |
AdamW | Adam + 权重衰减修正 | 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 |
| 损失函数 | MSE | BCELoss(二元交叉熵) |
为什么必须用 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 的两大法宝:
- 卷积层(Convolution):用小的「滤波器(卷积核)」在图片上滑动,提取局部特征(边缘、纹理、角点)
- 池化层(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 激活,训练更稳定 |
| 不用 Dropout | GAN 里通常用 BatchNorm | Dropout 造成随机性,影响对抗平衡 |
| 学习率 | 生成器和判别器都用较小的 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 |
| 二分类 | Sigmoid | BCELoss |
| 多分类 | 无(CrossEntropy 自带 Softmax) | CrossEntropyLoss |
两个模式切换(千万别忘):
model.train() # 训练时
model.eval() # 测试/推理时(配合 torch.no_grad())
总结:深度学习的语法骨架其实很小——张量运算 +
nn.Module定义模型 + 训练循环五步 + DataLoader 喂数据。复杂的是不同网络结构的设计思想(卷积提取局部特征、残差解决深层退化、GAN 对抗生成、孪生度量相似度)。建议读者把每个示例代码亲手运行、修改参数观察变化,比只看不练有效十倍。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐




所有评论(0)