条件随机场之CRF++源码详解-训练

  上篇的CRF++源码阅读中, 我们看到CRF++如何处理样本以及如何构造特征。本篇文章将继续探讨CRF++的源码,并且本篇文章将是整个系列的重点,会介绍条件随机场中如何构造无向图、前向后向算法、如何计算条件概率、如何计算特征函数的期望以及如何求似然函数的梯度。本篇将结合条件随机场公式推导和CRF++源码实现来讲解以上问题。原文链接

开启多线程

  我们接着上一篇encoder.cpp文件中的learn函数继续看,该函数的下半部分将会调用具体的学习算法做训练。目前CRF++支持两种训练算法,一种是拟牛顿算法中的LBFGS算法,另一种是MIRA算法, 本篇文章主要探讨LBFGS算法的实现过程。在learn函数中,训练算法的入口代码如下:

switch (algorithm) {
    case MIRA:                    //MIRA算法的入口
      if (!runMIRA(x, &feature_index, &alpha[0],
                   maxitr, C, eta, shrinking_size, thread_num)) {
        WHAT_ERROR("MIRA execute error");
      }
      break;
    case CRF_L2:                  //LBFGS-L2正则化的入口函数
      if (!runCRF(x, &feature_index, &alpha[0],
                  maxitr, C, eta, shrinking_size, thread_num, false)) {
        WHAT_ERROR("CRF_L2 execute error");
      }
      break;
    case CRF_L1:                  //LBFGS-L1正则化的入口函数
      if (!runCRF(x, &feature_index, &alpha[0],
                  maxitr, C, eta, shrinking_size, thread_num, true)) {
        WHAT_ERROR("CRF_L1 execute error");
      }
      break;
  }

