ginka-generator/docs/dataset-labels-design.md
unanmed abbad781ab feat: 添加少数结构性标签
Co-authored-by: Copilot <copilot@github.com>
2026-04-27 14:56:21 +08:00

9.4 KiB
Raw Permalink Blame History

数据集标签设计文档

背景

在当前 VQ-VAE + MaskGIT 的联合训练方案中VQ-VAE 的 codebook 承担着地图风格与多样性的控制职能,但缺少可用户感知的语义维度。为提升生成的可控性,计划在数据集中添加一组可程序化标注的结构标签,作为额外条件输入训练,从而使模型能够接受来自用户的高层语义约束(如"生成一个左右对称的地图"、"高房间数量"等)。

所有标签均可在数据集构建阶段(info.ts / parseFloorInfo)或训练前的两趟扫描中自动计算,无需人工标注。


标签一览

标签名 类型 取值 标注时机
symmetryH boolean 左右对称 单张地图
symmetryV boolean 上下对称 单张地图
symmetryC boolean 中心对称 单张地图
roomCountLevel 0|1|2 Low / Medium / High 两趟扫描
branchLevel 0|1|2 Low / Medium / High 两趟扫描
outerWall boolean 是否外包围墙壁 单张地图

标签一:对称性

定义

针对经过转换后的地图(convertedMap)矩阵逐格比较,仅当完全满足条件时才标记为对应对称类型。三种对称相互独立,可同时成立。

对称类型 条件(对所有 x ∈ [0, W), y ∈ [0, H) 成立)
左右对称 map[y][x] === map[y][W - 1 - x]
上下对称 map[y][x] === map[H - 1 - y][x]
中心对称 map[y][x] === map[H - 1 - y][W - 1 - x]

