Pytorch学习笔记(一)

2018-12-10

深度学习的开源框架——Pytorch学习笔记,由脸书开发的一个开源框架,要比tensorflow等框架简单许多。
本文涉及Pytorch的特性Autograd以及实现一个简单的神经网络的基本步骤。

阅读时若遇到函数请翻阅Pytorch_中文文档查找。

Autograd: 自动求导

pyTorch里神经网络能够训练就是靠autograd包。我们来看下这个包,然后我们使用它来训练我们的第一个神经网络。

autograd 包提供Tensor所有操作的自动求导方法。
这是一个运行时定义的框架,这意味着你的反向传播是根据你代码运行的方式来定义的,因此每一轮迭代都可以各不相同。
以这些例子来讲,让我们用更简单的术语来看看这些特性。

张量

torch.Tensor 是torch库的核心类,它包装了一个Tensor,并且几乎支持所有的定义在其上的操作。。如果你把Tensor类的 .requires_grad 设置为True,它就会计算其上的梯度。 一旦完成了你的运算,你可以调用 .backward()来自动计算出所有的梯度。 而关于这一张量的梯度则集中于 .grad 属性中。

有一个对自动求导实现非常关键的类:Function。

TensorFunction 互相连接起来,构建了一个无圈图,对所有的历史进行了编码。每个张量都有一个叫 .grad_fn 的属性,是生成该tensor的Function的索引(而那些由我们创建的Tensor的.grad_fn是None)

如果你想计算导数,那么你就可以对一个Tensor调用它的.backward()方法。如果Tensor是一个标量(只有一个元素的数据),你不需要指明backward()的任何参数,如果它有多个元素的话,就需要指定一个gradient的参数,该参数与调用该Tensor的形状相同。

1
import torch

创建一个tensor并且设置 requires_grad=True 来跟踪计算。

1
2
x = torch.ones(2, 2, requires_grad=True)
print(x)

输出:

1
2
tensor([[1., 1.],
[1., 1.]], requires_grad=True)

给它来个运算:

1
2
y = x + 2
print(y)

输出:

1
2
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward0>)

y 是由运算产生的张量,所以它有grad_fn

1
print(y.grad_fn)

输出:

1
<AddBackward0 object at 0x7f0ea616bac8>

给 y再来一些运算:

1
2
3
z = y * y * 3
out = z.mean()
print(z, out)

输出:

1
2
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward1>)

.requires_grad_( ... )改变现有张量的 requires_grad 标志。如果没有提供.requires_grad参数的话,输入的标志默认是False。

1
2
3
4
5
6
7
a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)

输出:

1
2
3
False
True
<SumBackward0 object at 0x7f0e86396e48>

梯度

让我们反向传播,由于out是一个标量,out.backward() 等价于 out.backward(torch.tensor(1)).

1
out.backward()

输出 d(out)/dx的梯度

1
print(x.grad)

输出:

1
2
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])

最终得出的结果应该是一个全是4.5的矩阵。设置输出的变量为o。我们通过这一公式来计算:

有了autograd,我们可以做一些疯狂的事情!

1
2
3
4
5
6
7
8
x = torch.randn(3, requires_grad=True)

y = x * 2
while y.data.norm() < 1000:
y = y * 2
# y.data.norm代表y这个表达式求偏导数(或导数)在x=x的处置时候的取值

print(y)

输出:

1
tensor([-1178.9551,  1202.9015,   293.6342], grad_fn=<MulBackward0>)  #每次的输出可能不同

1
2
3
4
gradients = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(gradients)

print(x.grad)

输出:

1
tensor([ 102.4000, 1024.0000,    0.1024])

你可以使用with torch.no_grad()停止自动的梯度计算,即 使tensor的属性.requires_grad=True

1
2
3
4
5
print(x.requires_grad)
print((x ** 2).requires_grad)

with torch.no_grad():
print((x ** 2).requires_grad)

输出:

1
2
3
True
True
False

比如说我们想要计算下面的一个式子的导数:

我们可以直接用下面的代码来实现:

1
2
3
4
5
6
import torch

x = torch.ones(2,1,requires_grad = True)
z=4*x*x
y=z.norm()
y

