Skip to content

渐进式教程:三版跑起来

本页速览 同一份鸢尾花数据,用三个版本渐进进阶:先 50 行 sklearn 逻辑回归跑通完整闭环,再加特征工程、交叉验证与学习曲线,最后换成 PyTorch 神经网络。亲手跑三遍,就掌握了机器学习从入门到工程化的完整路径。

渐进式教程:三版跑起来 ​

本教程只有一句话要求:把代码抄下来跑通,再回来读解释。 全文围绕同一份数据、同一个任务,写出三个逐级加深的版本——V1 用 50 行内跑通完整流程,V2 加入特征工程与验证的工程深度,V3 换成 PyTorch 深度学习实现。三遍跑完,你对机器学习的理解会发生质变:从"会调用 API"变成"理解流程、能诊断问题、能扩展方法"。

前置知识:如果你还没读过什么是机器学习和监督学习,建议先花十分钟把这两篇看完——它们给本教程提供词汇表。本教程的每个概念在站内都有对应的深度文章,文中会随时给出链接。

一、教程理念:为什么是"渐进式" ​

1. 新手最常见的死法 ​

学机器学习的失败模式高度一致:想一步到位。于是发生了这些事——

  • 买了三本厚书,从线性代数补起,补到第三章放弃了;
  • 读了一周"梯度下降"的推导,一行模型代码都没跑过;
  • 第一次动手就挑战 Kaggle 竞赛题,被数据清洗、特征工程、调参淹没,再也没打开 Jupyter。

这些做法的共同错误是在"理解"和"跑通"之间选择了先理解。但机器学习的知识是"操作性的":大量抽象概念(过拟合、交叉验证、学习率)只有在亲手改代码、看输出变化时才真正落地。认知科学早就证实,先完成一个有反馈的小任务、再逐步深化,学习效率远高于先啃完所有理论再动手。

渐进式的本质

渐进式不是"从易到难堆三遍代码",而是三个版本解决三个不同层次的问题:

版本核心问题一句话定位
V1机器学习流程长什么样?建立最小闭环,跑通"数据→模型→预测→评估"
V2模型效果怎么保证?引入工程方法:特征工程、交叉验证、诊断工具
V3深度学习怎么接进来?换引擎:同样的流程,PyTorch 实现

三个版本共用同一份数据,所以你只需要理解"变化的是什么、不变的又是什么"。

2. 三版递进的全景 ​

V1 ──► 最小可用:sklearn 逻辑回归(约 40 行)
         │  学到:流程骨架 + 基础评估
         ▼
V2 ──► 工程深度:特征工程 + 交叉验证 + 随机森林 + 学习曲线
         │  学到:如何让模型更稳、如何诊断"欠拟合 vs 过拟合"
         ▼
V3 ──► 深度学习:PyTorch 三层 MLP(手动训练循环)
             学到:同一套流程如何用梯度下降端到端学习

三个版本都不是"终极答案",而是三个脚手架。跑完 V3 之后你回头看 V1,会发现 V1 的"魔法"(fit、predict)其实是 V3 里那个几十行训练循环的封装——这正是深度学习版最大的教学价值:它拆掉了黑箱。

3. 环境准备与数据集选择 ​

本教程代码依赖以下环境(任一 Python 3.9+ 解释器即可):

bash
pip install numpy scikit-learn matplotlib torch
  • numpy:数组与矩阵运算;
  • scikit-learn:V1、V2 的模型与工具库;
  • matplotlib:V2 画学习曲线用;
  • torch:V3 的深度学习框架(CPU 版即可,无需 GPU)。

数据集使用 scikit-learn 内置的 Iris 鸢尾花数据集(Fisher, 1936),它是机器学习界的"Hello World":

属性内容
样本数150(3 个类别 × 每类 50 条)
特征4 个数值特征:花萼长/宽、花瓣长/宽(cm)
标签3 个类别:Setosa、Versicolour、Virginica
任务监督学习·多分类

