机器学习算法交易教程第二版(六)
原文:
zh.annas-archive.org/md5/a3861c820dde5bc35d4f0200e43cd519译者:飞龙
第十五章:主题建模 – 总结财经新闻
在上一章中,我们使用了词袋(BOW)模型将非结构化文本数据转换为数字格式。该模型抽象了词序,并将文档表示为词向量,其中每个条目表示令牌对文档的相关性。由此产生的文档-术语矩阵(DTM)—或作为术语-文档矩阵的转置—用于比较文档之间或基于其令牌内容的查询向量的相似性,因此,找到干草堆中的大头针。它提供了有用的功能来对文档进行分类,例如在我们的情感分析示例中。
然而,这种文档模型产生了高维度数据和非常稀疏的数据,但它很少总结内容或接近理解内容是什么。在本章中,我们将使用无监督机器学习从文档中提取隐藏的主题,使用主题建模。这些主题可以以自动化方式为大量文档提供详细的见解。它们非常有用,可以理解干草堆本身,并允许我们基于文档与各种主题的关联度对文档进行标记。
主题模型生成复杂且可解释的文本特征,可成为从大量文档中提取交易信号的第一步。它们加快了文档的审阅,帮助识别和聚类类似的文档,并支持预测建模。
应用程序包括无监督地发现公司披露或收入电话抄本、客户评论或合同中潜在有见地的主题。此外,文档-主题关联有助于通过分配例如情感度量或更直接的后续相关资产收益来进行标记。
更具体地说,阅读完本章后,您将了解:
-
主题建模的演变,它的成就以及为什么它很重要
-
使用潜在语义索引(LSI)降低 DTM 的维度
-
使用概率隐含语义分析(pLSA)提取主题
-
潜在狄利克雷分配(LDA)如何改进 pLSA 成为最流行的主题模型
-
可视化和评估主题建模结果
-
使用 sklearn 和 Gensim 运行 LDA
-
如何将主题建模应用于收入电话和财经新闻文章的集合
您可以在 GitHub 存储库的相应目录中找到此章节的代码示例和其他资源链接。笔记本包括图像的彩色版本。
学习潜在主题 – 目标和方法
主题建模发现了捕捉文档集合中超越个别单词的语义信息的隐藏主题。它旨在解决一个关键挑战,即机器学习算法从文本数据中学习时,超越了“实际写了什么”这个词汇层次,达到了“意图是什么”的语义层次。生成的主题可用于根据其与各种主题的关联情况对文档进行注释。
在实际应用中,主题模型自动总结大量的文档,以便于组织和管理,同时也便于搜索和推荐。同时,它使人类能够理解文档到达了一定程度,以至于人类可以解释主题的描述。
主题模型还缓解了经常困扰 BOW 模型的维度灾难;用高维稀疏向量表示文档可能使相似度度量嘈杂,导致距离测量不准确,并导致文本分类模型的过拟合。
此外,BOW 模型失去了上下文以及语义信息,因为它忽略了单词顺序。它还无法捕捉同义词(多个单词具有相同的含义)或多义性(一个单词具有多个含义)。由于后者,当文档没有按搜索或比较时使用的术语进行索引时,文档检索或相似性搜索可能会失去意义。
BOW 模型的这些缺点引发了一个问题:我们如何从数据中学习到有意义的主题,从而促进与文献数据的更有成效的交互?
主题模型初次尝试改进向量空间模型(于 1970 年代中期开发)时,应用线性代数来降低 DTM 的维度。这种方法类似于我们在第十三章,使用无监督学习进行数据驱动风险因子和资产配置中讨论的主成分分析算法。虽然有效,但没有基准模型很难评估这些模型的结果。作为回应,出现了概率模型,假设存在明确的文档生成过程,并提供算法来反向工程该过程并恢复潜在主题。
以下表格突出了模型演变的关键里程碑,我们将在接下来的章节中更详细地讨论它们:
| 模型 | 年份 | 描述 |
|---|---|---|
| 潜在语义索引(LSI) | 1988 | 通过减少词空间的维度捕捉语义文档-术语关系 |
| 概率潜在语义分析(pLSA) | 1999 | 反向工程一个生成过程,假设单词生成一个主题,文档是主题的混合 |
| 潜在狄利克雷分配(LDA) | 2003 | 为文档添加了一个生成过程:三级分层贝叶斯模型 |
潜在语义索引
潜在语义索引(LSI)—也称为潜在语义分析(LSA)—旨在改进省略了包含查询词同义词的相关文档的查询结果(Dumais 等人,1988 年)。其目标是建模文档与术语之间的关系,以便可以预测术语应与文档关联,即使由于单词使用的变异性,没有观察到这种关联。
LSI 使用线性代数来找到给定数量k的潜在主题,通过分解 DTM。更具体地说,它使用奇异值分解(SVD)来找到使用k个奇异值和向量的最佳低秩 DTM 近似。换句话说,LSI 建立在我们在第十三章,使用无监督学习的数据驱动风险因子和资产配置中遇到的一些降维技术上。作者还尝试过分层聚类,但发现这对于此目的来说太受限制了。
在这种情况下,SVD 识别一组未相关的索引变量或因子,通过其因子值的向量表示每个术语和文档。 图 15.1说明了 SVD 如何将 DTM 分解为三个矩阵:包含正交奇异向量的两个矩阵和具有奇异值的对角矩阵,该奇异值用作缩放因子。
假设输入 DTM 中存在一些相关性,则奇异值会衰减。因此,选择T个最大奇异值会产生原始 DTM 的低维近似,且丢失的信息相对较少。在压缩版本中,原本有N个条目的行或列只有T < N个条目。
DTM 的 LSI 分解可以解释如图 15.1所示:
-
第一个
矩阵表示文档与主题之间的关系。
-
对角矩阵通过其语料库强度对主题进行缩放。
-
第三个矩阵建模了术语-主题关系。
图 15.1:LSI 和 SVD
将第一个两个矩阵相乘产生的矩阵的行对应于原始文档投影到潜在主题空间中的位置。
如何使用 sklearn 实现 LSI
我们将使用上一章介绍的 BBC 文章数据来说明 LSI,因为它们足够小,可以快速训练,并且允许我们将主题分配与类别标签进行比较。有关其他实施细节,请参阅笔记本latent_semantic_indexing。
我们首先加载文档,并创建一个包含 50 篇文章的训练和(分层)测试集。然后,我们使用TfidfVectorizer对数据进行向量化,以获得加权 DTM 计数,并过滤出出现在不到 1%或超过 25%的文档中的词语,以及常见的停用词,以获得大约 2,900 个词汇:
vectorizer = TfidfVectorizer(max_df=.25, min_df=.01,
stop_words='english',
binary=False)
train_dtm = vectorizer.fit_transform(train_docs.article)
test_dtm = vectorizer.transform(test_docs.article)
我们使用 scikit-learn 的TruncatedSVD类,它只计算k个最大的奇异值,以减少 DTM 的维度。确定性的arpack算法提供了一个精确的解,但默认的“随机化”实现对于大矩阵更有效。
我们计算了五个主题以匹配五个类别,这解释了总 DTM 方差的仅 5.4%,因此使用更多主题是合理的:
svd = TruncatedSVD(n_components=5, n_iter=5, random_state=42)
svd.fit(train_dtm)
svd.explained_variance_ratio_.sum()
0.05382357286057269
LSI 为 DTM 确定了一个新的正交基,将排名降低到所需主题的数量。训练过的svd对象的.transform()方法将文档投影到新的主题空间中。这个空间由减少文档向量维度而产生,并且对应于本节前面所示的转换:
train_doc_topics = svd.transform(train_dtm)
train_doc_topics.shape
(2175, 5)
我们可以对文章进行采样,以查看其在主题空间中的位置。我们选择了一个与主题 1 和 2 最(积极)相关的“政治”文章:
i = randint(0, len(train_docs))
train_docs.iloc[i, :2].append(pd.Series(doc_topics[i], index=topic_labels))
Category Politics
Heading What the election should really be about?
Topic 1 0.33
Topic 2 0.18
Topic 3 0.12
Topic 4 0.02
Topic 5 0.06
此示例的主题分配与每个类别的平均主题权重一致,如图 15.2所示(“政治”是最右边的条)。它们说明了 LSI 如何将k个主题表达为k维空间中的方向(笔记本包括每个类别的平均主题分配在二维空间中的投影)。
每个类别都有明确的定义,测试分配与训练分配匹配。但是,权重既有正值又有负值,这使得解释主题更加困难。
图 15.2:训练和测试数据的 LSI 主题权重
我们还可以显示与每个主题最相关的单词(绝对值)。主题似乎捕捉到了一些语义信息,但并没有明显区分(参见图 15.3)。
图 15.3:LSI 主题的前 10 个单词
优缺点
LSI 的优点包括消除噪音和缓解维度诅咒。它还捕获了一些语义方面,如同义词,并通过它们的主题关联来聚类文档和术语。此外,它不需要对文档语言有所了解,并且信息检索查询和文档比较都很容易。
然而,LSI 的结果很难解释,因为主题是具有正值和负值的词向量。此外,没有底层模型可以允许拟合的评估,也没有在选择要使用的维度或主题数量时提供指导。
概率隐含语义分析
概率隐含语义分析(pLSA)以统计视角看待 LSI/LSA,并创建一个生成模型来解决 LSA 缺乏理论基础的问题(Hofmann 2001)。
pLSA 明确地将词 w 出现在文档 d 中的概率建模为条件独立的多项式分布的混合,其中涉及主题 t。
对于词-文档共现的形成,有对称和不对称的两种表述。前者假设单词和文档都是由潜在主题类生成的。相反,不对称模型假设在给定文档的情况下选择主题,并且在给定主题的情况下产生单词。
主题数量是在训练之前选择的超参数,并不是从数据中学习得到的。
图 15.4 中的板块表示法描述了概率模型中的统计依赖关系。更具体地说,它对称编码了刚才描述的不对称模型的关系。每个矩形代表多个项目:外部块代表 M 个文档,而内部阴影矩形象征着每个文档的 N 个单词。我们只观察文档及其内容;模型推断出隐藏或潜在的主题分布:
图 15.4:pLSA 模型的统计依赖关系的板块表示法
现在让我们看看如何在实践中实现这个模型。
如何使用 sklearn 实现 pLSA
pLSA 相当于使用 Kullback-Leibler 散度目标的非负矩阵分解(NMF)。因此,我们可以使用 sklearn.decomposition.NMF 类来实现这个模型,按照 LSI 示例。
使用由 TfidfVectorizer 产生的 DTM 的相同训练-测试拆分,我们这样适配 pLSA:
nmf = NMF(n_components=n_components,
random_state=42,
solver='mu',
beta_loss='kullback-leibler',
max_iter=1000)
nmf.fit(train_dtm)
我们得到了一个重建误差的度量,它是对之前解释的方差度量的替代:
nmf.reconstruction_err_
316.2609400385988
由于其概率性质,pLSA 仅产生正主题权重,这导致了更直接的主题-类别关系,如 图 15.5 所示,适用于测试和训练集:
图 15.5:pLSA 对训练和测试数据的主题权重
我们还注意到,描述每个主题的词列表开始变得更有意义;例如,“娱乐”类别与主题 4 最直接关联,其中包括“电影”,“明星”等词,正如您在图 15.6 中所看到的:
图 15.6:pLSA 的每个主题的前几个词
优点和局限性
使用概率模型的好处是,我们现在可以通过评估它们在训练期间学习的参数给出的新文档的概率来比较不同模型的性能。这也意味着结果具有清晰的概率解释。此外,pLSA 捕捉到了更多的语义信息,包括一词多义。
另一方面,与 LSI 相比,pLSA 增加了计算复杂性,并且该算法可能仅产生局部而不是全局最大值。最后,它不会为新文档产生生成模型,因为它将它们视为给定的。
潜在狄利克雷分配
潜在狄利克雷分配(LDA)通过为主题添加一个生成过程(Blei、Ng 和 Jordan,2003)扩展了 pLSA。它是最流行的主题模型,因为它倾向于生成人类可以关联的有意义的主题,可以将主题分配给新文档,并且是可扩展的。LDA 模型的变体可以包括元数据,如作者或图像数据,或者学习分层主题。
LDA 的工作原理
LDA 是一个假设主题是单词概率分布、文档是主题分布的分层贝叶斯模型。更具体地说,该模型假设主题遵循稀疏狄利克雷分布,这意味着文档仅反映了一小部分主题,而主题仅频繁使用了有限数量的术语。
狄利克雷分布
狄利克雷分布产生可以用作离散概率分布的概率向量。也就是说,它随机生成一定数量的值,这些值为正并总和为一。它有一个正实值参数 ,它控制概率的集中度。值越接近零,意味着只有少数值将为正,并且接收大部分概率质量。图 15.7 说明了
= 0.1 时大小为 10 的三次绘制:
图 15.7:来自狄利克雷分布的三次绘制
笔记本 dirichlet_distribution 包含一个模拟,让您可以尝试不同的参数值。
生成模型
当作者将文章添加到文档集时,LDA 主题模型假定以下生成过程:
-
用由狄利克雷概率定义的比例随机混合一小部分主题。
-
对文本中的每个单词,根据文档-主题概率选择其中一个主题。
-
根据主题的单词列表中的主题-单词概率选择一个词。
因此,文章内容取决于每个主题的权重以及构成每个主题的术语。狄利克雷分布控制文档的主题和主题的词的选择。它编码了一个文档仅涵盖少数主题的想法,而每个主题仅使用少量频繁的单词。
图 15.8 中 LDA 模型的板符号总结了这些关系,并突出显示了关键的模型参数:
图 15.8:LDA 模型的统计依赖关系,以板块符号表示
反向工程过程
生成过程显然是虚构的,但事实证明是有用的,因为它允许恢复各种分布。LDA 算法逆向工程了想象作者的工作,并得出了对文档-主题-词关系进行简洁描述的总结:
-
每个主题对文档的百分比贡献
-
每个单词与主题的概率关联
LDA 解决了从文档体和它们包含的单词中恢复分布的贝叶斯推理问题,通过逆向工程所假定的内容生成过程。 Blei 等人(2003 年)的原始论文使用变分贝叶斯(VB)来近似后验分布。替代方案包括吉布斯采样和期望传播。我们将简要介绍 sklearn 和 Gensim 库的实现。
如何评估 LDA 主题
无监督主题模型不能保证结果是有意义的或可解释的,并且没有客观的度量来评估结果的质量,就像在监督学习中一样。人类主题评估被认为是黄金标准,但可能昂贵,并且不易大规模获得。
更客观地评估结果的两个选项包括困惑度,它在未见文档上评估模型,以及主题连贯性度量,旨在评估所发现模式的语义质量。
困惑度
困惑度,当应用于 LDA 时,衡量模型恢复的主题-词概率分布对未见文本文档样本的预测能力。它基于这个分布p的熵H(p),并针对标记集w计算:
接近零的度量意味着分布在预测样本方面更好。
主题连贯性
主题连贯性衡量主题模型结果的语义一致性,即人类是否会将与主题相关的单词及其概率视为有意义。
为此,它通过测量与主题最相关的单词之间的语义相似度来对每个主题进行评分。更具体地说,连贯性度量基于观察到的定义一个主题的单词集合W的概率。
有两个连贯性度量被设计用于 LDA,并且已经显示与主题质量的人类判断相一致,即 UMass 和 UCI 度量。
UCI 度量(Stevens 等,2012 年)将词对的分数定义为两个不同的(顶部)主题词w[i],w[j]之间的点间互信息(PMI)的和w以及平滑因子的乘积:
概率是根据滑动窗口在外部语料库(如维基百科)上的词共现频率计算的,因此可以将这个度量视为与语义基准的外部比较。
与此相反,UMass 指标(Mimno 等人,2011 年)使用训练语料库中来自多个文档D的共现性来计算一致性得分:
与将模型结果与外部真实值进行比较不同,此度量反映了内在一致性。两种度量方法都经过评估,与人类判断很好地一致(Röder、Both 和 Hinneburg,2015 年)。在这两种情况下,接近零的值意味着主题更一致。
如何使用 sklearn 实现 LDA
我们将像以前一样使用 BBC 数据,并使用 sklearn 的decomposition.LatentDirichletAllocation类训练一个具有五个主题的 LDA 模型(有关参数的详细信息,请参阅 sklearn 文档和笔记本lda_with_sklearn中的实现细节):
lda_opt = LatentDirichletAllocation(n_components=5,
n_jobs=-1,
max_iter=500,
learning_method='batch',
evaluate_every=5,
verbose=1,
random_state=42)
ldat.fit(train_dtm)
LatentDirichletAllocation(batch_size=128, doc_topic_prior=None,
evaluate_every=5, learning_decay=0.7, learning_method='batch',
learning_offset=10.0, max_doc_update_iter=100, max_iter=500,
mean_change_tol=0.001, n_components=5, n_jobs=-1,
n_topics=None, perp_tol=0.1, random_state=42,
topic_word_prior=None, total_samples=1000000.0, verbose=1)
该模型在训练期间跟踪样本内困惑度,并在此度量停止改善时停止迭代。我们可以像往常一样使用 sklearn 对象进行持久化和加载结果:
joblib.dump(lda, model_path / 'lda_opt.pkl')
lda_opt = joblib.load(model_path / 'lda_opt.pkl')
如何使用 pyLDAvis 可视化 LDA 结果
话题可视化有助于使用人类判断评估话题质量。 pyLDAvis 是 LDAvis 的 Python 版本,由 R 和D3.js(Sievert 和 Shirley,2014 年)开发。我们将介绍关键概念;每个 LDA 应用笔记本都包含示例。
pyLDAvis 显示了主题之间的全局关系,同时通过检查与每个单独主题最密切关联的术语以及与每个术语相关联的主题,促进了其语义评估。它还解决了语料库中频繁出现的术语往往支配了定义主题的单词分布的挑战。
为此,LDAVis 引入了术语w对主题t的相关性 r。相关性通过计算两个指标的加权平均值产生了对话题的术语的灵活排名:
-
话题t与术语w的关联程度,表示为条件概率p(w | t)
-
显著性或提升,它衡量了术语w对主题 t 的频率p(w | t)与其在所有文档中的总体频率p(w)的比较
更具体地说,我们可以计算术语w和主题t的相关性r,给定用户定义的权重,如下所示:
该工具允许用户交互地更改以调整相关性,这会更新术语的排名。用户研究发现
产生最合理的结果。
如何使用 Gensim 实现 LDA
Gensim 是一个专门的自然语言处理(NLP)库,具有快速的 LDA 实现和许多附加功能。我们还将在下一章关于词向量的笔记本lda_with_gensim中使用它(有关详细信息,请参阅安装目录中的相关说明)。
我们将由 sklearn 的CountVectorizer或TfIdfVectorizer生成的 DTM 转换为 Gensim 数据结构,如下所示:
train_corpus = Sparse2Corpus(train_dtm, documents_columns=False)
test_corpus = Sparse2Corpus(test_dtm, documents_columns=False)
id2word = pd.Series(vectorizer.get_feature_names()).to_dict()
Gensim 的 LDA 算法包括许多设置:
LdaModel(corpus=None,
num_topics=100,
id2word=None,
distributed=False,
chunksize=2000, # No of doc per training chunk.
passes=1, # No of passes through corpus during training
update_every=1, # No of docs to be iterated through per update
alpha='symmetric',
eta=None, # a-priori belief on word probability
decay=0.5, # % of lambda forgotten when new doc is examined
offset=1.0, # controls slow down of first few iterations.
eval_every=10, # how often estimate log perplexity (costly)
iterations=50, # Max. of iterations through the corpus
gamma_threshold=0.001, # Min. change in gamma to continue
minimum_probability=0.01, # Filter topics with lower probability
random_state=None,
ns_conf=None,
minimum_phi_value=0.01, # lower bound on term probabilities
per_word_topics=False, # Compute most word-topic probabilities
callbacks=None,
dtype=<class 'numpy.float32'>)
Gensim 还提供了一个LdaMulticore模型进行并行训练,可以利用 Python 的多进程功能加快训练速度。
模型训练只需要实例化LdaModel,如下所示:
lda_gensim = LdaModel(corpus=train_corpus,
num_topics=5,
id2word=id2word)
Gensim 评估主题一致性,如前一节所介绍的,并显示每个主题的最重要单词:
coherence = lda_gensim.top_topics(corpus=train_corpus, coherence='u_mass')
我们可以如下显示结果:
topic_coherence = []
topic_words = pd.DataFrame()
for t in range(len(coherence)):
label = topic_labels[t]
topic_coherence.append(coherence[t][1])
df = pd.DataFrame(coherence[t][0], columns=[(label, 'prob'),
(label, 'term')])
df[(label, 'prob')] = df[(label, 'prob')].apply(
lambda x: '{:.2%}'.format(x))
topic_words = pd.concat([topic_words, df], axis=1)
topic_words.columns = pd.MultiIndex.from_tuples(topic_words.columns)
pd.set_option('expand_frame_repr', False)
print(topic_words.head())
这显示了每个主题的顶级单词:
| 主题 1 | 主题 2 | 主题 3 | 主题 4 | 主题 5 | |||||
|---|---|---|---|---|---|---|---|---|---|
| 概率 | 术语 | 概率 | 术语 | 概率 | 术语 | 概率 | 术语 | 概率 | 术语 |
| 0.55% | 在线 | 0.90% | 最佳 | 1.04% | 移动 | 0.64% | 市场 | 0.94% | 劳工 |
| 0.51% | 网站 | 0.87% | 游戏 | 0.98% | 手机 | 0.53% | 增长 | 0.72% | 布莱尔 |
| 0.46% | 游戏 | 0.62% | 玩 | 0.51% | 音乐 | 0.52% | 销售 | 0.72% | 布朗 |
| 0.45% | 净 | 0.61% | 赢 | 0.48% | 电影 | 0.49% | 经济 | 0.65% | 选举 |
| 0.44% | 使用 | 0.56% | 赢 | 0.48% | 使用 | 0.45% | 价格 | 0.57% | 联合 |
图 15.9的左侧面板显示了主题一致性分数,突显了主题质量的衰减(至少部分是由于相对较小的数据集):
图 15.9:主题一致性和测试集分配
右侧面板显示了我们训练模型的 50 篇文章的测试集的评估。模型对四个错误,准确率为 92%。
对盈利电话中讨论的主题进行建模
在第三章,金融的替代数据 - 类别和用例中,我们学习了如何从 SeekingAlpha 网站抓取盈利电话数据。在本节中,我们将使用这个数据源进行主题建模。我使用了 2018 年至 2019 年之间的约 700 份盈利电话转录样本。这是一个相当小的数据集;对于实际应用,我们需要一个更大的数据集。
目录earnings_calls中包含了本节中使用的代码示例的多个文件。有关加载、探索和预处理数据的详细信息,请参阅笔记本lda_earnings_calls,以及用于描述下一步实验的run_experiments.py文件。
数据预处理
转录包括公司代表的个别声明,操作员和分析师的问答环节。我们将这些声明中的每一条都视为单独的文档,忽略操作员的声明,以获取 32,047 个项目,平均字数和中位数分别为 137 和 62:
documents = []
for transcript in earnings_path.iterdir():
content = pd.read_csv(transcript / 'content.csv')
documents.extend(content.loc[(content.speaker!='Operator') & (content.content.str.len() > 5), 'content'].tolist())
len(documents)
32047
我们使用 spaCy 对这些文档进行预处理,如第十三章,使用无监督学习进行数据驱动的风险因素和资产配置中所示(参考笔记本),并将清理和词形还原后的文本存储为一个新的文本文件。
如 图 15.10 所示,探索最常见的标记揭示出领域特定的停用词,如 "year" 和 "quarter",我们在第二步中去除,同时过滤掉少于 10 个词的语句,剩余约 22,582 个。
图 15.10:最常见的收益电话标记
模型训练和评估
为了说明,我们创建了一个包含出现在 0.5 到 25% 文档中的术语的 DTM,结果为 1,529 个特征。现在我们继续使用 25 个语料库训练 15 个主题模型。在 4 核 i7 上,这需要两分钟多一点。
如 图 15.11 所示,每个主题的前 10 个词识别出几个明显的主题,从明显的财务信息到临床试验(主题 5)、中国和关税问题(主题 9)以及技术问题(主题 11)。
图 15.11:收益电话主题中最重要的词语
使用 pyLDAvis 的相关度指标,将无条件频率相对于提升的权重设置为 0.6,主题定义变得更加直观,如 图 15.12 所示,关于中国和贸易战的第 7 个主题:
图 15.12:pyLDAVis 的交互式主题探索器
该笔记本还说明了如何根据主题关联查找文档。在这种情况下,分析师可以审查相关陈述以了解细微差别,使用情感分析进一步处理特定主题的文本数据,或者根据市场价格派生标签。
运行实验
为了说明不同参数设置的影响,我们运行了几百个实验,针对不同的 DTM 约束和模型参数。更具体地,我们让 min_df 和 max_df 参数分别从 50-500 个词和 10 到 100% 的文档变化,交替使用二进制和绝对计数。然后,我们使用 1 和 25 个语料库训练 LDA 模型,主题从 3 到 50 个。
图 15.13 中的图表以主题连贯性(较高为较好)和困惑度(较低为较好)的形式呈现了结果。连贯性在 25-30 个主题后下降,困惑度同样增加。
图 15.13:LDA 超参数设置对主题质量的影响
笔记本中包含量化参数与结果之间关系的回归结果。我们通常使用绝对计数和较小的词汇量能获得更好的结果。
与财经新闻相关的主题建模
笔记本 lda_financial_news 包含了应用于 2018 年前五个月超过 306,000 篇财经新闻文章的 LDA 示例。这些数据集已经发布在 Kaggle 上,文章来源于 CNBC、路透社、华尔街日报等。笔记本包含下载说明。
我们根据文章标题选择了最相关的 120,000 篇文章,共计 5400 万个标记,平均每篇文章 429 个单词。为了为 LDA 模型准备数据,我们依赖 spaCy 来删除数字和标点,并对结果进行词形还原。
图 15.14 突出显示了剩余的最常见标记和文章长度分布,其中中位数长度为 231 个标记;第 90 百分位是 642 个单词。
图 15.14:金融新闻数据的语料库统计
在 图 15.15 中,我们展示了一个使用 3,570 个标记的词汇表的模型的结果,基于 min_df=0.005 和 max_df=0.1,采用单次遍历以避免长时间训练 15 个主题。我们可以使用训练后的 LdaModel 的 top_topics 属性来获取每个主题最可能的词(详细信息请参阅笔记本)。
图 15.15:金融新闻主题的前 15 个词
这些主题概述了与时期相关的几个问题,包括 Brexit(主题 8)、朝鲜(主题 4)和特斯拉(主题 14)。
Gensim 提供了 LdaMultiCore 实现,允许使用 Python 的多进程模块进行并行训练,并且在使用四个工作线程时性能提高了 50%。但是,由于 I/O 瓶颈,使用更多工作线程并不会进一步减少训练时间。
摘要
在本章中,我们探讨了使用主题建模来深入了解大量文档内容的用途。我们涵盖了使用 DTM 的降维技术将文档投射到潜在主题空间中的潜在语义索引。虽然在解决由高维单词向量引起的维度灾难方面很有效,但它并不捕捉太多语义信息。概率模型对文档、主题和单词之间的相互作用做出了明确的假设,允许算法逆向工程文档生成过程,并在新文档上评估模型拟合度。我们了解到 LDA 能够提取出合理的主题,让我们以自动化的方式对大量文本获得高层次的理解,同时以有针对性的方式识别相关文档。
在下一章中,我们将学习如何训练神经网络,将单词嵌入到一个捕捉重要语义信息的高维向量空间中,并且可以使用生成的单词向量作为高质量的文本特征。
第十六章:用于收益电话和 SEC 申报的词嵌入
在前两章中,我们使用了词袋模型将文本数据转换为数值格式。结果是稀疏的、固定长度的向量,表示高维度的词空间中的文档。这使得可以评估文档的相似性,并创建特征来训练模型,以便分类文档的内容或对其中表达的情感进行评分。然而,这些向量忽略了术语使用的上下文,因此,即使两个句子包含相同的单词但顺序不同,它们也将由相同的向量编码,即使它们的含义完全不同。
本章介绍了一类使用神经网络来学习词或段落的向量表示的替代算法。这些向量是密集的而不是稀疏的,具有几百个实值条目,并称为嵌入,因为它们在连续向量空间中为每个语义单元分配一个位置。它们是通过训练模型来从上下文预测标记而产生的,因此类似的用法暗示了类似的嵌入向量。此外,嵌入通过它们的相对位置传达了词之间的关系等语义方面的信息。因此,它们是解决需要语义信息的深度学习模型任务的强大特征,例如机器翻译、问答或维护对话。
要基于文本数据制定交易策略,我们通常对文档的含义感兴趣,而不是单个标记。例如,我们可能希望创建一个数据集,其中包含代表推文或新闻文章的特征,并带有情感信息(参见第十四章,用于交易的文本数据 - 情感分析),或者是发布后给定时段内资产的收益。尽管词袋模型在对文本数据进行编码时丢失了大量信息,但它的优点在于表示了整个文档。然而,词嵌入已经进一步发展,以表示不止单个标记。例如,doc2vec扩展采用了加权词嵌入。最近,注意力机制出现了,用于产生更具上下文敏感性的句子表示,从而导致了变压器架构,例如BERT模型系列,在许多自然语言任务上性能显著提高。
具体来说,在完成本章和伴随的笔记本后,您将了解以下内容:
-
什么是词嵌入,它们如何工作以及为什么它们捕获语义信息
-
如何获取和使用预训练的词向量
-
哪些网络架构最有效地训练 word2vec 模型
-
如何使用 Keras、Gensim 和 TensorFlow 训练 word2vec 模型
-
可视化和评估词向量的质量
-
如何训练一个 word2vec 模型来预测 SEC 申报对股价走势的影响
-
doc2vec 如何扩展 word2vec 并用于情感分析
-
为什么 transformer 的注意力机制对自然语言处理产生了如此大的影响
-
如何在金融数据上微调预训练的 BERT 模型并提取高质量的嵌入
您可以在本章的 GitHub 目录中找到代码示例和额外资源的链接。本章使用神经网络和深度学习;如果不熟悉,您可能需要先阅读第十七章,交易的深度学习,介绍了关键概念和库。
单词嵌入如何编码语义
词袋模型将文档表示为稀疏的、高维度的向量,反映了它们包含的标记。而词嵌入则将标记表示为稠密、低维度的向量,以便于单词的相对位置反映它们在上下文中的使用方式。它们体现了语言学中的分布假设,该假设认为单词最好是通过其周围的上下文来定义。
单词向量能够捕捉许多语义方面;不仅会为同义词分配附近的嵌入,而且单词可以具有多个相似度程度。例如,“driver”一词可能类似于“motorist”或“factor”。此外,嵌入还编码了词对之间的关系,如类比(东京是日本,巴黎是法国,或者went 是 go 的过去式,saw 是 see 的过去式),我们将在本节后面进行说明。
嵌入是通过训练神经网络来预测单词与其上下文的关系或反之得到的。在本节中,我们将介绍这些模型的工作原理,并介绍成功的方法,包括 word2vec、doc2vec 以及更近期的 transformer 系列模型。
神经语言模型如何学习上下文中的用法
单词嵌入来自于训练一个浅层神经网络来预测给定上下文的单词。而传统的语言模型将上下文定义为目标单词之前的单词,而单词嵌入模型使用包围目标的对称窗口中的单词。相比之下,词袋模型使用整个文档作为上下文,并依赖(加权的)计数来捕捉单词的共现关系。
早期的神经语言模型使用了增加了计算复杂度的非线性隐藏层。而由 Mikolov、Sutskever 等人(2013)介绍的 word2vec 及其扩展简化了架构,使其能够在大型数据集上进行训练。例如,维基百科语料库包含超过 20 亿个标记。(有关前馈网络的详细信息,请参阅第十七章,交易的深度学习。)
word2vec – 可扩展的单词和短语嵌入
word2vec 模型是一个两层的神经网络,它以文本语料库作为输入,并为该语料库中的单词输出一组嵌入向量。有两种不同的架构,如下图所示,以有效地使用浅层神经网络学习单词向量(Mikolov、Chen 等,2013):
-
连续词袋(CBOW)模型使用上下文词向量的平均值作为输入来预测目标词,因此它们的顺序并不重要。CBOW 训练速度更快,对于频繁出现的术语可能略微更准确,但对不常见的词注意力较少。
-
相反,跳字(SG)模型使用目标词来预测从上下文中采样的词。它在小型数据集上表现良好,并且即使对于罕见的词或短语也能找到良好的表示。
图 16.1:连续词袋与跳字处理逻辑
模型接收一个嵌入向量作为输入,并与另一个嵌入向量计算点积。请注意,假设向量已被规范化,则当向量相等时点积被最大化(绝对值),当它们正交时则被最小化。
在训练期间,反向传播算法根据基于分类错误的目标函数计算的损失调整嵌入权重。我们将在下一节中看到 word2vec 如何计算损失。
训练通过在文档上滑动上下文窗口进行,通常被分段为句子。对语料库的每次完整迭代称为一个时代。根据数据,可能需要几十个时代才能使向量质量收敛。
跳字模型隐式因式分解一个包含各个单词和上下文对的点间互信息的词-上下文矩阵(Levy 和 Goldberg,2014)。
模型目标 – 简化 softmax
Word2vec 模型旨在预测一个潜在非常庞大的词汇中的单个词。神经网络通常在最后一层使用 softmax 函数作为输出单元实现多类目标,因为它将任意数量的实值映射到相等数量的概率。softmax 函数定义如下,其中 h 指代嵌入,v 指代输入向量,c 是单词 w 的上下文:
然而,由于 softmax 的复杂度与类别数量成比例,因为分母需要计算整个词汇表中所有单词的点积以标准化概率。Word2vec 通过使用 softmax 的修改版本或基于采样的逼近来提高效率:
-
分层 softmax 将词汇组织为具有单词作为叶节点的二叉树。到达每个节点的唯一路径可用于计算单词概率(Morin 和 Bengio,2005)。
-
噪声对比估计(NCE)对上下文之外的“噪声词”进行采样,并将多类任务近似为二元分类问题。随着样本数量的增加,NCE 导数逼近 softmax 梯度,但仅需 25 个样本即可获得与 softmax 相似的收敛速度增加 45 倍的收敛速度(Mnih 和 Kavukcuoglu,2013)。
-
负采样(NEG)省略了噪声词样本以近似 NCE,并直接最大化目标词的概率。因此,NEG 优化了嵌入向量的语义质量(相似用法的相似向量),而不是在测试集上的准确性。然而,与分层 softmax 目标(Mikolov 等人,2013 年)相比,它可能产生较少频繁词的较差表示。
自动化短语检测
预处理通常涉及短语检测,即识别通常一起使用并应该接收单个向量表示的标记(例如,纽约市;参见第十三章,数据驱动的风险因素和无监督学习的资产配置中的 n-gram 讨论)。
原始 word2vec 作者(Mikolov 等人,2013 年)使用一种简单的提升评分方法,如果两个词w[i],w[j]的联合出现超过给定的阈值相对于每个词的个别出现,通过折扣因子δ进行校正,则将它们识别为一个二元组:
评分器可以反复应用以识别连续更长的短语。
另一种选择是标准化的逐点互信息分数,这更准确,但计算成本更高。它使用相对词频P(w),在+1 和-1 之间变化:
使用语义算术评估嵌入
词袋模型创建反映标记在文档中存在和相关性的文档向量。正如第十五章,主题建模 - 总结财经新闻中所讨论的那样,潜在语义分析减少了这些向量的维度,并在此过程中确定了可以解释为潜在概念的内容。潜在狄利克雷分配将文档和术语表示为包含潜在主题权重的向量。
word2vec 产生的单词和短语向量没有明确的含义。但是,嵌入将类似的用法编码为模型创建的潜在空间中的接近。嵌入还捕捉语义关系,以便通过添加和减去单词向量来表示类比。
图 16.2显示了从“巴黎”指向“法国”的向量(它测量它们嵌入向量之间的差异)如何反映“首都”的关系。伦敦与英国之间的类似关系对应于相同的向量:术语“英国”的嵌入非常接近通过将“首都”的向量添加到术语“伦敦”的嵌入中获得的位置:
图 16.2:嵌入向量算术
正如单词可以在不同的上下文中使用一样,它们可以以不同的方式与其他单词相关联,而这些关系对应于潜在空间中的不同方向。因此,如果训练数据允许,嵌入应反映出几种类型的类比。
word2vec 的作者提供了一个包含 14 个类别的超过 25,000 个关系列表,涵盖地理、语法和句法以及家庭关系的方面,以评估嵌入向量的质量。正如前面的图表所示,该测试验证了目标词“UK”与添加代表类似关系“巴黎:法国”的向量到目标的补集“伦敦”之间的最近距离。
以下表格显示了示例数,并说明了一些类比类别。该测试检查* d 的嵌入距离 c +(b-a)*确定的位置有多接近。有关实现细节,请参阅evaluating_embeddings笔记本。
| 类别 | # 示例 | a | b | c | d |
|---|---|---|---|---|---|
| 首都-国家 | 506 | 雅典 | 希腊 | 巴格达 | 伊拉克 |
| 城市-州 | 4,242 | 芝加哥 | 伊利诺伊州 | 休斯顿 | 德克萨斯州 |
| 过去时 | 1,560 | 跳舞 | 跳舞 | 减少 | 减少 |
| 复数 | 1,332 | 香蕉 | 香蕉 | 鸟 | 鸟类 |
| 比较 | 1,332 | 坏 | 更坏 | 大 | 更大 |
| 相反 | 812 | 可接受 | 不可接受 | 察觉到 | 未察觉到 |
| 最高级 | 1,122 | 坏 | 最坏 | 大 | 最大 |
| 复数(动词) | 870 | 减少 | 减少 | 描述 | 描述 |
| 货币 | 866 | 阿尔及利亚 | 阿尔及利亚第纳尔 | 安哥拉 | 安哥拉宽扎 |
| 家庭 | 506 | 男孩 | 女孩 | 兄弟 | 姐妹 |
与其他无监督学习技术类似,学习嵌入向量的目标是为其他任务生成特征,例如文本分类或情感分析。有几种获得给定文档语料库的嵌入向量的选项:
-
使用从通用大语料库(如维基百科或 Google 新闻)学习的预训练嵌入
-
使用反映感兴趣领域的文档来训练您自己的模型
后续文本建模任务的内容越不通用和更专业化,第二种方法就越可取。然而,高质量的单词嵌入需要数据丰富的信息性文档,其中包含数亿字的单词。
首先,我们将看看如何使用预训练向量,然后演示如何使用金融新闻和 SEC 备案数据构建自己的 word2vec 模型的示例。
如何使用预训练单词向量
有几个预训练单词嵌入的来源。流行的选项包括斯坦福的 GloVE 和 spaCy 内置的向量(有关详细信息,请参阅using_pretrained_vectors笔记本)。在本节中,我们将重点放在 GloVe 上。
GloVe – 用于单词表示的全局向量
GloVe(全球单词表示向量,Pennington、Socher 和 Manning,2014)是斯坦福 NLP 实验室开发的无监督算法,它从聚合的全局词-词共现统计中学习单词的向量表示(请参阅 GitHub 上链接的资源)。可用以下网络规模的预训练向量:
-
Common Crawl,包含 420 亿或 840 亿令牌和 190 万或 220 万令牌的词汇量
-
维基百科 2014 + Gigaword 5,拥有 60 亿个标记和 40 万个标记的词汇表
-
Twitter 使用了 20 亿条推文,27 亿个标记,以及一个包含 120 万个标记的词汇表。
我们可以使用 Gensim 将文本文件转换为向量,使用 glove2word2vec 然后将它们加载到 KeyedVector 对象中:
from gensim.models import Word2Vec, KeyedVectors
from gensim.scripts.glove2word2vec import glove2word2vec
glove2word2vec(glove_input_file=glove_file, word2vec_output_file=w2v_file)
model = KeyedVectors.load_word2vec_format(w2v_file, binary=False)
Gensim 使用了前面描述的 word2vec 类比测试,使用了作者提供的文本文件来评估词向量。为此,该库具有 wv.accuracy 函数,我们使用它传递类比文件的路径,指示词向量是否以二进制格式存储,以及是否要忽略大小写。我们还可以将词汇表限制为最常见的以加快测试速度。
accuracy = model.wv.accuracy(analogies_path,
restrict_vocab=300000,
case_insensitive=True)
在维基百科语料库上训练的词向量覆盖了所有类比,并在各类别之间存在一定的变化,总体准确率为 75.44%:
| 类别 | # 样本 | 准确度 | 类别 | # 样本 | 准确度 | |
|---|---|---|---|---|---|---|
| 国家首都 | 506 | 94.86% | 比较级 | 1,332 | 88.21% | |
| 非洲和南美洲首都 | 8,372 | 96.46% | 对立词 | 756 | 28.57% | |
| 城市-州份 | 4,242 | 60.00% | 最高级 | 1,056 | 74.62% | |
| 货币 | 752 | 17.42% | 现在分词 | 1,056 | 69.98% | |
| 家庭 | 506 | 88.14% | 过去时 | 1,560 | 61.15% | |
| 国籍 | 1,640 | 92.50% | 复数 | 1,332 | 78.08% | |
| 形容词-副词 | 992 | 22.58% | 复数动词 | 870 | 58.51% |
图 16.3 比较了三种 GloVe 源在 10 万个最常见标记上的性能。它显示了 Common Crawl 向量的准确度略高,达到了 78%,覆盖了约 80% 的类比。而 Twitter 向量的覆盖率仅为 25%,准确度为 56.4%。
图 16.3: GloVe 在 word2vec 类比上的准确度
图 16.4 将在维基百科语料库上训练的 word2vec 模型的 300 维嵌入投影到两个维度上使用 PCA,测试了来自以下类别的 24,400 个以上的类比,准确率超过 73.5%:
图 16.4: 选定类比嵌入的二维可视化
自定义金融新闻嵌入
许多任务需要领域特定词汇的嵌入,而预训练于通用语料库的模型可能无法捕捉到这些词汇。标准的 word2vec 模型无法为词汇表中不存在的词汇分配向量,而是使用一个默认向量,降低了它们的预测价值。
例如,在处理行业特定文件时,词汇表或其用法可能随着新技术或产品的出现而发生变化。因此,嵌入也需要相应地演变。此外,像公司收益发布这样的文件使用了细微的语言,预训练于维基百科文章的 GloVe 向量可能无法正确反映这些语言特点。
在本节中,我们将使用金融新闻训练和评估领域特定的嵌入。我们首先展示了如何为这项任务预处理数据,然后演示了第一节中概述的 skip-gram 架构的工作原理,并最终可视化结果。我们还将介绍替代的更快的训练方法。
预处理 - 句子检测和 n-gram
为了说明 word2vec 网络架构,我们将使用包含超过 12 万 5 千篇相关文章的金融新闻数据集,该数据集我们在第十五章 主题建模 - 总结金融新闻 中介绍了。我们将按照该章节中的 lda_financial_news.ipynb 笔记本中概述的方式加载数据。financial_news_preprocessing.ipynb 笔记本包含了本节的代码示例。
我们使用 spaCy 内置的句子边界检测来将每篇文章分割成句子,去除较少信息的项目,例如数字和标点符号,并保留结果(如果长度在 6 到 99 个标记之间):
def clean_doc(d):
doc = []
for sent in d.sents:
s = [t.text.lower() for t in sent if not
any([t.is_digit, not t.is_alpha, t.is_punct, t.is_space])]
if len(s) > 5 or len(sent) < 100:
doc.append(' '.join(s))
return doc
nlp = English()
sentencizer = nlp.create_pipe("sentencizer")
nlp.add_pipe(sentencizer)
clean_articles = []
iter_articles = (article for article in articles)
for i, doc in enumerate(nlp.pipe(iter_articles, batch_size=100, n_process=8), 1):
clean_articles.extend(clean_doc(doc))
我们最终得到了 243 万个句子,平均每个句子包含 15 个标记。
接下来,我们创建 n-gram 来捕捉复合术语。 Gensim 允许我们根据组件的联合与个别出现的相对频率来识别 n-gram。Phrases 模块对标记进行评分,并且 Phraser 类相应地转换文本数据。
它将我们的句子列表转换为一个新的数据集,我们可以按如下方式写入文件:
sentences = LineSentence((data_path / f'articles_clean.txt').as_posix())
phrases = Phrases(sentences=sentences,
min_count=10, # ignore terms with a lower count
threshold=0.5, # only phrases with higher score
delimiter=b'_', # how to join ngram tokens
scoring='npmi') # alternative: default
grams = Phraser(phrases)
sentences = grams[sentences]
with (data_path / f'articles_ngrams.txt').open('w') as f:
for sentence in sentences:
f.write(' '.join(sentence) + '\n')
该笔记本演示了如何使用 2-gram 文件作为输入来重复此过程以创建 3-gram。我们最终得到了大约 2 万 5 千个 2-gram 和 1 万 5 千个 3-或 4-gram。检查结果显示,得分最高的术语是公司或个人的名称,这表明我们可能需要加强我们的初始清洁标准。有关数据集的其他详细信息,请参阅笔记本。
TensorFlow 2 中的 skip-gram 架构
在本节中,我们将演示如何使用 TensorFlow 2 的 Keras 接口构建一个 word2vec 模型,我们将在下一章中更详细地介绍。financial_news_word2vec_tensorflow 笔记本包含了代码示例和其他实现细节。
我们首先对文档进行标记化,并为词汇表中的每个项目分配唯一的 ID。首先,我们从上一节创建的句子中抽样一部分来限制训练时间:
SAMPLE_SIZE=.5
sentences = file_path.read_text().split('\n')
words = ' '.join(np.random.choice(sentences, size=int(SAMLE_SIZE* l en(sentences)), replace=False)).split()
我们需要至少 10 次出现在语料库中,保留包含 31,300 个标记的词汇表,并从以下步骤开始:
-
提取前 n 个最常见的单词以学习嵌入。
-
使用唯一整数索引这些 n 个单词。
-
创建一个
{index: word}字典。 -
用它们的索引替换 n 个词,并在其他地方使用虚拟值
'UNK':# Get (token, count) tuples for tokens meeting MIN_FREQ MIN_FREQ = 10 token_counts = [t for t in Counter(words).most_common() if t[1] >= MIN_FREQ] tokens, counts = list(zip(*token_counts)) # create id-token dicts & reverse dicts id_to_token = pd.Series(tokens, index=range(1, len(tokens) + 1)).to_dict() id_to_token.update({0: 'UNK'}) token_to_id = {t:i for i, t in id_to_token.items()} data = [token_to_id.get(word, 0) for word in words]
我们最终得到了 1740 万个标记和接近 6 万个标记的词汇表,包括长达 3 个词的组合。词汇表涵盖了大约 72.5% 的类比。
噪声对比估计 - 创建验证样本
Keras 包括一个 make_sampling_table 方法,允许我们创建一个训练集,其中包含上下文和噪声词的一对对应标记,根据它们的语料库频率进行采样。较低的因子会增加选择不常见词汇的概率;笔记本中的图表显示,0.1 的值将采样限制在前 10,000 个标记:
SAMPLING_FACTOR = 1e-4
sampling_table = make_sampling_table(vocab_size,
sampling_factor=SAMPLING_FACTOR)
生成目标-上下文词对
要训练我们的模型,我们需要一对一对的标记,其中一个代表目标,另一个从周围上下文窗口中选择,如之前在 图 16.1 的右侧面板中所示。我们可以使用 Keras 的 skipgrams() 函数如下所示:
pairs, labels = skipgrams(sequence=data,
vocabulary_size=vocab_size,
window_size=WINDOW_SIZE,
sampling_table=sampling_table,
negative_samples=1.0,
shuffle=True)
结果是 1.204 亿个上下文-目标对,正负样本均匀分布。负样本是根据我们在前一步中创建的 sampling_table 概率生成的。前五个目标和上下文词 ID 与它们匹配的标签如下所示:
pd.DataFrame({'target': target_word[:5],
'context': context_word[:5],
'label': labels[:5]})
target context label
0 30867 2117 1
1 196 359 1
2 17960 32467 0
3 314 1721 1
4 28387 7811 0
创建 word2vec 模型层
word2vec 模型包括以下内容:
-
一个接收表示目标-上下文对的两个标量值的输入层
-
一个共享的嵌入层,计算目标和上下文词的向量的点积
-
一个 sigmoid 输出层
输入层 有两个组件,一个用于目标-上下文对的每个元素:
input_target = Input((1,), name='target_input')
input_context = Input((1,), name='context_input')
共享的嵌入层 包含一个向量,每个词汇元素根据目标和上下文标记的索引进行选择:
embedding = Embedding(input_dim=vocab_size,
output_dim=EMBEDDING_SIZE,
input_length=1,
name='embedding_layer')
target = embedding(input_target)
target = Reshape((EMBEDDING_SIZE, 1), name='target_embedding')(target)
context = embedding(input_context)
context = Reshape((EMBEDDING_SIZE, 1), name='context_embedding')(context)
输出层 通过它们的点积测量两个嵌入向量的相似性,并使用 sigmoid 函数转换结果,我们在 第七章,线性模型——从风险因素到收益预测 中讨论逻辑回归时遇到过这个函数:
# similarity measure
dot_product = Dot(axes=1)([target, context])
dot_product = Reshape((1,), name='similarity')(dot_product)
output = Dense(units=1, activation='sigmoid', name='output')(dot_product)
这个 skip-gram 模型包含一个 200 维的嵌入层,每个词汇项都将假设不同的值。结果是,我们得到了 59,617 x 200 个可训练参数,再加上两个用于 sigmoid 输出的参数。
在每次迭代中,模型计算上下文和目标嵌入向量的点积,将结果通过 sigmoid 传递以产生概率,并根据损失的梯度调整嵌入。
使用 TensorBoard 可视化嵌入
TensorBoard 是一个可视化工具,允许将嵌入向量投影到二维或三维空间中,以探索单词和短语的位置。在加载我们创建的嵌入元数据文件后(参考笔记本),您还可以搜索特定术语以查看并探索其邻居,使用 UMAP、t-SNE 或 PCA 将其投影到二维或三维空间中(参见 第十三章,使用无监督学习进行数据驱动的风险因素和资产配置)。有关以下截图的更高分辨率彩色版本,请参考笔记本:
图 16.5:3D 嵌入和元数据可视化
如何使用 Gensim 更快地训练嵌入
TensorFlow 的实现在其体系结构方面非常透明,但速度不是特别快。自然语言处理(NLP)库 Gensim,我们在上一章中也用于主题建模,提供了更好的性能,并且更接近原始作者提供的基于 C 的 word2vec 实现。
使用非常简单。我们首先创建一个句子生成器,它只需将我们在预处理步骤中生成的文件名作为输入(我们将再次使用 3-grams):
sentence_path = data_path / FILE_NAME
sentences = LineSentence(str(sentence_path))
在第二步中,我们配置 word2vec 模型,使用熟悉的参数,涉及嵌入向量和上下文窗口的大小,最小标记频率以及负样本数量等:
model = Word2Vec(sentences,
sg=1, # set to 1 for skip-gram; CBOW otherwise
size=300,
window=5,
min_count=20,
negative=15,
workers=8,
iter=EPOCHS,
alpha=0.05)
在现代 4 核 i7 处理器上,一次训练大约需要 2 分钟。
我们可以持久化模型和词向量,或仅持久化词向量,如下所示:
# persist model
model.save(str(gensim_path / 'word2vec.model'))
# persist word vectors
model.wv.save(str(gensim_path / 'word_vectors.bin'))
我们可以验证模型性能,并继续训练,直到对结果满意为止:
model.train(sentences, epochs=1, total_examples=model.corpus_count)
在这种情况下,再训练六个额外的周期会产生最佳结果,所有涵盖词汇表的类比的准确率为 41.75%。图 16.6的左侧面板显示了正确/错误的预测和每个类别的准确度分布。
Gensim 还允许我们评估自定义语义代数。我们可以检查流行的"woman"+"king"-"man" ~ "queen"示例如下:
most_sim = best_model.wv.most_similar(positive=['woman', 'king'], negative=['man'], topn=10)
图中的右侧面板显示,“queen”是第三个标记,紧随“monarch”和不太明显的“lewis”之后,然后是几个王室成员:
图 16.6:类别和特定示例的类比准确度
我们还可以评估与给定目标最相似的标记,以更好地理解嵌入特征。我们基于对数语料库频率进行随机选择:
counter = Counter(sentence_path.read_text().split())
most_common = pd.DataFrame(counter.most_common(), columns=['token', 'count'])
most_common['p'] = np.log(most_common['count'])/np.log(most_common['count']).sum()similars = pd.DataFrame()
for token in np.random.choice(most_common.token, size=10, p=most_common.p):
similars[token] = [s[0] for s in best_model.wv.most_similar(token)]
下表举例说明了包含多个 n-gram 的结果:
| 目标 | 最接近的匹配 | ||||
|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | |
| 档案 | 概要 | 用户 | 政治顾问剑桥分析 | 复杂 | |
| 减持 | 转让 | 收购 | 接管 | 拜耳 | 合并 |
| 就绪性 | 训练 | 军事 | 指挥 | 空军 | 准备 |
| 军火库 | 核武器 | 俄罗斯 | 弹道导弹 | 武器 | 黎巴嫩真主党 |
| 供应中断 | 中断 | 原材料 | 中断 | 价格 | 下降 |
我们现在将继续开发一个与实际交易更密切相关的应用程序,使用 SEC 文件。
用于利用 SEC 文件进行交易的 word2vec
在本节中,我们将使用 Gensim 从年度 SEC 文件中学习单词和短语向量,以说明词嵌入对算法交易的潜在价值。在接下来的章节中,我们将将这些向量与价格回报结合为特征,训练神经网络从安全文件的内容中预测股票价格。
具体来说,我们将使用一个包含来自2013-2016 年间的超过 22,000 份 10-K 年度报告的数据集,这些报告由超过 6,500 家上市公司提交,并包含财务信息和管理评论(请参阅第二章,市场和基本数据-来源和技术)。
对应于 11,000 份备案的大约 3,000 家公司,我们有股价数据来标记用于预测建模的数据。(在sec-filings文件夹中的sec_preprocessing笔记本中查看数据源详细信息和下载说明以及预处理代码示例。)
预处理-句子检测和 n-grams
每个备案都是一个单独的文本文件,主索引包含备案元数据。我们提取了最具信息量的部分,即:
-
项目 1 和 1A:业务和风险因素
-
项目 7:管理层讨论
-
项目 7a:关于市场风险的披露
sec_preprocessing笔记本展示了如何使用 spaCy 解析和标记文本,类似于第十四章中的方法。我们不对标记进行词形还原,以保留单词用法的细微差别。
自动短语检测
与前一节一样,我们使用 Gensim 来检测由多个标记组成的短语,或 n-grams。笔记本显示,最常见的二元组包括common_stock,united_states,cash_flows,real_estate和interest_rates。
我们最终得到了一个词汇表,其中包含略多于 201,000 个标记,中位数频率为 7,表明我们可以通过增加训练 word2vec 模型时的最小频率来消除相当大的噪音。
使用回报标记备案以预测盈利惊喜
数据集附带了与这 10,000 个文件相关的股票代码和备案日期的列表。我们可以利用这些信息选择围绕备案发布期间的某一时期的股价。目标是训练一个使用给定备案的词向量作为输入来预测备案后回报的模型。
下面的代码示例显示了如何使用 1 个月的回报来标记单个备案:
with pd.HDFStore(DATA_FOLDER / 'assets.h5') as store:
prices = store['quandl/wiki/prices'].adj_close
sec = pd.read_csv('sec_path/filing_index.csv').rename(columns=str.lower)
sec.date_filed = pd.to_datetime(sec.date_filed)
sec = sec.loc[sec.ticker.isin(prices.columns), ['ticker', 'date_filed']]
price_data = []
for ticker, date in sec.values.tolist():
target = date + relativedelta(months=1)
s = prices.loc[date: target, ticker]
price_data.append(s.iloc[-1] / s.iloc[0] - 1)
df = pd.DataFrame(price_data,
columns=['returns'],
index=sec.index)
当我们在接下来的章节中使用深度学习架构时,我们将回到这一点。
模型训练
gensim.models.word2vec类实现了先前介绍的 skip-gram 和 CBOW 架构。笔记本word2vec包含了额外的实现细节。
为了促进内存高效的文本摄取,LineSentence类从提供的文本文件中创建一个包含在单个句子中的生成器:
sentence_path = Path('data', 'ngrams', f'ngrams_2.txt')
sentences = LineSentence(sentence_path)
Word2Vec类提供了本章中介绍的配置选项:
model = Word2Vec(sentences,
sg=1, # 1=skip-gram; otherwise CBOW
hs=0, # hier. softmax if 1, neg. sampling if 0
size=300, # Vector dimensionality
window=3, # Max dist. btw target and context word
min_count=50, # Ignore words with lower frequency
negative=10, # noise word count for negative sampling
workers=8, # no threads
iter=1, # no epochs = iterations over corpus
alpha=0.025, # initial learning rate
min_alpha=0.0001 # final learning rate
)
笔记本显示了如何持久保存和重新加载模型以继续训练,或者如何单独存储嵌入向量,例如用于机器学习模型中。
模型评估
基本功能包括识别相似的单词:
sims=model.wv.most_similar(positive=['iphone'], restrict_vocab=15000)
term similarity
0 ipad 0.795460
1 android 0.694014
2 smartphone 0.665732
我们还可以根据正面和负面的贡献来验证单个类比:
model.wv.most_similar(positive=['france', 'london'],
negative=['paris'],
restrict_vocab=15000)
term similarity
0 united_kingdom 0.606630
1 germany 0.585644
2 netherlands 0.578868
参数设置的性能影响
我们可以使用类比来评估不同参数设置的影响。以下结果非常突出(请参阅models文件夹中的详细结果):
-
负采样优于分层 softmax,同时训练速度更快。
-
skip-gram 架构优于 CBOW。
-
不同的
min_count设置影响较小;中间值 50 的性能最佳。
使用负采样和min_count为 50 的性能最佳的 skip-gram 模型进行进一步实验,结果如下:
-
小于 5 的上下文窗口减少了性能。
-
更高的负采样率提高了性能,但训练速度较慢。
-
更大的向量提高了性能,大小为 600 的向量在 38.5%的准确率时表现最佳。
使用 doc2vec 嵌入进行情感分析
文本分类需要组合多个单词嵌入。常见方法是对文档中每个单词的嵌入向量进行平均。这使用了所有嵌入的信息,并有效地使用向量加法到达嵌入空间中的不同位置。然而,有关单词顺序的相关信息丢失了。
相反,由 word2vec 作者在发布其原始贡献后不久开发的文档嵌入模型 doc2vec 直接为文本片段(如段落或产品评论)生成嵌入。与 word2vec 类似,doc2vec 也有两种变体:
-
分布式词袋(DBOW)模型对应于 word2vec 的 CBOW 模型。文档向量是通过训练网络来预测目标词,该网络基于上下文单词向量和文档的文档向量的合成任务。
-
分布式记忆(DM)模型对应于 word2vec 的 skip-gram 架构。文档向量是通过训练神经网络使用整个文档的文档向量来预测目标词而产生的。
Gensim 的Doc2Vec类实现了这个算法。我们将通过将其应用于我们在第十四章中介绍的 Yelp 情感数据集来说明 doc2vec 的用法。为了加快训练速度,我们将数据限制为具有相关星级评分的 50 万 Yelp 评论的分层随机样本。doc2vec_yelp_sentiment笔记本包含了本节的代码示例。
从 Yelp 情感数据创建 doc2vec 输入
我们加载了包含 600 万条评论的合并 Yelp 数据集,如第十四章,用于交易的文本数据-情感分析中所创建的,并对每个星级评分的评论进行了 10 万次采样:
df = pd.read_parquet('data_path / 'user_reviews.parquet').loc[:, ['stars',
'text']]
stars = range(1, 6)
sample = pd.concat([df[df.stars==s].sample(n=100000) for s in stars])
我们使用 nltk 的RegexpTokenizer进行简单快速的文本清洗:
tokenizer = RegexpTokenizer(r'\w+')
stopword_set = set(stopwords.words('english'))
def clean(review):
tokens = tokenizer.tokenize(review)
return ' '.join([t for t in tokens if t not in stopword_set])
sample.text = sample.text.str.lower().apply(clean)
在我们过滤掉长度小于 10 个标记的评论后,我们还剩下 485,825 个样本。图 16.6的左面板显示了每个评论的标记数量的分布。
gensim.models.Doc2Vec类以TaggedDocument格式处理文档,其中包含标记化的文档以及一个唯一的标记,允许在训练后访问文档向量:
sample = pd.read_parquet('yelp_sample.parquet')
sentences = []
for i, (stars, text) in df.iterrows():
sentences.append(TaggedDocument(words=text.split(), tags=[i]))
训练 doc2vec 模型
训练界面的工作方式与 word2vec 类似,并且还允许持续训练和持久化:
model = Doc2Vec(documents=sentences,
dm=1, # 1=distributed memory, 0=dist.BOW
epochs=5,
size=300, # vector size
window=5, # max. distance betw. target and context
min_count=50, # ignore tokens w. lower frequency
negative=5, # negative training samples
dm_concat=0, # 1=concatenate vectors, 0=sum
dbow_words=0, # 1=train word vectors as well
workers=4)
model.save((results_path / 'sample.model').as_posix())
我们可以查询 n 个与给定标记最相似的术语作为评估生成的单词向量的一种快速方法如下:
model.most_similar('good')
图 16.7 的右侧面板显示了返回的标记及其相似性:
图 16.7:每个评论中标记数量的直方图(左)和与标记 'good' 最相似的术语
使用文档向量训练分类器
现在,我们可以访问文档向量以为情感分类器创建特征:
y = sample.stars.sub(1)
X = np.zeros(shape=(len(y), size)) # size=300
for i in range(len(sample)):
X[i] = model.docvecs[i]
X.shape
(485825, 300)
我们像往常一样创建训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.2,
random_state=42,
stratify=y)
现在,我们继续训练一个 RandomForestClassifier,一个 LightGBM 梯度提升模型和一个多项逻辑回归。我们对随机森林使用了 500 棵树:
rf = RandomForestClassifier(n_jobs=-1, n_estimators=500)
rf.fit(X_train, y_train)
rf_pred = rf.predict(X_test)
我们使用了 LightGBM 分类器的提前停止,但它运行了完整的 5000 轮因为它持续提高了验证性能:
train_data = lgb.Dataset(data=X_train, label=y_train)
test_data = train_data.create_valid(X_test, label=y_test)
params = {'objective': 'multiclass',
'num_classes': 5}
lgb_model = lgb.train(params=params,
train_set=train_data,
num_boost_round=5000,
valid_sets=[train_data, test_data],
early_stopping_rounds=25,
verbose_eval=50)
# generate multiclass predictions
lgb_pred = np.argmax(lgb_model.predict(X_test), axis=1)
最后,我们建立了一个多项逻辑回归模型如下:
lr = LogisticRegression(multi_class='multinomial', solver='lbfgs',
class_weight='balanced')
lr.fit(X_train, y_train)
lr_pred = lr.predict(X_test)
当我们计算每个模型在验证集上的准确性时,梯度提升表现明显更好,达到 62.24%。图 16.8 显示了每个模型的混淆矩阵和准确性:
图 16.8:备选模型的混淆矩阵和测试准确性
在 第十四章 交易文本数据 - 情感分析 中,情感分类的结果为 LightGBM 提供了更好的准确性(73.6%),但我们使用了完整的数据集并包含了额外的特征。您可能想要测试增加样本量或调整模型参数是否使 doc2vec 的性能同样良好。
学到的教训和下一步
此示例应用情感分析使用了 doc2vec 来处理产品评论而不是财务文件。我们选择产品评论是因为很难找到足够大的用于从头开始训练单词嵌入并且还具有有用的情感标签或足够信息让我们分配这些标签,例如资产回报等的财务文本数据。
虽然产品评论可以帮助我们展示工作流程,但我们需要记住重要的结构差异:产品评论通常较短、非正式,并且针对一个特定对象。相比之下,许多财务文件更长、更正式,目标对象可能明确标识也可能不明确。财务新闻文章可能涉及多个目标,而企业披露可能有明确的来源,也可能讨论竞争对手。例如,分析师报告也可能同时讨论同一对象或主题的积极和消极方面。
简而言之,财务文件中表达的情感的解释通常需要更复杂、更细致、更细粒度的方法,从不同方面建立对内容含义的理解。决策者通常也关心模型是如何得出结论的。
这些挑战尚未解决,仍然是非常活跃的研究领域,其中最大的困难之一是适合的数据源的稀缺性。然而,自 2018 年以来显著提高了各种 NLP 任务性能的最新突破表明,金融情感分析在未来几年也可能变得更加健壮。我们将在接下来转向这些创新。
新的前沿 – 预训练的变压器模型
Word2vec 和 GloVe 嵌入捕捉到比词袋方法更多的语义信息。但是,它们只允许每个令牌有一个固定长度的表示,不区分上下文特定的用法。为了解决诸如同一个词的多重含义(称为多义性)等未解决的问题,出现了几种新模型,这些模型建立在旨在学习更多上下文化单词嵌入的注意力机制上(Vaswani 等,2017)。这些模型的关键特征如下:
-
使用双向语言模型同时处理文本的左到右和右到左以获得更丰富的上下文表示
-
使用半监督预训练在大型通用语料库上学习通用语言方面的嵌入和网络权重,这些可以用于特定任务的微调(一种我们将在第十八章,用于金融时间序列和卫星图像的 CNNs中更详细讨论的迁移学习形式)
在本节中,我们简要描述了注意力机制,概述了最近的变压器模型——从变压器中的双向编码器表示(BERT)开始——如何利用它来提高关键 NLP 任务的性能,引用了几个预训练语言模型的来源,并解释了如何将它们用于金融情感分析。
注意力是你所需要的一切
注意力机制明确地建模了句子中单词之间的关系,以更好地整合上下文。它最初被应用于机器翻译(Bahdanau,Cho 和 Bengio,2016),但此后已成为各种任务的神经语言模型的核心组成部分。
直到 2017 年,循环神经网络(RNNs),它们按顺序从左到右或从右到左处理文本,代表了自然语言处理任务(如翻译)的最新技术。例如,谷歌自 2016 年末起在生产中就采用了这样的模型。顺序处理意味着需要多个步骤来语义连接远距离位置的单词,并且排除了并行处理,而这在现代专用硬件(如 GPU)上会大大加速计算。(关于 RNNs 的更多信息,请参阅第十九章,多元时间序列和情感分析的 RNNs。)
相比之下,Transformer 模型,由开创性论文 Attention is all you need(Vaswani 等人,2017)引入,只需一个恒定数量的步骤来识别语义相关的单词。它依赖于一种自注意力机制,可以捕捉句子中所有单词之间的联系,而不考虑它们的相对位置。该模型通过给每个句子中的其他单词分配一个注意力分数来学习单词的表示,该分数决定其他单词应该对表示的贡献程度。然后,这些分数将指导所有单词表示的加权平均值,输入到一个全连接网络中,以生成目标单词的新表示。
Transformer 模型采用了一种编码器-解码器的架构,其中包括多个层,每个层并行使用了多个注意力机制(称为头部)。它在各种翻译任务上取得了巨大的性能提升,并且更重要的是,激发了一波新的研究,致力于神经语言模型解决更广泛范围的任务。在 GitHub 上链接的资源中包含了关于注意力机制如何工作的各种优秀视觉解释,所以我们在这里不会详细介绍。
BERT – 迈向更普适的语言模型
2018 年,Google 发布了 BERT 模型,全称为 Bidirectional Encoder Representations from Transformers(Devlin 等人,2019)。对于自然语言理解任务,它在十一项任务上取得了突破性的成果,从问答和命名实体识别到释义和情感分析,都得到了 通用语言理解评估(GLUE)基准的衡量(请参考 GitHub 获取任务描述和排行榜链接)。
BERT 引入的新思想引发了一系列新的研究,产生了数十项超越非专业人员的改进,很快超过了由 DeepMind 设计的更具挑战性的 SuperGLUE 基准(Wang 等人,2019)。因此,2018 年现在被认为是自然语言处理研究的一个转折点;现在,Google 搜索和微软的必应都在使用 BERT 的变体来解释用户查询并提供更准确的结果。
我们将简要概述 BERT 的关键创新,并提供如何开始使用它及其后续增强版的指示,其中包括几个提供预训练模型的开源库。
关键创新 – 更深的注意力和预训练
BERT 模型基于 两个关键思想,即前一节中描述的 transformer 架构 和 无监督预训练,这样它就不需要为每个新任务从头开始训练;相反,它的权重被微调:
-
BERT 通过使用 12 或 24 层(取决于架构),每层有 12 或 16 个注意头,将注意机制提升到一个新的(更深)水平。这导致最多 24 × 16 = 384 个注意机制来学习特定于上下文的嵌入。
-
BERT 使用无监督的、双向的预训练来提前在两个任务上学习其权重:遮盖语言建模(在左右上下文中给出缺失的单词)和下一句预测(预测一句话是否跟在另一句话后面)。
无上下文模型(如 word2vec 或 GloVe)为词汇表中的每个单词生成一个单一的嵌入:单词“bank”在“bank account”和“river bank”中具有相同的无上下文表示。相比之下,BERT 学习根据句子中的其他单词来表示每个单词。作为双向模型,BERT 能够表示句子“I accessed the bank account”中的单词“bank”,不仅基于“我访问了”作为单向上下文模型,还基于“account”。
BERT 及其后继版本可以在通用语料库(如维基百科)上进行预训练,然后调整其最终层以适应特定任务,并微调其权重。因此,您可以使用具有数十亿参数的大规模、最先进的模型,而只需支付几个小时而不是几天或几周的培训成本。几个库提供了这样的预训练模型,您可以构建一个定制的情感分类器,以适应您选择的数据集。
使用预训练的最先进模型
本节描述的最新 NLP 突破展示了如何利用足够大的网络从未标记的文本中获取语言知识,这些 Transformer 体系结构减少了对单词顺序和上下文的假设;相反,它们从大量数据中学习语言的更微妙的理解,使用数亿甚至数十亿的参数。
我们将重点介绍几个使预训练网络和优秀的 Python 教程可用的库。
Hugging Face Transformers 库
Hugging Face 是一家美国初创公司,开发旨在提供个性化 AI 驱动通信的聊天机器人应用。2019 年底,该公司筹集了 1500 万美元,以进一步开发其非常成功的开源 NLP 库 Transformers。
该库提供了用于自然语言理解和生成的通用架构,拥有超过 32 个预训练模型,涵盖 100 多种语言,并在 TensorFlow 2 和 PyTorch 之间具有深层的互操作性。它有很好的文档。
spacy-transformers 库包含了用于在 spaCy 管道中方便地包含预训练变换器模型的包装器。有关更多信息,请参阅 GitHub 上的参考链接。
AllenNLP
AllenNLP 由微软联合创始人保罗·艾伦创建并维护,与华盛顿大学的研究人员密切合作。它被设计为一个用于在各种语言任务上开发最先进深度学习模型的研究库,基于 PyTorch 构建。
它提供了从问答到句子标注等关键任务的解决方案,包括阅读理解、命名实体识别和情感分析。预训练的RoBERTa模型(BERT 的更强大版本;Liu 等,2019 年)在斯坦福情感树库上实现了超过 95%的准确率,并且只需几行代码即可使用(参见 GitHub 上的文档链接)。
交易文本数据——经验教训与下一步计划
正如在“使用 doc2vec 嵌入进行情感分析”一节末尾所强调的那样,金融文件存在重要的结构特征,这些特征通常会使其解释变得复杂,并削弱基于简单词典的方法。
在最近一项金融情感分析调查中,Man、Luo 和 Lin(2019 年)发现,大多数现有方法只能识别高级极性,如积极、消极或中性。然而,导致实际决策的实际应用通常需要更细致和透明的分析。此外,缺乏具有相关标签的大型金融文本数据集限制了使用传统机器学习方法或神经网络进行情感分析的潜力。
刚才描述的预训练方法,原则上能够更深入地理解文本信息,因此具有相当大的潜力。然而,使用变换器进行的大多数应用研究都集中在诸如翻译、问答、逻辑或对话系统等自然语言处理任务上。与金融数据相关的应用仍处于初级阶段(例如,参见 Araci 2019)。考虑到预训练模型的可用性以及它们从金融文本数据中提取更有价值信息的潜力,这种情况可能很快就会改变。
总结
在本章中,我们讨论了一种利用浅层神经网络进行无监督机器学习的新型生成文本特征的方法。我们看到了由此产生的词嵌入如何捕捉到一些有趣的语义方面,超越了单个标记的含义,捕捉到了它们被使用的一些上下文。我们还介绍了如何使用类比和线性代数评估单词向量的质量。
我们使用 Keras 构建了生成这些特征的网络架构,并将更高性能的 Gensim 实现应用于金融新闻和美国证券交易委员会的备案文件。尽管数据集相对较小,但 word2vec 嵌入确实捕捉到了有意义的关系。我们还展示了如何通过股价数据进行适当标记,从而形成监督学习的基础。
我们应用了 doc2vec 算法,该算法生成的是文档而不是令牌向量,以构建基于 Yelp 商业评论的情感分类器。虽然这不太可能产生可交易的信号,但它说明了如何从相关文本数据中提取特征并训练模型来预测可能对交易策略有信息意义的结果的过程。
最后,我们概述了最近的研究突破,承诺通过可预先训练的架构的可用性来产生更强大的自然语言模型,这些模型仅需要微调即可。然而,对金融数据的应用仍处于研究前沿。
在下一章中,我们将深入探讨本书的最后部分,该部分涵盖了各种深度学习架构如何对算法交易有用。
第十七章:交易的深度学习
本章开启了第四部分,涵盖了几种深度学习(DL)建模技术如何对投资和交易有用。DL 已经在许多领域取得了许多突破,从图像和语音识别到机器人和智能代理,引起了广泛关注,并重振了对人工智能(AI)的大规模研究。人们对快速发展有着很高的期望,并且预计将会出现更多解决困难实际问题的解决方案。
在本章中,我们将介绍前馈神经网络,以介绍与后续章节中涵盖的各种 DL 架构相关的神经网络工作要素。具体来说,我们将演示如何使用反向传播算法高效训练大型模型,并管理过拟合的风险。我们还将展示如何使用流行的 TensorFlow 2 和 PyTorch 框架,这些框架将贯穿第四部分。
最后,我们将基于由深度前馈神经网络生成的信号开发、回测和评估交易策略。我们将设计和调整神经网络,并分析关键超参数选择如何影响其性能。
总之,在阅读本章并审阅随附的笔记本后,您将了解:
-
深度学习如何解决复杂领域的 AI 挑战
-
推动 DL 走向当前流行的关键创新
-
前馈网络如何从数据中学习表示
-
在 Python 中设计和训练深度神经网络(NNs)
-
使用 Keras、TensorFlow 和 PyTorch 实现深度神经网络
-
构建和调整深度神经网络以预测资产回报
-
设计和回测基于深度神经网络信号的交易策略
在接下来的章节中,我们将在此基础上设计各种适用于不同投资应用的架构,特别关注替代文本和图像数据。
这些包括针对序列数据(如时间序列或自然语言)量身定制的循环神经网络(RNNs),以及特别适用于图像数据但也可以与时间序列数据一起使用的卷积神经网络(CNNs)。我们还将涵盖深度无监督学习,包括自动编码器和生成对抗网络(GANs),以及强化学习来训练能够与环境进行交互式学习的代理程序。
您可以在 GitHub 存储库的相应目录中找到本章的代码示例和附加资源链接。笔记本中包括图片的彩色版本。
深度学习 - 新技术和重要性
第二部分中涵盖的机器学习(ML)算法在解决各种重要问题方面表现良好,包括文本数据,如第三部分所示。然而,它们在解决诸如识别语音或对图像中的对象进行分类等核心人工智能问题上成功较少。这些限制促使了深度学习的发展,最近的深度学习突破大大促进了对人工智能的兴趣再度增长。有关包含并扩展本节许多观点的全面介绍,请参见 Goodfellow、Bengio 和 Courville(2016),或者看看 LeCun、Bengio 和 Hinton(2015)的简短版本。
在本节中,我们概述了深度学习如何克服其他机器学习算法的许多限制。这些限制特别限制了在需要复杂努力提取信息特征的高维和非结构化数据上的性能。
我们在第二和第三部分中介绍的机器学习技术最适合处理具有明确定义特征的结构化数据。例如,我们看到如何使用第十四章中的文档-文本矩阵将文本数据转换为表格数据,交易的文本数据-情感分析。深度学习通过学习数据的表示来克服设计信息特征的挑战,可能需要手动进行,从而更好地捕捉其与结果相关的特征。
更具体地说,我们将看到深度学习如何学习数据的分层表示,以及为什么这种方法对于高维、非结构化数据效果很好。我们将描述神经网络如何使用多层、深度架构来组成一组嵌套函数并发现分层结构。这些函数根据前一层的学习计算数据的连续和越来越抽象的表示。我们还将看看反向传播算法如何调整网络参数,以便这些表示最好地满足模型的目标。
我们还将简要概述深度学习如何融入人工智能的演变以及旨在实现当前人工智能目标的各种方法。
分层特征驯服了高维数据
正如第二部分中所讨论的那样,监督学习的关键挑战是从训练数据推广到新样本。随着数据的维度增加,泛化变得指数级困难。我们在第十三章中遇到了这些困难的根本原因,即无监督学习的数据驱动风险因素和资产配置的维度诅咒。
这个诅咒的一个方面是,体积随着维度的增加而呈指数增长:对于边长为 10 的超立方体,随着维度从三增加到四,体积从 10³增加到 10⁴。相反,对于给定样本大小,数据密度会呈指数级下降。换句话说,为了保持一定的密度,所需的观察次数呈指数增长。
另一个方面是,当特征与输出之间的功能关系被允许跨越越来越多的维度变化时,它们变得更加复杂。正如第六章,机器学习过程中讨论的那样,ML 算法在高维空间中学习任意函数时会遇到困难,因为候选者数量呈指数级增长,而可用于推断关系的数据密度同时下降。为了缓解这个问题,算法假设目标函数属于某个特定的类,并对在解决当前问题时在该类中寻找最佳解的搜索施加约束。
此外,算法通常假设新点的输出应与附近训练点的输出相似。这种先验平滑性假设或局部恒定性假设,即学习的函数在小区域内不会发生太大变化,正如 k 最近邻算法所示(参见第六章,机器学习过程)。然而,随着维度数量的增加,数据密度指数级下降,训练样本之间的距离自然上升。因此,随着目标函数的潜在复杂性增加,附近训练示例的概念变得不太有意义。
对于传统的 ML 算法,所需参数和训练样本的数量通常与算法能够区分的输入空间中的区域数量成正比。DL 旨在通过假设特征的层次结构生成数据,从而克服从有限数量的训练点学习指数数量的区域的挑战。
DL 作为表示学习
许多人工智能任务,如图像或语音识别,需要关于世界的知识。其中一个关键挑战是对这些知识进行编码,以便计算机可以利用它。几十年来,ML 系统的发展需要相当的领域专业知识,以将原始数据(如图像像素)转换为学习算法可以用来检测或分类模式的内部表示。
同样,ML 算法对交易策略增加了多少价值,很大程度上取决于我们能够工程化特征,以表示数据中的预测信息,以便算法可以处理它。理想情况下,特征应捕获结果的独立驱动因素,正如在第四章,金融特征工程-如何研究 Alpha 因子和第二部分和第三部分中讨论的,在设计和评估捕获交易信号的因子时。
与依赖手工设计的特征不同,表示学习使 ML 算法能够自动发现对于检测或分类模式最有用的数据表示。DL 将这种技术与关于特征性质的特定假设相结合。有关更多信息,请参见 Bengio、Courville 和 Vincent(2013)。
DL 如何从数据中提取层次特征
DL 背后的核心思想是一个多层次的特征层次结构生成了数据。因此,DL 模型编码了目标函数由一组嵌套的简单函数组成的先验信念。这一假设允许在给定数量的训练样本的情况下,区分的区域数量呈指数增长。
换句话说,DL 是一种从数据中提取概念层次结构的表示学习方法。它通过组合简单但非线性的函数来学习这种层次结构表示,这些函数逐步地将一个级别(从输入数据开始)的表示转换为稍微更抽象的高级表示。通过组合足够多的这些转换,DL 能够学习非常复杂的函数。
应用于分类任务时,例如,更高层次的表示往往会放大对区分对象最有帮助的数据方面,同时抑制无关的变化源。正如我们将在第十八章更详细地看到的,用于金融时间序列和卫星图像的 CNNs,原始图像数据只是像素值的二维或三维数组。表示的第一层通常学习侧重于特定方向和位置的边缘的存在或缺失的特征。第二层通常学习依赖于特定边缘排列的主题,而不考虑它们位置的小变化。接下来的层可能会组装这些主题以表示相关对象的部分,随后的层将检测到对象作为这些部分的组合。
DL 的关键突破在于一个通用的学习算法可以提取适合于对高维、非结构化数据进行建模的层次特征,而这种方式的可扩展性比人类工程学无限得多。因此,DL 的崛起与非结构化图像或文本数据的大规模可用性并不奇怪。在这些数据源在替代数据中也占据重要地位的程度上,DL 对算法交易变得高度相关。
好消息和坏消息 - 通用逼近定理
通用逼近定理正式化了 NNs 捕捉输入和输出数据之间任意关系的能力。George Cybenko(1989)证明了使用 sigmoid 激活函数的单层 NNs 可以表示Rn的闭合和有界子集上的任何连续函数。Kurt Hornik(1991)进一步表明,能够进行分层特征表示的不是特定形状的激活函数,而是多层次架构,这进而使得 NNs 能够逼近通用函数。
然而,该定理并不能帮助我们确定表示特定目标函数所需的网络架构。我们将在本章的最后一节中看到,有许多参数需要优化,包括网络的宽度和深度、神经元之间的连接数量以及激活函数的类型。
此外,能够表示任意函数并不意味着网络实际上可以学习给定函数的参数。过去 20 多年来,反向传播,即用于神经网络的最流行的学习算法之一,才在规模上变得有效。不幸的是,考虑到优化问题的高度非线性性质,不能保证它会找到绝对最佳解而不仅仅是相对好的解决方案。
深度学习与机器学习和人工智能的关系
人工智能有着悠久的历史,至少可以追溯到 20 世纪 50 年代作为一个学术领域,而作为人类探究的主题则更久远,但自那时以来,经历了几次热情的高涨和低落(有关深入调查,请参见尼尔森,2009 年)。机器学习是一个重要的子领域,在统计学等相关学科中有着悠久的历史,并在 20 世纪 80 年代变得突出起来。正如我们刚刚讨论的那样,并且在图 17.1中所示,深度学习是一种表示学习,本身是机器学习的一个子领域。
人工智能的最初目标是实现通用人工智能,即被认为需要人类级智能来解决的问题,并对世界进行推理和逻辑推断,并自动改进自己的能力。不涉及机器学习的人工智能应用包括编码有关世界的信息的知识库,以及用于逻辑操作的语言。
在历史上,大量的人工智能工作都致力于开发基于规则的系统,旨在捕获专家知识和决策规则,但是由于过度复杂而硬编码这些规则的尝试经常失败。相比之下,机器学习意味着从数据中学习规则的概率方法,并旨在规避人为设计的基于规则系统的限制。它还涉及到更窄、任务特定目标的转变。
下图勾勒了各种人工智能子领域之间的关系,概述了它们的目标,并突出了它们在时间线上的相关性。
图 17.1:人工智能时间线和子领域
在接下来的部分中,我们将看到如何实际构建神经网络。
设计神经网络
深度学习依赖于神经网络,它由一些关键构建模块组成,而这些模块又可以以多种方式配置。在本节中,我们介绍了神经网络的工作原理,并阐述了用于设计不同架构的最重要组成部分。
(人工)神经网络最初受到了生物学习模型的启发,例如人脑,要么试图模仿其工作原理并取得类似的成功,要么通过模拟来更好地理解。当前的神经网络研究不太依赖于神经科学,至少因为我们对大脑的理解尚未达到足够的细致程度。另一个约束是总体规模:即使从 20 世纪 50 年代开始,神经网络中使用的神经元数量每年都以指数倍增长,它们也只会在 2050 年左右达到人脑的规模。
我们还将解释反向传播,通常简称为反向传播,如何使用梯度信息(损失函数对参数的偏导数值)根据训练误差调整所有神经网络参数。各种非线性模块的组合意味着目标函数的优化可能非常具有挑战性。我们还介绍了旨在加速学习过程的反向传播的改进。
一个简单的前馈神经网络架构
在本节中,我们介绍了基于多层感知器(MLP)的前馈神经网络(feedforward NNs),它由一个或多个连接输入与输出层的隐藏层组成。在前馈神经网络中,信息只从输入流向输出,因此它们可以被表示为有向无环图,如下图所示。相比之下,循环神经网络(RNNs;见第十九章,用于多元时间序列和情感分析的 RNNs)包括从输出回到输入的循环,以跟踪或记忆过去的模式和事件。
我们将首先描述前馈神经网络的架构以及如何使用 NumPy 实现它。然后我们将解释反向传播如何学习神经网络的权重,并在 Python 中实现它,以训练一个二分类网络,即使类别不是线性可分的也能产生完美的结果。有关实现细节,请参见笔记本build_and_train_feedforward_nn。
前馈神经网络由多个层组成,每个层接收一份输入数据样本并产生一个输出。变换链始于输入层,将源数据传递给几个内部或隐藏层之一,以输出层结束,该层计算与样本输出值进行比较的结果。
隐藏层和输出层由节点或神经元组成。全连接或密集层的节点连接到上一层的一些或所有节点。网络架构可以通过其深度(由隐藏层的数量衡量)、每个层的宽度和节点数量来总结。
每个连接都有一个用于计算输入值的线性组合的权重。一层也可以有一个偏差节点,它始终输出 1,并由后续层中的节点使用,类似于线性回归中的常数。训练阶段的目标是学习这些权重的值,以优化网络的预测性能。
每个隐藏层节点计算前一层的输出和权重的点积。激活函数转换结果,成为后续层的输入。此转换通常是非线性的(就像逻辑回归中使用的 Sigmoid 函数一样;参见 第七章,线性模型 - 从风险因素到回报预测,关于线性模型),以便网络可以学习非线性关系;我们将在下一节讨论常见的激活函数。输出层计算最后一个隐藏层的输出与其权重的线性组合,并使用与 ML 问题类型匹配的激活函数。
因此,网络输出的计算从输入中流经一系列嵌套函数,并称为前向传播。图 17.2说明了一个具有二维输入向量、宽度为三的隐藏层和两个输出层节点的单层前馈 NN。这种架构足够简单,因此我们仍然可以轻松地绘制它,并且说明关键概念。
图 17.2:具有一个隐藏层的前馈架构
网络图显示,每个隐藏层节点(不包括偏差)都有三个权重,一个用于输入层偏差,两个用于每个两个输入变量。同样,每个输出层节点都有四个权重来计算隐藏层偏差和激活的乘积和。总共有 17 个要学习的参数。
右侧的前向传播面板列出了隐藏层和输出层中一个示例节点 h 和 o 的计算,分别表示隐藏层和输出层。隐藏层中的第一个节点对其权重和输入的线性组合 z 应用 Sigmoid 函数,类似于逻辑回归。因此,隐藏层同时运行三个逻辑回归,并且反向传播算法确保它们的参数很可能不同,以最好地通知后续层。
输出层使用softmax激活函数(参见 第六章,机器学习过程),该函数将逻辑 Sigmoid 函数推广到多个类别。它调整了隐藏层输出与其权重的点积,以表示类别的概率(在这种情况下仅为两个以简化演示)。
前向传播也可以表示为嵌套函数,其中 h 再次表示隐藏层,o 表示输出层以产生 NN 对输出的估计:。
关键设计选择
一些神经网络设计选择与其他监督学习模型相似。例如,输出取决于 ML 问题的类型,如回归、分类或排名。给定输出,我们需要选择一个成本函数来衡量预测成功和失败,并选择一个算法来优化网络参数以最小化成本。
NN 特定的选择包括层数和每层节点的数量,不同层节点之间的连接以及激活函数的类型。
一个关键问题是训练效率:激活的功能形式可以促进或阻碍可用于反向传播算法的梯度信息的流动,该算法根据训练错误调整权重。具有大输入值范围的平坦区域的函数具有非常低的梯度,当参数值停留在这种范围内时,可以阻碍训练进度。
一些架构添加了跳跃连接,建立了超出相邻层的直接链接,以促进梯度信息的流动。另一方面,有意省略连接可以减少参数数量,限制网络的容量,并可能降低泛化误差,同时也减少了计算成本。
隐藏单元和激活函数
除了 sigmoid 函数之外,还有几种非线性激活函数被成功使用。它们的设计仍然是一个研究领域,因为它们是允许 NN 学习非线性关系的关键元素。它们也对训练过程有关键影响,因为它们的导数决定了错误如何转化为权重调整。
一个非常流行的激活函数是修正线性单元(ReLU)。激活被计算为g(z) = max(0, z),对于给定的激活z,结果形式类似于看涨期权的支付。当单元处于活跃状态时,导数是常数。ReLU 通常与需要存在偏置节点的仿射输入变换结合使用。它们的发现极大地改善了前馈网络的性能,与 S 型单元相比,它们经常被推荐为默认选项。有几个 ReLU 扩展旨在解决 ReLU 在不活动时学习梯度下降时的限制和它们的梯度为零的问题(Goodfellow、Bengio 和 Courville,2016)。
对于逻辑函数σ的另一种选择是双曲正切函数 tanh,它产生在范围[-1, 1]的输出值。它们是密切相关的,因为。两个函数都受到饱和的影响,因为它们的梯度在非常低和高的输入值时变得非常小。然而,tanh 通常表现更好,因为它更接近恒等函数,所以对于小的激活值,网络的行为更像是一个线性模型,这反过来又促进了训练。
输出单元和成本函数
NN 输出格式和成本函数的选择取决于监督学习问题的类型:
-
回归问题使用线性输出单元,计算其权重与最终隐藏层激活的点积,通常与均方误差成本一起使用。
-
二元分类使用 sigmoid 输出单元来模拟伯努利分布,就像逻辑回归一样,其中隐藏激活作为输入
-
多类问题依赖于 softmax 单元,它们推广了逻辑 sigmoid 并模拟了超过两个类别的离散分布,正如之前展示的那样
二元和多类问题通常使用交叉熵损失,与均方误差相比,这显着提高了训练效果(有关损失函数的其他信息,请参见第六章,机器学习过程)。
如何正则化深度 NN
NN 近似任意函数的容量的缺点是过度拟合的风险大大增加。对过度拟合的最佳保护是在更大的数据集上训练模型。数据增强,例如创建图像的略微修改版本,是一个强大的替代方法。为此目的生成合成金融训练数据是一个活跃的研究领域,我们将在第二十章,自编码器用于条件风险因素和资产定价中讨论这一点(例如,Fu 等人 2019 年)。
作为获取更多数据的替代或补充,正则化可以帮助减轻过度拟合的风险。在本书中到目前为止讨论的所有模型中,都有一些形式的正则化,它修改学习算法以减少其泛化误差,而不会对其训练误差产生负面影响。示例包括添加到岭和套索回归目标中的惩罚以及用于决策树和基于树的集成模型的分割或深度约束。
经常,正则化采用对参数值的软约束形式,以权衡一些额外的偏差以获得较低的方差。一个常见的实际发现是,具有最低泛化误差的模型不是具有精确正确参数大小的模型,而是一个经过很好正则化的更大的模型。可以结合使用的流行的 NN 正则化技术包括参数范数惩罚,提前停止和丢弃。
参数范数惩罚
我们在第七章,线性模型-从风险因素到收益预测中遇到了参数范数惩罚,分别用作L1 和 L2 正则化的套索和岭回归。在 NN 上下文中,参数范数惩罚通过添加一个代表参数的 L1 或 L2 范数的项来类似地修改目标函数,权重由需要调整的超参数加权。对于 NN,偏置参数通常不受限制,只有权重。
L1 正则化可以通过将权重减少到零来产生稀疏的参数估计。相比之下,L2 正则化保留了参数显著减少成本函数的方向。惩罚或超参数的值可以在各个层之间变化,但添加的调整复杂性很快变得令人难以承受。
早停止
我们在第十二章 提升您的交易策略中遇到了早停止作为一种正则化技术。它可能是最常见的神经网络正则化方法,因为它既有效又简单:它监视模型在验证集上的性能,并在一定数量的观察次数内停止训练,以防止过拟合。
早停止可以被看作是有效的超参数选择,它可以自动确定正确的正则化量,而参数惩罚则需要超参数调整来确定理想的权重衰减。只要注意避免前瞻性偏见:当早停止使用不可用于策略实施的样本外数据时,回测结果将过度正面。
Dropout
Dropout 是指在前向或后向传播过程中以给定概率随机省略个别单元。因此,这些被省略的单元不会对训练误差做出贡献,也不会接收更新。
该技术计算成本低廉,不限制模型或训练过程的选择。虽然需要更多迭代才能达到相同的学习量,但由于计算成本较低,每次迭代速度更快。Dropout 通过防止单元在训练过程中弥补其他单元的错误来降低过拟合的风险。
更快地训练 - 深度学习的优化
反向传播是指计算目标函数相对于我们希望更新的内部参数的梯度,并利用这些信息来更新参数值。梯度是有用的,因为它指示导致成本函数最大增加的参数变化方向。因此,根据负梯度调整参数会产生最佳的成本减少,至少对于非常接近观察样本的区域来说是这样。有关关键梯度下降优化算法的出色概述,请参阅 Ruder(2017)。
训练深度神经网络可能会耗费大量时间,这是由于非凸目标函数和可能庞大的参数数量所导致的。几个挑战可能会显著延迟收敛,找到一个糟糕的最优解,或者导致振荡或偏离目标:
-
局部极小值 可能会阻止收敛到全局最优解并导致性能差。
-
具有低梯度的平坦区域 可能不是局部最小值,也可能阻止收敛,但很可能远离全局最优解。
-
由于乘以几个大权重而导致的具有高梯度的陡峭区域可能会导致过度调整
-
RNN 中的深层结构或长期依赖性需要在反向传播过程中乘以许多权重,导致梯度消失,使得至少部分 NN 接收到少量或没有更新
已经开发了几种算法来解决其中一些挑战,即随机梯度下降的变体和使用自适应学习率的方法。虽然自适应学习率并没有单一的最佳算法,但已经显示出一些希望。
随机梯度下降
梯度下降通过梯度信息迭代地调整这些参数。对于给定的参数 ,基本梯度下降规则通过损失函数相对于该参数的负梯度乘以学习速率
来调整该值:
梯度可以对所有训练数据、随机批量数据或单个观测(称为在线学习)进行评估。随机样本产生随机梯度下降(SGD),如果随机样本在整个训练过程中对梯度方向是无偏估计,则通常导致更快的收敛。
然而,存在许多挑战:很难预先定义一个能够促进有效收敛的学习率或速率调度——太低的速率会延长过程,而太高的速率可能会导致反复超调和围绕最小值振荡甚至发散。此外,相同的学习率可能不适用于所有参数,即在所有变化方向上都不适用。
动量
基本梯度下降的一个流行改进是将动量添加到加速收敛到局部最小值。动量的图示经常使用一个位于细长峡谷中心的局部最优值的例子(实际上,维度要比三维高得多)。它意味着一个位于深而窄的峡谷内的最小值,该峡谷的壁非常陡峭,其中一侧的梯度很大,另一侧朝着该区域底部的局部最小值的斜坡要缓得多。梯度下降自然而然地沿着陡峭的梯度走,将反复调整上下峡谷的墙壁,向着最小值的方向移动得更慢。
动量旨在通过跟踪最近的方向并通过最近的梯度和当前计算值的加权平均来调整参数来解决这种情况。它使用动量项 来衡量最新调整对此迭代更新 v[t] 的贡献:
Nesterov momentum 是对普通动量的简单改进。在这里,梯度项不是在当前参数空间位置计算!,而是从一个中间位置计算。其目标是纠正动量项过度偏离或指向错误方向(Sutskever 等人,2013)。
自适应学习率
选择适当的学习率非常具有挑战性,正如前一小节中所强调的随机梯度下降。与此同时,它是最重要的参数之一,它强烈影响训练时间和泛化性能。
虽然动量解决了一些学习率的问题,但是以引入另一个超参数,动量率 为代价。几种算法旨在根据梯度信息在整个训练过程中自适应地调整学习率。
AdaGrad
AdaGrad 累积所有历史的、参数特定的梯度信息,并继续根据给定参数的平方累积梯度来反比例地重新缩放学习率。其目标是减缓已经大量变化的参数的变化,鼓励还没有变化的参数进行调整。
AdaGrad 设计用于在凸函数上表现良好,在 DL 上表现不佳,因为它可能会根据早期梯度信息过快地降低学习率。
RMSProp
RMSProp 修改了 AdaGrad 以使用累积梯度信息的指数加权平均值。其目标是更加强调最近的梯度。它还引入了一个新的超参数,用于控制移动平均的长度。
RMSProp 是一种流行的算法,通常表现良好,由我们稍后将介绍的各种库提供,并在实践中经常使用。
Adam
Adam 代表 自适应动量估计,将 RMSProp 的一些方面与动量结合起来。它被认为是相当稳健的,并经常用作默认的优化算法(Kingma 和 Ba,2014)。
Adam 有几个带有建议的默认值的超参数,可能会从一些调整中受益:
-
alpha:学习率或步长确定更新权重的程度,较大(较小)的值在更新速度之前加快(减慢)学习;许多库使用默认值 0.001
-
beta[1]:第一矩估计的指数衰减率;通常设置为 0.9
-
beta[2]:第二矩估计的指数衰减率;通常设置为 0.999
-
epsilon:一个非常小的数字,用于防止除零;通常设置为 1e-8
总结 - 如何调整关键的超参数
超参数优化旨在调整模型的容量,使其匹配数据输入之间的关系的复杂性。过多的容量会增加过拟合的可能性,需要更多的数据,将额外的信息引入到学习过程中,减小模型的大小,或更积极地使用刚刚描述的各种正则化工具。
主要的诊断工具是描述在第六章,机器学习过程中的训练和验证错误的行为:如果验证错误恶化,而训练错误继续下降,那么模型就是过度拟合的,因为其容量太高。另一方面,如果性能不符合预期,可能需要增加模型的大小。
参数优化最重要的方面是架构本身,因为它很大程度上决定了参数的数量:其他条件相同,更多或更宽的隐藏层会增加容量。正如前面提到的,最佳性能通常与具有过量容量但使用像 dropout 或 L1/L2 惩罚这样的机制进行了良好正则化的模型相关联。
除了平衡模型大小和正则化之外,调整学习率也很重要,因为它可能会破坏优化过程并降低有效模型容量。自适应优化算法提供了一个很好的起点,就像 Adam 描述的那样,这是最流行的选项。
Python 从头开始的神经网络
为了更好地理解 NNs 的工作原理,我们将使用矩阵代数来表述单层架构和图 17.2中显示的前向传播计算,并使用 NumPy 实现它。你可以在笔记本build_and_train_feedforward_nn中找到代码示例。
输入层
图 17.2中显示的架构设计用于表示两个不同类别Y的二维输入数据X。以矩阵形式,X和Y的形状都是:
我们将使用 scikit-learn 的make_circles函数生成 50,000 个随机二进制样本,形成两个半径不同的同心圆,以便类别不是线性可分的:
N = 50000
factor = 0.1
noise = 0.1
X, y = make_circles(n_samples=N, shuffle=True,
factor=factor, noise=noise)
然后将一维输出转换为二维数组:
Y = np.zeros((N, 2))
for c in [0, 1]:
Y[y == c, c] = 1
'Shape of: X: (50000, 2) | Y: (50000, 2) | y: (50000,)'
图 17.3显示了数据的散点图,很明显不是线性可分的:
图 17.3:二元分类的合成数据
隐藏层
隐藏层h使用权重 W^h 将二维输入投影到三维空间,并通过偏置向量 b^h 将结果平移。为了执行这个仿射变换,隐藏层权重由一个矩阵 W^h 表示,而隐藏层偏置向量由一个三维向量表示:
隐藏层激活 H 是通过将输入数据与加入偏置向量后的权重的点积应用于 sigmoid 函数而得到的:
要使用 NumPy 实现隐藏层,我们首先定义 logistic sigmoid 函数:
def logistic(z):
"""Logistic function."""
return 1 / (1 + np.exp(-z))
然后我们定义一个函数,根据相关的输入、权重和偏置值计算隐藏层激活:
def hidden_layer(input_data, weights, bias):
"""Compute hidden activations"""
return logistic(input_data @ weights + bias)
输出层
输出层使用一个 权重矩阵 W^o 和一个二维偏置向量 b^o 将三维隐藏层激活 H 压缩回两维:
隐藏层输出的线性组合导致一个 矩阵 Z^o:
输出层激活由 softmax 函数 计算,该函数将 Z^o 规范化以符合离散概率分布的惯例:
我们在 Python 中创建一个 softmax 函数如下所示:
def softmax(z):
"""Softmax function"""
return np.exp(z) / np.sum(np.exp(z), axis=1, keepdims=True)
如此定义,输出层激活取决于隐藏层激活和输出层权重和偏置:
def output_layer(hidden_activations, weights, bias):
"""Compute the output y_hat"""
return softmax(hidden_activations @ weights + bias)
现在我们拥有了集成层并直接从输入计算 NN 输出所需的所有组件。
正向传播
forward_prop 函数将前述操作组合起来,从输入数据中产生输出激活作为权重和偏置的函数:
def forward_prop(data, hidden_weights, hidden_bias, output_weights, output_bias):
"""Neural network as function."""
hidden_activations = hidden_layer(data, hidden_weights, hidden_bias)
return output_layer(hidden_activations, output_weights, output_bias)
predict 函数根据权重、偏置和输入数据产生二元类别预测:
def predict(data, hidden_weights, hidden_bias, output_weights, output_bias):
"""Predicts class 0 or 1"""
y_pred_proba = forward_prop(data,
hidden_weights,
hidden_bias,
output_weights,
output_bias)
return np.around(y_pred_proba)
交叉熵损失函数
最后一块是根据给定标签评估 NN 输出的损失函数。损失函数 J 使用交叉熵损失 ,它对每个类别 c 的预测与实际结果的偏差进行求和:
在 Python 中,它的形式如下:
def loss(y_hat, y_true):
"""Cross-entropy"""
return - (y_true * np.log(y_hat)).sum()
如何使用 Python 实现反向传播
要使用反向传播更新神经网络的权重和偏置值,我们需要计算损失函数的梯度。梯度表示损失函数相对于目标参数的偏导数。
如何计算梯度
NN 组成一组嵌套函数,如前所述。因此,使用微积分的链式法则计算损失函数相对于内部隐藏参数的梯度。
对于标量值,给定函数 z = h(x) 和 y = o(h(x)) = o (z),我们使用链式法则计算 y 相对于 x 的导数如下:
对于向量,有 和
,使得隐藏层 h 从 R^n 映射到 R^m,z = h(x),y = o (z),我们得到:
我们可以使用矩阵表示更简洁地表达这一点,使用 h 的雅可比矩阵 :
它包含了对于 z 的每个 m 组件相对于每个 n 输入 x 的偏导数。y 相对于 x 的梯度 包含了所有的偏导数,因此可以写成:
损失函数的梯度
交叉熵损失函数 J 对于每个输出层激活 i = 1, ..., N 的导数是一个非常简单的表达式(详见笔记本),如下左侧为标量值,右侧为矩阵表示:
我们相应地定义loss_gradient函数:
def loss_gradient(y_hat, y_true):
"""output layer gradient"""
return y_hat - y_true
输出层梯度
要将更新传播回输出层权重,我们使用损失函数 J 对于权重矩阵的梯度:
和偏置项的梯度:
我们现在可以相应地定义output_weight_gradient和output_bias_gradient,两者都以损失梯度 作为输入:
def output_weight_gradient(H, loss_grad):
"""Gradients for the output layer weights"""
return H.T @ loss_grad
def output_bias_gradient(loss_grad):
"""Gradients for the output layer bias"""
return np.sum(loss_grad, axis=0, keepdims=True)
隐藏层梯度
损失函数对于隐藏层值的梯度计算如下,其中 表示逐元素的矩阵乘积:
我们定义一个hidden_layer_gradient函数来编码这个结果:
def hidden_layer_gradient(H, out_weights, loss_grad):
"""Error at the hidden layer.
H * (1-H) * (E . Wo^T)"""
return H * (1 - H) * (loss_grad @ out_weights.T)
隐藏层权重和偏置的梯度为:
相应的函数是:
def hidden_weight_gradient(X, hidden_layer_grad):
"""Gradient for the weight parameters at the hidden layer"""
return X.T @ hidden_layer_grad
def hidden_bias_gradient(hidden_layer_grad):
"""Gradient for the bias parameters at the output layer"""
return np.sum(hidden_layer_grad, axis=0, keepdims=True)
将所有内容整合起来
为了准备训练我们的网络,我们创建一个函数,该函数结合了先前的梯度定义,并从训练数据和标签以及当前的权重和偏置值计算相关的权重和偏置更新:
def compute_gradients(X, y_true, w_h, b_h, w_o, b_o):
"""Evaluate gradients for parameter updates"""
# Compute hidden and output layer activations
hidden_activations = hidden_layer(X, w_h, b_h)
y_hat = output_layer(hidden_activations, w_o, b_o)
# Compute the output layer gradients
loss_grad = loss_gradient(y_hat, y_true)
out_weight_grad = output_weight_gradient(hidden_activations, loss_grad)
out_bias_grad = output_bias_gradient(loss_grad)
# Compute the hidden layer gradients
hidden_layer_grad = hidden_layer_gradient(hidden_activations,
w_o, loss_grad)
hidden_weight_grad = hidden_weight_gradient(X, hidden_layer_grad)
hidden_bias_grad = hidden_bias_gradient(hidden_layer_grad)
return [hidden_weight_grad, hidden_bias_grad, out_weight_grad, out_bias_grad]
测试梯度
笔记本包含一个测试函数,该函数将先前使用多元微积分解析导出的梯度与我们通过轻微扰动单个参数获得的数值估计进行比较。测试函数验证了输出值的变化与分析梯度估计的变化类似。
使用 Python 实现动量更新
要将动量合并到参数更新中,定义一个update_momentum函数,该函数将我们刚刚使用的compute_gradients函数的结果与每个参数矩阵的最新动量更新组合起来:
def update_momentum(X, y_true, param_list, Ms, momentum_term, learning_rate):
"""Compute updates with momentum."""
gradients = compute_gradients(X, y_true, *param_list)
return [momentum_term * momentum - learning_rate * grads
for momentum, grads in zip(Ms, gradients)]
update_params函数执行实际的更新:
def update_params(param_list, Ms):
"""Update the parameters."""
return [P + M for P, M in zip(param_list, Ms)]
训练网络
要训练网络,我们首先使用标准正态分布随机初始化所有网络参数(参见笔记本)。对于给定的迭代次数或周期,我们运行动量更新并计算训练损失如下:
def train_network(iterations=1000, lr=.01, mf=.1):
# Initialize weights and biases
param_list = list(initialize_weights())
# Momentum Matrices = [MWh, Mbh, MWo, Mbo]
Ms = [np.zeros_like(M) for M in param_list]
train_loss = [loss(forward_prop(X, *param_list), Y)]
for i in range(iterations):
# Update the moments and the parameters
Ms = update_momentum(X, Y, param_list, Ms, mf, lr)
param_list = update_params(param_list, Ms)
train_loss.append(loss(forward_prop(X, *param_list), Y))
return param_list, train_loss
图 17.4 绘制了使用动量项为 0.5 和学习率为 1e-4 的 50,000 个训练样本进行 50,000 次迭代的训练损失。它显示损失需要超过 5,000 次迭代才开始下降,但然后下降速度非常快。我们没有使用 SGD,这可能会显著加速收敛。
图 17.4:每次迭代的训练损失
图 17.5 中的图表展示了具有三维隐藏层的神经网络从二维数据中学习的函数,这些数据有两个不是线性可分的类。左侧面板显示了源数据和决策边界,它误分类了非常少的数据点,并且随着持续训练将进一步改善。
中央面板显示了隐藏层学习的输入数据的表示。网络学习权重,以便将输入从二维投影到三维,从而使得两个类能够线性分离。右侧图显示了输出层如何以 0.5 的输出维度值作为线性分离的截止值:
图 17.5:可视化神经网络学习的函数
总结:我们已经看到一个非常简单的网络,只有一个包含三个节点的隐藏层和总共 17 个参数,能够学习如何使用反向传播和带动量的梯度下降来解决非线性分类问题。
我们接下来将回顾如何使用流行的 DL 库,这些库有助于设计复杂的架构并进行快速训练,同时使用复杂技术来防止过拟合并评估结果。
流行的深度学习库
目前最流行的 DL 库是 TensorFlow(由 Google 支持)、Keras(由 Francois Chollet 领导,现在在 Google)和 PyTorch(由 Facebook 支持)。开发非常活跃,截至 2020 年 3 月,PyTorch 版本为 1.4,TensorFlow 版本为 2.2。TensorFlow 2.0 将 Keras 作为其主要接口,有效地将两个库合并为一个。
所有的库都提供了我们在本章中讨论过的设计选择、正则化方法和反向传播优化。它们还能够在一个或多个图形处理单元(GPU)上进行快速训练。这些库在重点上略有不同,TensorFlow 最初设计用于在生产中部署,在工业界很普遍,而 PyTorch 在学术研究者中很受欢迎;然而,接口正在逐渐趋同。
我们将使用与上一节相同的网络架构和数据集来说明 TensorFlow 和 PyTorch 的使用。
利用 GPU 加速
DL 非常计算密集,而且好的结果通常需要大型数据集。因此,模型训练和评估可能会变得非常耗时。GPU 高度优化了深度学习模型所需的矩阵运算,并且往往具有更多的处理能力,使得加速 10 倍或更多不罕见。
所有流行的深度学习库都支持使用 GPU,并且一些还允许在多个 GPU 上进行并行训练。最常见的 GPU 类型由 NVIDIA 生产,配置需要安装和设置 CUDA 环境。这个过程不断发展,取决于您的计算环境,可能会有一定挑战。
利用 GPU 的更简单方法是通过 Docker 虚拟化平台。有大量的镜像可供您在由 Docker 管理的本地容器中运行,避免了您可能遇到的许多驱动程序和版本冲突。TensorFlow 在其网站上提供了 Docker 镜像,也可以与 Keras 一起使用。
在 GitHub 上查看 DL 笔记本和安装目录中的参考和相关说明。
如何使用 TensorFlow 2
TensorFlow 在 2015 年 9 月发布后不久成为了领先的深度学习库,在 PyTorch 之前一年。TensorFlow 2 简化了随着时间推移变得越来越复杂的 API,通过将 Keras API 作为其主要接口。
Keras 被设计为高级 API,以加速使用 TensorFlow、Theano 或 CNTK 等计算后端设计和训练深度神经网络的迭代工作流程。它在 2017 年被整合到 TensorFlow 中。您还可以结合两个库的代码,以利用 Keras 的高级抽象以及定制的 TensorFlow 图操作。
此外,TensorFlow 采用了即时执行。以前,您需要为编译成优化操作的完整计算图进行定义。运行编译后的图形需要配置会话并提供所需的数据。在即时执行下,您可以像常规 Python 代码一样逐行运行 TensorFlow 操作。
Keras 支持稍微简单的 Sequential API 和更灵活的 Functional API。我们将在此介绍前者,并在后续章节中的更复杂示例中使用 Functional API。
要创建模型,我们只需要实例化一个Sequential对象,并提供一个包含标准层序列及其配置的列表,包括单元数、激活函数类型或名称。
第一个隐藏层需要关于它从输入层通过input_shape参数接收到的矩阵中特征数的信息。在我们的简单案例中,只有两个。Keras 通过我们稍后将传递给本节中的fit方法的batch_size参数在训练期间推断需要处理的行数。TensorFlow 通过前一层的units参数推断接收到的输入的大小:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation
model = Sequential([
Dense(units=3, input_shape=(2,), name='hidden'),
Activation('sigmoid', name='logistic'),
Dense(2, name='output'),
Activation('softmax', name='softmax'),
])
Keras API 提供了许多标准构建模块,包括循环和卷积层,各种正则化选项,一系列损失函数和优化器,以及预处理,可视化和日志记录(请参阅 GitHub 上的 TensorFlow 文档链接以供参考)。它也是可扩展的。
模型的summary方法生成对网络架构的简明描述,包括层类型和形状的列表以及参数数量:
model.summary()
Layer (type) Output Shape Param #
=================================================================
hidden (Dense) (None, 3) 9
_________________________________________________________________
logistic (Activation) (None, 3) 0
_________________________________________________________________
output (Dense) (None, 2) 8
_________________________________________________________________
softmax (Activation) (None, 2) 0
=================================================================
Total params: 17
Trainable params: 17
Non-trainable params: 0
接下来,我们编译 Sequential 模型以配置学习过程。为此,我们定义优化器,损失函数以及一种或多种在训练期间监视的性能指标:
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['accuracy'])
Keras 使用回调函数来在训练期间启用某些功能,例如在 TensorBoard 中记录信息以供交互式显示(见下一节):
tb_callback = TensorBoard(log_dir='./tensorboard',
histogram_freq=1,
write_graph=True,
write_images=True)
要训练模型,我们调用它的fit方法,并在训练数据之外传递多个参数:
model.fit(X, Y,
epochs=25,
validation_split=.2,
batch_size=128,
verbose=1,
callbacks=[tb_callback])
请参阅笔记本以可视化决策边界,它类似于我们先前的手动网络实现的结果。不过,使用 TensorFlow 进行训练速度快了几个数量级。
如何使用 TensorBoard
TensorBoard 是 TensorFlow 附带的一套优秀的可视化工具,包括可视化工具以简化对 NNs 的理解,调试和优化。
您可以使用它来可视化计算图,绘制各种执行和性能指标,甚至可视化网络处理的图像数据。它还允许比较不同的训练运行。
运行how_to_use_tensorflow笔记本时,需要安装 TensorFlow,然后可以从命令行启动 TensorBoard:
tensorboard --logdir=/full_path_to_your_logs ## e.g. ./tensorboard
或者,您可以首先加载扩展程序,然后通过引用log目录类似地启动 TensorBoard,在您的笔记本中使用它:
%load_ext tensorboard
%tensorboard --logdir tensorboard/
首先,可视化包括训练和验证指标(请参阅图 17.6的左面板)。
此外,您还可以查看各个时期的权重和偏差的直方图(图 17.6 的右面板;时期从后到前演变)。这很有用,因为它允许您监视反向传播是否成功地在学习过程中调整权重以及它们是否收敛。
权重的值应该在多个时期内从它们的初始化值改变并最终稳定:
图 17.6:TensorBoard 学习过程可视化
TensorBoard 还允许您显示和交互式探索网络的计算图,通过单击各个节点从高级结构向下钻取到底层操作。我们简单示例架构的可视化(请参阅笔记本)已经包含了许多组件,但在调试时非常有用。有关更详细的参考,请参阅 GitHub 上更详细的教程链接。
如何使用 PyTorch 1.4
PyTorch 是在由 Yann LeCunn 领导的Facebook AI 研究(FAIR)团队开发的,并于 2016 年 9 月发布了第一个 alpha 版本。它与 NumPy 等 Python 库深度集成,可以用于扩展其功能,具有强大的 GPU 加速和使用其 autograd 系统进行自动微分。通过更低级别的 API,它提供比 Keras 更细粒度的控制,并且主要用作深度学习研究平台,但也可以在启用 GPU 计算的同时替代 NumPy。
它采用即时执行,与 Theano 或 TensorFlow 等使用静态计算图的方式形成对比。与最初为了快速但静态执行而定义和编译网络不同,它依赖于其 autograd 包来自动对张量操作进行微分;也就是说,它在“飞行中”计算梯度,以便更轻松地部分修改网络结构。这称为按运行定义,意味着反向传播是由代码运行方式定义的,这又意味着每次迭代都可能不同。PyTorch 文档提供了关于此的详细教程。
结合结果灵活性和直观的 Python 首选界面以及执行速度,这导致了它的迅速普及和众多支持库的开发,这些支持库扩展了其功能。
让我们通过实现我们的简单网络架构来看看 PyTorch 和 autograd 如何工作(详细信息请参见how_to_use_pytorch笔记本)。
如何创建 PyTorch 的 DataLoader
我们首先将 NumPy 或 pandas 输入数据转换为torch张量。从 NumPy 到 PyTorch 的转换非常简单:
import torch
X_tensor = torch.from_numpy(X)
y_tensor = torch.from_numpy(y)
X_tensor.shape, y_tensor.shape
(torch.Size([50000, 2]), torch.Size([50000]))
我们可以使用这些 PyTorch 张量首先实例化一个TensorDataset,然后在第二步实例化一个包含有关batch_size信息的DataLoader:
import torch.utils.data as utils
dataset = utils.TensorDataset(X_tensor,y_tensor)
dataloader = utils.DataLoader(dataset,
batch_size=batch_size,
shuffle=True)
如何定义神经网络架构
PyTorch 使用Net()类定义了一个 NN 架构。其核心元素是forward函数。autograd 自动定义了相应的backward函数来计算梯度。
任何合法的张量操作都可以用于forward函数,提供了设计灵活性的记录。在我们的简单情况下,我们只是在初始化其属性后通过功能输入输出关系链接张量:
import torch.nn as nn
class Net(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super(Net, self).__init__() # Inherited from nn.Module
self.fc1 = nn.Linear(input_size, hidden_size)
self.logistic = nn.LogSigmoid()
self.fc2 = nn.Linear(hidden_size, num_classes)
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
"""Forward pass: stacking each layer together"""
out = self.fc1(x)
out = self.logistic(out)
out = self.fc2(out)
out = self.softmax(out)
return out
然后我们实例化一个Net()对象,并且可以按照以下方式检查其架构:
net = Net(input_size, hidden_size, num_classes)
net
Net(
(fc1): Linear(in_features=2, out_features=3, bias=True)
(logistic): LogSigmoid()
(fc2): Linear(in_features=3, out_features=2, bias=True)
(softmax): Softmax()
)
为了说明即时执行,我们还可以检查第一个张量中初始化的参数:
list(net.parameters())[0]
Parameter containing:
tensor([[ 0.3008, -0.2117],
[-0.5846, -0.1690],
[-0.6639, 0.1887]], requires_grad=True)
要启用 GPU 处理,您可以使用net.cuda()。请参阅 PyTorch 文档以将张量放置在 CPU 和/或一个或多个 GPU 单元上。
我们还需要定义损失函数和优化器,使用一些内置选项:
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate)
如何训练模型
模型训练包括对每个 epoch 的外循环,即对训练数据的每次传递,以及对 DataLoader 产生的批次的内循环。这执行学习算法的前向和后向传递。需要小心地调整数据类型以满足各种对象和函数的要求;例如,标签需要是整数,特征应该是 float 类型:
for epoch in range(num_epochs):
print(epoch)
for i, (features, label) in enumerate(dataloader):
features = Variable(features.float())
label = Variable(label.long())
# Initialize the hidden weights
optimizer.zero_grad()
# Forward pass: compute output given features
outputs = net(features)
# Compute the loss
loss = criterion(outputs, label)
# Backward pass: compute the gradients
loss.backward()
# Update the weights
optimizer.step()
笔记本还包含一个示例,使用 livelossplot 包绘制损失,这是由 Keras 提供的开箱即用的功能。
如何评估模型预测
要从我们训练的模型中获得预测,我们传递特征数据并将预测转换为 NumPy 数组。我们获得了每个类别的 softmax 概率:
test_value = Variable(torch.from_numpy(X)).float()
prediction = net(test_value).data.numpy()
Prediction.shape
(50000, 2)
从这里开始,我们可以像以前一样继续计算损失指标或可视化结果,再次生成我们之前找到的决策边界的一个版本。
可选方案
对深度学习的巨大兴趣导致了几个竞争性库的开发,这些库促进了神经网络的设计和训练。最突出的包括以下示例(还请参阅 GitHub 上的参考资料)。
Apache MXNet
MXNet,由 Apache 基金会孵化,是一个用于训练和部署深度神经网络的开源深度学习软件框架。它专注于可扩展性和快速模型训练。他们包括了 Gluon 高级接口,使得原型设计、训练和部署深度学习模型变得容易。MXNet 已被亚马逊选为 AWS 上的深度学习工具。
Microsoft Cognitive Toolkit(CNTK)
Cognitive Toolkit,以前称为 CNTK,是微软对深度学习库的贡献。它将神经网络描述为通过有向图的一系列计算步骤,类似于 TensorFlow。在这个有向图中,叶节点代表输入值或网络参数,而其他节点代表对它们的输入进行的矩阵操作。CNTK 允许用户构建和组合从深度前馈神经网络、卷积网络到循环网络(RNNs/LSTMs)的流行模型架构。
fastai
fastai 库旨在使用现代最佳实践简化训练快速而准确的神经网络。这些实践是从该公司对深度学习的研究中产生的,该公司提供了免费的软件和相关课程。fastai 支持处理图像、文本、表格和协同过滤数据的模型。
为长短策略优化神经网络
在实践中,我们需要探索对神经网络架构的设计选项以及我们如何训练它的变化,因为我们从一开始就无法确定哪种配置最适合数据。在本节中,我们将探讨使用在 第十二章 中开发的数据集预测每日股票回报的简单前馈神经网络的各种架构(请参见该章节的 GitHub 目录中的笔记本 preparing_the_model_data)。
为此,我们将定义一个函数,根据几个架构输入参数返回一个 TensorFlow 模型,并使用我们在第七章,线性模型 - 从风险因素到收益预测中介绍的MultipleTimeSeriesCV交叉验证备选设计。为了评估模型预测的信号质量,我们构建了一个基于模型集成的简单基于排名的多头空头策略,在样本内交叉验证期间表现最佳的模型基础上。为了限制假发现的风险,我们然后评估该策略在样本外测试期间的表现。
有关详细信息,请参见optimizing_a_NN_architecture_for_trading笔记本。
工程特征以预测每日股票收益
为了开发我们的交易策略,我们使用了从 2010 年到 2017 年的八年期间的 995 只美国股票的日收益。我们将使用在第十二章,提升您的交易策略中开发的特征,其中包括波动率和动量因子,以及带有横截面和部门排名的滞后收益。我们按如下方式加载数据:
data = pd.read_hdf('../12_gradient_boosting_machines/data/data.h5',
'model_data').dropna()
outcomes = data.filter(like='fwd').columns.tolist()
lookahead = 1
outcome= f'r{lookahead:02}_fwd'
X = data.loc[idx[:, :'2017'], :].drop(outcomes, axis=1)
y = data.loc[idx[:, :'2017'], outcome]
定义一个神经网络架构框架
为了自动化生成我们的 TensorFlow 模型,我们创建了一个函数,根据后续可以在交叉验证迭代期间传递的参数来构建和编译模型。
以下的make_model函数说明了如何灵活定义搜索过程的各种架构元素。dense_layers参数将网络的深度和宽度定义为整数列表。我们还使用dropout进行正则化,表示为在[0,1]范围内的浮点数,用于定义在训练迭代中排除给定单元的概率:
def make_model(dense_layers, activation, dropout):
'''Creates a multi-layer perceptron model
dense_layers: List of layer sizes; one number per layer
'''
model = Sequential()
for i, layer_size in enumerate(dense_layers, 1):
if i == 1:
model.add(Dense(layer_size, input_dim=X_cv.shape[1]))
model.add(Activation(activation))
else:
model.add(Dense(layer_size))
model.add(Activation(activation))
model.add(Dropout(dropout))
model.add(Dense(1))
model.compile(loss='mean_squared_error',
optimizer='Adam')
return model
现在我们可以转向交叉验证过程,评估各种神经网络架构。
交叉验证设计选项以调整 NN
我们使用MultipleTimeSeriesCV将数据分割为滚动训练和验证集,包括 24 * 12 个月的数据,同时保留最后 12 * 21 天的数据(从 2016 年 11 月 30 日开始)作为保留测试。我们对每个模型进行 48 个 21 天期的训练,并在 3 个 21 天期内评估其结果,这意味着在交叉验证和测试期间共有 12 个拆分:
n_splits = 12
train_period_length=21 * 12 * 4
test_period_length=21 * 3
cv = MultipleTimeSeriesCV(n_splits=n_splits,
train_period_length=train_period_length,
test_period_length=test_period_length,
lookahead=lookahead)
接下来,我们为交叉验证定义一组配置。这些包括两个隐藏层和 dropout 概率的几个选项;我们只会使用 tanh 激活,因为一次试验没有显示出与 ReLU 相比的显著差异。(我们也可以尝试不同的优化器。但我建议您不要运行这个实验,以限制已经是计算密集型工作的内容):
dense_layer_opts = [(16, 8), (32, 16), (32, 32), (64, 32)]
dropout_opts = [0, .1, .2]
param_grid = list(product(dense_layer_opts, activation_opts, dropout_opts))
np.random.shuffle(param_grid)
len(param_grid)
12
要运行交叉验证,我们定义一个函数,根据MultipleTimeSeriesCV生成的整数索引来生成训练和验证数据,如下所示:
def get_train_valid_data(X, y, train_idx, test_idx):
x_train, y_train = X.iloc[train_idx, :], y.iloc[train_idx]
x_val, y_val = X.iloc[test_idx, :], y.iloc[test_idx]
return x_train, y_train, x_val, y_val
在交叉验证期间,我们使用之前定义的网格中的一组参数训练一个模型 20 个时期。每个时期结束后,我们存储一个包含学习权重的checkpoint,这样我们就可以重新加载它们,以快速生成最佳配置的预测,而无需重新训练。
每个时期结束后,我们计算并存储验证集的信息系数(IC)按天计算:
ic = []
scaler = StandardScaler()
for params in param_grid:
dense_layers, activation, dropout = params
for batch_size in [64, 256]:
checkpoint_path = checkpoint_dir / str(dense_layers) / activation /
str(dropout) / str(batch_size)
for fold, (train_idx, test_idx) in enumerate(cv.split(X_cv)):
x_train, y_train, x_val, y_val = get_train_valid_data(X_cv, y_cv,
train_idx, test_idx)
x_train = scaler.fit_transform(x_train)
x_val = scaler.transform(x_val)
preds = y_val.to_frame('actual')
r = pd.DataFrame(index=y_val.groupby(level='date').size().index)
model = make_model(dense_layers, activation, dropout)
for epoch in range(20):
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=1, validation_data=(x_val, y_val))
model.save_weights(
(checkpoint_path / f'ckpt_{fold}_{epoch}').as_posix())
preds[epoch] = model.predict(x_val).squeeze()
r[epoch] = preds.groupby(level='date').apply(lambda x: spearmanr(x.actual, x[epoch])[0]).to_frame(epoch)
ic.append(r.assign(dense_layers=str(dense_layers),
activation=activation,
dropout=dropout,
batch_size=batch_size,
fold=fold))
使用 NVIDIA GTX 1080 GPU,20 个时期的批处理大小为 64 个样本的计算时间超过 1 小时,而批处理大小为 256 个样本则约为 20 分钟。
评估预测性能
让我们首先看一下在交叉验证期间实现了最高中位数日 IC 的五个模型。以下代码计算这些值:
dates = sorted(ic.index.unique())
cv_period = 24 * 21
cv_dates = dates[:cv_period]
ic_cv = ic.loc[cv_dates]
(ic_cv.drop('fold', axis=1).groupby(params).median().stack()
.to_frame('ic').reset_index().rename(columns={'level_3': 'epoch'})
.nlargest(n=5, columns='ic'))
结果表显示,使用 32 个单位的 32 个架构在两层中以及在第一/第二层中分别使用 16/8 的架构表现最佳。这些模型还使用了dropout,并且使用给定数量的时期对所有折叠进行了 64 个样本的批处理训练。中位数 IC 值在 0.0236 和 0.0246 之间变化:
| 稠密层 | 丢失率 | 批次大小 | 时期 | IC |
|---|---|---|---|---|
| (32, 32) | 0.1 | 64 | 7 | 0.0246 |
| (16, 8) | 0.2 | 64 | 14 | 0.0241 |
| (16, 8) | 0.1 | 64 | 3 | 0.0238 |
| (32, 32) | 0.1 | 64 | 10 | 0.0237 |
| (16, 8) | 0.2 | 256 | 3 | 0.0236 |
接下来,我们将看看参数选择如何影响预测性能。
首先,我们通过时期可视化不同配置的每折日信息系数(平均值),以了解训练持续时间如何影响预测准确性。然而,在图 17.7中的图表突出显示出一些明确的模式;IC 在模型之间变化很小,并且在时期之间并没有特别系统地变化:
图 17.7:各种模型配置的信息系数
为了获得更具统计意义的见解,我们使用普通最小二乘法(OLS)进行线性回归(参见第七章,线性模型 - 从风险因素到收益预测),使用关于层、丢失率和批次大小选择以及每个时期的虚拟变量:
data = pd.melt(ic, id_vars=params, var_name='epoch', value_name='ic')
data = pd.get_dummies(data, columns=['epoch'] + params, drop_first=True)
model = sm.OLS(endog=data.ic, exog=sm.add_constant(data.drop('ic', axis=1)))
图 17.8中的图表绘制了每个回归系数的置信区间;如果不包含零,则系数在百分之五的水平上是显著的。y 轴上的 IC 值反映了与舍弃每个虚拟变量类别的配置的样本平均值相对差异(0.0027,p 值:0.017)。
在所有配置中,批处理大小为 256 和丢失率为 0.2 对性能产生了显著(但微小)的正面影响。类似地,训练七个时期产生了略微优越的结果。根据 F 统计量,回归总体上是显著的,但 R2 值非常低,接近零,强调了数据中噪音相对于参数选择传递的信号的高程度。
图 17.8:OLS 系数和置信区间
基于集成信号回测策略
要将我们的 NN 模型转换为交易策略,我们生成预测,评估其信号质量,创建定义如何根据这些预测进行交易的规则,并回测实施这些规则的策略的性能。请参阅笔记本backtesting_with_zipline以获取本节中的代码示例。
集成预测以产生可交易信号
为了减少预测的方差并对样本内过拟合进行套期保值,我们结合了在前一节表中列出的三个最佳模型的预测,并平均了结果。
为此,我们定义以下generate_predictions()函数,该函数接收模型参数作为输入,加载所需时期模型的权重,并为交叉验证和样本外期间创建预测(这里仅显示关键内容以节省空间):
def generate_predictions(dense_layers, activation, dropout,
batch_size, epoch):
checkpoint_dir = Path('logs')
checkpoint_path = checkpoint_dir / dense_layers / activation /
str(dropout) / str(batch_size)
for fold, (train_idx, test_idx) in enumerate(cv.split(X_cv)):
x_train, y_train, x_val, y_val = get_train_valid_data(X_cv, y_cv,
train_idx,
test_idx)
x_val = scaler.fit(x_train).transform(x_val)
model = make_model(dense_layers, activation, dropout, input_dim)
status = model.load_weights(
(checkpoint_path / f'ckpt_{fold}_{epoch}').as_posix())
status.expect_partial()
predictions.append(pd.Series(model.predict(x_val).squeeze(),
index=y_val.index))
return pd.concat(predictions)
我们使用 Alphalens 和 Zipline 回测来存储评估结果。
使用 Alphalens 评估信号质量
为了对集成模型预测的信号内容有所了解,我们使用 Alphalens 计算了根据预测分位数区分的五个等权重投资组合的回报差异(见图 17.9)。在一个交易日的持有期间,最高分位和最低分位之间的差距约为 8 个基点,这意味着 alpha 为 0.094,beta 为 0.107:
图 17.9:信号质量评估
使用 Zipline 回测策略
基于 Alphalens 分析,我们的策略将为具有最高正预测回报和最低负预测回报的 50 只股票输入长和短头寸,只要每边至少有 10 个选项。该策略每天进行交易。
图 17.10中的图表显示,该策略在样本内和样本外表现良好(在交易成本之前):
图 17.10:样本内和样本外回测表现
它在 36 个月的期间内产生了年化收益率为 22.8%,在样本内 24 个月为 16.5%,在样本外 12 个月为 35.7%。夏普比率在样本内为 0.72,在样本外为 2.15,提供了 0.18(0.29)的 alpha 和 0.24(0.16)的 beta 在/样本外。
如何进一步改进结果
相对简单的架构产生了一些有希望的结果。要进一步提高性能,首先可以添加新功能和更多数据到模型中。
或者,您可以使用更复杂的架构,包括适用于顺序数据的 RNN 和 CNN,而香草前馈 NN 并不设计捕获特征的有序性。
我们将在下一章中转向这些专用架构。
摘要
在本章中,我们将 DL(深度学习)介绍为一种从高维、非结构化数据中提取层次特征的表征学习形式。我们看到了如何使用 NumPy 设计、训练和正则化前馈神经网络。我们演示了如何使用流行的 DL 库 PyTorch 和 TensorFlow,这些库适用于从快速原型到生产部署的用例。
最重要的是,我们使用 TensorFlow 设计和调优了一个神经网络(NN),能够在样本内和样本外期间生成可交易的信号,从而获得了可观的回报。
在下一章中,我们将探讨 CNNs,它们特别适用于图像数据,但也适用于序列数据。