Tensorflow 2.x入門教程

凌逆戰發表於2022-03-01

前言

  至於為什麼寫這個教程,首先是為了自己學習做個記錄,其次是因為Tensorflow的API寫的很好,但是他的教程寫的太亂了,不適合新手學習。tensorflow 1 和tensorflow 2 有相似之處但是不相容,tensorflow 2將keras融合了。TensorFlow™ 是一個採用 資料流圖(data flow graphs),用於數值計算的開源軟體庫。圖中得節點(Nodes)表示數學操作,圖中的線(edges)則表示在節點間相互聯絡的多維資料陣列,即張量(tensor)。它靈活的架構讓你可以在多種平臺上展開計算,例如臺式計算機中的一個或多個CPU(或GPU),伺服器,移動裝置等等。

TensorFlow的主要優點:

  • 靈活性:支援底層數值計算,C++自定義操作符
  • 可移植性:從伺服器到PC到手機,從CPU到GPU到TPU
  • 分散式計算:分散式平行計算,可指定操作符對應計算裝置

層次結構

  TensorFlow的層次結構從低到高可以分成如下五層:硬體層,核心層,低階API,中階API,高階API。

  • 第一層:硬體層,TensorFlow支援CPU、GPU或TPU加入計算資源池。
  • 第二層:核心層,為C++實現的核心,kernel可以跨平臺分佈執行。
  • 第三層:低階API,由Python實現的操作符,提供了封裝C++核心的低階API指令。主要包括各種張量操作運算元、計算圖、自動微分。如tf.Variable,tf.constant,tf.function,tf.GradientTape,tf.nn.softmax... 
  • 第四層:中階API,由Python實現的模型元件,對低階API進行了函式封裝。主要包括資料管道(tf.data)、特徵列(tf.feature_column)、啟用函式(tf.nn)、模型層(tf.keras.layers)、損失函式(tf.keras.losses)、評估函式(tf.keras.metrics)、優化器(tf.keras.optimizers)、回撥函式(tf.keras.callbacks) 等等。
  • 第五層:高階API,由Python實現的模型成品。主要為tf.keras.models提供的模型的類介面,主要包括:模型的構建(Sequential、functional API、Model子類化)、型的訓練(內建fit方法、內建train_on_batch方法、自定義訓練迴圈、單GPU訓練模型、多GPU訓練模型、TPU訓練模型)、模型的部署(tensorflow serving部署模型、使用spark(scala)呼叫tensorflow模型)。 

概述

建立張量

tf.constant(value, dtype=tf.float32)    # 常數
tf.range(start, limit=None, delta=1)    # 生成一個範圍內間隔為delta的 張量
tf.linspace(start, stop, num) # 在一個間隔內生成均勻間隔的值
tf.zeros()  # 建立全0張量
tf.ones()  # 建立全1張量
tf.zeros_like(input)  # 建立和input一樣大小的張量
tf.fill(dims, value)    # 建立shape為dim,全為value的張量

tf.random.uniform([5], minval=0, maxval=10)     # 均勻分佈隨機
tf.random.normal([3, 3], mean=0.0, stddev=1.0)  # 正態分佈隨機

tf.Variable(initial_value)  # 變數

tf.Variable:

  • name:變數的名字,預設情況下,會自動獲得唯一的變數名
  • trainable:設定為 False 可以關閉梯度。例如,訓練計步器就是一個不需要梯度的變數

tf.rank(a):求矩陣的秩

變數的裝置位置

為了提高效能,TensorFlow 會嘗試將張量和變數放在與其 dtype 相容的最快裝置上。這意味著如果有 GPU,那麼大部分變數都會放置在 GPU 上,不過,我們可以重寫變數的位置。

