在维基百科上训练的分类模型,通过微调语言模型(维基百科和IMDb语料库的风格不同),迁移到IMDb数据集的情感分类模型中。
自监督学习:使用嵌入在自变量中的标签来训练模型,而不需要外部标签。例如,训练模型预测文本中的下一个单词。
import fastbook
fastbook.setup_book()
from fastbook import *
from IPython.display import display,HTML
在自然语言处理(NLP)的迁移学习中,通用语言模型微调(ULMFit)方法是一个非常有效的三阶段过程:
- 预训练语言模型:首先,我们从一个大型语料库(如维基百科)预训练一个语言模型。这个模型学习了语言的基本结构和词汇。
- 领域特定微调:然后,我们将预训练的语言模型在目标任务的相关语料库上进行微调。例如,如果我们的目标是IMDb电影评论分类,我们会在IMDb的评论数据集上进行微调,这样模型就能适应那里的语言风格和专有名词。
- 分类器微调:最后,我们在微调后的语言模型的基础上,进一步训练一个分类器来执行特定的任务,比如情感分析。
这个过程的关键在于,通过在目标任务的语料库上进行微调,语言模型能够更好地理解和生成与任务相关的文本。这种方法已经在多个NLP任务中显示出了显著的性能提升。
Universal Language Model Fine-tuning for Text Classification https://arxiv.org/abs/1801.06146 (ACL-2018)
文本预处理
如何使用神经网络来预测一个句子中的下一个单词?
在构建语言模型时,我们面临的一个挑战是处理不同长度的句子和可能非常长的文档。为了预测句子中的下一个词,我们可以采用以下步骤:
- 构建词汇表:将数据集中的所有文档连接成一个长字符串,并将其分割成单词(或语义单元token),形成一个非常长的单词列表(或“词汇表vocab”)。
- 索引替换:将每个单词替换为其在词汇表中的索引。
- 创建嵌入矩阵:为词汇表中的每个单词创建一个嵌入向量,并将这些向量组成一个嵌入矩阵。
- 使用嵌入矩阵:将嵌入矩阵作为神经网络的第一层。嵌入矩阵可以直接接受步骤2中创建的原始词汇索引作为输入。这与把代表索引的独热编码向量作为输入矩阵等效,但速度更快、效率更高。
在处理文本时,我们需要考虑序列的概念。我们的自变量将是从第一个单词开始到倒数第二个单词结束的单词序列,而因变量将是从第二个单词开始到最后一个单词结束的单词序列。
我们的词汇表将包含预训练模型中已有的常见单词和特定于我们语料库的新单词(例如电影术语或演员姓名)。我们的嵌入矩阵将相应地构建:对于预训练模型词汇表中的单词,我们将使用预训练模型嵌入矩阵中的相应行;但对于新单词,我们没有预先训练的嵌入向量,因此我们将用随机向量初始化相应的行。
通过这种方式,我们的神经网络能够学习预测给定序列中下一个单词的能力,这是构建语言模型的基础。
在自然语言处理(NLP)中创建语言模型涉及一系列步骤,每个步骤都有其专业术语,并且fastai和PyTorch提供了相应的类来帮助实现。以下是这些步骤的概要:
- 分词(Tokenization):将文本转换为单词(或字符、子字符串)列表,这取决于模型的粒度。
- 数值化(Numericalization):创建一个包含所有唯一单词(词汇表)的列表,并通过查找其在词汇表中的索引,将每个单词转换为一个数字。
- 语言模型数据加载器创建(Language model data loader creation):fastai提供了一个
LMDataLoader
类,它自动处理创建一个从自变量偏移一个单位的因变量。它还处理了一些重要细节,例如如何以保持因变量和自变量所需结构的方式来把数据集洗乱。 - 语言模型创建(Language model creation):我们需要一种特殊的模型来处理任意大小的输入列表。有多种方法可以实现这一点;在这里,将使用递归神经网络(RNN)。
分词
“将文本转换成单词列表”时,如何处理标点符号?如何处理“don’t”?如何处理有连字符的词?如何处理长长的医学或化学词汇?
基于单词
用空格拆分句子,并应用特定的语言规则,即使在没有空格的情况下,也要尝试分离部分意义(例如将 “don’t” 分割为 “do n’t”)。通常,标点符号也被分割成独立的单元。
基于子词
根据最常出现的子字符串将单词拆分成更小的部分。例如,“occasion” 可能被分词为 “o c ca sion”。
基于字符
将句子分成几个单独的字符。
语义单元token
:由分词过程创建的列表中的一个元素。它可以是一个单词、一个单词的一部分(子词),或单个字符。
用fastai单词分词
使用IMDb数据集
from fastai.text.all import *
path = untar_data(URLs.IMDB)
使用get_text_files获取文本文件
files = get_text_files(path, folders = ['train', 'test', 'unsup'])
txt = files[0].open().read()
txt[:75] # 对一篇评论分词,只显示部分
# 'I love a good sappy love story (and I\'m a guy) but when I rented "Love Stor'
分词器分词
spacy = WordTokenizer()
toks = first(spacy([txt]))
print(coll_repr(toks, 30))
(#144) ['I','love','a','good','sappy','love','story','(','and','I',"'m",'a','guy',')','but','when','I','rented','"','Love','Story','"','I','prayed','for','the','end','to','come','as'...]
coll_repr(collection, n)
函数显示结果,可以显示collection的前n项,不含标点。可以看出是按标点符号分开的,he's
被分成he
和's
。那一句话结束的.
和句中的.
,spaCy也做了如下处理。
first(spacy(['The U.S. dollar $1 is $1.00.']))
(#9) ['The','U.S.','dollar','$','1','is','$','1.00','.']
使用了 spacy
库对一段文本进行了处理。spacy
是一个用于自然语言处理的 Python 库,它可以用于词性标注、命名实体识别、依存关系解析等任务。
tkn = Tokenizer(spacy)
print(coll_repr(tkn(txt), 31))
(#157) ['xxbos','i','love','a','good','sappy','love','story','(','and','xxmaj','i',"'m",'a','guy',')','but','when','i','rented','"','love','xxmaj','story','"','i','prayed','for','the','end','to'...]
fastai使用Tokenizer
类为分词过程添加了一些附加功能:
如上有一些以“xx”开头的语义单词,这在英语中不是一个常见的单词前缀,而是特殊的语义单元。例如,列表第一项,xxbos,表示一个新文本的开始(“BOS”是一个标准的NLP缩写“beginning of stream”,意味着“流的开始”)。通过识别这个定义为开始的语义单元,该模型能够明白它需要“忘记”之前说过的话,并专注于即将到来的单词。有助于模型在处理连续文本流时,能够区分不同文本之间的界限。
从某种意义上说,这些规则旨在使模型更容易识别句子的重要部分。将原始的英语语言序列翻译成简化的分词语言(易于模型学习)。
在自然语言处理中,特殊的标记化规则可以帮助模型更有效地学习和表示文本数据。例如:
- 重复字符的处理:如果一个句子中有连续的四个感叹号(“!!!”),规则会将其替换为一个特殊的重复字符标记,后面跟着数字4,然后是一个单独的感叹号。这样,模型的嵌入矩阵就可以编码关于重复标点的一般概念,而不是为每个标点的每个重复次数都需要一个单独的标记。
- 大写字母的处理:一个大写的单词会被替换为一个特殊的大写标记,后面跟着这个单词的小写版本。这样,嵌入矩阵只需要单词的小写版本,从而节省了计算和内存资源,但模型仍然可以学习到大写的概念。
这些规则使得模型能够以更紧凑的形式学习文本的特征,同时保留了文本的重要语义信息,如强调和命名实体的大写形式。
一些特殊语义单元
xxbos 表示一个文本的开始
xxmsj 表示下一个单词以大写开头
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)>]
以下是对每种功能的简要概述:
- 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) #最大只显示31个元素
"(#11) ['xxbos','©','xxmaj','fast.ai','xxrep','3','w','.fast.ai','/','xxup','index']"
根据子词分词
子词分词方法是处理像中文和日文这样的无空格语言的有效方式。这种方法不依赖于空格来分隔意义,而是通过识别语料库中常见的字母组合来构建词汇表。然后,使用这个词汇表来分词。过程:
- 分析语料库:检查一定数量的文档,找出最常出现的字母组合。例如,如果我们分析2000条电影评论,我们可能会发现“电影”、“好看”、“演技”等字词组合频繁出现。
- 使用词汇表分词:根据步骤1中创建的词汇表,将文本分解为子词单元。例如,“我的名字是郝杰瑞”可能会被分解为“我的”、“名字”、“是”、“郝”、“杰”、“瑞”。
这种方法特别适用于处理那些词汇创造性很强、新词频繁出现的语言,因为它允许模型通过已知的子词单元来理解和生成未曾见过的词汇。这也有助于机器学习模型更好地处理语言的复杂性和多样性。
语料库使用前2000条影评:
txts = L(o.open(encoding='utf-8').read() for o in files[:2000])
实例化分词器
## 文本中有'gbk'编码无法处理的字符
def clean_text(text):
return text.encode('gbk', 'ignore').decode('gbk')
txts = L(clean_text(o.open(encoding='utf-8').read()) for o in files[:2000])
def subword(sz):
sp = SubwordTokenizer(vocab_sz=sz)
sp.setup(txts)
return ' '.join(first(sp([txt]))[:40])
subword(1000)
# '▁I ▁love ▁a ▁good ▁sa pp y ▁love ▁story ▁( and ▁I \' m ▁a ▁guy ) ▁but ▁when ▁I ▁r ent ed ▁" L o ve ▁St or y " ▁I ▁p ra y ed ▁for ▁the ▁end ▁to'
使用fastai中的子词分词器时,特殊字符_
代表原文中的空格字符。
在subword函数中,sz
是一个形参,它代表了想创建的词汇表的大小。这个参数将被传递给 SubwordTokenizer
,用于确定在分词时应该使用的最大词汇表大小。
SubwordTokenizer
是一个分词器,它可以将文本分解为子词单元。子词单元是介于单词和字符之间的文本单元,它们可以更好地处理稀有词和词根变化。
vocab_sz
参数决定了分词器在分解文本时可以使用的子词单元的最大数量。如果 vocab_sz
较大,那么分词器可以使用更多的子词单元,这可能会导致更好的模型性能,但也可能会增加模型的复杂性和训练时间。
sp.setup(txts)
这行代码是在训练分词器,使其学习如何将文本分解为子词单元。txts
应该是一个包含大量文本的列表,分词器将从这些文本中学习子词单元。
' '.join(first(sp([txt]))[:40])
这行代码是在使用训练好的分词器将文本分解为子词单元,然后取出前40个子词单元,并将它们连接成一个字符串。txt
应该是一个字符串,代表要被分词的文本。
subword(200) # 更小的vocab,每个语义单元含更少的字符,需更多语义单元表示一个句子
# "▁I ▁lo ve ▁a ▁ g o o d ▁ s a p p y ▁lo ve ▁st or y ▁ ( an d ▁I ' m ▁a ▁ g u y ) ▁but ▁w h en ▁I ▁ r"
subword(10000) # 更大的vocab,那大多数单词将出现在vocab中,训练快,但是嵌入矩阵大,需更多数据学习
# '▁I ▁love ▁a ▁good ▁ s appy ▁love ▁story ▁( and ▁I \' m ▁a ▁guy ) ▁but ▁when ▁I ▁rented ▁" Lo ve ▁Story " ▁I ▁pray ed ▁for ▁the ▁end ▁to ▁come ▁as ▁quickly ▁and ▁pain less ly'
子词分词器是在字符分词和词分词之间的扩展,无需开发语言特定的算法,强大!!!
fastai数值化
根据上述单词分词,
toks = tkn(txt)
print(coll_repr(tkn(txt), 31))
# (#157) ['xxbos','i','love','a','good','sappy','love','story','(','and','xxmaj','i',"'m",'a','guy',')','but','when','i','rented','"','love','xxmaj','story','"','i','prayed','for','the','end','to'...]
toks200 = txts[:200].map(tkn) # 小子集实验
toks200[0]
# (#157) ['xxbos','i','love','a','good','sappy','love','story','(','and'...]
num = Numericalize()
num.setup(toks200)
coll_repr(num.vocab,20)
# "(#2112) ['xxunk','xxpad','xxbos','xxeos','xxfld','xxrep','xxwrep','xxup','xxmaj','the','.',',','a','and','of','to','is','it','i','in'...]"
在fastai库中,Numericalize
是一个将文本转换为数字的过程,这对于准备数据以供模型训练非常重要。这里是如何使用 Numericalize
的一些关键点:
- 特殊规则标记:在词汇表的开始部分,会有一些特殊的标记,如
xxunk
(未知词),xxpad
(填充词)等,这些用于处理文本中的特殊情况。 - 频率排序:之后,每个单词根据其在语料库中出现的频率被添加到词汇表中,最常见的词汇排在前面。
- 参数设置:
min_freq
:这个参数决定了一个单词必须在语料库中出现的最小次数才能被包含在词汇表中。默认值为3,意味着出现次数少于3次的单词会被替换为xxunk
。max_vocab
:这个参数限制了词汇表的最大大小。默认值为60000,表示只有出现频率最高的60000个单词会被保留,其他的会被替换为xxunk
。
- 使用自定义词汇表:如果你有一个预先定义好的词汇表,你可以通过
vocab
参数将其传递给Numericalize
对象,这样就可以根据你的词汇表来数值化数据集。 - 数值化对象的使用:创建
Numericalize
对象后,你可以像使用函数一样调用它,将文本转换为数字序列。
nums = num(toks)[:20]; nums
# TensorText([ 2, 18, 171, 12, 74, 0, 171, 102, 40, 13, 8, 18, 157, 12, 210, 37, 28, 65, 18, 1561])
# 是否可以映射回源文本
' '.join(num.vocab[o] for o in nums)
# "xxbos i love a good xxunk love story ( and xxmaj i 'm a guy ) but when i rented"
将文本分批作为语言模型的输入
不同于图像,将张量调整为相同大小然后堆叠,语言模型要按顺序阅读文本才能有效地预测下一个单词。每一个新批次都应该从上一个批次停止的地方开始。
假设有以下文本:
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. First we will look at the processing steps necessary to convert text into numbers and how to customize it. By doing this, we'll have another example of the PreProcessor used in the data block API.\nThen we will study how we build a language model and train it for a while.
单词分词
stream = "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. First we will look at the processing steps necessary to convert text into numbers and how to customize it. By doing this, we'll have another example of the PreProcessor used in the data block API.\nThen we will study how we build a language model and train it for a while."
tokens = tkn(stream)
bs,seq_len = 6,15
d_tokens = np.array([tokens[i*seq_len:(i+1)*seq_len] for i in range(bs)])
df = pd.DataFrame(d_tokens)
display(HTML(df.to_html(index=False,header=None)))
- 选择序列长度:假设我们选择序列长度为5,这意味着我们将数据分割成长度为5的子数组。
- 创建子数组:从原始数组的开始,我们按顺序取出长度为5的片段,直到覆盖整个数组。
- 保持顺序:在处理这些子数组时,我们需要保持它们的顺序,因为模型会依赖这个顺序来预测下一个标记。
每个子数组都会依次输入到模型中。这样,模型可以在处理当前子数组时,记住之前子数组的信息,从而更好地预测接下来的标记。
这种方法允许模型有效地处理大规模数据集,同时保持了数据的顺序性和上下文信息,这对于文本生成和其他序列预测任务非常重要。
bs,seq_len = 6,5
d_tokens = np.array([tokens[i*15:i*15+seq_len] for i in range(bs)])
df = pd.DataFrame(d_tokens)
display(HTML(df.to_html(index=False,header=None)))
# 第二、第三个
bs,seq_len = 6,5
d_tokens = np.array([tokens[i*15+seq_len:i*15+2*seq_len] for i in range(bs)])
df = pd.DataFrame(d_tokens)
display(HTML(df.to_html(index=False,header=None)))
bs,seq_len = 6,5
d_tokens = np.array([tokens[i*15+10:i*15+15] for i in range(bs)])
df = pd.DataFrame(d_tokens)
display(HTML(df.to_html(index=False,header=None)))
在处理电影评论数据集时,将文本转换为连续的流是一个重要的步骤。这里是整个过程的详细解释:
- 文本串联:首先,我们将所有文本串联起来形成一个长文本流。这就像将所有评论拼接成一篇超长的文章。
- 随机化顺序:为了让模型不依赖于特定的文本顺序,我们在每个训练周期(epoch)开始时随机打乱文档的顺序。这样做可以提高模型的泛化能力。
- 分割成批次:然后,我们将这个长文本流分割成多个批次。如果我们有50,000个标记,并且设置批次大小为10,那么我们将得到10个包含5,000个标记的小流。
- 保持标记顺序:在分割时,我们保持标记的顺序不变。例如,第一个小流包含标记1到5,000,第二个小流包含标记5,001到10,000,以此类推。这样做是为了让模型能够连续地阅读文本。
- 添加特殊标记:在预处理期间,我们在每个新条目的开始添加一个
xxbos
标记,以便模型知道何时开始阅读新的文本条目。 - 模型的内部状态:由于模型具有内部状态,它可以记住之前读取的内容,因此无论我们选择的序列长度如何,它都能产生相同的激活。
- fastai库的自动化:当我们使用fastai库创建一个
LMDataLoader
时,上述所有步骤都在幕后自动完成。我们首先将Numericalize
对象应用于分词后的文本,以将其转换为数字序列。
这个过程确保了模型在训练时能够处理大量的文本数据,同时保持了文本的结构和上下文信息。
nums200 = toks200.map(num)
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 i love a good xxunk love story ( and xxmaj i 'm a guy ) but when i rented"
' '.join(num.vocab[o] for o in y[0][:20]) #因变量的第一行,自变量偏移一个单位
# 'i love a good xxunk love story ( and xxmaj i \'m a guy ) but when i rented "'
训练文本分类器
在使用迁移学习训练先进的文本分类器时,确实有两个主要步骤:
- 微调语言模型:首先,我们需要将预训练在维基百科上的语言模型微调到IMDb评论的语料库上。这意味着我们要让模型适应IMDb评论的特定语言风格和用词。
- 训练分类器:一旦语言模型被微调,我们就可以使用它来训练一个分类器,该分类器将能够根据评论的内容预测电影评价是正面还是负面。
使用数据块训练语言模型
在fastai中,当TextBlock
被传递给DataBlock
时,会自动处理**分词(tokenization)和数值化(numericalization)**。你可以将传递给Tokenize
和Numericalize
的所有参数也传递给TextBlock
。不要忘记DataBlock
的summary
方法,它对于调试数据问题非常有用。
下面是我们如何使用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)
这段代码中的
TextBlock.from_folder(path, is_lm=True)
是在创建一个TextBlock
对象,这是fastai
库中用于处理文本数据的一个类。
from_folder(path, is_lm=True)
是TextBlock
类的一个类方法,它从指定的文件夹中加载文本数据。path
参数是你的数据的路径。is_lm=True
参数表示这个TextBlock
是用于语言模型的。语言模型是一种预测下一个词的模型,所以它需要看到所有的词,而不只是标签。当is_lm=True
时,TextBlock
会将所有的文本数据视为一个连续的文本流,而不是分割成单独的样本。
在DataBlock
中使用TextBlock
的一个不同之处是,我们不是直接使用类(即TextBlock(...)
),而是调用一个类方法。TextBlock
之所以特殊,是因为设置数值化器的词汇表可能需要很长时间(我们必须读取并分词每个文档来获取词汇表)。为了尽可能高效,它执行了一些优化:
- 它在一个临时文件夹中保存分词后的文档,这样就不必多次分词。
- 它并行运行多个分词过程,以利用计算机的CPU。
我们需要告诉TextBlock
如何访问文本,以便它可以进行这个初始预处理——这就是from_folder
的作用。
然后,show_batch
以通常的方式工作。这意味着它会显示数据批次的样本,这对于检查数据是否正确加载和处理非常有用。这是一个调试数据问题时的重要步骤,因为它可以让你直观地看到数据的实际外观,从而更容易发现潜在的问题。
总的来说,TextBlock
通过类方法提供了一个高效的方式来处理文本数据,确保在创建语言模型或其他自然语言处理任务时,数据预处理既快速又准确。这种方法的优势在于它减少了重复工作,并通过并行处理最大化了资源利用率。这些特性使得fastai
成为处理大规模文本数据集的强大工具。(Windows上不可以多进程并行处理,所以代码中这样设置num_workers=0)
dls_lm.show_batch(max_n=2)
微调语言模型
在神经网络中,我们将使用嵌入(embeddings)来将整数词索引转换为激活值,这与我们在协同过滤和表格建模中所做的类似。然后,我们将这些嵌入输入到一个循环神经网络(RNN)中,使用一种称为AWD-LSTM的架构。预训练模型中的嵌入会与为预训练词汇表中不存在的词添加的随机嵌入合并。这一过程在language_model_learner
中自动处理。
简单来说,嵌入是一种将词汇映射到高维空间中的向量的技术,这些向量能够捕捉到词汇的语义信息。在预训练的语言模型中,这些嵌入已经学习到了大量的语言结构和单词之间的关系。当我们在模型中加入新词时,我们会创建随机的嵌入向量,并在训练过程中逐渐调整它们,使其与预训练的嵌入相融合。
AWD-LSTM是一种特殊的RNN架构,它通过使用Dropout技术来避免过拟合,同时保持了模型的复杂性和表达能力。在language_model_learner
中,AWD-LSTM模型可以自动处理新词的嵌入,并将它们与预训练的嵌入合并,从而使模型能够有效地处理新的文本数据。
learn = language_model_learner(
dls_lm, AWD_LSTM, drop_mult=0.3,
metrics=[accuracy, Perplexity()]).to_fp16()
创建一个语言模型训练器的过程。
创建了一个语言模型学习器,它是一个用于训练语言模型的对象。
language_model_learner
是 fastai 库中的一个函数,它接收以下参数:
dls_lm
:一个 DataLoader 对象,它包含了训练和验证数据集。AWD_LSTM
:模型架构,这里使用的是 AWD-LSTM 架构。drop_mult
:一个浮点数,用于控制 dropout 层的比例。Dropout 是一种正则化技术,可以帮助防止模型过拟合。drop_mult=0.3
表示 dropout 比例为 30%。metrics
:一个列表,包含了用于评估模型性能的指标。这里使用的是准确率(accuracy)和困惑度(Perplexity)。
.to_fp16()
是一个方法,它将模型的权重从 float32 转换为 float16,以节省内存和加速训练,但可能会稍微降低模型的精度。这种方法通常在 GPU 上训练大型模型时使用。
learn.fit_one_cycle(1, 2e-2)
保存和加载模型
每个训练周期都需花费很长时间,所以将在训练过程中保存中间模型的结果。所以使用fit_one_cycle而不是fine_tine。language_model_learner在使用预训练模型时,会自动调用freeze,只训练嵌入层(模型中唯一包含随机初始化权重的部分)即,对于那些在我们的IMDb词汇表中但不在预训练模型词汇表中的词的嵌入。
保存
learn.save('1epoch')
# Path('/home/sunqx/.fastai/data/imdb/models/1epoch.pth')
加载
要在另一台机器上加载模型,或者稍后继续训练,可以使用以下方法加载名为 1epoch.pth
的文件内容:
首先,确保模型架构与保存 .pth
文件时使用的架构相同。然后,可以使用 torch.load()
函数来加载状态字典(state_dict),并使用 load_state_dict()
方法将其应用到模型中。
learn = learn.load('1epoch')
一旦初始训练完成,就可以在解冻后继续微调模型了。
learn.unfreeze()
learn.fit_one_cycle(10, 2e-3)
保存除了最后一层的所有模型(不包括最后一层的模型叫做编码器),这将模型的激活值转换为词汇表中选择每个语义单元的概率。
learn.save_encoder('finetuned')
编码器
:该模型不包括特定于任务的最终层。当应用于卷积神经网络时,该术语与“body”的含义大致相同,但“编码器”更倾向于在自然语言处理和生成式大模型中使用。
这就完成了微调语言模型。接下来可以用它和IMDb情感标签来微调分类器,然而在微调分类器之前可以尝试:使用模型生成随机评论。
文本生成
训练模型用来猜测句子中的下一个单词,可以用来写新评论:
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 the great acting and excellent direction . It 's a story about a married man who wants to be love and his wife . He lives with his dad , who has fallen into love with a
# i liked this movie because it seemed like a typical Hong Kong action flick . There are two areas : Hong Kong and a lot of Hong Kong , plus most of the cinema is different than in
添加了一些随机性,根据模型返回的概率选择一个随机单词,因此两次不会获得完全相同的评论。
创建分类器的数据加载器
从语言模型微调转向分类器微调。概括地说,语言模型预测文档的下一个单词,因此它不需要任何外部标签。然而,分类器预测一个外部标签——在IMDb的例子中,这个外部标签表示的是文档的情感。
分类数据块
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)
dls_clas.show_batch(max_n=3)
从数据块的构建看,与之前两个重要的不同:
TextBlock.from_floder
函数不再有is_lm=True
参数,默认是False
,告诉TextBlock,有常规的语义单元数据,而不是使用下一个语义单元作为标签。- 传入了为语言模型微调创建的
vocab
,以确保使用相同的语义单元及索引之间的对应关系,否则,在微调语言模型中学习的嵌入对这个模型没有意义。
将多个文档整理成小批次
nums_samp = toks200[:10].map(num) #10个一批次
nums_samp.map(len)
# (#10) [157,247,154,182,73,221,169,215,772,114]
PyTorch的DataLoaders需要将一个批次中的所有数据项整理成单个张量,单个张量具有固定的形状(即在每个轴上都有特定的长度,所有数据项必须一致)。跟图像上的处理很相似,但是不能或者还没有尝试过对文本裁剪等。数据增强还没有在自然语言处理中得到很好的探索,所以也许在自然语言处理中也有机会使用裁剪!但是可以填充文本。
扩展文本,使它们大小相同。为此使用特殊的填充语义单元,这个语义单元将被模型忽略。此外,为了避免内存问题,并提高性能,将把长度大致相同的文本分批放在一起(对训练集进行一些排序)。其结果是,整理成一批的文件往往长度相似。不会将每批都填充为相同大小,而是使用每批中最大文档的大小作为目标大小。(idea:对图像做类似的处理,对不规则矩形图像尤其有用)
当使用TextBlock
和is_lm=False
时,数据块API会自动为我们完成排序和填充。(对于语言模型数据,没有这样的问题,因为我们首先将所有文档连接在一起,然后将它们分成大小相等的部分。)
创建一个模型对文本分类:训练分类器之前的最后一步是从我们微调的语言模型加载编码器。我们使用load_encoder而不是load,因为我们只有预训练的权重可用于编码器;如果加载了不完整的模型,load默认会引发异常:
learn = text_classifier_learner(dls_clas, AWD_LSTM, drop_mult=0.5,
metrics=accuracy).to_fp16()
learn = learn.load_encoder('finetuned') # 从微调的语言模型加载编码器
微调分类模型
用不同的学习率和逐渐解冻的方式训练。CV中经常一次性解冻所有模型,但对于NLP分类器,一次解冻几层会有不同的效果:
learn.fit_one_cycle(1, 2e-2)
只需一个时期,训练效果还不错!可以将-2
传入freeze_to
来冻结除最后两个参数组之外的所有参数组:
learn.freeze_to(-2) # 解冻最后两层
learn.fit_one_cycle(1, slice(1e-2/(2.6**4),1e-2)) #学习率,它是一个范围,表示学习率从 1e-2/(2.6**4) 线性增加到 1e-2。这种设置通常用于训练深度神经网络,可以帮助模型更好地收敛。
然后多结冻一点,继续训练:
learn.freeze_to(-3)
learn.fit_one_cycle(1, slice(5e-3/(2.6**4),5e-3))
最后解冻整个模型:
learn.unfreeze()
learn.fit_one_cycle(2, slice(1e-3/(2.6**4),1e-3))
对原文本进行翻转后,使用反向的文本进行训练另一个模型,并并计算这两个模型的预测平均值,准确率又有所提高。
使用预训练模型,我们可以构建一个功能强大的微调语言模型,既可以生成虚假评论,也可以帮助对虚假评论进行分类。这项技术也可能被用于恶意目的。
虚假信息和语言模型
随着生成算法的不断进步,分类或鉴别算法也需要不断地更新以保持有效性。这是一个动态平衡的过程,需要持续的研究和开发。
两种模型:能够生成文本的语言模型,以及判断评论正面或负面的分类器。构建一个先进的分类器通常涉及使用预训练的语言模型,将其微调到特定任务的语料库上,然后使用其编码器(encoder)部分配合一个新的头部(head)来进行分类。
https://nbviewer.org/github/fastai/fastbook/blob/master/10_nlp.ipynb