# 作业九：BRep 与图神经网络（GNN）——CAD 几何体分类

**发布日期**：2026 年 5 月 24 日

**截止日期**：2026 年 5 月 31 日 23:59（CST）

---

## 一、作业背景

经过九周的学习，我们沿着几何深度学习（GDL）的主线，完成了从基础架构到 3D 几何表示的完整旅程：

| 周 | 作业 | 数据域 | 架构 | 核心对称性 |
|----|------|--------|------|-----------|
| 1-2 | 1-2 | 无结构（向量） | MLP | 无 |
| 3 | 3 | 规则网格（图像） | CNN | 平移等变 |
| 4 | 4 | 序列 | Transformer | 置换等变 + 位置编码 |
| 5 | 5 | 图 | GCN | 置换等变 |
| 6 | 6 | 点云 | PointNet | 置换不变（max pool） |
| 7 | 7 | 体素 | 3D CNN | 3D 平移等变 |
| 8 | 8 | SDF（隐式） | DeepSDF | 连续函数表示 |
| **9** | **9** | **BRep（参数化）** | **GNN** | **置换等变 + 拓扑结构** |

本次作业是课程的**收官之作**——我们回到图神经网络（A5），但将其应用于一个全新的场景：**CAD 中的边界表示（BRep, Boundary Representation）**。

### 什么是 BRep？

**边界表示（BRep）** 是现代 CAD/CAM 系统中最主流的 3D 形状表示方式。它通过描述实体的**边界**来定义形状：

- **面（Face）**：实体的表面片段，每个面有一个几何曲面类型（平面、柱面、球面、锥面、环面等）
- **边（Edge）**：两个面的交线，有几何曲线类型（直线、圆弧等）
- **顶点（Vertex）**：边的端点

BRep 的核心优势在于它精确保留了 CAD 设计意图中的**拓扑关系**和**几何信息**——这些信息在点云、体素、SDF 等表示中被丢弃。

### BRep → 图：天然的映射

BRep 的拓扑结构天然对应一个图：

$$\text{BRep} \to \text{Graph}: \quad \text{面} \to \text{节点}, \quad \text{共享边} \to \text{图边}$$

- **节点**（面）的特征：面类型、面积、曲率统计量
- **边**（面-面连接）的特征：共享边类型、二面角、凸凹性

这使得 GNN 成为处理 BRep 数据的天然选择。

### 五种 CAD 基本体的 BRep 拓扑

| 形状 | 面数 | 边数 | 顶点数 | 面类型 |
|------|------|------|--------|--------|
| Cube（长方体） | 6 | 12 | 8 | 6 planar |
| Cylinder（圆柱体） | 3 | 2 | 0 | 2 planar + 1 cylindrical |
| Sphere（球体） | 1 | 0 | 0 | 1 spherical |
| Cone（圆锥体） | 2 | 1 | 1 | 1 planar + 1 conical |
| Torus（环面体） | 1 | 0 | 0 | 1 toroidal |

观察这个表——每种基本体的 BRep 拓扑结构完全不同。Cube 是一个 6 节点 12 边的图；Cylinder 是 3 节点 2 边的路径图；Sphere 和 Torus 退化为单节点无边图。这种拓扑差异正是 GNN 可以利用的分类信号。

### UV-Net：直接在 BRep 上学习

上面的"BRep → 面邻接图"映射虽然自然，但丢失了大量信息——我们只保留了面类型、面积、曲率等标量，而每个面的**精确几何形状**被简化成了一个特征向量。

2021 年，Autodesk Research 的 Jayaraman 等人在 CVPR 上发表了 **UV-Net**，提出了一种直接在 BRep 上学习的方法。其核心 insight 是利用参数曲面/曲线自身的 **UV 参数域**来表示几何：

- **面的 UV-Grid**：每个 BRep 面有一个参数曲面 $\mathbf{S}(u, v)$。在参数域上均匀采样 $10 \times 10$ 个点，每个点附带 7 个通道（3D 坐标 + 法向量 + trimming mask），形成一个 $(10 \times 10 \times 7)$ 的"图像"，用 **2D CNN** 处理。
- **边的 UV-Grid**：每条边有一条参数曲线 $\mathbf{C}(u)$，均匀采样 10 个点（坐标 + 切向量），形成 $(10 \times 6)$ 的序列，用 **1D CNN** 处理。
- CNN 输出的面/边嵌入作为面邻接图的节点/边特征，再通过 **GNN（扩展的 GIN）** 进行消息传递。

