目录

AaronJny

诗酒繁华,书剑天涯。

X

机器学习里的Hello World——TensorFlow 2.0在MNIST数据集上的尝试

首先,TensorFlow 2.0 已经正式发布很久啦,TensorFlow 2.0 真香 ~

我刚开始用 TensorFlow 的时候,还是 1.4 版本。有一说一,我觉得 1.x 版本的 TensorFlow 真心不怎么好用,虽然很灵活,但实现模型太过繁琐,接口很乱,还有很多冗余接口。

后来刚接触到 Keras,便觉得这是一股清流,Keras 封装的接口非常简洁,你完全可以使用 Keras 以极快的速度完成模型的构建。但它也有个明显的缺点——因为封装程度比较高,它的灵活性不足,这个时候还是要靠 TensorFlow 1.x。

在我快要转投 Pytorch 阵营的时候,TensorFlow 2.0 出来啦 ~然后……

image.png

MNIST 数据集作为机器学习里的“Hello World”,本文以 MNIST 数据集来演示 TensorFlow 2.0 的简单使用。

一、使用 TensorFlow 2.0 的低级接口编写模型

机器学习框架很难实时跟进业内的最新研究成果,一些比较新的模型在框架里面可能是没有的。如果我们需要手动去实现一些自定义的模型,TensorFlow 2.0 的低级接口给我们留下了足够的灵活性,以完成此项工作。

当然了,这里我们不需要写什么新模型,就简单用低级接口实现一个 MNIST 上的全连接神经网络。

简单介绍下 MNIST 数据集(我觉得大家都是知道的,但以防万一,我还是多说一下 = =),这是一个手写数字(0-9)的数据集,共 7 万张图片,其中 6 万张作为训练集,1 万张作为验证集(又叫开发集)。每张手写数字图片都是 28*28 的灰度图片。

我们可以通过 tf.keras 直接获取数据集。

import tensorflow as tf

(x_train, y_train), (x_dev, y_dev) = tf.keras.datasets.mnist.load_data()
print(x_train.shape, y_train.shape, x_dev.shape, y_dev.shape)

我们看一下数据维度:

(60000, 28, 28) (60000,) (10000, 28, 28) (10000,)

OK,没有问题,和我们前面说的一致。因为是编写全连接神经网络,所以需要把 28*28 的图片,reshape 成长度为 784 的一维向量,我们先对数据进行预处理,并封装成 tf.data.Dataset 对象:

# -*- coding: utf-8 -*-
# @File    : data.py
# @Author  : AaronJny
# @Time    : 2019/12/17
# @Desc    :
import tensorflow as tf


def preprocess_1d(x, y):
    """
    预处理数据,28*28的图片将被展平为长度为784的向量
    """
    x = tf.cast(x, dtype=tf.float32)
    # 按像素值的深浅压缩为[0,1]之间
    x = x / 255.
    # 展平图片
    x = tf.reshape(x, (-1, 28 * 28))
    # 将标签转成one-hot形式
    y = tf.cast(y, dtype=tf.int32)
    y = tf.one_hot(y, 10)
    return x, y


def load_data_1d(batch_size=128):
    """
    加载mnist数据集.返回两个tf.data.Dataset对象,分别为训练数据集和验证数据集。
    x.shape==(None,784),y.shape==(None,10)
    """
    # 利用keras下载并加载数据集
    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
    # 封装成tf.data.Dataset数据集对象
    train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train))
    # 设置每次迭代的mini_batch大小
    train_db = train_db.batch(batch_size)
    # 对数据进行预处理
    train_db = train_db.map(preprocess_1d)
    # 打乱数据顺序
    train_db = train_db.shuffle(10000)

    # 封装数据集对象
    test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
    # 设置mini_batch
    test_db = test_db.batch(batch_size)
    # 进行数据预处理
    test_db = test_db.map(preprocess_1d)

    return train_db, test_db

有几点需要注意一下:

  • 为提升训练效果,图片像素按比例压缩到[0,1]之间
  • 为了将图片输入到全连接神经网络中,图片从[28,28]被展平为[784,]
  • 为提升训练效果,训练数据集被随机打乱,而模型不会在验证集上学习,所以验证集没必要打乱顺序

接下来我们实现一个全连接模型:

# -*- coding: utf-8 -*-
# @File    : model1.py
# @Author  : AaronJny
# @Time    : 2019/12/17
# @Desc    :
import matplotlib.pyplot as plt
import tensorflow as tf
from data import load_data_1d

# 获取训练数据集和验证数据集
train_db, dev_db = load_data_1d(128)

# ########创建参数#########
# 使用xavier进行初始化
initializer = tf.initializers.GlorotNormal()
# 第一层全连接层参数
w1 = tf.Variable(initializer((784, 256)))
b1 = tf.Variable(initializer((256,)))
# 第二层全连接层参数
w2 = tf.Variable(initializer((256, 128)))
b2 = tf.Variable(initializer((128,)))
# 第三层(输出层)参数(10分类问题,所以节点数为10)
w3 = tf.Variable(initializer((128, 10)))
b3 = tf.Variable(initializer((10,)))

