线性回归建议(线性回归终极指南)(1)

> Photo by Lanju Fotografie on Unsplash

在本文中,我们将讨论机器学习中使用的线性回归模型。 为这篇文章建模将意味着使用机器学习技术从数据中学习一组功能与我们希望预测的功能之间的关系。 让我们引入一些数据以使这个想法更加具体。

from sklearn.datasets import load_boston import pandas as pd import seaborn as sns import matplotlib.pyplot as plt import numpy as np from sklearn.model_selection import learning_curve from sklearn.metrics import make_scorer %matplotlib inline np.random.seed(42) boston_data = load_boston() boston_df = pd.DataFrame(boston_data.data, columns=boston_data.feature_names) TARGET = boston_data.target

这是我们的数据的一些描述:

· CRIM —城镇居民人均犯罪率

· ZN-25,000平方英尺以上的土地划为住宅用地的比例。

· IND—每个城镇非零售业务英亩的比例。

· CHAS —查尔斯河虚拟变量(如果束缚河,则为1;否则为0)

· NOX-一氧化氮浓度(百万分之几)

· RM —每个住宅的平均房间数

· AGE-1940年之前建造的自有单位的比例

· DIS-与五个波士顿就业中心的加权距离

· RAD —径向公路的可达性指数

· TAX —每10,000美元的全值财产税率

· PTRATIO-各镇师生比例

· B — 1000(Bk-0.63)²,其中Bk是按城镇划分的黑人比例

· LSTAT-人口地位降低百分比

· TARGET-拥有住房的中位价值(以$ 1000为单位)

此数据集的目标是否使用要素(目标以外的所有特征)来预测目标(中间房屋价值)。

线性回归

我们该怎么做?

对于我们的第一步,让我们简化一下问题。 假设我们只想使用LSAT来预测TARGET。

plt.scatter(boston_df['LSTAT'], target)

线性回归建议(线性回归终极指南)(2)

在x轴上,我们有LSTAT和y轴TARGET。 单看它似乎存在负相关关系:随着LSTAT上升,TARGET下降。

成本/评估功能

我们如何解决根据LSTAT预测目标的问题? 一个开始思考的好地方是:说我们开发了许多模型来预测目标,那么我们将如何挑选最佳模型呢? 一旦确定了这一点,我们的目标便是最小化/最大化该值。

如果您可以将问题简化为一个评估指标,这将非常有用,因为这样可以很轻松地迭代模型开发。 但是,在工业上,这可能很棘手。 有时候,您不清楚模型要最大化/最小化的程度。 但这对另一个职位是一个挑战。

因此,对于这个问题,我将提出以下评估指标:均方误差(MSE)。 要了解MSE,请定义一些术语:

线性回归建议(线性回归终极指南)(3)

· 这是我们对第i个数据点的预测值

线性回归建议(线性回归终极指南)(4)

· 这是第i个数据点的实际值

· n —数据点数

因此,MSE为:

线性回归建议(线性回归终极指南)(5)

用英语来说,对于每个点,我们从实际值中减去预测值。 然后,由于我们不在乎错误的方向,所以我们对差值求平方。 最后,我们将所有这些值取平均值。 基本上,我们是说我们希望我们的预测与实际值之间的平均距离较小。

您可能想知道,为什么我们对值取平方而不是取绝对值。 事实证明,对于以下某些数学运算,对值进行平方可以很好地解决问题。 同样,它是最大似然估计。 但是,这确实有效果,因为我们对所有差异进行平方运算,因此可以在平均数中加权较大的误差。

我们的模型

现在我们有了成本函数,我们如何找到一种最小化成本函数的方法? 在这篇文章中,我们将回顾线性回归模型。 该模型如下:

线性回归建议(线性回归终极指南)(6)

其中j是我们拥有的预测变量的数量,而beta值是我们的系数,其中beta 0是截距。 基本上,我们的模型是预测变量与截距的线性组合。

现在我们有了一个模型和一个成本函数,我们面临的挑战就是为我们的模型找到使数据的MSE最小化的beta值。 对于线性回归,实际上存在一个封闭形式的解,称为法线方程。 不过,在本文中,我们将使用一种在机器学习中更常见的技术-梯度下降。

梯度下降

梯度下降是我们从优化中借鉴的技术。 这是一种非常简单但功能强大的算法,可用于查找函数的最小值。