with tf.device('CPU:0'):
  # Create some tensors
  a = tf.Variable([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
  b = tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
  c = tf.matmul(a, b)

print(c)

使用assign重新分配張量

a.assign([5, 6])  # a = [5, 6]
a.assign_add([2, 3])  # a = a+[2,3]
a.assign_sub([7, 9])  # a=a-[7,9]

維度變換

維度變換相關函式主要有 tf.reshape,tf.squeeze,tf.expand_dims,tf.transpose。

  • tf.reshape():改變張量的形狀
  • tf.squeeze():減少維度為1的維度
  • tf.expand_dims(input, axis):增加維度
  • tf.transpose(a, perm=None):交換維度

tf.reshape可以改變張量的形狀,但是其本質上不會改變張量元素的儲存順序,所以,該操作實際上非常迅速,並且是可逆的。

合併分隔

和numpy類似,可以用tf.concattf.stack方法對多個張量進行合併,可以用tf.split方法把一個張量分割成多個張量。

tf.concat和tf.stack有略微的區別,tf.concat是連線,不會增加維度,而tf.stack是堆疊,會增加維度。

a = tf.constant([[1.0, 2.0], [3.0, 4.0]])  # (2,2)
b = tf.constant([[5.0, 6.0], [7.0, 8.0]])  # (2,2)

c = tf.concat([a, b], axis=0)  # (4, 2)
c = tf.stack([a, b], axis=0)  # (2, 2, 2)
d = tf.split(c, 2, axis=0)  # [(1, 2, 2),(1, 2, 2)]

Tensor與Array的轉換

c = np.array(b)     # tensor 轉 np
c = b.numpy()       # tensor 轉 np
tf.convert_to_tensor(c)     # np 轉 tensor

數學運算

tf.add(a,b)        # 加法 a+b
tf.multiply(a,b)       # 逐元素乘法a*b
tf.matmul(a,b)     # 矩陣乘法a@b

型別轉換

tensorflow支援的模型有:tf.float16、tf.float64、tf.int8、tf.int16、tf.int32...

a = tf.constant([2.2, 3.3, 4.4], dtype=tf.float64)
b = tf.cast(a, dtype=tf.float16)    # 型別轉換

計算圖

  有三種計算圖的構建方式:靜態計算圖動態計算圖,以及Autograph。在TensorFlow1.0時代,採用的是靜態計算圖,需要先使用TensorFlow的各種運算元建立計算圖,然後再開啟一個會話Session,顯式執行計算圖。而在TensorFlow2.0時代,採用的是動態計算圖,即每使用一個運算元後,該運算元會被動態加入到隱含的預設計算圖中立即執行得到結果,而無需開啟Session。使用動態計算圖(Eager Excution)的好處是方便除錯程式,它會讓TensorFlow程式碼的表現和Python原生程式碼的表現一樣,寫起來就像寫numpy一樣,各種日誌列印,控制流全部都是可以使用的。使用動態計算圖的缺點是執行效率相對會低一些。因為使用動態圖會有許多次Python程式和TensorFlow的C++程式之間的通訊。而靜態計算圖構建完成之後幾乎全部在TensorFlow核心上使用C++程式碼執行,效率更高。此外靜態圖會對計算步驟進行一定的優化,剪去和結果無關的計算步驟。

  如果需要在TensorFlow2.0中使用靜態圖,可以使用@tf.function裝飾器將普通Python函式轉換成對應的TensorFlow計算圖構建程式碼。執行該函式就相當於在TensorFlow1.0中用Session執行程式碼。使用tf.function構建靜態圖的方式叫做 Autograph。當然Autograph機制能夠轉換的程式碼並不是沒有任何約束的,有一些編碼規範需要遵循,否則可能會轉換失敗或者不符合預期。

  1. 被@tf.function修飾的函式應儘可能使用TensorFlow中的函式而不是Python中的其他函式。例如使用tf.print而不是print,使用tf.range而不是range,使用tf.constant(True)而不是True.
  2. 避免在@tf.function修飾的函式內部定義tf.Variable
  3. 被@tf.function修飾的函式不可修改該函式外部的列表或字典等資料結構變數

  計算圖由節點(nodes)和線(edges)組成。節點表示操作符Operator,或者稱之為運算元,線表示計算間的依賴。實線表示有資料傳遞依賴,傳遞的資料即張量。虛線通常可以表示控制依賴,即執行先後順序。

import tensorflow as tf

# 使用autograph構建靜態圖
@tf.function
def strjoin(x,y):
    z =  tf.strings.join([x,y],separator = " ")
    tf.print(z)
    return z

result = strjoin(tf.constant("hello"),tf.constant("world"))

print(result)

您可以像這樣測量靜態圖和動態圖效能差異:

Tensorflow 2.x入門教程
x = tf.random.uniform(shape=[10, 10], minval=-1, maxval=2, dtype=tf.dtypes.int32)

def power(x, y):
  result = tf.eye(10, dtype=tf.dtypes.int32)
  for _ in range(y):
    result = tf.matmul(x, result)
  return result

print("Eager execution:", timeit.timeit(lambda: power(x, 100), number=1000))        # 2.56378621799

# 將python函式轉換為圖形
power_as_graph = tf.function(power)
print("Graph execution:", timeit.timeit(lambda: power_as_graph(x, 100), number=1000))        # 0.683253670
View Code

我們還可以再函式前使用裝飾器 @tf.function 呼叫tf.function,同時也可以使用 tf.config.run_functions_eagerly(True) 關閉Function建立和執行圖形的能力。

  前面在介紹Autograph的編碼規範時提到構建Autograph時應該避免在@tf.function修飾的函式內部定義tf.Variable。但是如果在函式外部定義tf.Variable的話,又會顯得這個函式有外部變數依賴,封裝不夠完美。一種簡單的思路是定義一個類,並將相關的tf.Variable建立放在類的初始化方法中。而將函式的邏輯放在其他方法中。

Tensorflow 2.x入門教程
class DemoModule(tf.Module):
    def __init__(self, init_value=tf.constant(0.0), name=None):
        super(DemoModule, self).__init__(name=name)
        with self.name_scope:  # 相當於with tf.name_scope("demo_module")
            self.x = tf.Variable(init_value, dtype=tf.float32, trainable=True)

    @tf.function
    def addprint(self, a):
        with self.name_scope:
            self.x.assign_add(a)
            tf.print(self.x)
            return self.x
View Code

自動微分

  自動微分用於訓練神經網路的反向傳播非常有用,TensorFlow 會記住在前向傳遞過程中哪些運算以何種順序發生。隨後,在後向傳遞期間,以相反的順序遍歷此運算列表來計算梯度。

Tensorflow一般使用tf.GradientTape來記錄正向運算過程,然後反向傳播自動計算梯度值。

$$f(x)=ax^2+bx+c$$

x = tf.Variable(0.0,name = "x",dtype = tf.float32)
a = tf.constant(1.0)
b = tf.constant(-2.0)
c = tf.constant(1.0)

with tf.GradientTape() as tape:
    y = a*tf.pow(x,2) + b*x + c
    
dy_dx = tape.gradient(y,x)
print(dy_dx)    # tf.Tensor(-2.0, shape=(), dtype=float32)

對常量張量也可以求導,只不過需要增加watch

Tensorflow 2.x入門教程
with tf.GradientTape() as tape:
    tape.watch([a,b,c])
    y = a*tf.pow(x,2) + b*x + c
    
dy_dx,dy_da,dy_db,dy_dc = tape.gradient(y,[x,a,b,c])
print(dy_da)    # tf.Tensor(0.0, shape=(), dtype=float32)
print(dy_dc)    # tf.Tensor(1.0, shape=(), dtype=float32)
View Code

可以求二階導數

Tensorflow 2.x入門教程
with tf.GradientTape() as tape2:
    with tf.GradientTape() as tape1:   
        y = a*tf.pow(x,2) + b*x + c
    dy_dx = tape1.gradient(y,x)   
dy2_dx2 = tape2.gradient(dy_dx,x)

print(dy2_dx2)    # tf.Tensor(2.0, shape=(), dtype=float32)
View Code

利用梯度和優化器求最小值

Tensorflow 2.x入門教程
# 求f(x) = a*x**2 + b*x + c的最小值
# 使用optimizer.apply_gradients

x = tf.Variable(0.0,name = "x",dtype = tf.float32)
a = tf.constant(1.0)
b = tf.constant(-2.0)
c = tf.constant(1.0)

optimizer = tf.keras.optimizers.SGD(learning_rate=0.01)
for _ in range(1000):
    with tf.GradientTape() as tape:
        y = a*tf.pow(x,2) + b*x + c
    dy_dx = tape.gradient(y,x)  # 計算梯度
    optimizer.apply_gradients(grads_and_vars=[(dy_dx,x)])  # 根據梯度更新變數
    
tf.print("y =",y,"; x =",x)
View Code

如果不想被計算梯度:

with tf.GradientTape(watch_accessed_variables=False) as tape:
    pass

使用TensorFlow實現神經網路模型的一般流程包括:

  1. 準備資料
  2. 定義模型
  3. 訓練模型
  4. 評估模型
  5. 推理模型
  6. 儲存模型

資料輸入

  tensorflow支援 Numpy 陣列、Pandas DataFrame、Python 生成器、csv檔案、文字檔案、檔案路徑、TFrecords檔案等方式構建資料管道。如果您的資料很小並且適合記憶體,我們建議您使用tf.data.Dataset.from_tensor_slices()從Numpy array構建資料管道

  • Dataset:如果您有大型資料集並且需要進行分散式訓練
  • Sequence:如果您有大型資料集並且需要執行大量在 TensorFlow 中無法完成的自定義 Python 端處理(例如,如果您依賴外部庫進行資料載入或預處理)
  • 通過tfrecords檔案方式構建資料管道較為複雜,需要對樣本構建tf.Example後壓縮成字串寫到tfrecoreds檔案,讀取後再解析成tf.Example。但tfrecoreds檔案的優點是壓縮後檔案較小,便於網路傳播,載入速度較快。

Numpy構建資料

官方推薦使用 tf.data.Dataset.from_tensors() 或 tf.data.Dataset.from_tensor_slices() 建立資料集,Dataset支援一類特殊的操作Trainformation(打亂、生成epoch...等操作)

  • data.map(function):將轉換函式對映到資料集每一個元素
  • data.batch(batch_size):構建batch
  • data.shuffle(buffer_size):隨機打亂輸入資料,從該緩衝區中隨機取樣元素
  • data.repeat():repeat的功能就是將整個序列重複多次,一般不帶引數
  • data.prefetch(tf.data.experimental.AUTOTUNE)  :預先取 資料
  • data.take():取樣,從開始位置取前幾個元素
  • ...
features = np.arange(0, 100, dtype=np.int32)    # # (100,)
labels = np.zeros(100, dtype=np.int32)  # (100,)

data = tf.data.Dataset.from_tensor_slices((features, labels))   # 建立資料集
data = data.repeat()    # 無限期地補充資料
data = data.shuffle(buffer_size=100)    # 打亂資料
data = data.batch(batch_size=4)     # 批量資料
data = data.prefetch(buffer_size=1)     # 預取批處理(預載入批處理,消耗更快)

for batch_x, batch_y in data.take(5):
    print(batch_x.shape, batch_y.shape)     # (4,) (4,)
    break

注意:如果你打算多次呼叫,你可以使用迭代器的方式:

Tensorflow 2.x入門教程
ite_data = iter(data)
for i in range(5):
batch_x, batch_y = next(ite_data)
print(batch_x, batch_y)

for i in range(5):
batch_x, batch_y = next(ite_data)
print(batch_x, batch_y)
View Code

提升管道效能

  訓練深度學習模型常常會非常耗時。模型訓練的耗時主要來自於兩個部分,一部分來自資料準備,另一部分來自引數迭代。引數迭代過程的耗時通常依賴於GPU來提升。而資料準備過程的耗時則可以通過構建高效的資料管道進行提升。

以下是一些構建高效資料管道的建議。

  1. 使用 prefetch 方法讓資料準備和引數迭代兩個過程相互並行。
  2. 使用 interleave 方法可以讓資料讀取過程多程式執行,並將不同來源資料夾在一起。
  3. 使用 map 時設定num_parallel_calls 讓資料轉換過程多進行執行。
  4. 使用 cache 方法讓資料在第一個epoch後快取到記憶體中,僅限於資料集不大情形。
  5. 使用 map轉換時,先batch,然後採用向量化的轉換方法對每個batch進行轉換。

生成器構建資料

def generate_features():
    # 函式生成一個隨機字串
    def random_string(length):
        return ''.join(random.choice(string.ascii_letters) for m in range(length))
    # 返回一個隨機字串、一個隨機向量和一個隨機整數
    yield random_string(4), np.random.uniform(size=4), random.randint(0, 10)


data = tf.data.Dataset.from_generator(generate_features, output_types=(tf.string, tf.float32, tf.int32))
data = data.repeat()        # 無限期地補充資料
data = data.shuffle(buffer_size=100)    # 打亂資料
data = data.batch(batch_size=4)     # 批量資料(將記錄聚合在一起)
data = data.prefetch(buffer_size=1)     # 預取批量(預載入批量以便更快的消耗)

# Display data.
for batch_str, batch_vector, batch_int in data.take(5):
    # (4,) (4, 4) (4,)
    print(batch_str.shape, batch_vector.shape, batch_int.shape)

keras.utils.Sequence

特別是,keras.utils.Sequence該類提供了一個簡單的介面來構建 Python 資料生成器,該生成器可以感知多處理並且可以洗牌。

Sequence必須實現兩種方法:

  • __getitem__:返回一個batch資料
  • __len__:整型,返回batch的數量
import tensorflow as tf
from keras.utils.data_utils import Sequence

class SequenceDataset(Sequence):
    def __init__(self, batch_size):
        self.input_data = tf.random.normal((640, 8192, 1))
        self.label_data = tf.random.normal((640, 8192, 1))
        self.batch_size = batch_size

    def __len__(self):
        return int(tf.math.ceil(len(self.input_data) / float(self.batch_size)))

    # 每次輸出一個batch
    def __getitem__(self, idx):
        batch_x = self.input_data[idx * self.batch_size:(idx + 1) * self.batch_size]
        batch_y = self.label_data[idx * self.batch_size:(idx + 1) * self.batch_size]
        return batch_x, batch_y


sequence = SequenceDataset(batch_size=64)

for batch_idx, (x, y) in enumerate(sequence):
    print(batch_idx, x.shape, y.shape)  # tf.float32
    # 0 (64, 8192, 1) (64, 8192, 1)
    break

搭建模型

  深度學習模型一般由各種模型層組合而成,如果這些內建模型層不能夠滿足需求,我們也可以通過編寫tf.keras.Lambda匿名模型層或繼承tf.keras.layers.Layer基類構建自定義的模型層。其中tf.keras.Lambda匿名模型層只適用於構造沒有學習引數的模型層。

搭建模型有以下3種方式構建模型:

  1. Sequential順序模型:用於簡單的層堆疊, 其中每一層恰好有一個輸入張量一個輸出張量
  2. 函式式API模型:多輸入多輸出,或者模型需要共享權重,或者模型具有殘差連線等非順序結構,
  3. 繼承Model基類自定義模型:如果無特定必要,儘可能避免使用Model子類化的方式構建模型,這種方式提供了極大的靈活性,但也有更大的概率出錯

順序建模

model = keras.Sequential([
    layers.Dense(2, activation="relu", name="layer1"),
    layers.Dense(3, activation="relu", name="layer2"),
    layers.Dense(4, name="layer3"),
])

還可以通過add方法建立順序模型

model = keras.Sequential()
model.add(layers.Dense(2, activation="relu"))
model.add(layers.Dense(3, activation="relu"))
model.add(layers.Dense(4))

因為模型不知道輸入shape,所以起初模型沒有權重,因此我們需要告知模型輸入shape

# 在第一層新增Input
model.add(keras.Input(shape=(4,)))
model.add(layers.Dense(2, activation="relu"))

# 或者在第一層新增input_shape
model.add(layers.Dense(2, activation="relu", input_shape=(4,)))

順序模型可以配合add 和 model.summary() 在模型的任何位置檢視該層的輸入輸出。

函式式API建模

  函式式API搭建模型比Sequential更加靈活,可以處理具有非線性拓撲、共享層甚至多個輸入或輸出的模型。

inputs = keras.Input(shape=(784,))
x = layers.Dense(64, activation="relu")(inputs)
x = layers.Dense(64, activation="relu")(x)
outputs = layers.Dense(10)(x)
model = keras.Model(inputs=inputs, outputs=outputs, name="mnist_model")
model.summary()     # 檢視模型摘要

還可以將模型繪製為圖形

keras.utils.plot_model(model, "my_first_model_with_shape_info.png", show_shapes=True)

補充:函式式模型是可以巢狀的

自定義建模

  在 TensorFlow 中,模型類的繼承關係為: 

  tf.keras.Modeltf.keras.layers.Layertf.Module

  通常使用Layer類來定義內部計算塊,並使用Model類定義外部模型(訓練的物件)。

繼承tf.Module栗子:

Tensorflow 2.x入門教程
class SequentialModule(tf.Module):
    def __init__(self, name=None):
        super().__init__(name=name)
        self.dense_1 = Dense(in_features=3, out_features=3)
        self.dense_2 = Dense(in_features=3, out_features=2)

    def __call__(self, x):
        x = self.dense_1(x)
        return self.dense_2(x)

my_model = SequentialModule(name="the_model")
View Code

繼承tf.keras.layers.Layer栗子:

Tensorflow 2.x入門教程
class ResBlock(layers.Layer):
    def __init__(self, kernel_size, **kwargs):
        super(ResBlock, self).__init__(**kwargs)
        self.kernel_size = kernel_size

    def build(self, input_shape):
        self.conv1 = layers.Conv1D(filters=64, kernel_size=self.kernel_size,
                                   activation="relu", padding="same")
        self.conv2 = layers.Conv1D(filters=32, kernel_size=self.kernel_size,
                                   activation="relu", padding="same")
        self.conv3 = layers.Conv1D(filters=input_shape[-1],
                                   kernel_size=self.kernel_size, activation="relu", padding="same")
        self.maxpool = layers.MaxPool1D(2)
        super(ResBlock, self).build(input_shape)  # 相當於設定self.built = True

    def call(self, inputs):
        x = self.conv1(inputs)
        x = self.conv2(x)
        x = self.conv3(x)
        x = layers.Add()([inputs, x])
        x = self.maxpool(x)
        return x

    # 如果要讓自定義的Layer通過Functional API 組合成模型時可以序列化,需要自定義get_config方法。
    def get_config(self):
        config = super(ResBlock, self).get_config()
        config.update({'kernel_size': self.kernel_size})
        return config


resblock = ResBlock(kernel_size=3)
resblock.build(input_shape=(None, 200, 7))
resblock.compute_output_shape(input_shape=(None, 200, 7))
View Code

繼承tf.Module栗子:

Tensorflow 2.x入門教程
class ImdbModel(models.Model):
    def __init__(self):
        super(ImdbModel, self).__init__()

    def build(self, input_shape):
        self.embedding = layers.Embedding(MAX_WORDS, 7)
        self.block1 = ResBlock(7)
        self.block2 = ResBlock(5)
        self.dense = layers.Dense(1, activation="sigmoid")
        super(ImdbModel, self).build(input_shape)

    def call(self, x):
        x = self.embedding(x)
        x = self.block1(x)
        x = self.block2(x)
        x = layers.Flatten()(x)
        x = self.dense(x)
        return (x)


model = ImdbModel()
model.build(input_shape=(None, 200))
model.compile(optimizer='Nadam', loss='binary_crossentropy', metrics=['accuracy', "AUC"])
View Code

Model類具有Layer類相同的API,但有以下區別:

  • Model類 提供了內建的訓練  model.fit()  、評估 model.evaluate()  和預測  model.predict()  API
  • Model類 可以通過 model.layers 屬性公開內層的列表。
  • Model類 提供了儲存和序列化 API  save()、save_weights()...

  Layer 類對應於“層”,Model 類對應於“模型,如果您想知道“我應該使用Layer類還是Model類?”,請問自己:我需要呼叫fit()它嗎?我需要save() 嗎?如果是這樣,請與Model。如果不是,請使用Layer。

把Layer 類和Model 類用在一起,吃個栗子:

Tensorflow 2.x入門教程
class Sampling(layers.Layer):
    """使用(z_mean, z_log_var)對z進行取樣,z是對一個數字進行編碼的向量"""

    def call(self, inputs):
        z_mean, z_log_var = inputs
        batch = tf.shape(z_mean)[0]
        dim = tf.shape(z_mean)[1]
        epsilon = tf.keras.backend.random_normal(shape=(batch, dim))
        return z_mean + tf.exp(0.5 * z_log_var) * epsilon


class Encoder(layers.Layer):
    """將MNIST數字對映為一個三元組(z_mean, z_log_var, z)"""

    def __init__(self, latent_dim=32, intermediate_dim=64, name="encoder", **kwargs):
        super(Encoder, self).__init__(name=name, **kwargs)
        self.dense_proj = layers.Dense(intermediate_dim, activation="relu")
        self.dense_mean = layers.Dense(latent_dim)
        self.dense_log_var = layers.Dense(latent_dim)
        self.sampling = Sampling()

    def call(self, inputs):
        x = self.dense_proj(inputs)
        z_mean = self.dense_mean(x)
        z_log_var = self.dense_log_var(x)
        z = self.sampling((z_mean, z_log_var))
        return z_mean, z_log_var, z


class Decoder(layers.Layer):
    """將已編碼的數字向量z轉換回可讀的數字"""

    def __init__(self, original_dim, intermediate_dim=64, name="decoder", **kwargs):
        super(Decoder, self).__init__(name=name, **kwargs)
        self.dense_proj = layers.Dense(intermediate_dim, activation="relu")
        self.dense_output = layers.Dense(original_dim, activation="sigmoid")

    def call(self, inputs):
        x = self.dense_proj(inputs)
        return self.dense_output(x)


class VariationalAutoEncoder(keras.Model):
    """將編碼器和解碼器組合成端到端的訓練模型。"""

    def __init__(self, original_dim, intermediate_dim=64, latent_dim=32, name="autoencoder", **kwargs):
        super(VariationalAutoEncoder, self).__init__(name=name, **kwargs)
        self.original_dim = original_dim
        self.encoder = Encoder(latent_dim=latent_dim, intermediate_dim=intermediate_dim)
        self.decoder = Decoder(original_dim, intermediate_dim=intermediate_dim)

    def call(self, inputs):
        z_mean, z_log_var, z = self.encoder(inputs)
        reconstructed = self.decoder(z)
        # Add KL divergence regularization loss.
        kl_loss = -0.5 * tf.reduce_mean(z_log_var - tf.square(z_mean) - tf.exp(z_log_var) + 1)
        self.add_loss(kl_loss)
        return reconstructed
View Code

自定義迴圈訓練模型

Tensorflow 2.x入門教程
original_dim = 784
vae = VariationalAutoEncoder(original_dim, 64, 32)

optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
mse_loss_fn = tf.keras.losses.MeanSquaredError()

loss_metric = tf.keras.metrics.Mean()

(x_train, _), _ = tf.keras.datasets.mnist.load_data()
x_train = x_train.reshape(60000, 784).astype("float32") / 255

train_dataset = tf.data.Dataset.from_tensor_slices(x_train)
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)

epochs = 2

# Iterate over epochs.
for epoch in range(epochs):
    print("Start of epoch %d" % (epoch,))

    # Iterate over the batches of the dataset.
    for step, x_batch_train in enumerate(train_dataset):
        with tf.GradientTape() as tape:
            reconstructed = vae(x_batch_train)
            # Compute reconstruction loss
            loss = mse_loss_fn(x_batch_train, reconstructed)
            loss += sum(vae.losses)  # Add KLD regularization loss

        grads = tape.gradient(loss, vae.trainable_weights)
        optimizer.apply_gradients(zip(grads, vae.trainable_weights))

        loss_metric(loss)

        if step % 100 == 0:
            print("step %d: mean loss = %.4f" % (step, loss_metric.result()))
View Code

內建迴圈方法

Tensorflow 2.x入門教程
vae = VariationalAutoEncoder(784, 64, 32)

optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)

