# CRF++源码分析——模型的加载#

来源:互联网 发布:股票那个软件最好 编辑:程序博客网 时间:2024/06/02 01:58

CRF++源码分析——模型的加载

接触CRF++有一段时间了,也看了一些CRF++的一些源码,再次打算对CRF++的源码进行分析,整理下以前对CRF++的理解。对CRF++的分析主要分为三部分:
1-模型的训练
2-模型的加载
3-任务的应用

由于模型训练的分析比较复杂,所以我打算放在最后来分析,按2->3->1的顺序来讲。关于CRF++的一些基本知识不再单独讲解,不熟悉的可以参考CRF++自带的说明文档,或者和说明文档比较接近的这篇文章来了解!本文以CRF++中自带样例中的一个(/example/chunking下)来分析。

由于知识储备有限,分析的有问题的地方还请见谅和指教!


CRF++在执行具体任务(对各种序列打标签)之前会将学习到的模型加载到自己的内存中,本模块主要分析CRF++加载模型的过程!

1、特征值

为了下面的讲解方便,我们引入特征值的概念。
我们知道对于Unigram类型的特征模板,产生的特征函数的数量是L.N,对于Bigram类型的特征模板,产生特征函数的数量是L.L.N(其中L是标注集中类别数量,N是从模板中扩展处理的字符串种类)。我们在特征模板与特征函数之间增加一层概念。
设x是特征模板t扩展处理的字符串集X(t)(上边的N表示的是本集合的大小\势)元素,我们将x应用于T(即将T所指定的位置的字符串指定为x)得到T的一个特征值。我们有(Y表示标注集,Y的大小为L):

  1. 特征值是包括特征模板与对应位置字符串组成的二元组,特征模板t生成的特征值集合为
    V(t)={(t,x)|xX(t)}|V(t)|=|X(t)|=N
  2. Unigram特征函数是包括特征模板,对应位置字符串和标注组成的三元组,U特征模板t生成的特征函数的集合
    F(t)={(t,x,y)|xX(t),yY}={(v,y)|vV(t),yY},|F(t)|=|V(t)|.|Y|=NL
    .
  3. Bigram特征函数是包括特征模板,对应位置字符串和两个标注组成的四元组,U特征模板t生成的特征函数的集合
    F(t)={(t,x,y1,y2)|xX(t),y1Y,y2Y}={(v,y1,y2)|vV(t),y1Y,y2Y}
    ,
    |F(t)|=|V(t)|.|Y|.|Y|=N.L.L
    .
    注意,对于观察序列中的某个观察单位来说,特征模板指定的位置可能会超出序列的范围。例如,有观察序列(x1,x2,);对x1,x2来讲,位置%x[-2,0]分别向前超出了2个,1个单位,为了保证特征值的完备性,CRF++用_B1,_B2,分别表示观察序列前1,2,个位置的观测值。同理,用_B+1,_B+2,分别表示观察序列后1,2,个位置的观测值。事实上,设一个模板t=U00:%x[-3,0],那么_B1,_B2,_B3X(t)(假设训练集中存在长度大于等于3的序列,这种假设一般总会成立的)。

2、模型文件

如果我们对用CRF++训练模型的时候带上(-t)参数,那么训练程序在产生一个二进制的模型文件的同时会产生一个txt格式的可视化的模型文件model.txt。对CRF++来讲,两种格式的文件所存储的数据是等价的,而且CRF++加载的模型一般是二进制的,txt格式的模型文件是为方便我们分析。既然两种格式的文件是等价的,我们便通过可以分析txt格式的模型来分析二进制模型文件的加载过程。
生成可视化模型的训练参数示例

模型文件展示

模型文件中给出了五个集合(列表),在model.txt中以空行隔开,按顺序从前往后分别为参数集合、标注列表Y、特征模板集合T、特征值集合V,特征函数的权重列表W。这五个中以集合结尾的表示集合内部在文件中的先后顺序是对CRF是无关的,以列表结尾的表示列表在文件中的先后位置是十分重要的。

(1)参数集

