上图为CPU,是任何计算机的核心。
当两个计算任务一起执行时,执行总时间小于他们分开执行的总和。但是同时使用CPU和GPU的计算,经常需要在内存和显存之间复制数据,造成数据的通信,但是可以看到执行计算和通信的总时间小于两者分别执行的耗时之和。
一台机器可以安装多个GPU(1-16),在训练和预测的时候,将一个小批量计算切分到多个GPU上来达到加速目的,常用的切分方案有:
1.数据并行:将小批量分成n块,每个GPU拿到完整参数计算每一块数据的梯度,通常性能更好
假设一台机器上有K个GPU,给定需要的训练的模型,每块GPU及其相应的显存将分别独立维护一份完整的模型参数。在模型训练的任意一次迭代中,给定一个随机小批量,将该批量中样本划分成K份并分给每块显卡的显存一份。每块GPU将根据相应显存所分到的小批量子集和所维护的模型参数分别计算模型参数的本地梯度,把K块显卡的显存上的本地梯度相加,便得到当前的小批量随机梯度,之后,每块GPU都使用这个小批量随机梯度分别更新相应显存所维护的那一分完整的模型参数。
# 多GPU训练
%matplotlib inline
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
#使用LeNet作为基础网络
# 初始化模型参数
scale = 0.01
W1 = torch.randn(size=(20, 1, 3, 3)) * scale
b1 = torch.zeros(20)
W2 = torch.randn(size=(50, 20, 5, 5)) * scale
b2 = torch.zeros(50)
W3 = torch.randn(size=(800, 128)) * scale
b3 = torch.zeros(128)
W4 = torch.randn(size=(128, 10)) * scale
b4 = torch.zeros(10)
params = [W1, b1, W2, b2, W3, b3, W4, b4]
# 定义模型
def lenet(X, params):
h1_conv = F.conv2d(input=X, weight=params[0], bias=params[1])
h1_activation = F.relu(h1_conv)
h1 = F.avg_pool2d(input=h1_activation, kernel_size=(2, 2), stride=(2, 2))
h2_conv = F.conv2d(input=h1, weight=params[2], bias=params[3])
h2_activation = F.relu(h2_conv)
h2 = F.avg_pool2d(input=h2_activation, kernel_size=(2, 2), stride=(2, 2))
h2 = h2.reshape(h2.shape[0], -1)
h3_linear = torch.mm(h2, params[4]) + params[5]
h3 = F.relu(h3_linear)
y_hat = torch.mm(h3, params[6]) + params[7]
return y_hat
# 交叉熵损失函数
loss = nn.CrossEntropyLoss(reduction='none')
# 向多个设备分发参数并附加梯度 没有参数不能再GP上评估网络
def get_params(params,device):
new_params = [p.clone().to(device) for p in params]
for p in new_params:
# 对每个参数计算梯度
p.requires_grad_()
return new_params
new_params = get_params(params,d2l.try_gpu(0))
print('b1 weight:',new_params[1])
print('b1 grad:',new_params[1].grad)
运行结果:
b1 weight: tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
device='cuda:0', requires_grad=True)
b1 grad: None
#allreduce 函数将所有向量相加,并将结果广播给所有GPU 将数据复制到累计结果的设备
def allreduce(data):
# data是一个list
for i in range(1,len(data)):
# 先将数据转换到gpu0上,全部相加到data[0]
data[0][:] += data[i].to(data[0].device)
for i in range(1,len(data)):
# 将相加之后的数据在重新分配到其他的gpu上
data[i] = data[0].to(data[i].device)
# 在不同的设备上创建具有不同值的向量并聚合他们
data =[torch.ones((1,2),device=d2l.try_gpu(i))*(i+1) for i in range(2)]
print("allreduce之前\n",data[0],'\n',data[1])
allreduce(data)
print("allreduce之后\n",data[0],'\n',data[1])
运行结果:
allreduce之前
tensor([[1., 1.]], device='cuda:0')
tensor([[2., 2.]], device='cuda:1')
allreduce之后
tensor([[3., 3.]], device='cuda:0')
tensor([[3., 3.]], device='cuda:1')
# 简单的工具函数 将一个小批量数据均匀的分布在多个CPU上
data = torch.arange(20).reshape(4,5)
devices =[torch.device('cuda:0'),torch.device('cuda:1')]
# 使用内置函数scatter
split = nn.parallel.scatter(data,devices)
print('input:',data)
print('load into:',devices)
print('output:',split)
运行结果:
input: tensor([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]])
load into: [device(type='cuda', index=0), device(type='cuda', index=1)]
output: (tensor([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]], device='cuda:0'), tensor([[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]], device='cuda:1'))
# 为方便复用,我们定义了可以同时拆分数据和标签的split_batch函数
def split_batch(X,y,devices):
"""将X,y拆分到多个设备上"""
assert X.shape[0] == y.shape[0]
return (nn.parallel.scatter(X,devices),
nn.parallel.scatter(y,devices))
# 在一个小批量上实现多GPU训练
# 不需要编写任何特定的代码来实现并行性,小批量内的设备之间没有任何依赖关系,是自动的并行执行
def train_batch(X,y,device_params,devices,lr):
X_shards,y_shards = split_batch(X,y,devices)
# 在每个GPU上分别计算损失
ls = [loss(lenet(X_shard,device_w),y_shard).sum()
for X_shard,y_shard,device_w in zip(
X_shards,y_shards,device_params)]
for l in ls:# 反向传播在每个GPU上分别执行
l.backward()
# 将每个GPU的所有梯度相加,并将其广播到所有GPU
with torch.no_grad():
# i代表的是层
for i in range(len(device_params[0])):
allreduce(
[device_params[c][i].grad for c in range(len(devices))])
# 在每个GPU上分别更新模型参数
for param in device_params:
d2l.sgd(param,lr,X.shape[0]) #在这里 使用全尺寸的小批量
#定义训练函数:需要分配GPU并将所有的模型参数复制到所有设备上
def train(num_gpus,batch_size,lr):
train_iter,test_iter = d2l.load_data_fashion_mnist(batch_size)
devices =[d2l.try_gpu(i) for i in range(num_gpus)]
# 将模型参数复制到num_gpus个GPU
device_params =[get_params(params,d) for d in devices]
num_epochs =10
animator = d2l.Animator('epoch','test acc',xlim=[1,num_epochs])
timer =d2l.Timer()
for epoch in range(num_epochs):
timer.start()
for X,y in train_iter:
# 为单个小批量执行多GPU训练
train_batch(X,y,device_params,devices,lr)
# 同步
torch.cuda.synchronize()
timer.stop()
# 在GPU上评估模型
animator.add(epoch+1,(d2l.evaluate_accuracy_gpu(
lambda x:lenet(x,device_params[0]),test_iter,devices[0]),))
print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,'
f'在{str(devices)}')
# 在单个GPU上运行的效果 批量大小256 学习率0.2
train(num_gpus=1,batch_size=256,lr=0.2)
# 增加为两个GPU
train(num_gpus=2,batch_size=256,lr=0.2)
保持批量大小和学习率没变,GPU的数量增加,测试精度的提升的结果并不明显,因为有额外的通信开销,所以没有看到训练时间明显的降低。
# 简洁版
import torch
from torch import nn
from d2l import torch as d2l
# 选择resnet18 使用更小的卷积核、步长和填充 删除最大汇聚层
def resnet18(num_classes,in_channels=1):
"""稍加修改的ResNet-18模型"""
def resnet_block(in_channels, out_channels, num_residuals,
first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(d2l.Residual(in_channels, out_channels,
use_1x1conv=True, strides=2))
else:
blk.append(d2l.Residual(out_channels, out_channels))
return nn.Sequential(*blk)
# 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层
net = nn.Sequential(
nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU())
net.add_module("resnet_block1", resnet_block(
64, 64, 2, first_block=True))
net.add_module("resnet_block2", resnet_block(64, 128, 2))
net.add_module("resnet_block3", resnet_block(128, 256, 2))
net.add_module("resnet_block4", resnet_block(256, 512, 2))
net.add_module("global_avg_pool", nn.AdaptiveAvgPool2d((1,1)))
net.add_module("fc", nn.Sequential(nn.Flatten(),
nn.Linear(512, num_classes)))
return net
# 训练回路中初始化网络
net = resnet18(10)
# 获取GPU列表
devices = d2l.try_all_gpus()
# 我们将在训练代码实现中初始化网络
#在所有设备上初始化网络参数 将小批量数据分配到所有设备上
#跨设备并行计算损失及梯度 聚合梯度并相应的更新参数
def train(net, num_gpus, batch_size, lr):
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
devices = [d2l.try_gpu(i) for i in range(num_gpus)]
def init_weights(m):
if type(m) in [nn.Linear, nn.Conv2d]:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights)
# 在多个GPU上设置模型
net = nn.DataParallel(net, device_ids=devices)
trainer = torch.optim.SGD(net.parameters(), lr)
loss = nn.CrossEntropyLoss()
timer, num_epochs = d2l.Timer(), 10
animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
for epoch in range(num_epochs):
net.train()
timer.start()
for X, y in train_iter:
trainer.zero_grad()
X, y = X.to(devices[0]), y.to(devices[0])
l = loss(net(X), y)
l.backward()
trainer.step()
timer.stop()
animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(net, test_iter),))
print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,'
f'在{str(devices)}')
2.模型并行:将模型分成n块,每个GPU拿到一块模型计算他们的前向和方向结果,通常用于模型大到单GPU放不下
3.通道并行(数据+模型并行)
分布式计算和之前的单机多卡没有本质区别
上图中的数据已经很老了,但是表达的观点是一致的,尽量的本地多通讯,减少在机器之间的做通讯。
在做分布式的时候,减少跨机器的样例:
1.每个计算服务器读取小批量中的一小块
2.进一步将数据切分到每个GPU上
3.每个worker从参数服务器那里获取模型参数
4.复制参数到每个GPU上
5.每个GPU计算梯度,将所有GPU上的梯度求和
6.梯度传回服务器,每个服务器对梯度求和,并更新参数
每个工作器都是同步计算一个批量,称为同步的SGD.假设有n个GPU,每个GPU每次处理b个样本,那么同步SGD等价于在单个GPU上运行批量大小为nb的SGD.在理想情况下,n个GPU可以得到行对个单GPU的n倍加速。
t1=在单个GPU上计算b个样本梯度时间,假设有m个参数,一个工作器每次发送和接受m个参数、梯度。t2=发送和接受所用的时间,每个批量的计算时间为max(t1,t2),选取足够大的b,使得t1>t2,增加b或n导致更大的批量大小,导致需要更多计算来得到给定的模型精度。
一些课堂笔记:
1.使用一个大数据集,需要好的GPU-GPU和机器-机器带宽,高效的数据读取和预处理
模型需要好的计算通讯比,使用足够大的批量大小来得到好的系统性能,使用高效的优化算法对应大批量大小。
2.分布式同步数据并行是多GPU数据并行在多机器上的拓展,网络通讯通常是瓶颈,需要注意使用特别大的批量大小时的收敛效率
3.更复杂的分布式有异步、模型并行
4.dataparallel是一种模式,在数据并行中每个GPU都会得到模型的所有参数,batchsize通常不超过10*n(数据集中有n个类别)
在语言里面加入各种不同的背景噪音,改变图片的颜色和形状,更像是一个正则项,我们只会在训练的时候进行增强,测试的时候不会。通过变形数据来获取多样性从而使得模型泛化性能更好。常见的图片增光包括反转、切割、变色。训练数据的分布没有改变只是多样性增加了。
%matplotlib inline
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
# 400*500的图像作为例子
d2l.set_figsize()
img = d2l.Image.open('../img/cat1.jpg')
d2l.plt.imshow(img)
# apply函数 多次运行图像增广方法并显示所有结果
def apply(img,aug,num_rows=2,num_cols=4,scale=1.5):
Y = [aug(img) for _ in range(num_rows*num_cols)]
d2l.show_images(Y,num_rows,num_cols,scale=scale)
# 使用transform模块来创建RandomFilpLeftRight实例 50%的几率使图像向左或向右反转
apply(img,torchvision.transforms.RandomHorizontalFlip())
# 使用transform模块来创建RandomFilpTopBottom实例 50%的几率使图像上下反转
apply(img,torchvision.transforms.RandomVerticalFlip())
# 随机剪裁10%到100%的区域,宽高比从0.5-2随机取值,宽度和高度像素都缩放到200
shape_aug = torchvision.transforms.RandomResizedCrop(
(200,200),scale=(0.1,1),ratio=(0.5,2))
apply(img,shape_aug)
# 改变图像颜色:亮度、对比度、饱和度和色调
apply(img,torchvision.transforms.ColorJitter(
brightness=0.5,contrast=0,saturation=0,hue=0))
apply(img, torchvision.transforms.ColorJitter(
brightness=0, contrast=0, saturation=0, hue=0.5))
# 同时修改亮度,对比度,饱和度和色调
color_aug = torchvision.transforms.ColorJitter(
brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
apply(img, color_aug)
# 结合多种图像增广方法 compose来综合不同的图像增广方法
augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(),color_aug,shape_aug
])
apply(img,augs)
# 使用图像增广来训练模型
all_images = torchvision.datasets.CIFAR10(train=True,root="../data",download=True)
d2l.show_images([all_images[i][0] for i in range(32)],4,8,scale=0.8)
# 使用最简单的随机反转,将图像转换成tensor格式:批量大小,通道数,高度,宽度的32位浮点数
train_augs=torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor()
])
test_augs = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()
])
# 读取图像和应用图像增广
# PyTorch数据集提供的transform参数应用图像增广来转化图像
def load_cifar10(is_train,augs,batch_size):
dataset = torchvision.datasets.CIFAR10(
root="../data",train=is_train,transform=augs,download=True)
dataloader = torch.utils.data.DataLoader(
dataset,batch_size=batch_size,shuffle=is_train,
num_workers=4)
return dataloader
# 使用多GPU对ResNet-18模型进行训练
def train_batch_ch13(net,X,y,loss,trainer,devices):
"""使用多GPU"""
if isinstance(X,list):
"""微调BERT中所需"""
X = [x.to(devices[0])for x in X]
else:
X = X.to(devices[0])
y = y.to(devices[0])
net.train()
trainer.zero_grad()
pred = net(X)
l =loss(pred,y)
l.sum().backward()
trainer.step()
train_loss_sum =l.sum()
train_acc_sum = d2l.accuracy(pred,y)
return train_loss_sum,train_acc_sum
def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices=d2l.try_all_gpus()):
"""用多GPU进行模型训练"""
timer, num_batches = d2l.Timer(), len(train_iter)
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1],
legend=['train loss', 'train acc', 'test acc'])
net = nn.DataParallel(net, device_ids=devices).to(devices[0])
for epoch in range(num_epochs):
# 4个维度:储存训练损失,训练准确度,实例数,特点数
metric = d2l.Accumulator(4)
for i, (features, labels) in enumerate(train_iter):
timer.start()
l, acc = train_batch_ch13(
net, features, labels, loss, trainer, devices)
metric.add(l, acc, labels.shape[0], labels.numel())
timer.stop()
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[2], metric[1] / metric[3],
None))
test_acc = d2l.evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {metric[0] / metric[2]:.3f}, train acc '
f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on '
f'{str(devices)}')
#定义train_with_data_aug函数,使用图像增广来训练模型
# 该函数获取所有的GPU,并使用Adam作为训练的优化算法,将图像增广应用于训练集,最后调用刚刚定义的用于训练和评估模型的train_ch13函数
batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10, 3)
# 随机初始化
def init_weights(m):
if type(m) in [nn.Linear, nn.Conv2d]:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
def train_with_data_aug(train_augs, test_augs, net, lr=0.001):
train_iter = load_cifar10(True, train_augs, batch_size)
test_iter = load_cifar10(False, test_augs, batch_size)
loss = nn.CrossEntropyLoss(reduction="none")
trainer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices)
# 使用基于随机左右翻转的图像增广来训练模型
train_with_data_aug(train_augs, test_augs, net)
迁移学习是将从源数据集上学到的知识迁移到目标数据集上,迁移学习中的一种常用技术是微调。
1.在源数据集上预寻来你一个神经网络的模型,源模型
2.创建一个新的神经网络模型,即目标模型,它复制了源模型上除了输出层外的所有模型涉及及其参数,假设这些模型参数包含了源数据集上学习到的知识同样适用于目标数据集,假设源模型的输出层跟源数据集的标签紧密相关,在目标模型中不予蚕蛹
3.为目标模型添加一个输出大小为目标数据集类别个数的输出层,并随机初始化该层的模型参数。
4.在目标数据集上训练目标模型,将从头训练输出层,其余层的参数都是基于源模型的参数微调得到的。
当目标数据集远小于源数据集时,微调有助于提升模型的泛化能力。
在一个小数据集上实现微调
# 将在一个小型数据集上微调ResNet模型。该模型已在ImageNet数据集上进行了预训练。
# 这个小型数据集包含数千张包含热狗和不包含热狗的图像,将使用微调模型来识别图像中是否包含热狗
%matplotlib inline
import os
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
# 该数据集包含1400张热狗的“正类”图像,以及包含尽可能多的其他食物的“负类”图像。
# 含着两个类别的1000张图片用于训练,其余的则用于测试
#@save
d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL + 'hotdog.zip',
'fba480ffa8aa7e0febbb511d181409f899b9baa5')
data_dir = d2l.download_extract('hotdog')
# 创建两个实例来分别读取训练和测试数据集中的所有图像文件
train_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'train'))
test_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test'))
# 显示了前8个正类样本图片和最后8张负类样本图片
hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]
d2l.show_images(hotdogs + not_hotdogs, 2, 8, scale=1.4);
# 训练期间从图像中裁切随机大小和随机长宽比的区域,然后将该区域缩放为输入图像224*224
# 将图像的高度和宽度都缩放到256像素,然后裁剪中央区域224*224作为输入
# 分别标准化每个通道 该通道的每个值减去该通道的平均值,然后将结果除以该通道的标准差
# 使用RGB通道的均值和标准差,以标准化每个通道
# 通道正则化是因为imgnet做了这个事情
normalize = torchvision.transforms.Normalize(
[0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
train_augs = torchvision.transforms.Compose([
torchvision.transforms.RandomResizedCrop(224),
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
normalize])
test_augs = torchvision.transforms.Compose([
torchvision.transforms.Resize(256),
torchvision.transforms.CenterCrop(224),
torchvision.transforms.ToTensor(),
normalize])
# 指定pretrained=True以自动下载预训练的模型参数
pretrained_net = torchvision.models.resnet18(pretrained=True)
# 预训练的源模型实例包含许多特征层和一个输出层fc。 此划分的主要目的是促进对除输出层以外所有层的模型参数进行微调
# 给出源模型的成员变量fc
pretrained_net.fc=Linear(in_features=512, out_features=1000, bias=True)
# 在ResNet的全局平均汇聚层后,全连接层转换为ImageNet数据集的1000个类输出
# 构建一个新的神经网络作为目标模型。 它的定义方式与预训练源模型的定义方式相同,
# 只是最终层中的输出数量被设置为目标数据集中的类数
# 目标模型finetune_net中成员变量features的参数被初始化为源模型相应层的模型参数
# 模型参数是在ImageNet数据集上预训练的,并且 足够好,因此通常只需要较小的学习率即可微调这些参数
# 成员变量output的参数是随机初始化的,通常需要更高的学习率才能从头开始训练
finetune_net = torchvision.models.resnet18(pretrained=True)
# 输出层随机初始化一个线性层 输出类别为2
finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2)
# 最后一层的weight随机初始化权重
nn.init.xavier_uniform_(finetune_net.fc.weight);
# 定义了一个训练函数train_fine_tuning,该函数使用微调
# 如果param_group=True,输出层中的模型参数将使用十倍的学习率
def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5,
param_group=True):
train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train'), transform=train_augs),
batch_size=batch_size, shuffle=True)
test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'test'), transform=test_augs),
batch_size=batch_size)
devices = d2l.try_all_gpus()
loss = nn.CrossEntropyLoss(reduction="none")
if param_group:
params_1x = [param for name, param in net.named_parameters()
if name not in ["fc.weight", "fc.bias"]]
trainer = torch.optim.SGD([{'params': params_1x},
{'params': net.fc.parameters(),
'lr': learning_rate * 10}],
lr=learning_rate, weight_decay=0.001)
else:
trainer = torch.optim.SGD(net.parameters(), lr=learning_rate,
weight_decay=0.001)
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices)
# 使用较小的学习率,通过微调预训练获得的模型参数
train_fine_tuning(finetune_net, 5e-5)
# 定义了一个相同的模型,但是将其所有模型参数初始化为随机值
# 由于整个模型需要从头开始训练,因此我们需要使用更大的学习率
scratch_net = torchvision.models.resnet18()
scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2)
train_fine_tuning(scratch_net, 5e-4, param_group=False)
训练是一个目标数据集上的正常训练任务,但是用更强的正则化,使用更小的学习率
使用更少的数据迭代,元数据集远复杂于目标数据,通常微调效果更好
重用分类器权重,源数据集可能也有目标数据中的部分标号,可以使用预训练好模型分类器中对应标号对应的向量来做初始化。
固定一些层,神经网络通常学习有层次的特征表示:低层次的特征更加通用,高层次的特征则更跟数据集相关,可以固定底部一些层的参数,不参与更新
更强的正则化,微调对学习率不敏感。
图像数据集往往是以图像文件的形式存在的,从原始的图像文件开始,一步步整理、读取并将其转变为NDArray格式。
训练数据集高和宽都是32像素,并且含有RGB三个通道,为了方便训练使用数据集的小规模采样。
import collections
import math
import os
import shutil
import pandas as pd
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
# 提供包含前1000个训练图像和5个随机测试图像的数据集的小规模样本
d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip',
'2068874e4b9a9f0fb07ebe0ad2b29754449ccacd')
# 如果你使用完整的Kaggle竞赛的数据集,设置demo为False
demo = True
if demo:
data_dir = d2l.download_extract('cifar10_tiny')
else:
data_dir = '../data/cifar-10/'
# 定义函数将验证集从原始的训练集中拆分出来,此函数中的参数valid_ratio是验证集中的样本数
# 与原始训练集中的样本数之比,令n等于样本最少的类别中的图像数量,r是比率
# 验证集为每个类别拆分出([nr],1)张图像
# 将同类别的图像放置在同一个文件夹下
#@save
def copyfile(filename, target_dir):
"""将文件复制到目标目录"""
os.makedirs(target_dir, exist_ok=True)
shutil.copy(filename, target_dir)
#@save
def reorg_train_valid(data_dir, labels, valid_ratio):
"""将验证集从原始的训练集中拆分出来"""
# 训练数据集中样本最少的类别中的样本数
n = collections.Counter(labels.values()).most_common()[-1][1]
# 验证集中每个类别的样本数
n_valid_per_label = max(1, math.floor(n * valid_ratio))
label_count = {}
for train_file in os.listdir(os.path.join(data_dir, 'train')):
label = labels[train_file.split('.')[0]]
fname = os.path.join(data_dir, 'train', train_file)
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'train_valid', label))
if label not in label_count or label_count[label] < n_valid_per_label:
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'valid', label))
label_count[label] = label_count.get(label, 0) + 1
else:
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'train', label))
return n_valid_per_label
# 在预测期间正例测试集
def reorg_test(data_dir):
"""在预测期间整理测试集,方便读取"""
for test_file in os.listdir(os.path.join(data_dir,'test')):
copyfile(os.path.join(data_dir,'test',test_file),
os.path.join(data_dir,'train_valid_test','test','unknown'))
#@save 整理数据集
def read_csv_labels(fname):
"""读取fname来给标签字典返回一个文件名"""
with open(fname, 'r') as f:
# 跳过文件头行(列名)
lines = f.readlines()[1:]
tokens = [l.rstrip().split(',') for l in lines]
return dict(((name, label) for name, label in tokens))
labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
print('# 训练样本 :', len(labels)) 1000
print('# 类别 :', len(set(labels.values()))) 10
#@save
def reorg_test(data_dir):
"""在预测期间整理测试集,以方便读取"""
for test_file in os.listdir(os.path.join(data_dir, 'test')):
copyfile(os.path.join(data_dir, 'test', test_file),
os.path.join(data_dir, 'train_valid_test', 'test',
'unknown'))
def reorg_cifar10_data(data_dir, valid_ratio):
labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
reorg_train_valid(data_dir, labels, valid_ratio)
reorg_test(data_dir)
batch_size = 32 if demo else 128
valid_ratio = 0.1
reorg_cifar10_data(data_dir, valid_ratio)
# 使用图像增广来解决过拟合问题
# 水平翻转图像 对彩色图像三个RGB通道执行标准化
transform_train = torchvision.transforms.Compose([
# 在高度和宽度上将图像放大到40像素的正方形
torchvision.transforms.Resize(40),
# 随机裁剪出一个高度和宽度均为40像素的正方形图像,
# 生成一个面积为原始图像面积0.64到1倍的小正方形,
# 然后将其缩放为高度和宽度均为32像素的正方形
torchvision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0),
ratio=(1.0, 1.0)),
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
# 标准化图像的每个通道
torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465],
[0.2023, 0.1994, 0.2010])])
# 在测试期间,只对图像执行标准化,消除评估结果中的随机性
transform_test = torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465],
[0.2023, 0.1994, 0.2010])])
# 读取原始图像组成的数据集,每个样本都包含一张图片和一个标签
train_ds, train_valid_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_train) for folder in ['train', 'train_valid']]
valid_ds, test_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_test) for folder in ['valid', 'test']]
train_iter, train_valid_iter = [torch.utils.data.DataLoader(
# 训练时drop_last直接丢弃最后一个batchsize中不够的部分
dataset, batch_size, shuffle=True, drop_last=True)
for dataset in (train_ds, train_valid_ds)]
valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False,
drop_last=True)
test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False,
drop_last=False)
# 定义模型
def get_net():
num_classes = 10
# 3通道 10类别
net = d2l.resnet18(num_classes, 3)
return net
loss = nn.CrossEntropyLoss(reduction="none")
# 定义训练函数
def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay):
# 每个多少个lr_period,学习率减少lr_decay
trainer = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9,
weight_decay=wd)
# 调整lr
scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay)
num_batches, timer = len(train_iter), d2l.Timer()
legend = ['train loss', 'train acc']
if valid_iter is not None:
legend.append('valid acc')
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=legend)
# 使用多个GPU
net = nn.DataParallel(net, device_ids=devices).to(devices[0])
for epoch in range(num_epochs):
net.train()
metric = d2l.Accumulator(3)
for i, (features, labels) in enumerate(train_iter):
timer.start()
l, acc = d2l.train_batch_ch13(net, features, labels,
loss, trainer, devices)
metric.add(l, acc, labels.shape[0])
timer.stop()
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[2], metric[1] / metric[2],
None))
if valid_iter is not None:
valid_acc = d2l.evaluate_accuracy_gpu(net, valid_iter)
animator.add(epoch + 1, (None, None, valid_acc))
# 每一步之后更新
scheduler.step()
measures = (f'train loss {metric[0] / metric[2]:.3f}, '
f'train acc {metric[1] / metric[2]:.3f}')
if valid_iter is not None:
measures += f', valid acc {valid_acc:.3f}'
print(measures + f'\n{metric[2] * num_epochs / timer.sum():.1f}'
f' examples/sec on {str(devices)}')
# 训练和验证模型
devices, num_epochs, lr, wd = d2l.try_all_gpus(), 20, 2e-4, 5e-4
lr_period, lr_decay, net = 4, 0.9, get_net()
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay)
# 获得具有超参数满意的模型后,使用所有标记数据来重新训练模型并对测试集进行分类
net, preds = get_net(), []
train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period,
lr_decay)
for X, _ in test_iter:
y_hat = net(X.to(devices[0]))
preds.extend(y_hat.argmax(dim=1).type(torch.int32).cpu().numpy())
sorted_ids = list(range(1, len(test_ds) + 1))
sorted_ids.sort(key=lambda x: str(x))
df = pd.DataFrame({'id': sorted_ids, 'label': preds})
df['label'] = df['label'].apply(lambda x: train_valid_ds.classes[x])
df.to_csv('submission.csv', index=False)
weight_decay是权重参数,但是lr_decay是优化算法。
方法和之前的树叶分类是一样的,但是对于模型使用微调来得到更优的模型参数。
import os
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
# 比赛数据集分为训练集和测试集,分别包含RGB(彩色)通道的10222张、10357张JPEG图像 120个类别
#@save
d2l.DATA_HUB['dog_tiny'] = (d2l.DATA_URL + 'kaggle_dog_tiny.zip',
'0cb91d09b814ecdc07b50f31f8dcad3e81d6a86d')
# 如果你使用Kaggle比赛的完整数据集,请将下面的变量更改为False
demo = True
if demo:
data_dir = d2l.download_extract('dog_tiny')
else:
data_dir = os.path.join('..', 'data', 'dog-breed-identification')
# 即从原始训练集中拆分验证集,然后将图像移动到按标签分组的子文件夹中
def reorg_dog_data(data_dir, valid_ratio):
labels = d2l.read_csv_labels(os.path.join(data_dir, 'labels.csv'))
d2l.reorg_train_valid(data_dir, labels, valid_ratio)
d2l.reorg_test(data_dir)
batch_size = 32 if demo else 128
valid_ratio = 0.1
reorg_dog_data(data_dir, valid_ratio)
transform_train = torchvision.transforms.Compose([
# 随机裁剪图像,所得图像为原始面积的0.08到1之间,高宽比在3/4和4/3之间。
# 然后,缩放图像以创建224x224的新图像
torchvision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0),
ratio=(3.0/4.0, 4.0/3.0)),
torchvision.transforms.RandomHorizontalFlip(),
# 随机更改亮度,对比度和饱和度
torchvision.transforms.ColorJitter(brightness=0.4,
contrast=0.4,
saturation=0.4),
# 添加随机噪声,转换成tensor形式
torchvision.transforms.ToTensor(),
# 标准化图像的每个通道 网络中的学习到的参数
torchvision.transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
# 测试时,只使用确定性的图像预处理操作
transform_test = torchvision.transforms.Compose([
torchvision.transforms.Resize(256),
# 从图像中心裁切224x224大小的图片
torchvision.transforms.CenterCrop(224),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
# 读取整理后含原始图像文件的数据集
train_ds, train_valid_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_train) for folder in ['train', 'train_valid']]
valid_ds, test_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_test) for folder in ['valid', 'test']]
train_iter, train_valid_iter = [torch.utils.data.DataLoader(
dataset, batch_size, shuffle=True, drop_last=True)
for dataset in (train_ds, train_valid_ds)]
valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False,
drop_last=True)
test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False,
drop_last=False)
# 微调模型 用一个可以训练的小型自定义输出网络来替换原始输出层
def get_net(devices):
finetune_net = nn.Sequential()
finetune_net.features = torchvision.models.resnet34(pretrained=True)
# 定义一个新的输出网络,共有120个输出类别
finetune_net.output_new = nn.Sequential(nn.Linear(1000, 256),
nn.ReLU(),
nn.Linear(256, 120))
# 将模型参数分配给用于计算的CPU或GPU
finetune_net = finetune_net.to(devices[0])
# 冻结参数
for param in finetune_net.features.parameters():
param.requires_grad = False
return finetune_net
# 在计算损失之前,首先获取预训练模型的输出层的输入作为小型自定义输出网络输入来计算损失
loss = nn.CrossEntropyLoss(reduction='none')
def evaluate_loss(data_iter, net, devices):
l_sum, n = 0.0, 0
for features, labels in data_iter:
features, labels = features.to(devices[0]), labels.to(devices[0])
outputs = net(features)
l = loss(outputs, labels)
l_sum += l.sum()
n += labels.numel()
return (l_sum / n).to('cpu')
# 模型训练函数train只迭代小型自定义输出网络的参数
def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay):
# 只训练小型自定义输出网络
net = nn.DataParallel(net, device_ids=devices).to(devices[0])
trainer = torch.optim.SGD((param for param in net.parameters()
if param.requires_grad), lr=lr,
momentum=0.9, weight_decay=wd)
scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay)
num_batches, timer = len(train_iter), d2l.Timer()
legend = ['train loss']
if valid_iter is not None:
legend.append('valid loss')
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=legend)
for epoch in range(num_epochs):
metric = d2l.Accumulator(2)
for i, (features, labels) in enumerate(train_iter):
timer.start()
features, labels = features.to(devices[0]), labels.to(devices[0])
trainer.zero_grad()
output = net(features)
l = loss(output, labels).sum()
l.backward()
trainer.step()
metric.add(l, labels.shape[0])
timer.stop()
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[1], None))
measures = f'train loss {metric[0] / metric[1]:.3f}'
if valid_iter is not None:
valid_loss = evaluate_loss(valid_iter, net, devices)
animator.add(epoch + 1, (None, valid_loss.detach().cpu()))
scheduler.step()
if valid_iter is not None:
measures += f', valid loss {valid_loss:.3f}'
print(measures + f'\n{metric[1] * num_epochs / timer.sum():.1f}'
f' examples/sec on {str(devices)}')
# 训练和验证模型
devices, num_epochs, lr, wd = d2l.try_all_gpus(), 10, 1e-4, 1e-4
lr_period, lr_decay, net = 2, 0.9, get_net(devices)
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,lr_decay)
#最终所有标记的数据(包括验证集)都用于训练模型和对测试集进行分类。 我们将使用训练好的自定义输出网络进行分类
net = get_net(devices)
train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period,
lr_decay)
preds = []
for data, label in test_iter:
output = torch.nn.functional.softmax(net(data.to(devices[0])), dim=0)
preds.extend(output.cpu().detach().numpy())
ids = sorted(os.listdir(
os.path.join(data_dir, 'train_valid_test', 'test', 'unknown')))
with open('submission.csv', 'w') as f:
f.write('id,' + ','.join(train_valid_ds.classes) + '\n')
for i, output in zip(ids, preds):
f.write(i.split('.')[0] + ',' + ','.join(
[str(num) for num in output]) + '\n')