本篇文章仅作为个人记录用途,不提供任何知识内容正确性的保障。若文章有问题,请联系我并告知,感激不尽。
本篇学习笔记的所有内容都基于https://www.learnpytorch.io/
本文主要内容:PyTorch建立模型的基本流程。
1. 数据
在machine learning中,数据是你能够想象到的任何行。任何形式,例如表格、图片、视频或音频都是数据。
Machine learning有两部分:
- 将你的数据,无论它是什么,转化为数字
- 挑选或构建一个模型,来尽可能的进行学习
但是我们现在没有任何数据,因此我们需要创造一些。现在我们来创建我们的数据,是一条直线。
我们将用linear regression来创建有未知参数的数据,然后用PyTorch来看看是否可以用gradient descent方法进行对这些参数的估计。
import torch
# Create known parameters
weight = 0.7
bias = 0.3
# Create data
start = 0
end = 1
step = 0.02
X = torch.arange(start, end, stop).unsqueeze(dim=1)
y = weight * X + bias
X[:10], y[:10] # check first ten data接下来,我们会构建一个模型,来学习X(features)和y(labels)的关系。
2. 将数据拆分为训练与测试集
在构建模型之前,我们需要将数据进行拆分。机器学习中最重要的一步之一,就是构建训练和测试集(training and test set),有时也有validation set。
这三种数据集有不同的目的:
| 数据集 | 目的 | 数据占比 | 使用频率 |
|---|---|---|---|
| 训练集(training set) | 让模型从数据中学习 | 60-80% | 总是需要 |
| 验证集(validation set) | 让模型根据这些数据进行调整 | 10-20% | 有时需要 |
| 测试集(testing set) | 对模型进行评估 | 10-20% | 总是需要 |
接下来,我们只会用到训练集与测试集,我们可以通过拆分X和y张量来创建他们。
train_split = int(0.8 * len(X)) # 80% of data used for traning set, 20% for testing set
X_train, y_train = X[:train_split], y[:train_split]
X_test, y_test = X[train_split:], y[train_split:]
len(X_train), len(y_train), len(X_test), len(y_test)
# output: (40, 40, 10, 10)我们现在的训练集有40个样本,测试集有10个样本。接下来我们创建一个函数将他们可视化。
import matplotlib.pyplot as plt
def plot_predictions(train_data=X_train,
train_labels=y_train,
test_data=X_test,
test_labels=y_test,
predictions=None):
plt.figure(figsize=(10, 7))
# Plot training data in blue
plt.scatter(train_data, train_labels, c='b', s=4, label='Training data')
# Plot test data in green
plt.scatter(test_data, test_labels, c='g', s=4, label='Testing data')
if predictions is not None:
plt.scatter(test_data, predictions, c='r', s=4, label='Predictions')
# show the legend
plt.legend(prop={'size': 14});plot_predictions()我们就能看到我们的数据是一条直线,训练集为蓝色,测试集为绿色!
3. 创建模型
现在我们有了数据,接下来我们要创建模型,用蓝色的点(训练集)来预测绿色的点(测试集)。
3.1 复习:Python中的OOP
在这一部分,我发现有大量的OOP相关内容,如果没有很好的掌握Python OOP,看起来会非常吃力。而我对于这个知识已经忘记的差不多了,因此就顺便进行复习吧!(这一部分在参考网站中是没有的,但我在这里也仅仅是过一遍)
objects and class
class BankAccount: currency = 'HKD' def __init__(self, customer, account_number, balance=0): self.customer = customer self.account_number = =account_number self.balance = balance在这段代码中我们能看到创建了一个名为
BankAccount的class,并使用def __init__(self, ...)进行初始化。初始化中,定义了很多属性,包括:customer,account_number,balance。
接下来:def deposit(self, amount): if amount > 0: self.balance += amount else: print('Invalid deposit amount:', amount)这里我们定义了一个方法(method),注意定义方法的时候,需要加入
self,但方法中调用属性并不需要在括号中添加,只需要self.attribute,这是因为传入的self这个实例已经带有所有属性,且可修改。- methods and atrributes
在上面的程序中,customer,account_number和balance是属性。deposit是方法。
def check_balance(self):
print('The balance of account number {:d} is {:s}{:.2f}'.format(self.account_number, self.currency, self.balance))这样我们又定义了一个方法,这个方法没有任何输入的值,但调用了实例的属性。
inheritance and polymorphism
一个子类(subclass)可以被如下定义:class SubClass(BaseClass1, BaseClass2, ...):
这里有一个例子:class SavingAccount(BankAccount): def __init__(self, customer, account_number, interest_rate, balance=0): self.interest_rate = interest_rate super().__init__(customer, account_number, balance) def add_interest(self): self.balance *= (1. + self.interest_rate / 100)
在上面的代码中,我们定义了一个BankAccount的子类SavingAccount。
- 子类可以继承父类的所有属性/方法
- 子类可以新增自己的属性/方法
- 子类可以重写(override)父类已有的方法
在上面的代码中:class SavingAccount(BankAccount):就说明,SavingAccount自动自动拥有 currency,customer,account_number,balance,deposit(),check_balance()等方法和属性。def __init__(...)是override,因为这个子类需要一个新的属性interest_rate。self.interest_rate = interest_rate先新增了自己的属性super().__init__(customer, account_number, balance)负责继承父类的三个属性。
def add_interest(self)为新增的方法。
方法重写:子类可以重写父类的方法,使其名称相同,但行为不同。
我觉得到继承这里就差不多了,可以直接开始学习新知识了。关于OOP的更多知识这里就不展开了。
PyTorch构建模型
PyTorch已经有4个必要的模块,你可以用他们来创建任何你可以想想到的神经网络。
他们是:torch.nn, torch.optim, torch.utils.data.Dataset, torch.utils.data.DataLoader
接下来我们将关注于前两个,进行说明。
torch.nntorch.nn包含所有computational graphs所需的模块。torch.nn.Parameter
储存可以用在nn.Module中的tensor。如果requires_grad=True,则会自动计算梯度(gradient),也叫做"autograd"。torch.nn.Module
这是所有神经网络模块的基础类,所有神经网络的构建模块都是其子类。如果你要在PyTorch中构建神经网络,你的模型应该继承nn.Module,需要实现forward()方法。torch.optim
包含很多优化算法,这些算法能够告诉模型的存储在nn.Parameter中的参数如何进行最好的改变,以减少损失。def forward()
所有nn.Module的子类都需要一个forward()方法,该方法定义了对传递给特定nn.Module的数据进行计算。
以上的看起来非常复杂,我在这里是看不懂的。接下来我尝试稍微理解一下:torch.nn包含来创建神经网络所需要的所有模块,这些模块进行组合就能够搭建任意复杂的神经网络。(例如包括线性层nn.Linear,激活函数nn.ReLU等等),因此你需要from torch import nnnn.Parameter是一种特殊的tensor,将tensor用这个进行包装后,PyTorch就知道这个张量是模型的参数,可以在训练中被更新,PyTorch也就能自动帮助他们计算梯度。nn.module当你在建立一个class的时候需要继承的。torch.optim中包含很多优化方法,可以将nn.Parameter中的参数进行优化。forward()会告诉PyTorch如何计算数据,由于mm.Module是通用模版,因此在自己设定的时候需要进行重写(override)。
在这里,torch.nn、torch.optim是模块,nn.Module是类,很容易搞混关系。
可以看看这个关系图(ChatGPT):
torch
├── nn
│ ├── Module ← 所有模型的基类
│ ├── Linear ← 一种层
│ ├── MSELoss ← 一种损失函数
│ └── ...
└── optim
├── SGD ← 优化器
├── Adam
└── ...
如果还没完全理解,接下来我们用一段代码的例子理解以上几个的用法以及关系。
import torch
from torch import nn
# Create linear regression model class
class LinearRegressionModel(nn.Module):
# Almost everything of machine learning is in nn.Module
# So we inherite it.
def __init__(self):
super().__init__() #inherite all attributes and methods from nn
self.weights = nn.Parameter(torch.randn(1,
requires_grad=True,
dtype=torch.float))
# Create a parameter called weights using nn.Parameter
# At first, it is random number, using torch.randn, rather than rand
# This parameter could autgrad and its type is float
self.bias = nn.Parameter(torch.randn(1,
requires_grad=True,
dtype=torch.float))
# Same as weights, create a parameter called bias.
# Forward method to define the computation in the model
def forward(self, x: torch.Tensor) -> torch.Tensor:
# self make forward could use properties in init
# x is input data, and it's tensor
# -> torch.Tensor means that this function return tensor
return self.weights * x + self.bias
# This is the linear regression formula这样,基本上nn.Module, nn.Parameter, forward()的用法以及用途可以有更好的理解。
3.2 查看一个PyTorch模型的内容
接下来我们要创建一个用我们创建的类创建一个模型实例,并且用.parameters()查看其参数!
torch.manual_seed(42)
# Create a instance of the model, subclass of nn.module
model_0 = LinearRegressionModel()
#Check the nn.Parameter(s) within the nn.Module subclass we created
list(model_0.parameters())
# output:
# [Parameter containing:
# tensor([0.3367], requires_grad=True),
# Parameter containing:
# tensor([0.1288], requires_grad=True)].parameters()是继承自nn.Module父类的方法,可以返回模型里所有可训练的参数。list()是Python内置函数,可以讲可迭代对象(iterable)进行转换为列表。
因此list(model.parameters())可以将model这个模型中的所有参数列出。
.parameters()的用处很大,可以向优化器等中传递模型中的参数!
如果我们想要查看模型的状态(state),就可以使用.state_dict()
model_0.state_dict()
# output: OrderedDict([('weights', tensor([0.3367])), ('bias', tensor([0.1288]))]).state_dict()可以让我们看到模型目前的状态,这个方法将状态打包为一个字典(dict)。
这个字典中:key是参数的名字,如weights, bias。value对应着tensor。
用torch.inference_mode()预测
# make predictions with model
with torch.interence_mode():
y_preds = model_0(X_test)看到这个代码,你可能会有很多疑问:
with是什么?.interference_mode()不是一个函数吗?为什么后面能加:?- 在
LinearRegression这个子类中没有定义能传入任何参数,为什么model_0能够传入参数X_test?
接下来,我们就来一个一个解决这些问题。
首先with是Python的上下文管理器(context manager)语法,其作用是:在进入一段代码前执行一些准备操作,在离开这段代码时自动清理或恢复状态。
在这段代码中,torch.inference_mode()关闭了一些操作,如梯度追踪、autograd等,使得前向计算更快,而当缩进代码执行完毕后,这个状态被清理,恢复为原来的状态。
第二个问题也同理,是由于context manager的语法。
最后第三个问题,为何model_0可以传入参数。
这是由于在nn.Module父类中,定义了一个特殊方法__call__(),这个方法能让对象(实例)想函数一样被调用,也就允许向实例中传入参数。
传入的参数X_test,会变成forward()函数的输入!因此可以返回y_preds!
值得注意的是,在nn.Module父类中,方法__call__()的定义为:(以下内容有ChatGPT提供)
# 省略了一些注释与额外逻辑
def __call__(self, *input, **kwargs):
result = self.forward(*input, **kwargs)
return resulttorch.no_grad()的作用与torch.inference_mode()相同。
讲完这三个问题,我们也就可以理解这两行代码是如何运作,进行预测的了。
接下来我们看一看预测的结果:
# Check the predictions
print(f'Numbers of testing samples: {len(X_test)}')
print(f'Numbers of prediction made: {len(y_preds)}')
print(f'Predicted values:\n{y_preds}')Output:
Numbers of testing samples: 10
Numbers of prediction made: 10
Predicted values:
tensor([[0.3982],
[0.4049],
[0.4116],
[0.4184],
[0.4251],
[0.4318],
[0.4386],
[0.4453],
[0.4520],
[0.4588]])接下来可以使用刚才的plot_predictions()函数进行可视化。
这里可以看到,红色的点全部在绿色的下面,可以说是一点都不沾边了,预测的相当差。
我们检查一下与真实值相差多少:
y_test - y_preds
'''
Output:
tensor([[0.4618],
[0.4691],
[0.4764],
[0.4836],
[0.4909],
[0.4982],
[0.5054],
[0.5127],
[0.5200],
[0.5272]])
'''可以说差的相当多了。但别忘记,我们现在还没有开始训练模型!
是时候进行改变了。
4. 训练模型
目前为止,我们的模型用随机的参数来计算预测,因此就是简单的随机猜数字。为了解决这个问题,我们可以不断对参数进行更新。
在这个例子中,我们知道参数weights=0.7, bias=0.3,但是大多数情况下,我们也不知道模型中的理想参数具体是多少。因此,需要写代码让模型自己寻找最佳的参数。
4.1 在PyTorch中创建损失函数(loss function)和优化器(optimizer)
为了让我们的模型能够自动更新参数,我们需要在模型训练中增加两样东西:损失函数与优化器。接下来我们先尝试理解这两个东西具体是什么。
损失函数:衡量你的模型的预测结果(例如y_preds)与真实值(例如y_test)差了多少,差得越少,模型越好。
在torch.nn模块中,PyTorch内置来很多常见的损失函数。
例如:
- MAE:
torch.nn.L1Loss() - MSE:
torch.nn.MSELoss() - BCELoss:
torch.nn.BCELoss()
如果你想找到更多PyTorch中的Loss function,可以看这个网页:https://docs.pytorch.org/docs/stable/nn.html
优化器:告诉你的模型应该如何更新其参数以得到最低的损失。
在torch.optim模块中,有很多内置类型:
- SGD:
torch.optim.SGD() - Adam:
torch.optim.Adam()
如果你想了解更多关于Optimizer的内容,可以看这个网页:https://docs.pytorch.org/docs/stable/optim.html
这里,很容易搞混这几个模块与刚才的nn.Module的层级关系,可以去前面的“建造模型”部分查看。
回到我们的代码。
我们要用MAE(torch.nn.L1Loss())作为我们的损失函数。
MAE是Mean absolute error,测量与真实值的差值的绝对值。
优化器方面,我们要用SGD,torch.optim.SGD(params, lr)
其中:
param是需要优化的模型参数,如weights和bias。
lr是学习率(learning rate),简单来说,学习率决定了优化器在每次更新参数的时候,调整的幅度大大小。
- 如果学习率过高,那么每一步调整太大,虽然学习的快,但有可能跳过最优解,导致无法找到合适的值。
- 如果学习率过小,速度太慢,有可能没学到之前就停滞了。
- 学习率是一个超参数(hyperparameter),是人手动设置的,不是模型学出来的。
- 常见的起始学习率:简单线性回归:
0.01,深度网络:0.001或更低 - 学习率可以通过训练过程动态调整,叫做学习率调度(learning rate scheduling)。
接下来我们直接开始创建损失函数&优化器
# Create a loss function
loss_fn = nn.L1Loss()
# Create a optimizer
optimizer = torch.optim.SGD(params=model_0.parameters(),
lr = 0.01)这里可能会感到疑惑:params不是可以更新的参数吗?为什么会是model_0.parameters()而不是model_0.weights和model_0.bias?
这是因为有时候参数会很多,手动维护太麻烦了。
而我们前文讲到了.parameters()能够返回模型中所有参数!所以在这里会很好用。
注意:nn.L1Loss()和torch.optim.SGD()是两个类,而loss_fn与optimizer是两个实例。
4.2 在PyTorch中创建一个优化循环
接下来我们要创建优化循环(training loop and testing loop),来让模型不停的优化。
优化循环包括:训练虚幻与测试循环。
训练循环包括模型遍历训练数据集,并学习features和labels之间的关系。
测试循环包括遍历模型测试集的数据,以及评估其学的多好。
4.3 创建一个训练循环(training loop)
对于一个训练循环,我们有以下几步需要做:
| 序号 | 名称 | 作用 | 代码 |
|---|---|---|---|
| 1 | forward pass | 模型遍历所有训练数据一次,执行forward()函数计算 | model(x_train) |
| 2 | 计算损失 | 模型的输出,也就是预测,与真实值相对比,并评估相差多少 | loss = loss_fn(y_pred, y_train) |
| 3 | zero gradients | 优化器的梯度设置到0,以便针对特定的训练数据计算 | optimizer.zero_grad() |
| 4 | 对损失函数进行反向传播 | 计算损失函数相对于每个参数(requires_grad=True)的梯度。这被称为反向传播。 | loss.backward() |
| 5 | 更新优化器 | 更新参数 | optimizer.step() |
以上是一个比较简洁的表格,介绍了五个步骤。接下来我详细的讲解一下这五个步骤,以便有一个更深刻的理解:
- Forward pass
做什么:将输入给模型,按照forward()中的定义预测y_pred。model(x_train)经过nn.Module.__call__调用到forward(x_train)
参与计算、且requires_grad=True的张量会被记录到计算图中,供后面backward()使用。 - Calculate loss
做什么:用存是函数比较预测的y_pred与标签(真实值)y_train,对差值进行评估。 - Zero gradients
做什么:把上一轮积累在参数上的梯度清空。因为PyTorch的梯度默认累加,不清零的话会把多次的梯度叠在一起。
调用optimizer这个实例中的zero_grad()方法 - Perform backpropagation on the loss
做什么:依据链式法则,沿着计算图计算所有可训练参数相对于loss的梯度,填写到每个参数的.grad里。
只有requires_grad=True的参数得到梯度。 - Optimizer step (gradient decent)
做什么:优化器读取每个参数的.grad,按照选定算法(SGD)和学习率(lr)更新参数值,使得下次预测更加接近标签。
这一步会修改参数数据本身(param.data),不会更改.grad
现在可能会遇到的问题:
计算图是什么?
loss是什么类型?loss是一个特殊的张量,是一个torch.tensor对象。
梯度是什么?
参数的.grad是什么?存储在哪里?
目前这些问题的大部分回答我都看不懂,所以等以后再进行详细研究。
如果你现在就想彻底理解这些概念,这里有两个视频可能会有帮助:
https://www.youtube.com/embed/Ilg3gGewQ5U?si=Fbgzgf32uiJmDzA8
https://www.youtube.com/embed/IHZwWFHWa-w?si=tCI9FjDGaAvX_tbU
注意:以上的顺序是很灵活的,只需要符合几个规则就可以:
- 在进行backpropagation(
loss.backward())前计算loss(loss = ...) - 在计算损失的梯度(
loss.backward())之前进行梯度清零(optimizer.zero_grad()) - 在backpropagation(
loss.backward())后更新优化器(optimizer.step())
4.4 创建一个测试循环(testing loop)
| 序号 | 名称 | 作用 | 代码 |
|---|---|---|---|
| 1 | forward pass | 模型遍历所有测试数据,使用forward()进行计算 | model(x_test) |
| 2 | 计算损失 | 模型的输出,也就是预测,与真实值相对比,并评估相差多少 | loss = loss_fn(y_pred, y_test) |
| 3 | 计算评估矩阵(optional) | 除了损失值意外,可能还需要计算其他指标,如准确率 | custom functions |
注意:测试循环不包括backpropagation (loss.backward())或更新优化器(optimizer.step()),这是因为在测试循环中,没有参数被改变。
接下来我们将代码运行:
torch.manual_seed(42)
# Set a number of epochs, this is also a hyperparameter
epochs = 100
# Create emply loss list to track values
train_loss_values = []
test_loss_values = []
epoch_count = []
for epoch in range(epochs):
# Training
# Set the model to training mode
model_0.train()
# 1. Forward pass
y_pred = model_0(X_train)
# 2. Calculate the loss
loss = loss_fn(y_pred, y_train)
# 3. Optimizer zero grad
optimizer.zero_grad()
# 4. Perform backpropagation
loss.backward()
# 5. Step the optimizer (gradient descent)
optimizer.step()
# Testing
# Set the model to evaluating mode, turn off things
model_0.eval()
# Truns off gradient tracking & couple of other things
with torch.inference_mode():
# Do the forward pass
test_pred = model_0(X_test)
# Calculate the loss
test_loss = loss_fn(test_pred, y_test.type(torch.float))
if epoch % 10 == 0:
epoch_count.append(epoch)
train_loss_values.append(loss.detach().numpy())
test_loss_values.append(test_loss.detach().numpy())
print(f"Epoch: {epoch} | MAE Train Loss: {loss} | MAE Test Loss: {test_loss}")接下来我尝试解释一下上面的代码中的training loop部分:model_0.train():打开training mode,将所有需要gradient的参数设置为"require gradient",模型需要更新参数。在训练阶段使用。
model_0.eval():打开evaluating mode,关闭所有gradient tracking,模型只做预测。在测试/验证阶段使用。
loss = loss_fn(y_pred, y_train):loss_fn是我们之前定义的损失函数。注意这个函数中,需要先输入预测的结果,再输入目标。
(虽然由于我们用的是绝对值损失函数,因此真种情况下,顺序反了也没关系)
loss.backward()为backpropagation,这一步计算了梯度(gradient)。optimizer.step()进行gradient descent,利用上一步中计算的gradient对参数进行更新。而参数更新后,梯度不会自动清零,而会进行累积。
因此需要optimizer.zero_grad()进行梯度清零。
ChatGPT生成的讲解图:
┌──────────────────────────────────────────────┐
│ ① Forward Pass │
│ model(X_train) → y_pred │
│ 模型进行前向传播,得到预测值。 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ ② Compute Loss │
│ loss = loss_fn(y_pred, y_train) │
│ 比较预测与真实值,计算损失。 │
│ 这一步不会计算梯度,只会建立计算图。 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ ③ Zero Grad │
│ optimizer.zero_grad() │
│ 清空上一次迭代累积的梯度,避免梯度累加。 │
│ 确保接下来计算的梯度只属于当前 batch。 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ ④ Backward Pass │
│ loss.backward() │
│ 反向传播:沿计算图计算所有参数的梯度, │
│ 将 ∂loss/∂param 存入 param.grad。 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ ⑤ Update Parameters │
│ optimizer.step() │
│ 优化器读取 param.grad,并更新参数: │
│ param = param - lr * param.grad │
│ 不会计算梯度,只会修改参数的值。 │
└──────────────────────────────────────────────┘
简单理解:
梯度(gradient):一个曲线的斜率
gradient descent就是让梯度最小(为0)->损失(loss)最小
上面的代码的输出结果大概会是这样:
'''
output:
Epoch: 0 | MAE Train Loss: 0.28983935713768005 | MAE Test Loss: 0.4541231691837311
Epoch: 10 | MAE Train Loss: 0.1746293306350708 | MAE Test Loss: 0.3194132149219513
Epoch: 20 | MAE Train Loss: 0.07638873159885406 | MAE Test Loss: 0.19773726165294647
Epoch: 30 | MAE Train Loss: 0.05069301277399063 | MAE Test Loss: 0.13647659122943878
Epoch: 40 | MAE Train Loss: 0.04463795945048332 | MAE Test Loss: 0.1100495308637619
Epoch: 50 | MAE Train Loss: 0.04098063334822655 | MAE Test Loss: 0.09699545800685883
Epoch: 60 | MAE Train Loss: 0.0375034399330616 | MAE Test Loss: 0.08666229248046875
Epoch: 70 | MAE Train Loss: 0.03407188132405281 | MAE Test Loss: 0.07907666265964508
Epoch: 80 | MAE Train Loss: 0.030638623982667923 | MAE Test Loss: 0.07080408930778503
Epoch: 90 | MAE Train Loss: 0.027199819684028625 | MAE Test Loss: 0.06253156810998917
'''
用可视化图像表示:
# Plot the loss curves
plt.plot(epoch_count, train_loss_values, label='Train loss')
plt.plot(epoch_count, test_loss_values, label='Test loss')
plt.title('Training and test loss curves')
plt.ylabel('Loss')
plt.xlabel('Epochs')
plt.legend();5. 开始预测!(inference)
在上面的过程中,我们编写了优化循环,包括训练循环与测试循环。
我们训练完模型,接下来就需要让他进行预测了。
在我们让模型进行预测(也叫做performing inference)的时候,需要记住3件事情:
- 将模型设置为evaluation mode (
model.eval()) - 用inference mode context manager进行预测(
with torch.inference_mode(): ...) - 所有预测结果应该使用同一个设备上的对象进行(例如,数据与模型仅在GPU或者数据和模型仅在CPU中)
# 1. Set the model to evaluation mode
model_0.eval()
# 2. Set up the inference mode in context manager
with torch.inference_mode():
y_preds_new = model_0(X_test)
y_preds_new这个输出的结果大概是:
tensor([[0.8141],
[0.8256],
[0.8372],
[0.8488],
[0.8603],
[0.8719],
[0.8835],
[0.8950],
[0.9066],
[0.9182]])接下来可视化这些结果:
Plot_predictions(predictions=y_preds_new)我们就可以看到我们现在预测的数据与之前相比已经接近很多了。
6. 保存并加载一个PyTorch模型(save and load)
现在我们已经训练了一个模型,接下来我们要对其进行保存并导出至其他地方。
对于保存和加载一个模型,PyTorch中一共有3种主要方法:
| PyTorch Method | 作用 |
|---|---|
torch.save | 使用Python的pickle工具将序列化的对象保存到磁盘。可以使用torch.save保存模型、tensor以及其他Python对象,如字典 |
torch.load | 使用picle工具的反序列化功能,将序列化的对象文件反序列化并加载到内存中。您还可以设置对象加载到具体哪个设备(CPU/GPU)中。 |
torch.nn.Module.load_state_dict | 使用已经保存的state_dict()对象加载模型的参数字典(model.state_dict()) |
我看到这些描述后的问题就是: pickle是什么?
简单来说,pickle是Python自带的序列化(serialization)模块。它能够将Python中的对象(例如列表、字典,包括模型)保存到文件,再从文件中恢复过来。
6.1 保存一个PyTorch模型的state_dict()
对于保存与加载模型的一个推荐的方法就是通过保存和加载模型的state_dict()。
我们首先来说一下这个过程的步骤:
- 使用Python的
pathlib模块,创建一个字典,叫做models。 - 创建一个文件路径(file path)来保存模型
- 调用
torch.save(obj, f),其中obj是目标模型的state_dict(),f是保存模型的文件名
注:PyTorch保存的模型或对象的文件通常以.pt或.pth结尾,例如saved_model_01.pth。
由于我没有学习过pathlib这个模块,所以看这部分的代码稍显吃力。
from pathlib import Path
# 1. Create model directory
MODEL_PATH = Path("models")
MODEL_PATH.mkdir(parents=True, exist_ok=True)
# 2. Create model save path
MODEL_NAME = '01_pytorch_workflow_model_0.pth'
MODEL_SAVE_PATH = MODEL_PATH / MODEL_NAME
# 3. Save the model state dict
print(f"Saving model to: {MODEL_SAVE_PATH}")
torch.save(obj=model_0.state_dict(), f=MODEL_SAVE_PATH)
# output: Saving model to: models/01_pytorch_workflow_model_0.pth由于我没学过pathlib,因此我接下来借助ChatGPT的力量,解释一下这个代码的内容。
这个代码的整体目标,就是将model_0的参数weights和bias保存到硬盘上,以便以后进行使用,不需要重新训练。
第一步:创建文件保存目录MODEL_PATH = Path(“models”)表示一个路径对象,对应当前文件夹下的models文件夹。MODEL_PATH.mkdir(parents=True, exist_ok=True)表示创建了这个文件夹。
其中:
`parents=True`允许创建多级目录
`exist_ok=True`是如果文件夹已经存在,不报错这两行代码的最终结果就是:在当前项目文件夹下创建了一个models文件夹。
第二步:创建模型保存路径MODEL_NAME = ...:文件名MODEL_SAVE_PATH = MODEL_PATH / MODEL_NAME:用pathlib的/运算符拼接成完整路径
结果:`MODEL_SAVE_PATH = 'models/01_pytorch_workflow_model_0.pth'1st
第三步:将模型参数保存print(f"Saving model to: {MODEL_SAVE_PATH}"):打印保存路径torch.save(obj=model_0.state_dict(), f=MODEL_SAVE_PATH):保存模型参数
其中:
`model.state_dict()`返回一个字典,里面保存了模型参数
`torch.save()`将这个对象(字典)序列化并且保存在文件中,底层调用的是`pickle`模块
最终结果:
models/
└── 01_pytorch_workflow_model_0.pth接下来,可以查看保存的文件路径:
!ls -l models/01_pytorch_workflow_model_0.pth
# output: -rw-r--r-- 1 root root 2117 Oct 31 09:56 models/01_pytorch_workflow_model_0.pth6.2 加载一个已保存的PyTorch模型的state_dict()
现在我们在models/01_pytorch_workflow_model_0.pth已经有一个已保存的模型的参数,接下来我们要用torch.nn.Module.load_state_dict(torch.load(f))进行加载,其中f是我们保存模型参数的文件路径。
为什么要在torch.nn.module.load_state_dict()中调用torch.load()?
因为我们仅仅保存了一个模型的state_dict(),也就是这个模型的参数,而没有保存整个模型。首先,我们需要使用torch.load()加载模型的参数(state_dict()),然后传递到我们模型的新实例中(他是nn.module的子类)。
为什么不直接保存整个模型?
保存整个模型,而不仅仅保存参数(state_dict())显得更加直觉,然而,Pytorch官方并不推荐这样做。这是因为当保存整个模型的时候,PyTorch会将模型的类名、模块的完整导入途径以及各种信息一起打包。这意味着如果模型文件与你当时的代码绑定了,如果你将来想要更改模型类的名称】文件路径等等信息,都会报错。
因此只保存参数字典(state_dict())更加灵活,不依赖任何类名与代码结构。
接下来,我们就用LinearRegressionModel()的另一个实例,来用load_state_dict()导入参数。
# Create a new instance of our model
loaded_model_0 = LinearRegressionModel()
# Load the state_dict of our saved model
loaded_model_0.load_state_dict(torch.load(f=MODEL_SAVE_PATH))
# output: <All keys matched successfully>我们先建立了一个新的实例,然后从MODEL_SAVE_PATH这个途径中导入了参数。
接下来我们用这个新的模型做一些预测(inference),看看是否与旧的模型相同:
loaded_model_0.eval()
with torch.inference_mode():
loaded_y_preds = loaded_model_0(X_test)
loaded_y_preds == y_preds_new
# output:
‘’’tensor([[True],
[True],
[True],
[True],
[True],
[True],
[True],
[True],
[True],
[True]])
‘’’我们可以看到,新模型与旧模型预测的数据是完全相同的,也就说明我们训练后的参数导入了新模型。
7. 总结
我们在这一节已经学习了相当多的内容,包括:
创建数据、拆分数据、创建模型、如何创建参数、如何创建loss function & optimizer,如何创建优化循环,如何用模型进行预测,以及如何将模型保存并加载。
总的来说内容真的相当多,并且某些过程现阶段还无法完全理解其原理。因此还需要记忆以及练习。
接下来总结一些需要注意的点:
- 在拆分数据的时候,如果想拆分成80%, 20%,那么
train_split = int(0.8 * len(X)),注意要转换为整型! - 在创建损失函数的时候多使用的函数属于
nn,而不是torch!因此要写nn.L1Loss()。 - 在创建优化器的时候,所传递的参数有:要修改的参数,也就是
modol.parameters(),注意parameters()要小写! - 在定义模型子类的时候,
slef.weight = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float)),requires_grad等要写在randn()括号里面,不要写在外面。
最后在给一下练习的答案,其实就是改了两个数字,但修改自己不看笔记重新写一遍,毕竟容易出问题的地方真是太多了。
import torch
from torch import nn
import matplotlib.pyplot as plt
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"Device: {device}")
import matplotlib.pyplot as plt
def plot_predictions(train_data=X_train,
train_labels=y_train,
test_data=X_test,
test_labels=y_test,
predictions=None):
plt.figure(figsize=(10, 7))
# Plot training data in blue
plt.scatter(train_data, train_labels, c='b', s=4, label='Training data')
# Plot test data in green
plt.scatter(test_data, test_labels, c='g', s=4, label='Testing data')
if predictions is not None:
plt.scatter(test_data, predictions, c='r', s=4, label='Predictions')
# show the legend
plt.legend(prop={'size': 14});
weight = 0.3
bias = 0.9
start = 0
end = 1
step = 0.01
X = torch.tensor(torch.arange(start, end, step)).unsqueeze(dim=1)
y = X * weight + bias
train_split = int(0.8 * len(X))
X_train = X[:train_split]
y_train = y[:train_split]
X_test = X[train_split:]
y_test = y[train_split:]
class LinearRegression(nn.Module):
def __init__(self):
super().__init__()
self.weight = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))
self.bias = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))
def forward(self, x):
return x * self.weight + self.bias
model = LinearRegression()
L_fn = nn.L1Loss()
optimizer = torch.optim.SGD(params=model.parameters(), lr=0.001)
epochs = 10000
for i in range(epochs):
model.train()
y_pred_train = model(X_train)
loss = L_fn(y_pred_train, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
model.eval()
with torch.inference_mode():
y_pred_test = model(X_test)
loss = L_fn(y_pred_test, y_test)
if i % 1000 == 0:
print(f"weight: {model.weight}, bias: {model.bias}, loss: {loss}")
model.eval()
with torch.inference_mode():
y_prediction = model(X_test)
plot_predictions(predictions=y_prediction)代码本身应该没什么问题,但plot_predictions函数定义部分,直接复制的话可能有缩进格式问题,需要手动修改。



