本课程主要介绍基于R的语言的深度学习,主要会介绍的是keras 这个包,或者说这个框架。同学学过之后能够理解深度学习整个框架并且能够熟练使用R和Keras构建深度学习模型。

资料:

  1. https://keras.io/examples/ -python
  2. https://keras.rstudio.com/ - R keras
  3. Tensorflow:https://tensorflow.rstudio.com/
  4. https://tensorflow.rstudio.com/tutorials/ 教程
  5. https://blogs.rstudio.com/ai/ 里面有很多相关文章
  6. keras cheatsheet :https://raw.githubusercontent.com/rstudio/cheatsheets/main/keras.pdf

1 什么是深度学习

最近,人工智能,机器学习,数据挖掘,深度学习这些概念都非常的火,人人都听说过这些词语,人人也都想把这些东西应用起来,但是鲜少有人能够很好的理解这些东西到底是什么,能够干什么原理是什么。

人工智能,artificial intelligence ,也就是AI,想必大多数人曾经通过文艺作品对AI这个概念有过不准确的了解。人工智能,AI起源于1950年代,其属于计算机科学的一个领域,这个领域的起源思想其实也非常的朴素,就是计算机能不能像人一样的思考。不过很遗憾,到今天位置,计算机离像人那样能思考的那一天还非常的遥远。

关于这个领域的一个定义是:the effort to automate intellectual tasks normally performed by humans。

人工智能领域包含了机器学习(machine leaning )和深度学习(deep learning),除此之外,还有很多其他内容属于人工只能这个领域,例如强化学习。

机器学习,有时候也叫统计机器学习,主要是解决两类问题,分类(classification)和回归(regression)。分类指的是将目标划分成为某一个类别,例如,图片中的动物是不是狗(二分类),或者图片中的数字是几(多酚类)。回归指的是预测出一个数值,例如预测某个区域的房价。二者本质上都是构建一个映射,数据到(data)目标(label)的一个映射

深度学习其实是深度学习中的一个特殊的领域,深度学习模型通过层(layer)构成,

神经网络本质上也是构建这样的映射,我们通过一个图来了解深度学习的工作原理。

深度学习在图片识别,语音识别,自然语言处理等领域有非常成功的应用,但是深度学习不是万能的,在很多场景深度学习也不是必要的,不要为了用某个技术而用某个技术。

安装keras

install.packages("keras")
library(keras)
install_keras() # 安装核心keeras库和tensorflow

2 hello world

深度学习是层数比较多的神经网络(简单这么理解),我们首先来做一个神经网络的例子,在这个例子我们来实现手写数字识别。

手写数字识别算是深度学习领域的hello world,这里使用MNIST数据集,该数据集雨欧60000张训练图像和10000张测试图像。手写数字是28*28像素的灰度图片,有10个类别-0-9。

library(keras)
mnist <- dataset_mnist() 
## Loaded Tensorflow version 2.9.2
train_images  <-   mnist$train$x 
train_labels <-  mnist$train$y 
test_images <-  mnist$test$x 
test_labels <- mnist$test$y

# tensorflow::install_tensorflow()
# tensorflow::tf_config()

dataset_mnist函数获取数据集。

在这里稍微提一下,图片数据在编程中是怎样体现的,图片转换成为数据的方式其实有很多种。在这个例子中,因为手写数字图片是黑白的,因此只需要一个矩阵记录不同像素点的亮度即可,我们可以查看一下图片数据。

plot(as.raster(train_images[1,,], max = 255))

train_images[1,,]
##       [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13]
##  [1,]    0    0    0    0    0    0    0    0    0     0     0     0     0
##  [2,]    0    0    0    0    0    0    0    0    0     0     0     0     0
##  [3,]    0    0    0    0    0    0    0    0    0     0     0     0     0
##  [4,]    0    0    0    0    0    0    0    0    0     0     0     0     0
##  [5,]    0    0    0    0    0    0    0    0    0     0     0     0     0
##  [6,]    0    0    0    0    0    0    0    0    0     0     0     0     3
##  [7,]    0    0    0    0    0    0    0    0   30    36    94   154   170
##  [8,]    0    0    0    0    0    0    0   49  238   253   253   253   253
##  [9,]    0    0    0    0    0    0    0   18  219   253   253   253   253
## [10,]    0    0    0    0    0    0    0    0   80   156   107   253   253
## [11,]    0    0    0    0    0    0    0    0    0    14     1   154   253
## [12,]    0    0    0    0    0    0    0    0    0     0     0   139   253
## [13,]    0    0    0    0    0    0    0    0    0     0     0    11   190
## [14,]    0    0    0    0    0    0    0    0    0     0     0     0    35
## [15,]    0    0    0    0    0    0    0    0    0     0     0     0     0
## [16,]    0    0    0    0    0    0    0    0    0     0     0     0     0
## [17,]    0    0    0    0    0    0    0    0    0     0     0     0     0
## [18,]    0    0    0    0    0    0    0    0    0     0     0     0     0
## [19,]    0    0    0    0    0    0    0    0    0     0     0     0     0
## [20,]    0    0    0    0    0    0    0    0    0     0     0     0    39
## [21,]    0    0    0    0    0    0    0    0    0     0    24   114   221
## [22,]    0    0    0    0    0    0    0    0   23    66   213   253   253
## [23,]    0    0    0    0    0    0   18  171  219   253   253   253   253
## [24,]    0    0    0    0   55  172  226  253  253   253   253   244   133
## [25,]    0    0    0    0  136  253  253  253  212   135   132    16     0
## [26,]    0    0    0    0    0    0    0    0    0     0     0     0     0
## [27,]    0    0    0    0    0    0    0    0    0     0     0     0     0
## [28,]    0    0    0    0    0    0    0    0    0     0     0     0     0
##       [,14] [,15] [,16] [,17] [,18] [,19] [,20] [,21] [,22] [,23] [,24] [,25]
##  [1,]     0     0     0     0     0     0     0     0     0     0     0     0
##  [2,]     0     0     0     0     0     0     0     0     0     0     0     0
##  [3,]     0     0     0     0     0     0     0     0     0     0     0     0
##  [4,]     0     0     0     0     0     0     0     0     0     0     0     0
##  [5,]     0     0     0     0     0     0     0     0     0     0     0     0
##  [6,]    18    18    18   126   136   175    26   166   255   247   127     0
##  [7,]   253   253   253   253   253   225   172   253   242   195    64     0
##  [8,]   253   253   253   253   251    93    82    82    56    39     0     0
##  [9,]   253   198   182   247   241     0     0     0     0     0     0     0
## [10,]   205    11     0    43   154     0     0     0     0     0     0     0
## [11,]    90     0     0     0     0     0     0     0     0     0     0     0
## [12,]   190     2     0     0     0     0     0     0     0     0     0     0
## [13,]   253    70     0     0     0     0     0     0     0     0     0     0
## [14,]   241   225   160   108     1     0     0     0     0     0     0     0
## [15,]    81   240   253   253   119    25     0     0     0     0     0     0
## [16,]     0    45   186   253   253   150    27     0     0     0     0     0
## [17,]     0     0    16    93   252   253   187     0     0     0     0     0
## [18,]     0     0     0     0   249   253   249    64     0     0     0     0
## [19,]     0    46   130   183   253   253   207     2     0     0     0     0
## [20,]   148   229   253   253   253   250   182     0     0     0     0     0
## [21,]   253   253   253   253   201    78     0     0     0     0     0     0
## [22,]   253   253   198    81     2     0     0     0     0     0     0     0
## [23,]   195    80     9     0     0     0     0     0     0     0     0     0
## [24,]    11     0     0     0     0     0     0     0     0     0     0     0
## [25,]     0     0     0     0     0     0     0     0     0     0     0     0
## [26,]     0     0     0     0     0     0     0     0     0     0     0     0
## [27,]     0     0     0     0     0     0     0     0     0     0     0     0
## [28,]     0     0     0     0     0     0     0     0     0     0     0     0
##       [,26] [,27] [,28]
##  [1,]     0     0     0
##  [2,]     0     0     0
##  [3,]     0     0     0
##  [4,]     0     0     0
##  [5,]     0     0     0
##  [6,]     0     0     0
##  [7,]     0     0     0
##  [8,]     0     0     0
##  [9,]     0     0     0
## [10,]     0     0     0
## [11,]     0     0     0
## [12,]     0     0     0
## [13,]     0     0     0
## [14,]     0     0     0
## [15,]     0     0     0
## [16,]     0     0     0
## [17,]     0     0     0
## [18,]     0     0     0
## [19,]     0     0     0
## [20,]     0     0     0
## [21,]     0     0     0
## [22,]     0     0     0
## [23,]     0     0     0
## [24,]     0     0     0
## [25,]     0     0     0
## [26,]     0     0     0
## [27,]     0     0     0
## [28,]     0     0     0

raster 类用于存储rgb颜色值的矩阵。

这里数据是通过数组array进行存储的,我们查看一下数组的维度。

dim(train_images)
## [1] 60000    28    28

可以看到,这个数据集包含了60000条数据,也就是60000个图片,每个图片由28*28个像素组成。

然后,我们要开始定义我们模型的结构。

network <- keras_model_sequential() %>%
layer_dense(units = 512, activation = "relu", input_shape = c(28 * 28)) %>%layer_dense(units = 10, activation = "softmax")

上面呢的代码中,keras_model_sequential函数表示我们要定一个深度学习模型了,layer_函数用于定义深度学习模型中的层。units用于指定这一层有多少输出,activation用于指定激活函数,在人工神经网络中,节点的激活函数定义了给定输入或一组输入的节点的输出。,input_shape用于指定数据的维度,我们的图片是28*28,因此这里也指定为28乘以28 。其他层同理。

神经网络的核心是層,可以理解为一个数据处理模块,讲数据过滤兵以某种更有用的形式输出出来。在这个例子中有两层稠密连接或者叫做完全连接神经层。最后一层的激活函数是softmax,并且unit为10,这意味着返回,是个概率值,其总和为1 。

为了训练网网络,还需要进行一些设置:

  1. 损失函数:损失函数告诉了模型的训练方向
  2. 优化器:模型根据数据和损失函数来进行跟新参数的方法
  3. 模型的度量:例如准确率

对应keras中,在编译模型的过程中,我们需要指定几个参数: 1. loss function ,损失函数 2. optimiser ,优化器 3. Metrics ,指标

network %>% compile(
optimizer = "rmsprop",
loss = "categorical_crossentropy", metrics = c("accuracy")
)

compile 函数只是修改了网络,而不是返回一个新对象。这意味着不需要重新赋值。

编译好模型之后,我们可以开始训练模型,但是在训练模型之前,我们对数据进行简单的处理:

train_images <- array_reshape(train_images, c(60000, 28 * 28)) 
dim(train_images)
## [1] 60000   784
train_images <- train_images / 255
test_images <- array_reshape(test_images, c(10000, 28 * 28)) 
test_images <- test_images / 255

需要注意的是,我们之前的数据集是60000-28-28的数组,意味着6万张28*28像素的图片。我们这里将数据转变成为2纬的数据。

注意这里使用的是array_reshape而不是dim函数。我们来看一个例子:

然后这里还将数据除以了255,这是为了讲数据转换到0-1之间,原始数据的最大值是255。

train_labels <- to_categorical(train_labels)
test_labels <- to_categorical(test_labels)

这里是对目标变量进行了转换,转换前后的结果我们进行一个对比。

mnist$train$y[1]
## [1] 5
train_labels[1,]
##  [1] 0 0 0 0 0 1 0 0 0 0

数据准备好之后就可以开始训练模型了。使用fit函数拟合模型

network %>% fit(train_images,train_labels,epochs=5,batch_size= 128)

训练期间显示出了损失值和精度。

其中,batch_size是一次训练选取的样本个数,epoch是指使用全部样本训练模型的次数。

举个例子说,假设样本量为100,batch_size为10,那么每次训练模型用10个样本。

如果epoch为10,那表示要将全部样本训练10次。一个Epoch就是将所有训练样本训练一次的过程。

但是每次训练只用10个样本,完成一个epoch需要训练10词,因此一共训练了100次,这个100就是iteration。

这里表示模型以每次128个样本的小批次迭代训练模型,训练一轮是469次,训练5轮。

训练好模型之后,可以使用测试数据测试模型。

metrics <- network %>% evaluate(test_images, test_labels)
metrics
##       loss   accuracy 
## 0.06361058 0.98130000

有了新数据之后,我们就可以使用新数据来进行预测。

network %>% predict(test_images[1:2,])
##              [,1]         [,2]         [,3]         [,4]        [,5]
## [1,] 1.072515e-08 3.230695e-10 1.146493e-06 1.531296e-04 7.05434e-12
## [2,] 5.373248e-11 2.117850e-07 9.999998e-01 1.619350e-08 1.27202e-18
##              [,6]         [,7]         [,8]         [,9]        [,10]
## [1,] 7.952181e-07 1.285119e-13 9.998432e-01 5.679169e-07 1.012717e-06
## [2,] 4.526011e-09 1.552863e-10 9.538847e-19 1.226291e-08 6.824760e-18

optimiser,loss,metric和激活函数的总结

通过这个简单的例子之后,我们进行一个简单的总结。

(1)可用的optimiser

Available optimizers
- SGD
- RMSprop
- Adam
- Adadelta
- Adagrad
- Adamax
- Nadam
- Ftrl

(2)可用的损失函数

Probabilistic losses
- BinaryCrossentropy class
- CategoricalCrossentropy class
- SparseCategoricalCrossentropy class
- Poisson class
- binary_crossentropy function
- categorical_crossentropy function
- sparse_categorical_crossentropy function
- poisson function
- KLDivergence class
- kl_divergence function
Regression losses
- MeanSquaredError class
- MeanAbsoluteError class
- MeanAbsolutePercentageError class
- MeanSquaredLogarithmicError class
- CosineSimilarity class
- mean_squared_error function
- mean_absolute_error function
- mean_absolute_percentage_error function
- mean_squared_logarithmic_error function
- cosine_similarity function
- Huber class
- huber function
- LogCosh class
- log_cosh function
Hinge losses for "maximum-margin" classification
- Hinge class
- SquaredHinge class
- CategoricalHinge class
- hinge function
- squared_hinge function
- categorical_hinge function

(3)可用的指标

Accuracy metrics
- Accuracy class
- BinaryAccuracy class
- CategoricalAccuracy class
- SparseCategoricalAccuracy class
- TopKCategoricalAccuracy class
- SparseTopKCategoricalAccuracy class
Probabilistic metrics
- BinaryCrossentropy class
- CategoricalCrossentropy class
- SparseCategoricalCrossentropy class
- KLDivergence class
- Poisson class
Regression metrics
- MeanSquaredError class
- RootMeanSquaredError class
- MeanAbsoluteError class
- MeanAbsolutePercentageError class
- MeanSquaredLogarithmicError class
- CosineSimilarity class
- LogCoshError class
Classification metrics based on True/False positives & negatives
- AUC class
- Precision class
- Recall class
- TruePositives class
- TrueNegatives class
- FalsePositives class
- FalseNegatives class
- PrecisionAtRecall class
- SensitivityAtSpecificity class
- SpecificityAtSensitivity class
Image segmentation metrics
- MeanIoU class
Hinge metrics for "maximum-margin" classification
- Hinge class
- SquaredHinge class
- CategoricalHinge class

(4) 可用的激活函数

relu,sigmoid,softmax,softplus,softsign,tanh,selu,elu,exponential,

深度学习的数据表示

通常情况下,我们构建模型时候,数据通常会用一个数据框来表述,例如iris数据集

iris %>% head()
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1          5.1         3.5          1.4         0.2  setosa
## 2          4.9         3.0          1.4         0.2  setosa
## 3          4.7         3.2          1.3         0.2  setosa
## 4          4.6         3.1          1.5         0.2  setosa
## 5          5.0         3.6          1.4         0.2  setosa
## 6          5.4         3.9          1.7         0.4  setosa

但是在深度学习中,数据用张量tensor表示,张量是向量和矩阵到任意维度的泛化。张量中维度通常称为轴。

0维张量称为标量,在R中用长度为1的向量表示。

1D tensor,一维数组就是1D tensor,关于tensor 的解释是:Tensors are a generalization of vectors and matrices to an arbitrary number of dimensions 。简单理解就是数组或者矩阵。

x <- rep(1:10)
as.array(x)
##  [1]  1  2  3  4  5  6  7  8  9 10

2D tensor 就是矩阵。有两个轴,行和列。

x <- matrix(rep(0, 3*5), nrow = 3, ncol = 5)
dim(x)
## [1] 3 5

3D tensor 就是高维张量,在R中用,超过3维的张量数组表示

x <- array(rep(0, 2*3*2), dim = c(2,3,2))
dim(x)
## [1] 2 3 2

关于张量的关键属性

  1. 轴:矩阵两个轴
  2. 格式 :描述每个轴的长度。例如2*3的矩阵就是两个轴的张量,第一个轴长度为2,第二个轴长度为3 。dim函数查看
  3. 数据类型:integer,double

在现实中,大多数表格数据使用2D tensor来表示,也就是(samples, features),大数据机器学习模型处理的也是这一类数据,也称为表格数据。

时间数据或者序列数据通常用3D tensor来表示,(samples, timesteps, features)

图片数据通常用4D tensor来表示,(samples, height, width, channels)or (samples, channels, height, width),

灰度图片只具有单个颜色因此可以用矩阵存储,但图片还是用3维张量表示。因为任意颜色可以用3原色表示,图片数据通常有3个通道,也就是说,一张图片需要三张矩阵来表示。

视频数据使用5D tensor 来表示。(samples, frames, height, width, channels)or(samples, frames, channels, height, width)

视频是帧序列,每一帧都是一幅彩色图像。

关于层的简单解释

我们之前的例子中,我们使用全连接层来构建网络。

layer_dense(units = 512, activation = "relu", input_shape = c(28 * 28))

这些层相当于一个数据转换,将输入的数据转化成为另外的数据。具体来说,转换如下:

output = relu(dot(W,input)+b)

input就是输入,W和b是参数,训练模型呢的过程就是找到最优参数的过程。dot表示点积。relu(x)=max(x,0)。

其他的层都是数据转换,只不过转换的规则不一样罢了。

关于深度学习的理解

深度学习就是通过张量运算的层组成,这些运算也都是关于数据的几何变换。因此,本质上深度学习是高维空间中复杂的几何变换。

我们可以想象有两种不同颜色的纸,被揉在了一起。这些纸就是你的数据,每种颜色的纸表示分类问题中的一类数据,而模型就是找到一种能够展开纸的方式,从而将两张纸分开。

训练模型的过程基本是如下几个步骤:

  1. 选取训练样本和对应的标签
  2. 运行模型,以的得到预测y
  3. 计算损失,损失描述了预测和真实的差异
  4. 以略微减少损失的方式更新模型的参数(如何更新取决于优化方法optimiser)

3 Keras 深度学习

训练深度学习模型总是围绕着一下对象:

  1. 数据
  2. 损失函数
  3. 优化器

训练可以用图下表示:

我们分别来介绍这些对象

layer - 层

层本质上是数据处理模块,他讲一个或者多个张量作为输入,输出一个或者多个张量。大多数情况下层都是有参数的。

不同的层适用于不同的张量格式和不同类型的数据处理。二维张量用全连接层 layer_dense。三维张量(样本,时间,特征)用layer_lstm层。存储图片的四维张量用layer_conv_3d层处理。

可以将深度学习理解为积木,你要做的是将层合理的组合在一起,以形成有用的数据转换通道。

层网络

深度学习模型是关于层的有向无环图,最常见的就是线性堆栈层,将单个输入映射到单个输出。除此之外,还有更加复杂的拓扑结构,包括:

  1. 双分支网络
  2. 多头网络
  3. 初始块

如何创建合适的层网络并不是一件容易的事情,其更像是一门艺术,而不是一门科学。虽然有一些原则,但只有不断的练习和尝试创造出合适的网络。

