我們是否使用了正確的工具?哪種框架最適合我的用例?這種方法可擴展嗎?我們是否考慮到可擴展性?如果你像我一樣是一個普通人,那麼可能已經體驗到有時會陷入應用程式開發的困境,以至於很難停下來,去思考一下我們是否採取了有效的方式。
在AI領域尤其如此。眾所周知,人工智慧是一個快速發展的領域。當天發表了新的研究。正在迅速開發的主要AI框架之間存在著巨大的鬥爭。發布了新的硬體體系結構,晶片和優化,以支持不斷增長的AI應用的部署。
什麼時候是停下來重新考慮的好時機?那只有你會知道。對我來說,這一刻已經到來了。我一直在使用Keras和Tensorflow的1.x(TF1)在工作和我的個人項目,因為我在這個領域開始。我完全喜歡Keras庫的高級方法和Tensorlfow的低級方法,這些方法可以讓我們在需要更多自定義時在後臺進行更改。
儘管我一直是Keras-Tensorflow兼容的忠實擁護者,但始終有一個非常具體的缺點:調試功能。如你所知,在Tensorflow中,存在這樣的範例:首先定義計算圖,然後進行編譯(或將其移至GPU),然後運行它。這種範例非常好,從技術上來講很有意義,但是,一旦在GPU中擁有了模型,幾乎就不可能對其進行調試。
這就是為什麼,自從TensorFlow 2.0以其Alpha版本發布以來已經過去了大約一年,我決定在TensorFlow 2.1與大家分享使用的體驗。
TensorFLow 2.1
事實上,我很難弄清楚應該如何使用這個新的TensorFlow版本,即著名的2.1穩定版本。TensorFlow 2編程與TensorFlow 1的不同之處在於面向對象編程與功能性編程的區別。
經過一些實驗後,我發現在TensorFlow 2.1中,有3種構建模型的方法:
Keras模式(tf.keras):基於圖形定義,並在以後運行圖形。渴望模式:基於定義執行的所有迭代定義圖的操作。圖形模式(tf.function):之前兩種方法的混合。讓我們來看看代碼。
Keras模式
import numpy as npimport tensorflow as tffrom tensorflow import kerasfrom tensorflow.keras.layers import Input, Dense, Flatten, Conv2Dfrom tensorflow.keras import Modelfrom tensorflow.keras.optimizers import Adamdef loss(y_true, y_pred): """Loss function""" return tf.square(y_true - y_pred)if __name__ == '__main__': # Learn to sum 20 nums, train and test datasets train_samples = tf.random.normal(shape=(10000, 20)) train_targets = tf.reduce_sum(train_samples, axis=-1) test_samples = tf.random.normal(shape=(100, 20)) test_targets = tf.reduce_sum(test_samples, axis=-1) # Model building with Keras functional API x = Input(shape=[20]) h = Dense(units=20, activation='relu')(x) h = Dense(units=10, activation='relu')(h) y = Dense(units=1)(h) model = Model(x,y) # Compiling model model.compile( optimizer=Adam(learning_rate=0.001), loss=loss, metrics=['mse']) # Training model.fit( x=train_samples, y=train_targets, batch_size=1, epochs=10, validation_data=(test_samples, test_targets), shuffle=True)
這是我們所有人都習慣的標準用法。僅使用具有自定義損失功能且具有平方誤差損失的普通Keras。該網絡是3個密集層的深層網絡。
# The networkx = Input(shape=[20])h = Dense(units=20, activation='relu')(x)h = Dense(units=10, activation='relu')(h)y = Dense(units=1)(h)
這裡的目的是教一個網絡,學習如何對20個元素的向量求和。因此,我們用的數據集為網絡提供數據[10000 x 20],因此10000個樣本每個都有20個特徵(要求和的元素)。
# Training samplestrain_samples = tf.random.normal(shape=(10000, 20))train_targets = tf.reduce_sum(train_samples, axis=-1)test_samples = tf.random.normal(shape=(100, 20))test_targets = tf.reduce_sum(test_samples, axis=-1)
我們可以運行該示例,然後得到通常看起來不錯的Keras輸出:
Epoch 1/1010000/10000 [==============================] - 10s 1ms/sample - loss: 1.6754 - val_loss: 0.0481Epoch 2/1010000/10000 [==============================] - 10s 981us/sample - loss: 0.0227 - val_loss: 0.0116Epoch 3/1010000/10000 [==============================] - 10s 971us/sample - loss: 0.0101 - val_loss: 0.0070
以上是一個Keras玩具示例的訓練時間為每秒鐘10秒
看完這個過程後,可能會想知道一個問題:TensorFlow 2版本承諾將在所有功能上實現哪些功能?如果與Keras軟體包的集成意味著不必安裝其他軟體包,那麼優點是什麼?
急切模式
在這種模式下,所有張量操作都是交互的,你可以設置一個斷點並訪問任何中間張量變量。但是,這種靈活性要付出代價:需要更明確的代碼。讓我們來看看:
import numpy as npimport tensorflow as tffrom tqdm import tqdmfrom tensorflow import kerasfrom tensorflow.keras.layers import Input, Dense, Flatten, Conv2Dfrom tensorflow.keras import Modelfrom tensorflow.keras.optimizers import Adamdef loss_compute(y_true, y_pred): return tf.square(y_true - y_pred)if __name__ == '__main__': # Learn to sum 20 nums train_samples = tf.random.normal(shape=(10000, 20)) train_targets = tf.reduce_sum(train_samples, axis=-1) test_samples = tf.random.normal(shape=(100, 20)) test_targets = tf.reduce_sum(test_samples, axis=-1) # Model building: Keras functional API x = Input(shape=[20]) h = Dense(units=20, activation='relu')(x) h = Dense(units=10, activation='relu')(h) y = Dense(units=1)(h) model = Model(x,y) # Training loop epochs = 10 optimizer = tf.keras.optimizers.Adam(learning_rate=0.001) for epoch in range(epochs): # Fancy progress bar pbar = tqdm(range(len(train_samples))) # Metrics loss_metric = keras.metrics.Mean() # Batches iteration, batch_size = 1 for batch_id in pbar: # Getting sample target pair sample = train_samples[batch_id] target = train_targets[batch_id] # Adding batch dim since batch=1 sample = tf.expand_dims(sample, axis=0) target = tf.expand_dims(target, axis=0) # Forward pass: needs to be recorded by gradient tape with tf.GradientTape() as tape: target_pred = model(sample) loss = loss_compute(target, target_pred) # Backward pass: # compute gradients w.r.t. the loss # update trainable weights of the model gradients = tape.gradient(loss, model.trainable_weights) optimizer.apply_gradients(zip(gradients, model.trainable_weights)) # Tracking progress loss_metric(loss) pbar.set_description('Training Loss: %.3f' % loss_metric.result().numpy()) # At the end of the epoch test the model test_targets_pred = model(test_samples) test_loss = loss_compute(test_targets, test_targets_pred) test_loss_avg = tf.reduce_mean(test_loss) print('Validation Loss: %.3f' % test_loss_avg)
閱讀代碼後,第一件事可能是:很多代碼僅用於執行a model.compile和a model.fit。但另一方面,可以控制之前所有發生的事情。
所以現在情況變了。通過這種方法,可以從頭開始設計事物的工作方式。現在可以指定以下內容:
指標:按樣品,批次或任何其他自定義統計來測量結果。根據需要使用穩定的舊移動平均線或任何其他自定義指標。損失函數:可以在損失函數定義中獲得所有想要的技巧,而Keras不會因為其_standarize_user_data(link)報錯。漸變:可以訪問漸變,並定義前進和後退通道的細節。指標是使用新tf.keras.metrics API指定的。只需採用所需的指標,對其進行定義並按以下方式使用它:
# Getting metric instancedmetric = tf.keras.metrics.Mean() # Run your model to get the loss and update the metricloss = [...]metric(loss)# Print the metric print('Training Loss: %.3f' % metric.result().numpy())
損耗函數和梯度分別在前向和後向傳遞中計算。在這種方法中,前向通行證必須由記錄tf.GradientTape。在tf.GradientTape將跟蹤(或磁帶)的直傳這樣做可以在計算後向通行的梯度所有的張量操作。換句話說:為了向後運行,必須記住要前進的道路。
# Forward pass: needs to be recorded by gradient tapewith tf.GradientTape() as tape: y_pred = model(x) loss = loss_compute(y_true, y_pred)# Backward pass:gradients = tape.gradient(loss, model.trainable_weights)optimizer.apply_gradients(zip(gradients, model.trainable_weights))
這非常簡單,在前向傳遞中,可以運行預測,並通過計算損失來查看預測的效果。在後向過程中,可以通過計算梯度來檢查權重如何影響該損失,然後嘗試通過更新權重(在優化程序的幫助下)來最小化損失。還可以在代碼中注意到,在每個時期結束時,都會計算驗證損失(通過僅運行前向傳遞而不更新權重)來看看結果:
Epoch 1:Loss: 1.310: 100%|███████████| 10000/10000 [00:41<00:00, 239.70it/s]Epoch 2:Loss: 0.018: 100%|███████████| 10000/10000 [00:41<00:00, 240.21it/s]Epoch 3:Loss: 0.010: 100%|███████████| 10000/10000 [00:41<00:00, 239.28it/s]
在同一臺機器上,每個紀元花費41s,即4倍的時間增量……而這只是一個虛擬模型。能想像如何將其擴展到RetinaNet,YOLO或MaskRCNN等實際用例模型嗎?TensorFlow的好夥伴意識到了這一點,並實現了圖形模式。
圖形模式
圖形模式(來自AutoGraph或tf.function)是前兩種之間的混合模式。可以了解此處和此處的內容。但是發現這些指南有些混亂,所以用自己的話來解釋它。
如果Keras模式是要定義圖形並稍後在GPU中運行,而eager模式是要以交互方式執行每個步驟,則graph模式可以讓我們像在eager模式下一樣進行編碼,但是運行訓練的速度幾乎與處於Keras模式(是的,在GPU中)。
關於eager模式的唯一變化是在圖形模式下,將代碼分解為小函數,並使用注釋了這些函數@tf.function。讓我們看一下情況如何變化:
import numpy as npimport tensorflow as tffrom tqdm import tqdmfrom tensorflow import kerasfrom tensorflow.keras.layers import Input, Dense, Flatten, Conv2Dfrom tensorflow.keras import Modelfrom tensorflow.keras.optimizers import Adam@tf.functiondef loss_compute(y_true, y_pred): """Loss function""" return tf.square(y_true - y_pred)@tf.functiondef forward_pass(model, sample, target): # Needs to be recorded by gradient tape with tf.GradientTape() as tape: target_pred = model(sample) loss = loss_compute(target, target_pred) # compute gradients w.r.t. the loss # (I know this belongs to backward pass but...) gradients = tape.gradient(loss, model.trainable_weights) return loss, gradients@tf.functiondef backward_pass(model, gradients, optimizer): optimizer.apply_gradients(zip(gradients, model.trainable_weights))if __name__ == '__main__': # Learn to sum 20 nums train_samples = tf.random.normal(shape=(10000, 20)) train_targets = tf.reduce_sum(train_samples, axis=-1) test_samples = tf.random.normal(shape=(100, 20)) test_targets = tf.reduce_sum(test_samples, axis=-1) # Model Functional API x = Input(shape=[20]) h = Dense(units=20, activation='relu')(x) h = Dense(units=10, activation='relu')(h) y = Dense(units=1)(h) model = Model(x,y) # Training loop epochs = 10 optimizer = tf.keras.optimizers.Adam(learning_rate=0.001) for epoch in range(epochs): # Fancy progress bar pbar = tqdm(range(len(train_samples))) # Metrics loss_metric = keras.metrics.Mean() # Batches iteration, batch_size = 1 for batch_id in pbar: # Getting sample target pair sample = train_samples[batch_id] target = train_targets[batch_id] # Adding batch dim since batch=1 sample = tf.expand_dims(sample, axis=0) target = tf.expand_dims(target, axis=0) # This is computed in graph mode # Computing loss and gradients w.r.t the loss loss, gradients = forward_pass(model, sample, target) # Updaing model weights backward_pass(model, gradients, optimizer) # Tracking progress loss_metric(loss) pbar.set_description('Training Loss: %.3f' % loss_metric.result().numpy()) # At the end of the epoch test the model test_targets_pred = model(test_samples) test_loss = loss_compute(test_targets, test_targets_pred) test_loss_avg = tf.reduce_mean(test_loss) print('Validation Loss: %.3f' % test_loss_avg)
現在,將了解前向和後向傳遞計算如何重構為@tf.function裝飾器已注釋的2個函數。
那麼,這裡到底發生了什麼?簡單。每當你用@tf.function裝飾器注釋函數時,都將與Keras一樣將這些操作「編譯」到GPU中。因此,通過注釋函數,可以告訴TensorFlow在GPU中的優化圖形中運行這些操作。
在幕後,真正發生的是該函數正在由AutoGraph解析tf.autograph。AutoGraph將獲取函數的輸入和輸出,並從中生成一個TensorFlow圖,這意味著它將解析操作以將輸入的輸出轉換為TensorFlow圖。生成的圖形將非常高效地運行到GPU中。
這就是為什麼它是一種混合模式的原因,因為除了用@tf.function裝飾器注釋的操作之外,所有操作都以交互方式運行。
這也意味著除了用修飾的函數中的變量和張量之外,將可以訪問所有變量和張量@tf.function,而其中的變量和張量只能訪問其輸入和輸出。這種方法建立了一種非常清晰的調試方式,可以在這種方式下以渴望的模式開始進行交互式開發,然後在模型準備就緒時,使用使其推向生產性能@tf.function。讓我們看看它的效果如何:
Epoch 1:Loss: 1.438: 100%|████████████| 10000/10000 [00:16<00:00, 612.3it/s]Epoch 2:Loss: 0.015: 100%|████████████| 10000/10000 [00:16<00:00, 615.0it/s]Epoch 3:Loss: 0.009: 72%|████████████| 7219/10000 [00:11<00:04, 635.1it/s]
驚人的16s/epoch。你可能會認為它不像Keras模式那樣快,但是,另一方面,你可以獲得所有調試功能和非常接近的性能。
在我看來,TensorFlow團隊在為開發人員提供更多靈活性而又不犧牲效率的前提下做出了出色的工作。