从零实现机器学习经典算法:线性回归、K-Means与决策树

从零实现机器学习经典算法:线性回归、K-Means与决策树

在实际机器学习项目中,很多开发者会遇到一个典型困境:虽然能调用sklearnfitpredict完成模型训练,但面对线性回归的损失函数、决策树的信息增益、K-Means的迭代过程等核心原理时,却感到模糊不清。这种“会用但不懂”的状态,在模型调优、排查异常结果或进行算法选型时,会带来巨大障碍。真正的精通,意味着你能从数学直觉和代码实现两个层面,理解算法如何工作、为何有效、以及何时会失效。

本文将以 Python 为实践语言,带你从零开始,不依赖高级封装,亲手实现线性回归、K-Means聚类、决策树这几个最具代表性的经典算法。我们将聚焦于算法最核心的机制,通过代码揭示其内部运作,并讨论每个算法在实际应用中的关键参数、常见陷阱和性能边界。无论你是希望夯实基础的机器学习初学者,还是想深入理解模型黑盒的工程实践者,这篇内容都将提供一条从“入门”到“精通”的清晰路径。

1. 理解机器学习算法的核心:从“调用”到“实现”

在直接动手写代码之前,我们需要建立一个正确的认知框架:机器学习算法不是魔法黑盒,而是一系列基于数学优化和统计学习的可执行步骤。从“调用API”到“亲手实现”,是理解这一点的关键跨越。

1.1 算法学习的三个层次

通常,我们对一个算法的掌握可以分为三个层次:

  1. 应用层:知道在sklearn中导入哪个类,如何调用fitpredict,并调整几个常见参数(如max_depth对于决策树)。这是项目快速上手的必备技能。
  2. 原理层:理解算法背后的目标函数(如线性回归的最小化均方误差)、优化方法(如梯度下降)以及核心概念(如信息增益、轮廓系数)。这决定了你能否进行有效的模型选择和调参。
  3. 实现层:能够用基础编程语言(如 Python + NumPy)将算法的数学步骤转化为实际运行的代码。这是检验你是否真正“吃透”一个算法的终极标准,它能让你深刻理解算法的计算复杂度、对数据的假设以及可能失败的边界条件。

本文的目标是带领你达到第三层。我们将暂时抛开sklearn的高级封装,从最原始的数学公式和编程逻辑出发。

1.2 环境与工具准备:最小化依赖

为了聚焦于算法本身,我们只需要最基础的科学计算环境。请确保你的 Python 环境已安装以下库:

# 使用 pip 进行安装 pip install numpy matplotlib scikit-learn

各库的作用如下:

  • NumPy:提供高效的数组操作和线性代数计算,是我们实现算法的数学基础。
  • Matplotlib:用于数据可视化和结果展示,帮助直观理解算法行为。
  • scikit-learn:我们暂时不会用它来调用算法,但会用它来生成模拟数据、分割数据集,并在最后与我们手写的算法结果进行对比验证,这是一个极好的 sanity check。

你可以通过以下代码片段快速验证环境:

import numpy as np import matplotlib.pyplot as plt from sklearn import datasets print(f"NumPy version: {np.__version__}") # 生成一个简单的数据集用于后续测试 X, y = datasets.make_regression(n_samples=100, n_features=1, noise=10, random_state=42) plt.scatter(X, y) plt.title("Sample Data for Verification") plt.show()

如果上述代码能成功运行并显示散点图,说明你的环境已就绪。

2. 线性回归:从最小二乘法到梯度下降

线性回归是理解机器学习优化思想的绝佳起点。它的目标是找到一条直线(或超平面),使得所有数据点到该直线的垂直距离(误差)的平方和最小。

2.1 数学模型与损失函数

对于简单线性回归y = w * x + b,其中w是权重(斜率),b是偏置(截距)。给定一组数据(x_i, y_i),我们定义损失函数为均方误差(MSE):Loss(w, b) = (1/n) * Σ(y_i - (w*x_i + b))^2我们的任务就是找到使Loss(w, b)最小的wb

