张量
张量如同数组和矩阵一样, 是一种特殊的数据结构。在PyTorch
中, 神经网络的输入、输出以及网络的参数等数据, 都是使用张量来进行描述。
张量的使用和Numpy
中的ndarrays
很类似, 区别在于张量可以在GPU
或其它专用硬件上运行, 这样可以得到更快的加速效果。如果你对ndarrays
很熟悉的话, 张量的使用对你来说就很容易了。
库的导入:
import torch
import numpy as np
张量初始化
张量有很多种不同的初始化方法, 先来看看四个简单的例子:
1. 直接生成张量
# 由原始数据直接生成张量, 张量类型由原始数据类型决定
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
2. 通过Numpy数组来生成张量
# 由已有的Numpy数组来生成张量(反过来也可以由张量来生成Numpy数组
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
3. 通过已有的张量来生成新的张量
# 新的张量将继承已有张量的数据属性(结构、类型), 也可以重新指定新的数据类型
x_ones = torch.ones_like(x_data) # 保留 x_data 的属性
print(f"Ones Tensor: \n {x_ones} \n")
x_rand = torch.rand_like(x_data, dtype=torch.float) # 重写 x_data 的数据类型:int -> float
print(f"Random Tensor: \n {x_rand} \n")
4. 通过指定数据维度来生成张量
# shape是元组类型, 用来描述张量的维数, 下面3个函数通过传入shape来指定生成张量的维数
shape = (2,3,)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)
print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")
张量的属性
从张量属性我们可以得到张量的维数、数据类型以及它们所存储的设备(CPU或GPU)。
来看一个简单的例子:
tensor = torch.rand(3,4)
print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")
张量运算
有超过100种张量相关的运算操作, 例如转置、索引、切片、数学运算、线性代数、随机采样等。更多的运算可以在这里查看
torch.autograd的简要介绍
torch.autograd
是 PyTorch 的自动差分引擎,可为神经网络训练提供支持。
背景
神经网络(NN)是在某些输入数据上执行的嵌套函数的集合。 这些函数由参数(由权重和偏差组成)定义,这些参数在 PyTorch 中存储在张量中。
训练 NN 分为两个步骤:
正向传播:在正向传播中,NN 对正确的输出进行最佳猜测。 它通过其每个函数运行输入数据以进行猜测。
反向传播:在反向传播中,NN 根据其猜测中的误差调整其参数。 它通过从输出向后遍历,收集有关函数参数(梯度)的误差导数并使用梯度下降来优化参数来实现。
在pytorch中的用法
看一个简单的具备了训练神经网络所需的一切的例子:
import torch
import torchvision
import numpy as np
# 从torchvision加载了经过预训练的 resnet18 模型
# 创建一个随机数据张量来表示具有 3 个通道的单个图像,高度&宽度为 64
# 其对应的label初始化为一些随机值
model = torchvision.models.resnet18(pretrained = True)
data = torch.rand(1,3,64,64)
labels = torch.rand(1,1000)
# 通过模型的每一层运行输入数据以进行预测
prediction = model(data) # forward pass
# 使用模型的预测和相应的标签来计算误差(loss) 下一步是通过网络反向传播此误差
# 当在误差张量上调用.backward()时开始反向传播
# 然后,Autograd 会为每个模型参数计算梯度并将其存储在参数的.grad属性中
loss = (prediction - labels).sum()
loss.backward() # backward pass
# 加载一个优化器,在本例中为 SGD,学习率为 0.01,动量为 0.9
# 在优化器中注册模型的所有参数
optim = torch.optim.SGD(model.parameters(), lr = 1e-2, momentum = 0.9)
# 调用.step()启动梯度下降, 优化器通过.grad中存储的梯度来调整每个参数
optim.step()
Autograd的微分
让我们来看看autograd
如何收集梯度。 我们用requires_grad=True
创建两个张量a
和b
。 这向autograd
发出信号,应跟踪对它们的所有操作:
a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)
我们从a
和b
创建另一个张量Q:
Q = 3*a**3 - b**2
同样,我们也可以将Q
聚合为一个标量,然后隐式地向后调用,例如Q.sum().backward()
使用Autograd的向量微积分
计算图
从概念上讲,Autograd 在由函数对象组成的有向无环图(DAG)中记录数据(张量)和所有已执行的操作(以及由此产生的新张量)。 在此 DAG 中,叶子是输入张量,根是输出张量。 通过从根到叶跟踪此图,可以使用链式规则自动计算梯度。
在正向传播中,Autograd 同时执行两项操作:
- 运行请求的操作以计算结果张量,并且
- 在 DAG 中维护操作的梯度函数。
当在 DAG 根目录上调用.backward()
时,反向传递开始。 autograd
然后:
- 从每个
.grad_fn
计算梯度, - 将它们累积在各自的张量的
.grad
属性中,然后 - 使用链式规则,一直传播到叶子张量。
下面是我们示例中 DAG 的直观表示。 在图中,箭头指向前进的方向。 节点代表正向传播中每个操作的反向函数。 蓝色的叶节点代表我们的叶张量a
和b
。
DAG 在 PyTorch 中是动态的。要注意的重要一点是,图是从头开始重新创建的; 在每个.backward()
调用之后,Autograd 开始填充新图。 这正是允许您在模型中使用控制流语句的原因。 您可以根据需要在每次迭代中更改形状,大小和操作。
从DAG中排除
torch.autograd
跟踪所有将其requires_grad
标志设置为True
的张量的操作。 对于不需要梯度的张量,将此属性设置为False
会将其从梯度计算 DAG 中排除。
即使只有一个输入张量具有requires_grad=True
,操作的输出张量也将需要梯度。
在 NN 中,不计算梯度的参数通常称为冻结参数。 如果事先知道您不需要这些参数的梯度,则“冻结”模型的一部分很有用(通过减少自动梯度计算,这会带来一些性能优势)。
从 DAG 中排除很重要的另一个常见用例是调整预训练网络。
在微调中,我们冻结了大部分模型,通常仅修改分类器层以对新标签进行预测。 让我们来看一个小例子来说明这一点。 和以前一样,我们加载一个预训练的 resnet18 模型,并冻结所有参数。
import torch
import torchvision
import numpy as np
from torch import nn, optim
model = torchvision.models.resnet18(pretrained = True)
# Freeze all the parameters in the network
for param in model.parameters():
param.requires_grad = False
# 假设我们要在具有 10 个标签的新数据集中微调模型
# 在 resnet 中,分类器是最后一个线性层model.fc
# 我们可以简单地将其替换为充当我们的分类器的新线性层(默认情况下未冻结)
model.fc = nn.Linear(512,10)
# 现在,除了model.fc的参数外,模型中的所有参数都将冻结
# 计算梯度的唯一参数是model.fc的权重和偏差
# Optimize only the classifier
optimizer = optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9)
请注意,尽管我们在优化器中注册了所有参数,但唯一可计算梯度的参数(因此会在梯度下降中进行更新)是分类器的权重和偏差。
神经网络
可以使用torch.nn
包构建神经网络。
现在您已经了解了autograd
,nn
依赖于autograd
来定义模型并对其进行微分。 nn.Module
包含层,以及返回output
的方法forward(input)
。
例如,查看以下对数字图像进行分类的网络:
卷积网
这是一个简单的前馈网络。 它获取输入,将其一层又一层地馈入,然后最终给出输出。
神经网络的典型训练过程如下:
- 定义具有一些可学习参数(或权重)的神经网络
- 遍历输入数据集
- 通过网络处理输入
- 计算损失(输出正确的距离有多远)
- 将梯度传播回网络参数
- 通常使用简单的更新规则来更新网络的权重:
weight = weight - learning_rate * gradient
定义网络的例子
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, 5 x 5 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) # 5 * 5 from image dimension
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 specify with a single number
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = torch.flatten(x, 1) # flatten all dimensions except the batch dimension
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
net = Net()
print(net)
只需要定义forward
函数,就可以使用autograd
自动定义backward
函数(计算梯度)。 可以在forward
函数中使用任何张量操作。
模型的可学习参数由net.parameters()
返回
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight
尝试一个32x32
随机输入。 注意:该网络的预期输入大小(LeNet)为32x32
。 要在 MNIST 数据集上使用此网络,请将图像从数据集中调整为32x32
。
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
使用随机梯度将所有参数和反向传播的梯度缓冲区归零:
net.zero_grad()
out.backward(torch.randn(1, 10))
注意
torch.nn
仅支持小批量。 整个torch.nn
包仅支持作为微型样本而不是单个样本的输入。
例如,nn.Conv2d
将采用nSamples x nChannels x Height x Width
的 4D 张量。
如果您只有一个样本,只需使用input.unsqueeze(0)
添加一个假批量尺寸。
在继续之前,让我们回顾一下到目前为止所看到的所有类:
torch.Tensor
-一个多维数组,支持诸如backward()
的自动微分操作。 同样,保持相对于张量的梯度。nn.Module
-神经网络模块。 封装参数的便捷方法,并带有将其移动到 GPU,导出,加载等的帮助器。nn.Parameter
-一种张量,即将其分配为Module
的属性时,自动注册为参数。autograd.Function
-实现自动微分操作的正向和反向定义。 每个Tensor
操作都会创建至少一个Function
节点,该节点连接到创建Tensor
的函数,并且编码其历史记录。
损失函数
损失函数采用一对(输出,目标)输入,并计算一个值,该值估计输出与目标之间的距离。
nn
包下有几种不同的损失函数。 一个简单的损失是:nn.MSELoss
,它计算输入和目标之间的均方误差。
例如:
output = net(input)
target = torch.randn(10) # a dummy target, for example
target = target.view(1, -1) # make it the same shape as output
criterion = nn.MSELoss()
loss = criterion(output, target)
print(loss)
执行结果:
tensor(1.1649, grad_fn=<MseLossBackward0>)
现在,如果使用.grad_fn
属性向后跟随loss
,您将看到一个计算图,如下所示:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
因此,当我们调用loss.backward()
时,整个图将被微分。 损失,并且图中具有requires_grad=True
的所有张量将随梯度累积其.grad
张量。
为了说明,让我们向后走几步:
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
执行结果:
<MseLossBackward0 object at 0x7f71283dd048>
<AddmmBackward0 object at 0x7f71283dd7f0>
<AccumulateGrad object at 0x7f71283dd7f0>
反向传播
要反向传播误差,我们要做的只是对loss.backward()
。 不过,您需要清除现有的梯度,否则梯度将累积到现有的梯度中。
现在,我们将其称为loss.backward()
,然后看一下向后前后conv1
的偏差梯度。
net.zero_grad() # zeroes the gradient buffers of all parameters
print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)
loss.backward()
print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)
执行结果:
conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([ 0.0188, 0.0172, -0.0044, -0.0141, -0.0058, -0.0013])
更新权重
实践中使用的最简单的更新规则是随机梯度下降(SGD):
weight = weight - learning_rate * gradient
使用简单的 Python 代码实现此目标:
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)
但是,在使用神经网络时,您希望使用各种不同的更新规则,例如 SGD,Nesterov-SGD,Adam,RMSProp 等。为实现此目的,我们构建了一个小包装: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
训练分类器
通常,当您必须处理图像,文本,音频或视频数据时,可以使用将数据加载到 NumPy 数组中的标准 Python 包。 然后,您可以将该数组转换为torch.*Tensor
。
- 对于图像,Pillow,OpenCV 等包很有用
- 对于音频,请使用 SciPy 和 librosa 等包
- 对于文本,基于 Python 或 Cython 的原始加载,或者 NLTK 和 SpaCy 很有用
专门针对视觉,Pytorch 创建了一个名为torchvision
的包,其中包含用于常见数据集(例如 Imagenet,CIFAR10,MNIST 等)的数据加载器,以及用于图像(即torchvision.datasets
和torch.utils.data.DataLoader
)的数据转换器。这提供了极大的便利,并且避免了编写样板代码。
使用 CIFAR10 数据集。 它具有以下类别:“飞机”,“汽车”,“鸟”,“猫”,“鹿”,“狗”,“青蛙”,“马”,“船”,“卡车”。 CIFAR-10 中的图像尺寸为3x32x32
,即尺寸为32x32
像素的 3 通道彩色图像。
训练图像分类器
按顺序执行以下步骤:
- 使用
torchvision
加载并标准化 CIFAR10 训练和测试数据集 - 定义卷积神经网络
- 定义损失函数
- 根据训练数据训练网络
- 在测试数据上测试网络
加载并标准化CIFAR10
import torch
import torchvision
import torchvision.transforms as transforms
# TorchVision 数据集的输出是[0, 1]范围的PILImage图像
# 我们将它们转换为归一化范围[-1, 1]的张量
# If running on Windows and you get a BrokenPipeError, try setting
# the num_worker of torch.utils.data.DataLoader() to 0.
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')
执行结果:
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./data/cifar-10-python.tar.gz
Extracting ./data/cifar-10-python.tar.gz to ./data
Files already downloaded and verified
展示一些训练图像:
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)))
执行结果:
dog truck frog horse
定义卷积神经网络
之前从“神经网络”部分复制神经网络,然后对其进行修改以获取 3 通道图像(而不是定义的 1 通道图像)。
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()
定义损失函数和优化器
使用分类交叉熵损失和带有动量的 SGD
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
训练网络
这是事情开始变得有趣的时候。 我们只需要遍历数据迭代器,然后将输入馈送到网络并进行优化即可。
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; data is a list of [inputs, labels]
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, 2000] loss: 2.196
[1, 4000] loss: 1.849
[1, 6000] loss: 1.671
[1, 8000] loss: 1.589
[1, 10000] loss: 1.547
[1, 12000] loss: 1.462
[2, 2000] loss: 1.382
[2, 4000] loss: 1.389
[2, 6000] loss: 1.369
[2, 8000] loss: 1.332
[2, 10000] loss: 1.304
[2, 12000] loss: 1.288
Finished Training
保存训练过的模型:
PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)
根据测试数据测试网络
我们已经在训练数据集中对网络进行了 2 次训练。 但是我们需要检查网络是否学到了什么。
我们将通过预测神经网络输出的类别标签并根据实际情况进行检查来进行检查。 如果预测正确,则将样本添加到正确预测列表中。
好的,第一步。 让我们显示测试集中的图像以使其熟悉:
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)))
GroundTruth: cat ship ship plane
outputs = net(images)
# 输出是 10 类的能量。 一个类别的能量越高,网络就认为该图像属于特定类别
# 因此,让我们获取最高能量的指数:
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]]
for j in range(4)))
# 网络在整个数据集上的表现
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))
执行结果
Accuracy of the network on the 10000 test images: 53 %
# 看看所有分类的准确率
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]))
执行结果
Accuracy of plane : 50 %
Accuracy of car : 62 %
Accuracy of bird : 51 %
Accuracy of cat : 32 %
Accuracy of deer : 31 %
Accuracy of dog : 35 %
Accuracy of frog : 77 %
Accuracy of horse : 70 %
Accuracy of ship : 71 %
Accuracy of truck : 52 %
如何在GPU上进行训练
就像将张量转移到 GPU 上一样,您也将神经网络转移到 GPU 上。
如果可以使用 CUDA,首先将我们的设备定义为第一个可见的 cuda 设备:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# Assuming that we are on a CUDA machine, this should print a CUDA device:
print(device)
出:
cuda:0
本节的其余部分假定device
是 CUDA 设备。
然后,这些方法将递归遍历所有模块,并将其参数和缓冲区转换为 CUDA 张量:
net.to(device)
请记住,您还必须将每一步的输入和目标也发送到 GPU:
inputs, labels = data[0].to(device), data[1].to(device)