主要内容

本页采用了机器翻译。点击此处可查看英文原文。

利用可解释的 FCDD 网络检测图像异常

本示例演示了如何使用单类全卷积数据描述 (FCDD) 异常检测网络,对药片图像中的缺陷进行检测。

异常检测的一个关键目标是,让人类观察者能够理解,为什么经过训练的神经网络会将某些图像分类为异常。FCDD 实现了 e 可解释分类,通过补充申述神经网络如何得出分类决策的信息,从而完善了类别预测结果 [1]。FCDD 网络返回一张热力图,其中每个像素的异常概率均以热力图形式呈现。分类器根据异常分数热图的平均值,将图像标记为“正常”或“异常”。

下载用于分类数据集的药片图像

本示例使用 PillQC 数据集。该数据集包含三类图像:normal 类为无缺陷图像,chip 类为药丸中存在芯片缺陷的图像,dirt 类为存在污渍污染的图像。该数据集包含 149 张 normal 图像、43 张 chip 图像和 138 张 dirt 图像。数据集的大小为 3.57 MB。

dataDir 设置为数据集的指定位置。使用 downloadPillQCData 辅助函数下载数据集。此函数作为支持文件包含在本示例中。该函数会下载一个 ZIP 文件,并将数据解压到 chipdirtnormal 这三个子目录中。

dataDir = fullfile(tempdir,"PillDefects");
downloadPillQCData(dataDir)

这张图展示了每个类别中的一张示例图像。左边是一颗没有瑕疵的正常药片,中间是一颗沾有污垢的药片,右边是一颗有缺口瑕疵的药片。尽管该数据集中的图像包含阴影、焦外模糊和背景颜色变化等情况,但本示例中采用的方法对这些图像采集产生的伪影具有稳健性。

montageImage.png

加载和预处理数据

创建一个用于读取和管理图像数据的 imageDatastore。请根据图像所在目录的名称,将每个图像分别命名为 chipdirtnormal

imageDir = fullfile(dataDir,"pillQC-main","images");
imds = imageDatastore(imageDir,IncludeSubfolders=true,LabelSource="foldernames");

将数据划分为训练集、标定集和测试集

使用 splitAnomalyData 函数创建训练集、标定集和测试集。本示例实现了一种基于异常值暴露的 FCDD 方法,其中训练数据主要由正常图像组成,并掺入少量异常图像。尽管该模型主要仅基于正常场景的样本进行训练,但它仍学会了如何区分正常场景和异常场景。

在训练数据集中,将正常图像的比例设定为 50%,并将每类异常图像的比例设定为 5%。将正常图像的 10% 以及每类异常图像的 20% 分配到标定集。将剩余的图像分配到测试集。

normalTrainRatio  = 0.5;
anomalyTrainRatio = 0.05;
normalCalRatio  = 0.10;
anomalyCalRatio = 0.20;
normalTestRatio  = 1 - (normalTrainRatio + normalCalRatio);
anomalyTestRatio = 1 - (anomalyTrainRatio + anomalyCalRatio);

anomalyClasses = ["chip","dirt"];
[imdsTrain,imdsCal,imdsTest] = splitAnomalyData(imds,anomalyClasses, ...
    NormalLabelsRatio=[normalTrainRatio normalCalRatio normalTestRatio], ...
    AnomalyLabelsRatio=[anomalyTrainRatio anomalyCalRatio anomalyTestRatio]);
Splitting anomaly dataset
-------------------------
* Finalizing... Done.
* Number of files and proportions per class in all the datasets:

                     Input                  Train                Validation                Test        
              NumFiles     Ratio     NumFiles     Ratio      NumFiles     Ratio     NumFiles     Ratio 
              ___________________    ____________________    ___________________    ___________________

    chip         43        0.1303        2        0.02381        9       0.17647       32        0.1641
    dirt        138       0.41818        7       0.083333       28       0.54902      103       0.52821
    normal      149       0.45152       75        0.89286       14       0.27451       60       0.30769

将训练数据进一步划分为两个数据集,其中一个仅包含正常数据,另一个仅包含异常数据。

[imdsNormalTrain,imdsAnomalyTrain] = splitAnomalyData(imdsTrain,anomalyClasses, ...
    NormalLabelsRatio=[1 0 0],AnomalyLabelsRatio=[0 1 0],Verbose=false);

增强训练数据