神经网络

使用 torch.nn 包可以进行神经网络的构建。
现在你对autograd有了初步的了解,而nn则是建立在autograd的基础上来进行模型的定义和微分。nn.Module中包含着神经网络的层,同时forward(input)方法能够将output进行返回。
举个例子,来看一下这个数字图像分类的神经网络。

这是一个简单的前馈神经网络。 从前面获取到输入的结果,从一层传递到另一层,最后输出最后结果。

一个典型的神经网络的训练过程是这样的:

  • 定义一个有着可学习的参数(或者权重)的神经网络
  • 对着一个输入的数据集进行迭代:
    • 用神经网络对输入进行计算
    • 计算代价值 (对输出值的修正到底有多少)
    • 将梯度传播回神经网络的参数中
    • 更新网络中的权重
      • 通常使用简单的更新规则: weight = weight - learning_rate * gradient

定义网络

先来定义一个网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import torch
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):

def __init__(self):
super(Net, self).__init__()
# 1 input image channel, 6 output channels, 5x5 square convolution
# kernel
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
# an affine operation: y = Wx + b
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
# Max pooling over a (2, 2) window
# maxpool2d的作用相当于把一个x*x的区域汇聚成一个m*m的点(m的大小由参数定义,可以用(m,m)定义也可以用m定义)
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# If the size is a square you can only specify a single number
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = x.view(-1, self.num_flat_features(x))
# F.relu(x)就是把x里面的每个元素都取max(x,0)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

def num_flat_features(self, x):
size = x.size()[1:] # all dimensions except the batch dimension
num_features = 1
for s in size:
num_features *= s
return num_features

net = Net()
print(net)


