SeaseIO-博客中文翻译-一-
SeaseIO 博客中文翻译(一)
原文:SeaseIO Blog
每日歌曲排序问题的学习排序项目-第 2 部分
如果你已经阅读了这篇博文的第 1 部分,你应该已经知道如何从可用的数据开始,使用开源库来建立一个学习排名(LTR)系统。
我们以未来工作的部分结束了这篇文章,其中我们提到了我们想要解决的三个方面。所以这篇博文分为三个主要部分:
-
- 在的第一部分中,我们对数据集进行采样,因为我们有兴趣发现,在拥有更少数据的情况下,我们是否仍然可以获得一个好的模型,从而获得与使用完整数据集进行训练类似(或更好)的结果。
- 在的第二部分,我们希望了解如果我们通过直接排序特定特征的值(在我们的例子中是降序)而不训练任何模型来对结果进行排序,会发生什么。
- 第三部分涉及通过使用库 SHAP 来解释模型的行为;特别是,我们将看到每个特性如何影响模型的输出。
【T8
让我们一起,一个接一个地,来看看为解决上述问题而执行的 3 个不同实现所获得的结果。
1)如果在对我们的数据集进行采样之后,我们根据歌曲位置值来估计相关性标签,会发生什么?
我们通过使用一个只从数据帧中过滤特定行的 Pandas 函数,对我们的完整数据集进行了采样;特别是,我们感兴趣的是获得仅包含从位置 1 到 21 的每日歌曲图表的子集:
subset = data_frame[data_frame["Position"] <= 21]
我们得到了一个由 412.385 个观察值组成的子集,约占整个数据集的 12%,它包含完全相同的数据结构(参见之前博客文章中解释的数据预处理和特征工程部分)。
我们已经直接从完整数据集的训练集和测试集中提取了训练集和测试集(80:20)(这是出于一般考虑中的第二个原因)。特征‘位置’(从 1 到 21)用于估计相关性等级,我们的目标变量;因此,从 0 到 20 的相关性标签是通过以降序颠倒位置值来获得的。这样做是为了使图表上第一个位置(位置 1)的歌曲具有最高的相关性标签(20),而图表上最后一个位置(位置 21)的歌曲具有最低的相关性标签(0)。
然后,我们使用为主要项目选择的相同算法和相关参数(在第 1 部分中描述)训练模型,并且我们使用相同的测试集评估模型的性能,以便在相同的“操场”上比较它们;获得的结果如下表所示:
一般注意事项:
-
- 使用子集的模型训练明显更快
- 我们必须确保训练集和测试集之间没有交集;这意味着测试集的观测值在训练时必须是未知的
- 尽管数据较少,但使用子集构建的模型具有最佳性能;一个原因可能是整个数据集比子集具有更大的可变性
- 将 Doc2Vec 编码应用于音轨名称变量在整个数据集中似乎比在子集中更好
- 当我们使用较小的测试集时,我们得到了更好的结果,尽管没有实质性的差异。在这个领域中,子集的测试集的维度(大约 84.000 个观察值)已经给了我们足够的信息。通常,我们会想象测试集越大越好;实际上,存在一个阈值,超过这个阈值,增加测试集的维数不会带来任何好处。
我们还研究了为什么在使用 Doc2Vec 编码时,eval ndcg@10 的值大于 train ndcg@10(一般情况下应该相反)。这是一种罕见的现象,但它可能发生在任何 ML 模型上;有几个原因:
-
- 测试集大小可能太小
- 测试集由比训练集“更容易”的例子组成
- 在我们的数据集中没有太多的差异
- 我们的训练/测试分割适用于这种偶然行为
无论如何,在这种情况下,差异是无关紧要的,但如果这是一个非常大的差异,我们需要进一步调查。一个建议是尝试使用 k-fold 交叉验证,或者如果模型需要太多时间来训练,就在不同的混合训练/测试集上重新训练,以查看趋势是否持续。
2)此外,如果我们直接使用流计数(降序)对结果进行排序,会有什么变化?我们会在搜索结果列表中得到相同的顺序吗(因此最大 NDCG)?!
为了找到这个问题的答案,我们使用了在数据预处理部分之后获得的完整数据集(具有从 0 到 20 的相关性标签)。
请注意:在下面的所有图像中,相关性标签由排名栏表示
T13T15
由于我们的目标是通过“流”计数对歌曲进行排序,因此查看一下相关矩阵是非常有用和有趣的,该表显示了变量之间的相关系数:
我们可以看到,流与位置(-0.1339)、流与排名(0.1697)之间的相关性很低,而位置与艺人之间的相关性最高(0.4665)。
我们根据'流'特性对整个数据集进行降序排序,通过下面的代码行按 query_ID 分组:
data_frame_sorted = data_frame.groupby(["query_ID"]).apply(lambda x: x.sort_values(["Streams"], ascending=False)).reset_index(drop=True)
一旦我们获得了有序数据集,我们就通过一部分代码手动计算 NDCG@10,其中我们只对每个查询的搜索结果列表的前 10 首歌曲直接应用 DCG 公式及其归一化变体。在下面的示例中,您可以看到分别针对 query_ID = 0 和 query_ID = 1 返回的前 10 个结果。
在排序后的数据集上,我们对“排名”列应用了以下步骤(针对每个查询):
-
- 算一下DCG @ 10【1】
-
- 计算出理想的 DCG@10
-
- 算算 NDCG@10 = DCG@10/理想的 DCG@10
-
- 对所有查询的 NDCG@10 值进行平均
最终结果是 NDCG@10 = 0.9347
在没有训练任何模型的情况下,我们通过直接使用流计数以降序对歌曲进行排序,获得了良好(以及更好)的 NDCG。从上图可以看出,对于查询 0,最大数据流都与图表顶部位置(排名 20)的歌曲相关,而对于查询 1,最大数据流与图表上的较高位置相关,但结果是分散的。
此外,我们还注意到,对于一些查询(如查询 0),排序列表的第一个结果总是由不同日期的同一首歌曲(相同的 ID 表示。假设一个搜索引擎返回一系列相同的项目(相同的歌曲)是没有意义的。我们决定只保留每首歌曲和每个查询的最大数据流数量的观察值:
再次按降序排序后(从上面关于查询 0 的图片中可以看到),最终结果(所有查询的平均值)是 NDCG@10 = 0.8212 。在这种情况下,删除一些观察值会使性能变差。
子集
我们使用子集而不是完整数据集重复了相同的实验,这些是观察到的结果:
-
- 变量之间的相关性较小:
- 位置和流之间的相关性为: -0.0810
- 排名和流之间的相关性是: 0.0810
- 位置与艺术家的相关性为: 0.1563
- 相同的 NDCG@10 ( 0.9347 )
- NDCG@10 略低( 0.8176 而不是 0.8212 ),此时我们只保留了每首歌曲的最大流的观察值。
- 变量之间的相关性较小:
3)通过使用库 SHAP 给出模型行为的解释,特别是每个特征如何影响模型的输出
如果你错过了我们之前关于 SHAP 的博文,请先阅读它,了解这个库的惊人工具,以及如何解释学习使用 TreeSHAP 算法对模型进行排序。
到目前为止,我们已经创建了一个数据集(完整)和一个子集,我们已经使用不同的数据预处理技术训练了几种类型的模型,现在我们想了解这些模型是如何实现这些结果的。
我们对 4 种不同的模型进行了比较:
SHAP 库【2】为每个预测创建模型解释,并描述如果从模型中移除特征 y 预测 x 如何变化。所谓的 SHAP 价值观就是答案。
由于我们使用 LambdaMART(多重加性回归树)构建 LTR 模型,因此我们使用了tree explainer【3】,这是一种在多项式时间内计算树和树集合的 SHAP 值的算法。
TreeSHAP 为我们提供了几种不同类型的绘图,每一种都突出了模型的一个特定方面。Matplotlib ,一个非常有用的可视化库,用于 Python 中图形的渲染。
汇总图
概要图给了我们全局可解释性。使用带有 plot_type = " bar 的 shap.summary_plot 函数,您可以用 x 轴上的平均值(|SHAP 值|)生成特征重要性图(按降序排列的变量)。
我们可以看到,流功能实际上是最重要的,其次是艺术家功能。
如果您想要显示预测值与目标变量的正负关系,您必须生成另一种类型的汇总图:
这些图不仅显示了可变的重要性(从上到下),还显示了特征值(使用从蓝色到红色的色谱)如何影响标签预测;每个观察(歌曲),每行都有一个点。
这里,流的数量越多,对相关性的正面影响就越高(SHAP 值大于零);而‘艺术家’特征与目标变量负相关。如果您还记得,Artists 列是使用留一技术编码的,该技术用与该级别相关的目标变量值的平均值替换分类值,但在计算平均值时排除当前行的目标值;因此,这个数值本身并不重要,但是更有趣的是知道哪个艺术家对应于那个值,以了解谁是最受欢迎的。
其他特征的排序看起来有点不同,但是总体上我们可以说该子集足以代表整个数据集。
决策图
决策图【4】展示了模型如何做出决策,显示了每个特征的累积效应。每条垂直线代表一个预测:
该系统无法获得完整数据集的图表,因此我们只能获得 500.000 个观察值的图表。有趣的方面是,在 M1 和 M2 中,已经编码的特征(col_0 到 col_7)干涉轻微,线条几乎是直的(可能因为是二元变量);另一方面,在 M3 和 M4 有更多的可变性,但是编码特征(0 到 99)太多了,很难分辨哪个音轨名称最有影响。
为了更好地理解决策图,让我们只考虑一个观察。下图显示了不同要素值对单个选定预测的影响,以及它们对模型的正面或负面影响。在我们的案例中,我们总是在不同的模型中绘制相同的观察结果(宋):
例如,在第一个图表中,我们可以看到第 3 个月、第 3 个工作日、第 16 天、col_3 True、Artists 45.981 对模型有正面影响,而其他功能有负面影响。
有趣的是,同一个特征值对最终模型输出的影响不同;此外,使用完整数据集,我们获得的模型输出值大于使用子集时的值(M2 为 4.79,M1 为 0.19,M4 为 3.52,M3 为 0.09)。区别是实质性的:原因可能是删除一些观察值会降低相同观察值的重要性。
力图
这个图给了我们一个局部可解释性,显示了单次观察的 SHAP 值。为了简单起见,让我们采用与上面相同的观察结果,并且只显示一个代表 M1 的力图:
从这个图中,你可以看到:
-
- f(x) 是模型预测值(0.19)
- 基值,即如果我们不知道当前输出的任何特性时预测的值
- 每个要素对输出的影响方式和程度:将预测值推高的要素显示为红色,将预测值推低的要素显示为蓝色。“艺术家”是 45.9809(即 Luis Fonsi)的事实产生了积极影响,而“流”是 27748 的事实产生了消极影响。
现在应该考虑 4 个不同的观察值,并检查其模型的输出值。回答特定查询(例如 query_ID = 0 对应于 Ecuador 地区)的这些歌曲的 SHAP 得分在下表中被报告和排序:
该模型的输出不是相关性标签,但是如果我们观察歌曲之间的 SHAP 分数的相对相关性,它代表相同的概念。由于 0.19 > -4.42 > -4.96 > -9.02,使用 SHAP 分数或相关性标签,结果的排序将是等同的。
依赖图
T2T4
依赖图【5】显示了两个特征对模型预测结果的边际影响。作为第一个例子,我报告了流和艺术家之间的依赖图:
这里每个点对应一个预测;在 x 轴上,我们有流值,在 y 轴上,我们有预测的 SHAP 值,颜色代表艺术家。我们可以看到一个很奇怪的情况:在流数很低的情况下,每个艺人真的会有所作为;反之亦然,在某个阈值之后,每个艺术家以相同的方式影响,并且 SHAP 值是高的和恒定的。
作为第二个例子,我使用相同的特征报告了依赖图,但是在轴上颠倒了它们:
即使在这种情况下,我们也能注意到“艺术家”造成了差异:艺术家的价值观< 50 (plus or minus) produce higher SHAP values than the others. As mentioned above, the ‘Artists’ feature has been encoded with a technique based on the mean of the target variable values (Position on chart) so the Artists who are known to always be in the highest positions are also those who have the lowest encoding values (so the average of the Position values < 50). This result suggests that Artists who have the same relevance, also have the same impact on the model; therefore the most popular Artists positively affect the model’s output.
T3
我们还可以为图表的日期创建这种绘图,也就是说,分别为日、月和工作日功能创建这种绘图,并根据流的数量给点着色:
在这种情况下,我们可以立即看到,日和工作日对完整数据集中的模型输出没有影响(这反映了汇总图的结果)。只有某些日子的流量比其他日子多(例如星期天)。
在子集中,不考虑流,我们可以注意到前半个月、前半年和前半周比后半周产生更大的 SHAP 值,这意味着对输出的积极影响。无论如何,没有额外的信息,很难解释这种类型的行为。
摘要
让我们在一个每日歌曲排名问题上回顾一下学习排名项目的要点。
在第一篇博文中,我们展示了从可用数据开始,创建和操作训练集和测试集,然后使用开源库训练排名模型,建立和构建学习排名(LTR)系统的管道。在第二篇博文中,我们通过处理数据进行了进一步的分析,以寻找见解,并通过使用 SHAP 图书馆对模型的行为进行了解释。所有获得的结果都是在 Kaggle【6】上关于全球每日歌曲排名的 Spotify 数据集上计算的。
外卖食品
-
- 数据预处理和特征工程是至关重要的,也是我们应该更加重视的部分
- 当我们有一个更大的数据集时,Doc2vec 编码技术在模型性能上似乎比哈希编码更好
- 较小的数据集可变性较小,可能更容易理解,但代表性较差
- 评估 ndcg@10 值可能大于训练 ndcg@10 值
- 特别是在某些领域,存在一个阈值,超过这个阈值,增加测试集的大小不会带来任何改进
- 在不训练任何模型的情况下,我们可以通过直接排序(降序或升序)特定特征的值来获得结果的良好排序
- SHAP 图书馆是解释这一特性重要性的有力工具。一些图表向我们展示了许多见解,有助于理解每个变量如何影响模型的输出;然而,如果不了解我们工作的背景,就不容易给出详尽的解释。
未来作品
到目前为止,我们只从 Region 列生成了“query_ID ”,而没有考虑歌曲排行榜的日期。在这篇博文第二部分描述的分析过程中,我们意识到我们可以把它看作是多个查询级别特性的散列,以避免每个查询的歌曲“重复”。在下一篇博文中,我们将创建 query_ID 作为多个变量(地区、日期、月份和工作日)的散列,我们将再次对获得的模型进行比较。
我希望这两篇博客文章很有趣,并且你已经学会了处理 LTR 任务和进行相关调查所需的工具。
// our service
不要脸的塞给我们培训和服务!
我提到过我们做学习排名和搜索相关性培训吗?
我们也提供这些主题的咨询,如果你想让你的搜索引擎更上一层楼,请联系!
// STAY ALWAYS UP TO DATE
订阅我们的时事通讯
你喜欢这个帖子吗?这个帖子是关于一个关于每日歌曲排名问题的学习排名项目。不要忘记订阅我们的时事通讯,以便随时了解信息检索世界的最新动态!
每日歌曲排序问题的学习排序项目-第 3 部分
我们又来了,用新的知识来排列实现。
如果你正在阅读这篇博文,你可能已经熟悉我们关于每日歌曲排名问题的内部项目,在我们以前的博客(第一部分和第二部分)中有所描述。如果没有,我建议你先读一读,以便更好地理解我在这里要阐述的内容。
在深入研究这种新方法的细节之前,让我们先对我们的项目做一个简短的总结:
在第一篇博文中,我们展示了建立学习排名(LTR)系统的流程:我们从可用的数据开始,一个 Kaggle 数据集【1】;我们通过几个数据清理和特征工程技术操纵它;我们创建了训练集和测试集,最后,我们使用 XGBoost 训练了一个排名模型。
在第二篇博文中,我们使用子集而不是完整的数据集进行了进一步的分析,以检查模型性能方面的差异。我们还通过使用名为 SHAP 的强大库来解释模型的行为。
在这篇博文中,我们将创建一个新的查询 Id,并检查在构建训练集时发生了什么。
在前面的实验中,我们从单个特征(“Region”列)生成了“query_ID ”,而没有考虑歌曲排行榜的日期。在博客文章第二部分的第二节中描述的分析过程中,我们意识到我们可以将查询视为多个查询级别特征的散列,以避免每个查询的歌曲“重复”:在特定地区的歌曲排行榜中,一首歌曲可以在排行榜上的同一位置停留几天,甚至几周。
此外,如果我们还考虑歌曲图表的日期(日、月和工作日),我们将获得更具体的查询,从而获得更准确的搜索结果,以便我们可以更好地代表用户的意图和他的需求。
查询 Id 生成
您应该仔细设计如何计算查询 Id。
查询 Id 表示每个查询文档样本中< Q,D >的 Q,并且您的数据集中的相同查询必须具有相同的 Id。
最简单的方法是当您的查询只是一系列自由文本术语时;因此,您必须将相同的术语序列与相同的查询 Id 相关联。仅此而已。
有时,您可能有一个由用户在搜索过程中选择的一系列过滤查询表示的查询。如果是这样,您可能希望将查询 Id 作为这些过滤器的组合来计算。查询越准确,搜索结果就越好。
事实上,生成查询 Id 的另一个好方法是将所有查询级别的特性串联起来(简单散列,聚类)。
查询级(或查询相关)特征描述了查询的属性,它们的值仅取决于查询实例。
在我们的例子中,它们是地区、日、月和工作日。
所以我们决定修改我们的管道,创建一个不同的查询 Id。
让我们回到预处理部分,通过使用一种新方法来生成它:
def **generate_id_from_columns**(input_data_frame, query_features, id_column):
str_id = input_data_frame[query_features[0]].astype(str)
for feature in query_features[1:]:
str_id = str_id + '_' + input_data_frame[feature].astype(str)
input_data_frame[id_column] = pd.factorize(str_id)[0]
在我们的例子中,参数是:
-
- input_data_frame 是 Spotify 数据集(CSV 文件)
- query_features 由以下查询级特性的集合表示:区域、日、月和工作日。
- id_column 是我们想要创建的特征(我们称之为' query_ID' )
一旦创建了“ str_id ”作为多个特性的散列,我们就用 Pandas factorize()函数对其进行操作,该函数将每个字符串与一个整数唯一地链接起来。
在这里,您可以找到一个示例来理解查询 Id 创建中的差异:
(请注意:为了稍后与“旧”模型进行比较,我们没有删除用于构建查询的日、月和工作日功能。)
我们还做了一些统计分析来比较新旧查询 Id。在下表中,您可以看到在新的实现中,我们有了更多的查询(从 54 个到 19675 个),并且每个查询的观察值更少。基本上,虽然我们以前每个查询有大约 400 个歌曲排行榜,但现在我们只有一个,包含多达 200 首歌曲(在某些情况下,我们没有完整的排行榜)。标准差告诉我们每个查询 Id 的行数是否均匀分布。由于这是两个完全不同的实现,所以不容易做直接的比较;然而,我们可以说,在这两种情况下,超过 80%的查询 id 包含超过设定阈值的足够数量的样本,这可能是好的。
训练集和测试集分离
一旦我们获得了具有新查询 Id 的数据帧,我们就按照以前的博客文章中采用的相同实现将其分为训练集和测试集,我将在这里更具体地描述:
-
- 如果我们的查询 id 有一些低于某个阈值的观察值,我们将这些观察值保存在一个名为 under_sampled_only_train 的新数据帧中,因为我们只希望它们出现在训练集中。只有当测试集出现的次数少于整个数据集的 20%时,我们才把它们中的一些移到测试集(具有最高观察次数的查询 id)。
- 所有的相关性标签必须平均分布。事实上,我们为每个相关性标签手动选择所有观察值的 20%,并将它们移动到测试集。这样做,我们确保所有的相关性标签,从 0 到 20,都在两个集合中。
有了新的查询 Id,我们意识到这个实现不能被应用。在这种情况下,对于每个查询,我们只有一个歌曲图表,因此假设只有一首歌曲具有最高的相关性(在图表的第一个位置):由此得出结论,我们只有一个相关性标签为 20 的观察结果,并且它永远不会出现在测试集中。
为简单起见,我们以 query_ID = 0 为例。在下表中,您可以更好地理解到目前为止所解释的内容。
查询 0 有 200 个观测值(一张宋图);我们只有很少的相关性标签从 10 到 20 的观察结果(正如在第 1 部分——相关性评级中应用的映射所预期的那样),它们永远不会被移动到测试集中。为什么?
-
- 对于相关性标签= 0 ,我们总共有 50 个观察值,我们取这些观察值的 20%,因此 10 行将被移动到测试集。
- 对于 Relevance Label = 7 我们总共有 10 个观察值,我们取这些观察值的 20%,因此将有 2 行被移动到测试集。
- 对于 Relevance Label = 10 ,我们总共有 3 个观察值,3 的 20%是 0.6,因此没有行将被移动到测试集(对于剩余的行也是如此)。
因此,不可能在两个集合中都有所有的相关性标签。
我们决定实现一种不同的方法:我们随机打乱数据帧,然后将其分成训练集和测试集,确保每个查询 Id 的 20%的观察结果移动到测试集,而与相关性标签无关。当然,最好在两个集合中都有相关性标签,但这样一来,它们的分布至少变得更加公平。
最后,我们使用 LambdaMART 训练了一个学习排序模型。使用旧的分割,我们将在具有所有相关性标签的样本上训练模型,但是它们中的大多数(从 10 到 20)将不会被测试,因为它们不存在于测试集中。如果没有对一部分重要数据进行测试,我们如何判断模型是否表现良好?!
结果
在这个新的实施中,我们采取了不同的方法,将管道分为两个部分:
-
- 当生成查询 Id 时
- 在我们分割数据集的阶段
让我们检查获得的结果:
您可以看到,我们使用哈希编码的变体(对于‘Title’特性)比另一个变体(Doc2Vec 编码)具有更好的模型性能。
使用哈希编码,我们检查了使用新旧查询 Id 训练模型之间的差异,然后我们在相同的测试集上测试了这些模型。两个型号的特征相同,因此可以进行比较;唯一改变的是查询 Id。在第一个实现(OldQueryId 模型)中,我们的目标是获得基于地区的最佳排序,而现在我们的目标是获得基于地区、日、月和工作日的最佳排序。
当我们使用两个不同模型的训练集和测试集时,我们必须确保它们之间没有交集:这意味着测试集的观察值在训练时必须是未知的。
例如,我们以“OldQueryId 模型”的训练集和“NewQueryId 模型”的测试集为例。我们必须检查它们是否有共同的观察值,这意味着如果两行对于除查询 Id 之外的所有特性都有相同的值,我们就认为这两行是相同的。我们在 Pandas【2】中使用了合并功能:
intersections = training_set.merge(test_set, how='inner', on='cols')
其中:
-
- 交集 =将包含训练集和测试集之间共有的所有行
- training_set :数据帧=旧查询 Id 的训练集
- test_set :要合并的对象=具有新查询 Id 的测试集(哈希)
- 如何:要执行的合并类型= '内部'连接以获取交集
- on:column to join on = 'cols'(除查询 Id 外的所有特性)
查询 Id 被排除在外,因为它是在这两个实现过程中唯一被不同操作的特性。在匹配的情况下,我们从训练集中删除公共行,并将它们留在测试集中。
总的来说,我们可以看到使用新查询 ID 的模型性能更好。作为多个查询级别特征的散列的查询 Id 能够返回更好的搜索结果排序。当我们使用新 query_ID 的训练集和旧 query_ID 的测试集(反之亦然)时, train-ndcg@10 下降了一点点。这可能是因为从定型集中删除了公共行,因此模型需要学习的输入数据较少。
最终考虑
如果可能,查询 Id 应包含所有查询级别的特征。
目标是达到每个查询 Id 样本的均匀分布。事实上,您需要小心计算查询 id 的方式:如果粒度太细,您可能会得到很多样本很少的查询 id;另一方面,如果你放松你的粒度,你可能最终会得到一个庞大的排名列表,但却不那么精确,因为它们代表了更广泛的概念。你应该试着平衡一下。
如果查询 id 欠采样,则丢弃训练样本。也许还应该说,这必须语境化。如果我们有可靠的数据,并且确信我们已经适当地管理了可能导致 NDCG 暴涨的条件,我们可以尝试将这些观察值保留在训练集中,或者为所有这些观察值分配一个新的查询 Id。
如果您清理数据集,然后分割它,您必须小心,因为您可能会得到一个不公平的测试集:
-
- 测试集不能有欠采样的查询 id
- 测试集不能有带有单个相关性标签的查询 id
- 测试集必须具有代表性,并且具有可接受的大小(根据每个查询的观察值)
- 可能地,所有相关性标签都应该在两个集合中
// our service
不要脸的塞给我们培训和服务!
我提到过我们做学习排名和搜索相关性培训吗?
我们还提供关于这些主题的咨询,如果您想让您的搜索引擎更上一层楼,请联系!
// STAY ALWAYS UP TO DATE
订阅我们的时事通讯
你喜欢这篇关于 Drop constant features:一个现实世界的学习排名场景的帖子吗?不要忘记订阅我们的时事通讯,以便随时了解信息检索世界的最新动态!
每日歌曲排序问题的学习排序项目-第 4 部分
你真的以为一切都结束了吗?好消息!
关于每日歌曲排名问题的学习排名项目已有第四集。如果你对将机器学习与搜索相结合感兴趣,在这篇博客文章中,我们将继续探索学习按国家和日期对顶级 k 歌检索进行排序的技术。
和往常一样,为了有一个整体的画面,我建议你看一下以前的剧集,在那里你将学习:
第 1 部分——如何从可用的数据开始并使用开源库
建立和构建一个学习分级(LTR)系统第 2 部分——如何通过使用强大的库 SHAP
解释模型的行为第 3 部分——如何创建查询 ID 并在分割训练集和测试集时要小心
在这篇博文中,我将尝试回答以下问题:
-
- 在使用查询级特性创建查询 Id 之后,删除它们有意义吗?
- 如果我删除欠采样查询会发生什么?
- 如果我将欠采样查询分组到一个不太细粒度的queryy Id 中,会发生什么?
我们开始吧!
问题 1)在使用查询级特征来创建查询 ID 之后移除它们:是或否?
正如在以前的博客文章中已经解释过的,训练集中的每一行都是一个查询-文档对;它由查询级、文档级和潜在的查询文档级特性组成。我们指定“ query_ID ”作为每个查询的惟一查询 ID,通过连接所有查询级别的特性(如果可能的话)。
T31
在我们的项目中,查询级别的特性是区域、日、月和工作日。我们使用它们来创建查询 Id 哈希 ( query_ID ),如下表所示:
很自然地想到我们可以删除查询级别的特性,因为该信息已经在查询 Id 中列出。
每个查询 Id 将具有其排序的文档列表,该列表将由查询级特征的相同值组成;那么保留它们的理由是什么呢?
另一方面,通过移除所有查询级特征,在重新排序时,模型将实际上仅依赖于文档级和查询文档级特征来决定如何重新排序文档;正确吗?还是低估?既然查询级别的特性是多余的,我们可以删除它们吗?
我们使用 XGBoost ( LambdaMART )训练了一个学习排序模型,并运行了两个不同的测试:
-
- 丢弃所有查询级特征(共 13 个特征)-> 丢弃
- 保留所有查询级特征(共 17 个特征)-> 保留
【T6
这里获得的结果(表 1):
我们可以看到没有很大的区别,但是保留查询级别的特性似乎可以获得更高的性能结果。
为了更好的比较,我们在相同的测试集上测试了它们;为此,训练集和测试集中的功能在数量和名称方面必须相同,否则我们会得到以下错误消息:“值错误:功能名称不匹配”。
T3T5
所以在这种情况下,我们没有删除查询级别的特性,而是保留并填充了 nan 值;这里举个例子:
只有这样模型才能被训练。获得的结果如下(表 2):
删除查询级别的特性(DROP-Table 1)或者用' nan' '值填充它们没有任何区别。从上表可以看出,无论我们是否在测试集中使用它们,它们都是完全不被考虑的;事实上,结果总是一样的。
另一方面,我们可以注意到,如果我们在训练时保留查询级别的特性,但我们不测试它们,eval-ndcg@10 会在某种程度上受到负面影响(表 3):
一个原因可能是保留查询级别的特征使模型能够调整其他特征的权重,从而影响树的构建。
还应该指出的是,当处理真实世界的数据时,可能会碰巧有太多的特征,这可能会在对分类特征应用编码技术时导致一些问题。例如,如果使用一次性编码技术,则基数越大的分类要素越多,创建的编码要素也就越多。出于这个原因,我们可能会妥协,只保留最有意义的查询级特性(或者选择其他编码技术)。
问题 2)如果查询 id 采样不足,则丢弃训练样本
评估每个查询 Id 的训练样本的分布是非常重要的,因为我们可能会得到不真实的结果。很少有观察让我们对文件的相关性没有足够的信心。此外,需要避免的两个常见错误是:
–如果我们的观察值数量有限,那么在分割过程中,我们可能会得到一些只有一个训练样本的查询。在这种情况下,查询组的 NDCG@K 将是 1,与模型无关;
–在分割过程中,我们可以将所有带有单一相关性标签的样本放入测试集中。
这就是为什么建议删除欠采样查询的原因。然而,正如已经说过的,这个概念必须结合具体情况。如果我们有可靠的数据,并且我们确信我们已经适当地控制了可能使 NDCG 暴涨的条件(如上所述),我们可以试着保留那些观察结果。
事实上,这就是我们在前面的实现中所做的;我们设置了一个阈值,对于具有低于该阈值的大量观察值的查询 id,我们将这些观察值保存在一个名为 under_sampled_only_train 的新数据帧中。这是因为我们将这些观察值保留在训练集中,并且仅当测试集出现的次数少于整个数据集的 20%时,才将其中一些移动到测试集(具有最高数量观察值的查询 id)。为什么采用这种方法?在大多数情况下,训练模型使用更多的数据(因为我们有它!)通常更好;然而,通过这个实验,我们调查并深入了解这是否真的带来了优势。
事实上,我们想知道:如果我们完全删除' under_sampled_only_train '观察值,而不是将它们保留在训练集中,会发生什么?
【T8
我们检查了通过保留欠采样查询 id(KEEP)和通过删除它们( DROP )来训练模型之间的差异,然后我们在相同的测试集上测试这些模型(它根本不包含欠采样查询!).这两个模型具有相同的特性(包括查询级特性),因此可以对它们进行比较;唯一改变的是总观察值的数量,也就是查询 id 的数量。结果总结如下(表 4):
我们可以注意到没有很大的区别。在我们的例子中,将欠采样查询保留在训练集中似乎可以获得相同或略好的结果,因此我们可以非常确定我们拥有可靠的数据,并且性能是真实的。
还应该说,这些结果受到阈值设置的影响;提高或降低阈值,您可以决定在 under_sampled_only_train 数据帧中包含更多或更少的观察值,并可能获得不同的性能。
阈值应该根据您的数据分布来设置——尝试不同的值,看看会发生什么!
问题 3)对欠采样查询进行分组
让我们看看另一个实验的结果。
如上所述,在我们的数据集中,有 4 个查询级别的特性(区域、日期、月份和工作日)用于创建查询 Id 散列。欠采样查询存储在名为 under_sampled_only_train 的数据帧中,并且只出现在训练集中;这个数据帧包含 252423 个观察值和 3333 个不同的查询 id。
为了使这些查询 Id 组更加“密集”,如果我们只给这些观察值分配一个不太详细的查询 Id,会发生什么情况?
我们尝试了两种不同的方法,每次都“放松”欠采样查询的查询 Id:
-
- 将所有具有相同“区域”和“月份”的观察值分组
- 将所有具有相同“区域”的观察分组
然后,我们检查了通过对欠采样查询进行分组来训练模型、让它们保持原样以及丢弃它们之间的差异。我们在同一个测试集上测试了这些模型(它根本不包含欠采样查询!).它们具有相同的特性(包括查询级特性),因此可以进行比较;唯一改变的是欠采样查询的查询 id。
我们如何将新的“query_ID”分组并分配给欠采样查询?
1.基于地区和月份的 query_ID
在under _ sampled _ only _ train数据帧中,我们有 16 个不同的“区域”值和 12 个不同的“月份”值。我们决定将对于“Region”和“Month”特性具有相同值的所有观察分组,为它们分配一个新的查询 Id:然后我们在 under_sampled_only_train 中获得 144 个唯一的查询 Id(而不是 3333 个):
从表中可以看到,对于所有“Region”值等于“ it ”且“Month”值等于“ 1 ”的观察值(即 1793),查询 Id 2226 已被赋值;而查询 Id 2227 已经被分配给具有等于“ it ”的“区域”值和等于“ 2 ”的“月份”值的所有观察(即 1408),等等。
2.仅基于区域的 query_ID
【T8
在under _ sampled _ only _ train数据帧中,我们有 16 个不同的“区域”值。我们决定将对“Region”特性具有相同值的所有观察值分组,为它们分配一个新的查询 Id:然后我们在 under_sampled_only_train (i
从表中可以看到,对于所有“Region”值等于“ it 的观测值(即 16799),查询 Id 2226 已经被赋值;而查询 Id 2227 已经被分配给具有等于“ nz ”的“区域”值的所有观察(即 12923),等等。
注意:为了避免重复和/或错误,新的查询 id 值不是随机分配给组的,而是我们使用了以下方法:
unique_query_id = under_sampled_only_train['query_ID'].unique().tolist()
unique_region = under_sampled_only_train['Region'].unique().tolist()
column_names = under_sampled_only_train.columns
under_sampled_only_train_grouped = pd.DataFrame(columns=column_names, dtype=int)
i = unique_query_id[0]
for value in unique_region:
new_dataframe = under_sampled_only_train[under_sampled_only_train['Region'] == value]
new_dataframe['query_ID'] = i
under_sampled_only_train_grouped = pd.concat([under_sampled_only_train_grouped, new_dataframe],
ignore_index=True, sort=False)
i = i + 1
我们已经从两个列表中的 under_sampled_only_train 数据帧中获得了唯一的“区域”值和唯一的“查询 Id”值,然后将前 16 个唯一的查询 ID 值作为新的“查询 ID”分配给每个特定的“区域”组。
这种选择是为了对仍然具有部分共同用户请求的行进行分组(即区域或区域和月份),从而创建具有比以前更通用的查询和更多观察的组。
这里的模特训练表演(表 5):
第一个和第四个“场景”与问题 2 中描述的相同,而第二个和第三个是在对训练集中的欠采样查询进行分组之后获得的结果。结果或多或少是相同的,但似乎保留或删除欠采样查询比将它们分组到一个不太细粒度的查询 Id 中要好。
然而,如果我们决定对这些观察进行分组,以使查询 Id 的列表更加“填充”,那么最好使用尽可能多的查询级特性,这可能会带来一些小的改进。
最终考虑
-
- 如果可能的话,最好是在数据集中保留所有查询级别的特征,尽管它们已经用于创建查询 Id。即使上面没有报道,这一点在问题 2 和问题 3 的实施过程中也得到了验证,我们在保持它们的时候总是得到最好的模型性能。
- 如果您有可靠的数据,并且确信您已经正确处理了可能导致 NDCG 暴涨的情况,您可以尝试将欠采样查询保留在训练集中,看看它是否有所改进。记得根据您的数据分布设置阈值!
- 最好尽可能准确地使用查询 id 来训练模型;如果您的查询只有很少的观察值,那么您可以通过稍微“放宽”查询 Id 的粒度来尝试对它们进行分组,从而包含更少的查询级特性。
// our service
不要脸的塞给我们培训和服务!
我提到过我们做学习排名和搜索相关性培训吗?
我们还提供关于这些主题的咨询,如果您想让您的搜索引擎更上一层楼,请联系!
// STAY ALWAYS UP TO DATE
订阅我们的时事通讯
你喜欢这个帖子吗?这个帖子是关于一个关于每日歌曲排名问题的学习排名项目。不要忘记订阅我们的时事通讯,以便随时了解信息检索世界的最新动态!
一个关于每日歌曲排序问题的学习排序项目
介绍
排名数据自然会出现在各种各样的情况下,尤其是在现代数据处理应用中(搜索引擎、推荐系统)。因此,在现实世界中,理解如何适应特定数据集并设计管道来解决排名问题至关重要。
这篇博客旨在说明如何设置和构建一个从可用数据开始的学习排名 (LTR)系统,创建和操作训练集和测试集,然后使用开源库训练一个排名模型。
T3T5
LTR 是机器学习在信息检索中的应用,其中训练数据由定义为查询-文档对的项目列表组成,相关等级与之相关联。应用 LRT 的关键挑战是根据给定的查询得出这些项目的最优排序。
问题陈述
这个内部项目始于 9 月份举行的 Sease 公司会议,是机器学习多学科黑客马拉松的一部分。Sease 的黑客马拉松的目标是将 Spotify 的全球每日歌曲排名数据集【8】用于 LTR 任务。
该数据集是关于 Spotify 用户在 53 个国家收听最多的 200 首歌曲的每日排名;它的时间跨度为 2017 年 1 月 1 日至 2018 年 1 月 9 日,包含超过 3 百万行、6629 位艺术家和 18598 首歌曲,总计数为 150 亿个流计数。下图显示了所有可用的数据集要素。挑战是修改 Spotify 数据集的结构,它最初不是为 LTR 任务创建的,以便获得具有估计相关性判断的查询-文档对。
我们的方法
在探索数据集的过程中,我们开始集思广益,理解哪些特征可能会或可能不会影响文档(歌曲)的相关性。我们从区分功能级别开始这个阶段:
-
- 单据级:描述单据的一个属性,其值只取决于单据实例;
- 查询级别:描述查询的一个属性,其值只取决于查询实例;
- 查询依赖:描述了查询与文档相关的一个属性,其值依赖于查询和文档实例。
下表详细列出了各项功能。
让我们建立一个学习排序实验
学习排名将机器学习与搜索引擎联系起来。在我们的假设场景中,我们假设 Spotify 的搜索引擎中部署了一个经过训练的排名模型,其中:
-
- 文档是歌曲、
- 查询是对地区/国家之一的搜索,
- 根据图表上的位置来估计相关性评级,
- 特征向量由所有其他 N 个给定特征组成。
【T8
Spotify 的搜索引擎在用户发出查询时输入“地区”(国家),然后用传统的 IR 推断出该查询的最佳结果,并将它们传递给重新排名模型。重新排序模型将在将最佳结果返回给用户之前对它们进行重新排序。换句话说,该模型能够基于歌曲与查询的相关性返回歌曲的重新排序(即,在我们的情况下,与该国家最相关的歌曲)。
数据预处理
数据预处理是流水线中最重要的步骤;它是一种数据挖掘技术,用于将原始数据转换为算法可以解释和解析的有用且合适的格式。
我们的数据预处理管道从数据清理阶段开始。
数据清洗是确保数据正确、一致、可用的过程,去除和操纵我们收集的真实世界数据中那些不相关和缺失的部分。我们在 Spotify 数据集中应用了这一技术,检查其完整性,在曲目名称和艺术家特征中发现了总共 657 个 NaN(不是数字)。
由于歌曲和相关艺术家在我们的数据集中重复出现(在不同日期的歌曲排行榜上),并且每个歌曲艺术家对都由 ID 唯一标识(即编码为枚举类型的 URL 功能),所以字典的创建帮助我们管理和填充这些缺失的值。字典由一组键值对组成;我们使用歌曲的单义 ID 作为键,曲目名称作为值。每当我们在曲目名称中发现一个丢失的值时,我们在字典键中检查相同的歌曲艺术家 ID,并用相应的字典值填充丢失的值。用同样的方法填充 Artist 列中包含缺失值的所有行。
此外,要素工程对于准备适当的输入数据集、选择最重要的要素、转换它们和/或从现有要素创建新要素至关重要。特别是,分类特征通常存储为文本值,需要用数字编码,因为大多数机器学习算法无法处理它们。因此,对于如何处理这个问题没有唯一的答案,我们面临的挑战是找出如何将这些文本属性转换成合适的数值。
让我们来详细了解一下这些特性的结构,以及我们决定用来解决这个问题的方法:
-
- 位置:宋在排行榜上的位置——相关度
此功能用于评估相关性评级,即我们的目标变量。使用位置值作为相关性标签是不合适的,它们的取值范围实际上是从 1 到 200,而且可能太宽,会误导模型。
因此,我们决定使用一种称为数据宁滨的量化形式对位置值进行分组,这是一种用于将数字分组为更少“箱”的数据预处理技术。我们测试了两种不同的变化,从 0 到 10 的相关性标签和从 0 到 20 的相关性标签,然后检查模型性能如何变化。在做宁滨数据时,我们试图在最相关的歌曲(那些在图表最高位置的歌曲)的分组中保持更好的粒度。下图是数据宁滨的一个示例。这里我们在所谓的排名栏中生成从 0 到 10 的相关性标签。
-
- 曲目名称:歌曲名称-文档级特征
我们决定使用两种不同的方法来编码这个变量:哈希编码和 doc 2 vec;然后基于模型的输出对这两种技术进行了比较。
特征散列将分类特征轨迹名称中的每个类别映射到预定范围内的整数。为了实现这个方法,我们使用了【1】库。
我们指定要编码的列、特征向量的维数(默认情况下等于 8)和要应用的散列函数(默认情况下为' md5 '算法)作为函数的主要参数。在编码之后,轨道名称特征已经被从 col_0 到 col _ 7[索引 0 到 7]的 8 个新的附加列所取代,这些列包含二进制值 1 或 0。
**
Doc2Vec 是一种无监督算法,将文档/句子表示为向量【2】。它改编自 Word2Vec,这是自然语言处理中最流行的技术之一,应用于整个文档,而不是单个单词。它使用深度学习和基于神经网络的技术来计算语料库中每个文档/句子的特征向量。Doc2Vec 的逻辑是,单词的含义也依赖于它出现的文档;为此,我们需要为每个句子(或段落)指定一个标签(即唯一的文档 id,名为段落 ID )。
主要步骤
-
- 预处理“轨道名称”特征字符串,转换成小写,仅保留字母数字和空格,并删除标点符号;
- 将每个句子(歌名)替换为其单词列表;
- 将这些句子转换成一个训练语料库 TaggedDocument ,由两部分组成:单词列表和相关标签。为了简单起见,我们只使用歌曲标题的索引作为标签,给每个句子分配一个唯一的整数 id。
-
- 创建词汇表,这是从训练语料库中提取的所有独特单词的列表
- 将带标签的文档作为输入传递给 Doc2Vec 函数。该函数中指定的其他参数有:
- dm :定义训练算法(默认 dm =1,为 分布式内存版本的段落向量(PV-DM));
- vector_size :定义特征向量的维数(默认为 100);
- min_count :忽略所有总频率低于设定值的单词。
- 使用 docvecs 属性[model . doc vecs . doc tag _ syn 0]获取训练期间看到的“文档标签”的所有训练向量。曲目名称功能已被 100 个新的附加编码列所取代,如下图所示。
此外,我们假设这个变量也可以用来提取歌曲的语言,这可以用作一个附加的特征。有几个库可以处理这项任务;我们尝试了 langdetect 和 guess_language-spirit ,这两个 Python 库没有限制,但是准确性很差。另外,像 TextBlob 和 Googletrans 这样的库具有很高的准确性,但日常请求有限,因为它们使用谷歌的 API 来检测语言。此外,我们不得不说,歌曲的名称往往太短,以至于无法进行良好的语言评估。此外,它可以不包含字母,而仅包含数字和/或符号。如果一首歌的歌名是‘百分百’,那是什么语言?说不出来吧?在这种情况下,我们应该添加一个 try/except 代码块来捕获并正确处理这种情况。
-
- 艺人:歌手或组合名称—文档级特征
T13T15
由于艺术家是一个具有许多级别的分类变量,因此可以使用留一技术【3】对其进行编码;它非常类似于目标编码,即使用与该级别相关的目标变量值的平均值替换分类值的过程,但在这种情况下,在计算平均值时,它会排除当前行的目标值(见下图)。
另一种方法可能是使用 One-Hot 编码技术,用于将分类值转换为一维数值向量。结果向量由 N 个元素组成,每个元素对应于分类特征的一个级别。除了指定当前类别的元素上的单个(热)1 之外,vector 将全部为 0。这种方法只是根据分类特征中唯一值的数量(N)创建附加(二进制)特征。很明显,对于包含 6629 个级别(即 6629 个艺术家)的变量,这种技术不是一个很好的选择,因为向数据集添加了大量的维度,这会导致并行性和多重共线性的问题。
-
- 流:流的数量-文档级特征
它可以保持原样,因为已经是数字了。
-
- 网址:歌曲的 Spotify 网址
URL 功能被编码为枚举类型(ID ),并用于数据预处理部分(处理缺失值),但随后被从训练集数据中移除。当我们考虑文档级的特性时,我们需要理解一个特性,不管查询是什么,是否会对文档的相关性产生影响。在我们的例子中,因为一个唯一的 URL 对应于一个唯一的歌曲-艺术家对,所以这个数据已经由曲目名称和艺术家表示,并且它不会向查询-文档对的重要性估计添加任何额外的有用信息。
-
- 日期:图表日期—查询级功能
【T12
日期可以分成多列,分别描述日、月和年,并用于提取工作日等附加特征(0 到 6 表示周一到周日)。尤其是月份和星期几,对于理解事件的周期性和季节性非常有用(例如夏天或圣诞节歌曲)。
-
- 地区:国家代码-查询
我们选择这个特性作为(搜索引擎的)查询,并通过 pandas factorize() 函数【4】操纵它,该函数将每个字符串与一个整数唯一地链接起来
模特培训
当数据集准备就绪且结构良好时,必须执行训练测试分割程序。机器学习算法使用训练集来训练模型,而测试数据集被保留并用于评估模型的性能。在我们的实验中,我们将 80%的数据集放在训练集中,其余 20%放在测试集中。我们已经决定使用一个最广泛使用的开源库来实现 LTR,这个库叫做XGBoost【5】,它是一个优化的分布式梯度增强库,旨在高效、灵活和可移植。
它在 Python 或其他编程语言可用的梯度推进框架下实现机器学习算法。它支持成对方式和列表方式。
然后,我们将相关性标签列、查询列和训练向量分离为三个不同的组件,以创建 XGBoost 矩阵,这是用 XGBoost 训练模型所需的结构。
有了训练集,我们使用 MART 方法训练模型,MART 方法是λrank 和 MART 算法的组合。
如果查看 XGBoost 文档【6】可以看到有三种排名方法;它们都使用 LambdaMART 算法,但目标函数不同。我们选择了' rank:ndcg '来执行列表排序,它直接查看整个文档列表(针对每个查询),并尝试为它提供最佳排序。
为了测量模型的输出,我们使用了一个称为归一化贴现累积收益(NDCG) 的指标,即 DCG 的归一化变量。当使用多级相关性时,这种度量通常是优选的。 DCG 旨在评估该模型是否能够在搜索结果列表的最高位置返回最相关的文档,在最低位置返回最不相关的文档(降序)。为了解释这个指标,我们只想知道我们离最大可实现的 DCG 有多近。将 DCG 值除以可能的最佳得分(理想的 DCG),我们获得 0 到 1 之间的归一化值,其中 1 表示可能的最佳排名。
特别是,我们使用了一个 ndcg@10 ,其中“@10”表示该指标仅针对搜索结果列表中的前 10 个项目/歌曲进行评估。然后,最终的评估度量是所有查询的平均值:可以对所有查询的 NDCG 值进行平均,以获得搜索引擎的排名算法在许多查询上的平均性能的度量。
常见错误
当我们创建训练集和测试集时,我们必须确保:
-
- 我们使用的是一组真实的样本;
- 对于每个查询,我们在训练集和测试集中有这些样本的平衡。
特别是,我们必须注意两种可能导致 NDCG 平均指数飙升的情况:
-
- 每个查询组一个样本:例如,如果对于给定的查询只有一个项目/歌曲,则 NDCG 将是 1(完美的),并且不管型号如何,平均值将以误导的方式增加;
- 一个相关性标签用于查询组中的所有样本:例如,如果给定查询的所有歌曲具有相同的相关性(在图表上的相同位置)。
【T6
幸运的是,我们没有这种情况,否则,最好删除这些查询。
结果
我们在下表中总结了训练模型后获得的 ndcg@10 值。实际上,我们创建了 4 个模型,使用不同的编码技术(用于曲目名称特征)和不同的方法对位置值进行分组以生成相关性标签。我们应用 doc2vec 编码并使用从 0 到 20 的相关性标签的模型实现了最高的 ndcg@10 值,因此具有最佳性能。
最终考虑
数据预处理和特征工程是至关重要的,也是使 LTR 项目适应真实场景的最重要的步骤。
如果我们有歌词可用,语言检测会更容易;将该语言作为一个额外的特性添加进来,对于提高模型的性能是很有用的。我们还可以使用这个特性创建一个查询文档级的特性(依赖于查询),并检查国家(代表我们的查询)和歌曲语言之间的相关性。
模型之间的性能差异微乎其微,在线评估模型(我们在我们的在线测试博客帖子中解释了如何做到这一点)以查看它们是否真的像它们看起来那样非常相似,或者离线评估是否无法识别这种差异,这将是非常有趣的。
未来作品
-
- 在将我们的数据集采样到前 20 首歌曲图表(而不是 200 首)之后,如果我们从歌曲位置值估计相关性标签,会发生什么?
-
- 此外,如果我们直接使用流计数(降序)对结果进行排序,可能会发生什么变化?我们会在搜索结果列表中得到相同的顺序吗(因此最大 NDCG)?!
我们将在以后的博客中讨论这些话题。
更多链接,了解我们在第六届伦敦信息检索会议上对该项目的讨论:
幻灯片:https://www2 . slide share . net/SeaseLtd/a-learning-to-rank-project-on-a-daily-song-ranking-problem
// our service
不要脸的塞给我们培训和服务!
我提到过我们做学习排名和搜索相关性培训吗?
我们也提供这些主题的咨询,如果你想让你的搜索引擎更上一层楼,请联系!
// STAY ALWAYS UP TO DATE
订阅我们的时事通讯
你喜欢这篇关于每日歌曲排名问题的学习排名项目的文章吗?不要忘记订阅我们的时事通讯,以便随时了解信息检索世界的最新动态!**
Apache Lucene BlendedInfixSuggester:工作原理、缺陷和改进
Apache Lucene/Solr 建议对 Sease 很重要:我们在过去探索过这个主题,我们坚信自动完成功能对许多搜索应用程序来说是至关重要的。
这篇博文详细探讨了 Lucene BlendedInfixSuggester 的当前状态、最新版本的一些错误(附带解决方案)以及一些可能的改进。
BlendedInfixSuggester
BlendedInfixSuggester 是 AnalyzingInfixSuggester 的一个扩展,增加了在匹配的文档中对查询的前缀匹配进行加权的功能。
如果点击越接近建议的开始,得分越高。
注意:在当前阶段,只有查询中的第一项会影响建议得分
Let’s see some of the configuration parameters from the official wiki:
-
-
blenderType :使用第一个匹配字的**位置计算位置权重系数。可以是下列值之一:
- position _ linear:weightFieldValue *(1–0.10 * position):匹配到起点将被给予更高的分数(默认值)
- position _ reciprocal:weightFieldValue/(1+position):匹配到起点将被给予比线性衰减更快的分数
- position _ exponential _ reciprocal:weightFieldValue/pow(1+position,指数):匹配到起点默认 2.0。**
-
| 描述 |
| 数据结构 | 辅助 Lucene 指数 |
| 建筑 | 对于每个文档,根据suggestAnalyzerFieldType对存储的内容进行分析,然后再对 EdgeNgram 令牌进行过滤。最后,使用这些标记构建一个辅助索引。 |
| 查找策略 | 根据 suggestAnalyzerFieldType 分析查询。然后,针对辅助 Lucene 索引触发短语搜索,从字段内容中每个标记的开头开始识别建议。 |
| 建议返回 | 字段的全部内容。 |
这种建议器现在非常普遍,因为它允许在字段内容中间提供建议,利用了字段提供的分析链。通过这种方式,可以提供考虑到同义词、停用词、词干和分析中使用的任何其他标记过滤器的建议,并基于内部标记匹配建议。最后,根据职位匹配情况对建议进行评分。示例的简单文档集如下:
[
{
"id":"44",
"title":"Video gaming: the history"},
{
"id":"11",
"title":"Nowadays Video games are a phenomenal economic business"},
{
"id":"55",
"title":"The new generation of PC and Console Video games"},
{
"id":"33",
"title":"Video games: multiplayer gaming"}]
以及一个简单的同义词映射:多人在线
让我们看一些例子:
| 自动完成的查询 | 建议 | 说明 |
| 《博彩》 |
- 【视频 gami ng:历史】
- “视频游戏:多人游戏”
- “如今视频游戏是一项惊人的经济业务”…
| 对输入查询进行分析,产生的令牌如下:“game”。在辅助索引中,对于每个字段内容,我们有 EdgeNgram 标记:“v”、“vi”、“vid”…、“g”、“ga”、“gam”、“T28”、“game”。所以匹配发生了,建议被返回。注意:前两个建议的排名较高,因为匹配的术语碰巧更接近建议的开头 |
让我们看看给定不同搅拌机类型的每个建议的得分:
| 查询 | 游戏 |
| 建议 | 首次位置匹配 | 位置线性 | 位置倒数 | 位置指数倒数 |
| gami ng:历史 | 1 | 1-0.1position = 0.9 | 1/(1+位置)= 1/2 = 0.5 | 1/(1+position)^2 = 1/4 =0.25t49】 |
| 视频游戏:多人游戏 | 1 | 1-0.1position = 0.9 | 1/(1+位置)= 1/2 =0.5 | 1/(1+位置)= 1/4 = 0.25 |
| 如今,视频游戏是一项惊人的经济业务 | 2 | 1-0.1*position = 0.8 | 1/(1+位置)= 1/3 = 0.3 | 1/(1+位置)= 1/9 =0.1 |
建议的最终得分为:
long score = (long) (weight * coefficient)
注意我强调数据类型的原因是它直接影响我们讨论的第一个 bug。
建议分数近似值
可选的 weightField 参数对于混合中缀建议器非常重要。
分配建议权重的值(从上述字段中提取)。
例如 建议可能来自产品名称字段,但建议权重取决于所建议产品的利润。
到目前为止,一切顺利,但不幸的是,这有两个问题。
错误 1 -未定义 WeightField>零建议分数
如何重现:不要在建议者配置
中定义任何 weight field效果:建议排名丢失,所有建议都是 0 分,匹配的位置不再重要
weight field 不是 BlendedInfixSuggester 的强制配置。
您的用例不能包含您的建议的任何权重,您只对位置评分感兴趣(BlendedInfixSuggester 首先存在的主要原因)。
不幸的是,目前这是不可能的:
如果没有定义 weight 字段,每个建议的权重将为 0 。
这是因为与文档字典中的每个文档相关联的权重很长。如果要从中提取权重的字段未定义(null),则返回的权重将仅为 0。
这不允许区分权重何时应该为 0(从字段中提取的值)或空(根本没有值)。
这里提出了一个解决方案【3】。
Bug 2 -小权重的建议分数近似值不佳
在建议的分数计算中存在误导性的数据类型转换:
long score = (long) (weight * coefficient)
如果与建议相关联的权重是单一的或足够小,这种看似无害的转换实际上会带来非常恶劣的影响。
权重 =1
视频 gami ng:历史
1-0.1 *位置=0.9 * 1 = cast = 0 1/(1+位置)= 1/2 =0.5 * 1 = cast = 0 1/(1+position)^2 = 1/4 =0.25 * 1 = cast = 0
权重 =2
视频 gami ng:历史
1-0.1 *位置=0.9 * 2 = cast = 1 1/(1+位置)= 1/2 =0.5 * 2 = cast = 1 1/(1+position)^2 = 1/4 =0.25 * 2 = cast = 0
基本上你冒着失去你的建议排名的风险,把分数降低到只有几个可能的值:0 或 1(在边缘情况下)
这里提出了一个解决方案【3】
多项匹配处理
在自动完成查询中有多个术语是很常见的,因此您的建议者应该能够相应地管理建议中的多个匹配。
给定一个简单的语料库(仅由以下建议组成)和查询:
【Mini Bar Frid】
你会看到这些建议:
-
- 1000 |迷你吧什么的 Frid ge
- 1000 |迷你吧别的东西 Frid ge
- 1000 |迷你吧冰箱什么的
- 1000 |迷你吧冰箱别的
- 1000 |迷你有事吧友葛
这是因为此时,第一个匹配项赢得所有(其他位置被忽略)。这带来了许多可能的联系(1000),应该打破给用户一个好的和直观的排名。
但是凭直觉,我希望得到类似这样的结果(注意 allTermsRequired=true,模式权重字段总是返回 1000)
-
- 迷你吧 Frid 葛什么的
- 迷你酒吧的朋友们,来点别的吧
- 迷你酒吧有事 Frid 葛
- 迷你酒吧别的东西 Frid ge
- 迷你有事吧 Frid 葛
让我们来看一个提议的解决方案【4】:
位置系数
除了只考虑建议中的第一个术语位置,还可以使用匹配术语[“mini”、“bar”、“冰箱”]中的所有匹配位置。
每个位置的匹配都会影响得分:
-
- 匹配的术语位置离理想的位置匹配有多远
- 查询:迷你吧 Fri,理想位置:【0,1, 2
- 建议 1 : 迷你吧有事 Fri dge,匹配位置:【0,1, 3 ]
- 建议 2 : 迷你吧别的 Fri dge、匹配位置:【0,1, 4 ]
- 建议 2 将被扣分为“Fri”比赛发生时距离理想位置4>32
- 错误位置发生得越早,罚分越重
- 查询:迷你吧 Fri,理想位置 : [0,1,2]
- 建议 1 : 迷你吧有事 Fri dge、匹配位置:【0,1, 3 ]
- 建议二 : 迷你有事吧Fridge、匹配职位:【0、 2 、 3
- 建议 2 将被额外扣分,因为位置 栏 中的第一个不匹配更接近建议 的开始
- 匹配的术语位置离理想的位置匹配有多远
仅考虑折扣位置证明有用:
Query1 :酒吧点点
Query2 :点点
建议:迷你酒吧点点冰箱
查询 1 建议匹配项位置:【1,2】
查询 2 建议匹配项位置:【2】
如果我们比较这两个查询的建议分数,仅仅因为第一个查询匹配 2 个词(连续的)而第二个查询只有一个匹配(比查询 1 中的第一个匹配位置更差)就惩罚第一个查询似乎是不公平的
引入这种先进的位置系数演算有助于改进所创建的实验测试的整体行为。
获得的结果很有希望:
查询:小酒吧 Fri
100 | 小酒吧 Fridge something
100 |小酒吧 Fridge something
100 |小酒吧 Fridge a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a
26 |小酒吧 something Fri dge 迷你酒吧其他东西 Fri dge
17 | 迷你其他东西酒吧 Fridge
8 |其他东西迷你酒吧冰箱
7 |其他东西迷你酒吧冰箱
精确的前缀匹配仍然有差距,但是让我们看看我们是否也能完成改进。
令牌计数系数
让我们来关注一下我们刚刚看到的前三个排名建议:
查询:迷你吧 Fri
100 | 迷你吧 Fridge something
100|迷你吧 Fridge something
100|迷你吧 Fridge a a a a a a a a a a a a a a a a a a a a a a a a a a
直觉上,我们希望这个订单能打破这种束缚。
匹配项的数量与建议项的总数越接近越好。
理想情况下,我们希望我们的最高评分建议尽可能只包含匹配的术语。我们也不想给其他建议带来强烈的不一致,我们应该只影响纽带。
这可以通过计算附加系数来实现,取决于术语计数:
令牌计数系数 =匹配术语计数/总术语计数
然后我们可以相应地调整这个值:
最终分数的 90%将来自位置系数
最终分数的 10%将来自令牌计数系数
查询:迷你吧 Fri
90 * 1.0+10 * 3/4 =97|迷你吧 Fridge something
90 * 1.0+10 * 3/5 =96|迷你吧 Fridge something
90 * 1.0+10 * 3/25 =【t8
这将需要一些额外的调整,但总的想法应该是当涉及多个术语匹配时,给 BlendedInfix 带来更好的排名功能!
如果你有任何建议,欢迎在下面留言!
代码可以在 Lucene 吉拉发布【4】附带的 Github Pull 请求中找到。
// our service
不要脸的塞给我们培训和服务!
我提到过我们做 Apache Solr 初学者和 Elasticsearch 初学者培训吗?
我们还提供关于这些主题的咨询,如果您想让您的搜索引擎更上一层楼,请联系!
// STAY ALWAYS UP TO DATE
订阅我们的时事通讯
你喜欢这篇关于 Apache Lucene BlendedInfixSuggester 的文章吗?不要忘记订阅我们的时事通讯,以便随时了解信息检索世界的最新动态!
Apache Lucene/Solr 提交者!
原文:https://web.archive.org/web/sease.io/2020/03/apache-lucene-solr-committer.html
我一直很喜欢 R&D,也一直着迷于看到我的想法付诸实践。2010 年 7 月,我开始了我在开源搜索领域的职业旅程,当时我是一名初级软件工程师,在 Roma3 大学进行了几个月的研究生研究后,我非常好奇信息检索的理论是如何在行业中实际实施的。在我以前的同事 Tommaso Teofili 的带领下,我接触了 Apache Lucene/Solr 项目,我被开源软件的技术和伦理深深打动了。很快,它成为我的主要爱好和最喜欢的信息检索工具。
在过去的 10 年里,我在欧洲多家公司的搜索软件工程岗位上工作过,我一直专注于开源搜索解决方案。
在 Apache Lucene/Solr 学习和工作丰富了我个人,也支付了我的账单。
所以我决定诚实地回报,我从迷你代码贡献开始,宣传开源软件并支持 Solr 用户邮件列表。随着时间的流逝,我能够帮助彭博和其他人学习如何排列贡献。
此外,我还喜欢邮件列表中来自全球各地的更多支持者,看到人们在信息检索任务中遇到的所有问题,并使用开源软件思考和设计可能的解决方案,这非常有趣。 博客大约在 2015 年出现,我认为传播我在技术博客上学到的东西是一种传播信息的好方法,可以帮助人们解决更多与搜索相关的问题。继续有人投稿,我也开始更多地参与活跃的全球社区,毕竟我在伦敦,一个技术和思想的熔炉,搜索也不例外,我在 Apache Lucene/Solr 伦敦会议上找到了一个有效的盟友。
接下来是许多会议、聚会和讲座,从东京的远东到加利福尼亚的远西,我个人喜欢分享、展示和讨论想法,这是提高自己和帮助他人的好方法!
2016 年,我创办了 Sease 公司,目标是通过开源软件在学术界和工业界之间搭建一座桥梁,我的社区贡献持续稳定,我们与我的团队建立了一个有凝聚力的团队,我们不断提供帮助,2019 年,我们开始了伦敦信息检索会议和大学研讨会(目前在意大利),以吸引学生,让他们对开源信息检索前景有更好的了解。
Apache Lucene/Solr 在过去的 10 年里一直处于核心地位:
我很荣幸地宣布,我现在是 APACHE LUCENE/SOLR 的委员了!
我快乐,平淡而简单🙂
感谢所有在过去几年中支持我的贡献的人,我将尽我所能继续帮助社区和改进项目。
// our service
不要脸的塞给我们培训和服务!
我提到过我们做 Apache Solr 初学者和 Elasticsearch 初学者培训吗?
我们还提供这些主题的咨询,如果您想让您的搜索引擎更上一层楼,请联系!
// STAY ALWAYS UP TO DATE
订阅我们的时事通讯
你喜欢这篇关于 Apache Lucene/Solr 提交者的帖子吗?不要忘记订阅我们的时事通讯,以便随时了解信息检索世界的最新动态!
Apache Solr 原子更新:多态方法
原文:https://web.archive.org/web/sease.io/2020/01/apache-solr-atomic-updates-polymorphic-approach.html
在这篇文章中,我们描述了一种解决应用程序需要完全更新和原子更新问题的方法,使用了面向对象编程中一个强大的概念:多态。
在面向对象编程中,多态性指的是一个变量、方法或对象采取多种形式的能力。
尽管为了提供高水平的视角,已经对示例上下文进行了抽象,但是所描述的方法的实际应用已经在户外搜索服务【1】中实现。
【T6
Alfresco 搜索服务通过利用 Apache Solr 为 Alfresco 内容服务提供搜索功能。企业版和社区版的 Alfresco 内容服务都使用它。
语境
现有代码从传入的数据模型中创建 SolrInputDocument 实例。文档一旦创建,就被发送到 Solr 进行索引。
每个文档代表一个域对象的完整状态:也就是说第一次发送的时候会被插入;下一次发送相同的文件(即具有相同 id 的文件)时,会替换现有文件。
T33
这是系统的核心部分,逻辑相当复杂:在几个地方创建了一个 SolrInputDocument 实例,并传递了许多方法,这些方法用一组特定的属性来丰富它。大概是这样的:
public void indexScenario1(DomainObject o) {
SolrInputDocument doc = new SolrInputDocument();
...
addAttributeSetA(doc, Domain);
addAttributeSetB(doc, Domain);
if (something)
addAttributeSetC(doc, Domain);
else
addAttributeSetD(doc, Domain);
...
挑战
在我们的贡献下,创建领域模型实例的系统部分有了一点改变:主要的改进包括使用“delta”对象的额外能力。换句话说,调用者代码能够向该索引组件提供“完整”或“部分”域对象(即,仅包含已更新内容的域对象)。
限制
到目前为止,你认为这是一个完美的适合使用原子更新!绝对正确:只包含更改位的域对象可以在partialSolrInputDocument实例中转换,然后发送到 Solr 进行索引。
然而,一个第一约束需要被解决:部分对象不会是一个独占场景,我们仍然必须处理完整对象。
第二个约束:如上所述,索引组件代表了系统的中心/关键部分,因此即使是最小的变更也会带来一定程度的风险,因此代码变更应该被最小化。
根据我们的经验,这需要一种“改变越少越好”的方法,而旧的好的面向对象编程在这方面绝对很棒!
什么是原子更新?
原子更新【2】是一种使用“更新”语义在客户端执行索引命令的方式,通过应用/索引表示域对象的部分状态的文档。
因此,实际上,使用原子更新,客户端能够只发送“部分”文档,该文档只包含需要应用于现有(即先前索引的)文档的更新。
T25T27
让我们看一个例子。索引以下文档后:
{
"id": 1
"title": "Design Patterns: Elements of Reusable Object-Oriented Software",
"author": [
"Erich Gamma",
"Richard Helm",
"Ralph Jonson"
]
}
你意识到“拉尔夫·约翰逊”中少了一个“h”(啊!误认这样一个古鲁的名字:不可接受!);另外,你忘了约翰·弗利塞斯……真是一场灾难!
所以你可以做下面两件事之一。
通常的方法是重新创建整个文档而不出错,并将其重新发送给 Solr:
{
"id":1
"title":"Design Patterns: Elements of Reusable Object-Oriented Software",
"author":[
"Erich Gamma",
"Richard Helm",
"Ralph Johnson",
"John Vlissides"
]
}
那个新文档完全用替换了索引的文档(注意:隐含的假设是 uniqueId 字段是“Id”)。
另一种方式允许我们只发送我们想在现有文档上更改的内容。在这种情况下,我们将向 Solr 发送这样一个文档:
{
"id": 1
"author": {
"remove": "Ralph Jonson",
"add": ["Ralph Johnson", "John Vlissides"]
}
}
它将把 id=1 的索引文档作为目标
- 它去掉 s 错误的值(“拉尔夫·琼森”)
- 它为作者添加了正确的值(“拉尔夫·乔”
- 这是另一个失踪的作者
如你所见,需要更新的字段的值不再是文字 值(如字符串、整数)或值列表;相反,我们有一个映射图,其中键是我们想要应用的更新命令(例如,删除、添加、设置),而值是我们想要用于更新的一个或多个文字值。****
关于 AtomicUpdates 的完整语义的更多信息可以在 Apache Solr 参考指南 [2]中找到:这里需要记住的是 Solr 方面,在幕后没有发生“真正的”部分更新:旧版本的文档是获取的,它是合并了和部分状态;之后,新的“完整”结果文档被再次索引。**
**尽管如此,它还是非常有益的,因为当你需要更新文档时,它减少了你可能转移到 Solr 的数据量。
在 Java 中,特别是在 SolrJ 中, SolrInputDocument 类表示我们发送给 Solr 用于索引的数据。这基本上是一个映射,所以我们可以添加,设置或删除字段和值。
我们对以下三种方法感兴趣:**** ```
// If a field with that name doesn’t exist it adds a new entry with the
// corresponding value, otherwise the value is collected together with
// the existing value(s)
// This is typically used on multivalued fields (i.e. calling twice this
// method on the same field, will collect 2 xvalues for that field)
addField(String name, Object value)
// Sets/Replaces a field value
setField(String name, Object value)
// Remove a field from the document
removeField(String name, Object value)
**同类**也用于表示**部分文档**。您可以通过在 *setField* 或 *addField* 方法中设置 map as 值来实现。贴图可以有一个或多个修改器:
* * **“add”**:将指定值添加到多值字段中。
* **“删除”**:从多值字段中删除指定值的所有匹配项。
* **"set"** :用指定的值设置或替换字段值,如果新值指定为' null '或空列表,则删除这些值。
注意还有两个额外的修饰符(inc,removeregex ),但是我们在这个上下文中对它们不感兴趣。
## 这个想法
记住我们上面的约束:
* * 现有代码总是做**全文档更新**
* 在调用者端已经实现了一个变化:**传入的域对象**将会是**完全的或者部分的**,这取决于用例
* Solr 文档实例价值化**跨越了许多方法**。创建一个 *SolrInputDocument* 实例,然后传递给几个设置文档状态的方法。
* 我们需要**部分更新**,但它们**不会是唯一的场景**:在某些情况下,我们仍然有**完全更新**
在 Java 中实现到目前为止描述的部分更新机制要求方法 *addField* 、 *setField* 或 *removeField* 知道它们的执行上下文(部分或完全更新)。因为在完全更新的情况下,添加一个新的作者会很简单
doc.addField(“author”, “Ralph Johnson”);
而在**部分更新**时,有必要考虑**第一次**发生添加时的差异:
List
authors.add(“Ralph Johnson”);
doc.addField(“author”, new HashMap() {{ “add”, authors}};
从随后的时间:
Map<String, Object> fieldModifier =
(Map<String,Object>)doc.getFieldValue(“author);
List
authors.add(“John Vlissides”);
上面的逻辑(可以写得更好)需要为每个添加/设置/删除调用的字段完成!有没有更好的处理方法?是的,当然:
T12
创建 *SolrInputDocument* 的子类:
public class PartialSolrInputDocument extends SolrInputDocument {
static Function<String, List