Py学习  »  机器学习算法

梯度下降:数学、Python 和机器学习背后的魔力

机器人规划与控制研究所 • 1 年前 • 224 次点击  


创建一个类似于 PyTorch 张量的自动计算梯度值的 Python 类是巩固对梯度下降的理解的绝佳方法。

一,介绍



在本文中,我将解释什么是梯度下降、为什么学习它很重要、它背后的基本数学原理以及它如何有益于机器学习,我甚至会提供一个简单的 Python 代码示例。此代码示例将模拟 PyTorch 张量类包装器,以自动计算梯度值等。

我假设您对 Python 有基本的了解,熟悉一些基本数学概念,并且对深入研究机器学习有着浓厚的兴趣。


二,什么是梯度下降




梯度下降是一种优化技术,用于精确定位可微分函数中的最低点(称为局部最小值)。在机器学习领域,梯度下降用于确定函数参数(例如系数)的值。此过程以最大程度地最小化成本函数的方式执行。

在梯度下降中,梯度值(也称为斜率或导数)测量函数在特定点的陡峭程度。它表示函数相对于其输入变量的变化率。

简单来说,梯度值可以看作是输入值在经过函数内的数学处理后对输出值的敏感度或影响力的度量。它还表明这种影响是正向的还是负向的。利用这个梯度值,我们可以轻松计算出输入值的多少变化可以改变输出以及改变的方向。

正导数(dy/dx > 0):

  • 当函数的导数在某点为正时,表示函数在该点增加。

  • 就函数图形而言,如果在该点画一条切线,它将具有正斜率,表示随着沿曲线向右移动,函数值会增加。

  • 想象一下在山坡上上坡。当你向右行走(沿着 x 增加的方向)时,你正在上坡,地面的坡度为正

负导数(dy/dx < 0):

  • 当函数的导数在某点为负时,表示函数在该点处正在减小。

  • 从图形上看,如果你画一条切线,它的斜率会是负的,表示沿曲线向右移动时,函数值会减小。

  • 想象一下在山坡上向下移动。当你向右行走(沿着 x 增加的方向)时,你正在下坡,地面的坡度为负。

零导数(dy/dx = 0):

  • 当函数的导数在某点为零时,意味着函数在该点具有平坦点或局部极值(最大值或最小值)。

  • 从图形上讲,如果画一条切线,它将是水平的(斜率为零),表示函数在该点没有变化。

  • 想象一下站在平坦的高原上。如果你向右或向左移动,地面的坡度是平坦的,所以你不会向上或向下移动。

例如,假设我们有一个函数f(x),它接受单个输入,并在应用一些数学运算后产生单个输出。计算此函数的梯度值需要评估输入值的微小变化会如何影响函数的最终输出值。

让我们通过图表来看一下


import matplotlib.pyplot as pltimport numpy as np
# simple functiondef func(x): return np.sin(x)
# derivate of func(x)def d_func(x): return np.cos(x)
X = np.arange(-5, 5, 0.4) # sample x valuesY = func(X) # y valuesZ = d_func(X) # derivates/gradient value
plt.plot(X, Y, c="blue", marker='o', label="(X, func(X))")plt.plot(X, Z, c='red', marker='o', label="(X, d_func(X))")plt.xlabel('X')plt.ylabel('Y/Z')plt.legend()plt.grid(True)print("(X, func(X)) = ", list(zip(X[:2], Y[:2])))print("(X, d_func(X)) = ", list(zip(X[:2], Z[:2])))


在这个图片中,红线表示导数输出,而蓝线表示实际输出。

  • 正导数:函数增加,斜率向上。

  • 负导数:函数减少,斜率向下。

  • 零导数:平坦点,斜率没有变化(中性)。

这种影响或敏感性的测量量化了输入和输出值之间的关系,以称为该函数的梯度值的数值来表示。

更多示例,


1. f(x) = x,那么这个函数的梯度值就是1。

2. 函数 f(x) = 2x 的梯度(或导数)为 2。

3. 函数 f(x) = x² 的梯度(或导数)是 2x。

