Week 11:AI 比赛代码整理——从混乱 notebook 到可复现项目
回到总路线:USTC 统计 AI / 量化 20 周成长计划
本周目标对应计划中的 Week 11:把 AI 比赛或课程项目整理成简历项目,沉淀数据清洗、baseline、特征工程、模型训练、调参、结果分析和文档。
本教程默认你使用 CachyOS + fish shell + VS Code + uv + Python + Git。所有命令默认是 fish。你可以整理真实比赛项目,也可以用一个课程数据集补做一个模块;重点是让项目结构、配置、随机种子、训练入口和 inference 入口清楚。
0. 本周详细教学:语法、规范、验收
本节不是追加在尾部的复习,而是本周正文的入口。先读这里,再做后面的命令和项目。
0.1 本周真正要学会什么
| 维度 | 要求 |
|---|---|
| 知识点 | notebook 拆分、配置、日志、实验记录 |
| 代码语法 | 能从空文件写出本周核心脚本,而不是只复制运行 |
| 程序规范 | 函数拆分、路径清楚、输入输出明确、错误能解释 |
| 交付物 | projects/ai-competition-review/ |
| 验收方式 | 从 fish 终端运行命令,得到可复查的文件或指标 |
0.2 代码语法精讲
下面的代码不是最终答案,而是本周必须理解的最小骨架:
from dataclasses import dataclass@dataclass(frozen=True) class Config: seed: int = 42 data_path: str = “data/train.csv” output_dir: str = “outputs”
config = Config() print(config)
读代码时按四步检查:输入从哪里来;中间变量的类型和 shape 是什么;函数或脚本输出什么;哪些错误应该显式报出来。
0.3 本周程序规范
- 所有路径用相对路径或 `pathlib.Path`,不要写死 `/home/miku/...`。
- 核心逻辑进 `src/`,notebook 只做探索和解释。
- 每个脚本能从 fish 终端运行,并在 README 写出命令。
- 输出必须落盘到 `reports/`、`figures/` 或 `outputs/`,不能只在屏幕上看。
0.4 本周练习分层
| 层级 | 任务 | 不合格表现 | 合格验收 |
|---|---|---|---|
| 最小练习 | 手写上面的最小骨架 | 只在 notebook 里运行 | 终端运行成功 |
| 标准练习 | 把逻辑拆成函数/模块 | 一个大脚本从头写到尾 | 至少 2 个函数,职责清楚 |
| 项目练习 | 生成本周交付物 projects/ai-competition-review/ | 只有屏幕输出 | 文件落盘,可复查 |
| 复盘练习 | 写 3 个错误和修复 | 只写“已解决” | 写清报错、原因、修复、预防 |
0.5 本周和主线的连接
- 回到总计划:USTC AI / Quant 练习手册
- 查详细练习索引:技术练习详解
- 查质量评分:最终质量门槛
1. 本周目标
完成后,你的项目应该从:
notebook_final.ipynb
notebook_final_v2.ipynb
try_again.py
submit_latest.csv
整理成:
projects/ai-competition-review/
├── README.md
├── solution.md
├── configs/
├── data/
├── src/
├── outputs/
├── submissions/
└── reports/
你要交付的不是“更漂亮的文件夹”,而是一个别人能理解、能运行、能复现、能生成预测文件的项目。
2. 前置条件
你需要已经具备:
- 会创建和激活 uv 虚拟环境。
- 会运行 Python 脚本。
- 理解 train / validation split。
- 至少有一个比赛、课程项目或公开数据集项目可以整理。
- 知道不要把大数据和私密 token 提交到 Git。
先确认:
python --version
uv --version
git --version
3. 建立整理用项目
如果你已有比赛项目,可以在原项目旁边新建一个干净版本;不要直接在混乱目录里硬改。
mkdir -p ~/Code/ustc-ai/week11-ai-competition-review
cd ~/Code/ustc-ai/week11-ai-competition-review
uv init --name week11-ai-competition-review
uv venv
source .venv/bin/activate.fish
uv add pandas numpy scikit-learn joblib
code .
命令解释:
| 命令 | 作用 |
|---|---|
uv init |
生成项目配置 |
uv venv |
建立隔离环境 |
source .venv/bin/activate.fish |
用 fish 激活环境 |
uv add ... |
安装表格比赛常用依赖 |
code . |
打开 VS Code 进行整理 |
如果你的比赛是图像或 NLP,可以额外安装 torch、torchvision、transformers 等;但本教程用表格分类 baseline 演示,因为它最适合讲清楚工程结构。
4. 目标文件布局
创建目录:
mkdir -p configs data/raw data/processed src/competition_project outputs/models submissions reports notebooks
推荐布局:
week11-ai-competition-review/
├── configs/
│ └── baseline.toml
├── data/
│ ├── raw/
│ │ ├── train.csv
│ │ └── test.csv
│ └── processed/
├── src/
│ └── competition_project/
│ ├── __init__.py
│ ├── config.py
│ ├── data.py
│ ├── features.py
│ ├── model.py
│ ├── train.py
│ ├── infer.py
│ └── utils.py
├── outputs/
│ └── models/
├── submissions/
├── reports/
│ └── experiments.md
├── notebooks/
├── README.md
└── solution.md
每个目录的职责:
| 路径 | 放什么 | 不放什么 |
|---|---|---|
configs/ |
超参数、路径、随机种子 | 临时实验结果 |
data/raw/ |
原始比赛数据 | 手工改过的数据 |
data/processed/ |
可重新生成的中间数据 | 唯一副本 |
src/competition_project/ |
可复用 Python 代码 | 大段实验笔记 |
outputs/models/ |
训练出的模型文件 | 源代码 |
submissions/ |
提交文件 | 模型权重 |
reports/ |
实验表、错误分析 | 原始数据 |
notebooks/ |
探索性分析 | 唯一可运行训练逻辑 |
5. 把旧文件迁移进新结构
假设旧项目在 ~/Downloads/my-competition-messy,先只复制你需要保留的文件:
cp ~/Downloads/my-competition-messy/train.csv data/raw/train.csv
cp ~/Downloads/my-competition-messy/test.csv data/raw/test.csv
cp ~/Downloads/my-competition-messy/best_notes.ipynb notebooks/eda_original.ipynb
如果文件名不同,就按你的实际情况改。原则是:
- 原始数据只进
data/raw/。 - notebook 只作为分析记录,不再作为主训练入口。
- 最终训练逻辑要落到
src/competition_project/train.py。 - 最终预测逻辑要落到
src/competition_project/infer.py。
6. 写配置文件
创建 configs/baseline.toml:
[paths] train_csv = "data/raw/train.csv" test_csv = "data/raw/test.csv" model_path = "outputs/models/baseline.joblib" submission_path = "submissions/baseline_submission.csv"[target] column = “target” id_column = “id”
[train] seed = 42 valid_size = 0.2
[model] type = “random_forest” n_estimators = 300 max_depth = 8
注意:上面假设你的训练集有 target 列,测试集有 id 列。真实比赛可能叫 label、SalePrice、isDefault 或其他名字,你需要改成自己的列名。
如果你复制后发现 TOML 报错,检查 section 名称。正确写法应该是:
[target]
column = "target"
id_column = "id"
7. 配置读取:config.py
创建 src/competition_project/__init__.py,可以先留空。
创建 src/competition_project/config.py:
from __future__ import annotationsimport tomllib from pathlib import Path from typing import Any
def load_config(path: str | Path) -> dict[str, Any]: config_path = Path(path) with config_path.open(“rb”) as file: return tomllib.load(file)
保持简单。Week 11 不需要一上来引入复杂配置框架。
8. 可复现工具:utils.py
创建 src/competition_project/utils.py:
from __future__ import annotationsimport os import random from pathlib import Path
import numpy as np
def seed_everything(seed: int) -> None: os.environ[“PYTHONHASHSEED”] = str(seed) random.seed(seed) np.random.seed(seed)
def ensure_parent(path: str | Path) -> None: Path(path).parent.mkdir(parents=True, exist_ok=True)
为什么要固定随机种子:
- train / validation split 会受随机数影响。
- 随机森林、神经网络初始化也会受随机数影响。
- 复盘报告里需要说明结果来自哪个 seed。
9. 数据读取:data.py
创建 src/competition_project/data.py:
from __future__ import annotationsimport pandas as pd
def read_train_test(train_csv: str, test_csv: str) -> tuple[pd.DataFrame, pd.DataFrame]: train = pd.read_csv(train_csv) test = pd.read_csv(test_csv) return train, test
def split_features_target( train: pd.DataFrame, target_column: str, ) -> tuple[pd.DataFrame, pd.Series]: y = train[target_column] x = train.drop(columns=[target_column]) return x, y
原则:读取数据和训练模型分开。不要在 train.py 里塞满所有细节。
10. 特征工程:features.py
创建 src/competition_project/features.py:
from __future__ import annotationsimport pandas as pd from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.pipeline import Pipeline from sklearn.preprocessing import OneHotEncoder, StandardScaler
def build_preprocessor(features: pd.DataFrame) -> ColumnTransformer: numeric_columns = features.select_dtypes(include=[“number”, “bool”]).columns.tolist() categorical_columns = [column for column in features.columns if column not in numeric_columns]
plain numeric_pipeline = Pipeline( steps=[ (“imputer”, SimpleImputer(strategy=“median”)), (“scaler”, StandardScaler()), ] ) categorical_pipeline = Pipeline( steps=[ (“imputer”, SimpleImputer(strategy=“most_frequent”)), (“one_hot”, OneHotEncoder(handle_unknown=“ignore”)), ] )
plain return ColumnTransformer( transformers=[ (“numeric”, numeric_pipeline, numeric_columns), (“categorical”, categorical_pipeline, categorical_columns), ] )
这个版本适合表格分类 baseline:
- 数值列:缺失值用中位数填充,然后标准化。
- 类别列:缺失值用众数填充,然后 one-hot。
- 测试集中出现训练集没见过的类别时,
handle_unknown="ignore"不会直接崩。
11. 模型定义:model.py
创建 src/competition_project/model.py:
from __future__ import annotationsfrom sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression
def build_model(config: dict): model_type = config[“model”][“type”] seed = config[“train”][“seed”]
plain if model_type == “random_forest”: return RandomForestClassifier( n_estimators=config[“model”][“n_estimators”], max_depth=config[“model”][“max_depth”], random_state=seed, n_jobs=-1, )
plain if model_type == “logistic_regression”: return LogisticRegression(max_iter=1000, random_state=seed)
plain raise ValueError(f”unsupported model type: {model_type}”)
这里先支持两个模型,方便你做 baseline 对比。不要在 Week 11 一次性塞入十几个模型;项目清晰比模型数量更重要。
12. 训练入口:train.py
创建 src/competition_project/train.py:
from __future__ import annotationsimport argparse from pathlib import Path
import joblib import pandas as pd from sklearn.metrics import accuracy_score, f1_score from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline
from competition_project.config import load_config from competition_project.data import read_train_test, split_features_target from competition_project.features import build_preprocessor from competition_project.model import build_model from competition_project.utils import ensure_parent, seed_everything
def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument(“—config”, type=Path, default=Path(“configs/baseline.toml”)) return parser.parse_args()
def main() -> None: args = parse_args() config = load_config(args.config) seed_everything(config[“train”][“seed”])
plain train, _ = read_train_test(config[“paths”][“train_csv”], config[“paths”][“test_csv”]) features, target = split_features_target(train, config[“target”][“column”])
plain drop_columns = [config[“target”].get(“id_column”)] drop_columns = [column for column in drop_columns if column and column in features.columns] features = features.drop(columns=drop_columns)
plain x_train, x_valid, y_train, y_valid = train_test_split( features, target, test_size=config[“train”][“valid_size”], stratify=target if target.nunique() < 20 else None, random_state=config[“train”][“seed”], )
pipeline = Pipeline( steps=[ ("preprocess", build_preprocessor(x_train)), ("model", build_model(config)), ] ) pipeline.fit(x_train, y_train) predictions = pipeline.predict(x_valid) metrics = { "accuracy": accuracy_score(y_valid, predictions), "f1_macro": f1_score(y_valid, predictions, average="macro"), } print(metrics) model_path = Path(config["paths"]["model_path"]) ensure_parent(model_path) joblib.dump({"pipeline": pipeline, "config": config, "metrics": metrics}, model_path) report_path = Path("reports/baseline_metrics.csv") ensure_parent(report_path) pd.DataFrame([metrics]).to_csv(report_path, index=False) print(f"model saved to {model_path}") print(f"metrics saved to {report_path}")
if name == “main”: main()
运行训练:
source .venv/bin/activate.fish
set -gx PYTHONPATH src
python -m competition_project.train --config configs/baseline.toml
set -gx PYTHONPATH src 的作用是告诉 Python:模块包在 src/ 目录下。以后你也可以改成正式打包方式,但 Week 11 先保持直接。
13. 推理入口:infer.py
创建 src/competition_project/infer.py:
from __future__ import annotationsimport argparse from pathlib import Path
import joblib import pandas as pd
from competition_project.utils import ensure_parent
def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument(“—model”, type=Path, default=Path(“outputs/models/baseline.joblib”)) parser.add_argument(“—test”, type=Path, default=Path(“data/raw/test.csv”)) parser.add_argument(“—output”, type=Path, default=Path(“submissions/baseline_submission.csv”)) return parser.parse_args()
def main() -> None: args = parse_args() artifact = joblib.load(args.model) pipeline = artifact[“pipeline”] config = artifact[“config”]
plain test = pd.read_csv(args.test) id_column = config[“target”].get(“id_column”) ids = test[id_column] if id_column in test.columns else pd.Series(range(len(test)), name=“id”) features = test.drop(columns=[id_column]) if id_column in test.columns else test
plain predictions = pipeline.predict(features) submission = pd.DataFrame({id_column or “id”: ids, config[“target”][“column”]: predictions})
plain ensure_parent(args.output) submission.to_csv(args.output, index=False) print(f”submission saved to {args.output}”)
if name == “main”: main()
运行推理:
source .venv/bin/activate.fish
set -gx PYTHONPATH src
python -m competition_project.infer --model outputs/models/baseline.joblib --test data/raw/test.csv --output submissions/baseline_submission.csv
这一步的意义:
- 比赛项目必须能生成提交文件。
infer.py不应该重新训练模型。- 训练和推理使用同一个预处理 pipeline,避免训练时和提交时特征不一致。
14. Git 忽略规则
创建或更新 .gitignore:
.venv/
__pycache__/
*.pyc
.ipynb_checkpoints/
data/raw/
data/processed/
outputs/models/
submissions/*.csv
解释:
- 虚拟环境不提交。
- 原始比赛数据通常不提交,尤其是有比赛协议或体积大时。
- 模型文件一般不提交到普通 Git 仓库。
- 可以提交
configs/、src/、reports/、README.md、solution.md。
15. 写 solution.md
solution.md 不需要很长,但要讲清楚方案:
# Solution
Task
- Competition / dataset name:
- Prediction target:
- Metric:
Data Cleaning
- Missing values:
- Categorical features:
- Numeric features:
Baseline
- Model:
- Validation split:
- Metric:
Improvements
- Feature engineering tried:
- Model changes tried:
- Hyperparameters tried:
Result
- Local validation:
- Public leaderboard if available:
- Private leaderboard if available:
My Contribution
- I implemented:
- I debugged:
I analyzed:
老师或面试官最关心的是:你做了什么、为什么这样做、结果如何、失败了什么。
16. 实验记录:reports/experiments.md
创建 reports/experiments.md:
# Experiments
ID Date Change Valid Metric Submission Notes exp001 2026-07-26 random forest baseline fill baseline_submission.csv first clean run exp002 2026-07-26 add feature group A fill compare with exp001
每次实验只记录一个主要变化。不要写“改了很多东西然后提升了”,那样无法复盘。
17. 练习
- 把
random_forest 改成 logistic_regression,记录指标差异。
- 增加一个简单特征,例如两个数值列的比值,并记录是否提升。
- 在
solution.md 中写清楚你负责的模块。如果原比赛中你只参与了一小部分,本周补做一个完整 baseline。
- 在
infer.py 里确认输出列名符合比赛提交格式。
- 把 notebook 中能复用的函数迁移到
src/competition_project/,notebook 只保留分析图和思路。
18. 验收检查
在项目根目录执行:
source .venv/bin/activate.fish
set -gx PYTHONPATH src
test -f configs/baseline.toml; and test -f src/competition_project/train.py; and test -f src/competition_project/infer.py
test -f README.md; and test -f solution.md; and test -f reports/experiments.md
python -m competition_project.train --config configs/baseline.toml
python -m competition_project.infer --model outputs/models/baseline.joblib --test data/raw/test.csv --output submissions/baseline_submission.csv
通过标准:
train.py 可以训练并保存模型。
infer.py 可以读取模型并生成提交 CSV。
- 配置、随机种子、路径都不硬编码在 notebook 里。
solution.md 写清楚任务、方案、结果和你的贡献。
reports/experiments.md 至少有一条真实实验记录。
19. 常见错误
19.1 fish 环境没有激活
打开新终端后先执行:
source .venv/bin/activate.fish
否则可能调用系统 Python,导致找不到 pandas 或 sklearn。
19.2 忘记设置 PYTHONPATH
如果看到 ModuleNotFoundError: No module named 'competition_project',执行:
set -gx PYTHONPATH src
然后重新运行 python -m competition_project.train。
19.3 配置文件 TOML 写错
TOML section 必须写成:
[target]
column = "target"
id_column = "id"
不要漏掉左方括号。
19.4 训练和推理特征不一致
不要在 infer.py 里重新写一套预处理逻辑。正确做法是把预处理器和模型放在同一个 sklearn Pipeline 里,一起保存。
19.5 把数据和模型权重提交到 Git
先检查:
git status --short
如果看到 data/raw/ 或 outputs/models/ 里的大文件准备提交,说明 .gitignore 没写好或文件已经被 Git 跟踪,需要先处理。
20. 下一步
进入 Week 12:把 Week 10 的深度学习项目或 Week 11 的比赛整理项目写成项目复盘报告。你会制作实验表、ablation、README、limitations,并学会用“证据”说明你的改进是否真的有效。
plain
気に入ったならばコメントを残してくださいね~