条件随机场CRF原理介绍 以及Keras实现

   2023-02-09 学习力690
核心提示:本文是对CRF基本原理的一个简明的介绍。当然,“简明”是相对而言中,要想真的弄清楚CRF,免不了要提及一些公式,如果只关心调用的读者,可以直接移到文末。 #按照之前的思路,我们依旧来对比一下普通的逐帧softmax和CRF的异同。 #CRF主要用于序列标注问题

本文是对CRF基本原理的一个简明的介绍。当然,“简明”是相对而言中,要想真的弄清楚CRF,免不了要提及一些公式,如果只关心调用的读者,可以直接移到文末。

 #

按照之前的思路,我们依旧来对比一下普通的逐帧softmax和CRF的异同。

 #

CRF主要用于序列标注问题,可以简单理解为是给序列中的每一帧都进行分类,既然是分类,很自然想到将这个序列用CNN或者RNN进行编码后,接一个全连接层用softmax激活,如下图所示

条件随机场CRF原理介绍 以及Keras实现

逐帧softmax并没有直接考虑输出的上下文关联

 #

然而,当我们设计标签时,比如用s、b、m、e的4个标签来做字标注法的分词,目标输出序列本身会带有一些上下文关联,比如s后面就不能接m和e,等等。逐标签softmax并没有考虑这种输出层面的上下文关联,所以它意味着把这些关联放到了编码层面,希望模型能自己学到这些内容,但有时候会“强模型所难”。

而CRF则更直接一点,它将输出层面的关联分离了出来,这使得模型在学习上更为“从容”:

条件随机场CRF原理介绍 以及Keras实现

CRF在输出端显式地考虑了上下文关联

 #

当然,如果仅仅是引入输出的关联,还不仅仅是CRF的全部,CRF的真正精巧的地方,是它以路径为单位,考虑的是路径的概率

 #

假如一个输入有k^n中不同的输出。我们可以将它用如下的网络图进行简单的可视化。在下图中,每个点代表一个标签的可能性,点之间的连线表示标签之间的关联,而每一种标注结果,都对应着图上的一条完整的路径。

条件随机场CRF原理介绍 以及Keras实现

4tag分词模型中输出网络图

而在序列标注任务中,我们的正确答案是一般是唯一的。比如“今天天气不错”,如果对应的分词结果是“今天/天气/不/错”,那么目标输出序列就是bebess,除此之外别的路径都不符合要求。换言之,在序列标注任务中,我们的研究的基本单位应该是路径,我们要做的事情,是从k^n类中选一类的分类问题!

这就是逐帧softmax和CRF的根本不同了:前者将序列标注看成是k^n分类问题。

具体来讲,在CRF的序列标注问题中,我们要计算的是条件概率

假设一 该分布是指数族分布。

这个假设意味着存在函数f(y1,…,yn;x)

,使得

条件随机场CRF原理介绍 以及Keras实现

其中函数可以视为一个打分函数,打分函数取指数并归一化后就得到概率分布。

假设二 输出之间的关联仅发生在相邻位置,并且关联是指数加性的。

这个假设意味着f(y1,…,yn;x)可以更进一步简化为

尽管已经做了大量简化,但一般来说,x无关,那么

条件随机场CRF原理介绍 以及Keras实现

 

这时候我们可以通过RNN或者CNN来建模。因此,该模型是可以建立的,其中概率分布变为

为了训练CRF模型,我们用最大似然方法,也就是用

其中第一项是原来概率式的分子的对数,它目标的序列的打分,虽然它看上去挺迂回的,但是并不难计算。真正的难度在于分母的对数)

这一项。

归一化因子,在物理上也叫配分函数,在这里它需要我们对所有可能的路径的打分进行指数求和,而我们前面已经说到,这样的路径数是指数量级的(k^n),因此直接来算几乎是不可能的。

事实上,归一化因子难算,几乎是所有概率图模型的公共难题。幸运的是,在CRF模型中,由于我们只考虑了临近标签的联系(马尔可夫假设),因此我们可以递归地算出归一化因子,这使得原来是指数级的计算量降低为线性级别。具体来说,我们将计算到时刻k个部分

它可以简单写为矩阵形式

条件随机场CRF原理介绍 以及Keras实现

归一化因子的递归计算图示。从t到t+1时刻的计算,包括转移概率和j+1节点本身的概率

如果不熟悉的读者,可能一下子比较难接受)式了。

 #

写出损失函数)后,就可以完成模型的训练了,因为目前的深度学习框架都已经带有自动求导的功能,只要我们能写出可导的loss,就可以帮我们完成优化过程了。

那么剩下的最后一步,就是模型训练完成后,如何根据输入找出最优路径来。跟前面一样,这也是一个从n

动态规划在本博客已经出现了多次了,它的递归思想就是:一条最优路径切成两段,那么每一段都是一条(局部)最优路径。在本博客右端的搜索框键入“动态规划”,就可以得到很多相关介绍了,所以不再重复了~

 #

