人工智能技术(一):基础深度学习模型
2024-11-30 13:30:33

本文主要讲述如何用pytorch编写基础的深度学习架构,并尝试用基础的架构拟合一个加法函数。

程序代码段

基本的代码段如下:

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
import torch
from torch.utils.data import Dataset, DataLoader

epochs = 200
lr = 1e-2

class BasicPlus(torch.nn.Module):
def __init__(self):
super().__init__()
self.l1 = torch.nn.Linear(2, 1)

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

class PlusDataset(Dataset):
def __init__(self,
data_num: 2000,
):
self.data_num = data_num
data_list = []
for i in range(data_num):
x = torch.rand(2)
y = torch.tensor([x[0] + x[1]])
data_list.append((x, y))
self.data_list = data_list

def __len__(self):
return self.data_num

def __getitem__(self, index):
return self.data_list[index]


if __name__ == "__main__":
# Setup datasets and dataloaders
train_dataset, val_dataset, eval_dataset = PlusDataset(700), PlusDataset(100), PlusDataset(200)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32)
eval_loader = DataLoader(eval_dataset, batch_size=32)

# Setup model and training hyperparameters
my_model = BasicPlus()
loss_fn = torch.nn.MSELoss()
optimizer = torch.optim.SGD(my_model.parameters(), lr=lr)

for epoch in range(epochs):
loss_value = 0.0
# Train
for batch_idx, (x, y) in enumerate(train_loader):
# Compute prediction and loss
pred = my_model(x)
loss = loss_fn(pred, y)

# Accumulate Loss for Visualization
loss_value += loss.item()

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

print(f"Epoch: {epoch + 1}, Loss: {loss_value / len(train_loader)}")

# Validate
with torch.no_grad():
val_loss_value = 0.0
for x, y in val_loader:
pred = my_model(x)
loss = loss_fn(pred, y)
val_loss_value += loss.item()
print(f"Validation Loss: {val_loss_value / len(val_loader)}")
# Eval
with torch.no_grad():
eval_loss_value = 0.0
for x, y in eval_loader:
pred = my_model(x)
loss = loss_fn(pred, y)
eval_loss_value += loss.item()
print(f"Eval Loss: {eval_loss_value / len(eval_loader)}")

# Visualize the result
print(my_model(torch.tensor([1.0, 1.0])))
print(my_model(torch.tensor([2.0, 2.0])))

步骤说明

这是一个用pytorch搭建的基础的深度学习代码框架。所有的深度学习流程,都遵循着这样的步骤:

  1. 数据预处理。
  2. 模型加载。
  3. 训练流程的编写。
  4. 模型的测试与保存。

预备知识:张量

pytorch中,最基本的数据是tensor——张量。pytorch中的张量与其他领域不同,您可以将之理解为标量、矩阵以及矩阵之矩阵的集合。比如:

1
2
3
4
torch.tensor(1) # 这是一个标量式的张量,可以看作一个数字(标量),无形状可言
torch.tensor([1,2]) # 这是一个矩阵式的张量,可以看作一个一行二列的矩阵,形状为3
torch.tensor([[1,2,3], [1,2,3]]) # 这是一个矩阵式的张量,可以看作一个二行三列的矩阵,形状为2x3
torch.tensor([[[1,2,3], [1,2,3]]]) # 这是一个矩阵之矩阵,可以看作一个一行一列的矩阵,成员是一个二行三列的矩阵,形状为1x2x3

您可以通过Tensor.shape来查看一个张量的形状。

也可以将其理解为,标量、列表、等长列表之列表的集合。这是从数据组织的角度,而非计算角度来理解张量。

数据预处理

深度学习的数据通常按照如下的方法组织:(输入,预期输出)。数据集的质量直接影响着模型的表现。

作为示例,我们所使用的数据集是代码生成的,在应用pytorch编码时,我们首先会将以其他形式组织的数据集,变化为torch.utils.data.Dataset的子类,这个类是将各种各样数据集做规范化的一个类。

作为子类,我们通常要重写这样三个方法:__init__()__len__()__getitem__()

__init__()是构造器,我们通常在这个方法中对数据做一些预处理,将数据变为(输入,预期输出)这样的格式,并将全部数据转化为可以迭代的结构,便于后续的迭代操作。在上述的示例代码中,我们在这个方法里完成了:导入数据、将数据组织为列表这两个功能。

__len__()方法,需要返回数据集的大小。

__getitem__()方法,则需要完成这样的功能:给定索引,返回数据集中对应的子数据。

数据预处理的关键在于预处理,通常,在代码编写完毕后,我们要print一些处理后的数据,观察它们的数据、形状是否与预期相符。

在加载数据为Dataset之后,我们还需要一个DataLoader来将Dataset进一步处理。DataLoader能够完成包括但不限于这样几个工作:对数据集进行分组、打乱数据集顺序等。在实际的训练过程中,如果从Dataset里,每次挑出一个来训练,会很慢,所以训练通常是按照批次(batch)来训练的,DataLoader就完成了这样一个工作。

模型加载:构造器

我们通常会借助torch.nn.Module,实现我们的模型。

我们通常会重写它的两个方法:__init__()forward()

在构造器__init__()中,我们会声明模型的架构,即内部包含哪些层。在我们的示例代码中,我们在构造器里定义了一个线性层(Linear Layer),所谓线性层,实际上功能和这样一个函数类似:
$$
\mathbf{y}=\mathbf{Wx}+\mathbf{b}
$$

torch.nn.Linear(2,1)的意思就是,声明上面公式中的$\mathbf{W}$是一个2$\times$1的矩阵。这个2和1代表的是in_featuresout_features。所谓feature就是输入的特征的数量,如果一个数据,能被表征成torch.tensor([1,2,3])的形式,其特征有3个。