让我们使用输入变化的概念来交叉验证前面示例的梯度值。为此,我们将使用一个较小的值 h = 1 和一个固定的输入值 x = 2。


示例 1:

原函数:f(x) = x

当我们将 (2 + h) 作为输入值传递时,我们得到 f(2 + h) = 2 + h 作为输出。这意味着输入保持不变。

梯度值为 1 表示输入值的变化对输出具有相同的影响,就像在这种情况下输出 (2 + h) 跟随输入 (h)。


例子2:

原函数:f(x) = 2x

当我们将 (2 + h) 作为输入值时,我们得到 f(2 + h) = 4 + 2h 作为输出。

梯度值为 2 意味着输入值的变化会导致输出变化两倍,如此处所观察到的,输出 (4 + 2h) 是输入变化 (2h) 的两倍。


示例 3:

原函数:f(x) = x²

当我们将 (2 + h) 作为输入值传递时,我们得到 f(2 + h) = 4 + h² + 4h 作为输出。

梯度值为 2x 表示输入值变化的影响取决于 x 的值。在这种情况下,如果 x = 2 + h,则输出变化 (4 + h² + 4h) 会受到 2 倍输入变化 (2(2 + h))=4+2h 的影响,从而说明梯度如何随输入值 x 而变化。



三,数学中的基本概念?


导数测量函数如何随其输入(通常表示为“x”)的变化而变化。换句话说,它告诉我们函数在特定点的变化率。您可以将其视为特定点的曲线斜率。

要计算导数,可以按照以下步骤操作:

  1. 从一个函数开始,比如说 f(x)。例如,f(x) = 2x²。

  2. 选择要求导数的点。假设 x = 3。

  3. 利用上述方程求该点的导数:f'(x) = lim (h -> 0) [f(x + h) — f(x)] / h

  4. 这里,“h”是一个接近于零的非常小的数字。它代表 x 值的微小变化。

  5. 代入数值并计算:f'(3) = lim (h -> 0) [2(3 + h)² — 2(3)²] / h

  6. 计算 'h' 趋近于零时的极限,得到 x = 3 处的导数。


f(x) = 2x² 的导数是 f'(x) = 4x。

f'(3)= 4(3)= 12

让我们通过图表来看一下

import matplotlib.pyplot as pltimport numpy as np
# simple functiondef func(x): return 2*(x**2)
# derivate of func(x)def d_func(x): h = 0.00001 return (func(x+h)-func(x))/h
X = np.arange(-25, 25, 1) #Y = func(X)Z = d_func(X)
plt.plot(X, Y, c="blue", label="(X, func(X))")plt.plot(X, Z, c='red', label="(X, d_func(X))")plt.xlabel('X')plt.ylabel('Y')plt.legend()plt.grid(True)


  • 正导数:函数增加,斜率向上。

  • 负导数:函数减少,斜率向下。

如何求函数的导数?

求导数的最基本方法是利用导数的极限定义。函数 f(x) 在点“a”处的导数定义为:

  • f'(a) = lim (h -> 0) [(f(a + h) - f(a)) / h]

在这个公式中,'h' 是一个接近于零的小数。这个极限表达式计算了 'h' 接近于零时切线的斜率。

以这种方式做事可能会非常慢,并且会消耗大量的计算机能力。为了使其更容易,我们可以使用不同的捷径和技巧。这些技巧包括幂法则、和/差法则、乘积法则、链式法则、三角函数、指数和对数函数以及商法则。


梯度下降

梯度下降背后的数学概念根植于微积分以及通过迭代调整输入参数来寻找函数最小值(有时是最大值)的思想。

该算法迭代调整模型或函数的参数(变量),以最小化或最大化特定目标(通常是成本或损失函数)。它通过遵循负梯度(用于最小化)或正梯度(用于最大化)的方向来实现这一点。当遇到梯度为零的点时,它可能会停止或改变其行为,具体取决于所使用的特定优化算法。

局部最小值:局部最小值是函数曲线上的一个点,该点的函数值低于其相邻点,但不一定具有整个函数中的最低值。在梯度下降中,找到局部最小值是一个关键目标,因为它代表了优化问题的潜在最优解。在局部最小值处,函数的导数(梯度)为零。这意味着在该点处,函数是平坦的,朝任何方向移动都会导致函数值增加。

