从入门到实践:探索深度学习中的卷积神经网络(CNN)详解与代码实现
卷积神经网络(CNN)详解与代码实现
目录
1.应用场景
2.卷积神经网络结构
2.1 卷积(convelution)
2.2 Relu激活函数
2.3 池化(pool)
2.4 全连接(full connection)
2.5 损失函数(softmax_loss)
2.6 前向传播(forward propagation)
2.7 反向传播(backford propagation)
2.8 随机梯度下降(sgd_momentum)
3.代码实现流程图以及介绍
4.代码实现(python3.6)
5.运行结果以及分析
6.参考文献
1.应用场景
卷积神经网络的应用不可谓不广泛,主要有两大类,数据预测和图片处理。数据预测自然不需要多说,图片处理主要包含有图像分类,检测,识别,以及分割方面的应用。
图像分类:场景分类,目标分类
图像检测:显著性检测,物体检测,语义检测等等
图像识别:人脸识别,字符识别,车牌识别,行为识别,步态识别等等
图像分割:前景分割,语义分割
2.卷积神经网络结构
卷积神经网络主要是由输入层、卷积层、激活函数、池化层、全连接层、损失函数组成,表面看比较复杂,其实质就是特征提取以及决策推断。
要使特征提取尽量准确,就需要将这些网络层结构进行组合,比如经典的卷积神经网络模型AlexNet:5个卷积层+3个池化层+3个连接层结构。
2.1 卷积(convolution)
卷积的作用就是提取特征,因为一次卷积可能提取的特征比较粗糙,所以多次卷积,以及层层纵深卷积,层层提取特征(千万要区别于多次卷积,因为每一层里含有多次卷积)。
这里可能就有小伙伴问:为什么要进行层层纵深卷积,而且还要每层多次?
你可以理解为物质A有自己的多个特征(高、矮、胖、瘦、、、),所以在物质A上需要多次提取,得到不同的特征,然后这些特征组合后发生化学反应生成物质B,
而物质B又有一些新的专属于自己的特征,所以需要进一步卷积。这是我个人的理解,不对的话或者有更形象的比喻还请不吝赐教啊。
在卷积层中,每一层的卷积核是不一样的。比如AlexNet
第一层:96*11*11(96表示卷积核个数,11表示卷积核矩阵宽*高) stride(步长) = 4 pad(边界补零) = 0
第二层:256*5*5 stride(步长) = 1 pad(边界补零) = 2
第三,四层:384*3*3 stride(步长) = 1 pad(边界补零) = 1
第五层:256*3*3 stride(步长) = 1 pad(边界补零) = 2
卷积的篇幅说了这么多,那么到底是如何进行运算的呢,虽说网络上关于卷积运算原理铺天盖地,但是个人总感觉讲得不够透彻,或者说本人智商有待提高,
希望通过如下这幅图(某位大神的杰作)来使各位看官们能够真正理解。
这里举的例子是一个输入图片(5*5*3),卷积核(3*3*3),有两个(Filter W0,W1),偏置b也有两个(Bios b0,b1),卷积结果Output Volumn(3*3*2),步长stride = 2。
输入:7*7*3 是因为 pad = 1 (在图片边界行和列都补零,补零的行和的数目是1),
(对于彩色图片,一般都是RGB3种颜色,号称3通道,7*7指图片高h * 宽w)
,补零的作用是能够提取图片边界的特征。
卷积核深度为什么要设置成3呢?这是因为输入是3通道,所以卷积核深度必须与输入的深度相同。至于卷积核宽w,高h则是可以变化的,但是宽高必须相等。
卷积核输出o[0,0,0] = 3 (Output Volumn下浅绿色框结果),这个结果是如何得到的呢? 其实关键就是矩阵对应位置相乘再相加(千万不要跟矩阵乘法搞混淆啦)
=> w0[:,:,0] * x[:,:,0]蓝色区域矩阵(R通道) + w0[:,:,1] * x[:,:,1]蓝色区域矩阵(G通道)+ w0[:,:,2] * x[:,:,2]蓝色区域矩阵(B通道) + b0(千万不能丢,因为 y = w * x + b)
第一项 => 0 * 1 + 0 * 1 + 0 * 1 + 0 * (-1) + 1 * (-1) + 1 * 0 + 0 * (-1) + 1 * 1 + 1 * 0 = 0
第二项 => 0 * (-1) + 0 * (-1) + 0 * 1 + 0 * (-1) + 0 * 1 + 1 * 0 + 0 * (-1) + 2 * 1 + 2 * 0 = 2
第三项 => 0 * 1 + 0 * 0 + 0 * (-1) + 0 * 0 + 2 * 0 + 2 * 0 + 0 * 1 + 0 * (-1) + 0 * (-1) = 0
卷积核输出o[0,0,0] = > 第一项 + 第二项 + 第三项 + b0 = 0 + 2 + 0 + 1 = 3
o[0,0,1] = -5 又是如何得到的呢?
因为这里的stride = 2 ,所以 输入的窗口就要滑动两个步长,也就是红色框的区域,而运算跟之前是一样的
第一项 => 0 * 1 + 0 * 1 + 0 * 1 + 1 * (-1) + 2 * (-1) + 2 * 0 + 1 * (-1) + 1 * 1 + 2 * 0 = -3
第二项 => 0 * (-1) + 0 * (-1) + 0 * 1 + 1 * (-1) + 2 * 1 + 0 * 0 + 2 * (-1) + 1 * 1 + 1 * 0 = 0
第三项 => 0 * 1 + 0 * 0 + 0 * (-1) + 2 * 0 + 0 * 0 + 1 * 0 + 0 * 1 + 2 * (-1) + 1 * (-1) = - 3
卷积核输出o[0,0,1] = > 第一项 + 第二项 + 第三项 + b0 = (-3) + 0 + (-3) + 1 = -5
之后以此卷积核窗口大小在输入图片上滑动,卷积求出结果,因为有两个卷积核,所有就有两个输出结果。
这里小伙伴可能有个疑问,输出窗口是如何得到的呢?
这里有一个公式:输出窗口宽 w = (输入窗口宽 w - 卷积核宽 w + 2 * pad)/stride + 1 ,输出高 h = 输出窗口宽 w
以上面例子, 输出窗口宽 w = ( 5 - 3 + 2 * 1)/2 + 1 = 3 ,则输出窗口大小为 3 * 3,因为有2个输出,所以是 3*3*2。
2.2 Relu激活函数
相信看过卷积神经网络结构(CNN)的伙伴们都知道,激活函数无处不在,特别是CNN中,在卷积层后,全连接(FC)后都有激活函数Relu的身影,
那么这就自然不得不让我们产生疑问:
问题1、为什么要用激活函数?它的作用是什么?
问题2、在CNN中为什么要用Relu,相比于sigmoid,tanh,它的优势在什么地方?
对于第1个问题:由 y = w * x + b 可知,如果不用激活函数,每个网络层的输出都是一种线性输出,而我们所处的现实场景,其实更多的是各种非线性的分布。
这也说明了激活函数的作用是将线性分布转化为非线性分布,能更逼近我们的真实场景。
对于第2个问题: 先看sigmoid,tanh分布
他们在 x -> 时,输出就变成了恒定值,因为求梯度时需要对函数求一阶偏导数,而不论是sigmoid,还是tanhx,他们的偏导都为0,
也就是存在所谓的梯度消失问题,最终也就会导致权重参数w , b 无法更新。相比之下,Relu就不存在这样的问题,另外在 x > 0 时,
Relu求导 = 1,这对于反向传播计算dw,db,是能够大大的简化运算的。
使用sigmoid还会存在梯度爆炸的问题,比如在进行前向传播和反向传播迭代次数非常多的情况下,sigmoid因为是指数函数,其结果中
某些值会在迭代中累积,并成指数级增长,最终会出现NaN而导致溢出。
2.3 池化
池化层一般在卷积层+ Relu之后,它的作用是:
1、减小输入矩阵的大小(只是宽和高,而不是深度),提取主要特征。(不可否认的是,在池化后,特征会有一定的损失,所以,有些经典模型就去掉了池化这一层)。
它的目的是显而易见的,就是在后续操作时能降低运算。
2、一般采用mean_pooling(均值池化)和max_pooling(最大值池化),对于输入矩阵有translation(平移),rotation(旋转),能够保证特征的不变性。
mean_pooling 就是输入矩阵池化区域求均值,这里要注意的是池化窗口在输入矩阵滑动的步长跟stride有关,一般stride = 2.(图片是直接盗过来,这里感谢原创)
最右边7/4 => (1 + 1 + 2 + 3)/4
max_pooling 最大值池化,就是每个池化区域的最大值放在输出对应位置上。
2.4 全连接(full connection)
作用:分类器角色,将特征映射到样本标记空间,本质是矩阵变换(affine)。
至于变换的实现见后面的代码流程图,或者最好是跟一下代码,这样理解更透彻。
2.5 损失函数(softmax_loss)
作用:计算损失loss,从而求出梯度grad。
常用损失函数有:MSE均方误差,SVM(支持向量机)合页损失函数,Cross Entropy交叉熵损失函数。
这几种损失函数目前还看不出谁优谁劣,估计只有在具体的应用场景中去验证了。至于这几种损失函数的介绍,
大家可以去参考《常用损失函数小结》https://blog.****.net/zhangjunp3/article/details/80467350,这个哥们写得比较详细。
在后面的代码实例中,用到的是softmax_loss,它属于Cross Entropy交叉熵损失函数。
softmax计算公式:
其中, 是要计算的类别 的网络输出,分母是网络输出所有类别之和(共有 个类别), 表示第 类的概率。
交叉熵损失:
其中, 是类别 的真实标签, 表示第 类的概率, 是样本总数, 是类别数。
梯度:
= 当 !=
= - 1 当 =
其中 表示真实标签对应索引下预测的目标值, 类别索引。
这个有点折磨人,原理讲解以及推导请大家可以参考这位大神的博客:http://www.cnblogs.com/zongfa/p/8971213.html。
2.6 前向传播(forward propagation)
前向传播包含之前的卷积,Relu激活函数,池化(pool),全连接(fc),可以说,在损失函数之前操作都属于前向传播。
主要是权重参数w , b 初始化,迭代,以及更新w, b,生成分类器模型。
2.7 反向传播(back propagation)
反向传播包含损失函数,通过梯度计算dw,db,Relu激活函数逆变换,反池化,反全连接。
2.8 随机梯度下降(sgd_momentum)
作用:由梯度grad计算新的权重矩阵w
sgd公式:
其中,η为学习率,gt为x在t时刻的梯度。
一般我们是将整个数据集分成n个epoch,每个epoch再分成m个batch,每次更新都利用一个batch的数据,而非整个训练集。
优点:batch的方法可以减少机器的压力,并且可以更快地收敛。
缺点:其更新方向完全依赖于当前的batch,因而其更新十分不稳定。
为了解决这个问题,momentum就横空出世了,具体原理详解见下路派出所(这名字霸气)的博客http://www.cnblogs.com/callyblog/p/8299074.html。
momentum即动量,它模拟的是物体运动时的惯性,即更新的时候在一定程度上保留之前更新的方向,同时利用当前batch的梯度微调最终的更新方向。
这样一来,可以在一定程度上增加稳定性,从而学习地更快,并且还有一定摆脱局部最优的能力:
其中,ρ 即momentum,表示要在多大程度上保留原来的更新方向,这个值在0-1之间,在训练开始时,由于梯度可能会很大,所以初始值一般选为0.5;
当梯度不那么大时,改为0.9。η 是学习率,即当前batch的梯度多大程度上影响最终更新方向,跟普通的SGD含义相同。ρ 与 η 之和不一定为1。
3.代码实现流程图以及介绍
代码流程图:费了老大劲,终于弄完了,希望对各位看官们有所帮助,建议对比流程图和跟踪代码,加深对原理的理解。
特别是前向传播和反向传播维度的变换,需要重点关注。
4.代码实现
当然,代码的整个实现是某位大神实现的,我只是在上面做了些小改动以及重点函数做了些注释,有不妥之处也希望大家不吝指教。
因为原始图片数据集太大,不好上传,大家可以直接在http://www.cs.toronto.edu/~kriz/cifar.html下载CIFAR-10 python version,
有163M,放在代码文件同路径下即可。
start.py
1 # -*- coding: utf-8 -*-
2 import matplotlib.pyplot as plt
3 '''同路径下py模块引用'''
4
5 try:
6 from . import data_utils
7 from . import solver
8 from . import cnn
9 except Exception:
10 import data_utils
11 import solver
12 import cnn
13
14 import numpy as np
15 # 获取样本数据
16 data = data_utils.get_CIFAR10_data()
17 # model初始化(权重因子以及对应偏置 w1,b1 ,w2,b2 ,w3,b3,数量取决于网络层数)
18 model = cnn.ThreeLayerConvNet(reg=0.9)
19 solver = solver.Solver(model, data,
20 lr_decay=0.95,
21 print_every=10, num_epochs=5, batch_size=2,
22 update_rule='sgd_momentum',
23 optim_config={'learning_rate': 5e-4, 'momentum': 0.9})
24 # 训练,获取最佳model
25 solver.train()
26
27 plt.subplot(2, 1, 1)
28 plt.title('Training loss')
29 plt.plot(solver.loss_history, 'o')
30 plt.xlabel('Iteration')
31
32 plt.subplot(2, 1, 2)
33 plt.title('Accuracy')
34 plt.plot(solver.train_acc_history, '-o', label='train')
35 plt.plot(solver.val_acc_history, '-o', label='val')
36 plt.plot([0.5] * len(solver.val_acc_history), 'k--')
37 plt.xlabel('Epoch')
38 plt.legend(loc='lower right')
39 plt.gcf().set_size_inches(15, 12)
40 plt.show()
41
42
43 best_model = model
44 y_test_pred = np.argmax(best_model.loss(data['X_test']), axis=1)
45 y_val_pred = np.argmax(best_model.loss(data['X_val']), axis=1)
46 print ('Validation set accuracy: ',(y_val_pred == data['y_val']).mean())
47 print ('Test set accuracy: ', (y_test_pred == data['y_test']).mean())
48 # Validation set accuracy: about 52.9%
49 # Test set accuracy: about 54.7%
50
51
52 # Visualize the weights of the best network
53 """
54 from vis_utils import visualize_grid
55
56 def show_net_weights(net):
57 W1 = net.params['W1']
58 W1 = W1.reshape(3, 32, 32, -1).transpose(3, 1, 2, 0)
59 plt.imshow(visualize_grid(W1, padding=3).astype('uint8'))
60 plt.gca().axis('off')
61 show_net_weights(best_model)
62 plt.show()
63 """
cnn.py
1 # -*- coding: utf-8 -*-
2 try:
3 from . import layer_utils
4 from . import layers
5 except Exception:
6 import layer_utils
7 import layers
8 import numpy as np
9
10 class ThreeLayerConvNet(object):
11 """
12 A three-layer convolutional network with the following architecture:
13 conv - relu - 2x2 max pool - affine - relu - affine - softmax
14 """
15
16 def __init__(self, input_dim=(3, 32, 32), num_filters=32, filter_size=7,
17 hidden_dim=100, num_classes=10, weight_scale=1e-3, reg=0.0,
18 dtype=np.float32):
19 self.params = {}
20 self.reg = reg
21 self.dtype = dtype
22
23 # Initialize weights and biases
24 C, H, W = input_dim
25 self.params['W1'] = weight_scale * np.random.randn(num_filters, C, filter_size, filter_size)
26 self.params['b1'] = np.zeros(num_filters)
27 self.params['W2'] = weight_scale * np.random.randn(num_filters*H*W//4, hidden_dim)
28 self.params['b2'] = np.zeros(hidden_dim)
29 self.params['W3'] = weight_scale * np.random.randn(hidden_dim, num_classes)
30 self.params['b3'] = np.zeros(num_classes)
31
32 for k, v in self.params.items():
33 self.params[k] = v.astype(dtype)
34
35
36 def loss(self, X, y=None):
37 W1, b1 = self.params['W1'], self.params['b1']
38 W2, b2 = self.params['W2'], self.params['b2']
39 W3, b3 = self.params['W3'], self.params['b3']
40
41 # pass conv_param to the forward pass for the convolutional layer
42 filter_size = W1.shape[2]
43 conv_param = {'stride': 1, 'pad': (filter_size - 1) // 2}
44
45 # pass pool_param to the forward pass for the max-pooling layer
46 pool_param = {'pool_height': 2, 'pool_width': 2, 'stride': 2}
47
48 # compute the forward pass
49 a1, cache1 = layer_utils.conv_relu_pool_forward(X, W1, b1, conv_param, pool_param)
50 a2, cache2 = layer_utils.affine_relu_forward(a1, W2, b2)
51 scores, cache3 = layers.affine_forward(a2, W3, b3)
52
53 if y is None:
54 return scores
55
56 # compute the backward pass
57 data_loss, dscores = layers.softmax_loss(scores, y)
58 da2, dW3, db3 = layers.affine_backward(dscores, cache3)
59 da1, dW2, db2 = layer_utils.affine_relu_backward(da2, cache2)
60 dX, dW1, db1 = layer_utils.conv_relu_pool_backward(da1, cache1)
61
62 # Add regularization 引入修正因子,重新计算损失,梯度
63 dW1 += self.reg * W1
64 dW2 += self.reg * W2
65 dW3 += self.reg * W3
66 reg_loss = 0.5 * self.reg * sum(np.sum(W * W) for W in
推荐阅读
-
[姿势估计] 实践记录:使用 Dlib 和 mediapipe 进行人脸姿势估计 - 本文重点介绍方法 2):方法 1:基于深度学习的方法:。
基于深度学习的方法:基于深度学习的方法利用深度学习模型,如卷积神经网络(CNN)或递归神经网络(RNN),直接从人脸图像中学习姿势估计。这些方法能够学习更复杂的特征表征,并在大规模数据集上取得优异的性能。方法二:基于二维校准信息估计三维姿态信息(计算机视觉 PnP 问题)。
特征点定位:人脸姿态估计的第一步是通过特征点定位来检测和定位人脸的关键点,如眼睛、鼻子和嘴巴。这些关键点提供了人脸的局部结构信息,可用于后续的姿势估计。
旋转表示:常见的旋转表示方法包括欧拉角和旋转矩阵。欧拉角通过三个旋转角度(通常是俯仰、偏航和滚动)描述头部的旋转姿态。旋转矩阵是一个 3x3 矩阵,表示头部从一个坐标系到另一个坐标系的变换。
三维模型重建:根据特征点的定位结果,三维人脸模型可用于姿势估计。通过将人脸的二维图像映射到三维模型上,可以估算出人脸的旋转和平移信息。这就需要建立人脸的三维模型,然后通过优化方法将模型与特征点对齐,从而获得姿势估计结果。
特征点定位
特征点定位是用于检测人脸关键部位的五官基础部分,还有其他更多的特征点表示方法,大家可以参考我上一篇文章中介绍的特征点检测方案实践:人脸校正二次定位操作来解决人脸校正的问题,客户在检测关键点的代码上略有修改,坐标转换部分客户见上图
def get_face_info(image).
img_copy = image.copy
image.flags.writeable = False
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = face_detection.process(image)
# 在图像上绘制人脸检测注释。
image.flags.writeable = True
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
box_info, facial = None, None
if results.detections: for detection in results.
for detection in results.detections: mp_drawing.Drawing.detection = 无
mp_drawing.draw_detection(image, detection)
面部 = detection.location_data.relative_keypoints
返回面部
在上述代码中,返回的数据是五官(6 个关键点的坐标),这是用 mediapipe 库实现的,下面我们可以尝试用另一个库:dlib 来实现。
使用 dlib
使用 Dlib 库在 Python 中实现人脸关键点检测的步骤如下:
确保已安装 Dlib 库,可使用以下命令: pip install dlib
导入必要的库:
加载 Dlib 的人脸检测器和关键点检测器模型:
读取图像并将其灰度化:
使用人脸检测器检测图像中的人脸:
对检测到的人脸进行遍历,并使用关键点检测器检测人脸关键点:
显示绘制了关键点的图像:
以下代码将参数 landmarks_part 添加到要返回的关键点坐标中。
-
从入门到实践:探索深度学习中的卷积神经网络(CNN)详解与代码实现