vae.compile(optimizer, loss=tf.keras.losses.MeanSquaredError())
vae.fit(x_train, x_train, epochs=2, batch_size=64)
View Code

函式式API和自定義Model類混搭訓練

Tensorflow 2.x入門教程
original_dim = 784
intermediate_dim = 64
latent_dim = 32

# Define encoder model.
original_inputs = tf.keras.Input(shape=(original_dim,), name="encoder_input")
x = layers.Dense(intermediate_dim, activation="relu")(original_inputs)
z_mean = layers.Dense(latent_dim, name="z_mean")(x)
z_log_var = layers.Dense(latent_dim, name="z_log_var")(x)
z = Sampling()((z_mean, z_log_var))
encoder = tf.keras.Model(inputs=original_inputs, outputs=z, name="encoder")

# Define decoder model.
latent_inputs = tf.keras.Input(shape=(latent_dim,), name="z_sampling")
x = layers.Dense(intermediate_dim, activation="relu")(latent_inputs)
outputs = layers.Dense(original_dim, activation="sigmoid")(x)
decoder = tf.keras.Model(inputs=latent_inputs, outputs=outputs, name="decoder")

# Define VAE model.
outputs = decoder(z)
vae = tf.keras.Model(inputs=original_inputs, outputs=outputs, name="vae")

