TensorFlow – 例子:卷积神经网络(CNN)

卷积神经网络(CNN)旨在解决图像识别问题,卷积神经网络在图像识别、机器视觉等方面有着广泛的应用。

计算机中的图像格式

这张杭州天际线的图片,可以看到很多建筑和颜色。那么计算机是如何处理这幅图像的呢?

图像由像素组成,像素表示图像中的一个颜色点。例如,一个40×30的图像,表示宽40个像素,高30个像素,总共有1200个像素。

根据色彩学原理,任何颜色都可由红、绿、蓝三种颜色混合组成,一个像素点被分成红、绿、蓝三种颜色分量,由这三个分量组合来表示任意颜色。

图

对于黑白图像,只有一个分量,表示灰度。

什么是卷积神经网络?

卷积神经网络和神经网络一样,是由具有可学习权值和偏差的神经元组成的。每个神经元接收几个输入,对它们进行加权求和,然后通过一个激活函数进行传递,经过多层神经元,最终输出结果。

整个卷积神经网络有一个损失函数(代价函数/目标函数),我们为神经网络开发的所有技巧仍然适用于卷积神经网络。

如前所述,图像实际上是由像素组成的二维矩阵,卷积神经网络所做的工作就是使用卷积、池化等操作从二维数组中提取特征,然后对图像进行识别。

卷积神经网络的历史

1962年Hubel和Wiesel通过对猫视觉皮层细胞的研究,提出了感受野(receptive field)的概念,1984年日本学者Fukushima基于感受野概念提出的神经认知机(neocognitron)可以看作是卷积神经网络的第一个实现网络,也是感受野概念在人工神经网络领域的首次应用。神经认知机将一个视觉模式分解成许多子模式(特征),然后进入分层递阶式相连的特征平面进行处理,它试图将视觉系统模型化,使其能够在即使物体有位移或轻微变形的时候,也能完成识别。

图

通常神经认知机包含两类神经元,即承担特征抽取的S-元和抗变形的C-元。S-元中涉及两个重要参数,即感受野与阈值参数,前者确定输入连接的数目,后者则控制对特征子模式的反应程度。许多学者一直致力于提高神经认知机的性能的研究:在传统的神经认知机中,每个S-元的感光区中由C-元带来的视觉模糊量呈正态分布。如果感光区的边缘所产生的模糊效果要比中央来得大,S-元将会接受这种非正态模糊所导致的更大的变形容忍性。我们希望得到的是,训练模式与变形刺激模式在感受野的边缘与其中心所产生的效果之间的差异变得越来越大。为了有效地形成这种非正态模糊,Fukushima提出了带双C-元层的改进型神经认知机。

Van Ooyen和Niehuis为提高神经认知机的区别能力引入了一个新的参数。事实上,该参数作为一种抑制信号,抑制了神经元对重复激励特征的激励。多数神经网络在权值中记忆训练信息。根据Hebb学习规则,某种特征训练的次数越多,在以后的识别过程中就越容易被检测。也有学者将进化计算理论与神经认知机结合,通过减弱对重复性激励特征的训练学习,而使得网络注意那些不同的特征以助于提高区分能力。上述都是神经认知机的发展过程,而卷积神经网络可看作是神经认知机的推广形式,神经认知机是卷积神经网络的一种特例。

为什么要使用卷积呢?

图像数据对于传统的全连接神经网络过于巨大。假如有一幅1000*1000的图像,如果把整幅图像作为向量,则向量的长度为1000000(10^6)。如果隐含层神经元的个数和输入一样,也是1000000;那么,输入层到隐含层的参数数据量有 10^{12},数量过于巨大,无法处理。

卷积神经网络使用卷积提取图像特征,压缩数据量,然后使用全连接网络进行图像识别处理。

卷积神经网络结构

卷积神经网络层次结构如下

卷积神经网络

主要有4层:

  • 卷积
  • 激活函数ReLu
  • 池化
  • 全连接层

注意: 关于卷积神经网络的详细内容,可参考我们的教程:深度学习 – 卷积神经网络(CNN)

TensorFlow实现CNN

接下来,我们将使用TensorFlow来实现CNN,数据集使用MNIST。

数据准备部分与前面的TensorFlow – 例子:人工神经网络(ANN)教程相同,如果已经看过,可略过。

TensorFlow实现CNN步骤如下:

  1. 准备数据集
  2. 输入层
  3. 卷积层
  4. 池化层
  5. 第二卷积层和池化层
  6. 全连接层
  7. Logit层

1. 准备数据集

1.1 导入数据

首先需要导入必要的库,除了TensorFlow,我们将使用:

  • numpy 计算、处理多维数组的python包
  • sklearn 机器学习相关的包,包含许多有用的函数

