FastText 分析與實踐


一. 前言

自然語言處理(NLP)是機器學習,人工智能中的一個重要領域。文本表達是 NLP中的基礎技術,文本分類則是 NLP 的重要應用。在 2016 年, Facebook Research 開源了名為 fasttext[1] 的文本表達和分類的計算庫。 fasttext 是基於文章 [2], [3], [4] 所提出算法的實現,針對變形詞匯表達,線性分類優化提供了優秀的解決方案。 本文試圖梳理 FastText 在文本表達和文本分類方面的工作,並進行實踐。

二. 詞嵌入

1. 背景介紹

詞表達是 NLP 處理中的關鍵技術。常見的方式如 one-hot,使用了高維稀疏向量表示詞匯,這樣的特征可以反映詞出現的頻率,卻不能反映詞之間的關系。與此對應,詞嵌入技術將詞匯的上下文關系嵌入一個低維空間,其中比較有代表性的方法如 word2vec [5], GloVe[6]。本段落將分析 word2vec 的原理。

2. word2vec

word2vec 將詞的上下文關系嵌入到低維空間。更具體而言,word2vec 將詞的上下文關系轉換為分類關系,並以此同時訓練詞嵌入向量和 logistic regression 分類器。

1. 邏輯回歸

logistic regression 是經典的線性分類模型。廣義上而言,線性模型由三個部分組成, 1. 輸入向量 2. 線性系數 3. 偏移(bias),而 bias 可以進一步表示成線性系數。所以,二分類的線性分類問題可以表示為輸入 $x$ 和系數 $w$ 的內積結果 $w^Tx$,結果的正負決定了數據的類別。分類器參數通過最小化損失函數 $l(y, w^Tx)$來完成,不同的損失函數定義了不同的分類模型。

下面列舉了svm, lr 的損失函數,其中 $y\in \{-1, 1\}$
logit : $\Sigma_{i=1}^N ln(1 + e^{-y_i w^Tx_i})$
svm : $\Sigma_{i=1}^N max(0, 1- y _iw^Tx_i) + \lambda||w||^2$

logistic regression 也廣泛地應用在多分類問題中,通過 softmax 函數計算數據屬於每個類別的概率完成分類。因為分類神經網絡的輸出層通常也設定為 softmax 函數,所以多分類 lr 也可以表示為淺層神經網絡。

下面我們分析 word2vec 如何將詞的上下文關系轉化為分類任務。

2. skip-gram

我們將詞的上下文定義為以詞 $w_i$ 為中心,窗口為 $k$ 前后范圍內的詞 $C_i = {w_{i-k}, w_{i-k+1}, ..., w_{i-1}, w_{i+1}, ... w_{i+k}}$。

skip-gram 將詞之間的關系變成了 $|V|$ 多分類問題,其中 $|V|$ 是詞庫大小。每個詞有兩個變量 $x_i, w_i$,前者為詞嵌入向量,后者是線性分類器的系數,在相關文章中又稱為上下文向量。

skip-gram 用中心詞匯來預測其上下文,如下圖:

 
skip-gram.png

3. cbow

Continuous bag of words(cbow) 是與 skip-gram 相對應的另一種將上下文轉化為分類任務的方式。

 
image.png

上圖顯示 cbow 用上下文詞向量的加和結果來預測其中心詞匯,其它的方式還有拼接。需要注意的是拼接的方式會導致上下文向量 $c_i$ 維度增大 $2 k$ 倍數, $k$ 為上下文窗口。

4. 針對多分類的計算優化

word2vec 將上下文關系轉化為多分類任務,進而訓練邏輯回歸模型,這里的類別數量是 $|V|$ 詞庫大小。通常的文本數據中,詞庫少則數萬,多則百萬,在訓練中直接訓練多分類邏輯回歸並不現實。

word2vec [5] 中提供了兩種針對大規模多分類問題的優化手段, negative sampling 和 hierarchical softmax。以 skip-gram 為例,中心詞對上下文的詞類是正面例子,對所有其它的詞則是負面例子。在優化中,negative sampling 只更新少量負面類,從而減輕了計算量。hierarchical softmax 將詞庫表示成前綴樹,從樹根到葉子的路徑可以表示為一系列二分類器,一次多分類計算的復雜度從 $|V|$ 降低到了樹的高度。

