第10篇 Fast AI深度学习课程——构建语言模型及文本情感分析

在学习了目标识别的网络构建与训练之后,我们总结一个模型的三元素为:数据、网络架构、损失函数。而采用的一般策略为迁移学习,即在已有的网络基础上,增加附加层;训练时首先冻结已有的网络的参数,训练附加层的系数;然后使用阶梯化的学习速率,训练整个网络。

在本节及下节课程中,我们将学习神经网络在自然语言处理方面的应用,包括构建语言模型、文本的情感分析、机器翻译等。在这一部分,我们所使用的技术策略同目标识别的一致:即迁移学习。

本节课程主要涉及训练语言模型和文本情感分析,依据从顶至底的学习路线,我们将深入到数据加载器、网络构造的具体实现中。

一、训练语言模型

在本系列课程的第一部分中,我们使用的工具包是torchtext,但该包未使用并行技术,对一些重复计算未使用缓存策略,因此其计算极其缓慢。而本节我们将使用fastai.text包,其是torchtextfastai.nlp的结合。

1. 数据准备

所使用的数据是IMDB的影评数据,下载方式是

curl -O http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz 

这个比wget方法要快很多。

IMDB训练数据共75000条,分为三类:negposunsupervisednegpos类各有12500条,剩余的50000条为unsupervised。测试数据有25000条,negpos类各12500条,无unsupervised类。

  • 数据格式统一化

    将数据整理成网络所需的统一的格式:即CSV文件,第一栏为标签,第二栏为文本,不存储索引,不存储列名。读取文本所用的函数是get_texts(),其接受一个路径作为变量,将该路径下的按类别存储的子路径下的文本,读取到数组中(每条文本作为一项),并将每条文本的类别序号拼接为数组。在存储前,对数据做随机化,使用如下语句生成随机序列作为样本重排的索引:

    np.random.seed(42)
    trn_idx = np.random.permutation(len(trn_texts))
    
    图 1. CSV文件格式,实际存储时无行序号,无列标签

    对语言模型,我们需要使用IMDB中的所有数据作为训练数据,并从中拆分出10%的测试数据,这一需求是通过sklearn包实现的。另外,训练语言模型时不需要标签,为满足通用的数据格式的要求,可以对文本数据设置任意标签。

        trn_texts,val_texts = sklearn.model_selection.train_test_split(
        np.concatenate([trn_texts,val_texts]), test_size=0.1)
    
  • 数据清洗与分词

    数据清洗主要是将html中的特殊字符替换成可读形式,如amp;替换为&;另外就是使用一个空格替换多个连续的空格、制表符等。使用的函数是fixup()

    分词时要在每条文本的开头添加文本起始符和文本域符号及序号:文本起始符选用的是样本中不常出现的字符;文本域符号及序号是针对那些包含标题和正文的文本准备的,以使数据格式足够灵活。分词使用的是spacy包,除了按照空格进行单词划分外,还会将诸如don't之类的缩写,进行拆分。课程中对spacy的分词功能进行了一层封装:做一些特殊处理,比如连续的大写单词用于强调,则转换后变为t_up标识符后接小写形式;连续出现的符号,转换后变为t_rep后接出现次数,再接该符号;等。对spacy的封装还实现了并行化处理,所用的函数为Tokenizer().proc_all_mp(),其接受使用partition_by_cores()函数将文本列表按照CPU的核数拆分成的子列表。

    在使用pandas进行文本读取时,使用关键字参数chunksize设置一次读取多少条数据,以避免内存溢出的现象:
    df_trn = pd.read_csv(LM_PATH/‘train.csv’, header=None, chunksize=chunksize)
    这样所得的df_trn是一个迭代器,将之传给get_all()函数,即可获得分词结果。(get_all()函数会调用get_texts()函数,这和读取原始数据时的get_texts()函数有冲突。)

  • 构建词库
    在获取分词结果后,使用python内置的Counter类对单词统计计数,选取出现频率最高的60000个词构造词库,并且每个词出现的次数不得低于一定的频次(当文本中的某个词出现频率太低时,训练过程中无法学到该词的有效信息,因此将之计入词库也是无用的)。同时添加特殊词_pad_,用于表示为使得一个批次的序列等长时添加的字符;添加特殊词_unk_,用于表示词库中找不到的词。最终得到词到数和数到词的两个字典:stoiitos

  • 构建符合网络输入要求的数据模型
    在这里,需要使用基于wiki103数据(一个基于Wikipedia文本构造的数据集,去除了一些过短或过长以及奇怪的文本)构建的英文语言模型。由于IMDBwiki103词库不同,需要做相应的对应。方法是使用wiki103itos参数(这个字典是和模型一起存储的),构建wiki103stoi字典;然后利用IMDBitos字典,查找每个词在wiki103的内嵌矩阵enc_wgts中的索引,构建IMDB的内嵌矩阵;如果未在wiki103中找到,则使用enc_wgts的均值代替。

    接着使用LanguageModelLoader类(定义位于fastai/text.py中)构造符合网络输入要求的数据加载器。数据加载器负责给数据模型投喂数据,因此,其最重要的特征就是需要满足python迭代器的要求,即具有__iter____len__方法。在LanguageModelLoader中,会将输入的分词流按bs拆分、对齐,然后在返回一个批次的数据时,对返回的字词流的长度bptt做随机化处理(按均值为bptt的高斯分布取值,并按照5%的概率将均值变为bptt/2)。这样随机化取值,可以在每个epoch开始时进行扰动,以使得每个epoch的投喂的数据有所差异。

    然后使用LanguageModelData类(定义位于fastai/text.py中)构造数据模型。

    trn_dl = LanguageModelLoader(np.concatenate(trn_lm), bs, bptt)
    val_dl = LanguageModelLoader(np.concatenate(val_lm), bs, bptt)
    md = LanguageModelData(PATH, 1, vs, trn_dl, val_dl, bs=bs, bptt=bptt)
    

    其中PATH用于指明路径,方便程序存储一些临时文件;1为填充字词(本例中为_pad_)的索引,在语言模型中可能用不到,但在情感分析、机器翻译等其他应用中会使用;vs为词库的长度。

