wap网站前景,阿里云网站建设套餐,营销型网站建设的优势有哪些,网络营销策划方案的设计相关说明
这篇文章的大部分内容参考自我的新书《解构大语言模型#xff1a;从线性回归到通用人工智能》#xff0c;欢迎有兴趣的读者多多支持。 本文涉及到的代码链接如下#xff1a;regression2chatgpt/ch06_optimizer/gradient_descent.ipynb
本文将讨论利用PyTorch实现…相关说明
这篇文章的大部分内容参考自我的新书《解构大语言模型从线性回归到通用人工智能》欢迎有兴趣的读者多多支持。 本文涉及到的代码链接如下regression2chatgpt/ch06_optimizer/gradient_descent.ipynb
本文将讨论利用PyTorch实现梯度下降法的细节。这是神经网络模型的共同工程基础。
关于大语言模型的内容推荐参考这个专栏。 内容大纲 相关说明一、为什么需要了解实现细节二、梯度下降法的理论基础三、代码实现 一、为什么需要了解实现细节
在我们使用经典机器学习模型对数据建模时首先会从实际应用场景出发初步分析数据的特征获取灵感和直觉然后通过数学的抽象和变换为问题选择合适的模型架构最后使用Python开源的算法库实现最终的模型其中模型的参数已经被估计出来。
从软件设计的角度来讲Python开源算法库在抽象Abstraction方面做得非常出色。它有效地隐藏了模型构建和训练的底层实现细节使我们只需关注高层的概念和操作即提供的一系列函数接口API。通过这些接口通常只需几十行代码就能完成模型的构建和训练。在这个过程中无须过多考虑模型背后复杂的数学计算计机估计模型参数的算法实现也不再成为障碍。在理想情况下所有底层的复杂性都被完美抽象数据科学家的工作更加轻松和便捷当然作为硬币的另一面这也可能导致数据科学家的门槛降低进而影响相关职位的数量和薪水。然而不幸的是或者幸运的是由于模型涉及复杂的数学抽象和计算即使软件设计和抽象再完美也无法完全掩盖其复杂性某些细节仍然可能“泄漏”出来影响用户对系统的理解和操作这就是抽象泄漏Leaky Abstraction。
举个例子在训练逻辑回归模型时某些数据集可能导致开源算法库出现错误无法估计模型参数。对于相对经典或简单的模型抽象泄漏的情况较少出现。然而对于更复杂的模型例如神经网络领域的深度学习和语言大模型可能出现大量的抽象泄漏问题。如果不理解底层实现的细节在这些领域将寸步难行从理论角度来看无法理解模型的精髓就难以有效地优化模型无法达到预期的模型效果从实际应用角度来看遇到程序问题难以修复训练时间过长除了参考示例实现很难灵活运用算法库也无法根据需求调整模型架构。
因此这个系列的文章将深入研究开源算法库的核心细节探讨如何基于模型的数学公式计算出相应的参数估计值。更具学术性的表述是——探讨解决最优化问题的算法。最优化问题有多种求解方法不同算法适用于不同的模型并在解决不同类型的问题上各有优势。鉴于篇幅限制本文将重点关注最基础的算法梯度下降法。后续的文章将继续讨论如何实现随机梯度下降法及其各种变种。
二、梯度下降法的理论基础
对于任何一个模型它都对应着一个损失函数 L L L假设选取的初始点为 a 0 , b 0 a_0,b_0 a0,b0现在将这两个点稍稍移动一点得到 a 1 , b 1 a_1,b_1 a1,b1。根据泰勒级数Taylor Series1暂时只考虑一阶导数2可以得到公式1其中 ∆ a a 1 − a 0 , ∆ b b 1 − b 0 ∆a a_1 - a_0,∆b b_1 - b_0 ∆aa1−a0,∆bb1−b0。 ∆ L L ( a 1 , b 1 ) − L ( a 0 , b 0 ) ≈ ∂ L ∂ a ∆ a ∂ L ∂ b ∆ b (1) ∆L L(a_1,b_1) - L(a_0,b_0) ≈\frac{∂L}{∂a} ∆a \frac{∂L}{∂b} ∆b \tag{1} ∆LL(a1,b1)−L(a0,b0)≈∂a∂L∆a∂b∂L∆b(1) 如果令 ( ∆ a , ∆ b ) − η ( ∂ L / ∂ a , ∂ L / ∂ b ) (2) (∆a,∆b) -η(∂L/∂a,∂L/∂b) \tag{2} (∆a,∆b)−η(∂L/∂a,∂L/∂b)(2)
其中 η 0 η 0 η0可以得到 ∆ L ≈ − η [ ( ∂ L / ∂ a ) 2 ( ∂ L / ∂ b ) 2 ] ≤ 0 ∆L ≈ -η[(∂L/∂a)^2 (∂L/∂b)^2] \le 0 ∆L≈−η[(∂L/∂a)2(∂L/∂b)2]≤0。这说明如果按公式2移动参数损失函数的函数值始终是下降的这正是我们想要达到的效果。如果一直重复这种移动数学上可以证明损失函数能最终得到它的最小值整个过程就像鸡蛋在圆底锅里滚动一样于是可以得到参数的迭代公式见公式3。 a k 1 a k − η ∂ L ∂ a b k 1 b k − η ∂ L ∂ b (3) a_{k 1} a_k - η \frac{∂L}{∂a} \\ b_{k 1} b_k - η \frac{∂L}{∂b} \tag{3} ak1ak−η∂a∂Lbk1bk−η∂b∂L(3)
也可以换一个类比角度来理解梯度下降法的核心思想。想象你站在一个山坡上目标是要找到最低的山谷。公式3就如同导航在山坡上指引着你下山的方向。如果地势是向下的损失函数的偏导数 ∂ L ⁄ ∂ a 0 ∂L⁄∂a 0 ∂L⁄∂a0那么你会朝着这个方向迈出一步相反如果地势是向上的 ∂ L ⁄ ∂ a 0 ∂L⁄∂a 0 ∂L⁄∂a0那么你会退回一步避免走向更高的地方。
在数学上向量 ∇ L ( ∂ L / ∂ a , ∂ L / ∂ b ) ∇L (∂L/∂a,∂L/∂b) ∇L(∂L/∂a,∂L/∂b)被称为损失函数L的梯度。这也是公式3表示的算法被称为梯度下降法的原因。同时可以证明函数的梯度正好是函数值下降得最快的方向因此梯度下降法也是最高效的“下降”方式。
综上可以将梯度下降法的主要算法归纳为三步根据当前参数和训练数据计算模型损失计算当前的损失函数梯度利用梯度迭代更新模型参数如图1所示。 图1 需要强调的是从严谨的数学角度来看多元可微函数 L L L在点 P P P上的梯度实际上是由 L L L在点 P P P上各个变量的偏导数构成的向量。然而在人工智能领域尤其是神经网络领域为了简化表达我们通常会用“变量的梯度” 3这一术语来指代该变量在特定情况下的偏导数或者对偏导数的估计值。
三、代码实现
下面将探索如何利用PyTorch提供的封装函数来实现梯度下降法。实现梯度下降法涉及3个关键步骤。
根据当前参数和训练数据计算模型损失。计算当前的损失函数梯度利用模型定义的损失函数及训练数据计算得到当前损失函数的梯度。需要注意的是损失函数梯度的计算依赖于损失函数的数学表达式、用于梯度计算的训练数据以及当前的参数估计值。这一步可以由PyTorch封装好的反向传播算法4Back PropagationBP来完成。利用梯度更新模型参数在计算得到损失函数的当前梯度后利用这个梯度来迭代更新模型参数的估计值。这一步可以由PyTorch提供的优化算法函数例如torch.optim.SGD来实现。
首先进行一些准备工作包括生成训练所需的数据和定义模型的结构。尽管这部分代码相对简单但仍需注意以下两点。
在程序清单1完整代码的第2—4行对变量x进行归一化处理。这一步的目的在于保证梯度下降法的稳定性。实际上读者可以很容易地修改代码不对x进行归一化处理但会影响梯度下降法的稳定性进而可能导致无法收敛的情况。在实际建模过程中几乎会对每个变量进行归一化处理以确保模型的稳健性和可靠性。在程序清单1的第9—28行通过继承torch.nn.Module的方式来定义线性回归模型。在具体的实现中需要重写两个核心函数__init__和forward。__init__函数定义了模型所需的参数及相应的初始值forward函数中描述了如何利用这些参数获得模型的预测结果5。 程序清单1 定义模型和产生训练数据 1 | # 产生训练用的数据2 | x_origin torch.linspace(100, 300, 200)3 | # 将变量x归一化否则梯度下降法很容易不稳定4 | x (x_origin - torch.mean(x_origin)) / torch.std(x_origin)5 | epsilon torch.randn(x.shape)6 | y 10 * x 5 epsilon7 | 8 | # 为了使用PyTorch的高层封装函数通过继承Module类来定义函数9 | class Linear(torch.nn.Module):
10 | def __init__(self):
11 |
12 | 定义线性回归模型的参数a, b
13 |
14 | super().__init__()
15 | self.a torch.nn.Parameter(torch.zeros(()))
16 | self.b torch.nn.Parameter(torch.zeros(()))
17 |
18 | def forward(self, x):
19 |
20 | 根据当前的参数估计值得到模型的预测结果
21 | 参数
22 | ----
23 | x torch.tensor变量x
24 | 返回
25 | ----
26 | y_pred torch.tensor模型预测值
27 |
28 | return self.a * x self.b
29 |
30 | def string(self):
31 |
32 | 输出当前模型的结果
33 |
34 | return fy {self.a.item():.2f} * x {self.b.item():.2f}接下来进入核心的算法实现阶段如程序清单2所示其中包括定义模型的损失函数、计算损失函数的梯度以及计算迭代更新参数估计值。这些步骤相对固定几乎适用于所有模型。或许第13行中的“将上一次的梯度清零”操作可能会引发一些读者的困惑。实际上这行代码与反向传播算法的工作机制息息相关后续的文章[TODO]将对其进行详细的解释和讨论。 程序清单2 梯度下降法 1 | # 定义模型2 | model Linear()3 | # 确定最优化算法4 | learning_rate 0.15 | optimizer torch.optim.SGD(model.parameters(), lrlearning_rate)6 | 7 | for t in range(20):8 | # 根据当前的参数估计值得到模型的预测结果9 | # 也就是调用forward函数
10 | y_pred model(x)
11 | # 计算损失函数
12 | loss (y - y_pred).pow(2).mean()
13 | # 将上一次的梯度清零
14 | optimizer.zero_grad()
15 | # 触发反向传播算法计算损失函数的梯度
16 | loss.backward()
17 | # 迭代更新模型参数的估计值
18 | optimizer.step()本章运用PyTorch提供的高级封装函数来实现梯度下降法。尽管如此整个算法的核心难点仍然被这些函数隐藏了其中有两个关键函数起到了重要作用。首先是optimizer.step()负责实现参数的迭代更新其细节相对简单可以轻松地实现如图2所示其次是负责反向传播算法的loss.backward()函数其实现相当复杂将在后续的文章[TODO]中详细讨论。 图2 回顾一下泰勒一阶展开式假设 f ( x 1 , x 2 , ⋯ , x n ) f(x_1,x_2,⋯,x_n) f(x1,x2,⋯,xn)是一个一阶可导的函数即 ∂ 2 f ∂ x i ∂ x j \frac{∂^2 f}{∂x_i ∂x_j } ∂xi∂xj∂2f都存在则 f ( x 1 , x 2 , ⋯ , x n ) f ( a 1 , a 2 , ⋯ , a n ) ∑ i 1 n ∂ f ( a 1 , a 2 ⋯ , a n ) ∂ x i ( x i − a i ) o ( ∑ i ∣ x i − a i ∣ ) f(x_1,x_2,⋯,x_n)f(a_1,a_2,⋯,a_n)\sum_{i 1}^n\frac{∂f(a_1,a_2⋯,a_n)}{∂x_i}(x_i-a_i) o(\sum_i|x_i-a_i |) f(x1,x2,⋯,xn)f(a1,a2,⋯,an)∑i1n∂xi∂f(a1,a2⋯,an)(xi−ai)o(∑i∣xi−ai∣)其中 o ( ∑ i ∣ x i − a i ∣ ) o(\sum_i|x_i-a_i |) o(∑i∣xi−ai∣)表示相对于 ∑ i ∣ x i − a i ∣ \sum_i|x_i-a_i | ∑i∣xi−ai∣的极小值。因此在x很靠近a时有 f ( x ) ≈ f ( a ) ∑ i ∂ f ( a ) ∂ x i ( x i − a i ) f(x) ≈ f(a) \sum_i\frac{∂f(a)}{∂x_i}(x_i - a_i) f(x)≈f(a)∑i∂xi∂f(a)(xi−ai)。但是当x离a较远时上述近似关系的误差就很大了。 ↩︎ 如果考虑多阶导数可以得到其他的最优化问题求解算法比如使用二阶导数的共轭梯度法Conjugate Gradient Method等。这些算法对于特定问题可以更快地得到收敛解但它们对损失函数的要求更多计算复杂度也更高并不适合神经网络和分布式机器学习所以这里不做深入探讨。 ↩︎ 这一概念在实际应用中非常重要因为在优化算法中需要计算或者估计损失函数关于某个参数的偏导数以指导这个参数的更新。然而若要准确地计算梯度就需要对多元函数的每个偏导数进行计算这让准确的数学表述变得非常烦琐。因此通过使用“变量的梯度”这一术语能够使表达更简洁并在实际操作中更加便利地进行参数更新和优化。 ↩︎ 在PyTorch中算法的正式名字是自动微分Autograd或Automatic Differentiation算法。这两者指的其实是同一个算法。 ↩︎ 或许有些读者会对“为什么将模型的预测函数称为forward”感到好奇。这是因为在神经网络领域常常将计算模型的预测结果并评估损失的步骤称为向前传播而将更新模型参数的步骤称为向后传播。这种命名习惯在PyTorch这个主要应用于神经网络的开源工具中得到了延续。 ↩︎