CRF代码解释

CRF代码选自[https://spaces.ac.cn/archives/5542],大神编写,由于对代码解释没有特别清晰,对初学者可能有一定难度,本文对此代码做适当解析,帮助读者阅读。
如果阅读到此文,还望静下心来,认真思考,代码是经过测试,且认为相当简洁高效,才为大家做一个分享,目的在于减少各位看官查找无用博客而浪费时间。
以下为代码

# -*- 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来对齐张量。
        """
        inputs, mask = inputs[:, :-1], inputs[:, -1:]
        states = K.expand_dims(states[0], 2)  # (batch_size, output_dim, 1)
        trans = K.expand_dims(self.trans, 0)  # (1, output_dim, output_dim)
        outputs = K.logsumexp(states + trans, 1)  # (batch_size, output_dim)
        outputs = outputs + inputs
        outputs = mask * outputs + (1 - mask) * states[:, :, 0]
        return outputs, [outputs]

    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形式
        if self.ignore_last_label:
            mask = 1 - y_true[:, :, -1:]
        else:
            mask = K.ones_like(y_pred[:, :, :1])
        y_true, y_pred = y_true[:, :, :self.num_labels], y_pred[:, :, :self.num_labels]
        path_score = self.path_score(y_pred, y_true)  # 计算分子(对数)
        init_states = [y_pred[:, 0]]  # 初始状态
        y_pred = K.concatenate([y_pred, mask])
        log_norm, _, _ = K.rnn(self.log_norm_step, y_pred[:, 1:], init_states)  # 计算Z向量(对数)
        log_norm = K.logsumexp(log_norm, 1, keepdims=True)  # 计算Z(对数)
        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)

class CRF(Layer) 继承keras.layers.Layer,继承后,重写build与call,通常对于call输入,第一次运行时会先加载build函数来指定输出维度,输入维度根据call(self, inputs)自动识别。

log_norm_step(self, inputs, states) 此函数,原作者是作为rnn函数的step_function输入,所以此时states是rnn模型的状态变量,inputs的shape为 (samples, …)的张量(没有时间维度),states是一个张量列表,最初输入值init_states,

**log_norm, _, _ = K.rnn(self.log_norm_step, y_pred[:, 1:], init_states)**

对于k.rnn函数

tf.keras.backend.rnn(
    step_function,
    inputs,
    initial_states,
    go_backwards=False,
    mask=None,
    constants=None,
    unroll=False,
    input_length=None,
    time_major=False,
    zero_output_for_mask=False
)

step_function:RNN步骤函数。参数:input:具有shape (samples, …)的张量(没有时间维度),表示在特定时间步骤的一批样品的输入。states:张量列表。返回:output:具有shape (samples, output_dim)的张量 (没有时间维度)。new_states:张量列表,长度和shape与“states”相同。列表中的第一个状态必须是前一个时间步的输出张量。
inputs:shape为(samples, time, …) (至少3D)的时间数据的张量,或嵌套张量,并且每个都具有shape (samples, time, …)。
initial_states:shape为(samples, state_size) 的张量(无时间维度),包含step函数中使用的状态的初始值。在state_size是嵌套形状的情况下,initial_states的形状也将遵循嵌套结构。
(以上解释为https://www.w3cschool.cn/tensorflow_python/tf_keras_backend_rnn.html复制)

运算为对于NLP领域,假设输入的shape为(samples, time,embedding),则rnn输出为每一个samples的embedding运行time次step_function所得出的last_output、outputs、new_states三个值。
last_output:shape为(samples, …) 输出的rnn的最新输出。
outputs:shape为(samples, time, …)的张量,其中每个条目 outputs[s, t] 是样本 s 在时间 t 的步骤函数输出值。
new_states:张量列表,步长函数返回的最新状态,shape为(samples, …)。

具体运算见代码:

batch_size = 2
time_step = 3
dim = 2
x = np.random.rand(batch_size, time_step, dim)  # [2,3,2]生成输入
init_state = np.zeros(2).reshape(2, 1)  # [2,1] 初始值设置为0

def step_func(inputs, states):
    o = K.sum(inputs, axis=1, keepdims=True) + states[0]
    return o, [o]
a,b,_ = K.rnn(step_func, inputs=x, initial_states=[init_state])
a:
<tf.Tensor: id=796, shape=(2, 1), dtype=float64, numpy=
array([[3.60335374],
       [2.27873122]])>
b:
<tf.Tensor: id=798, shape=(2, 3, 1), dtype=float64, numpy=
array([[[0.97500236],
        [2.7395605 ],
        [3.60335374]],

       [[0.41585083],
        [1.04170515],
        [2.27873122]]])>
x:
Out[206]:
array([[[0.14307762, 0.83192474],
        [0.86293904, 0.90161909],
        [0.04984334, 0.81394991]],

       [[0.09448456, 0.32136627],
        [0.54405903, 0.08179529],
        [0.92263206, 0.314394  ]]])

以上为个人感觉难懂的点,对于函数内部计算,原文作者已经解释非常清晰,就不再赘述,建议先大致阅读本文,之后详细阅读原文,会有更清晰的认识。
如有错误,还请各位看官指出,会第一时间修改。

你可能感兴趣的:(tensorflow)