课程6. 上下文词嵌入 Word2Vec
课程计划:
- 文本数据的特征;
- 构建词语的信息表征. 词袋模型(Bag of Words, LSA)
- 上下文嵌入. Word2vec
- 在实践中使用预训练的词向量表示
文本数据的特征
到目前为止,我们已经处理了以表格形式表示的数据类型(在传统机器学习中)和图像。我们了解了哪些机器学习模型和神经网络类型最适合这些类型的数据。
让我们回顾一下如何使用神经网络的分类问题作为例子来解决各种机器学习问题:
-
在输入中我们有一些描述一些对象的训练样本。对于训练样本的每个对象都有一组特征和一个目标变量——这就是我们学习预测的内容。
-
我们建立一个神经网络,它以数值表示的特征作为输入,并在输出中得到目标变量的一些预测。
-
接下来,我们在一个特殊的验证样本上选择网络超参数(例如隐藏层的数量和大小、训练周期数、优化算法等):对训练样本进行训练,并在验证样本上评估质量。接下来,我们固定在验证样本上显示最佳结果的超参数集。
-
接下来,您可以在单独的测试样本上评估训练模型的质量。
在这种情况下,需要注意的是,神经网络是一个数学模型。数值被馈送到它作为输入。它利用一些函数(线性和非线性)处理这些数值,并得到一个或多个数值作为输出。
当我们使用表格或图片时,一切都非常简单。这些数据已经以某些数值向量或矩阵(一般情况下为张量)的形式呈现。
但是如果我们使用一些非数字值(例如文本)作为特征会怎样?我们需要以某种方式学习处理文本,获得一些可以输入到神经网络的数字表示。今天我们将讨论如何获得文本的这种数值表示。
文本是另一种类型的数据。并且它在机器学习中的处理与处理图像和表格数据不同。为了理解为什么会这样,让我们重点介绍一下文本的主要属性以及它与其他类型数据的区别:
- 文本是来自有限字母表的标记序列,即序列的所有元素都采用离散值(标记是分类特征)。
- 文本的长度可以不同。
- 文本包含时间成分:在一定时间段内,文本从左到右阅读(或根据语言的不同,以另一个方向阅读。但重点是有一个方向)。
如何处理文本
要将文本数据输入机器学习模型(特别是神经网络),您需要以某种方式将文本表示为数字序列。如何才能做到这一点?
请注意,文本是一系列标记。标记可以是:
- 信件;
- 词语的部分;
- 字;
- 短语
- 句子;
- …
事实证明,文本编码任务可以简化为标记编码任务。然后将文本表示为标记序列,并从编码标记序列中组装出文本编码。
接下来我们将讨论如何对标记进行编码。
单词的矢量表示
在本节中,我们将讨论如何将标记和文本表示为数字向量的一些想法。以及如何使用这些token来解决与文本相关的问题(例如文本分类)。
One-hot encoding & Bag of Words (BoW)
独热编码和词袋(BoW)
将标记表示为向量的最简单的想法之一是独热编码。以下是全部内容:
让我们创建一个固定大小的字典。假设词典包含 n=50,000 个单词。
现在我们将字典中的每个单词表示为大小为
n
n
n 的向量。在字典中的第
i
i
i 个单词的向量中,除了第
i
i
i 个坐标处的唯一为1之外,该向量将包含所有零:
这是一种非常简单的表示单词的方式。可以立即发现以下缺点:
- 词向量不能反映单词的含义。
- 无法衡量两个单词在含义上的“相似性”(所有单词的向量都是彼此正交的);
- 向量非常稀疏,需要大量额外的内存;
- 词典大小有限;
- 无法处理词典中未收录的单词;
- 当改变字典大小时,需要重新计算向量。
显然,还存在着很多的不足。接下来我们将讨论如何表示单词来弥补这些不足。但首先,让我们讨论一下如何从单词的独热编码中获取整个文本的编码。
这很简单:一个句子的向量是其单词向量的总和。
这种文本编码称为词袋。然后,使用该文本表示,可以训练机器学习模型。例如,线性/逻辑回归甚至神经网络。
很明显,Bag of Words 继承了 One-Hot Encoding 的缺点。即:
- 句子向量不能很好地反映句子的含义。不考虑词序;
- 向量非常稀疏,需要大量额外的内存;
- 固定字典大小。
- 字典中未收录的单词无法处理。
- 当改变字典大小时,需要重新计算向量。
这里我们要注意以下几点:原则上,可以使用降维方法来降低 BoW 向量的维数。例如,PCA 或TSNE。
关于 BoW 不提供信息的另外一个考虑因素是,不同的词对于文本可能具有不同的重要性。 BoW 没有考虑到这一点。
在上面的例子中,几乎所有单词的值都是 1,即这种观点假设所有单词对于理解两篇文本都是相同的,但事实并非如此。例如,可以合理地假设冠词和连词在所有文档中都很常见,因此它们不会对特定句子的含义产生很大影响。逻辑上可以假设它们的“权重”(在文本的矢量表示中与这些词相对应的值)应该更小。
现在让我们继续讨论构建信息词向量表示的其他想法。
关于构建词向量表示的更多想法
Tf-iDF
构建单词和文本的信息向量表示的最重要的思想之一是 Tf-iDF。这个想法基于关于如何在构建单词和文本的向量表示时考虑单词对于特定文本的重要性的考虑。 Tf-iDF 长期以来一直被积极用于文本处理。 Tf-iDF 设计还允许解决诸如文档排名和从文档中提取关键字等问题。
有关 Tf-iDF 的更多信息,请参见此处:
潜在语义分析 (LSA)
LSA 是获取单词和文本的信息向量表示的另一个简单的想法。它由以下内容组成。让我们构建一个文档词矩阵:
现在我们将其分解为SVD分解:
事实证明,通过这种分解,矩阵
U
U
U 的行可以被视为文档向量,矩阵
V
T
V^T
VT 的列可以被视为词向量。 “可以认为”是指这些向量实际上会体现出单词和文本的含义。意义相近的词的向量会很接近,意义相远的词的向量会按照欧几里得距离很远。
现在:SVD 分解具有以下属性:矩阵 S S S 的对角线元素按降序排列。它们被称为奇异值。很显然,矩阵 S S S对角线上的数字越小,矩阵 U U U和 V T V^T VT对应的行和列对最终乘积的贡献就越小。
因此,我们只需将矩阵 U U U 和 V V V 的前行和前列分别保留为 m m m 即可。然后我们得到文档和文本的向量将小于长度 m m m。同时,大部分信息也会被保留在其中。
下面是经过 LSA 处理后,将向量维数降低至 m=2 后的文档和词向量的可视化。可以看出,单词和文档按主题分为三类:
我们来看看LSA的优点和缺点:
优点:
- 向量有意义;
- 能够在不显著损失质量的情况下减小嵌入的尺寸。
缺点:
- 文档数量多,计算复杂度高;
- 固定字典大小;
- 当改变文档集合时,需要重新计算向量;
- 该方法的概率模型与现实不符。它意味着单词和句子在某种意义上是呈正态分布的。但事实上情况似乎并非如此:实际上分布更像是泊松分布。
总结:在本文中你可以阅读到更多关于 LSA 的内容。
让我们继续讨论下一个想法:如何创建单词的信息向量表示。
上下文词嵌入. Word2Vec
上下文嵌入
让我们考虑三句话:
- 玛莎驾驶 ____________
- 轮胎 ____________ 被刺破了
- ____________ 有一个漂亮的白色框架。
我们还来看看这些句子中空缺的四个候选词:自行车、摩托车、汽车(机器)和马。问题:这些单词中的哪一个可以填补哪个空白?
这里产生了这样的想法:单词的含义取决于其使用的上下文。这就是根据上下文构建单词向量表示的想法的由来。
让我们思考一下如何根据上下文构建词向量。首先想到的想法是这样的:假设我们有一个文本语料库和一个大小为 n n n 的词典。让我们计算一下字典中的每个单词在该文本语料库中与字典中其他单词一起出现的次数。我们将构建一个大小为 n ⋅ n n \cdot n n⋅n 的矩阵,在该矩阵的第 i i i 行与第 j j j 列的交叉点处有一个数字,该数字反映词典中的第 i i i 个单词在文本语料库中的第 j j j 个单词的上下文中出现的次数。
让我们概述一下这种方法的优点和缺点:
优点:
- 矢量开始体现词语的含义!可以使用欧几里得距离比较它们的相似度;
缺点:
- 向量仍然相当稀疏,需要大量额外的内存;
- 字典大小有限。词典中没有收录的单词无法被处理;
- 当改变字典大小时,需要重新计算向量;
- 罕见词向量信息量不大。
还要注意,降维方法(PCA / TSNE)也可以应用于这种方法:
好的,它已经更好了:嵌入变得更具信息量。现在让我们注意以下几点:到目前为止我们所做的一切都是基于一些考虑,构建单词/文档的向量/矩阵,以某种方式反映单词/文档的含义。
如果我们尝试学习单词/文档向量会怎样?
Word2Vec
所以让我们制定我们想要的:
我们想要学习反映单词含义的低维词向量:它们可以使用某种度量相互比较。
我们将这种学习到的向量称为词嵌入。
我们将如何学习这样的向量:我们将教神经网络根据一个单词来预测可能在上下文中出现的单词(围绕这个词)。
神经网络将单词 X X X作为输入,并输出词典中所有单词的概率分布。输出中出现单词 Y Y Y的概率越大,在单词 X X X的上下文中遇到这个单词 Y Y Y的可能性就越大。
为了训练这样的神经网络,我们需要一个数据集——一组文本。 我们将使用大小为 5 的滑动窗口遍历数据集,并在窗口的每个位置使用中心词,教神经网络预测当前窗口中的单词。
因此,该问题的正式表述如下:
- 设置分类任务。类别数——字典大小 n n n。
- 神经网络以一个单词作为输入,产生 n n n个值——词典中单词的分布。
- 损失函数是网络产生的分布与正确分布(one-hot 向量)之间的交叉熵
作为神经网络,我们将采用两层全连接神经网络,层间没有激活函数。接下来我们将了解为什么这很重要。
该神经网络将以独热向量的形式接受一个单词作为输入。输出将是一个长度为 n n n 的向量,其概率分布在字典中的单词上。
假设我们训练了这样一个神经网络。现在让我们了解一下词嵌入在其中的位置。
为了做到这一点,让我们以稍微不同的方式描述我们的神经网络:
假设单词 into 为词典中的第
i
i
i 个单词,单词 banking 为第
j
j
j 个单词。注意,将单词into的one-hot向量与矩阵
A
A
A(网络第一层)相乘时,得到的结果应该是矩阵
A
A
A的第
i
i
i行。还要注意,神经网络输出向量的第 j 个元素(对应单词 banking)是第一层的输出与矩阵
B
B
B 第 j 列相乘的结果。
事实证明,根据网络,单词 into 和 banking 出现在相同上下文中的概率是矩阵 A A A 第 i i i 行与矩阵 B B B 第 j j j 列的标量积。
现在我们假设矩阵
A
A
A 的第
i
i
i 行和矩阵
B
B
B 的第
i
i
i 列可以被视为字典中第
i
i
i 个单词的嵌入。确实,你看:事实证明在训练过程中,神经网络学习到这样的词向量,使得两个在上下文中经常使用的词的向量之间的标量积尽可能大。
因此,矩阵 A A A的行或者矩阵 B B B的列可以作为词嵌入。嵌入的大小为 k k k,这取决于网络隐藏层的大小选择。可以使用标量积来比较嵌入。值越大,对应词语含义越接近。
这就解释了为什么我们没有在网络第一层之后添加激活函数。如果存在激活函数,则输出向量的第 j j j 个元素就不会是矩阵 A A A 和 B B B 的行和列的标量积。
让我们讨论一下这种方法的优点和缺点:
优点:
- 向量反映词语的含义;
- 向量的维数不依赖于字典的大小;
- 通过添加训练数据,可以进一步训练向量。
缺点:
- 固定字典大小。当文档词典的大小发生变化时,需要重新计算向量;
- 对于罕见词,嵌入不是最佳的;
- 具有相同词根的单词在神经网络中的处理方式不同。
eat, eater, eating
最后,我们注意到 Word2Vec 有两种可能的变体:
- Skip-Gram - 根据中心词预测上下文词
- CBOW - 根据上下文词预测中心词
Word2Vec 改进
- GloVe (全局向量)。它使用有关文本中单词和短语频率的统计信息来改进罕见词嵌入的学习。 你可以在这里阅读更多内容。
- FastText. 这个想法不是为整个单词构建嵌入,而是为部分构建嵌入。并从单词部分的嵌入中收集单词的嵌入。这使得可以获得许多不在词典中的单词的嵌入。
Word2Vec 特征
可以对Word2Vec向量进行向量运算。
例如:
v v v(king) - v v v(man) + v v v(woman) ≈ v v v(queen)
更多示例:
案例
数据处理
我们导入必要的库:
import string
import numpy as np
from nltk.tokenize import WordPunctTokenizer
from matplotlib import pyplot as plt
from IPython.display import clear_output
让我们加载要使用的数据。这是来自 Quora 网站的一系列问题。
# download the data:
#!wget https://www.dropbox.com/s/obaitrix9jyu84r/quora.txt?dl=1 -O ./quora.txt -nc
!wget https://raw.githubusercontent.com/MSUcourses/Data-Analysis-with-Python/main/Deep%20Learning/Files/quora.txt -O ./quora.txt -nc
# alternative download link: https://yadi.sk/i/BPQrUu1NaTduEw
输出:
让我们直接通过文件查看下里面内容:
让我们通过代码看一下数据元素:
data = list(open("./quora.txt", encoding="utf-8")) # 查看 Quora 中的第 55 个问题
data[55]
输出:
What are all the pros and cons of having dual citizenship?
让我们执行标记化和基本预处理:
# 我们从 NLTK(自然语言工具包)中“提取”了 WordPunctTokenizer(),用于处理自然语言,它将句子分解为单词和标点符号
tokenizer = WordPunctTokenizer()
print(tokenizer.tokenize(data[55]))
data_tok = [tokenizer.tokenize(x.lower()) for x in data] # 将所有内容转换为小写
输出:
[‘What’, ‘are’, ‘all’, ‘the’, ‘pros’, ‘and’, ‘cons’, ‘of’, ‘having’, ‘dual’, ‘citizenship’, ‘?’]
len(data)
输出:
537272
首先,让我们在可用的数据集上训练word2vec。您不需要手动构建模型,它已经在gensim
中可用。
from gensim.models import Word2Vec
model_obj = Word2Vec(data_tok,
vector_size=32, # 为每个单词构建一个 32 维向量
min_count=5, # 我们只考虑文本中出现至少 5 次的单词
window=5) # 用围绕目标词的 5 个词的窗口定义上下文
model = model_obj.wv # 将单词和它们的向量的对应关系保存在变量模型中
注意:
- gensim库要求numpy满足一些版本的限制,注意调整!
- 在
gensim
库的Word2Vec
实现中,window
参数指定的不是完整窗口大小,而是从窗口中心词到我们想要预测的单词的最大距离(以单词为单位)。例如,如果我们指定参数window=2
,并为句子“Eat some more of these soft French rolls and drink some tea”建立一个训练集,那么对于单词 soft,训练集中将包含以下对:(soft, more), (soft, these), (soft, French), (soft, rolls)。
现在我们可以访问字典中任何单词的向量:
model.get_vector('cat') # 考虑单词‘cat’的向量
输出:
由于单词以向量表示,因此现在可以计算它们之间的距离(或某种相似度)。例如,您可以评估哪些单词与给定单词最接近。
model.most_similar('parent')
# most_similar() - 一种获取一个单词、为其找到一个向量、通过某种接近度测量来搜索与其最接近的向量的方法;这里使用余弦距离度量
输出:
使用预先训练的词嵌入
为了获得高质量的嵌入,值得使用大型数据集。使用特定主题领域的数据也很有用。当然,训练需要大量时间,因此通常使用预先训练的词嵌入。
让我们加载小尺寸的预训练嵌入(25)。他们利用 Twitter 上的数据进行训练。
import gensim.downloader as api
model = api.load('glove-twitter-25') # 这里使用了 GloVe 向量
model.most_similar(positive=["лето"])
输出:
词向量表示的可视化
目前,每个单词都由一个 25 维的向量表示。为了将单词可视化,我们需要一种降维技术。为了简单起见,可以使用主成分分析 PCA。
len(model.key_to_index.keys())
输出:1193514
model.sort_by_descending_frequency() # 让我们选择最常出现的单词
输出:
WARNING:gensim.models.keyedvectors:sorting after vectors have been allocated is expensive & error-prone
或者是None
words = list(model.key_to_index.keys())[:1000] # 我们取出前 1000 个单词,并为其构建向量
print(words[::100])
word_vectors = np.asarray([model[x] for x in words])
输出:
# 让我们为所有这些构建一些(即二维)低维表示
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
pca = PCA(2)
scaler = StandardScaler()
word_vectors_pca = scaler.fit_transform(word_vectors)
word_vectors_pca = pca.fit_transform(word_vectors_pca)
为了实现可视化,让我们转向精彩的“bokeh”库。这些图表是交互式的。
import bokeh.models as bm, bokeh.plotting as pl
from bokeh.io import output_notebook
output_notebook()
def draw_vectors(x, y, radius=10, alpha=0.25, color='blue',
width=600, height=400, show=True, **kwargs):
""" 绘制数据点的交互式图表,并在悬停时显示辅助信息 """
if isinstance(color, str): color = [color] * len(x)
data_source = bm.ColumnDataSource({ 'x' : x, 'y' : y, 'color': color, **kwargs }) # ColumnDataSource 全部按列;x, y, 从外部提供的颜色
fig = pl.figure(active_scroll='wheel_zoom', width=width, height=height) # 绘制一个将动态重新缩放的图形
fig.scatter('x', 'y', size=radius, color='color', alpha=alpha, source=data_source) # 用点绘制
fig.add_tools(bm.HoverTool(tooltips=[(key, "@" + key) for key in kwargs.keys()]))
if show: pl.show(fig)
return fig
draw_vectors(word_vectors_pca[:, 0], word_vectors_pca[:, 1], token=words) # 你可以看到这些单词按照语言被分成了几组
输出:
我们可以看到,已经形成了几个集群。它们每个都有一些独特的特征:其中包含的单词的含义、语言或其他一些共同属性。该图表是交互式的。
使用 UMAP 进行降维
主成分分析是一项很棒的技术,但它只能捕捉数据中的线性关系。让我们转向 UMAP 技术,该技术考虑了给定点的邻居。您可以在上面的链接中阅读有关此技术的详细描述。
我们需要按照umap-learn功能包(使用的时候如果遇到报错,可以更换版本,我是用0.5.1没有问题,最新的可能是有问题,所以记得换版本)
import umap
embedding = umap.UMAP(n_neighbors=5).fit_transform(word_vectors)
# 我们将关注 5 个邻居
# fit_transform(word_vectors) - 转换所有源向量
draw_vectors(embedding[:, 0], embedding[:, 1], token=words)
# 现在,有更多明确界定的、彼此不相连的集群,俄语已经出现
输出:
(因为交互图无法上传,这里多放几张图,我们可以看到英语、俄语、符号等其它各个分别聚类到了一起)
如您所见,单词形成了更清晰的群组,肉眼即可区分。该图是交互式的;当您将鼠标悬停在某个点上时,就会显示其对应的单词。
你可以看到不同语言的单词并排排列。对于英语(我们的数据中包含最多词汇的语言),可以看到几个子组。
短语可视化
总之,我们不仅要构建单词的向量表示,还要构建整个短语的向量表示。为简单起见,我们将使用类似于词袋(BoW)的方法:一袋嵌入。我们将每个短语表示为其中包含的所有单词的平均嵌入。
def get_phrase_embedding(phrase): # 输入为一个短语——一个可变长度的标记序列
"""
通过聚合短语中单词的词向量将短语转换为一个向量。
"""
# 初始化一个与模型词向量维度相同的全零向量
vector = np.zeros([model.vector_size], dtype='float32')
# 对输入的短语进行小写处理,并使用分词器进行分词
phrase_tokenized = tokenizer.tokenize(phrase.lower())
# 遍历分词后的每个单词,若模型中有该单词的词向量,则将其添加到短语向量列表中
phrase_vectors = [model[x] for x in phrase_tokenized if model.has_index_for(x)]
# 如果短语向量列表不为空
if len(phrase_vectors) != 0:
# 对短语向量列表中的所有向量求平均值,得到短语的向量表示
# 这种求平均值的操作可以让我们摆脱文本长度可变的问题(将不同长度的文本转换为固定长度的向量)
vector = np.mean(phrase_vectors, axis=0)
# 如果短语向量列表为空,则返回全零向量
return vector
data[402687]
输出:
What gift should I give to my girlfriend on her birthday?
get_phrase_embedding(data[402687])
输出:
vector = get_phrase_embedding("Hello, today we speak about word vectors!")
# 我们可以得到任何句子的向量,也就是说,不仅限于来自训练样本的数据,因为这些向量已经经过了预训练
vector
输出:
此外,您还可以尝试以下操作:
- 使用 t-SNE 代替 UMAP(大样本上需要更长时间)
- 可视化整个数据集,而不仅仅是其中的一部分
- 使用来自
gensim
“model zoo” 的其他嵌入:gensim.downloader.info()
from gensim import downloader
downloader.info()['models']
这个方法会返回一个包含 gensim 下载器中所有可用资源信息的字典。这些资源包括预训练的词向量模型(如 Word2Vec、FastText 等)、数据集等。返回的字典包含了不同类型资源的元数据,例如模型的名称、描述、大小、下载链接等。