1.2 初试PyTorch神经网络
1.2.1 MNIST图像数据集
MNIST数据集是一组常见的图像,常用于测评和比较机器学习算法的性能。其中6万幅图像用于训练机器学习模型,另外1万幅用于测试模型。 这些大小为28像素× 28像素的单色(monochrome) 图像没有颜色。每个像素是一个0~255的数值,表示该像素的明暗度。
1.2.2 获取MNIST数据集
通过以下链接将MNIST数据集下载到我们的计算机本地硬盘。 训练数据: https://pjreddie.com/media/files/mnist_train.csv。 测试数据: https://pjreddie.com/media/files/mnist_test.csv。
MNIST的DataFrame有60 000行。这对应60 000幅训练图像。每一行数据包含785个值,第一个值是图像所表示的数字,其余的784个值是图像(尺寸为28像素× 28像素)的像素值。
我们使用通用的matplotlib库来显示图像。在下面的代码中,我们导入matplotlib库的pyplot包。 运行更新后的单元格,pyplot即可使用。 我们来看下面的代码。 首先,从MNIST数据中选取我们感兴趣的图像。第一幅图像,也就是第一行,可通过row = 0选定。df.iloc[row]选择数据集的第一行并赋值给变量data。 接着,我们从该行中选择第一个数字,并将其命名为label,也就是标签。 然后选择该行中其余的784个值,并将它们重新映射为一个28× 28的正方形数组。我们将这个数组赋值给变量img,因为它是图像。接着,我们将数组绘制为位图,并在标题中显示之前提取的标签。绘制位图的imshow()函数有很多标签选项,我们使用的两个选项分别指示pyplot无须平滑像素以及指定调色板的颜色为蓝色。
现在,我们看到了MNIST训练数据集中的第一幅图像。它看起来像5,标签也确认是5。
1.2.4 简单的神经网络
在开始编写神经网络代码之前,让我们先画出希望实现的目标。下图显示了我们的起始点和终点。 起始点是一幅MNIST数据集中的图像,它的像素个数为28×28=784。这意味着我们的神经网络的第一层必须有784个节点。对于输入层的大小,我们没有太多的选择。 可以选择的是最后的输出层。它需要回答“这是什么数字”的问题。答案是0~9的任意一个数字,也就是10种不同输出。最直接的解决方案是,为每一个可能的类别分配一个节点。
现在,我们准备好用PyTorch实现这个网络设计了。PyTorch简化了构建和运行神经网络的流程。为此,我们需要遵循PyTorch的编码规则。
当创建神经网络类时,我们需要继承PyTorch的torch.nn模块。这样一来,新的神经网络就具备了许多PyTorch的功能,如自动构建计算图、查看权重以及在训练期间更新权重等。 将torch.nn模块作为nn导入,是一种常见的命名方式。 接着,我们开始构建神经网络类(class)。下面的代码展示了一个名为Classifier的类,它继承了nn.Module。
class Classifier(nn.Module):
def __init__(self):
# 初始化PyTorch父类
super().__init__()
init(self)是一个特殊的函数,当我们从一个类中创建对象(object)时需要调用它。它通常用于设置一个对象,为被调用做好准备。 读者可能听说过它的另一个名字——构造函数(constructor)。这是一个很形象的名称。这里,super().init_()语句看似很神秘,但事实上只不过是调用了父类的构造函数。可以说,PyTorch.nn模块会为我们设置分类器,很简单吧。
现在,我们开始设计神经网络的结构。设计网络结构有多种方法。对于简单的网络,我们可以使用nn.Sequential(),它允许我们提供一个网络模块的列表。模块必须按照我们希望的信息传递顺序添加到容器中。 我们看到nn.Sequential()中包括以下模块。
- nn.Linear(784, 200)是一个从784个节点到200个节点的全连接映射。这个模块包含节点之间链接的权重,在训练时会被更新。
- nn.Sigmoid()将S型逻辑激活函数应用于前一个模块的输出,也就是本例中200个节点的输出。
- nn.Linear(200, 10)是将200个节点映射到10个节点的全连接映射。它包含中间隐藏层与输出层10个节点之间所有链接的权重。
- nn.Sigmoid()再将S型逻辑激活函数应用于10个节点的输出。其结果就是网络的最终输出。
有读者可能会问:nn.Linear因何得名?这是因为,当数值从输入端传递到输出端时,该模块对它们应用了Ax + B形式的线性函数。这里,A为链接权重,B为偏差(bias)。这两个参数都会在训练时被更新。它们也被称为可学习参数 (learnable parameter)。
我们已经定义了神经网络的模块以及正向的信息传导。不过,我们还没有定义如何计算误差以及用误差更新网络的可学习参数。 定义网络误差的方法有多种,PyTorch为常用的方法提供了方便的函数支持。其中,最简单的是均方误差(mean squared error)。均方误差先计算每个输出节点的实际输出和预期输出之差的平方,再计算平均值。PyTorch将其定义为 torch.nn.MSELoss()。
我们可以选择这个误差函数,并在构造函数中创建一个变量。 我们发现,“误差函数”(error function)和“损失函数”(loss function)这两个词常被互换使用,通常这是可以接受的。如果希望更精确一些,“误差”单纯指预期输出和实际输出之间的差值,而“损失”是根据误差计算得到的,需要考虑具体需要解决的问题。
我们需要使用误差,更准确地说是损失,来更新网络的链接权重。同样地,更新权重的方法有多种,PyTorch提供了函数来支持常用的几种方法。我们先使用一个《Python神经网络编程》中提到的简单方法——随机梯度下降(stochastic gradient descent,SGD),将学习率设置为0.01。
在上面的代码中,我们把所有可学习参数都传递给SGD优化器。这些参数可以通过self.parameters()访问,这也是PyTorch提供的功能之一。 PyTorch假定通过一个forward()方法向网络传递信息。我们需要自己创建一个forward()方法,但它可以非常简短。 这里,我们只将输入传递给self.model(),它由nn.Sequential()定义。模型的输出直接返回给forward()的主调函数。
下面我们回顾一下到目前为止的进展。
- 通过继承nn.Module,我们创建了一个神经网络类。它从nn.Module中继承了训练神经网络所需的大部分功能。
- 我们定义了处理信息的神经网络模块。对于简单的神经网络,我们选择使用精简的nn.Sequential方法。
- 我们定义了损失函数和更新网络可学习参数的优化器。 最后,我们添加了一个forward()函数,PyTorch会通过它将信息传递给网络。
- 现在,我们的神经网络类应该是下面这样的:
class Classifier(nn.Module):
def __init__(self):
# initialise parent pytorch class 初始化PyTorch父类
super().__init__()
# define neural network layers
self.model = nn.Sequential(
nn.Linear(784, 200),
nn.Sigmoid(),
nn.Linear(200, 10),
nn.Sigmoid()
)
# create loss function 创建损失函数
self.loss_function = nn.MSELoss()
# create optimiser, using simple stochastic gradient descent 创建优化器,使用简单的梯度下降
self.optimiser = torch.optim.SGD(self.parameters(), lr=0.01)
pass
def forward(self, inputs):
# simply run model 直接运行模型
return self.model(inputs)
接下来,我们该如何训练这个网络呢? 我们需要一个像forward()函数一样的train()函数吗? 实际上,这不是必需的。PyTorch允许我们按自己的想法构建网络的训练代码。 为了代码的整洁,我们选择与forward()保持一致,创建一个train()函数。 train()既需要网络的输入值,也需要预期的目标值。这样才可以与实际输出进行比较,并计算损失值。 train()函数首先做的,是使用forward()函数传递输入值给网络并获得输出值。 我们之前定义的损失函数在这里是用来计算损失值的。可以看出,PyTorch简化了计算过程。我们只需要向该函数提供网络的输出值和预期目标值即可。
下一步,是使用损失来更新网络的链接权重。 这3个步骤算得上是所有PyTorch神经网络的精髓所在。下面我们一步一步来具体讨论。
- 首先,optimiser.zero_grad()将计算图中的梯度全部归零。
- 其次,loss.backward()从loss函数中计算网络中的梯度。
- 最后,optimiser.step()使用这些梯度来更新网络的可学习参数。
在每次训练网络之前,我们需要将梯度归零。否则,每次loss. backward()计算出来的梯度会累积。
在1.1.4节,我们使用backward()函数计算了一个简单网络的梯度。在这里,backward()函数的用法是一样的。我们可以把计算图的最终节点看作损失函数。该函数对每个进入损失的节点计算梯度。这些梯度是损失随着每个可学习参数的变化。 优化器利用这些梯度,逐步(step)沿着梯度更新可学习参数。
1.2.5 可视化训练
跟踪训练的一种方法是监控损失。在train()中,我们在每次计算损失值时,将副本保存在一个列表里。 下面的代码在神经网络类的构造函数中创建一个初始值为0的计数器(counter)以及一个名为progress的空列表。 在train()函数中,我们可以每隔10个训练样本增加一次计数器的值,并将损失值添加进列表的末尾。 在上述代码中,% 10表示除以10之后的余数,当计数器为10、20、30等时,余数为0。这里使用的item()函数只是为了方便展开一个单值张量,获取里面的数字。 我们可以在每10 000次训练后打印计数器的值,这样可以了解训练进展的快慢。 要将损失值绘制成图,我们可以在神经网络类中添加一个新函数plot_progress()。 这段代码看起来很复杂,但其实只有两行。第一行将损失值列表progress转换为一个pandas DataFrame,这样方便我们绘制图。 第二行使用plot()函数的选项,调整图的设计和风格。.
1.2.6 MNIST数据集类
PyTorch使用torch.utils.data.DataLoader实现了一些实用的功能,比如自动打乱数据顺序、多个进程并行加载、分批处理等,需要先将数据载入一个torch.utils.data.Dataset对象。
为了简单起见,我们暂时不需要打乱数据顺序或分批处理。但是,我们仍会使用torch.utils.data.Dataset类,以积累使用PyTorch的经验。
通过以下代码导入PyTorch的torch.utils.data.Dataset类。
from torch.utils.data import Dataset
当我们从nn.Module继承一个神经网络类时,需要定义forward()函数。同样地,对于继承自Dataset的数据集,我们需提供以下两个特殊的函数。
- len(),返回数据集中的项目总数。
- getitem(),返回数据集中的第n项。
接下来,我们会创建一个MnistDataset类,并提供__len__()方法,允许PyTorch通过len(mnist_dataset)获取数据集的大小。 同时,我们也会提供__getitem__(),允许我们通过索引获取项目,例如使用mnist_dataset[3]访问第4项。 下面是MnistDataset类的具体定义。 首先,在创建该类的一个对象时,csv_file被读入一个名为data_df的pandas DataFrame。
- len() 函数的作用是返回DataFrame的大小。这很简单!
- getitem()函数则比较有趣。就像我们之前对MNIST数据所做的实验一样,我们从数据集中的第index项中提取一个标签(label)。
接着,我们创建了一个维度为10的张量变量target来表示神经网络的预期输出。除了与标签相对应的项是1之外,其他值皆为0。比如,标签0所对应的张量是[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],而标签4所对应的张量是[0, 0, 0, 0, 1, 0, 0, 0, 0, 0]。这种表示方法叫独热编码 (one-hot encoding)。
然后,我们以像素值创建一个张量变量image_values。所有像素值都被除以255,结果值的范围是0~1。
最后,getitem() 返回label、image_values和target (标签、像素值和目标张量)一共3个值。
即使PyTorch不需要,我们也可以为MnistDataset类添加一个制图方法,以方便查看我们正在处理的数据。为此,我们需要跟之前一样,导入matplotlib.pyplot库。
让我们检查一下到目前为止是否一切正常。首先,我们从类中创建一个数据集对象,并将其CSV文件位置传递给它。 我们知道类构造函数将CSV文件中的数据加载到pandas DataFrame中。让我们使用plot_image()函数绘制数据集中的第10幅图像。第10幅图像的索引是9,因为第一幅的索引是0。 我们应该看到一个手写数字图像“4”。标签也告诉我们它应该是“4”。 这证明我们的数据集类可以正确地加载数据了。
1.2.7 训练分类器
1.2.8 查询神经网络
1.2.9 简易分类器的性能
要知道我们的神经网络对图像分类的表现如何,一种直接的方法是对MNIST测试数据集中所有10 000幅图像进行分类,并记录正确分类的样本数。分类是否正确可以通过比较网络输出和图像的标签来分辨。
在以下代码中,分数score的初始值为0。接着遍历测试数据,并在每次网络输出与标签匹配时加分。
# test trained neural network on training data
score = 0
items = 0
for label, image_data_tensor, target_tensor in mnist_test_dataset:
answer = C.forward(image_data_tensor).detach().numpy()
if (answer.argmax() == label):
score += 1
pass
items += 1
pass
print(score, items, score/items)
answer.argmax()语句的作用是输出张量answer中最大值的索引。如果第一个值是最大的,则argmax是0。这是回答“哪个节点的值最大”的一种推荐方法。
下面打印最后得分以及神经网络答对的样本占总样本的分数。 从上图中可以看到,模型的最后分数约为87%。考虑到这是一个简单的网络,这个分数还不算太差。
|