##haohaohao###Keras中无损实现复杂(多入参)的损失函数

本文基于比较古旧的KERAS=2.1.5版本,运用了最新tf2.0以及tf.keras特性的更好版本请移步我的另一篇文章:

Ziyigogogo:Tensorflow2.0中复杂损失函数实现​zhuanlan.zhihu.com

前言

Keras中,直接利用API可以快速的实现一些功能简单的自定义损失函数:

model.compile(loss=YOUR_CUSTOM_LOSS_FUNCTION)

然而任何的简单都是有代价的,通过这个内置方法定义的损失函数有且只能有y_true和y_pred两个入参:

def simple_loss(y_true, y_pred):
    pass

由于Keras的目标是让非编码专业的人士也能接触AI,这样的设计也不是没有道理的,因为这样可以在降低初阶用户使用门槛的同时规避一些乱七八糟的Bug。但是,遵照这样的设计理念(Keras团队原话"as designed"),模型中无法直接获取fit_generator()中传入的target(y_true),导致复杂损失函数在Keras中的实现稍显麻烦(其实也不难)。不过,为了Keras漂亮的进度条,这点麻烦算什么呢?

背景

在复杂的模型设计中,Loss并不能简单的由y_true和y_pred计算出来,这里,我们用近年来著名的Mask-rcnn来帮助理解(细节其实不用多想,只用注意到需求就可以了):

粗略的来说,Mask-rcnn是由下面三个部分组成的

1. Backbone

前半部分选择Resnet, Xception等任一工作良好的卷积网络用作Feature提取,后半部分利用 Feature Pyramid Network(FPN) 成多尺度的Feature Map

2. Region Proposal Network (RPN)

根据Feature Map来生成感兴趣区域(ROIs)

3. 并行的两个子网络

  • ROI Classifier和Bounding Box Regressor 根据RPN提供的ROI中判断并生成最终检测目标的类别(class)与边界框(Bounding box)
  • Mask卷积网络 用于生成最后的Mask

这是一个包含多个子模型的复杂模型,#3子模型 的损失函数的在y_true和y_pred之外还需要 #2子模型 输出的ROIs作为入参。 此时,两个入参(y_true, y_pred)的简单损失函数便无法胜任了。

方法

接下来便开始讲解如何 无损的用Keras来构造类似def my_loss(y_true, y_pred, another_input_01, another_input_02, ...)这样的复杂损失函数。这里的无损,指的是相较于苏剑林-科学空间中的方法1,本文所介绍的方法不会损失Keras自带Metrics显示。事实上,本文介绍的方法更像是上述连接中方法的一个完善,但由于本人先受到MatterPort的启发找到解决方法以后再看到的这篇Blog,所以这里便不说是以苏神的想法为参考了。

作为例子,我们首先构造一个简单的网络结构以及一个简单的只有2个参数的自定义loss

from keras import layers as KL
from keras import models as KM

def create_simple_model():
    input_img = KL.Input([64, 64, 3])
    branch1 = KL.Conv2D(64, (3, 3), strides=(4, 4), activation="relu")(input_img)
    branch2 = KL.Conv2D(64, (3, 3), strides=(4, 4), activation="relu")(input_img)
    concat1 = KL.Concatenate()([branch1, branch2])
    deconv1 = KL.Deconv2D(1, (3, 3), strides=(4, 4), activation="relu")(concat1)
    output = KL.Conv2D(1, (1, 1), strides=(1, 1), activation="sigmoid")(deconv1)
    return KM.Model(inputs=input_img, outputs=output)

def my_simple_loss(y_true, y_pred):
    # do what you want here
    return binary_crossentropy(y_true, y_pred)

定义随机生成数据的generator:

import numpy as np

def fake_data_generator(num_samples):
    while (1):
        imgs = np.random.random((num_samples, 64, 64, 3)).astype("float32")
        masks = np.random.random((num_samples, 64, 64, 1)).astype("float32")
        yield imgs, masks

编译模型并开始训练:

train_gen = fake_data_generator(10)
val_gen = fake_data_generator(5)

model = create_simple_model()
model.summary()
model.compile(optimizer="adam", loss=my_simple_loss)
model.fit_generator(
    train_gen,
    epochs=10,
    steps_per_epoch=50,
    validation_data=val_gen,
    validation_steps=5
)

然后Keras经典的实时训练的进度条便出现了:

接下来便是重头戏了,多个入参的复杂损失函数如何实现呢?

我们首先定义这样一个函数,分别用网络中不同层deconv1, output的输出与y_pred分别求不同的loss然后相加得到最后总的loss(hint:把不同的loss结合起来求一个总的loss是一个很常用的技巧,可以综合不同loss的优点,在Data Science Bowl 2018中,第一名的获得者就是使用了加权的dice loss和bce loss最终得到了令人惊讶的成绩。当然,本文这里的2个loss结合的例子并没有什么道理,只是为了介绍方法,请勿生搬硬套)。

from keras.losses import mean_squared_error, binary_crossentropy

