Python 數(shù)據(jù)科學(xué)手冊 5.6 線性回歸

5.6 線性回歸

原文:In Depth: Linear Regression

譯者:飛龍

協(xié)議:CC BY-NC-SA 4.0

譯文沒有得到原作者授權(quán),不保證與原文的意思嚴(yán)格一致。

就像樸素貝葉斯(之前在樸素貝葉斯分類中討論)是分類任務(wù)的一個(gè)很好的起點(diǎn),線性回歸模型是回歸任務(wù)的一個(gè)很好的起點(diǎn)。 這些模型受歡迎,因?yàn)樗鼈兛梢钥焖贁M合,并且非??山忉尅?你可能熟悉線性回歸模型的最簡單形式(即使用直線擬合數(shù)據(jù)),但是可以擴(kuò)展這些模型,來建模更復(fù)雜的數(shù)據(jù)行為。

在本節(jié)中,在這個(gè)眾所周知問題背后,我們將從數(shù)學(xué)的快速直觀的了解開始,然后再看看如何將線性模型推廣到數(shù)據(jù)中更復(fù)雜的模式。

我們以標(biāo)準(zhǔn)導(dǎo)入來開始:

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns; sns.set()
import numpy as np

簡單線性回歸

我們以最熟悉的線性回歸開始,它是一個(gè)擬合數(shù)據(jù)的直線。擬合直線是這樣:

y = ax + b

其中a是眾所周知的斜率,b是眾所周知的截距。

考慮下面的數(shù)據(jù),它點(diǎn)綴在直線周圍,斜率為 2,截距為 -5。

rng = np.random.RandomState(1)
x = 10 * rng.rand(50)
y = 2 * x - 5 + rng.randn(50)
plt.scatter(x, y);

我們可以使用 Scikit-Learn 的LinearRegression估計(jì)其來擬合這個(gè)直線,并且構(gòu)造出最佳擬合直線。

from sklearn.linear_model import LinearRegression
model = LinearRegression(fit_intercept=True)

model.fit(x[:, np.newaxis], y)

xfit = np.linspace(0, 10, 1000)
yfit = model.predict(xfit[:, np.newaxis])

plt.scatter(x, y)
plt.plot(xfit, yfit);

數(shù)據(jù)的斜率和截距包含在模型的擬合參數(shù)中,其中 Scikit-Learn 總是以尾部的下劃線標(biāo)記。 這里的相關(guān)參數(shù)是coef_intercept_

print("Model slope:    ", model.coef_[0])
print("Model intercept:", model.intercept_)
Model slope:     2.02720881036
Model intercept: -4.99857708555

我們看到結(jié)果非常接近輸入,這是我們希望的。

然而,線性回歸估計(jì)器比這更加強(qiáng)大,除了簡單的直線擬合之外,它還可以處理這種形式的多維線性模型。

y = a0 + a1x1 + a2x2 + ...

其中有多個(gè)x值。 在幾何學(xué)上,這類似于使用平面擬合三維點(diǎn),或使用超平面擬合更高維度的點(diǎn)。

這種回歸的多維本質(zhì)使得它們更難以可視化,但是我們可以通過使用 NumPy 的矩陣乘法運(yùn)算符,構(gòu)建一些示例數(shù)據(jù)來查看其中的一個(gè)擬合:

rng = np.random.RandomState(1)
X = 10 * rng.rand(100, 3)
y = 0.5 + np.dot(X, [1.5, -2., 1.])

model.fit(X, y)
print(model.intercept_)
print(model.coef_)
0.5
[ 1.5 -2.   1. ]

這里數(shù)據(jù)y由三個(gè)隨機(jī)x值構(gòu)成,線性回歸恢復(fù)了用于構(gòu)建數(shù)據(jù)的系數(shù)。

以這種方式,我們可以使用單個(gè)LinearRegression估計(jì)器來將數(shù)據(jù)擬合為直線,平面或超平面。 這種方法似乎仍然限制于變量之間的嚴(yán)格線性關(guān)系,但事實(shí)證明,我們也可以使其寬松。

基函數(shù)回歸

用于將線性回歸適配變量之間的非線性關(guān)系的一個(gè)技巧是,根據(jù)基函數(shù)來轉(zhuǎn)換數(shù)據(jù)。在超參數(shù)和模型驗(yàn)證特征工程中使用的PolynomialRegression流水線中,我們已經(jīng)看到了其中的一個(gè)版本。這個(gè)想法是使用我們的多維線性模型:

