跳转至

识别 MNIST 手写体数字

在这篇文章中,我们将学习:

  • 使用 OneFlow 接口配置软硬件环境
  • 使用 OneFlow 的接口定义模型
  • 使用 train 类型作业函数做模型训练
  • 模型的保存和加载
  • 使用 predict 类型作业函数做模型校验
  • 使用 predict 类型作业函数做图像识别

本文通过使用 LeNet 模型,训练 MNIST 数据集向大家介绍使用 OneFlow 的各个核心环节,文末附有完整示例代码的链接。

在学习之前,也可以通过以下命令查看各脚本功能(脚本运行依赖 默认选择机器上的0号GPU,如果你安装的是CPU版本OneFlow,则脚本会自动调用CPU来做训练。)。

首先,同步本文档仓库并切换到对应路径:

git clone https://github.com/Oneflow-Inc/oneflow-documentation.git
cd oneflow-documentation/cn/docs/code/quick_start/

模型训练

python lenet_train.py
以上命令将对 MNIST 数据集进行训练,并保存模型。

输出:

File mnist.npz already exist, path: ./mnist.npz
5.9947124
1.0865117
0.5317516
0.20937675
0.26428983
0.21764673
0.23443426
...

以下的两个脚本 lenet_eval.pylenet_test.py 都依赖以上训练的结果,因此需要先运行以上脚本。或者你可以直接下载我们已经训练好的模型,则可以略过以上步骤,下载方法如下:

#在仓库docs/code/quick_start/目录下
wget https://oneflow-public.oss-cn-beijing.aliyuncs.com/online_document/docs/quick_start/lenet_models_1.zip
unzip lenet_models_1.zip

模型校验

python lenet_eval.py
以上命令,使用 MNIST 测试集对刚刚生成的模型进行校验,并给出准确率。

输出:

File mnist.npz already exist, path: ./mnist.npz
accuracy: 99.4%

图像识别

python lenet_test.py ./9.png
# 输出:prediction: 9
以上命令将使用之前训练的模型对我们准备好的 9.png 图片文件中的内容进行预测。 你也可以下载我们提取好的 mnist 图片,自行对更多图片文件的预测效果进行验证。

MNIST 数据集介绍