经过调试,基于Keras框架下,笔者得到了一个线性链CRF的简明实现,这也许是最简短的CRF实现了。这里分享最终的实现并介绍实现要点。

 #

前面我们已经说明了,实现CRF的困难之处是Z(x)的计算了。

那么怎么在深度学习框架中实现这种递归计算呢?要注意,从计算图的视角看,这是通过递归的方法定义一个图,而且这个图的长度还不固定。这对于pytorch这样的动态图框架应该是不为难的,但是对于tensorflow或者基于tensorflow的Keras就很难操作了(它们是静态图框架)。

不过,并非没有可能,我们可以用封装好的rnn函数来计算!我们知道,rnn本质上就是在递归计算

这就是CRF实现中最精致的部分了。

至于剩下的,是一些细节性的,包括:

1、为了防止溢出,我们通常要取对数,但由于归一化因子是指数求和,所以实际上是条件随机场CRF原理介绍 以及Keras实现这样的格式,它的计算技巧是:

条件随机场CRF原理介绍 以及Keras实现


tensorflow和Keras中都已经封装好了对应的logsumexp函数了,直接调用即可;

 

2、对于分子(也就是目标序列的得分)的计算技巧,在代码中已经做了注释,主要是通过用“目标序列”点乘“预测序列”来实现取出目标得分;

3、关于变长输入的padding部分如何进行mask?我觉得在这方面Keras做得并不是很好。为了简单实现这种mask,我的做法是引入多一个标签,比如原来是s、b、m、e四个标签做分词,然后引入第五个标签,比如x,将padding部分的标签都设为x,然后可以直接在CRF损失计算时忽略第五个标签的存在,具体实现请看代码。

 #

纯Keras实现的CRF层,欢迎使用~

# -*- coding:utf-8 -*-

from keras.layers import Layer
import keras.backend as K


class CRF(Layer):
    """纯Keras实现CRF层
    CRF层本质上是一个带训练参数的loss计算层,因此CRF层只用来训练模型,
    而预测则需要另外建立模型。
    """
    def __init__(self, ignore_last_label=False, **kwargs):
        """ignore_last_label:定义要不要忽略最后一个标签,起到mask的效果
        """
        self.ignore_last_label = 1 if ignore_last_label else 0
        super(CRF, self).__init__(**kwargs)
    def build(self, input_shape):
        self.num_labels = input_shape[-1] - self.ignore_last_label
        self.trans = self.add_weight(name='crf_trans',
                                     shape=(self.num_labels, self.num_labels),
                                     initializer='glorot_uniform',
                                     trainable=True)
    def log_norm_step(self, inputs, states):
        """递归计算归一化因子
        要点:1、递归计算;2、用logsumexp避免溢出。
        技巧:通过expand_dims来对齐张量。
        """
        states = K.expand_dims(states[0], 2) # (batch_size, output_dim, 1)
        trans = K.expand_dims(self.trans, 0) # (1, output_dim, output_dim)
        output = K.logsumexp(states+trans, 1) # (batch_size, output_dim)
        return output+inputs, [output+inputs]
    def path_score(self, inputs, labels):
        """计算目标路径的相对概率(还没有归一化)
        要点:逐标签得分,加上转移概率得分。
        技巧:用“预测”点乘“目标”的方法抽取出目标路径的得分。
        """
        point_score = K.sum(K.sum(inputs*labels, 2), 1, keepdims=True) # 逐标签得分
        labels1 = K.expand_dims(labels[:, :-1], 3)
        labels2 = K.expand_dims(labels[:, 1:], 2)
        labels = labels1 * labels2 # 两个错位labels,负责从转移矩阵中抽取目标转移得分
        trans = K.expand_dims(K.expand_dims(self.trans, 0), 0)
        trans_score = K.sum(K.sum(trans*labels, [2,3]), 1, keepdims=True)
        return point_score+trans_score # 两部分得分之和
    def call(self, inputs): # CRF本身不改变输出,它只是一个loss
        return inputs
    def loss(self, y_true, y_pred): # 目标y_pred需要是one hot形式
        mask = 1-y_true[:,1:,-1] if self.ignore_last_label else None
        y_true,y_pred = y_true[:,:,:self.num_labels],y_pred[:,:,:self.num_labels]
        init_states = [y_pred[:,0]] # 初始状态
        log_norm,_,_ = K.rnn(self.log_norm_step, y_pred[:,1:], init_states, mask=mask) # 计算Z向量(对数)
        log_norm = K.logsumexp(log_norm, 1, keepdims=True) # 计算Z(对数)
        path_score = self.path_score(y_pred, y_true) # 计算分子(对数)
        return log_norm - path_score # 即log(分子/分母)
    def accuracy(self, y_true, y_pred): # 训练过程中显示逐帧准确率的函数,排除了mask的影响
        mask = 1-y_true[:,:,-1] if self.ignore_last_label else None
        y_true,y_pred = y_true[:,:,:self.num_labels],y_pred[:,:,:self.num_labels]
        isequal = K.equal(K.argmax(y_true, 2), K.argmax(y_pred, 2))
        isequal = K.cast(isequal, 'float32')
        if mask == None:
            return K.mean(isequal)
        else:
            return K.sum(isequal*mask) / K.sum(mask)