2. 构建网络

采用迁移学习的策略,首先获取一个现成的语言模型。课程的讲授者已经基于wiki103数据构建了一个英文语言模型,下载方式为

wget -nH -r -np -P {PATH} http://files.fast.ai/models/wt103/

其为AWD LSTM网络,字词的内嵌矩阵为400维(em_sz参数),隐藏状态为1150维(nh参数),隐藏层层数为3(nl参数)。

通过数据模型mdget_model()函数,获取新的网络模型。

learner= md.get_model(opt_fn, em_sz, nh, nl, 
dropouti=drops[0], dropout=drops[1], wdrop=drops[2], dropoute=drops[3], dropouth=drops[4])

其中opt_fn为优化方法,本例中使用的是Adamem_sznhnl为加载wiki103时的参数;dropoutidropoutwdropdropoutedropouth分别为如下的丢弃率:输入层、RNN的权重矩阵、内嵌层、隐藏层。

LanguageModelData中的get_model()函数:

def get_model(self, opt_fn, emb_sz, n_hid, n_layers, **kwargs):
   m = get_language_model(self.n_tok, emb_sz, n_hid, n_layers, self.pad_idx, **kwargs)
   model = LanguageModel(to_gpu(m))
   return RNN_Learner(self, model, opt_fn=opt_fn)

get_model()中调用了get_language_model()函数,其定义如下:

def get_language_model(n_tok, emb_sz, nhid, nlayers, pad_token,
             dropout=0.4, dropouth=0.3, dropouti=0.5, dropoute=0.1, wdrop=0.5, tie_weights=True, qrnn=False, bias=False):

    rnn_enc = RNN_Encoder(n_tok, emb_sz, nhid=nhid, nlayers=nlayers, pad_token=pad_token,
                dropouth=dropouth, dropouti=dropouti, dropoute=dropoute, wdrop=wdrop, qrnn=qrnn)
    enc = rnn_enc.encoder if tie_weights else None
    return SequentialRNN(rnn_enc, LinearDecoder(n_tok, emb_sz, dropout, tie_encoder=enc, bias=bias))

get_language_model()函数中,首先获取了一个RNN_Encoder对象。一个RNN_Encoder实际上就是加入了许多Dropout机制的RNN网络。然后在RNN_Encoder对象上添加LinearDecoder头,该LinearDecoder对象实际为一个线性层外加一个Dropout层。

get_model()函数会将get_language_model()返回的模型,封装成一个LanguageModel类,该类派生自BasicModel。由于一些网络类所需的方法都已在BasicModel中定义,LanguageModel类仅需定义一个get_layer_groups()方法,用于阶梯化学习速率。

