深度学习训练加速方法

一.GPU利用率低怎么办

    • 增加batch size,增加GPU的内存占用率
    • 在数据加载时候,将num_workers线程数设置稍微大一点,推荐是8,16等,且开启pin_memory=True。
torch.utils.data.DataLoader(image_datasets[x],
                            batch_size=batch_size, 
                            shuffle=True,
                            num_workers=8,
                            pin_memory=True)

打开pin_memory打开,就省掉了将数据从CPU传入到缓存RAM里面,再给传输到GPU上;
为True时是直接映射到GPU的相关内存块上,省掉了一点数据传输时间。

二.训练加速

  • 数据并行指的是,多张 GPU 使用相同的模型副本,但是使用不同的数据批进行训练。

数据并行的具体原理流程为:

  1. 将模型加载至主设备上,作为 controller,一般设置为 cuda:0
  2. 在每次迭代时,执行如下操作:
  3. 将 controller 模型复制(broadcast)到每一个指定的 GPU 上
  4. 将总输入的数据 batch,进行均分,分别作为各对应副本的输入 (scatter)
  5. 每个副本独立进行前向传播,并进行反向传播,但只是求取梯度
  6. 将各副本的梯度汇总(gather)到 controller 设备,并进行求和 (reduced add)
    During the backwards pass, gradients from each replica are summed into the original module.
  7. 更具总梯度,更新 controller 设备上的参数(其他设备也会被更新)
net = torch.nn.DataParallel(model, device_ids=[0, 1, 2])
output = net(input_var)  # input_var can be on any device, including CP
# 流程伪代码
def train_batch(data, k):
    split data into k parts
    for i = 1, ..., k:  # run in parallel
        compute grad_i w.r.t. weight_i using data_i on the i-th GPU
    grad = grad_1 + ... + grad_k
    for i = 1, ..., k:  # run in parallel
        copy grad to i-th GPU
        update weight_i by using grad
  • 模型并行指的是,将模型的不同部分,分别放置于不同的 GPU 上,并将中间结果在 GPU 之间进行传递,使用同一部分数据。解决了单个模型太大,不能存放于一个 GPU 的情况。然而,需要注意的是,相较于在单个 GPU 上运行,其速度更慢。
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()

        self.features_1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3),
            nn.BatchNorm2d(16),
            nn.ReLU(inplace=True),  # 30
            ......
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),  # 12
        ).to('cuda:0')

        self.features_2 = nn.Sequential(
            nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=2),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),  # 5
            ......).to('cuda:1')  # 1

        self.classifier = nn.Sequential(
            nn.Dropout(),
            ......
            nn.Linear(1024, class_num)).to('cuda:1')

    def forward(self, x):
        out = self.features_1(x.to('cuda:0'))
        out = self.features_2(out.to('cuda:1'))
        out = out.view(-1, 384)
        out = self.classifier(out)
        out = F.softmax(out, dim=1)
        return out
# 此时,不在此需要使用 model = model.cuda()
model = ToyModel()

loss_fn = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.001)

optimizer.zero_grad()

for data in trainloader:
    images, labels = data

    # 要处理的部分
    images = images.to('cuda:0')
    labels = labels.to('cuda:1')   # 必须与输出所在 GPU 一致

    outputs = net(images)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

三.参考

BERT论文及QA

一. 论文

【NAACL-HLT-2019】BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

二. 常见问题

  • Mask相对于CBOW有什么异同点?
    • 相同点
      • CBOW的核心思想是:给定上下文,根据它的上文 Context-Before 和下文 Context-after 去预测input word。而BERT本质上也是这么做的,但是BERT的做法是给定一个句子,会随机Mask 15%的词,然后让BERT来预测这些Mask的词。
    • 不同点
      • 首先,在CBOW中,每个单词都会成为input word,而BERT不是这么做的,原因是这样做的话,训练数据就太大了,而且训练时间也会非常长。
      • 其次,对于输入数据部分,CBOW中的输入数据只有待预测单词的上下文,而BERT的输入是带有[MASK] token的“完整”句子,也就是说BERT在输入端将待预测的input word用[MASK] token代替了。
      • 另外,通过CBOW模型训练后,每个单词的word embedding是唯一的,因此并不能很好的处理一词多义的问题,而BERT模型得到的word embedding(token embedding)融合了上下文的信息,就算是同一个单词,在不同的上下文环境下,得到的word embedding是不一样的。
  • BERT的两个预训练任务对应的损失函数是什么?
    • BERT的损失函数由两部分组成,第一部分是来自 Mask-LM 的单词级别分类任务(多分类交叉熵),另一部分是句子级别的分类任务(二分类交叉熵)。通过这两个任务的联合学习,可以使得 BERT 学习到的表征既有 token 级别信息,同时也包含了句子级别的语义信息。
    • 具体的预训练工程实现细节方面,BERT 还利用了一系列策略,使得模型更易于训练,比如对于学习率的 warm-up 策略,使用的激活函数不再是普通的 ReLu,而是 GeLu,也使用了 dropout 等常见的训练技巧。
  • Bert的双向体现在什么地方?
    • 双向主要体现在Bert的预训练任务一:遮蔽语言模型(MLM),[MASK]通过attention均结合了左右上下文的信息,这体现了双向,attention是双向的,但GPT通过attention mask达到单向.
    • BERT(Transformer encoder)
    • GPT(Transformer decoder)
  • Bert的是怎样预训练的?
    • 下面两个任务共享Bert,使用不同的输出层,做Muti-Task。
      • MLM:将一句被mask的句子输入Bert模型,对模型输出的矩阵中mask对应位置的向量做分类,标签就是被mask的字在字典中对应的下标
      • NSP:训练一个下一句预测的二元分类任务。具体 来说,在为每个训练前的例子选择句子 A 和 B 时,50% 的情况下 B 是真的在 A 后面的下一个句子, 50% 的情况下是来自语料库的随机句子,然后将[cls]对应的向量取出做二分类训练
  • MASK的缺点
    • 因为Bert用于下游任务微调时, [MASK] 标记不会出现,它只出现在预训练任务中。这就造成了预训练和微调之间的不匹配,微调不出现[MASK]这个标记,模型好像就没有了着力点、不知从哪入手。所以只将80%的替换为[mask],但这也只是缓解、不能解决
    • 相较于传统语言模型,Bert的每批次训练数据中只有 15% 的标记被预测,这导致模型需要更多的训练步骤来收敛。
    • 被mask掉的token是相互独立的, 忽略了这些token之间的联系
  • BERT和Transformer学到了什么?(What does BERT learn about the structure of language?)
    • BERT的低层网络就学习到了短语级别的信息表征,BERT的中层网络就学习到了丰富的语言学特征(句法信息),而BERT的高层网络则学习到了丰富的语义信息特征