自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

基于CNN+PyTorch實(shí)現(xiàn)視覺檢測分類

譯文 精選
人工智能
在本文中,我們開發(fā)了一個卷積神經(jīng)網(wǎng)絡(luò)(CNN),用于汽車電子行業(yè)的視覺檢測分類任務(wù)。在此過程中,我們深入研究了卷積層的概念和相關(guān)數(shù)學(xué)知識,并研究了CNN實(shí)際看到的內(nèi)容以及圖像的哪些部分導(dǎo)致它們做出決策。

譯者 | 朱先忠

審校 | 重樓

本文給出了一個使用CNN+PyTorch實(shí)現(xiàn)汽車電子行業(yè)視覺檢測分類詳盡的實(shí)戰(zhàn)案例解析。

在本文中,我們開發(fā)了一個卷積神經(jīng)網(wǎng)絡(luò)(CNN),用于汽車電子行業(yè)的視覺檢測分類任務(wù)。在此過程中,我們深入研究了卷積層的概念和相關(guān)數(shù)學(xué)知識,并研究了CNN實(shí)際看到的內(nèi)容以及圖像的哪些部分導(dǎo)致它們做出決策。

第1部分:概念背景

1.任務(wù):將工業(yè)部件分類為合格件或廢品

在自動裝配線的一個工位中,帶有兩個突出金屬銷的線圈必須精確地定位在外殼中。金屬銷插入小插座中。在某些情況下,銷略微彎曲就會導(dǎo)致無法通過機(jī)器連接。視覺檢查的任務(wù)是識別這些線圈,以便可以自動將它們分類出來。

圖1:線圈、外殼和插座

為了進(jìn)行檢查,每個線圈都被單獨(dú)拾起并放在屏幕前。在這個位置,相機(jī)拍攝灰度圖像。然后,由CNN檢查并分類為合格品或廢品。

圖2:視覺檢查的基本設(shè)置和生成的圖像

現(xiàn)在,我們要定義一個卷積神經(jīng)網(wǎng)絡(luò),它能夠處理圖像并從預(yù)先分類的標(biāo)簽中學(xué)習(xí)。

2.什么是卷積神經(jīng)網(wǎng)絡(luò)(CNN )?

卷積神經(jīng)網(wǎng)絡(luò)是卷積濾波器和全連接神經(jīng)網(wǎng)絡(luò)(NN)的組合。CNN通常用于圖像處理,例如人臉識別或視覺檢查任務(wù),就像我們的情況一樣。卷積濾波器是矩陣運(yùn)算,它在圖像上滑動并重新計(jì)算圖像的每個像素。我們將在本文后面研究卷積濾波器。過濾器的權(quán)重不是預(yù)設(shè)的(例如Photoshop中的銳化函數(shù)),而是在訓(xùn)練期間從數(shù)據(jù)中學(xué)習(xí)而來。

3.卷積神經(jīng)網(wǎng)絡(luò)的架構(gòu)

首先,讓我們來看看CNN架構(gòu)的一個例子。為方便起見,我們選擇了稍后將要實(shí)現(xiàn)的模型。

圖3:我們的視覺檢測CNN架構(gòu)

我們希望將高度為400像素、寬度為700像素的檢測圖像輸入CNN。由于圖像是灰度的,因此相應(yīng)的PyTorch張量的大小為1x400x700。如果我們使用彩色圖像,我們將有3個輸入通道:一個用于紅色,一個用于綠色,一個用于藍(lán)色(RGB)。在這種情況下,張量將是3x400x700。

第一個卷積濾波器有6個大小為5x5的內(nèi)核,它們在圖像上滑動并生成6個獨(dú)立的新圖像,稱為特征圖,尺寸略有縮?。?x396x696)。圖3中未明確顯示ReLU激活。它不會改變張量的維度,但會將所有負(fù)值設(shè)置為零。ReLU之后是內(nèi)核大小為2x2的MaxPooling層,它將每幅圖像的寬度和高度減半。

所有三層(卷積、ReLU和MaxPooling)都是第二次實(shí)施的。這最終為我們帶來了16個特征圖,圖像高度為97像素,寬度為172像素。接下來,所有矩陣值都被展平并輸入到全連接神經(jīng)網(wǎng)絡(luò)的大小相同的第一層中。它的第二層已經(jīng)減少到120個神經(jīng)元。第三層和輸出層只有2個神經(jīng)元:一個代表標(biāo)簽“OK”,另一個代表標(biāo)簽“not OK”或“scrap”。

如果你還不清楚維度的變化,請耐心等待。我們將在下文中詳細(xì)研究不同類型的層(卷積、ReLU和MaxPooling)的工作原理及其對張量維度的影響。

4.卷積濾波器層

卷積濾波器的任務(wù)是查找圖像中的典型結(jié)構(gòu)/模式。常用的內(nèi)核大小為3x3或5x5。內(nèi)核的9個或25個權(quán)重不是預(yù)先指定的,而是在訓(xùn)練過程中學(xué)習(xí)的(這里我們假設(shè)只有一個輸入通道;否則,權(quán)重的數(shù)量將乘以輸入通道)。內(nèi)核在水平和垂直方向上以定義的步幅在圖像的矩陣表示上滑動(每個輸入通道都有自己的內(nèi)核)。內(nèi)核和矩陣的對應(yīng)值相乘并相加。每個滑動位置的求和結(jié)果形成新圖像,我們將其稱為特征圖。我們可以在卷積層中指定多個內(nèi)核。在這種情況下,我們會收到多個特征圖作為結(jié)果。內(nèi)核在矩陣上從左到右、從上到下滑動。因此,圖4顯示了內(nèi)核在其第五個滑動位置(不計(jì)算后面的“...”部分)。我們看到紅、綠、藍(lán)(RGB)三個輸入通道。每個通道只有一個內(nèi)核。在實(shí)際應(yīng)用中,我們通常為每個輸入通道定義多個內(nèi)核。

圖4:具有3個輸入通道和每個通道1個內(nèi)核的卷積層

內(nèi)核1為紅色輸入通道工作。在所示位置,我們計(jì)算特征圖中的相應(yīng)新值為(-0.7)*0+(-0.9)*(-0.2)+(-0.6)*0.5+(-0.6)*0.6+0.6*(-0.3)+0.7*(-1)+0*0.7+(-0.1)*(-0.1)+(-0.2)*(-0.1)=(-1.33)。綠色通道(內(nèi)核2)的相應(yīng)計(jì)算結(jié)果為-0.14,藍(lán)色通道(內(nèi)核3)的相應(yīng)計(jì)算結(jié)果為0.69。為了得到特征圖中特定滑動位置的最終值,我們將所有三個通道值相加并添加一個偏差(偏差和所有核權(quán)重都是在CNN訓(xùn)練期間定義的):(-1.33)+(-0.14)+0.69+0.2=-0.58。該值放置在特征圖中以黃色突出顯示的位置。

