作者:十方,三品炼丹师
本篇接上篇《都步入2021年,别总折腾"塔"了》,继续学习nlp。我们在做推荐系统的时候,所有离散特征(连续值也可以分桶处理)都给embedding了,nlp中也一样,每个单词,每个字,每个标点,都可以做embedding。那么问题来了,推荐系统的学习目标是点击率,那nlp中学词embedding的目标是啥?上文我们提到计数(上下文单词做BOW)的方法,生成每个词的稠密向量 。这种方法虽然不需要设定任何目标,但是靠谱吗?答案是非常不靠谱,语料库的单词有百万级别,百万*百万的矩阵,计算是不现实的,用降维方法都是要耗费大量的计算资源和时间,这时候word2vec的优势就体现出来了。
word2vec
word2vec本质上,就是充分挖掘了句子中上下文和单词的关系。前面说到要确定学习目标,所以我们可以构造完形填空的样本给模型去学习,比如"我__你",让模型学习这里应该填什么词,只要你给的自然语料库足够好(噪声小),那么模型学到的这个空填"爱"、“和”、“想”等等的概率,一定是大于其他词的,所以模型最终学到的是P(word要填的词|Context),同理我们也可以把上下文都弄成空白"_和_",让模型去学习P(Context是否包含某词|核心词)。其实就是CBOW和SKIP-GRAM两个模型目标了。十方以前刚学习nlp的时候,经常弄混CBOW和SKIP-GRAM,其实就是前者用上下文预测核心词,后者用核心词预测上下文包含的词。引用王喆老师一张经典的图。
看了这张图,脑海中应该已经浮现了tf的源码了吧。我们重点要讨论下下面这张图:
我们可以看到CBOW的方法里,出现了两个矩阵W和W',V是词典大小,N是embeding size。重点是W和W‘的转置,都是V*N,那我们到底用哪个作为w2v的embeding呢?这里有三种方案:
- 只使用W
- 只使用W‘的转置
- 同时使用两个权重
其实每种方案都是合理的,GloVe算法就是将两个权重相加,也取得了很好的效果。还有个值得思考的点是,中间层需不需要激活函数?对于CBOW和SKIP-GRAM这种简单的模型是不需要的,但是我们看下03年的论文,用前n-1个词去预测第n个词,来学习词向量,学习目标是p(wn | w1, w2, w3, ... , wn-1),如下图:
这里中间层又出现了tanh,所以W2V中间层如果用了激活函数会怎么样呢?欢迎评论区留言讨论。
最后一个问题是,在CBOW中,W'直接用W的转置可以不可以?直接共享参数。个人感觉当然可以,虽然十方没做过类似尝试。
关于复现
代码语言:javascript复制 embeddings = tf.Variable(tf.random_uniform(shape=(vocabulary_size, embedding_size), minval=-1.0, maxval=1.0))
softmax_weights = tf.Variable(tf.truncated_normal(shape=(vocabulary_size, embedding_size), stddev=1.0 / math.sqrt(embedding_size)))
softmax_biases = tf.constant(np.zeros(shape=(vocabulary_size), dtype=np.float32))
embed = tf.nn.embedding_lookup(embeddings, tf_train_dataset)
inputs = tf.reduce_sum(embed, 1)
loss = tf.reduce_mean(
tf.nn.sampled_softmax_loss(
softmax_weights, softmax_biases, inputs, tf_train_labels, num_sampled, vocabulary_size
)
)
optimizer = tf.train.AdagradOptimizer(1.0).minimize(loss)
十方很早就尝试过复现w2v,最初写出来的就类似这样,本以为这样就结束了,最后直接用embedings向量就可以了,然后去github上搜源码看,然后看到下文还有norm的操作。这里十方就要提醒大家,在真正自己要复现的时候,真的要多关注细节,多读别人复现的源码。下面是做norm的代码。
代码语言:javascript复制 norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
# W
normalized_embeddings = embeddings / norm
norm_ = tf.sqrt(tf.reduce_sum(tf.square(softmax_weights), 1, keep_dims=True))
normalized_softmax_weights = softmax_weights / norm_
norm_ = tf.sqrt(tf.reduce_sum(tf.square(normalized_softmax_weights normalized_embeddings), 1, keep_dims=True))
# W‘
normalized_embeddings_2 = (normalized_softmax_weights normalized_embeddings) / 2.0 / norm_
"简单"的word2vec
其实看似简单的word2vec,并没有那么简单,涉及到的细节很多。例如CBOW最后softmax,如果有100w单词,softmax计算效率是很低的,这就牵涉到loss的优化了。再例如CBOW和SKIP-GRAM需要用一个窗口构建样本训练,很难学到整体的词与词的关系,上文提到的GloVe就融合了矩阵分解的思想和滑窗,取得了非常出色的效果。虽然BERT现在秒天秒地,并不意味着w2v这些经典的算法我们不用去学习了,这些算法的思想,很多是可以借鉴的。