通过使用 transform 函数,并结合由辅助函数 augmentDataForPillAnomalyDetector 指定的自定义预处理操作,来扩充训练数据。该辅助函数已作为支持文件附在示例中。

augmentDataForPillAnomalyDetector 函数会对每张输入图像随机应用 90 度旋转以及水平和垂直翻转。

imdsNormalTrain = transform(imdsNormalTrain,@augmentDataForPillAnomalyDetector);
imdsAnomalyTrain = transform(imdsAnomalyTrain,@augmentDataForPillAnomalyDetector);

使用 transform 函数,并结合 addLabelData 辅助函数指定的运算,向标定数据集和测试数据集添加二进制标签。该辅助函数定义在本示例的末尾,它将 normal 类中的图像分配二进制标签 0,并将 chipdirt 类中的图像分配二进制标签 1

dsCal = transform(imdsCal,@addLabelData,IncludeInfo=true);
dsTest = transform(imdsTest,@addLabelData,IncludeInfo=true);

可视化九张增强训练图像的样本。

exampleData = readall(subset(imdsNormalTrain,1:9));
montage(exampleData(:,1));

创建 FCDD 模型

本示例使用全卷积数据描述 (FCDD) 模型 [1]。FCDD 的基本思路是训练一个神经网络,使其生成一张异常评分图,该图描述了输入图像中每个区域包含异常内容的概率。

pretrainedEncoderNetwork 函数返回 ImageNet 预训练的 Inception-v3 网络的前三个下采样层,以便将其用作预训练网络。

backbone = pretrainedEncoderNetwork("inceptionv3",3);

使用 fcddAnomalyDetector 函数并结合 Inception-v3 骨干网络,构建一个 FCDD 异常检测网络。

net = fcddAnomalyDetector(backbone);

训练网络或下载预训练网络

默认情况下,本示例会使用辅助函数 downloadTrainedNetwork 下载 FCDD 异常检测器的预训练版本。该辅助函数作为辅助文件附于本示例之后。您可以使用预训练网络直接运行整个示例,无需等待训练完成。

要训练网络,请将以下代码中的 doTraining 变量设置为 true。请在该字段中输入一个值,以指定用于训练 numEpochs 的 epoch 数。使用 trainFCDDAnomalyDetector 函数训练模型。

如果可用,请在一块或多块 GPU 上进行训练。使用 GPU 需要 Parallel Computing Toolbox™ 和支持 CUDA® 的 NVIDIA® GPU。有关详细信息,请参阅GPU 计算要求 (Parallel Computing Toolbox)。在 NVIDIA Titan RTX™ 上,训练大约需要 3 分钟。

doTraining = false;
numEpochs = 200;
if doTraining
    options = trainingOptions("adam", ...
        Shuffle="every-epoch",...
        MaxEpochs=numEpochs,InitialLearnRate=1e-4, ...
        MiniBatchSize=32,...
        BatchNormalizationStatistics="moving");
    detector = trainFCDDAnomalyDetector(imdsNormalTrain,imdsAnomalyTrain,net,options);
    modelDateTime = string(datetime("now",Format="yyyy-MM-dd-HH-mm-ss"));
    save(fullfile(dataDir,"trainedPillAnomalyDetector-"+modelDateTime+".mat"),"detector");
else
    trainedPillAnomalyDetectorNet_url = "https://ssd.mathworks.com/supportfiles/"+ ...
        "vision/data/trainedFCDDPillAnomalyDetectorSpkg.zip";
    downloadTrainedNetwork(trainedPillAnomalyDetectorNet_url,dataDir);
    load(fullfile(dataDir,"folderForSupportFilesInceptionModel", ...
        "trainedPillFCDDNet.mat"));
end

设置异常阈值

为异常检测器选择一个异常分数阈值,该检测器会根据图像的分数是高于还是低于该阈值来对图像进行分类。本示例使用了一个包含正常图像和异常图像的标定数据集来选择阈值。

获取标定集中的每个图像的平均异常得分和真实值。

scores = predict(detector,dsCal);
labels = imdsCal.Labels ~= "normal";

绘制正常类和异常类平均异常分数的直方图。这些分布通过建模预测的异常分数得到了很好的区分。

numBins = 20;
[~,edges] = histcounts(scores,numBins);
figure
hold on
hNormal = histogram(scores(labels==0),edges);
hAnomaly = histogram(scores(labels==1),edges);
hold off
legend([hNormal,hAnomaly],"Normal","Anomaly")
xlabel("Mean Anomaly Score")
ylabel("Counts")

使用 anomalyThreshold 函数计算最佳异常阈值。将前两个参量指定为标定数据集的真实值 (labels) 和预测异常分数 (scores)。将第三个参量指定为 true,因为真正的正异常图像的 labels 值是 trueanomalyThreshold 函数返回该检测器的最优阈值和接收者操作特征 (ROC) 曲线,并将其存储为一个 rocmetrics (Deep Learning Toolbox) 对象。

[thresh,roc] = anomalyThreshold(labels,scores,true);

将异常检测器的 Threshold 属性设置为最优值。

detector.Threshold = thresh;

使用 rocmetricsplot (Deep Learning Toolbox) 对象函数绘制 ROC 曲线。ROC 曲线展示了分类器在各种可能值下的性能。ROC 曲线上的每个点代表使用不同的阈值对标定集图像进行分类时的假阳性率(x-坐标)和真阳性率(y-坐标)。实心蓝线代表 ROC 曲线。红色虚线代表一个成功率为 50% 的无技能分类器。ROC 曲线下面积 (AUC) 度量反映了分类器的性能,而对应于完美分类器的最大 ROC AUC 值为 1.0。

plot(roc)
title("ROC AUC: "+ roc.AUC)

评估分类模型

将测试集中的每个图像分类为“正常”或“异常”。

testSetOutputLabels = classify(detector,dsTest);

获取每张测试图像的真实值。

testSetTargetLabels = dsTest.UnderlyingDatastores{1}.Labels;

使用 evaluateAnomalyDetection 函数计算性能度量,对异常检测器进行评估。该函数计算了若干度量,用于评估检测器在测试数据集上的准确率、精确率、灵敏度和特异度。

metrics = evaluateAnomalyDetection(testSetOutputLabels,testSetTargetLabels,anomalyClasses);
Evaluating anomaly detection results
------------------------------------
* Finalizing... Done.
* Data set metrics:

    GlobalAccuracy    MeanAccuracy    Precision    Recall     Specificity    F1Score    FalsePositiveRate    FalseNegativeRate
    ______________    ____________    _________    _______    ___________    _______    _________________    _________________

       0.96923          0.97778           1        0.95556         1         0.97727            0                0.044444     

metricsConfusionMatrix 属性包含测试集的混淆矩阵。提取混淆矩阵并绘制混淆图。例如本示例中的分类模型非常准确,预测出的假阳性率和假阴性率都很低。

M = metrics.ConfusionMatrix{:,:};
confusionchart(M,["Normal","Anomaly"])
acc = sum(diag(M)) / sum(M,"all");
title("Accuracy: "+acc)

如果您指定了多个异常类标签(例如示例中的 dirtchip),则 evaluateAnomalyDetection 函数将分别计算整个数据集以及每个异常类的度量。各类别的度量会通过 anomalyDetectionMetrics 对象 (metrics) 的 ClassMetrics 属性返回。

metrics.ClassMetrics
ans=2×2 table
         1    1×1 table
    0.9556    2×1 table

metrics.ClassMetrics(2,"AccuracyPerSubClass").AccuracyPerSubClass{1}
ans=2×1 table
    0.8438
    0.9903

解释分类决策

您可以利用异常检测器预测的异常热力图,来帮助解释一个图像为何被归类为正常或异常。这种方法有助于识别假阴性结果和假阳性结果中的规律。您可以利用这些模式,找出提高训练数据类平衡度或提升网络性能的策略。

计算异常热力图的显示范围

计算一个显示范围,该范围应反映整个标定集(包括正常图像和异常图像)中观察到的异常分数范围。在所有图像中使用相同的显示范围,比将每个图像分别缩放至其自身的最小值和最大值,能更方便地进行图像比较。将此显示范围应用于本示例中的所有热力图。

minMapVal = inf;
maxMapVal = -inf;
reset(dsCal)
while hasdata(dsCal)
    img = read(dsCal);
    map = anomalyMap(detector,img{1});
    minMapVal = min(min(map,[],"all"),minMapVal);
    maxMapVal = max(max(map,[],"all"),maxMapVal);
end
displayRange = [minMapVal,maxMapVal];

查看异常图像的热力图

请选择一张已正确分类的异常图像。该结果属于真阳性分类。显示图像。

testSetAnomalyLabels = testSetTargetLabels ~= "normal";
idxTruePositive = find(testSetAnomalyLabels' & testSetOutputLabels,1,"last");
dsExample = subset(dsTest,idxTruePositive);
img = read(dsExample);
img = img{1};
map = anomalyMap(detector,img);
imshow(anomalyMapOverlay(img,map,MapRange=displayRange,Blend="equal"))

查看正常图像的热力图

选择并显示一张已被正确分类的正常图像。该结果属于真阴性分类。

idxTrueNegative = find(~(testSetAnomalyLabels' | testSetOutputLabels));
dsExample = subset(dsTest,idxTrueNegative);
img = read(dsExample);
img = img{1};
map = anomalyMap(detector,img);
imshow(anomalyMapOverlay(img,map,MapRange=displayRange,Blend="equal"))

查看假阴性图像的热力图

假阴性是指存在药片缺陷异常的图像,但被网络分类为正常。利用网络给出的解释来深入了解分类错误的原因。

找出测试集中所有假阴性图像。使用 transform 函数获取假阴性图像的热图叠加图。该变换器的操作由一个匿名函数指定,该函数调用 anomalyMapOverlay 函数,以针对测试集中的每个假阴性结果生成热图叠加层。

falseNegativeIdx = find(testSetAnomalyLabels' & ~testSetOutputLabels);
if ~isempty(falseNegativeIdx)
    fnExamples = subset(dsTest,falseNegativeIdx);
    fnExamplesWithHeatmapOverlays = transform(fnExamples,@(x) {...
        anomalyMapOverlay(x{1},anomalyMap(detector,x{1}), ...
        MapRange=displayRange,Blend="equal")});
    fnExamples = readall(fnExamples);
    fnExamples = fnExamples(:,1);
    fnExamplesWithHeatmapOverlays = readall(fnExamplesWithHeatmapOverlays);
    montage(fnExamples)
    montage(fnExamplesWithHeatmapOverlays)
else
    disp("No false negatives detected.")
end

查看误报图像的热力图

误报是指那些没有药片缺陷异常,但被网络分类为异常的图像。找出测试集中的任何误报。利用网络给出的解释来深入了解分类错误的原因。例如,如果异常分数仅出现在图像背景中,可以在预处理阶段尝试抑制背景。

falsePositiveIdx = find(~testSetAnomalyLabels' & testSetOutputLabels);
if ~isempty(falsePositiveIdx)
    fpExamples = subset(dsTest,falsePositiveIdx);
    fpExamplesWithHeatmapOverlays = transform(fpExamples,@(x) { ...
        anomalyMapOverlay(x{1},anomalyMap(detector,x{1}), ...
        MapRange=displayRange,Blend="equal")});
    fpExamples = readall(fpExamples);
    fpExamples = fpExamples(:,1);
    fpExamplesWithHeatmapOverlays = readall(fpExamplesWithHeatmapOverlays);
    montage(fpExamples)
    montage(fpExamplesWithHeatmapOverlays)
else
    disp("No false positives detected.")
end
No false positives detected.

支持函数

addLabelData 辅助函数会将 data 中的标签信息转换为一热编码表示形式。

function [data,info] = addLabelData(data,info)
    if info.Label == categorical("normal")
        onehotencoding = 0;
    else
        onehotencoding = 1;
    end
    data = {data,onehotencoding};
end

参考资料

[1] Liznerski, Philipp, Lukas Ruff, Robert A. Vandermeulen, Billy Joe Franks, Marius Kloft, and Klaus-Robert Müller."Explainable Deep One-Class Classification."Preprint, submitted March 18, 2021. https://arxiv.org/abs/2007.01760.

[2] Ruff, Lukas, Robert A. Vandermeulen, Billy Joe Franks, Klaus-Robert Müller, and Marius Kloft."Rethinking Assumptions in Deep Anomaly Detection."Preprint, submitted May 30, 2020. https://arxiv.org/abs/2006.00339.

[3] Simonyan, Karen, and Andrew Zisserman."Very Deep Convolutional Networks for Large-Scale Image Recognition."Preprint, submitted April 10, 2015. https://arxiv.org/abs/1409.1556.

[4] ImageNet. https://www.image-net.org.

另请参阅

| | | | | | | | | (Deep Learning Toolbox) | (Deep Learning Toolbox)

主题