Tag Archives: CNN

Android+TensorFlow+CNN+MNIST 手写数字识别实现

Catalogue

  1. 1. Overview
  2. 2. Practice
    1. 2.1. Environment
    2. 2.2. Train & Evaluate(Python+TensorFlow)
    3. 2.3. Test(Android+TensorFlow)
  3. 3. Theory
    1. 3.1. MNIST
    2. 3.2. CNN(Convolutional Neural Network)
      1. 3.2.1. CNN Keys
      2. 3.2.2. CNN Architecture
    3. 3.3. Regression + Softmax
      1. 3.3.1. Linear Regression
      2. 3.3.2. Softmax Regression
  4. 4. References & Recommends

Overview

本文系“SkySeraph AI 实践到理论系列”第一篇,咱以AI界的HelloWord 经典MNIST数据集为基础,在Android平台,基于TensorFlow,实现CNN的手写数字识别。
Code~


Practice

Environment

  • TensorFlow: 1.2.0
  • Python: 3.6
  • Python IDE: PyCharm 2017.2
  • Android IDE: Android Studio 3.0

Train & Evaluate(Python+TensorFlow)

训练和评估部分主要目的是生成用于测试用的pb文件,其保存了利用TensorFlow python API构建训练后的网络拓扑结构和参数信息,实现方式有很多种,除了cnn外还可以使用rnn,fcnn等。
其中基于cnn的函数也有两套,分别为tf.layers.conv2d和tf.nn.conv2d, tf.layers.conv2d使用tf.nn.conv2d作为后端处理,参数上filters是整数,filter是4维张量。原型如下:
convolutional.py文件
def conv2d(inputs, filters, kernel_size, strides=(1, 1), padding=’valid’, data_format=’channels_last’,
dilation_rate=(1, 1), activation=None, use_bias=True, kernel_initializer=None,
bias_initializer=init_ops.zeros_initializer(), kernel_regularizer=None, bias_regularizer=None,
activity_regularizer=None, kernel_constraint=None, bias_constraint=None, trainable=True, name=None,
reuse=None)

gen_nn_ops.py 文件

def conv2d(input, filter, strides, padding, use_cudnn_on_gpu=True, data_format="NHWC", name=None)

官方Demo实例中使用的是layers module,结构如下:

  • Convolutional Layer #1:32个5×5的filter,使用ReLU激活函数
  • Pooling Layer #1:2×2的filter做max pooling,步长为2
  • Convolutional Layer #2:64个5×5的filter,使用ReLU激活函数
  • Pooling Layer #2:2×2的filter做max pooling,步长为2
  • Dense Layer #1:1024个神经元,使用ReLU激活函数,dropout率0.4 (为了避免过拟合,在训练的时候,40%的神经元会被随机去掉)
  • Dense Layer #2 (Logits Layer):10个神经元,每个神经元对应一个类别(0-9)

核心代码在cnn_model_fn(features, labels, mode)函数中,完成卷积结构的完整定义,核心代码如下.

也可以采用传统的tf.nn.conv2d函数, 核心代码如下。

