自然语言处理实战第二版(MEAP)(四)(3)https://developer.aliyun.com/article/1517995
8.1.2 RNNs 会记住你告诉它们的一切
你是否曾经不小心触摸到潮湿的油漆,并发现自己在触碰到东西时“重复使用”那种油漆?小时候,你可能会想象自己是一位印象派画家,通过用手指在周围的墙壁上涂油彩的方式与世界分享你的艺术。你将要学会如何建造一个更加专注的印象派文字画家。在第七章中,你想象了一个字母模板作为用 CNNs 处理文本的类比。现在,与其在句子中滑动一个单词模板,不如在它们还潮湿的时候用油漆辊滚动它们…!
想象一下,用干得慢的颜料给句子的字母涂上厚厚的一层。让我们在文本中创造出多样化的彩虹颜色。也许你甚至正在支持北公园的 LBGTQ 自豪周,给人行道和自行车道涂上斑马线。
图 8.2 意义的彩虹
现在,拿起一个干净的油漆滚筒,将其从句子的开头滚到结尾的字母上。你的滚筒会从一个字母上取下油漆,并将其重新放在先前字母的顶部。根据你的滚筒大小,少量的字母(或字母部分)会被滚到右边的字母上。第一个字母后的所有字母都会被涂抹在一起,形成一个模糊的条纹,只能模糊地反映出原始句子。
图 8.3 彩虹尽头的一锅金币
涂抹将先前字母的所有油漆汇集成原始文本的一个紧凑表示。但这是一个有用的、有意义的表示吗?对于人类读者来说,你所做的只是创建了一个多彩的混乱。它对于阅读它的人类来说并不传达多少意义。这就是为什么人类不会为自己使用这种文本含义的表示方式。然而,如果你考虑一下字符的涂抹,也许你能想象出机器是如何解释它的。对于机器来说,它肯定比原始字符序列要密集和紧凑得多。
在自然语言处理中,我们希望创建文本的紧凑、密集的向量表示。幸运的是,我们正在寻找的那种表示隐藏在你的油漆滚筒上!当你的干净的新滚筒被文本的字母涂抹时,它收集了你滚过的所有字母的记忆。这类似于你在第六章创建的词嵌入。但这种嵌入方法可以用于更长的文本片段。如果你愿意,你可以不停地滚动滚筒,不断地将更多的文本压缩成紧凑的表示。
在以前的章节中,你的标记主要是单词或单词 n-gram。你需要扩展你对标记的理解,将个别字符包括在内。最简单的 RNN 使用字符而不是单词作为标记。这被称为基于字符的 RNN。就像你在之前的章节中有单词和标记嵌入一样,你也可以认为字符也有意义。现在,你能理解这个在"Wet Paint!"字母末尾的涂抹如何表示文本所有字母的嵌入吗?
最后一个想象中的步骤可能会帮助你揭示这个思想实验中的隐藏含义。在你的脑海中,检查一下你的油漆滚筒上的嵌入。在你的脑海中,将其在一张干净的纸上滚开。记住,纸和你的滚筒只大到能容纳一个单独的字母。这将 输出 滚筒对文本的记忆的紧凑表示。而这个输出隐藏在你的滚筒里,直到你决定用它做点什么。这就是 RNN 中文本嵌入的工作方式。嵌入被隐藏在你的 RNN 中,直到你决定输出它们或与其他东西结合以重用它们。事实上,在许多 RNN 实现中,文本的这种向量表示存储在名为 hidden
的变量中。
重要
RNN 嵌入与你在第六章和第七章学到的单词和文档嵌入不同。RNN 在时间或文本位置上聚集意义。RNN 将意义编码到这个向量中,以便你可以在文本中重复使用后续的标记。这就像 Python 的 str.encode()
函数,用于创建 Unicode 文本字符的多字节表示。标记序列处理的顺序对最终结果,即编码向量,至关重要。所以你可能想把 RNN 嵌入称为 “编码”、“编码向量” 或 “编码张量”。这种词汇转变是在 Garrett Lander 的一个项目中受到鼓励的,该项目是对非常长且复杂的文档进行自然语言处理,例如患者病历或《穆勒报告》。[6] 这种新的词汇使他的团队更容易发展起自然语言处理管道的共享心理模型。
在本章后面要密切关注隐藏层。激活值存储在变量 h
或 hidden
中。这个张量内的这些激活值是文本中到目前为止的嵌入。每次处理一个新标记时,它都会被新值覆盖,因为你的自然语言处理管道正在汇总它到目前为止已读取的标记的含义。在图 8.4 中,你可以看到这种在嵌入向量中汇集含义的混合要比原始文本更加紧凑和模糊。
图 8.4 汇集含义到一个点中
你可以从油漆印迹中读出一些原始文本的含义,就像罗夏克墨点测试一样。罗夏克墨点是指用在纸牌上的墨水或油漆印迹,用于激发人们的记忆并测试他们的思维或心理健康^([7])。你油漆辊上的油漆印迹是原始文本的模糊、印象派式的呈现。这是你要达成的目标,而不仅是制造一团糟。你可以清洁你的辊子,冲洗并重复这个过程,得到不同的油漆印迹,这些印迹代表了你的神经网络的不同含义。很快你就会看到,这些步骤与 RNN 神经元层中的实际数学操作是相似的。
你的油漆辊沾污了句子末尾的许多字母,以至于末尾的感叹号几乎完全无法辨认。但正是这不可理解的部分,使你的机器能够在油漆辊的有限表面积内理解整个句子。你已经把句子的所有字母都涂到油漆辊的表面上了。如果你想看到油漆辊嵌入的信息,只需把它滚到一张干净的纸上即可。
在你的 RNN 中,你可以在将 RNN 滚动文本标记后输出隐藏层激活。对于人类来讲,编码信息可能不会有很多意义,但它给了你的油漆辊,即机器,整个句子的暗示。你的油漆辊收集了整个句子的印象。我们甚至使用“收集”这个词来表达对某人说的话的理解,就像“我从你刚刚说的话中收集到,将湿漆辊辊在湿漆上与 RNN 是相似的。”
你的油漆辊已将整个字母句子压缩或编码成一个短小的、模糊印象派风格的油漆条纹。在 RNN 中,这个印迹是一个由数字组成的向量或张量。编码向量中的每个位置或维度就像你的油漆印迹中的一个颜色。每个编码维度都保留着一个意义方面,你的 RNN 被设计成跟踪这些方面的含义。油漆在辊子上留下的印象(隐藏层激活)被持续回收,直到文本的末尾。接着,将所有这些印迹再次应用在油漆辊的新位置上,创建一个整个句子的新印象。
8.1.3 RNNs 隐藏他们的理解
对于 RNN 来说,一个关键的改变是通过逐个读取令牌来重复使用每个令牌的含义而维护一个隐藏嵌入。这个包含了 RNN 所理解的一切的权重隐藏向量包含在它所读取的文本点中。这意味着你不能一次性运行整个你正在处理的文本的网络。在先前的章节中,你的模型学习了将一个输入映射到一个输出的函数。但是,接下来你将看到,RNN 会学习一个程序,在你的文本上不断运行,直到完成。RNN 需要逐个读取你的文本的令牌。
一个普通的前馈神经元只是将输入向量乘以一堆权重来创建输出。无论你的文本有多长,CNN 或者前馈神经网络都必须执行相同数量的乘法来计算输出预测。线性神经网络的神经元一起工作,组合出一个新的向量来表示你的文本。 在图 8.5 中可以看到,一个普通的前馈神经网络接受一个向量输入(x
),将其乘以一组权重矩阵(W
),应用激活函数,然后输出一个转换过的向量(y
)。前馈网络层只能将一个向量转换为另一个向量。
图 8.5 普通的前馈神经元
在使用 RNNs 时,你的神经元不会看到整个文本的向量。相反,RNN 必须逐个令牌处理你的文本。为了跟踪已经读取的令牌,它记录一个隐藏向量(h
),可以传递给未来自己——产生隐藏向量的完全相同的神经元。在计算机科学术语中,这个隐藏向量被称为 状态。这就是为什么 Andrej Karpathy 和其他深度学习研究人员对 RNNs 的效果如此兴奋的原因。RNNs 使得机器终于能够学习 Turing 完备程序而不只是孤立的函数.5
图 8.6 循环神经元
如果你展开你的 RNN,它开始看起来像一个链……实际上是一个马尔可夫链。但这一次,你的窗口只有一个标记的宽度,并且您重用了先前标记的输出,结合当前标记,然后向前滚动到文本的下一个标记。庆幸的是,当你在第七章中滑动 CNN 窗口或卷积核时,已经开始做类似的事情。
你如何在 Python 中实现神经网络的递归?幸运的是,你不必像在编程面试中遇到的那样尝试使用递归函数调用。相反,你只需创建一个变量来存储与输入和输出分开的隐藏状态,并且你需要有一个单独的权重矩阵用于计算隐藏张量。列表 8.1 实现了一个最小的 RNN,从头开始,而不使用 PyTorch 的 RNNBase
类。
列表 8.1 PyTorch 中的递归
>>> from torch import nn >>> class RNN(nn.Module): ... ... def __init__(self, ... vocab_size, hidden_size, output_size): # #1 ... super().__init__() ... self.W_c2h = nn.Linear( ... vocab_size + hidden_size, hidden_size) # #2 ... self.W_c2y = nn.Linear(vocab_size + hidden_size, output_size) ... self.softmax = nn.LogSoftmax(dim=1) ... ... def forward(self, x, hidden): # #3 ... combined = torch.cat((x, hidden), axis=1) # #4 ... hidden = self.W_c2h(combined) # #5 ... y = self.W_c2y(combined) # #6 ... y = self.softmax(y) ... return y, hidden # #7
你可以看到这个新的 RNN 神经元现在输出不止一件事。你不仅需要返回输出或预测,而且需要输出隐藏状态张量以供“未来自己”神经元重用。
当然,PyTorch 实现有许多其他特性。PyTorch 中的 RNNs 甚至可以同时从左到右和从右到左训练!这被称为双向语言模型。当然,你的问题需要是“非因果”的,才能使用双向语言模型。在英语 NLP 中,非因果模型意味着你希望语言模型预测你已经知道的其他单词之前(左边)出现的单词。一个常见的非因果应用是预测在 OCR(光学字符识别)期间有意或无意地被屏蔽或损坏的内部单词。如果你对双向 RNNs 感兴趣,所有的 PyTorch RNN 模型(RNNs、GRUs、LSTMs,甚至 Transformers)都包括一个选项来启用双向递归。对于问答模型和其他困难的问题,与默认的向前方向(因果)语言模型相比,双向模型的准确率通常会提高 5-10%。这仅仅是因为双向语言模型的嵌入更加平衡,忘记了文本开头和结尾的内容一样多。
8.1.4 RNNs 记得你告诉它们的一切
要了解 RNNs 如何保留文档中所有标记的记忆,你可以展开图 8.7 中的神经元图。你可以创建神经元的副本,来展示“未来自己”在循环中遍历你的标记。这就像展开一个 for 循环,当你只需复制并粘贴循环内的代码行适当次数时。
图 8.7 展开 RNN 以揭示它的隐藏秘密
图 8.7 显示了一个 RNN 将隐藏状态传递给下一个“未来自己”神经元,有点像奥运接力选手传递接力棒。但是这个接力棒在被 RNN 反复回收利用时印上了越来越多的记忆。你可以看到在 RNN 最终看到文本的最后一个标记之前,输入标记的张量被修改了许多许多次。
RNNs 的另一个好处是你可以在任何位置取出输出张量。这意味着你可以解决像机器翻译、命名实体识别、文本匿名化和去匿名化、甚至政府文件开放化等挑战。^([10])
这两个特点是 RNNs 独有的特点。
- 你可以在一个文档中处理任意数量的 token。
- 在每个 token 处理完之后,你可以输出任何你需要的内容。
第一个特点其实并不是什么大不了的事情。正如你在 CNN 中看到的那样,如果你想处理长文本,只需要在输入张量的最大尺寸里面留出空间就可以了。事实上,到目前为止最先进的 NLP 模型——transformers,也是创建了最大长度限制并像 CNN 一样填充文本的。
然而,RNNs 的第二大特点真的很重要。想象一下,你可以用一个标记每个句子中每一个词汇的模型做出哪些事情。语言学家花费很多时间对话语进行图解并标记 token。RNNs 和深度学习已经改变了语言学研究的方式。只要看一下 SpaCy 可以在清单 8.2 中识别一些“hello world”文本中每个单词的语言学特征,就可以想象一下。
清单 8.2 SpaCy 用 RNNs 标记 token
>>> import pandas as pd >>> from nlpia2.spacy_language_model import nlp >>> >>> tagged_tokens = list(nlp('Hello world. Goodbye now!')) >>> interesting_tags = 'text dep_ head lang_ lemma_ pos_ sentiment' >>> interesting_tags = (interesting_tags + 'shape_ tag_').split() >>> pd.DataFrame([ ... [getattr(t, a) for a in interesting_tags] ... for t in tagged_tokens], ... columns=interesting_tags) text dep_ head lang_ lemma_ pos_ sentiment shape_ tag_ 0 Hello intj world en hello INTJ 0.0 Xxxxx UH 1 world ROOT world en world NOUN 0.0 xxxx NN 2 . punct world en . PUNCT 0.0 . . 3 Goodbye ROOT Goodbye en goodbye INTJ 0.0 Xxxxx UH 4 now advmod Goodbye en now ADV 0.0 xxx RB 5 ! punct Goodbye en ! PUNCT 0.0 ! .
拥有所有信息、在你需要的时候输出所有结果都是很好的。你可能很兴奋地想要在真正长的文本上尝试 RNNs,看看它到底能记住多少。
8.2 只使用姓氏预测一个人的国籍
为了快速让你掌握再循环利用,你将从最简单的 token(字母或标点符号)开始。你要建立一个模型,只使用名字中的字母来指导预测,可以预测出一个人的国籍,也叫“姓氏”。这种模型可能对你来说并不那么有用。你可能甚至担心它可能会被用于伤害某些特定文化的人。
就像你一样,作者的 LinkedIn 关注者们也对当我们提到正在训练一个模型来预测姓名的人口学特征时,感到怀疑。不幸的是,企业和政府确实使用这样的模型来识别和定位特定群体的人,这往往会产生有害的后果。但这些模型也可以用于好处。我们使用它们来帮助我们的非营利组织和政府客户将他们的对话 AI 数据集匿名化。然后志愿者和开源贡献者可以从这些经过匿名处理的对话数据库中训练 NLP 模型,根据用户的需求,同时保护用户的隐私,识别出有用的医疗保健或教育内容。
这个多语言数据集将让你有机会学习如何处理非英语单词常见的变音符号和其他装饰。为了保持趣味性,你将删除这些字符装饰和其他泄漏的 Unicode 字符。这样你的模型就可以学习你真正关心的模式,而不是基于这种泄漏而“作弊”。处理这个数据集的第一步是将其ASCII 化 - 将其转换为纯 ASCII 字符。例如,爱尔兰名字“O’Néàl”的 Unicode 表示中,“e”上有一个“重音符号”,在这个名字的“a”上有一个“重音符号”。而“O”和“N”之间的撇号可能是一个特殊的方向撇号,如果你不将其ASCII 化,它可能会不公平地提示你的模型该名字的国籍。你还需要删除经常添加到土耳其语、库尔德语、罗曼语和其他字母表的字母“C”上的西迪拉装饰。
>>> from nlpia2.string_normalizers import Asciifier >>> asciify = Asciifier() >>> asciify("O’Néàl") "O'Neal" >>> asciify("Çetin") 'Cetin'
现在你有了一个可以为广泛语言规范化字母表的流水线,你的模型会更好地泛化。你的模型几乎可以用于任何拉丁字母文字,甚至是从其他字母表转写为拉丁字母文字的文字。你可以使用完全相同的模型来对几乎任何语言的任何字符串进行分类。你只需要在你感兴趣的每种语言中标记几十个例子来“解决”。
现在让我们看看你是否已经创建了一个可解决的问题。一个可解决的机器学习问题是指:
- 你可以想象一个人类回答这些同样的问题
- 对于你想问你的模型的绝大多数“问题”,存在一个正确的答案
- 你不指望机器的准确度会比训练有素的人类专家高得多
想一想这个预测与姓氏相关的国家或方言的问题。记住,我们已经删除了很多关于语言的线索,比如独特于非英语语言的字符和装饰。这是一个可解决的问题吗?
从上面的第一个问题开始。你能想象一个人类仅从他们的姓氏的 ASCII 化就能确定一个人的国籍吗?就我个人而言,当我试图根据他们的姓氏猜测我的学生来自哪里时,我经常猜错。在现实生活中,我永远不会达到 100%的准确率,机器也不会。所以只要你能接受一个不完美的模型,这就是一个可解决的问题。如果你建立一个良好的管道,有大量标记的数据,你应该能够创建一个至少与你我一样准确的 RNN 模型。当你考虑到这一点时,它甚至可能比训练有素的语言学家更准确,这是相当令人惊讶的。这就是 AI 概念的来源,如果一台机器或算法能够做出智能的事情,我们就称之为 AI。
想想这个问题之所以难的原因。姓氏和国家之间没有一对一的映射。尽管姓氏通常在几代人之间被父母和子女共享,但人们倾向于四处迁移。而且人们可以改变自己的国籍、文化和宗教信仰。所有这些因素都会影响某个特定国家常见的姓名。有时个人或整个家庭决定改姓,尤其是移民、外国人和间谍。人们有很多不同的原因想要融入[¹¹]。文化和语言的融合是使人类在共同努力实现伟大事业方面如此出色的原因,包括人工智能。RNNs 会给你的国籍预测模型带来同样的灵活性。如果你想改名,这个模型可以帮助你设计,使其唤起你想要人(和机器)感知到的国籍。
浏览一些来自这个数据集的随机姓名,看看是否可以找到在多个国家中重复使用的字符模式。
清单 8.3 加载
>>> repo = 'tangibleai/nlpia2' # #1 >>> filepath = 'src/nlpia2/data/surname-nationality.csv.gz' >>> url = f"https://gitlab.com/{repo}/-/raw/main/{filepath}" >>> df = pd.read_csv(url) # #2 >>> df[['surname', 'nationality']].sort_values('surname').head(9) surname nationality 16760 Aalbers Dutch 16829 Aalders Dutch 35706 Aalsburg Dutch 35707 Aalst Dutch 11070 Aalto Finnish 11052 Aaltonen Finnish 10853 Aarab Moroccan 35708 Aarle Dutch 11410 Aarnio Finnish
在深入研究之前先快速查看一下数据。看起来荷兰人喜欢把他们的姓氏(姓氏)放在点名表的开头。一些荷兰姓氏以“Aa”开头。在美国,有很多企业名称以“AAA”开头,原因类似。而且似乎摩洛哥、荷兰和芬兰的语言和文化倾向于鼓励在词语开头使用三字母组“Aar”。所以你可以预料到这些国籍之间会有一些混淆。不要期望分类器达到 90%的准确率。
你还想要统计一下数据集中唯一类别的数量,这样你就知道你的模型将有多少选择。
清单 8.4 数据集中的唯一国籍
>>> df['nationality'].nunique() 37 >>> sorted(df['nationality'].unique()) ['Algerian', 'Arabic', 'Brazilian', 'Chilean', 'Chinese', 'Czech', 'Dutch', 'English', 'Ethiopian', 'Finnish', 'French', 'German', 'Greek', 'Honduran', 'Indian', 'Irish', 'Italian', 'Japanese', 'Korean', 'Malaysian', 'Mexican', 'Moroccan', 'Nepalese', 'Nicaraguan', 'Nigerian', 'Palestinian', 'Papua New Guinean', 'Peruvian', 'Polish', 'Portuguese', 'Russian', 'Scottish', 'South African', 'Spanish', 'Ukrainian', 'Venezuelan', 'Vietnamese']
在清单 8.4 中,你可以看到从多个来源收集到的三十七个独特的国籍和语言类别。这就是这个问题的难点所在。这就像是一个多项选择题,有 36 个错误答案,只有一个正确答案。而且这些地区或语言类别经常重叠。例如,阿尔及利亚人被认为是阿拉伯语的一种,巴西人是葡萄牙语的一种方言。有几个姓名跨越了这些国籍边界。所以模型不能为所有姓名都得到正确答案。它只能尽可能地返回正确答案。
各种国籍和数据源的多样性帮助我们进行名称替换,以匿名化我们多语言聊天机器人中交换的消息。这样可以在开源项目中共享会话设计数据集,例如本书第十二章讨论的聊天机器人。递归神经网络模型非常适用于匿名化任务,例如命名实体识别和虚构名称的生成。它们甚至可以用来生成虚构但逼真的社会安全号码、电话号码和其他个人身份信息(PII)。为了构建这个数据集,我们使用了从公共 API 中抓取的包含非洲、南美和中美洲以及大洋洲少数族裔国家数据的 PyTorch RNN 教程数据集。
在我们每周在 Manning 的 Twitch 频道上进行集体编程时,Rochdi Khalid 指出他的姓氏是阿拉伯语。他住在摩洛哥的卡萨布兰卡,在那里阿拉伯语是官方语言,与法语和柏柏尔语并存。这个数据集是从各种来源汇编而成的。[12]) 其中一些基于广泛的语言标签(如"Arabic")创建标签,而其他一些则以特定的国籍或方言为标签,如摩洛哥、阿尔及利亚、巴勒斯坦或马来西亚。
数据集偏见是最难弥补的偏见之一,除非你能找到要提升的群体的数据。除了公共 API,你还可以从内部数据中挖掘名称。我们的匿名化脚本从多语言聊天机器人对话中剥离出名称。我们将这些名称添加到了这个数据集中,以确保它是与我们的聊天机器人互动的用户种类的代表性样本。你可以在需要从各种文化中获得真正全球化的名称片段的自己的项目中使用这个数据集。
多样性也带来了挑战。你可以想象到,这些音译名称的拼写可能跨越国界甚至跨越语言。翻译和音译是两个不同的自然语言处理问题,你可以使用递归神经网络来解决。词语 “नमस्कार” 可以翻译成英语单词 “hello”。但在你的递归神经网络尝试翻译尼泊尔语单词之前,它将会音译尼泊尔语单词 “नमस्कार” 成为使用拉丁字符集的单词 “namaskāra”。大多数多语言深度学习流程都使用拉丁字符集(罗马脚本字母)来表示所有语言中的单词。
注意
音译是将一个语言的字母和拼写翻译成另一种语言的字母,从而可以使用在欧洲和美洲使用的拉丁字符集(罗马脚本字母)表示单词。一个简单的例子是将法语字符 “é” 的重音去除或添加,例如 “resumé”(简历)和 “école”(学校)。对于非拉丁字母表,如尼泊尔语,音译要困难得多。
以下是如何计算每个类别(国籍)内重叠程度的方法。
>>> fraction_unique = {} >>> for i, g in df.groupby('nationality'): >>> fraction_unique[i] = g['surname'].nunique() / len(g) >>> pd.Series(fraction_unique).sort_values().head(7) Portuguese 0.860092 Dutch 0.966115 Brazilian 0.988012 Ethiopian 0.993958 Mexican 0.995000 Nepalese 0.995108 Chilean 0.998000
除了跨国家的重叠之外,PyTorch 教程数据集中还包含了许多重复的名称。超过 94% 的阿拉伯语名称是重复的,其中一些在第 8.5 节中显示出来。其他国籍和语言,如英语、韩语和苏格兰语,似乎已经去重了。在你的训练集中重复条目使你的模型更紧密地适应于常见名称而不是不太频繁出现的名称。在数据集中复制条目是一种“平衡”数据集或强制统计短语频率的方法,以帮助准确预测流行名称和人口稠密国家。这种技术有时被称为“过度抽样少数类”,因为它增加了数据集中未被充分代表的类别的频率和准确性。
如果你对原始的姓氏数据感兴趣,请查看 PyTorch 的“RNN 分类教程”。^([13]) 在 Arabic.txt 中的 2000 个阿拉伯示例中,只有 108 个独特的阿拉伯姓氏。^([14])
第 8.5 节 姓氏过度抽样
>>> arabic = [x.strip() for x in open('.nlpia2-data/names/Arabic.txt')] >>> arabic = pd.Series(sorted(arabic)) 0 Abadi 1 Abadi 2 Abadi ... 1995 Zogby 1996 Zogby 1997 Zogby Length: 2000, dtype: object
这意味着即使是一个相对简单的模型(比如 PyTorch 教程中展示的模型),也应该能够正确地将像 Abadi 和 Zogby 这样的流行名称标记为阿拉伯语。通过计算数据集中与每个名称关联的国籍数量,你可以预期模型的混淆矩阵统计数据。
你将使用在第 8.5 节中加载的去重数据集。我们已经计算了重复项,为你提供了这些重复项的统计信息,而不会让你下载一个庞大的数据集。你将使用平衡抽样的国家数据,以鼓励你的模型平等对待所有类别和名称。这意味着你的模型将像准确预测流行国家的流行名称一样准确地预测罕见名称和罕见国家。这个平衡的数据集将鼓励你的 RNN 从它在名称中看到的语言特征中归纳出一般规律。你的模型更有可能识别出许多不同名称中常见的字母模式,尤其是那些帮助 RNN 区分国家的模式。我们在 nlpia2
仓库的 GitLab 上包含了关于如何获取准确的名称使用频率统计信息的信息。^([15]) 如果你打算在更随机的名称样本上在真实世界中使用这个模型,你需要记住这一点。
第 8.6 节 名称国籍重叠
>>> df.groupby('surname') >>> overlap = {} ... for i, g in df.groupby('surname'): ... n = g['nationality'].nunique() ... if n > 1: ... overlap[i] = {'nunique': n, 'unique': list(g['nationality'].unique())} >>> overlap.sort_values('nunique', ascending=False) nunique unique Michel 6 [Spanish, French, German, English, Polish, Dutch] Abel 5 [Spanish, French, German, English, Russian] Simon 5 [Irish, French, German, English, Dutch] Martin 5 [French, German, English, Scottish, Russian] Adam 5 [Irish, French, German, English, Russian] ... ... ... Best 2 [German, English] Katz 2 [German, Russian] Karl 2 [German, Dutch] Kappel 2 [German, Dutch] Zambrano 2 [Spanish, Italian]
为了帮助使这个数据集多样化,并使其更具代表性,我们添加了一些来自印度和非洲的姓名。并且通过计算重复项来压缩数据集。由此产生的姓氏数据集将 PyTorch RNN 教程的数据与多语言聊天机器人的匿名化数据结合起来。事实上,我们使用这个姓名分类和生成模型来匿名化我们聊天机器人日志中的姓名。这使我们能够在 NLP 数据集和软件方面“默认开放”。
重要提示
要找出机器学习流水线是否有可能解决您的问题,假装自己是机器。对训练集中的一些示例进行训练。然后尝试回答一些测试集中的“问题”,而不查看正确的标签。你的 NLP 流水线应该能够几乎和你一样好地解决你的问题。在某些情况下,你可能会发现机器比你更好,因为它们可以更准确地在脑海中平衡许多模式。
通过计算数据集中每个名称的最流行国籍,可以创建一个混淆矩阵,使用最常见的国籍作为特定名称的“真实”标签。这可以揭示数据集中的几个怪癖,应该影响模型学习的内容以及其执行此任务的效果如何。对于阿拉伯名字,根本没有混淆,因为阿拉伯名字非常少,而且没有一个被包含在其他国籍中。西班牙、葡萄牙、意大利和英国名字之间存在显著的重叠。有趣的是,在数据集中有 100 个苏格兰名字,其中没有一个最常被标记为苏格兰名字。苏格兰名字更常被标记为英国和爱尔兰名字。这是因为原始的 PyTorch 教程数据集中有成千上万个英国和爱尔兰名字,但只有 100 个苏格兰名字。
图 8.8 在训练之前数据集就产生了混淆
我们在原始 PyTorch 数据集中添加了 26 个国籍。这在类标签中创建了更多的歧义或重叠。许多名称在世界多个不同地区都很常见。RNN 可以很好地处理这种歧义,使用字符序列中模式的统计数据来指导其分类决策。
8.2.1 从头开始构建 RNN
这是您的RNN
类的核心代码,见列表 8.7。像所有 Python 类一样,PyTorch Module 类有一个*init*()
方法,您可以在其中设置一些配置值,以控制类的其余部分的工作方式。对于 RNN,您可以使用*init*()
方法设置控制隐藏向量中的神经元数量以及输入和输出向量大小的超参数。
对于依赖于分词器的自然语言处理应用程序,将分词器参数包含在 init 方法中是个好主意,这样可以更容易地从保存到磁盘的数据中再次实例化。否则,你会发现你在磁盘上保存了几个不同的模型。每个模型可能使用不同的词汇表或字典来对你的数据进行分词和向量化。如果它们没有在一个对象中一起存储,那么保持所有这些模型和分词器的连接是一种挑战。
在你的自然语言处理流水线中,向量化器也是如此。你的流水线必须一致地确定每个词汇的存储位置。如果你的输出是一个类别标签,你还必须一致地确定类别的排序。如果在每次重用模型时,你的类别标签的排序不完全一致,你很容易感到困惑。如果你的模型使用的数值值与这些类别的人类可读名称不一致地映射,输出将是一些混乱的无意义标签。如果你将向量化器存储在你的模型类中(见清单 8.7),它将确切地知道要将哪些类别标签应用于你的数据。
清单 8.7 RNN 的核心
>>> class RNN(nn.Module): >>> def __init__(self, n_hidden=128, categories, char2i): # #1 ... super().__init__() ... self.categories = categories ... self.n_categories = len(self.categories) # #2 ... print(f'RNN.categories: {self.categories}') ... print(f'RNN.n_categories: {self.n_categories}') ... self.char2i = dict(char2i) ... self.vocab_size = len(self.char2i) ... self.n_hidden = n_hidden ... self.W_c2h = nn.Linear(self.vocab_size + self.n_hidden, self.n_hidden) ... self.W_c2y = nn.Linear(self.vocab_size + self.n_hidden, self.n_categories) ... self.softmax = nn.LogSoftmax(dim=1) >>> def forward(self, x, hidden): # #3 ... combined = torch.cat((x, hidden), 1) ... hidden = self.W_c2h(combined) ... y = self.W_c2y(combined) ... y = self.softmax(y) ... return y, hidden # #4
从技术上讲,你的模型不需要完整的char2i
词汇表。它只需要你计划在训练和推断期间输入的一个独热令牌向量的大小。类别标签也是如此。你的模型只需要知道类别的数量。这些类别的名称对机器来说毫无意义。但是通过在你的模型中包含类别标签,你可以在需要调试模型内部时随时将它们打印到控制台。
8.2.2 逐个令牌训练 RNN
nlpia2
项目中包含 30000 多个姓氏的数据集,涵盖了 37 个以上的国家,即使在一台普通的笔记本电脑上也是可管理的。因此,你应该能够在合理的时间内使用nlpia2
来训练它。如果你的笔记本电脑有 4 个或更多的 CPU 核心和 6GB 或更多的 RAM,训练将花费大约 30 分钟。如果你限制自己只使用 10 个国家、10000 个姓氏,并且在学习率的选择上有一些幸运(或聪明),你可以在两分钟内训练出一个好的模型。
而不是使用内置的torch.nn.RNN
层,你可以使用普通的Linear
层从头开始构建你的第一个 RNN。这样可以让你的理解更加泛化,这样你就可以为几乎任何应用设计自己的 RNN。
清单 8.8 对单个样本进行训练必须循环遍历字符
>>> def train_sample(model, category_tensor, char_seq_tens, ... criterion=nn.NLLLoss(), lr=.005): """ Train for one epoch (one example name nationality tensor pair) """ ... hidden = torch.zeros(1, model.n_hidden) # #1 ... model.zero_grad() # #2 ... for char_onehot_vector in char_seq_tens: ... category_predictions, hidden = model( # #3 ... x=char_onehot_vector, hidden=hidden) # #4 ... loss = criterion(category_predictions, category_tensor) ... loss.backward() ... for p in model.parameters(): ... p.data.add_(p.grad.data, alpha=-lr) ... return model, category_predictions, loss.item()
nlpia2
包包含一个脚本,用于编排训练过程,并允许你尝试不同的超参数。
>>> %run classify_name_nationality.py # #1 surname nationality 0 Tesfaye Ethiopian ... [36241 rows x 7 columns]
提示
您应该在 iPython 控制台中使用 %run
魔术命令,而不是在终端中使用 python
解释器运行机器学习脚本。ipython 控制台类似于调试器。它允许您在脚本运行完成后检查所有全局变量和函数。如果取消运行或遇到停止脚本的错误,您仍然能够检查全局变量,而无需从头开始。
一旦您启动 classify_name_nationality.py
脚本,它将提示您关于模型超参数的几个问题。这是培养关于深度学习模型直觉的最佳方式之一。这也是为什么我们选择了一个相对较小的数据集和小问题,可以在合理的时间内成功训练。这使您可以尝试许多不同的超参数组合,并在微调模型时微调您对 NLP 的直觉。
列表 8.9 展示了一些超参数的选择,可以获得很好的结果。但我们给您留了足够的空间来自行探索各种选项的“超空间”。您能否找到一组超参数,以更高的准确率识别更广泛的国籍?
列表 8.9 可交互的提示,以便您可以调整超参数。
How many nationalities would you like to train on? [10]? 25 model: RNN( n_hidden=128, n_categories=25, categories=[Algerian..Nigerian], vocab_size=58, char2i['A']=6 ) How many samples would you like to train on? [10000]? 1500 What learning rate would you like to train with? [0.005]? 0.010 2%|▊ | 30/1500 [00:06<05:16, 4.64it/s]000030 2% 00:06 3.0791 Haddad => Arabic (1) ✓ 000030 2% 00:06 3.1712 Cai => Moroccan (21) ✗ should be Nepalese (22=22)
即使只有 128 个神经元和 1500 个周期的简化 RNN 模型,也需要几分钟才能收敛到一个合理的精确度。此示例在一台配备 4 核心(8 线程)i7 Intel 处理器和 64GB 内存的笔记本上进行训练。如果您的计算资源更有限,您可以在只有 10 个国籍的简化模型上进行训练,它应该会更快地收敛。请记住,许多名称被分配给多个国籍。有些国籍标签是更常见的语言标签,比如“阿拉伯语”,适用于很多很多国家。因此,您不应期望获得非常高的精确度,特别是当您给模型许多国籍(类别)选择时。
列表 8.10 训练输出日志
001470 98% 06:31 1.7358 Maouche => Algerian (0) ✓ 001470 98% 06:31 1.8221 Quevedo => Mexican (20) ✓ ... 001470 98% 06:31 0.7960 Tong => Chinese (4) ✓ 001470 98% 06:31 1.2560 Nassiri => Moroccan (21) ✓ mean_train_loss: 2.1883266236980754 mean_train_acc: 0.5706666666666667 mean_val_acc: 0.2934249263984298 100%|███████████| 1500/1500 [06:39<00:00, 3.75it/s]
看起来 RNN 在训练集上达到了 57%的准确率,在验证集上达到了 29%的准确率。这是对模型有用性的一种不公平的衡量方式。因为在将数据集拆分成训练和验证集之前,数据集已经去重,每个姓名-国籍组合只有一行数据。这意味着在训练集中与一个国籍相关联的姓名可能在验证集中与不同的国籍相关联。这就是为什么 PyTorch 教程在官方文档中没有创建测试或验证数据集的原因。他们不想让您感到困惑。
现在你了解了数据集中的歧义,你可以看到这个问题有多困难,而且这个 RNN 在字符序列中找到的模式上的泛化能力非常强。它在验证集上的泛化能力比随机猜测要好得多。即使每个名字关联的国籍没有歧义,随机猜测也只能在 25 个类别中获得 4%的准确率(1/25 == .04
)。
让我们试试一些在许多国家都使用的常见姓氏。一个叫 Rochdi Khalid 的工程师帮助创建了本章中的一个图表。他生活和工作在摩洛哥的卡萨布兰卡。尽管摩洛哥不是"Khalid"的最高预测,但摩洛哥位居第二!
>>> model.predict_category("Khalid") 'Algerian' >>> predictions = topk_predictions(model, 'Khalid', topk=4) >>> predictions text log_loss nationality rank 0 Khalid -1.17 Algerian 1 Khalid -1.35 Moroccan 2 Khalid -1.80 Malaysian 3 Khalid -2.40 Arabic
前三个预测都是阿拉伯语国家。我认为没有专家语言学家能够像这个 RNN 模型那样快速或准确地进行这种预测。
现在是时候深入挖掘,检查一些更多的预测,看看你是否能够弄清楚只有 128 个神经元如何能够如此成功地预测某人的国籍。
自然语言处理实战第二版(MEAP)(四)(5)https://developer.aliyun.com/article/1518001