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不变

tensor和numpy的数组之间的互操作非常容易且快速对于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