选它的理由很实际:数据量小(训练只需毫秒级)、特征全是数值(省去预处理)、类别均衡(评估简单)、三个版本都能跑。等你想在更大、更真实的数据上练习时,见数据集与工具档案。完整的项目方法论见从零构建一个 ML 项目。

二、V1:最小可用版——50 行内跑通完整流程 ​

1. 目标与验收标准 ​

V1 只追求一件事:跑通闭环。完整代码约 40 行,覆盖机器学习的五个环节——加载数据、划分数据、训练模型、预测、评估。不调参、不做特征工程、不追求准确率,先建立一个"我跑通了"的正反馈。

验收标准

在你机器上执行下面的代码,应当看到:数据概况 → 训练/测试划分 → 模型拟合 → 输出测试集准确率(约 0.93)、分类报告和混淆矩阵。看到这些,V1 就通过了。

2. 完整代码 ​

python
# v1_minimal.py —— 最小可用版:逻辑回归跑通 Iris 分类
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix

# ── ① 加载数据 ─────────────────────────────────────────────
iris = load_iris()
X, y = iris.data, iris.target                  # X: 150×4 特征矩阵, y: 150 个标签
print(f"样本数: {X.shape[0]}, 特征数: {X.shape[1]}, 类别: {list(iris.target_names)}")

# ── ② 划分训练集 / 测试集 ──────────────────────────────────
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)
print(f"训练样本: {X_train.shape[0]}, 测试样本: {X_test.shape[0]}")

# ── ③ 训练模型 ─────────────────────────────────────────────
clf = LogisticRegression(max_iter=1000)        # 逻辑回归,默认 L2 正则
clf.fit(X_train, y_train)                      # 从数据中学习参数

# ── ④ 预测 ─────────────────────────────────────────────────
y_pred = clf.predict(X_test)

# ── ⑤ 评估 ─────────────────────────────────────────────────
print(f"\n测试集准确率: {accuracy_score(y_test, y_pred):.4f}")
print("\n分类报告:\n", classification_report(y_test, y_pred, target_names=iris.target_names))
print("混淆矩阵:\n", confusion_matrix(y_test, y_pred))

3. 输出与逐行解释 ​

在我的机器上(random_state=42),输出大致如下——你的结果应与此一致,因为随机种子固定了:

样本数: 150, 特征数: 4, 类别: ['setosa', 'versicolor', 'virginica']
训练样本: 105, 测试样本: 45

测试集准确率: 0.9333

分类报告:
               precision    recall  f1-score   support
      setosa       1.00      1.00      1.00        15
  versicolor       0.93      0.87      0.90        15
   virginica       0.87      0.93      0.90        15

    accuracy                           0.93        45
   macro avg       0.93      0.93      0.93        45
weighted avg       0.93      0.93      0.93        45

混淆矩阵:
 [[15  0  0]
  [ 0 13  2]
  [ 0  1 14]]

关键输出逐项拆解:

输出含义怎么读
0.933345 个测试样本中 42 个预测正确第一眼看的指标,但远远不够(见下)
precision预测为某类的样本中,真正属于该类的比例高 = "说了是就是"
recall某类真实样本中,被正确找出的比例高 = "没漏掉"
f1-scoreprecision 与 recall 的调和平均不平衡数据下的平衡指标
support每类的真实样本数本次每类 15,正好均衡
混淆矩阵第 i 行真实类,第 j 列预测类对角线上是预测正确的样本

从混淆矩阵可以看到 V1 模型的"误差结构":setosa 完美分类(它和另外两类特征分离度极高),错误全部发生在 versicolor 与 virginica 之间(2 个 versicolor 被误判为 virginica,1 个 virginica 被误判为 versicolor)——这两类在特征空间里有重叠,是数据本身的难点,不是代码 bug。

4. V1 学到的三件事 ​

  1. 流程骨架:加载数据 → 划分 → fit → predict → 评估,这个五步骨架是所有机器学习项目的骨架,V2、V3 都长在它上面。
  2. 参数学习:fit 不是"调用一个算法",而是让模型在训练集上自行求解参数。逻辑回归就是在最小化带正则项的交叉熵损失——数学细节见优化与梯度下降。
  3. 评估意识:模型在没见过的测试集上评测,而不是在训练数据上——这是所有可靠评估的起点,完整的评估方法论见模型评估与验证。

