最近学习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
]
=
[
0.1
0.2
0.3
0.7
0.8
0.9
0.1
0.2
0.3
]
\begin{bmatrix} 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 \end{bmatrix}\begin{bmatrix} 0 \\ 2 \\ 3 \end{bmatrix}=\begin{bmatrix} 0.1 & 0.2 &0.3 \\ 0.7 & 0.8 &0.9 \\ 0.1 & 0.2 &0.3 \end{bmatrix}
⎣⎢⎢⎢⎢⎡0.10.40.70.20.50.20.50.80.10.40.30.60.90.30.6⎦⎥⎥⎥⎥⎤⎣⎡023⎦⎤=⎣⎡0.10.70.10.20.80.20.30.90.3⎦⎤
- 我们所关心的负采样的计算过程实际上只用了一行代码,也就是loss的定义,
tf.nn.nce_loss()
函数完成了这部分工作,这里我们提供了nce_weights 这与embeddings的维数一样,之前提到word2vec中每个单词会使用两个词向量表示,这就是另一个,负采样使用的词向量就是从这里面取出来的。在这些参数中,注意num_sampled
,这是负采样的数量,也就是对于一个正例,我们使用多少个单词为负例,num_classes
是分类数目,在原理中看这个比较清楚,其实对于一个输入v
v
v,我们实际上是找到
p
(
w
∣
v
)
p(w|v)
p(w∣v)最大的那个
w
w
w作为预测,这实际上也就是一个分类,正确的类别也就是
w
w
w,总类数就是vocabulary_size。接下来具体介绍一下
nce_loss()
函数的负采样机制:
在介绍负采样的文章中,我们总能看到一张图:
关于这张图的含义不再赘述,这在文中开始介绍的数学原理的文章中介绍的相当详细了。如果指定num_sampled
的值是64,也就是除了1个正类之外,从其他的N-1
类中选择64个类作为负类,关于这个负类的选择,肯定是词频越高,选到的可能性就越大,这也是在原理中提到的,也就是负采样的主要内容了,这一想法tf.nn.nce_loss()
函数内部实现,这里选择负类的计算公式:
P(k) = (log(k + 2) – log(k + 1)) / log(range_max + 1)
P
(
k
)
=
l
o
g
(
k
+
2
)
−
l
o
g
(
k
+
1
)
l
o
g
(
r
a
n
g
e
_
m
a
x
+
1
)
P(k) = \frac{log(k+2)-log(k+1)}{log(range\_max+1)}
P(k)=log(range_max+1)log(k+2)−log(k+1)
这里k也就是选择的某个词语的序号,观察这个公式可以发现,k越小,计算的结果越大,也就是选择的概率越大,这也就是之前创建数据集的时候,为什么要按照词频降序排序并编号的原因。这样负采样的过程真相大白了,接下来的优化,计算梯度在TensorFlow中当然就不需要我们手写了,直接定义优化器优化即可。
4.关于词向量相似度
在验证部分,使用了向量相似度的概念:
norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), axis=1, keep_dims=True))
norm_embeddings = embeddings / norm
valid_embeddings = tf.nn.embedding_lookup(norm_embeddings, valid_data)
similarity = tf.matmul(valid_embeddings, norm_embeddings, transpose_b=True)
首先进行词向量的标准化,也就是全部规范到 [0,1] 之间,并且每个分量的和为1,得益于广播机制,直接进行矩阵之间的计算即可。
关于向量之间相似的度量,一种普遍的做法就是余弦相似度,也就是在向量空间中,求向量的余弦夹角。余弦的范围是[-1, 1],如果两向量余弦为-1,表示完全相反,完全不同,如果是1,那么同向,认为相似度最高。
所以先对向量进行标准化,然后利用矩阵乘法,直接使用选择的验证向量乘以标准化的词向量,那么所得得值(一个内积)就是相似得度量,之后可以看到,对这些值进行排序,也就可以比较出那些向量最相似了。
CBOW
CBOW是由上下文预测中心词的方式,在代码实现上,主要有两个不同。
第一处是在生成批次训练数据时:
data_index = 0
def generate_batch(batch_size, bag_window):
global data_index
span = 2 * bag_window + 1
batch = np.ndarray(shape=(batch_size, span - 1), dtype=np.int32) # 列维数不再是1,改成span-1
labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
buffer = collections.deque(maxlen=span)
for _ in range(span):
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
for i in range(batch_size):
buffer_list = list(buffer)
labels[i, 0] = buffer_list.pop(bag_window) # 对应的标签 也就是中心词 ,并弹出这个中心词
batch[i] = buffer_list
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
return batch, labels
首先,CBOW既然是上下文多个词语预测中心词,所以自然需要拿到多个词语的索引,在这里就是span个(2 * bag_window,bag_window也就是窗口半径),其次,在标签和数据上,可以看到这与Skip-Gram方式就是做了一个反转,注意要在队列中弹出中心词(所在的位置就是bag_window)。
loss = tf.reduce_mean( tf.nn.nce_loss(nce_weights, nce_biases, train_labels, tf.reduce_sum(embeds, 1), num_sampled, vocabulary_size))
这里要注意的是tf.reduce_sum(embeds, 1)
,也就是对batch中一条数据进行了词向量维度上的求和,这对应CBOW原理中投影层所作的操作,主要是简化计算。
之外地其他计算与Skip-Gram一致。
补充
对于有些数据集,可能需要手动去除高频停用词,这里提供一个函数:
代码如下:
def remove_fre_stop_word(words):
'''
去掉一些高频的停用词 比如 的之类的
:param words: 词语列表
:return: 剔除高频停用词之后的词语列表
'''
t = 1e-5 # t 值
threshold = 0.8 # 剔除概率阈值
int_word_counts = collections.Counter(words)
total_count = len(words)
word_freqs = {w: c / total_count for w, c in int_word_counts.items()}
# 计算被删除的概率
prob_drop = {w: 1 - np.sqrt(t / f) for w, f in word_freqs.items()} # 计算删除概率
train_words = [w for w in words if prob_drop[w] < threshold]
return train_words
这里判断词语是否符合的计算公式是:
P
(
w
i
)
=
1
−
t
f
(
w
i
)
P(w_{i})=1-\sqrt{\frac{t}{f(w_{i})}}
P(wi)=1−f(wi)t
t
是一个阈值,一般是在 1e-3
到1e-8
之间,f(w)
表示单词的频率。
代码
全部代码放在cbow+skip-gram,可以参考 Skip_gram_demo
代码,使用得数据集是训练word2vec得经典数据集 text8。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/116705.html