def my_complex_loss_graph(target, deconv1, output):
    mse_deconv1 = mean_squared_error(target, deconv1)
    bce_output = binary_crossentropy(target, output)
    final_loss = mse_deconv1 + bce_output
    return K.mean(final_loss)

有了3个入参的损失函数,我们的模型也必须做相应的更改:

import tensorflow as tf 

def create_complex_model(mode="train"):
    assert mode in ("train", "predict"), "only 'train' and 'predict' mode supported"

    input_img = KL.Input([64, 64, 3])
    branch1 = KL.Conv2D(64, (3, 3), strides=(4, 4), activation="relu")(input_img)
    branch2 = KL.Conv2D(64, (3, 3), strides=(4, 4), activation="relu")(input_img)
    concat1 = KL.Concatenate(name="concat1")([branch1, branch2])
    deconv1 = KL.Deconv2D(1, (3, 3), strides=(4, 4), activation="relu")(concat1)
    output = KL.Conv2D(1, (1, 1), strides=(1, 1), activation="sigmoid")(deconv1)

    if mode == "train":
        #本文最开始提到过,keras generator中yield input, target的target是无法获取
        #参考github issues:https://github.com/keras-team/keras/issues/11812
        #所以为了取到target,我们必须须把target也当作inputs的一部分传进来即 
        #yield  [input,target], [], 然后再通过KL.Input按顺序获取
        target = KL.Input([64, 64, 1], name="target")
        my_complex_loss = KL.Lambda(
            lambda x: my_complex_loss_graph(*x), name="complex_loss"
        )([target, deconv1, output])
        inputs = [input_img, target]
        outputs = [output, my_complex_loss]
    else:
        #predict阶段,就不用计算loss了所以这里不加入loss层和metric层
        inputs = input_img
        outputs = output

    model = KM.Model(inputs=inputs, outputs=outputs)

    #重点
    model._losses = []
    model._per_input_losses = {}
    #通过add_loss来把之前通过KL.Lambda定义的层加入loss,当添加了多个loss层时,optimizer实际优  
    #化的是多个loss的和
    for loss_name in ["complex_loss"]:
        layer = model.get_layer(loss_name)
        if layer.output in model.losses:
            continue
        loss = tf.reduce_mean(layer.output, keepdims=True)
        model.add_loss(loss)
    #其实这里可以添加的不只loss, 有助于监视模型情况的metrics比如f1 score, iou等等也可以通过   
    #model.metrics_tensors.append()来添加

    return model

别被突然增加的代码吓到,其实原理很简单,把loss的计算图通过Lambda转换为layer然后把layer通过add_loss编译进模型,相应的,generator也需修改一下:

def fake_data_generator_2(num_samples):
    while (1):
        imgs = np.random.random((num_samples, 64, 64, 3)).astype("float32")
        masks = np.random.random((num_samples, 64, 64, 1)).astype("float32")
        inputs = [imgs, masks]
        targets = []
        yield inputs, targets

训练:

train_gen = fake_data_generator_2(10)
val_gen = fake_data_generator_2(5)
model = create_complex_model("train")
model.summary()
model.compile(
    optimizer="adam",
    loss=[None] * len(model.outputs)
)
model.fit_generator(
    train_gen,
    epochs=10,
    steps_per_epoch=50,
    validation_data=val_gen,
    validation_steps=5
)

Keras进度条如下:

最后

当然,如果你如果通过上面代码注释中的方法添加了多个loss和多个metrics的话,你的进度条可能是这样的(这里loss != mask_bce_loss+mask_dice_loss是因为如果把所有loss都显示在进度条上的话会看起来特别凌乱,所以我隐藏了一部分loss,实际上他们还是在工作的):

唔,真是...赏心悦目啊! Happy tuning!

 

 

Tensorflow 2.0自4月初alpha发布以来,引起了广泛关注。其中,谷歌携手@fchollet(Keras作者)及其团队对Keras库做出了大量Tensorflow专属的优化以及改动。再联想到独立(Stand alone)的Keras库最近一次更新2.2.4已经是大半年(2018年10月)以前的事情了,不禁八卦Keras团队的工作重心是不是从独立Keras转向了tf.keras来对抗Pytorch的竞争了呢?

前言

自TF2.0发布以来,我的工作就是把公司之前Tensorflow+Keras(stand-alone)的AI框架转移到Tensorflow2.0中。一方面是希望用到TF2.0的一系列新特性,另一方面,由于之前独立Keras与Tensorflow的工作流程中,遇到过多次版本不匹配带来不易察觉的问题,所以希望能利用上tf.keras来减少环境依赖从而避免这种无谓的坑。

通过数月的持续实践,我惊喜的发现tf.keras并不是直接无脑把独立keras搬进了Tensorflow,谷歌及Keras团队为tf.keras做出的一系列专属优化使得tf.keras无论是在执行性能,模型表现还是易用性上相比独立Keras+Tensroflow的模式都更胜一筹,2个简单的例子:

  • tf.data在构建数据管道(Input Pipeline)的时候,速度以及稳定性都完爆独立Keras中的DataSequence(实际测试中,良好的调优下tf.data.Dataset在model.fit()中数据准备的效率是DataSequence的4倍以上)
  • tf.distribute在多GPU训练中相对于独立Keras中的multi_gpu_model函数,在显存占用,训练速度,以及最终模型表现中都明显更优。

于是感受到明显技术进步的我决定写文章来记录我几个月以来对TF2.0使用的一些实践。

在上一篇文章中:

Ziyigogogo:Keras中无损实现复杂(多入参)的损失函数​zhuanlan.zhihu.com

我介绍过如何用独立Keras库如何实现复杂的多入参损失函数。对比之前的实现,这篇文章将介绍TF2.0中一种更好的方法,使得我们自定义的复杂损失函数可以更容易的在不同的模型架构中重复使用,下面直接上代码:

以下代码基于tf2.0 beta版本实现,安装方法:

pip install tensorflow-gpu==2.0.0-beta1

首先导包:

import tensorflow as tf
from tensorflow.python.keras import backend as K
from tensorflow.python.keras import layers as KL
from tensorflow.python.keras import models as KM
import numpy as np

接下来利用Subclass自定义一个损失函数层:

​
class WbceLoss(KL.Layer):
    def __init__(self, **kwargs):
        super(WbceLoss, self).__init__(**kwargs)
​
    def call(self, inputs, **kwargs):
        """
        # inputs:Input tensor, or list/tuple of input tensors.
        如上,父类KL.Layer的call方法明确要求inputs为一个tensor,或者包含多个tensor的列表/元组
        所以这里不能直接接受多个入参,需要把多个入参封装成列表/元组的形式然后在函数中自行解包,否则会报错。
        """
        # 解包入参
        y_true, y_weight, y_pred = inputs
        # 复杂的损失函数
        bce_loss = K.binary_crossentropy(y_true, y_pred)
        wbce_loss = K.mean(bce_loss * y_weight)
        # 重点:把自定义的loss添加进层使其生效,同时加入metric方便在KERAS的进度条上实时追踪
        self.add_loss(wbce_loss, inputs=True)
        self.add_metric(wbce_loss, aggregation="mean", name="wbce_loss")
        return wbce_loss

可以看到,相对于之前使用Lambda把损失函数包装成Layer的写法,我们现在使用了KL.Layer的Subclass写法,看起来似乎代码行数增加了,但是,使用起来却会方便许多:

def my_model():
    # input layers
    input_img = KL.Input([64, 64, 3], name="img")
    input_lbl = KL.Input([64, 64, 1], name="lbl")
    input_weight = KL.Input([64, 64, 1], name="weight")
    
    predict = KL.Conv2D(2, [1, 1], padding="same")(input_img)
    my_loss = WbceLoss()([input_lbl, input_weight, predict])
​
    model = KM.Model(inputs=[input_img, input_lbl, input_weight], outputs=[predict, my_loss])
    model.compile(optimizer="adam")
    return model

然后我们构建假的数据来实验一下我们的模型是否工作:

def get_fake_dataset():
    def map_fn(img, lbl, weight):
        inputs = {"img": img, "lbl": lbl, "weight": weight}
        targets = {}
        return inputs, targets
​
    fake_imgs = np.ones([500, 64, 64, 3])
    fake_lbls = np.ones([500, 64, 64, 1])
    fake_weights = np.ones([500, 64, 64, 1])
    fake_dataset = tf.data.Dataset.from_tensor_slices(
        (fake_imgs, fake_lbls, fake_weights)
    ).map(map_fn).batch(10)
    return fake_dataset
​
​
model = my_model()
my_dataset = get_fake_dataset()
model.fit(my_dataset)

 

然后就是熟悉的keras进度条了:

50/50 [==============================] - 1s 24ms/step - loss: 7.8311 - wbce_loss: 7.8311
​
# 可以根据需求,把多个自定义loss层加入模型:
554/554 [==============================] - 402s 725ms/step - loss: 0.3199 - wbce_loss: 0.0681 - dice_loss: 0.2518 
# 其中loss的数值就代表多个自定义loss的和: 0.3199 = 0.0681 + 0.2518

 

可以看到,相比上一篇文章中,在model构建之后手动向model._losses的私有属性中添加loss这种偏hack的方法,现在的实现更加优雅方便,可以说和即插即用相差无几了。但是正如上一篇文章中提到的按照Keras作者的设计:我们依然无法获取fit()中传入的target。所以需要把target和input一起传进来。所以,真正像pytorch中那样完全没有额外步骤的损失函数在这个限制开放以前,是无法在keras中实现的。

结束语

本文介绍了TF2.0中一种比之前更加便捷的复杂损失函数的写法,同时代码中刻意引出了利用tf.data来构建了简易的数据管道的列子。关于tf2.0中tf.data的详细用法以及最佳实践将会在下一篇文章中详细介绍。

你可能感兴趣的:(Deep,Learning)