runCRF函数中会初始化CRFEncoderThread数组,并启动每个线程,源码如下:

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) {
  ... //省略代码 for (size_t itr = 0; itr < maxitr; ++itr) { //开始迭代, 最大迭代次数为maxitr,即命令行参数-m
    for (size_t i = 0; i < thread_num; ++i) {
      thread[i].start();                    //启动每个线程,start函数中会调用CRFEncoderThread类中的run函数
    }

    for (size_t i = 0; i < thread_num; ++i) {
      thread[i].join();            //等待所有线程结束
    }  ... //省略代码

CRFEncoderThread类中的run函数调用gradient函数,完成一系列的核心计算。源码如下:

void run() {
    obj = 0.0;
    err = zeroone = 0;
    std::fill(expected.begin(), expected.end(), 0.0); //excepted变量存放期望
    for (size_t i = start_i; i < size; i += thread_num) {//每个线程并行处理多个句子, 并且每个线程处理的句子不相同, size是句子的个数
      obj += x[i]->gradient(&expected[0]);  //x[i]是TaggerImpl对象,代表一个句子, gradient函数主要功能: 1. 构建无向图  2. 调用前向后向算法 3. 计算期望
      int error_num = x[i]->eval();
      err += error_num;
      if (error_num) {
        ++zeroone;
      }
    }
  }

构造无向图

  我们知道条件随机场是概率图模型,几乎所有的概率计算都是在无向图上进行的。那么这个图是如果构造的呢?答案就在gradient函数第一个调用 —— buildLattice函数中。该函数完成2个核心功能,1. 构建无向图 2. 计算节点以及边上的代价,先看一下无向图的构造过程:

void TaggerImpl::buildLattice() {
  if (x_.empty()) {
    return;
  }

  feature_index_->rebuildFeatures(this); //调用该方法初始化节点(Node)和边(Path),并连接

  ... //省略代码
}
void FeatureIndex::rebuildFeatures(TaggerImpl *tagger) const {
  size_t fid = tagger->feature_id();           //取出当前句子的feature_id,上篇介绍构造特征的时候,在buildFeatures函数中会set feature_id
  const size_t thread_id = tagger->thread_id();

  Allocator *allocator = tagger->allocator();
  allocator->clear_freelist(thread_id);
  FeatureCache *feature_cache = allocator->feature_cache();
   //每个词以及对应的所有可能的label,构造节点
  for (size_t cur = 0; cur < tagger->size(); ++cur) { //遍历每个词,
    const int *f = (*feature_cache)[fid++]; //取出每个词的特征列表,词的特征列表对应特征模板里的Unigram特征
    for (size_t i = 0; i < y_.size(); ++i) { //每个词都对应不同的label, 每个label用数组的下标表示,每个特征+当前的label就是特征函数
      Node *n = allocator->newNode(thread_id); //初始化新的节点,即Node对象
      n->clear();
      n->x = cur; //当前词
      n->y = i;  //当前词的label
      n->fvector = f; //特征列表
      tagger->set_node(n, cur, i); //有一个二维数组node_存放每个节点
    }
  }
  //从第二个词开始构造节点之间的边,两个词之间有y_.size()*y_.size()条边
  for (size_t cur = 1; cur < tagger->size(); ++cur) {
    const int *f = (*feature_cache)[fid++]; //取出每个边的特征列表,边的特征列表对应特征模板里的Bigram特征
    for (size_t j = 0; j < y_.size(); ++j) {//前一个词的label有y_.size()种情况,即y_.size()个节点
      for (size_t i = 0; i < y_.size(); ++i) {//当前词label也有y_.size()种情况,即y_.size()个节点
        Path *p = allocator->newPath(thread_id);//初始化新的节点,即Path对象
        p->clear();     //add函数会设置当前边的左右节点,同时会把当前边加入到左右节点的边集合中
        p->add(tagger->node(cur - 1, j), //前一个节点
               tagger->node(cur,     i)); //当前节点
        p->fvector = f;
      }
    }
  }
}

图构造完成后, 接下来看看节点和边上的代价是如何计算的。那么代价是什么?我的理解就是特征函数值乘以特征的权重。这部分源码在buildLattice函数中,具体如下:

for (size_t i = 0; i < x_.size(); ++i) {
    for (size_t j = 0; j < ysize_; ++j) {
      feature_index_->calcCost(node_[i][j]); //计算节点的代价
      const std::vector<Path *> &lpath = node_[i][j]->lpath;
      for (const_Path_iterator it = lpath.begin(); it != lpath.end(); ++it) {
        feature_index_->calcCost(*it); //计算边的代价
      }
    }
}
//节点的代价计算函数
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];  }  \  //取每个特征以及当前节点的label,即为特征函数,且值为1,特征函数乘以权重(alpha_[*f + n->y])是代价,特征函数为1所以代价=alpha_[*f + n->y]*1,对所有代价求和
    n->cost =cost_factor_ *(T)c; } while (0) //cost_factor_是代价因子

  if (alpha_float_) {
    ADD_COST(float,  alpha_float_);
  } else {
    ADD_COST(double, alpha_); //将会在这里调用, 上一篇内容可以看到,CRF++初始化的是alpha_变量
  }
#undef ADD_COST
}//边的代价计算函数与节点类似,不再赘述

看完源码,我们举个例子来可视化一下无向图,仍然用上一篇中构造特征的那个例子。如果忘记了,出门左转回顾一下。上一个例子中有三个词,假设这三个词分别是“我”、“爱”、“你”。构建的无向图如图一所示。

图一

这个例子中,有三个词和三个label,每个label用0,1,2表示,之前我们说过用数组下标代替label。每个词有3个节点,且这三个节点的特征列表f是一样的,由于label不一样,所以他们的特征函数值不一样。由于没有bigram特征,所有边上的特征列表都是f=[-1]。大部分资料的无向图前后会加一个start节点和stop节点,加上后可以便于理解和公式推导。CRF++源码中没加,所以我们这里就没有表示。在这里node_[0][0]对应就是最左上角的节点,代表“我”这个词label为0的节点。我们再看一下node_[0][0]这个节点的代价如何计算的,node_[0][0]的cost = alpha_[0 + 0] + alpha_[3 + 0] = alpha_[0] + alpha_[3],由于alpha_第一次节点初始化为0,所以cost=0。其余节点和边计算方法类似。

