機(jī)器學(xué)習(xí)|深度學(xué)習(xí)卷積模型
在早期的圖像分類中,通常流程是先人工提取特征,然后用對應(yīng)的機(jī)器學(xué)習(xí)算法對特征進(jìn)行分類,分類的準(zhǔn)確率一般依賴特征選取的方法,甚至依賴經(jīng)驗主義。
Yann LeCun最早提出將卷積神經(jīng)網(wǎng)絡(luò)應(yīng)用到圖像識別領(lǐng)域的,其主要邏輯是使用卷積神經(jīng)網(wǎng)絡(luò)提取圖像特征,并對圖像所屬類別進(jìn)行預(yù)測,通過訓(xùn)練數(shù)據(jù)不斷調(diào)整網(wǎng)絡(luò)參數(shù),最終形成一套能自動提取圖像特征并對這些特征進(jìn)行分類的網(wǎng)絡(luò),如圖:

圖像處理
1、卷積神經(jīng)網(wǎng)絡(luò)
卷積神經(jīng)網(wǎng)絡(luò)(Convolutional Neural Network,CNN)是一種深度學(xué)習(xí)模型,它是一種多層的神經(jīng)網(wǎng)絡(luò),通常由輸入層、卷積層(Convolutional Layer)、池化層(Pooling Layer)和全連接層(Fully Connected Layer)組成。

卷積神經(jīng)網(wǎng)絡(luò)
- 卷積層:卷積層是CNN的核心,它通過卷積運算提取圖像的特征,并輸出特征圖,不同的卷積核的目的是提取圖像上的不同的特征,比如邊緣、線條等。
 - 池化層:池化層是對卷積層輸出的特征圖進(jìn)行降采樣,降低特征圖的維度,同時保留圖像中重要的信息。
 - 激活函數(shù):激活函數(shù)是對輸入的數(shù)據(jù)進(jìn)行非線性變換,主要目的是通過激活函數(shù)的參數(shù)化,使得神經(jīng)網(wǎng)絡(luò)能夠擬合非線性函數(shù)。
 - 全連接層:通過全連接層將卷積層提取的特征進(jìn)行組合。
 
2、池化
池化在上一篇《機(jī)器學(xué)習(xí)|深度學(xué)習(xí)基礎(chǔ)知識》介紹過,主要是降低采樣率,常用的方法有平均池化,最大池化,K-均值池化等,繼續(xù)上一篇代碼做優(yōu)化,通過??pytorch??的MaxPool2d函數(shù)實現(xiàn)最大池化:
import torch  
import torch.nn as nn  
import matplotlib.pyplot as plt  
  
# 定義包含池化層的網(wǎng)絡(luò)  
class SimplePoolNet(nn.Module):  
    def __init__(self):  
        super(SimplePoolNet, self).__init__()  
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)  
  
    def forward(self, x):  
        return self.pool(x)  
  
# 初始化網(wǎng)絡(luò)  
net = SimplePoolNet()  
  
# 模擬輸入數(shù)據(jù)  
# 假設(shè)輸入是一個1x1x4x4的張量(NxCxHxW),其中N是批次大小,C是通道數(shù)  
input_tensor = torch.tensor([[[[1, 2, 3, 4],  
                               [5, 6, 7, 8],  
                               [9, 10, 11, 12],  
                               [13, 14, 15, 16]]]])  
  
# 執(zhí)行池化操作  
output_tensor = net(input_tensor)  
  
# 可視化輸入和輸出  
plt.figure(figsize=(10, 5))  
  
# 顯示輸入  
plt.subplot(1, 2, 1)  
plt.imshow(input_tensor.squeeze(0).squeeze(0), cmap='gray')  
plt.title('Input')  
  
# 顯示輸出  
plt.subplot(1, 2, 2)  
plt.imshow(output_tensor.squeeze(0).squeeze(0), cmap='gray')  
plt.title('Output')  
  
