Word2Vec理解与代码演示

简介

Word2Vec被认为是自然语言处理(NLP)领域最大的、最近的突破之一。在谷歌、百度等搜索引擎搜索时会返回多个关于如何在标准库(如Gensim和TensorFlow)中实现Word2Vec的结果。另外,对于想了解word2vec底层原理的人来说,可以看看Tomas Mikolov文章(https://arxiv.org/pdf/1301.3781.pdf ),或者更进一步从代码了解word2vec的底层原理,作者使用C实现了原始版本。本文是基于python语言讲解的word2vec,主要使用NumPy这个包实现了Word2Vec。

引言

Word2Vec的目标是为自然语言处理任务生成单词(中文词汇)的向量表示。每个词向量通常有几百个维度(由自己指定,一般为256、512、768等),每个唯一的词在语料库的向量空间中有一个唯一的向量表示。例如,“happy”一词在4维向量空间中可以表示为[0.24,0.45,0.11,0.49],“sad”可以表示为[0.88,0.78,0.45,0.91]。从词到向量的转换也称为词嵌入。在深度学习和机器学习中,计算机只能对数字进行线性代数运算而不能对字符串进行代数运算,所以要将字符串转换为词向量。
要实现Word2Vec,一般有两种方法可供选择——Continuous Bag-Of-Words (CBOW)或continuous Skip-gram (SG)。CBOW尝试根据相邻的单词(上下文单词)预测输出(目标单词),而连Skip-gram则根据目标单词预测上下文单词。实际上,Word2Vec是基于分布假设的,即每个单词的上下文都在它附近的单词。因此,通过观察邻近的单词,我们可以尝试预测目标单词。
根据Mikolov的说法,以下是Skip-gram和CBOW的区别:
1.Skip-gram: 可以很好地处理少量的训练数据,甚至可以很好地表示罕见的单词或短语。
2.CBOW: 训练的速度比skip-gram快几倍,对常用单词的准确性稍好。
由于Skip-gram根据给定的单词来预测上下文单词,因此,如果两个单词(一个不经常出现,另一个更频繁出现)并排放置,则当对两个单词进行并排放置时,它们将具有相同的处理方式,由于每个单词都将同时被视为目标单词和上下文单词,因此可以最大程度地减少损失。 在CBOW中,不经常使用的单词将仅是用于预测目标单词的上下文单词集合的一部分。 因此,模型将为不频繁的单词分配较低的概率。
两种方法的神经网络架构图示如下:

在这里插入图片描述
在本文中主要讲述Skip-gram架构,工作包含以下部分:
数据准备: 定义语料库,清理,规范化和标记化单词。
超参数: 学习率,轮次,窗口大小,嵌入大小。
生成训练数据: 建立词汇表,对单词进行one-hot编码,建立字典,将id映射到单词,将单词映射到id。
模型训练: 将编码的单词向前传递,计算错误率,使用反向传播调整权重并计算损失。
推论: 获取单词向量并找到相似的单词。
进一步的改进: 使用Skip-gram负采样(SGNS)和Hierarchical Softmax加快训练时间。

实现过程

在本文中,我们将实现Skip-gram架构。为方便阅读,内容分为以下几部分:

  1. 数据准备: 定义语料库,清理,规范化和标记化单词。
  2. 超参数: 学习率,轮次,窗口大小,嵌入大小。
  3. 生成训练数据: 建立词汇表,对单词进行onehot编码,建立字典,将id映射到单词,将单次映射到id。
  4. 模型训练: 将编码的单词向前传递,计算错误率,使用反向传播调整权重并计算损失。
  5. 推论: 获取单词向量并找到相似的单词。
  6. 进一步的改进: 使用Skip-gram负采样(SGNS)和Hierarchical Softmax加快训练时间。

1. 数据准备

以如下预料为例:

natural language processing and machine learning is fun and exciting

这里没有删除停用词“and”和“is”。
文本数据是非结构化的,并且可能是“脏数据”。 清理它们将涉及一些步骤,例如删除停用词,标点符号,将文本转换为小写字母(取决于实际用例),替换数字等。 Gensim还提供了使用gensim.utils.simple_preprocess执行简单文本预处理的功能,该功能将文档转换为小写标记列表,而忽略太短或太长的标记。

text = "natural language processing and machine learning is fun and exciting"
corpus = [[word.lower() for word in text.split()]]

使用上述代码预处理之后,对语料库进行标记化。 结果如下:

[“natural”, “language”, “processing”, “ and”, “ machine”, “ learning”, “ is”, “ fun”, “and”, “ exciting”]

2. 超参数

在进入实际的实现之前,先定义一些稍后需要的超参数。

settings = {
	'window_size': 2	# context window +- center word
	'n': 10,		# dimensions of word embeddings, also refer to size of hidden layer
	'epochs': 50,		# number of training epochs
	'learning_rate': 0.01	# learning rate
}

[window_size]: 将window_size定义为2的地方,表示在目标词左右两边的词被视为上下文词。随着窗口的滑动,语料库中的每个单词都将成为目标单词。
[n]: 这是词嵌入的大小(隐藏层的大小),通常取决于使用的词汇数量。
[epochs]: 这是训练时期的数量。 在每个时期,我们循环浏览所有训练样本。
[learning_rate]: 学习率控制相对于损失梯度的权重调整量。
在这里插入图片描述
从上图可以看出window_size=2的词袋含义,第一个词前边没有词,因此只有后两个词作为上下文词汇,第二个词只有一个前文词汇两个后文词汇,之后的都是前边两个,后边两个。最后两个与前两个相同,缺失的是前文词汇。

3. 生成训练数据

在本节中主要目标是将语料库转换为Word2Vec模型要进行训练的one-hot编码表示。 从语料库中,有10个窗口(从#1到#10),每个窗口包含目标词和上下文词,如下所示。 每个窗口均由目标词及其上下文词组成,分别以橙色和绿色突出显示:
在这里插入图片描述

其中第一个和最后一个训练窗口中的第一个和最后一个元素的示例如下所示

# 1 [Target (natural)], [Context (language, processing)]
[list([1, 0, 0, 0, 0, 0, 0, 0, 0])
list([[0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0, 0]])]
*****#2 to #9 removed****
#10 [Target (exciting)], [Context (fun, and)]
[list([0, 0, 0, 0, 0, 0, 0, 0, 1])
list([[0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0]])]

为了生成性训练数据,首先初始化word2vec()对象。

# Initialise object
w2v = word2vec()
# Numpy ndarray with one-hot representation for [target_word, context_words]
training_data = w2v.generate_training_data(settings, corpus)

在函数generate_training_data中,执行以下操作:
self.v_count: 词汇长度(指语料库中唯一词的数量)。
self.words_list: 词汇表(也是唯一词)。
self.word_index: 字典,每个键作为词汇中的单词,值作为索引。
self.index_word: 字典,每个键作为索引,值作为单词中的单词
**for: ** 循环使用word2onehot函数将每个目标及其上下文单词的one-hot向量表示附加到training_data。

class word2vec():
  def __init__(self):
    self.n = settings['n']
    self.lr = settings['learning_rate']
    self.epochs = settings['epochs']
    self.window = settings['window_size']

  def generate_training_data(self, settings, corpus):
    word_counts = defaultdict(int)
    for row in corpus:
      for word in row:
        word_counts[word] += 1
    self.v_count = len(word_counts.keys())
    self.words_list = list(word_counts.keys())
    self.word_index = dict((word, i) for i, word in enumerate(self.words_list))
    self.index_word = dict((i, word) for i, word in enumerate(self.words_list))

    training_data = []
    for sentence in corpus:
      sent_len = len(sentence)
      for i, word in enumerate(sentence):
        w_target = self.word2onehot(sentence[i])
        w_context = []
        for j in range(i - self.window, i + self.window+1):
          if j != i and j <= sent_len-1 and j >= 0:
            w_context.append(self.word2onehot(sentence[j]))
        training_data.append([w_target, w_context])
    return np.array(training_data)

  def word2onehot(self, word):
    word_vec = [0 for i in range(0, self.v_count)]
    word_index = self.word_index[word]
    word_vec[word_index] = 1
    return word_vec

4. 模型训练

有了training_data,可以训练模型了。训练从w2v.train(training_data)开始,首先传入训练数据并调用函数train。Word2Vec模型由2个权重矩阵(w1和w2)组成,出于演示目的,分别将值初始化为形状(9x10)和(10x9)。 这有助于反向传播误差的计算,这将在本文的后面部分介绍。 在实际训练中,应该随机初始化权重(例如,使用np.random.uniform())
在这里插入图片描述

# Training
w2v.train(training_data)

class word2vec():
  def train(self, training_data):
     # getW1 - shape (9x10) and getW2 - shape (10x9)
     self.w1 = np.array(getW1)
     self.w2 = np.array(getW2)
     # self.w1 = np.random.uniform(-1, 1, (self.v_count, self.n))
     # self.w2 = np.random.uniform(-1, 1, (self.n, self.v_count))
 

最后两个注解部分是实际应该做的操作。

训练–前向传递

接下来,使用第一个训练示例开始训练第一个轮次,方法是将代表目标单词的一个one-hot向量的w_t传递给forward_pass函数。 在forward_pass函数中,在w1和w_t之间计算点积以产生h。 然后,使用w2和h进行另一个点积计算,以生成输出层u。 最后,对u进行softmax运算,以强制每个元素在0和1的范围内,从而在返回预测向量,在隐藏层h和输出层u的向量之前,得到提供预测的概率。
以显示第一个窗口(#1)中第一个训练样本的计算,其中目标词是“natural”,上下文词是“language”和“processing”。
在这里插入图片描述

训练-误差,反向传播和损失

使用y_pred,h和u,继续针对目标和上下文单词的这一特定集合计算误差。 这是通过将y_pred与w_c中每个上下文词之间的差异相加来完成的。
在这里插入图片描述
接下来,使用反向传播函数backprop来计算调整误差,使用反向传播函数通过传递误差EI,隐藏层h和目标词w_t的向量来更新权重。要更新权重,我们将要调整的权重(dl_dw1和dl_dw2)乘以学习率,然后从当前权重(w1和w2)中减去它。

class word2vec():
  ##Removed##
  
  for i in range(self.epochs):
    self.loss = 0
    for w_t, w_c in training_data:
      y_pred, h, u = self.forward_pass(w_t)
      EI = np.sum([np.subtract(y_pred, word) for word in w_c], axis=0)
      self.backprop(EI, h, w_t)
      self.loss += -np.sum([u[word.index(1)] for word in w_c]) + len(w_c) * np.log(np.sum(np.exp(u)))
    print('Epoch:', i, "Loss:", self.loss)

  def backprop(self, e, h, x):
    dl_dw2 = np.outer(h, e)
    dl_dw1 = np.outer(x, np.dot(self.w2, e.T))
    self.w1 = self.w1 - (self.lr * dl_dw1)
    self.w2 = self.w2 - (self.lr * dl_dw2)

损失:最后,根据损失函数,在完成每个训练样本后计算总损失。损失函数包括2个部分。 第一部分是输出层中所有元素的总和的负数(在softmax之前)。 第二部分获取上下文词的数量,并将输出层中所有元素(在指数之后)的和对数相乘。公式如下:
在这里插入图片描述

得到一个单词的向量

使用一组经过训练的权重,要做的第一件事就是查看词汇表中某个单词的单词向量。 可以简单地通过针对训练的权重(w1)查找单词的索引来做到这一点。 在下面的示例中,例如查找单词“ machine”的向量。

# Get vector for word
vec = w2v.word_vec("machine")

class word2vec():
  ## Removed ##
  
  # Get vector from word
  def word_vec(self, word):
    w_index = self.word_index[word]
    v_w = self.w1[w_index]
    return v_w

查找相似的词

另一件事是找到类似的单词。 即使词汇量很小,仍然可以通过计算单词之间的余弦相似度来实现vec_sim函数。。

w2v.vec_sim("machine", 3)

class word2vec():
  def vec_sim(self, word, top_n):
    v_w1 = self.word_vec(word)
    word_sim = {}

    for i in range(self.v_count):
      v_w2 = self.w1[i]
      theta_sum = np.dot(v_w1, v_w2)
      theta_den = np.linalg.norm(v_w1) * np.linalg.norm(v_w2)
      theta = theta_sum / theta_den

      word = self.index_word[i]
      word_sim[word] = theta

    words_sorted = sorted(word_sim.items(), key=lambda kv: kv[1], reverse=True)

    for word, sim in words_sorted[:top_n]:
      print(word, sim)

References

[1] https://derekchia.com/an-implementation-guide-to-word2vec-using-numpy-and-google-sheets/
[2]: https://github.com/nathanrooy/word2vec-from-scratch-with-python
[3]: https://nathanrooy.github.io/posts/2018-03-22/word2vec-from-scratch-with-python-and-numpy/
[4]: https://stats.stackexchange.com/questions/325053/why-word2vec-maximizes-the-cosine-similarity-between-semantically-similar-words
[5]: https://towardsdatascience.com/hierarchical-softmax-and-negative-sampling-short-notes-worth-telling-2672010dbe08

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凉寒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值