# Add KL divergence regularization loss.
kl_loss = -0.5 * tf.reduce_mean(z_log_var - tf.square(z_mean) - tf.exp(z_log_var) + 1)
vae.add_loss(kl_loss)

# Train.
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
vae.compile(optimizer, loss=tf.keras.losses.MeanSquaredError())
vae.fit(x_train, x_train, epochs=3, batch_size=64)
函式式API訓練模型
View Code

補充知識:自定義層

如果自定義模型層沒有需要被訓練的引數,一般推薦使用Lamda層實現。

mypower = layers.Lambda(lambda x:tf.math.pow(x,2))
mypower(tf.range(5))

如果自定義模型層有需要被訓練的引數,則可以通過繼承Layer基類實現。Layer的子類化一般需要重新實現初始化方法,Build方法和Call方法。如果built = False,呼叫__call__時會先呼叫build方法, 再呼叫call方法。

損失函式

  一般來說,監督學習的目標函式由損失函式和正則化項組成(Objective = Loss + Regularization)。對於keras模型,目標函式中的正則化項一般在各層中指定,例如使用Dense的 kernel_regularizer 和 bias_regularizer等引數指定權重使用l1或者l2正則化項,此外還可以用kernel_constraint 和 bias_constraint等引數約束權重的取值範圍,這也是一種正則化手段。

損失函式在模型編譯時候指定。

  • 對於迴歸模型,通常使用的損失函式是平方損失函式 mean_squared_error,簡寫為 mse,類實現形式為 MeanSquaredError 和 MSE
  • 對於二分類模型,通常使用的是二元交叉熵損失函式 binary_crossentropy。
  • 對於多分類模型,如果label是one-hot編碼的,則使用交叉熵損失函式 categorical_crossentropy。如果label是序號編碼的,則需要使用稀疏類別交叉熵損失函式 sparse_categorical_crossentropy。
  • 如果有需要,也可以自定義損失函式,自定義損失函式需要接收兩個張量y_true,y_pred作為輸入引數,並輸出一個標量作為損失函式值。

 常見的Loss可以參看Tensorflow的官網:tf.keras.losses

自定義損失函式

自定義損失函式接收兩個張量y_true,y_pred作為輸入引數,並輸出一個標量作為損失函式值。

def focal_loss(gamma=2., alpha=.25):
    def focal_loss_fixed(y_true, y_pred):
        pt_1 = tf.where(tf.equal(y_true, 1), y_pred, tf.ones_like(y_pred))
        pt_0 = tf.where(tf.equal(y_true, 0), y_pred, tf.zeros_like(y_pred))
        loss = -tf.sum(alpha * tf.pow(1. - pt_1, gamma) * tf.log(1e-07+pt_1)) \
           -tf.sum((1-alpha) * tf.pow( pt_0, gamma) * tf.log(1. - pt_0 + 1e-07))
        return loss
    return focal_loss_fixed

也可以對tf.keras.losses.Loss進行子類化,重寫call方法實現損失的計算邏輯,從而得到損失函式的類的實現。

class FocalLoss(losses.Loss):
    def __init__(self,gamma=2.0,alpha=0.25):
        self.gamma = gamma
        self.alpha = alpha

    def call(self,y_true,y_pred):
        pt_1 = tf.where(tf.equal(y_true, 1), y_pred, tf.ones_like(y_pred))
        pt_0 = tf.where(tf.equal(y_true, 0), y_pred, tf.zeros_like(y_pred))
        loss = -tf.sum(self.alpha * tf.pow(1. - pt_1, self.gamma) * tf.log(1e-07+pt_1)) \
           -tf.sum((1-self.alpha) * tf.pow( pt_0, self.gamma) * tf.log(1. - pt_0 + 1e-07))
        return loss

度量函式

  人們通常會通過度量函式來從另一個方面 評估模型的好壞,度量函式不要求連續可導,

  • 編譯模型時,可以通過列表形式指定多個評估指標。
  • 也可以自定義評估指標。自定義評估指標需要接收兩個張量y_true,y_pred作為輸入引數,並輸出一個標量作為評估值。
  • 也可以繼承 tf.keras.metrics.Metric自定義度量方法,update_state方法,result方法實現評估指標的計算邏輯,從而得到評估指標的類的實現形式。

Tensorflow內建的評估指標可以常見:tf.keras.metrics

函式自定義度量

  如果編寫函式形式的評估指標,則只能取epoch中各個batch計算的評估指標結果,這個結果通常會偏離拿整個epoch資料一次計算的結果。

Tensorflow 2.x入門教程
@tf.function
def ks(y_true,y_pred):
    y_true = tf.reshape(y_true,(-1,))
    y_pred = tf.reshape(y_pred,(-1,))
    length = tf.shape(y_true)[0]
    t = tf.math.top_k(y_pred,k = length,sorted = False)
    y_pred_sorted = tf.gather(y_pred,t.indices)
    y_true_sorted = tf.gather(y_true,t.indices)
    cum_positive_ratio = tf.truediv(
        tf.cumsum(y_true_sorted),tf.reduce_sum(y_true_sorted))
    cum_negative_ratio = tf.truediv(
        tf.cumsum(1 - y_true_sorted),tf.reduce_sum(1 - y_true_sorted))
    ks_value = tf.reduce_max(tf.abs(cum_positive_ratio - cum_negative_ratio)) 
    return ks_value
View Code

類自定義度量

由於訓練的過程通常是分批次訓練的,而評估指標要跑完一個epoch才能夠得到整體的指標結果。因此,類形式的評估指標更為常見。即需要編寫update_state方法在每個batch後更新相關中間變數的狀態,編寫result方法輸出最終指標結果。

Tensorflow 2.x入門教程
class KS(metrics.Metric):

    def __init__(self, name="ks", **kwargs):
        super(KS, self).__init__(name=name, **kwargs)
        self.true_positives = self.add_weight(
            name="tp", shape=(101,), initializer="zeros")
        self.false_positives = self.add_weight(
            name="fp", shape=(101,), initializer="zeros")

    @tf.function
    def update_state(self, y_true, y_pred):
        y_true = tf.cast(tf.reshape(y_true, (-1,)), tf.bool)
        y_pred = tf.cast(100 * tf.reshape(y_pred, (-1,)), tf.int32)

        for i in tf.range(0, tf.shape(y_true)[0]):
            if y_true[i]:
                self.true_positives[y_pred[i]].assign(
                    self.true_positives[y_pred[i]] + 1.0)
            else:
                self.false_positives[y_pred[i]].assign(
                    self.false_positives[y_pred[i]] + 1.0)
        return (self.true_positives, self.false_positives)

    @tf.function
    def result(self):
        cum_positive_ratio = tf.truediv(
            tf.cumsum(self.true_positives), tf.reduce_sum(self.true_positives))
        cum_negative_ratio = tf.truediv(
            tf.cumsum(self.false_positives), tf.reduce_sum(self.false_positives))
        ks_value = tf.reduce_max(tf.abs(cum_positive_ratio - cum_negative_ratio))
        return ks_value


y_true = ...
y_pred = ...

myks = KS()
myks.update_state(y_true, y_pred)
tf.print(myks.result())
View Code

優化器

  機器學習界有一群煉丹師,他們每天的日常是:拿來藥材(資料),架起八卦爐(模型),點著六味真火(優化演算法),就搖著蒲扇等著丹藥出爐了。不過,當過廚子的都知道,同樣的食材,同樣的菜譜,但火候不一樣了,這出來的口味可是千差萬別。火小了夾生,火大了易糊,火不勻則半生半糊。機器學習也是一樣,模型優化演算法的選擇直接關係到最終模型的效能。有時候效果不好,未必是特徵的問題或者模型設計的問題,很可能就是優化演算法的問題。

  深度學習優化演算法大概經歷了 SGD -> SGDM -> NAG ->Adagrad -> Adadelta(RMSprop) -> Adam -> Nadam 這樣的發展歷程。model.compile(optimizer=optimizers.SGD(learning_rate=0.01), loss=loss)

  • SGD:預設引數為純SGD, 設定momentum引數不為0實際上變成SGDM, 考慮了一階動量, 設定 nesterov為True後變成NAG,即 Nesterov Acceleration Gradient,在計算梯度時計算的是向前走一步所在位置的梯度
  • Adagrad:考慮了二階動量,對於不同的引數有不同的學習率,即自適應學習率。缺點是學習率單調下降,可能後期學習速率過慢乃至提前停止學習
  • RMSprop:考慮了二階動量,對於不同的引數有不同的學習率,即自適應學習率,對Adagrad進行了優化,通過指數平滑只考慮一定視窗內的二階動量
  • Adadelta:考慮了二階動量,與RMSprop類似,但是更加複雜一些,自適應性更強
  • Adam:同時考慮了一階動量和二階動量,可以看成RMSprop上進一步考慮了Momentum
  • Nadam:在Adam基礎上進一步考慮了 Nesterov Acceleration

  對於一般新手煉丹師,優化器直接使用Adam,並使用其預設引數就OK了。一些愛寫論文的煉丹師由於追求評估指標效果,可能會偏愛前期使用Adam優化器快速下降,後期使用SGD並精調優化器引數得到更好的結果。此外目前也有一些前沿的優化演算法,據稱效果比Adam更好,例如LazyAdam, Look-ahead, RAdam, Ranger等。

  初始化優化器時會建立一個變數optimier.iterations用於記錄迭代的次數。因此優化器和tf.Variable一樣,一般在@tf.function外建立。

優化器主要使用apply_gradients方法傳入變數和對應梯度從而來對給定變數進行迭代,

optimizer = tf.keras.optimizers.SGD(learning_rate=0.01)

with tf.GradientTape() as tape:
    ...
grads = tape.gradient(loss, model.trainable_weights)  # 根據損失 求梯度
optimizer.apply_gradients(zip(grads, model.trainable_weights))  # 根據梯度 優化模型

或者直接使用minimize方法對目標函式進行迭代優化。

@tf.function
def train(epoch=1000):
    for _ in tf.range(epoch):
        optimizer.minimize(loss, model.trainable_weights)
    tf.print("epoch = ", optimizer.iterations)
    return loss

當然,更常見的使用是在編譯時將優化器傳入model.fit

model.compile(optimizer=optimizers.SGD(learning_rate=0.01), loss=loss)

回撥函式

  tf.keras的回撥函式實際上是一個類,一般是在model.fit時作為引數指定,一般收集一些日誌資訊,改變學習率等超引數,提前終止訓練過程等等。

大部分時候,keras.callbacks子模組中定義的回撥函式類已經足夠使用了,如果有特定的需要,我們也可以通過對keras.callbacks.Callbacks實施子類化構造自定義的回撥函式。

  • BaseLogger: 收集每個epoch上metrics平均值,對stateful_metrics引數中的帶中間狀態的指標直接拿最終值無需對各個batch平均,指標均值結果將新增到logs變數中。該回撥函式被所有模型預設新增,且是第一個被新增的。
  • History: 將BaseLogger計算的各個epoch的metrics結果記錄到history這個dict變數中,並作為model.fit的返回值。該回撥函式被所有模型預設新增,在BaseLogger之後被新增。
  • EarlyStopping: 當被監控指標在設定的若干個epoch後沒有提升,則提前終止訓練。
  • TensorBoard: 為Tensorboard視覺化儲存日誌資訊。支援評估指標,計算圖,模型引數等的視覺化。
  • ModelCheckpoint: 在每個epoch後儲存模型。
  • ReduceLROnPlateau:如果監控指標在設定的若干個epoch後沒有提升,則以一定的因子減少學習率。
  • TerminateOnNaN:如果遇到loss為NaN,提前終止訓練。
  • LearningRateScheduler:學習率控制器。給定學習率lr和epoch的函式關係,根據該函式關係在每個epoch前調整學習率。
  • CSVLogger:將每個epoch後的logs結果記錄到CSV檔案中。
  • ProgbarLogger:將每個epoch後的logs結果列印到標準輸出流中。

Tensorboard

  Tensorboard有助於追蹤模型訓練過程的Scalars、Graphs、Distributions等等

  • Scalars:顯示損失和指標在每個時期如何變化。 還可以使用它來跟蹤訓練速度,學習率和其他標量值。
  • Graphs:可幫助您視覺化模型。
  • DistributionsHistograms 顯示張量隨時間的分佈。 可以 視覺化權重和偏差並驗證它們是否以預期的方式變化

在model.fit中使用

  當使用Model.fit() 函式進行訓練時, 新增 tf.keras.callback.TensorBoard 回撥函式可確保建立和儲存日誌

tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir="./event_file")
model.fit(... 
          callbacks=[tensorboard_callback])

在自定義框架中使用

summary_writer = tf.summary.create_file_writer("./event_file")
with summary_writer.as_default():
    tf.summary.scalar('train/loss', train_loss, step=epoch)
    tf.summary.scalar('train/accuracy', train_accuracy, step=epoch)
    tf.summary.scalar('val/loss', val_loss, step=epoch)
    tf.summary.scalar('val/accuracy', val_accuracy, step=epoch)

啟動Tensorboard,在當前資料夾中,cmd執行:

tensorboard --logdir "./"

訓練模型

訓練模型通常有3種方法,

  • 內建fit方法:支援對numpy array,tf.data.Dataset以及 Python generator資料進行訓練,並且可以通過設定回撥函式實現對訓練過程的複雜控制邏輯。
  • 內建train_on_batch方法
  • 自定義訓練迴圈

