TensorFlow 检测图片特征并实现图片分类和识别

83 阅读14分钟

  图片识别过程中,在处理图片时,如果可以将图片过滤到只剩下一些关键的组成元素(特征),通过匹配这些关键的组成元素可以更高效的检测图片内容。

⒈ 卷积(convolution)

  卷积是卷积神经网络(CNN)中的核心操作,用于提取图片或其他数据中的特征。其运作机制是通过滑动一个卷积核(filter)在输入数据上进行操作,最终生成一个新的特征图(feature map)。

  • 卷积核(filter):一个大小通常为 3×3 或 5×5 或 7×7 的矩阵,通过在输入数据上进行滑动来提取特征。以图片处理为例,一个 3×3 的卷积核在图片上进行滑动,每滑动一个像素,卷积核中每个位置的数值与对应图片位置的像素值相乘,将最终得到的 9 个结果相加得到一个新的输出值。

  • 特征图(feature map):经过卷积后的输出是一个特征图,表示输入数据中某些特征的激活情况。多个卷积核可以产生多个特征图,形成比较丰富的特征表示。

图像卷积前后效果.png

  上图为原图与经过卷积之后的特征图的对比,卷积所使用的卷积核为

[111101111]\begin{bmatrix} -1&-1&-1 \\ -1&0&-1 \\ -1&-1&-1 \end{bmatrix}

⒉ 池化(pooling)

  池化(pooling)是一种下采样的操作,用于减少特征图的尺寸,同时保留重要的空间信息。在卷积神经网络(CNN)中,池化通常出现在卷积层之后。

池化的作用
  • 减少特征图的尺寸:通过缩小输入特征图的宽高,减小计算量,减少参数数量,同时保持重要的特征信息

  • 防止过拟合:通过压缩数据量,可以防止模型在训练过程中过拟合到特定的局部特征

  • 增强特征的平移不变性:增强模型对图像位移以及变形的鲁棒性,即使图像出现轻微的平移,模型仍然能提取到相同的特征
池化的类型

  池化操作通常使用一个固定大小窗口(常见的窗口大小为 2×2)在特征图上进行滑动,并从窗口内选择一个值来代表窗口的输出。常用的池化方法有最大池化(max pooling)、最小池化(min pooling)以及平均池化(average pooling)。

  以最大池化为例,最大池化以窗口中的最大值作为窗口的输出,这样保留了特征图中最显著的特征信息,适合提取图像中最具有代表性的特征。

最大池化运行机制示意图

图像池化前后的效果对比

⒊ 模型与参数

  使用 keras 中的 Fashion MNIST 数据集,构建一个简单的卷积神经网络模型,通过这个模型来理解卷积神经网络的运行机制。

  Fashion MNIST 数据集用于替换 keras 中原有的 MNIST 数据集,MNIST 数据集为 60000 张 28×28 的手写数字 0 到 9 的黑白图片。类似的,Fashion MNIST 包括 10 种不同类型的服饰图片,数量、图片维度与 MNIST 数据集中的相同。

import tensorflow as tf
from tensorflow.keras import layers, models


data = tf.keras.datasets.fashion_mnist
# 拆分训练数据与测试数据
(training_images, training_labels), (test_images, test_labels) = data.load_data()

# 变更图片维度以及像素值
training_images = training_images.reshape(60000, 28, 28, 1)
training_images = training_images / 255.0
test_images = test_images.reshape(10000, 28, 28, 1)
test_images = test_images / 255.0

# 构建模型
model = models.Sequential([
    layers.Conv2D(64, (3, 3), activation="relu", input_shape=(28, 28, 1)),
    layers.MaxPooling2D(2, 2),
    layers.Conv2D(64, (3, 3), activation="relu"),
    layers.MaxPooling2D(2, 2),
    layers.Flatten(),
    layers.Dense(128, activation=tf.nn.relu),
    layers.Dense(10, activation=tf.nn.softmax)
])

# 查看模型结构
model.summary()

   Conv2D 的作用是将卷积核应用于输入图片,提取图片的特征。Conv2D 要求输入的图片必须有表示颜色的 channel 信息,所以代码开头需要对训练集和测试集的图片进行维度变更。

