Skip to main content

卷积神经网络

卷积神经网络

原理

卷积神经网络的核心是卷积核,卷积核在图像处理领域可以用来提取图像的纵向和横向特征。

卷积核的大小一般为奇数,如3x3,5x5,7x7等,卷积核通常与图像处理(over padding)后的图像进行卷积操作,卷积核在图像上滑动,每次滑动一个像素,对应位置的像素值与卷积核对应位置的值相乘,然后求和,最后将求和的结果作为卷积核中心像素的值,这样就得到了一个新的图像。

新的图像可以用更少的数据反应出图像的特征。这个过程就是特征提取。

我们从一个6x6的矩阵开始:

A=[a11a12a13a14a15a16a21a22a23a24a25a26a31a32a33a34a35a36a41a42a43a44a45a46a51a52a53a54a55a56a61a62a63a64a65a66]\mathbf{A} = \begin{bmatrix} a_{11} & a_{12} & a_{13} & a_{14} & a_{15} & a_{16} \\ a_{21} & a_{22} & a_{23} & a_{24} & a_{25} & a_{26} \\ a_{31} & a_{32} & a_{33} & a_{34} & a_{35} & a_{36} \\ a_{41} & a_{42} & a_{43} & a_{44} & a_{45} & a_{46} \\ a_{51} & a_{52} & a_{53} & a_{54} & a_{55} & a_{56} \\ a_{61} & a_{62} & a_{63} & a_{64} & a_{65} & a_{66} \\ \end{bmatrix}

我们的卷积核是一个3x3的矩阵:

K=[k11k12k13k21k22k23k31k32k33]\mathbf{K} = \begin{bmatrix} k_{11} & k_{12} & k_{13} \\ k_{21} & k_{22} & k_{23} \\ k_{31} & k_{32} & k_{33} \\ \end{bmatrix}

我们假设卷积核位于原始矩阵的左上角,覆盖的区域如下:

[a11a12a13a21a22a23a31a32a33]\begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \\ \end{bmatrix}

此时,输出矩阵的第一个元素O11O_{11}的计算为:

O11=k11a11+k12a12+k13a13+k21a21+k22a22+k23a23+k31a31+k32a32+k33a33O_{11} = k_{11} \cdot a_{11} + k_{12} \cdot a_{12} + k_{13} \cdot a_{13} \\ + k_{21} \cdot a_{21} + k_{22} \cdot a_{22} + k_{23} \cdot a_{23} \\ + k_{31} \cdot a_{31} + k_{32} \cdot a_{32} + k_{33} \cdot a_{33}

整个输出矩阵

卷积核在整个6x6矩阵上滑动(从左至右,从上至下),生成一个4x4的输出矩阵。输出矩阵的每个元素都按照上述方式计算。

