Tensorflow学习笔记——word2vec

 
 

本笔记记录一下鄙人在使用tf的心得,好让自己日后可以回忆一下。其代码内容都源于tf的tutorial里面的Vector Representations of Words。

现在我们一起来实现通过tf实现word2vec吧。

代码地址:https://github.com/tensorflow/tensorflow/blob/r1.2/tensorflow/examples/tutorials/word2vec/word2vec_basic.py


step1 数据集

url = 'http://mattmahoney.net/dc/'

def maybe_download(filename, expected_bytes):
     pass

filename = maybe_download('text8.zip', 31344016)

filename 是我们的待处理的目标文件。其实是它是在http://mattmahoney.net/dc/text8.zip 里面,而这个函数就判断本地时候存在该文件,若没有就网上读取(鄙人就先下载下来)。我不知道为毛它要判断文件大小跟预期一样,也不影响我们后面的工作。


step2 读取数据

def read_data(filename):
    """
    提取第一个文件当中的词列表
    :param filename:
    :return:
    """
    with zipfile.ZipFile(filename) as f:
        data = tf.compat.as_str(f.read(f.namelist()[0])).split()
    return data

这里不用细说啦。因为都是简单的i/o

vocabulary = read_data(filename)
vocabulary_size = 50000

读取了文件里面的词语后,为了方便,我们先定义自己的词典大小为5W。


step3 建立词典

def build_dataset(words, n_words):
    """
    建立字典数据库
    :param words:
    :param n_words:
    :return:
    """
    count = [['UNK', -1]]
    # 记录前49999个高频词,各自出现的次数
    count.extend(collections.Counter(words).most_common(n_words - 1))
    # Key value pair : {word: dictionary_index}
    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
            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
  • count:记录词语和对应词频的key-value
  • data:记录文本内的每一个次对应词典(dictionary)的索引
  • dictionary:记录词语和相应词典索引
  • reversed_dicitonary:记录词典索引和相应的词语,跟dictionary的key-value相反

这里的['UNK', -1]记录这一些词典没有记录的词语,因为我们只拿文本中出现次数最多的前49999词作为词典中的词语。这意味着有一些我们不认识的词语啊。那我们就将其当作是我们词典的“盲区”,不认识的词(unknown words)简称UNK。

# 词语索引,每个词语词频,词典,词典的反转形式
data, count, dictionary, reverse_dictionary = build_dataset(vocabulary, vocabulary_size)

调用该函数我们就获得词典的内容


step4 开始建立skip-gram模型需要的数据集合(data, label)

对于传统的ML或者DL都会使用有监督型的数据进行训练。对于skip-gram模型,我需要的数据集合应该是{(x)data: target word, (y)label: context words}。它跟CBOW是截然不同的,因为CBOW是需要通过上下文推断目标词语,所以需要的数据集合是{data: context words, label target word}。现在我们根据文本内容和从文本获得词典,我们开始建立训练数据集合。

# 给skip-gram模型生成训练集合
def generate_batch(batch_size, num_skips, skip_window):
    """
    :param batch_size: 训练批次(batch)的大小
    :param num_skips:  采样的次数
    :param skip_window:  上下文的大小
    :return:
    """
    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)
    # 上下文的组成:[skip_window target skip_window]
    span = 2 * skip_window + 1
    # 缓冲区
    buffer = collections.deque(maxlen=span)
    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
        target_to_avoid = [skip_window]
        for j in range(num_skips):
            #
            while target in target_to_avoid:
                target = random.randint(0, span - 1)
            target_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)

    data_index = (data_index + len(data) - span) % len(data)
    return batch, labels

这里我们得到的batch就是我们想要的输入词语/数据(词语在词典当中的索引),另外label是batch对应的目标词语/数据(词语在词典当中的索引)。这里举个例子,我们现在给定batch_size为8,num_skips为2,skip_window=1,给出文本为

"I am good at studying and learning ML. However, I don't like to read the English document."

我粗略算算词典

['I', 'am', 'good', 'at', 'studying', 'and', 'learning', 'ML', 'However', 'I', 'don't', 'like', 'to', 'read', the', 'English', 'document']

根据generate_batch的内容和给定参数,我们第一次获得内容应该是

['I', 'am', 'good', 'at', 'studying', 'and', 'learning', 'ML']

我们的上下文窗口(span)应该是 2 * 1 + 1 = 3。也就是窗口应该是

buffer=['I', 'am', 'good']

显然target应该是'am'也就是为buffer[skip_window]context word应该是['I', 'good']。这就构成了{x: data, y: label}之间的关系。
对于skip-gram模型的数据集合

  • {(x)data: 'am', (y)label: 'I'}
  • {(x)data: 'am', (y)label: 'good'}

