Normal Equation
對于損失函數,可以使其導數為零,尋找損失函數的極值點。
假設我們的模型隻有一維數據,模型是一條直線 ,我們共有 條訓練數據,損失函數為誤差平方和的平均數:
可以對 和 分别求導,導數為0時,損失函數最小。
上面兩個公式是損失函數 對 和 進行求偏導。當導數為0時,可以求得損失函數的最小值,即由上面兩個公式可以得到最優解 和 。
最優解為:
其中, ,即為 的均值。
以上就是一元線性回歸的最小二乘法求解過程。很多機器學習模型中都需要經曆上述過程:确定損失函數,求使損失函數最小的參數。求解過程會用到一些簡單的微積分,因此複習一下微積分中偏導數部分,有助于理解機器學習的數學原理。
更為一般的情形下,特征是多維的:
上面的公式中,我們其實是使用 來表示參數中的 ,将 添加到特征向量中,将 維特征擴展成 維,也就是在每個 中添加一個值為1的項。各個向量和矩陣形狀如下面公式所示。
其中,向量 表示模型中各個特征的權重;矩陣 的每一行是一條樣本,每條樣本中有 個特征值,分别為該樣本在不同維度上的取值;向量 為真實值。可以用内積的形式來表示求和項: 。用矩陣乘法的形式可以表示為: 。
一般機器學習領域更喜歡使用矩陣乘法的形式來表示一個模型,這不僅因為這樣表示起來更簡單,也是因為現代計算機對向量計算做了大量優化,無論是CPU還是GPU都喜歡向量計算,并行地處理數據,可以得到成百上千倍的加速比。
注意,公式中不加粗的表示标量,加粗的表示向量或矩陣。
比一元線性回歸更為複雜的是,多元線性回歸最優解不是一條直線,是一個多維空間中的超平面,訓練數據散落在超平面的兩側。
多元線性回歸一般尋找最優超平面
多元線性回歸的損失函數仍然使用“預測值-真實值”的平方來計算,上面公式為整個模型損失函數的向量表示。這裡出現了一個豎線組成的部分,它被稱作L2範數的平方。範數通常針對向量,也是一個機器學習領域經常用到的數學符号,下面公式展示了一個向量 的L2範數的平方以及其導數。
對線性回歸損失函數公式中的向量 求導,令導數為零:
上面公式是向量 的解,這是一個矩陣方程。使用這種方法求最優解,其實是在解這個矩陣方程,英文中稱這種方法為Normal Equation。
這個方法有一個問題,在線性代數課程中肯定曾提到過, 是滿秩(Full-Rank)或正定(Positive Definite)時,才能解方程組。“滿秩”或者“正定”到底什麼意思呢?用通俗的話來講,樣本中的數據必須足夠豐富,且有足夠的代表性,矩陣方程才有唯一解,否則矩陣方程會有多組解。如果特征有上萬維,但隻有幾十個樣本來訓練,我們很難得到一個滿意的最優解。
上述方法還有一個問題:公式中矩陣求逆的計算量比較大,複雜度在 級别。當特征維度達到百萬級以上或樣本數量極大時,計算時間非常長,單台計算機内存甚至存儲不下這些參數,求解矩陣方程的辦法就不現實了。
前面花了點時間描述線性回歸的求解過程,出現了不少公式,跟很多朋友一樣,筆者以前非常讨厭看公式,看到一些公式就頭大,因此覺得機器學習非常難。不過,靜下心來仔細讀一遍,會發現其實這些公式用到的都是微積分、線性代數中比較基礎的部分,并不需要高大上的知識,理工科背景的朋友應該都能看得懂。另外,複習一下矩陣和求導等知識有助于我們理解深度學習的一些數學原理。
求解損失函數最小問題,或者說求解使損失函數最小的最優化問題時,經常使用搜索的方法。具體而言,選擇一個初始點作為起點,然後開始不斷搜索,損失函數逐漸變小,當到達搜索叠代的結束條件時,該位置為搜索算法的最終結果。我們先随機猜測一個 ,然後對 值不斷進行調整,來讓 逐漸變小,最好能找到使得 最小的 。
具體來說,我們可以考慮使用梯度下降法(Gradient Descent),這個方法就是從某一個 的初始值開始,然後逐漸對權重進行更新,或者說每次用新計算的值覆蓋原來的值:
這裡的 也稱為學習率, 是梯度(Gradient)。微積分課中提到,在某個點,函數沿着梯度方向的變化速度最快。因為我們想最小化損失函數 ,因此,我們每次都沿着梯度下降,不斷向 降低最快的方向移動。
用圖像直觀來看,損失函數沿着梯度下降的過程如下所示。叠代過程最終收斂在了最小值附近,此時,梯度或者說導數接近0。
回到學習率 上, 代表在某個點上,我們對梯度的置信程度。一般情況下, 。 越大,表示我們希望損失函數以更快的速度下降, 越小,表示我們希望損失函數下降的速度變慢。如果 設置得不合适,每次的步進太大,損失函數很可能無法快速收斂到最小值。如下所示,損失函數經過很長時間也難以收斂到最小值。在實際應用中, 經常随着叠代次數變化而變化,比如,初始化時較大,後面漸漸變小。
我們之前提到過, 是一個向量,假設它是 維的,在更新 時,我們是要同時對 維所有 值進行更新,其中第 維就是使用上面的權重更新公式。
接下來我們簡單推導一下梯度公式,首先考慮隻有一條訓練樣本 的情況。由 ,其中, 是常數項,不影響最優解的取值,主要是為了方便求導。可以得到:
對單個訓練樣本,每次對梯度的更新規則如下所示:
這個規則有幾個看上去就很自然直觀的特性:
當隻有一個訓練樣本的時候,我們推導出了 LMS 規則。當一個訓練集有 個訓練樣本的時候, 。求導時,隻需要對多條訓練樣本的數據做加和。
因此,可以得出每個 的導數:
具體而言,這個算法為:
這一方法在每一次叠代時使用整個訓練集中的所有樣本來更新參數,也叫做批量梯度下降法(Batch Gradient Descent,BGD)。線性回歸的損失函數 是一個凸二次函數(Convex Quadratic Function),凸函數的局部極小值就是全局最小值,線性回歸的最優化問題隻有一個全局解。也就是說,假設不把學習率 設置的過大,叠代次數足夠多,梯度下降法總是收斂到全局最小值。
批量梯度下降在更新參數時要把所有樣本都要考慮進去。當數據量大、特征多時,每次叠代都使用全量數據并不現實;而且全量數據本身包含很多冗餘信息,數據量越大,冗餘信息越多,在求最優解時,冗餘信息并沒有太大幫助。一種妥協方法是,每次更新參數時,隻随機抽取部分樣本。一個比較極端的情況是,每次叠代時随機抽取一條樣本,使用單個樣本來更新本次叠代的參數,這個算法被稱為随機梯度下降(Stochastic gradient descent,SGD),如下所示:
另外,我們也可以每次随機抽取一個小批次(Mini-batch)的訓練數據,用這批數據更新本次叠代參數,這種算法被稱為Mini-batch SGD。Mini-batch SGD是BGD和SGD之間的一個妥協,Mini-batch SGD降低了SGD中随機性帶來的噪音,又比BGD更高效。
梯度下降法努力逼近最優解,求解速度在數據量大時有優勢,但不一定能得到絕對的最優解。在很多實際應用中,雖然梯度下降求解的點在最優點附近,但其實已經能夠滿足需求。考慮到這些因素,梯度下降法,尤其是随機梯度下降法被大量應用在機器學習模型求解上。除了以上介紹的幾種外,梯度下降法有很多變體。
前面推導了這麼多,Talk is cheap,Show some code。接下來,我們使用NumPy實現一個線性回歸模型,分别使用批量梯度下降和随機梯度下降。實現過程中我們會發現,有些問題是公式推導不會提及的工程問題,比如,計算過程中的數據太大,超出了 float64 的可表示範圍。工程實現體現了理論和實踐之間的差異,實際上,往往這些工程細節決定着機器學習框架的易用性。
import numpy as np
class LinearRegression:
def __init__(self):
# the weight vector
self.W = None
def train(self, X, y, method='bgd', learning_rate=1e-2, num_iters=100, verbose=False):
"""
Train linear regression using batch gradient descent or stochastic gradient descent
Parameters
----------
X: training data, shape (num_of_samples x num_of_features), num_of_samples rows of training sample, each training sample has num_of_features-dimension features.
y: target, shape (num_of_samples, 1).
method: (string) 'bgd' for Batch Gradient Descent or 'sgd' for Stochastic Gradient Descent
learning_rate: (float) learning rate or alpha
num_iters: (integer) number of steps to iterate for optimization
verbose: (boolean) if True, print out the progress
Returns
-------
losses_history: (list) of losses at each training iteration
"""
num_of_samples, num_of_features = X.shape
if self.W is None:
# initilize weights with values
# shape (num_of_features, 1)
self.W = np.random.randn(num_of_features, 1) * 0.001
losses_history = []
for i in range(num_iters):
if method == 'sgd':
# randomly choose a sample
idx = np.random.choice(num_of_samples)
loss, grad = self.loss_and_gradient(X[idx, np.newaxis], y[idx, np.newaxis])
else:
loss, grad = self.loss_and_gradient(X, y)
losses_history.append(loss)
# Update weights using matrix computing (vectorized)
self.W -= learning_rate * grad
if verbose and i % (num_iters / 10) == 0:
print('iteration %d / %d : loss %f' %(i, num_iters, loss))
return losses_history
def predict(self, X):
"""
Predict value of y using trained weights
Parameters
----------
X: predict data, shape (num_of_samples x num_of_features), each row is a sample with num_of_features-dimension features.
Returns
-------
pred_ys: predicted data, shape (num_of_samples, 1)
"""
pred_ys = X.dot(self.W)
return pred_ys
def loss_and_gradient(self, X, y, vectorized=True):
"""
Compute the loss and gradients
Parameters
----------
The same as self.train function
Returns
-------
tuple of two items (loss, gradient)
loss: (float)
gradient: (array) with respect to self.W
"""
if vectorized:
return linear_loss_grad_vectorized(self.W, X, y)
else:
return linear_loss_grad_for_loop(self.W, X, y)
def linear_loss_grad_vectorized(W, X, y):
"""
Compute the loss and gradients with weights, vectorized version
"""
# vectorized implementation
num_of_samples = X.shape[0]
# (num_of_samples, num_of_features) * (num_of_features, 1)
f_mat = X.dot(W)
# (num_of_samples, 1) - (num_of_samples, 1)
diff = f_mat - y
loss = 1.0 / 2 * np.sum(diff * diff)
# {(num_of_samples, 1).T dot (num_of_samples, num_of_features)}.T
gradient = ((diff.T).dot(X)).T
return (loss, gradient)
def linear_loss_grad_for_loop(W, X, y):
"""
Compute the loss and gradients with weights, for loop version
"""
# num_of_samples rows of training data
num_of_samples = X.shape[0]
# num_of_samples columns of features
num_of_features = X.shape[1]
loss = 0
# shape (num_of_features, 1) same with W
gradient = np.zeros_like(W)
for i in range(num_of_samples):
X_i = X[i, :] # i-th sample from training data
f = 0
for j in range(num_of_features):
f = X_i[j] * W[j, 0]
diff = f - y[i, 0]
loss = np.power(diff, 2)
for j in range(num_of_features):
gradient[j, 0] = diff * X_i[j]
loss = 1.0 / 2 * loss
return (loss, gradient)
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!