TensorFlow官方最新的教程原文翻译。

本文档阐述了如何使用机器学习来对鸢尾属植物的种类进行分类(分组)。本文深入TensorFlow的代码来了解它做了什么,并解释一些机器学习的基础知识。

如果符合以下情况,那么本文档就比较适合你:

  • 你对机器学习完全不了解或了解较少。
  • 你想学习如何编写TensorFlow的代码。
  • 你可以用Python编程(至少有一些了解)。

如果你对机器学习很精通但是刚刚接触TensorFlow,可以阅读开始使用TensorFlow:针对机器学习专家这篇文档。

鸢尾属植物分类问题

把自己想象成一个植物学家,你想找到一种方法来自动分辨出每个遇到鸢尾花的种类。机器学习提供了很多分类花的方法。比如一个复杂的机器学习程序可以基于花的照片来给它分类。我们的目标要求没有那么高——只想是想通过鸢尾属花的萼片花瓣的长宽来进行分类。

鸢尾属植物大概有300个种类,但是我们的程序只会对以下三种进行分类:

  • 山鸢尾
  • 维吉尼亚鸢尾
  • 变色鸢尾

鸢尾植物

从左至右依次为山鸢尾(来源于BY-SA 3.0 Radomil)、维吉尼亚鸢尾(源于BY-SA 3.0 Radomil)和变色鸢尾(来源于BY-SA 2.0 Radomil)

幸运的是一些人已经创建了一个包含萼片和花瓣数据的120鸢尾花的数据集。这个数据集已经成为机器学习分类问题的经典入门题。(MNIST数据集是包含手写数字的另外一个热门分类问题。)以下有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 2.5 4.5 1.7 0
5.7 3.8 1.7 0.3 0

让我们介绍一些术语:

  • 最后一列(种类)称为标签;前面四个列称为特征。特征是样本的特点,而标签是我们要进行预测的事物。
  • 一个样本包含了一组特征和花种类的标签。前面的表格显示了120个样本中的5个。

每个标签本来是一些字符串(比如“山鸢尾”),但是机器学习一般要依靠数字值的计算。因此一些人就将字符串映射成数字。下面就是与数字的对应关系:

  • 0 代表山鸢尾
  • 1 代表维吉尼亚鸢尾
  • 2 代表变色鸢尾

模型和训练

一个模型就是特征与标签之前的关系。对于鸢尾分类问题,模型定义了萼片和花瓣度量值与预测鸢尾种类之间的关系。一些简单的模型通过几行代数公式就能描述清楚,但是复杂机器学习模型有大量的参数很难描述清楚。

可以使用机器学习来决定四个特征与鸢尾种类的关系吗?就是说可以使用传统的编程技术(比如大量的条件判断)来构建一个模型吗?可能。你需要处理很长的数据来决定正确的萼片花瓣度量与特定种类之间的关系。但是好的机器学习方法会为你挑选模型。也就是说如果你提供了足够有代表性的样本给了机器学习类型的模型,程序会决定萼片、花瓣和种类之间的关系。

训练是机器学习一个阶段,在这个阶段中模型会逐步优化(学习)。鸢尾种类问题是一个监督学习的例子,会通过包含标签的样本中进行训练。(无监督学习中样本没有标签,模型通常是发现特征中的模式。)

获得事例程序

在获得本文档事例代码之前,进行以下操作:

  1. 安装TensorFlow。
  2. 如果使用virutalenv或者Anaconda安装的TensorFlow,激活你TensorFlow环境。
  3. 安装或者升级pandas使用如下命令:pip install pandas

使用如下步骤获得事例程序:

  1. 从github上克隆TensorFlow仓,输入以下命令:git clone https://github.com/tensorflow/models
  2. 改变目录到本文档使用的代码:cd models/samples/core/get_started/

get_started目录下你会找到premade_estimator.py程序。

运行事例程序

运行TensorFlow程序就像运行其他的Python程序一样。因此可以运行如下命令:

