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相似度来判断)。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,561评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,218评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,162评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,470评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,550评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,806评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,951评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,712评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,166评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,510评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,643评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,306评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,930评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,745评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,983评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,351评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,509评论 2 348

推荐阅读更多精彩内容