Keras结合Keras后端搭建个性化神经网络模型(不用原生Tensorflow)

  Keras是基于Tensorflow等底层张量处理库的高级API库。它帮我们实现了一系列经典的神经网络层(全连接层、卷积层、循环层等),以及简洁的迭代模型的接口,让我们能在模型层面写代码,从而不用仔细考虑模型各层张量之间的数据流动。

  但是,当我们有了全新的想法,想要个性化模型层的实现,Keras的高级API是不能满足这一要求的,而换成Tensorflow又要重新写很多轮子,这时,Keras的后端就派上用场了。Keras将底层张量库的函数功能统一封装在“backend”中,用户可以用统一的函数接口调用不同的后端实现的相同功能。所以,如果不追求速度的话,可以仅使用Keras实现你的任何独特想法,从而避免使用原生Tensorflow写重复的轮子。

  我们定义并训练一个神经网络模型需要考虑的要素有三个:层、损失函数、优化器。而我们创新主要在于前两个,因此下面介绍如何结合Keras高级API与后端,自定义特殊神经网络层以及损失函数。

自定义网络层

  自定义层可以通过两种方式实现:使用Lambda层和继承Layer类。

lambda层

  Lambda层仅能对输入做固定的变换,并不能定义可以通过反向传播训练的参数(通过Keras的fit训练),因此能实现的东西较少。以下代码实现了Dropout的功能:

from keras import backend as K
from keras import layers

def my_layer(x):     
  mask = K.random_binomial(K.shape(x),0.5)
  return x*mask*2
x = layers.Lambda(my_layer)(x) 

  其中my_layer函数是自定义层要实现的操作,传递参数只能是Lambda层的输入。定义好函数后,直接在layers.Lambda中传入函数对象即可。实际上,这些变换不整合在lambda层中而直接写在外面也是可以的:

from keras import backend as K
from keras import layers

x = layers.Dense(500,activation='relu')(x) 
mask = K.random_binomial(K.shape(x),0.5)
x = x*mask*2

  数据先经过一个全连接层,然后再被0.5概率Dropout。以上实现Dropout只是作举例,你可以以同样的方式实现其它的功能。

继承layer类

  如果你想自定义可以训练参数的层,就需要继承实现Keras的抽象类Layer。主要实现以下三个方法:

  1、__init__(self, *args, **kwargs):构造函数,在实例化层时调用。此时还没有添加输入,也就是说此时输入规模未知,但可以定义输出规模等与输入无关的变量。类比于Dense层里的units、activations参数。

  2、build(self, input_shape):在添加输入时调用(__init__之后),且参数只能传入输入规模input_shape。此时输入规模与输出规模都已知,可以定义训练参数,比如全连接层的权重w和偏执b。

  3、call(self, *args, **kwargs):编写层的功能逻辑。

单一输入

  当输入张量只有一个时,下面是实现全连接层的例子:

import numpy as np
from keras import layers,Model,Input,utils
from keras import backend as K
import tensorflow as tf

class MyDense(layers.Layer): 
  def __init__(self, units=32): #初始化
    super(MyDense, self).__init__()#初始化父类
    self.units = units  #定义输出规模
  def build(self, input_shape):   #定义训练参数
    self.w = K.variable(K.random_normal(shape=[input_shape[-1],self.units]))  #训练参数
    self.b = tf.Variable(K.random_normal(shape=[self.units]),trainable=True)  #训练参数
    self.a = tf.Variable(K.random_normal(shape=[self.units]),trainable=False) #非训练参数
  def call(self, inputs): #功能实现
    return K.dot(inputs, self.w) + self.b
  
#定义模型
input_feature = Input([None,28,28]) 
x = layers.Reshape(target_shape=[28*28])(input_feature)
x = layers.Dense(500,activation='relu')(x)  
x = MyDense(100)(x)
x = layers.Dense(10,activation='softmax')(x) 
  
model = Model(input_feature,x) 
model.summary() 
utils.plot_model(model)

  模型结构如下:

  在build()中,训练参数可以用K.variable或tf.Variable定义。并且,只要是用这两个函数定义并存入self中,就会被keras认定为训练参数,不管是在build还是__init__或是其它函数中定义。但是K.variable没有trainable参数,不能设置为Non-trainable params,所以还是用tf.Variable更好更灵活些。