如此类推。那num_skips有啥用呢?其实num_skips意味着需要对buffer进行多少次才采样,才开始对下一个buffer进行采样。


step5 开始建立skip-gram模型(重点来了)

batch_size = 128
embedding_size = 128 # 嵌入向量的维度
skip_window = 1     # 上下文的词数
num_skips = 2       # 多少次后重用输入的生成类别

# 我们使用随机邻居样本生成评估集合,这里我们限定了
# 评估样本一些数字ID词语,这些ID是通过词频产生
valid_size = 16
valid_window = 100
valid_examples = np.random.choice(valid_window, valid_size, replace=False)
num_sample = 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)

    with tf.device('/cpu:0'):
        # 随机生成初始词向量
        embeddings = tf.Variable(
            tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0)
        )
        # 根据batch的大小设置输入数据的batch
        embed = tf.nn.embedding_lookup(embeddings, train_inputs)
        # 设置权值
        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 = tf.reduce_mean(
        tf.nn.nce_loss(
            weights=nce_weights,
            biases=nce_biases,
            labels=train_labels,
            inputs=embed,
            num_sampled=num_sample,
            num_classes=vocabulary_size
        )
    )

    # learning rate 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)
    # 计算cosine相似度
    similarity = tf.matmul(valid_embeddings, normalized_embeddings, transpose_b=True)

    init = tf.global_variables_initializer()

这里可能没有之前这么简单了,因为不懂word2vec数学原理的人,完全看不懂代码,尽管你精通Python,也不知道为毛有这行代码和代码的含义。这里我不多讲word2vec的数学原理,迟点我会再一遍文章讲解word2vec的原理和疑问。这里我给出一篇我看过的详细的文章word2vec的数学原理,大家可以先阅览一下。我在这里稍微讲一下代码和附带的原理内容。

        # 随机生成初始词向量
        embeddings = tf.Variable(
            tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0)
        )

大家都知道word2vec就是说把词语变成向量形式,而在CBOW和skip-gram模型中,词向量是副产品,真正的目的是推断出上下文内容。这里就来一个要点了(是鄙人私以为的):

在模型的训练过程中,调整词向量和不断是推断逼近目标词语是同时进行。也就是说调整词向量->优化推断->调整词向量->优化推断->调整词向量->优化推断.... 最后达到两者同时收敛。这就是我们最后的目标。这是我从EM算法中类比获得的想法,关于EM算法,我会在之后添加文章(算法推导+代码)。

在DL和ML中我们都说到损失函数,不断优化损失函数使其最小,是我们的目标。这里的损失函数是什么呢?那就是

        tf.nn.nce_loss(
            weights=nce_weights,
            biases=nce_biases,
            labels=train_labels,
            inputs=embed,
            num_sampled=num_sample,
            num_classes=vocabulary_size
        )

我们刚刚说到要把推断出哪个词应该出现在上下文当中,就是涉及到一个概率问题了。既然是推断那就是要比较大小啦。那就是把词典中所有的词的有可能出现在上下文的概率都算一遍吗?确实!在早期word2vec论文发布时,就是这么粗暴。现在就当然不是啦。那就是用negative sample来推断进行提速啦。

我们知道在训练过程中,我们都知道label是哪个词。这意味着其他词对于这个样本就是negative了。那就好办啦。我就使得label词的概率最大化,其他词出现的概率最小化。当中涉及的数学知识就是Maximum likelihood 最大似然估计。不懂的回去复习呗。

之后我们用梯度下降法进行训练,这样我们就得到训练模型了。


step6 开始进行无耻的训练

num_steps = 100001

with tf.Session(graph=graph) as session:
    # 初始化变量
    init.run()

    print("Initialized")

    average_loss = 0
    for step in xrange(num_steps):

        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

        if step % 2000 == 0:
            if step > 0:
                average_loss /= 2000
            print('Average loss at step ', step, ': ', average_loss)
            average_loss = 0

        if step % 10000 == 0:
            sim = similarity.eval()
            for i in xrange(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 xrange(top_k):
                    close_word = reverse_dictionary[nearest[k]]
                    log_str = "%s %s," % (log_str, close_word)
                print(log_str)

    final_embeddings = normalized_embeddings.eval()

在每次训练中我们都给数据模型喂养(feed)一小批数据(batch_input, batch_labels)。这些数据是通过generate_batch()生成的。通过暴力的迭代,我们最后得到最终词向量(final_embedding)。在训练过程中,每2000次迭代打印损失值,每10000次迭代打印校验词的相似词(通过cosin相似度来判断)。

最后还差一个词向量降维后的图片,我迟点不上。现在准备煮饭咯....



作者:Salon_sai
链接:http://www.jianshu.com/p/1624ede1f2ac
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值