线性回归原理与实现:从最小二乘法到Scikit-learn实战

发布时间:2026/8/2 20:14:58
线性回归原理与实现:从最小二乘法到Scikit-learn实战 1. 从“猜”到“算”线性回归要解决的根本问题想象一个场景你是一家奶茶店的老板想搞清楚每天的气温和奶茶销量之间到底有什么关系。你记录了最近30天的数据发现气温越高销量似乎也越高。但“似乎”这个词太模糊了你没法精确地告诉店员“明天28度我们大概能卖多少杯” 你只能凭感觉猜。线性回归要做的就是把这种“凭感觉猜”变成“有依据地算”。它通过数学方法从一堆看似杂乱的数据点里找出一条最能代表它们整体趋势的直线或者平面、超平面这条线就是你的“预测公式”。有了它你就能输入明天的气温直接算出一个预期的销量从而指导备货、排班等决策。这背后的核心思想就是用一个简单的线性模型y ax b去逼近复杂的现实世界关系虽然现实往往非线性但在很多局部或特定场景下线性模型因其简单、可解释性强、计算高效依然是数据分析的基石工具。2. 原理拆解最小二乘法是如何“找到”最佳直线的我们知道了目标是找一条直线但“最佳”的标准是什么直观上看这条直线应该让所有的数据点都离它“尽可能近”。在数学上这个“近”是用垂直距离即因变量y方向上的误差的平方和来衡量的。这就是最小二乘法的核心寻找一条直线使得所有观测点的实际y值与直线上预测的y值之差的平方和最小。2.1 从几何与代数两个视角理解误差假设我们有n组观测数据 (x_i, y_i) i1,2,...,n。我们假设它们服从一个线性关系y_i β_0 β_1 * x_i ε_i。其中β_0是截距β_1是斜率ε_i是第i个观测的随机误差它代表了模型无法解释的部分比如某天突然的促销活动对销量的影响。我们的模型是ŷ_i β_0 β_1 * x_iŷ_i是预测值。那么第i个点的误差就是e_i y_i - ŷ_i。代数视角最小二乘法的目标函数就是所有误差的平方和Sum of Squared Errors, SSESSE(β_0, β_1) Σ(y_i - ŷ_i)^2 Σ(y_i - (β_0 β_1 * x_i))^2我们的任务就是找到一对(β_0, β_1)让这个SSE的值达到最小。几何视角我们可以把所有的观测y向量一个n维向量、由x构成的矩阵X的列空间一个二维子空间由全1向量和x向量张成想象在一个高维空间里。寻找最佳拟合直线本质上是在X的列空间里寻找一个向量ŷ即预测值向量使得它到实际观测向量y的欧几里得距离最短。根据几何知识这个最短距离是通过y向列空间做垂直投影得到的。因此最小二乘解给出的预测值ŷ就是y在X列空间上的投影。这个视角非常优美它将拟合问题转化为了一个空间投影问题。2.2 推导闭式解一阶导数为零如何找到使SSE最小的β_0和β_1这是一个二元函数求极值的问题最直接的方法是分别对β_0和β_1求偏导数并令其等于0。首先对β_0求偏导∂SSE/∂β_0 -2 * Σ(y_i - β_0 - β_1 * x_i) 0整理得Σy_i n * β_0 β_1 * Σx_i(方程1)然后对β_1求偏导∂SSE/∂β_1 -2 * Σ[(y_i - β_0 - β_1 * x_i) * x_i] 0整理得Σ(x_i * y_i) β_0 * Σx_i β_1 * Σ(x_i^2)(方程2)方程1和方程2构成了一个关于β_0和β_1的二元一次方程组称为正规方程组。解这个方程组就能得到著名的闭式解公式β_1 [n * Σ(x_i * y_i) - Σx_i * Σy_i] / [n * Σ(x_i^2) - (Σx_i)^2]β_0 (Σy_i / n) - β_1 * (Σx_i / n) ȳ - β_1 * x̄其中x̄和ȳ分别是x和y的样本均值。这个解是全局最优的因为SSE是关于β_0和β_1的凸二次函数一阶导数为零的点就是全局最小值点。注意这个推导过程假设误差ε_i是独立同分布且均值为0方差恒定。如果这些基本假设被严重违反如存在异方差性、自相关等最小二乘估计虽然仍是无偏的但可能不再是“最优”的方差不是最小此时需要考虑加权最小二乘或其他方法。3. 从一元到多元当世界不止一个影响因素奶茶销量可能不只受气温影响还受星期几、是否有促销、门店位置等因素影响。此时我们就需要将一元线性回归扩展到多元线性回归。模型变为y β_0 β_1 * x_1 β_2 * x_2 ... β_p * x_p ε其中x_1, x_2, ..., x_p是p个自变量特征。3.1 矩阵形式优雅与高效的统一使用矩阵表示能极大地简化描述和计算。令y为 n×1 的观测值向量。X为 n×(p1) 的设计矩阵第一列通常全为1对应截距β_0后面p列是各个特征的观测值。β为 (p1)×1 的系数向量[β_0, β_1, ..., β_p]^T。ε为 n×1 的误差向量。则模型可写为y Xβ ε最小二乘的目标仍然是最小化误差平方和SSE(β) (y - Xβ)^T (y - Xβ)通过对向量β求导涉及矩阵微积分令导数为零得到正规方程组的矩阵形式X^T X β X^T y当X^T X可逆时即X列满秩各特征间不存在严格的线性相关我们可以得到系数β的闭式解β (X^T X)^{-1} X^T y这个公式是多元线性回归理论的核心它清晰地展示了系数估计如何依赖于数据X和y。3.2 多重共线性当特征“抱团”时的问题在多元回归中一个关键挑战是多重共线性即某些自变量之间存在高度相关性。例如预测房价时同时使用“房屋面积”和“房间数量”这两个特征通常是相关的。多重共线性会带来什么问题系数估计不稳定(X^T X)接近奇异矩阵其逆矩阵变得非常敏感数据微小的变动可能导致系数估计值发生巨大变化。这会使模型难以解释因为系数不再可靠地代表该特征独自的贡献。标准误膨胀系数估计的标准误会变大导致t检验的统计量变小可能使得原本重要的特征变得“统计不显著”。如何诊断和处理诊断计算方差膨胀因子。VIF衡量了由于多重共线性一个自变量的系数估计的方差被放大了多少倍。通常VIF 10 被认为存在严重的多重共线性。处理特征选择使用领域知识或算法如LASSO回归、逐步回归剔除冗余特征。主成分回归将原始特征转换为一组不相关的主成分再用主成分做回归。岭回归在损失函数中加入系数平方和作为惩罚项即L2正则化这能稳定系数估计但会引入偏差以换取方差降低。4. 算法实现从公式到代码的跨越理解了原理实现就变成了“翻译”工作。我们将分别用纯Python/Numpy实现和借助Scikit-learn库实现并对比其异同。4.1 底层实现用Numpy“手搓”最小二乘这种方式能让我们透彻理解公式适合教学和定制化需求。import numpy as np class SimpleLinearRegression: 一元线性回归的纯Numpy实现 def __init__(self): self.coef_ None # 斜率 self.intercept_ None # 截距 def fit(self, X, y): 根据公式计算斜率和截距 X: 一维数组或列向量形状 (n_samples,) y: 一维数组形状 (n_samples,) X np.asarray(X).flatten() y np.asarray(y).flatten() n len(X) # 计算分子和分母 numerator n * np.sum(X * y) - np.sum(X) * np.sum(y) denominator n * np.sum(X**2) - np.sum(X)**2 if denominator 0: raise ValueError(分母为零X的方差为零或存在共线性问题。) self.coef_ numerator / denominator self.intercept_ np.mean(y) - self.coef_ * np.mean(X) return self def predict(self, X): X np.asarray(X).flatten() return self.intercept_ self.coef_ * X # 使用示例 if __name__ __main__: # 生成一些模拟数据 y 3 2*x 噪声 np.random.seed(42) X np.random.rand(100) * 10 y 3 2 * X np.random.randn(100) * 2 model SimpleLinearRegression() model.fit(X, y) print(f截距 (β0): {model.intercept_:.4f}) print(f斜率 (β1): {model.coef_:.4f}) # 预测新值 X_new np.array([5, 7.5, 10]) print(f预测值: {model.predict(X_new)})对于多元线性回归我们需要实现矩阵运算版本class MultipleLinearRegression: 多元线性回归的纯Numpy实现闭式解 def __init__(self, fit_interceptTrue): self.fit_intercept fit_intercept self.coef_ None # 系数向量包含截距如果存在 def fit(self, X, y): X: 二维数组形状 (n_samples, n_features) y: 一维数组形状 (n_samples,) X np.asarray(X) y np.asarray(y).flatten() if self.fit_intercept: # 在设计矩阵X前添加一列1用于估计截距 X np.c_[np.ones(X.shape[0]), X] # 核心公式β (X^T X)^{-1} X^T y XTX X.T X # 矩阵乘法 # 检查是否可逆 if np.linalg.matrix_rank(XTX) XTX.shape[0]: print(警告X^T X 矩阵接近奇异解可能不稳定。考虑使用正则化或检查共线性。) # 使用伪逆以增加数值稳定性 self.coef_ np.linalg.pinv(XTX) (X.T y) else: self.coef_ np.linalg.inv(XTX) (X.T y) return self def predict(self, X): X np.asarray(X) if self.fit_intercept: X np.c_[np.ones(X.shape[0]), X] return X self.coef_ # 使用示例两个特征 np.random.seed(42) n_samples 100 X_multi np.random.randn(n_samples, 2) # 两个特征 # 真实关系y 5 1.5*x1 - 2*x2 噪声 y_multi 5 1.5*X_multi[:, 0] - 2*X_multi[:, 1] np.random.randn(n_samples)*0.5 model_multi MultipleLinearRegression(fit_interceptTrue) model_multi.fit(X_multi, y_multi) print(系数包含截距:, model_multi.coef_)实操心得自己实现时数值稳定性是首要考虑。直接求逆np.linalg.inv(XTX)在XTX条件数很大即接近奇异时极易产生数值误差。在生产环境中更稳健的做法是使用奇异值分解或QR分解来求解最小二乘问题例如使用np.linalg.lstsq(X, y)函数它内部就采用了SVD方法能自动处理秩亏的情况。4.2 生产级实现拥抱Scikit-learn对于绝大多数实际应用使用成熟的库是更高效、更安全的选择。Scikit-learn提供了工业级的实现内置了各种优化和验证工具。from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, r2_score from sklearn.preprocessing import StandardScaler import pandas as pd # 假设我们有一个DataFrame df包含特征和标签 # df pd.read_csv(your_data.csv) # X df[[feature1, feature2, feature3]] # y df[target] # 这里用模拟数据 X, y make_regression(n_samples200, n_features3, noise10, random_state42) # 1. 数据分割永远要先分割再做任何预处理避免数据泄露 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 2. 特征标准化对于线性模型特别是如果使用正则化标准化非常重要 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) # 只在训练集上拟合scaler X_test_scaled scaler.transform(X_test) # 用训练集的参数转换测试集 # 3. 创建并训练模型 model LinearRegression(fit_interceptTrue) # 默认即为True model.fit(X_train_scaled, y_train) # 4. 查看模型参数 print(f截距: {model.intercept_:.4f}) print(f系数: {model.coef_}) # 5. 在测试集上评估 y_pred model.predict(X_test_scaled) mse mean_squared_error(y_test, y_pred) r2 r2_score(y_test, y_pred) print(f测试集均方误差(MSE): {mse:.4f}) print(f测试集决定系数(R²): {r2:.4f}) # 6. 预测新样本 new_sample np.array([[1.5, -0.5, 0.8]]) new_sample_scaled scaler.transform(new_sample) # 必须使用相同的scaler转换 prediction model.predict(new_sample_scaled) print(f新样本预测值: {prediction[0]:.4f})踩坑提醒使用Scikit-learn时一个非常容易忽略的坑是数据预处理流程。StandardScaler的fit_transform只能用在训练集上然后用训练集得到的均值和标准差去transform测试集和新数据。如果在整个数据集上fit_transform后再分割就造成了数据泄露因为测试集的信息“污染”了训练过程会导致评估结果过于乐观模型在实际应用中表现变差。务必遵循“分割 - 在训练集上拟合预处理器 - 转换训练集和测试集”这个铁律。5. 评估与诊断你的模型真的“好”吗得到一条拟合直线后我们不能只看它画出来像不像必须用定量指标和统计工具来评估其性能和可靠性。5.1 核心评估指标解读均方误差与均方根误差衡量预测值与真实值之间的平均差异。MSE对大的误差惩罚更重。MSE (1/n) * Σ(y_i - ŷ_i)^2RMSE sqrt(MSE)其量纲与y相同更易解释。决定系数 R²这是最常用的指标之一表示模型能够解释的目标变量方差的比例。R² 1 - (SSE / SST)其中SST Σ(y_i - ȳ)^2是总平方和。R² 越接近1说明模型对数据的拟合越好。但要注意R²会随着特征数量的增加而自然增大即使加入无关特征。因此在多元回归中更推荐看调整后R²它惩罚了特征数量Adj-R² 1 - [(1-R²)*(n-1)/(n-p-1)]。残差分析这是诊断模型假设是否成立的关键。我们需要绘制残差图残差 vs. 预测值。理想情况残差随机、均匀地分布在0附近呈水平带状无任何明显模式。发现问题漏斗形残差随预测值增大而散开暗示异方差性即误差方差不是常数。这会影响系数显著性检验的有效性。可考虑对y做变换如对数变换或使用加权最小二乘法。曲线模式残差呈现U型或倒U型说明模型可能遗漏了重要的非线性项如x²即模型设定偏误。自相关在时间序列数据中如果残差呈现趋势或周期性说明误差项之间存在自相关这会使标准误被低估。需要用时序模型处理。5.2 统计推断系数真的有意义吗我们得到的斜率β_1是0.5但这个0.5是真实的效应还是仅仅由抽样误差造成的这就需要假设检验。对于系数β_j我们通常检验的原假设是H0: β_j 0即该特征对y没有线性影响。检验统计量是 t 统计量t (β_j_hat - 0) / SE(β_j_hat)其中SE(β_j_hat)是系数估计的标准误。这个t值服从自由度为n-p-1的t分布。软件包如statsmodels会给出每个系数对应的p-value。p-value 显著性水平如0.05拒绝原假设认为该系数显著不为零对应特征对目标变量有显著的线性影响。p-value 显著性水平没有足够证据拒绝原假设不能认为该特征有显著影响。同时我们还可以为系数构建置信区间例如95%置信区间β_j_hat ± t_{0.025, df} * SE(β_j_hat)。这个区间提供了系数真实值可能范围的一个估计。# 使用statsmodels进行更详细的统计推断 import statsmodels.api as sm # 添加常数项截距 X_with_const sm.add_constant(X_train_scaled) model_sm sm.OLS(y_train, X_with_const).fit() # 打印详细的回归结果摘要 print(model_sm.summary())summary()输出会包含系数估计值、标准误、t值、p-value、置信区间以及R²、调整R²、F检验等整体模型检验结果是进行模型诊断和统计推断的利器。6. 超越普通最小二乘正则化与梯度下降当数据特征很多、样本量相对不足或特征间存在多重共线性时普通最小二乘法OLS可能不再是最佳选择。这时需要引入正则化或迭代求解方法。6.1 岭回归与LASSO应对过拟合与特征选择岭回归在OLS的损失函数中加入了系数向量的L2范数平方作为惩罚项Loss Σ(y_i - ŷ_i)^2 α * Σ(β_j^2)其中α 0是控制惩罚力度的超参数。L2惩罚会收缩所有系数但不会将任何系数恰好压缩到0。它主要解决多重共线性问题提高模型稳定性。LASSO回归则加入的是系数向量的L1范数作为惩罚项Loss Σ(y_i - ŷ_i)^2 α * Σ|β_j|L1惩罚的神奇之处在于它可以将一些不重要的特征的系数压缩至0从而实现自动特征选择得到一个稀疏模型解释性更强。from sklearn.linear_model import Ridge, Lasso from sklearn.model_selection import GridSearchCV # 岭回归 ridge Ridge() # 通过交叉验证选择最佳的超参数alpha param_grid {alpha: np.logspace(-3, 3, 13)} # 从10^-3到10^3 grid_search_ridge GridSearchCV(ridge, param_grid, cv5, scoringneg_mean_squared_error) grid_search_ridge.fit(X_train_scaled, y_train) print(f最佳岭回归 alpha: {grid_search_ridge.best_params_}) print(f最佳岭回归系数: {grid_search_ridge.best_estimator_.coef_}) # LASSO回归 lasso Lasso(max_iter10000) # LASSO求解需要更多迭代 param_grid {alpha: np.logspace(-3, 0, 7)} grid_search_lasso GridSearchCV(lasso, param_grid, cv5, scoringneg_mean_squared_error) grid_search_lasso.fit(X_train_scaled, y_train) print(f最佳LASSO alpha: {grid_search_lasso.best_params_}) print(f最佳LASSO系数注意稀疏性: {grid_search_lasso.best_estimator_.coef_})6.2 梯度下降当(X^T X)不可逆或数据太大时对于超大数据集样本数或特征数极大计算(X^T X)^{-1}的闭式解在内存和计算上都是不可行的。此时梯度下降及其变种随机梯度下降、小批量梯度下降成为主要的求解算法。梯度下降的思想很直观我们站在参数空间的一个随机点初始化的β环顾四周找到使损失函数SSE下降最快的方向负梯度方向然后朝那个方向走一小步学习率。重复这个过程直到走到一个最低点收敛。对于线性回归损失函数SSE的梯度非常简单∇SSE(β) -2 * X^T (y - Xβ)批量梯度下降的更新公式为β_new β_old - η * (1/n) * X^T (Xβ_old - y)其中η是学习率。class LinearRegressionGD: 使用批量梯度下降实现的线性回归 def __init__(self, learning_rate0.01, n_iters1000, fit_interceptTrue): self.lr learning_rate self.n_iters n_iters self.fit_intercept fit_intercept self.coef_ None self.loss_history [] def fit(self, X, y): X np.asarray(X) y np.asarray(y).reshape(-1, 1) n_samples, n_features X.shape if self.fit_intercept: X np.c_[np.ones((n_samples, 1)), X] n_features 1 # 初始化参数通常用小的随机数或零 self.coef_ np.zeros((n_features, 1)) # 梯度下降迭代 for i in range(self.n_iters): # 计算预测和误差 y_pred X self.coef_ error y_pred - y # 计算梯度 (1/n) * X^T * error gradients (1/n_samples) * X.T error # 更新参数 self.coef_ - self.lr * gradients # 记录损失可选 loss np.mean(error ** 2) self.loss_history.append(loss) # 简单收敛判断可选 if i % 100 0 and np.linalg.norm(gradients) 1e-6: print(f迭代 {i} 次后收敛。) break return self def predict(self, X): X np.asarray(X) if self.fit_intercept: X np.c_[np.ones((X.shape[0], 1)), X] return X self.coef_ # 使用示例 model_gd LinearRegressionGD(learning_rate0.1, n_iters500) model_gd.fit(X_train_scaled, y_train.reshape(-1,1)) print(梯度下降求解的系数:, model_gd.coef_.flatten())经验技巧梯度下降的性能极度依赖于学习率和特征尺度。如果学习率太大可能会在最小值附近震荡甚至发散如果太小收敛会非常缓慢。因此在使用梯度下降前必须对特征进行标准化如StandardScaler使所有特征处于相近的尺度这样学习率的选择会更容易收敛也会更快更稳定。这也是为什么在Scikit-learn的SGDRegressor使用随机梯度下降的线性回归器中默认设置penaltyNone时也建议对数据进行标准化。

相关新闻