published on in Python

人工神经网络・多层感知器

返回教程主页

上篇 人工神经网络・Softmax多分类

之前我们都是采用手工构造的数据来进行学习,这一次我们将处理一个真实场景的问题——手写数字识别。

具体问题是这样的: 我们需要构造一个神经网络模型来学习如何识别0-9这10个阿拉伯数字「包括0和9」。

我们会用到机器学习数据集中著名的MNIST数据集,该数据集收集了70000个手写阿拉伯数字图像,其中训练集有60000个,测试集有10000个,每个数字图像的大小为高28像素宽28像素。

以下为部分数据的内容:

mnist_show.png

这个问题要求我们的模型能够接收一个图像作为输入然后输出该图像是0-9中的哪一个数字,显然这是一个多分类问题,我们可以使用softmax。

引入所需的库

import torch
torch.manual_seed(3)
import torch.nn as nn
import torch.nn.functional as F
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor
from torch.utils.data import DataLoader
  • torch.nn包含大量PyTorch中神经网络相关的类;
  • MNIST是torchvision提供的MNIST数据工具,可以帮助我们下载管理MNIST数据;
  • ToTensor是torchvision提供的数据转换器,可以将图像转换为PyTorch的张量对象;
  • DataLoader是PyTorch的数据加载器,可以帮助我们处理数据加载任务;

载入数据集

我们需要创建训练集与数据集:

train_set    = MNIST('.', train=True, download=False, transform=ToTensor())
test_set     = MNIST('.', train=False, download=False, transform=ToTensor())

MNIST的第一个参数是数据集下载后存放的路径;参数train为真则载入训练集,若为假则载入测试集;参数download为真则会从网络进行下载,反之则不下载;参数transform可以指定一个数据转换器应,我们设置为ToTensor(),这可以为我们将图像转换为张量对象。

可以使用索引的方法取出数据集中的数据,例如:

data, label = train_set[0]

tain_set[0]取出第0份数据集中的数据,包括一张图像的张量data,以及该图像的标识label。其中张量data的元素值在0到1之间,label为一个标量数字「对应图像所显示的数字」。

构造数据加载器

我们需要对训练集与测试集分别构造数据加载器:

train_loader = DataLoader(train_set, batch_size=128, shuffle=True)
test_loader  = DataLoader(test_set, batch_size=128, shuffle=False)

DataLoader的第一个参数指向我们的数据集;参数设置batch_size=128表示每次对Dataloader进行迭代它会取128份数据;参数shuffle设置为真则会对数据集顺序进行打乱,反之则不会打乱顺序。

我们可以在for循环中用数据加载器迭代取出数据集中的数据:

for inputs, targets in train_loader:
	...

上述代码中inputs为128张图像的张量所组成的张量对象,我们将其作为模型的输入;targets为128个数字标量所组成的张量对象,我们将其作为损失函数的一个参数。

构建多层感知器模型

我们使用torch.nn提供的神经网络工具进行模型的构建,这样可以极大的方便模型构建的工作:

mlp = nn.Sequential(
    nn.Flatten(),
    nn.Linear(28 * 28 * 1, 256),
    nn.Sigmoid(),
    nn.Linear(256, 10),
    nn.Softmax(dim=1))

nn.Sequential可以产生一个序列结构的模型,我们将其命名为mlp——多层感知器。在mlp中有如下被叫做神经网络层的元素:

nn.Flatten(),
nn.Linear(784, 256),
nn.Sigmoid(),
nn.Linear(256, 10),
nn.Softmax(dim=1)

数据在进入mlp模型后会按顺序经过这些层的处理并最后进行输出。

数据首先经过的是nn.Flatten()层,最后经过nn.Softmax(dim=1)层进行输出。

  1. nn.Flatten()将图像张量进行拉平操作,使得模型的输入编程一个nx784的张量,n为图像数量,784为每一个图像的像素数量;
  2. nn.Linear(784, 256)实现之前的矩阵线性变换 $y = XW + b$,第一个参数为输入维度,第二个参数为输出维度;
  3. nn.Sigmoid()使用sigmoid函数作为激活函数;
  4. nn.Linear(256, 10)实现之前的矩阵线性变换 $y = XW + b$,第一个参数为输入维度,第二个参数为输出维度;
  5. nn.Softmax(dim=1)使用softmax函数作为激活函数;

由于存在一个以上的nn.Linear与激活函数的组合,因此该结构可以被叫做多层感知器。

该序列模型接收参数并输出的方式很简单:

outputs = mlp(inputs)

设定优化器

由于我们并没有像之前那样显式的定义线性变换层的权重W以及偏置b,因此,为了方便,我们直接使用PyTorch提供的优化器组建来处理权重与偏置的更新任务:

optimizer = torch.optim.Adam(mlp.parameters(), lr=0.001)

torch.optim.Adam是一种常见的优化器。该优化器的第一个参数指向模型所有需要更新优化的权重与偏置,我们使用mlp.parameters()获取它们;参数lr为学习率,这里我们设置为0.001

训练并测试模型

我们需要在for循环中进行模型的训练与测试:

