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

mhjjp 发布于15天前

原创 · 作者 | Giant

学校 | 浙江大学

研究方向 | 对话系统、text2sql

熟悉DL的朋友应该知道Tensorflow、Pytorch、Caffe这些成熟的框架,它们让广大AI爱好者站在巨人的肩膀上,避免了重复造轮子的工作。当你有一个好的想法,这些工具可以帮助你快速复现,将idea变现成代码、模型。

此前,我一直在用Tensorflow及其高级API-Keras框架,后者简洁明了的API风格能让一个复杂的模型简化到10行代码。最近,因项目需要接触了基于动态图的pyTorch框架,再一次验证了真香定律。类似python的语法,让开发者搭建一个深度学习模型就像写一个python函数那样简单。

类似其他框架,pytorch已经封装好了AlexNet、VGG等模型结构,但一个优秀的算法工程师肯定不满足于调用别人封装好的API;往往,开发者是评估了需求后实现自定义模型。

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

(1) 分析问题

这是什么问题,分类还是回归?传统ML方法能否解决?端到端还是多个子任务?等等

(2)准备数据

包含数据分析、清洗、归一、划分等步骤,通常会将数据封装成迭代器形式方便模型调用,节约资源

(3)模型设计

根据任务设计相应模型,包括损失函数、优化器的选择等

(4) 结果分析

观察模型训练结果,通过压力测试、badcase分析等决定模型是否work;可能会多次进行1-3步的迭代优化

对于复杂任务如文本生成、text2sql,需要对模型输出结果先进行解码、还原

(5)封装交付

随着业务迭代,可能会对模型多次训练、微调,或结构变动。

由于这是一篇guide兼踩坑指南,本文主要针对自己碰到过的问题进行总结,欢迎读者朋友们将自己遇到过的问题(附上解决方法就更好啦)留言,共同避坑。

准备篇

1.用好官方文档

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

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

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

在国内,有时因为网速原因通过官网下载会非常慢,可以找一些镜像资源快速安装。

# 豆瓣镜像快速安装 1.1.0 版本pytorch
pip install torch==1.1.0 -i https://pypi.douban.com/simple

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

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

最后,介绍一本pytorch官方推荐的入门书籍: 《Deep-Learning-with-PyTorch》 ,主要面向有python基础的同学,介绍如何从0用pytorch搭建一个深度学习项目(软件/硬件)。

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

Content

2.学好 numpy

pytorch的基本数据类型 从数据到模型,你可能需要1篇详实的pytorch踩坑指南 能和numpy对象“无缝切换”。很多关于张量的操作也和numpy的方法基本一致,所以想学好pytorch,可以先复习numpy。掌握了基本的矩阵操作,学习pytorch就不难啦。

(观察当前张量的 shape 变化,可以帮助你更好地了解数据的变换过程和debug)

3.本文测试环境

本文的实验环境为:

pytorch-1.1.0
python-3.6

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

数据篇

“数据决定了最终结果的上界,好的模型帮助你不断逼近这个上界"。

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

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

from torch.utils.data import DataLoader, Dataset

Dataset 是一个数据包装抽象类,我们往往希望加载自定义的数据,只需要继承该类,重写“__ getitem__ ”和“__ len_ _”两个方法即可。

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

例如,我想在类的初始化函数中对传入的文本分词,可能写成这样:

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大小的批数据,不需要一次性把所有数据都load到内存以减少占用:

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:
            # 传入一个 batch 的数据
            model(batch, "train")
            pass

是不是很简单呢!

接下来是一些碰到过的坑:

1.GPU / CPU 张量转换

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

2.数据填充对齐-pad

