認識線性回歸
回歸,是指研究一組隨機變量(
Y1 ,Y2 ,…,Yi
)和另一組(X1,X2,…,Xk
)變量之間關系的統計分析方法。
線性回歸是統計學中最基礎的數學模型,在很多學科的研究中都能看到線性回歸的影子,比如量化金融、計量經濟學等等。線性回歸通過對已有數據建模,從而實現對未知數據的預測。下面會通過一個房價預測的例子來詳細說明。
數據來自https://www.kaggle.com/kennethjohn/housingprice,該數據集提供了波特蘭市47套房屋的面積(HouseSize
)、卧室數量(Bedrooms
)和價格(Price
),如下:
import pandas as pd
data = pd.read_csv('https://files.cnblogs.com/files/blogs/478024/Housing-Prices.txt.zip')
print(data.head(5))
房屋面積(平方英尺) | 卧室數量 | 價格(美元) |
---|---|---|
2104 | 3 | 399900 |
1600 | 3 | 329900 |
2400 | 3 | 369000 |
1416 | 2 | 232000 |
3000 | 4 | 539900 |
基於已有數據,我們希望通過計算機的學習,找到數據中的規律,並用來預測其他房屋的價格。這是機器學習最朴素的應用場景。這個過程也被稱為監督學習(Supervised Learning
),即給定一些數據,使用計算機學習到一種模型,然后用它來預測新的數據。
在房價預測的例子中,想要預測的目標值房價是連續的,我們稱這類問題為回歸(Regression
)問題。與之相對應,當目標值只能在一個有限的離散集合里選擇,比如預測房價是否大於100萬,結果只有“是”和“否”兩種選項,我們稱這類問題為分類(Classification
)問題。
線性回歸初體驗
上面我們知道,通過訓練已有數據,得到模型,然后預測其他房屋的價格,這里,先使用scikit-learn
這個開源的python機器學習庫感受一下。
官網:https://scikit-learn.org/stable/index.html
(初次接觸機器學習沒有環境的朋友可以參考之前的文章《Python開發環境搭建》:https://www.cnblogs.com/bytesfly/p/python-environment.html)
import numpy as np
import pandas as pd
from sklearn.linear_model import SGDRegressor
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
# 獲取數據集
data = pd.read_csv('https://files.cnblogs.com/files/blogs/478024/Housing-Prices.txt.zip')
# 特征標准化,並選擇梯度下降法進行線性回歸
reg = make_pipeline(StandardScaler(), SGDRegressor())
# 機器學習(模型訓練)
reg.fit(data[['HouseSize', 'Bedrooms']], data['Price'])
# 待預測數據
predict_data = np.array([
[1000, 2],
[3000, 3],
[4000, 4],
[5000, 5]
])
# 預測
result = reg.predict(predict_data)
# 打印預測結果
print(np.c_[predict_data, result].astype(np.int))
預測結果:
房屋面積(平方英尺) | 卧室數量 | 價格(美元) |
---|---|---|
1000 | 2 | 211108 |
3000 | 3 | 480319 |
4000 | 4 | 610837 |
5000 | 5 | 741356 |
當然,實際生產中需要經過多次調優,且模型評估結果達到一定的要求才能正式上線。
特征工程
在討論線性回歸的數學表示之前,我覺得有必要提一下特征工程(Feature Engineering
)。上面“線性回歸初體驗”的代碼注釋中,有特征標准化
字眼,對應的代碼是StandardScaler()
,初學者應該會疑惑這個地方是什么意思,為什么需要這一步。
數據和特征決定了機器學習的上限,而模型和算法只是逼近這個上限而已。
可見,特征工程在機器學習中占有相當重要的地位。在實際應用當中,可以說特征工程是機器學習成功的關鍵。
Feature engineering is the process of using domain knowledge of the data to create features that make machine learning algorithms work.
如果你想要你的預測模型達到最佳,你不僅要選擇最優的算法,還要盡可能的從原始數據中獲取更多有價值的信息。這就是特征工程要做的事,它的目的是得到更好的訓練數據,是使用專業背景知識和技巧處理數據,使得特征能在機器學習算法上發揮更好的作用這一過程。
換句話說,特征工程就是一個把原始數據轉化成特征的過程,這些特征可以很好的描述這些數據,並且利用它們建立的模型在未知數據上的表現效果可以盡可能的好。從數學的角度來看,特征工程就是人工地去設計輸入自變量X
。
特征工程包含以下內容:
-
特征提取:將任意數據(如文本、圖像、聲音等)轉換為可用於機器學習的數字特征,因為機器學習進行的是數值相關的運算處理。這里我們收集影響房價的因素,比如房屋面積大小、卧室數量、出門到附近地鐵口的距離、周圍學校的數量等數字特征。
-
特征選擇:從特征集合中挑選一組最具統計意義的特征子集,從而達到
降維
的效果。這里我們為了簡化問題,只選擇房屋面積
和卧室數量
這兩個屬性作為特征值。實際生產中肯定需要根據專業背景知識選擇特征值。 -
特征預處理:通過一些轉換函數將特征數據轉換成更加適合算法模型的特征數據。
The
sklearn.preprocessing
package provides several common utility functions and transformer classes to change raw feature vectors into a representation that is more suitable for the downstream estimators.
Feature Scaling
(常叫”特征歸一化“、”標准化“),是特征預處理中的重要技術,有時甚至決定了算法能不能work以及work得好不好。為什么需要Feature Scaling
呢?
- 特征間的單位(尺度)可能不同,比如這里的房屋面積和卧室數量,房屋面積的變化范圍可能是[800,8000],卧室數量的變化范圍可能是[1,5],在計算時,尺度大的特征(房屋面積)會起決定性作用,而尺度小的特征(卧室數量)其作用可能會被忽略,為了消除特征間單位和尺度差異的影響,以對每維特征同等看待,需要對特征進行歸一化。
- 可以有效提高梯度下降(
Gradient Descent
)的收斂速度。等后面講過梯度下降法應該就能明白。
先看看如何使用sklearn.preprocessing
下的StandardScaler
:
import pandas as pd
from sklearn.preprocessing import StandardScaler
# 獲取數據集
data = pd.read_csv('https://files.cnblogs.com/files/blogs/478024/Housing-Prices.txt.zip',
usecols=['HouseSize', 'Bedrooms'])
# 這里為了演示,只選擇前5行數據進行特征標准化
data = data[:5]
scaler = StandardScaler()
# Compute the mean and std to be used for later scaling
scaler.fit(data)
# Perform standardization by centering and scaling
print(scaler.transform(data))
這樣,就把前5行原始數據轉換成了:
[[ 0. 0. ]
[-0.88604177 0. ]
[ 0.52037374 0. ]
[-1.20951734 -1.58113883]
[ 1.57518537 1.58113883]]
那么,StandardScaler
到底對數據做了怎樣的轉換呢?
注意:該轉換作用於每一列(即每個特征)。其中mean
表示這一列數據的平均值,σ
表示這一列數據的標准差。
這樣,原始數據就被映射到均值為0
,標准差為1
范圍內。
如何證明呢?其實並不困難。均值為0很顯然,每個原始值都減去mean
,再求和必然為0,均值也就自然是0。再看標准差:
把均值0代進去,就變成了求x的平方和了,再看分子、分母,其實是相等的(能get
到?想想均值與標准差之間的聯系?標准差是如何來的?)。如此一來算出來的結果正好是1。
清楚了StandardScaler
的轉換過程,我們也可以自己實現一下,如下:
import numpy as np
import pandas as pd
# 獲取數據集
data = pd.read_csv('https://files.cnblogs.com/files/blogs/478024/Housing-Prices.txt.zip',
usecols=['HouseSize', 'Bedrooms'])
# 這里為了演示,只選擇前5行數據進行標准化
data = data[:5]
for column in data.columns:
# 取出每一列(即每個特征)的數據
value = data[column]
# 求平均值
mean = np.mean(value)
# 求標准差
std = np.std(value)
# 對原始數值進行轉換
data[column] = (value - mean) / std
print(data)
運行結果如下:
HouseSize Bedrooms
0 0.000000 0.000000
1 -0.886042 0.000000
2 0.520374 0.000000
3 -1.209517 -1.581139
4 1.575185 1.581139
對比前面用sklearn.preprocessing
下的StandardScaler
計算出來的結果是一樣的。
溫馨提示:上面代碼中求標准差時用的是numpy
中的std
(計算時默認除以N
),而不是pandas
中的std
(計算時默認除以N-1
),否則計算出來的結果會有些差異。
線性回歸的數學表示
現在把線性回歸問題擴展到更一般的場景。假設x是多元的,或者說是多維的。比如,預測房價,需要考慮房屋面積大小、卧室數量、出門到附近地鐵口的距離、周圍學校或商場的數量等等。
這里,訓練已有房價數據就是為了確定參數w
,一旦確定了w
,就能根據x
的取值,求出y
,也就預測出了房價。
此時線性回歸的監督學習過程就可以被定義為:給定n個數據對(x, y)
,尋找最佳參數w
,使模型可以更好地擬合這些數據。
如何衡量模型是否以最優的方式擬合數據呢?
機器學習中常用損失函數(Loss Function
)來衡量這個問題。損失函數又稱為代價函數(Cost Function
),它計算了模型預測值y
和真實值y
之間的差異程度。從名字也可以看出,這個函數計算了模型犯錯的損失或代價,一般來說,損失函數值越小,數據擬合得可能越好(這里暫不考慮過擬合的情況)。
對於線性回歸,一個簡單實用的損失函數計算方式是預測值與真實值誤差的平方的均值。數理統計中叫做均方誤差(MSE: Mean Squared Error
)。MSE
的值越小,說明預測模型具有更好的精確度。
同樣,scikit-learn
也提供了對MSE
相關的描述與實現,詳情見:
https://scikit-learn.org/stable/modules/model_evaluation.html#mean-squared-error
下面簡單看下如何用scikit-learn
中的mean_squared_error
來計算模型誤差:
from sklearn.metrics import mean_squared_error
y_true = [3, -0.5, 2, 7]
y_pred = [2.5, 0.0, 2, 8]
print(mean_squared_error(y_true, y_pred))
打印結果為:
0.375
與你口算結果是否一致?
梯度下降法的推導
上面我們描述了線性回歸的數學表示以及計算損失的方法,這樣一來,問題就轉換成了,求解最佳參數w
,代入所有已知的訓練數據對(x, y)
到上面的均方差公式中,使得計算出來的損失值L(w)
盡可能小。其實,本質上是一個求解最優化問題。
值得注意的是,在房價預測中,房價影響因素x是自變量,房價y是因變量,但在這里的損失函數中,訓練數據對(x, y)是已知的(不再是變量了),而w可以看成是自變量,損失值L(w)是因變量,損失值L(w)隨着w取值的變化而變化。我們關心L(w)最小時,w的取值,這樣w就變成已知的了,此時認為模型可以擬合這些訓練數據,進而用來預測其他房屋的價格。
講到求損失函數的最小值,高中數學我們知道,一種辦法可以直接讓其導數為零,直接尋找損失函數的極小值點。這種方法求最優解,其實是在解這個矩陣方程,英文中稱這種方法為正規方程(Normal Equation
)。感興趣的朋友可以自行網上查詢一些資料學習一下。
由於正規方程法可能不存在唯一解,且當特征維度較大時,計算量比較大,非常耗時。下面重點介紹另外一種求解方法————梯度下降法。
求解損失函數最小問題,或者說求解使損失函數最小的最優化問題時,經常使用搜索的方法。具體而言,選擇一個初始點作為起點,然后開始不斷搜索,損失函數逐漸變小,當到達搜索迭代的結束條件時,該位置為搜索算法的最終結果。我們先隨機猜測一個
w
,然后對w
值不斷進行調整,來讓L(w)
逐漸變小,最好能找到使得L(w)
最小的w
。
一般情況下,0 < α < 1 。α越大,表示我們希望損失函數以更快的速度下降,α越小,表示我們希望損失函數下降的速度變慢。如果α設置得不合適,每次的步長太大,損失函數很可能無法快速收斂到最小值(步長太大可能直接跨過了極小值點);步長太小,計算次數過多,時間過長,效率就很差了。到這里,你能理解上面特征預處理時,對特征值進行歸一化、標准化的好處嗎?【可以有效提高梯度下降(
Gradient Descent
)的收斂速度】
當一個訓練集有m
個訓練樣本時,求導只需要對多條訓練樣本的數據做加和。
代入公式(15),如下:
w
是一個向量,假設它是n
維的,在更新w
時,需要同時對n
維所有w
值進行更新,其中第j
維就是使用這里的公式。
具體而言,這個算法為:
這一方法在每一次迭代時使用整個訓練集中的所有樣本來更新參數,也叫做批量梯度下降法(
Batch Gradient Descent,BGD
)。線性回歸的損失函數L
是一個凸二次函數(Convex Quadratic Function
),凸函數的局部極小值就是全局最小值,線性回歸的最優化問題只有一個全局解。也就是說,假設不把學習率α
設置的過大,迭代次數足夠多,梯度下降法總是收斂到全局最小值。
梯度下降法的實現
有了上面的層層鋪墊,可以用NumPy
自己實現一下。代碼的關鍵地方大多加了注釋,遇到不明白的步驟建議再讀讀前面的相關內容,遇到不熟悉的numpy函數建議單獨看看什么意思,遇到矩陣乘法可以用紙筆簡單演算一下。
import numpy as np
class Transformer:
def fit(self, x):
pass
def transform(self, x):
return x
class StandardTransformer(Transformer):
def __init__(self):
self.means = []
self.stds = []
def fit(self, x):
# 按照每個維度(特征)統計, 即按列統計
self.means = np.mean(x, axis=0)
self.stds = np.std(x, axis=0)
def transform(self, x):
x = x.copy()
for i in range(x.shape[1]):
# 對每列執行特征標准化
x[:, i] = (x[:, i] - self.means[i]) / self.stds[i]
return x
class LinearRegression:
def __init__(self, transformer: Transformer):
self.transformer = transformer
self.w = None
self.losses_history = []
def fit(self, x, y, alpha=0.0001, num_iters=1000):
num_of_samples, num_of_features = x.shape
# 計算均值與標准差,用於后面的特征標准化
self.transformer.fit(x)
# 特征標准化
x_ = self.transformer.transform(x)
# 在線性回歸的數學表示中,有過說明,為了簡化公式方便使用矩陣運算,在特征值第一列前面插入全為1的一列
x_ = np.column_stack((np.ones(num_of_samples), x_))
# 把y變成列向量,后面參與矩陣運算
y_ = y.reshape((num_of_samples, 1))
# 使用梯度下降法迭代計算w
self.gradient_descent(x_, y_, alpha, num_iters)
def gradient_descent(self, x, y, alpha: float, num_iters: int):
num_of_samples, num_of_features = x.shape
# 初始化參數w全為0
self.w = np.zeros((num_of_features, 1))
# 開始迭代
for i in range(num_iters):
# (num_of_samples, num_of_features) * (num_of_features, 1)
y_ = np.dot(x, self.w)
diff = y_ - y
# 這一步建議再回頭對照上面推導出來的梯度下降法的迭代公式
# 如果矩陣乘法有點難理解, 建議先在紙上對照公式看一個特征是怎么計算的(即x是按照一列一列參與運算的)
gradient = np.dot(diff.T, x).T
# 更新w
self.w -= alpha * gradient
# 計算損失
loss = np.sum(diff * diff) / num_of_samples / 2
# 加入到迭代過程中的損失統計列表
self.losses_history.append(loss)
def predict(self, x):
num_of_samples, num_of_features = x.shape
# 特征標准化
x_ = self.transformer.transform(x)
# 在線性回歸的數學表示中,有過說明,為了簡化公式方便使用矩陣運算,在特征值第一列前面插入全為1的一列
x_ = np.column_stack((np.ones(num_of_samples), x_))
# 根據訓練數據計算出來的w, 直接代入計算即可(矩陣乘法)
return np.dot(x_, self.w)
if __name__ == '__main__':
import pandas as pd
# 獲取數據集
data = pd.read_csv('https://files.cnblogs.com/files/blogs/478024/Housing-Prices.txt.zip')
# 調用上面自己實現的線性回歸(梯度下降法)
reg = LinearRegression(StandardTransformer())
# 訓練數據
reg.fit(data[['HouseSize', 'Bedrooms']].values, data['Price'].values)
# 待預測數據
predict_data = np.array([
[1000, 2],
[3000, 3],
[4000, 4],
[5000, 5]
])
# 預測
result = reg.predict(predict_data)
# 打印預測結果
print(np.c_[predict_data, result].astype(np.int))
# 畫圖觀察一下隨着迭代次數的增加, 損失值的變化
import matplotlib.pyplot as plt
losses_history = reg.losses_history
plt.plot(np.arange(0, len(losses_history)), losses_history)
plt.xlabel("num_of_iter")
plt.ylabel("loss")
plt.show()
此時的預測結果為:
房屋面積(平方英尺) | 卧室數量 | 價格(美元) |
---|---|---|
1000 | 2 | 178440 |
3000 | 3 | 426503 |
4000 | 4 | 567997 |
5000 | 5 | 709491 |
再看隨着迭代次數的增加,損失值的變化圖:
可見,在迭代到500次的時候損失值基本不再下降了。當然迭代多少次趨於穩定,與步長(學習率alpha
)密切相關。
總結
線性回歸是機器學習技術的一個很好的起點,對初學者走進機器學習很有幫助。
參考:
線性回歸的求解:https://lulaoshi.info/machine-learning/linear-model/minimise-loss-function.html