方法一:解析解(最小二乘法)对于线性回归,损失函数是凸函数,可以直接通过求导数为零的点得到全局最优解。其矩阵形式的解为:θ = (X^T * X)^(-1) * X^T * y其中X是增加了全为1的列(对应偏置b)的特征矩阵,θ是包含[w, b]的参数向量。

import numpy as np class LinearRegressionOLS: """使用最小二乘法(解析解)实现线性回归""" def __init__(self): self.weights = None # 存储训练得到的参数θ def fit(self, X, y): # 为X添加一列全1,用于计算偏置项b X_b = np.c_[np.ones((X.shape[0], 1)), X] # 应用解析解公式,使用np.linalg.pinv求伪逆以增强数值稳定性 self.weights = np.linalg.pinv(X_b.T.dot(X_b)).dot(X_b.T).dot(y) return self def predict(self, X): X_b = np.c_[np.ones((X.shape[0], 1)), X] return X_b.dot(self.weights)

关键解释

  • np.c_用于按列连接数组,这里为特征矩阵添加了一列偏置项。
  • np.linalg.pinv计算矩阵的 Moore-Penrose 伪逆。即使X^T * X不可逆(如特征共线时),pinv也能给出一个合理的解,比inv更稳健。
  • 这种方法在小数据集上非常快速准确,但当特征维度很高(>10^4)或样本量极大时,计算逆矩阵会非常昂贵甚至不可行。

方法二:数值解(梯度下降)梯度下降是一种迭代优化算法,通过不断沿损失函数梯度反方向更新参数,逐步逼近最小值。参数更新规则为:θ = θ - learning_rate * ∇Loss(θ)对于 MSE 损失,梯度∇Loss(θ)有解析形式。

class LinearRegressionGD: """使用批量梯度下降实现线性回归""" def __init__(self, learning_rate=0.01, n_iters=1000): self.lr = learning_rate self.n_iters = n_iters self.weights = None self.loss_history = [] # 记录每次迭代的损失值,用于监控 def fit(self, X, y): n_samples, n_features = X.shape # 初始化参数,权重w随机,偏置b初始为0 self.weights = np.random.randn(n_features + 1) X_b = np.c_[np.ones((n_samples, 1)), X] y = y.reshape(-1, 1) # 梯度下降迭代 for i in range(self.n_iters): # 计算预测值 y_pred = X_b.dot(self.weights).reshape(-1, 1) # 计算误差 error = y_pred - y # 计算梯度 (1/n) * X_b^T * error gradients = (2 / n_samples) * X_b.T.dot(error).flatten() # 更新参数 self.weights -= self.lr * gradients # 记录当前损失 loss = np.mean(error ** 2) self.loss_history.append(loss) return self def predict(self, X): X_b = np.c_[np.ones((X.shape[0], 1)), X] return X_b.dot(self.weights)

2.2 实现验证与关键参数讨论

现在,我们用模拟数据来验证我们手写的两个线性回归实现。

# 生成数据 from sklearn.model_selection import train_test_split X, y = datasets.make_regression(n_samples=200, n_features=1, noise=15, random_state=42) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 使用解析解模型 model_ols = LinearRegressionOLS() model_ols.fit(X_train, y_train) y_pred_ols = model_ols.predict(X_test) mse_ols = np.mean((y_test - y_pred_ols) ** 2) print(f"OLS Model - Weights: {model_ols.weights}, Test MSE: {mse_ols:.2f}") # 使用梯度下降模型 model_gd = LinearRegressionGD(learning_rate=0.1, n_iters=500) model_gd.fit(X_train, y_train) y_pred_gd = model_gd.predict(X_test) mse_gd = np.mean((y_test - y_pred_gd) ** 2) print(f"GD Model - Weights: {model_gd.weights}, Test MSE: {mse_gd:.2f}") # 绘制损失下降曲线 plt.plot(range(len(model_gd.loss_history)), model_gd.loss_history) plt.xlabel('Iteration') plt.ylabel('Loss (MSE)') plt.title('Gradient Descent Loss Convergence') plt.show()

