条件随机场CRF之从公式到代码

前言

基础的理论推导我就不再搬运了,网上有很多大大们写的都很好,但是我发现文章基本分成了两类,一类讲理论讲的特别好,但是缺少了与实际代码的结合;一类讲实践,主要是如何使用顺带提一下公式,主要是默认读者已经对公式烂熟于心了。所以我想做个桥梁,结合公式和代码实现把CRF捋一遍。对于理论还不是很熟的童鞋请参考文章最后的引用链接,讲的非常详细。文章中使用的代码来自于pytorch官方文档:https://pytorch.org/tutorials/beginner/nlp/advanced_tutorial.html

CRF实现细节

  • 生成模型和判别式模型
    简单一点理解,生成模型就是根据数据集去建立联合概率分布 P ( X , Y ) P(X,Y) P(X,Y)的模型,比如贝叶斯模型、HMM;判别式模式就是根据数据集中X和Y的对应关系去建立一个超平面用于分类,判别式模型的目的就是为了分类,比如SVM、神经网络。切记,切记HMM是生成模型而CRF是判别式模型! 更详细的解释请参考https://zhuanlan.zhihu.com/p/33397147里的2.2节。

  • 无向图的概率计算
    设有无向图,一般指马尔科夫网络,可以用因子分解将 P ( Y ) P(Y) P(Y) 写成若干个联合概率的乘积,这里的“若干个联合概率”是指将图分解成若干个最大团的乘积。比如下图中可以分解成两个最大团 ( x 1 , x 3 , x 4 ) 和 ( x 2 , x 3 , x 4 ) (x_1, x_3, x_4)和(x_2, x_3, x_4) (x1,x3,x4)(x2,x3,x4),所以:
    P ( Y ) = 1 Z ( x ) ∏ c = 1 2 ψ c ( c ) = 1 Z ( x ) ψ 1 ( x 1 , x 3 , x 4 ) ⋅ ψ 2 ( x 1 , x 3 , x 4 ) P(Y)=\frac{1}{Z(x)}\prod_{c=1}^{2}\psi_c(c)=\frac{1}{Z(x)}\psi_1(x_1, x_3, x_4)\cdot\psi_2(x_1, x_3, x_4) P(Y)=Z(x)1c=12ψc(c)=Z(x)1ψ1(x1,x3,x4)ψ2(x1,x3,x4)
    其中 Z ( x ) = ∑ Y ∏ c ψ c ( c ) Z(x)= \sum_{Y}\prod_{c}\psi_c (c) Z(x)=Ycψc(c)是归一化因子,这里可以参考softmax公式的分母。
    在这里插入图片描述

  • ψ c ( c ) \psi_c(c) ψc(c)的定义
    ψ c ( c ) \psi_c(c) ψc(c)是一个最大团 C C C上随机变量们的联合概率,一般取指数函数:
    ψ c ( Y c ) = e − E ( Y c ) = e ∑ k λ k f k ( c , y , x ) \psi_c (Y_c)=e^{-E(Y_c)}=e^{\sum_{k}\lambda_k f_k(c,y,x)} ψc(Yc)=eE(Yc)=ekλkfk(c,y,x),先不管 λ k f k ( c , y , x ) \lambda_k f_k(c,y,x) λkfk(c,y,x)是个什么鬼,把这一坨带入到上面的计算 P ( Y ) P(Y) P(Y)的公式中:
    P ( Y ) = 1 Z ( x ) ∏ c e ∑ k λ k f k ( c , y , x ) = 1 Z ( x ) e ∑ c ∑ k λ k f k ( c , y , x ) P(Y)=\frac{1}{Z(x)}\prod_{c}e^{\sum_{k}\lambda_k f_k(c,y,x)}=\frac{1}{Z(x)}e^{\sum_c\sum_{k}\lambda_k f_k(c,y,x)} P(Y)=Z(x)1cekλkfk(c,y,x)=Z(x)1eckλkfk(c,y,x)
    现在是不是已经得到的crf的计算公式,到底要如何计算后面再说,先看看这个式子是怎么来的。首先,为什么长这个样子?因为弄这个东东出来的目的就是在给定序列 X X X的情况下判定序列 Y Y Y的,奔着 P ( Y ∣ X ) P(Y|X) P(YX)去的,判别式模型!其次,为什么在最后的公式中连乘符号没了?因为 ψ c ( c ) \psi_c(c) ψc(c)这东东被定义成了指数函数,所以连乘变成了求和!

  • 如何最大化 P ( Y ) P(Y) P(Y)
    继续不管刚才那一坨怎么算,先从整体上把流程搞定再说细节。
    根据概率论课上老师教的,我们可以使用最大似然估计来计算分布的参数,即我们的目标就是最大化 l o g P ( Y ) logP(Y) logP(Y)
    l o g P ( Y ) = l o g 1 Z ( x ) e ∑ c ∑ k λ k f k ( c , y , x ) = ∑ c ∑ k λ k f k ( c , y , x ) − l o g Z ( x ) logP(Y) = log\frac{1}{Z(x)}e^{\sum_c\sum_{k}\lambda_k f_k(c,y,x)}=\sum_c\sum_{k}\lambda_k f_k(c,y,x) - logZ(x) logP(Y)=logZ(x)1eckλkfk(c,y,x)=ckλkfk(c,y,x)logZ(x)
    最大化 l o g P ( Y ) logP(Y) logP(Y)等价于最小化 − l o g P ( Y ) -logP(Y) logP(Y),所以目标就变成了最小化 − l o g P ( Y ) -logP(Y) logP(Y)
    − l o g P ( Y ) = l o g Z ( x ) − ∑ c ∑ k λ k f k ( c , y , x ) -logP(Y) = logZ(x) - \sum_c\sum_{k}\lambda_k f_k(c,y,x) logP(Y)=logZ(x)ckλkfk(c,y,x)
    对应到代码实现中,关注neg_log_likelihood()函数的最后一行

