官方教程:https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html
这是我在看官方教程过程中做的笔记,中间有一些东西是教程中没有讲到的,但是对理解代码,甚至是理解 pytorch 的一些思想很有用(王婆卖瓜,自卖自夸,溜…)相信你看了这一篇博客就基本入门 pytorch 了。
1. 数据处理
pytorch的数据处理类似于numpy,只不过换了个名字:tendsor
实际上就是 ndarray
。pytorch的优点在于可以利用GPU进行加速。
1.1 基本数据操作
#############
x.view(shape)
#############
# 这居然是一个renshape函数!
x = torch.randn(4,4)
x = x.view(-1,8)
print(x.size())
# result:
torch.Size([2, 8])
输出的torch.Size([2, 8])
实际上就是tuple
,可以用tuple的方式进行处理和操作。
#############
x.item()
#############
# x为tensor,输出为python正常数据
x = torch.randn(1)
print(x)
print(x.item())
# result:
tensor([-1.7816])
-1.7815848588943481
pytorch中的函数如果加上后缀 _
一般就是原地操作,也即操作之后原始变量在内存中的值会相应改变
y.add_(x)
1.2 tensor与numpy的关系
#############
b = x.numpy()
#############
# 将x转换为numpy数据类型,不过他们在内存中占用同一个地方。
# 也就是实际上b和x是同一个变量,对x进行操作之后,b的值也会响应改变
#############
b = torch.from_numpy(a)
#############
# 跟上面x.numpy()类似
1.3 cuda tensor
用.to()
实现在不同设备之间(cpu,gpu)移动数据
# let us run this cell only if CUDA is available
# We will use ``torch.device`` objects to move tensors in and out of GPU
if torch.cuda.is_available():
device = torch.device("cuda") # a CUDA device object
y = torch.ones_like(x, device=device) # directly create a tensor on GPU
x = x.to(device) # or just use strings ``.to("cuda")``
z = x + y
print(z)
print(z.to("cpu", torch.double)) # ``.to`` can also change dtype together!
##################### result ##########################
tensor([-0.7816], device='cuda:0')
tensor([-0.7816], dtype=torch.float64)
2. 自动梯度
2.1 tensor
torch.Tensor
是pytorch中的核心。如果 .requires_grad
被设置为 True
,则pytorch会记录这个tensor上的所有操作,当你调用 .backward()
时就会自动计算出所有的梯度(简直不要再方便了有木有!!!)如果想在某个操作之后停止记录对某变量的操作,使用 .detach()
即可,或者用with torch.no_grad():
包围住对应的代码块。
每个tensor都有一个 .grad_fn
属性,记录了该变量被创建时所使用的函数,
x = torch.ones(2, 2, requires_grad=True)
y = x + 2
print(y)
z = y * y * 3
out = z.mean()
print(z, out)
##################### result ##########################
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward0>)
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)
#############
y.data.norm()
#############
# 这就是一个求二范数的函数
x = torch.randn(3, requires_grad=True)
with torch.no_grad():
y = x * 2
z = torch.sqrt(torch.sum(torch.pow(y,2)))
z.requires_grad_(True)
print(x)
print(y)
print(z)
print(y.data.norm())
##################### result ##########################
tensor([-1.2562, -0.1328, 0.0917], requires_grad=True)
tensor([-2.5123, -0.2655, 0.1834])
tensor(2.5330, requires_grad=True)
tensor(2.5330)
3. 神经网络
pytorch中提供了 torch.nn
包以方便的实现神经网络,nn
依赖于前面 2. 自动梯度 中讲的 autograd
实现对权重梯度的计算。一个 nn.Module
至少包含有网络的 layers 和一个 forward
函数,该函数实现前向传播,得到网络的输出 output。而反向传播的 backward
方法会通过 autograd
自动被创建,无需我们自己定义,需要时直接调用即可。
一般来说,构建、训练神经网络包括以下步骤
- 描述网络结构,定义参数/权重
- 输入数据集,用网络进行前向传播
- 计算 loss
- 反向传播计算梯度
- 权重更新
weight = weight - learning_rate * gradient
3.1 构建网络
下面上一个非常简单的例子:
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
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))
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)
##################### result ##########################
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)
)
用 net.parameters()
方便的查看可训练的参数:
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight
##################### result ##########################
10
torch.Size([6, 1, 5, 5])
进行测试时有两种方法,得到的结果是一样的:
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
out = net.forward(input)
print(out)
##################### result ##########################
tensor([[-0.1014, 0.1486, 0.0921, -0.0425, -0.0668, -0.0370, 0.0513, 0.0671,
0.0178, -0.0078]], grad_fn=<AddmmBackward>)
tensor([[-0.1014, 0.1486, 0.0921, -0.0425, -0.0668, -0.0370, 0.0513, 0.0671,
0.0178, -0.0078]], grad_fn=<AddmmBackward>)
注意:
torch.nn
只支持mini-batch
,也就是输入的样本必须有一个 batch 维度,比如nn.Conv2d
的输入形式为 4D 的 tensor:nSamples
xnChannels
xHeight
xWidth
。如果输入只有一个样例,可以使用input.unsqueeze(0)
来增加一个 batch 为 1 的维度
3.2 损失函数
当我们调用 loss.backward()
时,整个图就会关于 loss 求出微分,那些 requires_grad=True
的 tensor 将会有一个 .grad
的 tensor 来保存梯度。
注意:调用
loss.backward()
后只是通过反向传播算法计算出了所有可训练参数的梯度,并赋给了他们的.grad
属性,但并没有对这些参数进行权重更新。如果想要更新权重,需要用到下一小节中讲的torch.optim
包。
注意:
.zero_grad()
非常重要!根据 pytorch 中的backward()
函数的计算,当网络参量进行反馈时,梯度是被积累的而不是被替换为新的值,因此需要每个 batch 都设置一遍zero_grad()
了。为什么这么设计呢?我觉得可能是因为对有的复杂网络,我们可能需要定义多个loss,对他们都计算loss之后才统一更新权重,这个时候就需要多个梯度累加了。
参考:https://discuss.pytorch.org/t/why-do-we-need-to-set-the-gradients-manually-to-zero-in-pytorch/4903/3
非常非常重要:
关于pytorch backword 更多的细节和问题可以参考:https://blog.csdn.net/douhaoexia/article/details/78821428
摘出我觉得写的非常好的一段:
计算图本质就是一个类似二叉树的结构,能获取回传梯度(grad)的只有计算图的叶节点。注意是获取,而不是求取。中间节点的梯度在计算求取并回传之后就会被释放掉,没办法获取。
假设有一个网络 x2 —> |f1| —> y2 —> |f2| —> z2 , f1 、f2 是两个普通的函数,z2=f2(y2)
,y2=f1(x2)
。那么执行一次z2.backward()
之后,发现x2.grad,w1.grad,w2.grad 都有值 ,但是 y2.grad 却是 None, 说明x2,w1,w2的梯度保留了,y2 的梯度获取不到。实际上,仔细想一想会发现,x2,w1,w2均为叶节点。在这棵计算树中 ,x2 与w1 是同一深度(底层)的叶节点,y2与w2 是同一深度,w2 是单独的叶节点,而y2 是x2 与 w1 的父节点,所以只有y2没有保留梯度值,印证了之前的说法。
与此同时也可以把网络某一部分参数,固定,不让其被训练,也就是设置requires_grad = False
。因为是叶节点(而不是中间节点),所以不求grad(grad为’None’),也不会影响网络的正常反向传播。
注意:据说 loss 必须是一个
scalar
,而不能是一个tensor
,具体为什么我还不太理解,理解后再补充。
output = net(input)
criterion = nn.MSELoss() # 定义loss函数
loss = criterion(output, ground_truth) # 计算loss
net.zero_grad() # 清理梯度,否则会积累
print(net.conv1.bias.grad)
loss.backward() # 反向传播
print(net.conv1.bias.grad)
##################### result ##########################
tensor([0., 0., 0., 0., 0., 0.])
tensor([-0.0122, -0.0004, -0.0031, -0.0044, -0.0003, -0.0003])
如果这个时候你观察 loss
的 .grad_fn
属性,你就会得到与他相关的一系列计算操作
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
比如下面的例子
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
##################### result ##########################
<MseLossBackward object at 0x7fcf8308c550>
<AddmmBackward object at 0x7fcf8308c208>
<AccumulateGrad object at 0x7fcf8308c208>
3.3 权重更新
torch提供了 torch.optim
来实现不同的优化算法
import torch.optim as optim
# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)
# in your training loop:
optimizer.zero_grad() # zero the gradient buffers
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step() # Does the update
4. 训练一个分类器
4.1 数据
torch 提供了一个包 torchvision
可以用来载入各种类型的数据,如图像、音频、文本等,免去了针对各种不同类型的数据专门寻找处理的包。它还含有一些常用数据集的 data loader,如 Imagenet,CIFAR10,MNIST等,可参见 torch.utils.data.DataLoader
,torchvision.datasets
上面这一段是翻译的官方教程,我感觉说的不太清楚,这个东西不明白不要紧,紧接着下面就是我的介绍。come on!
重要:为了便于理解,我们先来看一看
torch.utils.data.Dataset
类,他是一个抽象的数据集类。为了处理特定的数据,我们可以根据所需要处理的数据特点,继承这个类,自定义一个子类。我们自定义的子类至少要有两个属性__len__
和__getitem__
。前者返回数据集的长度,后者支持用 0~len(self) 的整数来索引数据集。
看一个例子
class RandomDataset(Dataset):
def __init__(self, size, length):
self.len = length
self.data = torch.randn(length, size)
def __getitem__(self, index):
return self.data[index]
def __len__(self):
return self.len
好,了解了 dataset
类,我们再来看一看dataloader
。
重要:
dataloader
的处理逻辑是先通过Dataset
类里面的__getitem__
函数获取单个的数据,然后组合成batch,再使用collate_fn
所指定的函数对这个batch做一些操作,比如padding啊之类的。
# 各参数的含义可以查看官方文档 https://pytorch.org/docs/stable/data.html
torch.utils.data.DataLoader(
dataset, batch_size=1, shuffle=False,
sampler=None, batch_sampler=None,
num_workers=0, collate_fn=<function default_collate>,
pin_memory=False, drop_last=False, timeout=0,
worker_init_fn=None)
好了,上面还提到一个 torchvision.datasets
,这又是个什么东西呀?
重要:简单来说,这就是 pytorch 官方提前给你定义好了一个(好多个)继承自
torch.utils.data.Dataset
的子类,他们跟前边我们自己定义的RandomDataset
并没有什么差别。比如torchvision.datasets.MNIST
就是针对 MNIST 数据集定义的一个数据集类,其他的类似,具体的应用可以参见下一小节 4.2.1 加载数据。其他已经定义好的数据集类可以参考官方文档 https://pytorch.org/docs/stable/torchvision/datasets.html
其实
torchvision
并不是仅仅为了处理数据集而设计的,除了torchvision.datasets
,他里边还定义了torchvision.models
,torchvision.transforms
,torchvision.utils
。关于他们各自的用法可以参考官方文档 https://pytorch.org/docs/stable/torchvision/index.html
4.2 训练一个图像分类器
包含以下步骤:
- 用
torchvision
加载训练和测试数据集 - 定义一个CNN
- 定义一个 loss 函数
- 训练
- 测试
4.2.1 加载数据
pytorch 载入的数据集是元组 tuple
形式,里面包括了数据及标签 (train_data, label)
,其中的 train_data 数据是 PIL 图像,灰度范围为[0,1] 可以转换为 torch.Tensor 形式。torchvision.transforms
是pytorch中的图像预处理包,可以用torchvision.transforms.Compose
把多个步骤整合到一起。transforms
中有以下常用函数
transforms.ToTensor() # 把PIL图像(H*W*C,灰度[0,255])转化为torch.Tensor(C*H*W,灰度[0.0,1.0])
transforms.ToPILImage() # 把tensor转化为PIL图像
transforms.Resize(size,interpolation=2) # 把给定的PIL图片resize到given size
transforms.Grayscale(num_output_channels=1) # 将PIL图像转换为灰度图像
transforms.*Crop(size) # 对PIL图像进行各种裁剪
transforms.Normalize(mean,std) # 用均值和标准差正规化一个tensor
加载数据集的代码:
import torch
import torchvision
import torchvision.transforms as transforms
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')
用 iter(dataloader)
生成迭代器,然后用 .next()
逐个取出数据。
# get some random training images
dataiter = iter(trainloader)
images, labels = dataiter.next()
# show images
imshow(torchvision.utils.make_grid(images))
上面代码中 make_grid
用于将多张图片拼成一张图片显示
#####################
torchvision.utils.make_grid(tensor, nrow=8, padding=2,
normalize=False, range=None, scale_each=False, pad_value=0)
# nrow: 每一行显示的图象数
# padding: 相邻图像间的填充像素数
# normalize: 归一化到[0,1]间
#####################
4.2.2 定义CNN和loss
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
layers...
def forward(self,x):
computations...
return x
net = Net()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
4.2.3 训练和测试
训练
for epoch in range(2):
running_loss = 0.
for i,data = enumerate(trainloader,0):
inputs,labels = data
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')
测试
with torch.no_grad():
for data in testloader:
retrive datas...
forward...
compute accuracy...
4.3 用GPU运算
跟第 1 部分中讲的相同,用 device
和 .to()
就可以,不过要注意数据加载时也要放在GPU中
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
net.to(device)
inputs, labels = inputs.to(device), labels.to(device)
5. 多块GPU并行计算
当你通过 .to(device)
指定使用 gpu 进行计算时,默认只使用一块 gpu,如果想要用多块 gpu 并行计算,非常简单,只需要一个语句(太残暴了!):
model = nn.DataParallel(model)
# model 是你定义的网络模型的实例
# 敲黑板,这短短的一行代码就是这一小节的核心!
当你使用上面一句代码之后,pytorch会自动将你一个batch的数据(基本平均地)分配给各个gpu。下面举个比较完整的例子,帮助加深记忆
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
class MyDataset(Dataset):
def __init__(self,params):
...
def __getitem__(self,index):
...
def __len__(self):
...
class MyModel(nn.Module):
def __init__(self,params):
super(Model, self).__init__()
layers...
def forward(self,input):
computations...
# 实例化模型
model = MyModel(some_params)
# 设置并行处理,核心!
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
if torch.cuda.device_count() > 1:
model = model.DataParallel(model)
model.to(device)
# 载入数据
my_loader = DataLoader(dataset=MyDataset(other_params),
batch_size=batch_size, shuffle=True)
# 运行模型
for data in my_loader:
input = data.to(device)
output = model(input)
print(output)