深度学习矿物薄片图像分类:从数据预处理到模型部署实战
简介这是基于深度学习实现矿物显微图像智能分类的完整项目源码与说明配套覆盖数据爬取、数据集划分、模型训练、评估、预测及浏览器端展示全流程。面向计算机、人工智能、数据科学等相关专业的学生和开发者既适合新手从零上手也可作为课程设计或毕业设计的实用参考。压缩包内共15个文件以7个Python脚本、2个Jupyter Notebook、2个HTML页面为主另有JavaScript交互文件、Markdown说明文档及模型结构示意图分别承担模型训练与预测、交互式调试、前端部署和文档说明等角色。包体仅1.52MB轻量紧凑便于快速下载与部署。整套代码已经测试运行通过功能稳定已有79人学习使用可帮助读者理解基于InceptionV3的迁移学习在矿物图像分类中的实际应用并掌握将深度学习模型转换为JavaScript模型进行端侧部署的方法。1. 矿物薄片鉴定不再是纯眼力活深度学习分类要把“看到什么”变成“是什么”一张矿物显微图像里往往同时出现辉石、角闪石、黑云母甚至还有次生蚀变矿物混在一起传统图像处理方法在颜色阈值和形态特征上绕来绕去换个视域或者换台显微镜就失效。深度学习要做的事情很直接训练一个卷积神经网络输入一张矿物薄片显微照片输出这幅图里最可能包含的矿物类别。这并不是简单的“按相似度找图”而是让模型自己学习矿物在正交偏光、单偏光条件下的纹理、解理、突起、干涉色这些肉眼判据把地质学家的经验转成网络权重。这项工作最现实的价值是能顶住重复性工作。岩矿鉴定里大量时间花在“扫视整个薄片、给主要矿物估含量”上一个能压住主要类别、能容忍多矿物共生的分类器配合少量人工抽检就能把常规鉴定时间压缩到原来的零头。本文按数据准备、模型构建、训练评估、落地上机的顺序展开适合已经跑通过基本 PyTorch 分类任务、想在岩矿数据上做出一个可用方案的工程师和地学方向的研究者。2. 矿物图像的数据规模与预处理决定分类效果的七成2.1 数据从哪来、怎么标颗粒级还是薄片级做矿物显微图像分类第一个要确定的是标注单元。常见做法有两种一种是在单颗矿物颗粒的截面上裁剪出小图给每一张小图标一个矿物名这是“颗粒级”标注另一种是整张视域照片标出“包含哪些矿物”这是“薄片级”标注。颗粒级数据干净、类别边界清晰训练出的模型在单矿物识别上表现好但真实鉴定场景里很少给你切好的单颗粒。薄片级数据更贴近实际但一张图里多种矿物共存就变成多标签问题不能直接用softmax cross entropy处理。我一般建议从颗粒级入手起步先用它把模型骨架跑通再采集薄片级数据做多标签微调。颗粒级数据集的规模不需要到 ImageNet 那种量级每类 300 到 500 张裁剪图就能训练一个可用的分类器前提是裁剪框要贴着矿物颗粒边缘把周围的胶结物尽量去掉。如果手头只有整张薄片扫描图可以先用标签软件标注矿物颗粒的多边形区域再按区域裁剪导出这一步用 LabelMe 或者 X-AnyLabeling 这类工具都能批量完成。标注类目建议按主要造岩矿物来定石英、长石可细分钾长石和斜长石、黑云母、白云母、角闪石、辉石、橄榄石、方解石。如果研究区域特定再加目标矿物类。类目太少模型分不开类目太多标注成本急剧上升尤其像斜长石和钾长石这种在薄片里区别微妙的光性矿物早期版本可以先合并为“长石”后面再拆。2.2 图像预处理参数极化信息不是越多越好矿物薄片图像有个特征同一矿物在单偏光PPL和正交偏光XPL下的表现完全不同。石英在单偏光下无色透明正交偏光下呈现灰白干涉色。公开的矿物薄片数据集通常会同时提供两种光性图像使用方法上有两个派别一种是把 PPL 和 XPL 两幅图当作两个通道拼起来喂给网络另一种是只用其中一种或者做两路输入的孪生结构。从工程简化角度先用单一光性的图像把流程跑通通常用正交偏光因为干涉色特征更丰富。预处理管线按下面这个配置来设在大多数矿物图像上表现稳定预处理步骤参数设置说明缩放尺寸224x224 或 256x256适配 ImageNet 预训练模型的输入尺寸中心裁剪若缩放到256则随机裁剪224配合 ResNet 系列标准输入归一化均值[0.485, 0.456, 0.406]ImageNet 统计值预训练权重需要归一化方差[0.229, 0.224, 0.225]同上增强方式随机旋转、水平翻转、轻微色彩抖动矿物颗粒方向无固定朝向旋转增强非常关键注意矿物图像尽量不要做过于强烈的色彩抖动。干涉色是鉴定矿物的重要依据把色相和饱和度大幅度扰动相当于破坏了矿物鉴别的核心线索。色彩抖动参数一般把brightness限制在 0.3、contrast限制在 0.3、saturation限制在 0.1 以内hue直接设为 0。如果使用了预训练权重归一化参数必须保持 ImageNet 的均值方差不变否则第一层的卷积核遇到过不去的数据分布收敛速度会明显变慢。2.3 目录组织与 Dataset 实现训练数据目录结构直接关系后续脚本能不能被别人看懂尤其标着“完整源码说明”的项目包清晰的数据目录本身就是一种说明。推荐用下面这种组织方式mineral_data/ ├── PPL/ │ ├── quartz/ │ ├── biotite/ │ ├── hornblende/ │ └── ... ├── XPL/ │ ├── quartz/ │ ├── biotite/ │ ├── hornblende/ │ └── ... ├── train.csv └── val.csvtrain.csv 每行包含image_path,label,split三个字段用 CSV 而不是目录推断类别的好处是方便日后加多标签标注。颗粒级起步阶段用下面的 PyTorch Dataset 实现可以同时兼容单标签目录和多标签 CSVimport torch from torch.utils.data import Dataset from PIL import Image import pandas as pd class MineralDataset(Dataset): def __init__(self, csv_file, img_dir, transformNone, multi_labelFalse): self.df pd.read_csv(csv_file) self.img_dir img_dir self.transform transform self.multi_label multi_label def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img_path f{self.img_dir}/{row[image_path]} image Image.open(img_path).convert(RGB) if self.multi_label: # 多标签模式label 字段为空格分隔的类别索引如 3 7 12 labels list(map(float, str(row[label]).split())) target torch.zeros(num_classes) for l in labels: target[int(l)] 1.0 else: target torch.tensor(int(row[label])) if self.transform: image self.transform(image) return image, target这段代码把单标签和多标签统一在同一个 Dataset 接口下后续切换标注策略不用重写数据加载层。multi_labelTrue时返回的是num_classes维的 0/1 向量配合多标签损失函数使用multi_labelFalse时返回的则是单个整数索引配合交叉熵损失。数据划分的比例遵循“验证集不等于测试集”的原则。矿物图像数据集通常不大6000 张以下时按 8:1:1 划分训练、验证、测试如果总量少于 2000 张就别拆测试集了用 5 折交叉验证来评估。类别不平衡是矿物数据里的常态常见矿物如石英样品好采集稀有矿物只有几十张图此时训练集里可以保留全部稀有类别样本只对富样本类别做欠采样。3. 模型选型与训练策略从预训练 ResNet 到多标签输出3.1 为什么不自建 CNN而是用预训练 ResNet矿物显微图像和自然图像差异很大没有 ImageNet 那种大规模矿物预训练模型可用所以标准做法是采用 ImageNet 预训练权重做迁移学习。自建的 5 层、6 层 CNN 从零训练在每类几百张图的小数据上很难收敛效果远不如预训练 ResNet 的最后一层微调。ResNet18 和 ResNet34 是矿物图像分类最常见的起步架构。ResNet 残差连接解决网络加深后的退化问题34 层的深度对矿物纹理特征已经足够再深到 ResNet50 以上容易过拟合除非每类样本数突破 1000。模块化一点的做法是加一个可配置的--arch参数让模型在 resnet18 和 resnet34 之间可切换方便后期验证。模型替换分类头的部分如下import torch.nn as nn from torchvision import models def build_model(num_classes, archresnet34, pretrainedTrue): if arch resnet18: model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None) elif arch resnet34: model models.resnet34(weightsmodels.ResNet34_Weights.IMAGENET1K_V1 if pretrained else None) else: raise ValueError(fUnsupported arch: {arch}) in_features model.fc.in_features # 替换最后一层全连接适配矿物类别数 model.fc nn.Sequential( nn.Dropout(0.3), # 缓解小数据过拟合 nn.Linear(in_features, num_classes) ) return model全连接前面的 2048 维特征是 ResNet 在 ImageNet 上学到的通用视觉表征对边缘、纹理、颜色分布都有响应。矿物解理纹和干涉色的边缘特征与自然图像的纹理结构存在可迁移性这是预训练权重能用、且只需要微调分类头的原因。Dropout(0.3)放在分类头前面比直接接一个线性层有更好的正则效果尤其在样本量不足时能明显减少验证集振荡。如果数据量较少还可以把骨干网络冻结只训练分类头数据量中等以上再逐步解冻最后两层的残差块。3.2 矿物薄片不是单标签任务换成多标签 BCE 思路回到岩矿鉴定的实际场景一个视域内两种乃至三种矿物共生是常态强行用 softmax 选最大概率类别会丢掉“同时存在”的信息。此时分类头不用nn.Linear softmax CrossEntropyLoss而是换成nn.Linear sigmoid BCEWithLogitsLoss。每个类别独立判断“是否存在”不同类别之间互斥关系由数据自己决定。BCEWithLogitsLoss 在数值上把 sigmoid 合并进了损失函数内部比手动F.sigmoid再算BCELoss数值稳定性更好。多标签模式下的模型训练损失和评估指标写法如下import torch.nn.functional as F from torchmetrics.classification import MultilabelF1Score criterion nn.BCEWithLogitsLoss() # 每个 batch 的前向计算 logits model(images) # shape: (batch, num_classes) loss criterion(logits, labels.float()) probs torch.sigmoid(logits) # 转成概率用于阈值判断 # 预测时用阈值 0.5 判定类别存在与否 preds (probs 0.5).int()多标签模式下不能只看 accuracy因为这张图的标签可能含有多个 1正确率只在所有类别同时匹配时才算对对矿物这类共生情况过严。用MultilabelF1Score或者按类别分别计算精确率和召回率重点观察少见矿物类别的 F1。样本不平衡时为稀有矿物类别设更高的惩罚权重通过 BCELoss 的pos_weight参数控制# pos_weight 为每个类别的正样本权重稀有类别给大权重 pos_weight torch.tensor([1.0, 1.0, 3.5, 1.0, 5.0, 1.2, ...]) criterion nn.BCEWithLogitsLoss(pos_weightpos_weight)pos_weight的计算公式是负样本数除以正样本数再乘一个系数。常见做法是先用训练集统计每个类别出现的次数稀有类别如含量最少的矿物的权重是富类别权重的 3 到 5 倍。这个操作能把模型对稀有矿物的召回率从 30% 拉到 60% 以上代价是精确率会有所下降实际使用时按业务需求调节比例。3.3 训练脚本与超参数初始学习率、batch size 和优化器搭配训练矿物分类模型常见优化器选择是 AdamW 而不是普通 SGD。AdamW 的解耦权重衰减对小数据集更友好不易出现 SGD 在调参不当时那种振荡。对应骨干网络和分类头使用不同学习率是目前效果稳定的策略骨干网络冻结时只训练分类头此时用 1e-3解冻骨干网络后骨干层学习率降到 1e-4分类头仍然用 1e-3这样预训练特征不会被大规模破坏。训练参数的可抄作业配置如下参数建议值说明优化器AdamWSGD 需要更长训练周期AdamW 收敛更快骨干网络学习率1e-4微调阶段破坏预训练特征分类头学习率1e-3新随机初始化的层需要更大学习率权重衰减1e-4对小数据集防止过拟合batch size32224x224 输入时单卡显存约 6-8 GB训练轮数30-50早停 patience 设为 10学习率调度CosineAnnealingLR相比 StepLR 更平滑末端收敛好训练过程的代码骨架from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR model build_model(num_classesnum_classes) optimizer AdamW([ {params: model.fc.parameters(), lr: 1e-3}, {params: model.parameters(), lr: 1e-4} ], weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_maxepochs) scaler torch.cuda.amp.GradScaler() # 混合精度加速 for epoch in range(epochs): model.train() for images, labels in train_loader: optimizer.zero_grad() with torch.cuda.amp.autocast(): logits model(images) loss criterion(logits, labels.float()) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step() # 每个 epoch 后跑一次验证集记录 F1保存最优权重torch.cuda.amp.autocast是混合精度训练的入口对显存紧张的机器能省接近一半显存训练速度提升 30% 到 50%。需要留意的是 AMP 下梯度数值范围会变化如果 loss 出现 NaN优先排查scaler.scale是否被正确调用或者把GradScaler初始化时的init_scale值降低。早停机制通过监控验证集 F1 实现连续 10 个 epoch 没有提升就加载历史最优权重退出训练。4. 评估与调优混淆矩阵和 Grad-CAM 定位模型的“矿物眼力”4.1 用混淆矩阵看矿物类别之间的具体混淆模式只看总准确率在矿物图像任务里很难暴露问题。石英和长石在正交偏光下都是浅色干涉色角闪石和黑云母在单偏光下都是深色模型最容易在这些光性相似的对子上面出错。混淆矩阵能直接告诉你模型在哪两类之间反复横跳进而决定要不要补样本、加特征或者合并类目。用下面这段代码输出训练过程保存的最优权重在测试集上的结果import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix, classification_report model.load_state_dict(torch.load(best_model.pth)) model.eval() all_preds, all_labels [], [] with torch.no_grad(): for images, labels in test_loader: logits model(images) preds torch.sigmoid(logits) if multi_label else torch.softmax(logits, dim1) preds (preds 0.5).int() if multi_label else preds.argmax(dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 多标签模式下按类别展开成二分类混淆矩阵 for i, cls in enumerate(class_names): y_true [1 if label[i] else 0 for label in all_labels] y_pred [1 if pred[i] else 0 for pred in all_preds] cm confusion_matrix(y_true, y_pred) plt.figure() sns.heatmap(cm, annotTrue, fmtd) plt.title(f{cls} confusion matrix) plt.savefig(fcm_{cls}.png)分类报告用classification_report单独输出每个类别的精确率、召回率和 F1。如果某个类的召回率显著低于精确率说明模型倾向于不预测这个类优先排查该类别的样本量是否过少、图像是否与另一个类过于相似。例如角闪石和黑云母都是单偏光下多色性明显的深色矿物如果两者混淆集中一个直接可用的调整方式是针对这两个类采集更多具有典型解理角度的样本前提是鉴定特征如角闪石的两组解理夹角 124 度与黑云母的极完全解理在图像上有区分度。4.2 Grad-CAM 可视化网络分类时在看矿物的哪个部位矿物显微图像分类的模型不能做黑盒“这图是石英”和“因为看到了石英的特征干涉色所以是石英”对地质人员来说完全不同。Grad-CAM 是一种用梯度定位网络注意力热区的方法把最终类别对应的梯度回传到最后一个卷积层输出一张跟输入图像同尺寸的热力图。用一个轻量的 Grad-CAM 实现来看预测依据class GradCAM: def __init__(self, model, target_layer): self.model model self.gradients None self.activations None target_layer.register_forward_hook(self.save_activation) target_layer.register_full_backward_hook(self.save_gradient) def save_activation(self, module, input, output): self.activations output.detach() def save_gradient(self, module, grad_input, grad_output): self.gradients grad_output[0].detach() def generate(self, image, class_idxNone): logits self.model(image.unsqueeze(0)) if class_idx is None: class_idx torch.argmax(logits, dim1).item() self.model.zero_grad() logits[0, class_idx].backward() weights self.gradients.mean(dim(2, 3), keepdimTrue) cam (weights * self.activations).sum(dim1, keepdimTrue) cam F.relu(cam) cam F.interpolate(cam, sizeimage.shape[1:], modebilinear) return cam.squeeze().cpu().numpy()使用目标层是model.layer4[-1]这是最后一个残差块的输出特征层语义信息最丰富。把热力图叠加到原始偏振光照片上能够直观判断模型到底关注了颗粒的边界、解理缝还是整个区域。如果热力图大面积落在胶结物或者背景区域说明模型“走偏了”它在用背景特征做判断而不是矿物本身此时要检查训练裁剪是否干净、数据增强里是否有非目标区域的扰动。4.3 分类精度上不去的五个调试方向矿物图像分类前期最常见的瓶颈排名如下按排查顺序排列排查方向检查手段典型表现标签噪声随机抽 200 张训练图请地质人员复核验证集 loss 反复震荡类别不均衡查看训练集类别分布柱状图稀有类召回率低于 30%数据量不足每类低于 150 张时优先扩样本训练 F1 高、验证 F1 骤降预处理与真实采集环境不一致在测试集上叠加实际显微镜拍摄参数微调训练集表现正常外部数据效果差光性模式单一单偏光数据训练的模型直接测正交偏光图泛化失败、注意力区域漂移标签噪声在矿物数据里很常见因为矿物颗粒边缘常有蚀变边或者包裹体标注人员容易把这类混合区域归到主导矿物一类。解决方式是标注时给每个裁剪图设“置信度”字段先把置信度低的数据用训练好的模型预标注一遍再让人工复核模型输出和原始标签不一致的样本。5. 落地技巧把训练好的模型变成薄片鉴定助手并验证真实场景表现模型训练完成后工程问题就变成如何部署和自验证。常见做法是写一个独立的推理脚本predict.py它接收一张图像路径输出预测矿物列表和置信度。注意推理时的预处理必须和训练时保持一致一个很隐蔽的坑是训练时用了RandomResizedCrop但推理时不能随机裁剪要换用CenterCrop否则同一张图每次预测结果会不同。推理脚本的预测部分要处理两件事对单颗粒裁剪图直接输出概率对整张薄片扫描图先做滑动窗口切块再汇总多个窗口的预测结果。滑动窗口切块的推荐尺寸是 224 像素、步长 112 像素重叠 50%这样矿物颗粒跨窗口时不会被截断。汇总时统计所有窗口各类别被预测出现的频率含量估算用频率值换算import torch from torchvision import transforms # 推理时的预处理必须和训练时的验证集一致 infer_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def predict_single_image(model, image_path, top_k3): image Image.open(image_path).convert(RGB) tensor infer_transform(image).unsqueeze(0) model.eval() with torch.no_grad(): logits model(tensor) probs torch.sigmoid(logits) # 多标签模式下每个类别独立概率 # 按预测概率从高到低排序返回 top_k 个类别 top_probs, top_indices torch.topk(probs, ktop_k) result [] for prob, idx in zip(top_probs[0], top_indices[0]): result.append((class_names[idx.item()], prob.item())) return result为了验证模型在真实薄片上的表现留一个与训练数据采集条件不同的验证集很重要。比如训练数据来自 A 显微镜和固定光源强度验证时用 B 显微镜在不同曝光条件下拍摄的薄片照这才能看出模型是否过拟合到特定成像环境。实际验证中发现精度下降超过 15 个百分点优先做色彩归一化或直方图匹配把不同显微镜的色彩差异拉到一个分布上。最后一个实用技巧是用 threshold 扫描选择预测阈值。多标签模型默认用 0.5 判定类别是否存在但矿物数据类别不平衡时0.5 往往对稀有类别太苛刻。在验证集上对每个类别单独扫描 0.2 到 0.8 之间的阈值选 F1 最高的那个作为该类的判定阈值。这个操作能有效提升对稀有矿物的召回率实现后保存阈值到 JSON 文件预测时读取对应类别阈值替代固定 0.5。模型从“给出类别概率”升级为“按业务标准做存在性判断”这个细节在真实岩矿鉴定流程里比继续调模型架构更实用。本文还有配套的精品资源点击获取