UV-Net 同时发布了 **SolidLetters** 数据集：约 96,000 个 3D CAD 实体，由 2002 种字体的 26 个字母经拉伸+倒角生成。与我们的 5 类基本体不同，SolidLetters 具有**类内的几何和拓扑双重变异**——同一个字母"a"，不同字体的面数、面类型、拓扑结构可能完全不同，这使得分类远不是"看一眼拓扑就能解决"的简单问题。

> **参考文献**：Jayaraman, P.K., et al. "UV-Net: Learning from Boundary Representations." CVPR 2021.

### 关键新概念：图级别分类

作业五（Cora 节点分类）中，我们在**一张大图**上做**节点级别**的分类。本次作业截然不同——我们对**许多小图**做**图级别**的分类：

| | 作业五（Cora） | 作业九（BRep） |
|---|---|---|
| 图的数量 | 1 张 | 1000（合成）/ ~96k（SolidLetters） |
| 节点数 | 2708 | 1~6（合成）/ 平均 ~33（SolidLetters） |
| 任务 | 节点分类 | 图分类 |
| 输出 | 每个节点一个标签 | 每张图一个标签 |
| 新机制 | — | **Global Pooling** |

图级别任务需要一个额外的步骤：**全局池化（Global Pooling）**——将不定数量的节点嵌入聚合为一个固定长度的图嵌入。这与作业六 PointNet 的 max pooling 有异曲同工之妙。

---

## 二、环境准备

### 安装依赖

```bash
pip install torch torchvision
pip install torch_geometric
pip install dgl                 # 任务三需要，用于加载 SolidLetters
pip install matplotlib scikit-learn networkx numpy tqdm
```

> **提示**：任务一/二的数据在代码中程序化生成，纯 CPU 数秒即可完成训练。任务三需要下载 SolidLetters 数据集（约 2GB），训练建议使用 GPU。

---

## 三、作业内容

### 任务一：构建 BRep 图数据集（`brep_gnn.py` 第一部分）

编写函数，为 5 类 CAD 基本体程序化生成 BRep 图数据。每个形状实例是一个 PyG `Data` 对象。

#### 具体步骤

1. **定义节点特征**（8 维）：

   | 维度 | 特征 | 说明 |
   |------|------|------|
   | 0-4 | 面类型 one-hot | `[planar, cylindrical, spherical, conical, toroidal]` |
   | 5 | 面积 | 由形状参数解析计算 |
   | 6 | 平均曲率 | $H = \frac{\kappa_1 + \kappa_2}{2}$，平面为 0 |
   | 7 | 高斯曲率 | $K = \kappa_1 \cdot \kappa_2$，柱面/锥面为 0 |

2. **定义边拓扑**：根据 BRep 面邻接关系构建 `edge_index`。例如，Cube 的 6 个面中，每个面与 4 个相邻面共享一条几何边，对面不相连。

3. **定义边特征**（4 维，可选）：边类型 one-hot `[line, circle]` + 二面角 + 凸凹性。

4. **为每类生成 200 个实例**：通过随机化形状参数（长宽高、半径等）和添加高斯噪声，产生同一类别内的多样性。

5. **划分数据集**：70% 训练 / 15% 验证 / 15% 测试。

#### 代码框架（供参考）