最后,如果我們將輸入矩陣的大小與特征圖的大小進(jìn)行比較,我們會發(fā)現(xiàn)通過核操作,我們在高度上損失了兩行,在寬度上損失了兩列。

5.ReLU激活層

卷積后,特征圖通過激活層。激活是賦予網(wǎng)絡(luò)非線性能力所必需的。兩種最常用的激活方法是Sigmoid和ReLU(整流線性單元)。ReLU激活將所有負(fù)值設(shè)置為零,同時保持正值不變。

圖5:特征圖的ReLU激活

在圖5中,我們看到特征圖的值逐個元素地通過了ReLU激活。

ReLU激活對特征圖的尺寸沒有影響。

6.MaxPooling層

池化層的主要任務(wù)是減小特征圖的大小,同時保留分類的重要信息。通常,我們可以通過計(jì)算內(nèi)核中某個區(qū)域的平均值或返回最大值來進(jìn)行池化。MaxPooling在大多數(shù)應(yīng)用中更有用,因?yàn)樗梢詼p少數(shù)據(jù)中的噪音。池化的典型內(nèi)核大小為2x2或3x3。

圖6:內(nèi)核為2x2的最大池化和平均池化

在圖6中,我們看到了內(nèi)核大小為2x2的MaxPooling和AvgPooling的示例。特征圖被劃分為內(nèi)核大小的區(qū)域,在這些區(qū)域中,我們?nèi)∽畲笾担ā鶰axPooling)或平均值(→AvgPooling)。

通過2x2核大小的池化,我們將特征圖的高度和寬度減半。

7.卷積神經(jīng)網(wǎng)絡(luò)中的張量維度

現(xiàn)在,我們已經(jīng)研究了卷積濾波器、ReLU激活和池化,我們可以修改圖3和張量的維度。我們從400x700大小的圖像開始。由于它是灰度的,因此只有1個通道,相應(yīng)的張量大小為1x400x700。我們將6個大小為5x5、步幅為1x1的卷積濾波器應(yīng)用于圖像。每個濾波器都返回自己的特征圖,因此我們收到6個。由于與圖4相比內(nèi)核較大(5x5而不是3x3),這次我們在卷積中丟失了4列和4行。這意味著,返回的張量大小為6x396x696。

下一步,我們將具有2x2內(nèi)核的MaxPooling應(yīng)用于特征圖(每個圖都有自己的池化內(nèi)核)。正如我們所了解的,這會將圖的尺寸減少2倍。因此,張量現(xiàn)在的大小為6x198x348。

現(xiàn)在,我們應(yīng)用16個大小為5x5的卷積濾波器。它們每個的內(nèi)核深度為6,這意味著每個濾波器為輸入張量的6個通道提供單獨(dú)的層。每個內(nèi)核層都會在6個輸入通道中的一個上滑動,如圖4所示,并且6個返回特征圖加起來為1。到目前為止,我們只考慮了一個卷積濾波器,但我們有16個。這就是為什么我們收到16個新的特征圖,每個特征圖比輸入小4列和4行。張量大小現(xiàn)在是16x194x3。

第2部分:定義和編碼CNN

從概念上講,我們已經(jīng)擁有了所需的一切?,F(xiàn)在,讓我們進(jìn)入前面1.1節(jié)中所描述的工業(yè)應(yīng)用場景。

1.加載所需的庫

我們將使用幾個PyTorch庫來加載數(shù)據(jù)、采樣和模型本身。此外,我們加載matplotlib.pyplot進(jìn)行可視化,并加載PIL進(jìn)行圖像轉(zhuǎn)換。

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
from torch.utils.data.sampler import WeightedRandomSampler
from torch.utils.data import random_split
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
import os
import warnings
warnings.filterwarnings("ignore")

2.配置你的設(shè)備并指定超參數(shù)

在設(shè)備中,我們存儲“cuda”或“cpu”,具體取決于你的計(jì)算機(jī)是否有可用的GPU。minibatch_size定義在模型訓(xùn)練期間,一次矩陣運(yùn)算將處理多少張圖像。learning_rate指定反向傳播期間參數(shù)調(diào)整的幅度,epochs定義我們在訓(xùn)練階段處理整組訓(xùn)練數(shù)據(jù)的頻率。

# 設(shè)備配置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using {device} device")

# 指定超參數(shù)
minibatch_size = 10
learning_rate = 0.01
epochs = 60

3.自定義加載器函數(shù)

為了加載圖像,我們定義了一個custom_loader。它以二進(jìn)制模式打開圖像,裁剪圖像內(nèi)部的700x400像素,將其加載到內(nèi)存中,并返回加載的圖像。作為圖像的路徑,我們定義相對路徑data/Coil_Vision/01_train_val_test。請確保數(shù)據(jù)存儲在你的工作目錄中。你可以從我的Dropbox下載文件CNN_data.zip。

#定義加載器函數(shù)
def custom_loader(path):
with open(path, 'rb') as f:
img = Image.open(f)
img = img.crop((50, 60, 750, 460))  #Size: 700x400 px
img.load()
return img

# 圖像路徑(本地路徑以加速加載)
path = "data/Coil_Vision/01_train_val_test"

4.定義數(shù)據(jù)集

我們將數(shù)據(jù)集定義為由圖像數(shù)據(jù)和標(biāo)簽組成的元組,0表示廢品,1表示合格品。方法datasets.ImageFolder()從文件夾結(jié)構(gòu)中讀取標(biāo)簽。我們使用轉(zhuǎn)換函數(shù)首先將圖像數(shù)據(jù)加載到PyTorch張量(值介于0和1之間),然后使用近似平均值0.5和標(biāo)準(zhǔn)差0.5對數(shù)據(jù)進(jìn)行歸一化。轉(zhuǎn)換后,圖像數(shù)據(jù)大致呈標(biāo)準(zhǔn)正態(tài)分布(平均值=0,標(biāo)準(zhǔn)差=1)。我們將數(shù)據(jù)集隨機(jī)分成50%的訓(xùn)練數(shù)據(jù)、30%的驗(yàn)證數(shù)據(jù)和20%的測試數(shù)據(jù)。

#用于加載的轉(zhuǎn)換函數(shù)
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5), (0.5))])

# 在文件夾結(jié)構(gòu)中創(chuàng)建數(shù)據(jù)集
dataset = datasets.ImageFolder(path, transform=transform, loader=custom_loader)
train_set, val_set, test_set = random_split(dataset, [round(0.5*len(dataset)), 
round(0.3*len(dataset)), 
round(0.2*len(dataset))])

5.平衡數(shù)據(jù)集

我們的數(shù)據(jù)是不平衡的。我們的好樣本比廢棄樣本多得多。為了減少訓(xùn)練期間對多數(shù)種類的偏見,我們使用WeightedRandomSampler在采樣期間為少數(shù)種類提供更高的概率。在lbls中,我們存儲訓(xùn)練數(shù)據(jù)集的標(biāo)簽。使用np.bincount(),我們計(jì)算0標(biāo)簽(bc[0])和1標(biāo)簽(bc[1])的數(shù)量。接下來,我們計(jì)算兩個種類(p_nOK和p_OK)的概率權(quán)重,并根據(jù)lst_train列表中數(shù)據(jù)集中的順序排列它們。最后,我們從WeightedRandomSampler實(shí)例化train_sampler。

# 定義一個采樣器來平衡這些類
# training dataset
lbls = [dataset[idx][1] for idx in train_set.indices]
bc = np.bincount(lbls)
p_nOK = bc.sum()/bc[0]
p_OK = bc.sum()/bc[1]
lst_train = [p_nOK if lbl==0 else p_OK for lbl in lbls]
train_sampler = WeightedRandomSampler(weights=lst_train, num_samples=len(lbls))

6.定義數(shù)據(jù)加載器

最后,我們?yōu)橛?xùn)練、驗(yàn)證和測試數(shù)據(jù)定義三個數(shù)據(jù)加載器。數(shù)據(jù)加載器向神經(jīng)網(wǎng)絡(luò)提供一批數(shù)據(jù)集,每批數(shù)據(jù)集由圖像數(shù)據(jù)和標(biāo)簽組成。

對于train_loader和val_loader,我們將批處理大小設(shè)置為10,并對數(shù)據(jù)進(jìn)行隨機(jī)打亂。test_loader使用隨機(jī)打亂數(shù)據(jù)和批處理大小1進(jìn)行操作。

# 用批尺寸定義加載器
train_loader = DataLoader(dataset=train_set, batch_size=minibatch_size, sampler=train_sampler)
val_loader = DataLoader(dataset=val_set, batch_size=minibatch_size, shuffle=True)
test_loader = DataLoader(dataset=test_set, shuffle=True)

7.檢查數(shù)據(jù):繪制5個OK和5個nOK部分

為了檢查圖像數(shù)據(jù),我們繪制了5個好樣本(“OK”)和5個廢品樣本(“nOK”)。為此,我們定義了一個2行5列的matplotlib圖形,并共享x軸和y軸。在代碼片段的核心中,我們嵌套了兩個for循環(huán)。外循環(huán)從train_loader接收數(shù)據(jù)批次。每個批次包含十張圖像和相應(yīng)的標(biāo)簽。內(nèi)循環(huán)枚舉批次的標(biāo)簽。在其主體中,我們檢查標(biāo)簽是否等于0—然后我們在第二行的“nOK”下繪制圖像—或者如果標(biāo)簽等于1—然后我們在第一行的“OK”下繪制圖像。一旦count_OK和count_nOK都大于或等于5,我們就中斷循環(huán),設(shè)置標(biāo)題并顯示圖形。

# 圖形和軸對象
fig, axs = plt.subplots(nrows=2, ncols=5, figsize=(20,7), sharey=True, sharex=True)

count_OK = 0
count_nOK = 0

# 循環(huán)遍歷加載器批次
for (batch_data, batch_lbls) in train_loader:

#循環(huán)遍歷batch_lbls
for i, lbl in enumerate(batch_lbls):

# 如果標(biāo)簽為0 (nOK),在第1行繪制圖形
if (lbl.item() == 0) and (count_nOK < 5):
axs[1, count_nOK].imshow(batch_data[i][0], cmap='gray')
axs[1, count_nOK].set_title(f"nOK Part#: {str(count_nOK)}", fontsize=14)
count_nOK += 1

#如果標(biāo)簽為1 (OK),在第0行繪制圖形
elif (lbl.item() == 1) and (count_OK < 5):
axs[0, count_OK].imshow(batch_data[i][0], cmap='gray')
axs[0, count_OK].set_title(f"OK Part#: {str(count_OK)}", fontsize=14)
count_OK += 1

#如果兩個計(jì)數(shù)器都是>=5停止循環(huán)
if (count_OK >=5) and (count_nOK >=5):
break

# 配置繪圖畫布
fig.suptitle("Sample plot of OK and nonOK Parts", fontsize=24)
plt.setp(axs, xticks=[], yticks=[]) 
plt.show()

圖7:OK(上行)和非OK部分(下行)的示例

在圖7中,我們看到大多數(shù)nOK樣本明顯彎曲,但有些樣本肉眼無法真正區(qū)分(例如右下樣本)。

8.定義CNN模型

該模型對應(yīng)于圖3中所示的架構(gòu)。我們將灰度圖像(僅一個通道)輸入到第一個卷積層,并定義6個大小為5(等于5x5)的內(nèi)核。卷積后跟ReLU激活和MaxPooling,內(nèi)核大小為2(2x2),步長為2(2x2)。所有三個操作都以圖3中所示的尺寸重復(fù)。在__init__()方法的最后一個塊中,16個特征圖被展平并輸入到具有等效輸入大小和120個輸出節(jié)點(diǎn)的線性層中。它被ReLU激活,并在第二個線性層中減少到只有2個輸出節(jié)點(diǎn)。

在forward()方法中,我們只需調(diào)用模型層并輸入x張量。

class CNN(nn.Module):

def __init__(self):
super().__init__()

# Define model layers
self.model_layers = nn.Sequential(

nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),

nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),

nn.Flatten(),
nn.Linear(16*97*172, 120),
nn.ReLU(),
nn.Linear(120, 2)
)

def forward(self, x):
out = self.model_layers(x)
return out

9.實(shí)例化模型并定義損失函數(shù)和優(yōu)化器

我們從CNN類實(shí)例化模型并將其推送到CPU或GPU上。由于我們有一個分類任務(wù),我們選擇使用CrossEntropyLoss函數(shù)。為了管理訓(xùn)練過程,我們調(diào)用隨機(jī)梯度下降(SGD)優(yōu)化器。

# 在cpu或gpu上定義模型
model = CNN().to(device)

#損失函數(shù)和優(yōu)化器
loss = nn.CrossEntropyLoss()

optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

10.檢查模型的大小

為了了解模型的參數(shù)大小,我們迭代model.parameters(),首先將所有模型參數(shù)(num_param)相加,其次將反向傳播期間要調(diào)整的參數(shù)(num_param_trainable)相加。最后,我們打印結(jié)果。

# Count number of parameters / thereof trainable
num_param = sum([p.numel() for p in model.parameters()])
num_param_trainable = sum([p.numel() for p in model.parameters() if p.requires_grad == True])