点击查看卷积核动画
Live Editor
// 你可以尝试更改矩阵尺寸与卷积核的尺寸来感受卷积过程
function example(props) {
  // 使用 XPath 查询选择输出框
  const xpathSelector =
    "/html/body/div/div[2]/div/div/main/div/div/div/div/article/div[2]/div[1]/div[4]";
  const myElement = document.evaluate(
    xpathSelector,
    document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;
  // 矩阵尺寸
  const matrixSize = 6;
  // 卷积核尺寸
  const kernelSize = 3;
  const matrix = Array.from({ length: matrixSize }, (_, i) =>
    Array.from({ length: matrixSize }, (_, j) => `a${i + 1}${j + 1}`)
  );
  const [position, setPosition] = useState([0, 0]);
  useEffect(() => {
    const positions = [];
    for (let i = 0; i <= matrixSize - kernelSize; i++) {
      for (let j = 0; j <= matrixSize - kernelSize; j++) {
        positions.push([i, j]);
      }
    }

    let index = 0;
    const interval = setInterval(() => {
      setPosition(positions[index]);
      index = (index + 1) % positions.length;
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return (
    <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', backgroundColor: '#f0f0f0' }}>
      <div style={{ display: 'grid', gridTemplateColumns: `repeat(${matrixSize}, 50px)`, gridGap: '5px', position: 'relative' }}>
        {matrix.map((row, i) =>
          row.map((cell, j) => (
            <div
              key={`${i}-${j}`}
              style={{
                width: '50px',
                height: '50px',
                backgroundColor: '#fff',
                border: '1px solid #ccc',
                display: 'flex',
                justifyContent: 'center',
                alignItems: 'center',
                fontSize: '18px',
                backgroundColor: i >= position[0] && i < position[0] + kernelSize && j >= position[1] && j < position[1] + kernelSize ? 'yellow' : '#fff'
              }}
            >
              {cell}
            </div>
          ))
        )}
      </div>
    </div>
  );
}
Result
Loading...

最终输出矩阵O\mathbf{O}为:

O=[O11O12O13O14O21O22O23O24O31O32O33O34O41O42O43O44]\mathbf{O} = \begin{bmatrix} O_{11} & O_{12} & O_{13} & O_{14} \\ O_{21} & O_{22} & O_{23} & O_{24} \\ O_{31} & O_{32} & O_{33} & O_{34} \\ O_{41} & O_{42} & O_{43} & O_{44} \\ \end{bmatrix}

每个OijO_{ij}的具体计算方法如前所述,通过卷积核在原始矩阵上的滑动和计算得到。

通过这个例子,可以清晰地看到卷积核是如何对矩阵进行操作并生成输出的。

常见卷积核及用途

  1. 水平边缘检测

    [111000111]\begin{bmatrix} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \end{bmatrix}

    用途:检测水平边缘。

  2. 垂直边缘检测

    [101101101]\begin{bmatrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \end{bmatrix}

    用途:检测垂直边缘。

  3. Sobel算子(水平)

    [101202101]\begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix}

    用途:检测水平边缘和梯度。

  4. Sobel算子(垂直)

    [121000121]\begin{bmatrix} 1 & 2 & 1 \\ 0 & 0 & 0 \\ -1 & -2 & -1 \end{bmatrix}

    用途:检测垂直边缘和梯度。

  5. 拉普拉斯算子

    [010141010]\begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix}

    用途:检测图像的二阶导数,强调边缘。

  6. 锐化

    [010151010]\begin{bmatrix} 0 & -1 & 0 \\ -1 & 5 & -1 \\ 0 & -1 & 0 \end{bmatrix}

    用途:提高图像的清晰度。

  7. 高斯模糊(3x3)

    116[121242121]\frac{1}{16} \begin{bmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{bmatrix}

    用途:平滑图像,减少噪声。

  8. 高斯模糊(5x5)

    1256[1464141624164624362464162416414641]\frac{1}{256} \begin{bmatrix} 1 & 4 & 6 & 4 & 1 \\ 4 & 16 & 24 & 16 & 4 \\ 6 & 24 & 36 & 24 & 6 \\ 4 & 16 & 24 & 16 & 4 \\ 1 & 4 & 6 & 4 & 1 \end{bmatrix}

    用途:更强的平滑效果。

  9. 边缘增强

    [111191111]\begin{bmatrix} -1 & -1 & -1 \\ -1 & 9 & -1 \\ -1 & -1 & -1 \end{bmatrix}

    用途:增强边缘,使图像轮廓更加明显。

  10. 均值滤波

    19[111111111]\frac{1}{9} \begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{bmatrix}

    用途:均匀地平滑图像。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
import cv2

# 设置中文字体
# 替换为你系统中支持中文的字体路径(windows)
font_path = r'C:\Windows\Fonts\simhei.ttf'
# mac(如果有的话)
# font_path = '/System/Library/Fonts/STHeiti Light.ttc'
font_prop = FontProperties(fname=font_path)

# 读取灰度图像
image = np.array(cv2.imread('data/people.bmp',cv2.IMREAD_GRAYSCALE))

# 定义卷积核
kernels = {
'水平边缘': np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]]),
'垂直边缘': np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]),
'Sobel水平': np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]),
'Sobel垂直': np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]]),
'拉普拉斯': np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]]),
'锐化': np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]]),
'高斯模糊3x3': np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]]) / 16,
'高斯模糊5x5': np.array([[1, 4, 6, 4, 1], [4, 16, 24, 16, 4], [6, 24, 36, 24, 6], [4, 16, 24, 16, 4], [1, 4, 6, 4, 1]]) / 256,
'边缘增强': np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]),
'均值滤波': np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]]) / 9
}