def neg_log_likelihood(self, sentence, tags):
    feats = self._get_lstm_features(sentence)
    forward_score = self._forward_alg(feats)
    gold_score = self._score_sentence(feats, tags)
    return forward_score - gold_score

forward_score 就是我们的 l o g Z ( x ) logZ(x) logZ(x), gold_score是后面的那一坨。

  • 解决细节
    上面一直都在把 ∑ c ∑ k λ k f k ( c , y , x ) \sum_c\sum_{k}\lambda_k f_k(c,y,x) ckλkfk(c,y,x)当成一坨不去纠结他,但还是到了不得不面对的时候(终是庄周梦了蝶,你是恩赐也是劫)。
    先来看一下crf的长相(开始疯狂盗图)
    还记得上面提到的无向图的概率计算吗?需要分解成若干最大团然后连乘,OK!辣么把上面的图分解成若干最大团即可。怎么分?因为模型建立的初衷就是要考虑到 i k − 1 i_{k-1} ik1 i k i_k ik的影响和 X X X对观测序列的影响,所以我们将图分解成若干个 ( i k − 1 , i k , X ) ({i_{k-1}, i_k, X}) (ik1,ik,X),其中 i k i_k ik表示观测变量的状态值,比如在BIO标注中状态取值范围是{B,I,O,START,STOP},则k最大取5, i k i_k ik有5个状态值可取。
    在这里插入图片描述
    只关注其中的某一个团 C i C_i Ci,将 ∑ c ∑ k λ k f k ( c , y , x ) \sum_c\sum_{k}\lambda_k f_k(c,y,x) ckλkfk(c,y,x)看成一个打分函数,表示在给定序列 X X X情况下,表现出( i k − 1 , i k i_{k-1}, i_k ik1,ik)的非归一化概率,这个概率与两个东东有关,一个是在序列 X X X情况下由 i k − 1 转 移 到 i k i_{k-1}转移到i_k ik1ik的概率(在线性CRF中,假设观测变量只受临近结点的影响),一个是给定序列 X X X情况下出现 i k i_k ik的概率,设:
    g ( i k − 1 , i k ; X ) g(i_{k-1},i_k;X) g(ik1,ik;X)表示在序列 X X X情况下由 i k − 1 转 移 到 i k i_{k-1}转移到i_k ik1ik的概率;
    h ( i k ; X ) h(i_k;X) h(ik;X)表示给定序列 X X X情况下出现 i k i_k ik的概率
    我们使用LSTM或者CNN来建模 X X X对应 i k i_k ik的映射,所以使用lstm的输出来代替 h ( i k ; X ) h(i_k;X) h(ik;X);考虑到深度学习模型已经能比较充分捕捉各个 i k i_k ik 与X 的联系,所以假设 i k − 1 转 移 到 i k i_{k-1}转移到i_k ik1ik的概率与X无关,所以 g ( i k − 1 , i k ; X ) = g ( i k − 1 , i k ) g(i_{k-1},i_k;X)=g(i_{k-1},i_k) g(ik1,ik;X)=g(ik1,ik)这个转移概率是我们要学习的参数。于是就把 ∑ c ∑ k λ k f k ( c , y , x ) \sum_c\sum_{k}\lambda_k f_k(c,y,x) ckλkfk(c,y,x)写成了 ∑ c ∑ k ( h ( i k ; X ) + g ( i k − 1 , i k ) ) \sum_c\sum_{k}(h(i_k;X) +g(i_{k-1},i_k)) ck(h(ik;X)+g(ik1,ik))
    再盗一张图来说明计算过程
    在这里插入图片描述
    先看一下上面两张图的关系,图中表示每一个 i k i_k ik分别有4个状态可以取,对应到代码中是5个状态。
    在这里插入图片描述

将图对应到公式中,k = 4对于每一步 t t t都需要知道 h k t + 1 ( i k ; X ) h_k^{t+1}(i_{k};X) hkt+1(ik;X)
Z i t Z_i^{t} Zit表示在当前时刻 t 以状态(标签) y 1 , … , y k y_1,…,y_k y1,,yk 为终点的所有路径的得分指数和,即前向算法中的前向变量。我们来关注一下 Z i t + 1 Z_i^{t+1} Zit+1是怎么来的, Z 1 t + 1 Z_1^{t+1} Z1t+1是图中(在时刻t中所有红色的连线) × \times ×(在t+1时刻状态为1的值)
Z 1 t + 1 = Z 1 t ⋅ G 11 ⋅ H t + 1 ( 1 ∣ X ) + Z 1 t ⋅ G 21 ⋅ H t + 1 ( 1 ∣ X ) + Z 1 t ⋅ G 31 ⋅ H t + 1 ( 1 ∣ X ) + Z 1 t ⋅ G 41 ⋅ H t + 1 ( 1 ∣ X ) Z_1^{t+1}=Z_1^{t}\cdot G_{11}\cdot H_{t+1}(1|X) +Z_1^{t}\cdot G_{21}\cdot H_{t+1}(1|X) +Z_1^{t}\cdot G_{31}\cdot H_{t+1}(1|X) +Z_1^{t}\cdot G_{41}\cdot H_{t+1}(1|X) Z1t+1=Z1tG11Ht+1(1X)+Z1tG21Ht+1(1X)+Z1tG31Ht+1(1X)+Z1tG41Ht+1(1X)
Z 2 t + 1 = Z 2 t ⋅ G 12 ⋅ H t + 1 ( 2 ∣ X ) + Z 2 t ⋅ G 22 ⋅ H t + 1 ( 2 ∣ X ) + Z 2 t ⋅ G 32 ⋅ H t + 1 ( 2 ∣ X ) + Z 2 t ⋅ G 42 ⋅ H t + 1 ( 2 ∣ X ) Z_2^{t+1}=Z_2^{t}\cdot G_{12}\cdot H_{t+1}(2|X) +Z_2^{t}\cdot G_{22}\cdot H_{t+1}(2|X) +Z_2^{t}\cdot G_{32}\cdot H_{t+1}(2|X) +Z_2^{t}\cdot G_{42}\cdot H_{t+1}(2|X) Z2t+1=Z2tG12Ht+1(2X)+Z2tG22Ht+1(2X)+Z2tG32Ht+1(2X)+Z2tG42Ht+1(2X)

