TensorFlow 机器学习秘籍(三)

原文:annas-archive.org/md5/fdd2c78f646d889f873e10b2a8485875

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:使用表格数据进行预测

目前可以轻松找到的大多数数据并不是由图像或文本文件组成,而是由关系型表格构成,每个表格可能包含数字、日期和简短文本,这些数据可以结合在一起。这是因为基于关系模型(可以通过某些列的值作为连接键将数据表结合在一起)的数据库应用得到了广泛采用。如今,这些表格是表格数据的主要来源,因此也带来了一些挑战。

下面是应用深度神经网络DNNs)于表格数据时常见的挑战:

  • 混合特征数据类型

  • 数据以稀疏格式呈现(零值数据多于非零值数据),这对于 DNN 找到最优解并不是最理想的情况。

  • 目前尚未出现最先进的架构,只有一些不同的最佳实践。

  • 针对单一问题可用的数据比常见的图像识别问题要少。

  • 非技术人员会产生怀疑,因为与更简单的机器学习算法相比,DNN 在表格数据上的可解释性较差。

  • 通常情况下,DNN 并不是表格数据的最佳解决方案,因为梯度提升方法(如 LightGBM、XGBoost 和 CatBoost)可能表现得更好。

即使这些挑战看起来相当困难,也请不要气馁。将深度神经网络(DNN)应用于表格数据时的挑战确实是严峻的,但另一方面,机会也同样巨大。斯坦福大学兼职教授、深度学习专家安德鲁·吴最近表示:“深度学习在用户众多的消费互联网公司中得到了极大的应用,因而产生了大量数据,但要突破到其他行业,那里的数据集较小,我们现在需要针对小数据的更好技术。

在本章中,我们将向你介绍一些处理小型表格数据的最佳方法,使用的是 TensorFlow。在此过程中,我们将使用 TensorFlow、Keras,以及两个专门的机器学习包:pandas(pandas.pydata.org/)和 scikit-learn(scikit-learn.org/stable/index.html)。在前几章中,我们经常使用 TensorFlow Datasets(www.tensorflow.org/datasets)和专门用于特征列的层(www.tensorflow.org/api_docs/python/tf/feature_column)。我们本可以在这一章中重复使用它们,但那样我们就会错过一些只有 scikit-learn 才能提供的有趣转换,而且交叉验证也会变得困难。

此外,考虑到使用 scikit-learn 如果你在比较不同算法在某个问题上的表现,并且需要标准化一个数据准备管道,不仅适用于 TensorFlow 模型,还适用于其他更经典的机器学习和统计模型。

为了安装 pandas 和 scikit-learn(如果你使用的是 Anaconda,它们应该已经安装在你的系统中),请按照以下指南操作:

本章我们将讨论一系列专注于从表格数据中学习的方法,这些数据以表格形式组织,行表示观察结果,列表示每个特征的观察值。

表格数据是大多数机器学习算法的常见输入数据,但对于 DNN 来说并不常见,因为 DNN 在处理其他类型的数据(如图像和文本)时表现更好。

针对表格数据的深度学习方法需要解决一些问题,比如数据异质性,这些问题并非主流,并且需要使用许多常见的机器学习策略,比如交叉验证,而这些策略在 TensorFlow 中目前尚未实现。

本章结束时,你应当掌握以下内容:

  • 处理数值数据

  • 处理日期数据

  • 处理类别数据

  • 处理有序数据

  • 处理高卡特性类别数据

  • 完成所有处理步骤

  • 设置数据生成器

  • 为表格数据创建自定义激活函数

  • 对难题进行测试运行

我们立即开始学习如何处理数值数据。你会惊讶于这些方法在许多表格数据问题中的有效性。

处理数值数据

我们将从准备数值数据开始。你有数值数据时,数据是:

  • 你的数据是浮动数字表示的

  • 你的数据是整数,并且有一定数量的唯一值(否则,如果只有少数值按顺序排列,你正在处理的是有序变量,如排名)。

  • 你的整数数据并不代表一个类别或标签(否则你正在处理一个类别变量)。

在处理数值数据时,有几种情况可能会影响 DNN 处理数据时的性能:

  • 缺失数据(NULL 或 NaN 值,甚至 INF 值)会导致 DNN 完全无法工作。

  • 常数值会导致计算变慢,并干扰每个神经元已经提供的偏差。

  • 偏斜分布

  • 非标准化数据,尤其是带有极端值的数据

在将数值数据输入神经网络之前,你必须确保所有这些问题已经得到妥善处理,否则你可能会遇到错误或学习过程无法正常进行。

准备工作

为了解决所有潜在问题,我们将主要使用来自 scikit-learn 的专门函数。在开始我们的配方之前,我们将把它们导入到环境中:

import numpy as np
import pandas as pd
try:
    from sklearn.impute import IterativeImputer
except:
    from sklearn.experimental import enable_iterative_imputer
    from sklearn.impute import IterativeImputer
from sklearn.ensemble import ExtraTreesRegressor
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, QuantileTransformer
from sklearn.feature_selection import VarianceThreshold
from sklearn.pipeline import Pipeline 

为了测试我们的配方,我们将使用一个简单的 3x4 表格,其中一些列包含 NaN 值,还有一些常数列不包含 NaN 值:

example = pd.DataFrame([[1, 2, 3, np.nan], [1, 3, np.nan, 4], [1, 2, 2, 2]], columns = ['a', 'b', 'c', 'd']) 

如何操作…

我们的配方将基于我们相对于以下内容的指示构建一个 scikit-learn 管道:

  • 一个特征被保留的最小可接受方差,否则你可能会将不需要的常量引入网络,进而阻碍学习过程(variance_threshold 参数)

  • 用作填充缺失值的基准策略是什么(imputer 参数,默认设置为用特征的均值替代缺失值),以便完成输入矩阵,使矩阵乘法成为可能(这是神经网络中的基本计算)

  • 我们是否应该使用基于所有数值数据缺失值的更复杂的填充策略(multivariate_imputer 参数),因为有时候数据点并非随机缺失,其他变量可能提供你所需的信息以进行正确的估计

  • 是否添加一个二进制特征,标示每个特征的缺失值位置,这是一个好的策略,因为你通常可以通过缺失模式找到有用的信息(add_indicator 参数)

  • 是否对变量的分布进行转换,以强制它们接近对称分布(quantile_transformer 参数,默认为 normal),因为网络将从对称的数据分布中学习得更好

  • 我们是否应该基于统计归一化来重新缩放输出,即在去除均值后除以标准差(scaler 参数,默认为 True

现在,考虑到这些,让我们按照以下方式构建我们的管道:

def assemble_numeric_pipeline(variance_threshold=0.0, 
                              imputer='mean', 
                              multivariate_imputer=False, 
                              add_indicator=True,
                              quantile_transformer='normal',
                              scaler=True):
    numeric_pipeline = []
    if variance_threshold is not None:
        if isinstance(variance_threshold, float):
            numeric_pipeline.append(('var_filter', 
                                    VarianceThreshold(threshold=variance_threshold)))
        else:
            numeric_pipeline.append(('var_filter',
                                     VarianceThreshold()))
    if imputer is not None:
        if multivariate_imputer is True:
            numeric_pipeline.append(('imputer', 
                                     IterativeImputer(estimator=ExtraTreesRegressor(n_estimators=100, n_jobs=-2), 
                                                      initial_strategy=imputer,
                                                      add_indicator=add_indicator)))
        else:
            numeric_pipeline.append(('imputer', 
                                     SimpleImputer(strategy=imputer, 
                                                   add_indicator=add_indicator)
                                    )
                                   )
    if quantile_transformer is not None:
        numeric_pipeline.append(('transformer',
                                 QuantileTransformer(n_quantiles=100, 
                                                     output_distribution=quantile_transformer, 
                                                     random_state=42)
                                )
                               )
    if scaler is not None:
        numeric_pipeline.append(('scaler', 
                                 StandardScaler()
                                )
                               )
    return Pipeline(steps=numeric_pipeline) 

我们现在可以通过指定我们的转换偏好来创建我们的数值管道:

numeric_pipeline = assemble_numeric_pipeline(variance_threshold=0.0, 
                              imputer='mean', 
                              multivariate_imputer=False, 
                              add_indicator=True,
                              quantile_transformer='normal',
                              scaler=True) 

我们可以立即在示例上尝试我们新的函数,首先应用fit方法,然后应用transform方法:

numeric_pipeline.fit(example)
np.round(numeric_pipeline.transform(example), 3) 

这是生成的输出 NumPy 数组:

array([[-0.707,  1.225, -0\.   , -0.707,  1.414],
       [ 1.414, -0\.   ,  1.225,  1.414, -0.707],
       [-0.707, -1.225, -1.225, -0.707, -0.707]]) 

如你所见,所有原始数据已经完全转换,所有缺失值都已被替换。

它是如何工作的…

如前所述,我们使用 scikit-learn 以便与其他机器学习解决方案进行比较,并且因为在构建此配方时涉及到一些独特的 scikit-learn 函数:

对于每个函数,您会找到一个指向 scikit-learn 文档的链接,提供关于该函数如何工作的详细信息。解释为什么 scikit-learn 方法对于这个配方(以及本章中您将找到的其他配方)如此重要至关重要。

处理图像或文本时,通常不需要为训练数据和测试数据定义特定的处理过程。这是因为您对两者应用的是确定性的转换。例如,在图像处理中,您只需将像素值除以 255 来进行标准化。

然而,对于表格数据,您需要更复杂的转换,并且这些转换完全不是确定性的,因为它们涉及学习和记住特定的参数。例如,当使用均值填充一个特征的缺失值时,您首先需要从训练数据中计算均值。然后,您必须对任何新的数据应用相同的填充值(不能重新计算新数据的均值,因为这些新数据可能来自稍有不同的分布,可能与您的 DNN 所学的值不匹配)。

所有这些都涉及跟踪从训练数据中学习到的许多参数。scikit-learn 可以帮助您,因为当您使用 fit 方法时,它会学习并存储从训练数据中推导出的所有参数。使用 transform 方法,您将使用通过 fit 学到的参数对任何新数据(或相同的训练数据)应用转换。

还有更多…

scikit-learn 函数通常返回一个 NumPy 数组。如果没有进行进一步的特征创建,使用输入列标记返回的数组并没有问题。不幸的是,由于我们创建的转换管道,这种情况并不成立:

  • 方差阈值将移除无用的特征

  • 缺失值填充将创建缺失的二进制指示器

我们实际上可以通过检查拟合的管道,找出哪些列已经被移除,哪些内容已经从原始数据中添加。可以创建一个函数来自动执行这一操作:

def derive_numeric_columns(df, pipeline):
    columns = df.columns
    if 'var_filter' in pipeline.named_steps:
        threshold = pipeline.named_steps.var_filter.threshold
        columns = columns[pipeline.named_steps.var_filter.variances_>threshold]
    if 'imputer' in pipeline.named_steps:
        missing_cols = pipeline.named_steps.imputer.indicator_.features_
        if len(missing_cols) > 0:
            columns = columns.append(columns[missing_cols] + '_missing')
    return columns 

当我们在示例中尝试时:

derive_numeric_columns(example, numeric_pipeline) 

我们获得一个包含剩余列和二进制指示符的 pandas 索引(由原始特征的名称和 _missing 后缀表示):

Index(['b', 'c', 'd', 'c_missing', 'd_missing'], dtype='object') 

跟踪你在转换列时的操作,可以帮助你在需要调试转换后的数据时,以及在使用像 shap (github.com/slundberg/shap) 或 lime (github.com/marcotcr/lime) 等工具解释 DNN 工作原理时提供帮助。

这个配方应该能满足你关于数值数据的所有需求。现在,让我们继续探索日期和时间。

处理日期

日期在数据库中非常常见,特别是在处理未来估算的预测(如销售预测)时,它们显得不可或缺。神经网络无法直接处理日期,因为它们通常以字符串形式表示。因此,你必须通过分离日期的数值元素来转换它们,一旦你将日期拆分成它的组成部分,你就得到了一些数字,任何神经网络都能轻松处理这些数字。然而,某些时间元素是周期性的(例如天、月、小时、星期几),低数字和高数字实际上是相邻的。因此,你需要使用正弦和余弦函数,这将使这些周期性数字以一种 DNN 可以理解和正确解释的格式呈现。

准备工作

由于我们需要编写一个使用 fit/transform 操作的类,这是 scikit-learn 中典型的操作方式,我们从 scikit-learn 导入 BaseEstimatorTransformerMixin 类进行继承。这个继承将帮助我们使我们的代码与 scikit-learn 的所有其他函数完美兼容:

from sklearn.base import BaseEstimator, TransformerMixin 

为了测试,我们还准备了一个包含日期的字符串形式的示例数据集,采用日/月/年格式:

example = pd.DataFrame({'date_1': ['04/12/2018', '05/12/2019',  
                                   '07/12/2020'],
                        'date_2': ['12/5/2018', '15/5/2015', 
                                   '18/5/2016'],
                        'date_3': ['25/8/2019', '28/8/2018', 
                                   '29/8/2017']}) 

提供的示例非常简短和简单,但它应该能说明我们在处理时的所有相关要点。

如何操作……

这一次我们将设计我们自己的类 DateProcessor。实例化该类后,它可以选择一个 pandas DataFrame,并将每个日期筛选并处理成一个新的 DataFrame,供 DNN 处理。

这个过程一次处理一个日期,提取日期、星期几、月份和年份(另外,还包括小时和分钟),并使用正弦和余弦变换对所有周期性时间进行转换:

class DateProcessor(BaseEstimator, TransformerMixin):
    def __init__(self, date_format='%d/%m/%Y', hours_secs=False):
        self.format = date_format
        self.columns = None
        self.time_transformations = [
            ('day_sin', lambda x: np.sin(2*np.pi*x.dt.day/31)),
            ('day_cos', lambda x: np.cos(2*np.pi*x.dt.day/31)),
            ('dayofweek_sin', 
                      lambda x: np.sin(2*np.pi*x.dt.dayofweek/6)),
            ('dayofweek_cos', 
                      lambda x: np.cos(2*np.pi*x.dt.dayofweek/6)),
            ('month_sin', 
                      lambda x: np.sin(2*np.pi*x.dt.month/12)),
            ('month_cos', 
                      lambda x: np.cos(2*np.pi*x.dt.month/12)),
            ('year', 
                      lambda x: (x.dt.year - x.dt.year.min()                          ) / (x.dt.year.max() - x.dt.year.min()))
        ]
        if hours_secs:
            self.time_transformations = [
                ('hour_sin', 
                      lambda x: np.sin(2*np.pi*x.dt.hour/23)),
                ('hour_cos', 
                      lambda x: np.cos(2*np.pi*x.dt.hour/23)),
                ('minute_sin', 
                      lambda x: np.sin(2*np.pi*x.dt.minute/59)),
                ('minute_cos', 
                      lambda x: np.cos(2*np.pi*x.dt.minute/59))
            ] + self.time_transformations

    def fit(self, X, y=None, **fit_params):
        self.columns = self.transform(X.iloc[0:1,:]).columns
        return self

    def transform(self, X, y=None, **fit_params):
        transformed = list()
        for col in X.columns:
            time_column = pd.to_datetime(X[col],
                                   format=self.format)
            for label, func in self.time_transformations:
                transformed.append(func(time_column))
                transformed[-1].name += '_' + label
        transformed = pd.concat(transformed, axis=1)
        return transformed

    def fit_transform(self, X, y=None, **fit_params):
        self.fit(X, y, **fit_params)
        return self.transform(X) 

现在我们已经将配方编写成 DateProcessor 类的形式,让我们进一步探索它的内部工作原理。

它是如何工作的……

整个类的关键是通过 pandas to_datetime 函数进行的转换,它将任何表示日期的字符串转换为 datetime64[ns] 类型。

to_datetime之所以有效,是因为你提供了一个模板(format参数),用来将字符串转换为日期。有关如何定义该模板的完整指南,请访问docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior

当你需要拟合和转换数据时,类会自动将所有日期处理成正确的格式,并进一步使用正弦和余弦函数进行变换:

DateProcessor().fit_transform(example) 

一些变换结果是显而易见的,但有些与周期时间相关的变换可能会让人感到困惑。我们花点时间探讨它们如何运作以及为什么会这样。

还有更多……

这个类不会返回时间元素的原始提取结果,如小时、分钟或天数,而是首先通过正弦变换,然后是余弦变换来转换它们。让我们绘制出它如何转换 24 小时,以便更好地理解这个方法:

import matplotlib.pyplot as plt
sin_time = np.array([[t, np.sin(2*np.pi*t/23)] for t in range(0, 24)])
cos_time = np.array([[t, np.cos(2*np.pi*t/23)] for t in range(0, 24)])
plt.plot(sin_time[:,0], sin_time[:,1], label='sin hour')
plt.plot(cos_time[:,0], cos_time[:,1], label='cos hour')
plt.axhline(y=0.0, linestyle='--', color='lightgray')
plt.legend()
plt.show() 

这是你将得到的图:

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_07_01.png

图 7.1:正弦和余弦变换后的小时时间绘图

从图中,我们可以看出一天的开始和结束是如何重合的,从而完成时间周期的闭环。每个变换也会返回相同的值,对于几个不同的小时来说都是如此。这就是为什么我们应该同时使用正弦和余弦的原因;如果你同时使用这两者,每个时间点都会有一对不同的正弦和余弦值,因此你可以精确地检测你在连续时间中的位置。通过将正弦和余弦值绘制成散点图,这一点也可以通过可视化方式进行解释:

ax = plt.subplot()
ax.set_aspect('equal')
ax.set_xlabel('sin hour')
ax.set_ylabel('cos hour')
plt.scatter(sin_time[:,1], cos_time[:,1])
plt.show() 

这是结果:

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_07_02.png

图 7.2:将小时时间的正弦和余弦变换结合到散点图中

就像时钟一样,小时被绘制在一个圆圈中,每个小时是分开的、独立的,但却是完整的周期连续体。

处理分类数据

字符串通常在表格数据中表示分类数据。分类特征中的每个唯一值代表一个质量,指的是我们正在检查的示例(因此,我们认为这些信息是定性的,而数字信息是定量的)。从统计学角度看,每个唯一值被称为水平,而分类特征被称为因子。有时你会看到用于分类的数字代码(标识符),当定性信息之前已被编码为数字时,但处理方式不会改变:信息是数字值,但应该当作分类数据处理。

由于你不知道每个分类特征中每个唯一值与特征中其他值的关系(如果你提前将值分组或排序,实际上是在表达你对数据的假设),你可以将每个唯一值视为一个独立的值。因此,你可以从每个唯一的分类值中推导出创建二进制特征的想法。这个过程被称为独热编码(one-hot encoding),它是最常见的数据处理方法,可以使得分类数据适用于深度神经网络(DNN)和其他机器学习算法。

比如,如果你有一个分类变量,其中包含红色、蓝色和绿色这些唯一值,你可以将它转化为三个独立的二进制变量,每个变量唯一地表示一个值,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_07_03.png

然而,这种方法对 DNN 来说存在一个问题。当你的分类变量拥有过多的取值(通常超过 255 个)时,所得到的二进制派生特征不仅数量过多,导致数据集变得庞大,而且还携带很少的信息,因为大部分数值将是零(我们称这种情况为稀疏数据)。稀疏数据对 DNN 有一定问题,因为当数据中有过多的零时,反向传播效果不好,因为信息的缺失会导致信号在网络中传播时无法产生有意义的变化。

因此,我们根据分类变量的唯一值数量区分低基数和高基数的分类变量,并且只对我们认为基数较低的分类变量进行处理(通常如果唯一值少于 255 个,我们认为它是低基数,但你也可以选择一个更低的阈值,比如 64、32,甚至是 24)。

准备工作

我们导入 scikit-learn 的独热编码函数,并准备一个简单的示例数据集,其中包含字符串和数值形式的分类数据:

from sklearn.preprocessing import OneHotEncoder
example = pd.DataFrame([['car', 1234], ['house', 6543], 
                  ['tree', 3456]], columns=['object', 'code']) 

现在我们可以继续执行方案了。

如何操作…

我们准备一个可以将数字转换为字符串的类,使用它后,每个数值型分类特征将与字符串一样进行处理。然后,我们准备好我们的方案,这个方案是一个 scikit-learn 管道,结合了我们的字符串转换器和独热编码(我们不会忘记通过将缺失值转换为唯一值来自动处理缺失数据)。

class ToString(BaseEstimator, TransformerMixin):
    def fit(self, X, y=None, **fit_params):
        return self
    def transform(self, X, y=None, **fit_params):
        return X.astype(str)
    def fit_transform(self, X, y=None, **fit_params):
        self.fit(X, y, **fit_params)
        return self.transform(X)

categorical_pipeline = Pipeline(steps=[
         ('string_converter', ToString()),
         ('imputer', SimpleImputer(strategy='constant', 
                                   fill_value='missing')),
         ('onehot', OneHotEncoder(handle_unknown='ignore'))]) 

尽管代码片段很简短,但它实际上实现了很多功能。我们来理解它是如何工作的。

它是如何工作的…

像我们之前看到的其他方法一样,我们只需要拟合并转换我们的示例:

categorical_pipeline.fit_transform(example).todense() 

由于返回的数组是稀疏的(即在数据集中零值占主导的特殊格式),我们可以使用 .todense 方法将其转换回我们常用的 NumPy 数组格式。

还有更多内容…

独热编码通过将每个类别的唯一值转换为自己的变量,生成许多新特征。为了给它们打标签,我们必须检查我们使用的 scikit-learn 独热编码实例,并从中提取标签:

def derive_ohe_columns(df, pipeline):
    return [str(col) + '_' + str(lvl) 
         for col, lvls in zip(df.columns,      
         pipeline.named_steps.onehot.categories_) for lvl in lvls] 

例如,在我们的示例中,现在我们可以通过调用以下函数来弄清楚每个新特征所代表的含义:

derive_ohe_columns(example, categorical_pipeline) 

结果为我们提供了有关原始特征和由二元变量表示的独特值的指示:

['object_car',
 'object_house',
 'object_tree',
 'code_1234',
 'code_3456',
 'code_6543'] 

正如你所看到的,结果同时提供了原始特征和由二元变量表示的独特值的指示。

处理序数数据

序数数据(例如,排名或评论中的星级)无疑更像数字数据,而不是类别数据,但我们必须首先考虑一些差异,然后才能将其直接作为数字来处理。类别数据的问题在于,你可以将其作为数字数据处理,但在标度中一个点与下一个点之间的距离,可能不同于下一个点与再下一个点之间的距离(从技术上讲,步骤可能不同)。这是因为序数数据并不代表数量,而只是表示顺序。另一方面,我们也将它视为类别数据,因为类别是独立的,而这样做会丧失顺序中隐含的信息。处理序数数据的解决方案就是将它视为数字和类别变量的组合。

准备工作

首先,我们需要从 scikit-learn 导入OrdinalEncoder函数,它将帮助我们对序数值进行数字化编码,即使它们是文本形式(例如,序数等级“差、中等、好”):

from sklearn.preprocessing import OrdinalEncoder 

然后,我们可以使用包含按序信息并记录为字符串的两个特征来准备我们的示例:

example = pd.DataFrame([['first', 'very much'], 
                        ['second', 'very little'], 
                        ['third', 'average']],
                       columns = ['rank', 'importance']) 

再次强调,示例只是一个玩具数据集,但它应该能帮助我们测试这个配方所展示的功能。

如何做……

到这个阶段,我们可以准备两个流水线。第一个流水线将处理序数数据,将其转换为有序的数字(该转换将保留原始特征的顺序)。第二个转换将对序数数据进行独热编码(这种转换将保留序数等级之间的步长信息,但不保留它们的顺序)。正如在本章前面“处理日期”配方中所述,对于你要在 DNN 中处理序数数据来说,来自原始数据的仅有两部分信息就足够了:

oe = OrdinalEncoder(categories=[['first', 'second', 'third'],  
                     ['very much', 'average', 'very little']])
categorical_pipeline = Pipeline(steps=[
            ('string_converter', ToString()),
            ('imputer', SimpleImputer(strategy='constant', 
                                      fill_value='missing')),
            ('onehot', OneHotEncoder(handle_unknown='ignore'))]) 

由于这个配方主要由 scikit-learn 流水线组成,所以它对你来说应该是相当熟悉的。让我们深入了解它,了解更多的工作原理。

它是如何工作的……

你所需要做的就是单独操作这些转换,然后将生成的向量堆叠在一起:

np.hstack((oe.fit_transform(example), categorical_pipeline.fit_transform(example).todense())) 

这是我们示例的结果:

matrix([[0., 0., 1., 0., 0., 0., 0., 1.],
        [1., 2., 0., 1., 0., 0., 1., 0.],
        [2., 1., 0., 0., 1., 1., 0., 0.]]) 

列可以通过之前看到的derive_ohe_columns函数轻松推导出来:

example.columns.tolist() + derive_ohe_columns(example, categorical_pipeline) 

这是包含转换后的列名的列表:

['rank',
 'importance',
 'rank_first',
 'rank_second',
 'rank_third',
 'importance_average',
 'importance_very little',
 'importance_very much'] 

通过将覆盖数值部分的变量与有序变量的唯一值结合起来,我们现在应该能够利用来自数据的所有真实信息。

处理高基数类别数据

在处理高基数类别特征时,我们可以使用前面提到的独热编码策略。然而,我们可能会遇到一些问题,因为生成的矩阵过于稀疏(许多零值),从而阻碍了我们的深度神经网络(DNN)收敛到一个好的解,或者使数据集变得不可处理(因为稀疏矩阵变为密集矩阵后可能占用大量内存)。

最好的解决方案是将它们作为数值标签特征传递给我们的深度神经网络(DNN),并让 Keras 嵌入层来处理它们(www.tensorflow.org/api_docs/python/tf/keras/layers/Embedding)。嵌入层实际上是一个权重矩阵,可以将高基数类别输入转换为低维度的数值输出。它本质上是一个加权线性组合,其权重经过优化,以将类别转换为最佳帮助预测过程的数字。

在幕后,嵌入层将你的类别数据转换为独热编码向量,这些向量成为一个小型神经网络的输入。这个小型神经网络的目的是将输入混合并组合成一个较小的输出层。该层执行的独热编码仅适用于数值标签的类别(不适用于字符串),因此正确转换我们的高基数类别数据是至关重要的。

scikit-learn 包提供了 LabelEncoder 函数作为一种可能的解决方案,但这种方法存在一些问题,因为它无法处理之前未见过的类别,也无法在拟合/转换模式下正常工作。我们的方案需要将其封装并使其适用于为 Keras 嵌入层生成正确的输入和信息。

准备工作

在这个方案中,我们需要重新定义 scikit-learn 中的 LabelEncoder 函数,并使其适用于拟合/转换过程:

from sklearn.preprocessing import LabelEncoder 

由于我们需要模拟一个高基数类别变量,我们将使用一个简单脚本创建的随机唯一值(由字母和数字组成)。这将使我们能够测试更多的示例:

import string
import random
def random_id(length=8):
    voc = string.ascii_lowercase + string.digits
    return ''.join(random.choice(voc) for i in range(length))
example = pd.DataFrame({'high_cat_1': [random_id(length=2) 
                                       for i in range(500)], 
                        'high_cat_2': [random_id(length=3) 
                                       for i in range(500)], 
                        'high_cat_3': [random_id(length=4) 
                                       for i in range(500)]}) 

这是我们随机示例生成器的输出:

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_07_04.png

第一列包含一个两字母代码,第二列使用三个字母,最后一列使用四个字母。

如何做到这一点……

在这个方案中,我们将准备另一个 scikit-learn 类。它扩展了现有的 LabelEncoder 函数,因为它能够自动处理缺失值。它记录了原始类别值与其对应的数值之间的映射关系,并且在转换时,它能够处理之前未见过的类别,将它们标记为未知:

class LEncoder(BaseEstimator, TransformerMixin):

    def __init__(self):
        self.encoders = dict()
        self.dictionary_size = list()
        self.unk = -1

    def fit(self, X, y=None, **fit_params):
        for col in range(X.shape[1]):
            le = LabelEncoder()
            le.fit(X.iloc[:, col].fillna('_nan'))
            le_dict = dict(zip(le.classes_, 
                               le.transform(le.classes_)))

            if '_nan' not in le_dict:
                max_value = max(le_dict.values())
                le_dict['_nan'] = max_value

            max_value = max(le_dict.values())
            le_dict['_unk'] = max_value

            self.unk = max_value
            self.dictionary_size.append(len(le_dict))
            col_name = X.columns[col]
            self.encoders[col_name] = le_dict

        return self

    def transform(self, X, y=None, **fit_params):
        output = list()
        for col in range(X.shape[1]):
            col_name = X.columns[col]
            le_dict = self.encoders[col_name]
            emb = X.iloc[:, col].fillna('_nan').apply(lambda x: 
                           le_dict.get(x, le_dict['_unk'])).values
            output.append(pd.Series(emb, 
                                name=col_name).astype(np.int32))
        return output
    def fit_transform(self, X, y=None, **fit_params):
        self.fit(X, y, **fit_params)
        return self.transform(X) 

就像我们迄今为止看到的其他类一样,LEncoder有一个拟合方法,能够存储信息以供将来使用,还有一个转换方法,基于之前拟合到训练数据时存储的信息应用转换。

它是如何工作的……

在实例化标签编码器后,我们只需拟合并转换我们的示例,将每个类别特征转化为一系列数字标签:

le = LEncoder()
le.fit_transform(example) 

完成所有编码以实现配方后,这个类的执行确实简单明了。

还有更多……

为了让 Keras 嵌入层正常工作,我们需要指定高基数类别变量的输入大小。通过访问我们示例中的le.dictionary_size,我们在示例变量中有412497502个不同的值:

le.dictionary_size 

在我们的示例中,示例变量分别有412497502个不同的值:

[412, 497, 502] 

这个数字包括缺失未知标签,即使在我们拟合的示例中没有缺失或未知元素。

完成所有处理

现在我们已经完成了处理不同类型表格数据的配方,在本配方中,我们将把所有内容封装到一个类中,这个类可以轻松处理所有的 fit/transform 操作,输入为 pandas DataFrame,并明确指定要处理的列以及处理方式。

准备好

由于我们将结合多个转换,我们将利用 scikit-learn 的FeatureUnion函数,这是一个可以轻松地将它们拼接在一起的函数:

from sklearn.pipeline import FeatureUnion 

作为测试数据集,我们将简单地合并之前使用过的所有测试数据:

example = pd.concat([
pd.DataFrame([[1, 2, 3, np.nan], [1, 3, np.nan, 4],[1, 2, 2, 2]], 
             columns = ['a', 'b', 'c', 'd']),
pd.DataFrame({'date_1': ['04/12/2018', '05/12/2019','07/12/2020'],
              'date_2': ['12/5/2018', '15/5/2015', '18/5/2016'],
              'date_3': ['25/8/2019', '28/8/2018', '29/8/2017']}),
pd.DataFrame([['first', 'very much'], ['second', 'very little'],   
              ['third', 'average']], 
             columns = ['rank', 'importance']),
pd.DataFrame([['car', 1234], ['house', 6543], ['tree', 3456]], 
             columns=['object', 'code']),
pd.DataFrame({'high_cat_1': [random_id(length=2) 
                             for i in range(3)], 
              'high_cat_2': [random_id(length=3) 
                             for i in range(3)], 
              'high_cat_3': [random_id(length=4) 
                             for i in range(3)]})
], axis=1) 

至于我们的玩具数据集,我们只需将迄今为止使用过的所有数据集合并在一起。

如何做到……

本配方的包装类已拆分为多个部分,以帮助您更好地检查和学习代码。第一部分包含初始化,它有效地整合了本章迄今为止所有看到的配方:

class TabularTransformer(BaseEstimator, TransformerMixin):

    def instantiate(self, param):
        if isinstance(param, str):
            return [param]
        elif isinstance(param, list):
            return param
        else:
            return None

    def __init__(self, numeric=None, dates=None, 
                 ordinal=None, cat=None, highcat=None,
                 variance_threshold=0.0, missing_imputer='mean',  
                 use_multivariate_imputer=False,
                 add_missing_indicator=True, 
                 quantile_transformer='normal', scaler=True,
                 ordinal_categories='auto', 
                 date_format='%d/%m/%Y', hours_secs=False):

        self.numeric = self.instantiate(numeric)
        self.dates = self.instantiate(dates)
        self.ordinal = self.instantiate(ordinal)
        self.cat  = self.instantiate(cat)
        self.highcat = self.instantiate(highcat)
        self.columns = None
        self.vocabulary = None 

在记录了所有包装器的关键参数之后,我们继续检查它的所有独立部分。请不要忘记,这些代码片段都属于同一个__init__方法,我们仅仅是重新使用之前看到的配方,因此关于这些代码片段的任何细节,请参考之前的配方。

这里我们记录了数字管道:

 self.numeric_process = assemble_numeric_pipeline(
                    variance_threshold=variance_threshold, 
                    imputer=missing_imputer, 
                    multivariate_imputer=use_multivariate_imputer, 
                    add_indicator=add_missing_indicator,
                    quantile_transformer=quantile_transformer,
                    scaler=scaler) 

之后,我们记录与管道处理时间相关的特征:

 self.dates_process = DateProcessor(
                   date_format=date_format, hours_secs=hours_secs) 

现在轮到有序变量了:

 self.ordinal_process = FeatureUnion(
                [('ordinal', 
                 OrdinalEncoder(categories=ordinal_categories)),
                 ('categorial',   
                 Pipeline(steps=[('string_converter', ToString()),
                 ('imputer', 
                 SimpleImputer(strategy='constant', 
                               fill_value='missing')),
                 ('onehot', 
                 OneHotEncoder(handle_unknown='ignore'))]))]) 

我们以分类管道作为结尾,包括低类别和高类别的管道:

 self.cat_process = Pipeline(steps=[
              ('string_converter', ToString()),
              ('imputer', SimpleImputer(strategy='constant', 
                                        fill_value='missing')),
              ('onehot', OneHotEncoder(handle_unknown='ignore'))])
        self.highcat_process = LEncoder() 

下一部分涉及拟合。根据不同的变量类型,将应用相应的拟合过程,新的处理或生成的列将记录在.columns索引列表中:

 def fit(self, X, y=None, **fit_params):
        self.columns = list()
        if self.numeric:
            self.numeric_process.fit(X[self.numeric])
            self.columns += derive_numeric_columns(
                               X[self.numeric], 
                               self.numeric_process).to_list()
        if self.dates:
            self.dates_process.fit(X[self.dates])
            self.columns += self.dates_process.columns.to_list()
        if self.ordinal:
            self.ordinal_process.fit(X[self.ordinal])
            self.columns += self.ordinal + derive_ohe_columns(
                      X[self.ordinal], 
                      self.ordinal_process.transformer_list[1][1])
        if self.cat:
            self.cat_process.fit(X[self.cat])
            self.columns += derive_ohe_columns(X[self.cat], 
                                               self.cat_process)
        if self.highcat:
            self.highcat_process.fit(X[self.highcat])
            self.vocabulary = dict(zip(self.highcat,  
                            self.highcat_process.dictionary_size))
            self.columns = [self.columns, self.highcat]
        return self 

transform方法提供了所有的转换和矩阵连接,以返回一个包含处理后数据的数组列表,第一个元素是数值部分,后面是代表高基数类别变量的数值标签向量:

 def transform(self, X, y=None, **fit_params):
        flat_matrix = list()
        if self.numeric:
            flat_matrix.append(
                   self.numeric_process.transform(X[self.numeric])
                              .astype(np.float32))
        if self.dates:
            flat_matrix.append(
                   self.dates_process.transform(X[self.dates])
                              .values
                              .astype(np.float32))
        if self.ordinal:
            flat_matrix.append(
                   self.ordinal_process.transform(X[self.ordinal])
                              .todense()
                              .astype(np.float32))
        if self.cat:
            flat_matrix.append(
                   self.cat_process.transform(X[self.cat])
                              .todense()
                              .astype(np.float32))
        if self.highcat:
            cat_vectors = self.highcat_process.transform(
                                                  X[self.highcat])
            if len(flat_matrix) > 0:
                return [np.hstack(flat_matrix)] + cat_vectors
            else:
                return cat_vectors
        else:
            return np.hstack(flat_matrix) 

最后,我们设置了fit_transform方法,它依次执行 fit 和 transform 操作:

 def fit_transform(self, X, y=None, **fit_params):
        self.fit(X, y, **fit_params)
        return self.transform(X) 

现在我们已经完成了所有的封装工作,可以看看它是如何工作的。

它是如何工作的……

在我们的测试中,我们根据列的类型将列名的列表赋值给变量:

numeric_vars = ['a', 'b', 'c', 'd']
date_vars = ['date_1', 'date_2', 'date_3']
ordinal_vars = ['rank', 'importance']
cat_vars = ['object', 'code']
highcat_vars = ['high_cat_1', 'high_cat_2', 'high_cat_3']
tt = TabularTransformer(numeric=numeric_vars, dates=date_vars, 
                        ordinal=ordinal_vars, cat=cat_vars, 
                        highcat=highcat_vars) 

在实例化了TabularTransformer并将需要处理的变量映射到它们的类型后,我们开始拟合并转换我们的示例数据集:

input_list = tt.fit_transform(example) 

结果是一个 NumPy 数组的列表。我们可以通过遍历它们并打印它们的形状,来检查输出的组成:

print([(item.shape, item.dtype) for item in input_list]) 

打印的结果显示第一个元素是一个较大的数组(所有过程的结合结果,除了高基数类别变量的部分):

[((3, 40), dtype('float32')), ((3,), dtype('int32')), ((3,), dtype('int32')), ((3,), dtype('int32'))] 

我们的 DNN 现在可以期望一个列表作为输入,其中第一个元素是数值矩阵,接下来的元素是要传递给类别嵌入层的向量。

还有更多内容……

为了能够追溯每个列和向量的名称,TabularTransformer有一个columns方法,tt.columns,可以调用。TabularTransformer还可以调用tt.vocabulary来获取关于类别变量维度的信息,这对于正确设置网络中嵌入层的输入形状是必需的。返回的结果是一个字典,其中列名是键,字典的大小是值:

{'high_cat_1': 5, 'high_cat_2': 5, 'high_cat_3': 5} 

现在我们已经有了这两个方法来追踪变量名(tt.columns)和定义高基数变量的词汇(tt.vocabulary),我们距离一个完整的深度学习框架已经只差一步,用于处理表格数据的深度学习。

设置数据生成器

在我们尝试在一个难度较大的测试任务上使用我们的框架之前,还缺少一个关键成分。前面的步骤展示了一个TabularTransformer,它可以有效地将 pandas DataFrame 转化为 DNN 可以处理的数值数组。然而,这个步骤只能一次性处理所有数据。下一步是提供一种方法,能够创建不同大小的数据批次。这可以通过使用tf.data或 Keras 生成器来实现,并且由于我们在本书之前已经探讨了许多tf.data的例子,这次我们将为 Keras 生成器准备代码,使其能够在 DNN 学习时动态生成随机批次。

准备工作

我们的生成器将继承自Sequence类:

from tensorflow.keras.utils import Sequence 

Sequence 类是拟合数据序列的基础对象,要求你实现自定义的 __getitem__(返回完整批次)和 __len__(报告完成一个周期所需的批次数)方法。

如何实现…

我们现在编写一个名为 DataGenerator 的新类,继承自 Keras 的 Sequence 类:

class DataGenerator(Sequence):
    def __init__(self, X, y,
                 tabular_transformer=None,
                 batch_size=32, 
                 shuffle=False,
                 dict_output=False
                 ):

        self.X = X
        self.y = y
        self.tbt = tabular_transformer
        self.tabular_transformer = tabular_transformer
        self.batch_size = batch_size
        self.shuffle = shuffle
        self.dict_output = dict_output
        self.indexes = self._build_index()
        self.on_epoch_end()
        self.item = 0

    def _build_index(self):
        return np.arange(len(self.y))

    def on_epoch_end(self):
        if self.shuffle:
            np.random.shuffle(self.indexes)

    def __len__(self):
        return int(len(self.indexes) / self.batch_size) + 1

    def __iter__(self):
        for i in range(self.__len__()):
            self.item = i
            yield self.__getitem__(index=i)

        self.item = 0

    def __next__(self):
        return self.__getitem__(index=self.item)

    def __call__(self):
        return self.__iter__()

    def __data_generation(self, selection):
        if self.tbt is not None:
            if self.dict_output:
                dct = {'input_'+str(j) : arr for j, 
                        arr in enumerate(
                  self.tbt.transform(self.X.iloc[selection, :]))}
                return dct, self.y[selection]
            else:
                return self.tbt.transform(
                     self.X.iloc[selection, :]), self.y[selection]
        else:
            return self.X.iloc[selection, :], self.y[selection]

    def __getitem__(self, index):
        indexes = self.indexes[
                  index*self.batch_size:(index+1)*self.batch_size]
        samples, labels = self.__data_generation(indexes)
        return samples, labels, [None] 

生成器已经设置好。接下来让我们进入下一部分,详细探索它是如何工作的。

它是如何工作的…

除了 __init__ 方法,该方法实例化类的内部变量外,DataGenerator 类还包括以下方法:

  • _build_index:用于创建提供数据的索引

  • on_epoch_end:在每个周期结束时,这个方法将随机打乱数据

  • __len__:报告完成一个周期所需的批次数

  • __iter__:使该类成为可迭代对象

  • __next__:调用下一个批次

  • __call__:返回 __iter__ 方法的调用

  • __data_generation:在这里,TabularTransformer 对数据批次进行操作,返回转换后的输出(作为数组列表或数组字典返回)

  • __getitem__:将数据拆分成批次,并调用 __data_generation 方法进行转换

这完成了最后一块拼图。通过使用最后两个配方,你只需填写几个参数,就可以将任何混合变量的表格数据完全转换并交付给 TensorFlow 模型。在接下来的两个配方中,我们将为你提供一些特定的技巧,以帮助我们的 DNN 更好地处理表格数据,并且我们将展示一个来自著名 Kaggle 竞赛的完整示例。

为表格数据创建自定义激活函数

对于图像和文本数据来说,由于数据是稀疏的,在 DNN 处理表格数据时反向传播误差更加困难。虽然 ReLU 激活函数广泛使用,但已经发现新的激活函数在这种情况下表现更好,可以提高网络性能。这些激活函数包括 SeLU、GeLU 和 Mish。由于 SeLU 已经包含在 Keras 和 TensorFlow 中(参见 www.tensorflow.org/api_docs/python/tf/keras/activations/seluwww.tensorflow.org/api_docs/python/tf/nn/selu),在这个配方中我们将使用 GeLU 和 Mish 激活函数。

准备工作

你需要导入常见的模块:

from tensorflow import keras as keras
import numpy as np
import matplotlib.pyplot as plt 

我们已经添加了 matplotlib,可以绘制这些新激活函数的工作效果,并了解它们有效性的原因。

如何实现…

GeLU 和 Mish 的数学公式已在它们的原始论文中定义,您可以在其中找到详细信息:

下面是翻译成代码的公式:

def gelu(x):
    return 0.5 * x * (1 + tf.tanh(tf.sqrt(2 / np.pi) * 
                        (x + 0.044715 * tf.pow(x, 3))))
keras.utils.get_custom_objects().update(
                         {'gelu': keras.layers.Activation(gelu)})
def mish(inputs):
    return inputs * tf.math.tanh(tf.math.softplus(inputs))
keras.utils.get_custom_objects().update(
                         {'mish': keras.layers.Activation(mish)}) 

这个方法有趣的地方在于 get_custom_objects 是一个函数,允许你在自定义的 TensorFlow 对象中记录新的函数,并且可以轻松地在层参数中将其作为字符串调用。你可以通过查看 TensorFlow 文档了解更多关于自定义对象的工作原理:www.tensorflow.org/api_docs/python/tf/keras/utils/get_custom_objects

它是如何工作的…

我们可以通过绘制正负输入与其输出的关系来了解这两个激活函数是如何工作的。使用 matplotlib 的几个命令将帮助我们进行可视化:

gelu_vals = list()
mish_vals = list()
abscissa = np.arange(-4, 1, 0.1)
for val in abscissa:
    gelu_vals.append(gelu(tf.cast(val, tf.float32)).numpy())
    mish_vals.append(mish(tf.cast(val, tf.float32)).numpy())

plt.plot(abscissa, gelu_vals, label='gelu')
plt.plot(abscissa, mish_vals, label='mish')
plt.axvline(x=0.0, linestyle='--', color='darkgray')
plt.axhline(y=0.0, linestyle='--', color='darkgray')
plt.legend()
plt.show() 

运行代码后,你应该得到以下图形:

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_07_05.png

图 7.3:GeLU 和 Mish 激活函数从输入到输出的映射

与 ReLU 激活函数类似,输入从零开始到正数的部分会被直接映射为输出(保持正激活时的线性关系)。有趣的部分出现在输入小于零时,实际上,因为它不像 ReLU 那样被抑制。在 GeLU 和 Mish 激活函数中,负输入的输出是经过抑制的变换,当输入非常负时会趋近于零。这避免了神经元“死亡”的问题,因为负输入仍然可以传递信息,也避免了神经元饱和的问题,因为过于负的值会被关闭。

通过不同的策略,GeLU 和 Mish 激活函数都能处理和传播负输入。这使得负输入的梯度是有定义的,并且不会对网络造成伤害。

在一个困难问题上进行测试

本章中,我们提供了一些有效处理表格数据的技巧。每个技巧本身并不是一个完整的解决方案,而是一个拼图的一部分。当这些部分组合在一起时,你可以获得出色的结果。在这最后一个技巧中,我们将展示如何将所有技巧结合起来,成功完成一个困难的 Kaggle 挑战。

Kaggle 竞赛 Amazon.com – Employee Access Challenge (www.kaggle.com/c/amazon-employee-access-challenge) 是一个因涉及高基数变量而著名的竞赛,也是比较梯度提升算法的一个重要基准。该竞赛的目的是开发一个模型,根据员工的角色和活动预测是否应该给予其访问特定资源的权限。结果应该以可能性形式给出。作为预测因子,你将使用不同的 ID 代码,这些代码对应于你正在评估访问权限的资源类型、员工在组织中的角色以及推荐经理。

准备工作

和往常一样,我们先导入 TensorFlow 和 Keras:

import tensorflow as tf
import tensorflow.keras as keras 

使用基于顺序的数据生成器可能会触发 TensorFlow 2.2 中的一些错误。这是由于启用执行(eager execution),因此作为预防措施,我们必须为此配方禁用它:

tf.compat.v1.disable_eager_execution() 

为了获取亚马逊数据集,最佳且最快的方法是安装CatBoost,一种使用数据集作为基准的梯度提升算法。如果它还没有安装在你的环境中,你可以通过运行pip install catboost命令轻松安装:

from catboost.datasets import amazon
X, Xt = amazon()
y = X["ACTION"].apply(lambda x: 1 if x == 1 else 0).values
X.drop(["ACTION"], axis=1, inplace=True) 

由于测试数据(已上传至Xt变量)包含未标记的目标变量,我们将只使用X变量中的训练数据。

如何实现…

首先,我们将定义此问题的 DNN 架构。由于问题只涉及具有高基数的分类变量,我们从为每个特征设置输入和嵌入层开始。

我们首先为每个特征定义一个输入,其中数据流入网络,然后每个输入被导入到其各自的嵌入层。输入的大小根据特征的唯一值数量来确定,输出的大小则基于输入大小的对数。每个嵌入层的输出随后传递到空间丢弃层(由于嵌入层会返回一个矩阵,空间丢弃层会将整个矩阵的列置空),然后进行展平。最后,所有展平后的结果被连接成一个单一的层。从此,数据必须通过两个带丢弃层的全连接层,最终到达输出响应节点,该节点是一个经过 sigmoid 激活的节点,返回一个概率作为答案:

def dnn(categorical_variables, categorical_counts,
        feature_selection_dropout=0.2, categorical_dropout=0.1,
        first_dense = 256, second_dense = 256, 
        dense_dropout = 0.2, 
        activation_type=gelu):

    categorical_inputs = []
    categorical_embeddings = []

    for category in categorical_variables:
        categorical_inputs.append(keras.layers.Input(
                 shape=[1], name=category))
        category_counts = categorical_counts[category]
        categorical_embeddings.append(
            keras.layers.Embedding(category_counts+1, 
                      int(np.log1p(category_counts)+1), 
                      name = category +  
                              "_embed")(categorical_inputs[-1]))

    def flatten_dropout(x, categorical_dropout):
        return keras.layers.Flatten()(
            keras.layers.SpatialDropout1D(categorical_dropout)(x))

    categorical_logits = [flatten_dropout(cat_emb, 
                                          categorical_dropout) 
                          for cat_emb in categorical_embeddings]
    categorical_concat = keras.layers.Concatenate(
                  name = "categorical_concat")(categorical_logits)
    x = keras.layers.Dense(first_dense,  
                   activation=activation_type)(categorical_concat)
    x = keras.layers.Dropout(dense_dropout)(x)  
    x = keras.layers.Dense(second_dense, 
                   activation=activation_type)(x)
    x = keras.layers.Dropout(dense_dropout)(x)
    output = keras.layers.Dense(1, activation="sigmoid")(x)
    model = keras.Model(categorical_inputs, output)

    return model 

该架构仅适用于分类数据。它将每个分类输入(预期为单个整数编码)传入一个嵌入层,嵌入层的输出是一个降维向量(其维度通过启发式方法int(np.log1p(category_counts)+1)计算得出)。接着应用SpatialDropout1D,最后将输出展平。SpatialDropout1D会去除输出矩阵一行中的所有连接,去掉所有通道中的一些信息。所有分类变量的输出将被连接并传递到一系列带 GeLU 激活和丢弃层的全连接层。最后,输出一个单一的 sigmoid 节点(因此你可以得到[0,1]范围内的概率输出)。

定义完架构后,我们将定义评分函数,取自 scikit-learn,并使用 TensorFlow 中的tf.py_function将它们转换为 Keras 可以使用的函数(www.tensorflow.org/api_docs/python/tf/py_function),这是一个包装器,可以将任何函数转化为一个一次可微分的 TensorFlow 操作,并可以进行即时执行。

作为得分函数,我们使用平均精度和 ROC AUC。这两者都能帮助我们了解在二分类问题上的表现,告诉我们预测的概率与真实值之间的接近程度。有关 ROC AUC 和平均精度的更多信息,请参见 scikit-learn 文档中的scikit-learn.org/stable/modules/generated/sklearn.metrics.average_precision_score.htmlscikit-learn.org/stable/modules/generated/sklearn.metrics.roc_auc_score.html#sklearn.metrics.roc_auc_score

我们还实例化了一个简单的绘图函数,可以绘制在训练过程中记录的选定误差和得分度量,既适用于训练集也适用于验证集:

from sklearn.metrics import average_precision_score, roc_auc_score
def mAP(y_true, y_pred):
    return tf.py_function(average_precision_score, 
                          (y_true, y_pred), tf.double)
def auc(y_true, y_pred):
    try:
        return tf.py_function(roc_auc_score, 
                              (y_true, y_pred), tf.double)
    except: 
        return 0.5
def compile_model(model, loss, metrics, optimizer):
    model.compile(loss=loss, metrics=metrics, optimizer=optimizer)
    return model
def plot_keras_history(history, measures):
    """
    history: Keras training history
    measures = list of names of measures
    """
    rows = len(measures) // 2 + len(measures) % 2
    fig, panels = plt.subplots(rows, 2, figsize=(15, 5))
    plt.subplots_adjust(top = 0.99, bottom=0.01, 
                        hspace=0.4, wspace=0.2)
    try:
        panels = [item for sublist in panels for item in sublist]
    except:
        pass
    for k, measure in enumerate(measures):
        panel = panels[k]
        panel.set_title(measure + ' history')
        panel.plot(history.epoch, history.history[measure], 
                   label="Train "+measure)
        panel.plot(history.epoch, history.history["val_"+measure], 
                   label="Validation "+measure)
        panel.set(xlabel='epochs', ylabel=measure)
        panel.legend()

    plt.show(fig) 

此时,你需要设置训练阶段。考虑到样本数量有限以及你需要测试你的解决方案,使用交叉验证是最好的选择。scikit-learn 中的StratifiedKFold函数将为你提供完成此任务的正确工具。

StratifiedKFold中,你的数据会随机(你可以提供一个种子值以确保可重复性)被划分为k个部分,每一部分的目标变量比例与原始数据中的比例相同。

这些k拆分被用来生成k个训练测试,这些测试可以帮助你推断你所设置的 DNN 架构的表现。事实上,在进行k次实验时,除了一个拆分,其余的都被用来训练模型,而那个被保留的拆分则每次都用于测试。这确保了你有k个测试,都是在未用于训练的拆分上进行的。

这种方法,尤其是在训练样本较少时,比选择单一的测试集来验证模型更为优越,因为通过抽取测试集,你可能会发现一个与训练集分布不同的样本。此外,使用单一的测试集也有可能导致过拟合。如果你反复测试不同的解决方案,最终可能会找到一个非常适合测试集的解决方案,但这个解决方案并不一定具有泛化能力。

让我们在这里实践一下:

from sklearn.model_selection import StratifiedKFold
SEED = 0
FOLDS = 3
BATCH_SIZE = 512
skf = StratifiedKFold(n_splits=FOLDS, 
                      shuffle=True, 
                      random_state=SEED)
roc_auc = list()
average_precision = list()
categorical_variables = X.columns.to_list()
for fold, (train_idx, test_idx) in enumerate(skf.split(X, y)):

    tt = TabularTransformer(highcat = categorical_variables)
    tt.fit(X.iloc[train_idx])   
    categorical_levels = tt.vocabulary

    model = dnn(categorical_variables,
                categorical_levels, 
                feature_selection_dropout=0.1,
                categorical_dropout=0.1,
                first_dense=64,
                second_dense=64,
                dense_dropout=0.1,
                activation_type=mish)

    model = compile_model(model, 
                          keras.losses.binary_crossentropy, 
                          [auc, mAP], 
                          tf.keras.optimizers.Adam(learning_rate=0.0001))

    train_batch = DataGenerator(X.iloc[train_idx], 
                                y[train_idx],
                                tabular_transformer=tt,
                                batch_size=BATCH_SIZE,
                                shuffle=True)

    val_X, val_y = tt.transform(X.iloc[test_idx]), y[test_idx]

    history = model.fit(train_batch,
                        validation_data=(val_X, val_y),
                        epochs=30,
                        class_weight=[1.0, 
                                   (np.sum(y==0) / np.sum(y==1))],
                        verbose=2)

    print("\nFOLD %i" % fold)
    plot_keras_history(history, measures=['auc', 'loss'])

    preds = model.predict(val_X, verbose=0, 
                          batch_size=1024).flatten()
    roc_auc.append(roc_auc_score(y_true=val_y, y_score=preds))
    average_precision.append(average_precision_score(
                                 y_true=val_y, y_score=preds))

print(f"mean cv roc auc {np.mean(roc_auc):0.3f}")
print(f"mean cv ap {np.mean(average_precision):0.3f}") 

该脚本会为每个折叠执行训练和验证测试,并存储结果,这些结果将帮助你正确评估你在表格数据上的深度神经网络(DNN)表现。

它是如何工作的……

每个折叠将打印出一张图,详细展示 DNN 在训练集和验证集上的表现,包括对数损失和 ROC AUC:

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_07_06.png

图 7.4:DNN 在训练集和验证集上的表现

所有的折叠都有类似的轨迹,在训练和验证曲线在 5 个 epoch 之后明显分离,并且在 15 个 epoch 后差距扩大,暗示训练阶段可能存在一定的过拟合。通过修改你的 DNN 架构,并改变如学习率或优化算法等参数,你可以放心地进行实验,尝试获得更好的结果,因为交叉验证过程可以确保你做出正确的决策。

第八章:卷积神经网络

卷积神经网络CNNs)是近年来图像识别领域取得重大突破的关键。在本章中,我们将讨论以下主题:

  • 实现一个简单的 CNN

  • 实现一个高级 CNN

  • 重新训练现有的 CNN 模型

  • 应用 StyleNet 和神经风格项目

  • 实现 DeepDream

提醒读者,本章的所有代码可以在这里在线获取:github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook,以及 Packt 仓库:github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook

介绍

在前面的章节中,我们讨论了密集神经网络DNNs),其中一层的每个神经元都与相邻层的每个神经元相连接。在本章中,我们将重点介绍一种在图像分类中表现良好的特殊类型的神经网络:CNN。

CNN 是由两个组件组成的:一个特征提取模块,后接一个可训练的分类器。第一个组件包括一堆卷积、激活和池化层。一个 DNN 负责分类。每一层的神经元都与下一层的神经元相连接。

在数学中,卷积是一个应用于另一个函数输出的运算。在我们的例子中,我们考虑使用矩阵乘法(滤波器)作用于图像。对于我们的目的,我们将图像视为一个数字矩阵。这些数字可以代表像素或图像属性。我们将对这些矩阵应用卷积操作,方法是将一个固定宽度的滤波器在图像上移动,并使用逐元素相乘得到结果。

请参阅下图,以便更好地理解图像卷积的工作原理:

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_01.png

图 8.1:一个 2x2 的卷积滤波器应用于一个 5x5 的输入矩阵,生成一个新的 4x4 特征层

图 8.1中,我们看到如何将卷积滤波器应用于图像(长×宽×深度),从而创建一个新的特征层。在这里,我们使用一个2x2的卷积滤波器,作用于5x5输入的有效空间,并且在两个方向上的步幅都为 1。结果是一个4x4的矩阵。这个新特征层突出了输入图像中激活滤波器最多的区域。

CNN 还具有其他操作来满足更多的需求,例如引入非线性(ReLU),或聚合参数(最大池化、平均池化)以及其他类似操作。上面的示例图展示了在一个5x5数组上应用卷积操作,卷积滤波器是一个2x2的矩阵。步幅为 1,并且我们只考虑有效的放置位置。在此操作中的可训练变量将是2x2滤波器的权重。

在卷积操作之后,通常会跟随一个聚合操作,例如最大池化。池化操作的目标是减少参数数量、计算负担和内存使用。最大池化保留了最强的特征。

以下图示提供了最大池化操作的一个示例。在这个例子中,池化操作的区域是一个 2x2 的区域,步长为 2。

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_02.png

图 8.2:在 4x4 输入图像上应用最大池化操作

图 8.2 展示了最大池化操作的工作原理。在这里,我们有一个 2x2 的窗口,在一个 4x4 的输入图像上滑动,步长为 2。结果是一个 2x2 的矩阵,它就是每个区域的最大值。

虽然我们将通过创建自己的 CNN 来进行图像识别,但我建议使用现有的架构,正如我们在本章其余部分将要做的那样。

通常我们会使用一个预训练的网络,并通过新的数据集和一个新的全连接层对其进行再训练。这种方法很有利,因为我们不必从零开始训练模型;我们只需要对预训练模型进行微调,以适应我们的新任务。我们将在本章稍后的 重新训练现有的 CNN 模型 部分进行演示,其中我们将重新训练现有架构,以提高 CIFAR-10 的预测性能。

不再拖延,我们立即开始实现一个简单的 CNN。

实现一个简单的 CNN

在这个实例中,我们将开发一个基于 LeNet-5 架构的 CNN,LeNet-5 首次由 Yann LeCun 等人于 1998 年提出,用于手写和机器打印字符的识别。

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_03.png

图 8.3:LeNet-5 架构 – 原始图像来源于 [LeCun 等人, 1998]

该架构由两组 CNN 组成,包含卷积-ReLU-最大池化操作,用于特征提取,随后是一个扁平化层和两个全连接层,用于分类图像。

我们的目标是提高对 MNIST 数字的预测准确性。

准备开始

要访问 MNIST 数据,Keras 提供了一个包(tf.keras.datasets),它具有出色的数据集加载功能。(请注意,TensorFlow 也提供了自己的现成数据集集合,通过 TF Datasets API。)加载数据后,我们将设置模型变量,创建模型,按批次训练模型,然后可视化损失、准确率和一些样本数字。

如何做到…

执行以下步骤:

  1. 首先,我们将加载必要的库并启动图形会话:

    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf 
    
  2. 接下来,我们将加载数据并将图像重塑为四维矩阵:

    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() 
    # Reshape
    x_train = x_train.reshape(-1, 28, 28, 1)
    x_test = x_test.reshape(-1, 28, 28, 1)
    #Padding the images by 2 pixels
    x_train = np.pad(x_train, ((0,0),(2,2),(2,2),(0,0)), 'constant')
    x_test = np.pad(x_test, ((0,0),(2,2),(2,2),(0,0)), 'constant') 
    

    请注意,这里下载的 MNIST 数据集包括训练集和测试集。该数据集由灰度图像(形状为(num_sample, 28, 28)的整数数组)和标签(范围为 0-9 的整数)组成。我们对图像进行了 2 像素的填充,因为在 LeNet-5 论文中,输入图像是32x32的。

  3. 现在,我们将设置模型的参数。记住,图像的深度(通道数)是 1,因为这些图像是灰度图像。我们还将设置一个种子,以确保结果可复现:

    image_width = x_train[0].shape[0]
    image_height = x_train[0].shape[1]
    num_channels = 1 # grayscale = 1 channel
    seed = 98
    np.random.seed(seed)
    tf.random.set_seed(seed) 
    
  4. 我们将声明我们的训练数据变量和测试数据变量。我们将为训练和评估使用不同的批次大小。你可以根据可用的物理内存调整这些大小:

    batch_size = 100
    evaluation_size = 500
    epochs = 300
    eval_every = 5 
    
  5. 我们将对图像进行归一化,将所有像素的值转换为统一的尺度:

    x_train = x_train / 255
    x_test = x_test/ 255 
    
  6. 现在我们将声明我们的模型。我们将有一个特征提取模块,由两个卷积/ReLU/最大池化层组成,接着是一个由全连接层构成的分类器。此外,为了使分类器能够工作,我们将特征提取模块的输出展平,以便可以在分类器中使用。请注意,我们在分类器的最后一层使用了 softmax 激活函数。Softmax 将数值输出(logits)转换为概率,使其总和为 1:

    input_data = tf.keras.Input(dtype=tf.float32, shape=(image_width,image_height, num_channels), name="INPUT")
    # First Conv-ReLU-MaxPool Layer
    conv1 = tf.keras.layers.Conv2D(filters=6,
                                   kernel_size=5,
                                   padding='VALID',
                                   activation="relu",
                                   name="C1")(input_data)
    max_pool1 = tf.keras.layers.MaxPool2D(pool_size=2,
                                          strides=2, 
                                          padding='SAME',
                                          name="S1")(conv1)
    # Second Conv-ReLU-MaxPool Layer
    conv2 = tf.keras.layers.Conv2D(filters=16,
                                   kernel_size=5,
                                   padding='VALID',
                                   strides=1,
                                   activation="relu",
                                   name="C3")(max_pool1)
    max_pool2 = tf.keras.layers.MaxPool2D(pool_size=2,
                                          strides=2, 
                                          padding='SAME',
                                          name="S4")(conv2)
    # Flatten Layer
    flatten = tf.keras.layers.Flatten(name="FLATTEN")(max_pool2)
    # First Fully Connected Layer
    fully_connected1 = tf.keras.layers.Dense(units=120,
                                             activation="relu",
                                             name="F5")(flatten)
    # Second Fully Connected Layer
    fully_connected2 = tf.keras.layers.Dense(units=84,
                                             activation="relu",
                                             name="F6")(fully_connected1)
    # Final Fully Connected Layer
    final_model_output = tf.keras.layers.Dense(units=10,
                                               activation="softmax",
                                               name="OUTPUT"
                                               )(fully_connected2)
    
    model = tf.keras.Model(inputs= input_data, outputs=final_model_output) 
    
  7. 接下来,我们将使用 Adam(自适应矩估计)优化器来编译模型。Adam 使用自适应学习率和动量,使我们能够更快地达到局部最小值,从而加速收敛。由于我们的目标是整数,而不是独热编码格式,我们将使用稀疏分类交叉熵损失函数。然后,我们还将添加一个准确度指标,以评估模型在每个批次上的准确性:

    model.compile(
        optimizer="adam", 
        loss="sparse_categorical_crossentropy",
        metrics=["accuracy"] 
    
  8. 接下来,我们打印网络的字符串摘要:

    model.summary() 
    

    https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_04.png

    图 8.4:LeNet-5 架构

    LeNet-5 模型有 7 层,包含 61,706 个可训练参数。现在,让我们开始训练模型。

  9. 现在我们可以开始训练我们的模型了。我们通过随机选择的批次来遍历数据。每隔一段时间,我们选择在训练集和测试集批次上评估模型,并记录准确率和损失值。我们可以看到,经过 300 个周期后,我们很快在测试数据上达到了 96-97%的准确率:

    train_loss = []
    train_acc = []
    test_acc = []
    for i in range(epochs):
        rand_index = np.random.choice(len(x_train), size=batch_size)
        rand_x = x_train[rand_index]
        rand_y = y_train[rand_index]
    
        history_train = model.train_on_batch(rand_x, rand_y)
    
        if (i+1) % eval_every == 0:
            eval_index = np.random.choice(len(x_test), size=evaluation_size)
            eval_x = x_test[eval_index]
            eval_y = y_test[eval_index]
    
            history_eval = model.evaluate(eval_x,eval_y)
    
            # Record and print results
            train_loss.append(history_train[0])
            train_acc.append(history_train[1])
            test_acc.append(history_eval[1])
            acc_and_loss = [(i+1), history_train
     [0], history_train[1], history_eval[1]]
            acc_and_loss = [np.round(x,2) for x in acc_and_loss]
            print('Epoch # {}. Train Loss: {:.2f}. Train Acc (Test Acc): {:.2f} ({:.2f})'.format(*acc_and_loss)) 
    
  10. 这将产生以下输出:

    Epoch # 5\. Train Loss: 2.19\. Train Acc (Test Acc): 0.23 (0.34)
    Epoch # 10\. Train Loss: 2.01\. Train Acc (Test Acc): 0.59 (0.58)
    Epoch # 15\. Train Loss: 1.71\. Train Acc (Test Acc): 0.74 (0.73)
    Epoch # 20\. Train Loss: 1.32\. Train Acc (Test Acc): 0.73 (0.77)
     ...
    Epoch # 290\. Train Loss: 0.18\. Train Acc (Test Acc): 0.95 (0.94)
    Epoch # 295\. Train Loss: 0.13\. Train Acc (Test Acc): 0.96 (0.96)
    Epoch # 300\. Train Loss: 0.12\. Train Acc (Test Acc): 0.95 (0.97) 
    
  11. 以下是使用Matplotlib绘制损失和准确率的代码:

    # Matlotlib code to plot the loss and accuracy
    eval_indices = range(0, epochs, eval_every)
    # Plot loss over time
    plt.plot(eval_indices, train_loss, 'k-')
    plt.title('Loss per Epoch')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.show()
    # Plot train and test accuracy
    plt.plot(eval_indices, train_acc, 'k-', label='Train Set Accuracy')
    plt.plot(eval_indices, test_acc, 'r--', label='Test Set Accuracy')
    plt.title('Train and Test Accuracy')
    plt.xlabel('Epoch')
    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_08_05.png

    图 8.5:左图是我们 300 个训练周期中训练集和测试集的准确率。右图是 300 个周期中的 softmax 损失值。

  12. 如果我们想要绘制最新批次结果的示例,这里是绘制包含六个最新结果的样本的代码:

    # Plot some samples and their predictions
    actuals = y_test[30:36]
    preds = model.predict(x_test[30:36])
    predictions = np.argmax(preds,axis=1)
    images = np.squeeze(x_test[30:36])
    Nrows = 2
    Ncols = 3
    for i in range(6):
        plt.subplot(Nrows, Ncols, i+1)
        plt.imshow(np.reshape(images[i], [32,32]), cmap='Greys_r')
        plt.title('Actual: ' + str(actuals[i]) + ' Pred: ' + str(predictions[i]),
                                   fontsize=10)
        frame = plt.gca()
        frame.axes.get_xaxis().set_visible(False)
        frame.axes.get_yaxis().set_visible(False)
    plt.show() 
    

    上述代码的输出如下:

    https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_06.png

图 8.6:六个随机图像的图示,标题中包括实际值和预测值。左下角的图片被预测为 6,实际上它是 4。

使用一个简单的 CNN,我们在此数据集上取得了较好的准确率和损失结果。

它是如何工作的…

我们在 MNIST 数据集上的表现有所提升,构建了一个从零开始训练并迅速达到约 97%准确率的模型。我们的特征提取模块是卷积、ReLU 和最大池化的组合。我们的分类器是全连接层的堆叠。我们在批次大小为 100 的情况下进行训练,并查看了跨越各个 epoch 的准确率和损失情况。最后,我们还绘制了六个随机数字,发现模型预测失败,未能正确预测一张图片。模型预测为 6,实际上是 4。

CNN 在图像识别方面表现非常出色。部分原因在于卷积层生成其低级特征,当遇到图像中的重要部分时,这些特征会被激活。这种模型能够自我创建特征并将其用于预测。

还有更多…

在过去几年里,CNN 模型在图像识别方面取得了巨大进展。许多新颖的想法正在被探索,并且新的架构频繁被发现。该领域有一个庞大的科学论文库,名为 arXiv.org(arxiv.org/),由康奈尔大学创建和维护。arXiv.org 包含许多领域的最新文章,包括计算机科学及其子领域,如计算机视觉和图像识别(arxiv.org/list/cs.CV/recent)。

另见

这里有一些你可以用来了解 CNN 的优秀资源:

实现一个高级 CNN

能够扩展 CNN 模型进行图像识别至关重要,这样我们才能理解如何增加网络的深度。这样,如果我们有足够的数据,我们可能提高预测的准确性。扩展 CNN 网络的深度是以标准方式进行的:我们只需重复卷积、最大池化和 ReLU 直到我们对深度感到满意。许多更精确的图像识别网络都是以这种方式运行的。

数据加载和预处理可能会让人头疼:大多数图像数据集过大,无法完全加载到内存中,但为了提高模型性能,图像预处理是必须的。我们可以使用 TensorFlow 的 tf.data API 创建输入管道。这个 API 提供了一套用于加载和预处理数据的工具。通过它,我们将从 CIFAR-10 数据集实例化一个 tf.data.Dataset 对象(通过 Keras 数据集 API tf.keras.datasets 下载),将该数据集的连续元素合并成批次,并对每张图像应用变换。另外,在图像识别数据中,通常会在训练前随机扰动图像。在这里,我们将随机裁剪、翻转并调整亮度。

准备就绪

在本食谱中,我们将实现一种更高级的图像数据读取方法,并使用一个更大的 CNN 对 CIFAR-10 数据集进行图像识别(www.cs.toronto.edu/~kriz/cifar.html)。该数据集包含 60,000 张32x32的图像,属于 10 个可能类别中的一个。图像的潜在标签包括飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。请参阅另见部分的第一点。

官方的 TensorFlow tf.data 教程可以在本食谱末尾的另见部分找到。

如何操作…

执行以下步骤:

  1. 首先,我们加载必要的库:

    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    from tensorflow import keras 
    
  2. 现在我们将声明一些数据集和模型参数,然后声明一些图像变换参数,例如随机裁剪图像的大小:

    # Set dataset and model parameters
    batch_size = 128
    buffer_size= 128
    epochs=20
    #Set transformation parameters
    crop_height = 24
    crop_width = 24 
    
  3. 现在我们将使用 keras.datasets API 从 CIFAR-10 数据集中获取训练和测试图像。这个 API 提供了几个可以完全加载到内存的小型数据集,因此数据会以 NumPy 数组的形式表示(NumPy 是用于科学计算的核心 Python 库):

    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data() 
    
  4. 接下来,我们将使用 tf.data.Dataset 从 NumPy 数组创建训练和测试的 TensorFlow 数据集,以便利用 tf.data API 构建一个灵活高效的图像管道:

    dataset_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
    dataset_test = tf.data.Dataset.from_tensor_slices((x_test, y_test)) 
    
  5. 我们将定义一个读取函数,该函数将加载并稍微扭曲图像,以便使用 TensorFlow 内建的图像修改功能进行训练:

    # Define CIFAR reader
    def read_cifar_files(image, label):
        final_image = tf.image.resize_with_crop_or_pad(image, crop_width, crop_height)
        final_image = image / 255
        # Randomly flip the image horizontally, change the brightness and contrast
        final_image = tf.image.random_flip_left_right(final_image)
        final_image = tf.image.random_brightness(final_image,max_delta=0.1)
        final_image = tf.image.random_contrast(final_image,lower=0.5, upper=0.8)
        return final_image, label 
    
  6. 现在我们有了一个图像处理管道函数和两个 TensorFlow 数据集,我们可以初始化训练图像管道和测试图像管道:

    dataset_train_processed = dataset_train.shuffle(buffer_size).batch(batch_size).map(read_cifar_files)
    dataset_test_processed = dataset_test.batch(batch_size).map(lambda image,label: read_cifar_files(image, label, False)) 
    

    请注意,在这个例子中,我们的输入数据适合内存,因此我们使用from_tensor_slices()方法将所有图像转换为tf.Tensor。但是tf.data API 允许处理不适合内存的大型数据集。对数据集的迭代是以流式传输的方式进行的。

  7. 接下来,我们可以创建我们的序列模型。我们将使用的模型有两个卷积层,后面跟着三个全连接层。两个卷积层将分别创建 64 个特征。第一个全连接层将第二个卷积层与 384 个隐藏节点连接起来。第二个全连接操作将这 384 个隐藏节点连接到 192 个隐藏节点。最后一个隐藏层操作将这 192 个节点连接到我们要预测的 10 个输出类别。在最后一层我们将使用 softmax 函数,因为一张图片只能取一个确切的类别,所以输出应该是对这 10 个目标的概率分布:

    model = keras.Sequential(
        [# First Conv-ReLU-Conv-ReLU-MaxPool Layer
         tf.keras.layers.Conv2D(input_shape=[32,32,3],
                                filters=32,
                                kernel_size=3,
                                padding='SAME',
                                activation="relu",
                                kernel_initializer='he_uniform',
                                name="C1"),
        tf.keras.layers.Conv2D(filters=32,
                               kernel_size=3,
                               padding='SAME',
                               activation="relu",
                               kernel_initializer='he_uniform',
                               name="C2"),
         tf.keras.layers.MaxPool2D((2,2),
                                   name="P1"),
         tf.keras.layers.Dropout(0.2),
        # Second Conv-ReLU-Conv-ReLU-MaxPool Layer
         tf.keras.layers.Conv2D(filters=64,
                                kernel_size=3,
                                padding='SAME',
                                activation="relu",
                                kernel_initializer='he_uniform',
                                name="C3"),
        tf.keras.layers.Conv2D(filters=64,
                               kernel_size=3,
                               padding='SAME',
                               activation="relu",
                               kernel_initializer='he_uniform',
                               name="C4"),
         tf.keras.layers.MaxPool2D((2,2),
                                   name="P2"),
         tf.keras.layers.Dropout(0.2),
        # Third Conv-ReLU-Conv-ReLU-MaxPool Layer
         tf.keras.layers.Conv2D(filters=128,
                                kernel_size=3,
                                padding='SAME',
                                activation="relu",
                                kernel_initializer='he_uniform',
                                name="C5"),
        tf.keras.layers.Conv2D(filters=128,
                               kernel_size=3,
                               padding='SAME',
                               activation="relu",
                               kernel_initializer='he_uniform',
                               name="C6"),
         tf.keras.layers.MaxPool2D((2,2),
                                   name="P3"),
         tf.keras.layers.Dropout(0.2),
         # Flatten Layer
         tf.keras.layers.Flatten(name="FLATTEN"),
         # Fully Connected Layer
         tf.keras.layers.Dense(units=128,
                               activation="relu",
                               name="D1"),
        tf.keras.layers.Dropout(0.2),
        # Final Fully Connected Layer
        tf.keras.layers.Dense(units=10,
                              activation="softmax",
                              name="OUTPUT")
        ]) 
    
  8. 现在我们将编译我们的模型。我们的损失将是分类交叉熵损失。我们添加了一个精度度量,它接收模型预测的 logits 和实际目标,并返回用于记录训练/测试集统计信息的准确率。我们还运行 summary 方法以打印一个总结页面:

    model.compile(loss="sparse_categorical_crossentropy",
        metrics=["accuracy"]
    )
    model.summary() 
    

    https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_07.png

    图 8.7:模型摘要由 3 个 VGG 块组成(VGG - Visual Geometry Group - 块是一系列卷积层,后跟用于空间下采样的最大池化层),随后是一个分类器。

  9. 现在我们拟合模型,通过我们的训练和测试输入管道进行循环。我们将保存训练损失和测试准确率:

    history = model.fit(dataset_train_processed, 
                        validation_data=dataset_test_processed, 
                        epochs=epochs) 
    
  10. 最后,这里是一些Matplotlib代码,将绘制整个训练过程中的损失和测试准确率:

    # Print loss and accuracy
    # Matlotlib code to plot the loss and accuracy
    epochs_indices = range(0, 10, 1)
    # Plot loss over time
    plt.plot(epochs_indices, history.history["loss"], 'k-')
    plt.title('Softmax Loss per Epoch')
    plt.xlabel('Epoch')
    plt.ylabel('Softmax Loss')
    plt.show()
    # Plot accuracy over time
    plt.plot(epochs_indices, history.history["val_accuracy"], 'k-')
    plt.title('Test Accuracy per Epoch')
    plt.xlabel('Epoch')
    plt.ylabel('Accuracy')
    plt.show() 
    

    我们为这个配方得到以下图表:

    https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_08.png

图 8.8:训练损失在左侧,测试准确率在右侧。对于 CIFAR-10 图像识别 CNN,我们能够在测试集上达到大约 80%的准确率。

它是如何工作的…

在我们下载了 CIFAR-10 数据之后,我们建立了一个图像管道。我们使用这个训练和测试管道来尝试预测图像的正确类别。到最后,模型在测试集上达到了大约 80%的准确率。我们可以通过使用更多数据、微调优化器或增加更多 epochs 来达到更高的准确率。

另请参阅

重新训练现有的 CNN 模型

从零开始训练一个新的图像识别模型需要大量时间和计算资源。如果我们能拿一个预训练的网络,并用我们的图像重新训练它,可能会节省计算时间。在这个方案中,我们将展示如何使用一个预训练的 TensorFlow 图像识别模型,并对其进行微调,以便处理不同的图像集。

我们将演示如何使用来自预训练网络的迁移学习来处理 CIFAR-10。这个方法的思路是重用先前模型中卷积层的权重和结构,并重新训练网络顶部的全连接层。这个方法被称为微调

准备就绪

我们将使用的 CNN 网络采用一种非常流行的架构,叫做Inception。Inception CNN 模型由谷歌创建,在许多图像识别基准测试中表现出色。详细信息请参见另见部分第二个项目中提到的论文。

我们将介绍的主要 Python 脚本展示了如何获取 CIFAR-10 图像数据,并将其转换为 Inception 重训练格式。之后,我们将再次说明如何在我们的图像上训练 Inception v3 网络。

如何实现…

执行以下步骤:

  1. 我们将从加载必要的库开始:

    import tensorflow as tf
    from tensorflow import keras
    from tensorflow.keras.applications.inception_v3 import InceptionV3
    from tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictions 
    
  2. 我们现在将设置稍后通过tf.data.Dataset API 使用的参数:

    batch_size = 32
    buffer_size= 1000 
    
  3. 现在,我们将下载 CIFAR-10 数据,并声明用于稍后保存图像时引用的 10 个类别:

    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
    objects = ['airplane', 'automobile', 'bird', 'cat', 'deer',
               'dog', 'frog', 'horse', 'ship', 'truck'] 
    
  4. 然后,我们将使用tf.data.Dataset初始化数据管道,用于训练和测试数据集:

    dataset_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
    dataset_test = tf.data.Dataset.from_tensor_slices((x_test, y_test)) 
    
  5. Inception v3 在 ImageNet 数据集上进行过预训练,因此我们的 CIFAR-10 图像必须与这些图像的格式匹配。预期的宽度和高度应不小于 75,因此我们将图像调整为75x75的空间大小。然后,图像应该被归一化,我们将对每张图像应用 Inception 预处理任务(preprocess_input方法)。

    def preprocess_cifar10(img, label):
        img = tf.cast(img, tf.float32)
        img = tf.image.resize(img, (75, 75))
    return tf.keras.applications.inception_v3.preprocess_input(img) , label
    dataset_train_processed = dataset_train.shuffle(buffer_size).batch(batch_size).map(preprocess_cifar10)
    dataset_test_processed = dataset_test.batch(batch_size).map(preprocess_cifar10) 
    
  6. 现在,我们将基于 InceptionV3 模型创建我们的模型。我们将使用tensorflow.keras.applications API 加载 InceptionV3 模型。该 API 包含可以用于预测、特征提取和微调的预训练深度学习模型。然后,我们将加载没有分类头的权重。

    inception_model = InceptionV3(
        include_top=False,
        weights="imagenet",
        input_shape=(75,75,3)
    ) 
    
  7. 我们在 InceptionV3 模型的基础上构建自己的模型,添加一个具有三层全连接层的分类器。

    x = inception_model.output
    x= keras.layers.GlobalAveragePooling2D()(x)
    x = keras.layers.Dense(1024, activation="relu")(x)
    x = keras.layers.Dense(128, activation="relu")(x)
    output = keras.layers.Dense(10, activation="softmax")(x)
    model=keras.Model(inputs=inception_model.input, outputs = output) 
    
  8. 我们将把 Inception 的基础层设置为不可训练。只有分类器的权重会在反向传播阶段更新(Inception 的权重不会更新):

    for inception_layer in inception_model.layers:
        inception_layer.trainable= False 
    
  9. 现在我们将编译我们的模型。我们的损失函数将是分类交叉熵损失。我们添加了一个精度指标,该指标接收模型预测的对数和实际目标,并返回用于记录训练/测试集统计数据的精度:

    model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"]) 
    
  10. 现在我们将拟合模型,通过我们的训练和测试输入流水线循环进行:

    model.fit(x=dataset_train_processed , 
              validation_data=dataset_test_processed) 
    
  11. 到最后,该模型在测试集上达到了约 63% 的准确率:

    loss: 1.1316 - accuracy: 0.6018 - val_loss: 1.0361 - val_accuracy: 0.6366... 
    

它是如何工作的…

在下载了 CIFAR-10 数据之后,我们建立了一个图像流水线来将图像转换为所需的 Inception 格式。我们在 InceptionV3 模型之上添加了一个分类器,并对其进行了训练,以预测 CIFAR-10 图像的正确类别。到最后,该模型在测试集上达到了约 63% 的准确率。请记住,我们正在微调模型并重新训练顶部的全连接层,以适应我们的十类数据。

参见

应用 StyleNet 和神经风格项目

一旦我们训练了一个图像识别的 CNN,我们可以使用网络本身进行一些有趣的数据和图像处理。StyleNet 是一个过程,试图从一个图片中学习风格,并将其应用到第二张图片,同时保持第二张图片的结构(或内容)不变。为了做到这一点,我们必须找到与风格强相关的中间 CNN 节点,独立于图片内容。

StyleNet 是一个过程,它接受两幅图像并将一幅图像的风格应用到第二幅图像的内容上。它基于 2015 年 Leon Gatys 的著名论文,A Neural Algorithm of Artistic Style(参见下一节参见部分下的第一条)进行操作。作者在一些 CNN 中发现了一种包含中间层的特性。其中一些似乎编码了图片的风格,而另一些则编码了其内容。因此,如果我们在风格图片上训练风格层,并在原始图片上训练内容层,并反向传播这些计算出的损失,我们就可以将原始图片更改为更像风格图片。

准备工作

这个食谱是官方 TensorFlow 神经风格迁移的改编版本,可在本食谱结尾的参见部分找到。

为了实现这一点,我们将使用 Gatys 在A Neural Algorithm of Artistic Style中推荐的网络,称为imagenet-vgg-19

如何做…

执行以下步骤:

  1. 首先,我们将通过加载必要的库启动我们的 Python 脚本:

    import imageio
    import numpy as np
    from skimage.transform import resize
    import tensorflow as tf
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    import IPython.display as display
    import PIL.Image 
    
  2. 然后,我们可以声明两张图像的位置:原始图像和风格图像。对于我们的示例,我们将使用本书的封面图像作为原始图像;风格图像我们将使用文森特·凡高的 《星夜》。你也可以使用任何你想要的两张图片。如果你选择使用这些图片,它们可以在本书的 GitHub 网站上找到,github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook(导航到 StyleNet 部分):

    content_image_file = 'images/book_cover.jpg' 
    style_image_file = 'images/starry_night.jpg' 
    
  3. 现在,我们将使用 scipy 加载两张图像,并调整风格图像的大小,使其与内容图像的尺寸相符:

    # Read the images
    content_image = imageio.imread(content_image_file)
    style_image = imageio.imread(style_image_file)
    content_image = tf.image.convert_image_dtype(content_image, tf.float32)
    style_image = tf.image.convert_image_dtype(style_image, tf.float32)
    # Get shape of target and make the style image the same
    target_shape = content_image.shape
    style_image = resize(style_image, target_shape) 
    
  4. 接下来,我们将展示内容图像和风格图像:

    mpl.rcParams['figure.figsize'] = (12,12)
    mpl.rcParams['axes.grid'] = False
    plt.subplot(1, 2, 1)
    plt.imshow(content_image)
    plt.title("Content Image")
    plt.subplot(1, 2, 2)
    plt.imshow(style_image)
    plt.title("Style Image") 
    

    https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_09.png

    图 8.9:示例内容和风格图像

  5. 现在,我们将加载在 ImageNet 上预训练的 VGG-19 模型,但不包括分类头。我们将使用 tensorflow.keras.applications API。这个 API 包含了可用于预测、特征提取和微调的预训练深度学习模型。

    vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
    vgg.trainable = False 
    
  6. 接下来,我们将展示 VGG-19 的架构:

    [layer.name for layer in vgg.layers] 
    
  7. 在神经风格迁移中,我们希望将一张图像的风格应用到另一张图像的内容上。卷积神经网络(CNN)由多个卷积层和池化层组成。卷积层提取复杂特征,而池化层提供空间信息。Gatys 的论文推荐了一些策略,用于为内容图像和风格图像分配中间层。我们应当保留 block4_conv2 作为内容图像的层,可以尝试其他 blockX_conv1 层输出的不同组合来作为风格图像的层:

    content_layers = ['block4_conv2', 'block5_conv2']
    style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1']
    num_content_layers = len(content_layers)
    num_style_layers = len(style_layers) 
    
  8. 虽然中间特征图的值代表了图像的内容,但风格可以通过这些特征图的均值和相关性来描述。在这里,我们定义了 Gram 矩阵来捕捉图像的风格。Gram 矩阵衡量每个特征图之间的相关性程度。这个计算是针对每个中间特征图进行的,只获得图像的纹理信息。注意,我们会丢失关于图像空间结构的信息。

    def gram_matrix(input_tensor):
      result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor)
      input_shape = tf.shape(input_tensor)
      num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32)
      return result/(num_locations) 
    
  9. 接下来,我们构建一个模型,返回包含每个层名称及其相关内容/风格张量的风格和内容字典。Gram 矩阵应用于风格层:

    class StyleContentModel(tf.keras.models.Model):
      def __init__(self, style_layers, content_layers):
        super(StyleContentModel, self).__init__()
        self.vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
        outputs = [vgg.get_layer(name).output for name in style_layers + content_layers]
        self.vgg = tf.keras.Model([vgg.input], outputs)
        self.style_layers = style_layers
        self.content_layers = content_layers
        self.num_style_layers = len(style_layers)
        self.vgg.trainable = False
      def call(self, inputs):
        "Expects float input in [0,1]"
        inputs = inputs*255.0
        inputs = inputs[tf.newaxis, :]
        preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs)
        outputs = self.vgg(preprocessed_input)
        style_outputs, content_outputs = (outputs[:self.num_style_layers], 
                                                                outputs[self.num_style_layers:])
        style_outputs = [gram_matrix(style_output)
                         for style_output in style_outputs]
        content_dict = {content_name:value 
                        for content_name, value 
                        in zip(self.content_layers, content_outputs)}
        style_dict = {style_name:value
                      for style_name, value
                      in zip(self.style_layers, style_outputs)}
    
        return {'content':content_dict, 'style':style_dict} 
    
  10. 设置风格和内容的目标值,它们将用于损失计算:

    extractor = StyleContentModel(style_layers, content_layers)
    style_targets = extractor(style_image)['style']
    content_targets = extractor(content_image)['content'] 
    
  11. Adam 和 LBFGS 通常会有相同的误差并且很快收敛,但 LBFGS 在处理大图像时优于 Adam。虽然论文推荐使用 LBFGS,由于我们的图像较小,我们将选择 Adam 优化器。

    #Optimizer configuration
    learning_rate = 0.05
    beta1 = 0.9
    beta2 = 0.999
    opt = tf.optimizers.Adam(learning_rate=learning_rate, beta_1=beta1, beta_2=beta2) 
    
  12. 接下来,我们将计算总损失,它是内容损失和风格损失的加权和:

    content_weight = 5.0
    style_weight = 1.0 
    
  13. 内容损失将比较我们的原始图像和当前图像(通过内容层特征)。风格损失将比较我们预先计算的风格特征与输入图像中的风格特征。第三个也是最终的损失项将有助于平滑图像。我们在这里使用总变差损失来惩罚相邻像素的剧烈变化,如下所示:

    def style_content_loss(outputs):
        style_outputs = outputs['style']
        content_outputs = outputs['content']
        style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2) 
                               for name in style_outputs.keys()])
        style_loss *= style_weight / num_style_layers
        content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2) 
                              for name in content_outputs.keys()])
        content_loss *= content_weight / num_content_layers
        loss = style_loss + content_loss
        return loss 
    
  14. 接下来,我们声明一个工具函数。由于我们有一个浮动图像,需要将像素值保持在 0 和 1 之间:

    def clip_0_1(image):
      return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0) 
    
  15. 现在,我们声明另一个工具函数,将张量转换为图像:

    def tensor_to_image(tensor):
      tensor = tensor*255
      tensor = np.array(tensor, dtype=np.uint8)
      if np.ndim(tensor)>3:
        assert tensor.shape[0] == 1
        tensor = tensor[0]
      return PIL.Image.fromarray(tensor) 
    
  16. 接下来,我们使用梯度带运行梯度下降,生成我们的新图像,并显示如下:

    epochs = 100
    image = tf.Variable(content_image)
    for generation in range(epochs):
    
        with tf.GradientTape() as tape:
            outputs = extractor(image)
            loss = style_content_loss(outputs)
        grad = tape.gradient(loss, image)
        opt.apply_gradients([(grad, image)])
        image.assign(clip_0_1(image))
        print(".", end='')
    display.display(tensor_to_image(image)) 
    

    https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_10.png

