Pytorch(已完结)

(本文档只是给我自己看的,大部分来源菜鸟教程,小部分我不理解的我会自己补充)

开始

1
2
pip install torch
pip install torchvision

如果不想要GPU加速,可以换成

1
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu

然后直接运行第一段代码:

1
2
3
4
5
import torch

a = torch.randn(2, 3) #创建一个服从正态分布的随机张量,均值为 0,标准差为 1

print(a)

Pytorch torch(API)

类别 API 描述
Tensors is_tensor(obj) 检查 obj 是否为 PyTorch 张量
is_storage(obj) 检查 obj 是否为 PyTorch 存储对象
is_complex(input) 检查 input 数据类型是否为复数数据类型
is_conj(input) 检查 input 是否为共轭张量
is_floating_point(input) 检查 input 数据类型是否为浮点数据类型
is_nonzero(input) 检查 input 是否为非零单一元素张量
set_default_dtype(d) 设置默认浮点数据类型为 d
get_default_dtype() 获取当前默认浮点 torch.dtype
set_default_device(device) 设置默认 torch.Tensor 分配的设备为 device
get_default_device() 获取默认 torch.Tensor 分配的设备
numel(input) 返回 input 张量中的元素总数
Creation Ops tensor(data) 通过复制 data 构造无自动梯度历史的张量
sparse_coo_tensor(indices, values) 在指定的 indices 处构造稀疏张量,具有指定的值
as_tensor(data) data 转换为张量,共享数据并尽可能保留自动梯度历史
zeros(size) 返回一个用标量值 0 填充的张量,形状由 size 定义
ones(size) 返回一个用标量值 1 填充的张量,形状由 size 定义
arange(start, end, step) 返回一个 1-D 张量,包含从 startend 的值,步长为 step
rand(size) 返回一个从 [0, 1) 区间均匀分布的随机数填充的张量
randn(size) 返回一个从标准正态分布填充的张量
Math operations add(input, other, alpha) other(由 alpha 缩放)加到 input
mul(input, other) inputother 相乘
matmul(input, other) 执行 inputother 的矩阵乘法
mean(input, dim) 计算 input 在维度 dim 上的均值
sum(input, dim) 计算 input 在维度 dim 上的和
max(input, dim) 返回 input 在维度 dim 上的最大值
min(input, dim) 返回 input 在维度 dim 上的最小值

Tensor 创建

函数 描述
torch.tensor(data, dtype, device, requires_grad) 从数据创建张量
torch.as_tensor(data, dtype, device) 将数据转换为张量(共享内存)
torch.from_numpy(ndarray) 从 NumPy 数组创建张量(共享内存)
torch.zeros(*size, dtype, device, requires_grad) 创建全零张量
torch.ones(*size, dtype, device, requires_grad) 创建全一张量
torch.empty(*size, dtype, device, requires_grad) 创建未初始化的张量
torch.arange(start, end, step, dtype, device, requires_grad) 创建等差序列张量
torch.linspace(start, end, steps, dtype, device, requires_grad) 创建等间隔序列张量
torch.logspace(start, end, steps, base, dtype, device, requires_grad) 创建对数间隔序列张量
torch.eye(n, m, dtype, device, requires_grad) 创建单位矩阵
torch.full(size, fill_value, dtype, device, requires_grad) 创建填充指定值的张量
torch.rand(*size, dtype, device, requires_grad) 创建均匀分布随机张量(范围 [0, 1))
torch.randn(*size, dtype, device, requires_grad) 创建标准正态分布随机张量
torch.randint(low, high, size, dtype, device, requires_grad) 创建整数随机张量
torch.randperm(n, dtype, device, requires_grad) 创建 0 到 n-1 的随机排列

Tensor 操作

函数 描述
torch.cat(tensors, dim) 沿指定维度连接张量
torch.stack(tensors, dim) 沿新维度堆叠张量
torch.split(tensor, split_size, dim) 将张量沿指定维度分割
torch.chunk(tensor, chunks, dim) 将张量沿指定维度分块
torch.reshape(input, shape) 改变张量的形状
torch.transpose(input, dim0, dim1) 交换张量的两个维度
torch.squeeze(input, dim) 移除大小为 1 的维度
torch.unsqueeze(input, dim) 在指定位置插入大小为 1 的维度
torch.expand(input, size) 扩展张量的尺寸
torch.narrow(input, dim, start, length) 返回张量的切片
torch.permute(input, dims) 重新排列张量的维度
torch.masked_select(input, mask) 根据布尔掩码选择元素
torch.index_select(input, dim, index) 沿指定维度选择索引对应的元素
torch.gather(input, dim, index) 沿指定维度收集指定索引的元素
torch.scatter(input, dim, index, src) src 的值散布到 input 的指定位置
torch.nonzero(input) 返回非零元素的索引

数学运算

函数 描述
torch.add(input, other) 逐元素加法
torch.sub(input, other) 逐元素减法
torch.mul(input, other) 逐元素乘法
torch.div(input, other) 逐元素除法
torch.matmul(input, other) 矩阵乘法(matrix multiply)
torch.pow(input, exponent) 逐元素幂运算
torch.sqrt(input) 逐元素平方根
torch.exp(input) 逐元素指数函数
torch.log(input) 逐元素自然对数(e)
torch.sum(input, dim) 沿指定维度求和
torch.mean(input, dim) 沿指定维度求均值(average)
torch.max(input, dim) 沿指定维度求最大值
torch.min(input, dim) 沿指定维度求最小值
torch.abs(input) 逐元素绝对值
torch.clamp(input, min, max) 将张量值限制在指定范围内
torch.round(input) 逐元素四舍五入
torch.floor(input) 逐元素向下取整(地板这一块)
torch.ceil(input) 逐元素向上取整(天花板这一块)

随机数生成

函数 描述
torch.manual_seed(seed) 设置随机种子
torch.initial_seed() 返回当前随机种子
torch.rand(*size) 创建均匀分布随机张量(范围 [0, 1))
torch.randn(*size) 创建标准正态分布随机张量
torch.randint(low, high, size) 创建整数随机张量
torch.randperm(n) 返回 0 到 n-1 的随机排列

线性代数

函数 描述
torch.dot(input, other) 计算两个向量的点积
torch.mm(input, mat2) 矩阵乘法(matrix multiply)
torch.bmm(input, mat2) 批量矩阵乘法(batch mm)
torch.eig(input) 计算矩阵的特征值和特征向量
torch.svd(input) 计算矩阵的奇异值分解
torch.inverse(input) 计算矩阵的逆
torch.det(input) 计算矩阵的行列式
torch.trace(input) 计算矩阵的迹

设备管理

函数 描述
torch.cuda.is_available() 检查 CUDA 是否可用
torch.device(device) 创建一个设备对象(如 'cpu''cuda:0'
torch.to(device) 将张量移动到指定设备

实例

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

# 创建张量
x = torch.tensor([1, 2, 3])
y = torch.zeros(2, 3)

# 数学运算
z = torch.add(x, 1) # 逐元素加 1
print(z)

# 索引和切片
mask = x > 1
selected = torch.masked_select(x, mask)
print(selected)

# 设备管理
if torch.cuda.is_available():
device = torch.device('cuda')
x = x.to(device)
print(x.device)

其中:

  • 布尔掩码:使用布尔值(True / False)数组来选择或过滤另一个数组中特定元素的技术

比如实例里的 mask = x > 1 (True为保留的元素,False为忽略)

1
2
3
4
5
6
7
import numpy as np

arr = np.array([1, 2, 3, 4, 5, 6])
mask = arr > 3 # 生成布尔掩码:[False, False, False, True, True, True]

filtered = arr[mask] # 使用掩码筛选
print(filtered) # 输出: [4 5 6]

Pytorch torch.nn(API)

1、nn.Module 类

  • nn.Module 是所有自定义神经网络模型的基类。用户通常会从这个类派生自己的模型类,并在其中定义网络层结构以及前向传播函数(forward pass)

2、预定义层(Modules)

  • 包括各种类型的层组件,例如卷积层(nn.Conv1d, nn.Conv2d, nn.Conv3d)、全连接层(nn.Linear)、激活函数(nn.ReLU, nn.Sigmoid, nn.Tanh)等

3、容器类

  • nn.Sequential:允许将多个层按顺序组合起来,形成简单的线性堆叠网络
  • nn.ModuleListnn.ModuleDict:可以动态地存储和访问子模块,支持可变长度或命名的模块集合

4、损失函数(Loss Functions)

  • torch.nn 包含了一系列用于衡量模型预测与真实标签之间差异的损失函数,例如均方误差损失(nn.MSELoss)、交叉熵损失(nn.CrossEntropyLoss)等

5、实用函数接口(Functional Interface)

  • nn.functional(通常简写为 F),包含了许多可以直接作用于张量上的函数,它们实现了与层对象相同的功能,但不具有参数保存和更新的能力。例如,可以使用 F.relu() 直接进行 ReLU 操作,或者 F.conv2d() 进行卷积操作

6、初始化方法

  • torch.nn.init 提供了一些常用的权重初始化策略,比如 Xavier 初始化 (nn.init.xavier_uniform_()) 和 Kaiming 初始化 (nn.init.kaiming_uniform_()),这些对于成功训练神经网络至关重要

神经网络容器

类/函数 描述
torch.nn.Module 所有神经网络模块的基类
torch.nn.Sequential(*args) 按顺序组合多个模块
torch.nn.ModuleList(modules) 将子模块存储在列表中
torch.nn.ModuleDict(modules) 将子模块存储在字典中
torch.nn.ParameterList(parameters) 将参数存储在列表中
torch.nn.ParameterDict(parameters) 将参数存储在字典中

线性层

类/函数 描述
torch.nn.Linear(in_features, out_features) 全连接层
torch.nn.Bilinear(in1_features, in2_features, out_features) 双线性层

卷积层

类/函数 描述
torch.nn.Conv1d(in_channels, out_channels, kernel_size) 一维卷积层
torch.nn.Conv2d(in_channels, out_channels, kernel_size) 二维卷积层
torch.nn.Conv3d(in_channels, out_channels, kernel_size) 三维卷积层
torch.nn.ConvTranspose1d(in_channels, out_channels, kernel_size) 一维转置卷积层
torch.nn.ConvTranspose2d(in_channels, out_channels, kernel_size) 二维转置卷积层
torch.nn.ConvTranspose3d(in_channels, out_channels, kernel_size) 三维转置卷积层

池化层

类/函数 描述
torch.nn.MaxPool1d(kernel_size) 一维最大池化层
torch.nn.MaxPool2d(kernel_size) 二维最大池化层
torch.nn.MaxPool3d(kernel_size) 三维最大池化层
torch.nn.AvgPool1d(kernel_size) 一维平均池化层
torch.nn.AvgPool2d(kernel_size) 二维平均池化层
torch.nn.AvgPool3d(kernel_size) 三维平均池化层
torch.nn.AdaptiveMaxPool1d(output_size) 一维自适应最大池化层
torch.nn.AdaptiveAvgPool1d(output_size) 一维自适应平均池化层
torch.nn.AdaptiveMaxPool2d(output_size) 二维自适应最大池化层
torch.nn.AdaptiveAvgPool2d(output_size) 二维自适应平均池化层
torch.nn.AdaptiveMaxPool3d(output_size) 三维自适应最大池化层
torch.nn.AdaptiveAvgPool3d(output_size) 三维自适应平均池化层

激活函数

类/函数 描述
torch.nn.ReLU() ReLU 激活函数
torch.nn.Sigmoid() Sigmoid 激活函数
torch.nn.Tanh() Tanh 激活函数
torch.nn.Softmax(dim) Softmax 激活函数
torch.nn.LogSoftmax(dim) LogSoftmax 激活函数
torch.nn.LeakyReLU(negative_slope) LeakyReLU 激活函数
torch.nn.ELU(alpha) ELU 激活函数
torch.nn.SELU() SELU 激活函数
torch.nn.GELU() GELU 激活函数

损失函数

类/函数 描述
torch.nn.MSELoss() 均方误差损失
torch.nn.L1Loss() L1 损失
torch.nn.CrossEntropyLoss() 交叉熵损失
torch.nn.NLLLoss() 负对数似然损失
torch.nn.BCELoss() 二分类交叉熵损失
torch.nn.BCEWithLogitsLoss() 带 Sigmoid 的二分类交叉熵损失
torch.nn.KLDivLoss() KL 散度损失
torch.nn.HingeEmbeddingLoss() 铰链嵌入损失
torch.nn.MultiMarginLoss() 多分类间隔损失
torch.nn.SmoothL1Loss() 平滑 L1 损失

归一化层

类/函数 描述
torch.nn.BatchNorm1d(num_features) 一维批归一化层
torch.nn.BatchNorm2d(num_features) 二维批归一化层
torch.nn.BatchNorm3d(num_features) 三维批归一化层
torch.nn.LayerNorm(normalized_shape) 层归一化
torch.nn.InstanceNorm1d(num_features) 一维实例归一化层
torch.nn.InstanceNorm2d(num_features) 二维实例归一化层
torch.nn.InstanceNorm3d(num_features) 三维实例归一化层
torch.nn.GroupNorm(num_groups, num_channels) 组归一化

循环神经网络层

类/函数 描述
torch.nn.RNN(input_size, hidden_size) 简单 RNN 层
torch.nn.LSTM(input_size, hidden_size) LSTM 层
torch.nn.GRU(input_size, hidden_size) GRU 层
torch.nn.RNNCell(input_size, hidden_size) 简单 RNN 单元
torch.nn.LSTMCell(input_size, hidden_size) LSTM 单元
torch.nn.GRUCell(input_size, hidden_size) GRU 单元

嵌入层

类/函数 描述
torch.nn.Embedding(num_embeddings, embedding_dim) 嵌入层

Dropout 层

类/函数 描述
torch.nn.Dropout(p) Dropout 层
torch.nn.Dropout2d(p) 2D Dropout 层
torch.nn.Dropout3d(p) 3D Dropout 层

实用函数

函数 描述
torch.nn.functional.relu(input) 应用 ReLU 激活函数
torch.nn.functional.sigmoid(input) 应用 Sigmoid 激活函数
torch.nn.functional.softmax(input, dim) 应用 Softmax 激活函数
torch.nn.functional.cross_entropy(input, target) 计算交叉熵损失
torch.nn.functional.mse_loss(input, target) 计算均方误差损失

张量(tensor)

张量(Tensor)是 PyTorch 中的核心数据结构,用于存储和操作多维数组

张量可以视为一个多维数组,支持加速计算的操作

在 PyTorch 中,张量的概念类似于 NumPy 中的数组,但是 PyTorch 的张量可以运行在不同的设备上,比如 CPU 和 GPU,这使得它们非常适合于进行大规模并行计算,特别是在深度学习领域。

  • 维度(Dimensionality):张量的维度指的是数据的多维数组结构。例如,一个标量(0维张量)是一个单独的数字,一个向量(1维张量)是一个一维数组,一个矩阵(2维张量)是一个二维数组,以此类推
  • 形状(Shape):张量的形状是指每个维度上的大小。例如,一个形状为(3, 4)的张量意味着它有3行4列
  • 数据类型(Dtype):张量中的数据类型定义了存储每个元素所需的内存大小和解释方式。PyTorch支持多种数据类型,包括整数型(如torch.int8torch.int32)、浮点型(如torch.float32torch.float64)和布尔型(torch.bool

张量可以存储在 CPU 或 GPU 中,GPU 张量可显著加速计算,先放张菜鸟教程的图:

张量基本方式

但感觉直接说概念有点太过于抽象,不如先看看几个基本方式:

方法 说明 示例代码
torch.tensor(data) 从 Python 列表或 NumPy 数组创建张量 x = torch.tensor([[1, 2], [3, 4]])
torch.zeros(size) 创建一个全为零的张量 x = torch.zeros((2, 3))
torch.ones(size) 创建一个全为 1 的张量 x = torch.ones((2, 3))
torch.empty(size) 创建一个未初始化的张量 x = torch.empty((2, 3))
torch.rand(size) 创建一个服从均匀分布的随机张量,值在 [0, 1) x = torch.rand((2, 3))
torch.randn(size) 创建一个服从正态分布的随机张量,均值为 0,标准差为 1 x = torch.randn((2, 3))
torch.arange(start, end, step) 创建一个一维序列张量,类似于 Python 的 range x = torch.arange(0, 10, 2)
torch.linspace(start, end, steps) 创建一个在指定范围内等间隔的序列张量 x = torch.linspace(0, 1, 5)
torch.eye(size) 创建一个单位矩阵(对角线为 1,其他为 0) x = torch.eye(3)
torch.from_numpy(ndarray) 将 NumPy 数组转换为张量 x = torch.from_numpy(np.array([1, 2, 3]))

张量基本属性

然后张量的属性:

属性 说明 示例
.shape 获取张量的形状 tensor.shape
.size() 获取张量的形状 tensor.size()
.dtype 获取张量的数据类型 tensor.dtype
.device 查看张量所在的设备 (CPU/GPU) tensor.device
.dim() 获取张量的维度数 tensor.dim()
.requires_grad 是否启用梯度计算 tensor.requires_grad
.numel() 获取张量中的元素总数 tensor.numel()
.is_cuda 检查张量是否在 GPU 上 tensor.is_cuda
.T 获取张量的转置(适用于 2D 张量) tensor.T
.item() 获取单元素张量的值 tensor.item()
.is_contiguous() 检查张量是否连续存储 tensor.is_contiguous()

看了上面之后,结合下面示例可以更加理解:

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

# 创建一个 2D 张量
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
#指定为浮点数,也可以自己尝试 torch. 然后可以看到其他的变量类型

# 张量的属性
print("Tensor:\n", tensor)
print("Shape:", tensor.shape) # 获取形状
print("Size:", tensor.size()) # 获取形状(另一种方法)
print("Data Type:", tensor.dtype) # 数据类型
print("Device:", tensor.device) # 设备
print("Dimensions:", tensor.dim()) # 维度数
print("Total Elements:", tensor.numel()) # 元素总数
print("Requires Grad:", tensor.requires_grad) # 是否启用梯度
print("Is CUDA:", tensor.is_cuda) # 是否在 GPU 上
print("Is Contiguous:", tensor.is_contiguous()) # 是否连续存储

# 获取单元素值
single_value = torch.tensor(42)
print("Single Element Value:", single_value.item())

# 转置张量
tensor_T = tensor.T
print("Transposed Tensor:\n", tensor_T)

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Tensor:
tensor([[1., 2., 3.],
[4., 5., 6.]])
Shape: torch.Size([2, 3])
Size: torch.Size([2, 3])
Data Type: torch.float32
Device: cpu
Dimensions: 2
Total Elements: 6
Requires Grad: False
Is CUDA: False
Is Contiguous: True
Single Element Value: 42
Transposed Tensor:
tensor([[1., 4.],
[2., 5.],
[3., 6.]])

除此之外还有个是torch.stack((x, x), dim=?),这里表示将张量x和x在第几维度(dim)进行堆叠,比如说dim=0,1,2,这边类比到坐标轴上,那么

1
2
3
#dim=0 Z轴  层数+1(层)
#dim=1 Y轴 行数+1(行)
#dim=2 X轴 列数+1(列)

可以具体看个例子:

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

a = torch.randn(3, 4)
#b = torch.randn(2, 3)
#c = torch.eye(3)
#d = torch.from_numpy(np.array([[1, 2], [3, 4]]))
e = torch.stack((a, a), dim=0)
f = torch.stack((a, a), dim=1)
g = torch.stack((a, a), dim=2)


print(" ")
print("===== a =====")
print(a)
print(" ")
#print(b)
#print(c)
#print(c.shape)
#print(d)
print("===== dim=0 =====")
print(e)
print(e.shape)
print(" ")
print("===== dim=1 =====")
print(f)
print(f.shape)
print(" ")
print("===== dim=2 =====")
print(g)
print(g.shape)

运行后结果是:(size里的[2, 3, 4]就分别对应0,1,2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
===== a =====
tensor([[0.3607, 0.9021, 0.0285, 0.0186],
[0.1229, 0.5829, 0.8991, 0.4807],
[0.0543, 0.2008, 0.0578, 0.4865]])

===== dim=0 ===== // 直接层数 + 1(原本层数为1,在dim=0进行两个x的堆叠所以层数为2
tensor([[[0.3607, 0.9021, 0.0285, 0.0186],
[0.1229, 0.5829, 0.8991, 0.4807],
[0.0543, 0.2008, 0.0578, 0.4865]],

[[0.3607, 0.9021, 0.0285, 0.0186],
[0.1229, 0.5829, 0.8991, 0.4807],
[0.0543, 0.2008, 0.0578, 0.4865]]])
torch.Size([2, 3, 4])

===== dim=1 ===== // 直接行数 + 1(原本层数为1,在dim=1进行两个x的堆叠)
tensor([[[0.3607, 0.9021, 0.0285, 0.0186], // 此时由于1对应行,所以层变为3,每层函数变为2
[0.3607, 0.9021, 0.0285, 0.0186]],

[[0.1229, 0.5829, 0.8991, 0.4807],
[0.1229, 0.5829, 0.8991, 0.4807]],

[[0.0543, 0.2008, 0.0578, 0.4865],
[0.0543, 0.2008, 0.0578, 0.4865]]])
torch.Size([3, 2, 4])

===== dim=2 ===== // 直接列数 + 1
tensor([[[0.3607, 0.3607], // 将原本a的列转换为行然后copy一份(原理与上相同)
[0.9021, 0.9021],
[0.0285, 0.0285],
[0.0186, 0.0186]],

[[0.1229, 0.1229],
[0.5829, 0.5829],
[0.8991, 0.8991],
[0.4807, 0.4807]],

[[0.0543, 0.0543],
[0.2008, 0.2008],
[0.0578, 0.0578],
[0.4865, 0.4865]]])
torch.Size([3, 4, 2])

这样映射到XYZ轴后相比于直接写dim在新的维度上进行拼接,这个新的维度出现在第1维(从0开始计数)这样子好理解的多

张量基本操作

基础操作:

操作 说明 示例代码
+, -, *, / 元素级加法、减法、乘法、除法 z = x + y
torch.matmul(x, y) 矩阵乘法 z = torch.matmul(x, y)
torch.dot(x, y) 向量点积(仅适用于 1D 张量) z = torch.dot(x, y)
torch.sum(x) 求和 z = torch.sum(x)
torch.mean(x) 求均值 z = torch.mean(x)
torch.max(x) 求最大值 z = torch.max(x)
torch.min(x) 求最小值 z = torch.min(x)
torch.argmax(x, dim) 返回最大值的索引(指定维度) z = torch.argmax(x, dim=1)
torch.softmax(x, dim) 计算 softmax(指定维度) z = torch.softmax(x, dim=1)

这里大部分如果学过线性代数或者加减法的一般都很好理解,然后这里比较需要注意的就是torch.dot()、argmax()和softmax(),这里我们先看 torch.dot() ,这里的点积(适用于一维向量),其实就是我们数学中学的向量的点乘,具体公式为:

用torch来表示就是:

1
2
3
4
5
6
7
import torch

a = torch.tensor([1.0, 2.0, 3.0])
b = torch.tensor([4.0, 5.0, 6.0])

dot_product = torch.dot(a, b)
print(dot_product) # 输出: 1*4 + 2*5 + 3*6 = 32

然后就是 torch.argmax(),返回最大值的索引,不过这个最大值的索引是指将数组展开来后的,比如:

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

a = torch.randn(3, 4)

print(a)
print(a.min())
print(a.max())
print(a.mean())
print(a.argmax())

#tensor([[-0.0678, -0.9911, -1.4649, 0.9039],
# [ 1.1405, 0.7636, -0.0447, -0.4907],
# [-0.8256, -1.1506, 0.7874, -0.5046]])
#tensor(-1.4649)
#tensor(1.1405)
#tensor(-0.1620)
#tensor(4) <--- 指的是1.1405,最开始从0开始数

最后就是 torch.softmax() ,他是种常用的激活函数,主要用于多分类问题的输出层。它的作用是将一个 任意实数向量 转换成一个 概率分布,总结来说,就是每个数值的概率密度,对于输入如下向量:

公式如下:

为了避免整数溢出,实际通常减去最大值:

用torch表示就是:

1
2
3
4
5
6
7
8
9
10
11
import torch

import torch.nn.functional as F

a = torch.tensor([[1.0, 2.0, 3.0]])

probs = F.softmax(a, dim=1) # dim=0是行方向,dim=1是列方向

print(probs)

#tensor([[0.0900, 0.2447, 0.6652]])

形状操作:

操作 说明 示例代码
x.view(shape) 改变张量的形状(不改变数据) z = x.view(3, 4)
x.reshape(shape) 类似于 view,但更灵活 z = x.reshape(3, 4)
x.t() 转置矩阵 z = x.t()
x.unsqueeze(dim) 在指定维度添加一个维度 z = x.unsqueeze(0)
x.squeeze(dim) 去掉指定维度为 1 的维度 z = x.squeeze(0)
torch.cat((x, y), dim) 按指定维度连接多个张量 z = torch.cat((x, y), dim=1)
x.flatten() 将张量展平成一维 z = x.flatten()

同样地,这里只介绍unsqueezsqueezecat

先看个示例:

1
2
3
4
5
6
7
8
9
10
11
12
import torch

a = torch.tensor([[1.0, 2.0, 3.0]])
b = torch.tensor([[0.5, 0.5, 0.5]])

c = torch.cat((a, b), dim=0)
d = torch.cat((a, b), dim=1)
print(c)
print(d)
#tensor([[1.0000, 2.0000, 3.0000],
# [0.5000, 0.5000, 0.5000]])
#tensor([[1.0000, 2.0000, 3.0000, 0.5000, 0.5000, 0.5000]])

dim=0依旧是行方向,然后dim=1是列方向,用cat连接之后可以发现,c变为2行,而d变为6列

然后就是squeeze

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch

c = torch.randn(1, 3, 4)
e = c.squeeze(0)

print(c)
print(e)

#tensor([[[-0.9388, 0.3443, 0.7916, -1.5823],
# [-1.8765, -0.2879, 0.8101, 0.2462],
# [-1.5106, 0.7375, -0.7615, 0.1300]]])
#tensor([[-0.9388, 0.3443, 0.7916, -1.5823],
# [-1.8765, -0.2879, 0.8101, 0.2462],
# [-1.5106, 0.7375, -0.7615, 0.1300]])

很明显的可以看到,e被删去了0维(此时dim=0的大小是1,也就是c只有1层)

那么的,unsqueeze 同理

GPU加速

将张量转移到 GPU:

1
2
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
x = torch.tensor([1.0, 2.0, 3.0], device=device)

检查 GPU 是否可用:

1
torch.cuda.is_available()  # 返回 True 或 False

张量与Numpy互相操作

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
import numpy as np

a = np.array([[1.0, 2.0], [2.0, 3.0]])
b = torch.from_numpy(a)

print(a)
print(b)

#[[1. 2.]
# [2. 3.]]
#tensor([[1., 2.],
# [2., 3.]], dtype=torch.float64)

神经网络基础

此节点只做基本介绍,后面会有详细的单独介绍(公式部分也仅做了解)

神经元(Neuron)

神经元是神经网络的基本单元,它接收输入信号,通过加权求和后与偏置(bias)相加,然后通过激活函数处理以产生输出

神经元的权重和偏置是网络学习过程中需要调整的参数,其公式如下:

其中 表示权重, 表示输入, 表示偏置

神经元接收多个输入(例如x1, x2, …, xn),如果输入的加权和大于激活阈值(activation potential),则产生二进制输出:

层(Layer)

输入层和输出层之间的层被称为隐藏层,层与层之间的连接密度和类型构成了网络的配置

神经网络由多个层组成,包括:

  • 输入层(Input Layer):接收原始输入数据
  • 隐藏层(Hidden Layer):对输入数据进行处理,可以有多个隐藏层
  • 输出层(Output Layer):产生最终的输出结果

经典神经网络如下图:

这也是前馈神经网络的基本结构

前馈神经网络(FNN)

前馈神经网络(Feedforward Neural Network,FNN)是神经网络家族中的基本单元

前馈神经网络特点是数据从输入层开始,经过一个或多个隐藏层,最后到达输出层,全过程没有循环或反馈

前馈神经网络的基本结构:

  • 输入层: 数据进入网络的入口点。输入层的每个节点代表一个输入特征
  • 隐藏层:一个或多个层,用于捕获数据的非线性特征。每个隐藏层由多个神经元组成,每个神经元通过激活函数增加非线性能力
  • 输出层:输出网络的预测结果。节点数和问题类型相关,例如分类问题的输出节点数等于类别数
  • 连接权重与偏置:每个神经元的输入通过权重进行加权求和,并加上偏置值,然后通过激活函数传递

循环神经网络(RNN)

循环神经网络(Recurrent Neural Network, RNN)络是一类专门处理序列数据的神经网络,能够捕获输入数据中时间或顺序信息的依赖关系

RNN 的特别之处在于它具有”记忆能力”,可以在网络的隐藏状态中保存之前时间步的信息

循环神经网络用于处理随时间变化的数据模式

在 RNN 中,相同的层被用来接收输入参数,并在指定的神经网络中显示输出参数

卷积神经网络(CNN)

卷积神经网络(Convolutional Neural Network, CNN)是一种专门用于处理具有网格结构数据(如图像、视频、语音频谱图)的深度学习模型,通过局部连接、权值共享和池化等机制,自动学习空间层次特征,在计算机视觉任务中取得了巨大成功

实例

在 PyTorch 中,构建神经网络通常需要继承 nn.Module 类

nn.Module 是所有神经网络模块的基类,你需要定义以下两个部分:

  • __init__():定义网络层
  • forward():定义数据的前向传播过程

简单的全连接神经网络(Fully Connected Network):

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

class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
# 定义一个输入层到隐藏层的全连接层
self.fc1 = nn.Linear(2, 2) # 输入两个特征,输出两个特征
self.fc2 = nn.Linear(2, 1) # 输入两个特征,输出一个特征

def forward(self, x):
x = torch.relu(self.fc1(x)) # 使用relu激活函数
x = self.fc2(x)
return x

model = SimpleNN()

print(model)

#SimpleNN(
# (fc1): Linear(in_features=2, out_features=2, bias=True)
# (fc2): Linear(in_features=2, out_features=1, bias=True)
#)

初次看到可能有些地方会不理解,下面介绍几个:

super(SimpleNN, self).__init__() 调用父类的初始化方法

relu 是最常用的激活函数之一,其定义为:

relu计算简单,只需判断正负,而且正区梯度恒为1,利于训练

PyTorch 提供了许多常见的神经网络层,以下是几个常见的:

  • nn.Linear(in_features, out_features):全连接层,输入 in_features 个特征,输出 out_features 个特征
  • nn.Conv2d(in_channels, out_channels, kernel_size):2D 卷积层,用于图像处理
  • nn.MaxPool2d(kernel_size):2D 最大池化层,用于降维
  • nn.ReLU():ReLU 激活函数,常用于隐藏层
  • nn.Softmax(dim):Softmax 激活函数,通常用于输出层,适用于多类分类问

激活函数(Activation Function)

激活函数决定了神经元是否应该被激活。它们是非线性函数,使得神经网络能够学习和执行更复杂的任务。常见的激活函数包括:

Sigmoid: 用于二分类问题,输出值在 0 和 1 之间

Tanh: 输出值在 -1 和 1 之间,常用于输出层之前

ReLU(Rectified Linear Unit):目前最流行的激活函数之一,定义为 f(x) = max(0, x),有助于解决梯度消失问题

Softmax: 常用于多分类问题的输出层,将输出转换为概率分布

各激活方式如下:

1
2
3
4
5
6
7
8
9
10
import torch.nn.fuctional as F
import torch

output1 = F.relu(input)

output2 = torch.sigmoid(input)

output3 = torch.tanh(input)

output4 = F.softmax(input, dim=1)

损失函数(Loss Function)

logit(对数几率)是未经过归一化或激活函数(如 Softmax、Sigmoid)处理的线性输出,其表达式如下:

损失函数用于衡量模型的预测值与真实值之间的差异

常见的损失函数包括:

均方误差(MSELoss):回归问题常用,计算输出与目标值的平方差

交叉熵损失(CrossEntropyLoss):分类问题常用,计算输出和真实标签之间的交叉熵

BCEWithLogitsLoss:二分类问题,结合了 Sigmoid 激活和二元交叉熵损失

1
2
3
4
5
6
7
8
# 均方误差损失
criterion = nn.MSELoss()

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

# 二分类交叉熵损失
criterion = nn.BCEWithLogitsLoss()

优化器(Optimizer)

优化器负责在训练过程中更新网络的权重和偏置

常见的优化器包括:

SGD(随机梯度下降)

Adam(自适应矩估计)

RMSprop(均方根传播)

1
2
3
4
5
6
7
import torch.optim as optim

# 使用 SGD 优化器
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 使用 Adam 优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)

训练过程(Training Process)

训练神经网络涉及以下步骤:

  1. 准备数据:通过 DataLoader 加载数据
  2. 定义损失函数和优化器
  3. 前向传播:计算模型的输出
  4. 计算损失:与目标进行比较,得到损失值
  5. 反向传播:通过 loss.backward() 计算梯度
  6. 更新参数:通过 optimizer.step() 更新模型的参数
  7. 重复上述步骤,直到达到预定的训练轮数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 假设已经定义好了模型、损失函数和优化器

# 训练数据示例
X = torch.randn(10, 2) # 10 个样本,每个样本有 2 个特征
Y = torch.randn(10, 1) # 10 个目标标签

# 训练过程
for epoch in range(100): # 训练 100 轮
model.train() # 设置模型为训练模式
optimizer.zero_grad() # 清除梯度
output = model(X) # 前向传播
loss = criterion(output, Y) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新权重

if (epoch + 1) % 10 == 0: # 每 10 轮输出一次损失
print(f'Epoch [{epoch + 1}/100], Loss: {loss.item():.4f}')

Pytorch torch.optim

优化器是深度学习中的核心组件,负责根据损失函数的梯度调整模型参数,使模型能够逐步逼近最优解

在PyTorch中,torch.optim 模块提供了多种优化算法的实现

为什么需要优化器

  • 自动化参数更新:手动计算和更新每个参数非常繁琐
  • 加速收敛:使用优化算法比普通梯度下降更快找到最优解
  • 避免局部最优:某些优化器具有跳出局部最优的能力

常见优化器类型

优化器名称 主要特点 适用场景
SGD 简单基础 基础教学、简单模型
Adam 自适应学习率 大多数深度学习任务
RMSprop 适应学习率 RNN网络
Adagrad 参数独立学习率 稀疏数据

常用优化器

SGD (随机梯度下降)

1
2
3
import torch.optim as optim

optimizer = optim.SGD(params, lr=0.01, momentum=0, dampening=0, weight_decay=0, nesterov=False)

核心参数

  • lr (float):学习率(默认0.01)
  • momentum (float):动量因子(默认0)
  • weight_decay (float):L2正则化系数(默认0)

特点

  • 最简单的优化算法
  • 可以添加动量项加速收敛
  • 适合作为基准比较

Adam (自适应矩估计)

1
2
3
import torch.optim as optim

optimizer = optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0, amsgrad=False)

核心参数

  • betas (Tuple[float, float]):用于计算梯度和梯度平方的移动平均系数
  • eps (float):数值稳定项(默认1e-8)
  • amsgrad (bool):是否使用AMSGrad变体(默认False)

特点

  • 自适应学习率
  • 结合了动量概念
  • 大多数情况下的默认选择

高级技巧

学习率调度

1
2
3
4
5
6
7
8
9
import torch.optim.lr_scheduler as StepLR

optimizer = optim.SGD(model.parameters(), lr=0.1)
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)