```python
import torch
import numpy as np
from torch_geometric.data import Data
from torch_geometric.loader import DataLoader

torch.manual_seed(42)
np.random.seed(42)

NOISE_STD = 0.1
NUM_SAMPLES_PER_CLASS = 200
FACE_TYPES = ["planar", "cylindrical", "spherical", "conical", "toroidal"]

# ======================== 辅助函数 ========================

def _make_node_features(face_type_indices, areas, mean_curvs, gauss_curvs, noise_std):
    """从面属性构造节点特征矩阵 [num_faces, 8]"""
    n = len(face_type_indices)
    face_type_onehot = torch.zeros(n, 5)
    for i, idx in enumerate(face_type_indices):
        face_type_onehot[i, idx] = 1.0

    areas_t = torch.tensor(areas, dtype=torch.float32).unsqueeze(1)
    mean_t = torch.tensor(mean_curvs, dtype=torch.float32).unsqueeze(1)
    gauss_t = torch.tensor(gauss_curvs, dtype=torch.float32).unsqueeze(1)

    x = torch.cat([face_type_onehot, areas_t, mean_t, gauss_t], dim=1)
    x[:, 5:] += torch.randn(n, 3) * noise_std  # 对连续特征加噪声
    return x


def _make_edge_index(edges):
    """从 (src, dst) 对列表构建无向 edge_index [2, 2*E]"""
    if not edges:
        return torch.zeros(2, 0, dtype=torch.long)
    src = [e[0] for e in edges] + [e[1] for e in edges]
    dst = [e[1] for e in edges] + [e[0] for e in edges]
    return torch.tensor([src, dst], dtype=torch.long)

# ======================== Cube 生成（示例） ========================

def generate_cube(noise_std=NOISE_STD):
    a, b, c = [np.random.uniform(0.5, 2.0) for _ in range(3)]

    face_type_indices = [0] * 6  # 6 个平面
    areas = [a*b, a*b, b*c, b*c, a*c, a*c]
    mean_curvs = [0.0] * 6
    gauss_curvs = [0.0] * 6

    # 面邻接：top/bottom 各连 front/back/left/right，对面不相连
    edges = [
        (0,2),(0,3),(0,4),(0,5), (1,2),(1,3),(1,4),(1,5),
        (2,4),(2,5),(3,4),(3,5),
    ]

    x = _make_node_features(face_type_indices, areas, mean_curvs, gauss_curvs, noise_std)
    edge_index = _make_edge_index(edges)
    return Data(x=x, edge_index=edge_index, y=torch.tensor(0, dtype=torch.long))

# ======================== TODO: 实现其余 4 个形状 ========================

def generate_cylinder(noise_std=NOISE_STD):
    # TODO: 3 faces (2 planar caps + 1 cylindrical), 2 edges
    # 参数：r = uniform(0.3, 1.5), h = uniform(0.5, 3.0)
    # 面积：cap = π*r², lateral = 2π*r*h
    # 曲率：planar → 0, cylindrical → mean_curv = 1/(2r), gauss_curv = 0
    # 邻接：cap0 ↔ lateral, cap1 ↔ lateral
    ...

def generate_sphere(noise_std=NOISE_STD):
    # TODO: 1 face (spherical), 0 edges — 单节点图
    # 参数：r = uniform(0.3, 2.0)
    # 面积：4π*r²
    # 曲率：mean_curv = 1/r, gauss_curv = 1/r²
    ...

def generate_cone(noise_std=NOISE_STD):
    # TODO: 2 faces (1 planar base + 1 conical lateral), 1 edge
    # 参数：r = uniform(0.3, 1.5), h = uniform(0.5, 3.0)
    # slant = sqrt(r² + h²), half_angle = arctan(r/h)
    # 面积：base = π*r², lateral = π*r*slant
    # 邻接：base ↔ lateral
    ...

def generate_torus(noise_std=NOISE_STD):
    # TODO: 1 face (toroidal), 0 edges — 单节点图
    # 参数：R = uniform(1.0, 3.0), r = uniform(0.2, min(0.8, R*0.4))
    # 面积：4π²*R*r
    # 曲率：mean_curv = (R+r)/(2Rr), gauss_curv = 1/(Rr)
    ...

# ======================== 生成数据集并划分 ========================

GENERATORS = [generate_cube, generate_cylinder, generate_sphere, generate_cone, generate_torus]

all_data = []
for gen in GENERATORS:
    for _ in range(NUM_SAMPLES_PER_CLASS):
        all_data.append(gen())
np.random.shuffle(all_data)

n = len(all_data)
n_train = int(0.7 * n)
n_val = int(0.15 * n)

train_data = all_data[:n_train]
val_data = all_data[n_train:n_train+n_val]
test_data = all_data[n_train+n_val:]

train_loader = DataLoader(train_data, batch_size=32, shuffle=True)
val_loader = DataLoader(val_data, batch_size=32)
test_loader = DataLoader(test_data, batch_size=32)
```

---

### 任务二：GNN 图分类模型（`brep_gnn.py` 第二部分）

使用 PyTorch Geometric 的 GNN 层搭建图分类模型，对 BRep 图进行 5 类分类。

#### 模型架构

```
Input [num_nodes, 8]
  → Node Encoder: Linear(8, 64) + BN + ReLU
  → GIN Layer ×3: GINConv(MLP) + BN + ReLU
  → Global Pooling: mean_pool ‖ max_pool → [batch_size, 128]
  → Classifier: Linear(128, 64) + ReLU + Dropout + Linear(64, 5)
```

**关键选择**：

