loss为层作为输出

keras中的损失函数, 或者自定义损失函数直接在model.compile中使用的, 都是接受真实标签和类别预测概率两个变量, 计算得到的, 即loss为预测值与真实值的某种误差函数.

但有些任务/模型, 计算损失时比较复杂, 需要别的参数. 这里的参数指的不是超参数, 例如focal loss中的γ\gamma或者α\alpha, 这种超参数可以使用函数工厂的方法, 返回闭包, 具体方法见自定义损失函数一节. 这里的参数, 对应的是模型中某一层的输出, 可能是计算过程中需要引入其他的参数或变量, 也可能对应多输出且损失不是简单的加和情况(有交互).

在实际中常遇到的属于这种情况的有:

  • seq2seq任务, 或者序列标注任务等. NLP任务基本都需要对样本进行padding或者truncate, 对于这种序列任务, 最后的输出往往都是各个序列上损失的加和

    • 因此对于padding的位置, 计算整体损失时, 就不应当计入

    • 因此对应的损失函数需要引入mask变量

  • 句向量或任何其他形式向量之间的匹配. 例如FAQ问题, 将问题和答案都编码成长度相同的向量, 然后计算它们的余弦相似度, 作为loss, 正确答案loss小, 错误答案反之. .因此可以使用triplet loss:

    loss=max(0,m+cos(q,Awrong)cos(q,Aright))loss = \max\Big(0, m+\cos(q,A_{\text{wrong}})-\cos(q,A_{\text{right}})\Big)

    只要保证正确答案的cos值比错误答案的cos高即可, 大多少不重要, 以这个思路进行训练, 就对应上面的损失函数

    可以看到这个损失函数就不是简单的是输入输出的组合事情了.

Method 1

无论哪种方法都是单独地构造一个损失层, 区别在于:

  • 如何将损失层融入到模型中

  • 如何组织样本喂给模型进行fit

第一种方法以上面的FAQ为例, 参考自Keras中自定义复杂的loss函数中的第二部分.

代码如下:

from keras.layers import Input,Embedding,LSTM,Dense,Lambda
from keras.layers.merge import dot
from keras.models import Model
from keras import backend as K

word_size = 128
nb_features = 10000
nb_classes = 10
encode_size = 64
margin = 0.1

embedding = Embedding(nb_features,word_size)
lstm_encoder = LSTM(encode_size)

def encode(input):
    return lstm_encoder(embedding(input))

q_input = Input(shape=(None,))
a_right = Input(shape=(None,))
a_wrong = Input(shape=(None,))
q_encoded = encode(q_input)
a_right_encoded = encode(a_right)
a_wrong_encoded = encode(a_wrong)

q_encoded = Dense(encode_size)(q_encoded) #一般的做法是,直接讲问题和答案用同样的方法encode成向量后直接匹配,但我认为这是不合理的,我认为至少经过某个变换。

right_cos = dot([q_encoded,a_right_encoded], -1, normalize=True)
wrong_cos = dot([q_encoded,a_wrong_encoded], -1, normalize=True)

loss = Lambda(lambda x: K.relu(margin+x[0]-x[1]))([wrong_cos,right_cos])

model_train = Model(inputs=[q_input,a_right,a_wrong], outputs=loss)
model_q_encoder = Model(inputs=q_input, outputs=q_encoded)
model_a_encoder = Model(inputs=a_right, outputs=a_right_encoded)

model_train.compile(optimizer='adam', loss=lambda y_true,y_pred: y_pred)
model_q_encoder.compile(optimizer='adam', loss='mse')
model_a_encoder.compile(optimizer='adam', loss='mse')

model_train.fit([q,a1,a2], y, epochs=10)
#其中q,a1,a2分别是问题、正确答案、错误答案的batch,y是任意形状为(len(q),1)的矩阵

首先, 我们将损失函数单独地构建为一层:

loss = Lambda(lambda x: K.relu(margin+x[0]-x[1]))([wrong_cos,right_cos])

然后在构建Model时, 指定output为该loss:

model_train = Model(inputs=[q_input,a_right,a_wrong], outputs=loss)

这就相当于指定模型的输出不再是概率, 而是整体的损失. 因此传入到最后计算损失的函数中, 所用的y_pred就是模型得到的损失了, 所以compile方法写为:

model_train.compile(optimizer='adam', loss=lambda y_true,y_pred: y_pred)

这相当于自定义了一个损失函数(符合keras损失函数接受y_true和y_pred的原则), 该函数返回的就是y_pred, 而y_pred就是模型计算得到的样本损失.

由于以往对于分类任务, output参数指定的都是输出概率那一层, 因此在fit的时候对应的喂给output的就是样本真实标签. 而这里, 正确答案和错误答案都已经通过Input占位符的方式传入, 已经计算得到了损失, 不再需要额外的真值了, 所以代码最后一行中的yy, 可以是任意符合shape要求的array, 整个训练过程都不会使用到这个值(而且在构建模型时都不用给这个值一个占位符Input).

Method 2

再以seq2seq任务常见的mask需求为例, 介绍另一种形式, 但原理与方法一是相同的.

计算损失函数时要屏蔽占位符对应产生的损失, 因此可以写为:

def _get_loss(y_pred, y_true):
    y_true = tf.cast(y_true, dtype="int32")
    loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y_true, logits=y_pred)
    mask = tf.cast(tf.not_equal(y_true, 0), dtype="float32")
    loss = tf.reduce_sum(loss * mask, -1) / tf.reduce_sum(mask, -1)

mask是在损失函数中计算的, 当然也可以在compile时直接指定loss为该参数. 但在seq2seq任务中, decoder的输出为预测值, 其真值也会作为输入, 在解码时在每个step中引导下个step的输出, 作为训练的一部分, 而这个真值是从第二个step开始到最后一个step的序列. 因此如果再单独的使用一个Input作为真实结果的占位符, 就需要对原始序列进行处理, 比较麻烦, 同一个变量两次输入也不够优雅.

使用如下的方法:

loss = Lambda(lambda x: self._get_loss(*x))([output, target_decode_out])

self.model = Model([source_input, target_input], loss)
self.model.add_loss([loss])
self.model.compile(optimizer, None)

self.model.metrics_names.append("ppl")
self.model.metrics_tensors.append(Lambda(K.exp)(loss))
self.model.metrics_names.append("accuracy")
self.model.metrics_tensors.append(Lambda(lambda x: self._get_acc(x[0], x[1]))([output, target_decode_out]))

self.output_model = Model([source_input, target_input], output)

仍然是让loss单独作为一层, 并制定为训练模型的output, 但这里指定模型损失时不是在compile中使用lambda函数作为损失函数, 而是使用了模型的add_loss方法. 注意这里的loss要用list的形式传入.

这样如果模型的整体损失是由多部分组成, 就可以使用add_loss函数逐步添加, 而不用构思一个损失函数去做了.

另外需要注意, 由于已经使用了add_loss为模型添加了损失, 在compile的时候就不用再为模型指定损失了, 因此loss参数对应的指定为None.

另外的隐身为对metrics的灵活添加. 普通的metrics对应是在compile时以列表的形式指定, 可以指定多个. 评价函数也是如同损失函数, 接受真值和预测概率, 如果使用到模型中的层或参数, 也可以如同损失函数一样添加.

添加metrics方法使用到model的两个属性, metrics_namesmetrics_tensors, 分别对应评价方法的名字(供显示时使用)和函数, 这个需要直接计算出评价值, 训练的时候就按名称: 值的形式显示.

参考资料

最后更新于

这有帮助吗?