3. 總結

詞嵌入技術將詞的上下文關系嵌入到低維空間。word2vec 將詞的局部上下文轉化為了多分類任務,從而訓練邏輯回歸模型,並將邏輯回歸模型中的輸入部分作為詞嵌入輸出。

三. FastText 子詞嵌入

1. 背景介紹

詞嵌入技術在 NLP 領域發揮了越來越大的作用,相較於 one-hot 特征,詞嵌入具有維度低,體現詞間關系等特點。但傳統的詞嵌入技術忽略了詞的微變形特性,如英語中動詞的第三人稱,進行時或過去時變形。針對這個問題, [2] 提出使用子詞來學習詞的表達,每個詞由內部的 n-gram 字母串組成。在中文處理中, [7] 提出了類似的算法。

2. FastText 的實現

FastText 的子詞嵌入在 word2vec 的基礎上,引入了子詞這個因素,從而使得詞的微變形關系也能映射到嵌入空間中。

1. 子詞

在 fasttext 中,每個詞被看做是 n-gram 字母串包。為了區分前后綴情況,"<", ">" 符號被加到了詞的前后端。除了詞的子串外,詞本身也被包含進了 n-gram 字母串包。以 where 為例,$n=3$ 的情況下,其子串分別為
<wh, whe, her, ere, re>,以及其本身 <where>

注意,這里的 her 與單詞 <her> 是不同的。

現在我們看看 fasttext 具體是怎么計算子串的。

