自然语言处理(NLP):04 word2vec 入门介绍

本章节研究内容: 词向量介绍+word2vec两种架构cbow&skip-gram+google word2vec 源码分析+滑动窗口如何构建数据

by@ 沈福利

词向量表示

One-Hot Representation

NLP 中最直观,也是到目前为止最常用的词表示方法是 One-hot Representation,这种方法把每个词表示为一个很长的向量。这个向量的维度是词表大小,其中绝大多数元素为 0,只有一个维度的值为 1,这个维度就代表了当前的词。

举个栗子,

“话筒”表示为 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 …]

“麦克”表示为 [0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 …]

每个词都是茫茫 0 海中的一个 1。
这种 One-hot Representation 如果采用稀疏方式存储,会是非常的简洁:也就是给每个词分配一个数字 ID。比如刚才的例子中,话筒记为 3,麦克记为 8(假设从 0 开始记)。

问题:无法获取词与词之间的相似度;维数多个,稀疏严重

Distributed Represetation

Deep Learning 中一般用到的词向量是用 Distributed Representation表示的一种低维实数向量。例如: [0.792, −0.177, −0.107, 0.109, −0.542, …]。维度以 50 维和 100 维比较常见

通过训练将每个词映射K维的向量,可以通过词语间矩阵(例如: cos) 来判断语义相似度。 而Word2Vec 使用这种方式表示。
用这种方式表示的向量,“麦克”和“话筒”的距离会远远小于“麦克”和“天气”。

Distributed representation 用来表示词,通常被称为“Word Representation”或“Word Embedding”,中文俗称“词向量”。

语言模型训练

初步介绍

找常出现在每个单词附近的词,就能获得它们的映射关系.步骤如下:

  1. 先是获取大量文本数据(例如所有维基百科内容)
  2. 然后我们建立一个可以沿文本滑动的窗(例如一个窗里包含三个单词)
  3. 利用这样的滑动窗就能为训练模型生成大量样本数据。

窗口滑动,生成模型训练的数据集,通过下面的进行演示

  • 窗口锁定在句子的前三个单词上
    自然语言处理(NLP):04 word2vec 入门介绍_第1张图片
  • 前两个单词单做特征,第三个单词单做标签:生产了数据集中的第一个样本
    自然语言处理(NLP):04 word2vec 入门介绍_第2张图片
  • 窗口滑动到下一个位置并生产第二个样本
    自然语言处理(NLP):04 word2vec 入门介绍_第3张图片
    自然语言处理(NLP):04 word2vec 入门介绍_第4张图片
  • 所有数据集上全部滑动后,我们得到一个较大的数据集
    自然语言处理(NLP):04 word2vec 入门介绍_第5张图片
    图摘自:《Efficient Estimation of Word Representations in Vector Space.pdf》

自然语言处理(NLP):04 word2vec 入门介绍_第6张图片

CBOW 模型架构

CBOW 有两种可选的算法:层次 Softmax 和 Negative Sampling

CBOW 模型是预测 P(wt|wt-k,wt-(k-1)…,wt-1,wt+1,wt+2…,wt+k)

  • Softmax 算法:结合了 Huffman 编码,每个词 w 都可以从树的根结点沿着唯一一条路径被访问到;

  • Negative Sampling,原理说简单很简单,就是随机搞点负例。实际论文中提到按照词频的3/4 次方进行负样本的采样。

