Keras結合Keras後端搭建個性化神經網路模型(不用原生Tensorflow)

頎周發表於2020-08-20

  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 正則化

  裡面介紹了已實現層的自定義正則化,但沒有介紹自定義層的自定義正則化,這裡先挖個坑,以後要用再研究。

相關文章