关键参数与常见坑

  1. 学习率 (learning_rate):这是梯度下降最重要的超参数。
    • 值太大:损失函数会震荡甚至发散(损失值爆炸式增长)。
    • 值太小:收敛速度极慢,可能需要非常多的迭代次数。
    • 调试建议:通常从 0.01、0.1、1 等数量级开始尝试,并务必绘制损失曲线观察。如果曲线震荡,调小学习率;如果曲线下降太慢,可适当调大。
  2. 迭代次数 (n_iters):需要足够多次迭代以确保收敛。可以通过设置一个很小的阈值(如损失变化小于1e-6)作为早停条件,而不是固定迭代次数。
  3. 特征缩放:上述代码未进行特征缩放。如果特征量纲差异巨大(如x1范围是 0-1,x2范围是 10000-100000),梯度下降的收敛路径会变得非常曲折,甚至难以收敛。最佳实践是,在使用梯度下降前,对特征进行标准化(零均值、单位方差)处理
  4. 初始化:我们使用了随机初始化。对于线性回归,由于损失函数是凸的,初始化点不影响最终结果,但会影响到达最优点的迭代步数。

3. K-Means 聚类:无监督学习中的迭代优化

聚类是一种典型的无监督学习,目标是将数据点分组,使得同一组(簇)内的点彼此相似,不同组间的点不相似。K-Means 以其简单高效成为最常用的聚类算法之一。

3.1 算法流程与核心思想

K-Means 的目标是最小化每个点到其所属簇中心的距离平方和。算法通过交替执行以下两个步骤直至收敛:

  1. 分配步骤:将每个数据点分配到距离其最近的簇中心。
  2. 更新步骤:重新计算每个簇中所有点的均值,作为新的簇中心。

其数学目标是最小化:J = Σ Σ ||x - μ_k||^2,其中内层求和针对属于簇k的所有点x

class KMeansManual: """手动实现 K-Means 聚类算法""" def __init__(self, n_clusters=3, max_iters=300, tol=1e-4): self.n_clusters = n_clusters self.max_iters = max_iters self.tol = tol # 容忍度,中心点移动小于此值则认为收敛 self.centroids = None self.labels = None self.inertia_ = None # 保存最终的簇内误差平方和 def _init_centroids(self, X): """随机初始化簇中心:从数据点中随机选择K个""" n_samples = X.shape[0] random_indices = np.random.choice(n_samples, self.n_clusters, replace=False) centroids = X[random_indices] return centroids def _compute_distance(self, X, centroids): """计算每个点到每个簇中心的距离 (欧氏距离平方)""" n_samples = X.shape[0] n_clusters = centroids.shape[0] distances = np.zeros((n_samples, n_clusters)) for k in range(n_clusters): # 利用广播机制计算差值的平方和 distances[:, k] = np.sum((X - centroids[k]) ** 2, axis=1) return distances def fit(self, X): n_samples, n_features = X.shape # 1. 初始化簇中心 self.centroids = self._init_centroids(X) for i in range(self.max_iters): # 2. 分配步骤:计算距离并分配标签 distances = self._compute_distance(X, self.centroids) self.labels = np.argmin(distances, axis=1) # 3. 更新步骤:计算新的簇中心 new_centroids = np.zeros((self.n_clusters, n_features)) for k in range(self.n_clusters): # 找出属于当前簇k的所有点 cluster_points = X[self.labels == k] if len(cluster_points) > 0: new_centroids[k] = cluster_points.mean(axis=0) else: # 如果某个簇没有点,则重新随机初始化该中心 new_centroids[k] = X[np.random.randint(0, n_samples)] # 4. 检查收敛:中心点移动是否小于容忍度 centroid_shift = np.sqrt(np.sum((new_centroids - self.centroids) ** 2, axis=1)).sum() if centroid_shift < self.tol: print(f"Converged at iteration {i}") break self.centroids = new_centroids # 计算最终的簇内误差平方和 distances = self._compute_distance(X, self.centroids) self.inertia_ = np.sum(distances[np.arange(n_samples), self.labels]) return self def predict(self, X): """对新数据点预测所属簇""" distances = self._compute_distance(X, self.centroids) return np.argmin(distances, axis=1)