for epoch in range(100):
train(..)
validate(..)
scheduler.step() # 更新学习率

学习率调度就是:

按照一定规则,随着训练的进行,自动改变 lr,而不是一直用同一个值

StepLR(optimizer, step_size=30, gamma=0.1) 表示优化器每经过30轮,就将当前的学习率乘以gamma

总结下来就是:

  • 前 30 轮:lr = 0.1
    • 让模型快速从一堆随机参数中粗略找到一个“还行”的区域
  • 后 70 轮:逐渐把 lr 调小到 0.01 → 0.001 → 0.0001
    • 让模型在已经不错的区域里,慢慢微调、稳定收敛,避免 loss 抖动太厉害

参数分组优化

1
2
3
4
optim.SGD([
{'params': model.base.parameters()}, # 基础层
{'params': model.classifier.parameters(), 'lr': 1e-3} # 分类层
], lr=1e-2)

整个模型分成两大块:

  • model.base:基础特征提取部分(比如 ResNet 的 backbone)
  • model.classifier:最后的分类头

很明显的是,基础层使用外面的学习率(lr=1e-2),然后分类头单独使用1e-3的学习率

梯度裁剪

1
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

先把 model.parameters() 里所有参数的梯度拿出来

算一个“总的梯度范数”(就可以理解成“所有梯度的大小合在一起”的长度)

如果这个总大小 <= 1.0:不动

如果 > 1.0统一把所有梯度等比例缩小,缩小到“总大小 = 1.0”为止

所以可以知道,并不是我们理解上的裁剪,而是等比例缩放

假设某个模型只有两个参数,它们的梯度拼成一个向量可以理解为:

1
grad = [3, 4]
  • 它的 L2 范数是:
    total_norm = sqrt(3^2 + 4^2) = 5
  • 我们的 max_norm = 1.0

那缩放系数就是:

1
clip_coef = max_norm / total_norm = 1.0 / 5 = 0.2

于是裁剪后:

1
2
原梯度: [3, 4]
裁剪后: [3 * 0.2, 4 * 0.2] = [0.6, 0.8]

运用示例:

1
2
3
4
5
6
7
optimizer.zero_grad()
loss.backward() # 先算出梯度

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 再裁剪梯度

optimizer.step() # 最后用裁剪后的梯度更新参数


第一个神经网络

实例1

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

# 定义输入层大小、隐藏层大小、输出层大小和批量大小
inp, hid, outp, size = 10, 5, 1, 10

x = torch.randn(size, inp)
y = torch.tensor([[1.0], [0.0], [0.0],
[1.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0]]) # 目标数据

# 创建顺序模型,包含线性层、ReLU激活函数和Sigmoid激活函数
model = nn.Sequential(
nn.Linear(inp, hid), # 输入层 -> 隐藏层的线性变化
nn.ReLU(), # 隐藏层的激活函数
nn.Linear(hid, outp), # 隐藏层 -> 输出层的线性变化
nn.Sigmoid() # 输出层的激活函数
)

# 定义均方误差损失函数和随机梯度下降优化器
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01) #学习率为0.01

# 执行梯度下降算法进行模型训练
for epoch in range(50):
y_pred = model(x)
loss = criterion(y_pred, y)
print("epoch: ", epoch, " loss: ", loss)

optimizer.zero_grad()
loss.backward()
optimizer.step()
  • inp:输入层大小为 10,即每个数据点有 10 个特征
  • hid:隐藏层大小为 5,即隐藏层包含 5 个神经元
  • outp:输出层大小为 1,即输出一个标量,表示二分类结果(0 或 1)
  • size:每个批次包含 10 个样本

nn.Sequential 用于按顺序定义网络层

  • nn.Linear(inp, hid):定义输入层到隐藏层的线性变换,输入特征是 10 个,隐藏层有 5 个神经元
  • nn.ReLU():在隐藏层后添加 ReLU 激活函数,增加非线性
  • nn.Linear(hid, outp):定义隐藏层到输出层的线性变换,输出为 1 个神经元
  • nn.Sigmoid():输出层使用 Sigmoid 激活函数,将结果映射到 0 到 1 之间,用于二分类任务

梯度下降算法

假设我们有一个可微的损失函数 ,其中 θ 是模型的参数(例如神经网络的权重),我们希望找到使 最小的参数值 θ

梯度下降利用这样一个事实:函数在某点处的梯度方向是函数值增长最快的方向。因此,负梯度方向就是函数值下降最快的方向

于是,我们迭代地更新参数:

其中:

那么问题来了,现在我们找到了下降最快的方向时候,需要做什么?

答案就是,往下降最快的地方迈一步,这样的话迈完之后可以继续找下一个最小的地方,其中,迈多少就是步长,也就是学习率(lr),如果步子太大(学习率过高):可能一步跨过最低点,甚至越走越高(发散),如果步子太小(学习率过低):收敛太慢,训练效率低,所以,我们更新公式如下:

比如上面代码所写的那样:

1
2
3
optimizer.zero_grad()
loss.backward()
optimizer.step()

先清空旧梯度,然后计算当前梯度(下降最快的方向),然后沿着负梯度方向走一步,那会不会一直走下去呢?答案是不会,当下面几种情况下停止:

  • 损失函数变化非常小(收敛)
  • 达到最大训练轮数(epochs)
  • 验证集性能不再提升(防止过拟合)

下面举个例子:

Q1:为什么 “最快下降方向” ≠ “直达最低点”

A1:因为 梯度只反映局部信息(一阶导数),它假设函数在附近是线性的。但实际损失函数可能是弯曲的(非线性)。所以你只能“走一小步”,然后重新评估方向——这正是迭代优化的本质

实例2

你也可以自定义一个神经网络:

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

n_samples = 100
data = torch.randn(n_samples, 2)
labels = (data[:, 0]**2 + data[:, 1]**2 < 1).float().unsqueeze(1)

#定义神经网络
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
self.fc1 = nn.Linear(2, 4)
self.fc2 = nn.Linear(4, 1)
self.sigmoid = nn.Sigmoid() # 二分类激活函数

def forward(self, x):
x = torch.relu(self.fc1(x)) # 输入层用relu激活函数
x = torch.sigmoid(self.fc2(x)) # 输出层用sigmoid激活函数
return x

model = SimpleNN()

#定义损失函数和优化器
criterion = nn.BCELoss() # 二元交叉熵损失
optimizer = optim.SGD(model.parameters(), lr=0.1) # 随机梯度下降熵优化器

epochs = 100
for epoch in range(epochs):
outputs = model(data)
loss = criterion(outputs, labels)

#反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()

if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch + 1}/{epochs}], Loss: {loss.item():.4f}')

数据处理与加载

PyTorch 数据处理与加载的介绍:

  • 自定义 Dataset:通过继承 torch.utils.data.Dataset 来加载自己的数据集
  • DataLoaderDataLoader 按批次加载数据,支持多线程加载并进行数据打乱
  • 数据预处理与增强:使用 torchvision.transforms 进行常见的图像预处理和增强操作,提高模型的泛化能力
  • 加载标准数据集torchvision.datasets 提供了许多常见的数据集,简化了数据加载过程
  • 多个数据源:通过组合多个 Dataset 实例来处理来自不同来源的数据

自定义Dataset

torch.utils.data.Dataset 是一个抽象类,允许你从自己的数据源中创建数据集。

我们需要继承该类并实现以下两个方法:

  • __len__(self):返回数据集中的样本数量
  • __getitem__(self, idx):通过索引返回一个样本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import torch
from torch.utils.data import Dataset

class MyDataset(Dataset):
def __init__(self, X_Data, Y_Data):
"""
初始化数据 X_data 和 Y_data 是两个列表或数组
X_data: 输入特征
Y_data: 目标标签
"""
self.X_Data = X_Data
self.Y_Data = Y_Data

def __len__(self):
return len(self.X_Data) # 返回数据集大小

def __getitem__(self, index):
#返回指定的索引数据
x = torch.tensor(self.X_Data[index], dtype=torch.float32)
y = torch.tensor(self.Y_Data[index], dtype=torch.float32)
return x, y

X_Data = [[1, 2], [3, 4], [5, 6], [7, 8]] # 输入特征
Y_Data = [1, 0, 1, 0] # 输出特征

dataset = MyDataset(X_Data, Y_Data)

#打印数据
for i in range(len(dataset)):
print(dataset[i])

#(tensor([1., 2.]), tensor(1.))
#(tensor([3., 4.]), tensor(0.))
#(tensor([5., 6.]), tensor(1.))
#(tensor([7., 8.]), tensor(0.))

DataLoader加载数据

DataLoader 用于在pytorch中从 Dataset 中按批次(batch)加载数据

以上面的Dataset为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import torch
from torch.utils.data import Dataset, DataLoader

class MyDataset(Dataset):
def __init__(self, X_Data, Y_Data):
"""
初始化数据 X_data 和 Y_data 是两个列表或数组
X_data: 输入特征
Y_data: 目标标签
"""
self.X_Data = X_Data
self.Y_Data = Y_Data

def __len__(self):
return len(self.X_Data) # 返回数据集大小

def __getitem__(self, index):
#返回指定的索引数据
x = torch.tensor(self.X_Data[index], dtype=torch.float32)
y = torch.tensor(self.Y_Data[index], dtype=torch.float32)
return x, y

X_Data = [[1, 2], [3, 4], [5, 6], [7, 8]] # 输入特征
Y_Data = [1, 0, 1, 0] # 输出特征

dataset = MyDataset(X_Data, Y_Data)

for i in range(len(dataset)):
print(dataset[i])

dataLoader = DataLoader(dataset, batch_size=2, shuffle=True)

for epoch in range(1):
for index, (inputs, labels) in enumerate(dataLoader):
print(f'Batch {index + 1}:')
print(f'Inputs: {inputs}')
print(f'Labels: {labels}')

