PyTorch是Facebook的AI研究团队发布的python工具包,主要用于深度学习。这篇文章我们一起学习一下如何使用PyTorch搭建神经网络训练分类模型,这里我们用的数据集是Yann LeCun的MINIST数据集。首先我们来看看需要哪些python库:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
这些python库都是PyTorch里面很常见的函数库,数据集以及优化函数等,接着我们就可以设置一些基本的信息:
batch_size = 200
learning_rate = 0.01
epochs = 10
因为在PyTorch里面,数据是按照batch来批量输入的,然后就是学习率,然后epoch表示训练的次数,训练完一个数据集就表示一个epoch。接着我们需要指定训练集和测试集的数据加载器:
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('../data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('../data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True)
这里的torch.utils.data.DataLoader是数据加载器的接口,第一个参数传的是数据集,第二个参数batch_size表示每个batch加载多少个样本,第三个参数shuffle设置为True表示每个epoch重新打乱数据。在第一个参数中有个datasets.MNIST()方法,第一个参数’…/data’表示 processed/training.pt 和 processed/test.pt的主目录,第二个参数train设置为False表示测试集,设置为True表示训练集,download设置为True表示从互联网上下载数据集,然后transform表示一个函数,输入为target,输出对其的转换。在这个参数表达式中torchvision.transforms.Compose(transforms)表示将多个transform组合起来使用,其中的transforms.ToTensor()表示把一个取值范围是[0,255]的PIL.Image或者shape为(H,W,C)的numpy.ndarray,转换成形状为[C,H,W],取值范围是[0,1.0]的torch.FloadTensor,H和W表示高和宽,C表示channel,图片的通道,就是说吧普通的图片数据转换成tensor数据。transforms.Normalize((0.1307,), (0.3081,))表示给定均值:(R,G,B) 方差:(R,G,B),将会把Tensor正则化,看到这里有些朋友就会有疑问了,那这个transforms.Normalize((0.1307,), (0.3081,))正则化表达式是啥意思?其实在神经网络中,标准化之后的数据会表现出更好的效果。标准化处理指的是,数据减去均值,再除以标准差,那么数据就会呈现均值为0方差为1的分布。那么为什么标准化之后的数据会有更好的效果,原因是均值为0方差为1分布的数据在sigmoid激活函数之后求导得到的导数很大,梯度下降的速度很快,反指分布不均匀的数据在经过激活函数求导之后,得到的结果接近于0,这种现象被称为梯度消失,这里的表达式表示均值是0.1307,标准差是0.3081,这些系数都是数据集提供方计算好的数据。在我们指定好了数据加载器之后,系统就会自动下载minist数据集了:
接下来我们就可以初始化我们的线性层了,这里我们初始化三个线性层,代码如下所示:
w1, b1 = torch.randn(200, 784, requires_grad=True), torch.zeros(200, requires_grad=True)
w2, b2 = torch.randn(200, 200, requires_grad=True), torch.zeros(200, requires_grad=True)
w3, b3 = torch.randn(10, 200, requires_grad=True), torch.zeros(10, requires_grad=True)
这里的权值和bias向量我们分别用w和b表示,randn表示返回一个张量,包含了从标准正态分布(均值为0,方差为 1,即高斯白噪声)中抽取一组随机数。这里我解释一下为什么这里的线性层输入和输出是200、784和10。首先是第一个线性层,输入是784维输出是200维,这里输入是像素28*28的数字图片,所以输入是784,首先我们将784维降维到200维;然后我们设置一个隐藏层,隐藏层的输入和输出都是200,虽然维度没有变,但是在这个过程中还是进行一些变换的,比如激活函数;最后一个是输出层,我们的输出的是一个10维的向量,因为结果是0-9的数字分类,所以我们需要将200维降维到10维输出。初始化完成之后,我们再来初始化前向传播的过程:
def forward(x):
x = [email protected]() + b1
x = F.relu(x)
x = [email protected]() + b2
x = F.relu(x)
x = [email protected]() + b3
x = F.relu(x)
return x
这里我们使用ReLU作为激活函数,因为ReLU激活函数更容易学习优化,因为其分段线性性质,导致其前传,后传,求导都是分段线性。而传统的sigmoid函数,由于两端饱和,梯度接近于0,容易造成梯度消失。前向传播的部分比较容易理解。接着就是定义优化器和优化的标准:
optimizer = optim.SGD([w1, b1, w2, b2, w3, b3], lr=learning_rate)
criteon = nn.CrossEntropyLoss()
SGD是常见的优化算法,里面传入的是模型的参数和学习率,然后算是函数是交叉熵。在所有的初始化完成之后,我们就可以进行模型训练了:
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
data = data.view(-1, 28*28)
logits = forward(data)
loss = criteon(logits, target)
optimizer.zero_grad()
loss.backward()
# print(w1.grad.norm(), w2.grad.norm())
optimizer.step()
if batch_idx % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
test_loss = 0
correct = 0
for data, target in test_loader:
data = data.view(-1, 28 * 28)
logits = forward(data)
test_loss += criteon(logits, target).item()
pred = logits.data.max(1)[1]
correct += pred.eq(target.data).sum()
test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
在这个训练的代码中data.view(-1,28 * 28)表示返回一个有相同数据但大小不同的tensor,-1参数的作用在于基于另一参数,自动计算该维度的大小。因为我们用的是交叉熵的损失函数,交叉熵损失函数吧LogSoftMax和NLLLoss集成到一个函数中,第一个logits表示每个类别的得分,2-D tensor,shape为 batch*n,然后target是大小为n的1—D tensor,包含类别的索引(0到 n-1)。因为整个函数里面包含了softmax,所以我们在优化完损失函数之后就不需要再进行softmax操作了,zero_grad()表示清空所有被优化过的Variable的梯度,然后我们通过loss.backward()反向传播更新权重,optimizer.step()进行单次的优化操作。同样的道理,我们下半部分就是通过测试集来统计我们的损失和准确率。接着就是梯度下降的过程,我们可以得到整个模型的效果非常的差,准确率甚至只有十几个百分点,这是什么原因造成的呢?
其实这是初始化造成的问题,我们初始化使用的api是用的高斯分布初始化。大神何恺明针对ReLU激活函数提出了一种新的数据初始化方法,在PyTorch官方文档中是这么描述的:根据He, K等人于2015年在“Delving deep into rectifiers: Surpassing human-level performance on ImageNet classification”中描述的方法,用一个均匀分布生成值,填充输入的张量或变量。结果张量中的值采样自U(-bound, bound),其中bound = sqrt(2/((1 + a^2) * fan_in)) * sqrt(3)。也被称为He initialisation。在PyTorch库中有官方的API,我们可以运用何恺明初始化方法来初始化我们的神经网络数据:
torch.nn.init.kaiming_normal_(w1)
torch.nn.init.kaiming_normal_(w2)
torch.nn.init.kaiming_normal_(w3)
经过kaiming_normal初始化之后,我们再来看看神经网络输出的结果:
可以看到测试集的效果有了极大的提升,准确率达到了95%以上,这就是一个典型的三层神经网络多分类的PyTorch模型。希望这篇文章可以让大家对PyTorch神经网络模型的理解有所提升,本文中如有纰漏不足之处,也请各路大神不吝指教,如有转载,也请标明出处,谢谢大家。