文章目录

  • 1 PyTorch简介
  • 2 PyTorch入门
    • 2.1 Tensor
    • 2.2 自动微分Autograd
    • 2.3 神经网络
    • 2.4 损失函数
    • 2.5 优化器
    • 2.6 数据加载与预处理
    • 2.7 小试牛刀:CIFAR-10分类
  • 3 Tensor和Atuograd
    • 3.1 Tensor
      • 基础操作
      • 创建Tensor
      • 常用Tensor操作
      • 索引操作
      • 高级索引
      • Tensor类型
      • 逐元素操作
      • 归并操作
      • 比较
      • 线性代数
    • 3.2 Tensor和Numpy
      • 广播法则
    • 3.3 内部结构
    • 3.4 其它有关Tensor的话题
      • GPU/CPU
      • 持久化
      • 向量化
    • 3.5 小试牛刀:线性回归
  • 4 神经网络工具箱nn
    • 4.1 nn.Module
    • 4.2 常用的神经网络层
      • 4.2.1 图像相关层
      • 4.2.2 激活函数
      • 4.2.3 循环神经网络层

1 PyTorch简介

PyTorch是一个基于Torch的Python开源机器学习库,用于自然语言处理等应用程序。它主要由Facebookd的人工智能小组开发,不仅能够 实现强大的GPU加速,同时还支持动态神经网络,这一点是现在很多主流框架如TensorFlow都不支持的。 PyTorch提供了两个高级功能: 1.具有强大的GPU加速的张量计算(如Numpy) 2.包含自动求导系统的深度神经网络 除了Facebook之外,Twitter、GMU和Salesforce等机构都采用了PyTorch。

2 PyTorch入门

2.1 Tensor

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

import torch as t
import numpy as np
a = np.arange(6).reshape(2,3)
a

运行结果:
array([[0, 1, 2],
[3, 4, 5]])

# ndarrays转换tensor
b = t.from_numpy(a)
b

运行结果:
tensor([[0, 1, 2],
[3, 4, 5]], dtype=torch.int32)

tensor转换ndarrays
c = b.numpy()
c

运行结果:
array([[0, 1, 2],
[3, 4, 5]])

# abs
a = [1, -1, -2, 3]
b = t.FloatTensor(a) #32-bit float point
print(np.abs(a))
print(torch.abs(b))

运行结果:
[1 1 2 3]
tensor([1., 1., 2., 3.])

# multiple
a = [[1,2],[3,4]]
b = t.tensor(a)
print(np.matmul(a,a))
print(torch.mm(b,b))

运行结果:
[[ 7 10]
[15 22]]
tensor([[ 7, 10],
[15, 22]])

# array各类乘法
a = np.array(a)
print(a@a)
print(a*a)
print(np.dot(a, a))

运行结果:
[[ 7 10]
[15 22]]
[[ 1 4]
[ 9 16]]
[[ 7 10]
[15 22]]

# tensor各类乘法
print(b@b)
print(b*b)
# print(torch.dot(b, b)) # 只能对一维tensor点乘运算

运行结果:
tensor([[ 7, 10],
[15, 22]])
tensor([[ 1, 4],
[ 9, 16]])

# add
a = t.FloatTensor([[1,2],[3,4]])
b = t.from_numpy(np.array([[5,6], [7,8]]))
c = a + b
print(c)

运行结果:
tensor([[ 6., 8.],
[10., 12.]])

c = t.add(a, b)
c = a.add(b)
result = t.Tensor(2, 2)
result = t.add(a, b, out = result)
print(c)
print(result)

运行结果:
tensor([[ 6., 8.],
[10., 12.]])
tensor([[ 6., 8.],
[10., 12.]])

# 带下划线_的函数会改变形参
x = t.tensor([1, 2, 3])
y = t.tensor([4, 5, 6])
x.add(y)
y.add_(x)
print(x)
print(y)

运行结果:
tensor([1, 2, 3])
tensor([5, 7, 9])

Tensor和numpy对象共享内存,所以它们之间的转换很快,而且几乎不需要消耗资源。这也意味着,如果其中一个变了,另外一个也会随之改变。

a = np.array([1, 2, 3, 4])
b = t.from_numpy(a)
b.add_(1)
print(a)

运行结果:
[2 3 4 5]

2.2 自动微分Autograd

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

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

Variable主要包含三个属性。

  • data:保存Variable所包含的Tensor。
  • grad:保存data对应的梯度,grad也是个Variable,而不是Tensor,它和data的形状一样。
  • grad_fn:指向一个Function对象,这个Function用来计算反向传播计算输入的梯度。

从0.4起, Variable 正式合并入Tensor, Variable 本来实现的自动微分功能,Tensor就能支持。读者还是可以使用Variable(tensor), 但是这个操作其实什么都没做。建议读者以后直接使用tensor*. 要想使得Tensor使用autograd功能,只需要设置tensor.requries_grad=True.

# 为tensor设置 requires_grad 标识,代表着需要求导数
# pytorch 会自动调用autograd 记录操作
x = t.ones(2, 2, requires_grad = True)
# 上一步等价于
# x = t.ones(2,2)
# x.requires_grad = True
x

运行结果:
tensor([[1., 1.],
[1., 1.]], requires_grad=True)

y = x.sum()
print(y)

运行结果:
tensor(4., grad_fn=)

y.grad_fn

运行结果:
<SumBackward0 at 0x193dbe88988>

y.backward() # 反向传播,计算梯度
# y = x.sum() = (x[0][0] + x[0][1] + x[1][0] + x[1][1])
# 每个值的梯度都为1
x.grad

运行结果:
tensor([[1., 1.],
[1., 1.]])

grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以反向传播之前需把梯度清零。

y.backward()
x.grad
y.backward()
x.grad

运行结果:
tensor([[3., 3.],
[3., 3.]])

# 以下划线结束的函数是inplace操作,会修改自身的值,就像add_
x.grad.data.zero_()

运行结果:
tensor([[0., 0.],
[0., 0.]])

y.backward()
x.grad

运行结果:
tensor([[1., 1.],
[1., 1.]])

2.3 神经网络

Autograd实现了反向传播功能,但是直接用来写深度学习的代码在很多情况下还是稍显复杂,torch.nn是专门为神经网络设计的模块化接口。nn构建于Autograd之上,可用来定义和运行神经网络。nn.Module是nn中最重要的类,可以把它看做是一个网络的封装,包括网络各层定义以及forward方法,调用forward(input)方法,可返回前向传播的结果。我们以最早的卷积神经网络LeNet为例,来看看如何用nn.Module实现。LeNet的网络结构如下所示。

这是一个基础的前向传播(feed-forward)网络:接收输入,经过层层传递运算,得到输出。

(1)定义网络

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

import numpy as np
import torch as t
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):def __init__(self):super().__init__()self.conv1 = nn.Conv2d(1, 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)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)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)

运行结果:
Net(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)

只要在nn.Module的子类中定义了forward函数,backward函数就会被自动实现(利用Autograd)。在forward函数中可使用任何Variable支持的函数,还可以使用if、for循环、print、log等Python语法,写法和标准的Python写法一致。

网络的可学习参数通过net.parameters()返回,net.named_parameters可同时返回可学习的参数及名称。

params = list(net.parameters())
print(len(params))

运行结果:
10

for name, parameters in net.named_parameters():print(name, ';', parameters.size())

运行结果:
conv1.weight ; torch.Size([6, 1, 5, 5])
conv1.bias ; torch.Size([6])
conv2.weight ; torch.Size([16, 6, 5, 5])
conv2.bias ; torch.Size([16])
fc1.weight ; torch.Size([120, 400])
fc1.bias ; torch.Size([120])
fc2.weight ; torch.Size([84, 120])
fc2.bias ; torch.Size([84])
fc3.weight ; torch.Size([10, 84])
fc3.bias ; torch.Size([10])

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)) #反向传播

需要注意的是,torch.nn只支持mini-batch,不支持一次只输入一个样本,即一次必须是一个batch。如果只想输入一个样本,则用input.unsqueeze(0)将batch_size设为1。例如,nn.Conv2d输入必须是4维的,形如nSamples * nChannels * Height * Width。可将nSamples设为1,即1 * nChannels * Height * Width。

2.4 损失函数

nn实现了神经网络中大多数的损失函数,例如nn.MSELoss用来计算均方误差,nn.CrossEntropyLoss用来计算交叉熵损失。

output = net(input)
target = t.arange(0.0, 10)
criterion = nn.MSELoss()
loss = criterion(output, target)
loss

运行结果:
tensor(28.2825, grad_fn=<MseLossBackward>)

如果对loss进行反向传播溯源(使用grad_fn属性),可以看到它的计算图如下。

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear -> MSELoss -> loss

当调用loss.backward()时,该图会动态生成并自动微分,也会自动计算图中参数(Parameter)的导数。

net.zero_grad()
print('反向传播之前conv1.bias的梯度')
print(net.conv1.bias.grad)
loss.backward()
print('反向传播之后的conv1.bias的梯度')
print(net.conv1.bias.grad)

运行结果:
反向传播之前conv1.bias的梯度
tensor([0., 0., 0., 0., 0., 0.])
反向传播之后的conv1.bias的梯度
tensor([-0.0023, 0.0267, 0.0289, -0.0467, 0.0040, -0.0210])

2.5 优化器

在反向传播计算完所有参数的梯度后,还需要使用优化方法更新网络的权重和参数。例如,随机梯度下降法(SGD)的更新策略如下:

weight = weight - learning_rate * gradient

手动实现如下:

learning_rate = 0.01
for f in net.parameters():f.data.sub_(f.grad.data * learning_rate) #inplace减法

torch.optim中实现了深度学习中绝大多数的优化方法,例如RMSProp、Adam、SGD等,更便于使用,因此通常并不需要手动写上述代码。

import torch.optim as optim
#新建一个优化器
optimizer = optim.SGD(net.parameters(), lr = 0.01)
#先进行梯度清零
optimizer.zero_grad()
#计算损失
output = net(input)
loss = criterion(output, target)
#反向传播
loss.backward()
#更新参数
optimizer.step()

2.6 数据加载与预处理

在深度学习中数据加载与预处理是非常复杂繁琐的,但PyTorch提供了一些可简答简化和加快数据处理流程的工具。同时,对于常用的数据集,PyTorch也提供了封装好的接口供用户快速调用,这些数据集主要保存在torchvision中。

torchvision实现了常用的图像数据加载功能,例如ImageNet、CIFAR10、MNIST等,以及常用的数据转换操作,这极大地方便了数据加载。

2.7 小试牛刀:CIFAR-10分类

下面我们来尝试实现对CIFAR10数据集的分类,步骤如下:
(1)使用torchvision加载并预处理CIFAR10数据集。
(2)定义网络。
(3)定义损失函数和优化器。
(4)训练网络并更新网络参数。
(5)测试网络。

(1)CIFAR-10数据集加载及预处理

CIFAR-10是一个常用的彩色图片数据集,它有10个类别:airplane、automobile、bird、cat、deer、dog、frog、horse、ship和truck。每张图片都是3 * 32 * 32,也即3通道彩色图片,分辨率为32 * 32。提前下载数据集放到指定目录下,如E:/data/,在加载器中root参数指向该目录,程序检测到该文件已存在就直接解压。

import torch as t
import torch.nn as nn
import torch.nn.functional as F
from torch import optim
import torchvision as tv
import torchvision.transforms as transforms
from torchvision.transforms import ToPILImage
show = ToPILImage() #方便可视化

运行结果:

transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

运行结果:

trainset = tv.datasets.CIFAR10(root = 'D:\dataset',train = True,download = True, transform = transform)
trainloader = t.utils.data.DataLoader(trainset,batch_size = 4,shuffle = True,num_workers = 2)

运行结果:

testset = tv.datasets.CIFAR10(root = 'D:\dataset',train = False,download = True,transform = transform)
testloader = t.utils.data.DataLoader(testset,batch_size = 4,shuffle = False,num_workers = 2)

运行结果:

classes = ('plane', 'cat', 'bird', 'deer', 'dog', 'frog', 'hores', 'ship', 'truck')

运行结果:

data, label = trainset[100]
print(classes[label])

运行结果:
truck

show((data + 1)/2).resize((200, 200))

运行结果:

dataiter = iter(trainloader)
images,labels = dataiter.next()    # 返回4张图片及标签
print(' '.join('%11s' % classes[labels[j]] for j in range(4)))
show(tv.utils.make_grid((images+1)/2)).resize((400,100))

运行结果:
ship ship hores ship

class Net(nn.Module):def __init__(self):super(Net, self).__init__()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)def forward(self, x):x = F.max_pool2d(F.relu(self.conv1(x)), 2)x = F.max_pool2d(F.relu(self.conv2(x)), (2, 2))x = x.view(x.size()[0], -1)x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x))x = self.fc3(x)return xnet = Net()
print(net)

运行结果:
Net(
(conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)

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

运行结果:

for epoch in range(2):running_loss = 0.0for i,data in enumerate(trainloader,0):# 输入数据inputs,labels = data# 梯度清零optimizer.zero_grad()# forward + backwardoutputs = net(inputs)loss = criterion(outputs,labels)loss.backward()# 更新参数optimizer.step()# 打印log信息running_loss += lossif i % 2000 == 1999:     # 每2000个batch打印一次训练状态print('[%d,%5d] loss: %.3f' % (epoch+1, i+1, running_loss / 2000))running_loss = 0.0
print('Finished Trainning')

运行结果:
[1, 2000] loss: 2.231
[1, 4000] loss: 1.913
[1, 6000] loss: 1.725
[1, 8000] loss: 1.629
[1,10000] loss: 1.568
[1,12000] loss: 1.491
[2, 2000] loss: 1.438
[2, 4000] loss: 1.395
[2, 6000] loss: 1.380
[2, 8000] loss: 1.357
[2,10000] loss: 1.314
[2,12000] loss: 1.318
Finished Trainning


运 行结果:


运行结果:


运行结果:

3 Tensor和Atuograd

3.1 Tensor

Tensor,又名张量,读者可能对这个名词似曾相识,因为它不仅在PyTorch中出现过,也是Theano、TensorFlow、Torch和MXNet中重要的数据结构。关于张量的本质不乏深度剖析的文章,但从工程角度讲,可简单地认为它就是一个数组,且支持高效的科学计算。它可以是一个数(标量)、一维数组(向量)、二维数组(矩阵)或更高维的数组(高阶数据)。Tensor和numpy的ndarray类似,但PyTorch的tensor支持GPU加速。

基础操作

学习过Numpy的读者会对本节内容感到非常熟悉,因tensor的接口有意设计成与Numpy类似,以方便用户使用。但不熟悉Numpy也没关系,本节内容并不要求先掌握Numpy。

从接口的角度来讲,对tensor的操作可分为两类:

  1. torch.function,如torch.save等。
  2. 另一类是tensor.function,如tensor.view等。

为方便使用,对tensor的大部分操作同时支持这两类接口,在本书中不做具体区分,如torch.sum (torch.sum(a, b))tensor.sum (a.sum(b))功能等价。

而从存储的角度来讲,对tensor的操作又可分为两类:

  1. 不会修改自身的数据,如 a.add(b), 加法的结果会返回一个新的tensor。
  2. 会修改自身的数据,如 a.add_(b), 加法的结果仍存储在a中,a被修改了。

函数名以_结尾的都是inplace方式, 即会修改调用者自己的数据,在实际应用中需加以区分。

创建Tensor

在PyTorch中新建tensor的方法有很多,具体如表3-1所示。

表3-1: 常见新建tensor的方法

函数 功能
Tensor(*sizes) 基础构造函数
tensor(data,) 类似np.array的构造函数
ones(*sizes) 全1Tensor
zeros(*sizes) 全0Tensor
eye(*sizes) 对角线为1,其他为0
arange(s,e,step) 从s到e,步长为step
linspace(s,e,steps) 从s到e,均匀切分成steps份
rand/randn(*sizes) 均匀/标准分布
normal(mean,std)/uniform(from,to) 正态分布/均匀分布
randperm(m) 随机排列

这些创建方法都可以在创建的时候指定数据类型dtype和存放device(cpu/gpu).

其中使用Tensor函数新建tensor是最复杂多变的方式,它既可以接收一个list,并根据list的数据新建tensor,也能根据指定的形状新建tensor,还能传入其他的tensor,下面举几个例子。

# 指定tensor的形状
a = t.Tensor(2, 3)
a # 数值取决于内存空间的状态,print时候可能overflow

运行结果:
tensor([[3.2505e+21, 2.6076e-09, 3.2724e+21],
[1.0617e-08, 4.1961e-08, 2.6557e-06]])

# 用list的数据创建tensor
b = t.Tensor([[1,2], [3,4]])
b

运行结果:
tensor([[1., 2.],
[3., 4.]])

b.tolist() # 把tensor转为list

运行结果:
[[1.0, 2.0], [3.0, 4.0]]

tensor.size()返回torch.Size对象,它是tuple的子类,但其使用方式与tuple略有区别

b_size = b.size()
b_size

运行结果:
torch.Size([2, 2])

b.numel() # b中元素总个数,2*2,等价于b.nelement()

运行结果:
4

# 创建一个和b形状一样的tensor
c = t.Tensor(b_size)
# 创建一个元素为2和2的tensor
d = t.Tensor((2, 2))
print(c)
print(d)

运行结果:
tensor([[ 0.0000e+00, 0.0000e+00],
[-1.1794e+09, 4.5914e-41]])
tensor([2., 2.])

除了tensor.size(),还可以利用tensor.shape直接查看tensor的形状,tensor.shape等价于tensor.size()

c.shape

运行结果:
torch.Size([2, 2])

需要注意的是,t.Tensor(*sizes)创建tensor时,系统不会马上分配空间,只是会计算剩余的内存是否足够使用,使用到tensor时才会分配,而其它操作都是在创建完tensor之后马上进行空间分配。其它常用的创建tensor的方法举例如下。

a = t.ones(2, 3)
print(a)

运行结果:
tensor([[1., 1., 1.],
[1., 1., 1.]])

b = t.zeros(2, 3)
b

运行结果:
tensor([[0., 0., 0.],
[0., 0., 0.]])

c = t.arange(1, 6, 2)
c

运行结果:
tensor([1, 3, 5])

d = t.linspace(1, 10, 3)
d

运行结果:
tensor([ 1.0000, 5.5000, 10.0000])

e = t.eye(2, 3, dtype=t.int) # 对角线为1, 不要求行列数一致
print(e)

运行结果:
tensor([[1, 0, 0],
[0, 1, 0]], dtype=torch.int32)

torch.tensor是在0.4版本新增加的一个新版本的创建tensor方法,使用的方法,和参数几乎和np.array完全一致

a = t.tensor(1)
print('a = %s, a_scalar = %s' %(a, a.shape))

运行结果:
a = tensor(1), a_scalar = torch.Size([])

b = t.tensor([1,2])
print('b = %s, b_vetor = %s' % (b, b.shape))

运行结果:
b = tensor([1, 2]), b_vecor = torch.Size([2])

c = t.tensor([[1,2], [3,4]])
print('c = %s, c_matrix = %s' % (c, c.shape))

运行结果:
c = tensor([[1, 2],
[3, 4]]), c_matrix = torch.Size([2, 2])

tensor = t.Tensor(1,2) # 注意和t.tensor([1, 2])的区别
tensor.shape

运行结果:
torch.Size([1, 2])

常用Tensor操作

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

a = t.arange(0, 6)
b = a.view(2, 3)
print(a.shape)
print(a.view(2, 3))
print(a.shape)
print(b.shape)

运行结果:
torch.Size([6])
tensor([[0, 1, 2],
[3, 4, 5]])
torch.Size([6])
torch.Size([2, 3])

b.unsqueeze(1) # 注意形状,在第1维(下标从0开始)上增加“1”
#等价于 b[:,None]
b[:, None].shape

运行结果:
torch.Size([2, 1, 3])

b.unsqueeze(-2) # -2表示倒数第二个维度

运行结果:
tensor([[[0, 1, 2]],
[[3, 4, 5]]])

a = t.arange(0, 6)# a修改,b作为view之后的,也会跟着修改
b = a.view(2, 3)
a[1] = 10
b

运行结果:
tensor([[ 0, 10, 2],
[ 3, 4, 5]])

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

b.resize_(1, 3)
b

运行结果:
tensor([[ 0, 10, 2]])

b.resize_(3, 3) # 旧的数据依旧保存着,多出的大小会分配新空间
b

运行结果:
tensor([[ 0, 10, 2],
[ 3, 4, 5],
[32932988893659237, 29273792722501740, 31244220638625897]])

索引操作

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

a = t.randn(3, 4)
a

运行结果:
tensor([[-0.8151, -0.2518, 0.8990, -1.2529],
[ 0.9100, 1.6483, -1.2966, 1.6414],
[-0.1959, 0.2494, -0.4332, 0.3404]])

a[0] # 第0行(下标从0开始)

运行结果:
tensor([-0.8151, -0.2518, 0.8990, -1.2529])

a[:, 0] # 第0列

运行结果:
tensor([-0.8151, 0.9100, -0.1959])

a[0][2] # 第0行第2个元素,等价于a[0, 2]

运行结果:
tensor(-0.8990)

a[0, -1] # 第0行最后一个元素

运行结果:
tensor(-1.2529)

# None类似于np.newaxis, 为a新增了一个轴
# 等价于a.view(1, a.shape[0], a.shape[1])
a[None].shape

运行结果:
torch.Size([1, 3, 4])

a[None].shape # 等价于a[None,:,:]

运行结果:
torch.Size([1, 3, 4])

a[:,None,:].shape

运行结果:
torch.Size([3, 1, 4])

a[:,None,:,None,None].shape

运行结果:
torch.Size([3, 1, 4, 1, 1])

a = t.arange(0, 16).view(4, 4)
a

运行结果:
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])

# 选取对角线的元素
index = t.LongTensor([[0,1,2,3]])
a.gather(0, index)

运行结果:
tensor([[ 0, 5, 10, 15]])

# 选取反对角线上的元素
index = t.LongTensor([[3,2,1,0]]).t()
a.gather(1, index)

运行结果:
tensor([[ 3],
[ 6],
[ 9],
[12]])

# 选取两个对角线上的元素
index = t.LongTensor([[0,1,2,3],[3,2,1,0]]).t()
b = a.gather(1, index)
b

运行结果:
tensor([[ 0, 3],
[ 5, 6],
[10, 9],
[15, 12]])

gather相对应的逆操作是scatter_gather把数据从input中按index取出,而scatter_是把取出的数据再放回去。注意scatter_函数是inplace操作。

out = input.gather(dim, index)
-->近似逆操作
out = Tensor()
out.scatter_(dim, index)
# 把两个对角线元素放回去到指定位置
c = t.zeros(4,4)
c.scatter_(1, index, b.float())

运行结果:
tensor([[ 0., 0., 0., 3.],
[ 0., 5., 6., 0.],
[ 0., 9., 10., 0.],
[12., 0., 0., 15.]])

对tensor的任何索引操作仍是一个tensor,想要获取标准的python对象数值,需要调用tensor.item(), 这个方法只对包含一个元素的tensor适用

a = t.arange(0, 16).view(4, 4)
a[0, 0] #tensor中单个元素仍然为tensor

运行结果:
tensor(0)

a[0, 0].item()

运行结果:
0

d = a[0:1, 0:1, None]
print(d.shape)
d.item() # 只包含一个元素的tensor即可调用tensor.item,与形状无关

运行结果:
torch.Size([1, 1, 1])
0

高级索引

PyTorch在0.2版本中完善了索引操作,目前已经支持绝大多数numpy的高级索引。高级索引可以看成是普通索引操作的扩展,但是高级索引操作的结果一般不和原始的Tensor共享内存。

a = t.arange(0, 27).view(3,3,3)
a

运行结果:
tensor([[[ 0, 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]]])

a[[1, 2], [2, 2], [1, 0]]

运行结果:
tensor([16, 24])

a[[0, 2], ]

运行结果:
tensor([[[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8]],
[[18, 19, 20],
[21, 22, 23],
[24, 25, 26]]])

Tensor类型

Tensor有不同的数据类型,如表3-3所示,每种类型分别对应有CPU和GPU版本(HalfTensor除外)。默认的tensor是FloatTensor,可通过t.set_default_tensor_type 来修改默认tensor类型(如果默认类型为GPU tensor,则所有操作都将在GPU上进行)。Tensor的类型对分析内存占用很有帮助。例如对于一个size为(1000, 1000, 1000)的FloatTensor,它有1000*1000*1000=10^9个元素,每个元素占32bit/8 = 4Byte内存,所以共占大约4GB内存/显存。HalfTensor是专门为GPU版本设计的,同样的元素个数,显存占用只有FloatTensor的一半,所以可以极大缓解GPU显存不足的问题,但由于HalfTensor所能表示的数值大小和精度有限[^2],所以可能出现溢出等问题。

