tensorflow实现skip-gram模型

skip-gram模型

使用tensorflow 1版本实现Word2Vec中的Skip-Gram模型
数据集:链接:https://pan.baidu.com/s/1GY0_wnIiuC9SUVw5p4JVfg 提取码:snu9
分为以下七个步骤

  1. 导包
  2. 加载数据
  3. 数据预处理
  4. 数据采样
  5. 训练数据构造
  6. 网络的构建
  7. 训练

1.导包

import time
import numpy as np
import tensorflow as tf
import random
import pandas as pd
from collections import Counter
tf.__version__
输出:1.14.0

2.加载数据

with open('data/Javasplittedwords',encoding='UTF-8') as f:
	text=f.read()
words=text.split(" ")
words
len(words)
输出:8898942

tensorflow实现skip-gram模型_第1张图片

3.数据预处理

数据预处理过程主要包括:

  1. 替换文本中特殊符号并去除低频词
  2. 对文本分词
  3. 构建语料
  4. 单词映射表
    首先我们定义一个函数来完成前两步,即对文本的清洗和分词操作。
#定义函数来完成数据预处理
def preprocess(text, freq=50):
    '''
    对文本进行预处理
    参数
    ---
    text: 文本数据
    freq: 词频阈值
    '''
    # 对文本中的符号进行替换
    text = text.lower()
    text = text.replace('.', '  ')
    text = text.replace(',', '  ')
    text = text.replace('"', '  ')
    text = text.replace(';', '  ')
    text = text.replace('!', '  ')
    text = text.replace('?', '  ')
    text = text.replace('(', '  ')
    text = text.replace(')', '  ')
    text = text.replace('--', '  ')
    text = text.replace('?', '  ')
    text = text.replace(':', '  ')
    words = text.split()
    # 删除低频词,减少噪音影响
    word_counts = Counter(words)#这是一个字典 单词:频次
    trimmed_words = [word for word in words if word_counts[word] > freq]

    return trimmed_words

上面的函数实现了替换标点及删除低频词操作,返回分词后的文本。
下面是清洗后的数据

# 清洗文本并分词
words = preprocess(text)
print(words[:20])
输出:
['熟练掌握', 'java', '熟悉', 'python', 'shell', '熟练使用', 'git', 'svn', '能够', '发现', '问题', '精准', '定位问题', '快速', '解决问题', '熟悉', 'jvm', 'jvm', '优化', '经验']
len(set(words))
输出:
6791

有了分词后的文本,就可以构建我们的映射表。

# 构建映射表
vocab = set(words)
#形成两个映射表,一个是单词到索引,一个是索引到单词
vocab_to_int = {word: index for index, word in enumerate(vocab)}
int_to_vocab = {index: word for index, word in enumerate(vocab)}
print("total words: {}".format(len(words)))
print("unique words: {}".format(len(set(words))))
输出:
total words: 8623686
unique words: 6791

整个文本中单词大约为800万的规模,词典大小为6000左右,这个规模对于训练好的词向量其实是不够的,但可以训练出一个稍微还可以的模型。

# 对原文本进行vocab到int的转换
int_words = [vocab_to_int[w] for w in words]
print(int_words[:20])
len(int_words)
输出:
[2342, 963, 2470, 5433, 6497, 1520, 5799, 938, 4492, 5288, 5844, 4648, 6316, 5771, 2649, 2470, 198, 198, 4296, 5033]
8623686

4.采样

在skip-gram中,训练样本的形式是(input word,output word),其中output word 是input word的上下文。为了减少模型噪音并加速训练速度,在构造batch之前要对样本进行采样,剔除停用词等噪音因素。对停用词进行采样,例如“你”, “我”以及“的”这类单词进行剔除。剔除这些单词以后能够加快我们的训练过程,同时减少训练过程中的噪音。
我们采用以下公式:
tensorflow实现skip-gram模型_第2张图片

t = 1e-3 # t值
threshold = 0.7 # 剔除概率阈值
# 统计单词出现频次
int_word_counts = Counter(int_words)
total_count = len(int_words)
# 计算单词频率
word_freqs = {w: c/total_count for w, c in int_word_counts.items()}
# 计算被删除的概率
prob_drop = {w: 1 - np.sqrt(t / word_freqs[w]) for w in int_word_counts}
# 对单词进行采样
train_words = [w for w in int_words if prob_drop[w] < threshold]
drop_words=[int_to_vocab[w] for w in int_words if prob_drop[w] > threshold]
set(drop_words)
输出:
{'产品', '工作', '开发', '熟悉', '相关', '经验', '能力', '设计', '负责'}
len(int_words)
输出:8623686
len(train_words)
输出:7536370

