fastText Ngram 的处理过程

最近小研究了下fastText过程,挺有收获,这里单讲下fastText的ngram处理过程,其余训练一类和word2vec很像,有兴趣的请移步 fastText 源码分析。

基础信息

首先说下
(1)ngram的使用前提是数据稀疏,这一点极其重要,也是后文成立的关键;
(2)fastText训练和预测使用的隐藏层和输出层大小是一致的;
(3)fastText的ngram并没有有意保存ngram信息,只是hash并用于检索了;
(4)fastText的ngram分两种,第一种用于分类(supervised,需预设定label),第二种用于词向量(cbow和skipgram)。

然后步入正文,有代码有真相。
从数据读入开始。

void Dictionary::readFromFile(std::istream& in) {
  std::string word;
  int64_t minThreshold = 1;
  while (readWord(in, word)) { // readWord每次读入一个词,空格和常见的不可见字符都可做为分割符
    add(word);
    if (ntokens_ % 1000000 == 0 && args_->verbose > 1) {
      std::cerr << "\rRead " << ntokens_  / 1000000 << "M words" << std::flush;
    }
    if (size_ > 0.75 * MAX_VOCAB_SIZE) {  // 保证word和label的总数小于限额
      minThreshold++;
      threshold(minThreshold, minThreshold);  // 超过限额,按要求删除一些低频词
    }
  }
  threshold(args_->minCount, args_->minCountLabel);
  initTableDiscard();
  initNgrams();
  ... //省略输出统计信息的代码
}

用于词向量的ngram

首先看看计算word的subword过程:
下段代码需处理utf8字符,详见 “UTF8 编码原理简介”

// 该函数仅被 Dictionary::getSubword(...)调用
// Dictionary::getSubword(...)被用于训练词向量模型(skipgram和cbow)
// 、输出词向量(print-word-vectors和print-sentence-vectors)和计算词相似性(nn和analogies)
// !!!也就是说这个函数计算的ngram与分类不相关!!!
void Dictionary::computeSubwords(const std::string& word,
                               std::vector& ngrams) const {
  for (size_t i = 0; i < word.size(); i++) {
    std::string ngram;
    if ((word[i] & 0xC0) == 0x80) continue;
    for (size_t j = i, n = 1; j < word.size() && n <= args_->maxn; n++) {
      ngram.push_back(word[j++]);
      // 处理utf8字符,原理见前文帖子
      while (j < word.size() && (word[j] & 0xC0) == 0x80) { 
        ngram.push_back(word[j++]);
      }
      // 处理ngram
      if (n >= args_->minn && !(n == 1 && (i == 0 || j == word.size()))) {
        int32_t h = hash(ngram) % args_->bucket;
        ngrams.push_back(nwords_ + h); // 这个ngram用于检索subword
      }
    }
  }
}

用于分类的ngram

再来看用于分类的ngram
重点在于Dictionary::getLine(…)和Dictionary::addWordNgrams(…)

int32_t Dictionary::getLine(std::istream& in,
                            std::vector& words,
                            std::vector& labels,
                            std::minstd_rand& rng) const {
  std::vector word_hashes;
  int32_t ntokens = getLine(in, words, word_hashes, labels, rng); //计算词及词的hash
  if (args_->model == model_name::sup ) {  // !!!关键之处,分类问题与词向量的分界!!!
    addWordNgrams(words, word_hashes, args_->wordNgrams);
  }
  return ntokens;
}

void Dictionary::addWordNgrams(std::vector& line,
                           const std::vector& hashes,
                           int32_t n) const {
  if (pruneidx_size_ == 0) return;
  for (int32_t i = 0; i < hashes.size(); i++) {
    uint64_t h = hashes[i];
    // 这里的 "n" 就是wordNgrams了
    for (int32_t j = i + 1; j < hashes.size() && j < i + n; j++) {
      // 这个处理让我迷糊了好久,这个hash的计算过程和Dictionary::hash()不同
      // 那么词是如何接起来的呢,后来终于恍然大悟
      // !!!fastText使用两种ngram!!!
      // 词向量的ngram是分解单词,abc => a, ab, abc, b, bc
      // 分类的ngram是组合单词,a,b,c => a, ab, abc, b, bc , 这个长度由wordNgrams指定,这也是fastText的优势所在
      h = h * 116049371 + hashes[j];  
      int64_t id = h % args_->bucket;
      if (pruneidx_size_ > 0) {
        if (pruneidx_.count(id)) {
          id = pruneidx_.at(id);
        } else {continue;}
      }
      line.push_back(nwords_ + id);
    }
  }
}

你可能感兴趣的:(fastText)