鸢尾花分类问题

本文为 TensorFlow 入门案例,我们一起来探讨经典的鸢尾花分类问题 (Iris Flowers Dataset).

定义问题

假设你是一个植物学家,想将鸢尾花自动分类。机器学习提供了多种分类算法。比如,优秀的分类算法通过图像识别对花进行分类。而我们不想止步于此,我们想要在仅知道花瓣、花萼的长度以及宽度的情况下对花进行分类。

鸢尾花专家能识别出 300 多个花种,不过我们的程序目前在以下三种中进行分类:

从左至右,Iris setosa (by Radomil, CC BY-SA 3.0),Iris versicolor (by Dlanglois, CC BY-SA 3.0) 和 Iris virginica (by Frank Mayfield,CC BY-SA 2.0)。

我们找来 Iris 数据集,包含 120 条带有花萼、花瓣测量的数据。 Iris 数据集的前 5 行如下:

花萼长度 花萼宽度 花瓣长度 花瓣宽度 种属
6.4 2.8 5.6 2.2 2
5.0 2.3 3.3 1.0 1
4.9 2.5 4.5 1.7 2
4.9 3.1 1.5 0.1 0
5.7 3.8 1.7 0.3 0

我们首先来介绍一些基本术语:

每个标记都是一个字符串,但由于机器学习通常使用数字,因而我们将每个字符串与数字相对应,对应关系如下:

模型训练

模型 (model) 可以看作是特征与标记之间的关系。在鸢尾花问题中,模型定义了花萼花瓣测量数据与花种属之间的关系。有时短短几行代数符号就可以描述一个简单的模型;而有些复杂的模型包含大量的数学符号与复杂的变量关系,很难数字化表达。

现在问题来了:四个特征,一个花种属标记,你能在不使用机器学习的情况下,定义它们之间的关系么?换句话问,你能使用传统的程序语言 (比如大量诸如 if/else 的条件语句) 来创建模型么?有这个可能。如果你有大把的时间研究数据集,最终也许会找到花萼花瓣与花种属之间的关系。然而,一个好的机器学习算法能够为你预测模型。只要你有足够多、足够有代表性的数据,套用适当的模型,最终程序会帮你完美定义花种属与花萼花瓣的关系。

训练 (training) 是监督式机器学习的一个阶段,是模型逐渐优化 (自我学习) 的过程。 鸢尾花问题是有监督学习的一个典型,这类模型通过标记的样本数据训练得出。 还有一类机器学习:无监督学习。这类样本模型是未标记的,模型只通过特征寻找规律。

程序代码

新建文件 get_started/premade_estimator.py, 完整代码如下

import argparse
import os.path as osp
from dataclasses import dataclass

import pandas as pd
import tensorflow as tf
from tensorflow import keras

CSV_COLUMN_NAMES = ['SepalLength', 'SepalWidth', 'PetalLength', 'PetalWidth', 'Species']
SPECIES = ['Setosa', 'Versicolor', 'Virginica']


@dataclass
class Context:
    batch_size: int
    train_steps: int
    base_url: str = 'http://download.tensorflow.org/data'
    train_file: str = 'iris_training.csv'
    test_file: str = 'iris_test.csv'


def parse_args():
    parser = argparse.ArgumentParser(description='iris classifier')
    parser.add_argument('--batch_size', type=int, default=100, help='batch size')
    parser.add_argument('--train_steps', type=int, default=1000, help='number of training steps')
    args, _ = parser.parse_known_args()
    return Context(
        batch_size=args.batch_size,
        train_steps=args.train_steps
    )


def maybe_download():
    train_path = keras.utils.get_file(ctx.train_file, osp.join(ctx.base_url, ctx.train_file))
    test_path = keras.utils.get_file(ctx.test_file, osp.join(ctx.base_url, ctx.test_file))
    return train_path, test_path


def load_data(y_name=CSV_COLUMN_NAMES[-1]):
    train_path, test_path = maybe_download()
    train = pd.read_csv(train_path, names=CSV_COLUMN_NAMES, header=0)
    train_x, train_y = train, train.pop(y_name)

    test = pd.read_csv(test_path, names=CSV_COLUMN_NAMES, header=0)
    test_x, test_y = test, test.pop(y_name)
    return (train_x, train_y), (test_x, test_y)


def train_input_fn(features, labels, batch_size):
    features = dict(features)
    inputs = (features, labels)
    dataset = tf.data.Dataset.from_tensor_slices(inputs)
    return dataset.shuffle(1000).repeat().batch(batch_size)


def eval_input_fn(features, labels, batch_size):
    features = dict(features)
    if labels is None:
        inputs = features
    else:
        inputs = (features, labels)
    dataset = tf.data.Dataset.from_tensor_slices(inputs)
    return dataset.batch(batch_size)


def main():
    print(ctx)
    (train_x, train_y), (test_x, test_y) = load_data()
    feature_columns = [tf.feature_column.numeric_column(key=key) for key in train_x.keys()]
    classifier = tf.estimator.DNNClassifier(feature_columns=feature_columns, hidden_units=[10, 10], n_classes=3)
    classifier.train(input_fn=lambda: train_input_fn(train_x, train_y, ctx.batch_size), steps=ctx.train_steps)
    eval_result = classifier.evaluate(input_fn=lambda: eval_input_fn(test_x, test_y, ctx.batch_size))
    print('Test set accuracy: {accuracy:0.3f}'.format(**eval_result))

    expected = ['Setosa', 'Versicolor', 'Virginica']
    predict_x = {
        'SepalLength': [5.1, 5.9, 6.9],
        'SepalWidth': [3.3, 3.0, 3.1],
        'PetalLength': [1.7, 4.2, 5.4],
        'PetalWidth': [0.5, 1.5, 2.1],
    }
    predictions = classifier.predict(input_fn=lambda: eval_input_fn(predict_x, None, ctx.batch_size))
    template = 'Prediction is"{}" ({:.2f}%), expected"{}"'
    for predict, expect in zip(predictions, expected):
        class_id = predict['class_ids'][0]
        probability = predict['probabilities'][class_id]
        print(template.format(SPECIES[class_id], 100 * probability, expect))


if __name__ == '__main__':
    ctx = parse_args()
    main()

假设你已经按上一节完成了 TensorFlow 运行环境的搭建。 运行程序输出如下 (已删除无关重要的系统信息输出):

$ python3 -m get_started.premade_estimator
Context(batch_size=100, train_steps=1000, base_url='http://download.tensorflow.org/data', train_file='iris_training.csv', test_file='iris_test.csv')
Test set accuracy: 0.975
Prediction is "Setosa" (99.50%), expected "Setosa"
Prediction is "Versicolor" (99.35%), expected "Versicolor"
Prediction is "Virginica" (98.76%), expected "Virginica"

接下来,解读一下代码,从这份示例中可以抽取出一个最基本的模板 (后面会持续完善这个模板).

import argparse
from dataclasses import dataclass


@dataclass
class Context:
    batch_size: int


def parse_args():
    parser = argparse.ArgumentParser(description='arguments')
    parser.add_argument('--batch_size', type=int, default=100, help='batch size')
    args, _ = parser.parse_known_args()
    return Context(
        batch_size=args.batch_size
    )


def main():
    print(ctx)


if __name__ == '__main__':
    ctx = parse_args()
    main()

进一步,我们来看具体的步骤

引入数据集并解析

我们在 load_data 中引入了两个 csv 数据文件:

训练数据集和测试数据集在最开始是在同一个数据集中,后来该样本数据集被处理:其中的大部分作为训练数据、剩余部分作为测试数据。增加训练集样本数量通常能构造出更好的模型,而增加测试集样本的数量能够更好的评估模型。

Keras 是一个开源机器学习库,这里 from tensorflow import keras 引入的是 TensorFlow 对 Keras 的实现。 keras.utils.get_file 方法,使拷贝远程 CSV 文件到本地系统更便捷。

Pandas 是一个开源的 Python 库,Pandas 的 DataFrame 是类似表的数据结构,每一列有列头,每一行有行标。下例为 train.head()

   SepalLength  SepalWidth  PetalLength  PetalWidth  Species
0          6.4         2.8          5.6         2.2        2
1          5.0         2.3          3.3         1.0        1
2          4.9         2.5          4.5         1.7        2
3          4.9         3.1          1.5         0.1        0
4          5.7         3.8          1.7         0.3        0

创建特征列描述数据

特征列可以看作是一个数据结构 (Data Schema, Data Field Type),为你的模型解释每一个特征的数据。在鸢尾花问题中,我们想让模型将每一特征按照字面浮点值解释。就是说,我们希望模型将 5.4 这样的输入值直接解析为 5.4。而在某些机器学习问题中,我们喜欢将数据解析得不那么直接。特征列数据解释是一个很深的话题,我们将用一整篇文档详细描述。

从代码中来看,通过调用 tf.feature_column 模块函数创建了一个 feature_column 对象列表。每个对象描述了模型的一个输入。我们想要模型以浮点数值解释数据,可以调用 tf.feature_column.numeric_column 函数。在示例中,四列特征被直接解释为字面浮点数值,程序创建了特征列如下 (打印 feature_columns 变量):

NumericColumn(key='SepalLength', shape=(1,), default_value=None, dtype=tf.float32, normalizer_fn=None)
NumericColumn(key='SepalWidth', shape=(1,), default_value=None, dtype=tf.float32, normalizer_fn=None)
NumericColumn(key='PetalLength', shape=(1,), default_value=None, dtype=tf.float32, normalizer_fn=None)
NumericColumn(key='PetalWidth', shape=(1,), default_value=None, dtype=tf.float32, normalizer_fn=None)

选择模型类型

模型有很多,但找到最理想的模型需要一定的经验。我们选择神经网络来解决鸢尾花问题。 通过神经网络可以找到特征和标记间的复杂关系。神经网络是一个高度结构化的图,由一个或多个隐藏层组成。每个隐藏层包含一个或多个神经元。神经网络有不同的类别。这里我们使用全连接神经网络,就是说:每一层中神经元的输入,来自于上一层的所有神经元。举个例子,下图阐述的全连接神经网络,它包含 3 个隐藏层:

我们通过实例化一个 Estimator 类来指定模型类型。TensorFlow 提供两类 Estimator:

在示例程序中,使用预定义 DNNClassifier 构建神经网络将样本分类。实例化 DNNClassifier 代码如下

classifier = tf.estimator.DNNClassifier(
    feature_columns=feature_columns,
    hidden_units=[10, 10],
    n_classes=3)

理想的层数/神经元数量是由数据集或问题本身决定的。正如同机器学习领域的其它方方面面,选择好神经网络的形状,需要大量实验和多方面的知识储备。根据经验法则,增加隐藏层数量/神经元数量往往能构造更强大的模型,这需要更多数据的有效训练。

输入层/隐藏层/输出层

我们习惯说的神经网络的层数,是不包含第一层 (输入层), 以及最后一层 (输出层).

DNNClassifier 的构造函数有一个可选参数 optimizer 优化器,在这里我们的程序没有声明。优化器控制着模型怎样训练。当你在机器学习领域深入,优化器和学习率 (learning rate) 将会变的很重要。

训练模型

实例化 DNNClassifier, 只是搭建了一个模型的框架。 换句话说,我们织好了一张网络,但还没有载入数据 (调整权重). 通过调用 estimator 对象的 train 方法训练神经网络。如下:

classifier.train(
    input_fn=lambda: train_input_fn(train_x, train_y, ctx.batch_size),
    steps=ctx.train_steps)

train_input_fn 函数依赖于 Dataset API。这是一个高层 TensorFlow API,用于读取数据并转化成 train 方法所需的格式。

dataset = tf.data.Dataset.from_tensor_slices((dict(features), labels))
dataset = dataset.shuffle(buffer_size=1000).repeat().batch(batch_size=batch_size)
import tensorflow as tf

tf.enable_eager_execution()
dataset = tf.data.Dataset.from_tensor_slices([1, 2, 3, 4, 5])
batch = dataset.shuffle(10).batch(2).make_one_shot_iterator().get_next()
print(batch)

上述代码每次执行结果不一样,比如 tf.Tensor([1 2], shape=(2,), dtype=int32)

在初始化 DNNClassifier 实例时,增加可选参数 model_dir='./model', 然后执行代码 python -m get_started.premade_estimator --train_steps 7500, 将在代码目录下,新增 model 文件夹,通过 tensorboard 进行可视化,命令 tensorboard --logdir./model. 就可以通过 http://localhost:6006 访问到 loss 的可视化结果了,其效果如下图所示,loss 咔咔往下掉,看起来效果还不错。

评估模型

评估用来判断模型预测结果的有效性。为了评价鸢尾花分类模型的有效性,我们向模型传入一些花瓣花萼的测量值,让其预测传入数据的花种属,然后对比模型的预测结果与实际标记。为了评估模型的有效性,每个 estimator 都提供了 evaluate 方法。

eval_result = classifier.evaluate(
    input_fn=lambda: eval_input_fn(test_x, test_y, ctx.batch_size))

调用 classifier.evaluateclassifier.train 类似。最大的区别在于 classifier.evaluate 需要从测试数据集获取数据,而非训练数据集。换句话说,为了公平地评估模型的有效性,用来评估模型的样本和用于训练的样本必须不同。我们通过调用 eval_input_fn 函数处理了测试集的一批样本。

使用训练后的模型进行预测

现在我们训练好模型,而且“证明”了在鸢尾花分类问题中它还不错,虽然并不完美。现在我们用训练的模型在无标记样本 (没有标记仅有特征的样本) 上做预测;

predictions = classifier.predict(
    input_fn=lambda: eval_input_fn(predict_x, None, ctx.batch_size))

同 evaluate 方法一样,predict 方法通过 eval_input_fn 收集样本,只是这里不传入标记。

参考资料

相关推荐