多源输入

  如果输入包括多个张量,需要传入张量列表。实现代码如下:

import numpy as np
from keras import layers,Model,Input,utils
from keras import backend as K
import tensorflow as tf

class MyLayer(layers.Layer): 
  def __init__(self, output_dims):
    super(MyLayer, self).__init__()  
    self.output_dims = output_dims
  def build(self, input_shape):  
    [dim1,dim2] = self.output_dims
    self.w1 = tf.Variable(K.random_uniform(shape=[input_shape[0][-1],dim1]))
    self.b1 = tf.Variable(K.random_uniform(shape=[dim1]))  
    self.w2 = tf.Variable(K.random_uniform(shape=[input_shape[1][-1],dim2])) 
    self.b2 = tf.Variable(K.random_uniform(shape=[dim2])) 
  def call(self, x): 
    [x1, x2] = x
    y1 = K.dot(x1, self.w1)+self.b1 
    y2 = K.dot(x2, self.w2)+self.b2
    return K.concatenate([y1,y2],axis = -1)
 
#定义模型
input_feature = Input([None,28,28])#输入
x = layers.Reshape(target_shape=[28*28])(input_feature) 
x1 = layers.Dense(500,activation='relu')(x)  
x2 = layers.Dense(500,activation='relu')(x)  
x = MyLayer([100,80])([x1,x2])   
x = layers.Dense(10,activation='softmax')(x) 
  
model = Model(input_feature,x) 
model.summary() 
utils.plot_model(model,show_layer_names=False,show_shapes=True)

  模型结构如下:

  总之,传入张量列表,build传入的input_shape就是各个张量形状的列表。其它都与单一输入类似。

自定义损失函数

  根据Keras能添加自定义损失的特性,这里将添加损失的方法分为两类:

  1、损失需要根据模型输出与真实标签来计算,也就是只有模型的输出与外部真实标签作为计算损失的参数。

  2、损失无需使用外部真实标签,也就是只用模型内部各层的输出作为计算损失的参数。

  这两类损失添加的方式并不一样,希望以后Keras能把API再改善一下,这种冗余有时让人摸不着头脑。

第一类损失

  这类损失可以通过自定义函数的形式来实现。函数的参数必须是两个:真实标签与模型输出,不能多也不能少,并且顺序不能变。然后你可以在这个函数中定义你想要的关于输出与真实标签之间的损失。然后在model.compile()中将这个函数对象传给loss参数。代码示例如下(参考链接):

def customed_loss(true_label,predict_label): 
  loss = keras.losses.categorical_crossentropy(true_label,predict_label)  
  loss += K.max(predict_label)
  return loss

model.compile(optimizer='rmsprop', loss=customed_loss)

  如果硬是想用这种方法把模型隐层的输出拿来算损失的话,也不是不可以。只要把相应隐层的输出添加到模型的输出列表中,自定义损失函数就可以从模型输出列表中取出隐层输出来用了。即:

model = Model(input,[model_output, hidden_layer_output])

  当然,这样就把模型结构改了,如果不想改模型的结构而添加“正则化”损失,可以使用下面的方法。

第二类损失

  这类损失可以用Model.add_loss(loss)方法实现,loss可以使用Keras后端定义计算图来实现。但是显然,计算图并不能把未来训练用的真实标签传入,所以,add_loss方法只能计算模型内部的“正则化”损失。

  add_loss方法可以使用多次,损失就是多次添加的loss之和。使用了add_loss方法后,compile中就可以不用给loss赋值,不给loss赋值的话使用fit()时就不能传入数据的标签,也就是y_train。如果给compile的loss赋值,最终的目标损失就是多次add_loss添加的loss和compile中loss之和。另外,如果要给各项损失加权重的话,直接在定义loss的时候加上即可。代码示例如下:

loss = 100000*K.mean(K.square(somelayer_output))#somelayer_output是定义model时获得的某层输出
model.add_loss(loss)
model.compile(optimizer='rmsprop')

  以上讲的都是关于层输出的损失,层权重的正则化损失并不这样添加,自定义正则项可以看下面。

  keras中添加正则化_Bebr的博客-CSDN博客_keras 正则化

  里面介绍了已实现层的自定义正则化,但没有介绍自定义层的自定义正则化,这里先挖个坑,以后要用再研究。