CRF主要用于序列标注,感觉自己好像不太能用上。
要结合上文的标注,本身标注,整个序列信息。
考虑每种标注序列的概率。
https://zhuanlan.zhihu.com/p/37163081写的非常好。
1.打分函数和归一化因子
2。输出之间的关联仅发生在相邻位置,并且关联是指数加性的。
3.简化:
g函数与x无关。而且h(yi,x)使用RNN来提前训练。
4.使用-logP作为损失函数,
5.难的是Z(x),代表所有可能标注序列的分值。加入句子长度是m,而每个单词可能的标注选择有k种,则总共有k^m种。数量庞大。
在 CRF 模型中,由于我们只考虑了临近标签的联系(马尔可夫假设),因此我们可以递归地算出归一化因子,这使得原来是指数级的计算量降低为线性级别。
Z的下标表示到句子的第几个位置、上标表示结束的时候选的是第几个标注类型,G是g(yi,yi+1)取指数,
6.我们训练的是那些打分函数。训练完了之后如果进行预测呢?博客中说是动态规划?
# -*- 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)