Informed_machine_learning

semantic_loss_toy 代码逐行解析

这份文档对应当前目录下的 03_semantic_loss_toy 项目,目标不是只告诉你“它能跑”,而是把它拆成“你能自己改、自己讲”的程度。

说明:


1. 先看整个 toy 的执行链

这个 toy 的执行路径是:

  1. run.py
    • 解析命令行参数
    • 生成 ExperimentConfig
    • 调用 experiment.py 里的 run_experiment
  2. experiment.py
    • 设随机种子
    • 生成 labeled / unlabeled / val / test 数据
    • 读取约束定义
    • 分别训练 baseline 和 semantic-guided 两个模型
    • 保存 metrics.json 和图
  3. trainer.py
    • baseline:只用监督信号训练
    • semantic-guided:监督项 + semantic loss
    • 评估 accuracy、约束满足概率、硬满足率
    • 画训练曲线和决策区域图
  4. constraints.py
    • 定义“哪些二值赋值是合法的”
    • 计算满足约束的概率质量
    • 把它变成 semantic loss
  5. data.py
    • 生成 4 类二维 toy 数据
    • 人工保证各类大致平衡
    • 分出 labeled、unlabeled、val、test
  6. model.py
    • 定义最小 MLP
    • 输出 4 个独立 logits

所以这整个项目压成一句话就是:

先造一个 4 类分类问题,再把“合法输出结构”写成一组二值赋值集合,然后让模型在无标签样本上尽量把概率质量压到这些合法赋值上。


2. run.py

这个文件是命令行入口。

2.1 导入部分

2.2 parse_args

2.3 build_config

2.4 main

2.5 入口保护

这个文件的核心作用很纯粹:

它不管训练细节,只负责把“命令行世界”转成“实验配置世界”。


3. config.py

这个文件集中定义所有实验超参数。

3.1 导入部分

3.2 配置类声明

3.3 数据规模相关参数

3.4 模型和 batch 参数

3.5 优化相关参数

3.6 semantic loss 相关参数

3.7 绘图和设备参数

3.8 输出目录参数

3.9 ensure_results_dir

3.10 to_dict

这个文件的核心思想是:

所有模块都只接收一个 config,不各自偷偷维护一套参数。


4. data.py

这个文件负责定义 toy 数据分布。

4.1 导入和数据打包结构

4.2 随机种子

4.3 class_scores

这几行的本质是:

先人为定义 4 个“类势能面”,哪个分数最大,样本就属于哪个类。

4.4 make_labels

4.5 _sample_candidate_pool

4.6 make_balanced_dataset

这个函数很关键,它不是随便采样,而是主动让每一类样本数大致平衡。

这整个函数的目的就是:

尽量避免“某一类太少”,否则很容易把后面 accuracy 和 semantic effect 的比较搅乱。

4.7 create_datasets

这个文件的核心思想是:

故意制造“少量标注 + 大量无标签 + 类别平衡”的训练环境,让你能清楚观察 semantic loss 的作用。


5. constraints.py

这是整个 toy 最关键的文件,因为 semantic loss 真正定义在这里。

5.1 导入

5.2 ConstraintSpec

5.3 _all_binary_assignments

5.4 available_constraint_sets

5.5 get_constraint_spec

exactly_one

at_least_one

exactly_two_bad

非法名字

返回 ConstraintSpec

5.6 log_satisfaction_mass

这是 semantic loss 的数学核心。

这一段对应的直觉公式就是:

log P(约束成立) = log Σ_{a 属于合法赋值集合} P(a)

而单个赋值 a 的概率由各个输出位独立 sigmoid 给出。

5.7 satisfaction_probability

5.8 semantic_loss

- mean(log P(约束成立))

5.9 hard_constraint_satisfaction

这部分不参与训练,只用于评估“硬满足率”。

5.10 serialize_constraint_spec

这个文件的真正核心思想是:

语义知识不是一句口号,而是一个合法赋值集合;semantic loss 就是在问模型当前到底给这个集合分了多少概率质量。


6. model.py

这个文件很短,但它的设计选择很重要。

6.1 导入

6.2 TinySemanticMLP

这里最重要的不是“网络有多复杂”,而是:

输出层给的是 4 个独立 logits,而不是 softmax 概率。