3.2 算法验证与 K 值选择困境

我们使用经典的鸢尾花(Iris)数据集进行演示,但只使用其中两个特征以便可视化。

# 加载并准备数据 from sklearn.datasets import load_iris iris = load_iris() X_iris = iris.data[:, :2] # 只取前两个特征 (萼片长度和宽度) y_iris = iris.target # 真实标签,仅用于对比,算法本身不知道 # 使用手写 K-Means kmeans = KMeansManual(n_clusters=3, max_iters=100) kmeans.fit(X_iris) labels = kmeans.labels centroids = kmeans.centroids # 可视化聚类结果 plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.scatter(X_iris[:, 0], X_iris[:, 1], c=y_iris, cmap='viridis', edgecolor='k', s=50) plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=200, label='Centroids (Manual)') plt.title('Ground Truth (Iris Species)') plt.xlabel('Sepal Length') plt.ylabel('Sepal Width') plt.subplot(1, 2, 2) plt.scatter(X_iris[:, 0], X_iris[:, 1], c=labels, cmap='viridis', edgecolor='k', s=50) plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=200, label='Centroids (Manual)') plt.title('K-Means Clustering Result (Manual)') plt.xlabel('Sepal Length') plt.ylabel('Sepal Width') plt.legend() plt.show() print(f"簇内误差平方和 (Inertia): {kmeans.inertia_:.2f}")

K-Means 的核心挑战与调参

  1. K 值的选择:这是 K-Means 最大的挑战。我们如何知道数据中天然存在几个簇?
    • 肘部法则:绘制不同 K 值对应的簇内误差平方和(Inertia)。随着 K 增大,Inertia 会下降。选择 Inertia 下降速度突然变缓的点(像肘部弯曲),作为合理的 K 值。
    inertias = [] K_range = range(1, 10) for k in K_range: km = KMeansManual(n_clusters=k) km.fit(X_iris) inertias.append(km.inertia_) plt.plot(K_range, inertias, 'bo-') plt.xlabel('Number of clusters (K)') plt.ylabel('Inertia') plt.title('Elbow Method for Optimal K') plt.show()
    • 轮廓系数:更高级的方法,同时考虑簇内的凝聚度和簇间的分离度,值越接近 1 越好。
  2. 初始化的敏感性:随机初始化可能导致不同的局部最优解。生产环境中通常采用K-Means++初始化策略,它使初始中心点彼此远离,能有效提升收敛速度和结果稳定性。
  3. 对异常值和簇形状的假设:K-Means 使用欧氏距离,因此天然假设簇是凸形的、各向同性的(在各个方向方差相近),且对异常值敏感。对于非球形簇或方差差异大的簇,效果可能不佳(此时可考虑 DBSCAN 或高斯混合模型 GMM)。
  4. 数据标准化:与梯度下降一样,如果特征量纲不同,距离计算会被量级大的特征主导。在聚类前必须进行特征标准化

4. 决策树:基于信息论的规则构建

决策树通过一系列 if-else 规则对数据进行划分,目标是使划分后子集的“纯度”越来越高。理解决策树的关键在于理解其用于选择划分特征的准则。

4.1 核心概念:信息增益与基尼不纯度