Data type dtype CPU tensor GPU tensor
32-bit floating point torch.float32 or torch.float torch.FloatTensor torch.cuda.FloatTensor
64-bit floating point torch.float64 or torch.double torch.DoubleTensor torch.cuda.DoubleTensor
16-bit floating point torch.float16 or torch.half torch.HalfTensor torch.cuda.HalfTensor
8-bit integer (unsigned) torch.uint8 torch.ByteTensor torch.cuda.ByteTensor
8-bit integer (signed) torch.int8 torch.CharTensor torch.cuda.CharTensor
16-bit integer (signed) torch.int16 or torch.short torch.ShortTensor torch.cuda.ShortTensor
32-bit integer (signed) torch.int32 or torch.int torch.IntTensor torch.cuda.IntTensor
64-bit integer (signed) torch.int64 or torch.long torch.LongTensor torch.cuda.LongTensor

各数据类型之间可以互相转换,type(new_type)是通用的做法,同时还有floatlonghalf等快捷方法。CPU tensor与GPU tensor之间的互相转换通过tensor.cudatensor.cpu方法实现,此外还可以使用tensor.to(device)。Tensor还有一个new方法,用法与t.Tensor一样,会调用该tensor对应类型的构造函数,生成与当前tensor类型一致的tensor。torch.*_like(tensora) 可以生成和tensora拥有同样属性(类型,形状,cpu/gpu)的新tensor。 tensor.new_*(new_shape) 新建一个不同形状的tensor。

# 设置默认tensor,注意参数是字符串
t.set_default_tensor_type('torch.DoubleTensor')
a = t.Tensor(2,3)
a.dtype # 现在a是DoubleTensor,dtype是float64

运行结果:
torch.float64

# 恢复之前的默认设置
t.set_default_tensor_type('torch.FloatTensor')
# 把a转成FloatTensor,等价于b=a.type(t.FloatTensor)
b = a.float()
b.dtype

运行结果:
torch.float32

c = a.type_as(b)
c

运行结果:
tensor([[0., 0., 0.],
[0., 0., 0.]])

a.new(2,3) # 等价于torch.DoubleTensor(2,3),建议使用a.new_tensor

运行结果:
tensor([[0., 0., 0.],
[0., 0., 0.]], dtype=torch.float64)

t.zeros_like(a) #等价于t.zeros(a.shape,dtype=a.dtype,device=a.device)

运行结果:
tensor([[0., 0., 0.],
[0., 0., 0.]], dtype=torch.float64)

t.zeros_like(a, dtype=t.int16) #可以修改某些属性

运行结果:
tensor([[0, 0, 0],
[0, 0, 0]], dtype=torch.int16)

t.rand_like(a)

运行结果:
tensor([[0.9273, 0.8291, 0.5178],
[0.0474, 0.6219, 0.3060]], dtype=torch.float64)

a.new_ones(4,5, dtype=t.int)

运行结果:
tensor([[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]], dtype=torch.int32)

a.new_tensor([3,4])

运行结果:
tensor([3., 4.], dtype=torch.float64)

逐元素操作

这部分操作会对tensor的每一个元素(point-wise,又名element-wise)进行操作,此类操作的输入与输出形状一致。

函数 功能
abs/sqrt/div/exp/fmod/log/pow… 绝对值/平方根/除法/指数/求余/求幂…
cos/sin/asin/atan2/cosh… 相关三角函数
ceil/round/floor/trunc 上取整/四舍五入/下取整/只保留整数部分
clamp(input, min, max) 超过min和max部分截断
sigmod/tanh… 激活函数

对于很多操作,例如div、mul、pow、fmod等,PyTorch都实现了运算符重载,所以可以直接使用运算符。如a ** 2 等价于torch.pow(a,2), a * 2等价于torch.mul(a,2)

其中clamp(x, min, max)的输出满足以下公式:
yi={min,if xi<minxi,if min≤xi≤maxmax,if xi>maxy_i = \begin{cases} min, & \text{if } x_i \lt min \\ x_i, & \text{if } min \le x_i \le max \\ max, & \text{if } x_i \gt max\\ \end{cases} yi​=⎩⎪⎨⎪⎧​min,xi​,max,​if xi​<minif min≤xi​≤maxif xi​>max​
clamp常用在某些需要比较大小的地方,如取一个tensor的每个元素与另一个数的较大值。

a = t.arange(0, 6).view(2, 3).float()
a.cos()

运行结果:
tensor([[ 1.0000, 0.5403, -0.4161],
[-0.9900, -0.6536, 0.2837]])

a % 3 # 等价于t.fmod(a, 3)

运行结果:
tensor([[0., 1., 2.],
[0., 1., 2.]])

a ** 2 # 等价于t.pow(a, 2)

运行结果:
tensor([[ 0., 1., 4.],
[ 9., 16., 25.]])

# 取a中的每一个元素与3相比较大的一个 (小于3的截断成3)
print(a)
t.clamp(a, min=3)

运行结果:
tensor([[0., 1., 2.],
[3., 4., 5.]])
tensor([[3., 3., 3.],
[3., 4., 5.]])

b = a.sin_() # 效果同 a = a.sin();b=a ,但是更高效节省显存
a

运行结果:
tensor([[ 0.0000, 0.8415, 0.9093],
[ 0.1411, -0.7568, -0.9589]])

归并操作

此类操作会使输出形状小于输入形状,并可以沿着某一维度进行指定操作。如加法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

a = t.ones(3, 3)
a.sum()

运行结果:
tensor(9.)

a.sum(dim = 0)

运行结果:
tensor([3., 3., 3.])

a.sum(dim = 0, keepdim = True)

运行结果:
tensor([[3., 3., 3.]])

a = t.arange(0, 6).view(2, 3)
print(a)
a.cumsum(dim=1) # 沿着行累加

运行结果:
tensor([[0, 1, 2],
[3, 4, 5]])
tensor([[ 0, 1, 3],
[ 3, 7, 12]])

比较

比较函数中有一些是逐元素比较,操作类似于逐元素操作,还有一些则类似于归并操作。

函数 功能
gt/lt/ge/le/eq/ne 大于/小于/大于等于/小于等于/等于/不等
topk 最大的k个数
sort 排序
max/min 比较两个tensor最大最小值

表中第一行的比较操作已经实现了运算符重载,因此可以使用a>=ba>ba!=ba==b,其返回结果是一个ByteTensor,可用来选取元素。max/min这两个操作比较特殊,以max来说,它有以下三种使用情况:

  • t.max(tensor):返回tensor中最大的一个数
  • t.max(tensor,dim):指定维上最大的数,返回tensor和下标
  • t.max(tensor1, tensor2): 比较两个tensor相比较大的元素

至于比较一个tensor和一个数,可以使用clamp函数。下面举例说明。

a = t.linspace(0, 15, 6).view(2, 3)
a

运行结果:
tensor([[ 0., 3., 6.],
[ 9., 12., 15.]])

b = t.linspace(15, 0, 6).view(2, 3)
b

运行结果:
tensor([[15., 12., 9.],
[ 6., 3., 0.]])

a>b

运行结果:
tensor([[False, False, False],
[ True, True, True]])

t.gt(a, b)

运行结果:
tensor([[False, False, False],
[ True, True, True]])

a.gt(b)

运行结果:
tensor([[False, False, False],
[ True, True, True]])

a[a>b] # a中大于b的元素

运行结果:
tensor([ 9., 12., 15.])

t.max(a)

运行结果:
tensor(15.)

t.max(b, dim=1)
# 第一个返回值的15和6分别表示第0行和第1行最大的元素
# 第二个返回值的0和0表示上述最大的数是该行第0个元素

运行结果:
torch.return_types.max(
values=tensor([15., 6.]),
indices=tensor([0, 0]))

t.max(a,b)

运行结果:
tensor([[15., 12., 9.],
[ 9., 12., 15.]])

# 比较a和10较大的元素
t.clamp(a, min=10)

运行结果:
tensor([[10., 10., 10.],
[10., 12., 15.]])

线性代数

PyTorch的线性函数主要封装了Blas和Lapack,其用法和接口都与之类似。

函数 功能
trace 对角线元素之和(矩阵的迹)
diag 对角线元素
triu/tril 矩阵的上三角/下三角,可指定偏移量
mm/bmm 矩阵乘法,batch的矩阵乘法
addmm/addbmm/addmv/addr/badbmm… 矩阵运算
t 转置
dot/cross 内积/外积
inverse 求逆矩阵
svd 奇异值分解

需要注意的是,矩阵的转置会导致存储空间不连续,需调用它的.contiguous方法将其转为连续。

b = a.t()
b.is_contiguous()

运行结果:
False

b.contiguous()
b.is_contiguous()

运行结果:
True

3.2 Tensor和Numpy

Tensor和Numpy数组之间具有很高的相似性,彼此之间的互操作也非常简单高效。需要注意的是,Numpy和Tensor共享内存。由于Numpy历史悠久,支持丰富的操作,所以当遇到Tensor不支持的操作时,可先转成Numpy数组,处理后再转回tensor,其转换开销很小。

import numpy as np
a = np.ones([2, 3],dtype=np.float32)
a

运行结果:
array([[1., 1., 1.],
[1., 1., 1.]], dtype=float32)

b = t.from_numpy(a)
b

运行结果:
tensor([[1., 1., 1.],
[1., 1., 1.]])

b = t.Tensor(a) # 也可以直接将numpy对象传入Tensor
b

运行结果:
tensor([[1., 1., 1.],
[1., 1., 1.]])

a[0, 1]=100
b

运行结果:
tensor([[ 1., 100., 1.],
[ 1., 1., 1.]])

c = b.numpy() # a, b, c三个对象共享内存
c

运行结果:
array([[ 1., 100., 1.],
[ 1., 1., 1.]], dtype=float32)

注意: 当numpy的数据类型和Tensor的类型不一样的时候,数据会被复制,不会共享内存。

tensor = t.tensor(a)
tensor[0,0]=0
a

运行结果:
array([[ 1., 100., 1.],
[ 1., 1., 1.]], dtype=float32)

广播法则

广播法则(broadcast)是科学运算中经常使用的一个技巧,它在快速执行向量化的同时不会占用额外的内存/显存。
Numpy的广播法则定义如下:

  • 让所有输入数组都向其中shape最长的数组看齐,shape中不足的部分通过在前面加1补齐
  • 两个数组要么在某一个维度的长度一致,要么其中一个为1,否则不能计算
  • 当输入数组的某个维度的长度为1时,计算时沿此维度复制扩充成一样的形状

PyTorch当前已经支持了自动广播法则,但是笔者还是建议读者通过以下两个函数的组合手动实现广播法则,这样更直观,更不易出错:

  • unsqueeze或者view,或者tensor[None],:为数据某一维的形状补1,实现法则1
  • expand或者expand_as,重复数组,实现法则3;该操作不会复制数组,所以不会占用额外的空间。

注意,repeat实现与expand相类似的功能,但是repeat会把相同数据复制多份,因此会占用额外的空间。

a = t.ones(3, 2)
b = t.zeros(2, 3,1)
# 自动广播法则
# 第一步:a是2维,b是3维,所以先在较小的a前面补1 ,
#               即:a.unsqueeze(0),a的形状变成(1,3,2),b的形状是(2,3,1),
# 第二步:   a和b在第一维和第三维形状不一样,其中一个为1 ,
#               可以利用广播法则扩展,两个形状都变成了(2,3,2)
a+b

运行结果:
tensor([[[1., 1.],
[1., 1.],
[1., 1.]],

    [[1., 1.],[1., 1.],[1., 1.]]])
# 手动广播法则
# 或者 a.view(1,3,2).expand(2,3,2)+b.expand(2,3,2)
a[None].expand(2, 3, 2) + b.expand(2,3,2)

运行结果:
tensor([[[1., 1.],
[1., 1.],
[1., 1.]],

    [[1., 1.],[1., 1.],[1., 1.]]])

3.3 内部结构

tensor的数据结构分为头信息区(Tensor)和存储区(Storage),信息区主要保存着tensor的形状(size)、步长(stride)、数据类型(type)等信息,而真正的数据则保存成连续数组。由于数据动辄成千上万,因此信息区元素占用内存较少,主要内存占用则取决于tensor中元素的数目,也即存储区的大小。

一般来说一个tensor有着与之相对应的storage, storage是在data之上封装的接口,便于使用,而不同tensor的头信息一般不同,但却可能使用相同的数据。

a = t.arange(0, 16)
a.storage()

运行结果:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[torch.LongStorage of size 16]

b = a.view(4, 4)
b.storage()

运行结果:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[torch.LongStorage of size 16]

# 一个对象的id值可以看作它在内存中的地址
# storage的内存地址一样,即是同一个storage
id(b.storage()) == id(a.storage())

运行结果:
True

# a改变,b也随之改变,因为他们共享storage
a[1] = 100
b

运行结果:
tensor([[ 0, 100, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[ 12, 13, 14, 15]])

c = a[2:]
c.storage()

运行结果:
0
100
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[torch.LongStorage of size 16]

c.data_ptr(), a.data_ptr() # data_ptr返回tensor首元素的内存地址
# 可以看出相差16,这是因为4*4=16--相差四个元素,每个元素占4个字节(float)

运行结果:
(2096723957648, 2096723957632)

c[0] = -100 # c[0]的内存地址对应a[2]的内存地址
a

运行结果:
tensor([ 0, 100, -100, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15])

d = t.LongTensor(c.storage())
d[0] = 6666
b

运行结果:
tensor([[6666, 100, -100, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[ 12, 13, 14, 15]])

# 下面4个tensor共享storage
id(a.storage()) == id(b.storage()) == id(c.storage()) == id(d.storage())

运行结果:
True

a.storage_offset(), c.storage_offset(), d.storage_offset()

运行结果:
(0, 2, 0)

e = b[::2, ::2] # 隔2行/列取一个元素
id(e.storage()) == id(a.storage())

运行结果:
True

b.stride(), e.stride()

运行结果:
((4, 1), (8, 2))

e.is_contiguous()

运行结果:
False

可见绝大多数操作并不修改tensor的数据,而只是修改了tensor的头信息。这种做法更节省内存,同时提升了处理速度。在使用中需要注意。
此外有些操作会导致tensor不连续,这时需调用tensor.contiguous方法将它们变成连续的数据,该方法会使数据复制一份,不再与原来的数据共享storage。
另外读者可以思考一下,之前说过的高级索引一般不共享stroage,而普通索引共享storage,这是为什么?(提示:普通索引可以通过只修改tensor的offset,stride和size,而不修改storage来实现)。

3.4 其它有关Tensor的话题

GPU/CPU

tensor可以很随意的在gpu/cpu上传输。使用tensor.cuda(device_id)或者tensor.cpu()。另外一个更通用的方法是tensor.to(device)

a = t.randn(3, 4)
a.device

运行结果:
device(type=‘cpu’)

if t.cuda.is_available():a = t.randn(3,4, device=t.device('cuda:1'))# 等价于# a.t.randn(3,4).cuda(1)# 但是前者更快a.device

运行结果:
device(type=‘cpu’)

device = t.device('cpu')
a.to(device)

运行结果:
tensor([[-0.9587, 0.4530, -0.8041, -0.0767],
[-0.4112, 0.6370, -0.7862, 0.4911],
[ 0.5726, -2.2863, -0.2148, 0.7132]])

注意

  • 尽量使用tensor.to(device), 将device设为一个可配置的参数,这样可以很轻松的使程序同时兼容GPU和CPU
  • 数据在GPU之中传输的速度要远快于内存(CPU)到显存(GPU), 所以尽量避免频繁的在内存和显存中传输数据。

持久化

Tensor的保存和加载十分的简单,使用t.save和t.load即可完成相应的功能。在save/load时可指定使用的pickle模块,在load时还可将GPU tensor映射到CPU或其它GPU上。

if t.cuda.is_available():a = a.cuda(1) # 把a转为GPU1上的tensor,t.save(a,'a.pth')# 加载为b, 存储于GPU1上(因为保存时tensor就在GPU1上)b = t.load('a.pth')# 加载为c, 存储于CPUc = t.load('a.pth', map_location=lambda storage, loc: storage)# 加载为d, 存储于GPU0上d = t.load('a.pth', map_location={'cuda:1':'cuda:0'})

向量化

向量化计算是一种特殊的并行计算方式,相对于一般程序在同一时间只执行一个操作的方式,它可在同一时间执行多个操作,通常是对不同的数据执行同样的一个或一批指令,或者说把指令应用于一个数组/向量上。向量化可极大提高科学运算的效率,Python本身是一门高级语言,使用很方便,但这也意味着很多操作很低效,尤其是for循环。在科学计算程序中应当极力避免使用Python原生的for循环

def for_loop_add(x, y):result = []for i,j in zip(x, y):result.append(i + j)return t.Tensor(result)x = t.zeros(100)
y = t.ones(100)
%timeit -n 10 for_loop_add(x, y)
%timeit -n 10 x + y

运行结果:
1.29 ms ± 518 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
The slowest run took 11.88 times longer than the fastest. This could mean that an intermediate result is being cached.
7.73 µs ± 11.4 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

可见二者有超过几十倍的速度差距,因此在实际使用中应尽量调用内建函数(buildin-function),这些函数底层由C/C++实现,能通过执行底层优化实现高效计算。因此在平时写代码时,就应养成向量化的思维习惯,千万避免对较大的tensor进行逐元素遍历。

此外还有以下几点需要注意:

  • 大多数t.function都有一个参数out,这时候产生的结果将保存在out指定tensor之中。
  • t.set_num_threads可以设置PyTorch进行CPU多线程并行计算时候所占用的线程数,这个可以用来限制PyTorch所占用的CPU数目。
  • t.set_printoptions可以用来设置打印tensor时的数值精度和格式。
    下面举例说明。
a = t.arange(0, 20000000)
print(a[-1], a[-2]) # 32bit的IntTensor精度有限导致溢出
b = t.LongTensor()
t.arange(0, 20000000, out=b) # 64bit的LongTensor不会溢出
print(b[-1],b[-2])

运行结果:
tensor(19999999) tensor(19999998)
tensor(19999999) tensor(19999998)

a = t.randn(2,3)
a

运行结果:
tensor([[-1.5487, -1.1022, -0.9679],
[ 0.9768, 0.4358, -2.4326]])

t.set_printoptions(precision=10)
a

运行结果:
tensor([[-1.5486671925, -1.1021592617, -0.9678921700],
[ 0.9767541289, 0.4357590973, -2.4326353073]])

3.5 小试牛刀:线性回归

线性回归是机器学习入门知识,应用十分广泛。线性回归利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的,其表达形式为y=wx+b+ey = wx+b+ey=wx+b+e,eee为误差服从均值为0的正态分布。首先让我们来确认线性回归的损失函数:
loss=∑iN12(yi−(wxi+b))2loss = \sum_i^N \frac 1 2 ({y_i-(wx_i+b)})^2 loss=i∑N​21​(yi​−(wxi​+b))2
然后利用随机梯度下降法更新参数w\textbf{w}w和b\textbf{b}b来最小化损失函数,最终学得w\textbf{w}w和b\textbf{b}b的数值。

import numpy as np
import pandas as pd
import torch as t
import matplotlib.pyplot as plt
t.manual_seed(1000)
def get_fake_data(batch_size = 8):x = t.rand(batch_size, 1)*20y = x * 2 + 3 + t.randn(batch_size, 1)return x, y
x, y = get_fake_data()
plt.figure(figsize = (8, 5))
plt.scatter(x, y, label = 'trainning data')
plt.legend()
plt.show()

运行结果:

w = t.rand(1, 1, requires_grad = True)
b = t.zeros(1, 1, requires_grad = True)
lr = 1e-3
def computerLoss(pres, y):return t.sum(0.5*(pres - y)**2)
pres = x@w + b
loss = computerLoss(pres, y)
loss

运行结果:
tensor(1145.5450, grad_fn=)

x, y = get_fake_data()
x1 = t.linspace(0, 25, 8).view(8, 1)
y1 = x1*w.data + b.data
plt.figure(figsize = (8, 5))
plt.scatter(x, y, label = 'trainning data')
plt.plot(x1, y1, label = 'ppredict data', c = 'r')
plt.legend()
plt.show()

运行结果:

for i in range(1000):pres = x@w + bloss = computerLoss(pres, y)loss.backward()w.data -= w.grad.data * lrb.data -= b.grad.data * lrw.grad.zero_()b.grad.zero_()
x, y = get_fake_data()
x1 = t.linspace(0, 25, 8).view(8, 1)
y1 = x1*w.data + b.data
plt.figure(figsize = (8, 5))
plt.scatter(x, y, label = 'trainning data')
plt.plot(x1, y1, label = 'ppredict data', c = 'r')
plt.legend()
plt.show()

运行结果:

4 神经网络工具箱nn

autograd实现了自动微分系统,然而对深度学习来说过于底层,本章将介绍的nn模块,是构建于autograd之上的神经网络模块。除了nn之外,我们还会介绍神经网络中常用的工具,比如优化器optim、初始化init等。

4.1 nn.Module

第3章中提到,使用autograd可实现深度学习模型,但其抽象程度较低,如果用其来实现深度学习模型,则需要编写的代码量极大。在这种情况下,torch.nn应运而生,其是专门为深度学习设计的模块。torch.nn的核心数据结构是Module,它是一个抽象的概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法继承nn.Module,撰写自己的网络/层。下面先来看看如何使用nn.Module实现自己的全连接层。全连接层,又名仿射层,输入y和输入x满足y=Wx+b,W和b是可学习的参数。

import numpy as np
import torch as t
from torch import nn
# 定义线性模型:y = w * x + b
class Linear(nn.Module):    # 继承nn.Moduledef __init__(self,in_features,out_features):nn.Module.__init__(self)    # 等价于nn.Module.__init__(self)self.w = nn.Parameter(t.randn(in_features,out_features))self.b = nn.Parameter(t.randn(out_features))def forward(self,x):xw = x.mm(self.w)y = xw + self.b.expand_as(xw)return y
net = Linear(4, 3)
x = t.randn(2, 4)
y = net(x)
y

运行结果:
tensor([[-0.7086, 0.7078, 2.0764],
[ 1.3967, 1.7351, 1.3748]], grad_fn=)

for name, parameter in net.named_parameters():print(name, parameter)

运行结果:
w Parameter containing:
tensor([[-0.3579, -0.7854, -1.0309],
[ 0.4088, 2.4716, -1.1229],
[-0.0056, -1.9836, -0.1046],
[ 1.1730, 0.1006, 1.0346]], requires_grad=True)
b Parameter containing:
tensor([0.2090, 1.4149, 1.4662], requires_grad=True)

可见,全连接层的实现非常简单,其代码量不超过10行,但需注意以下几点:

  • 自定义层Linear必须继承nn.Module,并且在其构造函数中需调用nn.Module的构造函数,即super(Linear,self).init()或nn.Module.init(self)。
  • 在构造函数init中必须自己定义可学习的参数,并封装成Parameter,如在本例中我们把w和b封装成Parameter。Parameter是一种特殊的Variable,但其默认需要求导(requires_grad=True),感兴趣的读者可以通过nn.Parameter??查看Parameter类的源代码。
  • forward函数实现前向传播过程,其输入可以是一个或多个variable,对x的任何操作也必须是variable支持的操作。
  • 无须写反向传播函数,因其前向传播都是对variable进行操作,nn.Module能够利用autograd自动实现反向传播,这一点比Function简单许多。
  • 使用时,直观上可将net看成数学概念中的函数,调用net(x)即可得到x对应的结果。它等价于net.call(x),在call函数中,主要调用的是net.forward(x),另外还对钩子做了一些处理。所以在实际使用中应尽量使用net(x)而不是使用net.forward(x),关于钩子技术的具体内容将在下文讲到。
  • Module中的可学习参数可以通过named_parameters()或者parameters()返回迭代器,前者会给每个parameter附上名字,使其更具有辨识度。

可见,利用Module实现的全连接层,比利用Function实现的更简单,因其不再需要写反向传播函数。

Module能够自动检测到自己的parameter,并将其作为学习参数。除了parameter,Module还包含子Module,主Module能够递归查找子Module中的parameter。下面再来看看稍微复杂一点的网络:多层感知机。

多层感知机的网络结构如图所示。它由两个全连接层组成,采用sigmoid函数作为激活函数(图中没有画出)。

# 定义多层感知机
class Perceptron(nn.Module): # 继承nn.Moduledef __init__(self, in_features, hidden_features, out_features):super().__init__()self.layer1 = Linear(in_features, hidden_features)self.layer2 = Linear(hidden_features, out_features)def forward(self, x):y = self.layer1(x)y = self.layer2(y)return y
perceptron = Perceptron(3, 4, 1)
x = t.randn(2, 3)
y =perceptron(x)
y

运行结果:
tensor([[-2.2504],
[-0.5843]], grad_fn=)

for name, parameter in perceptron.named_parameters():print(name, parameter)

运行结果:
layer1.w Parameter containing:
tensor([[-0.5368, -0.6077, -1.3088, -0.1669],
[-0.2287, 0.3532, -2.0303, 0.6538],
[-1.0113, -1.9296, -0.2059, 1.3605]], requires_grad=True)
layer1.b Parameter containing:
tensor([ 0.9178, 1.1564, 1.1628, -0.6162], requires_grad=True)
layer2.w Parameter containing:
tensor([[-0.2354],
[-0.7007],
[ 0.4901],
[ 0.3775]], requires_grad=True)
layer2.b Parameter containing:
tensor([0.6461], requires_grad=True)

可见,即使是稍复杂的多层感知机,其实现依旧很简单。这里需要注意以下两个知识点。

  • 构造函数init中,可利用前面自定义的Linear层(Module)作为当前Module对象的一个子Module,它的可学习参数,也会成为当前Module的可学习参数。

  • 在前向传播函数中,我们有意识地将输出变量都命名为y,是为了能让Python回收一些中间层的输出,从而节省内存。但并不是所有的中间结果都会被回收,有些variable虽然名字被覆盖,但其在反向传播时仍需要用到,此时Python的内存回收模块将通过检查引用计数,不会回收这一部分内存。
    Module中parameter的全局命名规范如下:

  • Parameter直接命名。例如self.param_name = nn.Parameter(t.randn(3,4)),命名为param_name。

  • 子Module中的parameter,会在其名字之前加上当前Module的名字。例如self.sub_module = SubModule(),SubModule中有个parameter的名字也叫作param_name,那么二者拼接而成的parameter name就是sub_module.param_name。

为了方便用户使用,PyTorch实现了神经网络中绝大多数的layer,这些layer都继承于nn.Module,封装了可学习参数parameter,并实现了forward函数,且专门针对GPU运算进行了CuDNN优化,其速度和性能都十分优异。本书不准备对nn.Module中的所有层进行详细介绍,具体内容读者可参照官方文档或在IPython/Jupyter中使用nn.layer?查看。阅读文档时应主要关注以下几点。

  • 构造函数的参数,如nn.Linear(in_features,out_features,bias),需关注这三个参数的作用。
  • 属性、可学习参数和子Module。如nn.Linear中有weight和bias两个可学习参数,不包含子Module。
  • 输入输出的形状,如nn.Linear的输入形状是(N,input_features),输出形状为(N,output_features),N是batch_size。

这些自定义layer对输入形状都有假设:输入的不是单个数据,而是一个batch。若想输入一个数据,必须调用unsqueeze(0)函数将数据伪装成batch_size=1的batch。

下面将从应用层面出发,对一些常用的layer做简单介绍,更详细的用法请查看官方文档。

4.2 常用的神经网络层

4.2.1 图像相关层

图像相关层主要包括卷积层(Conv)、池化层(Pool)等,这些层在实际使用中分为一维(1D)、二维(2D)和三维(3D),池化方式又分为平均池化(AvgPool)、最大值池化(MaxPool)、自适应池化(AdaptiveAvgPool)等。卷积层除了常用的前向卷积外,还有逆卷积(TransposeConv)。下面举例说明。

from PIL import Image
from torchvision.transforms import ToTensor, ToPILImage
to_tensor = ToTensor() # img -> tensor
to_pil = ToPILImage()
lena = Image.open('C:\\Users\\Inuyasha\\Desktop\\lena.bmp')
lena

运行结果:

# 输入是一个batch,batch_size=1
input = to_tensor(lena).unsqueeze(0) # 锐化卷积核
kernel = t.ones(3, 3)/-9.
kernel[1][1] = 1
conv = nn.Conv2d(1, 1, (3, 3), 1, bias=False)
conv.weight.data = kernel.view(1, 1, 3, 3)out = conv(input)
to_pil(out.data.squeeze(0))

运行结果:

池化层可以看成是一种特殊的卷积层,用来下采样。但池化层没有可学习的参数,其weight是固定的

pool = nn.AvgPool2d(2,2)
list(pool.parameters())

运行结果:
[]

out = pool(input)
to_pil(out.data.squeeze(0))

运行结果:

除了卷积层和池化层,深度学习中还将常用到以下几个层。

  • Linear:全连接层。
  • BatchNorm:批规范化层,分为1D、2D和3D。除了标准的- BatchNorm之外,还有在风格迁移中常用到的InstanceNorm层。
  • Dropout:dropout层,用于防止过拟合,同样分为1D、2D和3D。
    下面通过例子讲解它们的使用方法。
# 输入 batch_size=2,维度3
input = t.randn(2, 3)
linear = nn.Linear(3, 4)
h = linear(input)
h

运行结果:
tensor([[-0.1105, 0.1743, 0.2182, 0.2658],
[ 1.0727, 1.7890, 0.5618, 0.4677]], grad_fn=<AddmmBackward>)

# 4 channel,初始化标准差为4,均值为0
bn = nn.BatchNorm1d(4)
bn.weight.data = t.ones(4) * 4
bn.bias.data = t.zeros(4)bn_out = bn(h)
# 注意输出的均值和方差
# 方差是标准差的平方,计算无偏方差分母会减1
# 使用unbiased=False 分母不减1
bn_out.mean(0), bn_out.var(0, unbiased=False)

运行结果:
(tensor([ 0.0000e+00, 0.0000e+00, -3.5763e-07, 0.0000e+00],
grad_fn=),
tensor([15.9995, 15.9998, 15.9946, 15.9843], grad_fn=))

# 每个元素以0.5的概率舍弃
dropout = nn.Dropout(0.5)
o = dropout(bn_out)
o # 有一半左右的数变为0

运行结果:
tensor([[-0.0000, -7.9999, -7.9986, -7.9961],
[ 7.9999, 0.0000, 0.0000, 0.0000]], grad_fn=<MulBackward0>)

以上很多例子中都对Module的属性直接操作,其大多数是可学习参数,一般会随着学习的进行而不断改变。实际使用中除非需要使用特殊的初始化,否则应尽量不要直接修改这些参数。

4.2.2 激活函数

PyTorch实现了常见的激活函数,其具体的接口信息可参见官方文档。这些激活函数可作为独立的layer使用。这里将介绍最常用的激活函数ReLU,其数学表达式为:

relu = nn.ReLU(inplace=True)
input = t.randn(2, 3)
print(input)
output = relu(input)
print(output) # 小于0的都被截断为0
# 等价于input.clamp(min=0)

运行结果:
tensor([[ 0.7430, -0.1240, -1.0032],
[ 1.2273, -0.5622, -0.7272]])
tensor([[0.7430, 0.0000, 0.0000],
[1.2273, 0.0000, 0.0000]])

ReLU函数有个inplace参数,如果设为True,它会把输出直接覆盖到输入中,这样可以节省内存/显存。之所以可以覆盖是因为在计算ReLU的反向传播时,只需根据输出就能推算出反向传播的梯度。但是只有少数的autograd操作支持inplace操作(如variable.sigmoid_()),除非你明确地知道自己在做什么,否则一般不要使用inplace操作。在以上例子中,都是将每一层的输出直接作为下一层的输入,这种网络称为前馈传播网络(Feedforward Neural Network)。对于此类网络,如果每次都写复杂的forward函数会有些麻烦,在此就有两种简化方式,ModuleList和Sequential。其中Sequential是一个特殊的Module,它包含几个子Module,前向传播时会将输入一层接一层地传递下去。ModuleList也是一个特殊的Module,可以包含几个子Module,可以像用list一样使用它,但不能直接把输入传给ModuleList。下面我们举例说明。

# Sequential的三种写法
net1 = nn.Sequential()
net1.add_module('conv', nn.Conv2d(3, 3, 3))
net1.add_module('batchnorm', nn.BatchNorm2d(3))
net1.add_module('activation_layer', nn.ReLU())net2 = nn.Sequential(nn.Conv2d(3, 3, 3),nn.BatchNorm2d(3),nn.ReLU())from collections import OrderedDict
net3= nn.Sequential(OrderedDict([('conv1', nn.Conv2d(3, 3, 3)),('bn1', nn.BatchNorm2d(3)),('relu1', nn.ReLU())]))
print('net1:', net1)
print('net2:', net2)
print('net3:', net3)

运行结果:
net1: Sequential(
(conv): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(batchnorm): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(activation_layer): ReLU()
)
net2: Sequential(
(0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
net3: Sequential(
(conv1): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(bn1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu1): ReLU()
)

# 可根据名字或序号取出子module
net1.conv, net2[0], net3.conv1

运行结果:
(Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)))

input = t.rand(1, 3, 4, 4)
output = net1(input)
output = net2(input)
output = net3(input)
output = net3.relu1(net1.batchnorm(net1.conv(input)))

运行结果:

modellist = nn.ModuleList([nn.Linear(3,4), nn.ReLU(), nn.Linear(4,2)])
input = t.randn(1, 3)
for model in modellist:input = model(input)
# 下面会报错,因为modellist没有实现forward方法
# output = modelist(input)

运行结果:

看到这里,读者可能会问,为何不直接使用Python中自带的list,而非要多此一举呢?这是因为ModuleList是Module的子类,当在Module中使用它时,就能自动识别为子Module。

下面我们举例说明。

class MyModule(nn.Module):def __init__(self):super(MyModule, self).__init__()self.list = [nn.Linear(3, 4), nn.ReLU()]self.module_list = nn.ModuleList([nn.Conv2d(3, 3, 3), nn.ReLU()])def forward(self):pass
model = MyModule()
model

运行结果:
MyModule(
(module_list): ModuleList(
(0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(1): ReLU()
)
)

for name, param in model.named_parameters():print(name, param.size())

运行结果:
module_list.0.weight torch.Size([3, 3, 3, 3])
module_list.0.bias torch.Size([3])

可见,list中的子Module并不能被主Module识别,而ModuleList中的子Module能够被主Module识别。这意味着如果用list保存子Module,将无法调整其参数,因其未加入到主Module的参数中。

除ModuleList之外还有ParameterList,它是一个可以包含多个parameter的类list对象。在实际使用中,使用方式与ModuleList类似。在构造函数init中用到list、tuple、dict等对象时,一定要思考是否应该用ModuleList或ParameterList代替。

4.2.3 循环神经网络层

近些年,随着深度学习和自然语言处理的结合加深,循环神经网络(RNN)的使用也越来越多,关于RNN的基础知识,推荐阅读colah的文章
入门。PyTorch中实现了如今最常用的三种RNN:RNN(vanilla RNN)、LSTM和GRU。此外还有对应的三种RNNCell。

RNN和RNNCell层的区别在于前者能够处理整个序列,而后者一次只处理序列中一个时间点的数据,前者封装更完备更易于使用,后者更具灵活性。RNN层可以通过组合调用RNNCell来实现。


运行结果:


运行结果:


运行结果:

深度学习框架之PyTorch相关推荐

  1. DL框架:主流深度学习框架(TensorFlow/Pytorch/Caffe/Keras/CNTK/MXNet/Theano/PaddlePaddle)简介、多个方向比较、案例应用之详细攻略

    DL框架:主流深度学习框架(TensorFlow/Pytorch/Caffe/Keras/CNTK/MXNet/Theano/PaddlePaddle)简介.多个方向比较.案例应用之详细攻略 目录 深 ...

  2. 手把手教你在Windows10环境下安装深度学习框架(pytorch or tensorflow)

    手把手教你在Windows10环境下安装深度学习框架(pytorch or tensorflow) 1. 安装Anaconda:(常用的python版本和各类包管理器) 1.1. 下载地址: 1.2. ...

  3. 深度学习框架(Pytorch)+ 机器人(ROS):ROS melodic 上安装 Turtlebot2 —> 安装 hokuyo 激光雷达 —> Python 3 的虚拟环境中调用 ROS 填坑记录

    复现论文需要ubuntu+ros+turtlebot2+hokuyo的配置,这里有一个关于turtlebot的教程: Learn TurtleBot and ROS 关于 turtlebot 的各种软 ...

  4. 【深度学习框架】pytorch之分布式数据并行化DDP

    文章目录 1. 引言 2. Quick Start 3. 基本概念 4. DDP使用流程 4.1 launch启动 4.2 spawn启动 5. 不是很相关的一些bug 参考文献 1. 引言 Dist ...

  5. 第04课:深度学习框架 PyTorch

    随着深度学习的研究热潮持续高涨,各种开源深度学习框架也层出不穷,包括 TensorFlow.PyTorch.Caffe2.Keras.CNTK.MXNet.Paddle.DeepLearning4.L ...

  6. 深度学习框架PyTorch与TensorFlow,谁更胜一筹?

    全世界只有3.14 % 的人关注了 爆炸吧知识 自从2012年深度学习再一次声名鹊起以来,许多机器学习框架都争先恐后地要成为研究人员和行业从业者的新宠.面对如些众多的选择,人们很难判断最流行的框架到底 ...

  7. 深度学习:常见深度学习框架【Theano、TensorFlow、Keras、Caffe/Caffe2、MXNet、CNTK、PyTorch】

    常见的深度学习框架有 TensorFlow .Caffe.Theano.Keras.PyTorch.MXNet等,如下图所示.这些深度学习框架被应用于计算机视觉.语音识别.自然语言处理与生物信息学等领 ...

  8. 原创 | 深度学习框架比较,我该选择哪一个?

    近年来,深度学习在很多机器学习领域都有着非常出色的表现,在图像识别.语音识别.自然语言处理.机器人.网络广告投放.医学自动诊断和金融等领域有着广泛应用.面对繁多的应用场景,深度学习框架有助于建模者节省 ...

  9. 深度学习框架量化感知训练的思考及OneFlow的解决方案

    作者 | BBuf 原文首发于公众号GiantPandaCV 0x0.总览 相信不少小伙伴都了解或者使用了一些深度学习框架比如PyTorch,TensorFlow,OneFlow(也是笔者目前正在参与 ...

最新文章

  1. 2020 AI前沿交流大会 | 聚焦CV/NLP最新学术及工业界实践
  2. 视频监控日常使用存在哪些故障
  3. python课程多少钱一节课-日照少儿python编程一节课多少钱
  4. js简单的设置快捷键,hotkeys捕获键盘键和组合键的输入
  5. 使用 Dockerfile 定制镜像
  6. MySQL视图的应用
  7. excel乘法公式怎么输入_python吊打Excel?那是你不会用!
  8. 最近新明白的SQL小知识
  9. Windows Boot Manager改成中文菜单
  10. 基于Python的电商数据分析系统
  11. 戴尔台式计算机主板型号,如何查看戴尔主板型号 查看戴尔主板型号的步骤
  12. swapidc不能连接到主机_kangle easypanel对接SWAP IDC虚拟主机销售平台完整教程 (linux)...
  13. 英国火箭设计者计划组建第一家私有太空游公司
  14. linux fdisk 挂盘
  15. Python图像识别-Opencv07 异或运算,图像加密
  16. 设计师2022面试UI作品集PSD样机模板
  17. R z-score 方法检测异常值
  18. EAS中F7控件监听变化后,手动修改带出值无效
  19. Stata:嵌套Logit模型(NestedLogit)
  20. 新浪服务器显示蓝屏05c代码,互联网学问:蓝屏代码0x00000050的原因及解决方法...

热门文章

  1. C语言—判断100到200的素数
  2. Python:利用Pillow库创建字符画
  3. [转载]基于UML的需求分析和系统设计(完整案例和UML图形演示)
  4. 当当网——(第一次上课的作业)(HTML)
  5. MUR6060PT-ASEMI快恢复二极管MUR6060PT
  6. C语言数据结构静态动态查找表实验
  7. c语言不用strcpy复制字符串,c语言程序(二十三)——字符串复制(不使用strcpy()函数)...
  8. World Locking Tools for Unity Sample (二)QR Space Pins
  9. 1万块钱存入银行,10年后,连本带利能取多少钱?
  10. micro的介绍、安装与使用