这件事非常关键,因为 semantic loss 要做的是对所有二值赋值建模。 如果直接用 softmax,one-hot 结构会被模型结构本身部分硬编码掉,演示效果就没那么干净。


7. trainer.py

这是训练、评估、画图的主战场。

7.1 导入部分

7.2 ManualAdamW

这部分是手写的 AdamW 优化器。

初始化

zero_grad

step

对你理解这个 toy 来说,这段不是 semantic loss 的重点,但它承担了一个工程角色:

让这个项目不依赖外部优化器实现细节,自己就能完成稳定训练。

7.3 小工具函数

_to_device

_one_hot_targets

semantic_weight_at

这意味着:

7.4 evaluate

这个函数负责统一评估。

这里有一个很重要的理解点:

在这个 toy 里,accuracy 和 constraint satisfaction 是两条不同的轴。

也就是说:

7.5 _make_optimizer

7.6 train_baseline

这是纯监督版本。

这个 baseline 很重要,因为它让你能回答一个干净的问题:

不加语义知识时,单靠少量标注,模型能学到什么程度?

7.7 train_semantic_guided

这是整份 toy 里最重要的训练函数。

total_loss = supervised_term + lambda_t * semantic_term

这整个函数最值得你记住的是:

semantic loss 并不是替代监督项,而是在无标签数据上补一条“输出结构应该像什么样”的训练信号。

7.8 save_metrics

7.9 plot_training_curves

左图:训练损失

右图:验证指标

7.10 plot_decision_and_constraint_maps

这个函数把“分类区域”和“约束满足概率区域”画在一张图里。

上排:决策区域

下排:约束满足概率

这个函数很有价值,因为它同时把两件事画出来了:

模型“把哪里判成哪一类”和“模型在什么区域更满足约束”,并不总是同一件事。


8. experiment.py

这个文件负责把前面的模块串起来。

8.1 导入

8.2 run_experiment

这里要特别注意:

这个文件的作用可以概括成:

它不定义新算法,只做一件事:把配置、数据、约束、训练、评估、可视化组织成一个完整实验。


9. 这份 toy 最值得你真正看懂的 5 个点

9.1 为什么这里不用 softmax + cross entropy

因为这里要演示的是“输出结构约束”。 如果直接用 softmax,模型天生就会把输出压成总和为 1 的分布,exactly-one 的一部分结构会被模型形式提前吸收掉。

这里故意写成:

这样你才能清楚看到 semantic loss 真正在补什么。

9.2 这个 toy 的 semantic loss 本质上在算什么?

它不是算“哪个类最大”。 它算的是:

当前模型输出分布,在所有合法二值赋值上的总概率质量

如果这个总质量大,说明模型更相信合法结构。 如果这个总质量小,说明模型把概率分到了很多不合法结构上。

9.3 为什么训练时 semantic loss 只用 unlabeled 样本?

因为这正是它最有代表性的使用方式:

这能最清楚地体现:

就算没有标签,只要你知道输出该长什么样,也能给模型额外训练信息。

9.4 为什么还要算 hard_constraint_satisfaction_rate

因为 mean_satisfaction_probability 是软指标。 它告诉你“平均来看合法概率多不多”。

但有时你还想知道:

真要把每一位硬阈值化,最终有多少输出真的落在合法集合里?

这就是硬满足率的作用。

9.5 这份 toy 和 logic_net_toy 最本质的区别是什么?

logic_net_toy

semantic_loss_toy

也就是说,这个 toy 更接近“语义直接进 loss”的思路。


10. 建议你接下来怎么读这份代码

如果你是第一次看,建议顺序是:

  1. 先看 run.py
    • 先知道脚本怎么启动
  2. 再看 experiment.py
    • 先抓住总体流程
  3. 再看 constraints.py
    • 这是论文思想真正落地的地方
  4. 再看 trainer.py
    • 看 semantic loss 是怎样接进训练循环的
  5. 最后再看 data.pymodel.py
    • 理解 toy 任务本身和模型结构

如果你是准备改代码,最值得先动的地方是:

这份解析读完后,你应该至少能自己说清楚三句话:

  1. 这个 toy 为什么用独立 sigmoid 而不是 softmax。
  2. log_satisfaction_mass 那一行广播到底在算什么。
  3. semantic loss 为什么能在无标签样本上提供训练信号。