CategoryResourceRepost/极客时间专栏/深度学习推荐系统实战/模型评估篇/24 | 离线评估:常用的推荐系统离线评估方法有哪些?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

120 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="24 | 离线评估:常用的推荐系统离线评估方法有哪些?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f1/df/f15b259d4e853yyd741924d8d00b39df.mp3"></audio>
你好,我是王喆。今天我们要进入一个全新的章节,模型评估篇。
在推荐系统这个行业,所有人都在谈效果。就像我们在学习推荐模型篇的时候,你肯定也有过这样的疑问:
- DIEN这个模型的效果到底怎么样啊
- 我们用深度学习来构建模型到底能让推荐系统效果提高多少啊?
- DeepFM的效果是不是会比Wide&amp;Deep好呢
**那这个所谓的“效果”到底指的是什么呢?我们一般用什么方法来衡量这个“效果”呢?我们又应该如何根据效果评估的结果来更新模型呢?**这就是模型评估篇要解决的问题。
在所有推荐系统的评估方法中,离线评估是最常用、最基本的。顾名思义,“离线评估”就是我们将模型部署于线上环境之前,在离线环境下进行的评估。由于不用部署到生产环境,“离线评估”没有线上部署的工程风险,也不会浪费宝贵的线上流量资源,而且具有测试时间短,可多组并行,以及能够利用丰富的线下计算资源等诸多优点。
因此在模型上线之前进行大量的离线评估是验证模型效果最高效的手段。这节课我们就来讲讲离线评估的主要方法以及怎么在Spark平台上实现离线评估。
## 离线评估的主要方法
离线评估的基本原理是在离线环境下将数据集分为“训练集”和“测试集”两部分“训练集”用来训练模型“测试集”用于评估模型。但是如何划分测试集和训练集其实这里面有很多学问。我总结了一下常用的离线评估方法主要有五种分别是Holdout检验、交叉检验、自助法、时间切割、离线Replay。接下来我们一一来看。
### Holdout检验、交叉检验和自助法
**首先我们来看Holdout检验。** Holdout 检验是最基础,最常用的离线评估方法,它将原始的样本集合随机划分为训练集和测试集两部分,**所以Holdout 检验的关键词就是“随机”**。举例来说,对于一个推荐模型,我们可以把样本按照 70%-30% 的比例随机分成两部分。其中70% 的样本用于模型的训练30% 的样本用于模型的评估。
虽然Holdout检验很简单实用但它的缺点也很明显就是评估的结果有一定随机性因为训练集和验证集的划分是随机的所以如果只进行少量的Holdout检验得到的评估指标会存在一定的波动。那为了消除这种随机性我们就要使用“交叉检验”的方法。
为了进行交叉检验我们需要先将全部样本划分成k个大小相等的样本子集然后依次遍历这k个子集每次把当前遍历到的子集作为验证集其余所有的子集作为训练集这样依次进行k次模型的训练和评估。最后我们再将所有k次评估指标的平均值作为最终的评估指标。在我们的实践中k经常取10也就是依次进行10次检验然后取指标均值。
不管是Holdout检验还是交叉检验都是基于划分训练集和测试集的方法进行模型评估的。然而当样本规模比较小时将样本集进行划分会让训练集进一步减小这往往会影响模型的训练效果。那有没有能维持训练集样本规模的验证方法呢
“自助法”就可以在一定程度上解决这个问题。我这里所说的**自助法Bootstrap是基于自助采样的检验方法**它的主要过程是对于总数为n的样本集合我们先进行n次有放回地随机抽样得到大小为n的训练集。在n次采样过程中有的样本会被重复采样有的样本没有被抽出过我们再将这些没有被抽出的样本作为验证集进行模型验证这就是自助法的验证过程。
虽然自主法能够保持训练集的规模,但是它的缺点也很明显,它其实改变了原有数据的分布,有可能让模型产生一定程度的偏差。至于,到底是自助采样增加训练样本规模的收益大,还是数据分布被改变带来的损失大,这就需要我们在实践中进行验证了。
### 时间切割
说完了前三种方法,我们再来看时间切割法。在“[模型实战准备(二)](https://time.geekbang.org/column/article/308812)”那节课里我们曾经讲过一个概念叫“未来信息”。它是说如果我们在t时刻进行模型预测那么 t+1 时刻的信息就是未来信息。在构建特征工程的时候,我们要避免引入“未来信息”。
其实在进行模型评估的时候我们同样不应该在训练集中包含“未来”的样本。怎么理解这句话呢比如我们所有的样本数据分布在t<sub>0</sub>到t<sub>n</sub>这样的时间轴上如果训练样本是通过随机采样得到的那么训练数据也会分布在t<sub>0</sub>到t<sub>n</sub>同样测试数据也会分布在t<sub>0</sub>到t<sub>n</sub>上。
如果你细想这个事情其实是有点反常理的。因为训练模型的时候我们已经使用了t<sub>n</sub>这个时间窗口的数据结果你却用它来预测t<sub>0</sub>的事件,这不是很荒谬吗?这就相当于你有一个时光机,已经穿越到了明天,知道股票会涨,结果你又穿越回来,预测说明天股票会涨,这哪是预测呢?这就是“作弊”。
为了防止这类“信息穿越”导致的模型作弊现象发生我们一般会使用时间切割的方案去划分训练集和测试集它的做法很简单。比如你一共处理了30天的样本从第25天末开始切割前25天的样本作为训练集后5天的样本作为测试集这样我们就从根源上切断了引入“未来信息”的可能。当然切割的比例到底如何也需要根据你的实践来定一般来说我们控制训练集跟测试集的比例在3:1到10:1之间比例太小训练样本不够比例太大测试结果不够稳定。
### 离线Replay
时间切割的方法虽然能避免“信息穿越”但也不是没有缺点的。它的缺点就在于整个评估过程是静态的模型不会随着评估的进行而更新这显然是不符合事实的。就拿我们刚才举的例子来说用前25天的数据做训练集用后5天的数据做测试集。如果在生产环境中模型是日更新的那后5天的评测过程就不准确因为在离线测试中我们并没有在后5天的评测过程中做到日更模型。
那怎么解决这个问题呢?我们也可以在离线状态下对线上更新过程进行仿真,让整个评估过程“动”起来。**业界把这样离线仿真式的评估方式叫做离线Replay。**
下图就是动态的Replay评估法与静态的时间分割评估法的对比示意图。我们可以看到“Replay评估方法”先根据产生时间对测试样本由早到晚地进行排序再让模型根据样本时间的先后进行预测。在模型更新的时间点上模型需要增量学习更新时间点前的测试样本更新模型后再继续评估更新点之后的样本。
<img src="https://static001.geekbang.org/resource/image/4a/fe/4a530c53ef61ca5fee367a58070c32fe.png" alt="" title="图1 静态时间分割评估与动态Replay评估 (出自《深度学习推荐系统》)">
你应该也发现了Replay评估的过程更接近于真实的线上环境因为它在线下还原了模型在线上的更新、预估过程。这也让Replay方法的评估结果更加权威可信毕竟我们最终的目标是让模型在线上产生更好的效果。
当然Replay评估方法也有弊端因为它需要在评估过程中不断更新模型这让评估过程的工程实现难度加大因为包含了模型训练的时间所以整个评估过程的总时长也会加长影响评估和调参的效率。到底是要评估的准确性还是要评估的效率这又是一个需要权衡的问题我们需要根据自己工程上的侧重点进行选择。
## 基于Spark的离线评估方法实践
熟悉了离线环节的主要模型评估方法就又到了实践的环节。其实无论是基于Python的TensorFlow还是基于Scala的Spark都有很多支持离线评估的库这里我们选择了Spark进行实践主要是因为在业界数据集很大的情况下Spark在分布式环境下划分训练集和测试集的效率是最高的。
下面我就来看一下如何使用Spark实现Holdout检验、交叉检验和时间切割评估法。至于另外两种方法由于自助法不太常用离线Replay又涉及过多的附加模块我们暂时就不在项目里实现。
实现Holdout检验的时候我们要清楚如何利用Spark随机划分测试集和训练集。它的关键代码只有下面这一行就是利用randomSplit函数把全量样本samples按比例分割成trainingSamples和testSamples。在Spark的后端这个randomSplit函数会在各个节点分布式执行所以整个执行效率是非常高的。源代码你可以参考 com.wzhe.sparrowrecsys.offline.spark.featureeng.FeatureEngForRecModel 中的splitAndSaveTrainingTestSamples函数。
```
val Array(trainingSamples, testSamples) = samples.randomSplit(Array(0.9, 0.1))
```
实现交叉检验的过程相对比较复杂好在Spark已经提供了交叉检验的接口可以直接使用我们直接看一下这部分的关键代码。
```
val cv = new CrossValidator()
.setEstimator(modelPipeline)
.setEvaluator(new BinaryClassificationEvaluator)
.setEstimatorParamMaps(paramGrid)
.setNumFolds(10) // Use 3+ in practice
val cvModel = cv.fit(training)
```
这段代码中有三个关键参数一是setEstimator这是我们要评估的对象它需要把我们构建的模型pipeline设置进去二是setEvaluator它用来设置评估所用的方法和指标三是setNumFolds它设置的是交叉检验中k的值也就是把样本分成多少份用于交叉检验。本质上Spark的CrossValidator其实是通过交叉检验来选择模型的最优参数但也可以通过模型中cvModel.avgMetrics参数查看模型的评估指标。
接下来我们来实现时间切割方法。既然是要按时间划分如果你知道样本的时间跨度直接用where语句就可以把训练集和测试集划分开了这也是我最推荐的方法因为它最高效不用专门判断切割点。
如果你不知道样本的时间跨度就要按照时间求取样本的分位数。具体来说就是通过Spark的approxQuantile函数我们可以找到划分样本集为8:2的训练集和测试集的时间戳的值。那么接下来我们根据这个值通过where语句划分就可以了。我把这个过程的关键代码贴到了下面供你参考。完整的源代码你可以参考 com.wzhe.sparrowrecsys.offline.spark.featureeng.FeatureEngForRecModel 中的splitAndSaveTrainingTestSamplesByTimeStamp函数。
```
//找到时间切割点
val quantile = smallSamples.stat.approxQuantile(&quot;timestampLong&quot;, Array(0.8), 0.05)
val splitTimestamp = quantile.apply(0)
//切割样本为训练集和测试集
val training = smallSamples.where(col(&quot;timestampLong&quot;) &lt;= splitTimestamp).drop(&quot;timestampLong&quot;)
val test = smallSamples.where(col(&quot;timestampLong&quot;) &gt; splitTimestamp).drop(&quot;timestampLong&quot;)
```
## 小结
这节课我们学习了五种主流的推荐模型离线评估方法它们分别是Holdout检验、交叉检验、自助法、时间切割和离线Replay。
其中Holdout检验最简单常用它通过随机划分的方式把样本集划分成训练集和测试集。而交叉检验的评估效果更加稳定准确它通过划分样本集为k份再进行k次评估取平均的方式得到最终的评估指标。
自助法是为了解决样本量过少而提出的它可以通过有放回采样的方式扩充训练集但有改变数据本身分布的风险。而时间切割法在某个时间点上把样本分成前后两份分别用于模型训练和评估避免引入未来信息。最后是离线Replay它通过仿真线上模型更新过程来进行评估是最接近线上环境的离线评估方法但实现起来比较复杂。
总之,各种评估方法都有优有劣,你需要根据实践中的侧重点选择使用,我把它们的优缺点也总结在了文稿的表格里,方便你进行对比。
<img src="https://static001.geekbang.org/resource/image/19/fd/198bea8ba6ae2ecfe2d65617a6efe7fd.jpeg" alt="">
这节课我们讲了评估模型效果的方法之一,离线评估。但我们并没有具体来讲“效果”的衡量指标到底是什么。别着急,下节课我们就来学习推荐系统主要使用的效果评估指标,也会利用这节课学习到的评估方法来生成这些指标。
## 课后思考
你觉得离线Replay这个方法跟我们之前讲过的增强学习有什么相似之处吗你知道它们两个还有什么更深层次的关系吗
期待在留言区看到你的发现和思考,我们下节课见!