條件隨機場之CRF++原始碼詳解-預測
這篇文章主要講解CRF++實現預測的過程,預測的演算法以及程式碼實現相對來說比較簡單,所以這篇文章理解起來也會比上一篇條件隨機場訓練的內容要容易。
預測
上一篇條件隨機場訓練的原始碼詳解中,有一個地方並沒有介紹。 就是訓練結束後,會把待優化權重alpha等變數儲存到檔案中,也就是輸出到指定的模型檔案。在執行預測的時候會從模型檔案讀出相關的變數,這個過程其實就是資料序列化與反序列化,該過程跟條件隨機場演算法關係不大,因此為了突出重點原始碼解析裡就沒有介紹這部分,有興趣的朋友可以自己研究一下。
CRF++預測的入口程式碼在crf_test.cpp的main函式中,最終會呼叫tragger.cpp的int crfpp_test(const Param ¶m)函式,期間會做一些輸入引數的處理、異常處理、讀取模型檔案等操作。一切準備就緒就會開啟待預測的檔案,進行預測。正式探討預測程式碼之前,我們先看下預測的理論基礎。條件隨機場的預測用到了維特比演算法,公式如下:
\begin{aligned} y^* &= \arg \max_yP_w(y|x) \\ &= \arg \max_y\frac{ \exp \left \{ \sum_{k=1}^Kw_kf_k(y,x) \right\}}{Z_w(x)} \\ &= \arg \max_y \exp \left \{\sum_{k=1}^Kw_kf_k(y,x) \right\} \\ &= \arg \max_y \ \sum_{k=1}^Kw_kf_k(y,x) \end{aligned}
從公式我們可以看出,我們求的概率最大值就是要求代價最大。接下來就看下CRF++的原始碼,程式碼在tragger.cpp的crfpp_test函式中:
while (*is) {//is是開啟的測試檔案,可以輸入多個測試檔案做預測 tagger.parse_stream(is.get(), os.get()); } bool TaggerImpl::parse_stream(std::istream *is, std::ostream *os) { if (!read(is) || !parse()) {//read函式在特徵篇講過,不再贅述,呼叫parse函式進行預測 return false; } if (x_.empty()) { return true; } toString(); //格式化輸出,-v 會輸出每個詞預測為某個label的概率,-n會輸出預測序列概率最大的前n個,如果理解上一篇訓練過程,再看這個函式就比較容易理解,無非就是概率計算,這裡不再贅述 os->write(os_.data(), os_.size()); //輸出到輸出檔案 return true; } 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; } void TaggerImpl::viterbi() { for (size_t i = 0;i < x_.size(); ++i) { //遍歷每個詞 for (size_t j = 0; j < ysize_; ++j) { //遍歷每個詞的每個label 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) { //從前一個詞到當前詞的代價之和 = max(前一個節點的代價 + 前一個節點的邊代價 + 當前節點代價) 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; //記錄最大的代價值, 如果best = 0代表第一個詞,沒有左邊,最大代價就是節點的代價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; }
預測的核心程式碼就看完了,大部分複用了訓練過程的邏輯。可以看到預測的過程跟公式是一致的,無非就是求能夠讓代價最大的label序列(標記序列),這就是維特比演算法。
總結
至此,我們的條件隨機場之CRF++原始碼詳解系列就結束了,主要涵蓋了特徵處理、訓練以及預測三個核心過程。結合CRF++原始碼我們可以更形象的、更通俗的去理解條件隨機場模型。以後想起條件隨機場模型,我們腦海浮現的不再是一堆公式,而是一個無向圖,在圖上進行代價計算、前向後向計算、期望值的計算以及梯度的計算等一系列的過程。希望這個系列對於正在學習條件隨機場的朋友能有幫助,如果本文闡述的有歧義、不通俗、不容易理解的地方,歡迎留言區交流,我將及時更正、回覆,希望我們一起提高。