y = a0 + a1x1 + a2x2 + ...

并從我們的一維輸入x中構(gòu)造x1x2、x3,以及其他。也就是,我們讓xn = fn(x),其中fn是轉(zhuǎn)換數(shù)據(jù)的函數(shù)。

例如,如果fn(x) = x ** n,我們的模型就變成了多項(xiàng)式回歸:

y = a0 + a1x + a2x^2 + a3x^3 + ...

要注意這仍然是個(gè)線性模型 -- 線性是指,參數(shù)an永遠(yuǎn)不會互相相乘或者相除。我們所做的是選取我們的一維x值并投影到更高維度上,以便線性擬合可以擬合xy的更復(fù)雜關(guān)系。

多項(xiàng)式基函數(shù)

多項(xiàng)式投影足夠?qū)嵱?,它?nèi)建于 Scikit-Learn,使用PolynomialFeatures轉(zhuǎn)換器。

from sklearn.preprocessing import PolynomialFeatures
x = np.array([2, 3, 4])
poly = PolynomialFeatures(3, include_bias=False)
poly.fit_transform(x[:, None])
array([[  2.,   4.,   8.],
       [  3.,   9.,  27.],
       [  4.,  16.,  64.]])

我們在這里看到,通過取每個(gè)值的指數(shù),轉(zhuǎn)換器將我們的一維數(shù)組轉(zhuǎn)換為三維數(shù)組。 然后這種新的更高維度的數(shù)據(jù)表示,可以用于線性回歸。

我們在特征工程中看到,實(shí)現(xiàn)這一目標(biāo)的最簡單的方法是使用流水線。我們以這種方式制作一個(gè) 7 階多項(xiàng)式模型:

from sklearn.pipeline import make_pipeline
poly_model = make_pipeline(PolynomialFeatures(7),
                           LinearRegression())

通過這種轉(zhuǎn)換,我們可以使用線性模型來擬合xy之間更復(fù)雜的關(guān)系。 例如,這里是一個(gè)帶有噪音的正弦波:

rng = np.random.RandomState(1)
x = 10 * rng.rand(50)
y = np.sin(x) + 0.1 * rng.randn(50)

poly_model.fit(x[:, np.newaxis], y)
yfit = poly_model.predict(xfit[:, np.newaxis])

plt.scatter(x, y)
plt.plot(xfit, yfit);

使用 7 階的多項(xiàng)式基函數(shù),我們的線性模型可以提供這個(gè)非線性數(shù)據(jù)的優(yōu)秀的擬合。

高斯基函數(shù)

當(dāng)然,也存在其他的基函數(shù)。 例如,一個(gè)有用的模式是擬合一個(gè)模型,它不是多項(xiàng)式基數(shù)的和,而是高斯基數(shù)的和。 結(jié)果可能如下圖所示:

繪圖中的陰影區(qū)域是縮放的基函數(shù),并且當(dāng)它們相加時(shí),它們通過數(shù)據(jù)再現(xiàn)平滑曲線。 這些高斯基函數(shù)不內(nèi)置在 Scikit-Learn 中,但是我們可以編寫一個(gè)自定義的轉(zhuǎn)換器來創(chuàng)建它們,如下圖所示(Scikit-Learn 轉(zhuǎn)換器實(shí)現(xiàn)為 Python 類;閱讀 Scikit-Learn 的源代碼,是了解如何創(chuàng)建它們的好方法):

from sklearn.base import BaseEstimator, TransformerMixin

class GaussianFeatures(BaseEstimator, TransformerMixin):
    """Uniformly spaced Gaussian features for one-dimensional input"""
    
    def __init__(self, N, width_factor=2.0):
        self.N = N
        self.width_factor = width_factor
    
    @staticmethod
    def _gauss_basis(x, y, width, axis=None):
        arg = (x - y) / width
        return np.exp(-0.5 * np.sum(arg ** 2, axis))
        
    def fit(self, X, y=None):
        # create N centers spread along the data range
        self.centers_ = np.linspace(X.min(), X.max(), self.N)
        self.width_ = self.width_factor * (self.centers_[1] - self.centers_[0])
        return self
        
    def transform(self, X):
        return self._gauss_basis(X[:, :, np.newaxis], self.centers_,
                                 self.width_, axis=1)
    