Conv2D 的参数中有一个 data_format 来限定输入图片的维度的顺序:如果 data_format 的值为 channels_last,则输入图片的维度顺序为 (batch_size, height, width, channels);如果 data_format 的值为 channels_first,则输入图片的维度顺序为 (batch_size, channels, height, width)。

  代码开头将所有图片都除以 255.0,该操作称为归一化。归一化可以提升模型训练的性能:

  • 归一化将图片中每个像素的像素值限定在 0 到 1 之间,这可以防止模型在训练过程中向具有高像素值的特征倾斜
  • 归一化可以使模型在训练过程中更快的收敛
  • 归一化通过降低模型对输入参数变化的敏感程度,防止模型过拟合
  • 归一化以后的数据可以与激活函数更好的兼容
  • 归一化可以确保经过数据增强之后的图像与原图保持一致性

  激活函数通过引入非线性,使得模型可以学习更加复杂的模式。
  如果不使用激活函数,神经网络的每一层输出只是承接了上一层输入的线性变换,无论神经网络有多少层,输出都是输入的线性组合。激活函数给神经元引入了非线性因素,使得神经网络可以逼近任何非线性函数。

  数据增强通过对已有的图片进行多种变变换,可以人为的增加数据集的大小以及多样性,同时还可以提升模型的泛化性能。常用的进行图片变换的方式有翻转、旋转、裁剪、缩放、颜色抖动、高斯噪声以及弹性变换。

  Flatten 的作用是将一个多维的输入转换成一个一维的张量,这个操作在将神经网络连接到一个全连接层(fully connected layer)之前是必须的。Dense 层是一个全连接层,其作用是将前一层的所有神经元与当前层的所有神经元连接,每个连接都有一个权重,通过权重和激活函数的运算,将输入数据映射到输出结果。

Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type)                         ┃ Output Shape                ┃         Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ conv2d (Conv2D)                      │ (None, 26, 26, 64)          │             640 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ max_pooling2d (MaxPooling2D)         │ (None, 13, 13, 64)          │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ conv2d_1 (Conv2D)                    │ (None, 11, 11, 64)          │          36,928 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ max_pooling2d_1 (MaxPooling2D)       │ (None, 5, 5, 64)            │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ flatten (Flatten)                    │ (None, 1600)                │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense (Dense)                        │ (None, 128)                 │         204,928 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense_1 (Dense)                      │ (None, 10)                  │           1,290 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
 Total params: 243,786 (952.29 KB)
 Trainable params: 243,786 (952.29 KB)
 Non-trainable params: 0 (0.00 B)

  conv2d 层有 64 个 3×3 的卷积核,输入图片的尺寸为 28×28。3×3 的卷积核会导致在卷积过程中图片边缘的一个像素会丢失,所以 conv2d 层最终的输出形状为 26×26×64。一个 3×3 的卷积需要学习 9 个参数外加一个偏移量,总共需要 10 个参数,64 个卷积总共需要 640 个参数。

  max_pooling2d 层不需要学习任何东西,只需要将特征图的尺寸减半。所以,该层的参数个数为 0,而输出形状为 13×13×64。

  conv2d_1 层经过卷积之后输出形状变成了 11×11×64。该层的 64 个卷积叠加 conv2d 层的 64 个卷积,总共需要学习 64×64×9 个参数,另外该层的 64 个卷积分别需要一个偏移量,所以,该层总共需要 64×64×9+64=36928 个参数。

  与 max_pooling2d 层类似,max_pooling2d_1 层也只需要将特征图尺寸减半,所以输出形状为 5×5×64。

  flatten 层的作用是将多维输入转换成一维张量输出,所以其输入形状为 5×5×64=1600。

  dense 层有 128 个神经元,其输出形状为 128。由于每个神经元相应的都需要学习一个权重参数和一个偏移量的参数,所以最终该层需要 1600×128+128=204928 个参数。

  dense_1 层有 10 个神经元,其输出形状为 10。该层需要承接 dense 层的 128 个输出作为输入,所以该层需要学习的参数个数为 128×10+10=1290。

  综上,整个模型训练过程中需要学习的参数个数为 243786。

⒋ 迁移学习(transfer learning)

  迁移学习旨在将一个领域的已训练模型的知识应用到另一个相关的领域中,尤其是当目标任务的数据量有限时。通过使用在大规模数据集上预训练的模型,迁移学习能够加速当前模型的训练并提高模型的性能。

  在传统机器学习中,模型通常在一个特定的数据集上进行训练,并且只能适用于该数据集的特定任务。迁移学习是将一个任务中学到的知识(通常来自大规模数据集的特征)迁移到另一个相关任务中。这个方法在深度学习中尤其有效,因为深层神经网络可以学到高层次的、通用的特征。

