1. 程式人生 > 實用技巧 >TensorFlow 篇 | TensorFlow 2.x 基於 Keras 的模型構建

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

Keras 介紹

  1. Keras 是一個用 Python 編寫的高階神經網路 API ,它是一個獨立的庫,能夠以 TensorFlowCNTK 或者 Theano 作為後端執行。 TensorFlow1.0 版本開始嘗試與 Keras 做整合,到 2.0 版釋出後更是深度集成了 Keras ,並緊密依賴 tf.keras 作為其中央高階 API ,官方亦高度推薦使用 keras API 來完成深度模型的構建。

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

    1. 對小白使用者友好: Keras 具有簡單且一致的介面,並對使用者產生的錯誤有明確可行的建議去修正。 TensorFlow 2.0 之前的版本,由於其程式碼編寫複雜, API

      介面混亂而且各個版本之間相容性較差,受到廣泛的批評,使用 Keras 進行統一化之後,會大大減少開發人員的工作量。

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

    3. 便於擴充套件:當編寫新的自定義模組時,可以非常方便的基於已有的介面進行擴充套件。

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

Keras 模型構建

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

。下面就分別介紹下這三種構建方式。

Sequential Model

  1. 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)
    複製程式碼
  2. 注意對於 Sequential 新增的第一層,可以包含一個 input_shapeinput_dimbatch_input_shape 引數來指定輸入資料的維度,詳見註釋部分。當指定了 input_shape 等引數後,每次 add 新的層,模型都在持續不斷地建立過程中,也就說此時模型中各層的權重矩陣已經被初始化了,可以通過呼叫 model.weights 來列印模型的權重資訊。

  3. 當然,第一層也可以不包含輸入資料的維度資訊,稱之為延遲建立模式,也就是說此時模型還未真正建立,權重矩陣也不存在。可以通過呼叫 model.build(batch_input_shape) 方法手動建立模型。如果未手動建立,那麼只有當呼叫 fit 或者其他訓練和評估方法時,模型才會被建立,權重矩陣才會被初始化,此時模型會根據輸入的資料來自動推斷其維度資訊。

  4. input_shape 中沒有指定 batch 的大小而將其設定為 None ,是因為在訓練與評估時所採用的 batch 大小可能不一致。如果設為定值,在訓練或評估時會產生錯誤,而這樣設定後,可以由模型自動推斷 batch 大小並進行計算,魯棒性更強。

  5. 除了這種順序性的新增 (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

  1. Keras函式式 API 是比 Sequential 更為靈活的建立模型的方式。它可以處理具有非線性拓撲結構的模型、具有共享層 (layers) 的模型以及多輸入輸出的模型。深度學習的模型通常是由層 (layers) 組成的有向無環圖,而函式式 API 就是構建這種圖的一種有效方式。

  2. 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()
    複製程式碼
  3. 與使用 Sequential 方法構建模型的不同之處在於,函式式 API 通過 keras.Input 指定了輸入 inputs 並通過函式呼叫的方式生成了輸出 outputs ,最後使用 keras.Model 方法構建了整個模型。

  4. 為什麼叫函式式 API ,從程式碼中可以看到,可以像函式呼叫一樣來使用各種層 (layers),比如定義好了 dense 層,可以直接將 inputs 作為 dense 的輸入而得到一個輸出 x ,然後又將 x 作為下一層的輸入,最後的函式返回值就是整個模型的輸出。

  5. 函式式 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 層之前的所有層。

  6. 函式式 API 生成的所有模型 (models) 都可以像層 (layers) 一樣被呼叫。還以自編碼器 (autoencoder) 為例,現在將它分成編碼器 (encoder) 和解碼器 (decoder) 兩部分,然後用 encoderdecoder 生成 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()
    複製程式碼

    程式碼中首先生成了兩個模型 encoderdecoder ,然後在生成 autoencoder 模型時,使用了模型函式呼叫的方式,直接將 autoencoder_inputencoded 分別作為 encoderdecoder 兩個模型的輸入,並最終得到 autoencoder 模型。

  7. 函式式 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_inputnumeric_input ,經過不同的處理層後,二者通過 Concatenate 結合到一起,最後又經過不同的處理層得到了兩個輸出 binary_predcategorical_pred 。該模型的結構圖如下圖所示:

  8. 函式式 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_onecategorical_input_two ,它們共享了一個 Embeddingshared_embedding 。該模型的結構圖如下圖所示:

自定義 Keras 層和模型

  1. tf.keras 模組下包含了許多內建的層 (layers),比如上面我們用到的 DenseEmbeddingReshape 等。有時我們會發現這些內建的層並不能滿足我們的需求,此時可以很方便建立自定義的層來進行擴充套件。自定義的層通過繼承 tf.keras.Layer 類來實現,且該子類要實現父類的 buildcall 方法。對於內建的 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. 其中 __init__ 方法用來初始化一些構建該層所需的基本引數, build 方法用來建立該層所需的權重矩陣 w 和偏差矩陣 bcall 方法則是層構建的真正執行者,它將輸入轉為輸出並返回。其實權重矩陣等的建立也可以在 __init__ 方法中完成,但是在很多情況下,我們不能提前預知輸入資料的維度,需要在例項化層的某個時間點來延遲建立權重矩陣,因此需要在 build 方法中根據輸入資料的維度資訊 input_shape 來動態建立權重矩陣。

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

    3. 如果需要對該層提供序列化的支援,則需要實現一個 get_config 方法來以字典的形式返回該層例項的建構函式引數。在給定 config 的字典後,可以通過呼叫該層的類方法 (classmethod) from_config 來重新建立該層, from_config 的預設實現如程式碼所示,層的重新建立見 layer recreate 程式碼部分,當然也可以重寫 from_config 類方法來提供新的建立方式。而重新建立新模型 (model) 的程式碼與 layer 重建的程式碼有所不同,它需要藉助於 keras.Model.from_config 方法來完成構建,詳見 model recreate 程式碼部分。

  2. 自定義的層是可以遞迴組合的,也就是說一個層可以作為另一個層的屬性。一般推薦在 __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 中一樣的效果,而且所有子層的權重矩陣都會作為新層的權重矩陣而存在。

  3. 層 (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)
    複製程式碼
  4. 層或模型的 call 方法預置有一個 training 引數,它是一個 bool 型別的變數,表示是否處於訓練狀態,它會根據呼叫的方法來設定值,訓練時為 True , 評估時為 False 。因為有一些層像 BatchNormalizationDropout 一般只會用在訓練過程中,而在評估和預測的過程中一般是不會使用的,所以可以通過該引數來控制模型在不同狀態下所執行的不同計算過程。

  5. 自定義模型與自定義層的實現方式比較相似,不過模型需要繼承自 tf.keras.ModelModel 類的有些 API 是與 Layer 類相同的,比如自定義模型也要實現 __init__buildcall 方法。不過兩者也有不同之處,首先 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 類,它由兩層組成,分別為 EncoderDecoder ,而這兩層也是自定義的。通過呼叫 model.weights 可以檢視該模型所有的權重資訊,當然這裡包含子層中的所有權重資訊。

  6. 對於自定義的層或模型,在呼叫其 summary, weights, variables, trainable_weightslosses 等方法或屬性時,要先確保層或模型已經被建立,不然可能報錯或返回為空,在模型除錯時要注意這一點。

配置層 (layer)

tf.keras.layers 模組下面有很多預定義的層,這些層大多都具有相同的建構函式引數。下面介紹一些常用的引數,對於每個層的獨特引數以及引數的含義,可以在使用時查詢官方文件即可,文件的解釋一般會很詳細。

  1. activation 指啟用函式,可以設定為字串如 reluactivations 物件 tf.keras.activations.relu() ,預設情況下為 None ,即表示線性關係。

  2. kernel_initializerbias_initializer ,表示層中權重矩陣和偏差矩陣的初始化方式,可以設定為字串如 Glorotuniform 或者 initializers 物件 tf.keras.initializers.GlorotUniform() ,預設情況下即為 Glorotuniform 初始化方式。

  3. kernel_regularizerbias_regularizer ,表示權重矩陣和偏差矩陣的正則化方式,上面介紹過,可以是 L1L2 正則化,如 tf.keras.regularizers.l2(1e-3) ,預設情況下是沒有正則項的。

模型建立方式對比

  1. 當構建比較簡單的模型,使用 Sequential 方式當然是最方便快捷的,可以利用現有的 Layer 完成快速構建、驗證的過程。

  2. 如果模型比較複雜,則最好使用函式式 API 或自定義模型。通常函式式 API 是更高階、更容易以及更安全的實現方式,它還具有一些自定義模型所不具備的特性。但是,當構建不容易表示為有向無環圖的模型時,自定義模型提供了更大的靈活性。

  3. 函式式 API 可以提前做模型校驗,因為它通過 Input 方法提前指定了模型的輸入維度,所以當輸入不合規範會更早的發現,有助於我們除錯,而自定義模型開始是沒有指定輸入資料的維度的,它是在執行過程中根據輸入資料來自行推斷的。

  4. 使用函式式 API 編寫程式碼模組化不強,閱讀起來有些吃力,而通過自定義模型,可以非常清楚的瞭解該模型的整體結構,易於理解。

  5. 在實際使用中,可以將函式式 API 和自定義模型結合使用,來滿足我們各式各樣的模型構建需求。

Keras 模型建立技巧

  1. 在編寫模型程式碼時,可以多參考借鑑別人的模型構建方式,有時會有不小的收穫。

  2. 在查詢所需的 tensorflow 方法時,如果 keras 模組下有提供實現則優先使用該方法,如果沒有則找 tf 模組下的方法即可,這樣可使得程式碼的相容性以及魯棒性更強。

  3. 在模型建立過程中,多使用模型和層的內建方法和屬性,如 summaryweights 等,這樣可以從全域性角度來審視模型的結構,有助於發現一些潛在的問題。

  4. 因為 TensorFlow 2.x 模型預設使用 Eager Execution 動態圖機制來執行程式碼,所以可以在程式碼的任意位置直接列印 Tensor 來檢視其數值以及維度等資訊,在模型除錯時十分有幫助。


作者:AlexanderJLiu
連結:https://juejin.im/post/6883776276813840398
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。