局部最大值: 局部最大值是函数曲线上的一个点,该点的函数值高于其相邻点,但不是整个函数中的最高值。在某些优化问题中,找到局部最大值可能是目标,尽管它不如找到局部最小值常见。在局部最大值处,就像在局部最小值处一样,函数的导数(梯度)为零。这表明在该点处,函数是平坦的,任何移动都会导致函数值下降。

梯度下降更新方程是用于调整模型参数以最小化或最大化目标函数的公式。它是寻找最优参数集的迭代过程的关键部分。

https://en.wikipedia.org/wiki/Gradient_descent

上面的等式描述了梯度下降算法的作用:

  • aₙ₊₁ 表示下一次迭代时变量(参数)的值。

  • aₙ 表示当前迭代中变量(参数)的当前值。

  • α(alpha)是学习率,一个小的正常数。

  • ∇F(aₙ)表示函数F关于参数aₙ的梯度。

为了使其更加简单理解,我们可以这样写出上述等式。

新参数 = 旧参数 — 学习率 * 梯度

  • New_Parameter:这表示您正在优化的参数的更新值

  • Old_Parameter:这是更新之前参数的当前值。

  • Learning_Rate:这是一个很小的正数,它决定了每次迭代中采取的步骤的大小。

  • 梯度:梯度是目标函数关于您尝试更新的参数的导数。

为了理解梯度下降更新方程,重要的是了解什么是成本估计函数以及它们在其中的重要性。

如果还有不清楚的地方,不必担心。在我提供更多解释之前,让我们确保您了解成本估算函数(例如均方误差)是什么。


成本估算功能

成本估算函数(例如均方误差 (MSE))是一种衡量算法在解决特定问题时的表现的方法,通常在机器学习或优化的背景下。

就 MSE 而言,它量化了数据集中预测值与实际值之间的平方差的平均值。较低的 MSE 表示预测更接近真实值,而较高的 MSE 则表示预测离真实值较远。

简单来说,MSE 通过测量模型预测结果与实际结果之间的误差来帮助您评估模型预测的准确性。目标是最小化此成本函数以提高模型的性能。

示例(均方误差):

假设您有一个简单的模型,可以根据学生学习的小时数预测学生在数学考试中应获得的分数。您收集了三名学生的数据,以下是实际分数和模型预测的分数:

实际得分:[80, 90, 70]

预测得分:[75, 85, 75]

要计算 MSE,请按照以下步骤操作:

1. 取每个学生的实际分数与预测分数之间的差值:

  • 对于第一个学生:(80-75)= 5

  • 对于第二个学生:(90-85)= 5

  • 第三名学生:(70-75)= -5

2. 计算下列各差的平方:

  • (5)² = 25

  • (5)² = 25

  • (-5)² = 25

计算这些平方差的平均值:(25 + 25 + 25) / 3 = 25

因此,在这种情况下,均方误差 (MSE)为 25。


连接所有部分


我假设到目前为止,您对所讨论的概念已经有了理论理解。您熟悉梯度值代表什么、成本函数是什么以及如何计算函数的梯度。现在,让我们将这些概念联系起来,了解它们如何结合在一起提高机器学习模型预测的准确性。简而言之,我们将探索如何利用成本函数、梯度下降和参数调整来减少预测模型中的错误,并使其更好地做出准确的预测。

模型训练流程:

1. 您首先要有一个预测模型,该模型接受一些输入数据并提供输出预测。该模型由某些参数(通常表示为权重和偏差)控制。为了衡量模型的性能,您需要定义一个成本函数(也称为损失函数或误差函数)。此函数量化模型预测值与实际目标值之间的误差。成本函数通常接受两个输入:模型预测值和实际目标值。它输出一个表示误差的数字。[均方误差 (MSE) ]

2. 您使用模型对一组训练数据进行预测。
对于每个数据点,您使用成本函数通过将模型的预测与实际目标值进行比较来计算损失。
损失可以衡量模型对该特定数据点的预测与事实的偏差程度。