损失函数和优化器

构建深度学习模型第一步是构建网络架构,接着需要选择:

  1. 损失函数:表示模型优化的目标
  2. 优化器:根据损失函数确定模型的更新方式,通常是随机梯度下降和其变体。

为正确的问题选择正确的损失函数很重要;例如对于二分类问题,可以使用二元交叉墒,对于多分类问题可以使用分类交叉墒,对于回归问题使用均方误差等等。如果有需要,还可以使用自己构造的损失函数。

Keras

Keras 官网: https://keras.rstudio.com。keras 是一个深度学习框架,其后端是Tensorflow。Keras 提供了一种更加简洁的方法训练任何深度学习模型。

Keras主要功能包括:

  1. 代码允许在CPU或GPU上运行
  2. 有用户友好的API,易于使用
  3. 对卷积神经网络和循环神经网络以及二者组合有内置支持
  4. 支持任意网络架构,可以构建任何深度学习模型

keras以及其R接口遵循MIT许可,这意味着可以在商业项目中任意使用。

Keras项目可以在https://keras.io上找到。

安装keras

在R中安装keras

install.packages("keras")
library(keras)
install_keras() # 安装核心keeras库和tensorflow

这将基于CPU安装Keras和tensorflow。如果想要在GPU上面训练深度学习模型,这需要具有NVIDIA GPU 和正确配置的CUDA和cuDNN库。则可以安装基于GPU的tensorflow后端引擎版本。

install_keras(tensorflow = "gpu")

如果不满足这些条件,则无法安装。

Keras 工作流

R中使用keras构建深度学习模型典型的工作流如下所示:

  1. 定义训练数据:输入张量和目标张量
  2. 定义层网络
  3. 选择损失函数,优化器和模型指标
  4. 使用fit函数拟合模型
  5. 评估模型
  6. 预测

定义模型有两种方式 1. keras_model()
2. keras_model_sequential()

keras_model_sequential() 函数用于线性堆栈模型。keras_model() 函数可以构造更加灵活的深度学习层网络。

network <- keras_model_sequential() %>%
layer_dense(units = 512, activation = "relu", input_shape = c(28 * 28)) %>%layer_dense(units = 10, activation = "softmax")

## 等价于

tensor_input <- layer_input(shape = c(28*28))
tensor_output <- tensor_input %>% layer_dense(units = 512,activation = "relu") %>% layer_dense(units = 10,activation = "softmax")

network <- keras_model(inputs = tensor_input,outputs = tensor_output)

一旦定义了结构,就可以设定损失函数,优化器,模型指标。使用compile函数。

具体有哪些设置,可以通过查看官方的网站查看:https://keras.io/api/optimizers/

network %>% compile(
  optimizer = "SGD",
  loss = "mse",
  metrics = "accuracy"
)

最后,使用fit函数拟合数据和模型。

深度学习环境

运行深度学习,最好使用Unix系统,虽然Keras 支持Windows,但是从长远来看,使用Unix可以避免很多麻烦。

如果本地安装深度学习环境有问题,可以使用云服务器中的r,例如:https://rstudio.cloud/projects。Rstudio 提供了一个免费的环境,但是内存比较小。

我们再来看几个例子。

二分类

在这个例子中我们使用IMDB数据集,数据集提供了关于电影的正面评论和负面评论评论。Keras包自带了MNIST数据集,并且已经经过预处理。

以下代码将加载数据集(当你第一次运行它,大约80MB的数据将下载到机器):

library(keras)

imdb <- dataset_imdb(num_words = 10000)
c(c(train_data, train_labels), c(test_data, test_labels)) %<-% imdb

这里使用了%<-%运算符,这个运算符可以将列表转变成为一组不同的变量。

c(c(train_data, train_labels), c(test_data, test_labels)) %<-% imdb
# 等价于

train_data <- imdb$train$x
train_labels <- imdb$train$y
test_data <- imdb$test$x
test_labels <- imdb$test$y

参数 num_words = 10000 ’意味着我们将只保留训练数据中最频繁出现的前10,000个单词。罕见的词会被丢弃。

变量’ train_data ‘和’ test_data ‘是评论列表,每个评论都是单词索引列表(编码单词序列)。’ train_labels ‘和’ test_labels ’是0和1的列表,其中0代表”negative”, 1代表”positive”:

str(train_data[[1]])
##  int [1:218] 1 14 22 16 43 530 973 1622 1385 65 ...
train_labels[[1]]
## [1] 1

因为我们将自己限制在最频繁出现的前10,000个单词中,所以没有单词索引会超过10,000:

max(sapply(train_data, max))
## [1] 9999

下面是如何快速将这些评论解码为英语单词的方法:

# word_index is a dictionary mapping words to an integer index
word_index <- dataset_imdb_word_index()
# We reverse it, mapping integer indices to words
reverse_word_index <- names(word_index)
names(reverse_word_index) <- word_index
# We decode the review; note that our indices were offset by 3
# because 0, 1 and 2 are reserved indices for "padding", "start of sequence", and "unknown".
decoded_review <- sapply(train_data[[1]], function(index) {
  word <- if (index >= 3) reverse_word_index[[as.character(index - 3)]]
  if (!is.null(word)) word else "?"
})
cat(decoded_review)
## ? this film was just brilliant casting location scenery story direction everyone's really suited the part they played and you could just imagine being there robert ? is an amazing actor and now the same being director ? father came from the same scottish island as myself so i loved the fact there was a real connection with this film the witty remarks throughout the film were great it was just brilliant so much that i bought the film as soon as it was released for ? and would recommend it to everyone to watch and the fly fishing was amazing really cried at the end it was so sad and you know what they say if you cry at a film it must have been good and this definitely was also ? to the two little boy's that played the ? of norman and paul they were just brilliant children are often left out of the ? list i think because the stars that play them all grown up are such a big profile for the whole film but these children are amazing and should be praised for what they have done don't you think the whole story was so lovely because it was true and was someone's life after all that was shared with us all

准备数据

我们的训练数据集train_data是一个列表。

class(train_data)
## [1] "list"

我们需要首先将列表转换成为张量,有两种方法:

  1. 填充列表,将数据转换成为(samples,word_indices)格式
  2. 对列表进行one hot 编码。One-Hot编码是分类变量作为二进制向量的表示。

不管选择哪一种,我们需要将数据转变成为张量。

vectorize_sequences <- function(sequences, dimension = 10000) {
  # Create an all-zero matrix of shape (len(sequences), dimension)
  results <- matrix(0, nrow = length(sequences), ncol = dimension)
  for (i in 1:length(sequences))
    # Sets specific indices of results[i] to 1s
    results[i, sequences[[i]]] <- 1
  results
}

# Our vectorized training data
x_train <- vectorize_sequences(train_data)
# Our vectorized test data
x_test <- vectorize_sequences(test_data)

这就是转换之后的样本。

str(x_train[1,])
##  num [1:10000] 1 1 0 1 1 1 1 1 1 0 ...

我们还应该向量化我们的标签,这很简单:

# Our vectorized labels
y_train <- as.numeric(train_labels)
y_test <- as.numeric(test_labels)

现在,我们的数据已经准备好了

构建网络

输入数据是向量-数据集是二维张量,标签是标量。对于这样的问题可以使用全连接层,并且使用relu激活函数。例如layer_dense(units = 10, activation = “relu”, input_shape = c(10000)) . 10 表示的是该层的隐藏单元数目,更多的隐藏单元意味着网络可以学习更加复杂的空间(10维空间),但是计算成本会更高。

关于全连接层网络,有几个关键点:

  1. 使用多少层
  2. 每层选择多少隐藏单元

在这个例子中,使用两个中间层,有10个隐藏单元。第三层输出预测结果。中间层使用rule激活函数,输出层使用sigmoid为激活函数。

代码如下所示。

library(keras)

model <- keras_model_sequential() %>% 
  layer_dense(units = 10, activation = "relu", input_shape = c(10000)) %>% 
  layer_dense(units = 10, activation = "relu") %>% 
  layer_dense(units = 1, activation = "sigmoid")

为什么要激活函数,其实不加激活函数也可以,但是激活函数可以将数据转换到更加丰富的空间。relu是最常使用的激活哈书,其他还包括prelu,elu等等。

最后需要选择一个损失函数和一个优化器。这里使用二元交叉墒,

model %>% compile(
  optimizer = "rmsprop",
  loss = "binary_crossentropy",
  metrics = c("accuracy")
)

如果需要需要设置细节参数,可以使用optimizer_函数

model %>% compile(
  optimizer = optimizer_rmsprop(learning_rate=0.001),
  loss = "binary_crossentropy",
  metrics = c("accuracy")
) 

训练模型

拆分数据集,将1w数据设置成为验证数据集。

val_indices <- 1:10000

x_val <- x_train[val_indices,]
partial_x_train <- x_train[-val_indices,]

y_val <- y_train[val_indices]
partial_y_train <- y_train[-val_indices]

接下来进行训练模型,进行20轮训练,以512个样本进行小批次训练。

model %>% compile(
  optimizer = "rmsprop",
  loss = "binary_crossentropy",
  metrics = c("accuracy")
)

history <- model %>% fit(
  partial_x_train,
  partial_y_train,
  epochs = 20,
  batch_size = 512,
  validation_data = list(x_val, y_val)
)

训练王城之后,查看fit的结果。

str(history)
## List of 2
##  $ params :List of 3
##   ..$ verbose: int 1
##   ..$ epochs : int 20
##   ..$ steps  : int 30
##  $ metrics:List of 4
##   ..$ loss        : num [1:20] 0.525 0.323 0.241 0.194 0.159 ...
##   ..$ accuracy    : num [1:20] 0.791 0.898 0.924 0.94 0.951 ...
##   ..$ val_loss    : num [1:20] 0.397 0.332 0.29 0.277 0.277 ...
##   ..$ val_accuracy: num [1:20] 0.866 0.871 0.887 0.889 0.889 ...
##  - attr(*, "class")= chr "keras_training_history"

fit返回的对象包括你和模型的参数,以及模型度量。fit对象可以直接作为plot函数的参数。

