梯度下降在 TensorFlow 中的眾多應用
已發表: 2022-03-11Google 的 TensorFlow 是用於訓練和部署深度學習模型的領先工具之一。 它能夠優化具有數億個參數的極其複雜的神經網絡架構,並帶有用於硬件加速、分佈式訓練和生產工作流程的各種工具。 這些強大的功能可以使它在深度學習領域之外顯得令人生畏和不必要。
但是 TensorFlow 既可以訪問也可以用於解決與訓練深度學習模型沒有直接關係的簡單問題。 TensorFlow 的核心只是一個針對張量運算(向量、矩陣等)和用於對任意計算序列執行梯度下降的微積分運算的優化庫。 經驗豐富的數據科學家會認識到“梯度下降”是計算數學的基本工具,但它通常需要實現特定於應用程序的代碼和方程。 正如我們將看到的,這就是 TensorFlow 的現代“自動微分”架構的用武之地。
TensorFlow 用例
- 示例 1:TensorFlow 2.0 中具有梯度下降的線性回歸
- 什麼是梯度下降?
- 示例 2:最大擴展單位向量
- 示例 3:生成對抗性 AI 輸入
- 最後的想法:梯度下降優化
- TensorFlow 中的梯度下降:從尋找最小值到攻擊 AI 系統
示例 1:TensorFlow 2.0 中具有梯度下降的線性回歸
示例 1 筆記本
在了解 TensorFlow 代碼之前,熟悉梯度下降和線性回歸非常重要。
什麼是梯度下降?
用最簡單的術語來說,它是一種數值技術,用於找到方程組的輸入,以使其輸出最小化。 在機器學習的背景下,方程組就是我們的模型,輸入是模型的未知參數,輸出是要最小化的損失函數,它表示模型和我們的數據之間存在多少誤差。 對於某些問題(如線性回歸),有一些方程可以直接計算使我們的誤差最小化的參數,但對於大多數實際應用,我們需要梯度下降等數值技術來獲得令人滿意的解決方案。
本文最重要的一點是,梯度下降通常需要對我們的方程進行佈局,並使用微積分來推導我們的損失函數和參數之間的關係。 使用 TensorFlow(以及任何現代自動微分工具),微積分為我們處理,因此我們可以專注於設計解決方案,而不必花時間在其實施上。
這是一個簡單的線性回歸問題的樣子。 我們有 150 名成年男性的身高 (h) 和體重 (w) 樣本,並從對這條線的斜率和標準差的不完美猜測開始。 經過大約 15 次梯度下降迭代後,我們得到了一個接近最優的解決方案。
讓我們看看我們如何使用 TensorFlow 2.0 生成上述解決方案。
對於線性回歸,我們說可以通過高度的線性方程來預測權重。
我們想要找到最小化預測值和真實值之間的平均平方誤差(損失)的參數 α 和 β(斜率和截距)。 所以我們的損失函數(在本例中為“均方誤差”或 MSE)如下所示:
我們可以看到均方誤差如何尋找一對不完美的線,然後是精確解(α=6.04,β=-230.5)。
讓我們用 TensorFlow 將這個想法付諸實踐。 首先要做的是使用張量和tf.*
函數編寫損失函數。
def calc_mean_sq_error(heights, weights, slope, intercept): predicted_wgts = slope * heights + intercept errors = predicted_wgts - weights mse = tf.reduce_mean(errors**2) return mse
這看起來很簡單。 所有標準代數運算符都為張量重載,所以我們只需要確保我們正在優化的變量是張量,我們使用tf.*
方法來處理其他任何事情。
然後,我們所要做的就是將其放入梯度下降循環中:
def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): # Any values to be part of gradient calcs need to be vars/tensors tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') # Hardcoding 25 iterations of gradient descent for i in range(25): # Do all calculations under a "GradientTape" which tracks all gradients with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) # This is the same mean-squared-error calculation as before predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) # Auto-diff magic! Calcs gradients between loss calc and params dloss_dparams = tape.gradient(loss, [tf_slope, tf_icept]) # Gradients point towards +loss, so subtract to "descend" tf_slope = tf_slope - learning_rate * dloss_dparams[0] tf_icept = tf_icept - learning_rate * dloss_dparams[1]
讓我們花點時間欣賞一下這是多麼整潔。 梯度下降需要計算損失函數對我們試圖優化的所有變量的導數。 應該涉及微積分,但我們實際上並沒有做任何事情。 神奇之處在於:
- TensorFlow 為在
tf.GradientTape()
下完成的每個計算構建一個計算圖。 - TensorFlow 知道如何計算每個操作的導數(梯度),因此它可以確定計算圖中的任何變量如何影響任何其他變量。
從不同的起點看這個過程如何?
梯度下降非常接近最優 MSE,但實際上收斂到與兩個示例中的最優值截然不同的斜率和截距。 在某些情況下,這只是梯度下降收斂到局部最小值,這是梯度下降算法的固有挑戰。 但可證明線性回歸只有一個全局最小值。 那麼我們是如何以錯誤的斜率和截距結束的呢?
在這種情況下,問題在於我們為了演示而過度簡化了代碼。 我們沒有對我們的數據進行歸一化,並且斜率參數與截距參數具有不同的特徵。 斜率的微小變化會產生巨大的損失變化,而截距的微小變化幾乎沒有影響。 可訓練參數規模的巨大差異導致斜率主導梯度計算,截距參數幾乎被忽略。
因此梯度下降有效地找到了非常接近初始截距猜測的最佳斜率。 而且由於誤差非常接近最優值,它周圍的梯度很小,所以每次連續迭代只移動一點點。 首先標準化我們的數據會顯著改善這種現象,但它不會消除它。
這是一個相對簡單的示例,但我們將在接下來的部分中看到,這種“自動微分”功能可以處理一些非常複雜的東西。
示例 2:最大擴展單位向量
示例 2 筆記本
下一個示例基於我去年參加的深度學習課程中的一個有趣的深度學習練習。
問題的要點是我們有一個“變分自動編碼器”(VAE),它可以從一組 32 個正態分佈的數字中生成逼真的面孔。 對於嫌疑人識別,我們希望使用 VAE 生成一組多樣化的(理論)面孔供證人選擇,然後通過生成更多與所選面孔相似的面孔來縮小搜索範圍。 對於這個練習,建議隨機化初始向量集,但我想找到一個最佳初始狀態。
我們可以這樣表述這個問題:給定一個 32 維空間,找到一組 X 單位向量,它們最大程度地分開。 在二維中,這很容易精確計算。 但是對於三個維度(或 32 個維度!),沒有直接的答案。 但是,如果我們可以定義一個適當的損失函數,當我們達到目標狀態時,它的最小值,也許梯度下降可以幫助我們到達那裡。
我們將從如上所示的一組隨機的 20 個向量開始,並嘗試三種不同的損失函數,每個損失函數的複雜度都在增加,以展示 TensorFlow 的能力。
讓我們首先定義我們的訓練循環。 我們將把所有的 TensorFlow 邏輯放在self.calc_loss()
方法下,然後我們可以簡單地為每種技術覆蓋該方法,循環這個循環。
# Define the framework for trying different loss functions # Base class implements loop, sub classes override self.calc_loss() class VectorSpreadAlgorithm: # ... def calc_loss(self, tensor2d): raise NotImplementedError("Define this in your derived class") def one_iter(self, i, learning_rate): # self.vecs is an 20x2 tensor, representing twenty 2D vectors tfvecs = tf.convert_to_tensor(self.vecs, dtype=tf.float32) with tf.GradientTape() as tape: tape.watch(tfvecs) loss = self.calc_loss(tfvecs) # Here's the magic again. Derivative of spread with respect to # input vectors gradients = tape.gradient(loss, tfvecs) self.vecs = self.vecs - learning_rate * gradients
第一個嘗試的技術是最簡單的。 我們定義了一個擴展度量,它是最接近的向量的角度。 我們希望最大化傳播,但通常將其設為最小化問題。 所以我們簡單地取價差指標的負數:
class VectorSpread_Maximize_Min_Angle(VectorSpreadAlgorithm): def calc_loss(self, tensor2d): angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi spread_metric = tf.reduce_min(angle_pairs + disable_diag) # Convention is to return a quantity to be minimized, but we want # to maximize spread. So return negative spread return -spread_metric
一些 Matplotlib 魔法將產生可視化。
這很笨拙(從字面上看!)但它有效。 一次只更新 20 個向量中的兩個,增加它們之間的空間,直到它們不再是最接近的,然後切換到增加新的兩個最接近向量之間的角度。 需要注意的重要一點是它有效。 我們看到 TensorFlow 能夠通過tf.reduce_min()
方法和tf.acos()
方法傳遞梯度來做正確的事情。
讓我們嘗試一些更詳細的東西。 我們知道,在最優解中,所有向量與其最近的鄰居應該具有相同的角度。 因此,讓我們將“最小角度的方差”添加到損失函數中。
class VectorSpread_MaxMinAngle_w_Variance(VectorSpreadAlgorithm): def spread_metric(self, tensor2d): """ Assumes all rows already normalized """ angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi all_mins = tf.reduce_min(angle_pairs + disable_diag, axis=1) # Same calculation as before: find the min-min angle min_min = tf.reduce_min(all_mins) # But now also calculate the variance of the min angles vector avg_min = tf.reduce_mean(all_mins) var_min = tf.reduce_sum(tf.square(all_mins - avg_min)) # Our spread metric now includes a term to minimize variance spread_metric = min_min - 0.4 * var_min # As before, want negative spread to keep it a minimization problem return -spread_metric
那個孤獨的北向向量現在迅速加入了它的對等點,因為與它最近的鄰居的角度很大,並且使現在被最小化的方差項尖峰。 但它最終仍由全球最小角度驅動,該角度仍然緩慢上升。 我必須改進的想法通常適用於這種 2D 情況,但不適用於任何更高的維度。
但是過分關注這種數學嘗試的質量是沒有意義的。 看看在均值和方差計算中涉及了多少張量運算,以及 TensorFlow 如何成功地跟踪和區分輸入矩陣中每個分量的每個計算。 而且我們不必進行任何手動微積分。 我們只是把一些簡單的數學放在一起,TensorFlow 為我們做了微積分。
最後,讓我們再嘗試一件事:基於力的解決方案。 想像一下,每一個向量都是一個拴在一個中心點上的小行星。 每顆行星都會散發出一種力量,將其與其他行星排斥。 如果我們要運行這個模型的物理模擬,我們應該最終得到我們想要的解決方案。
我的假設是梯度下降也應該起作用。 在最佳解決方案中,每個行星上與其他行星的切線力應該抵消為淨零力(如果它不為零,行星將移動)。 因此,讓我們計算每個向量上的力大小,並使用梯度下降將其推向零。
首先,我們需要定義使用tf.*
方法計算力的方法:
class VectorSpread_Force(VectorSpreadAlgorithm): def force_a_onto_b(self, vec_a, vec_b): # Calc force assuming vec_b is constrained to the unit sphere diff = vec_b - vec_a norm = tf.sqrt(tf.reduce_sum(diff**2)) unit_force_dir = diff / norm force_magnitude = 1 / norm**2 force_vec = unit_force_dir * force_magnitude # Project force onto this vec, calculate how much is radial b_dot_f = tf.tensordot(vec_b, force_vec, axes=1) b_dot_b = tf.tensordot(vec_b, vec_b, axes=1) radial_component = (b_dot_f / b_dot_b) * vec_b # Subtract radial component and return result return force_vec - radial_component
然後,我們使用上面的力函數定義我們的損失函數。 我們累積每個向量上的淨力併計算其大小。 在我們的最佳解決方案中,所有力都應該抵消,我們應該有零力。

