Press "Enter" to skip to content

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

原创 · 作者 | 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要略胜一筹。

 

 

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

 

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

 

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

 

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

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

Content

2.学好 numpy

 

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_ _”两个方法即可。

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

 

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 张量转换

 

模块对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有没有越界!比如输入数据到 层,对应的索引范围应该是0-9,如果输入1-10就会报错。

 

3.zip argument #1 must support iteration rror

这个错误是我在使用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

 

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

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注