plot(history)

图形显示出训练过程中的损失变化、注意,由于网络的随机初始化不同,结果可能略有不同。

可以看到,训练损失随时间的增加而减小,训练精度随时间的增加而增加。从图中可以看到,模型似乎有过拟合。这个时候可以降低训练的epoch。

重新开始训练模型,将epoch 设置为4

model <- keras_model_sequential() %>% 
  layer_dense(units = 10, activation = "relu", input_shape = c(10000)) %>% 
  layer_dense(units = 10, activation = "relu") %>% 
  layer_dense(units = 1, activation = "sigmoid")

model %>% compile(
  optimizer = "rmsprop",
  loss = "binary_crossentropy",
  metrics = c("accuracy")
)

model %>% fit(x_train, y_train, epochs = 4, batch_size = 512)
results <- model %>% evaluate(x_test, y_test)
results
##      loss  accuracy 
## 0.2879807 0.8844400

预测结果准确度达到了88%。

进行预测

使用predict函数进行预测

model %>% predict(x_test[1:10,])
##            [,1]
##  [1,] 0.2084693
##  [2,] 0.9995337
##  [3,] 0.8700750
##  [4,] 0.6286948
##  [5,] 0.9292664
##  [6,] 0.7097180
##  [7,] 0.9981054
##  [8,] 0.0183397
##  [9,] 0.9271227
## [10,] 0.9485350
k_clear_session() # 清理内存

可以优化的点

可以优化的点还有很多,其中包括:

  1. 使用更多的层,使用不同的隐藏节点
  2. 使用不同的损失函数
  3. 使用不同的激活函数

总结

  1. 事实上,训练模型很大一部分工作在于数据的处理。
  2. 具有relu激活函数的全连接层layer_dense非常常用。
  3. 对于二分类问题,需要有一个单元和sigmoid激活函数的全连接层结果
  4. 对于二分类问题,损失函数通常选择binary_crossentropy
  5. rmsprop是最常用的优化器,不知道选什么,就选它
  6. 过拟合是很难避免的,模型毕竟没有创造性,他最终会对从未见过的数据产生越来越遭的结果。

多分类

Reuters 数据集

Reuters 数据集是路透社新闻数据集,这些新闻被分类为46个不同的主题。该数据集也可以直接从Keras包获取。

library(keras)

reuters <- dataset_reuters(num_words = 10000)
c(c(train_data, train_labels), c(test_data, test_labels)) %<-% reuters

与IMDB数据集一样,参数’ num_words = 10000 ’将数据限制为数据中最常出现的10,000个单词。

我们有8982个训练示例和2246个测试示例:

length(train_data)
## [1] 8982
length(test_data)
## [1] 2246

与IMDB评论一样,每个示例都是一个整数列表(单词索引):

train_data[[1]]
##  [1]    1    2    2    8   43   10  447    5   25  207  270    5 3095  111   16
## [16]  369  186   90   67    7   89    5   19  102    6   19  124   15   90   67
## [31]   84   22  482   26    7   48    4   49    8  864   39  209  154    6  151
## [46]    6   83   11   15   22  155   11   15    7   48    9 4579 1005  504    6
## [61]  258    6  272   11   15   22  134   44   11   15   16    8  197 1245   90
## [76]   67   52   29  209   30   32  132    6  109   15   17   12

以下是如何将其解码为文字的方法:

word_index <- dataset_reuters_word_index()
reverse_word_index <- names(word_index)
names(reverse_word_index) <- word_index
decoded_newswire <- sapply(train_data[[1]], function(index) {
  # Note that our indices were offset by 3 because 0, 1, and 2
  # are reserved indices for "padding", "start of sequence", and "unknown".
  word <- if (index >= 3) reverse_word_index[[as.character(index - 3)]]
  if (!is.null(word)) word else "?"
})
cat(decoded_newswire)
## ? ? ? said as a result of its december acquisition of space co it expects earnings per share in 1987 of 1 15 to 1 30 dlrs per share up from 70 cts in 1986 the company said pretax net should rise to nine to 10 mln dlrs from six mln dlrs in 1986 and rental operation revenues to 19 to 22 mln dlrs from 12 5 mln dlrs it said cash flow per share this year should be 2 50 to three dlrs reuter 3

标签是0到45的一个值

train_labels[[1]]
## [1] 3

数据预处理

我们可以使用与前面例子完全相同的代码对数据进行向量化:

vectorize_sequences <- function(sequences, dimension = 10000) {
  results <- matrix(0, nrow = length(sequences), ncol = dimension)
  for (i in 1:length(sequences))
    results[i, sequences[[i]]] <- 1
  results
}

x_train <- vectorize_sequences(train_data)
x_test <- vectorize_sequences(test_data)

为了向量化标签,有两种可能:我们可以将标签列表转换为一个整数张量,或者我们可以使用”one-hot”编码。one hot编码是一种广泛使用的分类数据格式,也称为”分类编码”。

to_one_hot <- function(labels, dimension = 46) {
  results <- matrix(0, nrow = length(labels), ncol = dimension)
  for (i in 1:length(labels))
    results[i, labels[[i]] + 1] <- 1
  results
}

one_hot_train_labels <- to_one_hot(train_labels)
one_hot_test_labels <- to_one_hot(test_labels)

Keras提供了一种内置的方法进行one hot 编码。

one_hot_train_labels <- to_categorical(train_labels)
one_hot_test_labels <- to_categorical(test_labels)

构建网络

在这个例子中,我们构建的46分类。对于46分类,使用10个隐藏节点可能会导致信息的丢失,无法学会分离46个不同的类。此时隐藏单元需要设置多一点呢。代码如下所示。

model <- keras_model_sequential() %>% 
  layer_dense(units = 64, activation = "relu", input_shape = c(10000)) %>% 
  layer_dense(units = 64, activation = "relu") %>% 
  layer_dense(units = 46, activation = "softmax")

需要注意亮点:

  1. 这里使用了46个单元的全连接层作为网络结束的层。这意味着对于每个输出样本,网络将输出46维向量。
  2. 最后一层使用softmax激活函数,这意味着输出46类不同的概率分布。

对于多分类问题,最佳的损失函数是categorical_crossentropy,其衡量两个概率分布之间的距离,即真实标签与预测结果概率分布的距离。

定义好网络结构之后,编译模型。

model %>% compile(
  optimizer = "rmsprop",
  loss = "categorical_crossentropy",
  metrics = c("accuracy")
)

训练模型

划分训练集和验证集。

val_indices <- 1:1000

x_val <- x_train[val_indices,]
partial_x_train <- x_train[-val_indices,]

y_val <- one_hot_train_labels[val_indices,]
partial_y_train = one_hot_train_labels[-val_indices,]

进行20轮训练,即设置epoch为20。

history <- model %>% fit(
  partial_x_train,
  partial_y_train,
  epochs = 20,
  batch_size = 512,
  validation_data = list(x_val, y_val)
)

现实损失和准确度。

plot(history)

第6轮训练开始过拟合。重新训练,设置epoch为6 。

model <- keras_model_sequential() %>% 
  layer_dense(units = 64, activation = "relu", input_shape = c(10000)) %>% 
  layer_dense(units = 64, activation = "relu") %>% 
  layer_dense(units = 46, activation = "softmax")
  
model %>% compile(
  optimizer = "rmsprop",
  loss = "categorical_crossentropy",
  metrics = c("accuracy")
)

history <- model %>% fit(
  partial_x_train,
  partial_y_train,
  epochs = 6,
  batch_size = 512,
  validation_data = list(x_val, y_val)
)

results <- model %>% evaluate(x_test, one_hot_test_labels)
results
##      loss  accuracy 
## 1.0055158 0.7800534

从结果来看,准确度达到了77%,效果似乎不错。

test_labels_copy <- test_labels
test_labels_copy <- sample(test_labels_copy)
length(which(test_labels == test_labels_copy)) / length(test_labels)
## [1] 0.1936776

进行预测

使用predict函数进行预测。

predictions <- model %>% predict(x_test)

预测结果会显示出46类的概率

dim(predictions)
## [1] 2246   46

46类的概率和为1

sum(predictions[1,])
## [1] 1

输出最高的概率类

which.max(predictions[1,])
## [1] 4
k_clear_session() # 清理内存

需要注意的是,也可以不对标签进行编码,即不进行onee hot 编码。这个时候需要选择不同的损失函数。之前使用的categorical_crossentropy损失函数要求标签是分类编码,对于整数标签,可以使用sparse_categorical_crossentropy损失函数。

model %>% compile(
  optimizer = "rmsprop",
  loss = "sparse_categorical_crossentropy",
  metrics = c("accuracy")
)

需要注意这两个损失函数在数学上是一样的。

中间层节点的数量

我们的标签是46类,那么隐藏节点数目最好大于46。我们可以尝试小的隐藏节点。

model <- keras_model_sequential() %>% 
  layer_dense(units = 64, activation = "relu", input_shape = c(10000)) %>% 
  layer_dense(units = 4, activation = "relu") %>% 
  layer_dense(units = 46, activation = "softmax")
  
model %>% compile(
  optimizer = "rmsprop",
  loss = "categorical_crossentropy",
  metrics = c("accuracy")
)

model %>% fit(
  partial_x_train,
  partial_y_train,
  epochs = 20,
  batch_size = 128,
  validation_data = list(x_val, y_val)
)

节点过少意味着压缩信息,压缩信息意味着信息的丢失,信息丢失则意味着准确率降低。

可以优化的点

  1. 尝试更多的隐藏节点
  2. 使用更多的隐藏层

总结

  1. 如果是N分类,则需要以N个节点,激活函数以softmax的全连接层结束
  2. 对于多分类问题,最常用的损失函数是categorical_crossentropy,即分类交叉墒
  3. 对于多分类标签,有两种处理方法,首先使用one hot编码,并且使用categorical_crossentropy作为损失函数。将标签编码为整数,则需要使用sparse_categorical_crossentropy作为损失函数。
  4. 避免中间层节点数目小于分类数量。

回归

回归问题通常用于预测一个连续数值。在这个例子中,我们使用的是boston的房价数据。

该数据集包含了饿住房地区,犯罪率,当地财产税等等信息。数据集一共有506条数据,13个特征。该数据通常可以通过Keras包直接获取。

library(keras)

dataset <- dataset_boston_housing()
c(c(train_data, train_targets), c(test_data, test_targets)) %<-% dataset
str(train_data)
##  num [1:404, 1:13] 1.2325 0.0218 4.8982 0.0396 3.6931 ...
str(test_data)
##  num [1:102, 1:13] 18.0846 0.1233 0.055 1.2735 0.0715 ...

我们有404个训练样本和102个测试样本。该数据包括13个特征。输入数据中的13个特征为。不同特征的含义如下所示。

  1. Per capita crime rate.
  2. Proportion of residential land zoned for lots over 25,000 square feet.
  3. Proportion of non-retail business acres per town.
  4. Charles River dummy variable (= 1 if tract bounds river; 0 otherwise).
  5. Nitric oxides concentration (parts per 10 million).
  6. Average number of rooms per dwelling.
  7. Proportion of owner-occupied units built prior to 1940.
  8. Weighted distances to five Boston employment centres.
  9. Index of accessibility to radial highways.
  10. Full-value property-tax rate per $10,000.
  11. Pupil-teacher ratio by town.
  12. 1000 * (Bk - 0.63) ** 2 where Bk is the proportion of Black people by town.
  13. % lower status of the population.

目标是业主自住住房的中位数价值,以数千美元为单位:

str(train_targets)
##  num [1:404(1d)] 15.2 42.3 50 21.1 17.7 18.5 11.3 15.6 15.6 14.4 ...

数据预处理

输入数据的数据范围过大会让模型训练变得困难,因此通常需要对数据进行标准化。即数据减去平均值并且除以标准差,使用scale函数进行标准化。

mean <- apply(train_data, 2, mean)
std <- apply(train_data, 2, sd)
train_data <- scale(train_data, center = mean, scale = std)
test_data <- scale(test_data, center = mean, scale = std)

需要注意的是,对于训练数据和测试数据,都是使用训练数据的均值和标准差进行标准化。

构建网络

由于数据比较少,最好构建一个较小的网络。通常而言,训练数据越少,过拟合越严重,而小的网络能够缓解过拟合。

# Because we will need to instantiate the same model multiple times,
# we use a function to construct it.
build_model <- function() {
  model <- keras_model_sequential() %>% 
    layer_dense(units = 64, activation = "relu", 
                input_shape = dim(train_data)[[2]]) %>% 
    layer_dense(units = 64, activation = "relu") %>% 
    layer_dense(units = 1) 
    
  model %>% compile(
    optimizer = "rmsprop", 
    loss = "mse", 
    metrics = c("mae")
  )
}

从结果来看到,结束的层并没有设置激活函数,这意味着深度学习可以自由的预测任何范围的值。

因为激活函数会限制输出的范围,例如使用sigmoid激活函数,那么范围限定为0-1 /

对于回归问题,这里使用mse损失函数,即均方误差。使用MAE平均绝对误差最为监控指标。

K 折交叉验证

K折交叉验证能够更好的避免过拟合。K折交叉验证会将数据拆分为K份,每次使用K-1份数据进行训练,1份进行验证。

k <- 4
indices <- sample(1:nrow(train_data))
folds <- cut(indices, breaks = k, labels = FALSE)

num_epochs <- 100
all_scores <- c()
for (i in 1:k) {
  cat("processing fold #", i, "\n")
  # Prepare the validation data: data from partition # k
  val_indices <- which(folds == i, arr.ind = TRUE) 
  val_data <- train_data[val_indices,]
  val_targets <- train_targets[val_indices]
  
  # Prepare the training data: data from all other partitions
  partial_train_data <- train_data[-val_indices,]
  partial_train_targets <- train_targets[-val_indices]
  
  # Build the Keras model (already compiled)
  model <- build_model()
  
  # Train the model (in silent mode, verbose=0)
  model %>% fit(partial_train_data, partial_train_targets,
                epochs = num_epochs, batch_size = 1, verbose = 0)
                
  # Evaluate the model on the validation data
  results <- model %>% evaluate(val_data, val_targets, verbose = 0)
  all_scores <- c(all_scores, results[2])
}  
## processing fold # 1 
## processing fold # 2 
## processing fold # 3 
## processing fold # 4

查看不同交叉验证的结果

all_scores
##      mae      mae      mae      mae 
## 2.207794 2.375802 2.600051 2.340694

查看平均结果

mean(all_scores)
## [1] 2.381085

尝试更多的epoch

# Some memory clean-up
k_clear_session()
num_epochs <- 500
all_mae_histories <- NULL
for (i in 1:k) {
  cat("processing fold #", i, "\n")
  
  # Prepare the validation data: data from partition # k
  val_indices <- which(folds == i, arr.ind = TRUE)
  val_data <- train_data[val_indices,]
  val_targets <- train_targets[val_indices]
  
  # Prepare the training data: data from all other partitions
  partial_train_data <- train_data[-val_indices,]
  partial_train_targets <- train_targets[-val_indices]
  
  # Build the Keras model (already compiled)
  model <- build_model()
  
  # Train the model (in silent mode, verbose=0)
  history <- model %>% fit(
    partial_train_data, partial_train_targets,
    validation_data = list(val_data, val_targets),
    epochs = num_epochs, batch_size = 1, verbose = 0
  )
  mae_history <- history$metrics$val_mean_absolute_error
  all_mae_histories <- rbind(all_mae_histories, mae_history)
}

计算每折每轮的MAE分数的平均值

average_mae_history <- data.frame(
  epoch = seq(1:ncol(all_mae_histories)),
  validation_mae = apply(all_mae_histories, 2, mean)
)

对结果进行可视化

library(ggplot2)
ggplot(average_mae_history, aes(x = epoch, y = validation_mae)) + geom_line()

It may be a bit hard to see the plot due to scaling issues and relatively high variance. Let’s use geom_smooth() to try to get a clearer picture:

ggplot(average_mae_history, aes(x = epoch, y = validation_mae)) + geom_smooth()

从结果来看,MAE在120轮左右停止改善,构建最终模型。

# Get a fresh, compiled model.
model <- build_model()

# Train it on the entirety of the data.
model %>% fit(train_data, train_targets,
          epochs = 80, batch_size = 16, verbose = 0)

result <- model %>% evaluate(test_data, test_targets)

result

k_clear_session()

小结

  1. 回归问题通常使用MSE作为损失函数。回归指标可以使用MAE平均绝对误差
  2. 数据之间量纲差别很大的时候需要对数据进行标准化
  3. 当数据很少的时候,K折交叉验证可以更好的评估模型
  4. 数据较少的时候,使用隐藏层较少的网络,一个或者两个能更合适,能够避免过拟合。

深度学习常用概念总结

  1. 将数据转变成为张量的过程叫做向量化
  2. 神经网络输入相差较大的数据是不安全,会触发较大的梯度更新,从而阻止网络收敛。为了使得模型顺利构建,有必要对数据进行标准化。
  3. 缺失值,在神经网络中,将缺失值设置为0是安全的
  4. 减轻过拟合可以通过正则化完成.例如在某一层添加L2正则化
layer_dense(kernel_regularizer = regularizer_l2(0.001))
# 表示该层中权重矩阵每个洗漱将0.001*weight_coefficient_value 添加到损失中
  1. dropout ,其用于随机丢弃多个输出特征。例如[1,2]经过0.5的dropout可能会变成[0,2]。dropout率就是特征被归零的概率。 可以通过
layer_dropout()

添加dropout层。(防止过拟合:更多训练集合,降低网络复杂度,添加正则项,添加dropout)

4.计算机视觉

卷积神经网络普遍应用于计算机视觉。在Keras中,卷积神经网络是通过layer_conv_2d和layer_max_pooling_2d层构建。

我们重新看MNIST数据集这个例子,首先构建网络,如下所示。

library(keras)

model <- keras_model_sequential() %>% 
  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu",
                input_shape = c(28, 28, 1)) %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu")

需要注意的是,计算机视觉问题张量的输入格式是(image height,image width,image channels),在这种情况下,MNIST图形格式是(28,28,1)。随着不同层的数据转换,图片的尺寸不断缩小。通道数量通过filters参数进行指定。

model
## Model: "sequential"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  conv2d_2 (Conv2D)                  (None, 26, 26, 32)              320         
##  max_pooling2d_1 (MaxPooling2D)     (None, 13, 13, 32)              0           
##  conv2d_1 (Conv2D)                  (None, 11, 11, 64)              18496       
##  max_pooling2d (MaxPooling2D)       (None, 5, 5, 64)                0           
##  conv2d (Conv2D)                    (None, 3, 3, 64)                36928       
## ================================================================================
## Total params: 55,744
## Trainable params: 55,744
## Non-trainable params: 0
## ________________________________________________________________________________

可以看到,layer_conv_2d和layer_max_pooling_2d的输出都是(height,width,channels)的三维张量.

因为最终是分类问题,因此需要添加全连接层。即将张量(3,3,64)转变成为10分类的概率。

全连接层接受一维向量,即一张图片用一条向量表示。首先将三维输出转变成为一维,然后添加全连接层,代码如下所示。

model <- model %>% 
  layer_flatten() %>% 
  layer_dense(units = 64, activation = "relu") %>% 
  layer_dense(units = 10, activation = "softmax")

这里使用layer_flatten()将数据转换成为1维。并且添加了具有10个输出和具有softmax激活函数的最终层。

接着查看模型

(model)
## Model: "sequential"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  conv2d_2 (Conv2D)                  (None, 26, 26, 32)              320         
##  max_pooling2d_1 (MaxPooling2D)     (None, 13, 13, 32)              0           
##  conv2d_1 (Conv2D)                  (None, 11, 11, 64)              18496       
##  max_pooling2d (MaxPooling2D)       (None, 5, 5, 64)                0           
##  conv2d (Conv2D)                    (None, 3, 3, 64)                36928       
##  flatten (Flatten)                  (None, 576)                     0           
##  dense_1 (Dense)                    (None, 64)                      36928       
##  dense (Dense)                      (None, 10)                      650         
## ================================================================================
## Total params: 93,322
## Trainable params: 93,322
## Non-trainable params: 0
## ________________________________________________________________________________

从结果中可以看到,(3,3,64)转变成为了(576)。模型结构设定好之后,开始训练模型。

mnist <- dataset_mnist()
c(c(train_images, train_labels), c(test_images, test_labels)) %<-% mnist

train_images <- array_reshape(train_images, c(60000, 28, 28, 1))
train_images <- train_images / 255

test_images <- array_reshape(test_images, c(10000, 28, 28, 1))
test_images <- test_images / 255

train_labels <- to_categorical(train_labels)
test_labels <- to_categorical(test_labels)

model %>% compile(
  optimizer = "rmsprop",
  loss = "categorical_crossentropy",
  metrics = c("accuracy")
)
              
model %>% fit(
  train_images, train_labels, 
  epochs = 5, batch_size=64
)

在测试数据上评估模型

results <- model %>% evaluate(test_images, test_labels)
results
##       loss   accuracy 
## 0.02941903 0.99119997

可以看到,准确率提高了不少。为什么卷积神经网络效果好?这就需要深入理解layer_conv_2d和layer_max_pooling_2d 。

理解卷积神经网络

全连接层和卷积层的区别在于:全连接层学习的是特征空间的全局模型,对于图片而言就是所有像素点。而卷积层学习的是局部模式。

上面的代码中,kernel_size = c(3, 3),表示的就是这个局部的大小,表示3*3像素。

卷积网路有两个有趣的点:

  1. 学习的局部模式是可以迁移的,一旦学习了某个模式之后,卷积网络可以在其他任何地方识别。但是对于全连接网路,如果这个局部模式出现在不同的位置,则意味着需要重新学习该模式,因为从全局来看,位置不一样则意味着模式发生了改变。这意味着卷积神经网络处理图像效率更高,效果更好。
  2. 可以学习模型的空间层次结构。例如,第一卷积层可以学习小模式,第二卷积层学习由第一层特征构成的较大模式,等等。卷积神经网络能够有效学习复杂抽象的视觉概念。

卷积在三维张量上运算,三维张量称为特征图。特征图有两个空间轴,高和宽,以及深度轴(通道)。对于RGB图像,深度轴为长度为3,图像由三个颜色组成,红色,绿色和蓝色。对于黑白图片,深度轴长度为1。

卷积层从其输入特征图中提取小块,并将相同的变换应用于所有这些小块,从而生成输出特征图。输出结果依然是三维张量,有高度和宽度,但是深度轴长度可以是任意的,其取决于我们设置的参数,此时深度轴不再表示特饿定颜色,相反他们代表过滤器,用于对输入数据的特定方面进行编码。

在上面的例子中,第一个卷积层是(28,28,1)的特征图,输出大小为(26,26,32)的特征图。相当于32个c(26,26)的矩阵,表示该过滤器在输入中不同位置的响应(数据转换)。

model
## Model: "sequential"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  conv2d_2 (Conv2D)                  (None, 26, 26, 32)              320         
##  max_pooling2d_1 (MaxPooling2D)     (None, 13, 13, 32)              0           
##  conv2d_1 (Conv2D)                  (None, 11, 11, 64)              18496       
##  max_pooling2d (MaxPooling2D)       (None, 5, 5, 64)                0           
##  conv2d (Conv2D)                    (None, 3, 3, 64)                36928       
##  flatten (Flatten)                  (None, 576)                     0           
##  dense_1 (Dense)                    (None, 64)                      36928       
##  dense (Dense)                      (None, 10)                      650         
## ================================================================================
## Total params: 93,322
## Trainable params: 93,322
## Non-trainable params: 0
## ________________________________________________________________________________

下图显示了图形经过卷积轴的变化

定义卷积层的时候有两个关键参数:

  1. 从输入中提取块的大小(窗口),通常是 3-3或者5-5
  2. 输出特征图的深度:filters参数设置过滤器数量

卷积通过在啊三维输入特征图上滑动这些窗口,在每个可能的位置停止,提取这些小块(window-height,window-width,input-depth) ,并转换为(output-depth)一维向量。然后将所有这些一维向量在空间上重新组装称为格式(height,widthmoutput-depth)的三维输出图,如图所示。

在上面的例子中,需要注意的一点,为什么输入的长宽是(28,28),经过第一个卷积层变成了(26,26)。

这是因为我们设置的窗口是(3,3),如果窗口设置为c(5,5),那么则会变成(24,24),这种情况叫做,边界效应。

(每一个块相当于一个点,5-5的矩阵以3-3为块采样,采样9个块,9个块变成3-3的矩阵)

想要保持一致,则需要在layer_conv_2d层中将paddinng参数设置为same,模型valid,表示不填充。

输出宽度和高度于输入宽度和高度不一样除了上面这种原因(边界效应),另一个因素是步幅,两个窗口之间的距离是卷积层的一个参数,称为步幅,默认是1。

3-3卷积小块,2步幅。5-5的输入则变成了2-2

通常不会修改步幅。

最大池化操作

在上面的例子中,每一个layer_max_pooling_2d之后,特征图大小减半。

model
## Model: "sequential"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  conv2d_2 (Conv2D)                  (None, 26, 26, 32)              320         
##  max_pooling2d_1 (MaxPooling2D)     (None, 13, 13, 32)              0           
##  conv2d_1 (Conv2D)                  (None, 11, 11, 64)              18496       
##  max_pooling2d (MaxPooling2D)       (None, 5, 5, 64)                0           
##  conv2d (Conv2D)                    (None, 3, 3, 64)                36928       
##  flatten (Flatten)                  (None, 576)                     0           
##  dense_1 (Dense)                    (None, 64)                      36928       
##  dense (Dense)                      (None, 10)                      650         
## ================================================================================
## Total params: 93,322
## Trainable params: 93,322
## Non-trainable params: 0
## ________________________________________________________________________________

最大池化的作用是对特征图进行下采样。最大池化从输入特征图提取窗口,然后输出每个通道最大值。他在概念上于卷积层类似,最大池化通过2-2窗口和步幅2完成转换,以便完成下采样。

我们可以看不添加最大池化层,查看模型结构。

model <- keras_model_sequential() %>% 
  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu",
                input_shape = c(28, 28, 1)) %>% 
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_flatten() %>% 
  layer_dense(units = 64, activation = "relu") %>% 
  layer_dense(units = 10, activation = "softmax")

model
## Model: "sequential_1"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  conv2d_5 (Conv2D)                  (None, 26, 26, 32)              320         
##  conv2d_4 (Conv2D)                  (None, 24, 24, 64)              18496       
##  conv2d_3 (Conv2D)                  (None, 22, 22, 64)              36928       
##  flatten_1 (Flatten)                (None, 30976)                   0           
##  dense_3 (Dense)                    (None, 64)                      1982528     
##  dense_2 (Dense)                    (None, 10)                      650         
## ================================================================================
## Total params: 2,038,922
## Trainable params: 2,038,922
## Non-trainable params: 0
## ________________________________________________________________________________
k_clear_session()

我们可以对比一下添加了最大池化层和不添加之间参数量的区别,分别是93,322和2,038,922。

也就是说,之所以这么做是为了减少模型的参数。另外,最大池化不是实现类似下采样的唯一方法,例如我们可以修改步幅,使用平均池化层。但是通常而言,最大池化是一个更好的选择。

从头构建卷积网络

通常而言,深度学习需要大量数据,深度学习能够在训练数据集中找到有效特征,而不需要手动搜寻特征,这只有在大量训练样本的时候才能实现。

只用几十个样本可能很难训练出解决复杂问题的卷积网络,但是如果模型很小并且经过良好的正则化,并且任务简单,样本量也不需要很多。尽管缺少数据,但是无需任何特征工程,即使在非常小的图像数据集从头开始i训练,荏苒可以产生合理的结果。

另外,深度学习模型是可再利用的,例如你可以采用大规模数据上训练的图像分类或者语音,文本模型,然后将其应用到一个新的问题。

下载预训练模型的资源包括:

  1. Model Zoo-https://modelzoo.co/
  2. TensorFlow — Models & datasets :https://www.tensorflow.org/resources/models-datasets
  3. PyTorch Hub- https://pytorch.org/hub/
  4. Papers with Code:https://paperswithcode.com/
  5. Hugging Face 🤗-https://huggingface.co/

在这个例子中,我们使用kaggle中的一个猫狗图片数据集,数据连接。https://www.kaggle.com/competitions/dogs-vs-cats/data

数据集包含25000猫狗图片。我们创建三个新数据集,每个类别包含1000个样本的训练集和每个类别500个样本的验证集,以及每个了别500个样本的测试集。

一下是构造我们要用的数据集。

original_dataset_dir <- "/Users/milin/Keras2021/dogs-vs-cats/train" # 需要设置为绝对路径