plt.show()
池化
從上面生成的Output可以看出,池化有如下有點:
- 降低特征圖維度,減少計算量
 - 對微小的變化具有魯棒性
 
3、卷積
3.1 為什么需要卷積?
為什么需要卷積?在全連接網(wǎng)絡(luò)中,輸入層是100X100的矩陣(可以是圖像,也可以是其他特性信息),會被變換為10000X1的向量,這樣會存在幾個問題?
- 輸入的數(shù)據(jù)會由于變換為1維數(shù)據(jù),導(dǎo)致空間信息丟失,比如矩陣(1,1)和(2,1)位置本來是相連的,但是展開后變成(1,1)和(100,1),這樣相鄰的相關(guān)性就不存在了;
 - 輸入數(shù)據(jù)維度過多,會導(dǎo)致模型參數(shù)等比例增長,容易發(fā)生過擬合;
 
為了解決這些問題,所以卷積計算就出現(xiàn)了,具體怎么做的呢?
import torch  
import torch.nn as nn  
  
# 隨機(jī)生成一個10x10x1的矩陣(假設(shè)是單通道圖像)  
input_tensor = torch.rand(1, 1, 10, 10)  # (batch_size, channels, height, width)  
  
# 定義一個3x3的卷積層,padding=1以確保輸出尺寸與輸入相同  
conv_layer = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1)  
  
# 初始化卷積層的權(quán)重(這里PyTorch會自動處理)   
print("Initial weights of the convolution layer:")  
print(conv_layer.weight)  
  
# 進(jìn)行卷積計算  
output_tensor = conv_layer(input_tensor)  
  
# 打印輸出張量的尺寸  
print("Output tensor shape:", output_tensor.shape)  
  
# 如果需要,可以打印輸出張量的具體內(nèi)容  
print(output_tensor)為了方便理解,我從(https://poloclub.github.io/cnn-explainer/)找到動態(tài)圖如下:
圖片卷積計算過程
3.2 卷積計算
上一節(jié)說了為什么要有卷積,知道卷積就是類似濾波器做矩陣運算,其中具體過程如下:

卷積計算
卷積核:卷積核是卷積運算的參數(shù),它是一個矩陣,其數(shù)值對圖像中與卷積核同樣大小的子塊像素點進(jìn)行卷積計算時所采用的權(quán)重;
權(quán)重系數(shù):權(quán)重系數(shù)就是卷積核的參數(shù),捕獲圖像中某像素點及其鄰域像素點所構(gòu)成的特有空間模式;
填充:填充是指在圖像邊緣添加像素點,使得卷積核可以覆蓋到整個圖像,避免卷積運算時輸入圖像尺寸變??;
步長:步長是指卷積核移動的步數(shù),一般設(shè)置為2或者1,在圖像上表示移動多少個像素點,比如步長為2,圖像為100X100,卷積后的矩陣就是50X50;
感受野:感受野是指卷積核覆蓋的區(qū)域,比如3X3的卷積核,1層卷積感受野為3X3,2層卷積感受野為5X5...,根據(jù)最后一層卷積核與原始圖像的關(guān)聯(lián)關(guān)系;
多維卷積核:多維卷積核是指卷積核的維度大于2,比如3D圖像的卷積核就是3X3X3;
3.3 卷積算子
3.3.1 1X1卷積
1X1卷積,即輸入通道數(shù)與輸出通道數(shù)相同,不去考慮輸入數(shù)據(jù)局部信息之間的關(guān)系,而把關(guān)注點放在不同通道間,比如輸入通道數(shù)為3,輸出通道數(shù)為3,那么就是對每個通道做1X1卷積,得到3個輸出通道。

1X1卷積
從這里看,2D情況下1X1并沒有特殊之處,但是高維情況下,1X1就可以實現(xiàn)降維或者升維的效果,比如將各個維度相同矩陣通過1X1卷積,就可以獲得2D矩陣。
3.3.2 3D卷積
3D與2D卷積的區(qū)別是多了一個維度,輸入由(??,??????????,?????????)變?yōu)???,?????????,??????????,?????????),與之對應(yīng)的應(yīng)用場景如視頻和醫(yī)療圖像圖片等,其中示意圖和樣例代碼如下:

3D卷積
import torch
import torch.nn as nn
# 輸入數(shù)據(jù)
# 假設(shè)我們有一個批量大小為1,具有1個通道的3D數(shù)據(jù),其尺寸為(3, 3, 3)
input_data = torch.tensor([[
    [
        [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
        [[10, 11, 12], [13, 14, 15], [16, 17, 18]],
        [[19, 20, 21], [22, 23, 24], [25, 26, 27]]
    ]
]], dtype=torch.float32)
# 定義3D卷積層
# 輸入通道數(shù)為1,輸出通道數(shù)為1,卷積核尺寸為(2, 2, 2),步長為1,填充為0
conv3d_layer = nn.Conv3d(in_channels=1, out_channels=1, kernel_size=2, stride=1, padding=0)
# 初始化卷積核權(quán)重
conv3d_layer.weight.data = torch.tensor([[
    [
        [[1, 2], [3, 4]],
        [[5, 6], [7, 8]]
    ]
]], dtype=torch.float32)
# 初始化偏置
conv3d_layer.bias.data = torch.tensor([0], dtype=torch.float32)
# 計算3D卷積輸出
output = conv3d_layer(input_data)
# 輸出結(jié)果
print("Output shape:", output.shape)
print("Output data:", output)3.3.3 轉(zhuǎn)置卷積
什么是轉(zhuǎn)置卷積?
卷積的逆運算,用于增加上采樣中間層特征圖的空間維度,與通過卷積核減少輸入元素的常規(guī)卷積相反,轉(zhuǎn)置卷積通過卷積核廣播輸入元素,從而產(chǎn)生形狀大于輸入的輸出,其中示意圖和樣例代碼如下:

轉(zhuǎn)置卷積
def trans_conv(X, K):
    h, w = K.shape
    Y = torch.zeros((X.shape[0] + h - 1, X.shape[1] + w - 1))
    for i in range(X.shape[0]):
        for j in range(X.shape[1]):
            Y[i: i + h, j: j + w] += X[i, j] * K
    return Y
X = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
tcov = trans_conv(X, K)
print("trans_conv: ", trans_conv)
# 輸出結(jié)果
tensor([[ 0.,  0.,  1.],
        [ 0.,  4.,  6.],
        [ 4., 12.,  9.]])3.3.4 空洞卷積
空洞卷積顧名思義就補(bǔ)足空洞,通過類似上采樣的方式填充圖片,比如:

空洞卷積
- 通過空洞卷積可以再同樣尺寸的卷積核獲得更大的感受野,從而捕獲更多的信息;
 - 減少計算量,因為空洞卷積可以跳過部分輸入元素,從而減少計算量;
 
3.3.5 分離卷積
卷積神經(jīng)網(wǎng)絡(luò)解決了計算機(jī)視覺領(lǐng)域大部分問題,但是可以看到上面一個問題,就是卷積計算是通過矩陣操作,計算量比較大,如何降低計算量呢?擴(kuò)展到工業(yè)領(lǐng)域?利用矩陣計算的特點,將矩陣分解為兩個矩陣相乘,如下圖所示:

分離卷積
利用矩陣原理是拆分兩個向量的外積:

卷積外積
這樣可以看出計算量對比:原始的計算量為9次,而拆分計算量為3次+3次,比原始的計算量少了3次,所以對于更大的矩陣計算量將大大減少。
4、LeNet卷積神經(jīng)網(wǎng)絡(luò)
前面已經(jīng)介紹卷積模型,那么看看最早的卷積神經(jīng)網(wǎng)絡(luò)是如何設(shè)計,總體看來,由兩部分組成:
- 卷積編碼器:兩個卷積層組合,主要是對特征進(jìn)行提?。?/li>
 - 全連接層:三個全連接層組合,主要是對特征進(jìn)行分類;
 

LeNet架構(gòu)圖
代碼可以參考:https://zh.d2l.ai/chapter_convolutional-neural-networks/lenet.html,這里為了方便測試,我把代碼貼一下:
import torch
from torch import nn
from d2l import torch as d2l
# LeNet-5 
net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Flatten(),
    nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.Sigmoid(),
    nn.Linear(84, 10))
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape: \t',X.shape)
batch_size = 256
# 下載Fashion-MNIST數(shù)據(jù)集
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
# 計算數(shù)據(jù)集上的精度
def evaluate_accuracy_gpu(net, data_iter, device=None):
    if isinstance(net, nn.Module):
        net.eval()  # 設(shè)置為評估模式
        if not device:
            device = next(iter(net.parameters())).device
    metric = d2l.Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            if isinstance(X, list):
                X = [x.to(device) for x in X]
            else:
                X = X.to(device)
            y = y.to(device)
            metric.add(d2l.accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]
# 訓(xùn)練letnet
def train_letnet(net, train_iter, test_iter, num_epochs, lr, device):
    def init_weights(m):
        if type(m) == nn.Linear or type(m) == nn.Conv2d:
            nn.init.xavier_uniform_(m.weight)
    net.apply(init_weights)
    print('training on', device)
    net.to(device)
    optimizer = torch.optim.SGD(net.parameters(), lr=lr)
    loss = nn.CrossEntropyLoss()
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                            legend=['train loss', 'train acc', 'test acc'])
    timer, num_batches = d2l.Timer(), len(train_iter)
    for epoch in range(num_epochs):
        # 訓(xùn)練損失之和,訓(xùn)練準(zhǔn)確率之和,樣本數(shù)
        metric = d2l.Accumulator(3)
        net.train()
        for i, (X, y) in enumerate(train_iter):
            timer.start()
            optimizer.zero_grad()
            X, y = X.to(device), y.to(device)
            y_hat = net(X)
            l = loss(y_hat, y)
            l.backward()
            optimizer.step()
            with torch.no_grad():
                metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
            timer.stop()
            train_l = metric[0] / metric[2]
            train_acc = metric[1] / metric[2]
            if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
                animator.add(epoch + (i + 1) / num_batches,
                             (train_l, train_acc, None))
        test_acc = evaluate_accuracy_gpu(net, test_iter)
        animator.add(epoch + 1, (None, None, test_acc))
    print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
          f'test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
          f'on {str(device)}')
    
lr, num_epochs = 0.9, 10
train_letnet(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
# 輸出結(jié)果(運行在CPU上)
Conv2d output shape:     torch.Size([1, 6, 28, 28])
Sigmoid output shape:    torch.Size([1, 6, 28, 28])
AvgPool2d output shape:          torch.Size([1, 6, 14, 14])
Conv2d output shape:     torch.Size([1, 16, 10, 10])
Sigmoid output shape:    torch.Size([1, 16, 10, 10])
AvgPool2d output shape:          torch.Size([1, 16, 5, 5])
Flatten output shape:    torch.Size([1, 400])
Linear output shape:     torch.Size([1, 120])
Sigmoid output shape:    torch.Size([1, 120])
Linear output shape:     torch.Size([1, 84])
Sigmoid output shape:    torch.Size([1, 84])
Linear output shape:     torch.Size([1, 10])
...
loss 0.465, train acc 0.825, test acc 0.783
6611.0 examples/sec on cpu參考
(1)https://paddlepedia.readthedocs.io/en/latest/tutorials/CNN/convolution_operator/Deformable_Convolution.html
(2)https://zh.d2l.ai/chapter_convolutional-neural-networks/lenet.html
本文轉(zhuǎn)載自????周末程序猿??,作者:周末程序猿


















