深度学习 GPU与模型迁移 笔记

本文最后更新于:2023年6月17日 下午

多GPU计算

多GPU计算

方式

splitting

网络分区

  • 在多个GPU之间拆分网络。每个GPU将流入特定层的数据作为输入,跨多个后续层对数据进行处理,然后将数据发送到下一个GPU。
  • GPU的接口之间需要的密集同步可能是很难办的,还有层之间的接口需要大量的数据传输的时候数据量可能会超出GPU总线的带宽。

按层分区

  • 拆分层内的工作。
  • 我们需要大量的同步或屏障操作(barrier operation),因为每一层都依赖于所有其他层的结果。

数据并行

data-parallel
  • 每块GPU上的参数值都是相同且同步的
  • 每个GPU独立地维护一组完整的模型参数
  • 计算过程
    1. 在任何一次训练迭代中,给定的随机的小批量样本都将被分成k个部分,并均匀地分配到GPU上;
    2. 每个GPU根据分配给它的小批量子集,计算模型参数的损失和梯度;
    3. k个GPU中的局部梯度聚合,以获得当前小批量的随机梯度;
    4. 聚合梯度被重新分发到每个GPU中;
    5. 每个GPU使用这个小批量随机梯度,来更新它所维护的完整的模型参数集。

数据并行例子

手动实现

1
2
3
4
5
%matplotlib inline
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
定义模型
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
27
28
# 初始化模型参数
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')
数据同步
1
2
3
4
5
6
7
8
9
10
11
12
13
def get_params(params,device):
# 参数送到GPU
new_params = [p.to(device) for p in params]
# 对送到GPU中的参数附加梯度
for p in new_params:
p.requires_grad_()
return new_params

# 测试效果
new_params = get_params(params,d2l.try_gpy(0))
b1 权重: 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 梯度: None
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def allreduce(data):
# 把分发在各个GPU上的向量加起来
for i in range(1,len(data)):
data[0][:] += data[i].to(data[0].device)
for i in range(1,len(data)):
data[i][:] +=data[0].to(data[i].device)

# 测试结果
allreduce之前:
tensor([[1., 1.]], device='cuda:0')
tensor([[2., 2.]], device='cuda:1')
allreduce之后:
# 每块GPU都得到了所有GPU上的向量和
tensor([[3., 3.]], device='cuda:0')
tensor([[3., 3.]], device='cuda:1')
数据分发

在训练前,把数据均匀的分发到每块显卡。这里用了torch.nn封装好的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# nn.Parallel.scatter()函数说明

# 使用 4 x 5 的测试数据
data = torch.arrange(20).reshape(4,5)
devices = [torch.device('cuda:0'),torch.device('cuda:1')]
split = nn.Parallel.scatter(data,devices)

# 测试结果
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'))

1
2
3
4
5
# 辅助函数 拆分X,y到多个GPU
def split(X,y,devices):
assert X.shape[0] == y.shape[0]
return (nn.parallel.scatter(X,devices),
nn.parallel.scatter(y,devices))
训练
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def train_batch(X,y,device_params,devices,lr):
# X_shards,y_shards 送到显卡上的X,y
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:
l.backward()
with torch.no_grad():
# 将所有GPU中的局部梯度聚合,以获得当前小批量的随机梯度
# 对应计算过程3
for i in range(len(device_params[0])):
allreduce([
device_params[c][i].grad for c in range(len(devices))
])
# 在每块GPU上分别更新模型参数
# 对应计算过程4
for param in device_params:
d2l.sgd(param,lr,X.shape[0])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def train(num_gpus,batch_size,lr):
train_iter,test_iter = d2l.load_data_fashion_mnist(batch_size)
devices = [d2l.try_gpus(i) for i in range(num_cpus)]
# 调用get_params()将模型参数送入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):
train_batch(X,y,device_params,devices,lr)
# 等待当前设备上所有流中的所有核心完成。
# pytorch里是异步的,避免提前退出
# 正确测试时间
torch.cuda.synchronize()
time.stop()
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)}')


1
2
train(num_gpus=1, batch_size=256, lr=0.2)
train(num_gpus=2, batch_size=256, lr=0.2)

