Week 05:sklearn Baselines、数据划分与指标
本周进入 Phase 2:从“会清洗数据”转向“能建立一个可信的机器学习 baseline”。这里的重点不是追求最高分,而是建立一套以后每个项目都会复用的最小训练流程:固定数据划分、训练多个经典模型、记录指标、写下第一轮误差分析。
本文默认你使用 CachyOS、fish shell、VS Code、uv、Python、Git。命令都按 fish 写;如果不是 fish,需要自己转换激活命令。
0. 本周详细教学:语法、规范、验收
本节不是追加在尾部的复习,而是本周正文的入口。先读这里,再做后面的命令和项目。
0.1 本周真正要学会什么
| 维度 | 要求 |
|---|---|
| 知识点 | train/test、Dummy baseline、LogisticRegression、指标 |
| 代码语法 | 能从空文件写出本周核心脚本,而不是只复制运行 |
| 程序规范 | 函数拆分、路径清楚、输入输出明确、错误能解释 |
| 交付物 | src/train_baselines.py |
| 验收方式 | 从 fish 终端运行命令,得到可复查的文件或指标 |
0.2 代码语法精讲
下面的代码不是最终答案,而是本周必须理解的最小骨架:
from sklearn.dummy import DummyClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report
models = { “dummy”: DummyClassifier(strategy=“most_frequent”), “logreg”: LogisticRegression(max_iter=1000), } for name, model in models.items(): model.fit(X_train, y_train) pred = model.predict(X_valid) print(name) print(classification_report(y_valid, pred))
读代码时按四步检查:输入从哪里来;中间变量的类型和 shape 是什么;函数或脚本输出什么;哪些错误应该显式报出来。
0.3 本周程序规范
- 所有路径用相对路径或 `pathlib.Path`,不要写死 `/home/miku/...`。
- 核心逻辑进 `src/`,notebook 只做探索和解释。
- 每个脚本能从 fish 终端运行,并在 README 写出命令。
- 输出必须落盘到 `reports/`、`figures/` 或 `outputs/`,不能只在屏幕上看。
0.4 本周练习分层
| 层级 | 任务 | 不合格表现 | 合格验收 |
|---|---|---|---|
| 最小练习 | 手写上面的最小骨架 | 只在 notebook 里运行 | 终端运行成功 |
| 标准练习 | 把逻辑拆成函数/模块 | 一个大脚本从头写到尾 | 至少 2 个函数,职责清楚 |
| 项目练习 | 生成本周交付物 src/train_baselines.py | 只有屏幕输出 | 文件落盘,可复查 |
| 复盘练习 | 写 3 个错误和修复 | 只写“已解决” | 写清报错、原因、修复、预防 |
0.5 本周和主线的连接
- 回到总计划:USTC AI / Quant 练习手册
- 查详细练习索引:技术练习详解
- 查质量评分:最终质量门槛
1. 本周目标
完成一个可以重复运行的 src/train_baselines.py:
- 读取一个表格数据集。
- 区分特征
X与目标y。 - 做
train / validation / test三份划分。 - 训练至少 3 个 baseline 模型。
- 对分类或回归任务输出合适指标。
- 生成
reports/week05_baseline_results.md。 - 写下初步误差分析,而不是只贴一个分数。
你最终应理解:
baseline 不是“低级模型”,而是以后所有改进的比较基准。
没有 baseline,就不知道复杂模型到底有没有带来价值。
2. 前置条件
你应该已经完成 Week 01-04:
- 会在终端进入项目目录。
- 会用
uv创建虚拟环境。 - 会用 VS Code 打开项目。
- 会用 pandas 读取 CSV。
- 会做基础 EDA:缺失值、分布、相关性、简单图表。
- 会用 Git 保存阶段性成果。
检查工具:
python --version
uv --version
git --version
code --version
如果 python 指向系统 Python,不要急着安装包;先进入项目目录并激活 .venv。
3. 建议项目位置
如果你前几周已经有学习仓库,可以继续使用:
cd ~/Code/python-learning
为 Week 05 单独建一个小项目:
mkdir -p week05-sklearn-baselines
cd week05-sklearn-baselines
pwd
解释:
| 命令 | 作用 |
|---|---|
mkdir -p |
创建目录;目录已存在也不报错 |
cd |
进入项目目录 |
pwd |
确认当前路径,避免在错误目录里初始化项目 |
建议输出类似:
/home/miku/Code/python-learning/week05-sklearn-baselines
4. 初始化 uv 项目
uv init
uv venv
source .venv/bin/activate.fish
逐句解释:
| 命令 | 含义 |
|---|---|
uv init |
生成 pyproject.toml、README.md 等项目文件 |
uv venv |
在当前目录创建 .venv/ 虚拟环境 |
source .venv/bin/activate.fish |
fish shell 专用激活命令 |
激活后检查 Python 路径:
command -v python
python --version
你希望看到路径里包含当前项目:
/home/miku/Code/python-learning/week05-sklearn-baselines/.venv/bin/python
如果看到 /usr/bin/python,说明环境没有激活成功。
5. 安装依赖
Week 05 需要 pandas、numpy、scikit-learn,以及一个简单制表工具。
uv add pandas numpy scikit-learn tabulate
检查:
python -c "import pandas, sklearn; print('pandas', pandas.__version__); print('sklearn', sklearn.__version__)"
这里用 python -c 运行一行小检查,不是写项目代码。真正的训练逻辑要放进 src/train_baselines.py。
6. 建立文件布局
创建目录:
mkdir -p data/raw data/processed src reports
建议布局:
week05-sklearn-baselines/
├── .venv/ # 当前项目虚拟环境,不提交到 Git
├── data/
│ ├── raw/ # 原始数据,只读保存
│ └── processed/ # 处理后的数据,可重复生成
├── reports/
│ └── week05_baseline_results.md
├── src/
│ └── train_baselines.py
├── pyproject.toml
├── uv.lock
└── README.md
注意:data/raw/ 里的原始数据不要反复手工修改。你可以复制一份到 data/processed/ 做清洗输出,但原始文件最好保留。
7. 选择一个练习数据集
为了让脚本可直接运行,本周可以先用 sklearn 自带的 breast cancer 分类数据集。它适合练习:
- 二分类。
- 数值特征。
- 样本量不大,运行很快。
- 不需要联网下载。
- 可直接比较 Logistic Regression、KNN、Decision Tree、Random Forest。
真实项目中,你可以换成 Kaggle、UCI 或课程数据。baseline 结构保持不变。
8. 理解 train / validation / test
不要只划分 train/test。Week 05 应开始养成三份数据意识:
train 用来拟合模型参数
validation 用来比较模型、调参数、做选择
test 最后只使用一次,用来估计最终泛化表现
常见错误是:
在 test 上反复试模型,哪个分数高就选哪个。
这会让 test 变成新的 validation,最终分数偏乐观。正确流程是:
1. train 上训练多个模型
2. validation 上比较模型
3. 选定一个模型
4. 只在最后对 test 评估一次
对于分类任务,划分时使用 stratify=y,保证每份数据里的类别比例接近整体比例。
9. 创建 baseline 脚本
在 VS Code 中打开项目:
code .
创建文件:
src/train_baselines.py
把下面代码放进去:
from pathlib import Pathimport pandas as pd from sklearn.datasets import load_breast_cancer from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score, roc_auc_score from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.tree import DecisionTreeClassifier
RANDOM_STATE = 42 REPORT_PATH = Path(“reports/week05_baseline_results.md”)
def load_data(): dataset = load_breast_cancer(as_frame=True) X = dataset.data y = dataset.target target_names = list(dataset.target_names) return X, y, target_names
def split_data(X, y): X_train_valid, X_test, y_train_valid, y_test = train_test_split( X, y, test_size=0.2, random_state=RANDOM_STATE, stratify=y, ) X_train, X_valid, y_train, y_valid = train_test_split( X_train_valid, y_train_valid, test_size=0.25, random_state=RANDOM_STATE, stratify=y_train_valid, ) return X_train, X_valid, X_test, y_train, y_valid, y_test
def build_models(): return { “logistic_regression”: Pipeline( steps=[ (“scaler”, StandardScaler()), (“model”, LogisticRegression(max_iter=2000, random_state=RANDOM_STATE)), ] ), “knn”: Pipeline( steps=[ (“scaler”, StandardScaler()), (“model”, KNeighborsClassifier(n_neighbors=5)), ] ), “decision_tree”: DecisionTreeClassifier(max_depth=4, random_state=RANDOM_STATE), “random_forest”: RandomForestClassifier(n_estimators=200, random_state=RANDOM_STATE), }
def evaluate_classifier(model, X, y): predictions = model.predict(X) probabilities = model.predict_proba(X)[:, 1] return { “accuracy”: accuracy_score(y, predictions), “precision”: precision_score(y, predictions), “recall”: recall_score(y, predictions), “f1”: f1_score(y, predictions), “roc_auc”: roc_auc_score(y, probabilities), }
def format_metrics(metrics): return {name: round(value, 4) for name, value in metrics.items()}
def make_report(results, split_sizes, target_names, best_model_name, test_metrics): rows = [] for result in results: rows.append( ”| {model} | {accuracy:.4f} | {precision:.4f} | {recall:.4f} | {f1:.4f} | {roc_auc:.4f} |“.format( **result ) )
plain report = f"""# Week 05 Baseline Results
Dataset
- Source: sklearn breast cancer dataset
- Task: binary classification
- Target names:
Split
Split Rows Train {split_sizes[‘train’]} Validation {split_sizes[‘valid’]} Test {split_sizes[‘test’]} The validation set is used for model comparison. The test set is used once after selecting the best validation model.
Validation Metrics
Model Accuracy Precision Recall F1 ROC AUC {chr(10).join(rows)} Selected Model
- Best validation model by F1:
Final Test Metrics
Metric Value Accuracy {test_metrics[‘accuracy’]:.4f} Precision {test_metrics[‘precision’]:.4f} Recall {test_metrics[‘recall’]:.4f} F1 {test_metrics[‘f1’]:.4f} ROC AUC {test_metrics[‘roc_auc’]:.4f} Initial Error Analysis
Write 5-8 bullet points after reading the confusion matrix or wrong predictions. Suggested questions:
- Which class is easier to miss?
- Is high accuracy hiding weak recall?
- Does the model make more false positives or false negatives?
- Which metric should matter more for this problem?
- Is the current split stable enough, or do we need cross validation in Week 06? """ return report
def main(): X, y, target_names = load_data() X_train, X_valid, X_test, y_train, y_valid, y_test = split_data(X, y) models = build_models()
plain results = [] fitted_models = {} for name, model in models.items(): model.fit(X_train, y_train) fitted_models[name] = model metrics = evaluate_classifier(model, X_valid, y_valid) results.append({“model”: name, **metrics})
plain results = sorted(results, key=lambda item: item[“f1”], reverse=True) best_model_name = results[0][“model”] best_model = fitted_models[best_model_name] test_metrics = evaluate_classifier(best_model, X_test, y_test)
plain split_sizes = { “train”: len(X_train), “valid”: len(X_valid), “test”: len(X_test), } report = make_report(results, split_sizes, target_names, best_model_name, test_metrics)
REPORT_PATH.parent.mkdir(parents=True, exist_ok=True) REPORT_PATH.write_text(report, encoding="utf-8") print("Validation results:") print(pd.DataFrame(results).round(4).to_string(index=False)) print() print("Best model:", best_model_name) print("Test metrics:", format_metrics(test_metrics)) print("Report written to", REPORT_PATH)
if name == “main”: main()
10. 运行 baseline
确认环境已激活:
source .venv/bin/activate.fish
command -v python
运行脚本:
python src/train_baselines.py
你应该看到类似输出:
Validation results: model accuracy precision recall f1 roc_auc random_forest 0.9649 0.9714 0.9714 0.9714 0.9921 logistic_regression 0.9561 0.9706 0.9429 0.9565 0.9954 knn 0.9474 0.9701 0.9286 0.9489 0.9868 decision_tree 0.9298 0.9444 0.9444 0.9444 0.9355
Best model: random_forest Test metrics: {‘accuracy’: 0.9561, ‘precision’: 0.9459, ‘recall’: 0.9859, ‘f1’: 0.9655, ‘roc_auc’: 0.9898} Report written to reports/week05_baseline_results.md
具体数值可能因 sklearn 版本略有差异。重要的是:脚本能跑通,报告能生成,模型比较逻辑清楚。
11. 指标怎么读
11.1 Accuracy
accuracy = 预测正确的样本数 / 总样本数
优点:直观。缺点:类别不平衡时可能误导。例如 95% 都是负类,模型全预测负类也有 95% accuracy。
11.2 Precision
precision = 预测为正类且真的为正类 / 所有预测为正类
问题意识:模型说“是”的时候,有多大概率真的“是”?
11.3 Recall
recall = 预测为正类且真的为正类 / 所有真实正类
问题意识:真实正类里,模型抓住了多少?如果漏诊代价高,recall 很重要。
11.4 F1
F1 = precision 和 recall 的调和平均
当你同时关心 precision 与 recall,可用 F1 做第一选择指标。
11.5 ROC AUC
ROC AUC 衡量模型把正类排在负类前面的能力。它不只看一个阈值,而是看不同阈值下的整体区分能力。
12. 为什么 Logistic Regression 和 KNN 要标准化
StandardScaler() 会把每列特征变成大致均值 0、标准差 1。
需要标准化的模型:
- Logistic Regression:优化过程受特征尺度影响。
- KNN:距离计算会被大尺度特征支配。
- SVM:同样强烈依赖尺度。
不一定需要标准化的模型:
- Decision Tree。
- Random Forest。
- Gradient Boosting Tree。
所以脚本里只给 Logistic Regression 和 KNN 放进 Pipeline。
13. 怎样替换成自己的 CSV
假设你的数据是:
data/raw/my_dataset.csv
并且目标列叫 target。可以把 load_data() 改成:
def load_data():
data = pd.read_csv("data/raw/my_dataset.csv")
X = data.drop(columns=["target"])
y = data["target"]
target_names = sorted(y.astype(str).unique().tolist())
return X, y, target_names
如果有类别特征,暂时不要手工把字符串硬改成数字。Week 06 会学习 ColumnTransformer 和防泄漏 pipeline。Week 05 可以先选择全数值数据练手。
14. 报告怎么写
打开报告:
code reports/week05_baseline_results.md
在 Initial Error Analysis 下补充你的观察。不要只写:
Random forest is best.
应写成类似:
- Random forest 在 validation F1 上最高,但 Logistic Regression 的 ROC AUC 也很高,说明线性边界已经有不错区分能力。
- 如果任务重视漏检,recall 比 accuracy 更重要。当前最佳模型 test recall 高于 precision,错误更偏向 false positives。
- Validation 集只有一份,结果可能受随机划分影响。Week 06 需要用交叉验证确认排序是否稳定。
15. Git 保存成果
先看状态:
git status --short
如果这是独立学习项目,还没有 Git 仓库:
git init
创建 .gitignore 可以用 VS Code,新建文件 .gitignore,写入:
.venv/
__pycache__/
.ipynb_checkpoints/
data/raw/*.zip
提交:
git add pyproject.toml uv.lock README.md src reports .gitignore
git commit -m "Add week05 sklearn baseline training"
如果数据文件来自公开小数据,也可以提交;如果数据很大或有隐私,不要直接提交。
16. 本周练习
- 把
RANDOM_STATE改成 7、21、100,观察 validation 排名是否稳定。 - 把
DecisionTreeClassifier(max_depth=4)改成max_depth=2和max_depth=None,比较过拟合迹象。 - 把 KNN 的
n_neighbors改成 3、7、11,观察 F1 变化。 - 在报告里加一节
Metric Choice,说明为什么本任务不能只看 accuracy。 - 换一个 sklearn 自带数据集或你自己的 CSV,复用同一训练流程。
17. 验收检查
在项目根目录执行:
source .venv/bin/activate.fish
python src/train_baselines.py
test -f reports/week05_baseline_results.md; and echo "report exists"
然后人工检查:
src/train_baselines.py存在。reports/week05_baseline_results.md存在。- 报告里有模型名称、指标、划分方式、结果表格、初步误差分析。
- 代码里有固定的
RANDOM_STATE。 - test set 没有参与模型选择,只在最后评估一次。
- Git 至少有一次提交。
如果这些都满足,Week 05 合格。
18. 常见错误
错误 1:fish 激活命令写错
现象:
Unsupported use of '='
原因:在 fish 里用了 Bash / Zsh 的激活脚本。
修复:
source .venv/bin/activate.fish
错误 2:ModuleNotFoundError: No module named 'sklearn'
原因通常是没有激活当前项目虚拟环境,或依赖安装在别的环境。
检查:
command -v python
uv pip list
修复:
source .venv/bin/activate.fish
uv add scikit-learn
错误 3:忘记 stratify=y
分类任务如果类别不平衡,某一份 split 可能类别比例异常,导致分数不稳定。
修复:
train_test_split(X, y, stratify=y, random_state=RANDOM_STATE)
错误 4:在 test 上选模型
不要比较所有模型的 test 分数再选最高。这样 test 已经被你用来调参了。
正确做法:validation 选模型,test 做最终确认。
错误 5:把 baseline 当成最终结论
Week 05 的 baseline 只是起点。它告诉你:如果后续特征工程、交叉验证、模型选择没有超过 baseline,就说明复杂度没有带来收益。
19. 下一步
进入 Week 06:交叉验证与模型选择。你会把本周的一次固定划分,升级为 K-fold cross validation、GridSearchCV 和防止数据泄漏的 Pipeline。
plain
気に入ったならばコメントを残してくださいね~