Keras是基於Tensorflow(以前還可以基於別的底層張量庫,現在已並入TF)的高層API庫。它幫我們實現了一系列經典的神經網絡層(全連接層、卷積層、循環層等),以及簡潔的迭代模型的接口,讓我們能在模型層面寫代碼,從而不用仔細考慮模型各層張量之間的數據流動。
但是,當我們有了全新的想法,想要個性化模型層的實現時,僅靠Keras的高層API是不能滿足這一要求的。下面記錄使用TF與Keras快速搭建神經網絡模型。
自定義網絡層
實現往Keras的Model類中添加自定義層,有兩種方式:
1、定義Lambda層。
2、繼承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
更新:這樣做在Keras最新版本已經不支持了,只支持Lambda層了。
以上實現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就是各個張量形狀的列表。其它都與單一輸入類似。
自定義損失函數
下面介紹使用keas的fit函數訓練模型時,我們可以使用的自定義模型損失的方式。
根據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)
對於多輸出模型,loss可以定義多個,然后以列表或字典的形式傳入。傳列表形式如下:
def customed_loss1(true_label,predict_label): loss = ... return loss def customed_loss2(true_label,predict_label): loss = ... return loss model.compile(optimizer='rmsprop', loss=[customed_loss1,customed_loss2])
其中loss列表中loss的數量和順序要與模型輸出的數量和順序一致,不能少傳或多傳。傳字典的形式更明確一些,但是要給每個輸出定義name屬性,具體方法請看:
針對keras模型多輸出或多損失方法使用_樹莓派-CSDN博客
如果僅僅傳入一個loss,不被列表或字典所包含,keras會讓所有輸出都使用同一個loss函數。
另外,很重要的一點是,不論自定義loss函數傳出的是什么形狀的張量(keras fit時傳入loss的是帶有批量維度的張量),經過測試,我發現,keras會對這個張量的所有元素求一個均值以獲得這個loss的最終輸出標量。
第二類損失
這類損失可以用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 正則化
里面介紹了已實現層的自定義正則化,但沒有介紹自定義層的自定義正則化,這里先挖個坑,以后要用再研究。
自定義模型
以上介紹的都是基於Keras已實現的方法來定義模型的結構與損失,這種方式要在完全建立好模型之后才能獲取模型的輸出信息,模型結構一復雜就很不容易查找bug,並且一些復雜的loss並不好定義(比如WGAN-GP的GP)。而Tensorflow2.0更新后默認為eager模式,因此不用建立完整的數據流圖就能計算模型的中間結果,很方便。下面是通過重寫Keras的Model類來實現自定義模型的代碼:
import numpy as np import tensorflow as tf from tensorflow.keras import layers,Model,optimizers '''模型的定義與實例化''' class MyModel(Model): def __init__(self): super().__init__() #必須執行以初始化父類 self.dense1 = layers.Dense(3) self.optimizer = optimizers.SGD(10000) self.build(input_shape=(None,3))#執行build函數來確定輸入形狀 def call(self,inputs): return self.dense1(inputs) def loss(self,output): return tf.reduce_mean(output) def train(self,inputs): with tf.GradientTape() as tape: output = self.call(inputs) loss = tf.reduce_mean(output) print("loss: "+str(loss.numpy())) grads = tape.gradient(loss,self.trainable_weights) self.optimizer.apply_gradients(zip(grads,self.trainable_weights)) myModel = MyModel() myModel(np.random.random(size = [100,3])) #如果不執行build,Keras會以第一次傳入的輸入形狀為基准來建立模型權重 '''模型訓練''' for i in range(100): myModel.train(np.ones([10,3])) '''權重保存''' print("\n第一個模型保存的權重:") print(myModel.weights[1].numpy()) myModel.save_weights("myModel.h5")#保存權重 '''權重讀取''' myModel2 = MyModel() myModel2(np.random.random(size = [100,3])) print("\n第二個模型初始化的權重:") print(myModel2.weights[1].numpy()) myModel2.load_weights("myModel.h5")#讀取權重 print("\n第二個模型讀取后的權重:") print(myModel2.weights[1].numpy())
簡單來說就是繼承Model類,然后實現其中的__init__()方法和call()方法。其中要注意優化器的輸入,要用zip函數將梯度和權重一一配對,即使梯度和權重只有一個,也要先放到列表中,然后用zip進行配對。
__init__():用於定義和保存模型將要應用的層與權重,當然也能定義一些個性化的變量。
call():用於推理與訓練時調用,計算輸出。
正如代碼中注釋,實例化模型后要使用父類的build()來確定模型的輸入形狀,以確定整個模型的所有權重,一般可以直接在__init__()中調用。要注意的是,build()要傳入元組,也就是用小括號,用中括號會報錯(血的教訓!)。當然不調用build()也可以,模型會以第一次傳入的輸入為基准來建立模型權重。
因為不是直接使用Keras內置的接口定義的模型(比如model = Model(inputs,outputs)),所以不能再使用save()來保存模型。這是因為我們把數據流動直接定義在了call()中,Keras無法得知模型的數據流動走向(它不能直接從call()中獲取,我覺得有待升級)。但因為你把層存在了self中,因此可以使用save_weights()來保存層的權重(不保存計算拓撲),然后再用load_weights()讀取權重,當然只有結構相同的模型才能讀取。
另外,你還可以在模型中自定義loss()和train(),計算損失和更新權重。計算梯度需要將數據的流動放在tf.GradientTape()中,Tensorflow才能知道需要計算梯度的權重有哪些。上面的代碼只極其簡單地介紹了用法,直接對模型輸出的均值進行了梯度下降。
代碼輸出如下:
用自定義層
與上面給Keras內置接口調用的繼承Layer的自定義層不太一樣(我感覺是bug),這里雖然同樣也是繼承自Layer,但定義權重不能用tf.Variable,不然會出現保存的權重無法讀取的情況。這里使用add_weights(),代碼如下:
import numpy as np import tensorflow as tf from tensorflow.keras import layers,Model,optimizers class MyDense(layers.Layer): def __init__(self): super().__init__() self.w = self.add_weight(name = 'w', shape = [3, 4]) self.b = self.add_weight(name = 'b', shape = [4]) def call(self, inputs): return tf.matmul(inputs[:,:], self.w) + self.b '''模型的定義與實例化''' class MyModel(Model): def __init__(self): super().__init__() #必須執行以初始化父類 self.myDense = MyDense() self.optimizer = optimizers.SGD(10000) self.build(input_shape=(None,3))#執行build函數來確定輸入形狀 def call(self,inputs): return self.myDense(inputs) def loss(self,output): return tf.reduce_mean(output) def train(self,inputs): with tf.GradientTape() as tape: output = self.call(inputs) loss = tf.reduce_mean(output) print("loss: "+str(loss.numpy())) grads = tape.gradient(loss,self.trainable_weights) self.optimizer.apply_gradients(zip(grads,self.trainable_weights)) myModel = MyModel() myModel(np.random.random(size = [100,3])) #如果不執行build,Keras會以第一次傳入的輸入形狀為基准來建立模型權重 '''模型訓練''' for i in range(100): myModel.train(np.ones([10,3])) '''權重保存''' print("\n第一個模型保存的權重:") print(myModel.weights[1].numpy()) myModel.save_weights("myModel.h5")#保存權重 '''權重讀取''' myModel2 = MyModel() myModel2(np.random.random(size = [100,3])) print("\n第二個模型初始化的權重:") print(myModel2.weights[1].numpy()) myModel2.load_weights("myModel.h5")#讀取權重 print("\n第二個模型讀取后的權重:") print(myModel2.weights[1].numpy())
輸出如下:
WGAN-GP的Gradient penalty損失定義請參考鏈接:WGAN-GP loss定義