sklearn可用于导入MNIST数据集,预处理数据等。

import numpy as np
import tensorflow as tf

openml.org是一个分享机器学习数据和实验的公共存储库,每个人都可在上面分享、下载数据集。sklearn.datasets包中的fetch_openml函数可以从openml存储库下载数据集。我们将使用该函数从openml.org下载MNIST数据集,下载会花几分钟时间。

MNIST数据集中包含了样本特征集mnist.data及样本标签mnist.target

# 导入sklearn库中fetch_openml函数,下载MNIST数据集
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784')
print(mnist.keys())
print(mnist.data.shape)
print(mnist.target.shape)

使用train_test_split函数将数据集随机划分为训练子集和测试子集,并返回划分好的训练集测试集样本和训练集测试集标签。

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(mnist.data, mnist.target, test_size=0.2, random_state=42)
y_train  = y_train.astype(int)
y_test  = y_test.astype(int)
batch_size = len(X_train)

# 查看训练样本子集、训练样本标签子集、测试样本子集、测试样本标签子集的形状
print(X_train.shape, y_train.shape, X_test.shape, y_test.shape)

train_test_split函数的参数解释:

  • train_data:被划分的样本特征集
  • train_target:被划分的样本标签
  • test_size:如果是浮点数,在0-1之间,表示测试子集占比;如果是整数的话就是测试子集的样本数量
  • random_state:是随机数的种子

随机数种子

随机数的产生取决于种子,随机数和种子之间的关系遵从以下两个规则:
种子不同,产生不同的随机数;种子相同,即使实例不同也产生相同的随机数。

随机数种子,其实就是该组随机数的编号,在需要重复试验的时候,种子相同可以保证得到一组一样的随机数。比如你每次都填1,其他参数一样的情况下,得到的随机数组是一样的,但填0或不填,则每次都会不一样。

1.2 数据预处理

在进行训练之前,需要对数据集作归一化处理,可以提高模型收敛速度和模型精度。我们将使用最小最大值标准化方法,该方法的公式是:

(X-min_x)/(max_x - min_x)

sklearn库中已经为此提供了一个函数: MinMaxScaler()

## 导入MinMaxScaler
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
# 训练样本子集
X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
# 测试样本子集
X_test_scaled = scaler.fit_transform(X_test.astype(np.float64))

1.3 构造张量

构造输入特征列张量。TensorFlow中的特征列可以视为原始数据和 Estimator 之间的媒介。特征列功能强大,可以将各种原始数据转换为 Estimator 可以使用的格式。

feature_columns = [tf.feature_column.numeric_column('x', shape = X_train_scaled.shape[1:])]

2. 输入层

神经网络的卷积部分,我们将定义在一个cnn_model_fn函数里,包含了前面提到过的下面这些层:

  • 输入层
  • 卷积层
  • 池化层
  • 第二卷积层和池化层
  • 全连接层
  • Logit层

我们先从输入层开始,输入层接受数据输入。

def cnn_model_fn(features, labels, mode):
    # 输入层
    input_layer = tf.reshape(tensor = features["x"], shape = [-1, 28, 28, 1])

我们需要定义一个张量input_layer,接受数据输入。

features["x"]是样本数据,需要把样本数据重新组织,调整形状。tf.reshape函数根据shape改变features["x"]的形状,返回一个新的张量。

图片有高、宽和RGB通道。MNIST数据集是单一通道的图片,即黑白图片,尺寸为28像素x28像素,每个像素一个字节,所以一个图片字节大小是28x28x1。shape = [-1, 28, 28, 1]的意义是把输入的图像数据features["x"],划分成单个图片大小的数组。

在shape参数中,我们将批量大小(batch size)设置为-1,以便它采用features["x"]的形状。其优点是使批量大小参数可以调优。如果批量大小设置为7,那么张量将提供5488个值(28x28x7)。

3. 卷积层

# 第1个卷积层
conv1 = tf.layers.conv2d(
    inputs=input_layer,
    filters=14,
    kernel_size=[5, 5],
    padding="same",
    activation=tf.nn.relu)

第一个卷积层有14个过滤器,过滤器核大小为5×5,padding相同。相同的padding意味着输出张量和输入张量的高度和宽度相同。Tensorflow将向行和列添加零,以确保大小相同。

使用Relu激活函数。输出大小将为[28,28,14]。

4. 池化层

卷积层之后的下一步是池化计算。池化计算将降低数据的维数。您可以使用max_pooling2d模块,其大小为2×2,步长为2。使用上一层作为输入。输出大小将为 [batch_size, 14, 14, 14]

