NLP&NLUpytorchpytorch

从数据到模型,你可能需要1篇详实的pytorch踩坑指南

2020-01-20  本文已影响0人  叶琛_

2020年的第一篇文章,将会分享一下我最近第一次使用pytorch的经验(和踩过的坑)。

全文约 2200 字
阅读时间约 7 分钟

了解深度学习的朋友应该知道Tensorflow、Keras、Pytorch、Caffe这些成熟的框架,它们让广大的AI爱好者避免了重复造轮子的工作。当你有一个好的想法,这些工具可以帮助你最快速复现,将想法转成代码、模型。巨人的肩膀上,真的是风光无限好。

此前,我一直在用Tensorflow及其高级API-Keras框架,后者简洁明了的API风格能让一个复杂的模型在10行代码内搞定。最近,我刚好接到一个新任务,将一句自然语言问句(query)转换成结构化语言,简称“NL2SQL”。属于NLP语义解析领域子任务。

我将该任务拆分成了多个子任务联合训练。为了方便调用不同的子层,这一次尝试使用pytorch。上手之后发现,基于动态图的pytorch原来这么,让你轻松使用类似“python”的语法,从此搭建一个深度学习模型就像在写一个python函数。

当然一个优秀的算法工程师或程序员肯定不只满足于调用别人封装好的API;所以更多时候,开发者是评估了需求实现自定义模型。

经过一些实际项目的锻炼,我大致总结了使用深度学习解决1个实际问题的步骤:

0. 分析问题
 - 这是什么问题,传统方法能否解决?分类还是回归?端到端还是多个子任务?等等
1. 准备数据
 - 包含分析、清洗、归一、划分等步骤,通常会将数据封装成迭代器形式方便模型调用,节约资源
2. 模型设计
 - 根据任务设计相应模型,包括损失函数、优化器的选择等
3. 结果分析
 - 根据模型训练结果,分析模型是否work;可能会多次进行1-3步的迭代优化
 - 对于复杂任务如文本生成,这一步还需要对模型输出结果进行解码、还原
4. 封装交付
 - 可能会多次迭代

由于这是一篇guide兼踩坑指南,主要是对自己目前碰到的问题进行总结,很可能分析的不够完善。笔者会持续更新此文内容,也欢迎读者朋友们将自己遇到过的问题(附上解决方法就更好啦)留言,笔者会尽可能选择一些加入文章方便其他读者避坑。

写在开始:

1.用好官方文档

对于pytorch还陌生的朋友,入门的最好方法是直接看官方文档。从类、函数到具体对象,都有详尽清晰的介绍,同时提供了诸多示例,还不用翻墙。这一点个人认为比tensorflow做的好很多。

官网首页就是安装方法,根据python版本、安装包、OS的差异提供了不同路径,可谓考虑非常周到了。


pytorch-首页

在国内,有时因为网速原因通过官网安装会非常慢,我通常会上网找一些国内镜像资源解决。

当然,对于开发人员最有用的应该是Docs页面,提供了pytorch各个模块的解释和示例,还有源码链接。相信你的问题80%的问题都能在这儿解决。

pytorch-Docs
Docs 地址:
https://pytorch.org/docs/stable/index.html

2.学好 numpy

pytorch的基本数据类型torch.tensor能和numpy对象“无缝切换”。很多关于张量的操作也和numpy的方法基本一致,所以想学好pytorch,可以先复习下numpy。掌握基本的矩阵操作,学习pytorch就不难啦。
(ps:时刻观察当前张量的size变化,可以帮助你更好的了解数据所经历的操作)

3.本文测试环境

本文的实验环境为:

pytorch-0.4.1
python-3.6

下面让我们愉快的正式开始。


1.数据篇

“数据决定了最终结果的上界,好的模型帮助你不断逼近这个上界”,应该很多人都听过这句话。

2014年深度学习重新绽放活力以来,基于神经网络的模型不断刷新着各个领域的任务排行榜,某些任务甚至超越了人类表现。这背后是两大重要能力的支撑:快速的计算资源和庞大的数据。

pytorch工具包中提供了很多和数据准备相关的工具,比如最常用的有这两个:

from torch.utils.data import DataLoader, Dataset

Dataset是一个数据包装抽象类,我们往往希望加载自己的数据,只需要继承该类,重写两个方法即可。

Dataset
例如,我想在类的初始化函数中对传入的文本分词,可能写成类似这样:
class MyData(Dataset):
    
    def __init__(self, texts, labels, is_train=True):
        self.texts = [jieba.lcut(t) for t in texts]
        self.labels = labels
        # 其他操作 ....
  
    def __getitem__(self, item):
        
        token_id = convert_tokens_to_ids(self.texts[item]) # 词 -> token_id
        label = self.labels[item]
        return torch.LongTensor(token_id), torch.LongTensor([label]),

    def __len__(self):
        return len(self.texts)

然后我希望将数据封装成迭代器,每次访问数据时可以返回一个指定batch大小的批数据,以减少内存占用:

def get_dataloader(dataset, batch_size, shuffle=False, drop_last=False):
    data_iter = DataLoader(
        dataset=dataset,
        batch_size=batch_size,
        shuffle=shuffle,
        drop_last=drop_last
    )
    return data_iter

dataset = MyData(texts, labels)
dataloader = get_dataloader(dataset, batch_size=16) # 成功封装成迭代器啦

这样在进行训练或测试时,可以很方便的按batch调用数据。具体参数作用可以按“写在开始”中的地址去详细查看。

def train():
    model.cuda()
    model.train()
    for epoch in range(10):
        for batch in dataloader:
            model(batch, "train")
            pass

是不是很简单呢!

接下来讲一些碰到的坑(这块内容会不断更新滴):

1.GPU / CPU 张量转换

torch.cuda模块对tensor在CPU、GPU之前的切换提供了很好的支持。如果你的深度学习模型是在GPU环境下运行的model.cuda(),则需要将数据转换到GPU上再喂入模型;CPU上的数据和GPU数据直接计算时会抛错。

2.数据填充对齐-pad

一般来说NLP模型的输入是词ID矩阵,形状为[batch_szie, seq_len]。原始文本长度seq_len很可能是参差不齐的,但是神经网络的输入需要一个规整的张量,所以需要通过裁剪(丢失信息较多)或填充的方式使得它们变成定长。

以下代码是针对一个list进行填充(好像用什么框架都需要这一步╮(╯▽╰)╭ ;填充值一般习惯性选“0”)

def pad(s_list, pad_value=0):
    '''s_list = [[1,2,3,1,0],[1,2,3,]]'''
    max_len = max(len(i) for i in s_list)
    s_list = [s + [0] * (max_len - len(s)) 
                if len(s) < max_len 
                else s[:max_len] 
                for s in s_list]
    return s_list

2.模型篇

1.模型自定义

pytorch提供了和Keras类似的序列化方式来定义模型,一个简单的CNN网络可以写成:

import torch.nn as nn
model = nn.Sequential(
    nn.Conv2d(1,20,5)
    nn.ReLU()
)

但是实际开发中,这样写基本没什么意义,我们需要的是根据具体任务定义自己的模型。这在pytorch中也是很容易的一件事。分2步:继承Moulde类,重写init、forward函数

import torch.nn.functional as F

optimizer = Adam(lr=2e-5) # 优化器

class MyModel(nn.Module):
    def __init__(self, ):
        super(MyModel, self).__init__()
        self.bert = BertModel.from_pretrained('/chinese_bert_pytorch/', cache_dir=None)
        self.s_linear = torch.nn.Linear(768, 1)
    
    def forward(self, batch, task='train'):

        batch = [b.cuda() for b in batch] # if GPU

        if task == 'train':
            input, input_type, label = batch
            _, pooled = self.bert(input, input_type)
            out = self.s_linear(pooled) # 1.计算输出
            loss = F.binary_cross_entropy_with_logits(out, label).sum() # 2.计算loss
            optimzer.zero_grad() # 3.清空梯度
            loss.backward() # 4.反向传播计算参数梯度
            optimzier.step() # 5.根据梯度和优化策略,更新参数
            
        elif task == 'eval':
            input, input_type = batch
            pooled = self.bert(input, input_type) 
            out = self.s_linear(pooled)
            out = torch.sigmoid(out)
            return out

通常,我们在__init__函数中定义模型需要使用的层以及初始化等。forward函数,开始进行前向传播、反向传播(自动)、计算loss等过程。可以简单概括成5点:

1.计算模型输出 out
2.借损失函数计算和真实label之间的误差loss
3.清空梯度
4.反向传播计算梯度
5.更新参数

这样,我们就完成了对一个深度学习模型的训练、预测、更新过程。

trick,是在forward中同时传入任务类型task,既可以做训练又可以纯粹预测;因为预测时只进行了前向传播,所以通常将模型输出结果直接返回,再做后处理。

note,只有标量才能直接使用backward(),如果是对一个batch_size计算loss,得到的不是标量,要先使用.sum()转换成scalar。否则会报错:

RuntimeError: grad can be implicitly created only for scalar outputs
2.模型转换

