機(jī)器學(xué)習(xí)|深度學(xué)習(xí)卷積模型
在早期的圖像分類中,通常流程是先人工提取特征,然后用對(duì)應(yīng)的機(jī)器學(xué)習(xí)算法對(duì)特征進(jìn)行分類,分類的準(zhǔn)確率一般依賴特征選取的方法,甚至依賴經(jīng)驗(yàn)主義。
Yann LeCun最早提出將卷積神經(jīng)網(wǎng)絡(luò)應(yīng)用到圖像識(shí)別領(lǐng)域的,其主要邏輯是使用卷積神經(jīng)網(wǎng)絡(luò)提取圖像特征,并對(duì)圖像所屬類別進(jìn)行預(yù)測(cè),通過(guò)訓(xùn)練數(shù)據(jù)不斷調(diào)整網(wǎng)絡(luò)參數(shù),最終形成一套能自動(dòng)提取圖像特征并對(duì)這些特征進(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的核心,它通過(guò)卷積運(yùn)算提取圖像的特征,并輸出特征圖,不同的卷積核的目的是提取圖像上的不同的特征,比如邊緣、線條等。
- 池化層:池化層是對(duì)卷積層輸出的特征圖進(jìn)行降采樣,降低特征圖的維度,同時(shí)保留圖像中重要的信息。
- 激活函數(shù):激活函數(shù)是對(duì)輸入的數(shù)據(jù)進(jìn)行非線性變換,主要目的是通過(guò)激活函數(shù)的參數(shù)化,使得神經(jīng)網(wǎng)絡(luò)能夠擬合非線性函數(shù)。
- 全連接層:通過(guò)全連接層將卷積層提取的特征進(jìn)行組合。
2、池化
池化在上一篇《機(jī)器學(xué)習(xí)|深度學(xué)習(xí)基礎(chǔ)知識(shí)》介紹過(guò),主要是降低采樣率,常用的方法有平均池化,最大池化,K-均值池化等,繼續(xù)上一篇代碼做優(yōu)化,通過(guò)??pytorch?
?的MaxPool2d函數(shù)實(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è)輸入是一個(gè)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可以看出,池化有如下有點(diǎn):
- 降低特征圖維度,減少計(jì)算量
- 對(duì)微小的變化具有魯棒性
3、卷積
3.1 為什么需要卷積?
為什么需要卷積?在全連接網(wǎng)絡(luò)中,輸入層是100X100的矩陣(可以是圖像,也可以是其他特性信息),會(huì)被變換為10000X1的向量,這樣會(huì)存在幾個(gè)問(wèn)題?
- 輸入的數(shù)據(jù)會(huì)由于變換為1維數(shù)據(jù),導(dǎo)致空間信息丟失,比如矩陣(1,1)和(2,1)位置本來(lái)是相連的,但是展開后變成(1,1)和(100,1),這樣相鄰的相關(guān)性就不存在了;
- 輸入數(shù)據(jù)維度過(guò)多,會(huì)導(dǎo)致模型參數(shù)等比例增長(zhǎng),容易發(fā)生過(guò)擬合;
為了解決這些問(wèn)題,所以卷積計(jì)算就出現(xiàn)了,具體怎么做的呢?
import torch
import torch.nn as nn
# 隨機(jī)生成一個(gè)10x10x1的矩陣(假設(shè)是單通道圖像)
input_tensor = torch.rand(1, 1, 10, 10) # (batch_size, channels, height, width)
# 定義一個(gè)3x3的卷積層,padding=1以確保輸出尺寸與輸入相同
conv_layer = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1)
# 初始化卷積層的權(quán)重(這里PyTorch會(huì)自動(dòng)處理)
print("Initial weights of the convolution layer:")
print(conv_layer.weight)
# 進(jìn)行卷積計(jì)算
output_tensor = conv_layer(input_tensor)
# 打印輸出張量的尺寸
print("Output tensor shape:", output_tensor.shape)
# 如果需要,可以打印輸出張量的具體內(nèi)容
print(output_tensor)
為了方便理解,我從(https://poloclub.github.io/cnn-explainer/)找到動(dòng)態(tài)圖如下:
圖片卷積計(jì)算過(guò)程
3.2 卷積計(jì)算
上一節(jié)說(shuō)了為什么要有卷積,知道卷積就是類似濾波器做矩陣運(yùn)算,其中具體過(guò)程如下:
卷積計(jì)算
卷積核:卷積核是卷積運(yùn)算的參數(shù),它是一個(gè)矩陣,其數(shù)值對(duì)圖像中與卷積核同樣大小的子塊像素點(diǎn)進(jìn)行卷積計(jì)算時(shí)所采用的權(quán)重;
權(quán)重系數(shù):權(quán)重系數(shù)就是卷積核的參數(shù),捕獲圖像中某像素點(diǎn)及其鄰域像素點(diǎn)所構(gòu)成的特有空間模式;
填充:填充是指在圖像邊緣添加像素點(diǎn),使得卷積核可以覆蓋到整個(gè)圖像,避免卷積運(yùn)算時(shí)輸入圖像尺寸變小;
步長(zhǎng):步長(zhǎng)是指卷積核移動(dòng)的步數(shù),一般設(shè)置為2或者1,在圖像上表示移動(dòng)多少個(gè)像素點(diǎn),比如步長(zhǎng)為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)注點(diǎn)放在不同通道間,比如輸入通道數(shù)為3,輸出通道數(shù)為3,那么就是對(duì)每個(gè)通道做1X1卷積,得到3個(gè)輸出通道。
1X1卷積
從這里看,2D情況下1X1并沒(méi)有特殊之處,但是高維情況下,1X1就可以實(shí)現(xiàn)降維或者升維的效果,比如將各個(gè)維度相同矩陣通過(guò)1X1卷積,就可以獲得2D矩陣。
3.3.2 3D卷積
3D與2D卷積的區(qū)別是多了一個(gè)維度,輸入由(??,??????????,?????????)變?yōu)???,?????????,??????????,?????????),與之對(duì)應(yīng)的應(yīng)用場(chǎng)景如視頻和醫(yī)療圖像圖片等,其中示意圖和樣例代碼如下:
3D卷積
import torch
import torch.nn as nn
# 輸入數(shù)據(jù)
# 假設(shè)我們有一個(gè)批量大小為1,具有1個(gè)通道的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),步長(zhǎng)為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)
# 計(jì)算3D卷積輸出
output = conv3d_layer(input_data)
# 輸出結(jié)果
print("Output shape:", output.shape)
print("Output data:", output)
3.3.3 轉(zhuǎn)置卷積
什么是轉(zhuǎn)置卷積?
卷積的逆運(yùn)算,用于增加上采樣中間層特征圖的空間維度,與通過(guò)卷積核減少輸入元素的常規(guī)卷積相反,轉(zhuǎn)置卷積通過(guò)卷積核廣播輸入元素,從而產(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ǔ)足空洞,通過(guò)類似上采樣的方式填充圖片,比如:
空洞卷積
- 通過(guò)空洞卷積可以再同樣尺寸的卷積核獲得更大的感受野,從而捕獲更多的信息;
- 減少計(jì)算量,因?yàn)榭斩淳矸e可以跳過(guò)部分輸入元素,從而減少計(jì)算量;
3.3.5 分離卷積
卷積神經(jīng)網(wǎng)絡(luò)解決了計(jì)算機(jī)視覺領(lǐng)域大部分問(wèn)題,但是可以看到上面一個(gè)問(wèn)題,就是卷積計(jì)算是通過(guò)矩陣操作,計(jì)算量比較大,如何降低計(jì)算量呢?擴(kuò)展到工業(yè)領(lǐng)域?利用矩陣計(jì)算的特點(diǎn),將矩陣分解為兩個(gè)矩陣相乘,如下圖所示:
分離卷積
利用矩陣原理是拆分兩個(gè)向量的外積:
卷積外積
這樣可以看出計(jì)算量對(duì)比:原始的計(jì)算量為9次,而拆分計(jì)算量為3次+3次,比原始的計(jì)算量少了3次,所以對(duì)于更大的矩陣計(jì)算量將大大減少。
4、LeNet卷積神經(jīng)網(wǎng)絡(luò)
前面已經(jīng)介紹卷積模型,那么看看最早的卷積神經(jīng)網(wǎng)絡(luò)是如何設(shè)計(jì),總體看來(lái),由兩部分組成:
- 卷積編碼器:兩個(gè)卷積層組合,主要是對(duì)特征進(jìn)行提??;
- 全連接層:三個(gè)全連接層組合,主要是對(duì)特征進(jìn)行分類;
LeNet架構(gòu)圖
代碼可以參考:https://zh.d2l.ai/chapter_convolutional-neural-networks/lenet.html,這里為了方便測(cè)試,我把代碼貼一下:
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)
# 計(jì)算數(shù)據(jù)集上的精度
def evaluate_accuracy_gpu(net, data_iter, device=None):
if isinstance(net, nn.Module):
net.eval() # 設(shè)置為評(píng)估模式
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é)果(運(yùn)行在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)載自????周末程序猿??,作者:周末程序猿
