自从开始在网上写作以来,非常依赖Unsplash。这是一个创造高质量图像的地方。但是你知道Unsplash可以使用机器学习来帮助标记照片吗?
对于上传到Unsplash[…]的每个图像,我们通过一系列机器学习算法运行图像,以了解照片的内容,消除了参与者手动标记照片的需要。
https://unsplash.com/blog/introducing-unsplashs-new-uploaders/
给照片贴标签是一项重要的任务,使用机器可以快速完成。
因此,我们将建立一个模型,可以从图像中提取信息,并提供正确的标签。我们将使用卷积神经网络(CNN)对图像进行分类预测,以确定图像是否与“建筑物”、“森林”、“冰川”、“山脉”、“海洋”或“街道”有关。因此,这是一个图像分类问题。
库
除了我们通常在R中使用的循环库之外,我们还将使用keras。Keras是一种高级神经网络API,旨在实现快速实验。
library(keras) # 深度学习
library(tidyverse) # 数据处理
library(imager) # 图像处理
library(caret) # 模型评估
library(grid) # 在网格中显示图像
library(gridExtra) # 在网格中显示图像
RS <- 42 # 随机状态常数
请注意,我们创建了一个名为RS的变量,它只是一个数字,用于再现性。
数据集
数据由6种不同标签的图像组成:“建筑物”、“森林”、“冰川”、“山脉”、“海洋”和“街道”。
与前一篇文章不同,在前一篇文章中,图像像素数据已转换为一个.csv文件,这次我们使用数据生成器直接读取图像。
https://medium.com/data-folks-indonesia/hand-gesture-recognition-8c0e2927a8bb
为此,我们需要了解图像文件夹结构,如下所示。
seg_train
└── seg_train
├── buildings
├── forest
├── glacier
├── mountain
├── sea
└── street
seg_test
└── seg_test
├── buildings
├── forest
├── glacier
├── mountain
├── sea
└── street
在每个建筑物、森林、冰川、山、海和街道子文件夹中,会保存相应的图像。顾名思义,我们将使用seg_train进行模型训练,使用seg_test进行模型验证。
探索性数据分析
首先,我们需要找到每个类别的父文件夹地址。
folder_list <- list.files("seg_train/seg_train/")
folder_path <- paste0("seg_train/seg_train/", folder_list, "/")
folder_path
#> [1] "seg_train/seg_train/buildings/" "seg_train/seg_train/forest/" "seg_train/seg_train/glacier/" "seg_train/seg_train/mountain/"
#> [5] "seg_train/seg_train/sea/" "seg_train/seg_train/street/"
然后,列出每个父文件夹地址的所有seg_train图像地址。
file_name <-
map(folder_path, function(x) paste0(x, list.files(x))) %>%
unlist()
我们可以在下面看到,总共有14034个seg_train图像。
cat("Number of train images:", length(file_name))
#> Number of train images: 14034
让我们看两张训练的图片。
set.seed(RS)
sample_image <- sample(file_name, 18)
img <- map(sample_image, load.image)
grobs <- lapply(img, rasterGrob)
grid.arrange(grobs=grobs, ncol=6)
以第一张图片为例。
img <- load.image(file_name[1])
img
#> Image. Width: 150 pix Height: 150 pix Depth: 1 Colour channels: 3
如下图所示,该图像的尺寸为150×150×1×3。这意味着该特定图像具有150像素的宽度、150像素的高度、1像素的深度和3个颜色通道(对于红色、绿色和蓝色,也称为RGB)。
dim(img)
#> [1] 150 150 1 3
现在,我们将构建一个函数来获取图像的宽度和高度,并将该函数应用于所有图像。
get_dim <- function(x){
img <- load.image(x)
df_img <- data.frame(
width = width(img),
height = height(img),
filename = x
)
return(df_img)
}
file_dim <- map_df(file_name, get_dim)
head(file_dim)
#> width height filename
#> 1 150 150 seg_train/seg_train/buildings/0.jpg
#> 2 150 150 seg_train/seg_train/buildings/10006.jpg
#> 3 150 150 seg_train/seg_train/buildings/1001.jpg
#> 4 150 150 seg_train/seg_train/buildings/10014.jpg
#> 5 150 150 seg_train/seg_train/buildings/10018.jpg
#> 6 150 150 seg_train/seg_train/buildings/10029.jpg
我们得到了以下图像的宽度和高度分布。
hist(file_dim$width, breaks = 20)
hist(file_dim$height, breaks = 20)
summary(file_dim)
#> width height filename
#> Min. :150 Min. : 76.0 Length:14034
#> 1st Qu.:150 1st Qu.:150.0 Class :character
#> Median :150 Median :150.0 Mode :character
#> Mean :150 Mean :149.9
#> 3rd Qu.:150 3rd Qu.:150.0
#> Max. :150 Max. :150.0
正如我们所看到的,数据集具有不同的图像维度。所有宽度均为150像素。然而,最大和最小高度分别为150和76像素。在拟合到模型之前,所有这些图像必须具有相同的大小。这一点至关重要,因为:
拟合每个图像像素值的模型的输入层具有固定数量的神经元,
如果图像尺寸太高,训练模型可能会花费太长时间,并且
如果图像尺寸太低,则会丢失太多信息。
数据预处理
神经网络模型可能出现的一个问题是,它们倾向于存储seg_train数据集中的图像,因此当新的seg_test数据集出现时,它们无法识别它。
数据扩充是解决这一问题的众多技术之一。对于给定的图像,数据增强将稍微对其进行变换,以创建一些新图像。然后将这些新图像拟合到模型中。
通过这种方式,模型知道原始图像的许多版本,并且希望能够理解图像的含义,而不是记住它。我们将只使用一些简单的转换,例如:
随机水平翻转图像
随机旋转10度
按系数0.1随机缩放
随机水平移动总宽度的0.1
随机水平移动总高度的0.1
我们不使用垂直翻转,因为在我们的例子中,它们可以改变图像的含义。
可以使用image_data_generator函数完成此数据扩充。将生成器保存到名为train_data_gen的对象。请注意,train_data_gen仅在训练时应用,我们在预测时不使用它。
在train_data_gen中,我们还执行标准化以减少照明差异的影响。此外,CNN模型在[0..1]数据上的收敛速度快于[0..255]。为此,只需将每个像素值除以255即可。
train_data_gen <- image_data_generator(
rescale = 1/255, # 缩放像素值
horizontal_flip = T, # 水平翻转图像
vertical_flip = F, # 垂直翻转图像
rotation_range = 10, # 将图像从0旋转到45度
zoom_range = 0.1, # 放大或缩小范围
width_shift_range = 0.1, # 水平移位至宽度
height_shift_range = 0.1, # 水平移位到高度
)
我们将使用150×150像素作为输入图像的形状,因为150像素是所有图像中最常见的宽度和高度(再次查看EDA),并将大小设置为目标大小。
此外,我们将分批训练模型,每批32个观察值。
target_size <- c(150, 150)
batch_size <- 32
现在,从各自的目录中构建生成器来生成训练和验证数据集。因为我们有彩色RGB图像,所以将颜色模式设置为“RGB”。最后,使用train_data_gen作为生成器并应用先前创建的数据扩充。
# 用于训练数据集
train_image_array_gen <- flow_images_from_directory(
directory = "seg_train/seg_train/", # 数据文件夹
target_size = target_size, # 图像维度的目标
color_mode = "rgb", # 使用rgb颜色
batch_size = batch_size , # 每个批次中的图像数
seed = RS, # 设置随机种子
generator = train_data_gen # 数据增强
)
# 用于验证数据集
val_image_array_gen <- flow_images_from_directory(
directory = "seg_test/seg_test/",
target_size = target_size,
color_mode = "rgb",
batch_size = batch_size ,
seed = RS,
generator = train_data_gen
)
接下来,我们将看到目标变量中标签的比例,以检查类的不平衡性。
如果存在的话,分类器倾向于建立有偏见的学习模型,与多数类相比,少数类的预测准确率较差。我们可以通过对训练数据集进行上采样或下采样,以最简单的方式解决此问题。
output_n <- n_distinct(train_image_array_gen$classes)
table("Frequency" = factor(train_image_array_gen$classes)) %>%
prop.table()
#> Frequency
#> 0 1 2 3 4 5
#> 0.1561208 0.1618213 0.1712983 0.1789939 0.1620351 0.1697307
幸运的是,如上所述,所有的类都是相对平衡的!
建模
首先,让我们保存我们使用的训练和验证图像的数量。除了训练数据之外,我们还需要不同的数据进行验证,因为我们不希望我们的模型只擅长于预测它看到的图像,还可以推广到看不见的图像。这种对看不见图像的需求正是我们还必须在验证数据集上查看模型性能的原因。
因此,我们可以在下面看到,我们有14034张图像用于训练(如前所述),3000张图像用于验证模型。
train_samples <- train_image_array_gen$n
valid_samples <- val_image_array_gen$n
train_samples
#> [1] 14034
valid_samples
#> [1] 3000
我们将从最简单的模型逐步构建三个模型。
简单CNN
此模型只有4个隐藏层,包括最大池和平坦层,以及1个输出层,详情如下:
卷积层:滤波器16,核大小3×3,same填充,relu激活函数
最大池层:池大小2×2
平坦层
密集层:16节点,relu激活函数
密集层(输出):6个节点,softmax激活函数
请注意,我们使用平坦层作为从网络的卷积部分到密集部分的桥梁。基本上,平坦层——顾名思义——将最后一个卷积层的维度展平为单个密集层。例如,假设我们有一个大小为(8,8,32)的卷积层。这里,32是滤波器的数量。平坦层将把这个张量重塑成2048大小的向量。
在输出层,我们使用softmax激活函数,因为这是一个多类分类问题。最后,我们需要指定CNN输入层所需的图像大小。如前所述,我们将使用一个150×150像素的图像大小和3个RGB通道,存储在target_size中。
现在,我们准备好了。
# 设置初始随机权重
tensorflow::tf$random$set_seed(RS)
model <- keras_model_sequential(name = "simple_model") %>%
# 卷积层
layer_conv_2d(filters = 16,
kernel_size = c(3,3),
padding = "same",
activation = "relu",
input_shape = c(target_size, 3)
) %>%
# 最大池层
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# 平坦层
layer_flatten() %>%
# 全连接层
layer_dense(units = 16,
activation