前向-后向算法

 有了无向图,我们就可以在图上进行前向-后向算法。利用前向-后向算法,很容易计算标记序列在位置i(词)的label是yi的条件概率,以及在位置i-1(前一个词)与位置i(当前词)的label是yi-1与yi的条件概率。进行CRF++源码阅读之前先看一下条件随机场矩阵的表示形式。对一个句子的每一个位置(单词) i=1,2,…,n+1,定义一个 m 阶矩阵(m 是标记 yi 取值的个数),i=0代表start节点, i=n+1代表stop节点。

\begin{aligned}  M_i(x) &= \left \{  M_i(y_{i-1},y_i|x)\right \} \\ M_i(y_{i-1},y_i|x)&= \exp  \left \{ W_i(y_{i-1} ,y_i|x)\right \}\\ W_i(y_{i-1},y_i|x)&= \sum_{k=1}^Kw_k \cdot f_k(y_{i-1},y_i,x,i) \end{aligned}

\begin{align} f_k(y_{i-1},y_i,x,i) = \left \{ \begin{aligned}  &t_k(y_{i-1},y_i,x,i), \ \ k = 1,2,...,K_1 \\ &s_t(y_i,x,i), \ \ \  \ \ \ \ \  \ \  k = K_1 + l ; l = 1,2,...,K_2 \end{aligned}\right. \end{align}

W的解释:当前节点代价 + 与该节点相连的一条边的代价。

节点之间的转移概率,用矩阵的形式表现如下:

\begin{aligned} M_1(x) &= \begin{bmatrix} M_1(0,0|x) & M_1(0,1|x)  &M_1(0,2|x) \\ 0 & 0  &0 \\ 0 & 0  &0  \end{bmatrix} \\ \\ M_2(x) &=\begin{bmatrix} M_2(0,0|x) & M_2(0,1|x) & M_2(0,2|x)\\  M_2(1,0|x) & M_2(1,1|x) & M_2(1,2|x)\\  M_2(2,0|x) & M_2(2,1|x) & M_2(2,2|x) \end{bmatrix} \\ \\ M_i(x) \ &\mathbf{has \ the \ same \ form \  with} \ M_2(X), \ i = 3,...,n\\ \\ M_{n+1}(x) &=\begin{bmatrix}  1 & 0 & 0 \\   1 & 0 & 0  \\   1&0&0  \end{bmatrix} \\ \end{aligned}

Mi 的解释:以 \begin{aligned} M_2(2,1|x) \end{aligned} 为例,代表第2个位置(第2个词)label是1,前一个词label是2,计算Wi,再取exp后的值。接下来,我们看一下用矩阵表示的前向-后向算法。

对i = 0, 1, 2, ... n+1, 定义前向向量αi(x),对于起始状态i = 0:

\begin{align} \alpha_0(y|x) = \left \{ \begin{aligned}  &1, \ \ y = start \\ &0, \ \ else \end{aligned}\right. \end{align}

对于之后的状态 i=1,2,...,n+1,递推公式为:

\begin{aligned} a_i^T(y_i|x) = a^T_{i-1}(y_{i-1}|x)M_i(y_{i-1},y_i|x) \end{aligned}

假设label个数是m,α是m*1的列向量,Mi(yi-1,yi|x) 是m*m的矩阵,α解释:前一个单词每个节点的α分别乘以(与当前节点相连的边的代价 + 当前节点的代价),再求和 。

同样,后向算法β计算, 对于i = 0, 1, 2, ..., n+1,定义后向向量βi(x):

\begin{align} \beta_{n+1}(y_{n+1}|x) = \left \{ \begin{aligned}  &1, \ \ y_{n+1} = stop \\ &0, \ \ else \end{aligned}\right. \end{align}

向前递推公式如下:

\begin{aligned} \beta_i(y_i|x) = M_i(y_i,y_{i+1}|x)\beta_{i+1}(y_{i+1}|x) \end{aligned}

βi是m*1的列向量, Mi(yi,yi+1|x)是m*m的矩阵。β解释:(当前词与下一个词连接的边的代价 + 下一个词的代价) 分别乘以下一个词的β,再相加。

由前向-后向向量定义不难得到:

\begin{aligned} Z(x) = a_n^T(x) \cdot \mathbf{1} = \mathbf{1}^T \cdot \beta_1(x) \end{aligned}

需要注意一下,矩阵表示形式的代价是对特征函数乘以权重加和后再取exp的值, 而上面的CRF++ calcCost函数中并没有取exp值。

接下来继续看下α和β在CRF++中是如何计算的。在gradient函数中调用的forwardbackward函数即是这部分的核心代码,具体如下:

void TaggerImpl::forwardbackward() {
  if (x_.empty()) {
    return;
  }

  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();
    }
  }

  Z_ = 0.0;
  for (size_t j = 0; j < ysize_; ++j) { //计算Z(x)
    Z_ = logsumexp(Z_, node_[0][j]->beta, j == 0);
  }

  return;
}
void Node::calcAlpha() {
  alpha = 0.0;
  for (const_Path_iterator it = lpath.begin(); it != lpath.end(); ++it) { //这里遍历当前节点的左边(path)的集合, 对应的就是Mi(yi-1,yi|x)矩阵中的某一列
    alpha = logsumexp(alpha,
                      (*it)->cost +(*it)->lnode->alpha,
                      (it == lpath.begin()));  //函数里面回取exp,因此边的代价 + 上一个节点的α,会转化成相乘,取完exp还会再取log,取log为了方式直接exp导致的溢出
  }
  alpha += cost; //统一加上当前节点的代价, Mi(yi-1,yi|x)每列中每个元素都加了当前节点的代价, 只不过CRF++是在后面统一加上
}
void Node::calcBeta() { //与上面类似
  beta = 0.0;
  for (const_Path_iterator it = rpath.begin(); it != rpath.end(); ++it) {
    beta = logsumexp(beta,
                     (*it)->cost +(*it)->rnode->beta,
                     (it == rpath.begin()));
  }
  beta += cost; //这里需要注意,在矩阵的推导过程中,没有加当前节点的代价,但是CRF++里面加了, 后续我们会看到有一个减当前节点代价的一段代码
}
// log(exp(x) + exp(y));
//    this can be used recursivly
// e.g., log(exp(log(exp(x) + exp(y))) + exp(z)) =
// log(exp (x) + exp(y) + exp(z))// 这部分取log的操作是为了防止直接取exp溢出,具体的解释以及推导参考 计算指数函数的和的对数inline double logsumexp(double x, double y, bool flg) {
  if (flg) return y;  // init mode
  const double vmin = std::min(x, y);
  const double vmax = std::max(x, y);
  if (vmax > vmin + MINUS_LOG_EPSILON) {
    return vmax;
  } else {
    return vmax + std::log(std::exp(vmin - vmax) + 1.0);
  }
}