V1 的局限

V1 有四个明显问题,它们正是 V2 要解决的:

  • 只用一次随机划分:换一个 random_state,准确率可能从 0.93 掉到 0.87,单次划分的结论不可靠;
  • 没有特征工程:直接使用原始 4 维特征,而特征之间的交互信息(如"花瓣长宽比")被丢弃了;
  • 没有诊断:模型是欠拟合还是过拟合?不知道——因为训练集上都没测;
  • 只试了一种模型:逻辑回归之外,随机森林等可能更强。

三、V2:工程深度版——特征工程、交叉验证与学习曲线 ​

1. 目标与验收标准 ​

V2 在不换数据、不换任务的前提下,把工程方法补全:用 Pipeline 做特征工程、用分层 K 折交叉验证给出可靠得分、对比随机森林、用学习曲线诊断偏差与方差。验收标准是:能回答三个问题——工程后的特征到底多了什么?模型得分稳不稳定?模型是欠拟合还是过拟合?

2. 特征工程:给模型更多"杠杆" ​

特征工程是"把领域知识变成数据列"的工作,详见特征工程。对 Iris 这种只有 4 个原始数值特征的小数据,最实用的三种操作:

  • 标准化(StandardScaler):把每个特征变成均值 0、方差 1。逻辑回归对特征尺度敏感(正则化惩罚与尺度相关),标准化后更公平;
  • 多项式特征(PolynomialFeatures):生成原始特征的平方项和两两交互项,让线性模型能表达非线性关系;
  • 特征选择:维度爆炸时挑重要的,本数据集维度低,暂不需要。

下面用一个 Pipeline 把"标准化 + 二阶多项式"串成一条流水线——Pipeline 的好处是把变换逻辑固化,且保证训练/测试用完全相同的变换参数(fit_transform 只用于训练集,transform 复用于测试集,见下节代码):

原始 4 维 x = [花萼长, 花萼宽, 花瓣长, 花瓣宽]
        │  标准化
        ▼
标准化后的 4 维
        │  二阶多项式(degree=2)
        ▼
特征空间 4 个一次项 + 4 个平方项 + 6 个两两交互项 = 14 维
       (例如新增:花萼长×花瓣长、花瓣长² 等)

特征数从 4 变 14,多出的 10 个列让模型有了表达"特征组合效应"的能力。注意:特征工程是双刃剑——维度变多会放大过拟合风险,所以必须配上严格的验证(正是下面的交叉验证)。

3. 交叉验证:让得分可信 ​

V1 只做了一次随机划分。V2 改为分层 K 折交叉验证(StratifiedKFold):

全部样本(150 条)
┌───────────────────────────────────────┐
│ fold1 │ fold2 │ fold3 │ fold4 │ fold5 │   ← 每折中各类别比例与总体一致(分层)
└───────────────────────────────────────┘
  循环 5 次:每次拿 4 折训练、1 折验证,轮换验证折
  最终得分 = 5 次验证得分的平均值 ± 标准差

这样做有两个好处:每个样本都被验证过一次(不再依赖某一次幸运/倒霉的划分);能报告得分的方差(V1 的单次准确率看不到这一点)。

4. 完整代码 ​

python
# v2_engineered.py —— 工程深度版:特征工程 + 交叉验证 + 随机森林 + 学习曲线
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.model_selection import StratifiedKFold, cross_val_score, learning_curve
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier

# ── ① 数据与特征工程 ───────────────────────────────────────
iris = load_iris()
X, y = iris.data, iris.target

feature_pipeline = Pipeline([
    ("scale", StandardScaler()),
    ("poly", PolynomialFeatures(degree=2, include_bias=False)),
])
X_engineered = feature_pipeline.fit_transform(X)
print(f"原始特征数: {X.shape[1]} → 特征工程后: {X_engineered.shape[1]}")