· 选择一个随机的起始值

· 采取与当前点的渐变负比例成比例的步骤

· 重复直到收敛

如果函数是凸的,则此技术将找到全局最小值,否则,我们只能证明它将找到局部最小值。

我们需要回答的第一个问题是:成本函数是凸的吗? 让我们来看看:

线性回归建议(线性回归终极指南)(7)

上面我们所做的是取一系列LSTAT的系数值,并为每个系数值计算了我们数据中的MSE。 如果再绘制这些曲线,我们将得到上面的曲线-看起来很凸! 实际上,事实证明,带有线性回归模型的MSE函数将始终是凸的! 这意味着我们可以使用梯度下降来找到模型的最佳系数!

梯度下降比机器学习的普通方程式更普遍的原因之一是,随着我们增加特征数量,梯度下降的比例要好得多。 它也是一种通用的优化技术,会在整个机器学习中弹出,因此了解其工作原理非常有价值。

梯度下降

如果再次查看梯度下降的伪代码,您会发现我们真正需要做的就是计算梯度。 那么-什么是渐变? 它们只是关于系数的偏导数。 对于我们拥有的每个系数,我们将需要针对该系数计算MSE的导数。 让我们开始吧!

让我们通过一个截距和一个单独的变量LSTAT扩展我们的简单示例的成本:

线性回归建议(线性回归终极指南)(8)

现在,对于相对于beta 0的此导数,我们得到(乘以-1):

线性回归建议(线性回归终极指南)(9)

对于Beta 1:

线性回归建议(线性回归终极指南)(10)

现在,让我们运行我们的梯度下降算法,并确认MSE实际上正在下降:

线性回归建议(线性回归终极指南)(11)

beta_0: 34.553840879456807beta_1: -0.95004935376241229

线性回归建议(线性回归终极指南)(12)

上面打印的第一张图显示了我们进行梯度下降时的MSE值。 正如我们所期望看到的,随着算法的运行,MSE随着时间的推移而下降,这意味着我们不断接近最佳解决方案。

从图中还可以看到,我们可能早些停止了,因为MSE基本上在大约4,000次迭代中趋于平缓。

运行梯度下降发现最佳截距为34.55,最佳斜率为-0.95。

上图显示了数据之上的那条线,实际上看起来是最合适的线。

学习率

我们尚未讨论的一个参数是学习率。 该速率是一个超参数,用于确定我们离梯度方向的距离。 您如何知道该选择什么价值? 通常,可以尝试许多值,以下是我相信Andrew Ng建议的一些值:.001,.003,.01,.03,.1,.3、1、3

选择一个太小的值会导致收敛变慢。 选择一个太大的值可能导致超出最小值并产生偏差。

还有其他梯度下降优化器,它们更复杂,可以为您适应学习率的超时变化。 这也是您可以自己做的,随着时间的流逝,您会逐渐降低学习速度。

什么时候停止迭代?

在我的代码中,我只是选择将循环运行10,000次。 为什么是10,000? 除了我很确定它足够收敛之外,没有其他真正的原因。 通常这不是最佳实践。 一些更好的想法是:

· 监视每个循环之后的成本,并在其降低的幅度小于某个公差(例如0.001)时停止。

· 使用验证集并跟踪损失(例如,MSE)。 当它停止减少时,停止。

规范化数据

使用梯度下降时,您希望所有数据都标准化。 减去均值并除以所有训练功能的标准差。 如果您的成本函数不是凸的,通常可以使训练更快,并减少陷入局部最优的机会。

其他类型的梯度下降

我们在这里显示的梯度下降是香草形式,这意味着每次系数更新都会使用所有数据来计算梯度。 还有随机梯度下降,它仅使用一行数据来更新每个循环中的系数。 这具有更大的可扩展性,因为您只需要在更新之前一次只查看一个数据行,但由于尝试使用仅对单个数据点计算的梯度进行导航,因此随机性也要大得多。

梯度下降的另一种类型是小批量梯度下降。 这种形式是两者之间的折衷,您可以选择32个批量大小(或者更好的是从小批量开始并随着时期数增加的批量计划),并且梯度下降的每次迭代都使用32个随机行 用来计算梯度的数据(在重新对同一行进行重新采样之前,它会使用所有行)。 这提供了一些可伸缩性和一些随机性。 事实证明,这种随机性实际上对于非凸(深度学习)的成本函数很有用,因为它可以帮助模型摆脱局部最小值。 这是用于非凸成本函数的最常用方法。

