尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

线性回归从原理到实战:手写实现、模型诊断与优化全解析

线性回归从原理到实战:手写实现、模型诊断与优化全解析 1. 项目概述从“预测”到“理解”的回归之旅“回归”这个词听起来有点学术但它的核心思想其实非常朴素根据已有的数据去预测一个连续的值。比如给你一套房子的面积、房龄、地段信息你能估算出它的价格吗或者给你一个地区的广告投入数据你能预测其产品销量吗这就是回归分析要解决的问题。而线性回归作为回归家族中最基础、最经典、也最实用的成员是每个踏入机器学习领域的人都绕不开的第一站。我之所以把这次实验称为“回归之线性回归”是因为它不仅仅是完成一次模型训练。更重要的是我希望通过这个实验带你穿透“调包”的表象去理解模型背后的数学原理、代码实现的每一个细节以及那些在教科书里不会写的、只有在真实数据上“摸爬滚打”后才能获得的经验。无论你是刚接触机器学习的学生还是想夯实基础的数据从业者这次实验都将是一次从“会用”到“懂行”的深度拆解。我们将从最根本的数学公式推导开始手写实现核心算法再用成熟的库进行对比验证最后深入到模型评估、问题诊断和优化技巧。准备好了吗让我们开始这场从理论到实战的回归之旅。2. 核心原理拆解不止是“一条直线”很多人对线性回归的第一印象就是“找一条直线去拟合数据点”。这个理解没错但过于简化了。线性回归的“线性”指的是模型关于参数是线性的而不一定关于特征是线性的。这是初学者最容易混淆的一个关键点。2.1 模型定义与假设一个标准的多元线性回归模型可以表示为y θ₀ θ₁x₁ θ₂x₂ ... θₙxₙ ε其中y是我们想要预测的目标变量因变量比如房价。x₁, x₂, ..., xₙ是特征变量自变量比如面积、房龄。θ₀是截距项θ₁到θₙ是每个特征对应的权重系数。ε是误差项代表了模型无法解释的随机波动。这里的关键在于无论x本身是原始特征如面积还是特征的平方如面积²、对数如log(面积)甚至交互项如面积*房龄只要模型关于参数θ是线性的即θ都是一次方那它就属于线性回归的范畴。这大大扩展了线性回归的应用能力。线性回归的有效性建立在几个核心假设之上理解这些假设对于后续诊断模型问题至关重要线性关系因变量与自变量之间存在线性关系。独立性各个观测值之间是相互独立的。同方差性误差项ε的方差在所有观测点上应保持恒定。正态性误差项ε应服从均值为0的正态分布。无多重共线性自变量之间不应存在高度相关性。注意在实际项目中这些假设很难被完全满足。我们的目标不是追求完美的假设而是理解当假设被违背时会对模型产生什么影响以及我们有哪些工具和方法去检测和缓解这些问题。例如异方差性违反同方差假设会导致回归系数的标准误估计不准确从而影响假设检验。2.2 损失函数与求解目标我们如何找到那条“最好”的直线这就需要定义一个标准来衡量“好”与“坏”。在线性回归中最常用的标准是最小二乘法。其对应的损失函数是均方误差J(θ) (1/2m) * Σ (hθ(x⁽ⁱ⁾) - y⁽ⁱ⁾)²其中m是样本数量。hθ(x⁽ⁱ⁾)是模型对第i个样本的预测值。y⁽ⁱ⁾是第i个样本的真实值。求和符号Σ对所有样本i从1到m进行。我们的目标就是找到一组参数θ使得这个损失函数J(θ)的值达到最小。为什么用平方而不是绝对值主要原因有两个一是平方函数处处可导便于我们使用基于梯度的优化方法二是它对大误差的惩罚更重模型会更倾向于避免产生巨大的预测偏差。2.3 参数求解解析解与数值解求解最小化J(θ)的θ有两种主流方法。1. 解析解正规方程对于线性回归损失函数是一个凸函数理论上存在一个闭式解解析解。通过矩阵运算可以直接求出最优参数θ (XᵀX)⁻¹ Xᵀy其中X是包含所有样本特征并添加了一列1以对应截距θ₀的设计矩阵y是目标值向量。实操心得正规方程在特征维度n不大例如小于10000且样本量适中时非常高效因为它是一次性计算。但它的计算复杂度大约是O(n³)当特征维度很高时矩阵求逆(XᵀX)⁻¹会变得非常缓慢甚至不可行矩阵可能不可逆。此外如果XᵀX接近奇异矩阵即存在多重共线性求逆会很不稳定导致参数估计方差极大。2. 数值解梯度下降这是更通用、更常用的方法尤其适用于大数据集和高维特征。其核心思想是迭代更新参数沿着损失函数下降最快的方向负梯度方向移动一小步。 参数更新公式为θⱼ : θⱼ - α * (∂J(θ)/∂θⱼ)对于线性回归偏导数有具体的表达式∂J(θ)/∂θⱼ (1/m) * Σ (hθ(x⁽ⁱ⁾) - y⁽ⁱ⁾) * xⱼ⁽ⁱ⁾其中α是学习率控制每一步更新的幅度。梯度下降有三种主要变体批量梯度下降每次更新使用全部训练数据计算梯度。计算准确但慢不适合大数据集。随机梯度下降每次更新只随机使用一个样本计算梯度。速度快但更新方向波动大收敛过程不稳定。小批量梯度下降每次更新使用一个小批量如32、64、128个样本计算梯度。这是目前最常用的折中方案兼顾了稳定性和效率。3. 从零实现手撕线性回归代码理解了原理最好的巩固方式就是亲手实现它。我们将使用Python和NumPy不借助任何机器学习库如scikit-learn的核心回归模块来完成一个线性回归类。3.1 数据准备与预处理任何模型训练的第一步都是处理数据。我们使用一个经典的波士顿房价数据集或类似的回归数据集作为示例。import numpy as np import pandas as pd from sklearn.datasets import fetch_california_housing # 使用加州房价数据集替代波士顿数据集 from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 加载数据 housing fetch_california_housing() X housing.data y housing.target.reshape(-1, 1) # 将y转换为列向量 # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 特征标准化 - 这是梯度下降收敛的关键步骤 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 注意使用训练集的均值和方差来转换测试集 # 为特征矩阵添加一列1用于计算截距项θ0 X_train_b np.c_[np.ones((X_train_scaled.shape[0], 1)), X_train_scaled] X_test_b np.c_[np.ones((X_test_scaled.shape[0], 1)), X_test_scaled]踩坑记录特征标准化至关重要。如果特征尺度差异巨大如一个特征范围是0-1另一个是10000-100000那么损失函数的等高线会是非常扁长的椭圆形。梯度下降会沿着陡峭的方向快速下降但在平缓的方向进展缓慢导致收敛路径呈“之字形”速度极慢。标准化后所有特征都处于相近的尺度等高线更接近圆形梯度下降可以直指中心大大加快收敛速度。切记测试集的标准化参数必须来自训练集否则就引入了数据泄露。3.2 实现梯度下降算法接下来我们实现一个基于小批量梯度下降的线性回归类。class LinearRegressionGD: def __init__(self, learning_rate0.01, n_iters1000, batch_size32): 初始化线性回归模型。 :param learning_rate: 学习率控制参数更新步长。 :param n_iters: 迭代次数。 :param batch_size: 小批量样本大小。 self.lr learning_rate self.n_iters n_iters self.batch_size batch_size self.theta None # 模型参数 self.loss_history [] # 记录每次迭代的损失值用于监控训练过程 def _compute_loss(self, X, y): 计算当前参数下的均方误差损失。 m X.shape[0] predictions X.dot(self.theta) error predictions - y loss (1/(2*m)) * np.sum(error**2) return loss def fit(self, X, y): 使用小批量梯度下降训练模型。 :param X: 训练特征形状为 (m, n1)已添加偏置列。 :param y: 训练目标形状为 (m, 1)。 m, n X.shape # 初始化参数通常用小的随机数或零初始化 self.theta np.random.randn(n, 1) * 0.01 for iteration in range(self.n_iters): # 随机打乱数据 indices np.random.permutation(m) X_shuffled X[indices] y_shuffled y[indices] # 小批量迭代 for i in range(0, m, self.batch_size): X_batch X_shuffled[i:iself.batch_size] y_batch y_shuffled[i:iself.batch_size] # 计算预测值和误差 predictions X_batch.dot(self.theta) errors predictions - y_batch # 计算梯度 (对于当前小批量) gradients (1/len(X_batch)) * X_batch.T.dot(errors) # 更新参数 self.theta - self.lr * gradients # 记录整个训练集上的损失用于监控不用于更新参数 current_loss self._compute_loss(X, y) self.loss_history.append(current_loss) # 可选每100次迭代打印一次损失 if iteration % 100 0: print(fIteration {iteration}, Loss: {current_loss:.4f}) def predict(self, X): 使用训练好的模型进行预测。 return X.dot(self.theta) def get_params(self): 返回模型参数。 return self.theta3.3 训练模型与初步评估现在让我们用自己写的类来训练模型。# 初始化并训练模型 model_gd LinearRegressionGD(learning_rate0.1, n_iters2000, batch_size64) model_gd.fit(X_train_b, y_train) # 查看最终参数 print(训练得到的参数 (截距和系数):) print(model_gd.theta.flatten()) # 在训练集和测试集上进行预测 y_train_pred model_gd.predict(X_train_b) y_test_pred model_gd.predict(X_test_b)为了评估模型性能我们引入几个核心回归指标def evaluate_regression(y_true, y_pred): 计算并打印回归任务常用评估指标。 from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score mse mean_squared_error(y_true, y_pred) rmse np.sqrt(mse) # 均方根误差与目标变量同单位更易解释 mae mean_absolute_error(y_true, y_pred) # 平均绝对误差对异常值不敏感 r2 r2_score(y_true, y_pred) # 决定系数越接近1越好 print(f均方误差 (MSE): {mse:.4f}) print(f均方根误差 (RMSE): {rmse:.4f}) print(f平均绝对误差 (MAE): {mae:.4f}) print(f决定系数 (R² Score): {r2:.4f}) return mse, rmse, mae, r2 print(\n 训练集性能 ) _ evaluate_regression(y_train, y_train_pred) print(\n 测试集性能 ) _ evaluate_regression(y_test, y_test_pred)4. 进阶话题模型诊断、优化与对比手写实现让我们理解了内核但在实际工作中我们更多是站在巨人的肩膀上。接下来我们使用scikit-learn这个强大的工具箱并深入探讨模型诊断和优化。4.1 使用Scikit-learn与正规方程法from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error # sklearn的LinearRegression默认使用正规方程或奇异值分解SVD model_sk LinearRegression() model_sk.fit(X_train_scaled, y_train.ravel()) # 注意sklearn通常期望y是一维数组 y_pred_sk model_sk.predict(X_test_scaled) print(Scikit-learn模型性能) print(f测试集 RMSE: {np.sqrt(mean_squared_error(y_test, y_pred_sk)):.4f}) print(f截距: {model_sk.intercept_:.4f}) print(f系数: {model_sk.coef_})你会发现sklearn的结果和我们梯度下降的结果应该非常接近。这验证了我们手写实现的正确性。4.2 模型诊断你的线性回归“健康”吗训练完模型不能只看R²或RMSE必须进行诊断检查之前提到的那些统计假设是否被严重违反。1. 残差分析残差e y_true - y_pred是诊断的核心。健康的残差应该围绕0随机分布无明显的模式如曲线、漏斗形。服从均值为0的正态分布。import matplotlib.pyplot as plt import scipy.stats as stats # 计算残差 residuals y_test - y_pred_sk.reshape(-1, 1) fig, axes plt.subplots(1, 3, figsize(15, 4)) # 1. 残差 vs. 预测值散点图 axes[0].scatter(y_pred_sk, residuals, alpha0.5) axes[0].axhline(y0, colorr, linestyle--) axes[0].set_xlabel(Predicted Values) axes[0].set_ylabel(Residuals) axes[0].set_title(Residuals vs. Predicted) # 理想情况点均匀分布在水平线y0周围无规律。 # 2. 残差Q-Q图检验正态性 stats.probplot(residuals.flatten(), distnorm, plotaxes[1]) axes[1].set_title(Q-Q Plot) # 3. 残差直方图 axes[2].hist(residuals, bins30, edgecolorblack, densityTrue) axes[2].set_xlabel(Residuals) axes[2].set_title(Histogram of Residuals) # 可叠加正态分布曲线 from scipy.stats import norm x np.linspace(residuals.min(), residuals.max(), 100) axes[2].plot(x, norm.pdf(x, residuals.mean(), residuals.std()), r-, lw2) plt.tight_layout() plt.show()如果残差图呈现漏斗形即残差方差随预测值增大而增大则存在异方差性这会影响假设检验的可靠性。如果Q-Q图上的点明显偏离对角线则残差非正态。2. 多重共线性诊断特征间高度相关会导致系数估计不稳定难以解释。常用诊断工具是方差膨胀因子。from statsmodels.stats.outliers_influence import variance_inflation_factor import statsmodels.api as sm # 计算VIF X_with_const sm.add_constant(X_train_scaled) # 添加常数项 vif_data pd.DataFrame() vif_data[feature] [const] housing.feature_names.tolist() vif_data[VIF] [variance_inflation_factor(X_with_const, i) for i in range(X_with_const.shape[1])] print(vif_data)通常VIF 5 或 10 就表明存在较强的多重共线性需要考虑删除特征、合并特征或使用正则化方法。4.3 优化策略当简单线性回归不够用时1. 特征工程线性回归的性能很大程度上依赖于输入特征的质量。多项式特征如果怀疑存在非线性关系可以添加特征的平方项、立方项等。sklearn.preprocessing.PolynomialFeatures可以自动生成。交互项考虑特征之间的相互作用例如面积 * 房龄。分箱将连续特征离散化成几个区间如将年龄分为青年、中年、老年有时能捕捉非线性关系。处理异常值线性回归对异常值敏感。可以通过可视化箱线图识别并根据业务逻辑决定是修正、删除还是用缩尾法处理。2. 正则化岭回归与Lasso回归当特征多、样本少或存在多重共线性时普通最小二乘法容易产生过拟合系数过大。正则化通过在损失函数中添加惩罚项来约束系数大小。岭回归惩罚项是系数的L2范数平方λΣθᵢ²。它会收缩所有系数但不会将任何系数恰好设为0。Lasso回归惩罚项是系数的L1范数λΣ|θᵢ|。它倾向于产生稀疏解即将一些不重要的特征的系数压缩为0因此兼具特征选择的功能。from sklearn.linear_model import Ridge, Lasso from sklearn.model_selection import GridSearchCV # 使用网格搜索寻找最佳的正则化强度 alpha ridge Ridge() lasso Lasso(max_iter10000) # Lasso需要更多迭代才能收敛 param_grid {alpha: [0.001, 0.01, 0.1, 1, 10, 100, 1000]} ridge_grid GridSearchCV(ridge, param_grid, cv5, scoringneg_mean_squared_error) ridge_grid.fit(X_train_scaled, y_train.ravel()) print(f最佳岭回归 alpha: {ridge_grid.best_params_[alpha]}) lasso_grid GridSearchCV(lasso, param_grid, cv5, scoringneg_mean_squared_error) lasso_grid.fit(X_train_scaled, y_train.ravel()) print(f最佳Lasso回归 alpha: {lasso_grid.best_params_[alpha]}) # 比较系数 print(\n岭回归系数 (部分):, ridge_grid.best_estimator_.coef_[:5]) print(Lasso回归系数 (部分):, lasso_grid.best_estimator_.coef_[:5]) # 观察Lasso是否将某些系数设为了0 print(fLasso非零系数个数: {np.sum(lasso_grid.best_estimator_.coef_ ! 0)} / {len(lasso_grid.best_estimator_.coef_)})3. 鲁棒回归当数据中存在显著异常值而你又不想直接删除它们时可能包含重要信息可以考虑使用Huber回归等鲁棒回归方法它们对异常值的敏感性低于最小二乘法。5. 常见问题与实战排查指南在实际操作中你一定会遇到各种各样的问题。下面是一个快速排查指南。问题现象可能原因诊断方法与解决方案模型在训练集上表现很好在测试集上很差过拟合1.检查学习曲线绘制训练集和验证集误差随样本量变化的曲线。如果两条线差距大且验证误差高则是过拟合。2.解决方案增加训练数据、减少特征数量或使用Lasso进行特征选择、增强正则化增大岭回归/Lasso的alpha值。模型在训练集和测试集上表现都很差欠拟合1.检查特征特征与目标相关性可能很弱或者关系是非线性的。2.解决方案进行更深入的特征工程如添加多项式特征、交互项、尝试更复杂的模型但线性回归本身复杂度有限、检查是否有数据质量问题。梯度下降损失不下降或震荡剧烈学习率设置不当1.绘制损失历史曲线如果曲线上升或剧烈震荡学习率可能太大如果下降极其缓慢学习率可能太小。2.解决方案尝试一系列学习率如0.001, 0.01, 0.1, 1使用学习率衰减策略或使用自适应优化算法如Adam但在手写实现中我们专注于理解SGD。3.务必进行特征标准化这是稳定梯度下降的前提。某些特征的系数非常大或非常小难以解释特征尺度不一致或存在多重共线性1.检查特征尺度确保所有连续特征都已标准化或归一化。2.计算VIF诊断多重共线性。如果存在考虑删除高相关特征之一、使用PCA降维或转向岭回归。残差图显示明显的模式如曲线、漏斗形模型假设被违反1.曲线模式可能意味着存在非线性关系。尝试添加多项式特征或使用非线性变换如对数变换。2.漏斗形模式存在异方差性。可以考虑对目标变量y进行变换如对数变换或使用加权最小二乘法。预测出现负值但业务上不可能为负如房价模型未考虑目标变量的分布线性回归本身不做范围限制。如果目标变量严格为正且可能呈指数分布可以尝试对y取对数后建模即对数线性模型log(y) θX预测时再取指数变换回来。个人经验分享线性回归项目中最耗时的往往不是调参而是数据清洗和特征工程。我曾在一个预测项目上花了80%的时间在处理缺失值、异常值、构造有业务意义的特征和特征变换上。模型本身可能一天就搭好了但让模型真正“好用”的数据准备工作才是价值所在。另外一定要保存好数据预处理如标准化器和特征工程的管道在部署模型对新数据进行预测时必须使用与训练时完全相同的处理流程这是保证线上效果稳定的关键。
返回列表