模型文件中参数集展示
上图为我们的样例训练出的模型文件中的参数集,几个参数的意义分别为:
1. version 表示此模型的版本,应该是用来检查模型文件版本与CRF++版本的一致性。
2. cost-factor: 计算代价函数时会乘以此因子,应该和训练时的参数c没有关系。还没有对这个参数跟踪,理解不好,以后会补上。
3. maxid:特征函数与其权重是一一对应的,因此此参数表示特征函数,也就是特征函数权重列表的大小。
4. xsize:xsize 特征的列数。本样本的训练语料为三列,其中前两列为特征列,第三列为标注列。如下图所示:训练语料样例

(2)标注列表Y

标注列表展示
上图为本样例模型文件的标注列表,即为训练语料第三列所有标注的集合。可看出此集合的容量为14。因为CRF++系统会用到标注在列表中位置信息,因此标注在列表中的位置是重要的。

(3)特征模板集合T

特征模板展示
上图为模型文件中的特征模板集合,其即为我们训练时在模板文件中定义的特征模板。其中模板B00没有关联特征列的信息,只建立了前后标注的联系,因此其对应的特征值只有一个。

(4)特征值集合V

这里写图片描述

这里写图片描述
上面两个图是我在样例模型文件中截取的部分片段,可以看到特征模板B00的特征值只有一个。特征值分为两部分,空格前是一个整形值id,空格后表示一个具体的特征值。
特征值的顺序对CRF++是没有影响的,如果训练中没有设定-f参数的话,此集合会按空格后的特征值的顺序出现在文件中;因为按id排序便于我们分析数据,我们要对此集合按id重新排序。一个更为简单的方法是训练时增加-f 的参数,此时会按id的大小在文件中出现,而且加了-f参数后并不会对我们的分析产生影响。上图是按id顺序出现的特征值。
一个让人兴奋的地方是相邻Bigram类型的特征值的id差196=1414=|Y|.|Y|。相邻Unigram类型的特征值的id差14=|Y|。这是因为每个特征值对应的id为特征函数(权重)在特征函数权重列表中的起始位置,而且这两种类型的特征值产生的特征函数个数分别为|Y|.|Y|与|Y|。

(5)特征函数的权重列表W

特征函数的权重列表
特征函数的权重与特征函数是一一对应的,参数集合中的maxid给出了此列表的大小。我们用p1(f)表示特征函数f的权重在权重列表中的位置(从1开始),用p2(y)表示标注y在标注列表中的位置(从1开始),id(v)表示在特征值集合中特征值v对应的id,我们有

  • 对于Unigram类型的特征函数f=(v,y),有p1(f)=id(v)+p2(y).
  • 对于Unigram类型的特征函数f=(v,y1,y2),有p1(f)=id(v)+p2(y1)p2(y2).

3、模型加载

CRF++在执行具体任务之前会将模型文件加载到内存中对应的数据结构中,模型加载接口的参数有四个:

long short default description model m 0 set FILE for model file,模型文件路径 nbest n 0 output n-best results,得到前n个最好的额标注结果,0表示只返回一个最优 verbose v 0 set INT for verbose level,通过设置此参数可以获得对应标注的概率 cost-factor c 1.0 set cost factor,计算代价的时候会乘此因此,不过与模型文件中的同名参数是一个怎样的关系还没有深究。

本文章分析的所有的源码都在CRF++的根目录下。
模型加载的入口在tagger.cpp文件中,入口函数有几个可以用,但是核心部分都是调用相同的函数块,因此分析一个足矣。其中一个如下

166 bool ModelImpl::open(const Param &param) {167   nbest_ = param.get<int>("nbest");168   vlevel_ = param.get<int>("verbose");169   const std::string model = param.get<std::string>("model");170   feature_index_.reset(new DecoderFeatureIndex);171   if (!feature_index_->open(model.c_str())) {172     WHAT << feature_index_->what();173     feature_index_.reset(0);174     return false;175   }176   const double c = param.get<double>("cost-factor");177   feature_index_->set_cost_factor(c);178   return true;179 }

除第171行外其他部分的功能是获取上面提到的4个传入参数,CRF++会将模型文件加载到DecoderFeatureIndex类型对象中,下面我们分析函数
bool DecoderFeatureIndex::open(const char *model_filename),其对应带面段如下

192 bool DecoderFeatureIndex::open(const char *model_filename) {193   CHECK_FALSE(mmap_.open(model_filename)) << mmap_.what();194   return openFromArray(mmap_.begin(), mmap_.file_size());195 }

