引言?
大部分内容摘自《TensorFlow实战》(黄文坚),算是读书笔记,也会在网上搜集一些相关的文章进行补充。 我觉得这本书很nice呀,虽然没有对具体的神经网络进行详细的讲解,但是很通俗易懂,偏实战。 也会有看了神经网络的详解然后雨里雾里,再在这边看了总结的豁然开朗之感。
One-Hot Encoder
最早的词向量表示方法为One-Hot Encoder,将字词转成离散的单独的符号:
杭州 [0,0,0,0,0,0,0,1,0,……,0,0,0,0,0,0,0]
上海 [0,0,0,0,1,0,0,0,0,……,0,0,0,0,0,0,0]
宁波 [0,0,0,1,0,0,0,0,0,……,0,0,0,0,0,0,0]
北京 [0,0,0,0,0,0,0,0,0,……,1,0,0,0,0,0,0]
杭州、上海、宁波、北京各对应一个向量,向量中只有一个值为1,其余都为0。 使用One-Hot Encoder有一个问题,即对特征的编码是随机的,没有考虑到字词之间的关系。如北京、上海应该聚集到一起,华盛顿、纽约聚集在一起。并且效率低、计算麻烦。
什么是Word2Vec
循环神经网络是在NLP领域最常使用的神经网络,而Word2Vec则是将语言中的字词转化为计算机可以理解的稠密向量(Dense Vector),通过一个嵌入空间使得语义上相似的单词在该空间内距离很近。
Word2Vec分为:
CBOW(Continuous Bag of Words):从原始语句(例如:中国的首都是____)推测目标字词(例如:北京)
Skip-Gram:从目标字词推测出原始语句,在大型语料库中表现更好
负采样
:在Word2Vec的CBOW模型中,只训练一个二分类模型,区分真实的目标词汇和编造的噪声词汇(Negative Sampling)。只需要计算随机选择的k个词汇而并非全部。在实际中,使用Noise-Contrastive Estimation(NCE)Loss,在tf中对应为tf.nn.nce_loss()
模型拆解
Word2Vec模型其实就是简单化的神经网络:
图片来源:理解 Word2Vec 之 Skip-Gram 模型,作者写的超级详细,可以去读读。
输入是One-Hot Vector。 如果想要用300个特征来表示一个单词,则隐藏层的维度为300,且Hidden Layer没有激活函数。 Output Layer维度跟Input Layer的维度一样,用的是Softmax回归,输出概率分布。
将从输入层到隐含层的那些权重,作为每一个词汇表中的词的向量。
因为One-Hot Vector十分稀疏,消耗大量的计算资源。为了进行高效计算,只要选择选择矩阵中对应的向量中维度值为1的索引行:
左边向量中取值为1的对应维度为3(下标从0开始),那么计算结果就是矩阵的第3行(下标从0开始)—— [10, 12, 19],这样模型中的隐层权重矩阵便成了一个”查找表“(lookup table)
详细拆解参考如果看了此文还不懂 Word2Vec,那是我太笨
- CBOW
将一个词所在的上下文中的词作为输入,而那个词本身作为输出,也就是说,看到一个上下文,希望大概能猜出这个词和它的意思。通过在一个大的语料库训练,得到一个从输入层到隐含层的权重模型。如下图所示,第l个词的上下文词是i,j,k,那么i,j,k作为输入,它们所在的词汇表中的位置的值置为1。然后,输出是l,把它所在的词汇表中的位置的值置为1。训练完成后,就得到了每个词到隐含层的每个维度的权重,就是每个词的向量。例如第i个词的词向量为(Wi,1 Wi,2...Wi,m)
,m为向量的维度。
- Skip-gram
将一个词所在的上下文中的词作为输出,而那个词本身作为输入,也就是说,给出一个词,希望预测可能出现的上下文的词。通过在一个大的语料库训练,得到一个从输入层到隐含层的权重模型。如下图所示,第l个词的上下文词是i,j,k,那么i,j,k作为输出,它们所在的词汇表中的位置的值置为1。然后,输入是l,把它所在的词汇表中的位置的值置为1。训练完成后,就得到了每个词到隐含层的每个维度的权重,就是每个词的向量。
tf实现Word2Vec
使用Skip-Gram模式的Word2Vec,以“the quick brown fox jumped over the lazy dog”为例,训练样本为(quick,the),(quick,brown),(brown,quick),(brown,fox)等。训练时,希望模型可以从目标词汇quick推测出语境the,同时也需要制造随机的词汇作为负样本(噪声)。使用SGD(随机梯度下降算法)来更新模型中Word Embedding的参数,让概率分布的损失函数(NCE Loss)尽可能小。这样每个单词的Embedded Vector就会随着训练过程不断调整,直到处于一个最合适语料的空间位置。
基本每一句都有注释啦,可以结合原书来看此段代码😳
啊啊啊,我手工巧了一遍代码,才发现代码居然是tf上的demo…
# -*- coding: UTF-8 -*-
import collections
import math
import os
import random
import zipfile
import numpy as np
import urllib
import tensorflow as tf
import urllib
# --------------------------- 数据预处理---------------------------
url = 'http://mattmahoney.net/dc/'
# 从‘http://mattmahoney.net/dc’下载文本文件,里面约有17005207个用空格分隔好的英文句子。
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 verified',filename)
else:
print(statinfo.st_size)
raise Exception(
'Failed to verify' + filename +'. Can you get to it with a browser?'
)
return filename
filename = maybe_download("text8.zip",31344016)
# filename = "text8.zip"
# 解压文件,并使用`tf.compat.as_str`将数据转化成单词列表。
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))
# 创建vocabulary词汇表,选取前50000频数的单词,其余单词认定为Unknown,编号为0
vocabulary_size = 50000
def build_dataset(words):
count = [['UNK',-1]]
print("---------------------------------")
# collections.Counter统计单词列表中单词的频数
# most_common 取top 50000频数的单词作为vocabulary
# count = [(单词1,词频1),(单词2,词频2),...]
count.extend(collections.Counter(words).most_common(vocabulary_size -1))
dictionary = dict()
# 存入dic中
for word ,_ in count:
dictionary[word] = len(dictionary)
# 遍历单词列表,如果出现在dictionary中,则转化为编号。不在则为0。
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
# 字典的反转形式,可用编号查询出对应的单词
reverse_dictionary = dict(zip(dictionary.values(),dictionary.keys()))
return data,count,dictionary,reverse_dictionary
data,count,dictionary,reverse_dictionary = build_dataset(words)
# 删除原始单词列表,节约内存
del words
# 打印最高频出现的词汇及数量,[['UNK', 418391], ('the', 1061396), ('of', 593677), ('and', 416629), ('one', 411764)]
print('Most common words (+UNK)',count[:5])
# 打印data前10个单词
print('Sample data',data[:10],[reverse_dictionary[i] for i in data[:10]])
# ---------------------------生成Word2Vec的训练样本---------------------------
# 使用Skip-Gram模式,将原始数据:
# “the quick brown fox jumped over the lazy dog”转化为:
# (quick,the),(quick,brown),(brown,quick),(brown,fox)等。
data_index = 0
def generate_batch(batch_size , num_skips, skip_window): # 生成训练用的batch数据
# skip_window 单词最远可以联系的距离
# num_skips 对每个单词生成多少样本
global data_index # 定义为全局变量,因为会反复调用此函数
# batch_size 必须为num_skips的整数倍,确保每个batch包含一个词汇对应的所有样本
assert batch_size % num_skips == 0
assert num_skips <= 2 * skip_window
# ndarray对象是用于存放同类型元素的多维数组
batch = np.ndarray(shape = (batch_size),dtype = np.int32)
labels = np.ndarray(shape = (batch_size,1),dtype = np.int32)
# 某个单词创建样本时会使用到的单词数量,包括单词本身和它前后的单词
span = 2 * skip_window + 1
# 双向队列,使用append方法添加变量时,只会保留最后插入span个变量
buffer = collections.deque(maxlen=span)
# 从序号data_index开始,把span个单词顺序读入buffer作为初始值
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 # buffer中第skip_window个变量为目标单词
targets_to_avoid = [ skip_window ] # 生成样本时需要避免的单词列表
for j in range(num_skips):
while target in targets_to_avoid:
target = random.randint(0,span-1)
targets_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)
return batch ,labels
# 简单测试功能
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_size = 16 # 抽取的验证单词数
valid_window = 100 # 验证单词只从频数最高的100个单词中抽取
valid_examples = np.random.choice(valid_window,valid_size,replace=False)
num_sampled = 64 # 训练时用来做负样本的噪声单词的数量
# ---------------------------定义Skip-Gram模型的网络结构---------------------------
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_examples转换为constant
valid_dataset = tf.constant(valid_examples,dtype = tf.int32)
with tf.device('/cpu:0'):
# tf.random_uniform随机生成所有单词的词向量embeddings,单词表大小为50000,向量维度为128
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size,embedding_size],-1.0,1.0))
# tf.nn.embedding_lookup 查找输入train_inputs对应的向量
embed = tf.nn.embedding_lookup(embeddings,train_inputs)
# 使用NCE Loss作为训练的优化目标
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的计算方式
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))
# 定义优化器为SGD,lr为1.0
optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)
# 计算嵌入向量embeddings的L2范数norm
norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings),1,keep_dims = True))
# 规范化
normalized_embeddings = embeddings /norm
# tf.nn.embedding_lookup 查询验证单词的嵌入向量
valid_embeddings = tf.nn.embedding_lookup(
normalized_embeddings, valid_dataset)
# 计算验证单词的嵌入向量与词汇表中所有单词的相似性
similarity = tf.matmul(valid_embeddings,normalized_embeddings,transpose_b = True)
# 初始化所有模型参数
init = tf.global_variables_initializer()
# 最大的迭代次数
num_steps = 100001
with tf.Session(graph = graph) as session:
init.run()
print("Initialized")
average_loss = 0
for step in range(num_steps):
# 生成一个batch的inputs和labels数据
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
# 2000此循环,计算一下平均loss并显示出来
if step % 2000 ==0:
if step >0 :
average_loss /= 2000
print("Average loss at step ", step, ": ",average_loss)
average_loss = 0
# 每10000次循环,计算一次验证单词与全部单词的相似度,并将最相似的8个单词展示出来
if step % 10000 ==0:
sim = similarity.eval()
for i in range(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 range(top_k):
close_word = reverse_dictionary[nearest[k]]
#close_word = reverse_dictionary.get(nearest[k])
log_str = "%s %s ," % (log_str,close_word)
print(log_str)
final_embeddings = normalized_embeddings.eval()
# ---------------------------可视化---------------------------
import matplotlib.pyplot as plt
# low_dim_embs 是降维到2维的单词的空间向量,在图表中展示每个单词的位置
def plot_with_labels(low_dim_embs,labels,filename = 'tsne.png'):
assert low_dim_embs.shape[0] >= len(labels),"More labels than embeddings"
plt.figure(figsize = (18,18))
for i,label in enumerate(labels):
x, y = low_dim_embs[i,:]
plt.scatter(x,y) # 显示散点图(单词的位置)
# plt.annotate为单词本身
plt.annotate(label,
xy = (x,y),
xytext = (5,2),
textcoords = 'offset points',
ha = 'right',
va = 'bottom')
plt.savefig(filename) # 保存图片到本地
# 降维,将原始的128维嵌入向量降到2维,展示词频最高的100个单词
from sklearn.manifold import TSNE
tsne = TSNE(perplexity=30, n_components=2,init="pca",n_iter = 5000)
plot_only = 100
low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only,:])
labels = [reverse_dictionary[i] for i in range(plot_only)]
plot_with_labels(low_dim_embs,labels)