系統學習機器學習之隨機場(四)--CRF++原始碼分析
1. 簡述
最近要應用CRF模型,進行序列識別。選用了CRF++工具包,具體來說是在VS2008的C#環境下,使用CRF++的windows版本。本文總結一下了解到的和CRF++工具包相關的資訊。
參考資料是CRF++的官方網站:CRF++: Yet Another CRF toolkit,網上的很多關於CRF++的博文就是這篇文章的全部或者部分的翻譯,本文也翻譯了一些。
2. 工具包下載
第一,版本選擇,當前最新版本是2010-05-16日更新的CRF++ 0.54版本,不過這個版本以前我用過一次好像執行的時候存在一些問題,網上一些人也說有問題,所以這裡用的是2009-05-06: CRF++ 0.53版本。關於執行出錯的資訊有
第二,檔案下載,這個主頁上面只有最新的0.54版本的檔案,網上可以搜尋,不過不是資源不是很多,我在CSDN上面下載了一個CRF++0.53版本的,包含linux和windows版本,其要花掉10個積分。因為,我沒有找到比較穩定、長期、免費的連結,這裡上傳一份這個檔案:CRF++ 0.53 Linux和Windows版本。
補充:
目前見到的版本,大概是CRF++ 0.58.
3. 工具包檔案
doc資料夾:就是官方主頁的內容。
example資料夾:有四個任務的訓練資料、測試資料和模板檔案。
sdk資料夾:CRF++的標頭檔案和靜態連結庫。
crf_learn.exe:CRF++的訓練程式。
crf_test.exe:CRF++的預測程式
libcrfpp.dll:訓練程式和預測程式需要使用的靜態連結庫。
實際上,需要使用的就是crf_learn.exe,crf_test.exe和libcrfpp.dll,這三個檔案。
4. 命令列格式
4.1 訓練程式
命令列:
% crf_learn template_file train_file model_file
這個訓練過程的時間、迭代次數等資訊會輸出到控制檯上(感覺上是crf_learn程式的輸出資訊到標準輸出流上了),如果想儲存這些資訊,我們可以將這些標準輸出流到檔案上,命令格式如下:
% crf_learn template_file train_file model_file >> train_info_file
有四個主要的引數可以調整:
-a CRF-L2 or CRF-L1
規範化演算法選擇。預設是CRF-L2。一般來說L2演算法效果要比L1演算法稍微好一點,雖然L1演算法中非零特徵的數值要比L2中大幅度的小。
-c float
這個引數設定CRF的hyper-parameter。c的數值越大,CRF擬合訓練資料的程度越高。這個引數可以調整過度擬合和不擬合之間的平衡度。這個引數可以通過交叉驗證等方法尋找較優的引數。
-f NUM
這個引數設定特徵的cut-off threshold。CRF++使用訓練資料中至少NUM次出現的特徵。預設值為1。當使用CRF++到大規模資料時,只出現一次的特徵可能會有幾百萬,這個選項就會在這樣的情況下起到作用。
-p NUM
如果電腦有多個CPU,那麼那麼可以通過多執行緒提升訓練速度。NUM是執行緒數量。
帶兩個引數的命令列例子:
% crf_learn -f 3 -c 1.5 template_file train_file model_file
4.2 測試程式
命令列:
% crf_test -m model_file test_files
有兩個引數-v和-n都是顯示一些資訊的,-v可以顯示預測標籤的概率值,-n可以顯示不同可能序列的概率值,對於準確率,召回率,執行效率,沒有影響,這裡不說明了。
與crf_learn類似,輸出的結果放到了標準輸出流上,而這個輸出結果是最重要的預測結果資訊(測試檔案的內容+預測標註),同樣可以使用重定向,將結果儲存下來,命令列如下。
% crf_test -m model_file test_files >> result_file
5. 檔案格式
5.1 訓練檔案
下面是一個訓練檔案的例子:
訓練檔案由若干個句子組成(可以理解為若干個訓練樣例),不同句子之間通過換行符分隔,上圖中顯示出的有兩個句子。每個句子可以有若干組標籤,最後一組標籤是標註,上圖中有三列,即第一列和第二列都是已知的資料,第三列是要預測的標註,以上面例子為例是,根據第一列的詞語和和第二列的詞性,預測第三列的標註。
當然這裡有涉及到標註的問題,這個就是很多paper要研究的了,比如命名實體識別就有很多不同的標註集。這個超出本文範圍。
5.2 測試檔案
測試檔案與訓練檔案格式自然是一樣的,用過機器學習工具包的這個一般都理解吧。
與SVM不同,CRF++沒有單獨的結果檔案,預測結果通過標準輸出流輸出了,因此前面4.2節的命令列中,將結果重定向到檔案中了。結果檔案比測試檔案多了一列,即為預測的標籤,我們可以計算最後兩列,一列的標註的標籤,一列的預測的標籤,來得到標籤預測的準確率。
5.3 模板檔案
5.3.1 模板基礎
模板檔案中的每一行是一個模板。每個模板都是由%x[row,col]來指定輸入資料中的一個token。row指定到當前token的行偏移,col指定列位置。
由上圖可見,當前token是the這個單詞。%x[-2,1]就就是the的前兩行,1號列的元素(注意,列是從0號列開始的),即為PRP。
5.3.2 模板型別
有兩種型別的模板,模板型別通過第一個字元指定。
Unigram template: first character, 'U'
當給出一個"U01:%x[0,1]"的模板時,CRF++會產生如下的一些特徵函式集合(func1 ... funcN) 。
這幾個函式我說明一下,%x[0,1]這個特徵到前面的例子就是說,根據詞語(第1列)的詞性(第2列)來預測其標註(第3列),這些函式就是反應了訓練樣例的情況,func1反映了“訓練樣例中,詞性是DT且標註是B-NP的情況”,func2反映了“訓練樣例中,詞性是DT且標註是I-NP的情況”。
模板函式的數量是L*N,其中L是標註集中類別數量,N是從模板中擴充套件處理的字串種類。
Bigram template: first character, 'B'
這個模板用來描述二元特徵。這個模板會自動產生當前output token和前一個output token的合併。注意,這種型別的模板會產生L * L * N種不同的特徵。
Unigram feature 和 Bigram feature有什麼區別呢?
unigram/bigram很容易混淆,因為通過unigram-features也可以寫出類似%x[-1,0]%x[0,0]這樣的單詞級別的bigram(二元特徵)。而這裡的unigram和bigram features指定是uni/bigrams的輸出標籤。
unigram: |output tag| x |all possible strings expanded with a macro|
bigram: |output tag| x |output tag| x |all possible strings expanded with a macro|
這裡的一元/二元指的就是輸出標籤的情況,這個具體的例子我還沒看到,example資料夾中四個例子,也都是隻用了Unigram,沒有用Bigarm,因此感覺一般Unigram feature就夠了。
5.3.3 模板例子
這是CoNLL 2000的Base-NP chunking任務的模板例子。只使用了一個bigram template ('B')。這意味著只有前一個output token和當前token被當作bigram features。“#”開始的行是註釋,空行沒有意義。
6. 樣例資料
example資料夾中有四個任務,basenp,chunking,JapaneseNE,seg。前兩個是英文資料,後兩個是日文資料。第一個應該是命名實體識別,第二個應該是分詞,第三個應該是日文命名實體識別,第四個不清楚。這裡主要跑了一下前兩個任務,後兩個是日文的搞不懂。
根據任務下面的linux的腳步檔案,我寫了個簡單的windows批處理(其中用重定向儲存了資訊),比如命名為exec.bat,跑了一下。批處理檔案放在要跑的任務的路徑下就行,批處理檔案內容如下:
..\..\crf_learn -c 10.0 template train.data model >> train-info.txt
..\..\crf_test -m model test.data >> test-info.txt
這裡簡單解釋一下批處理,批處理檔案執行後的當前目錄就是該批處理檔案所在的目錄(至少我的是這樣,如果不是,可以使用cd %~dp0這句命令,~dp0表示了“當前碟符和路徑”),crf_learn和crf_test程式在當前目錄的前兩級目錄上,所以用了..\..\。
下面這個轉自:http://www.hankcs.com/ml/crf-code-analysis.html,(我做了部分修改)
本文按照呼叫順序抽絲剝繭地分析了CRF++的程式碼,詳細註釋了主要函式,並指出了程式碼與理論公式的對應關係。內容包括擬牛頓法的目標函式、梯度、L2正則化、L-BFGS優化、概率圖構建、前向後向演算法、維特比演算法等。
訓練
先從訓練開始說起吧
- /**
- * 命令列式訓練
- * @param argc 命令個數
- * @param argv 命令陣列
- * @return 0表示正常執行,其他表示錯誤
- */
- int crfpp_learn(int argc, char **argv)
該函式解析命令列之後呼叫:
- /**
- * 訓練CRF模型
- * @param param 引數
- * @return
- */
- int crfpp_learn(const Param ¶m)
該函式會呼叫:
- /**
- * 訓練
- * @param templfile 模板檔案
- * @param trainfile 訓練檔案
- * @param modelfile 模型檔案
- * @param textmodelfile 是否輸出文字形式的模型檔案
- * @param maxitr 最大迭代次數
- * @param freq 特徵最低頻次,也就是說,在某特徵出現的次數超過該值,才進入模型,預設為1,即只要出現就進入模型。
- * @param eta 收斂閾值
- * @param C cost-factor 實際定義的是權重共享係數
- * @param thread_num 執行緒數
- * @param shrinking_size 該引數在CRF演算法中沒用,在MIRA演算法中使用,也就是與CRF模型無關,可以不考慮。
- * @param algorithm 訓練演算法
- * @return
- */
- bool learn(const char *templfile,
- const char *trainfile,
- const char *modelfile,
- bool textmodelfile,
- size_t maxitr,
- size_t freq,
- double eta,
- double C,
- unsigned short thread_num,
- unsigned short shrinking_size,
- int algorithm);
該函式先讀取特徵模板和訓練檔案
- /**
- * 開啟配置檔案和訓練檔案
- * @param template_filename
- * @param train_filename
- * @return
- */
- bool open(const char *template_filename, const char *train_filename);
這個open方法並沒有構建訓練例項,而是簡單地解析特徵模板和統計標註集:
- /**
- * 讀取特徵模板檔案
- * @param filename
- * @return
- */
- bool openTemplate(const char *filename);
- /**
- * 讀取訓練檔案中的標註集
- * @param filename
- * @return
- */
- bool openTagSet(const char *filename);
這裡補充一下:
每個句子表示一個樣例,每個樣例中的單詞+標註,作為一個token。這裡在open結束後,系統開始針對每個樣例中的每個token,對映特徵函式。比如例子中,有一個特徵模板有19個特徵函式,那麼任何一個樣子(句子)中的任何一個單詞+標註就有19個特徵。所有這些特徵全部綜合儲存在feature_cache中,你可以把這個cache裡理解為一個二維快取,其中,水平方向為樣本類別,也就是Y集,例如該例子中Y有14個元素,也就是水平方向寬度為14,則ID每次針對一元模板增加14,也就是在相同水平位置對應的下一個垂直位置放置。最後整個cache就是一個水平為Y集,垂直為tokent特徵集X的快取。而該快取的元素值對應特徵出現的次數(因為要對所有樣例的所有token的所有特徵函式統計)。需要指出的是,特徵函式和ID之間的關係是預先確定好的,例如特徵函式作用後結果為U00:B-2,則直接找到相應編碼表中的B-2對應的index即可,這個表我也不是很清楚,因為是分詞標註領域的一個標準。
TaggerImpl儲存訓練樣例,x_儲存相應的output序列,result_儲存相應的狀態序列,answer_儲存模型算出來的狀態序列;為了實現多執行緒併發處理,另外儲存了處理該TaggerImpl的執行緒thread_id_;output序列中的每一個token都對應一個feature集合,整個output序列對應了feature集合的序列,系統將所有訓練樣例的feature集合順序儲存在一個feature_cache中,因此在每一個TaggerImpl中儲存了自己的feature序列在feature_cache中偏移量feature_id_,而這個feature_cache存在於FeatureIndex物件中。系統中所有的TaggerImpl都共享一個FeatureIndex物件;為了DP程式設計的方便,又包含一個Node二維陣列,橫軸對應output中的每一個token,縱軸代表系統狀態集合中的每一個狀態。Node儲存DP中的每一個狀態,包括alpha,beta,verterbi路徑前驅等。
回到learn方法中來,做完了這些諸如IO和引數解析之後,learn方法會根據演算法引數的不同而呼叫不同的訓練演算法。取最常用的說明如下:
- /**
- * CRF訓練
- * @param x 句子列表
- * @param feature_index 特徵編號表
- * @param alpha 特徵函式的代價
- * @param maxitr 最大迭代次數
- * @param C cost factor
- * @param eta 收斂閾值
- * @param shrinking_size 未使用
- * @param thread_num 執行緒數
- * @param orthant 是否使用L1範數
- * @return 是否成功
- */
- bool runCRF(const std::vector<TaggerImpl *> &x, EncoderFeatureIndex *feature_index, double *alpha, size_t maxitr,
- float C, double eta, unsigned short shrinking_size, unsigned short thread_num, bool orthant)
計算梯度,
補充:
需要注意的是,CRF++實現的是線性鏈式CRF。主要區別在於勢函式的計算不同,其他相同。計算梯度主要的方式是,經驗分佈的數學期望與模型的條件概率的數學期望的差,再加上正則項,經驗分佈的數學期望為訓練資料集中隨機變數 (x,y)滿足特徵約束的個數,模型的條件概率的數學期望的計算實質上是計算條件概率p(y|x,alpha)。因此,演算法主要就是計算條件概率。
建立多個CRFEncoderThread,平均地將句子分給每個執行緒。每個執行緒的工作其實只是計算梯度:
- /**
- * 計算梯度
- * @param expected 梯度向量
- * @return 損失函式的值
- */
- double TaggerImpl::gradient(double *expected)
梯度計算時,先構建網格:
- void TaggerImpl::buildLattice()
由於CRF是概率圖模型,所以有一些圖的特有概念,如頂點和邊:
- /**
- * 圖模型中的節點
- */
- struct Node
- /**
- * 邊
- */
- struct Path
buildLattice方法呼叫rebuildFeatures對每個時刻的每個狀態分別構造邊和頂點,實際上是條件概率的矩陣計算:
- for (size_t cur = 0; cur < tagger->size(); ++cur)
- {
- const int *f = (*feature_cache)[fid++];
- for (size_t i = 0; i < y_.size(); ++i)
- {
- Node *n = allocator->newNode(thread_id);
- n->clear();
- n->x = cur;
- n->y = i;
- n->fvector = f;
- tagger->set_node(n, cur, i);
- }
- }
- for (size_t cur = 1; cur < tagger->size(); ++cur)
- {
- const int *f = (*feature_cache)[fid++];
- for (size_t j = 0; j < y_.size(); ++j)
- {
- for (size_t i = 0; i < y_.size(); ++i)
- {
- Path *p = allocator->newPath(thread_id);
- p->clear();
- p->add(tagger->node(cur - 1, j), tagger->node(cur, i));
- p->fvector = f;
- }
- }
- }
這也就是大家經常看到的類似如下的圖:
補充下:
這裡採用矩陣方式計算條件概率,對於一階線性鏈式CRF,在圖模型中增加起始Y0,和結束Yn+1,Yi-1 為Y',Yi為y,定義一組矩陣{Mi(x)|i = 1, 2, ......n+1},其中每個Mi(x)是一個y*y階隨機變數矩陣,矩陣中每個元素為:
圖示如下:
則條件概率為,其中歸一化:
然後計算每個節點和每條邊的代價(也就是特徵函式乘以相應的權值,簡稱代價):
- /**
- * 計算狀態特徵函式的代價
- * @param node 頂點
- */
- void FeatureIndex::calcCost(Node *n) const
- {
- n->cost = 0.0;
- #define ADD_COST(T, A) \
- do { T c = 0; \
- for (const int *f = n->fvector; *f != -1; ++f) { c += (A)[*f + n->y]; } \
- n->cost =cost_factor_ *(T)c; } while (0)
- if (alpha_float_)
- {
- ADD_COST(float, alpha_float_);
- }
- else
- {
- ADD_COST(double, alpha_);
- }
- #undef ADD_COST
- }
- /**
- * 計算轉移特徵函式的代價
- * @param path 邊
- */
- void FeatureIndex::calcCost(Path *p) const
- {
- p->cost = 0.0;
- #define ADD_COST(T, A) \
- { T c = 0.0; \
- for (const int *f = p->fvector; *f != -1; ++f) { \
- c += (A)[*f + p->lnode->y * y_.size() + p->rnode->y]; \
- } \
- p->cost =cost_factor_*(T)c; }
- if (alpha_float_)
- {
- ADD_COST(float, alpha_float_);
- }
- else
- {
- ADD_COST(double, alpha_);
- }
- }
其中fvector是當前命中特徵函式的起始id集合,對於每個起始id,都有連續標籤個數種y值;n->y是當前時刻的標籤,由於每個特徵函式都必須同時接受x和y才能決定輸出1或0,所以要把兩者加起來才能確定最終特徵函式的id。用此id就能在alpha向量中取到最終的權值,將權值累加起來,乘以一個倍率(也就是所謂的代價引數cost_factor),得到最終的代價cost。
對於邊來說,也是類似的,只不過對每個起始id,都有連續標籤個數平方種y值組合。
這部分對應
需要強調的是:演算法內部對於exp沒有計算,實際上,所有關於exp{F}的計算,都是隻計算F,而在實際使用中,exp{F}參與計算時,直接採用log形式。例如下面的前向演算法中,要計算兩個expX1,expX2的乘積,則直接用log(X1+X2)表示,程式碼直接計算X1+X2
前向後向演算法
網格建完了,就可以在這個圖上面跑前向後向演算法了:
- /**
- * 前向後向演算法
- */
- void forwardbackward();
該方法依次計算前後向概率:
- for (int i = 0; i < static_cast<int>(x_.size()); ++i)
- {
- for (size_t j = 0; j < ysize_; ++j)
- {
- node_[i][j]->calcAlpha();
- }
- }
- for (int i = static_cast<int>(x_.size() - 1); i >= 0; --i)
- {
- for (size_t j = 0; j < ysize_; ++j)
- {
- node_[i][j]->calcBeta();
- }
- }
計算前向概率的具體實現是:
- void Node::calcAlpha()
- {
- alpha = 0.0;
- for (const_Path_iterator it = lpath.begin(); it != lpath.end(); ++it)
- {
- alpha = logsumexp(alpha, (*it)->cost + (*it)->lnode->alpha, (it == lpath.begin()));
- }
- alpha += cost;
- }
其中cost是我們剛剛計算的當前節點的M_i(x),而alpha則是當前節點的前向概率。lpath是入邊,如程式碼和圖片所示,一個頂點可能有多個入邊。
對應:
後向概率同理略過。
前後向概率都有了之後,計算規範化因子:
- Z_ = 0.0;
- for (size_t j = 0; j < ysize_; ++j)
- {
- Z_ = logsumexp(Z_, node_[0][j]->beta, j == 0);
- }
對應著
關於函式logsumexp的意義,請參考《計算指數函式的和的對數》。
於是完成整個前後向概率的計算。
期望值的計算
節點期望值
所謂的節點期望值指的是節點對應的狀態特徵函式關於條件分佈p(Y|X)的數學期望。
- for (size_t i = 0; i < x_.size(); ++i)
- {
- for (size_t j = 0; j < ysize_; ++j)
- {
- node_[i][j]->calcExpectation(expected, Z_, ysize_);
- }
- }
calcExpectation具體實現是:
- /**
- * 計算節點期望
- * @param expected 輸出期望
- * @param Z 規範化因子
- * @param size 標籤個數
- */
- void Node::calcExpectation(double *expected, double Z, size_t size) const
- {
- const double c = std::exp(alpha + beta - cost - Z);
- for (const int *f = fvector; *f != -1; ++f)
- {
- expected[*f + y] += c;
- }
- for (const_Path_iterator it = lpath.begin(); it != lpath.end(); ++it)
- {
- (*it)->calcExpectation(expected, Z, size);
- }
- }
第一個for對應下式的求和
概率求和意味著得到期望。
第二個for對應邊的期望值。
邊的期望值
所謂邊的期望指的是邊對應的轉移特徵函式關於條件分佈p(Y|X)的數學期望。
- /**
- * 計算邊的期望
- * @param expected 輸出期望
- * @param Z 規範化因子
- * @param size 標籤個數
- */
- void Path::calcExpectation(double *expected, double Z, size_t size) const
- {
- const double c = std::exp(lnode->alpha + cost + rnode->beta - Z);
- for (const int *f = fvector; *f != -1; ++f)
- {
- expected[*f + lnode->y * size + rnode->y] += c;
- }
- }
對應下式的求和
這樣就得到了條件分佈的數學期望:
梯度計算
- for (size_t i = 0; i < x_.size(); ++i)
- {
- for (const int *f = node_[i][answer_[i]]->fvector; *f != -1; ++f)
- {
- --expected[*f + answer_[i]];
- }
- s += node_[i][answer_[i]]->cost; // UNIGRAM cost
- const std::vector<Path *> &lpath = node_[i][answer_[i]]->lpath;
- for (const_Path_iterator it = lpath.begin(); it != lpath.end(); ++it)
- {
- if ((*it)->lnode->y == answer_[(*it)->lnode->x])
- {
- for (const int *f = (*it)->fvector; *f != -1; ++f)
- {
- --expected[*f + (*it)->lnode->y * ysize_ + (*it)->rnode->y];
- }
- s += (*it)->cost; // BIGRAM COST
- break;
- }
- }
- }
–expected表示模型期望(條件分佈)減去觀測期望,得到目標函式的梯度:
有人可能要問了,expected的確存的是條件分佈的期望,但觀測期望還沒計算呢,把條件分佈的期望減一是幹什麼?
這是因為對觀測資料(訓練資料)來講,它一定是對的,也就是在y!=answer_[i]的時候概率為0,在y=answer_[i]的時候概率為1,乘以特徵函式的輸出1,就等於1,這就是觀測期望。也就是上面說的,訓練資料中(x,y)出現的次數。
維特比演算法
緊接著gradient函式還順便調了一下TaggerImpl::viterbi:
- void TaggerImpl::viterbi()
- {
- for (size_t i = 0; i < x_.size(); ++i)
- {
- for (size_t j = 0; j < ysize_; ++j)
- {
- double bestc = -1e37;
- Node *best = 0;
- const std::vector<Path *> &lpath = node_[i][j]->lpath;
- for (const_Path_iterator it = lpath.begin(); it != lpath.end(); ++it)
- {
- double cost = (*it)->lnode->bestCost + (*it)->cost + node_[i][j]->cost;
- if (cost > bestc)
- {
- bestc = cost;
- best = (*it)->lnode;
- }
- }
- node_[i][j]->prev = best;
- node_[i][j]->bestCost = best ? bestc : node_[i][j]->cost;
- }
- }
- double bestc = -1e37;
- Node *best = 0;
- size_t s = x_.size() - 1;
- for (size_t j = 0; j < ysize_; ++j)
- {
- if (bestc < node_[s][j]->bestCost)
- {
- best = node_[s][j];
- bestc = node_[s][j]->bestCost;
- }
- }
- for (Node *n = best; n; n = n->prev)
- {
- result_[n->x] = n->y;
- }
- cost_ = -node_[x_.size() - 1][result_[x_.size() - 1]]->bestCost;
- }
其中prev構成一個前驅陣列,在動態規劃結束後通過prev回溯最優路徑的標籤y,存放於result陣列中。
跑viterbi演算法的目的是為了評估當前模型的準確度,以輔助決定是否終止訓練。關於Viterbi演算法,可以參考:系統學習機器學習之馬爾科夫假設(一)--HMM
正則化
為了防止過擬合,CRF++採用了L1或L2正則化:
- if (orthant)
- { // L1
- for (size_t k = 0; k < feature_index->size(); ++k)
- {
- thread[0].obj += std::abs(alpha[k] / C);
- if (alpha[k] != 0.0)
- {
- ++num_nonzero;
- }
- }
- }
- else
- {
- num_nonzero = feature_index->size();
- for (size_t k = 0; k < feature_index->size(); ++k)
- {
- thread[0].obj += (alpha[k] * alpha[k] / (2.0 * C));
- thread[0].expected[k] += alpha[k] / C;
- }
- }
以L2正則為例,L2正則在目標函式上加了一個正則項:
+
其中,σ是一個常數,在CRF++中其平方被稱作cost-factor,1/2*σ^2控制著懲罰因子的強度。可見要最小化目標函式,正則化項也必須儘量小才行。模型引數的平方和小,其複雜度就低,於是就不容易過擬合。關於L1、L2正則化推薦看Andrew Ng的ML公開課。
目標函式加了正則項之後,梯度順理成章地也應加上正則項的導數:
+Wi/σ^2
這也就是程式碼中為什麼要自加這兩項的原因了:
- thread[0].obj += (alpha[k] * alpha[k] / (2.0 * C));
- thread[0].expected[k] += alpha[k] / C;
L-BFGS優化
梯度和損失函式有了,之後就是通用的凸函式LBFGS優化了。CRF++直接將這些引數送入一個LBFGS模組中:
- if (lbfgs.optimize(feature_index->size(), &alpha[0], thread[0].obj, &thread[0].expected[0], orthant, C) <=
- 0)
- {
- return false;
- }
據說這個模組是用一個叫f2c的工具從FORTRAN程式碼轉成的C程式碼,可讀性並不好,也就不再深入了。
- // lbfgs.c was ported from the FORTRAN code of lbfgs.m to C
- // using f2c converter
- //
- // http://www.ece.northwestern.edu/~nocedal/lbfgs.html
預測
預測就簡單多了,主要對應下列方法:
- bool TaggerImpl::parse()
- {
- CHECK_FALSE(feature_index_->buildFeatures(this)) << feature_index_->what();
- if (x_.empty())
- {
- return true;
- }
- buildLattice();
- if (nbest_ || vlevel_ >= 1)
- {
- forwardbackward();
- }
- viterbi();
- if (nbest_)
- {
- initNbest();
- }
- return true;
- }
主要的方法也就是建立網格和維特比這兩個,由於前面訓練的時候已經分析過,這裡就不再贅述了。
Reference