一元線性迴歸
假設你想計算匹薩的價格。 雖然看看菜單就知道了,不過也可以用機器學習方法建一個線性迴歸模型,通過分析匹薩直徑與價格的線性關係,來預測任意直徑匹薩的價格。假設我們查到了部分匹薩的直徑與價格的數據,這就構成了訓練數據,如下表所示:
import matplotlib.pyplot as plt
def runplt():
plt.figure()
plt.title("Cost and diameter")
plt.xlabel("Diameter/inch")
plt.ylabel("Cost/dollar")
plt.axis([0,25,0,30])
plt.grid(True)
return plt
plt = runplt()
X = [[6],[8],[10],[14],[18]]
y = [[7],[9],[13],[17.5],[18]]
plt.plot(X,y,'k.')
plt.show()
上圖中,x軸表示匹薩直徑,y軸表示匹薩價格。 能夠看出,匹薩價格與其直徑正相關,這與我們的日常經驗也比較吻合,自然是越大越貴。
下面就用 scikit-learn 來建模
#創建並擬合模型
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(X,y)
print("Predict 12 inch cost:$%.2f" % model.predict([[12]]))
>> Predict 12 inch cost:$13.68
一元線性迴歸假設輸入變量和輸出變量之間存在線性關係,這個線性模型所構成的空間是一個超平面(hyperplane)。 超平面是 n 維歐氏空間中維度減一的線性子空間,如平面中的直線、空間中的平面,總比包含它的空間少一維。 在一元線性迴歸中,一個維度是輸入變量,另一個維度是輸出變量,總共兩維。 因此,其超平面只有一維,就是一條線。
上述代碼中 sklearn.linear_model.LinearRegression 類是一個估計器(estimator)。 估計器依據觀測值來預測結果。 在 scikit-learn 裏面,所有的估計器都帶有 fit() 和 predict() 方法。 fit() 用來分析模型參數,predict() 是通過 fit() 算出的模型參數構成的模型,對解釋變量進行預測獲得的值。 因爲所有的估計器都有這兩種方法,所有 scikit-learn 很容易實驗不同的模型。 LinearRegression 類的 fit() 方法學習下面的一元線性迴歸模型:
plt = runplt()
X = [[6],[8],[10],[14],[18]]
y = [[7],[9],[13],[17.5],[18]]
model = LinearRegression()
model.fit(X,y)
X2 = [[0], [10], [14], [25]]
y2 = model.predict(X2)
plt.plot(X, y, 'k.')
plt.plot(X2, y2, 'g-')
plt.show()
帶成本函數的模型擬合評估
由若干參數生成的迴歸直線。 如何判斷哪一條直線纔是最佳擬合呢?
一元線性迴歸擬合模型的參數估計常用方法是普通最小二乘法(ordinary least squares )或線性最小二乘法(linear least squares)。 首先,我們定義出擬合成本函數,然後對參數進行數理統計。
成本函數(cost function)也叫損失函數(loss function),用來定義模型與觀測值的誤差。 模型預測的價格與訓練集數據的差異稱爲殘差(residuals)或訓練誤差(training errors)。 後面會用模型計算測試集,那時模型預測的價格與測試集數據的差異稱爲預測誤差(prediction errors)或訓練誤差(test errors)。模型的殘差是訓練樣本點與線性迴歸模型的縱向距離,如下圖所示:
model = LinearRegression()
model.fit(X,y)
X2 = [[0], [10], [14], [25]]
y2 = model.predict(X2)
plt.plot(X, y, 'k.')
plt.plot(X2, y2, 'g-')
# 殘差預測值
yr = model.predict(X)
for idx, x in enumerate(X):
plt.plot([x,x], [y[idx], yr[idx]], 'r-')
plt.show()
import numpy as np
print("殘差平方和:%.2f" %np.mean((model.predict(X) - y)**2))
解一元線性迴歸的最小二乘法
通過成本函數最小化獲得參數,先求相關係數b。 按照頻率論的觀點,首先需要計算 x 的方差和 x 與 y 的協方差。α = 12.9 − 0.9762931034482758 × 11.2 = 1.9655172413793114
這樣就通過最小化成本函數求出模型參數了。 把匹薩直徑帶入方程就可以求出對應的價格了,如 11 英寸直徑價格 $12.70,18 英寸直徑價格 $19.54
print(np.var(X, ddof=1))
模型評估
前面用學習算法對訓練集進行估計,得出了模型的參數。 如何評價模型在現實中的表現呢?現在假設有另一組數據,作爲測試集進行評估model = LinearRegression()
model.fit(X,y)
X_test = [[8], [9], [11], [16], [12]]
y_test = [[11], [8.5], [15], [18], [11]]
SStot = np.sum((y_test-np.mean(y_test))**2)
SSres = np.sum((y_test-model.predict(X_test))**2)
print("SStot=",SStot)
print("SSres=",SSres)
print("R2=", 1-SSres/SStot)
print("model score=", model.score(X_test, y_test))
>> SStot=56.8
SSres=19.1980
R2=0.66200
model score=0.66200
多元線性迴歸
可以看出匹薩價格預測的模型 R 方值並不顯著。 如何改進呢?匹薩的價格其實還會受到其他因素的影響。 比如,匹薩的價格還與上面的輔料有關。 讓我們再爲模型增加一個輸入變量。 用一元線性迴歸已經無法解決了,我們可以用更具一般性的模型來表示,即多元線性迴歸
同時要升級測試集數據:
學習算法評估三個參數的值:兩個相關因子和一個截距。可以通過矩陣運算來實現,因爲矩陣沒有除法運算,所以用矩陣的轉置運算和逆運算來實現:
from numpy.linalg import inv
from numpy import dot, transpose
X = [[1,6,2],[1,8,1],[1,10,0],[1,14,2],[1,18,0]]
y = [[7],[9],[13],[17.5],[18]]
print(dot(inv(dot(transpose(X),X)),dot(transpose(X),y)))
>> [[1.1875][1.01041667][0.39583333]]
利用矩陣的方法實現,可以得到同樣的結果:
from numpy import *
xMat = mat(X)
yMat = mat(y)
xTx = xMat.T*xMat
print(xTx)
if linalg.det(xTx)==0.0:
print("The matrix is singular")
else:
ws = xTx.I * (xMat.T*yMat)
print(ws)
更新價格預測模型
from numpy.linalg import inv
from numpy import dot, transpose
import numpy as np
from sklearn.linear_model import LinearRegression
X = [[6,2],[8,1],[10,0],[14,2],[18,0]]
y = [[7],[9],[13],[17.5],[18]]
model = LinearRegression()
model.fit(X,y)
X_test = [[8,2], [9,0], [11,2], [16,2], [12,0]]
y_test = [[11], [8.5], [15], [18], [11]]
predictions = model.predict(X_test)
for i, prediction in enumerate(predictions):
print("Predicted: %s, Target: %s" %(prediction, y_test[i]))
print("R2=%.2f"%model.score(X_test, y_test))
>> Predicted: [10.06250019], Target: [11]
Predicted: [10.28125019], Target: [8.5]
Predicted: [13.09375019], Target: [15]
Predicted: [18.14583353], Target: [18]
Predicted: [13.31250019], Target: [11]
R2=0.77
增加輸入變量讓模型擬合效果更好了。 爲什麼只用一個測試集評估一個模型的效果是不準確的,如何通過將測試集數據分塊的方法來測試,讓模型的測試效果更可靠。 不過現在至少可以認爲匹薩價格預測問題,多元迴歸確實比一元迴歸效果更好。 假如輸入變量和輸出變量的關係不是線性的呢?下面來研究一個特別的多元線性迴歸的情況,可以用來構建非線性關係模型。
多項式迴歸
下面用多項式迴歸,一種特殊的多元線性迴歸方法,增加了指數項( 次數大於 1)。 現實世界中的曲線關係全都是通過增加多項式實現的,其實現方式和多元線性迴歸類似。 本例還用一個輸入變量,匹薩直徑。 用下面的數據對兩種模型做個比較:二次迴歸(Quadratic Regression),即迴歸方程有個二次項,公式如下:
model = LinearRegression()
model.fit(X_train,y_train)
xx = np.linspace(0,26,100)
yy = model.predict(xx.reshape(-1,1))
plt = runplt()
plt.plot(X_train, y_train, 'k.')
plt.plot(xx,yy)
quadratic_featurizer = PolynomialFeatures(degree=2)
X_train_quadratic = quadratic_featurizer.fit_transform(X_train)
X_test_quadratic = quadratic_featurizer.transform(X_test)
model_quadratic = LinearRegression()
model_quadratic.fit(X_train_quadratic, y_train)
xx_quadratic = quadratic_featurizer.transform(xx.reshape(-1,1))
plt.plot(xx, model_quadratic.predict(xx_quadratic), 'r-')
cubic_featurizer = PolynomialFeatures(degree=3)
X_train_cubic = cubic_featurizer.fit_transform(X_train)
X_test_cubic = cubic_featurizer.transform(X_test)
model_cubic = LinearRegression()
model_cubic.fit(X_train_cubic, y_train)
xx_cubic = cubic_featurizer.transform(xx.reshape(-1,1))
plt.plot(xx, model_cubic.predict(xx_cubic))
plt.show()
print("一元線性迴歸R2=", model.score(X_test, y_test))
print("二次迴歸 R2=", model_quadratic.score(X_test_quadratic, y_test))
print("三次迴歸 R2=", model_cubic.score(X_test_cubic, y_test))
>> 一元線性迴歸R2=0.8097
二次迴歸R2=0.8675
三次迴歸R2=0.8357
可以看出,三次擬合的 R 方值更低,雖然其圖形經過了更多的點, 可以認爲這是擬合過度(overfitting)的情況。 這種模型並沒有從輸入和輸出中推導出一般的規律,而是記憶訓練集的結果,這樣在測試集的測試效果就不好了。
正則化
正則化(Regularization)是用來防止擬合過度的一堆方法。 正則化向模型中增加信息,經常是一種對抗複雜性的手段。 與奧卡姆剃刀原理(Occam's razor)所說的具有最少假設的論點是最好的觀點類似。 正則化就是用最簡單的模型解釋數據。scikit-learn 提供了一些方法來使線性迴歸模型正則化。 其中之一是嶺迴歸 (Ridge Regression,RR,也叫 Tikhonov regularization),通過放棄最小二乘法的無偏性,以損失部分信息、降低精度爲代價獲得迴歸係數更爲符合實際、更可靠的迴歸方法。 嶺迴歸增加 L2 範數項(相關係數向量平方和的平方根)來調整成本函數(殘差平方和):
scikit-learn 也提供了最小收縮和選擇算子 (Least absolute shrinkage and selection operator,LASSO),增加 L1 範數項來調整成本函數(殘差平方和):
LASSO 方法會產生稀疏參數,大多數相關係數會變成 0,模型只會保留一小部分特徵。 而嶺迴歸還是會保留大多數儘可能小的相關係數。 當兩個變量相關時,LASSO 方法會讓其中一個變量的相關係數會變成 0,而嶺迴歸是將兩個係數同時縮小。
scikit-learn 還提供了彈性網(elastic net)正則化方法,通過線性組合 L1 和 L2 兼具 LASSO 和嶺迴歸的內容。 可以認爲這兩種方法是彈性網正則化的特例。
下面對三次擬合的數據加入Lasso和嶺迴歸正則化項。
# LASSO迴歸
from sklearn.linear_model import Lasso
lasso = Lasso(alpha=0.05, normalize=True)
lasso.fit(X_train_cubic, y_train)
print("Lasso R2=", lasso.score(X_test_cubic, y_test))
#嶺迴歸
from sklearn.linear_model import Ridge
ridge = Ridge(alpha=0.05, normalize=True)
ridge.fit(X_train_cubic, y_train)
print("Ridge R2=", ridge.score(X_test_cubic, y_test))
>>三次迴歸R2=0.8357
Lasso R2=0.8482
Ridge R2=0.8358
梯度下降法擬合模型
前面的內容全都是通過最小化成本函數來計算參數的:這裏 X 是輸入變量矩陣,當變量很多(上萬個)的時候, 右邊第一項計算量會非常大。 另外,如果右邊第一項行列式爲 0,即奇異矩陣,那麼就無法求逆矩陣了。 這裏我們介紹另一種參數估計的方法,梯度下降法(gradient descent)。 擬合的目標並沒有變,我們還是用成本函數最小化來進行參數估計。
梯度下降法被比喻成一種方法,一個人蒙着眼睛去找從山坡到溪谷最深處的路。 他看不到地形圖,所以只能沿着最陡峭的方向一步一步往前走。 每一步的大小與地勢陡峭的程度成正比。 如果地勢很陡峭,他就走一大步,因爲他相信他仍在高處,還沒有錯過溪谷的最低點。 如果地勢比較平坦,他就走一小步。 這時如果再走大步,可能會與最低點失之交臂。 如果真那樣,他就需要改變方向,重新朝着溪谷的最低點前進。 他就這樣一步一步的走啊走,直到有一個點走不動了,因爲路是平的了,於是他卸下眼罩,已經到了谷底深處。
通常,梯度下降算法是用來評估函數的局部最小值的。 我們前面用的成本函數如下:
可以用梯度下降法來找出成本函數最小的模型參數值。 梯度下降法會在每一步走完後,計算對應位置的導數,然後沿着梯度(變化最快的方向)相反的方向前進, 總是垂直於等高線。需要注意的是,梯度下降法來找出成本函數的局部最小值。 一個三維凸(convex)函數所有點構成的圖像一個碗,碗底就是唯一局部最小值。 非凸函數可能有若干個局部最小值,也就是說整個圖形看着像是有多個波峯和波谷。 梯度下降法只能保證找到的是局部最小值,並非全局最小值。 殘差平方和構成的成本函數是凸函數,所以梯度下降法可以找到全局最小值。
梯度下降法的一個重要超參數是步長(learning rate),用來控制矇眼人步子的大小,就是下降幅度。 如果步長足夠小,那麼成本函數每次迭代都會縮小,直到梯度下降法找到了最優參數爲止。 但是,步長縮小的過程中,計算的時間就會不斷增加。 如果步長太大,這個人可能會重複越過谷底,也就是梯度下降法可能在最優值附近搖擺不定。
如果按照每次迭代後用於更新模型參數的訓練樣本數量劃分,有兩種梯度下降法。批量梯度下降(Batch gradient descent)每次迭代都用所有訓練樣本。 隨機梯度下降(Stochastic gradientdescent,SGD)每次迭代都用一個訓練樣本,這個訓練樣本是隨機選擇的。 當訓練樣本較多的時候,隨機梯度下降法比批量梯度下降法更快找到最優參數。 批量梯度下降法一個訓練集只能產生一個結果。 而 SGD 每次運行都會產生不同的結果。 SGD 也可能找不到最小值,因爲升級權重的時候只用一個訓練樣本。 它的近似值通常足夠接近最小值,尤其是處理殘差平方和這類凸函數的時候。
下面用 scikit-learn 的 SGDRegressor 類來計算模型參數。 它可以通過優化不同的成本函數來擬合線性模型,默認成本函數爲殘差平方和。 本例中,我們用波士頓住房數據的 13 個解釋變量來預測房屋價格:
import numpy as np
from sklearn.datasets import load_boston
from sklearn.linear_model import SGDRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.cross_validation import cross_val_score
from sklearn.cross_validation import train_test_split
data = load_boston()
X_train, X_test, y_train, y_test = train_test_split(data.data, data.target)
X_scaler = StandardScaler()
y_scaler = StandardScaler()
X_train = X_scaler.fit_transform(X_train)
y_train = y_scaler.fit_transform(y_train.reshape(-1,1))
X_test = X_scaler.transform(X_test)
y_test = y_scaler.transform(y_test.reshape(-1,1))
model = SGDRegressor(loss='squared_loss')
scores = cross_val_score(model, X_train, y_train, cv=5)
print("交叉驗證R方值:", scores)
print("交叉驗證R方均值:", np.mean(scores))
model.fit(X_train,y_train)
print("測試集R方值:", model.score(X_test, y_test))