自然语言处理实战第二版(MEAP)(一)(2)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
NLP 自学习平台,3个模型定制额度 1个月
全局流量管理 GTM,标准版 1个月
简介: 自然语言处理实战第二版(MEAP)(一)

自然语言处理实战第二版(MEAP)(一)(1)https://developer.aliyun.com/article/1517822

2.4 你的分词器工具箱

因此,你遇到的每个应用程序都需要考虑哪种类型的分词器适合你的应用程序。一旦你决定要尝试哪些类型的标记,你就需要配置一个 Python 包来实现这个目标。

你可以选择几种分词器实现:^([7])

  1. Python:str.splitre.split
  2. NLTK:PennTreebankTokenizerTweetTokenizer
  3. spaCy:最先进的分词是其存在的原因
  4. Stanford CoreNLP:语言学准确,需要 Java 解释器
  5. Huggingface:BertTokenizer,一个 WordPiece 分词器

2.4.1 最简单的分词器

分词句子的最简单方法是使用字符串中的空格作为单词的“分隔符”。在 Python 中,这可以通过标准库方法 split 来实现,该方法可用于所有 str 对象实例,以及 str 内置类本身。

假设你的 NLP 管道需要解析 WikiQuote.org 的引用,并且它在一个标题为《偷书贼》的引用上遇到了麻烦。^([8])

列表 2.1 从《偷书贼》中拆分为标记的示例引用
>>> text = ("Trust me, though, the words were on their way, and when "
...         "they arrived, Liesel would hold them in her hands like "
...         "the clouds, and she would wring them out, like the rain.")
>>> tokens = text.split()  # #1
>>> tokens[:8]
['Trust', 'me,', 'though,', 'the', 'words', 'were', 'on', 'their']
图 2.1 分词短语


正如你所见,这个内置的 Python 方法对这个句子的分词工作做得还可以。它唯一的“错误”是在标记内包括了逗号。这将阻止你的关键字检测器检测到相当多重要的标记:['我', '虽然', '走', '的', '途中', '云', '外', '有', '雨']。这些词“云”和“雨”对于这段文字的意义非常重要。所以你需要做得更好一点,确保你的分词器能够捕捉到所有重要的单词,并像莉泽尔一样“保持”它们。

2.4.2 基于规则的分词

结果表明,解决将标点符号与单词分开的挑战有一个简单的方法。您可以使用正则表达式标记器来创建处理常见标点模式的规则。这里只是一个特定的正则表达式,您可以用来处理标点符号 “hanger-ons”。而且当我们在这里处理时,这个正则表达式将对具有内部标点符号的单词(例如带有撇号的所有格词和缩写)进行智能处理。

你将使用正则表达式对来自 Peter Watts 的书 Blindsight 中的一些文本进行标记化。这段文本描述了最 足够 的人类如何在自然选择(以及外星人入侵)中幸存。[9] 对于您的标记器也是一样的。您需要找到一个 足够 的标记器来解决您的问题,而不是完美的标记器。您可能甚至无法猜出什么是 正确最适合 的标记。您将需要一个准确度数字来评估您的 NLP 流水线,并告诉您哪个标记器应该在您的选择过程中幸存下来。这里的示例应该帮助您开始培养对正则表达式标记器应用的直觉。

>>> import re
>>> pattern = r'\w+(?:\'\w+)?|[^\w\s]'  # #1
>>> texts = [text]
>>> texts.append("There's no such thing as survival of the fittest. "
...              "Survival of the most adequate, maybe.")
>>> tokens = list(re.findall(pattern, texts[-1]))
>>> tokens[:8]
["There's", 'no', 'such', 'thing', 'as', 'survival', 'of', 'the']
>>> tokens[8:16]
['fittest', '.', 'Survival', 'of', 'the', 'most', 'adequate', ',']
>>> tokens[16:]
['maybe', '.']

好多了。现在标记器将标点符号与单词的末尾分开,但不会将包含内部标点符号的单词分割开,比如单词 “There’s” 中的撇号。所以所有这些单词都被标记了: “There’s”, “fittest”, “maybe”。这个正则表达式标记器甚至可以处理具有撇号之后超过一个字母的缩写,比如 “can’t”, “she’ll”, “what’ve”。它甚至可以处理错别字,比如 ‘can"t’ 和 “she,ll”,以及 “what`ve”。但是,即使有更多的例子,例如 “couldn’t’ve”, “ya’ll’ll” 和 “y’ain’t”,这种宽松匹配内部标点符号的方式可能不是您想要的。

小贴士

Pro tip: 你可以用正则表达式 r'\w+(?:\'\w+){0,2}|[^\w\s]' 来处理双重缩写。

这是要记住的主要思想。无论您如何精心制作您的标记器,它都很可能会破坏一些原始文本中的信息。当您切割文本时,您只需确保您留在地板上的信息对于您的管道来说并非必需即可。此外,思考您的下游 NLP 算法也是有帮助的。稍后,您可能会配置大小写折叠、词干提取、词形还原、同义词替换或计数向量化算法。当您这样做时,您将不得不考虑您的标记器正在做什么,这样您的整个管道就可以共同完成您期望的输出。

看一下按字典顺序排序的词汇表中的前几个标记,针对这段简短文本:

>>> import numpy as np
>>> vocab = sorted(set(tokens))  # #1
>>> '  '.join(vocab[:12])  # #2
", . Survival There's adequate as fittest maybe most no of such"
>>> num_tokens = len(tokens)
>>> num_tokens
18
>>> vocab_size = len(vocab)
>>> vocab_size
15

你可以看到,你可能希望考虑将所有标记转换为小写,以便“Survival”被识别为与“survival”相同的单词。而且你可能希望有一个同义词替换算法,将“There’s”替换为“There is”出于类似的原因。但是,只有当你的分词器保留缩写和所有格撇号附加到其父标记时,这才有效。

