PyTorch是一个用于机器学习和深度学习的开源深度学习框架,由Facebook于2016年发布,其主要实现了自动微分功能,并引入动态计算图使模型建立更加灵活。Pytorch可分为前后端两个部分,前端是与用户直接交互的python API,后端是框架内部实现的部分,包括Autograd,它是一个自动微分引擎。
Pytorch基于已有的张量库Torch开发,在PyTorch的早期版本中,使用的是Torch7,后来随着PyTorch的发展,逐渐演变成了PyTorch所使用的张量库。
现如今,Pytorch已经成为开源机器学习系统中,在科研领域市场占有率最高的框架,其在AI顶会上的占比在2022年已达80% [1]。
本文通过详实的代码,从如何安装PyTorch开始,一步一步带领读者熟悉PyTorch和Jupyter Notebook,最终使用PyTorch实现线性回归、逻辑回归以及图像分类,非常适合0基础初学者。
今天为大家带来一份非常详尽的PyTorch教程。本文共分3大部分:
安装PyTorch和Jupyter Notebook
用PyTorch实现线性回归
使用逻辑回归实现图像分类
文章超长,秉承用代码搞定一切的原则,内含大量代码,建议收藏,并分享给你喜欢的人。同时如果有什么疑问,也欢迎留言告知我们。
Tips:为了方便演示,文中代码和返回结果是用截图形式给出。本系列中的所有代码都以Jupyter Notebook形式提供,托管在Jovian。托管链接:
https://jvn.io/aakashns/e5cfe043873f4f3c9287507016747ae5
安装PyTorch和Jupyter Notebook
我们将使用Anaconda的Python发行版来安装库和管理虚拟环境,对于交互式编码和实验,我们将使用Jupyter Notebook。
首先按照官方教程安装Anaconda,接下来安装jovian:
$ pip install jovian --upgrade
下载针对本教程的notebook:
$ jovian clone e5cfe043873f4f3c9287507016747ae5
此时会创建一个01-pytorch-basics的目录,包含01-pytorch-basics.ipynb和environment.yml文件。
$ cd 01-pytorch-basics$ conda env update
目的是不破坏本地Python环境,使用一个虚拟环境。接下来激活
$ conda activate 01-pytorch-basics
启动Jupyter
$ jupyter notebook
打开浏览器,输入http://localhost:8888 。然后点有01-pytorch-basics.ipynb字样的就开始了。
本质上PyTorch是处理Tensor的库。所以我们先来简单看下tensor类型:
因为很多时候我们需要在NumPy的数组和PyTorch数组之间进行转换。
Commit并上传:
上传的项目还可以用 jovian clone 下载回来。操作类似git,这里不再累述。
简单的开场之后,我们直接进入硬核阶段。
用PyTorch实现线性回归
对于线性回归,相信大家都很熟悉了,各种机器学习的书第一个要讲的内容必定有线性回归。
这一部分,我们首先通过最原始的手动操作,来理解整个线性回归的原理和操作流程。
接着我们会再介绍使用PyTorch内置的函数,通过自动化的方式实现线性回归。
线性回归模型中,每个目标变量都被估算为输入变量的加权和及偏差。
先看一张表:
表格第一列是地区,第二类是温度单位华氏度,第二列是降水量单位毫米,第四列是湿度,第五列是苹果产量,第六列是橙子产量。
下面这段代码的目的是为了预估出苹果和橙子在不同地区、不同环境中的产量。
yield_apple = w11 * temp + w12 * rainfall + w13 * humidity + b1yield_orange = w21 * temp + w22 * rainfall + w23 * humidity + b2
分别个温度temp、降水量rainfall、湿度humidity加上不同的权重(w11,w12,w13),最后再加一个b1或者b2的偏差。
通过使用被称为梯度下降的优化技术,少量多次调整权重以获得更精准的预测结果。
训练数据
在Jupyter Notebook里导入NumPy和PyTorch
训练数据我们inputs和targets两个矩阵表示,每个观察一行,每个变量一列。
接下来转换成PyTorch的tensors:
变量和偏差也用矩阵表示,从随机数值开始
模型可以表示为
我们这样定义模型:
生成预测
对比一下原始数据
发现差距很大,因为一开始我们用的是随机数值,所以数据合不上很正常。
接下来我们需要通过损失函数,来评估我们的模型和实际差距多大。分为3个步骤
计算两个矩阵(preds和targets)之间的差异
平方差矩阵的所有元素以消除负值
计算结果矩阵中元素的平均值
最终结果为均方误差MSE
计算梯度:
使用PyTorch可以自动计算损耗的梯度或导数w.r.t. 权重和偏差,因为requires_grad被设置为True。
计算权重梯度:
重置梯度:
使用梯度下降调整重量和偏差
我们将使用梯度下降优化算法减少损失并改进我们的模型,该算法具有以下步骤:
生成预测
计算损失
计算梯度w.r.t权重和偏差
通过减去与梯度成比例的小量来调整权重
将渐变重置为零
让我们逐步实现上述步骤
接下来分别用代码表示:
最终效果:
再来看看损失:
说明有进步。那么我们就重复上面的过程,把损失减到最小。每词重复,我们成为1个epoch。我们先来100个epoch:
再看看效果:
损失很低了。print一下结果:
用PyTorch内置函数实现线性回归
了解了上述原理后,我们就可以用PyTorch内置的函数,简化我们的工作量。
接下来我们创建一个TensorDataset和一个DataLoader:
TensorDataset允许我们使用数组索引表示法(上面代码中的[0:3])访问训练数据的一小部分。 它返回一个元组(或对),其中第一个元素包含所选行的输入变量,第二个元素包含目标。
用for-in循环就可以了
用nn.linear自动初始化
刚开始我们是手动随机输入的初识权重。现在我们可以使用nn.linear自动完成初始化工作。
对于我们的线性回归模型,我们有一个权重矩阵和一个偏差矩阵。
接下来我们重复上面的流程,首先通过损失函数计算出差距,接着不断的降低损失。
以及用内置损失函数mse_loss:
优化的时候,我们可以使用优化器optim.SGD,不用手动操作模型的权重和偏差。
SGD代表随机梯度下降。 它被称为随机因为样本是分批选择的(通常是随机抽样)而不是单个组。
训练模型,思路上面已经讲过了,直接看代码
上面要注意的一些事项:
我们使用前面定义的数据加载器来获取每次迭代的batch数据
我们不是手动更新参数(权重和偏差),而是使用opt.step来执行更新,而使用opt.zero_grad将梯度重置为零
我们还添加了一个日志语句,用于打印每10个时期最后一批数据的丢失,以跟踪训练的进度。 loss.item返回存储在损失tensor中的实际值
来100个epoch
结果:
对比一下:
Commit并上传
现在可以将你的代码上传到我们的Notebook了。看了这么多代码估计你可能已经忘记怎么Commit了。
用PyTorch的逻辑回归实现图像分类
数据集来自MNIST手写数字数据库。它由手写数字(0到9)的28px乘28px灰度图像以及每个图像的标签组成。
导入torch、torchvision和MNIST
看一下lengh:
这个数据集有60000张图片,可以用来训练模型。还有一个10,000个图像的附加测试集,可以通过将train = False传递给MNIST类来创建。
该图像是PIL.Image.Image类的对象,由28x28图像和标签组成。PIL是Python成像库Pillow。
我们可以使用matplotlib在Jupyter中查看图像,matplotlib是Python中数据科学的事实绘图和图形库。
先看数据集里的几个图片:
除了导入matplotlib之外,还添加了一个特殊声明%matplotlib inline,告知Jupyter要在Notebook中绘制图形。
没有这个声明的话,Jupyter将在弹出窗口中显示图像。以%开头的语句称为IPython magic命令,用于配置Jupyter本身的行为。
PyTorch无法直接处理图像,需要将图像转换成tensor。
PyTorch数据集允许我们指定一个或多个转换函数,这些函数在加载时应用于图像。
torchvision.transforms包含许多这样的预定义函数,我们将使用ToTensor变换将图像转换为PyTorchtensor。
现在图像转换为1x28x28的tensor。第一个维度用于跟踪颜色通道。由于MNIST数据集中的图像是灰度级的,因此只有一个通道。 其他数据集具有彩色图像,在这种情况下有3个通道:红色,绿色和蓝色(RGB)。
让我们看一下tensor内的一些样本值:
0表示黑色,1表示白色,中间的值表示不同的灰度。嗯还可以使用plt.imshow将tensor绘制为图像。
请注意,我们只需要将28x28矩阵传递给plt.imshow,而不需要通道尺寸。
我们还传递了一个颜色映射(cmap ='gray'),表示我们想要查看灰度图像。
训练和验证数据集
在构建真实世界的机器学习模型时,将数据集分成3个部分是很常见的:
训练集:用于训练模型,即计算损失并使用梯度下降调整模型的权重
验证集:用于在训练时评估模型,调整超参数(学习率等)并选择最佳版本的模型
测试集:用于比较不同的模型或不同类型的建模方法,并报告模型的最终准确性
在MNIST数据集中,有60,000个训练图像和10,000个测试图像。测试集是标准化的,以便不同的研究人员可以针对同一组图像报告其模型的结果。
由于没有预定义的验证集,我们必须手动将60,000个图像拆分为训练和验证数据集
让我们定义一个函数,随机选择验证集的图像的给定部分。
split_indices随机地混洗数组索引0,1,... n-1,并从中为验证集分离出所需的部分。
在创建验证集之前对索引进行混洗是很重要的,因为训练图像通常由目标标签排序,即0s的图像,然后是1s的图像,接着是2s的图像,依此类推。
如果我们仅通过选择最后20%的图像来选择20%的验证集,则验证集将仅包括8s和9s的图像,而训练集将不包含8s和9s的图像,这样就不可能训练一个好的模型。
我们随机调整了指数,并选择了一小部分(20%)作为验证集。
模型
现在我们已经准备好了数据加载器,我们可以定义我们的模型。
逻辑回归模型几乎与线性回归模型相同,即存在权重和偏差矩阵,并且使用简单矩阵运算(pred = x @ w.t()+ b)获得输出
就像我们使用线性回归一样,我们可以使用nn.Linear来创建模型,而不是手动定义和初始化矩阵
由于nn.Linear期望每个训练示例都是一个tensor,因此每个1x28x28图像tensor需要在传递到模型之前被展平为大小为784(28 * 28)的tensor
每个图像的输出是大小为10的tensor,tensor的每个元素表示特定目标标记(即0到9)的概率。 图像的预测标签只是具有最高概率的标签
我们看看权重和偏差
虽然这里我们的额参数量编程了7850个,但总的思路是类似的。我们从的数据集中,取得第一个batch,包含100张图片,传递给模型。
直接输出的话报错。因为我们的输入数据的形状不正确。我们的图像形状为1x28x28,但我们需要它们是784的矢量。
即我们需要将它们"展评"。我们将使用一个tensor的.reshape方法,这将允许我们有效地"查看"每个图像作为平面向量,而无需真正更改基础数据。
要在我们的模型中包含此附加功能,我们需要通过从PyTorch扩展nn.Module类来定义自定义模型。
在__init__构造函数方法中,我们使用nn.Linear实例化权重和偏差。
在我们将一批输入传递给模型时调用的forward方法中,我们将输入tensor展平,然后将其传递给self.linear。
xb.reshape(-1,28 * 28)向PyTorch指明,我们想要具有两个维度的xbtensor的视图,其中沿第二维度的长度是28 * 28(即784)。
.reshape的一个参数可以设置为-1(在这种情况下是第一个维度),让PyTorch根据原始tensor的形状自动计算出来。
请注意,模型不再具有.weight和.bias属性(因为它们现在位于.linear属性中),但它确实有一个.parameters方法,该方法返回包含权重和偏差的列表,并且可以使用PyTorch优化器。
我们的新自定义模型可以像以前一样使用。 让我们看看它是否有效。
对于100个输入图像中的每一个,我们得到10个输出,每个类别一个。 如前所述,我们希望这些输出表示概率,但是为此,每个输出行的元素必须介于0到1之间并且加起来为1,这显然不是这里的情况。
要将输出行转换为概率,我们使用softmax函数,它具有以下公式:
首先,我们将输出行中的每个元素yi替换为e ^ yi,这使得所有元素都为正,然后我们将每个元素除以所有元素的总和,以确保它们加起来为1。
虽然很容易实现softmax函数,我们将使用PyTorch中提供的实现,因为它适用于多维tensor(在我们的例子中是输出行列表)。
softmax函数包含在torch.nn.functional包中,并要求我们指定必须应用softmax的维度。
最后,我们可以通过简单地选择每个输出行中具有最高概率的元素的索引来确定每个图像的预测标签。
这是使用torch.max完成的,它返回最大元素和沿tensor的特定维度的最大元素的索引。
上面打印的数字是第一批训练图像的预测标签。 我们将它们与实际标签进行比较。
显然,预测标签和实际标签完全不同。这是因为我们已经开始使用随机初始化的权重和偏差。
我们需要训练模型,即使用梯度下降调整权重以做出更好的预测。
评估度量和损失函数
与线性回归一样,我们需要一种方法来评估模型的执行情况。一种自然的方法是找到正确预测的标签百分比,即预测的准确性。
==运算符执行具有相同形状的两个tensor的逐元素比较,并返回相同形状的tensor,对于不相等的元素包含0,对于相等的元素包含1。
将结果传递给torch.sum会返回正确预测的标签数。最后,我们除以图像总数来获得准确性。
让我们计算第一批数据的当前模型的准确性。显然,我们预计它会非常糟糕。
虽然精度是我们(人类)评估模型的好方法,但它不能用作使用梯度下降优化模型的损失函数,原因如下:
这不是一个可区分的功能。torch.max和==都是非连续和非可微操作,因此我们无法使用精度来计算重量和偏差的梯度
它没有考虑模型预测的实际概率,因此无法为渐进式改进提供足够的反馈
由于这些原因,准确性是分类的一个很好的评估指标,但不是一个好的损失函数。 分类问题常用的损失函数是交叉熵,其具有以下公式:
虽然它看起来很复杂,但实际上非常简单:
对于每个输出行,选择正确标签的预测概率。例如。如果图像的预测概率是[0.1,0.3,0.2,...]并且正确的标签是1,我们选择相应的元素0.3并忽略其余的
然后,取所选概率的对数。如果概率很高,即接近1,则其对数是非常小的负值,接近于0。如果概率低(接近0),则对数是非常大的负值。我们还将结果乘以-1,结果是预测不良的损失的大正值 最后,获取所有输出行的交叉熵的平均值,以获得一批数据的总体损失
与准确度不同,交叉熵是一种连续且可微分的函数,它还为模型中的渐进改进提供了良好的反馈(正确标签导致较低损失的概率略高)。这使它成为损失函数的一个很好的选择。
PyTorch提供了一种有效且张量友好的交叉熵实现,作为torch.nn.functionalpackage的一部分。
此外,它还在内部执行softmax,因此我们可以直接传递模型的输出而不将它们转换为概率。
由于交叉熵是在所有训练样本上平均的正确标签的预测概率的负对数,因此一种解释结果数的方法,例如,2.23是e ^ -2.23,平均值约为0.1,作为正确标签的预测概率。降低损失,改善模型。
优化
我们将使用optim.SGD优化器在训练期间更新权重和偏差,但学习率更高,为1e-3。
batch大小,学习率等参数需要在训练机器学习模型时提前选取,并称为超参数。
选择正确的超参数对于在合理的时间内训练准确的模型至关重要,并且是研究和实验的活跃领域。随意尝试不同的学习率,看看它如何影响训练过程。
训练模型
现在我们已经定义了数据加载器,模型,损失函数和优化器,我们已准备好训练模型。
训练过程几乎与线性回归相同。但是,我们将增加我们之前定义的拟合函数,以使用每个epoch末尾的验证集来评估模型的准确性和损失。
我们首先定义一个函数loss_batch:
计算一批数据的损失
如果提供了优化程序,则可以选择执行梯度下降更新步骤
可选地使用预测和实际目标来计算度量(例如,准确度)
优化器是一个可选参数,以确保我们可以重用loss_batch来计算验证集上的损失。
我们还将batch处理的长度作为结果的一部分返回,因为它在组合整个数据集的损失/度量时非常有用。
接下来,我们定义一个函数evaluate,它计算验证集的总体损失。
如果不能立即清楚此函数的作用,请尝试在单独的单元格中执行每个语句,然后查看结果。
我们还需要重新定义精确度以直接操作整批输出,以便我们可以将其用作拟合度量。
请注意,我们不需要将softmax应用于输出,因为它不会更改结果的相对顺序。
这是因为e ^ x是增加函数,即如果y1> y2,则e ^ y1> e ^ y2,并且在对值求平均值以获得softmax之后也是如此。
让我们看看模型如何使用初始权重和偏差集在验证集上执行。
初始准确度低于10%,这是人们对随机初始化模型的预期(因为它有十分之一的机会通过随机猜测获得标签)。
另请注意,我们使用.format方法和消息字符串仅打印小数点后的前四位数。
我们现在可以使用loss_batch和evaluate轻松定义拟合函数。
我们现在准备训练模型。 让我们训练5个epoch并观察结果。
不错哦!来更多点eploch
线图更直观的显示一下效果
从上面的图片中可以清楚地看出,即使经过很长时间的训练,该模型也可能不会超过90%的准确度阈值。
一个可能的原因是学习率可能太高。模型的参数可能会围绕具有最低损耗的最佳参数集"弹跳"。
您可以尝试降低学习速度和训练几个epoch,看看它是否有帮助。
更可能的原因是该模型不够强大。如果你还记得我们的初始假设,我们假设输出(在这种情况下是类概率)是输入(像素强度)的线性函数,通过对权重矩阵执行矩阵乘法并添加偏差来获得。
这是一个相当弱的假设,因为图像中的像素强度和它所代表的数字之间可能实际上不存在线性关系。
虽然它对于像MNIST这样的简单数据集(使我们达到85%的准确度)工作得相当好,但我们需要更复杂的模型来捕捉图像像素和标签之间的非线性关系,以便识别日常物品,动物等复杂任务。
使用单个图像进行测试
虽然到目前为止我们一直在跟踪模型的整体精度,但在一些样本图像上查看模型的结果也是一个好主意。
让我们用10000个图像的预定义测试数据集中的一些图像测试我们的模型。 我们首先使用ToTensor变换重新创建测试数据集。
数据集中单个图像的样本:
让我们定义一个辅助函数predict_image,它返回单个图像张量的预测标签。
img.unsqueeze只是在1x28x28张量的开始处添加另一个维度,使其成为1x1x28x28张量,模型将其视为包含单个图像的批处理。
通过收集更多的训练数据,增加/减少模型的复杂性以及更改超参数,确定我们的模型表现不佳的位置可以帮助我们改进模型。
最后,让我们看看测试集上模型的整体损失和准确性。
我们希望这与验证集上的准确度/损失相似。如果没有,我们可能需要一个更好的验证集,它具有与测试集类似的数据和分布(通常来自现实世界数据)。
保存并加载模型
由于我们已经长时间训练模型并获得了合理的精度,因此将权重和偏置矩阵保存到磁盘是个好主意,这样我们可以在以后重用模型并避免从头开始重新训练。以下是保存模型的方法。
.state_dict方法返回一个OrderedDict,其中包含映射到模型右侧属性的所有权重和偏置矩阵。
要加载模型权重,我们可以实例化MnistModel类的新对象,并使用.load_state_dict方法。
正如完整性检查一样,让我们验证此模型在测试集上具有与以前相同的损失和准确性。
好了。大功告成了。又到了我们的commit环节。以防你已经忘记了怎么操作: