Pytorch(持续更新ing)

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

开始

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)

张量(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)相加,然后通过激活函数处理以产生输出

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

其中w表示权重,x表示输入,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}')

第一个神经网络

实例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我们手动添加了一个维度,所以这里给他去除