Pytorch学习

Pytorch基础

Posted by Wwt on July 31, 2018

本文参照chenyuntc 的github项目 pytorch-book,部分内容略微修改,仅作学习参考使用

Pytorch基础

Tensor

Tensor 是Pytorch中重要的数据结构,可以认为是一个高维数组。它可以是一个数(标量)、一维数组(向量)、二维数组(矩阵)以及更高维的数组。Tensor和Numpy的ndarrays类似,但Tensor可以使用GPU进行加速。Tensor的使用和Numpy及Matlab的接口十分相似。

from __future__ import print_function
import torch as t

# 构建 5x3 矩阵,只是分配了空间,未初始化
x = t.Tensor(5, 3)  
x

1.00000e-07 *
  0.0000  0.0000  5.3571
  0.0000  0.0000  0.0000
  0.0000  0.0000  0.0000
  0.0000  5.4822  0.0000
  5.4823  0.0000  5.4823
[torch.FloatTensor of size 5x3]1.00000
# 使用[0,1]均匀分布随机初始化二维数组
x = t.rand(5, 3)  
x
 0.3673  0.2522  0.3553
 0.0070  0.7138  0.0463
 0.6198  0.6019  0.3752
 0.4755  0.3675  0.3032
 0.5824  0.5104  0.5759
[torch.FloatTensor of size 5x3]
print(x.size()) # 查看x的形状
torch.Size([5, 3])
x.size()[1], x.size(1) # 查看列的个数, 两种写法等价
y = t.rand(5, 3)
# 加法的第一种写法
x + y
 0.4063  0.7378  1.2411
 0.0687  0.7725  0.0634
 1.1016  1.4291  0.7324
 0.7604  1.2880  0.4597
 0.6020  1.0124  1.0185
print('最初y')
print(y)
#加法的第二种写法
t.add(x,y)
tensor([[0.9639,  0.8763,  0.2834],
        [ 1.3785,  1.5090,  1.3919],
    	[ 0.7139,  0.6348,  0.8439],
        [ 0.7022,  1.5079,  0.4776],
        [ 1.7892,  1.6383,  0.7774]])

#加法的第三种写法:指定加法结果的输出目标为result
result = t.Tensor(5,3)
t.add(x,y,out=result)
print(result)
tensor([[ 0.9639,  0.8763,  0.2834],
        [ 1.3785,  1.5090,  1.3919],
        [ 0.7139,  0.6348,  0.8439],
        [ 0.7022,  1.5079,  0.4776],
        [ 1.7892,  1.6383,  0.7774]])
print('第一种加法,y的结果')
y.add(x) # 普通加法,不改变y的内容
print(y)

print('第二种加法,y的结果')
y.add_(x) # inplace 加法,y变了
print(y)
[torch.FloatTensor of size 5x3]
注意,函数名后面带下划线_ 的函数会修改Tensor本身。例如,x.add_(y)x.t_()会改变 x,但x.add(y)x.t()返回一个新的Tensor x不变。

tensornumpy的数组之间的互操作非常容易且快速。对于Tensor不支持的操作,可以先转为numpy数组处理,之后再转回Tensor
a = t.ones(5)#创建一个全1的tensor
tensor([ 1.,  1.,  1.,  1.,  1.])
b = a.numpy() # Tensor -> Numpy
array([1., 1., 1., 1., 1.], dtype=float32)
import numpy as np
a = np,ones(5)
b = t.from_numpy(a) #Numpy->Tensor
print(a)
print(b)
[1. 1. 1. 1. 1.]
tensor([ 1.,  1.,  1.,  1.,  1.], dtype=torch.float64)
#Tensor和numpy对象共享内存,所以他们之间的转换很快,而且几乎不会消耗什么资源。但这也意味着,如果其中一个变了,另外一个也会随之改变。
函数 功能
index_select(input, dim, index) 在指定维度dim上选取,比如选取某些行、某些列
masked_select(input, mask) 例子如上,a[a>0],使用ByteTensor进行选取
non_zero(input) 非0元素的下标
gather(input, dim, index) 根据index,在dim维度上选取数据,输出的size与index一样
常用Tensor操作

