我们都知道,Keras可以使用自定义层,可以使用自定义模型,但是这两者之间缺少一样东西,那就是自定义子模型。这个在Keras官方文档中略有提及(函数式模型),但是介绍的不多,我们这里将它单独拿出来讲。
一般的,在Keras中,已经封装了很多常见的模型,例如CNN,RNN等,但是现在深度学习模型发展迅速,可能一个月一个变化,半年就是一个大变化。因此,我们可能需要更新大量的代码。除了从张量开始进行修改外(即使用K作为后端直接操作向量),还可以利用已知的层构建出更大层,利用已知的模型构建出更大的模型,增加模块的复用性,尤其是当我们需要进行对比实验时,就不需要总是重复很多子模块的开发,增加模型的可读性。
本次我们介绍四种模式,来写属于我们自己的子模型:自定义层、自定义层中层、自定义子模型以及最新的模型子类。
1. 自定义层
当我们需要创建一个新的层的时候,最彻底的办法就是重新写一个层。在Keras中,自定义层需要继承Layer,然后最低实现5个方法,即初始化函数(__init__
),构建可训练参数(build
),配置函数(config
),计算输出(compute_output_shape
)和具体运算(call
)。
1.1 层的声明
一个层,首先应当声明其样子,下面是我们的一个全连接层的例子:
class Dense(Layer):
"""
### 层的定义描述
# Example
### 例子使用
# Arguments
### 参数说明
# Input shape
nD tensor with shape: `(batch_size, ..., input_dim)`.
The most common situation would be
a 2D input with shape `(batch_size, input_dim)`.
# Output shape
nD tensor with shape: `(batch_size, ..., units)`.
For instance, for a 2D input with shape `(batch_size, input_dim)`,
the output would have shape `(batch_size, units)`.
"""
@interfaces.legacy_dense_support # 让函数支持keras 1.x的 API
def __init__(self, units,
activation=None,
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='zeros',
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None,
**kwargs):
if 'input_shape' not in kwargs and 'input_dim' in kwargs:
kwargs['input_shape'] = (kwargs.pop('input_dim'),)
super(Dense, self).__init__(**kwargs)#这一行要有,会省去一些麻烦
#全是各种参数配置
self.units = units
self.activation = activations.get(activation)
### 全是其他参数配置
self.input_spec = InputSpec(min_ndim=2)
self.supports_masking = True
#注意点1:创建可训练的参数
def build(self, input_shape):
assert len(input_shape) >= 2
input_dim = input_shape[-1]
#注意点2:可训练的参数
self.kernel = self.add_weight(shape=(input_dim, self.units),
initializer=self.kernel_initializer,
name='kernel',
regularizer=self.kernel_regularizer,
constraint=self.kernel_constraint)
if self.use_bias:
self.bias = self.add_weight(shape=(self.units,),
initializer=self.bias_initializer,
name='bias',
regularizer=self.bias_regularizer,
constraint=self.bias_constraint)
else:
self.bias = None
self.input_spec = InputSpec(min_ndim=2, axes={-1: input_dim})
self.built = True #设置为真
#注意点3:当我们使用该类创建完一个示例后,实例名()就是在调用Call里的函数
def call(self, inputs):
output = K.dot(inputs, self.kernel)
if self.use_bias:
output = K.bias_add(output, self.bias, data_format='channels_last')
if self.activation is not None:#是否使用激活函数
output = self.activation(output)
return output
#注意点4:计算输出的形状的,下面例子为Dense的维度变换
def compute_output_shape(self, input_shape):
assert input_shape and len(input_shape) >= 2
assert input_shape[-1]
output_shape = list(input_shape)#继承输入的形状
output_shape[-1] = self.units #改变最后一维为当前单元数
return tuple(output_shape)
#注意点5:可以使用config获取该类信息
def get_config(self):
config = {
'units': self.units,
'activation': activations.serialize(self.activation),
###配置信息,省略
}
base_config = super(Dense, self).get_config()
return dict(list(base_config.items()) + list(config.items()))
1.2 层的调用
在上节进行层的声明后,我们就可以使用了,上面声明是Keras自带的Dense层,因此使用起来,我们会更加熟悉。下面是一个dense层的简单调用:
"""声明一个全连接层"""
dense_layer = Dense(hidden_size, activation=hidden_activation)
"""调用这个全连接层"""
dense_output = dense_layer(dense_input)
更多的详情,可以参见《自定义层》。
2. 层中层
这个想法来源于苏大神《让Keras更酷一些》的创意,他提出可以使用一个自定义的“层中层”的概念来复用Keras原有的层,在调用的时候则仍然像普通的层一样调用,从而增加复用性。
首先,要先声明一个层中层的基类,它需要一个新增的方法就是复用(reuse
)。
class OurLayer(Layer):
"""定义新的Layer,增加reuse方法,允许在定义Layer时调用现成的层
"""
def reuse(self, layer, *args, **kwargs):
if not layer.built:
if len(args) > 0:
inputs = args[0]
else:
inputs = kwargs['inputs']
if isinstance(inputs, list):
input_shape = [K.int_shape(x) for x in inputs]
else:
input_shape = K.int_shape(inputs)
layer.build(input_shape)
outputs = layer.call(*args, **kwargs)
for w in layer.trainable_weights:
if w not in self._trainable_weights:
self._trainable_weights.append(w)
for w in layer.non_trainable_weights:
if w not in self._non_trainable_weights:
self._non_trainable_weights.append(w)
return outputs
这样,我们再定义层中层时,就可以继承自己这个基类层,并使用“复用”函数来调用已有的层。下面是一个声明例子:
class OurDense(OurLayer):
"""原来是继承Layer类,现在继承OurLayer类
"""
def __init__(self, hidden_dimdim, output_dim,
hidden_activation='linear',
output_activation='linear', **kwargs):
super(OurDense, self).__init__(**kwargs)
self.hidden_dim = hidden_dim
self.output_dim = output_dim
self.hidden_activation = hidden_activation
self.output_activation = output_activation
def build(self, input_shape):
"""在build方法里边添加需要重用的层,
当然也可以像标准写法一样条件可训练的权重。
"""
super(OurDense, self).build(input_shape)
self.h_dense = Dense(self.hidden_dim,
activation=self.hidden_activation)
self.o_dense = Dense(self.output_dim,
activation=self.output_activation)
def call(self, inputs):
"""直接reuse一下层,等价于o_dense(h_dense(inputs))
"""
h = self.reuse(self.h_dense, inputs)
o = self.reuse(self.o_dense, h)
return o
def compute_output_shape(self, input_shape):
return input_shape[:-1] + (self.output_dim,)
这样在调用自己的层就和我们调用dense层一样随意了。
3. 子模型
但是事实上,一个更加规范的写法是写成子模型,这样不用额外自定义层,只需要follow Keras本身的风格和代码,就可以完成。
所谓的子模型就是只搭建模型,并不进行编译和优化。例如下面是一个自定义子模型的声明:
def double_dense_layer(input_shape=None,hidden_size=100,hidden_activation="relu",output_size=3,output_activation="softmax",loss=0.6):
'''
双层dense,一般可用于最终的分类
:param input_shape: 输入向量的维度
:param hidden_size: 隐藏dense单元数
:param hidden_activation: 隐藏层激活函数
:param output_size: 输出dense单元数
:param output_activation: 输出层激活函数
:param loss: 遗忘概率
:return: 预测结果
'''
text=Input(shape=(int(input_shape[1]),),)
##全连接
dense = Dense(hidden_size, activation=hidden_activation)(text)
##随机损失
output = Dropout(loss)(dense)
#最终预测结果
preds = Dense(output_size, activation=output_activation)(output)
model=Model(inputs=[text], outputs=preds)
return model
在调用的时候,我们仍然像调用普通的层一样调用:
"""声明一个双全连接层"""
ddl=double_dense_layer(input_shape=globalpooling_output.shape,output_activation='sigmoid',output_size=1)
"""调用这个双全连接层"""
finalactivation_out=ddl(globalpooling_output)
4. 模型子类
随着TF2.0的正式发布,模型子类已经可以应用于Keras(版本>=1.6.1)中,这已经和pytorch构建模型的方法非常相似了,下面来看一个Keras中自带的IMDB的CNN模型的模型子类的实现。
from keras.layers import Dense, Dropout, Activation
from keras.layers import Embedding
from keras.layers import Conv1D, GlobalMaxPooling1D
import keras
from easykeras.example.imdb_util import get_train_test
# epoch 10: 88.03
def get_config():
return {"input_shape": (400,),
"max_features": 5000,
"max_len": 400,
"embedding_dims": 50,
"filters": 250,
"kernel_size": 3,
"hidden_dims": 250,
"class_num": 1,
"activation": 'sigmoid'}
class CnnModel(keras.Model):
def __init__(self, config):
"""
初始化模型,准备需要的类
:param config: 参数集合
"""
super(CnnModel, self).__init__(name='cnn_model')
self.embedding_layer = Embedding(config.max_features, config.embedding_dims, input_length=config.max_len)
self.dropout_layer = Dropout(0.2)
self.con_layer = Conv1D(config.filters, config.kernel_size, padding='valid', activation='relu', strides=1)
self.global_pooling_layer = GlobalMaxPooling1D()
self.dense_layer = Dense(config.hidden_dims)
self.dropout_layer2 = Dropout(0.2)
self.activation_layer2 = Activation('relu')
self.final_activation = config.activation
self.class_layer = Dense(config.class_num, activation=self.final_activation)
def call(self, inputs):
"""
构建模型过程,使用函数式模型
:return:
"""
embedding_output = self.embedding_layer(inputs) # (?,400,50)
dropout_output = self.dropout_layer(embedding_output)
con_output = self.con_layer(dropout_output) # (?,398,250)
global_pooling_output = self.global_pooling_layer(con_output) # (?,250)
dense_output = self.dense_layer(global_pooling_output) # (?,250)
activation_output2 = self.activation_layer2(dense_output)
final_out = self.class_layer(activation_output2) # (?,1)
return final_out
class Config:
def __init__(self, config={}):
for key, value in config.items():
setattr(self, key, value)
if __name__ == "__main__":
# 主程序
x_train, y_train, x_test, y_test = get_train_test()
model_config = Config(get_config())
cnn_model = CnnModel(model_config)
# 设定编译参数
loss = 'binary_crossentropy'
optimizer = 'adam'
metrics = ['accuracy']
cnn_model.compile(loss=loss, optimizer=optimizer, metrics=metrics)
# 设置训练超参数
batch_size = 32
epochs = 10
# 训练过程
cnn_model.fit(x=x_train, y=y_train, batch_size=batch_size, epochs=epochs, validation_data=(x_test, y_test))
可以看出其模型构建过程和Pytorch几乎是一模一样。它只需要实现2个方法(init方法用于准备所需要的层,Call方法实现前向传播过程),如果该模型会被当做子模型被其他模型调用,则需要添加def compute_output_shape(self, input_shape):
函数用于计算输出的维度即可。
5. 小结
本章我们主要讲解了4种自定义子模型的方法,分别是自定义层、自定义层中层、自定义子模型以及最新的模型子类方法。
这里推荐使用的是官方的自定义层(继承layer)和模型子类(继承model)这两个方法,因为这是可以无缝衔接原有的Keras的框架。当我们需要进行最底层的新设计时,使用自定义层的方法,因为它可以在最底层操作张量的变换,不要忘记在模型构件时需要声明一下自定义层。当我们只是组装模型中的子模型以便于我们可以更快的进行更换组件时,使用模型子类,尤其是借鉴pytorch中的modellist可以很方便的进行模型的对比实验。
无论使用哪种方式,最重要的是能够清晰的根据一个公式进行代码的正确复现,它更多的看中的是你对于计算图模型和张量变换的熟悉,需要有较强的数学功底,更进一步的,可能需要你创造一个数学公式,并为这个数学公式赋予实际意义,从而进行编码,进行实际的生产和科研。