深度学习 - 卷积神经网络(CNN)介绍+实例说明

发布时间 2023-07-11 14:25:47作者: yinghualeihenmei

https://blog.csdn.net/weixin_46072771/article/details/108590347

卷积神经网络(CNN)简介
CNN基础
前面我们讲解了机器学习基础知识,包括多层感知器等问题。
下面我们要介绍的目标识别与分类,就是在前面问题的基础上进行扩展,实现对于图像等分类和识别。实现对图像的高准确率识别离不开一种叫做卷积神经网络的深度学习技术。

卷积神经网络主要应用于计算机视觉相关任务,但它能处理的任务并不局限于图像,其实语音识别也是可以使用卷积神经网络。

下面将专注于在keras中使用卷积神经网络(CNN)来处理图像。
我们将使用识别Mnist手写数字、cifar10图像数据以及猫和狗图像识别数据来让大家对于卷积神经网络有一个大概的了解。

什么是卷积神经网络?
当计算机看到一张图像(输入一张图像)时,它看的是一大堆像素值。(如下图)

 

当我们人类对图像进行分类时,这些数字毫无用处,可它们却是计算机可获得的唯一输入。

现在的问题是:当你提供给计算机这一数组后,它将输出描述该图像属于某一特定分类的概率的数字(比如:80% 是猫、15% 是狗、5%是鸟)。

我们人类是通过特征来区分猫和狗,现在想要计算机能够区分开猫和狗图片,就要计算机搞清楚猫猫狗狗各自的特有特征。计算机可以通过寻找诸如边缘和曲线之类的低级特点来分类图片,继而通过一系列卷积层级建构出更为抽象的概念。这是 CNN(卷积神经网络)工作方式的大体概述。

为什叫卷积神经网络?
CNN 的确是从视觉皮层的生物学上获得启发的。
简单来说:视觉皮层有小部分细胞对特定部分的视觉区域敏感。
例如:一些神经元只对垂直边缘兴奋,另一些对水平或对角边缘兴奋。

CNN 工作概述指的是你挑一张图像,让它历经一系列卷积层、非线性层、池化(下采样(downsampling))层和全连接层,最终得到输出。正如之前所说,输出可以是最好地描述了图像内容的一个单独分类或一组分类的概率。

增加卷积层 tf.keras.layers.Conv2D() -- 增强提取特征的能力
增加池化层 tf.keras.layers.MaxPooling2D() -- 使得模型的视野变大
Dropout层 tf.keras.layers.Dropout() -- 抑制过拟合(当发现模型过拟合的时候再添加,不然可能会影响正常拟合正确率)
全连接层 tf.keras.layers.Dense(10, activation='softmax') -- 最终输出预测结果

增加卷积层 tf.keras.layers.Conv2D() -- 增强提取特征的能力
增加池化层 tf.keras.layers.MaxPooling2D() -- 使得模型的视野变大
Dropout层 tf.keras.layers.Dropout() -- 抑制过拟合(当发现模型过拟合的时候再添加,不然可能会影响正常拟合正确率)
全连接层 tf.keras.layers.Dense(10, activation='softmax') -- 最终输出预测结果

什么是卷积?
卷积是指将卷积核应用到某个张量的所有点上,通过将卷积核在输入的张量上滑动而生成经过滤波处理的张量。

总结起来一句话:
卷积完成的是 对图像特征的提取或者说信息匹配,当一个包含某些特征的图像经过一个卷积核的时候,一些卷积核被激活,输出特定信号。

 

关于图片数据的基础知识:链接?

卫星图片二分类实例(湖泊与机场)
import random # 进行乱序
import pathlib # 面向对象的路径管理工具
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
import IPython.display as display # 用来显示图片

图像数据的导入,与预处理
数据路径的提取
data_root = pathlib.Path('./数据集/飞机场和湖泊图片(二分类数据)')
data_root
>>>PosixPath('数据集/飞机场和湖泊图片(二分类数据)')

