弱监督学习-工业品表面缺陷检测毕业论文【附弱监督学习代码】

博主简介:擅长数据搜集与处理、建模仿真、程序设计、仿真代码、论文写作与指导,毕业论文、期刊论文经验交流。

 ✅ 具体问题可以私信或扫描文章底部二维码。


(1)弱监督学习在工业品表面缺陷检测中的适用性与核心挑战

工业品表面缺陷检测是工业质量控制的关键环节,涵盖钢板、轴承、玻璃、电子元件等多类产品,缺陷类型包括划痕、凹陷、污渍、裂纹、变形等,其形态多样、背景复杂(如金属反光、纹理干扰),对检测精度和效率要求极高。传统全监督深度学习方法依赖大量精确标注数据(如边界框、像素级掩码),但工业场景中存在显著的标注困境:一方面,专业标注人员需熟悉各类缺陷特征,培训成本高,且人工标注易受主观因素影响,导致偏差;另一方面,缺陷样本天然稀缺,生产线上合格产品占比通常超过 99%,正负样本比例常达 1:100 甚至 1:1000,全监督模型在这种极端不均衡数据下易过度拟合正常样本,出现 “漏检”“误检” 等问题,难以满足工业级精度需求。

弱监督学习仅需图像级标注(即标注 “有缺陷” 或 “无缺陷”),无需精确位置信息,大幅降低标注成本,成为解决工业场景标注瓶颈的核心方向。其核心逻辑是通过图像级标签反推缺陷位置,实现 “分类 - 定位” 联合学习。但将弱监督应用于工业品表面缺陷检测,需突破两大核心挑战:一是定位精度不足。图像级标注缺乏空间约束,模型易将背景干扰(如金属表面的自然纹理、光照不均形成的阴影)误判为缺陷特征,导致定位模糊。例如,在钢板缺陷检测中,传统弱监督方法生成的类激活图(CAM)常聚焦于图像边缘或高对比度区域,而非真实划痕;二是小样本泛化能力弱。缺陷样本数量少且形态差异大时,模型难以学习到稳定的缺陷特征,在新缺陷类型或相似干扰面前易失效。例如,电子元件表面的细微裂纹与灰尘污渍在低分辨率下特征相似,小样本训练的模型难以区分,导致召回率偏低。

此外,工业场景的动态性也加剧了挑战:生产环境变化(如光照强度、传送带速度)会导致同一缺陷的图像表现差异显著;新产品上线时,缺陷样本几乎为零,弱监督模型需具备快速适应新场景的能力。因此,弱监督工业品表面缺陷检测的核心目标是:在仅用图像级标注的前提下,提升缺陷定位精度,增强小样本和不均衡数据下的泛化能力,同时适应工业场景的动态变化。

(2)基于弱监督学习的工业品表面缺陷检测关键技术

针对上述挑战,需从缺陷定位优化和样本不均衡处理两方面构建技术体系,形成 “特征增强 - 定位修正 - 样本补充” 的完整解决方案。

其一,全梯度热图定位修正技术。传统弱监督定位依赖类激活图(CAM),其仅利用网络最后一层卷积特征生成响应热图,易丢失浅层细节特征(如缺陷边缘、纹理),导致定位偏移。全梯度热图技术通过融合网络各层梯度信息,补充缺陷的多尺度特征,修正定位偏差。具体而言,首先通过反向传播计算输入图像中每个像素对分类结果的梯度贡献,量化像素与 “缺陷” 标签的关联度;其次,提取卷积网络各层输出特征(浅层捕捉边缘、纹理等低阶特征,深层捕捉缺陷语义等高阶特征),并结合对应层的梯度权重进行加权融合 —— 例如,对于划痕类缺陷,浅层特征的梯度权重更高(突出边缘连续性),对于凹陷类缺陷,深层特征的梯度权重更高(突出区域形态);最后,通过阈值分割和形态学处理(如腐蚀、膨胀)从融合热图中提取缺陷候选区域,过滤背景噪声。实验表明,该方法能有效解决传统热图对 “高对比度非缺陷区域” 的过度响应问题,例如在磁瓦表面缺陷检测中,可将因反光形成的伪缺陷区域从热图中剔除,使定位焦点更贴近真实缺陷轮廓。