model.fit

model.fit(
    x=None, y=None, batch_size=None, epochs=1, verbose='auto',
    callbacks=None, validation_split=0.0, validation_data=None, shuffle=True,
    initial_epoch=0, steps_per_epoch=None)

引數:

  • x:輸入資料,可以是:
    • numpy陣列、陣列列表(如果模型有多個輸入)
    • Tensorflow張量或張量列表(如果模型有多個輸入)
    • 如果模型具有命名輸入,則將輸入名稱對映到相應的陣列/張量的字典。
    • tf.data資料集,應該返回一個(inputs, targets)或 的元組(inputs, targets, sample_weights)
    • 生成器或keras.utils.Sequence返回(inputs, targets) 或(inputs, targets, sample_weights)。
  • y:目標資料。與輸入資料一樣x,它可以是 Numpy 陣列或 TensorFlow 張量。它應該是一致的x(你不能有 Numpy 輸入和張量目標,或者相反)。如果x是資料集、生成器或keras.utils.Sequence例項,y則不應指定(因為目標將從 獲取x)。
  • batch_size:每次梯度更新的樣本數。如果未指定,將預設為 32。如果您的資料是資料集、生成器或例項的形式(因為它們生成批次),請不要指定。
  • epochs:訓練模型的週期數
  • verbose:'auto'、0、1 或 2。詳細模式。0 =靜默,1 = 進度條,2 = 每個 epoch 一行。'auto' 在大多數情況下預設為 1
  • callbacks:訓練期間呼叫的回撥列表。tf.keras.callbacks
  • validation_split:在 0 和 1 之間浮動。從訓練資料集中分離一部分資料用於驗證,並將在每個 epoch 結束時評估該資料的損失和指標
  • validation_data:驗證資料集
  • shuffle:布林值(是否在每個 epoch 之前對訓練資料進行洗牌)
  • initial_epoch:整數。開始訓練的epoch(對於恢復之前的訓練執行很有用)。

返回:history,呼叫 history.history 可以檢視訓練期間損失值和度量值的記錄

train_on_batch

  該內建方法相比較fit方法更加靈活,可以不通過回撥函式而直接在batch層次上更加精細地控制訓練的過程。

for epoch in tf.range(1, epoches + 1):
    for x, y in ds_train:
        train_result = model.train_on_batch(x, y)

    for x, y in ds_valid:
        valid_result = model.test_on_batch(x, y, reset_metrics=False)

自定義訓練迴圈

 自定義訓練迴圈無需編譯模型,直接利用優化器根據損失函式反向傳播迭代引數,擁有最高的靈活性。

訓練迴圈包括按順序重複執行三個任務:

  • 給模型輸入batch資料以生成輸出
  • 通過將輸出與標籤進行比較來計算損失
  • 使用GradientTape計算梯度
  • 使用這些梯度優化變數
optimizer = keras.optimizers.SGD(learning_rate=1e-3)  # 例項化一個優化器
loss_fn = keras.losses.BinaryCrossentropy()  # 例項化損失函式

train_loss = keras.metrics.Mean(name='train_loss')
valid_loss = keras.metrics.Mean(name='valid_loss')

train_metric = keras.metrics.BinaryAccuracy(name='train_accuracy')
valid_metric = keras.metrics.BinaryAccuracy(name='valid_accuracy')


@tf.function
def train_step(features, labels):
    with tf.GradientTape() as tape:
        logits = model(features, training=True)
        loss_value = loss_fn(labels, logits)
        # loss_value += sum(model.losses)   # 新增額外的損失
    grads = tape.gradient(loss_value, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))

    train_loss.update_state(loss_value)
    train_metric.update_state(labels, logits)


@tf.function
def valid_step(features, labels):
    val_logits = model(features, training=False)

    loss_value = loss_fn(labels, val_logits)
    valid_loss.update_state(loss_value)
    valid_metric.update_state(labels, val_logits)


epochs = 2
for epoch in range(epochs):
    start_time = time.time()
    for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
        loss_value = train_step(x_batch_train, y_batch_train)
    # 在每個epoch結束時執行驗證迴圈
    for x_batch_val, y_batch_val in val_dataset:
        valid_step(x_batch_val, y_batch_val)

    if epoch % 5 == 0:
        print('Epoch={},Loss:{},Accuracy:{},Valid Loss:{},Valid Accuracy:{}'.format(epoch, train_loss.result(),
                                                                                    train_metric.result(),
                                                                                    valid_loss.result(),
                                                                                    valid_metric.result()))
    train_loss.reset_states()
    valid_loss.reset_states()
    train_metric.reset_states()
    valid_metric.reset_states()

    print("執行時間: %.2fs" % (time.time() - start_time))

評估模型

通過自定義訓練迴圈訓練的模型沒有經過編譯,無法直接使用model.evaluate(ds_valid)方法

model.evaluate(x = x_test,y = y_test)

推理模型

可以使用以下方法:

model.predict(ds_test)

model(x_test)

model.call(x_test)

model.predict_on_batch(x_test)

推薦優先使用model.predict(ds_test)方法,既可以對Dataset,也可以對Tensor使用。

儲存和載入模型

  可以使用Keras方式儲存模型,也可以使用TensorFlow原生方式儲存。前者僅僅適合使用Python環境恢復模型,後者則可以跨平臺進行模型部署。推薦使用後一種方式進行儲存。

Keras儲存和載入模型

儲存的模型包括:

  • 模型架構 / 配置
  • 模型權重值
  • 模型的編譯資訊(如果 compile() 被呼叫)
  • 優化器及其狀態(如果有)(這使您可以在離開的地方重新開始訓練)
# 儲存模型
model.save('path/to/location')  

del model  #刪除現有模型

# 載入模型
model = models.load_model('path/to/location')

我們可以使用兩種格式將整個模型儲存到磁碟:

  • TensorFlow SavedModel 格式(推薦):它儲存了模型架構、權重和呼叫函式的跟蹤 Tensorflow 子圖。這使 Keras 能夠恢復內建層和自定義物件。
  •  Keras H5 格式(較舊的):包含模型架構、權重值和compile()資訊。它是SavedModel的輕量級替代品。
    • 通過model.add_loss()和model.add_metric()新增的外部損失和度量 不會被儲存
    • 自定義物件(如自定義層)的計算圖不包含在儲存的h5檔案中。在載入時,Keras需要訪問這些物件的Python類/函式來重構模型。

可以通過以下方式儲存 H5 格式:

  • 傳遞save_format='h5'給save()
  • 以.h5或.keras結尾的檔名傳遞給save()

 我們還可以只儲存模型結構

json_str = model.to_json()  # 儲存模型結構
model_json = models.model_from_json(json_str)  # 恢復模型結構

或者只儲存模型權重

# 儲存模型權重
model.save_weights(...)

# 恢復模型結構
model_json = models.model_from_json(json_str)
model_json.compile(...)

# 載入權重
model_json.load_weights(...)

TensorFlow原生方式儲存和載入

儲存模型結構與模型引數到檔案,該方式儲存的模型具有跨平臺性便於部署

model.save(..., save_format="tf")   # 儲存模型
model_loaded = tf.keras.models.load_model(...)  # 載入模型

也可以僅儲存權重

model.save_weights(...,save_format = "tf")

彙總

Tensorflow 2.x入門教程
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
import time

# 超引數
lr = 1e-3   # 學習率
batch_size = 64
epochs = 2

# 資料準備
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train = np.reshape(x_train, (-1, 784))
x_test = np.reshape(x_test, (-1, 784))

# 保留1萬個樣品用於驗證
x_val = x_train[-10000:]
y_val = y_train[-10000:]
x_train = x_train[:-10000]
y_train = y_train[:-10000]

# 準備訓練資料集
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(batch_size)

# 準備驗證資料集
val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_dataset = val_dataset.batch(batch_size)

# 搭建模型
inputs = keras.Input(shape=(784,), name="digits")
x1 = layers.Dense(64, activation="relu")(inputs)
x2 = layers.Dense(64, activation="relu")(x1)
outputs = layers.Dense(10, name="predictions")(x2)
model = keras.Model(inputs=inputs, outputs=outputs)

optimizer = keras.optimizers.SGD(learning_rate=lr)    # 例項化一個優化器
loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)  # 例項化損失函式

train_metric = keras.metrics.BinaryAccuracy(name='train_accuracy')
valid_metric = keras.metrics.BinaryAccuracy(name='valid_accuracy')

@tf.function
def train_step(x, y):
    with tf.GradientTape() as tape:
        logits = model(x, training=True)
        loss_value = loss_fn(y, logits)
        # loss_value += sum(model.losses)   # 新增額外的損失
    grads = tape.gradient(loss_value, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))
    train_metric.update_state(y, logits)
    return loss_value