一般来说NLP模型的输入是词ID矩阵,形状为 [batch_size, 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

模型篇

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
import torch.nn as nn

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 needs 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 函数中定义前向传播、反向传播(pytorch后端自动实现)、计算loss等过程。可以简单概括成5点:

1.计算模型输出 out

2.借损失函数计算和真实label之间的误差loss

3.清空梯度

4.反向传播计算梯度

5.更新参数

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

这里介绍一个小 trick: 在forward中同时传入任务类型 task ,这样1份代码既可以做训练又可以做预测;因为预测时只进行了前向传播,所以通常将模型输出结果直接返回,再做后处理。

另外需要注意, 只有标量才能直接使用backward() ,如果是对一个batch_size计算loss,得到的不是标量,要先使用tensor.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等都可以),想必会有事半功倍的效果。

训练篇

1.损失函数

pytorch根据模型输出和真实label计算损失时,一般使用损失函数。对于常用的二元交叉熵损失函数 binary_cross_entropy_with_logits ,有2个注意点:

(1)计算损失时,input和target需要先转换成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 Error

报错输出的典型信息:

[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有没有越界!比如输入数据到 从数据到模型,你可能需要1篇详实的pytorch踩坑指南 层,对应的索引范围应该是0-9,如果输入1-10就会报错。

3.zip argument #1 must support iteration rror

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

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

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

Loss篇

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

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

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也有即插即用,部署方便等优点。综上,最理想的状态是各个工具都能灵活使用,且适当了解框架底层的架构、源码,可以按需debug、自定义模型和loss等等。

1.pyTorch官网

2.horace.io/pytorch-vs-te

3.[PyTorch]论文pytorch复现中遇到的BUG

本文由作者授权AINLP原创发布于公众号平台,欢迎投稿,AI、NLP均可。 原文链接,点击"阅读原文"直达:

https://zhuanlan.zhihu.com/p/149771904

推荐阅读

这个NLP工具,玩得根本停不下来

文本自动摘要任务的“不完全”心得总结番外篇——submodular函数优化

Node2Vec 论文+代码笔记

模型压缩实践收尾篇——模型蒸馏以及其他一些技巧实践小结

中文命名实体识别工具(NER)哪家强?

学自然语言处理,其实更应该学好英语

斯坦福大学NLP组Python深度学习自然语言处理工具Stanza试用

太赞了!Springer面向公众开放电子书籍,附65本数学、编程、机器学习、深度学习、数据挖掘、数据科学等书籍链接及打包下载

数学之美中盛赞的 Michael Collins 教授,他的NLP课程要不要收藏?

自动作诗机&藏头诗生成器:五言、七言、绝句、律诗全了

这门斯坦福大学自然语言处理经典入门课,我放到B站了

关于AINLP

AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括文本摘要、智能问答、聊天机器人、机器翻译、自动生成、知识图谱、预训练模型、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLPer(id:ainlper),备注工作/研究方向+加群目的。

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

阅读至此了,点个在看吧 :point_down:

推荐阅读

超赞!百度词法分析工具 LAC 全面升级,2.0 版在线极速体验

这个NLP工具,玩得根本停不下来

征稿启示| 200元稿费+5000DBC(价值20个小时GPU算力)

文本自动摘要任务的“不完全”心得总结番外篇——submodular函数优化

Node2Vec 论文+代码笔记

模型压缩实践收尾篇——模型蒸馏以及其他一些技巧实践小结

中文命名实体识别工具(NER)哪家强?

学自然语言处理,其实更应该学好英语

斯坦福大学NLP组Python深度学习自然语言处理工具Stanza试用

太赞了!Springer面向公众开放电子书籍,附65本数学、编程、机器学习、深度学习、数据挖掘、数据科学等书籍链接及打包下载

数学之美中盛赞的 Michael Collins 教授,他的NLP课程要不要收藏?

自动作诗机&藏头诗生成器:五言、七言、绝句、律诗全了

这门斯坦福大学自然语言处理经典入门课,我放到B站了

关于AINLP

AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括文本摘要、智能问答、聊天机器人、机器翻译、自动生成、知识图谱、预训练模型、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLPer(id:ainlper),备注工作/研究方向+加群目的。

阅读至此了,点个在看吧 :point_down:

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

  • crazyladybug509
  • ticklishsnake
  • tinyfrog
  • silverleopard
  • McDonaldAldrich