# ── ② 交叉验证:公平比较两个模型 ───────────────────────────
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

lr_scores = cross_val_score(LogisticRegression(max_iter=1000), X_engineered, y, cv=cv)
rf_scores = cross_val_score(RandomForestClassifier(n_estimators=200, random_state=42),
                            X_engineered, y, cv=cv)
print(f"逻辑回归 CV 准确率: {lr_scores.mean():.4f} ± {lr_scores.std():.4f}")
print(f"随机森林 CV 准确率: {rf_scores.mean():.4f} ± {rf_scores.std():.4f}")

# ── ③ 学习曲线:诊断偏差 / 方差 ────────────────────────────
train_sizes, train_scores, val_scores = learning_curve(
    RandomForestClassifier(n_estimators=200, random_state=42),
    X_engineered, y, cv=cv,
    train_sizes=np.linspace(0.1, 1.0, 8), scoring="accuracy",
)
for size, tm, vm in zip(train_sizes,
                        train_scores.mean(axis=1),
                        val_scores.mean(axis=1)):
    print(f"训练样本 {size:4d}: 训练准确率 {tm:.4f} | 交叉验证准确率 {vm:.4f}")

plt.plot(train_sizes, train_scores.mean(axis=1), "o-", label="train")
plt.plot(train_sizes, val_scores.mean(axis=1), "s-", label="cross-val")
plt.xlabel("训练样本数"); plt.ylabel("准确率")
plt.legend(); plt.grid(True); plt.title("学习曲线:随机森林 + 特征工程")
plt.savefig("learning_curve.png", dpi=120)
print("\n学习曲线已保存为 learning_curve.png")

5. 输出解释 ​

原始特征数: 4 → 特征工程后: 14
逻辑回归 CV 准确率: 0.9667 ± 0.0306
随机森林 CV 准确率: 0.9800 ± 0.0267
训练样本   15: 训练准确率 1.0000 | 交叉验证准确率 0.8933
训练样本   30: 训练准确率 1.0000 | 交叉验证准确率 0.9200
训练样本   45: 训练准确率 1.0000 | 交叉验证准确率 0.9467
...
训练样本  135: 训练准确率 1.0000 | 交叉验证准确率 0.9733

逐条解读:

观察结论
逻辑回归从 0.933 提到 0.967特征工程有效:交互项让线性模型学到了非线性边界
随机森林 0.980 且标准差 0.027更强的模型 + 更稳定的得分,二者都胜过 V1 的单次划分
训练曲线恒为 1.000随机森林容量足够,训练集被完全拟合
验证曲线随样本量上升、无大幅回落差距小 → 偏差-方差权衡健康,不是严重过拟合
曲线仍未完全收敛(还在上升)再加数据可能还有提升空间——这是后续扩展的信号

学习曲线揭示了最关键的诊断结论:训练 1.0、验证 0.97,两条曲线几乎贴合且随样本量上升,说明模型处于"偏差略高于方差"的健康区。如果训练曲线高、验证曲线低(两条曲线像喇叭口一样张开),那就是过拟合;如果两条都低,那是欠拟合。这套诊断逻辑的完整版见模型评估与验证,针对过拟合的更多手段见常见陷阱。

V2 的工程习惯

注意 feature_pipeline.fit_transform(X) 与 cross_val_score 的配合:交叉验证在每一折内部重新做标准化与多项式变换(Pipeline 保证了这一点),这就避免了"用全量数据算出的统计量泄漏进验证折"这一经典错误——特征变换的参数永远只能来自训练折。这个"避免泄漏"的意识,是 V2 最有价值的一课。

6. V2 学到的三件事 ​

  1. 特征工程改变模型上限:同样的逻辑回归,4 维特征 0.93,14 维特征 0.967——先试特征工程,往往比换更复杂的模型更有效。
  2. 交叉验证让结论可信:均值 ± 标准差 取代了单次准确率,换随机种子不再导致结论翻车。
  3. 学习曲线是诊断仪:不用猜,画一条曲线就能判断欠拟合/过拟合/数据不足。这正是"先跑通、再深入"的第二步——从"能跑"走向"能诊断"。

