计算机视觉101:从算法到部署

计算机视觉101:从算法到部署

    • 前言
    • [1 PyTorch基础](#1 PyTorch基础 "#1_PyTorch_25")
      • [1.1 什么是PyTorch](#1.1 什么是PyTorch "#11_PyTorch_29")
      • [1.2 为什么使用PyTorch](#1.2 为什么使用PyTorch "#12_PyTorch_35")
    • [2 PyTorch 工作流基础知识​](#2 PyTorch 工作流基础知识 "#2_PyTorch__45")
      • [2.1 图像读取与存储​](#2.1 图像读取与存储 "#21__48")
      • [2.2 数据(准备和加载)](#2.2 数据(准备和加载) "#22__73")
      • [2.3 构建模型](#2.3 构建模型 "#23__213")
      • [2.4 训练模型](#2.4 训练模型 "#24__394")
        • [2.4.1 在 PyTorch 中创建损失函数和优化器](#2.4.1 在 PyTorch 中创建损失函数和优化器 "#241__PyTorch__406")
        • [2.4.2 在 PyTorch 中创建优化循环](#2.4.2 在 PyTorch 中创建优化循环 "#242__PyTorch__444")
        • [2.4.3 PyTorch 训练循环](#2.4.3 PyTorch 训练循环 "#243_PyTorch__454")
        • [2.4.4 PyTorch 测试循环](#2.4.4 PyTorch 测试循环 "#244_PyTorch__463")
      • [2.5 使用训练好的 PyTorch 模型进行预测(推理)](#2.5 使用训练好的 PyTorch 模型进行预测(推理) "#25__PyTorch__581")
      • [2.6 保存和加载 PyTorch 模型](#2.6 保存和加载 PyTorch 模型 "#26__PyTorch__632")
        • [2.6.1 保存 PyTorch 模型的 state_dict()](#2.6.1 保存 PyTorch 模型的 state_dict() "#261__PyTorch__state_dict_647")
        • [2.6.2 加载保存的 PyTorch 模型的 state_dict()](#2.6.2 加载保存的 PyTorch 模型的 state_dict() "#262__PyTorch__state_dict_684")
      • [2.7 全部整合在一起](#2.7 全部整合在一起 "#27__721")
        • [2.7.1 数据](#2.7.1 数据 "#271__760")
        • [2.7.2 构建一个 PyTorch 线性模型](#2.7.2 构建一个 PyTorch 线性模型 "#272__PyTorch__831")
        • [2.7.3 训练](#2.7.3 训练 "#273__903")
        • [2.7.4 进行预测](#2.7.4 进行预测 "#274__1026")
        • [2.7.5 保存和加载模型](#2.7.5 保存和加载模型 "#275__1068")
    • [3 优质阅读材料​](#3 优质阅读材料 "#3__1143")

计算机视觉101:从算法到部署 ,计算机视觉(Computer Vision, CV)是人工智能的核心领域之一,它赋予机器"看"和"理解"世界的能力,广泛应用于自动驾驶、医疗影像、工业检测、增强现实等前沿场景。然而,从理论到落地,CV工程师不仅需要掌握算法原理,更要具备工程化思维,才能让模型真正服务于现实需求。本课程以​​"学以致用"​​为核心理念,采用​​"理论+代码+部署"​​三位一体的教学方式,带您系统掌握计算机视觉的核心技术栈:

  1. 从基础开始:理解图像处理(OpenCV)、特征工程(SIFT/SURF)等传统方法,夯实CV基本功
  2. 进阶深度学习:掌握CNN、Transformer等现代模型(PyTorch),学会数据增强、模型调优技巧
  3. 实战部署落地:学习模型量化、ONNX转换、边缘计算(TensorRT/RKNN)

让算法在真实场景中高效运行通过​​Python代码示范+工业级案例(如缺陷检测、人脸识别)​​,您将逐步构建从​​算法开发到嵌入式部署​​的完整能力链,最终独立完成一个可落地的CV项目。

无论您是希望转行AI的开发者、在校学生,还是寻求技术突破的工程师,本课程都将助您跨越"纸上谈兵"的瓶颈,成为兼具​​算法能力与工程思维​​的CV实战派。

​​🚀 现在开始,用代码让机器真正"看见"世界!​

前言

这个教程适合谁?机器学习、深度学习或 AI 领域的初学者,并希望学习 PyTorch。谁在使用 PyTorch?Meta (Facebook)、Tesla 和 Microsoft 等许多世界上最大的科技公司以及 OpenAI 等人工智能研究公司都使用 PyTorch 来支持研究并将机器学习引入他们的产品。


1 PyTorch基础


1.1 什么是PyTorch

PyTorch是一个开源机器学习和深度学习框架。PyTorch 允许您作和处理数据,并使用 Python 代码编写机器学习算法。

例如,Andrej Karpathy(特斯拉 AI 主管)已经就特斯拉如何使用 PyTorch 为其自动驾驶计算机视觉模型提供支持进行了多次演讲。PyTorch 还用于农业等其他行业,为拖拉机上的计算机视觉提供动力 。

1.2 为什么使用PyTorch

机器学习研究人员喜欢使用 PyTorch。截至 2022 年 2 月,PyTorch 是 Papers With Code 上最常用的深度学习框架,Paper With Code是一个用于跟踪机器学习研究论文及其附带代码存储库的网站。

PyTorch 还有助于在后台处理许多事情,例如 GPU 加速(使您的代码运行得更快)。

因此,您可以专注于作数据和编写算法,PyTorch 将确保它快速运行。

如果 Tesla 和 Meta (Facebook) 等公司使用它来构建模型,为数百个应用程序提供支持,驾驶数千辆汽车,并向数十亿人提供内容,那么它显然在开发方面也具有强大的能力。

2 PyTorch 工作流基础知识​


2.1 图像读取与存储​

机器学习和深度学习的本质是从过去获取一些数据,构建一种算法(如神经网络)来发现其中的模式,并使用发现的模式来预测未来。

有很多方法可以做到这一点,而且一直在发现许多新方法。

但让我们从小处着手。

我们从"一条直线"开始怎么样?

我们看看是否可以构建一个 PyTorch 模型来学习"直线"的模式并进行匹配。

在本模块中,我们将涵盖标准的 PyTorch 工作流程(它可以根据需要进行调整,但涵盖了主要步骤的轮廓)。

目前,我们将使用这个工作流程来预测一条简单的直线,但工作流程的步骤可以根据您所解决的问题进行重复和更改。

具体来说,我们将涵盖:

  1. 准备数据:数据可以是几乎任何东西,但为了开始,我们将创建一个简单的直线
  2. 构建模型:在这里,我们将创建一个模型来学习数据中的模式,我们还将选择一个损失函数、优化器并构建一个训练循环。
  3. 将模型拟合到数据(训练) :我们已经有了数据和模型,现在让模型(尝试)在(训练)数据中寻找模式。
  4. 进行预测和评估模型(推理) :我们的模型在数据中找到了模式,让我们将其发现与实际(测试)数据进行比较。
  5. 保存和加载模型:你可能希望在别处使用你的模型,或者稍后再次使用它,这里我们将涵盖这些内容。
  6. 将所有内容整合在一起:让我们将以上所有内容结合起来。

2.2 数据(准备和加载)

我想强调的是,机器学习中的"数据"几乎可以是你能想象到的任何东西。一个数字表格(比如一个大的 Excel 电子表格)、各种类型的图像、视频、歌曲或播客等音频文件、蛋白质结构、文本等等。

机器学习是一个由两部分组成的游戏:

  1. 将你的数据,无论是什么,转化为数字(一种表示形式)。
  2. 选择或构建一个模型,尽可能好地学习这种表示形式。

有时第一点和第二点可以同时完成。

但如果你没有数据会怎样?

现在就是我们的情况了。

没有数据。

但我们可以创建一些。

让我们把数据创建成一条直线。

我们将使用线性回归来创建具有已知参数的数据(这些参数可以通过模型学习),然后我们将使用 PyTorch 来查看是否可以构建一个模型,使用梯度下降来估计这些参数。

如果上面提到的术语现在对你来说没什么意义,不用担心,我们会看到它们在实际中的应用,我会在下面提供额外的资源,供你进一步学习。

ini 复制代码
# Create *known* parameters
weight = 0.7
bias = 0.3

# Create data
start = 0
end = 1
step = 0.02
X = torch.arange(start, end, step).unsqueeze(dim=1)
y = weight * X + bias

X[:10], y[:10]
css 复制代码
(tensor([[0.0000],
         [0.0200],
         [0.0400],
         [0.0600],
         [0.0800],
         [0.1000],
         [0.1200],
         [0.1400],
         [0.1600],
         [0.1800]]),
 tensor([[0.3000],
         [0.3140],
         [0.3280],
         [0.3420],
         [0.3560],
         [0.3700],
         [0.3840],
         [0.3980],
         [0.4120],
         [0.4260]]))

太棒了!现在我们将开始构建一个能够学习 X (特征)和 y (标签)之间关系的模型。

将数据分为训练集和测试集

我们已经有一些数据了。

但在我们构建模型之前,需要将其分割开。

机器学习项目中最重要的步骤之一是创建训练集和测试集(当需要时,还需要创建验证集)。

数据集的每个分割都有特定的目的:

训练集:模型从这些数据中学习(就像你在学期期间学习的课程材料一样)。

验证集:模型在这份数据上进行调优(就像你在期末考试前做的模拟考试一样)。

测试集:模型将在这个数据集上进行评估,以测试它学到了什么(就像你在学期末参加的期末考试一样)。

目前,我们只需使用训练集和测试集,这意味着我们将有一个数据集供我们的模型学习,同时也会对其进行评估。

我们可以通过分割我们的 X 和 y 张量来创建它们。

注意:在处理实际数据时,这一步通常在项目的最开始进行(测试集应始终与其他所有数据分开)。我们希望模型从训练数据中学习,然后使用测试数据来评估,以了解它对未见过的示例的泛化能力如何。

scss 复制代码
# Create train/test split
train_split = int(0.8 * len(X)) # 80% of data used for training set, 20% for testing 
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)
scss 复制代码
(40, 40, 10, 10)

太好了,我们有了 40 个训练样本( X_train & y_train )和 10 个测试样本( X_test & y_test )。

我们创建的模型将尝试学习 X_train & y_train 之间的关系,然后我们将在 X_test 和 y_test 上评估它学到的内容。

但目前我们的数据只是纸上的数字。

让我们创建一个函数来可视化它。

ini 复制代码
def plot_predictions(train_data=X_train, 
                     train_labels=y_train, 
                     test_data=X_test, 
                     test_labels=y_test, 
                     predictions=None):
  """
  Plots training data, test data and compares predictions.
  """
  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:
    # Plot the predictions in red (predictions were made on the test data)
    plt.scatter(test_data, predictions, c="r", s=4, label="Predictions")

  # Show the legend
  plt.legend(prop={"size": 14});
scss 复制代码
plot_predictions();

现在,我们的数据不再仅仅是纸上的数字,而是一条直线。

注意:现在是向你介绍数据探索者的座右铭......"可视化,可视化,可视化!"

每当你处理数据并将其转化为数字时,请记住这一点,如果你能可视化某事物,它将极大地帮助理解。

机器喜欢数字,我们人类也喜欢数字,但我们还喜欢观察事物。

2.3 构建模型

现在我们已经有一些数据了,让我们构建一个模型,用蓝色点来预测绿色点。

我们将直接开始。

我们先写代码,然后再解释一切。

让我们使用纯 PyTorch 来复制一个标准的线性回归模型。

python 复制代码
# Create a Linear Regression model class
class LinearRegressionModel(nn.Module): # <- almost everything in PyTorch is a nn.Module (think of this as neural network lego blocks)
    def __init__(self):
        super().__init__() 
        self.weights = nn.Parameter(torch.randn(1, # <- start with random weights (this will get adjusted as the model learns)
                                                dtype=torch.float), # <- PyTorch loves float32 by default
                                   requires_grad=True) # <- can we update this value with gradient descent?)

        self.bias = nn.Parameter(torch.randn(1, # <- start with random bias (this will get adjusted as the model learns)
                                            dtype=torch.float), # <- PyTorch loves float32 by default
                                requires_grad=True) # <- can we update this value with gradient descent?))

    # Forward defines the computation in the model
    def forward(self, x: torch.Tensor) -> torch.Tensor: # <- "x" is the input data (e.g. training/testing features)
        return self.weights * x + self.bias # <- this is the linear regression formula (y = m*x + b)

上面发生了很多事情,但让我们一步一步来分解。

PyTorch 模型构建基础

PyTorch 有四个(或多或少)基本模块,你可以使用它们来创建几乎任何你能想象到的类型的神经网络。

它们是 torch.nntorch.optimtorch.utils.data.Datasettorch.utils.data.DataLoader。目前,我们将专注于前两个,稍后再介绍另外两个。

torch.nn:包含计算图的所有构建模块(本质上是一系列特定方式执行的计算)。

torch.nn.Parameter:存储可用于 nn.Module 的张量。如果 requires_grad=True 梯度(用于通过梯度下降更新模型参数)自动计算,这通常被称为"自动求导"。

torch.nn.Module:所有神经网络模块的基类,所有神经网络的构建模块都是其子类。如果你在 PyTorch 中构建神经网络,你的模型应该继承 nn.Module 。需要实现 forward() 方法。

torch.optim:包含各种优化算法(这些算法告诉存储在 nn.Parameter 中的模型参数如何最佳地改变,以改进梯度下降并在一定程度上减少损失)。

def forward():所有 nn.Module 子类都需要一个 forward() 方法,这个方法定义了对传递给特定 nn.Module 的数据将进行的计算(例如,上述线性回归公式)。

如果以上听起来很复杂,可以这样理解,PyTorch 神经网络中的几乎所有内容都来自 torch.nn

  • nn.Module 包含更大的构建模块(层)。
  • nn.Parameter 包含较小的参数,如权重和偏差(将这些组合起来形成 nn.Module (s))
  • forward() 指导较大的模块如何在 nn.Module (s)内对输入(充满数据的张量)进行计算
  • torch.optim 包含优化方法,用于改进 nn.Parameter 内的参数,以更好地表示输入数据

通过继承 nn.Module 创建 PyTorch 模型的基本构建块。对于继承自 nn.Module 的对象,必须定义 forward() 方法。

检查 PyTorch 模型的参数

现在我们把这些事情处理好,用我们创建的类来创建一个模型实例,并使用 .parameters() 来检查它的参数。

ini 复制代码
# Set manual seed since nn.Parameter are randomly initialized
torch.manual_seed(42)

# Create an instance of the model (this is a subclass of nn.Module that contains nn.Parameter(s))
model_0 = LinearRegressionModel()

# Check the nn.Parameter(s) within the nn.Module subclass we created
list(model_0.parameters())
ini 复制代码
[Parameter containing:
 tensor([0.3367], requires_grad=True),
 Parameter containing:
 tensor([0.1288], requires_grad=True)]

我们也可以使用 .state_dict() 获取模型的状态(模型包含的内容)。

bash 复制代码
# List named parameters 
model_0.state_dict()
less 复制代码
OrderedDict([('weights', tensor([0.3367])), ('bias', tensor([0.1288]))])

注意到从 model_0.state_dict() 中 weights 和 bias 的值出来时是随机的浮点张量吗?

这是因为我们在上面使用 torch.randn() 初始化了它们。

本质上,我们希望从随机参数开始,并让模型更新它们以拟合我们的数据最佳(我们在创建直线数据时设置的硬编码的 weight 和 bias 值)。

由于我们的模型初始值是随机的,所以目前它的预测能力会很差。

使用 torch.inference_mode() 进行预测

为了检查这一点,我们可以将测试数据 X_test 传入,看看它预测 y_test 的准确程度。

当我们将数据传递给模型时,它会通过模型的 forward() 方法,并使用我们定义的计算来生成结果。

让我们做一些预测。

ini 复制代码
# Make predictions with model
with torch.inference_mode(): 
    y_preds = model_0(X_test)

# Note: in older PyTorch code you might also see torch.no_grad()
# with torch.no_grad():
#   y_preds = model_0(X_test)

你可能注意到了我们使用了 torch.inference_mode() 作为上下文管理器(这就是 with torch.inference_mode(): 的作用)来进行预测。

顾名思义, torch.inference_mode() 是在用模型进行推理(做出预测)时使用的。

torch.inference_mode() 会关闭很多东西(比如梯度跟踪,这对于训练是必要的,但对于推理不是必要的),以使前向传递(数据通过 forward() 方法)更快。

我们已经做出了一些预测,让我们看看它们的样子。

python 复制代码
# Check the predictions
print(f"Number of testing samples: {len(X_test)}") 
print(f"Number of predictions made: {len(y_preds)}")
print(f"Predicted values:\n{y_preds}")
ini 复制代码
Number of testing samples: 10
Number of predictions 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]])

注意,每个测试样本都有一个预测值。

这是因为我们使用的数据类型。对于我们的直线,一个 X 值映射到一个 y 值。

然而,机器学习模型非常灵活。你可以有 100 个 X 值映射到一个、两个、三个或 10 个 y 值。这完全取决于你正在处理的工作。

我们的预测仍然是纸上的数字,让我们用上面创建的 plot_predictions() 函数来可视化它们。

ini 复制代码
plot_predictions(predictions=y_preds)
复制代码
y_test - y_preds
css 复制代码
tensor([[0.4618],
        [0.4691],
        [0.4764],
        [0.4836],
        [0.4909],
        [0.4982],
        [0.5054],
        [0.5127],
        [0.5200],
        [0.5272]])

这些预测看起来相当糟糕...

这样想也合理,毕竟我们的模型只是用随机参数值来做出预测。

它甚至还没看过蓝色点,试图去预测绿色点。

是时候改变这种情况了。

2.4 训练模型

现在我们的模型使用随机参数进行计算,基本上是在猜测(随机地)。

为了解决这个问题,我们可以更新其内部参数(我也将参数称为模式),使用 nn.Parameter() 和 torch.randn() 随机设置的 weights 和 bias 值,使其更好地代表数据。

我们可以用硬编码的方式实现这一点(因为我们知道默认值 weight=0.7 和 bias=0.3 ),但这有什么乐趣呢?

很多时候,你不知道模型的理想参数是什么。

相反,编写代码让模型尝试自己找出这些参数会更有趣。

2.4.1 在 PyTorch 中创建损失函数和优化器

为了让我们的模型能够自行更新参数,我们需要在我们的食谱中添加一些其他的东西。

这也是一个损失函数以及一个优化器。

它们的作用是:

损失函数:衡量你的模型的预测(例如 y_preds )与真实标签(例如 y_test )相比有多错误。越低越好;PyTorch 在 torch.nn 中提供了许多内置的损失函数。回归问题的平均绝对误差(MAE)( torch.nn.L1Loss() )。二分类问题的二元交叉熵( torch.nn.BCELoss() )。

优化器:告诉你的模型如何更新其内部参数以最好地降低损失。你可以在 torch.optim 中找到各种优化函数的实现。 随机梯度下降( torch.optim.SGD() )。Adam 优化器( torch.optim.Adam() )。

让我们创建一个损失函数和一个优化器,它们可以帮助我们改进模型。

你正在处理什么类型的问题将决定你使用哪种损失函数和哪种优化器。

然而,有一些常见的值已知效果良好,例如 SGD(随机梯度下降)或 Adam 优化器。对于回归问题(预测一个数字)可以使用 MAE(平均绝对误差)损失函数,对于分类问题(预测是或否)可以使用二元交叉熵损失函数。

对于我们的问题,因为我们是在预测一个数字,所以让我们在 PyTorch 中使用 MAE(位于 torch.nn.L1Loss() 下)作为我们的损失函数。

平均绝对误差(MAE,在 PyTorch 中: torch.nn.L1Loss )衡量两个点(预测和标签)之间的绝对差异,然后对所有样本取平均值。

我们将使用 SGD, torch.optim.SGD(params, lr) ,其中:

  • params 是你希望优化的目标模型参数(例如,我们之前随机设置的 weights 和 bias 值)。
  • lr 是你希望优化器更新参数的学习率,数值越高意味着优化器会尝试更大的更新(这些有时可能太大,导致优化器无法正常工作),数值越低意味着优化器会尝试更小的更新(这些有时可能太小,导致优化器需要花费太长时间找到理想值)。学习率被认为是一个超参数(因为它是由机器学习工程师设置的)。学习率的常见起始值是 0.01 、 0.001 、 0.0001 ,但这些值也可以随着时间的推移进行调整(这被称为学习率调度)。

让我们看看代码吧。

ini 复制代码
# Create the loss function
loss_fn = nn.L1Loss() # MAE loss is same as L1Loss

# Create the optimizer
optimizer = torch.optim.SGD(params=model_0.parameters(), # parameters of target model to optimize
                            lr=0.01) # learning rate (how much the optimizer should change parameters at each step, higher=more (less stable), lower=less (might take a long time))
2.4.2 在 PyTorch 中创建优化循环

太棒了!现在我们有了损失函数和优化器,是时候创建训练循环(和测试循环)了。

训练循环包括模型遍历训练数据并学习 features 和 labels 之间的关系。

测试循环包括遍历测试数据,评估模型在训练数据上学习到的模式有多好(模型在训练过程中永远不会看到测试数据)。

每一个都被称为一个"循环",因为我们希望我们的模型能够查看(遍历)每个数据集中的每个样本。

2.4.3 PyTorch 训练循环

对于训练循环,我们将构建以下步骤:

  1. 前向传播:模型遍历所有训练数据一次,执行其 forward() 函数计算。 model(x_train)
  2. 计算损失:模型的输出(预测结果)与真实值进行比较,以评估其错误程度。 loss = loss_fn(y_pred, y_train)
  3. 零梯度:优化器的梯度被设为零(它们默认是累积的),以便可以为特定的训练步骤重新计算。 optimizer.zero_grad()
  4. 对损失进行反向传播:计算损失相对于每个待更新模型参数(每个带 requires_grad=True 的参数)的梯度。这被称为反向传播,因此称为"反向"。 loss.backward()
  5. 更新优化器(梯度下降)使用 requires_grad=True 更新参数,根据损失梯度来改进它们。 optimizer.step()
2.4.4 PyTorch 测试循环

关于测试循环(评估我们的模型),典型步骤包括:

  1. 前向传播 :模型遍历所有测试数据一次,执行其 forward() 函数计算。 model(x_test)
  2. 计算损失:模型的输出(预测结果)与真实值进行比较,以评估其错误程度。 loss = loss_fn(y_pred, y_test)
  3. 计算评估指标(可选)除了损失值,你可能还希望计算其他评估指标,例如在测试集上的准确率。 Custom functions 自定义函数

注意测试循环不包含执行反向传播( loss.backward() )或更新优化器( optimizer.step() ),这是因为测试期间模型中的参数没有发生变化,它们已经被计算过了。对于测试,我们只关心模型前向传播的输出。

让我们将以上所有内容结合起来,训练我们的模型 100 个 epoch(通过数据的正向传递),并且每 10 个 epoch 评估一次。

ini 复制代码
torch.manual_seed(42)

# Set the number of epochs (how many times the model will pass over the training data)
epochs = 100

# Create empty loss lists to track values
train_loss_values = []
test_loss_values = []
epoch_count = []

for epoch in range(epochs):
    ### Training

    # Put model in training mode (this is the default state of a model)
    model_0.train()

    # 1. Forward pass on train data using the forward() method inside 
    y_pred = model_0(X_train)
    # print(y_pred)

    # 2. Calculate the loss (how different are our models predictions to the ground truth)
    loss = loss_fn(y_pred, y_train)

    # 3. Zero grad of the optimizer
    optimizer.zero_grad()

    # 4. Loss backwards
    loss.backward()

    # 5. Progress the optimizer
    optimizer.step()

    ### Testing

    # Put the model in evaluation mode
    model_0.eval()

    with torch.inference_mode():
      # 1. Forward pass on test data
      test_pred = model_0(X_test)

      # 2. Caculate loss on test data
      test_loss = loss_fn(test_pred, y_test.type(torch.float)) # predictions come in torch.float datatype, so comparisons need to be done with tensors of the same type

      # Print out what's happening
      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} ")
yaml 复制代码
Epoch: 0 | MAE Train Loss: 0.31288138031959534 | MAE Test Loss: 0.48106518387794495 
Epoch: 10 | MAE Train Loss: 0.1976713240146637 | MAE Test Loss: 0.3463551998138428 
Epoch: 20 | MAE Train Loss: 0.08908725529909134 | MAE Test Loss: 0.21729660034179688 
Epoch: 30 | MAE Train Loss: 0.053148526698350906 | MAE Test Loss: 0.14464017748832703 
Epoch: 40 | MAE Train Loss: 0.04543796554207802 | MAE Test Loss: 0.11360953003168106 
Epoch: 50 | MAE Train Loss: 0.04167863354086876 | MAE Test Loss: 0.09919948130846024 
Epoch: 60 | MAE Train Loss: 0.03818932920694351 | MAE Test Loss: 0.08886633068323135 
Epoch: 70 | MAE Train Loss: 0.03476089984178543 | MAE Test Loss: 0.0805937647819519 
Epoch: 80 | MAE Train Loss: 0.03132382780313492 | MAE Test Loss: 0.07232122868299484 
Epoch: 90 | MAE Train Loss: 0.02788739837706089 | MAE Test Loss: 0.06473556160926819 

我们的损失似乎在每个 epoch 都在下降,让我们画出来看看。

ini 复制代码
# 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();

损失曲线显示了损失随时间下降。记住,损失是衡量你的模型错误程度的指标,所以越低越好。

但为什么损失会下降?

嗯,多亏了我们的损失函数和优化器,模型的内部参数( weights 和 bias )被更新了,以更好地反映数据中的潜在模式。

让我们检查模型的 .state_dict() ,看看我们的模型接近我们为权重和偏差设置的原始值的程度。

python 复制代码
# Find our model's learned parameters
print("The model learned the following values for weights and bias:")
print(model_0.state_dict())
print("\nAnd the original values for weights and bias are:")
print(f"weights: {weight}, bias: {bias}")
css 复制代码
The model learned the following values for weights and bias:
OrderedDict([('weights', tensor([0.5784])), ('bias', tensor([0.3513]))])

And the original values for weights and bias are:
weights: 0.7, bias: 0.3

我们的模型非常接近计算 weight 和 bias 的原始精确值(如果我们再训练更长时间,可能会更接近)。

它可能永远不会完美地猜测它们(尤其是在使用更复杂的数据集时),但这没关系,通常你可以用接近的近似值做非常酷的事情。

这是机器学习和深度学习的整体思路,存在一些描述我们数据的理想值,而不是手动找出它们,我们可以训练一个模型来程序化地找出它们。

2.5 使用训练好的 PyTorch 模型进行预测(推理)

一旦你训练好了一个模型,你很可能想要用它进行预测。

我们在上面的训练和测试代码中已经看到了这一点,在训练/测试循环之外执行它的步骤是相似的。

在使用 PyTorch 模型进行预测(也称为执行推理)时,需要记住三件事:

  1. 将模型设置为评估模式( model.eval() )。
  2. 使用推理模式上下文管理器进行预测( with torch.inference_mode(): ... )。
  3. 所有预测都应该在相同设备上的对象进行(例如,数据与模型仅使用 GPU 或仅使用 CPU)。

前两项确保所有 PyTorch 在训练过程中使用的幕后有助计算和设置,但不是推理所必需的都被关闭(这会导致计算速度更快)。第三项确保你不会遇到跨设备错误。

ini 复制代码
# 1. Set the model in evaluation mode
model_0.eval()

# 2. Setup the inference mode context manager
with torch.inference_mode():
  # 3. Make sure the calculations are done with the model and data on the same device
  # in our case, we haven't setup device-agnostic code yet so our data and model are
  # on the CPU by default.
  # model_0.to(device)
  # X_test = X_test.to(device)
  y_preds = model_0(X_test)
y_preds
css 复制代码
tensor([[0.8141],
        [0.8256],
        [0.8372],
        [0.8488],
        [0.8603],
        [0.8719],
        [0.8835],
        [0.8950],
        [0.9066],
        [0.9182]])

我们已经用训练好的模型做了一些预测,它们看起来怎么样呢?

ini 复制代码
plot_predictions(predictions=y_preds)

那些红点看起来比之前要近得多!

让我们来学习如何在 PyTorch 中保存和加载模型。

2.6 保存和加载 PyTorch 模型

如果你训练了一个 PyTorch 模型,你很可能会想保存它并导出到某个地方。

也就是说,你可能会在 Google Colab 或带有 GPU 的本地计算机上训练它,但现在你希望将其导出到某种应用程序中,以便其他人可以使用它。

或者,你可能想要保存你的模型进度,稍后回来再加载它。

在 PyTorch 中保存和加载模型,有三种主要方法需要了解(以下内容均来自 PyTorch 保存和加载模型指南):

torch.save:使用 Python 的 pickle 工具将序列化对象保存到磁盘。模型、张量以及字典等各种其他 Python 对象都可以使用 torch.save 进行保存。

torch.load:使用 pickle 的反序列化功能来反序列化并加载 Pickle 格式的 Python 对象文件(如模型、张量或字典)到内存中。您还可以设置将对象加载到哪个设备(CPU、GPU 等)。

torch.nn.Module.load_state_dict:使用保存的 state_dict() 对象加载模型的参数字典 ( model.state_dict() )。

2.6.1 保存 PyTorch 模型的 state_dict()

保存和加载模型以进行推理(做出预测)的推荐方法是通过保存和加载模型的 state_dict() 。

让我们看看我们如何分步骤完成这个操作:

  1. 我们将使用 Python 的 pathlib 模块创建一个名为 models 的目录来保存模型。
  2. 我们将创建一个文件路径来保存模型。
  3. 我们将 torch.save(obj, f) 称为 obj ,其中 obj 是目标模型的 state_dict() ,而 f 是保存模型的文件名。

注意:对于 PyTorch 保存的模型或对象,通常以 .pt 或 .pth 结尾,例如 saved_model_01.pth 。

ini 复制代码
from pathlib import Path

# 1. Create models 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(), # only saving the state_dict() only saves the models learned parameters
           f=MODEL_SAVE_PATH) 
css 复制代码
Saving model to: models/01_pytorch_workflow_model_0.pth
bash 复制代码
# Check the saved file path
!ls -l models/01_pytorch_workflow_model_0.pth
2.6.2 加载保存的 PyTorch 模型的 state_dict()

既然我们现在在 models/01_pytorch_workflow_model_0.pth 处有一个保存的模型 state_dict() ,我们可以使用 torch.nn.Module.load_state_dict(torch.load(f)) 来加载它,其中 f 是我们保存的模型 state_dict() 的文件路径。

为什么在 torch.nn.Module.load_state_dict() 中调用 torch.load() ?

因为我们只保存了模型的 state_dict() ,这是一个包含学习参数的字典,而不是整个模型,所以我们首先需要使用 torch.load() 加载 state_dict() ,然后将这个 state_dict() 传递给我们的模型的新实例(这是一个 nn.Module 的子类)。

为什么不保存整个模型?

保存整个模型而不是仅保存 state_dict() 更直观。

这种保存整个模型的方法的缺点是,序列化的数据与模型保存时使用的特定类和精确的目录结构绑定在一起...

因此,当你的代码用于其他项目或经过重构后,可能会以各种方式出现错误。

所以我们使用灵活的方法来保存和加载仅 state_dict() ,这本质上又是一个模型参数的字典。

让我们通过创建另一个 LinearRegressionModel() 的实例来测试它,它是 torch.nn.Module 的子类,因此将拥有内置的 load_state_dict() 方法。

ini 复制代码
# Instantiate a new instance of our model (this will be instantiated with random weights)
loaded_model_0 = LinearRegressionModel()

# Load the state_dict of our saved model (this will update the new instance of our model with trained weights)
loaded_model_0.load_state_dict(torch.load(f=MODEL_SAVE_PATH))

现在要测试我们加载的模型,让我们用它(进行预测)在测试数据上进行推理。

ini 复制代码
# 1. Put the loaded model into evaluation mode
loaded_model_0.eval()

# 2. Use the inference mode context manager to make predictions
with torch.inference_mode():
    loaded_model_preds = loaded_model_0(X_test) # perform a forward pass on the test data with the loaded model

2.7 全部整合在一起

我们目前已经涵盖了相当多的内容。

但一旦你进行了一些练习,你将像在街上跳舞一样轻松完成上述步骤。

说到实践,让我们把到目前为止所做的一切结合起来。

除了这次我们将使我们的代码设备无关(所以如果有 GPU 可用,它将使用它,如果没有,它将默认使用 CPU)。

这一部分将比上面少很多注释,因为我们将要进行的已经涵盖了。

我们将从导入我们需要的标准库开始。

python 复制代码
# Import PyTorch and matplotlib
import torch
from torch import nn # nn contains all of PyTorch's building blocks for neural networks
import matplotlib.pyplot as plt

# Check PyTorch version
torch.__version__

现在让我们通过设置 device="cuda" (如果可用)来使我们的代码设备无关,否则将默认为 device="cpu" 。

python 复制代码
# Setup device agnostic code
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

如果你有访问 GPU 的权限,上述内容应该已经打印出来了:

sql 复制代码
Using device: cuda

否则,接下来的计算将使用 CPU。这对我们的小数据集来说没问题,但对更大的数据集来说会花费更长的时间。

2.7.1 数据

让我们像之前一样创建一些数据。

首先,我们将硬编码一些 weight 和 bias 值。

然后,我们将生成一个介于 0 和 1 之间的数值范围,这些将作为我们的 X 值。

最后,我们将使用 X 值,以及 weight 和 bias 值,通过线性回归公式( y = weight * X + bias )来创建 y 。

ini 复制代码
# Create weight and bias
weight = 0.7
bias = 0.3

# Create range values
start = 0
end = 1
step = 0.02

# Create X and y (features and labels)
X = torch.arange(start, end, step).unsqueeze(dim=1) # without unsqueeze, errors will happen later on (shapes within linear layers)
y = weight * X + bias 
X[:10], y[:10]
css 复制代码
(tensor([[0.0000],
         [0.0200],
         [0.0400],
         [0.0600],
         [0.0800],
         [0.1000],
         [0.1200],
         [0.1400],
         [0.1600],
         [0.1800]]),
 tensor([[0.3000],
         [0.3140],
         [0.3280],
         [0.3420],
         [0.3560],
         [0.3700],
         [0.3840],
         [0.3980],
         [0.4120],
         [0.4260]]))

现在我们已经有一些数据了,让我们将其分成训练集和测试集。

我们将使用 80/20 的分割比例,其中 80%用于训练数据,20%用于测试数据。

scss 复制代码
# Split data
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)
scss 复制代码
(40, 40, 10, 10)

太好了,让我们将它们可视化以确保它们看起来正常。

bash 复制代码
# Note: If you've reset your runtime, this function won't work, 
# you'll have to rerun the cell above where it's instantiated.
plot_predictions(X_train, y_train, X_test, y_test)
2.7.2 构建一个 PyTorch 线性模型

我们已经有了数据,现在是时候建立一个模型了。

我们将创建与之前相同风格的模型,只是这次,我们不会使用 nn.Parameter() 手动定义模型的权重和偏差参数,而是使用 nn.Linear(in_features, out_features) 来为我们完成这些。

其中 in_features 表示输入数据的维度数量, out_features 表示希望输出到的维度数量。

在我们的案例中,这两者都是 1 ,因为我们的数据每个标签( y )有 1 个输入特征( X )。

使用 nn.Parameter 创建线性回归模型与使用 nn.Linear 的区别。 torch.nn 模块还有许多预构建计算的例子,包括许多流行且实用的神经网络层。

ruby 复制代码
# Subclass nn.Module to make our model
class LinearRegressionModelV2(nn.Module):
    def __init__(self):
        super().__init__()
        # Use nn.Linear() for creating the model parameters
        self.linear_layer = nn.Linear(in_features=1, 
                                      out_features=1)
    
    # Define the forward computation (input data x flows through nn.Linear())
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.linear_layer(x)

# Set the manual seed when creating the model (this isn't always needed but is used for demonstrative purposes, try commenting it out and seeing what happens)
torch.manual_seed(42)
model_1 = LinearRegressionModelV2()
model_1, model_1.state_dict()
lua 复制代码
(LinearRegressionModelV2(
   (linear_layer): Linear(in_features=1, out_features=1, bias=True)
 ),
 OrderedDict([('linear_layer.weight', tensor([[0.7645]])),
              ('linear_layer.bias', tensor([0.8300]))]))

注意 model_1.state_dict() 的输出, nn.Linear() 层为我们创建了一个随机的 weight 和 bias 参数。

现在让我们将模型放到 GPU 上(如果可用)。

我们可以使用 .to(device) 来更改 PyTorch 对象的设备。

首先让我们检查模型的当前设备。

perl 复制代码
# Check model device
next(model_1.parameters()).device
ini 复制代码
device(type='cpu')

太好了,模型默认在 CPU 上。

让我们改为在 GPU 上运行(如果可用)。

vbnet 复制代码
# Set model to GPU if it's available, otherwise it'll default to CPU
model_1.to(device) # the device variable was set above to be "cuda" if available or "cpu" if not
next(model_1.parameters()).device

太好了!由于我们的设备无关代码,上面的单元格无论是否有 GPU 都能正常工作。

如果你确实有访问支持 CUDA 的 GPU,你应该会看到类似以下的输出:

ini 复制代码
device(type='cuda', index=0)
2.7.3 训练

是时候构建训练和测试循环了。

首先我们需要一个损失函数和一个优化器。

我们使用之前用过的相同函数, nn.L1Loss() 和 torch.optim.SGD() 。

我们需要将新模型的参数( model.parameters() )传递给优化器,以便它在训练过程中调整这些参数。

0.01 的学习率之前效果很好,我们再次使用它。

ini 复制代码
# Create loss function
loss_fn = nn.L1Loss()

# Create optimizer
optimizer = torch.optim.SGD(params=model_1.parameters(), # optimize newly created model's parameters
                            lr=0.01)

漂亮,损失函数和优化器都准备好了,现在让我们使用训练和测试循环来训练和评估我们的模型。

与之前的训练循环相比,这一步我们唯一的不同之处是将数据放在目标 device 上。

我们已经使用 model_1.to(device) 将我们的模型放在了目标 device 上。

我们也可以用同样的方式处理数据。

那样的话,如果模型在 GPU 上,数据也在 GPU 上(反之亦然)。

这次让我们更进一步,设定 epochs=1000 。

ini 复制代码
torch.manual_seed(42)

# Set the number of epochs 
epochs = 1000 

# Put data on the available device
# Without this, error will happen (not all model/data on device)
X_train = X_train.to(device)
X_test = X_test.to(device)
y_train = y_train.to(device)
y_test = y_test.to(device)

for epoch in range(epochs):
    ### Training
    model_1.train() # train mode is on by default after construction

    # 1. Forward pass
    y_pred = model_1(X_train)

    # 2. Calculate loss
    loss = loss_fn(y_pred, y_train)

    # 3. Zero grad optimizer
    optimizer.zero_grad()

    # 4. Loss backward
    loss.backward()

    # 5. Step the optimizer
    optimizer.step()

    ### Testing
    model_1.eval() # put the model in evaluation mode for testing (inference)
    # 1. Forward pass
    with torch.inference_mode():
        test_pred = model_1(X_test)
    
        # 2. Calculate the loss
        test_loss = loss_fn(test_pred, y_test)

    if epoch % 100 == 0:
        print(f"Epoch: {epoch} | Train loss: {loss} | Test loss: {test_loss}")
yaml 复制代码
Epoch: 0 | Train loss: 0.5551779866218567 | Test loss: 0.5739762187004089
Epoch: 100 | Train loss: 0.006215683650225401 | Test loss: 0.014086711220443249
Epoch: 200 | Train loss: 0.0012645035749301314 | Test loss: 0.013801801018416882
Epoch: 300 | Train loss: 0.0012645035749301314 | Test loss: 0.013801801018416882
Epoch: 400 | Train loss: 0.0012645035749301314 | Test loss: 0.013801801018416882
Epoch: 500 | Train loss: 0.0012645035749301314 | Test loss: 0.013801801018416882
Epoch: 600 | Train loss: 0.0012645035749301314 | Test loss: 0.013801801018416882
Epoch: 700 | Train loss: 0.0012645035749301314 | Test loss: 0.013801801018416882
Epoch: 800 | Train loss: 0.0012645035749301314 | Test loss: 0.013801801018416882
Epoch: 900 | Train loss: 0.0012645035749301314 | Test loss: 0.013801801018416882

注意:由于机器学习的随机性,你的模型在 CPU 或 GPU 上训练可能会得到略微不同的结果(不同的损失值和预测值)。即使你在两种设备上使用相同的随机种子,这种情况也会发生。如果差异很大,你可能需要查找错误;但如果差异很小(理想情况下是这样),你可以忽略它。

太好了!这个损失看起来非常低。

让我们检查一下我们的模型学习了哪些参数,并将它们与硬编码的原始参数进行比较。

python 复制代码
# Find our model's learned parameters
from pprint import pprint # pprint = pretty print, see: https://docs.python.org/3/library/pprint.html 
print("The model learned the following values for weights and bias:")
pprint(model_1.state_dict())
print("\nAnd the original values for weights and bias are:")
print(f"weights: {weight}, bias: {bias}")
lua 复制代码
The model learned the following values for weights and bias:
OrderedDict([('linear_layer.weight', tensor([[0.6968]], device='cuda:0')),
             ('linear_layer.bias', tensor([0.3025], device='cuda:0'))])

And the original values for weights and bias are:
weights: 0.7, bias: 0.3

这模型简直完美了。

然而,在实践中,你很少能提前知道完美的参数。

而且,如果你提前知道模型需要学习的参数,机器学习的乐趣又在哪里呢?

此外,在许多现实世界的机器学习问题中,参数数量可能远远超过数百万。

我不知道你怎么样,但我宁愿让计算机通过代码来处理这些,而不是手动处理。

2.7.4 进行预测

现在我们已经有了训练好的模型,让我们开启它的评估模式并进行一些预测。

ini 复制代码
# Turn model into evaluation mode
model_1.eval()

# Make predictions on the test data
with torch.inference_mode():
    y_preds = model_1(X_test)
y_preds
ini 复制代码
tensor([[0.8600],
        [0.8739],
        [0.8878],
        [0.9018],
        [0.9157],
        [0.9296],
        [0.9436],
        [0.9575],
        [0.9714],
        [0.9854]], device='cuda:0')

如果你在 GPU 上进行预测,可能会注意到上述输出在末尾有 device='cuda:0' 。这意味着数据在 CUDA 设备 0 上(由于零索引,这是你的系统可以访问的第一个 GPU),如果你将来使用多个 GPU,这个数字可能会更高。

现在让我们绘制模型的预测结果。

注意:许多数据科学库,如 pandas、matplotlib 和 NumPy,无法使用存储在 GPU 上的数据。因此,当你尝试使用这些库中的一个函数与未存储在 CPU 上的张量数据时,可能会遇到一些问题。要解决这个问题,你可以在目标张量上调用 .cpu() ,以返回目标张量在 CPU 上的副本。

ini 复制代码
# plot_predictions(predictions=y_preds) # -> won't work... data not on CPU

# Put data on the CPU and plot it
plot_predictions(predictions=y_preds.cpu())

哇!看那些红点,它们几乎与绿点完全对齐。我想是额外的训练轮次起了作用。

2.7.5 保存和加载模型

我们对模型的预测结果很满意,所以将其保存到文件中,以便以后使用。

ini 复制代码
from pathlib import Path

# 1. Create models directory 
MODEL_PATH = Path("models")
MODEL_PATH.mkdir(parents=True, exist_ok=True)

# 2. Create model save path 
MODEL_NAME = "01_pytorch_workflow_model_1.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_1.state_dict(), # only saving the state_dict() only saves the models learned parameters
           f=MODEL_SAVE_PATH) 

为了确保一切运行正常,让我们再加载它一次。

我们将:

  • 创建 LinearRegressionModelV2() 类的新实例
  • 使用 torch.nn.Module.load_state_dict() 加载模型状态字典
  • 将模型的新实例发送到目标设备(以确保我们的代码与设备无关)
python 复制代码
# Instantiate a fresh instance of LinearRegressionModelV2
loaded_model_1 = LinearRegressionModelV2()

# Load model state dict 
loaded_model_1.load_state_dict(torch.load(MODEL_SAVE_PATH))

# Put model to target device (if your data is on GPU, model will have to be on GPU to make predictions)
loaded_model_1.to(device)

print(f"Loaded model:\n{loaded_model_1}")
print(f"Model on device:\n{next(loaded_model_1.parameters()).device}")
ini 复制代码
Loaded model:
LinearRegressionModelV2(
  (linear_layer): Linear(in_features=1, out_features=1, bias=True)
)
Model on device:
cuda:0

现在我们可以评估加载的模型,看看它的预测是否与保存之前的预测一致。

ini 复制代码
# Evaluate loaded model
loaded_model_1.eval()
with torch.inference_mode():
    loaded_model_1_preds = loaded_model_1(X_test)
y_preds == loaded_model_1_preds
ini 复制代码
tensor([[True],
        [True],
        [True],
        [True],
        [True],
        [True],
        [True],
        [True],
        [True],
        [True]], device='cuda:0')

一切都合理!

3 优质阅读材料​


相关推荐
KIDAKN1 小时前
JavaEE->多线程2
java·算法·java-ee
uwvwko1 小时前
数据结构学习——树的储存结构
数据库·学习·算法·
森焱森2 小时前
基于GD32F4XX串口环形缓冲区,北斗2.1协议,RD模块数据解析代码
c语言·单片机·算法·架构
秋风&萧瑟2 小时前
【C++】C++枚举、const、static的用法
c++·算法
玉~你还好吗2 小时前
【LeetCode#第228题】汇总区间(简单题)
算法·leetcode
_周游2 小时前
【数据结构】_二叉树部分特征统计
数据结构·算法
零点BUG2 小时前
推荐系统召回机制全景指南:从经典算法到工业级实践
算法
双叶8363 小时前
(C++)素数的判断(C++教学)(C语言)
c语言·开发语言·数据结构·c++·算法
风靡晚4 小时前
汽车毫米波雷达增强感知:基于相干扩展和高级 IAA 的超分辨率距离和角度估计.
算法·汽车·信息与通信·信号处理·fmcw