gauss_model = make_pipeline(GaussianFeatures(20),
                            LinearRegression())
gauss_model.fit(x[:, np.newaxis], y)
yfit = gauss_model.predict(xfit[:, np.newaxis])

plt.scatter(x, y)
plt.plot(xfit, yfit)
plt.xlim(0, 10);

我們把這個(gè)例子放在這里,只是為了說明多項(xiàng)式基函數(shù)沒有任何魔法:如果你對數(shù)據(jù)的生成過程有一些直覺,它使你認(rèn)為一個(gè)或另一個(gè)基可能是適當(dāng)?shù)?,你就可以使用它們?/p>

正則化

將基函數(shù)引入到我們的線性回歸中,使得模型更加靈活,但也可以很快導(dǎo)致過擬合(參考在超參數(shù)和模型驗(yàn)證特征工程中的討論)。 例如,如果我們選擇太多的高斯基函數(shù),我們最終得到的結(jié)果看起來不太好:

model = make_pipeline(GaussianFeatures(30),
                      LinearRegression())
model.fit(x[:, np.newaxis], y)

plt.scatter(x, y)
plt.plot(xfit, model.predict(xfit[:, np.newaxis]))

plt.xlim(0, 10)
plt.ylim(-1.5, 1.5);

將數(shù)據(jù)投影到 30 維的基上,該模型具有太多的靈活性,并且在由數(shù)據(jù)約束的位置之間達(dá)到極值。 如果我們繪制高斯基相對于它們的位置的系數(shù),我們可以看到這個(gè)原因:

def basis_plot(model, title=None):
    fig, ax = plt.subplots(2, sharex=True)
    model.fit(x[:, np.newaxis], y)
    ax[0].scatter(x, y)
    ax[0].plot(xfit, model.predict(xfit[:, np.newaxis]))
    ax[0].set(xlabel='x', ylabel='y', ylim=(-1.5, 1.5))
    
    if title:
        ax[0].set_title(title)

    ax[1].plot(model.steps[0][1].centers_,
               model.steps[1][1].coef_)
    ax[1].set(xlabel='basis location',
              ylabel='coefficient',
              xlim=(0, 10))
    
model = make_pipeline(GaussianFeatures(30), LinearRegression())
basis_plot(model)

該圖的底部圖像顯示了基函數(shù)在每個(gè)位置的幅度。 當(dāng)基函數(shù)重疊時(shí),這是典型的過擬合行為:相鄰基函數(shù)的系數(shù)相互排斥并相互抵消。 我們知道這樣的行為是有問題的,如果我們可以通過懲罰模型參數(shù)的較大值,來限制模型中的這種尖峰。 這種懲罰被稱為正則化,有幾種形式。

嶺回歸(L2 正則化)

也許最常見的正則化形式被稱為嶺回歸或 L2 正則化,有時(shí)也稱為 Tikhonov 正則化。 這通過懲罰模型系數(shù)的平方和(第二范數(shù))來實(shí)現(xiàn);在這種情況下,模型擬合的懲罰將是:

其中α是控制懲罰強(qiáng)度的自由參數(shù)。 這種類型的懲罰模型是構(gòu)建在 Scikit-Learn 中的Ridge估計(jì)器:

from sklearn.linear_model import Ridge
model = make_pipeline(GaussianFeatures(30), Ridge(alpha=0.1))
basis_plot(model, title='Ridge Regression')

α參數(shù)基本上是控制所得模型復(fù)雜度的旋鈕。 在極限α→0中,結(jié)果恢復(fù)標(biāo)準(zhǔn)線性回歸; 在極限α→∞中,所有模型響應(yīng)都將被抑制。 特別是嶺回歸的一個(gè)優(yōu)點(diǎn)是,它的計(jì)算很高效 -- 比原始線性回歸模型,幾乎不需要更多的計(jì)算成本。

套索回歸(L1 正則化)

另一種非常常見的正則化類型稱為套索,懲罰回歸系數(shù)的絕對值(第一范數(shù))之和:

雖然這在概念上非常類似于嶺回歸,但是結(jié)果可能會令人驚訝:例如,由于幾何原因,套索回歸可能的情況下傾向于稀疏模型:即,它優(yōu)先將模型系數(shù)設(shè)置為恰好為零。