base_dir <- "/Users/milin/Keras2021/cats_and_dogs_small_dataset"
dir.create(base_dir)
## Warning in dir.create(base_dir): '/Users/milin/Keras2021/
## cats_and_dogs_small_dataset' already exists
train_dir <- file.path(base_dir, "train")
dir.create(train_dir)
## Warning in dir.create(train_dir): '/Users/milin/Keras2021/
## cats_and_dogs_small_dataset/train' already exists
validation_dir <- file.path(base_dir, "validation")
dir.create(validation_dir)
## Warning in dir.create(validation_dir): '/Users/milin/Keras2021/
## cats_and_dogs_small_dataset/validation' already exists
test_dir <- file.path(base_dir, "test")
dir.create(test_dir)
## Warning in dir.create(test_dir): '/Users/milin/Keras2021/
## cats_and_dogs_small_dataset/test' already exists
train_cats_dir <- file.path(train_dir, "cats")
dir.create(train_cats_dir)
## Warning in dir.create(train_cats_dir): '/Users/milin/Keras2021/
## cats_and_dogs_small_dataset/train/cats' already exists
train_dogs_dir <- file.path(train_dir, "dogs")
dir.create(train_dogs_dir)
## Warning in dir.create(train_dogs_dir): '/Users/milin/Keras2021/
## cats_and_dogs_small_dataset/train/dogs' already exists
validation_cats_dir <- file.path(validation_dir, "cats")
dir.create(validation_cats_dir)
## Warning in dir.create(validation_cats_dir): '/Users/milin/Keras2021/
## cats_and_dogs_small_dataset/validation/cats' already exists
validation_dogs_dir <- file.path(validation_dir, "dogs")
dir.create(validation_dogs_dir)
## Warning in dir.create(validation_dogs_dir): '/Users/milin/Keras2021/
## cats_and_dogs_small_dataset/validation/dogs' already exists
test_cats_dir <- file.path(test_dir, "cats")
dir.create(test_cats_dir)
## Warning in dir.create(test_cats_dir): '/Users/milin/Keras2021/
## cats_and_dogs_small_dataset/test/cats' already exists
test_dogs_dir <- file.path(test_dir, "dogs")
dir.create(test_dogs_dir)
## Warning in dir.create(test_dogs_dir): '/Users/milin/Keras2021/
## cats_and_dogs_small_dataset/test/dogs' already exists

复制图片

fnames <- paste0("cat.", 1:1000, ".jpg")
file.copy(file.path(original_dataset_dir, fnames), 
          file.path(train_cats_dir)) 

fnames <- paste0("cat.", 1001:1500, ".jpg")
file.copy(file.path(original_dataset_dir, fnames), 
          file.path(validation_cats_dir))

fnames <- paste0("cat.", 1501:2000, ".jpg")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(test_cats_dir))

fnames <- paste0("dog.", 1:1000, ".jpg")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(train_dogs_dir))

fnames <- paste0("dog.", 1001:1500, ".jpg")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(validation_dogs_dir)) 

fnames <- paste0("dog.", 1501:2000, ".jpg")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(test_dogs_dir))

我们新的数据集中包含了2000张训练数据,1000验证数据集,1000测试数据。

cat("total training cat images:", length(list.files(train_cats_dir)), "\n")
## total training cat images: 1000
cat("total training dog images:", length(list.files(train_dogs_dir)), "\n")
## total training dog images: 1000
cat("total validation cat images:", length(list.files(validation_cats_dir)), "\n")
## total validation cat images: 500
cat("total validation dog images:", length(list.files(validation_dogs_dir)), "\n")
## total validation dog images: 500
cat("total test cat images:", length(list.files(test_cats_dir)), "\n")
## total test cat images: 500
 cat("total test dog images:", length(list.files(test_dogs_dir)), "\n")
## total test dog images: 500

构建网络

卷积网络是交替layer_conv_2d()和layer_max_pooling_2d()来进行构建的。

随着问题的复杂,网络也会变得复杂,如何构建网络,没有技巧,需要多尝试。

library(keras)

model <- keras_model_sequential() %>% 
  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu",
                input_shape = c(150, 150, 3)) %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_flatten() %>% 
  layer_dense(units = 512, activation = "relu") %>% 
  layer_dense(units = 1, activation = "sigmoid")

我们查看网络结构

 (model)
## Model: "sequential"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  conv2d_3 (Conv2D)                  (None, 148, 148, 32)            896         
##  max_pooling2d_3 (MaxPooling2D)     (None, 74, 74, 32)              0           
##  conv2d_2 (Conv2D)                  (None, 72, 72, 64)              18496       
##  max_pooling2d_2 (MaxPooling2D)     (None, 36, 36, 64)              0           
##  conv2d_1 (Conv2D)                  (None, 34, 34, 128)             73856       
##  max_pooling2d_1 (MaxPooling2D)     (None, 17, 17, 128)             0           
##  conv2d (Conv2D)                    (None, 15, 15, 128)             147584      
##  max_pooling2d (MaxPooling2D)       (None, 7, 7, 128)               0           
##  flatten (Flatten)                  (None, 6272)                    0           
##  dense_1 (Dense)                    (None, 512)                     3211776     
##  dense (Dense)                      (None, 1)                       513         
## ================================================================================
## Total params: 3,453,121
## Trainable params: 3,453,121
## Non-trainable params: 0
## ________________________________________________________________________________

通常特征图的深度会逐渐增加,而特征图的高宽则会降低,所有卷积网络都有这样的特征。

构建好网络之后需要进行编译,依然使用RMSprop优化器,最后使用了sigmoid激活函数结果网络,并且是二分类问题,使用交叉墒来作为损失函数。

model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(learning_rate = 1e-4),
  metrics = c("acc")
)

数据预处理

图片数据是以JPEG文件存储起来的,我们需要将数据向量化。步骤如下:

  1. 读取图片温江
  2. 将JPEG内容解码为RGB像素文件
  3. 将这些像素文件转变成为张量
  4. 将像素值0-255 缩放到0-1

keras 有许多图片处理的工具,这里可以使用image_data_generator函数直接对图片进行处理,将图片直接转变成为张量。

# All images will be rescaled by 1/255
train_datagen <- image_data_generator(rescale = 1/255)
validation_datagen <- image_data_generator(rescale = 1/255)

train_generator <- flow_images_from_directory(
  # This is the target directory
  train_dir,
  # This is the data generator
  train_datagen, # 训练数据生成器
  # All images will be resized to 150x150
  target_size = c(150, 150),
  batch_size = 20,
  # Since we use binary_crossentropy loss, we need binary labels
  class_mode = "binary"
)

validation_generator <- flow_images_from_directory(
  validation_dir,
  validation_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

这里创建的关于数据的生成器,其产生150-150的RGP图像和二分类标签。

可以使用generator_next函数生成数据。

for (i in 1:4) {
  batch <- generator_next(train_generator)
  plot(as.raster(batch[[1]][1,,,]))
}

接着使用fit_generator/fit拟合模型。

history <- model %>% fit(
  train_generator,
  steps_per_epoch = 100,# 
  epochs = 5, # 30
  validation_data = validation_generator,
  validation_steps = 50
)

由于每次生成20个样本,2000个样本全部训练完需要100次,steps_per_epoch = 100。validation_steps = 50表示从验证生成器中抽取50次数据进行评估

保存模型。

model %>% save_model_hdf5(“cats_and_dogs_small_1.h5”)

对训练期间的损失和准确率进行可视化。

plot(history)

由于数据量比较少我们可以尝试一些方法结果过拟合问题。

将现有数据进行拓展

我们可以尝试对现有的图片进行转变,从而生成新的图形。image_data_generator可以读取数据并且进行一些随机变换。

datagen <- image_data_generator(
  rescale = 1/255,
  rotation_range = 40,
  width_shift_range = 0.2,
  height_shift_range = 0.2,
  shear_range = 0.2,
  zoom_range = 0.2,
  horizontal_flip = TRUE,
  fill_mode = "nearest"
)
  1. rotation_range 表示随机旋转图片的范围
  2. width_shift_range/height_shift_range 表示随机评移图片的范围
  3. shear_range 表示随机剪切
  4. zoom_range 表示随机缩放
  5. horizontal_flip 随机反转
  6. fill_mode 填充创建新像素的策略,nearest表示就近原则。 跟多选项需要查看文档。

我们查看增强之后的图像。

# We pick one image to "augment"
fnames <- list.files(train_cats_dir, full.names = TRUE)
img_path <- fnames[[10]]

# Convert it to an array with shape (150, 150, 3)
img <- image_load(img_path, target_size = c(150, 150))
img_array <- image_to_array(img)
img_array <- array_reshape(img_array, c(1, 150, 150, 3))

# Generated that will flow augmented images
augmentation_generator <- flow_images_from_data(
  img_array, 
  generator = datagen, 
  batch_size = 1 
)

# Plot the first 4 augmented images
op <- par(mfrow = c(2, 2), pty = "s", mar = c(1, 0, 1, 0))
for (i in 1:4) {
  batch <- generator_next(augmentation_generator)
  plot(as.raster(batch[1,,,]))
}

par(op)

使用这种方法创建图形,则可以创建任意多不相同的图片,但是这些图片依然是高度相关的。不足以完全摆脱过拟合。

进一步尝试添加dropout层。

model <- keras_model_sequential() %>% 
  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu",
                input_shape = c(150, 150, 3)) %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_flatten() %>% 
  layer_dropout(rate = 0.5) %>% 
  layer_dense(units = 512, activation = "relu") %>% 
  layer_dense(units = 1, activation = "sigmoid")  
  
model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(learning_rate = 1e-4),
  metrics = c("acc")
)

使用数据扩充和数据丢弃来训练模型。

datagen <- image_data_generator(
  rescale = 1/255,
  rotation_range = 40,
  width_shift_range = 0.2,
  height_shift_range = 0.2,
  shear_range = 0.2,
  zoom_range = 0.2,
  horizontal_flip = TRUE
)

test_datagen <- image_data_generator(rescale = 1/255)

train_generator <- flow_images_from_directory(
  train_dir,
  datagen,
  target_size = c(150, 150),
  batch_size = 32,
  class_mode = "binary"
)

validation_generator <- flow_images_from_directory(
  validation_dir,
  test_datagen,
  target_size = c(150, 150),
  batch_size = 32,
  class_mode = "binary"
)

history <- model %>% fit_generator(
  train_generator,
  steps_per_epoch = 100,
  epochs = 100,
  validation_data = validation_generator,
  validation_steps = 50
)
## Warning in fit_generator(., train_generator, steps_per_epoch = 100, epochs
## = 100, : `fit_generator` is deprecated. Use `fit` instead, it now accept
## generators.

保存模型

model %>% save_model_hdf5("cats_and_dogs_small_2.h5")

可视化模型解雇欧

plot(history)

进一步我们还可以设置正则化。虽然我们可以做很多措施,但是由于数据量有限,那么模型效果的上限是比较低的。为了进一步提高准确率,我们可以使用预训练模型。

使用预训练的模型

预先训练的模型是先在大型数据集上训练的已经保存的模型,如果原始数据足够大,那么预训练模型的层次结构可以有效的充当一个通用结构,即使新问题与原始问题完全不一样。

