keras框架要求给定的损失函数有且只有y_pre和y_true两个入参。但许多损失函数并不是这个格式,比如推荐系统中用到的三元组损失,又比如变分词嵌入中变分推断模型的损失函数。因而需要用一些“奇技淫巧”才能在keras中实现复杂损失函数。
背景
在实现变分词嵌入版的itag时,我遇到了复杂损失函数的问题,使用了变分词嵌入的模型有两个损失函数,一个是主任务(标签推荐)的损失函数,一个是变分推断的损失函数。前者有对应的y_pre和y_true,而后者并没有,不仅没有,损失函数的运算步骤还非常复杂。
这导致了我无法使用keras框架,但itag本身是keras实现的,如果搬运到pytorch中,会耗费我大量的时间重构代码,最后我选择在keras中使用一些hook的方法实现复杂损失函数。
苏剑林有一篇利用embedding层实现复杂损失函数的博文,因为embedding本质上是一个矩阵,换句话说它可以当一个全连接层用,可以通过embedding实现一些复杂的损失函数,有兴趣的可以去看卡。但我用的不是这个方法。
问题分析
使用了变分词嵌入的模型有两个损失函数,一个是主任务的损失一个是变分词嵌入的损失,其中变分词嵌入的损失的具体实现如下(有修改):
1 2 3 4 5 6 7 8 9
| ... weight_decay, learning_rate = get_decay_rate(epochs) ... reg_loss = weight_decay * embedding.regularizer() ... loss = cross_entropy + reg_loss ... self.optimizer = tf.compat.v1.train.AdamOptimizer(learning_rate).minimize(loss, global_step=global_step, var_list=trainables) ...
|
- 根据当前的epoch值生成权重递减值和学习率(具体算法不重要)
- 调用嵌入层中的一个方法生成变分损失
- 对这个损失进行权重递减
- 和主任务损失相加
- 反向传播,用了adam优化器但是仍然每轮手动调整学习率
上面的embedding.regularizer()方法实现如下:
1 2 3 4 5 6
| # KL散度(变分估计损失)计算函数 def regularizer(self): k1, k2, k3 = 0.63576, 1.8732, 1.48695 log_alpha = self.clip(self.embedding_logdropout_ratio) KLD = -tf.reduce_sum(k1 * tf.sigmoid(k2 + k3 * log_alpha) - 0.5 * tf.nn.softplus(-log_alpha) - k1) return KLD
|
即变分词嵌入的损失的计算实际上需要的外部输入只有一个epoch,并不符合keras框架的对损失函数要求,因此不能使用keras的loss设定方法。另外还有一个问题,在keras框架中,epoch中无法从被训练的模型内部获取,因而也需要一个解决方案。
对keras进行hooc
通过定义一个“损失层”,在层内完成复杂损失的计算逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class ComplexLoss(Layer): def __init__(self, **kwargs): super(ComplexLoss, self).__init__(**kwargs) def build(self, input_shape): super(ComplexLoss, self).build(input_shape)
def call(self, inputs, **kwargs): ''' y_true, y_pred用于计算主任务损失,即y_true也需要作为输入数据。 由之前计算KL散度的代码可知,计算KL散度需要外部输入epoch和变分嵌入层的embedding_logdropout_ratio。 为了代码的解耦(不在嵌入层内进行损失的计算),由嵌入层将embedding_logdropout_ratio作为返回值,传递给ComplexLoss层。 ''' y_true, y_pred, embedding_logdropout_ratio, epoch = inputs cross_loss = mean_negative_log_probs(y_true, y_pred) reg_loss = reg_loss_function(embedding_logdropout_ratio, epoch) main_loss = cross_loss + reg_loss self.add_loss(main_loss, inputs=True) self.add_metric(main_loss, aggregation="mean", name="loss") return main_loss
|
在使用一个定制loss层后,loss的计算在这个层内完成了,因而keras的外围代码也需要修改:
(以下代码为简化版,仅为说明ComplexLoss的用法,实际上无法运行)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| encoder_input = Input() decoder_input = Input()
y_true_input = Input()
epoch_input = Input()
encoder = EnCoder() decoder = Decoder() loss_layer = ComplexLoss() decoder_dense = Dense()
encoder_outputs, state, embedding_logdropout_ratio = encoder(encoder_input) decoder_outputs, n_state = decoder(encoder_outputs, initial_state=state) output = decoder_dense(decoder_outputs) main_loss = loss_layer([y_true_input, output, embedding_logdropout_ratio, epoch_input])
model = Model(inputs=[encoder_input, decoder_input, y_true_input, epoch_input], outputs=[main_loss])
adam = optimizers.Adam(lr=0.001)
model.compile(optimizer=adam) model.fit()
|
以上代码和普通的keras代码的两个主要区别是:
- y_true需要作为input的一部分(以传递给损失层)
- 不再需要在compile中指定损失函数(因为已经通过损失层实现了)
总结
使用以上损失层的方法,几乎可以在keras中定义任何形式的损失函数而不受框架的限制。