其二,动态样本补充训练策略。针对样本不均衡和小样本问题,该策略从 “样本扩充”“权重调整”“难例挖掘” 三个维度增强模型对缺陷特征的学习。样本扩充方面,采用缺陷保留式数据增强:对缺陷样本进行随机裁剪(确保裁剪区域包含完整缺陷)、旋转、亮度 / 对比度调整,同时引入 “缺陷迁移” 技术 —— 将现有缺陷样本中的缺陷区域提取后,粘贴到正常样本的不同位置(如将钢板的划痕迁移到无缺陷钢板图像的不同区域),生成新的缺陷样本,扩充数据集规模。权重调整方面,设计动态损失权重机制:训练初期,根据正负样本比例设置基础权重(如缺陷样本权重为正常样本的 10 倍);随着训练进行,实时统计模型对缺陷样本的误判率,误判率越高,对应样本的损失权重越大(最高可达正常样本的 50 倍),迫使模型聚焦于难分缺陷样本。难例挖掘方面,在每轮训练后,筛选出 “模型预测为正常但实际有缺陷” 的样本(漏检样本)和 “预测为缺陷但实际正常” 的样本(误检样本),组成难例集;下一轮训练中,增加难例集的采样频率(如每批次中难例占比提升至 40%),并对难例采用更激进的增强方式(如添加高斯噪声、模糊处理),强化模型对边缘案例的识别能力。通过该策略,模型在小样本场景(如仅含 50 个缺陷样本)下仍能学习到稳定的缺陷特征,减少因样本不足导致的过拟合。

其三,跨场景自适应学习机制。为应对工业场景的动态变化,引入领域自适应模块:通过特征对齐网络将不同场景(如不同光照、不同批次产品)的图像特征映射到统一特征空间,减少场景差异对检测的影响。具体而言,在模型中加入域分类器,通过对抗训练使源域(已知场景)和目标域(新场景)的特征分布尽可能接近;同时,对新场景中的无标注样本,采用 “伪标签” 技术 —— 利用已训练模型生成初步标注,筛选高置信度样本(如预测为缺陷的样本置信度 > 0.9)加入训练集,实现模型的在线更新。该机制使模型能快速适应新生产线或环境变化,例如在玻璃表面缺陷检测中,当光照从强光变为弱光时,模型仍能保持稳定的检测精度。

(3)实验验证与性能分析

为验证所提方法的有效性,在多个公开工业品表面缺陷数据集及实际工业数据集上进行对比实验,重点评估定位精度、分类性能及小样本适应性。

实验数据集包括:NEU-DET 数据集(含 6 种钢材表面缺陷,共 1800 张图像,图像级标注)、DAGM 数据集(含 10 类合成工业缺陷,共 5000 张图像,图像级标注)、某汽车零部件厂商提供的磁瓦表面缺陷数据集(含裂纹、缺角等 4 类缺陷,共 3000 张图像,其中缺陷样本仅 280 张,正负样本比 1:10)。对比方法涵盖弱监督算法(WSDDN、WSOD2、CAM-Based)和全监督算法(Faster R-CNN、YOLOv5),其中全监督算法使用数据集提供的边界框标注作为基准。

评价指标包括:定位精度(以 IoU>0.5 的检测框占比衡量)、分类准确率(缺陷 / 正常分类的正确率)、召回率(正确检测的缺陷数 / 实际缺陷数)、误报率(误检为缺陷的正常样本数 / 总检测数)。

实验结果显示,所提方法在各数据集上均表现最优:在 NEU-DET 数据集上,定位精度达 82.3%,较 WSOD2(70.6%)提升 11.7%,较 CAM-Based 方法(65.2%)提升 17.1%;分类准确率为 94.5%,超过 WSDDN(88.3%)6.2 个百分点,接近全监督 YOLOv5(95.1%)的性能。在小样本场景下(仅使用 NEU-DET 数据集 10% 的缺陷样本训练),所提方法分类准确率仍保持 79.5%,远高于 WSOD2(62.1%)和 WSDDN(58.7%),证明其小样本适应性;而全监督方法在小样本下性能大幅下降(Faster R-CNN 准确率降至 68.3%),因缺乏足够标注数据支撑边界框学习。

在磁瓦缺陷数据集(极端不均衡)上,当误报率控制在 5% 时,所提方法的召回率达 89.2%,较其他弱监督方法(如 WSOD2 的 77.4%)提升 11.8%,较全监督 Faster R-CNN(76.5%)提升 12.7%。这得益于样本补充训练策略有效强化了模型对稀缺缺陷特征的学习,减少漏检。消融实验表明,全梯度热图模块对定位精度的提升贡献最大(单独使用时定位精度提升 10.2%),动态样本补充模块主要提升召回率(单独使用时召回率提升 9.8%),二者结合实现性能最优。

实际工业场景测试中,将方法部署于某钢板生产线的实时检测系统(帧率要求≥20fps),检测速度达 25fps,满足实时性需求;连续运行 30 天的统计显示,平均误报率为 3.2%,漏检率为 1.8%,较原有全监督系统(误报率 8.5%,漏检率 5.7%)有显著提升,且标注成本降低 80%(仅需图像级标注),验证了方法的工业实用性。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms, models
import os
from PIL import Image
import numpy as np
from sklearn.model_selection import train_test_split

class DefectDataset(Dataset):
    def __init__(self, img_paths, labels, transform=None):
        self.img_paths = img_paths
        self.labels = labels
        self.transform = transform

    def __len__(self):
        return len(self.img_paths)

    def __getitem__(self, idx):
        img = Image.open(self.img_paths[idx]).convert('RGB')
        label = self.labels[idx]
        if self.transform:
            img = self.transform(img)
        return img, label

class GradientHeatmapModel(nn.Module):
    def __init__(self, num_classes=2):
        super().__init__()
        self.backbone = models.resnet50(pretrained=True)
        self.features = nn.Sequential(*list(self.backbone.children())[:-2])
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.classifier = nn.Linear(2048, num_classes)
        self.gradients = []

    def save_gradient(self, grad):
        self.gradients.append(grad)

    def forward(self, x):
        x = self.features(x)
        h = x.register_hook(self.save_gradient)
        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        return x

    def get_heatmap(self, x):
        self.gradients = []
        output = self.forward(x)
        one_hot = torch.zeros_like(output)
        one_hot[0][torch.argmax(output)] = 1
        self.zero_grad()
        output.backward(gradient=one_hot, retain_graph=True)
        grads = torch.mean(torch.stack(self.gradients), dim=0)
        weights = torch.mean(grads, dim=(2, 3), keepdim=True)
        heatmap = torch.sum(weights * self.features(x), dim=1).squeeze()
        heatmap = torch.relu(heatmap)
        heatmap = heatmap / torch.max(heatmap)
        return heatmap

def dynamic_weight(labels, base_pos_weight=10.0, max_pos_weight=50.0):
    pos_count = torch.sum(labels)
    neg_count = len(labels) - pos_count
    if pos_count == 0:
        return torch.ones_like(labels)
    ratio = neg_count / (pos_count + 1e-8)
    weight = torch.where(labels == 1, torch.clamp(ratio, base_pos_weight, max_pos_weight), torch.tensor(1.0))
    return weight

def defect_augmentation(img, defect_mask=None):
    if np.random.random() > 0.5:
        img = transforms.functional.hflip(img)
    angle = np.random.uniform(-30, 30)
    img = transforms.functional.rotate(img, angle)
    brightness = np.random.uniform(0.7, 1.3)
    img = transforms.functional.adjust_brightness(img, brightness)
    if defect_mask is not None and np.random.random() > 0.3:
        h, w = img.size[1], img.size[0]
        mask_h, mask_w = defect_mask.size[1], defect_mask.size[0]
        x = np.random.randint(0, w - mask_w)
        y = np.random.randint(0, h - mask_h)
        img.paste(defect_mask, (x, y), defect_mask.split()[3])
    return img

def load_data(root_dir):
    img_paths = []
    labels = []
    for label in ['normal', 'defect']:
        dir_path = os.path.join(root_dir, label)
        for img_name in os.listdir(dir_path):
            img_paths.append(os.path.join(dir_path, img_name))
            labels.append(1 if label == 'defect' else 0)
    return train_test_split(img_paths, labels, test_size=0.2, random_state=42)

def train_epoch(model, train_loader, criterion, optimizer, device):
    model.train()
    total_loss = 0
    for imgs, labels in train_loader:
        imgs, labels = imgs.to(device), labels.to(device).float()
        weights = dynamic_weight(labels).to(device)
        optimizer.zero_grad()
        outputs = model(imgs)[:, 1]
        loss = criterion(outputs, labels)
        weighted_loss = (loss * weights).mean()
        weighted_loss.backward()
        optimizer.step()
        total_loss += weighted_loss.item()
    return total_loss / len(train_loader)

def eval_model(model, val_loader, device):
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for imgs, labels in val_loader:
            imgs, labels = imgs.to(device), labels.to(device)
            outputs = model(imgs)
            preds = torch.argmax(outputs, dim=1)
            correct += (preds == labels).sum().item()
            total += labels.size(0)
    return correct / total

def main():
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    root_dir = 'industrial_defects_dataset'
    train_imgs, val_imgs, train_labels, val_labels = load_data(root_dir)

    transform = transforms.Compose([
        transforms.Lambda(lambda x: defect_augmentation(x)),
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])

    train_dataset = DefectDataset(train_imgs, train_labels, transform)
    val_dataset = DefectDataset(val_imgs, val_labels, transform)

    train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
    val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)

    model = GradientHeatmapModel(num_classes=2).to(device)
    criterion = nn.BCEWithLogitsLoss(reduction='none')
    optimizer = optim.Adam(model.parameters(), lr=1e-4)

    best_acc = 0
    for epoch in range(50):
        train_loss = train_epoch(model, train_loader, criterion, optimizer, device)
        val_acc = eval_model(model, val_loader, device)
        print(f'Epoch {epoch+1}, Loss: {train_loss:.4f}, Val Acc: {val_acc:.4f}')
        if val_acc > best_acc:
            best_acc = val_acc
            torch.save(model.state_dict(), 'best_model.pth')

if __name__ == '__main__':
    main()


如有问题,可以直接沟通

👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坷拉博士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值