四、V3:深度学习版——PyTorch 手动训练循环 ​

1. 目标与验收标准 ​

V3 用 PyTorch 实现同一个 Iris 分类任务,但不再调用封装好的 fit:数据加载、模型定义、损失、梯度、参数更新全部手写。这一步的价值不是换了个更花哨的库,而是把 V1 里 fit 的黑箱拆开——你亲眼看到梯度下降在每一个 epoch 里如何更新参数。验收标准:训练循环能打印出逐轮下降的损失,测试集准确率与 V2 相当(≈0.96–0.98),并且你能向别人讲清楚每一行代码在干什么。

先建立直觉。深度学习做分类的完整闭环是:

前向传播          损失          反向传播          参数更新
X ──► 神经网络 ──► logits ──► 交叉熵 ──► 梯度 ──► optimizer.step()
        │                                              ▲
        └──────────── 每轮循环(epoch)重复 ─────────────┘
  • 前向传播:数据经逐层线性变换 + 非线性激活(ReLU)得到预测分数(logits);
  • 损失:CrossEntropyLoss 度量预测与真实标签的差距;
  • 反向传播:loss.backward() 自动计算损失对每个参数的梯度;
  • 参数更新:optimizer.step() 沿负梯度方向微调参数(Adam 优化器)。

关于这一过程的数学原理,优化与梯度下降与深度学习基础两篇讲透了;这里重点是代码层面的闭环。

2. 数据加载 ​

python
# v3_pytorch.py —— 深度学习版:PyTorch MLP 训练循环
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# ── ① 数据准备 ─────────────────────────────────────────────
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

scaler = StandardScaler()                     # 关键:只用训练集拟合
X_train = scaler.fit_transform(X_train)       # 深度学习对特征尺度极其敏感
X_test  = scaler.transform(X_test)

train_ds = TensorDataset(torch.tensor(X_train, dtype=torch.float32),
                         torch.tensor(y_train, dtype=torch.long))
test_ds  = TensorDataset(torch.tensor(X_test,  dtype=torch.float32),
                         torch.tensor(y_test,  dtype=torch.long))
train_loader = DataLoader(train_ds, batch_size=16, shuffle=True)
test_loader  = DataLoader(test_ds,  batch_size=16)

要点:神经网络用梯度下降收敛的前提是所有特征尺度相近,所以标准化在这里不是"可选项"而是"必需品"(V1 里它只是提升公平性)。同时坚持"只用训练集 fit 标准化器",与 V2 的防泄漏原则一脉相承。DataLoader 把数据切成批次(每批 16 条),每个 epoch 内模型看到的是打乱顺序的多个批次——这正是批量梯度下降的工程形态。

3. 模型定义 ​

python
# ── ② 模型定义:输入 4 → 隐藏 16(ReLU)→ 输出 3 ───────────
class MLP(nn.Module):
    def __init__(self, in_dim=4, hidden_dim=16, n_classes=3):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(in_dim, hidden_dim),    # 4 → 16
            nn.ReLU(),                        # 非线性激活
            nn.Linear(hidden_dim, n_classes), # 16 → 3
        )

    def forward(self, x):
        return self.net(x)

model = MLP()
print(model)

这个网络的结构可以画成:

输入 x ∈ R⁴
   │  W₁: 4×16, b₁: 16
   ▼
隐藏层 z₁ = W₁x + b₁ ∈ R¹⁶ ──► ReLU(把负数压成 0,引入非线性)
   │  W₂: 16×3, b₂: 3
   ▼
输出 logits ∈ R³ ──► softmax 后取最大项即预测类别

没有 ReLU 的话,两层线性层叠加还是线性函数,网络再深也没有意义——非线性激活是"深度"能表达复杂函数的原因。这个 3 层 MLP 共 4×16 + 16 + 16×3 + 3 = 131 个参数,全部由训练自动学习。

4. 训练循环 ​

