Try   HackMD
tags: tensorflow

0. Tensorflow 2.0 不負責任教學

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

想到的時候就做點筆記,不然過三天之後就忘光了(x
Carrot蘿蔔Sun, May 31, 2020 11:33 AM


前言

沒想到再回到tensorflow懷抱的這天
版本已經更新到了2.1.0
蘿蔔頭頂上都新開了幾場初雪
TF更早已不是當初那青澀的少年了

遙想起sess.run()的那個年代
其實說不定還是帶著幾分懷念的呢 ~

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

好吧並沒有,我再也不想用session來開圖了
那根本就不是應該出現在python style的語言形式

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

隨著版本更迭來到了2.0.0
昔日神經網路API的另外一大巨頭Keras
現在被收錄到了TF底下
對於我這一路走來都信奉著TF神教的狂熱者
真的是一大福音呢 ~
儘管我認為總有一天也要把pytorch學起來就是了
大家做的事情都差不多
不要細分那麼多彼此嘛 ~

以下記錄了一些蘿蔔使用TF2開荒以來
遇到的各式各樣神奇的問題
又或是一些值得記錄下來的code
希望這篇文章的內容可以為需要TF2教學的你
或是過了幾個月之後忘東忘西的蘿蔔
帶來相當實質的幫助 ~~

快速模型寫法

  • Overview
import tensorflow as tf
import numpy as np
from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, Flatten, Dense

def customize_model(x_train):
    # 以tensorflow的shape順序而言,第一維是batch,不需要指定給Input
    inputs = Input(shape= x_train.shape[1:])
    conv1 = Conv2D(16, (3, 3), activation= "relu", padding= "same")(inputs)
    conv2 = Conv2D(16, (3, 3), activation= "relu", padding= "same")(conv1)
    pool = MaxPooling2D((2, 2), padding= "same")(conv2)
    flatten = Flatten()(pool)
    output = Dense(10, activation= "softmax")(flatten)
    
    model = tf.keras.Model(inputs, output)
    model.compile(optimizer= "Adam",
                  loss= "categorical_crossentropy",
                  metrics= ["accuracy"])
    return model
    
my_model = customize_model(x_train, y_train, x_valid, y_valid)
history = my_model.fit(x_train, y_train,
                       validation_data= (x_valid, y_valid),
                       batch_size= 64,
                       epochs= 50,
                       shuffle= True,
                       callback= [])

在快速模型寫法當中
我們可以先直觀的定義一個ForwardPropagation的function
就像上面的customize_model一樣
讓假想的訓練資料一層一層的傳遞下去
直到獲得output之後
再使用tf.keras.Model()指定輸入tensor與輸出tensor
便完成了model物件初步的建立

下一步呢則是為model指定optimizer以及loss function
使用model.compile()
並且傳入參數optimizer= "Adam"以及loss= "categorical_crossentropy"
目前這裡是假設要做一個圖片類別的分類器啦
參數的選擇會因為任務型態的不同而有所差別

在model物件回傳回來之後
便接著使用model.fit()開始訓練

通用模型寫法

一般而言
在通用模型中,我們會需要實作以下幾個區塊
他們分別為

  • Models and Layers

    • 這應該沒什麼好說的,沒有模型的話難道要練元氣彈嗎XD
  • Hyper parameters

    • 我們在這一區定義出所有的超參數、路徑、損失函數、優化器等等
      並同時把模型建立出來
  • train_step and valid_step

    • 這兩個method會被定義成@tf.function
      主要負責計算模型的前後傳播和梯度下降
  • checkpoint_manager

    • 訓練好的model和op當然要好好保存起來呀~
      ckpt manager可以幫你辦到這件事情
  • train_model

    • 我們在這裡定義訓練的主要流程 (翻譯:各式各樣複雜的處理XD)

這些區塊在快速寫法中
都被keras高級的API包的妥妥貼貼
造成這些寫法雖然很方便
但真要實作某些內容的時候卻又綁手綁腳
於是,我們終究還是得回來看看這些底層的寫法
並把所有區塊都寫成自己想要的樣子吧!

底層??
Am I a joke to you???
TensorFlow

Models and Layers

  • Customized Layer Class
class LAYER_NAME(tf.keras.layers.Layer):
    def __init__(self):
        super(LAYER_NAME, self).__init__()
        
        # 一些你需要的sublayers
        self.dense = Dense(10, activation= "softmax")
    
    def SOME_FUNCTIONS(self):
        # 一些你會用到的member funciton
        pass
        
    def call(x):
        # model正向傳播的流程
        output = self.dense(x)
        return output

這裡最重要的是class繼承自tf.keras.layers.Layer
而且一定要把需要的layer定義成member

  • Customized Model Class
class MODEL_NAME(tf.keras.Model):
    def __init__(self):
        super(MODEL_NAME, self).__init__()
        
        # 一些你需要的layers
        self.layer = LAYER_NAME()
    
    def SOME_FUNCTIONS(self):
        # 一些你會用到的member funciton
        pass

    def call(x):
        # model正向傳播的流程
        output = self.layer(x)
        return output

Model class則是繼承自tf.keras.Model

Hyper parameters

# 一些你的模型中會用到的超參數
CHANNEL = 16
BATCH_SIZE = 128
EPOCH = 200

# 用超參數來為子資料夾命名,不然你根本就不知道哪個是哪個XD
RUN_SETTING = "{}channel_{}batchsize_{}epoch".format(CHANNEL, BATCH_SIZE, EPOCH)
CKPT_PATH = os.path.join("ckpt", RUN_SETTING)
LOG_PATH = os.path.join("logs", RUN_SETTING)

MODEL = MODEL_NAME()
OPTIMIZER = tf.keras.optimizers.Adam()
LOSS_OBJECT = tf.keras.losses.CategoricalCrossentropy()

# 這是等等用來寫tensorboard的兩個物件
metrics_loss = tf.keras.metrics.CategoricalCrossentropy()
metrics_acc = tf.keras.metrics.CategoricalAccuracy()

train_step and valid_step

  • train_step function
# 單個tape的寫法

@tf.function
def train_step(x, y):
    with tf.GradientTape() as YOUR_TAPE:
        y_pred = MODEL(x)
        loss = LOSS_OBJECT(y, y_pred)
        
    gradients = YOUR_TAPE.gradient(loss, MODEL.trainable_variables)
    OPTIMIZER.apply_gradients(zip(gradients, MODEL.trainable_variables))
    
    # 如果你要把數據記錄到tensorboard上面的話
    # 就在這裡把loss和acc存進metrics
    metrics_loss(loss)
    metrics_acc(y, y_pred)
# 多個tape的寫法,例如GAN中的generator和discriminator

@tf.function
def train_step(noise, img_true):
    with tf.GradientTape() as g_tape, tf.GradientTape() as d_tape:
        img_fake = Generator(noise)
        
        score_fake = Discriminator(img_fake)
        score_true = Discriminator(img_true)
        
        g_loss = loss_object(tf.ones_like(score_fake), score_fake)
        d_loss = loss_object(tf.zeros_like(score_fake), score_fake)
                +loss_object(tf.ones_like(score_true), score_true)
    
    g_grad = g_tape.gradient(g_loss, Generator.trainable_variables)
    d_grad = d_tape.gradient(d_loss, Discriminator.trainable_variables)
    
    g_optimizer.apply_gradients(zip(g_grad, Generator.trainable_variables))
    d_optimizer.apply_gradients(zip(d_grad, Discriminator.trainable_variables))
    
    # 如果你要把數據記錄到tensorboard上面的話
    # 就在這裡把loss和acc存進metrics
    metrics_loss(g_loss)
    metrics_acc(score_fake, tf.ones_like(score_fake))

首先train_step必須被定義為@tf.function
接著資料流將會經過以下數個階段

  1. 通過模型計算Forward Propagation
  2. 用損失函數計算loss
  3. 取出模型中可訓練參數的梯度
  4. 讓optimizer對參數進行梯度下降
  5. 記錄參數以便在tensorboard上觀察數據變化 (optional)

值得一提的是,如果你的網路中存在著需要迭代訓練的多個模型
比如說GAN中的generator和discriminator
那就可以使用上方第二個多個tape的寫法
各自定義出各自的tape,獨立地進行梯度下降
這樣一來
就算網路架構再複雜都不用怕啦 ~

checkpoint_manager

# 定義這個ckpt要存下那些物件,通常會把model本身以及optimizer存起來
ckpt = tf.train.Checkpoint(model= YOUR_MODEL,
                           optimizer= YOUR_OPTIMIZER)

ckpt_manager = tf.train.CheckpointManager(ckpt, CKPT_PATH, max_to_keep= 5)

if ckpt_manager.latest_checkpoint:
    ckpt.restore(ckpt_manager.latest_checkpoint)
  
    # 確認最新的資料是已經練到第幾個epoch的權重
    last_epoch = int(ckpt_manager.latest_checkpoint.split("-")[-1])
    print("讀取最新的 checkpoint,模型已訓練 {} 個 epochs ".format(last_epoch))
    
    # 將model和optimizer從ckpt中讀取出來
    model = ckpt.model
    optimizer = ckpt.optimizer
  
else:
    last_epoch = 0
    print("沒找到 checkpoint,即將開始重新訓練。")
    
# 後續在train_model當中,可以自行決定存檔週期
# 使用以下的函式
# 這個回傳值為儲存路徑,其實用不太到
ckpt_save_path = ckpt_manager.save()

這個大區塊主要處理了以下幾件事情

  1. 定義儲存路徑
  2. 定義ckpt以及manager
  3. 檢查目標路徑中是否存在著之前已經訓練過的網路
    如果有的話就讀進來測試或繼續訓練
    沒有的話就重頭開始
  4. (在訓練的迴圈中)用manager儲存權重

千萬記得要在訓練過程中才callckpt_manager.save()
我們會在下個區塊中再次看到它~

train_model

# 這個writer等等會拿來將數據寫到tensorboard上
summary_writer = tf.summary.create_file_writer(LOG_PATH)

for epoch in range(last_epoch + 1, EPOCH + 1):
    # 怒train它一發,這裡假設你已經把資料預處理成batch的形式
    for x, y in zip(x_train, y_train):
        train_step(x, y)
        
    # 存檔! 再次申明這個回傳值基本上用不太到
    ckpt_save_path = ckpt_manager.save()
    
    # 寫入tensorboard
    with summary_writer.as_default():
        tf.summary.scalar("Train_Loss", metrics_loss.result(), step=epoch)
        tf.summary.scalar("Train_Accuracy", metrics_accuracy.result(), step=epoch)
        
    # 把存在metrics裡的內容清空    
    metrics_loss.reset_states()
    metrics_acc.reste_states()    

train_model的核心步驟如下

  1. 建立一個用來遍歷epoch的迴圈
  2. 把每個batch丟進去train
  3. ckpt存檔
  4. logs存檔 (寫入tensorboard)
  5. 重置metrics的內容