斯坦福-CS246-大数据挖掘笔记-全-

斯坦福 CS246 大数据挖掘笔记(全)

001:课程介绍;MapReduce与Spark

在本节课中,我们将学习课程的整体介绍,并探讨用于分析海量数据的核心计算工具和架构,特别是MapReduce和Spark。

课程概述与动机

欢迎来到CS246:海量数据集挖掘课程。这是一个激动人心的新季度。

如今,计算领域正经历一个激动人心的转变。历史上,计算机主要用于生成数据(例如物理模拟)。而现在,计算机首次被大规模用于分析海量数据。数据蕴含着巨大的价值和知识,我们的目标就是从数据中提取这些价值和知识。通常,我们处理的数据越多,能提取的价值和知识就越多。

数据挖掘、大数据、预测分析、数据科学、机器学习和人工智能等术语,在本质上都是这一核心理念在不同阶段的体现。这一切都得益于我们终于拥有了海量数据强大的计算能力,将两者结合就能创造奇迹。

本课程的重点在于规模。即使是当前机器学习和深度学习领域的革命,也是拥有海量数据和强大算力的结果。因此,本课程将聚焦于如何处理海量数据集,以及并行化在这些场景中的重要性。

课程内容与方法

我们可以将分析方法分为两大类:

  • 描述性方法:用于描述数据的结构,发现人类可理解的模式。例如,聚类,用于发现数据中出现的不同类别或类型。
  • 预测性方法:给定某些变量,用于预测其他变量。一个典型的例子是推荐系统,根据用户历史预测其可能喜欢的电影。

本课程融合了理论计算机科学(算法)、机器学习(统计模型)和系统(数据库管理系统)的知识。我们将使用可扩展的算法和机器学习模型,结合处理海量数据的方法,来解决实际问题。

学习目标

我们将学习如何从不同类型的数据中提取价值并进行预测:

  • 处理超高维数据
  • 处理图结构数据
  • 处理数据流(无限、永不终止的数据)。
  • 处理有标签无标签数据。

我们还将探讨支持这些数据处理的计算架构:

  • MapReduce:一种分布式计算框架,便于编写处理海量数据的程序。
  • 流式与在线算法:一次处理一个数据点,决策后即丢弃,适用于持续流入的数据。
  • 单机内存算法:经典的单机处理方法。

课程将结合这些数据类型和计算架构,解决诸多现实世界问题,例如构建推荐系统、市场篮子分析、垃圾邮件检测、网页搜索和重复文档检测等。

数学与算法工具

我们将使用多种数学和算法工具:

  • 线性代数,特别是奇异值分解,用于解决推荐系统等问题。
  • 优化方法,特别是简单但极其有用的随机梯度下降,用于大规模优化。
  • 动态规划
  • 哈希技术,特别是布隆过滤器局部敏感哈希

课程结构与期望

课程结构围绕几个核心模块组织:处理高维数据、图数据、数据流、机器学习,以及这些方法在特定领域的应用。

我们的目标是,在课程结束时,你能像一位技艺精湛的大厨,面对任何数据挑战,都能从容应对,提出合适的解决方案。

课程管理与后勤

本季度课程任务繁重,但我们拥有一支优秀的助教团队。我们将从下周开始安排答疑时间,计划每周工作日安排两次,周末安排一次,以确保为大家提供充分的支持。

课程资源

  • 课程网站:将发布讲义幻灯片、阅读材料、作业及解答。
  • 教材:《Mining of Massive Datasets》。可以购买纸质版,也可以从指定URL免费获取PDF。每讲内容都对应书中的一个章节。
  • MOOC课程:有一个基于CS246的在线开放课程,可以在那里找到相关视频。
  • 特别辅导:在课程前两周,我们将组织三次特别辅导:
    1. Spark教程:Spark是一个用于分析海量数据的分布式系统。
    2. 线性代数复习
    3. 概率统计与证明技巧复习
      所有辅导课程都会被录制并发布。

沟通方式

我们主要使用Piazza进行课程沟通。请确保登录并关注Piazza上的动态。课程通知、作业更正等都会通过Piazza发布。Piazza不仅是一个向助教提问的平台,更鼓励同学们互相解答问题,助教团队会参与并认可正确的答案。积极参与Piazza讨论将获得额外学分。

课程作业与考核

课程考核包括以下几个方面:

  • 四次大型作业:每次占总成绩的10%。第一次作业(作业0)是一个简短的Spark教程,旨在帮助大家熟悉软件环境,预计耗时1-2小时。后续作业预计每份需要约20小时完成,请务必尽早开始。
  • 每周小测验:每周会有简短的电子测验。测验在周二发布,九天后(周四午夜)截止。截止时间非常严格,系统不允许任何宽限。测验可以多次提交,系统会随机改变题目参数,最终成绩以最近一次提交为准。
  • 期末考试:占总成绩的40%,日期定在3月19日。
  • 额外学分:积极参与Piazza讨论或报告课程材料中的错误,最高可获得占总成绩1%的额外学分。

先修要求与诚信准则

我们期望学生具备较强的编程背景(Python或Java)、基础的算法知识、概率论、线性代数、多元微积分和数据库系统基础。如果你在某些方面有所欠缺,可以通过课程提供的复习材料和“即时学习”来弥补。但编程能力是必须的,因为课程最终需要编写代码。

我们将严格遵守斯坦福大学的荣誉准则。你可以讨论作业算法,但必须注明讨论对象,并且必须独立编写自己的代码。严禁抄袭他人代码或使用网络上的代码。我们将使用工具检查代码抄袭情况。

相关课程与后续机会

CS246有一个姊妹课程CS246H,每周一次讲座,深入探讨用于数据处理的分布式系统内部原理(如Hadoop文件系统、MapReduce、Spark等)。CS246H的讲座也会被录制。

课程结束后,我们提供CS341:海量数据集挖掘项目,你可以将所学知识应用于实践,在指导下进行为期10周的数据密集型研究项目。此外,我们也为感兴趣的同学提供研究助理职位。

海量数据处理架构

现在,我们开始探讨用于处理数据的大规模分布式系统

如今,大规模计算基础设施通常由通过商用网络连接的大量商用机构成,即大型数据中心。挑战在于:如何分配计算?如何简化在成千上万台机器上编写软件的过程?以及当机器故障时如何处理?

一个简单的计算是:假设一台服务器每三年故障一次(约1000天)。如果你有数千台服务器,那么预计每天会有一台故障。如果你有百万台服务器,那么每天将有约1000台机器故障。因此,软件必须具备容错能力

核心思想:计算向数据移动

网络传输数据耗时很长。传统的思路是将数据从存储位置传输到计算节点。更好的思路是将计算带到数据所在之处,即在存储文件的本地进行计算。

容错机制:数据多副本存储

为了处理故障,关键思想是将同一文件存储在多个位置(副本)。这样,即使一台机器宕机,数据在其他地方仍有备份。

SparkHadoop就是为解决这类问题而构建的系统。它们为工程师或科学家抽象了底层细节,提供了:

  1. 存储结构/文件系统(如Hadoop的HDFS)。
  2. 编程模型(如MapReduce或Spark模型)。
    程序员只需按照编程模型编写代码,无需担心文件如何分区、存储以及机器故障等问题。

分布式文件系统

为了实现持久化存储,我们使用分布式文件系统。它提供一个全局文件命名空间,但文件实际分布式存储。典型使用模式是处理巨大的文件(数百GB或TB级别),这些文件通常只写入一次,之后多次读取进行分析。

在Hadoop中,大文件被分割成多个数据块(例如64MB)。每个数据块会被复制多份,存储在不同的机器上,甚至尽可能分布在数据中心的不同位置。一个主节点(NameNode)负责存储元数据,记录所有数据块的位置。客户端库通过访问主节点来定位并获取具体数据块。这种机制确保了可靠的分布式存储。

MapReduce 编程模型

MapReduce是一种旨在简化并行编程的编程模型,能自动管理硬件和软件故障,轻松处理大规模数据。其开源实现包括Hadoop、Spark和Flink。

MapReduce程序分三步运行:

  1. Map阶段:对每个输入元素应用用户编写的map函数,输出一组键值对
  2. 分组阶段:系统自动对Map输出的键值对进行排序和混洗,将相同键的所有值分组到一起。
  3. Reduce阶段:对每个键及其对应的值列表,应用用户编写的reduce函数进行聚合。

程序员只需编写mapreduce两个函数。

实例:词频统计

假设有一个巨大的文本文档,需要统计每个单词的出现次数。

  • Map函数:读取文档中的每个单词,输出 (word, 1)
  • 分组:系统自动将所有相同的 word 键分组,形成 (word, [1, 1, 1, ...])
  • Reduce函数:对每个 word 对应的值列表求和,输出 (word, total_count)

通过将大文档分割成多个数据块,可以在不同机器上并行运行多个Map任务。所有Map任务完成后,系统进行分组,然后Reduce任务也可以并行执行。MapReduce环境自动处理数据分区、任务调度、分组、容错和机器间通信。

容错处理

  • Map任务失败:只需在存有相同数据块的其他机器上重新运行该Map任务。
  • Reduce任务失败:只需重新执行进行中的Reduce任务。

Spark:更现代的框架

Spark旨在解决MapReduce的两个主要问题:

  1. 编程模型限制:许多问题不易用MapReduce模型描述。
  2. 性能瓶颈:Hadoop MapReduce需要将中间结果持久化到磁盘,导致速度较慢,且多个作业链式执行时效率低下。

Spark是一种数据流系统,它进行了以下泛化:

  • 允许任意数量的任务阶段,而不仅是Map和Reduce。
  • 允许除Map和Reduce外的其他函数。
  • 只要数据流是单向的(有向无环图),就可以实现故障恢复。

Spark核心:弹性分布式数据集

Spark的核心抽象是弹性分布式数据集。RDD是分布在集群中的只读记录分区集合,可以缓存在内存中。RDD可以通过转换其他RDD或从Hadoop数据源创建。

RDD支持两种操作:

  • 转换:基于现有RDD创建新RDD,如 map, filter, join, union等。转换采用惰性求值,直到遇到“动作”操作时才真正计算。
  • 动作:触发计算并返回值或将数据导出,如 count, collect, reduce, save

Spark允许用户构建复杂的有向无环图数据流,所有计算在内存中进行,避免了大量的磁盘I/O,因此通常比Hadoop MapReduce快得多。

Spark生态系统

Spark拥有丰富的库,包括Spark SQL(类似SQL的查询)、MLlib(机器学习)、GraphX(图计算)和Spark Streaming(流处理)。现代数据分析栈通常由底层分布式文件系统、集群资源管理器(如Mesos)、Spark计算引擎以及上层各种应用库构成。

Spark vs. Hadoop MapReduce

  • 速度:Spark通常更快,但前提是数据能在内存中容纳。如果数据量远超内存,Spark性能会严重下降,而MapReduce由于依赖磁盘,更能处理超大规模数据。
  • 资源使用:Spark占用大量内存,可能影响集群其他任务;MapReduce资源需求更温和。
  • 易用性:Spark提供更高级的API,编程更灵活方便。
  • 通用性:Spark不仅限于MapReduce模型,支持更广泛的计算模式。

适合MapReduce的问题

适合MapReduce的问题通常具有以下特点:

  • 批量处理:对巨大文件进行顺序扫描和统计计算,例如词频统计、链接分析、构建语言模型(统计N元词组频率)。
  • 连接操作:在大型关系表之间进行等值连接,可以自然地通过Map(提取连接键)和Reduce(合并记录)来实现。

MapReduce的高效性瓶颈通常不在于计算复杂度,而在于通信成本,即产生的中间键值对的数量以及需要混洗的数据量。因此,在设计MapReduce作业时,关键目标是尽量减少中间数据的大小。

总结

本节课中,我们一起学习了CS246课程的概览、动机和后勤安排。我们深入探讨了用于海量数据处理的核心架构:从分布式文件系统和容错机制的基础,到经典的MapReduce编程模型及其词频统计实例,再到更现代、灵活的Spark框架及其核心RDD概念。我们了解了Spark与Hadoop的对比,以及哪些类型的问题适合用MapReduce范式解决。记住,在处理海量数据时,控制通信和中间数据规模往往是实现可扩展性的关键。

请记得领取课程讲义,我们周四将开始第一个正式主题的学习。

002:频繁项集与关联规则 🧺

在本节课中,我们将要学习数据挖掘中的一个核心概念:如何从海量交易数据中发现频繁出现的商品组合(频繁项集),并从中推导出有意义的关联规则(例如,“购买尿布和牛奶的顾客也倾向于购买啤酒”)。我们将从基本概念入手,逐步学习高效的算法,以应对大规模数据的挑战。

概述

关联规则挖掘的目标是识别数据中项目之间的有趣关系。其典型应用是市场篮子分析,即分析顾客购物篮中的商品组合。通过分析,我们可以优化货架摆放、进行商品推荐等。本节课将首先定义频繁项集和关联规则,然后介绍计算它们的核心算法。

1. 基本概念与定义

频繁项集

首先,我们需要理解几个核心概念。项集 指的是一组项目的集合。支持度 是衡量一个项集出现频率的指标,定义为包含该项集的交易(或“篮子”)数量。

频繁项集 是指支持度不低于用户指定阈值 s 的项集。例如,如果阈值 s=3,那么一个项集必须在至少3个交易中出现,才能被认为是频繁的。

关联规则

关联规则是一种“如果-那么”形式的规则,格式为 {I1, I2, ..., Ik} -> {J}。其含义是:如果一个交易包含了左侧项集 {I1, I2, ..., Ik} 中的所有项目,那么它也很可能包含右侧的项目 J

为了衡量规则的质量,我们引入两个指标:

  • 支持度:规则左右两侧所有项目同时出现的交易数量。它衡量了规则的普遍性。
  • 置信度:在包含左侧项集的交易中,同时也包含右侧项目的比例。它衡量了规则的可靠性。其计算公式为:
    confidence = support({I1,...,Ik, J}) / support({I1,...,Ik})

一个高置信度的规则意味着当左侧条件满足时,右侧结果出现的可能性很高。

问题定义

关联规则挖掘的正式问题是:找出所有支持度 ≥ s 且置信度 ≥ c 的关联规则,其中 s 和 c 是用户给定的阈值。

2. 关联规则挖掘的两步法

挖掘关联规则可以分解为两个主要步骤:

  1. 找出所有频繁项集:这是计算量最大、最核心的一步。一旦我们知道了所有频繁项集及其支持度,生成规则就相对简单。
  2. 从频繁项集中生成关联规则:对于一个频繁项集 I,我们可以将其划分为两个非空子集 A 和 B(A ∪ B = I, A ∩ B = ∅),从而生成形如 A -> B 的规则。该规则的置信度可以轻松计算为 support(I) / support(A)。我们只保留置信度高于阈值 c 的规则。

以下是从频繁项集生成关联规则的具体步骤:

  • 对于每一个频繁项集 I
  • 生成 I 的所有非空真子集 A
  • 对于每一个 A,输出规则 A -> (I - A)
  • 计算该规则的置信度:support(I) / support(A)
  • 如果置信度 ≥ c,则保留该规则

3. A-Priori 算法:发现频繁项集

现在,我们聚焦于最关键的步骤:如何高效地发现频繁项集。最直接的暴力方法是统计所有可能项集的出现次数,但对于海量数据(例如上百万商品),可能的项集数量是天文数字,内存根本无法容纳所有计数器。

A-Priori 算法的核心思想是利用 频繁项集的单调性如果一个项集是频繁的,那么它的所有子集也一定是频繁的。反之,如果一个项集不是频繁的,那么它的所有超集也一定不是频繁的。

基于这一原理,A-Priori 算法采用一种逐层搜索的策略:

  1. 第一遍扫描:统计每个单个项目的出现次数,找出所有频繁的1-项集(即支持度 ≥ s 的单品)。
  2. 第二遍扫描:基于频繁的1-项集,生成候选的2-项集(即由两个频繁单品组成的对)。再次扫描数据,只统计这些候选2-项集的支持度,找出频繁的2-项集。
  3. 第三遍及后续扫描:用频繁的2-项集生成候选的3-项集(生成时确保候选3-项集的每个2-项子集都是频繁的),然后扫描数据统计,找出频繁的3-项集。以此类推,直到无法生成新的频繁项集为止。

这种方法极大地减少了需要跟踪的候选项集数量,因为大量不可能是频繁的项集在早期就被修剪掉了。

4. PCY 算法:进一步优化内存使用

A-Priori 算法在第一次扫描时,大部分内存是空闲的(只用来计数单个商品)。PCY 算法巧妙地利用了这部分空闲内存。

在第一次扫描数据时,PCY 算法除了计数单个商品,还做了一件事:

  • 对于交易中的每一对商品,将其哈希到一个哈希表的某个“桶”中,并增加该桶的计数器。
  • 第一次扫描结束后,我们得到一个哈希桶计数数组。如果一个桶的计数值小于支持度阈值 s,那么这个桶就是“非频繁的”。

关键推论:如果一个商品对哈希到了一个非频繁的桶中,那么这个商品对本身绝不可能是频繁的(因为即使有哈希碰撞,该桶的总出现次数仍不足)。

在第二次扫描生成候选2-项集时,PCY 增加了一个过滤条件:一个商品对 {i, j} 成为候选,必须满足:

  1. 商品 ij 各自都是频繁的(A-Priori 的条件)。
  2. 商品对 {i, j} 哈希到的桶是频繁的。

这样,PCY 算法通过第一遍扫描收集的额外哈希信息,在第二遍扫描前进一步修剪了大量不可能成为频繁项集的候选对,从而节省了更多内存。

5. 处理超大规模数据:减少扫描次数

当数据量极大,即使两遍扫描也成本很高时,我们可以采用基于抽样的方法。

随机抽样法

  • 从整个数据集中随机抽取一个能放入内存的子样本。
  • 在内存中,对这个子样本运行 A-Priori 或 PCY 算法(注意,为了能发现潜在的频繁模式,需要按比例调低支持度阈值)。
  • 在样本中找到的频繁项集作为“候选”,最后再用整个数据集扫描一遍,验证这些候选是否真正频繁。

SON 算法

  • 将大数据集分成多个能放入内存的块。
  • 对每个数据块独立运行内存算法,找出在该块内“频繁”的项集(使用全局支持度阈值)。
  • 所有在任何一个数据块中频繁的项集,都被收集为全局候选集。
  • 最后,通过一次全局扫描,验证这些候选集在整个数据集上的支持度。

这些方法的核心思想是:一个项集要在整个数据集中频繁,它至少必须在某个子集(样本或数据块)中频繁。因此,我们可以用较少的全局扫描次数来发现所有真正的频繁项集。

总结

本节课我们一起学习了关联规则挖掘的基础。我们首先定义了频繁项集、支持度、置信度等核心概念。然后,我们深入探讨了发现频繁项集的关键算法:从基础的 A-Priori 算法开始,它利用单调性进行逐层搜索和剪枝;到更高效的 PCY 算法,它通过哈希技术进一步优化内存使用;最后,为了应对超大规模数据,我们介绍了基于抽样和分区的 SON 等方法,以减少对磁盘数据的扫描次数。这些算法是许多现代推荐系统和商业智能工具的基石。

003:局部敏感哈希

在本节课中,我们将要学习一种名为“局部敏感哈希”的强大技术。这是一种非常巧妙且极具影响力的哈希函数应用方法,能够帮助我们高效地处理海量高维数据,例如快速进行图像搜索或查找相似文档。

概述:我们面临的问题

我们正在讨论高维数据。今天我们将探讨局部敏感哈希,未来几周将讨论聚类和降维。这些技术将为我们构建现实世界的推荐系统和推荐算法奠定理论基础。

我们面临的核心任务是:如何在海量数据集中快速找到相似项?例如,在Pinterest这样的应用中,用户从图像中选择一个子部分,系统需要在包含数十亿图像的数据库中,实时找出与该子部分相似的图像。

为了实现这一点,我们首先需要将图像(或文档)表示为某种形式。通常,我们会使用深度神经网络将图像编码为一个由0和1组成的向量(嵌入)。查询时,我们将查询图像通过相同的神经网络得到其编码,然后需要计算查询向量与数据库中所有向量(例如30亿个)的相似度,并找出最相似的项。显然,我们不能线性扫描整个数据库,因为那太慢了。

因此,问题在于:如何在常数时间内(即不依赖于数据量大小)找到最近邻?

应用场景与抽象问题

这项技术有两个主要应用方向:

  1. 最近邻查询:给定一个查询点,在数据集中找到其最近邻。
  2. 全对相似搜索:在数据集中找出所有相似度超过某个阈值的对象对。

例如:

  • 谷歌:爬取整个网络后,需要找出所有近似重复的网页。
  • 推荐系统:找出与目标客户最相似的其他客户,以便进行推荐。
  • 计算机视觉:基于相似特征进行图像检索或图像补全。

我们将问题抽象如下:给定一组高维数据点和一个距离(或相似度)函数,目标是找出所有距离小于阈值 s 的数据点对。

朴素的方法是使用双重嵌套循环,计算所有点对的距离。这种方法的时间复杂度是 O(n²),对于海量数据(如10亿个点)是完全不可行的。然而,我们将看到,即使输出规模可能达到平方级,我们也能在线性时间内解决这个问题

解决方案:局部敏感哈希

我们将要学习的技术家族称为局部敏感哈希。其核心思想是:

  • 使用特殊的哈希函数将数据项(如图像、文档)哈希到不同的“桶”中。
  • 这些哈希函数被设计成具有“局部敏感性”:相似的对象有很高的概率被哈希到同一个桶中,而不相似的对象则很可能被哈希到不同的桶中
  • 这样,我们只需要检查那些被哈希到同一个桶中的对象对,它们就是潜在的相似对(候选对)。

这种方法不是完全精确的,它允许一定的误差(假阴性和假阳性),但能带来巨大的计算效率提升。


具体案例:查找相似文档

为了具体说明,我们以“查找相似文档对”为例。假设我们有100万个文档,目标是找出所有相似度超过阈值 s 的文档对。朴素方法需要约 5×10¹¹ 次比较,即使每秒能进行100万次比较,也需要5天。对于10亿个文档,时间更是不可想象。因此,我们需要更聪明的方法。

我们将通过三个步骤来解决这个问题:

  1. 分片化:将文档表示为0/1布尔向量(集合)。
  2. 最小哈希:将大型布尔向量压缩为短签名,同时保持相似性。
  3. 局部敏感哈希:使用哈希技术,聚焦于可能相似的文档对(候选对)。

以下是这三个步骤的详细说明。

第一步:分片化

分片化的目的是将文档转化为一个由元素组成的集合,通常表示为一个稀疏的0/1布尔向量。

具体做法如下:

  • 一个文档的 k-分片 是该文档中出现的连续k个标记(token)序列。标记可以是字符、单词等。为简单起见,我们假设标记是字符。
  • 我们将每个k-分片通过一个哈希函数映射为一个整数(例如4字节整数)。
  • 这样,一个文档就被表示为其所有k-分片哈希值组成的集合。

例如:
文档内容:A B C A B
2-分片(k=2):AB, BC, CA, AB
分片集合(去重后):{AB, BC, CA}

实践中,k值通常取10左右。这种表示的好处是:

  • 直观上相似的文档会有很多共同的分片。
  • 更改一个单词只会影响少数几个分片,因此即使使用不同词汇的文档也可能保持较高的相似性。

相似度度量:Jaccard相似度
在得到文档的分片集合表示后,我们使用 Jaccard相似度 来衡量文档间的相似性。

对于两个集合 C1C2,其Jaccard相似度定义为:
sim(C1, C2) = |C1 ∩ C2| / |C1 ∪ C2|

即,共同分片数除以总的不同分片数。Jaccard相似度的值在0到1之间。Jaccard距离则为 1 - sim(C1, C2)

我们可以将文档集合表示为一个巨大的0/1矩阵:

  • :所有可能的分片(或其哈希值)。
  • :各个文档。
  • 单元格值:1表示该分片出现在该文档中,0表示未出现。

在这个矩阵表示下,Jaccard相似度的计算依然成立:交集对应逻辑“与”,并集对应逻辑“或”。

第二步:最小哈希

现在,我们有了大型的稀疏布尔矩阵。直接比较所有列(文档)的代价仍然很高。因此,我们需要压缩这些列,生成短签名,同时要求签名的相似度能近似等于原始列的Jaccard相似度

这就是最小哈希技术。它是一种针对Jaccard相似度的局部敏感哈希函数。

最小哈希的定义如下:

  1. 选择一个随机排列 π,对矩阵的行进行重排。
  2. 对于每一列(文档)C,定义其哈希值 h_π(C) 为:在排列 π 的顺序下,第一个值为1的行的索引。

关键性质:
对于任意两个列 C1C2,在随机排列 π 下,它们的最小哈希值相等的概率恰好等于它们的Jaccard相似度。
Pr[h_π(C1) = h_π(C2)] = sim(C1, C2)

直观解释:
考虑两列的所有行类型:

  • A类行:两列值都为1。
  • B类行:C1为1,C2为0。
  • C类行:C1为0,C2为1。
  • D类行:两列值都为0。

当我们按随机排列顺序扫描时,第一个遇到非D类行(即至少一列为1)时停止。此时,两列哈希值相等的充要条件是,我们停在一个A类行(两列都为1)。因此,这个概率就是 A/(A+B+C),这正是Jaccard相似度的定义。

生成签名矩阵:
我们不会只使用一个哈希函数。我们会选择多个(例如100个)随机排列 π1, π2, ..., πn,对每一列计算每个排列下的最小哈希值。这样,对于每个文档,我们就得到了一个签名向量 [h_π1(C), h_π2(C), ..., h_πn(C)]

所有文档的签名向量组成了签名矩阵。签名矩阵的行数等于哈希函数个数,列数等于文档数。

签名相似度:
两个文档的签名相似度,定义为它们的签名向量中,对应位置值相等的比例。根据最小哈希的性质,这个比例的期望值等于它们原始的Jaccard相似度。使用的哈希函数越多,这个估计就越准确。

第三步:局部敏感哈希

现在,我们有了一个签名矩阵,它紧凑地保留了文档间的相似性信息。但我们的目标不是计算所有对的签名相似度(那仍然是 O(n²) 的),而是快速找出那些相似度可能超过阈值 s 的候选对。

这就是局部敏感哈希的用武之地。它是一个通用步骤,可以与任何局部敏感哈希函数(如最小哈希)结合使用。

核心思想:
我们希望通过哈希,使得相似度高的列(文档)有很大概率被哈希到同一个桶中,从而成为候选对;而相似度低的列则大概率被哈希到不同桶中。

具体方法:将签名矩阵分割成“带”和“行”。

  1. 将签名矩阵的 b 行(即使用了 b 个哈希函数)分割成 B 个“带”,每个带包含 r 行。因此有 b = B * r
  2. 对于每一个带:
    • 将该带内每个文档的 r 行签名作为一个子向量。
    • 使用一个哈希函数将这个 r 维子向量哈希到一个具有大量桶的哈希表中。
  3. 我们宣布:如果两个文档在至少一个带中被哈希到了同一个桶中,那么它们就是一个候选对

工作原理分析:
假设两个文档的Jaccard相似度为 t

  • 在单个带(包含 r 行)中,由于最小哈希的性质,两文档在该带内所有 r 行签名完全一致的概率是 t^r
  • 因此,它们在某个特定带内不一致的概率是 1 - t^r
  • 那么,它们在所有 B 个带中都不一致的概率是 (1 - t^r)^B
  • 最终,它们在至少一个带中一致(即成为候选对)的概率为:
    P(t) = 1 - (1 - t^r)^B

这个函数 P(t) 是一个S形曲线,其形状由参数 Br 决定。

参数 Br 的影响:
通过调整 B(带数)和 r(每个带的行数),我们可以控制 P(t) 曲线的形状,使其在目标相似度阈值 s 附近尽可能陡峭,像一个阶跃函数。

  • 我们希望:当 t > s(真正相似的文档对)时,P(t) 接近1(高召回率,低假阴性)。
  • t < s(不相似的文档对)时,P(t) 接近0(高精确率,低假阳性)。

权衡:

  • 增加 r(每个带行数):会使曲线向右移动,提高精确率(减少假阳性),但可能降低召回率(增加假阴性)。
  • 增加 B(带数):会使曲线向左移动,提高召回率(减少假阴性),但可能降低精确率(增加假阳性)。

在实际应用中,我们可以根据对假阳性和假阴性的容忍度来调整 Br。通常,我们可以接受一定数量的假阳性,因为可以在后续步骤中对候选对进行精确的相似度计算来过滤它们。而假阴性(漏掉的相似对)则可能永远丢失。


总结

在本节课中,我们一起学习了局部敏感哈希这一强大的技术框架,用于在海量数据集中高效地查找相似项。

  1. 分片化:我们将文档转化为k-分片集合,并用Jaccard相似度进行度量。
  2. 最小哈希:我们学习了一种针对Jaccard相似度的局部敏感哈希函数。它通过随机排列和取首个“1”行索引的方式,将大型稀疏向量压缩为短签名,并神奇地保持了相似性:Pr[h(C1)=h(C2)] = sim(C1, C2)
  3. 局部敏感哈希:我们学习了通用的“带与行”技术。通过将签名矩阵分带哈希,我们将相似度与成为候选对的概率关系从一条直线“弯曲”成一条S形曲线。通过调整参数,我们可以让这条曲线在目标阈值处变得陡峭,从而高效地筛选出高相似度的候选对,而无需比较所有数据对。

这项技术是许多现代大规模搜索和推荐系统的基石。在下节课中,我们将进一步探讨如何为其他相似度度量(如余弦相似度、欧氏距离)设计局部敏感哈希函数,并更深入地分析参数选择对性能的影响。

004:局部敏感哈希理论

在本节课中,我们将深入学习局部敏感哈希的理论基础,理解其工作原理,并探讨如何将其推广到不同的相似性度量函数上。


概述

我们将首先回顾局部敏感哈希的基本概念和流程,然后深入探讨其背后的数学原理。核心目标是理解如何通过调整参数来优化哈希函数的性能,使其能够高效地从海量数据集中找出相似项。我们还将学习如何将这一技术应用于余弦距离和欧几里得距离等不同的相似性度量。


回顾:局部敏感哈希流程

上一节我们介绍了局部敏感哈希的动机。本节中,我们来看看其标准处理流程。

局部敏感哈希旨在解决海量数据集中寻找相似项这一计算量巨大的问题。如果进行两两比较,复杂度是二次方的。局部敏感哈希通过将相似项哈希到同一个桶中,将问题复杂度降低到线性级别。

以下是标准的三步流程:

  1. Shingling:将文档表示为K-shingle(或K-gram)的集合。一个K-shingle是文档中连续出现的K个标记序列(标记可以是字符、单词等)。

    • 例如,对于文档“ABCAB”和K=2,得到的2-shingle集合是 {AB, BC, CA}。
    • 此步骤的输出是文档的集合表示。
  2. Min-Hashing:将大型的集合表示压缩为更短的“签名”向量,同时保持集合间的相似性。其核心性质是:两个集合经Min-Hash后得到相同值的概率,等于这两个集合的Jaccard相似度。

    • 公式:P(h(A) = h(B)) = Jaccard(A, B) = |A ∩ B| / |A ∪ B|
  3. 局部敏感哈希:对Min-Hash签名进行哈希,使得相似签名落入相同桶的概率高,不相似签名落入相同桶的概率低。通过将签名矩阵划分为b个波段,每个波段包含r行,我们只将那些在至少一个波段中所有r行都匹配的文档对视为候选对,进行后续详细的相似度计算。


核心:理解S曲线与参数调优

我们之前看到,如果只使用一个哈希函数,两个文档成为候选对的概率与其相似度呈线性关系,这无法有效过滤不相似的文档对。

理想情况是,我们想要一个类似阶跃函数的“S曲线”:当相似度高于某个阈值t时,成为候选对的概率接近1;当相似度低于t时,概率接近0。

局部敏感哈希通过组合多个哈希函数来逼近这个理想曲线。关键参数是波段数b和每个波段的行数r

以下是推导候选对概率的步骤:

  • 设两个文档的Jaccard相似度为 s
  • 在Min-Hash签名中,某一行两个值相等的概率是 s
  • 因此,在一个特定波段(包含r行)中,所有r行都相等的概率是 s^r
  • 那么,该波段不匹配的概率是 1 - s^r
  • 所有b个波段都不匹配的概率是 (1 - s^r)^b
  • 最终,至少有一个波段匹配(即成为候选对)的概率为:
    P(candidate) = 1 - (1 - s^r)^b

我们可以通过调整br来改变这个概率曲线的形状。


参数影响分析

让我们分析br如何影响S曲线。

  • 固定b,增加r:每个波段匹配的条件变得更严格(需要更多行一致)。这使得概率曲线向右移动,只有相似度更高的文档对才有较高概率成为候选。这减少了假正例(不相似却被视为候选),但可能增加假反例(相似却被遗漏)。
  • 固定r,增加b:有更多机会(波段)让文档对匹配。这使得概率曲线向左上方移动,更多相似度较低的文档对也可能成为候选。这减少了假反例,但增加了假正例。

因此,br的选取是一个权衡。通常,我们根据想要的相似度阈值t来选取br,使得S曲线在t附近尽可能陡峭。

假正例与假反例的权衡

  • 假正例:不相似的文档对被错误地视为候选。代价是后续进行不必要的昂贵相似度计算。
  • 假反例:相似的文档对从未被当作候选。代价是永远丢失这些相似对。
  • 在许多应用中,假反例比假正例更值得关注,因为计算资源可以增加,但丢失的数据无法找回。

理论推广:局部敏感哈希族

为了使局部敏感哈希适用于Jaccard相似度以外的度量,我们需要为其定义“局部敏感哈希族”。

一个哈希函数族H被称为是(d1, d2, p1, p2)-敏感的,如果对于任意两点xy

  1. dist(x, y) ≤ d1,则 P[h(x) = h(y)] ≥ p1
  2. dist(x, y) ≥ d2,则 P[h(x) = h(y)] ≤ p2

其中d1 < d2p1 > p2。我们希望p1尽可能接近1,p2尽可能接近0,并且d1d2尽可能接近,这样哈希函数就能清晰地区分“近”和“远”的点。

对于Min-Hash和Jaccard距离d = 1 - s,其哈希族是(1/3, 2/3, 2/3, 1/3)-敏感的。这个性质不够强,我们需要放大它。


放大哈希族:AND与OR构造

我们可以通过组合基本的哈希函数来构造新的、性能更好的哈希族。

  • AND构造:创建新哈希函数g,它由r个基本哈希函数组成。g(x) = g(y) 当且仅当这r个基本函数的结果全部相等。这使概率变为p^r

    • 效果:p1p2都减小,但p2减小得更快(因为p2 < p1)。这使曲线在右侧(高距离/低相似度区域)下降更快,减少了假正例。
  • OR构造:创建新哈希函数g,它由b个基本哈希函数组成。g(x) = g(y) 当且仅当这b个基本函数中至少有一个结果相等。这使概率变为1 - (1-p)^b

    • 效果:p1p2都增大,但p1增大得更快。这使曲线在左侧(低距离/高相似度区域)上升更快,减少了假反例。

组合使用:通过先进行r路AND构造,再进行b路OR构造(即我们之前的分b个波段,每波段r行的技术),我们可以同时放大p1和缩小p2,从而得到接近理想阶跃函数的S曲线。


应用于其他距离度量

局部敏感哈希的强大之处在于它可以适配不同的距离度量。一旦我们为某种距离定义了合适的局部敏感哈希族,剩下的流程(生成签名、分波段、哈希)是完全相同的。


余弦距离

余弦距离通过向量间的夹角来衡量差异。对于向量uv,其夹角为θ,余弦相似度为cos(θ),距离可定义为θ/π

对应的哈希族:随机超平面法

  • 生成一个随机单位向量r
  • 哈希函数定义为:h(v) = sign(r · v)。如果点积非负,结果为+1,否则为-1。
  • 核心性质P[h(u) = h(v)] = 1 - θ/π。即两个向量哈希值相同的概率等于1减去其归一化夹角。
  • 直观理解:h(v)表示向量v位于随机超平面r的哪一侧。两个向量夹角越小,它们位于超平面同一侧的概率就越大。

通过生成多个随机超平面(即多个哈希函数),我们可以得到签名向量(由+1和-1组成),然后应用标准的波段技术。


欧几里得距离

对于欧氏空间中的点,我们使用基于投影的哈希族。

对应的哈希族:随机投影法

  1. 随机选择一条直线。
  2. 将这条直线划分为若干长度为a的等宽区间(桶)。
  3. 哈希函数h(p)将点p投影到这条直线上,并返回其所在桶的索引。
  • 核心性质:如果两点距离d远小于a,则它们落入同一桶的概率至少为1 - d/a。如果d大于a,则落入同一桶的概率较低。
  • 直观理解:相近的点投影后很可能在同一个桶里,而相距较远的点很可能被分开。使用多条随机直线(多个哈希函数)可以提高稳定性。

同样,得到投影桶索引的签名后,即可使用波段技术进行处理。


总结

本节课中我们一起学习了局部敏感哈希的核心理论:

  1. 核心思想:设计哈希函数,使得相似项以高概率哈希到相同值,不相似项以低概率哈希到相同值。
  2. 关键参数:通过调整波段数b和每波段行数r,我们可以塑造S曲线的形状,在相似度阈值附近实现陡峭的过渡,从而平衡假正例和假反例。
  3. 理论框架:引入了(d1, d2, p1, p2)-敏感哈希族的概念,以及通过AND和OR构造来放大其区分能力的通用方法。
  4. 推广性:局部敏感哈希不仅适用于Jaccard相似度(Min-Hashing),通过设计不同的哈希族,也适用于余弦距离(随机超平面法)和欧几里得距离(随机投影法)等多种相似性度量。

掌握这些原理,使你能够根据具体的数据类型和相似性定义,合理设计并调优局部敏感哈希方案,从而在海量数据挖掘任务中实现高效且准确的相似性搜索。

005:聚类分析

在本节课中,我们将要学习聚类分析的基本概念和方法。聚类是一种无监督学习技术,旨在将数据点分组,使得同一组内的点彼此相似,而不同组间的点彼此相异。我们将探讨两种主要的聚类方法:层次聚类和点分配聚类,并介绍处理大规模数据集的扩展算法。

层次聚类

上一节我们介绍了聚类的核心目标,本节中我们来看看层次聚类。层次聚类通过构建一个树状图(树状图)来展示数据点如何被逐层合并或分割成簇。它有两种主要方式:自底向上(凝聚式)和自顶向下(分裂式)。

核心概念与操作

层次聚类的关键操作是反复合并两个最接近的簇。这引出了三个需要回答的问题:

  1. 如何表示一个包含多个点的簇?
  2. 如何确定两个簇之间的距离?
  3. 如何合并簇,以及何时停止合并过程?

在欧几里得空间中,一个簇通常用其质心来表示。质心是簇中所有点的平均值,其计算公式为:
$$
\text{质心} = \frac{1}{n} \sum_{i=1}^{n} \mathbf{x}_i
$$
其中,$ n $ 是簇中点的数量,$ \mathbf{x}_i $ 是每个点的坐标向量。质心是到簇内所有点平均距离最小的点。

对于非欧几里得空间(例如集合数据),我们无法计算平均值。此时,可以使用中心点。中心点是簇中一个真实的点,它到簇内所有其他点的距离之和(或其他聚合度量)最小。中心点比质心更具可解释性,因为它是一个真实存在的数据点。

簇间距离度量

确定两个簇是否“接近”需要定义簇间距离。以下是几种常见的方法:

  • 质心距离:计算两个簇质心之间的欧几里得距离。公式为:
    $$
    d_{\text{centroid}}(C_1, C_2) = |\text{centroid}(C_1) - \text{centroid}(C_2)|
    $$
  • 最近邻距离:计算两个簇中任意两点之间的最小距离。公式为:
    $$
    d_{\text{min}}(C_1, C_2) = \min_{\mathbf{x} \in C_1, \mathbf{y} \in C_2} |\mathbf{x} - \mathbf{y}|
    $$
  • 簇内凝聚性度量:也可以不直接测量距离,而是评估合并两个簇后新簇的“凝聚性”。例如,可以计算新簇的直径(簇内最远两点间的距离)或平均点对距离。如果合并导致凝聚性指标(如直径)增长过大,则可能不应合并。

选择哪种度量取决于数据的预期结构。对于形状规则、近似球形的簇,质心距离效果较好。对于形状复杂、可能相互缠绕的簇,最近邻距离通常更有效。

停止准则

层次聚类过程可以持续到所有点合并为一个簇。然而,我们通常需要确定最终的簇划分。以下是一些停止准则:

  • 指定簇数量:当合并到用户预设的 $ K $ 个簇时停止。
  • 距离阈值:当最近的两个簇之间的距离超过某个阈值时停止。
  • 凝聚性变化:当合并操作导致簇的直径或密度等凝聚性指标发生显著恶化时停止。

在实际操作中,通常会构建完整的树状图,然后根据树状图中连接的高度(代表合并时的距离)来决定在何处切割,以获得有意义的簇。

点分配聚类:K-means 算法

了解了层次聚类后,我们转向另一大类方法:点分配聚类。其中最著名的是 K-means 算法及其改进版 K-means++。K-means 假设数据存在于欧几里得空间中,并且需要预先指定簇的数量 $ K $。

算法步骤

K-means 是一个迭代优化算法,步骤如下:

  1. 初始化质心:选择 $ K $ 个点作为初始簇质心。随机选择效果不佳。K-means++ 采用了一种更好的策略:

    • 随机选择第一个质心。
    • 对于每个后续质心,选择一个新点,其被选中的概率与它到已选质心的最短距离的平方成正比。这确保了初始质心能分散在整个数据空间中。
  2. 分配点:遍历所有数据点,将每个点分配到离它最近的质心所属的簇中。最近通常指欧几里得距离最近。

  3. 更新质心:重新计算每个簇的质心(即该簇所有点的平均值)。

  4. 迭代:重复步骤 2 和 3,直到质心的位置不再发生变化(或变化很小),即算法收敛。

选择K值

K-means 需要预先指定 $ K $,但这通常未知。一种常用的方法是“肘部法则”:

  • 尝试不同的 $ K $ 值(例如从 1 到 10)。
  • 对于每个 $ K $,运行 K-means 并计算所有点到其所属簇质心的平均距离(或距离平方和)。
  • 绘制 $ K $ 值与这个平均距离的关系图。
  • 随着 $ K $ 增大,平均距离会下降。当 $ K $ 增加到真实簇数时,平均距离的下降幅度会突然变缓,图表上会出现一个“肘部”拐点。这个拐点对应的 $ K $ 值通常是较好的选择。

处理大规模数据集的扩展算法

标准的 K-means 假设数据能放入内存。对于海量数据集,我们需要更高效的算法。

BFR 算法

BFR 算法是 K-means 的变体,专为处理非常大、可能无法完全装入内存的数据集而设计。它假设簇在欧几里得空间中呈轴对齐的正态分布(即每个维度独立)。

BFR 的核心思想是用汇总统计信息来代表簇,所需内存与簇的数量成正比,而非数据点数量。它为每个簇维护三个统计量:

  • $ N $:簇中点的数量。
  • $ \text{SUM} $:一个 $ D $ 维向量,记录每个维度上所有点坐标之和。
  • $ \text{SUMSQ} $:一个 $ D $ 维向量,记录每个维度上所有点坐标的平方和。

利用这些统计量,可以轻松计算簇的质心($ \text{SUM}/N $)和每个维度上的方差。

算法流程与马氏距离

BFR 将数据点分为三类集合进行处理:

  • 废弃集:已确定属于某个簇的点,用上述统计量汇总后即可“遗忘”该点。
  • 压缩集:一些点彼此接近,但又不接近任何现有簇质心。它们被聚类成子簇,并用统计量汇总,留待后续处理。
  • 保留集:无法立即处理的孤立点,暂时保存。

判断一个点是否“足够接近”一个簇以加入废弃集,BFR 使用了马氏距离。马氏距离考虑了数据在不同维度上的方差,其计算公式为:
$$
d_{\text{Mahalanobis}}(\mathbf{x}, \mathbf{c}) = \sqrt{\sum_{i=1}^{D} \frac{(x_i - c_i)2}{\sigma_i2}}
$$
其中,$ \mathbf{c} $ 是簇质心,$ \sigma_i^2 $ 是该簇在第 $ i $ 维上的方差。马氏距离将各维度标准化,使得在数据分布较散的维度上,距离惩罚变小。如果数据簇服从正态分布,则大约 95% 的点其马氏距离在 2 个标准差(即 $ 2\sqrt{D} $ )以内。因此,可以将阈值设为此值来决定是否将点加入簇。

CURE 算法

CURE 算法旨在发现任意形状的簇,而不仅仅是球形簇。它使用一组分散的代表点来刻画一个簇的形状,而不是单个质心。

算法步骤

  1. 采样与初始聚类:从数据中随机抽取一个能放入内存的样本。对该样本使用层次聚类算法,得到初始簇划分。
  2. 选取代表点:对于每个初始簇,选取固定数量(如 4 个)分散的点作为代表点。然后,将这些代表点向簇的质心方向收缩一定比例(如 20%)。收缩操作使大而稀疏的簇收缩更多,有助于避免将孤立点误分配给它们。
  3. 分配所有点:重新扫描整个数据集。对于每个点,找到离它最近的代表点(而非质心),并将该点分配给该代表点所属的簇。

通过使用多个代表点并向中心收缩,CURE 能够更好地捕捉复杂形状的簇,并对异常值更具鲁棒性。

总结

本节课中我们一起学习了聚类分析的核心技术。我们首先介绍了层次聚类,它通过构建树状图来展示数据的层次结构,并讨论了簇表示、距离度量和停止准则。接着,我们深入探讨了点分配聚类的代表——K-means 算法,包括其步骤、初始化和如何选择 K 值。最后,为了应对海量数据集的挑战,我们学习了两种扩展算法:BFR 算法利用汇总统计和马氏距离高效处理大数据;CURE 算法则通过多个代表点来识别任意形状的簇。这些方法为从高维数据中发现有意义的结构提供了强大的工具。

006:降维 - SVD与CUR分解

在本节课中,我们将要学习两种重要的降维方法:奇异值分解(SVD)和CUR分解。我们将探讨它们如何将高维数据矩阵压缩为低维表示,理解其背后的数学原理,并比较各自的优缺点。

数据矩阵与降维动机

我们通常可以将数据表示为一个大的 M 行 N 列的矩阵。这个矩阵通常可以被一个共享较小公共维度 R 的矩阵乘积所近似。这意味着我们可以将原始的 M×N 矩阵分解为几个更小矩阵的乘积。

上一节我们介绍了降维的基本概念,本节中我们来看看具体的数学表示。

降维的直观理解

降维的核心思想是:数据虽然存在于高维空间中,但实际上可能只占据一个更低维度的流形。我们的目标是识别出这些数据真正存在的潜在维度或潜在因子。

以下是降维过程的直观步骤:

  1. 识别数据中方差最大的方向作为第一个潜在维度。
  2. 找到与第一个方向正交且方差次大的方向作为第二个潜在维度。
  3. 以此类推,直到能够充分描述数据,而剩余的方差很小。

矩阵的秩与维度

如何衡量矩阵的维度?矩阵的秩本质上衡量了其维度。矩阵的秩等于其线性无关的行(或列)的数量。如果一个矩阵的秩为 R,那么用 R 维坐标就可以精确表示它。

奇异值分解(SVD)

奇异值分解是一种将任意实数矩阵 A 唯一分解为三个特定矩阵乘积的方法。

公式表示
A = U Σ V^T

其中:

  • A:原始的 M×N 数据矩阵。
  • U:M×R 的左奇异向量矩阵,列向量是单位正交的。
  • Σ:R×R 的对角矩阵,对角线上的元素称为奇异值(σ₁, σ₂, ..., σ_R),按降序排列且均为正数。
  • V^T:R×N 的矩阵,是右奇异向量矩阵 V 的转置,其行向量也是单位正交的。

这个分解也可以写成外积和的形式:
A = Σ (σ_i * u_i * v_i^T),对 i 从 1 到 R 求和。

SVD的性质与解释

SVD分解具有唯一性。矩阵 U 和 V 的列是单位正交的,这意味着 U^T U = IV^T V = I(I 为单位矩阵)。

在应用中,我们可以这样解释:

  • U(用户-概念矩阵):描述了每个用户属于各个潜在概念(如“科幻片”、“爱情片”)的程度。
  • Σ(奇异值矩阵):表示每个潜在概念的重要性或强度。奇异值越大,该概念对数据变异的贡献越大。
  • V^T(电影-概念矩阵):描述了每部电影属于各个潜在概念的程度。

基于SVD的降维与最优性

要进行降维,我们可以将较小的奇异值设为零。例如,如果我们想将数据降至 K 维(K < R),则令 σ_{K+1} = σ_{K+2} = ... = σ_R = 0。

代码逻辑描述

# 假设已计算得到 U, Sigma, V_T
K = 2  # 目标维度
Sigma_reduced = Sigma.copy()
Sigma_reduced[K:, :] = 0  # 将第K个之后的奇异值置零
Sigma_reduced[:, K:] = 0
A_reconstructed = U @ Sigma_reduced @ V_T  # 得到降维后的近似矩阵

这样做会引入重构误差,但SVD的神奇之处在于:对于给定的目标秩 K,通过归零最小奇异值得到的近似矩阵 A_k,是在所有可能的秩为 K 的近似矩阵中,与原矩阵 A 的弗罗贝尼乌斯范数误差最小的。弗罗贝尼乌斯范数可视为矩阵间的欧几里得距离。

公式表示
||A - A_k||_F 在所有秩为 K 的矩阵 B 中是最小的。

计算SVD:幂迭代法

在实际计算中,我们可以通过计算矩阵 A^T A 的特征分解来间接得到SVD。

  1. 计算 A^T A,这是一个对称矩阵。
  2. 使用幂迭代法求其主特征向量和特征值:
    • 初始化一个随机向量 x_0
    • 迭代:x_{k+1} = (A^T A * x_k) / ||A^T A * x_k||
    • x_k 收敛时,即为主要特征向量。
    • 对应特征值 λ = x^T (A^T A) x
  3. A^T A 的特征向量就是 SVD 中的右奇异向量 V,特征值的平方根就是奇异值 σ。
  4. 类似地,通过计算 A A^T 可以得到左奇异向量 U。

专业线性代数库(如 LAPACK)有更高效稳定的算法,其复杂度约为 O(min(M, N) * max(M, N)^2)。

SVD的应用示例:查询与相似性

通过SVD,我们可以将用户和电影映射到低维概念空间。即使两个用户在原始评分空间中没有共同评分的电影,他们在概念空间中的距离也可能很近,这得益于SVD挖掘出了数据背后的相关性。

例如,一个查询“喜欢《黑客帝国》的用户”可以被映射到概念空间。另一个只给《异形》和《宁静号》高分的用户也会被映射到概念空间。虽然在原始空间他们看似无关,但在低维概念空间中他们的位置可能非常接近。

SVD的优缺点

优点

  • 最优性:在给定秩 K 的限制下,SVD能提供最小的弗罗贝尼乌斯范数重构误差。

缺点

  1. 可解释性差:奇异向量是所有原始行或列的线性组合,是抽象的数学概念,难以直接对应现实世界的具体含义。
  2. 密度性:即使输入矩阵 A 非常稀疏,分解得到的 U 和 V 矩阵也通常是稠密的,这会增加存储和计算开销。

CUR分解

为了克服SVD的缺点,特别是可解释性和稀疏性问题,我们引入CUR分解。CUR分解旨在用稀疏矩阵来近似原始矩阵。

CUR分解的原理

CUR分解将矩阵 A 近似分解为三个矩阵的乘积:A ≈ C U R

矩阵定义

  • C:从原始矩阵 A 中选取的一组列(Columns)。
  • R:从原始矩阵 A 中选取的一组行(Rows)。
  • U:一个稠密但较小的矩阵,计算自 C 和 R 的交集矩阵 W 的伪逆。

构造过程

  1. 采样列和行:不是均匀随机采样,而是依据“重要性”进行概率采样。列(或行)的重要性定义为其弗罗贝尼乌斯范数的平方(即所有元素平方和)。重要性越高,被采样的概率越大。
  2. 构造矩阵 W:取被选中的行和列的交集元素,构成一个小矩阵 W。
  3. 计算矩阵 U:计算 W 的伪逆 W⁺ 作为 U 的核心。伪逆通过计算 W 的SVD得到:若 W = X Z Y^T,则 W⁺ = Y Z⁺ X^T,其中 Z⁺ 是将 Z 对角线元素(奇异值)取倒数得到的矩阵。如果 W 可逆,则伪逆等于真逆。

CUR的理论保证与直觉

理论表明,如果选取大约 K log K 列和 K log K 行,那么CUR分解的重构误差最多是SVD最佳秩K近似误差的 (2 + ε) 倍。实践中,通常选取 4K 列和 4K 行来获得良好的秩K近似。

直观上,CUR倾向于选取范数大的列/行,这些点往往远离坐标原点,更可能代表数据变异的主要方向。与SVD寻找正交基不同,CUR的“基”就是实际的数据点,因此可能更贴合数据的实际分布。

CUR分解的优缺点

优点

  1. 可解释性强:每个“轴”(C的列或R的行)都是一个真实的原始数据点(如一部具体的电影、一位具体的科学家),易于解释。
  2. 保持稀疏性:如果原始矩阵 A 是稀疏的,那么 C 和 R 也是稀疏的,大大节省了存储空间。
  3. 计算高效:基于随机采样,计算速度非常快。

缺点

  1. 可能包含重复项:重要性高的行/列可能被多次采样。解决方法是对重复采样的行/列进行缩放。
  2. 近似误差:相对于SVD,在相同维度下误差更大,但可以通过使用更多(但稀疏的)维度来弥补。

SVD与CUR对比

考虑一个稀疏的作者-会议发表量矩阵(约50万作者×3.6万会议):

  • SVD:产生稠密的 U 和 V 矩阵,存储开销大,但精度高。
  • CUR:产生稀疏的 C 和 R 矩阵,存储效率极高。为了达到与SVD相近的精度,CUR可能需要更多的维度,但由于其稀疏性,总的非零元素存储量仍可能远小于SVD的稠密矩阵。

实验表明,在相同存储成本(空间比)下,CUR通常能达到比SVD更高的重构精度;或者说,在相同精度要求下,CUR需要存储的非零元素更少。

总结

本节课中我们一起学习了两种核心的降维技术:

  1. 奇异值分解(SVD):一种最优的数学分解方法,能给出给定秩下的最佳近似,但存在可解释性差和结果稠密的问题。
  2. CUR分解:一种基于随机采样的分解方法,通过使用原始数据行和列作为“基”,获得了优异的可解释性和稀疏性,计算效率高,在误差上接近SVD。

这两种方法为我们处理海量高维数据提供了强大的工具,下一周我们将学习它们在推荐系统等实际场景中的应用。

007:推荐系统 I

概述

在本节课中,我们将要学习推荐系统的基础知识。我们将从推荐系统的重要性讲起,然后深入探讨两种核心的推荐方法:基于内容的推荐和协同过滤。课程将涵盖它们的工作原理、优缺点以及实际应用中的考量。


推荐系统简介

推荐系统旨在向用户展示他们可能感兴趣的内容。这不仅仅是电子商务平台的核心功能,也广泛应用于新闻、视频、音乐等众多领域。

过去,我们处于信息稀缺的环境,例如实体店的货架空间有限。如今,我们进入了信息极度丰富的网络时代,平台拥有数百万甚至数亿的商品。瓶颈不再是展示空间,而是用户的认知能力。因此,我们需要通过推荐系统来过滤内容,只展示对用户最相关的信息。

一个著名的例子是两本关于登山灾难的书籍《Into Thin Air》和《Touching the Void》。后者出版更早,但销量平平。当亚马逊通过关联规则开始推荐《Touching the Void》给喜欢《Into Thin Air》的用户后,前者的销量开始飙升,最终成为畅销书。这体现了推荐系统发掘“长尾”商品、驱动销售的能力。

“长尾”理论指出,除了热门商品,还存在大量不那么流行但仍有受众的商品。例如,在亚马逊的图书销售中,长尾商品(非畅销书)贡献了超过57%的销售额。因此,一个有效的推荐系统对于挖掘长尾价值、提升平台收入至关重要。


推荐系统的问题定义与挑战

问题形式化

X 为用户集合,S 为商品集合。推荐系统的目标是学习一个效用函数 u,该函数能够预测用户 x 对商品 s 的评分 r

u: X × S → R

其中 R 是评分集合,可以是1-5星、0-1的实数,或是简单的“喜欢/不喜欢”二元信号。

核心挑战

我们通常将用户-商品评分组织成一个矩阵(效用矩阵),其中行代表用户,列代表商品。

以下是推荐系统面临的两个关键问题:

  1. 收集已知评分:让用户为商品评分是困难的。显式评分(如打星)需要用户主动参与,而用户往往不愿被打扰。隐式反馈(如购买行为、观看时长)虽然容易收集,但难以区分低评分和用户短暂注意。
  2. 推断未知评分:效用矩阵通常是极度稀疏的,我们的核心任务就是填充这些缺失值。我们更关心准确预测用户可能喜欢的高分项,而不是他们讨厌的低分项。

评估挑战

评估推荐系统比想象中更复杂。例如,在搜索引擎中,用户点击顶部结果可能出于习惯而非内容真正相关,这会给评估带来噪声。因此,我们需要设计合理的评估指标。


基于内容的推荐系统

上一节我们介绍了推荐系统的基本框架和挑战,本节中我们来看看第一种具体的实现方法:基于内容的推荐。

核心思想

基于内容的推荐系统会向用户推荐与其过去高评分商品相似的商品。它通过分析用户已评分商品的内容特征来构建用户画像,然后匹配具有相似特征的新商品。

例如,一个喜欢金属乐的用户给Metallica的专辑打了高分,系统就会推荐其他具有相似特征(如相同流派、乐队)的专辑。

工作原理

系统需要为每个商品构建一个特征档案(Item Profile),并为每个用户构建一个偏好档案(User Profile)。

  • 商品档案:由描述商品的特征组成。对于电影,可能是类型、导演、演员;对于文本,可能是关键词。
  • 用户档案:通常基于用户历史评分商品的档案加权平均得到,评分越高,权重越大。

预测用户对新商品 i 的喜好度时,可以计算用户档案 x 与商品档案 i 的余弦相似度:

cosine_sim(x, i) = (x · i) / (||x|| * ||i||)

然后,我们可以使用诸如局部敏感哈希(LSH)等技术来高效查找与用户档案最相似的商品。

特征构建示例:TF-IDF

对于文本内容,常用TF-IDF(词频-逆文档频率)来提取重要特征词。

  • 词频(TF):衡量一个词在特定文档中的重要性。
    TF(t, d) = (词 t 在文档 d 中出现的次数) / (文档 d 中的总词数)
    
  • 逆文档频率(IDF):衡量一个词的区分度。在所有文档中都出现的词(如“的”)区分度低。
    IDF(t) = log(总文档数 N / 包含词 t 的文档数)
    
  • TF-IDF:最终的特征权重。
    TF-IDF(t, d) = TF(t, d) * IDF(t)
    

文档的档案即由其所有词的TF-IDF分数向量构成。

优缺点分析

以下是基于内容推荐系统的优缺点:

优点

  • 无需其他用户数据:仅依赖用户自身的历史数据。
  • 可推荐小众商品:能向品味独特的用户推荐新的、不流行的商品。
  • 可解释性强:易于向用户解释推荐理由(例如,“因为你喜欢A和B,所以我们推荐了C”)。

缺点

  • 特征依赖:需要领域知识来定义和提取有效特征,对于多媒体内容(视频、音频)可能很困难。
  • 新用户问题:新用户没有历史数据,无法立即产生推荐(需要“冷启动”策略,如注册时选择兴趣)。
  • 过度特化:系统可能只推荐与用户已有兴趣高度相似的商品,缺乏多样性,无法发现用户潜在的新兴趣。

基于内容的系统完全忽略了其他用户的评分信息,而这部分信息可能非常有价值。接下来,我们将转向利用这些信息的协同过滤方法。


协同过滤推荐系统

上一节我们讨论了基于内容的推荐,它完全依赖于商品特征和用户自身的历史。本节中我们来看看协同过滤,它利用了“集体智慧”。

核心思想

协同过滤通过寻找与目标用户 X 评分模式相似的其他用户(邻居),然后基于这些邻居的评分来预测 X 对未评分商品的喜好。其核心假设是:如果用户 XY 在过去对某些商品的评分一致,那么他们对新商品的评分也可能一致。

用户-用户协同过滤

步骤如下:

  1. 计算用户相似度:找到与目标用户 X 最相似的用户。皮尔逊相关系数是常用的度量,因为它考虑了用户评分尺度的差异(有些用户打分严,有些打分松)。
    sim(X, Y) = Σ_{i ∈ I_XY} (r_{X,i} - avg_r_X) * (r_{Y,i} - avg_r_Y) / (σ_X * σ_Y)
    
    其中 I_XY 是用户 XY 共同评分过的商品集合,avg_r_Xavg_r_Y 是各自的平均分,σ 是标准差。
  2. 选择邻居:选取 k 个与 X 最相似的用户,记为邻居集合 N
  3. 生成预测:预测用户 X 对商品 i 的评分。一种简单方法是取邻居评分的平均值。更优的方法是进行加权平均,权重即为相似度。
    pred(X, i) = avg_r_X + [ Σ_{Y ∈ N} sim(X, Y) * (r_{Y,i} - avg_r_Y) ] / Σ_{Y ∈ N} |sim(X, Y)|
    

商品-商品协同过滤

我们也可以从商品的角度出发,寻找相似的商品。

  1. 计算商品相似度:同样使用皮尔逊相关系数,但此时计算的是商品之间的相似度(基于所有对它们都评过分用户的评分向量)。
  2. 选择邻居:对于目标商品 i,找到 k 个最相似的商品。
  3. 生成预测:预测用户 X 对商品 i 的评分,基于用户 Xi 的邻居商品的评分进行加权平均。

在实践中,商品-商品协同过滤往往比用户-用户更有效、更稳定,因为商品的属性(如类型、风格)比用户多变的口味更固定,且商品数量通常少于用户数量。

引入基线预测

为了进一步提高预测准确性,可以引入一个基线预测值,它结合了整体平均分、用户偏差和商品偏差。

baseline(X, i) = μ + b_X + b_i

其中:

  • μ 是全局平均分。
  • b_X 是用户 X 的评分偏差(X 的平均分 - μ)。
  • b_i 是商品 i 的评分偏差(i 的平均分 - μ)。

最终的预测可以在这个基线值上,加上基于邻居的调整值。

优缺点分析

以下是协同过滤推荐系统的优缺点:

优点

  • 无需内容特征:可以推荐任何类型的商品,只要存在用户评分。
  • 能发现复杂关联:可以发现用户自己可能都未察觉的潜在兴趣关联。

缺点

  • 冷启动问题
    • 新用户:没有评分历史,无法找到相似用户。
    • 新商品:没有收到任何评分,无法被推荐。
  • 数据稀疏性:在拥有海量商品和用户的平台上,两个用户共同评分过的商品可能极少,难以可靠地计算相似度。
  • 流行度偏见:系统倾向于推荐热门商品,难以向品味独特的用户推荐小众商品。

由于基于内容的推荐和协同过滤优缺点互补,在实际应用中常采用混合方法,以构建更强大的推荐系统。


推荐系统的评估与实操要点

上一节我们探讨了协同过滤的原理,本节中我们来看看如何评估推荐系统,并讨论一些工程实践中的关键点。

评估方法

评估通常在保留一部分已知评分(作为测试集)的效用矩阵上进行。

  1. 准确性指标

    • 均方根误差(RMSE):衡量预测评分与实际评分的整体偏差。计算简单,但可能无法反映真实用户体验,因为我们更关心高分项预测是否准确。
    • Top-K 精确度(Precision@K):在系统推荐的K个商品中,有多少个是用户真正喜欢的(在测试集中评分高)。这更贴近实际业务目标。
    • 排名相关性:比较系统预测的排名顺序与用户真实偏好的排名顺序是否一致。
  2. 针对隐式反馈(0/1模型)的指标

    • 覆盖率(Coverage):系统能为多大比例的用户生成预测。一个高精度但低覆盖率的系统可能不如一个精度稍低但能服务所有用户的系统。
    • 精确率/召回率与AUC:用于评估二元分类(推荐/不推荐)的性能。

  1. 超越准确性的考量
    • 多样性/惊喜度:推荐列表不应千篇一律,偶尔推荐一些用户兴趣边界之外但可能喜欢的商品,能提升用户体验和探索性。
    • 上下文感知:推荐应考虑用户当前会话的上下文(例如,刚浏览了鞋子,接着推荐裤子)。
    • 排序效果:推荐列表的排列顺序本身也影响点击率。

性能与复杂度

协同过滤中最耗时的步骤是寻找最近邻(用户或商品)。我们可以利用之前学到的技术进行优化:

  • 聚类:将相似的用户或商品预先分组。
  • 降维:使用PCA、SVD等技术减少数据维度。
  • 局部敏感哈希(LSH):高效近似查找最近邻。
    这些方法可以将计算复杂度从二次方降低到接近线性,使系统能够处理百万级用户和商品。

实践建议

一个重要的原则是:充分利用所有可用数据。简单的模型(如协同过滤)在大量数据上的表现,往往优于复杂模型在有限数据上的表现。首先确保你使用了所有可能的信息(用户行为、商品属性、上下文等),然后再考虑模型优化。


总结与下节预告

本节课中我们一起学习了推荐系统的基础。我们首先了解了推荐系统的商业价值和核心问题定义。然后,我们深入研究了两种经典方法:

  1. 基于内容的推荐:通过分析商品特征和用户历史来匹配相似商品。优点是可解释性强、能处理新商品,但受限于特征提取和过度特化。
  2. 协同过滤:利用用户群体的评分数据,通过寻找相似用户或商品来产生推荐。优点是无须特征、能发现复杂关联,但受冷启动和数据稀疏性问题困扰。

我们还讨论了评估推荐系统的多种指标和工程实践中的关键点,例如覆盖率、多样性以及利用聚类和降维技术来提升系统效率。

在下一讲(周四的课程)中,我们将介绍更强大的潜在因子模型,它通过将用户和商品映射到同一个低维潜在空间中来克服协同过滤的一些局限性。我们也会讲述著名的Netflix大奖赛的故事,该竞赛正是推动了潜在因子模型的发展,并为我们提供了许多关于构建工业级推荐系统的宝贵见解。

008:推荐系统 II

在本节课中,我们将学习推荐系统的进阶技术,特别是隐因子模型。我们将从回顾Netflix竞赛开始,深入探讨如何结合全局效应、协同过滤和矩阵分解来构建高性能的推荐引擎,并最终了解赢得百万美元大奖的解决方案。

问题背景:Netflix竞赛

几年前,Netflix发起了一项名为“Netflix Prize”的挑战赛。他们提供了约1亿条用户对电影的评分数据作为训练集。这些数据涵盖了约50万用户对1.8万部电影在2000年至2005年间的评分。

测试集则包含了约280万条评分,是每个用户最后给出的部分评分。竞赛的目标是:基于训练集构建推荐引擎,预测测试集中的评分,并最小化预测评分与真实评分之间的均方根误差

当时Netflix生产系统的RMSE为0.951。竞赛规定,任何团队若能将该误差降低10%(即达到约0.8553),即可赢得100万美元奖金。最终,超过2700支队伍参与了这场竞赛。

数据与评估

数据可以看作一个巨大的矩阵:行代表电影,列代表用户。矩阵中的每个单元格表示某个用户对某部电影的评分。我们拥有1亿个已知评分(非零单元格),但相对于矩阵的总单元格数(约90亿)来说,数据非常稀疏。

评估方式如下:我们被给予矩阵中的部分已知评分(黄色部分),需要预测被隐藏的评分(灰色部分)。通过计算预测值与真实值之间的差异来衡量性能,具体公式为均方根误差

$$
RMSE = \sqrt{\frac{1}{|Test|} \sum_{(u,i) \in Test} (r_{ui} - \hat{r}_{ui})^2}
$$

我们的目标就是最小化这个RMSE。

获胜方案概览

获胜团队名为“BellKor‘s Pragmatic Chaos”。他们的方法核心在于对数据进行多尺度建模

  1. 全局效应:捕捉整体趋势,如电影平均评分、用户平均评分。
  2. 区域效应(矩阵分解):使用隐因子模型捕捉用户和电影在潜在空间中的交互。
  3. 局部效应(协同过滤):基于相似项目进行非常局部的预测。

接下来,我们将逐一探讨这些部分,并了解如何将它们整合。

全局效应建模

全局效应旨在为预测建立一个基线。例如,预测用户Joe对电影《第六感》的评分:

  • 整个数据集的平均评分是3.7星。
  • 《第六感》的平均评分比全局平均高0.5星。
  • 用户Joe的平均评分比全局平均低0.2星。

那么,基线预测就是:3.7 + 0.5 - 0.2 = 4.0星。这可以形式化为:

$$
b_{ui} = \mu + b_u + b_i
$$

其中,$\mu$是全局平均分,$b_u$是用户$u$的偏差,$b_i$是物品$i$的偏差。

上一节我们介绍了如何建立全局基线,本节中我们来看看如何结合更局部的信息。

局部效应:基于学习的协同过滤

传统的协同过滤基于物品相似度。例如,预测用户$u$对物品$i$的评分时,会找到与$i$最相似的、且被$u$评分过的物品集合$N(i;u)$,然后进行加权平均:

$$
\hat{r}{ui} = \frac{\sum s_{ij} \cdot r_{uj}}{\sum_{j \in N(i;u)} s_{ij}}
$$

其中,$s_{ij}$是物品$i$和$j$的相似度。

然而,这种方法存在几个问题:相似度度量是任意的,且忽略了用户间的相互依赖关系。

一个改进方法是使用插值权重 $w_{ij}$ 来代替相似度 $s_{ij}$,并专注于预测与基线估计的偏差。改进后的公式如下:

$$
\hat{r}{ui} = b + \frac{\sum_{j \in N(i;u)} w_{ij} \cdot (r_{uj} - b_{uj})}{\sum_{j \in N(i;u)} w_{ij}}
$$

这里的关键转变是,我们不再直接预测绝对评分,而是预测评分相对于基线$b_{uj}$的偏差。$w_{ij}$ 是需要从数据中学习的权重,它表示物品$j$对物品$i$的影响程度。

如何学习权重 $w_{ij}$?

我们的目标是最小化训练数据上的平方误差和。因为最小化平方误差和等价于最小化RMSE(开方和除以常数都是单调变换)。因此,我们定义损失函数 $J(w)$:

$$
J(w) = \sum_{(u,i) \in Train} (r_{ui} - \hat{r}_{ui})^2
$$

将预测公式 $\hat{r}_{ui}$ 代入,我们就得到了一个关于权重 $w$ 的二次函数。我们可以使用梯度下降法来找到最小化 $J(w)$ 的 $w$ 值。

梯度下降的更新公式为:
$$
w^{\text{new}} = w^{\text{old}} - \eta \cdot \nabla J(w^{\text{old}})
$$
其中,$\eta$ 是学习率,$\nabla J$ 是梯度。

通过这种方式学到的权重 $w_{ij}$ 是基于数据优化的,而不是基于任意的相似度度量,因此通常能获得更好的预测性能。

区域效应:隐因子模型(矩阵分解)

隐因子模型的核心思想是将用户和物品映射到一个共同的低维潜在空间中。在这个空间中,用户向量与物品向量的点积可以预测评分。

我们假设评分矩阵 $R$ 可以近似分解为两个矩阵的乘积:
$$
R \approx Q \cdot P^T
$$
其中,矩阵 $Q$ 的每一行对应一个物品的隐因子向量,矩阵 $P$ 的每一行对应一个用户的隐因子向量。对于用户$u$和物品$i$,预测评分为:
$$
\hat{r}_{ui} = q_i \cdot p_u^T
$$

这类似于奇异值分解(SVD),但有一个关键区别:SVD会最小化整个矩阵(包括缺失值,被视为0)的重建误差。而在推荐系统中,我们只关心对已知评分的重建误差。因此,我们需要解决以下优化问题:

$$
\min_{P, Q} \sum_{(u,i) \in Train} (r_{ui} - q_i \cdot p_uT)2
$$

过拟合与正则化

如果隐因子数量 $k$ 设置得过大,模型可能会过度拟合训练数据,即在训练集上误差很小,但在未见过的测试集上误差很大。这种现象称为过拟合

为了防止过拟合,我们引入正则化项,修改后的目标函数为:

$$
\min_{P, Q} \sum_{(u,i) \in Train} (r_{ui} - q_i \cdot p_uT)2 + \lambda_1 \sum_i ||q_i||^2 + \lambda_2 \sum_u ||p_u||^2
$$

正则化项 $\lambda ||\cdot||^2$ 惩罚了隐因子向量的长度(L2范数)。$\lambda$ 是正则化参数,控制着拟合数据与保持模型简洁性之间的权衡。其直观解释是:对于数据丰富的用户或物品,误差项占主导,模型会尽力拟合;对于数据稀疏的用户或物品,正则化项占主导,会将其向量拉向原点(接近全局平均),做出更保守的预测。

随机梯度下降

直接使用梯度下降求解上述问题,每次更新需要遍历所有训练样本(1亿条),计算开销巨大。随机梯度下降 是更高效的选择。它的核心思想是:每次更新只基于一个或一小批(mini-batch)训练样本计算梯度噪声估计,然后进行更新。

虽然每次更新的方向可能不准确,但由于更新速度极快,总体上往往能比标准梯度下降更快地收敛到较好的解。

整合所有组件

最终的模型是前面所有组件的集成:

  1. 全局基线:$b_{ui} = \mu + b_u + b_i$
  2. 隐因子交互:$q_i \cdot p_u^T$
  3. 时间效应:将用户偏差 $b_u$ 和物品偏差 $b_i$ 建模为随时间变化的函数(例如,按时间分箱)。

因此,完整的预测公式为:
$$
\hat{r}_{ui}(t) = \mu + b_u(t) + b_i(t) + q_i \cdot p_u^T
$$

模型中的所有参数($\mu, b_u(t), b_i(t), P, Q$)都通过带有正则化的随机梯度下降联合学习得到。

Netflix竞赛的最终角逐

通过引入时间动态模型,误差可以降至0.876。为了进一步逼近目标,领先团队采用了“厨房水槽”法:他们构建了数百个不同的预测模型(包括各种变体和集成),然后将这些模型的预测结果以复杂的方式进行混合,形成最终的预测。

最终,BellKor‘s Pragmatic Chaos 团队以仅领先第二名20分钟提交的优势,达到了10.09%的改进目标,赢得了100万美元奖金。这场竞赛深刻地推动了推荐系统领域的发展,并突显了精心调整正则化、模型集成以及利用数据中时间模式的重要性。

总结与思考

本节课中我们一起学习了构建高级推荐系统的核心方法:

  1. 全局基线:建立用户和物品的偏差模型。
  2. 基于学习的协同过滤:用可优化的插值权重替代固定的相似度。
  3. 隐因子模型(矩阵分解):将用户和物品映射到潜在空间,用点积捕捉交互。
  4. 正则化:防止过拟合,确保模型泛化能力。
  5. 时间动态:将偏差参数建模为时间的函数,以捕捉趋势变化。
  6. 模型集成:结合多个模型的预测以提升性能。

Netflix竞赛表明,成功的关键在于细致的数据分析、对过拟合的控制(正则化)以及灵活地整合多种信号。在实际应用中,除了显式评分,我们还需要考虑隐式反馈(如观看完成率、订阅留存率),这些同样是构建有效推荐系统的重要标签。

009:PageRank算法详解

在本节课中,我们将要学习如何分析和计算大规模图结构中节点的重要性。我们将重点介绍PageRank算法,这是由Google创始人Larry Page和Sergey Brin提出的核心算法,用于衡量网页的重要性。我们将从图结构数据的普遍性讲起,逐步深入到PageRank的递归定义、随机游走模型、矩阵特征值问题,并最终学习如何高效地计算PageRank。

图结构数据无处不在

上一节我们介绍了课程的整体安排,本节中我们来看看什么是图结构数据。图结构数据是指可以自然地表示为图的数据,图由一组节点和连接这些节点的边组成。

以下是图结构数据的一些典型例子:

  • 社交网络:节点代表人,边代表人与人之间的关系。
  • 媒体生态系统:例如政治博客之间的链接网络,可以用于分析观点形成和两极分化。
  • 科学引文网络:节点代表科学论文或科学家,边代表引用关系,可以揭示不同科学领域之间的联系。
  • 通信与基础设施网络:如互联网、道路网、电网等,可以建模为图以进行优化和鲁棒性分析。

将万维网视为图

上一节我们看到了图在多个领域的应用,本节中我们来看看一个核心案例:万维网。我们可以将万维网视为一个巨大的图,其中节点是网页,边是网页之间的超链接。这形成了一个有向的连接网络。

早期组织网络的方式(如雅虎目录)是人工分类。而现代网络搜索面临两个核心问题:如何评估海量、不可信甚至包含垃圾信息网页的可信度;以及如何找到查询的最佳答案,即使答案页面可能不包含查询关键词。

解决这些问题的关键在于利用网络图的结构。通过分析链接,我们可以理解图中节点(网页)的重要性,这个研究领域称为链接分析。今天我们将重点讨论该领域的主要方法:PageRank

PageRank:递归定义与流方程

上一节我们提出了评估节点重要性的需求,本节中我们来看看PageRank算法的核心思想。一个直观的想法是:一个网页的重要性取决于指向它的其他网页的重要性。重要的网页投出的票更有分量。

这引出了一个递归定义:网页j的PageRank得分r_j等于所有指向j的网页i的PageRank得分之和,其中每个网页i将其重要性均分给它指向的所有网页。

用公式表示如下:
r_j = Σ_{i -> j} (r_i / d_i)
其中,d_i是网页i的出链数量。

这被称为流方程,可以想象影响力沿着边流动。每个节点从入边收集影响力,并将其均分给出边。

从流方程到矩阵方程

上一节我们得到了PageRank的流方程,本节中我们来看看如何将其转化为矩阵形式以便计算。

我们定义一个随机邻接矩阵M。如果网页id_i个出链,并且i指向j,则矩阵元素M_{ji} = 1 / d_i,否则为0。这样,矩阵M的每一列之和为1(列随机矩阵)。

我们再定义一个排名向量r,其第i个分量是网页i的PageRank得分,并且所有得分之和为1。

可以证明,流方程等价于以下矩阵方程:
r = M * r

这意味着排名向量r是矩阵M的特征值为1的特征向量。这为我们提供了另一种视角和计算方法。

随机游走解释

上一节我们得到了矩阵特征值形式的PageRank方程,本节中我们来看看一个更直观的解释:随机游走模型

想象一个随机冲浪者在网络上浏览:他从一个随机网页开始,每次在当前页面上随机点击一个链接前往下一个页面。这个过程无限进行下去。

p(t)是一个向量,其第i个分量表示冲浪者在时刻t位于网页i的概率。那么,从t时刻到t+1时刻的概率分布更新为:
p(t+1) = M * p(t)

当这个过程达到稳态,即概率分布不再随时间变化时,有 p = M * p。这正是我们之前得到的方程。因此,PageRank得分可以解释为随机冲浪者长期访问各个网页的极限概率分布。

流方程、特征向量和随机游走模型在此完美统一。

问题:终止点与陷阱

上一节我们建立了PageRank的理论模型,本节中我们来看看这个模型在真实网络图中会遇到的问题。

第一个问题是终止点:即没有出链的网页。随机游走者到达这种页面后便“无处可去”,导致重要性分数从系统中“泄漏”。

第二个问题是采集器陷阱:即一组网页,其所有出链都在组内。随机游走者一旦进入该组,便永远无法离开,最终该组会吸收所有重要性分数。

这两种情况都会导致基本的幂迭代法失效或得到不合理的结果。

解决方案:随机跳转

上一节我们指出了基本模型的问题,本节中我们来看看Google的解决方案:引入随机跳转

我们修改随机游走者的行为:在每一步,冲浪者以概率β(通常设为0.8或0.9)按照链接随机浏览;以概率1-β进行随机跳转,即瞬间跳转到网络中的任何一个随机网页(均匀分布)。

这解决了两个问题:

  • 解决采集器陷阱:冲浪者有机会跳出循环或封闭组。
  • 解决终止点:我们可以将终止点视为一个特殊的随机跳转:到达终止点后,冲浪者以概率1跳转到随机网页。

修正后的PageRank方程为:
r_j = β * Σ_{i -> j} (r_i / d_i) + (1-β) / n
其中n是网页总数。

高效的PageRank计算

上一节我们得到了修正后的PageRank方程,本节中我们来看看如何高效地计算它,尤其是在海量图上。

修正后的方程可以写成矩阵形式 r = A * r,其中 A = βM + (1-β)[1/n]。矩阵A是稠密矩阵,直接存储和计算不可行。

通过代数变换,我们可以将迭代步骤改写为:
r_new = β * M * r_old + [(1-β)/n] * 1
这里1是全1向量。这个形式非常关键,因为矩阵M是稀疏的(边数决定),我们只需要高效地计算M * r_old,然后为每个节点加上一个常数即可。

以下是完整的PageRank算法步骤:

  1. 初始化排名向量r(如均匀分布)。
  2. 重复直到收敛:
    • 计算 r'_j = β * Σ_{i -> j} (r_i / d_i) (对所有j)。
    • 计算 S = Σ_j r'_j。由于存在终止点,S可能小于1。
    • 设置 r_j = r'_j + (1 - S) / n (对所有j)。这同时处理了随机跳转和重要性泄漏。

大规模计算与数据布局

上一节我们给出了内存充足时的算法,本节中我们来看看当图大到无法全部装入内存时,如何进行计算。

假设排名向量r可以放入内存,而矩阵M存储在磁盘上。我们可以顺序读取M的每一行(源节点i及其出链列表),并根据r_old[i]的值更新所有目标节点jr_new中的值。这只需要对r_new进行随机写访问。

如果连向量r_new都无法完全装入内存,我们可以采用分块更新策略。将r_new分成若干块,每次只将一块调入内存。然后扫描整个矩阵M和向量r_old,但只更新当前内存中块对应的目标节点。这需要多次扫描数据。

更优的方法是分块-条带化更新。我们不仅将r_new分块,还将矩阵M也按目标节点重新组织成“条带”。第k个条带只包含指向第k个块中节点的边。这样,在更新第k个块时,我们只需要顺序读取第k个条带和整个r_old向量,避免了不必要的磁盘访问。每个边在整个计算过程中只被读取一次。

总结与扩展

本节课中我们一起学习了PageRank算法。我们从图结构数据的普遍性出发,探讨了衡量节点重要性的需求。PageRank通过递归定义(流方程)将节点重要性与其入链节点的重要性关联起来。

我们看到了该定义的三种等价形式:

  1. 流方程:直观的递归公式。
  2. 矩阵特征值问题r = M * r,将求解转化为寻找随机矩阵主特征向量。
  3. 随机游走模型:将PageRank解释为随机冲浪者的极限分布。

为了解决真实网络中存在的终止点和采集器陷阱问题,我们引入了带随机跳转的修正模型。最终,我们推导出了高效的幂迭代计算方法,并讨论了如何通过巧妙的数据布局(分块-条带化)在海量数据集上实现PageRank计算。

PageRank是链接分析的基础。其变体包括主题敏感PageRank(非均匀跳转分布)、HITS算法(区分枢纽和权威页面)以及TrustRank(用于对抗垃圾链接)。我们将在后续课程中探讨与网络垃圾对抗的相关内容。

010:链接垃圾与社交网络简介

在本节课中,我们将学习PageRank算法的扩展应用,包括主题特定PageRank和随机游走重启算法,并探讨如何利用这些技术构建推荐系统。最后,我们将深入研究网络上的链接垃圾问题,并学习如何使用信任排名来对抗垃圾农场。

PageRank回顾与扩展

上一节我们介绍了PageRank算法,它通过模拟随机游走者的行为来评估图中节点的重要性。随机游走者可以沿着链接浏览,也可以“传送”到图中的任意节点。我们通过构建一个随机转移矩阵,并使用幂迭代法求解其主特征向量(即平稳分布),来计算每个节点的PageRank分数。

以下是处理“死胡同”和“蜘蛛陷阱”的算法伪代码:

# 初始化所有节点的分数为 1/N
# 重复直到收敛:
#   S = beta * (从所有入链邻居处获得的分数之和)
#   # S 的总和将小于 1,因为乘以了 beta < 1
#   leaked_rank = (1 - S的总和) / N
#   每个节点的新分数 = 其获得的S分数 + leaked_rank

这个算法可以在大型图上高效运行,通常在50次迭代内收敛。

然而,标准的PageRank衡量的是节点在整个图中的通用重要性,并未考虑页面的主题。接下来,我们将探讨如何使其变得“主题特定”。

主题特定PageRank

本节中,我们来看看如何使PageRank与特定主题相关。目标是评估网页在特定主题(如体育、历史)下的重要性,而不仅仅是根据整体图结构。

其核心思想是修改随机游走者的“传送”行为。在标准PageRank中,游走者传送时会均匀地跳转到图中任何一个节点。而在主题特定PageRank中,我们定义一个“传送集”S,它只包含与特定主题相关的页面。每当游走者决定传送时,它只会跳转到S集合中的某个随机页面。

公式变化如下:
在标准PageRank的转移矩阵中,传送部分为 (1 - beta) * (1 / N),其中N是节点总数。
在主题特定PageRank中,传送部分变为 (1 - beta) * (1 / |S|),但仅对属于传送集S的节点添加此项。不属于S的节点只获得由链接转移带来的分数(乘以beta)。

因此,对于每个不同的传送集S,我们都会得到一个不同的PageRank向量R_S。这也被称为个性化PageRank。

计算过程与标准PageRank类似,只需在迭代更新分数时,确保只有传送集中的节点获得来自传送的分数增量。

以下是选择传送集(即主题)的一些方法:

  • 使用开放目录(如DMOZ)中权威页面作为主题代表。
  • 对用户查询进行分类,根据分类结果选择对应的主题传送集。
  • 根据用户历史查询、书签或当前浏览页面的上下文来推断主题。

实际应用中,我们可以为多个重要主题预计算不同的PageRank向量。当用户发起搜索时,先检索包含关键词的页面,然后使用与查询主题最相关的那个PageRank向量来对这些页面进行排序。

随机游走重启与推荐系统

上一节我们介绍了主题特定PageRank,本节我们将其推向极致:将传送集S设置为仅包含一个节点。这被称为随机游走重启。游走者总是从同一个起始节点U开始,每次传送(重启)都回到U。这样,其他节点相对于U的PageRank分数,就量化了该节点与起始节点U在图中的“接近度”或“相关性”。

这种方法能比简单的最短路径更好地衡量节点间的关联强度,因为它考虑了所有路径(包括间接路径),并考虑了节点的度数(避免高度数节点的干扰)。

这种方法在推荐系统中极其有用。例如,可以构建一个二部图,连接“用户-物品”、“作者-会议”或“论文-关键词”。通过在这个图上运行以某个节点(如用户当前感兴趣的物品)为起点的随机游走重启,我们可以找到与该节点最接近的其他节点,作为推荐候选。

一个实例:Pinterest的推荐系统
Pinterest拥有一个巨大的图:约40亿个图钉(Pin)、30亿个画板(Board)和2000亿条边(图钉被保存到画板)。其推荐系统的工作原理如下:

  1. 给定一个查询图钉(用户当前观看的)。
  2. 以该图钉为起始节点,在“图钉-画板”二部图上模拟随机游走重启。
  3. 随机游走过程:从当前图钉随机跳转到一个包含它的画板,然后从该画板随机跳转到一个新的图钉,如此反复,并以一定概率重启回查询图钉。
  4. 记录每个被访问图钉的访问次数。
  5. 返回访问次数最高的前K个图钉作为推荐结果。

这种方法之所以高效且可扩展,原因如下:

  • 局部性:随机游走倾向于停留在起始节点附近,我们只关心排名靠前的节点,因此无需遍历整个巨图。运行时间与图的大小无关,只与局部探索范围有关。
  • 实时计算:无需预计算所有节点对的接近度,可以按需为每个查询实时运行快速随机游走模拟(每秒可进行数十亿次推荐)。
  • 图剪枝:可以通过移除超高度的图钉和画板(它们会使游走过度分散)来压缩图,使其能放入单机内存(例如压缩后约200GB)。
  • 灵活性:可以引入边权重(例如偏向用户语言的内容),或使用“早停”策略(当排名第1000的节点被访问足够多次时即停止游走)。

网络垃圾与垃圾农场

现在,我们转向网络安全的另一面:链接垃圾。垃圾是指任何旨在人为提升网页在搜索引擎结果中排名的活动,这与页面的真实价值相悖。估计约有10-15%的网页是垃圾页面。

早期的垃圾主要针对术语垃圾,例如在页面背景色中隐藏大量关键词,或复制高排名页面内容。搜索引擎通过分析文本颜色、频率,以及使用锚文本(其他页面链接到该页面时使用的文字)和PageRank来对抗这类垃圾。

然而,垃圾发布者随之转向链接垃圾,即创建特殊的链接结构来人为提升目标页面的PageRank。这种结构被称为垃圾农场

垃圾农场的典型结构:

  1. 目标页面T:垃圾发布者想要提升排名的页面。
  2. 可访问页面:垃圾发布者不拥有但能发布评论和链接的页面(如博客评论区),这些页面链接到T。
  3. 自有页面:垃圾发布者完全控制的大量页面(数量为M)。T链接到所有这些自有页面,而这些自有页面又都链接回T,形成一个紧密的互链结构。

通过数学推导可以证明,这种结构能带来两大好处:

  1. 乘数效应:从可访问页面获得的PageRank贡献 x,在目标页面T处会被放大。公式近似为:PageRank(T) ≈ x / (1 - beta^2)。当beta=0.85时,乘数约为3.6。
  2. 规模效应:目标页面T的PageRank还与自有页面数M成正比。垃圾发布者拥有的页面越多,T的排名就越高。

信任排名:对抗垃圾农场

为了检测和对抗垃圾农场,我们引入信任排名。其核心思想基于“近似隔离”原理:好的页面很少链接到坏的页面,而坏的页面则经常相互链接。

信任排名的步骤如下:

  1. 选取信任种子集:人工识别出一小部分可信页面(例如.gov、.edu域名下的页面)。
  2. 信任传播:以信任种子集作为传送集,运行主题特定PageRank。这实质上是将信任值沿链接在网络中传播。
  • 每个被信任页面初始获得信任值。
  • 页面将其信任值按出链数量平分,并乘以一个衰减因子beta后传递给所链接的页面。
  • 页面的总信任值是所有入链传递来的信任值之和。
  1. 判定垃圾:得到每个页面的信任分数后,有两种方式判定垃圾:
  • 方案A:阈值法:直接设定一个信任分数阈值,低于该阈值的页面被视为垃圾。
  • 方案B:垃圾质量法:更优的方法是计算页面的垃圾质量
    • r(p) 为页面p的标准PageRank分数(全局重要性)。
    • r+(p) 为页面p的信任排名分数(来自可信集的重要性)。
    • 定义 垃圾质量 = (r(p) - r+(p)) / r(p)
    • 如果一个页面全局排名很高 (r(p) 大),但来自可信集的排名很低 (r+(p) 小),那么它的垃圾质量就很高,很可能是通过垃圾农场获得排名的。

选择信任种子集需要在“覆盖面广”(确保所有好页面都能通过短路径被覆盖)和“规模小”(减少人工审核成本)之间取得平衡。启发式方法包括选择标准PageRank最高的K个页面,或直接使用受控域名(如.edu)下的页面。

总结

本节课中我们一起学习了:

  1. 主题特定PageRank:通过将随机游走者的传送目标限制在与特定主题相关的页面集合中,来衡量节点在特定主题下的重要性。
  2. 随机游走重启:将传送集固定为单个起始节点,用于衡量图中其他节点与该节点的接近度,是构建高效、可扩展推荐系统的强大工具。
  3. 链接垃圾与垃圾农场:了解了垃圾发布者如何通过创建特殊的互链结构(垃圾农场)来人为提升页面PageRank。
  4. 信任排名:学习了如何通过以可信页面集为起点运行个性化PageRank来传播信任,并通过比较页面的全局PageRank与信任排名分数(计算垃圾质量)来有效识别和过滤垃圾页面。

这些技术展示了如何通过巧妙地利用图结构和随机游走模型,来解决信息检索、推荐系统和网络安全中的核心问题。

011:图中的社区检测

在本节课中,我们将要学习如何在图中进行社区检测。社区检测的目标是发现图中那些内部连接紧密、外部连接稀疏的节点组,这些组通常被称为集群、模块或社区。我们将介绍两种主要方法:基于个性化PageRank的局部社区发现方法和基于模块度最大化的全局社区发现方法。

概述

上一节我们介绍了图中节点的重要性度量(如PageRank)。本节中,我们来看看如何发现图中的社区结构。社区结构是许多真实网络(如社交网络、生物网络)的内在特征。我们的目标是设计出能够高效、可扩展地发现这些社区的方法。

社区检测简介

现实世界中的网络,如社交网络、生物网络和信息网络,通常具有非随机的结构。它们存在一些潜在的、有组织的模式。我们的目标就是发现并提取这些潜在的组织结构。这些结构有时被称为集群、模块或社区。

核心思想是,网络可以被分割成一些内部连接紧密的单元。这些单元可以像下图所示那样,以层次化的方式嵌套存在。

例如,一个社交网络可能包含三个大的社区,而每个大社区内部又包含若干个子社区。我们的目标就是发现这种结构,并识别出属于每个社区的节点。

下图是另一个例子。虽然在小图上识别这些紧密连接的节点组看似简单,但核心问题在于:我们如何定义“好”的社区?应该使用什么样的优化目标函数?以及如何高效地求解?

今天,我们将讨论非重叠社区检测,即每个节点只属于一个社区。如果我们有一个理想的社区结构,其邻接矩阵经过排序后,会呈现出明显的块状结构:社区内部连接密集(矩阵块内为深色),社区之间连接稀疏(矩阵块外为浅色)。

然而,我们实际得到的邻接矩阵是未经排序的,看起来是随机的。因此,从看似随机的数据中发现这种潜在结构是一个极具挑战性的问题。

应用实例

以下是社区检测的几个应用场景:

  • 网络广告:可以构建一个二分图,其中一方是广告商,另一方是关键词。如果广告商对一组关键词出价,则存在边。通过社区检测,可以发现对相似关键词组合感兴趣的广告商群体。例如,可能发现一个与“赌博”相关的关键词集群,其中又包含一个专注于“体育博彩”的子集群。
  • 电影-演员网络:构建一个二分图,连接演员和他们参演的电影。对此图进行社区检测,第一层级的集群可能对应不同的语言或国家,如阿根廷电影、墨西哥电影、巴西电影等。
  • 个人社交网络:分析个人的朋友网络(Ego Network),可以发现不同的社群,如高中同学、家人、大学朋友、研究组成员等。这些社群可能是重叠或嵌套的。

问题设定与假设

我们的假设是图规模很大,但足够放入内存。通过优化,大约可以在16GB内存中处理2亿个节点和20亿条边。因此,即使在笔记本电脑上,也能处理相当大规模的网络。

尽管图可以放入内存,但其规模之大使得我们只能承受线性时间复杂度的算法。本节课将介绍一种非常高效的计算PageRank的方法,用于发现图中的密集社区。

这种方法的一个优点是,其运行时间与输出的社区大小成正比,而与整个图的大小无关。这意味着我们甚至可以在有限时间内处理理论上无限大的图,因为它只访问图中很小的一部分。

方法一:基于个性化PageRank的局部社区发现

核心思路

该方法从一个给定的种子节点出发,旨在找到包含该种子节点的社区。

基本思想是:在种子节点上运行个性化PageRank,其中传送集仅包含该种子节点。直觉是,如果种子节点属于一个连接紧密的社区,那么随机游走者将在该社区内“被困住”很长时间。一旦游走者“逃出”该社区,就会迅速扩散到图的其他部分而消失。

算法大纲

以下是该方法的步骤概述:

  1. 选择种子节点:选取一个感兴趣的种子节点 s
  2. 运行个性化PageRank:以 s 为传送集,计算个性化PageRank向量。
  3. 按分数排序节点:根据个性化PageRank分数降序排列所有节点。
  4. 执行扫描操作:通过扫描操作识别出优质社区。

扫描操作是该方法的关键。我们按PageRank分数降序逐个将节点加入候选集合,并计算当前集合的某种质量指标(通常是电导率,下文会详述)。绘制质量指标随集合大小变化的曲线(扫描曲线),曲线中的局部最小值通常对应一个好的社区划分。

上图展示了一个扫描曲线的例子。第一个局部最小值对应一个较小的社区(无线箭头),第二个更深的局部最小值对应一个更大的社区(绿色箭头)。较小的社区嵌套在较大的社区之中。

社区质量度量:电导率

在深入扫描操作之前,我们需要定义如何衡量一个社区(节点子集)的质量。给定一个无向图,我们将节点集划分为两个不相交的子集 ABB = V \ A)。我们的目标是最大化社区内部连接,最小化社区之间的连接。

为此,我们定义两个量:

  • :连接集合 A 内部与外部节点的边的权重之和。
    • 公式:cut(A) = Σ_{i∈A, j∉A} w_{ij}
  • 体积:与集合 A 中节点相关联的所有边的权重之和(即 A 中所有节点的度之和)。
    • 公式:vol(A) = Σ_{i∈A} degree(i)

一个直观的想法是最小化 cut(A)。但这存在缺陷,例如,它可能倾向于选择只有一个节点的集合(割为1),而这通常不是有意义的社区。

更好的度量是电导率,它考虑了社区的内部连接,类似于表面积与体积之比。

  • 电导率公式Φ(A) = cut(A) / min(vol(A), 2m - vol(A))
    • 其中 m 是图中总边数(对于加权图是总权重),2m 是整个图的体积。
    • 分母取 vol(A)vol(V\A) 中较小者,是为了鼓励发现规模相对均衡的划分,避免选择极大的集合(其体积接近 2m,导致电导率很小但无意义)。

电导率越低,表示社区质量越好(内部连接紧密,外部连接稀疏)。

扫描操作详解

现在我们可以详细说明扫描操作:

  1. 将节点按个性化PageRank分数 r 降序排列:v1, v2, ..., vn,其中 r(v1) ≥ r(v2) ≥ ... ≥ r(vn)
  2. 按顺序考虑前 k 个节点组成的集合 S_k = {v1, v2, ..., vk},其中 k 从 1 到 n
  3. 对于每个 k,计算集合 S_k 的电导率 Φ(S_k)
  4. 绘制 Φ(S_k) 关于 k 的曲线。
  5. 识别曲线中的局部最小值,每个局部最小值对应的集合 S_k 被认为是一个优质的社区。

高效计算:扫描曲线可以在线性时间内计算。当我们从 S_k 扩展到 S_{k+1}(即加入节点 v_{k+1})时,可以快速更新割和体积:

  • vol(S_{k+1}) = vol(S_k) + degree(v_{k+1})
  • cut(S_{k+1}) = cut(S_k) + degree(v_{k+1}) - 2 * (v_{k+1} 与 S_k 中节点相连的边数)
    这样,只需一次遍历即可计算出整个曲线。

近似个性化PageRank计算

直接计算个性化PageRank需要多次迭代遍历整个图,成本高昂。为了 scalability,我们使用一种近似算法:PageRank Nibble

该算法的核心是惰性随机游走残差推送的思想。

  • 惰性随机游走:在每个节点,游走者以 1/2 概率停留在原地,以另外 1/2 概率随机跳到一个邻居。
  • 残差:定义残差 r(u) 为节点 u 的真实PageRank分数与当前估计值之间的误差。
  • 推送操作:算法维护两个向量:估计PageRank向量 p 和残差向量 r。初始时,p 全为0,r 在种子节点处为1,其余为0。
    • 当某个节点 u 的残差除以其度数大于某个阈值 εr(u)/degree(u) > ε)时,对该节点执行推送操作:
      1. 将一部分残差 (1-β)*r(u) 加入到 p(u) 中(β 是传送概率)。
      2. 将剩余残差 β*r(u) 平均推送给 u 的所有邻居 v,即每个邻居的残差增加 β*r(u) / degree(u)
      3. u 的残差 r(u) 设为0。
  • 迭代:重复上述推送操作,直到所有节点都满足 r(u)/degree(u) ≤ ε

算法优势

  • 运行时间为 O(1/(ε(1-β))),与图的大小无关,只与精度参数 εβ 有关。
  • 是一种局部算法,只访问图中PageRank值显著高于误差阈值的部分,非常适合寻找局部社区。
  • 有理论保证,如果图中存在电导率为 φ 的社区,该方法能找到电导率不超过 O(√φ) 的社区。

通过调整误差参数 ε,可以控制随机游走传播的范围。ε 越大,算法越局部,运行越快;ε 越小,结果越精确,但运行越慢。在扫描曲线中,我们只信任PageRank估计相对准确的前一部分节点,后面的部分由于误差累积可能不可靠。

方法二:基于模块度最大化的全局社区发现

上一节我们介绍了基于种子节点的局部社区发现方法。本节中,我们来看看一种不需要种子节点、旨在发现整个图所有社区的全局方法。

模块度定义

模块度 Q 用于衡量网络划分成社区的整体质量。对于给定的划分 S(将节点分到多个社区),模块度定义为:

  • 模块度公式Q(S) = (1/(2m)) * Σ_{c∈S} [ (Σ_{i,j∈c} A_{ij}) - (Σ_{i,j∈c} (degree(i)*degree(j))/(2m) ) ]
    • m:图中总边数(或总权重)。
    • A_{ij}:节点 ij 之间的边权重(无边则为0)。
    • 第一项 Σ_{i,j∈c} A_{ij}:社区 c 内部实际的边权重之和。
    • 第二项 Σ_{i,j∈c} (degree(i)*degree(j))/(2m):在零模型下,社区 c 内部期望的边权重之和。

零模型(配置模型):我们构造一个随机图,其中每个节点 i 的度数 degree(i) 与原图相同,但边是随机连接的。在这个模型中,节点 ij 之间期望的边数为 (degree(i)*degree(j))/(2m)

模块度的核心思想是:一个好的社区划分,其社区内部的连接应远高于随机连接下的期望值Q 的取值范围在 [-1, 1] 之间,通常大于 0.3 就认为社区结构比较显著。

我们的目标是找到使模块度 Q 最大化的划分 S

Louvain 算法

Louvain 算法是一种高效、贪婪的模块度最大化算法,时间复杂度约为 O(n log n)。它支持加权图,并能产生层次化的社区结构。

算法分为两个重复进行的阶段:

阶段一:局部优化

  1. 初始化:每个节点独自成为一个社区。
  2. 遍历所有节点(顺序可能影响结果,但通常影响不大)。对于每个节点 i,考虑将其移动到每个邻居节点 j 所在的社区。
  3. 计算每种移动带来的模块度增益 ΔQ
  4. 将节点 i 移动到能带来最大正增益 ΔQ 的社区。如果所有移动都不能带来正增益,则节点 i 保持不动。
  5. 重复遍历所有节点,直到没有节点可以移动(即模块度无法再提高)。此时达到一个局部最优。

计算 ΔQ 的公式可以只依赖于社区和节点的局部统计信息(如社区内部边权重和、社区总度数、节点与社区之间的边权重和等),因此计算非常高效。

阶段二:网络凝聚

  1. 将阶段一找到的所有社区分别凝聚成新的“超节点”。
  2. 超节点之间的边权重,等于原图中对应两个社区之间所有边的权重之和。
  3. 超节点内部的自环边权重,等于原社区内部所有边的权重之和。
  4. 这样就得到了一个新的、更小的加权网络。

迭代:将新生成的网络作为输入,重新应用阶段一阶段二。如此反复,直到网络结构不再变化(模块度无法再提高),或者整个网络被凝聚成一个节点。

层次化输出:这个过程自然产生了一个社区划分的层次结构。最初每个节点是一个社区,然后小社区合并成大社区,最终合并为整个网络。我们可以选择层次结构中模块度最大的那一层划分作为最终结果。

上图展示了Louvain算法在一个小网络上的运行过程。从左到右,先是初始划分,然后经过局部优化和凝聚,产生新的网络,再次优化,最终得到层次化结构。

方法比较与总结

本节课中我们一起学习了两种社区检测方法:

  1. 基于个性化PageRank的局部方法

    • 优点:运行快,与图规模无关,适合寻找特定种子节点周围的局部社区。可以处理大规模图。
    • 缺点:需要指定种子节点;一次只找一个社区;结果受参数 ε 影响。
    • 适用场景:当你关心某个特定节点属于哪个社区时。
  2. 基于模块度最大化的Louvain全局方法

    • 优点:不需要种子节点;能发现整个图的社区结构;输出层次化结果;速度快,在实践中非常流行。
    • 缺点:是贪婪算法,可能找到局部最优解;结果可能受节点遍历顺序影响。
    • 适用场景:当你想要了解整个网络的全局社区组成和层次结构时。

如何选择

  • 如果问题聚焦于单个或少数几个节点及其所属社区,使用第一种方法。
  • 如果需要对整个网络进行全面的社区划分和分析,使用第二种方法。

这两种方法为我们提供了强大的工具,用以揭示隐藏在海量图数据中的丰富组织结构。

012:图表示学习 🧠

在本节课中,我们将要学习如何为图中的节点计算坐标表示,以便后续执行各种任务,如链接预测和节点分类。我们将探讨如何自动学习网络的结构特征,从而避免繁琐的手工特征工程。


概述 📋

图表示学习的核心目标是将图中的每个节点映射到一个低维向量空间中。这些向量(或称为嵌入)应能捕捉节点在网络中的结构信息,使得网络中结构相似的节点在嵌入空间中也彼此接近。一旦获得这些嵌入,我们就可以轻松地在其上应用各种机器学习算法。


图表示学习的目标 🎯

我们的目标是学习一个编码器函数,该函数将每个节点映射到一个低维向量。我们希望嵌入空间中的节点相似性(例如点积)能够近似原始网络中的节点相似性。

核心公式
设编码器函数为 ENC(v) = z_v,其中 z_v 是节点 vd 维嵌入向量。我们希望:
sim(u, v) ≈ z_u · z_v
这里,sim(u, v) 是原始网络中节点 uv 的相似度。


定义节点相似性 🔗

上一节我们介绍了学习节点嵌入的目标,本节中我们来看看如何定义节点在原始网络中的相似性。一个关键且灵活的方法是使用随机游走

我们定义节点相似性为:节点 v 在从节点 u 开始的随机游走中被访问到的概率。换句话说,如果两个节点经常在短随机游走中共同出现,那么它们就是相似的。

核心思想
通过优化嵌入向量,使得点积 z_u · z_v 能够编码随机游走的共现概率 P(v | u)


优化问题与损失函数 ⚙️

基于上述思想,我们可以将学习嵌入表示构建为一个优化问题。我们希望最大化从节点嵌入预测其网络邻居的可能性。

损失函数
L = -∑_{u ∈ V} ∑_{v ∈ N_R(u)} log P(v | z_u)
其中,N_R(u) 是通过随机游走策略 R 定义的节点 u 的邻居集合。

为了计算概率 P(v | z_u),我们使用 softmax 函数:
P(v | z_u) = exp(z_u · z_v) / ∑_{n ∈ V} exp(z_u · z_n)


负采样:提升计算效率 🚀

直接计算上述损失函数非常耗时,因为 softmax 分母需要对图中所有节点求和。为了解决这个问题,我们引入 负采样 技术。

负采样通过用一组随机采样的“负”节点来近似整个求和项,从而将计算复杂度从 O(|V|^2) 降低到 O(|V|)

近似公式
log P(v | z_u) ≈ log σ(z_u · z_v) + ∑_{i=1}^{K} E_{n_i ~ P_n} [log σ(-z_u · z_{n_i})]
其中,σ 是 sigmoid 函数,K 是负样本数量,P_n 是节点的采样分布(通常与节点度成正比)。

以下是负采样的关键步骤:

  1. 为每个正样本(即出现在邻居中的节点对 (u, v))采样 K 个负样本节点。
  2. 负样本节点通常从整个图的节点分布中采样,且偏向于高频(高连接度)节点。
  3. 通过这种方式,模型在拉近正样本对的同时,推远随机负样本对。

DeepWalk 算法 🚶‍♂️

基于随机游走和负采样的思想,我们得到了 DeepWalk 算法。该算法可以看作是词向量模型 Word2Vec 在图结构数据上的直接应用。

以下是 DeepWalk 的主要步骤:

  1. 对图中的每个节点,执行固定长度的短随机游走多次,生成节点序列(相当于文本中的“句子”)。
  2. 将这些序列作为输入,使用 Skip-gram 模型(即给定中心词预测上下文词)来学习节点嵌入。
  3. 在训练 Skip-gram 模型时,使用负采样来优化计算效率。

Node2Vec:更灵活的随机游走 🔄

上一节我们介绍了基础的 DeepWalk 算法,本节中我们来看看它的一个强大扩展——Node2Vec。Node2Vec 通过引入有偏的随机游走策略,提供了在 广度优先搜索(BFS)深度优先搜索(DFS) 之间进行权衡的能力,从而学习到更丰富、更具任务相关性的嵌入表示。

Node2Vec 的随机游走由两个参数控制:

  • 返回参数 p:控制游走返回上一个节点的概率。较小的 p 值会增加回溯,使游走更局部化(类似 BFS)。
  • 进出参数 q:控制游走向外探索的概率。较小的 q 值会使游走更倾向于探索远离起点的节点(类似 DFS)。

游走概率公式
假设随机游走刚从节点 t 走到节点 v,现在需要决定下一个节点 x。设 d_{tx} 为节点 tx 的最短路径距离(对于当前节点 v 的邻居,d_{tx} 只能是 0, 1 或 2)。到下一个节点 x 的未归一化转移概率为:

  • 如果 d_{tx} = 0(回到 t),概率权重为 1/p
  • 如果 d_{tx} = 1(与 t 距离相同),概率权重为 1
  • 如果 d_{tx} = 2(远离 t),概率权重为 1/q

通过调整 pq,我们可以让模型学习到侧重于网络局部微观结构(p 小,q 大)或全局宏观社区结构(p 大,q 小)的嵌入。


嵌入的应用 🛠️

一旦我们获得了节点的低维嵌入向量,就可以将其用于各种下游任务。

以下是几个主要的应用方向:

  • 节点分类:将节点的嵌入向量 z_v 作为特征,输入到一个分类器(如逻辑回归、神经网络)中,预测节点的标签。
  • 链接预测:给定一对节点 (u, v),通过一个函数 f(z_u, z_v)(如点积、连接后通过神经网络)来预测它们之间是否存在边。
  • 图聚类:对节点的嵌入向量进行聚类(如 K-Means),从而发现图中的社区结构。
  • 图分类:为了对整个图进行分类(例如判断分子图的毒性),可以简单地将图中所有节点的嵌入向量求和或平均,得到图的整体表示,再用于分类。

总结 📝

本节课中我们一起学习了图表示学习的基本原理和方法。我们从学习节点嵌入的目标出发,探讨了如何利用随机游走定义节点相似性,并构建了相应的优化问题。为了高效求解,我们引入了负采样技术。在此基础上,我们介绍了 DeepWalk 和 Node2Vec 两种经典算法,其中 Node2Vec 通过有偏随机游走在 BFS 和 DFS 之间取得平衡,能够学习到更灵活的嵌入表示。最后,我们了解了这些嵌入向量在节点分类、链接预测和图分类等任务中的广泛应用。图表示学习是一个活跃的研究领域,这些基础方法为处理复杂的网络数据提供了强大的工具。

013:大规模机器学习 I

在本节课中,我们将要学习大规模机器学习的基础知识,特别是如何构建大规模决策树模型。我们将从机器学习的基本目标开始,探讨监督学习、泛化等核心概念,并详细讲解决策树的构建原理。最后,我们将介绍如何在超大规模数据集上,使用并行计算框架(如MapReduce)高效地构建决策树。


机器学习的目标

上一节我们介绍了课程的整体安排,本节中我们来看看机器学习的核心目标。机器学习的目标是学习一个函数,该函数能够将输入映射到输出。

公式: y = f(x)

其中,x 是一个多维特征向量,y 是输出变量。给定一组训练样本 (x, y),我们希望学习或近似函数 f,从而能够根据新的输入 x 预测输出 y


学习范式

以下是几种主要的学习范式:

  • 监督学习:对于每一个输入 x,我们都给定一个监督信号(标签)y
  • 无监督学习:只给定输入 x,目标是学习 x 的某种结构,例如聚类。
  • 半监督学习:同时拥有带标签和不带标签的数据,目标是利用未标记数据来帮助估计函数 f
  • 主动学习:系统先做出预测或决策,然后根据获得的反馈进行学习。
  • 迁移学习:学习一个函数 f,使其在一个新的数据域 Z 上也能表现良好。

预测任务类型

根据输出 y 的类型,预测任务可以分为:

  • 回归y 是实数。
  • 分类y 是类别(如颜色、大小)。
  • 复杂对象预测y 是更复杂的结构,如物品排序或句子解析树。

泛化与过拟合

我们真正关心的是模型在未见过的数据上的表现,这种能力称为泛化

核心概念:我们使用训练数据(红色部分)来构建函数 f,但目标是让 f 在未来的测试数据(蓝色部分)上做出良好预测。如果模型在训练数据上表现很好,但在新数据上表现很差,则称为过拟合

因此,机器学习的关键不是完美记忆训练数据,而是构建能够泛化到未知数据的模型。


大数据的重要性

模型性能的提升往往更多地依赖于训练数据的规模,而不仅仅是模型本身的复杂性。当数据量很大时,即使相对简单的模型也能取得出色的效果。当前机器学习革命的两大支柱是:强大的计算系统/硬件,以及海量的可用数据。


决策树介绍

现在,我们来看一种经典且强大的模型——决策树。决策树是一种树形结构模型,它通过一系列基于属性的测试来决定输出。

决策树结构

一个决策树包含两种节点:

  1. 内部节点(决策节点):包含一个测试条件,形式为“属性 A 是否满足条件 C?”。根据测试结果(是/否),数据被分到左子树或右子树。
  2. 叶节点(预测节点):包含一个最终的预测值(对于回归是数值,对于分类是类别)。

预测过程:对于一个输入数据点 x,从根节点开始,根据其属性值评估每个节点的条件,沿着相应的分支向下移动,直到到达一个叶节点,该叶节点的值即为预测结果。

决策树的构建

决策树采用自顶向下、递归分割的方式构建。关键在于在每个节点上决定:

  1. 是否停止分裂?如果停止,则将该节点变为叶节点并进行预测。
  2. 如果继续分裂,选择哪个属性和哪个值进行分裂

以下是构建决策树的伪代码框架:

def build_tree(node, data):
    if stopping_criteria(data):
        node = create_prediction_node(data)
        return node
    else:
        best_split = find_best_split(data) # 找到最佳分裂属性和值
        left_data, right_data = split_data(data, best_split)
        node.left_child = build_tree(new_node(), left_data)
        node.right_child = build_tree(new_node(), right_data)
        return node

我们需要定义三个核心函数:find_best_split, stopping_criteria, create_prediction_node


如何找到最佳分裂

对于回归任务(预测实数值)

我们使用纯度的概念。目标是找到一个分裂,能最大程度地减少输出 y 的方差。

公式:最大化 Purity = Var(D) - [ (|D_left|/|D|) * Var(D_left) + (|D_right|/|D|) * Var(D_right) ]
其中 Var(D) 是数据集 Dy 的方差。方差减少得越多,分裂后子集的纯度越高。

对于分类任务(预测类别)

我们使用信息增益的概念。信息增益衡量了已知属性 X 的值后,关于类别 Y 的不确定性减少了多少。

计算步骤

  1. 计算 Y 的熵:H(Y) = -Σ p(y) log₂ p(y)
  2. 计算已知 XY 的条件熵:H(Y|X) = Σ p(x) * H(Y|X=x)
  3. 信息增益:IG(Y|X) = H(Y) - H(Y|X)

信息增益越高的属性,越适合作为分裂点。


何时停止分裂(停止准则)

  1. 叶节点纯度:如果到达某个节点的所有数据都具有相同的输出 y(分类),或 y 的方差小于某个阈值(回归),则停止。
  2. 数据量过小:如果到达节点的训练样本数量太少(例如少于100个),继续分裂可能导致过拟合,因此停止。

如何创建叶节点(预测)

  • 回归:通常使用到达该叶节点的所有数据 y 值的平均值作为预测值。
  • 分类:使用到达该叶节点的所有数据中最常见的类别作为预测值。

大规模决策树:PLANET算法

当数据集太大,无法单机处理时,我们需要并行算法。PLANET(Parallel Learner for Assembling Numerous Ensemble Trees)是一个基于MapReduce的大规模决策树构建算法。

核心思想

  • 按层构建:一次MapReduce作业构建树的一层。
  • 主节点协调:一个主节点维护整个树的状态,决定如何生长。
  • 候选分裂:预先为每个属性确定一组候选分裂值(如使用等深直方图采样)。
  • MapReduce评估分裂:Mapper处理数据子集,计算每个候选分裂的局部统计量;Reducer聚合这些统计量,计算每个分裂的全局纯度,并选出最佳分裂。

算法主要步骤

  1. 初始化:运行一次MapReduce作业,为每个属性确定候选分裂值(属性元数据)。
  2. 寻找最佳分裂:这是核心循环。主节点针对当前需要生长的叶子节点,发起MapReduce作业。
    • Mapper:加载当前树模型和候选分裂。对每条数据,判断其落入哪个叶子节点,并为该节点相关的每个候选分裂更新统计量(如y的和、平方和、计数)。
    • Reducer:接收同一个候选分裂来自不同Mapper的统计量,进行聚合。利用聚合后的统计量计算该分裂的纯度,并为每个叶子节点选出纯度最高的分裂。
  3. 主节点决策:主节点收集Reducer的结果,更新树模型(执行最佳分裂)。然后判断新的子节点中的数据量:如果数据量小,则触发“内存中构建”优化;如果数据量大,则准备下一轮“寻找最佳分裂”作业。
  4. 内存中构建:当某个节点数据量足够小,可以装入单机内存时,直接在该机器上使用标准算法构建子树,避免启动昂贵的MapReduce作业。

方差计算的优化

方差可以通过充分统计量高效计算和聚合:
公式Var(D) = (1/n) Σ y² - ((1/n) Σ y)²
其中 n(计数)、Σ y(和)、Σ y²(平方和)可以在不同数据子集上独立计算,然后轻松聚合。


决策树的优势与集成方法

决策树易于理解、实现和部署,能处理数值和类别特征,是数据挖掘中最流行的工具之一。

为了获得更好、更稳定的性能,通常不只训练一棵树,而是训练一个树集合

  • 装袋法:从原始数据集中有放回地采样,生成多个不同的训练集,在每个训练集上独立训练一棵树。预测时,对所有树的输出取平均(回归)或投票(分类)。
  • 随机森林:在装袋法的基础上,每棵树在训练时还只随机使用特征的一个子集。这进一步增加了树之间的差异性,通常能获得更好的泛化性能。

总结

本节课中我们一起学习了大规模机器学习的基础,重点深入探讨了决策树模型。我们了解了决策树如何通过递归分割来构建,以及如何用信息增益或纯度来选择最佳分裂点。更重要的是,我们学习了PLANET算法,它展示了如何利用MapReduce框架的并行能力,在超大规模数据集上高效构建决策树。最后,我们提到了通过集成方法(如装袋法和随机森林)可以进一步提升决策树的性能和鲁棒性。下节课我们将继续探讨另一种强大的模型——神经网络。

014:大规模机器学习 II

概述

在本节课中,我们将学习大规模机器学习中的支持向量机(SVM)及其优化方法。我们将从监督学习的基本概念开始,逐步深入到SVM的原理、损失函数、优化技巧,并最终探讨如何将这些技术并行化以处理海量数据集和复杂模型。

监督学习回顾

上一节我们介绍了大规模机器学习的基本概念,本节中我们来看看监督学习的核心思想。在监督学习中,我们有一个预测函数 F(x),用于预测标签 Y。标签 Y 可以是实数(回归问题)、类别(分类问题)或更复杂的结构(如排序)。本节课我们将重点讨论二元分类任务,即标签 Y 取值为 +1-1

我们的目标是训练一个模型,使其在未见过的测试数据上也能表现良好,这被称为模型的泛化能力。主要的挑战是避免过拟合,即模型过度学习训练数据的特性,导致在测试数据上表现不佳。

损失函数与优化

机器学习本质上是一个优化问题。我们通过最小化损失函数来衡量和优化模型性能。损失函数 L 衡量预测值 Ŷ 与实际标签 Y 之间的差异。

我们希望最小化期望损失:
min E[L(F(x), Y)]

由于我们无法直接计算测试数据的期望损失,因此转而最小化训练数据上的平均损失:
min (1/n) Σ L(F(x_i), Y_i)

一个简单的损失函数是阶跃函数,但它不可导,不利于使用梯度下降法优化。因此,我们通常使用其替代品,例如合页损失

合页损失函数

合页损失函数的公式如下:
L_hinge = max(0, 1 - y_i * (w·x_i + b))

以下是合页损失函数的特点:

  • 当预测正确且置信度高时,损失为0。
  • 当预测接近决策边界时,损失开始线性增加。
  • 当预测错误时,损失线性增长。

这种平滑的特性使其易于求导,适合梯度下降优化。

支持向量机

决策树在处理高维数据时可能表现不佳。本节中我们来看看支持向量机,它特别擅长处理高维数据。

SVM的核心思想是找到一个超平面,能最好地分隔两类数据点。这个超平面由权重向量 w 和偏置项 b 定义,其方程为:
w·x + b = 0

我们不仅希望找到一个分隔超平面,更希望找到间隔最大的那个。间隔 γ 定义为离超平面最近的数据点的距离。最大化间隔能提高分类器的置信度和泛化能力。

间隔计算与优化

x 到超平面的距离公式为:
|w·x + b| / ||w||

为了便于优化,我们通常约束 ||w|| = 1。SVM的优化目标可以形式化为:
max γ, 使得对于所有 i,有 y_i (w·x_i + b) ≥ γ

通过一些数学变换,上述问题可以等价于一个更易处理的优化问题:
min (1/2) ||w||^2, 使得 y_i (w·x_i + b) ≥ 1

这被称为硬间隔SVM,它假设数据是线性可分的。

软间隔与松弛变量

现实中数据往往不是线性可分的。为此,我们引入软间隔SVM,它允许一些数据点被错误分类,但会施加惩罚。

我们引入松弛变量 ξ_i 来衡量分类错误的程度。优化目标变为:
min (1/2) ||w||^2 + C Σ ξ_i, 使得 y_i (w·x_i + b) ≥ 1 - ξ_i, 且 ξ_i ≥ 0

其中,C 是一个正则化参数,控制对误分类的惩罚力度:

  • C 很大:惩罚很重,模型倾向于更小的训练错误,但可能过拟合。
  • C 很小:惩罚很轻,模型允许更多错误,但可能欠拟合。

结合合页损失,SVM的最终形式(原始形式)为:
min (1/2) ||w||^2 + C Σ max(0, 1 - y_i (w·x_i + b))

优化方法:梯度下降及其变体

上一节我们推导了SVM的优化目标,本节中我们来看看如何求解。SVM的目标函数是凸函数,理论上可以用标准求解器,但对于海量数据效率太低。

我们使用基于梯度的方法。对权重 w 求导,梯度为:
∇J(w) = w + C Σ ∇L_hinge

其中,合页损失的导数为:
∇L_hinge = 0, 若 y_i (w·x_i + b) ≥ 1;否则为 -y_i x_i

标准的梯度下降每次迭代需要计算整个数据集的梯度,计算成本为 O(n),对于大数据集很慢。

随机梯度下降

随机梯度下降每次迭代只随机使用一个样本计算梯度并更新参数:
w := w - η [w + C ∇L_hinge(x_i, y_i)]

SGD收敛更快,但更新路径更嘈杂。它在实践中非常有效,能极大缩短训练时间。

小批量梯度下降

小批量梯度下降是折衷方案,每次使用一小批样本计算梯度:
w := w - η [w + (C/batch_size) Σ ∇L_hinge(x_batch, y_batch)]

它平衡了SGD的效率和批量梯度下降的稳定性,是现代深度学习中最常用的优化器。

扩展到多类分类与模型并行化

基本的SVM是二元分类器。对于多类问题,常用策略是训练多个“一对多”分类器,然后进行投票。

随着任务变复杂,模型参数数量激增(例如大型神经网络有数亿参数)。这引出了新的挑战:模型并行化

并行化维度

大规模机器学习可以从三个维度进行并行化:

  1. 数据并行:将数据分割到不同机器上。
  2. 模型并行:将模型参数分割到不同机器上。
  3. 工作负载划分:处理计算图不同阶段之间的依赖关系。

参数服务器架构

为了协调大规模并行训练,常用参数服务器架构。参数服务器是一个分布式键值存储,负责维护和更新所有模型参数 w

工作流程如下:

  1. 工作节点从参数服务器拉取最新参数。
  2. 工作节点用本地数据计算梯度。
  3. 工作节点将梯度推送给参数服务器。
  4. 参数服务器异步更新参数。

异步随机梯度下降

传统的SGD要求同步更新。异步SGD 允许工作节点在不加锁的情况下直接更新参数服务器。尽管这可能导致梯度覆盖,但理论证明,在高维稀疏数据下,它依然能收敛,且能获得近乎线性的加速比。

其核心伪代码如下:

# 分区数据
for each parallel process:
    while not converged:
        # 随机采样一个数据点 (x_i, y_i)
        # 计算稀疏梯度 g (仅非零分量)
        # 异步推送梯度 g 到参数服务器并更新

谷歌的 DistBelief 系统就采用了数据并行、模型并行和异步SGD,开启了大规模深度学习的新时代。

总结

本节课我们一起学习了支持向量机这一强大的分类模型。我们从其最大化间隔的核心思想出发,推导了硬间隔和软间隔的优化形式。为了高效处理海量数据,我们介绍了随机梯度下降及其变体。最后,面对超大规模模型,我们探讨了通过参数服务器和异步SGD实现数据与模型并行的架构。这些技术是当今处理海量数据集和复杂机器学习模型的基石。

015:数据流挖掘 I

在本节课中,我们将要学习如何处理无限的数据流。我们将探讨当数据以流的形式持续到达,且无法全部存储时,如何通过构建数据摘要来回答关键查询。具体来说,我们将学习如何从数据流中进行采样,以及如何回答关于滑动窗口的查询。


数据流简介

上一节我们介绍了课程的整体脉络。本节中我们来看看数据流这一特殊场景。

数据流管理指的是数据随着时间推移在线到达的情况。在许多实际应用中,输入数据是无限的。例如,社交媒体网站上的帖子是实时生成的,你永远不会有一个完整的帖子数据集。搜索引擎接收的查询也是一个接一个到来的流,你永远不会拥有所有查询的完整集合。

这意味着我们可以将数据视为无限且非平稳的,其分布会随时间变化。这正是其有趣且需要专门算法的原因。数据是无限的,你永远无法看到其全部内容。

处理这类问题的核心在于,输入元素以高速率到达,并且你无法存储所有到达的数据。因此,关键问题变成了:如何在有限的内存下进行关键计算和回答查询?这类算法的诀窍始终在于如何保存或构建数据的某种关键摘要,以便无需保存迄今为止看到的所有数据就能回答查询。

一个相关的概念是在线算法。例如,随机梯度下降在某种意义上就是一种流式算法。在机器学习中,我们称之为在线学习,它允许我们在新数据到达时实时更新模型。算法能够适应数据的变化,其方式是对模型进行小的增量更新。


流处理模型与查询类型

以下是流处理的基本模型:

  • 数据流进入流处理器。
  • 处理器拥有有限的工作存储空间,可能还有一些归档存储。
  • 查询会不断到来,我们需要能够回答这些查询。

我们将讨论可以回答哪些类型的查询,以及使用哪些算法来回答。

今天我们将讨论两种类型的查询:

  1. 从数据流中采样。
  2. 回答关于滑动窗口的查询。

对于滑动窗口查询,我们可能想问:“在流的最后 K 个元素中,有多少个是类型 X 的?”如果有大量存储空间,这很容易,只需保存最后 K 个元素并计数即可。但我们不希望这样做。同样,构建流的随机样本时,一种方法是保存整个流再采样,但随着流变长,需要保存的数据也越多,这也不可取。

在周四的课程中,我们将讨论:

  • 过滤数据流(选择具有属性 X 的元素)。
  • 估计流中不同元素的数量。
  • 估计流的矩(如平均值、标准差)。
  • 寻找流中的频繁项集。

流处理的应用

流处理有很多应用场景:

  • 网络搜索查询:例如,谷歌想知道哪些查询今天比昨天更频繁。
  • 点击流:分析网站上的用户点击行为。
  • 社交媒体新闻流:识别 Twitter、Facebook 上的热门话题。
  • 传感器网络:传感器实时感知环境数据,需要总结和分析,以触发警报等。
  • 电话呼叫记录:识别特定用户的所有呼叫,或将呼叫记录归到相应的人。
  • 网络数据包监控:交换机监控经过的数据包,收集最优路由信息,检测拒绝服务攻击等。

从数据流中采样

采样固定比例

首先,我们讨论如何从数据流中采样固定比例的元素。随着流变大,样本也会相应变大。

一种简单的策略是:对于每个到达的元素,以概率 p(例如 p = 0.1)决定是否保存它,否则丢弃。这可以通过生成一个随机数来实现。

但必须注意:采样方式取决于你想要回答的问题。考虑以下场景:一个搜索引擎希望回答“用户在同一天内重复发出相同查询的频率是多少?”。

假设每个用户一天发出 x 个唯一查询(各一次)和 d 个重复查询(各两次)。那么:

  • 总查询数:x + 2d
  • 唯一查询数:x + d
  • 重复查询的比例应为:d / (x + d)

如果采用前述的简单随机采样(以 10% 的概率保存每个查询实例),会出现问题:

  • 单次查询被采样的概率是 0.1,所以样本中约有 x/10 个单次查询。
  • 一个重复查询(两次)同时被采样的概率是 0.1 * 0.1 = 0.01,所以样本中约有 d/100 个重复查询。

如果我们直接在样本中计算重复查询的比例,会得到 (d/100) / (x/10 + d/100) = d/(10x + d),这与真实的 d/(x+d) 不符,导致了错误估计。

正确的采样方式:这个问题本质上是关于“每个用户”的。因此,我们应该对用户进行采样,而不是对查询实例进行采样。例如,使用哈希函数将用户 ID 均匀哈希到 10 个桶中,只保存哈希到第一个桶的用户的所有查询。这样,对于被采样的用户,我们可以准确计算其重复查询比例,然后进行平均,从而得到对整体用户群体的无偏估计。

一般方法:要对数据流进行 a/b 比例的采样,可以对每个元素的键(key)应用一个哈希函数,将其均匀哈希到 b 个桶中。如果哈希值落在前 a 个桶,则保存该元素。例如,要获得 30% 的样本,可以哈希到 10 个桶,并保存哈希值落在前 3 个桶的元素。


采样固定大小:蓄水池采样

现在,我们讨论一个更有趣的问题:如何在潜在的无限流上维持一个固定大小的随机样本。

我们希望在任何时刻 k,都拥有一个包含 S 个元素的样本,并且要求:在迄今为止看到的 k 个元素中,每个元素出现在样本中的概率都相等,即 S / k。随着 k 增长,这个概率会减小,但样本大小 S 保持不变。

蓄水池采样算法步骤如下:

  1. 将流的前 S 个元素直接放入样本中。
  2. 对于第 n 个到达的元素(n > S):
    • 以概率 S / n 决定保留这个新元素。
    • 如果决定保留,则从当前样本中随机移除一个现有元素,并将新元素放入样本。
    • 如果决定不保留,则样本保持不变。

算法正确性证明(归纳法)

  • 基础情况:当 n = S 时,所有 S 个元素都在样本中,概率为 S/S = 1,成立。
  • 归纳假设:假设在处理完 n 个元素后,每个元素在样本中的概率为 S/n
  • 归纳步骤:考虑第 n+1 个元素到达。
    • 对于前 n 个元素中的某一个,它要继续留在样本中,有两种情况:
      1. 新元素被丢弃(概率 1 - S/(n+1))。
      2. 新元素被保留(概率 S/(n+1)),但该旧元素没有被选为被替换的元素(概率 (S-1)/S)。
    • 因此,一个旧元素留在样本中的概率是:
      [1 - S/(n+1)] + [S/(n+1) * (S-1)/S] = n/(n+1)
    • 根据归纳假设,该元素之前就在样本中的概率是 S/n。所以,在处理完 n+1 个元素后,它仍在样本中的概率是:
      (S/n) * (n/(n+1)) = S/(n+1)
    • 对于新到达的第 n+1 个元素,它进入样本的概率显然是 S/(n+1)
  • 因此,在处理完 n+1 个元素后,所有 n+1 个元素出现在样本中的概率都等于 S/(n+1)。归纳成立。

回答滑动窗口查询

问题定义

现在,我们讨论如何回答关于长滑动窗口的查询。假设我们有一个数据流,只对最近的 N 个元素(窗口)感兴趣。N 非常大,以至于无法将最后 N 个元素全部存入内存。

例如,在亚马逊上,每个商品可以看作一个流:一次交易中若售出该商品则发出 1,否则发出 0。我们想回答:“在最近 N 次交易中,商品 X 被售出了多少次?”

我们考虑一个更一般化的问题:给定一个由 01 组成的流,对于任意 k1 ≤ k ≤ N),回答“在最后 k 个元素中有多少个 1?”。

我们不希望的简单方案是:假设流是平稳的,用全局的 1 的比例来估计窗口内的数量。这忽略了数据分布可能随时间变化的事实。


DGIM 算法

我们将介绍 DGIM 算法,它不假设数据均匀分布,并且能在 O(log² N) 的存储空间内给出估计值,其误差最多为 50%(可调整)。

核心思想:用指数增长的“桶”来汇总流的历史信息,但这里“桶的大小”定义为桶内 1 的数量,而不是时间长度。

桶的定义与规则

  • 每个桶记录其结束时间戳(模 N 以节省空间)和桶内 1 的数量。
  • 桶内 1 的数量必须是 2 的幂次(如 1, 2, 4, 8...)。
  • 对于每个 2 的幂次 2^i,我们最多只允许有 1 个或 2 个桶。
  • 桶按结束时间排序,更晚的桶(更近的数据)更大。
  • 桶的时间范围不重叠(可以紧邻)。
  • 当一个桶的结束时间超出当前窗口范围(即早于 N 个时间单位以前)时,丢弃该桶。

更新过程(新元素到达)

  1. 检查丢弃:如果最早(最老)的桶已超出窗口,则丢弃它。
  2. 处理新元素
    • 如果新元素是 0,无需任何操作。
    • 如果新元素是 1
      a. 创建一个新的桶,大小为 1,结束时间为当前时间。
      b. 检查是否违反了“每个尺寸最多 2 个桶”的规则。如果某个尺寸的桶变成了 3 个,则将最早(时间最老)的两个该尺寸的桶合并为一个两倍大小的桶(大小相加,结束时间为较晚的那个桶的结束时间)。
      c. 合并可能引发连锁反应,需要持续检查并合并,直到所有尺寸的桶数量都不超过 2。

查询过程
当查询“最后 k 个元素中有多少个 1?”时:

  1. 找到所有结束时间在最近 k 个时间单位内的桶。
  2. 除了最后一个(时间上最早,可能只有部分在窗口内)桶,将其它所有桶的 1 的数量相加。
  3. 加上最后一个桶的 1 的数量的一半。
  4. 这个总和就是我们的估计值。

误差分析

  • 设最后一个(部分覆盖窗口的)桶的大小为 2^r
  • 我们只加了它的一半,即 2^{r-1},因此最大可能误差就是 2^{r-1}
  • 由于桶的大小是 2 的幂次,且对于每个更小的尺寸 2^0, 2^1, ..., 2^{r-1} 都至少有一个完整的桶在窗口内,这些桶的总和至少为 1 + 2 + 4 + ... + 2^{r-1} = 2^r - 1
  • 因此,真实值至少为 (2^r - 1) + 至少半个最后桶 ≈ 2^r。我们的误差 2^{r-1} 最多是真实值的 50%。

降低误差
可以通过允许每个尺寸有更多桶来降低误差。如果允许每个尺寸最多有 R 个桶,则误差上界可以降至 1/R。当然,这需要更多的存储空间 (O(R * log² N))。


扩展

  1. 查询任意 k ≤ N:DGIM 算法天然支持。查询时,只需考虑结束时间在最近 k 个时间单位内的桶,并应用相同的规则(全加最后一个桶之前的所有桶,再加最后一个桶的一半)。
  2. 处理整数流求和:假设流中的元素是非负整数,我们想求最后 k 个元素的和。
    • 方法一(位分解):将每个整数的二进制表示的每一位看作一个独立的 0/1 流。对每一位运行 DGIM 算法来估计该位在窗口内为 1 的次数。则总和 = Σ(第 i 位为1的次数 * 2^i)。
    • 方法二(扩展桶定义):重新定义“桶的大小”为桶内元素值的和。要求桶内元素和不超过 2^bb 是桶的尺寸指数)。更新和合并规则需要相应调整,确保合并后桶的和不超过下一个尺寸的界限。查询方式类似。

总结

本节课中我们一起学习了数据流挖掘的基础内容:

  1. 数据流采样

    • 固定比例采样:需根据查询目标谨慎选择采样的键(如按用户采样而非按事件采样),常用哈希法实现。
    • 固定大小采样:使用蓄水池采样算法,能在无限流上维持一个等概率的固定大小随机样本。
  2. 滑动窗口查询

    • 使用 DGIM 算法 估计二进制流滑动窗口中 1 的数量。
    • 核心是维护一组遵循特定规则(指数大小、数量限制)的桶来摘要历史数据。
    • 该算法能以 O(log² N) 的空间提供误差有界的估计,并可通过参数调整误差。
    • 算法可扩展以支持查询任意窗口大小 k,以及处理整数流求和问题。

这些技术是处理高速、无限数据流的基础,使我们能够在有限的内存资源下进行有效的监控和分析。

016:数据流挖掘 II

概述

在本节课中,我们将继续学习数据流挖掘技术。我们将介绍四种新的方法,用于处理高速、大规模的数据流。这些方法包括布隆过滤器、计数不同元素的Flajolet-Martin算法、估计数据流矩的AMSS方法,以及用于频繁项集计数的指数衰减窗口技术。这些技术都旨在用有限的内存资源,对无限的数据流进行高效的近似计算。


布隆过滤器:一种高效的流数据过滤方法

上一节我们讨论了数据流的基本概念和计数方法。本节中,我们来看看如何高效地过滤数据流,即只选择具有特定属性X的元素。

抽象地说,我们希望将流中的每个元素视为一个键值对,并给定一个键的集合S。我们需要判断流中的元素是否在集合S中。一个显而易见的解决方案是使用哈希表存储S中的所有元素,但当我们无法存储整个哈希表时(例如,需要为百万用户各自维护一个信任邮箱列表),就需要更节省空间的方法。

布隆过滤器提供了一个巧妙的解决方案。其核心思想是使用一个长度为n比特的数组和k个独立的哈希函数。

以下是布隆过滤器的构建与查询过程:

  1. 初始化:创建一个长度为n的比特数组,所有位初始化为0。
  2. 构建:对于集合S中的每个元素s,使用k个哈希函数 h1(s), h2(s), ..., hk(s) 分别计算哈希值(对应数组中的位置),并将这些位置的值设置为1。
  3. 查询:当流中元素a到达时,计算其k个哈希值。仅当这k个位置的值全部为1时,才输出(或“放行”)元素a;否则,丢弃它。

布隆过滤器保证没有假阴性(即,所有在S中的元素都会被放行),但可能存在假阳性(即,某些不在S中的元素也可能被放行)。

假阳性概率分析

假设集合S的大小为m,比特数组长度为n,使用k个哈希函数。经过推导,假阳性的概率近似为:
(1 - e^(-k*m/n))^k

通过选择合适的k值,可以最小化这个错误率。例如,当 m=10亿,n=80亿比特时,最优的k约为6,此时假阳性率可降至约2%。布隆过滤器因其简单、高效且可并行化,被广泛应用于硬件实现和预处理步骤中。


计数不同元素:Flajolet-Martin算法

在过滤数据流之后,我们常常需要知道流中出现了多少种不同的元素。例如,统计一周内不同用户访问的独立网页数量。同样,我们无法存储所有已见元素。

Flajolet-Martin算法提供了一种估算不同元素数量的方法。其核心是追踪哈希值二进制表示中末尾零的个数。

算法步骤如下:

  1. 选取一个哈希函数,将元素均匀地映射到 [0, 2^L - 1] 范围内的整数。
  2. 对于流中每个元素a,计算其哈希值 h(a),并将其表示为二进制。
  3. ρ(a)h(a) 的二进制表示中,从最低位开始连续零的个数(即末尾零的长度)。
  4. 在整个流处理过程中,记录所见到的最大 ρ(a) 值,记为 R
  5. 估算的不同元素数量为 2^R

算法原理

其背后的直觉是:如果哈希函数是均匀的,那么看到一个末尾有r个零的哈希值的概率是 2^(-r)。因此,需要看到大约 2^r 个不同的元素,才有较大概率观察到这样一个哈希值。所以,观察到的最大 R 值可以用来估算不同元素的数量 2^R

为了获得更稳定、更精确的估计,实践中会使用多个哈希函数。将多个哈希函数得到的 R 值分组,取每组的中位数,再对这些中位数取平均,作为最终的估计值。


估计数据流矩:AMSS算法

接下来,我们探讨如何估计数据流的矩(Moments)。设流中元素来自一个全集,m_i 表示元素i在流中出现的次数。流的第k阶矩定义为:∑ (m_i)^k

  • 0阶矩:不同元素的数量(即刚才解决的问题)。
  • 1阶矩:流的总长度(容易计算)。
  • 2阶矩:称为“惊奇数”,衡量元素频率分布的均匀程度。分布越不均匀,值越大。

AMSS算法可以在不保存所有元素计数的情况下,无偏地估计流的矩(以2阶矩为例)。

算法思路如下:

  1. 假设已知流长度n(后续会处理未知情况)。
  2. 随机选择一个起始时间点t(从1到n均匀选择)。
  3. 初始化一个随机变量X,记录在时间t出现的元素,比如是元素i。
  4. 从时间t开始到流结束,统计元素i出现的次数,记为c。
  5. 对于这个随机变量X,其对于2阶矩的估计值为:f(X) = n * (2c - 1)
  6. 维护多个这样的随机变量X,最终的估计值是所有 f(X) 的平均值。

为何有效?

可以证明,E[f(X)] = ∑ (m_i)^2,即 f(X) 的期望值正是我们想求的2阶矩。其原理在于,当对所有可能的起始时间t的估计值求和时,会形成一个“伸缩和”,最终只剩下各元素计数的平方项。

对于无限流和未知长度n的情况,我们可以结合蓄水池采样技术。始终保持k个随机变量X。当新元素到达时,以 k/(当前流长度) 的概率决定是否用这个新元素替换掉一个已有的X,并开始新的计数。这样就能保证在任何时刻,每个时间点被选为起始点的概率都是近似相等的。


频繁项集与指数衰减窗口

最后,我们讨论如何在流数据中查找频繁项集(如经常一起购买的商品对)。为每个可能的项对维护一个流是不现实的,因为数量是商品数的平方。

指数衰减窗口技术提供了一种优雅的启发式方法。它不固定一个时间窗口,而是让历史数据的影响随时间指数级衰减,从而更强调近期的数据。

其核心操作是为每个监控的项(如商品)维护一个权重。假设衰减因子为常数c(一个接近1的小数,如0.99)。

  1. 每到来一个新的时间步,将所有项的当前权重乘以 (1 - c)
  2. 如果新到达的篮子中包含某项x,则给x的权重额外加上1。
  3. 项x在时刻t的权重可以形式化表示为:∑_{i=1 to t} (δ_i(x) * (1-c)^{t-i}),其中 δ_i(x) 在时刻i出现x时为1,否则为0。

这样,频繁出现的项会不断获得“加分”而保持较高的权重,而不常出现的项其权重会因持续的衰减而逐渐趋近于零。

应用

我们可以设定一个阈值。由于所有权重的总和收敛于 1/c,因此只有有限数量的项其权重会超过某个阈值(如 1/2)。系统可以只保留那些权重高于阈值的项及其计数,从而在有限内存中动态维护一个“当前最流行项”的列表。这种方法非常适合追踪实时热点,如热门电影、推特上的热门话题等。


总结

本节课我们一起学习了四种强大的数据流挖掘技术。

  1. 布隆过滤器:用于成员资格查询,以极小的空间代价换取可控的误报率,且绝无漏报。
  2. Flajolet-Martin算法:用于估算数据流中不同元素的数量,基于哈希值末尾零的分布。
  3. AMSS算法:用于无偏估计数据流的各阶矩(如二阶矩“惊奇数”),结合了随机采样和巧妙的估计函数。
  4. 指数衰减窗口:一种用于发现近期频繁模式(如项集)的启发式方法,通过指数衰减强调近期数据,并能自然地在有限内存中维护热点信息。

这些方法都是在内存受限条件下,对海量、高速数据流进行实时分析的基石。

017:计算广告

概述

在本节课中,我们将学习计算广告的基础知识,特别是如何将广告商与用户或查询进行匹配。我们将从抽象的在线二分图匹配问题入手,分析其算法和性能,然后将其与网络广告的实际问题联系起来,探讨如何设计算法以最大化广告平台的收入。


在线算法与离线算法

上一节我们介绍了海量数据流处理。网络广告是流算法的一个典型例子,本节我们将探讨与之相关的在线算法。

经典的算法模型是离线算法:算法一次性看到所有输入数据,然后计算某个函数并返回答案。

在线算法则不同:算法一次只能看到输入的一部分,并且必须立即做出不可撤销的决策。在处理完当前输入并做出决策后,下一个输入才会到达。这与数据流模型非常相似,元素一个接一个地到来,我们必须做出决策。


广告匹配问题模型

那么,广告模型是怎样的呢?以谷歌为例,它通过填充一个矩阵来赚钱:许多广告商对不同的关键词进行出价。当有人搜索某个关键词时,所有对该关键词出价的广告都有资格展示给该用户。

当然,如果有多个广告商都想为同一个查询展示广告,我们需要选择向该查询展示哪个广告。同时,同一个广告商可能对许多不同的查询感兴趣。

我们可以这样建模:我们有广告商1到K。广告商希望为一组特定的查询或关键词展示广告。每当新用户出现并输入查询时,符合条件的广告商集合就确定了。然后,我们需要决定向该用户展示哪个广告。

例如,用户A输入查询,广告商1和4符合条件。我们决定向用户A展示广告1。接着,新用户B到达并输入不同的查询,广告商2和3对此出价。我们必须从2和3中决定向用户B展示哪个广告。假设我们展示了广告2。然后用户C到达,只有广告商1对其查询感兴趣。但由于广告商1的预算可能已经用完,我们无法向用户C展示广告。用户D到达,只有广告商3出价,并且预算充足,我们就可以向用户D展示广告3。

这本质上是一个图匹配问题:我们有一个二分图,试图将左侧节点(广告商)与右侧节点(用户/查询机会)进行匹配。左侧节点只能匹配一个右侧节点。

这是一个在线问题,因为我们必须根据图逐步揭示的信息(即用户按什么顺序到达、提出什么查询)来实时决定将哪个广告商与哪个用户匹配。一旦我们向某个用户展示了广告,就不能再将同一个广告展示给其他用户。广告商提前到来,并声明他们希望出价的查询集合以及预算金额。


在线二分图匹配问题

现在,我们来谈谈这种抽象的在线二分图匹配问题。首先在抽象层面讨论问题,然后再将其与匹配广告商和用户的问题联系起来。

例子:我们有一个二分图,左侧是男孩,右侧是女孩。图中的边表示偏好。目标是匹配男孩和女孩,以满足尽可能多的偏好。

每个男孩最多匹配一个女孩,每个女孩也最多匹配一个男孩。以下是一个匹配示例,其基数(匹配对数)为3。

实际上,可能存在完美匹配:每个男孩和每个女孩都能根据偏好成功匹配。完美匹配总是最大匹配,但最大匹配不一定是完美匹配(例如,存在一个没有任何偏好的孤独男孩)。

问题:如何为给定的二分图找到匹配(最大匹配或完美匹配)?存在基于增广路径思想的Hopcroft-Karp多项式时间离线算法。但该算法假设提前知道整个图。

我们的目标不是提前知道整个图。在线图匹配问题是:我们提前知道一组男孩(例如服务器),在每一轮中,一个女孩(例如任务)的偏好会被揭示。此时,我们有两个选择:要么将女孩与她有偏好的某个男孩配对,要么不配对。如果我们配对,那个男孩就被“占用”了。

实际例子:将任务分配给服务器。任务一个接一个到达,每个任务声明它可以在哪些服务器上运行(偏好集)。我们必须决定是将任务调度到某个服务器,还是让任务等待。


贪心在线匹配算法

现在让我们定义一个算法来解决这个在线匹配问题。

我们将采用一种贪心在线算法:在新的女孩节点到达时,如果在她偏好的男孩中至少有一个是未匹配的,我们就将她与其中一个未匹配的男孩匹配。如果没有符合条件的未匹配男孩,我们就无法匹配这个女孩,继续处理下一个。

问题:这个贪心算法有多好?

我们使用竞争比来衡量算法的好坏。竞争比定义为,对于所有可能的输入序列,贪心算法产生的匹配基数 |M_greedy| 与最优离线算法产生的匹配基数 |M_opt| 之比的最小值。

公式
竞争比 = min_{所有输入I} ( |M_greedy(I)| / |M_opt(I)| )

这衡量了贪心算法在最坏情况下能达到最优解的比例。


贪心算法的竞争比分析

让我们分析贪心算法的竞争比。考虑贪心算法不能给出最优解的情况。

定义

  • G:在最优匹配中被匹配,但在贪心匹配中未被匹配的女孩集合。
  • B:与 G 中女孩相邻的男孩集合。

根据定义,我们可以得到以下不等式:

  1. |M_opt| ≤ |M_greedy| + |G| (最优匹配大小不超过贪心匹配大小加上未被贪心匹配的女孩数)。
  2. 集合 B 中的每个男孩都必须在贪心匹配中被匹配了(否则,如果存在未匹配的男孩与 G 中某个女孩相邻,贪心算法本可以匹配他们,产生矛盾)。因此,|M_greedy| ≥ |B|
  3. 在最优解中,G 中的所有女孩都必须匹配到 B 中的某个男孩。因此,|G| ≤ |B|

结合不等式2和3,我们得到:|G| ≤ |B| ≤ |M_greedy|

现在,结合不等式1和这个关系,在最坏情况下,|G||B| 都等于 |M_greedy|。代入不等式1:
|M_opt| ≤ |M_greedy| + |G| = |M_greedy| + |M_greedy| = 2|M_greedy|

这意味着:
|M_greedy| / |M_opt| ≥ 1/2

因此,贪心算法的竞争比是 1/2。在最坏情况下,它能达到最优解50%的效果。

最坏情况示例:当第一批到达的女孩节点,我们做出了“错误”的匹配决定,消耗了本应在后期用于匹配其他女孩的男孩资源,导致后期无法进行更多匹配。


网络广告简史与模型

现在,让我们将话题转回网络广告,并将其与在线图匹配问题联系起来。

网络广告的早期形式是横幅广告,采用 CPM 模式,即按千次展示付费。广告商为广告展示付费,而不论用户是否点击。这导致用户参与度低,投资回报率也低。

大约在2000年,出现了基于效果的广告,例如 CPC 模式,即按点击付费。广告商只在用户点击广告时才付费。谷歌在2002年采用了这种模式。这种模式激励广告平台展示更相关的广告,因为只有点击才能带来收入。

谷歌AdWords问题:我们有一个到达搜索引擎的查询流。多个广告商对每个查询出价。当查询 q 到达时,搜索引擎必须决定展示哪些广告商子集。目标是最大化搜索引擎的收入。

一个重要细节是:我们不应仅仅展示出价最高的广告,而应展示能带来最高预期收入的广告,即 出价 * 点击率。点击率需要估计,且是用户特定的。

两个主要挑战

  1. 广告商有有限预算,并且对多个查询出价。
  2. 广告的点击率未知,需要预测。

关于点击率的另一个复杂因素是探索与利用的权衡:对于新广告,我们不知道其点击率,是应该尝试展示它以收集数据,还是继续展示已知表现良好的广告?这将在下节课讨论。


简化模型下的贪心算法

如果我们对广告模型做一些简化,之前讨论的贪心图匹配算法可以直接应用。

简化假设

  • 每个查询只展示一个广告。
  • 所有广告商预算相同(B)。
  • 所有广告点击率相同。
  • 每个广告的出价相同(例如1美元)。

在这些假设下,我们可以应用之前的贪心算法:对于一个查询,选择任何对该查询出价且仍有预算的广告商。我们已经知道该算法的竞争比是 1/2

例子:两个广告商A和B,预算均为4。A对查询X出价,B对查询X和Y出价。查询流为:XXXX YYYY。

  • 最优解:将前4个X分配给A,后4个Y分配给B。收入为8。
  • 贪心算法(可能的最坏情况):将前4个X分配给B,耗尽其预算。当Y到达时,A不对Y出价,B已无预算,因此无法展示广告。收入为4。
    竞争比 = 4/8 = 1/2。

Balance 算法及其分析

现在,我们介绍一个性能更好的算法——Balance算法

算法规则:当查询到达时,在所有对该查询出价且仍有预算的广告商中,选择剩余预算最多的那一个。

继续使用上面的例子(A对X出价,B对X和Y出价,预算均为4,查询流:XXXX YYYY)。Balance算法的分配可能如下:X1->A, X2->B, X3->A, X4->B。此时A和B各花费2。当Y到达时,可以将Y1->B, Y2->B。总收入为6。竞争比 = 6/8 = 3/4。

分析(两个广告商情况)
假设有两个广告商A1和A2,预算均为 B。最优解会耗尽两个预算(总收入 2B)。Balance算法至少会耗尽其中一个广告商的预算。假设它耗尽了A2的预算,但分配给A1的查询比最优解少 x 个。那么Balance的收入为 2B - x

通过分析Balance算法总是选择剩余预算最多的广告商这一特性,可以证明 x ≤ B/2。因此,最小收入为 2B - B/2 = 3B/2。竞争比 = (3B/2) / (2B) = 3/4

对于多个广告商,可以证明Balance算法的竞争比为 1 - 1/e ≈ 0.63,并且没有在线算法能取得比这更好的竞争比。

最坏情况场景:有N个广告商,预算 B > N。查询分轮到达,每轮有 B 个查询。出价结构呈“三角形”:第1轮所有广告商都出价,第2轮从第2个到第N个广告商出价,第3轮从第3个到第N个广告商出价,以此类推。

  • 最优解:第1轮所有查询给A1,第2轮所有给A2,...,第N轮所有给AN。总收入 N * B
  • Balance算法:由于总是选择剩余预算最多的,预算会相对均匀地消耗。通过数学分析(利用调和级数和自然对数的关系),可以得出Balance算法在消耗完所有预算前能运行大约 N * (1 - 1/e) 轮。因此,其收入约为 B * N * (1 - 1/e),竞争比即为 1 - 1/e

广义Balance算法

在更一般的情况下,广告商出价和预算都不同。基本的Balance算法可能表现不佳。

例子:广告商1对查询Q出价1美元,预算110。广告商2对查询Q出价10美元,预算100。查询Q出现10次。Balance算法会因为广告商1预算更高而总是选择它,收入10美元。但最优解是选择广告商2,收入100美元。

为了处理不均匀的预算和出价,可以对Balance进行推广:

定义 f_i 为广告商 i 已花费预算的比例。定义函数 ψ_i = bid_i * (1 - e^{-(1 - f_i)})。当查询到达时,将其分配给 ψ_i 值最大的广告商。可以证明,这种推广的算法也能达到 1 - 1/e 的竞争比。


总结

本节课我们一起学习了计算广告中的核心匹配问题。

  1. 我们首先介绍了在线算法与离线算法的区别。
  2. 我们将广告匹配抽象为在线二分图匹配问题,并分析了简单的贪心算法,其竞争比为 1/2
  3. 接着,我们介绍了性能更优的 Balance 算法。对于两个广告商,其竞争比为 3/4;对于多个广告商,其最优竞争比为 1 - 1/e ≈ 0.63
  4. 最后,我们讨论了如何将Balance算法推广到处理不同出价和预算的一般情况。

这些算法为在线广告系统中实时决定广告展示提供了理论基础,确保了平台即使在最坏情况下也能获得有保障的收入比例。

018: 通过实验学习

欢迎来到CS246课程的倒数第二周。首先是一些通知:作业四的截止日期是今晚午夜。恭喜所有坚持完成这些有挑战性但也很令人兴奋的长期作业的同学。下周四我们将进行整个课程的复习课,这对期末考试会非常有帮助。我们还将发布一些往年的期末考试题作为练习材料,请关注课程网站。我们会提供所有必要的工具来帮助大家应对期末考试。

今天我们将讨论“通过实验学习”,这基本上是周二关于网络广告课程的延续。我们之所以关心实验学习,是因为这类技术不仅适用于广告,也普遍适用于需要测试网站多种设置或配置的场景。在本节课结束时,我将为大家提供处理这类问题的一些工具、技巧和方法。

首先,让我们继续讨论网络广告。周二我们主要讨论了如何实时匹配广告商和查询,即如何为用户找到最相关的广告。但我们没有讨论如何估计点击率,即如何确定特定用户点击某个广告的频率。

同样,回想几周前我们讨论推荐引擎时,也强调了“冷启动”这个重大问题。冷启动问题是指,当系统中有新物品或新用户加入时,关于该物品的信息非常稀缺,如何将其推荐给最合适的用户?例如,Netflix上有新电影或新剧集时,如何推荐它?

我们将在这两个看似不同的问题之间建立联系。原因在于,网络广告领域每天都有大量新广告上线,如何将这些新广告匹配给平台上最合适的用户?

我们的目标是:如何尽可能快地通过实验收集关于广告或物品的信息,以便我们能够做出最佳的推荐决策。

其核心思想是:每当展示一个广告或推荐一个产品时,我们都可以收集关于该物品的更多数据。这不仅包括用户点击(表示兴趣)时,也包括当物品或广告被忽略时,我们同样能获得信息。例如,在谷歌上,如果你持续忽略右侧的广告,谷歌会得到一个信号,表明这些广告可能不相关或不有趣。反之,如果你点击了,谷歌会得到一个积极的信号,从而提升这些广告的排名。

因此,直观上看,这些平台并非静态系统,而是始终在运行实验。每次用户互动都可以被利用为一次正在进行的实验,收集有用信息,并基于此做出决策。

让我们回到周二讨论的网络广告例子,并尝试使其更深入一些。假设你的公司(比如谷歌)是硅谷众多依赖广告收入的公司之一。你的目标是最大化收入,即希望展示尽可能多的广告,并确保人们点击、转化、成为用户并支付订阅费等。

传统的广告方式是按展示付费(CPM模式)。这意味着每次在平台上展示广告,你都会向购买广告位的人收费。这种模式不仅来自网络,也来自更古老的媒体,如报纸和广告牌。广告牌越大,你支付的费用就越多,因为展示效果更持久、更容易被看到。

在这种情况下,广告提供者的最佳策略就是选择出价最高者。如果你有多个实体愿意为你的广告空间付费,你只需接受出价最高的那个,这样就能最大化收入。

然而,这种模式被网络革命化的原因是,很难评估这些广告的效果。你只知道广告被展示在报纸或高速公路上,但不知道有多少人会关注它们。

网络广告的新方式是按点击付费(CPC)。在这种模式下,最大化收入的最佳策略是能够计算预期收入。预期收入不仅是广告的函数,也是用户在平台上执行查询的函数。我们需要估计这些广告被点击的频率。

因此,我们的估计收入公式为:
估计收入 = P(点击广告 | 特定查询) * 广告商为该广告支付的金额

这个简单公式的问题在于,等号右边的金额是已知的(广告商出价),但左边的点击概率是未知的。我们不知道用户点击特定广告的概率,必须通过时间来测试。

这正是我们今天要讨论的核心。为了让大家了解这个行业的规模,我查了一下相关文章,这个行业规模大约在1.2万亿美元左右。这是一个巨大的市场,任何能优化收入的技术都意味着在分一块非常大的蛋糕,因此很多人每天都在研究这个问题。

除了广告,这些技术还有更广泛的应用。第一个例子是临床试验。在临床试验中,你需要研究不同治疗的效果,同时希望最小化对患者的不良影响。你在测试新药物或新手术技术时,需要设立治疗组和对照组,但又不希望接受治疗的人遭受不良影响。

另一个完全不同的例子是网络路由。例如,覆盖全球的互联网,其架构是“尽力而为”的,没有固定的路由方案,数据包路由不是确定性的。每次在线发送数据包时,都会进行自适应路由。网络连接中断时,数据包必须被重新路由。因此,目标是通过探索不同路由来最小化网络延迟。你不想有一个从A点到B点的固定路由,而总是希望尝试其他可能的路由,以防万一,或者当某条路由变快时能及时发现。

最后一个例子是资产定价,这对每个初创公司来说都是一个大问题。每个新公司出现在市场上时,如何确定产品价格?一旦你知道生产某个产品的成本,如何正确定价?你需要测试不同的价格点,最终目标是最大化收入。与其在某个价格点上运行三个月再决定是否提价或降价,不如同时测试不同的价格点,找出哪个卖得最好,哪个卖得最差。

我们如何解决这类问题呢?今天我们将讨论老丨虎丨机问题,不是漫画书里的强盗,而是多臂老丨虎丨机。我们从强盗(bandit)联想到了章鱼(octopus)。想象一只擅长赌博的章鱼在拉斯维加斯同时玩多台老丨虎丨机。这类技术之所以被称为“老丨虎丨机”,是因为赌博通常从人们那里抢钱。我们今天要做的就是最小化这种“赌博”会从我们这里抢走多少钱。

那么,K臂老丨虎丨机的设置是怎样的?我们有一台老丨虎丨机,但它的特点是,不是只有一个摇臂,而是有多个摇臂。假设你有5台老丨虎丨机,你想同时使用它们。我们如何描述每个摇臂?每个摇臂a可能赢(给你1个硬币,奖励为1),这有一个固定但未知的概率,我们称之为 μ_a。它也可能输(给你0个硬币,奖励为0),这个概率是 1 - μ_a。另一个重要假设是,所有抽取都是独立的。基本上,我们可以为每个摇臂分配不同的获胜概率。

这里的问题是:我们如何拉动摇臂以最大化总奖励? 现在,开始思考广告工作方式与我们定义这个问题之间的映射关系。基本上,每个查询就是一台老丨虎丨机,每台老丨虎丨机可以展示不同的广告。拉动一个摇臂并获得奖励意味着用户点击了那个广告。这就是将网络广告世界映射到多臂老丨虎丨机问题的方式。

因此,我们想做的是估计每个摇臂的获胜概率 μ_a,在广告背景下,这可以看作是点击率。

这里的直觉是:每次我们拉动一个摇臂,就是在进行一次实验。我们不仅记录用户是否点击了广告,还利用所有这些信息让系统更了解哪些广告被人们喜欢,哪些被忽略。然后基于此,我们慢慢优化策略,决定展示哪些广告,最终淘汰哪些广告。

让我们形式化这个设置。这被称为随机K臂老丨虎丨机。设置如下:K 在整个讲座中代表摇臂的数量,我们有 K 个选择。每个选择 a 与一个未知的概率分布相关联,该分布取值在 0 到 1 之间。我们将进行 T 轮游戏,T 是我们的时间变量。

在每一轮 T,我们执行以下操作:决定选择一个摇臂 a_t,然后从该摇臂的概率分布中获得一个随机样本 x_t。再次提醒,奖励与之前的抽取是独立的。在我们的设定中,每次用户点击或不点击广告,我们都认为这与之前向用户展示的内容无关。

我们的目标是最大化实验运行期间可能获得的总奖励之和。

这里缺少什么?我们仍然不知道每个摇臂的 μ_a,即我们选择的特定摇臂的获胜率。但现在,每次我们拉动摇臂 a,我们都能对 μ_a 了解更多。这正对应我们之前讨论的冷启动问题:当你面对这些新老丨虎丨机时,你对每个摇臂一无所知。但一旦我们开始拉动摇臂,我们就开始收集信息。我们拉动摇臂的次数越多,就越能估计该摇臂的概率分布,最终它会收敛到我们感兴趣的真实值 μ_a。

让我们看一个例子。随着时间的推移,如图所示,这是我们的摇臂选择。我们总共有 K 个摇臂,这些是所有结果。你从机器2开始,然后跳到K,然后看到机器2给了你奖励,你再试一次,得到0,跳到机器1,得到几个奖励,继续前进。

本质上,我们正在运行一种优化算法,但它是在线的,因为我们没有在给定时间 T 获得所有输入,我们必须分析每一步。就像我们过去几周看到的其他在线算法一样,我们必须在每个时间点做出选择。每次我们对系统有更多信息时,我们就能做出更明智的选择。

这里的缺点是,我们只获得关于所选动作的信息。每当我选择摇臂 a2 时,我并没有收集到关于摇臂 a1 的额外信息。因此,我对摇臂 a1 的概率分布的估计保持不变。

这就引出了利用与探索的权衡。如果你信任摇臂 a2 并因为它给你更多奖励而持续拉动它,你就不会探索其他摇臂的潜力。

问:查询是否包含一个摇臂?
答:查询是你的设置。查询给了你这个表格(即所有可能的摇臂/广告)。然后,这些是针对该查询可以展示的不同广告。每个查询都有一个不同的表格。这就是为什么它是多臂老丨虎丨机。如果你把它做成老丨虎丨机,那么你就在系统上进行整个实验。每个查询都是一个独立的实体。有额外的文献表明,当两个查询在语义上接近时,你可以从一个表格中收集信息并转移到另一个,但我们今天不深入讨论。

问:X 在哪里?
答:X 是结果。在时间1,摇臂 a2 给了你奖励 0(没给硬币)。在时间3,它给了你一个硬币。这是一个二进制矩阵,因为我们只考虑点击率,选项只有点击或不点击。

那么,我们如何解决老丨虎丨机问题?基本上,我们寻找的是一种策略(或策略生成函数),它能告诉我拉哪个摇臂。我们的策略(希望)将取决于奖励的历史记录。对于那些正在学习或将要学习强化学习的同学来说,这基本上是一个强化学习问题。我们有一个智能体,并试图为该智能体学习一个策略。我试图在这两个领域之间画一个平行线。

我们如何量化算法的性能?基本上,我们将计算一个称为遗憾的量。直观地说,如果你知道有一个老丨虎丨机摇臂能持续给你更多钱,你应该总是使用那个摇臂。但这基于我们知道所有概率分布的假设,而我们并不知道。因此,我们将尝试尽可能快地估计它们,以最小化我们的遗憾,即最小化我们在这个实验中损失的钱。

让我们更正式地计算遗憾。我们的 μ_a 是概率分布的均值。我们取所有可能的结果,然后取均值。最佳摇臂的奖励是我们的 μ,即所有可能 μ 参数中的最大值。然后,我们直到时间 T 拉动不同的摇臂,在每个时间步,我们可以计算瞬时遗憾。时间 T 的瞬时遗憾就是最佳可能结果 μ 与我们时间 T 选择的摇臂 a_t 的 μ 值之间的差值。

总遗憾的计算方法是对我们考虑的实验中的所有时间步求和。




我们今天要解决的目标是:找到一个策略(决定拉哪个摇臂),保证随着时间趋于无穷,平均遗憾趋于 0。我们希望确保总遗憾除以实验长度的比值趋于 0。有趣的是,我们试图做的这种最小化比之前讨论的更严格。我们不仅仅是试图最大化收入或最小化遗憾,实际上是在寻找最佳策略,因为我们让这个量趋于 0。基本上,随着时间的推移,遗憾最终会变得非常小,并且只在开始时产生,然后在整个实验期间被稀释,这个比值就趋于 0。因此,我们实际上解决了一个比仅仅最小化遗憾更严格的问题。

如果我们知道所有摇臂的收益,策略将是微不足道的:我们总是选择最大值 μ*,即选择所有可能摇臂中最好的 μ。

如果我们只关心估计收益呢?实际上,当我们刚启动系统时,这是我们唯一能负担得起的事情。我们不知道所有的 μ,所以想估计它们。我们做的是:同等地频繁选择每个 K 摇臂。基本上,我们在实验期间进行循环轮询,给每个摇臂 T/K 次拉动。我们如何计算估计值?它不过是所有返回奖励的平均值。这个变量编码了我们的硬币返回:赢为1,输为0。我们取它们的平均值。反过来,遗憾将再次计算为对所有可能摇臂的求和,再次使用最佳策略 μ* 与我们摇臂估计值之间的差值。


这些是我们要处理的两个主要量。

现在让我们尝试解决这个问题。我们说的第一个技术是:既然遗憾是根据平均奖励定义的,如果我们能估计平均奖励,那么我们也能最小化遗憾,这两个量是相关的。第一个能想到的算法是贪心算法:总是选择具有最高平均奖励的动作。

让我们看一个例子。假设 K=2,有两个不同的动作。我们知道动作1以概率 0.3 获得奖励1,动作2以概率 0.7 获得奖励1。所以摇臂2明显比摇臂1好。

现在,看看会发生什么。你玩 a1 并获得奖励1。你玩 a2 却获得奖励0。你认为这里会发生什么?是的,你会更新你的概率估计。你给 a1 更多权重,给 a2 更少权重。在你玩了 a1 和 a2 之后,你会更新你的估计。结果,你对 a1 的估计将等于1,对 a2 的估计将等于0。我们到达了一个状态,a1 的平均奖励永远不会降到0,因此,你将永远不会再玩动作 a2。这是一个病态的例子,虽然很小,但它突出了贪心算法的一个缺点:它可能收敛到一个非最优解。

这在决策中并不少见,是一个非常经典的问题。正如我之前提到的,每当你做决定时,你都想在探索(收集关于收益的数据)和利用(基于已收集的数据做决策)之间找到平衡。

问:在之前的例子中,我们是如何选择初始摇臂的?
答:我们基本上每个都拉了一次。我们今天将看到不同的技术,有些是确定性的,有些不是。

好的,现在我们知道了这种权衡。贪心算法的问题在于,我们非常快地从探索阶段(我们拉了摇臂1和摇臂2)过渡到了利用阶段,然后我们就被困在使用 a1 上,尽管我们知道最终回报会更低,因为其概率更低。因此,贪心算法的问题在于它没有充分探索空间。

更现实地说,探索是指拉动一个你从未拉过的摇臂,而利用是指持续拉动当前具有最高 μ 估计值的摇臂 a。一旦你的系统中有一个摇臂具有最高的可能收益,你就持续拉动那个摇臂。

既然我们已经明确了贪心算法的问题,让我们尝试转向一些能克服过早陷入局部次优解的方法。

这里的想法是研究贪心算法的一个变体,称为 ε-贪心算法。其工作方式如下:我们仍然运行实验 T 步,但我们添加了一个参数 ε,它依赖于 T。我们的 ε(T) 的量级是 1/T。这个参数有趣的地方在于它随时间衰减:实验持续时间越长,它变得越小。

在这个变体中,我们以概率 ε(T) 进行探索,随机均匀地选择一个摇臂。反之,以概率 1 - ε(T) 进行利用,选择具有最高可能奖励的摇臂。

现在,有一篇论文中的定理证明,如果你以合适的方式选择 ε(T),那么以下良好性质成立:在合适的 ε(T) 选择下,我们的总遗憾量级为 K log T。如果我们尝试计算,记得我们曾试图让总遗憾除以实验长度的比值趋于 0。如果我们代入这个量,它的量级是 K log T / T,我们知道当 T 趋于无穷时,这个极限趋于 0,因为 log T / T 趋于 0。这样我们就解决了最小化问题。

但别急。现在我要告诉你为什么在现实中这种方法并不总是被使用。

ε-贪心算法有哪些问题?首先,它不够优雅。它基本上交错进行探索和利用这两个阶段。有些阶段只是探索不同的选项,随机均匀地选择不同的摇臂;另一个阶段只是利用,总是押注于能提供最佳奖励的摇臂。但除此之外,还有一个更重要的根本问题:即使是探索阶段也可能做出糟糕的选择。既然它随机均匀地选择任何摇臂,你认为这为什么是个问题?

想一想。为什么在探索阶段随机选择摇臂是次优的?答案:如果你已经对一个摇臂有信息,如果你已经从某个摇臂拉了100次,每次都是0,你可能不想继续尝试探索那个摇臂。完美的答案。基本上,答案是:如果你已经知道一个摇臂的估计值非常低,为什么还要在探索阶段继续使用它?ε-贪心算法的问题在于,你的探索阶段是没有信息的。所有在时间 T 之前收集的不同摇臂的抽取信息基本上都被忽略了。我们可以做得更好。ε-贪心算法没有标准贪心算法那样陷入困境的问题,但我们知道,通过忽略这些信息,我们可以利用其他东西来获得更好的性能。

因此,探索和利用需要比较不同摇臂的估计结果。我们如何比较摇臂?让我们看一个非常简单的例子,这次不是两个,而是三个摇臂。对于其中一些,我们有很多数值。对于摇臂1,我们知道10次中有5次得到正面(奖励)。对于摇臂2,我们只有一个实例,但知道它是正面的。对于摇臂3,我们知道10次中有8次得到正面。

你接下来会选择哪个摇臂?你如何做出选择?这里需要考虑的关键方面是这些量的分母。对于某些摇臂,我们有很多数据点,但对于摇臂2,我们只有一个数据点。如果我们计算收益估计,摇臂2将是获胜选项,因为它是 1/1。理想情况下,我们会持续利用摇臂2。但实际上,对这个值的信心非常低,因为我们只进行了一次抽取。我们对摇臂1和摇臂3的估计有更高的信心。这就是我们想要利用的关键见解。我们不仅要看均值(期望收益),还要考虑我们期望收益的置信度

现在让我们尝试引入置信度,看看它如何使我们的框架更强大。

我们如何使用置信区间?置信区间基本上可以定义为一个数值范围,我们确信均值以一定的概率落在这个范围内。例如,如果我们知道 μ_a 在 0.2 和 0.5 之间,且这个概率是 0.95,那么我们对结果就比置信值只有 0.5 时更有信心。这就是我们现在关心的主要见解。

如果我们尝试拉动那个摇臂的次数较少,那么我们的估计就不那么准确,因此置信区间会大得多。这里的直觉是:如果你不经常拉动一个摇臂,即使估计值很低,那个摇臂仍然有潜力。你不知道它最终是否会给你更高的回报。相反,你关于一个摇臂的信息越多,区间就越缩小。经过多轮之后,你开始对该摇臂分配的概率分布有信心。

我们如何利用这些信息来获得更好的结果?假设我们知道我们的置信区间。你可以尝试以下方法:不是尝试具有最高均值的动作(这是我们之前做的,我们估计每个摇臂的 μ 值,然后在利用阶段选择具有最高 μ 值的摇臂),我们这里做的是:尝试具有置信区间上界最高的动作。

假设我们的 μ 更容易理解。我们不只关心均值,还关心上界。原因基本上是,我们试图保持乐观。我们说,如果我们从那个摇臂做更多抽取,并且实际上均值慢慢向上界漂移,那么那个摇臂就有更多潜力,应该被更多地探索。这就是使用置信区间而不仅仅是估计值背后的直觉。

这被称为乐观策略。它也常用于贪心算法的上下文中。基本上,它基于这样的信念:在现有证据下,每个动作都尽可能好。

让我们用数字和更好的图示来看看。现在我们有一些图。我们从这里开始。这是摇臂 a 的不同分布。一个分布如下:这是中值,这些是我们置信区间的边界。我们知道在 99.99% 置信区间下,蓝色条的上界要高得多。因此,我们不会总是选择红色的(因为它具有最高的平均奖励),而是会选择蓝色的。在我们进行更多探索之后,蓝色条的置信区间缩小了。我们收集了关于这个摇臂的更多信息,结果发现我们过于乐观了。平均值移动了,但没那么大。现在我们可以回到红色摇臂,因为我们知道它的上界是最高的。

这样,正如你所看到的,我们持续收集信息,持续进行探索,直到置信区间缩小,最终我们有足够的数据来为每个摇臂做出明智的决定,确定哪个是最好的。

将其转化为广告:如果你的系统上有一个新广告,你只展示了一天,它没有得到很多点击,你不会立即放弃。你继续展示它,继续获取数据,然后在你对广告的点击率有了很好的估计之后,再决定是否应该最频繁地展示它。否则,如果你不这样做,广告往往会立即消失,因为有些广告总是被使用,已经被人们点击过了。

问:是否可以采取一种贝叶斯方法,你有一个先验,然后更新你的信念?
答:很好的问题。我们将在讲座末尾讨论汤普森采样,我不会用贝叶斯方法来解释,但你会很容易看到相似之处。基本上,有不同的方法可以对数据进行采样,其中一种正是为不同的概率分配先验,然后你进行的抽取越多,更新的就越多,然后你的采样就会更有信息量。很好的问题,谢谢。

现在,我们如何计算我们的置信边界?情况是这样的:我们的变量 Y_a1 到 Y_aM 是摇臂 a 在前 M 次试验中的收益。试验的总持续时间为 M 步,我们的 Y 变量是随机变量,可以取值在 0 和 1 之间。摇臂 a 的平均收益是我们的 μ_a,我们计算估计值的方法再次是 M 次试验中可能回报的总和除以 M。

我们想做的是:找到边界 B,使得以高概率,真实 μ_a 与估计值之间的差异小于边界 B,并且我们希望边界 B 尽可能小,因为我们希望我们的估计尽可能可靠。

目标是基本上找到这个差异小于我们的量 B(我们的边界)的概率。

我们如何做到这一点?概率论中有一个非常优雅的结果,称为霍夫丁不等式。这个不等式为我们提供了平均值偏离期望值超过一定量的概率上界,这正是我们需要的。我们之前看到,我们有 μ 的期望值,以及我们在 M 次试验后计算的平均值。我们想知道这两个量彼此偏离多少。

再次强调,在理想世界中,我们会知道 μ_a 帽的值,因为这意味着我们已经知道了估计值,但我们不知道。我们必须通过运行多次试验来估计它。我们想找出实际值与我们的估计值之间的差异。

这里的技巧如下:我们采用这个不等式,它以以下方式表达:我们之前讨论的概率,即真实值与期望值之间的差异大于我们的边界 B 的概率,我们知道这个概率将小于 2 乘以 e 的 -2B²M 次方。我们可以称这个为 δ,这将是我们的置信值。

我们如何从这个霍夫丁不等式推导出一些可操作的东西,用于我们的计算?我们发现,置信区间 B 基本上与我们的置信水平相关联。假设我们的 δ 是 95% 或 99%,这将转化为一个特定的 B 值。我们做的是:我们取这个量,然后我们希望这个量小于或等于 δ。然后我们只使用标准的对数性质。我们把 2 留在那里,然后把指数移到另一边。现在我们想提取 B 的值。我们将把 2 移到另一边,应用平方根,得到 B,不等式的方向改变。然后我们得到这个最终结果:B 必须大于等于 以 2 为底的对数 (2/δ) 除以 2M 的平方根。

关于这个量的有趣见解如下:正如你所看到的,试验次数 M 越多(实验运行时间越长),这个量就越大,我们的边界 B 就越小。这就是我们之前所说的:我们从特定摇臂进行的抽取越多,我们对该概率估计的信心就越高。同样,我们的置信水平 δ 越大,我们的 B 就越小。我们越希望置信水平高,我们的边界就越严格,这样我们的 μ 和期望 μ 就越接近。


问题到此为止。很好,让我们继续。

现在我们已经弄清楚了如何应用它。再次强调,B 是我们的上界,M 是我们玩那个动作的次数。我们之前做了数学推导来提取 B。我们可以做的是:将 B 设置为等于这个量。它将等于 以 2 为底的对数 T 除以 M 的平方根的两倍。再次强调,M 是我们在该特定摇臂上进行的试验次数。如果我们这样做,那么我们将得到以下量:我们的两个 μ 在这个特定边界内的概率将小于或等于 2 乘以 T 的 -4 次方。

有趣的是,正如你所想象的,随着时间推移,它很快收敛到 0。时间流逝,很快这个边界将非常接近 0。

这里的陷阱是什么?如果我们不玩动作 A,如果我们基本上忘记了那个老丨虎丨机摇臂,或者我们长时间停止展示某个特定广告,那么我们的上界 B 将会增加。这意味着,在这种策略下,我们永远不会永久排除某个特定选项。某个老丨虎丨机摇臂不会完全从我们的实验中消失,我们会时不时地拉动它,看看是否能进一步更新估计值。相反,上界错误的概率随着时间 T 而减小。实验运行得越久,我们拉动摇臂的次数越多,我们的边界就越小。因此,我们对该特定摇臂的概率分布就越有信心。

如果你想让自己相信这两个结果:为什么上界的概率随时间 T 减小?我们已经看到了。我们在这里看到了。它是 2 乘以 以 2 为底的对数 T,这除以试验次数。如果试验次数随时间减少,基本上,这个量只会增加。时间流逝,我们不持续测试那个特定摇臂,那么试验次数保持不变,我们的边界就会增加。相反,如果你进行更多试验,这个量会增加,我们的边界会减小。这就是背后的直觉。

现在,为什么除了这为问题提供了一个非常优雅的解决方案之外,它还捕捉了另一个重要方面?用户的行为不是静止的。将用户建模为静止的概率分布是非常不现实的,因为我们的品味会随时间变化,我们关心的广告可能在两个月后发生变化,或者我们看的电影会根据季节、心情或测试等而变化。这基本上捕捉了这样一个事实:系统中的每个实体都引入了一些随机性。曾经忽略特定广告的用户,甚至某个曾经忽略特定广告的人口统计群体,他们的品味可能会随时间变化。这就是为什么我们持续运行这些测试。如果广告没有被展示,边界会持续增加。最终,广告会再次出现在谷歌上,然后谷歌会再给它一次运行机会,基本上测试人们是否感兴趣。因此,它也捕捉了用户行为的这个有趣特征。

现在我们如何使用这个置信边界的概念,如何将其插入到我们的多臂老丨虎丨机问题中以使其更高效?这个算法被称为 UCB1,代表“上置信边界”。它之所以被称为“1”,是因为在原始论文中有这个算法的多个变体。今天我们将介绍基本版本,但如果你去看论文,你会看到一堆不同的变体。

我们如何使用它?我们首先将所有系统变量设置为零。我们将我们的新估计值和试验次数设置为 0。


再次强调,μ_a 是我们对特定摇臂 a 的收益估计,M_a 是我们在该摇臂上进行的拉动次数。我们运行实验 T 步。对于每个摇臂 a,我们计算其上界。上界的计算方式如下:我们知道估计值 μ_a(经过几次试验后,我们大致知道摇臂的预期成功率),加上 α 乘以我们之前计算的上置信区间。


我们之前所做的所有数学基本上都归结为这一项。

然后我们的 UCB 策略如下:我们基本上选择摇臂 j,它是所有可能摇臂中具有最大值的那个。然后我们拉动摇臂 j(与最大可能回报相关联的那个)。然后我们观察我们的奖励 Y_t(实际回报)。UCB 的最后一步是更新这个方程。我们在这里做什么?我们更新我们的 M_j(加1步),然后我们将更新我们的 μ_j 估计值。我们这样做的方式如下:让我们拆分这个方程以便你能跟上。在右边,你会看到 M_j - 1 乘以 μ 估计值。这基本上给出了所有先前 M 步的平均值,你乘以所有步数。然后你做的是,加上最后一步的结果。我们的 Y_t 是我们在时间 t 观察到的结果。然后我们再次计算平均值。基本上,我们做的是:每次我们有一个观察结果,我们就更新该摇臂的 μ_j 估计值。

关于这个算法,最后一个有趣的事情是讨论这个 α 参数。α 参数基本上编码了探索与利用之间的权衡。如果你将 α 设置为 0,你认为会发生什么?没有探索,对吧?你将 α 设置为 0,那么你的选择总是基于该特定摇臂的平均估计值。当我们把 α 设为 0 时,我们实际上把它变成了最开始看到的贪心策略。我们计算我们的估计值,然后选择最好的一个,更新,然后回到那一步。这就是我们如何抵消这一项的贡献。相反,如果我们将 α 推到一个非常大的值,我们做的主要是探索。在实际运行的生产系统中,α 的值不是固定一个月左右的。有数据科学家和团队决定 α 随时间变化的值。因此,时间越长,一旦你对初始探索阶段满意,你就尝试更多地转向利用部分。这就是为什么 α 是这种方法中一个非常有用的可调参数。

到目前为止有什么问题吗?好的。

让我们再讨论一下 UCB 的结果。我们的置信区间随着我们采取的行动总数增长而增长,这我们之前已经看到,因为 M 增长,所以边界变小。

同时,这基本上确保了每个摇臂将被无限频繁地尝试。但它仍然在探索和利用之间取得平衡。这就是我之前解释的见解:当我们知道估计值低于某个值时,我们不会永远忽略一个广告或某个老丨虎丨机摇臂。我们会时不时地回到它。

我们的 α 在这里基本上扮演了置信区间的角色。如果我们将 α 设为 0,我们是在说我们确信我们的估计是正确的。相反,如果我们让 α 非常大,那么我们是在说我们不信任我们的估计,然后我们持续进行探索,直到我们对每个摇臂有更多信息。

好的。我们在这里所做的,基本上是我们编码了一类策略,或者几乎是一个哲学论点:我们试图在面对不确定性时引入乐观主义。你可以有不同的策略,你可以更保守,不尝试探索太多。但我们这里做的是,我们相信通过触及状态空间中未探索的部分,我们总能获得额外的奖励。

问:只是想确认一下,这是在线学习吗?
答:正确,它始终依赖我们。是的,它是在线的,因为我们没有不同摇臂的 μ 估计值。我们必须随时间收集它。然后我们基本上随时间学习这些值。正如我之前与强化学习做的类比,你有一个在开始时知识为零的智能体,你玩的轮次越多,智能体获得的知识就越多,它就能做出更好的选择。很好。

好的。我留给你们去查阅,肯定有其他版本的这些论点不是基于完全乐观的,但它们往往更保守。但我们喜欢乐观,所以我告诉你们我们能找到的最乐观的算法。

我们不会为了节省时间而深入探讨其数学细节。如果你回头去看 UCB 论文,他们还证明了这个有趣的不等式。基本上,它表明,如果你试图计算我们之前讨论的总遗憾的期望,这将小于或等于以下量。这个量将是 K 乘以 T 的对数的量级。所以,总遗憾除以我们运行实验的时间量的比值将小于等于 K 乘以 log T / T。再次强调,正如我们之前所说,当时间趋于无穷时,这个量趋于 0。因此,即使是 UCB1 技术也满足我们最初想要的标准:我们希望遗憾随时间趋于 0。所以它与 ε-贪心等的条件完全匹配。

好的,让我们总结一下。然后我将向你们展示一些来自现实世界的例子。

我们已经看到了多臂老丨虎丨机问题,它基本上是探索与利用权衡的形式化。它与我们在课程中讨论的其他在线优化非常相似,比如周二看到的随机梯度下降等。但它带有非常有限的反馈。例如,在 SGD 中,我们有梯度的不同分量,你可以计算导数等等。而在这里,我们只有来自不同摇臂抽取的信息。因此,我们必须用这有限的数据尽力而为。有趣的是,我们介绍了几种算法,即使简单,也能在时间趋于无穷时实现无遗憾。

有什么问题吗?还是我们跳到例子?

问:在极限情况下,我们实现了零遗憾,但在某些情况下,比如医疗案例,拉动老丨虎丨机摇臂就像开处方药,单个错误的代价很高,怎么办?
答:非常好的问题。如果我们不在广告的背景下,错过一次点击并不严重,但如果我们进行临床试验,犯一个大错误会付出更大的代价。我一开始提到临床试验,更多是为了告诉你们这套技术可以应用于那些场景,但当遗憾比广告高得多时,你不会直接使用这种技术。还有其他更保守的方法。正如我之前所说,今天我展示的是最乐观的版本,因为它非常适用于广告或推荐系统,比如 Netflix。但在医疗试验的情况下,你会做一些更保守的事情。谢谢,很好的问题。还有其他问题吗?还是我们跳到例子?

好的,让我们看例子。我想展示的第一件事是:就像计算机科学中的许多其他算法一样,如果你计划将来做研究或已经在做,我总是建议这样做:总是尝试绘制理论最坏情况。在我们的案例中,我们关心累积遗憾(我们想要最小化的量)与实际累积遗憾(你实际得到的)。

让我们以这个例子为例,其中 K=10。我们有10个不同的摇臂。我们运行模拟一百万轮,然后我们有一个给出 0/1 奖励的均匀分布。有趣的是,我们之前理论结果的上界大约在 14000 左右,但实际上真实的累积遗憾很快趋于平稳,大约在 200 左右。这个模拟只是为了展示多臂老丨虎丨机效果非常好,可以给你非常小的遗憾。在我们有了这个初始探索阶段之后,它收敛得非常快。基本上,你从图的这部分看到的非常低的凸起是,我们时不时地尝试其他摇臂,正如我们之前所说。我们为探索所付出的代价非常小。

这就是为什么累积遗憾随时间很快趋于 0。

让我们看一个用例。这更与推荐系统相关。想想 Pinterest。Pinterest 是一个平台,用户在上面创建图板,钉选与不同主题、爱好或他们拥有的物品相关的图片。Pinterest 的问题是,新的图钉或平台上展示的新广告,在开始时没有足够的信号表明它们有多好。一个新用户加入并创建一个新图板,你怎么知道那个图板会对其他人有趣?你如何让人们与那个图板或那个广告互动?

这很好地映射到我们之前看到的多臂老丨虎丨机问题。我们想做的是:尝试最大化来自几个未知老丨虎丨机的奖励。我们的老丨虎丨机基本上是上面不同的图钉。我们想决定我们想玩哪些,以及我们想以什么顺序玩它们。每个图钉被视为一个摇臂,用户参与度被视为奖励。思考这件事非常有趣,归根结底,Pinterest 是一家依靠广告维持的公司。但在这个领域,关键是你有大量的用户参与度,因为你希望你的平台增长。你想吸引更多用户,你想让用户对你产生的内容充满热情和兴趣。因此,我们今天看到的不仅适用于广告,它也可以用来研究你的内容在多大程度上创造了用户留存、用户参与度和流量。再次强调,这里我们想找到探索和利用之间的权衡,因为在这个平台上的风险可能是总是展示人们经常点击的广告。假设你正在查看关于服装的图板。有些品牌是每个人一生中至少买过一次或想要的,你可能只是用同样的品牌不断“轰炸”用户。当然,这是一种会给你带来一些点击率的技术,但它可能会将系统困在某个局部最优解中。这就是为什么你想确保,即使是你向用户展示的内容,你也总是在探索和利用之间取得平衡。

Pinterest 应用的解决方案如下:他们应用标准的老丨虎丨机算法,他们做的是:观察平台上的一组图钉和广告。然后根据人们的收益(人们点击或喜欢某些图片的次数),算法将选择一个特定的摇臂来接收我们的估计收益。


再次强调,我会不断重复这一点:记住,只有所选摇臂的反馈被观察到,这非常重要。每次 Pinterest 决定对某个图钉或某个图板进行排名时,他们不会获得关于用户有多喜欢或认为该内容相关的

019:优化子模函数 🎯

在本节课中,我们将学习一个激动人心的主题:优化子模函数。其核心应用在于以数学化的方式量化多样性,例如在推荐系统中确保推荐内容的多样性。我们将从问题动机出发,介绍子模函数的数学定义与性质,并学习如何利用贪心算法高效地解决这类优化问题。


期末考试安排 📝

在进入正题之前,我们先来了解一下本课程的期末考试安排。

期末考试将于下周二下午3:30至6:30举行。考试地点根据学生证ID的首字母划分:

  • ID以A-L开头的学生,请前往420号楼040室。
  • ID以M-Z开头的学生,请前往Bishop Auditorium。

考试为开卷形式,允许携带笔记和教材,但禁止使用互联网。你可以使用电脑阅读和搜索课程笔记,也可以使用计算器应用进行算术运算,但禁止编写或运行任何代码(例如Python)。请确保你的电脑处于离线状态。

建议携带一个电源排插,以防笔记本电脑电量耗尽。

对于远程学习的SCPD学生,有两种选择:在斯坦福校园参加考试,或通过指定的监考人在24小时窗口期内远程完成考试。具体细节将通过SCPD邮件通知。


问题动机:推荐系统的多样性 🗞️

上一节我们介绍了期末考试的具体安排。本节中,我们来看看今天课程的核心问题:如何在推荐系统中实现多样性。

当我们进行推荐(如新闻、商品)时,展示空间是有限的。如果我们只推荐用户最可能喜欢的单一类型内容,推荐结果会高度冗余。例如,新闻推荐可能只包含关于同一主题的多篇文章。

我们的目标是战略性地利用有限的推荐位,覆盖用户可能感兴趣的所有方面,实现多样性。例如,在一天的新闻中,可能涉及法国干预马里、美国国防部长提名、奥斯卡获奖预测等多个主题。一个好的推荐系统应该覆盖这些不同的主题,而不是重复推荐同一主题的文章。


数学形式化:最大覆盖问题 📐

那么,如何将“多样性”数学化呢?我们的思路是将其建模为一个覆盖问题

我们可以将当天的新闻关键词视为一个“词云”,每篇文章覆盖词云的一部分。我们的目标是选择固定数量(k篇)的文章,尽可能覆盖词云中更多的区域(即更多的关键词)。

以下是具体的数学定义:

  1. 覆盖对象:概念集合(例如,命名实体、关键词),记作 W
  2. 覆盖者:文档集合 D。每个文档 d 覆盖一个关键词子集 X_d ⊆ W

我们的目标函数 F 定义如下:对于选定的文档子集 A ⊆ DF(A) 等于这些文档所覆盖关键词的并集的大小。
F(A) = | ∪_{d∈A} X_d |

优化目标是:找到一个大小恰好为 k 的文档子集 A,使得 F(A) 最大化。这被称为最大覆盖问题


贪心算法及其近似保证 ⚙️

上一节我们将多样性问题形式化为最大覆盖问题。本节中我们来看看如何求解它。

最大覆盖问题是NP难问题,无法高效求得精确最优解。一个简单有效的启发式算法是贪心算法(或称爬山算法)。

算法步骤如下:

  1. 初始化推荐集合 A 为空集。
  2. 进行 k 次循环,每次循环中:
    • 遍历所有尚未入选的文档 d
    • 计算将 d 加入当前集合 A 所带来的边际收益F(A ∪ {d}) - F(A)
    • 选择能带来最大边际收益的文档 d*,将其加入集合 AA = A ∪ {d*}

算法质量保证:Nemhauser, Wolsey, Fisher 在1978年证明,对于具有单调性子模性的函数 F,上述贪心算法得到的解 A 满足:
F(A) ≥ (1 - 1/e) * F(OPT) ≈ 0.63 * F(OPT)
其中 F(OPT) 是最优解的值。这意味着贪心算法至少能获得63%的最优解效果,这是一个非常强的近似保证。


子模函数:定义与性质 🔑

上一节我们提到,贪心算法的优异性能依赖于目标函数 F单调性子模性。本节我们来详细探讨这两个性质。

首先,我们的覆盖函数 F(A) = | ∪_{d∈A} X_d | 显然是单调的:如果 A ⊆ B,那么 F(A) ≤ F(B)。因为增加推荐文档只会覆盖更多关键词,不会减少。

关键在于子模性。它直观上反映了“边际收益递减”规律:向一个较小的集合添加元素带来的收益,大于向一个较大的集合添加同一元素带来的收益。

形式化定义(边际收益递减形式):
函数 F 是子模的,如果对于任意集合 A ⊆ B 和任意元素 d ∉ B,满足:
F(A ∪ {d}) - F(A) ≥ F(B ∪ {d}) - F(B)

在我们的覆盖问题中,这很好理解:当推荐集 A 较小时,新文档 d 可能覆盖许多全新的关键词,边际收益高。当推荐集 B 已经很大时,d 覆盖的关键词可能大多已被 B 中的文档覆盖,因此边际收益较低。

重要性质:子模函数在非负线性组合下是封闭的。即,如果 F1, F2, ..., Fm 都是子模函数,λi ≥ 0,那么 F(A) = Σ λi * Fi(A) 也是子模函数。这个性质在组合多个优化目标时非常有用。


扩展:概率化覆盖与个性化权重 ⚖️

基本的覆盖模型存在一个缺点:它对概念的覆盖是“全有或全无”的。一个文档要么覆盖某个关键词,要么不覆盖,而没有考虑覆盖的强度(例如,关键词出现的频率)以及不同关键词对用户的重要性。

为此,我们引入概率化覆盖模型:

  1. 概念权重:每个概念(关键词)c 对特定用户有一个重要性权重 w_c。这实现了个性化
  2. 覆盖强度:文档 d 覆盖概念 c 的强度定义为 cover_d(c),可以理解为文档中提到概念 c 的概率或频率。
  3. 集合覆盖概率:对于推荐集合 A,它覆盖概念 c 的概率是“至少有一篇文档覆盖c”的概率:
    cover_A(c) = 1 - Π_{d∈A} (1 - cover_d(c))
  4. 新目标函数:我们希望最大化所有概念上的加权覆盖和:
    F(A) = Σ_{c} w_c * cover_A(c)

可以证明,这个新的 F(A) 仍然是单调子模函数。因此,我们依然可以使用贪心算法来近似优化它,并且享有相同的 (1-1/e) 近似比保证。


加速技巧:惰性贪心算法 🐢

贪心算法在每轮迭代中都需要重新计算所有剩余文档的边际收益,时间复杂度为 O(k * |D|)。当文档库 D 很大时,这可能成为瓶颈。

利用子模函数的边际收益递减性质,我们可以采用惰性评估来大幅加速,即“惰性贪心算法”。

核心观察:对于同一文档 d,其在第 i 轮迭代中的边际收益 Δ_i(d),一定大于等于其在后续第 j (j>i) 轮中的边际收益 Δ_j(d)。因为随着集合 A 变大,d 能带来的新收益只会减少。

惰性贪心流程

  1. 第一轮,计算所有文档的边际收益,选出最优者 d1
  2. 后续轮次,我们维护一个按“上一轮计算的边际收益值”降序排列的文档列表(该值是当前边际收益的一个上界)。
  3. 我们只评估列表顶部的文档(即上界最高的文档)的真实边际收益。
  4. 如果评估后,该文档的真实收益仍然高于列表中下一个文档的“上界”,那么由于收益递减性质,我们可以确定它就是本轮最优选择,无需评估其他文档。
  5. 更新选中文档的收益,并调整列表顺序,继续下一轮。

在实践中,惰性贪心能避免大量不必要的边际收益计算,通常比标准贪心快几个数量级。


个性化权重的学习:乘性权重更新 🔄

到目前为止,我们假设概念权重 w_c 是已知的。那么如何从用户交互中学习这些个性化的权重呢?

我们采用乘性权重更新算法:

  1. 初始化所有权重,例如设为1。
  2. 当向用户推荐一个文档集 A 后,收集用户反馈。为简化,假设反馈为“喜欢”(+1)或“不喜欢”(-1)。
  3. 对于被推荐文档覆盖的所有概念 c,根据用户反馈更新其权重:
    • 如果反馈为“喜欢”(+1):w_c = w_c * β,其中 β > 1(如1.1)。
    • 如果反馈为“不喜欢”(-1):w_c = w_c / β
  4. 对于未被覆盖的概念,权重保持不变。

直观解释:如果用户喜欢一篇包含某些概念的文档,我们就提高这些概念的权重;反之则降低。参数 β 控制了更新的幅度。通过持续的反馈循环,权重会逐渐收敛到反映用户真实兴趣的分布。

对于更细致的反馈(如评分1-5星),可以通过调整 β 的取值来对应不同的更新强度。


总结 🎓

本节课我们一起学习了如何优化子模函数来实现推荐系统的多样性。

  1. 问题形式化:将多样性需求建模为最大覆盖问题,目标是选择k个项目以覆盖最多的重要概念。
  2. 核心数学工具:引入了子模函数,其“边际收益递减”的性质是贪心算法有效的关键。
  3. 优化算法:使用贪心算法可以高效地获得至少63%最优效果的近似解。
  4. 加速技巧:利用子模性质设计的惰性贪心算法能极大提升计算效率。
  5. 模型扩展:通过概率化覆盖个性化概念权重,使模型更贴合实际。
  6. 在线学习:通过乘性权重更新算法,可以从用户反馈中动态学习并调整个性化权重,形成完整的推荐闭环。

这套基于子模函数优化的框架,为处理多样性、覆盖性和个性化相结合的复杂选择问题提供了强大而优雅的解决方案。

020: 课程回顾与总结

在本节课中,我们将对斯坦福大学CS246《海量数据集挖掘》课程的核心内容进行一次全面的回顾。我们将梳理本学期所学的关键算法、模型和系统,涵盖从数据处理基础到高级机器学习应用的广泛主题。本次回顾旨在帮助大家为期末考试做好准备,巩固对海量数据挖掘核心概念的理解。


第20章:课程最终回顾

大家好,欢迎来到本学期的最后一节课。在课程开始前,我想说几句话。首先,我要感谢大家在这个季度中的出色表现,感谢你们在作业、讨论和课堂提问中的辛勤付出。你们应该为自己感到自豪,因为你们在本课程中学到了大量非常实用的方法,这些知识工具在未来的研究或工业界工作中处理数据时将会非常有用。祝大家在期末考试中一切顺利,享受即将到来的春假。

现在,教学团队将带领大家回顾本学期涵盖的所有重要主题和概念。


MapReduce与频繁项集挖掘

上一节我们介绍了课程的整体情况,本节中我们来看看数据处理的基础框架MapReduce以及频繁项集挖掘。

MapReduce 是一种为大规模数据集设计的编程模型,易于并行化且具有容错性。其执行分为三个步骤:

  1. Map阶段:处理每个输入,为每个键分配值。
  2. 分组与洗牌阶段:收集键值对,进行排序和分发,将数据分配给不同的Reducer。
  3. Reduce阶段:处理具有相同键的所有数据,合并并输出结果。

MapReduce通过存储中间输出来处理任务失败,可以重启失败的任务而无需重新开始整个作业。Spark等数据流系统则对MapReduce模型进行了泛化,允许更灵活的任务编排。

频繁项集挖掘 在市场篮子分析等场景中非常有用,可用于计算置信度、兴趣度并得出关联规则。

以下是两种在内存中计算频繁项集的主要算法:

  • Apriori算法:采用两轮扫描。第一轮统计单个项的出现次数;第二轮,利用单调性原理,只对由频繁单项组成的候选对进行计数,从而节省内存。
  • PCY算法:同样是两轮扫描,但在第一轮中,它利用剩余内存维护一个用于统计候选对的哈希表。第二轮中,将哈希表转换为位图,用于快速过滤候选对。

对于无法装入内存的超大规模数据,我们可以使用上述算法作为构建模块:

  • 简单随机抽样:但不能保证找到所有频繁项集。
  • SON算法:将数据分成小块处理,可以保证找到所有频繁项集。
  • Toivonen算法改进版:从随机样本开始,不仅计算频繁项集,还计算其负边界。如果在第二轮验证中负边界内没有项集是频繁的,则可以保证已找到所有频繁项集。

局部敏感哈希与聚类分析

上一节我们讨论了如何从海量数据中找出频繁模式,本节中我们来看看如何高效地发现相似项或文档,以及如何对数据进行分组。

局部敏感哈希 是一种用于高效查找相似项的重要算法,其时间复杂度为 O(N) 而非 O(N²)。LSH包含三个步骤:

  1. Shingling:将文档转换为由K个连续词元(K-grams)构成的集合表示。
  2. 最小哈希:构造一个哈希函数,使得两个文档的哈希值相同的概率等于它们的Jaccard相似度。
  3. LSH处理:通过“与”和“或”操作组合多个哈希函数,以放大相似文档被分到同一桶的概率,同时降低不相似文档被分到同一桶的概率。

具体来说,如果采用 b 个波段、每个波段 r 行的策略,两个文档成为候选对的概率公式为:
P = 1 - (1 - s^r)^b
其中 s 是文档间的Jaccard相似度。

聚类分析 旨在将相似的数据点分组。主要有两类方法:

  • 点分配聚类:如K-means算法,初始化中心点,根据距离分配点,然后迭代优化。BFR算法是K-means的改进版,适用于海量数据。
  • 层次聚类:每个点自成一类,然后基于距离度量(如最大距离、平均距离)迭代合并最相似的类。这种方法有时能发现K-means无法发现的特殊形状簇(如环形簇)。

降维与推荐系统

上一节我们探讨了如何发现数据中的相似性与分组,本节中我们来看看如何简化数据表示以及如何构建推荐系统。

进行降维的动机包括节省存储、加速处理、发现隐藏结构等。课程中主要介绍了两种方法:

  • 奇异值分解:将矩阵 M 分解为 U Σ V^T。通过保留最大的 k 个奇异值并置零其余值,可以实现降维。SVD可通过计算 M^T MM M^T 的特征分解来求解。
  • CUR分解:通过非均匀采样矩阵 M 的行和列来构建分解 M ≈ C U R。CUR分解更具可解释性且能保持稀疏性,但可能产生冗余特征。

推荐系统 的目标是根据用户-物品评分矩阵预测缺失的评分。

  • 基于内容的推荐:根据用户过去高评分的物品特征,推荐具有相似特征的物品。
  • 协同过滤
    • 用户-用户协同过滤:基于相似用户对物品的评分进行预测。
    • 物品-物品协同过滤:基于用户对相似物品的评分进行预测。
    • 可使用Jaccard距离、余弦相似度、皮尔逊相关系数等作为相似性度量。
  • 潜在因子模型:将评分矩阵 R 分解为两个低秩矩阵的乘积(R ≈ P Q^T),这些因子代表了用户和物品空间中的抽象概念。通常使用随机梯度下降来最小化包含正则化项的损失函数,以防止过拟合。

PageRank与图算法

上一节我们学习了如何为用户推荐物品,本节中我们来看看如何衡量网页的重要性以及如何在图中发现社区。

PageRank 是一种用于确定网页重要性的方法。一个页面的排名取决于链接到它的页面的数量和重要性。其基本思想可以通过一个迭代公式表示,其中引入了“跳转”概率 β 来避免某些问题。对于页面 j,其PageRank值 r_j 可以通过以下公式迭代计算:
r_j = β * Σ_{(i→j)} (r_i / d_i) + (1 - β) / N
其中 d_i 是页面 i 的出链数,N 是总页面数。这可以转化为矩阵形式并用幂迭代法求解。

主题敏感PageRank 是PageRank的变体,跳转时只跳转到一组特定的相关页面,而不是所有页面。

在图算法中,一个核心问题是发现图中的社区。一个重要工具是个性化PageRank扫描法

  1. 使用个性化PageRank计算图中所有节点的得分。
  2. 按得分对节点排序。
  3. 按顺序将节点加入集合,并计算该集合的传导率
  4. 选择传导率的局部最小值作为社区的边界。

我们还可以进行基于模体的谱聚类,通过将原始图转换为加权图来寻找特定子图结构(模体)定义的密集区域。此外,寻找完全二分子图的问题可以巧妙地转化为频繁项集挖掘问题。

图嵌入 旨在将图中的节点映射到连续的向量空间,同时保留节点在图中的重要属性。Node2Vec算法使用有偏随机游走来生成节点的邻居序列,通过优化目标函数,使得在嵌入空间中节点的相似性(如点积)能够反映它们在随机游走中的共现概率。


监督学习:决策树与支持向量机

上一节我们讨论了图数据的表示与学习,本节中我们回到经典的监督学习领域,看看两种强大的分类与回归模型。

监督学习 的目标是根据带有特征 X 和标签 Y 的数据集,学习一个预测函数。任务主要分为分类(离散标签)和回归(连续标签)。

决策树 通过一系列规则对数据进行划分。

  • 如何分裂:对于回归任务,使用纯度值(方差的加权平均减少量);对于分类任务,使用信息增益(基于熵)。
  • 何时停止:当节点中的数据足够“纯”(方差小)或数据量太少时停止分裂,以防止过拟合。
  • 如何预测:在叶节点,回归任务取平均值,分类任务取众数。

对于海量数据,可以使用MapReduce框架(如PLANET算法)并行构建决策树。装袋法 通过训练多个不同数据子集上的树并聚合结果(如投票或平均)来提升性能。随机森林 在此基础上,在每次分裂时随机选择特征子集进行考虑,以增加多样性,通常能取得极佳的分类效果。

支持向量机 主要用于二分类问题,目标是找到一个最大化分类间隔的超平面。对于非线性可分数据,引入铰链损失和正则化项。最终的优化问题为最小化以下损失函数:
L = 1/2 ||w||² + C Σ_i max(0, 1 - y_i (w·x_i + b))
通常使用梯度下降法求解参数 wb


流算法

