自然语言处理实战第二版(MEAP)(一)(2)https://developer.aliyun.com/article/1517825
2.7 单词片段分词器
对于单词来说,将其视为不可分割的原子意义和思想是很自然的。然而,你可能会发现一些单词并不在空格或标点符号上清晰划分。而且,许多复合词或专有名词内部有空格,如果想要它们保持在一起,需要更深入地研究单词的统计特征。考虑如何通过邻近的字符来构建单词,而不是在分隔符,如空格和标点符号处切割文本。
2.7.1 按块组合字符成句子片段
与其考虑将字符串分解为标记,不如让你的分词器寻找紧密相邻使用的字符,比如"i"在"e"的前面。你可以组合起来属于一起的字符和字符序列。这些字符簇可以成为你的标记。NLP 管道只关注标记的统计信息。希望这些统计信息能符合我们对单词的期望。
这些字符序列中的许多将是完整的单词,甚至是复合词,但许多将是单词的部分。事实上,所有子词标记器都在词汇表中为您的每个单词的每个字符保留一个标记。这意味着只要新的文本不包含它之前没有见过的新字符,它就永远不需要使用一个 OOV(词汇外)标记。子词标记器尝试将字符最佳地聚集在一起以创建令牌。利用字符 n-gram 计数的统计数据,这些算法可以识别出可作为令牌的单词片段,甚至是句子片段。
通过将字符聚集在一起来识别单词可能看起来很奇怪。但对于机器来说,文本中意义元素之间唯一明显、一致的分界线就是字节或字符之间的边界。字符频繁一起使用的频率可以帮助机器识别与子词标记(例如单个音节或复合词的部分)相关联的含义。
在英语中,即使是单个字母也带有微妙的情感(情绪)和含义(语义)。然而,在英语中只有 26 个独特的字母。这并不给单个字母在任何一个主题或情感上专门化留下空间。尽管如此,精明的营销人员知道,有些字母比其他字母更酷。品牌将尝试通过选择带有像“Q”和“X”或“Z”这样的异国字母的名称来展示自己技术先进。这也有助于 SEO(搜索引擎优化),因为较少见的字母在可能的公司和产品名称中更容易被发现。你的 NLP 管道将捕捉到所有这些含义、内涵和意图的暗示。你的标记计数器将为机器提供其需要推断经常一起使用的字母簇的含义的统计数据。
子词标记器唯一的劣势是它们必须多次通过您的文本语料库,才能收敛到最佳词汇表和分词器。子词标记器必须像 CountVectorizer 一样被训练或适合您的文本。事实上,在下一节中,您将使用 CountVectorizer 来了解子词标记器的工作原理。
子词分词有两种主要方法:BPE(字节对编码)和 Wordpiece 分词。
BPE
在本书的上一版中,我们坚持认为单词是您需要考虑的英语中最小的含义单位。随着 Transformer 和其他使用 BPE 和类似技术的深度学习模型的兴起,我们改变了主意。基于字符的子词标记器已被证明对于大多数 NLP 问题更加灵活和强大。通过从 Unicode 多字节字符的构建块中构建词汇表,您可以构建一个可以处理您将要看到的每一个自然语言字符串的词汇表,词汇表中的令牌数量只需为 50,000 个即可。
你可能认为 Unicode 字符是自然语言文本中包含意义的最小单位。对于人类来说可能是这样,但对于机器来说绝对不是。正如 BPE 的名称所暗示的,字符不必是你的 基本词汇 的基本意义单位。你可以将字符分割成 8 位字节。GPT-2 使用字节级 BPE 分词器从组成它们的字节中自然组合出所有你需要的 Unicode 字符。虽然在基于字节的词汇表中处理 Unicode 标点需要一些特殊规则,但不需要对基于字符的 BPE 算法进行其他调整。字节级 BPE 分词器允许你用 256 个最小令牌的基础词汇大小来表示所有可能的文本。GPT-2 模型可以通过其默认的由 50,000 个多字节 合并令牌 加上 256 个个别字节令牌组成的 BPE 词汇表实现最先进的性能。
您可以将 BPE (Byte Pair Encoding) 分词器算法看作是社交网络中的媒人。BPE 将经常出现在一起且看起来是“朋友”的字符配对在一起。然后为这些字符组合创建一个新的标记。BPE 可以在文本中经常使用这些标记对时,将多字符标记配对。并且会一直这样做,直到在您的词汇限制大小中允许的频繁使用的字符序列有很多。
BPE 正在改变我们对自然语言标记的看法。自然语言处理工程师终于让数据说话。在构建自然语言处理流程时,统计思维比人类直觉要好。机器可以看到 大多数 人如何使用语言。你只对你使用特定单词或音节时的含义熟悉。Transformers 现在已经在某些自然语言理解和生成任务上超越了人类读者和作者,包括在子词标记中找到含义。
你尚未遇到的一个复杂情况是在遇到新词时该做何选择的困境。在先前的例子中,我们只是不断地将新词加入到我们的词汇表中。但在现实世界中,你的流程将会在一个初始文档语料库上进行训练,这个语料库可能或可能不代表它将来可能看到的所有类型的标记。如果你的初始语料库缺少后来遇到的一些单词,那么你将没有一个插槽来放置那个新单词的计数。因此,在训练初始流程时,你将始终保留一个插槽 (维度) 来存放你的 词汇外 (OOV) 标记的计数。因此,如果你最初的文档集不包含女孩名为"Aphra",那么对名为 Aphra 的所有计数将被汇总到 OOV 维度中,作为 Amandine 和其他罕见单词的计数。
要在您的向量空间中给予 Aphra 平等的表示,您可以使用 BPE。BPE 将罕见的单词分解为更小的片段,以在语料库中为自然语言创建一个元素的周期表。所以,因为“aphr”是一个常见的英语前缀,您的 BPE 分词器可能会为她的词汇中的计数提供两个插槽:一个用于“aphr”和一个用于“a”。实际上,您可能会发现词汇槽位是“ aphr”和“a”,因为 BPE 对待空格与字母表中的任何其他字符没有任何区别。
BPE 为您提供了处理希伯来语名称(如 Aphra)的多语言灵活性。它还使您的管道对常见的拼写错误和打字错误具有健壮性,例如“aphradesiac”。每个词,包括诸如“African American”之类的少数 2-gram,在 BPE 的投票系统中都有代表。过去使用 OOV(词汇外)令牌来处理人类沟通的罕见怪癖的日子一去不复返。由于这个原因,像 transformers 这样的最先进的深度学习 NLP 管道都使用类似于 BPE 的词片分词技术。
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-gram)以及单个字符令牌。这些是 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-gram 和 2-gram。而且CountVectorizer
按词法顺序组织词汇表,因此以空格字符(' '
)开头的 n-gram 首先出现。一旦向量化器知道它需要能够计数的令牌是什么,它就可以将文本字符串转换为向量,每个字符 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 ...
DataFrame 包含每个句子的一列和每个字符 2-gram 的一行。查看前四行,其中字节对(字符 2-gram)“ a”的出现次数在这两个句子中出现了五次。所以即使在构建 BPE 分词器时,空格也算作“字符”。这是 BPE 的优点之一,它会弄清楚您的令牌分隔符是什么,因此即使在没有单词之间有空格的语言中,它也会起作用。而且 BPE 将适用于替换密码文本,如 ROT13,这是一个将字母表向前旋转 13 个字符的玩具密码。
>>> df.sort_values('total').tail(3) Trust me... There's ... total uc 11 9 20 e 18 8 26 t 31 14 45
然后,BPE 分词器会找到最常见的 2 元组并将它们添加到永久词汇表中。随着时间的推移,它会删除较不常见的字符对,因为它读取文本越深入,那些稀有的字符对在文本末尾之前出现的可能性就越小。对于熟悉统计学的人来说,它使用贝叶斯模型对您的文本进行建模,不断更新对标记频率的先验预测。
>>> df['n'] = [len(tok) for tok in vectorizer.vocabulary_] >>> df[df['n'] > 1].sort_values('total').tail() Trust me... There's ... total n c 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 元组"en"和"an"甚至是" c"和"e"。然后,BPE 算法将使用这个较小的字符二元组词汇再次遍历文本。它将寻找这些字符二元组彼此之间以及单个字符的频繁配对。这个过程将持续进行,直到达到最大标记数,并且最长的可能字符序列已经被纳入词汇表。
注意
您可能会看到关于wordpiece分词器的提及,它在某些高级语言模型中使用,例如BERT
及其衍生产品。它的工作方式与 BPE 相同,但实际上使用底层语言模型来预测字符串中的相邻字符。它会消除对语言模型精度影响最小的字符。数学上有微妙的差异,产生微妙不同的标记词汇表,但您无需刻意选择此分词器。使用它的模型将在其管道中内置。
BPE 分词器的一个重大挑战是必须针对您的个体语料库进行训练。因此,BPE 分词器通常仅用于 Transformer 和大型语言模型(LLM),您将在第九章学习到这些内容。
另一个 BPE 分词器的挑战是您需要进行大量的簿记工作,以跟踪每个已训练的分词器与您已训练的模型之间的对应关系。这是 Huggingface 的一项重大创新之一。他们简化了存储和共享所有预处理数据的过程,例如分词器词汇表,以及语言模型。这使得重用和共享 BPE 分词器变得更加容易。如果您想成为自然语言处理专家,您可能希望模仿 HuggingFace 的做法,使用自己的 NLP 预处理流程。
2.8 标记的向量
现在,您已经将文本分解为有意义的标记,接下来该怎么处理呢?如何将它们转换为机器可理解的数字?最简单最基本的做法是检测您感兴趣的特定标记是否存在。您可以硬编码逻辑来检查重要的标记,称为关键词。
这可能对第一章中的问候意图识别器很有效。我们在第一章末尾的问候意图识别器寻找了像“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
因此,标记化可以帮助你减少在简单意图识别管道中的假阳性数量,该管道用于寻找问候词的存在。这通常被称为关键词检测,因为你的词汇表限于你认为重要的一组词。然而,必须考虑到所有可能出现在问候语中的单词,包括俚语、拼写错误和打字错误,这是相当麻烦的。创建一个循环来迭代所有这些单词将是低效的。我们可以利用线性代数的数学和numpy
的向量化操作来加速这个过程。
为了有效地检测标记,您将想要使用三个新技巧:
- 文档的矩阵和向量表示
- numpy 中的向量化操作
- 离散向量的索引
你将首先学习最基本、直接、原始和无损的表示单词的矩阵方式,即一热编码。
2.8.1 词袋(Bag-of-Words)向量
是否有办法将所有那些自动钢琴乐谱挤入一个单一的向量中?向量是表示任何对象的好方法。通过向量,我们可以通过检查它们之间的欧几里得距离来比较文档。向量允许我们在自然语言上使用所有的线性代数工具。这确实是 NLP 的目标,对文本进行数学处理。
让我们假设您可以忽略我们文本中单词的顺序。对于文本的向量表示的第一次尝试,您可以将它们全部混合在一起形成一个“袋子”,每个句子或短文档一个袋子。事实证明,只知道一个文档中存在哪些单词就能给你的 NLU 管道提供很多关于文档内容的信息。事实上,这是大型互联网搜索引擎公司使用的表示方法。即使对于几页长的文档,词袋向量也有助于概括文档的本质。
让我们看看当我们混淆和计算《偷书贼》中文本中的单词时会发生什么:
>>> bow = sorted(set(re.findall(pattern, text))) >>> bow[:9] [',', '.', 'Liesel', 'Trust', 'and', 'arrived', 'clouds', 'hands', 'her'] >>> bow[9:19] ['hold', 'in', 'like', 'me', 'on', 'out', 'rain', 'she', 'the', 'their'] >>> bow[19:27] ['them', 'they', 'though', 'way', 'were', 'when', 'words', 'would']
即使使用这种杂乱的词袋,你也可以大致感受到这个句子是关于:“信任”,“词语”,“云”,“雨”,和一个名叫“丽莎尔”的人。你可能会注意到一件事,那就是 Python 的sorted()
将标点符号放在字符之前,将大写字母放在小写字母之前。这是 ASCII 和 Unicode 字符集中字符的顺序。然而,你的词汇表的顺序并不重要。只要你在所有这样标记化的文档中保持一致,机器学习管道将可以同样有效地使用任何词汇顺序。
你可以使用这种新的词袋向量方法,将每个文档的信息内容压缩到一个更易于处理的数据结构中。对于关键词搜索,你可以将你的独热词向量从播放钢琴卷轴表示中OR成一个二进制词袋向量。在播放钢琴的类比中,这就像同时演奏几个旋律音符,以创建一个“和弦”。与在你的 NLU 管道中逐个“重播”它们不同,你会为每个文档创建一个单一的词袋向量。
你可以使用这个单一向量来表示整个文档。因为向量都需要是相同长度的,你的词袋向量需要和你的词汇量大小一样长,即你文档中唯一标记的数量。你可以忽略很多不作为搜索词或关键词的词。这就是为什么在进行词袋标记化时通常忽略停用词。这对于搜索引擎索引或信息检索系统的第一过滤器来说是一个极其高效的表示。搜索索引只需要知道每个单词在每个文档中的存在与否,以帮助你以后找到这些文档。
这种方法原来对帮助机器“理解”一组单词作为一个单一的数学对象是至关重要的。如果你将你的标记限制为最重要的 1 万个单词,你可以将你对虚构的 3500 句子书的数字表示压缩到 10 千字节,或者对于你的虚构的 3000 本书的语料库,大约是 30 兆字节。对于这样一个规模适中的语料库,独热向量序列将需要数百吉字节。
文本的 BOW 表示的另一个优点是它允许你在常数时间(O(1)
)内在你的语料库中找到相似的文档。你找不到比这更快的方法了。BOW 向量是实现这种速度的反向索引的前身。在计算机科学和软件工程中,你总是在寻找能够实现这种速度的数据结构。所有主要的全文搜索工具都使用 BOW 向量来快速找到你需要的内容。你可以在 EllasticSearch、Solr,[32] PostgreSQL 以及最先进的网络搜索引擎(例如 Qwant,[33]],SearX,[34],以及 Wolfram Alpha[35]])中看到自然语言的这种数值表示。
幸运的是,在给定的任何一段文本中,词汇表中的单词很少被使用。对于大多数词袋(bag-of-words)应用程序,我们保持文档简短,有时只需要一句话就足够。因此,与一次性击打钢琴上的所有音符不同,你的词袋向量更像是一个广泛而愉悦的钢琴和弦,是一组能很好地合作并带有意义的音符(单词)的组合。即使在同一语句中有一些不常用在一起的词(“不和谐”,即奇怪的用词),你的自然语言生成流水线或聊天机器人也可以处理这些和弦。甚至“不和谐”(奇怪的用词)也是关于一种陈述的有用信息,可以被机器学习流水线利用起来。
下面是如何将标记放入二进制向量中,指示特定句子中是否存在某个单词。这组句子的向量表示可以被“索引”,以指示哪些词语在哪个文档中被使用。这个索引类似于你在许多教科书末尾找到的索引,只不过它不是跟踪单词出现在哪一页上,而是跟踪它出现在哪个句子(或相关向量)中。然而教科书索引通常只关心与书的主题相关的重要单词,而你却跟踪每一个单词(至少现在是这样)。
稀疏表示
或许你会想,如果你处理一个庞大的语料库,你可能最终会得到成千上万个甚至是数百万个在你的词汇表中的唯一标记。这意味着你需要在表示我们关于 Liesel 的 20 个标记的句子的向量表达中存储许多零。与向量相比,dict
使用的内存要少得多。将单词与它们的 0/1 值配对的任何映射都比向量更有效。但是你不能对 dict
进行数学运算。这就是为什么 CountVectorizer 使用稀疏的 numpy 数组来保存词在词频向量中的计数的原因。使用字典或稀疏数组作为向量可以确保只在词典中的数百万个可能单词之一出现在特定文档中时存储一个 1。
但是如果你想查看一个单独的向量以确保一切工作正常,那么 Pandas 的 Series
是最好的选择。然后你会将它包装在一个 Pandas DataFrame 中,这样你就可以向你的二进制向量“语料库”中添加更多的句子引用。
2.8.2 点积
在自然语言处理中你会经常用到点积,所以确保你理解它是什么。如果你已经能够在头脑中进行点积,请跳过本节。
点积也被称为内积,因为两个向量(每个向量中的元素数)或矩阵(第一个矩阵的行和第二个矩阵的列)的“内部”维度必须相同,因为这是产品发生的地方。这类似于两个关系数据库表的“内连接”。
点积也称为标量积,因为它产生一个标量值作为其输出。这有助于将其与叉积区分开来,后者产生一个向量作为其输出。显然,这些名称反映了正式数学符号中用于表示点积((\cdot))和叉积((\times))的形状。标量积输出的标量值可以通过将一个向量的所有元素与第二个向量的所有元素相乘,然后将这些普通乘积相加来计算。
这里是一个你可以在你的 Pythonic 头脑中运行的 Python 片段,以确保你理解什么是点积:
列表 2.2 示例点积计算
>>> v1 = np.array([1, 2, 3]) >>> v2 = np.array([2, 3, 4]) >>> v1.dot(v2) 20 >>> (v1 * v2).sum() # #1 20 >>> sum([x1 * x2 for x1, x2 in zip(v1, v2)]) # #2 20
提示
点积等同于矩阵乘积,可以在 NumPy 中用np.matmul()
函数或@
运算符完成。由于所有向量都可以转换为 Nx1 或 1xN 矩阵,所以你可以通过转置第一个向量,使它们的内部维度对齐,像这样使用这个简写运算符在两个列向量(Nx1)上:v1.reshape-1, 1.T @ v2.reshape-1, 1
,这样就输出了你的标量积在一个 1x1 矩阵中:array([[20]])
这是自然语言文档(句子)的第一个向量空间模型。不仅可以进行点积,还可以对这些词袋向量进行其他向量操作:加法、减法、或运算、与运算等。甚至可以计算诸如欧几里得距离或这些向量之间的角度之类的东西。将文档表示为二进制向量的这种方式具有很强的功能。这在许多年里一直是文档检索和搜索的支柱。所有现代 CPU 都有硬编址内存指令,可以高效地哈希、索引和搜索这样的大型二进制向量集合。尽管这些指令是为另一个目的而构建的(索引内存位置以从 RAM 检索数据),但它们同样有效地用于搜索和检索文本的二进制向量操作。
NLTK 和 Stanford CoreNLP 存在时间最长,并且在学术论文中用于 NLP 算法比较的最广泛使用。尽管 Stanford CoreNLP 有一个 Python API,但它依赖于 Java 8 CoreNLP 后端,必须单独安装和配置。因此,如果你想在学术论文中发布你的工作结果,并将其与其他研究人员的工作进行比较,你可能需要使用 NLTK。学术界最常用的分词器是 PennTreebank 分词器:
>>> from nltk.tokenize import TreebankWordTokenizer >>> texts.append( ... "If conscience and empathy were impediments to the advancement of " ... "self-interest, then we would have evolved to be amoral sociopaths." ... ) # #1 >>> tokenizer = TreebankWordTokenizer() >>> tokens = tokenizer.tokenize(texts[-1])[:6] >>> tokens[:8] ['If', 'conscience', 'and', 'empathy', 'were', 'impediments', 'to', 'the'] >>> tokens[8:16] ['advancement', 'of', 'self-interest', ',', 'then', 'we', 'would', 'have'] >>> tokens[16:] ['evolved', 'to', 'be', 'amoral', 'sociopaths', '.']
spaCy Python 库包含一个自然语言处理流水线,其中包括一个标记器。事实上,这个包的名称来自于"space"和"Cython"这两个词。SpaCy 使用 Cython 包构建,以加速文本的标记,通常使用space字符(" ")作为分隔符。SpaCy 已经成为 NLP 的多功能工具,因为它的多功能性和 API 的优雅性。要使用 spaCy,你可以通过修改解析器对象内的管道元素来自定义你的 NLP 流水线,通常命名为nlp
。
而且 spaCy 已经“内置电池”。因此,即使加载了默认最小的 spaCy 语言模型,你也可以进行标记和句子分割,以及词性和抽象语法树标记 - 所有这些都可以通过一个函数调用完成。当你在一个字符串上调用nlp()
时,spaCy 会对文本进行标记化,并返回一个Doc
(文档)对象。一个Doc
对象是一个包含在文本中找到的句子和标记序列的容器。
spaCy 包为每个标记标注了它们的语言功能,以提供有关文本的语法结构的信息。Doc
对象中的每个标记对象都有提供这些标签的属性。
例如:* token.text
单词的原始文本 * token.pos_
作为人类可读字符串的语法部分标签 * token.pos
表示语法部分标签的整数 * token.dep_
表示标记在句法依赖树中的作用 * token.dep
对应于句法依赖树位置的整数
.text
属性提供了标记的原始文本。当你请求标记的str表示时,就会提供这个。一个 spaCy Doc
对象允许你对一个文档对象进行去标记化,以重新创建整个输入文本。也就是说,标记之间的关系。你可以使用这些函数来更深入地检查文本。
>>> import spacy >>> nlp = spacy.load("en_core_web_sm") >>> text = "Nice guys finish first." # #1 >>> doc = nlp(text) >>> for token in doc: >>> print(f"{token.text:<11}{token.pos_:<10}{token.dep:<10}") Nice ADJ 402 guys NOUN 429 finish VERB 8206900633647566924 first ADV 400 . PUNCT 445
2.9 挑战性的标记
汉语、日语和其他象形文字语言并不受限于用于构成标记或单词的字母数量。这些传统语言中的字符更像是绘画,被称为“象形文字”。中文和日文语言中有成千上万个独特的字符。而这些字符的使用方式与我们在英语等字母语言中使用单词的方式相似。但每个汉字通常不是一个完整的单词。一个字符的含义取决于两边的字符。而且单词之间没有用空格分隔。这使得将中文文本分词成单词或其他意思的分组成为一项具有挑战性的任务。
jieba
包是一个可以用来将繁体中文文本分词的 Python 包。它支持三种分词模式:1)“全模式”用于从句子中检索所有可能的词语,2)“精确模式”用于将句子切分为最精确的片段,3)“搜索引擎模式”用于将长词分割成更短的词语,有点像拆分复合词或找到英语中单词的根源。在下面的例子中,中文句子“西安是一座举世闻名的文化古城”翻译成“Xi’an is a city famous world-wide for its ancient culture.” 或者,更简洁直接的翻译可能是“Xi’an is a world-famous city for her ancient culture.”。
从语法的角度来看,你可以将这个句子分成:西安 (Xi’an), 是 (is), 一座 (a), 举世闻名 (world-famous), 的 (adjective suffix), 文化 (culture), 古城 (ancient city)。字座
是修饰城
的量词,表示“古老”。jieba
的accurate mode
模式会以这种方式分割句子,以便你能正确提取文本的精确解释。
第 2.3 节 Jieba 的精确模式
>>> import jieba >>> seg_list = jieba.cut("西安是一座举世闻名的文化古城") # #1 >>> list(seg_list) ['西安', '是', '一座', '举世闻名', '的', '文化', '古城']
Jieba 的 accurate 模式可以最小化标记或单词的总数。这为这个短句提供了 7 个标记。Jieba 试图尽可能保持更多的字符在一起。这将降低检测单词边界的误报率或类型 1 错误。
在全模式下,jieba 将尝试将文本分割为更小的单词,数量也更多。
第 2.4 节 Jieba 的全模式
>>> import jieba ... seg_list = jieba.cut("西安是一座举世闻名的文化古城", cut_all=True) # #1 >>> list(seg_list) ['西安', '是', '一座', '举世', '举世闻名', '闻名', '的', '文化', '古城']
现在你可以尝试搜索引擎模式,看看是否可能进一步分解这些标记:
第 2.5 节 Jieba 的搜索引擎模式
>>> seg_list = jieba.cut_for_search("西安是一座举世闻名的文化古城") # #1 >>> list(seg_list) ['西安', '是', '一座', '举世', '闻名', '举世闻名', '的', '文化', '古城']
不幸的是,Jieba 的词性标注模型不支持后续版本的 Python (3.5+)。
>>> import jieba >>> from jieba import posseg >>> words = posseg.cut("西安是一座举世闻名的文化古城") >>> jieba.enable_paddle() # #1 >>> words = posseg.cut("西安是一座举世闻名的文化古城", use_paddle=True) >>> list(words) [pair('西安', 'ns'), pair('是', 'v'), pair('一座', 'm'), pair('举世闻名', 'i'), pair('的', 'uj'), pair('文化', 'n'), pair('古城', 'ns')]
你可以在 (github.com/fxsjy/jieba
) 找到更多有关 jieba 的信息。SpaCy 还包含了一些中文语言模型,可以对中文文本进行分词和标记,做得相当不错。
>>> import spacy >>> spacy.cli.download("zh_core_web_sm") # #1 >>> nlpzh = spacy.load("zh_core_web_sm") >>> doc = nlpzh("西安是一座举世闻名的文化古城") >>> [(tok.text, tok.pos_) for tok in doc] [('西安', 'PROPN'), ('是', 'VERB'), ('一', 'NUM'), ('座', 'NUM'), ('举世闻名', 'VERB'), ('的', 'PART'), ('文化', 'NOUN'), ('古城', 'NOUN')]
如你所见,spaCy 提供了稍微不同的标记和词性标注,更贴近每个词的原始含义,而不是这个句子的上下文。
2.9.1 一个复杂的图画
与英文不同,中文和日文(汉字)等象形文字中没有词干或词形还原的概念。然而,有一个相关的概念。汉字最基本的组成部分叫做部首。要更好地理解部首,首先必须了解汉字是如何构成的。汉字有六种类型:1)象形字,2)形声字,3)会意字,4)指事字,5)借音字,以及 6)假借字。前四类是最重要的,也包括了大部分的汉字。
- 象形字(Pictographs)
- 形声字(Pictophonetic characters)
- 会意字(Associative compounds)
2 个象形字(Pictographs)
象形字是由真实物体的图像创造而成,比如口和门的汉字。
2 个形声字(Pictophonetic characters)
形声字是由一个部首和一个单独的汉字合并而成。其中一部分代表其意义,另一部分表示其发音。例如,妈(mā,妈妈)= 女(女性)+ 马(mǎ,马)。将女插入到马中得到妈。女是语义部首,表示汉字的意义(女性)。马是一个有着类似发音(mǎ)的单独汉字。你可以看到,母亲(妈)这个汉字是女性和马两个汉字的组合。这与英文的“同音词”概念相似-发音相似但意思截然不同的词语。但是中文使用额外的汉字来消除同音词的歧义。女性的汉字
3 个会意字(Associative compounds)
会意字可以分为两部分:一个表示图像,另一个表示意义。
例如,旦(黎明),上部分(日)像太阳,下部分(一)类似地平线。
指事字(Self-explanatory characters)
指事字由于不能用图像来表示,所以用单个抽象符号来表示。例如,上(上升)、下(下降)。
如你所见,像词干和词形还原这样的过程对于许多汉字来说更难或者不可能。分开一个汉字的部分可能会完全改变其意义。而且,组合部首以创建汉字没有规定的顺序或规则。
尽管如此,有些英语中的词干变化比中文更难。例如,自动去除像“我们”、“他们”等词的复数形式在英语中很难,但在中文中很简单。中文通过词缀来构造字符的复数形式,类似于在英语单词结尾加 s。中文的复数化后缀字符是们。朋友(friend)一词变为朋友们(friends)。
即使是“我们”,“他们 / 他们”和“y’all”的字符也使用相同的复数后缀:我们(we / us),他们(they / them),你们(you)。但是,在英语中,您可以从许多动词中删除“ing”或“ed”以获得根词。但是,在中文中,动词变位在前面或末尾使用一个额外的字符来指示时态。没有规定动词变位的规则。例如,检查字符“学”(学习),“在学”(学习)和“学过”(学过)。在中文中,您还可以使用后缀“学”来表示学术学科,例如“心理学”或“社会学”。在大多数情况下,您希望保持集成的中文字符而不是将其缩小到其组件。
结果证明,这是所有语言的一个好习惯。让数据说话。除非统计数据表明它有助于您的 NLP 管道运行得更好,否则不要进行词干提取或词形还原。当“smarter”和“smartest”减小为“smart”时,不会丢失多少意义。确保词干提取不会使您的 NLP 管道变得愚蠢。
让字符和单词如何使用的统计数据帮助你决定如何,或者是否要分解任何特定的单词或 n-gram。在下一章中,我们将向您展示一些工具,如 Scikit-Learn 的TfidfVectorizer
,它处理所有需要正确处理所需的繁琐帐户。
缩略词
您可能想知道为什么要将缩写wasn't
拆分为was
和n't
。对于某些应用程序,例如使用语法树的基于语法的 NLP 模型,重要的是将单词was
和not
分开,以使语法树解析器具有一组已知语法规则的一致,可预测的标记作为其输入。有各种标准和非标准的缩写单词的方法,通过将缩写减小为其组成单词,依赖树解析器或语法分析器只需要编程来预测单个单词的各种拼写,而不是所有可能的缩写。
从社交网站如 Twitter 和 Facebook 中对非正式文本进行标记化
NLTK 库包括一种基于规则的分词器,用于处理来自社交网络的短,非正式,有表情的文本:casual_tokenize
它处理表情符号,表情符号和用户名。 reduce_len
选项删除不太有意义的字符重复。 reduce_len
算法保留三个重复项,以近似原始文本的意图和情感。
>>> from nltk.tokenize.casual import casual_tokenize >>> texts.append("@rickrau mind BLOOOOOOOOWWWWWN by latest lex :*) !!!!!!!!") >>> casual_tokenize(texts[-1], reduce_len=True) ['@rickrau', 'mind', 'BLOOOWWWN', 'by', 'latest', 'lex', ':*)', '!', '!', '!']
2.9.2 使用 n-gram 扩展词汇表
让我们重新审视本章开始时遇到的“冰淇淋”问题。记得我们谈论过试图让“ice”和“cream”在一起。
我尖叫,你尖叫,我们都为冰淇淋尖叫。
但是我不知道有多少人为“cream”而尖叫。除非他们即将滑倒。因此,您需要一种方法来使您的单词向量保持“ice”和“cream”在一起。
我们都会 gram n -gram
一个 n-gram 是一个包含多达 n 个元素的序列,这些元素是从这些元素的序列中提取出来的,通常是一个字符串。通常 n-gram 的 “元素” 可以是字符、音节、单词,甚至是用来表示 DNA 或 RNA 序列中化学氨基酸标记的符号 “A”、“D” 和 “G”。^([38])
在本书中,我们只关心单词的n-gram,而不是字符。^([39]) 所以在本书中,当我们说 2-gram 时,我们指的是一对单词,比如 “冰淇淋”。当我们说 3-gram 时,我们指的是一组三个单词,比如 “超出常规” 或 “约翰·塞巴斯蒂安·巴赫” 或 “给我个谜语”。n-grams 不一定要在一起有特殊意义,比如复合词。它们只需在一起频繁出现,以引起你的标记计数器的注意。
为什么要使用 n-grams?正如你之前看到的,当一个 token 序列被向量化为词袋向量时,它会失去这些词序中固有的许多含义。通过将你的 token 概念扩展到包括多词 token,n-grams,你的 NLP 流水线可以保留语句中单词顺序中固有的大部分含义。例如,意义颠倒的词 “not” 将保持与其相邻单词的联系,这是它应该的。没有 n-gram 分词,它会自由漂浮。它的含义将与整个句子或文档相关联,而不是与其相邻单词相关联。2-gram “was not” 保留了比单独的 1-gram 中更多的 “not” 和 “was” 单词的含义。当你将一个单词与其在流水线中的邻居联系起来时,会保留一些单词的上下文。
在下一章中,我们将向你展示如何识别这些 n-grams 中包含的相对于其他 n-grams 的信息量,你可以用它来减少你的 NLP 流水线需要跟踪的标记(n-grams)数量。否则,它将不得不存储和维护每个单词序列的列表。对 n-grams 的优先处理将帮助它识别 “三体问题” 和 “冰淇淋”,而不特别关注 “三个身体” 或 “碎冰”。在第四章中,我们将词对甚至更长的序列与它们的实际含义联系起来,而不是与它们各自单词的含义联系起来。但是现在,你需要你的分词器生成这些序列,这些 n-grams。
停用词
停用词是任何语言中频繁出现但携带的实质信息较少的常见词。一些常见停用词的例子包括 ^([40])
- a, an
- the, this
- and, or
- of, on
历史上,为了减少从文本中提取信息的计算工作量,停用词已被排除在 NLP 流水线之外。尽管这些词本身携带的信息很少,但停用词可以作为 n-gram 的一部分提供重要的关联信息。考虑以下两个例子:
Mark 向 CEO 汇报
Suzanne 向董事会报告作为 CEO
在你的 NLP 流水线中,你可能会创建诸如 reported to the CEO
和 reported as the CEO
这样的 4-gram。如果从这些 4-gram 中删除停用词,两个例子都将被简化为 reported CEO
,你将丧失关于专业层级的信息。在第一个例子中,Mark 可能是 CEO 的助手,而在第二个例子中,Suzanne 是 CEO 向董事会汇报的 CEO。不幸的是,在你的流水线中保留停用词会产生另一个问题:它增加了 n-grams 所需的长度,以利用停用词形成的这些否则毫无意义的连接。这个问题迫使我们至少保留 4-gram,如果你想避免人力资源示例的歧义。
设计停用词过滤器取决于你的特定应用。词汇量将决定 NLP 流水线中所有后续步骤的计算复杂性和内存需求。但是停用词只是你总词汇量的一小部分。一个典型的停用词列表只包含 100 个左右频繁而不重要的单词。但是一个包含 20,000 个单词的词汇量将需要跟踪推特、博客文章和新闻文章等大型语料库中 95% 的单词。而且这仅仅是针对 1-gram 或单词令牌的情况。一个旨在捕捉大型英语语料库中 95% 的 2-grams 的 2-gram 词汇量通常会有 100 万个以上的唯一 2-gram 令牌。
你可能担心词汇量会影响你必须获取的训练集的大小,以避免过度拟合任何特定单词或单词组合。而且你知道,训练集的大小决定了需要处理的所有内容的处理量。但是,从 20,000 个单词中去掉 100 个停用词并不会显著加快你的工作速度。而且对于 2-gram 词汇,通过去掉停用词而获得的节省微乎其微。此外,对于 2-gram 词汇,当你随意去除停用词而不检查文本中使用这些停用词的 2-gram 的频率时,你会丢失更多的信息。例如,你可能会错过关于 “The Shining” 作为一个独特标题的提及,而将关于那部暴力、令人不安的电影的文本视为与提及 “Shining Light” 或 “shoe shining” 的文档相同。
因此,如果你有足够的内存和处理带宽来在更大的词汇表上运行流水线中的所有 NLP 步骤,你可能不想担心偶尔忽略一些不重要的词语。如果你担心用大词汇表过度拟合一个小训练集,有更好的方法来选择你的词汇表或减少你的维度比忽略停用词更好。在你的词汇表中包括停用词允许文档频率过滤器(在第三章中讨论)更准确地识别和忽略你特定领域中信息量最少的词语和 n-grams。
spaCy 和 NLTK 包含各种预定义的停用词集,适用于各种用例。^([42]) 你可能不需要像列表 2.6 中那样广泛的停用词列表,但如果你需要,你应该查看一下 spaCy 和 NLTK 的停用词列表。如果你需要更广泛的停用词集,你可以 SearX
^([43]) ^([44]) 搜索维护着多种语言停用词列表的 SEO 公司。
如果你的 NLP 流水线依赖于一个经过精细调整的停用词列表来实现高准确度,那么它可能是一个重大的维护头痛。人类和机器(搜索引擎)不断变化着忽略哪些词语。^([45]) ^([46]) 如果你能找到广告商使用的停用词列表,你可以用它们来检测欺骗性网页和 SEO(搜索引擎优化)内容。如果一个网页或文章很少使用停用词,那么它可能被“优化”来欺骗你。列表 2.6 使用了从这些来源中创建的详尽的停用词列表。通过从示例文本中过滤掉这个广泛的词汇集,你可以看到“翻译中丢失的意义”量。在大多数情况下,忽略停用词并不能提高你的 NLP 流水线的准确性。
列表 2.6 广泛的停用词列表
>>> import requests >>> url = ("https://gitlab.com/tangibleai/nlpia/-/raw/master/" ... "src/nlpia/data/stopword_lists.json") >>> response = requests.get(url) >>> stopwords = response.json()['exhaustive'] # #1 >>> tokens = 'the words were just as I remembered them'.split() # #2 >>> tokens_without_stopwords = [x for x in tokens if x not in stopwords] >>> print(tokens_without_stopwords) ['I', 'remembered']
这是一句有意义的句子,出自 Ted Chiang 的一篇短篇小说,讲述了机器帮助我们记住我们的陈述,这样我们就不必依赖有缺陷的记忆。^([47]) 在这个短语中,你失去了三分之二的词语,只保留了一些意义的含义。但是你可以看到,通过使用这个特别详尽的停用词集,一个重要的标记“words”被丢弃了。有时候,你可以在不使用冠词、介词甚至动词“to be”的情况下表达你的观点。但这会降低你的 NLP 流水线的精度和准确性,但至少会丢失一些意义。
你可以看到,有些单词比其他单词更有意义。想象一下做手语或赶着给自己写一张便条的人。当他们赶时间时会选择跳过哪些单词?这就是语言学家确定停用词列表的方法。但是,如果你赶时间,而你的 NLP 并不像你一样赶时间,那么你可能不想浪费时间创建和维护停用词列表。
这里有另一个不那么详尽的常见停用词列表:
代码清单 2.7 NLTK 停用词列表
>>> import nltk >>> nltk.download('stopwords') >>> stop_words = nltk.corpus.stopwords.words('english') >>> len(stop_words) 179 >>> stop_words[:7] ['i', 'me', 'my', 'myself', 'we', 'our', 'ours'] >>> [sw for sw in stopwords if len(sw) == 1] ['i', 'a', 's', 't', 'd', 'm', 'o', 'y']
第一人称为主题的文档非常无聊,对你来说还更重要的是,它的信息量很低。NLTK 包在其停用词列表中包括代词(不仅仅是第一人称代词)。这些单个字母的停用词更加好奇,但如果你经常使用 NLTK 分词器和波特词干剪裁器,它们就是有意义的。当使用 NLTK 分词器和剪裁器分裂缩略词时,这些单个字母标记会经常出现。
警告
在sklearn
、spacy
、nltk
和 SEO 工具中的英文停用词集合非常不同,并且它们在不断发展。在撰写本文时,sklearn
有 318 个停用词,NLTK 有 179 个停用词,spaCy 有 326 个停用词,我们的“详尽”SEO 列表包括 667 个停用词。
这是考虑不要过滤停用词的一个很好的理由。如果你这样做,其他人可能无法重现你的结果。
取决于您想要丢弃多少自然语言信息;)你可以取多个停用词列表的并集或交集,用于你的流程。这里有一些我们发现的停用词列表,但我们很少在生产中使用任何一个停用词列表:
代码清单 2.8 停用词列表的集合
>>> resp = requests.get(url) >>> len(resp.json()['exhaustive']) 667 >>> len(resp.json()['sklearn']) 318 >>> len(resp.json()['spacy']) 326 >>> len(resp.json()['nltk']) 179 >>> len(resp.json()['reuters']) 28
自然语言处理实战第二版(MEAP)(一)(4)https://developer.aliyun.com/article/1517832