我们模型的假设

每当您处理模型时,最好了解模型所做的假设。 我打算在此处撰写有关此内容的部分,但Duke已经做得非常出色:http://people.duke.edu/~rnau/testing.htm。

使用Scikit-Learn

现在我们已经了解了一些理论和实现,现在让我们转向一个软件库,对数据进行线性回归。 这对于学习从头开始编写模型非常有用,但是在实践中,使用经过测试和广泛使用的库通常会更好。

from sklearn.linear_model import SGDRegressor from sklearn.metrics import mean_squared_error from sklearn.preprocessing import StandardScaler

记住要扩展您的数据-非常重要!

scaler = StandardScaler() scaler.fit(boston_df) scaled_df = scaler.transform(boston_df)

Scikit-learn有一个非常不错的API。 它提供了许多模型,并且所有模型都具有拟合和预测功能。 您可以在X和y数据上调用fit来训练模型,然后对新特征进行预测以获得预测值。 Scikit学习还提供了许多可用于评估的指标,例如MSE。 在这里,我输出根MSE(RMSE),因为这使我们回到了目标的原始比例,我发现它更容易理解。

使用我们的SGDRegressor(使用梯度下降运行线性回归),tol告诉模型何时停止迭代,而eta0是我们的初始学习率。

linear_regression_model = SGDRegressor(tol=.0001, eta0=.01) linear_regression_model.fit(scaled_df, target) predictions = linear_regression_model.predict(scaled_df) mse = mean_squared_error(target, predictions) print("RMSE: {}".format(np.sqrt(mse))) RMSE: 4.721352143256387

我们使用scikit-learn进行的培训的RMSE最终为4.72。

多项式变量

如果您还记得我们针对目标的LSTAT的图,则看起来存在多项式关系。 线性回归适合线性关系,但是如果添加多项式特征(例如LSTAT²),则可以拟合更复杂的关系。 SKLearn使这变得容易:

from sklearn.preprocessing import PolynomialFeatures poly = PolynomialFeatures(2, include_bias=False) poly_df = poly.fit_transform(boston_df) scaled_poly_df = scaler.fit_transform(poly_df) print(scaled_poly_df.shape)(506, 104) linear_regression_model.fit(scaled_poly_df, target) predictions = linear_regression_model.predict(scaled_poly_df) mse = mean_squared_error(target, predictions) print("RMSE: {}".format(np.sqrt(mse))) RMSE: 3.77419484950651

多项式特征命令生成了一个新的特征矩阵,该矩阵包含度数小于或等于指定度数的所有特征的多项式组合(在我们的示例2中)。 然后,我们对这些数据进行缩放,并将其输入到我们的模型中。 而且我们得到了更好的RMSE — 3.77。 但是请注意,这些结果仅在我们的训练数据上用于说明。

分类变量

线性回归是拥有分类数据时需要注意的模型之一。 如果您具有一个值1、2和3的要素实际上表示"男性","女性","无响应",那么即使它们是数字,也不想以此方式将其提供给模型。 如果这样做,模型将为该功能分配一个系数-可能为0.1。 这意味着作为女性将预测值提高0.1,将无响应值提高0.2。 但是,也许女性应该将得分提高1.2,而没有反应仅提高.001。 为了解决这个问题,您应该将这些值转换为虚拟变量,以便每个值都可以具有自己的权重。 您可以在此处查看如何使用scikit-learn执行此操作。

解释模型

线性回归是一个很好的统计模型,已经存在了很长时间。 人们可以使用许多统计技术来对其进行评估和解释。 我们不会全部介绍它们,实际上,我们将主要关注非常简单的方法,这些方法在机器学习中可能比统计数据更常见。 为了更好地掌握统计技术,请阅读《统计学习入门》第3章,并查看statsmodels软件包。

首先,让我们看一下模型从所有特征中学到的系数:

linear_regression_model.fit(scaled_df, target) sorted(list(zip(boston_df.columns, linear_regression_model.coef_)), key=lambda x: abs(x[1])) [('AGE', -0.15579031087838216), ('INDUS', -0.36890070005440012), ('ZN', 0.61249837977769284), ('TAX', -0.6639660631363058), ('CRIM', -0.7135059713991182), ('CHAS', 0.73578321065548924), ('B', 0.87494012630072004), ('RAD', 1.1142142863056546), ('NOX', -1.2452942744431366), ('PTRATIO', -1.9425283730193659), ('DIS', -2.2549312823672696), ('RM', 3.0623224309690911), ('LSTAT', -3.4699728921831285)]

这些系数是多少? 它们代表要素中一个单位变化的房屋平均价格变化,而模型中的其他要素保持不变。 例如,在其他所有条件不变的情况下,LSTAT的单位增加将我们的目标(住房价格)降低3.469,RM的单位增加将我们的目标提高3.062。

真是太好了! 我们也许可以说,如果您想增加房屋价值,那么增加RM和减少LSTAT可能是一个起点。 我之所以说是因为线性回归正在研究相关性。 在我们的数据中,似乎确实是这种情况,但这本身并不意味着这些功能具有因果关系。 不过,它可能是寻找因果关系的好地方,并且确实代表了数据中可见的关系。

置信区间

通常在机器学习中,使估计值具有置信区间非常有用。 有多种方法可以执行此操作,但是一种相当通用的方法是使用引导程序。

引导程序是随机样本,它替换了我们的数据,并且该样本的大小与原始数据相同。 这是一种生成同一数据的多个视图的方法。 让我们为数据创建1,000个引导程序。

from sklearn.utils import resample n_bootstraps = 1000 bootstrap_X = [] bootstrap_y = [] for _ in range(n_bootstraps): sample_X, sample_y = resample(scaled_df, target) bootstrap_X.append(sample_X) bootstrap_y.append(sample_y)

然后,在每个这些数据集上,我们都可以拟合您的模型并获取系数:

linear_regression_model = SGDRegressor(tol=.0001, eta0=.01) coeffs = [] for i, data in enumerate(bootstrap_X): linear_regression_model.fit(data, bootstrap_y[i]) coeffs.append(linear_regression_model.coef_) coef_df = pd.DataFrame(coeffs, columns=boston_df.columns) coef_df.plot(kind='box') plt.xticks(rotation=90)

线性回归建议(线性回归终极指南)(13)

此图显示了我们训练的所有模型的每个特征所获得的系数值的范围。 AGE是一个特别有趣的功能,因为系数值既为正值又为负值,这是一个很好的信号,表明AGE与我们的目标之间可能没有任何关系。

同样,我们可以看到LSTAT的系数值具有较大的方差,而PTRATIO的系数具有较小的方差,这增加了我们对系数估计的信心。

我们甚至可以进一步挖掘LSTAT系数:

coef_df['LSTAT'].plot(kind='hist')

线性回归建议(线性回归终极指南)(14)

coef_df['LSTAT'].describe()

count 1000.000000

mean -3.599465

std 0.687444

min -5.825069 25% -4.058086 50% -3.592409 75% -3.120958

max -1.575822

Name: LSTAT, dtype: float64

这真是太好了! 现在我们可以很有把握地说,LSTAT的实际系数为负,几乎可以肯定在-1.2和-5.5之间。

训练/测试拆分和交叉验证

到目前为止,我们一直在训练我们拥有的所有数据。 这可能是有道理的,因为我们希望通过尽可能多地进行训练来最大程度地利用数据。 但是,另一方面,很难评估模型的运行情况。 这样做的原因是,如果仅使用模型训练所依据的数据来计算MSE分数,则可能会发现,当引入模型未经训练的数据时,其性能会非常差。

这个想法称为过拟合。 基本上,当模型在训练数据上的性能好于新数据时,它会过度拟合训练数据所特有的,无法推广的内容。

另一方称为偏见。 当模型确实无法很好地拟合数据时,就会具有很高的偏见。 在这种情况下,对于训练数据和在训练期间未看到的数据,MSE都会很高。

在机器学习中,总是在偏差和方差之间进行权衡。 随着模型变得越来越复杂,存在过度拟合训练数据的风险。

既然我们知道仅查看培训数据上的MSE存在问题,那么我们该如何做才能更好地判断普遍性? 以及诊断过度拟合和偏见? 通常,我们将数据分为两组:训练集和测试集。

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(scaled_df, target, test_size=0.33, random_state=42)

