cover_image

fastText的源码,看这一篇就够了!

王文彬 AI老炮观察站
2018年12月03日 17:51

图片

面对一个新的模型,我们可能会想这个模型谁提出的,可以做什么。然后考虑是否有需要,开始用之前,会问这个模型支持哪些参数,数据格式是什么,至此,可以试试效果。试完效果发现咦还不错,后面可能会横向、纵向比较。使用一段时候开始研究源码,除可以弄清楚原理外,还可以学习很多编程技巧,知其所以然。因此,本解析将会围绕这些展开讨论。

1 fasteText能干什么

1.1 fastText是什么

fastText是Facebook AI Reserch在16年开源的一个词向量及文本分类工具,性能比肩深度学习而且速度更快,能够训练模型“在使用标准多核CPU的情况下10分钟内处理超过10亿个词汇”,特别是与深度模型对比,fastText能将训练时间由数天缩短到几秒钟。并且,它在模型架构上跟word2vec非常相似,两者作者都是Tomas Mikolov。

1.2 fastText支持哪些参数

现在已经知道fastText能够干什么,而且词向量已经有成名的word2vec,所以让我们聚焦在fastText的另外一个功能分类。在使用模型前,比较重要的就是模型提供了什么参数,具体参考fastText源码[1]。

fastText 提供的几个功能

参数说明
supervised监督学习,训练分类器
test评价一个分类器
predict预测标签
predict-prob预测标签,并输出预测概率
skipgram训练skipgram模型
cbow训练cbow模型
print-vectors输入一个训练好的模型,打印向量

fastText 分类提供的参数

参数说明默认值
-inputtraining file path
-outputoutput file path
-verboseverbosity level2
-minCountminimal number of word occurrences[1]
-minCountLabelminimal number of label occurrences[0]
-wordNgramsword ngram的最大长度[1]
-bucket提供word 和char ngram最大值[2000000]
-minnchar ngram的最小长度[0]
-maxnchar ngram 的最大长度[0]
-tsampling threshold[0.0001]
-labellabels prefix[__label__]
-lrlearning rate[0.1]
-lrUpdateRatechange the rate of updates for the learning rate[100]
-dimsize of word vectors[100]
-wssize of the context window[5]
-epochnumber of epochs[5]
-negnumber of negatives sampled[5]
-lossloss function {ns, hs, softmax}[softmax]
-threadnumber of threads[12]
-pretrainedVectors对于分类问题可以提供与训练的词向量[]
-saveOutputwhether output params should be saved[0]

1.3 fastText 输入输出

输入:一行为一条训练数据,每个词可以用空格等常用分隔符分割,对于分类问题来说,在类别前面加"__label__"表示其为该行训练数据的标签,当然可以自定义了,可以使用-label自定义,训练文件支持 UTF-8 格式,字符会按照UTF-8编码分割。

输出:输出支持topK,带概率.

1.4 fastText 为什么如此快

  • 多线程,不加锁,同时更新参数,会带来一点点噪音,但是影响不大。

  • 模型抽象,最后一层都抽象为LR,预先计算sigmoid的值。见model模块分析。

  • 模型本身足够简单。

2 fastText与word2vec的异同

如果有对word2vec原理不清楚的可以看这篇博客word2vec[2],详细介绍了word2vec的数学原理。

2.1 相同点

了解word2vec的同学,可以发现fasttext的模型结构和思想基本一致,倒是省去了理解的步骤。

2.2 不同点

  • 增加分类

  • 增加word和char ngram

3 源码解析

fasttext整体的结构图如下:$$x_i$$为根据dictionary找到每个词对应的id,根据id取对应的向量。然后,求和平均得到中间隐变量h。最后,经过Negative Sampling、Softmax或者Hierarchical Softmax进行分类。

图片

3.1 源码结构

源码提供了以下几个模块,核心model模块提供各种算法:

图片

3.2 头文件解析

说明
args.h提供主要参数解析
dictionary.h主要模块,提供计数、映射、ngram
fasttext.h集合各个类
matrix.h词向量矩阵
model.h核心模块,各种算法
real.h数据类型
utils.h文件相关的工具类
vector.h向量抽象类

3.3 主要类fasttext.h

图片