@tf.function
def test_step(x, y):
    val_logits = model(x, training=False)
    valid_metric.update_state(y, val_logits)


for epoch in range(epochs):
    start_time = time.time()
    for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
        loss_value = train_step(x_batch_train, y_batch_train)

        if step % 200 == 0:
            print("訓練損失( %d: %.4f" % (step, float(loss_value)))

    train_acc = train_metric.result()
    print("訓練精度: %.4f" % (float(train_acc),))
    train_metric.reset_states()  # 在每個epoch結束時重置訓練指標

    # 在每個epoch結束時執行驗證迴圈
    for x_batch_val, y_batch_val in val_dataset:
        test_step(x_batch_val, y_batch_val)

    val_acc = valid_metric.result()
    valid_metric.reset_states()
    print("驗證精度: %.4f" % (float(val_acc),))
    print("執行時間: %.2fs" % (time.time() - start_time))
View Code

GPU訓練

指定GPU訓練

深度學習的訓練過程常常非常耗時,一個模型訓練幾個小時是家常便飯,訓練幾天也是常有的事情,有時候甚至要訓練幾十天。

訓練過程的耗時主要來自於兩個部分,一部分來自資料準備,另一部分來自引數更新。

  資料準備過程可以使用更多程式處理資料來縮減時間。引數更新時間可以應用GPU或者Google的TPU來進行加速。

  當存在可用的GPU時,如果不特意指定device,tensorflow會自動優先選擇使用GPU來建立張量和執行張量計算。但如果是在公司或者學校實驗室的伺服器環境,存在多個GPU和多個使用者時,為了不讓單個同學的任務佔用全部GPU資源導致其他同學無法使用(tensorflow預設獲取全部GPU的全部記憶體資源許可權,但實際上只使用一個GPU的部分資源),我們通常會在開頭增加以下幾行程式碼以控制每個任務使用的GPU編號和視訊記憶體大小,以便其他同學也能夠同時訓練模型。

gpus = tf.config.list_physical_devices("GPU")
tf.print(gpus)
# [PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU'),
#  PhysicalDevice(name='/physical_device:GPU:1', device_type='GPU'),
#  PhysicalDevice(name='/physical_device:GPU:2', device_type='GPU'),
#  PhysicalDevice(name='/physical_device:GPU:3', device_type='GPU')]

if gpus:
    gpu0 = gpus[0]  # 如果有多個GPU,僅使用第0個GPU
    tf.config.experimental.set_memory_growth(gpu0, True)  # 設定GPU視訊記憶體用量按需使用
    # 或者也可以設定GPU視訊記憶體為固定使用量(例如:4G)
    # tf.config.experimental.set_virtual_device_configuration(gpu0,
    #    [tf.config.experimental.VirtualDeviceConfiguration(memory_limit=4096)])
    tf.config.set_visible_devices([gpu0], "GPU")

單機多卡訓練

  TensorFlow 在 tf.distribute.MirroredStrategy  中為我們提供了單機多卡訓練策略,使用這種策略時,我們只需例項化一個 MirroredStrategy 策略,並將模型構建的程式碼放入 strategy.scope() 的上下文環境中:

strategy = tf.distribute.MirroredStrategy()

with strategy.scope():
    # 模型構建程式碼

可以在引數中指定裝置,如:

# 指定只使用第 0、1 號 GPU 參與分散式策略。
strategy = tf.distribute.MirroredStrategy(devices=["/gpu:0", "/gpu:1"])

MirroredStrategy 的步驟如下:

  • 訓練開始前,該策略在所有 N 個計算裝置上均各複製一份完整的模型;
  • 每次訓練傳入一個batch的資料時,將資料分成 N 份,分別傳入 N 個計算裝置(即資料並行);
  • N 個計算裝置使用本地變數(映象變數)分別計算自己所獲得的部分資料的梯度;
  • 使用分散式計算的 All-reduce 操作,在計算裝置間高效交換梯度資料並進行求和,使得最終每個裝置都有了所有裝置的梯度之和;
  • 使用梯度求和的結果更新本地變數(映象變數);
  • 當所有裝置均更新本地變數後,進行下一輪訓練(即該並行策略是同步的)。

預設情況下,TensorFlow 中的 MirroredStrategy 策略使用 NVIDIA NCCL 進行 All-reduce 操作。

tf.distribute.Strategy和model.fit

tf.distribute.Strategy被整合到tf.keras,tf.keras是用於構建和訓練模型的高階 API。您可以使用 Model.fit 來無縫分散式訓練模型

TensorFlow 分佈策略支援所有型別的 Keras 模型——Sequential、Functional和subclassed。以下是您需要在程式碼中更改的內容:

  • 建立例項 tf.distribute.Strategy。
  • 在strategy.scope中建立 Keras 模型、優化器和度量
num_epochs = 5
batch_size_per_replica = 64 # 每個顯示卡上的batch數
learning_rate = 0.001

strategy = tf.distribute.MirroredStrategy()
print('Number of devices: %d' % strategy.num_replicas_in_sync)  # 輸出裝置數量
batch_size = batch_size_per_replica * strategy.num_replicas_in_sync     # 總batch_size

dataset = ...

with strategy.scope():
    model = tf.keras.applications.MobileNetV2(weights=None, classes=2)
    model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
        loss=tf.keras.losses.sparse_categorical_crossentropy,
        metrics=[tf.keras.metrics.sparse_categorical_accuracy]
    )

model.fit(dataset, epochs=num_epochs)

分散式訓練彙總

Tensorflow 2.x入門教程
import os
import tensorflow as tf
import tensorflow_datasets as tfds

strategy = tf.distribute.MirroredStrategy()
print('裝置數量: {}'.format(strategy.num_replicas_in_sync))

epochs = 12
batch_size_per_replica = 64
batch_size = batch_size_per_replica * strategy.num_replicas_in_sync


# 資料集
def scale(image, label):
    image = tf.cast(image, tf.float32)
    image /= 255

    return image, label


datasets, info = tfds.load(name='mnist', with_info=True, as_supervised=True)
mnist_train, mnist_test = datasets['train'], datasets['test']
train_dataset = mnist_train.map(scale).cache().shuffle(10000).batch(batch_size)
eval_dataset = mnist_test.map(scale).batch(batch_size)

with strategy.scope():
    model = tf.keras.Sequential([
        tf.keras.layers.Conv2D(32, 3, activation='relu', input_shape=(28, 28, 1)),
        tf.keras.layers.MaxPooling2D(),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(64, activation='relu'),
        tf.keras.layers.Dense(10)
    ])

    model.compile(loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
                  optimizer=tf.keras.optimizers.Adam(),
                  metrics=['accuracy'])

checkpoint_dir = './training_checkpoints'  # 定義用於儲存檢查點的檢查點目錄
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt_{epoch}")  # 定義檢查點檔案的名稱


# 定義一個函式來衰減學習速率。
def decay(epoch):
    if epoch < 3:
        return 1e-3
    elif epoch >= 3 and epoch < 7:
        return 1e-4
    else:
        return 1e-5


# 定義一個回撥函式,用於在每個epoch的末尾列印學習速率
class PrintLR(tf.keras.callbacks.Callback):
    def on_epoch_end(self, epoch, logs=None):
        print('\nepoch的 {} 學習率是 {}'.format(epoch + 1, model.optimizer.lr.numpy()))


# 把所有的回撥放在一起
callbacks = [tf.keras.callbacks.TensorBoard(log_dir='./logs'),
             tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_prefix,
                                                save_weights_only=True),
             tf.keras.callbacks.LearningRateScheduler(decay),
             PrintLR()]

# 訓練和評估
model.fit(train_dataset, epochs=epochs, callbacks=callbacks)
View Code

 

儲存為SavedModel格式模型,您可以使用或不使用Strategy.scope.

Tensorflow 2.x入門教程
path = 'saved_model/'
model.save(path, save_format='tf')

# 載入模型,不Strategy.scope
unreplicated_model = tf.keras.models.load_model(path)
unreplicated_model.compile(
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    optimizer=tf.keras.optimizers.Adam(),
    metrics=['accuracy'])

eval_loss, eval_acc = unreplicated_model.evaluate(eval_dataset)
print('Eval loss: {}, Eval Accuracy: {}'.format(eval_loss, eval_acc))



# 載入模型Strategy.scope
with strategy.scope():
    replicated_model = tf.keras.models.load_model(path)
    replicated_model.compile(loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
                             optimizer=tf.keras.optimizers.Adam(),
                             metrics=['accuracy'])

    eval_loss, eval_acc = replicated_model.evaluate(eval_dataset)
    print('Eval loss: {}, Eval Accuracy: {}'.format(eval_loss, eval_acc))