现在我们有了两套单独的数据集,我们可以训练我们的训练数据,并为我们的训练和测试数据计算指标(最佳做法是在调整模型后才使用您的测试数据):

linear_regression_model = SGDRegressor(tol=.0001, eta0=.01) linear_regression_model.fit(X_train, y_train) train_predictions = linear_regression_model.predict(X_train) test_predictions = linear_regression_model.predict(X_test) train_mse = mean_squared_error(y_train, train_predictions) test_mse = mean_squared_error(y_test, test_predictions) print("Train MSE: {}".format(train_mse)) print("Test MSE: {}".format(test_mse)) Train MSE: 23.33856804462054 Test MSE: 21.820947809040835

优秀的! 现在,我们在培训和测试数据上都有RMSE。 而且两者都很接近,这表明我们没有过度拟合的问题。 他们俩都低吗? 这表明存在很高的偏见。

一种研究方法是绘制学习曲线。 学习曲线绘制了我们的误差函数(MSE)以及用于训练的各种数据。 这是我们的情节:

线性回归建议(线性回归终极指南)(15)

您可以看到少于50个训练示例,训练的MSE很好,而交叉验证则很差(我们还没有谈论交叉验证,因此现在将其视为测试)。 如果我们只有那么多数据,那么它看起来像是一个高方差问题。

随着数据的增加,我们开始同时提高两个得分,并且它们变得非常接近,这表明我们没有高方差问题。 通常,在高方差的情况下,该图的两条线相距很远,如果我们继续添加更多数据,它们可能会收敛。

该图看起来更像我们有一个高偏差问题,因为我们的两条曲线非常接近且平坦。 但是,很难确定地说,因为我们可能已经达到了最佳的MSE。 在那种情况下,这将不是一个高偏差问题。 如果我们的曲线在MSE高于最佳水平时变平了,那将是一个问题。 在现实生活中,您不知道最佳的MSE是多少,因此您必须对您是否认为降低偏见会提高您的分数有一个理论上的认识,或者只是尝试一下!

解决高偏差/高方差问题

那么,既然您已经诊断出偏差或方差问题,该如何解决?

对于高方差:

· 获取更多培训数据

· 尝试一些较小的功能

· 尝试一个不太复杂的模型

· 添加正则化

对于高偏见:

· 尝试添加功能

· 尝试更复杂的模型

交叉验证和调整超参数

之前我们提到过这句话:交叉验证。 让我们现在谈谈。 到目前为止,我们已经知道,将数据分为训练集和测试集是一个好主意,以便更好地了解模型的实际效果。 很好,但是想像一下我们想测试多个不同的模型或测试模型的不同参数-例如,不同的学习率或容忍度。 我们将如何决定哪种型号或哪种参数最好? 我们会在训练数据上训练所有内容并在测试数据上进行测试吗? 希望您会发现这没有任何意义,因为那样一来,我们实际上将处在以前的位置,而无法测试我们如何处理从未见过的数据。 因此-我们希望保持测试设置不受污染,因为在一个完美的世界中,我们只有在完成所有实验并认为找到了最佳模型后才对其进行测试。

听起来我们需要第三组数据-验证组。 基本上,我们可以做的是将训练数据分为两组:训练集和验证集。 所有模型都将在训练集中进行训练,然后在我们的验证集中进行测试。 然后,我们采用在验证方面表现最佳的模型,并查看其在测试中的表现如何。 我们的测试结果表示我们认为模型可以处理看不见的数据,然后我们就完成了。

注意:这里的假设是我们的测试和验证为我们的总体样本设置了代表性。 例如,如果您的验证集中的平均房价为100万,但人口总数为30万,则您的样本不正确。 通常,我们将可用数据随机抽样到三个集合中,但是确认这些集合是好的表示总是好的。 否则,您会发现在验证和测试中运行良好的模型在实践中表现不佳。

实际上,我们通常不使用k-fold交叉验证来创建单个验证集。 这就是我们选择k的值,例如3。然后我们将训练数据分成3折。 我们随机选择2折进行训练,然后将其余部分用于测试。 然后,我们再重复2次,总共3次,以便所有观察都用于训练和验证,每个观察都恰好用于一次验证。 然后,我们将所有三个分数(在我们的示例中为MSE)取平均值,以获得特定模型的分数。 然后,我们可以针对多个模型重复此过程以找到最佳模型。

