ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于CNN的传统村落图景分类:从数据构建到部署的完整实践

2026/9/18 23:19:47 拓冰建站 浏览量
基于CNN的传统村落图景分类:从数据构建到部署的完整实践 简介一份基于卷积神经网络CNN的传统村落图景分类研究PDF面向城乡规划、建筑学与人工智能交叉领域的研究者、规划师及相关专业学生为其提供可参考的自动化分类方法与研究思路。研究借助自建的传统村落数据库提取村落肌理、地形地貌、建筑形态结构和表皮材质等特征按省份对全国传统村落图景进行自动识别与归类可作为传统村落数字管理信息系统建设及后续量化研究的素材与支撑。文中还引入深度学习、空间量化与人工智能分析手段并附有研究对象的图像处理流程和实验说明读者可了解如何将其应用于文化遗产保护中的图像分类任务获取数据集构建、特征提取与分类实验的完整逻辑。资源包共1个PDF文件大小5.87MB已有219人学习浏览适合希望快速了解深度学习在传统村落研究中落地路径的读者参考。1. 传统村落图景分类的难点恰好长在卷积神经网络的强项上真正做过传统村落数字化项目的人都会遇到一个反直觉的现象给建筑单体做分类ResNet 随便一训就有九成多准确率可一旦把任务换成“以村落为单位的图景分类”准确率会突然掉到七成以下。原因在于传统村落图景不是对象识别而是场景判别——徽派马头墙和江南水乡的封火墙在局部纹理上高度相似闽南红砖厝和岭南镬耳屋在屋顶色彩上也有重叠。你让模型去判断“这是哪个村子的典型风貌”它面对的其实是区域风格、建筑密度、植被环境和拍摄角度的联合分布。这正是卷积神经网络相对前馈神经网络和 Transformer 的结构优势所在。CNN 通过局部感受野和权值共享把“邻域相关性”直接编码进网络结构对图像中的平移、尺度变化和纹理组合有天然鲁棒性在样本量只有几千张的村落图景数据上这种归纳偏置比需要海量数据才能驯服的全局自注意力机制要可靠得多。下面按数据集构建、训练脚本、调参手段、部署验证四条线把这条路走通。2. 先把图景数据构建成能训的样本集2.1 图源采集、内容去重与目录结构约束训练样本的质量上限决定模型上限村落图景分类尤其如此。常见的做法是先确定类别体系再按类别去公开图源和历史影像平台采集。假设把任务定为六类徽派村落、江南水乡、闽南红砖、川西碉楼、北方窑洞、岭南镬耳屋每类目标收集 800~1500 张。采集脚本的要点是内容去重和请求限速否则同一条街道的照片会被搜索引擎以不同裁剪尺寸重复抓取导致验证集和训练集之间出现像素级重复准确率虚高。import time, hashlib, requests from pathlib import Path def download_images(urls, save_dir, sleep1.0): save_dir Path(save_dir) save_dir.mkdir(parentsTrue, exist_okTrue) for idx, url in enumerate(urls): try: r requests.get(url, timeout10, headers{User-Agent: Mozilla/5.0}) r.raise_for_status() name hashlib.md5(r.content).hexdigest() # 按内容指纹去重 out_path save_dir / f{name}.jpg if not out_path.exists(): out_path.write_bytes(r.content) time.sleep(sleep) except Exception as exc: print(f[{idx}] download failed: {exc})这段脚本用hashlib.md5(r.content)对图片二进制内容取指纹同一个图无论 URL 参数怎么变只要内容一致就不会重复入库。sleep参数控制请求间隔避免对图源服务器造成压力。采集完成后按data/{类别名}/{图片名}.jpg组织目录再生成一份labels.csv供训练脚本读取路径和标签一一对应后续做分层抽样和错误分析都方便。2.2 用 CLIP 粗筛低质量图像再把标签判定的标准固定下来村落图景数据集里低质量样本的比例通常比想象中高很多无人机俯拍和地面平拍混在一起、画面里出现大面积现代建筑、强雾导致天际线不可辨。逐张人工清洗一万张图太慢我一般先用 CLIP 做一轮粗筛把“图景不清”和“图景清楚”分开。CLIP 不适合做细粒度村落类型判别但判断“图像内容与一段描述是否匹配”的能力很强正好用来做质量过滤。import torch, clip from PIL import Image device cuda if torch.cuda.is_available() else cpu model, preprocess clip.load(ViT-B/32, devicedevice) prompt_pairs [ 一张清晰的传统村落全景照片, 一张模糊或有遮挡的照片, ] def is_clear(img_path, thresh0.3): image preprocess(Image.open(img_path)).unsqueeze(0).to(device) text clip.tokenize(prompt_pairs).to(device) with torch.no_grad(): logits model(image, text)[0].softmax(dim-1) return logits[0, 0].item() thresh粗筛只是去掉明显低质的图真正的难点是标签判定的主观性。同一个村子有人按“建筑风格”归类有人按“地理区域”归类模型学到的边界就会不一致。固定标准的做法是以“图中主体建筑的典型风格”为唯一标签依据行人、招牌、车辆等现代元素不参与判定只要画面里传统建筑群占比过半就按该村落所属类型打标。把这条规则写进数据标注文档多人协作时才不会越标越乱。2.3 训练集/验证集划分与常见的偏置陷阱划分数据集时最容易犯的错是随机划分而不看来源。图源爬虫是按关键字抓取的同一篇文章的配图可能被连续抓下随机划分后相似的图片会同时出现在训练集和验证集里。稳妥做法是先按目录分组再对组做分层抽样。按村落 ID 或图源 URL 的域名做分组保证同一个来源的图只落在一侧。from sklearn.model_selection import train_test_split import pandas as pd df pd.read_csv(labels.csv) df[source_group] df[path].apply(lambda p: p.split(/)[0]) # 按来源分组 train_idx, val_idx train_test_split( df.index, test_size0.2, stratifydf[label], shuffleTrue, random_state42 ) train_df, val_df df.loc[train_idx], df.loc[val_idx]stratifydf[label]保证每个村落类别在验证集中占比与全集一致避免某个小类在验证集里只有个位数样本导致评估指标波动过大。分组之后还要检查一遍验证集中是否存在与训练集同源或连拍的图片肉眼抽查几十张比任何自动化指标都靠谱。3. 可复现的卷积神经网络村景分类训练脚本3.1 网络选型对比为什么 ResNet18 起步生产再换 MobileNetV3传统村落图景分类属于典型的静态图像单帧判别和遥感领域用 3D 卷积神经网络处理多光谱时序数据不是一类问题不需要引入额外的维度。在 1 万张以内的数据规模下ResNet18 是性价比最稳的起点它只有 1100 万参数预训练权重丰富训练速度快特征提取能力对村景这种中粒度场景完全够用。真正要避免的是直接上 ResNet152 或两层深的 ViT小数据集中深层网络更容易记住训练集的屋面纹理和植被色彩而不是村落风格本身。网络参数量ImageNet 预训练推理耗时CPU适用阶段ResNet1811.2M有约 18ms/张实验基线、特征分析MobileNetV3-Large5.4M有约 8ms/张生产部署、嵌入式EfficientNet-B05.3M有约 12ms/张兼顾精度与速度ViT-B/1686M有约 35ms/张数据量 5 万以上再考虑实际项目中先用 ResNet18 跑通流程、确认数据质量达到目标精度后再把同样的训练管线换成 MobileNetV3 微调一轮模型体积缩小一半精度损失通常在 0.5% 以内。3.2 完整训练脚本 train.py数据加载、预训练权重与损失函数下面给出一份可以直接训练六类村落图景的代码使用 PyTorch 实现。数据增强部分针对村景特点做了调整不只用随机裁剪和翻转还加了小幅度的色彩抖动模拟不同天气和光照条件下的色偏。import torch import torch.nn as nn import torchvision from torchvision import transforms from torch.utils.data import Dataset, DataLoader from PIL import Image import pandas as pd class VillageDataset(Dataset): def __init__(self, df, transformNone): self.df df self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img Image.open(row[path]).convert(RGB) if self.transform: img self.transform(img) return img, row[label] train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds VillageDataset(train_df, train_transform) val_ds VillageDataset(val_df, val_transform) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) model torchvision.models.resnet18(weightsIMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 6) criterion nn.CrossEntropyLoss(label_smoothing0.1) optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() loss criterion(model(images), labels) loss.backward() optimizer.step() scheduler.step() torch.save(model.state_dict(), fcheckpoints/epoch_{epoch}.pth)参数说明如下RandomResizedCrop的scale(0.6, 1.0)比 ImageNet 常用的(0.08, 1.0)更保守因为村落图景的主体建筑通常占画面比例较大裁剪太狠会让模型学到建筑局部碎片。label_smoothing0.1用于软化硬标签六个村落类别之间有显著相似性硬标签会逼着模型把相似风格的差异也学到极致反而伤害泛化。学习率3e-4配合 AdamW 是微调预训练权重的安全起点CosineAnnealingLR的T_max30让学习率在 30 个 epoch 内从初始值平滑降到接近零省去手动调节学习率衰减节点的麻烦。3.3 训练完成后先看混淆矩阵而不是只看 top-1训练结束后打印验证集准确率只是第一步更关键的是混淆矩阵。村落图景分类的错误往往集中在特定类别对之间——徽派村落和江南水乡相互误判、闽南红砖和岭南镬耳屋相互误判这些在总准确率上只表现为几个百分点但恰恰是后续调优的方向所在。from sklearn.metrics import confusion_matrix import numpy as np model.eval() all_preds, all_labels [], [] with torch.no_grad(): for images, labels in val_loader: images, labels images.cuda(), labels.cuda() preds model(images).argmax(dim-1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) print(cm)拿到混淆矩阵后我习惯先把每个类别的召回率列出来找召回率低于 85% 的类别再去看它的误判去向。如果 A 类大量被误判为 B 类说明这两类在数据分布上存在交集下一步要做的是回到原始图片分析差异而不是急着换模型。4. 传统村落图景分类的调参与防过拟合手段4.1 类别不均衡用 WeightedRandomSampler 平衡村落类型样本自然采集的村落图景天然不均衡徽派村落因为知名度高、图源多可能攒下两千张而川西碉楼只有四百张。不处理的话模型会偏向样本量大的类别碉楼的召回率可能跌到五成以下。两种常见处理方式中直接复制少数类图片容易过拟合更稳的是在采样层面做加权。from torch.utils.data import WeightedRandomSampler label_counts train_df[label].value_counts().to_dict() sample_weights [1.0 / label_counts[lab] for lab in train_df[label]] sampler WeightedRandomSampler(sample_weights, num_sampleslen(train_df), replacementTrue) train_loader DataLoader(train_ds, batch_size32, samplersampler, num_workers4)WeightedRandomSampler让每个样本被抽到的概率与类别样本数的倒数成正比少数类在一个 epoch 里会被采样多次多数类则被降频。replacementTrue表示允许重复采样这是必要的——如果不允许重复少数类样本根本不足以保证每个 batch 里都有。配合标签平滑一起使用可以在不加重过拟合的前提下提升少数类识别率。4.2 相似村落风格混淆标签平滑与多任务地理分支徽派与江南水乡的混淆不是数据清洗能完全解决的。两者的共同点太多白墙、灰瓦、临水布局、高密度聚落。针对这种情况我一般会给模型加一个额外的地理区域分类分支形成多任务学习结构。主分支负责村落风格分类辅助分支负责判断所属地理片区共享卷积层特征。import torch.nn as nn class VillageNet(nn.Module): def __init__(self, num_classes6, num_regions7): super().__init__() backbone torchvision.models.resnet18(weightsIMAGENET1K_V1) self.features nn.Sequential(*list(backbone.children())[:-1]) in_features backbone.fc.in_features self.fc_cls nn.Linear(in_features, num_classes) self.fc_region nn.Linear(in_features, num_regions) def forward(self, x): feat self.features(x).flatten(1) return self.fc_cls(feat), self.fc_region(feat) criterion_cls nn.CrossEntropyLoss(label_smoothing0.1) criterion_region nn.CrossEntropyLoss() for images, (labels, regions) in train_loader: optimizer.zero_grad() logits_cls, logits_region model(images) loss criterion_cls(logits_cls, labels) 0.2 * criterion_region(logits_region, regions) loss.backward() optimizer.step()地理分支的作用是给共享特征施加一个先验约束徽派和江南水乡虽然风格接近但地理片区不同特征提取器被迫保留能区分两者的信息。权重系数0.2是经验值取值范围一般在 0.1~0.5 之间太大会让模型优先优化地理任务而忽略主分类。这个思路的原理和 ArcFace 这类度量学习损失不同ArcFace 直接压缩同类特征距离而多任务分支属于隐式正则化实现门槛更低对工程团队更友好。4.3 误差分析往回看中间层热区定位学习盲点训练到第 20 个 epoch 时如果验证集准确率停滞不要急着调学习率先用可视化检查模型到底在看什么。传统的 tensorboard 特征图输出对工程师不直观CNN 各层的特征图是人眼难以解读的高维张量。我通常用 grad-CAM 或者现成的可视化工具包生成热力图叠加在原图上肉眼判断模型是否锁定了建筑主体。提示理论细节想系统补一遍的可以翻看李宏毅关于 CNN 的讲义里面关于感受野和特征图可视化的部分讲得很细。但要记住网络结构只是骨架村落图景分类的误差大头通常来自数据分布而不是网络本身。如果热力图大面积落在植被、天空或道路上说明模型学的是场景背景而不是建筑风格。这时候应该回到数据层面增加不同季节、不同天气下的村落样本把背景变化的范围扩得更大迫使模型关注建筑结构本身。反过来如果热力图集中在建筑上但准确率仍然低才是真正的类间特征重叠问题再考虑换损失函数或加分支。5. 可视化验证和导出部署用的模型5.1 用 Grad-CAM 确认模型依据的是建筑特征而非背景纹理训练完成后用验证集里每个类别抽三张图做 Grad-CAM 检查。torchcam 库封装好的实现可以直接用指定最后一个残差模块作为目标层即可。from torchcam.methods import GradCAM from torchcam.utils import overlay_mask camera GradCAM(model, target_layerfeatures.8) with torch.no_grad(): out model(img.unsqueeze(0)) act_map camera(0, out.argmax(dim-1).item()) result overlay_mask(img, act_map, alpha0.6)act_map是模型在目标层的梯度加权激活图overlay_mask把它叠加到原图上。检查时可以借 cnnexplainer 离线包这类可视化工具从旁印证它的优势是离线环境下也能逐层查看特征响应不依赖远端服务。重点是确认热区落在建筑群上而不是落在水面倒影或天空上。如果个别类别错了把它的热力图和混淆矩阵放在一起看能很快定位是模型判据错误还是数据标注矛盾。5.2 导出 ONNX 并做边界样本的盲测最后一步是把 PyTorch 模型导出为 ONNX方便在 Web 或移动端推理。导出时要把动态 batch 维度打开否则部署时一次只能预测一张图。import torch model.load_state_dict(torch.load(checkpoints/best.pth, map_locationcpu)) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, village_cls.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch}, logits: {0: batch}}, opset_version17 )dynamic_axes里的{0: batch}表示第 0 维是动态的这样导出的模型既能处理单张图也能一次处理一个 batch。opset_version17对应较新的推理引擎版本兼容性和算子支持都更完整。导出后在测试机上用 onnxruntime 跑一遍确认输出与 PyTorch 原模型一致即可。盲测样本不要用验证集里的图片专门找数据分布之外的边界情况雨后积水的皖南村落、傍晚逆光的闽南红砖房、翻新过的仿古商业街、无人机高视角俯瞰图。把这些样本跑一遍记录每类置信度分布。漏判率高的类别说明训练数据里缺少对应场景把同类型盲测图补充进训练集重新训练一轮。这个验证动作比任何评价指标都更能说明模型在真实业务里的可靠性。本文还有配套的精品资源点击获取