1. 程式人生 > >基於Keras mnist手寫數字識別---Keras卷積神經網路入門教程

基於Keras mnist手寫數字識別---Keras卷積神經網路入門教程

目錄

1、一些說明

本部落格是參考Tensorflow官方的使用Tensorflow實現的mnist手寫數字識別例子,使用Tensorflow內建的keras實現的mnist,獲得了和原有用tensorflow編寫的程式碼相當的效能。本文也可以作為真正意義上使用Keras實現的卷積神經網路入門Demo。參考連線如下:

專案目錄結構如下

mnist-keras
---data/  		存放的是下載的資料集
---logs/ 		存放tensorflow日誌
---constants.py  	定義常量
---test.py		載入模型預測單張圖片
---train.py 		主檔案,模型定義以及訓練
---utils.py 		定義工具函式

2、常量定義

constants.py如下

# coding=utf-8

# 全域性變數定義

# CVDF mirror of http://yann.lecun.com/exdb/mnist/
SOURCE_URL = 'http://yann.lecun.com/exdb/mnist/' # 指定工作目錄,資料集就會儲存在這裡 WORK_DIRECTORY = 'data' # 影象的大小 IMAGE_SIZE = 28 # 影象的通道數,為1,即為灰度影象 NUM_CHANNELS = 1 # 影象想素值的範圍 PIXEL_DEPTH = 255 # 分類數目,0~9總共有10類 NUM_LABELS = 10 # 驗證集大小 VALIDATION_SIZE = 5000 # Size of the validation set. # 種子 SEED = 66478 # Set to None for random seed.
# 批次大小 BATCH_SIZE = 64 # 訓練多少個epoch NUM_EPOCHS = 10 # 驗證集大小 EVAL_BATCH_SIZE = 64

3、工具函式

我把原來在convolutional.py中的部分工具函式移動到了utils.py這個檔案裡面,使主檔案(train.py)更加簡潔。知道每個函式的功能就行。utils.py如下

# coding=utf-8
# 相容python3
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import gzip
import os

import numpy
import tensorflow as tf
from six.moves import urllib
from six.moves import xrange  # pylint: disable=redefined-builtin

from constants import *


def data_type(argv):
    """根據argv.use_fp16返回是否使用半精度"""
    if argv.use_fp16:
        return tf.float16
    else:
        return tf.float32


def maybe_download(filename):
    """
    如果沒有下載檔案filename,那麼把檔案下載到WORK_DIRECTORY
    """
    if not tf.gfile.Exists(WORK_DIRECTORY):
        tf.gfile.MakeDirs(WORK_DIRECTORY)
    filepath = os.path.join(WORK_DIRECTORY, filename)
    if not tf.gfile.Exists(filepath):
        filepath, _ = urllib.request.urlretrieve(SOURCE_URL + filename, filepath)
        with tf.gfile.GFile(filepath) as f:
            size = f.size()
        print('Successfully downloaded', filename, size, 'bytes.')
    return filepath


def extract_data(filename, num_images):
    """
    解壓filename指定的影象資料集為4D tensor [num_images,  y, x, channels]。
    影象的值從[0, 255]被縮放到了[-0.5, 0.5]。
    """
    print('Extracting', filename)
    with gzip.open(filename) as bytestream:
        bytestream.read(16)
        buf = bytestream.read(IMAGE_SIZE * IMAGE_SIZE * num_images * NUM_CHANNELS)
        data = numpy.frombuffer(buf, dtype=numpy.uint8).astype(numpy.float32)
        data = (data - (PIXEL_DEPTH / 2.0)) / PIXEL_DEPTH
        data = data.reshape(num_images, IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS)
        return data


def extract_labels(filename, num_images):
    """把標籤解壓為一個int64的向量。"""
    print('Extracting', filename)
    with gzip.open(filename) as bytestream:
        bytestream.read(8)
        buf = bytestream.read(1 * num_images)
        labels = numpy.frombuffer(buf, dtype=numpy.uint8).astype(numpy.int64)
    return labels