print(f"Our model has {num_param:,} parameters. Thereof trainable are {num_param_trainable:,}!")

打印結(jié)果告訴我們,該模型有超過3200萬個參數(shù),其中所有參數(shù)都是可訓(xùn)練的。

11.定義一個用于驗(yàn)證和測試的函數(shù)

在開始模型訓(xùn)練之前,讓我們準(zhǔn)備一個函數(shù)來支持驗(yàn)證和測試。函數(shù)val_test()需要一個數(shù)據(jù)加載器和CNN模型作為參數(shù)。它使用torch.no_grad()關(guān)閉梯度計(jì)算并迭代數(shù)據(jù)加載器。有了一批圖像和標(biāo)簽,它將圖像輸入模型,并使用output.argmax(1)對返回的logits確定模型的預(yù)測分類。此方法返回最大值的索引;在我們的例子中,這代表分類索引。

我們計(jì)算并總結(jié)正確的預(yù)測,并保存圖像數(shù)據(jù)、預(yù)測的分類和錯誤預(yù)測的標(biāo)簽。最后,我們計(jì)算準(zhǔn)確率并將其與錯誤分類的圖像一起返回作為函數(shù)的輸出。

def val_test(dataloader, model):
# 獲取數(shù)據(jù)集大小
dataset_size = len(dataloader.dataset)

# 關(guān)閉梯度計(jì)算以進(jìn)行驗(yàn)證
with torch.no_grad():
# 循環(huán)數(shù)據(jù)集
correct = 0
wrong_preds = []
for (images, labels) in dataloader:
images, labels = images.to(device), labels.to(device)

#從模型中獲取原始值
output = model(images)

# 推導(dǎo)預(yù)測
y_pred = output.argmax(1)

# 對所有批次進(jìn)行正確的分類計(jì)數(shù)
correct += (y_pred == labels).type(torch.float32).sum().item()

# Save wrong predictions (image, pred_lbl, true_lbl)
for i, _ in enumerate(labels):
if y_pred[i] != labels[i]:
wrong_preds.append((images[i], y_pred[i], labels[i]))

# Calculate accuracy
acc = correct / dataset_size

return acc, wrong_preds

12.模型訓(xùn)練

模型訓(xùn)練由兩個嵌套的for循環(huán)組成。外循環(huán)迭代定義的epoch數(shù),內(nèi)循環(huán)枚舉train_loader。枚舉返回一批圖像數(shù)據(jù)和相應(yīng)的標(biāo)簽。圖像數(shù)據(jù)(images)被傳遞給模型,我們在輸出中接收模型的響應(yīng)logit。outputs和真實(shí)標(biāo)簽被傳遞給損失函數(shù)?;趽p失l,我們執(zhí)行反向傳播并使用optimizer.step更新參數(shù)。outputs是維度為batchsizex輸出節(jié)點(diǎn)的張量,在我們的例子中是10x2。我們通過行上最大值的索引(0或1)接收模型的預(yù)測。

最后,我們計(jì)算正確預(yù)測的數(shù)量(n_correct)、真正的OK部分(n_true_OK)和樣本數(shù)量(n_samples)。在每個第二個訓(xùn)練周期,我們計(jì)算訓(xùn)練準(zhǔn)確率、真正的OK份額,并調(diào)用驗(yàn)證函數(shù)(val_test())。在訓(xùn)練過程中,所有三個值都會被打印出來以供參考。在最后一行代碼中,我們將模型及其所有參數(shù)保存在“model.pth”中。

acc_train = {}
acc_val = {}
# 對世代進(jìn)行迭代處理
for epoch in range(epochs):

n_correct=0; n_samples=0; n_true_OK=0
for idx, (images, labels) in enumerate(train_loader):
model.train()
# 如果可用,請將數(shù)據(jù)推送到gpu
images, labels = images.to(device), labels.to(device)

#向前傳播
outputs = model(images)
l = loss(outputs, labels)

# 向后傳播和優(yōu)化
optimizer.zero_grad()
l.backward()
optimizer.step()

# 獲取預(yù)測標(biāo)簽(.max返回(value,index))
_, y_pred = torch.max(outputs.data, 1)

# 計(jì)算正確的分類
n_correct += (y_pred == labels).sum().item()
n_true_OK += (labels == 1).sum().item()
n_samples += labels.size(0)

# 在世代結(jié)束時:計(jì)算準(zhǔn)確性和打印信息
if (epoch+1) % 2 == 0:
model.eval()
# 計(jì)算準(zhǔn)確性
acc_train[epoch+1] = n_correct / n_samples
true_OK = n_true_OK / n_samples
acc_val[epoch+1] = val_test(val_loader, model)[0]

#打印信息
print (f"Epoch [{epoch+1}/{epochs}], Loss: {l.item():.4f}")
print(f"      Training accuracy: {acc_train[epoch+1]*100:.2f}%")
print(f"      True OK: {true_OK*100:.3f}%")
print(f"      Validation accuracy: {acc_val[epoch+1]*100:.2f}%")

# 保存模型和狀態(tài)詞典
torch.save(model, "model.pth")

在我的筆記本電腦的GPU上訓(xùn)練需要幾分鐘。強(qiáng)烈建議從本地驅(qū)動器加載圖像;否則,訓(xùn)練時間可能會增加幾個數(shù)量級!

訓(xùn)練的打印輸出表明損失已顯著減少,驗(yàn)證準(zhǔn)確率(模型未用于更新其參數(shù)的數(shù)據(jù)的準(zhǔn)確率)已達(dá)到98.4%。

如果我們繪制訓(xùn)練和驗(yàn)證準(zhǔn)確率在各個時期的圖表,則可以更好地了解訓(xùn)練進(jìn)度。我們可以輕松做到這一點(diǎn),因?yàn)槲覀兠總€第二個訓(xùn)練周期都保存了值。

我們用plt.subplots()創(chuàng)建matplotlib圖和軸,并在準(zhǔn)確率字典的鍵上繪制值。

# 實(shí)例化圖形和軸對象
fig, ax = plt.subplots(figsize=(10,6))
plt.plot(list(acc_train.keys()), list(acc_train.values()), label="training accuracy")
plt.plot(list(acc_val.keys()), list(acc_val.values()), label="validation accuracy")
plt.title("Accuracies", fontsize=24)
plt.ylabel("%", fontsize=14)
plt.xlabel("Epochs", fontsize=14)
plt.setp(ax.get_xticklabels(), fontsize=14)
plt.legend(loc='best', fontsize=14)
plt.show()

圖8:模型訓(xùn)練期間的訓(xùn)練和驗(yàn)證準(zhǔn)確率

13.加載訓(xùn)練好的模型

