当前位置:   article > 正文

词嵌入

词嵌入

所谓词嵌入,通俗来讲,是指将一个词语(word)转换为一个向量 (vector)表示, 所以词嵌入有时又被叫作“word2vec"。在CharRNN中,输入序列数据的每一步是一个字母 。 具体来说,先对这些字母使用了独热编码再输入到 RNN 中,在实际应用中 , 每一步只输入一个字母显然是不太合适的 , 更加高效的方法是每一步输入一个单词 。如果还继续使用独热表示,那么每一步输入的向量维数会非常大,比如说使用的单词的数量为 10000,那么独热表示形成的向量形状是 (10000,)。另外一方面,独热表示实际上完全平等看待了单词表中的所有单词 ,忽略了单词之间的联系 。所谓 word2vec,是指学习一个映射f,它可以将单词变成向量表示: vec = f(word)。在 RNN 的每一步输入中,不再用词语的独热表示,而是用映射之后的 vec输入模型,这样模型不仅会得到更丰富的有关词语的信息,而且输入的维数还下降了,因此性能会大大提高。使用密集和低维向量的好处之一是利于计算:大多数神经网络工具包无法很好地处理非常高维的稀疏向量。

本文会介绍两种词嵌入的方法 CBOW 和 Skip-Gram词嵌入的原理,然后会以 Skip-Gram 为例,详细介绍怎么在 TensorFlow 中实现它 。

一、词嵌入的原理

应当如何学习到上述的映射 f ? 一般来说有两种方法 , 一种方法是基于 “计数” 的 , 即在大型语料库中 , 计算一个词语和另一个词语同时出现的概率,将经常同时出现的词映射到向量空间的相近位置,另一种方法是基于“预测” 的,即从一个词或几个词出发 , 预测它们可能的相邻词,在预测过程中自然而然地学习到了词嵌入的映射 f。 通常使用的是基于预测的方法。具体来讲, 又有两种基于预测的方法,分别叫 CBOW 和 Skip-Gram,接下来分别介绍下它们的原理。

1、CBOW 实现词嵌入的原理

CBOW 的全称为 Continuous Bag of Words,即连续词袋模型 ,它的核心思想是利用某个词语的上下文预测这个词语 。

输入的单词被独热表示为x,经过一个全连接层得到隐含层 h, h再经过一个全连接层得到输出 y。 V是词汇表的中单词的数量,因此 独热表示的 x的形状是(V,)。输出 y相当于做 Softmax操作前的 logits, 它的形状也是(V,)。隐层的神经元数量为 N, N一般设定为小于 Y的值,如 256, 512等。 通过BP(反向传播)算法及随机梯度下降来学习权重。训练完成后,隐层的值被当作是词的嵌入表示,即 word2vec 中的“ vec”。

损失定义:在上述结构中,整个网络相当于是一个 V 类的分类器 。 V 是单词表中单词的数量,这个值往往非常大,所以比较难以训练,通常会简单修改网络的结构,将 V 类分类变成两类分类。模型会做一个两类分类:判断一个词汇是否属于“躁声词汇” 。 一般地,设上下文为 h , 该上下文对应的真正目标词汇为wt,躁声词汇为w,优化函数是:,其中: 代表的是利用 wt 和 h 对应的词嵌入向量进行一次 Logistic 回归得到的概率。这样的 Logistic 回归实际可以看作一层神经网络 。 因为 wt 为真实的目标单词,所以希望对应的 D=1。 另外w为与句子没关系的词汇,所以希望对应的D=O。通过优化二分类损失函数来训练模型后,最后得到的模型中的隐含层可以看作是 word2vec 中的“ vec”向量 。

 

2、Skip-Gram 实现词嵌入的原理

Skip-Gram 方法和 CBOW 方法正好相反,使用“出现的词”来预测“上下文文中词”。例如在 The man fell in love with the (woman)中,woman-> man, woman -> fell。

 

二、词嵌入tensorflow实现(Skip-Gram)

1、下载语料库

  1. # 语料库下载地址
  2. url = 'http://mattmahoney.net/dc/'
  3. def maybe_download(filename, expected_bytes):
  4. """
  5. 这个函数的功能是:
  6. 如果filename不存在,就在上面的地址下载它。
  7. 如果filename存在,就跳过下载。
  8. 最终会检查文字的字节数是否和expected_bytes相同。
  9. """
  10. if not os.path.exists(filename):
  11. print('start downloading...')
  12. filename, _ = urllib.request.urlretrieve(url + filename, filename)
  13. statinfo = os.stat(filename)
  14. if statinfo.st_size == expected_bytes:
  15. print('Found and verified', filename)
  16. else:
  17. print(statinfo.st_size)
  18. raise Exception(
  19. 'Failed to verify ' + filename + '. Can you get to it with a browser?')
  20. return filename
  21. # 下载语料库text8.zip并验证下载
  22. filename = maybe_download('text8.zip', 31344016)

 

2、读取语料库数据并转换格式

  1. # 将语料库解压,并转换成一个word的list
  2. def read_data(filename):
  3. """
  4. 这个函数的功能是:
  5. 将下载好的zip文件解压并读取为word的list
  6. """
  7. with zipfile.ZipFile(filename) as f:
  8. data = tf.compat.as_str(f.read(f.namelist()[0])).split()
  9. return data
  10. vocabulary = read_data(filename)
  11. print('Data size', len(vocabulary)) # 总长度为1700万左右
  12. # 输出前100个词。
  13. print(vocabulary[0:100])

 

3、用语料库制作单词表

单词表就是,将单词映射为一个数字,这个数字是该单词的 id。如5234 代表单词 anarchism, 3081 代表单词 originated。单词表一般只包含最常用的些词。对于剩下的不常用的词,会将其换为一个罕见词标记“ UNK”。所有的罕见词都会被映射为同一个单词 id。原来的训练数据 vocabulary是一个 单词的列表,在经过转换后, 变成了一个单词 id 的列表,即程序中的变量 data,它的形式是[5234, 3081, 12, 6, 195, 2, 3134, 46, ....]。

  1. # 制作一个词表,将不常见的词变成一个UNK标识符
  2. # 词表的大小为5万(即我们只考虑最常出现的5万个词)
  3. vocabulary_size = 50000
  4. def build_dataset(words, n_words):
  5. """
  6. 函数功能:将原始的单词表示变成index
  7. """
  8. count = [['UNK', -1]]
  9. count.extend(collections.Counter(words).most_common(n_words - 1))
  10. dictionary = dict()
  11. for word, _ in count:
  12. dictionary[word] = len(dictionary)
  13. data = list()
  14. unk_count = 0
  15. for word in words:
  16. if word in dictionary:
  17. index = dictionary[word]
  18. else:
  19. index = 0 # UNK的index为0
  20. unk_count += 1
  21. data.append(index)
  22. count[0][1] = unk_count
  23. reversed_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
  24. return data, count, dictionary, reversed_dictionary
  25. data, count, dictionary, reverse_dictionary = build_dataset(vocabulary,
  26. vocabulary_size)
  27. del vocabulary # 删除已节省内存
  28. # 输出最常出现的5个单词
  29. print('Most common words (+UNK)', count[:5])
  30. # 输出转换后的数据库data,和原来的单词(前10个)
  31. print('Sample data', data[:10], [reverse_dictionary[i] for i in data[:10]])
  32. # 我们下面就使用data来制作训练集
  33. data_index = 0

 

4、生成训练样本

变量 data包含了训练集中所有的数据,现在把它转换成训练时使用的 batch 数据 。一个 batch 可以看作是一些“单词对”的集合 , 如 woman-> man, woman -> fell,箭头左边表示“出现的单词”,右边表示该单词所在的“上下文”中的单词,这是采用Skip-Gram的方法。生成训练样本方法如下,它一共有三个参数,batch_size是单词对的个数,skip_window用来决定一个句子上下文的长度,在生成单词对时 ,会在语料库中先取出一个长度为 skip_window*2+ 1 连续单词列表,这个连续的单词列表是上面程序中的变量 buffer。 buffer 中最中间的那个单词是 Skip-Gram 方法中“ 出现的单词”,其余 skip_window*2 个单词是它的“上下文”。会在 skip_window*2 个单词中随机选取 num_skips 个单词,放入标签 labels。