- **GIN（Graph Isomorphism Network）** 而非 GCN：回顾作业五 Q7，GIN 使用 `sum` 聚合 + MLP，理论上与 1-WL 同构测试等价强度，是最具表达力的消息传递 GNN
- **Global Mean+Max Pooling**：拼接均值和最大值池化，兼顾"平均信号"和"极端特征"
- **Node Encoder**：将原始 8 维特征映射到隐藏空间，同时用 BatchNorm 归一化不同尺度的特征

#### PyG 的图批处理机制

与作业五（单图 transductive）不同，图分类任务需要在**多个大小不同的图**上做 mini-batch 训练。PyG 的 `DataLoader` 通过以下方式批处理：

1. 将 batch 内所有图的节点**拼接**成一个大的不连通图
2. 自动调整 `edge_index` 的偏移量
3. 生成 `batch` 向量：`batch[i] = k` 表示节点 $i$ 属于 batch 中的第 $k$ 个图

```
Graph 0: 6 nodes    Graph 1: 3 nodes    Graph 2: 1 node
    ↓ 拼接后 ↓
[0,0,0,0,0,0, 1,1,1, 2]  ← batch 向量
```

`global_mean_pool(x, batch)` 对每个图的节点嵌入取均值，输出 `[batch_size, hidden_dim]`。

#### 代码框架（供参考）

```python
from torch_geometric.nn import GINConv, global_mean_pool, global_max_pool

class BRepGNN(torch.nn.Module):
    def __init__(self, in_channels, hidden_channels, out_channels,
                 num_layers=3, dropout=0.3):
        super().__init__()
        # TODO: 实现 node_encoder (Linear + BN + ReLU)
        # TODO: 实现 num_layers 个 GINConv 层（每层包含一个 2 层 MLP）
        # TODO: 实现 classifier (Linear + ReLU + Dropout + Linear)
        # Hint: GINConv(nn) 需要传入一个 nn.Module 作为内部 MLP
        #       mlp = nn.Sequential(Linear(h, h), ReLU(), Linear(h, h))
        #       self.convs.append(GINConv(mlp))
        ...

    def forward(self, x, edge_index, batch):
        # TODO: node_encoder
        # TODO: GIN layers with BN + ReLU
        # TODO: global pooling (mean + max, 拼接)
        # TODO: classifier
        # Hint: x_mean = global_mean_pool(x, batch)
        #       x_max = global_max_pool(x, batch)
        #       x = torch.cat([x_mean, x_max], dim=1)
        ...

# ======================== 训练循环 ========================

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = BRepGNN(8, 64, 5, num_layers=3, dropout=0.3).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=1e-4)

def train_epoch():
    model.train()
    total_loss, correct, total = 0, 0, 0
    for batch in train_loader:
        batch = batch.to(device)
        optimizer.zero_grad()
        out = model(batch.x, batch.edge_index, batch.batch)
        loss = F.cross_entropy(out, batch.y)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * batch.num_graphs
        correct += (out.argmax(dim=1) == batch.y).sum().item()
        total += batch.num_graphs
    return total_loss / total, correct / total

@torch.no_grad()
def evaluate(loader):
    model.eval()
    correct, total = 0, 0
    for batch in loader:
        batch = batch.to(device)
        out = model(batch.x, batch.edge_index, batch.batch)
        correct += (out.argmax(dim=1) == batch.y).sum().item()
        total += batch.num_graphs
    return correct / total

# TODO: 训练 80 个 epoch，记录 loss / accuracy
# TODO: 绘制训练曲线并保存为 brep_gnn_curves.png
# TODO: 计算混淆矩阵并保存为 brep_confusion.png
```

#### 参考超参数

| 参数 | 建议值 | 说明 |
|------|--------|------|
| `hidden_dim` | 64 | GNN 隐藏层维度 |
| `num_layers` | 3 | GIN 层数 |
| `learning_rate` | 0.01 | 学习率 |
| `weight_decay` | 1e-4 | L2 正则化 |
| `dropout` | 0.3 | 分类头的 Dropout |
| `epochs` | 80 | 训练轮数 |
| `batch_size` | 32 | 图批次大小 |

---

### 任务三：SolidLetters 真实数据集上的 BRep 图分类（`solidletters_gnn.py`）

任务一/二中的 5 类基本体过于简单——拓扑结构差异显著（6 面 vs 3 面 vs 1 面），GNN 几乎不需要学习就能轻松分类。现在，我们挑战一个真实的 BRep 数据集。