View Code

tf.distribute.Strategy和自定義訓練迴圈

1、在mirrored_strategy.scope() 內建立模型和優化器

with mirrored_strategy.scope():
  model = tf.keras.Sequential([tf.keras.layers.Dense(1, input_shape=(1,))])
  optimizer = tf.keras.optimizers.SGD()

2、呼叫 tf.distribute.Strategy.experimental_distribute_dataset 建立分散式資料集

dataset = tf.data.Dataset.from_tensors(([1.], [1.])).repeat(100).batch(global_batch_size)
dist_dataset = mirrored_strategy.experimental_distribute_dataset(dataset)

3、使用 tf.nn.compute_average_loss 計算損失。tf.nn.compute_average_loss對每個樣本損失求和並將總和除以global_batch_size。

def compute_loss(labels, predictions):
    per_example_loss = loss_object(labels, predictions)
    return tf.nn.compute_average_loss(per_example_loss, global_batch_size=global_batch_size)

4、將train_step放入 tf.distribute.Strategy.run 中,並傳入之前建立的資料集

5、使用 tf.distribute.Strategy.reduce 來聚合 tf.distribute.Strategy.run。tf.distribute.Strategy.run返回每個GPU結果。您還可以tf.distribute.Strategy.experimental_local_results獲取結果值列表。

def train_step(inputs):
    features, labels = inputs

    with tf.GradientTape() as tape:
        predictions = model(features, training=True)
        loss = compute_loss(labels, predictions)

    gradients = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(gradients, model.trainable_variables))
    return loss


@tf.function
def distributed_train_step(dist_inputs):
    per_replica_losses = mirrored_strategy.run(train_step, args=(dist_inputs,))
    return mirrored_strategy.reduce(tf.distribute.ReduceOp.SUM, per_replica_losses, axis=None)

6、迭代dist_dataset並迴圈執行訓練:

for dist_inputs in dist_dataset:
    print(distributed_train_step(dist_inputs))

分散式訓練彙總

Tensorflow 2.x入門教程
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
import os


strategy = tf.distribute.MirroredStrategy()     # 例項化MirroredStrategy
print('裝置數量: {}'.format(strategy.num_replicas_in_sync))

epochs = 10
batch_size_per_replica = 64  # 每個GPU上得batch數
global_batch_size = batch_size_per_replica * strategy.num_replicas_in_sync  # 總batch數

# 資料集
fashion_mnist = tf.keras.datasets.fashion_mnist
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
train_images = train_images[..., None]
test_images = test_images[..., None]

# 獲取[0,1]範圍內的影像
train_images = train_images / np.float32(255)
test_images = test_images / np.float32(255)

buffer_size = len(train_images)

train_dataset = tf.data.Dataset.from_tensor_slices((train_images, train_labels)).shuffle(buffer_size).batch(
    global_batch_size)
test_dataset = tf.data.Dataset.from_tensor_slices((test_images, test_labels)).batch(global_batch_size)

train_dist_dataset = strategy.experimental_distribute_dataset(train_dataset)    # 分散式資料集
test_dist_dataset = strategy.experimental_distribute_dataset(test_dataset)      # 分散式資料集

# 建立模型
def create_model():
    model = tf.keras.Sequential([
        tf.keras.layers.Conv2D(32, 3, activation='relu'),
        tf.keras.layers.MaxPooling2D(),
        tf.keras.layers.Conv2D(64, 3, activation='relu'),
        tf.keras.layers.MaxPooling2D(),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(64, activation='relu'),
        tf.keras.layers.Dense(10)
    ])

    return model


# 建立一個檢查點目錄來儲存檢查點
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")


with strategy.scope():
    # 建立訓練損失
    # 將reduction設定為“none”,這樣我們可以在之後進行reduction,併除以全域性batch size
    loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
        from_logits=True,
        reduction=tf.keras.losses.Reduction.NONE)

    def compute_loss(labels, predictions):
        per_example_loss = loss_object(labels, predictions)
        return tf.nn.compute_average_loss(per_example_loss, global_batch_size=global_batch_size)

    test_loss = tf.keras.metrics.Mean(name='test_loss')     # 建立測試損失
    train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')  # 訓練精度
    test_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='test_accuracy')  # 測試精度

    # 模型、優化器和檢查點必須建立在 'strategy.scope' 下。
    model = create_model()
    optimizer = tf.keras.optimizers.Adam()
    checkpoint = tf.train.Checkpoint(optimizer=optimizer, model=model)


# 訓練step
def train_step(inputs):
    images, labels = inputs

    with tf.GradientTape() as tape:
        predictions = model(images, training=True)
        loss = compute_loss(labels, predictions)

    gradients = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(gradients, model.trainable_variables))

    train_accuracy.update_state(labels, predictions)
    return loss


# 測試step
def test_step(inputs):
    images, labels = inputs

    predictions = model(images, training=False)
    t_loss = loss_object(labels, predictions)

    test_loss.update_state(t_loss)
    test_accuracy.update_state(labels, predictions)


# 分散式訓練
@tf.function
def distributed_train_step(dataset_inputs):
    per_replica_losses = strategy.run(train_step, args=(dataset_inputs,))
    return strategy.reduce(tf.distribute.ReduceOp.SUM, per_replica_losses, axis=None)


# 分散式測試
@tf.function
def distributed_test_step(dataset_inputs):
    return strategy.run(test_step, args=(dataset_inputs,))


for epoch in range(epochs):
    # 訓練迴圈
    total_loss = 0.0
    num_batches = 0
    for x in train_dist_dataset:
        total_loss += distributed_train_step(x)
        num_batches += 1
    train_loss = total_loss / num_batches

    # 測試迴圈
    for x in test_dist_dataset:
        distributed_test_step(x)

    if epoch % 10 == 0:
        checkpoint.save(checkpoint_prefix)

    print("Epoch {}, Loss: {}, Accuracy: {}, Test Loss: {}, Test Accuracy: {}".format(epoch + 1, train_loss,
                                                                                      train_accuracy.result() * 100,
                                                                                      test_loss.result(),
                                                                                      test_accuracy.result() * 100))

    test_loss.reset_states()
    train_accuracy.reset_states()
    test_accuracy.reset_states()
View Code

tf.distribute.Strategy 可以再沒有strategy的情況下恢復最新的檢查點並測試

Tensorflow 2.x入門教程
test_dataset = tf.data.Dataset.from_tensor_slices((test_images, test_labels)).batch(global_batch_size)
eval_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='eval_accuracy')

new_model = create_model()
new_optimizer = tf.keras.optimizers.Adam()


@tf.function
def eval_step(images, labels):
    predictions = new_model(images, training=False)
    eval_accuracy(labels, predictions)


checkpoint = tf.train.Checkpoint(optimizer=new_optimizer, model=new_model)
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))

for images, labels in test_dataset:
    eval_step(images, labels)

print('在沒有strategy的情況下恢復儲存的模型後的準確性: {}'.format(eval_accuracy.result() * 100))
View Code

模型部署

tensorflow-serving

TensorFlow Lite

  TensorFlow Lite 是 TensorFlow 在移動和 IoT 等邊緣裝置端的解決方案,提供了 Java、Python 和 C++ API 庫,可以執行在 Android、iOS 和 Raspberry Pi 等裝置上。AI技術在邊緣裝置上的應用,TFLite 將會是愈發重要的角色。

  目前 TFLite 只提供了推理功能,在伺服器端進行訓練後,經過如下簡單處理即可部署到邊緣裝置上。

  • 模型轉換:由於邊緣裝置計算等資源有限,使用 TensorFlow 訓練好的模型,模型太大、執行效率比較低,不能直接在移動端部署,需要通過相應工具進行轉換成適合邊緣裝置的格式。
  • 邊緣裝置部署:本節以 android 為例,簡單介紹如何在 android 應用中部署轉化後的模型,完成 Mnist 圖片的識別。

參考

【電子書】簡單粗暴 TensorFlow 2

【知乎】最全Tensorflow2.0 入門教程持續更新

【和鯨社群】30天吃掉那隻TensorFlow2.0 | Github

【書籍】TensorFlow 2深度學習開源書 | PDF下載 提取碼:juqs

【bilibili】tensorflow2.0入門與實戰 2019年最通俗易懂的課程

【bilibili】神經網路與深度學習——TensorFlow2.0實戰【中文課程】

【github】TensorFlow-Examples 

【github】TensorFlow-2.x-Tutorials