引言?
大部分内容摘自《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 )