圖靈程序叢書 —《數據科學入門》— Ch8 梯度下降法

Ch8 梯度下降法

此係列記錄《數據科學入門》學習筆記


8.2 梯度下降的思想

梯度下降法只能找到局部最優解,而不是全局最優解;

當有多個全局最優解時,可以通過多嘗試一些初始點來重複搜索;

當一個函數沒有最小點時,計算可能會陷入死循環。


8.2 估算梯度

def sum_of_squares(v):
    return sum(v_i ** 2 for v_i in v)

# 單變量函數的導數可通過差商來定義
def difference_quotient(f, x, h):
    return (f(x + h) - f(x)) / h

def square(x):
    return x * x

def derivative(x):
    return 2 * x


# python 無法直接運算極限,但可以通過計算一個很小的變動e的差商來估算微分
import matplotlib.pyplot as plt
from pylab import *
from functools import partial

# 固定f=x^2,h=0.00001
derivative_estimate = partial(difference_quotient, square, h=0.00001)
mpl.rcParams['axes.unicode_minus']=False
mpl.rcParams['font.sans-serif'] = ['SimHei']  
x = range(-10, 10)
plt.title('精確的導數值和估計值')
plt.plot(x, list(map(derivative, x)), 'rx', label='actual')
plt.plot(x, list(map(derivative_estimate, x)), 'b+', label='estimate')
plt.legend(loc=9)
plt.show()


# 計算偏導數
# 把導數看成是其第i個變量的函數,其他變量保持不變,以此來計算他的第i個偏導數
def partial_difference_quotient(f, v, i, h):
    w = [v_j + (h if j == i else 0) for j, v_j in enumerate(v)]     #只對v的第i個元素加h
    return (f(w) - f(v)) / h

def estimate_gradient(f, v, h):
    return [partial_difference_quotient(f, v, i, h) for i, _ in enumerate(v)]

"""
‘差商估算法’的主要缺點是計算代價很大,如果v長度爲n,那麼estimate_gradient爲了計算f需要2n個不同的輸入變量。
如果需要反覆計算題都,那需要做很多額外的工作
"""


8.3 使用梯度

# 用梯度方法可以從三維向量中找到最小值
# 可以先找出隨機初始點,並在梯度的反方向以小步逐步前進,知道梯度變得非常小
"""下列程序總是止於一個非常接近[0, 0, 0]的v值,tolerance的值設定的越小, v的值就越接近[0, 0, 0]"""
import random
import numpy as np

def step(v, direction, step_size):
    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]

def distance(v, w):
    return np.sqrt(sum((v_i - w_i) ** 2 for v_i, w_i in zip(v, w)))

# 隨機選擇三維向量的初始點
v = [random.randint(-10, 10) for i in range(3)]
tolerance = 0.0000001

while True:
    gradient = sum_of_squares_gradient(v)
    next_v = step(v, gradient, -0.01) 
    if distance(next_v, v) < tolerance:
        break
    v = next_v
print(v)
# [-3.688294692016085e-06, -2.868673649345841e-06, -1.6392420853404806e-06]


8.4 選擇正確步長

選擇合適的步長更像藝術而非科學,主流方法有:
1、使用固定步長;
2、隨着時間增長逐步減小步長;
3、在每一步中通過最小化目標函數值來選擇步長。此方法看上去不錯,但是計算代價也最大。
可以嘗試一系列步長,並選出使目標函數值最小的的那個步長來求其近似值:step_sizes = [100, 10, 1, 0.1, 0.01, 0.001, 0.0001, 0.00001]

# 某些步長可能會導致函數的輸入無效,故需要創建一個對無效輸入值返回無限制的‘安全應用’函數(即這個值永遠不會成爲任何函數的最小值)
def safe(f):
    """return a function that is the same as f, except that it outputs infinity whenever f produces an error"""
    def safe_f(*arg, **kwargs):
        try:
            return f(*args, **kwargs)
        except:
            return float('inf')  # python 裏的‘無限值’
    return safe_f


8.5 綜合

# 批量梯度下降法(batch gradient descend),因爲在每一步梯度計算中,他都會搜索整個數據集(target_fn代表整個數據集的殘差)

def minimize_batch(target_fn, gradient_fn, theta_0, tolerance=0.000001):
    """use gradient descent to find theta that minimizes target fuction"""
    
    step_sizes = [100, 10, 1, 0.1, 0.01, 0.001, 0.0001, 0.00001]
    
    theta = theta_0                     # 設定theta爲初始值
    target_fn = safe(target_fn)         # target_fn的安全版
    value = target_fn(theta)            # 試圖最小化的值
    
    while True:              # while true爲死循環,需要用break來退出
        gradient = gradient_fn(theta)
        next_thetas = [step(theta, gradient, -step_size) for step_size in step_sizes]
    
    # 選擇一個使殘差函數最小的值
    next_theta = min(next_thetas, key=target_fn)
    next_value = target_fn(next_theta)
    
    # 當‘收斂’時停止
    if abs(value - next_value) < tolerance:
        return theta
    else:
        theta, value = next_theta, next_value


# 最大化某個函數值 = 最小化這個函數的負值

def negate(f):
    """return a function that for any input x returns -f(x)"""
    return lambda *args, **kwargs: -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)


8.6 隨機梯度下降法(stochastic gradient descent)

殘差函數往往具有可加性,意味着整個數據集上的預測殘差剛好是每個數據點預測殘差之和。
每次僅計算一個點的梯度(並向前跨一步),反覆計算直到到達停止點。
對於每一個數據點都會進行一步梯度計算,這種方法也許會在最小值附近一直循環下去,所以每當停止獲得改進,我們都會減小步長並最終退出。

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]

def minimize_stochastic(target-fn, gradient_fn, x, y, theta_0, alpha_0=0.01):
    
    def scalar_multipy(c, v):
        return [c * v_i for v_i in v]
    
    def vector_subtract(v, w):
        return [v_i - w_i for v_i, w_i in zip(v,w)]
        
    data = zip(x, y)
    theta = theta_0                               # 初始值設定
    alpha = alpha_0                               # 初始步長
    min_theta, min_value = None, float('inf')    # 迄今爲止的最小值
    iterations_with_no_improvement = 0
    
    # 當循環超過100次仍然無改進,則停止循環
    while iterations_with_no_improvement < 100:
        value = sum(target_fn(x_i, y_i, theta) for x_i, y_i in data)
        
        if value < min_value:
            # 如果找到新的最小值,記住它,並返回到最初的步長
            min_theta, min_value = theta, value
            iterations_with_no_improvement =  0
            alpha = alpha_0
        else:
            # 嘗試縮小步長,否則沒有改進
            iterations_with_no_improvement += 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_multipy(alpha, gradient_i))
            
    return min_theta

def maximize_stochastic(target_fn, gradient_fn, x, y, theta_0, alpha_0=0.01):
    return minimize_stochastic(negate(target_fn), negate_all(gradient_fn), x, y, theta_0, alpha_0)

以上是Ch8的相關內容
2018.03.08   YR

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