阅读完上述代码会发现,这里的α计算除了没有对最终结果取exp以外,跟上面矩阵推导的α计算是一样的。可以利用矩阵方法和CRF++的算法具体算一下α或β的值,对比一下理解的会更深, 这个过程并不复杂。

概率计算

  有了α和β,就可以进行条件概率和期望的计算。一个句子在位置i的label是yi的条件概率,以及在位置i-1与位置i标记为yi-1与yi的概率:

\begin{aligned}  P(Y_i= y_i|x) &= \frac{a_i^T(y_i|x) \beta_i(y_i|x)}{Z(x)} \\ P(Y_{i-1} = y_{i-1} ,Y_i= y_i|x) &=\frac{a_{i-1}^T(y_{i-1}|x)M_i(y_{i-1},y_i|x)\beta_i(y_i|x)}{Z(x)} \end{aligned}

第一个式子可以说是节点的概率,第二个式子是节点之间边的概率。有了条件概率,就可以计算特征函数f关于条件分布 P(Y|X) 的数学期望是:

\begin{aligned}  E_{p(Y|X)}[f_k] &= \sum_yP(y|x)f_k(y,x) \\ &=\sum_{i=1}^{n+1}\sum_{y_{i-1}\ y_i}f_k(y_{i-1},y_i,x,i) \frac{a_{i-1}^TM_i(y_{i-1},y_i|x)\beta_i(y_i|x)}{Z(x)} \end{aligned}

