Mistral AI 与 Meta:对比顶级开源 LLM
这是对 Mistral 7B 与 Llama 2 7B、Mixtral 8x7B 与 Llama 2 70B 的比较。
https://medium.com/@luisroque?source=post_page---byline--565c1bc1516e--------------------------------https://towardsdatascience.com/?source=post_page---byline--565c1bc1516e-------------------------------- Luís Roque
·发布于Towards Data Science ·阅读时间:16 分钟·2024 年 1 月 23 日
–
本文由 Rafael Guedes 和我共同撰写。
介绍
在自然语言处理(NLP)的最新发展中,特别是在大型语言模型(LLMs)方面,重点是提升模型性能,这通常会导致模型规模的增大。可以预见的是,模型规模的扩大也增加了计算成本和推理延迟,带来了在实际应用场景中部署和使用 LLM 时的障碍。
Mistral AI 是一家总部位于巴黎的欧洲公司,他们一直在研究如何提高模型性能,同时减少部署 LLM 所需的计算资源,以便能在实际用例中应用。Mistral 7B 是他们开发的最小型 LLM,它将两个创新概念引入到传统的 Transformer 架构中,分别是 Group-Query Attention(GQA)和 Sliding Window Attention(SWA)。这些组件加速了推理速度,并减少了在解码过程中对内存的需求,从而实现更高的吞吐量,并能够处理更长的令牌序列,同时不牺牲生成的响应质量,相比于在基准数据集上的 Llama 2 7B 表现更佳。
Mistral 7B 并不是他们唯一开发的模型,他们还创建了 Mixtral 8x7B 来与更大规模的 LLM(如 Llama 2 70B)竞争。除了使用 GQA 和 SWA 外,这个版本还增加了第三个…
Mistral-NeMo: 通过量化 Minitron 减少 4.1 倍大小
剪枝、知识蒸馏和 4 位量化如何使先进的 AI 模型变得更加易于访问和具备成本效益
https://medium.com/@bnjmn_marie?source=post_page---byline--9d6ad7b70981--------------------------------https://towardsdatascience.com/?source=post_page---byline--9d6ad7b70981-------------------------------- 本杰明·马里
·发表于Towards Data Science ·9 分钟阅读·2024 年 8 月 29 日
–
作者提供的图片 — 制作自Pixabay的插图
NVIDIA 的 Minitron 通过剪枝最不重要的权重来压缩大型语言模型(LLMs),然后通过知识蒸馏进行重新训练。这种方法显著减少了模型的大小,同时保持了其准确性。
NVIDIA 发布了 Llama 3.1 和 Mistral-NeMo 的 Minitron 版本,分别将其参数数量从 8B 减少到 4B,以及从 12B 减少到 8B。
为什么这很重要?
虽然 Mistral-NeMo 无法在消费级 GPU 上运行,但其 Minitron 版本可以。一个 24 GB 的 GPU 就足够了。然而,这也可以通过对 Mistral-NeMo 进行量化来实现。4 位量化方法现在已经足够准确。
但是,如果我们也能对 Minitron 模型进行量化呢?对于一个经过 Minitron 剪枝的模型,量化是否仍然足够准确?
例如,Mistral-NeMo-Minitron 的 4 位版本可以在 8 GB 的 GPU 上运行,显著降低推理成本。
在这篇文章中,我回顾了 Minitron 方法,探讨了如何通过剪枝和知识蒸馏来压缩大型语言模型(LLMs)。我们将…
Mistral 与 Mixtral:比较 7B、8x7B 和 8x22B 大语言模型
在 Google Colab 中运行 7B 和 22B 模型
https://dmitryelj.medium.com/?source=post_page---byline--58ab5b2cc8ee--------------------------------https://towardsdatascience.com/?source=post_page---byline--58ab5b2cc8ee-------------------------------- Dmitrii Eliuseev
·发表于 Towards Data Science ·阅读时间 10 分钟·2024 年 4 月 20 日
–
图片由 Img IX 提供,Unsplash
不久前,所有的 IT 新闻频道都报道了新发布的开源 Mixtral 8x22B 模型,它在 MMLU(大规模多任务语言理解)或 WinoGrande(常识推理)等基准测试中超越了 ChatGPT 3.5。这是开源模型领域的一大成就。学术基准测试自然很有趣,但这个模型在实际操作中表现如何?它的系统要求是什么?与之前的语言模型相比,它真的更好吗?在本文中,我将测试四种不同的模型(7B、8x7B、22B 和 8x22B,带有和不带有“专家混合”架构),我们将一起看到结果。
让我们开始吧!
顺便提一下,我与 Mistral AI 没有任何商业关系,所有的测试都是我个人独立完成的。
稀疏专家混合(SMoE)
在大语言模型(LLM)时代刚开始时,人们就已经知道,通常来说,模型越大,智能越强,知识越丰富,结果也越好。但更大的模型也意味着更高的计算成本。如果一个聊天机器人需要 5 分钟才能回应,没人愿意等下去。“专家混合”(mixture of experts)的直观想法是……
高性能时间序列预测的 KAN 专家混合模型
探索 RMoK 模型及其架构,并使用 Python 进行小规模实验。
https://medium.com/@marcopeixeiro?source=post_page---byline--5227e1d2aba2--------------------------------https://towardsdatascience.com/?source=post_page---byline--5227e1d2aba2-------------------------------- Marco Peixeiro
·发布于Towards Data Science ·10 分钟阅读·2024 年 9 月 11 日
–
Kolmogorov-Arnold 网络(KAN)的引入为深度学习领域做出了重要贡献,因为它代表了多层感知器(MLP)的替代方案。
MLP 当然是许多深度学习模型的构建模块,包括像 N-BEATS、NHiTS 和 TSMixer 这样最先进的预测方法。
然而,在一个使用 KAN、MLP、NHiTS 和 NBEATS 的预测基准测试中,我们发现 KAN 通常非常慢,并且在各种预测任务中表现持续较差。需要注意的是,该基准测试是在 M3 和 M4 数据集上进行的,这些数据集包含超过 99,000 个独特的时间序列,频率范围从每小时到每年。
最终,当时应用 KAN 进行时间序列预测的结果令人失望,并不是一种推荐的做法。
现在,随着可逆 KAN 混合模型(RMoK)的提出,情况发生了变化,相关内容可以在论文中找到:KAN4TSF: KAN 及基于 KAN 的模型对时间序列预测有效吗?
在本文中,我们首先探讨可逆混合 KAN 模型的架构和内部工作原理…
机器学习初学者应该阅读论文
这是为什么以及如何做到的
https://pascaljanetzky.medium.com/?source=post_page---byline--506a074ffc10--------------------------------https://towardsdatascience.com/?source=post_page---byline--506a074ffc10-------------------------------- Pascal Janetzky
·发表于Towards Data Science ·4 分钟阅读·2024 年 12 月 10 日
–
每天,超过 100 篇新的计算机科学和机器学习论文会被列在arXiv上。尽管这些作品在列出之前不一定经过同行评审,但它仍然是一个巨大的信息宝库。为了了解更多情况,请查看下面的图表,该图表显示了自 2009 年以来每月提交的增长情况,数据来自 arXiv:
每月提交到 arXiv 的计算机科学论文数量。图片由作者提供,数据可以从arXiv公开获取。
做个简单的计算,假设平均每篇论文需要 3 小时才能从头到尾阅读完。按照上述数字,需要 300 小时(或 12 天!)才能读完所有这些论文。而这只是读完一天的论文——第二天,我们又得重新开始,继续阅读类似数量的出版物。显然,这对于专家和初学者来说都是不可行的。
通常,作为机器学习的初学者,你可能会问:我需要阅读论文吗?而且,考虑到有这么多论文,我到底该怎么做呢?这就是原因和方法!
为什么机器学习初学者应该阅读论文
论文就是一场讲座:为了在顶级机器学习会议上被接受,论文的写作必须简洁明了。论文通常包括对主题的介绍、方法部分、结果和总结。总体而言,论文的内容就是对一个非常狭窄的主题进行(浓缩的)讲解。原因如下:
机器学习工程 101:对错误“DataLoader worker (pid(s) xxx) exited unexpectedly”的全面解释
深入探讨 PyTorch DataLoader 与多进程
https://mengliuz.medium.com/?source=post_page---byline--f3a6a983911e--------------------------------https://towardsdatascience.com/?source=post_page---byline--f3a6a983911e-------------------------------- 赵梦柳
·发表于 Towards Data Science ·阅读时长 6 分钟·2024 年 6 月 3 日
–
作为日常使用 PyTorch 库的众多用户之一,我相信许多机器学习工程师在训练过程中迟早会遇到“DataLoader worker (pid(s) xxx) exited unexpectedly”这个问题。
这令人沮丧。
当使用 num_workers > 0 调用 DataLoader 时,通常会触发这个错误。许多在线帖子提供了简单的解决方案,比如将 num_workers 设置为 0,这样当前的问题就会消失,但实际上会引发新的问题。
本文将向你展示一些可能有助于解决这个问题的技巧。我将深入探讨 Torch.multiprocessing 模块,并展示一些有用的虚拟内存监控和泄漏防止技术。在极少数情况下,即使没有内存泄漏,torch.multiprocessing 工作进程的异步内存占用和释放仍然可能触发该问题。最终解决方案是优化虚拟内存的使用,理解 torch.multiprocessing 的行为,并在 getitem 方法中进行垃圾回收。
注意:我使用的平台是 Ubuntu 20.04。为了适应其他平台,许多终端命令需要做相应的调整。
图片来源:pxhere.com/en/photo/1379760#google_vignette
暴力解决方案及其缺点
如果你在网上搜索,大多数遇到相同问题的人都会告诉你暴力解决方案;只需将 DataLoader 中的 num_workers 设置为 0,问题就会消失。
如果你的数据集较小,并且能容忍较长的训练时间,这将是最简单的解决方案。然而,根本问题依然存在,如果你有一个非常大的数据集,设置 num_workers=0 将导致非常慢的性能,有时会慢 10 倍。因此,我们必须进一步研究问题并寻找替代解决方案。
监控你的虚拟内存使用情况
当 DataLoader 工作进程退出时,究竟发生了什么?
要捕捉系统中的最后一个错误日志,请在终端中运行以下命令,它将为你提供更详细的错误信息。
dmesg -T
通常,你会看到真实的原因是“内存不足”。但是为什么会出现内存不足的问题呢?具体是什么导致了额外的内存消耗?
当我们在 DataLoader 中设置 num_workers=0 时,只有一个主进程运行训练脚本。只要数据批次能够装入内存,它就会正常运行。
然而,当设置 num_workers > 0 时,情况就不一样了。DataLoader 会启动子进程,并将 prefetch_factornum_workers* 的数据预加载到内存中以加速训练。默认情况下,prefetch_factor = 2。预加载的数据将消耗机器的虚拟内存(但好消息是它不会占用 GPU,因此你不需要缩小批次大小)。所以,我们需要做的第一件事是监控系统的虚拟内存使用情况。
监控虚拟内存使用情况的最简单方法之一是使用 psutil 包,它将监控虚拟内存的使用百分比
import psutil
print(psutil.virtual_memory().percent)
你还可以使用 tracemalloc 包,它将为你提供更详细的信息:
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
当实际的 RAM 满时,空闲的数据将流入交换空间(因此它是你虚拟内存的一部分)。要检查交换空间,使用以下命令:
free -m
在训练期间临时更改交换空间大小(例如,增加到 16G),在终端中执行以下命令:
swapoff -a
fallocate -l 16G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
/dev/shm(或者在某些情况下,/run/shm)是用于存储临时文件的另一种文件系统,应该进行监控。只需运行以下命令,你将看到文件系统中的驱动器列表:
df -h
要临时调整它的大小(例如,增加到 16GB),只需运行:
sudo mount -o remount,size=16G /dev/shm
Torch.multiprocessing 最佳实践
然而,虚拟内存只是问题的一部分。如果在调整交换磁盘后问题仍然存在怎么办?
问题的另一面是 torch.multiprocessing 模块的底层问题。官方网页上有许多最佳实践建议:
torch.multiprocessing 是 Python 模块的直接替代品。它支持完全相同的操作,但进行了扩展……
除了这些,还应该考虑另外三种方法,特别是关于内存使用的方面。
第一个问题是共享内存泄漏。泄漏意味着每次子进程运行后,内存没有正确释放,你可以通过监控运行时的虚拟内存使用情况来观察这个现象。内存消耗会不断增加,直到达到“内存不足”的程度。这是典型的内存泄漏现象。
那么,是什么导致了内存泄漏呢?
让我们来看看 DataLoader 类本身:
github.com/pytorch/pytorch/blob/main/torch/utils/data/dataloader.py
查看 DataLoader 的内部结构时,我们会看到,当nums_worker > 0
时,调用的是_MultiProcessingDataLoaderIter
。在_MultiProcessingDataLoaderIter
内部,Torch.multiprocessing 创建了工作队列。Torch.multiprocessing 使用两种不同的内存共享和缓存策略:file_descriptor和file_system。虽然file_system不需要文件描述符缓存,但它容易导致共享内存泄漏。
要检查你的机器使用的是哪种共享策略,只需在脚本中添加以下内容:
torch.multiprocessing.get_sharing_strategy()
要获取系统文件描述符限制(Linux),请在终端运行以下命令:
ulimit -n
要将共享策略切换为file_descriptor:
torch.multiprocessing.set_sharing_strategy(‘file_descriptor’)
要统计已打开的文件描述符数量,请运行以下命令:
ls /proc/self/fd | wc -l
只要系统允许,建议使用file_descriptor策略。
第二个问题是多进程工作者启动方法。简而言之,这是关于是否使用 fork 或 spawn 作为工作者启动方法的争论。Fork 是 Linux 中启动多进程的默认方式,可以避免某些文件复制,因此速度更快,但在处理 CUDA 张量和第三方库(如 OpenCV)时,可能会遇到问题。
要使用 spawn 方法,你只需将参数*multiprocessing_context=* “spawn”
传递给 DataLoader。
第三,确保数据集对象可序列化/可拾取
有一篇非常不错的文章进一步讨论了进程折叠中的“按需复制”效应:ppwwyyxx.com/blog/2022/Demystify-RAM-Usage-in-Multiprocess-DataLoader/
简而言之,不再推荐在__getitem__
方法中创建文件名列表并加载它们。可以创建一个 numpy 数组或 pandas 数据框来存储文件名列表,以便进行序列化。如果你熟悉 HuggingFace,使用 CSV/数据框是加载本地数据集的推荐方法:huggingface.co/docs/datasets/v2.19.0/en/package_reference/loading_methods#datasets.load_dataset.example-2
如果你的数据加载器非常慢怎么办?
好的,现在我们对多进程模块有了更好的理解。但这就是故事的结局吗?
听起来真的很疯狂。如果你有一个大且重的数据集(例如,每个数据点 > 5 MB),就有可能遇到上述问题,我将告诉你为什么。秘密就在于多进程工作进程的异步内存释放。
诀窍很简单:黑进 torch
库,在 _MultiProcessingDataLoaderIter 类的数据队列前后添加一行 psutil.virtual_memory().percent:
[## pytorch/torch/utils/data/dataloader.py 在 70d8bc2da1da34915ce504614495c8cf19c85df2 ·…
使用强大的 GPU 加速的 Python 中的张量和动态神经网络 - pytorch/torch/utils/data/dataloader.py 在…
github.com](https://github.com/pytorch/pytorch/blob/70d8bc2da1da34915ce504614495c8cf19c85df2/torch/utils/data/dataloader.py?source=post_page-----f3a6a983911e--------------------------------#L1130)
类似这样的:
print(“before clearing”, psutil.virtual_memory().percent)
data = self._data_queue.get(timeout=timeout)
print("after", psutil.virtual_memory().percent)
在我的情况下,我将 DataLoader 的 num_workers
设置为 8,并观察到如下情况:
所以内存不断上涨——但这算是内存泄漏吗?其实不算。问题的根本原因是 dataloader 的工作进程加载速度快于释放速度,创建了 8 个任务,却只释放了 2 个。这就是内存溢出的根本原因。解决方案很简单:只需在你的 getitem 方法的开头添加一个垃圾回收器即可:
import gc
def __getitem__(self, idx):
gc.collect()
现在你已经做好了准备!
参考文献
-
pytorch.org/docs/stable/multiprocessing.html#sharing-strategies
-
stackoverflow.com/questions/76491885/how-many-file-descriptors-are-open
-
psutil.readthedocs.io/en/latest/index.html#psutil.virtual_memory
-
stackoverflow.com/questions/4970421/whats-the-difference-between-virtual-memory-and-swap-space
-
britishgeologicalsurvey.github.io/science/python-forking-vs-spawn/
-
stackoverflow.com/questions/64095876/multiprocessing-fork-vs-spawn
ML 变形:通过串联 ML 模型实现优化结果
知识蒸馏、模型压缩和规则提取的普遍原理
https://medium.com/@vadim.arzamasov?source=post_page---byline--d89d952627a9--------------------------------https://towardsdatascience.com/?source=post_page---byline--d89d952627a9-------------------------------- Vadim Arzamasov
·发表于 Towards Data Science ·阅读时长 7 分钟·2024 年 10 月 23 日
–
图 1。此图及其他图像由作者在 recraft.ai 的帮助下创建
机器学习(ML)模型训练通常遵循一个熟悉的流程:首先收集数据,清理并准备数据,然后进行模型拟合。但如果我们能将这一过程进一步推进呢?就像一些昆虫在达到成熟之前会经历剧烈的变化一样,ML 模型也可以以类似的方式进化(参见 Hinton 等人[1])——我将其称为ML 变形。这个过程涉及将不同的模型串联在一起,最终生成的模型比直接从头开始训练的模型质量要好得多。
其工作原理如下:
-
从一些初步知识开始,数据 1。
-
在这些数据上训练一个机器学习模型,模型 A(例如神经网络)。
-
使用模型 A生成新的数据,数据 2。
-
最后,使用数据 2 来拟合你的目标模型,模型 B。
图 2。ML 变形的示意图
你可能已经熟悉这一概念,知识蒸馏就是用一个较小的神经网络替换较大的神经网络。但 ML 变形更进一步,初始模型(模型 A)和最终模型(模型 B)不必是神经网络。
示例:MNIST 数据集上的 ML 变形
想象一下,你的任务是使用 MNIST 手写数字图像数据集训练一个多类决策树,但只有 1,000 张图像有标签。你可以直接在这个有限的数据上训练决策树,但准确度大约只能达到 0.67。这并不好,对吧?或者,你可以使用机器学习变换方法来提高结果。
但在深入探讨解决方案之前,我们先快速回顾一下支持这种方法的技术和研究。
1. 知识蒸馏(2015)
即使你没有使用知识蒸馏,你也可能见过它的应用。例如,Meta 建议蒸馏其 Llama 3.2 模型,以便将其适配到特定任务[2]。或者看看 DistilBERT——一个蒸馏版的 BERT[3],或者 DMD 框架,它通过蒸馏 Stable Diffusion 来加速图像生成速度,提升了 30 倍[4]。
知识蒸馏的核心是将知识从一个大型、复杂的模型(教师)传递给一个较小、效率更高的模型(学生)。该过程包括创建一个转移集,该集包含原始训练数据和由教师模型伪标注的额外数据(无论是原始数据还是合成数据)。这些伪标签被称为软标签——它们源自教师模型在多个类别上的预测概率。这些软标签提供了比硬标签(简单的类别指示符)更丰富的信息,因为它们反映了教师的信心,并捕捉到类别之间的微妙相似性。例如,它们可能表明一个特定的“1”比“5”更像“7”。
通过在这个丰富的转移集上进行训练,学生模型可以有效地模仿教师模型的表现,同时更加轻便、快速且易于使用。
以这种方式获得的学生模型比仅在原始训练集上训练得到的模型更准确。
2. 模型压缩(2007)
模型压缩[5]通常被视为知识蒸馏的前奏,但两者之间存在重要差异。与知识蒸馏不同,模型压缩似乎并未使用软标签,尽管文献中有一些相关说法[1,6]。我没有找到任何证据表明软标签是过程的一部分。事实上,原始论文中的方法甚至不依赖于人工神经网络(ANNs)作为模型 A。相反,它使用了一组模型——如支持向量机(SVMs)、决策树、随机森林等。
模型压缩通过逼近特征分布p(x)来创建转移集。然后,这个集由模型 A标注,提供条件分布p(y|x)。原始工作中的关键创新是一种名为 MUNGE 的技术,用于逼近p(x)。与知识蒸馏一样,目标是训练一个较小、更高效的模型 B,并保留较大模型 A的性能。
正如知识蒸馏中所示,这种训练方式得到的压缩模型,通常能够超过直接在原始数据上训练的类似模型,因为传递集中嵌入了丰富的信息[5]。
通常,“模型压缩”一词被更广泛地用来指代任何减少模型 A大小的技术[7,8]。这包括像知识蒸馏这样的技术,也包括不依赖传递集的技术,如修剪、量化或神经网络的低秩近似。
3. 规则提取(1995)
当问题不在于计算复杂度或内存,而在于模型决策过程的不透明性时,教育性规则提取提供了一种解决方案[9]。在这种方法中,训练一个更简单、更易解释的模型(模型 B)来复制不透明教师模型(模型 A)的行为,目的是推导出一组人类可读的规则。这个过程通常从将未标记的示例(通常是随机生成的)输入到模型 A开始,模型 A对这些示例进行标记,生成一个传递集。然后,使用这个传递集来训练透明的学生模型。例如,在分类任务中,学生模型可能是一个决策树,它输出如下规则:“如果特征 X1 大于阈值 T1 且特征 X2 小于阈值 T2,则分类为正类”。
教育性规则提取的主要目标是紧密模仿教师模型的行为,以保真度为衡量标准——即学生模型与教师模型之间的准确性,作为主要的质量衡量标准。
有趣的是,研究表明,通过这种方法创建的透明模型,有时能够比直接在用于构建模型 A的原始数据上训练的类似模型达到更高的准确率[10,11]。
教育性规则提取属于被称为“全局”模型解释方法的更广泛技术家族,这其中还包括分解性和折衷规则提取。更多详情请参见[12]。
4. 仿真作为模型 A
模型 A不一定是一个机器学习模型——它也可以是一个经济或物理过程的计算机仿真,例如模拟飞机机翼周围的气流。在这种情况下,数据 1由定义该过程的微分方程或差分方程组成。对于任何给定的输入,仿真通过数值解这些方程来做出预测。然而,当这些仿真变得计算开销较大时,就需要一种更快速的替代方案:一个代理模型(模型 B),它可以加速诸如优化之类的任务[13]。当目标是识别输入空间中的重要区域,例如系统稳定性的区域时,开发一个可解释的模型 B,这一过程称为情境发现[14]。为了生成用于代理建模和情境发现的传递集(数据 2),模型 A会在一个多样化的输入集上运行。
回到我们的 MNIST 示例
在 TDS 上的一篇深刻的文章中,Niklas von Moers展示了半监督学习如何提高卷积神经网络(CNN)在相同输入数据上的表现。这个结果适用于 ML 变换流水线的第一阶段,在该阶段,Model A是一个经过训练的 CNN 分类器。转移集Data 2包含原本标注的 1,000 个训练样本以及约 55,000 个由Model A高置信度预测的伪标注样本。接下来,我在Data 2上训练我们的目标Model B,一个决策树分类器,并达到了 0.86 的准确率——远高于仅在Data 1的标注部分训练时的 0.67。这意味着,将决策树与 CNN 解决方案链式连接,将决策树的错误率从 0.33 降低到了 0.14。相当大的提升,是吧?
查看完整的实验代码,请访问 GitHub 仓库。
结论
总结来说,ML 变换并非总是必要的——特别是当你的唯一关注点是准确性,并且不需要可解释性、更快的推理或减少存储需求时。但在其他情况下,链式模型可能会比直接在原始数据上训练目标模型产生显著更好的结果。
图 2:为方便参考,图示再次呈现
对于分类任务,过程包括:
-
Data 1:原始的,完全或部分标注的数据。
-
Model A:在Data 1上训练的模型。
-
Data 2:包括伪标注数据的转移集。
-
Model B:最终模型,旨在满足额外要求,如可解释性或效率。
那么,为什么我们不总是使用 ML 变换呢?挑战通常在于找到合适的转移集,Data 2 [9]。但这是另一个话题。
参考文献
[1] Hinton, Geoffrey. “蒸馏神经网络中的知识。” arXiv 预印本 arXiv:1503.02531 (2015 年)。
[2] 介绍 Llama 3.2
[3] Sanh, Victor 等人. “DistilBERT:BERT 的蒸馏版:更小、更快、更便宜、更轻量。” arXiv 预印本 arXiv:1910.01108 (2019 年)。
[4] Yin, Tianwei 等人. “一步扩散与分布匹配蒸馏。” IEEE/CVF 计算机视觉与模式识别会议论文集。2024 年。
[5] Buciluǎ, Cristian, Rich Caruana, 和 Alexandru Niculescu-Mizil. “模型压缩” 第 12 届 ACM SIGKDD 国际会议《知识发现与数据挖掘》论文集,2006 年。
[6] 知识蒸馏,维基百科
[7] 太空深度学习模型压缩技术概述,发表于 Medium
[8] 使用未标注的问答数据集蒸馏 BERT,发表于 Towards Data Science
[9] Arzamasov, Vadim, Benjamin Jochum, 和 Klemens Böhm. “教育规则提取以学习可解释模型 — 一项实证研究.” arXiv 预印本 arXiv:2112.13285 (2021).
[10] Domingos, Pedro. “通过多模型从示例中获取知识” MACHINE LEARNING-INTERNATIONAL WORKSHOP THEN CONFERENCE-. MORGAN KAUFMANN PUBLISHERS, INC., 1997.
[11] De Fortuny, Enric Junque, 和 David Martens. “基于主动学习的教学规则提取.” IEEE 神经网络与学习系统交易 26.11 (2015): 2664–2677.
[12] Guidotti, Riccardo, 等. “解释黑盒模型的方法调查.” ACM 计算机调查 (CSUR) 51.5 (2018): 1–42.
[13] 代理模型,维基百科
[14] Python 中的情景发现,Water Programming上的博客文章
[15] 让模型从自身学习,发表于 Towards Data Science
将简单线性回归剖析到最基础的层面
MLBasics #1:用简单线性回归揭开机器学习算法的神秘面纱
https://medium.com/@rfeers?source=post_page---byline--8d83cac9873a--------------------------------https://towardsdatascience.com/?source=post_page---byline--8d83cac9873a-------------------------------- Josep Ferrer
·发布于 Towards Data Science ·阅读时长 8 分钟·2024 年 1 月 14 日
–
图片由作者提供。ML Basics。简单线性回归。
在数据和计算机程序的世界里,机器学习的概念可能听起来像一个难以破解的难题,充满了复杂的数学和复杂的思想。
这就是为什么今天我想放慢脚步,看看使这一切运作的基本内容。我将开始发布一系列新的文章,名为 MLBasics。
我们将回顾那些简单却至关重要的模型,它们是机器学习的基本组成部分。可以把它看作是从一个大拼图中开始,从最简单的部分入手。我们回归到简单的内容,在这里很容易理解发生了什么。
所以,跟着我们一起走,看看我们如何将其拆解,并使一切变得清晰明了。
让我们一步一步地一起深入了解简单线性回归吧!👇🏻🤓
#1. 简单线性回归简介
预测分析的领域广阔,但在其核心是线性回归——最简单的方法,用来理解数据趋势。
虽然它扩展到多个变量时可能会让人觉得有些艰难,但今天我们的重点将专注于简单线性回归。
MLOps — MLflow Pipelines 的温和入门
图片由Sean Robertson提供,来自Unsplash
使用 MLflow 管理您的端到端机器学习生命周期
https://medium.com/@marcellopoliti?source=post_page---byline--c7bcec88a6ec--------------------------------https://towardsdatascience.com/?source=post_page---byline--c7bcec88a6ec-------------------------------- Marcello Politi
·发表于Towards Data Science ·8 分钟阅读·2024 年 3 月 13 日
–
介绍
各种统计数据显示,50% 到 90% 的模型未能成功投入生产。这通常是由于未能有效地组织工作流程。学术界(或 Kaggle 上)获得的技能,往往不足以支撑一个可以被成千上万用户使用的机器学习系统。
在寻找机器学习行业工作时,最为抢手的技能之一就是能够使用能协调复杂流程的工具,如 MLflow。
在本文中,我们将了解如何将一个项目结构化为多个步骤,并以有序的方式管理所有步骤。
我在Deepnote上运行本文的脚本:一个基于云的笔记本,非常适合协作数据科学项目和原型设计。
什么是 MLflow?
MLflow 是一个由Databricks开发的开源平台,用于机器学习的端到端生命周期管理。
MLflow 提供多种功能,如监控训练中的模型,使用工件存储等……
MLOps — 使用 PyTest 进行数据验证
图片由Michael Dziedzic提供,来自Unsplash
运行确定性和非确定性测试以验证你的数据集
https://medium.com/@marcellopoliti?source=post_page---byline--749641874871--------------------------------https://towardsdatascience.com/?source=post_page---byline--749641874871-------------------------------- Marcello Politi
·发表于Towards Data Science ·9 分钟阅读·2024 年 6 月 11 日
–
介绍
在 MLOps 管道中,我们尽量自动化尽可能多的步骤,目标是最小化程序员直接干预可能导致的错误数量,同时也要关注数据集验证。我相信大家都熟悉机器学习的第 1 条规则:垃圾进,垃圾出。无论我们开发的模型有多么复杂,如果数据集没有得到妥善处理,我们很有可能会得到糟糕的结果。
在本文中,我们将看到如何使用PyTest对数据集进行自动化验证。
我使用Deepnote运行本文中的脚本:这是一个基于云的笔记本,非常适合协作的数据科学项目和原型开发。
关于 ETL
初次接触机器学习的人通常需要解决一些像Kaggle上找到的挑战。在这些挑战中,我们几乎总是有一个静态数据集,它不会随时间变化。然而,在现实世界中,情况并非完全如此。
在处理实际的机器学习产品时,数据可能会不断变化。由此产生的…
MMM:用于市场营销组合建模和广告支出回报率(ROAS)的贝叶斯框架
使用 PyMC 的贝叶斯框架来建模媒体渠道表现、广告支出回报率(ROAS)和预算分配
https://medium.com/@luisroque?source=post_page---byline--ccade4005bd5--------------------------------https://towardsdatascience.com/?source=post_page---byline--ccade4005bd5-------------------------------- Luís Roque
·发布于 Towards Data Science ·18 分钟阅读·2024 年 6 月 6 日
–
这篇文章由 Rafael Guedes 共同撰写。
介绍
可扩展的互联网企业依赖营销来推动增长。当然,不仅如此,在一定规模下,极少数公司能够承受不在客户获取方面做到极其高效的成本。两大热门话题,企业正在大量投资以将人工智能(AI)能力引入营销领域,分别是媒体组合建模(MMM)和客户生命周期价值(LTV)预测。两者的目标都是提高企业在营销上的投资回报。本文将介绍 MMM 是什么以及应用 MMM 的最佳实践。
MMM 是一种技术,允许营销团队衡量他们的投资影响及其如何促进转化。随着过去几年可用的广告平台激增,这项任务的复杂性迅速增加。这一现象将潜在客户分散到了不同的媒体渠道,这些渠道可以分为离线或在线两类。传统的离线渠道与数字支持脱节,可能包括报纸、广播、电视广告、优惠券以及展会上的摊位。在线渠道爆炸性增长,企业将它们结合使用…
使用 FastAPI、Azure 和 Docker 进行模型部署
使用 FastAPI 服务机器学习模型的完整指南
https://medium.com/@sabrine.bendimerad1?source=post_page---byline--10e5cfbc1f4f--------------------------------https://towardsdatascience.com/?source=post_page---byline--10e5cfbc1f4f-------------------------------- Sabrine Bendimerad
·发表于Towards Data Science ·10 分钟阅读·2024 年 9 月 28 日
–
欢迎来到我MLOps 系列的第三篇文章。在第一篇文章中,我们探讨了 Docker 及其如何简化应用程序打包。在第二篇文章中,我们使用MLflow、Azure和Docker来管理机器学习模型。现在,在这一第三部分,我们将通过构建一个FastAPI应用程序,将我们之前存储的模型部署到 Azure 上,从而将所有内容整合起来。这将允许我们创建一个全球可访问的预测服务!
什么是 API?
API就像一座桥梁。当你与 Python 中的库进行交互时,你就是在使用它的 API。它是一个应用程序的公开部分,你可以与之交互,而其背后的所有内容则是隐藏的。
API 通常用于与 Web 应用程序进行通信,它们提供一组返回数据的 URL(你发送带有一些参数的请求,并收到响应)。通常,数据以像 JSON 或 XML 这样的格式返回,这些格式易于解析。这与返回 HTML 的网页不同,HTML 包括渲染页面所需的信息。通过 API,你只会得到原始数据。
有些 API 是公开的,而其他的是私有的。在构建 API 时,你决定分享哪些数据,以及如何分享……
模型漂移介绍与概念
了解机器学习模型漂移背后的一些概念,并理解为什么 MLOps 在今天的世界中如此重要。
https://ivopbernardo.medium.com/?source=post_page---byline--e32c5305da2a--------------------------------https://towardsdatascience.com/?source=post_page---byline--e32c5305da2a-------------------------------- Ivo Bernardo
·发表于 Towards Data Science ·6 分钟阅读·2024 年 6 月 22 日
–
模型会发生变化,因为世界在变化——图片来源:arptrastogi 通过 Unsplash.com
税收、死亡和模型漂移是生活中唯一的三大确定性。好吧,我可能在这句格言中加入了最后一个,但事实是所有模型都会遭遇衰退。
开发机器学习模型后,你总会看到相同的模式发展:
-
在开发过程中,模型在测试集上的表现是预期的。
-
模型在投入生产后表现不同(通常,表现稍差)。
-
模型的性能随着时间的推移而下降。
几年后,你的模型表现很可能比最初开发时差得多。这可能由多种原因引起,但根本原因是世界在变化。
当世界变化时,我们用来表示现实信息的数据也会发生变化。 潜在的数据分布发生偏移,这必然会影响我们的机器学习模型如何学习和表现。
在这篇博客文章中,我们将探讨一些情况示例,其中世界的潜在变化如何影响你的模型……
模型评估与任务评估
由作者使用 Dall-E 3 创建的图像
理解 LLM 应用中的差异
https://aparnadhinak.medium.com/?source=post_page---byline--5bc742054957--------------------------------https://towardsdatascience.com/?source=post_page---byline--5bc742054957-------------------------------- Aparna Dhinakaran
·发表于Towards Data Science ·9 分钟阅读·2024 年 3 月 26 日
–
想象一下飞机。你首先想到的是什么?现在再想象一架波音 737 和一架V-22 鱼鹰。这两者都是旨在运输货物和人员的飞机,但它们服务的目的不同——一种更为通用(商业航班和货运),另一种非常具体(为特种作战部队执行渗透、撤离和补给任务)。它们看起来完全不同,因为它们是为不同的活动而设计的。
随着 LLM 的兴起,我们见证了第一批真正的通用机器学习模型。它们的通用性在许多方面帮助了我们:
-
同一个工程团队现在可以进行情感分析和结构化数据提取
-
许多领域的从业者可以共享知识,从而使整个行业能够相互受益于彼此的经验
-
有许多行业和工作领域,其中相同的经验是有用的
但正如我们在飞机中看到的,通用性需要与在特定任务上出色表现截然不同的评估方法,归根结底,商业价值通常来自于解决特定的问题。
这是模型评估与任务评估差异的一个很好的类比。模型评估侧重于总体的综合评估,而任务评估则侧重于评估特定任务的表现。
不止一个 LLM 评估
LLM 评估 这个术语常常被广泛使用。OpenAI 早期发布了一些工具来进行 LLM 评估,例如。大多数从业者更关注 LLM 任务评估,但这一区分并不总是很清晰。
有什么区别?
模型评估关注的是模型的“整体健身情况”。它在各种任务上的表现如何?
任务评估则专门设计用来检查模型是否适合你的特定应用。
一般锻炼并且身体素质较好的人,在真实比赛中可能会在职业相扑选手面前表现不佳,而模型评估无法与任务评估在评估你特定需求的能力上相提并论。
模型评估
模型评估专门用于构建和微调通用模型。它们基于你给模型提出的一组问题以及你用来评分的地面真实答案。可以将其想象成参加 SAT 考试。
虽然模型评估中的每个问题都不同,但通常有一个共同的测试领域。每个指标都有一个特定的目标主题或技能。例如,HellaSwag 的表现已经成为衡量 LLM 质量的流行方式。
HellaSwag 数据集包含了一系列上下文和多项选择题,每个问题都有多个可能的完成选项。只有一个选项是合乎逻辑且合理的,其他选项虽然看似合理,但其实是错误的。这些完成项旨在对 AI 模型提出挑战,不仅要求语言理解能力,还需要常识推理才能选择正确的选项。
这是一个例子:
一盘土豆被放入烤箱并取出。一大盘蛋糕被翻过来并放到柜台上。一大盘肉
A. 被放到烤土豆上
B. 土豆和腌菜被放入烤箱中
C. 被准备好后,由助手在完成时从烤箱中取出。
另一个例子是 MMLU。MMLU 涵盖了多个学科的任务,包括科学、文学、历史、社会科学、数学以及法律和医学等专业领域。这些学科的多样性旨在模拟人类学习者所需的知识和理解的广度,使其成为测试模型处理多面语言理解挑战能力的好方法。
这里有一些例子——你能解答它们吗?
在以下哪些热力学过程中,理想气体的内能增加等于加热到气体的热量?
A. 恒温
B. 恒体积
C. 恒压
D. 绝热过程
图片来源:作者
Hugging Face 排行榜可能是获取此类模型评估的最知名平台。排行榜跟踪开源的大型语言模型,并记录许多模型评估指标。这通常是一个很好的起点,用来理解开源 LLM 在不同任务表现上的差异。
多模态模型需要更多的评估。Gemini 论文展示了多模态引入了许多其他基准,比如 VQAv2,它测试理解和整合视觉信息的能力。这些信息不仅仅是简单的物体识别,而是对行动和物体之间关系的解读。
同样,针对音频和视频信息以及如何跨模态整合的指标也存在。
这些测试的目标是区分两个模型或同一个模型的两个不同快照。选择一个适合你应用的模型很重要,但这是你做的事,一般情况下只是一次性操作或非常不频繁的操作。
作者提供的图片
任务评估
更常见的问题是通过任务评估来解决的。基于任务的评估目标是分析使用 LLM 作为评判者的模型表现。
-
你的检索系统是否获取了正确的数据?
-
你的回答中有幻觉吗?
-
系统是否用相关的答案回答了重要问题?
有些人可能对 LLM 评估其他 LLM 感到有些不确定,但我们每天都有人工评估其他人。
模型评估和任务评估的真正区别在于:在模型评估中,我们会提出许多不同的问题,而在任务评估中,问题保持不变,只有数据会变化。例如,假设你在操作一个聊天机器人。你可以在数百次客户互动中使用你的任务评估,并问它:“这里有幻觉吗?”这个问题在所有对话中始终相同。
作者提供的图片
有几个库旨在帮助实践者构建这些评估:Ragas,Phoenix(完全披露:作者领导了开发 Phoenix 的团队),OpenAI,LlamaIndex。
它们是如何工作的?
任务评估整体上评估应用程序的每个输出的表现。我们来看看构建一个评估任务需要哪些内容。
建立基准
基础在于建立一个稳健的基准。这从创建一个能够准确反映 LLM 将遇到的场景的黄金数据集开始。该数据集应包含地面真实标签——通常来源于细致的人工审核——作为对比标准。别担心,通常你可以用几十到几百个示例来完成。选择合适的 LLM 进行评估也至关重要。虽然它可能与应用程序的主要 LLM 不同,但应该与成本效益和准确性目标保持一致。
制定评估模板
任务评估过程的核心是评估模板。该模板应清晰定义输入(例如,用户查询和文档)、评估问题(例如,文档与查询的相关性)和预期的输出格式(二元或多类别相关性)。根据应用程序的具体需求,可能需要调整模板,以确保它能够准确评估 LLM 在黄金数据集上的表现。
这里是一个用来评估问答任务的模板示例。
You are given a question, an answer and reference text. You must determine whether the given answer correctly answers the question based on the reference text. Here is the data:
[BEGIN DATA]
************
[QUESTION]: {input}
************
[REFERENCE]: {reference}
************
[ANSWER]: {output}
[END DATA]
Your response should be a single word, either "correct" or "incorrect", and should not contain any text or characters aside from that word.
"correct" means that the question is correctly and fully answered by the answer.
"incorrect" means that the question is not correctly or only partially answered by the answer.
度量标准与迭代
在你的黄金数据集上运行评估,可以生成关键度量指标,如准确率、精确度、召回率和 F1 分数。这些指标为评估模板的有效性提供了洞察,并突出改进的领域。迭代至关重要;根据这些度量标准精炼模板,确保评估过程与应用目标保持一致,同时避免对黄金数据集的过拟合。
在任务评估中,仅依赖总体准确率是不够的,因为我们总是会遇到显著的类别不平衡。精确度和召回率提供了更全面的视角,强调准确识别相关和不相关结果的重要性。平衡的度量方法确保评估对提升 LLM 应用有实际贡献。
LLM 评估的应用
一旦评估框架就绪,下一步是将这些评估直接应用到你的 LLM 应用中。这涉及将评估过程集成到应用程序的工作流程中,以便实时评估 LLM 对用户输入的响应。这个持续的反馈循环对于保持和提高应用的相关性和准确性至关重要。
系统生命周期中的评估
有效的任务评估不仅限于单一阶段,而是贯穿整个大语言模型(LLM)系统的生命周期。从生产前的基准测试和评估,到生产中的持续性能评估, LLM 评估 确保系统始终能够响应用户需求。
示例:模型是否出现幻觉?
让我们更详细地看一个幻觉示例。
作者示例
由于幻觉是大多数从业者面临的普遍问题,现有一些基准数据集可供使用。这是一个很好的起步,但你通常需要在公司内部拥有一个定制的数据集。
接下来的重要步骤是开发提示模板。这里同样一个好的库可以帮助你入门。我们之前看到了一个示例提示模板,这里我们看到另一个专门针对幻觉的模板。你可能需要根据你的需求进行调整。
In this task, you will be presented with a query, a reference text and an answer. The answer is
generated to the question based on the reference text. The answer may contain false information, you
must use the reference text to determine if the answer to the question contains false information,
if the answer is a hallucination of facts. Your objective is to determine whether the reference text
contains factual information and is not a hallucination. A 'hallucination' in this context refers to
an answer that is not based on the reference text or assumes information that is not available in
the reference text. Your response should be a single word: either "factual" or "hallucinated", and
it should not include any other text or characters. "hallucinated" indicates that the answer
provides factually inaccurate information to the query based on the reference text. "factual"
indicates that the answer to the question is correct relative to the reference text, and does not
contain made up information. Please read the query and reference text carefully before determining
your response.
[BEGIN DATA]
************
[Query]: {input}
************
[Reference text]: {reference}
************
[Answer]: {output}
************
[END DATA]
Is the answer above factual or hallucinated based on the query and reference text?
Your response should be a single word: either "factual" or "hallucinated", and it should not include any other text or characters.
"hallucinated" indicates that the answer provides factually inaccurate information to the query based on the reference text.
"factual" indicates that the answer to the question is correct relative to the reference text, and does not contain made up information.
Please read the query and reference text carefully before determining your response.
现在你已经准备好将黄金数据集中的查询传递给你的评估 LLM,并让它标记出幻觉。当你查看结果时,记住应该存在类别不平衡。你需要跟踪精确度和召回率,而不是整体准确率。
构建混淆矩阵并将其可视化非常有用。当你有了这样的图表时,你可以对你的 LLM 性能感到放心。如果性能不尽如人意,你可以随时优化提示模板。
评估任务评估性能的示例,以便用户能够建立对其评估的信心
在评估构建完成后,你现在拥有了一个强大的工具,可以用已知的精确度和召回率标记所有数据。你可以使用它来跟踪你系统中的幻觉,无论是在开发阶段还是生产阶段。
区别总结
让我们总结一下任务评估和模型评估之间的区别。
表格由作者提供
要点总结
最终,模型评估和任务评估在构建功能性 LLM 系统时都非常重要。理解何时以及如何应用每种评估方法是很关键的。对于大多数从业者来说,大部分时间将花费在任务评估上,这为系统在特定任务上的性能提供了衡量标准。
使用信用卡欺诈数据的模型可解释性
为什么模型可解释性很重要
https://dan-to.medium.com/?source=post_page---byline--f219ff7ec89d--------------------------------https://towardsdatascience.com/?source=post_page---byline--f219ff7ec89d-------------------------------- Danila Morozovskii
·发布于 Towards Data Science ·17 分钟阅读·2024 年 6 月 12 日
–
最近,我偶然发现了一本在线书籍,书中描述了可用于机器学习模型可解释性的不同工具(christophm.github.io/interpretable-ml-book/
)。机器学习模型不应该是黑箱,并且可以解释的这一想法让我非常着迷,我决定深入研究这个话题。之前,当我开始进行一个新的机器学习项目时,我会遵循相同的程序:识别问题、熟悉数据集、特征工程、选择模型、训练/测试和超参数调优,以及结果分析。然而,我没有意识到自己漏掉了最关键的一步:模型可解释性。
什么是模型可解释性?
模型可解释性是解释黑箱(模型)如何工作以及如何做出预测的过程。让我们假设一个情况,一个人申请了信用贷款并被拒绝,因为模型做出了负面预测。任何人都会想知道为什么被拒绝,以及他们可能需要改变什么,才能使决定变得积极,而银行员工只能指着机器学习模型说“它是这么说的!”。这种情况并不好,并且会损害银行的声誉,因为看起来银行对其产品没有控制权。如果银行员工能够向客户解释哪些特定特征在做出预测时发挥了作用,那就会好得多。
使用 MLflow、Azure 和 Docker 进行模型管理
跟踪实验和管理模型的指南
https://medium.com/@sabrine.bendimerad1?source=post_page---byline--2920b51a5bdd--------------------------------https://towardsdatascience.com/?source=post_page---byline--2920b51a5bdd-------------------------------- Sabrine Bendimerad
·发表于 Towards Data Science ·阅读时长 10 分钟·2024 年 9 月 17 日
–
在第一篇文章中,我们探索了 Docker 将应用程序及其依赖项打包成可移植容器的强大能力,确保在各种环境中保持一致性。
在此基础上,本文介绍了MLflow,这是一个在机器学习工作流中用于实验追踪和模型管理的重要工具。我们将演示如何在 Docker 容器内部署和使用 MLflow,以确保可移植性并避免与依赖项相关的问题。容器化的 MLflow 服务器将部署在Azure上,以提供更好的可扩展性、远程访问,且更重要的是团队协作。
什么是 MLflow
MLflow 是一个开源平台,简化了机器学习生命周期的管理,从实验追踪到模型部署。它提供了一个稳定的框架,用于记录实验、管理代码和追踪模型版本,确保你的工作流程在团队间是可复现的且井然有序。
MLflow 可以集成到你机器学习管道的各个阶段。它提供了四个主要组件:
- MLflow Tracking:这是最广泛使用的功能,允许你记录和查询实验。它追踪有用的细节……
模型选择:类别平衡 第一部分
关于匿名化数据类别平衡的教程。
https://medium.com/@panData?source=post_page---byline--14b17003186f--------------------------------https://towardsdatascience.com/?source=post_page---byline--14b17003186f-------------------------------- Leo Anello 💡
·发表于数据科学前沿 ·阅读时间 22 分钟·2024 年 10 月 10 日
–
我将为你带来一个机器学习模型选择项目,涉及匿名化数据的多变量分析。
这是一个全面的项目,我们将从头到尾进行讲解——从定义业务问题到模型部署(尽管部署部分我们会留到其他时间)。
这个项目将有两部分完整的教程,我希望带你了解一系列技术,其中包括处理匿名化数据的复杂性——这是由于数据隐私问题,越来越多地出现在职场中的一种情况。
[## GitHub - Anello92/Model-Selection-and-Class-Balancing
本仓库包含一个关于机器学习模型选择的全面教程,专注于多变量分析…
github.com](https://github.com/Anello92/Machine-Learning-Model-Selection-and-Class-Balancing?source=post_page-----14b17003186f--------------------------------)
那么,处理这种类型的数据的最大挑战是什么呢?就是你没有任何关于每个变量所代表的内容的信息。
现在,这可有点棘手,不是吗?你会收到数据,在不知道每个变量代表什么的情况下,你需要基于这些数据开发一个机器学习模型。
我们还将借此机会深入探讨模型选择。哪种机器…
构建 5 个机器学习模型 第二部分
构建、比较和优化机器学习模型。
https://medium.com/@panData?source=post_page---byline--3be49fb0dc61--------------------------------https://towardsdatascience.com/?source=post_page---byline--3be49fb0dc61-------------------------------- Leo Anello 💡
·发表于Towards Data Science ·阅读时间 38 分钟·2024 年 10 月 10 日
–
模型选择
现在我们进入项目的第二部分——机器学习模型选择与多变量分析中的匿名化数据。
这第二部分是魅力所在——预测建模,机器学习。每个人都迫不及待地想直接进入构建机器学习模型的阶段。我理解这一点,我也感到同样的兴奋,因为我热爱这个阶段。
但在我们深入之前,我们必须经过数据处理——这正是我们在上一篇教程中涵盖的内容。
关于机器学习模型选择中的匿名数据类平衡的综合教程
towardsdatascience.com
安装和加载软件包
我们首先安装XGBoost包,这是参与机器学习竞赛的 Kaggle 平台用户的最爱之一。
# This package does not come with Anaconda and needs to be installed
!pip install -q xgboost
这个软件包不包括Anaconda,因此你需要单独安装它。要安装它,我们使用命令……
模型验证技术解析:带有代码示例的可视化指南
模型评估与优化
12 种必须了解的机器学习验证方法
https://medium.com/@samybaladram?source=post_page---byline--eb13bbdc8f88--------------------------------https://towardsdatascience.com/?source=post_page---byline--eb13bbdc8f88-------------------------------- Samy Baladram
·发表于Towards Data Science ·阅读时长 26 分钟·2024 年 11 月 30 日
–
每天,机器都会做出数百万个预测——从检测照片中的物体到帮助医生发现疾病。但在相信这些预测之前,我们需要知道它们是否准确。毕竟,没有人愿意使用一个大多数时候都错误的机器!
这时,验证就显得尤为重要。验证方法测试机器的预测结果,以衡量其可靠性。虽然这听起来很简单,但实际上存在多种验证方法,每种方法都是为了应对机器学习中的特定挑战而设计的。
在这里,我将这些验证技术——全部 12 种——以树状结构组织,展示它们如何从基本概念发展成更为专业的技术。当然,我们将使用清晰的可视化图像和一致的数据集,展示每种方法的不同之处以及为什么选择方法至关重要。
所有可视化图像:作者使用 Canva Pro 创建。已优化为移动端显示;在桌面端可能会显得过大。
什么是模型验证?
模型验证是测试机器学习模型在未见过或未在训练中使用过的数据上表现如何的过程。基本上,我们使用现有数据来检查模型的表现,而不是使用新的数据。这帮助我们在实际使用模型之前识别问题。
有多种验证方法,每种方法都有其特定的优势,并且解决不同的验证挑战:
-
不同的验证方法可能会产生不同的结果,因此选择正确的方法很重要。
-
一些验证技术在特定类型的数据和模型中效果更佳。
-
使用不正确的验证方法可能会导致关于模型真实表现的误导性结果。
这里有一棵树形图,展示了这些验证方法之间的关系:
这棵树形图展示了哪些验证方法相互关联。
接下来,我们将更仔细地研究每种验证方法,展示它们是如何工作的。为了更容易理解,我们将通过清晰的示例,展示这些方法如何在实际数据中运作。
📊 📈 我们的运行示例
我们将始终使用相同的示例,帮助你理解每种验证方法。虽然这个数据集可能不适合某些验证方法,但为了教学目的,使用这个示例使得比较不同方法并观察每种方法如何工作的过程更加容易。
📊 高尔夫游戏数据集
我们将使用这个数据集,它根据天气条件预测某人是否会打高尔夫。
列:‘Overcast(独热编码为 3 列)’,‘Temperature’(以华氏度表示),‘Humidity’(百分比),‘Windy’(是/否)和‘Play’(是/否,目标特征)
import pandas as pd
import numpy as np
# Load the dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rainy', 'rainy', 'rainy', 'overcast',
'sunny', 'sunny', 'rainy', 'sunny', 'overcast', 'overcast', 'rainy',
'sunny', 'overcast', 'rainy', 'sunny', 'sunny', 'rainy', 'overcast',
'rainy', 'sunny', 'overcast', 'sunny', 'overcast', 'rainy', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0,
72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0,
88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0,
90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0,
65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True,
True, False, True, True, False, False, True, False, True, True, False,
True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes',
'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes',
'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)
# Data preprocessing
df = pd.DataFrame(dataset_dict)
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
df['Wind'] = df['Wind'].astype(int)
# Set the label
X, y = df.drop('Play', axis=1), df['Play']
📈 我们的模型选择
我们将在所有测试中使用决策树分类器。如果你不熟悉它,可以参考以下文章:
## 决策树分类器解析:附带代码示例的视觉指南(面向初学者)
对我们最喜欢的倒立树的全新看法
[towardsdatascience.com
我们选择这个模型是因为我们可以很容易地将结果模型绘制为树形结构,每个分支显示不同的决策。为了简化操作并专注于如何测试模型,我们将使用默认的scikit-learn
参数,并设置固定的random_state
。
让我们明确这两个术语:决策树分类器是我们的学习算法——它是找到数据中模式的方法。当我们将数据输入该算法时,它会创建一个模型(在这种情况下,是一棵显示不同决策的树)。这个模型就是我们实际用来进行预测的模型。
from sklearn.tree import DecisionTreeClassifier, plot_tree
import matplotlib.pyplot as plt
dt = DecisionTreeClassifier(random_state=42)
每次我们以不同的方式拆分数据进行验证时,都会得到不同的模型和不同的决策规则。一旦我们的验证表明算法可靠地工作,我们将使用所有数据创建一个最终模型。这个最终模型就是我们实际用来预测某人是否会打高尔夫的模型。
设置好这一切后,我们现在可以集中精力了解每种验证方法的工作原理,以及它如何帮助我们根据天气条件做出更好的高尔夫球预测。我们将逐一检查每种验证方法。
保留法
保留法是检验我们模型效果的最基础方法。在这些方法中,我们基本上将一部分数据专门用于测试。
训练-测试拆分
这种方法很简单:我们将数据分成两部分。我们使用一部分来训练模型,另一部分来测试模型。在分割数据之前,我们会随机打乱数据顺序,以确保原始数据的顺序不会影响结果。
训练集和测试集的大小取决于我们的总数据集大小,通常用它们的比例来表示。为了确定它们的大小,您可以遵循以下指导原则:
-
对于小型数据集(大约 1,000–10,000 个样本),使用 80:20 的比例。
-
对于中等规模的数据集(大约 10,000–100,000 个样本),使用 70:30 的比例。
-
大型数据集(超过 100,000 个样本),使用 90:10 的比例。
from sklearn.model_selection import train_test_split
### Simple Train-Test Split ###
# Split data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Train and evaluate
dt.fit(X_train, y_train)
test_accuracy = dt.score(X_test, y_test)
# Plot
plt.figure(figsize=(5, 5), dpi=300)
plot_tree(dt, feature_names=X.columns, filled=True, rounded=True)
plt.title(f'Train-Test Split (Test Accuracy: {test_accuracy:.3f})')
plt.tight_layout()
这种方法很容易使用,但也有一些局限性 —— 结果可能会因为我们如何随机分割数据而有很大变化。这就是为什么我们总是需要尝试不同的random_state
来确保结果的一致性。此外,如果我们起初的数据不多,可能没有足够的数据来充分训练或测试我们的模型。
训练-验证-测试拆分
这种方法将数据分为三部分。中间部分,称为验证数据,用来调整模型的参数,我们的目标是尽量减少该部分的误差。
由于在调整过程中会多次考虑验证结果,我们的模型可能会在验证数据上表现得太好(这正是我们想要的)。这就是我们为什么要设立单独的测试集的原因。我们只在最后一次测试它 —— 它能真实地反映出我们的模型效果如何。
以下是常见的数据拆分方式:
-
对于较小的数据集(1,000–10,000 个样本),使用 60:20:20 的比例。
-
对于中等规模的数据集(10,000–100,000 个样本),使用 70:15:15 的比例。
-
大型数据集(>100,000 个样本),使用 80:10:10 的比例。
### Train-Validation-Test Split ###
# First split: separate test set
X_temp, X_test, y_temp, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Second split: separate validation set
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp, test_size=0.25, random_state=42
)
# Train and evaluate
dt.fit(X_train, y_train)
val_accuracy = dt.score(X_val, y_val)
test_accuracy = dt.score(X_test, y_test)
# Plot
plt.figure(figsize=(5, 5), dpi=300)
plot_tree(dt, feature_names=X.columns, filled=True, rounded=True)
plt.title(f'Train-Val-Test Split\nValidation Accuracy: {val_accuracy:.3f}'
f'\nTest Accuracy: {test_accuracy:.3f}')
plt.tight_layout()
保留法根据数据量的不同会有不同的表现。当你有大量数据(>100,000 个样本)时,它效果很好。但当你数据较少(<1,000 个样本)时,这种方法可能不是最理想的。在数据较少的情况下,你可能需要使用更高级的验证方法,以便更好地了解你的模型到底有多有效。
📊 转向交叉验证
我们刚刚了解到,留出法可能在小数据集上效果不佳。这正是我们目前面临的挑战——我们只有 28 天的数据。按照留出法原则,我们将保留 14 天的数据作为最终测试数据。这样,我们剩下 14 天的数据可以用于尝试其他验证方法。
# Initial train-test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, shuffle=False)
在接下来的部分,我们将看到交叉验证方法如何将这 14 天的数据多次划分,并以不同的方式进行测试。这让我们即使在数据有限的情况下,也能更好地了解模型的实际效果。
交叉验证
交叉验证改变了我们测试模型的方式。我们不再仅仅用一种数据划分方式测试一次模型,而是通过多次使用相同数据的不同划分来进行测试。这有助于我们更好地理解模型的实际表现。
交叉验证的主要思想是多次测试我们的模型,每次的训练集和测试集都来自我们数据的不同部分。这有助于避免由于数据划分极端(如特别好或特别差)而带来的偏差。
这为什么很重要呢?假设我们的模型在某次测试中得到 95%的准确率,而在另一种测试方法下只得到 75%的准确率,哪一个结果才是真正反映模型表现的呢?交叉验证通过提供多个测试结果,而不仅仅是一个,帮助我们回答这个问题。这让我们更清楚地了解模型的实际表现。
K 折法
基础 K 折交叉验证 K折交叉验证解决了基本数据划分方法的一个大问题:过于依赖单一的数据划分方式。与其只进行一次数据划分,K折将数据划分成K个相等的部分。然后,它多次测试模型,每次使用不同的部分进行测试,而其他部分则用于训练。
我们选择的K数值会影响我们如何测试模型。大多数人使用 5 或 10 作为K,但这个数值也可以根据我们拥有的数据量和项目需求来调整。假设我们使用K = 3。这意味着我们将数据分成三等份。然后我们将模型训练和测试三次。每次,2/3 的数据用于训练,1/3 的数据用于测试,但每次测试时,所用的测试部分都会不同。这样,每个数据片段都会同时用于训练和测试。
from sklearn.model_selection import KFold, cross_val_score
# Cross-validation strategy
cv = KFold(n_splits=3, shuffle=True, random_state=42)
# Calculate cross-validation scores
scores = cross_val_score(dt, X_train, y_train, cv=cv)
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Plot trees for each split
plt.figure(figsize=(4, 3.5*cv.get_n_splits(X_train)))
for i, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train)):
# Train and visualize the tree for this split
dt.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
plt.subplot(cv.get_n_splits(X_train), 1, i+1)
plot_tree(dt, feature_names=X_train.columns, impurity=False, filled=True, rounded=True)
plt.title(f'Split {i+1} (Validation Accuracy: {scores[i]:.3f})\nTrain indices: {train_idx}\nValidation indices: {val_idx}')
plt.tight_layout()
验证准确率: 0.433 ± 0.047
当我们完成所有轮次后,我们会计算所有K测试的平均表现。这个平均值为我们提供了一个更可靠的衡量标准,来评估我们的模型表现如何。我们还可以通过观察不同测试轮次之间结果的变化,来了解我们的模型有多稳定。
分层 K 折 基本的 K 折交叉验证通常效果不错,但当我们的数据不平衡时——即某些类型的数据比其他类型多得多——它可能会遇到问题。例如,如果我们有 100 个数据点,其中 90 个是 A 类型,而只有 10 个是 B 类型,随机划分这些数据可能会导致某些划分中没有足够的 B 类型数据来进行合理的测试。
分层 K 折交叉验证通过确保每个数据划分与原始数据的分布相同来解决这个问题。如果我们的完整数据集中有 10% 是 B 类型,那么每个划分也将包含大约 10% 的 B 类型数据。这使得我们的测试更加可靠,特别是在某些数据类型比其他类型稀少时。
from sklearn.model_selection import StratifiedKFold, cross_val_score
# Cross-validation strategy
cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)
# Calculate cross-validation scores
scores = cross_val_score(dt, X_train, y_train, cv=cv)
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Plot trees for each split
plt.figure(figsize=(5, 4*cv.get_n_splits(X_train)))
for i, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train)):
# Train and visualize the tree for this split
dt.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
plt.subplot(cv.get_n_splits(X_train), 1, i+1)
plot_tree(dt, feature_names=X_train.columns, impurity=False, filled=True, rounded=True)
plt.title(f'Split {i+1} (Validation Accuracy: {scores[i]:.3f})\nTrain indices: {train_idx}\nValidation indices: {val_idx}')
plt.tight_layout()
验证准确率:0.650 ± 0.071
保持这种平衡有两个好处。首先,它确保每个划分能够恰当地代表我们数据的分布。其次,它使得我们的测试结果更加一致。这意味着,如果我们多次测试模型,我们很可能每次都会得到类似的结果。
重复 K 折 有时,即使我们使用了 K 折验证,测试结果在不同的随机划分之间也可能发生较大的变化。重复 K 折通过多次运行整个 K 折过程来解决这个问题,每次使用不同的随机划分。
例如,假设我们运行 5 折交叉验证三次。这意味着我们的模型总共会进行 15 次训练和测试。通过如此多次的测试,我们可以更好地判断结果中的差异是来自随机因素,还是能真正反映出模型的性能。缺点是,所有这些额外的测试需要更多的时间来完成。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/f2b033a8e6b90cc1bad5a07059d1457a.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/53d8b614cdb52f63a8289ec002c7dce5.png
from sklearn.model_selection import RepeatedKFold
# Cross-validation strategy
n_splits = 3
cv = RepeatedKFold(n_splits=n_splits, n_repeats=2, random_state=42)
# Calculate cross-validation scores
scores = cross_val_score(dt, X_train, y_train, cv=cv)
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Plot trees for each split
total_splits = cv.get_n_splits(X_train) # Will be 6 (3 folds × 2 repetitions)
plt.figure(figsize=(5, 4*total_splits))
for i, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train)):
# Train and visualize the tree for this split
dt.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
# Calculate repetition and fold numbers
repetition, fold = i // n_splits + 1, i % n_splits + 1
plt.subplot(total_splits, 1, i+1)
plot_tree(dt, feature_names=X_train.columns, impurity=False, filled=True, rounded=True)
plt.title(f'Split {repetition}.{fold} (Validation Accuracy: {scores[i]:.3f})\n'
f'Train indices: {list(train_idx)}\n'
f'Validation indices: {list(val_idx)}')
plt.tight_layout()
验证准确率:0.425 ± 0.107
当我们查看重复 K 折结果时,由于我们有很多组测试结果,我们可以做的不仅仅是计算平均值——我们还可以了解我们对结果的信心。这使我们更好地理解模型的可靠性。
重复分层 K 折 这种方法结合了我们刚刚学习的两件事:保持类别平衡(分层)和进行多轮测试(重复)。它在测试多次的同时保持了不同类型数据的正确比例。这在我们的数据集较小且不平衡时尤其有效——例如,当我们有大量一种类型的数据,而其他类型的数据较少时。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/f9084cb48d717d1a53b287556171438e.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/9736bf44bf4d82ce511033183bcb338a.png
from sklearn.model_selection import RepeatedStratifiedKFold
# Cross-validation strategy
n_splits = 3
cv = RepeatedStratifiedKFold(n_splits=n_splits, n_repeats=2, random_state=42)
# Calculate cross-validation scores
scores = cross_val_score(dt, X_train, y_train, cv=cv)
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Plot trees for each split
total_splits = cv.get_n_splits(X_train) # Will be 6 (3 folds × 2 repetitions)
plt.figure(figsize=(5, 4*total_splits))
for i, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train)):
# Train and visualize the tree for this split
dt.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
# Calculate repetition and fold numbers
repetition, fold = i // n_splits + 1, i % n_splits + 1
plt.subplot(total_splits, 1, i+1)
plot_tree(dt, feature_names=X_train.columns, impurity=False, filled=True, rounded=True)
plt.title(f'Split {repetition}.{fold} (Validation Accuracy: {scores[i]:.3f})\n'
f'Train indices: {list(train_idx)}\n'
f'Validation indices: {list(val_idx)}')
plt.tight_layout()
验证准确率:0.542 ± 0.167
然而,这种方法有一个权衡:它需要更多的时间来运行。每次我们重复整个过程时,训练模型所需的时间会成倍增加。在决定是否使用这种方法时,我们需要考虑,是否值得花费额外的时间来获得更可靠的结果。
分组 K 折交叉验证 有时,我们的数据自然分为一些应该保持在一起的组。例如,高尔夫数据中,我们可能有来自同一个高尔夫球场的多次测量数据。如果我们将来自一个高尔夫球场的部分测量数据放入训练数据,而其他的放入测试数据,就会出现问题:我们的模型可能会在训练过程中间接了解测试数据,因为它看到了来自同一球场的其他测量数据。
分组 K 折交叉验证通过保持来自同一组的数据(例如来自同一高尔夫球场的所有测量数据)一起划分,来解决这一问题。这可以防止我们的模型在训练过程中无意中看到不应该看到的信息,从而让我们误以为它表现得比实际情况更好。
# Create groups
groups = ['Group 1', 'Group 4', 'Group 5', 'Group 3', 'Group 1', 'Group 2', 'Group 4',
'Group 2', 'Group 6', 'Group 3', 'Group 6', 'Group 5', 'Group 1', 'Group 4',
'Group 4', 'Group 3', 'Group 1', 'Group 5', 'Group 6', 'Group 2', 'Group 4',
'Group 5', 'Group 1', 'Group 4', 'Group 5', 'Group 5', 'Group 2', 'Group 6']
# Simple Train-Test Split
X_train, X_test, y_train, y_test, groups_train, groups_test = train_test_split(
X, y, groups, test_size=0.5, shuffle=False
)
# Cross-validation strategy
cv = GroupKFold(n_splits=3)
# Calculate cross-validation scores
scores = cross_val_score(dt, X_train, y_train, cv=cv.split(X_train, y_train, groups=groups_train))
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Plot trees for each split
plt.figure(figsize=(4, 3.5*cv.get_n_splits(X_train)))
for i, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train, groups=groups_train)):
# Get the groups for this split
train_groups = sorted(set(np.array(groups_train)[train_idx]))
val_groups = sorted(set(np.array(groups_train)[val_idx]))
# Train and visualize the tree for this split
dt.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
plt.subplot(cv.get_n_splits(X_train), 1, i+1)
plot_tree(dt, feature_names=X_train.columns, impurity=False, filled=True, rounded=True)
plt.title(f'Split {i+1} (Validation Accuracy: {scores[i]:.3f})\n'
f'Train indices: {train_idx} ({", ".join(train_groups)})\n'
f'Validation indices: {val_idx} ({", ".join(val_groups)})')
plt.tight_layout()
验证准确度: 0.417 ± 0.143
当我们处理自然分组的数据时,这种方法尤其重要,比如来自同一个高尔夫球场的多次天气数据,或者同一地点在不同时间收集的数据。
时间序列划分 当我们在常规的 K 折交叉验证中随机划分数据时,我们假设每个数据点不会影响其他数据点。但这对于随时间变化的数据并不适用,因为过去发生的事情会影响未来的结果。时间序列划分通过调整 K 折交叉验证,更好地处理这种时间顺序数据。
时间序列划分并非随机分割数据,而是按顺序使用数据,从过去到未来。训练数据仅包括测试数据之前的时间段的信息。这与我们在现实生活中使用模型的方式一致,即我们利用过去的数据来预测未来的事件。
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
# Cross-validation strategy
cv = TimeSeriesSplit(n_splits=3)
# Calculate cross-validation scores
scores = cross_val_score(dt, X_train, y_train, cv=cv)
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Plot trees for each split
plt.figure(figsize=(4, 3.5*cv.get_n_splits(X_train)))
for i, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train)):
# Train and visualize the tree for this split
dt.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
plt.subplot(cv.get_n_splits(X_train), 1, i+1)
plot_tree(dt, feature_names=X_train.columns, impurity=False, filled=True, rounded=True)
plt.title(f'Split {i+1} (Validation Accuracy: {scores[i]:.3f})\n'
f'Train indices: {train_idx}\n'
f'Validation indices: {val_idx}')
plt.tight_layout()
验证准确度: 0.556 ± 0.157
例如,假设K=3,并且我们有高尔夫数据。我们可以使用一月和二月的天气数据训练,来预测三月的高尔夫打球模式。接着,我们使用一月到三月的数据来预测四月,依此类推。通过只向前推进时间,这种方法能更真实地反映我们的模型在预测基于天气的未来高尔夫打球模式时的表现。
留出法
留一交叉验证 (LOOCV) 留一交叉验证 (LOOCV) 是最彻底的验证方法。它仅使用一个样本进行测试,其他所有样本用于训练。验证会重复进行,直到每一条数据都被用作测试。
假设我们有 100 天的高尔夫天气数据。LOOCV 会训练并测试模型 100 次。每次,它使用 99 天的数据进行训练,1 天的数据进行测试。这种方法消除了测试中的任何随机性——如果你多次在相同的数据上运行 LOOCV,你将始终得到相同的结果。
然而,LOOCV 需要很长的计算时间。如果你有N个数据点,你需要训练模型N次。对于大型数据集或复杂模型,这可能需要的时间太长,无法实际使用。一些简单的模型,如线性模型,有一些捷径使得 LOOCV 变得更快,但并不是所有模型都适用。
from sklearn.model_selection import LeaveOneOut
# Cross-validation strategy
cv = LeaveOneOut()
# Calculate cross-validation scores
scores = cross_val_score(dt, X_train, y_train, cv=cv)
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Plot trees for each split
plt.figure(figsize=(4, 3.5*cv.get_n_splits(X_train)))
for i, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train)):
# Train and visualize the tree for this split
dt.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
plt.subplot(cv.get_n_splits(X_train), 1, i+1)
plot_tree(dt, feature_names=X_train.columns, impurity=False, filled=True, rounded=True)
plt.title(f'Split {i+1} (Validation Accuracy: {scores[i]:.3f})\n'
f'Train indices: {train_idx}\n'
f'Validation indices: {val_idx}')
plt.tight_layout()
验证准确率:0.429 ± 0.495
LOOCV 在数据量不多,需要最大限度利用每一份数据时表现得非常好。由于结果依赖于每一条数据,如果数据中有噪声或异常值,结果可能会有很大变化。
Leave-P-Out 交叉验证 Leave-P-Out 基于 Leave-One-Out 的思想,但它每次测试时使用 P 个数据点,而不是仅测试一个数据点。这在 Leave-One-Out 和 K-fold 验证之间创造了平衡。我们选择的 P 值会改变模型的测试方式以及所需的时间。
Leave-P-Out 的主要问题是可能的测试组合数量增长得非常快。例如,如果我们有 100 天的高尔夫天气数据,并且每次测试 5 天(P=5),那么选择这 5 天的方式有数百万种不同的组合。当数据量很大或 P 值较大时,测试所有这些组合会耗费大量时间。
from sklearn.model_selection import LeavePOut, cross_val_score
# Cross-validation strategy
cv = LeavePOut(p=3)
# Calculate cross-validation scores (using all splits for accuracy)
scores = cross_val_score(dt, X_train, y_train, cv=cv)
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Plot first 15 trees
n_trees = 15
plt.figure(figsize=(4, 3.5*n_trees))
for i, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train)):
if i >= n_trees:
break
# Train and visualize the tree for this split
dt.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
plt.subplot(n_trees, 1, i+1)
plot_tree(dt, feature_names=X_train.columns, impurity=False, filled=True, rounded=True)
plt.title(f'Split {i+1} (Validation Accuracy: {scores[i]:.3f})\n'
f'Train indices: {train_idx}\n'
f'Validation indices: {val_idx}')
plt.tight_layout()
验证准确率:0.441 ± 0.254
由于这些实际限制,Leave-P-Out 通常用于需要非常彻底测试且数据集足够小以使其可行的特殊情况。它在研究项目中尤其有用,在这些项目中,获取最准确的测试结果比测试所需的时间更为重要。
随机方法
ShuffleSplit 交叉验证 ShuffleSplit 与其他验证方法不同,它采用完全随机的分割方式。与 K-fold 按有序方式划分数据,或像 Leave-P-Out 那样测试所有可能的组合不同,ShuffleSplit 每次都会创建随机的训练和测试分割。
ShuffleSplit 与 K-fold 的不同之处在于,分割不遵循任何固定模式。在 K-fold 中,每条数据都恰好用于一次测试。但在 ShuffleSplit 中,一天的高尔夫天气数据可能被用于多次测试,也可能根本不被用于测试。这种随机性为我们提供了一种不同的方式来理解模型的表现。
ShuffleSplit 在大数据集上特别有效,而 K-折交叉验证可能需要花费过多时间来运行。我们可以选择测试多少次,无论数据量多大。同时,我们还可以控制每次划分的大小。这让我们能够在全面测试和运行时间之间找到一个良好的平衡。
from sklearn.model_selection import ShuffleSplit, train_test_split
# Cross-validation strategy
cv = ShuffleSplit(n_splits=3, test_size=0.2, random_state=41)
# Calculate cross-validation scores
scores = cross_val_score(dt, X_train, y_train, cv=cv)
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Plot trees for each split
plt.figure(figsize=(4, 3.5*cv.get_n_splits(X_train)))
for i, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train)):
# Train and visualize the tree for this split
dt.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
plt.subplot(cv.get_n_splits(X_train), 1, i+1)
plot_tree(dt, feature_names=X_train.columns, impurity=False, filled=True, rounded=True)
plt.title(f'Split {i+1} (Validation Accuracy: {scores[i]:.3f})\n'
f'Train indices: {train_idx}\n'
f'Validation indices: {val_idx}')
plt.tight_layout()
验证准确率:0.333 ± 0.272
由于 ShuffleSplit 可以创建任意数量的随机划分,它在我们希望查看模型性能如何随不同的随机划分而变化,或在我们需要更多的测试以确保结果的可靠性时非常有用。
分层 ShuffleSplit 分层 ShuffleSplit 结合了随机划分和保持不同类型数据的正确混合。像分层 K-折交叉验证一样,它确保每个划分的每种类型的数据占比与整个数据集相同。
该方法为我们提供了双赢的局面:既有随机划分的自由,又有保持数据平衡的公平性。例如,如果我们的高尔夫数据集有 70% 的“是”天和 30% 的“否”天,每个随机划分都会尽量保持这一 70-30 的比例。这在数据不均衡时尤其有用,因为随机划分可能会无意中创建不代表我们数据的测试集。
from sklearn.model_selection import StratifiedShuffleSplit, train_test_split
# Cross-validation strategy
cv = StratifiedShuffleSplit(n_splits=3, test_size=0.2, random_state=41)
# Calculate cross-validation scores
scores = cross_val_score(dt, X_train, y_train, cv=cv)
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Plot trees for each split
plt.figure(figsize=(4, 3.5*cv.get_n_splits(X_train)))
for i, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train)):
# Train and visualize the tree for this split
dt.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
plt.subplot(cv.get_n_splits(X_train), 1, i+1)
plot_tree(dt, feature_names=X_train.columns, impurity=False, filled=True, rounded=True)
plt.title(f'Split {i+1} (Validation Accuracy: {scores[i]:.3f})\n'
f'Train indices: {train_idx}\n'
f'Validation indices: {val_idx}')
plt.tight_layout()
验证准确率:0.556 ± 0.157
然而,保持划分的随机性以及数据类型的正确混合可能会很棘手。该方法有时需要在完全随机和保持完美比例之间做出一些小的妥协。在实际使用中,这些小的折衷很少会引起问题,且通常保持测试集的平衡比拥有完全随机的划分更为重要。
🌟 验证技术总结与代码总结
总结来说,模型验证方法分为两大类:留出法和交叉验证法:
留出法 · 训练-测试分割:最简单的方法,将数据分成两部分
· 训练-验证-测试分割:一种三分法用于更复杂的模型开发
交叉验证法 交叉验证法通过多轮验证更好地利用可用数据:
K-折交叉验证法 这些方法将数据分为 K 个部分,而不是一个单独的划分:
· 基本 K-折交叉验证:轮流使用不同的测试集
· 分层 K-折交叉验证:保持各个划分中的类别平衡
· 分组 K-折交叉验证:保留数据分组
· 时间序列分割:尊重时间顺序
· 重复 K-折交叉验证
· 重复分层 K-折交叉验证
留出法 这些方法将验证推向极限:
· 留 P 法:一次对 P 个数据点进行测试
· 留一法:对单个数据点进行测试
随机方法 这些方法引入了受控的随机性:
· ShuffleSplit:重复创建随机划分
· 分层 ShuffleSplit:随机划分且保持类别平衡
import pandas as pd
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import (
# Hold-out methods
train_test_split,
# K-Fold methods
KFold, # Basic k-fold
StratifiedKFold, # Maintains class balance
GroupKFold, # For grouped data
TimeSeriesSplit, # Temporal data
RepeatedKFold, # Multiple runs
RepeatedStratifiedKFold, # Multiple runs with class balance
# Leave-out methods
LeaveOneOut, # Single test point
LeavePOut, # P test points
# Random methods
ShuffleSplit, # Random train-test splits
StratifiedShuffleSplit, # Random splits with class balance
cross_val_score # Calculate validation score
)
# Load the dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rainy', 'rainy', 'rainy', 'overcast',
'sunny', 'sunny', 'rainy', 'sunny', 'overcast', 'overcast', 'rainy',
'sunny', 'overcast', 'rainy', 'sunny', 'sunny', 'rainy', 'overcast',
'rainy', 'sunny', 'overcast', 'sunny', 'overcast', 'rainy', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0,
72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0,
88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0,
90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0,
65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True,
True, False, True, True, False, False, True, False, True, True, False,
True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes',
'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes',
'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)
# Data preprocessing
df = pd.DataFrame(dataset_dict)
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
df['Wind'] = df['Wind'].astype(int)
# Set the label
X, y = df.drop('Play', axis=1), df['Play']
## Simple Train-Test Split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.5, shuffle=False,
)
## Train-Test-Validation Split
# First split: separate test set
# X_temp, X_test, y_temp, y_test = train_test_split(
# X, y, test_size=0.2, random_state=42
# )
# Second split: separate validation set
# X_train, X_val, y_train, y_val = train_test_split(
# X_temp, y_temp, test_size=0.25, random_state=42
# )
# Create model
dt = DecisionTreeClassifier(random_state=42)
# Select validation method
#cv = KFold(n_splits=3, shuffle=True, random_state=42)
#cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)
#cv = GroupKFold(n_splits=3) # Requires groups parameter
#cv = TimeSeriesSplit(n_splits=3)
#cv = RepeatedKFold(n_splits=3, n_repeats=2, random_state=42)
#cv = RepeatedStratifiedKFold(n_splits=3, n_repeats=2, random_state=42)
cv = LeaveOneOut()
#cv = LeavePOut(p=3)
#cv = ShuffleSplit(n_splits=3, test_size=0.2, random_state=42)
#cv = StratifiedShuffleSplit(n_splits=3, test_size=0.3, random_state=42)
# Calculate and print scores
scores = cross_val_score(dt, X_train, y_train, cv=cv)
print(f"Validation accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
# Final Fit & Test
dt.fit(X_train, y_train)
test_accuracy = dt.score(X_test, y_test)
print(f"Test accuracy: {test_accuracy:.3f}")
验证准确率: 0.429 ± 0.495
测试准确率: 0.714
对上述结果的评论: 验证准确率和测试准确率之间的巨大差距,以及验证分数中非常高的标准差,表明我们的模型表现不稳定。这种不一致性很可能来源于在我们的小型天气数据集上使用 LeaveOneOut 验证——在单个数据点上进行测试导致性能剧烈波动。使用较大的验证集的不同验证方法可能会给我们带来更可靠的结果。
选择正确的验证方法
选择如何验证模型并不简单——不同的情况需要不同的方法。理解使用哪种方法可能意味着获得可靠结果或误导性结果之间的差异。以下是选择验证方法时应该考虑的一些方面:
1. 数据集大小
数据集的大小对选择哪种验证方法最有效有很大的影响。让我们来看一下不同大小的数据集:
大数据集(超过 100,000 个样本)
当你有大量数据集时,测试所需的时间成为主要考虑因素之一。简单的保留验证(将数据一次性分为训练集和测试集)通常效果不错,因为你有足够的数据进行可靠的测试。如果需要使用交叉验证,使用 3 折或使用 ShuffleSplit 进行较少轮次的验证可以在不花费太多时间的情况下获得良好的结果。
中等数据集(1,000 到 100,000 个样本)
对于中等大小的数据集,常规的 K 折交叉验证效果最佳。使用 5 折或 10 折可以在可靠结果和合理的计算时间之间取得良好的平衡。这种数据量通常足以创建具有代表性的划分,而不会使得测试时间过长。
小型数据集(少于 1,000 个样本)
小型数据集,例如我们 28 天高尔夫记录的例子,需要更仔细的测试。在这种情况下,Leave-One-Out 交叉验证或重复 K 折交叉验证(使用更多折数)实际上可以很好地工作。尽管这些方法的运行时间较长,但在数据量不大的情况下,它们帮助我们获得最可靠的结果。
2. 计算资源
在选择验证方法时,我们需要考虑计算资源。在数据集大小、模型复杂度和所使用的验证方法之间存在三方面的平衡:
快速训练模型
像决策树、逻辑回归和线性 SVM 这样的简单模型可以使用更彻底的验证方法,如 Leave-One-Out 交叉验证或重复分层 K 折交叉验证,因为它们训练速度较快。由于每轮训练只需几秒钟或几分钟,我们可以承受多次验证迭代。即使是使用 N 轮训练的 LOOCV,也可能对这些算法来说是可行的。
资源密集型模型
深度神经网络、拥有大量树的随机森林或梯度提升模型的训练时间较长。在使用这些模型时,更加密集的验证方法,如重复 K 折交叉验证或 Leave-P-Out,可能不太实际。我们可能需要选择更简单的方法,如基本的 K 折交叉验证或 ShuffleSplit,以保持合理的测试时间。
内存考虑因素
一些方法,如 K 折交叉验证,需要同时跟踪多个数据划分。ShuffleSplit 可以帮助解决内存限制问题,因为它一次只处理一个随机划分。对于具有复杂模型(如需要大量内存的深度神经网络)的大规模数据集,可能需要使用更简单的保留方法。如果我们在内存有限的情况下仍需要彻底的验证,可以使用时间序列划分,因为它自然地按顺序处理数据,而不需要一次性将所有划分存储在内存中。
当资源有限时,使用一个我们可以顺利运行的更简单的验证方法(例如基本的 K 折交叉验证)比尝试运行一个我们无法完成的更复杂方法(例如 Leave-P-Out)要好。
3. 类别分布
类别不平衡会强烈影响我们应该如何验证模型。对于不平衡数据,分层验证方法变得至关重要。像分层 K 折交叉验证和分层 ShuffleSplit 这样的方式确保每个测试划分与完整数据集的类别分布大致相同。如果不使用这些分层方法,一些测试集可能完全没有某个类别,这样就无法正确测试模型的预测效果。
4. 时间序列
当处理随时间变化的数据时,我们需要特殊的验证方法。常规的随机划分方法效果不佳,因为时间顺序很重要。对于时间序列数据,我们必须使用像时间序列划分(Time Series Split)这样的方式,尊重时间顺序。
5. 群组依赖
许多数据集包含自然的相关数据组。在验证模型时,这些数据中的连接需要特殊处理。当数据点相关时,我们需要使用像 Group K-fold 这样的方式,以防止我们的模型错误地学习到不该学习的东西。
实用指南
这张流程图将帮助你为你的数据选择最合适的验证方法。下面的步骤概述了一个清晰的选择最佳验证方法的过程,前提是你有足够的计算资源。
最后的备注
模型验证对于构建可靠的机器学习模型至关重要。在探索了许多验证方法,从简单的训练-测试划分到复杂的交叉验证方法后,我们发现,总有一种适合你的数据的验证方法。
虽然机器学习在不断变化,出现了新的方法和工具,但这些基本的验证规则始终不变。当你很好地理解这些原则时,我相信你会建立起人们可以信任和依赖的模型。
深入阅读
关于验证方法的详细解释,读者可以参考官方文档,里面提供了全面的使用和参数说明。
技术环境
本文使用的是 Python 3.7 和 scikit-learn 1.5。尽管所讨论的概念通常适用,但具体的代码实现可能会因版本不同而有所变化。
关于插图
除非另有说明,否则所有图片均由作者创作,并结合了 Canva Pro 的授权设计元素。
𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝙈𝙤𝙙𝙚𝙡 𝙀𝙫𝙖𝙡𝙪𝙖𝙩𝙞𝙤𝙣 & 𝙊𝙥𝙩𝙞𝙢𝙞𝙯𝙖𝙩𝙞𝙤𝙣 𝙢𝙚𝙩𝙝𝙤𝙙𝙨 𝙝𝙚𝙧𝙚:
模型评估与优化
查看列表3 个故事https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/18fa82b1435fa7d5571ee54ae93a6c62.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c95e89d05d1de700c631c342cd008de0.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/30e20e1a8ba3ced1e77644b706acd18d.png
𝙔𝙤𝙪 𝙢𝙞𝙜𝙝𝙩 𝙖𝙡𝙨𝙤 𝙡𝙞𝙠𝙚:
分类算法
查看列表8 个故事https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/f95c1a80b88fe6220b18cd3b2a83a30d.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/6ea70d9d2d9456e0c221388dbb253be8.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/7221f0777228e7bcf08c1adb44a8eb76.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/835013c69e08fec04ad9ca465c2adf6c.png
集成学习
查看列表4 个故事https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/1bd2995b5cb6dcc956ceadadc5ee3036.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/22a5d43568e70222eb89fd36789a9333.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/8ea1a2f29053080a5feffc709f5b8669.png
使用马尔可夫链建模 DAU
如何使用 Duolingo 的增长模型预测 DAU 并控制预测结果
https://medium.com/@wowone?source=post_page---byline--640ea4fddeb4--------------------------------https://towardsdatascience.com/?source=post_page---byline--640ea4fddeb4-------------------------------- Vladimir Kukushkin
·发表于 Towards Data Science ·阅读时间:21 分钟·2024 年 12 月 2 日
–
1. 引言
毫无疑问,DAU、WAU 和 MAU——每日、每周和每月的 活跃用户——是关键的业务指标。由 Duolingo 前 CPO Jorge Mazal 撰写的文章 “How Duolingo reignited user growth” 位列 Lenny’s Newsletter 博客增长板块的第一篇文章。在这篇文章中,Jorge 特别关注了 Duolingo 如何建模 DAU 指标的方法(参见另一篇文章 “Meaningful metrics: how data sharpened the focus of product teams” 由 Erin Gustafson 撰写)。这种方法有多个优点,但我想重点介绍如何使用这种方法进行 DAU 预测。
新的一年即将来临,许多公司目前正在为明年的预算进行规划。成本估算通常需要 DAU 预测。在本文中,我将展示如何使用 Duolingo 的增长模型进行预测。我还将解释为什么这种方法相比于标准的时间序列预测方法更为优越,以及如何根据团队的计划(例如市场营销、激活、产品团队)调整预测结果。
本文内容与代码相结合,并附有模拟数据集,研究结果完全可复现。Jupyter notebook 版本可以在 这里 查看。最后,我将分享一个以 Google Spreadsheet 格式设计的 DAU “计算器”。
我将以“我们”的身份叙述,就像我们在一起讨论一样。
2. 方法论
简要回顾一下Duolingo 的增长模型是如何工作的。在用户生命周期的第 d 天(d = 1, 2, …)中,用户可以处于以下 7 种(互斥)状态之一:new
、current
、reactivated
、resurrected
、at_risk_wau
、at_risk_mau
、dormant
。这些状态是根据用户今天、过去 7 天或过去 30 天是否活跃来定义的。定义摘要如下表所示:
在定义了这些状态(作为集合 S)后,我们可以将用户行为视为一个马尔科夫链。以下是用户轨迹的一个例子:new
→ current
→ current
→ at_risk_wau
→…→ at_risk_mau
→…→ dormant
。设 M 为与此马尔科夫过程相关的转移矩阵:m_{i, j} = P(s_j | s_i) 表示用户从状态 s_i 转移到状态 s_j 的概率,其中 s_i, s_j ∈ S。这样一个矩阵是从历史数据中推断出来的。
如果我们假设用户行为是平稳的(与时间无关),那么矩阵 M 完全描述了未来所有用户的状态。假设长度为 7 的向量 u_0 包含给定一天(记为第 0 天)在各状态下的用户计数。根据马尔科夫模型,在第二天第 1 天,我们预计会有以下数量的用户状态 u_1:
通过递归应用此公式,我们可以推导出未来任意天 t > 0 时,处于某些状态的用户数量。
除了初始分布 u_0 外,我们还需要提供未来每天将会出现的新用户数量。我们将把这个问题作为一般的时间序列预测问题来处理。
现在,计算出 u_t 后,我们可以确定第 t 天的 DAU 值:
DAU_t = #New_t + #Current_t + #Reactivated_t + #Resurrected_t
此外,我们可以轻松计算 WAU 和 MAU 指标:
WAU_t = DAU_t + #AtRiskWau_t,
MAU_t = DAU_t + #AtRiskWau_t + #AtRiskMau_t。
最后,这里是算法大纲:
-
对于每个预测日 t = 1, …, T,计算预期的新用户数量 #New_1, …, #New_T。
-
对于每个用户的每个生命周期天,分配其中一个 7 个状态。
-
从历史数据中计算转移矩阵 M。
-
计算初始状态计数 u_0 对应于 t=0 的第 t 天。
-
递归地计算 u_{t+1} = M^T * u_t。
-
计算每个预测日 t = 1, …, T 的 DAU、WAU 和 MAU。
3. 实现
本节内容专注于实现的技术方面。如果你更感兴趣的是研究模型的属性,而非代码,可以跳过本节,直接查看第四部分。
3.1 数据集
我们使用基于 SaaS 应用历史数据的模拟数据集。数据存储在dau_data.csv.gz文件中,包含三列:user_id
、date
和registration_date
。每条记录表示一个用户活跃的日期。数据集包括来自2020-11-01
到2023-10-31
的 51480 个用户的活动指标。此外,还包括 2020 年 10 月的数据,以正确计算用户状态,因为at_risk_mau
和dormant
状态需要前一个月的数据。
import pandas as pd
df = pd.read_csv('dau_data.csv.gz', compression='gzip')
df['date'] = pd.to_datetime(df['date'])
df['registration_date'] = pd.to_datetime(df['registration_date'])
print(f'Shape: {df.shape}')
print(f'Total users: {df['user_id'].nunique()}')
print(f'Data range: [{df['date'].min()}, {df['date'].max()}]')
df.head()
Shape: (667236, 3)
Total users: 51480
Data range: [2020-10-01 00:00:00, 2023-10-31 00:00:00]
这就是 DAU 时间序列的样子。
df.groupby('date').size()\
.plot(title='DAU, historical')
假设今天是 2023 年 10 月 31 日,我们想预测 2024 年接下来的 DAU 指标。我们定义了两个全局常量PREDICTION_START
和PREDICTION_END
,它们包含了预测期间。
PREDICTION_START = '2023-11-01'
PREDICTION_END = '2024-12-31'
3.2 预测新用户数量
从新用户预测开始。我们使用prophet库作为预测时间序列数据的最简单方法之一。new_users
系列包含这样的数据。我们从原始的df
数据集中提取它,选择registration date
等于date
的行。
new_users = df[df['date'] == df['registration_date']]\
.groupby('date').size()
new_users.head()
date
2020-10-01 4
2020-10-02 4
2020-10-03 3
2020-10-04 4
2020-10-05 8
dtype: int64
prophet
要求时间序列以包含ds
和y
两列的 DataFrame 格式提供,因此我们将new_users
系列重新格式化为new_users_prophet
DataFrame。我们还需要准备的是创建future
变量,包含用于预测的某些日期:从prediction_start
到prediction_end
。这个逻辑在predict_new_users
函数中实现。下面的图表展示了过去和未来时期的预测。
import logging
import matplotlib.pyplot as plt
from prophet import Prophet
# suppress prophet logs
logging.getLogger('prophet').setLevel(logging.WARNING)
logging.getLogger('cmdstanpy').disabled=True
def predict_new_users(prediction_start, prediction_end, new_users_train, show_plot=True):
"""
Forecasts a time-seires for new users
Parameters
----------
prediction_start : str
Date in YYYY-MM-DD format.
prediction_end : str
Date in YYYY-MM-DD format.
new_users_train : pandas.Series
Historical data for the time-series preceding the prediction period.
show_plot : boolean, default=True
If True, a chart with the train and predicted time-series values is displayed.
Returns
-------
pandas.Series
Series containing the predicted values.
"""
m = Prophet()
new_users_train = new_users_train\
.loc[new_users_train.index < prediction_start]
new_users_prophet = pd.DataFrame({
'ds': new_users_train.index,
'y': new_users_train.values
})
m.fit(new_users_prophet)
periods = len(pd.date_range(prediction_start, prediction_end))
future = m.make_future_dataframe(periods=periods)
new_users_pred = m.predict(future)
if show_plot:
m.plot(new_users_pred)
plt.title('New users prediction');
new_users_pred = new_users_pred\
.assign(yhat=lambda _df: _df['yhat'].astype(int))\
.rename(columns={'ds': 'date', 'yhat': 'count'})\
.set_index('date')\
.clip(lower=0)\
['count']
return new_users_pred
new_users_pred = predict_new_users(PREDICTION_START, PREDICTION_END, new_users)
new_users_pred
系列存储预测的新用户数量。
new_users_pred.tail(5)
date
2024-12-27 52
2024-12-28 56
2024-12-29 71
2024-12-30 79
2024-12-31 74
Name: count, dtype: int64
3.3 获取状态
在实际操作中,最合理的计算方法是通过 SQL 查询在存储数据的数据库中执行。接下来,我们将使用duckdb库模拟这种查询。
我们希望为每个用户在应用中的生命周期中的每一天分配 7 种状态之一。根据定义,对于每一天,我们需要考虑至少过去 30 天的数据。这时,SQL 窗口函数发挥了作用。然而,由于df
数据只包含活跃的日期记录,我们需要明确扩展它们,包含用户未活跃的日期。换句话说,而不是像这样一组记录:
user_id date registration_date
1234567 2023-01-01 2023-01-01
1234567 2023-01-03 2023-01-01
我们想得到这样的一个列表:
user_id date is_active registration_date
1234567 2023-01-01 TRUE 2023-01-01
1234567 2023-01-02 FALSE 2023-01-01
1234567 2023-01-03 TRUE 2023-01-01
1234567 2023-01-04 FALSE 2023-01-01
1234567 2023-01-05 FALSE 2023-01-01
... ... ... ...
1234567 2023-10-31 FALSE 2023-01-01
为了提高可读性,我们将以下 SQL 查询拆分成多个子查询。
-
full_range
:为每个用户创建一个完整的日期序列。 -
dau_full
:获取包含活跃和非活跃记录的完整列表。 -
states
:为每个用户生命周期的每一天分配 7 种状态之一。
import duckdb
DATASET_START = '2020-11-01'
DATASET_END = '2023-10-31'
OBSERVATION_START = '2020-10-01'
query = f"""
WITH
full_range AS (
SELECT
user_id, UNNEST(generate_series(greatest(registration_date, '{OBSERVATION_START}'), date '{DATASET_END}', INTERVAL 1 DAY))::date AS date
FROM (
SELECT DISTINCT user_id, registration_date FROM df
)
),
dau_full AS (
SELECT
fr.user_id,
fr.date,
df.date IS NOT NULL AS is_active,
registration_date
FROM full_range AS fr
LEFT JOIN df USING(user_id, date)
),
states AS (
SELECT
user_id,
date,
is_active,
first_value(registration_date IGNORE NULLS) OVER (PARTITION BY user_id ORDER BY date) AS registration_date,
SUM(is_active::int) OVER (PARTITION BY user_id ORDER BY date ROWS BETWEEN 6 PRECEDING and 1 PRECEDING) AS active_days_back_6d,
SUM(is_active::int) OVER (PARTITION BY user_id ORDER BY date ROWS BETWEEN 29 PRECEDING and 1 PRECEDING) AS active_days_back_29d,
CASE
WHEN date = registration_date THEN 'new'
WHEN is_active = TRUE AND active_days_back_6d BETWEEN 1 and 6 THEN 'current'
WHEN is_active = TRUE AND active_days_back_6d = 0 AND IFNULL(active_days_back_29d, 0) > 0 THEN 'reactivated'
WHEN is_active = TRUE AND active_days_back_6d = 0 AND IFNULL(active_days_back_29d, 0) = 0 THEN 'resurrected'
WHEN is_active = FALSE AND active_days_back_6d > 0 THEN 'at_risk_wau'
WHEN is_active = FALSE AND active_days_back_6d = 0 AND ifnull(active_days_back_29d, 0) > 0 THEN 'at_risk_mau'
ELSE 'dormant'
END AS state
FROM dau_full
)
SELECT user_id, date, state FROM states
WHERE date BETWEEN '{DATASET_START}' AND '{DATASET_END}'
ORDER BY user_id, date
"""
states = duckdb.sql(query).df()
查询结果保存在states
DataFrame 中:
3.4 计算转移矩阵
获得这些状态后,我们可以计算状态转移频率。在第 4.3 节中,我们将研究预测如何依赖于考虑转移的周期,因此预先按日聚合这些数据是合理的。结果生成的transitions
数据框包含date
、state_from
、state_to
和cnt
列。
现在,我们可以计算转移矩阵 M。我们实现了get_transition_matrix
函数,它接受transitions
数据框和一对日期,这些日期涵盖了要考虑的转移期。
作为基准,让我们计算从2022-11-01
到2023-10-31
的全年转移矩阵。
M = get_transition_matrix(transitions, '2022-11-01', '2023-10-31')
M
任何转移矩阵的每一行的和都等于 1,因为它表示从一个状态转移到任何其他状态的概率。
3.5 获取初始状态计数
初始状态是通过get_state0
函数和相应的 SQL 查询从states
数据框中检索的。该函数的唯一参数是我们想要获取初始状态的日期。我们将结果分配给state0
变量。
def get_state0(date):
query = f"""
SELECT state, count(*) AS cnt
FROM states
WHERE date = '{date}'
GROUP BY state
"""
state0 = duckdb.sql(query).df()
state0 = state0.set_index('state').reindex(states_order)['cnt']
return state0
state0 = get_state0(DATASET_END)
state0
state
new 20
current 475
reactivated 15
resurrected 19
at_risk_wau 404
at_risk_mau 1024
dormant 49523
Name: cnt, dtype: int64
3.6 预测 DAU
下面的predict_dau
函数接受所有之前预测 DAU 所需的变量,并对由start_date
和end_date
参数定义的日期范围进行预测。
def predict_dau(M, state0, start_date, end_date, new_users):
"""
Predicts DAU over a given date range.
Parameters
----------
M : pandas.DataFrame
Transition matrix representing user state changes.
state0 : pandas.Series
counts of initial state of users.
start_date : str
Start date of the prediction period in 'YYYY-MM-DD' format.
end_date : str
End date of the prediction period in 'YYYY-MM-DD' format.
new_users : int or pandas.Series
The expected amount of new users for each day between `start_date` and `end_date`.
If a Series, it should have dates as the index.
If an int, the same number is used for each day.
Returns
-------
pandas.DataFrame
DataFrame containing the predicted DAU, WAU, and MAU for each day in the date range,
with columns for different user states and tot.
"""
dates = pd.date_range(start_date, end_date)
dates.name = 'date'
dau_pred = []
new_dau = state0.copy()
for date in dates:
new_dau = (M.transpose() @ new_dau).astype(int)
if isinstance(new_users, int):
new_users_today = new_users
else:
new_users_today = new_users.astype(int).loc[date]
new_dau.loc['new'] = new_users_today
dau_pred.append(new_dau.tolist())
dau_pred = pd.DataFrame(dau_pred, index=dates, columns=states_order)
dau_pred['dau'] = dau_pred['new'] + dau_pred['current'] + dau_pred['reactivated'] + dau_pred['resurrected']
dau_pred['wau'] = dau_pred['dau'] + dau_pred['at_risk_wau']
dau_pred['mau'] = dau_pred['dau'] + dau_pred['at_risk_wau'] + dau_pred['at_risk_mau']
return dau_pred
dau_pred = predict_dau(M, state0, PREDICTION_START, PREDICTION_END, new_users_pred)
dau_pred
这就是PREDICTION_START
- PREDICTION_END
期间 DAU 预测dau_pred
的样子。除了预期的dau
、wau
和mau
列,输出还包含每个预测日期每个状态下的用户数量。
最后,我们计算 DAU、WAU 和 MAU 的真实值(以及用户状态计数),将它们保存在dau_true
数据框中,并将预测值和真实值一起绘制。
query = f"""
SELECT date, state, COUNT(*) AS cnt
FROM states
GROUP BY date, state
ORDER BY date, state;
"""
dau_true = duckdb.sql(query).df()
dau_true['date'] = pd.to_datetime(dau_true['date'])
dau_true = dau_true.pivot(index='date', columns='state', values='cnt')
dau_true['dau'] = dau_true['new'] + dau_true['current'] + dau_true['reactivated'] + dau_true['resurrected']
dau_true['wau'] = dau_true['dau'] + dau_true['at_risk_wau']
dau_true['mau'] = dau_true['dau'] + dau_true['at_risk_wau'] + dau_true['at_risk_mau']
dau_true.head()
pd.concat([dau_true['dau'], dau_pred['dau']])\
.plot(title='DAU, historical & predicted');
plt.axvline(PREDICTION_START, color='k', linestyle='--');
我们已经获得了预测结果,但到目前为止,尚不清楚它是否公正。在下一节中,我们将评估该模型。
4. 模型评估
4.1 基准模型
首先,让我们检查是否真的需要建立一个复杂的模型来预测 DAU。难道不应该通过提到的prophet
库将 DAU 作为一般时间序列来进行预测吗?下面的predict_dau_prophet
函数实现了这一点。我们尝试使用库中可用的一些调整,以使预测更准确。特别是:
-
我们使用逻辑回归模型而不是线性回归,以避免负值;
-
我们明确添加了月度和年度季节性;
-
我们去除离群值;
-
我们明确将 1 月和 2 月的高峰期定义为“假期”。
def predict_dau_prophet(prediction_start, prediction_end, dau_true, show_plot=True):
# assigning peak days for the new year
holidays = pd.DataFrame({
'holiday': 'january_spike',
'ds': pd.date_range('2022-01-01', '2022-01-31', freq='D').tolist() + \
pd.date_range('2023-01-01', '2023-01-31', freq='D').tolist(),
'lower_window': 0,
'upper_window': 40
})
m = Prophet(growth='logistic', holidays=holidays)
m.add_seasonality(name='monthly', period=30.5, fourier_order=3)
m.add_seasonality(name='yearly', period=365, fourier_order=3)
train = dau_true.loc[(dau_true.index < prediction_start) & (dau_true.index >= '2021-08-01')]
train_prophet = pd.DataFrame({'ds': train.index, 'y': train.values})
# removining outliers
train_prophet.loc[train_prophet['ds'].between('2022-06-07', '2022-06-09'), 'y'] = None
train_prophet['new_year_peak'] = (train_prophet['ds'] >= '2022-01-01') &\
(train_prophet['ds'] <= '2022-02-14')
m.add_regressor('new_year_peak')
# setting logistic upper and lower bounds
train_prophet['cap'] = dau_true.max() * 1.1
train_prophet['floor'] = 0
m.fit(train_prophet)
periods = len(pd.date_range(prediction_start, prediction_end))
future = m.make_future_dataframe(periods=periods)
future['new_year_peak'] = (future['ds'] >= '2022-01-01') & (future['ds'] <= '2022-02-14')
future['cap'] = dau_true.max() * 1.1
future['floor'] = 0
pred = m.predict(future)
if show_plot:
m.plot(pred);
# converting the predictions to an appropriate format
pred = pred\
.assign(yhat=lambda _df: _df['yhat'].astype(int))\
.rename(columns={'ds': 'date', 'yhat': 'count'})\
.set_index('date')\
.clip(lower=0)\
['count']\
.loc[lambda s: (s.index >= prediction_start) & (s.index <= prediction_end)]
return pred
代码最终变得相当复杂,说明不能简单地将prophet
应用于 DAU 时间序列。
接下来,我们测试多个预测范围的预测结果:3 个月、6 个月和 12 个月。因此,我们得到 3 个测试集:
-
3 个月预测周期:
2023-08-01
-2023-10-31
, -
6 个月预测周期:
2023-05-01
-2023-10-31
, -
1 年预测周期:
2022-11-01
-2023-10-31
。
对于每个测试集,我们计算了MAPE损失函数。
from sklearn.metrics import mean_absolute_percentage_error
mapes = []
prediction_end = '2023-10-31'
prediction_horizon = [3, 6, 12]
for offset in prediction_horizon:
prediction_start = pd.to_datetime(prediction_end) - pd.DateOffset(months=offset - 1)
prediction_start = prediction_start.replace(day=1)
prediction_end = '2023-10-31'
pred = predict_dau_prophet(prediction_start, prediction_end, dau_true['dau'], show_plot=False)
mape = mean_absolute_percentage_error(dau_true['dau'].reindex(pred.index), pred)
mapes.append(mape)
mapes = pd.DataFrame({'horizon': prediction_horizon, 'MAPE': mapes})
mapes
MAPE 误差较高:18% — 35%。最短的预测周期有最高的误差,意味着该模型主要是针对长期预测进行调优的。这是这种方法的另一个不便之处:我们必须针对每个预测周期来调整模型。不管怎样,这就是我们的基准。在下一部分,我们将与更先进的模型进行比较。
4.2 一般评估
在这一部分,我们评估了第 3.6 节中实现的模型。目前我们将过渡期设置为预测开始前的一年。我们将在第 4.3 节中研究预测如何依赖于过渡期。至于新用户,我们使用两种选项运行模型:实际值和预测值。同样地,我们固定了 3 个预测周期,并在这些周期上测试模型。
以下的make_predicion
辅助函数实现了所描述的选项。它接受prediction_start
和prediction_end
参数,定义给定预测周期的开始和结束时间,new_users_mode
可以是true
或predict
,以及transition_period
。后者参数的选项将在后文进一步解释。
import re
def make_prediction(prediction_start, prediction_end, new_users_mode='predict', transition_period='last_30d'):
prediction_start_minus_1d = pd.to_datetime(prediction_start) - pd.Timedelta('1d')
state0 = get_state0(prediction_start_minus_1d)
if new_users_mode == 'predict':
new_users_pred = predict_new_users(prediction_start, prediction_end, new_users, show_plot=False)
elif new_users_mode == 'true':
new_users_pred = new_users.copy()
if transition_period.startswith('last_'):
shift = int(re.search(r'last_(\d+)d', transition_period).group(1))
transitions_start = pd.to_datetime(prediction_start) - pd.Timedelta(shift, 'd')
M = get_transition_matrix(transitions, transitions_start, prediction_start_minus_1d)
dau_pred = predict_dau(M, state0, prediction_start, prediction_end, new_users_pred)
else:
transitions_start = pd.to_datetime(prediction_start) - pd.Timedelta(240, 'd')
M_base = get_transition_matrix(transitions, transitions_start, prediction_start_minus_1d)
dau_pred = pd.DataFrame()
month_starts = pd.date_range(prediction_start, prediction_end, freq='1MS')
N = len(month_starts)
for i, prediction_month_start in enumerate(month_starts):
prediction_month_end = pd.offsets.MonthEnd().rollforward(prediction_month_start)
transitions_month_start = prediction_month_start - pd.Timedelta('365D')
transitions_month_end = prediction_month_end - pd.Timedelta('365D')
M_seasonal = get_transition_matrix(transitions, transitions_month_start, transitions_month_end)
if transition_period == 'smoothing':
i = min(i, 12)
M = M_seasonal * i / (N - 1) + (1 - i / (N - 1)) * M_base
elif transition_period.startswith('seasonal_'):
seasonal_coef = float(re.search(r'seasonal_(0\.\d+)', transition_period).group(1))
M = seasonal_coef * M_seasonal + (1 - seasonal_coef) * M_base
dau_tmp = predict_dau(M, state0, prediction_month_start, prediction_month_end, new_users_pred)
dau_pred = pd.concat([dau_pred, dau_tmp])
state0 = dau_tmp.loc[prediction_month_end][states_order]
return dau_pred
def prediction_details(dau_true, dau_pred, show_plot=True, ax=None):
y_true = dau_true.reindex(dau_pred.index)['dau']
y_pred = dau_pred['dau']
mape = mean_absolute_percentage_error(y_true, y_pred)
if show_plot:
prediction_start = str(y_true.index.min().date())
prediction_end = str(y_true.index.max().date())
if ax is None:
y_true.plot(label='DAU true')
y_pred.plot(label='DAU pred')
plt.title(f'DAU prediction, {prediction_start} - {prediction_end}')
plt.legend()
else:
y_true.plot(label='DAU true', ax=ax)
y_pred.plot(label='DAU pred', ax=ax)
ax.set_title(f'DAU prediction, {prediction_start} - {prediction_end}')
ax.legend()
return mape
总体来说,我们有 6 个预测场景:2 个新用户选项和 3 个预测周期。下图展示了结果。左侧的图表与new_users_mode = 'predict'
选项相关,而右侧的图表与new_users_mode = 'true'
选项相关。
fig, axs = plt.subplots(3, 2, figsize=(15, 6))
mapes = []
prediction_end = '2023-10-31'
prediction_horizon = [3, 6, 12]
for i, offset in enumerate(prediction_horizon):
prediction_start = pd.to_datetime(prediction_end) - pd.DateOffset(months=offset - 1)
prediction_start = prediction_start.replace(day=1)
args = {
'prediction_start': prediction_start,
'prediction_end': prediction_end,
'transition_period': 'last_365d'
}
for j, new_users_mode in enumerate(['predict', 'true']):
args['new_users_mode'] = new_users_mode
dau_pred = make_prediction(**args)
mape = prediction_details(dau_true, dau_pred, ax=axs[i, j])
mapes.append([offset, new_users_mode, mape])
mapes = pd.DataFrame(mapes, columns=['horizon', 'new_users', 'MAPE'])
plt.tight_layout()
这里是总结预测质量的 MAPE 值:
mapes.pivot(index='horizon', columns='new_users', values='MAPE')
我们注意到了多个方面。
-
一般来说,模型表现出的结果比基准要好得多。实际上,基准模型仅基于历史的 DAU 数据,而模型则利用了用户状态信息。
-
然而,对于 1 年预测周期和
new_users_mode='predict'
,MAPE 误差非常大:65%。这个值是相应基准误差(21%)的 3 倍。另一方面,new_users_mode='true'
选项提供了一个更好的结果:8%。这意味着新用户预测对模型有着巨大的影响,特别是在长期预测中。对于较短的周期,这个差异则不那么明显。造成这种差异的主要原因是 1 年周期包括了圣诞节这一极端值。因此,i) 很难预测如此高的新用户值,ii) 这个周期对用户行为、过渡矩阵以及 DAU 值有着重大影响。因此,我们强烈建议谨慎地实现新用户预测。基准模型专门为圣诞节期间进行了调整,因此它超越了马尔可夫模型也就不足为奇了。 -
当新用户预测准确时,模型能够很好地捕捉到趋势。这意味着使用过去 365 天的数据来计算转移矩阵是一个合理的选择。
-
有趣的是,真实的新用户数据对于 3 个月的预测结果更差。这纯粹是巧合。2023 年 10 月错误的新用户预测逆转了预测的 DAU 趋势,并使得 MAPE 略有下降。
现在,让我们分解预测误差,看看哪些状态贡献最大。这里的误差指的是dau_pred
- dau_true
的值,相对误差指的是(dau_pred
- dau_true
)/ dau_true
,对应的左侧和右侧图表如下所示。为了聚焦于这一方面,我们将配置范围缩小到三个月的预测期以及new_users_mode='true'
选项。
dau_component_cols = ['new', 'current', 'reactivated', 'resurrected']
dau_pred = make_prediction('2023-08-01', '2023-10-31', new_users_mode='true', transition_period='last_365d')
figure, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
dau_pred[dau_component_cols]\
.subtract(dau_true[dau_component_cols])\
.reindex(dau_pred.index)\
.plot(title='Prediction error by state', ax=ax1)
dau_pred[['current']]\
.subtract(dau_true[['current']])\
.div(dau_true[['current']])\
.reindex(dau_pred.index)\
.plot(title='Relative prediction error (current state)', ax=ax2);
从左侧图表中我们可以看到,误差主要由current
状态贡献。这并不令人惊讶,因为这个状态对 DAU 的贡献最大。reactivated
和resurrected
状态的误差较低。另一个有趣的现象是,current
状态的误差大多为负值,而resurrected
状态的误差大多为正值。前者可能是由于在预测期内出现的新用户比过去的用户更加活跃。后者则表明,resurrected
用户实际上对 DAU 的贡献低于转移矩阵的预期,因此dormant
→resurrected
的转化率被高估了。
至于相对误差,分析current
状态的误差是有意义的。因为reactivated
和resurrected
状态的日活跃用户数量较少,所以相对误差较大且噪声较多。current
状态的相对误差介于-25%到 4%之间,这个范围相当大。由于我们已经固定了新用户的预测,这个误差只能用转移矩阵的不准确性来解释。特别是,current
→current
的转化率大致为 0.8,这个值较高,因此它对误差贡献很大。所以如果我们想改善预测,首先需要考虑调整这一转化率。
4.3 转移期的影响
在上一节中,我们固定了转移期:预测开始前 1 年。现在我们将研究为了获得更准确的预测,这一时期应该多长。我们考虑相同的预测期:3 个月、6 个月和 12 个月。为了减少新用户预测的噪声,我们使用新用户数量的实际值:new_users_mode='true'
。
这里介绍了transition_period
参数的变化。其值通过last_<N>d
模式进行遮掩,其中N
表示转移期的天数。对于每个预测期,我们计算了 12 个不同的转移期,分别为 1 个月、2 个月、…、12 个月。然后我们为每个选项计算 MAPE 误差并绘制结果。
result = []
for prediction_offset in prediction_horizon:
prediction_start = pd.to_datetime(prediction_end) - pd.DateOffset(months=prediction_offset - 1)
prediction_start = prediction_start.replace(day=1)
for transition_offset in range(1, 13):
dau_pred = make_prediction(
prediction_start, prediction_end, new_users_mode='true',
transition_period=f'last_{transition_offset*30}d'
)
mape = prediction_details(dau_true, dau_pred, show_plot=False)
result.append([prediction_offset, transition_offset, mape])
result = pd.DataFrame(result, columns=['prediction_period', 'transition_period', 'mape'])
result.pivot(index='transition_period', columns='prediction_period', values='mape')\
.plot(title='MAPE by prediction and transition period');
结果表明,最佳的转移期长度取决于预测范围。较短的预测期需要较短的转移期:对于 3、6 和 12 个月的预测,最小的 MAPE 误差分别出现在 1、4 和 8 个转移期。这显然是因为较长的预测期包含了一些只有通过较长转移期才能捕捉到的季节性效应。此外,似乎对于较长的预测期,MAPE 曲线呈 U 形,这意味着过长或过短的转移期都不利于预测。我们将在下一节展开讨论这个想法。
4.4 过时性与季节性
然而,固定一个单一的转移矩阵来预测整个未来一年的情况似乎并不是一个好主意:这样的模型过于僵化。通常,用户行为会根据季节变化。例如,在圣诞节后出现的用户可能会有行为上的变化。另一个典型的情况是用户在夏季改变他们的行为。在本节中,我们将尝试考虑这些季节性效应。
因此,我们希望预测从 2022 年 11 月开始的未来 1 年内的日活跃用户数(DAU)。我们不再仅使用一个计算自预测开始前 8 个月的单一转移矩阵M_base
,而是根据上一小节的结果(并在下面标记为last_240d
选项),将这个矩阵与一个季节性矩阵M_seasonal
结合使用。后者是按月计算的,滞后 1 年。例如,为了预测 2022 年 11 月的 DAU,我们将M_seasonal
定义为 2021 年 11 月的转移矩阵。然后,我们将预测的时间范围转移到 2022 年 12 月,并计算 2021 年 12 月的M_seasonal
,以此类推。
为了混合M_base
和M_seasonal
,我们定义了以下两个选项。
-
seasonal_0.3
:M = 0.3 *M_seasonal
+ 0.7 *M_base
。0.3 是经过一些实验后选择的局部最小值的权重。 -
smoothing
:M = i/(N-1) *M_seasonal
+ (1 - i/(N - 1)) *M_base
,其中 N 是预测期内的月份数,i = 0, …, N - 1 是月份索引。这个配置的想法是随着预测月份的推进,从最新的转移矩阵M_base
逐渐切换到季节性矩阵。
result = pd.DataFrame()
for transition_period in ['last_240d', 'seasonal_0.3', 'smoothing']:
result[transition_period] = make_prediction(
'2022-11-01', '2023-10-31',
'true',
transition_period
)['dau']
result['true'] = dau_true['dau']
result['true'] = result['true'].astype(int)
result.plot(title='DAU prediction by different transition matrices');
mape = pd.DataFrame()
for col in result.columns:
if col != 'true':
mape.loc[col, 'mape'] = mean_absolute_percentage_error(result['true'], result[col])
mape
根据 MAPE 错误,seasonal_0.3
配置提供了最佳结果。有趣的是,smoothing
方法比 last_240d
更差。从上面的图表可以看出,所有三个模型从 2023 年 7 月开始都低估了 DAU 的值,尤其是 smoothing
模型。看起来从 2023 年 7 月开始出现的新用户比 2022 年的用户更活跃。可能是应用程序得到了足够的改善,或者市场团队做得很出色。结果,smoothing
模型过度依赖 2022 年 7 月至 10 月的过时过渡数据,表现得比其他模型更差。
4.5 最终解决方案
总结一下,让我们对 2024 年做一个最终的预测。我们使用 seasonal_0.3
配置和新用户的预测值。
dau_pred = make_prediction(
PREDICTION_START, PREDICTION_END,
new_users_mode='predict',
transition_period='seasonal_0.3'
)
dau_true['dau'].plot(label='true')
dau_pred['dau'].plot(label='seasonal_0.3')
plt.title('DAU, historical & predicted')
plt.axvline(PREDICTION_START, color='k', linestyle='--')
plt.legend();
5. 讨论
在第四部分中,我们从预测准确性角度研究了模型的表现。现在让我们从实际角度讨论该模型。
除了准确性差之外,将 DAU 作为时间序列进行预测(参见第 4.1 节)使得这种方法非常僵化。本质上,它以一种方式做出预测,使得它最能拟合历史数据。实际上,在为来年做规划时,我们通常对未来有一些明确的预期。例如,
-
市场团队将推出一些更有效的新营销活动,
-
激活团队计划改进用户引导流程,
-
产品团队将发布一些新功能,以更好地吸引和留住用户。
我们的模型可以考虑到这些预期。对于上述示例,我们可以分别调整新用户的预测、new
→ current
和 current
→ current
的转化率。结果,我们可以得到一个与历史数据不完全匹配但更为现实的预测。该模型的特点不仅仅是灵活——它是可解释的。你可以轻松地与利益相关者讨论这些调整,他们也能理解预测是如何工作的。
该模型的另一个优点是它不需要预测某个用户在某一天是否会活跃。有时,二分类器会被用于这个目的。这种方法的缺点是我们需要对每个用户(包括所有休眠用户)以及预测时间范围内的每一天应用这样的分类器。这是一个巨大的计算开销。相比之下,马尔可夫模型只需要初始状态量(state0
)。此外,这类分类器通常是黑箱模型:它们难以解释且难以调整。
马尔可夫模型也有一些局限性。正如我们已经看到的,它对新用户的预测非常敏感。一个错误的新用户数量可能会完全破坏预测结果。另一个问题是,马尔可夫模型是“无记忆”的,意味着它没有考虑用户的历史。例如,它不能区分一个当前
用户是新手、老手,还是重新激活
/复活
的用户。这些用户类型的留存率显然应该不同。此外,正如我们之前讨论的,用户行为可能会因季节、营销来源、国家等因素而有所不同。到目前为止,我们的模型还无法捕捉到这些差异。然而,这可能是进一步研究的一个课题:我们可以通过为不同的用户群体拟合更多的转移矩阵来扩展该模型。
最后,正如我们在导言中承诺的,我们提供了一个DAU 电子表格计算器。在Prediction
工作表中,你需要填写初始状态分布行(标记为蓝色)和新用户预测列(标记为紫色)。在Conversions
工作表中,你可以调整转移矩阵的值。请记住,矩阵的每一行的总和应等于 1。
暂时就这些。我希望这篇文章对你有所帮助。如果你有任何问题或建议,欢迎在下面的评论区提问,或者通过LinkedIn直接联系我。
本文中的所有图片均由作者生成。