决策树学习的关键是:在每一个节点,选择哪个特征进行分割,以及分割点在哪里。常用的准则有两个:

  • 信息增益:基于信息熵。熵表示随机变量的不确定性,熵越大,不确定性越高。信息增益 = 父节点的熵 - 子节点的加权平均熵。我们选择信息增益最大的特征进行分割。
  • 基尼不纯度:衡量一个随机选中的样本在子集中被分错的可能性。基尼不纯度越小,集合纯度越高。CART 树默认使用基尼系数。

我们以实现分类树(CART)为例,使用基尼不纯度作为划分标准。

class Node: """决策树节点类""" def __init__(self, feature_index=None, threshold=None, left=None, right=None, value=None): self.feature_index = feature_index # 用于分割的特征索引 self.threshold = threshold # 分割阈值 self.left = left # 左子树 (<= threshold) self.right = right # 右子树 (> threshold) self.value = value # 如果是叶节点,存储预测的类别 class DecisionTreeClassifierManual: """手动实现决策树分类器 (CART)""" def __init__(self, max_depth=5, min_samples_split=2): self.max_depth = max_depth self.min_samples_split = min_samples_split self.root = None def _gini(self, y): """计算基尼不纯度""" m = y.size if m == 0: return 0 # 计算每个类别的比例 p = np.bincount(y) / m # 基尼不纯度 = 1 - Σ(p_i^2) return 1 - np.sum(p ** 2) def _best_split(self, X, y): """寻找最佳分割特征和阈值""" m, n = X.shape if m <= 1: # 样本数不足以分割 return None, None # 计算父节点的基尼不纯度 parent_gini = self._gini(y) best_gini = float('inf') best_feature, best_threshold = None, None # 遍历所有特征 for feature_idx in range(n): # 获取该特征的所有唯一值作为候选阈值 thresholds = np.unique(X[:, feature_idx]) for threshold in thresholds: # 根据阈值划分左右子集 left_mask = X[:, feature_idx] <= threshold right_mask = ~left_mask if np.sum(left_mask) == 0 or np.sum(right_mask) == 0: continue # 分割无效 # 计算加权平均基尼不纯度 gini_left = self._gini(y[left_mask]) gini_right = self._gini(y[right_mask]) n_left, n_right = np.sum(left_mask), np.sum(right_mask) weighted_gini = (n_left / m) * gini_left + (n_right / m) * gini_right # 如果找到了更优的分割 if weighted_gini < best_gini: best_gini = weighted_gini best_feature = feature_idx best_threshold = threshold # 计算信息增益(基尼减少量) info_gain = parent_gini - best_gini # 如果信息增益非常小,则不分割 if info_gain < 1e-7: return None, None return best_feature, best_threshold def _build_tree(self, X, y, depth=0): """递归构建决策树""" num_samples, num_features = X.shape num_classes = len(np.unique(y)) # 终止条件 if (depth >= self.max_depth or num_samples < self.min_samples_split or num_classes == 1): # 创建叶节点,值为最常见的类别 most_common_class = np.argmax(np.bincount(y)) return Node(value=most_common_class) # 寻找最佳分割 feature_idx, threshold = self._best_split(X, y) if feature_idx is None: # 无法找到有效分割 most_common_class = np.argmax(np.bincount(y)) return Node(value=most_common_class) # 根据最佳分割划分数据 left_mask = X[:, feature_idx] <= threshold right_mask = ~left_mask # 递归构建左右子树 left_subtree = self._build_tree(X[left_mask], y[left_mask], depth + 1) right_subtree = self._build_tree(X[right_mask], y[right_mask], depth + 1) # 返回当前节点 return Node(feature_index=feature_idx, threshold=threshold, left=left_subtree, right=right_subtree) def fit(self, X, y): self.root = self._build_tree(X, y) return self def _predict_single(self, x, node): """对单个样本进行预测""" if node.value is not None: # 到达叶节点 return node.value # 根据特征值和阈值决定走向左子树还是右子树 if x[node.feature_index] <= node.threshold: return self._predict_single(x, node.left) else: return self._predict_single(x, node.right) def predict(self, X): return np.array([self._predict_single(x, self.root) for x in X])

