学习了Andrew NG的机器学习系列课程,下面我们来训练一个神经网络的模型。
设立目标:
- 找一组数据
- 定义一个网络
- 训练网络参数(假设函数、损失函数、随即梯度下降)
- 测试网络效果
下面的例子使用pytorch的图像分类经典示例,pytorch很适合新人上手,搭建环境方便,api调用简单。
训练一个图像分类器
一、找一组数据
import torch
import torchvision
import torchvision.transforms as transforms
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
示例中使用了CIFAR-10的数据集。
CIFAR-10数据集由10个类的60000个32x32彩色图像组成,每个类有6000个图像。有50000个训练图像和10000个测试图像。
数据集分为五个训练批次和一个测试批次,每个批次有10000个图像。测试批次包含来自每个类别的恰好1000个随机选择的图像。训练批次以随机顺序包含剩余图像,但一些训练批次可能包含来自一个类别的图像比另一个更多。总体来说,五个训练集之和包含来自每个类的正好5000张图像。
以下是数据集中的类,以及来自每个类的10个随机图像:
除了CIFAR-10,CIFAR-100也是公开的数据集,包含100个类目。
pytorch提供的torchvision包已经对CIFAR-10做了封装,提供简单易用的API来下载、加载数据集,torchvision.datasets.CIFAR10可以下载并且对图片数据做预处理,示例中将01归一化至-0.50.5。数据集大约几十M,如果无法直接下载(墙),可以直接加载本地./data下的cifar-10-batches-py目录,将download参数改为false。
pytorch的工具包提供的数据集加载函数DataLoader,可以通过shuffle来控制数据集是否被打乱,示例中训练集是被打乱的,测试集则没有,打乱的数据集在重复训练参数收敛后有较好的模型泛化,固定顺序的数据集则在每次预测后得到相同的结果,适合测试复现问题。值得说的是DataLoader函数中的batch_size参数,这个参数来控制一次加载的图片个数,前面Andrew课程中损失函数是针对整个数据集来计算的,而这里batch_size则是每次计算损失函数的图片数量。训练时将训练集拆分为N个子集,这样可以减少每次lost的计算量,而且使模型泛化更好。
二、定义一个神经网络
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
net = Net()
神经网络可以通过 torch.nn 包来构建。一个 nn.Module 包括层和一个方法 forward(input) 它会返回输出(output)。
上面代码定义的网络如下:
nn.Conv2d(3, 6, 5)
3是输入数据的channel,6是输出channel,5是卷积核大小。这里注意输出channel和输入channel不是一个维度,假设输入channel是3,输出channel是6,卷积核是5x5,则conv计算时会生成6组filter,每组filter有3个channel(同输入channel),卷积核为5x5,这样假设一份32x32的三通道数据输入,会分别与6组filter在3个通道上做5x5卷积,经过计算,数据从32x32x3变化到28x28x3x6。
self.pool = nn.MaxPool2d(2, 2)
池化,pooling 是仿照人的视觉系统进行降维(降采样),用更高层的抽象表示图像特征。池化层对特征图进行压缩。1.使特征图变小,简化网络计算复杂度,减少下一层的参数和计算量,防止过拟合;2.进行特征压缩,提取特征,保留主要的特征;保持某种不变性。
self.fc1 = nn.Linear(16 * 5 * 5, 120)
全连接层(fully connected layers,FC)在整个卷积神经网络中起到“分类器”的作用。如果说卷积层、池化层和激活函数层等操作是将原始数据映射到隐层特征空间的话,全连接层则起到将学到的“分布式特征表示”映射到样本标记空间的作用
F.relu()
激活函数,Andrew课程中使用sigmoid。
ReLU 的优点:
• 分段线性函数。相比于sigmoid/tanh,ReLU 只需要一个阈值就可以得到激活值,而不用去算一大堆复杂的运算。
• 无饱和问题,明显减轻梯度消失问题
• 深度网络能够进行优化的关键
• 加快训练速度
三、训练网络参数(假设函数、损失函数、随即梯度下降)
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
定义一个损失函数和优化器 让我们使用分类交叉熵Cross-Entropy 作损失函数,动量SGD做优化器。
训练网络 这里事情开始变得有趣,我们只需要在数据迭代器上循环传给网络和优化器 输入就可以。
for epoch in range(2): # loop over the dataset multiple times
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# get the inputs
inputs, labels = data
# zero the parameter gradients
optimizer.zero_grad()
# forward + backward + optimize
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# print statistics
running_loss += loss.item()
if i % 2000 == 1999: # print every 2000 mini-batches
print('[%d, %5d] loss: %.3f' %
(epoch + 1, i + 1, running_loss / 2000))
running_loss = 0.0
print('Finished Training')
输出:
[1, 2000] loss: 2.187
[1, 4000] loss: 1.852
[1, 6000] loss: 1.672
[1, 8000] loss: 1.566
[1, 10000] loss: 1.490
[1, 12000] loss: 1.461
[2, 2000] loss: 1.389
[2, 4000] loss: 1.364
[2, 6000] loss: 1.343
[2, 8000] loss: 1.318
[2, 10000] loss: 1.282
[2, 12000] loss: 1.286
Finished Training
其中,优化器迭代直至梯度下降收敛。
optimizer.step()
示例中,将CIFAR-10训练集中50000张图片,每4张作为一个子集计算lost优化参数,每2000个子集打印一次平均lost,可以看到lost在不断收敛,12500个子集计算完毕后再重新使用原本数据集计算,这时虽然使用相同的数据集,但是数据顺序被shuffle过,lost仍在收敛,但收敛速度下降。
四、测试网络效果
import matplotlib.pyplot as plt
import numpy as np
def imshow(img):
img = img / 2 + 0.5 # unnormalize
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# get some random training images
dataiter = iter(trainloader)
images, labels = dataiter.next()
# show images
imshow(torchvision.utils.make_grid(images, 4))
# print labels
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))
outputs = net(images)
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4)))
输出:
Predicted: cat truck horse deer
50%正确率!
代码中,
plt.imshow(np.transpose(npimg, (1, 2, 0)))
matplotlib.pyplot.imshow接收的是RGB数据,这时候CIFAR-10的tensor数据需要transpose。
_, predicted = torch.max(outputs, 1)
使用测试集来测试网络效果
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (
100 * correct / total))
五、思考
Q:为什么定义这样的网络,有没有更好的网络,怎么评价一个网络
A:示例中的网络结构简单,训练速度快,准去率并不高。一个好的网络需要兼顾效果和性能,效果指标由具体解决的问题来确定,性能则需要考虑最终产品的生产环境、兼顾训练成本。
一定范围内,网络的深度越深效果越好,宽度越宽效果越好。
将示例中的网络宽度变大:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 16, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(16, 32, 5)
self.fc1 = nn.Linear(32 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 32 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
测试集上准确率从原先的50%提升至66%:
Accuracy of the network on the 10000 test images: 66 %
深度和宽度是深度神经网络的两个基本维度:
1.更深的网络,有更好的非线性表达能力,可以学习更复杂的变换,从而可以拟合更加复杂的特征,更深的网络可以更简单地学习复杂特征。
网络加深会带来梯度不稳定、网络退化的问题,过深的网络会使浅层学习能力下降。深度到了一定程度,性能就不会提升了,还有可能会下降。
2.足够的宽度可以保证每一层都学到丰富的特征,比如不同方向,不同频率的纹理特征。宽度太窄,特征提取不充分,学习不到足够信息,模型性能受限。
宽度贡献了网络大量计算量,太宽的网络会提取过多重复特征,加大模型计算负担。
除了深度和宽度,分辨率也决定着效果和性能。
Q:示例中的网络有多少个参数?
先回忆一下Andrew NG的课程:
Layer1-->Layer2,共有3(L1) x 3(L2) + 3(bias)个weight
Layer2-->Layer3,共有3(L2) x 1(L3) + 3(bias)个weight
示例中:
self.conv1 = nn.Conv2d(3, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
Layer0-->Layer1,共有5 x 5(卷积核)x 3(L0) x 6 (L1)+ 6(bias)个weight
Layer1-->Layer2,共有5 x 5(卷积核)x 6(L1) x 16 (L2)+ 16(bias)个weight
Layer2-->Layer3,共有16 * 5 * 5(L2) x 120(L3)+ 120(bias)个weight
Layer3-->Layer4,共有120(L3) x 84(L4)+ 84(bias)个weight
Layer4-->Layer5,共有84(L4) x 10(L5) + 10(bias)个weight
pythorch中可以直接输出weight,我们来验证一下:
params = list(net.parameters())
print(len(params))
for param in params:
print(param.size())
输出:
10
torch.Size([6, 3, 5, 5])
torch.Size([6])
torch.Size([16, 6, 5, 5])
torch.Size([16])
torch.Size([120, 400])
torch.Size([120])
torch.Size([84, 120])
torch.Size([84])
torch.Size([10, 84])
torch.Size([10])