fasttext 是最顶层的模块,它的主要功能是训练和预测,首先是训练功能的调用路径,第一个函数是 train,它的主要作用是 初始化参数,启动多线程训练。

  1. //  训练

  2. void FastText::train(const Args args) {

  3.  args_ = std::make_shared<Args>(args);

  4.  dict_ = std::make_shared<Dictionary>(args_);

  5.  if (args_->input == "-") {

  6.    // manage expectations

  7.    throw std::invalid_argument("Cannot use stdin for training!");

  8.  }

  9.  std::ifstream ifs(args_->input);

  10.  if (!ifs.is_open()) {

  11.    throw std::invalid_argument(

  12.        args_->input + " cannot be opened for training!");

  13.  }

  14.  // 字典的构建

  15.  dict_->readFromFile(ifs);

  16.  ifs.close();

  17.  // 加载预训练的词向量

  18.  if (args_->pretrainedVectors.size() != 0) {

  19.    loadVectors(args_->pretrainedVectors);

  20.  } else {

  21.    // 否则随机生成词典,词典的大小等于词的个数加上ngram个个数或者说是参数bucket的大小

  22.    input_ = std::make_shared<Matrix>(dict_->nwords()+args_->bucket, args_->dim);

  23.    input_->uniform(1.0 / args_->dim);

  24.  }

  25.  // 区分是训练词向量还是分了,本质上一样,只是一个目标是词一个是类别

  26.  // 因此输出矩阵大大小不一样

  27.  if (args_->model == model_name::sup) {

  28.    output_ = std::make_shared<Matrix>(dict_->nlabels(), args_->dim);

  29.  } else {

  30.    output_ = std::make_shared<Matrix>(dict_->nwords(), args_->dim);

  31.  }

  32.  output_->zero();

  33.  startThreads();

  34.  model_ = std::make_shared<Model>(input_, output_, args_, 0);

  35.  if (args_->model == model_name::sup) {

  36.    model_->setTargetCounts(dict_->getCounts(entry_type::label));

  37.  } else {

  38.    model_->setTargetCounts(dict_->getCounts(entry_type::word));

  39.  }

  40. }

具体多线程训练部分,按照线程数把文件分成多个部分,每个部分独立更新参数,线程之间并没有加锁。因此,会带来一点噪音,但是对最后的结果没有多大影响。word2vec 实现也没有加锁。

  1. void FastText::trainThread(int32_t threadId) {

  2.  std::ifstream ifs(args_->input);

  3.  // 根据线程id选择要训练的数据,按照字节数分割,不是按照行分割

  4.  utils::seek(ifs, threadId * utils::size(ifs) / args_->thread);

  5.  // 每个线程都会新建一个model,但是model参数是共享的

  6.  Model model(input_, output_, args_, threadId);

  7.  // 区分是词向量还是分类

  8.  if (args_->model == model_name::sup) {

  9.    model.setTargetCounts(dict_->getCounts(entry_type::label));

  10.  } else {

  11.    model.setTargetCounts(dict_->getCounts(entry_type::word));

  12.  }

  13.  const int64_t ntokens = dict_->ntokens();

  14.  int64_t localTokenCount = 0;

  15.  std::vector<int32_t> line, labels;

  16.  while (tokenCount_ < args_->epoch * ntokens) {

  17.    real progress = real(tokenCount_) / (args_->epoch * ntokens);

  18.    real lr = args_->lr * (1.0 - progress);

  19.    // 不同的模型选择不同的训练方式

  20.    if (args_->model == model_name::sup) {

  21.      localTokenCount += dict_->getLine(ifs, line, labels);

  22.      supervised(model, lr, line, labels);

  23.    } else if (args_->model == model_name::cbow) {

  24.      localTokenCount += dict_->getLine(ifs, line, model.rng);

  25.      cbow(model, lr, line);

  26.    } else if (args_->model == model_name::sg) {

  27.      localTokenCount += dict_->getLine(ifs, line, model.rng);

  28.      skipgram(model, lr, line);

  29.    }

  30.    // 迭代过程中,根据迭代次数,降低学习率

  31.    if (localTokenCount > args_->lrUpdateRate) {

  32.      tokenCount_ += localTokenCount;

  33.      localTokenCount = 0;

  34.      if (threadId == 0 && args_->verbose > 1)

  35.        loss_ = model.getLoss();

  36.    }

  37.  }

  38.  if (threadId == 0)

  39.    loss_ = model.getLoss();

  40.  ifs.close();

  41. }

你没有看错,fasttext提供的三个功能,它们的代码很相似,最后的更新后抽象成model.update(line, labels[i], lr)。supervised的输入就是词对应的id、标签、学习率等。model的update见3.5 model分析。

  1. void FastText::supervised(

  2.    Model& model,

  3.    real lr,

  4.    const std::vector<int32_t>& line,

  5.    const std::vector<int32_t>& labels) {

  6.  if (labels.size() == 0 || line.size() == 0) return;

  7.  // 支持一条样本多个标签,随机选择一个标签

  8.  std::uniform_int_distribution<> uniform(0, labels.size() - 1);

  9.  int32_t i = uniform(model.rng);

  10.  // 更新参数

  11.  model.update(line, labels[i], lr);

  12. }

  13. void FastText::cbow(Model& model, real lr,

  14.                    const std::vector<int32_t>& line) {

  15.  std::vector<int32_t> bow;

  16.  std::uniform_int_distribution<> uniform(1, args_->ws);

  17.  for (int32_t w = 0; w < line.size(); w++) {

  18.    int32_t boundary = uniform(model.rng);

  19.    bow.clear();

  20.    // 选择上下文,预测中间词

  21.    for (int32_t c = -boundary; c <= boundary; c++) {

  22.      if (c != 0 && w + c >= 0 && w + c < line.size()) {

  23.        const std::vector<int32_t>& ngrams =

  24.                              dict_->getSubwords(line[w + c]);

  25.        bow.insert(bow.end(), ngrams.cbegin(), ngrams.cend());

  26.      }

  27.    }

  28.    model.update(bow, line[w], lr);

  29.  }

  30. }

  31. void FastText::skipgram(Model& model, real lr,

  32.                        const std::vector<int32_t>& line) {

  33.  std::uniform_int_distribution<> uniform(1, args_->ws);

  34.  for (int32_t w = 0; w < line.size(); w++) {

  35.    int32_t boundary = uniform(model.rng);

  36.    const std::vector<int32_t>& ngrams = dict_->getSubwords(line[w]);

  37.    // 根据中间词,预测上下文

  38.    for (int32_t c = -boundary; c <= boundary; c++) {

  39.      if (c != 0 && w + c >= 0 && w + c < line.size()) {

  40.            model.update(ngrams, line[w + c], lr);

  41.      }

  42.    }

  43.  }

  44. }

