教你 30 分钟写完 MNIST 作业

laekov 2018.05.20

Overview

题目要求

随便用什么框架去 Kaggle 上做手写数字识别的数据集.

你可以选择自己写一个框架. (谁爱写谁写)

THU PACMAN 实验室最近在筹划写一个框架, 有兴趣的同学可以联系我.

最好多实现几个模型, 调调参, 写报告需要.

需要做的事

  • 找一个框架
  • 抄一遍 Tutorial
  • 写一个处理数据 (csv格式) 的模块
  • 写一个模型
  • 写训练/验证/测试的东西

CNTK 框架

微软的框架. Python 文档见https://cntk.ai/pythondocs/index.html

安装. 见官网. pip, conda, install_cntk.exe 什么的都行.

略.

使用 CNTK 框架.

import os
import cntk as C
import numpy as np

轻松愉快.

处理 CSV

数据格式

第一行不用管

1234567890,0,0,...,255,0

一行一张图, 第一个是数字, 后面是28x280-255的灰度值.

可以用 pandas 之类的又麻烦又难用的库.

不如自己写个小函数.

def csv_reader(filename, batch_size = 1, is_test = False):
    with open(filename, 'r') as f:
        f.readline()
        batch = { 'image': [], 'label': [] }
        while True:
            raw_line = f.readline().split(',')
            if len(raw_line) < 10:
                break
            label = np.zeros(10, dtype = float)
            if not is_test:
                label[int(raw_line[0])] = 1
                raw_line = raw_line[1:]
            batch['image'].append(np.array([ float(x) for x in raw_line ], dtype = float).reshape((1, 28, 28)))
            batch['label'].append(label)
            if len(batch['image']) == batch_size:
                yield batch
                batch = { 'image': [], 'label': [] }
        if len(batch['image']) > 0:
            yield batch

模型

计算部分

LeNet

一层CNN一层ReLU 一口咸菜一口粥

两遍, 然后全连接两层 (中间有一个隐层).

线性结构, CNTK自带Sequential化简.

def create_model(features):
    with C.layers.default_options(init=C.glorot_uniform(), activation=C.relu):
        h = features
        h = C.layers.Convolution2D(filter_shape=(5,5),
                num_filters=8,
                strides=(2,2),
                pad=True, name='first_conv')(h)
        h = C.layers.Convolution2D(filter_shape=(5,5),
                num_filters=16,
                strides=(2,2),
                pad=True, name='second_conv')(h)
        r = C.layers.Dense(10, activation=None, name='classify')(h)
        return r

Loss部分

不管是啥抄 Tutorial 就好.

一个简单的交叉熵. 输入是模型的输出 (十维向量) 和标签 (One-hot 的十维向量)

def create_criterion_function(model, labels):
    loss = C.cross_entropy_with_softmax(model, labels)
    errs = C.classification_error(model, labels)
    return loss, errs

拼起来部分

def main(is_test = False):
    x = C.input_variable((1, 28, 28), name = 'image')
    y = C.input_variable((10, ), name = 'label')
    z = create_model(x)
    model = z(x / 255)
    loss, label_error = create_criterion_function(model, y)

训练验证测试

训练

训练一个epoch的过程是: 把一个batch的数据正向跑一遍, 反向算梯度, 更新权重.

使用自带的 learnertrainer 即可.

        learning_rate = 0.2
        lr_schedule = C.learning_parameter_schedule(learning_rate)
        learner = C.sgd(z.parameters, lr_schedule)
        trainer = C.Trainer(z, (loss, label_error), [learner])

训练一个epoch的过程就是把数据读进来喂给trainer的无脑过程.

def train_epoch(loss, learner, trainer):
    i = 0
    for batched_data in csv_reader('train.csv', batch_size = 64):
        trainer.train_minibatch(batched_data)
        training_loss = trainer.previous_minibatch_loss_average
        i += 1
        if i % 100 == 0:
            print('Trained %d batches, loss = %e' % (i, training_loss))

验证

把一部分数据拿来算一遍精度, 实时监测训练进度, 防止过拟合.

算精度相当于把训练结果拿出来. 使用 model.eval 函数.

善用np.arrayargmax函数.

def validate(model):
    i = 0
    tot = 0.
    corr = 0.
    for batched_data in csv_reader('train.csv', batch_size = 64):
        data = { 'image': batched_data['image'] }
        ans = model.eval(data)
        for posbs, answer in zip(ans, batched_data['label']):
            x = np.array(posbs).argmax()
            corr += 1. if x == answer.argmax() else 0.
            tot += 1.
        i += 1
        if i % 100 == 0:
            print('Tested %d batches' % (i))
    print('Accuracy = %.7f' % (corr / tot))
    return corr / tot

测试

跑测试集, 把答案写进result.csv. 和 Validate 大同小异.

def test(model):
    with open('result.csv', 'w') as f:
        f.write('ImageId,Label\n')
        base_id = 1
        i = 0
        for batched_data in csv_reader('test.csv', batch_size = 256, is_test = True):
            data = { 'image': batched_data['image'] }
            ans = model.eval(data)
            for j, a in enumerate(ans):
                f.write('%d,%d\n' % (j + base_id, np.array(a).argmax()))
            base_id += len(ans)
            i += 1
            if i % 10 == 0:
                print('Inferred %d images' % base_id)

总过程

重复以下过程: 训练, 验证. 找到一个不错的模型就存下来.

直到你觉得OK了就把你觉得OK的模型Load进来跑测试.

trainersave_checkpointload_from_checkpoint 函数. 参数就是文件名. 很方便.

def main(is_test = False):
    x = C.input_variable((1, 28, 28), name = 'image')
    y = C.input_variable((10, ), name = 'label')
    z = create_model(x)
    model = z(x / 255)
    loss, label_error = create_criterion_function(model, y)
    if not is_test:
        max_epoch = 10
        learning_rate = 0.2
        lr_schedule = C.learning_parameter_schedule(learning_rate)
        learner = C.sgd(z.parameters, lr_schedule)
        trainer = C.Trainer(z, (loss, label_error), [learner])
        if os.path.isfile('lenet.pm'):
            trainer.restore_from_checkpoint('lenet.pm')
        best_acc = validate(model)
        for i in range(max_epoch):
            print('Running epoch %d' % i)
            train_epoch(loss, learner, trainer)
            acc = validate(model)
            trainer.save_checkpoint('lenet.pm')
            if acc > best_acc:
                trainer.save_checkpoint('lenet_best.pm')
    else:
        if os.path.isfile('lenet.pm'):
            trainer.restore_from_checkpoint('lenet.pm')
        test(model)

思考题和讨论

验证集的划分

我没划分直接在训练集上跑的. 肯定不对. 划分也不难.

模型复杂化

只用修改 create_model 函数就行了.

调参

程序里写的常数都能调. 比如 lr. 模型里还有一些默认的参数, 详见文档.

性能优化

CNTK自带优先在GPU上跑.

GPU加速有多快? 也就几十倍.?

hint

这回提供的代码我良心地跑过了且确实能跑能用.

屠龙宝刀点击就送