原文:
towardsdatascience.com/nlp-illustrated-part-1-text-encoding-41ba06c0f512
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5de16d507d802b50a228a1ebc20307a2.png
今天,我们开始一个新的系列,关于自然语言处理(NLP)。这很令人兴奋,因为 NLP 是我们随处可见的所有花哨的大型语言模型(LLMs)的支柱——想想 Claude、GPT 和 Llama。
简而言之,NLP 帮助机器理解人类语言——无论是理解它、分析它,甚至生成它。
如果你一直在跟随我们的**深度学习之旅**,我们已经了解到,在本质上,神经网络遵循一个简单的原则:它们接收一个输入,施展数学魔法,然后输出一个结果。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/510668ce6eb1a8adf3f3f798c0dc218b.png
然而,为了使神经网络能够做到这一点,输入和输出都必须是它们理解的格式:数字。
这个规则适用于我们处理任何简单模型的情况…
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/a55a71d67515e6402538c9e152084bb8.png
…或者一个高度复杂的模型如 GPT。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/60b61b9687b79d97c4ac761ce3a4381d.png
现在事情变得有趣了。我们通过文本与模型如 GPT 进行交互。例如,我们可能会问它:“印度的首都是哪里?”,模型能够理解这段文本并提供一个回答。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/e55ec0da3f61713b75f69348ab537c31.png
我们不能将文本输入到神经网络中
但等等——我们不是刚刚说过神经网络不能直接处理文本,而需要数字吗?
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c451c705039dd6da13c53d465ed4df39.png
欢迎回到互联网的角落,我们在这里处理听起来复杂的机器学习概念,并通过图解的方式展示它们——最终发现它们其实并不那么复杂!
向 ChatGPT 提问
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5de16d507d802b50a228a1ebc20307a2.png
我们需要在将文本输入神经网络之前将其转换为数字
这就是文本编码的作用,在这篇文章中,我们将探讨一些处理这种文本到数字转换的简单方法。
独热编码
编码文本最简单的方法之一是通过独热编码。
让我们分解一下:想象我们有一个包含 10,000 个词的词典。这个词典中的每个词都有一个独特的位置。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/ae3b8bade0e4e3f1fa9bcd66b1a5b37a.png
我们词典中的第一个词“about”位于位置 1,而最后一个词“zoo”则位于位置 10,000。同样,每个其他词都有其独特的位置在两者之间。
现在,让我们说我们想要编码单词“dogs”。首先,我们在字典中查找它的位置…
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/949bb082b3b829b7887f10eff0c5c7d7.png
…并且发现“dogs”在第 850 个位置。为了表示它,我们创建一个包含 10,000 个零的向量,然后将第 850 个位置设置为 1,如下所示:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5a79e0a20d3187ecc0b0e939c2728b79.png
这就像一个开关:如果单词的位置匹配,开关是开着的(1)如果不匹配,开关是关着的(0)。
现在,假设我们想要编码这个句子:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/ef4f2517ece76389c06816a5100d6e30.png
除了“dogs”的词向量外,我们还找到了“barks”的词向量…
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/0d7324ba83b4730015548b634986cefc.png
…以及“loudly”:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/88e90b2eb96a74ecd68ea31a46c7f2a5.png
然后为了表示整个句子,我们将这些单独的词向量堆叠成一个矩阵,其中每一行对应一个单词的向量:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/2cfc1535d883d15ad5aefd55f39b520d.png
这形成了一个句子矩阵,行对应于单词。虽然这很简单且直观,但独热编码有一个很大的缺点:低效。
每个词向量都非常大,大部分都是零。例如,对于一个包含 10,000 个单词的字典,每个向量包含 10,000 个元素,其中 99.99%是零。如果我们扩展到更大的字典——比如拥有大约 170,000 个单词的剑桥英语词典——这种低效性变得更加明显。
现在想象一下通过将这些 170,000 大小的词向量堆叠成句子矩阵来编码一个句子——它很快就会变得巨大且难以管理。为了解决这些问题,我们转向一个更有效的方法:词袋模型。
词袋模型
词袋模型(BoW)通过为整个句子创建一个单一向量,而不是为每个单词创建单独的向量来简化文本表示。
想象我们有这四个句子想要编码:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cd72aed94c9039f3a0b77d62f1845b9f.png
如果你知道这个引用的来源,那你就赚到了。如果你不知道,我们就假装这是人们常说的话。
第一步是创建一个包含这四个句子中所有独特单词的字典。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/be7b2529540b1a14cd52eb60bf96c744.png
BoW 字典
每个句子都表示为一个向量,其长度等于我们字典中独特单词的数量。并且向量中的每个元素代表字典中的一个单词,并设置为该单词在句子中出现的次数。
例如,如果我们取第一句话“onions have layers,”它的向量看起来是这样的:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/a273e357db509422d8adb78293c19549.png
句子 1 的 BoW 编码
“onions”出现了一次,“have”出现了一次,“layers”出现了一次。所以,这个句子的向量会在这些位置上有一个1。
同样,我们可以编码剩余的句子:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/bc11486bb94c57546990721a1c19f6cb.png
句子的 BoW 编码
让我们再编码一个例子:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/9c3f7ad7335fd562e76a36c4bfbd8098.png
对于这个句子,单词“layers”和“have”重复了两次,因此它们在向量中的对应位置将具有值2。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/e168448ec21499e93eb08fef70248e37.png
句子的 BoW 编码
下面是如何在 Python 中实现 BoW 的示例:
fromsklearn.feature_extraction.textimportCountVectorizer sentences=["Onions have layers","Ogres have layers","You get it?","We both have layers"]bag_of_words=CountVectorizer()X=bag_of_words.fit_transform(sentences)print("BoW dictionary:",bag_of_words.get_feature_names_out())print("BoW encoding:n",X.toarray())BoW dictionary:['both''get''have''it''layers''ogres''onions''we''you']BoW encoding:[[001010100][001011000][010100001][101010010]]虽然 BoW 在计数单词方面简单有效,但它无法捕捉单词的顺序或上下文。例如,考虑以下两个句子中的单词“bark”:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/3530657f70805fc4e6e8a28228d306db.png
在“dogs bark loudly”中的单词“bark”与在“the tree’s bark”中的单词“bark”完全不同的意义。但 BoW 在这两种情况下都会将“bark”视为相同,忽略了周围单词提供的意义差异。
二元组
这就是二元组发挥作用的地方。通过查看相邻的单词,它们有助于捕捉更多的上下文。让我们用这两个句子来说明这一点:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/3530657f70805fc4e6e8a28228d306db.png
就像在 BoW 方法中一样,我们首先创建一个字典:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/31ac8b23036d62771724b73dc5662bb5.png
然而,这次,除了单个单词外,我们还包括单词对(二元组)。这些二元组是通过查看每个句子中直接相邻的单词形成的。
例如,在句子“dogs bark loudly”中,二元组将是:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/988ae32cf20068b950858d6ab35a14f0.png
在“the tree’s bark”中,这些是二元组:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/12914e7decf4b262c92a6365150023f4.png
我们将其添加到我们的字典中,以获得我们的二元组词典:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/8f2d87f84332ec859ed2e1bfacf3b9b1.png
二元组词典
接下来,我们将每个句子表示为一个向量。与 BoW 类似,这个向量中的每个元素都对应于字典中的一个单词或二元组,其值表示该单词或二元组在句子中出现的次数。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d618f5ba77ac44d591f5c0ab13a89d17.png
二元组编码
使用二元组允许我们通过捕捉相邻单词之间的关系来保留上下文。因此,如果一个句子包含“tree’s bark”而另一个句子包含“dogs bark”,这些二元组将以不同的方式表示,从而保留它们的意义。
下面是如何在 Python 中实现二元组的示例:
fromsklearn.feature_extraction.textimportCountVectorizer sentences=["dogs bark loudly","the tree's bark"]bigram=CountVectorizer(ngram_range=(1,2))#(1, 2) specifies that we want single words and bigramsX=bigram.fit_transform(sentences)print("Bigram dictionary:",bigram.get_feature_names_out())print("Bigram encoding:n",X.toarray())Bigram dictionary:['bark''bark loudly''dogs''dogs bark''loudly''the''the tree''tree''tree bark']Bigram encoding:[[111110000][100001111]]N-gram
正如二元组将两个连续的单词分组一样,我们可以将这个概念扩展到n-gram,其中n代表一起分组的单词数量。例如,当n=3(三元组)时,我们会将三个连续的单词分组,例如“dogs bark loudly”。同样,当n=5时,我们会将五个连续的单词分组,从而从文本中捕捉到更多的上下文。
这种方法使我们能够捕捉到文本数据中更丰富的关系和上下文,但它也增加了字典的大小和计算复杂度。
TF-IDF
虽然“词袋模型”和“二元组”在计数单词和捕捉基本上下文方面很有效,但它们没有考虑单词在句子或多个句子中的重要性或独特性。这就是**TF-IDF(词频-逆文档频率)**发挥作用的地方。它根据以下因素对单词进行加权:
词频 (TF):一个单词在句子中出现的频率
逆文档频率 (IDF):一个单词在所有句子中的稀有程度或独特性
这种加权系统使得 TF-IDF 在突出句子中的重要单词同时,可以降低常见单词的影响。
为了看到这个在实际中的应用,让我们将 TF-IDF 应用于我们熟悉的四句话集合。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cd72aed94c9039f3a0b77d62f1845b9f.png
和之前一样,我们创建了一个包含我们句子中所有唯一单词的字典。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/be7b2529540b1a14cd52eb60bf96c744.png
词频 (TF)
要计算一个单词的 TF,我们使用以下公式:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/eb8058bbbaf1419d22ad4c081617e8a1.png
例如,对于第一句话中的单词“onions”…
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/a932816ddeca7dbcf44dc931968111f3.png
…TF 是:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cb43fd529e8ba209ffc4dfab3ba06fa7.png
同样,让我们计算第一句话中“both”的 TF:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/8d666af75971c7be3bb7ac5ad9a70a29.png
使用相同的逻辑,我们可以得到所有四个句子中所有单词的 TF,如下所示:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/ea34605d30f2d2fa8aba69771b30e493.png
每个句子中所有单词的 TF
注意,一个单词的 TF 可能在不同句子中有所不同。例如,单词 “both” 在前三句中没有出现,所以这些句子的 TF 为 0。然而,在最后一句中,它出现在四个单词中的其中一个,其 TF 为 1/4。
逆文档频率 (IDF)
接下来,我们计算每个单词的 IDF。IDF 会给出现在句子中较少的单词更高的值,从而强调那些出现在较少句子中的单词。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/81c6cd34d90cd5c675e23597b8ce790c.png
例如,我们看到单词“both”只出现在四句话中的一句:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/fa88445df7a1a16603c5e48b04e32809.png
因此,其 IDF 为:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/6075c0557da4b4daff635ba5f403313a.png
同样,我们可以得到字典中其他单词的 IDF:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/4c0efa3505eb82a716b966892f5e907f.png
字典中所有单词的 IDF
在这里,单词“both”只出现在第 4 句话中,这使得它的 IDF 得分比像“have”这样出现在多句中的常见单词要高。
与 TF 不同,一个单词的 IDF 在所有句子中保持一致。
TF-IDF
一个单词最终的 TF-IDF 分数是其 TF 和 IDF 的乘积:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/36d61736f9de29f36bfbe16639b2f61e.png
这导致句子向量,其中每个单词的得分反映了它在句子中的重要性(TF)以及它在所有句子中的独特性(IDF)。
将 TF 和 IDF 项代入我们的公式,我们得到最终的 TF-IDF 句子向量:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/31eb951acd9c335e074bbb6e1d384692.png
所有句子的 TD-IDF 编码
这是我们在 Python 中计算 TF-IDF 的方法:
fromsklearn.feature_extraction.textimportTfidfVectorizer sentences=["Onions have layers","Ogres have layers","You get it?","We both have layers"]tfidf=TfidfVectorizer()X=tfidf.fit_transform(sentences)print("TF-IDF dictionary:",tfidf.get_feature_names_out())print("TF-IDF encoding:n",X.toarray())注意:Python 的结果可能与手动计算略有不同,因为:
1. L2 Normalization:Scikit-learn 的
TfidfVectorizer默认将向量归一化到单位长度。2. 调整后的 IDF 公式:IDF 计算包括一个平滑项,以防止对于出现在所有句子中的单词进行除零操作。在这里了解更多关于这个信息。
虽然我们讨论的方法是 NLP 中的基本构建块,但它们存在显著的局限性。
1 – 这些方法缺乏语义理解。它们无法理解单词的意义,也无法识别像“fast”(快)和“quick”(迅速)这样的同义词之间的关系。虽然双词组可以提供一些局部上下文,但它们仍然错过了更深层次的联系和细微的意义差异。
2 – 这些方法依赖于刚性表示,将单词视为孤立实体。例如,我们直观地理解“king”(国王)和“queen”(王后)是相关的,但这些方法将“king”和“queen”表示为与“king”和“apple”(苹果)一样无关,完全忽略了它们的相似性。
3 – 他们面临可扩展性挑战。它们依赖于稀疏、高维向量,随着词典大小的增加,这些向量变得更加难以处理和效率低下。
如果我们能够以捕捉单词的意义、相似性和关系的方式来表示单词,这正是词嵌入所追求的。词嵌入通过创建密集、有意义的向量,同时保留上下文和语义关系,从而革新了文本编码。
在下一篇文章中,**NLP 图解,第二部分:词嵌入**,我们将探讨这些嵌入如何超越基本的词频统计,捕捉单词之间复杂、细微的关系!
NLP 图解,第二部分:词嵌入
如果您有任何问题/评论,请通过LinkedIn与我联系或给我发送电子邮件至[email protected]。
备注:除非另有说明,所有插图均由作者绘制。