当然fasttext.h还提供了预测的代码,相对训练来说就很简单了,主要就是调用了model的predict函数,会在model中介绍。

3.4 主要类 dictionary.h

dictionary类中词的保存格式如下,还保存了类型和char gram。

  1. struct entry {

  2.  std::string word;  // 词

  3.  int64_t count;  // 出现的次数

  4.  entry_type type; // 类型,是词还是类别标签

  5.  std::vector<int32_t> subwords;  //char gram

  6. };

整个dictionary类的主要函数功能说明

图片

  1. int32_t Dictionary::find(const std::string& w, uint32_t h) const {

  2.  int32_t word2intsize = word2int_.size();

  3.  int32_t id = h % word2intsize;

  4.  // 根据word 和 hash值 查找其id

  5.  while (word2int_[id] != -1 && words_[word2int_[id]].word != w) {

  6.    id = (id + 1) % word2intsize;

  7.  }

  8.  return id;

  9. }

  10. void Dictionary::add(const std::string& w) {

  11.  int32_t h = find(w);

  12.  ntokens_++;

  13.  // 将词添加到words_中

  14.  if (word2int_[h] == -1) {

  15.    entry e;

  16.    e.word = w;

  17.    e.count = 1;

  18.    e.type = getType(w);

  19.    words_.push_back(e);

  20.    word2int_[h] = size_++;

  21.  } else {

  22.    words_[word2int_[h]].count++;

  23.  }

  24. }

  25. void Dictionary::getSubwords(const std::string& word,

  26.                           std::vector<int32_t>& ngrams,

  27.                           std::vector<std::string>& substrings) const {

  28.  int32_t i = getId(word);

  29.  ngrams.clear();

  30.  substrings.clear();

  31.  if (i >= 0) {

  32.    ngrams.push_back(i);

  33.    substrings.push_back(words_[i].word);

  34.  }

  35.  if (word != EOS) {

  36.    computeSubwords(BOW + word + EOW, ngrams, &substrings);

  37.  }

  38. }

  39. uint32_t Dictionary::hash(const std::string& str) const {

  40.  uint32_t h = 2166136261;

  41.  for (size_t i = 0; i < str.size(); i++) {

  42.    h = h ^ uint32_t(int8_t(str[i]));

  43.    h = h * 16777619;

  44.  }

  45.  return h;

  46. }

  47. void Dictionary::computeSubwords(const std::string& word,

  48.                               std::vector<int32_t>& ngrams,

  49.                               std::vector<std::string>* substrings) const {

  50.  for (size_t i = 0; i < word.size(); i++) {

  51.    std::string ngram;

  52.    if ((word[i] & 0xC0) == 0x80) continue;

  53.    for (size_t j = i, n = 1; j < word.size() && n <= args_->maxn; n++) {

  54.      ngram.push_back(word[j++]);

  55.      while (j < word.size() && (word[j] & 0xC0) == 0x80) {

  56.        ngram.push_back(word[j++]);

  57.      }

  58.      if (n >= args_->minn && !(n == 1 && (i == 0 || j == word.size()))) {

  59.        int32_t h = hash(ngram) % args_->bucket;

  60.        pushHash(ngrams, h);

  61.        if (substrings) {

  62.          substrings->push_back(ngram);

  63.        }

  64.      }

  65.    }

  66.  }

  67. }

  68. void Dictionary::threshold(int64_t t, int64_t tl) {

  69.  sort(words_.begin(), words_.end(), [](const entry& e1, const entry& e2) {

  70.      if (e1.type != e2.type) return e1.type < e2.type;

  71.      return e1.count > e2.count;

  72.    });

  73.  words_.erase(remove_if(words_.begin(), words_.end(), [&](const entry& e) {

  74.        return (e.type == entry_type::word && e.count < t) ||

  75.               (e.type == entry_type::label && e.count < tl);

  76.      }), words_.end());

  77.  words_.shrink_to_fit();

  78.  size_ = 0;

  79.  nwords_ = 0;

  80.  nlabels_ = 0;

  81.  std::fill(word2int_.begin(), word2int_.end(), -1);

  82.  for (auto it = words_.begin(); it != words_.end(); ++it) {

  83.    int32_t h = find(it->word);

  84.    word2int_[h] = size_++;

  85.    if (it->type == entry_type::word) nwords_++;

  86.    if (it->type == entry_type::label) nlabels_++;

  87.  }

  88. }

  89. void Dictionary::addWordNgrams(std::vector<int32_t>& line,

  90.                               const std::vector<int32_t>& hashes,

  91.                               int32_t n) const {

  92.  for (int32_t i = 0; i < hashes.size(); i++) {

  93.    uint64_t h = hashes[i];

  94.    for (int32_t j = i + 1; j < hashes.size() && j < i + n; j++) {

  95.      h = h * 116049371 + hashes[j];

  96.      pushHash(line, h % args_->bucket);

  97.    }

  98.  }

  99. }

  100. void Dictionary::addSubwords(std::vector<int32_t>& line,

  101.                             const std::string& token,

  102.                             int32_t wid) const {

  103.  if (wid < 0) { // out of vocab

  104.    if (token != EOS) {

  105.      computeSubwords(BOW + token + EOW, line);

  106.    }

  107.  } else {

  108.    if (args_->maxn <= 0) { // in vocab w/o subwords

  109.      line.push_back(wid);

  110.    } else { // in vocab w/ subwords

  111.      const std::vector<int32_t>& ngrams = getSubwords(wid);

  112.      line.insert(line.end(), ngrams.cbegin(), ngrams.cend());

  113.    }

  114.  }

  115. }

