1. 程式人生 > >清華大學thulac分詞和詞性標註程式碼理解

清華大學thulac分詞和詞性標註程式碼理解

     清華大學的thulac中分分詞詞法包,包含有中文分詞和詞性標註,從測試精度和速度看,效果還不錯,github上有提供c++和python程式碼,c++程式碼包含有訓練和測試程式碼,python程式碼只有測試程式碼,速度也較慢,github地址為:https://github.com/thunlp/THULAC。

     根據github上提出的參考文獻,完全無法看懂程式碼和文章有什麼關係,程式碼也比較難以理解,因此在記錄一下自己對於程式碼分詞原理的理解,希望對於後續研究的人有些幫助。

      認真的研究了一下c++程式碼,發現thulac程式碼與基於壓縮感知機原理的中文分詞類似,同樣採用來

7個特徵,首先對於輸入文字,生成所有字的特徵,同時初始化一個dat陣列,一個特徵權重陣列fl_weights,fl_weights大小語總的特徵數目相等,即每個特徵對於於fl_weights的一個權重值,dat為特徵在fl_weights中對於的權重的索引,即我們要查詢某個特徵featurefl_weights對於的權重,首先需要查詢datfeature對於的索引index,得到index後我們便可以得到feature對於的權重為fl_weights[index]。那麼dat是怎麼索引的呢,如果總的features個數為N,那麼dat的大小也要為N,當通常隨著訓練資料的增大,生成的特徵數目也會很大,這
dat就需要花費很大的記憶體,那麼thulac中是怎麼做的呢?

       首先說一下壓縮感知機演算法中特徵的生成,對於輸入句子s中的每個字wi,會根據其前後2個字生成7個特徵,這7個特徵分別為wi-2wi-1,wi-1wi-1wi,wi,wiwi+1wi+1,wi+1wi+2Thulac中在這7個特徵後面添加了一個separator=’ ’即空格;對於分詞,我們需要預測每個字屬於BMES的權重,因此,最後需要生成每個特徵對於於BMES的特徵,所以對每個句子中的每個字實際會生成7×4=28個特徵。關於thulac中特徵生成C++程式碼如下:

inline void feature_generation(RawSentence& seq,
                Indexer<RawSentence>& indexer,
                Counter<Word>* bigram_counter=NULL){
        int mid=0;
        int left=0;int left2=0;
        int right=0;int right2=0;
        RawSentence key;
        RawSentence bigram;
        for(int i=0;i<seq.size();i++){
            mid=seq[i];
            left=(i>0)?(seq[i-1]):(SENTENCE_BOUNDARY);
            left2=((i-2)>=0)?(seq[i-2]):(SENTENCE_BOUNDARY);
            right=((i+1)<seq.size())?(seq[i+1]):(SENTENCE_BOUNDARY);
            right2=((i+2)<seq.size())?(seq[i+2]):(SENTENCE_BOUNDARY);
            
            if(bigram_counter){
                if(i==0){
                    bigram.clear();
                    bigram.push_back(left2);bigram.push_back(left);
                    bigram_counter->update(bigram);
                    bigram.clear();
                    bigram.push_back(left);bigram.push_back(mid);
                    bigram_counter->update(bigram);
                    bigram.clear();
                    bigram.push_back(mid);bigram.push_back(right);
                    bigram_counter->update(bigram);
                }else{
                    bigram.clear();
                    bigram.push_back(right);bigram.push_back(right2);
                    bigram_counter->update(bigram);
                }
            }
            
            key.clear();
            key.push_back(mid);key.push_back(SEPERATOR);key.push_back('1');
            indexer.get_index(key);//indexer.dict為push(key),即最後一個放進indexer的key為indexer.dict[0],indexer.list則相反,最後一個key=indexer.list[-1]
            key.clear();
            key.push_back(left);key.push_back(SEPERATOR);key.push_back('2');
            indexer.get_index(key);//indexer儲存的為特徵的索引值,indexer[i]={ , , }
            key.clear();
            key.push_back(right);key.push_back(SEPERATOR);key.push_back('3');
            indexer.get_index(key);
            
            key.clear();
            key.push_back(left);key.push_back(mid);key.push_back(SEPERATOR);key.push_back('1');
            indexer.get_index(key);
            key.clear();
            key.push_back(mid);key.push_back(right);key.push_back(SEPERATOR);key.push_back('2');
            indexer.get_index(key);
            key.clear();
            key.push_back(left2);key.push_back(left);key.push_back(SEPERATOR);key.push_back('3');
            indexer.get_index(key);
            key.clear();
            key.push_back(right);key.push_back(right2);key.push_back(SEPERATOR);key.push_back('4');
            indexer.get_index(key);
        }
    };

       程式碼中key用於提取每個特徵,並將所有特徵儲存到indexer