def fake_data(num_images):
    """生成MNIST需要的假的資料集。"""
    data = numpy.ndarray(
        shape=(num_images, IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS),
        dtype=numpy.float32)
    labels = numpy.zeros(shape=(num_images,), dtype=numpy.int64)
    for image in xrange(num_images):
        label = image % 2
        data[image, :, :, 0] = label - 0.5
        labels[image] = label
    return data, labels


def error_rate(predictions, labels):
    """返回錯誤率"""
    return 100.0 - (
            100.0 *
            numpy.sum(numpy.argmax(predictions, 1) == labels) /
            predictions.shape[0])

4、模型定義以及訓練

接下來講train.py這個檔案,因為是模型定義以及訓練模型的檔案,我會講的詳細些。

4.1、匯入庫

# coding=utf-8
# 相容python3
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import argparse
import scipy

# 從tensorflow裡匯入keras和keras.layer
from tensorflow import keras
from tensorflow.keras import layers

# 匯入工具函式
from utils import *

4.2、主入口

解析引數use_fp16和self_test,用到是ArgumentParser,就不詳細解釋了。

if __name__ == '__main__':
    # 定義parser
    parser = argparse.ArgumentParser()
    parser.add_argument(
        '--use_fp16',
        default=False,
        help='Use half floats instead of full floats if True.',
        action='store_true')
    parser.add_argument(
        '--self_test',
        default=False,
        action='store_true',
        help='True if running a self test.')
    # 解析引數
    FLAGS, unparsed = parser.parse_known_args()
    # 呼叫主函式
    main(FLAGS)

4.3、主函式

4.3.1、獲取訓練資料

def main(argv=None):
    if argv.self_test:
        """
        為了測試模型是否可以執行,生成了一些隨機資料集。
        """
        print('Running self-test...')
        # 生成訓練集
        train_data, train_labels = fake_data(256)
        # 生成驗證集
        validation_data, validation_labels = fake_data(EVAL_BATCH_SIZE)
        # 生成測試集
        test_data, test_labels = fake_data(EVAL_BATCH_SIZE)
        # 只訓練一個epoch
        num_epochs = 1
    else:
        """
        準備手寫數字資料集。
        """
        # 下載資料集
        train_data_filename = maybe_download('train-images-idx3-ubyte.gz')
        train_labels_filename = maybe_download('train-labels-idx1-ubyte.gz')
        test_data_filename = maybe_download('t10k-images-idx3-ubyte.gz')
        test_labels_filename = maybe_download('t10k-labels-idx1-ubyte.gz')

        # 把下載的資料解壓為numpy陣列
        train_data = extract_data(train_data_filename, 60000)
        train_labels = extract_labels(train_labels_filename, 60000)
        test_data = extract_data(test_data_filename, 10000)
        test_labels = extract_labels(test_labels_filename, 10000)

        # 分割train_data與train_labels,得到訓練集以及驗證集
        validation_data = train_data[:VALIDATION_SIZE, ...]
        validation_labels = train_labels[:VALIDATION_SIZE]
        train_data = train_data[VALIDATION_SIZE:, ...]
        train_labels = train_labels[VALIDATION_SIZE:]
        num_epochs = NUM_EPOCHS
        
        # 儲存一下第一張圖片,用來測試
        img0 = test_data[0]
        # 因為test_data被縮放到了-0.5~0.5,所以要恢復到原來的範圍[0, 255]
        img0 = img0*PIXEL_DEPTH+PIXEL_DEPTH/2
        # 儲存
        cv2.imwrite('test0.png', img0)
        
    # 對label進行one-hot編碼,因為模型的最後一層有10個輸出單元(10個類別)
    train_labels = keras.utils.to_categorical(train_labels)
    validation_labels = keras.utils.to_categorical(validation_labels)
    test_labels = keras.utils.to_categorical(test_labels)

4.3.1、定義模型