提示

确保在似乎你的管道在某个特定文本上表现不佳时查看你的词汇表。你可能需要修改你的分词器,以确保它可以“看到”所有它需要为你的 NLP 任务做得好的标记。

2.4.3 SpaCy

也许你不希望你的正则表达式分词器将缩写保持在一起。也许你想要将单词“isn’t”识别为两个单独的单词,“is”和“n’t”。这样,你就可以将“n’t”和“not”的同义词合并为一个标记。这样,你的 NLP 管道就可以理解“the ice cream isn’t bad”与“the ice cream is not bad”表示相同的意思。对于一些应用,比如全文搜索、意图识别和情感分析,你希望能够像这样解开或扩展缩写。通过分割缩写,你可以使用同义词替换或缩写扩展来提高搜索引擎的召回率和情感分析的准确性。

重要提示

我们将在本章后面讨论大小写折叠、词干提取、词形还原和同义词替换。对于诸如作者归因、风格转移或文本指纹等应用,要谨慎使用这些技术。你希望你的作者归因或风格转移管道保持忠实于作者的写作风格和他们使用的确切拼写。

SpaCy 将分词器直接集成到其先进的 NLU 管道中。实际上,“spaCy”的名称是基于单词“space”,即用于西方语言中分隔单词的分隔符。在应用规则将标记分隔开的同时,spaCy 还向标记添加了许多附加的标签。因此,spaCy 通常是你需要使用的第一个和最后一个分词器。

让我们看看 spaCy 如何处理我们的一系列深思家名言:

>>> import spacy  # #1
>>> spacy.cli.download('en_core_web_sm')  # #2
>>> nlp = spacy.load('en_core_web_sm')  # #3
>>> doc = nlp(texts[-1])
>>> type(doc)
spacy.tokens.doc.Doc
>>> tokens = [tok.text for tok in doc]
>>> tokens[:9]
['There', "'s", 'no', 'such', 'thing', 'as', 'survival', 'of', 'the']
>>> tokens[9:17]
['fittest', '.', 'Survival', 'of', 'the', 'most', 'adequate', ',']

如果你将结果与学术论文或工作中的同事进行比较,那么该标记化对你可能更有用。Spacy 在幕后做了更多工作。你下载的那个小语言模型还使用一些句子边界检测规则来识别句子中的断点。语言模型是一组正则表达式和有限状态自动机(规则)。这些规则很像你在英语课上学到的语法和拼写规则。它们用于分词和标记你的单词,以便为它们标记有用的东西,比如它们的词性和它们在单词之间关系语法树中的位置。

>>> from spacy import displacy
>>> sentence = list(doc.sents)[0]  # #1
>>> svg = displacy.render(sentence, style="dep",
...     jupyter=False)  # #2
>>> open('sentence_diagram.svg', 'w').write(svg)  # #3
>>> # displacy.serve(sentence, style="dep") # #4
>>> # !firefox 127.0.0.1:5000
>>> displacy.render(sentence, style="dep")  # #5

有三种方式可以从displacy创建和查看句子图:在您的 web 浏览器中,您的网页中的一个动态 HTML+SVG 文件,在您的硬盘驱动器上的一个静态 SVG 文件,或者在 jupyter 笔记本中的一个内联 HTML 对象中。如果您浏览到您本地硬盘上的sentence_diagram.svg文件或localhost:5000服务器,您应该可以看到一个句子图,可能比您在学校中可以制作的还要好。


您可以看到 spaCy 不仅仅是将文本分隔成标记。它可以识别句子边界,自动将您的文本分割成句子。它还会标记各种属性的标记,比如它们在句子的语法中的词性(PoS)甚至是角色。您可以在displacy下看到每个标记的词形。^([11]) 本章后面我们会解释词形化和大小写折叠以及其他词汇压缩方法对某些应用的帮助。

因此,spaCy 在准确性和一些“内置”功能方面似乎相当出色,比如所有那些标记标记的词形和依赖关系。那速度呢?

2.4.4 标记器竞赛

SpaCy 可以在约 5 秒钟内解析本书一章的 AsciiDoc 文本。首先下载本章的 AsciiDoc 文本文件:

>>> import requests
>>> text = requests.get('https://proai.org/nlpia2-ch2.adoc').text
>>> f'{round(len(text) / 10_000)}0k'  # #1
'60k'

在我写下您正在阅读的这句话的 AsciiDoc 文件中大约有 160,000 个 ASCII 字符。以每秒字词数为标准的标记速度基准是什么意思?

>>> import spacy
>>> nlp = spacy.load('en_core_web_sm')
>>> %timeit nlp(text)  # #1
4.67 s ± 45.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
>>> f'{round(len(text) / 10_000)}0k'
'160k'
>>> doc = nlp(text)
>>> f'{round(len(list(doc)) / 10_000)}0k'
'30k'
>>> f'{round(len(doc) / 1_000 / 4.67)}kWPS'  # #2
'7kWPS'

对于约 150,000 个字符或 34,000 个英文和 Python 文本字词,或约每秒 7000 个字词,大约需要近 5 秒钟。

这对于你的个人项目来说可能足够快了。但在一个医疗记录摘要项目中,我们需要处理数千个大型文档,其中包含与您在整本书中找到的相当数量的文本相当的文本量。我们医疗记录摘要管道中的延迟是该项目的关键指标。因此,这个功能齐全的 spaCy 管道至少需要 5 天的时间来处理 10,000 本书,比如 NLPIA 或典型的 10,000 名患者的医疗记录。

如果对于您的应用程序来说速度还不够快,您可以禁用不需要的 spaCy 管道的任何标记特性。

>>> nlp.pipe_names  # #1
['tok2vec', 'tagger', 'parser', 'attribute_ruler', 'lemmatizer', 'ner']
>>> nlp = spacy.load('en_core_web_sm', disable=nlp.pipe_names)
>>> %timeit nlp(text)
199 ms ± 6.63 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

