建立卷积神经网络模型
磐创AI自从开始在网上写作以来,非常依赖Unsplash。这是一个创造高质量图像的地方。但是你知道Unsplash可以使用机器学习来帮助标记照片吗?
对于上传到Unsplash[…]的每个图像,我们通过一系列机器学习算法运行图像,以了解照片的内容,消除了参与者手动标记照片的需要。
给照片贴标签是一项重要的任务,使用机器可以快速完成。
因此,我们将建立一个模型,可以从图像中提取信息,并提供正确的标签。我们将使用卷积神经网络(CNN)对图像进行分类预测,以确定图像是否与“建筑物”、“森林”、“冰川”、“山脉”、“海洋”或“街道”有关。因此,这是一个图像分类问题。
库
除了我们通常在R中使用的循环库之外,我们还将使用keras。Keras是一种高级神经网络API,旨在实现快速实验。
library(keras) # 深度学习
library(tidyverse) # 数据处理
library(imager) # 图像处理
library(caret) # 模型评估
library(grid) # 在网格中显示图像
library(gridExtra) # 在网格中显示图像
RS <- 42 # 随机状态常数
请注意,我们创建了一个名为RS的变量,它只是一个数字,用于再现性。
数据集
数据由6种不同标签的图像组成:“建筑物”、“森林”、“冰川”、“山脉”、“海洋”和“街道”。
与前一篇文章不同,在前一篇文章中,图像像素数据已转换为一个.csv文件,这次我们使用数据生成器直接读取图像。
为此,我们需要了解图像文件夹结构,如下所示。
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像素。在拟合到模型之前,所有这些图像必须具有相同的大小。这一点至关重要,因为:
1. 拟合每个图像像素值的模型的输入层具有固定数量的神经元,
2. 如果图像尺寸太高,训练模型可能会花费太长时间,并且
3. 如果图像尺寸太低,则会丢失太多信息。
数据预处理
神经网络模型可能出现的一个问题是,它们倾向于存储seg_train数据集中的图像,因此当新的seg_test数据集出现时,它们无法识别它。
数据扩充是解决这一问题的众多技术之一。对于给定的图像,数据增强将稍微对其进行变换,以创建一些新图像。然后将这些新图像拟合到模型中。
通过这种方式,模型知道原始图像的许多版本,并且希望能够理解图像的含义,而不是记住它。我们将只使用一些简单的转换,例如:
1. 随机水平翻转图像
2. 随机旋转10度
3. 按系数0.1随机缩放
4. 随机水平移动总宽度的0.1
5. 随机水平移动总高度的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个输出层,详情如下:
1. 卷积层:滤波器16,核大小3×3,same填充,relu激活函数
2. 最大池层:池大小2×2
3. 平坦层
4. 密集层:16节点,relu激活函数
5. 密集层(输出):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 = "relu") %>%
# Output Layer
layer_dense(units = output_n,
activation = "softmax",
name = "Output")
summary(model)
#> Model: "simple_model"
#> _________________________________________________________________
#> Layer (type) Output Shape Param #
#> =================================================================
#> conv2d (Conv2D) (None, 150, 150, 16) 448
#> _________________________________________________________________
#> max_pooling2d (MaxPooling2D) (None, 75, 75, 16) 0
#> _________________________________________________________________
#> flatten (Flatten) (None, 90000) 0
#> _________________________________________________________________
#> dense (Dense) (None, 16) 1440016
#> _________________________________________________________________
#> Output (Dense) (None, 6) 102
#> =================================================================
#> Total params: 1,440,566
#> Trainable params: 1,440,566
#> Non-trainable params: 0
#> _________________________________________________________________
构建完成后,我们对模型进行编译和训练。
我们使用分类交叉熵作为损失函数,因为这也是一个多类分类问题。我们使用默认学习率为0.001的adam优化器,因为adam是最有效的优化器之一。
为了简单起见,我们还使用准确率作为衡量标准。更重要的是,由于我们不喜欢一个类别高于其他类别,而且每个类别都是平衡的,因此与精确性、敏感性或特异性相比,准确率更受青睐。我们将对模型进行10个epoch的训练。
model %>%
compile(
loss = "categorical_crossentropy",
optimizer = optimizer_adam(lr = 0.001),
metrics = "accuracy"
)
# 拟合数据
history <- model %>%
fit_generator(
# 训练数据
train_image_array_gen,
# 训练epoch数
steps_per_epoch = as.integer(train_samples / batch_size),
epochs = 10,
# 验证数据
validation_data = val_image_array_gen,
validation_steps = as.integer(valid_samples / batch_size)
)
plot(history)
从第十个epoch的最终训练和验证准确率可以看出,它们具有相似的值,并且相对较高,这意味着没有出现过拟合。
接下来,我们将对验证数据集上的所有图像进行预测(而不是像在训练中那样按批次进行预测)。首先,让我们将每个图像及其对应类的路径制成表格。
val_data <- data.frame(file_name = paste0("seg_test/seg
test/", val_image_array_gen$filenames)) %>%
mutate(class = str_extract(file_name, "buildings|forest|glacier|mountain|sea|street"))
head(val_data)
#> file_name class
#> 1 seg_test/seg_test/buildings\20057.jpg buildings
#> 2 seg_test/seg_test/buildings\20060.jpg buildings
#> 3 seg_test/seg_test/buildings\20061.jpg buildings
#> 4 seg_test/seg_test/buildings\20064.jpg buildings
#> 5 seg_test/seg_test/buildings\20073.jpg buildings
#> 6 seg_test/seg_test/buildings\20074.jpg buildings
然后,我们将每个图像转换为一个数组。不要忘记对像素值进行标准化,也就是说,将它们除以255。
image_prep <- function(x, target_size) {
arrays <- lapply(x, function(path) {
img <- image_load(
path,
target_size = target_size,
grayscale = F
)
x <- image_to_array(img)
x <- array_reshape(x, c(1, dim(x)))
x <- x/255
})
do.call(abind::abind, c(arrays, list(along = 1)))
}
test_x <- image_prep(val_data$file_name, target_size)
dim(test_x)
#> [1] 3000 150 150 3
接下来,预测:
pred_test <- predict_classes(model, test_x)
head(pred_test)
#> [1] 4 0 0 0 4 3
现在,将每个预测解码为相应的类。
decode <- function(x){
case_when(
x == 0 ~ "buildings",
x == 1 ~ "forest",
x == 2 ~ "glacier",
x == 3 ~ "mountain",
x == 4 ~ "sea",
x == 5 ~ "street",
)
}
pred_test <- sapply(pred_test, decode)
head(pred_test)
#> [1] "sea" "buildings" "buildings" "buildings" "sea" "mountain"
最后,分析混淆矩阵。
cm_simple <- confusionMatrix(as.factor(pred_test), as.factor(val_data$class))
acc_simple <- cm_simple$overall['Accuracy']
cm_simple
#> Confusion Matrix and Statistics
#>
#> Reference
#> Prediction buildings forest glacier mountain sea street
#> buildings 348 24 14 20 35 106
#> forest 8 418 3 4 4 19
#> glacier 7 5 357 53 38 5
#> mountain 19 6 98 381 61 5
#> sea 13 1 75 65 363 6
#> street 42 20 6 2 9 360
#>
#> Overall Statistics
#>
#> Accuracy : 0.7423
#> 95% CI : (0.7263, 0.7579)
#> No Information Rate : 0.1843
#> P-Value [Acc > NIR] : < 0.00000000000000022
#>
#> Kappa : 0.6909
#>
#> Mcnemar's Test P-Value : 0.0000000001327
#>
#> Statistics by Class:
#>
#> Class: buildings Class: forest Class: glacier Class: mountain Class: sea Class: street
#> Sensitivity 0.7963 0.8819 0.6456 0.7257 0.7118 0.7186
#> Specificity 0.9224 0.9850 0.9559 0.9236 0.9357 0.9684
#> Pos Pred Value 0.6362 0.9167 0.7677 0.6684 0.6941 0.8200
#> Neg Pred Value 0.9637 0.9780 0.9227 0.9407 0.9407 0.9449
#> Prevalence 0.1457 0.1580 0.1843 0.1750 0.1700 0.1670
#> Detection Rate 0.1160 0.1393 0.1190 0.1270 0.1210 0.1200
#> Detection Prevalence 0.1823 0.1520 0.1550 0.1900 0.1743 0.1463
#> Balanced Accuracy 0.8593 0.9334 0.8007 0.8247 0.8238 0.8435
从混淆矩阵可以看出,模型很难区分每个类别。验证数据集的准确率为74%。有106个街道图像预测为建筑物,占所有街道图像的20%以上。这是有道理的,因为在许多街道图像中,建筑物也存在。
我们可以通过各种方式提高模型性能。但是现在,让我们通过简单地改变架构来改进它。
更深的CNN
现在我们制作一个更深的CNN,有更多的卷积层。以下是体系结构:
1. 块1:2个卷积层和1个最大池层
2. 块2:1个卷积层和1个最大池层
3. 块3:1个卷积层和1个最大池层
4. 块4:1个卷积层和1个最大池层
5. 平坦层
6. 一个致密层
7. 输出层
tensorflow::tf$random$set_seed(RS)
model_big <- keras_model_sequential(name = "model_big") %>%
# 第一个卷积层
layer_conv_2d(filters = 32,
kernel_size = c(5,5), # 5 x 5 filters
padding = "same",
activation = "relu",
input_shape = c(target_size, 3)
) %>%
# 第二个卷积层
layer_conv_2d(filters = 32,
kernel_size = c(3,3), # 3 x 3 filters
padding = "same",
activation = "relu"
) %>%
# 最大池层
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# 第三个卷积层
layer_conv_2d(filters = 64,
kernel_size = c(3,3),
padding = "same",
activation = "relu"
) %>%
# 最大池层
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# 第四个卷积层
layer_conv_2d(filters = 128,
kernel_size = c(3,3),
padding = "same",
activation = "relu"
) %>%
# 最大池层
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# 第五个卷积层
layer_conv_2d(filters = 256,
kernel_size = c(3,3),
padding = "same",
activation = "relu"
) %>%
# 最大池层
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# 平坦层
layer_flatten() %>%
# 密集层
layer_dense(units = 64,
activation = "relu") %>%
# 输出层
layer_dense(name = "Output",
units = output_n,
activation = "softmax")
summary(model_big)
#> Model: "model_big"
#> _________________________________________________________________
#> Layer (type) Output Shape Param #
#> =================================================================
#> conv2d_5 (Conv2D) (None, 150, 150, 32) 2432
#> _________________________________________________________________
#> conv2d_4 (Conv2D) (None, 150, 150, 32) 9248
#> _________________________________________________________________
#> max_pooling2d_4 (MaxPooling2D) (None, 75, 75, 32) 0
#> _________________________________________________________________
#> conv2d_3 (Conv2D) (None, 75, 75, 64) 18496
#> _________________________________________________________________
#> max_pooling2d_3 (MaxPooling2D) (None, 37, 37, 64) 0
#> _________________________________________________________________
#> conv2d_2 (Conv2D) (None, 37, 37, 128) 73856
#> _________________________________________________________________
#> max_pooling2d_2 (MaxPooling2D) (None, 18, 18, 128) 0
#> _________________________________________________________________
#> conv2d_1 (Conv2D) (None, 18, 18, 256) 295168
#> _________________________________________________________________
#> max_pooling2d_1 (MaxPooling2D) (None, 9, 9, 256) 0
#> _________________________________________________________________
#> flatten_1 (Flatten) (None, 20736) 0
#> _________________________________________________________________
#> dense_1 (Dense) (None, 64) 1327168
#> _________________________________________________________________
#> Output (Dense) (None, 6) 390
#> =================================================================
#> Total params: 1,726,758
#> Trainable params: 1,726,758
#> Non-trainable params: 0
#> _________________________________________________________________
其余部分与前面所做的相同。
model_big %>%
compile(
loss = "categorical_crossentropy",
optimizer = optimizer_adam(lr = 0.001),
metrics = "accuracy"
)
history <- model_big %>%
fit_generator(
train_image_array_gen,
steps_per_epoch = as.integer(train_samples / batch_size),
epochs = 10,
validation_data = val_image_array_gen,
validation_steps = as.integer(valid_samples / batch_size)
)
plot(history)
pred_test <- predict_classes(model_big, test_x)
pred_test <- sapply(pred_test, decode)
cm_big <- confusionMatrix(as.factor(pred_test), as.factor(val_data$class))
acc_big <- cm_big$overall['Accuracy']
cm_big
#> Confusion Matrix and Statistics
#>
#> Reference
#> Prediction buildings forest glacier mountain sea street
#> buildings 390 3 24 24 11 34
#> forest 3 465 11 7 8 11
#> glacier 2 0 367 35 9 1
#> mountain 0 2 82 415 17 1
#> sea 3 1 57 42 461 6
#> street 39 3 12 2 4 448
#>
#> Overall Statistics
#>
#> Accuracy : 0.8487
#> 95% CI : (0.8353, 0.8613)
#> No Information Rate : 0.1843
#> P-Value [Acc > NIR] : < 0.00000000000000022
#>
#> Kappa : 0.8185
#>
#> Mcnemar's Test P-Value : < 0.00000000000000022
#>
#> Statistics by Class:
#>
#> Class: buildings Class: forest Class: glacier Class: mountain Class: sea Class: street
#> Sensitivity 0.8924 0.9810 0.6637 0.7905 0.9039 0.8942
#> Specificity 0.9625 0.9842 0.9808 0.9588 0.9562 0.9760
#> Pos Pred Value 0.8025 0.9208 0.8865 0.8027 0.8088 0.8819
#> Neg Pred Value 0.9813 0.9964 0.9281 0.9557 0.9798 0.9787
#> Prevalence 0.1457 0.1580 0.1843 0.1750 0.1700 0.1670
#> Detection Rate 0.1300 0.1550 0.1223 0.1383 0.1537 0.1493
#> Detection Prevalence 0.1620 0.1683 0.1380 0.1723 0.1900 0.1693
#> Balanced Accuracy 0.9275 0.9826 0.8222 0.8746 0.9301 0.9351
这一结果总体上优于早期模型,因为模型更复杂,因此能够捕获更多的特征。我们在验证数据集上获得了85%的准确率。虽然对街道图像的预测已经有所改善,但对冰川图像的预测仍在进行中。
带预训练权重的CNN
实际上,研究人员已经为图像分类问题开发了许多模型,从VGG模型系列到谷歌开发的最新最先进的EfficientNet。
为了便于学习,在本节中,我们将使用VGG16模型,因为它是所有模型中最简单的模型之一,它只包括我们前面介绍的卷积层、最大池层和密集层。这个过程被称为迁移学习,它将预训练好的模型的知识转移到解决我们的问题上。
最初的VGG16模型接受了1000个类的训练。为了使其适合我们的问题,我们将排除模型的顶层(密集层),并插入我们版本的预测层,其中包括一个全局平均池层(作为平坦层的替代)、一个具有64个节点的密集层和一个具有6个节点的输出层(用于6个类)。
让我们看看总体架构。
# 加载没有顶层的原始模型
input_tensor <- layer_input(shape = c(target_size, 3))
base_model <- application_vgg16(input_tensor = input_tensor,
weights = 'imagenet',
include_top = FALSE)
# 添加我们的自定义层
predictions <- base_model$output %>%
layer_global_average_pooling_2d() %>%
layer_dense(units = 64, activation = 'relu') %>%
layer_dense(units = output_n, activation = 'softmax')
# 这是我们将要训练的模型
vgg16 <- keras_model(inputs = base_model$input, outputs = predictions)
summary(vgg16)
#> Model: "model"
#> _________________________________________________________________
#> Layer (type) Output Shape Param #
#> =================================================================
#> input_1 (InputLayer) [(None, 150, 150, 3)] 0
#> _________________________________________________________________
#> block1_conv1 (Conv2D) (None, 150, 150, 64) 1792
#> _________________________________________________________________
#> block1_conv2 (Conv2D) (None, 150, 150, 64) 36928
#> _________________________________________________________________
#> block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
#> _________________________________________________________________
#> block2_conv1 (Conv2D) (None, 75, 75, 128) 73856
#> _________________________________________________________________
#> block2_conv2 (Conv2D) (None, 75, 75, 128) 147584
#> _________________________________________________________________
#> block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
#> _________________________________________________________________
#> block3_conv1 (Conv2D) (None, 37, 37, 256) 295168
#> _________________________________________________________________
#> block3_conv2 (Conv2D) (None, 37, 37, 256) 590080
#> _________________________________________________________________
#> block3_conv3 (Conv2D) (None, 37, 37, 256) 590080
#> _________________________________________________________________
#> block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
#> _________________________________________________________________
#> block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160
#> _________________________________________________________________
#> block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808
#> _________________________________________________________________
#> block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808
#> _________________________________________________________________
#> block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
#> _________________________________________________________________
#> block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808
#> _________________________________________________________________
#> block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808
#> _________________________________________________________________
#> block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808
#> _________________________________________________________________
#> block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
#> _________________________________________________________________
#> global_average_pooling2d (GlobalAveragePooling2D) (None, 512) 0
#> _________________________________________________________________
#> dense_3 (Dense) (None, 64) 32832
#> _________________________________________________________________
#> dense_2 (Dense) (None, 6) 390
#> =================================================================
#> Total params: 14,747,910
#> Trainable params: 14,747,910
#> Non-trainable params: 0
#> _________________________________________________________________
我们可以直接使用vgg16进行训练和预测,但同样,为了学习,让我们自己从头开始创建vgg16模型。
model_bigger <- keras_model_sequential(name = "model_bigger") %>%
# 块一
layer_conv_2d(filters = 64,
kernel_size = c(3, 3),
activation='relu',
padding='same',
input_shape = c(94, 94, 3),
name='block1_conv1') %>%
layer_conv_2d(filters = 64,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block1_conv2') %>%
layer_max_pooling_2d(pool_size = c(2, 2),
strides=c(2, 2),
name='block1_pool') %>%
# 块二
layer_conv_2d(filters = 128,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block2_conv1') %>%
layer_conv_2d(filters = 128,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block2_conv2') %>%
layer_max_pooling_2d(pool_size = c(2, 2),
strides=c(2, 2),
name='block2_pool') %>%
# 块三
layer_conv_2d(filters = 256,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block3_conv1') %>%
layer_conv_2d(filters = 256,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block3_conv2') %>%
layer_conv_2d(filters = 256,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block3_conv3') %>%
layer_max_pooling_2d(pool_size = c(2, 2),
strides=c(2, 2),
name='block3_pool') %>%
# 块四
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block4_conv1') %>%
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block4_conv2') %>%
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block4_conv3') %>%
layer_max_pooling_2d(pool_size = c(2, 2),
strides=c(2, 2),
name='block4_pool') %>%
# 块五
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block5_conv1') %>%
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block5_conv2') %>%
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block5_conv3') %>%
layer_max_pooling_2d(pool_size = c(2, 2),
strides=c(2, 2),
name='block5_pool') %>%
# 全连接层
layer_global_average_pooling_2d() %>%
layer_dense(units = 64, activation = 'relu') %>%
layer_dense(units = output_n, activation = 'softmax')
model_bigger
#> Model
#> Model: "model_bigger"
#> _________________________________________________________________
#> Layer (type) Output Shape Param #
#> =================================================================
#> block1_conv1 (Conv2D) (None, 94, 94, 64) 1792
#> _________________________________________________________________
#> block1_conv2 (Conv2D) (None, 94, 94, 64) 36928
#> _________________________________________________________________
#> block1_pool (MaxPooling2D) (None, 47, 47, 64) 0
#> _________________________________________________________________
#> block2_conv1 (Conv2D) (None, 47, 47, 128) 73856
#> _________________________________________________________________
#> block2_conv2 (Conv2D) (None, 47, 47, 128) 147584
#> _________________________________________________________________
#> block2_pool (MaxPooling2D) (None, 23, 23, 128) 0
#> _________________________________________________________________
#> block3_conv1 (Conv2D) (None, 23, 23, 256) 295168
#> _________________________________________________________________
#> block3_conv2 (Conv2D) (None, 23, 23, 256) 590080
#> _________________________________________________________________
#> block3_conv3 (Conv2D) (None, 23, 23, 256) 590080
#> _________________________________________________________________
#> block3_pool (MaxPooling2D) (None, 11, 11, 256) 0
#> _________________________________________________________________
#> block4_conv1 (Conv2D) (None, 11, 11, 512) 1180160
#> _________________________________________________________________
#> block4_conv2 (Conv2D) (None, 11, 11, 512) 2359808
#> _________________________________________________________________
#> block4_conv3 (Conv2D) (None, 11, 11, 512) 2359808
#> _________________________________________________________________
#> block4_pool (MaxPooling2D) (None, 5, 5, 512) 0
#> _________________________________________________________________
#> block5_conv1 (Conv2D) (None, 5, 5, 512) 2359808
#> _________________________________________________________________
#> block5_conv2 (Conv2D) (None, 5, 5, 512) 2359808
#> _________________________________________________________________
#> block5_conv3 (Conv2D) (None, 5, 5, 512) 2359808
#> _________________________________________________________________
#> block5_pool (MaxPooling2D) (None, 2, 2, 512) 0
#> _________________________________________________________________
#> global_average_pooling2d_1 (GlobalAveragePooling2D) (None, 512) 0
#> _________________________________________________________________
#> dense_5 (Dense) (None, 64) 32832
#> _________________________________________________________________
#> dense_4 (Dense) (None, 6) 390
#> =================================================================
#> Total params: 14,747,910
#> Trainable params: 14,747,910
#> Non-trainable params: 0
#> _________________________________________________________________
请注意,model_bigger的每个层的参数数量与vgg16完全相同。
迁移学习的优点是,我们不必从随机权重开始训练模型,而是从原始模型的预训练权重开始。这些预训练好的权重已经针对图像分类问题进行了优化,我们只需对它们进行微调以符合我们的目的。
因此,隐喻是:
我们站在巨人的肩膀上。
也就是说,让我们将vgg16的所有权重分配给模型。
set_weights(model_bigger, get_weights(vgg16))
下面是我们的模型层的摘要:
layers <- model_bigger$layers
for (i in 1:length(layers))
cat(i, layers[[i]]$name, "")
#> 1 block1_conv1
#> 2 block1_conv2
#> 3 block1_pool
#> 4 block2_conv1
#> 5 block2_conv2
#> 6 block2_pool
#> 7 block3_conv1
#> 8 block3_conv2
#> 9 block3_conv3
#> 10 block3_pool
#> 11 block4_conv1
#> 12 block4_conv2
#> 13 block4_conv3
#> 14 block4_pool
#> 15 block5_conv1
#> 16 block5_conv2
#> 17 block5_conv3
#> 18 block5_pool
#> 19 global_average_pooling2d_1
#> 20 dense_5
#> 21 dense_4
请注意,层19–21仍然具有随机权重,因为它们是由我们创建的,并且不是来自原始模型。我们只需要冻结所有层以便单独训练这些层。
freeze_weights(model_bigger, from = 1, to = 18)
为了训练这些预测层,我们只需使用前面的设置。
# 编译模型
model_bigger %>% compile(loss = "categorical_crossentropy",
optimizer = optimizer_adam(lr = 0.001),
metrics = "accuracy")
history <- model_bigger %>%
fit_generator(
train_image_array_gen,
steps_per_epoch = as.integer(train_samples / batch_size),
epochs = 10,
validation_data = val_image_array_gen,
validation_steps = as.integer(valid_samples / batch_size)
)
现在,对模型进行微调。要做到这一点,我们应该对优化器应用较低的学习率,以便建立的预训练权重不会混乱。我们将使用0.00001的学习率。
此外,为了节省时间,我们只对模型进行4个epoch的训练。
在微调之前,不要忘记解冻要训练的层。在本例中,我们将解冻所有层
unfreeze_weights(model_bigger)
# 以低学习率重新编译
model_bigger %>% compile(loss = "categorical_crossentropy",
optimizer = optimizer_adam(lr = 0.00001),
metrics = "accuracy")
history <- model_bigger %>%
fit_generator(
train_image_array_gen,
steps_per_epoch = as.integer(train_samples / batch_size),
epochs = 4,
validation_data = val_image_array_gen,
validation_steps = as.integer(valid_samples / batch_size)
)
plot(history)
pred_test <- predict_classes(model_bigger, test_x)
pred_test <- sapply(pred_test, decode)
cm_bigger <- confusionMatrix(as.factor(pred_test), as.factor(val_data$class))
acc_bigger <- cm_bigger$overall['Accuracy']
cm_bigger
#> Confusion Matrix and Statistics
#>
#> Reference
#> Prediction buildings forest glacier mountain sea street
#> buildings 396 0 2 1 2 13
#> forest 1 469 2 2 4 0
#> glacier 1 2 479 61 5 0
#> mountain 0 0 50 452 4 0
#> sea 1 1 16 7 492 2
#> street 38 2 4 2 3 486
#>
#> Overall Statistics
#>
#> Accuracy : 0.9247
#> 95% CI : (0.9146, 0.9339)
#> No Information Rate : 0.1843
#> P-Value [Acc > NIR] : < 0.0000000000000002
#>
#> Kappa : 0.9095
#>
#> Mcnemar's Test P-Value : 0.00281
#>
#> Statistics by Class:
#>
#> Class: buildings Class: forest Class: glacier Class: mountain Class: sea Class: street
#> Sensitivity 0.9062 0.9895 0.8662 0.8610 0.9647 0.9701
#> Specificity 0.9930 0.9964 0.9718 0.9782 0.9892 0.9804
#> Pos Pred Value 0.9565 0.9812 0.8741 0.8933 0.9480 0.9084
#> Neg Pred Value 0.9841 0.9980 0.9698 0.9707 0.9927 0.9939
#> Prevalence 0.1457 0.1580 0.1843 0.1750 0.1700 0.1670
#> Detection Rate 0.1320 0.1563 0.1597 0.1507 0.1640 0.1620
#> Detection Prevalence 0.1380 0.1593 0.1827 0.1687 0.1730 0.1783
#> Balanced Accuracy 0.9496 0.9929 0.9190 0.9196 0.9769 0.9752
模型在验证数据集上的准确率为92%!尽管如此,仍然存在一些错误分类,因为没有一个模型是完美的。以下是预测的摘要:
1. 有些建筑被错误地预测为街道,反之亦然。同样,这是由于一些包含街道的建筑物图像混淆了模型。
2. 森林的预测几乎是完美的。
3. 许多冰川被预测为山脉和海洋,也有许多山脉被预测为冰川。
4. 海洋预测良好。
结论
rbind(
"Simple CNN" = acc_simple,
"Deeper CNN" = acc_big,
"Fine-tuned VGG16" = acc_bigger
)
#> Accuracy
#> Simple CNN 0.7423333
#> Deeper CNN 0.8486667
#> Fine-tuned VGG16 0.9246667
我们已经成功地完成了6个类别的图像分类:“建筑物”、“森林”、“冰川”、“山”、“海”和“街道”。
由于图像是非结构化数据,可以通过使用神经网络进行机器学习来解决这一问题,神经网络可以自动进行特征提取,而无需人工干预。
为了获得更好的性能,我们使用卷积神经网络对密集层进行连续预测。最后,我们使用VGG16模型进行初始化权重,达到92%的准确率。