原文:
annas-archive.org/md5/fdd2c78f646d889f873e10b2a8485875
译者:飞龙
第四章:线性回归
线性回归可能是统计学、机器学习和科学领域中最重要的算法之一。它是最广泛使用的算法之一,理解如何实现它及其不同变种非常重要。线性回归相较于许多其他算法的一个优势是它非常易于解释。我们为每个特征得到一个数字(系数),这个数字直接表示该特征如何影响目标(即所谓的因变量)。
例如,如果你需要预测一栋房子的售价,并且你获得了一组包含房屋特征的历史销售数据(例如地块大小、房屋质量和状况的指标,以及离市中心的距离),你可以轻松地应用线性回归。你可以通过几步获得一个可靠的估算器,所得模型也容易理解并向他人解释。事实上,线性回归首先估算一个基准值,即截距,然后为每个特征估算一个乘法系数。每个系数可以将每个特征转化为预测的正向或负向部分。通过将基准值和所有系数转化后的特征相加,你可以得到最终预测。因此,在我们的房屋售价预测问题中,你可能会得到一个正系数表示地块大小,这意味着较大的地块会卖得更贵,而离市中心的负系数则表示位于郊区的房产市场价值较低。
使用 TensorFlow 计算此类模型快速、适合大数据,并且更容易投入生产,因为它可以通过检查权重向量进行普遍的解释。
在本章中,我们将向你介绍如何通过 Estimators 或 Keras 在 TensorFlow 中实现线性回归的配方,并进一步提供更为实用的解决方案。事实上,我们将解释如何使用不同的损失函数进行调整,如何正则化系数以实现特征选择,以及如何将回归应用于分类、非线性问题和具有高基数的类别变量(高基数指的是具有许多独特值的变量)。
请记住,所有的代码都可以在 GitHub 上找到,链接:github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook
。
在本章中,我们将介绍涉及线性回归的配方。我们从使用矩阵求解线性回归的数学公式开始,然后转向使用 TensorFlow 范式实现标准线性回归及其变体。我们将涵盖以下主题:
-
学习 TensorFlow 回归方法
-
将 Keras 模型转化为 Estimator
-
理解线性回归中的损失函数
-
实现 Lasso 和 Ridge 回归
-
实现逻辑回归
-
求助于非线性解决方案
-
使用 Wide & Deep 模型
本章结束时,你会发现,使用 TensorFlow 创建线性模型(以及一些非线性模型)非常简单,利用提供的配方可以轻松完成。
学习 TensorFlow 中的线性回归方法
线性回归中的统计方法,使用矩阵和数据分解方法,非常强大。无论如何,TensorFlow 有另一种方式来解决回归问题中的斜率和截距系数。TensorFlow 可以通过迭代方式解决这些问题,即逐步学习最佳的线性回归参数,以最小化损失,就像我们在前面的章节中所看到的配方一样。
有趣的是,实际上在处理回归问题时,你不需要从零开始编写所有代码:Estimators 和 Keras 可以帮助你完成这项工作。Estimators 位于 tf.estimator
中,这是 TensorFlow 的一个高级 API。
Estimators 在 TensorFlow 1.3 中首次引入(见 github.com/tensorflow/tensorflow/releases/tag/v1.3.0-rc2
)作为 “现成 Estimators”,这些是预先制作的特定程序(例如回归模型或基本神经网络),旨在简化训练、评估、预测以及模型导出以供服务使用。使用现成的程序有助于以更简单、更直观的方式进行开发,而低级 API 则用于定制或研究解决方案(例如,当你想要测试你在论文中找到的解决方案时,或当你的问题需要完全定制的方法时)。此外,Estimators 可以轻松部署到 CPU、GPU 或 TPU 上,也可以在本地主机或分布式多服务器环境中运行,而无需对模型进行任何额外的代码更改,这使得它们适用于生产环境的现成使用场景。这也是 Estimators 不会在短期内从 TensorFlow 中消失的原因,即使 Keras,正如上一章所介绍的,是 TensorFlow 2.x 的主要高级 API。相反,更多的支持和开发将致力于 Keras 和 Estimators 之间的集成,你将很快在我们的配方中看到,如何轻松地将 Keras 模型转化为你自己的自定义 Estimators。
开发 Estimator 模型涉及四个步骤:
-
使用
tf.data
函数获取数据 -
实例化特征列
-
实例化并训练 Estimator
-
评估模型性能
在我们的配方中,我们将探索这四个步骤,并为每个步骤提供可重用的解决方案。
准备工作
在这个配方中,我们将循环遍历数据点的批次,并让 TensorFlow 更新斜率和 y 截距。我们将使用波士顿房价数据集,而不是生成的数据。
波士顿住房数据集来源于 Harrison, D. 和 Rubinfeld, D.L. 的论文 Hedonic Housing Prices and the Demand for Clean Air(《环境经济学与管理学杂志》,第 5 卷,第 81-102 页,1978 年),该数据集可以在许多分析包中找到(例如 scikit-learn 中),并且存在于 UCI 机器学习库以及原始的 StatLib 存档(http://lib.stat.cmu.edu/datasets/boston)。这是一个经典的回归问题数据集,但并非简单数据集。例如,样本是有顺序的,如果你没有随机打乱样本,进行训练/测试集划分时可能会产生无效且有偏的模型。
详细来说,数据集由 1970 年波士顿人口普查中的 506 个普查区组成,包含 21 个变量,涉及可能影响房地产价值的各个方面。目标变量是房屋的中位数货币价值,单位为千美元。在这些可用的特征中,有一些非常明显的特征,如房间数量、建筑物年龄和邻里犯罪水平,还有一些不那么明显的特征,如污染浓度、附近学校的可用性、高速公路的接入情况和距离就业中心的远近。
回到我们的解决方案,具体来说,我们将找到一个最优特征集,帮助我们估算波士顿的房价。在下一节讨论不同损失函数对这一问题的影响之前,我们还将展示如何从 Keras 函数开始创建一个回归 Estimator,在 TensorFlow 中进行自定义,这为解决不同问题提供了重要的自定义选项。
如何操作…
我们按以下步骤继续进行:
我们首先加载必要的库,然后使用 pandas 函数将数据加载到内存中。接下来,我们将预测变量与目标变量(MEDV,中位房价)分开,并将数据划分为训练集和测试集:
import tensorflow as tf
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
housing_url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.data'
path = tf.keras.utils.get_file(housing_url.split("/")[-1], housing_url)
columns = ['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV']
data = pd.read_table(path, delim_whitespace=True,
header=None, names=columns)
np.random.seed(1)
train = data.sample(frac=0.8).copy()
y_train = train['MEDV']
train.drop('MEDV', axis=1, inplace=True)
test = data.loc[~data.index.isin(train.index)].copy()
y_test = test['MEDV']
test.drop('MEDV', axis=1, inplace=True)
接下来,我们为我们的方案声明两个关键函数:
-
make_input_fn
是一个函数,用于从转换为 Python 字典的 pandas DataFrame(特征作为键,值为特征向量的 pandas Series)创建一个tf.data
数据集。它还提供批量大小定义和随机打乱功能。 -
define_feature_columns
是一个函数,用于将每一列的名称映射到特定的tf.feature_column
转换。tf.feature_column
是一个 TensorFlow 模块 (www.tensorflow.org/api_docs/python/tf/feature_column
),提供能够以适当方式处理各种数据的函数,以便将其输入到神经网络中。
make_input_fn
函数用于实例化两个数据函数,一个用于训练(数据被打乱,批量大小为 256,设置为消耗 1400 个周期),一个用于测试(设置为单个周期,无打乱,因此顺序保持原样)。
define_feature_columns
函数用于通过numeric_column
函数(www.tensorflow.org/api_docs/python/tf/feature_column/numeric_column
)映射数值变量,通过categorical_column_with_vocabulary_list
(www.tensorflow.org/api_docs/python/tf/feature_column/categorical_column_with_vocabulary_list
)映射类别变量。两者都会告诉我们的估算器如何以最佳方式处理这些数据:
learning_rate = 0.05
def make_input_fn(data_df, label_df, num_epochs=10,
shuffle=True, batch_size=256):
def input_function():
ds = tf.data.Dataset.from_tensor_slices((dict(data_df), label_df))
if shuffle:
ds = ds.shuffle(1000)
ds = ds.batch(batch_size).repeat(num_epochs)
return ds
return input_function
def define_feature_columns(data_df, categorical_cols, numeric_cols):
feature_columns = []
for feature_name in numeric_cols:
feature_columns.append(tf.feature_column.numeric_column(
feature_name, dtype=tf.float32))
for feature_name in categorical_cols:
vocabulary = data_df[feature_name].unique()
feature_columns.append(
tf.feature_column.categorical_column_with_vocabulary_list(
feature_name, vocabulary))
return feature_columns
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns = define_feature_columns(data, categorical_cols, numeric_cols)
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
接下来的步骤是实例化线性回归模型的估算器。我们将回顾线性模型的公式,y = aX + b,这意味着存在一个截距值的系数,然后对于每个特征或特征转换(例如,类别数据是独热编码的,因此每个变量值都有一个单独的系数)也会有一个系数:
linear_est = tf.estimator.LinearRegressor(feature_columns=feature_columns)
现在,我们只需要训练模型并评估其性能。使用的指标是均方根误差(越小越好):
linear_est.train(train_input_fn)
result = linear_est.evaluate(test_input_fn)
print(result)
以下是报告的结果:
INFO:tensorflow:Loss for final step: 25.013594.
...
INFO:tensorflow:Finished evaluation at 2020-05-11-15:48:16
INFO:tensorflow:Saving dict for global step 2800: average_loss = 32.715736, global_step = 2800, label/mean = 22.048513, loss = 32.715736, prediction/mean = 21.27578
在这里是个很好的地方,可以注意如何判断模型是否出现了过拟合或欠拟合。如果我们的数据被分为测试集和训练集,并且在训练集上的表现优于测试集,则说明我们正在过拟合数据。如果准确率在测试集和训练集上都在增加,则说明模型欠拟合,我们应该继续训练。
在我们的案例中,训练结束时的平均损失为 25.0。我们的测试集平均损失为 32.7,这意味着我们可能已经过拟合,应该减少训练的迭代次数。
我们可以可视化估算器在训练数据时的表现,以及它与测试集结果的比较。这需要使用 TensorBoard(www.tensorflow.org/tensorboard/
),TensorFlow 的可视化工具包,稍后在本书中将详细讲解。
无论如何,你只需使用4\. Linear Regression with TensorBoard.ipynb
笔记本,而不是4\. Linear Regression.ipynb
版本,就可以重现这些可视化内容。两者都可以在本书的 GitHub 仓库中找到,链接为github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook
。
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_01.png
图 4.1:回归估算器训练损失的 TensorBoard 可视化
可视化结果显示,估计器快速拟合了问题,并在 1,000 个观测批次后达到了最佳值。随后,它围绕已达到的最小损失值波动。由蓝点表示的测试性能接近最佳值,从而证明即使是在未见过的示例上,模型也表现稳定。
它是如何工作的……
调用适当 TensorFlow 功能的估计器,从数据函数中筛选数据,并根据匹配的特征名称和tf.feature_column
函数将数据转换为适当的形式,完成了整个工作。剩下的就是检查拟合情况。实际上,估计器找到的最佳拟合线并不保证就是最优拟合线。是否收敛到最优拟合线取决于迭代次数、批量大小、学习率和损失函数。始终建议在训练过程中观察损失函数,因为这有助于排查问题或调整超参数。
还有更多……
如果你想提高线性模型的性能,交互特征可能是关键。这意味着你在两个变量之间创建组合,而这个组合比单独的特征更能解释目标。在我们的波士顿住房数据集中,结合房屋的平均房间数和某个地区低收入人群的比例,可以揭示更多关于邻里类型的信息,并帮助推断该地区的住房价值。我们通过将它们传递给tf.feature_column.crossed_column
函数来组合这两个特征。估计器在接收这些输出作为特征的一部分时,会自动创建这个交互特征:
def create_interactions(interactions_list, buckets=5):
interactions = list()
for (a, b) in interactions_list:
interactions.append(tf.feature_column.crossed_column([a, b], hash_bucket_size=buckets))
return interactions
derived_feature_columns = create_interactions([['RM', 'LSTAT']])
linear_est = tf.estimator.LinearRegressor(feature_columns=feature_columns+derived_feature_columns)
linear_est.train(train_input_fn)
result = linear_est.evaluate(test_input_fn)
print(result)
这里是训练损失和相应测试集结果的图表。
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_02.png
图 4.2:带有交互特征的回归模型的 TensorBoard 图
观察现在的拟合速度比之前更快、更稳定,这表明我们为模型提供了更多有用的特征(交互特征)。
另一个有用的配方函数适用于处理预测:估计器将其作为字典返回。一个简单的函数将把所有内容转换为更有用的预测数组:
def dicts_to_preds(pred_dicts):
return np.array([pred['predictions'] for pred in pred_dicts])
preds = dicts_to_preds(linear_est.predict(test_input_fn))
print(preds)
将预测结果作为数组有助于你以比字典更方便的方式重用和导出结果。
将 Keras 模型转化为估计器
到目前为止,我们已经使用了tf.estimator
模块中特定的 Estimators 来解决我们的线性回归模型。这具有明显的优势,因为我们的模型大部分是自动运行的,并且我们可以轻松地在云端(如 Google 提供的 Google Cloud Platform)和不同类型的服务器(基于 CPU、GPU 和 TPU)上进行可伸缩的部署。然而,通过使用 Estimators,我们可能会缺乏模型架构的灵活性,而这正是 Keras 模块化方法所要求的,我们在前一章中已经讨论过。在这个配方中,我们将通过展示如何将 Keras 模型转换为 Estimators 来解决这个问题,从而同时利用 Estimators API 和 Keras 的多样性。
准备工作
我们将使用与前一配方中相同的波士顿住房数据集,同时还将利用make_input_fn
函数。与之前一样,我们需要导入我们的核心包:
import tensorflow as tf
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
我们还需要从 TensorFlow 导入 Keras 模块。
import tensorflow.keras as keras
将tf.keras
导入为keras
还允许您轻松地重用之前使用独立 Keras 包编写的任何脚本。
如何操作…
我们的第一步将是重新定义创建特征列的函数。事实上,现在我们必须为我们的 Keras 模型指定一个输入,这是在原生 Estimators 中不需要的,因为它们只需要一个tf.feature
函数来映射特征:
def define_feature_columns_layers(data_df, categorical_cols, numeric_cols):
feature_columns = []
feature_layer_inputs = {}
for feature_name in numeric_cols:
feature_columns.append(tf.feature_column.numeric_column(feature_name, dtype=tf.float32))
feature_layer_inputs[feature_name] = tf.keras.Input(shape=(1,), name=feature_name)
for feature_name in categorical_cols:
vocabulary = data_df[feature_name].unique()
cat = tf.feature_column.categorical_column_with_vocabulary_list(feature_name, vocabulary)
cat_one_hot = tf.feature_column.indicator_column(cat)
feature_columns.append(cat_one_hot)
feature_layer_inputs[feature_name] = tf.keras.Input(shape=(1,), name=feature_name, dtype=tf.int32)
return feature_columns, feature_layer_inputs
互动也是一样的。在这里,我们还需要定义将由我们的 Keras 模型使用的输入(在本例中是独热编码):
def create_interactions(interactions_list, buckets=5):
feature_columns = []
for (a, b) in interactions_list:
crossed_feature = tf.feature_column.crossed_column([a, b], hash_bucket_size=buckets)
crossed_feature_one_hot = tf.feature_column.indicator_column(crossed_feature)
feature_columns.append(crossed_feature_one_hot)
return feature_columns
准备好必要的输入后,我们可以开始模型本身。这些输入将被收集在一个特征层中,该层将数据传递给一个batchNormalization
层,该层将自动标准化数据。然后数据将被导向输出节点,该节点将生成数值输出。
def create_linreg(feature_columns, feature_layer_inputs, optimizer):
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1, kernel_initializer='normal', activation='linear')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()], outputs=outputs)
model.compile(optimizer=optimizer, loss='mean_squared_error')
return model
在此时,已经设置了所有必要的输入,新函数被创建,我们可以运行它们:
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
interactions_columns = create_interactions([['RM', 'LSTAT']])
feature_columns += interactions_columns
optimizer = keras.optimizers.Ftrl(learning_rate=0.02)
model = create_linreg(feature_columns, feature_layer_inputs, optimizer)
现在我们已经获得了一个工作的 Keras 模型。我们可以使用model_to_estimator
函数将其转换为 Estimator。这需要为 Estimator 的输出建立一个临时目录:
import tempfile
def canned_keras(model):
model_dir = tempfile.mkdtemp()
keras_estimator = tf.keras.estimator.model_to_estimator(
keras_model=model, model_dir=model_dir)
return keras_estimator
estimator = canned_keras(model)
将 Keras 模型转换为 Estimator 后,我们可以像以前一样训练模型并评估结果。
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
当我们使用 TensorBoard 绘制拟合过程时,我们将观察到训练轨迹与之前 Estimators 获得的轨迹非常相似:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_03.png
图 4.3:使用 Keras 线性 Estimator 进行训练
Canned Keras Estimators 确实是将 Keras 用户定义解决方案的灵活性与 Estimators 高性能训练和部署结合在一起的快速而健壮的方法。
工作原理…
model_to_estimator
函数并不是 Keras 模型的包装器。相反,它解析你的模型并将其转换为静态 TensorFlow 图,从而实现分布式训练和模型扩展。
还有更多…
使用线性模型的一个重要优势是能够探索其权重,并了解哪个特征对我们获得的结果产生了影响。每个系数会告诉我们,鉴于输入在批处理层被标准化,特征相对于其他特征的影响(系数在绝对值上是可以比较的),以及它是增加还是减少结果(根据正负符号):
weights = estimator.get_variable_value('layer_with_weights-1/kernel/.ATTRIBUTES/VARIABLE_VALUE')
print(weights)
无论如何,如果我们从模型中提取权重,我们会发现无法轻松解释它们,因为它们没有标签且维度不同,因为tf.feature
函数应用了不同的转换。
我们需要一个函数来提取从特征列中正确的标签,因为在将它们输入到预设估算器之前,我们已经将它们映射过:
def extract_labels(feature_columns):
labels = list()
for col in feature_columns:
col_config = col.get_config()
if 'key' in col_config:
labels.append(col_config['key'])
elif 'categorical_column' in col_config:
if col_config['categorical_column']['class_name']=='VocabularyListCategoricalColumn':
key = col_config['categorical_column']['config']['key']
for item in col_config['categorical_column']['config']['vocabulary_list']:
labels.append(key+'_val='+str(item))
elif col_config['categorical_column']['class_name']=='CrossedColumn':
keys = col_config['categorical_column']['config']['keys']
for bucket in range(col_config['categorical_column']['config']['hash_bucket_size']):
labels.append('x'.join(keys)+'_bkt_'+str(bucket))
return labels
该函数仅适用于 TensorFlow 2.2 或更高版本,因为在早期的 TensorFlow 2.x 版本中,get_config
方法在tf.feature
对象中并不存在。
现在,我们可以提取所有标签,并有意义地将每个输出中的权重与其相应的特征匹配:
labels = extract_labels(feature_columns)
for label, weight in zip(labels, weights):
print(f"{label:15s} : {weight[0]:+.2f}")
一旦你得到了权重,就可以通过观察每个系数的符号和大小,轻松地得出每个特征对结果的贡献。然而,特征的尺度可能会影响大小,除非你事先通过减去均值并除以标准差对特征进行了统计标准化。
理解线性回归中的损失函数
了解损失函数对算法收敛性的影响非常重要。在这里,我们将说明 L1 和 L2 损失函数如何影响线性回归的收敛性和预测。这是我们对预设 Keras 估算器进行的第一次自定义。本章的更多配方将在此基础上通过添加更多功能来增强该初始估算器。
准备工作
我们将使用与前面配方中相同的波士顿房价数据集,并使用以下函数:
* define_feature_columns_layers
* make_input_fn
* create_interactions
然而,我们将更改损失函数和学习率,看看收敛性如何变化。
如何实现…
我们按如下方式继续配方:
程序的开始与上一个配方相同。因此,我们加载必要的包,并且如果波士顿房价数据集尚不可用,我们将下载它:
import tensorflow as tf
import tensorflow.keras as keras
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
之后,我们需要重新定义create_linreg
,通过添加一个新的参数来控制损失类型。默认值仍然是均方误差(L2 损失),但现在在实例化预设估算器时可以轻松更改:
def create_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error']):
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1, kernel_initializer='normal',
activation='linear')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()],
outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
这样做之后,我们可以通过使用不同学习率的Ftrl
优化器显式地训练我们的模型,更适合 L1 损失(我们将损失设置为平均绝对误差):
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
interactions_columns = create_interactions([['RM', 'LSTAT']])
feature_columns += interactions_columns
optimizer = keras.optimizers.Ftrl(learning_rate=0.02)
model = create_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_absolute_error',
metrics=['mean_absolute_error', 'mean_squared_error'])
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
这是我们通过切换到 L1 损失函数得到的结果:
{'loss': 3.1208777, 'mean_absolute_error': 3.1208777, 'mean_squared_error': 27.170328, 'global_step': 2800}
我们现在可以使用 TensorBoard 来可视化训练过程中的性能:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_04.png
图 4.4:均方误差优化
结果图显示了均方误差的良好下降,直到 400 次迭代后减慢,并在 1,400 次迭代后趋于稳定,形成一个平台。
如何运作……
在选择损失函数时,我们还必须选择一个相应的学习率,以确保其与我们的问题匹配。在这里,我们测试了两种情况,第一种是采用 L2,第二种是首选 L1。
如果我们的学习率较小,收敛过程将需要更多时间。然而,如果学习率过大,我们的算法可能会无法收敛。
还有更多……
为了理解发生了什么,我们应当观察大学习率和小学习率对L1 范数和L2 范数的作用。如果学习率过大,L1 可能会停滞在次优结果,而 L2 可能会得到更差的性能。为了可视化这一点,我们将查看关于两种范数的学习步长的一维表示,如下所示:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_05.png
图 4.5:L1 和 L2 范数在较大和较小学习率下的表现
如前所示,小学习率确实能保证更好的优化结果。较大的学习率与 L2 不太适用,但可能对 L1 证明是次优的,因为它会在一段时间后停止进一步优化,而不会造成更大的损害。
实现 Lasso 和 Ridge 回归
有几种方法可以限制系数对回归输出的影响。这些方法被称为正则化方法,其中最常见的两种正则化方法是 Lasso 和 Ridge 回归。在本食谱中,我们将讲解如何实现这两种方法。
准备工作
Lasso 和 Ridge 回归与普通线性回归非常相似,不同之处在于我们在公式中加入了正则化项,以限制斜率(或偏斜率)。这背后可能有多个原因,但常见的一个原因是我们希望限制对因变量有影响的特征数量。
如何操作……
我们按照以下步骤继续进行:
我们将再次使用波士顿房价数据集,并按照之前的食谱设置函数。特别是,我们需要define_feature_columns_layers
、make_input_fn
和 create_interactions
。我们再次首先加载库,然后定义一个新的 create_ridge_linreg
,在其中我们使用keras.regularizers.l2
作为我们密集层的regularizer
来设置一个新的 Keras 模型:
import tensorflow as tf
import tensorflow.keras as keras
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
def create_ridge_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error'],
l2=0.01):
regularizer = keras.regularizers.l2(l2)
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1,
kernel_initializer='normal',
kernel_regularizer = regularizer,
activation='linear')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()], outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
完成这些后,我们可以再次运行之前的线性模型,并使用 L1 损失来观察结果的改进:
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
interactions_columns = create_interactions([['RM', 'LSTAT']])
feature_columns += interactions_columns
optimizer = keras.optimizers.Ftrl(learning_rate=0.02)
model = create_ridge_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error', 'mean_squared_error'],
l2=0.01)
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
这是 Ridge 回归的结果:
{'loss': 25.903751, 'mean_absolute_error': 3.27314, 'mean_squared_error': 25.676477, 'global_step': 2800}
此外,这里是使用 TensorBoard 进行训练的图表:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_06.png
图 4.6:Ridge 回归训练损失
我们也可以通过创建一个新函数来复制 L1 正则化:
create_lasso_linreg.
def create_lasso_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error', metrics=['mean_absolute_error'],
l1=0.001):
regularizer = keras.regularizers.l1(l1)
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1,
kernel_initializer='normal',
kernel_regularizer = regularizer,
activation='linear')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()], outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
interactions_columns = create_interactions([['RM', 'LSTAT']])
feature_columns += interactions_columns
optimizer = keras.optimizers.Ftrl(learning_rate=0.02)
model = create_lasso_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error', 'mean_squared_error'],
l1=0.001)
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
这是从 L1 Lasso 回归得到的结果:
{'loss': 24.616476, 'mean_absolute_error': 3.1985352, 'mean_squared_error': 24.59167, 'global_step': 2800}
此外,这里是训练损失的图表:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_07.png
图 4.7:Lasso 回归训练损失
比较 Ridge 和 Lasso 方法时,我们注意到它们在训练损失方面没有太大差异,但测试结果偏向 Lasso。这可能是由于一个噪声变量必须被排除,才能让模型得到改进,因为 Lasso 会定期从预测估计中排除无用的变量(通过赋予它们零系数),而 Ridge 只是对它们进行下调权重。
它是如何工作的…
我们通过向线性回归的损失函数添加一个连续的 Heaviside 阶跃函数来实现 Lasso 回归。由于阶跃函数的陡峭性,我们必须小心步长。如果步长太大,模型将无法收敛。关于 Ridge 回归,请参见下一节所需的更改。
还有更多…
弹性网回归是一种回归方法,通过将 Lasso 回归与 Ridge 回归结合,在损失函数中添加 L1 和 L2 正则化项。
实现弹性网回归很简单,跟随前两个方法,因为你只需更改正则化器。
我们只需创建一个 create_elasticnet_linreg
函数,它将 L1 和 L2 强度的值作为参数传入:
def create_elasticnet_linreg(feature_columns, feature_layer_inputs,
optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error'],
l1=0.001, l2=0.01):
regularizer = keras.regularizers.l1_l2(l1=l1, l2=l2)
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1,
kernel_initializer='normal',
kernel_regularizer = regularizer,
activation='linear')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()],
outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
最后,我们重新运行从数据开始的完整训练步骤,并评估模型的性能:
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
interactions_columns = create_interactions([['RM', 'LSTAT']])
feature_columns += interactions_columns
optimizer = keras.optimizers.Ftrl(learning_rate=0.02)
model = create_elasticnet_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error',
'mean_squared_error'],
l1=0.001, l2=0.01)
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
这是得到的结果:
{'loss': 24.910872, 'mean_absolute_error': 3.208289, 'mean_squared_error': 24.659771, 'global_step': 2800}
这是 ElasticNet 模型的训练损失图:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_08.png
图 4.8:ElasticNet 训练损失
获得的测试结果与 Ridge 和 Lasso 相差不大,位于它们之间。如前所述,问题在于从数据集中去除变量以提高性能,正如我们现在所看到的,Lasso 模型是最适合执行这一任务的。
实现逻辑回归
对于本教程,我们将实现逻辑回归,利用乳腺癌威斯康星数据集(archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Diagnostic)
)来预测乳腺癌的概率。我们将从基于乳腺肿块的细针穿刺(FNA)图像计算得到的特征中预测诊断结果。FNA 是一个常见的乳腺癌检测方法,通过小量组织活检进行,活检样本可以在显微镜下进行检查。
该数据集可以直接用于分类模型,无需进一步转换,因为目标变量由 357 个良性病例和 212 个恶性病例组成。这两个类别的样本数并不完全相同(在进行二分类回归模型时,这是一个重要要求),但它们的差异并不极端,使我们能够构建一个简单的例子并使用普通准确率来评估模型。
请记得检查你的类别是否平衡(换句话说,是否具有大致相同数量的样本),否则你需要采取特定的方法来平衡样本,例如应用权重,或者你的模型可能会提供不准确的预测(如果你需要更多细节,可以参考以下 Stack Overflow 问题:datascience.stackexchange.com/questions/13490/how-to-set-class-weights-for-imbalanced-classes-in-keras
)。
准备工作
逻辑回归是一种将线性回归转化为二分类的方法。这是通过将线性输出转换为一个 sigmoid 函数来实现的,该函数将输出值缩放到 0 和 1 之间。目标值是 0 或 1,表示一个数据点是否属于某个类别。由于我们预测的是一个 0 到 1 之间的数值,当预测值超过指定的阈值时,预测结果被分类为类别 1,否则为类别 0。对于本例,我们将阈值设置为 0.5,这样分类就变得和四舍五入输出一样简单。
在分类时,无论如何,有时候你需要控制自己犯的错误,这对于医疗应用(比如我们提出的这个例子)尤其重要,但对于其他应用(例如保险或银行领域的欺诈检测),这也是一个值得关注的问题。事实上,在分类时,你会得到正确的预测结果,但也会有假阳性和假阴性。假阳性是模型在预测为阳性(类别 1)时,真实标签却为阴性所产生的错误。假阴性则是模型将实际为阳性的样本预测为阴性。
在使用 0.5 阈值来决定类别(正类或负类)时,实际上你是在平衡假阳性和假阴性的期望值。实际上,根据你的问题,假阳性和假阴性错误可能会有不同的后果。例如,在癌症检测的情况下,显然你绝对不希望发生假阴性错误,因为这意味着将一个实际患病的病人误判为健康,可能导致生命危险。
通过设置更高或更低的分类阈值,你可以在假阳性和假阴性之间进行权衡。较高的阈值将导致更多的假阴性,而假阳性较少。较低的阈值将导致较少的假阴性,但假阳性更多。对于我们的示例,我们将使用 0.5 的阈值,但请注意,阈值也是你需要考虑的因素,尤其是在模型的实际应用中。
如何实现…
我们按照以下步骤继续进行示例:
我们首先加载库并从互联网恢复数据:
import tensorflow as tf
import tensorflow.keras as keras
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
breast_cancer = 'https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data'
path = tf.keras.utils.get_file(breast_cancer.split("/")[-1], breast_cancer)
columns = ['sample_code', 'clump_thickness', 'cell_size_uniformity',
'cell_shape_uniformity',
'marginal_adhesion', 'single_epithelial_cell_size',
'bare_nuclei', 'bland_chromatin',
'normal_nucleoli', 'mitoses', 'class']
data = pd.read_csv(path, header=None, names=columns, na_values=[np.nan, '?'])
data = data.fillna(data.median())
np.random.seed(1)
train = data.sample(frac=0.8).copy()
y_train = (train['class']==4).astype(int)
train.drop(['sample_code', 'class'], axis=1, inplace=True)
test = data.loc[~data.index.isin(train.index)].copy()
y_test = (test['class']==4).astype(int)
test.drop(['sample_code', 'class'], axis=1, inplace=True)
接下来,我们指定逻辑回归函数。与我们之前的线性回归模型相比,主要的修改是将单个输出神经元的激活函数从linear
改为sigmoid
,这就足够让我们得到一个逻辑回归模型,因为我们的输出将是一个概率值,范围从 0.0 到 1.0:
def create_logreg(feature_columns, feature_layer_inputs, optimizer,
loss='binary_crossentropy', metrics=['accuracy'],
l2=0.01):
regularizer = keras.regularizers.l2(l2)
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1,
kernel_initializer='normal',
kernel_regularizer = regularizer,
activation='sigmoid')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()], outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
最后,我们运行我们的程序:
categorical_cols = []
numeric_cols = ['clump_thickness', 'cell_size_uniformity', 'cell_shape_uniformity',
'marginal_adhesion', 'single_epithelial_cell_size', 'bare_ nuclei',
'bland_chromatin',
'normal_nucleoli', 'mitoses']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
optimizer = keras.optimizers.Ftrl(learning_rate=0.007)
model = create_logreg(feature_columns, feature_layer_inputs, optimizer, l2=0.01)
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=300, batch_size=32)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
下面是我们逻辑回归的准确率报告:
{'accuracy': 0.95, 'loss': 0.16382739, 'global_step': 5400}
此外,你可以在这里找到损失图:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_09.png
图 4.9:逻辑回归模型的 TensorBoard 训练损失图
通过几个命令,我们在准确率和损失方面取得了不错的结果,尽管目标类别略有不平衡(良性病例比恶性病例更多)。
它是如何工作的…
逻辑回归的预测基于 sigmoid 曲线,若要相应地修改我们之前的线性模型,我们只需要切换到 sigmoid 激活函数。
还有更多…
当你在进行多类或多标签预测时,你不需要通过不同类型的一对多(OVA)策略来扩展二分类模型,而只需要扩展输出节点的数量,以匹配你需要预测的类别数。使用带有 sigmoid 激活函数的多个神经元,你将得到一个多标签方法,而使用 softmax 激活函数,你将获得一个多类预测。你将在本书的后续章节中找到更多的示例,说明如何使用简单的 Keras 函数来实现这一点。
寻求非线性解决方案
线性模型具有较强的可接近性和可解释性,因为特征列与回归系数之间存在一对一的关系。然而,有时候你可能希望尝试非线性解法,以检查更复杂的模型是否能更好地拟合你的数据,并以更专业的方式解决你的预测问题。支持向量机(SVMs)是一种与神经网络竞争了很长时间的算法,且由于最近在大规模核机器的随机特征方面的进展,它们仍然是一个可行的选择(Rahimi, Ali; Recht, Benjamin. Random features for large-scale kernel machines. In: Advances in neural information processing systems. 2008. 第 1177-1184 页)。在本示例中,我们展示了如何利用 Keras 获得非线性解法来解决分类问题。
准备工作
我们仍然会使用之前示例中的函数,包括define_feature_columns_layers
和make_input_fn
。和逻辑回归示例一样,我们将继续使用乳腺癌数据集。和以前一样,我们需要加载以下包:
import tensorflow as tf
import tensorflow.keras as keras
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
到目前为止,我们已经准备好继续执行这个步骤。
如何做…
除了之前的包外,我们还专门导入了RandomFourierFeatures
函数,它可以对输入进行非线性变换。根据损失函数,RandomFourierFeatures
层可以逼近基于核的分类器和回归器。之后,我们只需要应用我们通常的单输出节点并获取预测结果。
根据你使用的 TensorFlow 2.x 版本,你可能需要从不同的模块导入它:
try:
from tensorflow.python.keras.layers.kernelized import RandomFourierFeatures
except:
# from TF 2.2
from tensorflow.keras.layers.experimental import RandomFourierFeatures
现在我们开发create_svc
函数。它包含了一个 L2 正则化项用于最终的全连接节点,一个用于输入的批归一化层,以及一个插入其中的RandomFourierFeatures
层。在这个中间层中,产生了非线性特征,你可以通过设置output_dim
参数来确定层生成的非线性交互的数量。当然,你可以通过增加 L2 正则化值来对比在设置较高output_dim
值后出现的过拟合,从而实现更多的正则化:
def create_svc(feature_columns, feature_layer_inputs, optimizer,
loss='hinge', metrics=['accuracy'],
l2=0.01, output_dim=64, scale=None):
regularizer = keras.regularizers.l2(l2)
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
rff = RandomFourierFeatures(output_dim=output_dim, scale=scale, kernel_initializer='gaussian')(norm)
outputs = keras.layers.Dense(1,
kernel_initializer='normal',
kernel_regularizer = regularizer,
activation='sigmoid')(rff)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()], outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
和之前的示例一样,我们定义了不同的列,设置了模型和优化器,准备了输入函数,最后我们训练并评估结果:
categorical_cols = []
numeric_cols = ['clump_thickness', 'cell_size_uniformity', 'cell_shape_uniformity',
'marginal_adhesion', 'single_epithelial_cell_size', 'bare_nuclei', 'bland_chromatin',
'normal_nucleoli', 'mitoses']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
optimizer = keras.optimizers.Adam(learning_rate=0.00005)
model = create_svc(feature_columns, feature_layer_inputs, optimizer,
loss='hinge', l2=0.001, output_dim=512)
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=500, batch_size=512)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
这里是报告的准确度。为了获得更好的结果,你需要尝试不同的RandomFourierFeatures
层的输出维度和正则化项的组合:
{'accuracy': 0.95 'loss': 0.7390725, 'global_step': 1000}
这是来自 TensorBoard 的损失图:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_10.png
图 4.10:基于 RandomFourierFeatures 的模型的损失图
由于我们使用了比平常更大的批次,图形的效果确实相当好。由于任务的复杂性(需要训练大量的神经元),通常更大的批次会比较小的批次效果更好。
它是如何工作的…
随机傅里叶特征是一种近似支持向量机(SVM)核函数的方法,从而实现更低的计算复杂度,并使得这种方法在神经网络实现中也变得可行。如果你需要更深入的解释,可以阅读本文开头引用的原始论文,或者你可以参考这个非常清晰的 Stack Exchange 回答:stats.stackexchange.com/questions/327646/how-does-a-random-kitchen-sink-work#327961
。
还有更多……
根据损失函数的不同,你可以得到不同的非线性模型:
-
铰链损失将你的模型设定为支持向量机(SVM)。
-
逻辑损失将你的模型转化为核逻辑回归模型(分类性能几乎与支持向量机(SVM)相同,但核逻辑回归可以提供类别概率)。
-
均方误差将你的模型转化为一个核回归模型。
由你决定首先尝试哪种损失函数,并决定如何设置来自随机傅里叶变换的输出维度。一般建议是,你可以从较多的输出节点开始,并逐步测试减少节点数量是否能改善结果。
使用宽深模型
线性模型相较于复杂模型有一个巨大优势:它们高效且易于解释,即使在你使用多个特征且特征之间存在相互作用时也如此。谷歌研究人员提到这一点作为记忆化的力量,因为你的线性模型将特征与目标之间的关联记录为单一系数。另一方面,神经网络具备泛化的力量,因为在其复杂性中(它们使用多个权重层并且相互关联每个输入),它们能够近似描述支配过程结果的一般规则。
宽深模型,如谷歌研究人员所构想的那样(arxiv.org/abs/1606.07792
),能够融合记忆化和泛化,因为它们将线性模型(应用于数值特征)与泛化(应用于稀疏特征,例如编码为稀疏矩阵的类别特征)结合在一起。因此,名称中的宽指的是回归部分,深指的是神经网络部分:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_11.png
图 4.11:宽模型(线性模型)如何与神经网络在宽深模型中融合(摘自 Cheng, Heng-Tze 等人的论文《Wide & deep learning for recommender systems》,《第 1 届深度学习推荐系统研讨会论文集》,2016 年)。
这样的结合可以在处理推荐系统问题时取得最佳结果(例如 Google Play 中展示的推荐系统)。Wide & Deep 模型在推荐问题中表现最好,因为每个部分处理的是正确类型的数据。宽度部分处理与用户特征相关的特征(密集的数值特征、二进制指示符或它们在交互特征中的组合),这些特征相对稳定;而深度部分处理表示先前软件下载的特征字符串(稀疏输入在非常大的矩阵中),这些特征随着时间变化而更为不稳定,因此需要一种更复杂的表示方式。
准备工作
实际上,Wide & Deep 模型同样适用于许多其他数据问题,推荐系统是它们的专长,且此类模型在 Estimators 中已经很容易获得(见www.tensorflow.org/api_docs/python/tf/estimator/DNNLinearCombinedEstimator
)。在这个示例中,我们将使用一个混合数据集,成人数据集(archive.ics.uci.edu/ml/datasets/Adult
)。该数据集也被广泛称为人口普查数据集,其目的是预测基于人口普查数据,您的年收入是否超过 5 万美元。可用的特征种类非常多样,从与年龄相关的连续值,到具有大量类别的变量,包括职业。然后,我们将使用每种不同类型的特征,输入到 Wide & Deep 模型的正确部分。
如何操作…
我们首先从 UCI 存档中下载成人数据集:
census_dir = 'https://archive.ics.uci.edu/ml/machine-learning-databases/adult/'
train_path = tf.keras.utils.get_file('adult.data', census_dir + 'adult.data')
test_path = tf.keras.utils.get_file('adult.test', census_dir + 'adult.test')
columns = ['age', 'workclass', 'fnlwgt', 'education', 'education_num',
'marital_status', 'occupation', 'relationship', 'race',
'gender', 'capital_gain', 'capital_loss', 'hours_per_week',
'native_country', 'income_bracket']
train_data = pd.read_csv(train_path, header=None, names=columns)
test_data = pd.read_csv(test_path, header=None, names=columns, skiprows=1)
然后,我们根据需要选择特征子集,并提取目标变量,将其从字符串类型转换为整数类型:
predictors = ['age', 'workclass', 'education', 'education_num',
'marital_status', 'occupation', 'relationship', 'gender']
y_train = (train_data.income_bracket==' >50K').astype(int)
y_test = (test_data.income_bracket==' >50K.').astype(int)
train_data = train_data[predictors]
test_data = test_data[predictors]
该数据集需要额外处理,因为某些字段存在缺失值。我们通过用均值替换缺失值来处理这些数据。作为一般规则,我们必须在将数据输入 TensorFlow 模型之前,填补所有缺失的数据:
train_data[['age', 'education_num']] = train_data[['age', 'education_num']].fillna(train_data[['age', 'education_num']].mean())
test_data[['age', 'education_num']] = test_data[['age', 'education_num']].fillna(train_data[['age', 'education_num']].mean())
现在,我们可以通过正确的 tf.feature_column
函数来定义列:
-
数值列:处理数值型数据(如年龄)
-
分类列:处理分类值,当唯一类别数量较少时(如性别)
-
嵌入:处理当唯一类别数量较多时的分类值,将分类值映射到一个密集的低维数值空间
我们还定义了一个函数,用于简化分类列和数值列之间的交互:
def define_feature_columns(data_df, numeric_cols, categorical_cols, categorical_embeds, dimension=30):
numeric_columns = []
categorical_columns = []
embeddings = []
for feature_name in numeric_cols:
numeric_columns.append(tf.feature_column.numeric_column(feature_name, dtype=tf.float32))
for feature_name in categorical_cols:
vocabulary = data_df[feature_name].unique()
categorical_columns.append(tf.feature_column.categorical_column_with_vocabulary_list(feature_name, vocabulary))
for feature_name in categorical_embeds:
vocabulary = data_df[feature_name].unique()
to_categorical = tf.feature_column.categorical_column_with_vocabulary_list(feature_name,
vocabulary)
embeddings.append(tf.feature_column.embedding_column(to_categorical,
dimension=dimension))
return numeric_columns, categorical_columns, embeddings
def create_interactions(interactions_list, buckets=10):
feature_columns = []
for (a, b) in interactions_list:
crossed_feature = tf.feature_column.crossed_column([a, b], hash_bucket_size=buckets)
crossed_feature_one_hot = tf.feature_column.indicator_column( crossed_feature)
feature_columns.append(crossed_feature_one_hot)
return feature_columns
现在所有函数已经定义完毕,我们将不同的列进行映射,并添加一些有意义的交互(例如将教育与职业进行交叉)。我们通过设置维度参数,将高维分类特征映射到一个固定的 32 维低维数值空间:
numeric_columns, categorical_columns, embeddings = define_feature_columns(train_data,
numeric_cols=['age', 'education_num'],
categorical_cols=['gender'],
categorical_embeds=['workclass', 'education',
'marital_status', 'occupation',
'relationship'],
dimension=32)
interactions = create_interactions([['education', 'occupation']], buckets=10)
映射完特征后,我们将它们输入到我们的估计器中(参见www.tensorflow.org/api_docs/python/tf/estimator/DNNLinearCombinedClassifier
),并指定由宽部分处理的特征列和由深部分处理的特征列。对于每个部分,我们还指定优化器(通常线性部分使用 Ftrl,深部分使用 Adam),并且对于深部分,我们指定隐藏层的架构,作为一个神经元数量的列表:
estimator = tf.estimator.DNNLinearCombinedClassifier(
# wide settings
linear_feature_columns=numeric_columns+categorical_columns+interactions, linear_optimizer=keras.optimizers.Ftrl(learning_rate=0.0002),
# deep settings
dnn_feature_columns=embeddings,
dnn_hidden_units=[1024, 256, 128, 64],
dnn_optimizer=keras.optimizers.Adam(learning_rate=0.0001))
然后,我们继续定义输入函数(与本章其他食谱中所做的没有不同):
def make_input_fn(data_df, label_df, num_epochs=10, shuffle=True, batch_size=256):
def input_function():
ds = tf.data.Dataset.from_tensor_slices((dict(data_df), label_df))
if shuffle:
ds = ds.shuffle(1000)
ds = ds.batch(batch_size).repeat(num_epochs)
return ds
return input_function
最后,我们训练 Estimator 1,500 步并在测试数据上评估结果:
train_input_fn = make_input_fn(train_data, y_train,
num_epochs=100, batch_size=256)
test_input_fn = make_input_fn(test_data, y_test,
num_epochs=1, shuffle=False)
estimator.train(input_fn=train_input_fn, steps=1500)
results = estimator.evaluate(input_fn=test_input_fn)
print(results)
我们在测试集上获得了约 0.83 的准确率,这是通过使用 Estimator 的 evaluate 方法报告的:
{'accuracy': 0.83391684, 'accuracy_baseline': 0.76377374, 'auc': 0.88012385, 'auc_precision_recall': 0.68032277, 'average_loss': 0.35969484, 'label/mean': 0.23622628, 'loss': 0.35985297, 'precision': 0.70583993, 'prediction/mean': 0.21803579, 'recall': 0.5091004, 'global_step': 1000}
这是训练损失和测试估计(蓝点)的图示:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_04_12.png
图 4.12:Wide & Deep 模型的训练损失和测试估计
对于完整的预测概率,我们只需从 Estimator 使用的字典数据类型中提取它们。predict_proba
函数将返回一个 NumPy 数组,包含正类(收入超过 50K 美元)和负类的概率,分别位于不同的列中:
def predict_proba(predictor):
preds = list()
for pred in predictor:
preds.append(pred['probabilities'])
return np.array(preds)
predictions = predict_proba(estimator.predict(input_fn=test_input_fn))
它是如何工作的…
Wide & Deep 模型代表了一种将线性模型与更复杂的神经网络方法结合使用的方式。与其他 Estimator 一样,这个 Estimator 也非常简单易用。该方法在其他应用中的成功关键,绝对在于定义输入数据函数并将特征与tf.features_columns
中更合适的函数进行映射。
第五章:梯度提升树
本章介绍了梯度提升树:TensorFlow(TF)的方法。它是一类机器学习算法,通过一组弱预测模型(通常是决策树)生成预测模型。该模型以阶段性方式构建,并通过使用任意(可微)损失函数来进行泛化。梯度提升树是一类非常流行的算法,因为它们可以并行化(在树构建阶段),本地处理缺失值和异常值,并且需要最少的数据预处理。
引言
在本章中,我们简要展示了如何使用BoostedTreesClassifier
处理二分类问题。我们将应用这种技术解决一个实际的商业问题,使用一个流行的教育数据集:预测哪些顾客可能会取消他们的预订。这个问题的数据——以及其他几个商业问题——以表格格式存在,通常包含多种不同的特征类型:数值型、类别型、日期等。在缺乏复杂领域知识的情况下,梯度提升方法是创建一个可解释的、即开即用的解决方案的不错选择。在接下来的部分,将通过代码演示相关的建模步骤:数据准备、结构化为函数、通过tf.estimator
功能拟合模型,以及结果的解释。
如何实现…
我们首先加载必要的包:
import tensorflow as tf
import numpy as np
import pandas as pd
from IPython.display import clear_output
from matplotlib import pyplot as plt
import matplotlib.pyplot as plt
import seaborn as sns
sns_colors = sns.color_palette('colorblind')
from numpy.random import uniform, seed
from scipy.interpolate import griddata
from matplotlib.font_manager import FontProperties
from sklearn.metrics import roc_curve
原则上,类别变量可以简单地重新编码为整数(使用如LabelEncoder
之类的函数),而梯度提升模型也能很好地工作——数据预处理的这些最小要求是树集成方法流行的原因之一。然而,在本案例中,我们希望集中展示模型的可解释性,因此我们要分析各个指标的值。基于这个原因,我们创建了一个在 TF 友好的格式中执行独热编码的函数:
def one_hot_cat_column(feature_name, vocab):
return tf.feature_column.indicator_column(
tf.feature_column.categorical_column_with_vocabulary_list(feature_name, vocab))
如引言中所述,在这个案例中,我们将使用以下网址提供的酒店取消数据集:
www.sciencedirect.com/science/article/pii/S2352340918315191
我们选择这个数据集是因为它对于读者可能遇到的典型商业预测问题来说相当现实:数据中包含时间维度,并且有数值型和类别型特征。同时,它也相当干净(没有缺失值),这意味着我们可以专注于实际的建模,而不是数据处理:
xtrain = pd.read_csv('../input/hotel-booking- demand/hotel_bookings.csv')
xtrain.head(3)
数据集有时间维度,因此可以基于reservation_status_date
进行自然的训练/验证集划分:
xvalid = xtrain.loc[xtrain['reservation_status_date'] >= '2017-08-01']
xtrain = xtrain.loc[xtrain['reservation_status_date'] < '2017-08-01']
将特征与目标分开:
ytrain, yvalid = xtrain['is_canceled'], xvalid['is_canceled']
xtrain.drop('is_canceled', axis = 1, inplace = True)
xvalid.drop('is_canceled', axis = 1, inplace = True)
我们将列分为数值型和类别型,并以 TF 期望的格式进行编码。我们跳过一些可能改善模型性能的列,但由于它们的特性,它们引入了泄露的风险:在训练中可能改善模型性能,但在对未见数据进行预测时会失败。在我们的案例中,其中一个变量是 arrival_date_year
:如果模型过于依赖这个变量,当我们提供一个更远未来的数据集时(该变量的特定值显然会缺失),模型将会失败。
我们从训练数据中去除一些额外的变量——这个步骤可以在建模过程之前基于专家判断进行,或者可以通过自动化方法进行。后一种方法将包括运行一个小型模型并检查全局特征的重要性:如果结果显示某一个非常重要的特征主导了其他特征,它可能是信息泄露的潜在来源:
xtrain.drop(['arrival_date_year','assigned_room_type', 'booking_changes', 'reservation_status', 'country', 'days_in_waiting_list'], axis =1, inplace = True)
num_features = ["lead_time","arrival_date_week_number",
"arrival_date_day_of_month",
"stays_in_weekend_nights",
"stays_in_week_nights","adults","children",
"babies","is_repeated_guest", "previous_cancellations",
"previous_bookings_not_canceled","agent","company",
"required_car_parking_spaces",
"total_of_special_requests", "adr"]
cat_features = ["hotel","arrival_date_month","meal","market_segment",
"distribution_channel","reserved_room_type",
"deposit_type","customer_type"]
def one_hot_cat_column(feature_name, vocab):
return tf.feature_column.indicator_column(
tf.feature_column.categorical_column_with_vocabulary_list( feature_name,
vocab))
feature_columns = []
for feature_name in cat_features:
# Need to one-hot encode categorical features.
vocabulary = xtrain[feature_name].unique()
feature_columns.append(one_hot_cat_column(feature_name, vocabulary))
for feature_name in num_features:
feature_columns.append(tf.feature_column.numeric_column(feature_name,
dtype=tf.float32))
下一步是为提升树算法创建输入函数:我们指定如何将数据读入模型,用于训练和推理。我们使用 tf.data
API 中的 from_tensor_slices
方法直接从 pandas 读取数据:
NUM_EXAMPLES = len(ytrain)
def make_input_fn(X, y, n_epochs=None, shuffle=True):
def input_fn():
dataset = tf.data.Dataset.from_tensor_slices((dict(X), y))
if shuffle:
dataset = dataset.shuffle(NUM_EXAMPLES)
# For training, cycle thru dataset as many times as need (n_epochs=None).
dataset = dataset.repeat(n_epochs)
# In memory training doesn't use batching.
dataset = dataset.batch(NUM_EXAMPLES)
return dataset
return input_fn
# Training and evaluation input functions.
train_input_fn = make_input_fn(xtrain, ytrain)
eval_input_fn = make_input_fn(xvalid, yvalid, shuffle=False, n_epochs=1)
现在我们可以构建实际的 BoostedTrees 模型。我们设置了一个最小的参数列表(max_depth
是其中一个最重要的参数)——在定义中没有指定的参数将保持默认值,这些可以通过文档中的帮助函数查找。
params = {
'n_trees': 125,
'max_depth': 5,
'n_batches_per_layer': 1,
'center_bias': True
}
est = tf.estimator.BoostedTreesClassifier(feature_columns, **params)
# Train model.
est.train(train_input_fn, max_steps=100)
一旦我们训练好一个模型,就可以根据不同的评估指标来评估其性能。BoostedTreesClassifier
包含一个 evaluate
方法,输出涵盖了广泛的可能指标;使用哪些指标进行指导取决于具体应用,但默认输出的指标已经能让我们从多个角度评估模型(例如,如果我们处理的是一个高度不平衡的数据集,auc
可能会有些误导,此时我们应该同时评估损失值)。更详细的说明,请参考文档:www.tensorflow.org/api_docs/python/tf/estimator/BoostedTreesClassifier
:
# Evaluation
results = est.evaluate(eval_input_fn)
pd.Series(results).to_frame()
你看到的结果应该是这样的:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_05_01.png
pred_dicts = list(est.predict(eval_input_fn))
probs = pd.Series([pred['probabilities'][1] for pred in pred_dicts])
我们可以在不同的泛化层次上评估结果——以下给出全球性和局部性差异的具体说明。我们从接收者操作特征(ROC)曲线开始:这是一种图形,显示了分类模型在所有可能分类阈值下的性能。我们绘制假阳性率与真正阳性率的关系:一个随机分类器会表现为从 (0,0) 到 (1,1) 的对角线,越远离这种情况,朝左上角移动,我们的分类器就越好:
fpr, tpr, _ = roc_curve(yvalid, probs)
plt.plot(fpr, tpr)
plt.title('ROC curve')
plt.xlabel('false positive rate')
plt.ylabel('true positive rate')
plt.xlim(0,); plt.ylim(0,); plt.show()
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_05_02.png
图 5.1:训练分类器的 ROC 曲线
局部可解释性指的是对模型在单个示例层面的预测的理解:我们将创建并可视化每个实例的贡献。这对于需要向具有技术认知多样性的观众解释模型预测时尤其有用。我们将这些值称为方向性特征贡献(DFC):
pred_dicts = list(est.experimental_predict_with_explanations(eval_input_fn))
# Create DFC Pandas dataframe.
labels = yvalid.values
probs = pd.Series([pred['probabilities'][1] for pred in pred_dicts])
df_dfc = pd.DataFrame([pred['dfc'] for pred in pred_dicts])
df_dfc.describe().T
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_05_03.png
完整的 DFC 数据框的总结初看起来可能有些让人不知所措,实际上,通常我们会关注某些列的子集。每一行展示的是特征(第一行的arrival_date_week_number
,第二行的arrival_date_day_of_month
,依此类推)在验证集中的所有观测值的方向性贡献的汇总统计(如mean
、std
等)。
它是如何工作的……
以下代码块演示了提取某个记录的预测特征贡献所需的步骤。为了方便和可重用,我们首先定义了一个绘制选定记录的函数(为了更容易解释,我们希望使用不同的颜色绘制特征重要性,具体取决于它们的贡献是正向还是负向):
def _get_color(value):
"""To make positive DFCs plot green, negative DFCs plot red."""
green, red = sns.color_palette()[2:4]
if value >= 0: return green
return red
def _add_feature_values(feature_values, ax):
"""Display feature's values on left of plot."""
x_coord = ax.get_xlim()[0]
OFFSET = 0.15
for y_coord, (feat_name, feat_val) in enumerate(feature_values. items()):
t = plt.text(x_coord, y_coord - OFFSET, '{}'.format(feat_val), size=12)
t.set_bbox(dict(facecolor='white', alpha=0.5))
from matplotlib.font_manager import FontProperties
font = FontProperties()
font.set_weight('bold')
t = plt.text(x_coord, y_coord + 1 - OFFSET, 'feature\nvalue',
fontproperties=font, size=12)
def plot_example(example):
TOP_N = 8 # View top 8 features.
sorted_ix = example.abs().sort_values()[-TOP_N:].index # Sort by magnitude.
example = example[sorted_ix]
colors = example.map(_get_color).tolist()
ax = example.to_frame().plot(kind='barh',
color=[colors],
legend=None,
alpha=0.75,
figsize=(10,6))
ax.grid(False, axis='y')
ax.set_yticklabels(ax.get_yticklabels(), size=14)
# Add feature values.
_add_feature_values(xvalid.iloc[ID][sorted_ix], ax)
return ax
定义好样板代码后,我们可以以一种直接的方式绘制特定记录的详细图表:
ID = 10
example = df_dfc.iloc[ID] # Choose ith example from evaluation set.
TOP_N = 8 # View top 8 features.
sorted_ix = example.abs().sort_values()[-TOP_N:].index
ax = plot_example(example)
ax.set_title('Feature contributions for example {}\n pred: {:1.2f}; label: {}'.format(ID, probs[ID], labels[ID]))
ax.set_xlabel('Contribution to predicted probability', size=14)
plt.show()
输出如下:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_05_04.png
图 5.2:不同特征对预测概率的贡献
除了分析单个观测值的特征相关性外,我们还可以从全局(聚合)角度进行分析。全局可解释性指的是对模型整体的理解:我们将提取并可视化基于增益的特征重要性和置换特征重要性,同时也会展示聚合的 DFC。
基于增益的特征重要性衡量在对特定特征进行切分时损失变化,而置换特征重要性是通过评估模型在评估集上的表现来计算的,方法是将每个特征依次打乱,并将模型表现的变化归因于被打乱的特征。
通常,置换特征重要性比基于增益的特征重要性更为优选,尽管在潜在预测变量的度量尺度或类别数量不同,以及特征之间存在相关性时,二者的方法都可能不可靠。
计算置换特征重要性的函数如下:
def permutation_importances(est, X_eval, y_eval, metric, features):
"""Column by column, shuffle values and observe effect on eval set.
source: http://explained.ai/rf-importance/index.html
A similar approach can be done during training. See "Drop-column importance"
in the above article."""
baseline = metric(est, X_eval, y_eval)
imp = []
for col in features:
save = X_eval[col].copy()
X_eval[col] = np.random.permutation(X_eval[col])
m = metric(est, X_eval, y_eval)
X_eval[col] = save
imp.append(baseline - m)
return np.array(imp)
def accuracy_metric(est, X, y):
"""TensorFlow estimator accuracy."""
eval_input_fn = make_input_fn(X,
y=y,
shuffle=False,
n_epochs=1)
return est.evaluate(input_fn=eval_input_fn)['accuracy']
我们使用以下函数来显示最相关的列:
features = CATEGORICAL_COLUMNS + NUMERIC_COLUMNS
importances = permutation_importances(est, dfeval, y_eval, accuracy_metric,
features)
df_imp = pd.Series(importances, index=features)
sorted_ix = df_imp.abs().sort_values().index
ax = df_imp[sorted_ix][-5:].plot(kind='barh', color=sns_colors[2], figsize=(10, 6))
ax.grid(False, axis='y')
ax.set_title('Permutation feature importance')
plt.show()
这将为你输出如下结果:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_05_05.png
图 5.3:不同特征的置换特征重要性
我们使用以下函数以相同的方式显示增益特征重要性列:
importances = est.experimental_feature_importances(normalize=True)
df_imp = pd.Series(importances)
# Visualize importances.
N = 8
ax = (df_imp.iloc[0:N][::-1]
.plot(kind='barh',
color=sns_colors[0],
title='Gain feature importances',
figsize=(10, 6)))
ax.grid(False, axis='y')
这将为你输出如下结果:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_05_06.png
图 5.4:不同特征的增益特征重要性
DFCs 的绝对值可以被平均,以理解全局层面的影响:
dfc_mean = df_dfc.abs().mean()
N = 8
sorted_ix = dfc_mean.abs().sort_values()[-N:].index # Average and sort by absolute.
ax = dfc_mean[sorted_ix].plot(kind='barh',
color=sns_colors[1],
title='Mean |directional feature contributions|',
figsize=(10, 6))
ax.grid(False, axis='y')
这将输出以下结果:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_05_07.png
图 5.5: 不同特征的平均方向性特征贡献
在这个教程中,我们介绍了 GradientBoostingClassifier
的 TensorFlow 实现:一种灵活的模型架构,适用于广泛的表格数据问题。我们构建了一个模型来解决一个实际的业务问题:预测客户可能会取消酒店预订的概率,在这个过程中,我们介绍了 TF Boosted Trees 管道的所有相关组件:
-
为模型准备数据
-
使用
tf.estimator
配置GradientBoostingClassifier
-
评估特征重要性和模型可解释性,既要从全局层面,也要从局部层面
另见
有大量文章介绍梯度提升算法家族:
-
一篇精彩的 Medium 博文:
medium.com/analytics-vidhya/introduction-to-the-gradient-boosting-algorithm-c25c653f826b
-
官方 XGBoost 文档:
xgboost.readthedocs.io/en/latest/tutorials/model.html
-
LightGBM 文档:
papers.nips.cc/paper/6907-lightgbm-a-highly-efficient-gradient-boosting-decision-tree.pdf
第六章:神经网络
在本章中,我们将介绍神经网络以及如何在 TensorFlow 中实现它们。后续大部分章节将基于神经网络,因此学习如何在 TensorFlow 中使用它们是非常重要的。
神经网络目前在图像和语音识别、阅读手写字、理解文本、图像分割、对话系统、自动驾驶汽车等任务中创下了纪录。虽然这些任务将在后续章节中涵盖,但介绍神经网络作为一种通用、易于实现的机器学习算法是很重要的,这样我们后面可以进一步展开。
神经网络的概念已经存在了几十年。然而,直到最近,由于处理能力、算法效率和数据规模的进步,我们才有足够的计算能力来训练大型网络,因此神经网络才开始获得广泛关注。
神经网络本质上是一系列操作应用于输入数据矩阵的过程。这些操作通常是加法和乘法的组合,之后应用非线性函数。我们已经见过的一个例子是逻辑回归,这在第四章、线性回归中有所讨论。逻辑回归是各个偏斜特征乘积的和,之后应用非线性的 sigmoid 函数。神经网络通过允许任意组合的操作和非线性函数进行更广泛的泛化,这包括应用绝对值、最大值、最小值等。
神经网络中最重要的技巧叫做反向传播。反向传播是一种允许我们根据学习率和损失函数输出更新模型变量的过程。我们在第三章、Keras和第四章、线性回归中都使用了反向传播来更新模型变量。
关于神经网络,另一个需要注意的重要特性是非线性激活函数。由于大多数神经网络只是加法和乘法操作的组合,它们无法对非线性数据集进行建模。为了解决这个问题,我们将在神经网络中使用非线性激活函数。这将使神经网络能够适应大多数非线性情况。
需要记住的是,正如我们在许多已介绍的算法中看到的,神经网络对我们选择的超参数非常敏感。在本章中,我们将探讨不同学习率、损失函数和优化过程的影响。
还有一些我推荐的学习神经网络的资源,它们会更深入、更详细地讲解这个话题:
-
描述反向传播的开创性论文是 Yann LeCun 等人所写的Efficient Back Prop。PDF 文件位于这里:
yann.lecun.com/exdb/publis/pdf/lecun-98b.pdf
。 -
CS231,卷积神经网络与视觉识别,由斯坦福大学提供。课程资源可以在这里找到:
cs231n.stanford.edu/
。 -
CS224d,自然语言处理的深度学习,由斯坦福大学提供。课程资源可以在这里找到:
cs224d.stanford.edu/
。 -
深度学习,由 MIT 出版社出版的书籍。Goodfellow 等,2016 年。本书可以在这里找到:
www.deeplearningbook.org
。 -
在线书籍 神经网络与深度学习 由 Michael Nielsen 编写,可以在这里找到:
neuralnetworksanddeeplearning.com/
。 -
对于更务实的方法和神经网络的介绍,Andrej Karpathy 写了一个很棒的总结,里面有 JavaScript 示例,叫做 黑客的神经网络指南。该文可以在这里找到:
karpathy.github.io/neuralnets/
。 -
另一个很好的总结深度学习的网站是 深度学习入门,由 Ian Goodfellow、Yoshua Bengio 和 Aaron Courville 编写。该网页可以在这里找到:
randomekek.github.io/deep/deeplearning.html
。
我们将从介绍神经网络的基本概念开始,然后逐步深入到多层网络。在最后一节,我们将创建一个神经网络,学习如何玩井字游戏。
在本章中,我们将覆盖以下内容:
-
实现操作门
-
与门和激活函数的工作
-
实现单层神经网络
-
实现不同的层
-
使用多层神经网络
-
改善线性模型的预测
-
学习玩井字游戏
读者可以在 github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook
上找到本章的所有代码,并在 Packt 的代码库中找到:github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook
。
实现操作门
神经网络最基本的概念之一是它作为操作门的功能。在本节中,我们将从乘法运算作为门开始,然后再考虑嵌套的门操作。
准备工作
我们将实现的第一个操作门是 f(x) = a · x:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_01.png
为了优化这个门,我们将 a 输入声明为变量,并将x作为我们模型的输入张量。这意味着 TensorFlow 会尝试改变 a 的值,而不是 x 的值。我们将创建损失函数,它是输出值和目标值之间的差异,目标值为 50。
第二个嵌套的操作门将是 f(x) = a · x + b:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_02.png
再次,我们将声明 a 和 b 为变量,x 为我们模型的输入张量。我们再次优化输出,使其趋向目标值 50。需要注意的有趣之处在于,第二个示例的解并不是唯一的。有许多不同的模型变量组合都能使输出为 50。对于神经网络,我们并不太在意中间模型变量的值,而更关注最终的输出值。
如何操作…
为了在 TensorFlow 中实现第一个运算门 f(x) = a · x,并将输出训练到值 50,请按照以下步骤操作:
-
首先,通过以下方式加载 TensorFlow:
import tensorflow as tf
-
现在我们需要声明我们的模型变量和输入数据。我们将输入数据设为 5,这样乘数因子将是 10(即 5*10=50),具体如下:
a = tf.Variable(4.) x_data = tf.keras.Input(shape=(1,)) x_val = 5.
-
接下来,我们创建一个 Lambda 层来计算操作,并创建一个具有以下输入的功能性 Keras 模型:
multiply_layer = tf.keras.layers.Lambda(lambda x:tf.multiply(a, x)) outputs = multiply_layer(x_data) model = tf.keras.Model(inputs=x_data, outputs=outputs, name="gate_1")
-
现在,我们将声明我们的优化算法为随机梯度下降,具体如下:
optimizer=tf.keras.optimizers.SGD(0.01)
-
现在,我们可以优化我们的模型输出,使其趋向目标值 50。我们将使用损失函数作为输出与目标值 50 之间的 L2 距离。我们通过不断输入值 5 并反向传播损失来更新模型变量,使其趋向 10,具体如下所示:
print('Optimizing a Multiplication Gate Output to 50.') for i in range(10): # Open a GradientTape. with tf.GradientTape() as tape: # Forward pass. mult_output = model(x_val) # Loss value as the difference between # the output and a target value, 50. loss_value = tf.square(tf.subtract(mult_output, 50.)) # Get gradients of loss with reference to the variable "a" to adjust. gradients = tape.gradient(loss_value, a) # Update the variable "a" of the model. optimizer.apply_gradients(zip([gradients], [a])) print("{} * {} = {}".format(a.numpy(), x_val, a.numpy() * x_val))
-
前面的步骤应该会得到以下输出:
Optimizing a Multiplication Gate Output to 50\. 7.0 * 5.0 = 35.0 8.5 * 5.0 = 42.5 9.25 * 5.0 = 46.25 9.625 * 5.0 = 48.125 9.8125 * 5.0 = 49.0625 9.90625 * 5.0 = 49.5312 9.95312 * 5.0 = 49.7656 9.97656 * 5.0 = 49.8828 9.98828 * 5.0 = 49.9414 9.99414 * 5.0 = 49.9707
接下来,我们将对嵌套的两个运算门 f(x) = a · x + b 进行相同的操作。
-
我们将以与前一个示例完全相同的方式开始,但会初始化两个模型变量,
a
和b
,具体如下:import tensorflow as tf # Initialize variables and input data x_data = tf.keras.Input(dtype=tf.float32, shape=(1,)) x_val = 5. a = tf.Variable(1., dtype=tf.float32) b = tf.Variable(1., dtype=tf.float32) # Add a layer which computes f(x) = a * x multiply_layer = tf.keras.layers.Lambda(lambda x:tf.multiply(a, x)) # Add a layer which computes f(x) = b + x add_layer = tf.keras.layers.Lambda(lambda x:tf.add(b, x)) res = multiply_layer(x_data) outputs = add_layer(res) # Build the model model = tf.keras.Model(inputs=x_data, outputs=outputs, name="gate_2") # Optimizer optimizer=tf.keras.optimizers.SGD(0.01)
-
我们现在优化模型变量,将输出训练到目标值 50,具体如下所示:
print('Optimizing two Gate Output to 50.') for i in range(10): # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. two_gate_output = model(x_val) # Loss value as the difference between # the output and a target value, 50. loss_value = tf.square(tf.subtract(two_gate_output, 50.)) # Get gradients of loss with reference to # the variables "a" and "b" to adjust. gradients_a = tape.gradient(loss_value, a) gradients_b = tape.gradient(loss_value , b) # Update the variables "a" and "b" of the model. optimizer.apply_gradients(zip([gradients_a, gradients_b], [a, b])) print("Step: {} ==> {} * {} + {}= {}".format(i, a.numpy(), x_val, b.numpy(), a.numpy()*x_val+b.numpy()))
-
前面的步骤应该会得到以下输出:
Optimizing Two Gate Output to 50\. 5.4 * 5.0 + 1.88 = 28.88 7.512 * 5.0 + 2.3024 = 39.8624 8.52576 * 5.0 + 2.50515 = 45.134 9.01236 * 5.0 + 2.60247 = 47.6643 9.24593 * 5.0 + 2.64919 = 48.8789 9.35805 * 5.0 + 2.67161 = 49.4619 9.41186 * 5.0 + 2.68237 = 49.7417 9.43769 * 5.0 + 2.68754 = 49.876 9.45009 * 5.0 + 2.69002 = 49.9405 9.45605 * 5.0 + 2.69121 = 49.9714
需要注意的是,第二个示例的解并不是唯一的。这在神经网络中并不那么重要,因为所有参数都被调整以减少损失。这里的最终解将取决于 a 和 b 的初始值。如果这些值是随机初始化的,而不是设为 1,我们会看到每次迭代后模型变量的最终值不同。
它是如何工作的…
我们通过 TensorFlow 的隐式反向传播实现了计算门的优化。TensorFlow 跟踪我们模型的操作和变量值,并根据我们的优化算法规范和损失函数的输出进行调整。
我们可以继续扩展运算门,同时跟踪哪些输入是变量,哪些输入是数据。这一点非常重要,因为 TensorFlow 会调整所有变量以最小化损失,但不会更改数据。
能够隐式跟踪计算图并在每次训练步骤中自动更新模型变量,是 TensorFlow 的一个重要特点,这也使得它非常强大。
与门和激活函数的工作
现在,我们可以将操作门连接在一起,我们希望通过激活函数运行计算图输出。在本节中,我们将介绍常见的激活函数。
准备工作
在本节中,我们将比较并对比两种不同的激活函数:sigmoid和rectified linear unit(ReLU)。回顾一下,这两个函数由以下方程给出:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_03.pnghttps://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_04.png
在这个例子中,我们将创建两个单层神经网络,它们具有相同的结构,唯一不同的是一个使用 sigmoid 激活函数,另一个使用 ReLU 激活函数。损失函数将由与 0.75 的 L2 距离决定。我们将随机提取批处理数据,然后优化输出使其趋向 0.75。
如何操作…
我们按以下步骤进行操作:
-
我们将从加载必要的库开始。这也是我们可以提到如何在 TensorFlow 中设置随机种子的好时机。由于我们将使用 NumPy 和 TensorFlow 的随机数生成器,因此我们需要为两者设置随机种子。设置相同的随机种子后,我们应该能够复制结果。我们通过以下输入来实现:
import tensorflow as tf import numpy as np import matplotlib.pyplot as plt tf.random.set_seed(5) np.random.seed(42)
-
现在,我们需要声明我们的批处理大小、模型变量和数据模型输入。我们的计算图将包括将正态分布数据输入到两个相似的神经网络中,这两个网络仅在末端的激活函数上有所不同,如下所示:
batch_size = 50 x_data = tf.keras.Input(shape=(1,)) x_data = tf.keras.Input(shape=(1,)) a1 = tf.Variable(tf.random.normal(shape=[1,1], seed=5)) b1 = tf.Variable(tf.random.uniform(shape=[1,1], seed=5)) a2 = tf.Variable(tf.random.normal(shape=[1,1], seed=5)) b2 = tf.Variable(tf.random.uniform(shape=[1,1], seed=5))
-
接下来,我们将声明我们的两个模型,sigmoid 激活模型和 ReLU 激活模型,如下所示:
class MyCustomGateSigmoid(tf.keras.layers.Layer): def __init__(self, units, a1, b1): super(MyCustomGateSigmoid, self).__init__() self.units = units self.a1 = a1 self.b1 = b1 # Compute f(x) = sigmoid(a1 * x + b1) def call(self, inputs): return tf.math.sigmoid(inputs * self.a1 + self.b1) # Add a layer which computes f(x) = sigmoid(a1 * x + b1) my_custom_gate_sigmoid = MyCustomGateSigmoid(units=1, a1=a1, b1=b1) output_sigmoid = my_custom_gate_sigmoid(x_data) # Build the model model_sigmoid = tf.keras.Model(inputs=x_data, outputs=output_sigmoid, name="gate_sigmoid") class MyCustomGateRelu(tf.keras.layers.Layer): def __init__(self, units, a2, b2): super(MyCustomGateRelu, self).__init__() self.units = units self.a2 = a2 self.b2 = b2 # Compute f(x) = relu(a2 * x + b2) def call(self, inputs): return tf.nn.relu(inputs * self.a2 + self.b2) # Add a layer which computes f(x) = relu(a2 * x + b2) my_custom_gate_relu = MyCustomGateRelu(units=1, a2=a2, b2=b2) outputs_relu = my_custom_gate_relu(x_data) # Build the model model_relu = tf.keras.Model(inputs=x_data, outputs=outputs_relu, name="gate_relu")
-
现在我们需要声明我们的优化算法并初始化变量,如下所示:
optimizer=tf.keras.optimizers.SGD(0.01)
-
现在,我们将对两个模型进行 750 次迭代训练,如下所示的代码块所示。损失函数将是模型输出与 0.75 的 L2 范数平均值。我们还将保存损失输出和激活输出值,稍后用于绘图:
# Run loop across gate print('\n Optimizing Sigmoid AND Relu Output to 0.75') loss_vec_sigmoid = [] loss_vec_relu = [] activation_sigmoid = [] activation_relu = [] for i in range(500): rand_indices = np.random.choice(len(x), size=batch_size) x_vals = np.transpose([x[rand_indices]]) # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. output_sigmoid = model_sigmoid(x_vals) output_relu = model_relu(x_vals) # Loss value as the difference as the difference between # the output and a target value, 0.75. loss_sigmoid = tf.reduce_mean(tf.square(tf.subtract(output_sigmoid, 0.75))) loss_vec_sigmoid.append(loss_sigmoid) loss_relu = tf.reduce_mean(tf.square(tf.subtract(output_relu, 0.75))) loss_vec_relu.append(loss_relu) # Get gradients of loss_sigmoid with reference to the variable "a1" and "b1" to adjust. gradients_a1 = tape.gradient(loss_sigmoid, my_custom_gate_sigmoid.a1) gradients_b1 = tape.gradient(loss_sigmoid , my_custom_gate_sigmoid.b1) # Get gradients of loss_relu with reference to the variable "a2" and "b2" to adjust. gradients_a2 = tape.gradient(loss_relu, my_custom_gate_relu.a2) gradients_b2 = tape.gradient(loss_relu , my_custom_gate_relu.b2) # Update the variable "a1" and "b1" of the model. optimizer.apply_gradients(zip([gradients_a1, gradients_b1], [my_custom_gate_sigmoid.a1, my_custom_gate_sigmoid.b1])) # Update the variable "a2" and "b2" of the model. optimizer.apply_gradients(zip([gradients_a2, gradients_b2], [my_custom_gate_relu.a2, my_custom_gate_relu.b2])) output_sigmoid = model_sigmoid(x_vals) output_relu = model_relu(x_vals) activation_sigmoid.append(np.mean(output_sigmoid)) activation_relu.append(np.mean(output_relu)) if i%50==0: print('sigmoid = ' + str(np.mean(output_sigmoid)) + ' relu = ' + str(np.mean(output_relu)))
-
要绘制损失和激活输出,我们需要输入以下代码:
plt.plot(activation_sigmoid, 'k-', label='Sigmoid Activation') plt.plot(activation_relu, 'r--', label='Relu Activation') plt.ylim([0, 1.0]) plt.title('Activation Outputs') plt.xlabel('Generation') plt.ylabel('Outputs') plt.legend(loc='upper right') plt.show() plt.plot(loss_vec_sigmoid, 'k-', label='Sigmoid Loss') plt.plot(loss_vec_relu, 'r--', label='Relu Loss') plt.ylim([0, 1.0]) plt.title('Loss per Generation') plt.xlabel('Generation') plt.ylabel('Loss') plt.legend(loc='upper right') plt.show()
激活输出需要如以下图所示进行绘制:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_05.png
图 6.1:具有 sigmoid 激活函数的网络与具有 ReLU 激活函数的网络的计算图输出
这两个神经网络具有相似的架构和目标(0.75),但使用了两种不同的激活函数:sigmoid 和 ReLU。需要注意的是,ReLU 激活网络比 sigmoid 激活网络更快速地收敛到 0.75 这一目标,如下图所示:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_06.png
图 6.2:该图展示了 sigmoid 和 ReLU 激活网络的损失值。注意 ReLU 损失在迭代初期的极端情况
它是如何工作的…
由于 ReLU 激活函数的形式,它比 sigmoid 函数更频繁地返回零值。我们将这种行为视为一种稀疏性。这种稀疏性加速了收敛速度,但却失去了对梯度的控制。另一方面,sigmoid 函数具有非常好的梯度控制,且不像 ReLU 激活函数那样会产生极端值,以下表格进行了说明:
激活函数 | 优势 | 劣势 |
---|---|---|
Sigmoid | 输出较少极端 | 收敛较慢 |
ReLU | 快速收敛 | 可能产生极端输出值 |
还有更多内容…
在本节中,我们比较了 ReLU 激活函数和 sigmoid 激活函数在神经网络中的表现。虽然有许多其他激活函数常用于神经网络,但大多数都属于以下两类:第一类包含类似 sigmoid 函数形状的函数,如 arctan、hypertangent、Heaviside 阶跃函数等;第二类包含类似 ReLU 函数形状的函数,如 softplus、leaky ReLU 等。我们在本节中讨论的关于比较这两种函数的大部分内容,适用于这两类激活函数。然而,值得注意的是,激活函数的选择对神经网络的收敛性和输出有很大影响。
实现一个单层神经网络
我们已经具备了实施神经网络所需的所有工具,因此在本节中,我们将创建一个在Iris
数据集上运行的单层神经网络。
准备工作
在本节中,我们将实现一个具有一个隐藏层的神经网络。理解全连接神经网络大多数基于矩阵乘法是非常重要的。因此,确保数据和矩阵的维度正确对齐也很重要。
由于这是一个回归问题,我们将使用均方误差(MSE)作为损失函数。
如何操作…
我们按如下步骤继续操作:
-
为了创建计算图,我们将从加载以下必要的库开始:
import matplotlib.pyplot as plt import numpy as np import tensorflow as tf from sklearn import datasets
-
现在,我们将加载
Iris
数据集,并通过以下代码将长度存储为目标值:iris = datasets.load_iris() x_vals = np.array([x[0:3] for x in iris.data]) y_vals = np.array([x[3] for x in iris.data])
-
由于数据集较小,我们希望设置一个种子,使得结果可复现,具体如下:
seed = 3 tf.set_random_seed(seed) np.random.seed(seed)
-
为了准备数据,我们将创建一个 80-20 的训练集和测试集拆分,并通过 min-max 缩放将
x
特征规范化到0
和1
之间,具体如下所示:train_indices = np.random.choice(len(x_vals), round(len(x_vals)*0.8), replace=False) test_indices = np.array(list(set(range(len(x_vals))) - set(train_indices))) x_vals_train = x_vals[train_indices] x_vals_test = x_vals[test_indices] y_vals_train = y_vals[train_indices] y_vals_test = y_vals[test_indices] def normalize_cols(m): col_max = m.max(axis=0) col_min = m.min(axis=0) return (m-col_min) / (col_max - col_min) x_vals_train = np.nan_to_num(normalize_cols(x_vals_train)) x_vals_test = np.nan_to_num(normalize_cols(x_vals_test))
-
现在,我们将声明批次大小和数据模型输入,具体代码如下:
batch_size = 50 x_data = tf.keras.Input(dtype=tf.float32, shape=(3,))
-
关键部分是用适当的形状声明我们的模型变量。我们可以声明我们隐藏层的大小为任意大小;在下面的代码块中,我们设置它为五个隐藏节点:
hidden_layer_nodes = 5 a1 = tf.Variable(tf.random.normal(shape=[3,hidden_layer_nodes], seed=seed)) b1 = tf.Variable(tf.random.normal(shape=[hidden_layer_nodes], seed=seed)) a2 = tf.Variable(tf.random.normal(shape=[hidden_layer_nodes,1], seed=seed)) b2 = tf.Variable(tf.random.normal(shape=[1], seed=seed))
-
现在我们将分两步声明我们的模型。第一步将创建隐藏层的输出,第二步将创建模型的
final_output
,如下所示:请注意,我们的模型从三个输入特征到五个隐藏节点,最后到一个输出值。
hidden_output = tf.keras.layers.Lambda(lambda x: tf.nn.relu(tf.add(tf.matmul(x, a1), b1))) final_output = tf.keras.layers.Lambda(lambda x: tf.nn.relu(tf.add(tf.matmul(x, a2), b2))) model = tf.keras.Model(inputs=x_data, outputs=output, name="1layer_neural_network")
-
现在我们将使用以下代码声明优化算法:
optimizer = tf.keras.optimizers.SGD(0.005)
-
接下来,我们循环执行训练迭代。我们还将初始化两个列表,用于存储我们的
train
和test_loss
函数。在每个循环中,我们还希望从训练数据中随机选择一个批次,以适应模型,如下所示:# First we initialize the loss vectors for storage. loss_vec = [] test_loss = [] for i in range(500): rand_index = np.random.choice(len(x_vals_train), size=batch_size) rand_x = x_vals_train[rand_index] rand_y = np.transpose([y_vals_train[rand_index]]) # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. output = model(rand_x) # Apply loss function (MSE) loss = tf.reduce_mean(tf.square(rand_y - output)) loss_vec.append(np.sqrt(loss)) # Get gradients of loss with reference to the variables to adjust. gradients_a1 = tape.gradient(loss, a1) gradients_b1 = tape.gradient(loss, b1) gradients_a2 = tape.gradient(loss, a2) gradients_b2 = tape.gradient(loss, b2) # Update the variables of the model. optimizer.apply_gradients(zip([gradients_a1, gradients_b1, gradients_a2, gradients_b2], [a1, b1, a2, b2])) # Forward pass. output_test = model(x_vals_test) # Apply loss function (MSE) on test loss_test = tf.reduce_mean(tf.square(np.transpose([y_vals_test]) - output_test)) test_loss.append(np.sqrt(loss_test)) if (i+1)%50==0: print('Generation: ' + str(i+1) + '. Loss = ' + str(np.mean(loss))) print('Generation: ' + str(i+1) + '. Loss = ' + str(temp_loss))
-
我们可以用
matplotlib
和以下代码绘制损失:plt.plot(loss_vec, 'k-', label='Train Loss') plt.plot(test_loss, 'r--', label='Test Loss') plt.title('Loss (MSE) per Generation') plt.xlabel('Generation') plt.ylabel('Loss') plt.legend(loc='upper right') plt.show()
我们继续通过绘制以下图表来进行本次实验:
https://example.org(img/B16254_06_07.png)
图 6.3:我们绘制了训练集和测试集的损失(MSE)
注意,我们还可以看到训练集的损失不像测试集那样平滑。这是由于两个原因:首先,我们使用的批量大小比测试集小,尽管差距不大;第二个原因是我们在训练集上训练,而测试集不影响模型的变量。
工作原理…
我们的模型现在已经被可视化为神经网络图,如下所示:
https://example.org(img/B16254_06_08.png)
图 6.4:神经网络图
前面的图是我们的神经网络的可视化,隐藏层有五个节点。我们输入三个值:sepal length(S.L.)、sepal width(S.W.)和petal length(P.L.)。目标将是花瓣宽度。总共,模型中将有 26 个变量。
实现不同的层
重要的是要知道如何实现不同的层。在前面的示例中,我们实现了全连接层。在这个示例中,我们将进一步扩展我们对各种层的了解。
准备工作
我们已经探讨了如何连接数据输入和完全连接的隐藏层,但是在 TensorFlow 中还有更多类型的内置函数可用作层。最常用的层是卷积层和最大池层。我们将展示如何在一维数据和二维数据上创建和使用这些层。首先,我们将看看如何在一维数据上使用这些层,然后是二维数据。
尽管神经网络可以以任何方式分层,但最常见的设计之一是首先使用卷积层和全连接层创建特征。如果然后有太多的特征,常见的做法是使用最大池层。
在这些层之后,通常会引入非线性层作为激活函数。卷积神经网络(CNNs),我们将在第八章中讨论,通常具有卷积层、最大池化层和激活层。
如何操作…
我们将首先查看一维数据。我们需要为这个任务生成一个随机数据数组,操作步骤如下:
-
我们将首先加载所需的库,如下所示:
import tensorflow as tf import numpy as np
-
现在我们将初始化一些参数,并使用以下代码创建输入数据层:
data_size = 25 conv_size = 5 maxpool_size = 5 stride_size = 1 num_outputs = 5 x_input_1d = tf.keras.Input(dtype=tf.float32, shape=(data_size,1), name="input_layer")
-
接下来,我们将定义一个卷积层,如下所示:
对于我们的示例数据,批处理大小为
1
,宽度为1
,高度为25
,通道大小为1
。还请注意,我们可以通过output_size=(W-F+2P)/S+1
公式来计算卷积层的输出尺寸,其中W
是输入尺寸,F
是滤波器尺寸,P
是填充尺寸,S
是步幅尺寸。my_conv_output = tf.keras.layers.Conv1D(kernel_size=(conv_size), filters=data_size, strides=stride_size, padding="VALID", name="convolution_layer")(x_input_1d)
-
接下来,我们添加一个 ReLU 激活层,如下所示:
my_activation_output = tf.keras.layers.ReLU(name="activation_layer")(my_conv_output)
-
现在我们将添加一个最大池化层。这个层将在我们的一维向量上创建一个
maxpool
,并在一个移动窗口中应用。对于这个例子,我们将初始化它,使宽度为 5,如下所示:TensorFlow 的
maxpool
参数与卷积层的非常相似。虽然maxpool
参数没有滤波器,但它有大小、步幅和填充选项。由于我们有一个宽度为 5 的窗口,并且使用有效填充(没有零填充),所以我们的输出数组将少 4 个元素。my_maxpool_output = tf.keras.layers.MaxPool1D(strides=stride_size, pool_size=maxpool_size, padding='VALID', name="maxpool_layer")(my_activation_output)
-
我们将连接的最后一层是全连接层。在这里,我们将使用一个密集层,如下所示的代码块:
my_full_output = tf.keras.layers.Dense(units=num_outputs, name="fully_connected_layer")(my_maxpool_output)
-
现在我们将创建模型,并打印每一层的输出,如下所示:
print('>>>> 1D Data <<<<') model_1D = tf.keras.Model(inputs=x_input_1d, outputs=my_full_output, name="model_1D") model_1D.summary() # Input print('\n== input_layer ==') print('Input = array of length %d' % (x_input_1d.shape.as_list()[1])) # Convolution print('\n== convolution_layer ==') print('Convolution w/ filter, length = %d, stride size = %d, results in an array of length %d' % (conv_size,stride_size,my_conv_output.shape.as_list()[1])) # Activation print('\n== activation_layer ==') print('Input = above array of length %d' % (my_conv_output.shape.as_list()[1])) print('ReLU element wise returns an array of length %d' % (my_activation_output.shape.as_list()[1])) # Max Pool print('\n== maxpool_layer ==') print('Input = above array of length %d' % (my_activation_output.shape.as_list()[1])) print('MaxPool, window length = %d, stride size = %d, results in the array of length %d' % (maxpool_size,stride_size,my_maxpool_output.shape.as_list()[1])) # Fully Connected print('\n== fully_connected_layer ==') print('Input = above array of length %d' % (my_maxpool_output.shape.as_list()[1])) print('Fully connected layer on all 4 rows with %d outputs' % (my_full_output.shape.as_list()[1]))
-
前面的步骤应该会生成以下输出:
>>>> 1D Data <<<< Model: "model_1D" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_layer (InputLayer) [(None, 25, 1)] 0 _________________________________________________________________ convolution_layer (Conv1D) (None, 21, 25) 150 _________________________________________________________________ activation_layer (ReLU) (None, 21, 25) 0 _________________________________________________________________ maxpool_layer (MaxPooling1D) (None, 17, 25) 0 _________________________________________________________________ fully_connected_layer (Dense (None, 17, 5) 130 ================================================================= Total params: 280 Trainable params: 280 Non-trainable params: 0 _________________________________________________________________ == input_layer == Input = array of length 25 == convolution_layer == Convolution w/ filter, length = 5, stride size = 1, results in an array of length 21 == activation_layer == Input = above array of length 21 ReLU element wise returns an array of length 21 == maxpool_layer == Input = above array of length 21 MaxPool, window length = 5, stride size = 1, results in the array of length 17 == fully_connected_layer == Input = above array of length 17 Fully connected layer on all 4 rows with 17 outputs
一维数据对神经网络来说非常重要。时间序列、信号处理和一些文本嵌入都被认为是一维数据,并且在神经网络中被频繁使用。
现在我们将考虑相同类型的层,但对于二维数据,顺序是等效的:
-
我们将首先初始化变量,如下所示:
row_size = 10 col_size = 10 conv_size = 2 conv_stride_size = 2 maxpool_size = 2 maxpool_stride_size = 1 num_outputs = 5
-
然后我们将初始化输入数据层。由于我们的数据已经有了高度和宽度,我们只需要在两个维度上扩展它(批处理大小为 1,通道大小为 1),如下所示:
x_input_2d = tf.keras.Input(dtype=tf.float32, shape=(row_size,col_size, 1), name="input_layer_2d")
-
就像一维示例中一样,现在我们需要添加一个二维卷积层。对于滤波器,我们将使用一个随机的 2x2 滤波器,步幅为 2,方向上都为 2,并使用有效填充(换句话说,不使用零填充)。由于我们的输入矩阵是 10x10,因此卷积输出将是 5x5,如下所示:
my_convolution_output_2d = tf.keras.layers.Conv2D(kernel_size=(conv_size), filters=conv_size, strides=conv_stride_size, padding="VALID", name="convolution_layer_2d")(x_input_2d)
-
接下来,我们添加一个 ReLU 激活层,如下所示:
my_activation_output_2d = tf.keras.layers.ReLU(name="activation_layer_2d")(my_convolution_output_2d)
-
我们的最大池化层与一维情况非常相似,唯一不同的是我们需要为最大池化窗口和步幅声明宽度和高度。在我们的例子中,我们将在所有空间维度上使用相同的值,因此我们将设置整数值,如下所示:
my_maxpool_output_2d = tf.keras.layers.MaxPool2D(strides=maxpool_stride_size, pool_size=maxpool_size, padding='VALID', name="maxpool_layer_2d")(my_activation_output_2d)
-
我们的全连接层与一维输出非常相似。我们使用一个稠密层,具体如下:
my_full_output_2d = tf.keras.layers.Dense(units=num_outputs, name="fully_connected_layer_2d")(my_maxpool_output_2d)
-
现在我们将创建模型,并打印每一层的输出,具体如下:
print('>>>> 2D Data <<<<') model_2D = tf.keras.Model(inputs=x_input_2d, outputs=my_full_output_2d, name="model_2D") model_2D.summary() # Input print('\n== input_layer ==') print('Input = %s array' % (x_input_2d.shape.as_list()[1:3])) # Convolution print('\n== convolution_layer ==') print('%s Convolution, stride size = [%d, %d] , results in the %s array' % ([conv_size,conv_size],conv_stride_size,conv_stride_size,my_convolution_output_2d.shape.as_list()[1:3])) # Activation print('\n== activation_layer ==') print('Input = the above %s array' % (my_convolution_output_2d.shape.as_list()[1:3])) print('ReLU element wise returns the %s array' % (my_activation_output_2d.shape.as_list()[1:3])) # Max Pool print('\n== maxpool_layer ==') print('Input = the above %s array' % (my_activation_output_2d.shape.as_list()[1:3])) print('MaxPool, stride size = [%d, %d], results in %s array' % (maxpool_stride_size,maxpool_stride_size,my_maxpool_output_2d.shape.as_list()[1:3])) # Fully Connected print('\n== fully_connected_layer ==') print('Input = the above %s array' % (my_maxpool_output_2d.shape.as_list()[1:3])) print('Fully connected layer on all %d rows results in %s outputs' % (my_maxpool_output_2d.shape.as_list()[1],my_full_output_2d.shape.as_list()[3])) feed_dict = {x_input_2d: data_2d}
-
上述步骤应该会得到以下输出:
>>>> 2D Data <<<< Model: "model_2D" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_layer_2d (InputLayer) [(None, 10, 10, 1)] 0 _________________________________________________________________ convolution_layer_2d (Conv2D (None, 5, 5, 2) 10 _________________________________________________________________ activation_layer_2d (ReLU) (None, 5, 5, 2) 0 _________________________________________________________________ maxpool_layer_2d (MaxPooling (None, 4, 4, 2) 0 _________________________________________________________________ fully_connected_layer_2d (De (None, 4, 4, 5) 15 ================================================================= Total params: 25 Trainable params: 25 Non-trainable params: 0 _________________________________________________________________ == input_layer == Input = [10, 10] array == convolution_layer == [2, 2] Convolution, stride size = [2, 2] , results in the [5, 5] array == activation_layer == Input = the above [5, 5] array ReLU element wise returns the [5, 5] array == maxpool_layer == Input = the above [5, 5] array MaxPool, stride size = [1, 1], results in [4, 4] array == fully_connected_layer == Input = the above [4, 4] array Fully connected layer on all 4 rows results in 5 outputs
它是如何工作的…
我们现在应该知道如何在 TensorFlow 中使用卷积层和最大池化层,处理一维和二维数据。不管输入的形状如何,我们最终都会得到相同大小的输出。这对于展示神经网络层的灵活性非常重要。本节还应再次提醒我们,形状和大小在神经网络操作中的重要性。
使用多层神经网络
我们将通过在低出生体重数据集上使用多层神经网络,应用我们对不同层的知识于实际数据。
准备工作
现在我们知道如何创建神经网络并处理层,我们将应用这种方法来预测低出生体重数据集中的出生体重。我们将创建一个有三层隐藏层的神经网络。低出生体重数据集包括实际出生体重和一个指示变量,表明给定的出生体重是否高于或低于 2500 克。在这个示例中,我们将目标设为实际出生体重(回归),然后查看分类结果的准确性。最终,我们的模型应该能够判断出生体重是否低于 2500 克。
如何操作…
我们按照以下步骤继续操作:
-
我们将开始加载库,具体如下:
import tensorflow as tf import matplotlib.pyplot as plt import csv import random import numpy as np import requests import os
-
我们将使用
requests
模块从网站加载数据。之后,我们将把数据拆分为感兴趣的特征和目标值,具体如下:# name of data file birth_weight_file = 'birth_weight.csv' # download data and create data file if file does not exist in current directory if not os.path.exists(birth_weight_file): birthdata_url = 'https://github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook/blob/master/ch6/06_Using_Multiple_Layers/birth_weight.csv' birth_file = requests.get(birthdata_url) birth_data = birth_file.text.split('\r\n') birth_header = birth_data[0].split('\t') birth_data = [[float(x) for x in y.split('\t') if len(x)>=1] for y in birth_data[1:] if len(y)>=1] with open(birth_weight_file, "w") as f: writer = csv.writer(f) writer.writerows([birth_header]) writer.writerows(birth_data) f.close() # read birth weight data into memory birth_data = [] with open(birth_weight_file, newline='') as csvfile: csv_reader = csv.reader(csvfile) birth_header = next(csv_reader) for row in csv_reader: birth_data.append(row) birth_data = [[float(x) for x in row] for row in birth_data] # Extract y-target (birth weight) y_vals = np.array([x[8] for x in birth_data]) # Filter for features of interest cols_of_interest = ['AGE', 'LWT', 'RACE', 'SMOKE', 'PTL', 'HT', 'UI'] x_vals = np.array([[x[ix] for ix, feature in enumerate(birth_header) if feature in cols_of_interest] for x in birth_data])
-
为了帮助结果的可重复性,我们现在需要为 NumPy 和 TensorFlow 设置随机种子。然后我们声明我们的批处理大小,具体如下:
# make results reproducible seed = 3 np.random.seed(seed) tf.random.set_seed(seed) # set batch size for training batch_size = 150
-
接下来,我们将数据分割为 80-20 的训练集和测试集。之后,我们需要对输入特征进行归一化,使其值在 0 到 1 之间,采用最小-最大缩放,具体如下:
train_indices = np.random.choice(len(x_vals), round(len(x_vals)*0.8), replace=False) test_indices = np.array(list(set(range(len(x_vals))) - set(train_indices))) x_vals_train = x_vals[train_indices] x_vals_test = x_vals[test_indices] y_vals_train = y_vals[train_indices] y_vals_test = y_vals[test_indices] # Record training column max and min for scaling of non-training data train_max = np.max(x_vals_train, axis=0) train_min = np.min(x_vals_train, axis=0) # Normalize by column (min-max norm to be between 0 and 1) def normalize_cols(mat, max_vals, min_vals): return (mat - min_vals) / (max_vals - min_vals) x_vals_train = np.nan_to_num(normalize_cols(x_vals_train, train_max, train_min)) x_vals_test = np.nan_to_num(normalize_cols(x_vals_test, train_max, train_min))
对输入特征进行归一化是一种常见的特征转换方法,尤其对于神经网络特别有用。如果我们的数据在 0 到 1 之间进行中心化,这将有助于激活函数的收敛。
-
由于我们有多个具有相似初始化变量的层,现在我们需要创建一个函数来初始化权重和偏置。我们使用以下代码完成此任务:
# Define Variable Functions (weights and bias) def init_weight(shape, st_dev): weight = tf.Variable(tf.random.normal(shape, stddev=st_dev)) return(weight) def init_bias(shape, st_dev): bias = tf.Variable(tf.random.normal(shape, stddev=st_dev)) return(bias)
-
现在我们需要初始化我们的输入数据层。将有七个输入特征,输出将是出生体重(以克为单位):
x_data = tf.keras.Input(dtype=tf.float32, shape=(7,))
-
全连接层将用于所有三个隐藏层,每个层都会使用三次。为了避免重复代码,我们将创建一个层函数,以便在初始化模型时使用,具体如下:
# Create a fully connected layer: def fully_connected(input_layer, weights, biases): return tf.keras.layers.Lambda(lambda x: tf.nn.relu(tf.add(tf.matmul(x, weights), biases)))(input_layer)
-
现在是时候创建我们的模型了。对于每一层(以及输出层),我们将初始化一个权重矩阵、一个偏置矩阵和全连接层。对于这个示例,我们将使用大小分别为 25、10 和 3 的隐藏层:
我们使用的模型将有 522 个变量需要拟合。为了得到这个数字,我们可以看到在数据和第一个隐藏层之间有 7*25+25=200 个变量。如果我们继续这样加总,我们会得到 200+260+33+4=497 个变量。这比我们在逻辑回归模型中使用的九个变量要大得多。
#--------Create the first layer (25 hidden nodes)-------- weight_1 = init_weight(shape=[7,25], st_dev=5.0) bias_1 = init_bias(shape=[25], st_dev=10.0) layer_1 = fully_connected(x_data, weight_1, bias_1) #--------Create second layer (10 hidden nodes)-------- weight_2 = init_weight(shape=[25, 10], st_dev=5.0) bias_2 = init_bias(shape=[10], st_dev=10.0) layer_2 = fully_connected(layer_1, weight_2, bias_2) #--------Create third layer (3 hidden nodes)-------- weight_3 = init_weight(shape=[10, 3], st_dev=5.0) bias_3 = init_bias(shape=[3], st_dev=10.0) layer_3 = fully_connected(layer_2, weight_3, bias_3) #--------Create output layer (1 output value)-------- weight_4 = init_weight(shape=[3, 1], st_dev=5.0) bias_4 = init_bias(shape=[1], st_dev=10.0) final_output = fully_connected(layer_3, weight_4, bias_4) model = tf.keras.Model(inputs=x_data, outputs=final_output, name="multiple_layers_neural_network")
-
接下来,我们将声明优化器(使用 Adam 优化算法),并循环执行训练迭代。我们将使用 L1 损失函数(绝对值)。我们还将初始化两个列表,用于存储我们的
train
和test_loss
函数。在每次循环中,我们还希望随机选择一批训练数据进行模型拟合,并在每 25 代时打印状态,如下所示:# Declare Adam optimizer optimizer = tf.keras.optimizers.Adam(0.025) # Training loop loss_vec = [] test_loss = [] for i in range(200): rand_index = np.random.choice(len(x_vals_train), size=batch_size) rand_x = x_vals_train[rand_index] rand_y = np.transpose([y_vals_train[rand_index]]) # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. output = model(rand_x) # Apply loss function (MSE) loss = tf.reduce_mean(tf.abs(rand_y - output)) loss_vec.append(loss) # Get gradients of loss with reference to the weights and bias variables to adjust. gradients_w1 = tape.gradient(loss, weight_1) gradients_b1 = tape.gradient(loss, bias_1) gradients_w2 = tape.gradient(loss, weight_2) gradients_b2 = tape.gradient(loss, bias_2) gradients_w3 = tape.gradient(loss, weight_3) gradients_b3 = tape.gradient(loss, bias_3) gradients_w4 = tape.gradient(loss, weight_4) gradients_b4 = tape.gradient(loss, bias_4) # Update the weights and bias variables of the model. optimizer.apply_gradients(zip([gradients_w1, gradients_b1, gradients_w2, gradients_b2, gradients_w3, gradients_b3, gradients_w4, gradients_b4], [weight_1, bias_1, weight_2, bias_2, weight_3, bias_3, weight_4, bias_4])) # Forward pass. output_test = model(x_vals_test) # Apply loss function (MSE) on test temp_loss = tf.reduce_mean(tf.abs(np.transpose([y_vals_test]) - output_test)) test_loss.append(temp_loss) if (i+1) % 25 == 0: print('Generation: ' + str(i+1) + '. Loss = ' + str(loss.numpy()))
-
上一步应该会得到如下输出:
Generation: 25\. Loss = 1921.8002 Generation: 50\. Loss = 1453.3898 Generation: 75\. Loss = 987.57074 Generation: 100\. Loss = 709.81696 Generation: 125\. Loss = 508.625 Generation: 150\. Loss = 541.36774 Generation: 175\. Loss = 539.6093 Generation: 200\. Loss = 441.64032
-
以下是一个代码片段,它使用
matplotlib
绘制训练和测试损失:plt.plot(loss_vec, 'k-', label='Train Loss') plt.plot(test_loss, 'r--', label='Test Loss') plt.title('Loss per Generation') plt.xlabel('Generation') plt.ylabel('Loss') plt.legend(loc='upper right') plt.show()
我们通过绘制以下图表继续进行该步骤:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_09.png
图 6.5:在上图中,我们绘制了为预测出生体重(单位:克)而训练的神经网络的训练和测试损失。注意,大约经过 30 代后,我们已经得到一个很好的模型。
-
现在,我们需要输出训练和测试的回归结果,并通过创建一个指示器来将它们转化为分类结果,判断它们是否高于或低于 2,500 克。为了找出模型的准确性,我们需要使用以下代码:
# Model Accuracy actuals = np.array([x[0] for x in birth_data]) test_actuals = actuals[test_indices] train_actuals = actuals[train_indices] test_preds = model(x_vals_test) train_preds = model(x_vals_train) test_preds = np.array([1.0 if x < 2500.0 else 0.0 for x in test_preds]) train_preds = np.array([1.0 if x < 2500.0 else 0.0 for x in train_preds]) # Print out accuracies test_acc = np.mean([x == y for x, y in zip(test_preds, test_actuals)]) train_acc = np.mean([x == y for x, y in zip(train_preds, train_actuals)]) print('On predicting the category of low birthweight from regression output (<2500g):') print('Test Accuracy: {}'.format(test_acc)) print('Train Accuracy: {}'.format(train_acc))
-
上一步应该会得到如下输出:
Test Accuracy: 0.7631578947368421 Train Accuracy: 0.7880794701986755
正如你所看到的,训练集准确率和测试集准确率都相当不错,且模型没有出现欠拟合或过拟合的情况。
它是如何工作的…
在本步骤中,我们创建了一个回归神经网络,具有三个完全连接的隐藏层,用于预测低出生体重数据集的出生体重。在下一个步骤中,我们将尝试通过将逻辑回归模型转变为多层神经网络来改进它。
改进线性模型的预测
在本步骤中,我们将通过提高低出生体重预测的准确性来改进我们的逻辑回归模型。我们将使用神经网络。
准备就绪
对于本步骤,我们将加载低出生体重数据,并使用具有两个隐藏层的神经网络,这些隐藏层采用 sigmoid 激活函数来拟合低出生体重的概率。
怎么做…
我们按以下步骤继续进行:
-
我们首先加载库并初始化我们的计算图,如下所示:
import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import requests import os.path import csv
-
接下来,我们加载、提取并规范化数据,方法与前面的步骤相同,不同的是这里我们将使用低出生体重指标变量作为目标,而不是实际出生体重,如下所示:
# Name of data file birth_weight_file = 'birth_weight.csv' birthdata_url = 'https://github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook/blob/master/ch6/06_Using_Multiple_Layers/birth_weight.csv' # Download data and create data file if file does not exist in current directory if not os.path.exists(birth_weight_file): birth_file = requests.get(birthdata_url) birth_data = birth_file.text.split('\r\n') birth_header = birth_data[0].split('\t') birth_data = [[float(x) for x in y.split('\t') if len(x) >= 1] for y in birth_data[1:] if len(y) >= 1] with open(birth_weight_file, "w") as f: writer = csv.writer(f) writer.writerows([birth_header]) writer.writerows(birth_data) # read birth weight data into memory birth_data = [] with open(birth_weight_file, newline='') as csvfile: csv_reader = csv.reader(csvfile) birth_header = next(csv_reader) for row in csv_reader: birth_data.append(row) birth_data = [[float(x) for x in row] for row in birth_data] # Pull out target variable y_vals = np.array([x[0] for x in birth_data]) # Pull out predictor variables (not id, not target, and not birthweight) x_vals = np.array([x[1:8] for x in birth_data]) train_indices = np.random.choice(len(x_vals), round(len(x_vals)*0.8), replace=False) test_indices = np.array(list(set(range(len(x_vals))) - set(train_indices))) x_vals_train = x_vals[train_indices] x_vals_test = x_vals[test_indices] y_vals_train = y_vals[train_indices] y_vals_test = y_vals[test_indices] def normalize_cols(m, col_min=np.array([None]), col_max=np.array([None])): if not col_min[0]: col_min = m.min(axis=0) if not col_max[0]: col_max = m.max(axis=0) return (m - col_min) / (col_max - col_min), col_min, col_max x_vals_train, train_min, train_max = np.nan_to_num(normalize_cols(x_vals_train)) x_vals_test, _, _ = np.nan_to_num(normalize_cols(x_vals_test, train_min, train_max))
-
接下来,我们需要声明批量大小、种子(以确保结果可重复)以及输入数据层,如下所示:
batch_size = 90 seed = 98 np.random.seed(seed) tf.random.set_seed(seed) x_data = tf.keras.Input(dtype=tf.float64, shape=(7,))
-
如前所述,我们现在需要声明初始化变量和模型中各层的函数。为了创建一个更好的逻辑函数,我们需要创建一个函数,它返回输入层上的逻辑层。换句话说,我们将使用一个完全连接的层,并为每一层返回一个 sigmoid 元素。需要记住的是,我们的损失函数将包含最终的 sigmoid,因此我们希望在最后一层中指定不返回输出的 sigmoid,如下所示:
# Create variable definition def init_variable(shape): return(tf.Variable(tf.random.normal(shape=shape, dtype="float64", seed=seed))) # Create a logistic layer definition def logistic(input_layer, multiplication_weight, bias_weight, activation = True): # We separate the activation at the end because the loss function will # implement the last sigmoid necessary if activation: return tf.keras.layers.Lambda(lambda x: tf.nn.sigmoid(tf.add(tf.matmul(x, multiplication_weight), bias_weight)))(input_layer) else: return tf.keras.layers.Lambda(lambda x: tf.add(tf.matmul(x, multiplication_weight), bias_weight))(input_layer)
-
现在我们将声明三层(两层隐藏层和一层输出层)。我们将从为每一层初始化权重和偏置矩阵开始,并定义各层操作,如下所示:
# First logistic layer (7 inputs to 14 hidden nodes) A1 = init_variable(shape=[7,14]) b1 = init_variable(shape=[14]) logistic_layer1 = logistic(x_data, A1, b1) # Second logistic layer (14 hidden inputs to 5 hidden nodes) A2 = init_variable(shape=[14,5]) b2 = init_variable(shape=[5]) logistic_layer2 = logistic(logistic_layer1, A2, b2) # Final output layer (5 hidden nodes to 1 output) A3 = init_variable(shape=[5,1]) b3 = init_variable(shape=[1]) final_output = logistic(logistic_layer2, A3, b3, activation=False) # Build the model model = tf.keras.Model(inputs=x_data, outputs=final_output, name="improving_linear_reg_neural_network")
-
接下来,我们定义损失函数(交叉熵)并声明优化算法,如下所示:
# Loss function (Cross Entropy loss) def cross_entropy(final_output, y_target): return tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=final_output, labels=y_target)) # Declare optimizer optimizer = tf.keras.optimizers.Adam(0.002)
交叉熵是一种衡量概率之间距离的方法。在这里,我们希望衡量确定性(0 或 1)与我们模型的概率(0 < x < 1)之间的差异。TensorFlow 通过内置的 sigmoid 函数实现交叉熵。这一点也很重要,因为它是超参数调优的一部分,我们更有可能找到适合当前问题的最佳损失函数、学习率和优化算法。为了简洁起见,本食谱中不包括超参数调优。
-
为了评估和比较我们的模型与之前的模型,我们需要在图中创建一个预测和准确性操作。这将允许我们输入整个测试集并确定准确性,如下所示:
# Accuracy def compute_accuracy(final_output, y_target): prediction = tf.round(tf.nn.sigmoid(final_output)) predictions_correct = tf.cast(tf.equal(prediction, y_target), tf.float32) return tf.reduce_mean(predictions_correct)
-
我们现在准备开始我们的训练循环。我们将训练 1,500 代,并保存模型损失以及训练和测试集的准确性,以便稍后绘制图表。我们的训练循环通过以下代码启动:
# Training loop loss_vec = [] train_acc = [] test_acc = [] for i in range(1500): rand_index = np.random.choice(len(x_vals_train), size=batch_size) rand_x = x_vals_train[rand_index] rand_y = np.transpose([y_vals_train[rand_index]]) # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. output = model(rand_x) # Apply loss function (Cross Entropy loss) loss = cross_entropy(output, rand_y) loss_vec.append(loss) # Get gradients of loss with reference to the weights and bias variables to adjust. gradients_A1 = tape.gradient(loss, A1) gradients_b1 = tape.gradient(loss, b1) gradients_A2 = tape.gradient(loss, A2) gradients_b2 = tape.gradient(loss, b2) gradients_A3 = tape.gradient(loss, A3) gradients_b3 = tape.gradient(loss, b3) # Update the weights and bias variables of the model. optimizer.apply_gradients(zip([gradients_A1, gradients_b1,gradients_A2, gradients_b2, gradients_A3, gradients_b3], [A1, b1, A2, b2, A3, b3])) temp_acc_train = compute_accuracy(model(x_vals_train), np.transpose([y_vals_train])) train_acc.append(temp_acc_train) temp_acc_test = compute_accuracy(model(x_vals_test), np.transpose([y_vals_test])) test_acc.append(temp_acc_test) if (i+1)%150==0: print('Loss = ' + str(loss.numpy()))
-
前面的步骤应产生以下输出:
Loss = 0.5885411040188063 Loss = 0.581099555117532 Loss = 0.6071769535895101 Loss = 0.5043174136225906 Loss = 0.5023625777095964 Loss = 0.485112570717733 Loss = 0.5906992621835641 Loss = 0.4280814147901789 Loss = 0.5425164697605331 Loss = 0.35608561907724867
-
以下代码块展示了如何使用
matplotlib
绘制交叉熵损失以及训练集和测试集的准确性:# Plot loss over time plt.plot(loss_vec, 'k-') plt.title('Cross Entropy Loss per Generation') plt.xlabel('Generation') plt.ylabel('Cross Entropy Loss') plt.show() # Plot train and test accuracy plt.plot(train_acc, 'k-', label='Train Set Accuracy') plt.plot(test_acc, 'r--', label='Test Set Accuracy') plt.title('Train and Test Accuracy') plt.xlabel('Generation') plt.ylabel('Accuracy') plt.legend(loc='lower right') plt.show()
我们获得了每代的交叉熵损失图,如下所示:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_10.png
图 6.6:1,500 次迭代的训练损失
在大约 150 代之后,我们已经达到了一个不错的模型。随着训练的继续,我们可以看到在剩余的迭代中几乎没有什么提升,如下图所示:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_11.png
图 6.7:训练集和测试集的准确性
如你在前面的图中所见,我们很快得到了一个不错的模型。
它是如何工作的…
在考虑使用神经网络建模数据时,必须权衡其优缺点。虽然我们的模型比之前的模型收敛得更快,也许准确率更高,但这也有代价;我们训练了更多的模型变量,过拟合的风险也更大。要检查是否发生了过拟合,我们需要查看训练集和测试集的准确率。如果训练集的准确率持续增加,而测试集的准确率保持不变或稍有下降,我们可以假设发生了过拟合。
为了应对欠拟合,我们可以增加模型的深度或训练更多的迭代次数。为了应对过拟合,我们可以增加数据量或在模型中加入正则化技术。
还需要注意的是,我们的模型变量不像线性模型那样具有可解释性。神经网络模型的系数比线性模型更难解释,因为它们用来解释模型中各特征的重要性。
学习玩井字棋
为了展示神经网络的适应性,我们现在尝试使用神经网络来学习井字棋的最优走法。在这一过程中,我们已知井字棋是一个确定性游戏,且最优走法已被确定。
准备开始
为了训练我们的模型,我们将使用一组棋盘位置,并在其后附上多个不同棋盘的最优回应。通过仅考虑在对称性方面不同的棋盘位置,我们可以减少训练的棋盘数量。井字棋棋盘的非恒等变换包括 90 度、180 度和 270 度的旋转(无论方向)、水平反射和垂直反射。基于这一思想,我们将使用一份包含最优走法的棋盘简短清单,应用两个随机变换,然后将其输入到神经网络进行学习。
由于井字棋是一个确定性游戏,值得注意的是,先手的一方应该要么获胜,要么与对方平局。我们希望模型能够对我们的走法做出最优回应,并最终导致平局。
如果我们使用 1 表示 X,-1 表示 O,0 表示空格,那么下图展示了如何将棋盘位置和最优走法视为数据行:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_12.png
图 6.8:这里,我们展示了如何将棋盘和最优走法视为数据行。请注意,X = 1,O = -1,空格为 0,并且我们从 0 开始索引。
除了模型损失外,为了检查模型的表现,我们将执行两项操作。我们首先要做的检查是从训练集中移除一个位置和最优走法的数据行。这将帮助我们判断神经网络模型是否能够推广到它未见过的走法。评估模型的第二种方法是最终与它进行一局对弈。
可以在 GitHub 的目录中找到此食谱的所有可能棋盘和最优走法列表,网址为github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook/tree/master/ch6/08_Learning_Tic_Tac_Toe
,或者在 Packt 的仓库中找到github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook
。
如何做到这一点…
我们按照以下步骤继续进行:
-
我们需要首先加载此脚本所需的库,代码如下:
import tensorflow as tf import matplotlib.pyplot as plt import csv import numpy as np import random
-
接下来,我们声明用于训练模型的批次大小,代码如下:
batch_size = 50
-
为了使得可视化棋盘变得更加简单,我们将创建一个函数,输出带有 X 和 O 的井字棋棋盘。可以通过以下代码实现:
def print_board(board): symbols = ['O', ' ', 'X'] board_plus1 = [int(x) + 1 for x in board] board_line1 = ' {} | {} | {}'.format(symbols[board_plus1[0]], symbols[board_plus1[1]], symbols[board_plus1[2]]) board_line2 = ' {} | {} | {}'.format(symbols[board_plus1[3]], symbols[board_plus1[4]], symbols[board_plus1[5]]) board_line3 = ' {} | {} | {}'.format(symbols[board_plus1[6]], symbols[board_plus1[7]], symbols[board_plus1[8]]) print(board_line1) print('___________') print(board_line2) print('___________') print(board_line3)
-
现在,我们需要创建一个函数,返回一个新棋盘和一个在变换下的最优响应位置。可以通过以下代码实现:
def get_symmetry(board, response, transformation): ''' :param board: list of integers 9 long: opposing mark = -1 friendly mark = 1 empty space = 0 :param transformation: one of five transformations on a board: rotate180, rotate90, rotate270, flip_v, flip_h :return: tuple: (new_board, new_response) ''' if transformation == 'rotate180': new_response = 8 - response return board[::-1], new_response elif transformation == 'rotate90': new_response = [6, 3, 0, 7, 4, 1, 8, 5, 2].index(response) tuple_board = list(zip(*[board[6:9], board[3:6], board[0:3]])) return [value for item in tuple_board for value in item], new_response elif transformation == 'rotate270': new_response = [2, 5, 8, 1, 4, 7, 0, 3, 6].index(response) tuple_board = list(zip(*[board[0:3], board[3:6], board[6:9]]))[::-1] return [value for item in tuple_board for value in item], new_response elif transformation == 'flip_v': new_response = [6, 7, 8, 3, 4, 5, 0, 1, 2].index(response) return board[6:9] + board[3:6] + board[0:3], new_response elif transformation == 'flip_h': # flip_h = rotate180, then flip_v new_response = [2, 1, 0, 5, 4, 3, 8, 7, 6].index(response) new_board = board[::-1] return new_board[6:9] + new_board[3:6] + new_board[0:3], new_response else: raise ValueError('Method not implmented.')
-
棋盘列表及其最优响应存储在一个
.csv
文件中,该文件位于 GitHub 仓库中的目录,网址为github.com/nfmcclure/tensorflow_cookbook
或 Packt 仓库中的github.com/PacktPublishing/TensorFlow-Machine-Learning-Cookbook-Second-Edition
。我们将创建一个函数,加载包含棋盘和响应的文件,并将其存储为一个元组列表,代码如下:def get_moves_from_csv(csv_file): ''' :param csv_file: csv file location containing the boards w/ responses :return: moves: list of moves with index of best response ''' moves = [] with open(csv_file, 'rt') as csvfile: reader = csv.reader(csvfile, delimiter=',') for row in reader: moves.append(([int(x) for x in row[0:9]],int(row[9]))) return moves
-
现在,我们需要将所有部分结合起来,创建一个函数,返回一个随机变换的棋盘和响应。可以通过以下代码实现:
def get_rand_move(moves, rand_transforms=2): # This function performs random transformations on a board. (board, response) = random.choice(moves) possible_transforms = ['rotate90', 'rotate180', 'rotate270', 'flip_v', 'flip_h'] for i in range(rand_transforms): random_transform = random.choice(possible_transforms) (board, response) = get_symmetry(board, response, random_transform) return board, response
-
接下来,我们加载数据并创建一个训练集,代码如下:
moves = get_moves_from_csv('base_tic_tac_toe_moves.csv') # Create a train set: train_length = 500 train_set = [] for t in range(train_length): train_set.append(get_rand_move(moves))
-
请记住,我们要从训练集中删除一个棋盘和最优响应,看看模型是否能够泛化并做出最佳决策。以下棋盘的最佳走法是将棋子放在索引位置 6:
test_board = [-1, 0, 0, 1, -1, -1, 0, 0, 1] train_set = [x for x in train_set if x[0] != test_board]
-
现在我们可以初始化权重和偏置,并创建我们的模型:
def init_weights(shape): return tf.Variable(tf.random_normal(shape)) A1 = init_weights([9, 81]) bias1 = init_weights([81]) A2 = init_weights([81, 9]) bias2 = init_weights([9])
-
现在,我们创建我们的模型。请注意,我们在以下模型中没有包含
softmax()
激活函数,因为它已包含在损失函数中:# Initialize input data X = tf.keras.Input(dtype=tf.float32, batch_input_shape=[None, 9]) hidden_output = tf.keras.layers.Lambda(lambda x: tf.nn.sigmoid(tf.add(tf.matmul(x, A1), bias1)))(X) final_output = tf.keras.layers.Lambda(lambda x: tf.add(tf.matmul(x, A2), bias2))(hidden_output) model = tf.keras.Model(inputs=X, outputs=final_output, name="tic_tac_toe_neural_network")
-
接下来,我们将声明优化器,代码如下:
optimizer = tf.keras.optimizers.SGD(0.025)
-
现在,我们可以使用以下代码进行神经网络的训练循环。请注意,我们的
loss
函数将是最终输出对数(未经标准化)的平均 softmax:# Initialize variables loss_vec = [] for i in range(10000): rand_indices = np.random.choice(range(len(train_set)), batch_size, replace=False) batch_data = [train_set[i] for i in rand_indices] x_input = [x[0] for x in batch_data] y_target = np.array([y[1] for y in batch_data]) # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. output = model(np.array(x_input, dtype=float)) # Apply loss function (Cross Entropy loss) loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=output, labels=y_target)) loss_vec.append(loss) # Get gradients of loss with reference to the weights and bias variables to adjust. gradients_A1 = tape.gradient(loss, A1) gradients_b1 = tape.gradient(loss, bias1) gradients_A2 = tape.gradient(loss, A2) gradients_b2 = tape.gradient(loss, bias2) # Update the weights and bias variables of the model. optimizer.apply_gradients(zip([gradients_A1, gradients_b1, gradients_A2, gradients_b2], [A1, bias1, A2, bias2])) if i % 500 == 0: print('Iteration: {}, Loss: {}'.format(i, loss))
-
以下是绘制模型训练过程中损失的代码:
plt.plot(loss_vec, 'k-', label='Loss') plt.title('Loss (MSE) per Generation') plt.xlabel('Generation') plt.ylabel('Loss') plt.show()
我们应该得到以下每代的损失曲线:
https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_06_13.png
图 6.9:井字棋训练集在 10,000 次迭代后的损失
在前面的图表中,我们绘制了训练步骤中的损失曲线。
-
为了测试模型,我们需要查看它在从训练集移除的测试板上的表现。我们希望模型能够泛化并预测移动的最优索引,即索引编号 6。大多数时候,模型会成功,如下所示:
test_boards = [test_board] logits = model.predict(test_boards) predictions = tf.argmax(logits, 1) print(predictions)
-
前面的步骤应该会得到以下输出:
[6]
-
为了评估我们的模型,我们需要与训练好的模型对战。为此,我们必须创建一个函数来检查是否获胜。这样,程序就能知道何时停止请求更多的移动。可以通过以下代码实现:
def check(board): wins = [[0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6]] for i in range(len(wins)): if board[wins[i][0]]==board[wins[i][1]]==board[wins[i][2]]==1.: return 1 elif board[wins[i][0]]==board[wins[i][1]]==board[wins[i][2]]==-1.: return 1 return 0
-
现在我们可以循环并与模型进行游戏。我们从一个空白的棋盘开始(全为零),然后让用户输入一个索引(0-8)表示要下的位置,然后将其输入模型进行预测。对于模型的移动,我们选择最大的可用预测,并且该位置必须是空的。从这场游戏中,我们可以看到我们的模型并不完美,如下所示:
game_tracker = [0., 0., 0., 0., 0., 0., 0., 0., 0.] win_logical = False num_moves = 0 while not win_logical: player_index = input('Input index of your move (0-8): ') num_moves += 1 # Add player move to game game_tracker[int(player_index)] = 1. # Get model's move by first getting all the logits for each index [potential_moves] = model(np.array([game_tracker], dtype=float)) # Now find allowed moves (where game tracker values = 0.0) allowed_moves = [ix for ix, x in enumerate(game_tracker) if x == 0.0] # Find best move by taking argmax of logits if they are in allowed moves model_move = np.argmax([x if ix in allowed_moves else -999.0 for ix, x in enumerate(potential_moves)]) # Add model move to game game_tracker[int(model_move)] = -1. print('Model has moved') print_board(game_tracker) # Now check for win or too many moves if check(game_tracker) == -1 or num_moves >= 5: print('Game Over!') win_logical = True elif check(game_tracker) == 1: print('Congratulations, You won!') win_logical = True
-
前面的步骤应该会得到以下交互式输出:
Input index of your move (0-8): 4 Model has moved | | ___________ | X | ___________ | | O Input index of your move (0-8): 6 Model has moved O | | ___________ | X | ___________ X | | O Input index of your move (0-8): 2 Model has moved O | | X ___________ | X | ___________ X | O | O Congratulations, You won!
如你所见,人类玩家很快且轻松地战胜了机器。
它是如何工作的…
在这一部分,我们训练了一个神经网络,通过输入棋盘位置和一个九维向量来玩井字游戏,并预测最优反应。我们只需要输入几个可能的井字游戏棋盘,并对每个棋盘应用随机变换来增加训练集的大小。
为了测试我们的算法,我们移除了一个特定棋盘的所有实例,并查看我们的模型是否能够泛化并预测最优反应。最后,我们与模型进行了一场样本游戏。这个模型还不完美。我们可以通过使用更多数据或应用更复杂的神经网络架构来改进它。但更好的做法是改变学习的类型:与其使用监督学习,我们更应该使用基于强化学习的方法。