您可以禁用您不需要的管道元素以加速标记器:

  • tok2vec:单词嵌入
  • tagger:词性(.pos.pos_
  • parser:语法树角色
  • attribute_ruler:细粒度的 POS 和其他标记
  • lemmatizer:词形标记
  • ner:命名实体识别标记

NLTK 的 word_tokenize 方法通常用作标记器速度比较的标尺:

>>> import nltk
>>> nltk.download('punkt')
True
>>> from nltk.tokenize import word_tokenize
>>> %timeit word_tokenize(text)
156 ms ± 1.01 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
>>> tokens = word_tokenize(text)
>>> f'{round(len(tokens) / 10_000)}0k'
'10k'

你可能觉得你找到了标记器竞赛的赢家吗?不要那么快。您的正则表达式标记器有一些非常简单的规则,因此它应该运行得相当快:

>>> pattern = r'\w+(?:\'\w+)?|[^\w\s]'
>>> tokens = re.findall(pattern, text)  # #1
>>> f'{round(len(tokens) / 10_000)}0k'
'20k'
>>> %timeit re.findall(pattern, text)
8.77 ms ± 29.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

这并不奇怪。正则表达式可以在 Python 的低级 C 例程内被编译和高效运行。

小贴士

当速度比准确性更重要时,请使用正则表达式分词器。如果您不需要像 spaCy 和其他管道提供的额外语言标签,您的分词器就不需要浪费时间去尝试识别这些标签。^([12]) 并且每次使用reregex包中的正则表达式时,优化编译版本会缓存在内存中。因此通常不需要预编译(使用re.compile())您的正则表达式。

2.5 Wordpiece 分词器

或许我们会自然地把词视为不可分割的,具有独立意义和思考的基本单元。然而,你可能会发现有些词在空格或标点符号上并不分明,以及很多复合词或命名实体内部有空格。因此,需要深入挖掘,并考虑什么使一个词成为一个词的统计规律。可以考虑如何从相邻字符构建词,而不是在分隔符(如空格和标点符号)处将文本分开。

2.5.1 聚合字符成句部

您的分词器可以寻找紧密相邻使用的字符,例如在“i”之前的“e”,而不是考虑将字符串分割成标记。您可以将这些字符和字符序列组合在一起。^([13]) 这些字符团体可以成为您的标记。NLP 管道只关注标记的统计数据。希望这些统计数据与我们对“词”是什么的期望相吻合。

许多这些字符序列将是整个单词,甚至是复合词的一部分,但其中很多将是词的部分。事实上,所有子词分词器都在词汇表中为每个字符维护一个标记。这意味着只要新文本不包含它以前没有见过的字符,它就永远不需要使用 OOV(词汇外)标记。子词分词器尝试将字符最优地聚集在一起以创建标记。利用字符 n-gram 计数的统计数据,这些算法可以识别出构成良好标记的词部甚至句部。

通过聚合字符来确定词似乎有点奇怪。但对于机器来说,在文本中识别意义元素之间唯一明显、一致的分割就是字节或字符间的边界。并且字符一起使用的频率可以帮助机器确定与子词标记相关的意义,例如单个音节或复合词的部分。

在英语中,即使是单个字母也与微妙的情感(情绪)和含义(语义)相关联。然而,英语中只有 26 个唯一的字母。这并不留下单个字母在任何一个主题或情感上专攻的余地。尽管如此,精明的营销人员知道有些字母比其他字母更酷。品牌将尝试通过选择具有像 “Q”、“X” 或 “Z” 这样的奇异字母的名称来展示自己技术先进。这也有助于 SEO(搜索引擎优化),因为更罕见的字母更容易在可能的公司和产品名称中被找到。你的 NLP 流水线将捕捉到所有这些意义、内涵和意图的线索。你的标记计数器将为机器提供它需要推断经常一起使用的字母簇的含义的统计数据。

子词分词器唯一的劣势是它们必须在收敛于最佳词汇表和分词器之前多次通过你的文本语料库。与 CountVectorizer 类似,子词分词器必须像 CountVectorizer 一样被训练或适应于你的文本。事实上,在下一节中,你将使用 CountVectorizer 来了解子词分词器的工作原理。

子词分词有两种主要方法:BPE(字节对编码)和 Wordpiece 分词。

BPE

在本书的上一版中,我们坚持认为单词是英语中你需要考虑的最小意义单位。随着使用 BPE 和类似技术的 Transformer 和其他深度学习模型的兴起,我们改变了主意。基于字符的子词分词器已被证明对于大多数 NLP 问题更具多功能性和鲁棒性。通过从 Unicode 多字节字符的构建块构建词汇表,你可以构建一个能处理你将要见到的每个可能的自然语言字符串的词汇表,所有这些只需 50,000 个令牌的词汇表即可。

你可能会认为 Unicode 字符是自然语言文本中含义的最小单元。对于人类来说,也许是这样,但对于机器来说却不是。正如 BPE 的名字所暗示的,字符不必是你基本词汇的基本含义原子。你可以将字符分割成 8 位字节。GPT-2 使用字节级 BPE 分词器来自然地组成你需要的所有 Unicode 字符,从构成它们的字节中。尽管在基于字节的词汇表中处理 Unicode 标点符号需要一些特殊规则,但对于基于字符的 BPE 算法不需要其他调整。字节级 BPE 分词器允许你用最少的 256 个令牌的基本(最小)词汇量来表示所有可能的文本。GPT-2 模型可以使用仅有 50,000 个多字节合并令牌加上 256 个单独字节令牌的默认 BPE 词汇表实现最先进的性能。

BPE(字节对编码)分词算法可以想象成一个社交网络中的红娘。BPE 会把经常相邻出现且看起来“友好”的字符配对起来,然后为这些字符组合创建一个新的标记。BPE 可以在您的文本中经常出现的地方组合多字符标记。并且它会一直这样做,直到达到您在词汇量限制中允许的常用字符序列数量为止。

BPE 正在改变我们对自然语言标记的看法。自然语言处理工程师终于让数据说话了。在构建自然语言处理流水线时,统计思维比人类直觉更好。机器可以看到大多数人如何使用语言。而您只熟悉您在使用特定单词或音节时的意思。变换器现在已经在某些自然语言理解和生成任务中超越了人类读者和作者,包括在子词标记中找到含义。

您尚未遇到的一个复杂情况是当您遇到一个新单词时该怎么办的困境。在前面的例子中,我们只是不断向我们的词汇表中添加新单词。但在现实世界中,您的流水线将在一个初始文档语料库上进行训练,该语料库可能代表或可能不代表它将来可能见到的所有种类的标记。如果您的初始语料库缺少后来遇到的某些单词,您将没有一个词汇表位置来放置该新单词的计数。因此,在训练初始流水线时,您将始终保留一个位置(维度)来保存您的超出词汇量(OOV)标记的计数。因此,如果您的原始文档集中不包含女孩的名字“阿芙拉”,则所有阿芙拉的计数都将作为阿曼丁和其他稀有单词的计数被合并到 OOV 维度中。

要在您的向量空间中给予阿芙拉平等的表示,您可以使用 BPE。BPE 会将稀有单词拆分成较小的片段,为您语料库中的自然语言创建一个元素周期表。所以,因为“aphr”是一个常见的英语前缀,您的 BPE 分词器可能会为阿芙拉在您的词汇表中留下两个位置来计数:一个是“aphr”,另一个是“a”。实际上,您可能会发现词汇表中的位置是“ aphr”和“a”,因为 BPE 对空格的处理方式与字母表中的其他字符没有区别。^([15])

BPE 让您可以处理希伯来语名字(如 Aphra)的多语言灵活性。它还可以使您的流程对常见拼写错误和打字错误具有健壮性,例如"aphradesiac"。每个单词,包括少数 2-grams(例如"African American"),在 BPE 的投票系统中都有表示。^([16]) 过去使用处理人类交流的稀有怪癖的 OOV (Out-of-Vocabulary) 标记的方法已经过时了。因此,最先进的深度学习 NLP 流水线(如 transformers)都使用类似于 BPE 的词片标记方法。^([17])

BPE 通过使用字符标记和词片标记来拼写任何未知单词或单词部分,从而保留了一些新单词的含义。例如,如果我们的词汇表中没有"syzygy",我们可以将其表示为六个标记:“s”,“y”,“z”,“y”,“g"和"y”。也许"smartz"可以表示为两个标记:“smart"和"z”。

听起来很聪明。让我们看看它如何在我们的文本语料库中工作:

>>> import pandas as pd
>>> from sklearn.feature_extraction.text import CountVectorizer
>>> vectorizer = CountVectorizer(ngram_range=(1, 2), analyzer='char')
>>> vectorizer.fit(texts)
CountVectorizer(analyzer='char', ngram_range=(1, 2))

您创建了一个CountVectorizer类,它将文本分词为字符而不是单词。它将计算标记对(字符 2-grams)以及单个字符标记。这些是 BPE 编码中的字节对。现在您可以检查词汇表以查看它们的样子。

>>> bpevocab_list = [
...    sorted((i, s) for s, i in vectorizer.vocabulary_.items())]
>>> bpevocab_dict = dict(bpevocab_list[0])
>>> list(bpevocab_dict.values())[:7]
['  ', ' a', ' c', ' f', ' h', ' i', ' l']

我们将CountVectorizer配置为将文本分割成所有可能的字符 1-grams 和 2-grams。CountVectorizer将词汇按照词法顺序进行组织,因此以空格字符(' ')开头的 n-grams 位于前面。一旦向量化器知道需要计数的标记,它就可以将文本字符串转换为向量,其中每个字符 n-gram 词汇表中的标记都有一个维度。

>>> vectors = vectorizer.transform(texts)
>>> df = pd.DataFrame(
...     vectors.todense(),
...     columns=vectorizer.vocabulary_)
>>> df.index = [t[:8] + '...' for t in texts]
>>> df = df.T
>>> df['total'] = df.T.sum()
>>> df
    Trust me...  There's ... total
 t           31      14          45
 r            3       2           5
 u            1       0           1
 s            0       1           1
              3       0           3
..           ...     ...        ...
at            1       0           1
ma            2       1           3
yb            1       0           1
be            1       0           1
e.            0       1           1
<BLANKLINE>
[148 rows x 3 columns]

数据框中每个句子都包含一列,每个字符 2-gram 都包含一行。看看前四行,其中字节对(字符 2-gram)" a" 在这两个句子中出现了五次。因此,即使在构建 BPE 分词器时,空格也会计入"字符"。这是 BPE 的优点之一,它将找出您的标记分隔符是什么,所以它甚至可以在没有单词之间有空格的语言中工作。而且 BPE 可以在 ROT13 这样的代换密码文本上起作用,ROT13 是一种玩具密码,将字母向前旋转 13 个字符。

>>> df.sort_values('total').tail()
        Trust me...  There's ... total
    en        10           3       13
    an        14           5       19
    uc        11           9       20
    e         18           8       26
    t         31          14       45

然后,BPE 分词器会找到最常见的 2-grams 并将其添加到永久词汇表中。随着时间的推移,它会删除较不常见的字符对,因为它们不太可能再次在文本中频繁出现。

>>> df['n'] = [len(tok) for tok in vectorizer.vocabulary_]
>>> df[df['n'] > 1].sort_values('total').tail()
    Trust me...  There's ... total n
ur           8          4            12  2
en          10          3            13  2
an          14          5            19  2
uc          11          9            20  2
e           18          8            26  2

所以 BPE 分词器中的下一轮预处理将保留字符 2-grams “en” 和 “an”,甚至 " t" 和 “e”。然后,BPE 算法将使用这个较小的字符 bigram 词汇再次通过文本。它会寻找这些字符 bigrams 与彼此和单个字符的频繁配对。这个过程将继续,直到达到最大标记数,并且最长的可能字符序列已经被纳入词汇表。

注意

你可能会看到关于wordpiece分词器的提及,它被用在一些高级语言模型中,如 BERT 及其派生版本。它的工作方式与 BPE 相同,但它实际上使用底层语言模型来预测字符串中的相邻字符。它会从词汇表中消除对这个语言模型准确性影响最小的字符。数学上有些微的差异,产生了略有不同的标记词汇表,但您不需要故意选择这个分词器。使用它的模型将在其流水线中内置它。

基于 BPE 的分词器的一个重大挑战是它们必须在您的个人语料库上进行训练。因此,BPE 分词器通常仅用于您将在第九章学习的变形金刚和大型语言模型(LLM)。

BPE 分词器的另一个挑战是您需要进行的所有簿记,以跟踪每个训练过的分词器与您训练过的每个模型对应。这是 Huggingface 的一项重大创新之一。他们简化了存储和共享所有预处理数据的过程,例如分词器词汇表,与语言模型一起。这使得重复使用和共享 BPE 分词器变得更容易。如果你想成为一个 NLP 专家,你可能想模仿 HuggingFace 在你自己的 NLP 预处理流水线中所做的事情。

2.6 标记的向量

现在你已经把你的文本分解成了有意义的标记,你会怎么处理它们呢?你怎样才能把它们转换成对机器有意义的数字?最简单、最基本的事情是检测你感兴趣的特定标记是否存在。你可以硬编码逻辑来检查重要标记,称为关键词

这对于你的问候意图识别器可能效果很好。我们在第一章末尾的问候意图识别器寻找文本字符串开头的词语,如 “Hi” 和 “Hello”。你的新标记化文本将帮助你检测诸如 “Hi” 和 “Hello” 等词语的存在或缺失,而不会因为 “Hiking” 和 “Hell” 等词语而混淆。有了你的新分词器,你的 NLP 流水线将不会把单词 “Hiking” 错误地解释为问候 “Hi king”。

>>> hi_text = 'Hiking home now'
>>> hi_text.startswith('Hi')
True
>>> pattern = r'\w+(?:\'\w+)?|[^\w\s]'  # #1
>>> 'Hi' in re.findall(pattern, hi_text)  # #2
False
>>> 'Hi' == re.findall(pattern, hi_text)[0]  # #3
False

因此,标记化可以帮助您减少简单意图识别管道中的假阳性数量,该管道寻找问候词的存在。这通常被称为关键词检测,因为您的词汇表限于您认为重要的一组词汇。但是,想到可能出现在问候语中的所有单词,包括俚语、拼写错误和错别字,这相当繁琐。创建一个 for 循环来遍历它们将是低效的。我们可以使用线性代数的数学和numpy的向量化操作来加速此过程。

为了高效地检测标记,您将需要使用三种新技巧:

  1. 文档的矩阵和向量表示
  2. numpy 中的向量化操作
  3. 离散向量的索引

您将首先学习将单词表示为矩阵的最基本、直接、原始和无损的方法,即单独独热编码。

2.6.1 单独独热向量

现在您已成功将文档分割成所需的单词类型,您可以准备将它们转换成向量。数字向量是我们在自然语言文本上进行数学或处理所需要的。

>>> import pandas as pd
>>> onehot_vectors = np.zeros(
...     (len(tokens), vocab_size), int)  # #1
>>> for i, tok in enumerate(tokens):
...     if tok not in vocab:
...         continue
...     onehot_vectors[i, vocab.index(tok)] = 1  # #2
>>> df_onehot = pd.DataFrame(onehot_vectors, columns=vocab)
>>> df_onehot.shape
(18, 15)
>>> df_onehot.iloc[:,:8].replace(0, '')  # #3
    ,  .  Survival  There's adequate as fittest maybe
0                       1
1
2
3
4                                   1
5
6
7
8                                           1
9      1
10              1
11
12
13
14                               1
15  1
16                                                1
17     1

在这个表示两句引用的表格中,每一行都是文本中单个词的向量表示。该表格有 15 列,因为这是您的词汇表中唯一单词的数量。该表格有 18 行,每个单词在文档中占据一行。列中的“1”表示该位置在文档中存在一个词汇单词。

您可以从上到下“阅读”一个独热编码(向量化)的文本。您可以看到文本中的第一个词是“有”,因为第一行下面的1位于“有”的列标签下。接下来的三行(行索引 1、2 和 3)为空白,因为我们在右边截断了表格以便它适合在页面上显示。文本的第五行,带有偏移索引号4,向我们展示了文本中的第五个词是“足够”,因为在该列中有一个1

单独独热向量非常稀疏,每个行向量中只包含一个非零值。为了显示,此代码将0替换为空字符串(’'),以便更容易阅读。但是该代码实际上并没有改变您在 NLP 管道中处理的数据的DataFrame`。上面的 Python 代码只是为了让阅读更轻松,这样您就可以看到它看起来有点像播放器钢琴卷轴,或者可能是音乐盒鼓。

Pandas 的DataFrame使得这个输出稍微容易阅读和解释。DataFrame.columns跟踪每列的标签。这使您可以使用字符串为表格中的每一列标记,例如其代表的标记或单词。DataFrame还可以在DataFrame.index中跟踪每行的标签,以便快速查找。

重要提示

不要向任何你打算在你的机器学习流水线中使用的DataFrame添加字符串。标记器和向量化器的目的,比如这个一热向量化器,是创建一个你的 NLP 流水线可以对其进行数学运算的数值数组。你不能对字符串进行数学运算。

表的每一行都是一个二进制行向量,你可以看到为什么它也被称为一热向量:除了一行中的一个位置(列)之外,其他位置都是0或空白。只有一个列,或者向量中的一个位置是“热”的(1)。一个1表示打开,或热。一个0表示关闭,或缺失。

使用这种词向量表示和文档表格表示的一个很好的特点是没有信息丢失。令人满意的是,令牌的确切序列以表示文档的一热向量的顺序编码在表中。只要你记住哪些词由哪列表示,你就可以完美地从这个一热向量表中重构出原始的令牌序列。即使你的标记器只有 90%的准确率生成你认为有用的令牌,这个重构过程也是 100%准确的。因此,像这样的一热词向量通常用于神经网络、序列到序列语言模型和生成语言模型。它们是任何需要保留原始文本中所有含义的模型或 NLP 流水线的良好选择。

提示

一热编码器(向量化器)没有从文本中丢弃任何信息,但我们的标记器丢弃了。我们的正则表达式标记器丢弃了有时出现在单词之间的空白字符(\s)。因此,你不能用一个解标记器完美地重构原始文本。然而,像 spaCy 这样的标记器跟踪这些空格字符,并且实际上可以完美地解标记一个令牌序列。spaCy 是因为准确高效地和准确地记录空格的这一特性而命名的。

这一系列一热向量就像原始文本的数字录音。如果你看得够仔细,你可能会想象上面的一和零的矩阵是一个玩家钢琴卷。[²⁰] 或者它可能是音乐盒金属鼓上的凹痕。[²¹] 顶部的词汇表告诉机器在词序列或钢琴音乐中的每一行中播放哪个“音符”或单词,就像图 2.2 中一样。

图 2.2 玩家钢琴卷


与钢琴卷或音乐盒不同,你的机械词记录器和播放器只允许一次使用一个“手指”。它只能一次播放一个“音符”或一个词。它是一热的。而且单词之间的间距没有变化。

重要的是,你已经将自然语言单词的句子转换为一系列数字,或向量。现在,你可以让计算机像处理任何其他向量或数字列表一样阅读和对这些向量进行数学运算。这使得你的向量可以被输入到任何需要这种向量的自然语言处理流水线中。第 5 至 10 章的深度学习流水线通常需要这种表示形式,因为它们可以被设计为从这些原始文本表示中提取意义的“特征”。而且深度学习流水线可以从意义的数值表示生成文本。因此,从后面章节的你的自然语言生成流水线中流出的单词流通常将被表示为一系列 one-hot 编码的向量,就像自动钢琴可能为西部世界中不那么人工的观众演奏一首歌一样。

现在,你所需要做的就是想出如何构建一个能够理解并以新的方式组合这些词向量的“自动钢琴”。最终,你希望你的聊天机器人或自然语言处理流水线能为我们演奏一首歌,或者说出一些你之前没听过的话。在第九章和第十章,当你学习到适用于像这样的 one-hot 编码令牌序列的递归神经网络时,你将有机会做到这一点。

这种用 one-hot 词向量表示的句子保留了原始句子的所有细节、语法和顺序。而且你已经成功地将单词转换为计算机能“理解”的数字。它们也是一种计算机非常喜欢的特殊类型的数字:二进制数字。但对于一个简短的句子来说,这是一个很大的表格。如果你仔细想一想,你已经扩展了存储文档所需的文件大小。对于一个长文档来说,这可能不实用。

你的文档集的这种无损数值表示有多大?你的词汇量(向量长度)会变得非常庞大。英语至少包含 20,000 个常见单词,如果包括名称和其他专有名词,则有数百万个。而且你的 one-hot 向量表示需要为你想要处理的每个文档创建一个新的表格(矩阵)。这几乎就像你的文档的原始“图像”。如果你做过任何图像处理,你就知道如果想从数据中提取有用信息,你需要进行维度缩减。

让我们通过一些数学来让你了解一下这些“钢琴卷”有多么庞大和难以控制。在大多数情况下,你在自然语言处理流水线中使用的标记词汇表将远远超过 10,000 或 20,000 个标记。有时甚至可以是数十万甚至数百万个标记。假设你的自然语言处理流水线词汇表中有一百万个标记。然后假设你有 3000 本书,每本书有 3500 个句子,每个句子有 15 个单词——这些都是短书的合理平均值。那就是很多很大的表(矩阵),每本书一个。那将使用 157.5TB。你可能甚至无法把它存储到磁盘上。

哪怕你非常高效,并且在矩阵中每个数字只使用一个字节,那也是超过一百万亿字节。以每个单元一个字节的计算,你需要将近 20TB 的存储空间来存放用这种方式处理的一小本书架上的书籍。幸运的是,你不会永久使用这种数据结构来存储文档。你只是在处理文档时临时使用它,存储在内存中,一次处理一个单词。

因此,存储所有这些零,并记录所有文档中单词的顺序并没有太多意义。这不实际。也不是很有用。你的数据结构没有从自然语言文本中抽象或泛化出来。这样的自然语言处理流水线在现实世界中并没有做任何真正的特征提取或维度缩减,以帮助你的机器学习在实际情况下运行良好。

你真正想做的是将文档的含义压缩到其本质中。你想将文档压缩成一个单一的向量,而不是一个大表。而且你愿意放弃完美的“回忆”。你只是想捕捉文档中的大部分含义(信息),而不是全部。

你的正则表达式分词器和独热向量对于创建反向索引非常有效。就像教科书末尾的索引一样,你的独热向量矩阵可以用来快速找到所有至少使用过一次特定单词的字符串或文档。因此,到目前为止你学到的工具可以作为个性化搜索引擎的基础。然而,你看到了搜索和信息检索只是自然语言处理的许多许多应用之一。要解决更高级的问题,你将需要更高级的分词器和更复杂的文本向量表示。Python 包spaCy就是你在寻找的最先进的分词器。

2.6.2 SpaCy

也许您不希望您的正则表达式标记器将缩写词保持在一起。也许您想要将单词"isn’t"识别为两个单独的单词,“is"和"n’t”。这样,您可以将"n’t"和"not"这两个同义词合并为一个标记。这样,您的 NLP 管道就能理解"the ice cream isn’t bad"与"the ice cream is not bad"的含义相同。对于一些应用程序,例如全文搜索、意图识别和情感分析,您希望能够展开或扩展这样的缩写词。通过拆分缩写词,您可以使用同义词替换或扩展缩写词来提高搜索引擎的召回率和情感分析的准确性。

重要提示

我们将在本章后面讨论大小写折叠、词干提取、词形还原和同义词替换。在应用程序(如作者归因、风格转移或文本指纹)中使用这些技术时要小心。您希望您的作者归因或风格转移管道保持忠于作者的写作风格和他们使用的确切拼写。

SpaCy 将标记器直接集成到其最先进的 NLU 管道中。实际上,“spaCy"这个名字是基于单词"space”,就像西方语言中用于分隔单词的分隔符一样。而且,spaCy 在应用规则拆分标记的同时,还向标记添加了许多其他标签。因此,spaCy 通常是您唯一需要使用的第一个和最后一个标记器。

让我们看看 SpaCy 如何处理我们的一系列深思者引用。首先,您将使用一个对 spacy.load 函数的薄包装器,以便您的 NLP 管道是幂等的。幂等的管道可以多次运行,并每次都达到相同的结果:

>>> import spacy  # #1
>>> from nlpia2.spacy_language_model import load  # #2
>>> nlp = load('en_core_web_sm')  # #3
>>> nlp
<spacy.lang.en.English...>

现在您已经下载了小型的 SpaCy 语言模型并将其加载到内存(RAM)中,您可以使用它来标记和标记任何文本字符串。这将创建一个新的 SpaCy Doc对象,其中包含 SpaCy 使用该语言模型理解文本的内容。

[source,python]
>>> doc = nlp(texts[-1])
>>> type(doc)
<class 'spacy.tokens.doc.Doc'>

SpaCy 已经阅读并解析了您的文本,将其分割为标记。Doc对象包含一系列Token对象,每个对象应该是一个小的思想或含义包(通常是单词)。看看这些标记是否是您预期的:

>>> tokens = [tok.text for tok in doc]  # #1
>>> tokens[:9]
['There', "'s", 'no', 'such', 'thing', 'as', 'survival', 'of', 'the']
>>> tokens[9:17]
['fittest', '.', 'Survival', 'of', 'the', 'most', 'adequate', ',']

Spacy 在幕后做的远不止将您的文本分割成标记。您下载的那个小语言模型还通过一些句子边界检测规则来识别句子分隔符。语言模型是一组正则表达式和有限状态自动机(规则)。这些规则很像你在英语课上学到的语法和拼写规则。它们用于将单词标记为有用的东西,如它们的词性和它们在单词之间的语法树关系中的位置。再仔细看看那个句子图表

>>> from spacy import displacy
>>> sentence = list(doc.sents)[0]  # #1
>>> # displacy.serve(sentence, style="dep") # #2
>>> # !firefox 127.0.0.1:5000
>>> displacy.render(sentence, style="dep")

图表应该内联显示在 jupyter 笔记本中,或者如果在ipythonjupyter-console)中运行此代码,则应在单独的窗口中显示。如果启动了 displacy web 服务器,您可以通过在端口 5000 上浏览到 localhost(127.0.0.1)(127.0.0.1:5000)来查看图表。您应该看到一个句子图表,这可能比您在学校能做的更正确:


你可以看到,spaCy 做的远不止将文本分割成令牌那么简单。它会识别句子边界,自动将文本分段为句子。它还使用各种属性标记令牌,例如它们的词性(PoS)甚至它们在句子语法中的角色。你可以在displacy下看到显示的词元。^([23]) 本章后面你将了解词形还原、大小写折叠和其他词汇压缩方法对某些应用有何帮助。

因此,从准确性和一些“内置电池”功能(例如词元和依赖项的所有令牌标记)来看,spaCy 似乎相当不错。速度如何?

2.6.3 令牌化竞赛

SpaCy 可以在大约 5 秒钟内解析本书一章的 AsciiDoc 文本。首先下载本章的 AsciiDoc 文本文件:

>>> import requests
>>> text = requests.get('https://proai.org/nlpia2-ch2.adoc').text
>>> f'{round(len(text) / 10_000)}0k'  # #1
'170k'

在我写下您正在阅读的这个句子的 AsciiDoc 文本文件中,大约有 170,000 个 Unicode 字符。从词数每秒来看,这意味着什么,这是令牌化速度的标准基准?

>>> from nlpia2.spacy_language_model import load
>>> nlp = load('en_core_web_sm')
>>> %timeit nlp(text)  # #1
4.67 s ± 45.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
>>> f'{round(len(text) / 10_000)}0k'
'170k'
>>> doc = nlp(text)
>>> f'{round(len(list(doc)) / 10_000)}0k'
'40k'
>>> f'{round(len(doc) / 1_000 / 4.67)}kWPS'  # #2
'8kWPS'

对于大约 15 万个字符或英语和 Python 文本中的约 34,000 个单词,或每秒约 7,000 个单词的标准,这几乎需要 5 秒钟。

对于您的个人项目,这可能已经足够快了。但是在商业业务的典型医疗记录摘要项目中,您可能需要每分钟处理数百个大型文档。这是每秒处理几个文档。如果每个文档包含本书中的文本量(近一百万个令牌),那么每秒几乎可以达到一百万个令牌。医疗记录摘要管道的延迟可以是项目的关键指标。例如,在一个项目中,使用 SpaCy 作为分词器处理 10,000 份患者医疗记录花费了 5 天的时间。

如果您需要加速您的令牌化器,一种选择是禁用 spaCy 管道中您的应用程序不需要的标记功能:

>>> nlp.pipe_names  # #1
['tok2vec', 'tagger', 'parser', 'attribute_ruler', 'lemmatizer', 'ner']
>>> nlp = load('en_core_web_sm', disable=['tok2vec', 'tagger', 'parser'])
>>> nlp.pipe_names
['attribute_ruler', 'lemmatizer', 'ner']
>>> %timeit nlp(text)
199 ms ± 6.63 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

您可以禁用不需要的管道元素以加速分词器:

  • tok2vec: 词嵌入
  • tagger: 词性(.pos.pos_
  • parser: 语法树角色
  • attribute_ruler: 精细的词性和其他标记
  • lemmatizer: 词元标记器
  • ner: 命名实体识别标记器

在令牌化器速度比较中,NLTK 的word_tokenize方法通常用作速度的基准:

>>> import nltk
>>> nltk.download('punkt')
True
>>> from nltk.tokenize import word_tokenize
>>> %timeit word_tokenize(text)
156 ms ± 1.01 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
>>> tokens = word_tokenize(text)
>>> f'{round(len(tokens) / 10_000)}0k'
'30k'

你难道发现了分词器竞赛的赢家吗?不要太快。你的正则表达式分词器有一些非常简单的规则,因此也应该运行得非常快:

>>> pattern = r'\w+(?:\'\w+)?|[^\w\s]'
>>> tokens = re.findall(pattern, text)  # #1
>>> f'{round(len(tokens) / 10_000)}0k'
'40k'
>>> %timeit re.findall(pattern, text)
8.77 ms ± 29.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

现在这不太令人惊讶。Python 在低级别的 C 例程中编译和运行正则表达式非常高效。除了速度外,正则表达式和 NLTK 令牌化器经常用于学术论文。这帮助像你这样的人精确复现他们的结果。因此,如果你尝试复制别人的工作,请确保使用他们的分词器,无论是 NLTK、正则表达式还是 spaCy 的特定版本。在本书中,你只是尝试学习事物的工作原理,所以我们没有费心追踪我们使用的 spaCy 和 NLTK 的特定版本。但是如果你正在与其他进行 NLP 研究的人分享你的结果,你可能需要为自己做这些事情。

提示

当速度比准确度更重要或其他人将尝试复制你的结果时,请使用正则表达式分词器。如果你不需要 spaCy 提供的额外标记,你的分词器不需要浪费时间来处理单词的语法和含义来创建这些标记。每次使用reregex包中的正则表达式时,它的已编译和优化版本都会缓存在 RAM 中。因此,通常没有必要使用re.compile()预编译你的正则表达式。

自然语言处理实战第二版(MEAP)(一)(3)https://developer.aliyun.com/article/1517828

相关文章
|
3月前
|
机器学习/深度学习 人工智能 自然语言处理
Python自然语言处理实战:文本分类与情感分析
本文探讨了自然语言处理中的文本分类和情感分析技术,阐述了基本概念、流程,并通过Python示例展示了Scikit-learn和transformers库的应用。面对多义性理解等挑战,研究者正探索跨域适应、上下文理解和多模态融合等方法。随着深度学习的发展,这些技术将持续推动人机交互的进步。
118 1
|
3月前
|
自然语言处理 监控 数据挖掘
|
2月前
|
人工智能 自然语言处理 Java
Java中的自然语言处理应用实战
Java中的自然语言处理应用实战
|
3月前
|
机器学习/深度学习 数据采集 人工智能
Python 高级实战:基于自然语言处理的情感分析系统
**摘要:** 本文介绍了基于Python的情感分析系统,涵盖了从数据准备到模型构建的全过程。首先,讲解了如何安装Python及必需的NLP库,如nltk、sklearn、pandas和matplotlib。接着,通过抓取IMDb电影评论数据并进行预处理,构建情感分析模型。文中使用了VADER库进行基本的情感分类,并展示了如何使用`LogisticRegression`构建机器学习模型以提高分析精度。最后,提到了如何将模型部署为实时Web服务。本文旨在帮助读者提升在NLP和情感分析领域的实践技能。
91 0
|
4月前
|
自然语言处理 API 数据库
自然语言处理实战第二版(MEAP)(六)(5)
自然语言处理实战第二版(MEAP)(六)
43 3
|
4月前
|
机器学习/深度学习 自然语言处理 机器人
自然语言处理实战第二版(MEAP)(六)(4)
自然语言处理实战第二版(MEAP)(六)
37 2
|
4月前
|
机器学习/深度学习 人工智能 自然语言处理
自然语言处理实战第二版(MEAP)(六)(2)
自然语言处理实战第二版(MEAP)(六)
36 2
|
4月前
|
机器学习/深度学习 自然语言处理 机器人
自然语言处理实战第二版(MEAP)(六)(3)
自然语言处理实战第二版(MEAP)(六)
53 1
|
3月前
|
机器学习/深度学习 自然语言处理 PyTorch
【从零开始学习深度学习】48.Pytorch_NLP实战案例:如何使用预训练的词向量模型求近义词和类比词
【从零开始学习深度学习】48.Pytorch_NLP实战案例:如何使用预训练的词向量模型求近义词和类比词
|
1天前
|
机器学习/深度学习 人工智能 自然语言处理
AI技术在自然语言处理中的应用
【9月更文挑战第17天】本文主要介绍了AI技术在自然语言处理(NLP)领域的应用,包括文本分类、情感分析、机器翻译和语音识别等方面。通过实例展示了AI技术如何帮助解决NLP中的挑战性问题,并讨论了未来发展趋势。

热门文章

最新文章