现在,您希望最小化所有数据点的总体损失以改进模型。梯度下降在这里发挥作用。梯度下降的工作原理是计算成本函数相对于模型参数的梯度(导数)。该梯度. 

3指向成本函数最急剧增加的方向。
为了最小化损失,您希望朝梯度的反方向移动,因此您从当前参数值中减去梯度。

4. 使用上一步获得的梯度信息更新模型的参数。更新规则通常涉及学习率 (α),它决定了你在梯度相反方向上迈出的步伐有多大。参数更新的公式通常如下所示:aₙ₊₁ = aₙ — α ⋅ ∇F(aₙ)。

当您使用梯度下降法迭代更新参数时,模型会学习做出更好的预测,从而最大限度地减少训练数据的整体损失。


四,通过示例代码理解



为了阐明上面讨论的主题,我提供了一个将梯度下降应用于线性回归问题的直接例子。

在这个例子中,我们将从一组随机的输入和输出值开始。输出值将具有与输入值相关的一些随机性。我的目的是展示如何应用梯度下降算法来调整线性回归线的斜率和截距,从而最大限度地减少损失。

我将以三种方式呈现同一个示例,以演示计算梯度的三种不同方法:

1. 有限差分近似。

2. 导数方程。

3.使用自动梯度值计算功能。


有限差分近似。

此代码演示了如何使用梯度下降来调整线性回归模型的参数(权重和偏差)以拟合噪声数据。目标是最小化预测值和实际输出值之间的均方误差。


# approach using Finite Difference Approximation.# Import necessary librariesimport


    
 matplotlib.pyplot as pltimport numpy as np
# an array ranging from -0.5 to 0.5 with a step of 0.01inputX = np.arange(-0.5, 0.5, 0.01) # random values with a normal distribution to add noise to the input.noise = np.random.normal(0, 0.2, inputX.shape)# output values with noise.outputY = inputX+noiseprint("Input Values : ", inputX[:2], " Output Values : ", outputY[:2])# so input dataset is ready inputX, outputYplt.scatter(inputX, outputY, c="blue", label="Dataset")
# initial weights and biasweight = 0.1 # any valuebias = 1 # any value
# calculates the predicted values based on the input, weight, and bias.def linear_regression_equation(input_value, weight, bias): # X = independent value # Y = Dependent Value # M = SLOPE # B = INTERCEPT/BIAS # = y=mx+b predicted_value = (weight * input_value) + bias return predicted_value
# calculates the mean squared error (MSE)def cost_function(input_value, weight, bias, target_value): predicted_value = linear_regression_equation(input_value, weight, bias) difference = (target_value - predicted_value)**2 return sum(difference)/len(difference)
# calculates gradients using finite difference approximation.def gradient_value_using_approx(inputX, outputY, weight, bias): # this approach is easy to implement but, # it takes more computation power and time. f = cost_function h = 0.001 w_grad_val = (f(inputX, weight+h, bias, outputY)-f(inputX, weight, bias, outputY))/h b_grad_val = (f(inputX, weight, bias+h, outputY)-f(inputX, weight, bias, outputY))/h return (w_grad_val, b_grad_val)
# the loss (MSE) before any learningbefore_loss_value = cost_function(inputX, weight, bias, outputY)plt.plot(inputX, linear_regression_equation(inputX, weight, bias), c="orange", label="Before Learning")
# training parametersepochs = 300learning_rate = 0.08# Weights and bias are updated using the learning rate # and gradients to minimize the loss.for _ in range(epochs): (w_grad_val, b_grad_val) = gradient_value_using_approx(inputX, outputY, weight, bias) weight = weight - (learning_rate*w_grad_val) bias = bias - (learning_rate*b_grad_val)
# the loss (MSE) after the specified number of epochs.after_loss_value = cost_function(inputX, weight, bias, outputY)print(f"Loss Value (Before Learning) : {before_loss_value}, Loss Value (After Learning) : {after_loss_value}")# plot the linear regression line in greenplt.plot(inputX, linear_regression_equation(inputX, weight, bias), c="green", label=f"After {epochs} epochs learning")plt.legend()plt.grid(True)

输出 :

