编译:yxy
出品:ATYUN订阅号
Keras是最广泛使用的深度学习框架之一。它在易于使用的同时,在性能方面也与TensorFlow,Caffe和MXNet等更复杂的库相当。除非你的应用程序需要一些非常低级别和复杂的代码,否则Keras会为你提供最好的帮助!
而对于Keras来说,还有更多的东西可以满足你的需求。今天我们分享了一些相对少用但又很棒的东西,你可以用Keras和你需要的代码来实现它。这些将帮助你直接在Keras中编写所有自定义内容,而无需切换到其他更繁琐和复杂的库。
自定义度量和损失函数
Keras自带许多内置度量和损失函数,这些函数在大多数情况下都非常有用。但很可惜,只有最常见的度量和损失函数是内置的。所有度量基本都是某种形式的准确率,损失倒是有很多选择,但最新的研究成果并不多。如果你想要一些前沿的东西,你需要自己实现。
而这就是我们要做的了!所有Keras损失和度量的定义方式与具有两个输入变量的函数相同:地面真值(ground truth)和预测值,函数始终返回度量或损失的值。你唯一需要注意的是,矩阵上的任何操作都应该Keras与TensorFlow的Tensors完全兼容,因为这是Keras总是期望从这些自定义函数中获得的格式。这可以通过使用Python的math,Keras或TensorFlow操作来实现。
看起来很简单!以下是如何创建和应用自定义损失和自定义度量的示例。我实现了通常用于度量图像质量的PSNR度量。而对于损失函数,我实现了Charbonnier,它已经被证明比L1或L2损失更能抵抗异常值。我们编写函数后,只需将它们传递给我们的模型编译函数即可!
Charbonnier:https://arxiv.org/pdf/1701.03077.pdf
代码语言:javascript复制import math
代码语言:javascript复制from kerasimport backend as K
代码语言:javascript复制
代码语言:javascript复制# Define our custom metric
代码语言:javascript复制def PSNR(y_true, y_pred):
代码语言:javascript复制 max_pixel= 1.0
代码语言:javascript复制 return 10.0 * math.log10((max_pixel** 2)/ (K.mean(K.square(y_pred- y_true))))
代码语言:javascript复制
代码语言:javascript复制# Define our custom loss function
代码语言:javascript复制def charbonnier(y_true, y_pred):
代码语言:javascript复制 epsilon= 1e-3
代码语言:javascript复制 error= y_true- y_pred
代码语言:javascript复制 p= K.sqrt(K.square(error) K.square(epsilon))
代码语言:javascript复制 return K.mean(p)
代码语言:javascript复制
代码语言:javascript复制# Compile our model
代码语言:javascript复制adam= Adam(lr=0.0001)
代码语言:javascript复制model.compile(loss=[charbonnier], metrics=[PSNR], optimizer=adam)
自定义层
与度量和损失函数类似,如果你想要使用标准卷积,池化和激活函数之外的东西,你可能会发现自己需要创建自定义的层。在这种情况下,你可以按照我在下面给出的代码示例来实现它!
从Keras文档中我们最需要实现的是:
- call(x):这就是层的逻辑所在。除非你希望你的层支持屏蔽(mask),否则你只需关心传递给call的第一个参数:输入张量。
- get_output_shape_for(input_shape):如果你的层修改了其输入的形状,则应在此处指定形状转换的逻辑。这可以让Keras进行自动形状推断。
在下面的例子中,我想要一个能自动将图片调整到我想要的大小的层。为此,我需要使用blinear,bicubic或最近邻调整(nearest neighbour resizing)。我定义了call()函数的第一个输入为x(即图像张量),和第二个输入(可选)method(这是我要选择的调整大小的方法。调整的scale被定义在初始化函数__init__内 。要坚持使用TensorFlow操作(所以我们总是使用Keras或TensorFlow张量),我们根据取整的scale调整并返回图像。在get_output_shape_for()函数中我计算并返回输出张量的完整形状。
现在我们已经编写了自定义层的代码,假设我们的图像张量被定义为image,我们要将它与Functional API一起使用,就像这样调用它:
image_2 = resize_layer(scale = 2)(image,method =“bilinear”)
代码语言:javascript复制import tensorflow as tf
代码语言:javascript复制
代码语言:javascript复制def tf_int_round(num):
代码语言:javascript复制 return tf.cast(tf.round(num), dtype=tf.int32)
代码语言:javascript复制
代码语言:javascript复制class resize_layer(layers.Layer):
代码语言:javascript复制 # Initialize variables
代码语言:javascript复制 def __init__(self, scale,**kwargs):
代码语言:javascript复制 self.scale= scale
代码语言:javascript复制 super(resize_layer,self).__init__(**kwargs)
代码语言:javascript复制
代码语言:javascript复制 def build(self, input_shape):
代码语言:javascript复制 super(resize_layer,self).build(input_shape)
代码语言:javascript复制
代码语言:javascript复制 # Defining how we will call our function
代码语言:javascript复制 def call(self, x, method="bicubic"):
代码语言:javascript复制 height= tf_int_round(tf.cast(tf.shape(x)[1],dtype=tf.float32)* self.scale)
代码语言:javascript复制 width= tf_int_round(tf.cast(tf.shape(x)[2],dtype=tf.float32)* self.scale)
代码语言:javascript复制
代码语言:javascript复制 if method== "bilinear":
代码语言:javascript复制 return tf.image.resize_bilinear(x, size=(height, width))
代码语言:javascript复制 elif method== "bicubic":
代码语言:javascript复制 return tf.image.resize_bicubic(x, size=(height, width))
代码语言:javascript复制 elif method== "nearest":
代码语言:javascript复制 return tf.image.resize_nearest_neighbor(x, size=(height, width))
代码语言:javascript复制
代码语言:javascript复制 # Defining the computation of the output shape
代码语言:javascript复制 def get_output_shape_for(self, input_shape):
代码语言:javascript复制 height= tf_int_round(tf.cast(tf.shape(x)[1],dtype=tf.float32)* self.scale)
代码语言:javascript复制 width= tf_int_round(tf.cast(tf.shape(x)[2],dtype=tf.float32)* self.scale)
代码语言:javascript复制 return (self.input_shape[0], height, width, input_shape[3])
代码语言:javascript复制
代码语言:javascript复制# Using our new custom layer with the Functional API
代码语言:javascript复制image_2= resize_layer(scale=2)(image, method="bilinear")
内置预处理
Keras带有几个在ImageNet上具有预训练的权重的模型,你可以直接使用它们。但是,如果你想直接使用这些模型,需要事先调整图像大小,因为最后完全连接层会强制固定输入大小。例如,Xception模型使用299×299的图像进行训练,那么所有图像都必须设置为大小以避免错误。除此之外,模型可能会有一些其他类型的你希望在向模型传递图像时自动应用它们的预处理或后处理。
我们可以使用Keras的Lambda层在模型中内置任何数学或预处理操作!lambda将简单地定义你要应用的操作。全层Lambda允许你将功能完全融入模型中。查看下面的代码,了解我们如何在模型中嵌入重新调整大小以及Xception的预处理!
代码语言:javascript复制from keras.applications.nasnetimport Xception, preprocess_input
代码语言:javascript复制from keras.modelsimport Sequential, Model
代码语言:javascript复制from keras.layers.coreimport Lambda
代码语言:javascript复制from keras.backendimport tf as ktf
代码语言:javascript复制
代码语言:javascript复制# Initialize a Xception model
代码语言:javascript复制Xception_model= Xception(include_top=True, weights='imagenet', input_tensor=None, input_shape=None)
代码语言:javascript复制
代码语言:javascript复制# Any required pre-processing should be baked into the model
代码语言:javascript复制input_tensor= Input(shape=(None,None,3))
代码语言:javascript复制x= Lambda(lambda image: ktf.image.resize_images(image, (299,299)))(input_tensor)
代码语言:javascript复制x= Lambda(lambda image: preprocess_input(image))(x)
代码语言:javascript复制output_tensor= Xception_model(x)
代码语言:javascript复制
代码语言:javascript复制final_Xception_model= Model(input_tensor, output_tensor)
重复块的函数化
如果我们想要编写一个大型模型,比如50或甚至100层深的模型,代码就会变得非常混乱。当你必须定义极多的层,除非都是残差连接或稠密连接,否则你会发现代码极为散乱!
相反,我们实际上可以使用functional API的一个小技巧,将重复代码块定义为函数。例如,ResNet具有许多具有相同基本组件(批标准化,激活函数和卷积)的重复的残差块。因此,我们可以简单地将这些操作定义为函数中的一个块,从而极大地简化代码。查看下面的代码,它实现了ResNet和DenseNet块,并向你展示了如何使用它们。
view source
代码语言:javascript复制def preact_conv(inputs, k=3, filters=64):
代码语言:javascript复制 outputs= BatchNormalization()(inputs)
代码语言:javascript复制 outputs= Activation('relu')(outputs)
代码语言:javascript复制 outputs= Conv2D(filters, kernel_size=(k, k), padding='same',
代码语言:javascript复制 kernel_initializer="glorot_normal")(outputs)
代码语言:javascript复制
代码语言:javascript复制 return outputs
代码语言:javascript复制
代码语言:javascript复制def ResidualBlock(inputs, kernal_size=3, filters=64):
代码语言:javascript复制 outputs= preact_conv(inputs, k=kernal_size, n_filters=filters)
代码语言:javascript复制 outputs= preact_conv(outputs, k=kernal_size, n_filters=filters)
代码语言:javascript复制
代码语言:javascript复制 outputs= add([outputs, inputs])
代码语言:javascript复制 return outputs
代码语言:javascript复制
代码语言:javascript复制def DenseBlock(stack, n_layers, growth_rate):
代码语言:javascript复制 new_features= []
代码语言:javascript复制 for iin range(n_layers):
代码语言:javascript复制 layer= preact_conv(stack, filters=growth_rate)
代码语言:javascript复制 new_features.append(layer)
代码语言:javascript复制 # stack new layer
代码语言:javascript复制 stack= concatenate([stack, layer], axis=-1)
代码语言:javascript复制 new_features= concatenate(new_features, axis=-1)
代码语言:javascript复制 return new_features
代码语言:javascript复制
代码语言:javascript复制
代码语言:javascript复制# Applying a stack of 5 Residual Blocks for a ResNet, just 5 lines of code
代码语言:javascript复制# If we wrote this out layer by layer, this would probably take 4-5x the number of lines
代码语言:javascript复制x= ResidualBlock(x)
代码语言:javascript复制x= ResidualBlock(x)
代码语言:javascript复制x= ResidualBlock(x)
代码语言:javascript复制x= ResidualBlock(x)
代码语言:javascript复制x= ResidualBlock(x)
代码语言:javascript复制
代码语言:javascript复制# Applying a stack of 5 Dense Blocks for a DenseNet, just 5 lines of code
代码语言:javascript复制# DenseNets are even more complex to implements than ResNets, so if we wrote
代码语言:javascript复制# this out layer by layer, this would probably take 5-10x the number of lines
代码语言:javascript复制x= DenseBlock(x, n_layers=4, growth_rate=12)
代码语言:javascript复制x= DenseBlock(x, n_layers=6, growth_rate=12)
代码语言:javascript复制x= DenseBlock(x, n_layers=8, growth_rate=12)
代码语言:javascript复制x= DenseBlock(x, n_layers=10, growth_rate=12)
代码语言:javascript复制x= DenseBlock(x, n_layers=12, growth_rate=12)