3. 设置损失函数

损失函数的设置也是在get_model()函数中完成的,在LanguageModel的基础上,get_model()会将之进一步封装为RNN_Learner对象。RNN_Learner类派生自Learner,其crit属性即标识了损失函数,若未设置,在通过_get_crit()函数获取;RNN_Learner中的_get_crit()函数即返回的交互熵函数。

4. 迁移训练

通过learner.model.load_state_dict()函数加载wiki103模型的参数,然后进行迁移训练即可。

二、文本分类器

在获取语言模型之后(实际上仅需RNN_Encoder部分,不需要后面的LinearDecoder部分),就可按照迁移学习的步骤,进行情感分类。

1. 数据整理

构造用于文本分类网络的数据集:

trn_ds = TextDataset(trn_clas, trn_labels)
val_ds = TextDataset(val_clas, val_labels)

其中TextDataset派生自PytorchDatasetDataset是一个抽象的尅索引类,即具有__getitem__()__len__();同时提供了用于两个数据集进行拼接的函数__add__(),定义如下:

class Dataset(object):
    def __getitem__(self, index):
        raise NotImplementedError
    def __len__(self):
        raise NotImplementedError
    def __add__(self, other):
        return ConcatDataset([self, other])

TextDataset实现了Dataset__getitem__()__len__()

利用数据集构建数据加载器:

trn_dl = DataLoader(trn_ds, bs//2, transpose=True, num_workers=1, pad_idx=1, sampler=trn_samp)
val_dl = DataLoader(val_ds, bs, transpose=True, num_workers=1, pad_idx=1, sampler=val_samp)

数据加载器是一个迭代器,负责每次提供一个批次的数据。而对于用于分类的文本文件,其还会将一个批次的文本进行填充对齐,填充的字符的索引由pad_idx参数限定。通常,使用参数shuffle标识返回的数据是否需要随机化。而对文本数据,若一个批次的文本长度相差很大,对齐操作会导致空间的浪费。因此需要对文本按长度排序(对训练数据需要进行轻微随机化),这是通过使用sampler参数指定抽样器实现的。

trn_samp = SortishSampler(trn_clas, key=lambda x: len(trn_clas[x]), bs=bs//2)
val_samp = SortSampler(val_clas, key=lambda x: len(val_clas[x]))
2. 构建网络

使用get_rnn_classifier()函数构建分类网络。

m = get_rnn_classifier(bptt, 20*70, c, vs, emb_sz=em_sz, n_hid=nh, n_layers=nl, pad_token=1,
          layers=[em_sz*3, 50, c], drops=[dps[4], 0.1],
          dropouti=dps[0], wdrop=dps[1], dropoute=dps[2], dropouth=dps[3])
learn = RNN_Learner(md, TextModel(to_gpu(m)), opt_fn=opt_fn)

其中layers参数指明了在RNN_Encoder上所添加的各层的节点数,其中第一层设为em_sz*3是由于对RNN_Encoder的输出,分别做了平均池化和最大池化,然后与原输出进行了拼接;drops指明了各层之间的Dropout率。

损失函数和训练过程就没什么好说的了。

三、一些补充

1. 倒读文本

如果把文本翻转,倒着读文本,可得到另一个分类器,然后综合两个分类器的结果,可得到一个准确率更高的结果。此时注意将wiki103的模型路径从fwd_wt103.h5改为bwd_wt103.h5

2. 学习速率的坡形变化

learner.fit()中,设置use_clr参数,可实现学习速率的坡形变化。其第一个数值为学习速率的最大值与最小值之间的比,第二个数值为学习速率的峰值位置下降周期与上升周期的比。

3. 使用VNC,可视化操作远程服务器

在服务器上安装X Windows (xorg)、Lightweight window manager (lxde-core)、VNC server (tightvncserver)、Firefox (firefox)、Terminal (lxterminal)、Some fonts (xfonts-100dpi)。然后启动tightvncserver :13 -geometry 1200x900。在客户端使用TightVNC Viewer一类的工具,即可可视化操作远程服务器。

4. 使用Fire生成带命令行参数的脚本
5. 使用软连接将Fast.AI代码包链接到pythonsite-packages文件夹下(或在使用pip install -e .)
图 2.使用VNC可视化操作远程服务器

一些有用的链接

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值