Word2Vec也称Word Embeddings,中文的叫法为“词向量”或“词嵌入”,是一种非常高效的,可以从原始语料中学习字词空间向量的预测模型。
在Word2Vec出现之前,通常将字词转为One-Hot Encoder ,一个词对应一个向量(一个向量中只有一个1,其余皆为0),通常要将一篇文章中每一个词都转成一个向量,而整篇文章则变为一个稀疏矩阵。这样的方法没有考虑字词之间的关系,没有提供任何关联信息,例如我们对我们对动物 和 狗之间的从属关系,关联程度一无所知。且稀疏数据的训练的效率较低。
而向量空间模型(Vector Space Models)则可以有效的解决这个问题,它将字词转为连续值(相对于One-Hot编码的离散值)的向量表达,并且其中意思相近的词将映射到向量空间中相似位置。
Word2Vec就是一种计算非常高效的,可以从原始语料中学习字词空间向量的预测模型。它主要分为CBOW和Skip-Gram两种模式,其中CBOW是从原始语句(比如:中国的首都是_____)推测目标字词(比如:北京);而Skip-Gram则正好相反,它是从目标字词推测出原始语句。其中CBOW对小型数据比较合适,而Skip-Gram在大型语料中表现的更好,使用Word2Vec训练语料能得到一些非常有趣的结果,比如意思相近的词在向量空间中的位置会非常接近。
本文将使用Skip-Gram模式的Word2-Vec,以“the quick brown fox jumped over the lazy dog”为例,我们要构造一个语境与目标词汇的映射关系,其中语境包含一个单词的左边和右边的词汇,假设我们的滑窗尺寸为1,则以quick为例,其单词及语境包含(the, quick, brown),则对目标词汇quick,其对应的训练样本为(quick, the)和(quick, brown)。我们训练时,希望模型能够从目标词汇quick预测出语境the,同时也需要制造随机的词汇作为负样本(噪声),我们希望预测的概率分布在正样本the上尽可能大,而在随机的负样本上尽可能的小。这里的做法就是通过优化算法比如SGD来更新模型中Word Embedding的参数,让概率分布的损失函数(NCE Loss)尽可能的小。这样每个单词的词向量就会随着训练过程不断调整,直到处于一个最适合语料的空间位置。这样我们的损失函数最小,最符合语料,同时预测出正确单词的概率也最高。
以下为训练Word2Vec之Skip-Gram的步骤及重要参数的意义:
一、读入文本,将每个单词分隔开,存为一个单词列表。
二、统计单词列表中词频,
二维数组count:记录单词频数,形如[['UNK', -1], [top1, 25631], [top2, 3541],…]
词典dictionary:将top50000词频的单词按词频从大到小的顺序存入,其中key为单词了,value为编号(1-50000)
单词编号列表data:遍历单词列表,如当前单词在词典中,则值为value,否则为0
reverse_dictionary:dictionary的key与value值反转
三、生成word2vec的训练样本,返回目标单词batch数组和其对应语境labels数组
batch:一维数组,长度为batch_size,存放目标单词编号
labels:二维数组(batch_size, 1) ,在与batch数组下标相同的位置存放目标单词的语境单词编号
训练样本的构成:
以“the quick brown fox jumped over the lazy dog”这句话为例。我们要构造一个语境与目标词汇的映射关系,其中语境包括一个单词左边和右边的词汇,假设我们的滑窗尺寸为1,因为Skip-Gram模型是从目标词汇预测语境,所以我们的数据集就成了(quick, the)、(quick, brown)、(brown, quick)、(brown, fox)。我们训练时,希望模型能从目标词汇quick预测出语境the。
四、训练
随机生成所有单词的词向量为一个词向量矩阵,
在词向量矩阵中根据单词编号查找到训练数据(即batch)对应的向量们,
tf.nn.nce_loss计算出词向量embedding在训练数据上的loss,并使用tf.reduce_mean进行汇总
训练完后embeddings除以其L2范数得到标准化后的normalized_embeddings,即最终的词向量矩阵
以下为本文实现Word2Vec的实现代码,进行了前向计算的测评,代码及详细注释如下:
import collections
import math
import os
import random
import zipfile
import numpy as np
import urllib
import tensorflow as tf
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
'''############################################05《TensorFlow实战》实现Word2Vec Skip-Gram##################################################'''
url = "http://mattmahoney.net/dc/"
'''下载文本数据的函数'''
def maybe_download(filename, expected_bytes):
if not os.path.exists(filename):
filename, _ = urllib.request.urlretrieve(url + filename, filename) #下载数据的压缩文件
#核对文件尺寸
statinfo = os.stat(filename)
if statinfo.st_size == expected_bytes:
print('Found and verfied', filename)
else:
print(statinfo.st_size)
raise Exception(
'Failed to verify ' + filename + '. Can you get to it with browser?'
)
return filename
filename = maybe_download('text8.zip', 31344016) #根据文件名和byte下载数据
def read_data(filename):
with zipfile.ZipFile(filename) as f:
data = tf.compat.as_str(f.read(f.namelist()[0])).split() #解压文件并将数据转成单词的列表
return data
words = read_data(filename)
print('Data size', len(words))
# print(words[:5])
vocabulary_size = 50000
'''创建vocabulary词汇表'''
def build_dataset(words):
count = [['UNK', -1]] #词频统计,['UNK', -1]表示未知单词的频次设为-1
#使用collections.Counter()方法统计单词列表中单词的频数, 使用.most_common()方法取top50000频数的单词(频次从大到小)作为vocabulary
# # >> > Counter('abcdeabcdabcaba').most_common(3)
# # [('a', 5), ('b', 4), ('c', 3)]
count.extend(collections.Counter(words).most_common(vocabulary_size - 1))
#创建一个词典dict,将top50000的单词存入,以便快速查询
dictionary = dict()
#将top50000单词按频次给定其编号存入dictionary,key为词,value为编号(从1到50000)
for word, _ in count: #_用作一个名字,来表示某个变量是临时的或无关紧要的
dictionary[word] = len(dictionary)
#print(len(dictionary))
data = list() #单词列表转换后的编码
unk_count = 0
for word in words: #遍历单词列表
if word in dictionary: #如果该单词存在于dictionary中
index = dictionary[word] #取该单词的频次为编号
else: #如果dictionary中没有该单词
index = 0 #编号设为0
unk_count += 1
data.append(index)
count[0][1] = unk_count #未知单词数(除了top50000以外的单词)
reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
#返回单词列表中单词转换为编码(编码为该单词的频次)的data列表、每个单词的频数统计count、词汇表dictionary及其反转形式reverse_dictionary
return data, count, dictionary, reverse_dictionary
data, count, dictionary, reverse_dictionary = build_dataset(words)
del words #删除原始单词表,节约内存
print("Most common words (+UNK)", count[:5]) #打印未知单词和最高频单词及它们的频次
print("Sample data", data[:10], [reverse_dictionary[i] for i in data[:10]]) #打印单词列表中前10个单词的编号及单词本身
data_index = 0
'''生成word2vec的训练样本,返回目标单词编号batch数组和其对应语境编号labels数组
skip-gram模式将原始数据"the quick brown fox jumped"转为(quick,the),(qucik,brown),(brown,quick),(brown,fox)等样本'''
def generate_batch(batch_size, #一个批次的大小
num_skips, #num_skips为对每个单词生成多少个样本
skip_window): #指单词最远可以联系的距离,设为1代表只能跟紧邻的两个单词生成样本,比如quick只能和前后的单词生成(quick,the),(qucik,brown)
global data_index #单词序号设为global,确保在调用该函数时,该变量可以被修改
#python 中assert断言是声明其布尔值必须为真的判定,其返回值为假,就会触发异常
assert batch_size % num_skips == 0 #skip-gram中参数的要求
assert num_skips <= 2 * skip_window
batch = np.ndarray(shape=(batch_size), dtype=np.int32) #初始化batch,存放目标单词
labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32) #初始化labels,存放目标单词的语境单词们
span = 2 * skip_window + 1 #对某个单词创建相关样本时会用到的单词数量,包含目标单词和它前后的单词
buffer = collections.deque(maxlen=span) #创建一个最大容量为span的deque,即双向队列
for _ in range(span):
buffer.append(data[data_index]) #从序号data_index开始, 把span个单词的编码顺序读入buffer作为初始值,循环完后,buffer填满,里面为目标单词和需要的单词
data_index = (data_index + 1) % len(data)
for i in range(batch_size // num_skips): #批次的大小➗每个单词生成的样本数=该批次中单词的数量
target = skip_window #即buffer中下标为skip_window的变量为目标单词
targets_to_avoid = [skip_window] #避免使用的单词列表,要预测的时语境,不包含单词本身
for j in range(num_skips):
while target in targets_to_avoid: #生成随机数直到不在targets_to_avoid中,代表可以使用的语境单词
target = random.randint(0, span-1)
targets_to_avoid.append(target) #该语境单词已使用,加入避免使用的单词列表
batch[i * num_skips + j] = buffer[skip_window] #feature即目标词汇
labels[i * num_skips + j, 0] = buffer[target] #label即当前语境单词
buffer.append(data[data_index]) #对一个目标单词生成完所有样本后,再读入下一个单词(同时会抛掉buffer中第一个单词)
data_index = (data_index + 1) % len(data) #单词序号+1
#获得了batch_size个训练样本,返回目标单词编号batch数组和其对应语境单词编号labels数组
return batch, labels
'''测试word2vec训练样本生成'''
# batch, labels = generate_batch(batch_size=8, num_skips=2, skip_window=1)
# for i in range(8):
# print(batch[i], reverse_dictionary[batch[i]], "-->", labels[i][0], reverse_dictionary[labels[i, 0]])
batch_size = 128
embedding_size = 128 #单词转为词向量的维度,一般为50-1000这个范围内的值
skip_window = 1
num_skips = 2
#生成验证数据valid_samples
valid_size = 16 #用来抽取的验证单词数
valid_window = 100 #验证单词从频数最高的100个单词中抽取
valid_examples = np.random.choice(valid_window, valid_size, replace=False) #从valid_window中随机抽取valid_size个数字,返回一维数组
num_sampled = 64 #训练时用来做负样本的噪声单词的数量
'''定义Skip-Gram Word2Vec模型的网络结构'''
graph = tf.Graph()
with graph.as_default():
train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
valid_dataset = tf.constant(valid_examples, dtype=tf.int32) #验证单词的索引 shape(1, 16)
with tf.device('/cpu:0'): #限定所有计算在CPU上执行,因为接下去的一些计算操作在GPU上可能还没有实现
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0)) #随机生成所有单词的词向量embeddings,范围[-1, 1]
embed = tf.nn.embedding_lookup(embeddings, train_inputs) #在embeddings tensor中查找输入train_inputs编号对应的向量embed
nce_weights = tf.Variable( #使用tf.truncated_normal截断的随机正态分布初始化NCE Loss中的权重参数nce_weights
tf.truncated_normal([vocabulary_size, embedding_size], stddev=1.0 / math.sqrt(embedding_size)))
nce_biases = tf.Variable(tf.zeros([vocabulary_size])) #偏置初始化为0
#使用tf.nn.nce_loss计算出词向量embedding在训练数据上的loss,并使用tf.reduce_mean进行汇总
loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weights, #权重
biases=nce_biases, #偏置
labels=train_labels, #标记
inputs=embed, #
num_sampled=num_sampled, #负样本噪声单词数量
num_classes=vocabulary_size)) #可能的分类的数量(单词数)
optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)
'''余弦相似度计算'''
#L2范数又叫“岭回归”,作用是改善过拟合,L2范数计算方法:向量中各元素的平方和然后开根号
norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True)) #计算嵌入向量embeddings的L2范数
normalized_embeddings = embeddings / norm #embeddings除以其L2范数得到标准化后的normalized_embeddings
valid_embeddings = tf.nn.embedding_lookup( #根据验证单词的索引valid_dataset,查询验证单词的嵌入向量
normalized_embeddings, valid_dataset)
# 计算验证单词的嵌入向量与词汇表中所有单词的相似性, valid_embeddings * (normalized_embeddings的转置)
similarity = tf.matmul(valid_embeddings, normalized_embeddings, transpose_b=True)
init = tf.global_variables_initializer()
'''可视化Word2Vec,low_dim_embds为降维到2维的单词的空间向量'''
def plot_with_labels(low_dim_embds, labels, filename='tsne.png'):
assert low_dim_embds.shape[0] >= len(labels), "More labels than embeddings"
plt.figure(figsize=(18, 18)) #图片大小
for i, label in enumerate(labels):
x, y = low_dim_embds[i,:]
plt.scatter(x, y) #显示散点图
plt.annotate(label, #展示单词本身
xy=(x, y),
xytext=(5, 2),
textcoords='offset points',
ha='right',
va='bottom')
plt.savefig(filename)
'''测试'''
num_steps = 100001 #训练100001轮
with tf.Session(graph=graph) as session:
init.run()
print("Initialized")
average_loss = 0
for step in range(num_steps):
#获得训练数据和数据标记
batch_inputs, batch_labels = generate_batch(batch_size, num_skips, skip_window)
feed_dict = {train_inputs:batch_inputs, train_labels:batch_labels}
_, loss_val = session.run([optimizer, loss], feed_dict=feed_dict) #执行优化器运算和损失计算
average_loss += loss_val #损失值累加
if step % 2000 == 0: #每两千轮计算几次平均损失值
if step > 0:
average_loss /= 2000
print("Average loss at step", step, ": ", average_loss)
average_loss = 0
#每一万轮,计算一次验证单词与全部单词的相似度,并将与每个验证单词最相近的8各单词展示出来
if step % 10000 == 0:
# tensor.eval():在`Session`中评估这个张量。调用此方法将执行所有先前的操作,这些操作将生成生成此张量的操作所需的输入。
sim = similarity.eval() #shape,(16, 50000)
#print(tf.shape(sim).eval())
for i in range(valid_size): #对每一个验证单词
valid_word = reverse_dictionary[valid_examples[i]] #根据前面随机抽取的验证单词编号(即频次),在反转字典中取出该验证单词
top_k = 8
#.argsort()从小到大排列,返回其对应的索引,由于-sim(),所以返回的索引是相似度从大到小的
nearest = (-sim[i, :]).argsort()[1:top_k+1] #计算得到第i个验证单词相似度最接近的前8个单词的索引
log_str = "Nearest to %s:" % valid_word
for k in range(top_k):
close_word = reverse_dictionary[nearest[k]] #相似度最接近的第i个单词
log_str = "%s %s," % (log_str, close_word)
print(log_str)
final_embeddings = normalized_embeddings.eval() #最终训练完的词向量矩阵
'''展示降维后的可视化效果'''
#使用sklearn.manifold.TSNE实现降维,这里直接将原始的128维嵌入向量降到2维
tsne = TSNE(perplexity=30, #困惑度,默认30
n_components=2, #降到多少维
init='pca', #初始化的嵌入
n_iter=5000) #优化的最大迭代次数。至少应该是250。
plot_only = 150
low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only, :]) #进行降维,输入shape为 (n_samples, n_features) or (n_samples, n_samples)
labels = [reverse_dictionary[i] for i in range(plot_only)]
plot_with_labels(low_dim_embs, labels) #用该可视化函数进行展示
结果:
将训练好的向量降维并可视化,如图所示,数字等相近的单词在空间中的位置相近