如果你想將模型用于生產(chǎn)而不僅僅是用于研究目的,強(qiáng)烈建議你保存并加載模型及其所有參數(shù)。保存已經(jīng)是訓(xùn)練代碼的一部分。從驅(qū)動器加載模型同樣簡單。

#從文件中讀取模型
model = torch.load("model.pth")
model.eval()

14.使用測試數(shù)據(jù)再次檢查模型準(zhǔn)確性

請記住,我們保留了另外20%的數(shù)據(jù)用于測試。這些數(shù)據(jù)對于模型來說是全新的,之前從未加載過。我們可以使用這些全新的數(shù)據(jù)再次檢查來驗(yàn)證準(zhǔn)確性。由于驗(yàn)證數(shù)據(jù)已加載但從未用于更新模型參數(shù),因此我們期望其準(zhǔn)確性與測試值相似。為了進(jìn)行測試,我們在test_loader上調(diào)用val_test()函數(shù)。

print(f"test accuracy: {val_test(test_loader,model)[0]*100:0.1f}%")

在具體示例中,我們的測試準(zhǔn)確率達(dá)到了99.2%,但這在很大程度上取決于機(jī)會(記?。簣D像在訓(xùn)練、驗(yàn)證和測試數(shù)據(jù)中的隨機(jī)分布)。

15.可視化錯誤分類的圖像

錯誤分類的圖像的可視化非常簡單。首先,我們調(diào)用val_test()函數(shù),它返回一個元組。其中,包含索引位置0處的準(zhǔn)確率值(tup[0])和索引位置1處的另一個元組(tup[1]),其中包含圖像數(shù)據(jù)(tup[1][0])、預(yù)測標(biāo)簽(tup[1][1])和錯誤分類圖像的真實(shí)標(biāo)簽(tup[1][2])。如果tup[1]不為空,我們將枚舉它并使用適當(dāng)?shù)臉?biāo)題繪制錯誤分類的圖像。

%matplotlib inline

# Call test function
tup = val_test(test_loader, model)

#檢查是否發(fā)生了錯誤的預(yù)測
if len(tup[1])>=1:

# 遍歷錯誤預(yù)測的圖像
for i, t in enumerate(tup[1]):
plt.figure(figsize=(7,5))
img, y_pred, y_true = t
img = img.to("cpu").reshape(400, 700)
plt.imshow(img, cmap="gray")
plt.title(f"Image {i+1} - Predicted: {y_pred}, True: {y_true}", fontsize=24)
plt.axis("off")
plt.show()
plt.close()
else:
print("No wrong predictions!")

在我們的示例中,我們只有一個錯誤分類的圖像,它占測試數(shù)據(jù)集的0.8%(我們有125張測試圖像)。該圖像被分類為OK,但標(biāo)簽為nOK。坦率地說,我也會將其錯誤分類。

圖9:錯誤分類的圖像

第3部分:在生產(chǎn)中使用訓(xùn)練好的模型

1.加載模型、所需的庫和參數(shù)

在生產(chǎn)階段,我們假設(shè)CNN模型已經(jīng)過訓(xùn)練,并且參數(shù)已準(zhǔn)備好加載。我們的目標(biāo)是將新圖像加載到模型中,并讓其對相應(yīng)的電子元件是否適合組裝進(jìn)行分類(參見第1.1節(jié))。

我們首先加載所需的庫,將設(shè)備設(shè)置為“cuda”或“cpu”,定義CNN類(與第2.8章完全相同),然后使用torch.load()從文件加載模型。我們需要在加載參數(shù)之前定義CNN類;否則,參數(shù)無法正確分配。

#加載所需的庫
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
from PIL import Image
import os

#  設(shè)備配置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 完全按照第2.8節(jié)來定義CNN模型
class CNN(nn.Module):

def __init__(self):
super(CNN, self).__init__()

# 定義模型層
self.model_layers = nn.Sequential(

nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),

nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),

nn.Flatten(),
nn.Linear(16*97*172, 120),
nn.ReLU(),
nn.Linear(120, 2),
#nn.LogSoftmax(dim=1)
)

def forward(self, x):
out = self.model_layers(x)
return out

# 加載模型的參數(shù)
model = torch.load("model.pth")
model.eval()

通過運(yùn)行此代碼片段,我們將CNN模型加載到計(jì)算機(jī)內(nèi)存中并對其進(jìn)行參數(shù)化。

2.將圖像加載到數(shù)據(jù)集中

至于訓(xùn)練階段,我們需要準(zhǔn)備圖像以供CNN模型處理。我們從指定的文件夾加載它們,裁剪內(nèi)部700x400像素,并將圖像數(shù)據(jù)轉(zhuǎn)換為PyTorch張量。

#定義自定義數(shù)據(jù)集
class Predict_Set(Dataset):
def __init__(self, img_folder, transform):
self.img_folder = img_folder
self.transform = transform
self.img_lst = os.listdir(self.img_folder)

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

def __getitem__(self, idx):
img_path = os.path.join(self.img_folder, self.img_lst[idx])
img = Image.open(img_path)
img = img.crop((50, 60, 750, 460))  #Size: 700x400
img.load()
img_tensor = self.transform(img)
return img_tensor, self.img_lst[idx]

我們在名為Predict_Set()的自定義數(shù)據(jù)集類中執(zhí)行所有步驟。在__init__()中,我們指定圖像文件夾,接受轉(zhuǎn)換函數(shù),并將圖像文件夾中的圖像加載到列表self.img_lst中。方法__len__()返回圖像文件夾中的圖像數(shù)量。__getitem__()從文件夾路徑和圖像名稱組成圖像路徑,裁剪圖像的內(nèi)部部分(就像我們對訓(xùn)練數(shù)據(jù)集所做的那樣),并將轉(zhuǎn)換函數(shù)應(yīng)用于圖像。最后,它返回圖像張量和圖像名稱。

3.路徑、轉(zhuǎn)換函數(shù)和數(shù)據(jù)加載器

數(shù)據(jù)準(zhǔn)備的最后一步是定義一個數(shù)據(jù)加載器,允許對圖像進(jìn)行迭代以進(jìn)行分類。在此過程中,我們指定圖像文件夾的路徑,并將轉(zhuǎn)換函數(shù)定義為管道,首先將圖像數(shù)據(jù)加載到PyTorch張量,其次將數(shù)據(jù)規(guī)范化為大約-1到+1的范圍。我們將自定義數(shù)據(jù)集Predict_Set()實(shí)例化為變量predict_set,并定義數(shù)據(jù)加載器predict_loader。由于我們沒有指定批處理大小,predict_loader每次返回一張圖像。

# 指向圖像的路徑(最好是本地路徑,以加速加載)
path = "data/Coil_Vision/02_predict"

# 用于加載的轉(zhuǎn)換函數(shù)
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5), (0.5))])

# 創(chuàng)建數(shù)據(jù)集作為自定義數(shù)據(jù)集的實(shí)例
predict_set = Predict_Set(path, transform=transform)

# 定義加載程序
predict_loader = DataLoader(dataset=predict_set)

4.分類自定義函數(shù)

到目前為止,用于分類的圖像數(shù)據(jù)的準(zhǔn)備工作已經(jīng)完成。但是,我們?nèi)匀蝗鄙僖粋€自定義函數(shù),該函數(shù)將圖像傳輸?shù)紺NN模型,將模型的響應(yīng)轉(zhuǎn)換為分類,并返回分類結(jié)果。這正是我們使用predict()所做的事情。

def predict(dataloader, model):

# 關(guān)閉梯度計(jì)算
with torch.no_grad():

img_lst = []; y_pred_lst = []; name_lst = []
#循環(huán)遍歷數(shù)據(jù)加載程序
for image, name in dataloader:
img_lst.append(image)
image = image.to(device)

# 從模型中獲取原始值
output = model(image)

#推導(dǎo)預(yù)測
y_pred = output.argmax(1)
y_pred_lst.append(y_pred.item())
name_lst.append(name[0])

return img_lst, y_pred_lst, name_lst

predict()需要數(shù)據(jù)加載器和CNN模型作為其參數(shù)。其核心是迭代數(shù)據(jù)加載器,將圖像數(shù)據(jù)傳輸?shù)侥P?,并使用output.argmax(1)將模型響應(yīng)解釋為分類結(jié)果—0表示廢品(nOK),1表示合格零件(OK)。圖像數(shù)據(jù)、分類結(jié)果和圖像名稱附加到列表中,列表作為函數(shù)的結(jié)果返回。

5.預(yù)測標(biāo)簽和繪制圖像

最后,我們要利用自定義函數(shù)和加載器對新圖像進(jìn)行分類。在文件夾“data/Coil_Vision/02_predict”中,我們保留了四張等待檢查的電子元件圖像。請記住,我們希望CNN模型告訴我們是否可以使用這些組件進(jìn)行自動組裝,或者是否需要對它們進(jìn)行分類,因?yàn)樵趪L試將它們推入插座時,引腳可能會引起問題。

我們調(diào)用自定義函數(shù)predict(),它返回圖像列表、分類結(jié)果列表和圖像名稱列表。我們枚舉列表并以名稱和分類作為標(biāo)題繪制圖像。

# 預(yù)測圖像的標(biāo)簽
imgs, lbls, names  = predict(predict_loader, model)

#對分類圖像進(jìn)行迭代
for idx, image in enumerate(imgs):
plt.figure(figsize=(8,6))
plt.imshow(image.squeeze(), cmap="gray")
plt.title(f"\nFile: {names[idx]}, Predicted label: {lbls[idx]}", fontsize=18)
plt.axis("off")
plt.show()
plt.close()

圖10:生產(chǎn)階段的分類結(jié)果

我們看到左側(cè)的兩幅圖像被歸類為合格商品(標(biāo)簽1),右側(cè)的兩幅圖像被歸類為廢品(標(biāo)簽0)。由于我們的訓(xùn)練數(shù)據(jù),該模型非常敏感,即使針腳有輕微彎曲也會導(dǎo)致它們被歸類為廢品。

第4部分:CNN在“決策”中考慮了什么?

到目前為止,我們已經(jīng)深入研究了CNN和我們的工業(yè)應(yīng)用場景的細(xì)節(jié)。這似乎是一個很好的機(jī)會,可以更進(jìn)一步,嘗試了解CNN模型在處理圖像數(shù)據(jù)時“看到”了什么。為此,我們首先研究卷積層,然后檢查圖像的哪些部分對于分類特別重要。

1.研究卷積濾波器的尺寸

為了更好地理解卷積濾波器的工作原理以及它們對圖像的作用,讓我們更詳細(xì)地檢查工業(yè)示例中的層。

要訪問這些層,我們枚舉model.children(),它是模型結(jié)構(gòu)的生成器。如果該層是卷積層,我們將其附加到列表all_layers中,并將權(quán)重的維度保存在conv_weights中。如果我們有ReLU或MaxPooling層,則沒有權(quán)重。在這種情況下,我們將層和“*”附加到相應(yīng)的列表中。接下來,我們枚舉all_layers,打印層類型和權(quán)重的維度。

# 設(shè)置為空的列表,以存儲圖層和權(quán)重
all_layers = []; conv_weights = []

# 迭代模型的結(jié)構(gòu)
# (First level nn.Sequential)
for _, layer in enumerate(list(model.children())[0]):
if type(layer) == nn.Conv2d:
all_layers.append(layer)
conv_weights.append(layer.weight)
elif type(layer) in [nn.ReLU, nn.MaxPool2d]:
all_layers.append(layer)
conv_weights.append("*")

# 打印層和權(quán)重維度信息
for idx, layer in enumerate(all_layers):
print(f"{idx+1}. Layer: {layer}")
if type(layer) == nn.Conv2d:
print(f"          weights: {conv_weights[idx].shape}")
else:
print(f"          weights: {conv_weights[idx]}")
print()

圖11:層和權(quán)重的維度

請將代碼片段的輸出與圖3進(jìn)行比較。第一個卷積層有一個輸入——只有一個通道的原始圖像——并返回六個特征圖。我們應(yīng)用六個內(nèi)核,每個內(nèi)核的深度為1,大小為5x5。相應(yīng)地,權(quán)重的維度為torch.Size([6,1,5,5])。相比之下,第4層接收六個特征圖作為輸入并返回16個圖作為輸出。我們應(yīng)用16個卷積內(nèi)核,每個內(nèi)核的深度為6,大小為5x5。因此,權(quán)重的維度為torch.Size([16, 6, 5, 5])。

2.可視化卷積濾波器的權(quán)重

現(xiàn)在,我們知道了卷積濾波器的尺寸。接下來,我們想看看它們的權(quán)重,這是它們在訓(xùn)練過程中獲得的。由于我們有如此多不同的過濾器(第一個卷積層中有6個,第二個卷積層中有16個),因此在兩種情況下,我們都選擇第一個輸入通道(索引0)。

import itertools

#遍歷所有層
for idx_out, layer in enumerate(all_layers):

#如果層是一個卷積濾波器
if type(layer) == nn.Conv2d:

# 打印層名稱
print(f"\n{idx_out+1}. Layer: {layer} \n")

# 準(zhǔn)備繪圖并計(jì)算出權(quán)重
plt.figure(figsize=(25,6))
weights = conv_weights[idx_out][:,0,:,:] # only first input channel
weights = weights.detach().to('cpu')

# 枚舉過濾器權(quán)重(僅限第一個輸入通道)
for idx_in, f in enumerate(weights):
plt.subplot(2,8, idx_in+1)
plt.imshow(f, cmap="gray")
plt.title(f"Filter {idx_in+1}")

# 打印文本
for i, j in itertools.product(range(f.shape[0]), range(f.shape[1])):
if f[i,j] > f.mean():
color = 'black'
else:
color = 'white'
plt.text(j, i, format(f[i, j], '.2f'), horizontalalignment='center', verticalalignment='center', color=color)

plt.axis("off")
plt.show()
plt.close()

我們遍歷all_layers。如果該層是卷積層(nn.Conv2d),則打印該層的索引和該層的核心數(shù)據(jù)。接下來,我們準(zhǔn)備一個圖并提取第一個輸入層的權(quán)重矩陣作為示例。我們枚舉所有輸出層并使用plt.imshow()繪制它們。最后,我們在圖像上打印權(quán)重值,以便我們直觀地可視化卷積濾波器。

圖12:6+16個卷積濾波器的可視化(輸入層索引0)

圖12顯示了第1層的六個卷積濾波器內(nèi)核和第4層的16個內(nèi)核(用于輸入通道0)。右上角的模型示意圖用紅色輪廓表示濾波器。我們看到大多數(shù)值接近0,有些值在正或負(fù)0.20–0.25范圍內(nèi)。這些數(shù)字代表圖4中卷積所使用的值。這為我們提供了接下來要檢查的特征圖。

3.檢查特征圖

根據(jù)圖4,我們通過輸入圖像的卷積獲得第一個特征圖。因此,我們從test_loader加載一個隨機(jī)圖像并將其推送到CPU(如果你在GPU上操作CNN的話)。

# 測試加載程序的批大小為1
img = next(iter(test_loader))[0].to(device)
print(f"\nImage has shape: {img.shape}\n")

# 繪制圖像
img_copy = img.to('cpu')
plt.imshow(img_copy.reshape(400,700), cmap="gray")
plt.axis("off")
plt.show()

圖13:上述代碼的輸出為隨機(jī)圖像

現(xiàn)在,我們將圖像數(shù)據(jù)img傳遞到第一個卷積層(all_layers[0]),并將輸出保存在results中。接下來,我們遍歷all_layers,并將上一層操作的輸出提供給下一層。這些操作是卷積、ReLU激活或MaxPoolings。我們將每個操作的輸出附加到results中。

# 將圖像通過第一層
results = [all_layers[0](img)]

# 將上一層的結(jié)果傳遞給下一層
for idx in range(1, len(all_layers)):  # Start at 1, first layer already passed!
results.append(all_layers[idx](results[-1]))  # 將最后一個結(jié)果傳遞給該圖層

最后,我們繪制原圖以及經(jīng)過第一層(卷積),第二層(ReLU),第三層(MaxPooling),第四層(第二次卷積),第五層(第二次ReLU),第六層(第二次MaxPooling)之后的特征圖。

圖14:經(jīng)過卷積、ReLU和MaxPooling層后的原始圖像和特征圖

我們看到卷積核(比較圖12)重新計(jì)算了圖像的每個像素。這表現(xiàn)為特征圖中灰度值的改變。與原始圖像相比,一些特征圖更加清晰,或者具有更強(qiáng)的黑白對比度,而其他特征圖似乎褪色了。

由于負(fù)值設(shè)置為零,ReLU操作將深灰色變?yōu)楹谏?/span>

MaxPooling使圖像幾乎保持不變,同時在兩個維度上將圖像大小減半。

4.可視化對分類影響最大的圖像區(qū)域

在完成之前,讓我們分析一下圖像的哪些區(qū)域?qū)τ诜诸悶閺U品(索引0)或合格部件(索引1)特別具有決定性。為此,我們使用梯度加權(quán)類激活映射(gradCAM)。該技術(shù)計(jì)算訓(xùn)練模型相對于預(yù)測類別的梯度(梯度顯示輸入(圖像像素)對預(yù)測的影響程度)。每個特征圖(=卷積層的輸出通道)的梯度平均值構(gòu)成了計(jì)算可視化熱圖時與特征圖相乘的權(quán)重。

但是,還是讓我們一步一步地分析一下。

def gradCAM(x):

# 運(yùn)行模型并進(jìn)行預(yù)測
logits = model(x)
pred = logits.max(-1)[-1] # 返回最大值(0或1)

# 在最終的conv層上獲取激活量
last_conv = model.model_layers[:5]
activations = last_conv(x)

# 計(jì)算關(guān)于模型預(yù)測的梯度
model.zero_grad()
logits[0,pred].backward(retain_graph=True)

# 計(jì)算最后一個層每個輸出通道的平均梯度
pooled_grads = model.model_layers[3].weight.grad.mean((1,2,3))

#乘以每個輸出通道與其對應(yīng)的平均梯度
for i in range(activations.shape[1]):
activations[:,i,:,:] *= pooled_grads[i]

# 以所有加權(quán)輸出通道的平均值計(jì)算熱圖
heatmap = torch.mean(activations, dim=1)[0].cpu().detach()

return heatmap

我們定義一個函數(shù)gradCAM,它需要輸入數(shù)據(jù)x(圖像或特征圖),并返回?zé)釄D。

在第一個塊中,我們在CNN模型中輸入x并接收logits,這是一個形狀為[1,2]的張量,只有兩個值。這些值表示類別0和1的預(yù)測概率。我們選擇較大值的索引作為模型的預(yù)測pred。

在第二個塊中,我們提取模型的前五層(從第一個卷積到第二個ReLU),并將它們保存到last_conv。我們在選定的層中運(yùn)行x并將輸出存儲在激活中。顧名思義,這些是第二個卷積層(ReLU激活后)的激活(等于特征圖)。

在第三個塊中,我們對預(yù)測類別logits[0,pred]的logit值進(jìn)行反向傳播。換句話說,我們計(jì)算CNN相對于預(yù)測的所有梯度。梯度顯示輸入數(shù)據(jù)(原始圖像像素)的變化對模型輸出(預(yù)測)的影響有多大。結(jié)果保存在PyTorch計(jì)算圖中,直到我們使用model.zero_grad()將其刪除。

在第四個塊中,我們計(jì)算輸入通道的梯度平均值,以及圖像或特征圖的高度和寬度。結(jié)果,我們收到從第二個卷積層返回的16個特征圖的16個平均梯度。我們將它們保存在pooled_grads中。

在第五個塊中,我們迭代從第二個卷積層返回的16個特征圖,并使用平均梯度pooled_grads對它們進(jìn)行加權(quán)。此操作對那些對預(yù)測具有高重要性的特征圖(及其像素)產(chǎn)生更大的影響;反之亦然。從現(xiàn)在開始,激活不再包含特征圖,而是加權(quán)特征圖。

