PyTorch for Deep Learning & Machine Learning – Full Course(2) - NoTe0x007
原链接:
https://www.youtube.com/watch?v=V_xro1bcAuA
写在前面
这一系列是我在学习他人的优质课程(博客文章、视频课程等)时所做的学习笔记,根据我自身的水平来进行学习,同时会进行思维的发散,补充原文中没有提到或者有错误的地方。同时也会进行经常性的更新和整理,让它和我的当下的状态更加契合。
这期是pytorch深度学习的第二期,是视频的Chapter 1部分,也就是Pytorch的工作流程。上一期大概看了一周,看视频还是太慢了,所以这期就直接看他的笔记,我认为会快很多。
Introduction to PyTorch Workflow
通常来讲,一个典型的Pytorch工作流程是这样的
- 准备和加载数据
- 构建模型
- 让模型适配数据(训练)
- 预测与模型评估(推理)
- 模型保存与加载
- 全流程整合
首先先加载我们这一章要用的环境。
1 2 3 4 5 6
| import torch from torch import nn import matplotlib.pyplot as plt
torch.__version__
|
示例:创建线性回归模型
Creating a dataset with linear regression
在深度学习中,数据基本上可以是任何事情,例如
- excel表格
- 任何种类的图片
- 视频
- 广播或音乐等音频
- DNA
- 文本
深度学习是这两方面的游戏:
- 把数据变成数学表示。
- 建立一个模型来学习数学表示的模式。
为了演示这个过程,作者利用线性回归生成一些已知数据,使用线性回归公式,构造一条具有已知参数的直线,直线方程为$y = 0.7x + 0.3$。
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
| weight = 0.7 bias = 0.3
start = 0 end = 1 step = 0.02 X = torch.arange(start, end, step).unsqueeze(dim=1) y = weight * X + bias X[:10], y[:10]
len(X), len(y)
|
Creating training and test sets (the most important concept in ML)
在深度学习中,最重要的事情就是创建训练集和测试集。现在让我们尝试在这个已知的线性回归上创建训练集和测试集。我们从X和Y中分别拿出前80%做训练集,拿后20%做测试集(即40和10)。
1 2 3 4
| train_split = int(0.8 * len(X)) 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)
|
为了方便查看这些数据,最重要的就是进行可视化。这个时候就轮到我们的matplotlab出场了,让我们定义一个函数然后调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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)) plt.scatter(train_data, train_labels, c="b", s=4, label="Training data") plt.scatter(test_data, test_labels, c="g", s=4, label="测试集数据") if predictions is not None: plt.scatter(test_data, predictions, c="r", s=4, label="模型预测") plt.legend(prop={"size": 14}) plot_predictions()
|
图像看起来就长这样。
真是笔直的一条线啊
Creating our first PyTorch model
接下来我们就可以开始构建模型了。这个课程将全成使用类来构建模型,所以视频作者让我们先去学习python的面向对象编程。还好我的基本功足够扎实,可以直接来看代码,不需要看python的OOP。
作者不止一次强调了模型的工作原理:从随机初始值(在这个线性回归中就是模型的两个参数Weight和Bias)开始,观察训练数据,不断调整这些随机值,使其逼近理想值(即我们最开始给的真实值)。
他是如何做到的呢?主要依赖两个核心算法:梯度下降和反向传播。同时视频作者也给出了我们两个视频链接,讲解这两个算法。正好我对这两个算法并不是很熟悉,我可以把这两个视频拿过来看。
梯度下降(这个还是3b1b的视频呢)本质上就是不断接近一个函数的最小值,找到最速下降的地方,也就是梯度的负值。这个函数便是查看输出是否符合预期的成本函数(输入就是每个神经元所需要的那些权重和最后的偏差值)。拿手写识别来举例,如果说答案是什么数字都有,那这个成本函数就很高,而如果答案只给了一个数字很高的概率,那就说明这个成本很低,是对的。
那么如何快速的计算梯度呢?就需要反向传播算法了。还是拿手写数字识别来说,当我们需要代表2的那个神经元变高的时候,我们就反推回去,推高导出它结果的权重或偏置,或者根据上一层已经很高的神经元直接去改进而更改它的值也说不定。
以我的数学能力,我就能掌握这么多,接下来的问题就不是我所能操心的了。我们还是来看看怎么训练吧。这里面我们定义一个LinearRegressionModel类,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class LinearRegressionModel(nn.Module): def __init__(self): super().__init__() self.weights = 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: torch.Tensor) -> torch.Tensor: return self.weights * x + self.bias
|
本质上来讲,前向传播就是在定义如何从x到y,而我们需要管的权重变量也只有两个,即weights和bias。
Discussing important model building classes
构建Pytorch模型需要很多重要的类,例如:
torch.nn,包含构建计算图的所有组件(神经网络本质上就是一种计算图)。
torch.nn.Parameter,定义模型需要学习的参数,通常我们可以直接使用torch.nn中现成的层,这些层会自动为我们设置好这些参数。
torch.nn.Module,所有神经网络模块的基类,如果你继承这个类,必须重写forward()方法,该方法定义了反向传播的具体逻辑。
torch.optim,Pytorch中优化器的所在地,它们负责协助执行梯度下降。
同时视频作者也给出了一个速查网页。
Checking out the internals of our model
我们已经定义好了模型,接下来就可以创建了,实例化LinearRegressionModel,然后使用parameters()方法来查看模型的参数,或者用state_dict方法来查看名称和值一一对应的参数字典。
1 2 3 4 5 6 7 8 9
| torch.manual_seed(42) model_0 = LinearRegressionModel() list(model_0.parameters())
model_0.state_dict()
|
Making predictions with our model
为了检验模型的预测能力,我们来看看模型基于X_test预测y_test的效果如何。当我们向模型直接传入数据时,模型会对数据执行forward方法。
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
| y_preds = model_0(X_test) y_preds
with torch.inference_mode(): y_preds = model_0(X_test) y_preds
with torch.no_grad(): y_preds = model_0(X_test) y_preds
|
这三种不同的调用方式,决定了是否有梯度追踪、计算图构建,并且决定了内存的大小。
- 直接调用,它会记录
forward中的所有操作,构建一个计算图(即输出的grad_fn=<AddBackward0>),以便后续调用.backward()进行反向传播来计算梯度。不过这样会浪费大量的内存,推荐在训练阶段使用,因为此时你可能需要计算梯度来更新权重。
- 使用
torch.no_grad,输出的张量没有grad_fn,节省了梯度追踪带来的开销,但是依旧可能构建了计算图,最好是在旧版代码或者兼容模式下使用,目前已经不推荐使用了。
- 使用
torch.inference_mode,这是专门为推理设计的模式,不仅关闭了梯度追踪,同时完全禁止构建计算图。是目前官方推荐的最佳实践。
接下来,我们把已经计算完的预期值与之前的数据放在一起进行比较并可视化。
1
| plot_predictions(predictions=y_preds)
|
红色点就是预测的值,似乎和那些数据偏差都很大
Training a model with PyTorch (intuition building)
训练的核心思想,就是让模型从一组未知的参数(随机初始化)逐步演化为一组已知的参数。衡量模型的偏差程度(有多差)的常用方法是使用损失函数。进行训练所使用的核心组件包括:
- 损失函数,用于衡量模型预测值与真实理想值之间差距的函数,数值越低越好。
- 优化器(optimizer),根据模型损失调整模型参数(这个模型中就是
Weight和bias)以降低损失。
配置优化器时,通常需要设置两个关键参数。
params,指定需要优化的模型参数,例如params=model_0.parameters()。
lr,学习率,这是一个超参数,定义了优化器每一步调整参数的幅度。lr越大,调整幅度就越大。
在Pytorch中,我们还需要实现训练循环和测试循环。
Setting up a loss function and optimizer
1 2 3 4 5 6 7
| loss_fn = nn.L1Loss()
optimizer = torch.optim.SGD(params=model_0.parameters(), lr=0.01)
|
那么应该选择哪种损失函数和优化器呢?这取决于具体问题,随着经验的积累我们会掌握针对特定问题集,那些方法有效而哪些无效。例如对于这个例子这种简单的回归问题,使用nn.L1Loss作为损失函数配合torch.optim.SGD()优化器就足够了,但是如果是猫狗大战这种分类问题,可能就需要使用nn.BCELoss,即二元交叉熵损失。
PyTorch training loop intuition
一轮训练循环中需要包含以下核心步骤。
- 前向传播,让数据流经模型的
forward()方法以生成预测结果。
- 计算损失,将前向传播的预测结果与真实标签进行对比,计算损失值。
- 反向传播,沿着网络反向计算模型各参数相对于损失的梯度。
- 优化器更新,利用优化器根据计算出的梯度调整模型参数,以降低损失。
Running our training loop epoch by epoch
知道了如何进行一个训练循环,那么我们就可以尝试对模型进行训练了。
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
| torch.manual_seed(42)
epochs = 200
epoch_count = [] loss_values = [] test_loss_values = []
for epoch in range(epochs): model_0.train() y_pred = model_0(X_train) loss = loss_fn(y_pred, y_train) optimizer.zero_grad() loss.backward() optimizer.step() model_0.eval() with torch.inference_mode(): test_pred = model_0(X_test) test_loss = loss_fn(test_pred, y_test) if epoch % 10 == 0: epoch_count.append(epoch) loss_values.append(loss) test_loss_values.append(test_loss) print(f"Epoch: {epoch} | Loss: {loss} | Test loss: {test_loss}") print(model_0.state_dict())
|
输出的训练进度如下:
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
| Epoch: 0 | Loss: 0.31288138031959534 | Test loss: 0.48106518387794495 OrderedDict({'weights': tensor([0.3406]), 'bias': tensor([0.1388])}) Epoch: 10 | Loss: 0.1976713240146637 | Test loss: 0.3463551998138428 OrderedDict({'weights': tensor([0.3796]), 'bias': tensor([0.2388])}) Epoch: 20 | Loss: 0.08908725529909134 | Test loss: 0.21729660034179688 OrderedDict({'weights': tensor([0.4184]), 'bias': tensor([0.3333])}) Epoch: 30 | Loss: 0.053148526698350906 | Test loss: 0.14464017748832703 OrderedDict({'weights': tensor([0.4512]), 'bias': tensor([0.3768])}) Epoch: 40 | Loss: 0.04543796554207802 | Test loss: 0.11360953003168106 OrderedDict({'weights': tensor([0.4748]), 'bias': tensor([0.3868])}) Epoch: 50 | Loss: 0.04167863354086876 | Test loss: 0.09919948130846024 OrderedDict({'weights': tensor([0.4938]), 'bias': tensor([0.3843])}) Epoch: 60 | Loss: 0.03818932920694351 | Test loss: 0.08886633068323135 OrderedDict({'weights': tensor([0.5116]), 'bias': tensor([0.3788])}) Epoch: 70 | Loss: 0.03476089984178543 | Test loss: 0.0805937647819519 OrderedDict({'weights': tensor([0.5288]), 'bias': tensor([0.3718])}) Epoch: 80 | Loss: 0.03132382780313492 | Test loss: 0.07232122868299484 OrderedDict({'weights': tensor([0.5459]), 'bias': tensor([0.3648])}) Epoch: 90 | Loss: 0.02788740023970604 | Test loss: 0.06473556160926819 OrderedDict({'weights': tensor([0.5629]), 'bias': tensor([0.3573])}) Epoch: 100 | Loss: 0.024458957836031914 | Test loss: 0.05646304413676262 OrderedDict({'weights': tensor([0.5800]), 'bias': tensor([0.3503])}) Epoch: 110 | Loss: 0.021020209416747093 | Test loss: 0.04819049686193466 OrderedDict({'weights': tensor([0.5972]), 'bias': tensor([0.3433])}) Epoch: 120 | Loss: 0.01758546754717827 | Test loss: 0.04060482233762741 ... Epoch: 180 | Loss: 0.00893248151987791 | Test loss: 0.005023092031478882 OrderedDict({'weights': tensor([0.6951]), 'bias': tensor([0.2993])}) Epoch: 190 | Loss: 0.00893248151987791 | Test loss: 0.005023092031478882 OrderedDict({'weights': tensor([0.6951]), 'bias': tensor([0.2993])}) Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
|
Writing testing loop code
我们绘制一下损失曲线看看是什么样的。
1 2 3 4 5 6
| plt.plot(epoch_count, np.array(torch.tensor(loss_values).numpy()), 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();
|
这个损失其实就很低了
对比训练之后的权重,已经和真实值非常接近了。同时我们画出训练之后模型的预测值的图片。
1 2 3 4 5 6 7
| model_0.state_dict()
weight, bias
with torch.inference_mode(): y_preds_new = model_0(X_test) plot_predictions(predictions=y_preds_new);
|
可以看出数据是非常接近的了
Saving/loading a model
在PyTorch中保存和加载模型,需要掌握下面三种核心方法。
torch.save(),将PyTorch对象以Python的pickle序列化格式进行保存。
torch.load(),加载已保存的PyTorch对象。
torch.nn.Module.load_state_dict(),加载模型已保存的状态字典。
接下来我们对已经训练好的模型进行保存。
1 2 3 4 5 6 7 8 9 10 11
| from pathlib import Path
MODEL_PATH = Path("models") MODEL_PATH.mkdir(parents=True, exist_ok=True)
MODEL_NAME = "01_pytorch_workflow_model_0.pth" MODEL_SAVE_PATH = MODEL_PATH / MODEL_NAME
print(f"Saving model to: {MODEL_SAVE_PATH}")
torch.save(obj=model_0.state_dict(),f=MODEL_SAVE_PATH)
|
我们保存的是模型的状态字典,而非整个模型,因此如果要加载这个模型,我们需要重新实例化一个类,并将保存好的state_dict()加载到这个新实例中。
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
| loaded_model_0 = LinearRegressionModel() loaded_model_0.load_state_dict(torch.load(f=MODEL_SAVE_PATH)) loaded_model_0.state_dict()
loaded_model_0.eval() with torch.inference_mode(): loaded_model_preds = loaded_model_0(X_test) loaded_model_preds
model_0.eval() with torch.inference_mode(): y_preds = model_0(X_test) y_preds == loaded_model_preds
|
执行的时候它会弹出一个FutureWarning,在将来在使用torch.load方法时,会默认将waights_only参数设为true,也就是说让这个方法只能加载张量和基本数据结构,禁止执行代码。
Putting everything together
最后,PyTorch已经有了现成的线性模型供我们使用。我们采用真实的线性模型重新写一次。
真实的线性模型即nn.Linear,他接受两个必须的构造参数:in_features和out_features,是输入样本和输出样本的大小。完整的训练和可视化代码如下:
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
|
import torch from torch import nn import matplotlib.pyplot as plt
device = "cpu" X = torch.arange(0,100,0.2,dtype=torch.float32).unsqueeze(1) true_k = 0.5 true_b = 0.2 Y = X*true_k + true_b train_size = int(X.size()[0] *0.8) train_X = X[:train_size].to(device) train_Y = Y[:train_size].to(device) test_X = X[train_size:].to(device) test_Y = Y[train_size:].to(device) epochs = 200 loss_values = []
class TestLinearModel(nn.Module): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.linear_layer = nn.Linear(in_features=1, out_features=1) def forward(self, x:torch.Tensor): return self.linear_layer(x)
model = TestLinearModel().to(device) loss_fn = nn.L1Loss() optimizer = torch.optim.SGD(params=model.parameters(),lr=0.01) for epoch in range(epochs): model.train() pred_Y = model(train_X) loss = loss_fn(pred_Y, train_Y) optimizer.zero_grad() loss.backward() optimizer.step() if epoch %10 ==0: loss_values.append(loss) print(f"Epoch:{epoch},Loss:{loss},State:{model.state_dict()}")
model.eval() with torch.inference_mode(): pred_Y = model(test_X) def plot_predictions(train_data=train_X,train_labels=train_Y,test_data=test_X,test_labels=test_Y,predictions=pred_Y): """ 可视化训练集、测试集数据,并对比模型预测结果。 """ plt.figure(figsize=(10, 7)) plt.scatter(train_data, train_labels, c="b", s=4, label="Training data") plt.scatter(test_data, test_labels, c="g", s=4, label="Test data") if predictions is not None: plt.scatter(test_data, predictions, c="r", s=4, label="Predition") plt.legend(prop={"size": 14}) plot_predictions()
|
输出的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Epoch:0,Loss:59.06752395629883,State:OrderedDict({'linear_layer.weight': tensor([[-0.5922]]), 'linear_layer.bias': tensor([0.6444])}) Epoch:10,Loss:4.604989528656006,State:OrderedDict({'linear_layer.weight': tensor([[0.2046]]), 'linear_layer.bias': tensor([0.6625])}) Epoch:20,Loss:4.547124862670898,State:OrderedDict({'linear_layer.weight': tensor([[0.2032]]), 'linear_layer.bias': tensor([0.6605])}) Epoch:30,Loss:4.489261150360107,State:OrderedDict({'linear_layer.weight': tensor([[0.2018]]), 'linear_layer.bias': tensor([0.6585])}) Epoch:40,Loss:4.431396484375,State:OrderedDict({'linear_layer.weight': tensor([[0.2004]]), 'linear_layer.bias': tensor([0.6565])}) Epoch:50,Loss:4.373531341552734,State:OrderedDict({'linear_layer.weight': tensor([[0.1990]]), 'linear_layer.bias': tensor([0.6545])}) Epoch:60,Loss:4.315667152404785,State:OrderedDict({'linear_layer.weight': tensor([[0.1976]]), 'linear_layer.bias': tensor([0.6525])}) Epoch:70,Loss:4.257802963256836,State:OrderedDict({'linear_layer.weight': tensor([[0.1962]]), 'linear_layer.bias': tensor([0.6505])}) Epoch:80,Loss:4.19993782043457,State:OrderedDict({'linear_layer.weight': tensor([[0.1948]]), 'linear_layer.bias': tensor([0.6485])}) Epoch:90,Loss:4.142073154449463,State:OrderedDict({'linear_layer.weight': tensor([[0.1934]]), 'linear_layer.bias': tensor([0.6465])}) Epoch:100,Loss:4.084209442138672,State:OrderedDict({'linear_layer.weight': tensor([[0.1920]]), 'linear_layer.bias': tensor([0.6445])}) Epoch:110,Loss:4.026343822479248,State:OrderedDict({'linear_layer.weight': tensor([[0.1906]]), 'linear_layer.bias': tensor([0.6425])}) Epoch:120,Loss:3.968479871749878,State:OrderedDict({'linear_layer.weight': tensor([[0.1892]]), 'linear_layer.bias': tensor([0.6405])}) Epoch:130,Loss:3.9106154441833496,State:OrderedDict({'linear_layer.weight': tensor([[0.1878]]), 'linear_layer.bias': tensor([0.6385])}) Epoch:140,Loss:3.8612771034240723,State:OrderedDict({'linear_layer.weight': tensor([[0.1866]]), 'linear_layer.bias': tensor([0.6366])}) Epoch:150,Loss:3.817622184753418,State:OrderedDict({'linear_layer.weight': tensor([[0.1855]]), 'linear_layer.bias': tensor([0.6349])}) Epoch:160,Loss:3.773967981338501,State:OrderedDict({'linear_layer.weight': tensor([[0.1845]]), 'linear_layer.bias': tensor([0.6331])}) Epoch:170,Loss:3.730314016342163,State:OrderedDict({'linear_layer.weight': tensor([[0.1834]]), 'linear_layer.bias': tensor([0.6314])}) Epoch:180,Loss:3.686659812927246,State:OrderedDict({'linear_layer.weight': tensor([[0.1824]]), 'linear_layer.bias': tensor([0.6296])}) Epoch:190,Loss:3.6430046558380127,State:OrderedDict({'linear_layer.weight': tensor([[0.1813]]), 'linear_layer.bias': tensor([0.6279])})
|
最后的预测结果可视化如下
这个k学的很不错,但是b总是有问题
我们牢记整个训练的流程:前向传播(预测数据),计算损失函数,反向传播,更新梯度。
总结
本次内容是视频课程的Chapter 1部分,我们了解了用torch进行模型训练的大致流程,一个模型最主要的就是层结构、损失函数和优化器。我们以一个非常简单的ground_truth线性模型来跑通了整个流程,在接下来我们或许还会学习其他的神经网络,那么这里的知识就是使用PyTorch的基本功。