我們可以復(fù)制嶺回歸的圖像,但使用 L1 歸一化系數(shù),看到這種行為:

from sklearn.linear_model import Lasso
model = make_pipeline(GaussianFeatures(30), Lasso(alpha=0.001))
basis_plot(model, title='Lasso Regression')

利用套索回歸的乘法,大多數(shù)系數(shù)恰好為零,功能行為由可用基函數(shù)的一小部分建模。 與嶺正則化一樣,α參數(shù)調(diào)整懲罰的強(qiáng)度,并且應(yīng)通過例如交叉驗(yàn)證來確定(參考超參數(shù)和模型驗(yàn)證特征工程中的討論)。

示例:預(yù)測自行車流量

例如,我們來看看我們是否可以根據(jù)天氣,季節(jié)和其他因素,來預(yù)測西雅圖 Fremont 大橋的自行車流量。 我們已經(jīng)在使用時(shí)間序列中,看到這些數(shù)據(jù)。

在本節(jié)中,我們將把自行車數(shù)據(jù)與另一個(gè)數(shù)據(jù)連接到一起,并嘗試確定天氣和季節(jié)因素(溫度,降水和日光時(shí)間)在多大程度上影響這條路上的自行車流量。 幸運(yùn)的是,NOAA 提供他們的氣象站日常數(shù)據(jù)(我使用站點(diǎn)號碼 USW00024233),我們可以輕松地使用 Pandas 連接兩個(gè)數(shù)據(jù)源。 我們將執(zhí)行一個(gè)簡單的線性回歸,將天氣和其他信息與自行車計(jì)數(shù)相關(guān)聯(lián),以便估計(jì)這些參數(shù)中的任何一個(gè)的變化,如何影響特定日期的人數(shù)。

特別地,這是一個(gè)例子,說明如何將 Scikit-Learn 的工具用于統(tǒng)計(jì)建模框架,其中假定模型的參數(shù)具有可解釋的含義。 如前所述,這不是機(jī)器學(xué)習(xí)中的標(biāo)準(zhǔn)方法,但是對于某些模型,可以這么解釋。

我們開始加載兩個(gè)數(shù)據(jù)集,按日期索引:

# !curl -o FremontBridge.csv https://data.seattle.gov/api/views/65db-xm6k/rows.csv?accessType=DOWNLOAD
import pandas as pd
counts = pd.read_csv('FremontBridge.csv', index_col='Date', parse_dates=True)
weather = pd.read_csv('data/BicycleWeather.csv', index_col='DATE', parse_dates=True)

下面我們會計(jì)算總共的自行車日常流量,并將其放到自己的DataFrame中:

daily = counts.resample('d').sum()
daily['Total'] = daily.sum(axis=1)
daily = daily[['Total']] # remove other columns

我們以前看到,使用模式通常每天都有所不同; 我們通過添加表示星期幾的二元列,來解釋它:

days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
for i in range(7):
    daily[days[i]] = (daily.index.dayofweek == i).astype(float)

與之類似,我們可能預(yù)期,騎車人在假期表現(xiàn)不同。讓我們?yōu)榇艘蔡砑右粋€(gè)指標(biāo)。

from pandas.tseries.holiday import USFederalHolidayCalendar
cal = USFederalHolidayCalendar()
holidays = cal.holidays('2012', '2016')
daily = daily.join(pd.Series(1, index=holidays, name='holiday'))
daily['holiday'].fillna(0, inplace=True)

我們也猜測,日光的小時(shí)數(shù)也應(yīng)該騎車人數(shù)。讓我們使用標(biāo)準(zhǔn)的天文計(jì)算來添加這個(gè)信息。

def hours_of_daylight(date, axis=23.44, latitude=47.61):
    """Compute the hours of daylight for the given date"""
    days = (date - pd.datetime(2000, 12, 21)).days
    m = (1. - np.tan(np.radians(latitude))
         * np.tan(np.radians(axis) * np.cos(days * 2 * np.pi / 365.25)))
    return 24. * np.degrees(np.arccos(1 - np.clip(m, 0, 2))) / 180.

daily['daylight_hrs'] = list(map(hours_of_daylight, daily.index))
daily[['daylight_hrs']].plot()
plt.ylim(8, 17)
# (8, 17)