接下來講一下dat的生成,我們知道,對於所有的特徵,其中有某些特徵的第一項或這前兩項是相同的,例如總的特徵數為N,其中有M個特徵的第一項是相同的,那麼我們就可以用dat的前M個元素儲存這M個特徵的第一項,接下來查詢第一項相同的情況下,第二項相同的項儲存到dat,dat生成的具體程式碼實現如下:

 void make_dat(std::vector<KeyValue>& lexicon,int no_prefix=0){
        std::sort(lexicon.begin(),lexicon.end(),&compare_words);

        int size=(int)lexicon.size();
        std::vector<int> children;
        Word prefix;
        prefix.clear();
        gen_children(lexicon,0,prefix,children);//第一個字的特徵children
        int base=assign(0,children,true);//給dat複製
        dat[0].base=base;
        for(int i=0;i<(int)lexicon.size();i++){
            Word& word=lexicon[i].key;
            //std::cout<<word<<"\n";
            //std::cout.flush();
            int off=get_info(word);
            if(off<=0)off=(int)word.size();
            for(int offset=off;offset<=(int)word.size();offset++){
                prefix.clear();
                for(int j=0;j<offset;j++)prefix.push_back(word[j]);
                int p_base=-get_info(prefix);
                
                gen_children(lexicon,i,prefix,children);
                int base=assign(p_base,children,offset==(int)word.size());
            }
            off=-get_info(word);
            if(no_prefix){
                dat[off].base=lexicon[i].value;
            }else{
                dat[dat[off].base].base=lexicon[i].value;
            }
			/*
            if(i&&(i%100000==0)){
                printf("%f\r",(double)i/size);
                std::cout.flush();
            }
			*/
        }

    }

       得到dat後,那麼對於輸入句子查詢中每個字對應的特徵索引,對於只含有一個字的特徵的索引儲存在uni_base,相鄰兩個字的特徵對應索引儲存在bi_base,,例如對於相鄰兩個字ch1,ch2uni_base,bi_base特徵為uni_base[ch1]=dat[ch1].base+separatorbi_base[ch1ch2]=dat[dat[ch1].base+ch2]+separator

此部分程式碼如下:

/*
     * 找出以ch1 ch2為字元的dat的下標
     * */
    inline void find_bases(int dat_size,int ch1,int ch2,int& uni_base,int&bi_base){
        if(ch1>32 &&ch1<128)ch1+=65248;//對於標點符號,數字、中英文字元,其indx為ascii碼+65248
        if(ch2>32 &&ch2<128)ch2+=65248;
        if(dat[ch1].check){
            uni_base=-1;bi_base=-1;return;//如果dat[ch1].check=1,則不匹配,返回為uni_base=-1
        }
        uni_base=dat[ch1].base+SEPERATOR;//uni_base為dat[ch1].base+空格
        int ind=dat[ch1].base+ch2;
        if(ind>=dat_size||dat[ind].check!=ch1){
            bi_base=-1;return;
        }
        bi_base=dat[ind].base+SEPERATOR;
    }

       得到特徵索引後,便可以通過索引uni_base,bi_base查詢特徵矩陣分類fl_weights中每個特徵對應的權重,之後通過解碼得到預測標籤,