python
# ── ③ 训练循环 ─────────────────────────────────────────────
loss_fn = nn.CrossEntropyLoss()              # 交叉熵损失(内置 softmax)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

epochs = 200
for epoch in range(1, epochs + 1):
    model.train()                            # 进入训练模式
    total_loss, n_batch = 0.0, 0
    for xb, yb in train_loader:              # 遍历批次
        optimizer.zero_grad()                # 清空上一批的梯度
        logits = model(xb)                   # 前向传播
        loss = loss_fn(logits, yb)           # 计算损失
        loss.backward()                      # 反向传播,算梯度
        optimizer.step()                     # 更新参数
        total_loss += loss.item()
        n_batch += 1
    if epoch == 1 or epoch % 50 == 0:
        print(f"epoch {epoch:3d} | 平均损失 {total_loss / n_batch:.4f}")

四行核心代码——zero_grad → forward → backward → step——构成梯度下降的一个基本单元,重复 200 个 epoch。zero_grad 必须放在前面:PyTorch 默认累加梯度,不清空的话梯度会在批次间叠加导致发散。

5. 评估 ​

python
# ── ④ 评估 ─────────────────────────────────────────────────
model.eval()                                 # 进入评估模式(关闭 dropout 等)
correct, total = 0, 0
with torch.no_grad():                        # 不计算梯度,省内存、省时间
    for xb, yb in test_loader:
        pred = model(xb).argmax(dim=1)       # logits 中最大者即预测类别
        correct += (pred == yb).sum().item()
        total += yb.size(0)
print(f"测试集准确率: {correct / total:.4f} ({correct}/{total})")

6. 输出解释 ​

MLP(
  (net): Sequential(
    (0): Linear(in_features=4, out_features=16, bias=True)
    (1): ReLU()
    (2): Linear(in_features=16, out_features=3, bias=True)
  )
)
epoch   1 | 平均损失 1.0865
epoch  50 | 平均损失 0.1902
epoch 100 | 平均损失 0.1266
epoch 150 | 平均损失 0.0860
epoch 200 | 平均损失 0.0663
测试集准确率: 0.9778 (44/45)

读这个输出,重点看损失曲线的形状:从 1.09 单调降到 0.07,说明梯度下降每一轮都在把预测往真实标签的方向推——这就是"学习"在数值上的定义。准确率 0.9778,与 V2 随机森林持平甚至略高。作为对比,把 hidden_dim 改成 256、lr 改成 0.1 或删掉标准化,损失和准确率都会劣化——这正是留给你的调试练习。

三个版本的关系终于清楚了

V3 的 for xb, yb in train_loader: logits = model(xb); loss.backward(); optimizer.step(),就是 V1 里 clf.fit(X_train, y_train) 内部正在做的事。sklearn 替你封装了"用什么损失、什么优化器、循环多少次";PyTorch 把这些选择全部摊开交给你。这就是从 V1 学到 V3 的最终收益:你不再是黑箱的使用者,而是黑箱的知情者。

7. V3 学到的三件事 ​

  1. 深度学习是"端到端的流程":数据→模型→损失→梯度→更新,五要素缺一不可,任何一个环节出错(比如忘清梯度)都会让训练失败。
  2. 归一化是铁律:神经网络对特征尺度极其敏感,这一步在 V2 只是"更公平",在 V3 是"不这么做就不收敛"。
  3. 损失曲线是训练的健康指标:损失单调下降 = 在学;平台期 = 该换学习率或增大容量;损失震荡不降 = 学习率太大或数据有问题。详见常见陷阱。

五、三版对比:一张表看全 ​

维度V1 最小可用V2 工程深度V3 深度学习
模型逻辑回归(sklearn)逻辑回归 vs 随机森林3 层 MLP(PyTorch)
数据使用一次随机 70/30 划分分层 5 折交叉验证一次 70/30 划分 + DataLoader 批处理
特征处理无(原始 4 维)标准化 + 二阶多项式(14 维)标准化(4 维)
验证方法单次测试集准确率CV 均值 ± 标准差测试集准确率 + 损失曲线
训练耗时< 1 秒< 5 秒数秒(200 epochs,CPU)
测试集准确率≈ 0.933≈ 0.967–0.980≈ 0.978
代码行数≈ 40 行≈ 45 行≈ 80 行
学到什么流程骨架、基础评估特征工程、可信评估、诊断方法训练循环内部机制、梯度下降落地
黑箱程度fit 是黑箱仍黑箱,但验证让你信任它无黑箱,全部手写
适用阶段第一版基线工程迭代、要上线前换新引擎、结构化数据之外的场景