Z i t + 1 = Z i t ⋅ G 1 i ⋅ H t + 1 ( i ∣ X ) + Z i t ⋅ G 2 i ⋅ H t + 1 ( i ∣ X ) + Z i t ⋅ G 3 i ⋅ H t + 1 ( i ∣ X ) + Z i t ⋅ G 4 i ⋅ H t + 1 ( i ∣ X ) Z_i^{t+1}=Z_i^{t}\cdot G_{1i}\cdot H_{t+1}(i|X) +Z_i^{t}\cdot G_{2i}\cdot H_{t+1}(i|X) +Z_i^{t}\cdot G_{3i}\cdot H_{t+1}(i|X) +Z_i^{t}\cdot G_{4i}\cdot H_{t+1}(i|X) Zit+1=ZitG1iHt+1(iX)+ZitG2iHt+1(iX)+ZitG3iHt+1(iX)+ZitG4iHt+1(iX)
其中 G 是对 g ( y i , y j ) g(y_i,y_j) g(yi,yj) 各个元素取指数后的矩阵
G i j = e g ( y i , y j ) G_{ij}=e^{g(y_i,y_j)} Gij=eg(yi,yj)
同理, H t + 1 ( y k ∣ X ) = e h t + 1 ( ( y k ∣ X ) H_{t+1}(y_{k}|X)=e^{h_{t+1}((y_{k}|X)} Ht+1(ykX)=eht+1((ykX)
对应到代码中

def _forward_alg(self, feats):
    # Do the forward algorithm to compute the partition function
    init_alphas = torch.full((1, self.tagset_size), -10000.)
    # START_TAG has all of the score.
    init_alphas[0][self.tag_to_ix[START_TAG]] = 0.

    # Wrap in a variable so that we will get automatic backprop
    forward_var = init_alphas

    # Iterate through the sentence
    for feat in feats:
        alphas_t = []  # The forward tensors at this timestep
        for next_tag in range(self.tagset_size):
            # 状态特征函数的得分
            emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)

            # 状态转移函数的得分
            trans_score = self.transitions[next_tag].view(1, -1)

            # 从上一个单词的每个状态转移到next_tag状态的得分
            # 所以next_tag_var是一个大小为tag_size的数组
            next_tag_var = forward_var + trans_score + emit_score

            # The forward variable for this tag is log-sum-exp of all the
            # scores.
            alphas_t.append(log_sum_exp(next_tag_var).view(1))
        forward_var = torch.cat(alphas_t).view(1, -1)
    terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
    alpha = log_sum_exp(terminal_var)
    return alpha

解释一下:

  1. 在上述的代码中,所有计算都在 l o g log log空间下进行,所以公式中的 × \times ×都变成了 + + +
  2. 示例中的状态总共5个,分别是[B,I,O,START,STOP],所以self.transitions是一个(5,5)的矩阵;
  3. feats是BiLstm的输出,形状(句子数量,单词数量,5),所以feats.shape()[1]就是步长,即上图中t的取值,比如一句话有11个单词,那么 t ∈ ( 1 , 2 , . . . 11 ) t\in (1,2,...11) t(1,2,...11)
  4. emit_score 就是我们公式中的H()函数的输出,代码中每次先expand了一次,对应着公式 Z i t + 1 = Z i t ⋅ G 11 ⋅ H t + 1 ( i ∣ X ) + Z i t ⋅ G 21 ⋅ H t + 1 ( i ∣ X ) + Z i t ⋅ G 31 ⋅ H t + 1 ( i ∣ X ) + Z i t ⋅ G 41 ⋅ H t + 1 ( i ∣ X ) Z_i^{t+1}=Z_i^{t}\cdot G_{11}\cdot H_{t+1}(i|X) +Z_i^{t}\cdot G_{21}\cdot H_{t+1}(i|X) +Z_i^{t}\cdot G_{31}\cdot H_{t+1}(i|X) +Z_i^{t}\cdot G_{41}\cdot H_{t+1}(i|X) Zit+1=ZitG11Ht+1(iX)+ZitG21Ht+1(iX)+ZitG31Ht+1(iX)+ZitG41Ht+1(iX)中的 H t + 1 ( i ∣ X ) H_{t+1}(i|X) Ht+1(iX)
  5. trans_score 就是我们的状态转移函数G(),代码中每次取一行进行计算
    trans_score = self.transitions[next_tag].view(1, -1),即公式中的 G 1 i , G 2 i , G 3 i , G 4 i G_{1i},G_{2i},G_{3i}, G_{4i} G1i,G2i,G3i,G4i
  6. 代码中的next_tag_var = forward_var + trans_score + emit_score就是公式中的 Z i t Z_i^{t} Zit了,我们的前向变量;
  7. 求出所有的 Z i t Z_i^{t} Zit后使用log_sum_exp()来计算我们所需要的logZ(x);
  8. 如何计算 l o g ∑ i = 1 k e a i log\sum_{i=1}^{k}e^{a_i} logi=1keai
    l o g ∑ i = 1 k e a i = A − l o g ∑ i = 1 k e a i − A log\sum_{i=1}^{k}e^{a_i}=A-log\sum_{i=1}^{k}e^{a_i-A} logi=1keai=Alogi=1keaiA 其中 A = m a x ( a 1 , a 2 , . . . , a n ) A=max({a_1, a_2, ..., a_n}) A=max(a1,a2,...,an)
    所以log_sum_exp()长这样:
def log_sum_exp(vec):
    max_score = vec[0, argmax(vec)]
    max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])
    return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))
  • viterbi解码算法
    官方给出的代码中实现了很标准的viterbi算法,只要记住
    δ t + 1 = m a x 1 ⩽ j ⩽ k ( δ t + ∑ c ∑ k λ k f k ( c , y , x ) ) \delta _{t+1}=max_{1\leqslant j\leqslant k }(\delta _t+\sum_c\sum_{k}\lambda_k f_k(c,y,x)) δt+1=max1jk(δt+ckλkfk(c,y,x))具体理论请参加李航大大的统计学习,就不再搬运了。