这是一段更直观地描述此过程的视频:https://www.youtube.com/watch?v=TIgfjmp-4BA

使用sklearn,此过程非常简单:

from sklearn.model_selection import RandomizedSearchCV param_dist = {"eta0": [ .001, .003, .01, .03, .1, .3, 1, 3]} linear_regression_model = SGDRegressor(tol=.0001) n_iter_search = 8 random_search = RandomizedSearchCV(linear_regression_model, param_distributions=param_dist, n_iter=n_iter_search, cv=3, scoring='neg_mean_squared_error') random_search.fit(X_train, y_train) print("Best Parameters: {}".format(random_search.best_params_)) print("Best Negative MSE: {}".format(random_search.best_score_)) Best Parameters: {'eta0': 0.01} Best Negative MSE: -25.322156767075665

在这里,我们实际上使用了随机搜索,它通常比搜索所有可能的值要好。 通常,您想为许多不同的旋钮尝试许多不同的参数,并且对所有内容进行网格搜索(尝试所有可能的组合)效率不高。 通常,您希望像上面一样使用随机搜索(随机选择组合)。 但是,由于我们只有少量的值,因此我们通过将n_iter_search设置为我们想要尝试的值的数量来将其强制为网格搜索。

我们还将cv = 3设置为3倍,并使用负MSE,因为scikit-learn中的CV函数试图最大化一个值。

您可以在此处了解有关随机搜索和网格搜索的更多信息:http://scikit-learn.org/stable/modules/grid_search.html

另外,scikit-learn还有许多其他CV函数,这些函数很有用,尤其是当您要测试具有相同折痕的不同模型时。 这是一些文档:http://scikit-learn.org/stable/modules/cross_validation.html

正则化

为了说明高方差模型,我提到了正则化。 您可以将正则化视为通过学习复杂关系来惩罚模型的一种方法。 对于线性回归,采取三种流行方法的形式。 所有这些方法都以限制我们的特征上的系数的大小为中心。 这样的想法是,如果我们高估了预测变量的影响(较大的系数),则可能是我们过度拟合了。 注意:我们仍然可以使用较大的系数。 正则化只是说,MSE的降低必须证明系数幅度的增加是合理的。

· L1正则化(Lasso):将系数的绝对值之和添加到成本函数中。 此方法可以将系数强制为零,然后可以将其用作特征选择的一种方法。

· L2正则化(Ridge):将系数平方值的总和添加到成本函数中。

· Elastic Net 弹性网:您同时添加两者并选择如何对其加权。

这些方法中的每一个都采用一个加权因子,该因子告诉您在成本函数中应对正则化项加权多少。 在scikit-learn中,它称为alpha。 零的alpha值不会增加任何代价,而较高的alpha值会因为系数较大而对模型造成很大的损失。 您可以使用交叉验证来发现Alpha值。

Sklearn使这变得容易:

from sklearn.linear_model import ElasticNetCV # l1 ratio of zero is l2 and visa-versa # alphas are how much to weight regularization clf = ElasticNetCV(l1_ratio=[.1, .5, .7, .9, .95, .99, 1], alphas=[.1, 1, 10]) clf.fit(X_train, y_train) train_predictions = clf.predict(X_train) test_predictions = clf.predict(X_test) print("Train MSE: {}".format(mean_squared_error(y_train, train_predictions))) print("Test MSE: {}".format(mean_squared_error(y_test, test_predictions))) Train MSE: 23.500345265727802 Test MSE: 21.60819303537859

在这里,我们使用了具有内置交叉验证的ElasticNetCV函数,以便为alpha选择最佳值。 l1_ratio是要赋予L1正则化的权重。 剩余的权重将应用于L2。

恭喜你!

如果您做到了这一点,那么恭喜! 那是大量的信息,但是我保证,如果您花时间吸收它,您将对线性回归及其可以做的许多事情有一个非常扎实的理解!

此外,您可以在此处找到代码的更好呈现的版本。

如果您想获得使用Python进行分析的真正深入知识,请查看我的课程!

(本文翻译自Tyler Folkman的文章《The Ultimate Guide to Linear Regression》,参考:https://towardsdatascience.com/the-ultimate-guide-to-linear-regression-4f31ae937a86)

,