深度学习实战:基于卷积神经网络的图形分类

公众号:尤而小屋

作者:Peter

编辑:Peter

大家好,我是Peter~

本文是深度学习实战案例连载更新的第一篇文章:基于Keras实现卷积神经网络CNN对图像的二分类识别,包含完整的建模代码。

数据获取

数据主要是两个zip压缩文件:train.zip(569+M)和test.zip(284+M),获取方式:

1、文末提供基于百度网盘数据获取方式;

2、如果你可以科学上网,也可以在kaggle官网下载开源数据集:www.kaggle.com/competition...

3、直接添加小编好友获取

下面的文章都是基于你已经获取压缩文件,并且解压到了本地,生成了两个文件目录:/train/test

因为代码中存在对目录的相关操作,务必保证按照文章中的操作进行;否则,代码可能无法正常运行

python操作文件目录

在这里先简单总结下python中操作文件和目录的3个包:os、shutil和pathlib

os包

python 复制代码
1-处理文件和目录
os.access(path, mode) - 检查路径的访问权限
os.chdir(path) - 改变当前工作目录
os.getcwd() - 获取当前工作目录
os.listdir(path) - 列出指定路径下的文件和目录
os.mkdir(path) - 创建文件夹
os.remove(path) - 删除文件
os.rmdir(path) - 删除空文件夹
os.rename(src, dst) - 重命名文件或目录
os.stat(path) - 获取文件或目录信息
os.walk(top) - 遍历top目录下所有子目录和文件

2-执行系统命令
os.system(command) - 执行系统命令,直接返回命令执行状态
os.popen(command).read() - 执行系统命令,返回命令输出结果
os.getcwd() - 获取当前进程工作目录
os.chdir(path) - 改变当前进程工作目录
os.getpid() - 获取当前进程id
os.kill(pid, sig) - 向进程发送signal信号

3-获取环境变量
os.environ - 获取系统环境变量
os.getenv(key) - 获取指定环境变量的值
os.putenv(key, value) - 设置环境变量

4-其他功能
os.path - 用于路径处理的模块
os.linesep - 系统行分隔符
os.name - 当前系统名称
os.urandom(n) - 生成n字节长度的随机字节序列

shutil包

python中的shutil库提供了高级的文件、文件夹、压缩包处理功能,常用的主要有以下几个方面:

python 复制代码
1. 复制文件/文件夹

- shutil.copy(src, dst) - 复制文件
- shutil.copy2(src, dst) - 复制文件和元数据
- shutil.copystat(src, dst) - 仅复制元数据
- shutil.copytree(src, dst) - 递归复制文件夹

2. 移动文件/文件夹

- shutil.move(src, dst) - 移动文件/文件夹

3. 删除文件/文件夹

- shutil.rmtree(path) - 递归删除文件夹
- shutil.unlink(path) - 删除文件

4. 打包压缩

- shutil.make_archive(base_name, format,...) - 创建压缩包
- shutil.unpack_archive(filename,...) - 解压压缩包

5. 其他

- shutil.disk_usage(path) - 返回路径占用的磁盘空间信息
- shutil.chown(path, user, group) - 修改文件权限
- shutil.which(cmd) - 返回命令路径

shutil库建立在os模块之上,提供了更易用的高级接口,可以通过import shutil来使用。

pathlib包

pathlib模块提供了面向对象的路径管理方法,可以更简单的处理文件系统路径。主要功能包括:

python 复制代码
1. 创建Path对象
# 从一个路径字符串创建Path对象
from pathlib import Path
p = Path('example.txt')

2. 访问路径

- p.exists() - 检查路径是否存在
- p.is_file() - 检查路径是否是一个文件
- p.is_dir() - 检查路径是否是一个目录
- p.name - 文件名
- p.stem - 不包含扩展名的文件名
- p.suffix - 文件扩展名
- p.parent - 父目录路径

3. 操作文件系统

- p.mkdir() - 创建目录
- p.rmdir() - 删除目录
- p.unlink() - 删除文件
- p.rename() - 重命名路径
- p.open() - 打开文件并返回一个文件对象

4. 路径运算

可以用/拼接子路径,也支持os.path的运算:

- p / 'subpath' - 拼接子路径
- p.resolve() - 返回绝对路径
- p.is_absolute() - 检查是否是绝对路径

5.  glob模式匹配

- p.glob('*.py') - 匹配该路径下的py文件
- p.rglob('*.py') - 递归匹配所有子目录中的py文件

下面介绍完整的过程:

构建数据

由于个人PC配置有限,在这里使用部分的数据进行建模。从原始数据集图像中复制部分图像到指定目录下(构成少量数据集)。

在这里创建3个数据集:

  • 训练集train
  • 验证集validation
  • 测试集test

In [1]:

arduino 复制代码
import os
import shutil
import pathlib 

每次重新运行的时候,都要保证cats_dogs_small文件夹是不存在的

In [2]:

ini 复制代码
# 原始解压目录,已经存在
original_dir = pathlib.Path("train")  

# 保存小数据集的目录(直接生成,未提前创建)
new_base_dir = pathlib.Path("cats_dogs_small")  

In [3]:

new_base_dir

Out[3]:

scss 复制代码
WindowsPath('cats_dogs_small')

In [4]:

python 复制代码
def make_subset(subset_name, start_index, end_index):
    """
    作用:从索引start_index到end_index,复制图像到子目录new_base_dir/{subset_name}/cat(dog)
    subset_name可以是train、validation或者test
    """
    
    for category in ["cat", "dog"]:  # cat或者dog同时遍历创建数据
        dir = new_base_dir / subset_name / category  #  拼接子目录完整路径
        os.makedirs(dir)  # 创建目录
        
        # 列表推导式生成图片名称
        fnames = [f"{category}.{i}.jpg" for i in range(start_index, end_index)]  
        
        # 对图片名称的循环复制
        for fname in fnames:
             # copyfile:从src路径复制到dst路径
            shutil.copyfile(src=original_dir / fname, 
                           dst=dir / fname)

In [5]:

分别创建训练集(索引号从0到999,不包含1000)、验证集(从1000到1499,不包含1500)和测试集(从1500到2499,不包含2500)

ini 复制代码
make_subset("train", start_index=0, end_index=1000)

In [6]:

ini 复制代码
make_subset("validation", start_index=1000, end_index=1500)

In [7]:

ini 复制代码
make_subset("test", start_index=1500, end_index=2500)

搭建卷积神经网络CNN

基于keras搭建卷积神经网络:卷积层和最大池化层的累加,再加上最后的展平层和密集连接输出层。

In [8]:

ini 复制代码
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(180,180,3))

# 像素尺寸缩放
x = layers.Rescaling(1. / 255)(inputs)

x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(x)  # 卷积层
x = layers.MaxPooling2D(pool_size=2)(x)  # 最大池化层
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)

x = layers.Flatten()(x)  # 展平层

outputs = layers.Dense(1, activation="sigmoid")(x)  # 密集连接层
model = keras.Model(inputs=inputs, outputs=outputs)

查看模型概要:

In [9]:

python 复制代码
model.summary()
Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 180, 180, 3)]     0         
                                                                 
 rescaling (Rescaling)       (None, 180, 180, 3)       0         
                                                                 
 conv2d (Conv2D)             (None, 178, 178, 32)      896       
                                                                 
 max_pooling2d (MaxPooling2D  (None, 89, 89, 32)       0         
 )                                                               
                                                                 
 conv2d_1 (Conv2D)           (None, 87, 87, 64)        18496     
                                                                 
 max_pooling2d_1 (MaxPooling  (None, 43, 43, 64)       0         
 2D)                                                             
                                                                 
 conv2d_2 (Conv2D)           (None, 41, 41, 128)       73856     
                                                                 
 max_pooling2d_2 (MaxPooling  (None, 20, 20, 128)      0         
 2D)                                                             
                                                                 
 conv2d_3 (Conv2D)           (None, 18, 18, 256)       295168    
                                                                 
 max_pooling2d_3 (MaxPooling  (None, 9, 9, 256)        0         
 2D)                                                             
                                                                 
 conv2d_4 (Conv2D)           (None, 7, 7, 256)         590080    
                                                                 
 flatten (Flatten)           (None, 12544)             0         
                                                                 
 dense (Dense)               (None, 1)                 12545     
                                                                 
=================================================================
Total params: 991,041
Trainable params: 991,041
Non-trainable params: 0
_________________________________________________________________

模型的编译,设置损失、优化器和评估指标:

In [10]:

ini 复制代码
# 模型编译compile

model.compile(loss="binary_crossentropy",  # 二分类使用binary_crossentropy
              optimizer="rmsprop", 
              metrics=["accuracy"])

数据预处理image_dataset_from_directory()

keras肯定是不能直接处理图像数据。因此数据在输入模型之前,应该将图像数据格式化为经过预处理的浮点数张量。将图片JPEG文件转成浮点数张量的步骤:

  • 读取JPEG文件,并解码为RGB像素网格
  • 将像素网格转为浮点数张量,并将张量的大小调节相同
  • 将数据打包成批量

读取图像

Keras包含函数image_dataset_from_directory(),通过建立数据管道,将图片文件迅速转成张量批量

In [11]:

ini 复制代码
from tensorflow.keras.utils import image_dataset_from_directory

train_dataset = image_dataset_from_directory(
    new_base_dir / "train",  # 目录
    image_size=(180, 180),  # 图像大小
    batch_size=32)  # 批量大小

validation_dataset = image_dataset_from_directory(
    new_base_dir / "validation",
    image_size=(180, 180),
    batch_size=32)

test_dataset = image_dataset_from_directory(
    new_base_dir / "test",
    image_size=(180, 180),
    batch_size=32)
Found 2000 files belonging to 2 classes.
Found 1000 files belonging to 2 classes.
Found 2000 files belonging to 2 classes.

上面列出了每个目录下的文件数量和类别数目。

理解TensorFlow DataSet对象

TensorFlow提供了tf.data 这个API,用于为机器学习模型创建管道,最重要的类是tf.data.Dataset

该对象类是一个迭代器 ,可以在for循环中使用,返回输入数据和标签组成的批量。可以将对象直接传入Keras的fit方法中。

In [12]:

python 复制代码
# Dataset类还拥有一个用于修改数据集的函数式API


import numpy as np
import tensorflow as tf

random_numbers = np.random.normal(size=(1000,16))
random_numbers

Out[12]:

css 复制代码
array([[-1.97578218, -0.90409304, -0.09623576, ...,  2.41728279,         0.91704091,  1.6140268 ],
       [-0.93297431,  0.22050653,  0.26043918, ..., -0.15646056,        -2.90198359, -2.43836605],
       [ 1.79131853, -0.87672883,  0.27288246, ...,  1.58597872,        -0.64024683,  0.52037157],
       ...,
       [-0.01100054, -0.69301064, -0.90628903, ...,  1.07197402,        -1.04802286, -0.25163522],
       [ 0.23100176, -0.59347372,  1.40955734, ...,  1.32233625,         0.0113596 , -1.25762504],
       [-1.32839193,  0.39171769, -0.2087147 , ...,  0.43798809,        -0.22259379,  0.98210044]])

from_tensor_slices该类方法可以利用numpy数组或者数组的元组或字典来创建一个Dataset对象:

In [13]:

ini 复制代码
dataset = tf.data.Dataset.from_tensor_slices(random_numbers)

dataset

Out[13]:

xml 复制代码
<_TensorSliceDataset element_spec=TensorSpec(shape=(16,), dtype=tf.float64, name=None)>

根据数据集生成单个样本:

In [14]:

python 复制代码
for i, element in enumerate(dataset):
    print(element.shape)
    
    if i >= 2:
        break
(16,)
(16,)
(16,)

使用.batch方法来批量生成数据:

In [15]:

python 复制代码
batched_dataset = dataset.batch(32)  # 调用batach方法批量生成数据

for i, element in enumerate(batched_dataset):
    print(element.shape)
    if i > 2:
        break
(32, 16)
(32, 16)
(32, 16)
(32, 16)

该对象还有其他方法:

  • .shuffle(buffer_size):打乱缓冲区元素。
  • .prefetch(buffer_size):将缓冲区元素预取到GPU内存中,以提高设备利用率。
  • .map(callable):对数据集的每个元素进行某项变换(函数callable的输入是数据集生成的单个元素)。

In [16]:

python 复制代码
# map方法的使用

map_dataset = dataset.map(lambda x:tf.reshape(x,(4,4)))

for i, element in enumerate(map_dataset):
    print(element.shape)
    if i > 2:
        break
(4, 4)
(4, 4)
(4, 4)
(4, 4)

Dataset对象的输出

In [17]:

bash 复制代码
# 一个Dataset对象的输出

for data_batch, labels_batch in train_dataset:
    print("data_batch.shape: ", data_batch.shape)
    print("labels_batch.shape", labels_batch.shape)
    break
data_batch.shape:  (32, 180, 180, 3)
labels_batch.shape (32,)

利用Dataset对象训练模型(未正则化)

In [18]:

ini 复制代码
callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath="convnet_from_scratch.keras",  # 保存位置
        save_best_only=True,  # 只有当val_loss指标的当前值低于训练过程之前的所有值时,回调函数才会保存一个新文件
        monitor="val_loss"
    )
]

history = model.fit(
    train_dataset, 
    epochs=30,
    validation_data=validation_dataset,
    callbacks=callbacks
)
python 复制代码
Epoch 1/30
63/63 [==============================] - 40s 617ms/step - loss: 0.6948 - accuracy: 0.5035 - val_loss: 0.6943 - val_accuracy: 0.5000
Epoch 2/30
63/63 [==============================] - 39s 615ms/step - loss: 0.6943 - accuracy: 0.5095 - val_loss: 0.6921 - val_accuracy: 0.5000
Epoch 3/30
63/63 [==============================] - 38s 600ms/step - loss: 0.6865 - accuracy: 0.5755 - val_loss: 0.6728 - val_accuracy: 0.5270
Epoch 4/30
63/63 [==============================] - 37s 594ms/step - loss: 0.6363 - accuracy: 0.6430 - val_loss: 0.6469 - val_accuracy: 0.6120
Epoch 5/30
63/63 [==============================] - 38s 609ms/step - loss: 0.6001 - accuracy: 0.6665 - val_loss: 0.9687 - val_accuracy: 0.5550
Epoch 6/30
63/63 [==============================] - 39s 621ms/step - loss: 0.5801 - accuracy: 0.7055 - val_loss: 0.6030 - val_accuracy: 0.6790
......
Epoch 29/30
63/63 [==============================] - 38s 605ms/step - loss: 0.0244 - accuracy: 0.9900 - val_loss: 2.0662 - val_accuracy: 0.7230
Epoch 30/30
63/63 [==============================] - 38s 609ms/step - loss: 0.0550 - accuracy: 0.9820 - val_loss: 2.2703 - val_accuracy: 0.7120

精度和损失可视化

对训练过程中精度和损失的可视化:

In [19]:

ini 复制代码
import matplotlib.pyplot as plt
%matplotlib inline

acc = history.history["accuracy"]
val_acc = history.history["val_accuracy"]
loss = history.history["loss"]
val_loss = history.history["val_loss"]

epochs = range(1, len(acc) + 1)

plt.figure()
plt.plot(epochs, acc, "bo", label="Training acc")
plt.plot(epochs, val_acc, "b", label="Validation acc")
plt.title("Training and validation accuracy")
plt.legend()

plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.legend()
plt.show()

测试集上评估模型

将前面使用回调函数时保存的最佳模型直接导进来,然后用在测试集上进行评估:

In [20]:

ini 复制代码
test_model = keras.models.load_model("convnet_from_scratch.keras") 
test_loss, test_acc = test_model.evaluate(test_dataset)

print(f"Test accuracy: {test_acc:.3f}")
63/63 [==============================] - 7s 114ms/step - loss: 0.5807 - accuracy: 0.7395
Test accuracy: 0.739

数据增强

数据增强的目标是,模型在训练时不会操作两个完全相同图片,有助于观察到数据的更多内容,从而具有更强的泛化能力。

在模型的一开始添加数据增强层

添加增强层

In [21]:

ini 复制代码
# 3个增强层
data_augment = keras.Sequential(  
    [layers.RandomFlip("horizontal"),  # 水平翻转应用于随机抽取的50%图像
     layers.RandomRotation(0.1),  # 将图像在[-10%,10%]的范围内随机旋转或者说[-36°, +36°]
     layers.RandomZoom(0.2)  # 放大或者缩小图像,在[-20%,+20%]范围内随机取值
    ]
)

显示增强后的图像

In [22]:

scss 复制代码
plt.figure(figsize=(10,10))

for images, _ in train_dataset.take(1):
    for i in range(9):
        augmented_images = data_augment(images)  # 将数据增强代码块用于图像批量
        ax = plt.subplot(3,3,i+1)
        plt.imshow(augmented_images[2].numpy().astype("uint8")) # 9次迭代:对同一个图像的增强
        plt.axis("off")

数据增强 +dropout正则化

基于数据增强和dropout正则化构建卷积神经网络:

In [23]:

ini 复制代码
inputs = keras.Input(shape=(180,180,3))

x = data_augment(inputs)  # 添加数据增强
x = layers.Rescaling(1./255)(x)
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)

x = layers.Dropout(0.5)(x)  # drop比例为0.5

outputs = layers.Dense(1, activation="sigmoid")(x)

In [24]:

ini 复制代码
model = keras.Model(inputs=inputs, outputs=outputs)
model.compile(loss="binary_crossentropy",
             optimizer="rmsprop",
             metrics=["accuracy"]
             )

基于正则化的卷积神经网络

In [25]:

ini 复制代码
callbacks = [
    keras.callbacks.ModelCheckpoint(
    filepath="convnet_from_scratch_with_augmentation.keras",
    save_best_only=True,
    monitor="val_loss")
]

history = model.fit(train_dataset,  # 此时的模型model已经是基于数据增强的
                   epochs=100,
                   validation_data=validation_dataset,
                   callbacks=callbacks)
python 复制代码
Epoch 1/100
63/63 [==============================] - 40s 626ms/step - loss: 0.7104 - accuracy: 0.4965 - val_loss: 0.6924 - val_accuracy: 0.5000
Epoch 2/100
63/63 [==============================] - 40s 633ms/step - loss: 0.6934 - accuracy: 0.5195 - val_loss: 0.6902 - val_accuracy: 0.5810
Epoch 3/100
63/63 [==============================] - 40s 631ms/step - loss: 0.6938 - accuracy: 0.5285 - val_loss: 0.6742 - val_accuracy: 0.5780
Epoch 4/100
63/63 [==============================] - 39s 626ms/step - loss: 0.6606 - accuracy: 0.6110 - val_loss: 0.6832 - val_accuracy: 0.5270
Epoch 5/100
63/63 [==============================] - 40s 629ms/step - loss: 0.6434 - accuracy: 0.6250 - val_loss: 0.6279 - val_accuracy: 0.6250
......省略
Epoch 98/100
63/63 [==============================] - 39s 624ms/step - loss: 0.1888 - accuracy: 0.9365 - val_loss: 1.0649 - val_accuracy: 0.8340
Epoch 99/100
63/63 [==============================] - 39s 617ms/step - loss: 0.1616 - accuracy: 0.9480 - val_loss: 0.5868 - val_accuracy: 0.8350
Epoch 100/100
63/63 [==============================] - 40s 642ms/step - loss: 0.1545 - accuracy: 0.9470 - val_loss: 0.8994 - val_accuracy: 0.8220

模型评估

In [26]:

ini 复制代码
test_model = keras.models.load_model("convnet_from_scratch_with_augmentation.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)

print(f"Test accuracy: {test_acc:.3f}")
63/63 [==============================] - 7s 112ms/step - loss: 0.5057 - accuracy: 0.7990
Test accuracy: 0.799

精度和损失可视化

In [27]:

ini 复制代码
import matplotlib.pyplot as plt
%matplotlib inline

acc = history.history["accuracy"]
val_acc = history.history["val_accuracy"]
loss = history.history["loss"]
val_loss = history.history["val_loss"]

epochs = range(1, len(acc) + 1)

plt.figure()
plt.plot(epochs, acc, "bo", label="Training acc")
plt.plot(epochs, val_acc, "b", label="Validation acc")
plt.title("Training and validation accuracy")
plt.legend()

plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.legend()
plt.show()
相关推荐
-Nemophilist-7 分钟前
机器学习与深度学习-1-线性回归从零开始实现
深度学习·机器学习·线性回归
艾派森1 小时前
大数据分析案例-基于随机森林算法的智能手机价格预测模型
人工智能·python·随机森林·机器学习·数据挖掘
2 小时前
开源竞争-数据驱动成长-11/05-大专生的思考
人工智能·笔记·学习·算法·机器学习
忘梓.2 小时前
划界与分类的艺术:支持向量机(SVM)的深度解析
机器学习·支持向量机·分类
Chef_Chen2 小时前
从0开始机器学习--Day17--神经网络反向传播作业
python·神经网络·机器学习
MarkHD3 小时前
第十一天 线性代数基础
线性代数·决策树·机器学习
打羽毛球吗️3 小时前
机器学习中的两种主要思路:数据驱动与模型驱动
人工智能·机器学习
小馒头学python4 小时前
机器学习是什么?AIGC又是什么?机器学习与AIGC未来科技的双引擎
人工智能·python·机器学习
正义的彬彬侠4 小时前
《XGBoost算法的原理推导》12-14决策树复杂度的正则化项 公式解析
人工智能·决策树·机器学习·集成学习·boosting·xgboost
羊小猪~~4 小时前
神经网络基础--什么是正向传播??什么是方向传播??
人工智能·pytorch·python·深度学习·神经网络·算法·机器学习