原文:
annas-archive.org/md5/ef46492e324ad2e4ffd771274a9405da译者:飞龙
第九章:使用混合和堆叠解决方案进行集成
当你开始在 Kaggle 上竞争时,很快就会意识到你不能仅凭一个精心设计的模型获胜;你需要集成多个模型。接下来,你将立即想知道如何设置一个有效的集成。周围很少有指南,而且 Kaggle 的经验比科学论文还要多。
这里要说明的是,如果集成是赢得 Kaggle 竞赛的关键,那么在现实世界中,它与复杂性、维护性差、难以重现以及微小的技术成本相关,而这些成本往往掩盖了优势。通常,那种能让你从排名较低跃升至排行榜顶部的微小提升,对于现实世界的应用来说并不重要,因为成本掩盖了优势。然而,这并不意味着集成在现实世界中完全没有使用。在有限的形式下,如平均和混合几个不同的模型,集成使我们能够以更有效和更高效的方式解决许多数据科学问题。
在 Kaggle 中,集成不仅仅是提高预测性能的一种方法,它也是一种团队合作策略。当你与其他队友一起工作时,将每个人的贡献整合在一起,往往会产生比个人努力更好的结果,并且还可以通过将每个人的努力结构化,朝着明确的目标组织团队工作。实际上,当工作在不同时区进行,并且每个参与者都有不同的约束条件时,像结对编程这样的协作技术显然是不可行的。一个团队成员可能因为工作时间受到限制,另一个可能因为学习和考试,等等。
在竞赛中,团队往往没有机会,也不一定需要,将所有参与者同步和协调到同一任务上。此外,团队内的技能也可能有所不同。
在团队中共享的良好的集成策略意味着个人可以继续根据自己的常规和风格工作,同时仍然为团队的成功做出贡献。因此,即使不同的技能在使用基于预测多样性的集成技术时也可能成为优势。
在本章中,我们将从您已经了解的集成技术开始,因为它们嵌入在随机森林和梯度提升等算法中,然后进一步介绍针对多个模型的集成技术,如平均、混合和堆叠。我们将为您提供一些理论、一些实践,以及一些代码示例,您可以在 Kaggle 上构建自己的解决方案时将其用作模板。
我们将涵盖以下主题:
-
集成算法简介
-
将模型平均集成到集成中
-
使用元模型混合模型
-
堆叠模型
-
创建复杂的堆叠和混合解决方案
在让您阅读本章并尝试所有技术之前,我们必须提到一个关于集成对我们和所有 Kaggle 竞赛者的伟大参考:由Triskelion (Hendrik Jacob van Veen) 和几位合作者 (Le Nguyen The Dat, Armando Segnini) 在 2015 年撰写的博客文章。Kaggle 集成指南最初可以在mlwave博客上找到 (mlwave.com/kaggle-ensembling-guide),但现在已不再更新,但您可以从usermanual.wiki/Document/Kaggle20ensembling20guide.685545114.pdf检索指南的内容。该文章整理了当时 Kaggle 论坛上关于集成的多数隐性和显性知识。
集成算法简介
模型集成可以优于单个模型的想法并非最近才出现。我们可以追溯到维多利亚时代的英国爵士 弗朗西斯·高尔顿,他发现,为了猜测在县博览会上一头牛的重量,从一群或多或少受过教育的人那里收集的大量估计的平均值比从专家那里得到的单个精心设计的估计更有用。
在 1996 年,Leo Breiman 通过说明袋装技术(也称为“自助聚合”过程)来形式化使用多个模型组合成一个更具预测性的模型的想法,这后来导致了更有效的随机森林算法的发展。在此之后,其他集成技术如梯度提升和堆叠也被提出,从而完成了我们今天使用的集成方法范围。
您可以参考几篇文章来了解这些集成算法最初是如何设计的:
-
对于随机森林,请阅读 Breiman, L. 的文章 Bagging predictors。机器学习 24.2 – 1996: 123-140。
-
如果您想更详细地了解提升最初是如何工作的,请阅读 Freund, Y. 和 Schapire, R.E. 的文章 Experiments with a new boosting algorithm. icml. Vol. 96 – 1996,以及 Friedman, J. H. 的文章 Greedy function approximation: a gradient boosting machine. Annals of Statistics (2001): 1189-1232。
-
至于堆叠,请参考 Ting, K. M. 和 Witten, I. H. 的文章 Stacking bagged and dagged models,1997 年,这是该技术的第一个正式草案。
Kaggle 竞赛中集成预测者的第一个基本策略直接来源于分类和回归的 bagging 和随机森林策略。它们涉及对各种预测进行平均,因此被称为平均技术。这些方法在 11 年前举办的第一个 Kaggle 竞赛中迅速出现,也得益于 Kaggle 之前的 Netflix 竞赛,在那里基于不同模型结果平均的策略主导了场景。鉴于它们的成功,基于平均的基本集成技术为许多即将到来的竞赛设定了标准,并且它们至今仍然非常有用和有效,有助于在排行榜上获得更高的分数。
Stacking,这是一种更复杂且计算量更大的方法,在竞赛问题变得更加复杂和参与者之间的竞争更加激烈时出现。正如随机森林方法启发了对不同预测的平均一样,提升法极大地启发了堆叠方法。在提升法中,通过顺序重新处理信息,你的学习算法可以以更好和更完整的方式建模问题。实际上,在梯度提升中,为了建模先前迭代无法掌握的数据部分,会构建顺序决策树。这种想法在堆叠集成中得到了重申,在那里你堆叠先前模型的输出并重新处理它们,以获得预测性能的提升。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/Rob_Mulla.png
罗布·穆拉
罗布与我们分享了他在集成和从 Kaggle 中学到的经验。作为竞赛、笔记本和讨论的大师,以及 Biocore LLC 的高级数据科学家,我们可以从他丰富的经验中学到很多东西。
你最喜欢的竞赛类型是什么?为什么?在技术和解决方法方面,你在 Kaggle 上的专长是什么?
我最喜欢的竞赛类型是那些涉及独特数据集,需要结合不同类型的建模方法来提出新颖解决方案的竞赛。我喜欢当竞赛不仅仅是训练大型模型,而是真正需要深入理解数据并实施利用特定任务架构的想法时。我不试图专精于任何特定的方法。当我第一次开始参加 Kaggle 时,我主要坚持使用梯度提升模型,但为了在近年来保持竞争力,我加深了对深度学习、计算机视觉、NLP 和优化的理解。我最喜欢的竞赛需要使用不止一种技术。
你是如何参加 Kaggle 竞赛的?这种方法和你在日常工作中所做的方法有何不同?
我在某些方面将 Kaggle 比赛与工作项目非常相似。首先是对数据理解。在现实世界项目中,你可能需要定义问题和开发一个好的指标。在 Kaggle 中,这些已经为你准备好了。接下来是理解数据和指标之间的关系——以及开发和测试你认为将最好解决问题的建模技术。与现实生活中数据科学相比,Kaggle 最大的不同之处在于最后一点,即集成和调整模型以获得微小的优势——在许多现实世界的应用中,这些类型的大型集成不是必要的,因为计算成本与性能提升之间的差距可能很小。
告诉我们你参加的一个特别具有挑战性的比赛,以及你使用了哪些见解来应对任务。
我参加的一个极具挑战性的比赛是 NFL 头盔冲击检测 比赛。它涉及视频数据,我对这个领域没有先前的经验。它还要求研究常见的方法和阅读该主题的现有论文。我必须工作在两阶段的方法上,这增加了解决方案的复杂性。另一个我认为具有挑战性的比赛是室内定位导航 比赛。它涉及建模、优化,以及真正理解数据。我在比赛中并没有做得很好,但我学到了很多。
Kaggle 是否帮助你在职业生涯中?如果是,那么是如何帮助的?
是的。Kaggle 在帮助我在数据科学领域获得知名度方面发挥了重要作用。我也在知识和理解新技术方面有所增长,并且遇到了许多才华横溢的人,他们帮助我在技能和机器学习理解方面成长。
我的团队在 NFL 头盔冲击检测 比赛中获得了第二名。在那场比赛之前,我还参加了许多 NFL 比赛。比赛的主持人联系了我,最终这帮助我获得了现在的职位。
在你的经验中,不经验丰富的 Kagglers 通常忽略了什么?你现在知道什么,而当你刚开始时希望知道的呢?
我认为不经验丰富的 Kagglers 有时过于担心模型的集成和超参数调整。这些在比赛的后期很重要,但除非你已经建立了一个良好的基础模型,否则它们并不重要。我也认为完全理解比赛指标非常重要。许多 Kagglers 忽略了理解如何优化解决方案以适应评估指标的重要性。
你在过去比赛中犯过哪些错误?
很多。我曾经过度拟合模型,花费时间在最终没有带来益处的事情上。然而,我认为这对我在未来比赛中更好地应对是必要的。这些错误可能在特定的比赛中伤害了我,但帮助我在后来的比赛中变得更好。
对于数据分析或机器学习,您会推荐使用哪些特定的工具或库?
对于数据探索性分析(EDA),了解如何使用 NumPy、Pandas 和 Matplotlib 或另一个绘图库来操作数据。对于建模,了解如何使用 Scikit-learn 设置适当的交叉验证方案。标准模型如 XGBoost/LightGBM 了解如何设置基线是有用的。深度学习库主要是 TensorFlow/Keras 或 PyTorch。了解这两个主要深度学习库中的任何一个都很重要。
将模型平均到一个集成中
为了更好地介绍平均集成技术,让我们快速回顾 Leo Breiman 为集成设计的所有策略。他的工作代表了集成策略的一个里程碑,他在当时发现的方法在广泛的问题中仍然相当有效。
Breiman 探索了所有这些可能性,以确定是否有一种方法可以减少那些倾向于过度拟合训练数据的强大模型的误差方差,例如决策树。
从概念上讲,他发现集成效果基于三个要素:我们如何处理训练案例的抽样,我们如何构建模型,以及最后,我们如何组合获得的不同模型。
关于抽样,测试并发现的方法有:
-
粘贴,即使用示例(数据行)的子样本(不放回抽样)构建多个模型
-
袋装,即使用随机选择的 bootstrap 示例(放回抽样)构建多个模型
-
随机子空间,即使用特征(数据列)的子样本(不放回抽样)构建多个模型
-
随机补丁,一种类似于袋装的方法,除了在为每个模型选择时也抽样特征,就像在随机子空间中一样
我们抽样而不是使用相同信息的原因是,通过子抽样案例和特征,我们创建了所有与同一问题相关的模型,同时每个模型又与其他模型不同。这种差异也适用于每个模型如何过度拟合样本;我们期望所有模型以相同的方式从数据中提取有用的、可推广的信息,并以不同的方式处理对预测无用的噪声。因此,建模中的变化减少了预测中的变化,因为错误往往相互抵消。
如果这种变化如此有用,那么下一步不应该只是修改模型学习的数据,还应该修改模型本身。我们有两种主要的模型方法:
-
同类型模型的集成
-
不同模型的集成
有趣的是,如果我们将要组合的模型在预测能力上差异太大,那么以某种方式集成并不会带来太多帮助。这里的要点是,如果你能组合能够正确猜测相同类型预测的模型,那么它们可以在平均错误预测时平滑它们的错误。如果你正在集成性能差异太大的模型,你很快就会意识到这没有意义,因为总体效果将是负面的:因为你没有平滑你的错误预测,你也在降低正确的预测。
这是有关于平均的一个重要限制:它只能使用一组不同的模型(例如,因为它们使用不同的样本和特征进行训练)如果它们在预测能力上相似。以一个例子来说,线性回归和k最近邻算法在建模问题和从数据中捕捉信号方面有不同的方式;得益于它们核心的(独特的)特征函数形式,这些算法可以从数据中捕捉到不同的预测细微差别,并在预测任务的特定部分上表现更好,但当你使用平均时,你实际上无法利用这一点。相比之下,算法必须捕获信号的不同方式是堆叠实际上可以利用的,因为它可以从每个算法中获取最佳结果。
基于此,我们可以总结出,为了使基于平均的集成(平均多个模型的输出)有效,它应该:
-
建立在训练在不同样本上的模型之上
-
建立在从可用特征的不同子样本中使用的模型之上
-
由具有相似预测能力的模型组成
从技术上讲,这意味着模型的预测应该在预测任务上保持尽可能不相关,同时达到相同的准确度水平。
现在我们已经讨论了平均多个机器学习模型的机遇和限制,我们最终将深入探讨其技术细节。平均多个分类或回归模型有三种方法:
-
多数投票,使用多个模型中最频繁的分类(仅适用于分类模型)
-
平均值或概率
-
使用值或概率的加权平均值
在接下来的几节中,我们将详细讨论每种方法在 Kaggle 比赛中的具体应用。
多数投票
通过改变我们在集成中使用的示例、特征和模型(如果它们在预测能力上相似,如我们之前讨论的)来产生不同的模型,这需要一定的计算工作量,但不需要你构建一个与使用单个模型时设置完全不同的数据处理管道。
在这个流程中,你只需要收集不同的测试预测,记录所使用的模型,训练时如何采样示例或特征,你使用的超参数,以及最终的交叉验证性能。
如果比赛要求你预测一个类别,你可以使用多数投票;也就是说,对于每个预测,你选择你的模型中最频繁预测的类别。这适用于二元预测和多类别预测,因为它假设你的模型有时会有错误,但它们大多数时候可以正确猜测。多数投票被用作“错误纠正程序”,丢弃噪声并保留有意义的信号。
在我们的第一个简单示例中,我们演示了多数投票是如何工作的。我们首先创建我们的示例数据集。使用 Scikit-learn 中的make_classification函数,我们生成一个类似 Madelon 的数据集。
原始的 Madelon 是一个包含数据点的合成数据集,这些数据点被分组在某个维超立方体的顶点上,并随机标记。它包含一些信息性特征,混合了无关的和重复的特征(以在特征之间创建多重共线性),并且它包含一定量的注入随机噪声。由Isabelle Guyon(SVM 算法的创造者之一)为 2003 年 NIPS 特征选择挑战赛所构思,Madelon 数据集是具有挑战性的合成数据集的模型示例。甚至一些 Kaggle 比赛也受到了它的启发:www.kaggle.com/c/overfitting和更近期的www.kaggle.com/c/dont-overfit-ii。
我们将在本章中用这个 Madelon 数据集的重建作为测试集成技术的基础:
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
X, y = make_classification(n_samples=5000, n_features=50,
n_informative=10,
n_redundant=25, n_repeated=15,
n_clusters_per_class=5,
flip_y=0.05, class_sep=0.5,
random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.33, random_state=0)
在将其分为训练集和测试集之后,我们继续实例化我们的学习算法。我们将仅使用三个基础算法:SVMs、随机森林和k最近邻分类器,以默认超参数进行演示。你可以尝试更改它们或增加它们的数量:
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import log_loss, roc_auc_score, accuracy_score
model_1 = SVC(probability=True, random_state=0)
model_2 = RandomForestClassifier(random_state=0)
model_3 = KNeighborsClassifier()
下一步仅仅是训练每个模型在训练集上:
model_1.fit(X_train, y_train)
model_2.fit(X_train, y_train)
model_3.fit(X_train, y_train)
在这一点上,我们需要对每个模型和集成进行测试集预测,并使用多数投票集成这些预测。为此,我们将使用 SciPy 中的mode函数:
import numpy as np
from scipy.stats import mode
preds = np.stack([model_1.predict(X_test),
model_2.predict(X_test),
model_3.predict(X_test)]).T
max_voting = np.apply_along_axis(mode, 1, preds)[:,0]
首先,我们检查每个单独模型的准确性:
for i, model in enumerate(['SVC', 'RF ', 'KNN']):
acc = accuracy_score(y_true=y_test, y_pred=preds[:, i])
print(f"Accuracy for model {model} is: {acc:0.3f}")
我们看到三个模型的表现相似,大约在0.8左右。现在是我们检查多数投票集成的时候了:
max_voting_accuray = accuracy_score(y_true=y_test, y_pred=max_voting)
print(f"Accuracy for majority voting is: {max_voting_accuray:0.3f}")
投票集成实际上更准确:0.817,因为它成功地整合了大多数正确的信号。
对于多标签问题(当你可以预测多个类别时),你可以简单地选择那些被预测超过一定次数的类别,假设一个相关性阈值表示对类别的预测是信号,而不是噪声。例如,如果你有五个模型,你可以将这个阈值设置为 3,这意味着如果一个类别被至少三个模型预测,那么这个预测应该被认为是正确的。
在回归问题中,以及当你预测概率时,实际上你不能使用多数投票。多数投票仅与类别所有权相关。相反,当你需要预测数字时,你需要数值地组合结果。在这种情况下,求助于平均数或加权平均数将为你提供组合预测的正确方法。
模型预测的平均值
在比赛中平均不同模型的预测时,你可以认为所有预测都具有潜在的相同预测能力,并使用算术平均数来得出平均值。
除了算术平均数之外,我们还发现使用以下方法也非常有效:
-
几何平均数:这是将n个提交相乘,然后取结果的1/n次幂。
-
对数平均数:类似于几何平均数,你对提交取对数,将它们平均在一起,然后取结果的指数。
-
调和平均数:这是取你提交的倒数算术平均数,然后取结果的倒数。
-
幂平均数:这是取提交的n次幂的平均值,然后取结果的1/n次幂。
简单的算术平均总是非常有效,基本上是一个无需思考就能奏效的方法,其效果往往比预期的更好。有时,像几何平均数或调和平均数这样的变体可能会更有效。
继续上一个例子,我们现在将尝试找出当我们切换到ROC-AUC作为评估指标时,哪种平均数效果最好。首先,我们将评估每个单独模型的性能:
proba = np.stack([model_1.predict_proba(X_test)[:, 1],
model_2.predict_proba(X_test)[:, 1],
model_3.predict_proba(X_test)[:, 1]]).T
for i, model in enumerate(['SVC', 'RF ', 'KNN']):
ras = roc_auc_score(y_true=y_test, y_score=proba[:, i])
print(f"ROC-AUC for model {model} is: {ras:0.5f}")
结果显示范围从0.875到0.881。
我们第一次测试使用的是算术平均数:
arithmetic = proba.mean(axis=1)
ras = roc_auc_score(y_true=y_test, y_score=arithmetic)
print(f"Mean averaging ROC-AUC is: {ras:0.5f}")
结果的 ROC-AUC 分数明显优于单个性能:0.90192。我们还测试了几何平均数、调和平均数、对数平均数或幂平均数是否能优于普通平均数:
geometric = proba.prod(axis=1)**(1/3)
ras = roc_auc_score(y_true=y_test, y_score=geometric)
print(f"Geometric averaging ROC-AUC is: {ras:0.5f}")
harmonic = 1 / np.mean(1\. / (proba + 0.00001), axis=1)
ras = roc_auc_score(y_true=y_test, y_score=harmonic)
print(f"Geometric averaging ROC-AUC is: {ras:0.5f}")
n = 3
mean_of_powers = np.mean(proba**n, axis=1)**(1/n)
ras = roc_auc_score(y_true=y_test, y_score=mean_of_powers)
print(f"Mean of powers averaging ROC-AUC is: {ras:0.5f}")
logarithmic = np.expm1(np.mean(np.log1p(proba), axis=1))
ras = roc_auc_score(y_true=y_test, y_score=logarithmic)
print(f"Logarithmic averaging ROC-AUC is: {ras:0.5f}")
运行代码将告诉我们,它们都不行。在这种情况下,算术平均数是集成时的最佳选择。实际上,在几乎所有情况下,比简单平均数更有效的是将一些先验知识融入到组合数字的方式中。这发生在你在平均计算中对模型进行加权时。
加权平均
当对模型进行加权时,你需要找到一种经验方法来确定正确的权重。一种常见的方法是,尽管非常容易导致自适应过拟合,但可以通过在公共排行榜上测试不同的组合,直到找到得分最高的组合。当然,这并不能保证你在私人排行榜上得分相同。在这里,原则是加权效果更好的部分。然而,正如我们详细讨论过的,由于与私人测试数据的重要差异,公共排行榜的反馈往往不可信。然而,你可以使用你的交叉验证分数或出卷分数(后者将在稍后的堆叠部分中讨论)。事实上,另一种可行的策略是使用与模型交叉验证性能成比例的权重。
虽然这有点反直觉,但另一种非常有效的方法是将提交内容与它们的协方差成反比进行加权。实际上,因为我们正通过平均来努力消除误差,所以基于每个提交的独特方差进行平均,使我们能够更重地加权那些相关性较低且多样性较高的预测,从而更有效地减少估计的方差。
在下一个示例中,我们首先将我们的预测概率创建为一个相关矩阵,然后我们继续进行以下操作:
-
移除对角线上的一个值并用零替换
-
通过行平均相关矩阵以获得一个向量
-
取每行总和的倒数
-
将它们的总和归一化到 1.0
-
使用得到的加权向量进行预测概率的矩阵乘法
下面是这个代码的示例:
cormat = np.corrcoef(proba.T)
np.fill_diagonal(cormat, 0.0)
W = 1 / np.mean(cormat, axis=1)
W = W / sum(W) # normalizing to sum==1.0
weighted = proba.dot(W)
ras = roc_auc_score(y_true=y_test, y_score=weighted)
print(f"Weighted averaging ROC-AUC is: {ras:0.5f}")
得到的 ROC-AUC 值为0.90206,略好于简单的平均。给予更多不相关预测更高的重视是一种经常成功的集成策略。即使它只提供了轻微的改进,这也可能足以将比赛转为你的优势。
在你的交叉验证策略中进行平均
正如我们所讨论的,平均不需要你构建任何特殊的复杂管道,只需要一定数量的典型数据管道来创建你将要平均的模型,无论是使用所有预测相同的权重,还是使用一些经验发现的权重。唯一测试它的方法是在公共排行榜上运行提交,从而冒着自适应拟合的风险,因为你的平均评估将仅基于 Kaggle 的响应。
在直接在排行榜上测试之前,你也可以在训练时间通过在验证折(你未用于训练模型的折)上运行平均操作来测试。这将为你提供比排行榜上更少的偏见反馈。在下面的代码中,你可以找到一个交叉验证预测是如何安排的示例:
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True, random_state=0)
scores = list()
for k, (train_index, test_index) in enumerate(kf.split(X_train)):
model_1.fit(X_train[train_index, :], y_train[train_index])
model_2.fit(X_train[train_index, :], y_train[train_index])
model_3.fit(X_train[train_index, :], y_train[train_index])
proba = np.stack(
[model_1.predict_proba(X_train[test_index, :])[:, 1],
model_2.predict_proba(X_train[test_index, :])[:, 1],
model_3.predict_proba(X_train[test_index, :])[:, 1]]).T
arithmetic = proba.mean(axis=1)
ras = roc_auc_score(y_true=y_train[test_index],
y_score=arithmetic)
scores.append(ras)
print(f"FOLD {k} Mean averaging ROC-AUC is: {ras:0.5f}")
print(f"CV Mean averaging ROC-AUC is: {np.mean(scores):0.5f}")
依赖于上述代码中的交叉验证结果可以帮助你评估哪种平均策略更有前途,而无需直接在公共排行榜上进行测试。
对 ROC-AUC 评估进行平均校正
如果你的任务将根据 ROC-AUC 分数进行评估,简单地平均你的结果可能不够。这是因为不同的模型可能采用了不同的优化策略,它们的输出可能差异很大。一种解决方案是对模型进行校准,这是我们之前在第五章中讨论的一种后处理类型,但显然这需要更多的时间和计算努力。
在这些情况下,直接的解决方案是将输出概率转换为排名,然后简单地平均这些排名(或者对它们进行加权平均)。使用 min-max 缩放器方法,你只需将每个模型的估计值转换为 0-1 的范围,然后继续进行预测的平均。这将有效地将你的模型的概率输出转换为可以比较的排名:
from sklearn.preprocessing import MinMaxScaler
proba = np.stack(
[model_1.predict_proba(X_train)[:, 1],
model_2.predict_proba(X_train)[:, 1],
model_3.predict_proba(X_train)[:, 1]]).T
arithmetic = MinMaxScaler().fit_transform(proba).mean(axis=1)
ras = roc_auc_score(y_true=y_test, y_score=arithmetic)
print(f"Mean averaging ROC-AUC is: {ras:0.5f}")
当你直接处理测试预测时,这种方法工作得非常完美。如果你在交叉验证期间尝试平均结果,你可能会遇到问题,因为你的训练数据的预测范围可能与你的测试预测范围不同。在这种情况下,你可以通过训练一个校准模型来解决该问题(参见 Scikit-learn 上的概率校准scikit-learn.org/stable/modules/calibration.html和第五章),将预测转换为每个模型的真正、可比较的概率。
使用元模型进行混合模型
我们在第一章中详细讨论的 Netflix 竞赛不仅证明了平均对于数据科学竞赛中的难题是有利的;它还提出了你可以使用一个模型更有效地平均模型结果的想法。获胜团队 BigChaos 在其论文(Töscher, A., Jahrer, M., 和 Bell, R.M. The BigChaos Solution to the Netflix Grand Prize. Netflix prize documentation – 2009)中多次提到了混合,并提供了关于其有效性和工作方式的许多提示。
简而言之,混合是一种加权平均过程,其中用于组合预测的权重是通过保留集和在此之上训练的元模型来估计的。元模型简单来说就是从其他机器学习模型输出中学习的机器学习算法。通常,元学习器是一个线性模型(但有时也可以是非线性的;关于这一点,我们将在下一节中详细讨论),但实际上你可以使用任何你想要的,但会有一些风险,我们将在后面讨论。
获得混合的方法非常直接:
-
在开始构建所有模型之前,你应从训练数据中随机提取一个保留样本(在团队中,你们所有人都应使用相同的保留样本)。通常,保留数据大约是可用数据的 10%;然而,根据情况(例如,训练数据中的示例数量,分层),它也可能更少或更多。像往常一样,在采样时,你可以强制分层以确保采样具有代表性,并且你可以使用对抗性验证来测试样本是否真的与训练集其余部分的分布相匹配。
-
在剩余的训练数据上训练所有模型。
-
在保留数据和测试数据上进行预测。
-
将保留预测作为元学习器的训练数据,并重新使用元学习器模型,使用你的模型的测试预测来计算最终的测试预测。或者,你可以使用元学习器来确定在加权平均中应使用的预测因子及其权重。
这种方法有很多优点和缺点。让我们先从优点开始。首先,它很容易实现;你只需要弄清楚保留样本是什么。此外,使用元学习算法可以确保你将找到最佳权重,而无需在公共排行榜上进行测试。
在弱点方面,有时,根据样本大小和所使用的模型类型,减少训练示例的数量可能会增加你的估计器的预测方差。此外,即使你在采样保留数据时非常小心,你仍然可能陷入自适应过拟合,即找到适合保留数据的权重,但这些权重不具有可推广性,特别是如果你使用了一个过于复杂的元学习器。最后,使用保留数据作为测试目的具有与我们在模型验证章节中讨论的训练和测试分割相同的限制:如果保留样本的样本量太小,或者由于某种原因,你的采样不具有代表性,那么你将无法获得可靠的估计。
混合的最佳实践
在混合中,你使用的元学习器的类型可以产生很大的差异。最常见的选择是使用线性模型或非线性模型。在线性模型中,线性或逻辑回归是首选。使用正则化模型也有助于排除无用的模型(L1 正则化)或减少不那么有用的模型的影响(L2 正则化)。使用这类元学习器的一个限制是,它们可能会给某些模型分配负贡献,正如你将从模型系数的值中看到的那样。当你遇到这种情况时,模型通常过拟合,因为所有模型都应该对集成(或最坏的情况是,完全不贡献)的建设做出积极贡献。Scikit-learn 的最新版本允许你只强制执行正权重,并移除截距。这些约束作为正则化器,防止过拟合。
作为元学习器的非线性模型较为少见,因为它们在回归和二分类问题中容易过拟合,但在多分类和多标签分类问题中它们通常表现出色,因为它们可以模拟现有类别之间的复杂关系。此外,如果除了模型的预测之外,你还向它们提供原始特征,它们通常表现更好,因为它们可以发现任何有助于它们正确选择更值得信赖的模型的交互作用。
在我们的下一个例子中,我们首先尝试使用线性模型(逻辑回归)进行混合,然后使用非线性方法(随机森林)。我们首先将训练集分为用于混合元素的训练部分和用于元学习器的保留样本。之后,我们在可训练的部分上拟合模型,并在保留样本上进行预测。
from sklearn.preprocessing import StandardScaler
X_blend, X_holdout, y_blend, y_holdout = train_test_split(X_train, y_train, test_size=0.25, random_state=0)
model_1.fit(X_blend, y_blend)
model_2.fit(X_blend, y_blend)
model_3.fit(X_blend, y_blend)
proba = np.stack([model_1.predict_proba(X_holdout)[:, 1],
model_2.predict_proba(X_holdout)[:, 1],
model_3.predict_proba(X_holdout)[:, 1]]).T
scaler = StandardScaler()
proba = scaler.fit_transform(proba)
现在我们可以使用保留样本上的预测概率来训练我们的线性元学习器:
from sklearn.linear_model import LogisticRegression
blender = LogisticRegression(solver='liblinear')
blender.fit(proba, y_holdout)
print(blender.coef_)
得到的系数如下:
[[0.78911314 0.47202077 0.75115854]]
通过观察系数,我们可以判断哪个模型对元集成模型的贡献更大。然而,记住系数在未良好校准时也会重新调整概率,因此一个模型的系数较大并不一定意味着它是最重要的。如果你想要通过观察系数来了解每个模型在混合中的作用,你首先必须通过标准化(在我们的代码示例中,这是使用 Scikit-learn 的StandardScaler完成的)来重新调整它们。
我们的结果显示,SVC 和k最近邻模型在混合中的权重比随机森林模型更高;它们的系数几乎相同,并且都大于随机森林的系数。
一旦元模型训练完成,我们只需在测试数据上预测并检查其性能:
test_proba = np.stack([model_1.predict_proba(X_test)[:, 1],
model_2.predict_proba(X_test)[:, 1],
model_3.predict_proba(X_test)[:, 1]]).T
blending = blender.predict_proba(test_proba)[:, 1]
ras = roc_auc_score(y_true=y_test, y_score=blending)
print(f"ROC-AUC for linear blending {model} is: {ras:0.5f}")
我们可以使用非线性元学习器,例如随机森林,尝试相同的事情:
blender = RandomForestClassifier()
blender.fit(proba, y_holdout)
test_proba = np.stack([model_1.predict_proba(X_test)[:, 1],
model_2.predict_proba(X_test)[:, 1],
model_3.predict_proba(X_test)[:, 1]]).T
blending = blender.predict_proba(test_proba)[:, 1]
ras = roc_auc_score(y_true=y_test, y_score=blending)
print(f"ROC-AUC for non-linear blending {model} is: {ras:0.5f}")
集合选择技术由Caruana、Niculescu-Mizil、Crew和Ksikes正式化,为使用线性或非线性模型作为元学习器提供了一个替代方案。
如果你对更多细节感兴趣,请阅读他们著名的论文:Caruana, R.,Niculescu-Mizil, A.,Crew, G.,和 Ksikes, A. 从模型库中进行集合选择(第二十一届国际机器学习会议论文集,2004 年)。
集合选择实际上是一个加权平均,因此它可以简单地被认为是线性组合的类似物。然而,它是一个受约束的线性组合(因为它属于爬山优化的一部分),它还将选择模型并只对预测应用正权重。所有这些都有助于最小化过拟合的风险,并确保一个更紧凑的解决方案,因为解决方案将涉及模型选择。从这个角度来看,在所有过拟合风险较高的问题上(例如,因为训练案例数量很少或模型过于复杂)以及在现实世界应用中,由于它简单而有效的解决方案,推荐使用集合选择。
当使用元学习器时,你依赖于其自身成本函数的优化,这可能与竞赛采用的指标不同。集合选择的另一个巨大优势是它可以优化到任何评估函数,因此当竞赛的指标与机器学习模型通常优化的规范不同时,通常建议使用它。
实施集合选择需要以下步骤,如前述论文所述:
-
从你的训练模型和保留样本开始。
-
在保留样本上测试所有你的模型,并根据评估指标,保留最有效的模型进行选择(即集合选择)。
-
然后,继续测试其他可能添加到集合选择中的模型,以便所提出的选择的平均值优于之前的一个。你可以有放回或无放回地进行。无放回时,你只将一个模型放入选择集合一次;在这种情况下,程序就像在正向选择之后的一个简单平均。(在正向选择中,你迭代地向解决方案添加性能提升最大的模型,直到添加更多模型不再提升性能。)有放回时,你可以将一个模型放入选择多次,从而类似于加权平均。
-
当你无法获得任何进一步的改进时,停止并使用集合选择。
这里是一个集合选择的简单代码示例。我们首先从原始训练数据中推导出一个保留样本和一个训练选择。我们拟合模型并在我们的保留样本上获得预测,就像之前与元学习器混合时所见:
X_blend, X_holdout, y_blend, y_holdout = train_test_split
(X_train, y_train, test_size=0.5, random_state=0)
model_1.fit(X_blend, y_blend)
model_2.fit(X_blend, y_blend)
model_3.fit(X_blend, y_blend)
proba = np.stack([model_1.predict_proba(X_holdout)[:, 1],
model_2.predict_proba(X_holdout)[:, 1],
model_3.predict_proba(X_holdout)[:, 1]]).T
在下一个代码片段中,通过一系列迭代创建集成。在每个迭代中,我们尝试依次将所有模型添加到当前的集成中,并检查它们是否提高了模型。如果这些添加中的任何一个在保留样本上优于之前的集成,则集成将被更新,并且性能水平将提高到当前水平。
如果没有额外的改进可以提高集成,循环就会停止,并报告集成的组成:
iterations = 100
proba = np.stack([model_1.predict_proba(X_holdout)[:, 1],
model_2.predict_proba(X_holdout)[:, 1],
model_3.predict_proba(X_holdout)[:, 1]]).T
baseline = 0.5
print(f"starting baseline is {baseline:0.5f}")
models = []
for i in range(iterations):
challengers = list()
for j in range(proba.shape[1]):
new_proba = np.stack(proba[:, models + [j]])
score = roc_auc_score(y_true=y_holdout,
y_score=np.mean(new_proba, axis=1))
challengers.append([score, j])
challengers = sorted(challengers, key=lambda x: x[0],
reverse=True)
best_score, best_model = challengers[0]
if best_score > baseline:
print(f"Adding model_{best_model+1} to the ensemble",
end=': ')
print(f"ROC-AUC increases score to {best_score:0.5f}")
models.append(best_model)
baseline = best_score
else:
print("Cannot improve further - Stopping")
最后,我们计算每个模型被插入平均值的次数,并计算我们在测试集上的平均权重:
from collections import Counter
freqs = Counter(models)
weights = {key: freq/len(models) for key, freq in freqs.items()}
print(weights)
你可以通过各种方式使该过程更加复杂。由于这种方法可能会在初始阶段过度拟合,你可以从一个随机初始化的集成集开始,或者,正如作者所建议的,你可能已经从集合并行中开始使用n个表现最好的模型(你决定n的值,作为一个超参数)。另一种变化涉及在每个迭代中对可以进入选择的模型集应用采样;换句话说,你随机排除一些模型不被选中。这不仅会将随机性注入到过程中,而且还会防止特定模型在选择中占主导地位。
组合堆叠模型
堆叠首次在David Wolpert的论文(Wolpert, D. H. Stacked generalization. Neural networks 5.2 – 1992)中提到,但这个想法在多年后才被广泛接受和普及(例如,只有到 2019 年 12 月发布的 0.22 版本,Scikit-learn 才实现了堆叠包装器)。这主要是因为 Netflix 竞赛,其次是 Kaggle 竞赛。
在堆叠中,你始终有一个元学习器。然而,这次它不是在保留集上训练,而是在整个训练集上训练,这得益于折叠外(OOF)预测策略。我们已经在第六章,设计良好的验证中讨论了这种策略。在 OOF 预测中,你从一个可复制的k-折交叉验证分割开始。可复制的意味着,通过记录每一轮每个训练和测试集中的案例,或者通过随机种子保证的可重复性,你可以为需要成为堆叠集成一部分的每个模型复制相同的验证方案。
在 Netflix 竞赛中,堆叠和混合经常被互换使用,尽管 Wolpert 最初设计的方法实际上意味着利用基于k-折交叉验证的方案,而不是保留集。事实上,堆叠的核心思想不是像平均那样减少方差;它主要是为了减少偏差,因为预计每个参与堆叠的模型都将掌握数据中存在的一部分信息,以便在最终的元学习器中重新组合。
让我们回顾一下在训练数据上进行的 OOF 预测是如何工作的。在测试模型时,在每次验证中,你都在训练数据的一部分上训练一个模型,并在从训练中保留的另一部分上进行验证。
通过记录验证预测并重新排序以重建原始训练案例的顺序,你将获得对你所使用的训练集的模型预测。然而,由于你使用了多个模型,并且每个模型都预测了它没有用于训练的案例,因此你的训练集预测不应该有任何过拟合效应。
获得所有模型的 OOF 预测后,你可以继续构建一个元学习器,该学习器根据 OOF 预测(第一级预测)预测你的目标,或者你可以在之前的 OOF 预测之上继续产生进一步的 OOF 预测(第二级或更高级预测),从而创建多个堆叠层。这与 Wolpert 本人提出的一个想法相兼容:通过使用多个元学习器,你实际上是在模仿一个没有反向传播的完全连接的前馈神经网络的结构,其中权重被优化计算,以单独在每个层级别最大化预测性能。从实际的角度来看,堆叠多层已被证明非常有效,对于单算法无法获得最佳结果的复杂问题,它工作得非常好。
此外,堆叠的一个有趣方面是,你不需要具有可比预测能力的模型,就像在平均和通常在混合中那样。事实上,甚至表现更差的模型也可能是堆叠集成的一部分。一个k最近邻模型可能无法与梯度提升解决方案相提并论,但当你使用其 OOF 预测进行堆叠时,它可能产生积极的影响,并提高集成的预测性能。
当你训练了所有堆叠层后,就到了预测的时候了。至于产生在各个堆叠阶段使用的预测,重要的是要注意你有两种方法来做这件事。Wolpert 的原始论文建议在所有训练数据上重新训练你的模型,然后使用这些重新训练的模型在测试集上进行预测。在实践中,许多 Kagglers 没有重新训练,而是直接使用为每个折叠创建的模型,并在测试集上进行多次预测,最后进行平均。
在我们的经验中,当使用少量k折时,堆叠通常在预测测试集之前在所有可用数据上完全重新训练时更有效。在这些情况下,样本一致性可能真的会在预测质量上产生差异,因为训练在较少数据上意味着估计的方差更大。正如我们在第六章中讨论的,在创建 OOF 预测时,始终最好使用高数量的折,在 10 到 20 之间。这限制了保留的示例数量,并且,在没有对所有数据进行重新训练的情况下,您可以直接使用从交叉验证训练模型获得的预测的平均值来获得您的测试集预测。
在我们的下一个例子中,为了说明目的,我们只有五个 CV 折,结果被堆叠了两次。在下图中,您可以跟踪数据和模型如何在堆叠过程的各个阶段之间移动:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_09_01.png
图 9.1:两层堆叠过程的示意图,最终对预测进行平均
注意:
-
训练数据被输入到堆叠的每一层(堆叠的第二层中的 OOF 预测与训练数据相结合)
-
在从 CV 循环中获得 OOF 预测后,模型在完整的训练数据集上重新训练:
-
最终预测是所有堆叠预测器获得的预测的简单平均值
现在我们来看看代码,了解这个图如何转换为 Python 命令,从第一层训练开始:
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True, random_state=0)
scores = list()
first_lvl_oof = np.zeros((len(X_train), 3))
fist_lvl_preds = np.zeros((len(X_test), 3))
for k, (train_index, val_index) in enumerate(kf.split(X_train)):
model_1.fit(X_train[train_index, :], y_train[train_index])
first_lvl_oof[val_index, 0] = model_1.predict_proba(
X_train[val_index, :])[:, 1]
model_2.fit(X_train[train_index, :], y_train[train_index])
first_lvl_oof[val_index, 1] = model_2.predict_proba(
X_train[val_index, :])[:, 1]
model_3.fit(X_train[train_index, :], y_train[train_index])
first_lvl_oof[val_index, 2] = model_3.predict_proba(
X_train[val_index, :])[:, 1]
在第一层之后,我们在完整的数据集上重新训练:
model_1.fit(X_train, y_train)
fist_lvl_preds[:, 0] = model_1.predict_proba(X_test)[:, 1]
model_2.fit(X_train, y_train)
fist_lvl_preds[:, 1] = model_2.predict_proba(X_test)[:, 1]
model_3.fit(X_train, y_train)
fist_lvl_preds[:, 2] = model_3.predict_proba(X_test)[:, 1]
在第二次堆叠中,我们将重用第一层中的相同模型,并将堆叠的 OOF 预测添加到现有变量中:
second_lvl_oof = np.zeros((len(X_train), 3))
second_lvl_preds = np.zeros((len(X_test), 3))
for k, (train_index, val_index) in enumerate(kf.split(X_train)):
skip_X_train = np.hstack([X_train, first_lvl_oof])
model_1.fit(skip_X_train[train_index, :],
y_train[train_index])
second_lvl_oof[val_index, 0] = model_1.predict_proba(
skip_X_train[val_index, :])[:, 1]
model_2.fit(skip_X_train[train_index, :],
y_train[train_index])
second_lvl_oof[val_index, 1] = model_2.predict_proba(
skip_X_train[val_index, :])[:, 1]
model_3.fit(skip_X_train[train_index, :],
y_train[train_index])
second_lvl_oof[val_index, 2] = model_3.predict_proba(
skip_X_train[val_index, :])[:, 1]
再次,我们在第二层对完整数据进行重新训练:
skip_X_test = np.hstack([X_test, fist_lvl_preds])
model_1.fit(skip_X_train, y_train)
second_lvl_preds[:, 0] = model_1.predict_proba(skip_X_test)[:, 1]
model_2.fit(skip_X_train, y_train)
second_lvl_preds[:, 1] = model_2.predict_proba(skip_X_test)[:, 1]
model_3.fit(skip_X_train, y_train)
second_lvl_preds[:, 2] = model_3.predict_proba(skip_X_test)[:, 1]
堆叠通过平均第二层中所有堆叠的 OOF 结果来完成:
arithmetic = second_lvl_preds.mean(axis=1)
ras = roc_auc_score(y_true=y_test, y_score=arithmetic)
scores.append(ras)
print(f"Stacking ROC-AUC is: {ras:0.5f}")
结果的 ROC-AUC 分数约为0.90424,这比在相同数据和模型上之前的混合和平均尝试要好。
堆叠变体
堆叠的主要变体涉及改变测试数据在层之间的处理方式,是否只使用堆叠的 OOF 预测,还是在所有堆叠层中也使用原始特征,以及使用什么模型作为最后一个模型,以及各种防止过拟合的技巧。
我们讨论了一些我们亲自实验过的最有效的方法:
-
优化可能使用也可能不使用。有些解决方案不太关心优化单个模型;有些只优化最后一层;有些则优化第一层。根据我们的经验,优化单个模型很重要,我们更喜欢尽早在我们的堆叠集成中完成它。
-
模型可以在不同的堆叠层中有所不同,或者相同的模型序列可以在每个堆叠层中重复。 这里我们没有一个普遍的规则,因为这真的取决于问题。更有效的模型类型可能因问题而异。作为一个一般建议,将梯度提升解决方案和神经网络结合起来从未让我们失望。
-
在堆叠过程的第一个层次,尽可能多地创建模型。 例如,如果你的问题是分类问题,可以尝试回归模型,反之亦然。你也可以使用具有不同超参数设置的模型,从而避免过度优化,因为堆叠会为你做出决定。如果你使用神经网络,只需改变随机初始化种子就足以创建一个多样化的模型集合。你也可以尝试使用不同的特征工程,甚至使用无监督学习(例如,Mike Kim 在使用 t-SNE 维度解决他的问题时所做的那样:
www.kaggle.com/c/otto-group-product-classification-challenge/discussion/14295)。这种想法是,所有这些贡献的选择都是在堆叠的第二层完成的。这意味着在那个点上,你不需要进一步实验,只需专注于一组表现更好的模型。通过应用堆叠,你可以重用所有实验,并让堆叠为你决定在建模过程中使用到什么程度。 -
一些堆叠实现会采用所有功能或其中一部分功能进入后续阶段,这让人联想到神经网络中的跳层。我们注意到,在堆叠的后期引入特征可以提高你的结果,但请注意:这也引入了更多的噪声和过拟合的风险。
-
理想情况下,你的 OOF 预测应该来自具有高折叠数的交叉验证方案,换句话说,在 10 到 20 之间,但我们也看到过使用较低折叠数(如 5 折)的解决方案。
-
对于每个折叠,对同一模型进行多次数据袋装(重复抽样的重采样)然后平均所有模型的结果(OOF 预测和测试预测)有助于避免过拟合,并最终产生更好的结果。
-
注意堆叠中的早期停止。 直接在验证折上使用它可能会导致一定程度的过拟合,这最终可能或可能不会通过堆叠过程得到缓解。我们建议你谨慎行事,并始终基于训练折的验证样本应用早期停止,而不是验证折本身。
可能性是无限的。一旦你掌握了这种集成技术的基本概念,你所需要做的就是将你的创造力应用于手头的问题。我们将在本章的最后部分讨论这个关键概念,我们将研究一个 Kaggle 比赛的堆叠解决方案。
创建复杂的堆叠和混合解决方案
在本章的这一部分,你可能想知道应该将我们讨论过的技术应用到什么程度。从理论上讲,你可以在 Kaggle 上的任何比赛中使用我们提出的所有集成技术,而不仅仅是表格比赛,但你必须考虑一些限制因素:
-
有时,数据集很大,训练单个模型需要很长时间。
-
在图像识别比赛中,你只能使用深度学习方法。
-
即使你能在深度学习比赛中堆叠模型,可供堆叠的不同模型的选择也很有限。由于你被限制在深度学习解决方案中,你只能改变网络的小设计方面和一些超参数(有时只是初始化种子),而不会降低性能。最终,鉴于相同的模型类型和架构中相似性多于差异,预测将过于相似,相关性过高,从而限制了集成技术的有效性。
在这些条件下,复杂的堆叠制度通常不可行。相比之下,当您拥有大量数据集时,平均和混合通常是可能的。
在早期的比赛中,以及所有最近的表格比赛中,复杂的堆叠和混合解决方案主导了比赛。为了给您一个关于在比赛中堆叠所需复杂性和创造性的概念,在本节的最后,我们将讨论由 Gilberto Titericz (www.kaggle.com/titericz) 和 Stanislav Semenov (www.kaggle.com/stasg7) 为 Otto Group 产品分类挑战赛 (www.kaggle.com/c/otto-group-product-classification-challenge) 提供的解决方案。该比赛于 2015 年举行,其任务要求根据 93 个特征将超过 200,000 个产品分类到 9 个不同的类别。
Gilberto 和 Stanislav 提出的解决方案包含三个级别:
-
在第一级,有 33 个模型。除了一个 k 近邻簇,其中只有 k 参数不同之外,所有模型都使用了相当不同的算法。他们还使用了无监督的 t-SNE。此外,他们基于维度操作(在最近邻和簇的距离上执行的计算)和行统计(每行中非零元素的数量)构建了八个特征。所有 OOF 预测和特征都传递到了第二级。
-
在第二层,他们开始优化超参数,进行模型选择和袋装(通过重采样创建了多个相同模型的版本,并对每个模型的结果进行了平均)。最终,他们只对三种模型在所有数据上进行了重新训练:XGBoost、AdaBoost 和神经网络。
-
在第三层,他们首先对 XGBoost 和神经网络进行几何平均,然后将其与 AdaBoost 的结果平均,从而准备了一个加权平均的结果。
我们可以从这个解决方案中学到很多,而不仅仅局限于这个比赛。除了复杂性(在第二层,每个模型重采样的次数在数百次左右)之外,值得注意的是,关于本章中讨论的方案存在多种变体。创造性和试错显然主导了解决方案。这在许多 Kaggle 比赛中相当典型,因为问题很少从一场比赛到另一场比赛相同,每个解决方案都是独特的且难以重复。
许多 AutoML 引擎,例如AutoGluon,或多或少地试图从这些程序中汲取灵感,以提供一系列预定义的自动化步骤,通过堆叠和混合确保您获得最佳结果。
查看arxiv.org/abs/2003.06505以获取 AutoGluon 构建其堆叠模型所使用的算法列表。列表相当长,您将找到许多为自己的堆叠解决方案提供灵感的想法。
然而,尽管他们实施了围绕最佳实践的一些最佳做法,但与一支优秀的 Kagglers 团队所能实现的结果相比,他们的结果总是略逊一筹,因为你在实验和组合集成的方式中的创造力是成功的关键。这一点也适用于我们本章的内容。我们向您展示了集成最佳实践;将它们作为起点,通过混合想法并根据您在 Kaggle 比赛或您正在处理的现实世界问题中进行创新来创建自己的解决方案。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/Xavier_Conort.png
Xavier Conort
为了总结本章,我们采访了 Xavier Conort,他是 2012-2013 年的比赛大师,排名第一。他是 Kaggle 历史初期许多 Kagglers 的灵感来源,现在是他自己公司的创始人兼首席执行官,Data Mapping and Engineering。他向我们讲述了他在 Kaggle 的经历、他的职业生涯以及更多内容。
你最喜欢的比赛类型是什么?为什么?在技术和解决方法方面,你在 Kaggle 上的专长是什么?
我真的很喜欢那些需要从多个表格中进行特征工程才能获得好成绩的比赛。我喜欢挖掘好的特征,尤其是对于我来说全新的商业问题。这让我对自己的能力解决新问题充满了信心。除了好的特征工程,堆叠也帮助我获得了好成绩。我使用它来混合多个模型或转换文本或高分类变量为数值特征。我最喜欢的算法是 GBM,但我测试了许多其他算法来增加我的混合多样性。
您是如何处理 Kaggle 比赛的?这种方法和您日常工作的方法有何不同?
我的主要目标是尽可能从每次比赛中学习到知识。在参加比赛之前,我试图评估我将发展哪些技能。我不害怕超越我的舒适区。多亏了排行榜的反馈,我知道我可以快速从我的错误中学习。日常工作中很少有这样的机会。很难评估我们正在努力解决的问题的实际质量。因此,我们只是保持谨慎,倾向于重复过去的食谱。我认为没有 Kaggle,我无法学到这么多。
请告诉我们您参加的一个特别具有挑战性的比赛,以及您使用了哪些见解来应对这项任务。
我最喜欢的比赛是 GE Flight Quest*,这是由 GE 组织的一项比赛,参赛者需要预测美国国内航班的到达时间。我特别喜欢比赛私人排行榜的设计方式。它通过对我们预测在比赛截止日期之后发生的航班的准确性进行评分,来测试我们预测未来事件的能力。*
因为我们只有几个月的历史(如果我的记忆正确,是 3 或 4 个月),我知道有很强的过拟合风险。为了减轻这种风险,我决定只构建与航班延误有明显的因果关系的特征,例如测量天气条件和交通状况的特征。我非常小心地排除了机场名称从我主要特征列表中。事实上,在短短几个月的历史中,一些机场没有经历过恶劣的天气条件。因此,我非常担心我最喜欢的机器学习算法 GBM 会使用机场名称作为良好天气的代理,然后无法很好地预测那些在私人排行榜上的机场。为了捕捉一些机场比其他机场管理得更好的事实,并略微提高我的排行榜分数,我最终确实使用了机场名称,但仅作为残余效应。这是我的第二层模型的一个特征,它使用第一层模型的预测作为偏移量。这种方法可以被认为是一种两步提升,其中你在第一步中抑制了一些信息。我从应用此方法以捕捉地理空间残余效应的精算师那里学到了这一点。
Kaggle 是否帮助了您的职业生涯?如果是的话,是如何帮助的?
这无疑帮助了我作为数据科学家的职业生涯。在转向数据科学之前,我是一名保险行业的精算师,对机器学习一无所知,也不认识任何数据科学家。多亏了 Kaggle 竞赛的多样性,我加速了我的学习曲线。多亏了我的好成绩,我能够展示我的成绩记录,并说服雇主一个 39 岁的精算师可以独立成功开发新技能。而且多亏了 Kaggle 的社区,我与世界各地的许多充满激情的数据科学家建立了联系。我最初与他们竞争或对抗时非常开心。最后,我有机会与他们中的一些人一起工作。Jeremy Achin 和 Tom De Godoy,DataRobot 的创始人,在我被邀请加入 DataRobot 之前是我的竞争对手。如果没有 Kaggle 的帮助,我认为我可能还在保险行业作为精算师工作。
你是否曾经使用过你在 Kaggle 比赛中做过的某些事情来构建你的投资组合,以展示给潜在雇主?
我必须承认,我曾参加过一些比赛,目的是为了给我的雇主或潜在客户留下深刻印象。这很有效,但乐趣更少,压力更大。
在你的经验中,没有经验的新手 Kagglers 经常忽略什么?你现在知道什么,而你在最初开始时希望知道的呢?
我建议没有经验的 Kagglers 不要在比赛期间查看发布的解决方案,而应尝试自己找到好的解决方案。我很高兴在 Kaggle 早期,竞争者没有分享代码。这迫使我通过艰难的方式学习。
你在过去比赛中犯过哪些错误?
一个错误是继续参加设计不良且存在泄露的比赛。这纯粹是浪费时间。你从那些比赛中学不到很多东西。
有没有你推荐的特定工具或库用于数据分析或机器学习?
梯度提升机是我最喜欢的算法。我最初使用了 R 的 gbm,然后是 Scikit-learn GBM,然后是 XGBoost,最后是 LightGBM。大多数时候,它一直是我的获胜解决方案的主要成分。为了了解 GBM 学习的内容,我推荐使用 SHAP 包。
当人们参加比赛时,他们应该记住或做些什么最重要的事情?
竞争是为了学习。竞争是为了与其他充满激情的数据科学家建立联系。不要只是为了赢得比赛而竞争。
摘要
在本章中,我们讨论了如何将多个解决方案进行集成,并提出了你可以用来开始构建自己解决方案的一些基本代码示例。我们从随机森林和梯度提升等模型集成背后的思想开始。然后,我们继续探讨不同的集成方法,从简单的测试提交平均到跨多层堆叠模型的元建模。
正如我们讨论的结尾,集成更多是基于一些共享的共同实践的艺术形式。当我们探索到一个成功的复杂堆叠机制,并在 Kaggle 竞赛中获胜时,我们对其如何针对数据和问题本身进行定制组合感到惊讶。你不能只是拿一个堆叠,复制到另一个问题上,并希望它是最佳解决方案。你只能遵循指南,通过大量的实验和计算工作,自己找到由平均/堆叠/混合的多种模型组成的最佳解决方案。
在下一章中,我们将开始深入研究深度学习竞赛,从计算机视觉竞赛开始,用于分类和分割任务。
加入我们书籍的 Discord 空间
加入书籍的 Discord 工作空间,每月与作者进行一次 问我任何问题 的活动:
第十章:计算机视觉建模
计算机视觉任务是机器学习实际应用中最受欢迎的问题之一;它们是许多 Kagglers(包括我本人,即 Konrad)进入深度学习的门户。在过去的几年里,该领域取得了巨大的进步,新的 SOTA 库仍在不断发布。在本章中,我们将为您概述计算机视觉中最受欢迎的竞赛类型:
-
图像分类
-
目标检测
-
图像分割
我们将从图像增强的简短部分开始,这是一组任务无关的技术,可以应用于不同的问题以提高我们模型的一般化能力。
增强策略
虽然深度学习技术在图像识别、分割或目标检测等计算机视觉任务中取得了极大的成功,但底层算法通常非常数据密集:它们需要大量数据以避免过拟合。然而,并非所有感兴趣的领域都满足这一要求,这就是数据增强发挥作用的地方。这是指一组图像处理技术,它们创建图像的修改版本,从而增强训练数据集的大小和质量,导致深度学习模型性能的改善。增强数据通常代表一组更全面的可能数据点,从而最小化训练集和验证集之间的距离,以及任何未来的测试集。
在本节中,我们将回顾一些更常见的增强技术,以及它们软件实现的选项。最常用的变换包括:
-
翻转:沿水平或垂直轴翻转图像
-
旋转:按给定角度(顺时针或逆时针)旋转图像
-
裁剪:随机选择图像的一个子区域
-
亮度:修改图像的亮度
-
缩放:将图像增加到更大的(向外)或更小的(向内)尺寸
下面,我们将通过使用美国演员和喜剧传奇人物贝蒂·怀特的图像来展示这些变换在实际中的应用:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_01.png
图 10.1:贝蒂·怀特图像
我们可以沿垂直或水平轴翻转图像:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_02.png
图 10.2:贝蒂·怀特图像 – 垂直翻转(左侧)和水平翻转(右侧)
旋转相当直观;注意背景中图像的自动填充:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_03.png
图 10.3:贝蒂·怀特图像 – 顺时针旋转
我们还可以将图像裁剪到感兴趣的区域:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_04.png
图 10.4:Betty White 图像 – 裁剪
在高层次上,我们可以这样说,增强可以以两种方式之一应用:
-
离线:这些通常用于较小的数据集(较少的图像或较小的尺寸,尽管“小”的定义取决于可用的硬件)。想法是在数据集预处理步骤中生成原始图像的修改版本,然后与“原始”图像一起使用。
-
在线:这些用于较大的数据集。增强后的图像不会保存到磁盘上;增强操作是在小批量中应用,并馈送到模型中。
在接下来的几节中,我们将为您概述两种最常用的图像数据集增强方法:内置的 Keras 功能和albumentations包。还有其他一些选项可供选择(skimage、OpenCV、imgaug、Augmentor、SOLT),但我们将重点关注最受欢迎的几种。
本章讨论的方法侧重于由 GPU 驱动的图像分析。张量处理单元(TPUs)的使用是一个新兴但仍然相对小众的应用。对图像增强与 TPU 驱动分析感兴趣的读者被鼓励查看 Chris Deotte(@cdeotte)的出色工作:
www.kaggle.com/cdeotte/triple-stratified-kfold-with-tfrecords
Chris 是一位四重 Kaggle 大师,也是一位出色的教育者,通过他创建的笔记本和参与的讨论;总的来说,任何 Kaggler 都值得关注的一个人,无论你的经验水平如何。
我们将使用来自甘薯叶病分类竞赛(www.kaggle.com/c/cassava-leaf-disease-classification)的数据。像往常一样,我们首先进行基础工作:首先,加载必要的包:
import os
import glob
import numpy as np
import scipy as sp
import pandas as pd
import cv2
from skimage.io import imshow, imread, imsave
# imgaug
import imageio
import imgaug as ia
import imgaug.augmenters as iaa
# Albumentations
import albumentations as A
# Keras
# from keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img
# Visualization
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
%matplotlib inline
import seaborn as sns
from IPython.display import HTML, Image
# Warnings
import warnings
warnings.filterwarnings("ignore")
接下来,我们定义一些辅助函数,以便稍后简化演示。我们需要一种将图像加载到数组中的方法:
def load_image(image_id):
file_path = image_id
image = imread(Image_Data_Path + file_path)
return image
我们希望以画廊风格显示多张图像,因此我们创建了一个函数,该函数接受一个包含图像以及所需列数的数组作为输入,并将该数组重塑为具有给定列数的网格:
def gallery(array, ncols=3):
nindex, height, width, intensity = array.shape
nrows = nindex//ncols
assert nindex == nrows*ncols
result = (array.reshape(nrows, ncols, height, width, intensity)
.swapaxes(1,2)
.reshape(height*nrows, width*ncols, intensity))
return result
在处理完模板后,我们可以加载用于增强的图像:
data_dir = '../input/cassava-leaf-disease-classification/'
Image_Data_Path = data_dir + '/train_images/'
train_data = pd.read_csv(data_dir + '/train.csv')
# We load and store the first 10 images in memory for faster access
train_images = train_data["image_id"][:10].apply(load_image)
让我们加载一张单独的图像,以便我们知道我们的参考是什么:
curr_img = train_images[7]
plt.figure(figsize = (15,15))
plt.imshow(curr_img)
plt.axis('off')
下面就是:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_05.png
图 10.5:参考图像
在接下来的几节中,我们将演示如何使用内置的 Keras 功能和albumentations库从参考图像生成增强图像。
Keras 内置增强
Keras 库具有内置的增强功能。虽然不如专用包那么全面,但它具有易于与代码集成的优势。我们不需要单独的代码块来定义增强变换,而可以将它们包含在 ImageDataGenerator 中,这是我们可能无论如何都会使用的一个功能。
我们首先检查的 Keras 方法是基于 ImageDataGenerator 类。正如其名所示,它可以用来生成带有实时数据增强的图像数据批次。
ImageDataGenerator 方法
我们首先以以下方式实例化 ImageDataGenerator 类的对象:
import tensorflow as tf
from tensorflow.keras.preprocessing.image import ImageDataGenerator,
array_to_img, img_to_array, load_img
datagen = ImageDataGenerator(
rotation_range = 40,
shear_range = 0.2,
zoom_range = 0.2,
horizontal_flip = True,
brightness_range = (0.5, 1.5))
curr_img_array = img_to_array(curr_img)
curr_img_array = curr_img_array.reshape((1,) + curr_img_array.shape)
我们将所需的增强作为 ImageDataGenerator 的参数。官方文档似乎没有涉及这个话题,但实际结果表明,增强是按照它们作为参数定义的顺序应用的。
在上面的例子中,我们只使用了可能选项的一个有限子集;对于完整的列表,建议读者查阅官方文档:keras.io/api/preprocessing/image/.
接下来,我们使用 ImageDataGenerator 对象的 .flow 方法遍历图像。该类提供了三种不同的函数来将图像数据集加载到内存中并生成增强数据批次:
-
flow -
flow_from_directory -
flow_from_dataframe
它们都达到相同的目标,但在指定文件位置的方式上有所不同。在我们的例子中,图像已经存储在内存中,因此我们可以使用最简单的方法进行迭代:
i = 0
for batch in datagen.flow(
curr_img_array,
batch_size=1,
save_to_dir='.',
save_prefix='Augmented_image',
save_format='jpeg'):
i += 1
# Hard-coded stop - without it, the generator enters an infinite loop
if i > 9:
break
我们可以使用之前定义的辅助函数来检查增强图像:
aug_images = []
for img_path in glob.glob("*.jpeg"):
aug_images.append(mpimg.imread(img_path))
plt.figure(figsize=(20,20))
plt.axis('off')
plt.imshow(gallery(np.array(aug_images[0:9]), ncols = 3))
plt.title('Augmentation examples')
这里是结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_06.png
图 10.6:增强图像的集合
增强是一个非常有用的工具,但有效地使用它们需要做出判断。首先,显然是一个好主意可视化它们,以了解对数据的影响。一方面,我们希望引入一些数据变化以增加我们模型的一般化;另一方面,如果我们过于激进地改变图像,输入数据将变得不那么有信息量,模型的性能可能会受到影响。此外,选择使用哪些增强也可能具有问题特异性,正如我们通过比较不同的竞赛所看到的那样。
如果您查看上面的 图 10.6(来自 Cassava Leaf Disease Classification 竞赛的参考图像),我们应识别疾病的叶子可能具有不同的尺寸,指向不同的角度等,这既是因为植物的形状,也是因为图像拍摄方式的不同。这意味着垂直或水平翻转、裁剪和旋转等变换在这个上下文中都是有意义的。
相比之下,我们可以查看来自Severstal:钢铁缺陷检测竞赛的样本图像(www.kaggle.com/c/severstal-steel-defect-detection)。在这个竞赛中,参与者必须在钢板上定位和分类缺陷。所有图像都具有相同的大小和方向,这意味着旋转或裁剪会产生不真实的图像,增加了噪声,并对算法的泛化能力产生不利影响。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_07.png
图 10.7:Severstal 竞赛的样本图像
预处理层
作为原生 Keras 中的预处理步骤的数据增强的另一种方法是使用preprocessing层 API。该功能非常灵活:这些管道可以与 Keras 模型结合使用或独立使用,类似于ImageDataGenerator。
下面我们简要说明如何设置预处理层。首先,导入:
from tensorflow.keras.layers.experimental import preprocessing
from tensorflow.keras import layers
我们以标准的 Keras 方式加载预训练模型:
pretrained_base = tf.keras.models.load_model(
'../input/cv-course-models/cv-course-models/vgg16-pretrained-base',
)
pretrained_base.trainable = False
预处理层可以使用与其他层在Sequential构造函数内部使用相同的方式;唯一的要求是它们需要在我们的模型定义的开始处指定,在所有其他层之前:
model = tf.keras.Sequential([
# Preprocessing layers
preprocessing.RandomFlip('horizontal'), # Flip left-to-right
preprocessing.RandomContrast(0.5), # Contrast change by up to 50%
# Base model
pretrained_base,
# model head definition
layers.Flatten(),
layers.Dense(6, activation='relu'),
layers.Dense(1, activation='sigmoid'),
])
albumentations
albumentations软件包是一个快速图像增强库,它作为其他库的某种包装构建。
该软件包是经过在多个 Kaggle 竞赛中密集编码的结果(参见medium.com/@iglovikov/the-birth-of-albumentations-fe38c1411cb3),其核心开发者和贡献者中包括一些知名的 Kagglers,包括尤金·赫维琴亚 (www.kaggle.com/bloodaxe)、弗拉基米尔·伊格洛维科夫 (www.kaggle.com/iglovikov)、亚历克斯·帕里诺夫 (www.kaggle.com/creafz)和ZFTurbo (www.kaggle.com/zfturbo)。
完整的文档可以在albumentations.readthedocs.io/en/latest/找到。
下面我们列出重要特性:
-
针对不同数据类型的统一 API
-
支持所有常见的计算机视觉任务
-
与 TensorFlow 和 PyTorch 的集成
使用albumentations功能转换图像非常简单。我们首先初始化所需的转换:
import albumentations as A
horizontal_flip = A.HorizontalFlip(p=1)
rotate = A.ShiftScaleRotate(p=1)
gaus_noise = A.GaussNoise()
bright_contrast = A.RandomBrightnessContrast(p=1)
gamma = A.RandomGamma(p=1)
blur = A.Blur()
接下来,我们将转换应用于我们的参考图像:
img_flip = horizontal_flip(image = curr_img)
img_gaus = gaus_noise(image = curr_img)
img_rotate = rotate(image = curr_img)
img_bc = bright_contrast(image = curr_img)
img_gamma = gamma(image = curr_img)
img_blur = blur(image = curr_img)
我们可以通过'image'键访问增强图像并可视化结果:
img_list = [img_flip['image'],img_gaus['image'], img_rotate['image'],
img_bc['image'], img_gamma['image'], img_blur['image']]
plt.figure(figsize=(20,20))
plt.axis('off')
plt.imshow(gallery(np.array(img_list), ncols = 3))
plt.title('Augmentation examples')
这里是我们的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_08.png
图 10.8:使用 albumentations 库增强的图像
在讨论了增强作为处理计算机视觉问题的一个关键预处理步骤之后,我们现在可以应用这一知识到以下章节中,从一个非常常见的任务开始:图像分类。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/Chris_Deotte.png
Chris Deotte
在我们继续之前,让我们回顾一下我们与 Chris Deotte 的简短对话,我们在本书中多次提到他(包括在本章的早期),这有很好的理由。他是四届 Kaggle 大师级选手,也是 NVIDIA 的高级数据科学家和研究员,于 2019 年加入 Kaggle。
您最喜欢的竞赛类型是什么?为什么?在技术和解决方法方面,您在 Kaggle 上的专长是什么?
我喜欢与有趣数据相关的竞赛,以及需要构建创新新颖模型的竞赛。我的专长是分析训练好的模型,以确定它们的优点和缺点。之后,我喜欢改进模型和/或开发后处理来提升 CV LB。
您是如何处理 Kaggle 竞赛的?这种处理方式与您日常工作的处理方式有何不同?
我每次开始竞赛都会进行 EDA(探索性数据分析),创建本地验证,构建一些简单的模型,并将它们提交到 Kaggle 以获取排行榜分数。这有助于培养一种直觉,了解为了构建一个准确且具有竞争力的模型需要做什么。
请告诉我们您参加的一个特别具有挑战性的竞赛,以及您用来应对任务的见解。
Kaggle 的 Shopee – 价格匹配保证*是一个具有挑战性的竞赛,需要图像模型和自然语言模型。一个关键的见解是从两种模型中提取嵌入,然后确定如何结合使用图像和语言信息来找到产品匹配。
Kaggle 是否帮助了您的职业生涯?如果是的话,是如何帮助的?
是的。Kaggle 帮助我通过提高我的技能和增强我的简历的市场价值,成为 NVIDIA 的高级数据科学家。
许多雇主浏览 Kaggle 上的作品,以寻找具有特定技能的员工来帮助他们解决特定的项目。这样,我收到了许多工作机会的邀请。
在您的经验中,经验不足的 Kagglers 通常忽略了什么?您现在知道什么,而您希望在最初开始时就了解的呢?
在我看来,经验不足的 Kagglers 往往忽略了本地验证的重要性。看到自己的名字在排行榜上是很兴奋的。而且很容易专注于提高我们的排行榜分数,而不是交叉验证分数。
在过去的竞赛中,您犯过哪些错误?
很多时候,我犯了一个错误,就是过分相信我的排行榜分数,而不是交叉验证分数,从而选择了错误的最终提交。
对于数据分析或机器学习,您会推荐使用哪些特定的工具或库?
当然。在优化表格数据模型时,特征工程和快速实验非常重要。为了加速实验和验证的周期,使用 NVIDIA RAPIDS cuDF 和 cuML 在 GPU 上至关重要。
当人们参加比赛时,他们应该记住或做最重要的事情是什么?
最重要的是要享受乐趣并学习。不要担心你的最终排名。如果你专注于学习和享受乐趣,那么随着时间的推移,你的最终排名会越来越好。
您是否使用其他比赛平台?它们与 Kaggle 相比如何?
是的,我在 Kaggle 之外也参加过比赛。像 Booking.com 或 Twitter.com 这样的个别公司偶尔会举办比赛。这些比赛很有趣,涉及高质量的真实数据。
分类
在本节中,我们将演示一个端到端流程,该流程可以用作处理图像分类问题的模板。我们将逐步介绍必要的步骤,从数据准备到模型设置和估计,再到结果可视化。除了提供信息(并且很酷)之外,这一最后步骤如果需要深入检查代码以更好地理解性能,也可以非常有用。
我们将继续使用来自Cassava Leaf Disease Classification比赛的数据(www.kaggle.com/c/cassava-leaf-disease-classification)。
如往常一样,我们首先加载必要的库:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import datetime
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import tensorflow as tf
from tensorflow.keras import models, layers
from tensorflow.keras.preprocessing import image
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau
from tensorflow.keras.applications import EfficientNetB0
from tensorflow.keras.optimizers import Adam
import os, cv2, json
from PIL import Image
通常,定义几个辅助函数是个好主意;它使得代码更容易阅读和调试。如果您正在处理一个通用的图像分类问题,2019 年由谷歌研究大脑团队在论文中引入的EfficientNet系列模型可以作为一个良好的起点(arxiv.org/abs/1905.11946)。基本思想是平衡网络深度、宽度和分辨率,以实现所有维度的更有效扩展,从而获得更好的性能。对于我们的解决方案,我们将使用该系列中最简单的成员,EfficientNet B0,这是一个具有 1100 万个可训练参数的移动尺寸网络。
对于 EfficientNet 网络的详细解释,您可以从ai.googleblog.com/2019/05/efficientnet-improving-accuracy-and.html作为起点进行探索。
我们以 B0 为基础构建模型,随后是一个用于提高平移不变性的池化层和一个适合我们多类分类问题的激活函数的密集层:
class CFG:
# config
WORK_DIR = '../input/cassava-leaf-disease-classification'
BATCH_SIZE = 8
EPOCHS = 5
TARGET_SIZE = 512
def create_model():
conv_base = EfficientNetB0(include_top = False, weights = None,
input_shape = (CFG.TARGET_SIZE,
CFG.TARGET_SIZE, 3))
model = conv_base.output
model = layers.GlobalAveragePooling2D()(model)
model = layers.Dense(5, activation = "softmax")(model)
model = models.Model(conv_base.input, model)
model.compile(optimizer = Adam(lr = 0.001),
loss = "sparse_categorical_crossentropy",
metrics = ["acc"])
return model
关于我们传递给EfficientNetB0函数的参数的一些简要说明:
-
include_top参数允许你决定是否包含最终的密集层。由于我们想将预训练模型用作特征提取器,默认策略将是跳过它们,然后自己定义头部。 -
如果我们想从头开始训练模型,可以将
weights设置为None,或者如果我们要利用在大图像集合上预训练的权重,可以设置为'imagenet'或'noisy-student'。
下面的辅助函数允许我们可视化激活层,这样我们可以从视觉角度检查网络性能。这在开发一个在透明度方面臭名昭著的领域的直觉时非常有帮助:
def activation_layer_vis(img, activation_layer = 0, layers = 10):
layer_outputs = [layer.output for layer in model.layers[:layers]]
activation_model = models.Model(inputs = model.input,
outputs = layer_outputs)
activations = activation_model.predict(img)
rows = int(activations[activation_layer].shape[3] / 3)
cols = int(activations[activation_layer].shape[3] / rows)
fig, axes = plt.subplots(rows, cols, figsize = (15, 15 * cols))
axes = axes.flatten()
for i, ax in zip(range(activations[activation_layer].shape[3]), axes):
ax.matshow(activations[activation_layer][0, :, :, i],
cmap = 'viridis')
ax.axis('off')
plt.tight_layout()
plt.show()
我们通过为给定的模型创建基于“受限”模型的预测来生成激活,换句话说,使用整个架构直到倒数第二层;这是到 activations 变量的代码。函数的其余部分确保我们展示正确的激活布局,对应于适当卷积层中过滤器的形状。
接下来,我们处理标签并设置验证方案;数据中没有特殊结构(例如,时间维度或类别间的重叠),因此我们可以使用简单的随机分割:
train_labels = pd.read_csv(os.path.join(CFG.WORK_DIR, "train.csv"))
STEPS_PER_EPOCH = len(train_labels)*0.8 / CFG.BATCH_SIZE
VALIDATION_STEPS = len(train_labels)*0.2 / CFG.BATCH_SIZE
若想了解更多详细的验证方案,请参阅 第六章,设计良好的验证。
我们现在可以设置数据生成器,这对于我们的基于 TF 的算法循环图像数据是必要的。
首先,我们实例化两个 ImageDataGenerator 对象;这是当我们引入图像增强的时候。为了演示目的,我们将使用 Keras 内置的增强。之后,我们使用 flow_from_dataframe() 方法创建生成器,该方法用于生成具有实时数据增强的批处理张量图像数据:
train_labels.label = train_labels.label.astype('str')
train_datagen = ImageDataGenerator(
validation_split = 0.2, preprocessing_function = None,
rotation_range = 45, zoom_range = 0.2,
horizontal_flip = True, vertical_flip = True,
fill_mode = 'nearest', shear_range = 0.1,
height_shift_range = 0.1, width_shift_range = 0.1)
train_generator = train_datagen.flow_from_dataframe(
train_labels,
directory = os.path.join(CFG.WORK_DIR, "train_images"),
subset = "training",
x_col = "image_id",y_col = "label",
target_size = (CFG.TARGET_SIZE, CFG.TARGET_SIZE),
batch_size = CFG.BATCH_SIZE,
class_mode = "sparse")
validation_datagen = ImageDataGenerator(validation_split = 0.2)
validation_generator = validation_datagen.flow_from_dataframe(
train_labels,
directory = os.path.join(CFG.WORK_DIR, "train_images"),
subset = "validation",
x_col = "image_id",y_col = "label",
target_size = (CFG.TARGET_SIZE, CFG.TARGET_SIZE),
batch_size = CFG.BATCH_SIZE, class_mode = "sparse")
在指定了数据结构之后,我们可以创建模型:
model = create_model()
model.summary()
当我们的模型创建完成后,我们可以快速查看一个摘要。这主要用于检查,因为除非你有 photographic memory,否则你不太可能记住像 EffNetB0 这样复杂模型的层组成批次。在实践中,你可以使用摘要来检查输出过滤器的维度是否正确,或者参数计数(可训练的/不可训练的)是否符合预期。为了简洁起见,我们只展示了输出下面的前几行;检查 B0 的架构图将给你一个完整输出将有多长的概念。
Model: "functional_1"
__________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==========================================================================
input_1 (InputLayer) [(None, 512, 512, 3) 0
__________________________________________________________________________
rescaling (Rescaling) (None, 512, 512, 3) 0 input_1[0][0]
__________________________________________________________________________
normalization (Normalization) (None, 512, 512, 3) 7 rescaling[0][0]
___________________________________________________________________________
stem_conv_pad (ZeroPadding2D) (None, 513, 513, 3) 0 normalization[0][0]
___________________________________________________________________________
stem_conv (Conv2D) (None, 256, 256, 32) 864 stem_conv_pad[0][0]
___________________________________________________________________________
stem_bn (BatchNormalization) (None, 256, 256, 32) 128 stem_conv[0][0]
___________________________________________________________________________
stem_activation (Activation) (None, 256, 256, 32) 0 stem_bn[0][0]
___________________________________________________________________________
block1a_dwconv (DepthwiseConv2D (None, 256, 256, 32) 288 stem_activation[0][0]
___________________________________________________________________________
block1a_bn (BatchNormalization) (None, 256, 256, 32) 128 block1a_dwconv[0][0]
___________________________________________________________________________
在完成上述步骤后,我们可以继续拟合模型。在这一步中,我们还可以非常方便地定义回调。第一个是 ModelCheckpoint:
model_save = ModelCheckpoint('./EffNetB0_512_8_best_weights.h5',
save_best_only = True,
save_weights_only = True,
monitor = 'val_loss',
mode = 'min', verbose = 1)
检查点使用了一些值得详细说明的参数:
-
通过设置
save_best_only = True,我们可以保留最佳模型权重集。 -
我们通过只保留权重而不是完整的优化器状态来减小模型的大小。
-
我们通过找到验证损失的最低点来决定哪个模型是最优的。
接下来,我们使用防止过拟合的流行方法之一,早期停止。我们监控模型在保留集上的性能,如果给定数量的 epoch 内指标不再提升,则停止算法,在这个例子中是5:
early_stop = EarlyStopping(monitor = 'val_loss', min_delta = 0.001,
patience = 5, mode = 'min',
verbose = 1, restore_best_weights = True)
ReduceLROnPlateau回调监控保留集上的损失,如果在patience数量的 epoch 内没有看到改进,则降低学习率,在这个例子中是通过 0.3 的因子降低。虽然这不是万能的解决方案,但它可以经常帮助收敛:
reduce_lr = ReduceLROnPlateau(monitor = 'val_loss', factor = 0.3,
patience = 2, min_delta = 0.001,
mode = 'min', verbose = 1)
我们现在准备好拟合模型:
history = model.fit(
train_generator,
steps_per_epoch = STEPS_PER_EPOCH,
epochs = CFG.EPOCHS,
validation_data = validation_generator,
validation_steps = VALIDATION_STEPS,
callbacks = [model_save, early_stop, reduce_lr]
)
我们将简要解释我们之前没有遇到的两个参数:
-
训练生成器在每个训练 epoch 中产生
steps_per_epoch批次的批次。 -
当 epoch 结束时,验证生成器产生
validation_steps批次的批次。
在调用model.fit()之后的一个示例输出如下:
Epoch 00001: val_loss improved from inf to 0.57514, saving model to ./EffNetB0_512_8_best_weights.h5
模型拟合后,我们可以使用我们在开头编写的辅助函数检查样本图像上的激活。虽然这对于模型的成功执行不是必需的,但它可以帮助确定我们的模型在应用顶部的分类层之前提取了哪些类型的特征:
activation_layer_vis(img_tensor, 0)
这是我们可能会看到的情况:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_09.png
图 10.9:拟合模型的样本激活
我们可以使用model.predict()生成预测:
ss = pd.read_csv(os.path.join(CFG.WORK_DIR, "sample_submission.csv"))
preds = []
for image_id in ss.image_id:
image = Image.open(os.path.join(CFG.WORK_DIR, "test_images",
image_id))
image = image.resize((CFG.TARGET_SIZE, CFG.TARGET_SIZE))
image = np.expand_dims(image, axis = 0)
preds.append(np.argmax(model.predict(image)))
ss['label'] = preds
我们通过遍历图像列表来构建预测。对于每一张图像,我们将图像重塑到所需的维度,并选择具有最强信号的通道(模型预测类别概率,我们通过argmax选择最大的一个)。最终的预测是类别编号,与竞赛中使用的指标一致。
我们现在已经演示了一个用于图像分类的最小化端到端流程。当然,许多改进都是可能的——例如,更多的增强、更大的架构、回调定制——但基本模板应该为你提供一个良好的起点。
我们现在继续讨论计算机视觉中的第二个流行问题:目标检测。
目标检测
目标检测是计算机视觉/图像处理任务,我们需要在图像或视频中识别特定类别的语义对象实例。在前面章节讨论的分类问题中,我们只需要为每个图像分配一个类别,而在目标检测任务中,我们希望在感兴趣的对象周围绘制一个边界框来定位它。
在本节中,我们将使用来自 全球小麦检测 竞赛的数据(www.kaggle.com/c/global-wheat-detection)。在这个竞赛中,参与者必须检测小麦穗,即植物顶部含有谷物的穗状物。在植物图像中检测这些穗状物用于估计不同作物品种中小麦穗的大小和密度。我们将展示如何使用 Yolov5,一个在目标检测中建立已久的模型,并在 2021 年底之前是最先进的模型,直到它(基于初步结果)被 YoloX 架构超越,来训练一个解决此问题的模型。Yolov5 在竞赛中产生了极具竞争力的结果,尽管由于许可问题最终被组织者禁止使用,但它非常适合本演示的目的。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_10.png
图 10.10:检测到的小麦穗的样本图像可视化
在我们开始之前,有一个重要的问题需要提及,那就是边界框注释的不同格式;有不同(但数学上等价)的方式来描述矩形的坐标。
最常见的类型是 coco、voc-pascal 和 yolo。它们之间的区别可以从下面的图中清楚地看出:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_11.png
图 10.11:边界框的注释格式
我们还需要定义的一个部分是网格结构:Yolo 通过在图像上放置一个网格并检查是否有感兴趣的对象(在我们的例子中是小麦穗)存在于任何单元格中来检测对象。边界框被重新塑形以在图像的相关单元格内偏移,并且 (x, y, w, h) 参数被缩放到单位区间:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_12.png
图 10.12:Yolo 注释定位
我们首先加载训练数据的注释:
df = pd.read_csv('../input/global-wheat-detection/train.csv')
df.head(3)
让我们检查几个:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_13.png
图 10.13:带有注释的训练数据
我们从 bbox 列中提取边界框的实际坐标:
bboxs = np.stack(df['bbox'].apply(lambda x: np.fromstring(x[1:-1],
sep=',')))
bboxs
让我们看看这个数组:
array([[834., 222., 56., 36.],
[226., 548., 130., 58.],
[377., 504., 74., 160.],
...,
[134., 228., 141., 71.],
[430., 13., 184., 79.],
[875., 740., 94., 61.]])
下一步是从 Yolo 格式中提取坐标到单独的列:
for i, column in enumerate(['x', 'y', 'w', 'h']):
df[column] = bboxs[:,i]
df.drop(columns=['bbox'], inplace=True)
df['x_center'] = df['x'] + df['w']/2
df['y_center'] = df['y'] + df['h']/2
df['classes'] = 0
df = df[['image_id','x', 'y', 'w', 'h','x_center','y_center','classes']]
df.head(3)
Ultralytics 的实现对数据集的结构有一些要求,特别是注释存储的位置以及训练/验证数据的文件夹。
以下代码中文件夹的创建相当简单,但鼓励更好奇的读者查阅官方文档(github.com/ultralytics/yolov5/wiki/Train-Custom-Data):
# stratify on source
source = 'train'
# Pick a single fold for demonstration's sake
fold = 0
val_index = set(df[df['fold'] == fold]['image_id'])
# Loop through the bounding boxes per image
for name,mini in tqdm(df.groupby('image_id')):
# Where to save the files
if name in val_index:
path2save = 'valid/'
else:
path2save = 'train/'
# Storage path for labels
if not os.path.exists('convertor/fold{}/labels/'.
format(fold)+path2save):
os.makedirs('convertor/fold{}/labels/'.format(fold)+path2save)
with open('convertor/fold{}/labels/'.format(fold)+path2save+name+".
txt", 'w+') as f:
# Normalize the coordinates in accordance with the Yolo format requirements
row = mini[['classes','x_center','y_center','w','h']].
astype(float).values
row = row/1024
row = row.astype(str)
for j in range(len(row)):
text = ' '.join(row[j])
f.write(text)
f.write("\n")
if not os.path.exists('convertor/fold{}/images/{}'.
format(fold,path2save)):
os.makedirs('convertor/fold{}/images/{}'.format(fold,path2save))
# No preprocessing needed for images => copy them as a batch
sh.copy("../input/global-wheat-detection/{}/{}.jpg".
format(source,name),
'convertor/fold{}/images/{}/{}.jpg'.
format(fold,path2save,name))
我们接下来要做的是安装 Yolo 包本身。如果你在 Kaggle Notebook 或 Colab 中运行此代码,请确保已启用 GPU;实际上,即使没有 GPU,Yolo 的安装也可以工作,但你可能会因为 CPU 与 GPU 性能差异而遇到各种超时和内存问题。
!git clone https://github.com/ultralytics/yolov5 && cd yolov5 &&
pip install -r requirements.txt
我们省略了输出,因为它相当广泛。最后需要准备的是 YAML 配置文件,其中我们指定训练和验证数据的位置以及类别数。我们只对检测麦穗感兴趣,而不是区分不同类型,所以我们有一个类别(其名称仅用于符号一致性,在这种情况下可以是任意字符串):
yaml_text = """train: /kaggle/working/convertor/fold0/images/train/
val: /kaggle/working/convertor/fold0/images/valid/
nc: 1
names: ['wheat']"""
with open("wheat.yaml", 'w') as f:
f.write(yaml_text)
%cat wheat.yaml
有了这些,我们就可以开始训练我们的模型:
!python ./yolov5/train.py --img 512 --batch 2 --epochs 3 --workers 2 --data wheat.yaml --cfg "./yolov5/models/yolov5s.yaml" --name yolov5x_fold0 --cache
除非您习惯于从命令行启动事物,否则上述咒语肯定是晦涩难懂的,因此让我们详细讨论其组成:
-
train.py是用于从预训练权重开始训练 YoloV5 模型的主脚本。 -
--img 512表示我们希望原始图像(如您所见,我们没有以任何方式预处理)被缩放到 512x512。为了获得有竞争力的结果,您应该使用更高的分辨率,但此代码是在 Kaggle 笔记本中执行的,它对资源有一定的限制。 -
--batch指的是训练过程中的批量大小。 -
--epochs 3表示我们希望训练模型三个周期。 -
--workers 2指定数据加载器中的工作线程数。增加此数字可能会帮助性能,但在版本 6.0(截至本文写作时在 Kaggle Docker 图像中可用的最新版本)中存在已知错误,当工作线程数过高时,即使在有更多可用资源的机器上也是如此。 -
--data wheat.yaml是指向我们上面定义的数据规范 YAML 文件的文件。 -
--cfg "./yolov5/models/yolov5s.yaml"指定模型架构和用于初始化的相应权重集。您可以使用与安装提供的版本(请参阅官方文档以获取详细信息),或者您可以自定义自己的并保持它们以相同的.yaml格式。 -
--name指定结果模型要存储的位置。
我们将以下训练命令的输出分解。首先,基础工作:
Downloading the pretrained weights, setting up Weights&Biases https://wandb.ai/site integration, GitHub sanity check.
Downloading https://ultralytics.com/assets/Arial.ttf to /root/.config/Ultralytics/Arial.ttf...
wandb: (1) Create a W&B account
wandb: (2) Use an existing W&B account
wandb: (3) Don't visualize my results
wandb: Enter your choice: (30 second timeout)
wandb: W&B disabled due to login timeout.
train: weights=yolov5/yolov5s.pt, cfg=./yolov5/models/yolov5s.yaml, data=wheat.yaml, hyp=yolov5/data/hyps/hyp.scratch-low.yaml, epochs=3, batch_size=2, imgsz=512, rect=False, resume=False, nosave=False, noval=False, noautoanchor=False, evolve=None, bucket=, cache=ram, image_weights=False, device=, multi_scale=False, single_cls=False, optimizer=SGD, sync_bn=False, workers=2, project=yolov5/runs/train, name=yolov5x_fold0, exist_ok=False, quad=False, cos_lr=False, label_smoothing=0.0, patience=100, freeze=[0], save_period=-1, local_rank=-1, entity=None, upload_dataset=False, bbox_interval=-1, artifact_alias=latest
github: up to date with https://github.com/ultralytics/yolov5 <https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_002.png>
YOLOv5 <https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_003.png>.1-76-gc94736a torch 1.9.1 CUDA:0 (Tesla P100-PCIE-16GB, 16281MiB)
hyperparameters: lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3.0, warmup_momentum=0.8, warmup_bias_lr=0.1, box=0.05, cls=0.5, cls_pw=1.0, obj=1.0, obj_pw=1.0, iou_t=0.2, anchor_t=4.0, fl_gamma=0.0, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, degrees=0.0, translate=0.1, scale=0.5, shear=0.0, perspective=0.0, flipud=0.0, fliplr=0.5, mosaic=1.0, mixup=0.0, copy_paste=0.0
Weights & Biases: run 'pip install wandb' to automatically track and visualize YOLOv5 <https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_003.png>ns (RECOMMENDED)
TensorBoard: Start with 'tensorboard --logdir yolov5/runs/train', view at http://localhost:6006/
Downloading https://github.com/ultralytics/yolov5/releases/download/v6.1/yolov5s.pt to yolov5/yolov5s.pt...
100%|██████████████████████████████████████| 14.1M/14.1M [00:00<00:00, 40.7MB/s]
然后是模型。我们看到架构的摘要、优化器设置和使用的增强:
Overriding model.yaml nc=80 with nc=1
from n params module arguments
0 -1 1 3520 models.common.Conv [3, 32, 6, 2, 2]
1 -1 1 18560 models.common.Conv [32, 64, 3, 2]
2 -1 1 18816 models.common.C3 [64, 64, 1]
3 -1 1 73984 models.common.Conv [64, 128, 3, 2]
4 -1 2 115712 models.common.C3 [128, 128, 2]
5 -1 1 295424 models.common.Conv [128, 256, 3, 2]
6 -1 3 625152 models.common.C3 [256, 256, 3]
7 -1 1 1180672 models.common.Conv [256, 512, 3, 2]
8 -1 1 1182720 models.common.C3 [512, 512, 1]
9 -1 1 656896 models.common.SPPF [512, 512, 5]
10 -1 1 131584 models.common.Conv [512, 256, 1, 1]
11 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']
12 [-1, 6] 1 0 models.common.Concat [1]
13 -1 1 361984 models.common.C3 [512, 256, 1, False]
14 -1 1 33024 models.common.Conv [256, 128, 1, 1]
15 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']
16 [-1, 4] 1 0 models.common.Concat [1]
17 -1 1 90880 models.common.C3 [256, 128, 1, False]
18 -1 1 147712 models.common.Conv [128, 128, 3, 2]
19 [-1, 14] 1 0 models.common.Concat [1]
20 -1 1 296448 models.common.C3 [256, 256, 1, False]
21 -1 1 590336 models.common.Conv [256, 256, 3, 2]
22 [-1, 10] 1 0 models.common.Concat [1]
23 -1 1 1182720 models.common.C3 [512, 512, 1, False]
24 [17, 20, 23] 1 16182 models.yolo.Detect [1, [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]], [128, 256, 512]]
YOLOv5s summary: 270 layers, 7022326 parameters, 7022326 gradients, 15.8 GFLOPs
Transferred 342/349 items from yolov5/yolov5s.pt
Scaled weight_decay = 0.0005
optimizer: SGD with parameter groups 57 weight (no decay), 60 weight, 60 bias
albumentations: Blur(always_apply=False, p=0.01, blur_limit=(3, 7)), MedianBlur(always_apply=False, p=0.01, blur_limit=(3, 7)), ToGray(always_apply=False, p=0.01), CLAHE(always_apply=False, p=0.01, clip_limit=(1, 4.0), tile_grid_size=(8, 8))
train: Scanning '/kaggle/working/convertor/fold0/labels/train' images and labels
train: New cache created: /kaggle/working/convertor/fold0/labels/train.cache
train: Caching images (0.0GB ram): 100%|██████████| 51/51 00:00<00:00, 76.00it/
val: Scanning '/kaggle/working/convertor/fold0/labels/valid' images and labels..
val: New cache created: /kaggle/working/convertor/fold0/labels/valid.cache
val: Caching images (2.6GB ram): 100%|██████████| 3322/3322 [00:47<00:00, 70.51i
Plotting labels to yolov5/runs/train/yolov5x_fold0/labels.jpg...
AutoAnchor: 6.00 anchors/target, 0.997 Best Possible Recall (BPR). Current anchors are a good fit to dataset <https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_14.png>
图 10.14:带有注释的验证数据
一旦我们训练了模型,我们可以使用表现最佳模型的权重(Yolov5 有一个很酷的功能,可以自动保留最佳和最后一个周期的模型,将它们存储为 `best.pt` 和 `last.pt`)来生成测试数据上的预测:
```py
!python ./yolov5/detect.py --weights ./yolov5/runs/train/yolov5x_fold0/weights/best.pt --img 512 --conf 0.1 --source /kaggle/input/global-wheat-detection/test --save-txt --save-conf --exist-ok
我们将讨论特定于推理阶段的参数:
-
--weights指向我们上面训练的模型中最佳权重的位置。 -
--conf 0.1指定模型生成的候选边界框中哪些应该被保留。通常,这是一个在精确度和召回率之间的折衷(太低的阈值会导致大量误报,而将阈值调得太高则可能根本找不到任何麦穗头)。 -
--source是测试数据的位置。
为我们的测试图像创建的标签可以在本地进行检查:
!ls ./yolov5/runs/detect/exp/labels/
这是我们可能会看到的内容:
2fd875eaa.txt 53f253011.txt aac893a91.txt f5a1f0358.txt
348a992bb.txt 796707dd7.txt cc3532ff6.txt
让我们看看一个单独的预测:
!cat 2fd875eaa.txt
它具有以下格式:
0 0.527832 0.580566 0.202148 0.838867 0.101574
0 0.894531 0.587891 0.210938 0.316406 0.113519
这意味着在图像2fd875eaa中,我们的训练模型检测到了两个边界框(它们的坐标是行中的 2-5 项),并在行末给出了大于 0.1 的置信度分数。
我们如何将预测组合成所需格式的提交?我们首先定义一个辅助函数,帮助我们将坐标从 yolo 格式转换为 coco(如本竞赛所需):这是一个简单的顺序重排和通过乘以图像大小来归一化到原始值范围的问题:
def convert(s):
x = int(1024 * (s[1] - s[3]/2))
y = int(1024 * (s[2] - s[4]/2))
w = int(1024 * s[3])
h = int(1024 * s[4])
return(str(s[5]) + ' ' + str(x) + ' ' + str(y) + ' ' + str(w)
+ ' ' + str(h))
然后,我们继续生成一个提交文件:
-
我们遍历上述列出的文件。
-
对于每个文件,所有行都被转换为所需格式的字符串(一行代表一个检测到的边界框)。
-
这些行随后被连接成一个字符串,对应于这个文件。
代码如下:
with open('submission.csv', 'w') as myfile:
# Prepare submission
wfolder = './yolov5/runs/detect/exp/labels/'
for f in os.listdir(wfolder):
fname = wfolder + f
xdat = pd.read_csv(fname, sep = ' ', header = None)
outline = f[:-4] + ' ' + ' '.join(list(xdat.apply(lambda s:
convert(s), axis = 1)))
myfile.write(outline + '\n')
myfile.close()
让我们看看它的样子:
!cat submission.csv
53f253011 0.100472 61 669 961 57 0.106223 0 125 234 183 0.1082 96 696 928 126 0.108863 515 393 86 161 0.11459 31 0 167 209 0.120246 517 466 89 147
aac893a91 0.108037 376 435 325 188
796707dd7 0.235373 684 128 234 113
cc3532ff6 0.100443 406 752 144 108 0.102479 405 87 4 89 0.107173 576 537 138 94 0.113459 256 498 179 211 0.114847 836 618 186 65 0.121121 154 544 248 115 0.125105 40 567 483 199
2fd875eaa 0.101398 439 163 204 860 0.112546 807 440 216 323
348a992bb 0.100572 0 10 440 298 0.101236 344 445 401 211
f5a1f0358 0.102549 398 424 295 96
生成的submission.csv文件完成了我们的流程。
在本节中,我们展示了如何使用 YoloV5 解决目标检测问题:如何处理不同格式的注释,如何为特定任务定制模型,训练它,并评估结果。
基于这些知识,你应该能够开始处理目标检测问题。
现在我们转向计算机视觉任务中的第三大流行类别:语义分割。
语义分割
考虑到分割,最简单的方式是它将图像中的每个像素分类,将其分配给相应的类别;这些像素组合在一起形成感兴趣的区域,例如医学图像中器官上的病变区域。相比之下,目标检测(在上一节中讨论)将图像的片段分类到不同的对象类别,并在它们周围创建边界框。
我们将使用来自Sartorius – Cell Instance Segmentation竞赛(www.kaggle.com/c/sartorius-cell-instance-segmentation)的数据展示建模方法。在这个竞赛中,参与者被要求使用一组显微镜图像训练用于神经细胞实例分割的模型。
我们将围绕Detectron2构建解决方案,这是一个由 Facebook AI Research 创建的库,支持多种检测和分割算法。
Detectron2 是原始 Detectron 库 (github.com/facebookresearch/Detectron/) 和 Mask R-CNN 项目 (github.com/facebookresearch/maskrcnn-benchmark/) 的继任者。
我们首先安装额外的包:
!pip install pycocotools
!pip install 'git+https://github.com/facebookresearch/detectron2.git'
我们安装 pycocotools (github.com/cocodataset/cocoapi/tree/master/PythonAPI/pycocotools),这是我们格式化注释和 Detectron2(我们在这个任务中的工作马)所需的,以及 Detectron2 (github.com/facebookresearch/detectron2)。
在我们能够训练我们的模型之前,我们需要做一些准备工作:注释需要从组织者提供的 run-length encoding (RLE) 格式转换为 Detectron2 所需的 COCO 格式。RLE 的基本思想是节省空间:创建一个分割意味着以某种方式标记一组像素。由于图像可以被视为一个数组,这个区域可以用一系列直线(行或列方向)表示。
您可以通过列出索引或指定起始位置和后续连续块长度来编码每一行。下面给出了一个视觉示例:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_15.png
图 10.15:RLE 的视觉表示
微软的 Common Objects in Context (COCO) 格式是一种特定的 JSON 结构,它规定了图像数据集中标签和元数据的保存方式。下面,我们演示如何将 RLE 转换为 COCO 并与 k-fold 验证分割结合,以便为每个分割获得所需的训练/验证 JSON 文件对。
让我们从这里开始:
# from pycocotools.coco import COCO
import skimage.io as io
import matplotlib.pyplot as plt
from pathlib import Path
from PIL import Image
import pandas as pd
import numpy as np
from tqdm.notebook import tqdm
import json,itertools
from sklearn.model_selection import GroupKFold
# Config
class CFG:
data_path = '../input/sartorius-cell-instance-segmentation/'
nfolds = 5
我们需要三个函数将 RLE 转换为 COCO。首先,我们需要将 RLE 转换为二值掩码:
# From https://www.kaggle.com/stainsby/fast-tested-rle
def rle_decode(mask_rle, shape):
'''
mask_rle: run-length as string formatted (start length)
shape: (height,width) of array to return
Returns numpy array, 1 - mask, 0 - background
'''
s = mask_rle.split()
starts, lengths = [np.asarray(x, dtype=int)
for x in (s[0:][::2], s[1:][::2])]
starts -= 1
ends = starts + lengths
img = np.zeros(shape[0]*shape[1], dtype=np.uint8)
for lo, hi in zip(starts, ends):
img[lo:hi] = 1
return img.reshape(shape) # Needed to align to RLE direction
第二个将二值掩码转换为 RLE:
# From https://newbedev.com/encode-numpy-array-using-uncompressed-rle-for-
# coco-dataset
def binary_mask_to_rle(binary_mask):
rle = {'counts': [], 'size': list(binary_mask.shape)}
counts = rle.get('counts')
for i, (value, elements) in enumerate(
itertools.groupby(binary_mask.ravel(order='F'))):
if i == 0 and value == 1:
counts.append(0)
counts.append(len(list(elements)))
return rle
最后,我们将两者结合起来以生成 COCO 输出:
def coco_structure(train_df):
cat_ids = {name: id+1 for id, name in enumerate(
train_df.cell_type.unique())}
cats = [{'name': name, 'id': id} for name, id in cat_ids.items()]
images = [{'id': id, 'width': row.width, 'height': row.height,
'file_name':f'train/{id}.png'} for id,
row in train_df.groupby('id').agg('first').iterrows()]
annotations = []
for idx, row in tqdm(train_df.iterrows()):
mk = rle_decode(row.annotation, (row.height, row.width))
ys, xs = np.where(mk)
x1, x2 = min(xs), max(xs)
y1, y2 = min(ys), max(ys)
enc =binary_mask_to_rle(mk)
seg = {
'segmentation':enc,
'bbox': [int(x1), int(y1), int(x2-x1+1), int(y2-y1+1)],
'area': int(np.sum(mk)),
'image_id':row.id,
'category_id':cat_ids[row.cell_type],
'iscrowd':0,
'id':idx
}
annotations.append(seg)
return {'categories':cats, 'images':images,'annotations':annotations}
我们将我们的数据分割成非重叠的分割:
train_df = pd.read_csv(CFG.data_path + 'train.csv')
gkf = GroupKFold(n_splits = CFG.nfolds)
train_df["fold"] = -1
y = train_df.width.values
for f, (t_, v_) in enumerate(gkf.split(X=train_df, y=y,
groups=train_df.id.values)):
train_df.loc[v_, "fold"] = f
fold_id = train_df.fold.copy()
我们现在可以遍历分割:
all_ids = train_df.id.unique()
# For fold in range(CFG.nfolds):
for fold in range(4,5):
train_sample = train_df.loc[fold_id != fold]
root = coco_structure(train_sample)
with open('annotations_train_f' + str(fold) +
'.json', 'w', encoding='utf-8') as f:
json.dump(root, f, ensure_ascii=True, indent=4)
valid_sample = train_df.loc[fold_id == fold]
print('fold ' + str(fold) + ': produced')
for fold in range(4,5):
train_sample = train_df.loc[fold_id == fold]
root = coco_structure(train_sample)
with open('annotations_valid_f' + str(fold) +
'.json', 'w', encoding='utf-8') as f:
json.dump(root, f, ensure_ascii=True, indent=4)
valid_sample = train_df.loc[fold_id == fold]
print('fold ' + str(fold) + ': produced')
循环必须分块执行的原因是 Kaggle 环境的大小限制:笔记本输出的最大大小限制为 20 GB,5 个分割,每个分割有 2 个文件(训练/验证),总共 10 个 JSON 文件,超过了这个限制。
当在 Kaggle 笔记本中运行代码时,这些实际考虑因素值得记住,尽管对于这种“预备”工作,您当然可以在其他地方产生结果,并在之后将其作为 Kaggle 数据集上传。
在生成分割后,我们可以开始为我们的数据集训练 Detectron2 模型。像往常一样,我们首先加载必要的包:
from datetime import datetime
import os
import pandas as pd
import numpy as np
import pycocotools.mask as mask_util
import detectron2
from pathlib import Path
import random, cv2, os
import matplotlib.pyplot as plt
# Import some common detectron2 utilities
from detectron2 import model_zoo
from detectron2.engine import DefaultPredictor, DefaultTrainer
from detectron2.config import get_cfg
from detectron2.utils.visualizer import Visualizer, ColorMode
from detectron2.data import MetadataCatalog, DatasetCatalog
from detectron2.data.datasets import register_coco_instances
from detectron2.utils.logger import setup_logger
from detectron2.evaluation.evaluator import DatasetEvaluator
from detectron2.engine import BestCheckpointer
from detectron2.checkpoint import DetectionCheckpointer
setup_logger()
import torch
虽然一开始从 Detectron2 导入的数量可能看起来令人畏惧,但随着我们对任务定义的深入,它们的功能将变得清晰;我们首先指定输入数据文件夹、注释文件夹和定义我们首选模型架构的 YAML 文件路径:
class CFG:
wfold = 4
data_folder = '../input/sartorius-cell-instance-segmentation/'
anno_folder = '../input/sartoriusannotations/'
model_arch = 'mask_rcnn_R_50_FPN_3x.yaml'
nof_iters = 10000
seed = 45
这里值得提一下的是迭代参数(nof_iters 上方)。通常,模型训练是以完整遍历训练数据的次数(即周期数)来参数化的。Detectron2 的设计不同:一次迭代指的是一个 mini-batch,模型的不同部分使用不同的 mini-batch 大小。
为了确保结果可重复,我们固定了模型不同组件使用的随机种子:
def seed_everything(seed):
random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cudnn.deterministic = True
seed_everything(CFG.seed)
竞赛指标是不同交并比(IoU)阈值下的平均平均精度。作为对 第五章,竞赛任务和指标 的回顾,建议的一组对象像素与一组真实对象像素的 IoU 计算如下:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_001.png
该指标遍历一系列 IoU 阈值,在每个点上计算平均精度值。阈值值从 0.5 到 0.95,增量为 0.05。
在每个阈值值处,根据预测对象与所有地面真实对象比较后产生的真正例(TP)、假阴性(FN)和假阳性(FP)的数量计算一个精度值。最后,竞赛指标返回的分数是测试数据集中每个图像的个体平均精度的平均值。
下面,我们定义计算该指标所需的函数,并将其直接用于模型内部作为目标函数:
# Taken from https://www.kaggle.com/theoviel/competition-metric-map-iou
def precision_at(threshold, iou):
matches = iou > threshold
true_positives = np.sum(matches, axis=1) == 1 # Correct objects
false_positives = np.sum(matches, axis=0) == 0 # Missed objects
false_negatives = np.sum(matches, axis=1) == 0 # Extra objects
return np.sum(true_positives), np.sum(false_positives),
np.sum(false_negatives)
def score(pred, targ):
pred_masks = pred['instances'].pred_masks.cpu().numpy()
enc_preds = [mask_util.encode(np.asarray(p, order='F'))
for p in pred_masks]
enc_targs = list(map(lambda x:x['segmentation'], targ))
ious = mask_util.iou(enc_preds, enc_targs, [0]*len(enc_targs))
prec = []
for t in np.arange(0.5, 1.0, 0.05):
tp, fp, fn = precision_at(t, ious)
p = tp / (tp + fp + fn)
prec.append(p)
return np.mean(prec)
指标定义后,我们可以在模型中使用它:
class MAPIOUEvaluator(DatasetEvaluator):
def __init__(self, dataset_name):
dataset_dicts = DatasetCatalog.get(dataset_name)
self.annotations_cache = {item['image_id']:item['annotations']
for item in dataset_dicts}
def reset(self):
self.scores = []
def process(self, inputs, outputs):
for inp, out in zip(inputs, outputs):
if len(out['instances']) == 0:
self.scores.append(0)
else:
targ = self.annotations_cache[inp['image_id']]
self.scores.append(score(out, targ))
def evaluate(self):
return {"MaP IoU": np.mean(self.scores)}
这为我们创建 Trainer 对象提供了基础,它是围绕 Detectron2 构建的解决方案的核心:
class Trainer(DefaultTrainer):
@classmethod
def build_evaluator(cls, cfg, dataset_name, output_folder=None):
return MAPIOUEvaluator(dataset_name)
def build_hooks(self):
# copy of cfg
cfg = self.cfg.clone()
# build the original model hooks
hooks = super().build_hooks()
# add the best checkpointer hook
hooks.insert(-1, BestCheckpointer(cfg.TEST.EVAL_PERIOD,
DetectionCheckpointer(self.model,
cfg.OUTPUT_DIR),
"MaP IoU",
"max",
))
return hooks
我们现在继续以 Detectron2 风格加载数据集的训练/验证数据:
dataDir=Path(CFG.data_folder)
register_coco_instances('sartorius_train',{}, CFG.anno_folder +
'annotations_train_f' + str(CFG.wfold) +
'.json', dataDir)
register_coco_instances('sartorius_val',{}, CFG.anno_folder +
'annotations_valid_f' + str(CFG.wfold) +
'.json', dataDir)
metadata = MetadataCatalog.get('sartorius_train')
train_ds = DatasetCatalog.get('sartorius_train')
在我们实例化 Detectron2 模型之前,我们需要注意配置它。大多数值都可以保留为默认值(至少在第一次尝试时是这样);如果你决定进一步调整,从 BATCH_SIZE_PER_IMAGE(为了提高泛化性能)和 SCORE_THRESH_TEST(为了限制假阴性)开始:
cfg = get_cfg()
cfg.INPUT.MASK_FORMAT='bitmask'
cfg.merge_from_file(model_zoo.get_config_file('COCO-InstanceSegmentation/' +
CFG.model_arch))
cfg.DATASETS.TRAIN = ("sartorius_train",)
cfg.DATASETS.TEST = ("sartorius_val",)
cfg.DATALOADER.NUM_WORKERS = 2
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url('COCO-InstanceSegmentation/'
+ CFG.model_arch)
cfg.SOLVER.IMS_PER_BATCH = 2
cfg.SOLVER.BASE_LR = 0.001
cfg.SOLVER.MAX_ITER = CFG.nof_iters
cfg.SOLVER.STEPS = []
cfg.MODEL.ROI_HEADS.BATCH_SIZE_PER_IMAGE = 512
cfg.MODEL.ROI_HEADS.NUM_CLASSES = 3
cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = .4
cfg.TEST.EVAL_PERIOD = len(DatasetCatalog.get('sartorius_train'))
// cfg.SOLVER.IMS_PER_BATCH
训练模型很简单:
os.makedirs(cfg.OUTPUT_DIR, exist_ok=True)
trainer = Trainer(cfg)
trainer.resume_or_load(resume=False)
trainer.train()
你会注意到训练过程中的输出含有丰富的关于该过程进度的信息:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_16.png
图 10.16:Detectron2 的训练输出
模型训练完成后,我们可以保存权重并用于推理(可能在一个单独的笔记本中 – 参见本章前面的讨论)和提交准备。我们首先添加新的参数,允许我们正则化预测,设置置信度阈值和最小掩码大小:
THRESHOLDS = [.18, .35, .58]
MIN_PIXELS = [75, 150, 75]
我们需要一个辅助函数来将单个掩码编码成 RLE 格式:
def rle_encode(img):
'''
img: numpy array, 1 - mask, 0 - background
Returns run length as string formatted
'''
pixels = img.flatten()
pixels = np.concatenate([[0], pixels, [0]])
runs = np.where(pixels[1:] != pixels[:-1])[0] + 1
runs[1::2] -= runs[::2]
return ' '.join(str(x) for x in runs)
下面是生成每张图像所有掩码的主要函数,过滤掉可疑的(置信度分数低于THRESHOLDS)和面积小的(包含的像素少于MIN_PIXELS):
def get_masks(fn, predictor):
im = cv2.imread(str(fn))
pred = predictor(im)
pred_class = torch.mode(pred['instances'].pred_classes)[0]
take = pred['instances'].scores >= THRESHOLDS[pred_class]
pred_masks = pred['instances'].pred_masks[take]
pred_masks = pred_masks.cpu().numpy()
res = []
used = np.zeros(im.shape[:2], dtype=int)
for mask in pred_masks:
mask = mask * (1-used)
# Skip predictions with small area
if mask.sum() >= MIN_PIXELS[pred_class]:
used += mask
res.append(rle_encode(mask))
return res
我们然后准备存储图像 ID 和掩码的列表:
dataDir=Path(CFG.data_folder)
ids, masks=[],[]
test_names = (dataDir/'test').ls()
大型图像集的竞赛——如本节中讨论的——通常需要训练模型超过 9 小时,这是代码竞赛中规定的时限(见www.kaggle.com/docs/competitions)。这意味着在同一笔记本中训练模型和运行推理变得不可能。一个典型的解决方案是首先作为一个独立的笔记本在 Kaggle、Google Colab、GCP 或本地运行训练笔记本/脚本。第一个笔记本的输出(训练好的权重)被用作第二个笔记本的输入,换句话说,用于定义用于预测的模型。
我们通过加载我们训练好的模型的权重以这种方式继续进行:
cfg = get_cfg()
cfg.merge_from_file(model_zoo.get_config_file("COCO-InstanceSegmentation/"+
CFG.arch+".yaml"))
cfg.INPUT.MASK_FORMAT = 'bitmask'
cfg.MODEL.ROI_HEADS.NUM_CLASSES = 3
cfg.MODEL.WEIGHTS = CFG.model_folder + 'model_best_f' +
str(CFG.wfold)+'.pth'
cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.5
cfg.TEST.DETECTIONS_PER_IMAGE = 1000
predictor = DefaultPredictor(cfg)
我们可以通过加载我们训练好的模型的权重来可视化一些预测:
encoded_masks = get_masks(test_names[0], predictor)
_, axs = plt.subplots(1,2, figsize = (40, 15))
axs[1].imshow(cv2.imread(str(test_names[0])))
for enc in encoded_masks:
dec = rle_decode(enc)
axs[0].imshow(np.ma.masked_where(dec == 0, dec))
这里有一个例子:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_17.png
图 10.17:可视化 Detectron2 的样本预测与源图像
使用上面定义的辅助函数,以 RLE 格式生成提交的掩码非常简单:
for fn in test_names:
encoded_masks = get_masks(fn, predictor)
for enc in encoded_masks:
ids.append(fn.stem)
masks.append(enc)
pd.DataFrame({'id':ids, 'predicted':masks}).to_csv('submission.csv',
index=False)
pd.read_csv('submission.csv').head()
这里是最终提交的前几行:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_10_18.png
图 10.18:训练好的 Detectron2 模型的格式化提交
我们已经到达了本节的结尾。上面的流程演示了如何设置语义分割模型并进行训练。我们使用了少量迭代,但要达到有竞争力的结果,需要更长时间的训练。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/Laura_Fink.png
Laura Fink
为了总结本章,让我们看看 Kaggler Laura Fink 对她在平台上的时间的看法。她不仅是 Notebooks 大师,制作了许多精湛的 Notebooks,还是 MicroMata 的数据科学负责人。
你最喜欢的竞赛类型是什么?为什么?在技术和解决方法方面,你在 Kaggle 上的专长是什么?
我最喜欢的竞赛是那些能为人类带来好处的竞赛。我特别喜欢所有与健康相关的挑战。然而,对我来说,每个竞赛都像一场冒险,有自己的谜题需要解决。我真的很享受学习新技能和探索新的数据集或问题。因此,我并不专注于特定的技术,而是专注于学习新事物。我认为我因在探索性数据分析(EDA)方面的优势而闻名。
你是如何参加 Kaggle 竞赛的?这种方法和你在日常工作中所做的工作有什么不同?
当参加比赛时,我首先阅读问题陈述和数据描述。浏览论坛和公开的笔记本以收集想法后,我通常开始开发自己的解决方案。在初始阶段,我花了一些时间进行 EDA(探索性数据分析)以寻找隐藏的组并获取一些直觉。这有助于设置适当的验证策略,我认为这是所有后续步骤的基础。然后,我开始迭代机器学习管道的不同部分,如特征工程或预处理,改进模型架构,询问数据收集的问题,寻找泄漏,进行更多的 EDA,或构建集成。我试图以贪婪的方式改进我的解决方案。Kaggle 比赛非常动态,需要尝试不同的想法和不同的解决方案才能最终生存下来。
这肯定与我的日常工作不同,我的日常工作更侧重于从数据中获得见解,并找到简单但有效的解决方案来改进业务流程。在这里,任务通常比使用的模型更复杂。要解决的问题必须非常明确地定义,这意味着必须与不同背景的专家讨论应达到的目标,涉及哪些流程,以及数据需要如何收集或融合。与 Kaggle 比赛相比,我的日常工作需要更多的沟通而不是机器学习技能。
告诉我们您参加的一个特别具有挑战性的比赛,以及您使用了哪些见解来应对这项任务。
G2Net 引力波探测比赛是我最喜欢的之一。目标是检测隐藏在来自探测器组件和地球力量的噪声中的模拟引力波信号。在这场比赛中的一个重要见解是,你应该批判性地看待分析数据的标准方法,并尝试自己的想法。在我阅读的论文中,数据主要是通过使用傅里叶变换或常 Q 变换,在数据白化并应用带通滤波器后准备的。
很快就很明显,白化没有帮助,因为它使用了功率谱密度的样条插值,这本身就很嘈杂。将多项式拟合到噪声数据的小子集会增加另一个错误来源,因为过度拟合。
在去除白化之后,我尝试了不同超参数的 Constant-Q 变换,这长期以来一直是论坛和公共 Notebooks 中的领先方法。由于有两种引力波源可以覆盖不同的 Q 值范围,我尝试了一个在这些超参数上有所不同的模型集合。这证明在提高我的分数方面很有帮助,但后来我达到了一个极限。Constant-Q 变换对时间序列应用一系列滤波器,并将它们转换到频域。我开始问自己,是否有一种方法可以以更好、更灵活的方式执行这些滤波任务。就在这时,社区中提出了使用 1 维卷积神经网络的想法,我非常喜欢它。我们都知道,2 维卷积神经网络的滤波器能够根据图像数据检测边缘、线条和纹理。同样,可以使用“经典”滤波器,如拉普拉斯或索贝尔滤波器。因此,我自问:我们能否使用 1 维 CNN 来学习最重要的滤波器,而不是应用某种方式已经固定的变换?
我无法让我的 1 维卷积神经网络解决方案工作,但结果证明许多顶级团队都做得很好。G2Net 比赛是我最喜欢的之一,尽管我错过了赢得奖牌的目标。然而,我在过程中获得的知识和关于所谓标准方法的教训都是非常宝贵的。
Kaggle 是否帮助了你的职业生涯?如果是的话,是如何帮助的?
我在大学毕业后开始了我的第一份工作,成为一名 Java 软件开发者,尽管我在硕士论文期间已经接触过机器学习。我对进行更多数据分析很感兴趣,但在那时,几乎没有什么数据科学的工作,或者它们没有被这样命名。当我第一次听说 Kaggle 时,我立刻被它吸引。从那时起,我经常在晚上去 Kaggle 上玩。当时我并没有打算改变我的职位,但后来有一个研究项目需要机器学习技能。我能够通过在 Kaggle 上参与获得的知识证明我是这个项目的合适人选。这最终成为了我数据科学生涯的起点。
Kaggle 一直是我尝试新想法、学习新方法和工具、获取实践经验的好地方。通过这种方式获得的能力对我在工作中的数据科学项目非常有帮助。这就像是一次知识上的提升,因为 Kaggle 为你提供了一个沙盒,让你可以尝试不同的想法,发挥创造力而无需承担风险。在比赛中失败意味着至少有一个教训可以学习,但项目中的失败可能会对自己和其他人产生巨大的负面影响。
除了参加比赛外,另一种建立你个人作品集的绝佳方式是编写 Notebooks。这样做,你可以向世界展示你如何解决问题,以及如何传达洞察力和结论。当你必须与管理人员、客户和来自不同背景的专家合作时,后者非常重要。
在你的经验中,不经验的 Kagglers 通常忽略了什么?你现在知道什么,而当你最初开始时希望知道的呢?
我认为许多参加比赛的初学者被公开排行榜所吸引,没有良好的验证策略就构建模型。当他们在排行榜上衡量自己的成功时,他们很可能会对公开测试数据进行过度拟合。比赛结束后,他们的模型无法泛化到未见的私有测试数据,他们通常会下降数百名。我仍然记得在梅赛德斯-奔驰绿色制造比赛中,由于我无法爬升公开排行榜,我有多么沮丧。但当最终排名公布时,人们在上榜和下榜之间的波动让我感到惊讶。从那时起,我总是牢记,一个适当的验证方案对于管理欠拟合和过拟合的挑战非常重要。
你在比赛中犯过哪些错误?
我迄今为止犯的最大错误是在比赛开始时花费太多时间和精力在解决方案的细节上。实际上,在建立适当的验证策略之后,快速迭代不同的想法会更好。这样,找到改进的潜在方向更容易、更快,陷入某个地方的危险也小得多。
您会推荐使用哪些特定的工具或库来进行数据分析或机器学习?
在 Kaggle 社区活跃时,你可以学习和实践许多常见的工具和库,我唯一能做的就是推荐它们。保持灵活并了解它们的优缺点很重要。这样,你的解决方案不依赖于你的工具,而更多地依赖于你的想法和创造力。
当人们参加比赛时,他们应该记住或做哪件事是最重要的?
数据科学不是关于构建模型,而是关于理解数据和收集数据的方式。我迄今为止参加的许多比赛都显示了数据泄露或测试数据中隐藏的组,这些组可以通过探索性数据分析找到。
摘要
在本章中,我们从 Kaggle 比赛的角度为您概述了与计算机视觉相关的重要主题。我们介绍了增强,这是一种用于扩展算法泛化能力的技巧的重要类别,随后展示了三个最常见问题的端到端管道:图像分类、目标检测和语义分割。
在下一章中,我们将注意力转向自然语言处理,这是另一个极其广泛且受欢迎的问题类别。
加入我们书籍的 Discord 空间
加入书籍的 Discord 工作空间,每月与作者进行一次 问我任何问题 的活动:
第十一章:NLP 建模
自然语言处理(NLP)是一个在语言学、计算机科学和人工智能交叉领域的学科。其主要关注点是算法,用于处理和分析大量自然语言数据。在过去的几年里,它已经成为 Kaggle 竞赛中越来越受欢迎的话题。虽然该领域本身非常广泛,包括非常受欢迎的话题,如聊天机器人和机器翻译,但在本章中,我们将专注于 Kaggle 竞赛经常涉及的具体子集。
将情感分析视为一个简单的分类问题非常受欢迎,并且被广泛讨论,因此我们将从对问题的某种更有趣的变体开始:在推文中识别情感支持短语。我们将继续描述一个开放域问答问题的示例解决方案,并以一个关于 NLP 问题增强的章节结束,这是一个比其计算机视觉对应物受到更多关注的主题。
总结来说,我们将涵盖:
-
情感分析
-
开放域问答
-
文本增强策略
情感分析
Twitter 是最受欢迎的社会媒体平台之一,也是许多个人和公司的重要沟通工具。
在后一种情况下,在语言中捕捉情感尤为重要:一条积极的推文可以迅速传播,而一条特别消极的推文可能会造成伤害。由于人类语言很复杂,因此不仅需要决定情感,而且还需要能够调查“如何”:哪些单词实际上导致了情感描述?
我们将通过使用来自推文情感提取竞赛(www.kaggle.com/c/tweet-sentiment-extraction)的数据来展示解决这个问题的一种方法。为了简洁起见,我们省略了以下代码中的导入,但您可以在 GitHub 上本章相应笔记本中找到它们。
为了更好地了解这个问题,让我们先看看数据:
df = pd.read_csv('/kaggle/input/tweet-sentiment-extraction/train.csv')
df.head()
这里是前几行:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_11_01.png
图 11.1:训练数据中的样本行
实际的推文存储在text列中。每个推文都有一个相关的sentiment,以及存储在selected_text列中的支持短语(基于情感分配决策的推文部分)。
我们首先定义基本的清理函数。首先,我们想要去除网站 URL 和非字符,并将人们用来代替脏话的星号替换为单个标记,"swear"。我们使用一些正则表达式来帮助我们完成这项工作:
def basic_cleaning(text):
text=re.sub(r'https?://www\.\S+\.com','',text)
text=re.sub(r'[^A-Za-z|\s]','',text)
text=re.sub(r'\*+','swear',text) # Capture swear words that are **** out
return text
接下来,我们将从推文的正文中移除 HTML 以及表情符号:
def remove_html(text):
html=re.compile(r'<.*?>')
return html.sub(r'',text)
def remove_emoji(text):
emoji_pattern = re.compile("["
u"\U0001F600-\U0001F64F" #emoticons
u"\U0001F300-\U0001F5FF" #symbols & pictographs
u"\U0001F680-\U0001F6FF" #transport & map symbols
u"\U0001F1E0-\U0001F1FF" #flags (iOS)
u"\U00002702-\U000027B0"
u"\U000024C2-\U0001F251"
"]+", flags=re.UNICODE)
return emoji_pattern.sub(r'', text)
最后,我们希望能够移除重复的字符(例如,这样我们就有“way”而不是“waaaayyyyy”):
def remove_multiplechars(text):
text = re.sub(r'(.)\1{3,}',r'\1', text)
return text
为了方便,我们将四个函数组合成一个单一的清理函数:
def clean(df):
for col in ['text']:#,'selected_text']:
df[col]=df[col].astype(str).apply(lambda x:basic_cleaning(x))
df[col]=df[col].astype(str).apply(lambda x:remove_emoji(x))
df[col]=df[col].astype(str).apply(lambda x:remove_html(x))
df[col]=df[col].astype(str).apply(lambda x:remove_multiplechars(x))
return df
最后的准备涉及编写基于预训练模型(tokenizer参数)创建嵌入的函数:
def fast_encode(texts, tokenizer, chunk_size=256, maxlen=128):
tokenizer.enable_truncation(max_length=maxlen)
tokenizer.enable_padding(max_length=maxlen)
all_ids = []
for i in range(0, len(texts), chunk_size):
text_chunk = texts[i:i+chunk_size].tolist()
encs = tokenizer.encode_batch(text_chunk)
all_ids.extend([enc.ids for enc in encs])
return np.array(all_ids)
接下来,我们创建一个预处理函数,使我们能够处理整个语料库:
def preprocess_news(df,stop=stop,n=1,col='text'):
'''Function to preprocess and create corpus'''
new_corpus=[]
stem=PorterStemmer()
lem=WordNetLemmatizer()
for text in df[col]:
words=[w for w in word_tokenize(text) if (w not in stop)]
words=[lem.lemmatize(w) for w in words if(len(w)>n)]
new_corpus.append(words)
new_corpus=[word for l in new_corpus for word in l]
return new_corpus
使用我们之前准备好的函数,我们可以清理和准备训练数据。sentiment列是我们的目标,我们将其转换为虚拟变量(独热编码)以提高性能:
df.dropna(inplace=True)
df_clean = clean(df)
df_clean_selection = df_clean.sample(frac=1)
X = df_clean_selection.text.values
y = pd.get_dummies(df_clean_selection.sentiment)
下一个必要的步骤是对输入文本进行分词,以及将它们转换为序列(包括填充,以确保数据集跨数据集的长度相等):
tokenizer = text.Tokenizer(num_words=20000)
tokenizer.fit_on_texts(list(X))
list_tokenized_train = tokenizer.texts_to_sequences(X)
X_t = sequence.pad_sequences(list_tokenized_train, maxlen=128)
我们将使用DistilBERT为我们的模型创建嵌入,并直接使用它们。DistilBERT 是 BERT 的一个轻量级版本:权衡是参数减少 40%时的性能损失 3%。我们可以训练嵌入层并提高性能——但代价是大幅增加的训练时间。
tokenizer = transformers.AutoTokenizer.from_pretrained("distilbert-base-uncased")
# Save the loaded tokenizer locally
save_path = '/kaggle/working/distilbert_base_uncased/'
if not os.path.exists(save_path):
os.makedirs(save_path)
tokenizer.save_pretrained(save_path)
# Reload it with the huggingface tokenizers library
fast_tokenizer = BertWordPieceTokenizer(
'distilbert_base_uncased/vocab.txt', lowercase=True)
fast_tokenizer
我们可以使用之前定义的fast_encode函数,以及上面定义的fast_tokenizer,来编码推文:
X = fast_encode(df_clean_selection.text.astype(str),
fast_tokenizer,
maxlen=128)
数据准备就绪后,我们可以构建模型。为了这次演示,我们将采用这些应用中相当标准的架构:结合 LSTM 层、通过全局池化和 dropout 归一化,以及顶部的密集层。为了实现真正有竞争力的解决方案,需要对架构进行一些调整:一个“更重”的模型,更大的嵌入,LSTM 层中的更多单元,等等。
transformer_layer = transformers.TFDistilBertModel.from_pretrained('distilbert-base-uncased')
embedding_size = 128
input_ = Input(shape=(100,))
inp = Input(shape=(128, ))
embedding_matrix=transformer_layer.weights[0].numpy()
x = Embedding(embedding_matrix.shape[0],
embedding_matrix.shape[1],
embeddings_initializer=Constant(embedding_matrix),
trainable=False)(inp)
x = Bidirectional(LSTM(50, return_sequences=True))(x)
x = Bidirectional(LSTM(25, return_sequences=True))(x)
x = GlobalMaxPool1D()(x)
x = Dropout(0.5)(x)
x = Dense(50, activation='relu', kernel_regularizer='L1L2')(x)
x = Dropout(0.5)(x)
x = Dense(3, activation='softmax')(x)
model_DistilBert = Model(inputs=[inp], outputs=x)
model_DistilBert.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
对于数据的时序维度没有特殊需要关注,所以我们满足于将数据随机分为训练集和验证集,这可以在调用fit方法内部实现:
model_DistilBert.fit(X,y,batch_size=32,epochs=10,validation_split=0.1)
以下是一些示例输出:
Epoch 1/10
27480/27480 [==============================] - 480s 17ms/step - loss: 0.5100 - accuracy: 0.7994
Epoch 2/10
27480/27480 [==============================] - 479s 17ms/step - loss: 0.4956 - accuracy: 0.8100
Epoch 3/10
27480/27480 [==============================] - 475s 17ms/step - loss: 0.4740 - accuracy: 0.8158
Epoch 4/10
27480/27480 [==============================] - 475s 17ms/step - loss: 0.4528 - accuracy: 0.8275
Epoch 5/10
27480/27480 [==============================] - 475s 17ms/step - loss: 0.4318 - accuracy: 0.8364
Epoch 6/10
27480/27480 [==============================] - 475s 17ms/step - loss: 0.4069 - accuracy: 0.8441
Epoch 7/10
27480/27480 [==============================] - 477s 17ms/step - loss: 0.3839 - accuracy: 0.8572
从拟合的模型生成预测的过程是直接的。为了利用所有可用数据,我们首先在所有可用数据上重新训练我们的模型(因此没有验证):
df_clean_final = df_clean.sample(frac=1)
X_train = fast_encode(df_clean_selection.text.astype(str),
fast_tokenizer,
maxlen=128)
y_train = y
在生成预测之前,我们在整个数据集上重新拟合了模型:
Adam_name = adam(lr=0.001)
model_DistilBert.compile(loss='categorical_crossentropy',optimizer=Adam_name,metrics=['accuracy'])
history = model_DistilBert.fit(X_train,y_train,batch_size=32,epochs=10)
我们下一步是将测试数据处理成与用于模型训练数据相同的格式:
df_test = pd.read_csv('/kaggle/input/tweet-sentiment-extraction/test.csv')
df_test.dropna(inplace=True)
df_clean_test = clean(df_test)
X_test = fast_encode(df_clean_test.text.values.astype(str),
fast_tokenizer,
maxlen=128)
y_test = df_clean_test.sentiment
最后,我们生成预测:
y_preds = model_DistilBert.predict(X_test)
y_predictions = pd.DataFrame(y_preds,
columns=['negative','neutral','positive'])
y_predictions_final = y_predictions.idxmax(axis=1)
accuracy = accuracy_score(y_test,y_predictions_final)
print(f"The final model shows {accuracy:.2f} accuracy on the test set.")
最终模型在测试集上的准确率为0.74。以下我们展示了一些输出样例;正如您从这些几行中已经可以看到的,有些情况下情感对人类读者来说很明显,但模型未能捕捉到:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_11_02.png
图 11.2:预测结果中的示例行
我们现在已经演示了一个用于解决情感归属问题的样本管道(识别导致标注者在情感分类决策中做出决定的文本部分)。如果您想实现有竞争力的性能,以下是一些可以进行的改进,按可能的影响顺序排列:
-
更大的嵌入:这使我们能够在(处理过的)输入数据级别上捕获更多信息
-
更大的模型:LSTM 层中的单元更多
-
更长的训练:换句话说,更多的 epoch
虽然上述改进无疑会提高模型的性能,但我们管道的核心元素是可重用的:
-
数据清洗和预处理
-
创建文本嵌入
-
在目标模型架构中结合循环层和正则化
我们现在将讨论开放域问答,这是在 NLP 竞赛中经常遇到的问题。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/Abhishek_Thakur.png
Abhishek Thakur
我们采访了 Abhishek Thakur,他是世界上第一位四重 Kaggle 大师。他目前在 Hugging Face 工作,在那里他正在构建 AutoNLP;他还写了几乎唯一一本关于 Kaggle 的英文书籍(除了这本书之外!)接近(几乎)任何机器学习问题。
你在 Kaggle 上的专长是什么?
没有。每个竞赛都不同,从每个竞赛中都可以学到很多东西。如果我要有一个专长,我会在那个领域的所有竞赛中获胜。
你是如何处理 Kaggle 竞赛的?这种处理方式与你在日常工作中所做的是否不同?
我首先会查看数据,并试图理解它。如果我在竞赛中落后了,我会寻求公共 EDA 核的帮助。
当我接近 Kaggle(或非 Kaggle)上的问题时,我首先会建立一个基准。建立基准非常重要,因为它为你提供了一个可以比较未来模型的基准。如果我在建立基准方面落后了,我会尽量避免使用公共 Notebooks。如果我们那样做,我们只会朝一个方向思考。至少,我感觉是这样的。
当我完成一个基准测试后,我会尽量在不做任何复杂操作,比如堆叠或混合的情况下,尽可能多地提取信息。然后我会再次检查数据和模型,并尝试逐步改进基准。
日常工作中有时有很多相似之处。大多数时候都有一个基准,然后你必须提出技术、特征、模型来击败基准。
你参加的最有趣的竞赛是什么?你有什么特别的见解吗?
每个竞赛都很吸引人。
Kaggle 是否帮助你在职业生涯中取得进展?
当然,它有帮助。在过去的几年里,Kaggle 在招聘数据科学家和机器学习工程师方面赢得了非常好的声誉。Kaggle 排名和与许多数据集的经验无疑在行业中以某种方式有所帮助。你越熟悉处理不同类型的问题,你迭代的速度就越快。这在行业中是非常有用的。没有人愿意花几个月的时间做对业务没有任何价值的事情。
在您的经验中,不经验丰富的 Kagglers 通常忽略什么?您现在知道什么,而您希望自己在最初开始时就知道?
大多数初学者很容易放弃。加入 Kaggle 竞赛并受到顶尖选手的威胁是非常容易的。如果初学者想在 Kaggle 上成功,他们必须要有毅力。在我看来,毅力是关键。许多初学者也未能独立开始,而是一直坚持使用公共内核。这使得他们像公共内核的作者一样思考。我的建议是先从自己的竞赛开始,查看数据,构建特征,构建模型,然后深入内核和讨论,看看其他人可能有什么不同的做法。然后,将您所学到的知识融入到自己的解决方案中。
开放域问答
在本节中,我们将探讨Google QUEST Q&A Labeling竞赛(www.kaggle.com/c/google-quest-challenge/overview/description)。在这个竞赛中,问题-答案对由人类评分员根据一系列标准进行评估,例如“问题对话性”、“问题求证事实”或“答案有帮助”。任务是预测每个目标列(对应于标准)的数值;由于标签是跨多个评分员汇总的,因此目标列实际上是一个多元回归输出,目标列被归一化到单位范围。
在使用高级技术(如基于 transformer 的 NLP 模型)进行建模之前,通常使用更简单的方法建立一个基线是一个好主意。与上一节一样,为了简洁,我们将省略导入部分,但您可以在 GitHub 仓库中的笔记本中找到它们。
我们首先定义几个辅助函数,这些函数可以帮助我们提取文本的不同方面。首先,一个函数将输出给定字符串的词数:
def word_count(xstring):
return xstring.split().str.len()
竞赛中使用的指标是斯皮尔曼相关系数(在排名上计算的线性相关系数:en.wikipedia.org/wiki/Spearman%27s_rank_correlation_coefficient)。
由于我们打算构建一个 Scikit-learn 管道,定义一个指标作为评分器是有用的(make_scorer方法是一个 Scikit-learn 的包装器,它接受一个评分函数——如准确度或 MSE——并返回一个可调用的对象,该对象对估计器的输出进行评分):
def spearman_corr(y_true, y_pred):
if np.ndim(y_pred) == 2:
corr = np.mean([stats.spearmanr(y_true[:, i],
y_pred[:, i])[0]
for i in range(y_true.shape[1])])
else:
corr = stats.spearmanr(y_true, y_pred)[0]
return corr
custom_scorer = make_scorer(spearman_corr, greater_is_better=True)
接下来,一个小型的辅助函数,用于从l中提取大小为n的连续块。这将帮助我们稍后在不遇到内存问题时为我们的文本生成嵌入:
def chunks(l, n):
for i in range(0, len(l), n):
yield l[i:i + n]
我们将使用的特征集的一部分是来自预训练模型的嵌入。回想一下,本节的想法是构建一个不训练复杂模型的基线,但这并不妨碍我们使用现有的模型。
我们首先导入分词器和模型,然后以块的形式处理语料库,将每个问题/答案编码为固定大小的嵌入:
def fetch_vectors(string_list, batch_size=64):
# Inspired by https://jalammar.github.io/a-visual-guide-to-using-bert- for-the-first-time/
DEVICE = torch.device("cuda")
tokenizer = transformers.DistilBertTokenizer.from_pretrained
("../input/distilbertbaseuncased/")
model = transformers.DistilBertModel.from_pretrained
("../input/distilbertbaseuncased/")
model.to(DEVICE)
fin_features = []
for data in chunks(string_list, batch_size):
tokenized = []
for x in data:
x = " ".join(x.strip().split()[:300])
tok = tokenizer.encode(x, add_special_tokens=True)
tokenized.append(tok[:512])
max_len = 512
padded = np.array([i + [0] * (max_len - len(i)) for i in tokenized])
attention_mask = np.where(padded != 0, 1, 0)
input_ids = torch.tensor(padded).to(DEVICE)
attention_mask = torch.tensor(attention_mask).to(DEVICE)
with torch.no_grad():
last_hidden_states = model(input_ids,
attention_mask=attention_mask)
features = last_hidden_states[0][:, 0, :].cpu().numpy()
fin_features.append(features)
fin_features = np.vstack(fin_features)
return fin_features
我们现在可以继续加载数据:
xtrain = pd.read_csv(data_dir + 'train.csv')
xtest = pd.read_csv(data_dir + 'test.csv')
xtrain.head(4)
这里是前几行:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/B17574_11_03.png
图 11.3:训练数据中的样本行
我们指定了我们感兴趣的 30 个目标列:
target_cols = ['question_asker_intent_understanding',
'question_body_critical',
'question_conversational', 'question_expect_short_answer',
'question_fact_seeking',
'question_has_commonly_accepted_answer',
'question_interestingness_others',
'question_interestingness_self',
'question_multi_intent', 'question_not_really_a_question',
'question_opinion_seeking', 'question_type_choice',
'question_type_compare', 'question_type_consequence',
'question_type_definition', 'question_type_entity',
'question_type_instructions', 'question_type_procedure',
'question_type_reason_explanation',
'question_type_spelling',
'question_well_written', 'answer_helpful',
'answer_level_of_information', 'answer_plausible',
'answer_relevance', 'answer_satisfaction',
'answer_type_instructions', 'answer_type_procedure',
'answer_type_reason_explanation', 'answer_well_written']
关于它们的意义和解释的讨论,读者可以参考竞赛的数据页面,在www.kaggle.com/c/google-quest-challenge/data。
接下来,我们进行特征工程。我们首先计算问题标题和正文以及答案中的单词数量。这是一个简单但出人意料的有用特征,在许多应用中都很受欢迎:
for colname in ['question_title', 'question_body', 'answer']:
newname = colname + '_word_len'
xtrain[newname] = xtrain[colname].str.split().str.len()
xtest[newname] = xtest[colname].str.split().str.len()
我们创建的下一个特征是词汇多样性,计算文本块中独特单词的比例:
colname = 'answer'
xtrain[colname+'_div'] = xtrain[colname].apply
(lambda s: len(set(s.split())) / len(s.split()) )
xtest[colname+'_div'] = xtest[colname].apply
(lambda s: len(set(s.split())) / len(s.split()) )
当处理来自在线的信息时,我们可以通过检查网站地址的组成部分(我们将组成部分定义为由点分隔的地址元素)来提取可能的信息性特征;我们计算组成部分的数量,并将单个组成部分作为特征存储:
for df in [xtrain, xtest]:
df['domcom'] = df['question_user_page'].apply
(lambda s: s.split('://')[1].split('/')[0].split('.'))
# Count components
df['dom_cnt'] = df['domcom'].apply(lambda s: len(s))
# Pad the length in case some domains have fewer components in the name
df['domcom'] = df['domcom'].apply(lambda s: s + ['none', 'none'])
# Components
for ii in range(0,4):
df['dom_'+str(ii)] = df['domcom'].apply(lambda s: s[ii])
许多目标列处理的是答案对于给定问题的相关性。一种量化这种关系的方法是评估字符串对中的共享单词:
# Shared elements
for df in [xtrain, xtest]:
df['q_words'] = df['question_body'].apply(lambda s: [f for f in s.split() if f not in eng_stopwords] )
df['a_words'] = df['answer'].apply(lambda s: [f for f in s.split() if f not in eng_stopwords] )
df['qa_word_overlap'] = df.apply(lambda s: len(np.intersect1d(s['q_words'], s['a_words'])), axis = 1)
df['qa_word_overlap_norm1'] = df.apply(lambda s: s['qa_word_overlap']/(1 + len(s['a_words'])), axis = 1)
df['qa_word_overlap_norm2'] = df.apply(lambda s: s['qa_word_overlap']/(1 + len(s['q_words'])), axis = 1)
df.drop(['q_words', 'a_words'], axis = 1, inplace = True)
停用词和标点符号的出现模式可以告诉我们一些关于风格和意图的信息:
for df in [xtrain, xtest]:
## Number of characters in the text ##
df["question_title_num_chars"] = df["question_title"].apply(lambda x: len(str(x)))
df["question_body_num_chars"] = df["question_body"].apply(lambda x: len(str(x)))
df["answer_num_chars"] = df["answer"].apply(lambda x: len(str(x)))
## Number of stopwords in the text ##
df["question_title_num_stopwords"] = df["question_title"].apply(lambda x: len([w for w in str(x).lower().split() if w in eng_stopwords]))
df["question_body_num_stopwords"] = df["question_body"].apply(lambda x: len([w for w in str(x).lower().split() if w in eng_stopwords]))
df["answer_num_stopwords"] = df["answer"].apply(lambda x: len([w for w in str(x).lower().split() if w in eng_stopwords]))
## Number of punctuations in the text ##
df["question_title_num_punctuations"] =df['question_title'].apply(lambda x: len([c for c in str(x) if c in string.punctuation]) )
df["question_body_num_punctuations"] =df['question_body'].apply(lambda x: len([c for c in str(x) if c in string.punctuation]) )
df["answer_num_punctuations"] =df['answer'].apply(lambda x: len([c for c in str(x) if c in string.punctuation]) )
## Number of title case words in the text ##
df["question_title_num_words_upper"] = df["question_title"].apply(lambda x: len([w for w in str(x).split() if w.isupper()]))
df["question_body_num_words_upper"] = df["question_body"].apply(lambda x: len([w for w in str(x).split() if w.isupper()]))
df["answer_num_words_upper"] = df["answer"].apply(lambda x: len([w for w in str(x).split() if w.isupper()]))
在准备“复古”特征时——我们的重点是文本的简单汇总统计,而不关注语义结构——我们可以继续为问题和答案创建嵌入。理论上,我们可以在我们的数据上训练一个单独的 word2vec 类型的模型(或者微调现有的一个),但为了这次演示,我们将直接使用预训练的模型。一个有用的选择是来自 Google 的通用句子编码器(tfhub.dev/google/universal-sentence-encoder/4)。这个模型在多种数据源上进行了训练。它接受一段英文文本作为输入,并输出一个 512 维度的向量。
module_url = "../input/universalsentenceencoderlarge4/"
embed = hub.load(module_url)
将文本字段转换为嵌入的代码如下:我们按批处理遍历训练/测试集中的条目,为每个批次嵌入(出于内存效率的考虑),然后将它们附加到原始列表中。
最终的数据框是通过垂直堆叠每个批次级别的嵌入列表构建的:
embeddings_train = {}
embeddings_test = {}
for text in ['question_title', 'question_body', 'answer']:
train_text = xtrain[text].str.replace('?', '.').str.replace('!', '.').tolist()
test_text = xtest[text].str.replace('?', '.').str.replace('!', '.').tolist()
curr_train_emb = []
curr_test_emb = []
batch_size = 4
ind = 0
while ind*batch_size < len(train_text):
curr_train_emb.append(embed(train_text[ind*batch_size: (ind + 1)*batch_size])["outputs"].numpy())
ind += 1
ind = 0
while ind*batch_size < len(test_text):
curr_test_emb.append(embed(test_text[ind*batch_size: (ind + 1)*batch_size])["outputs"].numpy())
ind += 1
embeddings_train[text + '_embedding'] = np.vstack(curr_train_emb)
embeddings_test[text + '_embedding'] = np.vstack(curr_test_emb)
print(text)
给定问题和答案的向量表示,我们可以通过在向量对上使用不同的距离度量来计算字段之间的语义相似度。尝试不同度量的背后的想法是希望捕捉到各种类型的特征;在分类的背景下,这可以类比为使用准确性和熵来全面了解情况:
l2_dist = lambda x, y: np.power(x - y, 2).sum(axis=1)
cos_dist = lambda x, y: (x*y).sum(axis=1)
dist_features_train = np.array([
l2_dist(embeddings_train['question_title_embedding'], embeddings_train['answer_embedding']),
l2_dist(embeddings_train['question_body_embedding'], embeddings_train['answer_embedding']),
l2_dist(embeddings_train['question_body_embedding'], embeddings_train['question_title_embedding']),
cos_dist(embeddings_train['question_title_embedding'], embeddings_train['answer_embedding']),
cos_dist(embeddings_train['question_body_embedding'], embeddings_train['answer_embedding']),
cos_dist(embeddings_train['question_body_embedding'], embeddings_train['question_title_embedding'])
]).T
dist_features_test = np.array([
l2_dist(embeddings_test['question_title_embedding'], embeddings_test['answer_embedding']),
l2_dist(embeddings_test['question_body_embedding'], embeddings_test['answer_embedding']),
l2_dist(embeddings_test['question_body_embedding'], embeddings_test['question_title_embedding']),
cos_dist(embeddings_test['question_title_embedding'], embeddings_test['answer_embedding']),
cos_dist(embeddings_test['question_body_embedding'], embeddings_test['answer_embedding']),
cos_dist(embeddings_test['question_body_embedding'], embeddings_test['question_title_embedding'])
]).T
让我们分别收集距离特征到不同的列中:
for ii in range(0,6):
xtrain['dist'+str(ii)] = dist_features_train[:,ii]
xtest['dist'+str(ii)] = dist_features_test[:,ii]
最后,我们还可以创建文本字段的TF-IDF表示;一般想法是创建基于输入文本多种变换的多个特征,然后将它们输入到一个相对简单的模型中。
这样,我们可以在不拟合复杂深度学习模型的情况下捕获数据的特征。
我们可以通过分析文本的词和字符级别来实现这一点。为了限制内存消耗,我们对这两种类型的特征的最大数量设置上限(你的里程可能会有所不同;如果有更多的内存,这些限制可以增加):
limit_char = 5000
limit_word = 25000
我们实例化了字符级和词级向量器。我们问题的设置使得 Scikit-learn 的Pipeline功能的使用变得方便,允许在模型拟合过程中组合多个步骤。我们首先为标题列创建两个单独的转换器(词级和字符级):
title_col = 'question_title'
title_transformer = Pipeline([
('tfidf', TfidfVectorizer(lowercase = False, max_df = 0.3, min_df = 1,
binary = False, use_idf = True, smooth_idf = False,
ngram_range = (1,2), stop_words = 'english',
token_pattern = '(?u)\\b\\w+\\b' , max_features = limit_word ))
])
title_transformer2 = Pipeline([
('tfidf2', TfidfVectorizer(sublinear_tf=True,
strip_accents='unicode', analyzer='char',
stop_words='english', ngram_range=(1, 4), max_features= limit_char))
])
我们对正文使用相同的逻辑(两个不同的管道转换器):
body_col = 'question_body'
body_transformer = Pipeline([
('tfidf',TfidfVectorizer(lowercase = False, max_df = 0.3, min_df = 1,
binary = False, use_idf = True, smooth_idf = False,
ngram_range = (1,2), stop_words = 'english',
token_pattern = '(?u)\\b\\w+\\b' , max_features = limit_word ))
])
body_transformer2 = Pipeline([
('tfidf2', TfidfVectorizer( sublinear_tf=True,
strip_accents='unicode', analyzer='char',
stop_words='english', ngram_range=(1, 4), max_features= limit_char))
])
最后,对于答案列:
answer_col = 'answer'
answer_transformer = Pipeline([
('tfidf', TfidfVectorizer(lowercase = False, max_df = 0.3, min_df = 1,
binary = False, use_idf = True, smooth_idf = False,
ngram_range = (1,2), stop_words = 'english',
token_pattern = '(?u)\\b\\w+\\b' , max_features = limit_word ))
])
answer_transformer2 = Pipeline([
('tfidf2', TfidfVectorizer( sublinear_tf=True,
strip_accents='unicode', analyzer='char',
stop_words='english', ngram_range=(1, 4), max_features= limit_char))
])
我们通过处理数值特征来结束特征工程部分。我们只使用简单的方法:用缺失值插补处理 N/A 值,并使用幂转换器来稳定分布并使其更接近高斯分布(如果你在神经网络中使用数值特征,这通常很有帮助):
num_cols = [
'question_title_word_len', 'question_body_word_len',
'answer_word_len', 'answer_div',
'question_title_num_chars','question_body_num_chars',
'answer_num_chars',
'question_title_num_stopwords','question_body_num_stopwords',
'answer_num_stopwords',
'question_title_num_punctuations',
'question_body_num_punctuations','answer_num_punctuations',
'question_title_num_words_upper',
'question_body_num_words_upper','answer_num_words_upper',
'dist0', 'dist1', 'dist2', 'dist3', 'dist4', 'dist5'
]
num_transformer = Pipeline([
('impute', SimpleImputer(strategy='constant', fill_value=0)),
('scale', PowerTransformer(method='yeo-johnson'))
])
Pipelines 的一个有用特性是它们可以被组合和嵌套。接下来,我们添加处理分类变量的功能,然后将所有这些整合到一个ColumnTransformer对象中,以简化数据预处理和特征工程逻辑。输入的每个部分都可以以适当的方式处理:
cat_cols = [ 'dom_0', 'dom_1', 'dom_2',
'dom_3', 'category','is_question_no_name_user',
'is_answer_no_name_user','dom_cnt'
]
cat_transformer = Pipeline([
('impute', SimpleImputer(strategy='constant', fill_value='')),
('encode', OneHotEncoder(handle_unknown='ignore'))
])
preprocessor = ColumnTransformer(
transformers = [
('title', title_transformer, title_col),
('title2', title_transformer2, title_col),
('body', body_transformer, body_col),
('body2', body_transformer2, body_col),
('answer', answer_transformer, answer_col),
('answer2', answer_transformer2, answer_col),
('num', num_transformer, num_cols),
('cat', cat_transformer, cat_cols)
]
)
最后,我们准备好使用一个结合预处理和模型拟合的Pipeline对象:
pipeline = Pipeline([
('preprocessor', preprocessor),
('estimator',Ridge(random_state=RANDOM_STATE))
])
总是评估你的模型在样本外的性能是一个好主意:一个方便的做法是创建折叠外预测,这在第六章中已经讨论过。该过程涉及以下步骤:
-
将数据分成折叠。在我们的案例中,我们使用
GroupKFold,因为一个问题可以有多个答案(在数据框的单独行中)。为了防止信息泄露,我们想要确保每个问题只出现在一个折叠中。 -
对于每个折叠,使用其他折叠中的数据训练模型,并为选择的折叠以及测试集生成预测。
-
对测试集上的预测进行平均。
我们开始准备“存储”矩阵,我们将在这里存储预测。mvalid将包含折叠外的预测,而mfull是整个测试集预测的占位符,这些预测在折叠间平均。由于几个问题包含多个候选答案,我们在question_body上对KFold分割进行分层:
nfolds = 5
mvalid = np.zeros((xtrain.shape[0], len(target_cols)))
mfull = np.zeros((xtest.shape[0], len(target_cols)))
kf = GroupKFold(n_splits= nfolds).split(X=xtrain.question_body, groups=xtrain.question_body)
我们遍历折叠并构建单独的模型:
for ind, (train_index, test_index) in enumerate(kf):
# Split the data into training and validation
x0, x1 = xtrain.loc[train_index], xtrain.loc[test_index]
y0, y1 = ytrain.loc[train_index], ytrain.loc[test_index]
for ii in range(0, ytrain.shape[1]):
# Fit model
be = clone(pipeline)
be.fit(x0, np.array(y0)[:,ii])
filename = 'ridge_f' + str(ind) + '_c' + str(ii) + '.pkl'
pickle.dump(be, open(filename, 'wb'))
# Storage matrices for the OOF and test predictions, respectively
mvalid[test_index, ii] = be.predict(x1)
mfull[:,ii] += be.predict(xtest)/nfolds
print('---')
一旦拟合部分完成,我们就可以根据竞赛中指定的指标来评估性能:
corvec = np.zeros((ytrain.shape[1],1))
for ii in range(0, ytrain.shape[1]):
mvalid[:,ii] = rankdata(mvalid[:,ii])/mvalid.shape[0]
mfull[:,ii] = rankdata(mfull[:,ii])/mfull.shape[0]
corvec[ii] = stats.spearmanr(ytrain[ytrain.columns[ii]], mvalid[:,ii])[0]
print(corvec.mean())
最终得分是 0.34,作为一个起点来说相当可以接受。
在本节中,我们展示了如何在文本体上构建描述性特征。虽然这并不是 NLP 竞赛的获胜公式(得分尚可,但并不能保证进入奖牌区),但它是一个值得保留在工具箱中的有用工具。我们以一个概述文本增强技术的章节来结束这一章。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/kgl-bk/img/Shotaro_Ishihara.png
Shotaro Ishihara
我们这一章节的第二位访谈对象是 Shotaro Ishihara,别名 u++,他是PetFinder.my Adoption Prediction竞赛获胜团队的成员,同时也是一位竞赛和笔记大师。他目前是一家日本新闻媒体公司的数据科学家和研究员,并在日本出版了关于 Kaggle 的书籍,包括 Abhishek Thakur 书籍的翻译。他还维护着一个关于 Kaggle 活动的每周日文通讯 (www.getrevue.co/profile/upura)).
我们在哪里可以找到你写的/翻译的 Kaggle 书籍?
www.kspub.co.jp/book/detail/5190067.html 是基于Titanic GettingStarted竞赛的 Kaggle 入门指南。
book.mynavi.jp/ec/products/detail/id=123641 是 Abhishek Thakur 的《Approaching (Almost) Any Machine Learning Problem》的日文翻译。
你最喜欢的竞赛类型是什么?为什么?在技术和解决方法方面,你在 Kaggle 上的专长是什么?
在 Kaggle,我喜欢参加那些包含表格或文本数据集的竞赛。这类数据集对我来说很熟悉,因为它们在新闻媒体公司中广泛使用。我对处理这些数据集的方法有很好的了解。
你是如何参加 Kaggle 竞赛的?这种方法和你在日常工作中所做的方法有何不同?
第一个过程是相同的:通过探索性数据分析来思考如何解决这个问题。Kaggle 假设会使用高级机器学习,但在商业中并非如此。在实践中,我试图找到避免使用机器学习的方法。即使我确实使用了它,我也更愿意使用 TF-IDF 和线性回归等经典方法,而不是 BERT 等高级方法。
我们对了解如何避免在现实世界问题中使用机器学习感兴趣。你能给我们举一些例子吗?
在工作上处理自动文章摘要时,我们采用了一种更直接的提取方法(www.jstage.jst.go.jp/article/pjsai/JSAI2021/0/JSAI2021_1D2OS3a03/_article/-char/en),而不是基于神经网络的方法(www.jstage.jst.go.jp/article/pjsai/JSAI2021/0/JSAI2021_1D4OS3c02/_article/-char/en)。
使用机器学习很难保证 100%的性能,有时人们更倾向于选择简单的方法,这些方法易于人类理解和参与。
请告诉我们你参加的一个特别具有挑战性的竞赛,以及你用来解决这个任务的见解。
在PetFinder.my Adoption Prediction竞赛中,提供了一个多模态数据集。许多参赛者试图探索和使用所有类型的数据,主要方法是从图像和文本中提取特征,将它们连接起来,并训练 LightGBM。我也采用了同样的方法。令人惊讶的是,我的一个队友 takuoko(www.kaggle.com/takuok)开发了一个处理所有数据集端到端的大神经网络。设计良好的神经网络有可能在多模态竞赛中优于 LightGBM。这是我在 2019 年学到的一课。
这个教训今天仍然有效吗?
我认为答案是肯定的。与 2019 年相比,神经网络在处理多模态数据方面越来越好。
Kaggle 是否对你的职业生涯有所帮助?如果是的话,是如何帮助的?
是的。Kaggle 给了我很多数据分析的经验。我从 Kaggle 获得的人工智能知识极大地帮助我更成功地工作。我在 Kaggle 和商业工作中的成就是我获得 2020 年国际新闻媒体协会颁发的 30 Under 30 奖项和大奖的主要原因之一。Kaggle 还让我结识了许多人。这些关系无疑对我的职业发展做出了贡献。
你是如何通过 Kaggle 建立起你的作品集的?
学习到的技能、取得的竞赛结果以及发布的 Notebooks、书籍、通讯等。
你是如何推广你的发布的?
我拥有多种沟通渠道,并使用适当的工具进行推广。例如,Twitter、个人博客和 YouTube。
在你的经验中,没有经验的 Kagglers 通常忽略了什么?你现在知道什么,而你在最初开始时希望知道的?
探索性数据分析的重要性。在机器学习领域,有一个无免费午餐定理的概念。我们不仅应该学习算法,还应该学习如何应对挑战。无免费午餐定理是一个声明,即没有一种通用的模型能够在所有问题上都表现良好。
在机器学习竞赛中,找到适合数据集和任务特性的模型对于提高你的分数至关重要。
你在过去比赛中犯过哪些错误?
过度拟合到公共排行榜。在 LANL 地震预测 竞赛中,我在公共排行榜上得分很高,最终以第五名的成绩完成了比赛。然而,我的最终排名是 211^(th),这意味着我过于相信了一个有限的数据集。过度拟合是机器学习中一个非常流行的概念,我在 Kaggle 的痛苦经历中意识到了这一点的重要性。
你建议采取什么特定的方法来避免过度拟合?
仔细观察训练集和评估集是如何划分的非常重要。我试图构建一个验证集,使其能够重现这种划分。
你会推荐使用哪些特定的工具或库来进行数据分析或机器学习?
我喜欢 Pandas,这是一个处理表格数据集的必备库。我通过提取、聚合和可视化来使用它进行数据探索性分析。
你建议读者如何掌握 Pandas?
你可以查看一些社区教程。Kaggle 还提供了一些关于 Pandas 和特征工程的教程课程。
你使用其他竞赛平台吗?它们与 Kaggle 相比如何?
我有时会使用像 Signate、Nishika 等日本平台(upura.github.io/projects/data_science_competitions/)。这些平台在功能和 UX/UX 方面显然不如 Kaggle,但看到熟悉的主题,如日语,还是很有趣的。
文本增强策略
我们在前一章中广泛讨论了计算机视觉问题的增强策略。相比之下,针对文本数据的类似方法是一个不太被探索的领域(正如没有像albumentations这样的单一包所证明的那样)。在本节中,我们展示了处理该问题的可能方法之一。
基本技术
通常,首先检查基本方法是有益的,重点关注随机变化和同义词处理。Wei 和 Zou(2019)在arxiv.org/abs/1901.11196提供了对基本方法的有系统研究。
我们从同义词替换开始。用同义词替换某些单词会产生与原文意思相近但略有扰动的文本(如果你对同义词的来源等更多细节感兴趣,可以查看wordnet.princeton.edu/上的项目页面)。
def get_synonyms(word):
synonyms = set()
for syn in wordnet.synsets(word):
for l in syn.lemmas():
synonym = l.name().replace("_", " ").replace("-", " ").lower()
synonym = "".join([char for char in synonym if char in ' qwertyuiopasdfghjklzxcvbnm'])
synonyms.add(synonym)
if word in synonyms:
synonyms.remove(word)
return list(synonyms)
我们在上述定义的工作函数周围创建了一个简单的包装器,指定一段文本(包含多个单词的字符串),并最多替换其中的n个单词:
def synonym_replacement(words, n):
words = words.split()
new_words = words.copy()
random_word_list = list(set([word for word in words if word not in stop_words]))
random.shuffle(random_word_list)
num_replaced = 0
for random_word in random_word_list:
synonyms = get_synonyms(random_word)
if len(synonyms) >= 1:
synonym = random.choice(list(synonyms))
new_words = [synonym if word == random_word else word for word in new_words]
num_replaced += 1
if num_replaced >= n: # Only replace up to n words
break
sentence = ' '.join(new_words)
return sentence
让我们看看这个函数在实际中的应用:
print(f" Example of Synonym Replacement: {synonym_replacement('The quick brown fox jumps over the lazy dog',4)}")
Example of Synonym Replacement: The spry brown university fox jumpstart over the lazy detent
这并不完全符合莎士比亚的风格,但它确实传达了相同的信息,同时显著改变了风格。我们可以通过为每条推文创建多个新句子来扩展这种方法:
trial_sent = data['text'][25]
print(trial_sent)
the free fillin' app on my ipod is fun, im addicted
for n in range(3):
print(f" Example of Synonym Replacement: {synonym_replacement(trial_sent,n)}")
Example of Synonym Replacement: the free fillin' app on my ipod is fun, im addict
Example of Synonym Replacement: the innocent fillin' app on my ipod is fun, im addicted
Example of Synonym Replacement: the relinquish fillin' app on my ipod is fun, im addict
如您所见,使用同义词生成文本片段的变体相当直接。
接下来,交换是一种简单而有效的方法;我们通过随机交换文本中单词的顺序来创建一个修改后的句子。
仔细应用,这可以被视为一种可能有用的正则化形式,因为它干扰了像 LSTM 这样的模型所依赖的数据的顺序性。第一步是定义一个交换单词的函数:
def swap_word(new_words):
random_idx_1 = random.randint(0, len(new_words)-1)
random_idx_2 = random_idx_1
counter = 0
while random_idx_2 == random_idx_1:
random_idx_2 = random.randint(0, len(new_words)-1)
counter += 1
if counter > 3:
return new_words
new_words[random_idx_1], new_words[random_idx_2] = new_words[random_idx_2], new_words[random_idx_1]
return new_words
然后,我们围绕这个函数编写一个包装器:
# n is the number of words to be swapped
def random_swap(words, n):
words = words.split()
new_words = words.copy()
for _ in range(n):
new_words = swap_word(new_words)
sentence = ' '.join(new_words)
return sentence
同义词和交换不会影响我们修改的句子的长度。如果在一个特定的应用中修改该属性是有用的,我们可以在句子中删除或添加单词。
实现前者的最常见方法是随机删除单词:
def random_deletion(words, p):
words = words.split()
# Obviously, if there's only one word, don't delete it
if len(words) == 1:
return words
# Randomly delete words with probability p
new_words = []
for word in words:
r = random.uniform(0, 1)
if r > p:
new_words.append(word)
# If you end up deleting all words, just return a random word
if len(new_words) == 0:
rand_int = random.randint(0, len(words)-1)
return [words[rand_int]]
sentence = ' '.join(new_words)
return sentence
让我们看看一些例子:
print(random_deletion(trial_sent,0.2))
print(random_deletion(trial_sent,0.3))
print(random_deletion(trial_sent,0.4))
the free fillin' app on my is fun, addicted
free fillin' app on my ipod is im addicted
the free on my ipod is fun, im
如果我们可以删除,当然也可以添加。在句子中随机插入单词可以被视为 NLP 中向图像添加噪声或模糊的等效操作:
def random_insertion(words, n):
words = words.split()
new_words = words.copy()
for _ in range(n):
add_word(new_words)
sentence = ' '.join(new_words)
return sentence
def add_word(new_words):
synonyms = []
counter = 0
while len(synonyms) < 1:
random_word = new_words[random.randint(0, len(new_words)-1)]
synonyms = get_synonyms(random_word)
counter += 1
if counter >= 10:
return
random_synonym = synonyms[0]
random_idx = random.randint(0, len(new_words)-1)
new_words.insert(random_idx, random_synonym)
这是函数在行动中的样子:
print(random_insertion(trial_sent,1))
print(random_insertion(trial_sent,2))
print(random_insertion(trial_sent,3))
the free fillin' app on my addict ipod is fun, im addicted
the complimentary free fillin' app on my ipod along is fun, im addicted
the free along fillin' app addict on my ipod along is fun, im addicted
我们可以将上述讨论的所有变换组合成一个单一的功能,生成相同句子的四个变体:
def aug(sent,n,p):
print(f" Original Sentence : {sent}")
print(f" SR Augmented Sentence : {synonym_replacement(sent,n)}")
print(f" RD Augmented Sentence : {random_deletion(sent,p)}")
print(f" RS Augmented Sentence : {random_swap(sent,n)}")
print(f" RI Augmented Sentence : {random_insertion(sent,n)}")
aug(trial_sent,4,0.3)
Original Sentence : the free fillin' app on my ipod is fun, im addicted
SR Augmented Sentence : the disembarrass fillin' app on my ipod is fun, im hook
RD Augmented Sentence : the free app on my ipod fun, im addicted
RS Augmented Sentence : on free fillin' ipod is my the app fun, im addicted
RI Augmented Sentence : the free fillin' app on gratis addict my ipod is complimentary make up fun, im addicted
上文讨论的增强方法没有利用文本数据的结构——以一个例子来说,分析像“词性”这样的简单特征可以帮助我们构建更有用的原始文本变换。这是我们接下来要关注的方法。
nlpaug
我们通过展示nlpaug包(github.com/makcedward/nlpaug)提供的功能来结束本节。它聚合了文本增强的不同方法,并设计得轻量级且易于集成到工作流程中。以下是一些包含其中的功能示例。
! pip install nlpaug
我们导入字符级和词级增强器,我们将使用它们来插入特定方法:
import nlpaug.augmenter.char as nac
import nlpaug.augmenter.word as naw
test_sentence = "I genuinely have no idea what the output of this sequence of words will be - it will be interesting to find out what nlpaug can do with this!"
当我们将模拟的打字错误应用于测试句子时会发生什么?这种转换可以用多种方式参数化;有关参数及其解释的完整列表,鼓励读者查阅官方文档:nlpaug.readthedocs.io/en/latest/augmenter/char/keyboard.html。
aug = nac.KeyboardAug(name='Keyboard_Aug', aug_char_min=1,
aug_char_max=10, aug_char_p=0.3, aug_word_p=0.3,
aug_word_min=1, aug_word_max=10, stopwords=None,
tokenizer=None, reverse_tokenizer=None,
include_special_char=True, include_numeric=True,
include_upper_case=True, lang='en', verbose=0,
stopwords_regex=None, model_path=None, min_char=4)
test_sentence_aug = aug.augment(test_sentence)
print(test_sentence)
print(test_sentence_aug)
这是输出结果:
I genuinely have no idea what the output of this sequence of words will be - it will be interesting to find out what nlpaug can do with this!
I geb&ine:y have no kdeZ qhQt the 8uYput of tTid sequsnDr of aorVs will be - it wi,k be jnterewtlHg to find out what nlpaug can do with this!
我们可以模拟一个OCR 错误逐渐渗透到我们的输入中:
aug = nac.OcrAug(name='OCR_Aug', aug_char_min=1, aug_char_max=10,
aug_char_p=0.3, aug_word_p=0.3, aug_word_min=1,
aug_word_max=10, stopwords=None, tokenizer=None,
reverse_tokenizer=None, verbose=0,
stopwords_regex=None, min_char=1)
test_sentence_aug = aug.augment(test_sentence)
print(test_sentence)
print(test_sentence_aug)
我们得到:
I genuinely have no idea what the output of this sequence of words will be - it will be interesting to find out what nlpaug can do with this!
I 9enoine1y have no idea what the ootpot of this sequence of wokd8 will be - it will be inteke8tin9 to find out what nlpaug can du with this!
虽然有用,但在对数据进行创造性更改时,字符级变换的适用范围有限。让我们看看nlpaug在词级修改方面提供了哪些可能性。我们的第一个例子是用反义词替换固定百分比的单词:
aug = naw.AntonymAug(name='Antonym_Aug', aug_min=1, aug_max=10, aug_p=0.3,
lang='eng', stopwords=None, tokenizer=None,
reverse_tokenizer=None, stopwords_regex=None,
verbose=0)
test_sentence_aug = aug.augment(test_sentence)
print(test_sentence)
print(test_sentence_aug)
我们得到:
I genuinely have no idea what the output of this sequence of words will be - it will be interesting to find out what nlpaug can do with this!
I genuinely lack no idea what the output of this sequence of words will differ - it will differ uninteresting to lose out what nlpaug can unmake with this!
nlpaug 还为我们提供了例如替换同义词的可能性;这些转换也可以使用上面讨论的更基本的技术来实现。为了完整性,我们下面展示了一个小样本,它使用了一个底层的 BERT 架构:
aug = naw.ContextualWordEmbsAug(model_path='bert-base-uncased',
model_type='', action='substitute',
# temperature=1.0,
top_k=100,
# top_p=None,
name='ContextualWordEmbs_Aug', aug_min=1,
aug_max=10, aug_p=0.3,
stopwords=None, device='cpu',
force_reload=False,
# optimize=None,
stopwords_regex=None,
verbose=0, silence=True)
test_sentence_aug = aug.augment(test_sentence)
print(test_sentence)
print(test_sentence_aug)
这里是结果:
I genuinely have no idea what the output of this sequence of words will be - it will be interesting to find out what nlpaug can do with this!
i genuinely have no clue what his rest of this series of words will say - its will seemed impossible to find just what we can do with this!
如您所见,nlpaug 为修改您的文本输入以生成增强提供了广泛的选择。实际上应该选择哪一些很大程度上取决于上下文,并且这个决定需要一点领域知识,适合特定的应用。
一些可供进一步探索的地方包括入门级比赛,例如 使用灾难推文的自然语言处理 (www.kaggle.com/c/nlp-getting-started),以及更中级或高级的比赛,如 Jigsaw 毒性评论严重程度评级 (www.kaggle.com/c/jigsaw-toxic-severity-rating) 或 Google QUEST Q&A 标注 (www.kaggle.com/c/google-quest-challenge)。在这些所有情况下,nlpaug 都已被广泛使用——包括在获奖方案中。
摘要
在本章中,我们讨论了 NLP 竞赛的建模。我们展示了适用于 Kaggle 竞赛中出现的各种问题的传统和最先进的方法。此外,我们还触及了经常被忽视的主题——文本增强。
在下一章中,我们将讨论模拟比赛,这是一种近年来逐渐流行起来的新型竞赛类别。
加入我们本书的 Discord 空间
加入本书的 Discord 工作空间,参加每月一次的作者 问我任何问题 活动:
1092

被折叠的 条评论
为什么被折叠?