前边提到,CPU上的数据不能和GPU上数据直接计算,模型也是如此。要用GPU时,先简单做一个转换。

model.cuda() # 将模型所有参数和缓存转至GPU
if task == 'train':
    model.train()
else:
    model.eval() # 冻结 dropout、BN 层,具体参考官方文档
3.避免OOM

训练过程中由于loss.backward() 会将计算图的隐藏变量梯度清除,从而释放空间;但是测试的时候没有这一机制,因此有可能随着测试的进行中间变量越来越多,导致out of memory的发生。

pytorch0.4.1以上可以使用with torch.no_grad():来进行数值计算,不需要创建计算图;也就不会跟踪计算梯度,节省了内存/显存。

with torch.no_grad(): # 不进行梯度计算
    for batch in testloader:
       res = model(batch, task='eval')
       res = res.cpu() # 转换回CPU,节约显存
       pass

如果显存不够大支撑不了实验,一般有几种缓解方法:

1.加大显存
2.减小batch
3.使用一些策略及时释放显存

最后再次强调,学习pytorch的最好途径是阅读官方文档(中文翻译版亦可)。如果能跟着官方Doc学习,结合一些项目实战(NLP、CV等等都可以),想必会有事半功倍的效果。

3.训练篇

1.损失函数

pytorch根据模型输出和真实label计算损失时,一般使用损失函数。对于binary_cross_entropy_with_logits函数,这里有2个注意点:
(1)计算损失时,inputtarget需要先转换成float类型
(2)reduction参数可以决定返回的loss是tensor还是一个整数

import torch.nn.functional as F
print(F.binary_cross_entropy_with_logits(torch.LongTensor([[1.2,3.1,2.2]]).float(), torch.LongTensor([[1,2,3]]).float(), reduction='none'))
print(F.binary_cross_entropy_with_logits(torch.LongTensor([[1.2,3.1,2.2]]).float(), torch.LongTensor([[1,2,3]]).float(), reduction='mean'))
print(F.binary_cross_entropy_with_logits(torch.LongTensor([[1.2,3.1,2.2]]).float(), torch.LongTensor([[1,2,3]]).float(), reduction='sum'))
# output
# tensor([[ 0.3133, -2.9514, -3.8731]])
# tensor(-6.5112)
# tensor(-6.5112)
2.Device-side assert triggered (2020.2.17更新)

报错输出的典型信息:

[RuntimeError: cuda runtime error (59) : device-side assert triggered at /opt/conda/condabld/pytorch_1503970438496/work/torch/lib/THC/generic/THCStorage.c:32]....

这个错误一般在model进行forward前向传播中碰到,典型原因是GPU tensor 下索引失败引起的异常,out-of-bounds 即在[0, x]下,索引为负,或者超过 x。

建议:检查targets有没有越界!比如输入数据到nn.Linear(768, 10)层,对应的索引范围应该是0-9,如果输入1-10就会报错。

3.zip argument #1 must support iteration(2020.3.23更新)

这个错误是使用GPU多卡训练时碰到的;错误的原因是多gpu训练时,服务器自动把你的batch_size分成n_gpu份,每个gpu跑一些数据, 最后再合起来。我之所以出现这个bug是因为我在模型返回的时候(forward函数中),除了loss还返回了标量(这一批batch_size中正确预测的个数,int类型)。

所以多卡训练时避免从训练过程中返回标量;其他统计指标可以在训练完一个epoch再进行。如果是单卡训练,则返回标量还是张量,都没有问题了。


4.Loss篇

1.使用Cross_entropy损失函数时出现 RuntimeError: multi-target not supported at … (2020.2.17更新)

输入的真实标签必须为0~n-1(sparse编码,非one-hot),而且必须为1维的,如果设置标签为[nx1]的,则也会出现以上错误。
cross_entropy官网函数定义

# input (Tensor) – size = (N, C) where C = number of classes
# target (Tensor) – size = (N,) where each value is 0 <= targets[i] <= C - 1
torch.nn.functional.cross_entropy(input, target, ...)

写在最后:

pytorch框架虽然好用,但只是干活的工具不是目的;同时Keras也有即插即用,部署方便等优点。综上,最理想的状态是两个工具都能灵活使用,且能适当了解框架底层的架构、源码,可以按需自定义模型、loss等等。

Keras入门推荐:《python深度学习》- Keras之父作品
以往文章推荐:如何用python爬取美团点评数据并做情感分析

Reference:
[PyTorch]论文pytorch复现中遇到的BUG

上一篇下一篇

猜你喜欢

热点阅读