keras之自定义层和部分loss

层的自定义

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中自定义层的时候,重用已有的层,这将大大减少自定义层的代码量,自定义层的基本方法,其核心步骤是定义buildcall两个函数,其中build负责创建可训练的权重,而call则定义具体的运算。
经常用到自定义层的读者可能会感觉到,在自定义层的时候我们经常在重复劳动,比如我们想要增加一个线性变换,那就要在build中增加一个kernelbias变量(还要自定义变量的初始化、正则化等),然后在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(qp)=iqilogpi
其中 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值