MNIST 是一个手写数字的数据集。包括了训练集与测试集;训练集包含了60000张图片以及图片对应的标签,测试集包含了10000张图片以及图片测试的标签。Yann LeCun 等已经将图片进行了大小归一化及居中处理,并且打包为二进制文件供下载(http://yann.lecun.com/exdb/mnist/)。本文涉及的脚本会自动下载 MNIST 数据集。

定义训练模型

oneflow.nnoneflow.layers 模块中提供了常见的用于构建模型的算子。

def lenet(data, train=False):
    initializer = flow.truncated_normal(0.1)
    conv1 = flow.layers.conv2d(
        data,
        32,
        5,
        padding="SAME",
        activation=flow.nn.relu,
        name="conv1",
        kernel_initializer=initializer,
    )
    pool1 = flow.nn.max_pool2d(
        conv1, ksize=2, strides=2, padding="SAME", name="pool1", data_format="NCHW"
    )
    conv2 = flow.layers.conv2d(
        pool1,
        64,
        5,
        padding="SAME",
        activation=flow.nn.relu,
        name="conv2",
        kernel_initializer=initializer,
    )
    pool2 = flow.nn.max_pool2d(
        conv2, ksize=2, strides=2, padding="SAME", name="pool2", data_format="NCHW"
    )
    reshape = flow.reshape(pool2, [pool2.shape[0], -1])
    hidden = flow.layers.dense(
        reshape,
        512,
        activation=flow.nn.relu,
        kernel_initializer=initializer,
        name="dense1",
    )
    if train:
        hidden = flow.nn.dropout(hidden, rate=0.5, name="dropout")
    return flow.layers.dense(hidden, 10, kernel_initializer=initializer, name="dense2")

以上代码中,我们搭建了一个 LeNet 网络模型。

实现训练作业函数

OneFlow 中提供了 oneflow.global_function 装饰器,通过它,可以将一个 Python 函数转变为作业函数(job function)。

global_function 装饰器

oneflow.global_function 装饰器需要两个参数:typefunction_configtype用于指定作业函数的类型,type="train" 意味着作业函数用于训练,type="predict" 意味着作业函数用于预测。function_config 参数是一个 oneflow.function_config 对象,可用它配置作业函数的细节。

以下代码片段展示,我们定义了一个 train 类型的作业函数,因为没有设置 function_config,所以作业函数的其它配置为默认配置。

@flow.global_function(type="train")
def train_job(images:tp.Numpy.Placeholder((BATCH_SIZE, 1, 28, 28), dtype=flow.float),
              labels:tp.Numpy.Placeholder((BATCH_SIZE,), dtype=flow.int32)) -> tp.Numpy:
    #作业函数实现 ...

其中的 tp.Numpy.Placeholder 是数据占位符, -> tp.Numpy 指定这个作业函数在调用时,将返回一个 numpy 对象。

指定优化目标

我们可以通过 oneflow.optimizer 下的接口指定优化器及其优化目标。这样,OneFlow 在每次迭代训练作业的过程中,将以指定的方式优化目标。

@flow.global_function(type="train")
def train_job(
    images: tp.Numpy.Placeholder((BATCH_SIZE, 1, 28, 28), dtype=flow.float),
    labels: tp.Numpy.Placeholder((BATCH_SIZE,), dtype=flow.int32),
) -> tp.Numpy:
    with flow.scope.placement("gpu", "0:0"):
        logits = lenet(images, train=True)
        loss = flow.nn.sparse_softmax_cross_entropy_with_logits(
            labels, logits, name="softmax_loss"
        )

    lr_scheduler = flow.optimizer.PiecewiseConstantScheduler([], [0.1])
    flow.optimizer.SGD(lr_scheduler, momentum=0).minimize(loss)
    return loss

以上,我们通过 flow.nn.sparse_softmax_cross_entropy_with_logits 求得 loss ,并且将 loss 作为优化目标。

  • lr_scheduler 设定了学习率计划,[0.1]表明初始学习率为0.1;
  • flow.optimizer.SGD 则指定了优化器为 SGD;loss 作为参数传递给 minimize 表明优化器将以最小化 loss 为目标。

更多 optimizer 及其使用方法可以参见 oneflow.optimizer

调用作业函数并交互

调用作业函数就可以开始训练。

调用作业函数的返回结果,由定义作业函数时指定的返回值类型决定,可以返回一个,也可以返回多个结果。

返回一个结果的例子

lenet_train.py 中定义的作业函数:

@flow.global_function(type="train")
def train_job(
    images: tp.Numpy.Placeholder((BATCH_SIZE, 1, 28, 28), dtype=flow.float),
    labels: tp.Numpy.Placeholder((BATCH_SIZE,), dtype=flow.int32),
) -> tp.Numpy:
    with flow.scope.placement("gpu", "0:0"):
        logits = lenet(images, train=True)
        loss = flow.nn.sparse_softmax_cross_entropy_with_logits(
            labels, logits, name="softmax_loss"
        )

    lr_scheduler = flow.optimizer.PiecewiseConstantScheduler([], [0.1])
    flow.optimizer.SGD(lr_scheduler, momentum=0).minimize(loss)
    return loss
该作业函数的返回值类型为 tp.Numpy,则当调用时,会返回一个 numpy 对象:
for epoch in range(20):
        for i, (images, labels) in enumerate(zip(train_images, train_labels)):
            loss = train_job(images, labels)
            if i % 20 == 0:
                print(loss.mean())

我们调用了 train_job 并每循环20次打印1次 loss.mean()

返回多个结果的例子

在模型校验的代码 lenet_eval.py 中定义的作业函数:

@flow.global_function(type="predict")
def eval_job(
    images: tp.Numpy.Placeholder((BATCH_SIZE, 1, 28, 28), dtype=flow.float),
    labels: tp.Numpy.Placeholder((BATCH_SIZE,), dtype=flow.int32),
) -> Tuple[tp.Numpy, tp.Numpy]:
    with flow.scope.placement("gpu", "0:0"):
        logits = lenet(images, train=False)
        loss = flow.nn.sparse_softmax_cross_entropy_with_logits(
            labels, logits, name="softmax_loss"
        )

    return (labels, logits)

该作业函数的返回值类型为 Tuple[tp.Numpy, tp.Numpy],则当调用时,会返回一个 tuple 元组,里面有2个元素,每个元素都是一个 numpy 对象:

for i, (images, labels) in enumerate(zip(test_images, test_labels)):
            labels, logits = eval_job(images, labels)
            acc(labels, logits)
我们调用作业函数返回了 labelslogits,并用它们评估模型准确率。

同步与异步调用

本文所有代码都是同步方式调用作业函数,实际上 OneFlow 还支持异步方式调用作业函数,具体可参考获取作业函数的结果一文。

模型的初始化、保存与加载

模型的初始化与保存

通过 flow.checkpoint.save 方法保存模型。如下例:

if __name__ == '__main__':
  #加载数据及训练 ...
  flow.checkpoint.save("./lenet_models_1")

保存成功后,我们将得到名为 lenet_models_1目录 ,该目录中包含了与模型参数对应的子目录及文件。

模型的加载

在预测过程中,我们可以通过 flow.checkpoint.get 从文件中加载参数值到内存,再通过 flow.load_variables 将参数值更新到模型上。如下例:

if __name__ == '__main__':
  flow.load_variables(flow.checkpoint.get("./lenet_models_1"))
  #校验过程 ...

模型的校验

用于校验的 predict 类型的作业函数与 train 类型的作业函数 几乎没有区别 ,不同之处在于校验过程中的模型参数来自于已经保存好的模型,因此不需要初始化,不需要更新模型参数(所以也不用指定 optimizer)。

用于校验的作业函数的编写

@flow.global_function(type="predict")
def eval_job(
    images: tp.Numpy.Placeholder((BATCH_SIZE, 1, 28, 28), dtype=flow.float),
    labels: tp.Numpy.Placeholder((BATCH_SIZE,), dtype=flow.int32),
) -> Tuple[tp.Numpy, tp.Numpy]:
    with flow.scope.placement("gpu", "0:0"):
        logits = lenet(images, train=False)
        loss = flow.nn.sparse_softmax_cross_entropy_with_logits(
            labels, logits, name="softmax_loss"
        )

    return (labels, logits)

以上是用于校验的作业函数的实现,声明了返回值类型是 Tuple[tp.Numpy, tp.Numpy], 因此返回一个 tupletuple 中有2个元素,每个元素都是1个 numpy 对象。我们将调用predict类型作业函数,并根据返回结果计算准确率。

迭代校验

以下 acc 函数中统计样本的总数目,以及校验正确的总数目,我们将调用作业函数,得到 labelslogits

g_total = 0
g_correct = 0


def acc(labels, logits):
    global g_total
    global g_correct

    predictions = np.argmax(logits, 1)
    right_count = np.sum(predictions == labels)
    g_total += labels.shape[0]
    g_correct += right_count

调用校验作业函数:

if __name__ == "__main__":
    flow.load_variables(flow.checkpoint.get("./lenet_models_1"))
    (train_images, train_labels), (test_images, test_labels) = flow.data.load_mnist(
        BATCH_SIZE, BATCH_SIZE
    )

    for epoch in range(1):
        for i, (images, labels) in enumerate(zip(test_images, test_labels)):
            labels, logits = eval_job(images, labels)
            acc(labels, logits)

    print("accuracy: {0:.1f}%".format(g_correct * 100 / g_total))

以上,循环调用校验函数,最终输出在 MNIST 测试集上的准确率。

预测图片

将以上校验代码修改,使得校验数据来自于原始的图片而不是现成的数据集,我们就可以使用模型进行图片内容预测。

def load_image(file):
    im = Image.open(file).convert("L")
    im = im.resize((28, 28), Image.ANTIALIAS)
    im = np.array(im).reshape(1, 1, 28, 28).astype(np.float32)
    im = (im - 128.0) / 255.0
    im.reshape((-1, 1, 1, im.shape[1], im.shape[2]))
    return im


def main():
    if len(sys.argv) != 2:
        usage()
        return
    flow.load_variables(flow.checkpoint.get("./lenet_models_1"))

    image = load_image(sys.argv[1])
    logits = test_job(image)

    prediction = np.argmax(logits, 1)
    print("prediction: {}".format(prediction[0]))


if __name__ == "__main__":
    main()

完整代码

训练模型

代码:lenet_train.py

校验模型

代码:lenet_eval.py

预训练模型:lenet_models_1.zip

数字预测

代码:lenet_test.py

预训练模型:lenet_models_1.zip

MNIST 数据集图片:mnist_raw_images.zip