Tensorflow mnist官方Demo中的手寫數字識別的網路結構如下(從下往上看)。最下的那個是輸入input(定義了每張圖的大小和通道),然後依次是卷積層conv1、池化層pool1、conv2、pool2,接著是一個flatten層(把pool2之後輸出展開為一維向量),然後全連線層fc1,然後是一個dropout層(在呼叫evalute和predict時自動失效),fc2,最後是softmax層。
模型圖

模型的主要引數如下:

Name Output shape Kernel size Kernel count Strides Activation Padding Use bias
input_1 (None, 28, 28, 1)
conv1 (None, 28, 28, 32) (5, 5) 32 (1, 1) relu same True
pool1 (None, 14, 14, 32) (2, 2) (2, 2) same
conv2 (None, 14, 14, 64) (5, 5) 64 (1, 1) relu same True
pool2 (None, 7, 7, 64) (2, 2) (2, 2) same
flatten (None, 3136)
fc1 (None, 512) relu True
fc2 (None, 10)
softmax (None, 10)

實現模型的程式碼如下:

def inference(dtype):
    """
    使用keras定義mnist模型
    """
    # define a truncated_normal initializer
    tn_init = keras.initializers.truncated_normal(0, 0.1, SEED, dtype=dtype)
    # define a constant initializer
    const_init = keras.initializers.constant(0.1, dtype)
    # define a L2 regularizer
    l2_reg = keras.regularizers.l2(5e-4)

    # inputs: shape(None, 28, 28, 1)
    inputs = layers.Input(shape=(IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS), dtype=dtype)
    # conv1: shape(None, 28, 28, 32)
    conv1 = layers.Conv2D(32, (5, 5), strides=(1, 1), padding='same',
                          activation='relu', use_bias=True,
                          kernel_initializer=tn_init, name='conv1')(inputs)
    # pool1: shape(None, 14, 14, 32)
    pool1 = layers.MaxPool2D(pool_size=(2, 2), strides=(2, 2), padding='same', name='pool1')(conv1)
    # conv2: shape(None, 14, 14, 64)
    conv2 = layers.Conv2D(64, (5, 5), strides=(1, 1), padding='same',
                          activation='relu', use_bias=True,
                          kernel_initializer=tn_init,
                          bias_initializer=const_init, name='conv2')(pool1)
    # pool2: shape(None, 7, 7, 64)
    pool2 = layers.MaxPool2D(pool_size=(2, 2), strides=(2, 2), padding='same', name='pool2')(conv2)
    # flatten: shape(None, 3136)
    flatten = layers.Flatten(name='flatten')(pool2)
    # fc1: shape(None, 512)
    fc1 = layers.Dense(512, 'relu', True, kernel_initializer=tn_init,
                       bias_initializer=const_init, kernel_regularizer=l2_reg,
                       bias_regularizer=l2_reg, name='fc1')(flatten)
    # dropout
    dropout1 = layers.Dropout(0.5, seed=SEED)(fc1)
    # dense2: shape(None, 10)
    fc2 = layers.Dense(NUM_LABELS, activation=None, use_bias=True,
                       kernel_initializer=tn_init, bias_initializer=const_init, name='fc2',
                       kernel_regularizer=l2_reg, bias_regularizer=l2_reg)(dropout1)
    # softmax: shape(None, 10)
    softmax = layers.Softmax(name='softmax')(fc2)
    # make new model
    model = keras.Model(inputs=inputs, outputs=softmax, name='nmist')
    return model