如 skip_window=1, num_skips=2 的情况 。 首先选取一个长度为 3 的 buffer,假设是[’anarchism’, ’originated’,’as'],此时 originated 为中心单词, 剩下的两个单词为上下文 。再在这两个单词中选择 num_skips 个词形成标签 。 由于 num_skips = 2,所以只能将这两个单词都选上(标签不能重复), 最后生成的训练数据为 originated-> anarchism和 originated-> as。

  1. def generate_batch(batch_size, num_skips, skip_window):
  2. # data_index相当于一个指针,初始为0
  3. # 每次生成一个batch,data_index就会相应地往后推
  4. global data_index
  5. assert batch_size % num_skips == 0
  6. assert num_skips <= 2 * skip_window
  7. batch = np.ndarray(shape=(batch_size), dtype=np.int32)
  8. labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
  9. span = 2 * skip_window + 1 # [ skip_window target skip_window ]
  10. buffer = collections.deque(maxlen=span)
  11. # data_index是当前数据开始的位置
  12. # 产生batch后就往后推1位(产生batch)
  13. for _ in range(span):
  14. buffer.append(data[data_index])
  15. data_index = (data_index + 1) % len(data)
  16. for i in range(batch_size // num_skips):
  17. # 利用buffer生成batch
  18. # buffer是一个长度为 2 * skip_window + 1长度的word list
  19. # 一个buffer生成num_skips个数的样本
  20. # print([reverse_dictionary[i] for i in buffer])
  21. target = skip_window # target label at the center of the buffer
  22. # targets_to_avoid保证样本不重复
  23. targets_to_avoid = [skip_window]
  24. for j in range(num_skips):
  25. while target in targets_to_avoid:
  26. target = random.randint(0, span - 1)
  27. targets_to_avoid.append(target)
  28. batch[i * num_skips + j] = buffer[skip_window]
  29. labels[i * num_skips + j, 0] = buffer[target]
  30. buffer.append(data[data_index])
  31. # 每利用buffer生成num_skips个样本,data_index就向后推进一位
  32. data_index = (data_index + 1) % len(data)
  33. data_index = (data_index + len(data) - span) % len(data)
  34. return batch, labels
  35. # 默认情况下skip_window=1, num_skips=2
  36. # 此时就是从连续的3(3 = skip_window*2 + 1)个词中生成2(num_skips)个样本。
  37. # 如连续的三个词['used', 'against', 'early']
  38. # 生成两个样本:against -> used, against -> early
  39. batch, labels = generate_batch(batch_size=8, num_skips=2, skip_window=1)
  40. for i in range(8):
  41. print(batch[i], reverse_dictionary[batch[i]],
  42. '->', labels[i, 0], reverse_dictionary[labels[i, 0]])

 

5、定义模型

此处的模型实际可以抽象为:用一个单词预测另一个单词。在输出时,不使用 Softmax 损失,而使用 NCE 损失。即再选取一些“躁声词“作为负样本进行两类分类。NCE损失函数表示:,该损失函数计算上下文与目标单词之间的点积,采集每一个正样本的同时采集k个负样本。公式的第一项最小化正样本的损失,第二项最大化负样本的损失。现在如果将负样本作为第一项的变量输入,则损失函数结果应该很大。

batch_size = 128
embedding_size = 128  # 词嵌入空间是128维的。即word2vec中的vec是一个128维的向量
skip_window = 1       # skip_window参数和之前保持一致
num_skips = 2         # num_skips参数和之前保持一致

# 在训练过程中,会对模型进行验证 
# 验证的方法就是找出和某个词最近的词。
# 只对前valid_window的词进行验证,因为这些词最常出现
valid_size = 16     # 每次验证16个词
valid_window = 100  # 这16个词是在前100个最常见的词中选出来的
valid_examples = np.random.choice(valid_window, valid_size, replace=False)

# 构造损失时选取的噪声词的数量
num_sampled = 64
graph = tf.Graph()
with graph.as_default():
  # 输入的batch
  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)

  # 下面采用的某些函数还没有gpu实现,所以我们只在cpu上定义模型
  with tf.device('/cpu:0'):
    # 定义1个embeddings变量,相当于一行存储一个词的embedding
    embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
    # 利用embedding_lookup可以轻松得到一个batch内的所有的词嵌入
    embed = tf.nn.embedding_lookup(embeddings, train_inputs)

    # 创建两个变量用于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]))

  # tf.nn.nce_loss会自动选取噪声词,并且形成损失。
  # 随机选取num_sampled个噪声词
  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))

  # 得到loss后,我们就可以构造优化器了
  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
  # 找出和验证词的embedding并计算它们和所有单词的相似度
  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('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
      # 2000个batch的平均损失
      print('Average loss at step ', step, ': ', average_loss)
      average_loss = 0

    # 每1万步,我们进行一次验证
    if step % 10000 == 0:
      # sim是验证词与所有词之间的相似度
      sim = similarity.eval()
      # 一共有valid_size个验证词
      for i in xrange(valid_size):
        valid_word = reverse_dictionary[valid_examples[i]]
        top_k = 8  # 输出最相邻的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是我们最后得到的embedding向量
  # 它的形状是[vocabulary_size, embedding_size]
  # 每一行就代表着对应index词的词嵌入表示
  final_embeddings = normalized_embeddings.eval()

 

七、可视化

之前设定的 embedding_size=128, 即每个词都被表示为一个 128 维的向量 。虽然不能把 128 维的空间直接画出来,但下面的程序使用了 t-SNE 方法把 128 维空间映射到了 2 维 ,并画出最常使用的 500 个词的位置 。可视化的图片会保存为“tsne.png”。

def plot_with_labels(low_dim_embs, labels, filename='tsne.png'):
  assert low_dim_embs.shape[0] >= len(labels), 'More labels than embeddings'
  plt.figure(figsize=(18, 18))  # in inches
  for i, label in enumerate(labels):
    x, y = low_dim_embs[i, :]
    plt.scatter(x, y)
    plt.annotate(label,
                 xy=(x, y),
                 xytext=(5, 2),
                 textcoords='offset points',
                 ha='right',
                 va='bottom')

  plt.savefig(filename)

try:
  # pylint: disable=g-import-not-at-top
  from sklearn.manifold import TSNE
  import matplotlib
  matplotlib.use('agg')
  import matplotlib.pyplot as plt
  # 因为我们的embedding的大小为128维,没有办法直接可视化
  # 所以我们用t-SNE方法进行降维
  tsne = TSNE(perplexity=30, n_components=2, init='pca', n_iter=5000)
  # 只画出500个词的位置
  plot_only = 500
  low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only, :])
  labels = [reverse_dictionary[i] for i in xrange(plot_only)]
  plot_with_labels(low_dim_embs, labels)

except ImportError:
  print('Please install sklearn, matplotlib, and scipy to show embeddings.')


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/547345
推荐阅读
相关标签
  

闽ICP备14008679号