#(tensor([1., 2.]), tensor(1.))
#(tensor([3., 4.]), tensor(0.))
#(tensor([5., 6.]), tensor(1.))
#(tensor([7., 8.]), tensor(0.))
#Batch 1:
#Inputs: tensor([[1., 2.],
# [5., 6.]])
#Labels: tensor([1., 1.])
#Batch 2:
#Inputs: tensor([[7., 8.],
# [3., 4.]])
#Labels: tensor([0., 0.])
  • batch_size 为每次加载的样本量
  • shuffle 意为洗牌,也就是将数据打乱(通常训练时都需要打乱)
  • drop_last 如果数据集中的样本数不能被 batch_size 整除,设置为 True 时,丢弃最后一个不完整的 batch

每次循环都会返回每个批次与每个批次所包含的输入特征和目标标签

预处理和数据增强

torchvision.transforms 模块来进行常见的图像预处理和增强操作,如旋转、裁剪、归一化等

常见预处理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torchvision.transforms as transforms
from PIL import Image

#定义数据预处理的流水线
transform = transforms.Compose([
transforms.Resize((128, 128)), # 将图像调整为 128x128
transforms.ToTensor(), # 将图像转换为张量
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 标准化
])
image = Image.open('image.jpg')

image_tensor = transform(image)

print(image_tensor.shape)

#torch.Size([3, 128, 128])
  • transforms.Compose():将多个变换操作组合在一起
  • transforms.Resize():调整图像大小
  • transforms.ToTensor():将图像转换为 PyTorch 张量,值会被归一化到 [0, 1] 范围
  • transforms.Normalize():标准化图像数据,通常使用预训练模型时需要进行标准化处理

另外,transforms也提供了图片增强的功能

1
2
3
4
5
6
7
transform = transforms.Compose([
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.RandomRotation(30), # 随机旋转 30 度
transforms.RandomResizedCrop(128), # 随机裁剪并调整为 128x128
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

其中这里的mean和std分别为RGB 三个通道的经验均值(mean)和标准差(std),在Normalize()中分别在R,G,B三种通道中做以下操作:

  • mean = [0.485, 0.456, 0.406] 对应 R、G、B 通道的平均像素值(归一化到 [0,1] 后的均值)
  • std = [0.229, 0.224, 0.225] 对应 R、G、B 通道的标准差

加载图像数据集

对于图像数据集,torchvision.datasets 提供了许多常见数据集(如 CIFAR-10、ImageNet、MNIST 等)以及用于加载图像数据的工具

加载 MNIST 数据集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torchvision.datasets as datasets
import torchvision.transforms as transforms

# 定义预处理操作
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)) # 对灰度图像进行标准化
])

# 下载并加载 MNIST 数据集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

# 创建 DataLoader
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

# 迭代训练数据
for inputs, labels in train_loader:
print(inputs.shape) # 每个批次的输入数据形状
print(labels.shape) # 每个批次的标签形状
  • datasets.MNIST() 会自动下载 MNIST 数据集并加载
  • transform 参数允许我们对数据进行预处理
  • train=Truetrain=False 分别表示训练集和测试集

用多个数据源(Multi-source Dataset)

如果你的数据集由多个文件、多个来源(例如多个图像文件夹)组成,可以通过继承 Dataset 类自定义加载多个数据源

PyTorch 提供了 ConcatDataset 和 ChainDataset 等类来连接多个数据集

例如,假设我们有多个图像文件夹的数据,可以将它们合并为一个数据集

1
2
3
4
5
from torch.utils.data import ConcatDataset

# 假设 dataset1 和 dataset2 是两个 Dataset 对象
combined_dataset = ConcatDataset([dataset1, dataset2])
combined_loader = DataLoader(combined_dataset, batch_size=64, shuffle=True)

线性回归

线性回归是最基本的机器学习算法之一,用于预测一个连续值。

线性回归是一种简单且常见的回归分析方法,目的是通过拟合一个线性函数来预测输出。

对于一个简单的线性回归问题,模型可以表示为:

其中:

  • 是预测值(目标值)
  • 是输入特征
  • 是每个输入特征所对应的权重(待学习的权重)
  • 是偏置

现使用Pytorch来实现一个简单的线性回归

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

# 随机种子,确保每次结果一致
torch.manual_seed(42)

X = torch.randn(100 ,2)
true_w = torch.tensor([2.0, 3.0])
true_b = 4.0 # 偏置
Y = X @ true_w + true_b + torch.randn(100) * 0.1 # 加入一点噪声

print(X[:5])
print(Y[:5])

#tensor([[ 1.9269, 1.4873],
# [ 0.9007, -2.1055],
# [ 0.6784, -1.2345],
# [-0.0431, -1.6047],
# [-0.7521, 1.6487]])
#tensor([12.4460, -0.4663, 1.7666, -0.9357, 7.4781])

其中:

  • @表示的是矩阵乘法运算符,X @ true_w就是对每一行做点积,即X[i, :] @ true_w = X[i, 0] * 2.0 + X[i, 1] * 3.0
  • 噪声:torch.randn(100) * 0.1 生成了 100 个服从 标准正态分布(均值为 0,标准差为 1)的随机数,再乘以 0.1,变成 均值为 0、标准差为 0.1 的小扰动
  • 为什么要加入噪声?因为现实中数据存在测量误差、环境干扰等,如果不加入噪声,模型将完全由 X @ true_w + true_b 来决定,会出现百分百拟合,不符合实际

定义线性回归模型

可以通过继承 nn.Module 来定义一个简单的线性回归模型

在 PyTorch 中,线性回归的核心是 nn.Linear() 层,它会自动处理权重和偏置的初始化

1
2
3
4
5
6
7
8
9
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
self.linear = nn.Linear(2, 1) # 定义线性层,输入两个特征,输出一个特征

def forward(self, x):
return self.linear(x)

model = SimpleNN()

定义损失函数与优化器

线性回归的常见损失函数是 均方误差损失(MSELoss),用于衡量预测值与真实值之间的差异

1
2
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

训练模型

依旧按照前面训练过程,即前向传播 -> 计算损失 -> 反向传播 -> 计算梯度并清空 -> 更新/优化参数

1
2
3
4
5
6
7
8
9
10
11
12
13
epochs = 1000
for epoch in range(epochs):
model.train()
data = model(X)

loss = criterion = (data.squeeze(), Y) # 预测值data需要压缩成1维

optimizer.zero_grad()
loss.backward()
optimizer.step()

if (epoch + 1) % 100 == 0:
print(f'Epoch [{epoch + 1}/1000], Loss: {loss.item():.4f}')

其中:

  • “训练模式”:对于包含某些特定层的模型(如 DropoutBatchNorm 等),PyTorch 需要知道当前是在 训练 还是 推理/评估 阶段
层类型 训练模式 model.train() 评估模式 model.eval()
Dropout 随机“丢弃”一部分神经元(防止过拟合) 不丢弃,所有神经元都参与,但权重会缩放
BathNorm 使用当前 batch 的均值和方差做归一化 使用训练时累计的 全局均值和方差
  • 不过为什么之前的示例中没用到 train() ?因为对于只包含简单线性层的,其实在 train()eval() 下的工作行为完全一样,所以理论上可以不用写(Dropout 默认处于 eval 模式)
  • 为什么需要压缩成一维?因为目标值Y就是一维

评估

1
2
3
4
5
6
7
8
9
10
print(f"Predicted Weight: {model.linear.weight.data.numpy()}")
print(f"Predicted Bias: {model.linear.bias.data.numpy()}")

with torch.no_grad():
predictions = model(X)

plt.scatter(X[:, 0], Y, color='blue', label='True values')
plt.scatter(X[:, 0], predictions, color='red', label='Predictions')
plt.legend()
plt.show()

其中:

model.linear.weight.data.numpy()model.linear.bias.data.numpy() 是什么?

假设你的模型定义如下(这是最常见的情况):

1
model = torch.nn.Linear(2, 1)

那么 model 内部有两个可学习的参数:

  • weight:形状为 (1, 2) 的张量,对应两个输入特征的权重
  • bias:形状为 (1,) 的张量,对应偏置项

各部分含义:

.data 获取张量底层的数据(Tensor),不包含梯度信息
.numpy() 将 PyTorch 张量(CPU 上)转换为 NumPy 数组,便于打印或绘图

with torch.no_grad(): 是什么?

到代码的结尾,模型已经训练完了,现在要进行绘图操作或者对比真实值与预测值,那么就不需要梯度,如果不关闭自动求导(autograd)的话,只会浪费内存(保存中间结果)和浪费时间

总结

以上的整体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import torch
import numpy as np
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt

# 随机种子,确保每次结果一致
torch.manual_seed(42)

X = torch.randn(100 ,2)
true_w = torch.tensor([2.0, 3.0])
true_b = 4.0 # 偏置
Y = X @ true_w + true_b + torch.randn(100) * 0.1 # 加入一点噪声

class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
self.linear = nn.Linear(2, 1)

def forward(self, x):
return self.linear(x)

model = SimpleNN()

criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)


epochs = 1000
for epoch in range(epochs):
model.train()
data = model(X)

loss = criterion(data.squeeze(), Y) # 预测值data需要压缩成1维

optimizer.zero_grad()
loss.backward()
optimizer.step()

if (epoch + 1) % 100 == 0:
print(f'Epoch [{epoch + 1}/1000], Loss: {loss.item():.4f}')

print(f"Predicted Weight: {model.linear.weight.data.numpy()}")
print(f"Predicted Bias: {model.linear.bias.data.numpy()}")

#
with torch.no_grad():
predictions = model(X)

# 绘图
plt.scatter(X[:, 0], Y, color='blue', label='True values')
plt.scatter(X[:, 0], predictions, color='red', label='Predictions')
plt.legend()
plt.show()

结果如下:

卷积神经网络

卷积神经网络 (Convolutional Neural Networks, CNN) 是一类专门用于处理具有网格状拓扑结构数据(如图像)的深度学习模型

在图中,CNN 的输出层给出了三个类别的概率:Donald(0.2)、Goofy(0.1)和Tweety(0.7),这表明网络认为输入图像最有可能是 Tweety

基本结构

输入层(Input Layer)

接收原始图像数据,图像通常被表示为一个三维数组,其中两个维度代表图像的宽度和高度,第三个维度代表颜色通道(例如,RGB图像有三个通道)

卷积层(Convolutional Layer)

用卷积核提取局部特征,如边缘、纹理等,公式如下:

  • :输入图像
  • :卷积核(权重矩阵)
  • :偏置

应用一组可学习的滤波器(或卷积核)在输入图像上进行卷积操作,以提取局部特征

每个滤波器在输入图像上滑动,生成一个特征图(Feature Map),表示滤波器在不同位置的激活

卷积层可以有多个滤波器,每个滤波器生成一个特征图,所有特征图组成一个特征图集合

激活函数(Activation Function)

通常在卷积层之后应用非线性激活函数,如 ReLU(Rectified Linear Unit),以引入非线性特性,使网络能够学习更复杂的模式

池化层(Pooling Layer)

  • 用于降低特征图的空间维度,减少计算量和参数数量,同时保留最重要的特征信息
  • 最常见的池化操作是 最大池化(Max Pooling)平均池化(Average Pooling)
  • 最大池化选择区域内的最大值,而平均池化计算区域内的平均值

归一化层(Normalization Layer, 可选)

例如,局部响应归一化(Local Response Normalization, LRN)批归一化(Batch Normalization)

这些层有助于加速训练过程,提高模型的稳定性

归一化:

其中 是一个极小常数,防止除以0

全连接层(Fully Connected Layer)

在 CNN 的末端,将前面层提取的特征图展平(Flatten)成一维向量,然后输入到全连接层

全连接层的每个神经元都与前一层的所有神经元相连,用于综合特征并进行最终的分类或回归

输出层(Output Layer)

根据任务的不同,输出层可以有不同的形式

对于分类任务,通常使用 Softmax 函数将输出转换为概率分布,表示输入属于各个类别的概率

损失函数(Loss Function)

用于衡量模型预测与真实标签之间的差异

常见的损失函数包括交叉熵损失(Cross-Entropy Loss)用于多分类任务,均方误差(Mean Squared Error, MSE)用于回归任务

优化器(Optimizer)

用于根据损失函数的梯度更新网络的权重。常见的优化器包括随机梯度下降(SGD)、Adam、RMSprop等

正则化(Regularization)

包括 Dropout、L1/L2 正则化等技术,用于防止模型过拟合

这些层可以堆叠形成更深的网络结构,以提高模型的学习能力

CNN 的深度和复杂性可以根据任务的需求进行调整

实例

主要步骤:

  • 数据加载与预处理:使用 torchvision 加载和预处理 MNIST 数据
  • 模型构建:定义卷积层、池化层和全连接层
  • 训练:通过损失函数和优化器进行模型训练
  • 评估:测试集上计算模型的准确率
  • 可视化:展示部分测试样本及其预测结果

数据加载

1
2
3
4
5
6
7
8
9
10
11
transform = transforms.Compose([
transforms.ToTensor(), # 转为张量
transforms.Normalize((0.5,), (0.5,)) # 归一化到 [-1, 1]
])

# 加载 MNIST 数据集
train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transform, download=True)

train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)

定义CNN模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 定于一个CNN
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
# 定义卷积层:输入1通道,输出32通道,卷积核大小3x3
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
# 定义卷积层:输入32通道,输出64通道
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
# 定义全连接层
self.fc1 = nn.Linear(64 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10)

def forward(self, x):
x = F.relu(self.conv1(x)) # 第一层卷积 + ReLU
x = F.max_pool2d(x, 2) # 最大池化
x = F.relu(self.conv2(x)) # 第二层卷积 + ReLU
x = F.max_pool2d(x, 2) # 最大池化
x = x.view(-1, 64 * 7 * 7) # 展平操作 (改变张量形状)
x = F.relu(self.fc1(x)) # 全连接层 + ReLU
x = self.fc2(x) # 全连接层输出
return x

# 创建模型实例
model = SimpleNN()

其中:

  • 卷积核大小(kernel size)对卷积神经网络(CNN)的性能、感受野、参数量和特征提取能力有 显著影响 (现代CNN普遍使用3x3小卷积核)
  • 感受野:卷积核大小决定了 单个输出神经元能看到的输入区域范围大卷积核(如 7×7):感受野大,能捕获更大范围的上下文信息(适合检测大物体或全局结构),小卷积核(如 3×3):感受野小,更关注局部细节(如边缘、角点)
  • 参数量:假设输入通道数为,输出通道数为,卷积核大小为 X ,则参数量为:
  • stride:步长为1,代表卷积核每次在输入图像上移动的像素数,stride=1 表示卷积核每次向右(或向下)移动 1 个像素stride 越大,输出越小,计算量越少,但会 丢失更多空间信息),其输出尺寸公式(高度方向)如下:
  • padding:填充为1,在输入图像的 四周补 0(zero-padding),每边补 padding 个像素,padding=1 表示在 上下左右各补 1 行/列 0
  • stride=1,padding=1:输入为HxW,那么输出仍是HxW

损失函数、优化函数和训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 损失函数和优化器
criterion = nn.CrossEntropyLoss() # 多分类交叉熵损失
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # 学习率和动量

epochs = 5
model.train()
for epoch in range(epochs):
total_loss = 0
for images, labels in train_loader:
outputs = model(images)
loss = criterion(outputs, labels)

optimizer.zero_grad()
loss.backward()
optimizer.step()

total_loss += loss.item()
print(f"Epoch [{epoch+1}/{epochs}], Loss: {total_loss / len(train_loader):.4f}")

测试评估