3.5 核心类 model.h

盗图一张,很明白清楚的指出了model类做了些什么事情,参数和源码参数保持一致。

图片

所有核心算法都集中到了model类,里面提供了各种实现,包括LR,Huffman、BP等,还是很有意思的。同时这个模块把negativeSampling、hierarchicalSoftmax都抽象成LR。

图片

首先,让我们看下binaryLogistic这个函数,损失函数为常见的交叉熵损失函数。 如果是正类,则为f(x)=-log(score),如果为负类,则为:f(x)=-log(1-score) 现在看下其导数是什么,为了保持和代码一致,都使用源码的符号,推导如下:我们令x=wo->dotRow(hidden, target),我们都知道sigmoid函数的偏导为socre*(1-socre),对于正类而言:

图片

我们的目标是使得损失函数最小,所以是梯度下降,再结合学习率,因此hidden_的梯度更新如下:

图片


  1. real Model::binaryLogistic(int32_t target, bool label, real lr) {

  2.  real score = sigmoid(wo_->dotRow(hidden_, target));

  3.  real alpha = lr * (real(label) - score);

  4.  // 参考上文公式推导

  5.  grad_.addRow(*wo_, target, alpha);

  6.  wo_->addRow(hidden_, target, alpha);

  7.  if (label) {

  8.    return -log(score);

  9.  } else {

  10.    return -log(1.0 - score);

  11.  }

  12. }

具体negativeSampling、hierarchicalSoftmax、Softmax的实现

  1. real Model::negativeSampling(int32_t target, real lr) {

  2.  real loss = 0.0;

  3.  grad_.zero();

  4.  for (int32_t n = 0; n <= args_->neg; n++) {

  5.    // 负采样,一个正样本,n个负样本

  6.    if (n == 0) {

  7.      loss += binaryLogistic(target, true, lr);

  8.    } else {

  9.      loss += binaryLogistic(getNegative(target), false, lr);

  10.    }

  11.  }

  12.  return loss;

  13. }

  14. real Model::hierarchicalSoftmax(int32_t target, real lr) {

  15.  real loss = 0.0;

  16.  grad_.zero();

  17.  const std::vector<bool>& binaryCode = codes[target];

  18.  const std::vector<int32_t>& pathToRoot = paths[target];

  19.  // 根据目标点,找到其路径,和对应的二分类的标签

  20.  for (int32_t i = 0; i < pathToRoot.size(); i++) {

  21.    loss += binaryLogistic(pathToRoot[i], binaryCode[i], lr);

  22.  }

  23.  return loss;

  24. }

  25. real Model::softmax(int32_t target, real lr) {

  26.  grad_.zero();

  27.  // 计算和所有的label之间的值,如果为目标值,则对应为1,否则为0

  28.  computeOutputSoftmax();

  29.  for (int32_t i = 0; i < osz_; i++) {

  30.    real label = (i == target) ? 1.0 : 0.0;

  31.    real alpha = lr * (label - output_[i]);

  32.    grad_.addRow(*wo_, i, alpha);

  33.    wo_->addRow(hidden_, i, alpha);

  34.  }

  35.  return -log(output_[target]);

  36. }

看一下隐藏层的计算,需要注意的是:求和平均

  1. void Model::computeHidden(const std::vector<int32_t>& input, Vector& hidden)

  2.    const {

  3.  assert(hidden.size() == hsz_);

  4.  hidden.zero();

  5.  for (auto it = input.cbegin(); it != input.cend(); ++it) {

  6.    if (quant_) {

  7.      hidden.addRow(*qwi_, *it);

  8.    } else {

  9.      hidden.addRow(*wi_, *it);

  10.    }

  11.  }

  12.  // 求和平均

  13.  hidden.mul(1.0 / input.size());

  14. }

