梯度下降算法的Python實現

梯度下降算法的Python實現

http://yphuang.github.io/blog/2016/03/17/Gradient-Descent-Algorithm-Implementation-in-Python/

1.梯度下降算法的理解

我們在求解最優化問題的時候,需要最小化或最大化某一個目標函數。如線性迴歸中,就需要最小化殘差平方和。

某一向量的平方和函數可定義如下:

def sum_of_squares(v):
    """computes the sum of squared elements in v"""
    return sum(v_i ** 2 for v_i in v)

梯度定義

f(x,y,z)f(x,y,z)在點P0(x0,y0,z0)P0(x0,y0,z0)存在對所有自變量的偏導數,則稱向量(fx(P0),fy(P0),fz(P0))(fx(P0),fy(P0),fz(P0))爲函數ff在點P0P0梯度,記爲:

grad f=(fx(P0),fy(P0),fz(P0))grad f=(fx(P0),fy(P0),fz(P0))

由於某一方向上ll的方向導數公式可以寫成

fl(P0)=|grad f(P0)|cosθfl(P0)=|grad f(P0)|cosθ

其中,θθ爲梯度向量grad f(P0)grad f(P0)ll方向上的單位向量l0l0的夾角。

從而,當ff可微時,ffP0P0的梯度方向是ff的值增長最快的方向。

梯度下降算法的直觀理解

由於梯度方向是使得函數值下降最快的方向,從而快速求解到目標函數最小值的一種途徑就是:

  1. 隨機選擇一個起點,計算梯度;
  2. 朝着負梯度(即使得該點目標函數值下降最快的方向)移動一小步;
  3. 重新計算新的位置的梯度,繼續朝梯度方向移動一小步
  4. 如此循環,直到目標函數值的變化達到某一閾值或者最大迭代次數。

梯度下降算法的侷限性

以求解目標函數最小化爲例,梯度下降算法可能存在一下幾種情況:

  • 當目標函數存在全局最小值時,這種方法可以快速的找到最優解;
  • 當目標函數存在多個局部最小值時,可能會陷入局部最優解。因此需要從多個隨機的起點開始解的搜索。
  • 當目標函數不存在最小值點,則可能陷入無限循環。因此,有必要設置最大迭代次數。

2.梯度的估計

梯度的精確計算,需要對目標函數求偏導。

這裏,對於一元函數的梯度計算,我們可以採用如下函數進行估計:

from __future__ import division

def difference_quotient(f,x,h):
    return (f(x+h)-f(x))/h

其中,h0h→0.

對於多元函數,我們可以如下定義梯度估計函數:

# 先定義偏導估計函數
def partial_difference_quotient(f,v,i,h):
    """compute the i-th partial difference quotient of f at v"""
    w = [v_j + (h if j==i else 0)
            for j,v_j in enumerate(v)]
    return (f(w)-f(v))/h
# 再定義梯度估計函數
def estimate_gradient(f,v,h=0.00001):
    return [partial_difference_quotient(f,v,i,h)
           for i,_ in enumerate(v)]

3.梯度的使用

這裏,先通過一個簡單的例子測算梯度下降算法是否有效。

假設我們要求解的目標函數是:Min f(X)=XTXMin f(X)=XTX,其中,XX爲一個三維的向量。則其梯度爲2X2X,不難計算出其最小值點爲[0,0,0][0,0,0].

下面,通過梯度下降法來求解。

### 定義步長函數
def step(v,direction,step_size):
    """move step_size in the direction from v"""
    return [v_i + step_size * direction_i 
                   for v_i,direction_i in zip(v,direction)]
### 定義梯度
def sum_of_squares_gradient(v):
    return [2 * v_i for v_i in v]
import random

### 選擇一個隨機的起點
v = [random.randint(-10,10) for i in range(3)]

### 定義向量距離
#######################
#### 減法定義
def vector_substract(v,w):
    """substracts coresponding elements"""
    return [v_i - w_i
                   for v_i,w_i in zip(v,w)]

### 向量的點乘
def dot(v,w):
    return sum(v_i * w_i 
                      for v_i,w_i in zip(v,w))


### 向量的平房和
def sum_of_squares(v):
    """v_1*v_1+v_2*v_2+...+v_n*v_n"""
    return dot(v,v)


### 向量的距離
##### method 1:
def distance(v,w):
    """"""
    return sum_of_squares(vector_substract(v,w))

### 進行迭代計算
tolerance = 0.000000001
max_iter = 1000000
iter = 1

while True:
    gradient = sum_of_squares_gradient(v)
    next_v = step(v,gradient,-0.01)
    if (distance(next_v,v) < tolerance) or (iter > max_iter) :
        break
    v = next_v
    iter += 1
    
print v,iter
[0.0007944685438825013, -0.00026482284796083356, -0.0013241142398041702] 443

4.選擇合適的步長

雖然我們已經明確要沿着梯度的方向移動,然而移動的步長究竟爲多少比較合適,卻沒有一個通用的標準。因此,步長的選擇是一門基於經驗的藝術。

以下是幾種主流的步長選擇方法:

  • 使用固定步長;
  • 逐步壓縮步長;
  • 每一步的步長選擇通過最小化目標函數來確定。