'''神经网络的输出结果是这样的
Net (
# Conv2d为卷积层
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
# Linear为全连接层
(fc1): Linear (400 -> 120)
(fc2): Linear (120 -> 84)
(fc3): Linear (84 -> 10)
)

[拓展:什么是卷积?]以及[卷积的数学推导]
你只需要定义前向传播函数 forward , 后向传播函数 backward (梯度的计算) 就会使用autograd自动定义。你可以在forward函数里使用任何Tensor的运算。网络的学习到的参数可以通过net.parameters()获取。

1
2
3
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight

输出:

1
2
10
torch.Size([6, 1, 5, 5])

让我们随机输入一个 32x32 的LeNet数据。
要把MNIST dataset作为该网络的数据集,需要把数据 resize到32x32.

1
2
3
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

输出:

1
2
tensor([[ 0.1246, -0.0511,  0.0235,  0.1766, -0.0359, -0.0334,  0.1161,  0.0534,
0.0282, -0.0202]], grad_fn=<ThAddmmBackward>)

使所有参数的梯度恢复为0,然后使用随机梯度后向传播:

1
2
net.zero_grad()
out.backward(torch.randn(1, 10))

注意:
torch.nn 只支持mini-batches. 整个 torch.nn 包只接受批量样本,不接受单个样本。
例如, nn.Conv2d 接受一个4D的张量形如: nSamples x nChannels x Height x Width.
如果你只有一个样本,那就使用 input.unsqueeze(0) 创造一个假的mini-batch。
在进一步之前,我们来回顾目前你所见到的所有类。

复习一下前面我们学到的:

  • torch.Tensor - 一个多维度的数组,支持自动梯度 backward()。其梯度仍然保存在张量里。
  • nn.Module - 神经网络模型。方便的封装参数,可以导出模型到GPU,加载模型,导出模型等。
  • nn.Parameter - 一种变量,当将任何值赋予Module时自动注册为一个参数,比如上例中的conv1 conv2均为声明Module时自动生成的参数Parameter[0]和Parameter[1]。
  • autograd.Function - 实现了使用自动求导方法的前馈和后馈的定义。每个Tensor的操作都会生成至少一个独立的Function节点,与生成了Tensor的函数相连之后记录下操作历史。

到此, 我们覆盖了:

  • 定义一个网络
  • 处理输入和反向传播。

剩余的内容:

  • 计算损失
  • 更新网络的参数

损失函数

一个损失函数接受(output,targe)对作为输入,计算output和target相差的程度。
nn包里有多种不同的 loss functions 。最简单的损失函数是: nn.MSELoss ,计算输入和目标之间的均方误差损失函数。举个例子:

1
2
3
4
5
6
7
8
output = net(input)
target = torch.randn(10) # a dummy target, for example
# tensor.view(x,y) 把原来的tensor改为新的维度,数据不变,只是改变排列方式,-1代表这个维度消失。比如(-1,1)表示排成一列,(1,-1)表示排成一行
target = target.view(1, -1)
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

输出:

1
tensor(1.3638, grad_fn=<MseLossBackward>)

现在,如果你跟随loss从后往前看,使用.creator属性你可以看到这样的一个计算流程图:

1
2
3
4
input -> conv2d -> relu -> pool2d -> conv2d -> relu -> maxpool2d  
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss

因此当我们调用loss.backward()时,整个图通过代价来进行区分,图中所有参数属性为 requires_grad=True的Tensor都会以.grad来累积梯度。

1
2
3
print(loss.grad_fn)  # MSELoss
print(loss.grad_fn.next_functions[0][0]) # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0]) # ReLU

输出:

1
2
3
<MseLossBackward object at 0x7f0e86396a90>
<ThAddmmBackward object at 0x7f0e863967b8>
<ExpandBackward object at 0x7f0e863967b8>

反向传播

为了反向传播误差,我们必须使用loss.backward(). 首先需要清除已存在的梯度,然后把梯度累加起来。

现在我们就可以调用:loss.backward(), 来看看 conv1’s在进行反馈之后的偏置梯度如何。

1
2
3
4
5
6
7
8
9
net.zero_grad()     # 清空已存在的梯度

print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)

loss.backward()

print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

输出:

1
2
3
4
conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([ 0.0181, -0.0048, -0.0229, -0.0138, -0.0088, -0.0107])

神经网络包中包含着诸多用于神经网络的模块和代价函数,带有文档的完整清单请自行去官网查找。

最后一步需要学习的是:

  • 更新网络的权重

最简单的更新的规则是随机梯度下降法(SGD):

1
weight = weight - learning_rate * gradient

我们可以用简单的python来表示:

1
2
3
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)

神经网络里可以使用各种更新权重的方法, 比如:SGD, Nesterov-SGD, Adam, RMSProp等,为了使用这些方法,有一个小包 : torch.optim实现了这些方法。
用起来非常的容易:

1
2
3
4
5
6
7
8
9
10
11
import torch.optim as optim

# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)

# in your training loop:
optimizer.zero_grad() # 清空梯度
output = net(input)
loss = criterion(output, target)
loss.backward() # 损失回传
optimizer.step() # 更新权重

注意:
我们在使用前,要使用optimizer.zero_grad()把网络的参数梯度手动设置为0。这是因为我们前面在Backprop里说过,如果不清空的话,之前的梯度也会一起累加起来的。

现在我们已经学习了如果定义一个神经网络、计算误差和更新权重。但你现在也许会想。那么数据怎么办呢?

通常来讲,当你处理图像,声音,文本,视频时需要使用python中其他独立的包来将他们转换为numpy中的数组,之后再转换为torch.*Tensor

图像的话,可以用Pillow, OpenCV
声音处理可以用scipylibrosa
文本的处理使用原生Python或者Cython以及NLTKSpaCy都可以。
特别的对于图像,我们有torchvision这个包可用,其中包含了一些现成的数据集如:Imagenet, CIFAR10, MNIST等等。同时还有一些转换图像用的工具。 这非常的方便并且避免了写样板代码。

本教程使用CIFAR10数据集。 我们要进行的分类的类别有:’airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’。 这个数据集中的图像都是3通道,32x32像素的图片。

训练一个图片分类器

我们要按以下这几个步骤顺序:

  1. 使用torchvision来读取并预处理CIFAR10数据集
  2. 定义一个卷积神经网络
  3. 定义一个代价函数
  4. 在神经网络中训练训练集数据
  5. 使用测试集数据测试神经网络

1.读取并预处理CIFAR10

使用torchvision读取CIFAR10相当的方便。

1
2
3
import torch
import torchvision
import torchvision.transforms as transforms

torchvision数据集的输出是在[0, 1]范围内的PILImage图片。
我们此处使用归一化的方法将其转化为Tensor,数据范围为[-1, 1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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')

比如我们输出其中的一些图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import matplotlib.pyplot as plt
import numpy as np

# functions to show an image

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))
# print labels
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

输出:

1
ship  bird   car truck

如果出现以下错误,则需在main函数中调用以上程序。

2.定义一个卷积神经网络

这里我们使用之前的 神经网络 这一部分的代码,只不过把原来输入是1个通道改成3个通道.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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()

3.定义代价函数和优化器

这里我们使用Classification Cross-Entropy lossSGD.

1
2
3
4
import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

4.训练网络

我们只需一轮一轮迭代然后不断通过输入来进行参数调整就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
2
3
4
5
6
7
8
9
10
11
12
13
[1,  2000] loss: 2.178
[1, 4000] loss: 1.837
[1, 6000] loss: 1.660
[1, 8000] loss: 1.573
[1, 10000] loss: 1.509
[1, 12000] loss: 1.465
[2, 2000] loss: 1.384
[2, 4000] loss: 1.353
[2, 6000] loss: 1.341
[2, 8000] loss: 1.330
[2, 10000] loss: 1.276
[2, 12000] loss: 1.287
Finished Training

5.测试训练效果

我们将通过对比神经网络给出的预测标签 和 已知的ground-truth进行对比,可以得出正确与否,如果预测的正确,我们可以将样本加入正确预测的列表中。
第一步,让我们展示几张照片来熟悉一下。

1
2
3
4
5
6
dataiter = iter(testloader)
images, labels = dataiter.next()

# print images
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))

然后再来看看我们的神经网络是怎么看待它们的:

1
2
3
4
5
outputs = net(images)
_, predicted = torch.max(outputs, 1)

print('Predicted: ', ' '.join('%5s' % classes[predicted[j]]
for j in range(4)))

输出结果为:

1
2
GroundTruth:    cat  ship  ship plane
Predicted: cat plane car plane

对于小规模测试数据一般- -,接下来我们用规模更大的整个数据集来测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
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))

输出结果为:

1
Accuracy of the network on the 10000 test images: 56  %

看上去这玩意输出的结果比随机整的要好,随机选择的话从十个中选择一个出来,准确率大概只有10%。看上去神经网络学到了点东西。
emmmmmm那么到底哪些类别表现良好又是哪些类别不太行呢?
我们分类来跑一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs, 1)
c = (predicted == labels).squeeze()
for i in range(4):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] += 1

for i in range(10):
print('Accuracy of %5s : %2d %%' % (
classes[i], 100 * class_correct[i] / class_total[i]))

结果如下:

1
2
3
4
5
6
7
8
9
10
Accuracy of plane : 69 %
Accuracy of car : 77 %
Accuracy of bird : 40 %
Accuracy of cat : 35 %
Accuracy of deer : 24 %
Accuracy of dog : 41 %
Accuracy of frog : 80 %
Accuracy of horse : 62 %
Accuracy of ship : 52 %
Accuracy of truck : 54 %

好吧,接下来该怎么搞了?

我们该如何将神经网络运行在GPU上呢?

在GPU上进行训练

就像你把Tensor传递给GPU进行运算一样,你也可以将神经网络传递给GPU。
如果有GPU的话,我们把GPU作为我们的首选驱动:

1
2
3
4
5
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# Assume that we are on a CUDA machine, then this should print a CUDA device:

print(device)

如果输出cuda:0则表示为GPU。否则为CPU。
由于笔者穷,买不起GPU,这一章节我们就不讨论了哈..有需求者自行在官网查看教程。

接下来的工作

本文来源:「想飞的小菜鸡」的个人网站 vodkazy.cn

版权声明:本文为「想飞的小菜鸡」的原创文章,采用 BY-NC-SA 许可协议,转载请附上原文出处链接及本声明。

原文链接:https://vodkazy.cn/2018/12/10/Pytorch学习笔记(一)

支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者,更多文章请访问想飞的小菜鸡