预测的代码,只有当用于分类时,其才有用,基本操作,用堆保留最大的几个值。如果为最后一层为hierarchicalSoftmax,这需要遍历整个Huffman树,找其topK,对应源码dfs函数,实现了深度优先搜索。如果为Softmax,直接计算即可,找其topK,对应源码findKBest。

  1. void Model::predict(

  2.    const std::vector<int32_t>& input,

  3.    int32_t k,

  4.    real threshold,

  5.    std::vector<std::pair<real, int32_t>>& heap,

  6.    Vector& hidden,

  7.    Vector& output) const {

  8.  // 判断k值是否符合要求

  9.  if (k == Model::kUnlimitedPredictions) {

  10.    k = osz_;

  11.  } else if (k <= 0) {

  12.    throw std::invalid_argument("k needs to be 1 or higher!");

  13.  }

  14.  // 判断当前模型是否是分类模型

  15.  if (args_->model != model_name::sup) {

  16.    throw std::invalid_argument("Model needs to be supervised for prediction!");

  17.  }

  18.  heap.reserve(k + 1);

  19.  computeHidden(input, hidden);

  20.  // 根据最后一层不同,分别选择不同的遍历方式,找topk

  21.  if (args_->loss == loss_name::hs) {

  22.    dfs(k, threshold, 2 * osz_ - 2, 0.0, heap, hidden);

  23.  } else {

  24.    findKBest(k, threshold, heap, hidden, output);

  25.  }

  26.  std::sort_heap(heap.begin(), heap.end(), comparePairs);

  27. }

  28. void Model::findKBest(

  29.    int32_t k,

  30.    real threshold,

  31.    std::vector<std::pair<real, int32_t>>& heap,

  32.    Vector& hidden,

  33.    Vector& output) const {

  34.  computeOutputSoftmax(hidden, output);

  35.  for (int32_t i = 0; i < osz_; i++) {

  36.    if (output[i] < threshold) {

  37.      continue;

  38.    }

  39.    if (heap.size() == k && std_log(output[i]) < heap.front().first) {

  40.      continue;

  41.    }

  42.    // 把元素放到末尾

  43.    heap.push_back(std::make_pair(std_log(output[i]), i));

  44.    // 调整最小堆

  45.    std::push_heap(heap.begin(), heap.end(), comparePairs);

  46.    if (heap.size() > k) {

  47.      // 堆顶元素pop出堆中

  48.      std::pop_heap(heap.begin(), heap.end(), comparePairs);

  49.      // 删除最后一个元素

  50.      heap.pop_back();

  51.    }

  52.  }

  53. }

  54. void Model::dfs(

  55.    int32_t k,

  56.    real threshold,

  57.    int32_t node,

  58.    real score,

  59.    std::vector<std::pair<real, int32_t>>& heap,

  60.    Vector& hidden) const {

  61.  // 由于取的是对数,所以随着深度的加深,socre是越来越小,

  62.  // 如果中间已经小于阈值,其后续节点都不需要计算

  63.  if (score < std_log(threshold)) {

  64.    return;

  65.  }

  66.  // 同理

  67.  if (heap.size() == k && score < heap.front().first) {

  68.    return;

  69.  }

  70.  // 如果是叶子节点,计算该label的概率,加入到堆中

  71.  if (tree[node].left == -1 && tree[node].right == -1) {

  72.    heap.push_back(std::make_pair(score, node));

  73.    std::push_heap(heap.begin(), heap.end(), comparePairs);

  74.    if (heap.size() > k) {

  75.      std::pop_heap(heap.begin(), heap.end(), comparePairs);

  76.      heap.pop_back();

  77.    }

  78.    return;

  79.  }

  80.  real f;

  81.  if (quant_ && args_->qout) {

  82.    f = qwo_->dotRow(hidden, node - osz_);

  83.  } else {

  84.    f = wo_->dotRow(hidden, node - osz_);

  85.  }

  86.  f = 1. / (1 + std::exp(-f));

  87.  // 根据当前的概率,分别遍历左节点和右节点,属于先根遍历

  88.  dfs(k, threshold, tree[node].left, score + std_log(1.0 - f), heap, hidden);

  89.  dfs(k, threshold, tree[node].right, score + std_log(f), heap, hidden);

  90. }

参数的更新,需要注意一个地方,见注释

  1. void Model::update(const std::vector<int32_t>& input, int32_t target, real lr) {

  2.  assert(target >= 0);

  3.  assert(target < osz_);

  4.  if (input.size() == 0) {

  5.    return;

  6.  }

  7.  computeHidden(input, hidden_);

  8.  if (args_->loss == loss_name::ns) {

  9.    loss_ += negativeSampling(target, lr);

  10.  } else if (args_->loss == loss_name::hs) {

  11.    loss_ += hierarchicalSoftmax(target, lr);

  12.  } else {

  13.    loss_ += softmax(target, lr);

  14.  }

  15.  nexamples_ += 1;

  16.  // 由于是加权平均,所有最后的梯度也需要平均一下

  17.  if (args_->model == model_name::sup) {

  18.    grad_.mul(1.0 / input.size());

  19.  }

  20. }

最后一个,Huffman树的构建,比较有意思,原理参考下面图示,我们知道Huffman构建的过程,选择两个最小的没有使用过的数,构建一个非叶子节点。如果数据是倒序排序会出现什么情况呢。

图片

