目录
用自己的语言描述“卷积、卷积核、特征图、特征选择、步长、填充、感受野”。
3. 图3分别使用卷积核编辑,编辑,编辑 ,输出特征图
卷积常用于特征提取
实验过程中注意认真体会“特征提取”,弄清楚为什么卷积能够提取特征。
一、概念
用自己的语言描述“卷积、卷积核、特征图、特征选择、步长、填充、感受野”。
1. 卷积:
卷积经常使用的是一维卷积和二维卷积,一维卷积通常用在信号处理,二维卷积通常用于图像处理,卷积是指在滑动中提取特征的过程,我认为卷积本质上是加权叠加。
二维卷积的过程可以参考下图:
另外,在查找资料时我发现一个很形象很有意思的解释,是以扇巴掌为例讲述的,参考链接:https://www.cnblogs.com/jfdwd/p/9299130.html
关于卷积的一个血腥的讲解
比如说你的老板命令你干活,你却到楼下打台球去了,后来被老板发现,他非常气愤,扇了你一巴掌(注意,这就是输入信号,脉冲),于是你的脸上会渐渐地(贱贱地)鼓起来一个包,你的脸就是一个系统,而鼓起来的包就是你的脸对巴掌的响应,好,这样就和信号系统建立起来意义对应的联系。下面还需要一些假设来保证论证的严谨:假定你的脸是线性时不变系统,也就是说,无论什么时候老板打你一巴掌,打在你脸的同一位置(这似乎要求你的脸足够光滑,如果你说你长了很多青春痘,甚至整个脸皮处处连续处处不可导,那难度太大了,我就无话可说了哈哈),你的脸上总是会在相同的时间间隔内鼓起来一个相同高度的包来,并且假定以鼓起来的包的大小作为系统输出。好了,那么,下面可以进入核心内容——卷积了!
如果你每天都到地下去打台球,那么老板每天都要扇你一巴掌,不过当老板打你一巴掌后,你5分钟就消肿了,所以时间长了,你甚至就适应这种生活了……如果有一天,老板忍无可忍,以0.5秒的间隔开始不间断的扇你的过程,这样问题就来了,第一次扇你鼓起来的包还没消肿,第二个巴掌就来了,你脸上的包就可能鼓起来两倍高,老板不断扇你,脉冲不断作用在你脸上,效果不断叠加了,这样这些效果就可以求和了,结果就是你脸上的包的高度随时间变化的一个函数了(注意理解);如果老板再狠一点,频率越来越高,以至于你都辨别不清时间间隔了,那么,求和就变成积分了。可以这样理解,在这个过程中的某一固定的时刻,你的脸上的包的鼓起程度和什么有关呢?和之前每次打你都有关!但是各次的贡献是不一样的,越早打的巴掌,贡献越小,所以这就是说,某一时刻的输出是之前很多次输入乘以各自的衰减系数之后的叠加而形成某一点的输出,然后再把不同时刻的输出点放在一起,形成一个函数,这就是卷积,卷积之后的函数就是你脸上的包的大小随时间变化的函数。本来你的包几分钟就可以消肿,可是如果连续打,几个小时也消不了肿了,这难道不是一种平滑过程么?反映到剑桥大学的公式上,f(a)就是第a个巴掌,g(x-a)就是第a个巴掌在x时刻的作用程度,乘起来再叠加就ok了,大家说是不是这个道理呢?我想这个例子已经非常形象了,你对卷积有了更加具体深刻的了解了吗?
2. 卷积核:
卷积核就是图像处理时,给定输入图像,输入图像中一个小区域中像素加权平均后成为输出图像中的每个对应像素,其中由权值组成的矩阵是卷积核。
数学中的卷积和卷积神经网络中的卷积严格意义上是两种不同的运算。数学中卷积,主要是为了诸如信号处理、求两个随机变量和分布而定义的运算,需要“翻转”。
卷积神经网络中"卷积”,是为了提取图像的特征,其实只借鉴了“加权求和”的特点,其实是互相关,并非数学上所说的卷积计算。数学中的“卷积核”都是已知的或者给定的,卷积神经网络中“卷积核”本来就是trainable的参数,不是给定的,而是根据数据训练学习的。
3. 特征图:
每层卷积结束后得到的卷积值的组合就是特征图。即每次卷积运算后得出的结果就是特征图。
4. 特征选择:
特侦选择是从全部特征中选取一个特征子集,特征选择能剔除不相关或冗余的特征,从而达到减少特征个数,提高模型精确度,减少运行时间的目的。另外,选取出真正相关的特征简化模型,协助理解数据产生的过程。并且“数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已”
拓展:
4.1. 做特征选择的原因
考虑特征选择,是因为机器学习经常面临过拟合的问题。 过拟合的表现是模型参数太贴合训练集数据,模型在训练集上效果很好而在测试集上表现不好,也就是在高方差。简言之模型的泛化能力差。
即使用原始数据集的话:
- 耗时:特征个数越多,分析特征、训练模型所需的时间就越长。
- 过拟合:特征个数越多,容易引起“维度灾难”,模型也会越复杂,其推广能力会下降。
- 共线性:单因子对目标的作用被稀释,解释力下降。
4.2. 特征选择的常用方法
1、过滤式
变量排序就是一种典型的过滤式方法,该方法独立于后续要使用的模型。这种方法的关键就是找到一种能度量特征重要性的方法,比如pearson相关系数,信息论理论中的互信息等。变量排序方法的主要问题在于忽略了特征之间可能存在的相互依赖关系。一方面,即便排序靠前的特征,如果相关性较强,则引入了冗余的特征;另一方面,排序靠后的特征,虽然独立来看作用不明显,但可能与其它特征组合起来,就有很好的预测作用,如此就损失了有价值的特征。
2、包裹式
这类方法的核心思想在于,给定了某种模型,及预测效果评价的方法,然后针对特征空间中的不同子集,计算每个子集的预测效果,效果最好的,即作为最终被挑选出来的特征子集。注意集合的子集是一个指数的量级,故此类方法计算量较大。故而针对如何高效搜索特征空间子集,就产生了不同的算法。其中有一种简单有效的方法叫贪婪搜索策略,包括前向选择与后向删除。在前向选择方法中,初始化一个空的特征集合,逐步向其中添加新的特征,如果该特征能提高预测效果,即得以保留,否则就扔掉。后向删除即是说从所有特征构成的集合开始,逐步删除特征,只要删除后模型预测效果提升,即说明删除动作有效,否则就还是保留原特征。要注意到,包裹式方法要求针对每一个特征子集重新训练模型,因此计算量还是较大的。
3、嵌入式
嵌入式方法将特征选择融合在模型训练的过程中,比如决策树在分枝的过程中,就是使用的嵌入式特征选择方法,其内在还是根据某个度量指标对特征进行排序。
5. 步长:
卷积核(滤波器)每次移动的像素单位。
6. 填充:
在输入图像的边界填充元素(通常填充元素是0)。
7. 感受野:
感受野是输出特征图上某个像素对应到输入空间中的区域范围。感受野可以理解为特征图像素到输入区域的映射。感受野越大代表范围越大,也就是信息更加丰富。感受野越小代表越细节。
拓展:
7.1. 生物上的定义
感受器受刺激兴奋时,通过感受器官中的向心神经元将神经冲动(各种感觉信息)传到上位中枢,一个神经元所反应(支配)的刺激区域就叫做神经元的感受野(receptive field)。7.2. 深度学习的定义
在卷积神经网络中,感受野的定义是卷积神经网络每一层输出的特征图(feature map)上的像素点在原始图像上映射的区域大小。通俗的说,感受野就是输入图像对这一层输出的神经元的影响有多大。7.3. 感受野的作用
感受野用来表示网络内部的不同神经元对原图像的感受范围的大小,或者说convNets(cnn)每一层输出的特征图(feature map)上的像素点在原始图像上映射的区域大小。7.4. 感受野的计算公式
其中,RF是感受野。N_ RF和RF有点像,N代表neighbour, 指的是第n层的a feature在n- 1层的RF,记住N _RF只 是一个中间变量(某一层的感受野),不要和RF混淆。stride是步长, ksize是卷积核大小。
这个算法从最后一层往前层层迭代直到追溯回input image,从而计算出感受野(RF)。
可以看到在Conv1中的每一个单元所能看到的原始图像范围是3 x 3,而由于Conv2的每个单元都是由2 x 2范围的Conv1构成,回溯到原始图像,由于卷积核移动的时候重叠了一个像素,应该被减去,最终其实是能够看到的是5 x 5的原始图像范围。
因此我们说Conv1的感受野是3 x 3,Conv2的感受野是5 x 5.输入图像的每个单元的感受野被定义为1,这应该很好理解,因为每个像素只能看到自己。
二、探究不同卷积核的作用
1. 图1分别使用卷积核,输出特征图
使用卷积核
代码:
import torch
import matplotlib.pyplot as plt
import torch.nn.functional as F
import numpy as np
#生成图片
def create_pic():
picture = torch.Tensor([[0,0,0,255,255,255],
[0,0,0,255,255,255],
[0,0,0,255,255,255],
[0,0,0,255,255,255],
[0,0,0,255,255,255]])
return picture
#确定卷积网络
class MyNet(torch.nn.Module):
def __init__(self,kernel,kshape):
super(MyNet, self).__init__()
kernel = torch.reshape(kernel,kshape)
self.weight = torch.nn.Parameter(data=kernel, requires_grad=False)
def forward(self, picture):
picture = F.conv2d(picture,self.weight,stride=1,padding=0)
return picture
#确定卷积层
kernel = torch.tensor([-1.0,1.0])
#更改卷积层的形状适应卷积函数
kshape = (1,1,1,2)
#生成模型
model = MyNet(kernel=kernel,kshape=kshape)
#生成图片
picture = create_pic()
#更改图片的形状适应卷积层
picture = torch.reshape(picture,(1,1,5,6))
output = model(picture)
output = torch.reshape(output,(5,5))
plt.imshow(output,cmap='gray')
plt.show()
运行结果:
使用卷积核
kernel = torch.tensor([-1.0,1.0])
#更改卷积和的形状为转置
kshape = (1,1,2,1)
model = MyNet(kernel=kernel,kshape=kshape)
picture = create_pic()
picture = torch.reshape(picture,(1,1,5,6))
output = model(picture)
output = torch.reshape(output,(6,4))
plt.imshow(output,cmap='gray')
plt.show()
结果:
2. 图2分别使用卷积核,输出特征图
图2使用卷积核
代码:
#生成图像
def create_pic():
picture = torch.Tensor([[0,0,0,255,255,255],
[0,0,0,255,255,255],
[0,0,0,255,255,255],
[255,255,255,0,0,0],
[255,255,255,0,0,0],
[255,255,255,0,0,0]])
return picture
#确定卷积核
kernel = torch.tensor([-1.0,1.0])
kshape = (1,1,1,2)
#生成模型
model = MyNet(kernel=kernel,kshape=kshape)
picture = create_pic()
picture = torch.reshape(picture,(1,1,6,6))
print(picture)
output = model(picture)
output = torch.reshape(output,(6,5))
print(output)
plt.imshow(output,cmap='gray')
plt.show()
运行结果:
图2使用卷积核
kernel = torch.tensor([-1.0,1.0])
kshape = (1,1,2,1)
model = MyNet(kernel=kernel,kshape=kshape)
picture = create_pic()
picture = torch.reshape(picture,(1,1,6,6))
print(picture)
output = model(picture)
output = torch.reshape(output,(5,6))
print(output)
plt.imshow(output,cmap='gray')
plt.show()
3. 图3分别使用卷积核,, ,输出特征图
图3使用卷积核
代码:
def create_pic():
picture = torch.Tensor(
[[255,255,255,255,255,255,255,255,255],
[255,0 ,255,255,255,255,255,0 ,255],
[255,255,0 ,255,255,255,0 ,255,255],
[255,255,255,0 ,255,0 ,255,255,255],
[255,255,255,255,0 ,255,255,255,255],
[255,255,255,0 ,255,0 ,255,255,255],
[255,255,0 ,255,255,255,0 ,255,255],
[255,0 ,255,255,255,255,255,0 ,255],
[255,255,255,255,255,255,255,255,255],])
return picture
#生成卷积核
kernel = torch.tensor([-1.0,1.0])
#更改卷积核的形状适应卷积函数
kshape = (1,1,1,2)
model = MyNet(kernel=kernel,kshape=kshape)
picture = create_pic()
picture = torch.reshape(picture,(1,1,9,9))
print(picture)
output = model(picture)
output = torch.reshape(output,(9,8))
print(output)
plt.imshow(output,cmap='gray')
plt.show()
结果:
图3使用卷积核
代码:
kernel = torch.tensor([-1.0,1.0])
kshape = (1,1,2,1)
model = MyNet(kernel=kernel,kshape=kshape)
picture = create_pic()
picture = torch.reshape(picture,(1,1,9,9))
print(picture)
output = model(picture)
output = torch.reshape(output,(8,9))
print(output)
plt.imshow(output,cmap='gray')
plt.show()
结果:
图3使用卷积核
代码:
#确定卷积核
kernel = torch.tensor([[1.0,-1.0],
[-1.0,1.0]])
#更改卷积核的大小适配卷积函数
kshape = (1,1,2,2)
#生成网络模型
model = MyNet(kernel=kernel,kshape=kshape)
picture = create_pic()
picture = torch.reshape(picture,(1,1,9,9))
print(picture)
output = model(picture)
output = torch.reshape(output,(8,8))
print(output)
plt.imshow(output,cmap='gray')
plt.show()
结果:
4. 实现灰度图的边缘检测、锐化、模糊。
这部分我是借鉴老师的代码【23-24 秋学期】NNDL 作业6 卷积-CSDN博客,在老师代码的基础上进行修改得到的。
代码:
import numpy as np
import torch
from torch import nn
from torch.autograd import Variable
from PIL import Image
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# 加载图片
file_path = 'C:\\Users\\26980\\Desktop\\China.jpg'
im = Image.open(file_path).convert('L')
im = np.array(im, dtype='float32')
plt.subplot(331).set_title('原图')
plt.imshow(im.astype('uint8'), cmap='gray')
im = torch.from_numpy(im.reshape((1, 1, im.shape[0], im.shape[1])))
conv1 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
conv2 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
conv3 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
conv4 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
conv5 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
conv6 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
conv7 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
conv8 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
bottom_sobel = np.array([[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]], dtype='float32').reshape((1, 1, 3, 3))
conv1.weight.data = torch.from_numpy(bottom_sobel)
left_sobel = np.array([[1, 0, -1],
[2, 0, -2],
[1, 0, -1]], dtype='float32').reshape((1, 1, 3, 3))
conv2.weight.data = torch.from_numpy(left_sobel)
right_sobel = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]], dtype='float32').reshape((1, 1, 3, 3))
conv3.weight.data = torch.from_numpy(right_sobel)
top_sobel = np.array([[-1, 2, 1],
[0, 0, 0],
[-1, -2, -1]], dtype='float32').reshape((1, 1, 3, 3))
conv4.weight.data = torch.from_numpy(top_sobel)
sharpen = np.array([[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]], dtype='float32').reshape((1, 1, 3, 3))
conv5.weight.data = torch.from_numpy(sharpen)
blur = np.array([[0.0625, 0.125, 0.0625],
[0.125, 0.25, 0.125],
[0.0625, 0.125, 0.0625]], dtype='float32').reshape((1, 1, 3, 3))
conv6.weight.data = torch.from_numpy(blur)
emboss = np.array([[-2, -1, 0],
[-1, 1, 1],
[0, 1, 2]], dtype='float32').reshape((1, 1, 3, 3))
conv7.weight.data = torch.from_numpy(emboss)
outline = np.array([[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]], dtype='float32').reshape((1, 1, 3, 3))
conv8.weight.data = torch.from_numpy(outline)
y1 = conv1(Variable(im)).data.squeeze().numpy()
y2 = conv2(Variable(im)).data.squeeze().numpy()
y3 = conv3(Variable(im)).data.squeeze().numpy()
y4 = conv4(Variable(im)).data.squeeze().numpy()
y5 = conv5(Variable(im)).data.squeeze().numpy()
y6 = conv6(Variable(im)).data.squeeze().numpy()
y7 = conv7(Variable(im)).data.squeeze().numpy()
y8 = conv8(Variable(im)).data.squeeze().numpy()
# 可视化
plt.subplot(332).set_title('bottom_sobel')
plt.imshow(y1, cmap='gray')
plt.subplot(333).set_title('left_sobel')
plt.imshow(y2, cmap='gray')
plt.subplot(334).set_title('right_sobel')
plt.imshow(y3, cmap='gray')
plt.subplot(335).set_title('top_sobel')
plt.imshow(y4, cmap='gray')
plt.subplot(336).set_title('sharpen')
plt.imshow(y5, cmap='gray')
plt.subplot(337).set_title('blur')
plt.imshow(y6, cmap='gray')
plt.subplot(338).set_title('emboss')
plt.imshow(y7, cmap='gray')
plt.subplot(339).set_title('outline')
plt.imshow(y8, cmap='gray')
plt.show()
结果:
换一个像素低的图片看看结果:(这个图片是可以看出来像素的图片)
卷积核提取特征的效果和分辨率相关,分辨率越高提取出的特征也越清晰,因为本身含有的像素点就多,和卷积核进行运算的像素点多,自然提取出的特征清晰。
由于是借鉴老师的代码,这里我对老师的代码进行了注释:
import numpy as np
import torch
from torch import nn
from torch.autograd import Variable
from PIL import Image
import matplotlib.pyplot as plt
# 设置中文字符集和负号显示,防止在显示图像和图表时出现排版问题
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 #有中文出现的情况,需要u'内容
# 定义图像文件的路径
file_path = 'deer.jpg'
# 打开图像文件并将其转换为灰度图
im = Image.open(file_path).convert('L') # 读入一张灰度图的图片
# 将灰度图转换为numpy数组,方便后续处理
im = np.array(im, dtype='float32') # 将其转换为一个矩阵
# 打印图像的高和宽,以便后续处理时了解图像的大小
print(im.shape[0], im.shape[1])
# 将图像可视化并添加标题,然后显示出来,方便观察和调试
plt.imshow(im.astype('uint8'), cmap='gray') # 可视化图片
plt.title('原图')
plt.show()
# 将numpy数组的图像数据重塑为torch的张量格式,以便后续使用torch的功能进行处理
im = torch.from_numpy(im.reshape((1, 1, im.shape[0], im.shape[1])))
# 定义一个卷积层,输入通道数为1,输出通道数为1,卷积核的大小为3x3,偏置项为False,因为我们将自己定义卷积核
conv1 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
# 定义一个Sobel算子,用于轮廓检测,这个算子的大小也是3x3,元素值除了中心位置为8之外,其余位置为-1
sobel_kernel = np.array([[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]], dtype='float32') #对图像进行高斯模糊的卷积核,可以换成别的卷积核进行运算
sobel_kernel = sobel_kernel.reshape((1, 1, 3, 3)) # 适配卷积的输入输出
conv1.weight.data = torch.from_numpy(sobel_kernel) # 给卷积的 kernel 赋值 将Sobel算子赋值给卷积层的卷积核
# 将定义的图片和卷积层进行运算,得到边缘检测的结果
edge1 = conv1(Variable(im)) # 作用在图片上
# 将运算结果转换为numpy数组并去除可能存在的额外维度,以便后续处理和显示
x = edge1.data.squeeze().numpy() # squeeze方法可以去掉尺寸为1的维度,这样x就是一个二维数组了
print(x.shape) # 可以查看x的形状,用于确认处理结果
plt.imshow(x, cmap='gray')
plt.show()
5. 总结不同卷积核的特征和作用。
这里我参考:Image Kernels explained visually (setosa.io)网站对不同的卷积核进行总结。
图片来自:【2021-2022 春学期】人工智能-作业4:CNN - 卷积_锐化卷积核_windZ44Z的博客-CSDN博客
1. 高斯模糊卷积核:
高斯模糊使卷积结果在水平和垂直方向呈现高斯分布,更突出了中心点在像素模糊后的权重,相比于均值滤波而言,有着更好的模糊效果。
2. 锐化卷积核:
该卷积核利用的是图像中的边缘信息有着比周围像素更高的对比度,而经过卷积之后进一步增强了这种对比度,从而使图像显得棱角分明、画面清晰,起到锐化图像的效果。
3. 边缘检测卷积核
边缘检测卷积核利用边缘部分的两侧的数值会有较大差别的原理
当卷积核与边缘区域作卷积运算时得出的结果较大,在图片中显示外白色
在非边缘部分计算的卷积结果较小,表现为黑色;由此可以突出边缘显示。
总结:
1. 我深刻学习到了不同的卷积核提取不同的特征这句话,我认为卷积核可以看作是一种模板,它通过对图像进行卷积运算,能够提取出图像中与该模板相匹配的特征。例如,一个卷积核对图像中的边缘特征更加敏感,而另一个卷积核对图像中的纹理特征更加敏感。因此,通过设计不同的卷积核,可以提取出图像中不同的特征。
2. 在 Image Kernels explained visually (setosa.io)网站上可以更加直观的了解到卷积的过程
可以通过调整不同的卷积核来体会它的作用,同时,也可以通过这个网站查看自己运行的结果是否正确。
3. squeeze()可以去掉尺寸为1的维度。
squeeze()
函数会遍历输入张量的每一个维度,如果发现某个维度的尺寸为1,就会将这个维度挤压掉,从而得到一个新的张量。这个新的张量的形状就是原始张量去掉所有尺寸为1的维度后的形状。
4. 我发现卷积核提取特征的效果和分辨率相关,分辨率越高提取出的特征也越清晰,因为图片本身含有的像素点多,和卷积核进行运算的像素点多,提取出的特征更加清晰。
参考链接:
最容易理解的对卷积(convolution)的解释 - 交流_QQ_2240410488 - 博客园
深度学习:卷积神经网络(详解版)_卷积神经网络 详解-CSDN博客