梯度下降卡在局部最小值?

Gradient descent stuck in local minima?

提问人:Krellex 提问时间:11/15/2023 最后编辑:Krellex 更新时间:11/17/2023 访问量:171

问:

我正在运行梯度下降以找到非线性方程组的根,我想知道您如何检测该方法是否停留在局部最小值,因为我相信使用我使用的设置可能是这种情况?我的初始值是 [-2, -1],公差为 10^-2 和 20 次迭代。我读到的一件事是,如果残差开始趋于平缓或开始缓慢下降,则可能表明该方法被困在局部最小值中,但我不完全确定。我已经将我的残差及其迭代绘制为每次迭代的迭代值,我想知道我如何知道它是否停留在局部最小值。

def system(x):
    F = np.zeros((2,1), dtype=np.float64)
    F[0] = x[0]*x[0] + 2*x[1]*x[1] + math.sin(2*x[0])
    F[1] = x[0]*x[0] + math.cos(x[0]+5*x[1]) - 1.2
    return F
 
def jacb(x):
    J = np.zeros((2,2), dtype=np.float64)
    J[0,0] = 2*(x[0]+math.cos(2*x[0]))
    J[0,1] = 4*x[1]
    J[1,0] = 2*x[0]-math.sin(x[0]+5*x[1])
    J[1,1] = -5*math.sin(x[0]+5*x[1])
    return J


iterates, residuals = GradientDescent('system', 'jacb', np.array([[-2],[-1]]), 1e-2, 20, 0);

FullGradientDescent.py GradientDescentWithMomentum

我通常进行 20 次迭代测试,但我做了 200 次来说明残差的减慢enter image description here

enter image description here

Marat 建议使用 GD with momentum。 代码更改:

dn = 0
gamma = 0.8
dn_prev = 0
while (norm(F,2) > tol and n <= max_iterations):
    J = eval(jac)(x,2,fnon,F,*fnonargs)
    residuals.append(norm(F,2))
    
    dn = gamma * dn_prev+2*(np.matmul(np.transpose(J), F))
    dn_prev = dn

    lamb = 0.01       
    x = x - lamb * dn

残余使用具有动量的GDenter image description here

lastchance 建议做一个等值线图,这似乎显示了算法的行为,但它仍然没有收敛?enter image description here

python 数值方法 梯度下降 寻根

评论

0赞 Marat 11/15/2023
除非已知函数是凸函数,否则 GD 不保证转换为全局最小值。在实践中,我们用一些逃避浅层最小值的手段来支持GD,比如增加动量,正是出于这个原因。
0赞 Krellex 11/16/2023
@Marat 感谢您的回复,我已经更新了帖子以使用动力。我认为实现是正确的,残差看起来不像是卡在局部最小值?然而,它似乎仍然没有收敛
0赞 Krellex 11/16/2023
@lastchance 我将帖子更新为具有动量的 GD 的等高线图和行为,但我无法让它收敛
0赞 lastchance 11/16/2023
对不起,我意识到你正在寻找一个根,而不是最低限度。你的第一个方程可以写成 sin(2x) = -(x^2+2y^2),第二个方程可以写成 cos(x+5y)=1.2-x^2。第一个方程意味着如果有根,则 (x,y) 位于椭圆 x^2+2y^2<=1 内,并且 x 必然为负。第二个方程要求 x^2 >= 0.2。因此,如果存在根,那么它只能在 [-1,-sqrt(0.2)] 范围内有一个 x 值,在 [-sqrt(1/2),+sqrt(1/2)] 范围内有一个 y 值。你能把你的等高线图收紧到那个区域吗?

答:

1赞 lastchance 11/16/2023 #1

你的两个方程可以写成曲线y(x):

y=+-sqrt( (-sin(2x)-x^2) / 2 )

y = (arccos(1.2-x^2)-x)/5

分别是下图中的蓝线和红线。请注意,两者都有两个分支。交点是两个根。enter image description here根源可以通过多维牛顿-拉夫森找到:

import numpy as np
from math import sin, cos

#=======================================================================

def solver( eqns, x ):
    MAXITER = 1000
    TOL = 1.0e-10
    iter, residuals = 1, 1.0
    while iter <= MAXITER:
        print( x )
        F = Function( eqns, x )
        residuals = np.linalg.norm( F )
        if residuals <= TOL: break
        J = Jacobian( eqns, x )
        try: 
            deltax = np.linalg.solve( J, F )
        except np.LinAlgError:
            print( "Unable to solve" )
            break
        x = x - deltax
        iter += 1
    return x, residuals <= TOL

#=======================================================================

def Function( eqns, x ):
    F = np.zeros_like( x )
    for i, f in enumerate( eqns ): F[i] = f( x )
    return F

#=======================================================================

def Jacobian( eqns, x ):
    SMALL = 1.0e-6
    N = len( x )
    J = np.zeros( ( N, N ) )
    for i in range( N ):
       for j in range( N ):
           xplus  = x.copy();   xplus [j] = x[j] + 0.5 * SMALL
           xminus = x.copy();   xminus[j] = x[j] - 0.5 * SMALL
           J[i,j] = ( eqns[i]( xplus ) - eqns[i]( xminus ) ) / SMALL
    return J