图 8.10:使用 StyleNet 算法将书籍封面图像与《星夜》结合

请注意,可以通过更改内容和样式权重来使用不同的强调方式。

它是如何工作的…

我们首先加载了两张图像,然后加载了预训练的网络权重,并将层分配给内容图像和风格图像。我们计算了三个损失函数:内容图像损失、风格损失和总变差损失。然后,我们训练了随机噪声图片,以使用风格图像的风格和原始图像的内容。风格迁移可以用于照片和视频编辑应用、游戏、艺术、虚拟现实等。例如,在 2019 年游戏开发者大会上,Google 推出了 Stadia,可以实时改变游戏的艺术风格。其实时演示视频可在本食谱最后的另见部分查看。

另见

实现 DeepDream

训练好的 CNN 还有一个用途,那就是利用一些中间节点检测标签特征(例如,猫的耳朵或鸟的羽毛)。利用这一点,我们可以找到将任何图像转化为反映这些节点特征的方式,适用于我们选择的任何节点。这个教程是官方 TensorFlow DeepDream 教程的改编版本(参见下文 另见 部分的第一个项目)。欢迎访问 DeepDream 的创造者亚历山大·莫尔维茨夫(Alexander Mordvintsev)在 Google AI 博客上写的文章(下文 另见 部分的第二个项目)。希望通过这个教程,能够帮助你使用 DeepDream 算法探索 CNN 和其中创建的特征。

准备工作

最初,这项技术是为了更好地理解 CNN 如何“看”图像而发明的。DeepDream 的目标是过度解读模型检测到的模式,并生成具有超现实模式的激发性视觉内容。这种算法是一种新型的迷幻艺术。

如何进行操作…

执行以下步骤:

  1. 要开始使用 DeepDream,我们首先需要加载必要的库:

    import numpy as np
    import PIL.Image
    import imageio
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    import tensorflow as tf
    import IPython.display as display 
    
  2. 我们将准备图像进行梦幻化处理。我们将读取原始图像,将其重塑为最大 500 尺寸,并显示出来:

    # Read the images	
    original_img_file = path + 'images/book_cover.jpg' 
    original_img = imageio.imread(original_img_file)
    # Reshape to 500 max dimension
    new_shape = tf.cast((500, 500 * original_img.shape[1] / original_img.shape[0]), tf.int32)
    original_img = tf.image.resize(original_img, new_shape, method='nearest').numpy()
    # Display the image
    mpl.rcParams['figure.figsize'] = (20,6)
    mpl.rcParams['axes.grid'] = False
    plt.imshow(original_img)
    plt.title("Original Image") 
    
  3. 我们将加载在 ImageNet 上预训练的 Inception 模型,并去除分类头。我们将使用 tf.keras.applications API:

    inception_model = tf.keras.applications.InceptionV3(include_top=False, weights='imagenet') 
    
  4. 我们总结了这个模型。我们可以注意到,Inception 模型相当庞大:

    inception_model.summary() 
    
  5. 接下来,我们将选择用于后续 DeepDream 处理的卷积层。在 CNN 中,较早的层提取基本特征,如边缘、形状、纹理等,而较深的层则提取高级特征,如云、树木或鸟类。为了创建 DeepDream 图像,我们将专注于卷积层混合的地方。现在,我们将创建一个以这两个混合层为输出的特征提取模型:

    names = ['mixed3', 'mixed5']
    layers = [inception_model.get_layer(name).output for name in names]
    deep_dream_model = tf.keras.Model(inputs=inception_model.input, outputs=layers) 
    
  6. 现在我们将定义损失函数,返回所有输出层的总和:

    def compute_loss(img, model):
      # Add a dimension to the image to have a batch of size 1.
      img_batch = tf.expand_dims(img, axis=0)
      # Apply the model to the images and get the outputs to retrieve the activation.
      layer_activations = model(img_batch)
    
      # Compute the loss for each layer
      losses = []
      for act in layer_activations:
        loss = tf.math.reduce_mean(act)
        losses.append(loss)
      return  tf.reduce_sum(losses) 
    
  7. 我们声明了两个实用函数,用于撤销缩放并显示处理后的图像:

    def deprocess(img):
      img = 255*(img + 1.0)/2.0
      return tf.cast(img, tf.uint8)
    def show(img):
      display.display(PIL.Image.fromarray(np.array(img))) 
    
  8. 接下来,我们将应用梯度上升过程。在 DeepDream 中,我们不是通过梯度下降最小化损失,而是通过梯度上升最大化这些层的激活,通过最大化它们的损失。这样,我们会过度解读模型检测到的模式,并生成具有超现实模式的激发性视觉内容:

    def run_deep_dream(image, steps=100, step_size=0.01):
        # Apply the Inception preprocessing
        image = tf.keras.applications.inception_v3.preprocess_input(image)
        image = tf.convert_to_tensor(image)
        loss = tf.constant(0.0)
        for n in tf.range(steps):
            # We use gradient tape to track TensorFlow computations
            with tf.GradientTape() as tape:
                # We use watch to force TensorFlow to track the image
                tape.watch(image)
                # We compute the loss
                loss = compute_loss(image, deep_dream_model)
            # Compute the gradients
            gradients = tape.gradient(loss, image)
            # Normalize the gradients.
            gradients /= tf.math.reduce_std(gradients) + 1e-8 
    
            # Perform the gradient ascent by directly adding the gradients to the image
            image = image + gradients*step_size
            image = tf.clip_by_value(image, -1, 1)
            # Display the intermediate image
            if (n % 100 ==0):
                display.clear_output(wait=True)
                show(deprocess(image))
                print ("Step {}, loss {}".format(n, loss))
        # Display the final image
        result = deprocess(image)
        display.clear_output(wait=True)
        show(result)
        return result 
    
  9. 然后,我们将在原始图像上运行 DeepDream:

    dream_img = run_deep_dream(image=original_img, 
                               steps=100, step_size=0.01) 
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_11.png

    图 8.11:DeepDream 应用于原始图像

    虽然结果不错,但还是可以做得更好!我们注意到图像输出很杂乱,模式似乎以相同的粒度应用,并且输出分辨率较低。

  10. 为了改善图像,我们可以使用“八度”概念。我们对同一张图像进行梯度上升,并多次调整图像大小(每次增大图像的大小就是一次八度的改进)。通过这个过程,较小尺度上检测到的特征可以应用到更高尺度上,展现出更多细节的图案。

    OCTAVE_SCALE = 1.30
    image = tf.constant(np.array(original_img))
    base_shape = tf.shape(image)[:-1]
    float_base_shape = tf.cast(base_shape, tf.float32)
    for n in range(-2, 3):
        # Increase the size of the image
        new_shape = tf.cast(float_base_shape*(OCTAVE_SCALE**n), tf.int32)
        image = tf.image.resize(image, new_shape).numpy()
        # Apply deep dream
        image = run_deep_dream(image=image, steps=50, step_size=0.01)
    # Display output
    display.clear_output(wait=True)
    image = tf.image.resize(image, base_shape)
    image = tf.image.convert_image_dtype(image/255.0, dtype=tf.uint8)
    show(image) 
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_08_12.png

图 8.12:应用八度概念后的原始图像 DeepDream

通过使用八度概念,结果变得非常有趣:输出变得不那么嘈杂,网络能更好地放大它所看到的图案。

还有更多…

我们建议读者使用官方的 DeepDream 教程作为进一步了解的来源,同时也可以访问原始的 Google 研究博客文章,了解 DeepDream(请参阅以下 另见 部分)。

另见

第九章:循环神经网络

循环神经网络RNN)是建模顺序数据的现代主要方法。架构类名中的“循环”一词指的是当前步骤的输出成为下一步的输入(可能还会作为后续步骤的输入)。在序列中的每个元素上,模型都会同时考虑当前的输入和它对前面元素的“记忆”。

自然语言处理NLP)任务是 RNN 的主要应用领域之一:如果你正在阅读这一句,你就是通过前面出现的词来理解每个词的上下文。基于 RNN 的 NLP 模型可以利用这种方式实现生成任务,如新文本创作,也可以完成预测任务,如情感分类或机器翻译。

