资讯动态

Python人工智能实验包复现指南:环境配置、搜索算法与训练循环

发布时间:2026/10/30 5:06:29 来源:尧图企业网站定制
简介这份2023年春季复旦大学人工智能A实验压缩包面向正在学习人工智能、机器学习课程并需要完成实验与项目作业的高校学生也可供自学者复现课程案例。包内共52个文件以43个Python脚本为核心辅以Markdown说明、PDF实验报告与项目文档并含数据集压缩包整体约2.35MB便于按实验模块查阅代码、报告与数据。内容围绕lab1、lab2及Project展开涉及数据处理、模型实现、命名实体识别等实践环节既有可运行脚本也有实验报告与项目说明可帮助读者理解实验流程、对照实现思路、复盘结果并补充排错线索。已有108人学习适合作为课程实验参考与代码练习材料。1. 一套课程实验包的用法别当答案当一套可复现的 Python 人工智能流水线期末整理硬盘的时候翻出一个某高校人工智能 A 课程的实验压缩包解压出来是十几个 Python 脚本、几份 Notebook 和一堆数据目录。大多数人拿到这种资源的第一反应是找答案但对已经能写脚本的人它真正的价值在于把课上的每个知识点都变成了一段能跑起来的代码从图搜索到特征工程再到一个小型神经网络的训练循环全是同一套工程习惯串起来的。这套东西适合正在学 Python 与人工智能、想要一份完整参考实现的人也适合带实验课的人拿去当对照样本。下面按解压、配环境、跑通、改造的顺序把它逐层拆开来看。2. 解压后的第一件事目录结构、技术栈与运行环境2.1 顶层目录的划分逻辑这类实验包常见的组织方式是「一个实验一个目录」目录名带编号和主题词公共逻辑沉到单独的 common 包里。别急着打开某个 .py 文件先看清整个目录的边界后面配环境和排错全靠这张地图。ai-course-labs/ ├── lab01_search/ # 图搜索与启发式搜索 │ ├── search_core.py # BFS / DFS / A* 的通用实现 │ ├── run_experiments.py # 批量跑地图、输出路径长度与扩展节点数 │ └── maps/ # 迷宫、地图的纯文本描述文件 ├── lab02_ml_basics/ # 传统机器学习完整流程 │ ├── features.py # 特征构造与清洗 │ ├── train_eval.py # 训练、交叉验证、评估报告 │ └── data/ # 表格型数据集 ├── lab03_nn/ # 神经网络 │ ├── dataset.py # Dataset 与 DataLoader 封装 │ ├── model.py # 网络结构定义 │ ├── train.py # 训练循环与验证 │ └── configs/ # YAML 配置学习率、轮数、批大小 ├── lab04_notebook/ # 课堂演示用 Notebook ├── common/ # 日志、随机种子、路径工具 ├── requirements.txt └── README.md这个划分背后有一条清晰的线索lab01 是纯算法不依赖任何第三方数值库用标准库的heapq和collections就能跑lab02 面向表格数据走的是 numpy pandas scikit-learn 这条最稳的路lab03 才引入深度学习框架并且特意把数据管线、模型定义、训练循环拆成三个文件。拆成三个文件这件事值得学很多课程代码喜欢把所有东西塞进一个main.py改一个超参就要翻两百行改到后面自己都晕。common 目录则是在解决一个很实际的问题——路径、日志、随机种子这三样东西如果每个实验各写一遍最后必然不一致。我一般会先看run_experiments.py这类入口脚本而不是核心算法文件。入口脚本决定了一个实验怎么被调用、参数从哪来、结果写到哪里去看懂入口整套代码的调用关系就顺下来了。2.2 技术栈分层哪些库负责哪一段流程把依赖按「承担哪一段工作」来分类比只列一个 requirements.txt 有用得多因为出问题的时候你能立刻定位到是哪一层的事。目录/模块主要依赖承担的工作出问题时的典型表现lab01_search标准库 heapq、collections、itertools优先队列、状态去重、路径回溯结果不是最优解或运行时间暴涨lab02_ml_basicsnumpy、pandas、scikit-learn特征表构建、模型训练、交叉验证维度不匹配、类别比例失衡、指标异常lab03_nntorch、torchvision、tqdm数据加载、前向反向、指标统计损失不下降、显存溢出、训练/验证行为不一致commonPyYAML、logging配置读取、路径解析、日志与种子找不到文件、结果不可复现lab04_notebookjupyterlab、matplotlib课堂演示、可视化内核选错、图表不显示、中文乱码这套分层里最容易被忽略的是 common。表面上它只是几个工具函数实际上它决定了整套实验能不能复现。随机种子、路径解析、日志格式这三件事一旦散落在各个实验里跑出来的结果对不上你连从哪查起都不知道。另外要留意 scikit-learn 和 torch 的定位差别前者是「把模型当组件拼进 Pipeline」后者是「自己写训练循环」。两个实验的设计意图不同不要试图把 lab02 的写法直接搬到 lab03那样只会更乱。这也是这个包值得看的地方——它用两种形态展示了两类工程习惯。2.3 环境搭建顺序虚拟环境、依赖、内核注册环境这一步最容易出问题关键在于「先隔离、再装依赖、最后注册内核」这三步的先后顺序。跳过任一步后面都会遇到一些难以理解的问题。# 1. 建虚拟环境Python 版本按 README 说明来一般 3.9 以上即可 python -m venv .venv # 2. 激活环境macOS / Linux 用 sourceWindows 用 Scripts 下的 activate source .venv/bin/activate # 3. 升级 pip 再装依赖老版本 pip 解析依赖时会报一些奇怪的冲突 python -m pip install --upgrade pip pip install -r requirements.txt # 4. 把当前虚拟环境注册成 Jupyter 内核Notebook 才能 import 到刚装的包 python -m ipykernel install --user --name ai-labs --display-name Python (ai-labs)几个参数的含义值得说清楚--user表示内核配置写到当前用户目录不需要管理员权限--name是内核的内部标识Notebook 顶部的内核切换菜单里能看到--display-name是给人看的名字建议起个能区分的比如带上实验包的名字。第 4 步不做的话Notebook 里import torch会直接抛ModuleNotFoundError而终端里明明装好了——这是新手最常遇到的一类问题本质是 Notebook 用的内核和终端用的解释器不是同一个。装依赖时还有一个细节深度学习框架默认可能拉取体积很大的构建包如果机器没有独立显卡建议在装之前先确认 requirements.txt 里有没有 CPU 版本的相关说明。没有的话按官方索引单独装一次 CPU 版本再装剩下的依赖能省掉几十分钟的下载和一堆驱动相关的报错。3. 核心实验拆解搜索算法、特征工程与训练循环三条主线3.1 图搜索的通用骨架状态、动作与启发函数lab01 里最值得看的是搜索骨架的抽象方式。它没有为每种搜索算法各写一遍循环而是把「邻居怎么生成」抽成一个函数参数然后 BFS、DFS、A* 共用同一套状态管理。这个设计思想放在任何涉及状态空间的问题上都适用。# lab01_search/search_core.py —— 可复用的搜索骨架 from collections import deque import heapq def bfs(start, goal, neighbors): 广度优先只保证步数最少不看边的代价。状态必须可哈希。 queue deque([(start, [start])]) seen {start} while queue: state, path queue.popleft() if state goal: return path for nxt, _cost in neighbors(state): if nxt not in seen: seen.add(nxt) queue.append((nxt, path [nxt])) return None def astar(start, goal, neighbors, heuristic): A*heuristic 需要满足可采纳性否则找到的未必是最优解。 # 堆里存 (f, g, state, path)f g hg 放第二位用于同分时优先扩展浅层 open_heap [(heuristic(start), 0, start, [start])] best_g {start: 0} while open_heap: f, g, state, path heapq.heappop(open_heap) if state goal: return path, g for nxt, step_cost in neighbors(state): new_g g step_cost if new_g best_g.get(nxt, float(inf)): best_g[nxt] new_g heapq.heappush( open_heap, (new_g heuristic(nxt), new_g, nxt, path [nxt]), ) return None, float(inf)先看参数start和goal是可哈希的状态对象迷宫里可以是坐标元组滑块类问题里可以是一个把二维网格压平后的元组neighbors(state)返回一个(下一状态, 代价)的序列代价在 BFS 里被忽略在 A* 里参与累加heuristic(state)返回当前状态到目标的估计值常见的写法是曼哈顿距离或错位块数。逻辑上BFS 用seen做入队即标记的去重A* 用best_g记录每个状态已知的最小代价只有当新路径更便宜时才重新入堆。这个差别决定了 A* 能否在带权图上找到最优解如果只用seen做去重一个状态第一次出堆就锁死后面更优的路径就被丢掉了。这也是很多人在这个实验里遇到「明明写了 A*路径却比 BFS 还长」的根本原因。代码里path [nxt]这种写法在状态空间大时会带来明显的内存开销每个节点都复制一次完整路径。熟悉之后可以改成只记录 parent 指针最后从目标回溯内存占用会降一个量级。什么时候改扩展到几十万节点以上再改小规模迷宫里没必要提前优化。3.2 传统机器学习实验从特征表到评估报告lab02 展示的是一个完整的表格型建模流程它的价值在于「每一步都能单独替换」。整个流程被塞进 Pipeline标准化和模型绑在一起交叉验证时就不会出现「先用全量数据 fit 标准化器」这种数据泄漏。# lab02_ml_basics/train_eval.py —— 一次实验的完整闭环 import pandas as pd from sklearn.model_selection import train_test_split, cross_val_score from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report def build_pipeline(n_estimators200, max_depthNone, seed42): # 标准化对树模型意义不大保留是为了方便一行换成逻辑回归做对比 return Pipeline([ (scaler, StandardScaler()), (clf, RandomForestClassifier( n_estimatorsn_estimators, max_depthmax_depth, random_stateseed, n_jobs-1, )), ]) def run(csv_path, target_col, seed42): df pd.read_csv(csv_path) X df.drop(columns[target_col]).values y df[target_col].values # stratifyy 保证训练集和测试集的类别比例与原始数据一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_stateseed, stratifyy ) model build_pipeline(seedseed).fit(X_train, y_train) print(classification_report(y_test, model.predict(X_test), digits4)) # 交叉验证复用同一套 Pipeline标准化只在训练折上 fit cv cross_val_score(build_pipeline(seedseed), X, y, cv5) print(f5 折均值 {cv.mean():.4f} 标准差 {cv.std():.4f}) return model参数上test_size0.2是课程实验里最常见的划分比例样本量大可以压到 0.1stratifyy在类别不均衡时必开不开的话可能出现某一类在测试集里一个样本都没有的情况评估报告里直接显示 undefinedn_jobs-1表示用满所有 CPU 核心树模型并行训练几乎线性加速。这里有一个容易被忽略的点classification_report输出的是每个类别的精确率、召回率和 F1而不是一个总分。如果只看 accuracy遇到类别不均衡的数据集就会误判——多数类占九成时全猜多数类也能拿到 0.9 的准确率。这个实验包里同时打印分类报告和交叉验证结果就是在提醒这件事。cross_val_score返回的是一组分数而不是单个数字标准差同样重要。均值高但标准差很大的模型说明它对数据划分极其敏感换一批数据就可能不稳定。我一般会要求标准差控制在 0.03 以内超过就去检查特征是否有异常值或样本量是否太少。3.3 神经网络实验Dataset、DataLoader 与训练循环lab03 的训练循环写得比较朴素但训练、评估这两个函数的分工很清楚该有的train()/eval()切换和no_grad一个不少。这套骨架基本可以直接搬到自己的小项目里。# lab03_nn/train.py —— 训练与验证的最小闭环 import torch def train_one_epoch(model, loader, optimizer, criterion, device): model.train() # 影响 BatchNorm 与 Dropout 的行为漏掉会让结果整体偏 total_loss, correct, total 0.0, 0, 0 for x, y in loader: x, y x.to(device), y.to(device) optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() optimizer.step() # 乘 batch 大小再累加最后除总样本数避免最后一个不满批的 batch 拉偏均值 total_loss loss.item() * x.size(0) correct (logits.argmax(dim1) y).sum().item() total x.size(0) return total_loss / total, correct / total torch.no_grad() # 验证阶段不建计算图显存占用能降一大截 def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total 0.0, 0, 0 for x, y in loader: x, y x.to(device), y.to(device) logits model(x) total_loss criterion(logits, y).item() * x.size(0) correct (logits.argmax(dim1) y).sum().item() total x.size(0) return total_loss / total, correct / total核心参数有三个device决定张量在哪块硬件上写torch.device(cuda if torch.cuda.is_available() else cpu)就能同时兼容两种环境criterion分类任务用交叉熵注意交叉熵内部已经含 softmax模型最后一层不能再加一次optimizer.zero_grad()必须在backward()之前调用位置放错会让梯度在 batch 之间累积损失曲线变成一条乱跳的折线。loss.item() * x.size(0)这个写法值得单独说。很多人直接累加loss.item()然后除以 batch 数量当数据集大小不能被 batch 整除时最后一个 batch 样本更少但权重相同算出来的平均损失会轻微偏移。实验报告里对不上数经常就是这个原因。还有一个常见做法是在训练循环里加一个学习率调度器按 epoch 或按验证指标衰减。课程实验一般不用但自己改的时候加上往往能提升几个点代价是要多调一个超参。3.4 Notebook 与模块的分工边界lab04 的 Notebook 只做两件事调参演示和结果可视化。所有能复用的逻辑都在 lab01 到 lab03 的模块里Notebook 靠from lab03_nn.model import Net这种方式引用。这个边界划得很关键。反过来做——把训练逻辑全写进 Notebook——短期看着方便改两次之后就很难维护了单元格执行顺序混乱、变量残留、换个数据集要复制一整个 Notebook。我自己踩过的坑是同一个 Notebook 里改了模型结构但没重启内核旧权重还在内存里跑出来的准确率怎么调都对不上查了大半天。判断标准很简单一段代码如果在两个 Notebook 里都出现过就该抽成模块一段代码如果只跑一次看看结果留在 Notebook 里没关系。这个实验包的划分基本符合这条线。4. 跑通实验的实操路径依赖、数据落位与结果校验4.1 依赖安装与版本对齐课程实验包的 requirements.txt 往往写得很宽松直接pip install -r有可能装到和课程环境差异较大的版本。更稳的做法是先看一眼内容把关键库的版本范围收紧。# requirements.txt 的建议写法给下限同时给一个已知可用的上限 numpy1.24,2.0 # 2.x 之后部分旧接口有变动按 1.x 写的代码可能报错 pandas2.0 scikit-learn1.3 matplotlib3.7 torch2.0 jupyterlab4.0 tqdm PyYAML上限不是必须的但在教学场景里很有用半年后重新装环境没有上限的包可能已经改了几个大版本旧代码报的错会让人误以为是自己的问题。装完之后执行pip freeze env-lock.txt留一份快照下次复现直接照着快照装能省掉大量排查时间。如果安装过程中看到某个包在编译 C 扩展多半是预编译轮子没匹配上当前平台。这种情况优先升级 pip 再重试其次是换一个稍旧的版本实在不行再看是否需要系统级的编译工具链。别在这个时候硬扛换版本往往是最快的路。4.2 数据与资源文件的落位约定这类实验包的数据文件通常不随压缩包一起分发或者体积太大被单独放在了别处。跑之前先确认数据目录的存在性和文件命名比跑起来再报错要省事。# common/paths.py —— 用文件位置反推项目根避免写死绝对路径 from pathlib import Path ROOT Path(__file__).resolve().parents[1] DATA ROOT / data def check_layout(required): 启动时统一检查必需文件缺什么一次性报出来。 missing [p for p in required if not (ROOT / p).exists()] if missing: raise FileNotFoundError(缺少以下文件或目录 、.join(missing))用Path(__file__).resolve().parents[1]定位根目录的好处是不管从哪个工作目录启动脚本路径都能算对。直接用open(data/xxx.csv)这种相对路径在项目根目录跑没问题一旦cd进子目录或者从 Notebook 里调用就会抛FileNotFoundError——这是最容易让人误判成「数据没下载」的一类问题。check_layout这个函数看着不起眼实际很省事把所有必需文件列进去启动时一次性检查完缺三个文件就一次报三个而不是跑一段报一个。我习惯在每个入口脚本的第一行就调用它。4.3 三层校验随机种子、指标数值、可视化产物跑通不等于跑对。我的习惯是用三层校验来判断一次实验是否真的复现了种子固定后结果是否稳定、指标是否落在合理区间、可视化产物是否符合直觉。# common/seed.py —— 一次把所有随机源都固定住 import os import random import numpy as np def set_seed(seed: int 42): os.environ[PYTHONHASHSEED] str(seed) random.seed(seed) np.random.seed(seed) try: import torch torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 开启确定性会牺牲一点速度换来的是同一份代码两次运行结果一致 torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False except ImportError: pass这段代码要在导入 numpy 之后尽快调用越早越好。PYTHONHASHSEED影响哈希相关的遍历顺序集合和字典的迭代顺序变化会让「去重后的样本顺序」发生变化进而影响结果。cudnn.deterministic True会关掉一部分自动调优的卷积算法速度略降但结果可复现做实验报告时值得开。指标校验的标准因任务而异。分类任务看准确率和 F1 是否在同一量级如果 F1 明显低于准确率大概率是类别不均衡回归任务看 RMSE 是否远大于目标值的标准差是的话说明模型基本没学到东西。可视化则是最直观的一层搜索算法的路径画出来有没有穿墙、混淆矩阵是不是只亮对角线一眼就能看出问题。三层都过才算真的跑通。只跑不校验等于把实验跑成了随机数生成器。5. 排查清单跑不起来、结果对不上、内核崩溃的常见原因5.1 环境类问题现象Notebook 里import torch报 ModuleNotFoundError终端里同样的命令却正常。原因Notebook 绑定的内核和虚拟环境不是同一个Jupyter 默认用的可能是系统 Python。解决python -m ipykernel install --user --name ai-labs注册当前环境的内核然后在 Notebook 右上角手动切到Python (ai-labs)重启内核再跑。切完可以用import sys; print(sys.executable)确认路径指向虚拟环境。现象安装过程卡在某个包上十几分钟不动或者报编译错误。原因当前平台没有匹配的预编译轮子pip 退回到了源码编译而系统缺少编译工具链或者网络拉取的是体积很大的 GPU 构建包。解决先pip install --upgrade pip再重试仍失败就指定稍旧的版本或者按官方索引单独安装无独立显卡的机器优先装 CPU 版本。现象plt.show()之后图表没弹出来或者中文全部显示成方块。原因无图形界面的环境比如远程连接、容器内没有可用的绘图后端中文乱码则是字体没有配置。解决无界面环境改用matplotlib.use(Agg)并把图保存成文件中文乱码加两行字体配置指定一个系统里存在的中文字体并把axes.unicode_minus设为 False 解决负号显示问题。5.2 代码与数据类问题现象每次运行交叉验证的分数都不一样实验报告里写的数值复现不出来。原因只设了 numpy 的种子没设 Python 的random和框架内部的种子或者 DataLoader 开了多进程并且 shuffle 没绑 generator。解决统一调用前面那个set_seedDataLoader 里传入generatortorch.Generator().manual_seed(seed)同时把num_workers在调试阶段设为 0确认结果稳定后再开多进程。现象A跑出来的路径比 BFS 还长或者明明有解却返回 None。* 原因前者多半是启发函数不可采纳高估了实际代价后者常见于状态去重时机不对把尚未探索完的等价状态提前锁死。解决先把启发函数换成恒返回 0 的版本退化成 Dijkstra 看结果是否正确确认搜索框架没问题后再逐步加回启发项去重改为记录最小代价而非简单的 seen 集合。另外状态对象如果用列表这种不可哈希的类型入堆时还会报 TypeError统一转成元组。现象训练损失在前几个 epoch 剧烈震荡准确率一直是随机水平。原因学习率过大、输入没有归一化、zero_grad()位置放错导致梯度累积或者model.train()和model.eval()该切换的地方没切。解决先把学习率除以 10 试一次同时对输入做标准化在训练循环前后打印一次参数是否变化快速确认梯度是否真的回传了。这几条基本覆盖了我在同类实验包里见过的大部分问题。规律是环境问题看内核和路径结果问题看种子和随机源指标问题看类别分布和学习率。按这个顺序查比乱改代码快得多。6. 二次开发把这套实验包改成自己的练手底座当所有实验都能跑通、结果也稳定之后它对你的价值就到顶了接下来该考虑怎么把它改成自己的东西。我一般从三个方向动手按投入产出排序。第一个方向是统一入口。现在每个实验都有自己的run_experiments.py参数散落在各处。可以把入口收敛成一个命令行工具用argparse或typer把数据集、种子、输出目录都做成参数配置文件用 YAML 读取。这样跑对比实验时只改一个参数文件不用翻代码。# tools/cli.py —— 把散落的入口收成一个命令 import argparse from pathlib import Path from common.seed import set_seed def main(): parser argparse.ArgumentParser(description统一实验入口) parser.add_argument(--task, choices[search, ml, nn], requiredTrue) parser.add_argument(--config, typePath, defaultPath(configs/default.yaml)) parser.add_argument(--seed, typeint, default42) parser.add_argument(--out, typePath, defaultPath(runs)) args parser.parse_args() set_seed(args.seed) # 种子在入口统一设置避免各实验各写一遍 args.out.mkdir(parentsTrue, exist_okTrue) # 后续按 args.task 分发到对应模块结果统一写到 args.out print(ftask{args.task} config{args.config} seed{args.seed}) if __name__ __main__: main()第二个方向是把 Notebook 降级。所有演示性质的内容留在 Notebook逻辑一律抽到src/下的包里Notebook 只保留调用和画图。改造完之后你会发现自己再也不想在单元格里写训练循环了。第三个方向是补一个最小测试集不用追求覆盖率只测三类东西数据加载能否返回正确形状、模型前向一次能否输出预期维度、搜索算法在一张手写的小地图上能否给出已知最优解。这三条能挡住绝大多数低级回归。改造方向具体动作验证方式统一入口收敛为单一 CLI参数走配置文件同一条命令换--seed两次结果一致抽离逻辑Notebook 逻辑迁到src/只留调用Notebook 重启内核从头运行无报错最小测试数据形状、模型输出、搜索最优解pytest -q全部通过改造顺序上我建议先做第三个方向。听起来反直觉但补测试的过程会逼你把每个模块的输入输出边界想清楚后面改入口和抽包才有参照物。我是先重构再补测试吃过亏的重构过程中把一处状态去重的逻辑改错了没有任何报错只是结果悄悄变差了等发现问题时代码已经改了好几轮只能回滚重来。从那以后我每次动核心逻辑之前都强制先跑一遍测试再动第二行代码宁可多写二十行断言也不想再来一次那种排查。这套实验包本身可能只是某门课的作业集合但它把算法、数据流程、训练循环三条线摆在了同一个目录下正好能当自己项目的起点。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