前言
至於為什麼寫這個教程,首先是為了自己學習做個記錄,其次是因為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.concat和tf.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機制能夠轉換的程式碼並不是沒有任何約束的,有一些編碼規範需要遵循,否則可能會轉換失敗或者不符合預期。
- 被@tf.function修飾的函式應儘可能使用TensorFlow中的函式而不是Python中的其他函式。例如使用tf.print而不是print,使用tf.range而不是range,使用tf.constant(True)而不是True.
- 避免在@tf.function修飾的函式內部定義tf.Variable
- 被@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)
您可以像這樣測量靜態圖和動態圖效能差異:
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
我們還可以再函式前使用裝飾器 @tf.function 呼叫tf.function,同時也可以使用 tf.config.run_functions_eagerly(True) 關閉Function建立和執行圖形的能力。
前面在介紹Autograph的編碼規範時提到構建Autograph時應該避免在@tf.function修飾的函式內部定義tf.Variable。但是如果在函式外部定義tf.Variable的話,又會顯得這個函式有外部變數依賴,封裝不夠完美。一種簡單的思路是定義一個類,並將相關的tf.Variable建立放在類的初始化方法中。而將函式的邏輯放在其他方法中。
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
自動微分
自動微分用於訓練神經網路的反向傳播非常有用,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
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)
可以求二階導數
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)
利用梯度和優化器求最小值
# 求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)
如果不想被計算梯度:
with tf.GradientTape(watch_accessed_variables=False) as tape: pass
使用TensorFlow實現神經網路模型的一般流程包括:
- 準備資料
- 定義模型
- 訓練模型
- 評估模型
- 推理模型
- 儲存模型
資料輸入
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
注意:如果你打算多次呼叫,你可以使用迭代器的方式:
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)
提升管道效能
訓練深度學習模型常常會非常耗時。模型訓練的耗時主要來自於兩個部分,一部分來自資料準備,另一部分來自引數迭代。引數迭代過程的耗時通常依賴於GPU來提升。而資料準備過程的耗時則可以通過構建高效的資料管道進行提升。
以下是一些構建高效資料管道的建議。
- 使用 prefetch 方法讓資料準備和引數迭代兩個過程相互並行。
- 使用 interleave 方法可以讓資料讀取過程多程式執行,並將不同來源資料夾在一起。
- 使用 map 時設定num_parallel_calls 讓資料轉換過程多進行執行。
- 使用 cache 方法讓資料在第一個epoch後快取到記憶體中,僅限於資料集不大情形。
- 使用 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種方式構建模型:
- Sequential順序模型:用於簡單的層堆疊, 其中每一層恰好有一個輸入張量和一個輸出張量
- 函式式API模型:多輸入多輸出,或者模型需要共享權重,或者模型具有殘差連線等非順序結構,
- 繼承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.Model > tf.keras.layers.Layer > tf.Module。
通常使用Layer類來定義內部計算塊,並使用Model類定義外部模型(訓練的物件)。
繼承tf.Module栗子:
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")
繼承tf.keras.layers.Layer栗子:
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))
繼承tf.Module栗子:
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"])
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 類用在一起,吃個栗子:
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
自定義迴圈訓練模型
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()))
內建迴圈方法
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)
函式式API和自定義Model類混搭訓練
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訓練模型
補充知識:自定義層
如果自定義模型層沒有需要被訓練的引數,一般推薦使用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資料一次計算的結果。
@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
類自定義度量
由於訓練的過程通常是分批次訓練的,而評估指標要跑完一個epoch才能夠得到整體的指標結果。因此,類形式的評估指標更為常見。即需要編寫update_state方法在每個batch後更新相關中間變數的狀態,編寫result方法輸出最終指標結果。
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())
優化器
機器學習界有一群煉丹師,他們每天的日常是:拿來藥材(資料),架起八卦爐(模型),點著六味真火(優化演算法),就搖著蒲扇等著丹藥出爐了。不過,當過廚子的都知道,同樣的食材,同樣的菜譜,但火候不一樣了,這出來的口味可是千差萬別。火小了夾生,火大了易糊,火不勻則半生半糊。機器學習也是一樣,模型優化演算法的選擇直接關係到最終模型的效能。有時候效果不好,未必是特徵的問題或者模型設計的問題,很可能就是優化演算法的問題。
深度學習優化演算法大概經歷了 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:可幫助您視覺化模型。
- Distributions 和 Histograms 顯示張量隨時間的分佈。 可以 視覺化權重和偏差並驗證它們是否以預期的方式變化
在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")
彙總
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))
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)
分散式訓練彙總
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)
儲存為SavedModel格式模型,您可以使用或不使用Strategy.scope.
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))
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))
分散式訓練彙總
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()
tf.distribute.Strategy 可以再沒有strategy的情況下恢復最新的檢查點並測試
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))
模型部署
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
【和鯨社群】30天吃掉那隻TensorFlow2.0 | Github
【書籍】TensorFlow 2深度學習開源書 | PDF下載 提取碼:juqs
【bilibili】tensorflow2.0入門與實戰 2019年最通俗易懂的課程
【bilibili】神經網路與深度學習——TensorFlow2.0實戰【中文課程】
【github】TensorFlow-Examples
【github】TensorFlow-2.x-Tutorials