在本章中,我们将涵盖以下主题:

  • 文本生成

  • 情感分类

  • 时间序列 – 股票价格预测

  • 开放领域问答

我们要处理的第一个主题是文本生成:它很容易演示我们如何使用 RNN 生成新内容,因此可以作为一个温和的 RNN 入门。

文本生成

演示 RNN 强大功能的最著名应用之一是生成新文本(我们将在稍后的 Transformer 架构章节中回到这个应用)。

在这个实例中,我们将使用长短期记忆LSTM)架构——一种流行的 RNN 变体——来构建文本生成模型。LSTM 这个名字来源于它们开发的动机:传统的 RNN 在处理长依赖关系时会遇到困难(即所谓的梯度消失问题),而 LSTM 通过其架构解决了这一问题。LSTM 模型通过维持一个单元状态和一个“携带”信息来确保信号(以梯度的形式)在序列处理时不会丢失。在每个时间步,LSTM 模型会联合考虑当前的单词、携带的信息和单元状态。

这个主题本身并不简单,但出于实际应用考虑,完全理解结构设计并非必需。只需记住,LSTM 单元允许将过去的信息在稍后的时间点重新注入。

我们将使用纽约时报评论和标题数据集(www.kaggle.com/aashita/nyt-comments)来训练我们的模型,并利用它生成新的标题。我们选择这个数据集是因为它的中等规模(该方法不需要强大的工作站即可复现)和易获取性(Kaggle 是免费开放的,而一些数据源只能通过付费墙访问)。

如何实现…

和往常一样,首先我们导入必要的包。

import tensorflow as tf
from tensorflow import keras 
# keras module for building LSTM 
from keras.preprocessing.sequence import pad_sequences
from keras.layers import Embedding, LSTM, Dense
from keras.preprocessing.text import Tokenizer
from keras.callbacks import EarlyStopping
from keras.models import Sequential
import keras.utils as ku 

我们希望确保我们的结果是可复现的——由于 Python 深度学习生态系统内的相互依赖关系,我们需要初始化多个随机机制。

import pandas as pd
import string, os 
import warnings
warnings.filterwarnings("ignore")
warnings.simplefilter(action='ignore', category=FutureWarning) 

下一步是从 Keras 本身导入必要的功能:

from keras.preprocessing.sequence import pad_sequences
from keras.layers import Embedding, LSTM, Dense
from keras.preprocessing.text import Tokenizer
from keras.callbacks import EarlyStopping
from keras.models import Sequential
import keras.utils as ku 

最后,通常来说,定制化代码执行过程中显示的警告级别是方便的——尽管这未必总是符合纯粹主义者的最佳实践标准——这主要是为了处理围绕给 DataFrame 子集分配值的普遍警告:在当前环境下,清晰展示代码比遵守生产环境中的编码标准更为重要:

import warnings
warnings.filterwarnings("ignore")
warnings.simplefilter(action='ignore', category=FutureWarning) 

我们将定义一些函数,以便后续简化代码。首先,让我们清理文本:

def clean_text(txt):
    txt = "".join(v for v in txt if v not in string.punctuation).lower()
    txt = txt.encode("utf8").decode("ascii",'ignore')
    return txt 

我们将使用一个封装器,围绕内置的 TensorFlow 分词器,操作如下:

def get_sequence_of_tokens(corpus):
    ## tokenization
    tokenizer.fit_on_texts(corpus)
    total_words = len(tokenizer.word_index) + 1

    ## convert data to sequence of tokens 
    input_sequences = []
    for line in corpus:
        token_list = tokenizer.texts_to_sequences([line])[0]
        for i in range(1, len(token_list)):
            n_gram_sequence = token_list[:i+1]
            input_sequences.append(n_gram_sequence)
    return input_sequences, total_words 

一个常用的步骤是将模型构建步骤封装到函数中:

def create_model(max_sequence_len, total_words):
    input_len = max_sequence_len - 1
    model = Sequential()

    model.add(Embedding(total_words, 10, input_length=input_len))
    model.add(LSTM(100))
    model.add(Dense(total_words, activation='softmax'))
    model.compile(loss='categorical_crossentropy', optimizer='adam')

    return model 

以下是对序列进行填充的一些模板代码(在实际应用中,这部分的作用会变得更加清晰):

def generate_padded_sequences(input_sequences):
    max_sequence_len = max([len(x) for x in input_sequences])
    input_sequences = np.array(pad_sequences(input_sequences,                       maxlen=max_sequence_len, padding='pre'))

    predictors, label = input_sequences[:,:-1],input_sequences[:,-1]
    label = ku.to_categorical(label, num_classes=total_words)
    return predictors, label, max_sequence_len 

最后,我们创建一个函数,用来从已拟合的模型生成文本:

def generate_text(seed_text, next_words, model, max_sequence_len):
    for _ in range(next_words):
        token_list = tokenizer.texts_to_sequences([seed_text])[0]
        token_list = pad_sequences([token_list],                      maxlen=max_sequence_len-1, padding='pre')
        predicted = model.predict_classes(token_list, verbose=0)

        output_word = ""
        for word,index in tokenizer.word_index.items():
            if index == predicted:
                output_word = word
                break
        seed_text += " "+output_word
    return seed_text.title() 

下一步是加载我们的数据集(break 子句作为快速方法,只选择文章数据集,而不包括评论数据集):

curr_dir = '../input/'
all_headlines = []
for filename in os.listdir(curr_dir):
    if 'Articles' in filename:
        article_df = pd.read_csv(curr_dir + filename)
        all_headlines.extend(list(article_df.headline.values))
        break
all_headlines[:10] 

我们可以按如下方式检查前几个元素:

['The Opioid Crisis Foretold',
 'The Business Deals That Could Imperil Trump',
 'Adapting to American Decline',
 'The Republicans' Big Senate Mess',
 'States Are Doing What Scott Pruitt Won't',
 'Fake Pearls, Real Heart',
 'Fear Beyond Starbucks',
 'Variety: Puns and Anagrams',
 'E.P.A. Chief's Ethics Woes Have Echoes in His Past',
 'Where Facebook Rumors Fuel Thirst for Revenge'] 

正如现实中大多数文本数据的情况一样,我们需要清理输入文本。为了简单起见,我们仅进行基础的预处理:去除标点符号并将所有单词转换为小写:

corpus = [clean_text(x) for x in all_headlines] 

清理操作后,前 10 行的数据如下所示:

corpus[:10]
['the opioid crisis foretold',
 'the business deals that could imperil trump',
 'adapting to american decline',
 'the republicans big senate mess',
 'states are doing what scott pruitt wont',
 'fake pearls real heart',
 'fear beyond starbucks',
 'variety puns and anagrams',
 'epa chiefs ethics woes have echoes in his past',
 'where facebook rumors fuel thirst for revenge'] 

下一步是分词。语言模型要求输入数据以序列的形式呈现——给定一个由单词(词元)组成的序列,生成任务的关键在于预测上下文中下一个最可能的词元。我们可以利用 Keras 的 preprocessing 模块中内置的分词器。

清理后,我们对输入文本进行分词:这是从语料库中提取单独词元(单词或术语)的过程。我们利用内置的分词器来提取词元及其相应的索引。每个文档都会转化为一系列词元:

tokenizer = Tokenizer()
inp_sequences, total_words = get_sequence_of_tokens(corpus)
inp_sequences[:10]
[[1, 708],
 [1, 708, 251],
 [1, 708, 251, 369],
 [1, 370],
 [1, 370, 709],
 [1, 370, 709, 29],
 [1, 370, 709, 29, 136],
 [1, 370, 709, 29, 136, 710],
 [1, 370, 709, 29, 136, 710, 10],
 [711, 5]] 

[1,708][1,708, 251] 这样的向量表示从输入数据生成的 n-grams,其中整数是该词元在从语料库生成的总体词汇表中的索引。

我们已经将数据集转化为由词元序列组成的格式——这些序列的长度可能不同。有两种选择:使用 RaggedTensors(这在用法上稍微复杂一些)或者将序列长度统一,以符合大多数 RNN 模型的标准要求。为了简化展示,我们选择后者方案:使用 pad_sequence 函数填充短于阈值的序列。这一步与将数据格式化为预测值和标签的步骤容易结合:

predictors, label, max_sequence_len =                              generate_padded_sequences(inp_sequences) 

我们利用简单的 LSTM 架构,使用 Sequential API:

  1. 输入层:接收分词后的序列

  2. LSTM 层:使用 LSTM 单元生成输出——为了演示,我们默认取 100 作为值,但此参数(以及其他多个参数)是可定制的

  3. Dropout 层:我们对 LSTM 输出进行正则化,以减少过拟合的风险

  4. 输出层:生成最可能的输出标记:

    model = create_model(max_sequence_len, total_words)
    model.summary()
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    embedding_1 (Embedding)      (None, 23, 10)            31340     
    _________________________________________________________________
    lstm_1 (LSTM)                (None, 100)               44400     
    _________________________________________________________________
    dense_1 (Dense)              (None, 3134)              316534    
    =================================================================
    Total params: 392,274
    Trainable params: 392,274
    Non-trainable params: 0
    _________________________________________________________________ 
    

现在我们可以使用标准的 Keras 语法训练我们的模型:

model.fit(predictors, label, epochs=100, verbose=2) 

现在我们有了一个拟合的模型,我们可以检查其性能:基于种子文本,我们的 LSTM 生成的标题有多好?我们通过对种子文本进行分词、填充序列,并将其传入模型来获得预测结果:

print (generate_text("united states", 5, model, max_sequence_len))
United States Shouldnt Sit Still An Atlantic
print (generate_text("president trump", 5, model, max_sequence_len))
President Trump Vs Congress Bird Moving One
print (generate_text("joe biden", 8, model, max_sequence_len))
Joe Biden Infuses The Constitution Invaded Canada Unique Memorial Award
print (generate_text("india and china", 8, model, max_sequence_len))
India And China Deal And The Young Think Again To It
print (generate_text("european union", 4, model, max_sequence_len))
European Union Infuses The Constitution Invaded 

正如你所看到的,即使使用相对简单的设置(一个中等大小的数据集和一个基础模型),我们也能生成看起来相当真实的文本。当然,进一步的微调将允许生成更复杂的内容,这将是我们在第十章中讨论的内容,Transformers

另见

网上有多个优秀的资源可以学习 RNN:

情感分类

自然语言处理(NLP)中的一个常见任务是情感分类:根据文本片段的内容,识别其中表达的情感。实际应用包括评论分析、调查回复、社交媒体评论或健康护理材料。

我们将在 www-cs.stanford.edu/people/alecmgo/papers/TwitterDistantSupervision09.pdf 中介绍的 Sentiment140 数据集上训练我们的网络,该数据集包含 160 万条带有三类标注的推文:负面、中立和正面。为了避免本地化问题,我们对编码进行标准化(最好从控制台级别进行,而不是在笔记本内进行)。逻辑如下:原始数据集包含原始文本,这些文本——由于其固有性质——可能包含非标准字符(例如,表情符号,这在社交媒体通信中显然很常见)。我们希望将文本转换为 UTF8——这是英语 NLP 的事实标准。最快的方法是使用 Linux 命令行功能:

  • Iconv 是一个标准的工具,用于在编码之间进行转换

  • -f-t 标志分别表示输入编码和目标编码

  • -o 指定输出文件:

iconv -f LATIN1 -t UTF8 training.1600000.processed.noemoticon.csv -o training_cleaned.csv 

如何实现…

我们首先按如下方式导入必要的包:

import json
import tensorflow as tf
import csv
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
from tensorflow.keras import regularizers 

接下来,我们定义模型的超参数:

  • 嵌入维度是我们将使用的词嵌入的大小。在本食谱中,我们将使用 GloVe:一种无监督学习算法,基于维基百科和 Gigaword 的合并语料库的词共现统计信息进行训练。得到的(英文)单词向量为我们提供了有效的文本表示方式,通常称为嵌入。

  • max_lengthpadding_type是指定如何填充序列的参数(见前面的食谱)。

  • training_size指定了目标语料库的大小。

  • test_portion定义了我们将作为保留数据使用的数据比例。

  • dropout_valnof_units是模型的超参数:

embedding_dim = 100
max_length = 16
trunc_type='post'
padding_type='post'
oov_tok = "<OOV>"
training_size=160000
test_portion=.1
num_epochs = 50
dropout_val = 0.2
nof_units = 64 

让我们将模型创建步骤封装成一个函数。我们为我们的分类任务定义了一个相当简单的模型——一个嵌入层,后接正则化、卷积、池化,再加上 RNN 层:

def create_model(dropout_val, nof_units):

    model = tf.keras.Sequential([
    tf.keras.layers.Embedding(vocab_size+1, embedding_dim, input_length=max_length, weights=[embeddings_matrix], trainable=False),
    tf.keras.layers.Dropout(dropout_val),
    tf.keras.layers.Conv1D(64, 5, activation='relu'),
    tf.keras.layers.MaxPooling1D(pool_size=4),
    tf.keras.layers.LSTM(nof_units),
    tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    model.compile(loss='binary_crossentropy',optimizer='adam',                  metrics=['accuracy'])
    return model 

收集我们将用于训练的语料库内容:

num_sentences = 0
with open("../input/twitter-sentiment-clean-dataset/training_cleaned.csv") as csvfile:
    reader = csv.reader(csvfile, delimiter=',')
    for row in reader:
        list_item=[]
        list_item.append(row[5])
        this_label=row[0]
        if this_label=='0':
            list_item.append(0)
        else:
            list_item.append(1)
        num_sentences = num_sentences + 1
        corpus.append(list_item) 

转换为句子格式:

sentences=[]
labels=[]
random.shuffle(corpus)
for x in range(training_size):
    sentences.append(corpus[x][0])
    labels.append(corpus[x][1])
    Tokenize the sentences:
tokenizer = Tokenizer()
tokenizer.fit_on_texts(sentences)
word_index = tokenizer.word_index
vocab_size = len(word_index)
sequences = tokenizer.texts_to_sequences(sentences) 

使用填充规范化句子长度(见前一节):

padded = pad_sequences(sequences, maxlen=max_length, padding=padding_type, truncating=trunc_type) 

将数据集划分为训练集和保留集:

split = int(test_portion * training_size)
test_sequences = padded[0:split]
training_sequences = padded[split:training_size]
test_labels = labels[0:split]
training_labels = labels[split:training_size] 

在使用基于 RNN 的模型进行 NLP 应用时,一个关键步骤是embeddings矩阵:

embeddings_index = {};
with open('../input/glove6b/glove.6B.100d.txt') as f:
    for line in f:
        values = line.split();
        word = values[0];
        coefs = np.asarray(values[1:], dtype='float32');
        embeddings_index[word] = coefs;
embeddings_matrix = np.zeros((vocab_size+1, embedding_dim));
for word, i in word_index.items():
    embedding_vector = embeddings_index.get(word);
    if embedding_vector is not None:
        embeddings_matrix[i] = embedding_vector; 

在所有准备工作完成后,我们可以设置模型:

model = create_model(dropout_val, nof_units)
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 16, 100)           13877100  
_________________________________________________________________
dropout (Dropout)            (None, 16, 100)           0         
_________________________________________________________________
conv1d (Conv1D)              (None, 12, 64)            32064     
_________________________________________________________________
max_pooling1d (MaxPooling1D) (None, 3, 64)             0         
_________________________________________________________________
lstm (LSTM)                  (None, 64)                33024     
_________________________________________________________________
dense (Dense)                (None, 1)                 65        
=================================================================
Total params: 13,942,253
Trainable params: 65,153
Non-trainable params: 13,877,100
_________________________________________________________________ 

训练按常规方式进行:

num_epochs = 50
history = model.fit(training_sequences, training_labels, epochs=num_epochs, validation_data=(test_sequences, test_labels), verbose=2)
Train on 144000 samples, validate on 16000 samples
Epoch 1/50
144000/144000 - 47s - loss: 0.5685 - acc: 0.6981 - val_loss: 0.5454 - val_acc: 0.7142
Epoch 2/50
144000/144000 - 44s - loss: 0.5296 - acc: 0.7289 - val_loss: 0.5101 - val_acc: 0.7419
Epoch 3/50
144000/144000 - 42s - loss: 0.5130 - acc: 0.7419 - val_loss: 0.5044 - val_acc: 0.7481
Epoch 4/50
144000/144000 - 42s - loss: 0.5017 - acc: 0.7503 - val_loss: 0.5134 - val_acc: 0.7421
Epoch 5/50
144000/144000 - 42s - loss: 0.4921 - acc: 0.7563 - val_loss: 0.5025 - val_acc: 0.7518
Epoch 6/50
144000/144000 - 42s - loss: 0.4856 - acc: 0.7603 - val_loss: 0.5003 - val_acc: 0.7509 

我们还可以通过可视化来评估模型的质量:

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(len(acc))
plt.plot(epochs, acc, 'r', label='Training accuracy')
plt.plot(epochs, val_acc, 'b', label='Validation accuracy')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'r', label='Training Loss')
plt.plot(epochs, val_loss, 'b', label='Validation Loss')
plt.title('Training and validation loss')
plt.legend()
plt.show() 

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_09_01.png