Jupyter 笔记本输出

导数方程


相同的代码,但这次使用导数方程方法。


#.... Same Code as given in previous example .... def gradient_value_using_rules(inputX, outputY, weight, bias):    # recommended way    # using chain rule to get derivate of     # cost_function(linear_regression_equation(input))    #    w_grad_val = sum((-2 *inputX)*(outputY - ((weight*inputX)+bias)))/len(inputX)    b_grad_val = sum(-2*(outputY - ((weight*inputX)+bias)))/len(inputX)    return (w_grad_val, b_grad_val)
#.... Just replace gradient_value_using_approx to gradient_value_using_rules ....



同样结果:

机器学习模型通常涉及具有层、激活和损失函数的复杂架构。例如,在神经网络中,您有多个具有各种激活函数的层。手动计算此类模型的梯度可能非常困难,因此为了缓解此问题,我们可以使用 TensorFlow 和 PyTorch 等编程库来自动化此过程,使其更快且更不容易出错。

这些库不仅可以计算梯度,还可以提供一种在反向传播过程中调试和检查中间值的方法。这在诊断问题或实现自定义损失函数时非常有用。

对于本教程,我没有使用 PyTorch 或 TensorFlow,而是创建了一个简单的 Python 类包装器来管理此任务。通过使用此类对象包装我们的参数,我们可以轻松计算梯度值,而无需任何复杂的修改。此类在后台处理所有复杂性并为我们提供了一个用户友好的对象。我们可以像往常一样对此对象执行算术运算,但在后台,它会保留计算和算术运算的记录。最后,通过一个简单的.backward方法调用,我们可以轻松获取参数的梯度值。


自动计算梯度值

Python类,可以自动计算参数的梯度值。


class TensorValue: def __init__(self, num, ops= None, left=None, right=None): self.num = num self.ops = ops self.left = left self.right = right self.grad = 0.0 self.leaf = True if self.left or self.right: self.leaf = False def __add__(self, y): return self.__class__(self.num + getattr(y, "num", y), ops="+", left=self, right=y) def __radd__(self, y): return self.__add__(y) def __sub__(self, y): return self.__add__(-y) def __rsub__(self, y): return self.__add__(-y) def __mul__(self, y): return self.__class__(self.num * getattr(y,"num", y), ops="*", left=self, right=y) def __rmul__(self, y): return self.__mul__(y) def relu(self): return self.__class__(max(0, self.num), ops="rl", left=self)
def __neg__(self): return self.__mul__(-1) def __truediv__(self, y): return self.__mul__(y**-1) def __rtruediv__(self, y): return self.__mul__(y**-1) def __pow__(self, y): return self.__class__(self.num ** getattr(y, "num", y), ops="pow", left=self, right=y) def __repr__(self): return f"{self.__class__.__name__}({self.num})" def flush_gradient(self): if not self.leaf: self.grad = 0 if isinstance(self.left, self.__class__): self.left.flush_gradient() if isinstance(self.right, self.__class__): self.right.flush_gradient() def backward(self): self.grad = 1 return self.calculate_gradient_backward() def calculate_gradient_backward(self,): r = getattr(self.right, "num", self.right) l = getattr(self.left, "num", self.left) # the derivative of f(x, y) = x + y with respect to x is simply 1 if self.ops=="+": self.left.grad += (self.grad * 1) if isinstance(self .right, self.__class__): self.right.grad += (self.grad * 1) # the derivative of f(a, b) = a * b with respect to 'a' is 'b'. elif self.ops=="*": self.left.grad += (r * self.grad) if isinstance(self.right, self.__class__): self.right.grad += (l * self.grad) elif self.ops=="rl": self.left.grad += (int(self.num > 0) * self.grad) # the derivative of f(a, b) = a^b with respect to 'a' is 'b * a^(b-1)' elif self.ops=="pow": self.left.grad += ((r * (l ** (r - 1))) * self.grad) if isinstance(self.right, self.__class__): self.right.grad += ((l * (r ** (l -1))) * self.grad) if isinstance(self.left, self.__class__): self.left.calculate_gradient_backward() self.left.flush_gradient() if isinstance(self.right, self.__class__): self.right.calculate_gradient_backward() self.right.flush_gradient()


思想

该类背后的核心思想TensorValue是维护一个计算图,其中每个节点代表一个数值及其梯度。此图捕获对这些值执行的数学运算序列。当您使用 的实例执行操作时TensorValue,图中会创建新节点来表示这些操作的结果。梯度信息会在整个图中进行跟踪。

#示例:a 和 b 之间的算术运算 PyTorch Tensor
c = (a + b)*2;
d = a * b
e = torch.sin(c)

# 可视化计算图
make_dot(e, params={“a”: a, “b”: b})

计算图:


解释


此代码定义了一个名为的 Python 类,TensorValue它表示一个数值,并具有一些用于梯度计算的附加功能。

  • 该类使用数值 ( num)、可选操作 ( ops) 和可选左操作数和右操作数 (left和right) 进行初始化。它还具有梯度 ( grad) 和是否为叶节点 ( leaf) 的属性。

  • 该类重写了几个算术运算符(,,,,,,,,,,)__add__,以__radd__允许对的实例进行运算。__sub____rsub____mul____rmul____truediv____rtruediv____pow____neg__TensorValue

  • 该flush_gradient方法重置梯度值并且该backward方法启动梯度计算的反向传播。

  • 该calculate_gradient_backward方法根据执行的操作(加法、乘法、ReLU 或指数运算)计算梯度,并将它们递归传播到操作数。

让我们在同一个例子中使用这个类包装器。

import matplotlib.pyplot as pltimport random
inputX = [i*0.01 for i in range(-50, 50, 1)] #noise = [random.randrange(1, 100)*0.01 for _ in inputX]outputY = [(x+y) for x,y, in zip(inputX, noise)]print("Input Values : ", inputX[:2], " Output Values : ", outputY[:2])
plt.scatter(inputX, outputY, c="blue", label="Dataset")

weight = 0.1 # any valuebias = 1.0 # any valuedef linear_regression_equation(input_value, weight, bias): # X = independent value # Y = Dependent Value # M = SLOPE # B = INTERCEPT/BIAS # = y=mx+b predicted_value = (weight * input_value) + bias return predicted_value
def cost_function(input_value, weight, bias, target_value): c = 0 for i,t in zip(input_value, target_value): predicted_value = linear_regression_equation(i, weight, bias) difference = (t - predicted_value)**2 c += difference return c

def gradient_value_using_autograd_cal_wrapper(inputX, outputY, weight, bias): w = TensorValue(weight) # Here we are wrapping weight and bias b = TensorValue(bias) n = len(inputX) for x,y in zip(inputX, outputY): predicted_value = (w * x) + b loss = (y - predicted_value)**2 loss.backward() return ((w.grad/n), (b.grad/n)) # Boom! simple


before_loss_value = cost_function(inputX, weight, bias, outputY)plt.plot(inputX, [linear_regression_equation(i, weight, bias) for i in inputX], c="orange", label="Before Learning")epochs = 300learning_rate = 0.08for _ in range(epochs): (w_grad_val, b_grad_val) = gradient_value_using_autograd_cal_wrapper(inputX, outputY, weight, bias) weight = weight - (learning_rate*w_grad_val) bias = bias - (learning_rate*b_grad_val)
after_loss_value = cost_function(inputX, weight, bias, outputY)print(f"Loss Value (Before Learning) : {before_loss_value}, Loss Value (After Learning) : {after_loss_value}")plt.plot(inputX, [linear_regression_equation(i, weight, bias) for i in inputX], c="green", label=f"After {epochs} epochs learning")plt.legend()plt.grid(True)


同样结果:



五,结论


在本文中,我们通过各种示例探索了梯度下降的概念。我们介绍了导数和微分方程的基础知识,演示了如何计算函数的导数,讨论了成本函数,介绍了细化线性回归参数的关键概念,最后给出了一个能够自动计算梯度值的自动梯度计算类的示例。



六,参考文献


https://surajsinghbisht054.medium.com/mastering-gradient-descent-math-python-and-the-magic-behind-machine-learning-d12a7791f24e

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/171986