機器學習|深度學習卷積模型
在早期的圖像分類中,通常流程是先人工提取特征,然后用對應的機器學習算法對特征進行分類,分類的準確率一般依賴特征選取的方法,甚至依賴經驗主義。
Yann LeCun最早提出將卷積神經網絡應用到圖像識別領域的,其主要邏輯是使用卷積神經網絡提取圖像特征,并對圖像所屬類別進行預測,通過訓練數據不斷調整網絡參數,最終形成一套能自動提取圖像特征并對這些特征進行分類的網絡,如圖:
圖像處理
1、卷積神經網絡
卷積神經網絡(Convolutional Neural Network,CNN)是一種深度學習模型,它是一種多層的神經網絡,通常由輸入層、卷積層(Convolutional Layer)、池化層(Pooling Layer)和全連接層(Fully Connected Layer)組成。
卷積神經網絡
- 卷積層:卷積層是CNN的核心,它通過卷積運算提取圖像的特征,并輸出特征圖,不同的卷積核的目的是提取圖像上的不同的特征,比如邊緣、線條等。
- 池化層:池化層是對卷積層輸出的特征圖進行降采樣,降低特征圖的維度,同時保留圖像中重要的信息。
- 激活函數:激活函數是對輸入的數據進行非線性變換,主要目的是通過激活函數的參數化,使得神經網絡能夠擬合非線性函數。
- 全連接層:通過全連接層將卷積層提取的特征進行組合。
2、池化
池化在上一篇《機器學習|深度學習基礎知識》介紹過,主要是降低采樣率,常用的方法有平均池化,最大池化,K-均值池化等,繼續上一篇代碼做優化,通過??pytorch?
?的MaxPool2d函數實現最大池化:
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
# 定義包含池化層的網絡
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)
# 初始化網絡
net = SimplePoolNet()
# 模擬輸入數據
# 假設輸入是一個1x1x4x4的張量(NxCxHxW),其中N是批次大小,C是通道數
input_tensor = torch.tensor([[[[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]]]])
# 執行池化操作
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 為什么需要卷積?
為什么需要卷積?在全連接網絡中,輸入層是100X100的矩陣(可以是圖像,也可以是其他特性信息),會被變換為10000X1的向量,這樣會存在幾個問題?
- 輸入的數據會由于變換為1維數據,導致空間信息丟失,比如矩陣(1,1)和(2,1)位置本來是相連的,但是展開后變成(1,1)和(100,1),這樣相鄰的相關性就不存在了;
- 輸入數據維度過多,會導致模型參數等比例增長,容易發生過擬合;
為了解決這些問題,所以卷積計算就出現了,具體怎么做的呢?
import torch
import torch.nn as nn
# 隨機生成一個10x10x1的矩陣(假設是單通道圖像)
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)
# 初始化卷積層的權重(這里PyTorch會自動處理)
print("Initial weights of the convolution layer:")
print(conv_layer.weight)
# 進行卷積計算
output_tensor = conv_layer(input_tensor)
# 打印輸出張量的尺寸
print("Output tensor shape:", output_tensor.shape)
# 如果需要,可以打印輸出張量的具體內容
print(output_tensor)
為了方便理解,我從(https://poloclub.github.io/cnn-explainer/)找到動態圖如下:
圖片卷積計算過程
3.2 卷積計算
上一節說了為什么要有卷積,知道卷積就是類似濾波器做矩陣運算,其中具體過程如下:
卷積計算
卷積核:卷積核是卷積運算的參數,它是一個矩陣,其數值對圖像中與卷積核同樣大小的子塊像素點進行卷積計算時所采用的權重;
權重系數:權重系數就是卷積核的參數,捕獲圖像中某像素點及其鄰域像素點所構成的特有空間模式;
填充:填充是指在圖像邊緣添加像素點,使得卷積核可以覆蓋到整個圖像,避免卷積運算時輸入圖像尺寸變小;
步長:步長是指卷積核移動的步數,一般設置為2或者1,在圖像上表示移動多少個像素點,比如步長為2,圖像為100X100,卷積后的矩陣就是50X50;
感受野:感受野是指卷積核覆蓋的區域,比如3X3的卷積核,1層卷積感受野為3X3,2層卷積感受野為5X5...,根據最后一層卷積核與原始圖像的關聯關系;
多維卷積核:多維卷積核是指卷積核的維度大于2,比如3D圖像的卷積核就是3X3X3;
3.3 卷積算子
3.3.1 1X1卷積
1X1卷積,即輸入通道數與輸出通道數相同,不去考慮輸入數據局部信息之間的關系,而把關注點放在不同通道間,比如輸入通道數為3,輸出通道數為3,那么就是對每個通道做1X1卷積,得到3個輸出通道。
1X1卷積
從這里看,2D情況下1X1并沒有特殊之處,但是高維情況下,1X1就可以實現降維或者升維的效果,比如將各個維度相同矩陣通過1X1卷積,就可以獲得2D矩陣。
3.3.2 3D卷積
3D與2D卷積的區別是多了一個維度,輸入由(??,??????????,?????????)變為(??,?????????,??????????,?????????),與之對應的應用場景如視頻和醫療圖像圖片等,其中示意圖和樣例代碼如下:
3D卷積
import torch
import torch.nn as nn
# 輸入數據
# 假設我們有一個批量大小為1,具有1個通道的3D數據,其尺寸為(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卷積層
# 輸入通道數為1,輸出通道數為1,卷積核尺寸為(2, 2, 2),步長為1,填充為0
conv3d_layer = nn.Conv3d(in_channels=1, out_channels=1, kernel_size=2, stride=1, padding=0)
# 初始化卷積核權重
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)
# 輸出結果
print("Output shape:", output.shape)
print("Output data:", output)
3.3.3 轉置卷積
什么是轉置卷積?
卷積的逆運算,用于增加上采樣中間層特征圖的空間維度,與通過卷積核減少輸入元素的常規卷積相反,轉置卷積通過卷積核廣播輸入元素,從而產生形狀大于輸入的輸出,其中示意圖和樣例代碼如下:
轉置卷積
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)
# 輸出結果
tensor([[ 0., 0., 1.],
[ 0., 4., 6.],
[ 4., 12., 9.]])
3.3.4 空洞卷積
空洞卷積顧名思義就補足空洞,通過類似上采樣的方式填充圖片,比如:
空洞卷積
- 通過空洞卷積可以再同樣尺寸的卷積核獲得更大的感受野,從而捕獲更多的信息;
- 減少計算量,因為空洞卷積可以跳過部分輸入元素,從而減少計算量;
3.3.5 分離卷積
卷積神經網絡解決了計算機視覺領域大部分問題,但是可以看到上面一個問題,就是卷積計算是通過矩陣操作,計算量比較大,如何降低計算量呢?擴展到工業領域?利用矩陣計算的特點,將矩陣分解為兩個矩陣相乘,如下圖所示:
分離卷積
利用矩陣原理是拆分兩個向量的外積:
卷積外積
這樣可以看出計算量對比:原始的計算量為9次,而拆分計算量為3次+3次,比原始的計算量少了3次,所以對于更大的矩陣計算量將大大減少。
4、LeNet卷積神經網絡
前面已經介紹卷積模型,那么看看最早的卷積神經網絡是如何設計,總體看來,由兩部分組成:
- 卷積編碼器:兩個卷積層組合,主要是對特征進行提取;
- 全連接層:三個全連接層組合,主要是對特征進行分類;
LeNet架構圖
代碼可以參考: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數據集
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
# 計算數據集上的精度
def evaluate_accuracy_gpu(net, data_iter, device=None):
if isinstance(net, nn.Module):
net.eval() # 設置為評估模式
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]
# 訓練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):
# 訓練損失之和,訓練準確率之和,樣本數
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())
# 輸出結果(運行在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
本文轉載自????周末程序猿??,作者:周末程序猿