Test(Android+TensorFlow)

  • 核心是使用API接口: TensorFlowInferenceInterface.java
  • 配置gradle 或者 自编译TensorFlow源码导入jar和so
    compile ‘org.tensorflow:tensorflow-android:1.2.0’
  • 导入pb文件.pb文件放assets目录,然后读取

    String actualFilename = labelFilename.split(“file:///android_asset/“)[1];
    Log.i(TAG, “Reading labels from: “ + actualFilename);
    BufferedReader br = null;
    br = new BufferedReader(new InputStreamReader(assetManager.open(actualFilename)));
    String line;
    while ((line = br.readLine()) != null) {
    c.labels.add(line);
    }
    br.close();

  • TensorFlow接口使用
  • 最终效果:

Theory

MNIST

MNIST,最经典的机器学习模型之一,包含0~9的数字,28*28大小的单色灰度手写数字图片数据库,其中共60,000 training examples和10,000 test examples。
文件目录如下,主要包括4个二进制文件,分别为训练和测试图片及Label。

如下为训练图片的二进制结构,在真实数据前(pixel),有部分描述字段(魔数,图片个数,图片行数和列数),真实数据的存储采用大端规则。
(大端规则,就是数据的高字节保存在低内存地址中,低字节保存在高内存地址中)

在具体实验使用,需要提取真实数据,可采用专门用于处理字节的库struct中的unpack_from方法,核心方法如下:
struct.unpack_from(self._fourBytes2, buf, index)

MNIST作为AI的Hello World入门实例数据,TensorFlow封装对其封装好了函数,可直接使用
mnist = input_data.read_data_sets(‘MNIST’, one_hot=True)

CNN(Convolutional Neural Network)

CNN Keys

  • CNN,Convolutional Neural Network,中文全称卷积神经网络,即所谓的卷积网(ConvNets)。
  • 卷积(Convolution)可谓是现代深度学习中最最重要的概念了,它是一种数学运算,读者可以从下面链接[23]中卷积相关数学机理,包括分别从傅里叶变换和狄拉克δ函数中推到卷积定义,我们可以从字面上宏观粗鲁的理解成将因子翻转相乘卷起来。
  • 卷积动画。演示如下图[26],更多动画演示可参考[27]
  • 神经网络。一个由大量神经元(neurons)组成的系统,如下图所示[21]

    其中x表示输入向量,w为权重,b为偏值bias,f为激活函数。
  • Activation Function 激活函数: 常用的非线性激活函数有Sigmoid、tanh、ReLU等等,公式如下如所示。
    • Sigmoid缺点
      • 函数饱和使梯度消失(神经元在值为 0 或 1 的时候接近饱和,这些区域,梯度几乎为 0)
      • sigmoid 函数不是关于原点中心对称的(无0中心化)
    • tanh: 存在饱和问题,但它的输出是零中心的,因此实际中 tanh 比 sigmoid 更受欢迎。
    • ReLU
      • 优点1:ReLU 对于 SGD 的收敛有巨大的加速作用
      • 优点2:只需要一个阈值就可以得到激活值,而不用去算一大堆复杂的(指数)运算
      • 缺点:需要合理设置学习率(learning rate),防止训练时dead,还可以使用Leaky ReLU/PReLU/Maxout等代替
  • Pooling池化。一般分为平均池化mean pooling和最大池化max pooling,如下图所示[21]为max pooling,除此之外,还有重叠池化(OverlappingPooling)[24],空金字塔池化(Spatial Pyramid Pooling)[25]
    • 平均池化:计算图像区域的平均值作为该区域池化后的值。
    • 最大池化:选图像区域的最大值作为该区域池化后的值。

CNN Architecture

  • 三层神经网络。分别为输入层(Input layer),输出层(Output layer),隐藏层(Hidden layer),如下图所示[21]
  • CNN层级结构。 斯坦福cs231n中阐述了一种[INPUT-CONV-RELU-POOL-FC],如下图所示[21],分别为输入层,卷积层,激励层,池化层,全连接层。
  • CNN通用架构分为如下三层结构:
    • Convolutional layers 卷积层
    • Pooling layers 汇聚层
    • Dense (fully connected) layers 全连接层
  • 动画演示。参考[22]。

Regression + Softmax

机器学习有监督学习(supervised learning)中两大算法分别是分类算法和回归算法,分类算法用于离散型分布预测,回归算法用于连续型分布预测。
回归的目的就是建立一个回归方程用来预测目标值,回归的求解就是求这个回归方程的回归系数。
其中回归(Regression)算法包括Linear Regression,Logistic Regression等, Softmax Regression是其中一种用于解决多分类(multi-class classification)问题的Logistic回归算法的推广,经典实例就是在MNIST手写数字分类上的应用。

Linear Regression

Linear Regression是机器学习中最基础的模型,其目标是用预测结果尽可能地拟合目标label

  • 多元线性回归模型定义
  • 多元线性回归求解
  • Mean Square Error (MSE)
    • Gradient Descent(梯度下降法)
    • Normal Equation(普通最小二乘法)
    • 局部加权线性回归(LocallyWeightedLinearRegression, LWLR ):针对线性回归中模型欠拟合现象,在估计中引入一些偏差以便降低预测的均方误差。
    • 岭回归(ridge regression)和缩减方法
  • 选择: Normal Equation相比Gradient Descent,计算量大(需计算X的转置与逆矩阵),只适用于特征个数小于100000时使用;当特征数量大于100000时使用梯度法。当X不可逆时可替代方法为岭回归算法。LWLR方法增加了计算量,因为它对每个点做预测时都必须使用整个数据集,而不是计算出回归系数得到回归方程后代入计算即可,一般不选择。
  • 调优: 平衡预测偏差和模型方差(高偏差就是欠拟合,高方差就是过拟合)
    • 获取更多的训练样本 – 解决高方差
    • 尝试使用更少的特征的集合 – 解决高方差
    • 尝试获得其他特征 – 解决高偏差
    • 尝试添加多项组合特征 – 解决高偏差
    • 尝试减小 λ – 解决高偏差
    • 尝试增加 λ -解决高方差

Softmax Regression

  • Softmax Regression估值函数(hypothesis)
  • Softmax Regression代价函数(cost function)
  • 理解:
  • Softmax Regression & Logistic Regression:
    • 多分类 & 二分类。Logistic Regression为K=2时的Softmax Regression
    • 针对K类问题,当类别之间互斥时可采用Softmax Regression,当非斥时,可采用K个独立的Logistic Regression
  • 总结: Softmax Regression适用于类别数量大于2的分类,本例中用于判断每张图属于每个数字的概率。

References & Recommends

MNIST

Softmax

CNN

TensorFlow+CNN / TensorFlow+Android



By SkySeraph-2018

SkySeraph cnBlogs
SkySeraph CSDN

本文首发于skyseraph.com“Android+TensorFlow+CNN+MNIST 手写数字识别实现”

使用Keras卷积神经网络

这篇文章记录如何用 Keras 实现 卷积神经网络 CNN,并训练模型用于图片分类;以及 CNN 中一些超参的调整和自己的理解。

数据集

http://www.ivl.disco.unimib.it/activities/large-age-gap-face-verification/

这个图片数据集是一些名人的少年时和成年后的对比照片,格式为 100*100,RGB。

将图片分成成年和少年两个类别,实现的分类器是个二分器,训练出来的模型能够对输入的照片进行分类,给出一个 old 或者 young 的 label。

整理数据集为如下目录结构:

train  
├── old
│   ├── 1200 张图片
├── young
│   ├── 1200 张图片

validation  
├── old
│   ├── 600 张图片
├── young
│   ├── 600 张图片

test  
├── old
│   ├── 110 张图片
├── young
│   ├── 110 张图片

train 为训练组,validation 为验证组,训练时使用交叉验证,train 和 validation 的数据都会使用。

test 为验证模型的测试组。

图片展示

以下是 train/old 组展示的部分图片:

%matplotlib inline

import os  
import matplotlib.pyplot as plt  
import matplotlib.image as mpimg

some_old_pics = os.listdir('train/old')  
show_rows = show_columns = 5  
for k in range(show_rows * show_rows):  
    image = mpimg.imread('train/old/%s' % some_old_pics[k])
    plt.subplot(show_rows, show_rows, k+1)
    plt.imshow(image)
    plt.axis('off')

“平均脸”

其实就是求每组图片的平均值,代码参考这里

import os, numpy, PIL  
from PIL import Image  
import matplotlib.pyplot as plt  
def plot_average_pic(group):  
    dirs = ["%s/%s" % (group, i) for i in ['old', 'young']]
    for k in range(len(dirs)):
        dir = dirs[k]
        imlist = ['%s/%s' % (dir, c) for c in os.listdir(dir)]
        w, h = Image.open(imlist[0]).size
        N = len(imlist)
        arr = numpy.zeros((h, w, 3), numpy.float)
        for im in imlist:
            imarr = numpy.array(Image.open(im), dtype=numpy.float)
            arr = arr + imarr / N
        arr = numpy.array(numpy.round(arr), dtype=numpy.uint8)
        image = Image.fromarray(arr, mode="RGB")
        plt.subplot(2, 3, k+1)
        plt.imshow(image)
        plt.title(dir)
        plt.axis('off')

结果如下:

好神奇 …

从“平均脸”这个结果来看,两组 label 确实有比较明显的区分。

创建 CNN 模型

导入的模块:

from keras.preprocessing.image import ImageDataGenerator  
from keras.models import Sequential  
from keras.layers import Conv2D, MaxPooling2D  
from keras.layers import Activation, Dropout, Flatten, Dense  

先定义好一些参数,所有图片的输入尺寸 (100*100,RGB 三通道),train / validation 样本数,训练轮次 epochs,以及小批量梯度下降训练样本值 batch_size:

img_width, img_height = 100, 100

train_data_dir = './train'  
validation_data_dir = './validation'  
nb_train_samples = 1200  
nb_validation_samples = 600  
epochs = 100  
batch_size = 16

input_shape = (img_width, img_height, 3)  

创建 CNN

model = Sequential()  
model.add(Conv2D(32, (3, 3), padding='same', input_shape=input_shape))  
model.add(Activation('relu'))  
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(32, (3, 3), padding='same'))  
model.add(Activation('relu'))  
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(64, (3, 3), padding='same'))  
model.add(Activation('relu'))  
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Flatten())  
model.add(Dense(64))  
model.add(Activation('relu'))  
model.add(Dropout(0.5))