1
2
3
4
5
6
7
8
9
10
11
12
13
model.eval()  # 设置为评估模式
correct = 0
total = 0

with torch.no_grad(): # 评估时不需要计算梯度
for images, labels in test_loader:
outputs = model(images) # 前向传播,输出形状 [batch_size, 10]
_, predicted = torch.max(outputs, 1) # 取每行最大值的索引(预测类别)
total += labels.size(0) # 累计总样本数
correct += (predicted == labels).sum().item() # 累计正确预测数

accuracy = 100 * correct / total
print(f"Test Accuracy: {accuracy:.2f}%")

其中:

  • (predicted == labels) 返回布尔张量,sum()将True视为1,False视为0,item()将单元素张量转化为python数值

完整代码如下:(可视化部分可以不看)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import matplotlib.pyplot as plt

transform = transforms.Compose([
transforms.ToTensor(), # 转为张量
transforms.Normalize((0.5,), (0.5,)) # 归一化到 [-1, 1]
])

# 加载 MNIST 数据集
train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transform, download=True)

train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)

# 定于一个CNN
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
# 定义卷积层:输入1通道,输出32通道,卷积核大小3x3
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
# 定义卷积层:输入32通道,输出64通道
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
# 定义全连接层
self.fc1 = nn.Linear(64 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10)

def forward(self, x):
x = F.relu(self.conv1(x)) # 第一层卷积 + ReLU
x = F.max_pool2d(x, 2) # 最大池化
x = F.relu(self.conv2(x)) # 第二层卷积 + ReLU
x = F.max_pool2d(x, 2) # 最大池化
x = x.view(-1, 64 * 7 * 7) # 展平操作 (改变张量形状)
x = F.relu(self.fc1(x)) # 全连接层 + ReLU
x = self.fc2(x) # 全连接层输出
return x

# 创建模型实例
model = SimpleNN()

# 损失函数和优化器
criterion = nn.CrossEntropyLoss() # 多分类交叉熵损失
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # 学习率和动量

epochs = 5
model.train()
for epoch in range(epochs):
total_loss = 0
for images, labels in train_loader:
outputs = model(images)
loss = criterion(outputs, labels)

optimizer.zero_grad()
loss.backward()
optimizer.step()

total_loss += loss.item()
print(f"Epoch [{epoch+1}/{epochs}], Loss: {total_loss / len(train_loader):.4f}")

model.eval()
correct = 0
total = 0

with torch.no_grad():
for images, labels in test_loader:
outputs = model(images)
_, predictions = torch.max(outputs, 1)
total += labels.size(0)
correct += (predictions == labels).sum().item()

accuracy = 100 * correct / total
print(f"Test Accuracy: {accuracy:.2f}%")

# 可视化测试结果
dataiter = iter(test_loader)
images, labels = next(dataiter)
outputs = model(images)
_, predictions = torch.max(outputs, 1)

fig, axes = plt.subplots(1, 6, figsize=(12, 4))
for i in range(6):
axes[i].imshow(images[i][0], cmap='gray')
axes[i].set_title(f"Label: {labels[i]}\nPred: {predictions[i]}")
axes[i].axis('off')
plt.show()

循环神经网络

循环神经网络(Recurrent Neural Networks, RNN) 是一类神经网络架构,专门用于处理序列数据,能够捕捉时间序列或有序数据的动态信息,能够处理序列数据,如文本、时间序列或音频

RNN 的关键特性是其能够保持隐状态(hidden state),使得网络能够记住先前时间步的信息

基本结构

在传统的前馈神经网络中,数据是从输入层流向输出层的,而在 RNN 中,数据不仅沿着网络层级流动,还会在每个时间步骤上传播到当前的隐层状态,从而将之前的信息传递到下一个时间步骤

RNN公式如下:

其中:

  • :时间步 的隐状态
  • :时间步 的输入
  • :输入到隐状态的权重矩阵
  • :隐状态到隐状态的权重矩阵(循环连接)
  • :偏置项
  • :激活函数(或者用ReLU)

其输出为:

其中 是模型在时间步 的输出(如词汇表的概率分布)

隐状态:

  • 网络在时间步 对过去所有输入 的“记忆”或“摘要”
  • 它编码了到当前时刻为止的上下文信息
  • 在每个时间步,隐状态都会被更新,并传递给下一个时间步

初始隐状态 h0 通常被初始化为零向量(zero initialization),即:

  • 输入序列(Xt, Xt-1, Xt+1, …):图中的粉色圆圈代表输入序列中的各个元素,如Xt表示当前时间步的输入,Xt-1表示前一个时间步的输入,以此类推
  • 隐藏状态(ht, ht-1, ht+1, …):绿色矩形代表RNN的隐藏状态,它在每个时间步存储有关序列的信息。ht是当前时间步的隐藏状态,ht-1是前一个时间步的隐藏状态
  • 权重矩阵(U, W, V)
    • U:输入到隐藏状态的权重矩阵,用于将输入Xt转换为隐藏状态的一部分
    • W:隐藏状态到隐藏状态的权重矩阵,用于将前一时间步的隐藏状态ht-1转换为当前时间步隐藏状态的一部分
    • V:隐藏状态到输出的权重矩阵,用于将隐藏状态ht转换为输出Yt
  • 输出序列(Yt, Yt-1, Yt+1, …):蓝色圆圈代表RNN在每个时间步的输出,如Yt是当前时间步的输出
  • 循环连接:RNN的特点是隐藏状态的循环连接,这允许网络在处理当前时间步的输入时考虑到之前时间步的信息
  • 展开(Unfold):图中展示了RNN在序列上的展开过程,这有助于理解RNN如何在时间上处理序列数据。在实际的RNN实现中,这些步骤是并行处理的,但在概念上,我们可以将其展开来理解信息是如何流动的
  • 信息流动:信息从输入序列通过权重矩阵U传递到隐藏状态,然后通过权重矩阵W在时间步之间传递,最后通过权重矩阵V从隐藏状态传递到输出序列

模块

PyTorch 提供了几种 RNN 模块,包括:

  • torch.nn.RNN:基本的RNN单元
  • torch.nn.LSTM:长短期记忆单元,能够学习长期依赖关系
  • torch.nn.GRU:门控循环单元,是LSTM的简化版本,但通常更容易训练

使用 RNN 类时,您需要指定输入的维度、隐藏层的维度以及其他一些超参数

实例1

定义RNN模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import numpy as np

class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
# 定义RNN层
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
# 定义全连接层
self.fc = nn.Linear(hidden_size, output_size)

def forward(self, x):
# x: (batch_size, seq_len, input_size)
out, _ = self.rnn(x) # out: (batch_size, seq_len, hidden_size)
# 取序列最后一个时间步的输出作为模型的输出
out = out[:, -1, :] # (batch_size, hidden_size)
out = self.fc(out) # 全连接层
return out

其中:

  • Pytorch中,nn.RNN中输入张量的默认形状是:(seq_len,batch_size,input_size),但很多时候(尤其是在处理实际数据集时),我们习惯把 batch 维度放在最前面(batch_size,seq_len,input_size),所以我们需要设置 batch_first = True,如果不设 batch_first=True,你传入 (32, 10, 5)(32个样本,每条序列长10,每个时间步特征5维)会被误认为是“序列长度=32”,导致错误
  • out, _ = self.rnn(x) 中的 _ 是什么意思?可以查看RNN的返回值,发现返回了一个output(所有时间步的隐状态输出)和一个hidden(最后一个时间步的隐状态),在这个实例中我们只需要output而不需要hidden,就使用 _,表示这个变量我不想用

  • out = out[:, -1, :] 是什么意思?: 表示取 所有 batch 样本-1 表示取 最后一个时间步(即第 seq_len - 1 个位置),: 表示取该时间步的 全部 hidden 特征

训练数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 生成一些随机序列数据
num_samples = 1000
seq_len = 10
input_size = 5
output_size = 2 # 假设二分类问题

# 随机生成输入数据 (batch_size, seq_len, input_size)
X = torch.randn(num_samples, seq_len, input_size) # 生成3维数据,尺寸分别为 bxsxi
# 随机生成目标标签 (batch_size, output_size)
Y = torch.randint(0, output_size, (num_samples,)) # 数据在0~output_size之间,共num_samples列,1行

# 创建数据加载器
dataset = TensorDataset(X, Y)
train_loader = DataLoader(dataset, batch_size=32, shuffle=True)

损失函数,优化器和训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# hidden_size:每个 token 或时间步的特征向量有多少个元素
model = SimpleRNN(input_size=input_size, hidden_size=64, output_size=output_size)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

num_epochs = 10

for epoch in range(num_epochs):
model.train()
total_loss = 0
correct = 0
total = 0

for inputs, labels in train_loader:
outputs = model(inputs)
loss = criterion(outputs, labels)

optimizer.zero_grad()
loss.backward()
optimizer.step()

total_loss += loss.item()

print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {total_loss / len(train_loader):.4f}")

评估模型

1
2
3
4
5
6
7
8
9
10
11
12
13
# 测试模型
model.eval() # 设置模型为评估模式
with torch.no_grad():
total = 0
correct = 0
for inputs, labels in train_loader:
outputs = model(inputs)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

accuracy = 100 * correct / total
print(f"Test Accuracy: {accuracy:.2f}%")

实例2

也可以使用RNN来预测字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import numpy as np

# 数据集:字符序列预测
char_set = list("TechOtakusSaveTheWorld")
char_to_idx = {c: i for i, c in enumerate(char_set)}
idx_to_char = {i: c for i, c in enumerate(char_set)}

input_str = "TechOtakusSaveTheWorld"
target_str = "OtakusSaveTheWorldTech"
input_data = [char_to_idx[c] for c in input_str]
target_data = [char_to_idx[c] for c in target_str]

# 转换为独热编码
input_ont_hot = np.eye(len(char_set))[input_data]

# 转换为张量
inputs = torch.tensor(input_ont_hot, dtype=torch.float32)
targets = torch.tensor(target_data, dtype=torch.long)

# 模型超参数
input_size = len(char_set)
hidden_size = 8
output_size = len(char_set)
num_epochs = 500
lr = 0.1

# 定义RNN模型
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)

def forward(self, x, hidden):
out, hidden = self.rnn(x, hidden)
out = self.fc(out)
return out, hidden

model = SimpleRNN(input_size=input_size, hidden_size=hidden_size, output_size=output_size)

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=lr)

# 训练
losses =[]
hidden = None

for epoch in range(num_epochs):
optimizer.zero_grad()

outputs, hidden = model(inputs.unsqueeze(0), hidden)
hidden = hidden.detach() # 防止梯度爆炸

loss = criterion(outputs.view(-1, output_size), targets)
loss.backward()
optimizer.step()
losses.append(loss.item())

if (epoch + 1) % 20 == 0:
print(f"Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}")

# 测试RNN
with torch.no_grad():
test_hidden = None
test_output, _ = model(inputs.unsqueeze(0), test_hidden)
predicted = torch.argmax(test_output, dim=2).squeeze().numpy()

print("Input sequence: ", ''.join([idx_to_char[i] for i in input_data]))
print("Predicted sequence: ", ''.join([idx_to_char[i] for i in predicted]))

其中:

  • char_to_idx = {c: i for i, c in enumerate(char_set)} 表示构建 字符 → 索引 的字典,比如 'T' → 0, 'e' → 1, 'c' → 2, …, 'd' → 21,后面的 idx_to_char 同理,类似 0 -> 'T'
  • 独热编码:一种将 类别型变量(categorical variable) 转换为 机器学习模型可理解的数值形式 的常用方法,向量中只有一个位置是 1(表示“激活”或“选中”),其余都是 0,“1” 的位置对应原始类别,举个例子,比如说我们有颜色为 ["红","绿","蓝"],那么红的独热编码为 [1,0,0],而绿色是 [0,1,0],以此类推,但实际上这些类别没有大小或顺序关系(称为名义变量)(适用于不能直接处理字符串场景,但是不适用于类别太多的场景)
  • input_ont_hot = np.eye(len(char_set))[input_data] ,这里的 input_data 是整数数组的索引,然后np.eye()的作用就是构造一个NxN的矩阵
  • hidden为什么设置为None?当你传入 None,PyTorch 会根据输入自动创建合适形状的零张量,这是标准做法,尤其在训练开始时没有历史状态
  • 为什么需要 inputs.unsqueeze(0)RNN 要求输入是 3D 张量,但原始 inputs(seq_len, input_size)(2D),unsqueeze(0) 在第 0 维增加一个 batch 维度 → (1, seq_len, input_size)
  • 为什么需要 hidden.detach()?RNN 是按时间步展开的,理论上梯度会从当前 epoch 回传到训练开始,但实际上,我们通常只在当前 batch 内计算梯度,detach()切断 hidden state 的历史计算图,使梯度不会回传到上一个 epoch
  • 为什么需要 view(-1)?因为 criterion *=* nn.CrossEntropyLoss() 要求输入形状为(N,C),但是由上面可知我们为了可以正常进行RNN我们手动添加了一个维度,所以这里给他去除

数据集

PyTorch 提供了强大的数据加载和处理工具,主要包括:

  • torch.utils.data.Dataset:数据集的抽象类,需要自定义并实现 __len__(数据集大小)和 __getitem__(按索引获取样本)
  • torch.utils.data.TensorDataset:基于张量的数据集,适合处理数据-标签对,直接支持批处理和迭代
  • torch.utils.data.DataLoader:封装 Dataset 的迭代器,提供批处理、数据打乱、多线程加载等功能,便于数据输入模型训练
  • torchvision.datasets.ImageFolder:从文件夹加载图像数据,每个子文件夹代表一个类别,适用于图像分类任务

PyTorch 内置数据集

PyTorch 通过 torchvision.datasets 模块提供了许多常用的数据集,例如:

  • MNIST:手写数字图像数据集,用于图像分类任务
  • CIFAR:包含 10 个类别、60000 张 32x32 的彩色图像数据集,用于图像分类任务
  • COCO:通用物体检测、分割、关键点检测数据集,包含超过 330k 个图像和 2.5M 个目标实例的大规模数据集
  • ImageNet:包含超过 1400 万张图像,用于图像分类和物体检测等任务
  • STL-10:包含 100k 张 96x96 的彩色图像数据集,用于图像分类任务
  • Cityscapes:包含 5000 张精细注释的城市街道场景图像,用于语义分割任务
  • SQUAD:用于机器阅读理解任务的数据集

以上数据集可以通过 torchvision.datasets 模块中的函数进行加载,也可以通过自定义的方式加载其他数据集

torchvision 和 torchtext

  • torchvision: 一个图形库,提供了图片数据处理相关的 API 和数据集接口,包括数据集加载函数和常用的图像变换
  • torchtext: 自然语言处理工具包,提供了文本数据处理和建模的工具,包括数据预处理和数据加载的方式

torch.utils.data.Dataset

自定义数据集需要继承 torch.utils.data.Dataset 并重写以下两个方法:

  • __len__:返回数据集的大小。
  • __getitem__:按索引获取一个数据样本及其标签

(比如说本文《数据处理与加载》部分有说明定义Dataset和利用Dataloader来加载数据集,所以这里不再说明一遍)

Dataset 与 DataLoader 的自定义应用

比如说自定义个CSV:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import torch
import pandas as pd
from torch.utils.data import DataLoader, Dataset

class CSVDataset(Dataset):
def __init__(self, file_path):
self.data = pd.read_csv(file_path)