5.构造batch

tensorflow实现skip-gram模型_第3张图片
我们先来分析一下skip-gram的样本格式。skip-gram不同于CBOW,CBOW是基于上下文预测当前input word。而skip-gram则是基于一个input word来预测上下文,因此一个input word会对应多个上下文。我们来举个栗子“[熟练掌握 java 熟悉 python shell 熟练使用 git svn]”,如果我们固定skip_window=2的话,那么熟悉的上下文就是[熟练掌握, java, python, shell],如果我们的batch_size=1的话,那么实际上一个batch中有四个训练样本[熟悉,熟练掌握],[熟悉,java],[熟悉,python],[熟悉,shell]

上面的分析转换为代码就是两个步骤,第一个是找到每个input word的上下文,第二个就是基于上下文构建batch。

首先是找到input word的上下文单词列表:

Skip-Gram模型是通过输入词来预测上下文。因此我们要构造我们的训练样本。

对于一个给定词,离它越近的词可能与它越相关,离它越远的词越不相关,这里我们设置窗口大小为5,对于每个训练单词,我们还会在[1:5]之间随机生成一个整数R,用R作为我们最终选择output word的窗口大小。这里之所以多加了一步随机数的窗口重新选择步骤,是为了能够让模型更聚焦于当前input word的邻近词。

def get_targets(words, idx, window_size=5):
    '''
    获得input word的上下文单词列表
    参数
    ---
    words: 单词列表
    idx: input word的索引号
    window_size: 窗口大小
    '''
    target_window = np.random.randint(1, window_size+1)
    # 这里要考虑input word前面单词不够的情况
    start_point = idx - target_window if (idx - target_window) > 0 else 0
    end_point = idx + target_window
    # output words(即窗口中的上下文单词)
    targets = set(words[start_point: idx] + words[idx+1: end_point+1])
    return list(targets)

我们定义了一个get_targets函数,接收一个单词索引号,基于这个索引号去查找单词表中对应的上下文(默认window_size=5)。请注意这里有一个小trick,我在实际选择input word上下文时,使用的窗口大小是一个介于[1, window_size]区间的随机数。这里的目的是让模型更多地去关注离input word更近词。

我们有了上面的函数后,就能够轻松地通过input word找到它的上下文单词。有了这些单词我们就可以构建我们的batch来进行训练:

def get_batches(words, batch_size, window_size=5):
    '''
    构造一个获取batch的生成器
    '''
    n_batches = len(words) // batch_size
    
    # 仅取full batches
    words = words[:n_batches*batch_size]
    
    for idx in range(0, len(words), batch_size):
        x, y = [], []
        batch = words[idx: idx+batch_size]
        for i in range(len(batch)):
            batch_x = batch[i]
            batch_y = get_targets(batch, i, window_size)
            # 由于一个input word会对应多个output word,因此需要长度统一
            x.extend([batch_x]*len(batch_y))
            y.extend(batch_y)
        yield x, y

注意上面的代码对batch的处理。我们知道对于每个input word来说,有多个output word(上下文)。例如我们的输入是熟悉,上下文是[熟练掌握, java, python, shell],那么熟悉这一个batch中就有四个训练样本[熟悉, 熟练掌握], [熟悉, java], [熟悉, python], [熟悉, shell]。

6.构建网络

数据预处理结束后,就需要来构建我们的模型。在模型中为了加速训练并提高词向量的质量,我们采用负采样方式进行权重更新。

该部分主要包括:

  1. 输入层
  2. Embedding
  3. Negative Sampling

输入

train_graph = tf.Graph()
with train_graph.as_default():
    inputs = tf.placeholder(tf.int32, shape=[None], name='inputs')
    labels = tf.placeholder(tf.int32, shape=[None, None], name='labels')

嵌入层

输入层到隐层的权重矩阵作为嵌入层要给定其维度,一般embeding_size设置为50-300之间。
嵌入矩阵的矩阵形状为 _×ℎ__

vocab_size = len(int_to_vocab)
embedding_size = 300 # 嵌入维度
with train_graph.as_default():
    # 嵌入层权重矩阵
    embedding = tf.Variable(tf.random_uniform([vocab_size, embedding_size], -1, 1))
    # 实现lookup
    #实际上lookup就是一个按索引取值的方法
    embed = tf.nn.embedding_lookup(embedding, inputs)
    print(embed)
    输出:
    Tensor("embedding_lookup/Identity:0", shape=(?, 300), dtype=float32)

tf.nn.embedding_lookup函数的用法主要是选取一个张量里面索引对应的元素。tf.nn.embedding_lookup(params, ids):
params可以是张量也可以是数组等,id就是对应的索引