通过tensor.view方法可以调整tensor的形状,但必须保证调整前后元素总数一致。view不会修改自身的数据,返回的新tensor与源tensor共享内存,也即更改其中的一个,另外一个也会跟着改变。在实际应用中可能经常需要添加或减少某一维度,这时候squeezeunsqueeze两个函数就派上用场了。

a=t.arange(0,6)
a.view(2,3)
0  1  2
 3  4  5
[torch.FloatTensor of size 2x3]
b = a.view(-1, 3) # 当某一维为-1的时候,会自动计算它的大小
b
 0  1  2
 3  4  5
[torch.FloatTensor of size 2x3]

b.unsqueeze(1) # 注意形状,在第1维(下标从0开始)上增加“1”
(0 ,.,.) = 
  0  1  2

(1 ,.,.) = 
  3  4  5
[torch.FloatTensor of size 2x1x3]
b.unsqueeze(-2) # -2表示倒数第二个维度
(0 ,.,.) = 
  0  1  2

(1 ,.,.) = 
  3  4  5
[torch.FloatTensor of size 2x1x3]
c = b.view(1, 1, 1, 2, 3)
c.squeeze(0) # 压缩第0维的“1”
(0 ,0 ,.,.) = 
  0  1  2
  3  4  5
[torch.FloatTensor of size 1x1x2x3]
c.squeeze() # 把所有维度为“1”的压缩
 0  1  2
 3  4  5
[torch.FloatTensor of size 2x3]

resize是另一种可用来调整size的方法,但与view不同,它可以修改tensor的大小。如果新大小超过了原大小,会自动分配新的内存空间,而如果新大小小于原大小,则之前的数据依旧会被保存,看一个例子。

b.resize_(1, 3)
b
 0  100    2
[torch.FloatTensor of size 1x3]
b.resize_(3, 3) # 旧的数据依旧保存着,多出的大小会分配新空间
b
0.0000e+00  1.0000e+02  2.0000e+00
 3.0000e+00  4.0000e+00  5.0000e+00
 4.1417e+36  4.5731e-41  6.7262e-44
[torch.FloatTensor of size 3x3]
索引操作

Tensor支持与numpy.ndarray类似的索引操作,语法上也类似,下面通过一些例子,讲解常用的索引操作。如无特殊说明,索引出来的结果与原tensor共享内存,也即修改一个,另一个会跟着修改。

a = t.randn(3, 4)
a
 0.2355  0.8276  0.6279 -2.3826
 0.3533  1.3359  0.1627  1.7314
 0.8121  0.3059  2.4352  1.4577
[torch.FloatTensor of size 3x4]
a[0] # 第0行(下标从0开始)
 0.2355
 0.8276
 0.6279
-2.3826
a[:, 0] # 第0列
[torch.FloatTensor of size 4]
 0.2355
 0.3533
 0.8121
[torch.FloatTensor of size 3]
a[0][2] # 第0行第2个元素,等价于a[0, 2]
0.6279084086418152
a[0, -1] # 第0行最后一个元素
-2.3825833797454834
a[:2] # 前两行
 0.2355  0.8276  0.6279 -2.3826
 0.3533  1.3359  0.1627  1.7314
[torch.FloatTensor of size 2x4]
a[:2, 0:2] # 前两行,第0,1列
 0.2355  0.8276
 0.3533  1.3359
[torch.FloatTensor of size 2x2]
print(a[0:1,:2]) #第0行,前两列
print(a[0,:2]) #注意两者的区别:形状不同
0.2355  0.8276
[torch.FloatTensor of size 1x2]
 0.2355
 0.8276
[torch.FloatTensor of size 2]
归并操作

此类操作会使输出形状小于输入形状,并可以沿着某一维度进行指定操作。如加法sum,既可以计算整个tensor的和,也可以计算tensor中每一行或每一列的和 。

函数 功能
mean/sum/median/mode 均值/和/中位数/众数
norm/dist 范数/距离
std/var 标准差/方差
cumsum/cumprod 累加/累乘

以上大多数函数都有一个参数dim,用来指定这些操作是在哪个维度上执行的。关于dim(对应于Numpy中的axis)的解释众说纷纭,这里提供一个简单的记忆方式:

假设输入的形状是(m, n, k)

  • 如果指定dim=0,输出的形状就是(1, n, k)或者(n, k)
  • 如果指定dim=1,输出的形状就是(m, 1, k)或者(m, k)
  • 如果指定dim=2,输出的形状就是(m, n, 1)或者(m, n)