model.add(Dense(1))  
model.add(Activation('sigmoid'))

model.compile(loss='binary_crossentropy',  
              optimizer='rmsprop',
              metrics=['accuracy'])

定义 train 和 validation 的 ImageDataGenerator,图像增强,用缩放、镜像、旋转等方式增加图片,以便扩大数据量:

train_datagen = ImageDataGenerator(  
    rescale=1. / 255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)
validation_datagen = ImageDataGenerator(  
    rescale=1. / 255)
train_generator = train_datagen.flow_from_directory(  
    train_data_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary')
validation_generator = validation_datagen.flow_from_directory(  
    validation_data_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary')

开始训练:

from keras.callbacks import ModelCheckpoint, Callback, TensorBoard  
filepath="1.weights-improvement-{epoch:02d}-{val_acc:.2f}.h5"  
checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, save_best_only=True, mode='max')  
tensorboard = TensorBoard(log_dir='logs', histogram_freq=0)  
callbacks_list = [checkpoint, tensorboard]

model.fit_generator(  
    train_generator,
    steps_per_epoch=nb_train_samples // batch_size,
    epochs=epochs,
    validation_data=validation_generator,
    validation_steps=nb_validation_samples // batch_size,
    callbacks=callbacks_list)

可以看到前几次训练之后验证组的准确率 ( val_acc ) 就可以达到 82% 左右了。

( 一个 epoch 用 i7 CPU 需要 180s,而 GTX1080 GPU 只需要 3s )

100 轮训练后成绩最好的结果:

loss: 0.2957 - acc: 0.8875 - val_loss: 0.2994 - val_acc: 0.9139  

val_acc 比 acc 还要高 …

Tenserboard 上看到的训练过程:

验证模型:

导入效果最好的模型

from keras.models import load_model  
best_model = load_model('a.weights-improvement-64-0.91.h5')  

还是用 ImageDataGenerator:

datagen = ImageDataGenerator()  
test_generator = datagen.flow_from_directory(  
        './test',
        target_size=(img_width, img_height),
        batch_size=batch_size,
        class_mode='binary')

验证:

scores = best_model.evaluate_generator(  
        test_generator,
        val_samples=220)
print scores  

结果:

[2.7107817684386277, 0.82976878729858838]

准确率达到了 82.98%,效果还不错,但是距离训练时的 91.39% 还有一定的差距,比较明显的过拟合。

dropout

将模型中倒数第三层的 Dropout 参数,降低为 0.2,增加模型的泛化能力:

model.add(Dropout(0.2))  

训练及测试结果:

[train] loss: 0.2599 - acc: 0.9008 - val_loss: 0.2315 - val_acc: 0.9105
[test]  loss: 2.1801 - acc: 0.8636 

准确率提升至 86.36% 。

optimizer

将 optimizer 从 rmsprop 更改为当前比较流行的 adam:

model.compile(loss='binary_crossentropy',  
              optimizer='adam',
              metrics=['accuracy'])

结果:

[train] loss: 0.1916 - acc: 0.9250 - val_loss: 0.3205 - val_acc: 0.9037
[test]  loss: 1.4704 - acc: 0.8962

准确率进一步提升为 89.62% 。

从这次模型来看,测试结果与训练结果相当接近。

kernel size

将卷积层的卷积核大小从 (3,3) 改为 (5,5)

Conv2D(32, (5, 5), padding='same', input_shape=input_shape)  

可以理解为增加了神经网络的权重参数数量,因为卷积层的权重参数数量,以第一层卷积层为例:

3 * (3 * 3) * 32 + 32 = 896  

增大为:

3 * (5 * 5) * 32 + 32 = 2432  

( 乘式前面的 3 为卷积层的深度,第一层是输入层的深度;后面的 32 为过滤器的个数,加上的 32 为 bias 个数,每个过滤器 1 个 bias )

结果:

[train] loss: 0.1104 - acc: 0.9533 - val_loss: 0.3632 - val_acc: 0.9003
[test]  loss: 1.7149 - acc: 0.8902

可以看到训练时的训练组准确率达到了 95.33% 的较高水平,反映了权重参数数量增多的正面影响。

然而测试结果来看,与未改变卷积核大小时反而有些微下降,或许还应该降低 Dropout 的比例。

在此基础上将 Dropout 比例由 0.2 再次下降为 0.1,结果如下:

[train] loss: 0.1321 - acc: 0.9500 - val_loss: 0.3917 - val_acc: 0.8970
[test]  loss: 1.4034 - acc: 0.9124

测试的准确率超过了 91% !

padding

将卷积层中的 padding 参数改为默认的 valid,即:

Conv2D(32, (5, 5))  

保持 0.2 的 Dropout 比例

结果:

[train] loss: 0.1768 - acc: 0.9208 - val_loss: 0.3337 - val_acc: 0.8986
[test]  loss: 1.7612 - acc: 0.8902

从结果来看并没多大的区别。

activation

测试更换激活函数。比如使用 PReLU,Keras 里使用 PReLU 需要使用 advanced_activations 类,模型修改如下:

from keras.layers.advanced_activations import PReLU

model = Sequential()  
model.add(Conv2D(32, (5, 5), input_shape=input_shape, activation='linear'))  
model.add(PReLU())  
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(32, (5, 5), activation='linear'))  
model.add(PReLU())  
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(64, (5, 5), activation='linear'))  
model.add(PReLU())  
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Flatten())  
model.add(Dense(64))  
model.add(Activation('relu'))  
model.add(Dropout(0.1))