#### 数据集：SolidLetters（UV-Net, CVPR 2021）

| 属性 | 内容 |
|------|------|
| 规模 | ~96,000 个 3D CAD 实体 |
| 类别 | **26 类**（字母 a-z） |
| 来源 | 2002 种字体的字母经拉伸 (extrude) + 倒角 (fillet) 生成 |
| 平均面数 | ~33 个面/模型 |
| 格式 | DGL `.bin` 图文件（面邻接图 + UV-grid 节点/边特征） |
| 下载 | https://uv-net-data.s3.us-west-2.amazonaws.com/SolidLetters.zip |

**为什么 SolidLetters 有挑战性？**

- 同一字母（如"a"），不同字体的面数从十几个到上百个不等
- 类内拓扑差异极大：不同字体的"a"可能有完全不同的面邻接图
- 不同字母可能有相似的拓扑（如"o"和"c"只差一个缺口）
- 拉伸方向随机、是否有倒角也随机——不能仅靠拓扑"作弊"

#### 数据准备

1. 下载 `SolidLetters.zip` 并解压
2. 解压其中的 `graph.7z`，得到 `graph/` 文件夹（含 `train/` 和 `test/` 子目录）
3. 安装 DGL（用于加载 `.bin` 图文件）：`pip install dgl`

#### 从 UV-Grid 提取节点特征

UV-Net 原始数据中，每个面存储了 $(10 \times 10 \times 7)$ 的 UV-Grid 特征。为了适配我们的 GIN 模型，需要将 UV-Grid **池化为固定长度的节点特征向量**：

| 来源 | 池化方式 | 维度 | 含义 |
|------|---------|------|------|
| 3D 坐标 $(10 \times 10 \times 3)$ | 空间均值 | 3 | 面的质心位置 |
| 3D 坐标 | 空间标准差 | 3 | 面的空间展布（大小/形状） |
| 法向量 $(10 \times 10 \times 3)$ | 均值 | 3 | 面的主法向方向 |
| 法向量 | 标准差 | 3 | 面的弯曲程度（曲率代理） |
| Trimming mask $(10 \times 10 \times 1)$ | 均值 | 1 | 可见区域占比 |
| **合计** | | **13** | |

这个池化过程相当于对 UV-Net 的 2D CNN 做了一个极端简化——用统计量代替了学习到的卷积特征。虽然损失了空间局部信息，但保留了全局几何统计。

#### 具体步骤

1. **加载并转换数据**：从 DGL `.bin` 文件加载面邻接图，将 UV-Grid 池化为 13 维节点特征，转换为 PyG `Data` 对象。首次转换后缓存为 `.pt` 文件。

2. **复用 GIN 模型**：将任务二的 `BRepGNN` 模型适配到 26 类分类（修改 `in_channels=13, out_channels=26`），可适当增大隐藏维度和层数。

3. **训练并评估**：在 ~77k 训练样本上训练，在 ~19k 测试样本上评估。

4. **分析结果**：绘制训练曲线和混淆矩阵。观察哪些字母容易混淆（如 b/d、p/q、m/n）。

#### 代码框架（供参考）

```python
import dgl
from dgl.data.utils import load_graphs
from torch_geometric.data import Data
from pathlib import Path

def pool_face_uv_grid(uv_grid):
    """将面 UV-Grid (N, 10, 10, 7) 池化为 (N, 13) 节点特征"""
    xyz = uv_grid[:, :, :, :3]
    normals = uv_grid[:, :, :, 3:6]
    mask = uv_grid[:, :, :, 6:7]
    return torch.cat([
        xyz.mean(dim=(1,2)),      # 质心 (3D)
        xyz.std(dim=(1,2)),       # 展布 (3D)
        normals.mean(dim=(1,2)),  # 主法向 (3D)
        normals.std(dim=(1,2)),   # 弯曲度 (3D)
        mask.mean(dim=(1,2)),     # 可见占比 (1D)
    ], dim=1)

def dgl_to_pyg(bin_path):
    """加载 DGL 图并转换为 PyG Data"""
    graphs, label_dict = load_graphs(str(bin_path))
    g = graphs[0]
    node_feat = pool_face_uv_grid(g.ndata["x"])
    src, dst = g.edges()
    edge_index = torch.stack([src, dst], dim=0).long()
    label = label_dict["labels"].item()
    return Data(x=node_feat, edge_index=edge_index, y=torch.tensor(label, dtype=torch.long))

# TODO: 遍历 graph/train/ 和 graph/test/ 加载所有 .bin 文件
# TODO: 转换为 PyG 数据集，缓存为 .pt 文件
# TODO: 用 DataLoader 构建 train/test loader
# TODO: 训练 BRepGNN(in_channels=13, hidden=128, out_channels=26)
```