实现要点

  • 比较使用 convertedMap(标签化图块编号),而非原始 originMap,使不同塔的同类图块具有可比性。
  • 中心对称与左右、上下对称在数学上不蕴含关系(非充分也非必要),需独立计算。
  • 对于奇数尺寸的地图(如 13×13中心行/列与自身比较必然成立,无需特殊处理。

字段扩展(IFloorInfo

/** 左右对称 */
readonly symmetryH: boolean;
/** 上下对称 */
readonly symmetryV: boolean;
/** 中心对称 */
readonly symmetryC: boolean;

标签二:房间数量等级

定义

房间Room 是地图中由"空白节点"或"资源节点"组成的区域,同时满足以下三个条件:

  1. 位置条件:该节点在拓扑图中至少与 1 个分支节点Branch 相邻;
  2. 面积条件:节点所包含的地图格子数(tiles.size≥ 4
  3. 形状条件:节点所有格子的外接矩形的宽和高都大于 1(避免把单行/单列走廊计入房间)。

为什么这样定义房间

在拓扑图中,空白/资源节点天然是游戏空间的"腔体",而分支节点(门/怪物)是进入腔体的关卡节点。只要与至少一个分支节点相邻,就说明这片空间是需要"先过关才能进入/离开"的区域——包括怪物守着宝箱这类单入口房间,同样是典型的房间结构。面积和形状约束则过滤掉通道和死胡同。

等级划分

等级为 三档Low0/ Medium1/ High2通过以下两趟扫描确定

  1. 第一趟:遍历整个训练集,计算每张地图的房间数量 roomCount,收集为数组;
  2. 第二趟:对数组升序排序,取 1/3 和 2/3 分位数作为阈值 [th1, th2]
    • roomCount < th1 → Low0
    • th1 ≤ roomCount < th2 → Medium1
    • roomCount ≥ th2 → High2

等级划分力求三档样本数量均等(等频分箱),而非等距分箱。

外接矩形计算

给定节点的所有格子坐标集合(tiles,存储 y * width + x 的平坦坐标),还原为 (x, y) 后:


x_{\min} = \min_{t \in tiles}(t \bmod W), \quad x_{\max} = \max_{t \in tiles}(t \bmod W)

y_{\min} = \min_{t \in tiles}(\lfloor t / W \rfloor), \quad y_{\max} = \max_{t \in tiles}(\lfloor t / W \rfloor)

外接矩形宽 = $x_{\max} - x_{\min} + 1$,高 = $y_{\max} - y_{\min} + 1$,两者均需 $> 1$。

字段扩展

/** 房间数量(原始统计值,供两趟扫描使用) */
readonly roomCount: number;
/** 房间数量等级0=Low, 1=Medium, 2=High需两趟扫描后赋值 */
roomCountLevel: 0 | 1 | 2;

标签三:分支数量等级

定义

高连接度分支节点:拓扑图中 type === Branch 的节点,其非墙邻居节点总数(neighbors.size≥ 3。由于当前拓扑图中已不含墙节点,neighbors.size 即等于非墙邻居数,两者在实现上等价。

这类节点是地图中的"交叉口"——一个门或怪物后方至少有三条不同路线,是地图分叉度和策略深度的指征。

等级划分方式与房间数量等级相同:先统计每张地图中高连接度分支节点的数量,再等频分箱为 Low0/ Medium1/ High2

与房间数量的区别

维度 房间数量等级 分支数量等级
度量对象 空白/资源节点区域的封闭性 分支节点的路径分叉度
反映特征 地图内封闭房间的数量与密度 关键路口/多分支门怪的复杂度
典型高值 多房间迷宫风格地图 高度分叉、策略选择丰富的地图

字段扩展

/** 高连接度分支节点数量(原始统计值) */
readonly highDegBranchCount: number;
/** 分支数量等级0=Low, 1=Medium, 2=High需两趟扫描后赋值 */
branchLevel: 0 | 1 | 2;

标签四:外包围墙壁

定义

地图最外圈(最外一圈格子)的格子中,墙壁格子与入口格子之和占外圈总格子数的比例 > 90%,则标记为 outerWall = true


\text{outerWall} = \frac{|\{(x,y) \in \text{border} : \text{isWall}(x,y) \lor \text{isEntry}(x,y)\}|}{|\text{border}|} > 0.9

最外圈定义

对于 H \times W 的地图,最外圈为所有满足下列条件之一的格子:


x = 0 \; \lor \; x = W - 1 \; \lor \; y = 0 \; \lor \; y = H - 1

最外圈格子总数为 $2(H + W) - 4$(对于 13×13共 48 格)。

为什么入口也算"通过"

入口格子在游戏中是楼梯/传送点,不属于可通行的空地,在视觉和结构上等价于边界开口,属于外圈围合结构的合理组成部分,不应被视为"破坏围合"的元素。

实现要点

  • 使用 convertedMap 判断墙壁(tile === config.wall
  • 使用 originMap + converter.isEntry() 或直接对 convertedMap 判断入口(tile === config.entry)判断入口;
  • 两项合取计入分子。

字段扩展

/** 是否外包围墙壁 */
readonly outerWall: boolean;

实现方案

单张地图可直接计算的标签

以下标签可在 parseFloorInfo 中直接计算,加入 IFloorInfo

  • symmetryHsymmetryVsymmetryC
  • outerWall
  • roomCount(原始值)
  • highDegBranchCount(原始值)

需要两趟扫描的等级标签

roomCountLevelbranchLevel 依赖全局分位数,须在数据集构建完成后进行二次处理:

第一趟:构建所有楼层的 IFloorInfo写入 roomCount / highDegBranchCount
第二趟:收集所有楼层的原始值 → 计算 1/3, 2/3 分位 → 回填等级

在 Python 训练侧,推荐方式:

# 在 Dataset.__init__ 中完成两趟计算
counts = [item['roomCount'] for item in raw_data]
counts_sorted = sorted(counts)
th1 = counts_sorted[len(counts_sorted) // 3]
th2 = counts_sorted[2 * len(counts_sorted) // 3]

for item in raw_data:
    c = item['roomCount']
    item['roomCountLevel'] = 0 if c < th1 else (1 if c < th2 else 2)

同理处理 branchLevel

注意:分位数阈值应仅基于训练集统计,验证集 / 测试集使用相同的阈值映射,避免数据泄露。


训练集成

条件嵌入

将上述标签作为离散条件与 VQ-VAE 的 z 一同注入 MaskGIT

# 对称性:三个独立布尔值,可合并为 0~7 的整数 cond_sym
cond_sym = symmetryH * 4 + symmetryV * 2 + symmetryC * 1  # [0, 7]

# 房间等级0 / 1 / 2
cond_room = roomCountLevel

# 分支等级0 / 1 / 2
cond_branch = branchLevel

# 外包围墙壁0 / 1
cond_outer = int(outerWall)

每个条件通过独立的 nn.Embedding 映射为固定维度向量,与 VQ-VAE 的 z 序列沿序列维度拼接后,一同经 Cross-Attention 注入 MaskGIT。

条件 Dropout

与 z dropout 类似,训练时以一定概率(如 10~20%)将部分或全部结构标签替换为"无条件"null embedding使模型在推理时支持条件缺省CFG 风格)。


待细化事项

  • 90% 阈值是否合适?——保持 90%,如后续数据分布分析发现问题再调整。
  • 房间定义中分支节点邻居数量——改为至少 1 个,覆盖怪物守宝箱的单入口房间场景。
  • 分支等级邻居计数口径——使用非墙邻居(当前图结构中无墙节点,与 neighbors.size 等价)。
  • 是否新增通道数量/路径长度标签——暂不考虑
  • 条件嵌入维度对齐——各标签 Embedding 与 z 序列拼接后统一经 Cross-Attention 注入