# 使用NumPy实现卷积操作
def convolve2d(image, kernel):
# 获取图像和卷积核的尺寸
i_height, i_width = image.shape
k_height, k_width = kernel.shape

# 计算输出图像的尺寸
o_height = i_height - k_height + 1
o_width = i_width - k_width + 1

# 创建输出图像
output = np.zeros((o_height, o_width))

# 执行卷积操作
for y in range(o_height):
for x in range(o_width):
# 提取图像区域
region = image[y:y+k_height, x:x+k_width]
# 计算卷积值
output[y, x] = np.sum(region * kernel)

return output

# 应用卷积核
results = {}
for name, kernel in kernels.items():
# 为了处理边界,先对图像进行填充
if kernel.shape[0] == 5: # 对于5x5卷积核
pad_width = 2
else: # 对于3x3卷积核
pad_width = 1

padded_image = np.pad(image, pad_width, mode='constant')
filtered_image = convolve2d(padded_image, kernel)

# 归一化处理,确保像素值在有效范围内
filtered_image = np.clip(filtered_image, 0, 255).astype(np.uint8)
results[name] = filtered_image

# 显示结果
plt.figure(figsize=(15, 8))
for i, (name, result) in enumerate(results.items()):
plt.subplot(3, 4, i + 1)
plt.imshow(result, cmap='gray')
plt.title(name, fontproperties=font_prop)
plt.axis('off')

plt.tight_layout()
plt.show()

pooling 池化

池化(Pooling)是一种用于减少卷积神经网络(CNN)中特征图大小的操作。它通过将特征图上的局部区域进行聚合,得到一个更小的特征图。

池化操作类似卷积操作,使用的也是一个很小的矩阵,叫做池化核,但是池化核本身没有参数,只是通过对输入特征矩阵本身进行运算,它的大小通常是2x2、3x3、4x4等,然后将池化核在卷积得到的输出特征图中进行池化操作,需要注意的是,池化的过程中也有Padding方式以及步长的概念,与卷积不同的是,池化的步长往往等于池化核的大小。 最常见的池化操作为最大值池化(Max Pooling)和平均值池化(Average Pooling)两种。

import numpy as np

def pooling(input_array, pool_size=(2, 2), stride=None, mode='max'):
"""
池化操作函数

参数:
input_array: 输入数组,形状为[height, width]或[batch, height, width, channels]
pool_size: 池化窗口大小,默认为(2, 2)
stride: 步长,默认与pool_size相同
mode: 池化类型,'max'表示最大池化,'avg'表示平均池化

返回:
池化后的数组
"""
# 如果未指定stride,则默认与pool_size相同
if stride is None:
stride = pool_size

# 确保输入是numpy数组
input_array = np.asarray(input_array)

# 处理不同维度的输入
if input_array.ndim == 2: # 单通道2D输入
h, w = input_array.shape
d = 1
input_array = input_array.reshape(1, h, w, 1)
elif input_array.ndim == 3: # 带批次或通道的3D输入
raise ValueError("输入数组维度应为2D或4D")
elif input_array.ndim == 4: # 标准4D输入 [batch, height, width, channels]
pass
else:
raise ValueError("输入数组维度应为2D或4D")

# 获取输入尺寸
batch_size, height, width, channels = input_array.shape

# 计算输出尺寸
out_height = (height - pool_size[0]) // stride[0] + 1
out_width = (width - pool_size[1]) // stride[1] + 1

# 初始化输出数组
output = np.zeros((batch_size, out_height, out_width, channels))

# 执行池化操作
for b in range(batch_size):
for c in range(channels):
for i in range(out_height):
for j in range(out_width):
h_start = i * stride[0]
h_end = h_start + pool_size[0]
w_start = j * stride[1]
w_end = w_start + pool_size[1]

pool_region = input_array[b, h_start:h_end, w_start:w_end, c]

if mode == 'max':
output[b, i, j, c] = np.max(pool_region)
elif mode == 'avg':
output[b, i, j, c] = np.mean(pool_region)
else:
raise ValueError("支持的模式为'max'或'avg'")

# 如果输入是2D,则返回2D输出
if input_array.shape[0] == 1 and input_array.shape[3] == 1:
return output[0, :, :, 0]

return output

# 示例使用
if __name__ == "__main__":
# 创建测试数据
test_data = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]
])

# 最大池化
max_pooled = pooling(test_data, pool_size=(2, 2), mode='max')
print("最大池化结果:")
print(max_pooled)

# 平均池化
avg_pooled = pooling(test_data, pool_size=(2, 2), mode='avg')
print("平均池化结果:")
print(avg_pooled)
'''
最大池化结果:
[[ 6. 8.]
[14. 16.]]
平均池化结果:
[[ 3.5 5.5]
[11.5 13.5]]
'''
  • over padding(填充)

有时图像的特征在边缘上,例如

import numpy as np
import matplotlib.pyplot as plt

# 读取图像
inputs = np.array([
[255,1,2],
[255,1,2],
[255,1,2],]

)
# 用于提取纵向特征的卷积核
kernel = np.array([
[0,1,0],
[0,1,0],
[0,1,0]]
)

# 卷积操作结果,没能正确获取边缘的特征
'''
[[0. 2. 0.]
[0. 2. 0.]
[0. 2. 0.]]
'''


# 对输入图像进行填充
# array: 需要填充的数组
# pad_width: 填充的宽度(上下左右都填充)
# mode: 填充的方式,通常为'constant',
# 有0、空、最大、平均、中位等11种参数可以选,点击方法进入查看
# constant_values: 填充的值,通常为0
inputs = np.pad(
array=inputs,
pad_width=1,
mode='constant',
constant_values=0
)


# 卷积操作
out_put = np.zeros((inputs.shape[0] - kernel.shape[0] + 1, inputs.shape[1] - kernel.shape[1] + 1))
out_put_w = out_put.shape[0]
out_put_h = out_put.shape[1]

for i in range(out_put_w):
for j in range(out_put_h):
conv_result = np.sum(inputs[i:i+kernel.shape[0], j:j+kernel.shape[1]] * kernel)
out_put[i][j] = conv_result

# 卷积操作结果,正确的获取到了边缘的特征
print(out_put)
'''
[[510. 2. 4.]
[765. 3. 6.]
[510. 2. 4.]]
'''

stride(步幅)

步幅表示卷积核移动的步长,步幅越大,卷积核每次跳跃的距离就越多,卷积核的感受野越小。

tip

感受野(Receptive Field)的定义:源自生物专业术语,在机器学习中表示卷积神经网络每一层输出的特征图(feature map)上的像素点映射回输入图像上的区域大小。通俗点的解释是,特征图上一点,相对于原图的大小,也是卷积神经网络特征所能看到输入图像的区域。

import numpy as np

def convolution_2d(input_array, kernel, stride=3):
"""
实现2D卷积操作

参数:
input_array: 输入数组,形状为 (height, width)
kernel: 卷积核,形状为 (kernel_size, kernel_size)
stride: 卷积步长,默认为3

返回:
卷积结果数组
"""
# 获取输入数组和卷积核的尺寸
input_height, input_width = input_array.shape
kernel_size = kernel.shape[0]

# 计算输出数组的尺寸
output_height = (input_height - kernel_size) // stride + 1
output_width = (input_width - kernel_size) // stride + 1

# 初始化输出数组
output = np.zeros((output_height, output_width))

# 执行卷积操作
for i in range(output_height):
for j in range(output_width):
# 计算当前窗口的位置
start_i = i * stride
start_j = j * stride

# 提取当前窗口
window = input_array[start_i:start_i+kernel_size, start_j:start_j+kernel_size]

# 计算卷积和
output[i, j] = np.sum(window * kernel)

return output

# 示例使用
if __name__ == "__main__":
# 创建10x10的示例输入数组
input_array = np.ones((8, 8))
# 即输入数组每行列数据下标为 0-7 0-7

