本文是基于《Pytorch深度学习实战》一书第七章的内容所整理的学习笔记
相关代码的解释以及对应的拓展。
本文使用的代码均基于jupyter
%matplotlib inline
from matplotlib import pyplot as plt
import numpy as np
import torch
torch.set_printoptions(edgeitems=2, linewidth=75)
torch.manual_seed(123)
from torchvision import datasets
data_path = 'data/p1ch7/'
cifar10 = datasets.CIFAR10(data_path, train=True, download=True) # 实例画一个数据集用于训练数据,如果数据集不存在,则TorchVision将下载该数据集
cifar10_val = datasets.CIFAR10(data_path, train=False, download=True) # 使用train=False,获取一个数据集用于验证数据,并在需要时再次下载该数据集
class_names = ['airplane','automobile','bird','cat','deer',
'dog','frog','horse','ship','truck']
fig = plt.figure(figsize=(8,3))
num_classes = 10
for i in range(num_classes):
ax = fig.add_subplot(2, 5, 1 + i, xticks=[], yticks=[])
ax.set_title(class_names[i])
img = next(img for img, label in cifar10 if label == i)
plt.imshow(img)
plt.show()
数据集子模块为我们提供了对最流行的计算机视觉数据集的预存储访问,在每种情况下,数据集都作为torch.utils.data.Dataset的子类返回。
type(cifar10).__mro__
class X(object):pass
class Y(object):pass
class A(X, Y):pass
class B(Y):pass
class C(A, B):pass
C.__mro__w
表示数据集的抽象类,它不一定持有数据,但是它提供了对七进行统一访问的函数__len__()和__getitem__(),且子类必须继承上述两个函数
len(cifar10)
img, label = cifar10[99]
img, label, class_names[label]
且这个对象是RGB PIL(Python Imaging Library)图像的一个实例,可以被打印出来
plt.imshow(img)
plt.show()
torchvision.transforms
这个模块定义了一组可组合的、类似函数的对象,它可以作为参数传递到TorchVision模块的数据集
from torchvision import transforms
dir(transforms)
一旦ToTensor被实例化,就可以向调用函数一样调用它,以PIL图像作为参数,返回一个张量作为输出
from torchvision import transforms
to_tensor = transforms.ToTensor()
img_t = to_tensor(img)
img_t.shape
我们也可以将transform直接作为参数传递给dataset.CIFAR10
tensor_cifar10 = datasets.CIFAR10(data_path, train=True, download=False, transform=transforms.ToTensor())
与之前相比,访问数据集的元素将返回一个张量,而不是PIL图像
img_t, _ = tensor_cifar10[99]
type(img_t), img_t.shape, img_t.dtype
img_t.min(), img_t.max()
打印此时的图片结果
plt.imshow(img_t.permute(1, 2, 0)) # 将轴的顺序由CxHxW改为HxWxC以匹配matplotlib
plt.show()
变换非常方便,因为我们可以使用 transforms.Compose()将它们连接起来,以实现一系列transform操作,然后在数据加载器中直接透明地进行数据归一化和数据增强操作。
对每个通道进行归一化使其具有相同的分布,可以保证在相同的学习率下,通过梯度下降实现通道信息的混合和更新。
为了使每个通道的均值为0、标准差为 1,我们可以应用以下转换来计算数据集中每个通道的平均值和标淮差:v_n[c]=(v[c]-mean[c]) / stdev[c]。这正是transforms.Normalize0/所做的。
我们将数据集返回的素有张量沿着一个额外的纬度进行堆叠
imgs = torch.stack([img_t for img_t, _ in tensor_cifar10], dim=3)
imgs.shape
# 假设是时间步T1的输出
T1 = torch.tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 假设是时间步T2的输出
T2 = torch.tensor([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])
print(torch.stack((T1,T2),dim=0).shape)
print(torch.stack((T1,T2),dim=1).shape)
print(torch.stack((T1,T2),dim=1))
print(torch.stack((T1,T2),dim=2).shape)
计算每个通道的平均值
imgs.view(3, -1) # view(3, -1)保留了3个通道,并将剩余的所有纬度合并为一个纬度,从而计算出适当的尺寸大小。这里我们的3x32x32的图像被转换成一个3x1024的向量,然后对每个通道的1024个元素取平均值
imgs.view(3, -1).mean(dim=1) # 计算均值
imgs.view(3, -1).std(dim=1) # 计算标准差
transforms.Normalize((0.4915, 0.4823, 0.4468), (0.2470, 0.2435, 0.2616)) # 初始化Normalize变换
使用Compose连接多个变换
transformed_cifar10 = datasets.CIFAR10(
data_path, train=True, download=False,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4915, 0.4823, 0.4468),
(0.2470, 0.2435, 0.2616))
]))
transformed_cifar10_val = datasets.CIFAR10(
data_path, train=False, download=False,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4915, 0.4823, 0.4468),
(0.2470, 0.2435, 0.2616))
]))
注意,此时,从数据集绘制的图像不能为我们提供实际图像的真实表示
img_t, _ = transformed_cifar10[99]
plt.imshow(img_t.permute(1, 2, 0))
plt.show()
%matplotlib inline
from matplotlib import pyplot as plt
import numpy as np
import torch
torch.set_printoptions(edgeitems=2)
torch.manual_seed(123)
class_names = ['airplane','automobile','bird','cat','deer',
'dog','frog','horse','ship','truck']
from torchvision import datasets, transforms
data_path = 'data/p1ch7/'
cifar10 = datasets.CIFAR10(
data_path, train=True, download=False,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4915, 0.4823, 0.4468),
(0.2470, 0.2435, 0.2616))
]))
cifar10_val = datasets.CIFAR10(
data_path, train=False, download=False,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4915, 0.4823, 0.4468),
(0.2470, 0.2435, 0.2616))
]))
从CIFAR10中创建一个只包含鸟和飞机的数据集子集
label_map = {0: 0, 2: 1}
class_names = ['airplane', 'bird']
cifar2 = [(img, label_map[label])
for img, label in cifar10
if label in [0, 2]]
cifar2_val = [(img, label_map[label])
for img, label in cifar10_val
if label in [0, 2]]
import torch.nn as nn
n_out = 2
model = nn.Sequential(
nn.Linear(
3072, # 输入特征
512, # 隐藏层的大小
),
nn.Tanh(),
nn.Linear(
512, # 隐藏层的大小
n_out, # 输出类
)
)
我们需要认识到输出是分类的:它要么是一只鸟,要么是一架飞机。当我们必须表示一个分类变量时,我们应该用该变量的独热编码表示,如对于飞机使用[1,0],对于鸟使用[0,1],顺序任意。
理想情况下,网络将为飞机输出 torch.tensor([1.0,0.0]),为鸟输出; torch.tensor([0.0,1.0])。实际上,由于我们的分类器并不是很完美的,我们可以期望网络输出介于二者之间的结果。关键的实现是我们可以将输出解释为概率:第1项是“飞机”的概率,第2项是“鸟”的概率。
一些额外的约束。
Softmax,它获取一个值向量并生成另一个相同纬度的向量,其中的值满足我们刚刚列出的表示概率的约束条件。
def softmax(x):
return torch.exp(x) / torch.exp(x).sum()
x = torch.tensor([1.0, 2.0, 3.0])
softmax(x), softmax(x).sum()
Softmax是一个单调函数,因为输入中的较小值对应输出中的较小值。但是,它并不是比率不变的,因为值之间的比率没有被保留。
softmax = nn.Softmax(dim=1)
x = torch.tensor([[1.0, 2.0, 3.0],
[1.0, 2.0, 3.0]])
softmax(x)
在搭建的神经网络模型的末尾添加一个nn.Softmax()
model = nn.Sequential(
nn.Linear(3072, 512),
nn.Tanh(),
nn.Linear(512, 2),
nn.Softmax(dim=1))
img, _ = cifar2[0]
img_batch = img.view(-1).unsqueeze(0)
out = model(img_batch)
out
训练之后,通过输出概率的argmax来获得作为索引的标签,即获得最大概率的索引
_, index = torch.max(out, dim=1)
index
我们希望惩罚错误分类,所以我们需要最大化的是与正确的类out[class_index]相关的概率。其中out是softmax的输出,class_index
是一个向量。
与正确类相关的概率,被称为我们的模型给定参数的似然,即我们想要一个损失函数,当概率很低的时候,损失非常高——低到其他选择都有比它更高的概率。相反,当概率高于其他选择时,损失应该很低,而且我们并不是真的专注于将概率提高到1。
负对数似然(NLL),表达式为 NLL =-sum(log(out_i[c_i])),其中sum()用于对N个样本求和,而c_i是样本i的目标类别。
计算分类损失的步骤:
修改模型为:
model = nn.Sequential(
nn.Linear(3072, 512),
nn.Tanh(),
nn.Linear(512, 2),
nn.LogSoftmax(dim=1))
实例化NLL损失
loss = nn.NLLLoss()
img, label = cifar2[0]
out = model(img.view(-1).unsqueeze(0))
loss(out, torch.tensor([label]))
import torch
import torch.nn as nn
import torch.optim as optim
model = nn.Sequential(
nn.Linear(3072, 512),
nn.Tanh(),
nn.Linear(512, 2),
nn.LogSoftmax(dim=1))
learning_rate = 1e-2
optimizer = optim.SGD(model.parameters(), lr=learning_rate)
loss_fn = nn.NLLLoss()
n_epochs = 100
for epoch in range(n_epochs):
for img, label in cifar2:
out = model(img.view(-1).unsqueeze(0))
loss = loss_fn(out, torch.tensor([label]))
optimizer.zero_grad()
loss.backward()
optimizer.step()
print("Epoch: %d, Loss: %f" % (epoch, float(loss)))
通过在每个迭代周期上变换样本并一次估计一个或几个样本的梯度(提高稳定性),我们在梯度下降中有效地引入了随机性。
在小批量上估计的梯度是在整个数据集上估计的梯度的较差近似值,有助于收敛并防止优化在过程中陷入局部极小。
通常,小批量是一个固定的大小,需要我们在训练之前设置,就像学习率一样。这些被称为超参数,以区别于模型的参数。
有助于打乱数据和组织数据
数据加载器的工作是从数据集中采样小批量,这是我们能够灵活地选择不同的采样策略。一种非常常见的策略是在每个迭代周期洗牌后进行均匀采样。
DataLoader()构造函数至少接收一个数据集对象作为输入,以及batch_size和一个shuffle布尔值,该布尔值指示数据是否需要在每个迭代周期开始时被重新打乱:
import torch
import torch.nn as nn
import torch.optim as optim
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=True)
model = nn.Sequential(
nn.Linear(3072, 128),
nn.Tanh(),
nn.Linear(128, 2),
nn.LogSoftmax(dim=1))
learning_rate = 1e-2
optimizer = optim.SGD(model.parameters(), lr=learning_rate)
loss_fn = nn.NLLLoss()
n_epochs = 100
for epoch in range(n_epochs):
for imgs, labels in train_loader:
outputs = model(imgs.view(imgs.shape[0], -1))
loss = loss_fn(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print("Epoch: %d, Loss: %f" % (epoch, float(loss)))
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,shuffle=False)
correct = 0
total = 0
with torch.no_grad():
for imgs, labels in train_loader:
outputs = model(imgs.view(imgs.shape[0], -1))
_, predicted = torch.max(outputs, dim=1)
total += labels.shape[0]
correct += int((predicted == labels).sum())
print("Accuracy: %f" % (correct / total))
val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size=64,shuffle=False)
correct = 0
total = 0
with torch.no_grad():
for imgs, labels in val_loader:
outputs = model(imgs.view(imgs.shape[0], -1))
_, predicted = torch.max(outputs, dim=1)
total += labels.shape[0]
correct += int((predicted == labels).sum())
print("Accuracy: %f" % (correct / total))
更新网络结构,以获取更高的性能
使用交叉熵损失函数替换均方差损失函数
model = nn.Sequential(
nn.Linear(3072, 1024),
nn.Tanh(),
nn.Linear(1024, 512),
nn.Tanh(),
nn.Linear(512, 128),
nn.Tanh(),
nn.Linear(128, 2))
loss_fn = nn.CrossEntropyLoss()
import torch
import torch.nn as nn
import torch.optim as optim
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
shuffle=True)
model = nn.Sequential(
nn.Linear(3072, 1024),
nn.Tanh(),
nn.Linear(1024, 512),
nn.Tanh(),
nn.Linear(512, 128),
nn.Tanh(),
nn.Linear(128, 2))
learning_rate = 1e-2
optimizer = optim.SGD(model.parameters(), lr=learning_rate)
loss_fn = nn.CrossEntropyLoss()
n_epochs = 100
for epoch in range(n_epochs):
for imgs, labels in train_loader:
outputs = model(imgs.view(imgs.shape[0], -1))
loss = loss_fn(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print("Epoch: %d, Loss: %f" % (epoch, float(loss)))
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
shuffle=False)
correct = 0
total = 0
with torch.no_grad():
for imgs, labels in train_loader:
outputs = model(imgs.view(imgs.shape[0], -1))
_, predicted = torch.max(outputs, dim=1)
total += labels.shape[0]
correct += int((predicted == labels).sum())
print("Accuracy: %f" % (correct / total))
val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size=64,
shuffle=False)
correct = 0
total = 0
with torch.no_grad():
for imgs, labels in val_loader:
outputs = model(imgs.view(imgs.shape[0], -1))
_, predicted = torch.max(outputs, dim=1)
total += labels.shape[0]
correct += int((predicted == labels).sum())
print("Accuracy: %f" % (correct / total))
观察模型中可训练的参数的数量
sum([p.numel() for p in model.parameters()])
sum([p.numel() for p in model.parameters() if p.requires_grad == True])
linear = nn.Linear(3072, 1024)
linear.weight.shape, linear.bias.shape
本文主要讲解了: