优化的目标在于降低训练损失,只关注最小化目标函数上的表现]
深度学习问题中,我们通常会预先定义一个损失函数。有了损失函数以后,我们就可以使用优化算法试图将其最小化。在优化中,这样的损失函数通常被称作优化问题的目标函数(objective function)。依据惯例,优化算法通常只考虑最小化目标函数。
定义: 对于目标函数f(x),如果f(x)在x上的值比在x邻近的其他点的值更小,那么f(x)可能是一个局部最小值(local minimum)。 如果f(x)在x上的值是目标函数在整个定义域上的最小值,那么f(x)是全局最小值(global minimum)。
比如下面给定的函数
f ( x ) = x ⋅ cos ( π x ) , − 1.0 ≤ x ≤ 2.0 f(x) = x \cdot \text{cos}(\pi x), \qquad -1.0 \leq x \leq 2.0 f(x)=x⋅cos(πx),−1.0≤x≤2.0
当一个优化问题的数值解在局部最优解附近时,由于目标函数有关解的梯度接近或变成零,最终迭代求得的数值解可能只令目标函数局部最小化而非全局最小化。
刚刚我们提到,梯度接近或变成零可能是由于当前解在局部最优解附近造成的。事实上,另一种可能性是当前解在鞍点(saddle point)附近。
比如给定函数
f ( x ) = x 3 f(x) = x^3 f(x)=x3
多元函数上显示如下:
上图中,目标函数在CD方向是一个局部最大值,但是在AB方向是一个局部最小值。所有当某个函数在梯度为0的位置上可能是局部最小值、局部最大值或者鞍点。
条件:
- 当函数的海森矩阵在梯度为零的位置上的特征值全为正时,该函数得到局部最小值。
- 当函数的海森矩阵在梯度为零的位置上的特征值全为负时,该函数得到局部最大值。
- 当函数的海森矩阵在梯度为零的位置上的特征值有正有负时,该函数得到鞍点。
为什么海森矩阵判断?
回想一下我们是如何处理一元函数求极值问题的。例如, f ( x ) = x 2 f(x)=x^2 f(x)=x2,我们会先求一阶导数,即 f ′ ( x ) = 2 x f'(x)=2x f′(x)=2x,某点处的一阶导数一定等于 0。但这仅是一个必要条件,而非充分条件。对于 f ( x ) = x 2 f(x)=x^2 f(x)=x2来说,函数的确在一阶导数为零的点取得了极值,但是对于 f ( x ) = x 3 f(x)=x^3 f(x)=x3来说,显然只检查一阶导数是不足以下定论的。
例子:
f ( x , y , z ) = x 2 + y 2 + z 2 + 2 x + 4 y − 6 z f(x,y,z) = x^2+y^2+z^2+2x+4y-6z f(x,y,z)=x2+y2+z2+2x+4y−6z
求这个多元函数的极值:
首先对于某个变量(总共三个变量参数)的一阶导数为 0 0 0,意味着在这个变量方向上是极小或者极大值
∂ f ∂ x = 2 x + 2 = 0 , ∂ f ∂ y = 2 y + 4 = 0 , ∂ f ∂ z = 2 z − 6 = 0 \frac{\partial f}{\partial x} = 2x+2=0,\frac{\partial f}{\partial y} = 2y+4=0,\frac{\partial f}{\partial z} = 2z-6=0 ∂x∂f=2x+2=0,∂y∂f=2y+4=0,∂z∂f=2z−6=0
则三元函数的驻点是 ( − 1 , − 2 , 3 ) (-1, -2, 3) (−1,−2,3),表示在三个变量方向上都梯度都是为 0 0 0 的,但是这个点不知道是极小值或者极大值或者鞍点
求二阶导数
∂ 2 f ∂ x 2 = 2 , ∂ 2 f ∂ y 2 = 2 , ∂ 2 f ∂ z 2 = 2 \frac{\partial{^2} f}{\partial x{^2}} = 2,\frac{\partial{^2} f}{\partial y{^2}} = 2,\frac{\partial{^2} f}{\partial z{^2}} = 2 ∂x2∂2f=2,∂y2∂2f=2,∂z2∂2f=2
则表示成海森矩阵
A = ( 2 0 0 0 2 0 0 0 2 ) A = {\begin{pmatrix} 2&0&0\\ 0&2&0\\ 0&0&2 \end{pmatrix}} A=⎝⎛200020002⎠⎞
A矩阵所有值为正(正定矩阵)故 ( − 1 , − 2 , 3 ) (-1, -2, 3) (−1,−2,3)是极小值点,极小值为 f ( − 1 , − 2 , 3 ) = − 14 f(-1,-2,3)=-14 f(−1,−2,3)=−14
总结:海森矩阵可用于判断多元函数的极值点问题。
为什么会造成我们的损失函数难优化,其实有个原因就是因为激活函数存在使得函数计算梯度时候遇到梯度消失问题。在梯度函数上出现的以指数级递增或者递减的情况分别称为梯度爆炸或者梯度消失。
假设 g ( z ) = z , b [ l ] = 0 g(z) = z, b^{[l]} = 0 g(z)=z,b[l]=0,对于目标输出有: y ^ = W [ L ] W [ L − 1 ] . . . W [ 2 ] W [ 1 ] X \hat{y} = W^{[L]}W^{[L-1]}...W^{[2]}W^{[1]}X y^=W[L]W[L−1]...W[2]W[1]X
在计算梯度时,根据不同情况梯度函数也会以指数级递增或递减,导致训练导数难度上升,梯度下降算法的步长会变得非常小,需要训练的时间将会非常长。
解决办法有多种形式
通常会结合一些形式一起进行
定义:批梯度下降法(btach),即同时处理整个训练集。
其在更新参数时使用所有的样本来进行更新。对整个训练集进行梯度下降法的时候,我们必须处理整个训练数据集,然后才能进行一步梯度下降,即每一步梯度下降法需要对整个训练集进行一次处理,如果训练数据集很大的时候,处理速度就会比较慢。
所以换一种方式,每次处理训练数据的一部分进行梯度下降法,则我们的算法速度会执行的更快。
定义:Mini-Batch 梯度下降法(小批量梯度下降法)每次同时处理固定大小的数据集。
若mini-batch 的大小为 1,即是随机梯度下降法(stochastic gradient descent)
使用 Mini-Batch 梯度下降法,对整个训练集的一次遍历(epoch)只做 mini-batch个样本的梯度下降,一直循环整个训练集。
batch梯度下降法和Mini-batch 梯度下降法代价函数的变化趋势如下:
batch 梯度下降法:
随机梯度下降法(Mini-Batch=1):
因此,选择一个合适的大小进行 Mini-batch 梯度下降,可以实现快速学习,也应用了向量化带来的好处,且成本函数的下降处于前两者之间。
需要根据经验快速尝试,找到能够最有效地减少成本函数的值。那么第二种方式是通过优化梯度下降过程,会比梯度下降算法的速度更快些
这里我们会介绍三种类型的优化算法,不过很多原理都大同小异。
指数加权平均(Exponentially Weight Average) 是一种常用的序列数据处理方式,通常用在序列场景如金融序列分析、温度变化序列分析。
假设给定一个序列,例如北京一年每天的气温值,图中蓝色的点代表真实数据。
那么这样的气温值变化可以理解成优化的过程波动较大,异常较多。那么怎么平缓一些呢,这时候就要用到加权平均值了,如指数加权平均值。首先看一些效果。
这条红线怎么计算出来?通过指数加权的公式即:
S t = { Y 1 t = 1 β S t − 1 + ( 1 − β ) Y t t > 1 S_t=\left\{ \begin{array}{lll} Y_1&t=1\\ \beta S_{t-1}+(1-\beta)Y_t&t>1 \end{array} \right. St={Y1βSt−1+(1−β)Ytt=1t>1
其中 Y t Y_{t} Yt 为 t t t 下的实际值, S t S_{t} St 为 t t t 下加权平均后的值, β \beta β 为权重值。
上图的红线中, β \beta β 为 0.9 0.9 0.9, 那么第一天的温度计算为 S 1 = Y 1 S_{1} = Y1 S1=Y1
那么:
S 2 = 0.9 S 1 + 0.1 Y 2 . . . S 99 = 0.9 S 98 + 0.1 Y 99 S 100 = 0.9 S 99 + 0.1 Y 100 . . . S_2 = 0.9S_1+0.1Y_2\\ ...\\ S_{99} = 0.9 S_{98} + 0.1 Y_{99}\\ S_{100} = 0.9 S_{99} + 0.1 Y_{100}\\ ... S2=0.9S1+0.1Y2...S99=0.9S98+0.1Y99S100=0.9S99+0.1Y100...
假设就100天,那么合并的结果 S 100 = 0.1 Y 100 + 0.1 ∗ 0.9 Y 99 + 0.1 ∗ ( 0.9 ) 2 Y 98 + . . . S_{100} = 0.1 Y_{100} + 0.1 * 0.9 Y_{99} + 0.1 * {(0.9)}^2 Y_{98} + {...} S100=0.1Y100+0.1∗0.9Y99+0.1∗(0.9)2Y98+...
下图中,当取权重值 β = 0.98 β=0.98 β=0.98 时,可以得到图中更为平滑的绿色曲线。而当取权重值 β = 0.5 β=0.5 β=0.5 时,得到图中噪点更多的黄色曲线。 β \beta β 越大相当于求取平均利用的天数越多,曲线自然就会越平滑而且越滞后。这些系数被称作偏差修正(Bias Correction)
上述点数据,我们是否可以理解成梯度下降的过程,每一迭代优化计算出来的梯度值?
动量梯度下降(Gradient Descent with Momentum) 是计算梯度的指数加权平均数,并利用该值来更新参数值。
其中 β \beta β 通常设置为0.9,动量梯度下降法的整个过程为:
S d W [ l ] = β S d W [ l ] + ( 1 − β ) d W [ l ] S_{dW^{[l]}} = \beta S_{dW^{[l]}} + (1 - \beta) dW^{[l]} SdW[l]=βSdW[l]+(1−β)dW[l]
S d b [ l ] = β S d b [ l ] + ( 1 − β ) d b [ l ] S_{db^{[l]}} = \beta S_{db^{[l]}} + (1 - \beta) db^{[l]} Sdb[l]=βSdb[l]+(1−β)db[l]
W [ l ] : = W [ l ] − α S d W [ l ] W^{[l]} := W^{[l]} - \alpha S_{dW^{[l]}} W[l]:=W[l]−αSdW[l]
b [ l ] : = b [ l ] − α S d b [ l ] b^{[l]} := b^{[l]} - \alpha S_{db^{[l]}} b[l]:=b[l]−αSdb[l]
那么这样梯度下降过程会有什么变化,如下图所示:
使用动量梯度下降时,通过累加过去的梯度值来减少抵达最小值路径上的波动,加速了收敛,因此在横轴方向下降得更快,从而得到图中蓝色的曲线。当前后梯度方向一致时,动量梯度下降能够加速学习;而前后梯度方向不一致时,动量梯度下降能够抑制震荡。
其它改进动量算法,nesterov
前面讨论的方法都是对学习率进行全局地操作,并且对所有的参数都是一样的。
w 1 ← w 1 − η ∂ f ∂ w 1 , w 2 ← w 2 − η ∂ f ∂ w 2 w_1 \leftarrow w_1 - \eta \frac{\partial{f}}{\partial{w_1}}, w_2 \leftarrow w_2 - \eta \frac{\partial{f}}{\partial{w_2}} w1←w1−η∂w1∂f,w2←w2−η∂w2∂f
学习率调参是很耗费计算资源的过程,所以很多工作投入到发明能够适应性地对学习率调参的方法,甚至是逐个参数适应学习率调参。很多这些方法依然需要其他的超参数设置,但是其观点是这些方法对于更广范围的超参数比原始的学习率方法有更良好的表现。
AdaGrad算法会使用一个小批量随机梯度 g t g_t gt 按元素平方累加变量 s t s_t st。在时间步0,AdaGrad将 s 0 s_0 s0中每个元素初始化为0。在时间步t,首先将小批量随机梯度 g t g_t gt按元素平方后累加到变量 s t s_t st:
s t ← s t − 1 + g t ⊙ g t {s}_{t} \leftarrow {s}_{t-1} + {g}_{t} \odot {g}_{t} st←st−1+gt⊙gt
其中 ⊙ \odot ⊙ 是按元素相乘。接着,我们将目标函数自变量中每个元素的学习率通过按元素运算重新调整一下:
w t ← w t − 1 − α s t + ϵ ⊙ g t {w}_{t} \leftarrow {w}_{t-1} - \frac{\alpha}{\sqrt{s_t + \epsilon}} \odot {g}_{t} wt←wt−1−st+ϵα⊙gt
其中 α {\alpha} α是学习率, ϵ {\epsilon} ϵ是为了维持数值稳定性而添加的常数,如 1 0 − 6 10^{-6} 10−6。这里开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。
理解:
- 小批量随机梯度按元素平方的累加变量 s t s_t st出现在学习率的分母项中。
- 1、如果目标函数有关自变量中某个参数的偏导数一直都较大,那么该参数的学习率将下降较快;
- 2、反之,如果目标函数有关自变量中某个参数的偏导数一直都较小,那么该参数的学习率将下降较慢。
- 然而,由于 s t s_t st一直在累加按元素平方的梯度,自变量中每个元素的学习率在迭代过程中一直在降低(或不变)。所以,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。
刚才当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。为了解决这一问题,RMSProp算法对AdaGrad算法做了一点小小的修改。
不同于AdaGrad算法里状态变量 s t s_t st是截至时间步 t t t所有小批量随机梯度 g t g_t gt按元素平方和,RMSProp(Root Mean Square Prop)算法将这些梯度按元素平方做指数加权移动平均
s d w = β s d w + ( 1 − β ) ( d w ) 2 s_{dw} = \beta s_{dw} + (1 - \beta)(dw)^2 sdw=βsdw+(1−β)(dw)2
s d b = β s d b + ( 1 − β ) ( d b ) 2 s_{db} = \beta s_{db} + (1 - \beta)(db)^2 sdb=βsdb+(1−β)(db)2
w : = w − α s d w + ϵ d w w := w - \frac{\alpha}{\sqrt{s_{dw} + \epsilon}} dw w:=w−sdw+ϵαdw
b : = b − α s d b + ϵ d b b := b - \frac{\alpha}{\sqrt{s_{db} + \epsilon}} db b:=b−sdb+ϵαdb
其中 ϵ \epsilon ϵ是一样为了维持数值稳定一个常数。最终自变量每个元素的学习率在迭代过程中就不再一直降低。RMSProp有助于减少抵达最小值路径上的摆动,并允许使用一个更大的学习率 α α α,从而加快算法学习速度。
Adam 优化算法(Adaptive Moment Estimation,自适应矩估计) 将 Momentum 和 RMSProp 算法结合在一起。Adam算法在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均。
假设用每一个 mini-batch 计算 d W dW dW、 d b db db,第 t t t次迭代时:
动量计算梯度:
v d W = β 1 v d W + ( 1 − β 1 ) d W v_{dW} = \beta_1 v_{dW} + (1 - \beta_1) dW vdW=β1vdW+(1−β1)dW
v d b = β 1 v d b + ( 1 − β 1 ) d b v_{db} = \beta_1 v_{db} + (1 - \beta_1) db vdb=β1vdb+(1−β1)db
v d W [ l ] c o r r e c t e d = v d W [ l ] 1 − ( β 1 ) t v^{corrected}_{dW^{[l]}} = \frac{v_{dW^{[l]}}}{1 - (\beta_1)^t} vdW[l]corrected=1−(β1)tvdW[l]
RMSProp计算方式:
s d W = β 2 s d W + ( 1 − β 2 ) ( d W ) 2 s_{dW} = \beta_2 s_{dW} + (1 - \beta_2) {(dW)}^2 sdW=β2sdW+(1−β2)(dW)2
s d b = β 2 s d b + ( 1 − β 2 ) ( d b ) 2 s_{db} = \beta_2 s_{db} + (1 - \beta_2) {(db)}^2 sdb=β2sdb+(1−β2)(db)2
s d W [ l ] c o r r e c t e d = s d W [ l ] 1 − ( β 2 ) t s^{corrected}_{dW^{[l]}} = \frac{s_{dW^{[l]}}}{1 - (\beta_2)^t} sdW[l]corrected=1−(β2)tsdW[l]
其中 l l l为某一层, t t t为移动平均轮次的值
Adam 算法的参数更新:
原论文中作者建议的参数设置的值:
注:β1、β2、ϵ 通常不需要调试
资料来自:Alec Radford做的实验对比
如果设置一个固定的学习率 α α α
最常用的学习率退火方法:
由于在 z = w 1 x 1 + w 2 x 2 + . . . + w n x n + b z={w}_1{x}_1+{w}_2{x}_2 + ... + {w}_n{x}_n + b z=w1x1+w2x2+...+wnxn+b 公式中,当输入的数量 n n n较大时,如果每个 w i w_i wi 的值都小一些,这样它们的和得到的 z z z 也会非常大,所以会造成我们之前介绍的梯度消失或者梯度爆炸。所以都会初始化比较小的值。
对网络输入的特征进行标准化,能够缓解梯度消失或者梯度爆炸
标准化公式: x = x − μ σ x = \frac{x-\mu}{\sigma} x=σx−μ
这个公式其实与特征工程中的处理是一样的, μ \mu μ为平均值, σ \sigma σ为标准差。标准化的目的是所有特征的平均值为0,标准差为1。这属于机器学习基本的内容不过多进行叙述。
那么这种有什么好处?主要是对于损失函数带来的好处
标准化前的损失函数:
标准化后的损失函数:
这样的话,对于梯度下降无论从哪个位置开始迭代,都能以相对较少的迭代次数找到全局最优解。可以加速网络的学习。
理解这个原理,其实还是最初的这样的公式: z = w 1 x 1 + w 2 x 2 + . . . + w n x n + b z={w}_1{x}_1+{w}_2{x}_2 + ... + {w}_n{x}_n + b z=w1x1+w2x2+...+wnxn+b
如果激活函数的输入X近似设置成均值为 0,标准方差为 1,神经元输出 z 的方差就正则化到1了。虽然没有解决梯度消失和爆炸的问题,但其在一定程度上确实减缓了梯度消失和爆炸的速度。
对以下模型进行优化,分别实现动量和Adam进行对比
# LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SIGMOID
z1 = np.dot(W1, X) + b1
a1 = relu(z1)
z2 = np.dot(W2, a1) + b2
a2 = relu(z2)
z3 = np.dot(W3, a2) + b3
a3 = sigmoid(z3)
数据为 train_X, train_Y = sklearn.datasets.make_moons(n_samples=300, noise=.2)
创建的简单样本的二分类问题。
目录结构为optimization和utils,其中utils为封装好的相关函数(如relu,前向、反向传播函数)这些不需要大家再去实现,前面已经讲解过,optimization中有主要训练逻辑,其中需要大家去进行实现两个优化算法。
动量梯度和Adam优化算法的接口实现
相关包以及封装接口导入
import numpy as np
import math
import sklearn
import sklearn.datasets
from utils import initialize_parameters, forward_propagation, compute_cost, backward_propagation
from utils import load_dataset, predict
momentum算法实现
def initialize_momentum(parameters):
"""
初始化网络中每一层的动量梯度下降的指数加权平均结果参数
parameters['W' + str(l)] = Wl
parameters['b' + str(l)] = bl
return:
v['dW' + str(l)] = velocity of dWl
v['db' + str(l)] = velocity of dbl
"""
# 得到网络的层数
L = len(parameters) // 2
v = {}
# 初始化动量参数
for l in range(L):
v["dW" + str(l + 1)] = np.zeros(parameters['W' + str(l + 1)].shape)
v["db" + str(l + 1)] = np.zeros(parameters['b' + str(l + 1)].shape)
return v
def update_parameters_with_momentum(parameters, gradients, v, beta, learning_rate):
"""
动量梯度下降算法实现
"""
# 得到网络的层数
L = len(parameters) // 2
# 动量梯度参数更新
for l in range(L):
v["dW" + str(l + 1)] = beta * v["dW" + str(l + 1)] + (1 - beta) * (gradients["dW" + str(l + 1)])
v["db" + str(l + 1)] = beta * v["db" + str(l + 1)] + (1 - beta) * (gradients["db" + str(l + 1)])
parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * v["dW" + str(l + 1)]
parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * v["db" + str(l + 1)]
return parameters, v
adam算法实现
def initialize_adam(parameters):
"""
初始化Adam算法中的参数
"""
# 得到网络的参数
L = len(parameters) // 2
v = {}
s = {}
# 利用输入,初始化参数v,s
for l in range(L):
v["dW" + str(l + 1)] = np.zeros(parameters['W' + str(l + 1)].shape)
v["db" + str(l + 1)] = np.zeros(parameters['b' + str(l + 1)].shape)
s["dW" + str(l + 1)] = np.zeros(parameters['W' + str(l + 1)].shape)
s["db" + str(l + 1)] = np.zeros(parameters['b' + str(l + 1)].shape)
return v, s
def update_parameters_with_adam(parameters, gradients, v, s, t, learning_rate=0.01,
beta1=0.9, beta2=0.999, epsilon=1e-8):
"""
更新Adam算法网络的参数
"""
# 网络大小
L = len(parameters) // 2
v_corrected = {}
s_corrected = {}
# 更新所有参数
for l in range(L):
# 对梯度进行移动平均计算. 输入: "v, gradients, beta1". 输出: "v".
# 进行动量那部分的计算
v["dW" + str(l + 1)] = beta1 * v["dW" + str(l + 1)] + (1 - beta1) * gradients["dW" + str(l + 1)]
v["db" + str(l + 1)] = beta1 * v["db" + str(l + 1)] + (1 - beta1) * gradients["db" + str(l + 1)]
# 计算修正结果. 输入: "v, beta1, t". 输出: "v_corrected".
v_corrected["dW" + str(l + 1)] = v["dW" + str(l + 1)] / (1 - np.power(beta1, t))
v_corrected["db" + str(l + 1)] = v["db" + str(l + 1)] / (1 - np.power(beta1, t))
# 平方梯度的移动平均值. 输入: "s, gradients, beta2". 输出: "s".
s["dW" + str(l + 1)] = beta2 * s["dW" + str(l + 1)] + (1 - beta2) * np.power(gradients["dW" + str(l + 1)], 2)
s["db" + str(l + 1)] = beta2 * s["db" + str(l + 1)] + (1 - beta2) * np.power(gradients["db" + str(l + 1)], 2)
# 计算修正的结果. 输入: "s, beta2, t". 输出: "s_corrected".
s_corrected["dW" + str(l + 1)] = s["dW" + str(l + 1)] / (1 - np.power(beta2, t))
s_corrected["db" + str(l + 1)] = s["db" + str(l + 1)] / (1 - np.power(beta2, t))
# 更新参数. 输入: "parameters, learning_rate, v_corrected, s_corrected, epsilon". 输出: "parameters".
parameters['W' + str(l + 1)] = parameters['W' + str(l + 1)] - learning_rate * v_corrected[
"dW" + str(l + 1)] / np.sqrt(s_corrected["dW" + str(l + 1)] + epsilon)
parameters['b' + str(l + 1)] = parameters['b' + str(l + 1)] - learning_rate * v_corrected[
"db" + str(l + 1)] / np.sqrt(s_corrected["db" + str(l + 1)] + epsilon)
return parameters, v, s
运行效果如下
# adam算法优化效果
第 0 次迭代的损失值: 0.690552
第 1000 次迭代的损失值: 0.185501
第 2000 次迭代的损失值: 0.150830
第 3000 次迭代的损失值: 0.074454
第 4000 次迭代的损失值: 0.125959
第 5000 次迭代的损失值: 0.104344
第 6000 次迭代的损失值: 0.100676
第 7000 次迭代的损失值: 0.031652
第 8000 次迭代的损失值: 0.111973
第 9000 次迭代的损失值: 0.197940
Accuracy: 0.94
# momentum算法优化效果
第 0 次迭代的损失值: 0.690741
第 1000 次迭代的损失值: 0.685341
第 2000 次迭代的损失值: 0.647145
第 3000 次迭代的损失值: 0.619594
第 4000 次迭代的损失值: 0.576665
第 5000 次迭代的损失值: 0.607324
第 6000 次迭代的损失值: 0.529476
第 7000 次迭代的损失值: 0.460936
第 8000 次迭代的损失值: 0.465780
第 9000 次迭代的损失值: 0.464740
Accuracy: 0.7966666666666666