简介:一套面向计算机专业学生的高分机器学习大作业复现代码,源自导师指导下的毕业设计项目,评审得分98分。资源聚焦神经对话生成中的对抗学习,包含生成模型、判别模型、预训练与训练测试等完整流程,适合课程设计、期末大作业或算法实战练习。压缩包共20个文件,以Python脚本为主,辅以XML工程配置、说明文档PDF、README及项目文件,整体仅570KB,轻量但结构完整。已有278人学习下载。代码涵盖数据生成、模型搭建、对抗训练、评估测试等环节,使用者可对照论文复现实验,也可在此基础上调整参数或扩展思路。资源附有说明文档,便于理解项目背景与运行步骤,对有机器学习基础、希望快速获取可运行项目样板的学生尤为实用。
1. 论文复现代码不是"能跑就行":一份高分作业该有的可复现性
不少同学拿到论文复现代码,第一反应都是先把训练跑通,看到 loss 下降就松一口气,再截一张训练曲线图交给老师。等到答辩时被问一句"你复现的指标和论文差多少?为什么差?有哪些参数是你自己试出来的?",才发现除了"能跑"什么都答不上来。这篇笔记想解决的就是这个问题:怎么把一份机器学习高分作业意义上的论文复现代码,组织成可以直接使用、可以反复复现、可以解释清楚的东西。我会用 DeepWalk 这篇图表示学习论文作为贯穿例子,因为它的原理直观、代码量适中,很适合作为复现起步。
2. 从论文到代码:拆解一篇机器学习论文的复现路线
复现论文最忌讳上来就写代码。我一般先花半小时把论文的实验部分翻透,因为实验部分定义了"复现成功"到底是什么意思。代码错了可以改,目标错了,后面会一直往错的发现走。这里的关键是把两件事搞清楚:实验边界和数据流。
2.1 先界定实验边界:数据集、指标、baseline
以 DeepWalk 为例,这篇论文要证明的是:用随机游走生成的节点序列训练 Skip-gram,得到的节点向量能很好地完成节点分类任务。复现前要确认四件事:数据集是什么、任务是什么、指标是什么、基线方法是谁。
- 数据集:论文里常用 BlogCatalog、Flickr 这类社交网络图,也有用 Wikipedia 语料做对比的。
- 任务:多标签节点分类,用一部分节点训练逻辑回归,再在剩余节点上评估。
- 指标:一般看 macro-F1 和 micro-F1,两者计数方式不同。
- baseline:论文会对比谱聚类、普通图嵌入等方法。
这些信息不在摘要里,而在 Experiments 和 Implementation Details 部分。如果论文有附录,超参数表多半在附录里;找不到时,再看作者有没有放开源代码的配置,而不是凭空猜。公式描述的是理想情况,实验描述的是实际情况。很多论文的公式里省略了归一化、dropout、学习率衰减等实现细节,这些只能在实验部分或开源代码里找到。所以我的顺序是:先花 10 分钟看实验图表,再花 5 分钟找超参数表,最后才回去读方法。
我会把这些问题整理成一张表,直接写进作业报告里:
| 实验要素 | 论文中的常见设置 | 我在复现时采用的设置 |
|---|---|---|
| 数据集 | BlogCatalog / PPI | 本地样例图(先用小图跑通) |
| 任务 | 多标签节点分类 | 同上 |
| 数据划分 | 随机取 10%/50% 训练 | 固定随机种子,9:1 划分 |
| 评估指标 | macro-F1 / micro-F1 | macro-F1(附带 micro-F1) |
| 基线方法 | 谱聚类、普通图嵌入 | 随机向量作为下限 |
| 关键超参数 | walk_length=40, num_walks=10, window_size=5, dim=128 | 先用 walk_length=10 冒烟测试 |
注意最后一行,我的复现参数和论文不一样,但必须在报告里写明为什么不一样。先用小图和短游走做冒烟测试,是为了快速验证代码管道,并不是最终参数。把这类决策写进报告,老师就知道你清楚自己在做什么。如果论文正文没写超参数,去附录找表格;附录也没有,就去作者开源的 README 或 issue 里翻。再不行,就用一组常识性默认值,并在报告里注明"该项来自社区惯例,论文未披露"。这比编一个参数诚实得多。
另一个容易忽略的点是数据集格式。如果论文原始数据需要特殊渠道获取,可以换一个公开的等规模数据集,但要说明差异。这一步叫界定实验边界,它决定了后面所有代码的输入输出形态。
2.2 把论文的算法流程画成数据流:输入、采样、更新、评估
DeepWalk 的整体流程是:图 → 随机游走 → 节点序列 → Skip-gram → 节点向量 → 分类器。我习惯先用伪代码把数据流写出来,再动手填充实现:
# DeepWalk 算法流程(伪代码,重点是输入输出边界) def deepwalk(graph, num_walks, walk_length, window_size, embed_dim): # graph: 邻接表,例如 {0: [1, 2], 1: [0, 3]} # 1. 从每个节点出发,做 num_walks 次随机游走 walks = generate_random_walks(graph, num_walks, walk_length) # 2. 把游走序列交给 Skip-gram,得到节点向量 embeddings = skipgram(walks, window_size, embed_dim) # 3. 分类器吃向量,输出节点标签预测 return embeddingsgenerate_random_walks 输出的是二维列表,每一行是一条节点序列,例如 [3, 7, 2, 5]。skipgram 内部用滑窗构造"中心节点-上下文节点"对,再用 Word2Vec 训练。理解这个数据流之后,复现的每一步都能确认"这个模块的输入是什么,输出给谁"。
参数说明:walk_length 是游走长度,太长会引入噪声,太短表达不了局部结构;num_walks 控制采样覆盖度;window_size 影响上下文范围;embed_dim 是最终向量的维度。这四个参数是 DeepWalk 复现时最常调的。
把数据流画出来还有一个好处:代码结构可以跟着它走。因为你知道数据从哪来、经过哪些环节、最后到哪去,写代码时自然就能拆成多个模块,而不是把所有逻辑塞进一个脚本。很多论文复现代码一打开就是几百个函数,新手容易迷失;我一般先按数据流拆,把每个模块的输入输出写在 docstring 里,即使某些细节看不懂,也能通过边界判断它的作用。对一份机器学习项目的作业来说,可读比巧妙重要得多。
DeepWalk 的转移概率在论文里写得很简单:从当前节点均匀随机地选择一个邻居作为下一步。实现时要注意:如果节点有自环,邻接表里出现自身,游走可能会原地打转;如果图是无向且带权,转移概率要按权重归一化。复现时先按无权图实现,后续再决定要不要支持权重。
Skip-gram 的输入是节点序列,但序列里可能重复出现同一个节点多次,这是正常的。训练前需要把节点 id 映射到词表的整数索引,否则 gensim 的 Word2Vec 会把它当成字符串。这一映射最好和 dataset.py 里的 id_map 保持一致,否则就会遇到 5.4 节的错位问题。
很多模型官方会放出训练好的向量,那能不能直接拿来做分类实验?可以,但这不是论文复现,而是模型使用。高分作业要求的是你亲自把论文的方法跑通一遍,所以至少要把随机游走和 Skip-gram 训练这两段核心流程写在代码里,即使最终结果略低于官方向量,也说明你理解了方法。此外,如果原论文提供了开源代码,我的建议是不要先读训练部分,先读数据加载和评估部分。因为这两部分是实验边界的直接体现。训练部分每个人写法不同,但数据接口和评估逻辑必须和论文描述一致。
表格里的 baseline 不要只填论文的,自己加一个随机向量作为下限:把节点 id 随机映射到等维度的向量,用同样的分类流程评估。如果 DeepWalk 的指标还打不过随机向量,说明实现有 bug。这个习惯能快速发现数据对不齐或标签泄漏的问题。
3. 搭建可直接运行的复现工程:目录、配置、数据加载
论文复现代码能不能直接使用,最直观的标准是:别人把这个机器学习项目 clone 下来后,根据 README 里的命令一路执行,不需要额外改路径。我见过太多代码把自己电脑上的绝对路径写死在代码里,换个环境就崩。这里的关键是:目录结构清晰、配置集中、路径相对。
3.1 目录结构设计:让评分老师一眼看懂
下面是一个 DeepWalk 复现作业的目录布局。它不是唯一答案,但每个模块的职责非常清楚:
reproduce_deepwalk/ ├── README.md ├── requirements.txt ├── configs/ │ ├── deepwalk.yaml │ └── smoke.yaml ├── data/ │ ├── sample.edgelist │ └── sample.labels ├── src/ │ ├── __init__.py │ ├── dataset.py │ ├── random_walk.py │ ├── model.py │ ├── train.py │ └── evaluate.py └── scripts/ └── run_experiment.pysrc/dataset.py 负责读边列表和标签;src/random_walk.py 负责生成游走序列;src/model.py 封装 Skip-gram 嵌入;src/train.py 组织训练流程;src/evaluate.py 计算指标;scripts/run_experiment.py 是唯一入口,从配置读到训练评估串起来。README.md 里写清环境安装和复现命令,这就是可直接使用的门面。
逻辑说明:这份结构把数据、配置、源码、入口分开。评分老师能顺着文件名找到每个环节,而不是在一个巨型 main.py 里翻几百行。你写报告时,也可以按数据 → 游走 → 嵌入 →分类的顺序对应到文件位置。需要遵守的约定是:代码里只写相对路径,基于项目根目录运行命令。
不要做的事情:不要把训练好的嵌入文件、日志文件、数据集压缩包堆在根目录里。它们应该在运行时生成,放到 results/ 和 logs/ 下,并在 .gitignore 里忽略。一份干净的项目树,本身就是作业分数的一部分。
3.2 用 YAML 集中管理超参数:argparse 留给命令行覆盖
作业场景下,超参数散落在代码各处是大忌。常见做法是集中到一个配置文件里,例如:
# configs/deepwalk.yaml data: graph_path: data/sample.edgelist labels_path: data/sample.labels model: embed_dim: 128 walk_length: 40 num_walks: 10 window_size: 5 train: workers: 4 epochs: 5 seed: 42 alpha: 0.025 evaluate: train_ratio: 0.9 metric: macro_f1加载配置时,我用 PyYAML 的 safe_load,再配合 argparse 做少量覆盖。比如临时想换一个随机种子,不用改文件:
# scripts/run_experiment.py import argparse import yaml def load_config(): parser = argparse.ArgumentParser() parser.add_argument("--config", default="configs/deepwalk.yaml") parser.add_argument("--seed", type=int, default=None) args = parser.parse_args() with open(args.config, "r", encoding="utf-8") as f: config = yaml.safe_load(f) if args.seed is not None: config["train"]["seed"] = args.seed return config参数说明:--config 指定配置文件路径,--seed 覆盖配置里的随机种子。这样你在做参数敏感性实验时,只改 YAML,不动代码;命令行覆盖只用于临时实验。yaml.safe_load 而不是 yaml.load,避免反序列化任意对象的安全问题。
为什么不用 argparse 写全部参数?因为参数一多,命令行长且容易抄错;YAML 可以加注释,记录每个参数的含义和论文里的取值。如果课程要求必须用命令行传参,也可以保留 argparse,但我更推荐"配置文件 + 少量覆盖"的组合,这也是现在开源项目的主流。注意:在代码里不要 import config 后直接读模块属性,那会让路径纠缠不清,YAML 才是项目的单一事实来源。
3.3 图数据加载与预处理:边列表到邻接表
DeepWalk 的输入是一张图,最常见的格式是边列表:每行一条边,两个节点 id。我写的加载函数长这样:
# src/dataset.py from collections import defaultdict def load_edgelist(path): graph = defaultdict(list) id_map = {} nodes = set() with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue u, v = line.split()[:2] if u not in id_map: id_map[u] = len(id_map) if v not in id_map: id_map[v] = len(id_map) uid, vid = id_map[u], id_map[v] graph[uid].append(vid) graph[vid].append(uid) # 无向图 nodes.add(uid) nodes.add(vid) return graph, list(nodes), id_map逻辑说明:先把字符串节点映射成连续的整数 id,再用 defaultdict(list) 存邻接表。边列表是无向的,所以同时写入两个方向。跳过空行和注释行,这是数据文件里常见的脏数据。返回的 id_map 在后面映射标签时要用到。
参数说明:如果论文用的是有向图,这里就别加 graph[vid].append(uid),同时在配置里声明 graph_type: directed。孤立节点不会进邻接表,但会在 nodes 里,做分类时要保留它,否则评估索引对不上。这里踩过的坑,我会在第 5 章详细展开。
标签文件通常长这样:两列,第一列是节点,第二列是类别。对单标签分类可以直接和 id_map 对齐:
# src/dataset.py def load_labels(path, id_map): labels = {} with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue node_str, label = line.split()[:2] if node_str in id_map: labels[id_map[node_str]] = int(label) return labels注意:如果标签文件里出现了 id_map 没有的节点,直接忽略会损失数据。更稳妥的写法是先统计被忽略的节点数,打印一条警告。这样在结果偏低时,你能知道是数据对齐问题而不是算法问题。
数据加载完,我习惯马上做一次校验:打印节点数、边数、标签数,并检查是否存在度为 0 的节点:
# 脚本开头可选的 sanity check isolated = [n for n in nodes if not graph[n]] print(f"nodes={len(nodes)}, edges={sum(len(v) for v in graph.values())//2}, " f"labels={len(labels)}, isolated={len(isolated)}")这段日志看起来不起眼,但能在训练前暴露大部分数据问题。很多论文复现翻车,都发生在这一步之前。
4. 训练与评估:让复现结果逼近论文的 3 个关键动作
代码工程搭好,训练是下一步。这里不讲怎么调神经网络,只讲 3 个让结果可复现、可对齐的关键动作:先跑通、锁随机源、对齐评估协议。这里讲的顺序很重要:先冒烟,再锁定随机源,最后对齐评估。顺序反了,你会在一个随机波动的指标上反复调参,白白浪费一晚上。
4.1 先跑通一个闭环:用最小配置验证代码没写错
我见过太多的复现代码在"最终训练"阶段报错,等排查完已经耗尽信心。所以我在项目中加了一个 configs/smoke.yaml,把参数全部缩到最小:
# configs/smoke.yaml data: graph_path: data/sample.edgelist labels_path: data/sample.labels model: embed_dim: 16 walk_length: 5 num_walks: 2 window_size: 2 train: workers: 2 epochs: 1 seed: 42 alpha: 0.025 evaluate: train_ratio: 0.9 metric: macro_f1运行命令:
python scripts/run_experiment.py --config configs/smoke.yaml冒烟测试只要观察三件事:程序能完整跑完、训练 loss 在下降、评估函数输出一个正常的指标,比如 macro-F1 在 0.1 到 1.0 之间。如果连这个都过不了,就要把错误堆栈拉到最短,先修数据流问题,不要直接调大模型。
参数说明:embed_dim 从 128 降到 16,walk_length 从 40 降到 5,num_walks 从 10 降到 2,epochs 从 5 降到 1,训练时间从分钟级降到秒级。注意:冒烟测试的结果不能写进报告,它的作用只是验证代码管道没有断。等闭环保通后,再恢复论文参数跑正式实验。
冒烟测试通过后,再用 deepwalk.yaml 跑正式训练。此时最好开启日志记录,包括每个 epoch 的 loss 和当前时间,方便判断训练是否收敛。如果 loss 不降,先看数据是否对齐,再调 alpha。不要把训练日志只打在屏幕输出里,写进 logs/train.log 更干净。
4.2 随机种子与多进程:复现的确定性来自哪里
论文复现中最影响指标稳定性的不是算法本身,而是随机性。随机游走、负采样、逻辑回归初始化、数据划分都可能引入随机数。如果不设置种子,两次运行结果可能差 2 到 3 个百分点,你就没法判断改动是有效还是噪声。
# src/train.py import os import random import numpy as np def set_seed(seed): os.environ["PYTHONHASHSEED"] = str(seed) random.seed(seed) np.random.seed(seed)逻辑说明:PYTHONHASHSEED 影响字符串哈希,在数据加载时如果依赖节点 id 的哈希顺序,会引入随机性。random.seed 和 np.random.seed 覆盖 Python 和 NumPy 的随机源。注意这个设置要在任何数据加载之前调用,顺序不对就锁不住。
如果你用了 PyTorch,还需要 torch.manual_seed(seed);用了 gensim 的 Word2Vec,则需要给 Word2Vec 传 random_state。全局种子管不到这些框架内部的随机源。这个细节是很多人复现结果对不上的原因。
另外,gensim 的 Word2Vec 默认使用多线程,workers 参数会影响数据打乱顺序。单线程和多线程训练得到的嵌入不是完全一致。想完全复现,就把 workers 固定在配置里,比如 4,不要在不同机器上换来换去。
4.3 评估协议对齐:为什么你的数字和论文差一截
这是复现翻车的高发区。DeepWalk 的节点分类评估通常是用部分节点,比如 10% 训练逻辑回归,在剩余节点上测试,多次随机划分取平均。但论文可能用了某个特定的 train/test split,你没注意,导致结果偏差。我的评估代码会显式控制划分比例并固定种子:
# src/evaluate.py from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import f1_score def evaluate_embeddings(embeddings, labels, train_ratio=0.9, seed=42, metric="macro_f1"): X = [embeddings[n] for n in sorted(labels.keys())] y = [labels[n] for n in sorted(labels.keys())] X_train, X_test, y_train, y_test = train_test_split( X, y, train_size=train_ratio, random_state=seed, stratify=y ) clf = LogisticRegression(max_iter=1000) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) if metric == "macro_f1": return f1_score(y_test, y_pred, average="macro") return f1_score(y_test, y_pred, average="micro")逻辑说明:先按节点 id 排序确保特征和标签对齐,再用 train_test_split 按比例划分。stratify=y 让训练集和测试集的标签分布保持近似,这对不平衡标签很重要。random_state=seed 保证每次划分一致。
参数说明:train_ratio=0.9 表示用 90% 节点训练,10% 测试。很多论文写的是用 10% 的节点训练,那要设置 train_size=0.1 还是 train_size=train_ratio?一定要看论文的表述。我曾经在这里把 ratio 和 size 搞反,结果低了 10 多个点。还有,如果标签是多标签分类,stratify 不能直接用,需要换成 multi-label 的评估方式,或者评估每个标签的 F1 再取平均。
如果论文用了不同的分类器,比如 SVM 或 MLP,你要注意:DeepWalk 原论文用逻辑回归作为下游分类器,有些复现会用别的模型。不同分类器对嵌入质量的评价标准不同,换分类器就等于改变实验设置。所以第一版复现务必用逻辑回归,等结果对齐后再考虑其他分类器。
在训练中如何判断收敛?除了看最终指标,我还会记录每个 epoch 的 loss。Skip-gram 的 loss 通常不会降到 0,而是在一个区间内波动;如果 loss 一直不降,问题多半在前面的数据加载。如果只打印最终结果,中间过程就是黑匣子,出了问题很难定位。有一次我复现一篇交通类论文的样例代码时,数据加载和模型都写好了,但训练一直不收敛,最后发现是数据归一化顺序写错。从那以后,我固定用一个 sanity check 函数在训练前打印输入数据形状、均值和方差,如果均值不是有限数就直接终止。
更稳妥的做法是:所有评估都保留打印 macro_f1 和 micro_f1 两个值,在报告里同时给出:
print(f"macro_f1={macro_f1:.4f}, micro_f1={micro_f1:.4f}")跑 5 次取平均时,最好把 5 次结果都写进 results/metrics.json,而不是只写均值。这样老师能直接看到方差,比一句"稳定"更有说服力。我一般保存成 metrics_seed42.json、metrics_seed123.json 这样的文件。
5. 论文复现代码避坑指南:5 个常见的翻车现场
前面几章的流程都走过之后,剩下的就是查漏补缺。下面 5 个坑是我在复现 DeepWalk 类似项目时反复看到的,也在自己的作业里踩过。每条按现象 → 原因 → 解决写,希望能让你少走一点弯路。
5.1 依赖版本不一致:装了 3 小时,最后发现是 gensim 版本
现象:随机游走代码和网上的教程一致,但训练 Word2Vec 时报AttributeError: 'Word2Vec' object has no attribute 'wv',或者跑完结果和别人的完全不一样。
原因:gensim 4.x 改了接口,model['node']必须写成model.wv['node']。如果代码是从旧教程抄的,就会踩中。sklearn、NumPy 版本不同,逻辑回归的收敛行为也会略有差异,导致同样的代码在不同环境下跑出不同指标。
解决:把版本锁在 requirements.txt 里,写明gensim==4.3.2、scikit-learn==1.3.0、numpy<2.0。如果一定要用旧接口,就装gensim==3.8.3并调整代码。我一般用pip freeze > requirements.txt生成初始文件,再人工清理无关项。复现代码要能在干净环境里一键安装,而不是靠你电脑里的历史包。
5.2 数据划分不一致:你的训练集和论文的训练集不是一回事
现象:跑出来的 macro-F1 比论文低 5 个点,但算法实现看起来没问题。
原因:你用了 train_test_split 默认的 75% 训练,而论文用 10% 或 50% 训练;或者论文用了固定划分文件,你每次重新随机划分,相当于换了一个数据集。同一个模型在不同数据划分上的表现差异,很容易超过 5 个点。
解决:回到 2.1 的"实验边界"表,确认论文的 train/test split 方式和比例。如果论文没有明确,就按"随机划分 + 5 次平均"写在报告里,并在 README 中说明。最稳妥的是用和论文一致的划分文件;找不到时,固定种子并把划分逻辑写到配置里,让别人能复现你的划分。
5.3 指标口径错误:macro-F1 还是 micro-F1
现象:你的 micro-F1 很高,但报告里抄成 macro-F1,看起来和论文差一截;或者反过来,你报了 macro-F1,但论文用的是 micro-F1。
原因:多标签分类里 macro-F1 先按类别算 F1 再平均,对少数类敏感;micro-F1 把所有类别的 TP、FP、FN 汇总再算,指标值通常高于 macro。两个口径混用是复现报告里最常见的低级错误。
解决:在配置文件里明确 metric: macro_f1,代码里写死 metric 名,评估时同时输出两个值。报告里说清你报的是哪个,最好两个都给。最后再对照论文实验表,确认论文用的是哪个口径,别闭着眼抄数字。
5.4 随机游走漏了孤立节点或标签错位
现象:训练不报错,但画嵌入图时一堆节点堆在原点上,或者分类时索引越界。
原因:随机游走时如果节点度为 0,就没有邻居可走,循环会卡住或返回空序列。另一个常见问题是节点 id 映射对不上:边列表用的 id_map 和标签文件用的 id_map 不是同一份,导致 labels 错位。
解决:加载邻接表后立刻打印孤立节点数量,游走函数对孤立节点返回空序列;加载标签时,用边列表生成的 id_map 做转换,并把没匹配上的标签打印出来。我习惯在数据预处理后输出日志:
# 诊断代码片段 isolated = [n for n in nodes if not graph[n]] print(f"nodes={len(nodes)}, isolated_nodes={len(isolated)}, labels={len(labels)}")这行日志能让你在训练前就发现数据问题,而不是等 20 分钟训练完再面对一个烂结果。调代码时看起来很慢,实际上比反复训练快得多。
5.5 训练不稳定:loss 变成 NaN 或嵌入向量发散
现象:训练到某一步打印 loss 为 nan,或者嵌入向量的模越来越大,分类结果断崖式下降。
原因:常见于学习率过高或负采样没做归一化。在 Skip-gram 里,如果上下文窗口出现重复节点,更新步长过大,梯度会溢出。gensim 的 alpha=0.025 是经验值,但如果你把 min_count 设成 0,或窗口里全是低频节点,也可能触发。
解决:先用默认超参数跑通,再尝试调参。如果 loss 震荡,把 alpha 调小一个数量级,例如从 0.025 降到 0.0025。设置 min_count=1 可以保留所有节点,但要注意低频节点的统计噪声。还有一种做法是给梯度加裁剪,但 gensim 不直接暴露这个接口,所以降低学习率是最实际的手段。记录每个 epoch 的 loss,方便定位是第几步开始发散的。
遇到上述任何问题,我的第一反应不是搜索代码,而是先构造一个最小复现:把数据集换成一棵只有 3 个节点的树,把 walk_length 换成 2,看问题还在不在。这个习惯能帮你把环境问题和算法问题快速分开。很多时候,问题不在训练脚本,而在数据加载和版本依赖上。能在出错信息里看到数据的形状,比看到模型结构更有用。
6. 把复现代码变成高分作业:一键复现脚本与验证清单
前面几章的内容做完,代码已经能跑,但离高分作业还差一步:让别人,尤其是评分老师,能一条命令从头复现你的全部结果。这一章讲怎么把实验入口收拢,以及交付前怎么验证。
完整入口脚本应该把配置加载、数据读取、游走、训练、评估、结果持久化全部串起来:
# scripts/run_experiment.py 完整入口骨架 import json from src.dataset import load_edgelist, load_labels from src.random_walk import generate_random_walks from src.model import train_skipgram from src.evaluate import evaluate_embeddings def main(): config = load_config() # 见 3.2 节 graph, nodes, id_map = load_edgelist(config["data"]["graph_path"]) labels = load_labels(config["data"]["labels_path"], id_map) walks = generate_random_walks(graph, config["model"]["num_walks"], config["model"]["walk_length"]) embeddings = train_skipgram(walks, config["model"]["embed_dim"], config["model"]["window_size"]) macro_f1 = evaluate_embeddings( embeddings, labels, config["evaluate"]["train_ratio"], config["train"]["seed"], config["evaluate"]["metric"] ) result = {"macro_f1": macro_f1, "config": config} with open("results/metrics.json", "w", encoding="utf-8") as f: json.dump(result, f, indent=2, ensure_ascii=False) print(json.dumps({"macro_f1": macro_f1}, indent=2)) if __name__ == "__main__": main()逻辑说明:results/metrics.json 里同时保存指标和完整配置,这样后来任何时候都能知道该指标是在什么参数下跑出来的。注意脚本运行之前,要确保 results/ 目录存在,否则用 os.makedirs("results", exist_ok=True) 包一下。
交付前,我会按这张清单过一遍:
| 检查项 | 具体标准 |
|---|---|
| 环境可复现 | requirements.txt 锁定版本,README 有一键安装命令 |
| 路径可移植 | 代码中无绝对路径,全部相对项目根目录 |
| 数据可追溯 | 边列表、标签与 id_map 对齐,孤立节点已统计 |
| 训练可复现 | 随机种子固定,训练日志有记录,loss 收敛 |
| 评估可对齐 | train_ratio 和 metric 与论文一致,跑 5 次取均值 |
| 一键可复现 | 运行 run_experiment.py 后能从零产出 results/metrics.json |
最后给你一个加分技巧:做参数敏感性实验。把 walk_length 从 10 改成 20、40,记录 macro-F1 的变化,画一张小表写进报告。如果代码里支持通用覆盖参数,比如 --set model.walk_length 40,就可以用一个循环批量跑:
for wl in 10 20 40; do python scripts/run_experiment.py \ --config configs/deepwalk.yaml \ --set model.walk_length $wl done这组实验能让老师看到你不是只会跑通代码,而是真的理解每个参数对结果的影响。我自己以前交作业只交 main.py,老师问了三个问题全答不上来,后来花一小时补上配置、README、固定种子和评估脚本,结果从良变成了优。高分作业不是算法最炫的,而是最容易验证的。代码能跑只是起点,可复现才是终点。希望帮到你。
本文还有配套的精品资源,点击获取