我們還可以向數(shù)據(jù)中加入平均溫度和總降水。 除了降水的英寸之外,我們還添加一個(gè)標(biāo)志,指示一天是否干燥(降雨量為零):

# temperatures are in 1/10 deg C; convert to C
weather['TMIN'] /= 10
weather['TMAX'] /= 10
weather['Temp (C)'] = 0.5 * (weather['TMIN'] + weather['TMAX'])

# precip is in 1/10 mm; convert to inches
weather['PRCP'] /= 254
weather['dry day'] = (weather['PRCP'] == 0).astype(int)

daily = daily.join(weather[['PRCP', 'Temp (C)', 'dry day']])

最后,讓我們添加一個(gè)從第 1 天起增加的計(jì)數(shù)器,并且測量已經(jīng)過去了幾年。 這將讓我們度量,在每日的流量中,任何觀測到的年度增長或下降:

daily['annual'] = (daily.index - daily.index[0]).days / 365.

現(xiàn)在我們的數(shù)據(jù)有序了,我們可以看一看:

daily.head()

最后,我們可以在視覺上,比較總共的和預(yù)測的自行車流量。

daily[['Total', 'predicted']].plot(alpha=0.5);

很明顯,我們錯(cuò)過了一些關(guān)鍵特征,特別是在夏季。 我們的特征還不完整(即,人們不僅僅根據(jù)這些,決定是否騎車去上班),或者有一些非線性關(guān)系,我們沒有考慮到(例如,也許人們在高和低溫度下騎行較少)。 然而,我們的粗略近似足以提供一些見解,我們可以看一下線性模型的系數(shù),來估計(jì)每個(gè)特征對每日自行車數(shù)量的貢獻(xiàn):

params = pd.Series(model.coef_, index=X.columns)
params
Mon              504.882756
Tue              610.233936
Wed              592.673642
Thu              482.358115
Fri              177.980345
Sat            -1103.301710
Sun            -1133.567246
holiday        -1187.401381
daylight_hrs     128.851511
PRCP            -664.834882
dry day          547.698592
Temp (C)          65.162791
annual            26.942713
dtype: float64

沒有一些不確定性的度量,這些數(shù)字難以解釋。 我們可以使用數(shù)據(jù)的引導(dǎo)重采樣,來快速計(jì)算這些不確定性:

from sklearn.utils import resample
np.random.seed(1)
err = np.std([model.fit(*resample(X, y)).coef_
              for i in range(1000)], 0)

估計(jì)了這些誤差,讓我們再次看看結(jié)果:

print(pd.DataFrame({'effect': params.round(0),
                    'error': err.round(0)}))
              effect  error
Mon            505.0   86.0
Tue            610.0   83.0
Wed            593.0   83.0
Thu            482.0   85.0
Fri            178.0   81.0
Sat          -1103.0   80.0
Sun          -1134.0   83.0
holiday      -1187.0  163.0
daylight_hrs   129.0    9.0
PRCP          -665.0   62.0
dry day        548.0   33.0
Temp (C)        65.0    4.0
annual          27.0   18.0

我們首先看到,每周的基線的趨勢相對穩(wěn)定:平日比周末和假日有更多的騎車人。我們看到,每增加一小時(shí)的日光,就多出129 ± 9個(gè)騎車人; 每升高 1 攝氏度的溫度,就多出65 ± 4人;干燥的日子意味著平均有548 ± 33個(gè)騎車人,每一英寸的降水意味著665 ± ??62個(gè)人將自行車放在家里。一旦考慮到所有這些影響,我們每年都會適度增加27 ± 18新的日常騎車人。

我們的模型幾乎肯定缺少一些相關(guān)信息。例如,這個(gè)模型不能解釋非線性效應(yīng)(如降水和低溫的影響)以及每個(gè)變量內(nèi)的非線性趨勢(如在非常冷和非常熱的溫度下的騎行傾向)。此外,我們已經(jīng)拋棄了一些更細(xì)致的信息(如雨天的早上和下午之間的差異),我們忽略了天數(shù)之間的相關(guān)性(例如星期二下雨可能影響周三的數(shù)值,或連續(xù)下雨后的意想不到的陽光燦爛的日子的效果)。這些都是潛在的有趣效果,如果你愿意,你現(xiàn)在有了開始探索它們的工具!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容