# 对目录进行迭代
for item in data_root.iterdir():
print(item)
>>>数据集/飞机场和湖泊图片(二分类数据)/.DS_Store
数据集/飞机场和湖泊图片(二分类数据)/lake
数据集/飞机场和湖泊图片(二分类数据)/airplane

.DS_Store(英文全称 Desktop Services Store)是一种由苹果公司的Mac OS X操作系统所创造的隐藏文件,目的在于存贮目录的自定义属性,例如文件们的图标位置或者是背景色的选择。相当于 Windows 下的 desktop.ini。

提取路径下所有的数据

# 全局提取data_root.glob(正则表达式)
all_image_path = list(data_root.glob('*/*'))

# 记录图片总数
image_count = len(all_image_path)
print('共有', image_count, '张照片')
>>>共有 1400 张照片

# 转化为真正的路径(str数据类型)
all_image_path = [str(path) for path in all_image_path]
all_image_path[697: 703]
>>>['数据集/飞机场和湖泊图片(二分类数据)/lake/lake_039.jpg',
'数据集/飞机场和湖泊图片(二分类数据)/lake/lake_011.jpg',
'数据集/飞机场和湖泊图片(二分类数据)/lake/lake_005.jpg',
'数据集/飞机场和湖泊图片(二分类数据)/airplane/airplane_079.jpg',
'数据集/飞机场和湖泊图片(二分类数据)/airplane/airplane_045.jpg',
'数据集/飞机场和湖泊图片(二分类数据)/airplane/airplane_051.jpg']

提取类别名(湖泊 or 机场)

label_names = sorted(item.name for item in data_root.glob('*/')).remove('.DS_Store')
label_names
>>>['airplane', 'lake']

key(中文类别) — value(顺序编码),这样方便之后提取每个训练集照片所属的类别

label_to_index = dict((name, index) for index, name in enumerate(label_names))
label_to_index
>>>{'airplane': 0, 'lake': 1}

key(顺序编码) — value(中文类别),训练好模型后,使通过模型预测得结果由顺序编码翻译为类别名称

index_to_value = dict((value, key) for key, value in label_to_index.items())
index_to_value
>>>{0: 'airplane', 1: 'lake'}

提取标签集

# 上边已经提取了所有照片的路径
all_image_path[:3]
>>>['数据集/飞机场和湖泊图片(二分类数据)/lake/lake_133.jpg',
'数据集/飞机场和湖泊图片(二分类数据)/lake/lake_444.jpg',
'数据集/飞机场和湖泊图片(二分类数据)/airplane/airplane_203.jpg']

# 从上边可以看出,每张图变得上一路径名称就是其所属类别
# 通过 .parent.name方法,提取上层路径名称
pathlib.Path(all_image_path[0]).parent.name
>>>'lake'

# 0 -- 机场(airplane)
# 1 -- 湖泊(lake)
all_image_label = [label_to_index[pathlib.Path(path).parent.name] for path in all_image_path]
all_image_label[:3]
>>>[1, 1, 0]

到此为止,我们已经找到可所有数据的路径。
来检验一下是否正确;

for i in range(3):
image_index = random.choice(range(len(all_image_path))) # 随机抽取索引
display.display(display.Image(all_image_path[image_index]))
print('上图所属类别:', index_to_value[all_image_label[image_index]])
print('---------------')

总结一下现在有哪些数据
说明(调用方法): 示例
图片样本个数(image_count): 1400
所有图片路径(all_image_path): 数据集/飞机场和湖泊图片(二分类数据)/lake/lake_133.jpg
所有图片类别(all_image_label): 顺序编码(二分类)
类别标签(index_to_value): {0: ‘airplane’, 1: ‘lake’}
类别标签(label_to_index): {‘airplane’: 0, ‘lake’: 1}

解码图片数据
def load_image(image_path):
"""
该函数用于迭代,返回每一张图片的数据
image_path:图片路径
"""
img_raw = tf.io.read_file(image_path) # 读取图片为二进制格式
# img_tensor = tf.image.decode_image(img_raw) #(通用)由二进制格式解码为常用格式(但是type=unit8)

