清华大学的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中对于的权重的索引,即我们要查找某个特征feature在fl_weights对于的权重,首先需要查找dat中feature对于的索引index,得到index后我们便可以得到feature对于的权重为fl_weights[index]。那么dat是怎么索引的呢,如果总的features个数为N,那么dat的大小也要为N,当通常随着训练数据的增大,生成的特征数目也会很大,这dat就需要花费很大的内存,那么thulac中是怎么做的呢?
首先说一下压缩感知机算法中特征的生成,对于输入句子s中的每个字wi,会根据其前后2个字生成7个特征,这7个特征分别为wi-2wi-1,wi-1,wi-1wi,wi,wiwi+1,wi+1,wi+1wi+2。Thulac中在这7个特征后面添加了一个separator=’ ’即空格;对于分词,我们需要预测每个字属于B,M,E,S的权重,因此,最后需要生成每个特征对于于B,M,E,S的特征,所以对每个句子中的每个字实际会生成7×4=28个特征。关于thulac中特征生成C++代码如下:
inline void feature_generation(RawSentence& seq,
Indexer& indexer,
Counter* 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;i0)?(seq[i-1]):(SENTENCE_BOUNDARY);
left2=((i-2)>=0)?(seq[i-2]):(SENTENCE_BOUNDARY);
right=((i+1)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& lexicon,int no_prefix=0){
std::sort(lexicon.begin(),lexicon.end(),&compare_words);
int size=(int)lexicon.size();
std::vector 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<
得到dat后,那么对于输入句子查找中每个字对应的特征索引,对于只含有一个字的特征的索引保存在uni_base,相邻两个字的特征对应索引保存在bi_base,,例如对于相邻两个字ch1,ch2,uni_base,bi_base特征为uni_base[ch1]=dat[ch1].base+separator,bi_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;ivalues[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.valuevalue)){
best.value=tmp->value;
best.node_id=i;
best.label_id=j;
}
}
}
//std::cout<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
参数更新,压缩感知机中,预测值与输入值相等,则对应的特征权重+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;imodel->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为每个标签的状态转移矩阵。