梯度下降在 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 认证专家可根据公司最重要项目的需求提供给他们。