# 用一个list保存全部可训练参数,便于后面进行反向传播
trainable_vars = [w1, b1, w2, b2, w3, b3]
# 学习率
learn_rate = 0.01
# 训练多少个epoch
epochs = 30
# 记录训练损失值的列表,用于绘图
train_losses = []
# 记录验证集准确率的列表,用于绘图
val_accs = []

# ##########开始训练了##########
# 训练epochs个epoch
for epoch in range(1, epochs + 1):
    print('epoch {}...'.format(epoch))
    # 训练一个epoch
    for step, (x, y) in enumerate(train_db, start=1):
        # 对于每一个mini_batch
        with tf.GradientTape() as tape:
            # ###前向传播####
            # 第一层relu(wx+b)
            z1 = x @ w1 + b1
            a1 = tf.nn.relu(z1)
            # 第二层
            z2 = a1 @ w2 + b2
            a2 = tf.nn.relu(z2)
            # 第三层,softmax(wx+b)
            z3 = a2 @ w3 + b3
            a3 = tf.nn.softmax(z3)
            # 计算loss
            loss = tf.keras.losses.categorical_crossentropy(y, a3)
            loss = tf.reduce_mean(loss)
            # ######反向传播######
            # 对所有可训练参数求偏导
            grads = tape.gradient(loss, trainable_vars)
            # 梯度下降
            for var, grad in zip(trainable_vars, grads):
                # w=w-lr*dw
                var.assign_sub(learn_rate * grad)
        # 记录本次训练损失值
        train_losses.append(float(loss))
    # 一个epoch训练完成了,进行一次验证
    # 记录总样本数和正确样本数
    true_cnt = 0
    total_cnt = 0
    for x, y in dev_db:
        # 预测
        z1 = x @ w1 + b1
        a1 = tf.nn.relu(z1)
        z2 = a1 @ w2 + b2
        a2 = tf.nn.relu(z2)
        z3 = a2 @ w3 + b3
        a3 = tf.nn.softmax(z3)
        # 获取预测值
        y_pred = tf.argmax(a3, axis=1)
        # 正确标签
        y_true = tf.argmax(y, axis=1)
        # 计算正确的数量
        true_cnt += int(tf.reduce_sum(tf.cast(tf.equal(y_pred, y_true), dtype=tf.int32)))
        total_cnt += y_pred.shape[0]
    # 计算平均正确率
    val_accs.append(true_cnt / total_cnt)
    # 输出一下acc
    print('Val Accuracy', val_accs[-1])

# 训练完成了,绘制出训练loss变化图和验证acc变化图

# loss图
plt.figure()
plt.plot(train_losses, 'b')
# acc图
plt.figure()
plt.plot(val_accs, 'r')
plt.show()

简单训练 30 个 epoch,能够看到,在训练过程中,loss 逐步降低,验证集准确率逐步提高。30 个 epoch 以后,验证集准确率约为 96%.

image.png

二、使用 tf.keras 实现一个全连接神经网络

只使用低级接口实现神经网络毕竟还是太麻烦了,要写很多行代码,开发效率很低。幸好,tf.keras 已经为我们封装好了相关接口,下面,我们使用 tf.keras 实现一个全连接神经网络。(当然,如果你需要实现比较新的模型,或者自定义程度很高的网络,还是会需要用到低级接口,这个时候你就会感激 TensorFlow 的灵活性)

# -*- coding: utf-8 -*-
# @File    : model2.py
# @Author  : AaronJny
# @Time    : 2019/12/18
# @Desc    :
import tensorflow as tf
from data import load_data_1d

# 加载数据集
train_db, dev_db = load_data_1d(128)
# 构建模型
model = tf.keras.Sequential([
    # 第一层,节点为256,激活函数为relu
    tf.keras.layers.Dense(256, activation=tf.nn.relu),
    tf.keras.layers.Dense(128, activation=tf.nn.relu),
    tf.keras.layers.Dense(64, activation=tf.nn.relu),
    # 输出层,10分类问题,激活函数为softmax
    tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])
# 输出一下模型的结构
model.build((None, 784))
model.summary()
# 编译模型,配置优化器、损失函数以及监控指标
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=tf.keras.losses.categorical_crossentropy,
              metrics=['accuracy'])
# 调用tf.keras封装的训练接口,开始训练
model.fit_generator(train_db, epochs=20, validation_data=dev_db)

可以看到,代码非常简洁,短短几行代码就完成了任务。训练 20 个 epoch,模型在开发集上的准确率为 98%.

Epoch 18/20
469/469 [==============================] - 5s 12ms/step - loss: 0.0111 - accuracy: 0.9958 - val_loss: 0.0866 - val_accuracy: 0.9797
Epoch 19/20
469/469 [==============================] - 5s 11ms/step - loss: 0.0101 - accuracy: 0.9965 - val_loss: 0.0848 - val_accuracy: 0.9806
Epoch 20/20
469/469 [==============================] - 6s 12ms/step - loss: 0.0069 - accuracy: 0.9974 - val_loss: 0.0917 - val_accuracy: 0.9803

三、使用 tf.keras 实现一个卷积神经网络(类 LeNet 结构)

处理图片,还是卷积神经网络更合适一些。接下来让我们用 tf.keras 实现一个类似于 LeNet 结构的卷积神经网络。在这之前,为了满足输入要求,先处理一下数据集:

def preprocess_2d(x, y):
    """
    预处理数据,将[28,28]reshape为[28,28,1].
    """
    x = tf.cast(x, dtype=tf.float32)
    # 按像素值的深浅压缩为[0,1]之间
    x = x / 255.
    # h28,w28,信道1
    x = tf.reshape(x, (-1, 28, 28, 1))
    # 将标签转成one-hot形式
    y = tf.cast(y, dtype=tf.int32)
    y = tf.one_hot(y, 10)
    return x, y


def load_data_2d(batch_size=128):
    """
    加载mnist数据集.返回两个tf.data.Dataset对象,分别为训练数据集和验证数据集。
    x.shape==(None,28,28,1),y.shape==(None,10)
    """
    # 利用keras下载并加载数据集
    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
    # 封装成tf.data.Dataset数据集对象
    train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train))
    # 设置每次迭代的mini_batch大小
    train_db = train_db.batch(batch_size)
    # 对数据进行预处理
    train_db = train_db.map(preprocess_2d)
    # 打乱数据顺序
    train_db = train_db.shuffle(10000)

    # 封装数据集对象
    test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
    # 设置mini_batch
    test_db = test_db.batch(batch_size)
    # 进行数据预处理
    test_db = test_db.map(preprocess_2d)

    return train_db, test_db

数据处理过程和第一部分的数据处理过程差不多,没什么好说的,只是数据的 shape 改变了一下。下面开始构建网络:

# -*- coding: utf-8 -*-
# @File    : model3.py
# @Author  : AaronJny
# @Time    : 2019/12/18
# @Desc    :
import tensorflow as tf
from data import load_data_2d

# 加载数据集
train_db, dev_db = load_data_2d(128)
# 构建模型
model = tf.keras.Sequential([
    # 第一个卷积层,padding保持卷积后图片大小不变,依然是(28,28)
    tf.keras.layers.Conv2D(6, (3, 3), padding='same'),
    # 添加BN层,将数据调整为均值0,方差1
    tf.keras.layers.BatchNormalization(),
    # 最大池化层,池化后图片长宽减半
    tf.keras.layers.MaxPooling2D((2, 2), 2),
    # relu激活层
    tf.keras.layers.ReLU(),
    # 第二个卷积层
    tf.keras.layers.Conv2D(16, (5, 5)),
    tf.keras.layers.BatchNormalization(),
    tf.keras.layers.MaxPooling2D((2, 2), 2),
    tf.keras.layers.ReLU(),
    # 将节点展平为(None,-1)的形式,以作为全连接层的输入
    tf.keras.layers.Flatten(),
    # 第一个全连接层,120个节点
    tf.keras.layers.Dense(120),
    tf.keras.layers.BatchNormalization(),
    tf.keras.layers.ReLU(),
    # 第二个全连接层
    tf.keras.layers.Dense(84),
    tf.keras.layers.BatchNormalization(),
    tf.keras.layers.ReLU(),
    # 输出层,使用softmax激活
    tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])

# 输出网络结构
model.build((None, 28, 28, 1))
model.summary()
# 编译模型
model.compile(tf.keras.optimizers.Adam(0.001), loss=tf.keras.losses.categorical_crossentropy, metrics=['accuracy'])
# 开始训练
model.fit_generator(train_db, epochs=20, validation_data=dev_db)

训练 20 个 epoch,模型在验证集上的准确率为 99%。

Epoch 19/20
469/469 [==============================] - 13s 28ms/step - loss: 9.4982e-05 - accuracy: 1.0000 - val_loss: 0.0328 - val_accuracy: 0.9914
Epoch 20/20
469/469 [==============================] - 13s 28ms/step - loss: 7.2764e-05 - accuracy: 1.0000 - val_loss: 0.0330 - val_accuracy: 0.9915

结语

毕竟只是 Hello World,没什么难度,仅用来介绍 TensorFlow 2.0 的简单用法,希望对你有所帮助。


标题:机器学习里的Hello World——TensorFlow 2.0在MNIST数据集上的尝试
作者:AaronJny
地址:https://aaronjny.com/articles/2019/12/17/1576597057367.html