接下來對上面用到的函式進行一些說明:

  • Conv2D:Keras的卷積可由這一個函式完成,引數包含了一層的所需要的所有引數。主要引數如下
    • filters: 整數。指定卷積核(在Keras中對應的概念為kernel,在tesorflow中為weights)的個數。這個數目也是本層輸出shape的最後一個元素大小。
    • kernel_size: 一個整數(2D卷積視窗的寬和高均為這個整數)或者兩個整陣列成的列表(分別指定2D卷積視窗的高和寬)。和tensorflow不同的是,這裡指定卷積核大小時,只需要指定2D卷積視窗的寬和高,而不需要指定卷積核的第三個維度的大小(卷積核第三個維度的大小和輸入的shape的最後一個元素相同);例如,在tensorflow中,一個影象批次(batch)的shape為(16,28,28,1),卷積核的個數32,卷積核的大小為5,那麼,卷積核的shape需要為(32,5,5,1);在Keras中,32應該是第一個引數的值,(5,5)是kernel_size的值,而1,會根據輸入(16,28,28,1)的最後一個元素自動推算為1(相等)。
    • strides: 一個整數(strides的寬和高均為這個整數)或者兩個整陣列成的列表(分別strides的高和寬)。對於Conv2D,strides也只需要指定寬和高。
    • padding: “valid” 或者 “same”。 請參考吳恩達的卷積神經網路課程。
    • data_format: 字串。 “channels_last” 或者 “channels_first”。如果是"channels_last"(預設),那麼,輸入資料的shape為(batch_size, height, width, channels);如果為"channels_first",那麼輸入資料為shape為(batch_size, channels, height, width)。這個引數就是指定通道數的位置。
    • dilation_rate: 請參考官方文件。
    • activation: 此層使用的啟用函式的名稱。不指定的話就不使用啟用函式。
    • use_bias: 布林值。是否使用bias。
    • kernel_initializer: 指定kernel_initializer。參見: initializers
    • bias_initializer: 指定bias_initializer。參見: initializers
    • kernel_regularizer: 指定kernel_regularizer。參見regularizer
    • bias_regularizer: 指定bias_regularizer。參見regularizer
    • activity_regularizer: 請參考官方文件。
    • kernel_constraint: 請參考官方文件。
    • bias_constraint:請參考官方文件。

其他函式請自行查閱文件,反正定義模型是要比tensorflow簡單多了。

4.3.2、編譯模型

    # 獲取模型
    model = inference(data_type(argv))
    # 列印模型的資訊
    model.summary()

    # 編譯模型;第一個引數是優化器;第二個引數為loss,因為是多元分類問題,固為
    # 'categorical_crossentropy';第三個引數為metrics,就是在訓練的時候需
    # 要監控的指標列表。
    model.compile(optimizer=keras.optimizers.SGD(lr=0.01, momentum=0.9, decay=1e-5),
                  loss='categorical_crossentropy', metrics=['accuracy'])

4.3.3、訓練模型

    # 設定回撥
    callbacks = [
        # 把TensorBoard的日誌寫入資料夾'./logs'
        keras.callbacks.TensorBoard(log_dir='./logs'),
    ]

    # 開始訓練
    model.fit(train_data, train_labels, BATCH_SIZE, epochs=num_epochs,
              validation_data=(validation_data, validation_labels), callbacks=callbacks)

4.3.4、評估以及儲存模型

    # evaluate
    print('', 'evaluating on test sets...')
    loss, accuracy = model.evaluate(test_data, test_labels)
    print('test loss:', loss)
    print('test Accuracy:', accuracy)

    # save model
    model.save('mnist-model.h5')

4.3.5、預測單張圖片

test.py如下

# coding=utf-8
# 相容python3
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import cv2
import numpy as np

from tensorflow import keras


if __name__ == '__main__':
    # 讀圖片
    img0 = cv2.imread('test0.png', cv2.IMREAD_UNCHANGED)
    print(img0.shape)
    # 擴充套件維度為4維,因為模型的輸入需要是4維
    img0 = np.resize(img0, (1, img0.shape[0], img0.shape[1], 1))
    print(img0.shape)

    # 恢復模型以及權重
    model = keras.models.load_model('mnist-model.h5')
    # 獲取模型最後一層,也就是softmax層的輸出,輸出的shape為(1, 10)
    last_layer_output = model.predict(img0)

    # 獲取最大值的索引
    max_index = np.argmax(last_layer_output, axis=-1)
    print('predict number: %d' % (int(max_index[0])))
    print('probability: ', last_layer_output[0][max_index])