Word2vec代码理解

Word2vec

word2vec一开始主要是运用在NLP中,进行相邻词语的预测。列如“I HAVE A DREAM”如果我们把have当作中心词的话,如果滑动窗口是2,那么他的相邻词则是I A DREAM。这是我们要达到的目的。
skip-gram
如上图所示,我们主要是要求出在中心词为t的情况下邻近词 w t + j w^{t+j} wt+j的概率,如上图所示,因为我们假设每个单词预测的概率都是相互独立的,所以我们把它进行乘法,采用了最大似然估计。那么具体在代码中我们要怎么将word2vec进行实现呢?

建立词汇表

首先我们应该做的就是建立我们的词汇表,将我们所有用到的词语全部保存在里面。例如:“I HAVE A DREAM ”他所对应的词汇表我们可以表示为[I,HAVE,A,DREAM]这样的一个列表。而每个词语则有对应的下标,这样我们就可以表示出类似 HAVE:1这样的一个字典。然后我们把所有的词语加入我们的词汇表,当然我们现在的词语还很少,并且恰好没有重复的词语,当词语多了之后我们需要去重之后建立我们的词汇表。

	count = [['UNK', -1]]
	count.extend(collections.Counter(words).most_common(vocabulary_size - 1))	 # 在这里就已经去重了
	dictionary = dict()
	for word, _ in count:
		dictionary[word] = len(dictionary)  # 将单词和索引放在一起

同时我们还需要存储本来的句子顺序,因为这在我们后面进行配对的时候有着至关重要的作用。

Skip-gram

接下来我们就需要进行skip-gram了,在这个过程中我们有几个至关重要的参数。首先是batch_size,我们在训练当中如果词汇量很大的情况下我们是不会选择将所有的词带入进行计算的,而是选取一部分一部分分批进入进行训练。
然后是我们的skip_window,这个函数代表的是我们滑动窗口的大小,例如我们把A作为中心词,如果skip_window为1,那么他的邻近词则是HAVE和DREAM,如果滑动窗口为2,则他的邻近词可以是I,HAVE,DREAM。所以我们可以看出,滑动窗口的大小也可以表示我们选取邻近词的范围。
最后是num_skips,这个参数的意义主要是用来规定我们的skip窗口中我们要选取多少个单词进行配对,用作我们的label_word.