4.2 决策树实战:过拟合与剪枝

我们使用一个简单的二维非线性分类数据集来演示决策树的工作方式及其核心问题。

# 生成月亮形数据集 from sklearn.datasets import make_moons X_moons, y_moons = make_moons(n_samples=200, noise=0.2, random_state=42) # 训练一个深度较大的树(容易过拟合) tree_deep = DecisionTreeClassifierManual(max_depth=10) tree_deep.fit(X_moons, y_moons) # 训练一个深度较小的树(可能欠拟合) tree_shallow = DecisionTreeClassifierManual(max_depth=3) tree_shallow.fit(X_moons, y_moons) # 可视化决策边界 def plot_decision_boundary(clf, X, y, title): x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5 y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5 xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02)) Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) plt.contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm') plt.scatter(X[:, 0], X[:, 1], c=y, edgecolor='k', cmap='coolwarm') plt.title(title) plt.xlabel('Feature 1') plt.ylabel('Feature 2') plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plot_decision_boundary(tree_shallow, X_moons, y_moons, 'Decision Tree (Max Depth=3)') plt.subplot(1, 2, 2) plot_decision_boundary(tree_deep, X_moons, y_moons, 'Decision Tree (Max Depth=10) - Potential Overfitting') plt.show()

决策树的关键问题与调优

  1. 过拟合:这是决策树最显著的问题。如果不加限制,树会一直生长直到每个叶节点只包含一个样本,在训练集上达到 100% 准确率,但会学习到噪声和异常点,泛化能力极差。控制过拟合的主要手段是剪枝
    • 预剪枝:在构建树的过程中提前停止。通过max_depth(最大深度)、min_samples_split(节点最小分裂样本数)、min_samples_leaf(叶节点最小样本数)等参数控制。
    • 后剪枝:先构建一棵完整的树,然后自底向上,考察非叶节点。如果将其替换为叶节点能提升验证集性能,则进行剪枝。我们手写的简易版本只实现了预剪枝。
  2. 不稳定性:数据的微小变化可能导致生成完全不同的树。这是因为决策树在顶层选择特征时,信息增益或基尼系数的差异可能很小,随机性会影响选择。这也是集成方法(如随机森林)被广泛使用的原因——通过构建多棵树并投票来降低方差。
  3. 特征重要性:决策树可以天然地评估特征重要性。一个特征被用于分割的次数越多,或者其带来的不纯度下降越多,它通常就越重要。这为特征选择提供了依据。
  4. 处理连续值和缺失值:我们的简易实现假设特征是连续的,并通过遍历所有唯一值寻找阈值。工业级实现会使用更高效的二分查找。对于缺失值,常见策略是使用替代分割或将该样本分配到所有子节点并赋予权重。

5. 从手动实现到生产实践:常见问题排查与最佳实践

手动实现算法让我们洞悉了其内核,但在实际生产项目中,我们几乎总是使用sklearn这样的成熟库。理解底层原理能帮助我们在使用高级 API 时做出正确的决策和高效的排查。

5.1 模型不工作?通用排查清单

当你训练好的模型表现不佳时,可以按以下顺序排查:

问题现象可能原因检查方式处理建议
线性回归损失不下降或爆炸1. 学习率过大或过小
2. 特征未标准化
3. 迭代次数不足
绘制损失曲线;打印前几次迭代的权重变化;检查特征最大值/最小值。调整学习率;对特征进行StandardScaler标准化;增加迭代次数或添加早停。
K-Means 结果每次运行都不一样1. 随机初始化导致局部最优
2. K 值选择不当
固定随机种子 (random_state);运行多次取平均或最佳结果;绘制肘部曲线。使用KMeans++初始化(sklearn默认);尝试不同的 K 值;考虑使用轮廓系数。
决策树在训练集完美但测试集很差过拟合查看树的最大深度和叶节点样本数;绘制学习曲线(训练/测试得分 vs 树深度)。增加min_samples_splitmin_samples_leaf;减小max_depth;使用交叉验证调参。
所有模型性能都像随机猜测1. 特征与标签无关
2. 数据泄露或预处理错误
3. 评估指标用错
计算特征与标签的相关系数;检查训练/测试集划分是否正确;确认标签编码无误。进行特征工程;检查数据流水线;使用正确的评估指标(如分类用准确率/F1,回归用MSE/R2)。

5.2 算法选型速查指南

不同的算法有其固有的优势和假设。选择不当是项目失败的主要原因之一。

算法核心优势主要假设/局限典型应用场景
线性回归简单、可解释性强、计算快、可得到参数置信区间。假设线性关系、对异常值敏感、特征需独立。房价预测、销售额预估、任何假设输入输出呈线性关系的场景。
逻辑回归输出概率、可解释性强、不易过拟合。仍是线性模型(决策边界线性)、需要大量样本。二分类问题(如垃圾邮件识别、广告点击预测)。
决策树非参数、可处理数值和类别特征、无需特征缩放、可视化强。极易过拟合、不稳定、对不平衡数据敏感。需要解释规则(如信贷风控)、数据包含复杂 if-else 逻辑。
随机森林高准确率、抗过拟合、可评估特征重要性、并行化容易。计算和存储开销大、可解释性比单棵树差。大多数分类和回归任务的首选基准模型,尤其是表格数据。
K-Means简单、高效、可扩展性强。需指定K、对异常值和非球形簇敏感、依赖初始化。客户分群、图像压缩、异常检测(将远离簇的点视为异常)。
神经网络拟合能力极强、可处理图像/文本等非结构化数据。需要大量数据和算力、超参数多、黑盒模型。计算机视觉、自然语言处理、复杂模式识别。

5.3 生产环境最佳实践

  1. 永远从基线模型开始:不要一开始就使用最复杂的模型。先用线性回归或逻辑回归建立一个性能基线。这能帮你快速验证数据流水线,并了解问题的难度。
  2. 数据预处理是重中之重:包括处理缺失值、异常值、类别特征编码、特征缩放(对基于距离的模型如 K-Means、SVM 和基于梯度的模型至关重要)、以及特征工程。大部分性能提升来源于更好的数据,而非更复杂的模型。
  3. 系统化评估与验证
    • 务必使用训练集/验证集/测试集的分割。
    • 使用交叉验证来更稳健地评估模型性能和调参。
    • 选择与业务目标一致的评估指标(例如,在正负例极不平衡时,准确率是无效的,应关注精确率、召回率或 F1 分数)。
  4. 理解模型的局限性:知道你的模型在什么情况下会失败。例如,线性回归无法捕捉非线性关系;K-Means 无法发现流形形状的簇;深度很浅的决策树可能欠拟合。
  5. 可解释性与监控:尤其是在金融、医疗等领域,模型的可解释性可能比微小的精度提升更重要。同时,模型上线后需要持续监控其性能,因为数据分布可能会随时间漂移。

通过从零实现这些经典算法,你获得的不仅是对其数学本质的深刻理解,更是一种“算法思维”——能够将复杂的数学优化问题分解为可迭代、可编程的步骤。这种能力是应对未来更复杂模型(如梯度提升树、神经网络)的坚实基础。下一步,你可以尝试实现随机森林(通过组合多棵决策树)、支持向量机(SVM)的核技巧,甚至是一个简单的多层感知机(MLP),从而将这种“从原理到实现”的学习方法应用到更广阔的机器学习领域。