除去注释和accuracy的代码,真正的CRF的代码量也就30行左右,可以说跟哪个框架比较都称得上是简明的CRF实现了吧~

用纯Keras实现一些复杂的模型,是一件颇有意思的事情。目前仅在tensorflow后端测试通过,理论上兼容theano、cntk后端,但可能要自行微调。

 #

我的Github中还附带了一个使用CNN+CRF实现的中文分词的例子,用的是Bakeoff 2005语料,例子是一个完整的分词实现,包括viterbi算法、分词输出等。

Github地址:https://github.com/bojone/crf/

 

 

 
反对 0举报 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 拓端数据tecdat|使用Python中Keras的LSTM递归神经网络进行时间序列预测
    拓端数据tecdat|使用Python中Keras的LSTM递归神
     时间序列预测问题是预测建模问题中的一种困难类型。与回归预测建模不同,时间序列还增加了输入变量之间序列依赖的复杂性。用于处理序列依赖性的强大神经网络称为 递归神经网络。长短期记忆网络或LSTM网络是深度学习中使用的一种递归神经网络,可以成功地训
    03-08
  • 探索学习率设置技巧以提高Keras中模型性能 | 炼丹技巧
    探索学习率设置技巧以提高Keras中模型性能 | 炼
      学习率是一个控制每次更新模型权重时响应估计误差而调整模型程度的超参数。学习率选取是一项具有挑战性的工作,学习率设置的非常小可能导致训练过程过长甚至训练进程被卡住,而设置的非常大可能会导致过快学习到次优的权重集合或者训练过程不稳定。迁移学
    03-08
  • Keras函数式API介绍 keras框架介绍
    Keras函数式API介绍 keras框架介绍
    参考文献:Géron, Aurélien. Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow: Concepts, Tools, and Techniques to Build Intelligent Systems. O'Reilly Media, 2019.Keras的Sequential顺序模型可以快速搭建简易的神经网络,同时Ker
    02-09
  • keras——经典模型之LeNet5  实现手写字识别
    keras——经典模型之LeNet5 实现手写字识别
    经典论文:Gradient-Based Learning Applied to Document Recognition参考博文:https://blog.csdn.net/weixin_44344462/article/details/89212507构建LeNet-5模型#定义LeNet5网络深度为1的灰度图像def LeNet5(x_train, y_train, x_test, y_test):########搭
    02-09
  • Keras2.2 predict和fit_generator的区别
    查看keras文档中,predict函数原型:predict(self, x, batch_size=32, verbose=0)说明:只使用batch_size=32,也就是说每次将batch_size=32的数据通过PCI总线传到GPU,然后进行预测。在一些问题中,batch_size=32明显是非常小的。而通过PCI传数据是非常耗时的
    02-09
  • keras模块学习之-激活函数(activations)--笔
    本笔记由博客园-圆柱模板 博主整理笔记发布,转载需注明,谢谢合作!   每一个神经网络层都需要一个激活函数,例如一下样例代码:           from keras.layers.core import Activation, Densemodel.add(Dense(64))model.add(Activation('tanh'))或把
    02-09
  • 用于NLP的CNN架构搬运:from keras0.x to keras2.x
    用于NLP的CNN架构搬运:from keras0.x to keras
    本文亮点:将用于自然语言处理的CNN架构,从keras0.3.3搬运到了keras2.x,强行练习了Sequential+Model的混合使用,具体来说,是Model里嵌套了Sequential。本文背景:暑假在做一个推荐系统的小项目,老师让我们搜集推荐系统领域Top5的算法和模型,要求结合深度
    02-09
  • keras: 在构建LSTM模型时,使用变长序列的方法
    众所周知,LSTM的一大优势就是其能够处理变长序列。而在使用keras搭建模型时,如果直接使用LSTM层作为网络输入的第一层,需要指定输入的大小。如果需要使用变长序列,那么,只需要在LSTM层前加一个Masking层,或者embedding层即可。from keras.layers import
    02-09
  • win10 python3.7 Anaconda3 安装tensorflow+Keras
    win10 python3.7 Anaconda3 安装tensorflow+Ker
    首先tensorflow 不支持python3.7,只能用tf1.9 也就是说:py3.7+ tf 1.9 +keras 2.2.0 才可以https://docs.floydhub.com/guides/environments/这个链接可以查询不同版本应该下载那个到Tensorflow支持Python3.7的一个whl:Unofficial Windows Binaries for Pyth
    02-09
  • keras channels_last、preprocess_input、全连
    channels_last 和 channels_firstkeras中 channels_last 和 channels_first 用来设定数据的维度顺序(image_data_format)。对2D数据来说,"channels_last"假定维度顺序为 (rows,cols,channels), 而"channels_first"假定维度顺序为(channels, rows, cols)。
    02-09
点击排行