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

249 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 数据集标签设计文档
## 背景
在当前 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`
```typescript
/** 左右对称 */
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$。
### 字段扩展
```typescript
/** 房间数量(原始统计值,供两趟扫描使用) */
readonly roomCount: number;
/** 房间数量等级0=Low, 1=Medium, 2=High需两趟扫描后赋值 */
roomCountLevel: 0 | 1 | 2;
```
---
## 标签三:分支数量等级
### 定义
**高连接度分支节点**:拓扑图中 `type === Branch` 的节点,其**非墙邻居节点**总数(`neighbors.size`**≥ 3**。由于当前拓扑图中已不含墙节点,`neighbors.size` 即等于非墙邻居数,两者在实现上等价。
这类节点是地图中的"交叉口"——一个门或怪物后方至少有三条不同路线,是地图分叉度和策略深度的指征。
等级划分方式与房间数量等级相同:先统计每张地图中高连接度分支节点的数量,再等频分箱为 Low0/ Medium1/ High2
### 与房间数量的区别
| 维度 | 房间数量等级 | 分支数量等级 |
| -------- | -------------------------- | ---------------------------- |
| 度量对象 | 空白/资源节点区域的封闭性 | 分支节点的路径分叉度 |
| 反映特征 | 地图内封闭房间的数量与密度 | 关键路口/多分支门怪的复杂度 |
| 典型高值 | 多房间迷宫风格地图 | 高度分叉、策略选择丰富的地图 |
### 字段扩展
```typescript
/** 高连接度分支节点数量(原始统计值) */
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`)判断入口;
- 两项合取计入分子。
### 字段扩展
```typescript
/** 是否外包围墙壁 */
readonly outerWall: boolean;
```
---
## 实现方案
### 单张地图可直接计算的标签
以下标签可在 `parseFloorInfo` 中直接计算,加入 `IFloorInfo`
- `symmetryH`、`symmetryV`、`symmetryC`
- `outerWall`
- `roomCount`(原始值)
- `highDegBranchCount`(原始值)
### 需要两趟扫描的等级标签
`roomCountLevel``branchLevel` 依赖全局分位数,须在数据集构建完成后进行二次处理:
```
第一趟:构建所有楼层的 IFloorInfo写入 roomCount / highDegBranchCount
第二趟:收集所有楼层的原始值 → 计算 1/3, 2/3 分位 → 回填等级
```
在 Python 训练侧,推荐方式:
```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
```python
# 对称性:三个独立布尔值,可合并为 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 风格)。
---
## 待细化事项
- [x] 90% 阈值是否合适?——**保持 90%**,如后续数据分布分析发现问题再调整。
- [x] 房间定义中分支节点邻居数量——**改为至少 1 个**,覆盖怪物守宝箱的单入口房间场景。
- [x] 分支等级邻居计数口径——**使用非墙邻居**(当前图结构中无墙节点,与 `neighbors.size` 等价)。
- [x] 是否新增通道数量/路径长度标签——**暂不考虑**。
- [x] 条件嵌入维度对齐——**各标签 Embedding 与 z 序列拼接后统一经 Cross-Attention 注入**。