摘自google word2vec 源码: 创建Huffman tree 过程(C++)
// Create binary Huffman tree using the word counts
// Frequent words will have short uniqe binary codes
void CreateBinaryTree() {
  long long a, b, i, min1i, min2i, pos1, pos2, point[MAX_CODE_LENGTH];
  char code[MAX_CODE_LENGTH];
  long long *count = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long));
  long long *binary = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long));
  long long *parent_node = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long));
  for (a = 0; a < vocab_size; a++) count[a] = vocab[a].cn;
  for (a = vocab_size; a < vocab_size * 2; a++) count[a] = 1e15;
  pos1 = vocab_size - 1;
  pos2 = vocab_size;
  // Following algorithm constructs the Huffman tree by adding one node at a time
  for (a = 0; a < vocab_size - 1; a++) {
    // First, find two smallest nodes 'min1, min2'
    if (pos1 >= 0) {
      if (count[pos1] < count[pos2]) {
        min1i = pos1;
        pos1--;
      } else {
        min1i = pos2;
        pos2++;
      }
    } else {
      min1i = pos2;
      pos2++;
    }
    if (pos1 >= 0) {
      if (count[pos1] < count[pos2]) {
        min2i = pos1;
        pos1--;
      } else {
        min2i = pos2;
        pos2++;
      }
    } else {
      min2i = pos2;
      pos2++;
    }
    count[vocab_size + a] = count[min1i] + count[min2i];
    parent_node[min1i] = vocab_size + a;
    parent_node[min2i] = vocab_size + a;
    binary[min2i] = 1;
  }
  // Now assign binary code to each vocabulary word
  for (a = 0; a < vocab_size; a++) {
    b = a;
    i = 0;
    while (1) {
      code[i] = binary[b];
      point[i] = b;
      i++;
      b = parent_node[b];
      if (b == vocab_size * 2 - 2) break;
    }
    vocab[a].codelen = i;
    vocab[a].point[0] = vocab_size - 2;
    for (b = 0; b < i; b++) {
      vocab[a].code[i - b - 1] = code[b];
      vocab[a].point[i - b] = point[b] - vocab_size;
    }
  }
  free(count);
  free(binary);
  free(parent_node);
}


 // 摘自 google word2vec 负采样的过程
 // NEGATIVE SAMPLING
        if (negative > 0) for (d = 0; d < negative + 1; d++) {
          if (d == 0) {
            target = word;
            label = 1;
          } else {
            next_random = next_random * (unsigned long long)25214903917 + 11;
            target = table[(next_random >> 16) % table_size];
            if (target == 0) target = next_random % (vocab_size - 1) + 1;
            if (target == word) continue;
            label = 0;
          }
          l2 = target * layer1_size;
          f = 0;
          for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1neg[c + l2];
          if (f > MAX_EXP) g = (label - 1) * alpha;
          else if (f < -MAX_EXP) g = (label - 0) * alpha;
          else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
          for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];
          for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * neu1[c];
        }
        

Skip-gram模型架构

如果预料库比较大的时候,一般认为skip-gram 架构比CBOW 效果好

摘自论文: Distributed Representations of Words and Phrases.pdf

定义softmax 函数来计算相邻单词出现的概率。 W是词典的个数,长度非常的大。

接下来我们需要一种解决方法提升效率: 哈夫曼编码和负采样
自然语言处理(NLP):04 word2vec 入门介绍_第7张图片
哈夫曼编码: 使得预测上下文词典从平铺到一个哈夫曼树。
负采样: 正验证+负样本(较少了待预测词的维度)

最终通过softmax 输出每个单词的概率

训练原始数据

  • 推测当前单词可能的前后单词。我们设想一下滑动窗在训练数据时如下图

自然语言处理(NLP):04 word2vec 入门介绍_第8张图片

滑动窗口一次

  • 滑动窗口,为数据集提供了4个样本

自然语言处理(NLP):04 word2vec 入门介绍_第9张图片

滑动窗口再一次

  • 移动滑动窗到下一个位置:又产生了接下来4个样本

自然语言处理(NLP):04 word2vec 入门介绍_第10张图片

滑动结束样本结果

  • 依次类推,最终能得到一批样本

自然语言处理(NLP):04 word2vec 入门介绍_第11张图片
从现有的文本中获得了Skipgram模型的训练数据集,接下来让我们看看如何使用它来训练一个能预测相邻词汇的自然语言模型

最终skip-gram 的训练数据

这种多类问题,参数数据量很大,我们想要转化一种思路,例如: 判断两个单词是否相邻,问题: 由多分类专为二分类问题
自然语言处理(NLP):04 word2vec 入门介绍_第12张图片

负采样 Negative Sampling

逻辑回归问题: 1-表示两个单词相邻 0-两个单词不相邻

从数据集中的第一个样本开始。我们将特征输入到未经训练的模型,让它预测一个可能的相邻单词。

自然语言处理(NLP):04 word2vec 入门介绍_第13张图片

Word2vec训练流程

  • Embedding矩阵: 输入词从该矩阵查找
  • Context矩阵:对于上下文单词从该矩阵查找

自然语言处理(NLP):04 word2vec 入门介绍_第14张图片

  • 误差计算公式: error = target - sigmoid_scores

当我们循环遍历整个数据集多次时,嵌入会继续得到改进。然后我们就可以停止训练过程,丢弃Context矩阵,并使用Embeddings矩阵作为下一项任务的已被训练好的嵌入

自然语言处理(NLP):04 word2vec 入门介绍_第15张图片
自然语言处理(NLP):04 word2vec 入门介绍_第16张图片

窗口大小和负样本数量

word2vec训练过程中的两个关键超参数: 窗口大小和负样本数量

  • 窗口大小: gensim默认窗口大小为5
    自然语言处理(NLP):04 word2vec 入门介绍_第17张图片
  • 负样本的数量:论文认为5-20个负样本是比较理想的数量
    自然语言处理(NLP):04 word2vec 入门介绍_第18张图片

你可能感兴趣的:(Python,机器学习)