#### 参考超参数

| 参数 | 建议值 | 说明 |
|------|--------|------|
| `hidden_dim` | 128 | 比任务二更大，适配复杂数据 |
| `num_layers` | 4 | 更深的 GNN，捕捉更大范围拓扑 |
| `learning_rate` | 0.001 | 数据量大，适当降低学习率 |
| `dropout` | 0.3 | 防止过拟合 |
| `epochs` | 100 | 更多轮次 |
| `batch_size` | 64 | 图较大，适当降低 batch |

> **提示**：如果下载完整数据集不便或硬件有限，可以通过设置 `MAX_SAMPLES` 使用一个子集（如 10,000 个样本）进行实验，但这会降低最终准确率。

---

### 任务四（可选）：BRep 图结构可视化

使用 `networkx` 可视化 5 类基本体的面邻接图，按面类型着色。保存为 `brep_graphs.png`。

提示：

```python
import networkx as nx

G = nx.Graph()
# 添加节点（按面类型着色）和边（面-面连接）
# 用 nx.draw() 绘制
```

---

## 四、具体要求

| 项目 | 要求 |
|------|------|
| 编程语言 | Python 3.8+ |
| 框架 | PyTorch + PyTorch Geometric + DGL（用于加载 SolidLetters） |
| 任务一/二 | 合成数据（5 类 × 200 = 1000 图），测试准确率 ≥ **90%** |
| 任务三 | SolidLetters（26 类，~96k 图），测试准确率 ≥ **80%** |
| 模型 | GIN-based 图分类网络 |
| 可视化 | 训练曲线 + 混淆矩阵（两个数据集各一组） |
| GPU 支持 | 代码自动检测 CUDA，兼容纯 CPU 运行 |
| 代码规范 | 结构清晰，关键步骤有注释 |

---

## 五、运行方式

**任务一/二（合成数据）：**

```bash
python brep_gnn.py
```

> 数据在内存中生成，纯 CPU 训练 80 epoch 仅需数秒。

**任务三（SolidLetters）：**

```bash
# 1. 准备数据（首次运行，会自动转换 DGL → PyG 并缓存）
# 2. 训练
python solidletters_gnn.py --data_path /path/to/solidletters
```

> 首次加载需要数分钟（转换 DGL → PyG），之后从缓存加载很快。完整训练约 10-30 分钟（GPU）或 2-3 小时（CPU）。

---

## 六、检查方法

**任务一/二：**

1. **数据生成**：每类 200 个图，总计 1000 个。打印 5 种形状的拓扑统计确认正确。
2. **训练流程**：训练正常收敛，控制台每 10 epoch 打印 loss / accuracy。
3. **准确率达标**：测试集准确率 ≥ 90%。

**任务三：**

4. **数据加载**：成功加载 SolidLetters，打印数据集统计信息（样本数、类别分布、平均面数）。
5. **训练收敛**：Loss 持续下降，准确率持续上升。
6. **准确率达标**：测试集准确率 ≥ 80%。
7. **混淆矩阵分析**：`solidletters_confusion.png` 中哪些字母对容易混淆？这些混淆是否在直觉上合理？

---

## 七、思考问题（可选）

### Q1 | 节点级 vs 图级任务：全局池化的角色

作业五中，GCN 做的是**节点级分类**——每个节点一个标签。本次作业做的是**图级分类**——每张图一个标签。

(a) 图级任务需要将变长的节点集合聚合为一个固定长度的图嵌入向量。三种常见的全局池化方式为 `mean`、`max`、`sum`。分别写出它们的公式，并分析各自的优劣：

| 池化 | 公式 | 优势 | 劣势 |
|------|------|------|------|
| `mean` | $\mathbf{h}_G = \frac{1}{|V|}\sum_{i \in V} \mathbf{h}_i$ | ？ | ？ |
| `max` | $\mathbf{h}_G = \max_{i \in V} \mathbf{h}_i$ | ？ | ？ |
| `sum` | $\mathbf{h}_G = \sum_{i \in V} \mathbf{h}_i$ | ？ | ？ |

(b) 作业六 PointNet 对点云使用 max pooling 来获得全局特征。PointNet 的 max pooling 和本作业 GNN 的 `global_max_pool` 有什么联系和区别？它们都保证了什么不变性？

(c) 参考代码中同时使用 `mean` 和 `max` 然后拼接 (`torch.cat`)。为什么这种组合通常优于单独使用其中一种？

> **Hint**：对于 (a)，`sum` 保留图的大小信息（6 个面的 sum ≠ 1 个面的 sum），`mean` 对图大小不敏感，`max` 只保留极端值。对于 (b)，两者都将一个**集合**（无序的）映射为一个固定向量，保证**置换不变性**。区别在于 PointNet 的节点没有边连接（相当于 GNN 的 0 层版本）。

---

### Q2 | BRep vs 其他 3D 表示

BRep 是 CAD/CAM 的标准表示。但在课程中我们还学习了点云（A6）、体素（A7）、SDF（A8）。

(a) 填写下表，比较 8 种表示的关键属性：

| 表示 | 精度 | 拓扑信息 | 存储效率 | 是否可编辑 | 典型应用 |
|------|------|---------|---------|-----------|---------|
| 点云 | ？ | ？ | ？ | ？ | ？ |
| 体素 | ？ | ？ | ？ | ？ | ？ |
| 网格 (Mesh) | ？ | ？ | ？ | ？ | ？ |
| SDF | ？ | ？ | ？ | ？ | ？ |
| BRep | ？ | ？ | ？ | ？ | ？ |

(b) 为什么说 BRep 保留了其他表示所丢失的"设计意图"？举例说明：一个 Cube 经过倒角（chamfer）操作后，BRep 如何记录这个操作的结果，而点云/体素又如何表示同一形状？

(c) BRep 的一个劣势是什么？在什么场景下你会选择点云或 SDF 而非 BRep？

> **Hint**：BRep 精确存储了每个面的解析几何类型（平面方程、柱面参数等）和面-边-顶点的拓扑关系。点云只是表面采样点的坐标集合，丢失了面类型和拓扑连接。倒角后的 Cube 在 BRep 中有 12 个面（6 原面 + 6 倒角面），而点云只是多了一些采样点，无法区分"倒角面"和"原始面"。

---

### Q3 | PyG 的图批处理机制

(a) 在作业五中，Cora 只有一张图，没有"batch"的概念。但本作业有 1000 张不同大小的图。PyG 的 `DataLoader` 是如何将它们打包成 mini-batch 的？请描述以下 batch 中各张量的形状：

假设一个 batch 包含 3 张图：Cube (6 节点, 24 有向边), Cylinder (3 节点, 4 有向边), Sphere (1 节点, 0 边)。

| 张量 | 形状 | 说明 |
|------|------|------|
| `batch.x` | ？ | ？ |
| `batch.edge_index` | ？ | ？ |
| `batch.batch` | ？ | ？ |
| `batch.y` | ？ | ？ |

(b) 为什么 PyG 选择"拼接成大图"的方式，而不是像序列那样用 padding 对齐？（提示：考虑 Cube 有 24 条有向边但 Sphere 有 0 条边——padding `edge_index` 该怎么 pad？）

(c) `global_mean_pool(x, batch)` 是如何利用 `batch` 向量实现分图聚合的？写出其等价的纯 Python 伪代码。

> **Hint**：对于 (a)，10 个节点 (6+3+1) 被拼接，`batch.x` 的形状是 `[10, 8]`；`batch.edge_index` 中 Cylinder 的节点编号被偏移了 6（Cube 的节点数），Sphere 的被偏移了 9。对于 (c)，伪代码类似 `output[k] = mean(x[batch == k])`。

---

### Q4 | 从基础 GNN 到专用 BRep 架构：UV-Net 的设计哲学

在任务三中，我们将 UV-Net 的 UV-Grid 特征池化为 13 维统计量后喂给 GIN。但 UV-Net 原始架构使用了完整的 UV-Grid + CNN 编码。

(a) 我们在任务三中将面 UV-Grid $(10 \times 10 \times 7)$ 池化为 13 维统计量（均值、标准差、mask 占比）。这个池化操作丢失了什么信息？举例说明哪些面的细节无法被统计量捕捉。

(b) UV-Net 使用 2D CNN 处理每个面的 UV-Grid，提取 64 维面嵌入作为图节点特征。相比我们的统计量池化，CNN 编码器多捕获了什么？这对 SolidLetters 分类性能有何影响？（参考：UV-Net 在 SolidLetters 上达到 97.24%，我们的 GIN + 统计量方法大约能达到多少？）