计算特征函数的期望是因为后续计算梯度的时候会用到。这里,如果fk是unigram特征(状态特征),对应的条件概率是节点的概率, 如果是bigram特征(转移特征),条件概率就是边的概率。继续看下CRF++中是如何计算条件概率和特征函数的期望的,代码在gradient函数中:

for (size_t i = 0;   i < x_.size(); ++i) { //遍历每一个节点的,遍历计算每个节点和每条边上的特征函数,计算每个特征函数的期望
    for (size_t j = 0; j < ysize_; ++j) {
      node_[i][j]->calcExpectation(expected, Z_, ysize_);
    }
}
void Node::calcExpectation(double *expected, double Z, size_t size) const { //状态特征的期望
  const double c = std::exp(alpha + beta - cost - Z); //这里减去一个多余的cost,剩下的就是上面提到的节点的概率值 P(Yi=yi | x),这里已经取了exp,跟矩阵形式的计算结果一致
  for (const int *f = fvector; *f != -1; ++f) {
    expected[*f + y] += c;           //这里会把所有节点的相同状态特征函数对应的节点概率相加,特征函数值*概率再加和便是期望。由于特征函数值为1,所以直接加概率值
  }
  for (const_Path_iterator it = lpath.begin(); it != lpath.end(); ++it) { //转移特征的期望
    (*it)->calcExpectation(expected, Z, size);
  }
}
void Path::calcExpectation(double *expected, double Z, size_t size) const {
  const double c = std::exp(lnode->alpha + cost + rnode->beta - Z); //这里计算的是上面提到的边的条件概率P(Yi-1=yi-1,Yi=yi|x),这里取了exp,跟矩阵形式的计算结果一致
  for (const int *f = fvector; *f != -1; ++f) {
    expected[*f + lnode->y * size + rnode->y] += c; //这里把所有边上相同的转移特征函数对应的概率相加
  }
}

至此,CRF++中前后-后向算法、条件概率计算以及特征函数的期望便介绍完毕,接下来看看如何计算似然函数值和梯度。

计算梯度

  条件随机场的训练,我们这里主要看CRF++中应用的LBFGS算法。先做简单的推导, 再结合实际的CRF++源码去理解。条件随机场模型如下:

\begin{aligned} P_w(y|x) = \frac{\exp \left \{ \sum_{k=1}^K w_kf_k(x,y)\right \}}{ \sum_y  \left \{ \exp \sum_{i=1}^n w_if_i(x,y)\right \}} \end{aligned}

\begin{aligned} f_k(y,x) = \sum_{i=1}^nf_k(y_{i-1},y_i,x,i), k=1,2,...,K \end{aligned}

训练函数的对数似然如下:

\begin{aligned}  L(w) &=  \log \prod_{t}P_w(y^t|x^t) \\ &= \sum_{t} \log P_w(y^t|x^t) \\ &= \sum_{t} \left \{ \sum_{k=1}^Kw_kf_k(y^t,x^t)-\log Z_w(x) \right \}   \end{aligned}

t代表所有的训练样本, 一般使用m来表示,但是上面已经把m给用了, 为了避免歧义, 我们用t来表示训练样本。我们求似然函数最大值来求解最优参数w,同时也可以对似然函数加负号,通过求解最小值来求最优的w。这里我们与CRF++保持一致,将似然函数取负号,再对wj求导,推导如下:

\begin{aligned}  \frac{\partial L(w)}{\partial w_j} &= \sum_{t} \left \{ \frac{\sum_y  \left \{ f_i(x^t,y^t)\exp \sum_{i=1}^K w_if_i(x^t,y)\right \}}{Z_w(x)} - f_j(y^t,x^t) \right \} \\ &= \sum_{t} \left \{ \sum_y P(y|x^t)f_j(y, x^t) - f_j(y^t,x^t) \right \} \\ &= \sum_{t} \left \{ E_{P(y|x)}[f_j(y,x)] - f_j(y^t,x^t) \right \} \end{aligned}