Dp程式碼如下:

/** The DP algorithm(s) for path labeling */
inline int dp_decode(
        int l_size,///標籤個數
        int* ll_weights,///標籤間權重
        int node_count,///節點個數
        Node* nodes,///節點資料
        int* values,///value for i-th node with j-th label
        Alpha_Beta* alphas,///alpha value (and the pointer) for i-th node with j-th label
        int* result,
        int** pre_labels=NULL,///每種標籤可能的前導標籤(以-1結尾)
        int** allowed_label_lists=NULL///每個節點可能的標籤列表
        ){
    //calculate alphas
    int node_id;
    int* p_node_id;
    int* p_pre_label;
    int* p_allowed_label;//指向當前字所有可能標籤的陣列的指標
    register int k;//當前字的前一個節點可能的標籤(的編號)
    register int j;//當前字某一個可能的標籤(的編號)
    register Alpha_Beta* tmp;
    Alpha_Beta best;best.node_id=-1;
    Alpha_Beta* pre_alpha;
    int score;
    
    for(int i=0;i<node_count*l_size;i++)alphas[i].node_id=-2;
    for(int i=0;i<node_count;i++){//for each node
        p_allowed_label=allowed_label_lists?allowed_label_lists[i]:NULL;
        j=-1;
        int max_value=0;
        int has_max_value=0;
        while((p_allowed_label?
                    ((j=(*(p_allowed_label++)))!=-1)://如果有指定,則按照列表來
                    ((++j)!=l_size))){//否則列舉
            if((!has_max_value) || (max_value<values[i*l_size+j])){
                has_max_value=1;
                max_value=values[i*l_size+j];
            }
        }
        p_allowed_label=allowed_label_lists?allowed_label_lists[i]:NULL;
        j=-1;
        while((p_allowed_label?//遍歷尋找當前結點屬於j標籤的得分,找到最大的得分的j為當前結點的標籤
                    ((j=(*(p_allowed_label++)))!=-1)://如果有指定,則按照列表來
                    ((++j)!=l_size))){//否則列舉
            //if(max_value-20000>values[i*l_size+j])continue;//
            tmp=&alphas[i*l_size+j];//當前的j標籤
            tmp->value=0;
            p_node_id=nodes[i].predecessors;//取所有前繼節點
            p_pre_label=pre_labels?pre_labels[j]:NULL;
            while((node_id=*(p_node_id++))>=0){//列舉前繼節點,遍歷所有的前向結點,結算前向結點轉移到當前標籤j的得分,去最大的得分
                k=-1;
                while(p_pre_label?
                        ((k=(*p_pre_label++))!=-1):
                        ((++k)!=l_size)
                        ){
                    pre_alpha=alphas+node_id*l_size+k;
                    if(pre_alpha->node_id==-2)continue;//not reachable
                    score=pre_alpha->value+ll_weights[k*l_size+j];//前一節結點轉移得分等於前一個結點的得分+前一個標籤轉移到當前標籤的得分
                    if((tmp->node_id<0)||(score>tmp->value)){//如果當前的結點id小於0或者當前的結點值小於score
                        tmp->value=score;
                        tmp->node_id=node_id;
                        tmp->label_id=k;
                    }
                }
            }
            tmp->value+=values[i*l_size+j];//當前結點的值+當前結點的第j個標籤對應的權重
            
            if((nodes[i].type==1)||(nodes[i].type==3))//如果當前結點的型別為1或3,則id=-1
                tmp->node_id=-1;
            if(nodes[i].type>=0){
                if((best.node_id==-1)||(best.value<tmp->value)){
                    best.value=tmp->value;
                    best.node_id=i;
                    best.label_id=j;
                }
            }
        }
        //std::cout<<i<<" "<<best.value<<"\n";
    }
    //find the path and label the nodes of it.
    tmp=&best;//當前等於最大得分的結點
    while(tmp->node_id>=0){
        result[tmp->node_id]=tmp->label_id;//
        tmp=&(alphas[(tmp->node_id)*l_size+(tmp->label_id)]);
    }
    //debug
    /*(for(int i=0;i<node_count;i++){//for each node
        p_allowed_label=allowed_label_lists?allowed_label_lists[i]:NULL;   
        j=-1;
        std::cerr<<values[i*l_size+result[i]]<<" ";
        while((p_allowed_label?
                    ((j=(*(p_allowed_label++)))!=-1)://如果有指定,則按照列表來
                    ((++j)!=l_size))){//否則列舉
            tmp=&alphas[i*l_size+j];
            std::cerr<<values[i*l_size+j]<<" ";  
        }
        std::cerr<<"\n";
    }
    std::cerr<<"\n";*/
    //end of debug
    return best.value;
};

        引數更新,壓縮感知機中,預測值與輸入值相等,則對應的特徵權重+1,預測值語輸入值不相等,則對應的特徵權重-1;與壓縮感知機中特徵權重更新不同,thulac中,引數更新為對於所有的輸入標籤,其對應的特徵的權重+1,對於所有的預測標籤,其特徵權重-1;此部分程式碼如下:

 //update
            this->ngram_feature->update_weights(sequence,len,gold_standard,1,steps);//更新每個特徵對應的權重
            this->ngram_feature->update_weights(sequence,len,result,-1,steps);
            for(int i=0;i<len-1;i++){
                this->model->update_ll_weight(gold_standard[i],gold_standard[i+1],1,steps);//更新狀態轉移矩陣
                this->model->update_ll_weight(result[i],result[i+1],-1,steps);
       程式碼中,ll_weight為每個標籤的狀態轉移矩陣。


相關推薦

清華大學thulac詞性標註程式碼理解

     清華大學的thulac中分分詞詞法包,包含有中文分詞和詞性標註,從測試精度和速度看,效果還不錯,github上有提供c++和python程式碼,c++程式碼包含有訓練和測試程式碼,python程式碼只有測試程式碼,速度也較慢,github地址為:https://g

Python 文字挖掘:jieba中文詞性標註

#! /usr/bin/env python2.7 #coding=utf-8 import jieba import jieba.posseg #需要另外載入一個詞性標註模組 string = '其實大家買手機就是看個心情,沒必要比來比去的。' seg = jieba.posseg.cut(string

Deep Learning 在中文詞性標註任務中的應用

開源軟體包 SENNA 和 word2vec 中都用到了詞向量(distributed word representation),當時我就在想,對於我們的中文,是不是也類似地有字向

python3進行中文詞性標註

配置python環境(win10) 雙擊安裝,我安裝在了D:\python 中。 新增環境變數。 在我的電腦處右鍵 -> 高階系統設定 -> 環境變數 -> 系統變數 ->

一套準確率高且效率高的詞性標註工具-thulac

軟體簡介 THULAC(THU Lexical Analyzer for Chinese)由清華大學自然語言處理與社會人文計算實驗室研製推出的一套中文詞法分析工具包,具有中文分詞和詞性標註功能。THULAC具有如下幾個特點: 能力強。利用我們整合的目前世界上規模最大的人工分詞和詞性標註中文語料庫(

HMM與詞性標註、命名實體識別

HMM(隱馬爾可夫模型)是用來描述隱含未知引數的統計模型,舉一個經典的例子:一個東京的朋友每天根據天氣{下雨,天晴}決定當天的活動{公園散步,購物,清理房間}中的一種,我每天只能在twitter上看到她發的推“啊,我前天公園散步、昨天購物、今天清理房間了!”,那麼我可以根據她

用pyltp做詞性標註、ner

工具:win10、python2.7 主要參考官方文件 http://pyltp.readthedocs.io/zh_CN/latest/api.html# http://ltp.readthedocs.io/zh_CN/latest/install.html 1、安裝

文字處理(二)詞頻統計,jieba詞性標註,snownlp情感分析

這一篇接著上一篇處理後的資料進行操作,按照(一)中的步驟,這事應該將文字資料每一行中的高頻的正面詞去掉,因為多數是描述身體健康的短句,只有少數是描述脾臟檢查異常的,所以嘗試刪除掉描述身體健康的短句,只留下少數檢查異常的資料,對異常的檢查資料進行特徵提取,這是思路。所以這一篇目

結巴4--詞性標註

1 簡介 詞性(part-of-speech)是詞彙基本的語法範疇,通常也稱為詞類,主要用來描述一個詞在上下文的作用。例如,描述一個概念的詞就是名詞,在下文引用這個名詞的詞就是代詞。有的詞性經常會出現一些新的詞,例如名詞,這樣的詞性叫做開放式詞性。另外一些詞性

pyhanlp 詞性標註

pyhanlp中的分詞器簡介 pyhanlp實現的分詞器有很多,同時pyhanlp獲取hanlp中分詞器也有兩種方式 第一種是直接從封裝好的hanlp類中獲取,這種獲取方式一共可以獲取五種分詞器,而現在預設的就是第一種維特比分詞器 維特比 (viterbi):

HMM演算法-viterbi演算法的實現及與詞性標註、命名實體識別的引用

轉自:http://www.hankcs.com/nlp/hmm-and-segmentation-tagging-named-entity-recognition.html HMM(隱馬爾可夫模型)是用來描述隱含未知引數的統計模型,舉一個經典的例子:一個東京的朋友每天

jieba詞性標註

# ============================================================================= # jieba分詞 #import jieba # f1 =open("weibo.txt") # f2 =

jieba詞性標註自定義詞典支援特殊字元

jieba分詞可以自定義詞表和詞庫。但是目前版本尚不支援特殊字元(如空格等)。參考github上的網友們的解答,總結修改方法如下:1、修改目錄(我的為windows系統,使用miniconda,路徑供參考,具體則需要根據自己實際情況進行修改):檔案路徑 D:\ProgramD

自然語言處理工具pyhanlp詞性標註

Pyhanlp分詞與詞性標註的相關內容記得此前是有分享過的。可能時間太久記不太清楚了。以下文章是分享自“baiziyu”所寫(小部

jieba、自定義詞典提取高頻詞性標註及獲取的位置

準備工作 安裝anaconda環境,anaconda環境是一個強大的科學計算環境,自帶python2.7和3.4兩個版本以及很多科學計算庫 安裝完成後配置環境變數,然後在終端用pip install jieba安裝結巴分詞庫 jieba介紹 支援

Lucene.net(4.8.0) 學習問題記錄五: JIEbaLucene的結合,以及對器的思考

+= d+ ext eth reac chart rdl ret start 前言:目前自己在做使用Lucene.net和PanGu分詞實現全文檢索的工作,不過自己是把別人做好的項目進行遷移。因為項目整體要遷移到ASP.NET Core 2.0版本,而Lucene使用的版本

Python文本處理: 雲圖

wordcloud jieba python3 詞雲 分詞 ‘‘‘ import os import jieba # 分詞包 import numpy # numpy計算包 import codecs # codecs提供open方法指定打開的文件的語言編碼,它會在讀取時自動轉換為內

python使用jieba實現中文文檔去停用

分享圖片 lac lena idt center cut inpu span code 分詞工具的選擇:   現在對於中文分詞,分詞工具有很多種,比如說:jieba分詞、thulac、SnowNLP等。在這篇文檔中,筆者使用的jieba分詞,並且基於python3環境,選擇

Elasticsearch如何實現篩選功能(設定欄位不聚合操作)

0 起因 中文分詞中比較常用的分詞器是es-ik,建立索引的方式如下: 這裡我們為index personList新建了兩個欄位:name和district,注意索引名稱必須是小寫 (以下格式都是在kibana上做的) PUT /person_list { "mappings