简洁实现

1
2
3
import torch
from torch import nn
from d2l import torch as d2l
定义模型
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
27
28
#@save
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
1
2
net = resnet(18)
devices = d2l.try_all_gpus()
训练
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
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)
# SGD 优化
trainer = torch.optim.SGD(net,device_ids=devices)
loss = nn.CrossEntropyLoss()
timer,num_epochs = d2l.Timer()
animator = d2l.Animator('epoch','test acc',xlim=[1,num_epochs])
for epochs in range(num_epochs):
net.train()
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)}')
1
2
train(net, num_gpus=1, batch_size=256, lr=0.1)
train(net, num_gpus=1, batch_size=256, lr=0.1)

模型迁移

网络架构

  • 一个神经网络一般可以分为两个部分
    • 特征提取模块,将原始像素变为容易线性分割的特征
    • 分类模块,线性分类器进行分类

微调

在一个比较大的数据集上训练的模型,可以把它的特征提取模块拿出来重新用一用。

../_images/finetune.svg

训练

  • 目标数据集上正常的训练任务,使用更强的正则化
    • 使用更小的学习率
    • 使用更少的数据迭代
  • 源数据集远复杂于目标数据集,微调效果更好

重用分类器权重

最后一层分类层满足条件也可以进行迁移学习

  • 源数据集可能也有目标数据中的部分标号
  • 可以使用预训练好模型分类器中对应标号对应的向量来做初始化

固定一些层

是一种更强的正则

  • 神经网络通常学习有层次的特征表示
    • 低层次的特征更加通用
    • 高层次的特征则更与数据集有关
  • 可以固定底部的一些层的参数,不参与更新

代码

1
2
3
4
5
6
%matplotlib inline
import os
import torch
import torchvision
from torch import nn
from d2l import torch as d2l

数据加载

1
2
3
4
5
6
#@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'))

数据预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 使用RGB通道的均值和标准差,以标准化每个通道
normalize = torchvision.transforms.Normalize(
[0.485, 0.456, 0.406], [0.229, 0.224, 0.225])

# 把图片Resize 到 和 ImageNet预训练模型大小一样
train_augs = torchvision.transforms.Compose([
torchvision.transforms.RandomResizedCrop(224),
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
normalize])

test_augs = torchvision.transforms.Compose([
# 等比缩放为256 x 256 再取中间 224 x 224
# 解决不同照片大小不一样的问题
torchvision.transforms.Resize([256, 256]),
torchvision.transforms.CenterCrop(224),
torchvision.transforms.ToTensor(),
normalize])

拿模型、改模型

输出层随机初始化,从头开始训练

1
2
3
4
5
6
# 拿到预训练好的模型
finetune_net = torchvision.models.resnet18(pretrained=True)
# 对最后一层全连接层 fully_connect 分类类别改为2
finetune_net.fc = nn.Linear(finetune_net.fc.in_features,2)
# 对最后一层全连接层 fully_connect 的参数进行初始化
nn.init.xavier_uniform_(finetune_net.fc.weight)

定义训练函数

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
27
28
29
30
31
32
33
34
35
36
37
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:
# 拿出来非最后一层fully connected层的参数
params_1x = [param for name, param in net.named_parameters()
if name not in ["fc.weight", "fc.bias"]]
"""
复制的cen
params_1x = []
for name,param in net.named_parameters():
if name not in['fc.weight','fc.bias']:
param.requires_grad = False
params_1x.append(param)
"""
# 设置优化器参数
trainer = torch.optim.SGD([
# 非fc层参数
{'params': params_1x},
# fc层参数
{'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)

1
train_fine_tuning(finetune_net, 5e-5)

总结

  • 微调通过使用在大数据上得到的预训练好的模型来初始化新模型的权重来完成精度提升
  • 预训练模型质量很重要
  • 微调通常速度更快,精度更高

深度学习 GPU与模型迁移 笔记
https://anonymouslosty.ink/2023/06/15/深度学习 GPU与模型调度笔记/
作者
Ling yi
发布于
2023年6月15日
更新于
2023年6月17日
许可协议