# 第1个池化层
pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)

5. 第二个卷积层和池化层

第二个卷积层有32个过滤器,输出大小为[batch_size, 14,14, 32]。池化层的大小与之前相同,输出形状为[batch_size, 14, 14, 18]

# 第2个卷积层
conv2 = tf.layers.conv2d(
      inputs=pool1,
      filters=36,
      kernel_size=[5, 5],
      padding="same",
      activation=tf.nn.relu)

# 第2个池化层
pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)

6. 全连接层/Dense层

前面的几层都是在提取图像特征,接下来要把提取到的特征图(feature map),传递给全连接层,进行分类。特征图在与全连接层连接之前,必须先进行平整化处理。

全连接层有1764个神经元,采用Relu激活函数。此外,还添加了一个dropout率为0.3的正则化项,这意味着权重的30%将被设置为0。注意,dropout只发生在训练阶段。函数cnn_model_fn有一个参数mode来区分是训练或评估。

# 平整化处理,与全连接层的神经元数量匹配
pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 36])

# 1764个神经元,激活函数relu
dense = tf.layers.dense(inputs=pool2_flat, units=7 * 7 * 36, activation=tf.nn.relu)

# 正则化设置,防止过拟合
dropout = tf.layers.dropout(
      inputs=dense, rate=0.3, training=mode == tf.estimator.ModeKeys.TRAIN)

7. Logits层

最后,我们需要Logits层,它将获取完全连接层的输出,然后生成最终预测值。例如,在数字分类的情况下,输出将是一个10个值的张量,其中每个值表示一个分类(0到9)的分数。因此,让我们为数字分类示例定义这个logit层,其中我们只需要10个输出,并使用线性激活,线性激活是TensorFlow的dense()函数的默认值:

# Logits层
logits = tf.layers.dense(inputs=dropout, units=10)

logits层得到的是一个包括各个分类分数值的张量,我们可以使用这个值计算分类以及可能性:

predictions = {             
    # 生成预测              
    "classes": tf.argmax(input=logits, axis=1),             
    "probabilities": tf.nn.softmax(logits, name="softmax_tensor")  }    
  • tf.argmax() 返回参数中的最大值,即把张量中最大值的分类作为最终分类
  • tf.nn.softmax() 返回各个分类的可能性

当网络是在预测模式时,至此我们就可以直接返回了,下面的代码返回并显示预测值。

if mode == tf.estimator.ModeKeys.PREDICT:
    return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

当网络是在训练模式时,需要计算损失,进行优化计算(根据梯度下降算法和反向传播算法,不断调整权重,求取最小损失值),完成训练。

模型的偏差计算如下:

# 计算偏差
loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits)

最后一步是优化模型,即找到权重的最优值,完成训练。为此,使用学习率为0.001的梯度下降优化器。我们的目标是把损失降到最低。

# 优化模型
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
train_op = optimizer.minimize(
        loss=loss,
        global_step=tf.train.get_global_step())

我们已经完成了CNN构建,下面的代码在评估模式时可以显示性能指标(准确度)。

# 显示评估时的性能指标(准确度)
eval_metric_ops = {
      "accuracy": tf.metrics.accuracy(labels=labels, predictions=predictions["classes"])}
return tf.estimator.EstimatorSpec(mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)

下面是cnn_model_fn函数的代码:

def cnn_model_fn(features, labels, mode):
  """Model function for CNN."""
  # 输入层
  input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])

  # 第1个卷积层
  conv1 = tf.layers.conv2d(
      inputs=input_layer,
      filters=32,
      kernel_size=[5, 5],
      padding="same",
      activation=tf.nn.relu)

  # 第1个池化层
  pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)

  # # 第2个卷积层
  conv2 = tf.layers.conv2d(
      inputs=pool1,
      filters=36,
      kernel_size=[5, 5],
      padding="same",
      activation=tf.nn.relu)
  # 第2个池化层
  pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)

  # 全连接层/Dense层

  # 平整化处理,与全连接层的神经元数量匹配
  pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 36]) 
  # 1764个神经元,激活函数relu
  dense = tf.layers.dense(inputs=pool2_flat, units=7 * 7 * 36, activation=tf.nn.relu) 
  # 正则化设置,防止过拟合
  dropout = tf.layers.dropout(
      inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)

  # Logits层
  logits = tf.layers.dense(inputs=dropout, units=10)

  predictions = {
      # 生成预测
      "classes": tf.argmax(input=logits, axis=1),
      "probabilities": tf.nn.softmax(logits, name="softmax_tensor")
  }

  # 返回并显示预测值
  if mode == tf.estimator.ModeKeys.PREDICT:
    return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

  # 计算偏差
  loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits)

  # # 优化模型
  if mode == tf.estimator.ModeKeys.TRAIN:
    optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
    train_op = optimizer.minimize(
        loss=loss,
        global_step=tf.train.get_global_step())
    return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op)

  # 显示评估时的性能指标(准确度)
  eval_metric_ops = {
      "accuracy": tf.metrics.accuracy(
          labels=labels, predictions=predictions["classes"])}
  return tf.estimator.EstimatorSpec(
      mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)

