Categories
程式開發

TensorFlow 篇| TensorFlow 2.x 基於Keras 的模型構建


TensorFlow 篇| TensorFlow 2.x 基於Keras 的模型構建 1

「導語」TensorFlow 2.0 版發布以來,Keras 已經被深度集成於TensorFlow 框架中,Keras API 也成為了構建深度網絡模型的第一選擇。使用Keras 進行模型開發與迭代是每一個數據開發人員都需要掌握的一項基本技能,讓我們一起走進Keras 的世界一探究竟。

Keras 介紹

Keras 是一個用Python 編寫的高級神經網絡API ,它是一個獨立的庫,能夠以TensorFlow , CNTK 或者Theano 作為後端運行。 TensorFlow 從1.0 版本開始嘗試與Keras 做集成,到2.0 版發布後更是深度集成了Keras ,並緊密依賴tf.keras 作為其中央高級API ,官方亦高度推薦使用keras API 來完成深度模型的構建。

tf.keras 具有三個關鍵優勢:

2.1. 對小白用戶友好: Keras 具有簡單且一致的接口,並對用戶產生的錯誤有明確可行的建議去修正。 TensorFlow 2.0 之前的版本,由於其代碼編寫複雜, API 接口混亂而且各個版本之間兼容性較差,受到廣泛的批評,使用Keras 進行統一化之後,會大大減少開發人員的工作量。

2.2. 模塊化且可組合: Keras 模型通過可構建的模塊連接在一起,沒有任何限制,模型結構清晰,代碼容易閱讀。

2.3. 便於擴展:當編寫新的自定義模塊時,可以非常方便的基於已有的接口進行擴展。

Keras 使得TensorFlow 更易於使用,而且不用損失其靈活性和性能。

Keras 模型構建

在TensorFlow 2.x 版本中,可以使用三種方式來構建Keras 模型,分別是Sequential , 函數式(Functional) API 以及自定義模型(Subclassed)。下面就分別介紹下這三種構建方式。

順序模型

TensorFlow 篇| TensorFlow 2.x 基於Keras 的模型構建 2

在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 或batchinputshape 參數來指定輸入數據的維度,詳見註釋部分。當指定了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 。該模型的結構圖如下圖所示:

TensorFlow 篇| TensorFlow 2.x 基於Keras 的模型構建 3

函數式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 。該模型的結構圖如下圖所示:

TensorFlow 篇| TensorFlow 2.x 基於Keras 的模型構建 4

自定義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()

1.1. 其中__init__ 方法用來初始化一些構建該層所需的基本參數, build 方法用來創建該層所需的權重矩陣w 和偏差矩陣b , call 方法則是層構建的真正執行者,它將輸入轉為輸出並返回。其實權重矩陣等的創建也可以在init 方法中完成,但是在很多情況下,我們不能提前預知輸入數據的維度,需要在實例化層的某個時間點來延遲創建權重矩陣,因此需要在build 方法中根據輸入數據的維度信息input_shape 來動態創建權重矩陣。

1.2. 以上三個方法的調用順序為__init__ , build , call ,其中__init__ 在實例化層時即被調用,而build 和call 是在確定了輸入後才被調用。其實Layer 類中有一個內置方法__call__ ,在層構建時首先會調用該方法,而在方法內部會調用build 和call ,並且只有第一次調用call 時才會觸發build ,也就是說build 中的變量只能被創建一次,而call 是可以被調用多次的,比如訓練,評估時都會被調用。

1.3. 如果需要對該層提供序列化的支持,則需要實現一個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 來查看其數值以及維度等信息,在模型調試時十分有幫助。

參考資料

Keras Sequential 模型Keras 函數式APIKeras 編寫自定義層和模型