model.add(Dense(1))  
model.add(Activation('sigmoid'))

model.compile(loss='binary_crossentropy',  
              optimizer='adam',
              metrics=['accuracy'])

需要先声明一个 linear 激活函数的卷积层,然后再在后面增加一个 PReLU 层。

因为 PReLU 实际上会增加权重参数数量,因此使用了 0.1 的 Droupout。

结果:

[train] loss: 0.2656 - acc: 0.8850 - val_loss: 0.2623 - val_acc: 0.9054
[test]  loss: 1.9551 - acc: 0.8737

试下 LeakyReLU(alpha=0.001)

[train] loss: 0.2002 - acc: 0.9267 - val_loss: 0.2987 - val_acc: 0.8885

然而在加载最佳结果 load_model 时有报错,没有执行最佳模型的测试。在最后一组训练得出的 val_acc: 0.8497 结果的模型上测试结果如下:

[test]  loss: 2.1069 - acc: 0.8688

batch size

训练时的 batch size 对训练模型也有一定的影响,具体可参考知乎上的讨论

实际测试如下:( 在之前 91% 测试准确率模型基础上修改 batch_size )

单从这个测试结果上看,还是 batch size 为 16 的最佳。

对比 batch size = 16 的训练过程,batch size = 8 的 acc 开始时上升较快,val_acc 震荡更为明显:

