5分鐘快速掌握Adam優化算法
梯度下降是一種優化算法,遵循目標函數的負梯度以定位函數的最小值。
梯度下降的局限性是,所有輸入變量都使用單個步長(學習率)。像AdaGrad和RMSProp這樣的梯度下降的擴展會更新算法,以對每個輸入變量使用單獨的步長,但可能會導致步長迅速減小到非常小的值。自適應運動估計算法(Adam)是梯度下降的擴展,是AdaGrad和RMSProp等技術的自然繼承者,該技術可自動為目標函數的每個輸入變量調整學習率,并通過使用以指數方式降低梯度的移動平均值以更新變量。
在本教程中,您將發現如何從頭開始使用Adam優化算法開發梯度下降。完成本教程后,您將知道:
- 梯度下降是一種優化算法,它使用目標函數的梯度來導航搜索空間。
- 可以通過使用稱為Adam的偏導數的遞減平均值,將梯度下降更新為對每個輸入變量使用自動自適應步長。
- 如何從頭開始實施Adam優化算法并將其應用于目標函數并評估結果。
教程概述
本教程分為三個部分:他們是:
- 梯度下降
- Adam優化算法
- Adam梯度下降
二維測試問題
Adam的梯度下降優化
Adam可視化
梯度下降
梯度下降是一種優化算法。它在技術上稱為一階優化算法,因為它明確利用了目標目標函數的一階導數。一階導數,或簡稱為“導數”,是目標函數在特定點(例如,點)上的變化率或斜率。用于特定輸入。如果目標函數采用多個輸入變量,則將其稱為多元函數,并且可以將輸入變量視為向量。反過來,多元目標函數的導數也可以視為向量,通常稱為梯度。
梯度:多元目標函數的一階導數。
對于特定輸入,導數或梯度指向目標函數最陡峭的上升方向。
梯度下降是指一種最小化優化算法,該算法遵循目標函數的下坡梯度負值來定位函數的最小值。梯度下降算法需要一個正在優化的目標函數和該目標函數的導數函數。目標函數f()返回給定輸入集合的分數,導數函數f'()給出給定輸入集合的目標函數的導數。梯度下降算法需要問題中的起點(x),例如輸入空間中的隨機選擇點。
假設我們正在最小化目標函數,然后計算導數并在輸入空間中采取一步,這將導致目標函數下坡運動。下坡運動是通過首先計算輸入空間中的運動量來進行的,計算方法是將步長(稱為alpha或學習率)乘以坡度。然后從當前點減去該值,以確保我們逆梯度移動或向下移動目標函數。
x(t)= x(t-1)–step* f'(x(t-1))
在給定點的目標函數越陡峭,梯度的幅度越大,反過來,在搜索空間中采取的步伐也越大。使用步長超參數來縮放步長的大小。
步長(alpha):超參數,控制算法每次迭代時相對于梯度在搜索空間中移動多遠。
如果步長太小,則搜索空間中的移動將很小,并且搜索將花費很長時間。如果步長太大,則搜索可能會在搜索空間附近反彈并跳過最優值。
現在我們已經熟悉了梯度下降優化算法,下面讓我們看一下Adam算法。
Adam優化算法
自適應運動估計算法(簡稱“Adam”)是梯度下降優化算法的擴展。Diederik Kingma和Jimmy Lei Ba在2014年發表的題為“Adam:隨機優化方法”的論文中描述了該算法。Adam旨在加速優化過程,例如減少達到最佳狀態所需的功能評估次數,或提高優化算法的功能,例如產生更好的最終結果。這是通過為每個要優化的輸入參數計算步長來實現的。重要的是,每個步長都將根據每個變量遇到的梯度(偏導數)自動調整搜索過程的吞吐量。
讓我們逐步介紹該算法的每個元素。首先,對于作為搜索一部分而被優化的每個參數,我們必須維持一個矩矢量和指數加權無窮大范數,分別稱為m和v(真是希臘字母nu)。在搜索開始時將它們初始化為0.0。
m = 0
v = 0
該算法在從t=1開始的時間t內迭代執行,并且每次迭代都涉及計算一組新的參數值x,例如。從x(t-1)到x(t)。如果我們專注于更新一個參數,這可能很容易理解該算法,該算法概括為通過矢量運算來更新所有參數。首先,計算當前時間步長的梯度(偏導數)。
g(t)= f'(x(t-1))
接下來,使用梯度和超參數beta1更新第一時刻。
m(t)= beta1 * m(t-1)+(1 – beta1)* g(t)
然后,使用平方梯度和超參數beta2更新第二時刻。
v(t)= beta2 * v(t-1)+(1 – beta2)* g(t)^ 2
由于第一和第二力矩是用零值初始化的,所以它們是有偏的。接下來,對第一力矩和第二力矩進行偏差校正,并以第一力矩為起點:
mhat(t)= m(t)/(1 – beta1(t))
然后第二個時刻:
vhat(t)= v(t)/(1 – beta2(t))
注意,beta1(t)和beta2(t)指的是beta1和beta2超參數,它們在算法的迭代過程中按時間表衰減。可以使用靜態衰減時間表,盡管該論文建議以下內容:
beta1(t)= beta1 ^ t
beta2(t)= beta2 ^ t
最后,我們可以為該迭代計算參數的值。
x(t)= x(t-1)– alpha * mhat(t)/(sqrt(vhat(t))+ eps)
其中alpha是步長超參數,eps是一個較小的值(epsilon),例如1e-8,可確保我們不會遇到被零除的誤差,而sqrt()是平方根函數。
注意,可以使用對本文中列出的更新規則進行更有效的重新排序:
alpha(t)= alpha * sqrt(1 – beta2(t))/(1 – beta1(t)) x(t)= x(t-1)– alpha(t)* m(t)/(sqrt(v(t))+ eps)
回顧一下,該算法有三個超參數,它們是:
- alpha:初始步長(學習率),典型值為0.001。
- beta1:第一個動量的衰減因子,典型值為0.9。
- beta2:無窮大范數的衰減因子,典型值為0.999。
接下來,讓我們看看如何在Python中從頭開始實現該算法。
Adam梯度下降
在本節中,我們將探討如何使用Adam實現梯度下降優化算法。
二維測試問題
首先,讓我們定義一個優化函數。我們將使用一個簡單的二維函數,該函數將每個維的輸入平方,并定義有效輸入的范圍(從-1.0到1.0)。
下面的Objective()函數實現了此功能
- # objective function
- def objective(x, y):
- return x**2.0 + y**2.0
我們可以創建數據集的三維圖,以了解響應面的曲率。下面列出了繪制目標函數的完整示例。
- # 3d plot of the test function
- from numpy import arange
- from numpy import meshgrid
- from matplotlib import pyplot
- # objective function
- def objective(x, y):
- return x**2.0 + y**2.0
- # define range for input
- r_min, r_max = -1.0, 1.0
- # sample input range uniformly at 0.1 increments
- xaxis = arange(r_min, r_max, 0.1)
- yaxis = arange(r_min, r_max, 0.1)
- # create a mesh from the axis
- x, y = meshgrid(xaxis, yaxis)
- # compute targets
- results = objective(x, y)
- # create a surface plot with the jet color scheme
- figure = pyplot.figure()
- axis = figure.gca(projection='3d')
- axis.plot_surface(x, y, results, cmap='jet')
- # show the plot
- pyplot.show()
運行示例將創建目標函數的三維表面圖。我們可以看到全局最小值為f(0,0)= 0的熟悉的碗形狀。
我們還可以創建函數的二維圖。這在以后要繪制搜索進度時會很有幫助。下面的示例創建目標函數的輪廓圖。
- # contour plot of the test function
- from numpy import asarray
- from numpy import arange
- from numpy import meshgrid
- from matplotlib import pyplot
- # objective function
- def objective(x, y):
- return x**2.0 + y**2.0
- # define range for input
- bounds = asarray([[-1.0, 1.0], [-1.0, 1.0]])
- # sample input range uniformly at 0.1 increments
- xaxis = arange(bounds[0,0], bounds[0,1], 0.1)
- yaxis = arange(bounds[1,0], bounds[1,1], 0.1)
- # create a mesh from the axis
- x, y = meshgrid(xaxis, yaxis)
- # compute targets
- results = objective(x, y)
- # create a filled contour plot with 50 levels and jet color scheme
- pyplot.contourf(x, y, results, levels=50, cmap='jet')
- # show the plot
- pyplot.show()
運行示例將創建目標函數的二維輪廓圖。我們可以看到碗的形狀被壓縮為以顏色漸變顯示的輪廓。我們將使用該圖來繪制在搜索過程中探索的特定點。
現在我們有了一個測試目標函數,讓我們看一下如何實現Adam優化算法。
Adam梯度下降優化
我們可以將帶有Adam的梯度下降應用于測試問題。首先,我們需要一個函數來計算此函數的導數。
f(x)= x ^ 2
f'(x)= x * 2
x ^ 2的導數在每個維度上均為x * 2。 derived()函數在下面實現了這一點。
- # derivative of objective function
- def derivative(x, y):
- return asarray([x * 2.0, y * 2.0])
接下來,我們可以實現梯度下降優化。首先,我們可以選擇問題范圍內的隨機點作為搜索的起點。假定我們有一個數組,該數組定義搜索范圍,每個維度一行,并且第一列定義最小值,第二列定義維度的最大值。
- # generate an initial point
- x = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
- score = objective(x[0], x[1])
接下來,我們需要將第一時刻和第二時刻初始化為零。
- # initialize first and second moments
- m = [0.0 for _ in range(bounds.shape[0])]
- v = [0.0 for _ in range(bounds.shape[0])]
然后,我們運行由“ n_iter”超參數定義的算法的固定迭代次數。
- ...
- # run iterations of gradient descent
- for t in range(n_iter):
- ...
第一步是使用導數()函數計算當前解決方案的梯度。
- # calculate gradient
- gradient = derivative(solution[0], solution[1])
第一步是計算當前參數集的導數。
- # calculate gradient g(t)
- g = derivative(x[0], x[1])
接下來,我們需要執行Adam更新計算。為了提高可讀性,我們將使用命令式編程樣式一次執行一個變量的這些計算。
在實踐中,我建議使用NumPy向量運算以提高效率。
- ...
- # build a solution one variable at a time
- for i in range(x.shape[0]):
- ...
首先,我們需要計算力矩。
- # m(t) = beta1 * m(t-1) + (1 - beta1) * g(t)
- m[i] = beta1 * m[i] + (1.0 - beta1) * g[i]
然后是第二個時刻。
- # v(t) = beta2 * v(t-1) + (1 - beta2) * g(t)^2
- v[i] = beta2 * v[i] + (1.0 - beta2) * g[i]**2
然后對第一和第二時刻進行偏差校正。
- # mhat(t) = m(t) / (1 - beta1(t))
- mmhat = m[i] / (1.0 - beta1**(t+1))
- # vhat(t) = v(t) / (1 - beta2(t))
- vvhat = v[i] / (1.0 - beta2**(t+1))
然后最后是更新的變量值。
- # x(t) = x(t-1) - alpha * mhat(t) / (sqrt(vhat(t)) + eps)
- x[i] = x[i] - alpha * mhat / (sqrt(vhat) + eps)
然后,針對要優化的每個參數重復此操作。在迭代結束時,我們可以評估新的參數值并報告搜索的性能。
- # evaluate candidate point
- score = objective(x[0], x[1])
- # report progress
- print('>%d f(%s) = %.5f' % (t, x, score))
我們可以將所有這些結合到一個名為adam()的函數中,該函數采用目標函數和派生函數的名稱以及算法超參數,并返回在搜索及其評估結束時找到的最佳解決方案。
下面列出了完整的功能。
- # gradient descent algorithm with adam
- def adam(objective, derivative, bounds, n_iter, alpha, beta1, beta2, eps=1e-8):
- # generate an initial point
- x = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
- score = objective(x[0], x[1])
- # initialize first and second moments
- m = [0.0 for _ in range(bounds.shape[0])]
- v = [0.0 for _ in range(bounds.shape[0])]
- # run the gradient descent updates
- for t in range(n_iter):
- # calculate gradient g(t)
- g = derivative(x[0], x[1])
- # build a solution one variable at a time
- for i in range(x.shape[0]):
- # m(t) = beta1 * m(t-1) + (1 - beta1) * g(t)
- m[i] = beta1 * m[i] + (1.0 - beta1) * g[i]
- # v(t) = beta2 * v(t-1) + (1 - beta2) * g(t)^2
- v[i] = beta2 * v[i] + (1.0 - beta2) * g[i]**2
- # mhat(t) = m(t) / (1 - beta1(t))
- mmhat = m[i] / (1.0 - beta1**(t+1))
- # vhat(t) = v(t) / (1 - beta2(t))
- vvhat = v[i] / (1.0 - beta2**(t+1))
- # x(t) = x(t-1) - alpha * mhat(t) / (sqrt(vhat(t)) + eps)
- x[i] = x[i] - alpha * mhat / (sqrt(vhat) + eps)
- # evaluate candidate point
- score = objective(x[0], x[1])
- # report progress
- print('>%d f(%s) = %.5f' % (t, x, score))
- return [x, score]
注意:為了提高可讀性,我們有意使用列表和命令式編碼樣式,而不是矢量化操作。隨意將實現改編為帶有NumPy數組的矢量化實現,以實現更好的性能。
然后,我們可以定義我們的超參數并調用adam()函數來優化我們的測試目標函數。
在這種情況下,我們將使用算法的60次迭代,初始步長為0.02,beta1和beta2值分別為0.8和0.999。經過一些反復試驗后,發現了這些超參數值。
- # seed the pseudo random number generator
- seed(1)
- # define range for input
- bounds = asarray([[-1.0, 1.0], [-1.0, 1.0]])
- # define the total iterations
- n_iter = 60
- # steps size
- alpha = 0.02
- # factor for average gradient
- beta1 = 0.8
- # factor for average squared gradient
- beta2 = 0.999
- # perform the gradient descent search with adam
- best, score = adam(objective, derivative, bounds, n_iter, alpha, beta1, beta2)
- print('Done!')
- print('f(%s) = %f' % (best, score))
綜合所有這些,下面列出了使用Adam進行梯度下降優化的完整示例。
- # gradient descent optimization with adam for a two-dimensional test function
- from math import sqrt
- from numpy import asarray
- from numpy.random import rand
- from numpy.random import seed
- # objective function
- def objective(x, y):
- return x**2.0 + y**2.0
- # derivative of objective function
- def derivative(x, y):
- return asarray([x * 2.0, y * 2.0])
- # gradient descent algorithm with adam
- def adam(objective, derivative, bounds, n_iter, alpha, beta1, beta2, eps=1e-8):
- # generate an initial point
- x = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
- score = objective(x[0], x[1])
- # initialize first and second moments
- m = [0.0 for _ in range(bounds.shape[0])]
- v = [0.0 for _ in range(bounds.shape[0])]
- # run the gradient descent updates
- for t in range(n_iter):
- # calculate gradient g(t)
- g = derivative(x[0], x[1])
- # build a solution one variable at a time
- for i in range(x.shape[0]):
- # m(t) = beta1 * m(t-1) + (1 - beta1) * g(t)
- m[i] = beta1 * m[i] + (1.0 - beta1) * g[i]
- # v(t) = beta2 * v(t-1) + (1 - beta2) * g(t)^2
- v[i] = beta2 * v[i] + (1.0 - beta2) * g[i]**2
- # mhat(t) = m(t) / (1 - beta1(t))
- mmhat = m[i] / (1.0 - beta1**(t+1))
- # vhat(t) = v(t) / (1 - beta2(t))
- vvhat = v[i] / (1.0 - beta2**(t+1))
- # x(t) = x(t-1) - alpha * mhat(t) / (sqrt(vhat(t)) + eps)
- x[i] = x[i] - alpha * mhat / (sqrt(vhat) + eps)
- # evaluate candidate point
- score = objective(x[0], x[1])
- # report progress
- print('>%d f(%s) = %.5f' % (t, x, score))
- return [x, score]
- # seed the pseudo random number generator
- seed(1)
- # define range for input
- bounds = asarray([[-1.0, 1.0], [-1.0, 1.0]])
- # define the total iterations
- n_iter = 60
- # steps size
- alpha = 0.02
- # factor for average gradient
- beta1 = 0.8
- # factor for average squared gradient
- beta2 = 0.999
- # perform the gradient descent search with adam
- best, score = adam(objective, derivative, bounds, n_iter, alpha, beta1, beta2)
- print('Done!')
- print('f(%s) = %f' % (best, score))
運行示例將Adam優化算法應用于我們的測試問題,并報告算法每次迭代的搜索性能。
注意:由于算法或評估程序的隨機性,或者數值精度的差異,您的結果可能會有所不同。考慮運行該示例幾次并比較平均結果。
在這種情況下,我們可以看到在搜索53次迭代后找到了接近最佳的解決方案,輸入值接近0.0和0.0,評估為0.0。
- >50 f([-0.00056912 -0.00321961]) = 0.00001
- >51 f([-0.00052452 -0.00286514]) = 0.00001
- >52 f([-0.00043908 -0.00251304]) = 0.00001
- >53 f([-0.0003283 -0.00217044]) = 0.00000
- >54 f([-0.00020731 -0.00184302]) = 0.00000
- >55 f([-8.95352320e-05 -1.53514076e-03]) = 0.00000
- >56 f([ 1.43050285e-05 -1.25002847e-03]) = 0.00000
- >57 f([ 9.67123406e-05 -9.89850279e-04]) = 0.00000
- >58 f([ 0.00015359 -0.00075587]) = 0.00000
- >59 f([ 0.00018407 -0.00054858]) = 0.00000
- Done!
- f([ 0.00018407 -0.00054858]) = 0.000000
Adam可視化
我們可以在域的輪廓圖上繪制Adam搜索的進度。這可以為算法迭代過程中的搜索進度提供直觀的認識。我們必須更新adam()函數以維護在搜索過程中找到的所有解決方案的列表,然后在搜索結束時返回此列表。下面列出了具有這些更改的功能的更新版本。
- # gradient descent algorithm with adam
- def adam(objective, derivative, bounds, n_iter, alpha, beta1, beta2, eps=1e-8):
- solutions = list()
- # generate an initial point
- x = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
- score = objective(x[0], x[1])
- # initialize first and second moments
- m = [0.0 for _ in range(bounds.shape[0])]
- v = [0.0 for _ in range(bounds.shape[0])]
- # run the gradient descent updates
- for t in range(n_iter):
- # calculate gradient g(t)
- g = derivative(x[0], x[1])
- # build a solution one variable at a time
- for i in range(bounds.shape[0]):
- # m(t) = beta1 * m(t-1) + (1 - beta1) * g(t)
- m[i] = beta1 * m[i] + (1.0 - beta1) * g[i]
- # v(t) = beta2 * v(t-1) + (1 - beta2) * g(t)^2
- v[i] = beta2 * v[i] + (1.0 - beta2) * g[i]**2
- # mhat(t) = m(t) / (1 - beta1(t))
- mmhat = m[i] / (1.0 - beta1**(t+1))
- # vhat(t) = v(t) / (1 - beta2(t))
- vvhat = v[i] / (1.0 - beta2**(t+1))
- # x(t) = x(t-1) - alpha * mhat(t) / (sqrt(vhat(t)) + ep)
- x[i] = x[i] - alpha * mhat / (sqrt(vhat) + eps)
- # evaluate candidate point
- score = objective(x[0], x[1])
- # keep track of solutions
- solutions.append(x.copy())
- # report progress
- print('>%d f(%s) = %.5f' % (t, x, score))
- return solutions
然后,我們可以像以前一樣執行搜索,這一次將檢索解決方案列表,而不是最佳的最終解決方案。
- # seed the pseudo random number generator
- seed(1)
- # define range for input
- bounds = asarray([[-1.0, 1.0], [-1.0, 1.0]])
- # define the total iterations
- n_iter = 60
- # steps size
- alpha = 0.02
- # factor for average gradient
- beta1 = 0.8
- # factor for average squared gradient
- beta2 = 0.999
- # perform the gradient descent search with adam
- solutions = adam(objective, derivative, bounds, n_iter, alpha, beta1, beta2)
然后,我們可以像以前一樣創建目標函數的輪廓圖。
- # sample input range uniformly at 0.1 increments
- xaxis = arange(bounds[0,0], bounds[0,1], 0.1)
- yaxis = arange(bounds[1,0], bounds[1,1], 0.1)
- # create a mesh from the axis
- x, y = meshgrid(xaxis, yaxis)
- # compute targets
- results = objective(x, y)
- # create a filled contour plot with 50 levels and jet color scheme
- pyplot.contourf(x, y, results, levels=50, cmap='jet')
最后,我們可以將在搜索過程中找到的每個解決方案繪制成一條由一條線連接的白點。
- # plot the sample as black circles
- solutions = asarray(solutions)
- pyplot.plot(solutions[:, 0], solutions[:, 1], '.-', color='w')
綜上所述,下面列出了對測試問題執行Adam優化并將結果繪制在輪廓圖上的完整示例。
- # example of plotting the adam search on a contour plot of the test function
- from math import sqrt
- from numpy import asarray
- from numpy import arange
- from numpy.random import rand
- from numpy.random import seed
- from numpy import meshgrid
- from matplotlib import pyplot
- from mpl_toolkits.mplot3d import Axes3D
- # objective function
- def objective(x, y):
- return x**2.0 + y**2.0
- # derivative of objective function
- def derivative(x, y):
- return asarray([x * 2.0, y * 2.0])
- # gradient descent algorithm with adam
- def adam(objective, derivative, bounds, n_iter, alpha, beta1, beta2, eps=1e-8):
- solutions = list()
- # generate an initial point
- x = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
- score = objective(x[0], x[1])
- # initialize first and second moments
- m = [0.0 for _ in range(bounds.shape[0])]
- v = [0.0 for _ in range(bounds.shape[0])]
- # run the gradient descent updates
- for t in range(n_iter):
- # calculate gradient g(t)
- g = derivative(x[0], x[1])
- # build a solution one variable at a time
- for i in range(bounds.shape[0]):
- # m(t) = beta1 * m(t-1) + (1 - beta1) * g(t)
- m[i] = beta1 * m[i] + (1.0 - beta1) * g[i]
- # v(t) = beta2 * v(t-1) + (1 - beta2) * g(t)^2
- v[i] = beta2 * v[i] + (1.0 - beta2) * g[i]**2
- # mhat(t) = m(t) / (1 - beta1(t))
- mmhat = m[i] / (1.0 - beta1**(t+1))
- # vhat(t) = v(t) / (1 - beta2(t))
- vvhat = v[i] / (1.0 - beta2**(t+1))
- # x(t) = x(t-1) - alpha * mhat(t) / (sqrt(vhat(t)) + ep)
- x[i] = x[i] - alpha * mhat / (sqrt(vhat) + eps)
- # evaluate candidate point
- score = objective(x[0], x[1])
- # keep track of solutions
- solutions.append(x.copy())
- # report progress
- print('>%d f(%s) = %.5f' % (t, x, score))
- return solutions
- # seed the pseudo random number generator
- seed(1)
- # define range for input
- bounds = asarray([[-1.0, 1.0], [-1.0, 1.0]])
- # define the total iterations
- n_iter = 60
- # steps size
- alpha = 0.02
- # factor for average gradient
- beta1 = 0.8
- # factor for average squared gradient
- beta2 = 0.999
- # perform the gradient descent search with adam
- solutions = adam(objective, derivative, bounds, n_iter, alpha, beta1, beta2)
- # sample input range uniformly at 0.1 increments
- xaxis = arange(bounds[0,0], bounds[0,1], 0.1)
- yaxis = arange(bounds[1,0], bounds[1,1], 0.1)
- # create a mesh from the axis
- x, y = meshgrid(xaxis, yaxis)
- # compute targets
- results = objective(x, y)
- # create a filled contour plot with 50 levels and jet color scheme
- pyplot.contourf(x, y, results, levels=50, cmap='jet')
- # plot the sample as black circles
- solutions = asarray(solutions)
- pyplot.plot(solutions[:, 0], solutions[:, 1], '.-', color='w')
- # show the plot
- pyplot.show()
運行示例將像以前一樣執行搜索,但是在這種情況下,將創建目標函數的輪廓圖。
在這種情況下,我們可以看到在搜索過程中找到的每個解決方案都顯示一個白點,從最優點開始,逐漸靠近圖中心的最優點。