8. 训练和评估模型

TensorFlow中,Estimator是一种可极大地简化机器学习编程的高阶API。Estimator 会封装下列操作:

  • 训练
  • 评估
  • 预测
  • 导出以供使用

开发人员可以使用TensorFlow预创建的 Estimator,也可以编写自定义 Estimator。所有 Estimator(无论是预创建的还是自定义)都是基于 tf.estimator.Estimator 类的类。

首先,使用CNN模型定义一个Estimator。

# 创建Estimator
mnist_classifier = tf.estimator.Estimator(
    model_fn=cnn_model_fn, model_dir="./train/mnist_convnet_model")

CNN需要多次训练,创建一个日志钩子来存储softmax层的值,每50次迭代一次。

# 创建一个日志钩子
tensors_to_log = {"probabilities": "softmax_tensor"}
logging_hook = tf.train.LoggingTensorHook(tensors=tensors_to_log, every_n_iter=50)

已经准备好评估模型了。将批处理大小设置为100并洗牌数据。请注意,我们设置的培训步骤为16.000,它可以花很多时间来培训。要有耐心。

接下来训练模型,批量大小(batch_size)设置为100。请注意,训练steps设置为1000,可能会花10来分钟时间来训练,要有耐心。

# 训练模型
train_input_fn = tf.estimator.inputs.numpy_input_fn(
    x={"x": X_train_scaled},
    y=y_train,
    batch_size=100,
    num_epochs=None,
    shuffle=True)

mnist_classifier.train(
    input_fn=train_input_fn,
    steps=1000,
    hooks=[logging_hook])

模型完成训练后,就可以评估并打印结果

# 评估模型输出结果
eval_input_fn = tf.estimator.inputs.numpy_input_fn(
    x={"x": X_test_scaled},
    y=y_test,
    num_epochs=1,
    shuffle=False)

eval_results = mnist_classifier.evaluate(input_fn=eval_input_fn)
print(eval_results)

输出

{'accuracy': 0.96085715, 'loss': 0.13425979, 'global_step': 16000}

在当前架构下,获得了96%的准确率。你可以更改体系结构、批量大小和迭代次数来提高准确性。通常,CNN神经网络的表现会远远好于ANN或logistic回归。

小结

卷积神经网络对图像的处理效果很好,主要用于图像识别。

要构建CNN,需要遵循以下六个步骤:

步骤1: 输入层

此步骤将重新构造数据。形状等于像素数的平方根。例如,如果一张图片有156个像素,那么它的形状就是26×26。您需要指定图片是否有颜色。如果是,那么你有3的形状- RGB是3,否则是1。

input_layer = tf.reshape(tensor = features["x"],shape =[-1, 28, 28, 1]) 

步骤2: 卷积层

接下来,需要创建卷积层。你可以使用不同的过滤器来提取图像特征,可以指定过滤器内核大小和过滤器数量。

conv1 = tf.layers.conv2d(
      inputs=input_layer,
      filters=14,
      kernel_size=[5, 5],
      padding="same",
      activation=tf.nn.relu)

步骤3: 池化层

池化层减少了输入数据量。它通过提取子矩阵的最大值来实现。例如,如果子矩阵是[3,1,3,2],池化后将返回最大值3。

pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)  

步骤4: 继续添加卷积层和池化层

你可以添加任意数量的卷积层和池化层,进一步提取特征,降低数据量。谷歌使用超过20个卷积层的架构。

步骤5: 全连接层

全连接层是真正实现分类的层,在此步骤中,可以使用不同的激活函数。
此外,为防止过拟合,设置正则化参数dropout。

pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 36])

dense = tf.layers.dense(inputs=pool2_flat, units=7 * 7 * 36, activation=tf.nn.relu)
dropout = tf.layers.dropout(
      inputs=dense, rate=0.3, training=mode == tf.estimator.ModeKeys.TRAIN)

步骤6: Logits 层

Logits 层用于生成最终的预测值。

logits = tf.layers.dense(inputs=dropout, units=10)  


浙ICP备17015664号 浙公网安备 33011002012336号 联系我们 网站地图  
@2019 qikegu.com 版权所有,禁止转载