AAAMLP-Chapter-4: Arrange Machine Learning Projects

94 阅读4分钟

终于,我们来到了构建第一个机器学习模型的阶段。

在开始前,需要注意几点。

我们要在 IDE 里开发而不是 jupyter notebook,当然你也可以在 notebook 里做,这完全取决于你。我只会在数据探索 data exploration 和绘制图表时使用 notebook。

我们接下来会构建一个分类框架,因为分类任务时最常见的问题。完成构建之后,你可以在不怎么改变代码的情况训练新模型,或者优化模型。

首先看一下工程文件结构,先创建工程目录,假设命名为 project 。

在 project 文件夹下,你将会看到如下结构。

下面介绍其中各个文件夹和文件都是什么。

  • input/: 包含所有机器学习任务的输入数据文件,如果是 NLP 工程,这里可以保存词嵌入;如果是图像工程,这里可以保存所有图片。

  • src/: 包含所有 python 脚本文件,即 *.py 文件。

  • models/: 包含所有训练得到的模型。

  • notebooks/: 包含所有 notebook 文件,即 *.ipynb 文件。

  • README.md: md 格式的工程介绍,介绍中应写明模型该如何训练和使用。

  • LICENSE: 声明该工程遵循何种许可。


根据数据分布选择度量指标

接下来,假设我们要在 MNIST 数据集上构建分类模型。

首先载入数据集,使用 Pandas 读取数据。

然后检查目标值的分布,代码如下。

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn import datasets, manifold

data = datasets.fetch_openml(
    'mnist_784',
    version=1,
    return_X_y=True
)
pixel_values, targets = data
targets = targets.astype(int)
target_data = pd.DataFrame(targets)

绘制的统计图如下。

从图中可清晰地看到,各个目标类型的分布非常平均,因此我们可以使用 accuracy 或者 F1 作为模型度量指标。

这是处理机器学习任务的第一步:选择度量指标。


创建 Folds

接下来在 src/ 文件夹下新建 create_folds.py,在其中编写创建 folds 的逻辑,最终将创建好的 folds 保存在 input/mnist_train_folds.csv 中。

create_folds.py 代码如下。

# create_folds.py
import pandas as pd
from sklearn import datasets, model_selection

if __name__ == '__main__':
    data = datasets.fetch_openml(
        'mnist_784',
        version=1,
        return_X_y=True
    )
    pixel_values, targets = data
    targets = targets.astype(int)

    temp = pd.DataFrame(pixel_values)
    temp.loc[:, 'target'] = targets

    temp['kfold'] = -1
    df = temp.sample(frac=1).reset_index(drop=True)
    y = df.target.values

    kf = model_selection.StratifiedKFold(n_splits=5)
    for fold, (trn_, val_) in enumerate(kf.split(X=df, y=y)):
        df.loc[val_, 'kfold'] = fold
    df.to_csv('../input/mnist_train_folds.csv', index=False)

执行 python create_folds.py 可输出 folds 文件。


训练模型

当我们选择完评估指标,并创建好 folds 之后,接下来可以进行模型训练的工作了。

在 src/ 文件夹下创建 train.py ,代码如下。

# train.py
import joblib
import pandas as pd
from sklearn import tree, metrics

def run(fold):
    df = pd.read_csv('../input/mnist_train_folds.csv')

    df_train = df[df.kfold != fold].reset_index(drop=True)
    df_valid = df[df.kfold == fold].reset_index(drop=True)

    train_x = df_train.drop('target', axis=1).values
    train_y = df_train.target.values

    valid_x = df_valid.drop('target', axis=1).values
    valid_y = df_valid.target.values

    clf = tree.DecisionTreeClassifier()
    clf.fit(train_x, train_y)

    preds = clf.predict(valid_x)
    accuracy = metrics.accuracy_score(valid_y, preds)
    print(f'Fold: {fold}, Accuracy: {accuracy}')

    joblib.dump(clf, f'../models/dt_{fold}.bin')

if __name__ == '__main__':
    for i in range(5):
        run(i)

执行 python train.py 即可开始训练模型,并保存模型。

现在回过头来查看模型训练代码,你会发现这里有一些硬编码,例如 fold 数量、训练数据读取路径、模型输出路径。

因此需要一个陪着文件 config.py 来管理这些硬编码的配置参数,代码如下。

# config.py
FOLD_NUM = 5
TRAINING_FILE = '../input/mnist_train_folds.csv'
MODEL_OUTPUT = '../models/'

然后修改上述文件中对应的硬编码部分。

现在训练脚本仍有可优化的部分,比如我们对每个 fold 都调用了 run 方法,有时并不需要在同一个脚本中训练这么多次模型,这会导致内存消耗增多,有可能导致程序崩溃。

为了解决该问题,我们可以通过 argparse 来接受外部参数。

代码修改如下。

# train.py
import argparse

if __name__ == '__main__':
    parse = argparse.ArgumentParser()
    parse.add_argument(
        '--fold',
        type=int
    )
    args = parse.parse_args()
    run(args.fold)

执行 python train.py --fold 0 可以只在第 0 个 fold 上进行模型训练。

现在训练脚本已经提升不少了,但是还是有改进的地方,比如模型的选择。

当前模型是被硬编码在训练脚本内的,我们想要从外部方便地修改要训练的模型类别。

接下来创建 model_dispatcher.py 来处理模型的选择相关逻辑。

# model_dispatcher.py
from sklearn import tree

models = {
    'decision_tree_gini': tree.DecisionTreeClassifier(
        criterion='gini'
    ),
    'decision_tree_entropy': tree.DecisionTreeClassifier(
        criterion='entropy'
    )
}

该文件定义了一个模型选择字典,根据传入的 key 来选择不同的模型。

修改 train.py ,使用上述模型选择字典。

import model_dispatcher

def run(fold, model_key):
    ...
    clf = model_dispatcher.models['model_key']
    ...


if __name__ == '__main__':
    ...
    parse.add_argument(
        '--model',
        type=str
    )
    args = parse.parse_args()
    run(args.fold, args.model)

执行 python train.py --fold 0 --model dicision_tree_gini 上述描述的基础上,选择模型。

之后,再添加模型时,只需要修改 model_dispatcher.py 文件内的模型选择字典,其他都不用变。

MNIST 问题在各个书籍或博客里都有讨论,我尝试把该问题讲的更有趣一点,并给你展示了如何写一个基础的机器学习工程框架。

在代码里,导入 model_dispatcher.py 和 config.py 时,我没有用 import * 的写法,你也不应该使用这种写法,这会导致一些问题,比如我们不能直观的看到 models 字典是哪个文件定义的,或者有可能出现命名冲突。

写优秀、已读的代码是我们的必备素质,有许多数据科学家忽视了这一点。当我们和其他人合作或分享代码时,这个素质可以帮我们和他人节省大量时间。