本节课主要通过以下几个方面讲解了word2vec:
- 词向量出现的原因
- one-hot representation VS Dristributed representation
- 两种word2vec的算法:
- Skip-grams (SG):预测上下文
- Continuous Bag of Words (CBOW):预测目标单词 - 两种高效的训练方法:(以后补充)
- Hierarchical softmax
- Negative sampling - Skip-grams的tensorflow源码实现过程
- Skip-grams的纯手写实现过程
1. 词向量出现的原因
在标准的语言学中,单词就像一种语言学符号,它指代了世界上某些具体的事物。但是这样的解释应用在计算机系统去处理语言是困难的。用分类资源来处理词义的WordNet拥有很多词汇分类信息。但是他很难具体的判断两个单词的相似性,或者说不能把相似性量化。因此词向量产生了。
2. one-hot representation VS Dristributed representation
- one-hot representation
根据语料库的大小确定词向量的维度,当单词出现在某个位置,这个维度数值为1,剩余维度为0. 这样产生的词向量面临着两个问题:当语料库比较大时,词向量面临着维度灾难;由于one-hot向量之间是互相正交的,所以无论两个单词如何相似,one-hot向量都不能刻画其相似度。例如课程中讲到的motel和hotel:
- Dristributed representation
分布式的表示可以解决one-hot向量的问题。它通过模型的训练,将每个单词映射到一个低维的向量空间,而且他是一个dense的向量。完美的解决了one-hot向量表示的问题。唯一的缺点就是我们不能解释道具体每一个维度代表的什么。具体的效果可以通过后面的编码部分看到。
3. 两种word2vec的算法
word2vec的方法主要有Skip-grams (SG)和Continuous Bag of Words (CBOW)和两种模型。而Skip-Gram是从中心词推测出上下文;而CBOW是从上下文推测推测中心词。下面是两种模型的结构图:
下面主要围绕Skip-grams (SG)模型展开
3.1 Skip-grams (SG)的框架细节如下图中所示
上图中V代表语料字典的大小,d代表了单词embedding的维度。
w_t是一个中心单词的one-hot形式。W是中心词语embedding的矩阵。这两个相乘,相当于一个查表操作,在W中得到中心词语的embedding vector表示。中间的W’是存储上下文单词的embedding的矩阵。中心词语的embedding与上下文单词的embedding的矩阵相乘得到每个上下文备选词语与已经选定的中心词语的相似度,就是上文中提到的vc*uo。最相似的一个或几个我们就认为它们是中心词语的上下文。使用softmax得到的概率与真实上下文单词的one-hot形式作比较。还可以看出目前的预测并不准确。
3.2 Skip-grams (SG)所涉及到的公式以及推导过程
对于一个长度为T的语料库,假设我们为每一个词选取的上下文窗口的大小是m(指的是上下文各m个词),则我们的目标函数是最大化训练语料的对数似然概率:
其中t为中心词,θ是模型参数
对于p(o|c):
其中c代表中心词,o代表上下文某个词.
v_c和u_o分别是中心词和输出词的向量,也就是我们模型中的参数。
观察可以发现p(o|c)是一个softmax形式,softmax函数可以得到数字到概率分布的映射,也就是说可以讲数字转化为概率。因此这里使用softmax将一堆相似度转化为概率。
优化这个目标函数的算法是随机梯度下降法(SGD)
首先对"center"向量v_c进行求导,得到以下结果:
首先对"output"向量u_o进行求导,得到以下结果:
通过不断地迭代优化,得到作为中心词的向量表示V和作为上下文的向量表示U。课程中最后将两个向量拼接起来得到我们想要的词向量,也可以对应位置相加求和得到词向量。
5. Skip-grams的tensorflow源码实现过程
将TensorFlow实现word2vec的basic版过了一遍,写了详细的备注,并实现了一个小例子(Github)。
语料采用的金庸大师的《倚天屠龙记》,《停用词表》采用的哈工大发布的语料。
- 展示词向量具体效果的函数还没写,接下来会补充
- 后面nce loss具体细节会细看。
效果和代码如下:
由于文本有限,效果肯定也就有限啦~
from numpy import random
import numpy as np
import collections
import math
import tensorflow as tf
import jieba
from collections import Counter
# 此函数作用是对初始语料进行分词处理
def cut_txt(old_file):
cut_file = old_file + '_cut.txt' # 分词之后保存的文件名
fi = open(old_file, 'r', encoding='utf-8') #注意操作前要把文件转化成utf-8文件
text = fi.read() # 获取文本内容
new_text = jieba.cut(text, cut_all=False) # 采用精确模式切词
str_out = ' '.join(new_text)
#去除停用词
stopwords = [line.strip() for line in open('DataSet/中文停用词.txt', 'r',encoding='utf-8').readlines()]
for stopword in stopwords:
str_out=str_out.replace(' '+stopword+' ',' ')
fo = open(cut_file, 'w', encoding='utf-8')
fo.write(str_out)
ret_list=str_out.split()#训练语料
ret_set=list(set(ret_list))#字典
list_len=len(ret_list)
set_len=len(set(ret_list))
print('总字数 总词数 :')
print(list_len,set_len) #总字数 总词数
print('词频字典 :')
print(dict(Counter(ret_list)))# 词频字典
return ret_list,ret_set
# 预处理切词后的数据
def build_dataset(words, n_words):
count = [['UNK', -1]] #存放词频做大的n_words个单词,第一个元素为单词,第二个元素为词频。UNK为其他单词
count.extend(collections.Counter(words).most_common(n_words - 1))#获取词频做大的n_words-1个单词(因为有了UNK,所以少一个)
dictionary = dict() #建立单词的索引字典
for word, _ in count:
dictionary[word] = len(dictionary) #建立单词的索引字典,key为单词,value为索引值
data = list()# 建立存放训练语料对应索引号的list,也就是说将训练的语料换成每个单词对应的索引
unk_count = 0 #计数UNK的个数
for word in words:
if word in dictionary:
index = dictionary[word]#获取单词在字典中的索引
else:
index = 0 #字典中不存在的单词(UNK),在字典中的索引是0
unk_count += 1 #每次遇到字典中不存在的单词 UNK计数器加1
data.append(index)#将训练语料对应的索引号存入data中
count[0][1] = unk_count
reversed_dictionary = dict(zip(dictionary.values(), dictionary.keys()))#将单词的索引字典反转,即将key和value对换
return data, count, dictionary, reversed_dictionary#训练语料的索引;top大的单词词频;字典{单词:索引值};字典{索引值:单词}
# 为 skip-gram model 产生bathch训练样本.
#从文本总体的第skip_window+1个单词开始,每个单词依次作为输入,它的输出可以是上下文范围内的单词中的任何一个单词。一般不是取全部而是随机取其中的几组,以增加随机性。
def generate_batch(batch_size, num_skips, skip_window):#batch_size 就是每次训练用多少数据,skip_window是确定取一个词周边多远的词来训练,num_skips是对于一个输入数据,随机取其窗口内几个单词作为上下文(即输出标签)。
global data_index
assert batch_size % num_skips == 0#保证batch_size是 num_skips的整倍数,控制下面的循环次数
assert num_skips <= 2 * skip_window #保证num_skips不会超过当前输入的的上下文的总个数
batch = np.ndarray(shape=(batch_size), dtype=np.int32) #存储训练语料中心词的索引
labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)#存储训练语料中心词对应的上下文的索引
span = 2 * skip_window + 1 # [ skip_window target skip_window ]
#这个很重要,最大长度是span,后面如果数据超过这个长度,前面的会被挤掉,这样使得buffer里面永远是data_index周围的span歌数据,
#而buffer的第skip_window个数据永远是当前处理循环里的输入数据
buffer = collections.deque(maxlen=span)#一个完整的窗口存储器
if data_index + span > len(data):
data_index = 0
buffer.extend(data[data_index:data_index + span])
data_index += span #获取下一个要进入队列的训练数据的索引
for i in range(batch_size // num_skips):#一个batch一共需要batch个训练单词对,每个span中随机选取num_skips个单词对,所以要循环batch_size // num_skips次
target = skip_window # 中心词索引在buffer中的位置
targets_to_avoid = [skip_window] #自己肯定要排除掉,不能自己作为自己的上下文
for j in range(num_skips):#采样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] #这里保存的是训练时的输出序列,也就是标签
if data_index == len(data): #超长时回到开始
buffer.extend(data[0:span])
data_index = span
else:
buffer.append(data[data_index]) #append时会把queue的开始的一个挤掉
data_index += 1 #此处是控制模型窗口是一步步往后移动的
data_index = (data_index + len(data) - span) % len(data)# 倒回一个span,防止遗漏最后的一些单词
return batch, labels#返回 输入序列 输出序列
############ 第一步:对初始语料进行分词处理 ############
train_data,dict_data=cut_txt('DataSet/倚天屠龙记.txt')#切词
vocabulary_size =10000#字典的大小,只取词频top10000的单词
############ 第二步:预处理切词后的数据 ############
data, count, dictionary, reverse_dictionary = build_dataset(train_data,vocabulary_size)#预处理数据
print()
print(data)
print()
print(count)
print()
print(dictionary)
print()
print(reverse_dictionary)
print('Most common words (+UNK)', count[:5])#词频最高的前5个单词
print('Sample data', data[:10], [reverse_dictionary[i] for i in data[:10]])#前10个训练数据索引及其具体单词
############ 第三步:为 skip-gram model 产生bathch训练样本. ############
data_index = 0#控制窗口滑动的
batch, labels = generate_batch(batch_size=128, num_skips=8, skip_window=5)#产生一个batch的训练数据。batch大小128;从上下文中随机抽取8个单词作为输出标签;窗口大小5(即一个窗口下11个单词,1个人中心词,10个上下文单词);
for i in range(10):#输出一下一个batch中的前10个训练数据对(即10个训练样本)
print(batch[i], reverse_dictionary[batch[i]],'->', labels[i, 0], reverse_dictionary[labels[i, 0]])
############ 第四步: 构造一个训练skip-gram 的模型 ############
batch_size = 128 #一次更新参数所需的单词对
embedding_size = 128 # 训练后词向量的维度
skip_window = 5 #窗口的大小
num_skips = 8 # 一个完整的窗口(span)下,随机取num_skips个单词对(训练样本)
# 构造验证集的超参数
valid_size = 16 # 随机选取valid_size个单词,并计算与其最相似的单词
valid_window = 100 # 从词频最大的valid_window个单词中选取valid_size个单词
valid_examples = np.random.choice(valid_window, valid_size, replace=False)#选取验证集的单词索引
num_sampled = 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) #验证集
embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))#定义单词的embedding
embed = tf.nn.embedding_lookup(embeddings, train_inputs)#窗口查询中心词对应的embedding
# 为 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]))#偏差
# 对于一个batch,计算其平均的 NEC 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))
#采用随机梯度下降优化损失函数,学习率采用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)#选出验证集的单位向量
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('开始训练')
average_loss = 0
for step in range(num_steps):
batch_inputs, batch_labels = generate_batch(batch_size, num_skips, skip_window)#产生一个batch
feed_dict = {train_inputs: batch_inputs, train_labels: batch_labels}#tensor的输入
_, loss_val = session.run([optimizer, loss], feed_dict=feed_dict)#得到一个batch的损失值
average_loss += loss_val #损失值累加
if step % 2000 == 0:#每迭代2000次,就计算一次平均损失,并输出
if step > 0:
average_loss /= 2000
print('Average loss at step ', step, ': ', average_loss)
average_loss = 0 #每2000次迭代后,将累加的损失值归零
if step % 10000 == 0:#每迭代10000次 就计算一次与验证集最相似的单词,由于计算量很大,所以尽量少计算相似度
sim = similarity.eval()
for i in range(valid_size):
valid_word = reverse_dictionary[valid_examples[i]]#得到需验证的单词
top_k = 10 # 和验证集最相似的top_k个单词
nearest = (-sim[i, :]).argsort()[1:top_k + 1]#最邻近的单词的索引,[1:top_k + 1]从1开始,是跳过了本身
log_str = 'Nearest to %s:' % valid_word
for k in range(top_k):
close_word = reverse_dictionary[nearest[k]]#获得第k个最近的单词
log_str = '%s %s,' % (log_str, close_word) #拼接要输出的字符串
print(log_str)
final_embeddings = normalized_embeddings.eval()
6. Skip-grams的纯手写实现过程(还有问题,需要修改)
# 此函数作用是对初始语料进行分词处理后,作为训练模型的语料
def cut_txt(old_file):
import jieba
from collections import Counter
global cut_file # 分词之后保存的文件名
cut_file = old_file + '_cut.txt'
fi = open(old_file, 'r', encoding='utf-8')
text = fi.read() # 获取文本内容
new_text = jieba.cut(text, cut_all=False) # 精确模式
str_out = ' '.join(new_text)
str_out =str_out.replace(',', '').replace('。', '').replace('?', '').replace('!', '') \
.replace('“', '').replace('”', '').replace(':', '').replace('…', '').replace('(', '').replace(')', '') \
.replace('—', '').replace('《', '').replace('》', '').replace('、', '').replace('‘', '') \
.replace('’', '') # 去掉标点符号
#去除停用词
# stopwords = [line.strip() for line in open('DataSet/中文停用词.txt', 'r',encoding='utf-8').readlines()]
# for stopword in stopwords:
# str_out=str_out.replace(' '+stopword+' ',' ')
fo = open(cut_file, 'w', encoding='utf-8')
fo.write(str_out)
ret_list=str_out.split()#训练语料
ret_set=list(set(ret_list))#字典
list_len=len(ret_list)
set_len=len(set(ret_list))
print('总字数 总词数 :')
print(list_len,set_len) #总字数 总词数
print('词频字典 :')
print(dict(Counter(ret_list)))# 词频字典
return ret_list,ret_set
def derivate_v(t,j,X,Y):
v_c=Y[dict_data.index(train_data[t])] #中心词向量 1×d维
u_o=X[dict_data.index(train_data[t - j])] # 上下文的词向量 1×d维
exp_below=np.sum(np.exp(X.dot(np.transpose(v_c)))) #v_c 1×d维
temp_top=np.reshape(np.exp(X.dot(np.transpose(v_c))),(VOCA_LEN,1))
exp_top=np.sum((np.tile(temp_top,(1,DIMENSION_VECTOR)))*X,axis=0)
return u_o-exp_top/exp_below
def derivate_u(t,j,X,Y):
v_c = Y[dict_data.index(train_data[t])] # 中心词向量 1×d维
u_o = X[dict_data.index(train_data[t - j])] # 上下文的词向量 1×d维
exp_below = np.sum(np.exp(X.dot(np.transpose(v_c)))) # v_c 1×d维
exp_top=np.exp(u_o.dot(np.transpose(u_o)))*v_c
return v_c-exp_top/exp_below
from numpy import random
import numpy as np
import heapq
from sklearn.metrics.pairwise import cosine_similarity
train_data,dict_data=cut_txt('DataSet/倚天屠龙记.txt')
DIMENSION_VECTOR=50
WINDOWS=5
ALPHA=0.01
VOCA_LEN=len(dict_data)
V=np.transpose(random.random(size=(DIMENSION_VECTOR,VOCA_LEN))) # W矩阵 v×d维
U=np.copy(V) # U矩阵 v×d维
for t in range(WINDOWS,VOCA_LEN-WINDOWS):
for j in range(WINDOWS,-(WINDOWS+1),-1):
if j == 0: continue
V_new=V[dict_data.index(train_data[t])]-ALPHA*derivate_v(t,j,U,V)
U_new=U[dict_data.index(train_data[t-j])] - ALPHA * derivate_u(t,j,U,V)
# print(V[dict_data.index(train_data[t])])
# print(V_new)
V[dict_data.index(train_data[t])]=V_new
U[dict_data.index(train_data[t - j])]=U_new
print(t)
V_U=V+U
V1 = np.reshape(V_U[dict_data.index('张无忌')],(1,-1))
similarity=cosine_similarity(V1,V_U).tolist()[0]
max_num_index_list = list(map(similarity.index, heapq.nlargest(11, similarity)))
for k in range(len(max_num_index_list)):
print(dict_data[max_num_index_list[k]])
print('-------------------------------------')