对于一个句子来说,特征函数的期望减去特征函数真实值就是我们要计算的梯度,Σ代表对所有句子求和得到最终的梯度。接下来看下CRF++中是如何实现的,代码还是在gradient函数中:

for (size_t i = 0;   i < x_.size(); ++i) { //遍历每一个位置(词)
    for (const int *f = node_[i][answer_[i]]->fvector; *f != -1; ++f) { //answer_[i]代表当前样本的label,遍历每个词当前样本label的特征,进行减1操作,遍历所有节点减1就相当于公式中fj(y,x)
      --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;
      }
    }
  }
viterbi();  // call for eval() 调用维特比算法做预测,为了计算分类错误的次数,算法详细内容下篇介绍

return Z_ - s ;  //返回似然函数值,看L(w)推导的最后一步,大括号内有两项,其中一项是logZw(x),我们知道变量Z_是没有取exp的结果,我们要求这一项需要先对Z_取exp,取exp再取log相当于还是Z_,因此 logZw(x) = Z_
                 //再看另一项,是对当前样本代价求和,正好这一项是没有取exp的因此该求和项就等于s, 之前说过CRF++是对似然函数取负号,因此返回Z_ - s

至此,一个句子的似然函数值和梯度就计算完成了。公式的Σt 是对所有句子求和,CRF++的求和过程是在run函数调用gradient函数结束后由线程内汇总,然后所有线程结束后再汇总。runCRF函数剩下的代码便是所有线程完成一轮计算后的汇总逻辑,如下:

for (size_t i = 1; i < thread_num; ++i) { //汇总每个线程的数据
      thread[0].obj += thread[i].obj; //似然函数值
      thread[0].err += thread[i].err;
      thread[0].zeroone += thread[i].zeroone;
    }

    for (size_t i = 1; i < thread_num; ++i) {
      for (size_t k = 0; k < feature_index->size(); ++k) {
        thread[0].expected[k] += thread[i].expected[k]; //梯度值求和
      }
    }

    size_t num_nonzero = 0;
    if (orthant) {   // L1 根据L1或L2正则化,更新似然函数值
      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 { //L2
      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;
      }
    }

    ...省略代码
    if (lbfgs.optimize(feature_index->size(),
                       &alpha[0],
                       thread[0].obj,
                       &thread[0].expected[0], orthant, C) <= 0) { //传入似然函数值和梯度等参数,调用LBFGS算法
      return false;
    }

最终调用LBFGS算法更新w,CRF++中的LBFGS算法最终是调用的Fortran语言编译后的C代码,可读性比较差,本篇文章暂时不深入介绍。至此,一次迭代的计算过程便介绍完毕。

总结

  通过这篇文章的介绍,已经了解到了CRF++如何构建无向图、如何计算代价、如何进行前向-后向算法、如何计算特征函数的期望以及如何计算梯度。写这篇文章耗时最长,花了整整一天的时间。力求这篇文章通俗易懂,理论结合实践。希望能够把条件随机场这个比较枯燥的算法诠释好。文中有可能仍然有表达不通顺或者表达不通俗的地方,甚至可能会有表达错误的地方,如果存在上述问题欢迎评论区留言,我将第一时间更新。

原文地址:https://www.cnblogs.com/duma/p/10325724.html

时间: 2024-11-01 12:41:45

条件随机场之CRF++源码详解-训练的相关文章

条件随机场之CRF++源码详解-预测

这篇文章主要讲解CRF++实现预测的过程,预测的算法以及代码实现相对来说比较简单,所以这篇文章理解起来也会比上一篇条件随机场训练的内容要容易. 预测 上一篇条件随机场训练的源码详解中,有一个地方并没有介绍. 就是训练结束后,会把待优化权重alpha等变量保存到文件中,也就是输出到指定的模型文件.在执行预测的时候会从模型文件读出相关的变量,这个过程其实就是数据序列化与反序列化,该过程跟条件随机场算法关系不大,因此为了突出重点源码解析里就没有介绍这部分,有兴趣的朋友可以自己研究一下. CRF++预测

条件随机场之CRF++源码详解-开篇