def generate_batch(batch_size, num_skips, skip_window):
	# dived the data into batch_size batch and return the batches and the labels
	global data_index
	assert batch_size % num_skips == 0  # each word pair is a batch, so a training data [context target context] would increase batch number of 2.
	assert num_skips <= 2 * skip_window
	batch = np.ndarray(shape=(batch_size), dtype=np.int32)
	labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
	span = 2 * skip_window + 1 # [ skip_window target skip_window ]
	buffer = collections.deque(maxlen=span)   # buffer 长度为3的队列
	for _ in range(span):    # span=3
		buffer.append(data[data_index])
		# print(data_index)
		data_index = (data_index + 1) % len(data)
	for i in range(batch_size // num_skips):    # batch_size=8 , num_skips=2
		target = skip_window  # target label at the center of the buffer
		targets_to_avoid = [ skip_window ]

		for j in range(num_skips):
			while target in targets_to_avoid:
				target = random.randint(0, span - 1)      # randint两边都取得到
			targets_to_avoid.append(target)

			batch[i * num_skips + j] = buffer[skip_window]    # 取到buffer[1]的单词下标

			labels[i * num_skips + j, 0] = buffer[target]     # 取到span范围内的随机下标   0,1,2

		buffer.append(data[data_index])  # buffer继续加入其他单词下标
		data_index = (data_index + 1) % len(data)

	return batch, labels

上述代码中还有一个关键的变量是data_index,这个变量的主要作用是用来更新我们的队列,因为我们的队列当中存放的是我们的中心词以及相应的邻近词,但是由于我们是分批进行训练的,所以我们定义了全局变量data_index用来不断自增,以保证我们的队列是在不断更新的。在这个函数中我们得到了batch以及对应的label,其中他们的维度是由batch_size所规定的。至于num_skips的配对过程是在for j循环当中进行的,skip_window不变,target每次都取到不同的邻近词下标,从而进行配对。

embedding

接下来我们要做的就是embeding了,首先我们需要确定的是embedding_size,这个主要是将我们的单词进行降维表示,列如我们有1万个单词的话,我们要用one-hot编码表示的话,我们需要用1万维的向量进行表示。但是我们现在不想用那么多来进行表示,所以我们需要对它进行降维,这里embedding_size一般是自己决定的。

	embeddings = tf.Variable(
						tf.random.uniform([vocabulary_size, embedding_size], -1.0, 1.0))  # 嵌入表矩阵  embedding=128

这里主要是用了一个正太分布,通过满足正态分布的随机数来初始化神经网络中的参数是一个非常常用的方法。这个嵌入矩阵就是我们的输入层和隐层之间的这么一个权重矩阵。我们最后的目标也就是优化这么一个权重矩阵,希望这个矩阵可以只在128维就可以表示出我们的网络。
在一开始我们说到输入层是one-hot编码进行表示每个单词的,我们这里再进行了这么一个嵌入矩阵,就可以得到一个batch_sizeembedding_size的隐层。
one-hot
如上图所示,当我们将ont-hot表示的词向量与embedding矩阵进行相乘可以得到一个1
embedding_size的向量,那么当我们输入batch_size的词向量,我们就可以得到batch_size*embedding_size的隐层。但我们每次都将这么一个矩阵相乘带入计算是十分冗余的,因为这个运算其实只有1的时候有效,相当于选取1所在列的那一行,这其实不需要进行矩阵乘法也可以得到。所以在代码中我们使用

embed = tf.nn.embedding_lookup(embeddings, train_dataset)

来进行运算,也可以得到相同的效果

context-embedding/softmax_weights

	softmax_weights = tf.Variable(
						tf.random.truncated_normal([vocabulary_size, embedding_size],          
											stddev=1.0 / math.sqrt(embedding_size)))

我们可以知道我们的每个词语其实是有两个向量来进行表示的,一个是当它成为中心词的时候它需要一个向量表示,而它成为邻近词的时候他也需要一个向量进行表示。所以我们需要将这个权重也进行一个初始化。当中心词与邻近词的概率相乘得到的就是他们两之间的条件概率,即文章开头提到的公式
skip-gram
这就是我们想要得到的概率值,且这个概率值越大越好。

输出层

而我们一开始提到我们的词语都是用一个one-hot编码进行初始表示的,那么当我们最后输出的时候也需要是一个vocabulary_size的一个向量。
输出层所经历的计算
如上图所示我们隐层得到的batch_size*embedding_size的一个矩阵,其中每一行表示对一个词向量的表示,而我们希望在中心词是这个词向量的条件下,邻近词是相应的邻近词的概率越大越好,因此当隐层的一行向量与对应的context-embedding/softmax_weights相乘得到的一个概率值越大越好,而我们输出层是由one-hot进行表示的,则我们希望邻近词所对应的ont-hot表示与我们的输出应该越接近越好。但是一个中心词是有多个邻近词的,那么我们则需要取一个折中的权重使得这些邻近词的概率最大。

	loss = tf.reduce_mean(
						tf.nn.sampled_softmax_loss(softmax_weights, softmax_biases,
													train_labels, embed, num_sampled, vocabulary_size)) 
	optimizer = tf.train.AdagradOptimizer(1.0).minimize(loss)  # learning-rate = 1.0

这里具体怎么运算的我不是很清楚,但我认为可以将邻近词本身对应的one-hot向量进行一个相加,得到一个由多个1和0组成的vocabulary_size大小的向量,然后进行损失计算。又或者只是单纯的使得对应的概率越大越好。在损失计算的过程中我们可以得到如下的变换
最小化损失函数
如上图所示,我们最大化公式1的概率就可以等价为最小化这个loss函数,而我们的输出层是经过了softmax的,所以
输出层函数
上图是一个进行了softmax的输出层函数,我们现在需要把他带入进行求导,从而使得损失函数最小化。其中o表示邻近词,c表示中心词。
带入公式求导
但是我们可以发现这里的时间复杂度是和我们的节点大小V有关的,如果我们训练的词汇过多,这显然是十分庞大的计算量。
因此word2vec论文提出了负采样的优化方法。就是通过噪声词的加入从而对复杂度进行优化。简单来说就是将中心词wc生成背景词wo用一下两个相互独立的事件联合组成来近似。
1.即中心词wc和背景词wo同时出现在该训练窗口
2.即中心词和第K个噪声词不同时出现在训练窗口
同时出现
如上图所示,我们表现的是背景词和中心词同时出现在训练窗口的概率,并用sigmod函数进行表示。
联合概率

tf.nn.sampled_softmax_loss(softmax_weights, softmax_biases,
													train_labels, embed, num_sampled, vocabulary_size)

代码实现如图所示,至此我们就可以不断进行更新,最后得到能够表示网络或词汇的嵌入矩阵embeddings.

参考博客: 雷锋网
代码是B站大佬的git: B站UP

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值