资讯动态

线性回归、岭回归、逻辑回归与k-means:四大经典算法的工程实现与适用边界

发布时间:2026/10/16 6:41:53 来源:尧图企业网站定制
1. 从一条直线到数据分堆四个经典算法的真实使用边界很多人第一次接触机器学习都是从线性回归、岭回归、逻辑回归和 k-means 这四个算法开始的。它们看起来简单代码量也不大但真正在项目里用起来翻车的方式却五花八门。我见过有人拿线性回归去预测用户是否流失结果输出一堆 1.3、-0.7 这种没法解释的数值也见过有人用逻辑回归做聚类然后困惑为什么结果总是不稳定。问题的根源不在于算法本身而在于没有搞清楚每个算法到底在解决什么问题、它的输出是什么形态、什么场景下该用它、什么场景下该换别的。这篇文章不打算照本宣科地推导公式而是从实际落地的角度把这四个算法的实现思路、关键参数、常见坑点和适用边界讲清楚。每个算法我都会给出可运行的代码骨架同时解释为什么这样写、参数为什么这样设、遇到异常结果该怎么排查。无论你是刚入门想找个能跑通的参考还是已经用过但总觉得哪里不对劲应该都能从中找到一些有用的东西。需要提前说明的是本文的代码示例统一使用 Python 生态中最常见的数值计算和机器学习工具链不依赖特定平台或框架。所有数据集均使用模拟生成的虚拟数据不涉及任何真实业务数据。2. 线性回归最小二乘法的工程实现与它的三个软肋2.1 为什么最小二乘法是起点而不是终点线性回归的核心思想非常朴素找到一组权重参数使得预测值与真实值之间的残差平方和最小。这个最小二乘的目标函数之所以被广泛使用是因为它在数学上有闭式解计算效率高而且对误差服从高斯分布的假设下具有统计最优性。但工程实现中直接套用闭式解公式w (X^T X)^(-1) X^T y往往会遇到问题。最常见的就是矩阵不可逆。当特征之间存在高度相关性或者特征数量超过样本数量时X^T X会变成奇异矩阵或接近奇异矩阵求逆就会报错或者得到数值极不稳定的结果。我在实际项目中遇到过这样的情况用十几个高度相关的特征做房价预测闭式解直接抛出了LinAlgError: Singular matrix。当时的解决方案是改用梯度下降法来迭代求解或者引入正则化项。梯度下降的好处是不需要求逆坏处是需要调学习率和迭代次数收敛速度依赖特征缩放。import numpy as np class LinearRegressionGD: def __init__(self, learning_rate0.01, n_iters1000, tol1e-6): self.lr learning_rate self.n_iters n_iters self.tol tol self.weights None self.bias None self.loss_history [] def fit(self, X, y): n_samples, n_features X.shape self.weights np.zeros(n_features) self.bias 0.0 for i in range(self.n_iters): y_pred X.dot(self.weights) self.bias error y_pred - y dw (2 / n_samples) * X.T.dot(error) db (2 / n_samples) * np.sum(error) self.weights - self.lr * dw self.bias - self.lr * db loss np.mean(error ** 2) self.loss_history.append(loss) if i 0 and abs(self.loss_history[-2] - loss) self.tol: break return self def predict(self, X): return X.dot(self.weights) self.bias这段代码里有个细节值得注意学习率的设置和特征尺度强相关。如果某个特征的取值范围是 0 到 1另一个是 0 到 10000那么梯度下降会走出一条之字形的震荡路径收敛极慢甚至发散。所以实践中几乎总要配合标准化处理。2.2 特征缩放不是可选项而是必选项很多人写线性回归时跳过标准化这一步觉得反正模型会自己学权重。理论上没错但实际操作中不缩放特征会带来两个后果一是梯度下降收敛慢到无法接受二是正则化项对不同尺度的特征惩罚不均匀。我通常用StandardScaler做零均值单位方差标准化或者用MinMaxScaler缩放到 [0,1] 区间。选择哪个取决于数据分布如果特征近似高斯分布标准化更合适如果特征有明确的边界且分布不均匀归一化更稳妥。注意标准化参数必须只在训练集上拟合然后应用到验证集和测试集。如果全量数据一起标准化会造成信息泄露评估结果会偏乐观。2.3 残差诊断模型是否真的学到了东西训练完一个线性回归模型不能只看 R² 或 MSE 就完事。我习惯画三张图来诊断残差 vs 预测值、残差 QQ 图、残差 vs 每个特征。如果残差呈现明显的漏斗形或曲线形说明模型存在异方差性或非线性关系没被捕捉到。有一次我用线性回归预测某设备的能耗R² 达到了 0.92看起来很不错。但残差图显示高预测值区域的残差明显偏大说明模型在极端情况下系统性低估。后来加入二次项和交互项之后残差分布才变得均匀。这个经验告诉我线性回归的线性指的是参数线性不是特征线性可以通过构造多项式特征来拟合非线性关系。3. 岭回归当共线性成为问题时的那根救命稻草3.1 L2 正则化到底在做什么岭回归本质上就是在线性回归的损失函数上加了一个 L2 惩罚项Loss MSE alpha * ||w||²。这个改动看似简单但解决了一个非常实际的问题——当特征之间存在多重共线性时普通最小二乘估计的方差会急剧膨胀权重系数对数据的微小扰动极其敏感。加了 L2 惩罚之后权重被压缩向零靠近但不会精确等于零。这相当于在偏差和方差之间做了一个权衡牺牲一点无偏性换取方差的显著降低。从贝叶斯视角看这等价于给权重加了高斯先验。class RidgeRegression: def __init__(self, alpha1.0): self.alpha alpha self.weights None self.bias None def fit(self, X, y): n_samples, n_features X.shape X_centered X - X.mean(axis0) y_centered y - y.mean() I np.eye(n_features) self.weights np.linalg.solve( X_centered.T.dot(X_centered) self.alpha * I, X_centered.T.dot(y_centered) ) self.bias y.mean() - X.mean(axis0).dot(self.weights) return self def predict(self, X): return X.dot(self.weights) self.bias这里用np.linalg.solve而不是直接求逆数值稳定性更好。另外注意截距项不参与正则化所以先把数据中心化再计算。3.2 alpha 的选择从岭迹图到交叉验证alpha 是岭回归最关键的参数。alpha 太大所有权重都被压到接近零模型欠拟合alpha 太小退化成普通线性回归共线性问题依然存在。我通常先用岭迹图做一个粗略判断画出不同 alpha 下各特征权重的变化曲线找到权重趋于稳定的那个区间。然后用 K 折交叉验证在候选区间内精细搜索。def ridge_cv(X, y, alphas, k5): n_samples len(y) fold_size n_samples // k best_alpha None best_mse float(inf) for alpha in alphas: mse_list [] for i in range(k): val_start i * fold_size val_end val_start fold_size X_val X[val_start:val_end] y_val y[val_start:val_end] X_train np.concatenate([X[:val_start], X[val_end:]]) y_train np.concatenate([y[:val_start], y[val_end:]]) model RidgeRegression(alphaalpha) model.fit(X_train, y_train) pred model.predict(X_val) mse_list.append(np.mean((pred - y_val) ** 2)) avg_mse np.mean(mse_list) if avg_mse best_mse: best_mse avg_mse best_alpha alpha return best_alpha, best_mse提示alpha 的搜索范围建议用对数尺度比如np.logspace(-3, 3, 50)这样能覆盖多个数量级。3.3 岭回归不是万能的什么时候该换 Lasso 或 ElasticNet岭回归会把所有特征的权重都保留下来只是大小不同。如果特征维度很高且存在大量无关特征岭回归的模型解释性会很差。这时候 Lasso 的 L1 正则化能把无关特征的权重精确压缩到零起到特征选择的作用。但 Lasso 也有问题当特征高度相关时它倾向于随机保留其中一个而把其他的归零结果不稳定。ElasticNet 结合了 L1 和 L2既能做特征选择又能保持群组效应。我的经验是如果特征数量不多且都有业务含义岭回归足够如果特征维度高且稀疏优先考虑 ElasticNet。4. 逻辑回归名字叫回归但干的是分类的活4.1 Sigmoid 函数如何把实数映射成概率逻辑回归虽然名字里有回归但它解决的是分类问题。核心思路是先算一个线性组合z wX b然后通过 Sigmoid 函数σ(z) 1 / (1 e^(-z))把 z 映射到 (0,1) 区间解释为属于正类的概率。这个映射的好处是z 趋近正无穷时概率趋近 1z 趋近负无穷时概率趋近 0z 等于 0 时概率为 0.5。而且 Sigmoid 的导数有简洁形式σ(z) σ(z)(1 - σ(z))这让梯度计算非常高效。class LogisticRegression: def __init__(self, learning_rate0.1, n_iters1000, tol1e-6): self.lr learning_rate self.n_iters n_iters self.tol tol self.weights None self.bias None def _sigmoid(self, z): z np.clip(z, -500, 500) return 1.0 / (1.0 np.exp(-z)) def fit(self, X, y): n_samples, n_features X.shape self.weights np.zeros(n_features) self.bias 0.0 for i in range(self.n_iters): linear X.dot(self.weights) self.bias y_pred self._sigmoid(linear) dw (1 / n_samples) * X.T.dot(y_pred - y) db (1 / n_samples) * np.sum(y_pred - y) self.weights - self.lr * dw self.bias - self.lr * db if np.linalg.norm(dw) self.tol: break return self def predict_proba(self, X): return self._sigmoid(X.dot(self.weights) self.bias) def predict(self, X, threshold0.5): return (self.predict_proba(X) threshold).astype(int)代码里对 z 做了截断处理防止np.exp溢出。这个细节在特征尺度较大时特别重要不加截断会看到RuntimeWarning: overflow encountered in exp。4.2 决策边界与阈值调整0.5 不是金科玉律默认情况下我们用 0.5 作为分类阈值但这只在正负样本均衡且误分类代价相同时才合理。实际场景中如果正类样本很少比如欺诈检测或者漏判正类的代价远高于误判就需要降低阈值来提高召回率。我通常用 ROC 曲线和 PR 曲线来辅助决策。ROC 曲线对类别不平衡不敏感PR 曲线在正类稀少时更能反映模型的实际表现。具体选哪个阈值要结合业务场景来定。比如某风控场景要求召回率不低于 0.9那就从 PR 曲线上找到满足这个条件的最低阈值。4.3 多分类的两种策略OvR 与 Softmax二分类逻辑回归很容易扩展到多分类。一种是 One-vs-Rest为每个类别训练一个二分类器预测时取概率最大的那个。另一种是 Softmax 回归直接输出每个类别的概率各类概率之和为 1。OvR 的优点是实现简单每个分类器可以独立训练缺点是当类别很多时每个分类器都要用全量数据训练计算量大而且各类别的预测概率没有归一化保证。Softmax 的优点是输出天然归一化类别之间相互竞争通常效果更好缺点是假设各类别决策边界是线性的对复杂边界拟合能力有限。我的经验是类别数少于 5 且各类别样本量均衡时两者差别不大类别数多或者样本不均衡时优先试 Softmax。5. k-means无监督聚类中最容易被误用的算法5.1 算法流程看似简单但初始化很关键k-means 的流程用一句话就能说清楚随机选 k 个初始中心点把每个样本分配给最近的中心点然后重新计算每个簇的中心点重复直到中心点不再变化或达到最大迭代次数。但随机选初始点这一步是最大的隐患。如果初始点选得不好可能收敛到局部最优或者某个簇一开始就没有分配到样本导致空簇。经典的解决方案是 k-means 初始化第一个中心点随机选后续每个中心点以与已有中心点距离平方成正比的概率选取。这样能显著提高收敛质量和稳定性。class KMeans: def __init__(self, n_clusters3, max_iters300, tol1e-4, n_init10): self.k n_clusters self.max_iters max_iters self.tol tol self.n_init n_init self.centroids None self.labels_ None self.inertia_ None def _init_centroids(self, X): n_samples, n_features X.shape centroids np.zeros((self.k, n_features)) idx np.random.randint(n_samples) centroids[0] X[idx] for i in range(1, self.k): dist_sq np.min( [np.sum((X - centroids[j]) ** 2, axis1) for j in range(i)], axis0 ) prob dist_sq / dist_sq.sum() cumulative np.cumsum(prob) r np.random.rand() idx np.searchsorted(cumulative, r) centroids[i] X[idx] return centroids def fit(self, X): best_inertia float(inf) best_centroids None best_labels None for _ in range(self.n_init): centroids self._init_centroids(X) for _ in range(self.max_iters): distances np.array([ np.sum((X - c) ** 2, axis1) for c in centroids ]) labels np.argmin(distances, axis0) new_centroids np.array([ X[labels i].mean(axis0) if np.any(labels i) else centroids[i] for i in range(self.k) ]) shift np.sum((new_centroids - centroids) ** 2) centroids new_centroids if shift self.tol: break inertia sum( np.sum((X[labels i] - centroids[i]) ** 2) for i in range(self.k) ) if inertia best_inertia: best_inertia inertia best_centroids centroids.copy() best_labels labels.copy() self.centroids best_centroids self.labels_ best_labels self.inertia_ best_inertia return selfn_init10表示用不同的初始化跑 10 次取 inertia 最小的那次结果。这个做法虽然增加了计算量但能有效避免陷入糟糕的局部最优。5.2 k 值怎么定肘部法、轮廓系数与业务约束k-means 需要预先指定簇的数量 k这是它最被诟病的地方。常用的确定方法有两种肘部法和轮廓系数。肘部法画的是不同 k 值对应的 inertia 曲线找曲线下降速度突然变缓的那个肘点。但肘点有时候并不明显特别是数据分布比较均匀时。轮廓系数则综合考虑了簇内紧密度和簇间分离度取值在 -1 到 1 之间越大越好。def silhouette_score_manual(X, labels): n_samples len(X) scores [] for i in range(n_samples): same_cluster X[labels labels[i]] if len(same_cluster) 1: scores.append(0) continue a np.mean([ np.linalg.norm(X[i] - x) for x in same_cluster if not np.array_equal(x, X[i]) ]) b float(inf) for label in np.unique(labels): if label labels[i]: continue other_cluster X[labels label] avg_dist np.mean([np.linalg.norm(X[i] - x) for x in other_cluster]) b min(b, avg_dist) scores.append((b - a) / max(a, b)) return np.mean(scores)注意轮廓系数计算复杂度是 O(n²)样本量大时只适合抽样计算。除了这些统计指标业务约束往往更重要。比如做用户分群运营团队可能只能维护 5 个群组那 k 就定在 5 左右然后在附近微调看哪个效果最好。5.3 特征标准化对 k-means 的影响被严重低估k-means 使用欧氏距离作为相似度度量这意味着特征的尺度直接决定了它在距离计算中的权重。如果一个特征的取值范围是 0 到 1000另一个是 0 到 1那么第一个特征几乎完全主导了距离计算第二个特征形同虚设。我见过一个案例用用户年龄和年收入做聚类年龄范围 18-80收入范围 5万-500万。不标准化的话聚类结果几乎完全由收入决定年龄的影响被淹没。标准化之后两个特征才真正平等地参与聚类。所以做 k-means 之前标准化是必须的。至于用 Z-score 还是 MinMax取决于数据分布和对异常值的敏感程度。Z-score 对异常值更敏感MinMax 会把所有值压缩到固定区间但受极值影响大。6. 四个算法放在一起看选型逻辑与常见误用场景6.1 监督 vs 无监督第一刀怎么切线性回归、岭回归、逻辑回归都是有监督学习需要标签k-means 是无监督学习不需要标签。这是最根本的分界线。但实际项目中有时候标签的获取成本很高或者根本没有标签。这时候可以考虑先用 k-means 做聚类然后对每个簇进行人工标注或业务解读再把簇标签作为新的特征输入到监督模型中。这种半监督的思路在实践中很常见。另一个常见的误用是明明有标签却用 k-means 做分类。比如有人想预测用户是否会购买手头有历史购买记录却用 k-means 把用户分成两堆然后说这一堆是会买的那一堆是不会买的。这种做法的问题在于k-means 找到的簇边界和真实的分类边界没有必然联系聚类结果可能完全无法对应到业务标签上。6.2 输出形态决定使用方式四个算法的输出形态差异很大这直接决定了它们的使用方式算法输出形态典型用途评估指标线性回归连续实数预测数值MSE, MAE, R²岭回归连续实数共线性数据预测MSE, MAE, R²逻辑回归概率值(0-1)二分类/多分类AUC, F1, LogLossk-means簇标签数据分群轮廓系数, Inertia线性回归和岭回归的输出是连续值适合做预测逻辑回归输出概率适合做分类和排序k-means 输出离散的簇编号适合做探索性分析和特征工程。我经常看到有人把逻辑回归的概率输出直接当连续值用比如预测用户价值。这样做不是不行但要清楚概率的排序意义大于数值意义。概率从 0.3 变成 0.6 不代表价值翻倍只代表相对排序更靠前。6.3 正则化的统一视角线性回归、岭回归、逻辑回归都可以加正则化。线性回归加 L2 就是岭回归加 L1 就是 Lasso。逻辑回归加 L2 或 L1 同样常见。k-means 虽然没有显式的正则化项但通过限制簇的数量和初始化策略实际上也在控制模型复杂度。理解正则化的统一视角有助于在实际中灵活运用。正则化系数 alpha 或 C逻辑回归中 C 是 alpha 的倒数控制的是模型复杂度与拟合能力之间的权衡。数据量大、信噪比高时可以减小正则化强度数据量小、噪声大时需要加强正则化。7. 从零实现到生产可用的几个关键补丁7.1 数值稳定性那些教科书不会告诉你的细节自己实现这些算法时数值稳定性是最容易被忽视的问题。除了前面提到的 Sigmoid 溢出和矩阵求逆还有几个地方需要注意。计算方差时直接用np.var在数值很大时可能产生精度损失用(X - X.mean()).var()更稳妥。计算欧氏距离时np.sqrt(np.sum((a-b)**2))在维度很高时可能下溢可以考虑用np.linalg.norm或者先做归一化。逻辑回归的损失函数-y*log(p) - (1-y)*log(1-p)在 p 接近 0 或 1 时会趋向无穷。实践中要在 log 里面加一个极小值epsilon1e-15来避免log(0)。7.2 早停策略防止过拟合和无效计算梯度下降类的算法都需要早停策略。最简单的做法是监控损失函数当连续若干轮损失下降幅度小于阈值时停止。更精细的做法是监控验证集上的表现当验证集损失开始上升时停止这是标准的 Early Stopping。def train_with_early_stopping(model, X_train, y_train, X_val, y_val, patience10): best_val_loss float(inf) best_params None wait 0 for epoch in range(model.n_iters): model.partial_fit(X_train, y_train) val_pred model.predict(X_val) val_loss np.mean((val_pred - y_val) ** 2) if val_loss best_val_loss: best_val_loss val_loss best_params (model.weights.copy(), model.bias) wait 0 else: wait 1 if wait patience: model.weights, model.bias best_params break return model这个模式对线性回归、岭回归、逻辑回归都适用。k-means 虽然没有验证集的概念但可以用 inertia 的变化来早停。7.3 结果可复现随机种子的正确设置方式k-means 和逻辑回归的随机初始化会导致每次运行结果不同。为了保证可复现需要固定随机种子。但要注意np.random.seed是全局的在多线程或多次调用时会相互影响。更好的做法是使用np.random.RandomState实例。rng np.random.RandomState(42) indices rng.choice(n_samples, sizek, replaceFalse)这样每个需要随机性的组件持有自己的 RandomState互不干扰。在团队协作中固定随机种子并记录在配置文件中能避免我这里是好的你那里怎么不一样这类扯皮。8. 一些踩过坑之后才明白的经验关于线性回归我最大的教训是不要迷信 R²。R² 高不代表模型正确可能只是过拟合了。一定要看残差分布一定要做交叉验证。另外特征工程对线性回归的提升往往比换模型更大花时间构造有意义的交互项和多项式特征比调参划算得多。关于岭回归alpha 的选择没有绝对标准。我见过同一个数据集不同随机划分下最优 alpha 相差一个数量级。所以不要追求最优 alpha找到一个稳定区间内的值就够了。另外岭回归的系数不能直接解释为特征重要性因为正则化会压缩系数特征之间的相对重要性会发生变化。关于逻辑回归类别不平衡是最大的坑。正类只占 1% 时模型可能把所有样本都预测为负类准确率还有 99%但毫无用处。这时候要么调整阈值要么用类别权重要么换评估指标。我通常先看 PR 曲线再决定怎么处理。关于 k-means最大的坑是不标准化就聚类。其次是 k 值的选择不能只看统计指标要结合业务可解释性。还有就是 k-means 假设簇是球形的且大小相近如果真实簇是长条形或大小差异很大k-means 效果会很差这时候要考虑 DBSCAN 或高斯混合模型。最后分享一个实用技巧这四个算法虽然简单但它们是理解更复杂模型的基石。逻辑回归可以看作单层神经网络k-means 可以看作高斯混合模型的硬分配版本岭回归是理解正则化和贝叶斯先验的入口。把它们的原理和实现吃透再去学深度学习和概率图模型会顺畅很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