文章目录
文本预处理
Tokenization
使用 fastai 进行词标记化
子词分词
用 fasti 进行数值化
将我们的文本放入语言模型的批次中
训练文本分类器
使用 DataBlock 的语言模型
微调语言模型
保存和加载模型
文本生成
创建分类器数据加载器
微调分类器
虚假信息和语言模型
结论
在第 1 章中,我们看到深度学习可用于在自然语言数据集上获得很好的结果。我们的示例依赖于使用预训练语言模型并对其进行微调以对评论进行分类。该示例突出了 NLP 中的迁移学习与计算机视觉之间的区别:通常,在 NLP 中,预训练模型是针对不同的任务进行训练的。
我们所说的语言模型是一种经过训练可以猜测的模型文本中的下一个单词(已经阅读过之前的单词)。这种任务被称为自监督学习:我们不需要给我们的模型贴上标签,只需要给它提供很多很多的文本。它有一个自动从数据中获取标签的过程,这个任务并不简单:要正确猜测句子中的下一个单词,模型必须理解英语(或其他)语言。自我监督学习也可以用于其他领域;例如,见 “自我监督学习和计算机视觉”,介绍视觉应用。自监督学习通常不用于以下模型
自监督学习
使用嵌入在独立模型中的标签训练模型变量,而不是需要外部标签。例如,训练一个模型来预测文本中的下一个单词。
我们在第 1 章中用于对 IMDb 进行分类的语言模型 评论是在维基百科上预训练的。通过直接将这种语言模型微调到电影评论分类器,我们获得了很好的结果,但通过一个额外的步骤,我们可以做得更好。维基百科英语与 IMDb 英语略有不同,因此我们可以将预训练语言模型微调到 IMDb 语料库,然后将其用作分类器的基础,而不是直接跳转到分类器。
即使我们的语言模型知道我们在任务中使用的语言的基础知识(例如,我们的预训练模型是英语),它也有助于适应我们所针对的语料库的风格。可能更多
我们已经看到,使用 fastai,我们可以下载一个预训练的英语语言模型,并用它来获得 NLP 的最先进结果
当然,原因之一是了解您正在使用的模型的基础很有帮助。但是还有另一个非常实际的原因,那就是如果在微调分类模型之前微调(基于序列的)语言模型,您会得到更好的结果。例如,对于 IMDb 情绪分析任务,数据集包括 50,000 条额外的电影评论,这些评论没有附加任何正面或负面标签。由于训练集中有 25,000 条带标签的评论,验证集中有 25,000 条,因此总共有 100,000 条电影评论。我们可以使用所有这些评论来微调仅在维基百科文章上训练的预训练语言模型;这将产生一个特别擅长预测电影评论的下一个词的语言模型。
这被称为通用语言模型微调 (ULMFiT) 方法。介绍它的论文表明,这 在将学习迁移到分类任务之前,对语言模型进行微调的额外阶段会产生更好的预测。使用这种方法,我们将 NLP 中的迁移学习分为三个阶段,如图 10-1 所示。
图 10-1。ULMFiT 过程
现在,我们将使用前两章中介绍的概念,探索如何将神经网络应用于此语言建模问题。但在进一步阅读之前,停下来想想你将如何处理这个问题。
文本预处理
我们将如何使用它一点也不明显
我们已经了解了如何将分类变量用作神经网络的自变量。这是我们对单个分类变量采用的方法:
- 列出该分类变量的所有可能级别(我们将此列表称为vocab)。
- 用词汇表中的索引替换每个级别。
- 为此创建一个嵌入矩阵,每个级别包含一行(即,词汇表的每个项目)。
- 使用此嵌入矩阵作为神经网络的第一层。(专用嵌入矩阵可以将步骤 2 中创建的原始词汇索引作为输入;这等同于将表示索引的单热编码向量作为输入的矩阵,但速度更快,效率更高。)
我们可以对文本做几乎相同的事情!新的是序列的概念。首先,我们将数据集中的所有文档连接成一个很大的长字符串,并将其拆分为单词(或标记),从而得到一个非常长的单词列表。我们的自变量将是从我们非常长的列表中的第一个单词开始到倒数第二个单词结束的单词序列,而我们的因变量将是从第二个单词开始到最后一个单词结束的单词序列。
我们的词汇表将由预训练模型词汇表中已有的常用词和语料库特有的新词(例如电影术语或演员姓名)组成。我们的嵌入矩阵将相应地构建:对于我们预训练模型的词汇表中的单词,我们将在预训练模型的嵌入矩阵中取相应的行;但是对于新词,我们什么都没有,所以我们只用一个随机向量初始化相应的行。
创建语言模型所需的每个步骤都有来自自然语言处理领域的行话,以及可提供帮助的 fastai 和 PyTorch 类。步骤如下:
Tokenization
将文本转换为单词列表(或字符或子字符串,具体取决于模型的粒度)。
数值化
列出所有出现的唯一单词(词汇表),并通过在词汇表中查找其索引将每个单词转换为数字。
语言模型数据加载器创建
fastai 提供了一个LMDataLoader
类,它自动处理创建一个从自变量偏移一个标记的因变量。它还处理一些重要的细节,例如如何以因变量和自变量按要求保持其结构的方式对训练数据进行洗牌。
语言模型创建
我们需要一种特殊的模型来完成我们以前从未见过的事情:处理可以任意大或小的输入列表。有很多方法可以做到这一点;在本章中,我们将使用递归神经网络(RNN)。我们将在第 12 章详细介绍 RNN ,但现在,您可以将其视为另一个深度神经网络。
让我们详细了解每个步骤的工作原理。
Tokenization
当我们说“将文本转换为单词列表”时,我们遗漏了很多细节。例如,我们如何处理标点符号?我们如何处理 用“不要”这样的词?是一两个字吗?长的医学或化学词呢?是否应该将它们拆分成各自独立的意义片段?带连字符的单词怎么样?像德语和波兰语这样的语言呢,它们可以从很多很多的片段中创造出非常长的单词?像日语和汉语这样根本不使用词基,也没有真正明确定义单词概念的语言呢?
因为这些问题没有一个正确答案,所以没有一种方法可以进行标记化。主要有以下三种方法:
基于单词
在空格上拆分句子,以及应用特定语言的规则来尝试分隔含义的各个部分,即使没有空格(例如将“不要”变成“不要”)。通常,标点符号也被拆分成单独的标记。
基于子词
根据最常出现的子字符串将单词拆分成更小的部分。例如,“场合”可能被标记为“场合”。
基于字符
将一个句子拆分成各个字符。
我们将在这里查看单词和子词标记化,我们将把基于字符的标记化留给您在本章末尾的调查问卷中实施。
Token
标记化过程创建的列表的一个元素。它可以是一个词、一个词的一部分(子词)或单个字符。
使用 fastai 进行词标记化
fastai 没有提供自己的分词器,而是提供了一个
让我们用我们在 第 1 章中使用的 IMDb 数据集来尝试一下:
from fastai.text.all import *
path = untar_data(URLs.IMDB)
我们需要获取文本文件以试用分词器。就像get_image_files
(我们已经用过很多次)一样,获取一个路径中的所有图像文件,get_text_files
获取一个路径中的所有文本文件。我们还可以选择传递 folders
以将搜索限制在特定的子文件夹列表中:
files = get_text_files(path, folders = ['train', 'test', 'unsup'])
这是我们将标记化的评论(我们将在此处打印它的开头以节省空间):
txt = files[0].open().read(); txt[:75]
'This movie, which I just discovered at the video store, has apparently sit '
在我们撰写本书时,fastai 的默认英文单词分词器使用了一个名为spaCy的库。它有一个复杂的规则引擎,具有针对 URL、个别特殊英语单词等的特殊规则。SpacyTokenizer
然而,我们不会直接使用,而是使用WordTokenizer
,因为这将始终指向 fastai 当前的默认单词分词器(可能不一定是 spaCy,具体取决于您阅读本文的时间)。
让我们试试看。我们将使用 fastai 的coll_repr(collection,n)
功能来显示结果。这将显示collection
的第一个项目以及完整尺寸——这是默认使用的L
。请注意,fastai 的分词器采用一组文档进行分词,因此我们必须将其包装txt
在一个列表中:
spacy = WordTokenizer()
toks = first(spacy([txt]))
print(coll_repr(toks, 30))
(#201) ['This','movie',',','which','I','just','discovered','at','the','video','s
> tore',',','has','apparently','sit','around','for','a','couple','of','years','
> without','a','distributor','.','It',"'s",'easy','to','see'...]
如您所见,spaCy 主要只是分离出了单词和标点符号。但它在这里也做了其他事情:它将“it's”拆分为“it”和“'s”。这是直觉上的道理;这些是单独的词,真的。当您考虑必须处理的所有小细节时,令牌化是一项非常微妙的任务。幸运的是,spaCy 为我们很好地处理了这些——例如,在这里我们看到了“.”。当它终止一个句子时被分开,但不是首字母缩写词或数字:
first(spacy(['The U.S. dollar $1 is $1.00.']))
(#9) ['The','US','dollar','$','1','is','$','1.00','.']
fastai 然后使用Tokenizer
类向标记化过程添加一些额外的功能:
tkn = Tokenizer(spacy)
print(coll_repr(tkn(txt), 31))
(#228) ['xxbos','xxmaj','this','movie',',','which','i','just','discovered','at',
> 'the','video','store',',','has','apparently','sit','around','for','a','couple
> ','of','years','without','a','distributor','.','xxmaj','it',"'s",'easy'...]
请注意,现在有一些以字符“xx”开头的标记,这在英语中不是常见的单词前缀。这些是特殊标记。
例如,列表中的第一项xxbos
是一个特殊标记,表示新文本的开始(“BOS”是标准的 NLP 首字母缩写词,意思是“流的开始”)。通过识别这个开始标记,模型将能够学习它需要“忘记”之前说过的话,并专注于即将到来的单词。
这些特殊标记并非直接来自 spaCy。它们在那里是因为 fastai 默认添加它们,通过在处理文本时应用许多规则。这些规则旨在使模型更容易识别句子的重要部分。从某种意义上说,我们正在将原始英语语言序列翻译成一种简化的标记化语言——一种旨在让模型易于学习的语言。
例如,规则将用一个特殊的重复字符标记替换一系列的四个感叹号,然后是数字四,然后是一个感叹号。通过这种方式,模型的嵌入矩阵可以编码有关一般概念的信息,例如重复的标点符号,而不是为每个标点符号的每个重复次数都需要一个单独的标记。类似地,大写单词将被替换为特殊的大写标记,然后是该单词的小写版本。这样,嵌入矩阵只需要单词的小写版本,节省了计算和内存资源,但仍然可以学习大写的概念。
以下是您将看到的一些主要特殊标记:
xxbos
指示文本的开头(此处为评论)
xxmaj
指示下一个单词以大写字母开头(因为我们将所有内容都小写了)
xxunk
表示这个词是未知的
要查看使用的规则,您可以检查默认规则:
defaults.text_proc_rules
[<function fastai.text.core.fix_html(x)>,
<function fastai.text.core.replace_rep(t)>,
<function fastai.text.core.replace_wrep(t)>,
<function fastai.text.core.spec_add_spaces(t)>,
<function fastai.text.core.rm_useless_spaces(t)>,
<function fastai.text.core.replace_all_caps(t)>,
<function fastai.text.core.replace_maj(t)>,
<function fastai.text.core.lowercase(t, add_bos=True, add_eos=False)>]
与往常一样,您可以通过键入以下内容在笔记本中查看它们每个的源代码:
??replace_rep
以下是每个功能的简要总结:
fix_html
用可读版本替换特殊的 HTML 字符(IMDb 评论中有很多)
replace_rep
将任何重复三次或更多次的字符替换为用于重复的特殊标记 ( xxrep
)、重复次数,然后是字符
replace_wrep
将任何重复三次或更多次的单词替换为单词重复的特殊标记 ( xxwrep
)、重复次数,然后是单词
spec_add_spaces
在 / 和 # 周围添加空格
rm_useless_spaces
删除所有重复的空格字符
replace_all_caps
将全部大写的单词小写,并xxup
在其前面为所有大写添加一个特殊标记 ( )
replace_maj
xxmaj
将大写单词小写并在其前面 添加大写 ( ) 的特殊标记
lowercase
将所有文本小写并在开头 ( xxbos
) 和/或结尾 ( xxeos
)添加特殊标记
让我们看一下其中的几个:
coll_repr(tkn('© Fast.ai www.fast.ai/INDEX'), 31)
"(#11) ['xxbos','©','xxmaj','fast.ai','xxrep','3','w','.fast.ai','/','xxup','ind > ex'...]"
现在让我们来看看子词标记化是如何工作的。
子词分词
除了上一节中看到的单词标记化方法之外,另一种流行的标记化方法是子词标记化。 单词标记化依赖于空格提供有用的假设
要处理这些情况,通常最好使用子词标记化。这分两步进行:
- 分析文档语料库以找到最常出现的字母组。这些成为词汇。
- 使用这个子词单元的词汇对语料库进行标记。
让我们看一个例子。对于我们的语料库,我们将使用前 2,000 条电影评论:
txts = L(o.open().read() for o in files[:2000])
我们实例化我们的分词器,传入我们想要创建的词汇的大小,然后我们需要“训练”它。也就是说,我们需要让它阅读我们的文档并找到常见的字符序列来创建词汇表。这是用setup
. 正如我们很快就会看到的,setup
是一种特殊的 fastai 方法,它会在我们通常的数据处理管道中自动调用。但是,由于我们目前是手动完成所有操作,因此我们必须自己调用它。下面是一个针对给定词汇量执行这些步骤并显示示例输出的函数:
def subword(sz):
sp = SubwordTokenizer(vocab_sz=sz)
sp.setup(txts)
return ' '.join(first(sp([txt]))[:40])
让我们试试看:
subword(1000)
'▁This ▁movie , ▁which ▁I ▁just ▁dis c over ed ▁at ▁the ▁video ▁st or e , ▁has > ▁a p par ent ly ▁s it ▁around ▁for ▁a ▁couple ▁of ▁years ▁without ▁a ▁dis t > ri but or . ▁It'
使用 fastai 的子词分词器时,特殊字符 ▁
表示原始文本中的空格字符。
如果我们使用更小的词汇表,每个标记将代表更少的字符,并且将需要更多的标记来表示一个句子:
subword(200)
'▁ T h i s ▁movie , ▁w h i ch ▁I ▁ j us t ▁ d i s c o ver ed ▁a t ▁the ▁ v id e > o ▁ st or e , ▁h a s'
另一方面,如果我们使用更大的词汇表,最常见的英语单词将最终出现在词汇表中,我们就不需要那么多来表示一个句子:
subword(10000)
"▁This ▁movie , ▁which ▁I ▁just ▁discover ed ▁at ▁the ▁video ▁store , ▁has > ▁apparently ▁sit ▁around ▁for ▁a ▁couple ▁of ▁years ▁without ▁a ▁distributor > . ▁It ' s ▁easy ▁to ▁see ▁why . ▁The ▁story ▁of ▁two ▁friends ▁living"
选择子词词汇量大小代表了一种妥协:更大的词汇量意味着每个句子更少的标记,这意味着更快的训练、更少的内存和更少的模型要记住的状态;但不利的一面是,这意味着更大的嵌入矩阵,需要更多的数据来学习。
总的来说,子词标记化提供了一种在字符标记化(即使用小的子词词汇)和单词标记化(即使用大的子词词汇)之间轻松缩放的方法,并且无需开发特定于语言的算法即可处理每种人类语言。它甚至可以处理其他“语言”,例如基因组序列或 MIDI 音乐符号!出于这个原因,在去年它的 受欢迎程度飙升,它似乎有可能成为最常见的标记化方法(当你读到这篇文章时,它很可能已经是!)。
一旦我们的文本被分割成标记,我们需要将它们转换为数字。我们接下来看看。
用 fasti 进行数值化
数值化是将标记映射到整数的过程。 这些步骤与创建变量所需的步骤基本相同 Category
,例如 MNIST 中数字的因变量:
- 列出该分类变量(词汇)的所有可能级别。
- 用词汇表中的索引替换每个级别。
让我们看一下我们之前看到的单词标记化文本的实际效果:
toks = tkn(txt)
print(coll_repr(tkn(txt), 31))
(#228) ['xxbos','xxmaj','this','movie',',','which','i','just','discovered','at',
> 'the','video','store',',','has','apparently','sit','around','for','a','couple
> ','of','years','without','a','distributor','.','xxmaj','it',"'s",'easy'...]
与 一样SubwordTokenizer
,我们需要setup
呼吁Numericalize
; 这就是我们创建词汇的方式。这意味着我们首先需要我们的标记化语料库。由于标记化需要一段时间,因此由 fastai 并行完成;但对于本手动演练,我们将使用一小部分:
toks200 = txts[:200].map(tkn)
toks200[0]
(#228)
> ['xxbos','xxmaj','this','movie',',','which','i','just','discovered','at'...]
我们可以将其传递给setup
来创建我们的词汇表:
num = Numericalize()
num.setup(toks200)
coll_repr(num.vocab,20)
"(#2000) ['xxunk','xxpad','xxbos','xxeos','xxfld','xxrep','xxwrep','xxup','xxmaj
> ','the','.',',','a','and','of','to','is','in','i','it'...] “
我们的特殊规则标记首先出现,然后每个单词按频率顺序出现一次。默认Numericalize
为 min_freq=3
和max_vocab=60000
。max_vocab=60000
导致 fastai 将除最常见的 60,000 之外的所有单词替换为特殊的 未知单词标记,xxunk
。这对于避免拥有过大的嵌入矩阵很有用,因为这会减慢训练速度并占用过多内存,并且还可能意味着没有足够的数据来为稀有词训练有用 的表示。但是,最后一个问题最好通过设置min_freq
;来处理。默认值min_freq=3
意味着任何出现次数少于 3 次的单词都将替换为xxunk
.
fastai 还可以通过将单词列表作为vocab
参数传递,使用您提供的词汇对您的数据集进行数值化。
一旦我们创建了我们的Numericalize
对象,我们就可以像使用函数一样使用它:
nums = num(toks)[:20]; nums
tensor([ 2, 8, 21, 28, 11, 90, 18, 59, 0, 45, 9, 351, 499, 11, > 72, 533, 584, 146, 29, 12])
这一次,我们的令牌已经转换为我们的模型可以接收的整数张量。我们可以检查它们是否映射回原始文本:
' '.join(num.vocab[o] for o in nums)
'xxbos xxmaj this movie , which i just xxunk at the video store , has apparently > sit around for a'
现在我们有了数字,我们需要将它们分批放入我们的模型中。
将我们的文本放入语言模型的批次中
在处理图像时,我们需要将它们全部调整为相同的大小
假设我们有以下文本:
在本章中,我们将回顾我们在第 1 章中研究过的电影评论分类示例,并深入挖掘。首先,我们将了解将文本转换为数字所需的处理步骤以及如何对其进行自定义。通过这样做,我们将有另一个在数据块 API 中使用的预处理器示例。
然后我们将研究如何构建语言模型并对其进行一段时间的训练。
标记化过程将添加特殊标记并处理标点符号以返回此文本:
xxbos xxmaj 在本章中,我们将回顾我们在第 1 章中研究过的电影评论分类示例,并深入挖掘。xxmaj 首先我们将了解将文本转换为数字所需的处理步骤以及如何对其进行自定义。xxmaj 通过这样做,我们将得到数据块 xxup api 中使用的预处理器的另一个示例。\n xxmaj 然后我们会研究我们如何构建语言模型并训练一段时间。
我们现在有 90 个标记,用空格分隔。假设我们想要一个 6 的批量大小。我们需要将此文本分成 6 个长度为 15 的连续部分:
xxbos | xxmaj | in | this | chapter | , | we | will | go | back | over | the | example | of | classifying |
movie | reviews | we | studied | in | chapter | 1 | and | dig | deeper | under | the | surface | . | xxmaj |
first | we | will | look | at | the | processing | steps | necessary | to | convert | text | into | numbers | and |
how | to | customize | it | . | xxmaj | by | doing | this | , | we | ‘ll | have | another | example |
of | the | preprocessor | used | in | the | data | block | xxup | api | . | \n | xxmaj | then | we |
will | study | how | we | build | a | language | model | and | train | it | for | a | while | . |
在一个完美的世界中,我们可以将这一批提供给我们的模型。但这种方法无法扩展,因为在这个玩具示例之外,包含所有标记的单个批次不太可能适合我们的 GPU 内存(这里我们有 90 个标记,但所有 IMDb 评论加起来有几百万)。
因此,我们需要将这个数组更细地划分为固定序列长度的子数组。保持这些子阵列内部和跨这些子阵列的顺序很重要,因为我们将使用一个保持状态的模型,以便它在预测接下来发生的事情时记住之前读取的内容。
回到我们之前的例子,有 6 个批次,长度为 15,如果我们选择一个长度为 5 的序列,这意味着我们首先提供以下数组:
xxbos | xxmaj | in | this | chapter |
movie | reviews | we | studied | in |
first | we | will | look | at |
how | to | customize | it | . |
of | the | preprocessor | used | in |
will | study | how | we | build |
然后,这个:
, | we | will | go | back |
chapter | 1 | and | dig | deeper |
the | processing | steps | necessary | to |
xxmaj | by | doing | this | , |
the | data | block | xxup | api |
a | language | model | and | train |
最后:
over | the | example | of | classifying |
under | the | surface | . | xxmaj |
convert | text | into | numbers | and |
we | ‘ll | have | another | example |
. | \n | xxmaj | then | we |
it | for | a | while | . |
回到我们的电影评论数据集,第一步是通过将单个文本连接在一起将它们转换为流。与图像一样,最好随机化输入的顺序,因此在每个纪元开始时,我们将打乱条目以创建一个新流(我们打乱文档的顺序,而不是其中单词的顺序,或者文本将不再有意义!)。
然后我们将这个流切割成一定数量的连续文本块(这是我们的批量大小)。例如,如果流有 50,000 个标记,我们将批量大小设置为 10,这将为我们提供 10 个 5,000 个标记的迷你流。重要的是我们保留标记的顺序(因此第一个迷你流从 1 到 5,000,然后从 5,001 到 10,000……),因为我们希望模型读取连续的文本行(如前例所示) ). 在预处理期间,在每个文本的开头添加一个xxbos
标记,以便模型知道它何时在新条目开始时读取流。
因此,回顾一下,在每个时期,我们都会洗牌我们的文档集合并将它们连接成一个标记流。然后我们将该流切割成一批固定大小的连续迷你流。然后,我们的模型将按顺序读取迷你流,并且由于内部状态,无论我们选择的序列长度如何,它都会产生相同的激活。
当我们创建一个 LMDataLoader
. 我们通过首先将我们的Numericalize
对象应用于标记化文本来做到这一点
nums200 = toks200.map(num)
然后将其传递给LMDataLoader
:
dl = LMDataLoader(nums200)
让我们通过抓取第一批来确认这给出了预期的结果
x,y = first(dl)
x.shape,y.shape
(torch.Size([64, 72]), torch.Size([64, 72]))
然后查看自变量的第一行,它应该是第一个文本的开头:
' '.join(num.vocab[o] for o in x[0][:20])
'xxbos xxmaj this movie , which i just xxunk at the video store , has apparently > sit around for a'
因变量是相同的东西,但偏移了一个标记:
' '.join(num.vocab[o] for o in y[0][:20])
'xxmaj this movie , which i just xxunk at the video store , has apparently sit > around for a couple'
这结束了我们需要应用于我们的数据的所有预处理步骤。
训练文本分类器
正如我们在本章开头所见,训练一个
像往常一样,让我们从组装数据开始。
使用 DataBlock 的语言模型
fastai 在 TextBlock
传递给时自动处理标记化和数字化DataBlock
。所有的论点都可以 传递给Tokenizer
也 Numericalize
可以传递给 TextBlock
. 在下一章中,我们将讨论分别运行这些步骤的最简单方法,以简化调试,但您始终可以通过在数据子集上手动运行它们来进行调试,如前几节所示。并且不要忘记 DataBlock
的 handysummary
方法,它非常有用
以下是我们如何TextBlock
使用 fastai 的默认设置来创建语言模型:
get_imdb = partial(get_text_files, folders=['train', 'test', 'unsup'])
dls_lm = DataBlock(
blocks=TextBlock.from_folder(path, is_lm=True),
get_items=get_imdb, splitter=RandomSplitter(0.1)
).dataloaders(path, path=path, bs=128, seq_len=80)
与我们之前使用的类型不同的一件事 DataBlock
是,我们不只是直接使用类(即,TextBlock(...)
而是调用类方法。A 类方法是一种Python方法,顾名思义,属于类而不是对象。(如果您不熟悉类方法,请务必在线搜索有关类方法的更多信息,因为它们在许多 Python 库和应用程序中很常用;我们之前在本书中使用过几次,但现在还没有没有提醒他们注意。)TextBlock
特别的原因是设置数值器的词汇表可能需要很长时间(我们必须阅读并标记每个文档以获得词汇表)。
为了尽可能高效,fastai 进行了一些优化:
- 它将标记化的文档保存在一个临时文件夹中,因此不必对它们进行多次标记化。
- 它并行运行多个标记化进程,以利用您计算机的 CPU。
我们需要告诉TextBlock
如何访问文本,以便它可以进行初始预处理——这就是 from_folder
它所做的。
show_batch
然后以通常的方式工作:
dls_lm.show_batch(max_n=2)
text | text_ | |
0 | xxbos xxmaj it ’s awesome ! xxmaj in xxmaj story xxmaj mode , your going from punk to pro . xxmaj you have to complete goals that involve skating , driving , and walking . xxmaj you create your own skater and give it a name , and you can make it look stupid or realistic . xxmaj you are with your friend xxmaj eric throughout the game until he betrays you and gets you kicked off of the skateboard | xxmaj it ’s awesome ! xxmaj in xxmaj story xxmaj mode , your going from punk to pro . xxmaj you have to complete goals that involve skating , driving , and walking . xxmaj you create your own skater and give it a name , and you can make it look stupid or realistic . xxmaj you are with your friend xxmaj eric throughout the game until he betrays you and gets you kicked off of the skateboard xxunk |
1 | what xxmaj i ‘ve read , xxmaj death xxmaj bed is based on an actual dream , xxmaj george xxmaj barry , the director , successfully transferred dream to film , only a genius could accomplish such a task . \n\n xxmaj old mansions make for good quality horror , as do portraits , not sure what to make of the killer bed with its killer yellow liquid , quite a bizarre dream , indeed . xxmaj also , this | xxmaj i ‘ve read , xxmaj death xxmaj bed is based on an actual dream , xxmaj george xxmaj barry , the director , successfully transferred dream to film , only a genius could accomplish such a task . \n\n xxmaj old mansions make for good quality horror , as do portraits , not sure what to make of the killer bed with its killer yellow liquid , quite a bizarre dream , indeed . xxmaj also , this is |
现在我们的数据已准备就绪,我们可以微调预训练的语言模型。
微调语言模型
为了将整数词索引转换为可用于我们的神经网络的激活,我们将使用嵌入,就像我们对 协同过滤和表格建模。然后我们将使用一种名为 AWD-LSTM (我们将在第 12 章向您展示如何从头开始编写这样的模型 )。正如我们之前讨论的,预训练模型中的嵌入与为不在预训练词汇表中的单词添加的随机嵌入合并。这是在内部自动处理的 language_model_learner
:
learn = language_model_learner(
dls_lm, AWD_LSTM, drop_mult=0.3,
metrics=[accuracy, Perplexity()]).to_fp16()
默认使用的损失函数是交叉熵损失,因为我们本质上有一个分类问题(不同的类别是我们词汇中的单词)。这里使用的困惑度度量在 NLP 中经常用于语言模型:它是损失的指数(即 torch.exp(cross_entropy)
)。我们还包括准确性指标,以查看我们的模型在尝试预测下一个单词时正确的次数,因为交叉熵(正如我们所见)既难以解释又告诉我们更多关于模型的信心而不是其准确性。
让我们回到本章开头的流程图。第一个箭头已经为我们完成并在 fastai 中作为预训练模型提供,我们刚刚为第二阶段构建了DataLoaders
和。Learner
现在我们准备好微调我们的语言模型了!
训练每个 epoch 需要相当长的时间,因此我们将在训练过程中保存中间模型结果。由于 fine_tune
不为我们这样做,我们将使用fit_one_cycle
. 就像cnn_learner
,在使用预训练模型(这是默认设置)时language_model_learner
自动调用一样freeze
,所以这将只训练嵌入(模型中唯一包含随机初始化权重的部分——即,嵌入我们 IMDb 词汇表中的单词,但不在预训练模型词汇中):
learn.fit_one_cycle(1, 2e-2)
epoch | train_loss | vaild_loss | accuracy | perplexity | time |
0 | 4.120048 | 3.912788 | 0.299565 | 50.038246 | 11:39 |
这个模型需要一段时间来训练,所以这是一个谈论保存中间结果的好机会。
保存和加载模型
您可以像这样轻松保存模型的状态:
learn.save('1epoch')
这将在learn.path/models/中创建一个名为1epoch.pth的文件。如果你想在创建你的模型后将你的模型加载到另一台机器上 Learner
同样的方法,或者稍后恢复训练,你可以加载这个文件的内容,如下:
learn = learn.load('1epoch')
初始训练完成后,我们可以在解冻后继续微调模型:
learn.unfreeze()
learn.fit_one_cycle(10, 2e-3)
epoch | train_loss | vaild_loss | accuracy | perplexity | time |
0 | 3.893486 | 3.772820 | 0.317104 | 43.502548 | 12:37 |
1 | 3.820479 | 3.717197 | 0.323790 | 41.148880 | 12:30 |
2 | 3.735622 | 3.659760 | 0.330321 | 38.851997 | 12:09 |
3 | 3.677086 | 3.624794 | 0.333960 | 37.516987 | 12:12 |
4 | 3.636646 | 3.601300 | 0.337017 | 36.645859 | 12:05 |
5 | 3.553636 | 3.584241 | 0.339355 | 36.026001 | 12:04 |
6 | 3.507634 | 3.571892 | 0.341353 | 35.583862 | 12:08 |
7 | 3.444101 | 3.565988 | 0.342194 | 35.374371 | 12:08 |
8 | 3.398597 | 3.566283 | 0.342647 | 35.384815 | 12:11 |
9 | 3.375563 | 3.568166 | 0.342528 | 35.451500 | 12:05 |
完成此操作后,我们保存所有模型,除了最后一层将激活转换为选择词汇表中每个标记的概率。不包括最后一层的模型称为 编码器。我们可以保存它save_encoder
:
learn.save_encoder('finetuned')
编码器
该模型不包括特定于任务的最后一层。当应用于视觉 CNN 时,这个术语与“身体”的含义大致相同,但“编码器”往往更多地用于 NLP 和生成模型。
这就完成了文本分类过程的第二阶段:微调语言模型。我们现在可以用它来微调分类器
文本生成
因为我们的模型被训练来猜测句子的下一个词,所以我们可以用它来写新评论:
TEXT = "I liked this movie because"
N_WORDS = 40
N_SENTENCES = 2
preds = [learn.predict(TEXT, N_WORDS, temperature=0.75)
for _ in range(N_SENTENCES)]
print("\n".join(preds))
i liked this movie because of its story and characters . The story line was very > strong , very good for a sci - fi film . The main character , Alucard , was > very well developed and brought the whole story i liked this movie because i like the idea of the premise of the movie , the ( > very ) convenient virus ( which , when you have to kill a few people , the " > evil " machine has to be used to protect
如您所见,我们添加了一些随机性(我们根据模型返回的概率随机选择一个词),因此我们不会两次获得完全相同的评论。我们的模型没有任何关于句子结构或语法规则的编程知识,但它显然已经学到了很多关于英语句子的知识:我们可以看到它正确地将大写字母 ( I转换为i因为我们的规则需要两个或更多字符认为一个词是大写的,所以看到它小写是正常的)并且使用一致的时态。总体评价乍一看有道理,只有仔细阅读才能发现有些地方不对劲。对于几个小时内训练好的模型来说还不错!
但我们的最终目标不是训练一个模型来生成评论,而是对它们进行分类……所以让我们使用这个模型来做到这一点。
创建分类器数据加载器
我们现在正在从语言模型微调转向
这意味着我们DataBlock
的 NLP 分类结构看起来非常熟悉。它与我们在处理过的许多图像分类数据集上看到的几乎相同:
dls_clas = DataBlock(
blocks=(TextBlock.from_folder(path, vocab=dls_lm.vocab),CategoryBlock),
get_y = parent_label,
get_items=partial(get_text_files, folders=['train', 'test']),
splitter=GrandparentSplitter(valid_name='test')
).dataloaders(path, path=path, bs=128, seq_len=72)
与图像分类一样,show_batch
显示因变量(在本例中为情感)和每个自变量(电影评论文本):
dls_clas.show_batch(max_n=3)
text | category | |
0 | xxbos i rate this movie with 3 skulls , only coz the girls knew how to scream , this could ‘ve been a better movie , if actors were better , the twins were xxup ok , i believed they were evil , but the eldest and youngest brother , they sucked really bad , it seemed like they were reading the scripts instead of acting them … . spoiler : if they ‘re vampire ’s why do they freeze the blood ? vampires ca n’t drink frozen blood , the sister in the movie says let ’s drink her while she is alive … .but then when they ‘re moving to another house , they take on a cooler they ‘re frozen blood . end of spoiler \n\n it was a huge waste of time , and that made me mad coz i read all the reviews of how | neg |
1 | xxbos i have read all of the xxmaj love xxmaj come xxmaj softly books . xxmaj knowing full well that movies can not use all aspects of the book , but generally they at least have the main point of the book . i was highly disappointed in this movie . xxmaj the only thing that they have in this movie that is in the book is that xxmaj missy ’s father comes to xxunk in the book both parents come ) . xxmaj that is all . xxmaj the story line was so twisted and far fetch and yes , sad , from the book , that i just could n’t enjoy it . xxmaj even if i did n’t read the book it was too sad . i do know that xxmaj pioneer life was rough , but the whole movie was a downer . xxmaj the rating | neg |
2 | xxbos xxmaj this , for lack of a better term , movie is lousy . xxmaj where do i start … … \n\n xxmaj cinemaphotography - xxmaj this was , perhaps , the worst xxmaj i ‘ve seen this year . xxmaj it looked like the camera was being tossed from camera man to camera man . xxmaj maybe they only had one camera . xxmaj it gives you the sensation of being a volleyball . \n\n xxmaj there are a bunch of scenes , haphazardly , thrown in with no continuity at all . xxmaj when they did the ' split screen ' , it was absurd . xxmaj everything was squished flat , it looked ridiculous . \n\n xxmaj the color tones were way off . xxmaj these people need to learn how to balance a camera . xxmaj this ' movie ' is poorly made , and | neg |
查看DataBlock
定义,每个部分都与我们之前构建的数据块相似,但有两个重要的例外:
TextBlock.from_folder
不再有is_lm=True
参数。- 我们通过
vocab
我们为语言模型创建的微调。
我们通过vocab
语言模型的原因是为了确保我们使用相同的令牌对应关系来索引。否则,我们在微调语言模型中学到的嵌入对这个模型没有任何意义,微调步骤也没有任何用处。
通过传递is_lm=False
(或根本不传递is_lm
,因为它默认为False
),我们告诉TextBlock
我们有常规标记数据,而不是使用下一个标记作为标签。然而,我们必须应对一个挑战,这与将多个文档整理成一个小批量有关。让我们看一个例子,尝试创建一个包含前 10 个文档的小批量。首先我们将它们数值化:
nums_samp = toks200[:10].map(num)
现在让我们看看这 10 条影评每条有多少个标记:
nums_samp.map(len)
(#10) [228,238,121,290,196,194,533,124,581,155]
请记住,PyTorchDataLoader
需要将一批中的所有项目整理成单个张量,并且单个张量具有固定的形状(即,它在每个轴上都有特定的长度,并且所有项目必须一致)。这听起来应该很熟悉:我们在图像方面遇到了同样的问题。在那种情况下,我们使用裁剪、填充和/或压缩来使所有输入的大小相同。对文档进行裁剪可能不是一个好主意,因为我们似乎会删除一些关键信息(话虽如此,图像也存在同样的问题,我们在那里使用裁剪;NLP 的数据增强尚未得到很好的探索然而,所以也许实际上也有机会在 NLP 中使用裁剪!)。您不能真正“压缩”文档。这样就留下了填充!
我们将扩展最短的文本,使它们都具有相同的大小。为此,我们使用一个特殊的填充令牌,我们的模型将忽略该令牌。此外,为了避免内存问题和提高性能,我们将把大致相同长度的文本批处理在一起(对训练集进行一些混洗)。我们通过(近似地,对于训练集)在每个纪元之前按长度对文档进行排序来做到这一点。结果是整理成单个批次的文档往往具有相似的长度。我们不会将每批填充到相同的大小,而是使用每批中最大文档的大小作为目标大小。
调整图像大小
可以对图像做类似的事情,这对不规则大小的矩形图像特别有用,但在撰写本文时,还没有库对此提供良好的支持,也没有任何论文涵盖它。然而,我们计划很快将其添加到 fastai 中,因此请密切关注本书的网站;我们会在它运行良好后立即添加相关信息。
TextBlock
使用with时,数据块 API 会自动为我们完成排序和填充is_lm=False
。(对于语言模型数据,我们没有同样的问题,因为我们首先将所有文档连接在一起,然后将它们分成大小相等的部分。)
我们现在可以创建一个模型来对我们的文本进行分类:
learn = text_classifier_learner(dls_clas, AWD_LSTM, drop_mult=0.5,
metrics=accuracy).to_fp16()
训练分类器之前的最后一步是从我们微调的语言模型中加载编码器。我们使用load_encoder
而不是 load
因为我们只有可用于编码器的预训练权重;load
如果加载了不完整的模型,默认情况下会引发异常:
learn = learn.load_encoder('finetuned')
微调分类器
最后一步是训练有区别的学习率和 逐渐解冻。在计算机视觉中,我们经常一次解冻所有模型,但对于 NLP 分类器,我们发现一次解冻几个层会产生真正的不同:
learn.fit_one_cycle(1, 2e-2)
epoch | train_loss | vaild_loss | accuracy | time |
0 | 0.347427 | 0.184480 | 0.929320 | 00:33 |
在一个 epoch 中,我们得到了与第 1 章中训练相同的结果 ——还不错!我们可以传递-2
给freeze_to
冻结除最后两个参数组之外的所有参数:
learn.freeze_to(-2)
learn.fit_one_cycle(1, slice(1e-2/(2.6**4),1e-2))
epoch | train_loss | vaild_loss | accuracy | time |
0 | 0.247763 | 0.171683 | 0.934640 | 00:37 |
然后我们可以解冻更多并继续训练:
learn.freeze_to(-3)
learn.fit_one_cycle(1, slice(5e-3/(2.6**4),5e-3))
epoch | train_loss | vaild_loss | accuracy | time |
0 | 0.193377 | 0.156696 | 0.941200 | 00:45 |
最后,整个模型!
learn.unfreeze()
learn.fit_one_cycle(2, slice(1e-3/(2.6**4),1e-3))
epoch | train_loss | vaild_loss | accuracy | time |
0 | 0.172888 | 0.153770 | 0.943120 | 01:01 |
1 | 0.161492 | 0.155567 | 0.942640 | 00:57 |
我们达到了 94.3% 的准确率,这在短短三年内达到了最先进的表现
使用预训练模型让我们构建一个非常强大的微调语言模型,可以生成虚假评论或帮助对它们进行分类。这是令人兴奋的事情,但最好记住这项技术也可以用于恶意目的。
虚假信息和语言模型
在广泛使用的深度学习语言模型出现之前,即使是基于规则的简单算法也可用于创建 欺诈性账户并试图影响政策制定者。Jeff Kao,现在是 ProPublica 的计算记者分析了发送给美国联邦通信委员会 (FCC) 的关于 2017 年废除网络中立性提案的评论。在他的文章 “超过一百万支持废除网络中立的评论可能是伪造的”中,他报告了他如何发现一大群反对网络中立的评论,这些评论似乎是由某种疯狂的 Libs 风格的邮件合并产生的。在图 10-2中,Kao 对虚假评论进行了有用的颜色编码,以突出其公式化的性质。
图 10-2。FCC 在网络中立辩论期间收到的评论
Kao 估计“超过 2200 万条评论中只有不到 800,000 条……可以被认为是真正独特的”,并且“超过 99% 的真正独特的评论支持保持网络中立。”
鉴于自 2017 年以来语言建模取得的进步,现在几乎不可能捕捉到此类欺诈活动。您现在拥有了所有必要的工具来创建引人注目的语言模型——可以生成适合上下文的、可信的文本的东西。它不一定是完全准确或正确的,但它是合理的。想一想这项技术与我们近年来了解到的各种虚假宣传活动结合起来意味着什么。看一下图 10-3中显示的 Reddit 对话,其中一个语言模型基于
图 10-3。Reddit 上自言自语的算法
在这种情况下,解释说正在使用一种算法来生成对话。但想象一下,如果坏人决定在社交网络上发布这样的算法会发生什么——他们可以缓慢而谨慎地进行,让算法随着时间的推移逐渐培养追随者和信任度。让数以百万计的账户这样做并不需要太多资源。在这种情况下,我们可以很容易地想象到绝大多数在线讨论都来自机器人,而且没有人知道它正在发生。
我们已经开始看到使用机器学习的例子 生成身份。例如,图 10-4显示了 Katie Jones 的 LinkedIn 个人资料。
图 10-4。凯蒂·琼斯的 LinkedIn 个人资料
凯蒂·琼斯在 LinkedIn 上与华盛顿主流智囊团的几名成员有联系。但她不存在。你看到的那张图片是由一个生成对抗网络自动生成的,而一个名叫凯蒂琼斯的人实际上并没有从战略与国际研究中心毕业。
许多人假设或希望算法会在这里为我们辩护——我们将开发可以自动识别自动生成内容的分类算法。然而,问题在于这将永远是一场军备竞赛,其中可以使用更好的分类(或鉴别器)算法来创建更好的生成算法。
结论
在本章中,我们探索了 fastai 库涵盖的最后一个开箱即用的应用程序:文本。我们看到了两种类型的模型:可以生成文本的语言模型,以及确定评论是正面还是负面的分类器。为了构建最先进的分类器,我们使用了预训练语言模型,将其微调到我们任务的语料库中,然后使用其主体(编码器)和新头部进行 分类。