没错,正如你所见,只要从中间往两边遍历,因为两边的数据都是有序的,选择最小的两个数据,构建中间节点,源码就是这种思路,一看就懂,有木有。

  1. void Model::buildTree(const std::vector<int64_t>& counts) {

  2.  tree.resize(2 * osz_ - 1);

  3.  for (int32_t i = 0; i < 2 * osz_ - 1; i++) {

  4.    tree[i].parent = -1;

  5.    tree[i].left = -1;

  6.    tree[i].right = -1;

  7.    tree[i].count = 1e15;

  8.    tree[i].binary = false;

  9.  }

  10.  for (int32_t i = 0; i < osz_; i++) {

  11.    tree[i].count = counts[i];

  12.  }

  13.  int32_t leaf = osz_ - 1;

  14.  int32_t node = osz_;

  15.  // 中间节点往两边选择最小的点

  16.  for (int32_t i = osz_; i < 2 * osz_ - 1; i++) {

  17.    int32_t mini[2];

  18.    for (int32_t j = 0; j < 2; j++) {

  19.      if (leaf >= 0 && tree[leaf].count < tree[node].count) {

  20.        mini[j] = leaf--;

  21.      } else {

  22.        mini[j] = node++;

  23.      }

  24.    }

  25.    tree[i].left = mini[0];

  26.    tree[i].right = mini[1];

  27.    tree[i].count = tree[mini[0]].count + tree[mini[1]].count;

  28.    tree[mini[0]].parent = i;

  29.    tree[mini[1]].parent = i;

  30.    tree[mini[1]].binary = true;

  31.  }

  32.  for (int32_t i = 0; i < osz_; i++) {

  33.    std::vector<int32_t> path;

  34.    std::vector<bool> code;

  35.    int32_t j = i;

  36.    while (tree[j].parent != -1) {

  37.      path.push_back(tree[j].parent - osz_);

  38.      code.push_back(tree[j].binary);

  39.      j = tree[j].parent;

  40.    }

  41.    paths.push_back(path);

  42.    codes.push_back(code);

  43.  }

  44. }

4 源码修改

4.1 带权重fastText

结构图:

图片

反向传播:

图片



  1. class Attention {

  2.  protected:

  3.    std::vector<real> weight_softmax_;  // 权重softmax值

  4.    std::vector<real> weight_grad_;     // 权重梯度更新

  5.    int64_t size_;

  6.    const int32_t dim_;

  7.  public:

  8.    Attention(int32_t dim): dim_(dim) {}

  9.    explicit Attention(int64_t n, int32_t dim):

  10.                weight_softmax_(n),

  11.                weight_grad_(n),

  12.                size_(n),

  13.                dim_(dim) {}

  14.    // Attention(const Softmax&) = default;

  15.    // Attention& operator=(const Attention&) = delete;

  16.    void zero() {

  17.        std::fill(weight_softmax_.begin(), weight_softmax_.end(), 0.0);

  18.        std::fill(weight_grad_.begin(), weight_grad_.end(), 0.0);

  19.    }

  20.    std::vector<real>& getWeightSoftmax() {

  21.    return weight_softmax_;

  22.    }

  23.    std::vector<real>& getWeightGrad() {

  24.        return weight_grad_;

  25.    }

  26.    void resize(int64_t n) {

  27.        size_ = n;

  28.        weight_softmax_.resize(n);

  29.        weight_grad_.resize(n);

  30.    }

  31.    /*

  32.    * 权重softmax

  33.    */

  34.    void calWeightSoftmax(const Matrix& A, const std::vector<int32_t>& input) {

  35.        if (input.size() > weight_softmax_.size()) {

  36.            weight_softmax_.resize(input.size());

  37.        }

  38.        real sum = 0;

  39.        real max = 0;

  40.        if (input.size() > 0)

  41.            max = A.getWeight(input[0]);

  42.        for ( int64_t i = 0; i < input.size(); ++i ) {

  43.            if ( max < A.getWeight(input[i]))

  44.                max = A.getWeight(input[i]);

  45.        }

  46.        for ( int64_t i = 0; i < input.size(); ++i ) {

  47.             weight_softmax_[i] = std::exp(A.getWeight(input[i]) - max);

  48.             sum += weight_softmax_[i];

  49.        }

  50.        for ( int64_t i = 0; i < input.size(); i++ ) {

  51.             weight_softmax_[i] /= sum;

  52.        }

  53.    }

  54.    /*

  55.    * 计算权重的更新

  56.    */

  57.    void calWeightGrad(const Matrix& A, const std::vector<int32_t>& input,

  58.                        const Vector& grad, const Vector& hidden) {

  59.        if (input.size() > weight_grad_.size()) {

  60.            weight_grad_.resize(input.size());

  61.        }

  62.        // std::fill(weight_grad_.begin(), weight_grad_.end(), 0.0);

  63.        for (int32_t j = 0; j < input.size(); ++j) {

  64.            real sum = 0;

  65.            for (int i = 0; i < dim_; ++i) {

  66.                sum += grad[i]*(A.at(input[j], i) - hidden[i]);

  67.            }

  68.            weight_grad_[j] = weight_softmax_[j]*sum;

  69.        }

  70.        /*

  71.        for (int32_t i = 0; i < input.size(); i++) {

  72.            weight_grad_[i] /= A.size(1);

  73.        }

  74.        */

  75.    }

  76.    void computeHidden(const Matrix& A, const std::vector<int32_t>& input,

  77.                            Vector& hidden) {

  78.        // 计算 权重softmax

  79.        calWeightSoftmax(A, input);

  80.        auto it = input.cbegin();

  81.        auto it_s = weight_softmax_.cbegin();

  82.        for (; it != input.cend() && it_s != weight_softmax_.cend();

  83.                ++it, ++it_s) {

  84.                hidden.addRow(A, *it, *it_s);

  85.        }

  86.    // hidden.mul(1.0 / input.size());

  87.    }

  88. };

