Week 11:AI 比赛代码整理——从混乱 notebook 到可复现项目

Published 2026-07-26 11:00 4482 words 23 min read

This post is not yet available in English. Showing the original.
Week 11:AI 比赛代码整理——从混乱 notebook 到可复现项目。fish-first 终端教学,面向 CachyOS、VS Code、uv 和 Python 学习路线。
Oh My Pi / weekly tutorial / week11-ai-competition-code-cleanup
miku@cachyos:~/Code/python-learning$ omp teach week11-ai-competition-code-cleanup --fish-first --step-by-step
source644 行教学文档
weekWeek 11
shellfish-first 命令版
backlink20 周计划

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 代码语法精讲

下面的代码不是最终答案,而是本周必须理解的最小骨架:

omppython
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 本周和主线的连接

1. 本周目标

完成后,你的项目应该从:

ompprompt
notebook_final.ipynb
notebook_final_v2.ipynb
try_again.py
submit_latest.csv

整理成:

ompprompt
projects/ai-competition-review/
├── README.md
├── solution.md
├── configs/
├── data/
├── src/
├── outputs/
├── submissions/
└── reports/

你要交付的不是“更漂亮的文件夹”,而是一个别人能理解、能运行、能复现、能生成预测文件的项目。

2. 前置条件

你需要已经具备:

  • 会创建和激活 uv 虚拟环境。
  • 会运行 Python 脚本。
  • 理解 train / validation split。
  • 至少有一个比赛、课程项目或公开数据集项目可以整理。
  • 知道不要把大数据和私密 token 提交到 Git。

先确认:

ompfish
python --version
uv --version
git --version

3. 建立整理用项目

如果你已有比赛项目,可以在原项目旁边新建一个干净版本;不要直接在混乱目录里硬改。

ompfish
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,可以额外安装 torchtorchvisiontransformers 等;但本教程用表格分类 baseline 演示,因为它最适合讲清楚工程结构。

4. 目标文件布局

创建目录:

ompfish
mkdir -p configs data/raw data/processed src/competition_project outputs/models submissions reports notebooks

推荐布局:

ompprompt
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,先只复制你需要保留的文件:

ompfish
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

如果文件名不同,就按你的实际情况改。原则是:

  1. 原始数据只进 data/raw/
  2. notebook 只作为分析记录,不再作为主训练入口。
  3. 最终训练逻辑要落到 src/competition_project/train.py
  4. 最终预测逻辑要落到 src/competition_project/infer.py

6. 写配置文件

创建 configs/baseline.toml

omptoml
[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 列。真实比赛可能叫 labelSalePriceisDefault 或其他名字,你需要改成自己的列名。

如果你复制后发现 TOML 报错,检查 section 名称。正确写法应该是:

omptoml
[target]
column = "target"
id_column = "id"

7. 配置读取:config.py

创建 src/competition_project/__init__.py,可以先留空。

创建 src/competition_project/config.py

omppython
from __future__ import annotations

import 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

omppython
from __future__ import annotations

import 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

omppython
from __future__ import annotations

import 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

omppython
from __future__ import annotations

import 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

omppython
from __future__ import annotations

from 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

omppython
from __future__ import annotations

import 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=[
        (&quot;preprocess&quot;, build_preprocessor(x_train)),
        (&quot;model&quot;, build_model(config)),
    ]
)
pipeline.fit(x_train, y_train)
predictions = pipeline.predict(x_valid)

metrics = {
    &quot;accuracy&quot;: accuracy_score(y_valid, predictions),
    &quot;f1_macro&quot;: f1_score(y_valid, predictions, average=&quot;macro&quot;),
}
print(metrics)

model_path = Path(config[&quot;paths&quot;][&quot;model_path&quot;])
ensure_parent(model_path)
joblib.dump({&quot;pipeline&quot;: pipeline, &quot;config&quot;: config, &quot;metrics&quot;: metrics}, model_path)

report_path = Path(&quot;reports/baseline_metrics.csv&quot;)
ensure_parent(report_path)
pd.DataFrame([metrics]).to_csv(report_path, index=False)
print(f&quot;model saved to {model_path}&quot;)
print(f&quot;metrics saved to {report_path}&quot;)

if name == “main”: main()

运行训练:

ompfish
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

omppython
from __future__ import annotations

import 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()

运行推理:

ompfish
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

ompprompt
.venv/
__pycache__/
*.pyc
.ipynb_checkpoints/
data/raw/
data/processed/
outputs/models/
submissions/*.csv

解释:

  • 虚拟环境不提交。
  • 原始比赛数据通常不提交,尤其是有比赛协议或体积大时。
  • 模型文件一般不提交到普通 Git 仓库。
  • 可以提交 configs/src/reports/README.mdsolution.md

15. 写 solution.md

solution.md 不需要很长,但要讲清楚方案:

ompmarkdown
# 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

ompmarkdown
# Experiments





































IDDateChangeValid MetricSubmissionNotes
exp0012026-07-26random forest baselinefillbaseline_submission.csvfirst clean run
exp0022026-07-26add feature group Afillcompare with exp001

每次实验只记录一个主要变化。不要写“改了很多东西然后提升了”,那样无法复盘。

17. 练习

  1. random_forest 改成 logistic_regression,记录指标差异。
  2. 增加一个简单特征,例如两个数值列的比值,并记录是否提升。
  3. solution.md 中写清楚你负责的模块。如果原比赛中你只参与了一小部分,本周补做一个完整 baseline。
  4. infer.py 里确认输出列名符合比赛提交格式。
  5. 把 notebook 中能复用的函数迁移到 src/competition_project/,notebook 只保留分析图和思路。

18. 验收检查

在项目根目录执行:

ompfish
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 环境没有激活

打开新终端后先执行:

ompfish
source .venv/bin/activate.fish

否则可能调用系统 Python,导致找不到 pandas 或 sklearn。

19.2 忘记设置 PYTHONPATH

如果看到 ModuleNotFoundError: No module named 'competition_project',执行:

ompfish
set -gx PYTHONPATH src

然后重新运行 python -m competition_project.train

19.3 配置文件 TOML 写错

TOML section 必须写成:

omptoml
[target]
column = "target"
id_column = "id"

不要漏掉左方括号。

19.4 训练和推理特征不一致

不要在 infer.py 里重新写一套预处理逻辑。正确做法是把预处理器和模型放在同一个 sklearn Pipeline 里,一起保存。

19.5 把数据和模型权重提交到 Git

先检查:

ompfish
git status --short

如果看到 data/raw/outputs/models/ 里的大文件准备提交,说明 .gitignore 没写好或文件已经被 Git 跟踪,需要先处理。

20. 下一步

进入 Week 12:把 Week 10 的深度学习项目或 Week 11 的比赛整理项目写成项目复盘报告。你会制作实验表、ablation、README、limitations,并学会用“证据”说明你的改进是否真的有效。

plain

If you enjoyed this, leave a comment~

© 2026 江无没有月 @miku
Powered by theme astro-koharu · Inspired by Shoka