不过,和线性代数中不同的是,线性层可以对多个数据同时做操作。还记得我们之前说过的DataLoader吗,我们通常用模型+DataLoader的形式进行训练。两个数据组织成torch.tensor([[1,2,3], [1,2,3]])的形式,那么,线性层可以对这两个数据同时处理:

1
2
3
test_layer = torch.nn.Linear(3,1)
test_layer(torch.tensor([1,2,3], dtype=torch.float)) # 返回tensor([-1.1655], grad_fn=<ViewBackward0>)
test_layer(torch.tensor([[1,2,3], [4,5,6]], dtype=torch.float)) # 返回tensor([[-1.1655],[-3.2795]], grad_fn=<AddmmBackward0>)

如果您本地的结果和我不一样,无需担心,因为每次线性层初始化,其权重都会随机生成。

模型加载:前向传播

我们来讨论一下forward()方法。这个方法负责前向传播。前向传播过程,实际上就是推理过程。您需要在这个方法中编写处理输入的逻辑。依靠在__init__()中预先设置好的层,对输入一步步进行处理。我们的示例代码很简单,其forward()方法实现了这样一个功能,对于输入,经过线性层处理,然后输出。

训练流程编写

到目前为止,我们加载好了数据集,也编写了模型架构,让我们开始编写训练代码!

训练流程通常为:

  1. 初始化数据与模型。

  2. 迭代数据。

  3. 依靠模型,生成对输入的预测值。

  4. 计算预测值与预期的误差。

  5. 依靠优化器对模型权重进行优化、更新。

第零步:初始化

这里我们主要谈DataLoader的设置:

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

这里的batch_size是批次的大小,即模型同时对多少个数据进行处理,而shuffle参数则表示我们要对数据集进行打乱,我们通常对训练集进行打乱,原因见附录

第一步:迭代数据

我们有时会将DataLoader组织成一个枚举类,即:

1
for batch_idx, (x, y) in enumerate(train_loader):

因为有时,我们需要获取数据的下标信息。

第二步:生成预测

由于模型通常已经写好了__call__()方法,直接用model(x)类似的代码段即可。

第三步:计算误差

我们会预先设置一些损失函数,来计算误差。损失函数的选择通常是任务敏感的。比如分类任务常用交叉熵损失,预测任务常用均方误差损失。我们想实现一个加法器,那么就是一个预测任务,选用均方误差比较好,其公式为:
$$
MSE(T)=E((T-\theta)^2)=\frac{1}{n}\sum_{i=1}^n(y_i-\hat{y}_i)^2
$$
选用L1误差函数等也都可以。

第四步:优化

优化器是一个根据loss计算梯度,优化模型权重的模块。

为什么要优化?当然是为了loss最低。这是一个极其显然的想法。此外,我们也知道,现在可以改变的东西,只有模型的权重,所以我们的任务变成了:如何寻找到权重-损失函数的鞍点,横轴是权重,纵轴是损失。怎么找?梯度下降法。

假设我们的权重只有一个,而损失函数恰好满足$y=x^2$这个函数:

那么我们作这样一张图:

假设我们现在的函数在黑橙交点(这个点也是函数线上的点),为了优化,我们需要左移,左移的步长是橘红交点左侧红线的长度,这就引出了我们第一个公式:
$$
\exist \lambda, F(x-\lambda\nabla F(x_0))<F(x_0)
$$
其中,$x_0$就是我们的黑橙交点。这个$\lambda$就是学习率(learning rate,代码中的$lr$)。

优化器会根据学习率的大小,寻找到一个最优秀的参数设置。不过我们也能看出来,如果学习率太大,移动的步长就会很大,这会导致参数设置在极值点的两侧来回震荡;而学习率太小,移动的步长太小,这会导致训练很慢。

代码中,我们首先要将优化器的梯度设置为0,然后计算loss对于参数的梯度,最后应用优化器进行优化,这便是:

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

三行的作用。

测试流程编写

测试的时候,我们不需要模型更新参数,所以需要指定:

1
with torch.no_grad()

这表示,我们不需要梯度计算,因为梯度计算只和参数的优化有关,关闭它可以加快推理过程。

测试过程中,我们只需要让模型前向传播,然后得出loss即可。我们通常使用平均损失来观察模型的效果。

视觉化(Visualization)的重要性

视觉化是指将训练过程中的某些参数显示出来,这是很重要的。很多运算都是计算机自动完成的,不会显示出来,这不利于我们观测训练的过程。

常见的视觉化方法有这么几个:对于训练过程,可以应用tqdm设置进度条,观测训练的耗时;对于训练效果,可以设置训练几个epoch后,在验证集上进行推理,观察效果……

总结

基础深度学习模型的代码编写,有这样几个重点:

  1. 数据的预处理:将数据进行预先处理,并转化为可以迭代的结构。
  2. 模型的编写:模型内部包含哪些模块?前向传播该怎么做?
  3. 训练过程的编写:正确选择损失函数、优化器,设置参数。
  4. 视觉化:设置合适的视觉化方式,便于用户观测训练过程。

附录

训练集、验证集、测试集

训练集是用于训练、优化模型参数的集合;验证集是在训练过程中,用于推理,检测模型临时训练效果的集合;测试集则是在训练后,单纯用于推理,检测模型训练效果的集合。

Shuffle的必要性

假设我们的权重-损失函数有三个鞍点,其中只有一个是最低点。如果不shuffle,假设第一个epoch训练完毕,模型权重到达了一个局部最低点,而非极低点,那么后续的训练有可能无法跳出这个点。为了避免这个,我们通常会打乱数据集,创造更多的数据组合。

上一页
2024-11-30 13:30:33