void Dictionary::initNgrams() { for (size_t i = 0; i < size_; i++) { // 加入前后綴符號 <, > std::string word = BOW + words_[i].word + EOW; // 詞本身作為特殊子串加入包中 words_[i].subwords.push_back(i); // 計算並將詞子串加入包中 computeNgrams(word, words_[i].subwords); } } // 計算詞子串 void Dictionary::computeNgrams(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; for (size_t j = i, n = 1; j < word.size() && n <= args_->maxn; n++) { ngram.push_back(word[j++]); // 將 utf-8 編碼數據部分推入 ngram while (j < word.size() && (word[j] & 0xC0) == 0x80) { ngram.push_back(word[j++]); } if (n >= args_->minn && !(n == 1 && (i == 0 || j == word.size()))) { // fasttext 使用 open hash 進行 string->int 轉換 int32_t h = hash(ngram) % args_->bucket; ngrams.push_back(nwords_ + h); substrings.push_back(ngram); } } } } 

在 fasttext 中,minn 和 maxn 參數控制了子串的長度,其默認值分別為 3, 6。再以 where 為例,在默認子串參數情況下,其子串包內容為:

<wh, <whe, <wher, <where, whe, wher, where, where>, her, here, here>, ere, ere>, re> 以及自身 <where>

下面我們看 fasttext 是如何用子串來構造詞嵌入的,這里注意每個子串也有自身的嵌入表示。

void Model::computeHidden(const std::vector<int32_t>& input, Vector& hidden) const { assert(hidden.size() == hsz_); hidden.zero(); for (auto it = input.cbegin(); it != input.cend(); ++it) { if(quant_) { // product quantization 優化 // 子串加和 hidden.addRow(*qwi_, *it); } else { // 子串加和 hidden.addRow(*wi_, *it); } } // 平均 hidden.mul(1.0 / input.size()); } 

代碼非常簡明直接,每個詞是其子串嵌入表示的加和平均。

fasttext 的另一個優勢,是學習未知詞的表達。以 working 為例,如果這個詞在學習文本中沒有出現過,但文本包含了 work, 和 ing,那么 working 的詞嵌入向量也能夠合理地計算出來。

2. skip-gram

一個詞的嵌入是其子串嵌入的加和平均。當這個嵌入表示計算出來后,后續步驟與 word2vec 的 skip-gram 相同。下面是 fasttext 的 skip-gram 實現代碼:

void FastText::skipgram(Model& model, real lr, const std::vector<int32_t>& line) { std::uniform_int_distribution<> uniform(1, args_->ws); for (int32_t w = 0; w < line.size(); w++) { // 窗口大小 int32_t boundary = uniform(model.rng); const std::vector<int32_t>& ngrams = dict_->getNgrams(line[w]); for (int32_t c = -boundary; c <= boundary; c++) { if (c != 0 && w + c >= 0 && w + c < line.size()) { // 通過 ngrams 計算 line[w] 的表示,通過邏輯回歸優化 ngrams 的嵌入表示 model.update(ngrams, line[w + c], lr); } } } } 

我們來詳細看看 Model::update 函數是怎么用邏輯回歸來優化 ngrams 的嵌入表示

void Model::update(const std::vector<int32_t>& input, int32_t target, real lr) { assert(target >= 0); assert(target < osz_); if (input.size() == 0) return; // 子串加權平均得到詞嵌入表示 computeHidden(input, hidden_); // 針對分類問題的 3 種處理 // 1. negative sampling 優化 if (args_->loss == loss_name::ns) { loss_ += negativeSampling(target, lr); } // 2. hierarchical softmax 優化 else if (args_->loss == loss_name::hs) { loss_ += hierarchicalSoftmax(target, lr); } // 3. 直接計算多分類邏輯回歸 // 在文本分類模式下使用 else { loss_ += softmax(target, lr); } nexamples_ += 1; // 邏輯回歸產生了導數,在文本分類情況下導數向量需要除以子串數量 if (args_->model == model_name::sup) { grad_.mul(1.0 / input.size()); } // 將導數直接加到子串向量中 for (auto it = input.cbegin(); it != input.cend(); ++it) { wi_->addRow(grad_, *it, 1.0); } } 

在第二段,我們簡單介紹了針對大量多分類的優化策略, negative sampling 和 hierarchical sampling。這里我們看看 fasttext 是怎樣實現這些策略。

// 根據詞頻構構建采樣表 void Model::initTableNegatives(const std::vector<int64_t>& counts) { real z = 0.0; for (size_t i = 0; i < counts.size(); i++) { z += pow(counts[i], 0.5); } // 每個詞類在采樣表中數量與其詞頻開方成比例 for (size_t i = 0; i < counts.size(); i++) { real c = pow(counts[i], 0.5); for (size_t j = 0; j < c * NEGATIVE_TABLE_SIZE / z; j++) { negatives.push_back(i); } } std::shuffle(negatives.begin(), negatives.end(), rng); } // 負采樣 real Model::negativeSampling(int32_t target, real lr) { real loss = 0.0; grad_.zero(); // 負采樣 args_->neg 個類別 for (int32_t n = 0; n <= args_->neg; n++) { if (n == 0) { // 將當前詞作為正面例子對 target 類進行二分邏輯回歸訓練 loss += binaryLogistic(target, true, lr); } else { // 將當前詞作為負面例子對負采樣類進行二分邏輯回歸訓練 loss += binaryLogistic(getNegative(target), false, lr); } } return loss; } // 負采樣一個類 int32_t Model::getNegative(int32_t target) { int32_t negative; // 輪詢采樣表 do { negative = negatives[negpos]; negpos = (negpos + 1) % negatives.size(); } while (target == negative); return negative; } 

下面我們看看 hierarchical softmax 的實現

// 根據詞頻構造前綴樹,因為樹的內部節點數量為 |V| - 1 所以可以用 |V| * 2 的數組存儲樹結構 void Model::buildTree(const std::vector<int64_t>& counts) { tree.resize(2 * osz_ - 1); for (int32_t i = 0; i < 2 * osz_ - 1; i++) { tree[i].parent = -1; tree[i].left = -1; tree[i].right = -1; tree[i].count = 1e15; tree[i].binary = false; } for (int32_t i = 0; i < osz_; i++) { tree[i].count = counts[i]; } int32_t leaf = osz_ - 1; int32_t node = osz_; for (int32_t i = osz_; i < 2 * osz_ - 1; i++) { int32_t mini[2]; for (int32_t j = 0; j < 2; j++) { if (leaf >= 0 && tree[leaf].count < tree[node].count) { mini[j] = leaf--; } else { mini[j] = node++; } } tree[i].left = mini[0]; tree[i].right = mini[1]; tree[i].count = tree[mini[0]].count + tree[mini[1]].count; tree[mini[0]].parent = i; tree[mini[1]].parent = i; tree[mini[1]].binary = true; } for (int32_t i = 0; i < osz_; i++) { std::vector<int32_t> path; std::vector<bool> code; int32_t j = i; while (tree[j].parent != -1) { path.push_back(tree[j].parent - osz_); code.push_back(tree[j].binary); j = tree[j].parent; } paths.push_back(path); codes.push_back(code); } } // 用 hierarchical softmax 進行多分類計算 real Model::hierarchicalSoftmax(int32_t target, real lr) { real loss = 0.0; grad_.zero(); // 詞的 0,1 表示 const std::vector<bool>& binaryCode = codes[target]; // 詞到樹根的路徑 const std::vector<int32_t>& pathToRoot = paths[target]; for (int32_t i = 0; i < pathToRoot.size(); i++) { // 路徑上的節點對應二分類, 0,1 編碼決定分類 // 用二分邏輯回歸進行訓練 loss += binaryLogistic(pathToRoot[i], binaryCode[i], lr); } return loss; } 

最后我們看一下二分邏輯回歸的實現。

// 用一個正/負面例子更新二分邏輯回歸模型 real Model::binaryLogistic(int32_t target, bool label, real lr) { real score = sigmoid(wo_->dotRow(hidden_, target)); real alpha = lr * (real(label) - score); grad_.addRow(*wo_, target, alpha); wo_->addRow(hidden_, target, alpha); if (label) { return -log(score); } else { return -log(1.0 - score); } } 

3. cbow

第二段講解了 cbow 的原理是用中心詞的上下文來預測中心詞, 這里我們看看 fasttext 是如何實現 cbow 的。

void FastText::cbow(Model& model, real lr, const std::vector<int32_t>& line) { std::vector<int32_t> bow; std::uniform_int_distribution<> uniform(1, args_->ws); for (int32_t w = 0; w < line.size(); w++) { int32_t boundary = uniform(model.rng); bow.clear(); for (int32_t c = -boundary; c <= boundary; c++) { // 將上下文的子字符串加入包中 if (c != 0 && w + c >= 0 && w + c < line.size()) { const std::vector<int32_t>& ngrams = dict_->getNgrams(line[w + c]); bow.insert(bow.end(), ngrams.cbegin(), ngrams.cend()); } } // 用所有子字符串來預測中心詞,從而更新參數 model.update(bow, line[w], lr); } } 

fasttext 的處理非常簡潔,將上下文的子串全部加和平均作為輸入去預測中心詞。

3. 總結

fasttext 利用子詞改良了詞嵌入的質量,在嵌入學習中考慮了詞的內部結構。在具體實現中, fasttext 用子詞向量的加和平均表示詞向量, 提供了 skip-gram 和 cbow 模式訓練詞嵌入表達。

四. FastText 線性文本分類優化

1. 背景介紹

對於文本分類而言,線性分類器往往能夠達到非常優秀,媲美深度模型的效果。通常,線性模型的參數數量與詞庫大小相關,導致模型規模巨大。 fasttext 針對線性分類器模型進行了諸多優化,在不顯著損失分類器精度的情況下,減少了內存使用和計算時間。

2. fasttext 分類模型架構

fasttext 的詞嵌入是通過分類學習完成的,所以詞嵌入和文本分類模型可以用下圖統一表示。

 
Model Architecture.png

其中 $x_1, ..., x_N$ 表示一個文本中的 ngram 向量,一個文本的表示是所有 ngram 的加和平均。這和前文中提到的 cbow 相似,cbow 用上下文的 ngram 去預測中心詞,而此處用全部的 ngram 去預測指定類別。

與前文詞嵌入模型一樣, fasttext 模型在進行文本分類監督訓練時,既學習詞嵌入表達,也學習分類器線性系數。

3. 優化

1. 子空間量化

product quantization [8] 是一種保存數據間距離的壓縮技術。PQ 用一個碼本來近似數據,與傳統的 keams 訓練碼本不同的是, PQ 將數據空間划分為 k 個子空間,並分別用 kmeans 學習子空間碼本。數據的近似和重建均在子空間完成,最終拼接成結果。

在 fasttext 中,子空間碼本大小為 256,可以用 1 byte 表示。子空間的數量在 [2, d/2] 間取值。

除了用 PQ 對數據進行量化壓縮,fasttext 還提供了對分類系數的 PQ 量化選項。

PQ 的優化能夠在不影響分類其表現的情況下,將分類模型壓縮為原大小的 $\frac{1}{10}$。

2. 裁剪字典內容

fasttext 提供了一個誘導式裁剪字典的算法,保證裁剪后的字典內容覆蓋了所有的文章。具體而言,fasttext 存有一個保留字典,並在線處理文章,如果新的文章沒有被保留字典涵蓋,則從該文章中提取一個 norm 最大的詞和其子串加入字典中。

字典裁剪能夠有效將模型的數量減少,甚至到原有的 $\frac{1}{100}$。

4. 總結

fasttext 利用 Product Quantization 對字典中的 詞嵌入向量進行了壓縮,並使用誘導式字典方法,構造涵蓋全部文本的字典。兩者結合,能夠在不明顯損害分類算法表現的情況下,將分類模型大小減小數百倍 。

五. 實踐

本段我們對詞嵌入進行實踐。詞嵌入的比較有兩種方式,直接比較是驗證詞表達保存了人為標注的詞間關系,間接比較則是通過使用嵌入表達向量進行進一步學習,比如情緒預測[5],通過模型的表現判斷詞嵌入的質量。

我們用中文維基數據 [9] 進行了訓練,fasttext 的參數使用默認值, epoch 設置為50。此外 facebook 還提供了訓練好的多國語言表達 [10].

以下我們分別用 nn(最近鄰) 對中文維基訓練結果進行實踐:

// 乒乓球的近似詞匯 Query word? 乒乓球 壁球 0.837808 曲棍球 0.792717 網球 0.792332 排球 0.789665 手球 0.780589 田徑 0.780279 桌球 0.778775 舉重 0.776161 沙灘排球 0.775708 乒乓球隊 0.772797 
// "男乒乓球" 並不存在, fasttext 仍然可以得到合理結果 Query word? 男乒乓球 乒乓球 0.767142 中國男子乒乓球隊 0.725763 兵乓球 0.691391 體操 0.688922 張怡寧 0.688578 陳若琳 0.681761 跳水隊 0.678217 打乒乓球 0.677547 王楠 0.671578 吳敏霞 0.669105 

六. 總結

fasttext 是 facebook 開源的關於文本表達和文本分類的計算庫。fasttext 結合詞的子串信息計算詞表達,提高了對微變形詞匯的學習。針對文本分類模型, fasttext 使用了子空間量化和字典裁剪的策略,在不損失模型精度的情況下,將模型大小縮減數百倍。

在 fasttext 之上,可以做進一步優化,一個方向是在文本分類模型中,文本表達用類似 tf-idf 的方式對詞進行加權平均。在字典裁剪算法上,一個涵蓋所有文本的詞並不一定有區分能力,比如 "the" 這個單詞,可以嘗試從保留區分能力的視角來保留字典。

七. 引用

[1] https://github.com/facebookresearch/fastText
[2] Bojanowski, Piotr and Grave, Edouard and Joulin, Armand and Mikolov, Tomas. "Enriching Word Vectors with Subword Information".
[3] Joulin, Armand and Grave, Edouard and Bojanowski, Piotr and Mikolov, Tomas. "Bag of Tricks for Efficient Text Classification".
[4]Joulin, Armand and Grave, Edouard and Bojanowski, Piotr and Douze, Matthijs and Jegou, Herve and Mikolov, Tomas. "FastText.zip: Compressing text classification models".
[5] Mikolov, Tomas, et al. "Efficient Estimation of Word Representations in Vector Space".
[6] Jeffrey Pennington, Richard Socher, and Christopher D. Manning. 2014. "GloVe: Global Vectors for Word Representation".
[7] Xinxiong Chen, Lei Xu, Zhiyuan Liu, Maosong Sun, and Huanbo Luan. 2015. Joint learning of character and word embeddings. In Proc. IJCAI.
[8] Product quantization for nearest neighbor search. H Jegou, M Douze, C Schmid. IEEE Transactions on Pattern Analysis and Machine Intelligence
[9] https://dumps.wikimedia.org/zhwiki/
[10] https://github.com/facebookresearch/fastText/blob/master/pretrained-vectors.md



作者:machinelearning
鏈接:https://www.jianshu.com/p/9ea0d69dd55e
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

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



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