原文:Machine Learning for Economics and Finance in TensorFlow 2
四、树
基于树的模型已被证明对机器学习中的预测任务非常有用,并且最近已被应用于经济学和金融学中的问题并对其进行了修改。任何基于树的模型的基本单元是决策树,它使用一系列数据分区来解释结果。这种模型可以自然地被可视化为流程图。
虽然 TensorFlow 是为了解决深度学习问题而开发的,但它最近在其高级Estimators
API 中添加了基于树的模型的库。在这一章中,我们将检查这些库,并应用它们来训练基于阿拉斯加州住房抵押贷款披露法案(HMDA)应用数据的树型模型。1
决策树
决策树类似于具有特定数字和分类阈值的流程图,通常使用 Breiman 等人(1984 年)介绍的算法家族构建。在这一节中,我们将在概念层面上介绍决策树,重点是基本定义和训练过程。在这一章的后面,我们将关注在 TensorFlow 中实现决策树。请参见 Athey 和 Imbens (2016,2019)对决策树在经济学中的应用的概述,以及 Moscatelli 等人(2020)对企业违约预测的应用。
概观
决策树由分支和三种类型的节点组成:根、内部节点和叶子。根部是第一个样本分离发生的地方。也就是说,我们用完整的数据样本进入树,然后通过根,它分割样本。每个拆分都与一个分支相关联,该分支将根节点连接到内部节点,并可能连接到离开节点。与根节点非常相似,内部节点强加了一个条件来分割样本。内部节点通过分支连接到额外的内部节点或叶,每个分支又与样本分割相关联。最后,树在叶子节点处终止,叶子节点产生对类别的预测或概率分布。
举个例子,让我们考虑一下 HMDA 的抵押贷款申请数据。我们将构建一个简单的分类器,从抵押贷款申请中提取特征,然后预测它是被接受还是被拒绝。我们将从只有一个特征的树形模型开始:以千美元计的申请人收入。我们的目标只是训练模型,看看它如何分割样本。也就是说,我们想知道什么样的收入水平与接受和拒绝之间的分裂相关,假设我们不以任何其他东西为条件,如抵押贷款的规模或借款人的信用评级。图 4-1 显示了该图表。
图 4-1
使用 HMDA 数据的简单决策树(DT)模型
正如我们将在本章后面讨论的,决策树的一个参数是它的最大深度。我们可以通过计算树根和最远的叶子之间的分枝数来测量树的深度。在这种情况下,我们选择的最大深度为 1。这种树有时被称为“决策树桩”我们的简单模型预测,收入低于 25,500 美元的申请人将被拒绝,而收入高于或等于 25,500 美元的申请人将被接受。当然,这个模型太简单了,对大多数应用程序来说都没有用;然而,它为我们提供了一个起点。
在图 4-2 中,我们进一步扩展了这个练习,将树的最大深度增加到 3,并增加了第二个特征:人口普查区域收入与城市统计收入的比率,乘以 100。请注意,我们在图中使用了“地区收入”来描述这一特征。
图 4-2
根据 HMDA 数据训练的决策树模型,具有两个特征和三个最大深度
再次从根开始,我们可以看到决策树首先根据申请人的收入对样本进行划分。低收入申请者被拒绝。然后,它在剩余的申请人中执行另一个分区。对于低收入家庭,下一个内部节点检查他们居住地区的收入是否低于平均水平。如果是,他们被拒绝,如果不是,他们被接受。同样,对于高收入家庭,该树检查房屋所在区域的收入水平。然而,不管它是高还是低,申请都被接受。
图中还有一些其他值得观察的地方。首先,现在我们有了足够的深度,图表有了“内部节点”——也就是说,不是根或叶的节点。第二,不是所有的叶子对都必须包含“接受”和“拒绝”类。事实上,叶子的类别将取决于与叶子相关联的类别的经验分布。按照惯例,我们可以将具有超过 50%接受观察值的叶子视为“接受”叶子。或者,我们可以改为陈述叶的结果分布,而不是将其与特定的类相关联。
特征工程
术语“特征工程”在本书中不常使用,因为 TensorFlow 主要是为深度学习设计的,深度学习通常会自动执行特征提取。然而,值得指出的是,特征工程对于决策树模型是必要的,因为它们有一个限制性的函数形式。
特别地,决策树是通过执行越来越细粒度的样本分割来构建的。如果关系的函数形式没有被单个特征的阈值捕获,那么基于树的模型将很难发现它。例如,一个要素和因变量之间的线性关系无法通过截距和斜率来捕捉。这将需要一个复杂的阶跃函数,它可能由数百个阈值构成。
一个明显的例子是我们在图 4-1 和 4-2 中给出的 HMDA 例子中申请人收入的使用。虽然获得任何形式的抵押贷款可能需要一定的最低收入水平,但很明显,较低的收入应该允许小额抵押贷款。因此,我们实际上想要的是债务收入比,这是通常用来评估贷款决策的指标。
然而,如果我们不计算债务收入比并将其作为一个特征,决策树将需要许多内部节点来实现我们通过取一个比率能够做到的事情。由于这个原因,决策树仍然依赖于专家判断来通知特征工程过程。
培养
我们现在知道决策树利用递归样本分裂,但我们还没有说样本分裂本身是如何选择的。实际上,决策树算法将通过依次选择生成最低基尼系数杂质或最大“信息增益”的变量和阈值来执行样本分割基尼系数在等式 4-1 中给出。
方程式 4-1。K 类因变量的基尼杂质。
)
基尼系数是通过节点中类的经验分布来计算的。它告诉我们一个阶级在多大程度上控制了分配。作为一个例子,让我们考虑图 4-1 中使用的模型,其中我们对申请人的收入进行了单一样本分割。我们之前没有提到这一点,但在收入低于 25,500 美元门槛的申请人中,接受的概率为 0.656,拒绝的概率为 0.344。这使我们的基尼系数为 0.451。对于收入高于 25500 美元的人,拒绝的比率为 0.075,接受的比率为 0.925,基尼系数为 0.139。 2
请注意,如果分裂完美地将申请人分为拒绝和接受,那么每组的基尼系数将为零。也就是说,我们想要一个低的 Gini 杂质,算法通过执行分割来实现它,分割后每个节点内的异质性将减少。
接下来,我们将考虑信息增益,这是分割质量的另一个常用度量。与基尼系数相似,它衡量的是将样本分成节点后产生的无序程度的变化。为了理解信息增益,有必要首先理解信息熵的概念,我们在方程 4-2 中定义了它。
方程式 4-2。 信息熵 为 K 类情况。
)
让我们回到我们考虑基尼系数的例子。如果我们有一片叶子,接受的经验概率为 0.656,拒绝的经验概率为 0.344,那么信息熵就是 0.929。类似地,对于另一片叶子,接受和拒绝概率为 0.075 和 0.925,它将是 0.384。 3
因为我们的目标是减少数据中的熵,所以我们将使用一个叫做“信息增益”的度量这将测量通过执行样本分割从系统中移除了多少熵。在等式 4-3 中,我们将信息增益定义为父节点的熵与其子节点的加权熵之差。
方程式 4-3。信息增益。
)
在由分支连接的任何节点之间,“子”节点是由分裂产生的“父”节点的子样本。在等式 4-3 中,我们已经计算出两个子节点的熵为 0.929 和 0.384。节点的权重, w k ,是它们各自在总样本中的份额。假设第一片叶子包含 10%的观察值,第二片叶子包含剩下的 90%。这产生了子节点熵的加权和的值 0.4385。
在计算信息增益之前,我们必须先计算父节点的熵。为了便于说明,让我们假设根节点中的观察有 0.25 的概率被拒绝,0.75 的概率被接受。这为父节点产生 0.811 的熵。因此,熵的信息增益或减少是 0.3725(即 0.8110.4385)。
TensorFlow 将允许选择分裂算法的灵活性;然而,我们将推迟讨论 TensorFlow 中的实现细节,直到“随机森林”一节。这是因为 TensorFlow 目前仅支持梯度增强随机森林,这将需要引入额外的概念。
回归树
我们在上一节中讨论过的决策树使用类似流程图的结构来模拟具有分类结果的过程。然而,在大多数经济和金融应用中,我们有一个连续的因变量,这意味着我们不能使用决策树。对于这样的问题,我们可以使用“回归树”,其中“回归”用于机器学习环境中,表示连续的因变量。
回归树在结构上与决策树几乎相同。唯一不同的是树叶。不是将叶与类或类上的概率分布相关联,而是与叶中观察值的因变量的平均值相关联。
我们将遵循 Athey 和 Imbens (2019)中给出的回归树的处理方法,但会将其与 HMDA 数据集联系起来。首先,我们假设我们有一个特征, X i ,和一个连续的因变量, Y i 。对于这个特性,我们将使用以千美元为单位的申请人收入。对于因变量,我们将使用以千美元为单位的贷款规模。如果我们使用误差平方和作为损失函数,我们可以在第一次分裂之前计算根部的损失,如方程 4-4 所示。
方程式 4-4。均方根误差的初始和。
)
也就是说,我们不分割样本,所以所有的观察值都在同一片叶子上。那片叶子的预测值就是因变量的平均值,记为)。
使用 Athey 和 Imbens (2019)的符号,我们将使用 l 表示“左”分支, r 表示分裂的右分支, c 表示阈值。现在,让我们假设我们决定在申请人收入变量的根上执行单个分割。误差平方和可以用公式 4-5 计算。
方程式 4-5。一次分割后的误差平方和。
)
请注意,我们现在有两片叶子,这意味着我们必须计算两个误差平方和,每片叶子一个。从连接到左分支的叶子开始,我们计算叶子中所有观察值的平均值,表示为)。然后,我们对叶子中的每个观察值和叶子平均值之间的平方差求和,并将其加到右边叶子的平方差之和上,以同样的方式计算。
与决策树一样,我们可以根据模型参数的选择,例如最大树深度,对额外的分割重复这个过程。然而,一般来说,我们通常不会孤立地使用回归和决策树。相反,我们将在随机森林的上下文中使用它们,这将在下一节中讨论。
然而,孤立地使用单棵树也有一些好处。一个明显的优势是树的可解释性。在某些情况下,如信用建模,可解释性可能是一个法律要求。Athey 和 Imbens (2019)讨论的使用回归树的另一个好处是它们具有良好的统计特性。该树的输出是平均值,计算它的置信区间相对简单。然而,他们指出,平均值不一定是无偏的,但在 Athey 和 Imbens (2016)中提供了一个程序,使用样本分割来纠正偏差。
随机森林
虽然使用单独的决策树和回归树有一些优势,但在大多数机器学习应用程序中这并不是常见的做法。其原因主要与 Breiman (2001)提出的随机森林的预测功效有关。顾名思义,随机森林由许多树组成,而不仅仅是一棵树。
Athey 和 Imbens (2019)指出了随机森林和回归(或决策树)树之间的两个差异。首先,与回归树不同,随机森林中的单棵树只利用了样本的一部分。也就是说,对于每个单独的树,通过随机抽取固定数量的观察值并进行替换来引导样本。这个过程有时被称为“装袋”第二个是在每个阶段随机选择一组特征用于分割。这不同于回归树,回归树优化模型中的所有特征。
机器学习领域通常发现随机森林具有高度的预测准确性。它们在文献、机器学习竞赛和行业应用中表现出色。Athey 和 Imbens (2019)指出,随机森林通过增加计算平均值的平滑度,也优于回归树。
虽然随机森林几乎完全被用作预测工具,但最近的工作已经展示了它们如何被用于执行假设检验和统计推断。例如,Wager 和 Athey (2017 年)证明了叶级均值(即模型预测)渐近正态和无偏的条件,还展示了如何为模型预测构建置信区间。
图 4-3 说明了随机森林模型的预测过程。在第一步中,将特征集传递给每个单独的决策树或回归树。然后应用一系列阈值,这将取决于树本身的结构。由于在训练过程中——在特征的选择和观察的选择中——存在随机性,所以树不会有相同的结构。
图 4-3
从随机森林模型生成预测
随机森林中的每棵树都会产生一个预测。然后,将使用某个函数对预测进行汇总。在分类树中,通常使用对树的预测的多数投票来确定森林的分类。在回归树中,对树的预测进行平均是一种常见的选择。
最后,同时训练随机森林中的树,并且用于聚集目的的各个树的权重在训练过程本身期间不更新。在下一节中,我们将看看梯度增强树,它以几种方式修改随机森林,最重要的是,它在 TensorFlow 中有一个实现。
梯度增强树
虽然 TensorFlow 没有为回归树、决策树或随机森林提供高级 API,但它确实为训练梯度提升树提供了功能。梯度增强树和随机森林有两个区别,我们强调如下:
-
强与弱学习器:随机森林使用完全生长的树,可能有许多中间节点,而梯度增强使用“弱学习器”:中间节点很少(如果有的话)的浅树。在某些情况下,梯度推进使用“决策树桩”,这只是有一个根和一个单一的分裂。
-
顺序与并行训练:在一个随机森林中,每棵树都被并行训练,并且对树的加权方案不依赖于训练过程。在梯度推进中,每个树都是按顺序训练的,并且在给定先前训练的树的情况下,可以解决模型中的缺陷。
梯度推进过程依赖于经济学家熟悉的技术,即使基于树的模型并不熟悉。为了阐明这种模型是如何构建的,我们将通过一个例子,其中我们使用最小二乘法作为损失函数。我们将从定义一个函数开始,GI(X),在 i 次迭代之后,该函数产生模型目标 Y 的预测。与此相关,我们将定义一个基于树的模型,TI(X),它是在迭代 i 中引入的,是对GI(X)的改进,也是GI+1 方程 4-6 总结了函数之间的关系。
方程式 4-6。梯度增强中 树与预测函数 的关系。
)
由于gI+1(x)是一个从特征中产生预测的模型,它可以用目标变量 Y 和预测误差或残差 ϵ 来表示,如等式 4-7 所示。
方程式 4-7。定义 模型剩余 。
)
)
注意Y—GI(X)在迭代 i 时是固定的。因此,调整树模型的参数tI(x)会影响残差, ϵ 。我们可以通过最小化误差平方和来训练tI(x)ϵ’ϵ。或者,我们可以使用不同的损失函数。一旦TI(X)训练完毕,我们就可以更新预测函数,GI+1(X),然后在另一次迭代中重复这个过程,添加另一棵树。
在每一步,我们将使用前一次迭代的残差作为目标。例如,如果我们的第一棵树在一个有连续目标的问题上有正偏差,那么第二棵树可能会产生负偏差,当与第一棵树结合时,会减少模型偏差。
分类树
让我们看一个在 TensorFlow 中实现梯度推进决策树的例子。我们将利用 HMDA 的数据。由于我们使用决策树,我们将需要一个离散的因变量,并将利用应用程序的结果,它可以是接受或拒绝。
在清单 4-1 中,我们将通过导入pandas
和tensorflow
来开始这个过程。然后,我们将使用pandas
加载 HMDA 数据,并将其分配给pandas
数据帧hmda
。接下来,我们将使用操作feature_column.numeric_column()
定义容器来保存称为特性列的数据。我们将它们命名为匹配它们将包含的变量:applicantIncome
和areaIncome
。然后我们将这两个特性列合并成一个名为feature_list
的列表。
import pandas as pd
import tensorflow as tf
# Define data path.
data_path = '../chapter4/hmda.csv'
# Load hmda data using pandas.
hmda = pd.read_csv(data_path+"hmda.csv")
# Define applicant income feature column.
applicantIncome = tf.feature_column.numeric_column("applicantIncome")
# Define applicant msa relative income.
areaIncome = tf.feature_column.numeric_column("areaIncome")
# Combine features into list.
feature_list = [applicantIncome, areaIncome]
Listing 4-1Prepare data for use in gradient boosted classification trees
清单 4-2 中给出的下一步是为训练数据定义一个输入函数。该函数将返回特征和标签,稍后将传递给train
操作。我们通常希望为培训和评估过程定义单独的功能,但是为了这个例子的目的,我们将尽可能保持事情的简单。
因为我们已经定义了这个函数的最小版本,所以它没有参数。它构建了一个名为features
的字典,其中使用了个人收入和该地区中值收入的变量。然后,它使用从hmda
数据集中接受的应用程序来定义标签。
我们现在可以定义和训练这个模型,我们在清单 4-3 中做了。我们将首先使用高级Estimators
API 中的BoostedTreesClassifier
来定义模型。至少,我们需要提供特征列的列表,feature_columns
,以及样本被分成的批次数量,n_batches_per_layer
。因为数据集足够小,可以在单个批处理中处理,所以我们将第二个参数设置为 1。
# Define boosted trees classifier.
model = tf.estimator.BoostedTreesClassifier(
feature_columns = feature_list,
n_batches_per_layer = 1)
# Train model using 100 epochs.
model.train(input_fn, steps=100)
Listing 4-3Define and train a boosted trees classifier
# Define input data function.
def input_fn():
# Define dictionary of features.
features = {"applicantIncome": hmda['income'],
"areaIncome": hmda['area_income’]}
# Define labels.
labels = hmda['accepted'].copy()
# Return features and labels.
return features, labels
Listing 4-2Define function to generate input data function
最后,我们使用train
操作,以及我们之前定义的输入函数来训练模型。为了简单起见,我们仅将确定训练时期数量的参数steps
设置为 100。
一旦训练过程完成,我们可以应用evaluate
操作,以及我们的输入函数和一些步骤作为参数。我们将使用前面定义的相同输入函数,这意味着我们将评估样本内。虽然这通常不是推荐的做法,但是为了提供一个最小的例子,我们将在这里这样做。清单 4-4 中给出了执行评估和打印结果的代码。
# Evaluate model in-sample.
result = model.evaluate(input_fn, steps = 1)
# Print results.
print(pd.Series(result))
accuracy 0.635245
accuracy_baseline 0.598804
auc 0.665705
auc_precision_recall 0.750070
average_loss 0.632722
label/mean 0.598804
loss 0.632722
precision 0.628028
prediction/mean 0.598917
recall 0.958663
global_step 100.000000
dtype: float64
Listing 4-4Evaluate a boosted trees classifier
我们可以看到,控制台输出包含许多不同的性能指标,包括损失、正确预测的比例(准确性)和曲线下面积(AUC)。我们不会在这里详细讨论这些指标,但是有必要指出它们是由evaluate
操作自动生成的。
回归树
如果我们有一个连续的因变量,那么我们需要使用梯度推进回归树,而不是分类树。大部分代码将是相同的,但有一些变化。举个例子,假设我们现在想预测以千美元为单位的贷款金额,而不是申请结果,但是我们仍然想使用同样的两个特性。
为此,我们只需要修改数据输入函数并定义一个BoostedTreesRegressor
,而不是分类器。这两个步骤如清单 4-5 所示。
# Define input data function.
def input_fn():
features = {"applicantIncome": data['income'],
"msaIncome": data['area_income']}
targets = data['loan_amount'].copy()
return features, targets
# Define model.
model = tf.estimator.BoostedTreesRegressor(
feature_columns = feature_list,
n_batches_per_layer = 1)
Listing 4-5Define and train a boosted trees regressor
由于所有其他步骤都是相同的,我们将跳到打印评估操作的结果,这些结果在清单 4-6 中给出。
# Evaluate model in-sample.
result = model.evaluate(input_fn, steps = 1)
# Print results.
print(pd.Series(result))
average_loss 8217.281250
label/mean 277.759064
loss 8217.281250
prediction/mean 277.463928
global_step 100.000000
dtype: float64
Listing 4-6Evaluate a boosted trees regressor
请注意,我们现在在清单 4-6 中有了一组不同的指标。这是因为我们有一个连续的目标,而不是分类标签。在这种情况下,准确性和 AUC 等衡量标准不再有意义。
模型调整
最后,我们将通过讨论模型调整来结束本章,模型调整是我们调整模型参数以改善训练结果的过程。我们将重点关注梯度增强分类和回归树共有的五个模型参数:
-
树的数量:这是由
n_trees
参数指定的,它决定了在训练过程中将创建多少棵单独的树。默认值为 100,但如果模型对数据拟合不足,则可以增加该值;如果模型对数据拟合过度,则可以减小该值。 -
最大树深:使用
max_depth
参数设置,默认为 6。最大树深测量根和最远的叶子之间的分支数量。梯度增强树通常使用比随机森林或单个决策树更低的值。如果过拟合是一个问题,您可以减少最大树深度。 -
学习率:由于梯度提升树可以使用最小平方损失函数进行训练,因此可以使用随机梯度下降或其变体之一进行优化。因此,我们需要设置一个学习率,默认为 0.1。在难以收敛的应用中,我们可能希望降低
learning_rate
参数,增加历元数。 -
正则化:如果我们担心过拟合,对树应用正则化是有意义的,这会因为树深和节点多而惩罚它们。设置参数
l1_regularization
将惩罚应用于节点的权重的绝对值,而l2_regularization
将惩罚权重的平方。我们也可以使用tree_complexity
参数来惩罚叶子的数量。 -
修剪模式:默认情况下,TensorFlow 中的梯度增强算法不会对树进行修剪。要应用修剪,您必须为
tree_complexity
参数设置一个正值,然后将pruning_mode
设置为pre
或post
。预修剪树更快,因为当达到修剪阈值时,树的生长终止。后期修剪速度较慢,因为它需要我们首先生长树,然后修剪它,但它也可能允许算法发现其他有用的关系,否则它不会识别。
一般来说,当我们应用修剪时,我们主要关心的是减轻过拟合。我们希望训练一个模型,它能很好地预测样本中的数据,但不是通过记忆。调整我们在本节中定义的五个参数的值将有助于我们实现这一目标。
摘要
在本章中,我们介绍了基于树的模型的概念。我们看到了用于分类目的的决策树和用于预测连续目标的回归树。一般来说,树通常不会单独使用,而是组合在随机森林中或使用梯度增强。随机森林使用“完全成长”的树,这些树被并行训练,并通过对单个树的输出进行平均或应用多数投票来生成预测。通过最小化来自前一次迭代的模型残差来顺序训练梯度增强树。该过程可以使用最小平方损失函数,并且可以使用随机梯度下降或其某种变体来训练。
TensorFlow 是围绕深度学习构建的,因此最初不适合训练其他类型的机器学习模型,包括决策树和分类树。随着高级Estimators
API 和 TensorFlow 2 的推出,这种情况发生了变化。TensorFlow 现在为训练和评估梯度提升采油树提供强大的生产质量操作。除此之外,它还提供了各种有用的参数,通过这些参数我们可以调整模型以防止过拟合和欠拟合。一般来说,我们将通过迭代训练、评估和调优步骤来实现这一点。
文献学
艾希、s、G.W .和伊本斯。2016."异质因果效应的递归分割."美国国家科学院学报 27(113):7353–7360。
阿西和 G.W .因本斯。2019.“经济学家应该了解的机器学习方法。” arXiv。
布雷曼,2001 年。“随机森林。”机器学习45(1):5–32。
布雷曼、J .弗里德曼、C.J .斯通和 R.A .奥尔申。1984.“分类和回归树.”(CRC 出版社)。
Moscatelli,m .,F. Parlapiano,S. Narizzano 和 G. Viggiano。2020.“用机器学习进行公司违约预测。”专家系统及其应用 161。
韦杰、s .和 s .阿西。2017."使用随机森林的异质处理效果的估计和推断."美国统计协会杂志 1228–1242。
HMDA 数据集可从消费者金融保护局(CFPB)下载: www.consumerfinance.gov/data-research/hmda/
。它是公开可用的,并提供来自许多抵押贷款机构的数据,包括申请功能和决策。我们使用 2017 年来自阿拉斯加的所有申请数据。
2
通过计算 1-(0.6562 + 0.3442),我们得出基尼系数为 0.451。此外,我们通过计算 1-(0.0752 + 0.9252)得出基尼系数为 0.139。
3
我们计算第一片树叶的信息熵值为(0.656∫log20.656+0.344∫log20.344)第二片树叶的信息熵值为(0.075∫log20.075+0.925∫log20.925)。
五、图像分类
图像分类曾经是一项需要领域专业知识和使用特定问题模型的任务。随着深度学习作为计算机视觉中预测任务的通用建模技术的出现,这种情况发生了很大变化。机器学习文献和图像分类竞赛现在都由深度学习模型主导,这些模型通常不需要领域专业知识,因为这种模型自动识别和提取特征,消除了对特征工程的需要。
虽然学院派经济学家最近开始从机器学习中引入方法,但深度学习在图像分类方面的广泛使用已经落后。经济学中许多涉及图像数据的现有工作都利用了预处理过的夜间亮度值。这样的数据可以用来代理经济变量, 1 衡量不同地理层次的产出增长, 2 评估基础设施投资的影响。 3 关于该文献的概述,参见 Donaldson 和 Storeygard (2016 年)和 Gibson 等人(2020 年)。
图像数据集在经济和金融研究中仍未得到充分利用;然而,最近有一些值得注意的应用。纳伊克等人(2017 年)使用计算机视觉技术来测量邻里视觉外观的变化。然后,他们通过确定哪些社区特征与未来的外观改善有关来测试城市经济学的理论。Borgshulte 等人(2019)使用深度学习来衡量压力事件对首席执行官表观年龄的影响。他们表明,大衰退造成的压力与 CEO 的表观年龄增加大约 1 岁有关。
除了学术工作,计算机视觉应用——特别是那些涉及深度学习的应用——在行业环境中已经变得很常见。此外,由于图像数据集的激增和现成模型的质量,它们可能会在学术界和私营行业获得更多的使用。
在本章中,我们将对图像数据及其在经济和金融中的潜在用途进行概述。我们将专注于深度神经网络的开发,这些网络专门用于对图像进行分类,并在TensorFlow
及其高级 API 中实现,包括Keras
和Estimators
。我们还将讨论使用预训练模型并对其进行微调以提高性能。
图像数据
在讨论方法和模型之前,我们先来定义一下什么是图像。出于我们的目的,图像是像素强度的张量。例如,600x400 尺寸的灰度图像是具有 600 行和 400 列的矩阵。矩阵的每个元素都是 0 到 255 之间的整数值,其中该值对应于它所代表的像素的亮度。例如,值 0 对应于黑色,而值 255 对应于白色。
彩色图像有几种张量表示,但最常见的一种——也是我们在本书中几乎唯一使用的一种——是 3-张量。这种图像是 3-张量,因为它们包含一个矩阵,对于三个不同的颜色通道:红色、绿色和蓝色(RGB)具有相同的维度。每个矩阵保存其各自颜色的像素强度值,如图 5-1 所示。
图 5-1
RGB 图像中的每个像素对应于三维张量中的一个元素。图中标记了四个这样的元件。来源: www.kaggle.com/rhammell/ships-in-satellite-imagery/data
在本章中,我们将使用来自“卫星图像中的船只”数据集的图像,该数据集可在 Kaggle 上下载。 4 它包含 80x80x3 像素的彩色图像,是从较大的图像中提取出来的。如果子图像包含船只,则标记为 1,否则标记为 0。非船舶图像包含各种不同类型的土地覆盖,包括建筑物,植被和水。图 5-2 显示了从该数据集中选择的随机图像。
在经济和金融应用中,我们可以通过多种方式使用船只的卫星图像。在这一章中,我们将使用它们来建立一个分类器。这种分类器可用于对感兴趣位置的船只交通进行计数。随着每日卫星数据可用性的增加,这可用于以比官方统计更高的频率估计贸易流量。
图 5-2
“卫星图像中的船只”数据集中的船只示例
我们将从加载和准备清单 5-1 中的数据开始。第一步是导入相关的模块。这包括作为mpimg
的matplotlib.image
,我们将使用它来加载和操作图像;numpy
为np
将图像转换成张量;和os
,我们将使用它来执行操作系统的各种任务。接下来,我们将listdir()
应用于下载图像所在的目录,这将产生一个文件名列表。
现在我们可以构建每个文件的路径,我们将加载图像,将它们转换成numpy
数组,并将它们存储在两个列表中:一个用于船只图像,另一个用于不包含船只的图像。我们将通过使用 list comprehension 来构建每个图像的路径,并使用每个文件名中的第一个字符来识别相应的图像是否包含一艘船。文件0__20150718_184300_090b__-122.35324421973536_37.772113980272394.g
不包含船,而1__20180708_180908_0f47__-118.15328750044623_33.735783554733885.png
包含。
import matplotlib.image as mpimg
import numpy as np
import os
# Set image directory.
data_path = '../data/chapter5/shipsnet/'
# Generate file list.
images = os.listdir(image_path)
# Create list of ship images.
ships = [np.array(mpimg.imread(image_path+image))
for image in images if image[0] == '1']
# Create list of no-ship images.
noShips = [np.array(mpimg.imread(image_path+image))
for image in images if image[0] == '0']
Listing 5-1Prepare image data for use in TensorFlow
既然我们已经将数据加载到列表中,我们将在清单 5-2 中探索它。我们首先将matplotlib.pyplot
作为plt
导入,我们可以用它来绘制图像。然后我们将打印出ships
中一个物品的形状。这将返回元组(80,80,3),这意味着该图像是一个 3-像素张量。我们也可以通过选择张量中的一个坐标来打印任意像素。最后,我们使用imshow()
函数渲染图像,如图 5-3 所示。
图 5-3
来自数据集的船舶图像
import matplotlib.pyplot as plt
# Print item in list of ships.
print(np.shape(ships[0]))
(80, 80, 3)
# Print pixel intensies in [0,0] position.
print(ships[0][0,0])
[0.47058824 0.47058824 0.43137255]
# Show image of ship.
plt.imshow(ships[0])
Listing 5-2Exploring image data
当我们打印特定像素的颜色通道时,请注意这些值不是 0 到 255 之间的整数。相反,它们是介于 0 和 1 之间的实数。这是因为张量已经通过将所有元素除以 255 进行了归一化。在将图像用作为图像处理任务设计的神经网络模型的输入之前,我们通常需要这样做,因为它们通常需要[0,1]或[–1,1]范围内的输入。
神经网络
在我们介绍 TensorFlow 中为构建和训练图像分类模型而设计的高级 API 之前,我们将首先讨论神经网络,因为我们在本章中考虑的所有模型都是神经网络的某种变体。
图 5-4 显示了一个具有输入层、隐藏层和输出层的神经网络。 5 输入层包含 8 个"节点"或输入特征。这些节点乘以权重,权重由图中的线条表示。在应用乘法步骤之后,使用非线性“激活函数”来转换结果输出这产生了下一层“节点”,它被称为隐藏层,因为它不像输入和输出层那样被观察到。就像输入层一样,我们将隐藏层乘以权重,然后应用激活函数,得到输出层。
图 5-4
具有输入层、隐藏层和输出层的神经网络
请注意,输出图层是一个预测。在二进制分类问题中(即,船或非船),输出可以被解释为图像包含船的概率,并且因此将是 0 和 1 之间的实数。在有连续目标的问题中,输出层将产生实数预测。
与神经网络相比,线性回归模型不应用激活函数,也没有隐藏层。常见的线性回归模型如图 5-5 所示,以供比较。请注意,线性回归的输入层和输出层与神经网络没有什么不同。
图 5-5
线性回归模型
神经网络图和线性回归图之间的另一个相似之处是,边连接两个连续层之间的所有节点。在线性回归中,我们知道只有两层,输入层乘以权重(系数)得到输出层(拟合值)。在神经网络中,每当我们使用所谓的“密集”或“完全连接”层时,我们都会执行类似的操作:也就是说,我们将权重矩阵乘以与节点相关的值。
为了修正一个例子,让我们考虑图 5-4 所示的情况。我们将首先执行一个称为“正向传播”的步骤,这是一个为给定的一组要素计算预测值的过程。从输入层开始,第一个操作将特征 X 0 乘以权重w0。然后我们应用一个激活函数, f (),它产生下一层节点,X1。我们再次乘以下一组权重, w 1 ,并应用另一个激活函数,产生输出, Y 。这显示在等式 5-1 中。
方程式 5-1。具有密集层的神经网络中的前向传播。
)
)
我们也可以通过嵌套函数把它写成一行,就像方程 5-2 那样。
方程式 5-2。简洁表达为 正向传播 。
)
关于 X 0 、 w 0 、 X 1 的形状,哪些一定是真的?如果我们有 N 个观察值,那么X0 的形状将是 Nx8,因为我们有八个特征。这意味着 w 0 必须有八行,因为 w 0 中的行数必须等于 X 0 中的列数才能执行矩阵乘法。再者, X 0 和 w 0 的乘积形状将等于 X 0 中的行数,即 N、和 w 0 中的列数。既然我们知道下一层有四个节点,w0 一定是 8x4。同理,既然X1 是 Nx4, Y 是 Nx1,那么w1 一定是 4x1。
请注意,密集层只是神经网络中使用的一种类型的层。例如,当使用图像分类模型时,我们通常会使用专门的层,比如卷积层。我们将推迟这个讨论,直到我们在本章后面的 TensorFlow 中实现这样的网络。
硬
TensorFlow 2 提供了更紧密的高级 API 集成。例如,Keras 现在是 TensorFlow 的子模块,而以前它是一个独立的模块,允许可选地使用 TensorFlow 作为后端。在本节中,我们将讨论如何使用 TensorFlow 中的 Keras 子模块来定义和训练神经网络。
每当我们在 Keras 中定义一个模型时,我们都可以选择使用两个 API 中的一个:顺序 API 或功能 API。顺序 API 具有简单的语法,但是灵活性有限。函数式 API 非常灵活,但代价是更复杂的语法。我们将从使用顺序 API 定义图 5-4 中的神经网络开始。
顺序 API
图 5-4 中的神经网络由输入层、隐含层和输出层组成。此外,它是使用密集层构建的,如连接层 i 中的每个节点和层 i+1 中的每个节点的边所示。我们将在清单 5-3 中构建这个简单的神经网络,作为 Keras API 的第一个演示。
我们首先将tensorflow
作为tf
导入。接下来,我们使用tf.keras.Sequential()
在 Keras 中定义一个序列模型。一旦我们定义了一个顺序模型,我们就可以通过使用add()
方法来添加层。我们首先使用tf.keras.Input()
添加一个有八个特征列的输入层。接下来我们定义隐藏层,指定它有四个输出节点,如图 5-4 所示。我们还通过使用tf.keras.layers.Dense()
来构造层来指示它是密集的。我们还必须指定一个激活函数,它对输入和权重的乘积进行非线性转换。在本例中,我们使用了一个sigmoid
转换。
最后,我们再次使用add()
方法将另一个密集层添加到模型中,它有一个输出节点,并使用了一个sigmoid
激活函数。作为激活函数的这种选择的结果,模型的输出将是 0 和 1 之间的预测概率。如果我们有一个连续的目标,而不是一个离散的目标,我们可以使用一个linear
激活函数,这将允许线性预测。
import tensorflow as tf
# Define sequential model.
model = tf.keras.Sequential()
# Add input layer.
model.add(tf.keras.Input(shape=(8,)))
# Define hidden layer.
model.add(tf.keras.layers.Dense(4,
activation="sigmoid"))
# Define output layer.
model.add(tf.keras.layers.Dense(1,
activation="sigmoid"))
Listing 5-3Implement a simple neural network in Keras
假设我们要考虑一个更有意义的问题,比如船只的分类。我们需要修改什么?至少,我们必须改变输入层,它的形状是错误的。我们数据集中的图像是 80x80x3 像素。如果我们想将它们用作只有密集图层的网络的输入,我们就必须对图像进行整形。由于有 19,200 个像素(即 80803),我们将需要在输入层中有 19,200 个节点。
在清单 5-1 中,我们加载了图像,将它们转换成numpy
数组,并将它们存储为两个列表ships
和noShips
。在清单 5-4 中,我们将使用列表理解将 80×80×3 张量重塑为 19,200 个元素的向量。我们还将创建一个名为labels
的相应因变量,并将展平的特征堆叠成一个numpy
数组。
在我们可以训练我们的网络之前还有两个步骤。第一种是随机打乱数据,然后将其分成训练样本和测试样本。混洗确保我们在一个序列中没有长的船只或非船只图像簇,这会使使用随机梯度下降(SGD)学习变得困难。此外,分离测试样本是机器学习中的标准做法,用于确保我们不会使用用于训练模型的相同观察来评估模型拟合。这使我们能够识别过拟合何时发生。
import numpy as np
# Reshape list of ship images.
ships = [ship.reshape(19200,) for ship in ships]
# Reshape list of non-ship images.
noShips = [noShip.reshape(19200,) for noShip in
noShips]
# Define class labels.
labels = np.vstack([np.ones((len(ships), 1)),
np.zeros((len(noShips), 1))])
# Stack flattened images into numpy array.
features = np.vstack([ships, noShips])
Listing 5-4Reshape images for use in a neural network with dense layers
在清单 5-5 中,我们将使用sklearn
的model_selection
子模块来处理第一步。在这个模块中,我们将使用train_test_split
,它将允许我们指定标签、特性、应该在测试样本中的观察份额,以及一个随机种子以确保可再现性。默认情况下,参数shuffle
设置为True
,因此我们不需要调整它。
一旦我们的样本被打乱和分割,最后一步是修改网络,以允许在输入层有 19,200 个节点。清单 5-6 显示了修改后的网络架构。请注意,这对于所考虑的问题来说并不理想,但对于理解如何构建、训练和评估神经网络是有帮助的。
from sklearn.model_selection import train_test_split
# Shuffle and split sample.
X_train, X_test, y_train, y_test = \
train_test_split(features, labels,
test_size = 0.20, random_state=0
)
Listing 5-5Shuffle and split data into train and test samples
在我们开始培训过程之前,我们可能希望对我们的模型有一个高层次的概述。我们可以使用summary()
方法做到这一点,如清单 5-7 所示。如输出所示,我们的模型有 76,809 个参数。这可能已经让我们有理由担心模型会过拟合,但我们将看到机器学习为管理这个问题提供了许多策略。
我们还可以看到,大多数参数似乎都位于隐藏层中。这是我们将 19,200 个输入节点乘以权重的地方。这意味着我们需要一个权重矩阵,可以将 Nx19200 矩阵输入转换为 Nx4 矩阵。因此,它的形状必须是 19200x4,也就是 76,800 个参数。其余四个参数称为“偏差”,相当于回归中的常数项。我们将有一个隐藏层中的每个节点。同样,对于输出层,我们需要将一个 Nx4 矩阵转换为一个 Nx1 矩阵,这将需要一个 4x1 权重矩阵和一个偏置项,从而为我们提供五个额外的参数。
从摘要输出中我们可能注意到的另一件事是,参数分为两类:“可训练参数”和“不可训练参数”这是因为 Keras 为我们提供了冻结参数的选项,使它们不可跟踪。我们在这里不使用这个特性,但是我们稍后会回到它。
print(model.summary())
_____________________________________________________
Layer (type) Output Shape Param #
=====================================================
dense (Dense) (None, 4) 76804
_____________________________________________________
dense_1 (Dense) (None, 1) 5
=====================================================
Total params: 76,809
Trainable params: 76,809
Non-trainable params: 0
Listing 5-7Print a model summary in Keras
import tensorflow as tf
# Define sequential model.
model = tf.keras.Sequential()
# Add input layer.
model.add(tf.keras.Input(shape=(19200,)))
# Define hidden layer.
model.add(tf.keras.layers.Dense(4,
activation="sigmoid"))
# Define output layer.
model.add(tf.keras.layers.Dense(1,
activation="sigmoid"))
Listing 5-6Modify
a neural network to fit the input shape
我们现在已经看到了如何在 Keras 中定义一个模型并解释它的架构。下一步是通过指定在训练过程中要计算的损失函数、优化器和指标来“编译”模型。我们通过列出 5-8 ,选择binary_crossentropy
损失、adam
优化器和accuracy
指标(即,正确预测的份额)来做到这一点。
我们现在可以将fit()
方法应用于模型,这将启动训练过程。我们必须指定epochs
和batch_size
的数量。epochs
的数量对应于训练过程应该在整个样本上循环的次数,而batch_size
参数决定了在循环的每个增量中使用的观察值的数量。
# Compile the model.
model.compile(loss='binary_crossentropy',
optimizer="adam", metrics=['accuracy'])
# Train the model.
model.fit(X_train, y_train, epochs=100,
batch_size=32, validation_split = 0.20)
Listing 5-8Compile and train the model in Keras
注意,我们还将可选参数validation_split
设置为 0.20。这将分离出我们样本的额外 20%,该样本不会用于训练模型。在训练过程中,我们将在训练和验证样本中比较模型的指标性能。如果两者开始背离,这告诉我们模型过拟合,我们可能要终止训练过程或调整模型的参数。
在每个时期,模型输出训练样本和验证样本中的损失值和预测精度。根据准确性度量,该模型表现得相当好,正确预测了训练和验证样本中 75%的观察值。因为我们根本没有调整模型,所以我们不必担心我们选择的训练和模型参数会夸大验证样本的准确性。因此,评估测试样本并不是绝对必要的,但是为了便于说明,我们还是会这样做,如清单 5-9 所示。
# Evaluate the model.
model.evaluate(X_test, y_test)
loss: 0.5890 - accuracy: 0.7262
Listing 5-9Evaluate
the model on the test sample
我们可以看到精度稍低,但不足以让我们担心过拟合可能是一个问题。我们还要检查最后一个性能指标,即混淆矩阵。这通过指示我们是将 0 误分类为 1 还是将 1 误分类为 0,提供了对准确性的改进。清单 5-10 提供了计算混淆矩阵的代码。
我们将首先从sklearn.metrics
导入confusion_matrix
。接下来,我们将使用该模型对测试样本标签进行预测。预测是概率,但我们将使用阈值 0.5 来表示模型预测到图像包含一艘船。然后我们将真正的标签y_test
和预测y_pred
传递给confusion_matrix()
。生成的矩阵在行中包含真值,在列中包含预测值。例如,第 0 行第 1 列元素指示哪些观察值是真正的 0,但被归类为 1。
混淆矩阵表明所有的预测都是 0——也就是非船。因此,即使在训练、验证和测试样本中表现良好,我们的模型只是注意到 75%的观察值为 0,然后预测所有的观察值为 0,而不是试图学习数据中的模式。
from sklearn.metrics import confusion_matrix
# Generate predictions.
y_pred = model.predict(X_test)>0.5
# Print confusion matrix.
print(confusion_matrix(y_test, y_pred))
array([[581, 0],
[219, 0]])
Listing 5-10Evaluate the confusion matrix
不幸的是,这是我们在训练神经网络时会遇到的一个常见问题:样本通常是不平衡的。由于获得 75%的分类准确率具有挑战性,该模型将快速收敛于预测最常见的类别,而不是学习有意义的抽象。有两种方法可以避免这个问题。第一种是通过随机去除noShips
的观察值来平衡样本。第二种方法是在损失函数中应用权重,该权重增加了ships
类实例的贡献。我们将采用第二种方法,它在清单 5-11 中实现。
我们将从计算ships
和noShips
类的权重开始。这要求我们为每个类设置一个乘法常数,使得一个类的权重和观察次数的乘积对于所有类都是相同的。在我们的例子中,我们有 1000 艘船和 3000 张非船的图片。船只图像编码为 1,非船只图像编码为 0。如果我们计算y_train
的平均值,这将给出样本中 1 的份额,即 0.25。
我们将设置 0.25 作为noShips
、 cw 、 0 的权重,这将缩小其对损失的贡献。然后,我们可以将船只的重量CW1 设置为 1.0-CW0 或 0.75。因为 0.25 * 3000 = 0.75 * 1000 = 750,所以我们选择的方案是有效的。最后,我们定义了一个字典class_weights
,它使用类(0 或 1)作为键,使用权重作为值。然后我们将它传递给fit()
的class_weight
参数。
# Compute class weights.
cw0 = np.mean(y_train)
cw1 = 1.0 - cw0
class_weights = {0: cw0, 1: cw1}
# Train the model using class weights.
model.fit(X_train, y_train, epochs=100,
class_weight = class_weights,
batch_size=32,
validation_split = 0.20)
Listing 5-11Train the model with class weights
这一次,在训练、验证和测试样本中,模型预测精度提高到 0.87 以上。这足以排除模型只是预测最常见类别的可能性;然而,我们将再次检查混淆矩阵,看看加权方案在解决这个问题上的效果如何。
混淆矩阵如清单 5-12 所示。对角线上的元素显示正确的预测。对角线以外的元素显示不正确的预测。我们可以看到模型似乎不再过度预测 0(非船)。相反,现在大多数分类错误都是针对被错误分类为非船只的船只。
我们现在已经了解了如何使用具有密集层的神经网络来执行图像分类,并解决了我们在训练和评估过程中会遇到的许多常见问题。在接下来的部分中,我们将看到如何应用不同的层并对训练过程进行其他修改,以进一步提高模型性能。
# Generate predictions.
y_pred = model.predict(X_test)>0.5
# Print confusion matrix.
print(confusion_matrix(y_test, y_pred))
[[487 94]
[ 5 214]]
Listing 5-12Evaluate the impact of class weights on the confusion matrix
功能 API
虽然 Keras 中的顺序 API 简化了模型构建,但函数式 API 允许灵活性,但代价是复杂性略有增加。为了了解函数式 API 是如何工作的,让我们从重新定义清单 5-6 中的模型开始,但是使用函数式 API。这在清单 5-13 中给出。
我们将首先通过使用tf.keras.Input()
方法并提供一个形状来定义输入层。接下来,我们定义一个密集层,使用tf.keras.layers.Dense()
。请注意,我们已经将输入层作为参数传递给了它后面的密集层。类似地,我们定义一个输出层,再次使用密集层,并再次将前一层作为参数传递给它。最后一步是通过指定输入和输出层来定义模型。
我们现在有了一个与我们使用顺序 API 指定的模型没有什么不同的模型。我们可以编译它,总结它,用完全相同的方法训练它。
使用函数式 API 的优势可能并不明显,因为我们只是简单地复制了顺序式 API 的功能,但是使用了更多的代码行。为了了解函数式 API 在什么情况下可能有用,考虑这样一种情况,我们有一组额外的输入,我们希望将它们包含在模型中,但希望将它们与图像网络本身隔离开来。
import tensorflow as tf
# Define input layer.
inputs = tf.keras.Input(shape=(19200,))
# Define dense layer.
dense = tf.keras.layers.Dense(4,
activation="sigmoid")(inputs)
# Define output layer.
outputs = tf.keras.layers.Dense(1,
activation="sigmoid")(dense)
# Define model using inputs and outputs.
model = tf.keras.Model(inputs=inputs,
outputs=outputs)
Listing 5-13Define a model in Keras with the functional API
在船只检测示例中,我们可能有关于船只位置的元数据,比如经度和纬度。如果该模型能够了解在不同位置观察船只的可能性,它可以结合从图像中提取的特征来分配分类概率。
使用顺序 API 不可能做到这一点,因为我们只能将层堆叠在彼此之上,而我们的目标是创建两个并行网络,这两个网络在输出节点处或输出节点之上的某个地方连接。清单 5-14 展示了我们如何用函数式 API 做到这一点。我们假设我们有图像输入和元数据输入的 20 个特征,我们将定义两个独立的输入层,img_inputs
和meta_inputs
。然后,我们将这些输入隔离到单独的网络中,因为否则模型将很难确定当它们与 19,200 个像素值混合时如何最好地使用 20 个特征。我们将通过将它们传递给单独的密集层img_dense
和meta_dense
来实现这一点。再次注意,这对于顺序 API 是不可能的,因为我们必须显式定义层之间的连接。
import tensorflow as tf
# Define input layer.
img_inputs = tf.keras.Input(shape=(19200,))
meta_inputs = tf.keras.Input(shape=(20,))
# Define dense layers.
img_dense = tf.keras.layers.Dense(4,
activation="sigmoid")(img_inputs)
meta_dense = tf.keras.layers.Dense(4,
activation="sigmoid")(meta_inputs)
# Concatenate layers.
merged = tf.keras.layers.Concatenate(axis=1)([
img_dense, meta_dense])
# Define output layer.
outputs = tf.keras.layers.Dense(1,
activation="sigmoid")(merged)
# Define model using inputs and outputs.
model = tf.keras.Model(inputs=
[img_inputs, meta_inputs],
outputs=outputs)
Listing 5-14Define a multi-input model in Keras with the functional API
接下来,我们使用tf.keras.layers.Concatenate()
操作来合并两个密集层的输出。这将最初分离的网络重新组合成一个网络,该网络从图像中提取四个特征,从元数据中提取四个特征。这然后被传递到输出层,这允许我们定义完整的模型,现在需要两个输入层的列表。
除了定义多输入模型,我们还可以使用函数式 API 定义多输出模型。例如,我们可能希望训练一个模型来预测它作为输出,而不是使用元数据作为输入。我们可以使用带有图像输入的模型来预测分类标签(船舶或非船舶)和 GPS 坐标。关于在经济学中使用多输入模型的示例,请参见 Grodecka 和 Hull (2019)。
估计量
我们之前在章节 4 中提到过估算器 API。TensorFlow 还提供了使用估算器 API 来训练神经网络并进行预测的可能性。一般来说,如果您在生产环境中工作,并且不需要高度的灵活性,但是需要可靠性并希望最大限度地减少出错的可能性,那么您会希望考虑使用 Estimators API 而不是 Keras。
估计器 API 将允许您使用少量参数来完全指定神经网络的架构。让我们考虑一个深度神经网络分类器的例子。我们将首先定义特征列来包含我们的图像,并将它存储为清单 5-15 中的features_list
。之后,我们将定义输入函数,该函数返回将在训练过程中使用的特征和标签。然后我们将定义一个tf.estimator.DNNClassifier()
的实例,指定特性列和一个隐藏单元数量的列表作为输入。为了便于说明,我们将选择使用四个隐藏层的架构,分别为 256、128、64 和 32 个节点。
请注意,我们特意只为DNNClassifier
设置了所需的参数值。对于其他一切,我们使用默认值。这只是为了演示定义和训练一个有四个隐藏层的DNNClassifier
的简单性。我们也可以使用清单 5-16 中的语法来评估模型。
# Evaluate model in-sample.
result = model.evaluate(input_fn, steps = 1)
Listing 5-16Evaluate deep neural network classifiers using Estimators
# Define numeric feature columns for image.
features_list =
[tf.feature_column.numeric_column("image",
shape=(19200,))]
# Define input function.
def input_fn():
features = {"image": X_train}
return features, y_train
# Define a deep neural network classifier.
model = tf.estimator.DNNClassifier(
feature_columns=features_list,
hidden_units=[256, 128, 64, 32])
# Train the model.
model.train(input_fn, steps=20)
Listing 5-15Define a deep neural network classifier using Estimators
最后,除了我们在这里列出的之外,DNNClassifier
还有其他参数可以调整,以修改模型的架构或训练过程。下面我们介绍其中的六种。
-
班数:默认情况下,班数设置为两个;但是,对于多类问题,我们可以将
n_classes
参数设置为不同的值。 -
权重列:在样本不平衡的情况下,比如我们之前考虑的情况,有必要指定一个权重列,以便在损失函数中对类进行适当的加权。
DNNClassifier
通过weight_column
参数对此进行处理。 -
优化器:默认情况下,
DNNClassifier
使用Adagrad
优化器。如果您想使用不同的优化器,您可以使用优化器parameter
来指定它。 -
激活功能 :
DNNClassifier
对所有层应用相同的激活功能。默认情况下,它将使用整流线性单元(ReLU)激活;然而,您可以通过activation_fn
参数提供一个替代值,比如tf.nn.sigmoid
。 -
Dropout :在有大量参数的模型中,可以使用 Dropout 来防止过拟合。通过
dropout
参数设置一个 0 到 1 之间的数字。这是模型中给定节点在训练过程中被忽略的概率。常见的选择范围在 0.10 到 0.50 之间。默认情况下,不应用辍学。 -
批量标准化:对于很多应用来说,批量标准化减少了训练时间。它通过标准化每个小批量内观察值的平均值和方差来发挥作用。您可以通过将
batch_norm
设置为True
来使用批量标准化。
除了tf.estimator.DNNClassifier()
,估值器 API 还有一个针对连续目标的深度神经网络模型tf.estimator.DNNRegressor()
。它也有专门的模型,如深度网络(Cheng 等人(2016 年)介绍的,并在 Grodecka 和 Hull (2019 年)的经济学中应用)结合了线性模型和深度神经网络,前者可用于合并一次性编码变量,如固定效应,后者可用于连续特征。这些可作为tf.estimator.DNNLinearCombinedClassifier()
和tf.estimator.DNNLinearCombinedRegressor()
。
卷积神经网络
我们从训练一个具有密集层的神经网络来执行图像分类开始这一章。虽然这种方法没有错,但它通常会被替代的神经网络架构所主导。例如,具有卷积层的网络通常会提高精度并减小模型尺寸。在本节中,我们将介绍卷积神经网络(CNN ),并使用它来训练图像分类器。
卷积层
卷积神经网络(CNN)利用卷积层,卷积层被设计成处理图像数据。图 5-6 展示了这些层是如何工作的。为了简单起见,我们假设我们正在处理一个 4x4 像素的灰度图像,在图中显示为粉红色。卷积层将应用滤波器,如蓝色所示,方法是执行滤波器和图像片段的逐元素乘法,然后对结果矩阵的元素求和。在这种情况下,滤镜为 2x2,首先应用于图像的红色部分,产生标量值 0.7。然后,滤镜向右移动,应用于图像的下一个 2x2 片段,产生 0 值。对图像的所有 2×2 片段重复该过程,产生以黄色显示的 3×3 矩阵。
图 5-7 展示了卷积层如何适合 CNN。 6 第一层是输入层,接受形状(64,64,3)的彩色图像张量。接下来,应用具有 16 个滤波器的卷积层。请注意,每个滤镜都跨颜色通道应用,产生 64x64x16 的输出。除了执行图 5-7 所示的乘法步骤,该层还对输出的每个元素应用激活函数,使形状保持不变。注意,从该层中的操作产生的 16 个 64×64 矩阵中的每一个都被称为“特征图”
图 5-6
应用于 4x4 图像的 2x2 卷积滤波器
卷积层的输出然后被传递到“最大池”层。这是一种输出一组元素的最大值的过滤器。在这种情况下,它将从每个要素地图的每个 2x2 块中获取最大元素。我们将使用“步幅”2,这意味着在每次应用之后,我们将最大池过滤器向右(或向下)移动两个元素。该图层将输入尺寸为 64x64x16,并将其缩减为 32x32x16。
图 5-7
卷积神经网络的最小例子
接下来,我们将 32x32x16 最大池层输出展平为 323216x1 (16384,1)矢量,并将其传递给 128x1 密集层,其功能与我们在本章前面描述的一样。最后,我们将密集层输出传递到一个输出节点,这将产生一个预测的类概率。
训练卷积神经网络
在本章的前面,我们介绍了船只和非船只图像的数据集。然后,我们从密集层中构建了一个神经网络,并使用数据集来训练一个船舶分类器。正如我们已经注意到的,使用密集神经网络来训练图像分类器是低效的,因为它没有利用图像的结构,包括像素值和特征位置的空间相关性。
在这一小节中,我们将使用 TensorFlow 中的高级 Keras API 来为相同的分类问题定义一个 CNN。正如我们将看到的,这大大提高了效率。不仅模型参数的数量会下降,而且模型的精度实际上会提高。
清单 5-17 定义了一个卷积神经网络,其架构是为解决我们的问题而设计的。像往常一样,我们首先使用tf.keras.Sequential()
定义一个序列模型。接下来,我们添加输入层,它接受 shape (80,80,3)的图像,并应用卷积层,该层有 8 个滤波器,其kernel_size
为 3(即 3×3)。我们还指定该层应该对其输出的每个元素应用一个relu
激活函数。一个relu
激活简单地应用函数max(0,x)
,该函数为特征图的值设定阈值。
第二层也是卷积层。它有 4 个过滤器,一个kernel_size
为 3,一个relu
激活功能。最终的隐藏层通过将卷积层的特征映射输出展平成矢量来转换它们。展平允许我们将要素地图传递到输出图层,这是一个密集的图层,需要矢量输入。注意,像往常一样,我们在输出层使用一个sigmoid
激活函数,因为我们用两个类执行分类。
import tensorflow as tf
# Define sequential model.
model = tf.keras.Sequential()
# Add first convolutional layer.
model.add(tf.keras.layers.Conv2D(8,
kernel_size=3, activation="relu",
input_shape=(80,80,3)))
# Add second convolutional layer.
model.add(tf.keras.layers.Conv2D(4,
kernel_size=3, activation="relu"))
# Flatten feature maps.
model.add(tf.keras.layers.Flatten())
# Define output layer.
model.add(tf.keras.layers.Dense(1,
activation='sigmoid'))
Listing 5-17Define a convolutional neural network
接下来,我们将使用summary()
方法来查看模型的架构。这将帮助我们通过利用输入是图像的事实来确定我们能够在多大程度上减少模型大小。这显示在清单 5-18 中。请注意,参数的数量从 75,000 个减少到了 23,621 个。此外,在这 23,621 个参数中,有 23,105 个位于密集层中。卷积层总共只有 516 个参数。这表明,我们可以通过从密集层转移到卷积层来大幅提高效率。
在训练模型之前,我们必须准备数据。这一次,我们将利用图像本身作为输入,而不是像我们对密集神经网络所做的那样展平图像。清单 5-19 加载、准备数据,并将数据分成训练和测试样本。它还计算类权重。注意清单 5-18 从清单 5-1 的末端开始。
# Print summary of model architecture.
print(model.summary())
_____________________________________________________
Layer (type) Output Shape Param #
=====================================================
conv2d_9 (Conv2D) (None, 78, 78, 8) 224
_____________________________________________________
conv2d_10 (Conv2D) (None, 76, 76, 4) 292
_____________________________________________________
flatten_3 (Flatten) (None, 23104) 0
_____________________________________________________
dense_3 (Dense) (None, 1) 23105
=====================================================
Total params: 23,621
Trainable params: 23,621
Non-trainable params: 0
_____________________________________________________
Listing 5-18Summarize the model architecture
既然已经加载和准备了数据,并且定义了模型,接下来的步骤就是编译和训练它。清单 5-20 展示了这个过程,以及评估步骤,在这里我们编译测试数据集上的模型预测的准确性。
在仅仅 25 个纪元中,CNN 模型实现了 0.96 的训练准确度和 0.95 的验证准确度。此外,当我们使用测试数据集评估模型时,我们再次发现精确度为 0.95。尽管该网络比完全由致密层构建的网络具有更少的参数,但我们能够在更少的训练时期内实现更高的精度,因为我们利用了图像的结构。
# Compile the model.
model.compile(loss='binary_crossentropy',
optimizer='adam', metrics=['accuracy'])
# Train the model using class weights.
model.fit(X_train, y_train, epochs = 10,
class_weight = class_weights,
batch_size = 32,
validation_split = 0.20)
# Evaluate model.
model.evaluate(X_test, y_test)
Listing 5-20Train and evaluate the model
# Define class labels.
labels = np.vstack([np.ones((len(ships), 1)),
np.zeros((len(noShips), 1))])
# Stack flattened images into numpy array.
features = np.vstack([ships, noShips])
# Shuffle and split sample.
X_train, X_test, y_train, y_test = \
train_test_split(features, labels,
test_size = 0.20, random_state=0
)
# Compute class weights.
w0 = np.mean(y_train)
w1 = 1.0 - w0
class_weights = {0: w0, 1: w1}
Listing 5-19Prepare image data for training in a CNN
预训练模型
在许多情况下,没有足够的图像数据来使用最先进的架构训练 CNN。幸运的是,这很少是必要的,因为 CNN 的“卷积基础”——即卷积层和池层——从图像中提取一般特征,并经常可以重新用于各种模型,包括使用不同类别的模型。
一般来说,我们将使用预训练模型来执行两项任务:特征提取和微调。特征提取需要使用模型的卷积层来识别图像的一般特征,然后将图像输入到密集层,并在图像数据集上进行训练。当您想要使用与原始模型不同的一组类来训练模型时,通常会使用此选项。训练完分类器后,您可以选择执行“微调”,这包括以低学习速率训练整个模型,包括卷积基。这将稍微修改模型的视觉过滤器,以更好地与您的分类任务保持一致。
不需要在数据集上完全训练模型的一个好处是,您可以使用更复杂的体系结构,包括最先进的模型,如 ResNet、Xception、DenseNet 和 EfficientNet。除此之外,您将能够利用在大型数据集(如 ImageNet)上训练的最先进的通用视觉过滤器,而不是使用在少量图像上训练的卷积层。
特征抽出
我们将从检查预训练模型作为特征提取器的使用开始。第一步是加载一个预训练模型,我们可以使用 Keras 或 TensorFlow Hub 的applications
子模块来完成。出于这个例子的目的,我们将使用 Keras。
在清单 5-21 中,我们将使用 TensorFlow 定义一个ResNet50
模型,将weights
参数设置为imagenet
。这将加载ResNet50
模型架构,以及来自使用 ImageNet 数据集训练的模型版本的一组权重。我们还将为include_top
参数指定False
,这将移除用于执行分类的最终密集层。我们不需要这个,因为我们没有使用 ImageNet 类。
# Load model.
model = tf.keras.applications.resnet50.ResNet50(
weights='imagenet',
include_top=False
)
Listing 5-21Load a pretrained model using Keras applications
一旦模型被加载,我们可以应用summary()
方法来探索它的架构。这样做,你会注意到两件事。第一,层次多,参数 2500 多万。此外,几乎所有这些参数都属于“可训练参数”类别,这意味着如果您编译模型并应用fit()
方法,它们将被训练。第二,有些层可能不熟悉。
接下来,我们需要将卷积基设置为不可训练的,这是我们已经加载的模型的一部分,我们在清单 5-22 中做了这些。这将确保我们仅训练分类头,并且模型的其余部分仅用于从输入图像中提取特征。在这之后,我们将定义一个输入层,我们将把它传递给模型,将training
参数设置为False
,因为这不是一个具有可训练参数的层。该模型现在将能够接受形状(80,80,3)的图像张量并输出一组特征图。
因为我们希望模型产生预测的类概率,而不是特征图,所以我们需要将特征图重塑为向量。我们可以使用全局平均池层来实现这一点,这类似于我们前面描述的最大池操作,但是计算的是平均值,而不是最大值。我们现在可以定义一个密集输出图层和一个接受输入影像并输出类别概率的功能模型。最后,我们将编译并拟合模型。
# Set convolutional base to be untrainable.
model.trainable = False
# Define input layer.
inputs = tf.keras.Input(shape=(80, 80, 3))
x = model(inputs, training=False)
# Define pooling and output layers, and model.
x = tf.keras.layers.GlobalAveragePooling2D()(x)
outputs = tf.keras.layers.Dense(1)(x)
model = tf.keras.Model(inputs, outputs)
# Compile and train the model.
model.compile(loss='binary_crossentropy', optimizer="adam",
metrics=['accuracy'])
model.fit(X_train, y_train, epochs = 10,
class_weight = class_weights,
batch_size = 32,
validation_split = 0.20)
Listing 5-22Train the classification head of a pretrained model in Keras
正如我们对卷积基所做的那样,我们可以将summary()
方法应用于完整模型。如果我们这样做,我们会看到总参数是相似的,但可训练参数的数量从 25,000,000 下降到略高于 2000。这将使得在没有大量要训练的图像的情况下训练高度精确的分类器变得可行。它还可以通过显著减小可训练模型的大小来防止过拟合。
模型微调
最后一个可选步骤是执行模型微调。微调的目的是对卷积过滤器进行细微的调整,以便它们捕捉与您的分类问题更相关的特征。这一步相对简单,包括将卷积基设置为可训练的,重新编译模型,然后以较低的学习速率进行训练,如清单 5-23 所示。
如果您最后一次对模型应用summary()
方法,您会注意到它现在有超过 23,000,000 个可训练参数。由于这个原因,我们必须在低学习率上进行训练,以通过对预训练的卷积滤波器进行实质性的改变来防止模型过拟合。请注意,这种修改还会降低预训练参数中嵌入的信息的质量或“不学习”这些信息。
# Set convolutional base to be untrainable.
model.trainable = True
# Compile model with a low learning rate.
model.compile(loss='binary_crossentropy',
optimizer=tf.keras.optimizers.Adam(
learning_rate=1e-5),
metrics=['accuracy'])
# Perform fine-tuning.
model.fit(X_train, y_train, epochs = 10,
class_weight = class_weights)
Listing 5-23Fine-tune a pretrained model in Keras
摘要
虽然计算机视觉曾经需要使用复杂的模型和领域知识,但现在可以使用标准架构的卷积神经网络来执行。此外,我们已经达到了这样一个点,即卷积神经网络往往优于依赖特征工程的模型,这使得它足以掌握 CNN 来完成大多数任务。
图像分类的使用在学术经济学和金融学中仍未得到充分开发,但在工业中的经济应用中已经获得了更广泛的使用。在本章中,我们给出了一个使用卫星图像来识别船只的例子,这可以用于高频率地测量港口的船只流量。同样的方法也可以用于测量高速公路上的交通流量,计算停在商场的汽车数量,测量建筑施工的速度,或者识别土地覆盖的变化。
在这一章中,我们演示了如何使用密集层构建神经网络,它可用于各种不同的回归和分类任务。我们还讨论了如何定义和训练卷积神经网络,它利用了利用图像属性的特殊层。我们看到,相对于只有致密层的模型,这导致了所需参数数量的显著减少。
最后,我们讨论了如何加载预训练模型,并在我们的分类问题中使用它们。我们使用 ResNet50 模型,该模型使用 ImageNet 数据进行预处理,以从船只图像中提取特征。然后,我们使用这些特征来训练密集的分类器层。最后,我们还展示了如何通过以较低的学习速率训练卷积层来微调整个网络。
文献学
艾迪森博士和 b .斯图尔特。2015."夜灯再访:使用夜灯数据作为经济变量的代理."世界银行政策研究工作文件第 7496 号。
Bickenbach,E. Bode,P. Nunnenkamp 和 M. Sö der。2016.“夜灯和地区 GDP。”世界经济评论152(2):425–447。
布鲁姆和 m .克劳斯。2018.“顶灯-明亮的城市及其对经济发展的贡献.” CESifo 第 7411 号工作文件。
Borgshulte,m .,M. Guenzel,C. Liu 和 U. Malmendier。2019."首席执行官的压力和预期寿命:公司治理和财务困境的作用."工作文件。
陈,x 和 w .诺德豪斯。2011."使用光度数据作为经济统计的代理."美国国家科学院学报 108(21):8589–8594。
程海涛等,2016。“推荐系统的广度和深度学习.” arXiv。
唐纳森博士和斯托伊加德博士。2016.“俯视图:卫星数据在经济学中的应用.”《经济透视杂志》30(4):171–198。
吉布森,j . s .奥利维亚和 g .博-吉布森。2020."经济学中的夜灯:来源和用途."CSAE 工作文件系列 2020-01 (牛津大学非洲经济研究中心)。
Goldblatt,r .,K. Heilmann 和 Y. Vaizman。2019.“中分辨率卫星图像能测量小地理区域的经济活动吗?来自越南陆地卫星的证据。”《世界银行经济评论》即将出版。
格罗德卡和我赫尔。2019."地方税和公共服务对房地产价值的影响."瑞典中央银行第 374 号工作文件系列。
亨德森,v . a . storey gard 和 D. Weil。2012.“从外太空测量经济增长”美国经济评论102(2):994–1028。
LeNail,A. 2019。" NN-SVG:出版就绪的神经网络架构示意图."开源软件杂志 4 (33)。
米特尼克、O.A、p .亚涅斯-帕甘斯和 r .桑切斯。2018.“光明投资:使用海地光度数据测量交通基础设施的影响。 IZA 讨论文件第 12018 号。
n .纳伊克、S.D .科米纳斯、r .拉斯卡尔、E.L .格莱泽和 C.A .伊达尔戈 2017."计算机视觉揭示城市变化预测."美国国家科学院学报 114(29):7571–7576。
诺德豪斯,w .和 x .陈。2015.“更清晰的图像?作为经济统计指标的夜间灯光精确度的估计经济地理杂志15(1):217–246。
参见陈和诺德豪斯(2011),诺德豪斯和陈(2015),以及爱迪生和斯图尔特(2015)。
2
参见 Henderson 等人(2012 年)、Bluhm 和 Krause (2018 年)、Bickenbach 等人(2016 年)和 Goldblatt 等人(2019 年)。
3
见 Mitnik 等人(2018 年)。
4
数据集可在 Kaggle: www.kaggle.com/rhammell/ships-in-satellite-imagery/data
下载。它包含一个带有元数据的 JSON 文件,包括标签,以及一个包含船只和非船只图像的文件夹。
5
该图由 LeNail (2019)生成,并由作者修改。要使用该工具,请参见 https://doi.org/10.21105/joss.00747
。
6
该图由 LeNail (2019)生成,并由作者修改。要使用该工具,请参见 https://doi.org/10.21105/joss.00747
。
六、文本数据
经济和金融学科通常不愿意集成各种形式的非结构化数据。一个例外是文本,它已被应用于各种各样的经验问题。这在一定程度上可能是早期经济学成功应用的结果,如 Romer 和 Romer (2004 年)证明了衡量中央银行内部叙事的经验性价值。
文本被更广泛地采用也可能是因为它在经济学和金融学中的许多自然应用。例如,它可以用于提取潜在变量,如报纸中的经济政策不确定性,社交媒体内容中的 1 消费者通胀预期(Angelico 等人,2018 年),以及公告和文件中的央行和私营企业情绪。 2 它还可以用来预测银行困境(Cerchiello et al. 2017),衡量新闻媒体对商业周期的影响(Chahrour et al. 2019),识别消费者金融投诉中欺诈的描述(Bertsch et al. 2020),分析金融稳定性(Born et al . 2013Correa 等人 2020),预测经济变量(Hollrah 等人 2018;卡拉马拉等。al 2020),并研究央行决策。 3
当罗伯特·希勒(Robert Shiller)在美国经济协会(American Economic Association)发表题为“叙事经济学”(Shiller 2017)的主席演讲时,经济学中对文本数据的关注再次受到重视。他认为,经济学和金融学的学术工作未能解释流行叙事的兴衰,流行叙事有能力推动宏观经济和金融波动,即使叙事本身是错误的。然后,他建议该学科应该通过探索基于文本的数据集和方法来开始纠正这一缺陷的长期项目。
本章将讨论如何在经济学和金融学的背景下准备和应用文本。自始至终,我们将使用 TensorFlow 进行建模,但也将利用自然语言工具包(NLTK)来预处理数据。我们还将经常参考和使用 Gentzkow 等人(2019)的约定,该约定提供了经济学和金融学中许多文本分析主题的全面概述。
数据清理和准备
任何文本分析项目的第一步都是清理和准备数据。例如,如果我们想利用报纸上关于一家公司的文章来预测其股票市场的表现,我们首先需要收集报纸文章,然后将这些文章中的文本转换成数字格式。
我们将文本转换成数字的方式将决定我们可以执行什么类型的分析。因此,数据清理和准备步骤将是任何此类项目流程中的重要部分。我们将在这一小节中讨论它,重点是使用自然语言工具包(NLTK)实现它。
我们将从安装 NLTK 开始。然后我们将导入它并下载它的模型和数据集。您可以使用nltk.download('book')
下载与书籍相关的数据,nltk.download('popular')
下载最流行的包,或者nltk.download('all')
下载所有可用的数据集和模型,这就是我们在清单 6-1 中所做的。
# Install nltk.
!pip install nltk
# Import nltk.
import nltk
# Download all datasets and models
nltk.download('all')
Listing 6-1Install, import, and prepare NLTK
既然我们已经安装了 NLTK 并下载了所有的数据集和模型,我们就可以利用它的基本数据清理和准备工具了。但是,在我们这样做之前,我们需要准备一个数据集并引入一些符号。
收集数据
我们将使用的数据来自美国证券交易委员会(SEC)的文件,可以通过他们的在线系统 EDGAR 获得。4EDGAR 界面,如图 6-1 所示,允许用户进行各种查询。我们将首先打开公司备案界面。在这里,我们可以按公司名称搜索文档,或者指定搜索参数,返回符合该标准的所有公司的文档。假设我们想要创建一个项目来监控 SEC 关于金属采矿业的文档。在这种情况下,我们将通过标准行业分类(SIC)代码进行搜索。
图 6-1
EDGAR 公司档案搜索界面。资料来源:SEC.gov
调出 SEC 的 SIC 代码列表,我们可以看到金属矿业被分配了代码 1000,归能源和运输办公室负责,如图 6-2 所示。我们现在可以搜索具有 1000 SIC 代码的公司的所有申请,产生图 6-3 中给出的结果。每页列出公司、与申报相关的州或国家,以及中央索引键(CIK),可用于识别申报的个人或公司。
在我们的例子中,我们将选择“Americas Gold and Silver Corp .”的归档,您可以通过在 CIK 字段中搜索 0001286973 来找到它。从那里,我们将看看 2020-05-15 的 6k 财务文件中的图表 99.1 的文本。我们在图 6-4 中显示了该申请的标题和一些文本。
图 6-2
SIC 分类代码的部分列表。资料来源:SEC.gov
正如我们在图 6-4 中看到的,该文件对应于 2020 年第一季度,似乎包含了对评估其价值有用的公司信息。例如,我们可以看到关于公司收购的信息。它还讨论了特定地点的采矿生产计划。现在我们知道了如何从 EDGAR 系统中检索归档信息,并确定了感兴趣的特定归档,我们将引入符号来描述这些文本信息。然后,我们将回到 NLTK 中的清理和准备任务。
图 6-4
一家金属矿业公司的部分 6k 财务档案。资料来源:SEC.gov
图 6-3
金属矿业公司搜索结果的部分列表。资料来源:SEC.gov
文本数据符号
我们将使用的符号遵循 Gentzkow 等人(2019)。我们将让 D 来表示一组 N 文档或“语料库” C 将表示一个数字数组,它包含对每个文档的 K 特征的观察值,Dj∈D。在某些情况下,我们将使用 C 来预测结果 V ,或者在两步随机推理问题中使用拟合值)。
在我们应用 NLTK 清理和准备数据之前,我们必须回答以下两个问题:
-
什么是 D ?
-
D 的哪些特点要体现在 C 中?
如果我们只处理一份 6-K 文件,那么 D j 可能是该文件中的一个段落或句子。或者,如果我们有许多 6-K 备案,那么 D j 很可能代表单个备案。为了固定一个例子,我们假设 D 是单个 6k 填充中的句子集合——也就是我们前面讨论的那个。
那么,什么是 C ?这取决于我们希望从申请的每个句子中提取的特征或“记号”。在许多情况下,我们将使用字数作为特征;在这个例子中我们也会这样做。等式 6-1 给出了 C 的表达式,通常称为“文档-特征”或“文档-术语”矩阵。
方程式 6-1。 文档-特征矩阵 。
)
每个元素, c ij ,是单词 j 在句子 i 中出现的频率。我们可能会问的一个自然问题是哪些单词包含在矩阵中?我们应该在给定的词典中包含所有的单词吗?还是应该限制在语料库中至少出现一次的词?
数据准备
在实践中,我们将根据一些过滤标准选择最大数量的单词, K 。除此之外,在清理和数据准备过程中,我们通常还会删除所有非文字符号,如数字和标点符号。这通常由四个步骤组成,我们概括如下,然后在一个使用 NLTK 的示例中实现:
-
转换成小写:文本数据本来就是高维的,这将迫使我们尽可能地使用降维策略。我们可以这样做的一个简单方法是忽略大写。我们不是将“gold”和“Gold”作为单独的特征,而是将所有字符转换为小写,并将其视为同一个单词。
-
去掉停用词和生僻字:很多词不包含有意义的内容,比如冠词、连词、介词等。出于这个原因,我们通常会编制一个“停用词”列表,在清理过程中会从文本中删除这些停用词。如果我们的 C 矩阵由字数组成,那么知道单词“the”和“and”被使用了多少次并不能告诉我们多少关于我们感兴趣的主题的信息。类似地,当我们从文档术语矩阵中排除单词时,我们通常会排除罕见的单词,这些单词出现的频率不足以让模型辨别它们的含义。
-
词干化或词汇化:进一步降低数据维度的需求通常会导致我们执行“词干化”或“词汇化”词干需要把一个单词转换成它的词干。也就是说,我们可以将动词“running”映射为“run”由于许多单词将映射到同一个词干,这将减少问题的维数,就像转换成小写字母一样。删除词干可能会导致非单词,当项目的目标是产生可解释的输出时,这可能是不可取的。在这种情况下,我们将考虑使用词汇化,它将许多单词映射到一个单词,但使用单词的“基本”或“词典”版本,而不是词干。
-
去除 非文字元素:我们在经济和金融中遇到的大多数问题,都不可能使用标点符号、数字和特殊字符和符号。为此,我们将丢弃它们,而不是将它们包含在文档术语矩阵中。
我们现在将逐步完成 NLTK 中的这些清理和准备步骤。为了完整起见,我们将使用清单 6-2 中的urllib
和BeautifulSoup
从 SEC 网站下载 6-K 文件。理解这些库对于理解本章的其余部分并不是必要的。
from urllib.request import urlopen
from bs4 import BeautifulSoup
# Define url string.
url = 'https://www.sec.gov/Archives/edgar/data/1286973/000156459020025868/d934487dex991.htm'
# Send GET request.
html = urlopen(url)
# Parse HTML tree.
soup = BeautifulSoup(html.read())
# Identify all paragraphs.
paragraphs = soup.findAll('p')
# Create list of the text attributes of paragraphs.
paragraphs = [p.text for p in paragraphs]
Listing 6-2Download HTML and extract text
为了简单说明清单 6-2 的内容,我们先导入两个子模块:urllib.request
中的urlopen
和bs4
中的BeautifulSoup
。urlopen
子模块允许我们发送 GET 请求,这是从服务器请求文件的一种方式。在这种情况下,我们请求位于指定的url
的 HTML 文档。然后我们使用BeautifulSoup
从 HTML 创建一个解析树,这样我们就可以利用它的结构,通过标签来搜索它。接下来,我们搜索“p”或段落标记的所有实例。使用 list comprehension,我们将遍历每个实例,返回它的text
属性,我们将把它收集到一个字符串列表中。
回想一下,我们决定使用句子,而不是段落,作为我们的分析单位。这意味着我们需要将段落连接成一个字符串,然后决定如何识别该字符串中的句子。我们将从合并和打印清单 6-3 中的段落开始。
# Join paragraphs into single string.
corpus = " ".join(paragraphs)
# Print contents.
print(corpus)
Darren Blasutti VP, Corporate Development & Communications President and CEO Americas Gold and Silver Corporation Americas Gold and Silver Corporation 416-874-1708 Cautionary Statement on Forward-Looking Information: This news release contains "forward-looking information" within\n the meaning of applicable securities laws. Forward-looking information includes,\n ...
Listing 6-3Join paragraphs into single string
在打印文集时,我们可以看到它需要清理。它包含标点符号、停用词、换行符和特殊字符,所有这些都需要在计算文档特征矩阵之前删除。现在,我们可能想从清理步骤开始,但这样做会删除文本中构成句子的指示符。出于这个原因,我们将首先把文本分成句子。
虽然我们可以编写一个函数来基于标点符号的位置执行拆分,但这是自然语言处理中已经解决的问题,并且在 NLTK 工具箱中实现。在清单 6-4 中,我们导入 NLTK,实例化一个“句子标记器”,它将一个文本分割成单独的句子,然后将它应用于我们在上一步中构建的语料库。
import nltk
# Instantiate sentence tokenizer.
sentTokenizer = nltk.sent_tokenize
# Identify sentences.
sentences = sentTokenizer(corpus)
# Print the number of sentences.
print(len(sentences))
50
# Print a sentence.
print(sentences[7])
The Company continues to target commercial production by late Q2-2020 or early Q3-2020 and will be providing more regular updates regarding the operation between now and then.
Listing 6-4Tokenize text into sentences using NLTK
下一步是执行前面讨论的清理任务。尽管为此目的定义一个函数通常是有意义的,但为了清楚起见,我们将把它分成三个步骤。我们首先将所有字符转换成小写,并删除清单 6-5 中的停用词。现在,我们将在语料库中留下生僻字。
from nltk.corpus import stopwords
# Convert all characters to lowercase.
sentences = [s.lower() for s in sentences]
# Define stop words as a set.
stops = set(stopwords.words('english'))
# Instantiate word tokenizer.
wordTokenizer = nltk.word_tokenize
# Divide corpus into list of lists.
words = [wordTokenizer(s) for s in sentences]
# Remove stop words.
for j in range(len(words)):
words[j] = [w for w in words[j] if
w not in stops]
# Print first five words in first sentence.
print(words[0][:5])
['americas', 'gold', 'silver', 'corporation', 'reports']
Listing 6-5Convert characters to lowercase and remove stop words
在下一步中,我们将应用词干分析器,通过将每个单词折叠到词干中来减少数据集的维数。在清单 6-6 中,我们导入了 Porter stemmer (Porter 1980),实例化它,然后将其应用于每个句子中的每个单词。我们再次打印第一句话中的前五个单词。我们可以看到词干分析器将“corporate”映射到“corpor”,将“reports”映射到“report”。回想一下,词干并不总是单词。
from nltk.stem.porter import PorterStemmer
# Instantiate Porter stemmer.
stemmer = PorterStemmer()
# Apply Porter stemmer.
for j in range(len(words)):
words[j] = [stemmer.stem(w) for w in words[j]]
# Print first five words in first sentence.
print(words[0][:5])
['america', 'gold', 'silver', 'corpor', 'report']
Listing 6-6Replace words with their stems
清理过程的最后一步是删除特殊字符、标点符号和数字。我们将使用正则表达式来做到这一点,它通常被称为“正则表达式”正则表达式是一个短字符串,它对文本中可以识别的模式进行编码。在我们的例子中,字符串是[^a-z]+
。括号表示该模式覆盖一系列字符,即字母表中的所有字符。我们使用插入符号^
来否定这个模式,表示正则表达式应该只匹配不包含在其中的字符。这当然包括特殊符号、标点和数字。最后,+
符号表示我们允许这样的符号在序列中重复出现。
清单 6-7 实现了清洁过程中的最后一步。我们首先导入库re
,用来实现正则表达式。接下来,我们遍历每个句子中的每个单词,并用一个空字符串替换任何模式匹配。这给我们留下了一个句子列表,每个句子又被分解成一个单词列表。由于这个过程会留下一些空字符串,我们将重新连接每个句子中的单词。我们还将删除句子开头和结尾的任何空白。
import re
# Remove special characters, punctuation, and numbers.
for j in range(len(words)):
words[j] = [re.sub('[^a-z]+', '', w)
for w in words[j]]
# Rejoin words into sentences.
for j in range(len(words)):
words[j] = " ".join(words[j]).strip()
# Print sentence.
print(words[7])
compani continu target commerci product late q earli q provid regular updat regard oper
Listing 6-7Remove special characters and join words into sentences
再次打印同一个句子,我们可以看到它现在看起来与原来的形式大不相同。而不是一个句子,它看起来像一个词干的集合。实际上,在下一节中,我们将应用一种文本分析形式,将文档视为单词的集合,而忽略它们出现的顺序。这通常被称为“词汇袋”模型。
单词袋模型
在上一节中,我们提出了一种可能的文档术语(DT)矩阵的构造, C ,将使用字数作为特征。这种表示法不允许我们考虑语法或词序,但它允许我们捕捉词频。在经济和金融中有许多问题,我们将能够在这种限制下实现我们的目标。
我们描述的模型被称为“词袋”(BoW)模型,它是由 Salton 和 McGill (1983)在信息检索文献中介绍的。词汇袋一词似乎起源于 Harris (1954)的一个语言学语境:
我们建立了一个话语库,每个话语都是特定元素的特定组合。这种元素组合的存量成为一个因素……因为语言不仅仅是一袋单词,而是一种工具,在使用过程中形成了具有特定属性的工具。
在这一节中,我们将看到如何构建一个 BoW 模型,从上一节中清理和准备的数据开始。除了 NLTK,我们还将使用来自sklearn
的子模块来构建 DT 矩阵。虽然 NLTK 中有一些例程来执行这样的任务,但是它们不是核心模块的一部分,通常效率较低。
回想一下words
包含了我们从一家金属矿业公司的 6k 文件中提取的 50 个句子。我们将使用这个列表来构建清单 6-8 中的文档术语矩阵,这里我们从从sklearn.feature_extraction
导入text
开始。然后我们将实例化一个CounterVectorizer()
,它将计算每个句子中单词的频率,然后基于一些约束构造 C 矩阵,这些约束可以作为参数提供。为了便于说明,我们将max_features
设为 10。这将限制文档术语矩阵中的最大列数不超过 10。
接下来,我们将把fit_transform()
应用到words
,将其转换成文档术语矩阵 C 。由于 C 对于许多问题来说会很大,sklearn
将其保存为一个稀疏矩阵。您可以使用toarray()
方法将其转换为数组。我们还可以应用vectorizer()
的get_feature_names()
来恢复对应于每一列的术语。
from sklearn.feature_extraction import text
# Instantiate vectorizer.
vectorizer = text.CountVectorizer(max_features = 10)
# Construct C matrix.
C = vectorizer.fit_transform(words)
# Print document-term matrix.
print(C.toarray())
[[3 1 0 2 0 0 1 0 2 2]
[1 2 0 1 0 0 0 0 0 1]
...
...
...
[0 1 0 0 0 1 0 0 0 0]
[0 0 0 0 0 0 0 1 1 0]]
# Print feature names.
print(vectorizer.get_feature_names())
['america', 'compani', 'cost', 'gold', 'includ',
'inform', 'oper', 'product', 'result', 'silver']
Listing 6-8Construct the document-term matrix
打印文档术语矩阵和特性名称,我们可以看到我们恢复了十个不同特性的计数。虽然这对于说明来说是有用的,但是我们通常希望在实际应用中使用更多的特性;然而,允许更多的功能可能会导致包含不太有用的功能,这将需要使用过滤。
Sklearn
为我们提供了两个额外的参数,我们可以使用它们来执行过滤:max_df
和min_df
。max_df
参数决定了一个术语在从术语矩阵中删除之前可能出现在文档中的最大数量或比例。同样,最低门槛是由min_df
给出的。在这两种情况下,指定一个整数值(如 3)表示文档数,而指定一个浮点数(如 0.25)表示文档的比例。
指定最大阈值的价值在于,它将删除所有出现过于频繁而无法提供有意义变化的术语。例如,如果一个术语出现在超过 50%的文档中,我们可能希望通过将max_df
指定为 0.50 来删除它。在清单 6-9 中,我们再次计算文档术语矩阵,但是这次允许多达 1000 个术语,并且还应用过滤来删除出现在超过 50%或少于 5%的文档中的术语。
如果我们打印出 C 矩阵的形状,我们可以看到文档术语矩阵似乎没有受到最大特征限制 1000 的约束,因为只返回了 109 个特征列。这可能是我们选择最大文档频率和最小文档频率参数的结果,这些参数消除了对我们的目的不太可能有用的术语。
我们可以执行过滤的另一种方法是使用术语频率逆文档频率(tf-idf)度量,如公式 6-2 所示。
方程式 6-2。计算 j 列的词频倒数-文档频率
)
为文档术语矩阵 C 中的每个特征 j 计算 tf-idf。它由两个分量的乘积组成:(1)术语 j 在语料库中的所有文档中出现的频率,∑Icij;以及(2)文档计数的自然对数除以术语 j 至少出现一次的文档的数量)。tf-idf 度量随着 j 在整个语料库中出现的次数的增加而增加,并且随着 j 出现的文档的份额的减少而减少。如果 j 不经常使用或者在太多文档中使用,tf-idf 得分会很低。
# Instantiate vectorizer.
vectorizer = text.CountVectorizer(
max_features = 1000,
max_df = 0.50,
min_df = 0.05
)
# Construct C matrix.
C = vectorizer.fit_transform(words)
# Print shape of C matrix.
print(C.toarray().shape)
(50, 109)
# Print terms.
print(vectorizer.get_feature_names()[:10])
['abil', 'activ', 'actual', 'affect', 'allin', 'also', 'america', 'anticip', 'approxim', 'avail']
Listing 6-9Adjust the parameters of CountVectorizer()
在清单 6-10 中,我们重复了与清单 6-8 相同的步骤,但是我们使用了TfidfVectorizer()
,而不是CountVectorizer()
。这允许我们访问idf_
参数,它包含逆文档频率分数。然后,我们可以通过删除 tf-idf 得分低于某个阈值的列来选择性地执行筛选。
# Instantiate vectorizer.
vectorizer = text.TfidfVectorizer(max_features = 10)
# Construct C matrix.
C = vectorizer.fit_transform(words)
# Print inverse document frequencies.
print(vectorizer.idf_)
[2.36687628 1.84078318 3.14006616 2.2927683 2.44691898 2.22377543 1.8873032 2.22377543 2.22377543 2.2927683 ]
Listing 6-10Compute inverse document frequencies for all columns
在一些应用中,我们可能希望使用一个序列中的几个单词(n-grams)——而不是单个单词(unigrams)——作为我们的特征。我们可以通过设置TfidfVectorizer()
或CountVectorizer()
的ngram_range
参数来实现。在清单 6-11 中,我们将参数设置为(2,2),这意味着我们只允许两个单词的序列(二元模型)。请注意,元组中的第一个值是最小字数,第二个值是最大字数。我们可以看到,返回的特性名称集现在不同于我们在清单 6-9 中生成的 unigrams。
一般来说,应用词袋模型和计算文档术语矩阵将只是自然语言处理项目的第一步;然而,它应该是直截了当地看到,这样一个矩阵可以与计量经济学的标准工具相结合,以执行分析。例如,如果我们有一个与每份文件相关的因变量,比如一家公司在 SEC 备案的同一天的股票回报,我们可以将这两者结合起来训练一个预测模型或测试一个假设。
# Instantiate vectorizer.
vectorizer = text.TfidfVectorizer(
max_features = 10,
ngram_range = (2,2)
)
# Construct C matrix.
C = vectorizer.fit_transform(words)
# Print feature names.
print(vectorizer.get_feature_names())
['america gold', 'cosal oper', 'forwardlook inform', 'galena complex', 'gold silver', 'illeg blockad', 'oper result', 'recapit plan', 'relief canyon', 'silver corpor']
Listing 6-11Compute the document-term matrix for unigrams and bigrams
基于字典的方法
在前面的小节中,我们清理和准备了数据,然后使用单词袋模型对其进行了探索。这产生了一个 NxK 文档术语矩阵, C ,它由每个文档的字数组成。我们过滤了文档术语矩阵中的某些单词,但是对于我们希望在文本中找到什么特征,我们仍然不知道。
这种方法的一种替代方法是使用预先选择的单词“字典”,它被构造来捕捉文本中的一些潜在特征。这种方法通常被称为“基于词典的方法”,是经济学中最常用的文本分析形式。
经济学中基于词典的方法的早期应用是利用《华尔街日报》文章中的潜在“情绪”来研究新闻和股市表现之间的关系(Tetlock 2007)。后来的工作,如 Loughran 和 McDonald (2011)和 Apel 和 Blix 马尔迪(2014),引入了旨在测量特定潜在变量的字典,这导致它们在文献中的广泛使用。Loughran 和 McDonald (2011)介绍了一个 10-K 财务文件字典,它最终被用来衡量许多情况下的消极和积极情绪。阿佩尔和布利克斯·马尔迪(2014)推出了一部字典,用来衡量央行沟通中的“鹰派”和“鸽派”。
Gentzkow 等人(2019)认为,经济学和社会科学应该扩展他们用于进行文本分析的工具集。与其将基于字典的方法作为默认选择,不如仅在满足以下两个条件时才考虑使用它们:
-
你所拥有的关于潜在变量及其在文本中的表现方式的先验信息是强大而可靠的。
-
文本中关于潜在变量的信息是微弱而分散的。
一个理想的例子是贝克等人(2016)引入的经济政策不确定性(EPU)指数。他们想要测量的潜在变量是一个理论对象,他们通过识别涉及经济、政策和不确定性的词语的联合使用来捕捉文本。如果不为这样的对象指定一个字典,它就不太可能作为一个公共特性或主题出现在模型中。此外,在指定了词典之后,他们通过将 EPU 指数得分与人类对相同报纸文章的评分进行比较,证明了词典抓住了潜在的理论目标。
由于基于字典的方法实现起来很简单,并且不需要使用 TensorFlow,所以我们将用一个涉及 Loughran-McDonald (LM)字典的例子来演示它们是如何工作的。我们将从使用pandas
加载清单 6-12 中的 LM 字典开始。 5 我们将使用pandas
的read_excel
子模块,并指定文件路径和工作表名称。注意,我们已经指定了“阳性”表,因为在这个例子中我们将专门使用阳性词词典。
import pandas as pd
# Define data directory path.
data_path = '../data/chapter6/'
# Load the Loughran-McDonald dictionary.
lm = pd.read_excel(data_path+'LM2018.xlsx',
sheet_name = 'Positive',
header = None)
# Convert series to DataFrame.
lm = pd.DataFrame(lm.values, columns = ['Positive'])
# Convert to lower case.
lm = lm['Positive'].apply(lambda x: x.lower())
# Convert DataFrame to list.
lm = lm.tolist()
# Print list of positive words.
print(lm)
['able',
'abundance',
'abundant',
...
'innovator',
...
'winners',
'winning',
'worthy']
Listing 6-12Compute the Loughran-McDonald measure of positive sentiment
接下来,我们将把熊猫Series
转换成DataFrame
,并使用字典的列标题“Positive”。然后我们将使用一个lambda
函数将所有单词转换成小写,因为它们在 LM 字典中是大写的。最后,我们将把DataFrame
转换成一个列表对象,然后打印。查看最后三个术语,我们可以看到其中两个术语——winner 和 winning——很可能具有相同的词干。
一般来说,我们通常要么想从词典中提取词干,从语料库中提取词干,要么两者都不提取。因为我们已经对语料库进行了词干分析——即,来自 6k 填充的句子——我们也将对词典进行词干分析,在此过程中删除重复的词干。我们在清单 6-13 中这样做。
from nltk.stem.porter import PorterStemmer
# Instantiate Porter stemmer.
stemmer = PorterStemmer()
# Apply Porter stemmer.
slm = [stemmer.stem(word) for word in lm]
# Print length of list.
print(len(slm))
354
# Drop duplicates by converting to set.
slm = list(set(slm))
# Print length of list.
print(len(slm))
151
Listing 6-13Stem the LM dictionary
按照本章前面的步骤,我们将首先实例化一个 Porter 词干分析器,然后使用 list comprehension 将它应用于字典中的每个单词。原始列表包含 354 个单词。如果我们随后将该列表转换为一个集合,这将删除重复的词干,从而将字典术语的数量减少到 151 个。
下一步是获取包含我们从文档中提取的 50 个句子的words
列表,并统计阳性词干的实例。回想一下,我们对句子中的每个单词进行了清理和词干处理,然后将它们存储为字符串。我们需要遍历每个字符串,计算每个正面单词出现的次数。我们将在清单 6-14 中这样做。
# Define empty array to hold counts.
counts = []
# Iterate through all sentences.
for w in words:
# Set initial count to 0.
count = 0
# Iterate over all dictionary words.
for i in slm:
count += w.count(i)
# Append counts.
counts.append(count)
Listing 6-14Count positive words
在清单 6-14 中,我们从定义一个空列表来保存计数开始。然后,我们在外部循环中迭代包含在words
列表中的所有字符串。每当我们开始一个新句子时,我们将正字数设置为 0。然后我们遍历内部循环,遍历词干 LM 字典中的所有单词,计算它们在字符串中出现的次数,并将其加到总数中。我们将每个句子的总数追加到counts
。
图 6-5 显示了正字数的直方图。我们可以看到,大多数句子没有,而一个句子有十个以上。如果我们在文档级别执行这种分析,正如我们通常会做的那样,我们很可能会发现大多数 6k 申请的非零值。
图 6-5
6-K 填充中正单词数在句子中的分布
原则上,我们可以将我们的阳性计数作为一个特征包含在回归中。然而,在实践中,我们通常会使用 count 变量的转换,它有更自然的解释。如果我们没有零计数,我们可以使用计数的自然对数,使我们能够将估计的效应解释为对阳性百分比变化的影响。或者,我们可以使用正面单词与所有单词的比率。
最后,在经济和金融应用中,通常使用净指数,结合积极和消极或“鹰派”和“鸽派”,如等式 6-3 所示。通常,我们会取正字数和负字数之差,然后除以一个归一化因子。该因子可以是文档的总字数,或者是正项和负项的总和。
方程式 6-3。 净积极性指数 。
)
单词嵌入
到目前为止,我们已经使用了一键编码(虚拟变量)来构造单词的数字表示。这种方法的一个潜在缺点是,我们隐含地假设每对单词都是正交的。例如,“通货膨胀”和“价格”这两个词被认为彼此没有关系。
使用单词作为特征的替代方法是使用嵌入。与具有高维稀疏表示的单词向量相反,单词嵌入使用低维密集表示。这种密集的表示允许我们识别单词的相关程度。
图 6-6 提供了一个一键编码字和密集字嵌入的简单比较。“…通货膨胀急剧上升…”这句话可能会出现在央行的声明中,可以用这两种方法中的任何一种进行编码。如果我们使用 one-hot 编码方法,如图左侧所示,每个单词都将被转换为一个稀疏的高维向量。并且每个这样的向量将与所有其他向量正交。另一方面,如果我们使用嵌入,每个单词将与一个低维的密集表示相关联,如图 6-6 的右图所示。两个这样的向量之间的关系是可测量的,并且可以被捕获,例如,使用它们的内积。等式 6-4 给出了两个维度为n–x和 z 的向量的内积公式。
方程式 6-4。两个向量 x 和 z 的内积
)
图 6-6
一键编码和单词嵌入的比较
虽然内积可以给我们两个单词之间关系的简明摘要,但是它不能提供关于两个嵌入向量如何相关的更详细的信息。为此,我们可以直接比较一对向量中相同位置的元素。这些元素提供了对相同特征的测量。虽然我们可能无法确定潜在的特征是什么,但是我们知道在相同的位置有相似的值表明两个单词在该维度上是相关的。
与一次性编码相反,我们需要使用一些监督或无监督的方法来训练嵌入。由于嵌入需要捕捉单词中的含义和单词之间的关系,所以自己进行训练通常没有意义。除此之外,嵌入层将需要学习您执行分析所用的语言,并且您提供的语料库几乎肯定不足以完成该任务。
因此,您通常会使用预训练的单词嵌入。常见的选择包括 Word2Vec (Mikolov 等人 2013)和单词表示的全局向量(Pennington 等人 2014)。
请注意,单词嵌入和卷积层之间有很强的相似性。对于卷积层,我们说它们包括通用视觉滤波器。出于这个原因,使用预先训练在数百万张图像上的模型的卷积层通常是有意义的。此外,我们说过可以“微调”此类模型的训练,以提高特定图像分类任务的局部性能。单词嵌入也是如此。
主题建模
主题模型的目的是发现语料库中潜在的一组主题,并确定这些主题在各个文档中的存在程度。第一个主题模型,潜在的狄利克雷分配(LDA),在 Blei 等人(2003)中被引入到机器学习文献中,并且此后在许多领域中找到了应用,包括经济学和金融学。
虽然 TensorFlow 没有提供标准主题模型的实现,但它是许多复杂主题模型的首选框架。一般来说,主题模型如果利用深度学习,将更有可能在 TensorFlow 中实现。
由于主题建模在经济学中的应用越来越多,我们将在这一部分提供一个简短的介绍,尽管我们不会使用 TensorFlow。我们将从静态 LDA 模型 Blei et al. (2003)的理论概述开始,然后描述如何使用sklearn
实现和调整它。我们将通过讨论最近引入的模型变体来结束这一部分。
在 Blei 等人(2003)中,LDA 模型描述如下:
语料库的生成概率模型。其基本思想是将文档表示为潜在主题的随机混合,其中每个主题都以单词分布为特征。
有几个概念值得解释,因为它们会在本章和正文中反复出现。首先,该模型是“生成性的”,因为它生成一个新颖的输出——主题分布——而不是执行一个判别任务,例如学习一个文档的分类。第二,它是“概率性的”,因为该模型明确基于概率论并产生概率。第三,我们说主题是“潜在的”,因为它们没有被明确地测量或标记,而是被假定为文档的潜在特征。
虽然我们不会讨论求解 LDA 模型的细节,但我们将简要总结 Blei et al. (2003)中模型的基本假设,从符号开始。首先,他们假设单词是从长度为 V 的固定词汇表中抽取的,并使用独热编码向量来表示它们。接下来,他们将文档定义为一个由 N 个单词、 w = ( w 1 、 w 2 、…、 w N )组成的序列。最后,他们将一个语料库定义为文档的集合,D= {w1, w 2 ,…,wM}。
该模型对在语料库中生成文档 w 、文档 D 的基础过程做出三个假设:
-
每个文档中的字数 N 、 w 来自泊松分布。
-
潜在话题是从一个 k 维随机变量 θ 中抽取出来的,这个变量有一个狄利克雷分布: θ ~ Dir ( α )。
-
对于每个单词, n ,一个主题,zn,从一个以 θ 为条件的多项式分布中抽取。然后,根据主题的条件,从多项式分布中抽取单词本身, z n 。
作者认为字数的泊松分布不是一个重要的假设,使用一个更现实的假设会更好。狄利克雷分布的选择将 θ 约束为一个 (k-1) 维单形。它还提供了贝塔分布的多元推广,并由正值权重的 k 向量 α 来参数化。Blei 等人(2003)选择狄利克雷分布有三个原因:“…它属于指数族,具有有限维的充分统计量,并且与多项式分布共轭。”他们认为这将确保它在估计和推理算法中的适用性。
主题分布的概率密度 θ 在等式 6-5 中给出。
方程式 6-5。话题的分布。
)
在图 6-7 中,我们提供了在 k = 2 的情况下,从狄利克雷分布中随机抽取 100 次的直观图示。在图 6-7 的左侧面板中,我们设置α=【0.9,0.1】,在右侧面板中,我们设置α=【0.5,0.5】。在这两种情况下,所有点都位于单纯形上。也就是说,对与任何点相关的坐标求和将得到 1。此外,我们可以看到,选择相同的值 α 0 和 α 1 会产生沿单纯形均匀分布的点,而增加 α 0 的相对值会导致向水平轴倾斜(即,主题 θ k )。
图 6-7
从狄利克雷分布随机抽取的图,k=2,参数向量 0.9,0.1 和 0.5,0.5
接下来,我们将实现一个 LDA 模型,利用我们之前通过将 6-K 文件分成句子构建的文档语料库。回想一下,我们使用CountVectorizer
定义了一个文档术语矩阵 C 。我们将在清单 6-15 中使用这两者,我们从从sklearn.decomposition
导入LatentDirichletAllocation
开始。接下来,我们用我们首选的参数值实例化一个模型。在这种情况下,我们将只设置主题的数量,n_components
。这对应于理论模型中的 k 参数。
我们现在可以在文档术语矩阵上训练模型,并使用lda.components_
恢复输出wordDist
。注意wordDist
有形状(3,109)。行对应潜在主题,列对应权重。权重越高,一个词对于定义一个主题就越重要。 6
接下来,我们将利用输出结果wordDist
,为每个主题确定权重最高的单词。我们将定义一个空列表topics
来保存主题。在列表理解中,我们将遍历每个主题数组,并应用argsort()
来恢复对数组进行排序的索引。然后,我们将恢复最后五个索引并颠倒它们的顺序。
对于每个索引,我们将利用从vectorizer
中恢复的feature_names
来识别相关的术语。然后我们将打印主题列表。
一个完整的主题描述由词汇的权重向量组成。我们可以通过确定哪些单词具有足够高的权重来证明它们包含在主题描述中,从而选择如何描述这样的主题。在这种情况下,我们简单地使用了权重最高的五个单词;然而,原则上,我们可以使用阈值或其他标准。
from sklearn.decomposition import LatentDirichletAllocation
# Set number of topics.
k = 5
# Instantiate LDA model.
lda = LatentDirichletAllocation(n_components = k)
# Recover feature names from vectorizer.
feature_names = vectorizer.get_feature_names()
# Train model on document-term matrix.
lda.fit(C)
# Recover word distribution for each topic.
wordDist = lda.components_
# Define empty topic list.
topics = []
# Recover topics.
for i in range(k):
topics.append([feature_names[name] for
name in wordDist[i].argsort()[-5:][::-1]])
# Print list of topics.
print(topics)
[['inform', 'america', 'gold', 'forwardlook', 'result'],
['oper', 'compani', 'product', 'includ', 'relief'],
['silver', 'lead', 'cost', 'ounc', 'galena']]
Listing 6-15Perform LDA on 6-K filing text data
既然我们已经确定了主题,下一步就是确定这些主题描述了什么。在我们的简单示例中,我们恢复了三个主题。第一个似乎参考了与黄金相关的前瞻性信息。第二个似乎涉及公司的运营和生产。第三个话题是关于金属的成本。
最后,我们通过使用我们的模型的transform()
方法将主题概率分配给清单 6-16 中的句子来完成练习。
# Transform C matrix into topic probabilities.
topic_probs = lda.transform(C)
# Print topic probabilities.
print(topic_probs)
array([[0.0150523 , 0.97172408, 0.01322362],
[0.02115127, 0.599319 , 0.37952974],
[0.33333333, 0.33333333, 0.33333333],
...
[0.93766165, 0.03140632, 0.03093203],
[0.08632993, 0.82749933, 0.08617074],
[0.95509882, 0.02178363, 0.02311755]])
Listing 6-16Assign topic probabilities to sentences
正如我们在清单 6-16 中看到的,输出是一个 shape (3,50)的矩阵,它包含每个句子的主题概率总和为 1。例如,如果我们收集了每个日期单独的 6-K 文件,而不是查看文件中的句子,我们现在就有了主题比例的时间序列。
我们还在图 6-8 中绘制了主题比例。我们可以看到,在文档文档中,跨句子的主题似乎是持久的。例如,主题 1 在文档的开头和结尾占主导地位,而主题 3 在中间重要性上升。
图 6-8
按句子划分的主题比例
虽然我们考虑了一个不需要仔细选择模型或训练参数的简单示例,但sklearn
中的 LDA 实现实际上允许选择各种不同的参数。我们在下面考虑其中的六个参数:
-
主题先验:默认情况下,LDA 模型将使用 1/
n_components
作为 α 中所有元素的先验。然而,您可以通过为参数doc_topic_prior
显式提供主题分布来提供不同的先验。 -
学习方法:默认情况下,
sklearn
中的 LDA 模型将使用变分贝叶斯来训练模型,并将利用全样本来执行每次更新。但是,可以通过将learning_method
参数设置为'online'
来进行小批量训练。 -
批量:在使用在线培训的条件下,您还可以选择更改小批量的默认值 128。您可以使用
batch_size
参数来完成此操作。 -
学习衰减:使用在线学习方式时,可以使用
learning_decay
参数调整学习速率。较高的衰减值会降低我们从以前的迭代中保留的信息。默认值为 0.7,文档建议选择(0.5,1)区间内的衰减。 -
最大迭代次数:设置最大迭代次数将在达到阈值后终止训练过程。默认情况下,
max_iter
参数设置为 128。如果模型没有在 128 次迭代内收敛,您可能希望为该参数设置一个较高的值。
最后,Blei 等人(2003)提出的标准 LDA 模型的两个局限性值得讨论。首先,主题的数量和内容都不会随着语料库的变化而变化。对许多问题来说,这不是问题;然而,对于涉及时间序列维度的经济学和金融学应用程序,这可能会很成问题,我们将在下一段中对此进行阐述。第二,LDA 模型没有对提取的主题提供任何有意义的控制。如果我们希望跟踪数据中特定类型的事件,我们可能无法使用 LDA 模型来做到这一点,因为无法保证它会识别这些事件。
关于第一个问题——即在时间序列环境中使用 LDA 可能会出现两个问题。首先,该模型将审查那些只短暂出现的主题,如金融危机,即使它们在出现的时期相当重要。第二,它将在主题分布中引入“前瞻”偏差,强制未来出现的主题也成为整个样本中的主题。这会造成这样的印象,即 LDA 模型会预测到事件,而如果样本在事件发生之日被截断,它就不会预测到事件。
关于第二个问题,LDA 提出了两个问题。首先,我们不可能将模型引向感兴趣的主题。例如,我们不能向 LDA 模型提交主题查询。第二个问题是模型产生的主题通常很难解释。这是因为主题只是词汇表中所有单词的简单分布。如果不研究主题的分布,不检查被确定为占主导地位的文档,我们通常无法确定主题到底是什么。
然而,最近开发的模型试图克服静态 LDA 模型的局限性。例如,Blei 和 Lafferty (2006)介绍了主题模型的动态版本。此外,Dieng 等人(2019 年)通过引入动态嵌入式主题模型(D-ETM)进一步扩展了这一点。这种模式是动态的,允许使用大量的词汇,并倾向于产生更多可解释的主题。这解决了与原始静态 LDA 模型相关的两个问题。
文本回归
正如 Gentzkow 等人(2019)所讨论的,经济学和金融学中的大多数文本分析都围绕着词袋模型和基于词典的方法。虽然这些技术在某些情况下很有用,但它们并不是解决所有研究问题的最佳工具。因此,许多涉及经济学文本分析的项目可能会通过使用不同于自然语言处理的方法而得到改进。
一种选择是使用文本回归,这是一个简单的回归模型,包括文本特征,如来自术语-文档矩阵的列,作为回归量。Gentzkow 等人(2019 年)认为,文本回归是经济学家采用的一种很好的候选方法。这是因为经济学家主要使用线性回归进行实证研究,并且通常熟悉惩罚线性回归。因此,学习如何执行文本回归主要是关于构建文档术语矩阵,而不是学习如何估计回归。
我们将从在 TensorFlow 中执行简单的文本回归开始这一部分。为此,我们需要构建文档术语矩阵和一个连续的因变量。我们将使用 SEC 系统中苹果的所有 8k 文件来构建文件术语矩阵,而不是使用 6k 文件中的句子。 7
为了简洁起见,我们将省略数据收集过程的细节,只是说我们执行了本章前面讨论的相同步骤来生成文档术语矩阵x_train
,并将股票收益数据存储为y_train
。总的来说,我们利用了 144 个文件并提取了 25 个单字计数来构建x_train
。
回想一下第三章,带有 k 回归量的线性模型具有等式 6-6 中给出的形式。在这种情况下, k 回归量是来自文档术语矩阵的特征计数。请注意,我们使用 t 来索引文档,因为我们使用的是归档和返回的时间序列。
方程式 6-6。一个 线性模型 。
)
当然,我们可以利用 OLS,用解析表达式求解参数向量。然而,为了建立不易于分析的模型,我们将使用 LAD 回归。在清单 6-17 中,我们导入tensorflow
和numpy
,初始化一个常数项(alpha
和系数向量(beta
),将x_train
和y_train
转换成numpy
数组,然后定义一个函数(LAD
,将参数和数据转换成预测。
回想一下,我们必须使用tf.Variable()
来定义我们希望训练的参数,并且可以使用np.array()
或tf.constant()
来定义数据。
import tensorflow as tf
import numpy as np
# Draw initial values randomly.
alpha = tf.random.normal([1], stddev=1.0)
beta = tf.random.normal([25,1], stddev=1.0)
# Define variables.
alpha = tf.Variable(alpha, tf.float32)
beta = tf.Variable(beta, tf.float32)
# Convert data to numpy arrays.
x_train = np.array(x_train, np.float32)
y_train = np.array(y_train, np.float32)
# Define LAD model.
def LAD(alpha, beta, x_train):
prediction = alpha + tf.matmul(x_train, beta)
return prediction
Listing 6-17Prepare the data and model for a LAD regression in TensorFlow
接下来的步骤是定义一个损失函数并执行最小化,我们在清单 6-18 中就是这么做的。我们将使用平均绝对误差(MAE)损失,因为我们正在执行 LAD 回归。然后,我们将用默认参数值实例化一个Adam()
优化器。最后,我们将执行 1000 次训练迭代。
# Define number of observations.
N = len(x_train)
# Define function to compute MAE loss.
def maeLoss(alpha, beta, x_train, y_train):
y_hat = LAD(alpha, beta, x_train)
y_hat = tf.reshape(y_hat, (N,))
return tf.losses.mae(y_train, y_hat)
# Instantiate optimizer.
opt = tf.optimizers.Adam()
# Perform optimization.
for i in range(1000):
opt.minimize(lambda: maeLoss(alpha, beta,
x_train, y_train),
var_list = [alpha, beta])
Listing 6-18Define an MAE loss function and perform optimization
现在我们已经训练了一个模型,我们可以将任意输入输入到LAD
函数中,这将产生预测值。我们将使用x_train
为清单 6-19 中的y_train
生成预测y_pred
。
# Generate predicted values.
y_pred = LAD(alpha, beta, x_train)
Listing 6-19Generate predicted values from model
我们在图 6-9 中绘制了预测值与真实值的对比图。常数项与平均回报相匹配,预测似乎正确地捕捉了大多数变化的方向;然而,该模型通常无法解释数据中的大部分变化。
图 6-9
苹果股票回报的真实值和预测值
有几个与自然语言处理无关的原因可能解释了我们无法使用模型解释数据中的大部分变化。首先,1 天的时间窗口可能太大,可能会捕捉到与公告效果无关的进展。事实上,关于这个主题的许多经济学文献已经转移到集中于公告周围的更窄的窗口,例如 30 分钟。其次,我们在回归中没有包括任何非文本特征,如滞后回报、整个科技行业的回报或统计机构发布的数据。第三,预测意外回报具有挑战性,即使是好的模型通常也无法解释数据中的大多数变化。
然而,为了这个练习的目的,让我们把这些都放在一边,考虑一下我们如何改进纯粹基于公告的 NLP 的预测。一个好的起点可能是质疑我们是否选择了包含有意义的内容来解释回报的单字谜。考虑到我们不加批判地接受了由CountVectorizer()
选择的 25 个特性,更深思熟虑的特性选择可能会带来改进。回想一下,我们可以使用get_feature_names()
方法从矢量器中提取特征。在清单 6-20 中,我们这样做,然后打印从文本中提取的单字。
# Get feature names from vectorizer.
feature_names = vectorizer.get_feature_names()
# Print feature names.
print(feature_names)
['act', 'action', 'amend', 'amount', 'board',
'date', 'director', 'incom', 'law', 'made',
'make', 'net', 'note', 'offic', 'order',
'parti', 'price', 'product', 'quarter', 'refer',
'requir', 'respect', 'section', 'state', 'term']
Listing 6-20Generate predicted values from model
正如我们在清单 6-20 中看到的,许多术语似乎是中性的。根据他们在文本中被修改的方式,他们可以预测正或负的回报。如果模型能够在正确的上下文中处理使用,它可能会给正确签名的特征分配一个较大的量级。
我们可能会尝试通过扩展功能集、执行更广泛的过滤来确定我们包括的功能,或者更改模型规范以允许非线性,如功能交互,来解决这个问题。因为我们已经讨论了清理和过滤,我们将把重点放在特性的扩展和非线性的包含上。
假设训练集只包含 144 个观察值,我们可能会担心包含更多的特征会导致训练样本的改进,但会导致过拟合。这是一个合理的考虑,我们将通过使用惩罚回归模型来克服它。代价将是包括更多具有非零值的参数将降低损失函数值。因此,如果参数不能提供可观的预测值,我们将把它们归零或给它们分配较低的数值。
Gentzkow 等人(2019)定义了一个一般的惩罚估计量,作为等式 6-7 中最小化问题的解决方案。
方程式 6-7。一个惩罚估计量的最小化问题 。
)
请注意, l ( α , β )是损失函数,例如线性回归的 MAE 损失, λ 缩放罚值的大小, κ j ()是递增的罚函数,原则上可以根据参数而不同;然而,在实践中,我们通常会假设它对于所有的回归变量都是相同的。
我们经常会遇到三种类型的惩罚回归,每一种都由相关的选择 κ ():
-
LASSO 回归:最小绝对收缩选择参数(LASSO)模型使用 β 的l1 范数,将 κ 还原为绝对值或∣βj∣为所有 j 。套索回归中惩罚的函数形式将强制某些参数值为 0,从而产生稀疏的参数向量。
-
岭回归:岭回归使用 β 的 L 2 范数,得到
)。与套索回归不同,岭回归将生成系数未精确设置为零的 β 的密集表示。由于岭回归的罚项是一个凸函数,它将产生一个唯一的最小值。
-
弹性网回归:弹性网回归结合了套索和岭回归惩罚。那就是,
)对所有的 j 。
方程 6-8、6-9 和 6-10 分别给出了套索、脊和弹性网回归的最小化问题。
方程式 6-8。套索回归的最小化问题。
)
方程式 6-9。岭回归的最小化问题 。
)
方程式 6-10。弹性网回归的最小化问题。
)
我们将回到苹果股票回报预测问题,但现在将利用套索回归,这将产生一个稀疏系数向量。在我们的例子中,有许多中性术语在线性模型中可能增加最小的价值,它们不能被形容词修饰。通过使用套索回归,我们将允许模型通过分配零权重来决定是否完全忽略它们。
在我们修改模型之前,我们将首先再次应用CountVectorizer()
,但是这一次,我们将为 1000 个术语而不是 25 个术语构建一个文档术语矩阵。为了简洁起见,我们将省略细节,而是从过程的最后开始,其中feature_names
包含 1000 个元素,x_train
具有形状(144,1000)。
接下来,在清单 6-21 中,我们将重新定义beta
;设置惩罚的大小,lam
;重新定义损失函数,我们现在称之为lassoLoss()
。请注意,唯一的区别是我们添加了一个由lam
乘以β的 L 1 范数组成的项。除此之外,其他什么都没变。我们仍然使用LAD
函数进行预测,就像我们使用线性回归模型一样。
# Re-define coefficient vector.
beta = tf.random.normal([1000,1], stddev=1.0)
# Set value of lambda parameter.
lam = tf.constant(0.10, tf.float32)
# Modify the loss function.
def lassoLoss(alpha, beta, x_train, y_train,
lam = lam):
y_hat = LAD(alpha, beta, x_train)
y_hat = tf.reshape(y_hat, (N,))
loss = tf.losses.mae(y_train, y_hat) +
lam * tf.norm(beta, 1)
return loss
Listing 6-21Convert a LAD regression into a LASSO regression
在清单 6-22 中,我们将重复这些步骤,使用修改后的损失函数来训练模型,并在训练集上生成预测。
# Perform optimization.
for i in range(1000):
opt.minimize(lambda: lassoLoss(alpha, beta,
x_train, y_train),
var_list = [alpha, beta])
# Generate predicted values.
y_pred = LAD(alpha, beta, x_train)
Listing 6-22Train a LASSO model
现在我们已经有了 LASSO 模型的预测值,我们可以执行与真实回报的比较。图 6-10 描述了这种比较,提供了对图 6-9 的更新,图 6-9 进行了相同的练习,但是对于没有惩罚项并且只有 25 个特征的 LAD 模型。
我们可以看到,在具有 1000 个特征的 LASSO 模型下,性能有了实质性的提高;然而,我们可能会担心我们选择的惩罚幅度不够严厉,并且模型过拟合。为了评估这一点,我们可以将lam
调整到更高的值,并检查模型的性能。此外,我们可以使用测试集来执行交叉验证;然而,在只有 144 个观测值的时间序列环境中,这将更具挑战性。
现在,我们回想一下 LASSO 回归返回一个稀疏系数向量,并将检查有多少系数具有非零值。图 6-11 绘制了系数幅度的直方图。由此,我们可以看到超过 800 个特征被赋予了大约为零的值。虽然我们仍然有足够的特征来担心过拟合,但这并不太令人担心,因为惩罚函数的结果是模型忽略了 1000 个特征中的大多数。
图 6-10
使用套索模型的苹果股票收益的真实值和预测值
我们现在已经看到,通过采用一种正则化形式(即罚函数),我们可以利用回归中的附加特征。罚函数阻止我们简单地添加更多的参数来改善拟合。这样做将增加惩罚,这将迫使参数值通过显著提高拟合度来证明它们包含在模型中的合理性。这也意味着我们将能够包括更多的特征,并允许模型挑选出应该被赋予非零量值的特征。
图 6-11
使用套索模型的苹果股票收益的真实值和预测值
我们前面提到过,使用套索模型允许我们扩展特性集,这是提高性能的一种方法。我们提到的另一个选择是允许单词之间的依赖。我们可以通过允许模型中的非线性来做到这一点。原则上,我们可以设计这些特征。我们还可以利用任何非线性模型来执行这样的任务。此外,我们可以将它与惩罚项结合起来,就像我们对套索模型所做的那样,以避免过拟合。
虽然这些都是可行的策略,并且可以在 TensorFlow 中相对容易地实现,但我们将利用一个更通用的选项:深度学习。我们已经在第五章中讨论了图像背景下的深度学习,但我们在这里回到它,因为它为大多数文本回归问题提供了一种灵活而有效的建模策略。
“深度学习”(例如,神经网络)和“浅层学习”(例如,线性回归)之间的区别在于,浅层学习模型需要我们执行特征工程。例如,在线性文本回归中,我们必须决定哪些特征在文档术语矩阵中(例如,一元或二元)。我们还必须决定模型中允许多少特性。模型将决定哪些对解释数据中的变化最重要,但我们必须选择包括哪些。
再次回忆一下,图像不是这种情况。我们将像素值输入卷积神经网络,这些网络识别出越来越复杂的特征的连续层。首先,网络识别边缘。在下一层,他们确定了角落。每个后续图层都建立在前一图层的基础上,以识别对分类任务有用的新要素。
深度学习也可以以同样的方式用于文本。我们可以让神经网络为我们揭示这些关系,而不是通过使用特征工程来决定术语之间的关系。正如我们在第五章中所做的,我们将利用 TensorFlow 中的高级 Keras API。
在清单 6-23 中,我们定义了一个具有密集层的神经网络,我们将使用它来预测苹果的股票回报。这个网络和我们在第五章中定义的基于密集层的图像网络只有一个实质性的区别:使用了漏层。这里,我们包括了两个这样的层,每个层的比率都是 0.20。在训练阶段,这将随机删除 20%的节点,迫使模型学习鲁棒的关系,而不是使用大量的模型参数来记忆输出值。88
除此之外,请注意,我们已经定义了模型来接受具有 1000 个特性列的输入,这是我们在文档术语矩阵中包含的数量。我们也对所有隐藏层使用relu
激活功能。此外,我们在outputs
层使用了一个linear
激活函数,因为我们有一个连续的目标(股票回报)。
import tensorflow as tf
# Define input layer.
inputs = tf.keras.Input(shape=(1000,))
# Define dense layer.
dense0 = tf.keras.layers.Dense(64,
activation="relu")(inputs)
# Define dropout layer.
dropout0 = tf.keras.layers.Dropout(0.20)(dense0)
# Define dense layer.
dense1 = tf.keras.layers.Dense(32,
activation="relu")(dropout0)
# Define dropout layer.
dropout1 = tf.keras.layers.Dropout(0.20)(dense1)
# Define output layer.
outputs = tf.keras.layers.Dense(1,
activation="linear")(dropout1)
# Define model using inputs and outputs.
model = tf.keras.Model(inputs=inputs,
outputs=outputs)
Listing 6-23Define a deep learning model for text using the Keras API
我们选择的架构需要我们训练许多参数。回想一下,我们可以使用一个keras
模型的summary()
方法来检查这一点,我们在清单 6-24 中就是这么做的。该模型总共有 66,177 个可训练参数。
对于套索模型,我们已经担心过拟合,即使模型只有 1001 个参数,罚函数有效地迫使其中 850 个为零。我们现在有一个具有 66,177 个参数的模型,这应该使我们更加关注过拟合。这就是为什么我们使用了一种正规化(退出)的形式,以及为什么我们还将使用一个培训和验证样本。
# Print model architecture.
print(model.summary())
_____________________________________________________
Layer (type) Output Shape Param #
=====================================================
input_3 (InputLayer) [(None, 1000)] 0
_____________________________________________________
dense_5 (Dense) (None, 64) 64064
_____________________________________________________
dropout_1 (Dropout) (None, 64) 0
_____________________________________________________
dense_6 (Dense) (None, 32) 2080
_____________________________________________________
dropout_2 (Dropout) (None, 32) 0
_____________________________________________________
dense_7 (Dense) (None, 1) 33
=====================================================
Total params: 66,177
Trainable params: 66,177
Non-trainable params: 0
_____________________________________________________
Listing 6-24Summarize the architecture of a Keras model
回想一下,除了定义模型,我们还需要编译它。我们将这样做,并训练清单 6-25 中的模型。注意,我们使用 Adam 优化器、平均绝对误差(MAE)损失和 30%样本的验证分割。我们也将使用 20 个纪元。
# Compile the model.
model.compile(loss="mae", optimizer="adam")
# Train the model.
model.fit(x_train, y_train, epochs=20,
batch_size=32, validation_split = 0.30)
Epoch 1/20
100/100 [==============================] - 0s 5ms/sample - loss: 2.6408 - val_loss: 2.5870
...
Epoch 10/20
100/100 [==============================] - 0s 117us/sample - loss: 1.7183 - val_loss: 1.3514
...
Epoch 15/20
100/100 [==============================] - 0s 110us/sample - loss: 1.6641 - val_loss: 1.2014
...
Epoch 20/20
100/100 [==============================] - 0s 113us/sample - loss: 1.5932 - val_loss: 1.2536
Listing 6-25Compile and train the Keras model
正如我们在清单 6-25 中看到的,训练最初减少了训练和验证分割的损失;然而,到第 15 个时期,训练分割的损失继续下降,而验证分割的损失开始略有增加。这表明我们可能开始过度适应了。
图 6-12 使用model
的predict()
方法重复收益预测练习。虽然预测似乎比线性回归和套索回归有所改进,但这种改进很可能部分归因于过拟合。
图 6-12
使用神经网络的苹果股票收益的真实值和预测值
如果我们想进一步降低过拟合的风险,我们可以增加两个丢弃层的比率,或者减少隐藏层中的节点数量。
最后,请注意,利用单词序列,而不是忽略单词出现的顺序,可以显著提高模型性能。这将需要使用循环神经网络及其变体,包括长短期记忆(LSTM)模型。由于我们将使用相同系列的模型来执行时间序列分析,我们将推迟对第七章的介绍。
文本分类
在上一节中,我们讨论了如何使用 TensorFlow 来执行文本回归。一旦我们构建了文档术语矩阵,我们看到执行 LAD 回归和 LASSO 回归以及训练神经网络是相对简单的。然而,在某些情况下,我们会有一个离散的目标,并希望执行分类。幸运的是,TensorFlow 通过对我们已经定义的模型进行微小的调整,为我们提供了执行分类任务的灵活性。
例如,清单 6-26 展示了我们如何定义一个逻辑模型来执行分类。我们将假设我们使用相同的文档术语矩阵x_train
,但是现在已经用手工分类的标签代替了y_train
,我们通过读取单个 8k 文件,然后根据我们对内容的理解将它们分类为“正面”或“负面”。正数表示为 0,负数表示为 1。
# Define a logistic model.
def logitModel(x_train, beta, alpha):
prediction = tf.nn.softmax(tf.matmul(
x_train, beta) + alpha)
return prediction
Listing 6-26Define a logistic model to perform classification in TensorFlow
除了模型定义的变化,我们还需要修改损失函数来使用二进制交叉熵损失,我们在清单 6-27 中就是这么做的。之后,我们只需要在执行优化时更改函数句柄。其他一切都将像线性回归示例那样工作。
# Define number of observations.
N = len(x_train)
# Define function to compute MAE loss.
def logisticLoss(alpha, beta, x_train, y_train):
y_hat = LogitModel(alpha, beta, x_train)
y_hat = tf.reshape(y_hat, (N,))
loss = tf.losses.binary_crossentropy(
y_train, y_hat)
return loss
Listing 6-27Define a loss function for the logistic model to perform classification in TensorFlow
同样,如果我们想用清单 6-23 中定义的神经网络进行分类,我们只需要修改两行代码,如清单 6-28 所示。
# Change output layer to use sigmoid activation.
outputs = tf.keras.layers.Dense(1,
activation="sigmoid")(dropout1)
# Use categorical cross entropy loss in compilation.
model.compile(loss="binary_crossentropy", optimizer="adam")
Listing 6-28Modify a neural network to perform classification
我们改变了两件事:在outputs
层使用的激活函数和损失函数。首先,我们需要使用 sigmoid 激活函数,因为我们正在用两个类执行分类。第二,我们使用了binary_crossentropy
损失,这是两类分类问题的标准。如果我们有多个类的问题,我们将使用一个softmax
激活函数和一个categorical_crossentropy
损失。
有关神经网络分类的扩展概述,请参见第五章,该章涵盖了类似的内容,但在图像分类问题的背景下。此外,关于序列模型的信息,通常用于文本分类问题,参见第七章,该章使用相同的模型进行时间序列分析。
摘要
本章提供了文本分析目前在经济学和金融学中的应用,以及它在未来的应用。经济学家可能最不熟悉的部分是数据清理和准备步骤,它将文本转换为数字数据。最简单的版本是单词袋模型,它将单词从上下文中剥离出来,仅使用字数来概括文档的内容。虽然这种方法实现起来相对简单,但它功能强大,仍然是经济学中最常用的方法之一。
基于词典的方法也适用于单词袋模型。然而,我们不是计算一个文档中的所有术语,而是构建一个测量潜在变量的字典。正如 Gentzkow 等人(2019 年)所讨论的,这种方法经常用于经济学的文本分析,但对于许多研究应用来说,并不总是最好的工具。EPU 指数(Baker,Bloom and Davis 2016)可以说是经济学中基于词典的方法的理想用例,因为该指标在理论上很有趣,但不太可能成为语料库中的主导话题。
我们还讨论了单词嵌入,并了解了如何实现主题模型、文本回归模型和文本分类模型。这包括对文本使用深度学习模型的概述。然而,我们确实将序列模型的讨论推迟到了第七章,该章使用它们进行时间序列分析。
文献学
阿科斯塔,M. 2019。"中央银行透明度的新衡量标准及其对货币政策有效性的影响."工作文件。
Angelico,c .,J. Marcucci,M. Miccoli 和 F. Quarta。2018."我们可以用推特来衡量通胀预期吗?"工作文件。
阿佩尔,m。和 m。布利克斯马尔迪。2014."央行会议记录能提供多少信息?"经济学回顾65(1):53–76。
Ardizzi,g .,S. Emiliozzi,J. Marcucci 和 L. Monteforte。2020."新闻和消费卡支付."意大利银行经济工作报告。
arme lius h .、C. Bertsch、I. Hull 和 X. Zhang。2020.“传播信息:央行沟通的国际溢出效应.”《国际货币与金融杂志》 (103)。
阿西和 G.W .因本斯。2019.“经济学家应该了解的机器学习方法。”年度经济学评论11:685–725。
贝克、s . r . n .布鲁姆和 S.J .戴维斯。2016.《衡量经济政策的不确定性》经济学季刊131(4):1593–1636。
Bertsch,I. Hull,Y. Qi 和 X. Zhang。2020.“银行不当行为和网络借贷。”银行与金融杂志 116。
布莱,D.M .,A.Y. Ng 和 M.I .乔丹。2003."潜在的狄利克雷分配."机器学习研究杂志3:993–1022。
布雷医学博士和拉弗蒂博士。2006.“动态主题模型。”ICML 06:第 23 届机器学习国际会议论文集。113–120。
n .布鲁姆、S.R .贝克、S.J .戴维斯和 k .科斯特。2019."政策新闻和股票市场波动."油印本。
波恩,b,b .埃尔曼,和 m .弗拉茨彻。2013."央行关于金融稳定的沟通."经济日报 124。
Cerchiello,p .,G. Nicola,S. Ronnqvist 和 P. Sarlin。2017.“来自新闻和数字金融数据的深度学习银行困境.” arXiv。
Chahrour,r .,K. Nimark 和 S. Pitschner。2019."行业媒体焦点和总体波动."工作文件。
r .科雷亚、k .加鲁德、J.M .隆多诺和 n .米斯兰。2020.“央行金融稳定报告中的情绪。国际金融讨论论文 1203,美联储体系理事会(美国)。
迪昂、A.B、、F.J.R .鲁伊斯和 D.M .布雷。2019."动态嵌入主题模型."arXiv 预印本。
根茨科,m . b .凯利和 m .塔迪。2019."文本作为数据。"经济文献杂志57(3):535–574。
汉森和麦克马洪。2016."令人震惊的语言:理解央行沟通的宏观经济效应."国际经济学杂志 99。
s .汉森 m .麦克马洪和 a .普拉特。2018." FOMC 内部的透明度和审议:计算语言学方法."经济学季刊 133:801–870。
哈里斯,Z. 1954。“分布结构。”字10(2/3):146–162。
Hollrah、c . a . s . a . Sharpe 和 N.R. Sinha。2018.“有什么故事?经济预测价值的新视角。”财经讨论系列 2017-107,美联储体系理事会(美国)。
Kalamara,e .,A. Turrell,C. Redl,G. Kapetanios 和 S. Kapadia。2020."让文本有价值:利用报纸文本进行经济预测."英格兰银行员工工作文件第 865 号。
LeNail,A. 2019。" NN-SVG:出版就绪的神经网络架构示意图."开源软件杂志 4 (33)。
t .拉夫兰和 b .麦克唐纳。2011.“什么时候责任不是责任?文本分析、字典和 10-k。”财经杂志66(1):35–65。
Mikolov、K. Chen、G. Corrado 和 J. Dean。2013.“向量空间中单词表示的有效估计.”(arXiv)。
尼马克,K.P .和 s .皮茨纳。2019."新闻媒体与委托信息选择."经济理论杂志181:160–196。
Pennington,R. Socher 和 C. Manning。2014."手套:单词表示的全局向量."2014 年自然语言处理经验方法会议录。多哈:计算语言学协会。1532–1543.
皮茨纳,S. 2020。“企业如何定价?来自公司文件的叙述性证据。”欧洲经济评论 124。
波特,男硕士,1980 年。"后缀剥离算法."程序14(3):130–137。
罗默博士和罗默博士。2004."一种新的货币冲击测量方法:推导和含义."美国经济评论94:1055–1084。
索尔顿 g 还有 M.J .和麦吉尔。1983.*现代信息检索导论。*纽约州纽约市:麦格劳-希尔公司。
夏皮罗和 d .威尔逊。2019."相信美联储的话:使用文本分析评估中央银行目标的新方法."旧金山联邦储备银行工作文件 2019-02。
R.J .席勒,2017。“叙事经济学。”美国经济评论107(4):967–1004。
泰特洛克,第 2007 页。"赋予投资者情绪以内涵:媒体在股票市场中的作用. "财经杂志62(3):1139–1168。
有关经济政策不确定性(EPU)指数的构建和文献现状的概述,请参见 Baker 等人(2016 年)和 Bloom 等人(2019 年)。不同国家的 EPU 指数在 www.policyuncertainty.com
发布和更新。
2
衡量央行声明和财务文件中的情绪是基于文本的数据在经济学中最常见的两种用途。Loughran and McDonald (2011)是最早申请财务申报的公司之一。作为他们工作的副产品,他们引入了金融情绪词典,该词典已在经济学和金融学中广泛使用,包括用于解决央行的问题。阿佩尔和布利克斯·马尔迪(2014)后来推出了一个情绪词典,其中使用了央行特有的术语。
3
例如,参见 Hansen 和 McMahon (2016 年)、Hansen 等人(2018 年)、Acosta (2019 年)和 Armelius 等人(2020 年)。
4
您可以通过以下网址从 EDGAR 进行查询和下载文件: www.sec.gov/edgar/search-and-access
。
5
LM 字典目前可以在以下页面下载: https://sraf.nd.edu/textual-analysis/resources/#LM%20Sentiment%20Word%20Lists
。
6
lda.components_ 返回总和不为 1 的非规范化结果。出于这个原因,我们将它们称为权重,而不是概率。
7
经济和金融研究中最常用的 SEC 文件是 10-K、10-Q 和 8k 文件。10-K 和 10-Q 文件每年和每季度提交,包含大量文本。8k 文件是“新闻稿”,不定期提交以遵守信息披露规则。除了所有权信息,8k 文件是数量最多的,这也是我们选择它们作为这个项目的原因。
8
虽然我们没有在第五章讨论的密集神经网络模型中使用漏失层,但我们通常会在图像相关问题中使用它们来执行正则化。