本例将使用CBOW模型
来训练word2vec
,最终将所学到的词向量分布关系可视化出来,同时通过该例子练习使用nce_loss函数与word embedding技术,实现自己的word2vec。
描述
准备一段文字作为训练的样本,对其使用CBOW模型计算word2vec,并将各个词的向量关系用图展示出来。
1.引入头文件
本例的最后需要将词向量可视化出来。所以在代码行中有可视化相关的引入,即初始化,通过设置mpl的值让plot能够显示中文信息。ScikitLearn的t-SNE算法模块的作用是非对称降维,是结合了t分布将高维空间的数据点映射到低维空间的距离,主要用于可视化和理解高维数据。
代码word2vect.py
,引入头文件
import numpy as np
import tensorflow as tf
import random
import collections
from collections import Counter
import jieba
from sklearn.manifold import TSNE
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
mpl.rcParams['font.family'] = 'STSong'
mpl.rcParams['font.size'] = 20
这次重点关注的是词,不再对字进行one_hot
处理,所以需要借助分词工具将文本进行分词处理。本例中使用的是jieba
分词库,需要使用之前先安装该分词库。
在“运行”中,输入cmd,进入命令行模式。保证计算机联网状态下在命令行里输入:
Pip install jieba
安装完毕后可以新建一个py文件,代码简单测试一下:
import jieba
seg_list = jieba.cut("我爱人工智能") # 默认是精确模式
print(" ".join(seg_list))
输出:
我 爱 人工智能
如果能够正常运行并且可以分词,就表明jieba
分词库安装成功了。
2.准备样本创建数据集
这个环节使用一篇笔者在另一个领域发表的比较有深度的文章“阴阳人体与电能.txt” 来做样本,将该文件放到代码的同级目录下。
代码中使用get_ch_lable
函数将所有文字读入training_data
,然后在fenci函数里使用jieba分词库对training_data分词生成training_ci,将training_ci放入build_dataset里并生成指定长度(350)的字典。
代码 word2vect(续)
training_file = '人体阴阳与电能.txt'
# 中文字
def get_ch_lable(txt_file):
labels = ""
with open(txt_file, 'rb') as f:
for label in f:
# labels = labels + label.decode('utf-8')
labels = labels + label.decode('gb2312')
return labels
# 分词
def fenci(training_data):
seg_list = jieba.cut(training_data) # 默认是精确模式
training_ci = " ".join(seg_list)
training_ci = training_ci.split()
# 以空格将字符串分开
training_ci = np.array(training_ci)
training_ci = np.reshape(training_ci, [-1, ])
return training_ci
def build_dataset(words, n_words):
"""Process raw inputs into a dataset."""
count = [['UNK', -1]]
count.extend(collections.Counter(words).most_common(n_words - 1))
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 # dictionary['UNK']
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
training_data = get_ch_lable(training_file)
print("总字数", len(training_data))
training_ci = fenci(training_data)
print("总词数", len(training_ci))
training_label, count, dictionary, words = build_dataset(training_ci, 350)
words_size = len(dictionary)
print("字典词数", words_size)
print('Sample data', training_label[:10], [words[i] for i in training_label[:10]])
build_dataset中的实现方式是将统计词频0号位置给unknown(用UNK表示),其余按照频次由高到低排列。unknown的获取按照预设词典大小,比如350,则频次排序靠后于350的都视为unknown。
运行代码,生成结果如下:
总字数 1567
总词数 961
字典词数 350
Sample data [263, 31, 38, 30, 27, 0, 10, 9, 104, 197] ['如果', '人体', '与', '电能', '阴', 'UNK', '是', '身体', '里', '内在']
程序显示整个文章的总字数为1567个,总词数为961个,建立好的字典词数为350。接下来是将文字里前10个词即对应的索引显示出来。
3.获取批次数据
定义generate_batch
函数,取一定批次的样本数据。
代码 word2vect(续)
data_index = 0
def generate_batch(data, batch_size, num_skips, skip_window):
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)
span = 2 * skip_window + 1 # [ skip_window target 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):
target = skip_window # target label at the center of the buffer
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]
if data_index == len(data):
# print(data_index,len(data),span,len(data[:span]))
# buffer[:] = data[:span]
buffer = data[:span]
data_index = span
else:
buffer.append(data[data_index])
data_index += 1
# Backtrack a little bit to avoid skipping words in the end of a batch
data_index = (data_index + len(data) - span) % len(data)
return batch, labels
batch, labels = generate_batch(training_label, batch_size=8, num_skips=2, skip_window=1)
for i in range(8): # 取第一个字,后一个是标签,再取其前一个字当标签,
print(batch[i], words[batch[i]], '->', labels[i, 0], words[labels[i, 0]])
generate_batch函数中使用CBOW模型来构建样本,是从开始位置的一个一个字作为输入,然后将其前面和后面的字作为标签,再分别组合在一起变成2组数据。运行当前代码,输出如下:
31 人体 -> 38 与
31 人体 -> 263 阴阳
38 与 -> 31 人体
38 与 -> 30 电能
30 电能 -> 27 阴
30 电能 -> 38 与
27 阴 -> 0 UNK
27 阴 -> 30 电能
如果是Skip-Gram方法,根据字取标签的方法正好相反,输出会变成以下这样:
263 阴阳 -> 31 人体
38 与 -> 31 人体
31 人体 -> 38 与
31 人体 -> 263 阴阳
30 电能 -> 38 与
……
4.定义取样参数
下面代码中每批次取128个,每个词向量的维度为128,前后取词窗口为1,num_skips表示一个input生成2个标签,nce中负采样的个数为num_sampled。接下来是验证模型的相关参数,valid_size表示在0- words_size/2
中的数取随机不能重复的16个字来验证模型。
代码 word2vect(续)
111 batch_size = 128
112 embedding_size = 128 # embedding vector的维度
113 skip_window = 1 # 左右的词数量
114 num_skips = 2 # 一个input生成2个标签
115
116 valid_size = 16
117 valid_window = words_size/2 # 取样数据的分布范围
118 valid_examples = np.random.choice(valid_window, valid_size, replace=False)#0- words_size/2中的数取16个。不能重复
119 num_sampled = 64 # 负采样个数
5.定义模型变量
初始化图,为输入、标签、验证数据定义占位符,定义词嵌入变量embeddings为每个字定义128维的向量,并初始化为-1~1之间的均匀分布随机数。tf.nn.embedding_lookup是将输入的train_inputs转成对应的128维向量embed,定义nce_loss要使用的nce_weights和nce_biases。
代码9-26 word2vect(续)
120 tf.reset_default_graph()
121
122 train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
123 train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
124 valid_dataset = tf.constant(valid_examples, dtype=tf.int32)
125
126 # CPU上执行
127 with tf.device('/cpu:0'):
128 # 查找embeddings
129 embeddings = tf.Variable(tf.random_uniform([words_size,embedding_size], -1.0, 1.0)) #94个字,每个128个向量
130
131 embed = tf.nn.embedding_lookup(embeddings, train_inputs)
132
133 # 计算NCE的loss的值
134 nce_weights = tf.Variable( tf.truncated_normal([words_size,embedding_size],
135 stddev=1.0 / tf.sqrt(np.float32(embedding_size))))
136
137 nce_biases = tf.Variable(tf.zeros([words_size]))
在反向传播中,embeddings会与权重一起被nce_loss代表的loss值所优化更新。
6.定义损失函数和优化器
使用nce_loss计算loss来保证softmax时的运算速度不被words_size过大问题所影响,在nce中每次会产生num_sampled(64)个负样本来参与概率运算。优化器使用学习率为1的GradientDescentOptimizer。代码9-26 word2vect(续)
138 loss = tf.reduce_mean(
139 tf.nn.nce_loss(weights=nce_weights, biases=nce_biases,
140 labels=train_labels, inputs=embed,
141 num_sampled=num_sampled, num_classes=words_size))
142
143 # 梯度下降优化器
144 optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)
145
146 # 计算minibatch examples 和所有 embeddings的 cosine 相似度
147 norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
148 normalized_embeddings = embeddings / norm
149 valid_embeddings = tf.nn.embedding_lookup(normalized_embeddings,valid_dataset)
150 similarity = tf.matmul(valid_embeddings, normalized_embeddings,transpose_b=True)
验证数据取值时做了些特殊处理,将embeddings中每个词对应的向量进行平方和再开方得到norm,然后将embeddings与norm相除得到normalized_embeddings。当使用embedding_lookup获得自己对应normalized_embeddings中的向量valid_embeddings时,将该向量与转置后的normalized_embeddings相乘得到每个词的similarity。这个过程实现了一个向量间夹角余弦(Cosine)的计算。