介绍 最近在用条件随机场做切分标注相关的工作,系统学习了下条件随机场模型.能够理解推导过程,但还是比较抽象.因此想研究下模型实现的具体过程,比如:1) 状态特征和转移特征具体是什么以及如何构造 2)前向后向算法具体怎么实现 等等.那么,想要深入了解一个算法比较好的方式就是阅读现有的开源项目.阅读好的开源项目不但可以深入理解原理,还可以学习一些工程实践的经验.我阅读条件随机场的开源项目是CRF++.我在阅读CRF++源码的时候走过一些弯路也积累了一些经验,想把这个过程和经验总结下来,希望能够对正在

Java concurrent AQS 源码详解

一.引言 AQS(同步阻塞队列)是concurrent包下锁机制实现的基础,相信大家在读完本篇博客后会对AQS框架有一个较为清晰的认识 这篇博客主要针对AbstractQueuedSynchronizer的源码进行分析,大致分为三个部分: 静态内部类Node的解析 重要常量以及字段的解析 重要方法的源码详解. 所有的分析仅基于个人的理解,若有不正之处,请谅解和批评指正,不胜感激!!! 二.Node解析 AQS在内部维护了一个同步阻塞队列,下面简称sync queue,该队列的元素即静态内部类No

深入Java基础(四)--哈希表(1)HashMap应用及源码详解

继续深入Java基础系列.今天是研究下哈希表,毕竟我们很多应用层的查找存储框架都是哈希作为它的根数据结构进行封装的嘛. 本系列: (1)深入Java基础(一)--基本数据类型及其包装类 (2)深入Java基础(二)--字符串家族 (3)深入Java基础(三)–集合(1)集合父类以及父接口源码及理解 (4)深入Java基础(三)–集合(2)ArrayList和其继承树源码解析以及其注意事项 文章结构:(1)哈希概述及HashMap应用:(2)HashMap源码分析:(3)再次总结关键点 一.哈希概

Android View 事件分发机制源码详解(View篇)

前言 在Android View 事件分发机制源码详解(ViewGroup篇)一文中,主要对ViewGroup#dispatchTouchEvent的源码做了相应的解析,其中说到在ViewGroup把事件传递给子View的时候,会调用子View的dispatchTouchEvent,这时分两种情况,如果子View也是一个ViewGroup那么再执行同样的流程继续把事件分发下去,即调用ViewGroup#dispatchTouchEvent:如果子View只是单纯的一个View,那么调用的是Vie

Android编程之Fragment动画加载方法源码详解

上次谈到了Fragment动画加载的异常问题,今天再聊聊它的动画加载loadAnimation的实现源代码: Animation loadAnimation(Fragment fragment, int transit, boolean enter, int transitionStyle) { 接下来具体看一下里面的源码部分,我将一部分一部分的讲解,首先是: Animation animObj = fragment.onCreateAnimation(transit, enter, fragm

Spring IOC源码详解之容器依赖注入

Spring IOC源码详解之容器依赖注入 上一篇博客中介绍了IOC容器的初始化,通过源码分析大致了解了IOC容器初始化的一些知识,先简单回顾下上篇的内容 载入bean定义文件的过程,这个过程是通过BeanDefinitionReader来完成的,其中通过 loadBeanDefinition()来对定义文件进行解析和根据Spring定义的bean规则进行处理 - 事实上和Spring定义的bean规则相关的处理是在BeanDefinitionParserDelegate中完成的,完成这个处理需

Spring IOC源码详解之容器初始化

Spring IOC源码详解之容器初始化 上篇介绍了Spring IOC的大致体系类图,先来看一段简短的代码,使用IOC比较典型的代码 ClassPathResource res = new ClassPathResource("beans.xml"); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDe

IntentService源码详解

IntentService可以做什么: 如果你有一个任务,分成n个子任务,需要它们按照顺序完成.如果需要放到一个服务中完成,那么IntentService就会使最好的选择. IntentService是什么: IntentService是一个Service(看起来像废话,但是我第一眼看到这个名字,首先注意的是Intent啊.),所以如果自定义一个IntentService的话,一定要在AndroidManifest.xml里面声明. 从上面的"可以做什么"我们大概可以猜测一下Inten