無所不能的Embedding2 - 詞向量三巨頭之FastText詳解


Fasttext是FaceBook開源的文本分類和詞向量訓練庫。最初看其他教程看的我十分迷惑,咋的一會ngram是字符一會ngram又變成了單詞,最后發現其實是兩個模型,一個是文本分類模型[Ref2],表現不是最好的但勝在結構簡單高效,另一個用於詞向量訓練[Ref1],創新在於把單詞分解成字符結構,可以infer訓練集外的單詞。這里拿quora的詞分類數據集嘗試了下Fasttext在文本分類的效果, 代碼詳見 https://github.com/DSXiangLi/Embedding

Fasttext 分類模型

Fasttext分類模型結構很直觀是一個淺層的神經網絡。先對文本的每個詞做embedding得到\(w_i\), 然后所有詞的embedding做平均得到文本向量\(w_{doc}\),然后經過1層神經網絡對label進行預測

\[\begin{align} w_{doc} &= \frac{1}{n}\sum_{i=1}^n w_{i} \\ p &= \sigma {(\beta \cdot w_{doc})} \\ \end{align} \]

只說到這里,其實會發現和之前word2vec的CBOW基本是一樣的,區別在於CBOW預測的是center word, 而Fasttext預測的是label,例如新聞分類,情感分類等,同時CBOW只考慮window_size內的單詞,而Fasttext會使用變長文本內的所有單詞。

看到Fasttext對全文本的詞向量求平均, 第一反應是會丟失很多信息,對於短文本可能還好,但對於長文本效果應該不咋地。畢竟不能考慮到詞序信息,是詞袋模型的通病。Fasttext對此的解決辦法是加入n-gram特征。這里的n-gram是單詞級別的n-gram, 把相連的n個單詞當作1個單詞來做embedding,這樣就可以考慮到局部的詞序信息。當然副作用就是需要學習的embedding規模會大幅上升,只是2-gram就會比word要多得多。

Fasttext對此的解決方法是使用hashing把n-gram映射到bucket, 相同bucket的n-gram共享一個詞向量。

在Quora的文本數據集上我自己實現了一版fasttext分類模型, LeaderBoard的F1在0.71左右,因為要用Kernel提交太麻煩只在訓練集上跑了下在0.68左右,所以fasttext的分類模型確實是勝在一個快字。

Fasttext 詞向量模型

Fasttext另一個模型就是詞向量模型,是在Skip-gram的基礎上,創新加入了subword信息。也就是把單詞分解成字符串,模型學習的是字符串embedding
,單詞的embedding由字符embedding求平均得到,這也是Fasttext詞向量可以infer樣本外單詞的原因。

關於模型和訓練細節,和前一章講到的word2vec是一樣的,感興趣的可以來這里摟一眼 無所不能的Embedding 1 - Word2vec模型詳解&代碼實現

這里我們只細討論下和subword相關的源代碼。這里n-gram不再指單詞而是字符,模型參數maxn,minn會設定n-gram的上界和下界。當設定minn = 2, maxn=3的時候,‘where’單詞對應的subwords是<'wh','her','ere',re'>,還有<'where'>本身。

當時paper看到這里第一個反應是英文可以這么搞,因為英文可以分解成字符,且一些前綴后綴是有特殊含義的,中文咋整,拆偏旁部首么?!來看代碼答疑解惑

void Dictionary::initNgrams() {
  for (size_t i = 0; i < size_; i++) {
    std::string word = BOW + words_[i].word + EOW; // 詞前后加入<>用來區分單詞和字符
    words_[i].subwords.clear();
    words_[i].subwords.push_back(i); //先把單詞本身加入subword
    if (words_[i].word != EOS) {
      computeSubwords(word, words_[i].subwords);
    }
  }
}

void Dictionary::computeSubwords(
    const std::string& word,
    std::vector<int32_t>& ngrams,
    std::vector<std::string>* substrings) const {
  for (size_t i = 0; i < word.size(); i++) {
    std::string ngram;
    if ((word[i] & 0xC0) == 0x80) {
      continue; // 遇到10開頭字節跳過,保證中文從第一個字節開始讀
    }
    for (size_t j = i, n = 1; j < word.size() && n <= args_->maxn; n++) {
      ngram.push_back(word[j++]);
      while (j < word.size() && (word[j] & 0xC0) == 0x80) {
        ngram.push_back(word[j++]); // 如果是中文,讀取該字符的所有字節
      }
      if (n >= args_->minn && !(n == 1 && (i == 0 || j == word.size()))) {// subword滿足長度,得到hash值加入到ngrams里面
        int32_t h = hash(ngram) % args_->bucket;
        pushHash(ngrams, h);
        if (substrings) {
          substrings->push_back(ngram);
        }
      }
    }
  }
}

核心在computeSubwords部分, 乍一看十分迷幻的是(word[i] & 0xC0) == 0x80) 。這里0xC0二進制是11 00 00 00,和它做位運算是取字節的前兩個bit,0x80二進制是10 00 00 00,前兩位是10,其實也就是判斷word[i]前兩個bit是不是10,。。。10又是啥?

字符編碼筆記:ASCII,Unicode 和 UTF-8 這里附上阮神的博客,廣告費請結一下~

簡單來說就是Fasttext要求輸入為UTF-8編碼,這里需要用到UTF-8的兩條編碼規則:

  • 單字節的符號,字節的第一位是0后面7位是unicode,英文的ASCII和utf-8是一樣滴
  • n字節的符號,第一個字節的前n位是1,后面字節的前兩位一律是10,是的此10就是彼10。

因為所有的英文都是單字節,而中文在utf-8中通常占3個字節,也就是只有讀到中文字符中間字節的時候(word[i] & 0xC0) == 0x80) 判斷成立。所以判斷本身是為了完整的讀取一個字符的全部字節,也就是說中文的subword最小單位只能到單個漢字,而不會有更小的粒度了。而個人感覺漢字粒度並不能像英文單詞的構詞一樣帶來十分有效的信息,所以Fasttext的這一創新感覺對中文並不會有太多增益。

不過說起拆偏旁部首,螞蟻金服人工智能部在2018年還真發表過一個引入漢子偏旁部首信息的詞向量模型cw2vec理論及其實現,感興趣的可以去看看喲~


REF

  1. P. Bojanowski, E. Grave, A. Joulin, T. Mikolov, Enriching Word Vectors with Subword Information
  2. A. Joulin, E. Grave, P. Bojanowski, T. Mikolov, Bag of Tricks for Efficient Text Classification
  3. A. Joulin, E. Grave, P. Bojanowski, M. Douze, H. Jégou, T. Mikolov, FastText.zip: Compressing text classification models
  4. https://zhuanlan.zhihu.com/p/64960839

我的博客即將同步至 OSCHINA 社區,這是我的 OSCHINA ID:OSC_jHtwZy,邀請大家一同入駐:https://www.oschina.net/sharing-plan/apply


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM