一、卷積的概念
卷積來源于英文的Convolution,其中Con是積分,vol是轉、卷。
卷積是一種數學運算,常用于信号處理和圖像處理等領域,它用簡單的數學形式,描述了一個動态的過程。
卷積的定義如下(這個複雜的公式,在卷積神經網絡中可能是用不到):
設 f 和 g 是兩個定義在實數域上的函數,它們的卷積 f∗g 定義為:
其中 t 為實數,τ 是積分變量。
在離散形式下,如果 f 和 g 是兩個長度為 n 的向量,它們的卷積 f∗g 定義為:
其中 k 是整數,[i] 表示向量 f 的第 i 個元素。
二、神經網絡中的卷積
1. 神經網絡卷積概念
在卷積神經網絡中,卷積操作是一種特殊的線性變換,卷積核(也稱為濾波器)在輸入資料上進行滑動,每次計算與卷積核重疊部分的點乘和。
這樣的操作可以提取輸入資料的局部特征,實作特征的共享和抽象,進而使得網絡對輸入資料的變化更加魯棒和準确。
2. 卷積核
卷積核是一種可學習的濾波器,用于對輸入圖像進行特征提取。卷積核通常是一個小的二維矩陣,其大小通常為 k×k,其中 k 是一個正整數,稱為卷積核大小。卷積核的值通常是由神經網絡自動學習得到的。
卷積核的作用是提取輸入資料的局部特征。在卷積操作中,卷積核可以識别輸入圖像中的不同特征,例如邊緣、紋理、角落等,進而提取更加進階的特征表示。通過使用多個卷積核,可以提取不同類型的特征,形成更加複雜的特征表示,進而提高模型的性能。
不同的卷積核(即采用不同的二維矩陣)可以實作不同的效果,常見的卷積核有:
- Sobel卷積核:邊緣檢測;
- Scharr卷積核:也是邊緣檢測卷積核,比Sobel更加平滑;
- Laplacian 卷積核:用于檢測圖像中的邊緣和角點,具有旋轉不變性和尺度不變性;
- 高斯卷積核:用于圖像平滑,減少圖像中的噪聲和細節資訊;
- 梯度卷積核:用于檢測圖像中的梯度資訊,如水準和垂直方向的梯度;
- Prewitt 卷積核:用于檢測圖像中的邊緣資訊,與 Sobel 卷積核類似,但效果略差 ;
- Roberts 卷積核:用于檢測圖像中的邊緣資訊,與 Sobel 卷積核類似,但計算速度更快,精度稍低;
- LoG 卷積核:Laplacian of Gaussian 卷積核,是 Laplacian 卷積核和高斯卷積核的組合,用于檢測圖像中的邊緣和斑點。
3. 卷積核大小
卷積核的大小是卷積神經網絡中的一個超參數,通常與輸入資料的尺寸以及需要提取的特征的大小有關。在卷積神經網絡中,卷積核的大小通常比較小,例如常見的卷積核大小為 3 或 5,因為較小的卷積核可以更好地保留輸入圖像中的局部特征。
同時,卷積核的大小也需要根據卷積操作的步幅和填充等超參數進行選擇。在後面例子中,卷積核大小為 3,步幅為 1,填充為 1,即每次卷積操作會對輸入圖像中的 3 × 3 3\times33×3 的區域進行處理,并生成一個相同大小的卷積特征。填充的目的是為了保留輸入圖像的邊緣資訊,以避免在卷積操作中丢失像素。
需要注意的是,卷積核大小的選擇需要根據具體問題進行調整,通常需要通過實驗來确定最佳的超參數。
三、實作一個簡單的卷積功能
1. 卷積函數
import numpy as np
from PIL import Image
def convolve(image, kernel):
# 擷取圖像和卷積核的大小
image_rows, image_cols = image.shape
kernel_rows, kernel_cols = kernel.shape
# 計算輸出圖像的大小
output_rows = image_rows - kernel_rows + 1
output_cols = image_cols - kernel_cols + 1
# 初始化輸出圖像矩陣,全零的矩陣
output = np.zeros((output_rows, output_cols))
# 執行卷積操作
for row in range(output_rows):
for col in range(output_cols):
output[row, col] = np.sum(image[row:row + kernel_rows, col:col + kernel_cols] * kernel)
return output
自定義的卷積函數接收兩個參數:
- image: 輸入圖像
- kernel: 卷積核
卷積使用 valid 卷積的方式,在進行卷積操作時,輸出圖像的尺寸會變小,計算公式是:
(image_rows - kernel_rows + 1, image_cols - kernel_cols + 1)
程式使用兩個嵌套的循環周遊輸出圖像的每個像素,并計算該像素對應的卷積結果。
np.sum函數中的參數 image 對輸入圖像進行切片,矩陣會進行逐元素相乘(Hadamard乘積或元素級乘積)。image[row:row + kernel_rows, col:col + kernel_cols] 和kernel的大小都是 kernel_rows x kernel_cols, 相乘結果傳回一個相同形狀的矩陣。
2. 邊緣檢測卷積核調用示例
# 加載圖像
img = np.array(Image.open('lena_gray.jpg').convert('L'))
# 定義卷積核
kernel = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]])
# 執行卷積操作
output = convolve(img, kernel)
# 儲存輸出圖像
output_img = Image.fromarray(np.uint8(output))
output_img.save('lena_gray_convolved.jpg')
示例的卷積核是一個簡單的邊緣檢測器,用于檢測圖像中的邊緣。
這裡加載一張灰階圖:
程式輸出結果如下 :
3. 高斯卷積核示例
# 加載圖像
img = np.array(Image.open('lena_gray.jpg').convert('L'))
# 定義卷積核
def gaussian_kernel(size, sigma):
x, y = np.mgrid[-size:size+1, -size:size+1]
g = np.exp(-((x**2 + y**2)/(2.0*sigma**2)))
return g / g.sum()
kernel = gaussian_kernel(3, 1.5)
# 執行卷積操作
output = convolve(img, kernel)
# 儲存輸出圖像
output_img = Image.fromarray(np.uint8(output))
output_img.save('lena_gray_convolved.jpg')
輸出結果:
四、PyTorch計算卷積
1. 生成單通道圖像調用卷積
(1)生成單通道圖像torch.randn(1, 1, 28, 28)
下面用torch.randn(1, 1, 28, 28) 來生成随機數的 PyTorch 函數,它傳回一個大小為 (1, 1, 28, 28) 的張量。其中每個參數的具體含義如下:
- 第一個參數 1 表示生成的張量的 batch size(批大小)為 1。
- 第二個參數 1 表示生成的張量的通道數為 1(單通道圖像)。
- 第三個參數 28 表示生成的張量的高度為 28。
-
第四個參數 28 表示生成的張量的寬度為 28。
torch.randn(1, 1, 28, 28) 傳回的張量可以看作是大小為 1x28x28 的單通道圖像,每個像素的值是從标準正态分布(均值為 0,方差為 1)中随機采樣得到的。
(2)卷積層
nn.Conv2d 是 PyTorch 中用于定義卷積層的類。
代碼nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3, padding=1) 表示建立一個卷積層對象 conv_layer,參數的含義如下:
- in_channels=1 表示輸入通道數為 1,即輸入的是單通道的圖像。
- out_channels=16 表示輸出通道數為 16,即卷積核的數量為 16;卷積核的數量是一個經驗值,需要根據實際情況進行調整,并且會對模型的運作速度和記憶體占用等方面産生影響。過多的卷積核會導緻模型更加複雜,需要更多的計算和存儲資源,而過少的卷積核可能無法充分提取輸入資料的特征。。
- kernel_size=3 表示卷積核大小為 3×3。
- padding=1 表示在輸入的每個邊緣填充 1 個零。這樣做的目的是為了保持輸入輸出大小相同,即輸出特征圖的大小與輸入特征圖的大小相同。如果不進行填充操作,則卷積核會“越過”圖像的邊緣像素,進而導緻輸出特征圖的大小減小。
最終,可以通過調用 conv_layer(input_data) 來實作卷積操作,其中 input_data 是輸入的資料,卷積操作的結果将作為函數傳回值。
import torch
import torch.nn as nn
# 建立一個大小為 28*28 的單通道圖像
input_data = torch.randn(1, 1, 28, 28) # 一個大小為28x28的單通道圖像
# 建立卷積層,輸入通道數為 1
# 輸出通道數16
# 卷積核大小3*3
# 1個0填充
conv_layer = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3, padding=1)
# 對輸入資料進行卷積操作
output_data = conv_layer(input_data)
# 輸出結果
print(output_data.shape) # (1, 16, 28, 28)
卷積後得到了 1個批次、16 個大小為 28 × 28 28\times2828×28 的特征圖。
2. 加載灰階圖像進行卷積操作
下面示例中,卷積結果 [batch_size, channel,height,width] 會進行降維操作,以便于可視化顯示。
最後會使用 Image.fromarray ,将數組轉為圖檔顯示出來。
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
# 讀入示例圖檔
img = Image.open('lena_gray.jpg').convert('L') # 将示例圖檔轉換為灰階圖
plt.imshow(img, cmap='gray')
plt.show()
# 将圖檔轉換為張量并增加一個次元作為批次次元
img_tensor = transforms.ToTensor()(img).unsqueeze(0)
# 建立卷積層,輸入通道數為 1,輸出通道數1,卷積核大小3*3,1個0填充
conv_layer = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1)
# 對輸入資料進行卷積操作
output_tensor = conv_layer(img_tensor)
print(output_tensor.shape)
# 輸出 torch.Size([1, 1, 426, 397])
# 将卷積結果轉換為numpy數組并移除批次次元
output_np = output_tensor.squeeze(0).squeeze(0).detach().numpy()
print(output_np.shape)
# 輸出 (426, 397)
# 将卷積結果轉換為灰階圖像
output_img = Image.fromarray(np.uint8(output_np * 255), mode='L')
# 将卷積結果儲存為圖檔
output_img.save('output.jpg')
# 使用Matplotlib庫展示卷積結果
output_mat = plt.imread('output.jpg')
plt.imshow(output_mat, cmap='gray')
plt.show()
輸出:
3. 對彩色圖檔卷積,輸出1通道
對彩色圖檔進行卷積,要把輸入通道數改為3,加載時選擇RGB:
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
# 讀入示例彩色圖檔
img = Image.open('lena_color.png').convert('RGB')
plt.imshow(img)
plt.show()
# 将圖檔轉換為張量并增加一個次元作為批次次元
img_tensor = transforms.ToTensor()(img).unsqueeze(0)
# 建立卷積層,輸入通道數為 3,輸出通道數1,卷積核大小3*3,1個0填充
conv_layer = nn.Conv2d(in_channels=3, out_channels=1, kernel_size=3, padding=1)
# 對輸入資料進行卷積操作
output_tensor = conv_layer(img_tensor)
print(output_tensor.shape)
# 将卷積結果轉換為numpy數組并移除批次次元
#output_np = output_tensor.squeeze(0).squeeze(0).detach().numpy()
#print(output_np.shape)
# 将卷積結果轉換為灰階圖像
#output_img = Image.fromarray(np.uint8(output_np * 255), mode='L')
output_np = output_tensor.squeeze(0).detach().numpy() # 形狀為 (C, H, W)
output_np = np.repeat(output_np, 3, axis=0) # 将通道數由 1 改為 3
output_np = np.expand_dims(output_np, axis=1) # 添加一個新的次元
output_img = transforms.ToPILImage()(output_np) # 轉換為 PIL.Image 對象
# 将卷積結果儲存為圖檔
output_img.save('output.jpg')
# 使用Matplotlib庫展示卷積結果
output_mat = plt.imread('output.jpg')
plt.imshow(output_mat, cmap='gray')
plt.show()
輸入:
卷積結果:
4. 輸出3通道的卷積操作
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
# 讀入示例圖檔
img = Image.open('lena_color.png').convert('RGB')
plt.imshow(img)
plt.show()
# 将圖檔轉換為張量并增加一個次元作為批次次元
img_tensor = transforms.ToTensor()(img).unsqueeze(0)
# 建立卷積層,輸入通道數為3,輸出通道數為3,卷積核大小3*3,1個0填充
conv_layer = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3, padding=1)
# 對輸入資料進行卷積操作
output_tensor = conv_layer(img_tensor)
# 這時的形狀是 torch.Size([1, 3, 726, 724])
# # 将卷積結果轉換為圖像
output_np = output_tensor.squeeze(0).detach().numpy() # 形狀為 (C, H, W)
# 這時的形狀 (3, 726, 724)
output_np = np.transpose(output_np, (1, 2, 0)) # 轉置使得顔色通道在最後一個次元
# 這時的形狀 (726, 724, 3)
# 為了轉為圖像,下面要對資料處理,傳入的 numpy 數組中的資料類型不是 uint8 類型。
# 由于 transforms.ToPILImage() 隻支援 uint8 類型的資料,要把 float32 類型的 numpy 數組縮放到 0-255 的範圍,并轉換為 uint8 類型。 np.clip 函數和 np.uint8 來實作此功能
# 如果輸出1通道,ToPILImage會轉成unit8資料,但輸出3通道時候是轉成float32,需要自己加轉換
output_np = np.clip(output_np * 255, 0, 255).astype(np.uint8)
# 這時形狀沒有發生變化 (726, 724, 3)
output_img = transforms.ToPILImage()(output_np) # 轉換為 PIL.Image 對象
# 展示卷積結果
plt.imshow(output_img)
plt.show()
輸出: