卷积神经网络概述
卷积神经网络(CNN, Convolutional Neural Networks),名称来源于其特殊的卷积层结构,可以有效地提取图像的局部特征,进而捕捉到复杂的模式和特征,因此奠定了其在计算机视觉领域的重要地位。
相比于传统的全连接神经网络,卷积神经网络能够非常好地利用图像的特征,并以低效能消耗完成复杂的识别工作。
卷积神经网络一般分为三层结构:
卷积层:卷积神经网络最具有特色的部分,使用不同的卷积核,提取图像的多方特征。
池化层:池化操作的目的是降维,减少参数量,增加性能的同时可以降低模型对局部变化的灵敏度,提升其泛化能力。
全连接层:将提取的特征映射到样本的标签空间,以进行分类或者回归任务。这是模型处理的最后阶段,负责整合和预测最终结果。
由于全连接层的实现较为复杂,本文着重于直观介绍卷积神经网络的特色算法而非训练模型,因此在经历单一样本的卷积和池化操作之后,我们直接通过展平操作获得特征向量。
绝对不是懒得写
提取图片的特征向量
数据准备
我准备了三张16*16的图片,分别是字母F
和两个字母X
。
由于图片经过压缩,这里展示的可能有点像灰度图像,实际上我直接使用了二值图像,没有灰度。如果是灰度图像或者彩色图像,在导入时应当对其进行二值处理。
from PIL import Image
import numpy as np
# 通过Pillow直接导入图片,图片已经是16*16的黑白二值图像了
image = Image.open('datas/X.png')
# convert('1')可将图像转化为二值图像
image = image.convert('1')
# 转为numpy array
array = np.array(image)
这时候打印这个array,你会看到它是一个16*16的二维数组,元素是True和False。不过没有关系,这不影响后续操作。
卷积处理
卷积,是两个函数(信号)合并的一种方式,它可以通过一个函数对另一个函数进行“滤波”,以达到提取特征的目的。
对于此算法而言,我们要操作的信号是二维数组,其中一个信号是我们给定的固定数组,叫做卷积核(滤波器),而另一个信号就是我们从待处理的数组中抽取出的一部分。例如,这是右侧3×3滤波器对原数组的(2,2)坐标为中心的滤波操作。
滤波的基本方法是滤波器(卷积核)与图像局部区域的元素相乘后求和。
使用3×3的滤波器对16×16的数组进行步长为1的滤波操作,滤波器会经历以(2,2),(2,3),…(2,15),(3,2),…(15,15)为中心的样本,并将结果映射到一个新的二维数组中。显然,这个二维数组的尺寸是14×14。
实际上,卷积操作通常会使用多组正交的卷积核,以提高信号捕捉效率。例如,我使用了一组正交的卷积核分别对样本进行卷积处理,生成两张特征图。
from scipy.signal import convolve2d
import numpy as np
# 左下-右上卷积核
# 采用3*3卷积核,卷积核采用(-1, 2)二值
# 如果采用(0,1)那么对比度边缘(快速变化的区域)特征会被忽视
kernel_vy = np.array([[-1, -1, 2],
[-1, 2, -1],
[2, -1, -1]])
# 左上-右下卷积核
kernel_vx = np.array([[2, -1, -1],
[-1, 2, -1],
[-1, -1, 2]])
convolved_vx = convolve2d(array, kernel_vx, mode='valid')
convolved_vy = convolve2d(array, kernel_vy, mode='valid')
# 卷积结果
# 由于边缘覆盖问题,16*16的图经过3*3卷积核的采样之后,特征图的大小是14*14
print("左上-右下卷积结果:")
print(convolved_vx)
print("左下-右上卷积结果:")
print(convolved_vy)
很多教学视频喜欢采用(0, 1)二值构造卷积核,但是这样做相当于无视了0值的滤波。我这里采用的是(-1, 2)二值进行滤波。
我们可以获得如下的结果
池化
池化的过程其实相对简单,它的目的是降低维度,同时增加稳定性,它的操作大致是将数组拆分成多个n×n的小块,然后把每个小块分别处理得当新的值。
例如,我们的14×14特征图,以2×2的池化窗口,可以拆出7×7个数组。
一般的操作有最大池化和平均池化。最大池化就是取整个池化窗口里的最大值,平均池化就是取整个池化窗口的平均值。(对,就是这么简单)
from skimage.measure import block_reduce
import numpy as np
# 池化操作
# 在这里我们选择最大池化(在一个池化窗口中选取最大值作为池化结果)
# 14*14大小图片,以(2,2)池化的结果,得到的特征图大小是7*7
pool_size = (2, 2)
pooled_vx = block_reduce(convolved_vx, pool_size, np.max)
pooled_vy = block_reduce(convolved_vy, pool_size, np.max)
print("池化结果")
print(pooled_vx)
print(pooled_vy)
池化操作的结果就是,我们获得了更小的一组特征图
至此,卷积神经网络的特色操作已经完成,我们获得了一组拥有优良泛化能力的向量组。
提取特征向量
提取特征向量的一大方法就是展平和拼接。顾名思义,我们可以把二维的特征图,展开成一维的数组,然后把每个特征图展开得到的数组有序拼接,最后得到一个长数组,作为该图片的特征向量。
具体来说,我这里得到了两个7×7的池化后特征图,因此最后能够获得长度为2×7×7=98的特征向量。
# 将池化后的特征图展平成特征向量,这一操作可以直接使用numpy提供的flatten()方法完成
# 实际上转换为全连接层的输入还有其他方法,这里不训练模型,因此直接连接成一个向量
flattened_vx = pooled_vx.flatten()
flattened_vy = pooled_vy.flatten()
# 将两个方向的向量拼合,可以得到一个长向量,作为该图片的特征向量
# 特征向量的长度应当是2*7*7=98
feature_vector = np.concatenate((flattened_vx, flattened_vy))
print("特征向量:")
print(feature_vector)
效果展示
程序源码
from PIL import Image
from scipy.signal import convolve2d
from skimage.measure import block_reduce
import numpy as np
# 计算特征向量
def f_vector(image_path: str) -> np.ndarray:
# 通过Pillow直接导入图片,图片已经是16*16的黑白二值图像了
image = Image.open(image_path)
# convert('1')可将图像转化为二值图像(仅黑白,舍弃灰度)
image = image.convert('1')
# 转为numpy array
array = np.array(image)
# 左下-右上卷积核
# 采用3*3对角卷积核,卷积核采用(-1, 2)二值
# 如果采用(0,1)那么对比度边缘(快速变化的区域)特征会被忽视
kernel_vy = np.array([[-1, -1, 2],
[-1, 2, -1],
[2, -1, -1]])
# 左上-右下卷积核
kernel_vx = np.array([[2, -1, -1],
[-1, 2, -1],
[-1, -1, 2]])
# 进行卷积操作
convolved_vx = convolve2d(array, kernel_vx, mode='valid')
convolved_vy = convolve2d(array, kernel_vy, mode='valid')
# 池化操作
# 在这里我们选择最大池化(在一个池化窗口中选取最大值作为池化结果)
# 14*14大小图片,以(2,2)池化的结果,得到的特征图大小是7*7
pool_size = (2, 2)
pooled_vx = block_reduce(convolved_vx, pool_size, np.max)
pooled_vy = block_reduce(convolved_vy, pool_size, np.max)
# 将池化后的特征图展平成特征向量,这一操作可以直接使用numpy提供的flatten()方法完成
# 实际上转换为全连接层的输入还有其他方法,这里不训练模型,因此直接连接成一个向量
flattened_vx = pooled_vx.flatten()
flattened_vy = pooled_vy.flatten()
# 将两个方向的向量拼合,可以得到一个长向量,作为该图片的特征向量
# 特征向量的长度应当是2*7*7=98
feature_vector = np.concatenate((flattened_vx, flattened_vy))
return feature_vector
# 比较相似度,使用余弦值
def cos(x: np.ndarray, y: np.ndarray) -> float:
dot_product = np.dot(x, y)
norm_x = np.linalg.norm(x)
norm_y = np.linalg.norm(y)
return dot_product / (norm_x * norm_y)
if __name__ == "__main__":
sample_image = 'datas/X.png'
sample_f_vector = f_vector(sample_image)
x_image = 'datas/X_2.png'
x_f_vector = f_vector(x_image)
f_image = 'datas/F.png'
f_f_vector = f_vector(f_image)
print(f"相似度:X字符模板和X字符测试样本:{cos(sample_f_vector, x_f_vector):.4f}")
print(f"相似度:X字符模板和F字符测试样本:{cos(sample_f_vector, f_f_vector):.4f}")
直接应用的结果已经很不错了