python premade_estimator.py

运行程序会有输出,最后结束时会有三行预测值,如下:

...
Prediction is "Setosa" (99.6%), expected "Setosa"

Prediction is "Versicolor" (99.8%), expected "Versicolor"

Prediction is "Virginica" (97.9%), expected "Virginica"

如果程序没有输出预测而产生了一些错误,来看一下是否有以下问题:

  • 正确安装了TensorFlow吗?
  • 使用了正确的TensorFlow版本吗?程序premade_estimators.py需要至少1.4以上的版本。
  • 如果使用virtualenv或者Anaconda安装TensorFlow,激活环境了吗?

TensorFlow程序堆栈

如下图所示TensorFlow提供一个包含多层API的程序堆栈:

TensorFlow堆栈

TensorFlow程序环境

如果你刚开始编写TensorFlow程序,我们强烈建议你关注以下两个高层级的API:

  • Estimator
  • Dataset

使用其他的API也很方便,但本文档将关注这两个API。

程序本身

感谢你的耐心,让我们深入讲解一下代码。premade_estimator.py大体的架构——其他很多TensorFlow程序也类似:

  • 导入并解析数据集
  • 构建特征列描述数据
  • 选择模型的类型
  • 训练模型
  • 评估模型的有效性
  • 让训练好的模型作出预测

以下部分将详细展开。

导入和解析数据集

鸢尾分类程序需要的数据来源于两个.csv文件:

  • http://download.tensorflow.org/data/iris_training.csv,包含了训练集。
  • http://download.tensorflow.org/data/iris_test.csv,包含了测试集。

训练集包含了我们要训练模型用的样本;测试数据集包含了我们用来评估训练好模型性能的样本。

训练集和测试集本来是一个数据集。有人将它分开,一大部分用于训练集一小部分用于测试集。增加更多的样本一般有助于构建更好的模型,但是增加更多的样本到测试集能更好的衡量模型的有效性。不管是否已经被分开,测试集和训练集的样本必须分开。否则你可能无法验证模型的有效性。

premade_estimators.py程序依赖相邻iris_data.py文件中load_data函数读取和解析训练集和测试集。这里有一个更多注释的版本:

TRAIN_URL = "http://download.tensorflow.org/data/iris_training.csv"
TEST_URL = "http://download.tensorflow.org/data/iris_test.csv"

CSV_COLUMN_NAMES = ['SepalLength', 'SepalWidth',
                    'PetalLength', 'PetalWidth', 'Species']

...

def load_data(label_name='Species'):
    """Parses the csv file in TRAIN_URL and TEST_URL."""

    # Create a local copy of the training set.
    train_path = tf.keras.utils.get_file(fname=TRAIN_URL.split('/')[-1],
                                         origin=TRAIN_URL)
    # train_path now holds the pathname: ~/.keras/datasets/iris_training.csv

    # Parse the local CSV file.
    train = pd.read_csv(filepath_or_buffer=train_path,
                        names=CSV_COLUMN_NAMES,  # list of column names
                        header=0  # ignore the first row of the CSV file.
                       )
    # train now holds a pandas DataFrame, which is data structure
    # analogous to a table.

    # 1. Assign the DataFrame's labels (the right-most column) to train_label.
    # 2. Delete (pop) the labels from the DataFrame.
    # 3. Assign the remainder of the DataFrame to train_features
    train_features, train_label = train, train.pop(label_name)

    # Apply the preceding logic to the test set.
    test_path = tf.keras.utils.get_file(TEST_URL.split('/')[-1], TEST_URL)
    test = pd.read_csv(test_path, names=CSV_COLUMN_NAMES, header=0)
    test_features, test_label = test, test.pop(label_name)

    # Return four DataFrames.
    return (train_features, train_label), (test_features, test_label)

Keras是一个开源的机器学习库,tf.keras是TensorFlow实现的Keras。premade_estimator.py程序只使用了一个tf.keras函数,名为tf.keras.utils.get_file工具函数,用来复制远程CSV文件到本地文件系统上。

调用load_data返回两个(featurelabel)对儿,分别用于训练和测试集:

    # Call load_data() to parse the CSV file.
    (train_feature, train_label), (test_feature, test_label) = load_data()

Pandas是使用了几个TensorFlow函数的开源Python库。一个pandas的DataFrame是一个命名列名称和几个行的表。特征被load_data函数返回并打包在DataFrams中。例如test_feature DataFrame看起来如下:

    SepalLength  SepalWidth  PetalLength  PetalWidth
0           5.9         3.0          4.2         1.5
1           6.9         3.1          5.4         2.1
2           5.1         3.3          1.7         0.5
...
27          6.7         3.1          4.7         1.5
28          6.7         3.3          5.7         2.5
29          6.4         2.9          4.3         1.3

数据的描述

特征列是一种数据结构,它是用来描述模型怎样如何解释每个特征中的数据。在鸢尾问题中我们想要模型每个特征的文字直接解释为浮点值,也就是说我们想要模型把输入的值比如5.4解释成数字5.4。但是其他机器学习问题通常很少这样直接使用特征的值,都需要做一定的转换。使用特征列来解释数据是一个很大的话题,需要继续深入的阅读这个文档来了解。

从代码的角度看,你通过tf.feature_column构建了一个feature_column对象列表。每个对象描述了一个模型的输入。并告诉模型将数据解释为浮点值,调用tf.feature_column.numeric_column。在premade_esimator.py中,所有特征应该直接解释为浮点值,所以使用如下代码来创建一个特征列:

# Create feature columns for all features.
my_feature_columns = []
for key in train_x.keys():
    my_feature_columns.append(tf.feature_column.numeric_column(key=key))

这里有另外一种写法,虽然不是很优雅但是会更清晰一些:

my_feature_columns = [
    tf.feature_column.numeric_column(key='SepalLength'),
    tf.feature_column.numeric_column(key='SepalWidth'),
    tf.feature_column.numeric_column(key='PetalLength'),
    tf.feature_column.numeric_column(key='PetalWidth')
]

选择模型的类型

我们需要选择要训练模型的种类。有很多模型的类型,使用哪一种理想类型需要经验。我们选择神经网络来解决鸢尾问题。神经网络可以发现特征和标签之间复杂的关系。一个神经网络是高度结构化的计算图,通过一个或多个隐藏层组织起来的。每个隐藏层包含一个或着多个神经元。有几种类型的神经网络,我们这里要使用全连接神经网络,这就意味着在一层的神经元会将所有前一层每个神经元的输出作为输入。举个例子,下图演示了一个全连接神经网络由三个隐藏层组成:

  • 第一个隐藏层包含四个神经元
  • 第二个隐藏层包含三个神经元
  • 第三个隐藏层包含两个神经元

神经网络

一个含有三个隐藏层的神经网络

在实例化Estimator类的时候指定模型的类型。TensorFlow提供了两种类别的Estimator:

实施一个神经网络,premade_estimators.py程序使用了一个预制的Estimator,名为tf.estimator.DNNClassifier。这个Estimator构建一个对样本分类的神经网络。以下调用实例化DNNClassifer

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

使用hidden_unites参数定义在每个隐藏层的神经元的数量。参数使用一个列表,比如:

        hidden_units=[10, 10],

指定列表的长度要等于隐藏层的数量(这里是2)。列表中每个数字代表了特定隐藏层神经元的数量(第一个隐藏层和第二个都是是10)。改变隐藏层或者神经元的数量,只要指定不同的列表值给参数hidden_unites即可。

理想的层数和神经元数量依赖于你的问题和数据集。与其他机器学习的方面类似,选择理想型号的神经网络需要知识和经验的积累。一个大体的指引是,增加隐藏层和神经元的数量通常会构建一个更强大的模型,需要对更多的数据进行有效的训练。