最後一種似乎聽起來較爲合理,但是也是最爲耗時的。我們可以退而求其次:每一步的迭代步長通過從幾個有限離散的候選步長中來確定。

step_sizes = [100,10,1,0.1,0.01,0.001,0.0001,0.00001]

但是步長的選擇範圍過廣也可能造成另一個問題:自變量的輸入值可能超過定義域的範圍。因此,需要定義一個safe函數來避免這種情況的出現。假設我們要求的是目標函數最小化,則可以將『自變量的輸入值超過定義域的範圍』這種情況的目標函數值設爲無窮大。從而使得這種步長不會被考慮。

def safe(f):
    def safe_f(*args,**kwargs):
        try:
            return f(*args,**kwargs)
        except:
            return float('inf')
    return safe_f


5.整合

函數定義

假設我們的目標函數的定義爲最小化target_fn函數,參數的初始值爲theta_0,則整個梯度下降算法的實現函數如下:

def minimize_batch(target_fn,gradient_fn,theta_0,tolerance = 0.000001):
    """using gradient descent to find theta that minimizes target function"""
    step_sizes = [100,10,1,0.1,0.01,0.001,0.0001,0.00001]
    
    theta = theta_0
    target_fn = safe(target_fn)
    value = target_fn(theta)
    
    while True:
        gradient = gradient_fn(theta)
        next_thetas = [step(theta,gradient,-step_size)
                          for step_size in step_sizes]
        
    #選擇最小化目標函數的theta
    next_theta = min(next_thetas,key = target_fn)
    next_value = target_fn(next_theta)
    
    #停止準則
    if abs((value - next_value)/value) < tolerance:
        return theta,value
    
    else:
        theta,value = next_theta,next_value

測試

#
max_iter = 1000
iter = 1
theta_0 = [random.randint(-10,10) for i in range(3)]

while True:
    theta,value = minimize_batch(target_fn = sum_of_squares,gradient_fn = sum_of_squares_gradient,theta_0 = theta_0,tolerance = 0.0001)
    if (iter < max_iter) or (value == sum_of_squares(theta_0)):
        break
    theta_0 = theta
    iter+=1

    
print theta,iter

當我們的優化問題是最大化目標函數時,可做如下修改。

def negate(f):
    """return a function that for any input x returns -f(x)"""
    return lambda *args,**lwargs:-f(*args,**kwargs)


def negate_all(f):
    """the same when f returns a list of numbers"""
    return lambda *args,**kwargs:[-y for y in f(*args,**kwargs)]

def maximize_batch(target_fn,gradient_fn,theta_0,tolerance = 0.000001):
    return minimize_batch(negate(target_fn),
                         negate_all(gradient_fn),
                         theta_0,
                         tolerance)

6.隨機梯度下降

很多時候,我們要處理的目標函數是線性可加的,即目標函數形式爲:

f(ω)=i=1nfi(ω,xi,yi)f(ω)=∑i=1nfi(ω,xi,yi)

此時,梯度下降算法的迭代公式爲:

ωt+1=ωtηt+1i=1nfi(ωt,xi,yi)ωt+1=ωt−ηt+1∑i=1n▽fi(ωt,xi,yi)

如果我們面對的是一個nn非常大的數據集,在每一步的迭代中,由於要計算所有點的梯度fi▽fi,這樣會非常耗時。

而隨機梯度下降算法的基本思想是:每次迭代時,隨機選擇一個點的梯度fi▽fi來代替f▽f.此時,有:

ωt+1=ωtηt+1fik(ωt,xik,yik),ik1,2,3,...,nωt+1=ωt−ηt+1▽fik(ωt,xik,yik),ik∈1,2,3,...,n

之所以可以這麼做,是因爲E[fik(ωt,xik,yik)]=f(ωt)E[▽fik(ωt,xik,yik)]=▽f(ωt)。當ηt=O(1/t)ηt=O(1/t),算法在期望的意義下收斂。

import random

def in_random_order(data):
    """generator that returns the elements of data in random order"""
    indexes = [i for i,_ in enumerate(data)]
    random.shuffle(indexes)
    for i in indexes:
        yield data[i]
??random.shuffle

爲了避免陷入無限循環,當目標函數沒有得到優化時,則逐步縮短步長。

def minimize_stochastic(target_fn,gradient_fn,x,y,theta_0,alpha_0=0.01):
    data = zip(x,y)
    theta = theta_0
    alpha = alpha_0   # 初始化步長
    min_theta, min_value = None,float("inf")
    iterations_with_no_improvment = 0
    
    #當迭代100次均沒有提升,則停止
    while iterations_with_no_improvment < 100:
        value = sum(target_fn(x_i,y_i,theta) for x_i,y_i in data)
        
        if value < min_value:
            #found a new minimum,remember it
            min_theta,min_value = theta,value
            iterations_with_no_improvment = 0
            alpha= alpha_0
        else:
            iterations_with_no_improvment+=1
            alpha *=0.9
            
        #沿着梯度移動一步
        for x_i,y_i in in_random_order(data):
            gradient_i = gradient_fn(x_i,y_i,theta)
            theta = vector_substract(theta,scalar_multiply(alpha,gradient_i))
            
    return min_thata

參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章