TensorFlow 篇 | TensorFlow 2.x 基於 Keras 的模型構建
Keras 介紹
-
Keras
是一個用Python
編寫的高階神經網路API
,它是一個獨立的庫,能夠以TensorFlow
,CNTK
或者Theano
作為後端執行。TensorFlow
從1.0
版本開始嘗試與Keras
做整合,到2.0
版釋出後更是深度集成了Keras
,並緊密依賴tf.keras
作為其中央高階API
,官方亦高度推薦使用keras API
來完成深度模型的構建。 -
tf.keras
具有三個關鍵優勢:-
對小白使用者友好:
Keras
具有簡單且一致的介面,並對使用者產生的錯誤有明確可行的建議去修正。TensorFlow 2.0
之前的版本,由於其程式碼編寫複雜,API
Keras
進行統一化之後,會大大減少開發人員的工作量。 -
模組化且可組合:
Keras
模型通過可構建的模組連線在一起,沒有任何限制,模型結構清晰,程式碼容易閱讀。 -
便於擴充套件:當編寫新的自定義模組時,可以非常方便的基於已有的介面進行擴充套件。
-
-
Keras
使得TensorFlow
更易於使用,而且不用損失其靈活性和效能。
Keras 模型構建
在 TensorFlow 2.x
版本中,可以使用三種方式來構建 Keras
模型,分別是 Sequential
, 函式式 (Functional) API
以及自定義模型 (Subclassed)
Sequential Model
-
在
Keras
中,通常是將多個層 (layer
) 組裝起來形成一個模型 (model
),最常見的一種方式就是層的堆疊,可以使用tf.keras.Sequential
來輕鬆實現。以上圖中所示模型為例,其程式碼實現如下:import tensorflow as tf from tensorflow.keras import layers model = tf.keras.Sequential() # Adds a densely-connected layer with 64 units to the model: model.add(layers.Dense(64, activation='relu', input_shape=(16,))) # This is identical to the following: # model.add(layers.Dense(64, activation='relu', input_dim=16)) # model.add(layers.Dense(64, activation='relu', batch_input_shape=(None, 16))) # Add another: model.add(layers.Dense(64, activation='relu')) # Add an output layer with 10 output units: model.add(layers.Dense(10)) # model.build((None, 16)) print(model.weights) 複製程式碼
-
注意對於
Sequential
新增的第一層,可以包含一個input_shape
或input_dim
或batch_input_shape
引數來指定輸入資料的維度,詳見註釋部分。當指定了input_shape
等引數後,每次add
新的層,模型都在持續不斷地建立過程中,也就說此時模型中各層的權重矩陣已經被初始化了,可以通過呼叫model.weights
來列印模型的權重資訊。 -
當然,第一層也可以不包含輸入資料的維度資訊,稱之為延遲建立模式,也就是說此時模型還未真正建立,權重矩陣也不存在。可以通過呼叫
model.build(batch_input_shape)
方法手動建立模型。如果未手動建立,那麼只有當呼叫fit
或者其他訓練和評估方法時,模型才會被建立,權重矩陣才會被初始化,此時模型會根據輸入的資料來自動推斷其維度資訊。 -
input_shape
中沒有指定batch
的大小而將其設定為None
,是因為在訓練與評估時所採用的batch
大小可能不一致。如果設為定值,在訓練或評估時會產生錯誤,而這樣設定後,可以由模型自動推斷batch
大小並進行計算,魯棒性更強。 -
除了這種順序性的新增 (
add
) 外,還可以通過將layers
以引數的形式傳遞給Sequential
來構建模型。示例程式碼如下所示:import tensorflow as tf from tensorflow.keras import layers model = tf.keras.Sequential([ layers.Dense(64, activation='relu', input_shape=(16, )), layers.Dense(64, activation='relu'), layers.Dense(10) ]) # model.build((None, 16)) print(model.weights) 複製程式碼
函式式 API
-
Keras
的函式式 API
是比Sequential
更為靈活的建立模型的方式。它可以處理具有非線性拓撲結構的模型、具有共享層 (layers
) 的模型以及多輸入輸出的模型。深度學習的模型通常是由層 (layers
) 組成的有向無環圖,而函式式 API
就是構建這種圖的一種有效方式。 -
以
Sequential Model
一節中提到的模型為例,使用函式式 API
實現的方式如下所示:from tensorflow import keras from tensorflow.keras import layers inputs = keras.Input(shape=(16, )) dense = layers.Dense(64, activation='relu') x = dense(inputs) x = layers.Dense(64, activation='relu')(x) outputs = layers.Dense(10)(x) model = keras.Model(inputs=inputs, outputs=outputs, name='model') model.summary() 複製程式碼
-
與使用
Sequential
方法構建模型的不同之處在於,函式式 API
通過keras.Input
指定了輸入inputs
並通過函式呼叫
的方式生成了輸出outputs
,最後使用keras.Model
方法構建了整個模型。 -
為什麼叫
函式式 API
,從程式碼中可以看到,可以像函式呼叫一樣來使用各種層 (layers
),比如定義好了dense
層,可以直接將inputs
作為dense
的輸入而得到一個輸出x
,然後又將x
作為下一層的輸入,最後的函式返回值就是整個模型的輸出。 -
函式式 API
可以將同一個層 (layers
) 作為多個模型的組成部分,示例程式碼如下所示:from tensorflow import keras from tensorflow.keras import layers encoder_input = keras.Input(shape=(16, ), name='encoder_input') x = layers.Dense(32, activation='relu')(encoder_input) x = layers.Dense(64, activation='relu')(x) encoder_output = layers.Dense(128, activation='relu')(x) encoder = keras.Model(encoder_input, encoder_output, name='encoder') encoder.summary() x = layers.Dense(64, activation='relu')(encoder_output) x = layers.Dense(32, activation='relu')(x) decoder_output = layers.Dense(16, activation='relu')(x) autoencoder = keras.Model(encoder_input, decoder_output, name='autoencoder') autoencoder.summary() 複製程式碼
程式碼中包含了兩個模型,一個編碼器 (
encoder
) 和一個自編碼器 (autoencoder
),可以看到兩個模型共用了encoder_out
層,當然也包括了encoder_out
層之前的所有層。 -
函式式 API
生成的所有模型 (models
) 都可以像層 (layers
) 一樣被呼叫。還以自編碼器 (autoencoder
) 為例,現在將它分成編碼器 (encoder
) 和解碼器 (decoder
) 兩部分,然後用encoder
和decoder
生成autoencoder
,程式碼如下:from tensorflow import keras from tensorflow.keras import layers encoder_input = keras.Input(shape=(16, ), name='encoder_input') x = layers.Dense(32, activation='relu')(encoder_input) x = layers.Dense(64, activation='relu')(x) encoder_output = layers.Dense(128, activation='relu')(x) encoder = keras.Model(encoder_input, encoder_output, name='encoder') encoder.summary() decoder_input = keras.Input(shape=(128, ), name='decoder_input') x = layers.Dense(64, activation='relu')(decoder_input) x = layers.Dense(32, activation='relu')(x) decoder_output = layers.Dense(16, activation='relu')(x) decoder = keras.Model(decoder_input, decoder_output, name='decoder') decoder.summary() autoencoder_input = keras.Input(shape=(16), name='autoencoder_input') encoded = encoder(autoencoder_input) autoencoder_output = decoder(encoded) autoencoder = keras.Model( autoencoder_input, autoencoder_output, name='autoencoder', ) autoencoder.summary() 複製程式碼
程式碼中首先生成了兩個模型
encoder
和decoder
,然後在生成autoencoder
模型時,使用了模型函式呼叫
的方式,直接將autoencoder_input
和encoded
分別作為encoder
和decoder
兩個模型的輸入,並最終得到autoencoder
模型。 -
函式式 API
可以很容易處理多輸入和多輸出的模型,這是Sequential API
無法實現的。比如我們的模型輸入有一部分是類別型特徵
,一般需要經過Embedding
處理,還有一部分是數值型特徵
,一般無需特殊處理,顯然無法將這兩種特徵直接合並作為單一輸入共同處理,此時就會用到多輸入。而有時我們希望模型返回多個輸出,以供後續的計算使用,此時就會用到多輸出模型。多輸入與多輸出模型的示例程式碼如下所示:from tensorflow import keras from tensorflow.keras import layers categorical_input = keras.Input(shape=(16, )) numeric_input = keras.Input(shape=(32, )) categorical_features = layers.Embedding( input_dim=100, output_dim=64, input_length=16, )(categorical_input) categorical_features = layers.Reshape([16 * 64])(categorical_features) numeric_features = layers.Dense(64, activation='relu')(numeric_input) x = layers.Concatenate(axis=-1)([categorical_features, numeric_features]) x = layers.Dense(128, activation='relu')(x) binary_pred = layers.Dense(1, activation='sigmoid')(x) categorical_pred = layers.Dense(3, activation='softmax')(x) model = keras.Model( inputs=[categorical_input, numeric_input], outputs=[binary_pred, categorical_pred], ) model.summary() 複製程式碼
程式碼中有兩個輸入
categorical_input
和numeric_input
,經過不同的處理層後,二者通過Concatenate
結合到一起,最後又經過不同的處理層得到了兩個輸出binary_pred
和categorical_pred
。該模型的結構圖如下圖所示: -
函式式 API
另一個好的用法是模型的層共享,也就是在一個模型中,層被多次重複使用,它從不同的輸入學習不同的特徵。一種常見的共享層是嵌入層 (Embedding
),程式碼如下:from tensorflow import keras from tensorflow.keras import layers categorical_input_one = keras.Input(shape=(16, )) categorical_input_two = keras.Input(shape=(24, )) shared_embedding = layers.Embedding(100, 64) categorical_features_one = shared_embedding(categorical_input_one) categorical_features_two = shared_embedding(categorical_input_two) categorical_features_one = layers.Reshape([16 * 64])(categorical_features_one) categorical_features_two = layers.Reshape([16 * 64])(categorical_features_two) x = layers.Concatenate(axis=-1)([ categorical_features_one, categorical_features_two, ]) x = layers.Dense(128, activation='relu')(x) outputs = layers.Dense(1, activation='sigmoid')(x) model = keras.Model( inputs=[categorical_input_one, categorical_input_two], outputs=outputs, ) model.summary() 複製程式碼
程式碼中有兩個輸入
categorical_input_one
和categorical_input_two
,它們共享了一個Embedding
層shared_embedding
。該模型的結構圖如下圖所示:
自定義 Keras 層和模型
-
tf.keras
模組下包含了許多內建的層 (layers
),比如上面我們用到的Dense
,Embedding
,Reshape
等。有時我們會發現這些內建的層並不能滿足我們的需求,此時可以很方便建立自定義的層來進行擴充套件。自定義的層通過繼承tf.keras.Layer
類來實現,且該子類要實現父類的build
和call
方法。對於內建的Dense
層,使用自定義層來實現的話,其程式碼如下所示:import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers class CustomDense(layers.Layer): def __init__(self, units=32): super().__init__() self.units = units def build(self, input_shape): self.w = self.add_weight( shape=(input_shape[-1], self.units), initializer='random_normal', trainable=True, ) self.b = self.add_weight( shape=(self.units, ), initializer='random_normal', trainable=True, ) def call(self, inputs): return tf.matmul(inputs, self.w) + self.b def get_config(self): return {'units': self.units} @classmethod def from_config(cls, config): return cls(**config) inputs = keras.Input((4, )) layer = CustomDense(10) outputs = layer(inputs) model = keras.Model(inputs, outputs) model.summary() # layer recreate config = layer.get_config() new_layer = CustomDense.from_config(config) new_outputs = new_layer(inputs) print(new_layer.weights) print(new_layer.non_trainable_weights) print(new_layer.trainable_weights) # model recreate config = model.get_config() new_model = keras.Model.from_config( config, custom_objects={'CustomDense': CustomDense}, ) new_model.summary() 複製程式碼
-
其中
__init__
方法用來初始化一些構建該層所需的基本引數,build
方法用來建立該層所需的權重矩陣w
和偏差矩陣b
,call
方法則是層構建的真正執行者,它將輸入轉為輸出並返回。其實權重矩陣等的建立也可以在__init__
方法中完成,但是在很多情況下,我們不能提前預知輸入資料的維度,需要在例項化層的某個時間點來延遲建立權重矩陣,因此需要在build
方法中根據輸入資料的維度資訊input_shape
來動態建立權重矩陣。 -
以上三個方法的呼叫順序為
__init__
,build
,call
,其中__init__
在例項化層時即被呼叫,而build
和call
是在確定了輸入後才被呼叫。其實Layer
類中有一個內建方法__call__
,在層構建時首先會呼叫該方法,而在方法內部會呼叫build
和call
,並且只有第一次呼叫__call__
時才會觸發build
,也就是說build
中的變數只能被建立一次,而call
是可以被呼叫多次的,比如訓練,評估時都會被呼叫。 -
如果需要對該層提供序列化的支援,則需要實現一個
get_config
方法來以字典的形式返回該層例項的建構函式引數。在給定config
的字典後,可以通過呼叫該層的類方法 (classmethod) from_config
來重新建立該層,from_config
的預設實現如程式碼所示,層的重新建立見layer recreate
程式碼部分,當然也可以重寫from_config
類方法來提供新的建立方式。而重新建立新模型 (model
) 的程式碼與layer
重建的程式碼有所不同,它需要藉助於keras.Model.from_config
方法來完成構建,詳見model recreate
程式碼部分。
-
-
自定義的層是可以遞迴組合的,也就是說一個層可以作為另一個層的屬性。一般推薦在
__init__
方法中建立子層,因為子層自己的build
方法會在外層build
呼叫時被觸發而去執行權重矩陣的構建任務,無需在父層中顯示建立。還以Sequential Model
一節提到的模型為例作為說明,程式碼如下:from tensorflow import keras from tensorflow.keras import layers class MLP(layers.Layer): def __init__(self): super().__init__() self.dense_1 = layers.Dense(64, activation='relu') self.dense_2 = layers.Dense(64, activation='relu') self.dense_3 = layers.Dense(10) def call(self, inputs): x = self.dense_1(inputs) x = self.dense_2(x) x = self.dense_3(x) return x inputs = keras.Input((16, )) mlp = MLP() y = mlp(inputs) print('weights:', len(mlp.weights)) print('trainable weights:', len(mlp.trainable_weights)) 複製程式碼
從程式碼中可以看到,我們將三個
Dense
層作為MLP
的子層,然後利用它們來完成MLP
的構建,可以達到與Sequential Model
中一樣的效果,而且所有子層的權重矩陣都會作為新層的權重矩陣而存在。 -
層 (
layers
) 在構建的過程中,會去遞迴地收集在此建立過程中生成的損失 (losses
)。在重寫call
方法時,可通過呼叫add_loss
方法來增加自定義的損失。層的所有損失中也包括其子層的損失,而且它們都可以通過layer.losses
屬性來進行獲取,該屬性是一個列表 (list
),需要注意的是正則項的損失會自動包含在內。示例程式碼如下所示:import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers class CustomLayer(layers.Layer): def __init__(self, rate=1e-2, l2_rate=1e-3): super().__init__() self.rate = rate self.l2_rate = l2_rate self.dense = layers.Dense( units=32, kernel_regularizer=keras.regularizers.l2(self.l2_rate), ) def call(self, inputs): self.add_loss(self.rate * tf.reduce_sum(inputs)) return self.dense(inputs) inputs = keras.Input((16, )) layer = CustomLayer() x = layer(inputs) print(layer.losses) 複製程式碼
-
層或模型的
call
方法預置有一個training
引數,它是一個bool
型別的變數,表示是否處於訓練狀態,它會根據呼叫的方法來設定值,訓練時為True
, 評估時為False
。因為有一些層像BatchNormalization
和Dropout
一般只會用在訓練過程中,而在評估和預測的過程中一般是不會使用的,所以可以通過該引數來控制模型在不同狀態下所執行的不同計算過程。 -
自定義模型與自定義層的實現方式比較相似,不過模型需要繼承自
tf.keras.Model
,Model
類的有些API
是與Layer
類相同的,比如自定義模型也要實現__init__
,build
和call
方法。不過兩者也有不同之處,首先Model
具有訓練,評估以及預測介面,其次它可以通過model.layers
檢視所有內建層的資訊,另外Model
類還提供了模型儲存和序列化的介面。以AutoEncoder
為例,一個完整的自定義模型的示例程式碼如下所示:from tensorflow import keras from tensorflow.keras import layers class Encoder(layers.Layer): def __init__(self, l2_rate=1e-3): super().__init__() self.l2_rate = l2_rate def build(self, input_shape): self.dense1 = layers.Dense( units=32, activation='relu', kernel_regularizer=keras.regularizers.l2(self.l2_rate), ) self.dense2 = layers.Dense( units=64, activation='relu', kernel_regularizer=keras.regularizers.l2(self.l2_rate), ) self.dense3 = layers.Dense( units=128, activation='relu', kernel_regularizer=keras.regularizers.l2(self.l2_rate), ) def call(self, inputs): x = self.dense1(inputs) x = self.dense2(x) x = self.dense3(x) return x class Decoder(layers.Layer): def __init__(self, l2_rate=1e-3): super().__init__() self.l2_rate = l2_rate def build(self, input_shape): self.dense1 = layers.Dense( units=64, activation='relu', kernel_regularizer=keras.regularizers.l2(self.l2_rate), ) self.dense2 = layers.Dense( units=32, activation='relu', kernel_regularizer=keras.regularizers.l2(self.l2_rate), ) self.dense3 = layers.Dense( units=16, activation='relu', kernel_regularizer=keras.regularizers.l2(self.l2_rate), ) def call(self, inputs): x = self.dense1(inputs) x = self.dense2(x) x = self.dense3(x) return x class AutoEncoder(keras.Model): def __init__(self): super().__init__() self.encoder = Encoder() self.decoder = Decoder() def call(self, inputs): x = self.encoder(inputs) x = self.decoder(x) return x model = AutoEncoder() model.build((None, 16)) model.summary() print(model.layers) print(model.weights) 複製程式碼
上述程式碼實現了一個
AutoEncoder Model
類,它由兩層組成,分別為Encoder
和Decoder
,而這兩層也是自定義的。通過呼叫model.weights
可以檢視該模型所有的權重資訊,當然這裡包含子層中的所有權重資訊。 -
對於自定義的層或模型,在呼叫其
summary
,weights
,variables
,trainable_weights
,losses
等方法或屬性時,要先確保層或模型已經被建立,不然可能報錯或返回為空,在模型除錯時要注意這一點。
配置層 (layer)
在 tf.keras.layers
模組下面有很多預定義的層,這些層大多都具有相同的建構函式引數。下面介紹一些常用的引數,對於每個層的獨特引數以及引數的含義,可以在使用時查詢官方文件即可,文件的解釋一般會很詳細。
-
activation
指啟用函式,可以設定為字串如relu
或activations
物件tf.keras.activations.relu()
,預設情況下為None
,即表示線性關係。 -
kernel_initializer
和bias_initializer
,表示層中權重矩陣和偏差矩陣的初始化方式,可以設定為字串如Glorotuniform
或者initializers
物件tf.keras.initializers.GlorotUniform()
,預設情況下即為Glorotuniform
初始化方式。 -
kernel_regularizer
和bias_regularizer
,表示權重矩陣和偏差矩陣的正則化方式,上面介紹過,可以是L1
或L2
正則化,如tf.keras.regularizers.l2(1e-3)
,預設情況下是沒有正則項的。
模型建立方式對比
-
當構建比較簡單的模型,使用
Sequential
方式當然是最方便快捷的,可以利用現有的Layer
完成快速構建、驗證的過程。 -
如果模型比較複雜,則最好使用
函式式 API
或自定義模型。通常函式式 API
是更高階、更容易以及更安全的實現方式,它還具有一些自定義模型所不具備的特性。但是,當構建不容易表示為有向無環圖的模型時,自定義模型提供了更大的靈活性。 -
函式式 API
可以提前做模型校驗,因為它通過Input
方法提前指定了模型的輸入維度,所以當輸入不合規範會更早的發現,有助於我們除錯,而自定義模型開始是沒有指定輸入資料的維度的,它是在執行過程中根據輸入資料來自行推斷的。 -
使用
函式式 API
編寫程式碼模組化不強,閱讀起來有些吃力,而通過自定義模型,可以非常清楚的瞭解該模型的整體結構,易於理解。 -
在實際使用中,可以將
函式式 API
和自定義模型結合使用,來滿足我們各式各樣的模型構建需求。
Keras 模型建立技巧
-
在編寫模型程式碼時,可以多參考借鑑別人的模型構建方式,有時會有不小的收穫。
-
在查詢所需的
tensorflow
方法時,如果keras
模組下有提供實現則優先使用該方法,如果沒有則找tf
模組下的方法即可,這樣可使得程式碼的相容性以及魯棒性更強。 -
在模型建立過程中,多使用模型和層的內建方法和屬性,如
summary
,weights
等,這樣可以從全域性角度來審視模型的結構,有助於發現一些潛在的問題。 -
因為
TensorFlow 2.x
模型預設使用Eager Execution
動態圖機制來執行程式碼,所以可以在程式碼的任意位置直接列印Tensor
來檢視其數值以及維度等資訊,在模型除錯時十分有幫助。
作者:AlexanderJLiu
連結:https://juejin.im/post/6883776276813840398
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。