size中是否有”1”,取决于参数keepdimkeepdim=True会保留维度1。注意,以上只是经验总结,并非所有函数都符合这种形状变化方式,如cumsum

b = t.ones(2, 3)
b.sum(dim = 0, keepdim=True)
 2  2  2
[torch.FloatTensor of size 1x3]
# keepdim=False,不保留维度"1",注意形状
b.sum(dim=0, keepdim=False)
 2
 2
 2
[torch.FloatTensor of size 3]
b.sum(dim=1)
 3
 3
[torch.FloatTensor of size 2]

Autograd:自动微分

深度学习的算法本质上是通过反向传播求导数,而PyTorch的Autograd模块则实现了此功能。在Tensor上的所有操作,Autograd都能为它们自动提供微分,避免了手动计算导数的复杂过程 。

autograd.Variable是Autograd中的核心类,它简单封装了Tensor,并支持几乎所有Tensor有的操作。Tensor在被封装为Variable之后,可以调用它的.backward实现反向传播,自动计算所有梯度 。

from torch.autograd import Variable
# 使用Tensor新建一个Variable
x = Variable(t.ones(2, 2), requires_grad = True)
x
Variable containing:
 1  1
 1  1
[torch.FloatTensor of size 2x2]
y = x.sum()
Variable containing:
 4
[torch.FloatTensor of size 1]
y
y.grad_fn
y.backward() # 反向传播,计算梯度
# y = x.sum() = (x[0][0] + x[0][1] + x[1][0] + x[1][1])
# 每个值的梯度都为1
x.grad
Variable containing:
 1  1
 1  1
[torch.FloatTensor of size 2x2]
注意:grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以反向传播之前需把梯度清零。

定义网络

定义网络时,需要继承nn.Module,并实现它的forward方法,把网络中具有可学习参数的层放在构造函数__init__中。如果某一层(如ReLU)不具有可学习的参数,则既可以放在构造函数中,也可以不放,但建议不放在其中,而在forward中使用nn.functional代替。

import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        # nn.Module子类的函数必须在构造函数中执行父类的构造函数
        # 下式等价于nn.Module.__init__(self)
        super(Net, self).__init__()
        
        # 卷积层 '1'表示输入图片为单通道, '6'表示输出通道数,'5'表示卷积核为5*5
        self.conv1 = nn.Conv2d(1, 6, 5) 
        # 卷积层
        self.conv2 = nn.Conv2d(6, 16, 5) 
        # 仿射层/全连接层,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): 
        # 卷积 -> 激活 -> 池化 
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2) 
        # reshape,‘-1’表示自适应
        x = x.view(x.size()[0], -1) 
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)        
        return x

net = Net()
print(net)

#只要在nn.Module的子类中定义了forward函数,backward函数就会自动被实现(利用autograd)。在forward 函数中可使用任何tensor支持的函数,还可以使用if、for循环、print、log等Python语法,写法和标准的Python写法一致。
input = t.randn(1, 1, 32, 32)
out = net(input)
out.size()
#torch.Size([1,10])

net.zero_grad() # 所有参数的梯度清零
out.backward(t.ones(1,10)) # 反向传播

#损失函数
#nn实现了神经网络中大多数的损失函数,例如nn.MSELoss用来计算均方误差,nn.CrossEntropyLoss用来计算交叉熵损失。
output = net(input)
target = t.arange(0,10).view(1,10) 
criterion = nn.MSELoss()
loss = criterion(output, target)
loss # loss是个scalar
## 运行.backward,观察调用之前和调用之后的grad
net.zero_grad() # 把net中所有可学习参数的梯度清零
print('反向传播之前 conv1.bias的梯度')
print(net.conv1.bias.grad)
loss.backward()
print('反向传播之后 conv1.bias的梯度')
print(net.conv1.bias.grad)

import torch.optim as optim
#新建一个优化器,指定要调整的参数和学习率
optimizer = optim.SGD(net.parameters(), lr = 0.01)

# 在训练过程中
# 先梯度清零(与net.zero_grad()效果一样)
optimizer.zero_grad() 

# 计算损失
output = net(input)
loss = criterion(output, target)

#反向传播
loss.backward()

#更新参数
optimizer.step()

参考

pytorch-book