在学习了目标识别的网络构建与训练之后,我们总结一个模型的三元素为:数据、网络架构、损失函数。而采用的一般策略为迁移学习,即在已有的网络基础上,增加附加层;训练时首先冻结已有的网络的参数,训练附加层的系数;然后使用阶梯化的学习速率,训练整个网络。
在本节及下节课程中,我们将学习神经网络在自然语言处理方面的应用,包括构建语言模型、文本的情感分析、机器翻译等。在这一部分,我们所使用的技术策略同目标识别的一致:即迁移学习。
本节课程主要涉及训练语言模型和文本情感分析,依据从顶至底的学习路线,我们将深入到数据加载器、网络构造的具体实现中。
一、训练语言模型
在本系列课程的第一部分中,我们使用的工具包是torchtext
,但该包未使用并行技术,对一些重复计算未使用缓存策略,因此其计算极其缓慢。而本节我们将使用fastai.text
包,其是torchtext
和fastai.nlp
的结合。
1. 数据准备
所使用的数据是IMDB
的影评数据,下载方式是
curl -O http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
这个比wget
方法要快很多。
IMDB
训练数据共75000
条,分为三类:neg
,pos
,unsupervised
。neg
和pos
类各有12500
条,剩余的50000
条为unsupervised
。测试数据有25000
条,neg
和pos
类各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_
,用于表示词库中找不到的词。最终得到词到数和数到词的两个字典:stoi
和itos
。 -
构建符合网络输入要求的数据模型
在这里,需要使用基于wiki103
数据(一个基于Wikipedia
文本构造的数据集,去除了一些过短或过长以及奇怪的文本)构建的英文语言模型。由于IMDB
和wiki103
词库不同,需要做相应的对应。方法是使用wiki103
的itos
参数(这个字典是和模型一起存储的),构建wiki103
的stoi
字典;然后利用IMDB
的itos
字典,查找每个词在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
参数)。
通过数据模型md
的get_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
为优化方法,本例中使用的是Adam
;em_sz
、nh
、nl
为加载wiki103
时的参数;dropouti
、dropout
、wdrop
、dropoute
、dropouth
分别为如下的丢弃率:输入层、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
派生自Pytorch
的Dataset
。Dataset
是一个抽象的尅索引类,即具有__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
代码包链接到python
的site-packages
文件夹下(或在使用pip install -e .
)

一些有用的链接
- 课程wiki: 本节课程的一些相关资源,包括课程笔记、课上提到的博客地址等。
- AsciiDoc:
adoc
的官方网址,其中adoc
为Fast.AI
项目的文档所使用的标记语言。 - AWD LSTM:
LSTM
论文。 - AWD LSTM:
LSTM
的实现。 - 一篇关于学习速率的必读文章。