层的自定义
Keras中自定义层及其一些运用技巧,在这之中我们可以看到Keras层的精巧之处。
基本定义方法
在Keras中,自定义层的最简单方法是通过Lambda层的方式:
from keras.layers import *
from keras import backend as K
x_in = Input(shape=(10,))
x = lambda(lambda x:x+2)(x_in) # 对输入加上2
有时候,我们希望区分训练阶段和测试阶段,比如训练阶段给输入加入一些噪声,而测试阶段则去掉噪声,这需要用K.in_train_phase实现,比如
def add_noise_in_train(x):
x_ = x + K.random_normal(shape = K.shap(x))# 加上标准高斯噪声
return K.in_trian_phase(x_,x)
x_in = Input(shape=(10,))
x = Lambda(add_noise_in_train)(x_in) # 训练阶段加入高斯噪声,测试阶段去掉
当然,Lambda层仅仅适用于不需要增加训练参数的情形,如果想要实现的功能需要往模型新增参数,那么就必须要用到自定义Layer了。其实这也不复杂,相比于Lambda层只不过代码多了几行,官方文章已经写得很清楚了:
https://keras.io/layers/writing-your-own-keras-layers/
class MyLayer(Layer):
def __init__(self,output_dim,**kwargs):
self.output_dim = output_dim # 可以自定义一些属性,方便调用
super(MyLayer,self).__init__(**kwargs) # 必须
def build(self,input_shape):
# 添加可训练参数
self.kernel = self.add_weight(name='kernel',
shape=(input_shape[1],self.output_dim),
initializer='uniform',
trainable=True)
def call(self,x):
# 定义功能,相当于Lambda层的功能函数
return K.dot(x, self.kernel)
def compute_output_shape(self,input_shape):
# 计算输出形状,如果输入和输出形状一致,那么可以省略,否则最好加上
return (input_shape[0], self.output_dim)
双输出的层
平时我们碰到的所有层,几乎都是单输出的,包括Keras中自带的所有层,都是一个或者多个输入,然后返回一个结果输出的。那么Keras可不可以定义双输出的层呢?答案是可以,但要明确定义好output_shape,比如下面这个层,简单地将输入切开分两半,并且同时返回。
class SplitVector(Layer):
def __init__(self,**kwargs):
super(SplitVector,self).__init__(**kwargs)
def call(self,inputs):
# 按第二个维度对tensor进行切片,返回一个list
in_dim = K.int_shape(inputs)[-1]
return [inputs[:,:in_dim//2],inputs[:,in_dim//2:]]
def compute_output_shape(self,input_shape):
# output_shape也要是对应的list
in_dim = input_shape[-1]
return [(None,in_dim//2),(None,in_dim-in_dim//2)]
x1,x2 = SplitVector()(x_in)
层中层
在Keras中自定义层的时候,重用已有的层,这将大大减少自定义层的代码量,自定义层的基本方法,其核心步骤是定义build
和call
两个函数,其中build
负责创建可训练的权重,而call则定义具体的运算。
经常用到自定义层的读者可能会感觉到,在自定义层的时候我们经常在重复劳动,比如我们想要增加一个线性变换,那就要在build
中增加一个kernel
和bias
变量(还要自定义变量的初始化、正则化等),然后在call里边用K.dot
来执行,有时候还需要考虑维度对齐的问题,步骤比较繁琐。但事实上,一个线性变换其实就是一个不加激活函数的Dense
层罢了,如果在自定义层时能重用已有的层,那显然就可以大大节省代码量了。
OurLayer
首先,我们定义一个新的OurLayer
类:
class OurLayer(Layer):
'''定义新的Layer,增加reuse方法,允许在定义Layer时调用现成的层
'''
def reuse(self,layer,*args,**kwargs):#星号*把序列/集合解包(unpack)成位置参数,两个星号**把字典解包成关键字参数。
if not layer.built:
if len(args)>0:
inputs = args[0]
else:
inputs = kwargs['inputs']
if isinstance(inputs,list): #isinstance() 函数来判断一个对象是否是一个已知的类型,类似 type()。
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
这个OurLayer类继承了原来的Layer类,为它增加了reuse方法,就是通过它我们可以重用已有的层。
下面是一个简单的例子,定义一个层,运算如下
y = g ( f ( x W 1 + b 1 ) W 2 + b 2 ) y = g(f(xW_1 + b_1)W_2 + b_2) y=g(f(xW1+b1)W2+b2)
这里f,g
是激活函数,其实就是两个Dense
层的复合,如果按照标准的写法,我们需要在build
那里定义好几个权重,定义权重的时候还需要根据输入来定义shape,还要定义初始化等,步骤很多,但事实上这些在Dense
层不都写好了吗,直接调用就可以了,参考调用代码如下:
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,)
自定义loss
Keras的模型是函数式的,即有输入,也有输出,而loss即为预测值与真实值的某种误差函数。Keras本身也自带了很多loss函数,如mse、交叉熵等,直接调用即可。而要自定义loss,最自然的方法就是仿照Keras自带的loss进行改写。
比如,我们做分类问题时,经常用的就是softmax输出,然后用交叉熵作为loss。然而这种做法也有不少缺点,其中之一就是分类太自信,哪怕输入噪音,分类的结果也几乎是非1即0,这通常会导致过拟合的风险,还会使得我们在实际应用中没法很好地确定置信区间、设置阈值。因此很多时候我们也会想办法使得分类别太自信,而修改loss也是手段之一。
如果不修改loss,我们就是使用交叉熵去拟合一个one hot的分布。交叉熵的公式是
S ( q ∣ p ) = − ∑ i q i log p i S(q|p)=-\sum_i q_i \log p_i S(q∣p)=−i∑qilogpi
其中 p i p_i pi是预测的分布,而 q i q_i qi是真实的分布,比如输出为 [ z 1 , z 2 , z 3 ] [z_1,z_2,z_3] [z1,z2,z