本文为 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 数据文件:
iris_training.csv
iris_test.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:
LinearClassifier
, DNNClassifier
, DNNLinearCombinedClassifier
, LinearRegressor
, DNNRegressor
, DNNLinearCombinedRegressor
在示例程序中,使用预定义 DNNClassifier
构建神经网络将样本分类。实例化 DNNClassifier
代码如下
classifier = tf.estimator.DNNClassifier(
feature_columns=feature_columns,
hidden_units=[10, 10],
n_classes=3)
feature_columns
特征列,定义输入模型的数据结构hidden_units
定义隐藏层的数量,以及每一隐藏层中神经元的数量,因此赋值是一个列表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)
input_fn
获得训练数据的函数,train 方法的调用通过 train_input_fn
函数获得训练数据。steps
通过多少次迭代后停止模型训练。steps 参数越大,意味着训练模型的时间越长。但训练模型时间越长,并不意味着模型更好。ctx.train_steps
的缺省值为 1000,训练的步骤数是一个可以调优的超参数。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)
from_tensor_slices
将输入的特征和标记转化为一个 tf.data.Dataset
对象,Dataset API 的基类shuffle
将样本随机化,设置 buffer_size
值大于样本数量 (120) 以确保数据洗牌效果。随机的训练样本会使训练效果更好。repeat
使 train 方法有无穷的 (通过不断随机化过程模拟) 训练样本集。batch
每步训练选取一批样本,通过 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.evaluate
和 classifier.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
收集样本,只是这里不传入标记。