deeper

更深的网络。

再增加一层卷积层,模型修改如下:

model = Sequential()  
model.add(Conv2D(32, (5, 5), padding='same', input_shape=input_shape))  
model.add(Activation('relu'))  
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(32, (5, 5), padding='same'))  
model.add(Activation('relu'))  
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(64, (5, 5), padding='same'))  
model.add(Activation('relu'))  
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(128, (5, 5), padding='same'))  
model.add(Activation('relu'))  
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Flatten())  
model.add(Dense(128))  
model.add(Activation('relu'))  
model.add(Dropout(0.1))

model.add(Dense(1))  
model.add(Activation('sigmoid'))

model.compile(loss='binary_crossentropy',  
              optimizer='adam',
              metrics=['accuracy'])

结果:

[train] loss: 0.2415 - acc: 0.9075 - val_loss: 0.3782 - val_acc: 0.8581
[test]  loss: 2.5879 - acc: 0.8361

可以看到过拟合严重了,或许对于数据量较少的训练集,使用更深的网络并不是一个较好的选择,比较容易出现过拟合。

最后

使用了测试结果为 91% 的模型,随便在谷歌上找了些图片进行测试,效果还不赖,如下图:

老年组 90% 准确率

温老四你怎么了 …

少年组 95% 准确率

詹老汉亮了。

参考

https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html

from:https://zhengheng.me/2017/08/30/keras-cnn/