图 9.1:训练准确率与验证准确率随训练轮次变化

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_09_02.png

图 9.2:训练损失与验证损失随训练轮次变化

正如我们从两个图表中看到的,模型在有限的训练轮次后已经取得了不错的表现,并且在此之后稳定下来,只有少量波动。潜在的改进包括早停法,并扩大数据集的规模。

另见

有兴趣了解 RNN 在情感分类中的应用的读者可以查阅以下资源:

股票价格预测

像 RNN 这样的顺序模型非常适合时间序列预测——其中一个最为宣传的应用是金融数量的预测,尤其是不同金融工具的价格预测。在本食谱中,我们展示了如何将 LSTM 应用于时间序列预测问题。我们将重点关注比特币的价格——最受欢迎的加密货币。

需要说明的是:这是一个基于流行数据集的演示示例。它并不打算作为任何形式的投资建议;建立一个在金融领域适用的可靠时间序列预测模型是一项具有挑战性的工作,超出了本书的范围。

如何做…

我们首先导入必要的包:

import numpy as np 
import pandas as pd 
from matplotlib import pyplot as plt
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from sklearn.preprocessing import MinMaxScaler 

我们任务的通用参数是预测的未来范围和网络的超参数:

prediction_days = 30
nof_units =4 

如前所述,我们将把模型创建步骤封装到一个函数中。它接受一个参数units,该参数是 LSTM 内单元的维度:

def create_model(nunits):
    # Initialising the RNN
    regressor = Sequential()
    # Adding the input layer and the LSTM layer
    regressor.add(LSTM(units = nunits, activation = 'sigmoid', input_shape = (None, 1)))
    # Adding the output layer
    regressor.add(Dense(units = 1))
    # Compiling the RNN
    regressor.compile(optimizer = 'adam', loss = 'mean_squared_error')

    return regressor 

现在我们可以开始加载数据,并采用常见的时间戳格式。为了演示的目的,我们将预测每日平均价格——因此需要进行分组操作:

# Import the dataset and encode the date
df = pd.read_csv("../input/bitcoin-historical-data/bitstampUSD_1-min_data_2012-01-01_to_2020-09-14.csv")
df['date'] = pd.to_datetime(df['Timestamp'],unit='s').dt.date
group = df.groupby('date')
Real_Price = group['Weighted_Price'].mean() 

下一步是将数据分为训练期和测试期:

df_train= Real_Price[:len(Real_Price)-prediction_days]
df_test= Real_Price[len(Real_Price)-prediction_days:] 

理论上可以避免预处理,但在实践中它有助于加速收敛:

training_set = df_train.values
training_set = np.reshape(training_set, (len(training_set), 1))
sc = MinMaxScaler()
training_set = sc.fit_transform(training_set)
X_train = training_set[0:len(training_set)-1]
y_train = training_set[1:len(training_set)]
X_train = np.reshape(X_train, (len(X_train), 1, 1)) 

拟合模型非常简单:

regressor = create_model(nunits = nof_unit)
regressor.fit(X_train, y_train, batch_size = 5, epochs = 100)
Epoch 1/100
3147/3147 [==============================] - 6s 2ms/step - loss: 0.0319
Epoch 2/100
3147/3147 [==============================] - 3s 928us/step - loss: 0.0198
Epoch 3/100
3147/3147 [==============================] - 3s 985us/step - loss: 0.0089
Epoch 4/100
3147/3147 [==============================] - 3s 1ms/step - loss: 0.0023
Epoch 5/100
3147/3147 [==============================] - 3s 886us/step - loss: 3.3583e-04
Epoch 6/100
3147/3147 [==============================] - 3s 957us/step - loss: 1.0990e-04
Epoch 7/100
3147/3147 [==============================] - 3s 830us/step - loss: 1.0374e-04
Epoch 8/100 

通过拟合模型,我们可以在预测范围内生成一个预测,记得要反转我们的标准化处理,以便将值还原到原始尺度:

test_set = df_test.values
inputs = np.reshape(test_set, (len(test_set), 1))
inputs = sc.transform(inputs)
inputs = np.reshape(inputs, (len(inputs), 1, 1))
predicted_BTC_price = regressor.predict(inputs)
predicted_BTC_price = sc.inverse_transform(predicted_BTC_price) 

这是我们预测结果的样子:

plt.figure(figsize=(25,15), dpi=80, facecolor='w', edgecolor='k')
ax = plt.gca()  
plt.plot(test_set, color = 'red', label = 'Real BTC Price')
plt.plot(predicted_BTC_price, color = 'blue', label = 'Predicted BTC Price')
plt.title('BTC Price Prediction', fontsize=40)
df_test = df_test.reset_index()
x=df_test.index
labels = df_test['date']
plt.xticks(x, labels, rotation = 'vertical')
for tick in ax.xaxis.get_major_ticks():
    tick.label1.set_fontsize(18)
for tick in ax.yaxis.get_major_ticks():
    tick.label1.set_fontsize(18)
plt.xlabel('Time', fontsize=40)
plt.ylabel('BTC Price(USD)', fontsize=40)
plt.legend(loc=2, prop={'size': 25})
plt.show() 

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_09_03.png

图 9.3:实际价格和预测价格随时间变化

总体而言,很明显,即使是一个简单的模型也能生成合理的预测——但有一个重要的警告:这种方法仅在环境保持稳定的情况下有效,即过去和现在的值之间的关系随着时间的推移保持稳定。如果出现体制变化或突发干预,价格可能会受到显著影响,例如,如果某个主要司法管辖区限制了加密货币的使用(正如过去十年所发生的情况)。这种情况可以建模,但需要更复杂的特征工程方法,并且超出了本章的范围。

开放领域问答

问答QA)系统旨在模拟人类在网上搜索信息的过程,并通过机器学习方法提高提供答案的准确性。在这个示例中,我们将演示如何使用 RNN 预测关于维基百科文章的长短答案。我们将使用 Google 的自然问题数据集,并且在ai.google.com/research/NaturalQuestions/visualization上可以找到一个非常好的可视化工具,帮助理解 QA 背后的思想。

基本思想可以总结如下:对于每一对文章-问题对,你必须预测/选择从文章中直接提取的长答案和短答案:

  • 长答案是指回答问题的较长一段文字——通常是几句话或一段话。

  • 简短的回答可能是一个句子或短语,甚至在某些情况下是简单的 YES/NO。简短的答案总是包含在或作为某个合理长答案的子集。

  • 给定的文章可以(并且通常会)允许长短答案,具体取决于问题。

本章所展示的配方改编自 Xing Han Lu 公开的代码:www.kaggle.com/xhlulu

如何做…

正如往常一样,我们首先加载必要的包。这次我们使用 fasttext 嵌入来表示(可以从fasttext.cc/获取)。其他流行的选择包括 GloVe(在情感分析部分使用)和 ELMo(allennlp.org/elmo)。在 NLP 任务的性能方面没有明显的优劣之分,所以我们会根据需要切换选择,以展示不同的可能性:

import os
import json
import gc
import pickle
import numpy as np
import pandas as pd
from tqdm import tqdm_notebook as tqdm
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense, Embedding, SpatialDropout1D, concatenate, Masking
from tensorflow.keras.layers import LSTM, Bidirectional, GlobalMaxPooling1D, Dropout
from tensorflow.keras.preprocessing import text, sequence
from tqdm import tqdm_notebook as tqdm
import fasttext
from tensorflow.keras.models import load_model 

一般设置如下:

embedding_path = '/kaggle/input/fasttext-crawl-300d-2m-with-subword/crawl-300d-2m-subword/crawl-300d-2M-subword.bin' 

我们的下一步是添加一些样板代码,以便之后简化代码流。由于当前任务比之前的任务稍微复杂一些(或不太直观),我们将更多的准备工作封装在数据集构建函数中。由于数据集的大小,我们仅加载训练数据的一个子集,并从中采样带有负标签的数据:

def build_train(train_path, n_rows=200000, sampling_rate=15):
    with open(train_path) as f:
        processed_rows = []
        for i in tqdm(range(n_rows)):
            line = f.readline()
            if not line:
                break
            line = json.loads(line)
            text = line['document_text'].split(' ')
            question = line['question_text']
            annotations = line['annotations'][0]
            for i, candidate in enumerate(line['long_answer_candidates']):
                label = i == annotations['long_answer']['candidate_index']
                start = candidate['start_token']
                end = candidate['end_token']
                if label or (i % sampling_rate == 0):
                    processed_rows.append({
                        'text': " ".join(text[start:end]),
                        'is_long_answer': label,
                        'question': question,
                        'annotation_id': annotations['annotation_id']
                    })
        train = pd.DataFrame(processed_rows)

        return train
def build_test(test_path):
    with open(test_path) as f:
        processed_rows = []
        for line in tqdm(f):
            line = json.loads(line)
            text = line['document_text'].split(' ')
            question = line['question_text']
            example_id = line['example_id']
            for candidate in line['long_answer_candidates']:
                start = candidate['start_token']
                end = candidate['end_token']
                processed_rows.append({
                    'text': " ".join(text[start:end]),
                    'question': question,
                    'example_id': example_id,
                    'sequence': f'{start}:{end}'
                })
        test = pd.DataFrame(processed_rows)

    return test 

使用下一个函数,我们训练一个 Keras tokenizer,将文本和问题编码成整数列表(分词),然后将它们填充到固定长度,形成一个用于文本的单一 NumPy 数组,另一个用于问题:

def compute_text_and_questions(train, test, tokenizer):
    train_text = tokenizer.texts_to_sequences(train.text.values)
    train_questions = tokenizer.texts_to_sequences(train.question.values)
    test_text = tokenizer.texts_to_sequences(test.text.values)
    test_questions = tokenizer.texts_to_sequences(test.question.values)

    train_text = sequence.pad_sequences(train_text, maxlen=300)
    train_questions = sequence.pad_sequences(train_questions)
    test_text = sequence.pad_sequences(test_text, maxlen=300)
    test_questions = sequence.pad_sequences(test_questions)

    return train_text, train_questions, test_text, test_questions 

与基于 RNN 的 NLP 模型一样,我们需要一个嵌入矩阵:

def build_embedding_matrix(tokenizer, path):
    embedding_matrix = np.zeros((tokenizer.num_words + 1, 300))
    ft_model = fasttext.load_model(path)
    for word, i in tokenizer.word_index.items():
        if i >= tokenizer.num_words - 1:
            break
        embedding_matrix[i] = ft_model.get_word_vector(word)

    return embedding_matrix 

接下来是我们的模型构建步骤,用一个函数包装起来:

  1. 我们构建了两个 2 层的双向 LSTM,一个用于读取问题,另一个用于读取文本。

  2. 我们将输出连接起来,并将其传递到一个全连接层:

  3. 我们在输出上使用 sigmoid:

def build_model(embedding_matrix):
    embedding = Embedding(
        *embedding_matrix.shape, 
        weights=[embedding_matrix], 
        trainable=False, 
        mask_zero=True
    )

    q_in = Input(shape=(None,))
    q = embedding(q_in)
    q = SpatialDropout1D(0.2)(q)
    q = Bidirectional(LSTM(100, return_sequences=True))(q)
    q = GlobalMaxPooling1D()(q)

    t_in = Input(shape=(None,))
    t = embedding(t_in)
    t = SpatialDropout1D(0.2)(t)
    t = Bidirectional(LSTM(150, return_sequences=True))(t)
    t = GlobalMaxPooling1D()(t)

    hidden = concatenate([q, t])
    hidden = Dense(300, activation='relu')(hidden)
    hidden = Dropout(0.5)(hidden)
    hidden = Dense(300, activation='relu')(hidden)
    hidden = Dropout(0.5)(hidden)

    out1 = Dense(1, activation='sigmoid')(hidden)

    model = Model(inputs=[t_in, q_in], outputs=out1)
    model.compile(loss='binary_crossentropy', optimizer='adam')
    return model 

使用我们定义的工具包,我们可以构建数据集,具体如下:

directory = '../input/tensorflow2-question-answering/'
train_path = directory + 'simplified-nq-train.jsonl'
test_path = directory + 'simplified-nq-test.jsonl'
train = build_train(train_path)
test = build_test(test_path) 

这就是数据集的样子:

train.head() 

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_09_04.png

tokenizer = text.Tokenizer(lower=False, num_words=80000)
for text in tqdm([train.text, test.text, train.question, test.question]):
    tokenizer.fit_on_texts(text.values)
train_target = train.is_long_answer.astype(int).values
train_text, train_questions, test_text, test_questions = compute_text_and_questions(train, test, tokenizer)
del train 

现在我们可以构建模型本身:

embedding_matrix = build_embedding_matrix(tokenizer, embedding_path)
model = build_model(embedding_matrix)
model.summary()
Model: "functional_1"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            [(None, None)]       0                                            
__________________________________________________________________________________________________
input_2 (InputLayer)            [(None, None)]       0                                            
__________________________________________________________________________________________________
embedding (Embedding)           (None, None, 300)    24000300    input_1[0][0]                    
                                                                 input_2[0][0]                    
__________________________________________________________________________________________________
spatial_dropout1d (SpatialDropo (None, None, 300)    0           embedding[0][0]                  
__________________________________________________________________________________________________
spatial_dropout1d_1 (SpatialDro (None, None, 300)    0           embedding[1][0]                  
__________________________________________________________________________________________________
bidirectional (Bidirectional)   (None, None, 200)    320800      spatial_dropout1d[0][0]          
__________________________________________________________________________________________________
bidirectional_1 (Bidirectional) (None, None, 300)    541200      spatial_dropout1d_1[0][0]        
__________________________________________________________________________________________________
global_max_pooling1d (GlobalMax (None, 200)          0           bidirectional[0][0]              
__________________________________________________________________________________________________
global_max_pooling1d_1 (GlobalM (None, 300)          0           bidirectional_1[0][0]            
__________________________________________________________________________________________________
concatenate (Concatenate)       (None, 500)          0           global_max_pooling1d[0][0]       
                                                                 global_max_pooling1d_1[0][0]     
__________________________________________________________________________________________________
dense (Dense)                   (None, 300)          150300      concatenate[0][0]                
__________________________________________________________________________________________________
dropout (Dropout)               (None, 300)          0           dense[0][0]                      
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 300)          90300       dropout[0][0]                    
__________________________________________________________________________________________________
dropout_1 (Dropout)             (None, 300)          0           dense_1[0][0]                    
__________________________________________________________________________________________________
dense_2 (Dense)                 (None, 1)            301         dropout_1[0][0]                  
==================================================================================================
Total params: 25,103,201
Trainable params: 1,102,901
Non-trainable params: 24,000,300
__________________________________________________________________________________________________ 

接下来的步骤是拟合,按照通常的方式进行:

train_history = model.fit(
    [train_text, train_questions], 
    train_target,
    epochs=2,
    validation_split=0.2,
    batch_size=1024
) 

现在,我们可以构建一个测试集,来查看我们生成的答案:

directory = '/kaggle/input/tensorflow2-question-answering/'
test_path = directory + 'simplified-nq-test.jsonl'
test = build_test(test_path)
submission = pd.read_csv("../input/tensorflow2-question-answering/sample_submission.csv")
test_text, test_questions = compute_text_and_questions(test, tokenizer) 

我们生成实际的预测:

test_target = model.predict([test_text, test_questions], batch_size=512)
test['target'] = test_target
result = (
    test.query('target > 0.3')
    .groupby('example_id')
    .max()
    .reset_index()
    .loc[:, ['example_id', 'PredictionString']]
)
result.head() 

https://github.com/OpenDocCN/freelearn-dl-pt3-zh/raw/master/docs/ml-tf-cb/img/B16254_09_05.png

正如你所看到的,LSTM 使我们能够处理一些相当抽象的任务,比如回答不同类型的问题。这个配方的核心工作是在数据格式化为合适的输入格式,并在之后对结果进行后处理——实际的建模过程与前几章中的方法非常相似。

总结

在本章中,我们展示了 RNN 的不同功能。它们能够在一个统一的框架内处理多种具有顺序性任务(文本生成与分类、时间序列预测以及问答)。在下一章,我们将介绍 transformer:这一重要的架构类别使得在自然语言处理问题上取得了新的最先进成果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值