mirror of
https://github.com/unanmed/ginka-generator.git
synced 2026-05-14 04:41:12 +08:00
249 lines
9.4 KiB
Markdown
249 lines
9.4 KiB
Markdown
# 数据集标签设计文档
|
||
|
||
## 背景
|
||
|
||
在当前 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**(避免把单行/单列走廊计入房间)。
|
||
|
||
> **为什么这样定义房间**
|
||
>
|
||
> 在拓扑图中,空白/资源节点天然是游戏空间的"腔体",而分支节点(门/怪物)是进入腔体的关卡节点。只要与至少一个分支节点相邻,就说明这片空间是需要"先过关才能进入/离开"的区域——包括怪物守着宝箱这类单入口房间,同样是典型的房间结构。面积和形状约束则过滤掉通道和死胡同。
|
||
|
||
### 等级划分
|
||
|
||
等级为 **三档**:Low(0)/ Medium(1)/ High(2),通过以下两趟扫描确定:
|
||
|
||
1. **第一趟**:遍历整个训练集,计算每张地图的房间数量 `roomCount`,收集为数组;
|
||
2. **第二趟**:对数组升序排序,取 1/3 和 2/3 分位数作为阈值 `[th1, th2]`:
|
||
- `roomCount < th1` → Low(0)
|
||
- `th1 ≤ roomCount < th2` → Medium(1)
|
||
- `roomCount ≥ th2` → High(2)
|
||
|
||
等级划分力求三档样本数量均等(等频分箱),而非等距分箱。
|
||
|
||
### 外接矩形计算
|
||
|
||
给定节点的所有格子坐标集合(`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` 即等于非墙邻居数,两者在实现上等价。
|
||
|
||
这类节点是地图中的"交叉口"——一个门或怪物后方至少有三条不同路线,是地图分叉度和策略深度的指征。
|
||
|
||
等级划分方式与房间数量等级相同:先统计每张地图中高连接度分支节点的数量,再等频分箱为 Low(0)/ Medium(1)/ High(2)。
|
||
|
||
### 与房间数量的区别
|
||
|
||
| 维度 | 房间数量等级 | 分支数量等级 |
|
||
| -------- | -------------------------- | ---------------------------- |
|
||
| 度量对象 | 空白/资源节点区域的封闭性 | 分支节点的路径分叉度 |
|
||
| 反映特征 | 地图内封闭房间的数量与密度 | 关键路口/多分支门怪的复杂度 |
|
||
| 典型高值 | 多房间迷宫风格地图 | 高度分叉、策略选择丰富的地图 |
|
||
|
||
### 字段扩展
|
||
|
||
```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 注入**。
|