迁移学习的主要方式:

  • 特征提取:利用预训练模型的前几层(通常是卷积层)作为通用的特征提取器,然后在这些特征上构建新的分类器或回归模型
  • 微调:在新的任务上对预训练模型的部分或全部层进行微调。相比特征提取,微调允许更新更多的模型参数,使模型更加适应新任务的特定要求

⒌ Dropout 正则化

  Dropout 正则化旨在防止过拟合同时增强模型的泛化能力。通过在训练过程中随机丢弃一些神经元,使模型不会过于依赖某些特定的神经元,从而提高对不同数据的适应性。

  在每个训练步骤中,Dropout 会随机将某些神经元的输出设置为 0,即忽略这个神经元与其他神经元的连接。这样,模型在每次训练时都会产生一个不同的子网络,训练的权重不会过于依赖某些特定的神经元组合。

  丢弃概率 p 是 Dropout 的一个关键参数,表示每个神经元被丢弃的概率。模型在训练过程中会按照设定的丢弃概率随机将一部分神经元的输出设置为 0,剩下的神经元正常传递信号;模型在测试过程中不再丢弃神经元,由于在训练过程中丢弃了一些神经元,因此在测试过程中需要对神经元的输出进行缩放,即将神经元的输出乘以丢弃概率。

  Dropout 的使用导致模型的每次迭代都在使用不同的子集在工作,模型的收敛速度会变慢,导致整体训练的时间增加。
  卷积层中的空间结构非常重要,丢弃卷积层的神经元可能导致特征破坏,因此 Dropout 更多的使用于全连接层。

⒍ 代码实现图片分类以及识别

  InceptionV3 是一个预训练模型,以其在图片分类任务中的高效性和准确性而著称。InceptionV3 有多个卷积层以及紧随其后的池化层,每个卷积层都有不同尺寸的卷积核,可以捕获到不同尺度的特征。此外,InceptionV3 还包括一些辅助分类器,这些辅助分类器是与主网络一起训练的较小的子网络,可以帮助模型知道训练过程以及提高准确性。

  权重文件(weights file)包含预训练模型已经学习到的众多参数信息。从零开始训练一个新模型通常都会随机初始化模型的权重信息,这会导致模型的收敛速度变慢并影响模型训练的稳定性;使用权重文件中的信息进行初始化,可以借助预训练模型已有的知识加速训练。

import tensorflow as tf
from tensorflow.keras import layers, Model, Sequential
from tensorflow.keras.optimizers import RMSprop
from tensorflow.keras.applications.inception_v3 import InceptionV3
from tensorflow.keras.preprocessing import image_dataset_from_directory, image
import urllib.request


weights_url = "https://storage.googleapis.com/mledu-datasets/inception_v3_weights_tf_dim_ordering_tf_kernels_notop.h5"
weights_file = "inception_v3.h5"
# 下载权重文件
urllib.request.urlretrieve(weights_url, weights_file)

pre_trained_model = InceptionV3(input_shape=(150, 150, 3), include_top=False, weights=None)
pre_trained_model.load_weights(weights_file)

pre_trained_model.summary()

  通过上述代码可以查看 InceptionV3 的模型结构。我们选用网络中的 mixed7 层的输出作为我们本次训练的新模型的输入。

# 冻结预训练模型中的各层
for layer in pre_trained_model.layers:
    layer.trainable = False

last_layer = pre_trained_model.get_layer("mixed7")
last_output = last_layer.output

# 将输出展开为一维
x = layers.Flatten()(last_output)
# 添加全连接层(1024 个神经元)
x = layers.Dense(1024, activation="relu")(x)
# 输出层
x = layers.Dense(1, activation="sigmoid")(x)

# 定义模型并编译
model = Model(pre_trained_model.input, x)

model.compile(optimizer=RMSprop(learning_rate=0.001), loss="binary_crossentropy", metrics=["accuracy"])

  在开始之前首先要将预训练模型的各 layer 冻结,这样保留了各层在之前训练过程中已经学习到的知识,同时也减少了在新模型训练过程中需要更新的参数数量,从而可以加快整个训练过程。此外冻结操作还具有一定的正则化的效果,避免模型对新数据集的过拟合。

  在准备训练使用的图片时,为了利用 image_dataset_from_directory 自动给图片加标签,图片文件夹必须满足指定的结构,其中一级文件夹为总的图片目录;二级文件夹必须分为训练集(training)和验证集(validation),如果需要还可以增加测试集(test);三级文件夹代表给图片加的标签。

数据集文件夹结构

# 数据处理
# 归一化处理
def normalize(image, label):
    image = image / 255.0
    return image, label

train_dir = "/path/to/animals/training"
train_ds = image_dataset_from_directory(train_dir, image_size=(150, 150), batch_size=32)
# 训练集数据增强
data_augmentation = Sequential([
    layers.RandomFlip("horizontal_and_vertical"),
    layers.RandomRotation(0.2),
    layers.RandomZoom(0.1, 0.1),
])
train_ds = train_ds.map(lambda x, y: (data_augmentation(x, training=True), y))

train_ds = train_ds.map(normalize)

validation_dir = "/path/to/animals/validation"
validation_ds = image_dataset_from_directory(validation_dir, image_size=(150, 150), batch_size=32)
validation_ds = validation_ds.map(normalize)

history=model.fit(train_ds, epochs=50, validation_data=validation_ds)

# 保存模型
model.save("classification_model.h5")

  使用 image_dataset_from_directory 生成的训练集需要进行数据增强,代码中使用的数据增强手段包括:水平/垂直反转图片、随机角度旋转图片、缩放图片。另外,训练集和验证集里的图片都要进行归一化处理以便提高模型的训练效率和性能,同时确保模型在不同数据集上的一致性和稳定性。

训练结果

  经过 50 轮的训练,模型的准确率最终达到 0.98。训练好的模型保存,供后续进行图片识别的时候使用。

def load_and_preprocess_image(img_path, img_height, img_width):
    img = image.load_img(img_path, target_size=(img_height, img_width))
    img_array = image.img_to_array(img)
    img_array = np.expand_dims(img_array, axis=0)
    img_array /= 255.0
    return img_array

# 加载并预处理新图像
img_path = "/path/to/cat.jpg"
img_height, img_width = 150, 150
img_array = load_and_preprocess_image(img_path, img_height, img_width)


loaded_model = tf.keras.models.load_model(“classification_model.h5”)

predictions = loaded_model.predict(img_array)
print(predictions)

  使用训练好的模型进行图片识别,查看预测结果。

  除上述方法外,还可以通过 image_dataset_from_directory 生成测试集,然后使用模型对测试集中的图片进行预测,测试集的文件夹结构与训练集和验证集的类似。

test_dir = "/path/to/animals/test"
test_ds = image_dataset_from_directory(test_dir, image_size=(150, 150), batch_size=32)
test_ds = test_ds.map(normalize)

loaded_model = tf.keras.models.load_model('classification_model.h5')

predictions = loaded_model.predict(test_ds)
print(predictions)

  使用 image_dataset_from_directory 自动给数据集打标签,标签通常按照文件夹名称的首字母进行排序。在本文所使用的 DEMO 中,cat 对应 0, dog 对应 1。

  上述测试的输出结果为 [[0.9999777],[0.11082392]][[0.9999777 ], [0.11082392]],结果中的概率值表示的是当前图片为 cat 的概率。第一张图片为 cat 的概率是 0.99,表明模型认定这张图片为 cat;第二张图片为 cat 的概率是 0.11,表明模型认定这张图片为 dog

 模型预测结果的输出格式与模型输出层所使用的激活函数有关。
 文中模型要解决的是区分 cat 和 dog 的二分类问题,所以输出层的激活函数用的是 sigmoid,导致模型预测的输出结果格式为 [[0.9999777],[0.11082392]][[0.9999777 ], [0.11082392]];
 如果模型输出层的激活函数改为适用于多分类的 softmax,那么模型预测的输出结果格式会变为 [[a1,b1],[a2,b2]][[a_1, b_1], [a_2, b_2]],其中 a1a_1 表示第一张图片被模型认定为 cat 的概率,b1b_1 表示第一张图片被模型认定为 dog 的概率;a2a_2 表示第一张图片被模型认定为 cat 的概率,b2b_2 表示第一张图片被模型认定为 dog 的概率