(c) **BRepNet**（Lambourne et al., 2021）不仅使用面-面邻接图，还显式建模了 BRep 的层次结构：face → loop → coedge → edge。这种多层次图结构相比单层面邻接图有什么优势？

(d) 在任务一/二中，Sphere 和 Torus 都是单节点图（1 个面，0 条边），GIN 的消息传递对它们没有效果。在 SolidLetters 中是否存在类似的问题？如何增强图的表示来应对？

> **Hint**：
> - (a) 均值/标准差丢失了空间分布的**局部结构**，例如一个面上有一个小凹陷（法向量在局部翻转），均值法向量会部分抵消而看不到凹陷的存在。
> - (b) CNN 能捕获 UV-Grid 上的**空间模式**——边缘、角点、曲率变化的空间分布。UV-Net 在 SolidLetters 上 97.24% vs 我们大约 85-90%，差距主要来自面级几何信息的丢失。
> - (c) BRepNet 利用了 BRep 的**层次拓扑**，包含面-边-顶点之间的精细关系。例如，两个面通过不同数量的边连接这一信息在单层面邻接图中被压缩为一条边。
> - (d) SolidLetters 中面数较多（平均 ~33），消息传递通常有效。但可以考虑引入边节点（构建异构图）或使用更丰富的节点特征来增强表示。

---

### Q5 | 课程大总结：几何深度学习全景

这是课程的最后一次作业。请完成最终版的统一对比表，涵盖我们学过的**所有**数据域和架构：

| 周 | 数据域 | 表示 | 对称群 | 核心架构 | 关键不变/等变机制 | 邻域定义 |
|----|--------|------|--------|---------|----------------|---------|
| 1-2 | 向量 | $\mathbb{R}^n$ | ？ | MLP | ？ | ？ |
| 3 | 图像 | 2D 网格 | ？ | CNN | ？ | ？ |
| 4 | 序列 | 有序集合 | ？ | Transformer | ？ | ？ |
| 5 | 图 | $(V, E)$ | ？ | GCN/GAT | ？ | ？ |
| 6 | 点云 | $\{p_i\}_{i=1}^N$ | ？ | PointNet | ？ | ？ |
| 7 | 体素 | 3D 网格 | ？ | 3D CNN | ？ | ？ |
| 8 | SDF | $f: \mathbb{R}^3 \to \mathbb{R}$ | ？ | DeepSDF | ？ | ？ |
| **9** | **BRep** | 面-边-顶点拓扑 | ？ | GNN (GIN) | ？ | ？ |

并回答：

(a) 从 MLP → CNN → Transformer → GNN → PointNet → 3D CNN → DeepSDF → BRep GNN，这条路线体现了什么样的对称性递进关系？

(b) 回顾几何深度学习的 5G（Grids, Groups, Graphs, Geodesics, Gauges）框架。我们的课程主要覆盖了哪些部分？哪些部分是前沿研究方向？

(c) **用一句话总结几何深度学习的核心主张**。

> **Hint**：几何深度学习的核心：**选择正确的对称性（不变/等变），就能用最少的参数学到最好的表示**。不同的架构（MLP、CNN、GNN、Transformer、PointNet...）不是独立发明的——它们是同一个原则（"域 + 对称性 → 等变架构"）在不同几何域上的具体实例。

---

## 八、提交要求

将以下文件打包为 `学号_姓名_HW9.zip`：

- `brep_gnn.py` — 合成 BRep 图数据生成 + GNN 分类脚本（任务一/二）
- `brep_gnn_curves.png` — 合成数据训练曲线图
- `brep_confusion.png` — 合成数据混淆矩阵图
- `solidletters_gnn.py` — SolidLetters 数据加载 + GNN 分类脚本（任务三）
- `solidletters_curves.png` — SolidLetters 训练曲线图
- `solidletters_confusion.png` — SolidLetters 混淆矩阵图
- `brep_graphs.png` — BRep 图结构可视化（可选，任务四）
- 终端输出截图（包含两个数据集的最终测试准确率和每类准确率）

通过课程 QQ 群的作业系统上传。

---

## 九、参考依赖

```
torch
torch_geometric
dgl                 # 用于加载 SolidLetters 的 DGL 图文件
matplotlib
scikit-learn
networkx
numpy
tqdm
```