#=======================================================================

eqns = [ lambda x: x[0] ** 2 + 2 * x[1] ** 2 + sin( 2 * x[0] ),
         lambda x: x[0] ** 2 + cos( x[0] + 5 * x[1] ) - 1.2   ]
x = np.array( [-0.8, 0.5 ] )
#x = np.array( [-0.8, -0.1 ] )          # alternative root
x, result = solver( eqns, x )
if result:
    print( "Root: ", x )
else:
    print( "No root found" )

#=======================================================================

对于第一个起点,您将获得:

[-0.8  0.5]
[-0.85082789  0.38764034]
[-0.8407059   0.37917738]
[-0.84056296  0.37906059]
[-0.84056293  0.37906057]
Root:  [-0.84056293  0.37906057]

对于第二个起点,您将获得:

[-0.8 -0.1]
[-1.01262867 -0.06737605]
[-0.96586428 -0.0665185 ]
[-0.96353139 -0.06643953]
[-0.96352546 -0.06643926]
[-0.96352546 -0.06643926]
Root:  [-0.96352546 -0.06643926]

评论

0赞 Krellex 11/16/2023
感谢您在这篇文章中的详细工作!虽然牛顿的方法可能会收敛,但我感兴趣的是如何获得梯度下降以得到解决方案,如果它不能,我试图理解为什么它不能。
0赞 lastchance 11/16/2023
@Krellex,我不太使用梯度下降,所以不容易评论。但是,如果绘制两个函数 F0 和 F1 的等值线图,您会发现它们在根部附近大约相交 90 度。因此,如果您尝试快速减少一个,那么另一个几乎保持不变。此外,F1 的变化速度要快得多,因此对下降方向的影响要大得多。最后,您是否需要小至 0.01 的 lambda?
0赞 lastchance 11/16/2023
我也无法运行您链接到的 Python 代码,因此很难测试。您能否检查是否上传了最新的运行代码。
0赞 Krellex 11/16/2023
对不起。尝试运行时遇到什么错误?
0赞 lastchance 11/16/2023
好吧,首先我需要显式导入数学。在那之后,我收到一个弃用警告(在这里可能很重要),你设置了 F[]: 实际的失败错误是Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation.arr, residuals = GradientDescent('system', 'jacb', np.array([[-2],[-1]]), 1e-2, 200, 0); in GradientDescent J = eval(jac)(x,2,fnon,F,*fnonargs) TypeError: jacb() takes 1 positional argument but 4 were given
1赞 jlandercy 11/16/2023 #2

按如下方式重写系统:

import numpy as np
from scipy import optimize
import matplotlib.pyplot as plt

def system(x):
    return np.array([
        x[0]*x[0] + 2*x[1]*x[1] + np.sin(2*x[0]),
        x[0]*x[0] + np.cos(x[0] + 5*x[1]) - 1.2
    ])

创建一些空间来绘制最小值周围的轮廓:

xlin = np.linspace(-2, 2, 200)
ylin = np.linspace(-2, 2, 200)
X, Y = np.meshgrid(xlin, ylin)
Z = system([X, Y])

围绕您确定的潜在解决方案求解系统:

solutions = np.array([
    optimize.fsolve(system, x0=[0, 0]),
    optimize.fsolve(system, x0=[-1, 0]),
    optimize.fsolve(system, x0=[-1, 0.5])
])

# array([[ 0.        ,  0.        ],
#       [-0.96352546, -0.06643926],
#       [-0.84056293,  0.37906057]])

确认返回的点实际上是解决方案:

system(solutions.T).T

# array([[ 0.00000000e+00, -2.00000000e-01],   # Not converged
#       [ 8.88178420e-16,  6.66133815e-16],
#       [-2.22044605e-15, -4.88498131e-15]])

绘制整个过程以确认解决方案:

fig, axe = plt.subplots()
axe.contour(X, Y, np.log10(np.sqrt(Z[0]**2 + Z[1]**2)), 30, cmap="jet")
axe.plot(*solutions.T, linestyle="none", marker="o")
axe.grid()

enter image description here

第一点不是解决方案,求解器失败:(0,0)

optimize.fsolve(system, x0=[0, 0], full_output=True)
# (array([0., 0.]),
# {'nfev': 15,
#  'fjac': array([[-1.00000000e+00, -2.54340978e-08],
#         [ 2.54340978e-08, -1.00000000e+00]]),
#  'r': array([-3.12495738e+12,  2.10079410e+06, -5.44853621e-02]),
#  'qtf': array([5.08681955e-09, 2.00000000e-01]),
#  'fvec': array([ 0. , -0.2])},
# 5,
# 'The iteration is not making good progress, as measured by the improvement from the last ten iterations.')

基于文档

解决方案(或不成功的最后一次迭代的结果 调用)。

根据这些信息,您可以确认您的代码确实在域上找到了系统的两个根之一,额外的临界点不是解决方案。(0,0)