for epoch in range(10):
    print('training...')
    mlp = mlp.train()
    for step, (inputs, targets) in enumerate(train_loader):
        outputs = mlp(inputs)
        loss = F.cross_entropy(outputs, targets)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if step % 100 == 0:
            with torch.no_grad():
                compares = torch.argmax(outputs, dim=1) == targets
                accuracy = torch.mean(compares.type(torch.float32)).item()
            print(f'{epoch}:{step}, loss={loss.item()}, accuracy={accuracy}')

    print('testing...')
    mlp = mlp.eval()
    losses = []
    count = 0
    with torch.no_grad():
        for step, (inputs, targets) in enumerate(test_loader):
            outputs = mlp(inputs)
            loss = F.cross_entropy(outputs, targets)
            losses.append(loss.item())
            compares = torch.argmax(outputs, dim=1) == targets
            count += torch.sum(compares.type(torch.long)).item()
    loss = sum(losses) / len(losses)
    accuracy = count / len(test_set)
    print(f'loss={loss}, accuracy={accuracy}')

首先我们看到训练部分:

print('training...')
mlp = mlp.train()
for step, (inputs, targets) in enumerate(train_loader):
	outputs = mlp(inputs)
	loss = F.cross_entropy(outputs, targets)
	optimizer.zero_grad()
	loss.backward()
	optimizer.step()

	if step % 100 == 0:
		with torch.no_grad():
			compares = torch.argmax(outputs, dim=1) == targets
			accuracy = torch.mean(compares.type(torch.float32)).item()
		print(f'{epoch}:{step}, loss={loss.item()}, accuracy={accuracy}')
  • 我们在每一轮训练开始前将模型设置为训练模式: mlp = mlp.train()
  • 运行代码outputs = mlp(inputs)进行正向传播;
  • 使用交叉熵损失评估函数F.cross_entropy计算损失率;
  • 在进行反向传播loss.backward()前,我们需要调用优化器清除原宥的梯度optimizer.zero_grad()。在执行反向传播后我们调用optimizer.step()进行参数更新;
  • 我们每隔100步打印输出一下损失率loss以及准确率accuracy

接下来我们看到测试部分:

print('testing...')
mlp = mlp.eval()
losses = []
count = 0
with torch.no_grad():
	for step, (inputs, targets) in enumerate(test_loader):
		outputs = mlp(inputs)
		loss = F.cross_entropy(outputs, targets)
		losses.append(loss.item())
		compares = torch.argmax(outputs, dim=1) == targets
		count += torch.sum(compares.type(torch.long)).item()
loss = sum(losses) / len(losses)
accuracy = count / len(test_set)
print(f'loss={loss}, accuracy={accuracy}')
  • 我们先将模型转变为推理模式: mlp = mlp.eval()
  • 由于整个测试过程我们都不进行梯度计算,所以整个测试过程都可以放在with torch.no_grad():语句结构内进行;
  • 我们将每一次迭代中模型计算的损失率以及预测正确的图像数量分别存储到lossescount
  • 在for循环结束后,我们计算一下平均的损失率以及准确率然后打印输出。

我们一共完成了10轮训练,其中每一轮训练都会完整的使用所有训练集训练模型一次,并使用完整的测试集测试模型一次。

通过10轮的训练,我们的模型在测试集上的表现如下:

testing...
loss=1.4986492108695115, accuracy=0.9661

其中损失率loss为1.49865左右,准确率达到96.61%。

保存训练后的模型

在完成所有的训练与测试任务后,我们就可以将模型保存到磁盘,以便在未来直接使用:

torch.save(mlp, 'mlp.pt')
mlp = torch.load('mlp.pt')
print(mlp)

使用函数torch.save保存模型为本地文件;使用函数torch.load加载本地磁盘中的模型文件。

完整代码

import torch
torch.manual_seed(3)
import torch.nn as nn
import torch.nn.functional as F
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor
from torch.utils.data import DataLoader


train_set    = MNIST('.', train=True, download=False, transform=ToTensor())
test_set     = MNIST('.', train=False, download=False, transform=ToTensor())

train_loader = DataLoader(train_set, batch_size=128, shuffle=True)
test_loader  = DataLoader(test_set, batch_size=128, shuffle=False)

mlp = nn.Sequential(
    nn.Flatten(),
    nn.Linear(28 * 28 * 1, 256),
    nn.Sigmoid(),
    nn.Linear(256, 10),
    nn.Softmax(dim=1))

optimizer = torch.optim.Adam(mlp.parameters(), lr=0.001)

for epoch in range(0):
    print('training...')
    mlp = mlp.train()
    for step, (inputs, targets) in enumerate(train_loader):
        outputs = mlp(inputs)
        loss = F.cross_entropy(outputs, targets)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if step % 100 == 0:
            with torch.no_grad():
                compares = torch.argmax(outputs, dim=1) == targets
                accuracy = torch.mean(compares.type(torch.float32)).item()
            print(f'{epoch}:{step}, loss={loss.item()}, accuracy={accuracy}')

    print('testing...')
    mlp = mlp.eval()
    losses = []
    count = 0
    with torch.no_grad():
        for step, (inputs, targets) in enumerate(test_loader):
            outputs = mlp(inputs)
            loss = F.cross_entropy(outputs, targets)
            losses.append(loss.item())
            compares = torch.argmax(outputs, dim=1) == targets
            count += torch.sum(compares.type(torch.long)).item()
    loss = sum(losses) / len(losses)
    accuracy = count / len(test_set)
    print(f'loss={loss}, accuracy={accuracy}')

torch.save(mlp, 'mlp.pt')
mlp = torch.load('mlp.pt')
print(mlp)

下篇 人工神经网络・ReLU激活函数