上一节我们学习了传统的批量学习算法,本节中我们来看看当数据以高速流的形式到达时,如何用有限的内存实时计算其重要性质。

流算法 用于处理无法存储全部数据的数据流,并实时估计其属性。

布隆过滤器 是一种用于快速判断元素是否可能在集合中的数据结构。它使用一个位数组和多个哈希函数。当查询一个元素时,如果所有哈希位置均为1,则元素可能在集合中(存在误判可能);如果有任一位置为0,则元素肯定不在集合中。误判率随着数组大小增大而降低,但随哈希函数数量先减后增。

Flajolet-Martin算法 用于估计数据流中不同元素的个数。其核心是维护一个哈希值尾部连续零的最大数量 R,最终估计值约为 2^R

AMS方法 用于估计数据流的矩。特别是二阶矩(平方和),可用于估计数据库的自连接大小。算法随机选取流中的一个位置,记录该位置元素值 a 的出现次数 x,则估计值为 m * (2x - 1),其中 m 是流长度。通过多次取样平均可以提高精度。


网络广告与在线匹配

上一节我们研究了数据流的实时处理,本节中我们来看一个重要的应用:网络广告投放,它本质上是一个在线匹配问题。

在线二分图匹配 问题中,我们一边的节点(广告位)依次到达,需要即时决定将其匹配给另一边的哪个节点(广告主),以最大化总匹配数。简单的贪心算法竞争比为 1/2

在网络广告的简单设定下(所有广告主预算相同、点击率和价值相同),Balance算法(选择剩余预算最多的广告主)的竞争比可提升至 1 - 1/e。通过分析两个广告主的情况,可以证明其竞争比为 3/4

在更一般的设定下(出价、预算、点击率均不同),简单的Balance算法竞争比可能降至0。需要使用广义Balance算法,其选择函数同时考虑出价和剩余预算比例,才能重新获得良好的竞争比。


实验学习与多臂老丨虎丨机

上一节我们探讨了在线环境下的决策问题,本节中我们来看看如何通过“尝试-获取反馈-学习”的循环来进行优化,即多臂老丨虎丨机问题。

多臂老丨虎丨机 问题 formalize 了这种探索与利用的权衡。我们有 K 个老丨虎丨机臂,每个臂 a 以固定概率 μ_a 获得奖励。目标是在有限步数内最大化总奖励,但初始时不知道 μ_a

策略需要在探索(尝试新臂以获取信息)和利用(选择当前估计收益最高的臂)之间取得平衡:

  • ε-贪心策略:以概率 ε 随机探索,以概率 1-ε 进行贪心利用。
  • 上置信界算法:为每个臂 a 计算一个置信区间,选择具有最高上置信界的臂,即:
    UCB(a) = avg_reward(a) + c * sqrt( log(T) / n_a )
    其中 n_a 是臂 a 被尝试的次数,T 是总尝试次数。该算法平衡了探索(尝试次数少的臂置信区间宽)和利用(选择估值高的臂)。

总结

在本节课中,我们一起回顾了斯坦福大学CS246《海量数据集挖掘》课程的核心内容。我们从MapReduce数据处理框架和频繁模式挖掘出发,探讨了相似性搜索、聚类分析、降维技术以及推荐系统的构建。接着,我们深入研究了衡量网页重要性的PageRank算法、在图数据中发现社区的方法以及图嵌入技术。然后,我们回顾了监督学习中的两大支柱——决策树与支持向量机。最后,我们涵盖了处理数据流的特殊算法、网络广告中的在线匹配问题以及通过多臂老丨虎丨机模型进行实验学习的基本原理。希望这次系统的回顾能帮助大家巩固所学,为期末考试做好充分准备。祝大家好运!

021:线性代数复习

在本节课中,我们将一起回顾线性代数的基础知识,这些知识对于理解后续课程内容至关重要。我们将从向量和矩阵的基本操作开始,逐步深入到特征值与特征分解等核心概念。

向量与向量运算

首先,我们介绍一些关于列向量和行向量的基本定义。

列向量 v 包含若干元素,这些元素构成一列。其形状为 n × 1,表示它有 n 行和 1 列。

行向量 v 可以写作 [v1, v2, ..., vn]。其形状为 1 × n,表示它有 1 行和 n 列。

接下来,我们讨论点积运算。

假设有两个列向量 uv,它们的点积 u · v 是一个标量(数字),计算公式如下:

u · v = u1*v1 + u2*v2 + ... + un*vn

用求和符号表示为:

u · v = Σ (i=1 to n) ui * vi

点积的结果是一个数,而不是一个向量。

现在,我们来看向量的范数。

向量 v 的范数(通常指 L2 范数)定义为:

||v|| = sqrt(v1² + v2² + ... + vn²)

更一般地,LP 范数的通用公式为:

||v||_p = ( Σ (i=1 to n) |vi|^p )^(1/p)

p=2 时,即为 L2 范数。

最后,我们回顾三角不等式。

对于任意两个向量 uv,三角不等式成立:

||u + v|| ≤ ||u|| + ||v||

以及:

||u - v|| ≥ | ||u|| - ||v|| |

矩阵运算

上一节我们介绍了向量运算,本节中我们来看看矩阵的基本运算。

首先是矩阵加法。进行矩阵加法时,两个矩阵必须具有相同的形状。结果矩阵的每个元素是对应位置元素的和。

例如:

[1 2]   +   [5 6]   =   [6 8]
[3 4]       [7 8]       [10 12]

接下来是矩阵乘法。进行矩阵乘法时,第一个矩阵的列数必须等于第二个矩阵的行数。如果矩阵 A 的形状是 m × n,矩阵 B 的形状是 n × p,那么结果矩阵 C = A * B 的形状是 m × p

结果矩阵 C 中第 i 行第 j 列的元素 C_ij 的计算公式为:

C_ij = Σ (k=1 to n) A_ik * B_kj

例如,计算 [1 2; 3 4] * [5 6; 7 8]

  • C_11 = 1*5 + 2*7 = 19
  • C_12 = 1*6 + 2*8 = 22
  • C_21 = 3*5 + 4*7 = 43
  • C_22 = 3*6 + 4*8 = 50

矩阵乘法满足结合律和分配律:

  • 结合律:(A * B) * C = A * (B * C)
  • 分配律:A * (B + C) = A*B + A*C

但需要注意的是,矩阵乘法不满足交换律,即 A * B 通常不等于 B * A

现在,我们讨论矩阵的转置。

矩阵 A 的转置记作 A^T。其定义是:(A^T)_ij = A_ji。转置操作相当于将矩阵沿主对角线翻转。

例如:

A = [1 2]
    [3 4]
    [5 6]
A^T = [1 3 5]
      [2 4 6]

转置运算具有以下性质:

  1. (A^T)^T = A
  2. (A * B)^T = B^T * A^T
  3. (A + B)^T = A^T + B^T

特殊矩阵与线性无关性

在了解了基本运算后,我们来看几种特殊的矩阵以及线性无关性的概念。

以下是几种重要的矩阵类型:

  1. 对角矩阵:只有主对角线上的元素可能非零,其他位置均为零。其幂运算非常简便,D^k 的结果是将每个对角线元素分别求 k 次幂。
  2. 三角矩阵
    • 下三角矩阵:主对角线以上的元素全为零。
    • 上三角矩阵:主对角线以下的元素全为零。
  3. 对称矩阵:首先必须是方阵(行数等于列数),并且满足 A^T = A
  4. 正交矩阵:满足 U^T * U = U * U^T = I,即其逆矩阵等于其转置矩阵 U^{-1} = U^T。正交矩阵的列向量是一组标准正交基。

接下来,我们讨论线性组合、线性无关、张成空间和基的概念。

一组向量 {v1, v2, ..., vn}线性组合是指形如 α1*v1 + α2*v2 + ... + αn*vn 的向量,其中 αi 是系数。

如果方程 α1*v1 + α2*v2 + ... + αn*vn = 0 的唯一解是所有系数 αi 都为零,则称这组向量线性无关

一组向量的张成空间是指所有能表示为该组向量线性组合的向量构成的集合。

一个向量空间 S是一组线性无关的向量,并且这组向量的张成空间就是 S。空间的维数等于其基中向量的个数。基不唯一,但维数是确定的。

特征值与特征向量

本节我们将进入线性代数中一个核心且重要的部分:特征值与特征向量。

对于方阵 A,如果存在一个标量 λ 和一个非零向量 x,满足以下方程:

A * x = λ * x

那么,λ 称为矩阵 A 的一个特征值x 称为对应于特征值 λ特征向量。注意,零向量不被视为特征向量。

为了求解特征值和特征向量,我们将方程改写为:

(A - λI) * x = 0

其中 I 是单位矩阵。为了得到非零解 x,矩阵 (A - λI) 必须不可逆(即不满秩),这等价于其行列式为零:

det(A - λI) = 0

这个方程称为特征方程。解此方程可以得到所有可能的特征值 λ。对于每一个求得的特征值 λ,将其代回方程 (A - λI) * x = 0,求解得到的非零向量 x 就是对应的特征向量。

特征值和特征向量具有以下重要性质:

  1. 特征向量可以被任意非零标量缩放,通常我们将其规范化为单位长度。
  2. 对于实对称矩阵,其特征值都是实数。
  3. 三角矩阵的特征值就是其主对角线上的元素。
  4. 矩阵的迹(主对角线元素之和)等于其特征值之和。
  5. 矩阵的行列式等于其特征值的乘积。

特征分解

最后,我们介绍基于特征值和特征向量的矩阵分解——特征分解。

如果一个 n × n 方阵 An 个线性无关的特征向量 v1, v2, ..., vn,对应的特征值为 λ1, λ2, ..., λn,那么矩阵 A 可以进行如下分解:

A = P * D * P^{-1}

其中:

  • P 是以特征向量为列构成的矩阵:P = [v1, v2, ..., vn]
  • D 是以特征值为对角线元素构成的对角矩阵:D = diag(λ1, λ2, ..., λn)

特征分解非常有用,特别是在计算矩阵的高次幂时:

A^k = (P * D * P^{-1})^k = P * D^k * P^{-1}

由于 D 是对角矩阵,D^k 的计算非常简单,只需将对角线元素分别求 k 次幂即可,这大大简化了计算。

总结

本节课中我们一起学习了线性代数的核心基础知识。我们从向量和矩阵的基本定义与运算开始,逐步深入到特殊矩阵、线性无关性、张成空间和基等概念。最后,我们重点讲解了特征值、特征向量的求解及其性质,并介绍了强大的工具——特征分解。掌握这些内容将为理解后续课程中更复杂的算法和数据处理技术打下坚实的基础。

022: 证明技巧与概率回顾

在本节课中,我们将学习几种常用的数学证明技巧,并对概率论的基础知识进行回顾。这些内容对于理解课程后续的算法和分析至关重要。

证明技巧

上一节我们介绍了课程概述,本节中我们来看看几种核心的证明方法。

直接证明

直接证明是最基础的证明方法。它通过逻辑推理,从已知条件或公理出发,直接推导出结论。

例如,证明“任何奇数的平方都是奇数”。

  • 设任意奇数为 2k + 1,其中 k 为整数。
  • 其平方为 (2k + 1)^2 = 4k^2 + 4k + 1 = 2(2k^2 + 2k) + 1
  • 由于 2k^2 + 2k 是整数,因此 2(2k^2 + 2k) + 1 是奇数。
  • 因此,任何奇数的平方都是奇数。

逆否证明

有时直接证明一个命题“若A则B”比较困难,可以转而证明其等价的逆否命题“若非B则非A”。

例如,证明“若 x^2 是奇数,则 x 是奇数”。

  • 其逆否命题为:“若 x 是偶数,则 x^2 是偶数”。
  • x = 2k,则 x^2 = 4k^2 = 2(2k^2),显然是偶数。
  • 由于逆否命题为真,原命题也为真。

注意:逆否命题(若非B则非A)与原命题等价,但逆命题(若B则A)则不一定。

反证法

反证法通过假设结论不成立,推导出与已知事实或公理矛盾的结论,从而证明原结论必须成立。

例如,证明“√2 是无理数”。

  • 假设 √2 是有理数,可表示为既约分数 p/qpq 互质)。
  • 2 = p^2 / q^2,即 2q^2 = p^2
  • 因此 p^2 是偶数,p 也是偶数(因为奇数的平方是奇数)。设 p = 2k
  • 代入得 2q^2 = (2k)^2 = 4k^2,即 q^2 = 2k^2
  • 因此 q^2 是偶数,q 也是偶数。
  • 这与 pq 互质的假设矛盾。
  • 因此,假设错误,√2 是无理数。

分情况证明

当一个问题可以自然地划分为有限个互斥的情况时,可以分别证明每种情况下结论都成立,从而证明结论普遍成立。

例如,证明“若整数 n 不能被3整除,则 n^2 可表示为 3k + 1 的形式”。

  • 不能被3整除的整数有两种形式:n = 3p + 1n = 3p + 2
  • 情况1n = 3p + 1。则 n^2 = 9p^2 + 6p + 1 = 3(3p^2 + 2p) + 1,符合形式。
  • 情况2n = 3p + 2。则 n^2 = 9p^2 + 12p + 4 = 3(3p^2 + 4p + 1) + 1,符合形式。
  • 所有情况均已涵盖,故命题得证。

数学归纳法

数学归纳法用于证明与自然数 n 相关的命题。它包含两个步骤:

  1. 基础步骤:证明当 n = 1(或某个起始值)时命题成立。
  2. 归纳步骤:假设当 n = k 时命题成立(归纳假设),证明当 n = k + 1 时命题也成立。

例如,证明求和公式 1 + 2 + ... + n = n(n+1)/2

  • 基础步骤n=1 时,左边=1,右边=1*2/2=1,成立。
  • 归纳步骤:假设 n=k 时公式成立,即 1+2+...+k = k(k+1)/2
  • 则当 n=k+1 时,左边=(1+2+...+k) + (k+1) = k(k+1)/2 + (k+1) = (k+1)(k/2 + 1) = (k+1)(k+2)/2
  • 这正是 n=k+1 时的右边。因此,由归纳法可知公式对所有自然数 n 成立。

概率论基础

上一节我们介绍了多种证明技巧,本节中我们来看看概率论的核心概念。

基本定义

  • 样本空间 Ω:一个实验所有可能结果的集合。例如,掷一个公平骰子,Ω = {1, 2, 3, 4, 5, 6}。
  • 事件:样本空间 Ω 的任意子集。例如,事件 A = “掷出奇数” = {1, 3, 5}。
  • 概率函数 P:为每个事件 A 分配一个介于 0 和 1 之间的数值 P(A),表示该事件发生的可能性。它满足:
    • P(Ω) = 1
    • 对于互斥事件(即 A ∩ B = ∅),有 P(A ∪ B) = P(A) + P(B)

事件运算与概率

  • 并集 A ∪ B:事件 A B 发生。
  • 交集 A ∩ B:事件 A B 同时发生。
  • 一般加法公式P(A ∪ B) = P(A) + P(B) - P(A ∩ B)。减去交集是为了避免重复计算。
  • 并集上界(Union Bound):对于任意事件集合 A1, A2, ..., An,有 P(∪ Ai) ≤ Σ P(Ai)。这是一个有用的上界估计工具。

条件概率与独立性

  • 条件概率 P(A|B):在事件 B 已发生的条件下,事件 A 发生的概率。公式为:
    P(A|B) = P(A ∩ B) / P(B)
  • 事件独立性:如果 P(A|B) = P(A),或等价地 P(A ∩ B) = P(A)P(B),则称事件 A 和 B 相互独立。这意味着 B 的发生不影响 A 发生的概率。

贝叶斯定理

贝叶斯定理描述了如何根据新证据(B 发生)更新对某个假设(A 发生)的概率估计。公式为:
P(A|B) = [P(B|A) * P(A)] / P(B)
其中 P(B) = P(B|A)P(A) + P(B|¬A)P(¬A)

应用示例(疾病检测)

  • 已知某疾病在人群中的患病率 P(病) = 0.01
  • 检测的灵敏度(真有病且检测阳性)P(阳|病) = 0.9
  • 检测的假阳性率(真没病但检测阳性)P(阳|无病) = 0.1
  • 问:若某人检测呈阳性,其真实患病的概率 P(病|阳) 是多少?
  • 根据贝叶斯定理:P(病|阳) = (0.9 * 0.01) / (0.9*0.01 + 0.1*0.99) ≈ 0.083
  • 结果显示,即使检测呈阳性,真实患病的概率也只有约 8.3%。

随机变量及其分布

  • 随机变量 X:将样本空间中的每个结果映射到一个实数的函数。
  • 概率质量函数(PMF):描述离散随机变量取每个特定值的概率。
  • 概率密度函数(PDF):描述连续随机变量在某个值附近的可能性密度。X 落在区间 [a, b] 的概率为 ∫_a^b f(x) dx。PDF 满足 f(x) ≥ 0∫_{-∞}^{∞} f(x) dx = 1
  • 累积分布函数(CDF)F(x) = P(X ≤ x)。对于连续随机变量,F(x) = ∫_{-∞}^{x} f(t) dt。CDF 是单调非减且右连续的函数。

期望与方差

  • 期望(均值)E[X]:随机变量的加权平均值,衡量其中心趋势。
    • 离散:E[X] = Σ x * P(X=x)
    • 连续:E[X] = ∫ x * f(x) dx
  • 期望的线性性质E[aX + b] = aE[X] + b,且 E[X+Y] = E[X] + E[Y](即使 X 和 Y 不独立也成立)。
  • 方差 Var(X):衡量随机变量取值与其均值的偏离程度。Var(X) = E[(X - E[X])^2] = E[X^2] - (E[X])^2
  • 方差的性质Var(aX + b) = a^2 Var(X)Var(X+Y) = Var(X) + Var(Y) 仅当 X 与 Y 不相关时成立(独立必不相关,反之不必然)。

常见随机变量分布

以下是几种重要的分布:

离散分布

  • 伯努利分布:单次试验,成功概率为 pX ~ Bernoulli(p)
    • P(X=1)=p, P(X=0)=1-p
    • E[X] = p, Var(X) = p(1-p)
  • 几何分布:进行一系列独立伯努利试验,直到第一次成功所需的试验次数。X ~ Geometric(p)
    • P(X=k) = (1-p)^{k-1} p
    • E[X] = 1/p, Var(X) = (1-p)/p^2

连续分布

  • 均匀分布:在区间 [a, b] 上等可能取值。X ~ Uniform(a, b)
    • PDF: f(x) = 1/(b-a), for a ≤ x ≤ b
    • E[X] = (a+b)/2, Var(X) = (b-a)^2/12
  • 正态(高斯)分布:最重要的连续分布之一,由均值 μ 和方差 σ^2 参数化。X ~ N(μ, σ^2)
    • PDF: f(x) = (1/√(2πσ^2)) * exp(-(x-μ)^2/(2σ^2))
    • E[X] = μ, Var(X) = σ^2

概率不等式

最后,我们介绍两个用于概率估计的有力不等式。

  • 马尔可夫不等式:对于取非负值的随机变量 X 和任意 a > 0,有:
    P(X ≥ a) ≤ E[X] / a
    它给出了随机变量超过某个阈值的概率上界。

  • 切比雪夫不等式:对于任意随机变量 X(期望为 μ,方差为 σ^2)和任意 k > 0,有:
    P(|X - μ| ≥ k) ≤ σ^2 / k^2
    它给出了随机变量偏离其均值超过 k 个单位的概率上界。它可以由马尔可夫不等式应用于 (X-μ)^2 推导出来。

总结

本节课中我们一起学习了数学证明的几种核心技巧,包括直接证明、逆否证明、反证法、分情况证明和数学归纳法。随后,我们回顾了概率论的基础知识,涵盖了样本空间、事件、条件概率、贝叶斯定理、随机变量、期望、方差以及几个重要的概率分布(伯努利、几何、均匀、正态)。最后,我们介绍了马尔可夫不等式和切比雪夫不等式,它们是进行概率上界估计的有用工具。掌握这些内容将为学习海量数据集挖掘中的算法分析和概率模型打下坚实基础。

023: Spark 快速入门教程

在本节课中,我们将要学习 Spark 的基础知识。Spark 是一个用于大规模数据处理的强大框架,相较于早期的 MapReduce,它提供了更灵活、更高效的编程模型。我们将从如何启动 Spark 开始,逐步介绍其核心概念——弹性分布式数据集(RDD),并学习其基本操作。课程内容力求简单直白,让初学者能够快速上手。

P23-1:Spark 入门与部署模式

首先,如果你还没有安装 Spark,请下载 Spark 2.2.1 for Hadoop 版本。

Spark 有三种主要的部署模式:

  • 本地模式:在单个 JVM 中运行的单节点实例,通常用于在个人电脑上进行开发和测试。
  • 独立集群模式:在一组专用节点上设置 Spark 集群,所有计算都在这些 Spark 节点上进行。
  • 托管集群模式:通过资源管理器(如 YARN)来管理集群资源。当你提交 Spark 任务时,资源管理器会为你动态分配资源并启动一个 Spark 集群,任务结束后再回收资源。这种模式可以实现资源的高效共享,尤其适合同时运行 Spark 和 MapReduce 等不同框架的环境。

部署模式的选择也影响了数据的存储位置。在本地模式下,数据通常存储在本地磁盘。在分布式集群模式下,你需要一个分布式存储解决方案(如 HDFS 或 S3)来存储数据,以便所有节点都能访问,否则无法进行有效的分布式计算。

P23-2:理解 RDD(弹性分布式数据集)

上一节我们介绍了 Spark 的部署模式,本节中我们来看看 Spark 的核心抽象——RDD。RDD 是 Spark 的基本构建块,它是一个不可变的、可分区的数据集合。

RDD 有几个关键特性:

  • 不可变性:一旦创建,RDD 的内容就不能被修改。任何转换操作都会生成一个新的 RDD,这个新 RDD 记录了从父 RDD 到当前状态的“差异”,从而形成一个 RDD 的血缘关系图。
  • 惰性计算:RDD 的转换操作(如 mapfilter)是惰性的。它们只是定义了计算逻辑,并不会立即执行。只有当遇到一个行动操作(如 collectcount)需要输出结果时,Spark 才会根据血缘关系图触发实际的计算。
  • 容错性:得益于血缘关系图,如果某个 RDD 的分区数据丢失,Spark 可以追溯到源头数据并重新计算,从而实现容错。
  • 缓存:Spark 会乐观地缓存中间计算结果。如果内存不足,它会使用 LRU(最近最少使用)策略将数据移出缓存。你也可以手动指定某些 RDD 持久化到内存或磁盘。

RDD 主要有三种类型:

  • 普通 RDD:Spark 对其内容一无所知,操作相对有限。
  • 数值型 RDD:当 RDD 只包含数字时,Spark 能识别并允许你进行如 meansum 等统计操作。
  • 键值对 RDD:当 RDD 的元素都是二元组时,它被视为键值对 RDD。Spark 假设第一个元素是键,第二个元素是值,并允许你进行 reduceByKeygroupByKeyjoin 等基于键的操作。

P23-3:RDD 的操作:行动与转换

理解了 RDD 的基本概念后,本节我们来看看能对 RDD 执行的两类操作:行动转换

行动操作会触发实际计算,并返回一个非 RDD 的结果(如值、数组或直接将数据写入存储系统)。以下是几个常用的行动操作:

  • take(n):返回 RDD 中的前 n 个元素组成的数组。这是一个很好的调试工具,用于检查中间结果。
  • collect():返回 RDD 中的所有元素。注意:如果数据量巨大,此操作可能导致驱动程序内存溢出。
  • count():返回 RDD 中元素的个数。
  • saveAsTextFile(path):将 RDD 的内容保存为文本文件。路径可以指向本地文件系统、HDFS 或 S3。
  • foreach(func):对 RDD 中的每个元素应用函数 func,通常用于产生副作用(如打印)。

转换操作会从一个已有的 RDD 创建一个新的 RDD,但不会立即计算。以下是核心的转换操作:

  • map(func):将 RDD 中的每个元素通过函数 func 进行转换。例如,将文本行分割成单词数组。
    # 假设 data 是一个包含 “Apple,Amy” 等字符串的 RDD
    mapped_data = data.map(lambda line: line.split(','))
    # 结果:每个元素变成一个数组,如 [‘Apple‘, ‘Amy‘]
    
  • flatMap(func):与 map 类似,但要求 func 返回一个序列(如列表),然后“压平”这个序列,将其中所有元素合并成一个新的 RDD。
    flat_mapped_data = data.flatMap(lambda line: line.split(','))
    # 结果:一个包含所有单词的 RDD,如 ‘Apple‘, ‘Amy‘, ‘Bob‘, ‘Bob‘...
    
  • filter(func):返回一个由通过函数 func 筛选的元素组成的新 RDD。
    filtered_data = data.filter(lambda line: line.startswith(('A','E','I','O','U')))
    # 结果:只保留以元音字母开头的行
    
  • mapValues(func):仅适用于键值对 RDD。它对每个键值对中的应用函数 func,而不改变键。
  • groupByKey():将键值对 RDD 中具有相同键的值分组。这是一个宽依赖操作,会引起数据洗牌。
    # 假设 pair_rdd 为 [(‘Apple‘, ‘Amy‘), (‘Apple‘, ‘Alex‘), (‘Banana‘, ‘Bob‘)]
    grouped = pair_rdd.groupByKey()
    # 结果:[(‘Apple‘, <iterable of [‘Amy‘, ‘Alex‘]>), (‘Banana‘, <iterable of [‘Bob‘]>)]
    
  • reduceByKey(func):使用函数 func 合并具有相同键的值。func 必须是结合律交换律的(如加法)。它比 groupByKey 更高效,因为会在洗牌前先在本地进行合并。
    # 假设 word_pairs 为 [(‘a‘, 1), (‘b‘, 1), (‘a‘, 1)]
    reduced = word_pairs.reduceByKey(lambda x, y: x + y)
    # 结果:[(‘a‘, 2), (‘b‘, 1)]
    
  • sortBy(func) / sortByKey():根据给定的键或函数对 RDD 进行排序。
  • subtract(otherRDD):返回一个包含在第一个 RDD 中但不在第二个 RDD 中的元素的新 RDD(集合差集)。
  • join(otherRDD):对两个键值对 RDD 进行内连接,返回键在两个 RDD 中都存在的键值对。还有 leftOuterJoinrightOuterJoinfullOuterJoin

P23-4:编程语言选择与实战演示

上一节我们介绍了 RDD 的各种操作,本节我们来看看在实际编程中如何选择语言,并通过一个简单的例子来演示。

Spark 支持多种编程语言,主要包括 Python、Scala 和 Java。

  • Python:通过 PySpark 使用。语法简洁,易于上手,交互式编程体验好,是大多数初学者的首选。缺点是它不是 Spark 的原生语言(Spark 用 Scala 编写),某些高级功能可能支持不全或有细微差异。
  • Scala:Spark 的原生语言,性能最佳,API 最完整。语法非常简洁(有时甚至令人困惑),但类型系统强大,能在编译期捕获更多错误。学习曲线较陡。
  • Java:API 可用,但由于 Java 缺乏元组等原生支持,代码会显得非常冗长,需要处理复杂的泛型类型声明,开发体验较差。

实战演示:使用 Python 进行单词计数

让我们在 PySpark 交互式环境中快速实现一个单词计数的核心部分:

# 1. 启动 PySpark 后,首先加载一个文本文件(这里使用本地 /etc/hosts 示例)
hosts_rdd = sc.textFile(“/etc/hosts“)

# 2. 使用 flatMap 将每一行分割成单词,并压平
words_rdd = hosts_rdd.flatMap(lambda line: line.split())

# 3. 将每个单词映射成 (word, 1) 的键值对形式
word_pairs_rdd = words_rdd.map(lambda word: (word, 1))

# 4. 使用 reduceByKey 对相同单词的计数进行累加
word_counts_rdd = word_pairs_rdd.reduceByKey(lambda x, y: x + y)

# 5. 触发计算并查看部分结果
print(word_counts_rdd.take(10))

Scala 的简洁性与陷阱

在 Scala 中,同样的 reduceByKey 操作可以写得极其简洁:

val reduced = wordPairs.reduceByKey(_ + _)

这里的 _ + _ 是 Scala 的语法糖,第一个 _ 代表第一个参数,第二个 _ 代表第二个参数。

但 Scala 也有其独特之处,例如访问元组的元素:

val firstElem = myTuple._1 // 获取元组的第一个元素(注意是 1-based 索引)

关于项目构建

如果你使用 Scala 或 Java 编写独立的 Spark 应用(而非在 REPL 中),你需要使用 MavenSBT 这样的构建工具来管理依赖和打包。Maven 的配置文件是 pom.xml,它定义了项目结构、依赖库等。虽然功能强大,但其配置和错误排查对新手可能有一定挑战。Python 项目则通常不需要复杂的构建步骤。

P23-5:总结与建议

本节课中我们一起学习了 Spark 的快速入门知识。

我们首先了解了 Spark 的三种部署模式:本地、独立集群和托管集群,以及它们对数据存储的影响。接着,我们深入探讨了 Spark 的核心——弹性分布式数据集(RDD),理解了其不可变性、惰性计算和容错性等关键特性。然后,我们系统学习了 RDD 的两类操作:触发计算的行动(如 collect, count)和生成新 RDD 的转换(如 map, filter, reduceByKey, join),并通过一个简单的单词计数例子进行了演示。最后,我们比较了 Python、Scala 和 Java 三种编程语言在 Spark 开发中的优缺点。

对于 CS246 课程的学习,我们给出以下简明建议:

  1. 首选 Python:语法简单,易于调试,能让你更专注于算法和数据处理逻辑本身。
  2. 理解惰性计算:牢记转换操作是“懒”的,直到行动操作才会执行。利用 take(n) 进行阶段性调试。
  3. 区分宽窄依赖:了解像 groupByKeyjoin 这类会引起数据洗牌的宽依赖操作,它们代价较高。
  4. 谨慎使用 collect():确保数据量小到能放进驱动程序内存时再使用。

希望这篇教程能帮助你顺利开始使用 Spark 进行海量数据挖掘。

posted @ 2026-03-26 13:17  布客飞龙V  阅读(0)  评论(0)    收藏  举报