def calc_loss(self, tensor2d): n_vec = tensor2d.numpy().shape[0] all_force_list = [] for this_idx in range(n_vec): # Accumulate force of all other vecs onto this one this_force_list = [] for other_idx in range(n_vec): if this_idx == other_idx: continue this_vec = tensor2d[this_idx, :] other_vec = tensor2d[other_idx, :] tangent_force_vec = self.force_a_onto_b(other_vec, this_vec) this_force_list.append(tangent_force_vec) # Use list of all N-dimensional force vecs. Stack and sum. sum_tangent_forces = tf.reduce_sum(tf.stack(this_force_list)) this_force_mag = tf.sqrt(tf.reduce_sum(sum_tangent_forces**2)) # Accumulate all magnitudes, should all be zero at optimal solution all_force_list.append(this_force_mag) # We want to minimize total force sum, so simply stack, sum, return return tf.reduce_sum(tf.stack(all_force_list))
該解決方案不僅運行良好(除了前幾幀中的一些混亂),而且真正歸功於 TensorFlow。 這個解決方案涉及多個for
循環、一個if
語句和一個巨大的計算網絡,TensorFlow 成功地為我們追踪了所有這些的梯度。
示例 3:生成對抗性 AI 輸入
示例 3 筆記本
此時,讀者可能會想,“嘿!這篇文章不應該是關於深度學習的!” 但從技術上講,介紹是指超越“訓練深度學習模型”。 在這種情況下,我們不是在訓練,而是利用預訓練的深度神經網絡的一些數學特性來欺騙它給我們錯誤的結果。 事實證明,這比想像的要容易和有效得多。 所需要的只是又一小段 TensorFlow 2.0 代碼。
我們首先找到一個要攻擊的圖像分類器。 我們將使用 Dogs vs. Cats Kaggle 比賽的頂級解決方案之一; 具體來說,Kaggler 提出的解決方案“uysimty”。 他們提供了有效的貓對狗模型並提供了出色的文檔,這一切都歸功於他們。 這是一個強大的模型,由 18 個神經網絡層的 1300 萬個參數組成。 (歡迎讀者在相應的筆記本中閱讀更多相關內容。)
請注意,這裡的目標不是突出這個特定網絡的任何缺陷,而是展示任何具有大量輸入的標準神經網絡是如何脆弱的。
稍加修改,我就能弄清楚如何加載模型並預處理要由它分類的圖像。
這看起來像一個非常可靠的分類器! 所有樣本分類均正確且置信度高於 95%。 讓我們攻擊它!
我們想要生成一張明顯是貓的圖像,但讓分類器以高置信度判定它是狗。 我們怎麼能做到這一點?
讓我們從它正確分類的貓圖片開始,然後計算給定輸入像素的每個顏色通道(值 0-255)中的微小修改如何影響最終分類器輸出。 修改一個像素可能不會做太多,但也許所有 128x128x3 = 49,152 像素值的累積調整將實現我們的目標。
我們怎麼知道以哪種方式推動每個像素? 在正常的神經網絡訓練期間,我們嘗試最小化目標標籤和預測標籤之間的損失,使用 TensorFlow 中的梯度下降來同時更新所有 1300 萬個自由參數。 在這種情況下,我們將保持 1300 萬個參數不變,並調整輸入本身的像素值。
我們的損失函數是什麼? 好吧,這就是圖像看起來像貓的程度! 如果我們計算貓值相對於每個輸入像素的導數,我們就知道用哪種方式推動每個像素以最小化貓分類概率。
def adversarial_modify(victim_img, to_dog=False, to_cat=False): # We only need four gradient descent steps for i in range(4): tf_victim_img = tf.convert_to_tensor(victim_img, dtype='float32') with tf.GradientTape() as tape: tape.watch(tf_victim_img) # Run the image through the model model_output = model(tf_victim_img) # Minimize cat confidence and maximize dog confidence loss = (model_output[0] - model_output[1]) dloss_dimg = tape.gradient(loss, tf_victim_img) # Ignore gradient magnitudes, only care about sign, +1/255 or -1/255 pixels_w_pos_grad = tf.cast(dloss_dimg > 0.0, 'float32') / 255. pixels_w_neg_grad = tf.cast(dloss_dimg < 0.0, 'float32') / 255. victim_img = victim_img - pixels_w_pos_grad + pixels_w_neg_grad
Matplotlib 魔法再次有助於可視化結果。
哇! 在人眼看來,這些圖片中的每一張都是相同的。 然而,經過四次迭代,我們以 99.4% 的置信度讓分類器確信這是一隻狗!
讓我們確保這不是僥倖,它也在另一個方向起作用。
成功! 分類器最初以 98.4% 的置信度將其正確預測為狗,現在以 99.8% 的置信度認為它是貓。
最後,讓我們看一個示例圖像補丁,看看它是如何變化的。
正如預期的那樣,最終的補丁與原始補丁非常相似,每個像素僅在紅色通道的強度值中移動 -4 到 +4。 這種轉變不足以讓人類區分差異,而是完全改變了分類器的輸出。
最後的想法:梯度下降優化
在整篇文章中,為了簡單和透明,我們已經研究了手動將梯度應用於我們的可訓練參數。 然而,在現實世界中,數據科學家應該立即開始使用優化器,因為它們往往更有效,而且不會增加任何代碼膨脹。
有許多流行的優化器,包括 RMSprop、Adagrad 和 Adadelta,但最常見的可能是Adam 。 有時,它們被稱為“自適應學習率方法”,因為它們為每個參數動態地保持不同的學習率。 他們中的許多人使用動量項和近似高階導數,目的是逃避局部最小值並實現更快的收斂。
在從 Sebastian Ruder 借來的動畫中,我們可以看到各種優化器下降損失曲面的路徑。 我們展示的手動技術最能與“SGD”相媲美。 對於每個損失面,性能最好的優化器不會是相同的; 但是,更高級的優化器通常比更簡單的優化器執行得更好。
然而,成為優化器專家很少有用——即使對於那些熱衷於提供人工智能開發服務的人來說也是如此。 更好地利用開發人員的時間來熟悉一對夫婦,只是為了了解他們如何改進 TensorFlow 中的梯度下降。 之後,他們可以默認使用Adam ,並且只有在他們的模型不收斂時才嘗試不同的模型。
對於真正對這些優化器如何以及為何工作感興趣的讀者,Ruder 的概述(其中出現了動畫)是該主題的最佳和最詳盡的資源之一。
讓我們從第一部分更新我們的線性回歸解決方案以使用優化器。 以下是使用手動梯度的原始梯度下降代碼。
# Manual gradient descent operations def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') for i in range(25): with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) gradients = tape.gradient(loss, [tf_slope, tf_icept]) tf_slope = tf_slope - learning_rate * gradients[0] tf_icept = tf_icept - learning_rate * gradients[1]
現在,這裡是使用優化器的相同代碼。 您會看到它幾乎沒有任何額外的代碼(更改的行以藍色突出顯示):
# Gradient descent with Optimizer (RMSprop) def run_gradient_descent (heights, weights, init_slope, init_icept, learning_rate) : tf_slope = tf.Variable(init_slope, dtype= 'float32' ) tf_icept = tf.Variable(init_icept, dtype= 'float32' ) # Group trainable parameters into a list trainable_params = [tf_slope, tf_icept] # Define your optimizer (RMSprop) outside of the training loop optimizer = keras.optimizers.RMSprop(learning_rate) for i in range( 25 ): # GradientTape loop is the same with tf.GradientTape() as tape: tape.watch( trainable_params ) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors** 2 ) # We can use the trainable parameters list directly in gradient calcs gradients = tape.gradient(loss, trainable_params ) # Optimizers always aim to *minimize* the loss function optimizer.apply_gradients(zip(gradients, trainable_params))
而已! 我們在梯度下降循環之外定義了一個RMSprop
優化器,然後在每次梯度計算之後使用optimizer.apply_gradients()
方法來更新可訓練參數。 優化器是在循環之外定義的,因為它將跟踪歷史梯度以計算額外的項,如動量和高階導數。
讓我們看看使用RMSprop優化器的效果。
看起來很棒! 現在讓我們嘗試使用Adam優化器。
哇,這裡發生了什麼? Adam 中的動量機制似乎導致它超出了最優解並多次逆轉。 通常,這種動量機制有助於處理複雜的損失面,但在這種簡單的情況下會傷害我們。 這強調了在訓練模型時選擇優化器作為要調整的超參數之一的建議。
任何想要探索深度學習的人都希望熟悉這種模式,因為它廣泛用於自定義 TensorFlow 架構中,其中需要復雜的損失機制,而這些機制不容易包含在標準工作流程中。 在這個簡單的 TensorFlow 梯度下降示例中,只有兩個可訓練參數,但在使用包含數億個參數的架構時需要優化。
TensorFlow 中的梯度下降:從尋找最小值到攻擊 AI 系統
所有代碼片段和圖像均來自相應 GitHub 存儲庫中的筆記本。 它還包含所有部分的摘要,以及指向各個筆記本的鏈接,供希望查看完整代碼的讀者使用。 為了簡化消息,很多細節被遺漏了,可以在大量的內聯文檔中找到。
我希望這篇文章很有見地,它能讓你思考在 TensorFlow 中使用梯度下降的方法。 即使您自己不使用它,它也有望讓您更清楚地了解所有現代神經網絡架構的工作原理——創建模型、定義損失函數並使用梯度下降使模型適合您的數據集。
作為 Google Cloud 合作夥伴,Toptal 的 Google 認證專家可根據公司最重要項目的需求提供給他們。