n_classes参数是指定神经网络可以预测的可能值的数量。因为鸢尾问题需要分出3种鸢尾种类,我们设置n_classes为3。

构造器tf.Estimator.DNNClassifier使用了另外一个可选参数optimizer,我们的样例代码没有设置。optimizer控制了模型如何训练。当你更为专业的开发机器学习代码时,优化器和学习率会变得非常重要。

训练模型

tf.Estimator.DNNClassifier实例化构建了一个学习模型的框架。基本上说我们只是连接了一个网络,还没有让数据在其中流动起来。训练神经网络,调用Estimator对象的train方法。例如:

    classifier.train(
        input_fn=lambda:train_input_fn(train_feature, train_label, args.batch_size),
        steps=args.train_steps)

steps参数告诉train在特定数量迭代后停止训练。增加steps就会延长模型的训练时间,另外更长时间训练并不一定保证会得到更好的模型。args.train_steps默认值时1000。训练的步数是你可以优化的超级参数。选择合适的步数通常需要经验和试验来验证。

input_fn参数指定了提供训练数据的函数。这个调用train方法指定了train_input_fn函数会提供训练数据。下面是这个函数的定义签名:

def train_input_fn(features, labels, batch_size):

我们传递以下参数给train_input_fn

  • train_feature是一个Python词典类型,其中:
    • 每个键以特征的名称命名。
    • 每个值是一个数组包含了在训练集中每个样本的值。
  • train_label是一个数组包含了训练集中每个样本的标签数据。
  • args.batch_size是一个整型值定义了批次数量

train_input_fn函数依赖于Dataset API。这是个高层级的TensorFlow API来读取数据并转换成train方法需要的形式。下面调用将输入的特征和标签转换为tf.data.Dataset对象,就是Dataset API的基础类:

    dataset = tf.data.Dataset.from_tensor_slices((dict(features), labels))

tf.dataset类提供了很多有用函数来准备训练用的样本。下面的代码调用了三个这样的函数:

    dataset = dataset.shuffle(buffer_size=1000).repeat(count=None).batch(batch_size)

训练网络最好的方式是将训练样本按随机方式的输入。随机样本的输入需要调用tf.data.Dataset.shuffle。设置buffer_size成一个比样本数量(120)更大的值确保数据可以很好的洗牌。

在训练时,train方法通常处理样本很多次,调用tf.data.Dataset.repeat方法,不需要任何参数确保train方法无限制(没有再经过洗牌)的提供训练集的样本。

train方法每次处理一个样本批次tf.data.Dataset.batch方法创建一个由多个样本连接起来的批次。这些程序使用默认的批次数量是100,意味着batch方法将100个样本连接成一组。理想的批次数量依赖于要解决的问题。一个大体的指引是更小的批次数量会让train方法训练模型更快但是要牺牲(某些时候)准确率。

下面return声明将一个批次的样本返回给调用(train方法)。

   return dataset.make_one_shot_iterator().get_next()

评估模型

评估意思是查明模型作出预测的有效性。查明鸢尾分类模型的有效性,传递一些萼片和花瓣的度量值给模型,让模型作出预测鸢尾植物代表的种类。然后比较模型预测与实际标签。例如一个模型挑出了一半输入样本的正确种类,那么准确率就是0.5。下面的模型就会更有效一些:

测试集
特征 标签 预测
5.9 3 4.3 1.5 1 1
6.9 3.1 5.4 2.1 2 2
5.1 3.3 1.7 0.5 0 0
6 3.4 4.5 1.6 1 2
5.5 2.5 4 1.3 1 1

一个模型预测的准确率为80%

验证一个模型的有效性,每个Estimator提供了一个evaluate方法。premade_estimator.py程序调用evaluate方法如下:

# Evaluate the model.
eval_result = classifier.evaluate(
    input_fn=lambda:eval_input_fn(test_x, test_y, args.batch_size))

print('\nTest set accuracy: {accuracy:0.3f}\n'.format(**eval_result))