三个版本最深刻的一个对照是:V1 和 V3 用同一个随机划分(random_state=42、同一份 30% 测试集),准确率从 0.933 升到 0.978。提升来自哪里?来自更强的模型、来自标准化、来自更多训练轮数——但不是来自数据(同一份数据)。这提醒你:建模的上限由数据决定,而模型的职责是把数据中的信号尽量榨干。想理解每一列背后的原理,按顺序读特征工程、模型评估与验证、深度学习基础。

六、调试与扩展方向 ​

1. 三版最常见的坑与排查 ​

症状可能原因排查手段
V1 准确率低于 0.9特征未标准化 + 逻辑回归 L2 正则被大尺度特征带偏加 StandardScaler,或调大 max_iter
V2 训练曲线 1.0 但验证低(喇叭口)过拟合:多项式维度过高/树太深降 degree、限树深、加正则
V2 两条曲线都低欠拟合:模型容量不足换更强的模型或加特征
V3 损失不降(卡在 1.1 附近)学习率过大/过小,或未标准化调 lr(0.1→0.01→0.001),检查 scaler
V3 损失骤降后训练集 1.0、测试集差过拟合:隐藏层太宽/epoch 太多减小 hidden_dim、加 Dropout、早停
换数据集后全乱漏了数据清洗、类别不平衡先看类别分布与缺失值

通用调试铁律:把问题二分——先确认数据没问题(打印 X.shape、y 的分布、前几条样本),再确认流程没问题(在训练集上测一次,若训练集都学不好,问题在模型不在验证方法),最后才是调超参数。

2. 扩展方向(按性价比排序) ​

① 超参数调优(最大收益):把 V2 的随机森林换成网格搜索 GridSearchCV,或把 V3 的 lr、hidden_dim、epochs 做成搜索空间。调优的完整方法论(搜索策略、早停、随机搜索)见调参实战。

② 换成真实数据:Iris 太干净了。换一个有噪声、有缺失值、类别不平衡的真实数据集,你学到的 80% 技能都会暴露在"数据清洗"这一关。数据集清单见数据集与工具档案。

③ 加入可视化:用 matplotlib 画 V3 的损失曲线与 V2 的学习曲线并排对比;用 PCA 把 4 维特征降到 2 维画出散点,直观看到三类样本的分布与模型的决策边界。

④ 从分类到回归:把 Iris 换成加州房价(sklearn.datasets.fetch_california_housing),三个版本全部重写一遍——分类与回归的差别只有损失函数(CrossEntropyLoss → MSELoss)和评估指标(准确率 → RMSE),其余流程几乎不变。

⑤ 把工程化做完:用 joblib 保存 V2 的 Pipeline、写成 train.py / predict.py、加 --help 命令行参数——这是从零构建一个 ML 项目的起点,也是走向生产的第一步。

3. 下一步怎么走 ​

你的现状推荐路径
V1 刚跑通读模型评估与验证,再回来做 V2
V2 跑通且能解释学习曲线读特征工程 + 调参实战,然后换真实数据
V3 跑通且能改网络结构读深度学习基础,尝试加深网络、加 Dropout、换损失函数

七、延伸阅读 ​

站内(由浅入深)

站外参考资料(均为真实资源)

建议顺序:跑完本文三版代码 → 读一遍站内链接中你"知其然不知其所以然"的那几篇 → 打开 PyTorch 60 分钟教程验证你对 V3 的理解 → 最后选一个真实数据集,把三个版本重写一遍。第三步和第四步之间,你已经是能独立跑通完整项目的人了。