# 创建5x5的卷积核
kernel = np.ones((5, 5))

# 执行卷积操作,步幅为3
# 第一次卷积的区域为 0-4 0-4
# 第二次卷积的区域为 3-7 3-7
result = convolution_2d(input_array, kernel, stride=3)

print("输入数组形状:", input_array.shape)
print("卷积核形状:", kernel.shape)
print("卷积结果形状:", result.shape)
print("\n输入数组:")
print(input_array)
print("\n卷积核:")
print(kernel)
print("\n卷积结果:")
print(result)
'''
输入数组形状: (10, 10)
卷积核形状: (5, 5)
卷积结果形状: (2, 2)

输入数组:
[[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]]

卷积核:
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]

卷积结果:
[[25. 25.]
[25. 25.]]
'''

卷积神经网络对手写数字识别

这是pytorch实现

import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
import numpy as np

# 加载数据 & 预处理
digits = load_digits()
X = digits.images.astype(np.float32) / 16.0 # 归一化到0~1
y = digits.target
X = X[..., np.newaxis] # 添加通道维度 (n,8,8,1)
num_classes = 10

# 划分训练/验证集
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)

# 转换为PyTorch张量,并调整为NCHW格式
X_train = torch.tensor(X_train).permute(0, 3, 1, 2) # NHWC -> NCHW
X_val = torch.tensor(X_val).permute(0, 3, 1, 2)
y_train = torch.tensor(y_train, dtype=torch.long)
y_val = torch.tensor(y_val, dtype=torch.long)


# 定义模型
class SimpleConvNet(nn.Module):
def __init__(self):
super(SimpleConvNet, self).__init__()
self.conv = nn.Conv2d(1, 8, kernel_size=3, padding=1) # 输入1通道,输出8通道
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(kernel_size=2) # 2x2池化
self.fc = nn.Linear(4 * 4 * 8, num_classes) # 全连接层

def forward(self, x):
x = self.conv(x) # 卷积层
x = self.relu(x) # ReLU激活
x = self.pool(x) # 最大池化
x = x.reshape(x.shape[0], -1)
x = self.fc(x) # 全连接层
return x


# 创建模型、损失函数和优化器
model = SimpleConvNet()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)

# 训练参数
epochs = 10
batch_size = 64

# 训练循环
for epoch in range(epochs):
# 创建数据加载器进行批处理
indices = torch.randperm(len(X_train))

model.train() # 设置为训练模式
total_loss = 0

# 小批量训练
for i in range(0, len(X_train), batch_size):
# 获取批次数据
batch_indices = indices[i : i + batch_size]
x_batch = X_train[batch_indices]
y_batch = y_train[batch_indices]

# 前向传播
outputs = model(x_batch)
loss = criterion(outputs, y_batch)
total_loss += loss.item()

# 反向传播和优化
optimizer.zero_grad() # 清除之前的梯度
loss.backward() # 反向传播
optimizer.step() # 更新参数

# 验证
model.eval() # 设置为评估模式
with torch.no_grad(): # 不计算梯度
outputs = model(X_val)
_, predicted = torch.max(outputs, 1) # 获取最大值所在位置
accuracy = (predicted == y_val).float().mean()

print(
f"Epoch {epoch+1}/{epochs} loss={total_loss/len(indices)*batch_size:.4f} val_acc={accuracy:.4f}"
)
'''
Epoch 1/10 loss=2.2956 val_acc=0.4472
Epoch 2/10 loss=2.0604 val_acc=0.6167
Epoch 3/10 loss=1.5787 val_acc=0.7861
Epoch 4/10 loss=1.0126 val_acc=0.8000
Epoch 5/10 loss=0.6914 val_acc=0.7972
Epoch 6/10 loss=0.5458 val_acc=0.7917
Epoch 7/10 loss=0.4080 val_acc=0.8417
Epoch 8/10 loss=0.3853 val_acc=0.8778
Epoch 9/10 loss=0.3235 val_acc=0.9111
Epoch 10/10 loss=0.2700 val_acc=0.9250
'''