卷積的定義
????????卷積是圖像處理中最核心的操作之一,其本質是通過卷積核(濾波器)與圖像進行滑動窗口計算(像素值乘積之和),實現對圖像特征的提取、增強或抑制。
一、二維卷積--針對二維矩陣進行處理
1.1單通道
? ? ? ? 見得最多的卷積形式,它只有一個通道。
形式:
解釋:這里的卷積不是旁邊綠色的矩陣圖,右邊綠色的矩陣圖叫做特征圖(feature map),一般是卷積核與原圖進行卷積后的結果圖。這里的卷積核是
[0,1,2]
[2,2,0]
[0,1,2]?
卷積的過程就是從上到下,從左到右依次開卷,然后乘積求和相加。
1.2多通道
? ? ? ? 多通道卷積的通道數不在局限于1個,它的通道數是多個的,一般用在圖像為多通道圖像上,每一個通道的原圖形像素值與對應通道的卷積核進行計算,最后再進行多通道的合并(元素相加,在通道上進行特征融合)。
形式:
# 第1個子核(對應輸入的R通道)
[[w11, w12, w13],[w21, w22, w23],[w31, w32, w33]
]# 第2個子核(對應輸入的G通道)
[[u11, u12, u13],[u21, u22, u23],[u31, u32, u33]
]# 第3個子核(對應輸入的B通道)
[[v11, v12, v13],[v21, v22, v23],[v31, v32, v33]
]
二、三維卷積
????????在空間的長、寬兩個維度基礎上,再增加一個深度(或時間)維度進行卷積操作。
核心特點:
- 卷積核是三維的:比如尺寸為?
3×3×3
(長 × 寬 × 深度),像一個 “小立方體”。 - 作用對象是三維數據:比如視頻的連續幀(寬 × 高 × 時間幀)、CT/MRI 的立體醫學影像(長 × 寬 × 切片層)等。
三、反卷積
? ? ? ? 卷積的反向操作,但不是嚴格反向。卷積是將原圖的特征進行提取,會把原圖變小。卷積為計算Y。
? ? ? ? 反卷積也稱為轉置卷積,它是將小尺寸的特征圖恢復為大尺寸。反卷積為計算X。
四、膨脹卷積
? ? ? ? 膨脹卷積也叫空洞卷積,主要是為了讓卷積核的元素之間插入空格進行膨脹,擴大感受野。一般膨脹率參數L默認為1,公式為L-1。
????????感受野(Receptive Field):輸出特征圖上的一個像素點,對應輸入圖像(或原始數據)中多大范圍的區域。
API:nn.Conv2d(dilation = 2)? -----這表示空一格
代碼:
import torch
import torch.nn as nn
def test():c1 = nn.Conv2d(in_chanels=3,out_chanels=1,kernel_size=3,stride=1,paddiong=0,dilation=2)out = c1(input_data)print(out.shape)if __name__ == '__main__':input_data = torch.randn(1,3,7,7) #1表示批次,3表示通道,7表示高度,7表示寬度test()
結果:
torch.Size([1, 1, 3, 3])??
解釋:特征圖out根據公式可以得到現在的寬高,torchsize里的參數分別表示(batch,channel,height,weight)
?計算公式:
五、可分離卷積
5.1空間可分離卷積
? ? ? ? 將卷積核拆分成可以用數學公式相乘得到的兩個卷積核。用這兩個分別的獨立的核對原圖形進行操作。
? ? ? ? 拆分的目的:降低計算量。
API:nn.Conv2d(kernel_size = (x,y))
代碼:
def test1():c1 = nn.Conv2d(in_channels=3,out_channels=1,kernel_size=(3,1),stride=1,padding=0,dilation=1)out = c1(input_data)print(out.shape)c2 = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(1,3),stride=1,padding=0,dilation=1)out1 = c2(out)print(out1.shape)if __name__ == '__main__':input_data = torch.randn(1,3,7,7) #1表示批次,3表示通道,7表示高度,7表示寬度test1()
結果:
torch.Size([1, 1, 5, 7])
torch.Size([1, 1, 5, 5])
根據結果可以觀察到,最終的結果仍然是一樣的,計算公式可以用上圖計算得到。
#可分離卷積的另一種寫法
class SeparbleConv(nn.Module):def __init__(self):super(SeparbleConv,self).__init__()self.c1 = nn.Conv2d(in_channels=32,out_channels=64,kernel_size=(3,1),stride=1,padding=0,dilation=1)self.c2 = nn.Conv2d(in_channels=64,out_channels=16,kernel_size=(1,3),stride=1,padding=0,dilation=1)def forward(self,x):x = self.c1(x)out = self.c2(x)return outif __name__ == '__main__':input_data = torch.randn(1,32,214,214)model = SeparbleConv()out = model(input_data)print(out.shape)weight_desc = model.named_parameters()for name, param in weight_desc:print(name, param.shape)
結果:
?torch.Size([1, 16, 212, 212])
c1.weight torch.Size([64, 32, 3, 1])
c1.bias torch.Size([64])
c2.weight torch.Size([16, 64, 1, 3])
c2.bias torch.Size([16])
5.2深度可分離卷積
? ? ? ? 深度可分離卷積的卷積核分為兩部分,一部分為深度卷積核,一部分為1*1卷積(點卷積)。
卷積過程:
(1)輸入圖的每一個通道都有對應的卷積核,通道數有多少,卷積核的個數就有多少。
(2)對前一個深度可分離卷積的結果進行1*1卷積,對上一個特征圖進行特征融合,形成新的特征圖結果。這里不會改變形狀大小,只是把重要的特征凸顯出來,不重要的信息逐漸抹去。
注意:深度卷積時,要對其進行分組。
分組是為了減少參數量和計算量,如果仍按照傳統的全連接層網絡創建的話,參數會多的多。
eg:
傳統卷積中,每個輸出通道都依賴于所有輸入通道的信息。 例如:輸入 64 通道,輸出 128 通道,傳統卷積需要 64×128 個 3×3 卷積核,總參數量為 64×128×3×3 = 73,728。 這種 “全連接” 方式計算開銷極大,尤其在深層網絡中會導致參數量爆炸。
分組卷積將輸入通道和輸出通道按組隔離,每組內獨立進行卷積,最后拼接結果。 例如:同樣是輸入 64 通道、輸出 128 通道,若設置groups=2
:
輸入被分為 2 組(每組 32 通道);
輸出也被分為 2 組(每組 64 通道);
每組內的卷積:每個輸出通道只連接到對應的輸入組(如第一組的 64 個輸出通道僅由第一組的 32 個輸入通道計算得到)。
此時,每組需要 32×64 個 3×3 卷積核,兩組總參數量為 2×(32×64×3×3) = 36,864,相比傳統卷積減少近一半!
?代碼驗證:
#深度可分離卷積
class depthConv(nn.Module):def __init__(self):super(depthConv,self).__init__()self.c1 = nn.Conv2d(in_channels=8,out_channels=8, #參數量3*3*1*1*8 = 72kernel_size=(3,3),stride=1,padding=0,dilation=1,groups=8) #分組卷積# 這里的c1是深度可分離卷積中的depthwise卷積# 下面的c2是深度可分離卷積中的pointwise卷積self.c2 = nn.Conv2d(in_channels=8,out_channels=8, #參數量8*8*1*1 =64kernel_size=(1,1),stride=1,padding=0,dilation=1,)def forward(self,x):x = self.c1(x)out = self.c2(x)return out#普通卷積
class Conv(nn.Module):def __init__(self):super(Conv,self).__init__()self.c1 = nn.Conv2d(in_channels=8,out_channels=8, #參數量3*3*8*8 = 576kernel_size=(3,3),stride=1,padding=0,dilation=1,)def forward(self,x):out = self.c1(x)return outif __name__ == '__main__':input_data = torch.randn(1,8,32,32) #1表示批次,8表示通道,7表示高度,7表示寬度model = depthConv()out = model(input_data)print(out.shape)moedl2 = Conv()out2 = moedl2(input_data)print(out2.shape)#打印權重參數weight = model.named_parameters()for name,para in weight:print(name,para.shape)print('------------------------')weight2 = moedl2.named_parameters()for name,para in weight2:print(name,para.shape)
結果:
?torch.Size([1, 8, 30, 30])
torch.Size([1, 8, 30, 30])
c1.weight torch.Size([8, 1, 3, 3])
c1.bias torch.Size([8])
c2.weight torch.Size([8, 8, 1, 1])
c2.bias torch.Size([8])
------------------------
c1.weight torch.Size([8, 8, 3, 3])
c1.bias torch.Size([8])
六、扁平卷積
? ? ? ? 將標準卷積拆分成3個1*1的卷積核,然后分別對輸入層進行卷積計算。
七、分組卷積
? ? ? ? 將卷積分組放到兩個GPU中并發執行。這里的分組和前邊深度可分離卷積的分組一樣,都是為了解決參數量和計算量的問題。
代碼:
#分組卷積
class GroupConv(nn.Module):def __init__(self):super(GroupConv,self).__init__()self.c1 = nn.Conv2d(in_channels=32,out_channels=256, #參數量4*32*3*3*8 = 9216kernel_size=3,stride=1,padding=0,dilation=1,groups=8)def forward(self,x):out = self.c1(x)return outclass Conv(nn.Module):def __init__(self):super(Conv,self).__init__()self.c1 = nn.Conv2d(in_channels=32,out_channels=256, #參數量32*256*3*3 = 73728kernel_size=3,stride=1,padding=0,dilation=1,groups=1)def forward(self,x):out = self.c1(x)return out
if __name__ == '__main__':input_data = torch.randn(1,32,512,512) #1表示批次,32表示通道,512表示高度,512表示寬度model = Conv() #普通卷積out = model(input_data)print(out.shape)model1 = GroupConv() #分組卷積out = model1(input_data)print(out.shape)weight_group = model1.named_parameters()for name,weight in weight_group:print(name,weight.shape)print('上面的是分組權重信息,下面的是普通全連接權重信息------------------------')weight = model.named_parameters()for name,weight in weight:print(name,weight.shape)
結果:
torch.Size([1, 256, 510, 510])
torch.Size([1, 256, 510, 510])
c1.weight torch.Size([256, 4, 3, 3])
c1.bias torch.Size([256])
上面的是分組權重信息,下面的是普通全連接權重信息------------------------
c1.weight torch.Size([256, 32, 3, 3])
c1.bias torch.Size([256])
八、混洗分組卷積
???????? 分組卷積中最終結果會按照原先的順序進行合并組合,阻礙了模型在訓練時特征信息在通道間流動,削弱了特征表示。混洗分組卷積,主要是將分組卷積后的計算結果混合交叉在一起輸出。
九、整體結構--卷積神經網絡案例
?根據該圖定義一個可以識別手寫數字的卷積神經網絡案例:
步驟:
(1)導入數據集
(2)加載數據集
(3)構建網絡模型
(4)定義設備、輪次、學習率、優化器、損失函數、數據加載器;
(5)開始訓練
(6)模型保存----在代碼中暫未體現
(7)模型預測----在代碼中暫未體現
代碼:
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets,transforms
import os
from torchvision.datasets import MNIST
import torch.optim as optim#定義數據預處理
#數據地址
file_path = os.path.relpath(os.path.join(os.path.dirname(__file__), 'data'))
#定義數據集
train_data = MNIST(root=file_path,train=True,transform=transforms.ToTensor(),download=False)
#查看文件類別,標簽
class_names = train_data.classes
print(class_names)#定義數據加載器
train_loader = DataLoader(train_data,batch_size=32,shuffle=True,drop_last=True)
#當設置 drop_last=True 時:
# 如果訓練數據總量不能被 batch_size 整除,最后一個樣本數量不足 batch_size 的批次會被丟棄。
# 如果設置為 False,最后一個樣本數量不足 batch_size 的批次會被保留,但可能會導致訓練結果不穩定。#定義網絡結構
class MyNet(nn.Module):def __init__(self):super(MyNet,self).__init__()#定義網絡結構self.c1 = nn.Sequential(nn.Conv2d(in_channels=1,out_channels=6,kernel_size=5),nn.ReLU())self.s2 = nn.AdaptiveAvgPool2d(14) #自適應地調整為指定大小(這里是 14×14),而無需手動計算核大小、步長等參數。self.c3 = nn.Sequential(nn.Conv2d(in_channels=6,out_channels=16,kernel_size=5),nn.ReLU())self.s4 = nn.AdaptiveAvgPool2d(5)self.l5 = nn.Sequential(nn.Linear(16*5*5,120),nn.ReLU())self.l6 = nn.Sequential(nn.Linear(120,84),nn.ReLU())self.l7 = nn.Linear(84,10)def forward(self,x):#前向傳播x = self.c1(x)x = self.s2(x)x = self.c3(x)x = self.s4(x)x = x.view(x.size(0),-1)x = self.l5(x)x = self.l6(x)out = self.l7(x)return out#定義設備、學習率等參數
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MyNet()
model.to(device)epochs = 20
lr = 0.001
opt = optim.Adam(model.parameters(),lr=lr)
criterion = nn.CrossEntropyLoss()#訓練
for epoch in range(epochs):model.train()total_loss = 0acc_total = 0for i,(x,y) in enumerate(train_loader):x,y = x.to(device),y.to(device)opt.zero_grad() #梯度清零out = model(x)out = torch.argmax(out, dim=1)acc_total += (out == y).sum().item()loss = criterion(out,y)total_loss += loss.item()loss.backward() #反向傳播opt.step()if i%10 == 0: #每訓練10個batch打印一次print(f'epoch:{epoch},loss:{loss.item()}')print("epoch:", epoch, "loss:", total_loss/ len(train_loader.dataset), "acc:", acc_total / len(train_loader.dataset))