4.2 self-attetnion fastText

图片

图片

图片

  1. class Attention {

  2.  protected:

  3.    // 权重参数

  4.    std::vector<real> weight_softmax_;  // 权重softmax值

  5.    // std::vector<real> weight_softmax_grad_mat_;  //  hi 对 wj 的梯度

  6.    std::vector<real> weight_grad_;     // 权重梯度更新

  7.    int64_t size_;

  8.    const int32_t dim_;

  9.    // x self-attention 参数

  10.    std::vector<real> x_softmax_; // x softmax 权重

  11.    std::vector<real> y_;  // yi=sum(wij*xj) x的self-attention

  12.    std::vector<real> hi_xjk_grad_;  //  hi 对 xj,k 的梯度  其中j是固定的

  13.    std::vector<real> xj_grad_;  // xj的梯度

  14.    std::vector<real> max_x_softmax_;

  15.  public:

  16.    Attention(int32_t dim): dim_(dim) {}

  17.    explicit Attention(int64_t n, int32_t dim):

  18.                weight_softmax_(n),

  19.                weight_grad_(n),

  20.                size_(n),

  21.                dim_(dim),

  22.                x_softmax_(n*n),

  23.                y_(n*dim),

  24.                hi_xjk_grad_(dim*dim),

  25.                xj_grad_(dim){}

  26.    // Attention(const Softmax&) = default;

  27.    // Attention& operator=(const Attention&) = delete;

  28.    void zero() {

  29.        std::fill(weight_softmax_.begin(), weight_softmax_.end(), 0.0);

  30.        std::fill(weight_grad_.begin(), weight_grad_.end(), 0.0);

  31.    }

  32.    std::vector<real>& getWeightSoftmax() {

  33.    return weight_softmax_;

  34.    }

  35.    std::vector<real>& getWeightGrad() {

  36.        return weight_grad_;

  37.    }

  38.    void resize(int64_t n) {

  39.        if (n > size_) {

  40.            size_ = n;

  41.            weight_softmax_.resize(n);

  42.            weight_grad_.resize(n);

  43.            x_softmax_.resize(n*n);

  44.            y_.resize(n*dim_);

  45.        }

  46.    }

  47.    /*

  48.    * 权重softmax 归一化

  49.    */

  50.    void calWeightSoftmax(const Matrix& A,

  51.                          const std::vector<int32_t>& input) {

  52.        if (input.size() > weight_softmax_.size()) {

  53.            weight_softmax_.resize(input.size());

  54.        }

  55.        real sum = 0;

  56.        real max = 0;

  57.        if (input.size() > 0)

  58.            max = A.getWeight(input[0]);

  59.        for ( int64_t i = 0; i < input.size(); ++i ) {

  60.            if ( max < A.getWeight(input[i]))

  61.                max = A.getWeight(input[i]);

  62.        }

  63.        for ( int64_t i = 0; i < input.size(); ++i ) {

  64.             weight_softmax_[i] = std::exp(A.getWeight(input[i]) - max);

  65.             sum += weight_softmax_[i];

  66.        }

  67.        for ( int64_t i = 0; i < input.size(); i++ ) {

  68.             weight_softmax_[i] /= sum;

  69.        }

  70.    }

  71.    /*

  72.    * 计算权重的更新

  73.    */

  74.    void calWeightGrad(const Matrix& A,

  75.                       const std::vector<int32_t>& input,

  76.                       const Vector& hidden,

  77.                       const Vector& grad) {

  78.        if (input.size() > weight_grad_.size()) {

  79.            weight_grad_.resize(input.size());

  80.        }

  81.        // std::fill(weight_grad_.begin(), weight_grad_.end(), 0.0);

  82.        for (int32_t j = 0; j < input.size(); ++j) {

  83.            real tmp = 0;

  84.            for (int i = 0; i < dim_; ++i) {

  85.                tmp += grad[i]*(y_[j*dim_ + i]-hidden[i]);

  86.            }

  87.            weight_grad_[j] = weight_softmax_[j]*tmp;

  88.        }

  89.    }

  90.    void computeHidden(const Matrix& A, const std::vector<int32_t>& input,

  91.                            Vector& hidden) {

  92.        // 计算 权重softmax

  93.        calWeightSoftmax(A, input);

  94.        // 计算y值

  95.        calY(A, input);

  96.        hidden.zero();

  97.        for(int32_t i = 0; i < dim_; ++i) {

  98.            for( int32_t j=0; j<input.size(); ++j) {

  99.                hidden[i] += weight_softmax_[j]*y_[j*dim_+i];

  100.            }

  101.        }

  102.    // hidden.mul(1.0 / input.size());

  103.    }

  104.    // 计算self-attention 的权重  x_softmax_[i,j] = e^(xi*xj)/sum(e^xi*xk)

  105.    void calXSoftmax(const Matrix& A, const std::vector<int32_t>& input) {

  106.        if (input.size()*input.size() > x_softmax_.size())

  107.            x_softmax_.resize(input.size()*input.size());

  108.        std::fill(x_softmax_.begin(), x_softmax_.end(), 0.0);

  109.        if ( input.size() > max_x_softmax_.size())

  110.            max_x_softmax_.resize(input.size());

  111.        for(int32_t i = 0; i < input.size(); ++i) {

  112.            for(int32_t j = 0; j < input.size(); ++j) {

  113.                for(int32_t d = 0; d < dim_; ++d){

  114.                    x_softmax_[i*input.size() + j] += A.at(input[i], d)*A.at(input[j], d);

  115.                }

  116.                if ( j == 0)

  117.                    max_x_softmax_[i] = x_softmax_[i*input.size() + j];

  118.                else if (max_x_softmax_[i] < x_softmax_[i*input.size() + j])

  119.                    max_x_softmax_[i] = x_softmax_[i*input.size() + j];

  120.            }

  121.        }

  122.        for(int32_t i = 0; i < input.size(); ++i) {

  123.            real sum = 0;

  124.            for(int32_t j = 0; j < input.size(); ++j) {

  125.                x_softmax_[i*input.size() + j]=

  126.                        std::exp(x_softmax_[i*input.size() + j]

  127.                                  -max_x_softmax_[i]);

  128.                sum += x_softmax_[i*input.size() + j];

  129.            }

  130.            for(int32_t j = 0; j < input.size(); ++j) {

  131.                x_softmax_[i*input.size() + j] /= sum;

  132.            }

  133.        }

  134.    }

  135.    // 计算Y值, y[i] = sum(w[i,j]*xj)

  136.    void calY(const Matrix& A, const std::vector<int32_t>& input) {

  137.        if ( input.size()*dim_ > y_.size())

  138.            y_.resize(input.size()*dim_);

  139.        calXSoftmax(A, input);

  140.        std::fill(y_.begin(), y_.end(), 0.0);

  141.        for(int32_t i = 0; i < input.size(); ++i){

  142.            for(int32_t d = 0; d < dim_; ++d){

  143.                for(int32_t j = 0; j < input.size(); ++j){

  144.                    y_[i*dim_ + d] += x_softmax_[i*input.size() + j]*A.at(input[j], d);

  145.                }

  146.            }

  147.        }

  148.    }

  149.    // 反向传播  hi_xjk_grad_ hi 对 xjk 的梯度 其中j是固定的

  150.    void caHiXjkGrad(const Matrix& A,

  151.                     const std::vector<int32_t>& input, int32_t j) {

  152.        if (dim_*dim_ > hi_xjk_grad_.size())

  153.            hi_xjk_grad_.resize(dim_*dim_);

  154.        std::fill(hi_xjk_grad_.begin(), hi_xjk_grad_.end(), 0.0);

  155.        for (int32_t i = 0; i < dim_; ++i) {

  156.            for (int32_t k = 0; k < dim_; ++k) {

  157.                for (int32_t m = 0; m < input.size(); ++m) {

  158.                    if ( i == k) {

  159.                        hi_xjk_grad_[i*dim_ + k] +=

  160.                      weight_softmax_[m]*x_softmax_[m*input.size() + j];

  161.                    }

  162.                    real sum = 0;

  163.                    sum += A.at(input[m], k)*x_softmax_[m*input.size()

  164.                          + j]*(A.at(input[j], i) - y_[m*dim_ + i]);

  165.                    if (m == j) {

  166.                        sum -= y_[m*dim_ + k]*y_[m*dim_ + i];

  167.                        for (int32_t l = 0; l < input.size(); ++l) {

  168.                            sum += A.at(input[l], k)*x_softmax_[m*input.size()

  169.                                                + l]*A.at(input[l], i);

  170.                        }

  171.                    }

  172.                    hi_xjk_grad_[i*dim_ + k] += weight_softmax_[m]*sum;

  173.                }

  174.            }

  175.        }

  176.    }

  177.    // 反向传播 计算 损失函数对xj的梯度

  178.    void calXGrad(const Matrix& A, const std::vector<int32_t>& input,

  179.                        const Vector& grad, Vector& xgrad, int32_t id) {

  180.        caHiXjkGrad(A, input, id);

  181.        xgrad.zero();

  182.        for (int32_t k = 0; k < dim_; ++k) {

  183.            for (int32_t i = 0; i < dim_; ++i) {

  184.                xgrad[k] += grad[i]*hi_xjk_grad_[i*dim_ + k];

  185.            }

  186.        }

  187.    }

  188. };

5 总结

fastText是一个简单高效的文本分类工具,近两年应用广泛,用简单模型的训练时间得到比肩深度学习的效果。面对这么优秀的开源工具,本文为你详细剖析了其C++源码实现,并在源码基础上做了两个新的尝试:

  1. 带权重fasttext

  2. self-attention fastText

这两个尝试前者并不会显著增加训练时间,但是后者会。当然这只是我们一时技痒,加了attention的fastText,就像是给AK47绑上了重机枪的架子,失去了其本来意义。当然,我们希望本文的源码分析能给同行们一些有用的启示,欢迎大家继续互相交流讨论NLP技术。

若有意加入我们团队一起搞事情,把你亮闪闪的简历砸向chenkaijiang001@ke.com 。

继续滑动看下一个
AI老炮观察站
向上滑动看下一个