最后,在最后一個塊中,我們將熱圖計(jì)算為所有激活的平均特征圖。這是函數(shù)gradCAM返回的內(nèi)容。

在繪制圖像和熱圖之前,我們需要對兩者進(jìn)行轉(zhuǎn)換以進(jìn)行疊加。請記住,特征圖比原始圖片?。▍⒁姷?.3和第1.7節(jié)),熱圖也是如此。這就是我們需要函數(shù)upsampleHeatmap()的原因。該函數(shù)將像素值縮放到0到255的范圍,并將它們轉(zhuǎn)換為8位整數(shù)格式(cv2庫需要)。它將熱圖的大小調(diào)整為400x700像素,并將顏色圖應(yīng)用于圖像和熱圖。最后,我們疊加70%的熱圖和30%的圖像并返回繪圖的合成圖。

import cv2

def upsampleHeatmap(map, img):
m,M = map.min(), map.max()
i,I = img.min(), img.max()
map = 255 * ((map-m) / (M-m))
img = 255 * ((img-i) / (I-i))
map = np.uint8(map)
img = np.uint8(img)
map = cv2.resize(map, (700,400))
map = cv2.applyColorMap(255-map, cv2.COLORMAP_JET)
map = np.uint8(map)
img = cv2.applyColorMap(255-img, cv2.COLORMAP_JET)
img = np.uint8(img)
map = np.uint8(map*0.7 + img*0.3)
return map

我們希望將原始圖像和熱圖疊加層并排繪制在同一行中。為此,我們迭代數(shù)據(jù)加載器predict_loader,在圖像上運(yùn)行g(shù)radCAM()函數(shù),在熱圖和圖像上運(yùn)行upsampleHeatmap()函數(shù)。最后,我們使用matplotlib.pyplot將原始圖像和熱圖繪制在同一行中。

#在數(shù)據(jù)加載器上迭代
for idx, (image, name) in enumerate(predict_loader):

# 計(jì)算熱圖
image = image.to(device)
heatmap = gradCAM(image)
image = image.cpu().squeeze(0).permute(1,2,0)
heatmap = upsampleHeatmap(heatmap, image)

# 繪制圖像和熱圖
fig = plt.figure(figsize=(14,5))
fig.suptitle(f"\nFile: {names[idx]}, Predicted label: {lbls[idx]}\n", fontsize=24)
plt.subplot(1, 2, 1)
plt.imshow(image, cmap="gray")
plt.title(f"Image", fontsize=14)
plt.axis("off")
plt.subplot(1, 2, 2)
plt.imshow(heatmap)
plt.title(f"Heatmap", fontsize=14)
plt.tight_layout()
plt.axis("off")
plt.show()
plt.close()

圖15:圖像和熱圖(輸出的內(nèi)側(cè)兩行)

熱圖中的藍(lán)色區(qū)域?qū)δP偷臎Q策影響較小,而黃色和紅色區(qū)域則非常重要。我們發(fā)現(xiàn),在我們的使用場景中,電子元件(特別是金屬針腳)的輪廓對于將零件分類為廢品或合格零件起決定性作用。當(dāng)然,這是非常合理的,因?yàn)榇藞鼍爸兄饕幚韽澢尼樐_。

結(jié)論

卷積神經(jīng)網(wǎng)絡(luò)(CNN)如今是工業(yè)環(huán)境中用于視覺檢查任務(wù)的常用且廣泛使用的工具。在我們的應(yīng)用場景中,我們用相對較少的代碼行成功定義了一個模型,該模型以高精度將電子元件分類為合格零件或廢品。與傳統(tǒng)的視覺檢查方法相比,最大的優(yōu)勢是,沒有工藝工程師需要在圖像中指定視覺標(biāo)記來進(jìn)行分類。相反,CNN從標(biāo)記的示例中學(xué)習(xí),并能夠?qū)⑦@些知識復(fù)制到其他圖像中。在我們的特定場景中,626張標(biāo)記圖像足以進(jìn)行訓(xùn)練和驗(yàn)證。在更復(fù)雜的情況下,對訓(xùn)練數(shù)據(jù)的需求可能會更高。

gradCAM(梯度加權(quán)類激活映射)等算法有助于理解圖像中哪些區(qū)域與模型的決策特別相關(guān)。通過這種方式,它們通過建立對模型功能的信任,支持在工業(yè)環(huán)境中廣泛使用CNN。

總之,在本文中,我們探討了卷積神經(jīng)網(wǎng)絡(luò)內(nèi)部工作原理的許多細(xì)節(jié)。希望你享受這段旅程并深入了解CNN的工作原理。

譯者介紹

朱先忠,51CTO社區(qū)編輯,51CTO專家博客、講師,濰坊一所高校計(jì)算機(jī)教師,自由編程界老兵一枚。

原文標(biāo)題:Building a Vision Inspection CNN for an Industrial Application,作者:Ingo Nowitzky

責(zé)任編輯:姜華 來源: 51CTO內(nèi)容精選
相關(guān)推薦

2024-08-23 08:57:13

PyTorch視覺轉(zhuǎn)換器ViT

2024-11-21 16:06:02

2017-11-23 14:35:36

2017-09-12 16:31:21

TensorFlowLSTMCNN

2025-01-22 13:15:10

2024-07-18 00:00:25

PyTorch神經(jīng)網(wǎng)絡(luò)

2024-01-03 10:23:11

卷積神經(jīng)網(wǎng)絡(luò)CNNpytorch

2020-09-18 11:40:44

神經(jīng)網(wǎng)絡(luò)人工智能PyTorch

2017-08-16 10:12:10

CNN網(wǎng)絡(luò)數(shù)據(jù)

2013-04-24 10:23:02

Android基于膚色Android開發(fā)

2023-11-09 23:45:01

Pytorch目標(biāo)檢測

2020-06-04 12:55:44

PyTorch分類器神經(jīng)網(wǎng)絡(luò)

2024-09-23 09:10:00

R-CNN深度學(xué)習(xí)Python

2020-02-07 16:31:39

開源技術(shù) 趨勢

2011-07-20 17:29:12

iPhone 網(wǎng)絡(luò)

2020-11-03 13:38:28

開源技術(shù) 趨勢

2020-10-05 22:00:59

深度學(xué)習(xí)編程人工智能

2023-04-04 08:25:31

計(jì)算機(jī)視覺圖片

2024-11-19 13:17:38

視覺語言模型Pytorch人工智能

2022-10-14 16:44:48

新詞發(fā)現(xiàn)全卷積網(wǎng)絡(luò)模型算法
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號