Negative Sampling

负采样主要是为了解决梯度下降计算速度慢的问题。
TensorFlow中的tf.nn.sampled_softmax_loss会在softmax层上进行采样计算损失,计算出的loss要比full softmax loss低。

n_sampled = 100

with train_graph.as_default():
    softmax_w = tf.Variable(tf.truncated_normal([vocab_size, embedding_size], stddev=0.1))
    softmax_b = tf.Variable(tf.zeros(vocab_size))
    
    # 计算negative sampling下的损失
    loss = tf.nn.sampled_softmax_loss(softmax_w, softmax_b, labels, embed, n_sampled, vocab_size)
    
    cost = tf.reduce_mean(loss)
    optimizer = tf.train.AdamOptimizer().minimize(cost)

请注意代码中的softmax_w的维度是 _∗_ ,这是因为TensorFlow中的sampled_softmax_loss中参数weights的size是[num_classes, dim]

验证

在上面的步骤中,我们已经将模型的框架搭建出来,下面就让我们来训练训练一下模型。为了能够更加直观地观察训练每个阶段的情况。我们来挑选几个词,看看在训练过程中它们的相似词是怎么变化的。

为了更加直观的看到我们训练的结果,我们将查看训练出的相近语义的词。

with train_graph.as_default():
#     # 随机挑选一些单词
#     valid_size = 16 
#     valid_window = 10
#     # 从不同位置各选8个单词
#     valid_examples = np.array(random.sample(range(valid_window), valid_size//2))
#     valid_examples = np.append(valid_examples, 
#                                random.sample(range(1000,1000+valid_window), valid_size//2))
    valid_examples = [vocab_to_int['word'], 
                      vocab_to_int['北京'], 
                      vocab_to_int['英语'],
                      vocab_to_int['java'], 
                      vocab_to_int['华为'], 
                      vocab_to_int['审计'],
                      vocab_to_int['健身房'],
                      vocab_to_int['学历']]
    
    valid_size = len(valid_examples)
    # 验证单词集
    valid_dataset = tf.constant(valid_examples, dtype=tf.int32)
    
    # 计算每个词向量的模并进行单位化
    norm = tf.sqrt(tf.reduce_sum(tf.square(embedding), 1, keepdims=True))
    normalized_embedding = embedding / norm
    # 查找验证单词的词向量
    valid_embedding = tf.nn.embedding_lookup(normalized_embedding, valid_dataset)
    # 计算余弦相似度
    similarity = tf.matmul(valid_embedding, tf.transpose(normalized_embedding))

7.模型训练

epochs = 2 # 迭代轮数
batch_size = 1000 # batch大小
window_size = 5 # 窗口大小

with train_graph.as_default():
    saver = tf.train.Saver() # 文件存储

with tf.Session(graph=train_graph) as sess:
    iteration = 1
    loss = 0
    # 添加节点用于初始化所有的变量
    sess.run(tf.global_variables_initializer())

    for e in range(1, epochs+1):
        # 获得batch数据
        batches = get_batches(train_words, batch_size, window_size)
        start = time.time()
        for x, y in batches:
            
            feed = {inputs: x,
                    labels: np.array(y)[:, None]}
            train_loss, _ = sess.run([cost, optimizer], feed_dict=feed)
            
            loss += train_loss
            
            if iteration % 1000 == 0: 
                end = time.time()
                print("Epoch {}/{}".format(e, epochs),
                      "Iteration: {}".format(iteration),
                      "Avg. Training loss: {:.4f}".format(loss/1000),
                      "{:.4f} sec/batch".format((end-start)/1000))
                loss = 0
                start = time.time()
           
            # 计算相似的词
            if iteration % 1000 == 0:
                print('*'*100)
                # 计算similarity
                sim = similarity.eval()
                for i in range(valid_size):
                    valid_word = int_to_vocab[valid_examples[i]]
                    top_k = 8 # 取最相似单词的前8个
                    nearest = (-sim[i, :]).argsort()[1:top_k+1]
                    log = 'Nearest to [%s]:' % valid_word
                    for k in range(top_k):
                        close_word = int_to_vocab[nearest[k]]
                        log = '%s %s,' % (log, close_word)
                    print(log)
                print('*'*100)
            
            iteration += 1
            
    save_path = saver.save(sess, "checkpoints/text8.ckpt")
    embed_mat = sess.run(normalized_embedding)

结果(只训练了两代):
tensorflow实现skip-gram模型_第4张图片

你可能感兴趣的:(nlp,python,tensorflow,深度学习,nlp,人工智能)