def _viterbi_decode(self, feats):
    backpointers = []

    # Initialize the viterbi variables in log space
    init_vvars = torch.full((1, self.tagset_size), -10000.)
    init_vvars[0][self.tag_to_ix[START_TAG]] = 0

    # forward_var at step i holds the viterbi variables for step i-1
    forward_var = init_vvars
    for feat in feats:
        bptrs_t = []  # holds the backpointers for this step
        viterbivars_t = []  # holds the viterbi variables for this step

        for next_tag in range(self.tagset_size):
            # next_tag_var[i] holds the viterbi variable for tag i at the
            # previous step, plus the score of transitioning
            # from tag i to next_tag.
            # We don't include the emission scores here because the max
            # does not depend on them (we add them in below)
            next_tag_var = forward_var + self.transitions[next_tag]
            best_tag_id = argmax(next_tag_var)
            bptrs_t.append(best_tag_id)
            viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))
        # Now add in the emission scores, and assign forward_var to the set
        # of viterbi variables we just computed
        forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)
        backpointers.append(bptrs_t)

    # Transition to STOP_TAG
    terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
    best_tag_id = argmax(terminal_var)
    path_score = terminal_var[0][best_tag_id]

    # Follow the back pointers to decode the best path.
    best_path = [best_tag_id]
    for bptrs_t in reversed(backpointers):
        best_tag_id = bptrs_t[best_tag_id]
        best_path.append(best_tag_id)
    # Pop off the start tag (we dont want to return that to the caller)
    start = best_path.pop()
    assert start == self.tag_to_ix[START_TAG]  # Sanity check
    best_path.reverse()
    return path_score, best_path

参考文档

[1] https://www.jiqizhixin.com/articles/2018-05-23-3
[2] https://zhuanlan.zhihu.com/p/71190655
[3] https://zhuanlan.zhihu.com/p/33397147
[4] https://pytorch.org/tutorials/beginner/nlp/advanced_tutorial.html
[5] https://blog.csdn.net/zycxnanwang/article/details/90385259

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值