其中第193行是将二进制的模型文件按字节加载到数据结构mmap_中,此二进制数据的解析实在第194行,我们来分析函数
bool DecoderFeatureIndex::openFromArray(const char *ptr, size_t size),模型文件解析的核心逻辑便在此代码中。

197 bool DecoderFeatureIndex::openFromArray(const char *ptr, size_t size) {198   unsigned int version_ = 0;199   const char *end = ptr + size;200   read_static<unsigned int>(&ptr, &version_);201 202   CHECK_FALSE(version_ / 100 == version / 100)203       << "model version is different: " << version_204       << " vs " << version;205   int type = 0;206   read_static<int>(&ptr, &type);207   read_static<double>(&ptr, &cost_factor_);208   read_static<unsigned int>(&ptr, &maxid_);209   read_static<unsigned int>(&ptr, &xsize_);210 211   unsigned int dsize = 0;212   read_static<unsigned int>(&ptr, &dsize);213 214   unsigned int y_str_size;215   read_static<unsigned int>(&ptr, &y_str_size);216   const char *y_str = read_ptr(&ptr, y_str_size);217   size_t pos = 0;218   while (pos < y_str_size) {219     y_.push_back(y_str + pos);220     while (y_str[pos++] != '\0') {}221   }222 223   unsigned int tmpl_str_size;224   read_static<unsigned int>(&ptr, &tmpl_str_size);225   const char *tmpl_str = read_ptr(&ptr, tmpl_str_size);226   pos = 0;227   while (pos < tmpl_str_size) {228     const char *v = tmpl_str + pos;229     if (v[0] == '\0') {230       ++pos;231     } else if (v[0] == 'U') {232       unigram_templs_.push_back(v);233     } else if (v[0] == 'B') {234       bigram_templs_.push_back(v);235     } else {236       CHECK_FALSE(true) << "unknown type: " << v;237     }238     while (tmpl_str[pos++] != '\0') {}239   }240 241   make_templs(unigram_templs_, bigram_templs_, &templs_);242 243   da_.set_array(const_cast<char *>(ptr));244   ptr += dsize;245 246   alpha_float_ = reinterpret_cast<const float *>(ptr);247   ptr += sizeof(alpha_float_[0]) * maxid_;248 249   CHECK_FALSE(ptr == end) << "model file is broken.";250 251   return true;252 }

在分析此函数前,我们先看一下函数中多次调用的一个函数块

 17 const char *read_ptr(const char **ptr, size_t size) { 18   const char *r = *ptr; 19   *ptr += size; 20   return r; 21 } 22  23 template <class T> static inline void read_static(const char **ptr, 24                                                   T *value) { 25   const char *r = read_ptr(ptr, sizeof(T)); 26   memcpy(value, r, sizeof(T)); 27 }

*ptr指向一个T类型的对象,此方法的功能便是将*ptr地址的数据反序列化到T类型对象value中,并使指针*ptr后移。下面来分析openFromArray函数。

事实上,模型文件的加载便是按我们上文提到的各个部分的顺序依次解析。

(1) 参数集和的解析

对应于198-210行,分别从二进制数据结构mmp_中加载参数集中的各个参数。

(2) 标注列表Y的解析

对应于214-222行,首先,216行读取到整个标注类表串;然后218-221行按字符\0分开各个标注,并读取到属性y_(按在文件中的顺序)中。

(3) 特征模板集合T的解析

对应于223-241行,同样,首先读取这个的模板集合;然后,按\0分开,将Unigram类型模板读取到属性unigram_templs_中,将Bigram类型模板读取到属性bigram_templs_中;最后在241行将两种类型的模板合并到属性templs_中。

(4) 特征值集合V的解析

对应于243-245行,在211-212行读取的参数dsize其实是模型文件中特征值集合大小,然后通过set_array方法将特征值到对应的特征函数权重起始id的映射存入到属性da_(DoubleArrayImpl类型)中。为了提高搜索速度和空间利用率,CRF++采用Double Array Trie的结构存储此映射,因此二进制文件中是此结构的序列化(也是一个数组)。

(5) 特征函数权重列表W的解析

对应于246-247行,每个特征函数对应一个float(或double)类型的权重,将模型文件中的权重列表按顺序解析到数组alpha_float_中。

至此,CRF++便完成了模型加载。接下来便可以用加载到的这些统计信息来执行具体的任务了。

1 0
原创粉丝点击