最近学习word2vec,发现一些文章写的有点,略。。(>﹏<),而且有些代码有错误,这里记录一些学习代码过程中的问题,这里构建的方式是Skip-Gram,代码不全部写出,只写一些觉得重要的地方。
首先,如果想要了解详细的数学原理,可以移步word2vec中的数学原理,文档中写的非常非常详细,推荐度max。
另外,使用TensorFlow实现这两种,代码很大程度是一样的,这里主要介绍Skip-Gram方式的实现,CBOW只是简要注明不同之处。
Skip-Gram
1.构建数据集
这里主要是对词语列表进行更进一步的操作,例如生成词典并编号等等:
def generate_dataset(data):
num_words = len(data)
count = [['UNK', -1]]
count.extend(collections.Counter(data).most_common(vocabulary_size-1))
dic = {}
for val, count in count:
dic[val] = len(dic)
reverse_dic = dict(zip(dic.values(), dic.keys()))
num_data = []
for item in data:
if item in dic:
num_data.append(dic[item])
else:
num_data.append(0)
return dic, reverse_dic, num_data, num_words
说明:
- dic是生成的词典,按照词频从大到小选择了vocabulary_size个词语,注意特殊的
UNK
表示其他未收录的词语,所以真正使用Counter计数的时候添加的应该是vocabulary_size-1个词语,词典的组织形式是(词语–序号) - reverse_dic是反转的词典,也就是将词典的组织形式转化为(序号–词典)形式
- num_data与data的长度一样,只是将原本data中每个元素由具体单词(string)转化为序号(int)
- num_words是词典长度
- count的类型是
collections.Counter
- count中有一个
UNK
,主要是对于我们丢弃了一部分词语,例如之前去掉的高频停用词,或者规定很少出现的词语不纳入词典,将这些词语记为UNK
,可以看到在之后将词语列表转为序号列表时,将这些词标序号记为0 - 关于生成dictionary,生成的唯一编号使用的
len
函数,毕竟每次增加一个词语,那么词典的长度就会增加一,所以就生成了唯一的编号了(这个地方很简单,但是防止有的时候转不过弯来还是提一下)
2.生成batch数据
由全部数据生成一个batch的数据:
index_data = 0
def generate_batch(batch_size, n_skips, window_skip):
global index_data
assert batch_size % n_skips == 0
assert n_skips <= 2*window_skip
batch_x = np.ndarray(shape=(batch_size), dtype=np.int32)
batch_y = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
span = 2 * window_skip + 1
buffer = collections.deque(maxlen=span)
for _ in range(span):
buffer.append(num_data[index_data])
index_data = (index_data + 1)%num_words
for i in range(batch_size // n_skips):
target = window_skip
avoid_target = [window_skip]
for j in range(n_skips):
while target in avoid_target:
target = random.randint(0, span-1)
avoid_target.append(target)
batch_x[i*n_skips+j] = buffer[window_skip]
batch_y[i*n_skips+j] = buffer[target]
buffer.append(num_data[index_data])
index_data = (index_data+1) % num_words
return batch_x, batch_y
说明:
- 对于传入参数的理解:n_skip表示我们为每个中心词所生成的样本(或者说是训练数据)数目,window_skip表示半径,举个例子:I will go to school by bus. 假设到了某一步,中心词是 to ,window_skip是2,也就是使用 to 来预测 will go school by四个词语,但是我们不一定要生成所有的(to --> will)这样的数据,而是在中间随即选出 n_skip条数据,也就是在2window_skip条数据中选出n_skip条数据作为训练数据,所以才会要求 n_skip<=2window_skip,即函数开头的 assert 所判定的。至于另一个判定,主要是保证每个中心词所生成的数据条数都一样(对于每个中心词,都生成n_skip条数据,所以一个batch数量,应该是n_skip的整数倍)
- 关于batch_x, batch_y的维数问题,由于训练数据集其实就是一个词预测另一个词,所以它们的维数应该是一样的。但是可以看到batch_y被处理成了一个二维数组,这主要是因为下面使用的损失函数 nce_loss 的要求,其实(n, 1)的二维数组跟(n)维的列向量是一样的。
- 注意deque的滑动,这是一个队列,使用一个中心词生成一组数据后,就会向后滑动一个单词,在队首“挤”出一个单词,在队尾添加一个单词。
- 具体实现如何在 2*window_skip个词中随机选出n_skip个,这里使用了一个avoid_target数组,实现的很巧妙。
3.负采样计算
借助TensorFlow的函数,Skip_Gram网络的结构非常简单:
valid_exampled = np.random.choice(valid_window, valid_size, replace=False)
input_x = tf.placeholder(tf.int32, shape=[batch_size])
input_y = tf.placeholder(tf.int32, shape=[batch_size, 1])
valid_data = tf.constant(valid_exampled)
embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size]))
embed = tf.nn.embedding_lookup(embeddings, input_x)
nce_weight = tf.Variable(tf.truncated_normal([vocabulary_size, embedding_size],
stddev=1.0 / np.sqrt(embedding_size)))
nce_bias = tf.Variable(tf.zeros([vocabulary_size]))
loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weight,
biases=nce_bias,
labels=input_y,
inputs=embed,
num_classes=vocabulary_size,
num_sampled=num_sample))
optimizer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate).minimize(loss)
说明:
- 带有valid部分的代码都是用来生成验证数据的,主要是训练完成之后需要一个评价,可以随便生成一些验证数据,然后从词向量中按照相似度选出最相似的向量,看看以我们直观的感觉是否这些词是否类似。
- embeddings 就是词向量矩阵(从维数上就可以看出来),关于这个embed,使用了
tf.nn.embedding_lookup()
函数,这个函数的作用原理其实非常简单,如下面的公式所示,如果如果左边的大矩阵是词向量矩阵,也就是embeddings,那么提供一个索引矩阵,然后按照索引,这里是0,2,3,把第一个矩阵的中第0,2,3行拿出来组成一个矩阵。所以这个函数的两个参数分别是embeddings 和 input_x,也就是拿出这一个batch的训练数据所对应的行向量。
[ 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 0.2 0.1 0.3 0.5 0.4 0.6 ] [ 0 2 3 ]