本笔记记录一下鄙人在使用tf的心得,好让自己日后可以回忆一下。其代码内容都源于tf的tutorial里面的Vector Representations of Words。
现在我们一起来实现通过tf实现word2vec吧。
step1 数据集
url = 'http://mattmahoney.net/dc/'
def maybe_download(filename, expected_bytes):
pass
filename = maybe_download('text8.zip', 31344016)
filename 是我们的待处理的目标文件。其实是它是在http://mattmahoney.net/dc/text8.zip 里面,而这个函数就判断本地时候存在该文件,若没有就网上读取(鄙人就先下载下来)。我不知道为毛它要判断文件大小跟预期一样,也不影响我们后面的工作。
step2 读取数据
def read_data(filename):
"""
提取第一个文件当中的词列表
:param filename:
:return:
"""
with zipfile.ZipFile(filename) as f:
data = tf.compat.as_str(f.read(f.namelist()[0])).split()
return data
这里不用细说啦。因为都是简单的i/o
vocabulary = read_data(filename)
vocabulary_size = 50000
读取了文件里面的词语后,为了方便,我们先定义自己的词典大小为5W。
step3 建立词典
def build_dataset(words, n_words):
"""
建立字典数据库
:param words:
:param n_words:
:return:
"""
count = [['UNK', -1]]
# 记录前49999个高频词,各自出现的次数
count.extend(collections.Counter(words).most_common(n_words - 1))
# Key value pair : {word: dictionary_index}
dictionary = dict()
for word, _ in count:
dictionary[word] = len(dictionary)
# 记录每个词语对应与词典的索引
data = list()
unk_count = 0
for word in words:
if word in dictionary:
index = dictionary[word]
else:
index = 0
unk_count += 1
data.append(index)
# 记录没有在词典中的词语数量
count[0][1] = unk_count
reversed_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
return data, count, dictionary, reversed_dictionary
- count:记录词语和对应词频的key-value
- data:记录文本内的每一个次对应词典(dictionary)的索引
- dictionary:记录词语和相应词典索引
- reversed_dicitonary:记录词典索引和相应的词语,跟dictionary的key-value相反
这里的['UNK', -1]记录这一些词典没有记录的词语,因为我们只拿文本中出现次数最多的前49999词作为词典中的词语。这意味着有一些我们不认识的词语啊。那我们就将其当作是我们词典的“盲区”,不认识的词(unknown words)简称UNK。
# 词语索引,每个词语词频,词典,词典的反转形式
data, count, dictionary, reverse_dictionary = build_dataset(vocabulary, vocabulary_size)
调用该函数我们就获得词典的内容
step4 开始建立skip-gram模型需要的数据集合(data, label)
对于传统的ML或者DL都会使用有监督型的数据进行训练。对于skip-gram模型,我需要的数据集合应该是{(x)data: target word, (y)label: context words}。它跟CBOW是截然不同的,因为CBOW是需要通过上下文推断目标词语,所以需要的数据集合是{data: context words, label target word}。现在我们根据文本内容和从文本获得词典,我们开始建立训练数据集合。
# 给skip-gram模型生成训练集合
def generate_batch(batch_size, num_skips, skip_window):
"""
:param batch_size: 训练批次(batch)的大小
:param num_skips: 采样的次数
:param skip_window: 上下文的大小
:return:
"""
global data_index
assert batch_size % num_skips == 0
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)
# 上下文的组成:[skip_window target skip_window]
span = 2 * skip_window + 1
# 缓冲区
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 // num_skips):
target = skip_window
target_to_avoid = [skip_window]
for j in range(num_skips):
#
while target in target_to_avoid:
target = random.randint(0, span - 1)
target_to_avoid.append(target)
# 记录输入数据(中心词)
batch[i * num_skips + j] = buffer[skip_window]
# 记录输入数据对应的类型(上下文内容)
labels[i * num_skips + j, 0] = buffer[target]
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
data_index = (data_index + len(data) - span) % len(data)
return batch, labels
这里我们得到的batch就是我们想要的输入词语/数据(词语在词典当中的索引),另外label是batch对应的目标词语/数据(词语在词典当中的索引)。这里举个例子,我们现在给定batch_size为8,num_skips为2,skip_window=1,给出文本为
"I am good at studying and learning ML. However, I don't like to read the English document."
我粗略算算词典
['I', 'am', 'good', 'at', 'studying', 'and', 'learning', 'ML', 'However', 'I', 'don't', 'like', 'to', 'read', the', 'English', 'document']
根据generate_batch的内容和给定参数,我们第一次获得内容应该是
['I', 'am', 'good', 'at', 'studying', 'and', 'learning', 'ML']
我们的上下文窗口(span)应该是 2 * 1 + 1 = 3。也就是窗口应该是
buffer=['I', 'am', 'good']
显然target应该是'am'也就是为buffer[skip_window]而context word应该是['I', 'good']。这就构成了{x: data, y: label}之间的关系。
对于skip-gram模型的数据集合
- {(x)data: 'am', (y)label: 'I'}
- {(x)data: 'am', (y)label: 'good'}
如此类推。那num_skips有啥用呢?其实num_skips意味着需要对buffer进行多少次才采样,才开始对下一个buffer进行采样。
step5 开始建立skip-gram模型(重点来了)
batch_size = 128
embedding_size = 128 # 嵌入向量的维度
skip_window = 1 # 上下文的词数
num_skips = 2 # 多少次后重用输入的生成类别
# 我们使用随机邻居样本生成评估集合,这里我们限定了
# 评估样本一些数字ID词语,这些ID是通过词频产生
valid_size = 16
valid_window = 100
valid_examples = np.random.choice(valid_window, valid_size, replace=False)
num_sample = 64
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)
with tf.device('/cpu:0'):
# 随机生成初始词向量
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0)
)
# 根据batch的大小设置输入数据的batch
embed = tf.nn.embedding_lookup(embeddings, train_inputs)
# 设置权值
nce_weights = tf.Variable(
tf.truncated_normal([vocabulary_size, embedding_size], stddev=1.0 / math.sqrt(embedding_size)))
nce_biases = tf.Variable(tf.zeros([vocabulary_size]))
# 计算误差平均值
loss = tf.reduce_mean(
tf.nn.nce_loss(
weights=nce_weights,
biases=nce_biases,
labels=train_labels,
inputs=embed,
num_sampled=num_sample,
num_classes=vocabulary_size
)
)
# learning rate 1.0
optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)
norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
# 对词向量进行归一化
normalized_embeddings = embeddings / norm
# 根据校验集合,查找出相应的词向量
valid_embeddings = tf.nn.embedding_lookup(normalized_embeddings, valid_dataset)
# 计算cosine相似度
similarity = tf.matmul(valid_embeddings, normalized_embeddings, transpose_b=True)
init = tf.global_variables_initializer()
这里可能没有之前这么简单了,因为不懂word2vec数学原理的人,完全看不懂代码,尽管你精通Python,也不知道为毛有这行代码和代码的含义。这里我不多讲word2vec的数学原理,迟点我会再一遍文章讲解word2vec的原理和疑问。这里我给出一篇我看过的详细的文章word2vec的数学原理,大家可以先阅览一下。我在这里稍微讲一下代码和附带的原理内容。
# 随机生成初始词向量
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0)
)
大家都知道word2vec就是说把词语变成向量形式,而在CBOW和skip-gram模型中,词向量是副产品,真正的目的是推断出上下文内容。这里就来一个要点了(是鄙人私以为的):
在模型的训练过程中,调整词向量和不断是推断逼近目标词语是同时进行。也就是说调整词向量->优化推断->调整词向量->优化推断->调整词向量->优化推断.... 最后达到两者同时收敛。这就是我们最后的目标。这是我从EM算法中类比获得的想法,关于EM算法,我会在之后添加文章(算法推导+代码)。
在DL和ML中我们都说到损失函数,不断优化损失函数使其最小,是我们的目标。这里的损失函数是什么呢?那就是
tf.nn.nce_loss(
weights=nce_weights,
biases=nce_biases,
labels=train_labels,
inputs=embed,
num_sampled=num_sample,
num_classes=vocabulary_size
)
我们刚刚说到要把推断出哪个词应该出现在上下文当中,就是涉及到一个概率问题了。既然是推断那就是要比较大小啦。那就是把词典中所有的词的有可能出现在上下文的概率都算一遍吗?确实!在早期word2vec论文发布时,就是这么粗暴。现在就当然不是啦。那就是用negative sample来推断进行提速啦。
我们知道在训练过程中,我们都知道label是哪个词。这意味着其他词对于这个样本就是negative了。那就好办啦。我就使得label词的概率最大化,其他词出现的概率最小化。当中涉及的数学知识就是Maximum likelihood 最大似然估计。不懂的回去复习呗。
之后我们用梯度下降法进行训练,这样我们就得到训练模型了。
step6 开始进行无耻的训练
num_steps = 100001
with tf.Session(graph=graph) as session:
# 初始化变量
init.run()
print("Initialized")
average_loss = 0
for step in xrange(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
if step % 10000 == 0:
sim = similarity.eval()
for i in xrange(valid_size):
valid_word = reverse_dictionary[valid_examples[i]]
top_k = 8
nearest = (-sim[i, :]).argsort()[1: top_k + 1]
log_str = 'Nearest to %s: ' % valid_word
for k in xrange(top_k):
close_word = reverse_dictionary[nearest[k]]
log_str = "%s %s," % (log_str, close_word)
print(log_str)
final_embeddings = normalized_embeddings.eval()
在每次训练中我们都给数据模型喂养(feed)一小批数据(batch_input, batch_labels)。这些数据是通过generate_batch()生成的。通过暴力的迭代,我们最后得到最终词向量(final_embedding)。在训练过程中,每2000次迭代打印损失值,每10000次迭代打印校验词的相似词(通过cosin相似度来判断)。
最后还差一个词向量降维后的图片,我迟点不上。现在准备煮饭咯....