Apache Spark 配置设置的基础
使用 YARN 作为集群管理框架决定 Pyspark 配置参数
Apache Spark 是最流行的开源分布式计算平台之一,用于内存批处理和流处理。虽然它有望以常规方式快速处理数百万条记录,但如果最初配置不当,可能会导致无法接受的内存和 CPU 使用结果。Spark 应用程序的资源利用率非常重要,尤其是在 AWS 这样的云平台中。不必要的内存和 CPU 资源的使用以及长时间的工作过程可能会大大增加成本。
为了让 Spark 以高性能工作,出现了基于配置级和代码级的两个不同点。前者是在初始阶段正确配置 Spark,而后者是在考虑性能问题的情况下开发/审查代码。在这篇文章中,我的目标是深入了解配置设置。我还打算写另一篇关于编码最佳实践的文章。这篇文章主要是针对以集群模式运行纱线的 Pyspark 应用程序。
迭戈·根纳罗在 Unsplash 上的照片
火花建筑——以简单的方式
在继续讨论之前,我将简要介绍一下 Spark 架构和术语。Spark 使用主/从架构,带有一个名为驱动的中央协调器和一组名为执行器的可执行工作流,它们位于集群中的各个节点。
资源管理器是集群中所有应用之间资源分配的决策单元,是集群管理器的一部分。集群管理器是在集群上以容器的形式控制、管理和保留计算资源的进程。Spark 应用程序有许多集群管理器选项,其中之一是 Hadoop YARN。
当一个 Spark 应用启动时,资源管理器启动应用主机(AM) 并为其分配一个容器。AM 协调其应用程序中所有任务的执行。AM 可以被认为是一个非执行容器,具有向纱线请求容器的特殊能力,占用自己的资源。
一旦 AM 启动,它向资源管理器请求容器和容器的资源请求。成功接收集装箱后, AM 将其投放。在容器内部,它通过驱动运行用户应用程序代码。应用完成后, AM 将资源释放回资源管理器。如果 AM 崩溃或变得不可用,资源管理器可以创建另一个容器,并在其上重启 AM 。
驱动以集群模式放置在 AM 内部,负责将用户应用转换成更小的执行单元,称为任务,然后调度它们在执行器上运行。这些任务在 worker 节点上执行,然后将结果返回给驱动程序。驱动也通知 AM 执行者对应用的需求。它运行在自己的 JVM 中。执行者是工作者节点上的进程(计算单元),其工作是完成分配的任务。一个执行器容器就是一个 JVM。
Spark 上下文是 Spark 功能的主要入口。 Spark Context 还通过定期发送心跳消息来实时跟踪执行者。 Spark 上下文是由驱动为每个 Spark 应用程序创建的,当用户第一次提交时。它存在于 Spark 应用程序的整个生命周期中。火花上下文在火花应用完成后停止工作。
Spark 作业有两种不同的运行模式— 客户端模式和集群模式。区别主要取决于驱动在哪里运行。当它在客户端模式的客户端进程中运行时,它在集群模式的 AM 中运行。在生产中,集群模式是有意义的,客户端可以在初始化应用程序后离开。
纱线相关参数
Spark 的领先集群管理框架之一是 YARN。在纱线术语中,执行器和 AM 在容器内运行。容器就是内存和 CPU 的简单分配。当 Spark 应用在集群模式下通过 YARN 提交时,资源将由资源管理器以容器的形式进行分配。
在 yarn-site.xml 文件中,如果 Spark 作为集群管理框架与 yarn 一起使用,调整以下参数是一个很好的起点。在同一集群中运行火花应用和非火花应用的情况下,首先正确设置这些纱线参数是非常重要的。在某种程度上,这些参数将定义集群中 Spark 应用程序的边界。以下是需要正确设置的基本纱线参数。
yarn . node manager . resource . memory-MB
yarn . scheduler . max-allocation-MB
yarn . scheduler . minimum-allocation-MB
yarn . node manager . resource . CPU-v cores
yarn . scheduler . maximum-allocation-v cores
yarn . scheduler . minimum-allocation-v cores
yarn . node manager . resource . memory-MB简单来说就是单个节点中可以分配给容器的物理内存量。考虑到操作系统守护进程和节点中其他正在运行的进程,它必须低于节点的总 RAM 值。yarn . scheduler . minimum-allocation-MB和yarn . scheduler . maximum-allocation-MB参数分别表示单个容器可以获得的最小和最大内存分配值。
类似的推理也适用于容器的 CPU 分配。yarn . node manager . resource . CPU-vcores确定为单个节点中的所有容器分配的总可用 v cores。并且每个容器在作为下限和上限的yarn . scheduler . minimum-allocation-vcores和yarn . scheduler . maximum-allocation-v cores参数的值内获得 v cores。
Spark 中的内存管理
与 Spark 中的 CPU 利用率相比,内存利用率有点棘手。在深入研究配置调优之前,了解一下内存管理方面的情况会很有帮助。下图很好地总结了 Spark 中的内存管理。
火花记忆化合物
执行器容器(它是一个 JVM)分配一个由三部分组成的内存部分。它们分别是堆内存、堆外内存和开销内存。属性spark . memory . off Heap . enabled .默认禁用堆外内存。要使用堆外内存,可在启用后通过spark . memory . off Heap . size设置堆外内存的大小。关于 Spark 应用程序中堆外内存使用的详细解释,以及利弊可以在这里找到。
内存开销可以用spark . executor . memory overhead属性设置,默认情况下是 10%的 executor 内存,最小 384MB。它基本上涵盖了费用,如虚拟机管理费用,实习字符串,其他本地管理费用等。
有趣的事情开始了。与堆外内存不同,堆内存中的所有对象都由垃圾收集器(GC)绑定。为了简单起见,可以认为它由 3 个不同的区域组成,保留内存、用户内存和包括执行和存储内存的统一区域。保留内存用于存储内部对象。它是硬编码的,等于 300MB。
用户内存是为用户数据结构、Spark 中的内部元数据保留的,并在记录稀少且异常大的情况下防止 OOM 错误。使用 spark.memory.fraction 属性通过下面的公式简单估算。它决定了有多少 JVM 堆空间用于 Spark 执行内存。建议将此属性的默认值设置为 0.6。因此,用户内存等于 JVM 执行器内存(堆内存)的 40%。
User Memory = (Heap Size-300MB)*(1-spark.memory.fraction)# where 300MB stands for reserved memory and spark.memory.fraction propery is 0.6 by default.
在 Spark 中,执行和存储共享一个统一的区域。当不使用执行内存时,存储可以获取所有可用内存,反之亦然。在必要的情况下,执行可能会驱逐存储器,直到由spark . memory . storage fraction属性设置的某个限制。超过这个限制,执行在任何情况下都不能驱逐存储。该属性的默认值为 0.5。这个调整过程被称为动态占用机制。这种统一的内存管理是 Spark 从 1.6 开始的默认行为。执行和存储内存的初始值通过以下公式计算。
Execution memory = Usable Memory * spark.memory.fraction*(1-spark.memory.storageFraction)Storage memory = Usable Memory * spark.memory.fraction*spark.memory.storageFraction
执行内存用于存储混洗、连接、聚合、排序等操作中的临时数据。注意,数据操作实际上是在这一部分中处理的。另一方面,存储内存用于存储缓存和广播数据。正如预期的那样,执行内存优先于存储内存。任务的执行比缓存的数据更重要。如果执行内存不足,整个作业可能会崩溃。此外,重要的是要记住,在调整动态占用机制的参数时,驱逐过程是有代价的。内存回收的成本取决于缓存数据的存储级别,如 MEMORY_ONLY 和 MEMORY_AND_DISK_SER。Spark 中内存管理的清晰解释可以在这里找到。此外,您可以在这里找到另一个关于垃圾收集的内存管理视图。
主要配置设置
在设置相应的纱线参数并了解 Spark 中的内存管理后,我们进入下一部分——设置内部 Spark 参数。
正确设置下面列出的配置参数非常重要,它基本上决定了 Spark 的源消耗和性能。让我们来看看他们。
**spark . executor . instances:**spark 应用程序的执行者数量。
spark.executor.memory: 运行任务的每个执行器使用的内存量。
spark.executor.cores: 一个执行器可以运行的并发任务的数量。
spark.driver.memory: 数量用于驱动的内存。
spark.driver.cores: 用于驱动程序进程的虚拟内核数量。
**spark . SQL . shuffle . partitions:**为连接或聚合而重排数据时使用的分区数量。
**spark . Default . parallelism:**由连接和聚合等转换返回的弹性分布式数据集(rdd)中的默认分区数。
通过一个例子来理解配置设置背后的推理更好。假设我们有一个由 3 个节点组成的群集,具有指定的容量值,如下图所示。
一个火花簇例子
第一步是设置 spark.executor.cores 即多半是一个简单明了的属性。将大量的 vcores 分配给每个执行器会导致执行器数量的减少,从而降低并行性。另一方面,将少量的 vcores 分配给每个执行器会导致大量的执行器,因此可能会增加应用程序中的 I/O 成本。在上述标准的照明中,作为经验法则,一般设置为 5 。
第二步是决定spark . executor . instances属性。为了计算这个属性,我们首先确定每个节点的执行器数量。每个节点可能会为 Hadoop 和 OS 守护程序保留一个 vcore。这不是一条经验法则,您可以向系统管理员寻求帮助来决定这些值。
executor_per_node = (vcore_per_node-1)/spark.executor.coresexecutor_per_node = (16–1)/5 = 3spark.executor.instances = (executor_per_node * number_of_nodes)-1 spark.executor.instances = (3*3)-1 = 8
第三步是决定 spark.executor.memory 属性。为此,首先计算总的可用执行器内存,然后考虑内存开销,并从总的可用内存中减去。类似地,每个节点可能会为 Hadoop 和 OS 守护程序保留 1 GB。请注意,运行内存过多的执行器通常会导致过多的垃圾收集延迟。
total_executor_memory = (total_ram_per_node -1) / executor_per_nodetotal_executor_memory = (64–1)/3 = 21(rounded down)spark.executor.memory = total_executor_memory * 0.9spark.executor.memory = 21*0.9 = 18 (rounded down)memory_overhead = 21*0.1 = 3 (rounded up)
spark.driver.memory 可以设置成和 spark.executor.memory 一样,就像 spark.driver.cores 设置成和spark . executor . cores一样。
另一个突出的性质是spark . default . parallelism,可以借助下面的公式进行估算。建议集群中的每个 CPU 内核执行 2-3 个任务。Spark 在许多任务中重用一个 executor JVM,并有效地支持耗时约 200 毫秒的任务。因此,并行级别可以设置为大于集群中的内核数量。虽然公式的结果给出了一个线索,但鼓励考虑分区大小来调整并行度值。推荐的分区大小约为 128MB。通过使用重新分区和/或合并,可以在洗牌操作期间根据需要定义该属性。如果混洗操作中的数据量非常不同。在 Spark 执行的流程中,Spark . default . parallelism可能不会在会话级别设置
spark.default.parallelism = spark.executor.instances * spark.executor.cores * 2spark.default.parallelism = 8 * 5 * 2 = 80
对于数据帧,spark . SQL . shuffle . partitions可以和spark . default . parallelism属性一起设置。
请注意,所有配置设置方法都是基于最大化可用资源的利用率。然而,如果多个 Spark 应用程序运行在同一个集群上,并且共享一个公共资源池,那该怎么办呢?在那种情况下,利用spark . dynamic allocation . enabled属性可能是一种替代方法。与**spark . dynamic allocation . initial executors、spark . dynamic allocation . min executors、和spark . dynamic allocation . max executors 一起使用。**从属性名称可以理解,应用程序从初始的执行人编号开始,然后在执行需求高的情况下增加执行人编号,或者在执行人处于上下限内的空闲位置的情况下减少执行编号。
另一个观点可能是对运行多个 Spark 应用程序的环境应用实验方法。为了更好地阐明这一点,从验证工作持续时间等限制的配置开始。例如,计划的 Spark 应用程序每 10 分钟运行一次,预计不会持续超过 10 分钟。然后,只要不违反限制,逐步减少资源。
公平调度程序
如果您有一个运行大量 Spark 应用程序的环境,我也强烈建议您看看 YARN 中的 公平调度器 。它提供了一个更高的抽象来管理多个 Spark 应用程序的资源共享。目标是让所有应用程序在一段时间内获得或多或少相等的资源份额,而不是惩罚执行时间较短的应用程序。它的配置在两个文件中维护: yarn-site.xml 和 fair-schedular.xml 。
在公平调度器中,根据内存和 CPU 使用情况,通过利用队列来进行资源管理。这些队列之间公平地共享资源。队列的主要属性可以被计数为 minResources 、 maxResources 、 weights 和 schedulingPolicy 。为了更清楚起见,让我们假设您有相同的环境来开发新模型和在生产中运行调度应用程序,这是有原因的。可以为开发和生产应用程序定义单独的队列,类似地,可以为不同用户触发的应用程序定义不同的队列。此外,可以为不同的队列分配不同的权重,因此相应队列中的应用程序可以根据权重成比例地获得资源。对于同时使用 fair schedular 和 Spark 配置属性的问题,您可能会得到一个简单的答案。
有用的链接
https://medium.com/@ch.nabarun/apache-spark-optimization-techniques-54864d4fdc0c https://blog.cloudera.com/how-to-tune-your-apache-spark-jobs-part-2/ https://aws.amazon.com/tr/blogs/big-data/best-practices-for-successfully-managing-memory-for-apache-spark-applications-on-amazon-emr/ https://luminousmen.com/post/spark-anatomy-of-spark-application https://spark.apache.org/docs/latest/tuning.html#memory-management-overview
计数矢量器的基础
了解关于 CountVectorizer 的一切信息。
机器不能理解字符和单词。所以在处理文本数据时,我们需要用机器能够理解的数字来表示它。Countvectorizer 是一种将文本转换为数字数据的方法。为了向您展示它是如何工作的,让我们举一个例子:
text = [‘Hello my name is james, this is my python notebook’]
文本被转换成如下所示的稀疏矩阵。
我们在文本中有 8 个唯一的单词,因此在矩阵中有 8 个不同的列,每个列代表一个唯一的单词。该行代表字数。因为单词“is”和“my”重复出现了两次,所以我们对这些特定单词的计数为 2,对其余单词的计数为 1。
Countvectorizer 使文本数据可以很容易地直接用于机器学习和深度学习模型,如文本分类。
让我们再举一个例子,但这一次有不止一个输入:
text = [‘Hello my name is james' , ’this is my python notebook’]
我有两个文本输入,所发生的是每个输入被预处理,标记化,并表示为一个稀疏矩阵。默认情况下,Countvectorizer 将文本转换为小写,并使用单词级标记化。
现在我们已经看了一些例子,让我们实际编码!
我们将首先从导入必要的库开始。我们将使用 pandas 库来可视化矩阵,并使用 sk learn . feature _ extraction . text 库来执行矢量化。
import pandas as pdfrom sklearn.feature_extraction.text import CountVectorizer text = [‘Hello my name is james’,‘james this is my python notebook’,‘james trying to create a big dataset’,‘james of words to try differnt’,‘features of count vectorizer’] coun_vect = CountVectorizer()count_matrix = coun_vect.fit_transform(text) count_array = count_matrix.toarray() df = pd.DataFrame(data=count_array,columns = coun_vect.get_feature_names())print(df)
参数
- 小写
在标记前将所有字符转换成小写。默认值设置为 true,并采用布尔值。
text = [‘hello my name is james’,‘Hello my name is James’] coun_vect = CountVectorizer(**lowercase=False**)count_matrix = coun_vect.fit_transform(text) count_array = count_matrix.toarray() df = pd.DataFrame(data=count_array,columns = coun_vect.get_feature_names())print(df)
现在让我们尝试不使用“小写=假”
text = [‘hello my name is james’,‘Hello my name is James’] coun_vect = CountVectorizer()count_matrix = coun_vect.fit_transform(text) count_array = count_matrix.toarray() df = pd.DataFrame(data=count_array,columns = coun_vect.get_feature_names())print(df)
- 停 _ 字
停用词是任何语言中对句子没有多大意义的词。它们可以被安全地忽略,而不会牺牲句子的意义。有三种处理停用词的方法:
- 自定义停用字词列表
text = [‘Hello my name is james’,‘james this is my python notebook’,‘james trying to create a big dataset’,‘james of words to try differnt’,‘features of count vectorizer’] coun_vect = CountVectorizer(**stop_words= [‘is’,’to’,’my’]**)count_matrix = coun_vect.fit_transform(text) count_array = count_matrix.toarray() df = pd.DataFrame(data=count_array,columns = coun_vect.get_feature_names())print(df)
稀疏矩阵去掉这几个字后是,到和我的:
2.sklearn 内置停用词表
text = [‘Hello my name is james’,‘james this is my python notebook’,‘james trying to create a big dataset’,‘james of words to try differnt’,‘features of count vectorizer’] coun_vect = CountVectorizer(**stop_words=’english’**)count_matrix = coun_vect.fit_transform(text) count_array = count_matrix.toarray() df = pd.DataFrame(data=count_array,columns = coun_vect.get_feature_names())print(df)
3.使用 max_df 和 min_df(稍后介绍)
Max_df:
Max_df 代表最大文档频率。类似于 min_df,我们可以忽略频繁出现的单词。这些单词可能就像在每个文档中出现的单词“the ”,不会为我们的文本分类或任何其他机器学习模型提供有价值的信息,因此可以安全地忽略。Max_df 查看有多少文档包含该单词,如果它超过 max_df 阈值,则从稀疏矩阵中消除它。这个参数又可以是 2 种类型的值,百分比和绝对值。
使用绝对值:
text = [‘Hello my name is james’,‘james this is my python notebook’,‘james trying to create a big dataset’,‘james of words to try differnt’,‘features of count vectorizer’] coun_vect = CountVectorizer(**max_df=1**)count_matrix = coun_vect.fit_transform(text) count_array = count_matrix.toarray() df = pd.DataFrame(data=count_array,columns = coun_vect.get_feature_names())print(df)
单词“is”、“to”、“james”、“my”和“of”已从稀疏矩阵中删除,因为它们出现在多个文档中。
使用百分比:
text = [‘Hello my name is james’,‘james this is my python notebook’,‘james trying to create a big dataset’,‘james of words to try differnt’,‘features of count vectorizer’] coun_vect = CountVectorizer(**max_df=0.75**)count_matrix = coun_vect.fit_transform(text) count_array = count_matrix.toarray() df = pd.DataFrame(data=count_array,columns = coun_vect.get_feature_names())print(df)
如您所见,单词“james”在 5 个文档中出现了 4 个(85%),因此越过了 75%的阈值并从稀疏矩阵中移除
Min_df:
Min_df 代表最小文档频率,与计算单词在整个数据集中出现的次数的术语频率相反,文档频率计算数据集中(也称为行或条目)具有特定单词的文档的数量。当构建词汇表时,Min_df 忽略文档频率严格低于给定阈值的术语。例如,在您的数据集中,您可能有只出现在 1 或 2 个文档中的名称,现在这些名称可以被忽略,因为它们不能提供整个数据集的足够信息,而只能提供几个特定文档的信息。min_df 可以取绝对值(1,2,3…)或表示文档百分比的值(0.50,忽略出现在 50%文档中的单词)
使用绝对值:
text = [‘Hello my name is james’,‘james this is my python notebook’,‘james trying to create a big dataset’,‘james of words to try differnt’,‘features of count vectorizer’] coun_vect = CountVectorizer(**min_df=2**)count_matrix = coun_vect.fit_transform(text) count_array = count_matrix.toarray() df = pd.DataFrame(data=count_array,columns = coun_vect.get_feature_names())print(df)
- max_features
计数矢量器将选择出现频率最高的单词/特征/术语。它采用绝对值,因此如果您设置“max_features = 3”,它将选择数据中最常见的 3 个单词。
text = [‘This is the first document.’,’This document is the second document.’,’And this is the third one.’, ‘Is this the first document?’,]coun_vect = CountVectorizer(max_features=3)count_matrix = coun_vect.fit_transform(text)count_array = count_matrix.toarray()df = pd.DataFrame(data=count_array,columns = coun_vect.get_feature_names())print(df)df
- 双星
通过设置“binary = True”,CountVectorizer 不再考虑术语/单词的频率。如果发生,则设置为 1,否则为 0。默认情况下,binary 设置为 False。这通常在术语/单词的计数不能为机器学习模型提供有用信息时使用。
text = [‘This is the first document. Is this the first document?’ ]coun_vect = CountVectorizer(binary=True)count_matrix = coun_vect.fit_transform(text)count_array = count_matrix.toarray()df = pd.DataFrame(data=count_array,columns = coun_vect.get_feature_names())print(df)
尽管所有的单词在上面的输入中出现了两次,我们的稀疏矩阵只是用 1 来表示它
现在让我们看看我们是否使用了默认值:
- 词汇
它们是稀疏矩阵中单词的集合。
text = [‘hello my name is james’,‘Hello my name is James’]coun_vect = CountVectorizer()count_matrix = coun_vect.fit_transform(text)print(coun_vect.vocabulary_)
这些数字不代表单词的数量,而是代表单词在矩阵中的位置
如果你只是想要没有单词在稀疏矩阵中的位置的词汇表,你可以使用’ get_feature_names()'方法。如果您注意到这是我们在创建数据库和设置列时使用的相同方法。
text = [‘Hello my name is james’,‘james this is my python notebook’,‘james trying to create a big dataset’] coun_vect = CountVectorizer()count_matrix = coun_vect.fit_transform(text) print( coun_vect.get_feature_names())
CountVectorizer 只是处理文本数据的方法之一。Td-idf 是一种更好的数据矢量化方法。我建议你查看一下 sklearn 的官方文档以了解更多信息。
希望这有所帮助:)
企业数据驱动转型的指针
约翰·施诺布里奇在 Unsplash 上的照片
什么,为什么,以及共同的挑战
作为一名执行顾问和各种组织高级管理层的顾问,我与全球数十家财富 500 强企业合作,指导和支持他们各种形式的业务和数字化转型。转型的主题各不相同,从销售和收入管理转型、战略规划转型到定价和商业模式改革。然而,在所有这些努力中,一个越来越明显的共同点是在其商业实践中更好地使用数据和技术,无论是商业还是运营,以推动更大的竞争力并实现更远大的业务目标 。
随着关于分析、机器学习(ML)、人工智能(AI)…以及它们与当今企业的互动的流行词汇满天飞,数据驱动的转型的基础对于那些可能有兴趣着手实施的人来说可能并不总是很清楚。这篇文章旨在提供一个关于这个主题的快速入门。
什么是 数据驱动转型是什么意思
当我们说“数据驱动的转型”时,它指的是组织中涉及更多或更好地使用数据的任何计划,分析通常利用引入的新技术。
数据是分析和建模的来源,而分析和建模反过来将支持下游业务洞察提取或运营自动化。根据转换的具体用例、数据要求、数据格式(如时间序列、横截面、文本等)。)多样性(交易、人口统计、社会经济数据等。),体积,粒度,新旧程度都会不一样。
给定数据输入,可以应用传统分析(如描述性统计分析)或高级算法(如 AI/ML)来满足用例的目标,无论是客户获取预测、收入预测、员工流失预测,还是面向运营的用例,如使用 RPA(机器人流程自动化)实现保险索赔流程自动化。
在转型过程中,需要使用数字技术来清理、存储、转换数据和进行预测建模、生成见解或执行运营自动化,这些都是数字技术可以用来完成的最基本和最常见任务的几个例子。
根据转型的目标、变革的范围、所涉及的技术和业务合作伙伴,转型所需要的内容在不同的场景中会有很大的不同。要成功转型,除了技术方面,通常还需要其他方面的变革,如组织结构、文化以及员工的技能和能力。
为什么广受追捧
数据驱动的决策和转型的优势已经在各个领域和行业得到了广泛的见证和证明。麦肯锡全球研究所估计数据和分析如果大规模嵌入,每年可以创造价值 9.5 万亿到 15.4 万亿美元的价值[1]。业务收益可以是短期的,也可以是长期的,表现在以下几个方面:
支持 更好的决策,从而直接提高业务绩效 ,例如使用数据驱动的预测性客户细分和有针对性的营销来增加客户获取、需求增长或其他业务绩效 KPI(关键绩效指标)
推动 更高的生产力和运营效率 ,例如使用 RPA 实现保险索赔流程或仓库库存管理的自动化
其他 更长期的商业利益,如客户满意度、客户参与度或员工保留度 。
常见陷阱
尽管不同设置的转换细节可能会有很大差异,但仍有一些常见的挑战和陷阱:
数据质量和兼容性
然而,这是进行数据驱动的转换时最常见的挑战,也是最容易被忽略的。由于与存储和管理数据相关的任务的基本性质,通常由 IT 部门的初级人员执行,这一方面在组织中的所有“大”或“闪亮”项目中通常投资不足。
也就是说,数据是启动任何数据驱动的转型的输入和“燃料”。 正如我们所说的“垃圾进垃圾出”——如果数据质量不太好,就无法进行质量分析或 AI/ML 建模工作。 在设计转型和评估结果时,对数据状况进行完整而详细的审核应该是一项必不可少的尽职调查项目,这项工作可以在转型的准备阶段就开始进行。在转型的过程中,拥有提高数据质量的路线图通常也是必要的一步。
不同的数据/IT 系统和孤立的组织单位。
数据驱动的转换通常可以跨越组织的多个业务职能。以需求计划数字化转型为例(这是任何产品公司的关键业务操作),对于这种转型,所需的数据将来自相当多的功能:来自供应链的客户需求历史、来自销售的销售计划数据、来自 R&D 的新产品提供数据等等。这些数据集通常位于不同 IT 系统中组织的不同部分,不一定相互连接和同步。
了解来自不同业务部门的数据集的细微差别,收集和协调多个数据源,使它们相互兼容和连接,这是一项艰巨的任务。 然而,这一方面是启动跨职能转变的充分条件。
新技术的采用和整合
在数据驱动的转型中,新技术的引入无疑是不可避免的。由于技术、组织或人的心理的众多障碍因素, 在任何组织中采用一项新技术进入现有的业务流程都绝非易事 。新的数据平台或工具需要集成到现有的 IT 环境中;最重要的是,需要有效和高效地采用平台或工具,但通常很难实现。
专注于技术而不构建总体业务用例
技术可以为业务运营带来革命性的变化,但技术本身并不能创造价值。 如果缺乏足够的支持业务基础设施的变化,那么将会包括转换结果的整体交付——它们可能是过时的业务流程或工人的能力或上游业务操作,等等。再次以商业规划数字化转型为例,成功的端到端规划不仅需要利用人工智能/人工智能技术进行需求预测输入,还需要改进规划业务流程,以适应规划者在新规划工具中的角色;此外,规划师的新技能,如评估 AI/ML 和建立跨职能的人类共识等。也要求完成整个规划改造。
需要新的角色、能力、组织文化和心态变化
即使已经处理了转型中的所有技术方面,为了让转型随着时间的推移产生持续的效益,通常还需要新的角色、能力、组织文化和思维模式。改变组织文化,例如培养一种更加数据驱动的实践(相对于经验和直觉驱动),以及一种使用技术来提高运营效率和创新的思维模式等等。
[1] A. Ghia 、M. Langstaff、 D. Ware 和 Rob Wavra、加速公共部门的数据和分析转型 (2021),麦肯锡&公司
深度学习的基础:反向传播
从头开始反向传播的分步实践教程
劳伦·里奇蒙的照片
我现在已经研究深度学习有一段时间了,我成为了 PyTorch 或 TensorFlow 等当前深度学习框架的超级粉丝。然而,随着我越来越习惯这样简单但强大的工具,深度学习中核心概念的基础,如反向传播,开始淡出。我相信回到基础总是好的,我想做一个详细的实践教程来理清事情。
https://colab.research.google.com/drive/10sYc0tB2dw_jsw9D61SwliN1nqugyjP1?usp=sharing
介绍
深度学习的基本过程是使用学习到的权重执行网络定义的操作。比如著名的卷积神经网络(CNN)就是乘法、加法等。像素强度值具有由网络设计的这种规则。然后,如果我们想要分类图片是狗还是猫,我们应该以某种方式得到操作后的二进制结果,以告诉 1 是狗,0 是猫。
当我们训练网络时,我们只是简单地更新权重,以便输出结果变得更接近答案。换句话说,有了一个训练有素的网络,我们可以正确地将图像分类到它真正属于的类别。这就是反向传播的用武之地。我们计算梯度并逐步更新权重以满足目标。目标函数(又名损失函数)是我们如何量化答案和预测之间的差异。通过一个简单且可微的目标函数,我们可以很容易地找到全局最小值。然而,在大多数情况下,这并不是一个微不足道的过程。
图片来源维基百科
链式法则
没有链式法则,你就无法谈论反向传播。链规则使您能够以简单的方式计算局部梯度。
局部梯度的计算(费,2017)
反向传播的例子(费,2017)
这是一个简单的反向传播的例子。正如我们之前讨论的,输入数据是上面的 x 、 y 和 z 。圆形节点是操作,它们构成了功能 f 。由于我们需要知道每个输入变量对输出结果的影响,给定 x 、 y 或 z 的 f 的偏导数就是我们想要得到的梯度。然后,通过链规则,我们可以反向传播梯度,并获得每个局部梯度,如上图所示。
矢量化反向传播示例(费-李非,2017 年)
由于我们将在实际实现中进行更多的矢量化计算,这里有一个例子,函数 f 是 L2 范数。L2 范数的梯度正好是输入值的两倍,即上面的 2q 。那么,给定 W 的 q 的偏导数将是 2q 和 x 转置的内积。还有,同样给定的 x 会是 W 转置和 2q 的内积,而为什么 W 转置是因为 xᵢ 的每个偏导数都给定了 W 的列向量。
激活功能
在深度学习中,如果没有激活函数,层之间的一组线性操作终究只是一个大的线性函数。非线性激活函数进一步增加了模型的复杂性。我将介绍一个基本的激活函数及其导数,来计算我们反向传播的梯度。
乙状结肠的
双曲正切
整流线性单位
目标函数
在训练神经网络时,量化预测与答案的接近程度的有效方法是非常重要的。为了执行反向传播和更新影响输出预测的所有相关权重,需要可微分的目标函数(又名损失函数)。我将介绍两个目标函数,称为均方误差(MSE)和交叉熵损失函数。
均方误差
MSE 是当今最通用的损失项,常用于预测数值。它计算了预测和事实之间的平均平方距离。最终激活层通常遵循线性或 ReLU。
交叉熵
交叉熵通常用于从多个类别中预测单个标签。对于最终激活函数,它通常遵循 softmax,使得输出概率之和为 1,并且它提供了对损失项的非常简单的推导,如下所示。
反向传播
作者图片
对于如上所述的全连接网络,在反向传播中只需要考虑三件事。来自右侧的通过梯度,从激活函数的导数计算的局部梯度,以及关于权重和左侧输入的通过梯度。
第一个梯度来自损失项,通过如上所述的这些项的推导,我们可以开始从右向左传递梯度。从每一层,我们首先计算关于激活层的梯度。然后,该梯度与输入值(z’)的内积将是相对于我们的权重的梯度。此外,权重梯度的内积( w )将是下一个向左通过的梯度。
重复这个简单的过程是我们成功反向传播所需要的!
参考
[1]费-李非, CS231n:用于视觉识别的卷积神经网络,2017
[2] Stacey Ronaghan,深度学习:我应该使用哪些损失和激活函数?,2018
使用 Python 的 Matplotlib 的圆环图基础
使用 Matplotlib 绘制甜甜圈的不同方法快速指南
与饼状图有许多相似之处,这种创造性的可视化使其区别于其声名狼藉的表亲。开放的中心使切片看起来像条形,并将比较的焦点从面积和角度改变为长度。
甜甜圈和圆形进度条——作者图片
在本文中,我们将检查用 Matplolib 绘制环形图的两种方法。一种简单的方法是使用饼图和参数楔形图,另一种更复杂的方法是使用极轴和水平条形图。
图片兴趣探测调查
在 Matplotlib 中没有绘制环形图的方法,但是我们可以使用 wedgeprops 快速转换饼图。
让我们从一个简单的馅饼开始。
import matplotlib.pyplot as pltplt.pie([87,13], startangle=90, colors=['#5DADE2', '#515A5A'])plt.show()
饼图—作者图片
现在我们可以添加参数 wedgeprops 并定义边缘的宽度,其中一个意味着边界将一直延伸到中心。
fig, ax = plt.subplots(figsize=(6, 6))
ax.pie([87,13],
wedgeprops={'width':0.3},
startangle=90,
colors=['#5DADE2', '#515A5A'])plt.show()
甜甜圈——作者图片
这很简单。现在我们可以利用中心的空间让我们的数据更加明显。
fig, ax = plt.subplots(figsize=(6, 6))wedgeprops = {'width':0.3, 'edgecolor':'black', 'linewidth':3}ax.pie([87,13], wedgeprops=wedgeprops, startangle=90, colors=['#5DADE2', '#515A5A'])plt.title('Worldwide Access to Electricity', fontsize=24, loc='left')plt.text(0, 0, "87%", ha='center', va='center', fontsize=42)
plt.text(-1.2, -1.2, "Source: ourworldindata.org/energy-access", ha='left', va='center', fontsize=12)plt.show()
带细节的圆环图—图片由作者提供
当我们要显示简单的比较或比例时,圆环图特别有用。在我看来,使用它们的最佳方式就像一个圆形进度条,比如我们突出显示单一比例的例子。
在这些情况下,我们可以进一步简化图表。
fig, ax = plt.subplots(figsize=(6, 6))data = [87, 13]
wedgeprops = {'width':0.3, 'edgecolor':'black', 'lw':3}
patches, _ = ax.pie(data, wedgeprops=wedgeprops, startangle=90, colors=['#5DADE2', 'white'])patches[1].set_zorder(0)
patches[1].set_edgecolor('white')plt.title('Worldwide Access to Electricity', fontsize=24, loc='left')
plt.text(0, 0, f"{data[0]}%", ha='center', va='center', fontsize=42)
plt.text(-1.2, -1.3, "Source: ourworldindata.org/energy-access", ha='left', va='top', fontsize=12)plt.show()
圆形进度条—作者图片
酒吧
尽管这个解决方案比前一个更复杂,但它提供了一些令人兴奋的定制选项。
先说简单的。
from math import pifig, ax = plt.subplots(figsize=(6, 6), subplot_kw={'projection':'polar'})data = 87
startangle = 90x = (data * pi *2)/ 100 # convert x data from percentage
left = (startangle * pi *2)/ 360 # convert start from angleax.barh(1, x, left=left, height=1, color='#5DADE2')
plt.ylim(-3, 3)plt.show()
极轴中的水平条-作者图片
你大概能明白为什么这更复杂了。
现在我们从角度出发。因此,我们必须在将每个元素添加到轴之前转换其 x 位置。
from math import pifig, ax = plt.subplots(figsize=(6, 6), subplot_kw={'projection':'polar'})data = 87
startangle = 90x = (data * pi *2)/ 100
left = (startangle * pi *2)/ 360 #this is to control where the bar startsplt.xticks([])
plt.yticks([])
ax.spines.clear()ax.barh(1, x, left=left, height=1, color='#5DADE2')
plt.ylim(-3, 3)plt.text(0, -3, "87%", ha='center', va='center', fontsize=42)plt.show()
圆形进度条—作者图片
使用这种方法时,您有更多的选择;更容易添加多个进度条,定义它们之间的距离,并向可视化添加细节。
另一方面,文本的定位会变得很棘手。
from math import pi
import numpy as np
from matplotlib.patches import Patch
from matplotlib.lines import Line2Dfig, ax = plt.subplots(figsize=(6, 6))ax = plt.subplot(projection='polar')data = [82, 75, 91]
startangle = 90
colors = ['#4393E5', '#43BAE5', '#7AE6EA']xs = [(i * pi *2)/ 100 for i in data]
ys = [-0.2, 1, 2.2]
left = (startangle * pi *2)/ 360 #this is to control where the bar starts# plot bars and points at the end to make them round
for i, x in enumerate(xs):
ax.barh(ys[i], x, left=left, height=1, color=colors[i])
ax.scatter(x+left, ys[i], s=350, color=colors[i], zorder=2)
ax.scatter(left, ys[i], s=350, color=colors[i], zorder=2)
plt.ylim(-4, 4)# legend
legend_elements = [Line2D([0], [0], marker='o', color='w', label='Group A', markerfacecolor='#4393E5', markersize=10),
Line2D([0], [0], marker='o', color='w', label='Group B', markerfacecolor='#43BAE5', markersize=10),
Line2D([0], [0], marker='o', color='w', label='Group C', markerfacecolor='#7AE6EA', markersize=10)]
ax.legend(handles=legend_elements, loc='center', frameon=False)# clear ticks, grids, spines
plt.xticks([])
plt.yticks([])
ax.spines.clear()plt.show()
多个进度条—作者图片
楔形区的 width 属性是一个简单的参数,它可以使任何饼图更加激动人心,可读性更好。
有其他选项来绘制这种可视化效果也很棒。我看过另一个教程,他们展示了如何在饼图的中心画一个白色的圆圈,将它变成一个甜甜圈,这表明了如何通过数据可视化来获得创意。
感谢阅读我的文章!— 更多 Python DataViz 教程。
资源:
MPL 派;
MPL 楔形;
MPL 极坐标散点示例;
MPL Barh;
基于优化的元学习的少量学习基础
基于优化的元学习中 MAML、FOMAML 和爬虫方法背后的机制概述
凯利·西克玛在 Unsplash 上的照片
元学习方法可以大致分为基于度量、基于优化和基于模型的方法。在这篇文章中,我们将主要关注基于优化的元学习方法背后的数学。
术语。元学习模型用元训练数据集(用一组任务 τ = { τ ₁、 τ ₂、 τ ₃、…})训练,用元测试数据集(任务 τₜₛ )测试。每个任务 τᵢ 由任务训练集(即支持集)dᵢᵗʳt21】和任务测试集(即查询集) Dᵢ ᵗˢ.组成元学习问题的一种类型是N*-wayk-shot learning,其中我们在 N 个类之间进行选择,并利用每个类的 k 个示例进行学习。*
双向单次示例的元训练、元测试、支持和查询数据集的图示。图片作者。
迁移学习(微调)
在继续讨论元学习之前,我们将简要提及另一种常用的方法——通过微调转移学习,以将知识从基础模型(例如,通过识别许多不同的对象构建)转移到新任务(例如,专门识别狗)。这里的想法是建立在一般任务上预先训练的模型,并在新的特定任务上微调该模型(通过仅更新神经网络中有限的层集合和/或以较慢的学习速率)。我们将在这一节复习数学术语,这样我们就可以与后面要讨论的元学习进行比较和对比。
在微调设置中,我们将首先导出在 D ᵖʳᵉ-ᵗʳ上预训练的一组优化的参数 θ ᵖʳᵉ-ᵗʳ,
在微调过程中,我们将调整使训练集 D ᵗʳ损失最小的参数,
该等式示出了一个梯度步骤,但是实际上这是通过多个梯度步骤来优化的。作为示例,下面显示了参数空间中从预训练参数值 θ ᵖʳᵉ-ᵗʳ到微调参数值 θ 的路径。
微调。图片作者。
在通过微调的迁移学习中,希望基础模型已经学习了基本模式(如形状、对比度、图像中的物体),微调可以更快更容易地适应新的任务。然而,这种方法并不是专门围绕着学习而设计的。新任务可能不会与基本任务重叠,从而导致知识的转移性能不佳。另一方面,元学习是明确围绕构建任务和算法进行设计的,以便进行一般化的学习。
MAML
模型不可知元学习(MAML)是由 Finn 等人在 2017 年提出的。这是一种基于优化的元学习方法。其思想是,我们不是寻找对给定的训练数据集或经过微调的训练集有用的参数,而是寻找经过微调后可推广到其他测试集的最佳参数。
**为了一个任务。**对于给定的任务,我们将首先在微调步骤中使用支持训练数据集 D ᵗʳ。 D ᵗʳ的最佳参数 ϕ 为,
不同于微调(我们将在此停止),我们想要计算这个最优参数 ϕ 在查询测试数据集 D ᵗˢ上的表现,损失函数为 L ( ϕ , D ᵗˢ).目标是优化初始参数 θ ,使其在给定微调的情况下在查询测试集上表现良好。换句话说,我们在元训练步骤中更新 θ ,
这里我们需要计算∇_θ L ( ϕ , D ᵗˢ),它是损失函数关于 θ 的导数。
我们可以如下说明参数空间中的路径,
MAML 负责一项任务。图片作者。
请注意,我们不是在微调步骤直接更新 θ ,而是根据支持训练和测试数据集(灰色路径)判断最佳参数的方向,并在元训练步骤中更新 θ 。
用于任务集。不仅仅是一个任务,为了对各种任务进行归纳,我们可以通过对一组任务进行平均来执行每一步的元学习 τ = { τ ₁, τ ₂, τ ₃,…}。因此支撑组任务 τᵢ 的最佳参数 ϕᵢ 为:
元训练步骤是,
术语∇_θl(ϕᵢ, Dᵢ ᵗˢ)$可以进一步展开。下面我们将省略下标 i ,但是这种讨论适用于每个任务。对于链式法则,该术语可以表示为:
我们可以扩展早期的路径视觉效果,以包含多项任务,
多重任务的 MAML。图片作者。
在这里,我们对每个任务的最佳参数的方向性有所了解(用不同的颜色),并根据任务的平均值更新 θ (黑色路径)。
一阶 MAML
在 MAML 元学习步骤中,我们需要计算海森矩阵。作为替代,在一阶 MAML (FOMAML)中,可以通过将∇_θl(θ, D ᵗʳ)视为常数并因此忽略二阶导数项来使用一阶近似。这意味着我们将项∇_ θ ϕ 视为单位矩阵 I ,从而得到:
这可以直观地说明如下,
一级 MAML。图片作者。
注意,我们不是通过在计算图中一路展开来执行元梯度计算,而是使用一阶近似∇_ϕt16】l(ϕ, D ᵗˢ)作为更新 θ 的梯度。
爬行动物
爬行动物(OpenAI)是一种替代方法,其性能与 MAML 相当,但在计算和存储方面比 MAML 更有效,因为没有二阶导数的显式计算。
首先,我们将引入一个更新函数 Uᵏ ,它只是 MAML 中微调步骤的一个重新表述(和推广),
其中 k 是 ϕ 更新的次数。
对于爬虫,在每次迭代中,1)任务 τᵢ 被采样,2)在 k 更新后,计算 τᵢ 的最优参数 ϕᵢ ,以及 3)模型参数 θ 被更新为:
不是每个迭代一个任务,而是可以评估多个任务,产生如下的批处理版本,
在哪里
参数路径可以被示意性地可视化为,
爬行动物。图片作者。
将爬行动物与不同任务间平均的规则随机梯度下降区分开来的关键区别是对 ϕᵢ 在kt60】1 步上的估计,并使用ϕᵢθ作为更新 θ 的梯度。在标准随机梯度下降中,在每个梯度步骤之后更新参数( U ,其中 k =1)。作者 Nichol 等人已经表明,当 k > 1 时,这允许算法拾取高阶导数,并且随之而来的行为类似于 MAML,并且与 k =1 时明显不同。
资源
参考
原载于 2021 年 8 月 7 日https://boyangzhao . github . io。
马尔可夫链蒙特卡罗算法基础
这篇文章的目的是给马尔可夫链蒙特卡罗算法一个概念性的理解,以及我们为什么使用它们。
介绍
马尔可夫链蒙特卡罗是一组算法,用于通过从后验分布中取样来绘制后验分布。我们使用这种方法而不是二次近似方法的原因是,当我们遇到具有多个峰值的分布时,该算法可能会收敛到局部最大值,而不会给出后验分布的真实近似。然而,蒙特卡罗算法使用随机性和混沌理论的原理来解决问题,否则这些问题很难(如果不是不可能的话)通过分析来解决。
马尔科夫国王的类比
让我们用一个类比来开始这个讨论,我们可以在遍历不同类型的算法时更新这个类比。假设有 10 个岛屿以环形方式放置,并且有一个国王监管这些岛屿。马尔柯夫国王的受托人建议他,为了避免人民的反叛,他必须定期访问每个岛屿。条件是每个岛屿必须按人口比例参观。
岛的人口分布使得岛 10 的人口是岛 1 的 10 倍,岛 5 的人口是岛 1 的 5 倍,等等;因此,马尔科夫国王将停留在 10 号岛,比他停留在 1 号岛多 10 倍。Markov 国王的一位统计学家解释了一种方法,他可以用这种方法来计划他对这些岛屿的访问,并让他的人民高兴。方法是:
第一步:每周,Markov 王在移动到下一个岛或留在当前岛之间做出决定。
第二步:他抛硬币。如果硬币正面朝上,马尔科夫国王考虑以顺时针方向移动到下一个岛。如果硬币落在一个反面,国王马尔可夫考虑以逆时针方向移动到下一个岛。让我们把这个岛叫做求婚岛。
第三步:如果求婚岛的人口比当前岛的人口多,马尔科夫国王总是接受求婚,并移动到求婚岛。如果提议岛屿的人口少于当前岛屿的人口,那么他以population_proposal/population_current
的概率接受提议。因为这是一个概率,这也可能导致国王马尔科夫根本不动,拒绝建议。
下面给出了模拟这种情况的代码。随着周数的增加,你肯定会发现在岛上度过的周数与岛上的相对人口数成正比。
num_weeks <- 1e4
positions <- rep(0,num_weeks)
current<-10
for(i in 1:num_weeks){
##record current position
positions[i] <- current
##flip coin to generate a proposal
proposal <- current + sample(c(-1,1), size = 1)
##This is just to make sure that proposal remains between 1 and 10
if(proposal < 1) proposal <- 10
if(proposal >10) proposal <- 1
#move?
prob_move <- proposal/current
current <- ifelse(runif(1)<prob_move, proposal, current)
}
barplot(prop.table(table(positions)), ylim = c(0,0.2))
在上面给出的代码块中,随着周数的增加,条形图给出了一个稳定增加的分布,说明了每个岛屿的访问量与其人口规模成比例—(图片由作者提供)
大都会算法
这种算法被称为 Metropolis 算法。这是最简单的算法,属于马尔可夫链蒙特卡罗算法类。此外还有 Metropolis-Hastings 算法。
这两种算法的唯一不同之处在于,Metropolis 算法只是随机遍历负对数后验分布,而 Metropolis-Hastings 算法则提出了一个更加合理的建议。这可以参考它的数学这里更详细地理解。
让我们试着把这里的点点滴滴联系起来;你在后验分布上选择一个估计的起点。这是国王旅行开始时所在的岛屿。您生成一个随机数,建议点应该移动的方向。计算建议点和当前点的密度。如果建议点具有比当前更高的后验密度,则移动到建议点。如果它的密度较低,你以density_proposal/density_current
的概率移动到提议点。
所以一旦你一遍又一遍地这样做,你最终总会走向后防线的顶峰。沿途采集的样本有助于估计后验分布的形状。
我们使用这种算法的原因是,从后验样本中提取样本来告知我们后验的可能形状,因为如果我们试图找到后验的方程,然后试图优化它,我们将不得不求解一个非常复杂的积分。有时,这些积分甚至是不可解的,这就是为什么从最高密度区域采样给了我们一个更快更精确的关于后验形状的估计。
蒙特国王的类比
让我们假设 Markov 国王有一个叫 King Monte 的兄弟,他监管着边缘陡峭而中间平坦的山谷。为了方便起见,我们假设山谷的形状像一个碗,山谷中的人口分布与地形的陡度成反比。简单来说,在山谷陡峭的边缘周围,人口较低,在山谷平坦的中心周围,人口较高。蒙特国王被告知,就像马尔柯夫国王一样,为了避免他的人民叛变,他必须按照人口密度的比例访问山谷。这个问题比上一个问题稍微复杂一点。与金·马尔科夫的问题不同,我们在这里面对的是一大片连续的待开发土地。国王蒙特坐进他的车,在某个随机的方向上给了他的车一些随机的冲力。当汽车开始上坡时,它的动能开始转化为势能,直到汽车停下来掉头,并向中心移动。在一些预定的时间间隔,国王蒙特停下他的车,会见他所在地区的人;这个过程一遍又一遍地重复。从长远来看,国王蒙特将总是更多地访问人口较高的地区,因为重力将总是迫使他向中心而不是边缘。
让我们试着分析这是如何适用于确定后的形状。到目前为止,我们已经理解,计算后验概率的精确解在计算上过于昂贵;然而,我们能够计算任何单点的概率密度。我们还可以计算任意给定点的后验斜率。这其中的数学可以在这里找到。因此,该算法基本上运行一个动态模拟,接近准确的国王蒙特的汽车如何表现。唯一的区别是,我们的观点是在一个无摩擦的平原上前进。在开始模拟之前,我们还必须定义两件事情。一个是点在停止前应该走的步数,我们称之为跳步、,第二个是步长。这个点以随机的动量被弹向随机的方向。评估下一步的梯度和密度,并考虑能量因素。在我们完成所有的蛙跳步骤后,该点停止并采样其在后部的当前位置。代码可以在这里找到。下图显示了 5 个样本点的路径。
该图显示了马尔可夫链在 HMC 算法中的轨迹。每个点的方向和动量都是随机的。一旦我们增加样本的数量,链将总是从更接近(0,0)的区域中采样更多的样本
哈密顿蒙特卡罗
汉密尔顿并没有创造这个算法,虽然他确实对现代动力学贡献很大。由于 HMC 使用了动力学和能量守恒的原理,毫不夸张地说,达到了后验的顶峰,这种算法就以他的名字命名了。HMC 如此受欢迎的原因是因为与 Metropolis 或 Metropolis-Hastings 算法不同,该提议被拒绝的几率非常低。其原因是,下一个采样点是由一长串事件决定的,这些事件几乎总是导致下一个样本具有比当前样本更大的后验密度。唯一一次提案被拒绝是当系统的能量不守恒时,这通常发生在我们对提案密度的数值近似不好的时候。
然而,使用 HMC 的最大原因不是这个。由于参数数量较少,大都市和 HMC 的工作方式非常相似。HMC 总是更有效率,但结果是一样的。随着你向更高维度移动,后验模式离大多数概率质量存在的地方越来越远。很难想象超过 3 个维度,所以这样想。当参数的数量相对较少时,比如 3,后验模型会给出一个很好的近似值,来表示大部分概率质量的位置。当你有 1000 个参数时,分布的形状变得越来越像一个甜甜圈,大部分的概率质量离模式越远。
这导致 Metropolis 算法比 HMC 算法更频繁地拒绝提案,因此需要更多的时间来收敛。下面显示的图表对此给出了更多的含义。
每个密度上面的数字是维度的数量。随着参数数量的增加,模式离我们想要采样的值越来越远—(图片由作者提供)
摘要
在本文中,我们通过两个流行的 MCMC 方法来从概念上理解它们。在以后的文章中,我将介绍它们在 R 和 Stan 中的实现。本文中用于生成图表和模拟数据的所有代码都可以在这里找到。
照片由丹尼尔·利维斯·佩鲁西在 Unsplash 上拍摄
参考
[1] Richard McElreath,R 和 Stan(2020)【T0【2】斯蒂芬妮·格伦 用实例进行统计再思考。StatisticsHowTo.com中的“Metropolis-Hastings 算法/ Metropolis 算法”:对于我们其他人来说是基本的统计数据!https://www . statistics show to . com/metropolis-Hastings-algorithm/
【3】科林·卡罗尔,https://colind Carroll . com/2019/04/11/哈密顿-蒙特卡罗-从头开始/
基于 Python 的 Matplotlib 的 OHLC 海图基础
如何绘制股票分析的基本图表的快速指南
OHLC 图表与卷-图片由作者
历史可以追溯到 18 世纪,开盘-盘高-盘低-收盘(OHLC)图是最受欢迎的金融分析工具之一,通常用于说明股票价格随时间的变化。
在本文中,我们将了解如何使用 Matplotlib 从头开始构建 OHLC 图表,探索这种可视化的优势和局限性,并了解使用 mplfinance 的更直接的方法。
OHLC 图表是如何工作的?
图表由一系列垂直线组成,这些垂直线包含了价格的四个关键变量;一段时间内的最小值、最大值、初始值和结束值,通常以小时、天或周为单位。
OHLC——作者的形象
和蜡烛图有什么区别?
OHLC 与蜡烛图非常相似,因为它们都显示相同的信息,并用于说明一段时间内的价格。通常指股票、货币、债券、商品等。
OHLC 和烛台图表——作者图片
它们在显示数据的方式上略有不同;OHLC 开盘价总是在棍子的左边,收盘价在右边。
烛台的左右两边都没有标记。他们有一个盒子。
盒子的填充代表价格的方向。通常,实心或红色方框意味着价格下跌(熊市),因此开盘价是矩形的顶部。
空的或绿色的方框表示相反的情况(牛市),方框的顶部是收盘价。
烛台——来自 Investopedia
Matplotlib
让我们开始构建我们的 OHLC 图表。首先,我们将导入所需的库。
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import math
本例中的数据来自名为 S & P 500 股票数据的 Kaggle 数据集。
df = pd.read_csv('../data/all_stocks_5yr.csv')
df.head()
数据框的前五行-作者提供的图片
这是一个庞大的数据集,我们不会一次绘制所有这些数据,所以在开始之前,让我们选择一个较小的子集。
# filter Apple stocks
df_apple = df[df['Name'] == 'AAPL'].copy()# convert date column from string to date
df_apple['date'] = pd.to_datetime(df_apple['date']) # filter records after 2017
df_apple = df_apple[df_apple['date'].dt.year > 2017] df_apple.reset_index(inplace=True)
现在让我们画出树枝。它们应该从最低价格延伸到最高价格。
我们将为它创建一个 NumPy 数组,而不是使用日期作为 x。该数组的长度将从 0 变为数据帧的长度。操纵数字序列更容易,这将有助于定位棒和开盘价/收盘价的标记。
为了画线,我们将遍历数据框,为每条数据记录绘制一条线。
x = np.arange(0,len(df_apple))
fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in df_apple.iterrows():
plt.plot([x[idx], x[idx]], [val['low'], val['high']])plt.show()
矩形中的彩虹线——作者图片
太棒了,现在我们可以添加标记了。
x = np.arange(0,len(df_apple))
fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in df_apple.iterrows():
# high/low lines
plt.plot([x[idx], x[idx]],
[val['low'], val['high']],
color='black')
**# open marker
plt.plot([x[idx], x[idx]-0.1],
[val['open'], val['open']],
color='black')
# close marker
plt.plot([x[idx], x[idx]+0.1],
[val['close'], val['close']],
color='black')**plt.show()
OHLC 海图—图片由作者提供
在那里!用 Matplotlib 绘制 OHLC 图表非常容易。与烛台不同,你不需要颜色或符号中的不同填充物来理解可视化。
最简单的形式,这个图表是可读的和相对简单的。
颜色;色彩;色调
它们不是必须的,但是可以把我们的视觉带到另一个层次。
我们将在循环的开始添加一个绿色的变量;然后我们将添加一个条件来检查开盘价是否高于收盘价;为真时,我们将颜色改为红色。
x = np.arange(0,len(df_apple))
fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in df_apple.iterrows():
**color = '#2CA453'
if val['open'] > val['close']: color= '#F04730'**
plt.plot([x[idx], x[idx]],
[val['low'], val['high']],
**color=color**)
plt.plot([x[idx], x[idx]-0.1],
[val['open'], val['open']],
**color=color**)
plt.plot([x[idx], x[idx]+0.1],
[val['close'], val['close']],
**color=color**)plt.show()
彩色编码的 OHLC 海图——图片由作者提供
给我们的 OHLC 图表添加颜色使得过去的趋势更加明显,我们的可视化更加有洞察力。
一会儿
我们的可视化看起来已经很棒了,但是 x 轴已经没用了。
使用一个数字序列作为我们的 x 有助于画线和标记,但我们不能像这样告诉日期。
我们将使用 x 来定位我们的记号,并将日期作为标签。我们还需要考虑,如果我们打印每个日期,我们的 x 轴将是不可读的,所以我们将在绘图时跳过一些值。
x = np.arange(0,len(df_apple))
fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in df_apple.iterrows():
color = '#2CA453'
if val['open'] > val['close']: color= '#F04730'
plt.plot([x[idx], x[idx]],
[val['low'], val['high']],
color=color)
plt.plot([x[idx], x[idx]-0.1],
[val['open'], val['open']],
color=color)
plt.plot([x[idx], x[idx]+0.1],
[val['close'], val['close']],
color=color)
# ticks
plt.xticks(x[::3], df_apple.date.dt.date[::3])plt.show()
带有正确 x 轴的 OHLC 图表—作者图片
太好了!现在我们可以给我们的可视化添加一些样式,使它更有吸引力。
x = np.arange(0,len(df_apple))
fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in df_apple.iterrows():
color = '#2CA453'
if val['open'] > val['close']: color= '#F04730'
plt.plot([x[idx], x[idx]], [val['low'], val['high']], color=color)
plt.plot([x[idx], x[idx]-0.1], [val['open'], val['open']], color=color)
plt.plot([x[idx], x[idx]+0.1], [val['close'], val['close']], color=color)
# ticks
plt.xticks(x[::3], df_apple.date.dt.date[::3])
ax.set_xticks(x, minor=True)# labels
plt.ylabel('USD')# grid
ax.xaxis.grid(color='black', linestyle='dashed', which='both', alpha=0.1)# remove spines
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)# title
plt.title('Apple Stock Price', loc='left', fontsize=20)
plt.show()
带细节的 OHLC 图表—图片由作者提供
OHLC 图表是一个很好的分析起点,因为它给了我们一个很好的概述,我们可以在此基础上进行分析。
改进和定制
您可以绘制一些预测价格、置信区间、移动平均线、交易量和更多的变量和统计数据来补充您的可视化。
用 Matplotlib 从头开始构建我们的可视化给了我们很多自由。
x = np.arange(0,len(df_apple))
fig, (ax, ax2) = plt.subplots(2, figsize=(12,8), gridspec_kw={'height_ratios': [4, 1]})for idx, val in df_apple.iterrows():
color = '#2CA453'
if val['open'] > val['close']: color= '#F04730'
ax.plot([x[idx], x[idx]], [val['low'], val['high']], color=color)
ax.plot([x[idx], x[idx]-0.1], [val['open'], val['open']], color=color)
ax.plot([x[idx], x[idx]+0.1], [val['close'], val['close']], color=color)
# ticks top plot
ax2.set_xticks(x[::3])
ax2.set_xticklabels(df_apple.date.dt.date[::3])
ax.set_xticks(x, minor=True)# labels
ax.set_ylabel('USD')
ax2.set_ylabel('Volume')# grid
ax.xaxis.grid(color='black', linestyle='dashed', which='both', alpha=0.1)
ax2.set_axisbelow(True)
ax2.yaxis.grid(color='black', linestyle='dashed', which='both', alpha=0.1)# remove spines
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)# plot volume
ax2.bar(x, df_apple['volume'], color='lightgrey')
# get max volume + 10%
mx = df_apple['volume'].max()*1.1
# define tick locations - 0 to max in 4 steps
yticks_ax2 = np.arange(0, mx+1, mx/4)
# create labels for ticks. Replace 1.000.000 by 'mi'
yticks_labels_ax2 = ['{:.2f} mi'.format(i/1000000) for i in yticks_ax2]
ax2.yaxis.tick_right() # Move ticks to the left side
# plot y ticks / skip first and last values (0 and max)
plt.yticks(yticks_ax2[1:-1], yticks_labels_ax2[1:-1])
plt.ylim(0,mx)
# title
ax.set_title('Apple Stock Price\n', loc='left', fontsize=20)
# no spacing between the subplots
plt.subplots_adjust(wspace=0, hspace=0)
plt.show()
OHLC 图表与卷-图片由作者
Matplotlib 金融
在文章的开头,我提到了一种绘制 OHLC 图表的更简单的方法。mplfinance 是一个优秀的财务可视化工具包。
pip install --upgrade mplfinance
让我们来看看它有多容易使用。
数据框应包含以下字段:开盘、收盘、盘高、盘低和成交量。它还应该有一个日期时间索引。
我们的数据有适当的字段,所以我们只需要改变索引。
import mplfinance as mpfdf_google = df[df['Name'] == 'GOOGL'].copy()
df_google['date'] = pd.to_datetime(df_google['date'])
df_google = df_google[df_google['date'] > pd.to_datetime('2017-12-31')]
df_google = df_google.set_index('date')mpf.plot(df_google)
OHLC 海图—图片由作者提供
就这样,我们有了 OHLC 图表!
我们还可以添加移动平均线,并用一行代码可视化体积。
mpf.plot(df_google,type='candle',mav=(3, 5),volume=True, title='Google')
带移动平均线和成交量的 OHLC 图表——图片由作者提供
Matplotlib 财务实用程序比从头开始绘制图表更容易使用,但它们不容易定制。
对于大多数快速分析,您需要一个功能性和可读性强的图表,而 mplfinance 就足够了。
对于其他更具体的约束,当您需要向可视化中添加特定的组件或者定制样式以遵循一些设计标准时,mplfinance 可能会有所欠缺。
在这些情况下,使用 Matplotlib 从头开始绘制图表是值得的。我们可以很容易地添加、修改或删除可视化的任何部分,使它成为一个很好的工具。
感谢阅读我的文章。我希望你喜欢它!
查看更多 Python DataViz 教程。
推荐人:
mpl finance;
mplfinance —样式和定制;
Matplotlib —子情节 grid spec;
Matplotlib —x 坐标, y 坐标;
Matplotlib —右勾;
Matplotlib —棘;
Matplotlib —网格线、W3;
Matplotlib—Lines;
Matplotlib—Bars;
Python 生成器的基础
了解生成器的基础知识并在 Python 中实现它们
什么是 Python 生成器?
Python 生成器函数允许您声明一个行为类似迭代器的函数,允许程序员以一种快速、简单和干净的方式创建迭代器。迭代器是一个可以被迭代或循环的对象。它用于抽象数据容器,使其行为像一个可迭代的对象。更常用的可迭代对象的例子包括列表、字典和字符串。
在本文中,我们将借助一些例子学习在 Python 中创建和使用生成器。
用 Python 实现的简单类迭代器
让我们首先看一个简单的基于类的迭代器来产生奇数:
class get_odds:
def __init__(self, max):
self.n=3
self.max=max
def __iter__(self):
return self
def __next__(self):
if self.n <= self.max:
result = self.n
self.n += 2
return result
else:
raise StopIterationnumbers = get_odds(10)
print(next(numbers))
print(next(numbers))
print(next(numbers))# Output
3
5
7
如你所见,生成了一系列奇数。为了生成这个结果,我们在 get_odds 类中创建了一个自定义迭代器。对于要成为迭代器的对象,它应该实现将返回迭代器对象的 iter 方法,然后 next 方法将返回序列中的下一个值,并可能在没有值要返回时引发 StopIteration 异常。如你所见,创建迭代器的过程是漫长的,这就是我们求助于生成器的原因。同样,python 生成器是实现迭代器的一种简单方式。
生成器函数和常规函数的区别
常规函数和生成器函数之间的主要区别在于,生成器函数的状态是通过使用关键字 yield 来维护的,其工作方式非常类似于使用 return,但它有一些重要的区别。区别在于 yield 保存了函数的状态。下一次调用该函数时,将从停止的地方继续执行,使用让步前的变量值,而 return 语句将完全终止该函数。另一个区别是生成器函数甚至不运行函数,它只创建并返回一个生成器对象。最后,只有在生成器对象上调用 next() 时,生成器函数中的代码才会执行。
Python 中的生成器实现
让我们使用前面的代码实现相同的迭代器,只是使用了 python 生成器。
def get_odds_generator():
n=1
n+=2
yield n
n+=2
yield n
n+=2
yield n
numbers=get_odds_generator()
print(next(numbers))
print(next(numbers))
print(next(numbers))# Output
3
5
7
上面我首先创建了一个生成器函数,它有三个 yield 语句,当我们调用这个函数时,它返回一个迭代器对象的生成器。然后我们调用 next() 方法从这个对象中检索元素。第一个 print 语句给出了第一个 yield 语句的值 3,第二个 print 语句给出了第二个 yield 语句的值 5,最后一个 print 语句给出了第三个 yield 语句的值 7。如您所见,与我们基于类的迭代器相比,生成器函数要简单得多。
现在让我们尝试实现一个循环,让这个 python 生成器返回奇数,直到某个最大数。
def get_odds_generator(max):
n=1
while n<=max:
yield n
n+=2
numbers=get_odds_generator(3)
print(next(numbers))
print(next(numbers))
print(next(numbers))
作者图片
从输出中可以看到,生成了 1 和 3,之后引发了 StopIteration 异常。循环条件(n <=max) is False since max is 3 and n is 5, therefore the StopIteration exception was raised.
When comparing this code with our get_odds class, you can see that in our generator we never explicitly defined the iter method, the next method, or raised a StopIteration exception — these are handled implicitly by generators, making programming much easier and simpler to understand!
Iterators and generators are typically used to handle a large stream of data theoretically even an infinite stream of data. These large streams of data cannot be stored in memory at once, to handle this we can use generators to handle only one item at a time. Next, we will build a generator to produce an infinite stream of Fibonacci numbers. Fibonacci numbers are a series of numbers where the next element is the sum of the previous two elements.
def fibonacci_generator():
n1=0
n2=1
while True:
yield n1
n1, n2 = n2, n1 + n2sequence= fibonacci_generator()
print(next(sequence))
print(next(sequence))
print(next(sequence))
print(next(sequence))
print(next(sequence))# Output
0
1
1
2
3
As you can see from the code above, in defining the fibonacci_generator function, I first created the first two elements of the fibonacci series, then used an infinite while loop and inside it yield the value of n1 and then update the values so that the next term will be the sum of the previous two terms with the line n1,n2=n2,n1+n2. Our print statements gives us the sequence of numbers in the fibonacci sequence. If we had used a for loop and a list to store this infinite series, we would have run out of memory, however with generators we can keep accessing these terms for as long as we want since we are dealing with one item at a time.
Summary
From this article, we have covered the basics of python generators. By the way, we can also create generators on the fly using generator expressions, which you can read more about in this article by Richmond Alake. Thank you for reading and all code is available on my Github%20Generators.ipynb) 😃
推荐系统基础
用户相似性,项目相似性,协同过滤,基于内容的模型,潜在空间模型
来自 Pixabay
推荐系统主动向用户推荐相关项目。当宜。“主动”意味着这些项目只是出现——用户不需要搜索它们,甚至不需要意识到它们的存在。“相关”意味着当用户出现时,他们倾向于参与。“与他们接触”的确切含义取决于上下文。对于电影来说,engage 可能意味着看;对于产品购买。“适当的时候”是关键,这也是“智能”的来源。
让我们看几个例子。
- 你在线观看电影、节目和视频。推荐系统被动地观察你的互动。你看什么,你什么时候看,你看某个项目多长*。它会推荐其他它认为你会喜欢的视频。如果您正在使用遥控器观看电视,这将非常方便。在这种情况下,浏览(个性化)产品比搜索更方便。*
- 你在电子商务网站浏览或购买商品。一个推荐系统监视你的一举一动。和其他买家的信息。然后推荐新的项目给你考虑。
协同过滤
想象你在你的网站上销售商品。很多项目,比如 1000 个。您将购买数据保存在数据库中。你的数据库跟踪购买时间*、用户标识、物品标识和数量。用户用户标识在时间购买时间购买数量物品物品标识的实例。*
让我们看一个来自虚构数据库的数据示例,总结如下。我们忽略了购买时间。我们已经把其余的数据排列成一个矩阵。行索引用户。列索引项目。
***I1 I2 I3 I4**
**U1** 4 0 5 0
**U2** 6 1 7 0
**U3** 0 0 0 1
**U4** 0 0 1 1
**U5** 3 0 4 0*
在这个数据中,我们有五个用户 U1 到 U5,四个项目 I1 到 I4。Cell (U,I)跟踪用户 U 购买商品 I 的次数(到目前为止)。例如,U1 四次购买 I1。
超出购买频率
虽然在该数据单元(U,I)中记录了用户 U 购买项目 I 的次数,但是更一般地,该单元可以记录我们认为合理地跟踪用户 U 对项目 I 的密切关系的任何数字,例如从 1 到 5 的评级。在评价设置中,许多单元格没有值,因为不是每个用户都对每个项目进行评价。事实上,人们会认为评级矩阵中的大多数单元格都有空值。
我们能从这些数据中挖掘出什么?
在这个例子中,我们可以推断用户 U1 和 U5 是相似的。这是因为他们购买相同物品的次数大致相同。我们从下面的行向量中可以看到这一点。
***U1** 4 0 5 0
**U5** 3 0 4 0*
因此,通过比较矩阵的行向量,我们可以推断出用户之间的相似关系。
现在我们来比较列向量。我们可以推断 I1 和 I3 是相似的。它们被相同的用户购买了大致相同的次数。
*I1 I3
4 5
6 7
0 0
0 1
3 4*
因此,通过比较矩阵的列向量,我们可以推断出项目之间的相似关系。
这是协同过滤的关键点:仅从交易数据我们就可以推断出用户之间的相似关系和项目之间的相似关系。
购物篮
回想一下,我们在前面的部分中描述的矩阵,即从交易中构建的矩阵,是从四元组得到的(购买时间、用户 id 、物品 id 、数量)。这样的四倍代表个人购买。这就丢失了一条关键信息:哪些商品是在同一次交易中一起购买的,即在同一个购物车中。该信息对于根据“购买了 A 的人也购买了 B”范例操作的某些类型的推荐系统非常有用。当我们谈到这个话题时,我们会更详细地讨论。目前,我们只是把它浮出水面。
新用户,新物品
一个新用户没有任何交易历史,所以我们无法找到邻居。同样,一个新项目没有任何购买,所以我们无法找到它的邻居。因此,依赖用户邻居的算法对新用户无效,而依赖项目邻居的算法对新项目无效。
我们现在提出这些问题,以便在这篇文章的后面,我们可以看到各种算法如何处理它们。
内容
现在假设,除了交易数据,我们还有描述商品和用户的数据。我们将其称为内容*。项目可能有描述、标签、名称、产品品牌、产品类别、价格等。(产品类别的示例有服装、电子产品、家用、…)某些属性可能取决于类别。如智能手机的存储容量和相机分辨率,服装的面料类型。*
用户可以选择提供关于他们自己的附加信息。如性别和年龄*。甚至可能是价格偏好、喜欢的产品等。*
现在我们有了额外的数据,可以帮助我们推断项目之间的相似关系。事实上,甚至可能是比相似性更微妙的关系。比如一个 iPhone 充电器是一个 iPhone 的配件。
内容也可以帮助新用户或新项目。
对用户项目矩阵的第二次传递
在我们已经看到的用户商品矩阵中,单元格(U,I)记录了用户 U 购买商品 I 的次数(到目前为止)。考虑一个值为 0 的单元格(U,I)。我们想区分两种情况:
- 用户 U 知道商品 I,并选择不购买它。
- 用户 U 甚至可能不知道项目 I 的存在。
为什么要区分这两者?第一种情况,我们知道用户不想要 I,所以不应该推荐。
我们可以收集什么样的证据来证明用户有机会查看某个特定的项目。我们可以跟踪这个项目在过去被推荐给用户的次数。我们还可以跟踪用户最终访问项目页面的次数。从搜索开始,或者通过电子商务网站上的导航链接浏览。
代替推荐跟踪购买计数的用户项目矩阵,或者除此之外,我们可以考虑推荐跟踪评级的用户项目矩阵。比方说,这个电子商务网站让用户给商品打分,从 1 到 5。我们到目前为止介绍的概念,加上我们将很快介绍的概念,也适用于这种矩阵。简单地说,只要我们能从两个用户的行向量中量化他们有多相似,或者从他们的列向量中量化两个项目有多相似,我们就很好。事实上,我们甚至不需要能够计算两种类型的相似性。只有用户对用户或项目对项目可能就足够了。见下文。
使用评级代替计数有以下好处。评级是用户对项目好坏的明确反馈。此外,如果单元格的用户没有对单元格的项目进行评级,则评级矩阵中的单元格可以被明确标记为“未评级”。
另一方面,通常来说,评价商品的用户比购买商品的用户少得多。因此,对于相同的用户和商品,用户商品评级矩阵往往比用户商品购买频率矩阵稀疏得多。
总之,评级矩阵具有潜在的更原始的信号(评级),但是它的稀疏性可能抵消一些(或全部)这种优势。
在这篇文章中,我们不会对哪个更好采取立场。通常,如果两种类型的数据都可用,那么在推荐系统中使用购买频率和评级是有意义的。
推荐问题
现在让我们为推荐的实际问题做准备。我们有一个用户 u 和她最近的购买(或其他交互)。我们还拥有所有用户和所有商品的历史交易数据。我们还可能有关于项目和用户的附加内容。
使用所有这些信息,我们将向用户 u 推荐我们认为她会喜欢的商品。
一种方法是先找一些和 u 足够相似的用户,叫他们 v1,…,vk。在纯协同过滤中,这些用户可以通过比较各个用户的行向量与 u’s 来发现。然后,我们向你推荐出现在他们的用户向量中的合适的项目。(我们将在后面详述“合适的”。)这叫做基于用户的协同过滤*。我们向你推荐基于相似用户口味的商品。*
如果条目有可用的内容,我们可以考虑将它们的属性维添加到条目向量中,或者替换它们。所以现在和以前一样,相似的行向量对应相似的用户。这一次,我们将同时考虑交易模式和商品内容。
让我们用一个简单的例子来说明这一点。我们将维度设为
***iPhone6 iPihone7 iPhone8 iPhone10 iPhone12 Brand=Apple Product Family=iPhone***
下面我们在这些维度上看到两个用户向量。
***U1** 1 0 1 0 1 3 3
**U2** 0 1 0 1 0 3 3*
把 iPhone6 到 iPhone12 想象成特定的物品,它们的品牌和产品系列就是内容。尽管 U1 和 U2 没有共同购买的物品,但是内容特征揭示了他们购买的相似性。
人们通常表现出品牌忠诚度。例如,iPhone 用户可能倾向于继续购买(最新的)iPhone。正如我们在上面的例子中所看到的,基于内容的建模可以显示这种趋势,从而提供更好的推荐。简单来说,在观察到某个用户一直在购买较新版本的 iPhone 后,就可以期望向该用户推荐最新的 iPhone 型号。
找到其行向量与特定用户的行向量相似的用户的先决条件是能够定量地比较两个行向量。我们先讨论这个。然后我们将回来讨论建立在此基础上的推荐算法。
测量行向量之间的相似度
有许多方法可以量化两个向量有多相似。不同的方法产生不同的结果。出于这个原因,我们将讨论其中的几个,并检查它们不同的行为。这将有助于读者了解何时使用哪一种。为了便于说明,我们将使用来自推荐设置的示例。
在开始之前,提醒一下用户项矩阵中的一些单元格可能缺少值。考虑一个评级矩阵。并非所有用户和项目的组合都有评级,事实上,很可能没有。很多用户从来懒得给任何东西打分!
在应用下面给出的相似性度量之前,我们将丢弃两个向量中缺少一个或另一个向量值的任何分量。所以下面描述的度量适用于没有丢失值的向量。
好了,我们开始吧。我们的第一个是
点积
这将两个向量按分量相乘,然后对结果项求和。考虑我们前面介绍的例子,为了简洁起见,省略了特性名称。
例 1 : 物品特性的效果
***U1** 1 0 1 0 1 3 3
**U2** 0 1 0 1 0 3 3*
点积是 1 * 0+0 * 1+1 * 0+0 * 1+1 * 0+3 * 3+3 * 3。最后两项用粗体表示,使得点积非常正。因此,我们认为 U1 和 U2 非常相似。在我们的场景中,这是有意义的,因为向量的最后两个组件分别代表品牌和产品系列*。由于 U1 和 U2 多次偏向同一品牌和同一产品系列,认为它们相似是有道理的。*
现在考虑一个不同的例子。
例 2 : 购买次数与购买与否
***U1** 10 2 3 1
**U2** 3 2 3 1
**U3** 9 0 0 0*
在这种情况下,四个维度代表不同的项目,只能通过标识符(如 SKU 或条形码)来识别。
{U1,U3}的点积比{U1,U2}的点积更正。因此,前一对具有更高的相似性得分。人们可以认为相似性得分应该颠倒。这是因为 U1 和 U2 购买的物品的集合是相同的,而 U3 丢失了 U1 购买的 4 件物品中的 3 件。这真正让我们思考的是,购买数量相对于所购商品的身份有多重要。
缓解这个问题的一个方法是在应用点积之前适当地预处理数据。例如,我们可以将所有正数转换为 1。也就是说,我们只是跟踪某个特定用户是否购买了某件商品。
三个向量的预处理版本变成
***U1** 1 1 1 1
**U2** 1 1 1 1
**U3** 1 0 0 0*
现在,在这种情况下,点积的行为符合预期。
我们正在失去信息。在其他情况下,我们可能会为此付出代价。其实下面这个。
例 3 : 采购盘点事项
***U1** 9 1 2
**U2** 9 1 2
**U3** 1 1 1*
这些向量的二进制版本上的点积将失去辨别 U2 比 U3 更类似于 U1 的能力。
居中点积
好吧,让我们试试不同的方法。让我们将矢量居中而不是二值化它们。将向量居中会从每个分量中减去向量的平均值。这在例 3 中有帮助吗?让我们看看。
首先,我们将复制示例 3,添加一列记录相应行向量的平均值。(本栏为黑体字。)
***U1** 9 1 2 **4
U2** 9 1 2 **4
U3** 1 1 1 **1***
因此,示例 3 中的居中版本的载体是
***U1c** 5 -3 -2
**U2c** 5 -3 -2
**U3c** 0 0 0*
作为健全性检查,我们可以看到上面的每个行向量的总和为 0。这是必须的。
嗯,确实有帮助!U1c 和 U2c 的点积非常正,而 U1c 和 U3c 的点积为 0。
为什么会这样?实际上,居中显示了向量中值的相对差异。点产品能够有效地利用这些相对差异。也就是说,首先居中,然后取点积,这是两个向量协方差的一个很好的度量。
那么这在示例 2 中有效吗?让我们看看。首先,让我们复制下面的例子,像前面一样,添加一列相应行向量的平均值。
***U1** 10 2 3 1 **4
U2** 3 2 3 1 **9/4
U3** 9 0 0 0 **9/4***
居中的版本是
***U1c** 6 -2 -1 -3
**U2c** 3–9/4 2–9/4 3–9/4 1–9/4
**U3c** 9–9/4 -9/4 -9/4 -9/4*
和以前一样,健全性检查显示每行总和为 0。
在这种情况下,居中(单独)没有帮助。{U3c,U1c}的点积还是大于{U2c,U1c}的点积。
潜在的问题是,在我们的设置中,值 0 和 1 之间的差异远远大于 9 和 10 之间的差异。0 表示没有购买物品。1 表示买了。所以,对我们来说,1 和 0 之间的差别,远远大于 10 和 9 之间的差别。
题外话:居中评级向量
在研究我们可以做些什么来解决上一段描述的问题之前,让我们注意到,如果用户向量由评级组成,居中是一个动机良好的操作。居中将评级(通常为 1 到 5 等正等级)转换为明确的正、负或中性评级。正如我们之前看到的,这种转换有助于点积作为更好的相似性度量。
也就是说,当用户使用时,需要小心地进行居中操作。考虑一个刚刚给两个项目分别评分为 4 和 5 的用户。居中的评级分别为-0.5 和+0.5。这有道理吗?
一种可能更好的方法是对用户的平均评级使用贝叶斯估计。下面是它的一个简单(有效)的形式。在用户的实际评分上加上从所有用户中随机抽取的 n 个评分。现在计算这些综合评分的平均值。 n 是这里的一个自由参数,它控制我们先前的信念,即这个用户像一个典型的用户一样评价。如果实际上这个用户没有,那么随着用户进行更多的评级,平均评级将向用户实际评级的平均值移动。
尝试拉伸小数值
让我们回到由购买计数组成的用户向量。
好吧,所以 0 和 1 应该被认为不如 9 和 10 相似。同时,5 和 10 应该被认为不如 9 和 10 相似。
让我们看看是否可以通过“拉伸”小值来实现这一点,这样它们的差异就会被聚合。假设 x 是一个值。考虑一下转型
*f(x) = 1–1/e^x*
这将实现这样的拉伸,因为
*f(x)-f(x-1) = 1–1/e^x — (1–1/e^{x-1})
= 1/e^{x-1} — 1/e^x
= (1/e^{x-1})*(1–1/e)*
我们看到,随着 x 的增加,x 的连续值之间的差值减小。所以我们拉伸了小值,但没有拉伸大值。
或者,使用乙状结肠
我们可以从稍微不同的角度来看这个问题。我们可以从二进制化开始,即将每个正计数截断为 1,并使用类似 sigmoid 的函数(如 tanh)将其软化。我们可以调整双曲正切函数的参数(失调和增益),使 0 映射到接近 0,1 映射到接近 1。此外,它还具有所需的属性,即较高的计数映射到较高的值。
然后使用居中点积
在前面提到的计数的非线性变换之一之后,我们将每个向量居中,并像前面一样取点积。
这样够好了吗?
考虑这个例子。
例 4 : 计数分布
***U1** 1 2 3
**U2** 10 20 30
**U3** 3 2 1*
人们可以认为 U1 和 U2 应该比 U1 和 U3 更相似,因为它们具有相同的相对计数。实际数字之间的差异可以用一个简单的因素来解释:U1 是一个多产的买家。**
接下来,让我们看看如果使用中心点积作为相似性度量,我们会得到什么。
最后一列粗体字包含向量平均值。
***U1** 1 2 3 **2
U2** 10 20 30 **20
U3** 3 2 1 **2***
居中的版本是
***U1c** -1 0 1
**U2c** -10 0 10
U3c 1 0 -1*
{U1c,U2c}的相似性得分高于{U1c,U3c}。很好!如你所愿。
我们运气好吗?这在一般情况下行得通吗?我不会在这里讨论这个问题。相反,我会观察到,如果我们寻求一个基于相对计数的度量,那么一个合理的候选就是将计数归一化为概率向量。然后,我们可以使用合适的概率分布相似性度量。比如相对熵。
下面我们举例说明标准化。
首先,我们复制上面的例子,最后一列包含向量和。
***U1** 1 2 3 **6
U2** 10 20 30 **60
U3** 3 2 1 **6***
接下来,我们将每个向量除以它的和。我们得到了
***U1** 1/6 2/6 3/6
**U2** 1/6 2/6 3/6
**U3** 3/6 2/6 1/6*
我们不会继续解释相对熵和计算值。相反,我们只需观察中心点积在概率向量上的表现。
仔细查看零计数
到目前为止,在我们的示例中,我们已经隐含地将没有购买特定商品的用户视为该(用户、商品)对的负面信号。这种假设应该受到质疑。考虑一个项目数量非常大的设置,比方说至少有几十万个。这在电子商务环境中很常见。用户可能没有购买特定的商品,因为她甚至不知道它的存在。在这样的设置中,即使不是所有的用户向量,也是大部分的用户向量非常稀疏。也就是说,用户将只购买了目录中很小一部分项目。使用零计数作为负信号会放大这种假设的效果。
我们来阐述一下“放大这个假设的效果”。对于任何两个用户 u 和 v,以及我从大量商品中随机挑选的一件商品,很有可能 u 和 v 都不会购买 I。如下图 1 所示,居中的点积将此作为 u 和 v 相似的证据。因为宇宙中的大多数物品都不会被 u 或 v 购买,这样不正确的证据会被放大,如下图所示。
*(u, i) = 0 implies u dislikes i (1)
(v, i) = 0 implies v dislikes i (2)
(1)+(2) contributes evidence towards u and v being similar*
图 1 : 错误的推论被放大
使用评级而不是计数可以避免这个问题,因为我们有办法区分缺失值(没有评级)和实际值。也就是说,忽略购买次数,即只使用评分是没有意义的。前者要丰富得多。他们也给出了一个直接的购买信号。评级可能会有偏差。另一方面,一个项目被购买。也就是说,它是为禁止免费赠送的促销商品而付费的,这些商品可以被过滤掉。
那么我们如何处理零计数呢?我们可以考虑总是将零计数解释为缺失值。在这种解释下,被比较的两个行向量的所有分量将具有正计数。这是因为其中一个或另一个计数为零的组件将被丢弃。这没什么大不了的,所有用户向量现在只有正数!
如果可能的话,一个更好的方法是跟踪更多的数据,以便能够以合理的准确度估计用户是否只是不知道某件商品或者选择不购买它。例如跟踪用户是否访问了该项目的页面。
一个更高级的想法是假设我们有可用的互斥对,即从不一起购买的物品对,更一般的是属性对。如果用户的购买频率向量具有来自这样一对元素中的一个元素的正计数,以及来自另一个元素的零计数,则结合了互斥信息的零计数为用户没有(或不会)购买后者的概念提供了支持。当我们讨论基于项目的协同过滤时,我们将讨论推断互斥对。
Jaccard 系数:测量两组的重叠度
假设我们将用户过去的购买行为表示为购买物品的集合*。暂且不说我们正在丢失信息——购买计数——集合表示确实因其简单性而吸引人。*
用户的集合可以被一般化,以获取其他布尔值属性。如产品家族= PF* 。在下面的讨论中,为了简单起见,我们将只讨论项目集。*
有一个简单的衡量标准,允许我们根据两个用户的项目集的重叠程度对他们的相似性进行评分。这个度量称为 Jaccard 系数,是两个集合的交集大小与并集大小的比值。
举个例子。假设用户 U 和 V 分别购买了{a,b,c}和{b,c,d}。Jaccard 系数为|{b,c||/|{a,b,c,d}| = 2/4。
这项措施因其简单而吸引人。它也有助于非常快速的得分,尽管我们不会在这篇文章中详细阐述这方面的内容。它还缓解了前面讨论的“零计数”问题。与前面介绍的方法不同,Jaccard 系数忽略了任何一个用户都没有购买的所有商品。也就是说,它避免了图 1 中的错误推断。
也就是说,如果一个商品被其中一个用户购买,而另一个用户没有购买,则该商品总是对 Jaccard 系数产生负信号。这可不好。
多重性
Jaccard 系数可以推广到购买次数中吗?在集合的语言中,我们在问,它能被推广到多重集上的相似性度量吗?
是的。这包括将交集、并集和基数运算从集合推广到多重集合。
两个多重集 X 和 Y 的交集可以定义如下。首先,我们取多重集下面的集合的交集。接下来,对于这个交集中的每个元素 e,我们计算它的重数
*m(e) = min(m(e,X), m(e,Y))*
这里 m(e,Z)表示多重集 Z 中元素 e 的重数。
两个多重集 X 和 Y 的并集也有类似的定义。首先,我们取多重集下面的集合的并集。接下来,对于这个并集中的每个元素 e,我们计算它的重数
*m(e) = max(m(e,X), m(e,Y))*
多重集的基数是其中不同元素的多重数之和。
让我们看一个例子。考虑多重集 X = {3a,3b}和 Y = {2a,2b,1d}。“3a”表示重数为 3 的“a”。X 和 Y 的交点是{2a,2b}。X 和 Y 的并集是{3a,3b,1d}。所以 Jaccard 系数是 4/7,交集的基数除以并集的基数。
雅克卡系数对加权雅克卡系数
让我们称 Jaccard 系数到多重集的扩展为加权 Jaccard 系数。现在两者都有了,就可以问哪个效果更好了。答案是“看情况”。我们来细说一下。
考虑 X = {2a,2b},Y = {2a,2b,c}。加权雅克卡系数给出了⅘.丢弃多重性信息的未加权 Jaccard 系数给出了⅔.直观上,前者更准确地量化了这两个多重集的相似性。到目前为止一切顺利。
接下来,改为考虑 X={4a,4b}和 Y={2a,2b}。加权的 Jaccard 系数给出 4/8 =而未加权的给出 1。哪个更准确?人们可以为未加权的一个,因为 X 和 Y 有相同的项目集。
我们再次看到,根本原因是我们混淆了集合成员和多重性(比如购买计数)。正如我们在前面章节中所做的那样,我们可以通过适当的预处理来分离出这些影响。
基于用户的推荐器
为了重述,让我们首先总结一下到目前为止我们所描述的内容。我们有历史交易数据,可能还会增加内容。后者可以是关于项目的内容、关于用户的内容或者两者都有。根据这些数据,我们以前面描述的方式为每个用户构建一个向量。
基于用户的推荐器首先找到其行向量与 u 的行向量最相似(并且足够相似)的 k 个用户。( k 是自由参数。)我们将这些称为 u 的 k 邻居。
然后,它将这些邻居的行向量与 u 的行向量进行比较,以决定向 u 推荐什么。
让我们看一个简单的例子。将 k 设置为 3。比方说 u 的项目集是{a,b,c,d}。说 u 的三个最近邻的项集是{a,b,d, e },{b,c,d, e },以及{a,c,d, e }。突出的是‘e’出现在所有 u 的邻居的项目集合中,而不是在 u 的项目集合中,所以向 u 推荐‘e’是有意义的。
让我们将这个例子中的观点形式化和一般化,如下所示。
*1\. Find the intersection I of the item sets of the *k* neighbors.
2\. Recommend to *u* the items in I that are not currently in *u*’s item
set.*
当 k 不是很小时,比如说 k = 10,步骤 1 可能太保守了,因为我们取的是很多集合的交集。我们可以通过降低 k 来缓解这种情况。然而,这会影响推荐的质量,因为我们现在基于更少的邻居。一种不同的方法是放宽这种 k 的相交标准。这类似于谷歌对包含许多单词的查询所做的。并非所有这些都需要出现在匹配的文档中。
基于项目的推荐器
考虑一个新用户。到目前为止,这个用户几乎没有与系统交互(如果有的话)。所以没有足够的数据来找到这个用户的邻居。因此,基于用户的推荐器在这里是无效的。
基于项目的推荐器工作方式不同。它识别出在同一购物车中出现的比预期的更频繁的商品集合。这些集合被称为频繁项目集。对于购买了频繁项目集中的一些项目的新用户,可以推荐相同集合中的一些其他项目。
更深入一点,我们从例子开始。考虑下面的一系列交易。
*{a, c}, {a, c}, {a, c}, {b, c}, {d, c}*
a、b、c 和 d 是项目。每一组代表一个交易,想想购物车。在本例中,所有事务每个都有两个项目。我们能从这些交易中收集到什么?首先,对{a,c}是一个频繁项集。更重要的是,关联规则 a → b 是一个很好的规则。我们来阐述一下后者。
关联规则 X → Y 表示
*IF X THEN Y*
在我们的设置中,X 和 Y 是不相交的项目集,关联规则捕获了我们的意图,即如果用户购买了 X,推荐 Y。
不是所有的关联规则都一样好,所以我们需要一个关联规则良好性的概念。(事实上,如果没有这一点,我们将有大量的关联规则需要考虑,我们将一事无成。)
关联规则优度通常沿着两个维度定义:支持度和置信度*。关联规则的支持量化了规则的适用范围。规则的可信度量化了规则的结果给定先行结果的可能性。*
在形式上,支持度与 P(X+Y)成正比,即 X+Y 中的所有商品都在同一个购物车中的概率。(“+”表示集合并集。)置信度与 P(Y|X)成正比,即假设购物车包含所有 X,购物车包含所有 Y 的概率。
好的关联规则支持度高,可信度高。
让我们把这个应用到我们的例子中。在这个例子中,为了简单起见,我们将把一个项目集的支持定义为它发生的事务的数量。考虑规则 a → c。当{a,c}出现在三个事务中时,它的支持度是 3。它的置信度为 1,因为在 a 发生的每个事务中,c 也发生。现在考虑规则 c → a,它的支持度也是 3。然而,它的信心更低,⅗.这是因为在发生 c 的 5 个事务中,有 3 个也发生了 a。所以规则 a → c 优于规则 c → a,也就是说,当有人买 a 时我们应该推荐 c,而不是相反。
电梯
前面提到过,一个关联规则 X → Y 的置信度是 P(Y|X),Y 在包含 X 的购物车中的概率,现在考虑 P(Y)。如果这也很高呢?就是 Y 里面的物品很受欢迎。仅仅因为这个事实,P(Y|X)就可能很高。也就是说,这个关联规则的高可信度可能主要来自于它的结果非常受欢迎。
一种不同的方法叫做升力*,可以更好地解决这个问题。X → Y 的升力定义为 P(Y|X)/P(Y)。如果 Y 在包含 X 的篮子中比在随机篮子中更可能被找到,那么这个提升大于 1。*
作为一个关联规则强度的度量,lift 是否总是比 confidence 更有效?不总是。因为提升需要取 Y 的两个概率的比值,所以当 P(Y)很小时,它容易受到噪声的影响,这是经常发生的情况。
例如,假设 P(Y) = 0.001。也就是说,平均 1000 个篮子中有 1 个包含 Y,现在假设有 400 个篮子包含 X,其中一个恰好也包含 Y,给定 X,Y 的升力为 1000/400 = 2.5。
对项目间的负面关联进行建模
考虑一个关联规则 i → j,这个表示:如果用户购买我推荐 j 。如果我们也对负面联想建模,会有意义吗?即型号如果用户购买我不推荐 j* 。*
经历过从推荐系统接收推荐的读者可能倾向于回答是。这是因为他们经常看到推荐的商品对他们购买的商品毫无意义。用数据科学的语言来说,这样的建模可以减少误报。
规则 i → j 的解除可以在这方面帮助我们。远小于 1 的升力表示 j 与 I 负相关。
使用 lift 来推断负面关联应该保守地进行。让我们用一个例子来说明这一点。说 P(j)是 0.001,即。j 平均每 1000 个篮子里出现 1 个。现在考虑包含 I 的篮子,假设这些篮子都不包含 j,我们需要几千个这样的篮子,才能有把握地推断 j 不会出现在包含 I 的篮子里。
混合方法
考虑一个描述“当用户购买 a 时推荐 b”的关联规则 a → b。在基于项目的方法中,支持度和信心同等地衡量所有用户的贡献。我们可以通过了解我们推荐给谁来改进这一点。具体来说,我们会给予被推荐人的邻居的贡献更高的权重。
我们可以将此总结为
*recommend b when user u buys a and many users with tastes similar to those of u also buy b when they buy a.*
用户和物品在同一个空间
考虑物品的特征。如品牌*、价格、产品、类别。(还有很多其他的。)说这些特征是已知的。从交易数据中,我们可以推断出用户的喜好投射到这些特征上。我们来细说一下。*
假设某个用户一直购买苹果智能手机。由此可以推断出用户对苹果的偏好,至少是对智能手机的偏好。或许与安卓智能手机相反。我们甚至可以选择包含多种功能组合的偏好。喜欢苹果智能手机的用户可能更喜欢运行 Windows 的笔记本电脑。我们可以学习这种多特征偏好。
在同一个空间中把用户和项目都表示为向量是很吸引人的。我们可以使用我们到目前为止讨论过的任何向量相似性度量(例如点积)来查找匹配给定用户向量的项目向量,或者反过来。
用户的向量表示用户喜欢各种属性和组合的程度。例如品牌名称、价格、产品类别等等。项目的向量表示项目的属性。因此,类似于项目向量的用户向量意味着用户的偏好与项目的属性相匹配。
由于这些向量具有透明的解释,推荐者还可以附加一个解释来伴随任何特定的推荐。比如“我们向您推荐这款商品,因为它具备这些特质”。这里,特征是从用户和项目向量中匹配的那些特征中选择的。
潜在空间
对于某些类型的物品,很难手工设计一组好的属性,同时对描述物品的和喜欢或者不喜欢物品的有效。接下来,当然是给每件物品贴上属性标签。**
考虑 Youtube 视频。特定视频的潜在特征是什么,揭示了为什么它会引起某些人的共鸣?如果我们可以推断出这些,我们就可以向这些人推荐其他具有类似特征的视频。
这些特征甚至很难描述。(双关。)即使他们不是,谁会给一个新视频贴上适用于它的特定特征的标签呢?
事实证明,某些算法可以从用户-项目矩阵开始,并以某种方式从中推导出用户和项目都可以投影到的空间。这听起来很难理解。怎么会?
首先,让我们观察到,这个潜在空间通常比我们开始的用户向量或项目向量的维度低得多。用户向量的维度是项目的数量。这可能是数百万。一个项目的维度就是用户的数量。这也可能是数百万。相比之下,潜在空间模拟物品的特征有两个目的,一是描述它们,二是辨别用户的口味。在许多设置中,例如电影,少于一百个特征可能就足够了。当然,我们并没有说这些特征是什么,只是限制了多少“不动产”足以捕捉服务于我们目的的特征。
推断用户和项目向量具有相同的潜在空间
假设我们有可用的评价三元组(u,I,rui ),其中 rui 是用户 u 给予项目 I 的评价(比如从 1 到 5)。在有许多用户和许多项目的设置中,这可以被视为用户 X 项目的高维矩形稀疏矩阵。(稀疏,因为只有一小部分用户项目组合具有评级。)
从这个矩阵中,我们可以推断出低维度潜在空间中的用户和项目向量。推断的用户和项目向量应该使得观察到的评级可以从潜在空间中的相似性计算中预测。
我们所说的“潜在空间中的相似性计算”是什么意思?下面我们来解释一下这个。以我们的三元组数据集{(u,I,rui)}为例。将其随机分为训练集和测试集。比如五五开。从训练集中推断用户和项目向量。使用这些来预测每个三元组(u,I,rui)的评级 rui。如下。
设 latent(u)和 latent(i)表示用户 u 和项目 I 的潜在向量,设点积 latent(u)点 latent(i)作为 rui 的预测。
现在我们来看看训练过程。首先,我们必须选择潜在空间的维度。原则上,这本身可以由训练方法决定。(这将对应于模型选择或网格搜索,将该维度视为超参数。)即便如此,我们也必须选择应该对哪些维度值进行网格搜索。
我们经常跳过超参数调整,只选择看起来合理的值。考虑电影。我能马上想到至少五个特征:动作、喜剧、戏剧、…预料到可能还有其他有用的组合,我可能会选数字 20。当然,这看起来像猜测。我们仍然可以期望得到一个推荐器,它比明显错误的极端值(比如 1 和 100000)有所改进。
好了,现在我们已经选择了潜在空间的维度。接下来,让我们讨论我们寻求优化的标准,以便在这个空间中找到好的用户和项目向量。幸运的是,我们已经在前面的段落中暗示过这一点。用户向量 u 和项目向量 I 应该使得它们在训练集上预测的评级是尽可能最好的,如通过一些损失函数所测量的。通常使用平方损失。形式上,这个优化标准是找到最小化的用户和项目向量
*sum_{triple (u, i, rui) in training set}
(rui — latent(u) *dot* latent(i))²*
通常我们还会添加一个正则项来支持稀疏的用户和项目向量。
让我们用电影的背景来说明这种直觉。假设我们选择 n = 20,因为我们合理地认为我们大约需要这么多的特征来覆盖同一空间中的电影和用户口味的范围。任何特定用户的口味都不可能涵盖所有 20 个方面。更有可能的是,用户的口味涵盖了这 20 种口味中的一小部分(当然我们还不知道是哪一种)。同样,任何一部电影的向量都可能是稀疏的。
好了,还剩下什么。训练算法。在这种情况下,一种流行且有效的选择是一种特殊形式的随机梯度下降*。*
假设我们已经初始化了各种潜在的用户和项目向量。然后,我们从训练集中的三元组(u,I,rui)开始迭代训练,通过在整个训练集中的一次或多次传递。
考虑任何单一的训练迭代。根据(u,I,rui ),我们首先计算预测误差
*eui = rui — latent(u) *dot* latent(v)*
接下来,我们独立地将 latent(u)和 latent(i)中的每一个推向使 eui 更接近 0 的值。然后在下一组训练中重复。
总结
在这篇文章中,我们讨论了推荐系统的主题。我们研究了我们可以从中学习推荐的数据的性质。具体来说,(I)购买及其数量,以及(ii)用户评级。我们讨论了基于用户间相似性或项目间相似性的协同过滤。在这次讨论中,我们还讨论了各种相似性度量,如点积和 Jaccard 系数,以及各种预处理,如二值化和居中。
我们还讨论了如何利用内容来提高推荐的质量。最后,我们讨论了通过将用户数据和项目数据投影到同一个空间来操作的方法。
延伸阅读
篮子分析
确认琐碎,揭开神秘,发掘有用
什么是购物篮分析?
所有这些商品都属于同一个顾客购物篮吗?——朱利叶斯·德罗斯特在 Unsplash 上的照片
篮子分析是一个非常简单的概念。想象顾客在杂货店购物。他们从货架上拿起商品,走到收银台付款。然后,您收集来自客户的所有收据,并逐个客户地创建一个他们购买了什么的列表(即,他们的购物篮或购物车中有什么)。您可能会得到如下结果,其中{…}表示客户购物篮中的商品:
- 顾客 A:{花生酱、果冻、面包、米饭、麦片、牛奶}
- 顾客 B:{花生酱、果冻、香草、土豆}
- 顾客 C:{花生酱、面包、麦片、牛奶}
- 顾客 D:{牛奶,麦片}
看看这些购物品,有几个明显的联想:
- {花生酱} = > {果冻}有 67%的可能性(即,2/3 装了花生酱的篮子里也有果冻)
- {花生酱} = > {面包}有 67%的可能性(也就是说,2/3 装了花生酱的篮子里也有面包)
- { bread } = > {花生酱}有 100%的可能性(也就是说,每个装面包的篮子里都有花生酱)
- {面包、谷物} = > {牛奶} 100%概率(即,每个有面包和谷物的篮子里也有牛奶)
- {麦片,牛奶} = > {面包}有 67%的可能性(即,有麦片和牛奶的 2/3 篮子里也有面包)
这些被称为关联规则或者简称为关联规则。它们告诉我们哪些产品倾向于一起选择,以及知道一个产品在篮子里将如何预测篮子里可能还有什么。这种类型的信息非常有用(很快会有更多)。
发现关联的挑战在于可能组合的绝对数量。想象一个有 100 种不同产品的商店。购物篮中只有一件商品的顾客可以有 100 种可能的组合。拥有两种产品的客户可以有(100 x 99)/2 = 4,950 种可能的组合。如果项目数增加到 3,则有(1009998)/(32) = 161,700 种可能的组合。有了四种产品,就有了 3921255 种可能的组合。这是一个经典的组合问题,你有一组 N 个项目,并从中选择 R;给出可能组合数的方程是 N!/[R! (N-R)!].组合数学的增长甚至比指数增长还要快得多——这使得分析数据来识别相关产品变得不可能,除非你使用一个聪明的算法;无论你的计算资源如何,试图彻底检查每一个可能的组合都是不可行的。
Apriori 算法
幸运的是,有一个聪明的算法:“Apriori 算法”,它被设计成通过一个简单的先验假设来快速评估非常大的数据集。对于被认为是关联的一组项目,该组必须以足够的频率出现。**但是如果一组项目频繁出现,那么这些项目的子集也一定频繁出现。**这意味着我们可以简单地通过排除不常出现的子集来排除大量的候选关联。在我们这个微不足道的例子中,只有一个顾客购买了大米,但是有 3 个顾客购买了花生酱。这意味着米饭只出现了 25%,而花生酱出现了 75%。浏览列表,我们看到客户购物篮中的每种产品出现的频率如下:
- 谷物=> 75%
- 牛奶=> 75%
- 花生酱=> 75%
- 果冻=> 50%
- 面包=> 50%
- 大米=> 25%
- 草药=> 25%
- 土豆=> 25%
由于草药和土豆出现的频率相对较低,我们可以将它们排除在考虑范围之外,因为这些项目加上其他项目的组合也很少出现。这将搜索空间从 8 个项目减少到只有 6 个项目,从而将可能的集合数量从 256 个减少到只有 64 个。Apriori 算法将这种假设应用于所有可能的子集(例如,{花生酱、果冻}、{牛奶、谷物}),以快速消除整个潜在关联组,这使得处理大型数据集变得可行。
我真心推荐感兴趣的读者购买带 R 的机器学习,里面有关于 Apriori 算法的精彩章节。在 r 中使用这种技术非常简单。作为一个例子,让我从概念上向您展示应用这种算法需要多少行代码:
library(‘a rules’);data <- read.csv(file=”segment.csv”, sep=”,”)data_xtabs <- as.data.frame.matrix(xtabs(~account_id+segment_name, data));data_xtabs_logical <- as.data.frame(data_xtabs > 0);transactions <- as(data_xtabs_logical, “transactions”);summary(transactions);rules <- apriori(transactions, parameter = list(support = 0.01, confidence = 0.5, minlen = 2));summary(rules);
尽管我强烈建议您在使用 Apriori 算法之前先理解它背后的理论…是的,它确实很容易使用。完全公开我从我的例子中删除了 3-4 行对我们的讨论不重要的代码,但是上面的 R 代码非常有代表性,只有 8 行。在这 8 行代码中,一行用于加载 Apriori 库,四行用于加载数据,两行分别用于打印数据和规则的摘要。您可以看到为什么我是 R 的狂热爱好者,并强烈推荐那些对开发强大的分析功能非常感兴趣的组织习惯于使用 R(或 Python)并远离像 Excel 这样的简单工具。
当使用 Apriori 算法时,有三个关键概念你必须理解:支持、信任和提升。这些很容易理解:
- Support —这是对一组项目出现在数据中的频率的度量。例如,如果花生酱的支持度为 50%,这意味着 50%的顾客在购物篮中放了花生酱。同样,如果{花生酱、果冻}的支持度是 25%,那么每 4 个顾客中就有 1 个顾客的篮子里有的花生酱和果冻。所需的支持度越高,Apriori 算法不考虑的数据就越多。
- 信心——这是对一组相关产品预测能力的衡量。例如,如果{花生酱} =>{果冻}的置信度为 75%,这意味着每个放有花生酱的篮子也有 75%的机会在同一个篮子中放有果冻。类似地,说{面包,谷物} =>{牛奶}的置信度是 100%意味着每个既有面包又有谷物的篮子也有牛奶。所以信心告诉你,你能在多大程度上信任这个协会。置信度越高,您将删除的关联就越多(即,不予考虑)。
- 提升 —这是衡量您对相关产品组合的惊讶程度;不是普通意义上的惊讶,而是统计学意义上的惊讶。例如,假设你有一个篮子,里面有 1 个蓝球和 9 个红球。如果你伸手去抓一个随机的球,你有 10%的几率得到一个蓝球,90%的几率得到一个红球。让我们假设你把手伸进篮子 3 次,随机抓一个球,看看颜色,然后把球放回篮子里。据统计,得到 3 个蓝球的概率只有 0.1%,这意味着每 1000 次尝试中,你应该只有一次得到 3 个蓝球(平均)。如果你继续尝试,在这 1000 次尝试中,最终得到这样的结果 50 次,而不是 1 次或 2 次,那么你应该会非常惊讶。得到 50 次而不是 1 次这样的结果意味着它发生的频率是随机概率预测的 50 倍。所以 lift 基本上是用关联发生的频率除以关联偶然发生的概率(即关联不是真实的)。
在使用 Apriori 算法分析数据时,对于支持度、置信度或升力应该是多少,并没有正确的答案。然而,我喜欢使用一个简单的经验法则:
- 支持 —这应该根据您的数据进行缩放。例如,如果您正在查看客户群相互关联的频率,但只有 10%的客户重复购买,那么您的支持度不能超过 10%,因为 90%的数据都只有一件商品。所以我通常使用一个简单的规则,即支持度应该至少是我期望项目子集出现在数据中的频率的 1%。
- 信心——一个没有预测性的关联是没有用的,所以一般来说,只要有可能,我喜欢看到至少 50%的信心,这样当你使用这个关联时,你往往是“正确的”。
- 升力 —任何升力小于 10x 的尺子都应谨慎使用;我喜欢看到这种提升至少比偶然性高一个数量级,让我觉得这是一种真实的联系,而不仅仅是一种随机的巧合
琐碎的、神秘的、有用的
每当您使用像 Apriori 算法这样的算法来建立关联时,您通常会得到可分为三组的规则:
- 琐碎的事情
- 神秘的
- 有用的
琐碎的规则是显而易见的关联。例如,如果你用这种技巧告诉商业领袖,购买尿布的顾客也购买婴儿配方奶粉,你可能会被他们笑出房间。或者买花生酱的顾客也买果冻。这些都是显而易见的,不需要千兆字节的数据和复杂的算法就能发现。一般来说,您会从大多数业务数据中获得大量“琐碎”的规则。
神秘的规则是数据中出现的关联,但很难解释,即使关联是真实的而不是随机的,也不一定是你会做的事情。例如,假设您发现购买牛奶与购买自动铅笔密切相关。这将是一个令人挠头的规则,因为没有理由——至少我能想到——为什么把牛奶放入购物车的顾客也会放自动铅笔。你可能不会重新布置你的商店,把铅笔和钢笔放在牛奶区。所以我会把这些类型的挠头联想归类为“神秘”——尽管这并不意味着知道它们没有用。你用这些做什么取决于你的策略…这将在稍后讨论。
最后是有用的规则。这些规则并不琐碎,也不“神秘”或“怪异”,而且你实际上可以立即投入使用。你有没有想过,为什么当你走过家得宝或其他大型五金店时,他们会习惯性地在过道上挂一些不相关但看起来合适的产品样品?例如,我注意到我当地的家得宝会在木材区放少量的锤子和钉子…整个过道里的钉子比你需要的还要多,锤子的种类也比大多数人见过的多,但出于某种原因,他们还是在木材区放了一些样品。为什么?这是因为联想。通过人工分析、轶事或使用 Apriori 算法等技术,有人注意到购买木材的人也倾向于购买锤子和钉子。这不是一个微不足道的规则,因为我会假设大多数从木材通道购买木材的人已经至少有一把锤子和大量的钉子,因为木材不是大多数人心血来潮购买的东西。然而,人们仍然倾向于一起购买这些东西…如果你处理木材,也许你永远不会有足够的锤子和钉子?
分析!=策略…是的,我的朋友,你必须思考!
分析师和数据科学家有时会犯错误,认为分析就是一切。那是一个错误。没有战略的分析就像购买大量水泥和木材,然后期待房子自己建造。分析只是一个构建模块…战略实际上是使用这些构建模块来完成一些有用的事情。所以你用 R 写了 8 行代码,发现了一系列神秘而有用的关联(希望你明白我的意思,忽略那些无关紧要的)…现在恭喜你,你打算用这些关联做什么呢?如果你是一名分析师或数据科学家,像商业人士一样思考并问自己这个问题是很重要的:“那又怎样?”
如果你曾经逛过杂货店,你会注意到牛奶区总是在杂货店的后面。这是有目的的:它迫使你在杂货店徘徊,这将增加你购买更多东西而不仅仅是牛奶的机会。这就是策略!让我们考虑一个类似{花生酱} = > {果冻}的琐碎联想。对于这种关联,您可以采取两种基本策略:
- 将花生酱放在远离果冻的另一个通道,迫使购物者四处逛逛,增加他们购买其他东西的机会
- 将花生酱放在同一个过道上,以增加顾客在拿到花生酱时购买果冻的机会
哪种策略更好?没有正确的答案。事实上,我见过一些杂货店使用一种策略,而另一家使用另一种策略。将花生酱和果冻放在远离彼此的地方可能会增加顾客在寻找两种产品时抓住另一种产品的机会,但如果他或她不想玩“捉迷藏”,则可能会有顾客只抓住一种并离开的风险。将花生酱和果冻放在同一个货架上,增加了顾客同时拿到两种产品的机会,并轻松创造更多收入,但如果顾客被迫四处闲逛,他们可能会拿到第三种产品。决定的唯一方法是运行 A/B 测试,比较每个策略产生的收入。如果你想更好地理解 A/B 真正衡量的是什么,我也为写了一篇关于这个的文章。
线性回归的批、小批和随机梯度下降
三种基本梯度下降变体的实现和比较
图片由来自 Pixabay 的 geralt 提供,由作者修改
1.介绍
梯度下降算法是一种迭代一阶优化方法,用于找到函数的局部最小值(理想情况下是全局最小值)。它的基本实现和行为我已经在我的另一篇文章中描述过了。这一个集中在算法用来计算梯度和制作步骤的数据量的三个主要变量上。
这三种变体是:
- 批量梯度下降(BGD)
- 随机梯度下降
- 小批量梯度下降(mBGD)
在本文中,我们将在一个简单的线性回归任务中看到它们的性能。
简单回顾一下——一元线性函数定义为:
它由两个系数参数化:
- a0 -偏差
- a1 -函数的斜率。
出于演示目的,我们定义以下线性函数:
其中 σ 是白(高斯)噪声。下面的代码为我们将要使用的数据集生成了 100 个点。
我们希望最小化的成本函数(指标)是均方误差,定义为:
在一元函数的情况下,它可以明确地写成:
下面的代码计算给定的一组两个参数的 MSE 成本。
注意,对于我们的原始系数,由于随机误差(白噪声),最小成本函数不为 0(它将在每次运行时变化),此时等于:
下图显示了最佳点附近的这个函数。我们可以看到它有一个细长的碗的形状。
围绕全局最小值的成本函数;作者图片
要使用任何梯度下降算法,我们必须计算这个函数的梯度。因为对于一元线性回归,我们的算法最小化 2 个系数,我们必须分别计算它们的导数。让我们注意到:
现在,使用链规则我们获得以下结果:
下一节将关注算法本身。使用的代码可以在我的 GitHub 库上找到。
2.批量梯度下降
在批处理 GD 中,在每一步都使用整个数据集来计算梯度(记住:我们不计算成本函数本身)。下图显示了它在优化过程中的表现。它需要 86 次迭代来找到全局最优值(在给定的容差内)。
批量梯度下降过程的动画;作者图片
批量梯度下降的轨迹;作者图片
批量梯度下降的轨迹看起来很好——每一步都越来越接近最优,横向振荡随着时间的推移越来越小。这是它具有良好收敛速度的原因。
为了准确地找到它的收敛速度,我们必须做一些数学。为了不过分复杂,让我们假设我们的成本函数是强凸的(两次可微的)并且具有一个 Lipschitz 连续梯度,其中 L > 0 定义为:
第二个假设限制了渐变的速度。
如果您可以计算 L,那么您可以导出所谓的**“保证进度的界限”**,它是保证收敛的步长(学习速率):
然而,你不应该在实践中使用这个值,因为它真的很小而且收敛很慢。找到最佳学习率是一个巨大的话题,适合单独写一篇文章——只需检查一些东西,例如“回溯线搜索”,“阿米霍条件”或“沃尔夫条件”。
假设固定步长收敛速度取决于函数的凸性。
对于简单(弱)凸函数,收敛速度为[1]:
其中 k 是迭代次数。该速率称为“亚线性收敛”,对于给定的容差ε,需要以下迭代次数才能收敛[1]:
对于强凸函数,比率为[1]:
其中 0
Pros and Cons of Batch Gradient Descent:
优点:
- 一个简单的算法,只需要计算一个梯度
- 在训练期间可以使用固定的学习速率,并且可以预期 BGD 收敛
- 如果损失函数是凸的,非常快地收敛到全局最小值(对于非凸函数,非常快地收敛到局部最小值)
缺点:
- 即使使用矢量化实现,当数据集很大时(大数据的情况),速度也可能很慢
- 不是所有的问题都是凸的,所以梯度下降算法不是通用的
典型使用案例:
- 适合计算机内存的小型数据库
- 凸成本函数的问题(如 OLS,逻辑回归等。)
3.随机梯度下降
随机梯度下降的思想不是使用整个数据集来计算梯度,而是仅使用单个样本。目标是加快这一进程。就选择样本而言,有两条主要规则:
- 随机规则—随机选择的样本(可能重复)
- 循环规则—每个样本一次(无重复或重复次数最少)
随机规则更常见。
- 下图显示了 SGD 如何收敛到最终解(示例性运行)。红点表示为给定步长计算选择的样本。
SGS 收敛过程的动画;作者图片
由于其随机性,每次运行需要不同数量的步骤来达到全局最小值。在相同起点(0,0)和相同学习率(0.05)下运行 100 次所需的迭代直方图下方。
收敛所需的迭代次数;作者图片
与批处理 GD 相反,它不会直接收敛到解,因为它每次迭代只使用 1 个样本,这意味着步骤非常嘈杂。但是,它的效率要高得多,CPU/GPU 负载更少。这种影响对于小型数据库(像这样)几乎看不到,但在处理大数据时会对性能产生巨大影响。
下图显示了上例中 SGD 步骤的轨迹。
随机梯度下降的轨迹;作者图片
固定步长随机梯度下降的收敛速度[1]:
这意味着 SGD 不像批量梯度下降那样具有线性收敛速度——仅仅意味着它需要更多的迭代(但不一定需要计算时间)。
随机梯度下降的利与弊:
优点:
- 对于大型数据集,比批量 GD 收敛更快(时间更少)
- 可以逃离局部极小值
缺点:
- 步骤更嘈杂— SGD 可能需要更多迭代才能收敛到限制值
- 它可以在全局最优值附近“跳跃”——它可能需要比批量 GD 更大的容差
典型使用案例:
- 是用于训练人工神经网络的更高级随机算法的基础
4.小批量梯度下降
小批量梯度下降是一种在纯 SGD 和批量梯度下降之间找到良好平衡的方法。想法是使用一个观察子集来更新梯度。每个尺寸所用的点数称为批量,一个批量的每次迭代称为一个时期。下面的动画显示了每个步骤中使用的点的收敛过程(批量大小为 10)。
小批量梯度下降的收敛过程:作者图片
轨迹仍然是嘈杂的,但更稳定地走向最小值。
小批量梯度下降的轨迹;作者图片
该算法的收敛比介于 BGD 和 mBGD 之间,为[1]:
其中 b 是批量大小。
小批量梯度下降的利与弊:
优点:
- BGD 和新加坡元之间在效率方面的良好平衡
- 很容易放入计算机内存
- 可以避开局部最小值
缺点:
- 它仍然可以在全局最优值附近“反弹”——它可能需要比批量 GD 更大的容差,但小于 SGD
- 另一个需要优化的超参数—批量
典型使用案例:
- 这是深度神经网络训练中非常常见的算法
5。总结
我们经历了梯度下降算法的 3 个基本变体。在当代的 ML 中,使用了更先进和更有效的版本,但是仍然使用这里描述的基本思想。进一步的修改包括自适应学习率、各种动量(如内斯特罗夫)、平均等。
一些非常流行的实现有:
有一个正在进行的研究工作,以进一步改善他们的非凸函数(深度神经网络),其中包括各种想法,每个过程的数据。
如果你想了解更多关于本文主题的细节,我强烈建议你查阅这些阅读材料:
- 加州大学伯克利分校 Ryan Tibshirani 的随机梯度下降
- 加州大学伯克利分校 Ryan Tibshirani 的凸优化
- 用不一致随机梯度下降加速深度神经网络训练
- 深度神经网络训练中随机梯度下降的不收敛性
- 带洗牌的分布式随机梯度下降的收敛性分析
Batch Norm 直观地解释了它是如何工作的,以及为什么神经网络需要它
动手教程,直观深度学习系列
一个非常重要的深度学习层的温和指南,用简单的英语
Batch Norm 是现代深度学习实践者的工具箱中必不可少的一部分。在批量标准化论文中介绍之后不久,它就被认为在创建可以更快训练的更深层次的神经网络方面具有变革性。
Batch Norm 是现在很多架构中普遍使用的神经网络层。它通常作为线性或卷积块的一部分添加,有助于在训练期间稳定网络。
在本文中,我们将探讨什么是批处理规范,为什么我们需要它,以及它是如何工作的。
你可能也会喜欢阅读我的另一篇关于批处理规范的文章,这篇文章解释了为什么批处理规范如此有效。
如果你对一般的神经网络架构感兴趣,我有一些你可能会喜欢的文章。
但是在我们讨论批量规范化本身之前,我们先来了解一些关于规范化的背景。
标准化输入数据
当向深度学习模型输入数据时,标准做法是将数据归一化为零均值和单位方差。这意味着什么,我们为什么要这样做?
假设输入数据由几个特征 x1,x2,…xn 组成。每个要素可能有不同范围的值。例如,特性 x1 的值可能在 1 到 5 之间,而特性 x2 的值可能在 1000 到 99999 之间。
因此,对于每个特征列,我们分别获取数据集中所有样本的值,并计算平均值和方差。然后使用下面的公式对这些值进行归一化。
我们如何标准化(作者图片)
在下图中,我们可以看到数据归一化的效果。原始值(蓝色)现在以零为中心(红色)。这确保了所有的特征值现在都在相同的比例上。
标准化数据是什么样子的(图片由作者提供)
为了理解不进行归一化会发生什么,让我们来看一个只有两个比例完全不同的要素的示例。由于网络输出是每个特征向量的线性组合,这意味着网络学习每个特征在不同尺度上的权重。否则,大特性会淹没小特性。
然后在梯度下降过程中,为了“移动指针”以减少损失,网络必须对一个权重进行较大的更新。这可以导致梯度下降轨迹沿着一个维度来回振荡,从而采取更多的步骤来达到最小值。
不同尺度的特征需要更长时间才能达到最小值(图片由作者提供)
在这种情况下,损失景观看起来像一个狭窄的峡谷。我们可以沿着二维分解梯度。沿着一个维度是陡峭的,而沿着另一个维度则平缓得多。
由于梯度较大,我们最终对一个权重进行了较大的更新。这将导致渐变下降反弹到斜坡的另一侧。另一方面,沿着第二方向的较小梯度导致我们进行较小的权重更新,从而采取较小的步骤。这种不均匀的轨迹需要网络更长的时间才能收敛。
狭窄的山谷导致梯度下降从一个斜坡反弹到另一个斜坡(图片由作者提供)
相反,如果要素在相同的比例上,损失景观会像碗一样更加均匀。梯度下降然后可以平稳地进行到最小值。
归一化数据帮助网络更快收敛(图片由作者提供)
如果你想了解更多这方面的内容,请参阅我的《神经网络优化器》,其中详细解释了这一点,以及不同的优化器算法如何发展来应对这些挑战。
批量定额的必要性
既然我们理解了什么是规范化,那么需要批量规范化的原因就开始变得清晰了。
考虑网络的任何隐藏层。来自前一层的激活只是这一层的输入。例如,从下图中第 2 层的角度来看,如果我们“清空”所有之前的层,来自第 1 层的激活与原始输入没有什么不同。
要求我们对第一层的输入进行规范化的逻辑同样适用于每一个隐藏层。
每个隐藏层的输入是来自前一层的激活,也必须被标准化(作者的图像)
换句话说,如果我们能够以某种方式标准化来自每个先前层的激活,那么梯度下降将在训练期间更好地收敛。这正是批处理规范层为我们做的。
批量定额是如何工作的?
Batch Norm 只是插入到一个隐藏层和下一个隐藏层之间的另一个网络层。它的工作是获取第一个隐藏层的输出,并在将它们作为下一个隐藏层的输入传递之前对它们进行归一化。
批处理规范层在激活到达第 2 层之前对第 1 层的激活进行规范化
就像任何网络层的参数(如权重、偏差)一样,批规范层也有自己的参数:
- 两个可学的参数叫做β和γ。
- 两个不可学习的参数(平均移动平均值和方差移动平均值)被保存为批次标准层“状态”的一部分。
一批规范图层的参数(图片由作者提供)
这些参数是每批定额层。因此,如果我们在网络中有三个隐藏层和三个批次范数层,我们将有三个可学习的β和γ参数用于这三个层。移动平均参数也是如此。
每个批次定额层都有自己的参数副本(图片由作者提供)
在训练期间,我们一次向网络提供一小批数据。在转发过程中,网络的每一层都处理这一小批数据。批次定额层按如下方式处理其数据:
批量定额层执行的计算(图片由作者提供)
1。激活
来自前一层的激活作为输入被传递给批量定额。数据中的每个特征都有一个激活向量。
2.计算平均值和方差
对于每个激活向量,分别计算小批量中所有值的平均值和方差。
3.使标准化
使用相应的平均值和方差计算每个激活特征向量的归一化值。这些标准化值现在具有零均值和单位方差。
4.缩放和移位
这一步是 Batch Norm 引入的巨大创新,赋予了它力量。与要求所有归一化值的均值和单位方差为零的输入图层不同,Batch Norm 允许其值被移动(到不同的均值)和缩放(到不同的方差)。这是通过将归一化值乘以系数γ,再加上系数β来实现的。请注意,这是一个元素级乘法,而不是矩阵乘法。
这项创新的巧妙之处在于这些因素不是超参数(即由模型设计者提供的常数)但是是由网络学习的可训练参数。换句话说,每个批次范数层都能够以最佳方式找到自己的最佳因子,因此可以移动和缩放归一化值以获得最佳预测。
5.移动平均数
此外,Batch Norm 还保存平均值和方差的指数移动平均(EMA)的运行计数。在训练期间,它只是计算这个均线,但不做任何事情。在训练结束时,它只是将该值保存为层状态的一部分,供推断阶段使用。
我们将在稍后谈到推论时回到这一点。移动平均计算使用下面用 alpha 表示的标量“动量”。这是一个超参数,仅用于批处理范数移动平均值,不应与优化器中使用的动量相混淆。
向量形状
下面,我们可以看到这些向量的形状。计算特定要素的矢量时涉及的值也以红色突出显示。但是,请记住,所有的特征向量都是在一次矩阵运算中计算出来的。
批量范数向量的形状(图片由作者提供)
向前传球后,我们像平常一样向后传球。对所有层权重以及批标准层中的所有β和γ参数计算梯度并进行更新。
推理过程中的批量规范
正如我们上面讨论的,在训练过程中,批量定额从计算小批量的平均值和方差开始。然而,在推断过程中,我们有一个单一的样本,而不是一个小批量。在这种情况下,我们如何获得均值和方差?
这就是两个移动平均参数的用武之地,它们是我们在训练过程中计算出来并与模型一起保存的。在推断过程中,我们将这些保存的平均值和方差值用于批量定额。
推理时批量定额计算(图片由作者提供)
理想情况下,在训练期间,我们可以计算并保存全部数据的平均值和方差。但这将非常昂贵,因为我们必须在训练期间将整个数据集的值保存在内存中。相反,移动平均线很好地代表了数据的均值和方差。这样效率更高,因为计算是增量式的——我们只需记住最近的移动平均值。
批量定额层的放置顺序
对于批处理规范层应该放在架构中的什么位置,有两种观点——激活之前和激活之后。最初的论文把它放在前面,虽然我想你会发现文献中经常提到这两个选项。有人说“之后”会有更好的结果。
批量定额可在激活前或激活后使用(图片由作者提供)
结论
Batch Norm 是一个非常有用的层,您最终会在您的网络架构中经常使用它。希望这能让你很好地理解如何使用批处理规范。
理解为什么批处理规范有助于网络训练也是有用的,我将在另一篇文章中详细介绍。
最后,如果你喜欢这篇文章,你可能也会喜欢我关于变形金刚、音频深度学习和地理定位机器学习的其他系列。
让我们继续学习吧!
直观地解释了批量定额——它为什么有效?
实践教程,直观的深度学习系列
一个温和的指南的原因,批量规范层的成功,使训练收敛更快,在平原英语
由 Unsplash 上的absolute vision拍摄
批量范数层经常用于深度学习模型中,与卷积或线性层相关联。许多最先进的计算机视觉架构,如 Inception 和 Resnet,都依赖于它来创建可以更快训练的更深层次的网络。
在本文中,我们将探讨为什么批处理规范有效,以及为什么在训练模型时它需要较少的训练时期。
你可能也喜欢阅读我的另一篇关于 Batch Norm 的文章,这篇文章用简单的语言解释了什么是 Batch Norm,并一步一步地介绍了它是如何在幕后运作的。
如果你对一般的神经网络架构感兴趣,我有一些你可能会喜欢的文章。
批量定额为什么管用?
毫无疑问,Batch Norm 工作得非常好,并为深度学习架构设计和培训提供了大量可测量的好处。然而,奇怪的是,究竟是什么赋予了它如此神奇的力量,目前还没有一个普遍认同的答案。
诚然,已经提出了许多理论。但是多年来,关于这些理论中哪一个是正确的一直存在争议。
最初的发明者对为什么批处理规范有效的第一个解释是基于一种叫做内部协变量转移的东西。后来在麻省理工学院研究人员的另一篇论文中,该理论被驳斥,并基于损耗和梯度曲线的平滑提出了另一种观点。这是两个最著名的假设,所以让我们在下面回顾一下。
理论 1——内部协变量转移
如果你和我一样,我相信你会觉得这个术语很吓人!😄用简单的语言来说,这是什么意思?
假设我们要训练一个模型,模型需要学习的理想目标输出函数(虽然我们事先不知道)如下。
目标函数(图片由作者提供)
不知何故,假设我们输入到模型中的训练数据值只覆盖了输出值范围的一部分。因此,该模型只能学习目标函数的子集。
训练数据分布(图片由作者提供)
该模型不知道目标曲线的其余部分。什么都有可能。
目标曲线的其余部分(图片由作者提供)
假设我们现在向模型提供一些不同的测试数据,如下所示。这与模型最初训练时使用的数据有着非常不同的分布。该模型无法对这些新数据的预测进行归纳。
例如,如果我们用客机的图片训练一个图像分类模型,然后用军用飞机的图片测试它,这种情况就会发生。
测试数据有不同的分布(图片由作者提供)
这就是协变量转移的问题——尽管新数据仍然符合相同的目标函数,但模型输入的数据分布与之前训练的数据分布非常不同。
为了让模型知道如何适应这些新数据,它必须重新学习一些目标输出函数。这会减慢训练过程。如果我们从一开始就为模型提供了覆盖所有值的代表性分布,它就能够更快地了解目标输出。
既然我们了解了什么是“协变量转移”,那么让我们看看它是如何影响网络训练的。
在训练过程中,网络的每一层都学习一个输出函数来适应它的输入。假设在一次迭代中,层“k”接收来自前一层的小批量激活。然后,它通过基于该输入更新其权重来调整其输出激活。
然而,在每次迭代中,前一层‘k-1’也在做同样的事情。它调整其输出激活,有效地改变其分布。
协变量移位如何影响训练(图片由作者提供)
这也是层“k”的输入。换句话说,该层接收的输入数据的分布与以前不同。它现在被迫学习适应这种新的输入。正如我们所看到的,每一层最终都试图从不断变化的输入中学习,因此需要更长的时间来收敛并减缓训练。
因此,提出的假设是批处理范数有助于稳定这些从一次迭代到下一次迭代的移位分布,从而加快训练。
理论 2 —损耗和梯度平滑
麻省理工学院的论文发表的结果挑战了解决协变量移位是 Batch Norm 性能的原因的说法,并提出了不同的解释。
在典型的神经网络中,“损失景观”不是平滑的凸面。它非常颠簸,有陡峭的悬崖和平坦的表面。这给梯度下降带来了挑战——因为它可能会在它认为有希望遵循的方向上突然遇到障碍。为了弥补这一点,学习率保持在较低水平,这样我们在任何方向都只能迈出小步。
一个神经网络损失景观 (来源 ,经郝莉许可)
如果你想了解更多这方面的内容,请参阅我的关于神经网络优化器的文章,这篇文章更详细地解释了这一点,以及不同的优化器算法是如何发展来应对这些挑战的。
本文提出批处理范数的作用是通过改变网络权值的分布来平滑损失景观。这意味着梯度下降可以自信地朝着一个方向迈出一步,因为它知道在这个过程中不会发现突然的中断。因此,它可以通过使用更大的学习速率来迈出更大的步伐。
为了研究这一理论,本文进行了一项实验,以分析一个模型在训练过程中的损失景观。我们将尝试用一个简单的例子来形象化这一点。
假设我们有一个简单的网络,有两个权重参数(w1 和 w2)。这些权重的值可以显示在 2D 表面上,每个权重有一个轴。权重值的每个组合对应于这个 2D 平面上的一个点。
随着训练过程中重量的变化,我们移动到这个表面上的另一个点。因此,可以在训练迭代中绘制权重的轨迹。
请注意,下图仅显示了每个点的重量值。为了形象化这种损失,想象一个 3D 表面,第三个轴代表从页面出来的损失。如果我们对所有不同重量组合的损失进行测量和绘图,损失曲线的形状称为损失图。
(图片作者)
实验的目标是通过测量如果我们继续向同一方向移动,在不同点的损失和梯度看起来像什么,来检查损失情况。他们在有和没有批量标准的情况下进行测量,看看批量标准有什么影响。
假设在训练期间的某次迭代‘t’时,它在点 P(t)。它评估 Pt 处的损耗和梯度。然后,从该点开始,以一定的学习速率向前迈出一步,到达下一个点 P(t+1)。然后它倒回 P(t ),并以更高的学习速率重复该步骤。
换句话说,它通过沿着梯度方向采取三种不同大小的步骤(蓝色、绿色和粉色箭头),使用三种不同的学习速率,尝试了三种不同的替代方案。这给我们带来了 P(t+1)的三个不同的下一点。然后,在每一个 P(t+1)点,它测量新的损失和梯度。
在此之后,对同一网络重复所有三个步骤,但包括批量标准。
(图片作者)
现在,我们可以绘制出 P(t+1)点(蓝色、绿色和粉色)在单个方向上的损耗。起伏的红色曲线表示没有批次标准的损失,平滑下降的黄色曲线表示有批次标准的损失。
批量亏损平稳下降(图片由作者提供)
类似地,我们可以画出这些点上梯度的大小和方向。红色箭头显示梯度在大小和方向上剧烈波动,没有批次标准。黄色箭头显示梯度的大小和方向保持稳定,具有批次标准。
批量规范的渐变更平滑(图片由作者提供)
这个实验告诉我们,批量范数显著地平滑了损失情况。这对我们的训练有什么帮助?
理想的情况是,在下一个点 P(t+1),梯度也位于相同的方向。这意味着我们可以继续朝着同一个方向前进。这样可以让训练顺利进行,快速找到最小值。
另一方面,如果 P(t+1)处的最佳梯度方向将我们带向不同的方向,我们将会徒劳地沿着之字形路线前进。这将需要更多的训练迭代来收敛。
虽然这篇论文的发现到目前为止还没有受到质疑,但不清楚它们是否已经被完全接受为结束这场辩论的决定性证据。
不管哪个理论是正确的,我们可以肯定的是批处理规范提供了几个优点。
批量定额的优点
Batch Norm 提供的巨大好处是让模型更快收敛,加快训练速度。这使得训练对如何初始化权重和超参数的精确调整不太敏感。
批量定额让你使用更高的学习率。如果没有批处理规范,学习率必须保持较小,以防止大的异常梯度影响梯度下降。批处理规范有助于减少这些异常值的影响。
批次范数还降低了梯度对初始权重值的依赖性。由于权重是随机初始化的,因此在训练的早期阶段,异常权重值会扭曲梯度。因此,网络收敛需要更长的时间。批处理规范有助于抑制这些异常值的影响。
批量定额什么时候不适用?
批量定额不适用于较小的批量。这导致每个小批量的平均值和方差中有太多的噪声。
批量定额不适用于循环网络。每个时间步长之后的激活具有不同的分布,使得对其应用批量定额不切实际。
结论
即使我们不确定正确的解释,探索这些不同的理论也是令人着迷的,因为它让我们对神经网络的内部工作有了一些了解。
无论如何,批处理规范是我们在设计架构时绝对应该考虑的一层。
最后,如果你喜欢这篇文章,你可能也会喜欢我关于变形金刚、音频深度学习和地理定位机器学习的其他系列。
让我们继续学习吧!