img_tensor = tf.image.decode_jpeg(img_raw, channels=3) # 使用.jpeg图片专用的解码器

img_tensor = tf.image.resize(img_tensor, [256, 256])
# 改变形状(直接压缩变形,看着可能会变形,但对电脑来说没影响)
# img_tensor = tf.image.resize_with_crop_or_pad(img_tensor, [256, 256])
# 改变形状 通过裁剪或填充的方式,看着不会变形

img_tensor = tf.cast(img_tensor, tf.float32) # 数据类型转化为float32
img = img_tensor/255.0 # 进行归一化
return img


对所有图片路径进行切片操作

# 对所有图片路径进行切片操作
path_ds = tf.data.Dataset.from_tensor_slices(all_image_path)
path_ds
>>><TensorSliceDataset shapes: (), types: tf.string>

A.map(指定操作) 迭代A中的所有数据进行指定操作,并且将结果放在与A中对应的位置(代替原来的元素)

# A.map(指定操作) 迭代A中的所有数据进行指定操作,并且将结果放在与A中对应的位置(代替原来的元素)
image_dataset = path_ds.map(load_image)
image_dataset
>>><MapDataset shapes: (256, 256, 3), types: tf.float32>

# 顺便对标签集也进行切片操作
image_label = tf.data.Dataset.from_tensor_slices(all_image_label)
image_label
>>><TensorSliceDataset shapes: (), types: tf.int32>

合并图片集和类别集(方便之后进行batch和模型的训练)

dataset = tf.data.Dataset.zip((image_dataset, image_label))
dataset
>>><ZipDataset shapes: ((256, 256, 3), ()), types: (tf.float32, tf.int32)>

划分训练集和测试集

train_count = int(image_count * 0.8) # 取80%做为训练集
test_count = int(image_count - train_count) # 剩下的用作测试集

# .skip(int) 跳过指定个数的数据
train_dataset = dataset.skip(test_count)
# .take(int) 保留指定个数的数据
test_dataset = dataset.take(test_count)

关于batch,repeat,shuffle的讲解:推荐,官方

batch_size = 32

# .无限循环.乱序.分批(32个样本/批)
train_dataset = train_dataset.repeat().shuffle(buffer_size=train_count).batch(batch_size)
test_dataset = test_dataset.batch(batch_size)

到此为止,所有的数据整理完毕!

构建卷积神经网络模型
关于filters输出维度由输入层开始,逐层翻倍递增;到全局平均值化层,至峰值;再由全连接层逐层递减至合适维度,最终输出。
输出维度相同的卷积层可以同时出现在相接层处,后边再接一个池化层。(这样可以看为一组,然后多加几组。)

三种激活函数的区别:链接?

卷积层 tf.keras.layers.Conv2D()
参数说明:

filters = 32 /输出维度 [一般选择 2^n 拟合效果最佳];
kernel_size = (3, 3) /卷积核大小 [一般选择(3,3)或(5,5),偶尔会有(1,1)];
input_shape = 输入维度 [单张图片的数据维度,要减掉样本数的那个维度];
activation = 激活函数 [‘relu’, ‘sigmiod’, ‘tanh’]选一
padding = "valid"或"same"选其一(忽略大小写)
选择‘same’,则输入(None, 28, 28, 1)?(None, 28, 28, 32)输出
选择‘valid’,则输入(None, 28, 28, 1)?(None, 26, 26, 32)输出
作用:加强数据的特点(使之更加明显)
池化层 tf.keras.layers.MaxPooling2D()
默认参数:减小50% [(28, 28)?(14, 14)]
作用:进行最大池化,将减小图片尺寸。增大卷积核的视野,看的更加全面(虽然卷积层1/2的卷积核大小都是(3,3)但是卷积层2的卷积核经过了池化层,它所代表的内容是原来的2倍(9,9))。

全局平均值化层 tf.keras.layers.GlobalAveragePooling2D()
作用:由4维数据转化为2维数据,为了与全连接层对接

全连接层 tf.keras.layers.Dense()
重要参数:activation=‘softmax’
传入的是扁平化数据,可以多几时维度慢慢降下来。
作用:将最后的输出与全部特征连接,我们要使用全部的特征,为最后的分类的做出决策。

Dropout层 tf.keras.layers.Dropout()
抑制过拟合(当发现模型过拟合的时候再添加,不然可能会影响正常拟合正确率)

构建模型

model = tf.keras.Sequential() #顺序模型

# 卷积层(*2) + (池化层) = 1组
# 第1组
model.add(tf.keras.layers.Conv2D(64, (3, 3), input_shape=(256, 256, 3), activation='relu'))
model.add(tf.keras.layers.Conv2D(64, (3, 3), activation='relu'))
model.add(tf.keras.layers.MaxPooling2D())

# 第2组
model.add(tf.keras.layers.Conv2D(128, (3, 3), activation='relu'))
model.add(tf.keras.layers.Conv2D(128, (3, 3), activation='relu'))
model.add(tf.keras.layers.MaxPooling2D())

# 第3组
model.add(tf.keras.layers.Conv2D(256, (3, 3), activation='relu'))
model.add(tf.keras.layers.Conv2D(256, (3, 3), activation='relu'))
model.add(tf.keras.layers.MaxPooling2D())

# 第4组
model.add(tf.keras.layers.Conv2D(512, (3, 3), activation='relu'))
model.add(tf.keras.layers.MaxPooling2D())

# 第5组
model.add(tf.keras.layers.Conv2D(512, (3, 3), activation='relu'))
model.add(tf.keras.layers.MaxPooling2D())

# 第6组
model.add(tf.keras.layers.Conv2D(1024, (3, 3), activation='relu'))

# 全局平均值化层:卷积层和全连接层对接(输出维度的峰值层)
model.add(tf.keras.layers.GlobalAveragePooling2D())

# 全连接层:输出维度开始回调
model.add(tf.keras.layers.Dense(1024, activation='relu'))
model.add(tf.keras.layers.Dense(256, activation='relu'))
# 最终使用‘softmax’作激活函数构建输出层
model.add(tf.keras.layers.Dense(10, activation='softmax'))


构建优化器

# loss函数的区别和使用环境?
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['acc'])
1
2
3
4
训练模型

# 分别计算单次训练要迭代的次数(因为之前有 .batch(batch_size)) 才可以使用到所有的数据
steps_per_epoch = train_count//batch_size
validation_steps = test_count//batch_size

# epochs=10 取值比较低(约10*5min,烧鸡警告?)
history = model.fit(train_dataset, epochs=10, steps_per_epoch=steps_per_epoch,
validation_data=test_dataset, validation_steps=validation_steps)
>>>Train for 35 steps, validate for 8 steps
Epoch 1/10
35/35 [==============================] - 331s 9s/step - loss: 1.5925 - acc: 0.4938 - val_loss: 0.6857 - val_acc: 0.5469
Epoch 2/10
35/35 [==============================] - 327s 9s/step - loss: 0.5643 - acc: 0.7054 - val_loss: 0.3401 - val_acc: 0.9023
...

观察记录了哪些评量值

history.history.keys()
>>>dict_keys(['loss', 'acc', 'val_loss', 'val_acc'])
1
2
随着训练,测试集和训练集所测得的评量值变化

plt.plot(history.epoch, history.history.get('acc'), label='acc')
plt.plot(history.epoch, history.history.get('val_acc'), label='val_acc')
plt.legend()

plt.plot(history.epoch, history.history.get('loss'), label='loss')
plt.plot(history.epoch, history.history.get('val_loss'), label='val_loss')
plt.legend()
————————————————

版权声明:本文为CSDN博主「壮壮不太胖^QwQ」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_46072771/article/details/108590347