def __len__(self):
return len(self.data)

def __getitem__(self, index):
row = self.data.iloc[index]
# 取当前行除了最后一列的所有元素[:-1]
features = torch.tensor(row.iloc[:-1].to_numpy(), dtype=torch.float32)
# 取最后一行[-1]
label = torch.tensor(row.iloc[-1], dtype=torch.float32)
return features, label

# 实例化数据集和DataLoader
dataset = CSVDataset("xxx.csv")
dataloader = DataLoader(dataset=dataset, batch_size=4, shuffle=True)

for features, label in dataloader:
print("特征:", features)
print("标签:", label)
break

其中:

  • iloc 是 Pandas 库中基于整数位置(integer-location)的索引器,用于通过行号和列号来选择数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pandas as pd
import numpy as np

# 创建示例数据
data = pd.DataFrame({
'A': [1, 2, 3, 4],
'B': [5, 6, 7, 8],
'C': [9, 10, 11, 12]
})

# iloc 的各种用法
print(data.iloc[0]) # 第0行(第一行): [1, 5, 9]
print(data.iloc[-1]) # 最后一行: [4, 8, 12]
print(data.iloc[:, -1]) # 所有行的最后列: [9, 10, 11, 12]
print(data.iloc[:, :-1]) # 所有行,除了最后列:
# A B
# 0 1 5
# 1 2 6
# 2 3 7
# 3 4 8
print(data.iloc[0, 1]) # 第0行第1列: 5

数据转换

为什么需要数据转换?

数据预处理

  • 调整数据格式、大小和范围,使其适合模型输入
  • 例如,图像需要调整为固定大小、张量格式并归一化到 [0,1]

数据增强

  • 在训练时对数据进行变换,以增加多样性
  • 例如,通过随机旋转、翻转和裁剪增加数据样本的变种,避免过拟合

灵活性

  • 通过定义一系列转换操作,可以动态地对数据进行处理,简化数据加载的复杂度

基础变换操作

变换函数名称 描述 实例
transforms.ToTensor() 将PIL图像或NumPy数组转换为PyTorch张量,并自动将像素值归一化到 [0, 1] transform = transforms.ToTensor()
transforms.Normalize(mean, std) 对图像进行标准化,使数据符合零均值和单位方差 transform = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
transforms.Resize(size) 调整图像尺寸,确保输入到网络的图像大小一致 transform = transforms.Resize((256, 256))
transforms.CenterCrop(size) 从图像中心裁剪指定大小的区域 transform = transforms.CenterCrop(224)

1、ToTensor

将 PIL 图像或 NumPy 数组转换为 PyTorch 张量

同时将像素值从 [0, 255] 归一化为 [0, 1]

1
2
3
from torchvision import transforms

transform = transforms.ToTensor()

2、Normalize

对数据进行标准化,使其符合特定的均值和标准差

通常用于图像数据,将其像素值归一化为零均值和单位方差

1
transform = transforms.Normalize(mean=[0.5], std=[0.5])  # 归一化到 [-1, 1]

3、Resize

调整图像的大小

1
transform = transforms.Resize((128, 128))  # 将图像调整为 128x128

4、CenterCrop

从图像中心裁剪指定大小的区域

1
transform = transforms.CenterCrop(128)  # 裁剪 128x128 的区域

数据增强操作

变换函数名称 描述 实例
transforms.RandomHorizontalFlip(p) 随机水平翻转图像 transform = transforms.RandomHorizontalFlip(p=0.5)
transforms.RandomRotation(degrees) 随机旋转图像 transform = transforms.RandomRotation(degrees=45)
transforms.ColorJitter(brightness, contrast, saturation, hue) 调整图像的亮度、对比度、饱和度和色调 transform = transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1)
transforms.RandomCrop(size) 随机裁剪指定大小的区域 transform = transforms.RandomCrop(224)
transforms.RandomResizedCrop(size) 随机裁剪图像并调整到指定大小 transform = transforms.RandomResizedCrop(224)

1、RandomCrop

从图像中随机裁剪指定大小

1
transform = transforms.RandomCrop(128)

2、RandomHorizontalFlip

以一定概率水平翻转图像

1
transform = transforms.RandomHorizontalFlip(p=0.5)  # 50% 概率翻转

3、RandomRotation

随机旋转一定角度

1
transform = transforms.RandomRotation(degrees=30)  # 随机旋转 -30 到 +30 度

4、ColorJitter

随机改变图像的亮度、对比度、饱和度或色调

1
transform = transforms.ColorJitter(brightness=0.5, contrast=0.5)

组合变换

变换函数名称 描述 实例
transforms.Compose() 将多个变换组合在一起,按照顺序依次应用 transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), transforms.Resize((256, 256))])

通过 transforms.Compose 将多个变换组合起来

1
2
3
4
5
6
transform = transforms.Compose([
transforms.Resize((128, 128)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ToTensor(),
transforms.Normalize(mean=[0.5], std=[0.5])
])

自定义转换

如果 transforms 提供的功能无法满足需求,可以通过自定义类或函数实现

1
2
3
4
5
class CustomTransform:
def __call__(self, x):
return x * 2

transform = CustomTransfrom()

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import matplotlib.pyplot as plt
from torchvision import datasets
from torchvision import datasets, transforms


# 原始和增强后的图像可视化
transform_augment = transforms.Compose([
transforms.RandomHorizontalFlip(), # 随机水平翻转图像
transforms.RandomRotation(30), # 随机旋转30°
transforms.ToTensor()
])

# 加载数据集
dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform_augment)

# 显示图像
def show_images(dataset):
fig, axs = plt.subplots(1, 5, figsize=(15, 5))
for i in range(5):
image, label = dataset[i]
axs[i].imshow(image.squeeze(0), cmap='gray') # 将 (1, H, W) 转为 (H, W)
axs[i].set_title(f"Label: {label}")
axs[i].axis('off')
plt.show()

show_images(dataset)

Transform

Transformer 模型由 编码器(Encoder) 和 解码器(Decoder) 两部分组成,每部分都由多层堆叠的相同模块构成

编码器(Encoder)

编码器由 NN 层相同的模块堆叠而成,每层包含两个子层:

  • 多头自注意力机制(Multi-Head Self-Attention):计算输入序列中 每个词与其他词相关性
  • 前馈神经网络(Feed-Forward Neural Network):对每个词进行独立的非线性变换
  • 每个子层后面都接有 残差连接(Residual Connection) 和 层归一化(Layer Normalization)

解码器(Decoder)

解码器也由 NN 层相同的模块堆叠而成,每层包含三个子层:

  • 掩码多头自注意力机制(Masked Multi-Head Self-Attention):计算输出序列中每个词与前面词的相关性(使用掩码防止未来信息泄露)
  • 编码器-解码器注意力机制(Encoder-Decoder Attention):计算 输出序列与输入序列相关性
  • 前馈神经网络(Feed-Forward Neural Network):对每个词进行独立的非线性变换

同样,每个子层后面都接有残差连接和层归一化

在 Transformer 模型出现之前,NLP 领域的主流模型是基于 RNN 的架构,如长短期记忆网络(LSTM)和门控循环单元(GRU)。这些

模型通过顺序处理输入数据来捕捉序列中的依赖关系,但存在以下问题:

  1. 梯度消失问题:长距离依赖关系难以捕捉
  2. 顺序计算的局限性:无法充分利用现代硬件的并行计算能力,训练效率低下

核心思想

自注意力(Self-Attention)

自注意力机制是 Transformer 的核心组件

自注意力机制允许模型在处理序列时,动态地为每个位置分配不同的权重,从而捕捉序列中任意两个位置之间的依赖关系

  • 输入表示:输入序列中的每个词(或标记)通过词嵌入(Embedding)转换为向量表示
  • 注意力权重计算:通过计算查询(Query)、键(Key)和值(Value)之间的点积,得到每个词与其他词的相关性权重
  • 加权求和:使用注意力权重对值(Value)进行加权求和,得到每个词的上下文表示

  • Query (Q):当前词的“询问”向量 —— “我在找什么?”

  • Key (K):其他词的“键”向量 —— “我是什么?”
  • Value (V):其他词的“值”向量 —— “我能提供什么信息?”

公式如下:

其中:

  • 是查询矩阵, 是键矩阵, 是值矩阵
  • 是向量的维度,用于缩放点积,防止梯度爆炸

下面举个例子:

假设输入序列有 3 个词:["I", "love", "AI"]

对 “love” 这个词:

  • 它会分别计算与 “I”、“love”、“AI” 的相关性
  • 如果 “love” 和 “I”、“AI” 相关性高,则输出会融合这三者的信息
  • 最终 “love” 的新表示 = α₁·V(“I”) + α₂·V(“love”) + α₃·V(“AI”)

其中 α₁, α₂, α₃ 是注意力权重(softmax 后的值)

至于相关性怎么来的,这边简单说明就是通过大量训练数据来提高对应词的权重,权重高的相关性就高(更细节的这里不做说明)

多头注意力(Multi-Head Attention)

为了捕捉更丰富的特征,Transformer 使用多头注意力机制。它将输入分成多个子空间,每个子空间独立计算注意力,最后将结果拼接起来

  • 多头注意力的优势:允许模型关注序列中不同的部分,例如语法结构、语义关系等
  • 并行计算:多个注意力头可以并行计算,提高效率

位置编码(Positional Encoding)

由于 Transformer 没有显式的序列信息(如 RNN 中的时间步),位置编码被用来为输入序列中的每个词添加位置信息。通常使用正弦和余弦函数生成位置编码:

其中, 是词的位置, 是维度索引

编码器-解码器架构

和本章一开始的图一样

1
2
3
4
5
6
7
8
9
10
11
12
13
			---------> 前馈神经网络
|
|
Encoder -------------> 多头自注意力 ----------> 输出序列
| |
| |
|--------> Decoder -------------------> 多头自注意力
|
|
----------> 编码器-解码器注意力
|
|
----------> 前馈神经网络

应用

  1. 自然语言处理(NLP)
    • 机器翻译(如 Google Translate)
    • 文本生成(如 GPT 系列模型)
    • 文本分类、问答系统等。
  2. 计算机视觉(CV)
    • 图像分类(如 Vision Transformer)
    • 目标检测、图像生成等。
  3. 多模态任务
    • 结合文本和图像的任务(如 CLIP、DALL-E)

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import torch
import torch.nn as nn
import torch.optim as optim

class TransformerModel(nn.Module):
def __init__(self, input_dim, model_dim, num_heads, num_layers, output_dim):
super(TransformerModel, self).__init__()
self.embedding = nn.Embedding(input_dim, model_dim)
self.positional_encoding = nn.Parameter(torch.zeros(1, 1000, model_dim)) # 假设序列长度最大为1000
self.transformer = nn.Transformer(d_model=model_dim, nhead=num_heads, num_encoder_layers=num_layers)
self.fc = nn.Linear(model_dim, output_dim)

def forward(self, src, tgt):
src_seq_length, tgt_seq_length = src.size(1), tgt.size(1)
src = self.embedding(src) + self.positional_encoding[:, :src_seq_length, :]
tgt = self.embedding(tgt) + self.positional_encoding[:, :tgt_seq_length, :]
transformer_output = self.transformer(src, tgt)
output = self.fc(transformer_output)
return output

# 超参数
input_dim = 10000 # 词汇表大小
model_dim = 512 # 模型维度
num_heads = 8 # 多头注意力头数
num_layers = 6 # 编码器和解码器层数
output_dim = 10000 # 输出维度(通常与词汇表大小相同)

# 初始化模型、损失函数和优化器
model = TransformerModel(input_dim, model_dim, num_heads, num_layers, output_dim)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 假设输入数据
src = torch.randint(0, input_dim, (10, 32)) # (序列长度, 批量大小)
tgt = torch.randint(0, input_dim, (20, 32)) # (序列长度, 批量大小)

# 前向传播
output = model(src, tgt)

# 计算损失
loss = criterion(output.view(-1, output_dim), tgt.view(-1))

# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()

print("Loss:", loss.item())

其中:

  • nn.Embedding:将离散的 词索引(如 0, 1, 2, …)转换为 连续的向量表示(词嵌入)
1
2
3
4
5
6
# 假设 input_dim=1000, model_dim=512
embedding = nn.Embedding(1000, 512)

# 输入一个词索引 [2, 5, 8](代表 "I", "love", "AI")
word_ids = torch.tensor([[2, 5, 8]]) # (batch_size=1, seq_len=3)
embedded = embedding(word_ids) # 输出 (1, 3, 512) 的向量
  • nn.Parameter:将一个普通张量标记为模型的 可训练参数,使其在 model.parameters() 中被自动识别并更新

Pytorch Transformer

(12 封私信 / 80 条消息) Transformer模型详解(图解最完整版) - 知乎

【论文精读】Transformer:Attention Is All You Need | Nlog

Transformer 是现代机器学习中最强大的模型之一

Transformer 模型是一种基于自注意力机制(Self-Attention) 的深度学习架构,它彻底改变了自然语言处理(NLP)领域,并成为现代

深度学习模型(如 BERT、GPT 等)的基础

Transformer 是现代 NLP 领域的核心架构,凭借其强大的长距离依赖建模能力和高效的并行计算优势,在语言翻译和文本摘要等任务中

超越了传统的 长短期记忆 (LSTM) 网络

定义多头注意力

MultiHeadAttention 类封装了 Transformer 模型中常用的多头注意力机制,负责将输入拆分成多个注意力头,对每个注意力头施加注意

力,然后将结果组合起来,这样模型就可以在不同尺度上捕捉输入数据中的各种关系,提高模型的表达能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 自定义Transfromer模块
class MutiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super(MutiHeadAttention, self).__init__()
assert d_model % num_heads == 0, "d_model必须能被num_heads整除"

self.d_model = d_model # 模型维度
self.num_heads = num_heads # 注意力头数
self.d_k = d_model // num_heads # 每个头的维度

# 定义线性变换层(无需偏置)
self.W_q = nn.Linear(d_model, d_model) # 查询变换
self.W_k = nn.Linear(d_model, d_model) # 键变换
self.W_v = nn.Linear(d_model, d_model) # 值变换
self.W_o = nn.Linear(d_model, d_model) # 输出变换

def scale_dot_product_attention(self, Q, K, V, mask=None):
"""
计算缩放点积注意力
输入形状:
Q: (batch_size, num_heads, seq_length, d_k)
K, V: 同Q
输出形状: (batch_size, num_heads, seq_length, d_k)
"""
# 计算注意力分散(Q与K的点积)
# K.transpose(-2, 1)代表矩阵的转置
#原始K: (batch_size, num_heads, seq_length, d_k)
#转置后: (batch_size, num_heads, d_k, seq_length)
attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)

# 应用掩码
if mask is not None:
attn_scores = attn_scores.masked_fill(mask==0, -1e9)

# 计算注意力权重(softmax归一化)
attn_probs = torch.softmax(attn_scores, dim=-1)

# 对值向量加权求和
output = torch.matmul(attn_probs, V)

return output

def split_heads(self, x):
"""
将输入张量分割为多个头
输入形状: (batch_size, seq_length, d_model)
输出形状: (batch_size, num_heads, seq_length, d_k)
"""
batch_size, seq_length, d_model = x.size()
return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)

def combine_heads(self, x):
"""
将多个头的输出合并回原始形状
输入形状: (batch_size, num_heads, seq_length, d_k)
输出形状: (batch_size, seq_length, d_model)
"""
batch_size, _, seq_length, d_k = x.size()
return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)

def forward(self, Q, K, V, mask=None):
"""
前向传播
输入形状: Q/K/V: (batch_size, seq_length, d_model)
输出形状: (batch_size, seq_length, d_model)
"""
# 线性变换并分割多头
Q = self.split_heads(self.W_q(Q)) # (batch, heads, seq_len, d_k)
K = self.split_heads(self.W_k(K))
V = self.split_heads(self.W_v(V))

# 计算注意力
attn_output = self.scale_dot_product_attention(Q, K, V, mask)

# 合并多头并输出变换
output = self.W_o(self.combine_heads(attn_output))
return output

其中:

  • 什么是掩码? 掩码是一个与注意力分数形状相同的二进制张量(通常包含0和1),用于指定哪些位置的注意力应该被忽略或保留,在自回归模型(如Transformer decoder)中,当前位置只能关注到它前面的token,而不会看到未来的信息(掩码会屏蔽掉未来位置)(masked_fill)

假设我们有一个序列 [A, B, C, <pad>, <pad>]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
原始注意力分数矩阵:
[[ 1.2, 0.8, -0.3, 0.1, 0.2],
[ 0.5, 1.5, 0.2, -0.1, 0.3],
[-0.2, 0.4, 1.8, 0.5, -0.2],
[ 0.3, -0.1, 0.6, 0.9, 0.4],
[-0.1, 0.2, -0.3, 0.7, 1.1]]

掩码(1=有效,0=填充):
[[1, 1, 1, 0, 0],
[1, 1, 1, 0, 0],
[1, 1, 1, 0, 0],
[1, 1, 1, 0, 0],
[1, 1, 1, 0, 0]]

应用掩码后:
[[ 1.2, 0.8, -0.3, -1e9, -1e9],
[ 0.5, 1.5, 0.2, -1e9, -1e9],
[-0.2, 0.4, 1.8, -1e9, -1e9],
[ 0.3, -0.1, 0.6, -1e9, -1e9],
[-0.1, 0.2, -0.3, -1e9, -1e9]]

应用softmax后,填充位置的注意力权重会变成≈0,确保模型不会关注到无效位置

  • 为什么需要分割后又合并? 因为这是多头注意力的核心思想。多头注意力的核心思想是:将输入的特征空间分割成多个子空间,每个子空间独立计算注意力,最后合并结果,详情可以看以下步骤,然后就可以倒推combine的步骤了
1
2
3
def split_heads(self, x):
batch_size, seq_length, d_model = x.size()
return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)

输入: (batch_size, seq_length, d_model)

例如: (32, 10, 512) - 32个样本,序列长度10,特征维度512

view重塑: (batch_size, seq_length, num_heads, d_k)

假设 num_heads=8, d_k=64 (因为 8×64=512)

形状变为: (32, 10, 8, 64)

transpose(1, 2): 交换第1和第2维度

形状变为: (32, 8, 10, 64)

意义: 将8个头分离出来,每个头独立处理64维特征

  • 为什么需要 contiguous?但是为什么分割头的时候不需要进行 contiguous当我们对张量进行某些操作时,会 改变其逻辑视图但不改变底层内存布局,导致张量变成 非连续 状态。比如说,转置操作只改变了维度的顺序,但底层内存存储方式没变,变成了 非连续 状态,所以这个时候我们需要进行 contiguous 操作,然后这里我们有一个前提就是 view 操作要求内存一定要是连续的,而装置对于连不连续没有啥要求,所以合并头的时候需要进行 contiguous 操作,而分割的时候不需要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 错误示例
x = torch.randn(32, 8, 10, 64)
x_transposed = x.transpose(1, 2) # 非连续
try:
x_view = x_transposed.view(32, 10, 512) # 会报错!
except RuntimeError as e:
print(f"Error: {e}")
# 错误: view size is not compatible with input tensor's size and stride


#try:
# x_view = x.contiguous().view(32, 10, 512)
# print("success")
#except Exception as e:
# print(f"Error: {e}")
#success

定义前馈网络

1
2
3
4
5
6
7
8
9
10
11
# 前馈神经网络
class PositionWiseFeedForward(nn.Module):
def __init__(self, d_model, d_ff):
super(PositionWiseFeedForward, self).__init__()
self.fc1 = nn.Linear(d_model, d_ff) # 第一层全连接
self.fc2 = nn.Linear(d_ff, d_model) # 第二层全连接
self.relu = nn.ReLU() # 激活函数

def forward(self, x):
# 前馈网络的计算
return self.fc2(self.relu(self.fc1(x)))

位置编码

位置编码用于注入输入序列中每个 token 的位置信息

使用不同频率的正弦和余弦函数来生成位置编码

(具体公式可以见上一章)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 位置编码
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_seq_length):
super(PositionalEncoding, self).__init__()
pe = torch.zeros(max_seq_length, d_model) # 初始化位置编码矩阵
position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * -math.log(10000.0) / d_model)
# 10000^(2i/d_model)
pe[:, 0::2] = torch.sin(position * div_term) # 偶数位置使用正弦函数
pe[:, 1::2] = torch.cos(position * div_term) # 奇数位置使用余弦函数
self.register_buffer('pe', pe.unsqueeze(0)) # 注册为缓冲区

def forward(self, x):
return x + self.pe[:, x.size(1)] # 将位置编码添加到输入中

其中:

  • torch.arange() - 创建等差数列,torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1),创建一个从0开始,步长为1,最大长度为 max_seq_length 的等差数列,并且将形状变成 (max_seq_length, 1)
  • 为什么需要转换成列向量?位置编码需要为序列中的每个位置(0,1,2,…)计算不同的编码值,转换成列向量是为了后续的矩阵乘法运算
  • 为什么使用exp?exp(x * log(y)) = y^x,这是数学上的等价转换,避免直接计算大数的幂次方(数值稳定性),然后这个 - 就是单纯的负号,用于表示为分母
  • register_buffer() - 注册缓冲区,位置编码是预定义的、固定的编码模式,不应该通过学习来改变,需要随模型一起保存,以便推理时使用

构建编码器

编码器层:包含一个自注意力机制和一个前馈网络,每个子层后接残差连接和层归一化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 编码器层
# 编码器层:包含一个自注意力机制和一个前馈网络,每个子层后接残差连接和层归一化
class EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
super(EncoderLayer, self).__init__()
self.self_atten = MutiHeadAttention(d_model, num_heads)
self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model) # 层归一化
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)

def forward(self, x, mask):
# 自注意力机制
atten_output = self.self_atten(x, x, x, mask)
x = self.norm1(x + self.dropout(atten_output)) # 残差链接和层归一化

# 前馈网络
ff_output = self.feed_forward(x)
x = self.norm2(x + self.dropout(ff_output)) # 残差链接和层归一化
return x

其中:

  • Dropout 是一种在训练过程中 随机丢弃 神经网络中一部分神经元的技术,训练时:随机”关闭”一部分神经元(设为0),强制网络不依赖于任何单个神经元,推理时:所有神经元都参与计算,但需要对输出进行缩放
  • 为什么需要进行Dropout?防止过拟合,Dropout通过随机丢弃神经元,强制模型学习更鲁棒的特征

构建解码器

解码器层:包含一个自注意力机制、一个交叉注意力机制和一个前馈网络,每个子层后接残差连接和层归一化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 解码器层
# 解码器层:包含一个自注意力机制、一个交叉注意力机制和一个前馈网络,每个子层后接残差连接和层归一化
class DecoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
super(DecoderLayer, self).__init__()
self.self_atten = MutiHeadAttention(d_model, num_heads)
self.cross_atten = MutiHeadAttention(d_model, num_heads)
self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout) # 丢掉

def forward(self, x, enc_output, src_mask, tgt_mask):
# 自注意力
atten_output = self.self_atten(x, x, x, tgt_mask)
x = self.norm1(x + self.dropout(atten_output)) # 残差链接和层归一化

# 交叉注意力机制
atten_output = self.cross_atten(x, enc_output, enc_output, src_mask)
x = self.norm2(x + self.dropout(atten_output)) # 残差链接和层归一化

# 前馈网络
ff_output = self.feed_forward(x)
x = self.norm3(x + self.dropout(ff_output))

return x

其中:

  • enc_output - 编码器(Encoder)处理完整输入序列后的输出表示 (batch_size, src_seq_length, d_model)
  • src_mask - 用于屏蔽源序列(输入序列)中填充(padding)位置的掩码,形状通常为 (batch_size, 1, src_seq_length)(batch_size, src_seq_length)
  • tgt_mask - 用于屏蔽目标序列(输出序列)中未来位置填充位置的掩码,形状通常为 (batch_size, tgt_seq_length, tgt_seq_length)

示例:

  • 源序列:["I", "love", "you", "<pad>", "<pad>"]
  • 目标序列:["<sos>", "我", "爱", "你", "<eos>"]
1
2
3
4
5
6
7
# 参数对应关系

enc_output = 编码器对 ["I", "love", "you", "<pad>", "<pad>"] 的编码

src_mask = [1, 1, 1, 0, 0] # 屏蔽两个padding

tgt_mask = 因果掩码(不能看到未来) + 填充掩码(不能看到padding) # 确保不能看未来,且屏蔽padding
  • 自注意力:生成”爱”时,只能看到["<sos>", "我"]
  • 交叉注意力:生成”爱”时,可以关注源序列中相关的词(如”love”)

构建Transformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Transformer(nn.Module):
def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout):
super(Transformer, self).__init__()
self.encoder_embedding = nn.Embedding(src_vocab_size, d_model) # 编码器嵌入
self.decoder_embedding = nn.Embedding(tgt_vocab_size, d_model) # 解码器嵌入
self.positional_encoding = PositionalEncoding(d_model, max_seq_length) # 位置编码

