最近我在做一个建筑实景分割项目时,被一个看似很基础的问题卡了两周:模型在验证集上mIoU能到0.71,但一旦换到新区块,预测结果里"外墙""幕墙""遮阳板"就开始来回打架,错得非常有规律。后来回溯训练管线,发现病灶挺讽刺——我们在数据预处理时,把这些建筑语义类别都换成了纯数字ID,喂给模型的就是0、1、2、3……模型确实能背下来训练集里的像素分布,但对"墙为什么是墙、梁和柱的区别到底是什么"一窍不通。正好那段时间大语言模型编码(LLM Encodings)在非文本领域很火,我就试着把这类语义先验以文本嵌入的方式绕进分割网络训练流程,折腾了大概一个月,效果确实超出预期。
这篇文章就是那段时间的完整复盘。核心围绕一个方向:如何用大语言模型编码去增强AI模型训练中的建筑语义保留,让模型在类别极细、样本极不均衡、标注口径又很乱的建筑数据上记住"语义"而不是"ID"。适合正在做建筑语义分割、点云分类、BIM构件识别或者任何有细粒度类别需求的朋友参考,我会把选型思路、实施路径、踩坑记录和消融结果都摊开讲。
1. 建筑语义数据集的两个经典痛点,One-Hot编码为什么打不穿
1.1 类别粒度极细,样本分布几乎永远是畸形的
建筑数据集的类别表,恐怕是所有行业数据集里最劝退的之一。拿一个旧城区改造扫描项目来说,仅立面构件就拆出了17个类别,包括普通外墙、涂料外墙、陶板幕墙、玻璃幕墙、金属格栅、遮阳百叶、女儿墙、檐口线条等。其中"普通外墙"占了全部标注面积的42%,而"檐口线条""落水管"这类细长构件,占比合计不到0.5%。
这种分布下,标准做法是给每个类别一个One-Hot编码,配合交叉熵损失和类别权重。试过的朋友应该都有体会:类别权重一调,低频类别的预测是变多了,但经常是过量,把"玻璃幕墙"大面积误检成"玻璃栏板",因为这两个类别的像素外观实在太接近。本质原因是:One-Hot编码的向量之间距离全等,模型眼里类别ID和类别ID之间没有任何先验关系,只能靠视觉特征硬拗,低频类又没有足够的样本撑起稳定的视觉模板,于是相近类别的决策边界就成了摇晃的钢丝。
我用一个小的语义相似度工具算过这17个类别的视觉混淆率,排在前五的混淆对基本全是"结构位置不同、材质外观相近"的组合。这种错误模式本身就在提示:光靠视觉通路,信息不够。
1.2 标注口径混乱,同一构件在不同项目中叫法完全不一样
更头疼的是,建筑语料本身就缺乏统一同义词表。同样是"填充墙",在有些项目里标注为"内隔墙",在另一些项目里叫"非承重墙";同样是"阳台",有的图纸系统叫"露台",有的叫"悬挑板"。
这种标注口径差异在普通视觉任务里只是噪声,但在细粒度语义任务里是灾难。因为模型学习的是"类别ID到视觉特征"的映射,如果同一个物理实体在训练集和测试集里被赋予不同的ID,模型等于在背两套词典。而且,这类问题几乎不可能在标注阶段靠短期的对齐培训解决,毕竟很多数据来自历史项目归档,标注的人换了一茬又一茬。
当时我产生了一个很直觉的想法:模型不需要记住"ID=7的像素长什么样",它需要理解"ID=7对应的是建筑中的某种墙,墙和柱的根本区别在于承重方向与几何比例"。这种"语义"信息,在纯图像通路上几乎无法被直接监督,但在语言模型生成的文本描述里,却是现成的。这就成了我整个方案的地基。
2. 把大语言模型编码接进训练流程:我对比过的三种接入方法
先说整体思路:利用预训练大语言模型的文本编码器,把每个建筑类别对应的名称、属性、关系描述编码成稠密向量,再让分割网络在训练时感知这些向量。大模型编码在这里扮演的是"语义翻译官",把建筑领域里那些看不见的规则,翻译成网络可以用的梯度信号。
我实验了三种差异很大的接入方式,各有适用边界,下面按我实测的顺序展开。
2.1 方法一:类别文本嵌入直连——把语义向量拼进特征通道
最朴素的方案:用文本编码器为每个类别生成一个固定向量,然后像语义标签一样,把类别语义向量作为额外通道拼在解码器输入或者分类头之前。
以常见的分割网络为例,网络把图像编码成特征图B, C, H, W,分类头输入是每个像素的C维特征。我的做法是把类别语义矩阵做成可被像素索引的查找表,每个像素不是拿一个纯数字ID去查输出权重,而是让ID去查一个V, D的语义矩阵(V为类别数,D为文本嵌入维度),查出来的D维向量和图像特征C维拼接,再进分类器。
import torch import torch.nn as nn from transformers import AutoModel, AutoTokenizer class SemanticallyAwareHead(nn.Module): def __init__(self, feat_dim, num_classes, text_embed_dim=384, hidden_dim=256): super().__init__() # 这里选用的是通用文本编码器(冻结参数) self.text_encoder = AutoModel.from_pretrained("some-open-text-encoder") for p in self.text_encoder.parameters(): p.requires_grad = False self.embed_dim = text_embed_dim self.classifier = nn.Sequential( nn.Conv2d(feat_dim + text_embed_dim, hidden_dim, 1), nn.ReLU(inplace=True), nn.Conv2d(hidden_dim, num_classes, 1), ) def get_class_embeddings(self, class_texts): # class_texts: list of strings, 如 ["外墙", "承重墙", "窗"] tokens = self.tokenizer(class_texts, padding=True, return_tensors="pt").to(next(self.parameters()).device) with torch.no_grad(): embeds = self.text_encoder(**tokens).last_hidden_state[:, 0, :] return embeds.detach() # V, D def forward(self, feat, class_embed_lookup, class_ids): # 像素级查找:用分割标签的one-hot乘查找表,得到像素级语义向量 # 这里简化为只取当前batch用到的类别 B, C, H, W = feat.shape sem_feat = class_embed_lookup[class_ids].permute(0, 3, 1, 2) # B,D,H,W fused = torch.cat([feat, sem_feat], dim=1) return self.classifier(fused)这个方案的优点是完全不改变原有网络骨架,推理时可以提前算好类别嵌入矩阵,额外开销只有一个拼接和一层1x1卷积。缺点是:语义信号只是作为"额外先验"输入,网络可以选择忽略它——如果视觉特征已经足够拟合训练集,模型不会主动去利用语义向量。我实测下来,在小数据集上提升明显,但数据集一扩大就不够看了。
2.2 方法二:文本-视觉对比对齐——用互信息逼两者靠近
第二个方案借鉴了跨模态对比学习的思路,不直接改变分类头,而是在训练时增加一个辅助目标:同一类别的图像特征应该与其文本编码靠近,不同类别则推开。
具体来说,分割网络的主干特征图经过全局平均池化后,得到每张图的全局特征;同一batch内所有类别对应的文本编码经过线性投影后,构造对比损失。这里关键的是"该把什么和什么对齐"。直接对整图全局池化特征做对齐太粗,因为一张街景图里包含十几个类别。我后来改成用掩码池化——把分割标签当作mask,对每个类别区域内的特征做平均池化,把这块区域的特征向量与对应类别的文本向量拉近。
这个方案本质上是给网络加了一个跨模态正则化,梯度不仅来自像素分类,还来自"视觉特征和语言特征得保持一致"。实测下来它比方法一稳得多,尤其是缓解相近类别混淆上效果很明显,但训练时间增加了约15%,而且对掩码池化操作要额外小心处理小目标区域,否则池化出来的特征噪声极大。
2.3 方法三:知识蒸馏——让大模型当老师,但不是对logits蒸馏
第三种是我觉得最有意思、也是最终方案里采用的一种。一般的知识蒸馏是"大网络教小网络",但这里我蒸馏的不是预测结果,而是"语义命名能力"。
具体做法分两步。先把建筑类别名以及由大语言模型生成的属性描述句(比如"承重墙:主要承受上部结构传递的竖向荷载的墙体,通常厚度较大,与梁柱刚性连接")同时喂给文本编码器,得到语义候选向量。然后在网络的每个解码器阶段,额外训练一个小型投影头,去预测当前像素最可能对应哪一条语义描述向量。相当于网络不只是学一个离散分类,还在学一个连续语义空间中的定位。
这个方案的收益是最全面的,因为它在网络多个尺度上都施加了语义监督,浅层特征被逼着区分"墙体"和"非墙体"这种上位语义,深层特征再细化到具体是哪一种墙。缺点是训练复杂度高,需要维护语义候选向量集,并且要对每个batch在线计算相似度。
三种方式的对比我整理成了表,方便直接选型:
| 接入方式 | 额外训练开销 | 分类头改动 | 语义利用深度 | 适用场景 | 我的实测结论 |
|---|---|---|---|---|---|
| 类别文本嵌入直连 | 低 | 小(拼特征) | 浅 | 小数据快速试点 | 提升有限,模型容易忽略语义信号 |
| 文本-视觉对比对齐 | 中 | 无 | 中 | 有一定数据量的常规训练 | 类别混淆明显缓解,训练时间+15% |
| 语义描述蒸馏 | 较高 | 需加投影头 | 深 | 细粒度、长尾场景为主 | 效果最好,收敛也更快,复杂度可接受 |
最终我选的是方案三为主、方案二做辅助损失。原因很简单:建筑语义分割最难的从来不是分类器,而是"特征的语义组织",只动分类头不如直接在特征层面灌语义。
3. 实验室设计:提示词模板、数据增强与训练参数的坑
方案一旦确定,80%的精力反而耗在了"怎么写提示词""怎么配训练参数"上。这部分没什么学术文章会写得特别细,但实操里每一步都可能让效果翻倍或归零。
3.1 提示词模板远比你想象的敏感
用大语言模型编码建筑类别,第一步是定义每个类别喂给文本编码器的字符串。你可能会觉得直接把类别名丢进去就行——实测效果很差。因为建筑类别名常常是多义词,"幕墙"单独输入时,文本编码器注意力被"幕"这个字分散,生成的向量和"窗帘幕布"靠得更近,而不是"建筑幕墙"。
经过多轮调整,我沉淀出一套对建筑领域比较可靠的提示词模板:
{类别名},{核心定义或功能描述},通常出现在{典型位置},{与其他构件的典型关系}以"女儿墙"为例,完整的输入是:
女儿墙,建筑物屋顶周围的矮墙,通常沿屋面边缘连续设置,主要起防护和建筑立面收口作用,下部与屋顶结构层连接注意这里我没有直接让大语言模型"生成"文本,而是自己写了结构化模板。原因是想尽量控制文本编码器所在的语义空间稳定,避免大模型自由发挥出风格漂移的描述。每个句子的长度也做了归一化,太长的描述会导致注意力分散,太短则信息量不足。实测下来最佳长度是5到8个语义单元。
3.2 借用大模型做"在线同义词增强"
这是另一个让我意外的增益点。建筑领域的标注往往不统一,比如同一个"防火墙",不同项目里还可能写成"防火隔墙""防火分隔墙"。我在训练时用大语言模型批量生成每个类别的别名和近义描述,再和主描述交替输入文本编码器,相当于在语义空间里做了数据增强。
具体逻辑很朴素:网络在不同角度描述下都看到"墙"的语义向量,就能更不容易被标注词汇的差异带偏。我这里提到的"大语言模型生成别名",并不是在训练时反复调用,而是离线一次性生成,存入一个JSON文件,训练时随机挑选。
class_synonyms = { "防火墙": ["防火墙,用于阻止火焰蔓延的分隔墙体", "防火隔墙,防火分区边界处设置的墙体"], "玻璃幕墙": ["玻璃幕墙,由玻璃面板与支承结构组成的建筑外墙", "幕墙系统,悬挂于主体结构外侧的非承重外墙"] }训练迭代时,每个batch随机选一个同义项编码,并把它作为该类别本次迭代的语义锚点。这个方法对换数据集测试时的泛化效果贡献很大,后来消融时去掉它,跨区域测试的mIoU掉了将近2个点。
3.3 关键训练参数的选取逻辑
这些参数组合不是拍脑袋来的,每一条背后都有对应的一次失败实验:
| 参数项 | 我的最终取值 | 踩过的坑 |
|---|---|---|
| 文本编码器冻结 | 是,全冻结 | 最初微调了编码器,发现训练早期语义向量漂移严重,分割头跟着震荡 |
| 语义辅助损失权重 | 0.3(总损失=1.0分类+0.3语义) | 权重调到0.8时模型开始偏向语义分类,像素边界变模糊 |
| warmup 步数 | 2000步 | 不加warmup,对比损失前500步震荡幅度很大,一度以为要发散 |
| 语义投影头维度 | 256 | 维度太低语义信息被压缩,太高则在小数据集上过拟合 |
| 训练分辨率 | 768×768 | 分辨率太低时,女儿墙这类细长构件的掩码池化特征基本是噪声 |
训练时用到的损失函数组合如下,供直接参考:
total_loss = ce_loss(pred, label) + 0.3 * semantic_contrast_loss(feat, text_embeds, label)语义对比损失的具体实现并不复杂,就是常规的InfoNCE变体:以类别为粒度,把同一batch内同一类别的掩码池化特征互为正样本,与所有其他类别文本向量互为负样本。
4. 训练过程中我踩过的三个"翻车现场"和完整排查链路
4.1 文本嵌入维度的隐形天花板
第一次跑通方案二时,我心里预期至少涨两三个点,结果mIoU不升反降。当时非常困惑,理论推导和实现都没有明显错误,网络也正常收敛。后来把分类头断点输出拉出来看,发现一个现象:拼接后的特征向量中,文本嵌入维度占的比例太小,信号完全被图像特征淹没。我用的是OpenAI那类通用文本编码器,输出维度是1536维,而图像特征维度是256维,两者拼接后文本占了86%,按理说不该被淹没——但问题是,文本嵌入本身在BatchNorm层被重新归一化后,方差被压到和图像特征同量级,语义信息大量丢失。
正确的处理方式不是直接拼接,而是先对文本嵌入做独立的LayerNorm和线性压缩,映射到128维的语义空间,再和图像特征拼接。压缩后的语义向量反而保留了更集中的判别信息。这个改动单独带来了约1.8个点的mIoU提升。
4.2 辅助损失权重过高,模型开始"背文本"
这个问题发生在调参阶段,我把语义辅助损失权重从0.3加到了0.8,想着语义约束越强越好。结果训练曲线显示分类损失下降正常,但验证集mIoU却在30个epoch后掉头向下。把预测结果可视化出来,发现一个诡异的模式:模型把几乎所有细长的构件都预测为"柱",因为"柱"的语义描述文本在语义空间里和"竖长的物体"距离最近,而视觉上很多建筑设备管道也是竖长的。
排查到最后,问题出在辅助损失和分类损失的优化方向冲突:语义对比损失鼓励"视觉特征靠近文本向量",但当文本向量之间的距离本身小于部分类别之间的视觉距离时,强行拉近反而牺牲了像素级的判别边界。这就像让一个学生学习时总是先背标准答案,但考试偏偏考变式题,背得越熟错得越狠。
最终解决方案不复杂:把语义辅助损失从固定权重改成阶梯式衰减,前10个epoch用0.5让模型快速建立语义锚点,之后逐步降到0.1,让位于像素分类。这一条单独带来约1.2个点的提升,更重要的是彻底解决了"背文本"问题。
4.3 标注语言不一致导致的语义向量离群点
这是最有建筑行业特色、也是我最想分享的一个坑。项目数据来源既有中文标注的项目也有英文标注的项目,"外墙"是一类,"exterior wall"是另一类,甚至同一个中文项目里早期标注用"外墙面",后期统一成"外墙"。如果不处理,语义候选矩阵里会出现几乎重复但又不完全重合的向量,让对比学习的目标变得自相矛盾。
排查链路是这样的:先对语义向量做可视化(降维投影到二维),看到有几个类别的向量和它们最相近的类别向量距离超过正常水平两倍以上;再逐一查看对应的原始描述文本,发现是"窗""窗户""window"三个条目同时存在。处理方式是把跨语言的同义条目统一到单一规范名,再在描述文本里同时保留两种语言的别名。这样既避免了向量冗余,又保留了跨语言的泛化迁移能力。
5. 消融实验与评测:语义保留到底值几个点
5.1 评测指标:mIoU之外加一个混淆敏感度
很多建筑语义项目只看mIoU,但这个指标对"少量类别错得离谱"和"大量类别轻微错误"给出的分数可能一样,这会掩盖语义保留的价值。我在mIoU之外额外统计了"结构化混淆指数":把所有真实类别两两组合,统计互相误判的像素占比,再按照建筑结构分组(墙体系、门窗体系、屋面体系、构件体系),看混淆集中在组内还是跨组。如果模型语义结构学得好,混淆应该集中在组内(比如"玻璃幕墙"误判为"陶板幕墙"),而墙体不会误判成窗户。
5.2 消融结果对比
在同一个旧城区立面数据集上,我逐步加回各个模块,结果如下:
| 配置 | mIoU | 混淆指数(越低越好) | 墙体组内精度 | 构件组内精度 |
|---|---|---|---|---|
| 基线(One-Hot + CE) | 0.713 | 0.268 | 0.781 | 0.642 |
| + 类别文本嵌入直连 | 0.721 | 0.241 | 0.792 | 0.671 |
| + 文本-视觉对比对齐 | 0.744 | 0.196 | 0.817 | 0.703 |
| + 语义描述蒸馏 | 0.762 | 0.163 | 0.834 | 0.746 |
| 完整方案(蒸馏+对比+同义增强) | 0.783 | 0.121 | 0.865 | 0.793 |
从表中可以看到,完整方案相比基线mIoU涨了7个点,结构化混淆指数从0.268降到0.121,幅度非常可观。尤其值得注意的是一开始的猜想被验证了:增益最大的场景恰恰是样本占比低于2%的构件类别,"檐口线条""落水管"这类之前基本靠猜的类别,像素精度涨了近14个点。这说明语义先验对长尾分布的补偿作用比单纯调类别权重要健康得多,因为模型不再需要海量样本来脑补"这是什么"。
5.3 从预测结果里看到的实际变化
把两个模型在同一组测试图上的结果摆在一起看,差别很有意思。基线模型对建筑外立面的整体把握其实已经不差,大面积墙体基本正确,但女儿墙、勒脚、窗台板这些"边界构件"经常会融合在墙体里。加了语义编码后的模型,在这些细长构件的边缘会给出更明确的分离预测。我推测原因是:文本描述里包含了"女儿墙位于屋顶边缘顶部收口"这类空间关系信息,模型在训练时被引导去关注屋顶边缘附近区域的特征,而不是把所有平坦立面都当墙。
另一个直观变化是跨区域泛化。用旧城区项目训练的模型直接测评新城区一个待标注项目的数据,完整方案的mIoU比基线高了9.2个点。新城区项目的墙体构造和标注风格与旧城区有明显差异,能有这个提升,已经从侧面说明模型学到的不是某一块区域的像素模板,而是更接近"建筑语义"本身的东西。
6. 这套方案的边界条件与我在实践中形成的扩展思路
6.1 什么情况下不建议硬上这套方案
不是所有建筑AI项目都适合引入LLM编码。首先是数据规模:如果你的训练集只有几万张图像且类别只有三五个大项(比如只有"地面、墙、天花板"),One-Hot编码加一个良好的预训练分割网络完全够用,额外语义模块带来的提升可以被忽略,还会徒增训练复杂度。其次是类别关系问题:这套方案适合的是类别之间有"内容关联"的细粒度场景,比如墙体、柱、梁这类结构件相互独立但视觉相似的类别。如果类别本身就是完全互斥且视觉差异极大的(如"草地"和"天空"),文本编码提供不了额外信息,因为视觉已经足够区分。最后是预算约束:方案三需要增加投影头、语义候选矩阵和额外的对比损失计算,显存占用比基线高出约1.5GB,训练时间增加约20%,老显卡用户需要提前评估。
6.2 与大语言模型交互的工程化建议
在跑了几轮实验之后,我对"怎么和文本编码器打交道"有了一些工程上的体会。最重要的一条是构建语义描述的时候一定要做统一格式的模板,并且要控制句子长度,最长不要超过模型训练时的序列长度上限。更长的文本输入会触发截断,导致尾部信息直接丢失,而建筑构件描述里最关键的往往就是"与其他构件的关系"这部分,恰好常常位于句子尾部。我的解决方案是把最核心的定义放在描述的第一句,属性放在第二句,关系与位置放在第三句,这样即使截断也只损失次要信息。这条经验在后续做BIM构件识别时同样适用。
另外,保持文本编码器冻结是一个安全性选择。微调文本编码器理论上可以更好地适配建筑领域,但实际训练中梯度会同时更新语言模型和视觉模型,两个模型的学习率如果不严格对齐,很容易出现"语义空间漂移",表现为早期验证指标快速上升后骤降。如果你的任务确实需要微调文本编码器,建议给它设置一个比主干网络小20倍的学习率,并且密切监控语义向量的分布变化。
6.3 往BIM知识图谱方向扩展的可能性
这个方向目前我已经在尝试进一步延伸。建筑领域的语义不像自然图像那样只是一个"名词标签",它背后还连着构件关系、材料属性、施工逻辑等结构化信息。大语言模型编码捕捉到的语义还只是"语言知识",如果能进一步和BIM模型里的属性拓扑结构对接,比如让类别语义向量同时包含"该构件连接了哪些构件""是否承重""是否保温层"等结构化特征,训练出来的模型在语义保留能力上可能会再上一个台阶。
走在路上的同行应该都感受到了:建筑AI落地难,难题很多时候不在模型结构,而在"模型是否能理解建筑行业的思维方式"。大语言模型编码这条路,本质上是用一种低成本的方式把行业知识翻译成网络能用的梯度信号。我后续会继续在这个方向做更系统的实验,也希望这篇文章能给大家一个可以直接起跑的参考。