调用classifier.evaluate和调用classifier.train类似。最大的不同是classifier.evaluate必须是使用验证集的数据而不是训练集的数据。换句话说,为了验证模型有效性的公平,证模型的样本必须与训练用的样本是不同的。eval_input_fn函数会从验证集中获取批次样本。以下是eval_input_fn函数:

def eval_input_fn(features, labels=None, batch_size=None):
    """An input function for evaluation or prediction"""
    if labels is None:
        # No labels, use only features.
        inputs = features
    else:
        inputs = (features, labels)

    # Convert inputs to a tf.dataset object.
    dataset = tf.data.Dataset.from_tensor_slices(inputs)

    # Batch the examples
    assert batch_size is not None, "batch_size must not be None"
    dataset = dataset.batch(batch_size)

    # Return the read end of the pipeline.
    return dataset.make_one_shot_iterator().get_next()

大体上说eval_input_fn在调用classifier.evaluate时机型以下操作:

  1. 将测试集的特征和标签转换成tf.dataset对象。
  2. 为测试创建一个样本批次(这里不需要对测试集样本进行洗牌或者重复调用)。
  3. 返回测试样本批次给classifier.evaluate

运行这些代码会得到以下输出(或者近似的):

Test set accuracy: 0.967

0.967的准确率是指我们训练过的模型会正确预测测试集中30个鸢尾种类中的29个。

预测

现在我们已经训练了一个模型并“证明”它可以但不是非常完美的对鸢尾种类进行分类。现在让我们使用训练过的模型来做一些没有标签样本的预测,也就是这些样本只有特征没有标签。

在现实世界中没有标签的样本可能会来源于各种途径,包括APP、CSV文件和数据输入。今天我们只是简单的用手工方式提供以下三个没有标签的样本:

    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],
    }

每个Estimator提供了一个predict方法,premade_estimator.py如下调用:

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

evaluate方法一样我们的predict方法也从eval_input_fn方法中收集样本。

当我们进行预测的时候是不会传递标签给eval_input_fn方法的。因此,eval_input_fn会做如下操作:

  1. 对特征值3个元素进行转换。
  2. 从手工样本中创建一个3个样本的批次。
  3. classifier.predict返回这些样本批次。

predict方法返回了一个python的迭代器,为每个样本产生了一个字典类型的预测值。这个字典包含几个键。probabilities键中保存了一个三个浮点值组成的列表,每个代表了输入样本针对不同鸢尾种类的可能性。比如下面的probabilities列表:

'probabilities': array([  1.19127117e-08,   3.97069454e-02,   9.60292995e-01])

上面的列表指出:

  • 可以忽略不计这是一个山鸢尾
  • 3.97%可能是一个变色鸢尾
  • 96.0%可能是一个弗吉尼亚鸢尾

一个class_ids键包含了一个元素的数组指出了最可能的种类,比如:

'class_ids': array([2])

数字2对应了弗吉尼亚鸢尾。以下代码迭代返回predictions来报告每个预测:

for pred_dict, expec in zip(predictions, expected):
    template = ('\nPrediction is "{}" ({:.1f}%), expected "{}"')

    class_id = pred_dict['class_ids'][0]
    probability = pred_dict['probabilities'][class_id]
    print(template.format(iris_data.SPECIES[class_id], 100 * probability, expec))

运行程序产生如下输出:

...
Prediction is "Setosa" (99.6%), expected "Setosa"

Prediction is "Versicolor" (99.8%), expected "Versicolor"

Prediction is "Virginica" (97.9%), expected "Virginica"

总结

本文档提供了一个机器学习的简短介绍。

由于premade_estimators.py依赖高层级的API,机器学习复杂的数学原理被隐藏了。如果你想要对机器学习更为了解,我们建议还是要学习梯度递减、批次和神经网络的概念。

我们建议下一步阅读特征列这篇文档,解析了在机器学习中如何使用它来表示不同的数据。