Word2Vec
含义
人类之所以聪明,是因为拥有其他人类组成的网络和语言交流的媒介。语言是人类历史上一个相对较新的概念。感官和通过感官学习比语言早得多。随着语言的出现,人类相比其他动物产生了惊人的变化。动物可以交流,但不能通过交流来传递知识。理论知识转移对我们人类而言是独一无二的。理论知识转移的关键也是语言。
以下哪一项包含更多信息——几兆字节的图片或描述图片的句子?这个问题起初使我震惊。想一想,借助快速的互联网,眨眼之间就可以传输数百万字节的图像,而我们人类却只能非常缓慢地说话和交流。这直接意味着语言是信息传递的缓慢形式。信息论有不同的观点。它指出,语言是我们拥有的最好的压缩算法之一。如果使用正确,几句话不仅可以描述好几兆的图像,而且可以描述非常复杂的想法。互联网可能很快,但是由于语言实现的高度压缩,我们可以肯定地说,句子比图像携带更多的信息。
这里应该解决的最重要的问题是“我们如何表示单词的含义?”。单词被认为是符号,含义被认为是一个概念。语言是一种非常强大的结构。它可以仅通过字母(单词)模式来传达想法。现在,让我们思考一下。言语交流本质上是人类设计的一种抽象商品。教其他人一门语言是困难的,但是让计算机理解这完全是另一回事。对于仅在核心级别理解0和1的实体,理解复杂的单词及其语义听起来似乎是不可能的任务。
计算机如何理解词汇
机器理解词义的第一个尝试是通过Wordnet 。 Wordnet是单词及其同义词的同义词库。该词典有助于分析在给定上下文中含义相同的单词。这是一个人为制作的字典,占用大量人力。这种方法似乎确实有助于编码单词的含义,但这是一种主观的方法。对不同的人来说, “好”一词可能有不同的意思。我们需要一种更好的方法来编码单词的含义。
离散表示形式(Discrete representation)或局部表示形式(localist representation)将单词表示为一个单热向量(one-hot vectors)。这种表示单词的方式有助于我们将单词提供给计算机。这种方法的弊端是无止境的。我们无法表示单词的含义。语料库可以有很多单词,因此,one-hot向量将是巨大且效率低下的。为了对含义进行编码,可以用两个成对的相似单词(通过Wordnet)构建一个查找表,并将它们的one-hot表示保留在表中。如果语料库中有50,000个单词,您可以计算表格的大小吗? 😖
分布语义(Distributional semantics): 在这种方法中,词的含义由经常出现在其附近的词给出。 “您可以通过一个词的上下文来了解这个词”——J.R. Firth。在这种方法中,我们尝试通过单词的上下文来建模。我们堆叠这些上下文,然后进行概括以获取单词的含义。想一想,相似的词确实共享相似的上下文。之所以称为分布语义,是因为单词的含义不仅取决于自身,还取决于其旁边的单词。因此含义是分布式的。对此的另一种解释是,用这种方法表示单词的矢量不再是一个单热向量,它们很密集,并且含义分布在矢量的每个维度上。
Word2Vec
让我们进一步讨论分布语义。假设您有一个数据集,它为一组单词分配了否定或肯定的情绪。训练后,您的分类器必须根据目标词将相似词分组。这是获得“理解”单词的人造分类器的直接方法。但是,这并不能实现计算机可以与您交流这个首要目标。直到2013年,许多有希望的想法和算法被提出,这些想法和算法缓慢但稳步地提高着标准。但是,在2013年,word2vec的推出让NLP社区获得巨大的突破。在进入复杂描述之前,让我们一起可视化这个概念。
Word embeddings, an intuition
假设我们有四个词的语料库,分别是“人”,国王,皇后”和“茶杯。如果我们可以创建一个表,每一行放一个单词,每一列都是定义这些单词的某个属性,如何?请参考上表;在性别列中,男人和国王一词具有相同的值,而皇后则完全相反。这是指男人和国王具有相同的性别,而皇后则具有相反的性别。茶杯无性别,因此具有中性价值。同样,在活动列中,我们看到Man,King和Queen的值为1,而Teacup的值为0,因为这是非生物。如果我们将表想象成一个向量空间,把它的列想象成维度,那么我们看到每个词作为向量在每个维度上都有一定的权重。这个巧妙的想法正是word2vec的概念。您的分类器通过在被表示成矩阵的语料库学习单词联想,矩阵每列都是一个嵌入或单词具有一定权重的方向。这些权重共同构成了您的分类器相对于给定语料库的含义。但是,像如上表所示的示例是比较理想的情况——嵌入清晰可辨。在大多数情况下,由于分类器本身决定嵌入,因此很难解码每个嵌入背后的含义和推理。因此,总而言之,word2vec是一种利用神经网络学习将单词表示为矢量的技术,以针对给定语料库与其他单词形成单词关联。
可能性(Likelihood)
在本节中,我们将讨论问题和解决方案背后的数学原理。 问题是找到某种方法来从单词中获取含义。 含义的编码方式必须使计算机易于使用。 在分布式表示中,其思想是从单词旁边的单词(上下文单词)中理解单词的含义。
解决方案是可以通过两种正交方式。 在连续词袋模型(Continuous Bag of Words Model /CBOW)方法中,作者尝试根据上下文词预测中心词的概率。 在Skip-gram模型中,该方法根据中心单词预测上下文单词的概率。
CBOW: $$ 对于每个位置t=1,2,..,Tt=1,2,..,T,给定上下文单词 P( w{t} |w{t+j} ;\theta ) ,我们在固定大小m的窗口中预测中心单词,其中_{-m\leqslant j\leqslant m;\ j\neq 0} 。
Skip Gram: 对于每个位置t=1,2,..,Tt=1,2,..,T,我们在给定中心词的情况下,在固定大小mm的窗口中预测上下文词 wtw_{t}。
符号及其含义: 1. θ\theta: 训练的参数。 这里指的是每个单词的向量嵌入. 2. tt: 表示文本语料库中每个单词索引的变量. 语料库中有TT字数 3. mm: 上下文窗口的大小。 4. jj: 代表上下文单词索引的变量。
我们可以说,可能性就是我们的模型表现得如何——在给定的特定上下文中,概率模型有多大可能性预测出正确的单词
目标函数
有了似然函数,我们可以通过最小化平均负对数似然来表达似然最大化。 平均负对数似然函数是目标函数。
- 平均值:平均值为我们提供了平均误差。 规模(scale)与语料库的大小无关
- 负数:优化器应减少误差
- 对数log:乘积转化为求和
通过最小化J(θ)J(\theta)我们使模型的可能性最大化,从而成功地对语言进行了更好的建模。
概率函数
我们需要最大化的似然函数。 更好的方法不是最大化目标函数,而是最小化目标函数。 该方法缺少的要素之一是概率函数本身。 我们需要找到一个方程,可以对给定上下文中单词出现的概率进行建模。
我们考虑词汇表中的每个单词都由两个向量表示。 让我们考虑一个单词ww,那么两个向量将是: 1. vwv_w- —当单词是中心单词时 2. uwu_w - Uw——当单词是上下文单词时
这看起来像softmax函数不是吗? 让我们分解这个方程式
在分子中,我们有expuaTvb\exp u_{a}^{T}v_b 项。 这是两个词之间的点积。 这表示两个词有多接近。 对点积进行幂运算具有极好的效果。 它不仅把大数字变得更大,而且把小数字变得更小。 因此,分子讨论了两个单词的接近程度(相似程度)。
在分母中,我们有一个标准化项∑i=1VexpuiTvc\sum ^{V}_{i=1}\exp u_{i}^{T} v_{c}。 这将分子归一化并为我们提供百分比相似度。 当出现单词wbw_b时,该公式直接转换为单词waw_a的概率。
CBOW-教程
Word2vec分为两个部分。一个是CBOW(连续词袋),另一个是Skip-Gram模型。本节将带领读者完成CBOW在tensorflow中的旅程。以上各节是为读者提供CBOW和Skip-Gram适当的直观理解。这两个主题都是概率模型。通过了解上述目标函数可以更好地理解它们。在下一节中,我们将逐步介绍一个简单的CBOW教程。首先,我们将参考所需的导入。我们将主要使用NumPy数组和张量tenors,同时利用TensorFlow的gradient tape功能。我们将使用Matplotlib可视化词嵌入,并使用tqdm跟踪执行操作所需的时间。
import numpy as np
import tensorflow as tf
from tqdm import tqdm
import matplotlib.pyplot as plt
对于我们的数据,我们已经从练习网站上获取了参考
接下来,我们通过将语料库拆分成单词并删除标点符号和大写字母来对语料库执行tokenization,以使我们的模型更容易对单词进行分组。然后将形成的单词放到集合中。
# Converts the data into tokens
tokenized_text = tf.keras.preprocessing.text.text_to_word_sequence(data)
tokenized_text_size = len(tokenized_text)
# Creates a vocab of unique words
vocab = sorted(set(tokenized_text))
vocab_size = len(vocab)
print('Vocab Size: {}'.format(len(vocab)))
我们的代码非常重要的一部分:我们创建了一个名为vocab_to_ix
的字典,其中包含单词作为键(key),并包含其索引(来自vocab集)作为其值(value)。 ix_to_vocab
是vocab的NumPy数组版本,而text_as_int
是文本的NumPy数组,包含单词的索引而不是单词本身。
# Map the vocab words to individual indices
vocab_to_ix = {c:ix for ix,c in enumerate(vocab)}
# Map the indices to the words in vocab
ix_to_vocab = np.array(vocab)
# Convert the data into numbers
text_as_int = np.array([vocab_to_ix[c] for c in tokenized_text])
在下面的代码块中,我们定义了嵌入的数量,如上所述,这是定义数据集词的维数。我们选择的窗口大小为5
,这意味着我们将每4个上下文词得到一个中心词。在这里,context_vector
和center_vector
本质上是我们的嵌入矩阵,其中我们的单词将针对给定的语料库获取含义。
EMBEDDING_SIZE = 9
WINDOW_SIZE = 5
opt = tf.optimizers.Adam()
iterations = 1000
# Here the word vectors are represented as a row
context_vector = tf.Variable(np.random.rand(vocab_size, EMBEDDING_SIZE))
center_vector = tf.Variable(np.random.rand(vocab_size, EMBEDDING_SIZE))
在最关键的步骤中,我们的代码定义了我们的train_step
函数,该函数包含了gradient_tape
方法。该方法将list of indices and losses
作为其参数。使用tf.GradientTape()作为tape开始跟踪其下的变量所发生的变化(用于导数的计算)。 u_avg
定义为从上下文向量中获取的平均值。输出是通过u_avg
和center_vector
嵌入之间的矩阵相乘、softmax分布来计算的。本文已经提到,相对于给定的上下文词集,我们将看到所有中心词的softmax分布。从那里,我们选择目标中心词并计算softmax损失,然后将其反馈到tape.gradient
方法。 tape.gradient
方法可计算损失变量在每次迭代过程中所经历的变化,并对center_vector
和context_vector
嵌入求导。综上所述,我们的输出会更改嵌入矩阵的值。
def train_step(indices, loss_list):
"""The training step
Arguments:
indices (list): The indices of the vocab in the window
"""
with tf.GradientTape() as tape:
# Context
u_avg = 0
for count,index in enumerate(indices):
if count != WINDOW_SIZE//2:
u_avg += context_vector[index,:]
u_avg /= WINDOW_SIZE-1
# Center
output = tf.matmul(center_vector, tf.expand_dims(u_avg ,1))
soft_out = tf.nn.softmax(output, axis=0)
loss = soft_out[indices[WINDOW_SIZE//2]]
log_loss = -tf.math.log(loss)
loss_list.append(log_loss.numpy())
grad = tape.gradient(log_loss, [context_vector, center_vector])
opt.apply_gradients(zip(grad, [context_vector, center_vector]))
最后,在训练了该模型所需的迭代次数之后,我们可以使用TSNE根据每个矩阵形成的嵌来可视化单词。 这样在2D平面上,我们可以查看哪些单词彼此形成了集群(cluster)。
from sklearn.manifold import TSNE
TSNE_embedd = TSNE(n_components=2).fit_transform(center_vector.numpy())
TSNE_decod = TSNE(n_components=2).fit_transform(context_vector.numpy())
nt = 0
plt.figure(figsize=(25,5))
for x,y in TSNE_embedd:
plt.scatter(x,y)
plt.annotate(ix_to_vocab[cnt], (x,y))
cnt += 1
plt.show()
在这里,我们显示了一小部分单词的二维可视化。 我们看到这种关系还不够清楚,但是已经形成了一些含义集群,例如years。我们将在后面的部分中充分介绍此可视化。
CBOW visualization
一个最后的总结:在CBOW中,给定一组保持不变的上下文向量作为输入,迭代所有可能的中心词并计算目标中心词的softmax损失。
Computation involved in CBOW
Skip gram教程
在本节中,我们将讨论Skip-Gram。该算法与我们上面看到的算法非常相似。唯一的区别是使用上下文向量作为输出。我们不应该把Skip-Gram看成是CBOW的反面,而应该是一种同样可以利用上下文但要好一点的算法。我们将类似地导入所需的软件包,如CBOW所述,并执行类似的数据处理。
嵌入层数设置为9
,而窗口大小设置为5
。
EMBEDDING_SIZE = 9
WINDOW_SIZE = 5
opt = tf.optimizers.Adam()
iterations = 1000
# Here the word vectors are represented as row
context_vector = tf.Variable(np.random.rand(vocab_size, EMBEDDING_SIZE))
center_vector = tf.Variable(np.random.rand(vocab_size, EMBEDDING_SIZE))
训练步骤体现了Skip-gram和CBOW之间的区别。在CBOW中,我们的上下文向量集是固定的,而中心向量遍历所有可能的输出以计算损失。在Skip-gram中,我们的中心向量是固定的,并且在可用的上下文向量中对其进行逐一迭代。我们在训练步骤中实施了相同的步骤:将给定单词列表的中心单词设置为中心向量,并将其矩阵与上下文嵌入矩阵相乘,以提供所有可能单词的输出。取得输出的softmax分布后,将在当前目标上下文单词集上计算softmax损失。然后将计算出的损失输入tape.gradient
方法,并根据导数更改两个嵌入矩阵。
def train_step(indices, loss_list):
"""The training step
Arguments:
indices (list): The indices of the vocab in the window
"""
with tf.GradientTape() as tape:
# Center
loss = 0
#181, 9 -> 181,9 * 9,1 ->181,1
v_center = center_vector[ indices[WINDOW_SIZE//2],:]
output = tf.matmul(context_vector, tf.expand_dims(v_center ,1))
soft_out = tf.nn.softmax(output, axis=0)
for count,index in enumerate(indices):
if count != WINDOW_SIZE//2:
loss += soft_out[index]
log_loss = -tf.math.log(loss)
# Context
loss_list.append(np.array(log_loss))
grad = tape.gradient(log_loss, [context_vector, center_vector])
opt.apply_gradients(zip(grad, [context_vector, center_vector]))
我们再次使用TSNE
在2D平面上可视化单词。
可视化结果显示,与CBOW相比有明显的改进,类似的单词逐渐形成了集群。我们可以看到,years已经形成了自己的集群。同样,代表句子不同部分的单词也形成了它们的小集群。
Skip-gram visualization
总而言之,我们的中心向量嵌入V保持不变,它遍历上下文向量嵌入U中的所有可能单词,如GIF所示。从所有可能单词的softmax分布中,根据给定的一组目标上下文单词计算softmax损失。
Computations involved in Skip-Gram
损失与梯度
为了分析损失情况并研究可视化效果,我们对两种模型结构进行了两个语料库测试。第一个是一个自制的随机句子的微小语料库,而另一个是一个单一主题的较大的语料库。因此,我们将具有较大语料库的模型称为“规模扩展(scaled-up)”。
让我们看一下使用的语料库很小时CBOW架构的损失。由于我们处理单词而不仅仅是数字,因此这些尖峰一定会存在。
在处理大型语料库时,CBOW的损失在理论上要比较小的语料库好,但是仔细检查会发现损失的峰值仍然存在。
与小型语料库CBOW体系结构相比,skip-gram体系结构的表现更差,稍后我们将在可视化中看到类似的情况。 损失相对高于CBOW的损失。
“规模扩展”的Skip-Gram的损失也仍然比CBOW的损失高,但在单词联想的可视化中有一些变化出现。
可视化
CBOW (小型语料库)
用于CBOW的小型语料库会创建几个有意义的单词关联,例如个人名称分组或体育名称分组。电影的类型(西方,东方)也被分组在一起。
CBOW(大语料库)
CBOW在大语料库中的可视化不令人满意。Years可以说是唯一可区分的分组。
Skip-Gram(小语料库)
与CBOW的嵌入相比,使用小语料库的Skip-Gram产生的结果不令人满意。我们可以看到那里的一些运动名称已经组合在一起。但是除此之外,真的很难区分其他嵌入及其相关含义。
Skip-Gram(大语料库)
但是,Skip-Gram的放大版本为我们提供了非常好的关联。我们可以清楚地看到years都被分到一个单一的集群,而像a,an和at这样的单词找到了自己的集群。在左侧,我们看到as, about有自己的集群。就构成的单词联想而言,规模扩大的Skip-Gram毫无疑问击败了CBOW。
附录
损失情况
在这里,我将尝试得出关于模型参数的损失梯度。 对于词汇表中的每个ww,模型的参数包括uwu_{w}和vwv_{w}。 我们使用随机uwu_{w} 和vwv_{w} 初始化模型,然后根据目标函数 J(θ)J(\theta)的梯度更改它们的值。
∂J(θ)∂vt\frac{\partial J(\theta)}{\partial v_{t}}
在本节中,我们将研究目标函数针对中心单词的矢量表示的梯度的推��。
最后一个方程式为我们提供了直观的梯度模型(斜率)。 我们用观察到的上下文向量(uou_o)减去期望的上下文词向量(∑x=1VP(ux∣vc)ux)。
∂P(J(θ)∂uo\frac{\partial P( J(\theta)}{\partial u_{o}}
最后一个方程式为我们提供了直观的梯度模型(斜率)。 我们用观察到的上下文向量(uo)减去期望的上下文词向量(∑x=1VP(ux∣vc)ux)。在本节中,我们将研究目标函数相对于上下文词的矢量表示的梯度的推导。
结论和进一步阅读
第二部分链接
我们致力于建立读者对单词的分布式表示的信心。 一旦建立了直观感受,我们就可以轻松理解其强大之处并将该概念应用于其他地方。 Word2Vec是一个简单的概念,但却是一个相当漂亮的想法。
可以通过参考以下链接来丰富对词嵌入的理解:
斯坦福大学:
Yannick:
论文:
- https://arxiv.org/pdf/1301.3781.pdf
- https://proceedings.neurips.cc/paper/2013/file/9aa42b31882ec039965f3c4923ce901b-Paper.pdf
- https://www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf
教程:
一些快速入门教程视频:
-
https://www.youtube.com/watch?v=QyrUentbkvw&t=195s
我要感谢Krisha Mehta对附录的指导和审阅。 在我们的下一篇文章中,我们将重点放在负采样(Negative Sampling)和GloVe嵌入上。 😄
作者:
名称 | GitHub | |
---|---|---|
Devjyoti Chakrobarty | @Cr0wley_zz | @cr0wley-zz |
Aritra Roy Gosthipaty | @ariG23498 | @ariG23498 |