# 编码器和解码器层
self.encoder_layers = nn.ModuleList([EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
self.decoder_layers = nn.ModuleList([DecoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])

self.fc = nn.Linear(d_model, tgt_vocab_size) # 最终的全连接层(最终输出阶段)
self.dropout = nn.Dropout(dropout) # 丢弃

def generate_mask(self, src, tgt):
# 源代码:屏蔽填充符(假设填充符索引为0)
# 形状:(batch_size,1,1,seq_length)
src_mask = (src != 0).unsqueeze(1).unsqueeze(2)

# 目标掩码:屏蔽填充符和未来信息
# 形状:(batch_size,1,seq_length,1)
tgt_mask = (tgt != 0).unsqueeze(1).unsqueeze(2)
seq_length = tgt.size(1)

# 生成上三角矩阵掩码,防止解码时看到未来信息
nopeak_mask = (1 - torch.triu(torch.ones(1, seq_length, seq_length), diagonal=1)).bool()
tgt_mask = tgt_mask & nopeak_mask # 合并填充掩码和未来信息掩码
return src_mask, tgt_mask

def forward(self, src, tgt):
# 生成掩码
src_mask, tgt_mask = self.generate_mask(src, tgt)

# 编码器部分
src_embedded = self.dropout(self.positional_encoding(self.decoder_embedding(src)))
enc_output = src_embedded
for enc_layer in self.encoder_layers:
enc_output = enc_layer(enc_output, src_mask)

# 解码器部分
tgt_embedded = self.dropout(self.positional_encoding(self.decoder_embedding(tgt)))
dec_output = tgt_embedded
for dec_layer in self.decoder_layers:
dec_output = dec_layer(dec_output, enc_output, src_mask, tgt_mask)

# 最终输出
output = self.fc(dec_output)
return output

其中:

  • 关注到在编解码器层里有一个循环操作:[EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)]

循环做了什么?

  • for _ in range(num_layers):循环 num_layers 次(比如代码中 num_layers = 6
  • 每次循环都会 创建一个新的 EncoderLayer 实例
  • 最终生成一个包含 6 个独立 EncoderLayer 对象 的列表

为什么需要 nn.ModuleList

  • 管理子模块nn.ModuleList 是 PyTorch 的特殊容器,它会自动将里面的所有层注册为模型的子模块
  • 梯度追踪:确保所有层的参数都能被优化器识别和更新
  • 方便遍历:在 forward 中可以用 for enc_layer in self.encoder_layers: 依次处理

这行代码相当于:

1
2
3
4
5
6
7
8
9
# 当 num_layers = 6 时,等价于:
self.encoder_layers = nn.ModuleList([
EncoderLayer(d_model, num_heads, d_ff, dropout), # 第1层
EncoderLayer(d_model, num_heads, d_ff, dropout), # 第2层
EncoderLayer(d_model, num_heads, d_ff, dropout), # 第3层
EncoderLayer(d_model, num_heads, d_ff, dropout), # 第4层
EncoderLayer(d_model, num_heads, d_ff, dropout), # 第5层
EncoderLayer(d_model, num_heads, d_ff, dropout) # 第6层
])

为什么需要多层?

Transformer 的核心思想就是 深层堆叠(Deep Stacking)

  • 数据会依次经过 6 个编码器层,每层都会提取不同层次的特征
  • 第 1 层可能学习基础语法,第 6 层可能学习复杂语义

注意到 __init__ 里有个最终的全连接层,他的作用域如下图所示:

1
2
3
输入 → 嵌入 → 位置编码 → 编码器层×6 → 解码器层×6 → 全连接层 → 输出

这里就是self.fc
  • 注意到 generate_mask 这里有个 src_mask = (src != 0).unsqueeze(1).unsqueeze(2),这里 (src != 0)判断每个位置是否为填充符(Padding Token),用于创建 填充掩码(Padding Mask),在NLP任务中,不同句子长度不同,需要用 填充符 补到相同长度,这里的 增加维度 是为了 对齐注意力头
  • src != 0 会生成一个布尔掩码
    • True:该位置是真实词(需要关注)
    • False:该位置是填充符(需要忽略)
  • 上三角掩码:防止模型在生成当前词时”偷看”未来的词,对于上面代码中的 nopeak_mask = (1 - torch.triu(torch.ones(1, seq_length, seq_length), diagonal=1)).bool() 解释如下:(diagonal=1 是为了让 第一步先排除主对角线 )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 1. 创建全1矩阵
[[1,1,1,1,1],
[1,1,1,1,1],
[1,1,1,1,1],
[1,1,1,1,1],
[1,1,1,1,1]]

# 2. triu(diagonal=1) 保留对角线右上三角(从对角线上方第1条开始)
[[0,1,1,1,1], # 第0列置0
[0,0,1,1,1], # 第0-1列置0
[0,0,0,1,1], # 第0-2列置0
[0,0,0,0,1], # 第0-3列置0
[0,0,0,0,0]] # 全部置0

# 3. 1 - ... 取反,得到下三角+对角线 (这里对应 1 - torch.triu的部分)
[[1,0,0,0,0],
[1,1,0,0,0],
[1,1,1,0,0],
[1,1,1,1,0],
[1,1,1,1,1]]

# 4. .bool() 转为布尔值
[[True, False, False, False, False],
[True, True, False, False, False],
[True, True, True, False, False],
[True, True, True, True, False],
[True, True, True, True, True]]



列:要关注的词位置
0 1 2 3 4
行:当前词 ┌───────────────────┐
01 0 0 0 0 │ ← 第0个词只能关注自己
11 1 0 0 0 │ ← 第1个词能关注第01个词
21 1 1 0 0 │ ← 第2个词能关注第012个词
31 1 1 1 0 │ ← 第3个词能关注第0123个词
41 1 1 1 1 │ ← 第4个词能关注所有前面的词
└───────────────────┘
  • PS:上三角掩码更清晰易懂,更清晰地表达了不让看未来信息的意图,属于一种习惯,完全可以直接 torch.tril(torch.ones(5, 5), diagonal=0)(直接创造下三角)

训练模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 超参数
src_vocab_size = 5000 # 源词汇表大小
tgt_vocab_size = 5000 # 目标词汇表大小
d_model = 512 # 模型维度
num_heads = 8 # 注意力头数量
num_layers = 6 # 编码器和解码器层数
d_ff = 2048 # 前馈网络内层维度
max_seq_length = 100 # 最大序列长度
dropout = 0.1 # 丢弃概率

# 初始化模型
transformer = Transformer(src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout)

# 生成随机数据
src_data = torch.randint(1, src_vocab_size, (64, max_seq_length)) # 源序列
tgt_data = torch.randint(1, tgt_vocab_size, (64, max_seq_length)) # 目标序列

# 定义损失函数和优化器
creterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.Adam(transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)

# 训练循环
transformer.train()
for epoch in range(100):
optimizer.zero_grad()

# 输入目标序列时去掉最后一个词(用于预测下一个词)
output = transformer(src_data, tgt_data[:, :-1])

# 计算损失时,目标序列从第二个词开始(即预测下一个词)
# output形状: (batch_size, seq_length-1, tgt_vocab_size)
# 目标形状: (batch_size, seq_length-1)
loss = creterion(
output.contiguous().view(-1, tgt_vocab_size),
tgt_data[:, 1:].contiguous().view(-1)
)

loss.backward() # 反向传播
optimizer.step()
print(f"Epoch: {epoch + 1}, Loss: {loss.item()}")

其中:

  • betaseps 都是 Adam 优化器里的超参数,用来控制 梯度一阶/二阶动量的平滑程度数值稳定性,Adam 里会维护两个”滑动平均“
  • 一阶矩(类似带动量的梯度),公式如下:
  • 二阶矩(梯度平方的滑动平均),公式如下:

在这公式里:

  • :当前步的梯度
  • 对应 betas[0] 对应 betas[1]
  • 越接近 1,越“记仇”,历史梯度权重大,更新方向更稳定, 越接近 1,历史信息保留得越久,学习率调整会更平滑、变化更慢
  • 经典 Transformer(Attention is All You Need)里用的就是 betas=(0.9, 0.98)
  • 不推荐将两个值设置的无限接近于1,因为这样会导致历史权重梯度变得很大,效率很低

评估模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 评估模式
transformer.eval()

val_src_data = torch.randint(1, src_vocab_size, (64, max_seq_length))
val_tgt_data = torch.randint(1, tgt_vocab_size, (64, max_seq_length))

# 假设输入为一批英文和对应的中文翻译(已转换为索引)
# 示例数据:
# src_data: [[3, 14, 25, ..., 0, 0], ...] # 英文句子(0为填充符)
# tgt_data: [[5, 20, 36, ..., 0, 0], ...] # 中文翻译(0为填充符)
# 注意:实际应用中需对文本进行分词、编码、填充等预处理

with torch.no_grad():
val_output = transformer(val_src_data, val_tgt_data[:, :-1])
val_loss = creterion(
output.contiguous().view(-1, tgt_vocab_size),
tgt_data[:, 1:].contiguous().view(-1)
)
print(f"Validation Loss: {val_loss.item()}")

最终结果:


模型部署

(感觉现在学了也没啥用,这边先水一下)

PyTorch 模型部署 | 菜鸟教程

部署流程:

训练模型 -> 模型优化 -> 格式转换 -> 部署环境选择 -> 服务封装 -> 性能监控

模型导出格式

PyTorch 主要支持以下导出格式:

格式 特点 适用场景
TorchScript PyTorch原生格式,保持动态图特性 PyTorch生态内部使用
ONNX 开放标准,跨框架兼容 多框架协作环境
Torch-TensorRT NVIDIA优化格式 GPU推理加速

导出为TorchScript

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

# 加载预训练模型
model = torchvision.models.resnet18(pretrained=True)
model.eval()

# 示例输入
example_input = torch.rand(1, 3, 224, 224)

# 方法1: 通过追踪(tracing)导出
traced_script = torch.jit.trace(model, example_input)
traced_script.save("resnet18_traced.pt")

# 方法2: 通过脚本(scripting)导出
scripted_model = torch.jit.script(model)
scripted_model.save("resnet18_scripted.pt")

注意事项

  1. torch.jit.trace 更适合没有控制流的模型
  2. torch.jit.script 能处理包含条件判断等复杂逻辑的模型
  3. 导出前务必调用 model.eval()

模型保存和加载

基本保存和加载方法

保存整个模型

这是最简单的方法,保存模型的架构和参数:

1
2
3
4
5
6
7
8
9
10
11
12
import torch
import torchvision.models as models

# 创建并训练一个模型
model = models.resnet18(pretrained=True)
# ... 训练代码 ...

# 保存整个模型
torch.save(model, 'model.pth')

# 加载整个模型
loaded_model = torch.load('model.pth')

优点

  • 代码简单直观
  • 保存了完整的模型结构

缺点

  • 文件体积较大
  • 对模型类的定义有依赖

仅保存模型参数(推荐方式)

更推荐的方式是只保存模型的状态字典(state_dict):

1
2
3
4
5
6
7
# 保存模型参数
torch.save(model.state_dict(), 'model_weights.pth')

# 加载模型参数
model = models.resnet18() # 必须先创建相同架构的模型
model.load_state_dict(torch.load('model_weights.pth'))
model.eval() # 设置为评估模式

优点

  • 文件更小
  • 更灵活,可以加载到不同架构中
  • 兼容性更好

保存和加载训练状态

除了保存模型的状态字典,也可以保存优化器和损失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 保存检查点
checkpoint = {
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': loss,
# 可以添加其他需要保存的信息
}

torch.save(checkpoint, 'checkpoint.pth')

# 加载检查点
checkpoint = torch.load('checkpoint.pth')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
epoch = checkpoint['epoch']
loss = checkpoint['loss']

model.eval() # 或者 model.train() 取决于你的需求

跨设备加载模型

CPU/GPU兼容性处理

1
2
3
4
5
6
7
8
9
10
11
# 保存时指定map_location
torch.save(model.state_dict(), 'model_weights.pth')

# 加载到CPU(当模型是在GPU上训练时)
device = torch.device('cpu')
model.load_state_dict(torch.load('model_weights.pth', map_location=device))

# 加载到GPU
device = torch.device('cuda')
model.load_state_dict(torch.load('model_weights.pth', map_location=device))
model.to(device)

多GPU训练模型加载

1
2
3
4
5
6
# 保存多GPU模型
torch.save(model.module.state_dict(), 'multigpu_model.pth')

# 加载到单GPU
model = ModelClass()
model.load_state_dict(torch.load('multigpu_model.pth'))

模型转换与兼容性

PyTorch版本兼容性

1
2
# 保存时指定_use_new_zipfile_serialization=True以获得更好的兼容性
torch.save(model.state_dict(), 'model.pth', _use_new_zipfile_serialization=True)

转换为TorchScript

1
2
3
4
5
6
# 将模型转换为TorchScript格式
scripted_model = torch.jit.script(model)
torch.jit.save(scripted_model, 'model_scripted.pt')

# 加载TorchScript模型
loaded_script = torch.jit.load('model_scripted.pt')

最佳实践与常见问题

最佳实践

  1. 命名规范:使用有意义的文件名,如resnet18_epoch50.pth
  2. 定期保存:每隔几个epoch保存一次检查点
  3. 验证加载:保存后立即测试加载功能
  4. 文档记录:记录模型架构和训练参数
  5. 版本控制:将模型文件纳入版本控制系统

常见问题解决方案

问题1Missing key(s) in state_dict

解决:确保模型架构完全匹配,或使用strict=False参数:

1
model.load_state_dict(torch.load('model.pth'), strict=False)

问题2CUDA out of memory

解决:加载时先放到CPU:

1
model.load_state_dict(torch.load('model.pth', map_location='cpu'))

问题3无法加载旧版本模型

解决:尝试在不同PyTorch版本中加载,或转换模型格式


Pytorch 图像分类

图像分类是计算机视觉中最基础的任务之一,其目标是让计算机能够识别图像中的主要内容并将其归类到预定义的类别中。例如,识别一张图片中是猫还是狗

深度学习模型,特别是卷积神经网络(CNN),已成为图像分类任务的主流解决方案。PyTorch作为深度学习框架,提供了构建和训练CNN模型的完整工具链。

项目流程概述

一个完整的图像分类项目通常包含以下步骤:

  1. 数据准备与预处理
  2. 模型构建
  3. 模型训练
  4. 模型评估
  5. 模型应用

很明显,根据这张图,就可以简单写出一个CNN来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import torch.nn.functional as F

transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

# 加载CIFAR-10训练集
trainset = torchvision.datasets.CIFAR10(root="./data", train=True, download=True, transform=transform)

trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2)

# 加载CIFAR-10测试集
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2)

# 定义类别名称
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 卷积层1:输入3通道(RGB),输出6通道,5x5卷积核
self.cnn1 = nn.Conv2d(3, 6, 5)
# 池化层:2x2窗口,步长2
self.pool = nn.MaxPool2d(2, 2)
# 卷积层2:输入6通道,输出16通道,5x5卷积核
self.cnn2 = nn.Conv2d(6, 16, 5)
# 全连接层1:输入16*5*5,输出120
self.fc1 = nn.Linear(16 * 5 * 5, 120)
# 全连接层2:输入120,输出84
self.fc2 = nn.Linear(120, 84)
# 全连接层3:输入84,输出10(对应10个类别)
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
# 第一层卷积+ReLU+池化
x = self.pool(F.relu(self.cnn1(x)))
# 第二层卷积+ReLU+池化
x = self.pool(F.relu(self.cnn2(x)))
# 展平特征图
x = x.view(-1, 16 * 5 * 5)
# 全连接层+ReLU
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
# 输出层
x = self.fc3(x)
return x

net = Net()

# 损失器和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)


net.train()
epochs = 100

for epoch in range(epochs):
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
inputs, labels = data
optimizer.zero_grad()

output = net(inputs)
loss = criterion(output, labels)
loss.backward()
optimizer.step()

running_loss += loss.item()
if i % 2000 == 1999:
print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
running_loss = 0.0

print("Finished Training")

correct = 0
total = 0

with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)

_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

print(f'Accuracy on test images: {100 * correct / total:.2f}%')


class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs, 1)
c = (predicted == labels).squeeze()
for i in range(4):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] += 1

for i in range(10):
print(f'Accuracy of {classes[i]:5s}: {100 * class_correct[i] / class_total[i]:.2f}%')

# 保存模型参数
PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)

最后再进行一个加载:

1
2
3
4
5
6
7
8
# 加载模型
net = Net()
net.load_state_dict(torch.load(PATH))

# 使用模型进行预测
outputs = net(images)
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join(f'{classes[predicted[j]]:5s}' for j in range(4)))

Pytorch 文本情感分析

文本情感分析是自然语言处理(NLP)中的一项基础任务,旨在判断一段文本表达的情感倾向(正面/负面)。本项目将使用PyTorch构建一个深度学习模型,实现对电影评论的情感分类

情感分析的应用场景

  • 产品评论分析
  • 社交媒体舆情监控
  • 客户服务反馈分类
  • 市场趋势预测

所需库:

1
2
3
4
5
6
import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.data import Field, TabularDataset, BucketIterator
import spacy
import numpy as np

安装依赖:

1
2
pip install torch torchtext spacy
python -m spacy download en_core_web_sm

数据准备

数据集介绍

使用IMDB电影评论数据集,包含50,000条带有情感标签(正面/负面)的评论。

数据预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义字段处理
TEXT = Field(tokenize='spacy',
tokenizer_language='en_core_web_sm',
include_lengths=True)
LABEL = Field(sequential=False, use_vocab=False)

# 加载数据集
train_data, test_data = TabularDataset.splits(
path='./data',
train='train.csv',
test='test.csv',
format='csv',
fields=[('text', TEXT), ('label', LABEL)]
)

# 构建词汇表
TEXT.build_vocab(train_data,
max_size=25000,
vectors="glove.6B.100d")

模型构建

LSTM模型架构

img

4.2 模型实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SentimentLSTM(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.lstm = nn.LSTM(embedding_dim,
hidden_dim,
num_layers=n_layers,
bidirectional=True)
self.fc = nn.Linear(hidden_dim * 2, output_dim)
self.dropout = nn.Dropout(0.5)

def forward(self, text, text_lengths):
embedded = self.dropout(self.embedding(text))
packed_embedded = nn.utils.rnn.pack_padded_sequence(
embedded, text_lengths.to('cpu'))
packed_output, (hidden, cell) = self.lstm(packed_embedded)
hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1))
return self.fc(hidden)

模型训练

训练参数设置

1
2
3
4
5
6
7
8
9
10
11
12
13
# 模型参数
INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1
N_LAYERS = 2

# 初始化模型
model = SentimentLSTM(INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, N_LAYERS)

# 优化器和损失函数
optimizer = optim.Adam(model.parameters())
criterion = nn.BCEWithLogitsLoss()

训练循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def train(model, iterator, optimizer, criterion):
epoch_loss = 0
epoch_acc = 0

model.train()

for batch in iterator:
text, text_lengths = batch.text
predictions = model(text, text_lengths).squeeze(1)
loss = criterion(predictions, batch.label)

optimizer.zero_grad()
loss.backward()
optimizer.step()

epoch_loss += loss.item()
epoch_acc += accuracy(predictions, batch.label)

return epoch_loss / len(iterator), epoch_acc / len(iterator)

模型评估

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def evaluate(model, iterator, criterion):
epoch_loss = 0
epoch_acc = 0

model.eval()

with torch.no_grad():
for batch in iterator:
text, text_lengths = batch.text
predictions = model(text, text_lengths).squeeze(1)
loss = criterion(predictions, batch.label)
epoch_loss += loss.item()
epoch_acc += accuracy(predictions, batch.label)

return epoch_loss / len(iterator), epoch_acc / len(iterator)

准确率计算

1
2
3
4
5
def accuracy(preds, y):
rounded_preds = torch.round(torch.sigmoid(preds))
correct = (rounded_preds == y).float()
acc = correct.sum() / len(correct)
return acc

模型应用

预测新文本

1
2
3
4
5
6
7
8
9
def predict_sentiment(model, sentence):
tokenized = [tok.text for tok in nlp.tokenizer(sentence)]
indexed = [TEXT.vocab.stoi[t] for t in tokenized]
length = [len(indexed)]
tensor = torch.LongTensor(indexed).to(device)
tensor = tensor.unsqueeze(1)
length_tensor = torch.LongTensor(length)
prediction = torch.sigmoid(model(tensor, length_tensor))
return prediction.item()

示例预测

1
2
3
4
5
positive_review = "This movie was fantastic! I really enjoyed it."
negative_review = "The film was terrible and boring."

print(f"Positive review score: {predict_sentiment(model, positive_review):.4f}")
print(f"Negative review score: {predict_sentiment(model, negative_review):.4f}")