Keras中有许多图像分类预先训练模型,包括:

  • Xception
  • InceptionV3
  • ResNet50
  • VGG16
  • VGG19
  • MobileNet

在这里我们使用vgg16,其在ImageNet数据集上训练的大型卷积网络(140万个图片和1000个不同类)。使用application_vgg16函数获取预训练模型。

其他预训练模型也是以application_开头的函数。

library(keras)

conv_base <- application_vgg16(
  weights = "imagenet",
  include_top = FALSE,
  input_shape = c(150, 150, 3)
)

通常有三个参数: 1. weights模型权重,imagenet表示预训练模型的权重,NULL表示随机权重 2. include_top :是否包含最终的全连接层,如果包含则对应1000分类。很明显,我们应该选择不包含。 3. input_shape:输入格式。如果不设置,则接受任意张量。

查看预训练模型结构

(conv_base)
## Model: "vgg16"
## ________________________________________________________________________________
##  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           
## ================================================================================
## Total params: 14,714,688
## Trainable params: 14,714,688
## Non-trainable params: 0
## ________________________________________________________________________________

最终层的结果是4-4-512,我们需要添加一个全连接层,作为分类器。

有两种方式实现:

  1. 在数据集上运行预训练模型,然后将输出结果独应用到全连接层的。
  2. 在预模型最后添加全连接层形成新的模型。

我们分别来看。

使用预训练模型预测

base_dir <- "/Users/milin/Keras2021/cats_and_dogs_small_dataset"
train_dir <- file.path(base_dir, "train")
validation_dir <- file.path(base_dir, "validation")
test_dir <- file.path(base_dir, "test")

datagen <- image_data_generator(rescale = 1/255)
batch_size <- 20

extract_features <- function(directory, sample_count) {
  
  features <- array(0, dim = c(sample_count, 4, 4, 512))  
  labels <- array(0, dim = c(sample_count))
  
  generator <- flow_images_from_directory(
    directory = directory,
    generator = datagen,
    target_size = c(150, 150),
    batch_size = batch_size,
    class_mode = "binary"
  )
  
  i <- 0
  while(TRUE) {
    batch <- generator_next(generator)
    inputs_batch <- batch[[1]]
    labels_batch <- batch[[2]]
    features_batch <- conv_base %>% predict(inputs_batch)
    
    index_range <- ((i * batch_size)+1):((i + 1) * batch_size)
    features[index_range,,,] <- features_batch
    labels[index_range] <- labels_batch
    
    i <- i + 1
    if (i * batch_size >= sample_count)
      # Note that because generators yield data indefinitely in a loop, 
      # you must break after every image has been seen once.
      break
  }
  
  list(
    features = features, 
    labels = labels
  )
}

train <- extract_features(train_dir, 2000)
validation <- extract_features(validation_dir, 1000)
test <- extract_features(test_dir, 1000)

结果的格式是(sample,4,4,512),我们还需要将其展开,变成(sample,8192)

reshape_features <- function(features) {
  array_reshape(features, dim = c(nrow(features), 4 * 4 * 512))
}
train$features <- reshape_features(train$features)
validation$features <- reshape_features(validation$features)
test$features <- reshape_features(test$features)

最后定义全连接层构建的分类器

model <- keras_model_sequential() %>% 
  layer_dense(units = 256, activation = "relu", 
              input_shape = 4 * 4 * 512) %>% 
  layer_dropout(rate = 0.5) %>% 
  layer_dense(units = 1, activation = "sigmoid")

model %>% compile(
  optimizer = optimizer_rmsprop(lr = 2e-5),
  loss = "binary_crossentropy",
  metrics = c("accuracy")
)
## Warning in backcompat_fix_rename_lr_to_learning_rate(...): the `lr` argument has
## been renamed to `learning_rate`.
history <- model %>% fit(
  train$features, train$labels,
  epochs = 30,
  batch_size = 20,
  validation_data = list(validation$features, validation$labels)
)

这种方式的好处是非常快。

plot(history)

保存模型

model %>% save_model_hdf5("cats_and_dogs_small_preprocess.h5")

将预模型添加层

这种方式非常慢

model <- keras_model_sequential() %>% 
  conv_base %>% 
  layer_flatten() %>% 
  layer_dense(units = 256, activation = "relu") %>% 
  layer_dense(units = 1, activation = "sigmoid")

查看模型

 (model)

可以看到,参数非常多。在编译模型之前,我们需要冻结预训练模型,如果不冻结,那么预训练模型的参数会更新,从而破坏先前的学习。

使用freeze_weights函数冻结参数。

freeze_weights(conv_base)

冻结之后,只会添加全连接层的权重,

接着,我们使用上文同样的数据训练模型。

train_datagen = image_data_generator(
  rescale = 1/255,
  rotation_range = 40,
  width_shift_range = 0.2,
  height_shift_range = 0.2,
  shear_range = 0.2,
  zoom_range = 0.2,
  horizontal_flip = TRUE,
  fill_mode = "nearest"
)

test_datagen <- image_data_generator(rescale = 1/255)

train_generator <- flow_images_from_directory(
  train_dir,
  train_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

validation_generator <- flow_images_from_directory(
  validation_dir,
  test_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 2e-5),
  metrics = c("accuracy")
)

history <- model %>% fit_generator(
  train_generator,
  steps_per_epoch = 100,
  epochs = 30,
  validation_data = validation_generator,
  validation_steps = 50
)

save_model_hdf5(model, "cats_and_dogs_small_3.h5")

可视化

plot(history)

微调

对冻结后的某些层进行调整就是微调。步骤如下:

  1. 在预训练的模型中添加了自定义网络
  2. 冻结预训练的网络
  3. 训练添加的网络
  4. 解冻某些层
  5. 重新训练

我们之前的操作已经完成了前三步,接下来我们需要进行第四部。

summary(conv_base)

微调block3_conv1之后的所有层。吐过参数是to则是之前的所有层。之所以调整考后的层是因为,早期的层更加通用,并且避免调整过多参数。

unfreeze_weights(conv_base, from = "block3_conv1")

接下来就可以重新训练模型。

model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 1e-5),
  metrics = c("accuracy")
)

history <- model %>% fit_generator(
  train_generator,
  steps_per_epoch = 100,
  epochs = 100,
  validation_data = validation_generator,
  validation_steps = 50
)

保存模型

save_model_hdf5(model, "cats_and_dogs_small_4.h5")

可视化结果

plot(history)

评估模型结果

test_generator <- flow_images_from_directory(
  test_dir,
  test_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

model %>% evaluate_generator(test_generator, steps = 50)

可视化中间信号输出

可视化中间信号输出可以帮助我们了解图片是如何一步一步被抽象出不同特征的。 首先加载上文中构建的模型。

library(keras)

model <- load_model_hdf5("/Users/milin/Keras2021/cats_and_dogs_small_2.h5")
(model)  # As a reminder.
## Model: "sequential_5"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  conv2d_7 (Conv2D)                  (None, 148, 148, 32)            896         
##  max_pooling2d_7 (MaxPooling2D)     (None, 74, 74, 32)              0           
##  conv2d_6 (Conv2D)                  (None, 72, 72, 64)              18496       
##  max_pooling2d_6 (MaxPooling2D)     (None, 36, 36, 64)              0           
##  conv2d_5 (Conv2D)                  (None, 34, 34, 128)             73856       
##  max_pooling2d_5 (MaxPooling2D)     (None, 17, 17, 128)             0           
##  conv2d_4 (Conv2D)                  (None, 15, 15, 128)             147584      
##  max_pooling2d_4 (MaxPooling2D)     (None, 7, 7, 128)               0           
##  flatten_2 (Flatten)                (None, 6272)                    0           
##  dropout_3 (Dropout)                (None, 6272)                    0           
##  dense_11 (Dense)                   (None, 512)                     3211776     
##  dense_10 (Dense)                   (None, 1)                       513         
## ================================================================================
## Total params: 3,453,121
## Trainable params: 3,453,121
## Non-trainable params: 0
## ________________________________________________________________________________

接下来我们获取一张图片。

img_path <- "/Users/milin/Keras2021/cats_and_dogs_small_dataset/test/cats/cat.1666.jpg"


# We preprocess the image into a 4D tensor
img <- image_load(img_path, target_size = c(150, 150))
img_tensor <- image_to_array(img)
img_tensor <- array_reshape(img_tensor, c(1, 150, 150, 3))

# Remember that the model was trained on inputs
# that were preprocessed in the following way:
img_tensor <- img_tensor / 255

dim(img_tensor)
## [1]   1 150 150   3

显示图片

plot(as.raster(img_tensor[1,,,]))

去除全连接层

# Extracts the outputs of the top 8 layers:
layer_outputs <- lapply(model$layers[1:8], function(layer) layer$output)
# Creates a model that will return these outputs, given the model input:
activation_model <- keras_model(inputs = model$input, outputs = layer_outputs)

使用修改之后的模型进行预测

# Returns a list of five arrays: one array per layer activation
activations <- activation_model %>% predict(img_tensor)

查看第一层的结果

first_layer_activation <- activations[[1]]
dim(first_layer_activation)
## [1]   1 148 148  32

我们这里只有1章图片,是148-148的特征图,有32个通道。接下来我们可视化通道。

plot_channel <- function(channel) {
  rotate <- function(x) t(apply(x, 2, rev))
  image(rotate(channel), axes = FALSE, asp = 1, 
        col = terrain.colors(12))
}
plot_channel(first_layer_activation[1,,,5]) # 第5个通道

查看第20个通道

plot_channel(first_layer_activation[1,,,20])

同样我们可以查看第二层的不同通道,

second_layer_activation <- activations[[1]]

plot_channel(second_layer_activation[1,,,20])

有几点需要注意

  1. 第一层保留的信息最多,并且随着层数增加,中间信号会变得越来越抽象,开始表示更高层次的概念,例如猫鼻子,猫耳朵。
  2. 虽则层数增加,中间信号会变得悉数,因为某些过滤器可能是空白的

5.序列数据问题

6 其他深度学习模型

7.生成模型

8. 结束也是开始

参看其他文档