想象一下预测骰子掷出 4 的结果(这是骰子的单数形式)。用纯粹的概率思维方法来解决这个问题,一个人简单地说骰子有六个面。我们假设每个面都是等可能的,所以掷出 4 的概率是 1/6,或 16.666%。
然而,一个狂热的统计学家可能会说:“不!我们需要掷骰子来获取数据。如果我们能掷出 30 次或更多次,而且我们掷得越多,就越好,那么我们才能有数据来确定掷出 4 的概率。” 如果我们假设骰子是公平的,这种方法可能看起来很愚蠢,但如果不是呢?如果是这样,收集数据是发现掷出 4 的概率的唯一方法。我们将在第三章中讨论假设检验。
假设你有一枚公平的硬币和一枚公平的六面骰子。你想找出硬币翻转为正面和骰子掷出 6 的概率。这是两个不同事件的两个单独概率,但我们想找出这两个事件同时发生的概率。这被称为联合概率 。
这很容易理解,但为什么会这样呢?许多概率规则可以通过生成所有可能的事件组合来发现,这源自离散数学中的排列组合知识领域。对于这种情况,生成硬币和骰子之间的每种可能结果,将正面(H)和反面(T)与数字 1 到 6 配对。请注意,我在我们得到正面和一个 6 的结果周围放了星号“*”:
抛硬币和掷骰子时有 12 种可能的结果。我们感兴趣的只有一个,即“H6”,即得到一个正面和一个 6。因为只有一个结果符合我们的条件,而有 12 种可能的结果,所以得到一个正面和一个 6 的概率是 1/12。
非互斥事件又是什么呢,即可以同时发生的事件?让我们回到抛硬币和掷骰子的例子。得到正面或 6 的概率是多少?在你尝试将这些概率相加之前,让我们再次生成所有可能的结果,并突出显示我们感兴趣的结果:
为什么会这样?再次研究硬币翻转和骰子结果的组合,看看是否能找到一些可疑之处。注意当我们将概率相加时,我们在“H6”和“T6”中都重复计算了得到 6 的概率!如果这不清楚,请尝试找出得到正面或掷骰子得到 1 至 5 的概率:
嗯……稍微研究一下这些数字,问问咖啡是否真的是问题所在。再次注意,任何时候只有 0.5%的人口患有癌症。然而,65%的人口定期喝咖啡。如果咖啡会导致癌症,我们难道不应该有比 0.5%更高的癌症数字吗?难道不应该接近 65%吗?
这就是比例数字的狡猾之处。它们在没有任何给定上下文的情况下可能看起来很重要,而媒体头条肯定可以利用这一点来获取点击量:“新研究揭示 85%的癌症患者喝咖啡”可能会这样写。当然,这很荒谬,因为我们将一个常见属性(喝咖啡)与一个不常见属性(患癌症)联系起来。
人们很容易被条件概率搞混,因为条件的方向很重要,而且两个条件被混淆为相等。"在你是咖啡饮用者的情况下患癌症的概率"不同于"在你患癌症的情况下是咖啡饮用者的概率"。简单来说:很少有咖啡饮用者患癌症,但很多癌症患者喝咖啡。
如果你想在 Python 中计算这个问题,请查看示例 2-1。
示例 2-1. 在 Python 中使用贝叶斯定理
p_coffee_drinker = .65
p_cancer = .005
p_coffee_drinker_given_cancer = .85
p_cancer_given_coffee_drinker = p_coffee_drinker_given_cancer *
p_cancer / p_coffee_drinker
print (p_cancer_given_coffee_drinker)
因此,某人在是咖啡饮用者的情况下患癌症的概率仅为 0.65%!这个数字与某人在患癌症的情况下是咖啡饮用者的概率(85%)非常不同。现在你明白为什么条件的方向很重要了吗?贝叶斯定理就是为了这个原因而有用。它还可以用于将多个条件概率链接在一起,根据新信息不断更新我们的信念。
如果你想更深入地探讨贝叶斯定理背后的直觉,请参考附录 A。现在只需知道它帮助我们翻转条件概率。接下来让我们讨论条件概率如何与联合和并集操作相互作用。
朴素贝叶斯
贝叶斯定理在一种常见的机器学习算法——朴素贝叶斯中扮演着核心角色。乔尔·格鲁斯在他的书《从零开始的数据科学》(O'Reilly)中对此进行了介绍。
联合和条件概率
让我们重新审视联合概率以及它们与条件概率的相互作用。我想找出某人是咖啡饮用者且患有癌症的概率。我应该将P ( 咖啡 ) 和P ( 癌症 ) 相乘吗?还是应该使用P ( 咖啡|癌症 ) 代替P ( 咖啡 ) ?我应该使用哪一个?
选项 1: P ( 咖啡 ) × P ( 癌症 ) = .65 × .005 = .00325 选项 2: P ( 咖啡|癌症 ) × P ( 癌症 ) = .85 × .005 = .00425
如果我们已经确定我们的概率仅适用于患癌症的人群,那么使用P ( Coffee|Cancer ) 而不是P ( Coffee ) 是不是更有意义?前者更具体,适用于已经建立的条件。因此,我们应该使用P ( Coffee|Cancer ) ,因为P ( Cancer ) 已经是我们联合概率的一部分。这意味着某人患癌症并且是咖啡饮用者的概率为 0.425%:
P ( Coffee and Cancer ) = P ( Coffee|Cancer ) × P ( Cancer ) = .85 × .005 = .00425
这种联合概率也适用于另一个方向。我可以通过将P ( Cancer|Coffee ) 和P ( Coffee ) 相乘来找到某人是咖啡饮用者且患癌症的概率。正如你所看到的,我得到了相同的答案:
P ( Cancer|Coffee ) × P ( Coffee ) = .0065 × .65 = .00425
如果我们没有任何条件概率可用,那么我们能做的最好就是将P ( Coffee Drinker ) 和P ( Cancer ) 相乘,如下所示:
P ( Coffee Drinker ) × P ( Cancer ) = .65 × .005 = .00325
现在想想这个:如果事件 A 对事件 B 没有影响,那么这对条件概率P (B |A )意味着什么?这意味着P (B |A ) = P (B ),也就是说事件 A 的发生不会影响事件 B 发生的可能性。因此,我们可以更新我们的联合概率公式,无论这两个事件是否相关,都可以是:
P ( A AND B ) = P ( B ) × P ( A | B )
最后让我们谈谈并集和条件概率。如果我想计算发生事件 A 或事件 B 的概率,但事件 A 可能会影响事件 B 的概率,我们更新我们的求和规则如下:
P ( A OR B ) = P ( A ) + P ( B ) - P ( A | B ) × P ( B )
作为提醒,这也适用于互斥事件。如果事件 A 和事件 B 不能同时发生,则求和规则P (A |B ) × P (B )将得到 0。
二项分布
在本章的其余部分,我们将学习两种概率分布:二项分布和贝塔分布。虽然我们在本书的其余部分不会使用它们,但它们本身是有用的工具,对于理解在一定数量的试验中事件发生的方式至关重要。它们也将是理解我们在第三章中大量使用的概率分布的好起点。让我们探讨一个可能在现实场景中发生的用例。
假设你正在研发一种新的涡轮喷气发动机,并进行了 10 次测试。结果显示有 8 次成功和 2 次失败:
你原本希望获得 90%的成功率,但根据这些数据,你得出结论你的测试只有 80%的成功率。每次测试都耗时且昂贵,因此你决定是时候回到起点重新设计了。
然而,你的一位工程师坚持认为应该进行更多的测试。“唯一确定的方法是进行更多的测试,”她辩称。“如果更多的测试产生了 90%或更高的成功率呢?毕竟,如果你抛硬币 10 次得到 8 次正面,这并不意味着硬币固定在 80%。”
你稍微考虑了工程师的论点,并意识到她有道理。即使是一个公平的硬币翻转也不会总是有相同的结果,尤其是只有 10 次翻转。你最有可能得到五次正面,但也可能得到三、四、六或七次正面。你甚至可能得到 10 次正面,尽管这极不可能。那么,如何确定在底层概率为 90%的情况下 80%成功的可能性?
一个在这里可能相关的工具是二项分布 ,它衡量了在n 次试验中,k 次成功可能发生的概率,给定了p 的概率。
从视觉上看,二项分布看起来像图 2-1。
在这里,我们看到了在 10 次试验中每个柱子代表的k 次成功的概率。这个二项分布假设了一个 90%的概率p ,意味着成功发生的概率为 0.90(或 90%)。如果这是真的,那么我们在 10 次试验中获得 8 次成功的概率为 0.1937。在 10 次试验中获得 1 次成功的概率极低,为 0.000000008999,这就是为什么柱子几乎看不见。
我们还可以通过将八次或更少成功的柱子相加来计算八次或更少成功的概率。这将给我们八次或更少成功的概率为 0.2639。
图 2-1. 一个二项分布
那么我们如何实现二项分布呢?我们可以相对容易地从头开始实现(如在附录 A 中分享的),或者我们可以使用像 SciPy 这样的库。示例 2-2 展示了我们如何使用 SciPy 的binom.pmf()
函数(PMF 代表“概率质量函数”)来打印从 0 到 10 次成功的二项分布的所有 11 个概率。
示例 2-2. 使用 SciPy 进行二项分布
from scipy.stats import binom
n = 10
p = 0.9
for k in range (n + 1 ):
probability = binom.pmf(k, n, p)
print ("{0} - {1}" .format (k, probability))
正如你所看到的,我们提供n 作为试验次数,p 作为每次试验成功的概率,k 作为我们想要查找概率的成功次数。我们迭代每个成功次数x 与我们将看到该数量成功的相应概率。正如我们在输出中看到的,最可能的成功次数是九。
但是,如果我们将八个或更少成功的概率相加,我们会得到 0.2639。这意味着即使基础成功率为 90%,我们也有 26.39%的机会看到八个或更少的成功。所以也许工程师是对的:26.39%的机会并非没有,而且确实可能。
然而,在我们的模型中我们确实做了一个假设,接下来我们将讨论与 beta 分布相关的内容。
从头开始的二项分布
转到附录 A 了解如何在没有 scikit-learn 的情况下从头开始构建二项分布。
Beta 分布
我在使用二项分布的引擎测试模型时做了什么假设?我是否假设了一个参数为真,然后围绕它构建了整个模型?仔细思考并继续阅读。
我的二项分布可能存在问题的地方在于我假设 了基础成功率为 90%。这并不是说我的模型毫无价值。我只是展示了如果基础成功率为 90%,那么在 10 次试验中看到 8 个或更少的成功的概率为 26.39%。所以工程师当然没有错,可能存在一个基础成功率为 90%的情况。
但让我们反过来思考这个问题:如果除了 90%之外还有其他基础成功率可以产生 8/10 的成功呢?我们能否用基础成功率 80%?70%?30%?来看到 8/10 的成功?当我们固定 8/10 的成功时,我们能探索概率的概率吗?
与其创建无数个二项分布来回答这个问题,我们可以使用一个工具。Beta 分布 允许我们看到在给定alpha 成功和beta 失败的情况下事件发生的不同基础概率的可能性。
给出八次成功和两次失败的 beta 分布图表如图 2-2 所示。
图 2-2. Beta 分布
Desmos 上的 Beta 分布
如果你想与 beta 分布互动,可以在这里 找到 Desmos 图表。
请注意,x 轴表示从 0.0 到 1.0(0%到 100%)的所有基础成功率,y 轴表示在八次成功和两次失败的情况下给定该概率的可能性。换句话说,beta 分布允许我们看到在 8/10 成功的情况下概率的概率。把它看作是一种元概率,所以花点时间理解这个概念!
还要注意,贝塔分布是一个连续函数,这意味着它形成了一个十进制值的连续曲线(与二项分布中整洁且离散的整数相对)。这将使得贝塔分布的数学运算稍微困难,因为 y 轴上的给定密度值不是概率。我们通过曲线下的面积来找到概率。
贝塔分布是一种概率分布 ,这意味着整个曲线下的面积为 1.0,或者 100%。要找到概率,我们需要找到一个范围内的面积。例如,如果我们想评估 8/10 次成功将产生 90%或更高成功率的概率,我们需要找到 0.9 和 1.0 之间的面积,即 0.225,如图 2-3 中所阴影部分所示。
图 2-3. 90%到 100%之间的面积,为 22.5%
就像我们使用二项分布一样,我们可以使用 SciPy 来实现贝塔分布。每个连续概率分布都有一个累积密度函数(CDF) ,用于计算给定 x 值之前的面积。假设我想计算到 90%(0.0 到 0.90)的面积,如图 2-4 中所阴影部分所示。
图 2-4. 计算到 90%(0.0 到 0.90)的面积
使用 SciPy 的beta.cdf()
函数非常简单,我需要提供的唯一参数是 x 值,成功次数a 和失败次数b ,如示例 2-3 所示。
示例 2-3. 使用 SciPy 进行贝塔分布
from scipy.stats import beta
a = 8
b = 2
p = beta.cdf(.90 , a, b)
print (p)
根据我们的计算,底层成功概率为 90%或更低的概率为 77.48%。
我们如何计算成功概率为 90%或更高的概率,如图 2-5 中所阴影部分所示?
图 2-5. 成功概率为 90%或更高的情况
我们的累积密度函数只计算边界左侧的面积,而不是右侧。想想我们的概率规则,对于概率分布,曲线下的总面积为 1.0。如果我们想找到一个事件的相反概率(大于 0.90 而不是小于 0.90),只需从 1.0 中减去小于 0.90 的概率,剩下的概率将捕获大于 0.90 的情况。图 2-6 说明了我们如何进行这种减法。
图 2-6. 找到成功概率大于 90%的概率
示例 2-4 展示了我们如何在 Python 中计算这种减法操作。
示例 2-4. 在贝塔分布中进行减法以获得正确的面积
from scipy.stats import beta
a = 8
b = 2
p = 1.0 - beta.cdf(.90 , a, b)
print (p)
这意味着在 8/10 次成功的引擎测试中,成功率为 90% 或更高的概率只有 22.5%。但成功率低于 90% 的概率约为 77.5%。我们在这里的胜算不高,但如果我们感觉幸运,我们可以通过更多的测试来赌那 22.5% 的机会。如果我们的首席财务官为 26 次额外测试提供资金,结果是 30 次成功和 6 次失败,我们的贝塔分布将如 图 2-7 所示。
图 2-7. 30 次成功和 6 次失败后的贝塔分布
注意我们的分布变得更窄,因此更有信心地认为成功率在一个更小的范围内。不幸的是,我们达到 90% 成功率最低的概率已经减少,从 22.5% 降至 13.16%,如 例子 2-5 所示。
例子 2-5. 具有更多试验的贝塔分布
from scipy.stats import beta
a = 30
b = 6
p = 1.0 - beta.cdf(.90 , a, b)
print (p)
此时,最好离开并停止做测试,除非你想继续赌博对抗那 13.16% 的机会,并希望峰值向右移动。
最后,我们如何计算中间的面积?如果我想找到成功率在 80% 和 90% 之间的概率,如 图 2-8 所示,该怎么办?
图 2-8. 成功率在 80% 和 90% 之间的概率
仔细考虑一下你可能如何处理这个问题。如果我们像在 图 2-9 中那样,从 .80 后面的面积中减去 .90 后面的面积会怎样?
图 2-9. 获取 .80 和 .90 之间的面积
这会给我们提供 .80 和 .90 之间的面积吗?是的,它会,并且会产生一个 .3386 或 33.86% 的概率。这是我们如何在 Python 中计算它的方法(例子 2-6)。
例子 2-6. 使用 SciPy 计算贝塔分布中间区域
from scipy.stats import beta
a = 8
b = 2
p = beta.cdf(.90 , a, b) - beta.cdf(.80 , a, b)
print (p)
贝塔分布是一种迷人的工具,用于根据有限的观察来衡量事件发生与不发生的概率。它使我们能够推理关于概率的概率,并且我们可以在获得新数据时更新它。我们也可以将其用于假设检验,但我们将更加强调使用正态分布和 T 分布来进行这种目的,如 第三章 中所述。
从头开始的贝塔分布
要了解如何从头开始实现贝塔分布,请参考 附录 A。
结论
在本章中,我们涵盖了很多内容!我们不仅讨论了概率的基础知识、逻辑运算符和贝叶斯定理,还介绍了概率分布,包括二项式分布和贝塔分布。在下一章中,我们将讨论更著名的分布之一,正态分布,以及它与假设检验的关系。
如果你想了解更多关于贝叶斯概率和统计的知识,一本很棒的书是Bayesian Statistics the Fun Way ,作者是 Will Kurt(No Starch Press)。还可以在 O’Reilly 平台上找到互动的Katacoda 场景 。
练习
今天有 30%的降雨概率,你的雨伞订单准时到达的概率为 40%。你渴望今天在雨中散步,但没有雨伞你无法做到!
下雨的概率和你的雨伞到达的概率是多少?
今天有 30%的降雨概率,你的雨伞订单准时到达的概率为 40%。
只有在不下雨或者你的雨伞到达时,你才能出门办事。
不下雨或者你的雨伞到达的概率是多少?
今天有 30%的降雨概率,你的雨伞订单准时到达的概率为 40%。
然而,你发现如果下雨,你的雨伞准时到达的概率只有 20%。
下雨的概率和你的雨伞准时到达的概率是多少?
你从拉斯维加斯飞往达拉斯的航班上有 137 名乘客预订了座位。然而,这是拉斯维加斯的一个星期天早上,你估计每位乘客有 40%的可能性不会出现。
你正在努力计算要超售多少座位,以免飞机空飞。
至少有 50 名乘客不会出现的概率有多大?
你抛了一枚硬币 19 次,其中 15 次是正面,4 次是反面。
你认为这枚硬币有可能是公平的吗?为什么?
答案在附录 B 中。
第三章:描述性和推断性统计
统计学 是收集和分析数据以发现有用发现或预测导致这些发现发生的原因的实践。概率在统计学中经常起着重要作用,因为我们使用数据来估计事件发生的可能性。
统计学可能并不总是受到赞誉,但它是许多数据驱动创新的核心。机器学习本身就是一种统计工具,寻找可能的假设来相关不同变量之间的关系。然而,即使对于专业统计学家来说,统计学也存在许多盲点。我们很容易陷入数据所说的内容,而忘记了要问数据来自何处。随着大数据、数据挖掘和机器学习加速推动统计算法的自动化,这些问题变得更加重要。因此,拥有扎实的统计学和假设检验基础非常重要,这样你就不会把这些自动化处理当作黑匣子。
在本节中,我们将介绍统计学和假设检验的基础知识。从描述性统计开始,我们将学习总结数据的常见方法。之后,我们将进入推断统计,试图根据样本揭示总体属性。
什么是数据?
定义“数据”可能看起来有些奇怪,因为我们都使用并认为理所当然。但我认为有必要这样做。如果你问任何人数据是什么,他们可能会回答类似于“你知道的...数据!就是...你知道的...信息!”而不会深入探讨。现在它似乎被宣传为至关重要的事物。不仅是真相的来源...还有智慧!这是人工智能的燃料,人们相信你拥有的数据越多,你就拥有的真相就越多。因此,你永远不可能拥有足够的数据。它将揭示重新定义你的业务策略所需的秘密,甚至可能创造人工通用智能。但让我提供一个关于数据是什么的实用观点。数据本身并不重要。数据的分析(以及它是如何产生的)是所有这些创新和解决方案的驱动力。
想象一下,如果你拿到一张一个家庭的照片。你能根据这张照片揭示这个家庭的故事吗?如果你有 20 张照片呢?200 张照片?2,000 张照片?你需要多少张照片才能了解他们的故事?你需要他们在不同情况下的照片吗?一个人和一起的照片?和亲戚朋友在一起的照片?在家里和工作中的照片?
数据 就像照片一样;它提供了故事的快照。连续的现实和背景并没有完全捕捉到,也没有驱动这个故事的无限数量的变量。正如我们将讨论的,数据可能存在偏见。它可能存在缺口,缺少相关变量。理想情况下,我们希望有无限量的数据捕捉无限数量的变量,有如此之多的细节,我们几乎可以重新创造现实并构建替代现实!但这可能吗?目前,不可能。即使将全球最强大的超级计算机组合在一起,也无法接近以数据形式捕捉整个世界的全部内容。
因此,我们必须缩小范围,使我们的目标变得可行。父亲打高尔夫球的几张战略照片可以很容易地告诉我们他是否擅长高尔夫。但是仅凭照片来解读他的整个人生故事?那可能是不可能的。有很多东西是无法在快照中捕捉到的。这些实际问题也应该在处理数据项目时应用,因为数据实际上只是捕捉特定时间的快照,只捕捉到了它所针对的内容(就像相机一样)。我们需要保持我们的目标集中,这有助于收集相关和完整的数据。如果我们的目标过于宽泛和开放,我们可能会遇到虚假发现和不完整数据集的问题。这种实践,被称为数据挖掘 ,有其时机和地点,但必须小心进行。我们将在本章末重新讨论这个问题。
即使目标明确,我们仍然可能在数据方面遇到问题。让我们回到确定几张战略照片是否能告诉我们父亲是否擅长高尔夫的问题。也许如果你有一张他挥杆中的照片,你就能看出他的动作是否正确。或者也许如果你看到他在一个洞位上欢呼和击掌,你可以推断他得了一个好成绩。也许你只需拍一张他的记分卡的照片!但重要的是要注意所有这些情况都可能是伪造的或脱离了上下文。也许他在为别人欢呼,或者记分卡不是他的,甚至是伪造的。就像这些照片一样,数据并不能捕捉到背景或解释。这是一个非常重要的观点,因为数据提供线索,而不是真相。这些线索可以引导我们找到真相,也可能误导我们得出错误的结论。
这就是为什么对数据来源感到好奇是如此重要的技能。询问数据是如何创建的,由谁创建的,以及数据未捕捉到什么。很容易陷入数据所说的内容而忘记询问数据来自何处。更糟糕的是,有广泛的观点认为可以将数据填入机器学习算法中,并期望计算机解决所有问题。但正如谚语所说,“垃圾进,垃圾出”。难怪根据 VentureBeat 的数据,只有13%的机器学习项目成功 。成功的机器学习项目对数据进行了思考和分析,以及产生数据的过程。
描述性统计与推断性统计
当你听到“统计学”这个词时,你会想到什么?是计算平均值、中位数、众数、图表、钟形曲线和其他用于描述数据的工具吗?这是统计学最常被理解的部分,称为描述性统计 ,我们用它来总结数据。毕竟,浏览百万条数据记录还是对其进行总结更有意义?我们将首先涵盖统计学的这一领域。
推断性统计 试图揭示关于更大总体的属性,通常基于样本。它经常被误解,比描述性统计更难理解。通常我们对研究一个太大以至无法观察的群体感兴趣(例如,北美地区青少年的平均身高),我们必须仅使用该群体的少数成员来推断关于他们的结论。正如你所猜测的那样,这并不容易做到。毕竟,我们试图用一个可能不具代表性的样本来代表一个总体。我们将在探讨过程中探讨这些警告。
总体、样本和偏差
在我们深入研究描述性和推断性统计之前,将一些定义和与之相关的具体示例联系起来可能是一个好主意。
总体 是我们想要研究的特定感兴趣的群体,比如“北美地区所有 65 岁以上的老年人”,“苏格兰所有金毛猎犬”,或者“洛斯阿尔托斯高中目前的高中二年级学生”。请注意我们对定义总体的边界。有些边界很宽泛,涵盖了广阔地理区域或年龄组的大群体。其他则非常具体和小,比如洛斯阿尔托斯高中的高中二年级学生。如何确定总体的定义取决于你对研究感兴趣的内容。
样本 是总体的一个理想随机和无偏子集,我们用它来推断总体的属性。我们经常不得不研究样本,因为调查整个总体并不总是可能的。当然,如果人口小且易接触,那么获取一些人口是更容易的。但是测量北美地区所有 65 岁以上的老年人?那不太可能实际可行!
总体可以是抽象的!
需要注意的是,人口可以是理论的,而不是实际可触及的。在这些情况下,我们的人口更像是从抽象事物中抽取的样本。这里是我最喜欢的例子:我们对一个机场在下午 2 点到 3 点之间起飞的航班感兴趣,但在那个时间段内的航班数量不足以可靠地预测这些航班的延误情况。因此,我们可能将这个人口视为从所有在下午 2 点到 3 点之间起飞的理论航班中抽取的样本。
这类问题是为什么许多研究人员借助模拟生成数据的原因。模拟可能是有用的,但很少是准确的,因为模拟只捕捉了有限的变量,并且内置了假设。
如果我们要根据样本推断人口的属性,那么样本尽可能随机是很重要的,这样我们才不会扭曲我们的结论。这里举个例子。假设我是亚利桑那州立大学的一名大学生。我想找出美国大学生每周观看电视的平均小时数。我走出宿舍,开始随机询问路过的学生,几个小时后完成了数据收集。问题在哪里?
问题在于我们的学生样本可能存在偏见 ,这意味着它通过过度代表某一群体而牺牲其他群体来扭曲我们的发现。我的研究将人口定义为“美国的大学生”,而不是“亚利桑那州立大学的大学生”。我只是在一个特定大学对学生进行调查,代表整个美国的所有大学生!这真的公平吗?
不太可能全国各地的大学都具有相同的学生属性。如果亚利桑那州立大学的学生比其他大学的学生观看电视时间更长怎么办?使用他们来代表整个国家难道不会扭曲结果吗?也许这是可能的,因为在亚利桑那州的坦佩市通常太热了,所以看电视是一种常见的消遣(据说,我会知道;我在凤凰城住了很多年)。其他气候较温和的地方的大学生可能会进行更多的户外活动,看电视时间更少。
这只是一个可能的变量,说明用一个大学的学生样本来代表整个美国的大学生是一个不好的主意。理想情况下,我应该随机调查全国各地不同大学的大学生。这样我就有了更具代表性的样本。
然而,偏见并不总是地理性的。假设我竭尽全力在全美各地调查学生。我策划了一个社交媒体活动,在 Twitter 和 Facebook 上让各大学分享调查,这样他们的学生就会看到并填写。我收到了数百份关于全国学生电视习惯的回复,觉得我已经征服了偏见的野兽...或者我真的做到了吗?
如果那些足够在社交媒体上看到投票的学生也更有可能看更多电视呢?如果他们在社交媒体上花很多时间,他们可能不介意娱乐性的屏幕时间。很容易想象他们已经准备好在另一个标签上流媒体 Netflix 和 Hulu!这种特定类型的偏见,即特定群体更有可能加入样本的偏见,被称为自我选择偏差 。
糟糕!你就是赢不了,是吗?如果你足够长时间地考虑,数据偏差似乎是不可避免的!而且通常确实如此。许多混杂变量 ,或者我们没有考虑到的因素,都会影响我们的研究。数据偏差问题昂贵且难以克服,而机器学习尤其容易受到影响。
克服这个问题的方法是真正随机地从整个人口中选择学生,他们不能自愿地加入或退出样本。这是减轻偏见的最有效方法,但正如你所想象的那样,这需要大量协调的资源。
好了,关于人口、样本和偏差的讨论就到此为止。让我们继续进行一些数学和描述性统计。只要记住,数学和计算机不会意识到数据中的偏差。这取决于你作为一名优秀的数据科学专业人员来检测!始终要问关于数据获取方式的问题,然后仔细审查该过程可能如何使数据产生偏差。
机器学习中的样本和偏差
这些与抽样和偏差有关的问题也延伸到机器学习领域。无论是线性回归、逻辑回归还是神经网络,都会使用数据样本来推断预测。如果数据存在偏差,那么它将引导机器学习算法做出有偏见的结论。
这方面有许多记录的案例。刑事司法一直是机器学习的一个棘手应用,因为它一再显示出在每个意义上都存在偏见,由于数据集中存在少数族群,导致对少数族群进行歧视。2017 年,沃尔沃测试了训练过捕捉鹿、麋鹿和驯鹿数据集的自动驾驶汽车。然而,它在澳大利亚没有驾驶数据,因此无法识别袋鼠,更不用说理解它们的跳跃动作了!这两个都是有偏见数据的例子。
描述性统计
描述性统计是大多数人熟悉的领域。我们将介绍一些基础知识,如均值、中位数和众数,然后是方差、标准差和正态分布。
均值和加权均值
均值 是一组值的平均值。这个操作很简单:将值相加,然后除以值的数量。均值很有用,因为它显示了观察到的一组值的“重心”在哪里。
均值的计算方式对于人口和样本是相同的。示例 3-1 展示了八个值的样本以及如何在 Python 中计算它们的均值。
示例 3-1. 在 Python 中计算均值
sample = [1 , 3 , 2 , 5 , 7 , 0 , 2 , 3 ]
mean = sum (sample) / len (sample)
print (mean)
正如你所看到的,我们对八个人关于他们拥有的宠物数量进行了调查。样本的总和为 23,样本中的项目数为 8,因此这给我们一个均值为 2.875,因为 23/8 = 2.875。
你将看到两个版本的均值:样本均值x ¯ 和总体均值μ 如此表达:
x ¯ = x 1 + x 2 + x 3 + . . . + x n n = ∑ x i n μ = x 1 + x 2 + x 3 + . . . + x n N = ∑ x i N
回想一下求和符号∑ 表示将所有项目相加。n 和N 分别代表样本和总体大小,但在数学上它们表示相同的东西:项目的数量。对于称为样本均值x ¯ (“x-bar”)和总体均值μ (“mu”)的命名也是如此。无论是x ¯ 还是μ 都是相同的计算,只是根据我们处理的是样本还是总体而有不同的名称。
均值可能对你来说很熟悉,但关于均值有一些不太为人知的东西:均值实际上是一种称为加权均值 的加权平均值。我们通常使用的均值给予每个值相同的重要性。但我们可以操纵均值,给每个项目赋予不同的权重:
weighted mean = ( x 1 · w 1 ) + ( x 2 · w 2 ) + ( x 3 · w 3 ) + . . . ( x n · w n ) w 1 + w 2 + w 3 + . . . + w n
当我们希望某些值对均值的贡献大于其他值时,这将会很有帮助。一个常见的例子是对学术考试进行加权以得出最终成绩。如果你有三次考试和一次期末考试,我们给予每次考试 20%的权重,期末考试 40%的权重,我们如何表达它在示例 3-2 中。
示例 3-2. 在 Python 中计算加权均值
sample = [90 , 80 , 63 , 87 ]
weights = [.20 , .20 , .20 , .40 ]
weighted_mean = sum (s * w for s,w in zip (sample, weights)) / sum (weights)
print (weighted_mean)
我们通过相应的乘法对每个考试分数进行加权,而不是通过值计数进行除法,而是通过权重总和进行除法。权重不必是百分比,因为用于权重的任何数字最终都将被比例化。在示例 3-3 中,我们对每个考试赋予“1”的权重,但对最终考试赋予“2”的权重,使其比考试的权重大一倍。我们仍然会得到相同的答案 81.4,因为这些值仍然会被比例化。
示例 3-3. 在 Python 中计算加权平均值
sample = [90 , 80 , 63 , 87 ]
weights = [1.0 , 1.0 , 1.0 , 2.0 ]
weighted_mean = sum (s * w for s,w in zip (sample, weights)) / sum (weights)
print (weighted_mean)
中位数
中位数 是一组有序值中间最靠近的值。您按顺序排列值,中位数将是中间的值。如果您有偶数个值,您将平均两个中间值。我们可以看到在示例 3-4 中,我们样本中拥有的宠物数量的中位数为 7:
0 , 1 , 5 , *7 *, 9 , 10 , 14
示例 3-4. 在 Python 中计算中位数
sample = [0 , 1 , 5 , 7 , 9 , 10 , 14 ]
def median (values ):
ordered = sorted (values)
print (ordered)
n = len (ordered)
mid = int (n / 2 ) - 1 if n % 2 == 0 else int (n/2 )
if n % 2 == 0 :
return (ordered[mid] + ordered[mid+1 ]) / 2.0
else :
return ordered[mid]
print (median(sample))
当数据被异常值 或与其他值相比极端大和小的值所扭曲时,中位数可以成为均值的有用替代。这里有一个有趣的轶事来理解为什么。1986 年,北卡罗来纳大学教堂山分校地理学毕业生的年起薪平均值为250 , 000 。 其 他 大 学 平 均 为 250 , 000 。 其 他 大 学 平 均 为 22,000。哇,UNC-CH 一定有一个了不起的地理学项目!
但实际上,为什么北卡罗来纳大学的地理学项目如此赚钱呢?嗯...迈克尔·乔丹是他们的毕业生之一。确实,有史以来最著名的 NBA 球员之一,确实是从北卡罗来纳大学获得地理学学位的。然而,他开始他的职业生涯是打篮球,而不是学习地图。显然,这是一个造成了巨大异常值的混杂变量,它严重扭曲了收入平均值。
这就是为什么在受异常值影响较大的情况下(例如与收入相关的数据)中,中位数可能比均值更可取。它对异常值不太敏感,并严格根据它们的相对顺序将数据划分到中间,而不是准确地落在数字线上。当您的中位数与均值非常不同时,这意味着您的数据集具有异常值。
众数
众数 是出现频率最高的一组值。当您的数据重复时,想要找出哪些值出现最频繁时,它就变得很有用。
当没有任何值出现超过一次时,就没有众数。当两个值出现的频率相等时,数据集被认为是双峰的 。在示例 3-5 中,我们计算了我们的宠物数据集的众数,果然我们看到这是双峰的,因为 2 和 3 都出现最多(并且相等)。
示例 3-5. 在 Python 中计算众数
from collections import defaultdict
sample = [1 , 3 , 2 , 5 , 7 , 0 , 2 , 3 ]
def mode (values ):
counts = defaultdict(lambda : 0 )
for s in values:
counts[s] += 1
max_count = max (counts.values())
modes = [v for v in set (values) if counts[v] == max_count]
return modes
print (mode(sample))
在实际应用中,除非您的数据重复,否则众数不经常使用。这通常在整数、类别和其他离散变量中遇到。
方差和标准差
当我们开始讨论方差和标准差时,这就变得有趣起来了。方差和标准差让人们感到困惑的一点是,对于样本和总体有一些计算差异。我们将尽力清楚地解释这些差异。
总体方差和标准差
在描述数据时,我们经常对测量平均值和每个数据点之间的差异感兴趣。这让我们对数据的“分布”有了一种感觉。
假设我对研究我的工作人员拥有的宠物数量感兴趣(请注意,我将这定义为我的总体,而不是样本)。我的工作人员有七个人。
我取得他们拥有的所有宠物数量的平均值,我得到 6.571。让我们从每个值中减去这个平均值。这将展示给我们每个值距离平均值有多远,如表 3-1 所示。
表 3-1. 我的员工拥有的宠物数量
Value
Mean
Difference
0
6.571
-6.571
1
6.571
-5.571
5
6.571
-1.571
7
6.571
0.429
9
6.571
2.429
10
6.571
3.429
14
6.571
7.429
让我们在数轴上用“X”表示平均值来可视化这一点,见图 3-1。
图 3-1. 可视化我们数据的分布,其中“X”是平均值
嗯...现在考虑为什么这些信息有用。这些差异让我们了解数据的分布情况以及值距离平均值有多远。有没有一种方法可以将这些差异合并成一个数字,快速描述数据的分布情况?
你可能会想要取这些差异的平均值,但当它们相加时,负数和正数会互相抵消。我们可以对绝对值求和(去掉负号并使所有值为正)。一个更好的方法是在求和之前对这些差异进行平方。这不仅消除了负值(因为平方一个负数会使其变为正数),而且放大了较大的差异,并且在数学上更容易处理(绝对值的导数不直观)。之后,对平方差异取平均值。这将给我们方差 ,一个衡量数据分布范围的指标。
这里是一个数学公式,展示如何计算方差:
population variance = ( x 1 - m e a n ) 2 + ( x 2 - m e a n ) 2 + . . . + ( x n - m e a n ) 2 N
更正式地,这是总体的方差:
σ 2 = ∑ ( x i - μ ) 2 N
在 Python 中计算我们宠物示例的总体方差在示例 3-6 中展示。
示例 3-6. 在 Python 中计算方差
data = [0 , 1 , 5 , 7 , 9 , 10 , 14 ]
def variance (values ):
mean = sum (values) / len (values)
_variance = sum ((v - mean) ** 2 for v in values) / len (values)
return _variance
print (variance(data))
因此,我的办公室员工拥有的宠物数量的方差为 21.387755。好的,但这到底意味着什么?可以合理地得出结论,更高的方差意味着更广泛的分布,但我们如何将其与我们的数据联系起来?这个数字比我们的任何观察结果都要大,因为我们进行了大量的平方和求和,将其放在完全不同的度量标准上。那么我们如何将其压缩回原来的尺度?
平方的反面是平方根,所以让我们取方差的平方根,得到标准差 。这是将方差缩放为以“宠物数量”表示的数字,这使得它更有意义:
σ = ∑ ( x i - μ ) 2 N
要在 Python 中实现,我们可以重用variance()
函数并对其结果进行sqrt()
。现在我们有了一个std_dev()
函数,如示例 3-7 所示。
示例 3-7. 在 Python 中计算标准差
from math import sqrt
data = [0 , 1 , 5 , 7 , 9 , 10 , 14 ]
def variance (values ):
mean = sum (values) / len (values)
_variance = sum ((v - mean) ** 2 for v in values) / len (values)
return _variance
def std_dev (values ):
return sqrt(variance(values))
print (std_dev(data))
运行示例 3-7 中的代码,你会看到我们的标准差约为 4.62 只宠物。因此,我们可以用我们开始的尺度来表示我们的扩散,这使得我们的方差更容易解释。我们将在第五章中看到标准差的一些重要应用。
为什么是平方?
关于方差,如果σ 2 中的指数让你感到困扰,那是因为它提示你对其进行平方根运算以获得标准差。这是一个小小的提醒,告诉你正在处理需要进行平方根运算的平方值。
样本方差和标准差
在前一节中,我们讨论了总体的方差和标准差。然而,当我们为样本计算这两个公式时,我们需要应用一个重要的调整:
s 2 = ∑ ( x i - x ¯ ) 2 n - 1 s = ∑ ( x i - x ¯ ) 2 n - 1
你注意到了区别吗?当我们对平方差值求平均时,我们除以n -1 而不是总项目数n 。为什么要这样做?我们这样做是为了减少样本中的任何偏差,不低估基于我们的样本的总体方差。通过在除数中计算少一个项目的值,我们增加了方差,因此捕捉到了样本中更大的不确定性。
如果我们的宠物数据是一个样本,而不是一个总体,我们应相应地进行调整。在示例 3-8 中,我修改了之前的variance()
和std_dev()
Python 代码,可选择提供一个参数is_sample
,如果为True
,则从方差的除数中减去 1。
示例 3-8. 计算样本的标准差
from math import sqrt
data = [0 , 1 , 5 , 7 , 9 , 10 , 14 ]
def variance (values, is_sample: bool = False ):
mean = sum (values) / len (values)
_variance = sum ((v - mean) ** 2 for v in values) /
(len (values) - (1 if is_sample else 0 ))
return _variance
def std_dev (values, is_sample: bool = False ):
return sqrt(variance(values, is_sample))
print ("VARIANCE = {}" .format (variance(data, is_sample=True )))
print ("STD DEV = {}" .format (std_dev(data, is_sample=True )))
请注意,在示例 3-8 中,我的方差和标准差与之前将它们视为总体而不是样本的示例相比有所增加。回想一下,在示例 3-7 中,将其视为总体时,标准差约为 4.62。但是在这里将其视为样本(通过从方差分母中减去 1),我们得到的值约为 4.99。这是正确的,因为样本可能存在偏差,不完全代表总体。因此,我们增加方差(从而增加标准差)以增加对值分布范围的估计。较大的方差/标准差显示出较大范围的不确定性。
就像均值(样本为 x ¯ ,总体为 μ ),你经常会看到一些符号表示方差和标准差。样本和均值的标准差分别由s 和σ 指定。这里再次是样本和总体标准差的公式:
s = ∑ ( x i - x ¯ ) 2 n - 1 σ = ∑ ( x i - μ ) 2 N
方差将是这两个公式的平方,撤销平方根。因此,样本和总体的方差分别为s 2 和σ 2 :
s 2 = ∑ ( x i - x ¯ ) 2 n - 1 σ 2 = ∑ ( x i - μ ) 2 N
再次,平方有助于暗示应该取平方根以获得标准差。
正态分布
在上一章中我们提到了概率分布,特别是二项分布和贝塔分布。然而,最著名的分布是正态分布。正态分布 ,也称为高斯分布 ,是一种对称的钟形分布,大部分质量集中在均值附近,其传播程度由标准差定义。两侧的“尾巴”随着远离均值而变得更细。
图 3-2 是金毛犬体重的正态分布。请注意,大部分质量集中在 64.43 磅的均值附近。
图 3-2. 一个正态分布
发现正态分布
正态分布在自然界、工程、科学和其他领域中经常出现。我们如何发现它呢?假设我们对 50 只成年金毛犬的体重进行抽样,并将它们绘制在数轴上,如图 3-3 所示。
图 3-3. 50 只金毛犬体重的样本
请注意,我们在中心附近有更多的值,但随着向左或向右移动,我们看到的值越来越少。根据我们的样本,看起来我们很不可能看到体重为 57 或 71 磅的金毛犬。但体重为 64 或 65 磅?是的,这显然很可能。
有没有更好的方法来可视化这种可能性,以查看我们更有可能从人群中抽样到哪些金毛犬的体重?我们可以尝试创建一个直方图 ,它根据相等长度的数值范围将值分组(或“箱”),然后使用柱状图显示每个范围内的值的数量。在图 3-4 中,我们创建了一个将值分组为 0.5 磅范围的直方图。
这个直方图并没有显示出数据的任何有意义的形状。原因是因为我们的箱太小了。我们没有足够大或无限的数据量来有意义地在每个箱中有足够的点。因此,我们必须使我们的箱更大。让我们使每个箱的长度为三磅,如图 3-5 所示。
图 3-4. 一张金毛犬体重的直方图
图 3-5. 一个更有意义的直方图
现在我们有了进展!正如你所看到的,如果我们将箱的大小调整得恰到好处(在本例中,每个箱的范围为三磅),我们开始在数据中看到一个有意义的钟形。这不是一个完美的钟形,因为我们的样本永远不会完全代表人群,但这很可能是我们的样本遵循正态分布的证据。如果我们使用适当的箱大小拟合一个直方图,并将其缩放为面积为 1.0(这是概率分布所需的),我们会看到一个粗略的钟形曲线代表我们的样本。让我们在图 3-6 中将其与我们的原始数据点一起展示。
看着这个钟形曲线,我们可以合理地期望金毛寻回犬的体重最有可能在 64.43(均值)左右,但在 55 或 73 时不太可能。比这更极端的情况变得非常不太可能。
图 3-6。拟合到数据点的正态分布
正态分布的性质
正态分布具有几个重要的属性,使其有用:
概率密度函数(PDF)
标准偏差在正态分布中起着重要作用,因为它定义了它的“扩散程度”。实际上,它是与均值一起的参数之一。创建正态分布的概率密度函数(PDF) 如下:
f ( x ) = 1 σ 2 π e - 1 2 ( x - μ σ 2 )
哇,这真是一个冗长的话题,不是吗?我们甚至在第一章中看到我们的朋友欧拉数e 和一些疯狂的指数。这是我们如何在 Python 中表达它的示例 3-9。
示例 3-9。Python 中的正态分布函数
def normal_pdf (x: float , mean: float , std_dev: float ) -> float :
return (1.0 / (2.0 * math.pi * std_dev ** 2 ) ** 0.5 ) *
math.exp(-1.0 * ((x - mean) ** 2 / (2.0 * std_dev ** 2 )))
这个公式中有很多要解开的内容,但重要的是它接受均值和标准偏差作为参数,以及一个 x 值,这样你就可以查找给定值的可能性。
就像第二章中的贝塔分布一样,正态分布是连续的。这意味着要获取一个概率,我们需要积分一系列x 值以找到一个区域。
实际上,我们将使用 SciPy 来为我们进行这些计算。
累积分布函数(CDF)
对于正态分布,垂直轴不是概率,而是数据的可能性。要找到概率,我们需要查看一个给定范围,然后找到该范围下曲线的面积。假设我想找到金毛寻回犬体重在 62 到 66 磅之间的概率。图 3-7 显示了我们要找到面积的范围。
图 3-7。测量 62 到 66 磅之间概率的 CDF
我们已经在第二章中用贝塔分布完成了这个任务,就像贝塔分布一样,这里也有一个累积密度函数(CDF)。让我们遵循这种方法。
正如我们在上一章中学到的,CDF 提供了给定分布的给定 x 值的面积直到 。让我们看看我们的金毛寻回犬正态分布的 CDF 是什么样子,并将其与 PDF 放在图 3-8 中进行参考。
图 3-8. PDF 与其 CDF 并排
注意两个图之间的关系。CDF 是一个 S 形曲线(称为 Sigmoid 曲线),它在 PDF 中投影到该范围的面积。观察图 3-9,当我们捕捉从负无穷到 64.43(均值)的面积时,我们的 CDF 显示的值恰好为.5 或 50%!
图 3-9. 金毛寻回犬体重的 PDF 和 CDF,测量均值的概率
这个区域的面积为 0.5 或 50%,这是由于我们正态分布的对称性,我们可以期望钟形曲线的另一侧也有 50%的面积。
要在 Python 中使用 SciPy 计算到 64.43 的面积,使用norm.cdf()
函数,如示例 3-10 所示。
示例 3-10. Python 中的正态分布 CDF
from scipy.stats import norm
mean = 64.43
std_dev = 2.99
x = norm.cdf(64.43 , mean, std_dev)
print (x)
就像我们在第二章中所做的那样,我们可以通过减去面积来演绎地找到中间范围的面积。如果我们想找到 62 到 66 磅之间观察到金毛寻回犬的概率,我们将计算到 66 的面积并减去到 62 的面积,如图 3-10 所示。
图 3-10. 寻找中间范围的概率
在 Python 中使用 SciPy 进行这个操作就像在示例 3-11 中所示的两个 CDF 操作相减一样简单。
示例 3-11. 使用 CDF 获取中间范围概率
from scipy.stats import norm
mean = 64.43
std_dev = 2.99
x = norm.cdf(66 , mean, std_dev) - norm.cdf(62 , mean, std_dev)
print (x)
你应该发现在 62 到 66 磅之间观察到金毛寻回犬的概率为 0.4920,或者大约为 49.2%。
逆 CDF
当我们在本章后面开始进行假设检验时,我们会遇到需要查找 CDF 上的一个区域然后返回相应 x 值的情况。当然,这是对 CDF 的反向使用,所以我们需要使用逆 CDF,它会翻转轴,如图 3-11 所示。
这样,我们现在可以查找一个概率然后返回相应的 x 值,在 SciPy 中我们会使用norm.ppf()
函数。例如,我想找到 95%的金毛寻回犬体重在以下。当我使用示例 3-12 中的逆 CDF 时,这很容易做到。
图 3-11. 逆 CDF,也称为 PPF 或分位数函数
示例 3-12. 在 Python 中使用逆 CDF(称为ppf()
)
from scipy.stats import norm
x = norm.ppf(.95 , loc=64.43 , scale=2.99 )
print (x)
我发现 95%的金毛寻回犬体重为 69.348 磅或更少。
你也可以使用逆 CDF 生成遵循正态分布的随机数。如果我想创建一个模拟,生成一千个真实的金毛寻回犬体重,我只需生成一个介于 0.0 和 1.0 之间的随机值,传递给逆 CDF,然后返回体重值,如示例 3-13 所示。
示例 3-13. 从正态分布生成随机数
import random
from scipy.stats import norm
for i in range (0 ,1000 ):
random_p = random.uniform(0.0 , 1.0 )
random_weight = norm.ppf(random_p, loc=64.43 , scale=2.99 )
print (random_weight)
当然,NumPy 和其他库可以为您生成分布的随机值,但这突出了逆 CDF 很方便的一个用例。
从头开始实现 CDF 和逆 CDF
要学习如何在 Python 中从头开始实现 CDF 和逆 CDF,请参考附录 A。
Z 分数
将正态分布重新缩放,使均值为 0,标准差为 1 是很常见的,这被称为标准正态分布 。这样可以轻松比较一个正态分布的扩展与另一个正态分布,即使它们具有不同的均值和方差。
特别重要的是,标准正态分布将所有 x 值表示为标准差,称为Z 分数 。将 x 值转换为 Z 分数使用基本的缩放公式:
z = x - μ σ
这是一个例子。我们有两个来自两个不同社区的房屋。社区 A 的平均房屋价值为140 , 000 , 标 准 差 为 140 , 000 , 标 准 差 为 3,000。社区 B 的平均房屋价值为800 , 000 , 标 准 差 为 800 , 000 , 标 准 差 为 10,000。
μ A = 140,000 μ B = 800,000 σ A = 3,000 σ B = 10,000
现在我们有两个来自每个社区的房屋。社区 A 的房屋价值为150 , 000 , 社 区 B 的 房 屋 价 值 为 150 , 000 , 社 区 B 的 房 屋 价 值 为 815,000。哪个房屋相对于其社区的平均房屋更昂贵?
x A = 150,000 x B = 815,000
如果我们将这两个值用标准差表示,我们可以相对于每个社区的均值进行比较。使用 Z 分数公式:
z = x - mean standard deviation z A = 150000 - 140000 3000 = 3. 333 ¯ z B = 815000 - 800000 10000 = 1.5
因此,A 社区的房屋实际上相对于其社区要昂贵得多,因为它们的 Z 分数分别为3. 333 ¯ 和 1.5。
下面是我们如何将来自具有均值和标准差的给定分布的 x 值转换为 Z 分数,反之亦然,如示例 3-14 所示。
示例 3-14. 将 Z 分数转换为 x 值,反之亦然
def z_score (x, mean, std ):
return (x - mean) / std
def z_to_x (z, mean, std ):
return (z * std) + mean
mean = 140000
std_dev = 3000
x = 150000
z = z_score(x, mean, std_dev)
back_to_x = z_to_x(z, mean, std_dev)
print ("Z-Score: {}" .format (z))
print ("Back to X: {}" .format (back_to_x))
z_score()
函数将接受一个 x 值,并根据均值和标准差将其按标准偏差进行缩放。z_to_x()
函数将接受一个 Z 分数,并将其转换回 x 值。研究这两个函数,你可以看到它们的代数关系,一个解决 Z 分数,另一个解决 x 值。然后,我们将一个 x 值为 8.0 转换为3. 333 ¯ 的 Z 分数,然后将该 Z 分数转换回 x 值。
推断统计学
我们迄今为止所涵盖的描述性统计学是常见的。然而,当我们涉及推断统计学时,样本和总体之间的抽象关系得以充分发挥。这些抽象的微妙之处不是你想要匆忙通过的,而是要花时间仔细吸收。正如前面所述,我们作为人类有偏见,很快就会得出结论。成为一名优秀的数据科学专业人员需要你抑制那种原始的欲望,并考虑其他解释可能存在的可能性。推测没有任何解释,某一发现只是巧合和随机的也是可以接受的(也许甚至是开明的)。
首先让我们从为所有推断统计学奠定基础的定理开始。
中心极限定理
正态分布之所以有用的原因之一是因为它在自然界中经常出现,比如成年金毛犬的体重。然而,在自然人口之外的更迷人的背景下,它也会出现。当我们开始从人口中抽取足够大的样本时,即使该人口不遵循正态分布,正态分布仍然会出现。
假设我正在测量一个真正随机且均匀的人口。0.0 到 1.0 之间的任何值都是同等可能的,没有任何值有任何偏好。但当我们从这个人口中抽取越来越大的样本,计算每个样本的平均值,然后将它们绘制成直方图时,会发生一些有趣的事情。在示例 3-15 中运行这段 Python 代码,并观察图 3-12 中的图表。
示例 3-15. 在 Python 中探索中心极限定理
import random
import plotly.express as px
sample_size = 31
sample_count = 1000
x_values = [(sum ([random.uniform(0.0 , 1.0 ) for i in range (sample_size)]) / \
sample_size)
for _ in range (sample_count)]
y_values = [1 for _ in range (sample_count)]
px.histogram(x=x_values, y = y_values, nbins=20 ).show()
图 3-12. 取样本均值(每个大小为 31)并绘制它们
等等,当作为 31 个一组的均匀随机数取样然后求平均时,为什么会大致形成正态分布?任何数字都是等可能的,对吧?分布不应该是平坦的而不是钟形曲线吗?
发生了什么呢?样本中的单个数字本身不会产生正态分布。分布将是平坦的,其中任何数字都是等可能的(称为均匀分布 )。但当我们将它们作为样本分组并求平均时,它们形成一个正态分布。
这是因为中心极限定理 ,它表明当我们对一个人口取足够大的样本,计算每个样本的均值,并将它们作为一个分布绘制时,会发生有趣的事情:
样本均值的平均值等于总体均值。
如果总体是正态的,那么样本均值将是正态的。
如果总体不是正态分布,但样本量大于 30,样本均值仍然会大致形成正态分布。
样本均值的标准差等于总体标准差除以n 的平方根:
sample standard deviation = population standard deviation sample size
为什么上述所有内容很重要?这些行为使我们能够根据样本推断出关于人口的有用信息,即使对于非正态分布的人口也是如此。如果你修改前面的代码并尝试较小的样本量,比如 1 或 2,你将看不到正态分布出现。但当你接近 31 或更多时,你会看到我们收敛到一个正态分布,如图 3-13 所示。
图 3-13。较大的样本量接近正态分布
三十一是统计学中的教科书数字,因为这时我们的样本分布通常会收敛到总体分布,特别是当我们测量样本均值或其他参数时。当你的样本少于 31 个时,这时你必须依赖 T 分布而不是正态分布,随着样本量的减小,正态分布的尾部会变得越来越厚。我们稍后会简要讨论这一点,但首先让我们假设在谈论置信区间和检验时,我们的样本至少有 31 个。
置信区间
你可能听过“置信区间”这个术语,这经常让统计新手和学生感到困惑。置信区间 是一个范围计算,显示我们有多大信心相信样本均值(或其他参数)落在总体均值的范围内。
基于 31 只金毛猎犬的样本,样本均值为 64.408,样本标准差为 2.05,我有 95%的信心总体均值在 63.686 和 65.1296 之间。 我怎么知道这个?让我告诉你,如果你感到困惑,回到这段话,记住我们试图实现什么。我有意突出了它!
我首先选择一个置信水平(LOC) ,这将包含所需概率的总体均值范围。我希望有 95%的信心,我的样本均值落在我将计算的总体均值范围内。这就是我的 LOC。我们可以利用中心极限定理并推断出总体均值的这个范围是什么。首先,我需要关键 z 值 ,这是在标准正态分布中给出 95%概率的对称范围,如图 3-14 中所示。
我们如何计算包含 0.95 面积的对称范围?这作为一个概念比作为一个计算更容易理解。你可能本能地想使用 CDF,但随后你可能会意识到这里有更多的变动部分。
首先,您需要利用逆 CDF。从逻辑上讲,为了获得中心对称区域的 95%面积,我们将切掉剩余 5%面积的尾部。将剩余的 5%面积分成两半,每个尾部将给我们 2.5%的面积。因此,我们想要查找 x 值的面积是 0.025 和 0.975,如图 3-15 所示。
图 3-14。标准正态分布中心的 95%对称概率
图 3-15。我们想要给出面积为 0.025 和 0.975 的 x 值
我们可以查找面积为 0.025 和面积为 0.975 的 x 值,这将给出包含 95%面积的中心范围。然后我们将返回包含这个区域的相应下限和上限 z 值。请记住,我们在这里使用标准正态分布,因此它们除了是正/负之外是相同的。让我们按照 Python 中示例 3-16 中所示的方式计算这个。
示例 3-16。检索关键的 z 值
from scipy.stats import norm
def critical_z_value (p ):
norm_dist = norm(loc=0.0 , scale=1.0 )
left_tail_area = (1.0 - p) / 2.0
upper_area = 1.0 - ((1.0 - p) / 2.0 )
return norm_dist.ppf(left_tail_area), norm_dist.ppf(upper_area)
print (critical_z_value(p=.95 ))
好的,所以我们得到了±1.95996,这是我们在标准正态分布中心捕获 95%概率的关键 z 值。接下来,我将利用中心极限定理来产生误差边界(E) ,这是样本均值周围包含在该置信水平下的总体均值的范围。回想一下,我们的 31 只金毛猎犬样本的均值为 64.408,标准差为 2.05。计算这个误差边界的公式是:
E = ± z c s n E = ± 1.95996 * 2.05 31 E = ± 0.72164
如果我们将这个误差边界应用于样本均值,最终我们就得到了置信区间!
95% confidence interval = 64.408 ± 0.72164
这里是如何在 Python 中从头到尾计算这个置信区间的,详见示例 3-17。
示例 3-17. 在 Python 中计算置信区间
from math import sqrt
from scipy.stats import norm
def critical_z_value (p ):
norm_dist = norm(loc=0.0 , scale=1.0 )
left_tail_area = (1.0 - p) / 2.0
upper_area = 1.0 - ((1.0 - p) / 2.0 )
return norm_dist.ppf(left_tail_area), norm_dist.ppf(upper_area)
def confidence_interval (p, sample_mean, sample_std, n ):
lower, upper = critical_z_value(p)
lower_ci = lower * (sample_std / sqrt(n))
upper_ci = upper * (sample_std / sqrt(n))
return sample_mean + lower_ci, sample_mean + upper_ci
print (confidence_interval(p=.95 , sample_mean=64.408 , sample_std=2.05 , n=31 ))
因此,解释这个的方式是“基于我的 31 只金毛犬体重样本,样本均值为 64.408,样本标准差为 2.05,我有 95%的信心总体均值在 63.686 和 65.1296 之间。” 这就是我们描述置信区间的方式。
这里还有一件有趣的事情要注意,就是在我们的误差边界公式中,n 变大时,我们的置信区间变得更窄!这是有道理的,因为如果我们有一个更大的样本,我们对于总体均值落在更小范围内的信心就更大,这就是为什么它被称为置信区间。
这里需要注意的一个警告是,为了使这个工作起效,我们的样本大小必须至少为 31 个项目。这回到了中心极限定理。如果我们想将置信区间应用于更小的样本,我们需要使用方差更高的分布(更多不确定性的更胖尾巴)。这就是 T 分布的用途,我们将在本章末讨论它。
在第五章中,我们将继续使用线性回归的置信区间。
理解 P 值
当我们说某事是统计显著 时,我们指的是什么?我们经常听到这个词被随意地使用,但在数学上它意味着什么?从技术上讲,这与一个叫做 p 值的东西有关,这对许多人来说是一个难以理解的概念。但我认为当你将 p 值的概念追溯到它的发明时,它会更有意义。虽然这只是一个不完美的例子,但它传达了一些重要的思想。
1925 年,数学家罗纳德·费舍尔在一个聚会上。他的同事穆里尔·布里斯托尔声称她能通过品尝来区分是先倒茶还是先倒牛奶。对这一说法感到好奇,罗纳德当场设置了一个实验。
他准备了八杯茶。四杯先倒牛奶,另外四杯先倒茶。然后他把它们呈现给他的鉴赏家同事,并要求她识别每一杯的倒入顺序。令人惊讶的是,她全部正确识别了,这种情况发生的概率是 70 分之 1,或者 0.01428571。
这个 1.4%的概率就是我们所谓的p 值 ,即某事发生的概率是偶然而非因为一个假设的解释。不深入组合数学的兔子洞,穆里尔完全猜对杯子的概率是 1.4%。这到底告诉了你什么?
当我们设计一个实验,无论是确定有机甜甜圈是否导致体重增加,还是住在电力线附近是否导致癌症,我们总是要考虑到随机运气起了作用的可能性。就像穆里尔仅仅通过猜测正确识别茶杯的概率为 1.4%一样,总是有一种可能性,即随机性给了我们一个好手牌,就像老丨虎丨机一样。这有助于我们构建我们的零假设(H[0]) ,即所研究的变量对实验没有影响,任何积极的结果只是随机运气。备择假设(H[1]) 提出所研究的变量(称为控制变量 ) 导致了积极的结果。
传统上,统计显著性的阈值是 5%或更低的 p 值,即 0.05。由于 0.014 小于 0.05,这意味着我们可以拒绝零假设,即穆里尔是随机猜测的。然后我们可以提出备择假设,即穆里尔有特殊能力判断是先倒茶还是牛奶。
现在,这个茶会的例子没有捕捉到的一点是,当我们计算 p 值时,我们捕捉到了所有该事件或更罕见事件的概率。当我们深入研究下一个使用正态分布的例子时,我们将解决这个问题。
假设检验
过去的研究表明,感冒的平均恢复时间为 18 天,标准偏差为 1.5 天,并且符合正态分布。
这意味着在 15 到 21 天之间恢复的概率约为 95%,如图 3-16 和示例 3-18 所示。
图 3-16。在 15 到 21 天之间有 95%的恢复机会。
示例 3-18。计算在 15 到 21 天之间恢复的概率
from scipy.stats import norm
mean = 18
std_dev = 1.5
x = norm.cdf(21 , mean, std_dev) - norm.cdf(15 , mean, std_dev)
print (x)
我们可以从剩下的 5%概率推断出,恢复需要超过 21 天的概率为 2.5%,而少于 15 天的概率也为 2.5%。记住这一点,因为这将在后面至关重要!这驱动了我们的 p 值。
现在假设一个实验性新药物被给予了一个由 40 人组成的测试组,他们从感冒中恢复需要平均 16 天,如图 3-17 所示。
图 3-17。服用药物的一组人恢复需要 16 天。
这种药物有影响吗?如果你思考足够长时间,你可能会意识到我们所问的是:这种药物是否显示出统计上显著的结果?或者这种药物没有起作用,16 天的恢复只是与测试组的巧合?第一个问题构成了我们的备择假设,而第二个问题构成了我们的零假设。
我们可以通过两种方式来计算这个问题:单尾检验和双尾检验。我们将从单尾检验开始。
单尾检验
当我们进行单尾检验 时,通常使用不等式来构建零假设和备择假设。我们假设围绕着总体均值,并说它要么大于/等于 18(零假设 H 0 ),要么小于 18(备择假设 H 1 ):
H 0 : 总体 均值 ≥ 18 H 1 : 总体 均值 < 18
要拒绝我们的零假设,我们需要表明服用药物的患者的样本均值不太可能是巧合的。由于传统上认为小于或等于.05 的 p 值在统计上是显著的,我们将使用这个作为我们的阈值(图 3-17)。当我们在 Python 中使用逆 CDF 计算时,如图 3-18 和示例 3-19 所示,我们发现大约 15.53 是给我们左尾部分.05 面积的恢复天数。
图 3-18. 获取其后面 5%面积的 x 值
示例 3-19. 获取其后面 5%面积的 x 值的 Python 代码
from scipy.stats import norm
mean = 18
std_dev = 1.5
x = norm.ppf(.05 , mean, std_dev)
print (x)
因此,如果我们在样本组中实现平均 15.53 天或更少的恢复时间,我们的药物被认为在统计上足够显著地显示了影响。然而,我们的恢复时间的样本均值实际上是 16 天,不在这个零假设拒绝区域内。因此,如图 3-19 所示,统计显著性测试失败了。
图 3-19. 我们未能证明我们的药物测试结果在统计上是显著的
到达那个 16 天标记的面积是我们的 p 值,为.0912,并且我们在 Python 中计算如示例 3-20 所示。
示例 3-20. 计算单尾 p 值
from scipy.stats import norm
mean = 18
std_dev = 1.5
p_value = norm.cdf(16 , mean, std_dev)
print (p_value)
由于.0912 的 p 值大于我们的统计显著性阈值.05,我们不认为药物试验是成功的,并且未能拒绝我们的零假设。
双尾检验
我们之前进行的测试称为单尾检验,因为它只在一个尾部寻找统计显著性。然而,通常更安全和更好的做法是使用双尾检验。我们将详细说明原因,但首先让我们计算它。
要进行双尾检验 ,我们将零假设和备择假设构建在“相等”和“不相等”的结构中。在我们的药物测试中,我们会说零假设的平均恢复时间为 18 天。但我们的备择假设是平均恢复时间不是 18 天,这要归功于新药物:
H 0 : 总体 均值 = 18 H 1 : 总体 均值 ≠ 18
这有一个重要的含义。我们构建我们的备择假设不是测试药物是否改善感冒恢复时间,而是是否有任何 影响。这包括测试它是否增加了感冒的持续时间。这有帮助吗?记住这个想法。
自然地,这意味着我们将 p 值的统计显著性阈值扩展到两个尾部,而不仅仅是一个。如果我们正在测试 5% 的统计显著性,那么我们将其分割,并将每个尾部分配 2.5%。如果我们的药物的平均恢复时间落在任一区域内,我们的测试将成功,并拒绝零假设(图 3-20)。
图 3-20. 双尾检验
下尾和上尾的 x 值分别为 15.06 和 20.93,这意味着如果我们低于或高于这些值,我们将拒绝零假设。这两个值是使用图 3-21 和示例 3-21 中显示的逆 CDF 计算的。记住,为了得到上尾,我们取 .95 然后加上显著性阈值的 .025 部分,得到 .975。
图 3-21. 计算正态分布中心 95% 的区域
示例 3-21. 计算 5% 统计显著性范围
from scipy.stats import norm
mean = 18
std_dev = 1.5
x1 = norm.ppf(.025 , mean, std_dev)
x2 = norm.ppf(.975 , mean, std_dev)
print (x1)
print (x2)
药物测试组的样本均值为 16,16 既不小于 15.06 也不大于 20.9399。因此,就像单尾检验一样,我们仍然未能拒绝零假设。我们的药物仍然没有显示出任何统计显著性,也没有任何影响,如图 3-22 所示。
图 3-22. 双尾检验未能证明统计显著性
但是 p 值是什么?这就是双尾检验变得有趣的地方。我们的 p 值将捕捉不仅仅是 16 左侧的区域,还将捕捉右尾的对称等效区域。由于 16 比均值低 4 天,我们还将捕捉高于 20 的区域,即比均值高 4 天(图 3-23)。这是捕捉事件或更罕见事件的概率,位于钟形曲线的两侧。
图 3-23. p 值添加对称侧以获得统计显著性
当我们将这两个区域相加时,我们得到一个 p 值为 .1824。这远大于 .05,因此绝对不符合我们的 p 值阈值为 .05(示例 3-22)。
示例 3-22. 计算双尾 p 值
from scipy.stats import norm
mean = 18
std_dev = 1.5
p1 = norm.cdf(16 , mean, std_dev)
p2 = 1.0 - norm.cdf(20 , mean, std_dev)
p_value = p1 + p2
print (p_value)
那么为什么在双尾检验中还要添加对称区域的相反侧?这可能不是最直观的概念,但首先记住我们如何构建我们的假设:
H 0 : 总体 均值 = 18 H 1 : 总体 均值 ≠ 18
如果我们在“等于 18”与“不等于 18”的情况下进行测试,我们必须捕捉两侧任何等于或小于的概率。毕竟,我们试图证明显著性,这包括任何同样或更不可能发生的事情。我们在只使用“大于/小于”逻辑的单尾检验中没有这种特殊考虑。但当我们处理“等于/不等于”时,我们的兴趣范围向两个方向延伸。
那么双尾检验有什么实际影响?它如何影响我们是否拒绝零假设?问问自己:哪一个设定了更高的阈值?你会注意到,即使我们的目标是表明我们可能减少了某些东西(使用药物减少感冒恢复时间),重新构建我们的假设以显示任何影响(更大或更小)会创建一个更高的显著性阈值。如果我们的显著性阈值是小于或等于 0.05 的 p 值,我们的单尾检验在 p 值 0.0912 时更接近接受,而双尾检验则是大约两倍的 p 值 0.182。
这意味着双尾检验更难拒绝零假设,并需要更强的证据来通过测试。还要考虑这一点:如果我们的药物可能加重感冒并使其持续时间更长呢?捕捉这种可能性并考虑该方向的变化可能会有所帮助。这就是为什么在大多数情况下双尾检验更可取。它们往往更可靠,不会偏向于某一个方向的假设。
我们将在第五章和第六章再次使用假设检验和 p 值。
T 分布:处理小样本
让我们简要讨论如何处理 30 个或更少的较小样本;当我们在第五章进行线性回归时,我们将需要这个。无论是计算置信区间还是进行假设检验,如果样本中有 30 个或更少的项目,我们会选择使用 T 分布而不是正态分布。T 分布 类似于正态分布,但尾部更胖,以反映更多的方差和不确定性。图 3-24 显示了一个正态分布(虚线)和一个自由度为 1 的 T 分布(实线)。
图 3-24。T 分布与正态分布并列;请注意更胖的尾部。
样本大小越小,T 分布的尾部就越胖。但有趣的是,在接近 31 个项目后,T 分布几乎无法与正态分布区分开来,这很好地反映了中心极限定理的思想。
示例 3-23 展示了如何找到 95%置信度的临界 t 值 。当样本量为 30 或更少时,您可以在置信区间和假设检验中使用这个值。概念上与关键 z 值相同,但我们使用 T 分布而不是正态分布来反映更大的不确定性。样本量越小,范围越大,反映了更大的不确定性。
示例 3-23. 用 T 分布获取临界值范围
from scipy.stats import t
n = 25
lower = t.ppf(.025 , df=n-1 )
upper = t.ppf(.975 , df=n-1 )
print (lower, upper)
-2.063898561628021 2.0638985616280205
请注意,df
是“自由度”参数,正如前面所述,它应该比样本量少一个。
大数据考虑和得克萨斯神枪手谬误
在我们结束本章之前,最后一个想法。正如我们所讨论的,随机性在验证我们的发现中起着如此重要的作用,我们总是要考虑到它的可能性。不幸的是,随着大数据、机器学习和其他数据挖掘工具的出现,科学方法突然变成了一种倒退的实践。这可能是危险的;请允许我演示一下,从加里·史密斯的书标准偏差 (Overlook Press)中借鉴一个例子。
假设我从一副公平的牌中抽取四张牌。这里没有游戏或目标,只是抽取四张牌并观察它们。我得到两个 10,一个 3 和一个 2。我说:“这很有趣,我得到两个 10,一个 3 和一个 2。这有意义吗?接下来我抽的四张牌也会是两个连续的数字和一对吗?这里的潜在模型是什么?”
看到我做了什么吗?我拿了完全随机的东西,不仅寻找了模式,而且试图从中建立一个预测模型。这里微妙发生的是,我从未把得到这四张具有特定模式的牌作为我的目标。我是在它们发生之后 观察到它们。
这正是数据挖掘每天都会遇到的问题:在随机事件中找到巧合的模式。随着大量数据和快速算法寻找模式,很容易找到看起来有意义但实际上只是随机巧合的东西。
这也类似于我朝墙壁开枪。然后我在弹孔周围画一个靶子,然后邀请朋友过来炫耀我的惊人射击技巧。荒谬,对吧?嗯,很多数据科学家每天都在比喻性地做这件事,这就是得克萨斯神枪手谬误 。他们开始行动而没有目标,偶然发现了一些罕见的东西,然后指出他们发现的东西在某种程度上具有预测价值。
问题在于大数定律表明罕见事件很可能会发生;我们只是不知道是哪些。当我们遇到罕见事件时,我们会突出显示甚至推测可能导致它们的原因。问题在于:某个特定人赢得彩票的概率极低,但总会有人赢得 彩票。当有人赢得彩票时,我们为什么会感到惊讶?除非有人预测了赢家,否则除了一个随机的人走运之外,没有发生任何有意义的事情。
这也适用于相关性,我们将在第五章中学习。在具有数千个变量的庞大数据集中,很容易找到具有 0.05 p 值的统计显著性发现吗?当然!我会找到成千上万个这样的发现。我甚至会展示证据表明尼古拉斯·凯奇电影的数量与一年中溺水的人数相关 。
因此,为了防止得到德克萨斯神枪手谬误并成为大数据谬论的受害者,请尝试使用结构化的假设检验并为此目标收集数据。如果您使用数据挖掘,请尝试获取新鲜数据以查看您的发现是否仍然成立。最后,始终考虑事情可能是巧合的可能性;如果没有常识性的解释,那么很可能是巧合。
我们学会了在收集数据之前进行假设,但数据挖掘是先收集数据,然后进行假设。具有讽刺意味的是,我们在开始时更客观,因为我们会寻找数据来有意识地证明和反驳我们的假设。
结论
我们在本章学到了很多,你应该为走到这一步感到自豪。这可能是本书中较难的主题之一!我们不仅学习了从平均值到正态分布的描述性统计,还解决了置信区间和假设检验的问题。
希望你能稍微不同地看待数据。数据是某种东西的快照,而不是对现实的完全捕捉。单独的数据并不是很有用,我们需要背景、好奇心和分析数据来源的地方,然后我们才能对其进行有意义的洞察。我们讨论了如何描述数据以及根据样本推断更大人口的属性。最后,我们解决了一些数据挖掘中可能遇到的谬论,以及如何通过新鲜数据和常识来纠正这些谬论。
如果您需要回顾本章内容,不要感到难过,因为需要消化的内容很多。如果您想在数据科学和机器学习职业中取得成功,进入假设检验的思维模式也很重要。很少有从业者将统计和假设检验概念与机器学习联系起来,这是不幸的。
理解性和可解释性是机器学习的下一个前沿,因此在阅读本书的其余部分和职业生涯中继续学习和整合这些想法。
练习
你购买了一卷 1.75 毫米的线材用于你的 3D 打印机。你想测量线材直径与 1.75 毫米的实际接近程度。你使用卡钳工具在卷轴上抽取了五次直径值:
1.78, 1.75, 1.72, 1.74, 1.77
计算这组数值的均值和标准差。
一家制造商表示 Z-Phone 智能手机的平均使用寿命为 42 个月,标准差为 8 个月。假设服从正态分布,一个随机的 Z-Phone 使用寿命在 20 到 30 个月之间的概率是多少?
我怀疑我的 3D 打印机线材的平均直径不是广告中宣传的 1.75 毫米。我用我的工具抽取了 34 个测量值。样本均值为 1.715588,样本标准差为 0.029252。
我整个线轴的均值的 99% 置信区间是多少?
你的营销部门开始了一项新的广告活动,并想知道它是否影响了销售。过去销售额平均为每天 10 , 345 , 标 准 差 为 10 , 345 , 标 准 差 为 552。新的广告活动持续了 45 天,平均销售额为 $11,641。
这次广告活动是否影响了销售?为什么?(使用双尾检验以获得更可靠的显著性。)
答案在附录 B 中。
第四章:线性代数
稍微转换一下思路,让我们远离概率和统计,进入线性代数领域。有时人们会混淆线性代数和基本代数,可能认为它与使用代数函数y = mx + b 绘制线条有关。这就是为什么线性代数可能应该被称为“向量代数”或“矩阵代数”,因为它更加抽象。线性系统发挥作用,但以一种更加形而上的方式。
那么,线性代数到底是什么?嗯,线性代数 关注线性系统,但通过向量空间和矩阵来表示它们。如果你不知道什么是向量或矩阵,不用担心!我们将深入定义和探索它们。线性代数对于许多应用领域的数学、统计学、运筹学、数据科学和机器学习都至关重要。当你在这些领域中处理数据时,你正在使用线性代数,也许你甚至不知道。
你可以暂时不学习线性代数,使用机器学习和统计库为你完成所有工作。但是,如果你想理解这些黑匣子背后的直觉,并更有效地处理数据,理解线性代数的基础是不可避免的。线性代数是一个庞大的主题,可以填满厚厚的教科书,所以当然我们不能在这本书的一章中完全掌握它。然而,我们可以学到足够多,以便更加熟练地应用它并有效地在数据科学领域中导航。在本书的剩余章节中,包括第五章和第七章,也将有机会应用它。
什么是向量?
简单来说,向量 是空间中具有特定方向和长度的箭头,通常代表一段数据。它是线性代数的中心构建模块,包括矩阵和线性变换。在其基本形式中,它没有位置的概念,所以始终想象它的尾部从笛卡尔平面的原点(0,0)开始。
图 4-1 展示了一个向量v → ,它在水平方向移动三步,在垂直方向移动两步。
图 4-1。一个简单的向量
再次强调,向量的目的是直观地表示一段数据。如果你有一条房屋面积为 18,000 平方英尺,估值为 26 万美元的数据记录,我们可以将其表示为一个向量[18000, 2600000],在水平方向移动 18000 步,在垂直方向移动 260000 步。
我们数学上声明一个向量如下:
v → = x y v → = 3 2
我们可以使用简单的 Python 集合,如 Python 列表,在示例 4-1 中所示声明一个向量。
示例 4-1. 使用列表在 Python 中声明向量
v = [3 , 2 ]
print (v)
然而,当我们开始对向量进行数学计算,特别是在执行诸如机器学习之类的任务时,我们应该使用 NumPy 库,因为它比纯 Python 更高效。您还可以使用 SymPy 执行线性代数运算,在本章中,当小数变得不方便时,我们偶尔会使用它。然而,在实践中,您可能主要使用 NumPy,因此我们主要会坚持使用它。
要声明一个向量,您可以使用 NumPy 的array()
函数,然后可以像示例 4-2 中所示传递一组数字给它。
示例 4-2. 使用 NumPy 在 Python 中声明向量
import numpy as np
v = np.array([3 , 2 ])
print (v)
Python 速度慢,但其数值库不慢
Python 是一个计算速度较慢的语言平台,因为它不像 Java、C#、C 等编译为较低级别的机器代码和字节码。它在运行时动态解释。然而,Python 的数值和科学库并不慢。像 NumPy 这样的库通常是用低级语言如 C 和 C++编写的,因此它们在计算上是高效的。Python 实际上充当“胶水代码”,为您的任务集成这些库。
向量有无数的实际应用。在物理学中,向量通常被认为是一个方向和大小。在数学中,它是 XY 平面上的一个方向和比例,有点像运动。在计算机科学中,它是存储数据的一组数字。作为数据科学专业人员,我们将最熟悉计算机科学的上下文。然而,重要的是我们永远不要忘记视觉方面,这样我们就不会把向量看作是神秘的数字网格。没有视觉理解,几乎不可能掌握许多基本的线性代数概念,如线性相关性和行列式。
这里有一些更多的向量示例。在图 4-2 中,请注意一些向量在 X 和 Y 轴上具有负方向。具有负方向的向量在我们后面合并时会产生影响,基本上是相减而不是相加。
图 4-2. 不同向量的抽样
还要注意,向量可以存在于超过两个维度。接下来我们声明一个沿着 x、y 和 z 轴的三维向量:
v → = x y z = 4 1 2
要创建这个向量,我们在 x 方向走了四步,在 y 方向走了一步,在 z 方向走了两步。这在图 4-3 中有可视化展示。请注意,我们不再在二维网格上显示向量,而是在一个三维空间中,有三个轴:x、y 和 z。
图 4-3. 一个三维向量
当然,我们可以使用三个数值在 Python 中表示这个三维向量,就像在示例 4-3 中声明的那样。
示例 4-3. 在 Python 中使用 NumPy 声明一个三维向量
import numpy as np
v = np.array([4 , 1 , 2 ])
print (v)
像许多数学模型一样,可视化超过三维是具有挑战性的,这是我们在本书中不会花费精力去做的事情。但从数字上来看,仍然很简单。示例 4-4 展示了我们如何在 Python 中数学上声明一个五维向量。
v → = 6 1 5 8 3
示例 4-4. 在 Python 中使用 NumPy 声明一个五维向量
import numpy as np
v = np.array([6 , 1 , 5 , 8 , 3 ])
print (v)
添加和组合向量
单独看,向量并不是非常有趣。它表示一个方向和大小,有点像在空间中的移动。但当你开始组合向量,也就是向量相加 时,事情就变得有趣起来。我们实际上将两个向量的运动合并成一个单一的向量。
假设我们有两个向量 v → 和 w → 如图 4-4 所示。我们如何将这两个向量相加呢?
图 4-4. 将两个向量相加
我们将在稍后讨论为什么添加向量是有用的。但如果我们想要结合这两个向量,包括它们的方向和大小,那会是什么样子呢?从数字上来看,这很简单。你只需将各自的 x 值相加,然后将 y 值相加,得到一个新的向量,如示例 4-5 所示。
v → = 3 2 w → = 2 - 1 v → + w → = 3 + 2 2 + - 1 = 5 1
示例 4-5. 使用 NumPy 在 Python 中将两个向量相加
from numpy import array
v = array([3 ,2 ])
w = array([2 ,-1 ])
v_plus_w = v + w
print (v_plus_w)
但这在视觉上意味着什么呢?要将这两个向量视觉上相加,将一个向量连接到另一个向量的末端,然后走到最后一个向量的顶端(图 4-5)。你最终停留的位置就是一个新向量,是这两个向量相加的结果。
图 4-5. 将两个向量相加得到一个新向量
如图 4-5 所示,当我们走到最后一个向量 w → 的末端时,我们得到一个新向量 [5, 1]。这个新向量是将 v → 和 w → 相加的结果。在实践中,这可以简单地将数据相加。如果我们在一个区域中总结房屋价值和其平方英尺,我们将以这种方式将多个向量相加成一个单一向量。
请注意,无论我们是先将 v → 加上 w → 还是反之,都不影响结果,这意味着它是可交换 的,操作顺序不重要。如果我们先走 w → 再走 v → ,我们最终得到的向量 [5, 1] 与 图 4-6 中可视化的相同。
图 4-6. 向量的加法是可交换的
缩放向量
缩放 是增加或减小向量的长度。你可以通过乘以一个称为标量 的单个值来增加/减小向量。图 4-7 是向量v → 被放大 2 倍的示例。
图 4-7. 向量的缩放
从数学上讲,你将向量的每个元素乘以标量值:
v → = 3 1 2 v → = 2 3 1 = 3 × 2 1 × 2 = 6 2
在 Python 中执行这个缩放操作就像将向量乘以标量一样简单,就像在示例 4-6 中编写的那样。
示例 4-6. 在 Python 中使用 NumPy 缩放数字
from numpy import array
v = array([3 ,1 ])
scaled_v = 2.0 * v
print (scaled_v)
在这里,图 4-8 中的v → 被缩小了一半。
图 4-8. 将向量缩小一半
这里需要注意的一个重要细节是,缩放向量不会改变其方向,只会改变其大小。但是有一个轻微的例外,如图 4-9 中所示。当你用一个负数乘以一个向量时,它会翻转向量的方向。
图 4-9. 负标量会翻转向量方向
仔细思考一下,通过负数进行缩放实际上并没有改变方向,因为它仍然存在于同一条线上。这引出了一个称为线性相关的关键概念。
跨度和线性相关性
这两个操作,相加两个向量并对它们进行缩放,带来了一个简单但强大的想法。通过这两个操作,我们可以组合两个向量并对它们进行缩放,以创建我们想要的任何结果向量。图 4-10 展示了取两个向量v → 和w → ,进行缩放和组合的六个示例。这些向量v → 和w → ,固定在两个不同方向上,可以被缩放和相加以创建任何 新向量v + w → 。
图 4-10。缩放两个相加的向量允许我们创建任何新向量
再次,v → 和w → 在方向上固定,除了使用负标量进行翻转,但我们可以使用缩放自由地创建由v + w → 组成的任何向量。
整个可能向量空间称为span ,在大多数情况下,我们的 span 可以通过缩放和求和这两个向量来创建无限数量的向量。当我们有两个不同方向的向量时,它们是线性独立 的,并且具有这种无限的 span。
但在哪种情况下我们受限于可以创建的向量?思考一下并继续阅读。
当两个向量存在于相同方向,或存在于同一条线上时会发生什么?这些向量的组合也被限制在同一条线上,将我们的 span 限制在那条线上。无论如何缩放,结果的和向量也被困在同一条线上。这使它们成为线性相关 ,如图 4-11 所示。
图 4-11。线性相关向量
这里的 span 被困在与由两个向量组成的同一条线上。因为这两个向量存在于相同的基础线上,我们无法通过缩放灵活地创建任何新向量。
在三维或更高维空间中,当我们有一组线性相关的向量时,我们经常会被困在较低维度的平面上。这里有一个例子,即使我们有三维向量如图 4-12 所述,我们也被困在二维平面上。
图 4-12。三维空间中的线性相关性;请注意我们的张成空间被限制在一个平面上
后面我们将学习一个简单的工具叫做行列式来检查线性相关性,但我们为什么关心两个向量是线性相关还是线性无关呢?当它们线性相关时,很多问题会变得困难或无法解决。例如,当我们在本章后面学习方程组时,一个线性相关的方程组会导致变量消失,使问题无法解决。但如果你有线性无关性,那么从两个或更多向量中创建任何你需要的向量的灵活性将变得无价,以便解决问题!
线性变换
将具有固定方向的两个向量相加,但按比例缩放它们以获得不同的组合向量的概念非常重要。这个组合向量,除了线性相关的情况外,可以指向任何方向并具有我们选择的任何长度。这为线性变换建立了一种直观,我们可以使用一个向量以类似函数的方式来转换另一个向量。
基向量
想象我们有两个简单的向量i ^ 和j ^ (“i-hat”和“j-hat”)。这些被称为基向量 ,用于描述对其他向量的变换。它们通常长度为 1,并且指向垂直正方向,如图 4-13 中可视化的。
图 4-13。基向量i ^ 和j ^
将基向量视为构建或转换任何向量的构建块。我们的基向量用一个 2×2 矩阵表示,其中第一列是i ^ ,第二列是j ^ :
i ^ = 1 0 j ^ = 0 1 基向量 = 1 0 0 1
矩阵 是一组向量(如i ^ ,j ^ ),可以有多行多列,是打包数据的便捷方式。我们可以使用i ^ 和j ^ 通过缩放和相加来创建任何我们想要的向量。让我们从长度为 1 开始,并在图 4-14 中展示结果向量v → 。
图 4-14. 从基向量创建向量
我希望向量v → 落在[3, 2]处。如果我们将i ^ 拉伸 3 倍,j ^ 拉伸 2 倍,那么v → 会发生什么?首先,我们分别按照下面的方式对它们进行缩放:
3 i ^ = 3 1 0 = 3 0 2 j ^ = 2 0 1 = 0 2
如果我们在这两个方向上拉伸空间,那么这对 v → 会有什么影响呢?嗯,它将会随着 i ^ 和 j ^ 一起拉伸。这被称为线性变换 ,我们通过跟踪基向量的移动来进行向量的拉伸、挤压、剪切或旋转。在这种情况下(图 4-15),缩放 i ^ 和 j ^ 已经沿着我们的向量 v → 拉伸了空间。
图 4-15. 一个线性变换
但是 v → 会落在哪里呢?很容易看出它会落在这里,即 [3, 2]。记住向量 v → 是由 i ^ 和 j ^ 相加而成的。因此,我们只需将拉伸的 i ^ 和 j ^ 相加,就能看出向量 v → 落在哪里:
v → n e w = 3 0 + 0 2 = 3 2
一般来说,通过线性变换,你可以实现四种运动,如图 4-16 所示。
图 4-16。线性变换可以实现四种运动
这四种线性变换是线性代数的核心部分。缩放向量将拉伸或挤压它。旋转将转动向量空间,而反转将翻转向量空间,使i ^ 和j ^ 交换位置。
值得注意的是,你不能有非线性的变换,导致曲线或波浪状的变换不再遵守直线。这就是为什么我们称其为线性代数,而不是非线性代数!
矩阵向量乘法
这将引出线性代数中的下一个重要概念。在变换后跟踪i ^ 和j ^ 落在哪里的概念很重要,因为这不仅允许我们创建向量,还可以变换现有向量。如果你想要真正的线性代数启示,想想为什么创建向量和变换向量实际上是相同的事情。这完全取决于相对性,考虑到你的基向量在变换前后都是起点。
给定作为矩阵打包的基向量i ^ 和j ^ ,变换向量v → 的公式是:
x n e w y n e w = a b c d x y x n e w y n e w = a x + b y c x + d y
i ^ 是第一列[a, c ],j ^ 是第二列[b, d ]。我们将这两个基向量打包成一个矩阵,再次表示为一个在两个或更多维度中以数字网格形式表达的向量集合。通过应用基向量对向量进行转换被称为矩阵向量乘法 。这一开始可能看起来有些牵强,但这个公式是对缩放和添加i ^ 和j ^ 的快捷方式,就像我们之前对两个向量进行加法,并将该转换应用于任何向量v → 。
因此,实际上,矩阵确实是表示为基向量的转换。
要在 Python 中使用 NumPy 执行这种转换,我们需要将基向量声明为矩阵,然后使用dot()
运算符将其应用于向量v → (参见示例 4-7)。dot()
运算符将执行我们刚刚描述的矩阵和向量之间的缩放和加法。这被称为点积 ,我们将在本章中探讨它。
示例 4-7. NumPy 中的矩阵向量乘法
from numpy import array
basis = array(
[[3 , 0 ],
[0 , 2 ]]
)
v = array([1 ,1 ])
new_v = basis.dot(v)
print (new_v)
当考虑基向量时,我更喜欢将基向量分解然后将它们组合成一个矩阵。只需注意你需要转置 ,或者交换列和行。这是因为 NumPy 的array()
函数会执行我们不希望的相反方向,将每个向量填充为一行而不是一列。在 NumPy 中的转置示例可在示例 4-8 中看到。
示例 4-8. 分离基向量并将它们应用为一个转换
from numpy import array
i_hat = array([2 , 0 ])
j_hat = array([0 , 3 ])
basis = array([i_hat, j_hat]).transpose()
v = array([1 ,1 ])
new_v = basis.dot(v)
print (new_v)
这里有另一个例子。让我们从向量v → 为[2, 1]开始,i ^ 和j ^ 分别从[1, 0]和[0, 1]开始。然后我们将i ^ 和j ^ 转换为[2, 0]和[0, 3]。向量v → 会发生什么?通过手工使用我们的公式进行数学计算,我们得到如下结果:
x n e w y n e w = a b c d x y = a x + b y c x + d y x n e w y n e w = 2 0 0 3 2 1 = ( 2 ) ( 2 ) + ( 0 ) ( 1 ) ( 2 ) ( 0 ) + ( 3 ) ( 1 ) = 4 3
示例 4-9 展示了这个解决方案在 Python 中的应用。
示例 4-9。使用 NumPy 转换向量
from numpy import array
i_hat = array([2 , 0 ])
j_hat = array([0 , 3 ])
basis = array([i_hat, j_hat]).transpose()
v = array([2 ,1 ])
new_v = basis.dot(v)
print (new_v)
向量v → 现在落在[4, 3]处。图 4-17 展示了这个变换的样子。
图 4-17。一个拉伸的线性变换
这是一个将事情提升到一个新水平的例子。让我们取值为[2, 1]的向量v → 。i ^ 和j ^ 起始于[1, 0]和[0, 1],但随后被变换并落在[2, 3]和[2, -1]。v → 会发生什么?让我们在图 4-18 和示例 4-10 中看看。
图 4-18。一个进行旋转、剪切和空间翻转的线性变换
示例 4-10。一个更复杂的变换
from numpy import array
i_hat = array([2 , 3 ])
j_hat = array([2 , -1 ])
basis = array([i_hat, j_hat]).transpose()
v = array([2 ,1 ])
new_v = basis.dot(v)
print (new_v)
这里发生了很多事情。我们不仅缩放了i ^ 和j ^ ,还拉长了向量v → 。我们实际上还剪切、旋转和翻转了空间。当i ^ 和j ^ 在顺时针方向上交换位置时,你会知道空间被翻转了,我们将在本章后面学习如何通过行列式检测这一点。
矩阵乘法
我们学会了如何将一个向量和一个矩阵相乘,但是将两个矩阵相乘到底实现了什么?将矩阵乘法 看作是对向量空间应用多个变换。每个变换就像一个函数,我们首先应用最内层的变换,然后依次向外应用每个后续变换。
这里是我们如何对任意向量v → (值为[x, y ])应用旋转和剪切:
1 1 0 1 0 - 1 1 0 x y
我们实际上可以通过使用这个公式将这两个变换合并,将一个变换应用到最后一个上。你需要将第一个矩阵的每一行与第二个矩阵的每一列相乘并相加,按照“上下!上下!”的模式进行:
a b c d e f g h = a e + b g a f + b h c e + d y c f + d h
因此,我们实际上可以通过使用这个公式将这两个单独的变换(旋转和剪切)合并为一个单一的变换:
1 1 0 1 0 - 1 1 0 x y = ( 1 ) ( 0 ) + ( 1 ) ( 1 ) ( - 1 ) ( 1 ) + ( 1 ) ( 0 ) ( 0 ) ( 0 ) + ( 1 ) ( 1 ) ( 0 ) ( - 1 ) + ( 1 ) ( 0 ) x y = 1 - 1 1 0 x y
要在 Python 中使用 NumPy 执行这个操作,你可以简单地使用 matmul()
或 @
运算符来组合这两个矩阵(示例 4-11)。然后我们将转身并将这个合并的变换应用于一个向量 [1, 2]。
示例 4-11. 组合两个变换
from numpy import array
i_hat1 = array([0 , 1 ])
j_hat1 = array([-1 , 0 ])
transform1 = array([i_hat1, j_hat1]).transpose()
i_hat2 = array([1 , 0 ])
j_hat2 = array([1 , 1 ])
transform2 = array([i_hat2, j_hat2]).transpose()
combined = transform2 @ transform1
print ("COMBINED MATRIX:\n {}" .format (combined))
v = array([1 , 2 ])
print (combined.dot(v))
使用 dot()
与 matmul()
以及 @
一般来说,你应该更倾向于使用 matmul()
和它的简写 @
来组合矩阵,而不是 NumPy 中的 dot()
运算符。前者通常对于高维矩阵和元素广播有更好的策略。
如果你喜欢深入研究这些实现细节,这个 StackOverflow 问题是一个很好的起点 。
请注意,我们也可以将每个变换分别应用于向量 v → ,并且仍然会得到相同的结果。如果你用这三行替换最后一行,分别应用每个变换,你仍然会得到那个新向量 [-1, 1]:
rotated = transform1.dot(v)
sheered = transform2.dot(rotated)
print (sheered)
请注意,应用每个变换的顺序很重要!如果我们在 变换 2
上应用 变换 1
,我们会得到一个不同的结果 [-2, 3],如 示例 4-12 中计算的那样。因此矩阵点积不是可交换的,这意味着你不能改变顺序并期望得到相同的结果!
示例 4-12. 反向应用变换
from numpy import array
i_hat1 = array([0 , 1 ])
j_hat1 = array([-1 , 0 ])
transform1 = array([i_hat1, j_hat1]).transpose()
i_hat2 = array([1 , 0 ])
j_hat2 = array([1 , 1 ])
transform2 = array([i_hat2, j_hat2]).transpose()
combined = transform1 @ transform2
print ("COMBINED MATRIX:\n {}" .format (combined))
v = array([1 , 2 ])
print (combined.dot(v))
将每个变换看作一个函数,并且我们从最内层到最外层应用它们,就像嵌套函数调用一样。
行列式
当我们进行线性变换时,有时会“扩展”或“挤压”空间,这种情况发生的程度可能会有所帮助。从向量空间中的采样区域中取出一个样本区域,看看在我们对 i ^ 和 j ^ 进行缩放后会发生什么?
图 4-20. 一个行列式测量线性变换如何缩放一个区域
请注意,面积增加了 6.0 倍,这个因子被称为行列式 。行列式描述了在向量空间中采样区域随线性变换的尺度变化,这可以提供有关变换的有用信息。
示例 4-13 展示了如何在 Python 中计算这个行列式。
示例 4-13. 计算行列式
from numpy.linalg import det
from numpy import array
i_hat = array([3 , 0 ])
j_hat = array([0 , 2 ])
basis = array([i_hat, j_hat]).transpose()
determinant = det(basis)
print (determinant)
简单的剪切和旋转不会影响行列式,因为面积不会改变。图 4-21 和 示例 4-14 展示了一个简单的剪切,行列式仍然保持为 1.0,表明它没有改变。
图 4-21. 简单的剪切不会改变行列式
示例 4-14. 一个剪切的行列式
from numpy.linalg import det
from numpy import array
i_hat = array([1 , 0 ])
j_hat = array([1 , 1 ])
basis = array([i_hat, j_hat]).transpose()
determinant = det(basis)
print (determinant)
但是缩放会增加或减少行列式,因为这会增加/减少采样区域。当方向翻转时( i ^ , j ^ 顺时针交换位置),那么行列式将为负。图 4-22 和 示例 4-15 说明了一个行列式展示了一个不仅缩放而且翻转了向量空间方向的变换。
图 4-22. 在翻转空间上的行列式是负的
示例 4-15. 一个负的行列式
from numpy.linalg import det
from numpy import array
i_hat = array([-2 , 1 ])
j_hat = array([1 , 2 ])
basis = array([i_hat, j_hat]).transpose()
determinant = det(basis)
print (determinant)
因为这个行列式是负的,我们很快就看到方向已经翻转。但行列式告诉你的最关键的信息是变换是否线性相关。如果行列式为 0,那意味着所有的空间都被压缩到一个较低的维度。
在图 4-23 中,我们看到两个线性相关的变换,其中一个二维空间被压缩成一维,一个三维空间被压缩成两维。在这两种情况下,面积和体积分别为 0!
图 4-23。二维和三维中的线性依赖
示例 4-16 展示了前述 2D 示例的代码,将整个二维空间压缩成一个一维数轴。
示例 4-16。行列式为零
from numpy.linalg import det
from numpy import array
i_hat = array([-2 , 1 ])
j_hat = array([3 , -1.5 ])
basis = array([i_hat, j_hat]).transpose()
determinant = det(basis)
print (determinant)
因此,测试行列式为 0 对于确定一个变换是否具有线性依赖性非常有帮助。当你遇到这种情况时,你可能会发现手头上有一个困难或无法解决的问题。
特殊类型的矩阵
我们应该涵盖一些值得注意的矩阵情况。
方阵
方阵 是行数和列数相等的矩阵:
4 2 7 5 1 9 4 0 1
它们主要用于表示线性变换,并且是许多操作的要求,如特征分解。
身份矩阵
身份矩阵 是一个对角线为 1 的方阵,而其他值为 0:
1 0 0 0 1 0 0 0 1
身份矩阵有什么了不起的地方?当你有一个身份矩阵时,实际上是撤销了一个变换并找到了起始基向量。这在下一节解方程组中将起到重要作用。
逆矩阵
逆矩阵 是一个可以撤销另一个矩阵变换的矩阵。假设我有矩阵A :
A = 4 2 4 5 3 7 9 3 6
矩阵A 的逆被称为A - 1 。我们将在下一节学习如何使用 Sympy 或 NumPy 计算逆,但这就是矩阵A 的逆:
A - 1 = - 1 2 0 1 3 5 . 5 - 2 4 3 - 2 1 1 3
当我们在A - 1 和A 之间进行矩阵乘法时,我们得到一个身份矩阵。我们将在下一节关于方程组的 NumPy 和 Sympy 中看到这一点。
- 1 2 0 1 3 5 . 5 - 2 4 3 - 2 1 1 3 4 2 4 5 3 7 9 3 6 = 1 0 0 0 1 0 0 0 1
对角矩阵
与身份矩阵类似的是对角矩阵 ,它在对角线上有非零值,而其余值为 0。对角矩阵在某些计算中是可取的,因为它们代表应用于向量空间的简单标量。它出现在一些线性代数操作中。
4 0 0 0 2 0 0 0 5
三角矩阵
与对角矩阵类似的是三角矩阵 ,它在一个三角形值的对角线前有非零值,而其余值为 0。
4 2 9 0 1 6 0 0 5
三角矩阵在许多数值分析任务中是理想的,因为它们通常更容易在方程组中解决。它们也出现在某些分解任务中,比如LU 分解 。
稀疏矩阵
有时,你会遇到大部分元素为零且只有很少非零元素的矩阵。这些被称为稀疏矩阵 。从纯数学的角度来看,它们并不是特别有趣。但从计算的角度来看,它们提供了创造效率的机会。如果一个矩阵大部分为 0,稀疏矩阵的实现将不会浪费空间存储大量的 0,而是只跟踪非零的单元格。
sparse: 0 0 0 0 0 2 0 0 0 0 0 0
当你有大型稀疏矩阵时,你可能会明确使用稀疏函数来创建你的矩阵。
方程组和逆矩阵
线性代数的基本用例之一是解方程组。学习逆矩阵也是一个很好的应用。假设你被提供以下方程,需要解出x 、y 和z :
4 x + 2 y + 4 z = 44 5 x + 3 y + 7 z = 56 9 x + 3 y + 6 z = 72
你可以尝试手动尝试不同的代数运算来分离三个变量,但如果你想让计算机解决它,你需要将这个问题用矩阵表示,如下所示。将系数提取到矩阵A 中,方程右侧的值提取到矩阵B 中,未知变量提取到矩阵X 中:
A = 4 2 4 5 3 7 9 3 6 B = 44 56 72 X = x y z
线性方程组的函数是AX = B 。我们需要用另一个矩阵X 转换矩阵A ,结果得到矩阵B :
A X = B 4 2 4 5 3 7 9 3 6 · x y z = 44 56 72
我们需要“撤销”A ,这样我们就可以隔离X 并获得x 、y 和z 的值。撤销A 的方法是取A 的逆,用A - 1 表示,并通过矩阵乘法应用于A 。我们可以用代数方式表达这一点:
A X = B A - 1 A X = A - 1 B X = A - 1 B
要计算矩阵A 的逆,我们可能会使用计算机,而不是手动使用高斯消元法寻找解决方案,这在本书中我们不会涉及。这是矩阵A 的逆:
A - 1 = - 1 2 0 1 3 5 . 5 - 2 4 3 - 2 1 1 3
当我们将A - 1 与A 相乘时,将创建一个单位矩阵,一个除了对角线上为 1 之外全为零的矩阵。单位矩阵是线性代数中相当于乘以 1 的概念,意味着它基本上没有影响,将有效地隔离x 、y 和z 的值:
A - 1 = - 1 2 0 1 3 5 . 5 - 2 4 3 - 2 1 1 3 A = 4 2 4 5 3 7 9 3 6 A - 1 A = 1 0 0 0 1 0 0 0 1
要在 Python 中看到这个单位矩阵的运作,您将需要使用 SymPy 而不是 NumPy。 NumPy 中的浮点小数不会使单位矩阵显得那么明显,但在示例 4-17 中以符号方式进行,我们将看到一个清晰的符号输出。请注意,在 SymPy 中进行矩阵乘法时,我们使用星号 *** 而不是 @ 。
示例 4-17. 使用 SymPy 研究逆矩阵和单位矩阵
from sympy import *
A = Matrix([
[4 , 2 , 4 ],
[5 , 3 , 7 ],
[9 , 3 , 6 ]
])
inverse = A.inv()
identity = inverse * A
print ("INVERSE: {}" .format (inverse))
print ("IDENTITY: {}" .format (identity))
在实践中,浮点精度的缺失不会对我们的答案产生太大影响,因此使用 NumPy 来解决 x 应该是可以的。示例 4-18 展示了使用 NumPy 的解决方案。
示例 4-18. 使用 NumPy 解决一组方程
from numpy import array
from numpy.linalg import inv
A = array([
[4 , 2 , 4 ],
[5 , 3 , 7 ],
[9 , 3 , 6 ]
])
B = array([
44 ,
56 ,
72
])
X = inv(A).dot(B)
print (X)
因此 x = 2,y = 34,z = –8. 示例 4-19 展示了 SymPy 中的完整解决方案,作为 NumPy 的替代方案。
示例 4-19. 使用 SymPy 解决一组方程
from sympy import *
A = Matrix([
[4 , 2 , 4 ],
[5 , 3 , 7 ],
[9 , 3 , 6 ]
])
B = Matrix([
44 ,
56 ,
72
])
X = A.inv() * B
print (X)
这里是数学符号表示的解决方案:
A - 1 B = X - 1 2 0 1 3 5 . 5 - 2 4 3 - 2 1 1 3 44 56 72 = x y z 2 34 - 8 = x y z
希望这给了你逆矩阵的直觉以及它们如何用于解方程组的用途。
线性规划中的方程组
这种解方程组的方法也用于线性规划,其中不等式定义约束条件,目标是最小化/最大化。
PatrickJMT 在线性规划方面有很多优质视频 。我们也在附录 A 中简要介绍了它。
在实际操作中,你很少需要手动计算逆矩阵,可以让计算机为你完成。但如果你有需求或好奇,你会想了解高斯消元法。PatrickJMT 在 YouTube 上的视频 展示了许多关于高斯消元法的视频。
特征向量和特征值
矩阵分解 是将矩阵分解为其基本组件,类似于分解数字(例如,10 可以分解为 2 × 5)。
矩阵分解对于诸如找到逆矩阵、计算行列式以及线性回归等任务非常有帮助。根据你的任务,有许多分解矩阵的方法。在第五章中,我们将使用一种矩阵分解技术,QR 分解,来执行线性回归。
但在本章中,让我们专注于一种常见方法,称为特征分解,这经常用于机器学习和主成分分析。在这个层面上,我们没有精力深入研究每个应用。现在,只需知道特征分解有助于将矩阵分解为在不同机器学习任务中更易处理的组件。还要注意它只适用于方阵。
在特征分解中,有两个组成部分:用 lambda 表示的特征值 λ 和用 v 表示的特征向量,如 图 4-24 所示。
图 4-24. 特征向量和特征值
如果我们有一个方阵 A ,它有以下特征值方程:
A v = λ v
如果 A 是原始矩阵,它由特征向量 v 和特征值 λ 组成。对于父矩阵的每个维度,都有一个特征向量和特征值,并非所有矩阵都可以分解为特征向量和特征值。有时甚至会出现复数(虚数)。
示例 4-20 是我们如何在 NumPy 中为给定矩阵 A 计算特征向量和特征值的方法。
示例 4-20. 在 NumPy 中执行特征分解
from numpy import array, diag
from numpy.linalg import eig, inv
A = array([
[1 , 2 ],
[4 , 5 ]
])
eigenvals, eigenvecs = eig(A)
print ("EIGENVALUES" )
print (eigenvals)
print ("\nEIGENVECTORS" )
print (eigenvecs)
"""
EIGENVALUES
[-0.46410162 6.46410162]
EIGENVECTORS
[[-0.80689822 -0.34372377]
[ 0.59069049 -0.9390708 ]]
"""
那么我们如何从特征向量和特征值重建矩阵 A 呢?回想一下这个公式:
A v = λ v
我们需要对重构 A 的公式进行一些调整:
A = Q Λ Q - 1
在这个新公式中,Q 是特征向量,Λ 是对角形式的特征值,Q - 1 是 Q 的逆矩阵。对角形式意味着向量被填充成一个零矩阵,并且占据对角线,类似于单位矩阵的模式。
示例 4-21 在 Python 中将示例完整地展示,从分解矩阵开始,然后重新组合它。
示例 4-21. 在 NumPy 中分解和重组矩阵
from numpy import array, diag
from numpy.linalg import eig, inv
A = array([
[1 , 2 ],
[4 , 5 ]
])
eigenvals, eigenvecs = eig(A)
print ("EIGENVALUES" )
print (eigenvals)
print ("\nEIGENVECTORS" )
print (eigenvecs)
print ("\nREBUILD MATRIX" )
Q = eigenvecs
R = inv(Q)
L = diag(eigenvals)
B = Q @ L @ R
print (B)
"""
EIGENVALUES
[-0.46410162 6.46410162]
EIGENVECTORS
[[-0.80689822 -0.34372377]
[ 0.59069049 -0.9390708 ]]
REBUILD MATRIX
[[1\. 2.]
[4\. 5.]]
"""
正如你所看到的,我们重建的矩阵就是我们开始的那个矩阵。
结论
线性代数可能令人困惑,充满了神秘和值得思考的想法。你可能会发现整个主题就像一个大的兔子洞,你是对的!然而,如果你想要有一个长期成功的数据科学职业,继续对它感到好奇是个好主意。它是统计计算、机器学习和其他应用数据科学领域的基础。最终,它是计算机科学的基础。你当然可以一段时间不了解它,但在某个时候你会遇到理解上的限制。
你可能会想知道这些理念如何实际应用,因为它们可能感觉很理论。不用担心;我们将在本书中看到一些实际应用。但是理论和几何解释对于在处理数据时具有直觉是很重要的,通过直观地理解线性变换,你就能为以后可能遇到的更高级概念做好准备。
如果你想更多地了解线性规划,没有比3Blue1Brown 的 YouTube 播放列表“线性代数的本质” 更好的地方了。PatrickJMT 的线性代数视频 也很有帮助。
如果你想更加熟悉 NumPy,推荐阅读 Wes McKinney 的 O'Reilly 图书《Python 数据分析》(第二版)。它并不太关注线性代数,但提供了关于在数据集上使用 NumPy、Pandas 和 Python 的实用指导。
练习
向量v → 的值为[1, 2],然后发生了一个变换。i ^ 落在[2, 0],j ^ 落在[0, 1.5]。v → 会落在哪里?
向量v → 的值为[1, 2],然后发生了一个变换。i ^ 落在[-2, 1],j ^ 落在[1, -2]。v → 会落在哪里?
一个变换i ^ 落在[1, 0],j ^ 落在[2, 2]。这个变换的行列式是多少?
一个线性变换中是否可以进行两个或更多个线性变换?为什么?
解方程组以求解x 、y 和z :
3 x + 1 y + 0 z = = 54 2 x + 4 y + 1 z = 12 3 x + 1 y + 8 z = 6
以下矩阵是否线性相关?为什么?
2 1 6 3
答案在附录 B 中。
第五章:线性回归
数据分析中最实用的技术之一是通过观察数据点拟合一条直线,以展示两个或更多变量之间的关系。回归 试图将一个函数拟合到观察数据中,以对新数据进行预测。线性回归 将一条直线拟合到观察数据中,试图展示变量之间的线性关系,并对尚未观察到的新数据进行预测。
看一张线性回归的图片可能比阅读描述更有意义。在图 5-1 中有一个线性回归的例子。
线性回归是数据科学和统计学的中流砥柱,不仅应用了我们在前几章学到的概念,还为后续主题如神经网络(第七章)和逻辑回归(第六章)奠定了新的基础。这种相对简单的技术已经存在了两百多年,当代被称为一种机器学习形式。
机器学习从业者通常采用不同的验证方法,从数据的训练-测试分割开始。统计学家更有可能使用像预测区间和相关性这样的指标来进行统计显著性分析。我们将涵盖这两种思维方式,以便读者能够弥合这两个学科之间日益扩大的鸿沟,从而最好地装备自己。
图 5-1 线性回归的示例,将一条直线拟合到观察数据中
一个基本的线性回归
我想研究狗的年龄与它看兽医的次数之间的关系。在一个虚构的样本中,我们有 10 只随机的狗。我喜欢用简单的数据集(真实或其他)来理解复杂的技术,这样我们就能了解技术的优势和局限性,而不会被复杂的数据搞混。让我们将这个数据集绘制成图 5-2 所示。
图 5-2 绘制了 10 只狗的样本,显示它们的年龄和看兽医的次数
我们可以清楚地看到这里存在着线性相关性 ,意味着当这些变量中的一个增加/减少时,另一个也以大致相同的比例增加/减少。我们可以在图 5-3 中画一条线来展示这样的相关性。
图 5-3 拟合我们数据的一条线
我将在本章后面展示如何计算这条拟合线。我们还将探讨如何计算这条拟合线的质量。现在,让我们专注于执行线性回归的好处。它使我们能够对我们以前没有见过的数据进行预测。我的样本中没有一只 8.5 岁的狗,但我可以看着这条线估计这只狗一生中会有 21 次兽医就诊。我只需看看当x = 8.5 时,y = 21.218,如图 5-4 所示。另一个好处是我们可以分析可能存在关系的变量,并假设相关的变量之间是因果关系。
现在线性回归的缺点是什么?我不能期望每个结果都会完全 落在那条线上。毕竟,现实世界的数据是嘈杂的,从不完美,也不会遵循一条直线。它可能根本不会遵循一条直线!在那条线周围会有误差,点会在线的上方或下方。当我们谈论 p 值、统计显著性和预测区间时,我们将在数学上涵盖这一点,这些内容描述了我们的线性回归有多可靠。另一个问题是我们不应该使用线性回归来预测超出我们拥有数据范围之外的情况,这意味着我们不应该在x < 0 和x > 10 的情况下进行预测,因为我们没有这些值之外的数据。
图 5-4。使用线性回归进行预测,看到一个 8.5 岁的狗预测将有约 21.2 次兽医就诊
不要忘记抽样偏差!
我们应该质疑这些数据以及它们是如何抽样的,以便检测偏见。这是在单个兽医诊所吗?多个随机诊所?通过使用兽医数据是否存在自我选择偏见,只调查拜访兽医的狗?如果这些狗是在相同的地理位置抽样的,那会不会影响数据?也许在炎热的沙漠气候中的狗更容易因中暑和被蛇咬而去看兽医,这会使我们样本中的兽医就诊次数增加。
正如在第三章中讨论的那样,将数据视为真理的神谕已经变得时髦。然而,数据只是从一个总体中抽取的样本,我们需要对我们的样本有多好地代表性进行判断。对数据的来源同样感兴趣(如果不是更多),而不仅仅是数据所说的内容。
使用 SciPy 进行基本线性回归
在本章中,我们有很多关于线性回归要学习的内容,但让我们从一些代码开始执行我们已经了解的内容。
有很多平台可以执行线性回归,从 Excel 到 Python 和 R。但在本书中,我们将坚持使用 Python,从 scikit-learn 开始为我们完成工作。我将在本章后面展示如何“从头开始”构建线性回归,以便我们掌握像梯度下降和最小二乘这样的重要概念。
示例 5-1 是我们如何使用 scikit-learn 对这 10 只狗进行基本的、未经验证的线性回归的样本。我们使用 Pandas 获取这些数据 ,将其转换为 NumPy 数组,使用 scikit-learn 进行线性回归,并使用 Plotly 在图表中显示它。
示例 5-1. 使用 scikit-learn 进行线性回归
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
df = pd.read_csv('https://bit.ly/3goOAnt' , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
fit = LinearRegression().fit(X, Y)
m = fit.coef_.flatten()
b = fit.intercept_.flatten()
print ("m = {0}" .format (m))
print ("b = {0}" .format (b))
plt.plot(X, Y, 'o' )
plt.plot(X, m*X+b)
plt.show()
首先,我们从GitHub 上的这个 CSV 导入数据。我们使用 Pandas 将两列分离为X 和Y 数据集。然后,我们将LinearRegression
模型拟合到输入的X 数据和输出的Y 数据。然后我们可以得到描述我们拟合线性函数的m 和b 系数。
在图中,您将确实看到一条拟合线穿过这些点,如图 5-5 所示。
图 5-5. SciPy 将拟合一条回归线到您的数据
是什么决定了最佳拟合线到这些点?让我们接下来讨论这个问题。
残差和平方误差
统计工具如 scikit-learn 如何得出适合这些点的线?这归结为机器学习训练中的两个基本问题:
什么定义了“最佳拟合”?
我们如何得到那个“最佳拟合”呢?
第一个问题有一个相当确定的答案:我们最小化平方,或更具体地说是平方残差的和。让我们来详细解释一下。画出任何一条穿过点的线。残差 是线和点之间的数值差异,如图 5-6 所示。
图 5-6. 残差是线和点之间的差异
线上方的点将具有正残差,而线下方的点将具有负残差。换句话说,它是预测 y 值(来自线)与实际 y 值(来自数据)之间的减去差异。残差的另一个名称是误差 ,因为它反映了我们的线在预测数据方面有多么错误。
让我们计算这 10 个点与线y = 1.93939x + 4.73333 之间的差异,以及示例 5-2 中每个点的残差和示例 5-3。
示例 5-2. 计算给定线和数据的残差
import pandas as pd
points = pd.read_csv('https://bit.ly/3goOAnt' , delimiter="," ).itertuples()
m = 1.93939
b = 4.73333
for p in points:
y_actual = p.y
y_predict = m*p.x + b
residual = y_actual - y_predict
print (residual)
示例 5-3. 每个点的残差
-1.67272
1.3878900000000005
-0.5515000000000008
2.5091099999999997
-0.4302799999999998
-1.3696699999999993 f
0.6909400000000012
-2.2484499999999983
2.812160000000002
-1.1272299999999973
如果我们要通过我们的 10 个数据点拟合一条直线,我们很可能希望尽可能地减小这些残差,使线和点之间的间隙尽可能小。但是我们如何衡量“总体”呢?最好的方法是采用平方和 ,简单地对每个残差进行平方,或者将每个残差相乘,然后将它们求和。我们取每个实际的 y 值,并从中减去从线上取得的预测 y 值,然后对所有这些差异进行平方和。
一个直观的思考方式如图 5-7 所示,我们在每个残差上叠加一个正方形,每条边的长度都是残差。我们将所有这些正方形的面积相加,稍后我们将学习如何通过确定最佳m 和b 来找到我们可以实现的最小和。
图 5-7. 可视化平方和,即所有正方形的面积之和,其中每个正方形的边长等于残差
让我们修改我们在示例 5-4 中的代码来找到平方和。
示例 5-4. 计算给定直线和数据的平方和
import pandas as pd
points = pd.read_csv("https://bit.ly/2KF29Bd" ).itertuples()
m = 1.93939
b = 4.73333
sum_of_squares = 0.0
for p in points:
y_actual = p.y
y_predict = m*p.x + b
residual_squared = (y_predict - y_actual)**2
sum_of_squares += residual_squared
print ("sum of squares = {}" .format (sum_of_squares))
下一个问题是:如何找到能产生最小平方和的m 和b 值,而不使用像 scikit-learn 这样的库?让我们接着看。
寻找最佳拟合直线
现在我们有一种方法来衡量给定直线与数据点的质量:平方和。我们能够使这个数字越低,拟合就越好。那么如何找到能产生最小 平方和的正确m 和b 值呢?
我们可以采用几种搜索算法,试图找到解决给定问题的正确值集。你可以尝试蛮力 方法,随机生成m 和b 值数百万次,并选择产生最小平方和的值。这种方法效果不佳,因为即使找到一个体面的近似值也需要无尽的时间。我们需要一些更有指导性的东西。我将为你整理五种技术:闭式方程、矩阵求逆、矩阵分解、梯度下降和随机梯度下降。还有其他搜索算法,比如爬山算法(在附录 A 中有介绍),但我们将坚持使用常见的方法。
闭式方程
一些读者可能会问是否有一个公式(称为闭式方程 )通过精确计算来拟合线性回归。答案是肯定的,但仅适用于只有一个输入变量的简单线性回归。对于具有多个输入变量和大量数据的许多机器学习问题,这种奢侈是不存在的。我们可以使用线性代数技术进行扩展,我们很快将讨论这一点。我们还将借此机会学习诸如随机梯度下降之类的搜索算法。
对于只有一个输入和一个输出变量的简单线性回归,以下是计算m 和b 的闭式方程。示例 5-5 展示了如何在 Python 中进行这些计算。
m = n ∑ x y - ∑ x ∑ y n ∑ x 2 - ( ∑ x ) 2 b = ∑ y n - m ∑ x n
示例 5-5. 计算简单线性回归的m 和b
import pandas as pd
points = list (pd.read_csv('https://bit.ly/2KF29Bd' , delimiter="," ).itertuples())
n = len (points)
m = (n*sum (p.x*p.y for p in points) - sum (p.x for p in points) *
sum (p.y for p in points)) / (n*sum (p.x**2 for p in points) -
sum (p.x for p in points)**2 )
b = (sum (p.y for p in points) / n) - m * sum (p.x for p in points) / n
print (m, b)
这些用于计算m 和b 的方程式是从微积分中推导出来的,如果您有兴趣发现公式的来源,我们稍后在本章中将使用 SymPy 进行一些微积分工作。目前,您可以插入数据点数n ,并迭代 x 和 y 值来执行刚才描述的操作。
今后,我们将学习更适用于处理大量数据的现代技术。闭式方程式往往不适用于大规模应用。
计算复杂度
闭式方程式不适用于较大数据集的原因是由于计算机科学中称为计算复杂度 的概念,它衡量算法在问题规模增长时所需的时间。这可能值得熟悉一下;以下是关于这个主题的两个很棒的 YouTube 视频:
逆矩阵技术
今后,我有时会用不同的名称交替使用系数m 和b ,分别为β 1 和β 0 ,这是您在专业世界中经常看到的惯例,所以现在可能是一个毕业的好时机。
虽然我们在第四章中专门致力于线性代数,但在您刚接触数学和数据科学时,应用它可能有点令人不知所措。这就是为什么本书中的大多数示例将使用纯 Python 或 scikit-learn。但是,在合适的情况下,我会加入线性代数,以展示线性代数的实用性。如果您觉得这一部分令人不知所措,请随时继续阅读本章的其余部分,稍后再回来。
我们可以使用转置和逆矩阵,我们在第四章中介绍过,来拟合线性回归。接下来,我们根据输入变量值矩阵X 和输出变量值向量y 计算系数向量b 。在不深入微积分和线性代数证明的兔子洞中,这是公式:
b = ( X T · X ) - 1 · X T · y
你会注意到在矩阵X 上执行了转置和逆操作,并与矩阵乘法结合。这是我们在 NumPy 中执行此操作的方式,在例子 5-6 中得到我们的系数m 和b 。
例 5-6. 使用逆矩阵和转置矩阵拟合线性回归
import pandas as pd
from numpy.linalg import inv
import numpy as np
df = pd.read_csv('https://bit.ly/3goOAnt' , delimiter="," )
X = df.values[:, :-1 ].flatten()
X_1 = np.vstack([X, np.ones(len (X))]).T
Y = df.values[:, -1 ]
b = inv(X_1.transpose() @ X_1) @ (X_1.transpose() @ Y)
print (b)
y_predict = X_1.dot(b)
这并不直观,但请注意我们必须在X 列旁边堆叠一个“列”为 1 的列。原因是这将生成截距β 0 系数。由于这一列全为 1,它实际上生成了截距而不仅仅是一个斜率β 1 。
当你有大量数据和大量维度时,计算机可能开始崩溃并产生不稳定的结果。这是矩阵分解的一个用例,我们在线性代数的第四章中学到了。在这种特定情况下,我们取我们的矩阵X ,附加一个额外的列 1 来生成截距β 0 就像以前一样,然后将其分解为两个组件矩阵Q 和R :
X = Q · R
避免更多的微积分兔子洞,这里是我们如何使用Q 和R 来在矩阵形式b 中找到 beta 系数值:
b = R - 1 · Q T · y
而例子 5-7 展示了我们如何在 Python 中使用 NumPy 使用前述QR 分解公式执行线性回归。
例 5-7. 使用 QR 分解执行线性回归
import pandas as pd
from numpy.linalg import qr, inv
import numpy as np
df = pd.read_csv('https://bit.ly/3goOAnt' , delimiter="," )
X = df.values[:, :-1 ].flatten()
X_1 = np.vstack([X, np.ones(len (X))]).transpose()
Y = df.values[:, -1 ]
Q, R = qr(X_1)
b = inv(R).dot(Q.transpose()).dot(Y)
print (b)
通常,QR 分解是许多科学库用于线性回归的方法,因为它更容易处理大量数据,并且更稳定。我所说的稳定 是什么意思?数值稳定性 是算法保持错误最小化的能力,而不是在近似中放大错误。请记住,计算机只能工作到某个小数位数,并且必须进行近似,因此我们的算法不应随着这些近似中的复合错误而恶化变得重要。
感到不知所措吗?
如果你觉得这些线性代数示例中的线性回归让人不知所措,不要担心!我只是想提供一个线性代数实际用例的曝光。接下来,我们将专注于其他你可以使用的技术。
梯度下降
梯度下降 是一种优化技术,利用导数和迭代来最小化/最大化一组参数以达到目标。要了解梯度下降,让我们进行一个快速的思想实验,然后在一个简单的例子中应用它。
关于梯度下降的思想实验
想象一下,你在一个山脉中夜晚拿着手电筒。你试图到达山脉的最低点。在你迈出一步之前,你可以看到你周围的斜坡。你朝着斜坡明显向下的方向迈步。对于更大的斜坡,你迈出更大的步伐,对于更小的斜坡,你迈出更小的步伐。最终,你会发现自己在一个斜率为 0 的低点,一个值为 0。听起来不错,对吧?这种使用手电筒的方法被称为梯度下降 ,我们朝着斜坡向下的方向迈步。
在机器学习中,我们经常将我们将遇到的所有可能的平方损失总和视为多山的地形。我们想要最小化我们的损失,并且我们通过导航损失地形来实现这一点。为了解决这个问题,梯度下降有一个吸引人的特点:偏导数就像是那盏手电筒,让我们能够看到每个参数(在这种情况下是m 和b ,或者β 0 和β 1 )的斜率。我们朝着m 和b 的斜率向下的方向迈步。对于更大的斜率,我们迈出更大的步伐,对于更小的斜率,我们迈出更小的步伐。我们可以通过取斜率的一部分来简单地计算这一步的长度。这一部分被称为我们的学习率 。学习率越高,它运行得越快,但精度会受到影响。但学习率越低,训练所需的时间就越长,需要更多的迭代。
决定学习率就像在选择蚂蚁、人类或巨人来踏下斜坡。蚂蚁(小学习率)会迈出微小的步伐,花费不可接受的长时间才能到达底部,但会准确无误地到达。巨人(大学习率)可能会一直跨过最小值,以至于无论走多少步都可能永远无法到达。人类(适度学习率)可能具有最平衡的步幅,在速度和准确性之间找到正确的平衡,以到达最小值。
先学会走再学会跑
对于函数f ( x ) = ( x - 3 ) 2 + 4 ,让我们找到产生该函数最低点的 x 值。虽然我们可以通过代数方法解决这个问题,但让我们使用梯度下降来做。
这是我们试图做的可视化效果。如图 5-8 所示,我们希望“步进”x 朝向斜率为 0 的最小值。
图 5-8. 朝向斜率接近 0 的局部最小值迈进
在示例 5-8 中,函数f(x)
及其对x 的导数为dx_f(x)
。回想一下,我们在第一章中讨论了如何使用 SymPy 计算导数。找到导数后,我们继续执行梯度下降。
示例 5-8. 使用梯度下降找到抛物线的最小值
import random
def f (x ):
return (x - 3 ) ** 2 + 4
def dx_f (x ):
return 2 *(x - 3 )
L = 0.001
iterations = 100_000
x = random.randint(-15 ,15 )
for i in range (iterations):
d_x = dx_f(x)
x -= L * d_x
print (x, f(x))
如果我们绘制函数(如图 5-8 所示),我们应该看到函数的最低点明显在x = 3 处,前面的代码应该非常接近这个点。学习率用于在每次迭代中取斜率的一部分并从 x 值中减去它。较大的斜率将导致较大的步长,而较小的斜率将导致较小的步长。经过足够的迭代,x 将最终到达函数的最低点(或足够接近),其中斜率为 0。
梯度下降和线性回归
现在你可能想知道我们如何将其用于线性回归。嗯,这个想法是一样的,只是我们的“变量”是m 和b (或β 0 和β 1 )而不是x 。原因在于:在简单线性回归中,我们已经知道 x 和 y 值,因为这些值作为训练数据提供。我们需要解决的“变量”实际上是参数m 和b ,因此我们可以找到最佳拟合线,然后接受一个x 变量来预测一个新的 y 值。
我们如何计算m 和b 的斜率?我们需要这两者的偏导数。我们要对哪个函数求导?记住我们试图最小化损失,这将是平方和。因此,我们需要找到我们的平方和函数对m 和b 的导数。
我像示例 5-9 中所示实现了这两个m 和b 的偏导数。我们很快将学习如何在 SymPy 中执行此操作。然后我执行梯度下降来找到m 和b :100,000 次迭代,学习率为 0.001 就足够了。请注意,您将学习率设得越小,速度就越慢,需要的迭代次数就越多。但如果设得太高,它将运行得很快,但近似度较差。当有人说机器学习算法正在“学习”或“训练”时,实际上就是在拟合这样一个回归。
示例 5-9. 执行线性回归的梯度下降
import pandas as pd
points = list (pd.read_csv("https://bit.ly/2KF29Bd" ).itertuples())
m = 0.0
b = 0.0
L = .001
iterations = 100_000
n = float (len (points))
for i in range (iterations):
D_m = sum (2 * p.x * ((m * p.x + b) - p.y) for p in points)
D_b = sum (2 * ((m * p.x + b) - p.y) for p in points)
m -= L * D_m
b -= L * D_b
print ("y = {0}x + {1}" .format (m, b))
嗯,不错!那个近似值接近我们的闭式方程解。但有什么问题吗?仅仅因为我们通过最小化平方和找到了“最佳拟合直线”,这并不意味着我们的线性回归就很好。最小化平方和是否保证了一个很好的模型来进行预测?并不完全是这样。现在我向你展示了如何拟合线性回归,让我们退一步,重新审视全局,确定给定的线性回归是否是首选的预测方式。但在我们这样做之前,这里有一个展示 SymPy 解决方案的更多绕路。
使用 SymPy 进行线性回归的梯度下降
如果你想要得到这两个关于平方和函数的导数的 SymPy 代码,分别为 m 和 b ,这里是 示例 5-10 中的代码。
示例 5-10. 计算 m 和 b 的偏导数
from sympy import *
m, b, i, n = symbols('m b i n' )
x, y = symbols('x y' , cls=Function)
sum_of_squares = Sum((m*x(i) + b - y(i)) ** 2 , (i, 0 , n))
d_m = diff(sum_of_squares, m)
d_b = diff(sum_of_squares, b)
print (d_m)
print (d_b)
你会看到分别为 m 和 b 的两个导数被打印出来。请注意 Sum()
函数将迭代并将项相加(在这种情况下是所有数据点),我们将 x 和 y 视为查找给定索引 i 处值的函数。
在数学符号中,其中 e (x ) 代表平方和损失函数,这里是 m 和 b 的偏导数:
e ( x ) = ∑ i = 0 n ( ( m x i + b ) - y i ) 2 d d m e ( x ) = ∑ i = 0 n 2 ( b + m x i - y i ) x i d d b e ( x ) = ∑ i = 0 n ( 2 b + 2 m x i - 2 y i )
如果你想应用我们的数据集并使用梯度下降执行线性回归,你将需要执行一些额外的步骤,如示例 5-11 所示。我们需要替换n
、x(i)
和y(i)
的值,迭代所有数据点以计算d_m
和d_b
的导数函数。这样就只剩下m
和b
变量,我们将使用梯度下降寻找最优值。
示例 5-11. 使用 SymPy 解决线性回归
import pandas as pd
from sympy import *
points = list (pd.read_csv("https://bit.ly/2KF29Bd" ).itertuples())
m, b, i, n = symbols('m b i n' )
x, y = symbols('x y' , cls=Function)
sum_of_squares = Sum((m*x(i) + b - y(i)) ** 2 , (i, 0 , n))
d_m = diff(sum_of_squares, m) \
.subs(n, len (points) - 1 ).doit() \
.replace(x, lambda i: points[i].x) \
.replace(y, lambda i: points[i].y)
d_b = diff(sum_of_squares, b) \
.subs(n, len (points) - 1 ).doit() \
.replace(x, lambda i: points[i].x) \
.replace(y, lambda i: points[i].y)
d_m = lambdify([m, b], d_m)
d_b = lambdify([m, b], d_b)
m = 0.0
b = 0.0
L = .001
iterations = 100_000
for i in range (iterations):
m -= d_m(m,b) * L
b -= d_b(m,b) * L
print ("y = {0}x + {1}" .format (m, b))
如示例 5-11 所示,对我们的偏导数函数都调用lambdify()
是个好主意,将它们从 SymPy 转换为优化的 Python 函数。这将使计算在执行梯度下降时更快。生成的 Python 函数由 NumPy、SciPy 或 SymPy 检测到的其他数值库支持。之后,我们可以执行梯度下降。
最后,如果你对这个简单线性回归的损失函数感兴趣,示例 5-12 展示了 SymPy 代码,将x
、y
和n
的值代入我们的损失函数,然后将m
和b
作为输入变量绘制出来。我们的梯度下降算法将我们带到了损失景观中的最低点,如图 5-9 所示。
示例 5-12. 绘制线性回归的损失函数
from sympy import *
from sympy.plotting import plot3d
import pandas as pd
points = list (pd.read_csv("https://bit.ly/2KF29Bd" ).itertuples())
m, b, i, n = symbols('m b i n' )
x, y = symbols('x y' , cls=Function)
sum_of_squares = Sum((m*x(i) + b - y(i)) ** 2 , (i, 0 , n)) \
.subs(n, len (points) - 1 ).doit() \
.replace(x, lambda i: points[i].x) \
.replace(y, lambda i: points[i].y)
plot3d(sum_of_squares)
图 5-9. 简单线性回归的损失景观
过拟合和方差
你猜猜看:如果我们真的想最小化损失,即将平方和减少到 0,我们会怎么做?除了线性回归还有其他选择吗?你可能得出的一个结论就是简单地拟合一个触及所有点的曲线。嘿,为什么不只是连接点并用它来做预测,如图 5-10 所示?这样就得到了 0 的损失!
真糟糕,为什么我们要费力进行线性回归而不是做这个呢?嗯,记住我们的大局目标不是最小化平方和,而是在新数据上做出准确的预测。这种连接点模型严重过拟合 ,意味着它将回归形状调整得太精确到预测新数据时表现糟糕。这种简单的连接点模型对远离其他点的异常值敏感,意味着它在预测中具有很高的方差 。虽然这个例子中的点相对接近一条直线,但在其他具有更广泛分布和异常值的数据集中,这个问题会更严重。因为过拟合增加了方差,预测结果将会到处都是!
图 5-10. 通过简单连接点执行回归,导致损失为零
过拟合就是记忆
当有人说回归“记住”了数据而不是泛化它时,他们在谈论过拟合。
正如你所猜测的,我们希望在模型中找到有效的泛化,而不是记忆数据。否则,我们的回归模型简单地变成了一个数据库,我们只是查找数值。
在机器学习中,你会发现模型中添加了偏差,而线性回归被认为是一个高度偏置的模型。这与数据中的偏差不同,我们在第三章中有详细讨论。模型中的偏差 意味着我们优先考虑一种方法(例如,保持一条直线),而不是弯曲和完全适应数据。一个有偏差的模型留有一些余地,希望在新数据上最小化损失以获得更好的预测,而不是在训练数据上最小化损失。我想你可以说,向模型添加偏差可以抵消过拟合 ,或者说对训练数据拟合较少。
你可以想象,这是一个平衡的过程,因为这是两个相互矛盾的目标。在机器学习中,我们基本上是在说,“我想要将回归拟合到我的数据,但我不想拟合得太多 。我需要一些余地来预测新数据的不同之处。”
套索回归和岭回归
线性回归的两个比较流行的变体是套索回归和岭回归。岭回归在线性回归中添加了进一步的偏差,以一种惩罚的形式,因此导致它对数据拟合较少。套索回归将尝试边缘化嘈杂的变量,这在你想要自动删除可能不相关的变量时非常有用。
然而,我们不能仅仅将线性回归应用于一些数据,进行一些预测,并假设一切都没问题。即使是一条直线的线性回归也可能过拟合。因此,我们需要检查和缓解过拟合和欠拟合,以找到两者之间的平衡点。除非根本没有平衡点,否则你应该完全放弃该模型。
随机梯度下降
在机器学习的背景下,你不太可能像之前那样在实践中进行梯度下降,我们在所有训练数据上进行训练(称为批量梯度下降 )。在实践中,你更有可能执行随机梯度下降 ,它将在每次迭代中仅对数据集的一个样本进行训练。在小批量梯度下降 中,会使用数据集的多个样本(例如,10 或 100 个数据点)进行每次迭代。
为什么每次迭代只使用部分数据?机器学习从业者引用了一些好处。首先,它显著减少了计算量,因为每次迭代不必遍历整个训练数据集,而只需部分数据。第二个好处是减少过拟合。每次迭代只暴露训练算法于部分数据,使损失景观不断变化,因此不会稳定在损失最小值。毕竟,最小化损失是导致过拟合的原因,因此我们引入一些随机性来创建一点欠拟合(但希望不要太多)。
当然,我们的近似变得松散,所以我们必须小心。这就是为什么我们很快会谈论训练/测试拆分,以及其他评估我们线性回归可靠性的指标。
示例 5-13 展示了如何在 Python 中执行随机梯度下降。如果将样本大小改为大于 1,它将执行小批量梯度下降。
示例 5-13。执行线性回归的随机梯度下降
import pandas as pd
import numpy as np
data = pd.read_csv('https://bit.ly/2KF29Bd' , header=0 )
X = data.iloc[:, 0 ].values
Y = data.iloc[:, 1 ].values
n = data.shape[0 ]
m = 0.0
b = 0.0
sample_size = 1
L = .0001
epochs = 1_000_000
for i in range (epochs):
idx = np.random.choice(n, sample_size, replace=False )
x_sample = X[idx]
y_sample = Y[idx]
Y_pred = m * x_sample + b
D_m = (-2 / sample_size) * sum (x_sample * (y_sample - Y_pred))
D_b = (-2 / sample_size) * sum (y_sample - Y_pred)
m = m - L * D_m
b = b - L * D_b
if i % 10000 == 0 :
print (i, m, b)
print ("y = {0}x + {1}" .format (m, b))
当我运行这个时,我得到了一个线性回归 y = 1.9382830354181135x + 4.753408787648379。显然,你的结果会有所不同,由于随机梯度下降,我们实际上不会收敛到特定的最小值,而是会停留在一个更广泛的邻域。
随机性是坏事吗?
如果这种随机性让你感到不舒服,每次运行一段代码都会得到不同的答案,那么欢迎来到机器学习、优化和随机算法的世界!许多进行近似的算法都是基于随机性的,虽然有些非常有用,但有些可能效果不佳,正如你所预期的那样。
很多人把机器学习和人工智能看作是一种能够给出客观和精确答案的工具,但事实并非如此。机器学习产生的是带有一定不确定性的近似值,通常在生产中没有基本事实。如果不了解它的工作原理,机器学习可能会被滥用,不承认其非确定性和近似性质是不妥的。
虽然随机性可以创造一些强大的工具,但也可能被滥用。要小心不要使用种子值和随机性来 p-hack 一个“好”结果,并努力分析你的数据和模型。
相关系数
看看这个散点图 图 5-11 以及它的线性回归。为什么线性回归在这里效果不太好?
图 5-11。具有高方差的数据的散点图
这里的问题是数据具有很高的方差。如果数据极为分散,它将使方差增加到使预测变得不太准确和有用的程度,导致大的残差。当然,我们可以引入更偏向的模型,如线性回归,以不那么容易弯曲和响应方差。然而,欠拟合也会削弱我们的预测,因为数据如此分散。我们需要数值化地衡量我们的预测有多“偏离”。
那么如何对这些残差进行整体测量呢?你又如何了解数据中方差的糟糕程度呢?让我向你介绍相关系数 ,也称为皮尔逊相关系数 ,它以-1 到 1 之间的值来衡量两个变量之间关系的强度。相关系数越接近 0,表示没有相关性。相关系数越接近 1,表示强正相关 ,意味着一个变量增加时,另一个变量成比例增加。如果接近-1,则表示强负相关 ,这意味着一个变量增加时,另一个成比例减少。
请注意,相关系数通常表示为r 。在图 5-11 中高度分散的数据具有相关系数 0.1201。由于它比 1 更接近 0,我们可以推断数据之间关系很小。
这里是另外四个散点图,显示它们的相关系数。请注意,点越接近一条线,相关性越强。点更分散会导致相关性较弱。
图 5-12。四个散点图的相关系数
可以想象,相关系数对于查看两个变量之间是否存在可能的关系是有用的。如果存在强正负关系,它将对我们的线性回归有所帮助。如果没有关系,它们可能只会添加噪音并损害模型的准确性。
我们如何使用 Python 计算相关系数?让我们使用之前使用的简单10 点数据集 。分析所有变量对之间的相关性的快速简单方法是使用 Pandas 的corr()
函数。这使得轻松查看数据集中每对变量之间的相关系数,这种情况下只会是x
和y
。这被称为相关矩阵 。在示例 5-14 中查看。
示例 5-14。使用 Pandas 查看每对变量之间的相关系数
import pandas as pd
df = pd.read_csv('https://bit.ly/2KF29Bd' , delimiter="," )
correlations = df.corr(method='pearson' )
print (correlations)
正如您所看到的,x
和y
之间的相关系数0.957586
表明这两个变量之间存在强烈的正相关性。您可以忽略矩阵中x
或y
设置为自身且值为1.0
的部分。显然,当x
或y
设置为自身时,相关性将完美地为 1.0,因为值与自身完全匹配。当您有两个以上的变量时,相关性矩阵将显示更大的网格,因为有更多的变量进行配对和比较。
如果您更改代码以使用具有大量变化的不同数据集,其中数据分散,您将看到相关系数下降。这再次表明了较弱的相关性。
统计显著性
这里还有线性回归的另一个方面需要考虑:我的数据相关性是否巧合?在第三章中,我们研究了假设检验和 p 值,我们将在这里用线性回归扩展这些想法。
让我们从一个基本问题开始:我是否可能由于随机机会在我的数据中看到线性关系?我们如何能够确信这两个变量之间的相关性是显著的而不是巧合的 95%?如果这听起来像第三章中的假设检验,那是因为它就是!我们不仅需要表达相关系数,还需要量化我们对相关系数不是偶然发生的信心。
与我们在第三章中使用药物测试示例中所做的估计均值不同,我们正在基于样本估计总体相关系数。我们用希腊字母符号ρ (Rho)表示总体相关系数,而我们的样本相关系数是r 。就像我们在第三章中所做的那样,我们将有一个零假设H 0 和备择假设H 1 :
H 0 : ρ = 0 (意味着 没有 关系) H 1 : ρ ≠ 0 (关系 存在)
我们的零假设H 0 是两个变量之间没有关系,或更技术性地说,相关系数为 0。备择假设H 1 是存在关系,可以是正相关或负相关。这就是为什么备择假设被定义为ρ ≠ 0 ,以支持正相关和负相关。
让我们回到我们的包含 10 个点的数据集,如图 5-13 所示。我们看到这些数据点是多大概率是偶然看到的?它们恰好产生了看起来是线性关系?
图 5-13. 这些数据看起来具有线性相关性,我们有多大可能性是随机机会看到的?
我们已经在示例 5-14 中计算了这个数据集的相关系数为 0.957586。这是一个强有力的正相关。但是,我们需要评估这是否是由于随机运气。让我们以 95%的置信度进行双尾检验,探讨这两个变量之间是否存在关系。
我们在第三章中讨论了 T 分布,它有更厚的尾部以捕捉更多的方差和不确定性。我们使用 T 分布而不是正态分布进行线性回归的假设检验。首先,让我们绘制一个 T 分布,95%的临界值范围如图 5-14 所示。考虑到我们的样本中有 10 条记录,因此我们有 9 个自由度(10-1=9)。
图 5-14. 9 个自由度的 T 分布,因为有 10 条记录,我们减去 1
临界值约为±2.262,我们可以在 Python 中计算如示例 5-16 所示。这捕捉了我们 T 分布中心区域的 95%。
示例 5-16. 从 T 分布计算临界值
from scipy.stats import t
n = 10
lower_cv = t(n-1 ).ppf(.025 )
upper_cv = t(n-1 ).ppf(.975 )
print (lower_cv, upper_cv)
如果我们的检验值恰好落在(-2.262,2.262)的范围之外,那么我们可以拒绝我们的零假设。要计算检验值t ,我们需要使用以下公式。再次,r 是相关系数,n 是样本大小:
t = r 1 - r 2 n - 2 t = .957586 1 - .957586 2 10 - 2 = 9.339956
让我们在 Python 中将整个测试放在一起,如示例 5-17 所示。如果我们的检验值落在 95%置信度的临界范围之外,我们接受我们的相关性不是偶然的。
示例 5-17. 测试看起来线性的数据的显著性
from scipy.stats import t
from math import sqrt
n = 10
lower_cv = t(n-1 ).ppf(.025 )
upper_cv = t(n-1 ).ppf(.975 )
r = 0.957586
test_value = r / sqrt((1 -r**2 ) / (n-2 ))
print ("TEST VALUE: {}" .format (test_value))
print ("CRITICAL RANGE: {}, {}" .format (lower_cv, upper_cv))
if test_value < lower_cv or test_value > upper_cv:
print ("CORRELATION PROVEN, REJECT H0" )
else :
print ("CORRELATION NOT PROVEN, FAILED TO REJECT H0 " )
if test_value > 0 :
p_value = 1.0 - t(n-1 ).cdf(test_value)
else :
p_value = t(n-1 ).cdf(test_value)
p_value = p_value * 2
print ("P-VALUE: {}" .format (p_value))
这里的检验值约为 9.39956,明显超出了(-2.262,2.262)的范围,因此我们可以拒绝零假设,并说我们的相关性是真实的。这是因为 p 值非常显著:0.000005976。这远低于我们的 0.05 阈值,因此这几乎不是巧合:存在相关性。p 值如此之小是有道理的,因为这些点强烈地类似于一条线。这些点随机地如此靠近一条线的可能性极小。
图 5-15 展示了一些其他数据集及其相关系数和 p 值。分析每一个。哪一个可能对预测最有用?其他数据集存在什么问题?
图 5-15。不同数据集及其相关系数和 p 值
现在你有机会对来自图 5-15 的数据集进行分析后,让我们来看看结果。左侧图具有很高的正相关性,但只有三个数据点。数据不足显著提高了 p 值,达到 0.34913,并增加了数据发生偶然性的可能性。这是有道理的,因为只有三个数据点很可能会看到一个线性模式,但这并不比只有两个点好,这两个点只会连接一条直线。这提出了一个重要的规则:拥有更多数据将降低你的 p 值,特别是如果这些数据趋向于一条线。
第二幅图就是我们刚刚讨论的内容。它只有 10 个数据点,但形成了一个线性模式,我们不仅有很强的正相关性,而且 p 值极低。当 p 值如此之低时,你可以确定你正在测量一个经过精心设计和严格控制的过程,而不是某种社会学或自然现象。
图 5-15 中右侧的两幅图未能确定线性关系。它们的相关系数接近于 0,表明没有相关性,而 p 值不出所料地表明随机性起了作用。
规则是这样的:拥有更多数据且一致地类似于一条线,你的相关性的 p 值就会更显著。数据越分散或稀疏,p 值就会增加,从而表明你的相关性是由随机机会引起的。
决定系数
让我们学习一个在统计学和机器学习回归中经常见到的重要指标。决定系数 ,称为r 2 ,衡量一个变量的变异有多少是由另一个变量的变异解释的。它也是相关系数r 的平方。当r 接近完美相关(-1 或 1)时,r 2 接近 1。基本上,r 2 显示了两个变量相互作用的程度。
让我们继续查看我们从图 5-13 中的数据。在示例 5-18 中,使用我们之前计算相关系数的数据框代码,然后简单地对其进行平方。这将使每个相关系数相互乘以自己。
示例 5-18。在 Pandas 中创建相关性矩阵
import pandas as pd
df = pd.read_csv('https://bit.ly/2KF29Bd' , delimiter="," )
coeff_determination = df.corr(method='pearson' ) ** 2
print (coeff_determination)
决定系数为 0.916971 被解释为x 的变异的 91.6971%由y (反之亦然)解释,剩下的 8.3029%是由其他未捕获的变量引起的噪音;0.916971 是一个相当不错的决定系数,显示x 和y 解释彼此的方差。但可能有其他变量在起作用,占据了剩下的 0.083029。记住,相关性不等于因果关系,因此可能有其他变量导致我们看到的关系。
相关性不代表因果关系!
需要注意的是,虽然我们非常强调测量相关性并围绕其构建指标,请记住相关性不代表因果关系 !你可能以前听过这句口头禅,但我想扩展一下统计学家为什么这么说。
仅仅因为我们看到x 和y 之间的相关性,并不意味着x 导致y 。实际上可能是y 导致x !或者可能存在第三个未捕获的变量z 导致x 和y 。也可能x 和y 根本不相互导致,相关性只是巧合,因此我们测量统计显著性非常重要。
现在我有一个更紧迫的问题要问你。计算机能区分相关性和因果关系吗?答案是“绝对不!”计算机有相关性的概念,但没有因果关系。假设我加载一个数据集到 scikit-learn,显示消耗的水量和我的水费。我的计算机,或包括 scikit-learn 在内的任何程序,都不知道更多的用水量是否导致更高的账单,或更高的账单是否导致更多的用水量。人工智能系统很容易得出后者的结论,尽管这是荒谬的。这就是为什么许多机器学习项目需要一个人来注入常识。
在计算机视觉中,这也会发生。计算机视觉通常会对数字像素进行回归以预测一个类别。如果我训练一个计算机视觉系统来识别牛,使用的是牛的图片,它可能会轻易地将场地与牛进行关联。因此,如果我展示一张空旷的场地的图片,它会将草地标记为牛!这同样是因为计算机没有因果关系的概念(牛的形状应该导致标签“牛”),而是陷入了我们不感兴趣的相关性中。
估计的标准误差
衡量线性回归整体误差的一种方法是SSE ,或者平方误差和 。我们之前学过这个概念,其中我们对每个残差进行平方并求和。如果y ^ (读作“y-hat”)是线上的每个预测值,而y 代表数据中的每个实际 y 值,这里是计算公式:
S S E = ∑ ( y - y ^ ) 2
然而,所有这些平方值很难解释,所以我们可以使用一些平方根逻辑将事物重新缩放到它们的原始单位。我们还将所有这些值求平均,这就是估计的标准误差(S e ) 的作用。如果n 是数据点的数量,示例 5-19 展示了我们如何在 Python 中计算标准误差S e 。
S e = ∑ ( y - y ^ ) 2 n - 2
示例 5-19。计算估计的标准误差
Here is how we calculate it in Python:
import pandas as pd
from math import sqrt
points = list (pd.read_csv('https://bit.ly/2KF29Bd' , delimiter="," ).itertuples())
n = len (points)
m = 1.939
b = 4.733
S_e = sqrt((sum ((p.y - (m*p.x +b))**2 for p in points))/(n-2 ))
print (S_e)
为什么是n - 2 而不是像我们在第三章中的许多方差计算中所做的n - 1 ?不深入数学证明,这是因为线性回归有两个变量,而不只是一个,所以我们必须在自由度中再增加一个不确定性。
你会注意到估计的标准误差看起来与我们在第三章中学习的标准差非常相似。这并非偶然。这是因为它是线性回归的标准差。
预测区间
正如前面提到的,线性回归中的数据是从一个总体中取样得到的。因此,我们的回归结果只能和我们的样本一样好。我们的线性回归线也沿着正态分布运行。实际上,这使得每个预测的 y 值都像均值一样是一个样本统计量。事实上,“均值”沿着这条线移动。
还记得我们在第二章中讨论方差和标准差吗?这些概念在这里也适用。通过线性回归,我们希望数据以线性方式遵循正态分布。回归线充当我们钟形曲线的“均值”,数据围绕该线的分布反映了方差/标准差,如图 5-16 所示。
图 5-16。线性回归假设正态分布遵循该线
当我们有一个正态分布遵循线性回归线时,我们不仅有一个变量,还有第二个变量引导着分布。每个y 预测周围都有一个置信区间,这被称为预测区间 。
让我们通过兽医示例重新带回一些背景,估计一只狗的年龄和兽医访问次数。我想知道一个 8.5 岁狗的兽医访问次数的 95%置信度的预测区间。这个预测区间的样子如图 5-17 所示。我们有 95%的信心,一个 8.5 岁的狗将有 16.462 到 25.966 次兽医访问。
图 5-17。一个 8.5 岁狗的 95%置信度的预测区间
我们如何计算这个?我们需要得到误差边界,并在预测的 y 值周围加减。这是一个涉及 T 分布的临界值以及估计标准误差的庞大方程。让我们来看一下:
E = t .025 S e 1 + 1 n + n ( x 0 + x ¯ ) 2 n ( ∑ x 2 ) - ( ∑ x ) 2
我们感兴趣的 x 值被指定为x 0 ,在这种情况下是 8.5。这是我们如何在 Python 中解决这个问题的,如示例 5-20 所示。
示例 5-20。计算一只 8.5 岁狗的兽医访问预测区间
import pandas as pd
from scipy.stats import t
from math import sqrt
points = list (pd.read_csv('https://bit.ly/2KF29Bd' , delimiter="," ).itertuples())
n = len (points)
m = 1.939
b = 4.733
x_0 = 8.5
x_mean = sum (p.x for p in points) / len (points)
t_value = t(n - 2 ).ppf(.975 )
standard_error = sqrt(sum ((p.y - (m * p.x + b)) ** 2 for p in points) / (n - 2 ))
margin_of_error = t_value * standard_error * \
sqrt(1 + (1 / n) + (n * (x_0 - x_mean) ** 2 ) / \
(n * sum (p.x ** 2 for p in points) - \
sum (p.x for p in points) ** 2 ))
predicted_y = m*x_0 + b
print (predicted_y - margin_of_error, predicted_y + margin_of_error)
哎呀!这是很多计算,不幸的是,SciPy 和其他主流数据科学库都不会做这个。但如果你倾向于统计分析,这是非常有用的信息。我们不仅基于线性回归创建预测(例如,一只 8.5 岁的狗将有 21.2145 次兽医访问),而且实际上能够说出一些远非绝对的东西:一个 8.5 岁的狗会在 16.46 到 25.96 次之间访问兽医的概率为 95%。很棒,对吧?这是一个更安全的说法,因为它涵盖了一个范围而不是一个单一值,因此考虑了不确定性。
训练/测试分割
我刚刚进行的这个分析,包括相关系数、统计显著性和决定系数,不幸的是并不总是由从业者完成。有时候他们处理的数据太多,没有时间或技术能力这样做。例如,一个 128×128 像素的图像至少有 16,384 个变量。你有时间对每个像素变量进行统计分析吗?可能没有!不幸的是,这导致许多数据科学家根本不学习这些统计指标。
在一个不知名的在线论坛 上,我曾经看到一篇帖子说统计回归是手术刀,而机器学习是电锯。当处理大量数据和变量时,你无法用手术刀筛选所有这些。你必须求助于电锯,虽然你会失去可解释性和精度,但至少可以扩展到更多数据上进行更广泛的预测。话虽如此,抽样偏差和过拟合等统计问题并没有消失。但有一些实践方法可以用于快速验证。
为什么 scikit-learn 中没有置信区间和 P 值?
Scikit-learn 不支持置信区间和 P 值,因为这两种技术对于高维数据是一个悬而未决的问题。这只强调了统计学家和机器学习从业者之间的差距。正如 scikit-learn 的一位维护者 Gael Varoquaux 所说,“通常计算正确的 P 值需要对数据做出假设,而这些假设并不符合机器学习中使用的数据(没有多重共线性,与维度相比有足够的数据)....P 值是一种期望得到很好检查的东西(在医学研究中是一种保护)。实现它们会带来麻烦....我们只能在非常狭窄的情况下给出 P 值[有少量变量]。”
如果你想深入了解,GitHub 上有一些有趣的讨论:
如前所述,statsmodel 是一个为统计分析提供有用工具的库。只是要知道,由于前述原因,它可能不会适用于更大维度的模型。
机器学习从业者用于减少过拟合的基本技术之一是一种称为训练/测试分割 的实践,通常将 1/3 的数据用于测试,另外的 2/3 用于训练(也可以使用其他比例)。训练数据集 用于拟合线性回归,而测试数据集 用于衡量线性回归在之前未见数据上的表现。这种技术通常用于所有监督学习,包括逻辑回归和神经网络。图 5-18 显示了我们如何将数据分割为 2/3 用于训练和 1/3 用于测试的可视化。
这是一个小数据集
正如我们将在后面学到的,有其他方法可以将训练/测试数据集分割为 2/3 和 1/3。如果你有一个这么小的数据集,你可能最好使用 9/10 和 1/10 与交叉验证配对,或者甚至只使用留一交叉验证。查看“训练/测试分割是否必须是三分之一?” 以了解更多。
图 5-18. 将数据分割为训练/测试数据—使用最小二乘法将线拟合到训练数据(深蓝色),然后分析测试数据(浅红色)以查看预测在之前未见数据上的偏差
示例 5-21 展示了如何使用 scikit-learn 执行训练/测试分割,其中 1/3 的数据用于测试,另外的 2/3 用于训练。
训练即拟合回归
记住,“拟合”回归与“训练”是同义词。后者是机器学习从业者使用的词语。
示例 5-21. 在线性回归上进行训练/测试分割
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
df = pd.read_csv('https://bit.ly/3cIH97A' , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=1 /3 )
model = LinearRegression()
model.fit(X_train, Y_train)
result = model.score(X_test, Y_test)
print ("r²: %.3f" % result)
注意,train_test_split()
将获取我们的数据集(X 和 Y 列),对其进行洗牌,然后根据我们的测试数据集大小返回我们的训练和测试数据集。我们使用LinearRegression
的fit()
函数来拟合训练数据集X_train
和Y_train
。然后我们使用score()
函数在测试数据集X_test
和Y_test
上评估r 2 ,从而让我们了解回归在之前未见过的数据上的表现。测试数据集的r 2 值越高,表示回归在之前未见过的数据上表现越好。具有更高数值的数字表示回归在之前未见过的数据上表现良好。
我们还可以在每个 1/3 折叠中交替使用测试数据集。这被称为交叉验证 ,通常被认为是验证技术的黄金标准。图 5-20 显示了数据的每个 1/3 轮流成为测试数据集。
图 5-20. 三折交叉验证的可视化
示例 5-22 中的代码展示了跨三个折叠进行的交叉验证,然后评分指标(在本例中为均方和 [MSE])与其标准偏差一起平均,以展示每个测试的一致性表现。
示例 5-22. 使用三折交叉验证进行线性回归
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import KFold, cross_val_score
df = pd.read_csv('https://bit.ly/3cIH97A' , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
kfold = KFold(n_splits=3 , random_state=7 , shuffle=True )
model = LinearRegression()
results = cross_val_score(model, X, Y, cv=kfold)
print (results)
print ("MSE: mean=%.3f (stdev-%.3f)" % (results.mean(), results.std()))
当你开始关注模型中的方差时,你可以采用随机折叠验证 ,而不是简单的训练/测试拆分或交叉验证,重复地对数据进行洗牌和训练/测试拆分无限次,并汇总测试结果。在示例 5-23 中,有 10 次随机抽取数据的 1/3 进行测试,其余 2/3 进行训练。然后将这 10 个测试结果与它们的标准偏差平均,以查看测试数据集的表现一致性。
有什么问题?这在计算上非常昂贵,因为我们要多次训练回归。
示例 5-23. 使用随机折叠验证进行线性回归
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score, ShuffleSplit
df = pd.read_csv('https://bit.ly/38XwbeB' , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
kfold = ShuffleSplit(n_splits=10 , test_size=.33 , random_state=7 )
model = LinearRegression()
results = cross_val_score(model, X, Y, cv=kfold)
print (results)
print ("mean=%.3f (stdev-%.3f)" % (results.mean(), results.std()))
因此,当你时间紧迫或数据量过大无法进行统计分析时,训练/测试拆分将提供一种衡量线性回归在未见过的数据上表现如何的方法。
训练/测试拆分并不保证结果
值得注意的是,仅仅因为你应用了机器学习的最佳实践,将训练和测试数据拆分,这并不意味着你的模型会表现良好。你很容易过度调整模型,并通过一些手段获得良好的测试结果,但最终发现在现实世界中并不奏效。这就是为什么有时候需要保留另一个数据集,称为验证集 ,特别是当你在比较不同模型或配置时。这样,你对训练数据的调整以获得更好的测试数据性能不会泄漏信息到训练中。你可以使用验证数据集作为最后一道防线,查看是否过度调整导致你对测试数据过拟合。
即使如此,你的整个数据集(包括训练、测试和验证)可能一开始就存在偏差,没有任何拆分可以减轻这种情况。Andrew Ng 在他与 DeepLearning.AI 和 Stanford HAI 的问答环节 中讨论了这个问题,他通过一个例子说明了为什么机器学习尚未取代放射科医生。
多元线性回归
在本章中,我们几乎完全专注于对一个输入变量和一个输出变量进行线性回归。然而,我们在这里学到的概念应该基本适用于多变量线性回归。像r 2 、标准误差和置信区间等指标可以使用,但随着变量的增加变得更加困难。示例 5-24 是一个使用 scikit-learn 进行的具有两个输入变量和一个输出变量的线性回归示例。
示例 5-24。具有两个输入变量的线性回归
import pandas as pd
from sklearn.linear_model import LinearRegression
df = pd.read_csv('https://bit.ly/2X1HWH7' , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
fit = LinearRegression().fit(X, Y)
print ("Coefficients = {0}" .format (fit.coef_))
print ("Intercept = {0}" .format (fit.intercept_))
print ("z = {0} + {1}x + {2}y" .format (fit.intercept_, fit.coef_[0 ], fit.coef_[1 ]))
当模型变得充斥着变量以至于开始失去可解释性时,就会出现一定程度的不稳定性,这时机器学习实践开始将模型视为黑匣子。希望你相信统计问题并没有消失,随着添加的变量越来越多,数据变得越来越稀疏。但是,如果你退后一步,使用相关矩阵分析每对变量之间的关系,并寻求理解每对变量是如何相互作用的,这将有助于你努力创建一个高效的机器学习模型。
结论
在这一章中我们涵盖了很多内容。我们试图超越对线性回归的肤浅理解,并且不仅仅将训练/测试分割作为我们唯一的验证方式。我想向你展示割刀(统计学)和电锯(机器学习),这样你可以判断哪种对于你遇到的问题更好。仅在线性回归中就有许多指标和分析方法可用,我们涵盖了其中一些以了解线性回归是否可靠用于预测。你可能会发现自己处于一种情况,要么做出广泛的近似回归,要么使用统计工具仔细分析和整理数据。你使用哪种方法取决于情况,如果你想了解更多关于 Python 可用的统计工具,请查看statsmodel 库 。
在第六章中涵盖逻辑回归时,我们将重新审视r 2 和统计显著性。希望这一章能让你相信有方法可以有意义地分析数据,并且这种投资可以在成功的项目中产生差异。
练习
提供了一个包含两个变量x 和y 的数据集这里 。
执行简单的线性回归,找到最小化损失(平方和)的m 和b 值。
计算这些数据的相关系数和统计显著性(95%置信度)。相关性是否有用?
如果我预测x =50,那么y 的预测值的 95%预测区间是多少?
重新开始你的回归,并进行训练/测试分割。随意尝试交叉验证和随机折叠验证。线性回归在测试数据上表现良好且一致吗?为什么?
答案在附录 B 中。
第六章:逻辑回归和分类
在本章中,我们将介绍逻辑回归 ,一种根据一个或多个自变量预测结果概率的回归类型。这反过来可以用于分类 ,即预测类别而不是像线性回归那样预测实数。
我们并不总是对将变量表示为连续 感兴趣,其中它们可以表示无限数量的实数十进制值。有些情况下,我们更希望变量是离散 的,或者代表整数、布尔值(1/0,真/假)。逻辑回归是在一个离散的输出变量上进行训练的(二进制 1 或 0)或一个分类数字(整数)。它输出一个概率的连续变量,但可以通过阈值转换为离散值。
逻辑回归易于实现,并且相对抗干扰和其他数据挑战。许多机器学习问题最好通过逻辑回归来解决,提供比其他类型的监督式机器学习更实用和更高性能的解决方案。
就像我们在第五章中讨论线性回归时所做的那样,我们将尝试在统计学和机器学习之间找到平衡,使用两个学科的工具和分析。逻辑回归将整合我们从本书中学到的许多概念,从概率到线性回归。
理解逻辑回归
想象一下,发生了一起小型工业事故,你正在尝试了解化学物质暴露的影响。有 11 名患者暴露于不同小时数的化学物质中(请注意这是虚构数据)。一些患者出现了症状(值为 1),而另一些没有出现症状(值为 0)。让我们在图 6-1 中绘制它们,其中 x 轴是暴露的小时数,y 轴是他们是否出现了症状(1 或 0)。
图 6-1。绘制患者在* x *小时暴露后是否出现症状(1)或未出现症状(0)
患者在多长时间后开始出现症状?很容易看到,几乎在四小时后,我们立即从患者不出现症状(0)转变为出现症状(1)。在图 6-2 中,我们看到相同的数据带有一个预测曲线。
图 6-2。四小时后,我们看到患者开始出现症状的明显跳跃
对这个样本进行粗略分析,我们可以说,暴露时间少于四小时的患者几乎不可能出现症状,但暴露时间超过四小时的患者出现症状的概率为 100%。在这两组之间,大约在四小时左右立即跳跃到出现症状。
当然,在现实世界中,没有什么是如此清晰明了的。假设你收集了更多数据,在范围的中间有一些患者表现出症状与不表现症状的混合,如图 6-3 所示。
图 6-3. 中间存在一些表现出症状(1)和不表现症状(0)的患者混合
解释这一点的方式是,随着每小时的暴露,患者表现出症状的概率逐渐增加。让我们用一个逻辑函数 或一个 S 形曲线来可视化这一点,其中输出变量被挤压在 0 和 1 之间,如图 6-4 所示。
图 6-4. 将逻辑函数拟合到数据上
由于中间点的重叠,当患者表现出症状时没有明显的分界线,而是从 0%概率逐渐过渡到 100%概率(0 和 1)。这个例子展示了逻辑回归 如何产生一个曲线,表示属于真实类别(患者表现出症状)的概率在一个独立变量(暴露小时数)上。
我们可以重新利用逻辑回归,不仅预测给定输入变量的概率,还可以添加一个阈值来预测它是否属于该类别。例如,如果我得到一个新患者,并发现他们暴露了六个小时,我预测他们有 71.1%的机会表现出症状,如图 6-5 所示。如果我的阈值至少为 50%的概率表现出症状,我将简单地分类为患者将表现出症状。
图 6-5. 我们可以预期一个暴露了六个小时的患者有 71.1%的可能表现出症状,因为这大于 50%的阈值,我们预测他们将表现出症状
进行逻辑回归
那么我们如何进行逻辑回归呢?让我们首先看看逻辑函数,并探索其背后的数学。
逻辑函数
逻辑函数 是一个 S 形曲线(也称为sigmoid 曲线 ),对于给定的一组输入变量,产生一个在 0 和 1 之间的输出变量。因为输出变量在 0 和 1 之间,它可以用来表示概率。
这是一个输出一个输入变量x 的概率y 的逻辑函数:
y = 1.0 1.0 + e - ( β 0 + β 1 x )
请注意,这个公式使用了欧拉数e ,我们在第一章中讨论过。x 变量是独立/输入变量。β 0 和β 1 是我们需要解决的系数。
β 0 和 β 1 被打包在一个类似于线性函数的指数中,你可能会记得它看起来与 y = m x + b 或 y = β 0 + β 1 x 相同。这并非巧合;逻辑回归实际上与线性回归有着密切的关系,我们将在本章后面讨论。实际上,β 0 是截距(在简单线性回归中我们称之为b ),β 1 是x 的斜率(在简单线性回归中我们称之为m )。指数中的这个线性函数被称为对数几率函数,但现在只需知道整个逻辑函数产生了我们需要在 x 值上输出移动概率的 S 形曲线。
要在 Python 中声明逻辑函数,使用math
包中的exp()
函数声明e 指数,如示例 6-1 所示。
示例 6-1. Python 中用于一个自变量的逻辑函数
import math
def predict_probability (x, b0, b1 ):
p = 1.0 / (1.0 + math.exp(-(b0 + b1 * x)))
return p
让我们绘制一下看看它是什么样子,并假设Β [0] = –2.823 和 Β [1] = 0.62。我们将在示例 6-2 中使用 SymPy,输出图形显示在图 6-6 中。
示例 6-2. 使用 SymPy 绘制逻辑函数
from sympy import *
b0, b1, x = symbols('b0 b1 x' )
p = 1.0 / (1.0 + exp(-(b0 + b1 * x)))
p = p.subs(b0,-2.823 )
p = p.subs(b1, 0.620 )
print (p)
plot(p)
图 6-6. 一个逻辑函数
在一些教科书中,你可能会看到逻辑函数被这样声明:
p = e β 0 + β 1 x 1 + e β 0 + β 1 x
不要为此担心,因为这是相同的函数,只是代数上表达不同。注意,像线性回归一样,我们也可以将逻辑回归扩展到多于一个输入变量(x 1 , x 2 , . . . x n ),如此公式所示。我们只需添加更多的β x 系数:
p = 1 1 + e - ( β 0 + β 1 x 1 + β 2 x 2 + . . . β n x n )
拟合逻辑曲线
如何将逻辑曲线拟合到给定的训练数据集?首先,数据可以包含任意混合的十进制、整数和二进制变量,但输出变量必须是二进制(0 或 1)。当我们实际进行预测时,输出变量将在 0 和 1 之间,类似于概率。
数据提供了我们的输入和输出变量值,但我们需要解出β 0 和β 1 系数以拟合我们的逻辑函数。回想一下我们在 Chapter 5 中如何使用最小二乘法。然而,在这里不适用这种方法。相反,我们使用最大似然估计 ,顾名思义,最大化给定逻辑曲线输出观测数据的可能性。
要计算最大似然估计,实际上没有像线性回归那样的封闭形式方程。我们仍然可以使用梯度下降,或者让一个库来为我们做这件事。让我们从库 SciPy 开始涵盖这两种方法。
使用 SciPy
SciPy 的好处在于,模型通常具有一套标准化的函数和 API,这意味着在许多情况下,您可以复制/粘贴您的代码,然后在模型之间重复使用它。在 Example 6-3 中,您将看到我们的患者数据上执行的逻辑回归。如果您将其与我们在 Chapter 5 中的线性回归代码进行比较,您将看到在导入、分离和拟合数据方面几乎完全相同的代码。主要区别在于我使用LogisticRegression()
作为我的模型,而不是LinearRegression()
。
示例 6-3。在 SciPy 中使用普通逻辑回归
import pandas as pd
from sklearn.linear_model import LogisticRegression
df = pd.read_csv('https://bit.ly/33ebs2R' , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
model = LogisticRegression(penalty='none' )
model.fit(X, Y)
print (model.coef_.flatten())
print (model.intercept_.flatten())
在 SciPy 中运行模型后,我得到一个逻辑回归,其中β [0] = –3.17576395,β [1] = 0.69267212。当我绘制这个图时,应该看起来很好,就像在 Figure 6-7 中显示的那样。
图 6-7。绘制逻辑回归
这里有几件事情需要注意。当我创建LogisticRegression()
模型时,我没有指定penalty
参数,这会选择像l1
或l2
这样的正则化技术。虽然这超出了本书的范围,但我在以下注释“学习关于 SciPy 参数”中包含了简要见解,以便您手边有有用的参考资料。
最后,我将flatten()
系数和截距,这些系数和截距出来时是多维矩阵,但只有一个元素。Flattening 意味着将一组数字的矩阵折叠成较小的维度,特别是当元素少于维度时。例如,我在这里使用flatten()
来将嵌套在二维矩阵中的单个数字提取出来作为单个值。然后我有了我的β 0 和β 1 系数。
学习关于 SciPy 参数
SciPy 在其回归和分类模型中提供了许多选项。不幸的是,由于这不是一本专门关注机器学习的书籍,没有足够的带宽或页面来覆盖它们。
然而,SciPy 文档写得很好,逻辑回归页面在这里 找到。
如果很多术语都很陌生,比如正则化和l1
和l2
惩罚,还有其他很棒的 O’Reilly 书籍探讨这些主题。我发现其中一本更有帮助的书是由 Aurélien Géron 撰写的Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow 。
使用最大似然和梯度下降
正如我在整本书中所做的,我旨在提供关于从头开始构建技术的见解,即使库可以为我们完成。有几种方法可以自己拟合逻辑回归,但所有方法通常都转向最大似然估计(MLE)。MLE 最大化了给定逻辑曲线输出观测数据的可能性。这与平方和不同,但我们仍然可以应用梯度下降或随机梯度下降来解决它。
我会尽量简化数学术语,并尽量减少线性代数的内容。基本上,这个想法是找到使我们的逻辑曲线尽可能接近这些点的β 0 和β 1 系数,表明它最有可能产生这些点。如果你还记得第二章中我们学习概率时,我们通过将多个事件的概率(或可能性)相乘来组合它们。在这个应用中,我们正在计算我们会看到所有这些点的可能性,对于给定的逻辑回归曲线。
应用联合概率的概念,每个患者都有一个基于拟合的逻辑函数的可能性,如图 6-8 所示。
图 6-8. 每个输入值在逻辑曲线上都有相应的可能性
我们从逻辑回归曲线上方或下方获取每个点的可能性。如果点在逻辑回归曲线下方,我们需要从 1.0 中减去结果概率,因为我们也想最大化假阳性。
给定系数β [0] = –3.17576395 和β [1] = 0.69267212,示例 6-4 展示了我们如何在 Python 中计算这些数据的联合概率。
示例 6-4. 计算给定逻辑回归观察到所有点的联合概率
import math
import pandas as pd
patient_data = pd.read_csv('https://bit.ly/33ebs2R' , delimiter="," ).itertuples()
b0 = -3.17576395
b1 = 0.69267212
def logistic_function (x ):
p = 1.0 / (1.0 + math.exp(-(b0 + b1 * x)))
return p
joint_likelihood = 1.0
for p in patient_data:
if p.y == 1.0 :
joint_likelihood *= logistic_function(p.x)
elif p.y == 0.0 :
joint_likelihood *= (1.0 - logistic_function(p.x))
print (joint_likelihood)
这里有一个数学技巧,我们可以用来压缩那个if
表达式。正如我们在第一章中讨论的,当你将任何数的幂设为 0 时,结果总是 1。看看这个公式,并注意指数中对真(1)和假(0)情况的处理:
joint likelihood = ∏ i = 1 n ( 1.0 1.0 + e - ( β 0 + β 1 x i ) ) y i × ( 1.0 1.0 + e - ( β 0 + β 1 x i ) ) 1.0 - y i
要在 Python 中实现这一点,将for
循环中的所有内容压缩到示例 6-5 中。
示例 6-5. 在不使用if
表达式的情况下压缩联合概率计算
for p in patient_data:
joint_likelihood *= logistic_function(p.x) ** p.y * \
(1.0 - logistic_function(p.x)) ** (1.0 - p.y)
我到底做了什么?注意这个表达式有两个部分,一个是当y = 1 时,另一个是当y = 0 时。当任何数被提升到指数 0 时,结果将为 1。因此,无论y 是 1 还是 0,它都会导致另一侧的条件评估为 1 并且在乘法中没有影响。我们可以用数学表达式完全表达我们的if
表达式。我们无法对使用if
的表达式进行导数,所以这将很有帮助。
请注意,计算机可能会因为乘以多个小小数而不堪重负,这被称为浮点下溢 。这意味着随着小数变得越来越小,可能会在乘法中发生,计算机在跟踪那么多小数位数时会遇到限制。有一个巧妙的数学技巧可以解决这个问题。你可以对要相乘的每个小数取log()
,然后将它们相加。这要归功于我们在第一章中介绍的对数的加法性质。这样更加数值稳定,然后你可以调用exp()
函数将总和转换回来得到乘积。
让我们修改我们的代码,使用对数加法而不是乘法(参见示例 6-6)。请注意,log()
函数默认为基数e ,虽然任何基数在技术上都可以工作,但这是首选,因为e x 是其自身的导数,计算上更有效率。
示例 6-6. 使用对数加法
joint_likelihood = 0.0
for p in patient_data:
joint_likelihood += math.log(logistic_function(p.x) ** p.y * \
(1.0 - logistic_function(p.x)) ** (1.0 - p.y))
joint_likelihood = math.exp(joint_likelihood)
要用数学符号表示前面的 Python 代码:
joint likelihood = ∑ i = 1 n log ( ( 1.0 1.0 + e - ( β 0 + β 1 x i ) ) y i × ( 1.0 - 1.0 1.0 + e - ( β 0 + β 1 x i ) ) 1.0 - y i )
你想要计算前述表达式中的β 0 和β 1 的偏导数吗?我觉得不会。这太复杂了。天哪,在 SymPy 中表达那个函数本身就是一大口水!看看示例 6-7 中的内容。
示例 6-7. 在 SymPy 中表达逻辑回归的联合似然
joint_likelihood = Sum(log((1.0 / (1.0 + exp(-(b + m * x(i)))))**y(i) * \
(1.0 - (1.0 / (1.0 + exp(-(b + m * x(i))))))**(1 -y(i))), (i, 0 , n))
所以让我们让 SymPy 为我们做β 0 和β 1 的偏导数。然后我们将立即编译并使用它们进行梯度下降,如示例 6-8 所示。
示例 6-8. 在逻辑回归上使用梯度下降
from sympy import *
import pandas as pd
points = list (pd.read_csv("https://tinyurl.com/y2cocoo7" ).itertuples())
b1, b0, i, n = symbols('b1 b0 i n' )
x, y = symbols('x y' , cls=Function)
joint_likelihood = Sum(log((1.0 / (1.0 + exp(-(b0 + b1 * x(i))))) ** y(i) \
* (1.0 - (1.0 / (1.0 + exp(-(b0 + b1 * x(i)))))) ** (1 - y(i))), (i, 0 , n))
d_b1 = diff(joint_likelihood, b1) \
.subs(n, len (points) - 1 ).doit() \
.replace(x, lambda i: points[i].x) \
.replace(y, lambda i: points[i].y)
d_b0 = diff(joint_likelihood, b0) \
.subs(n, len (points) - 1 ).doit() \
.replace(x, lambda i: points[i].x) \
.replace(y, lambda i: points[i].y)
d_b1 = lambdify([b1, b0], d_b1)
d_b0 = lambdify([b1, b0], d_b0)
b1 = 0.01
b0 = 0.01
L = .01
for j in range (10_000 ):
b1 += d_b1(b1, b0) * L
b0 += d_b0(b1, b0) * L
print (b1, b0)
在计算β 0 和β 1 的偏导数后,我们将 x 值和 y 值以及数据点数n 代入。然后我们使用lambdify()
来编译导数函数以提高效率(它在幕后使用 NumPy)。之后,我们执行梯度下降,就像我们在第五章中所做的那样,但由于我们试图最大化而不是最小化,我们将每次调整添加到β 0 和β 1 中,而不是像最小二乘法中那样减去。
如您在示例 6-8 中所看到的,我们得到了β [0] = –3.17575 和β [1] = 0.692667。这与我们之前在 SciPy 中得到的系数值非常相似。
正如我们在第五章中学到的那样,我们也可以使用随机梯度下降,每次迭代只对一个或少数几个记录进行采样。这将延伸增加计算速度和性能的好处,同时防止过拟合。在这里再次覆盖将是多余的,所以我们将继续前进。
多变量逻辑回归
让我们尝试一个使用多个输入变量进行逻辑回归的示例。表 6-1 展示了一个虚构数据集中一些就业保留数据的样本(完整数据集在这里 )。
表 6-1。就业保留数据样本
性别
年龄
晋升次数
工龄
是否离职
1
32
3
7
0
1
34
2
5
0
1
29
2
5
1
0
42
4
10
0
1
43
4
10
0
此数据集中有 54 条记录。假设我们想要用它来预测其他员工是否会离职,这里可以使用逻辑回归(尽管这不是一个好主意,稍后我会详细说明原因)。回想一下,我们可以支持多个输入变量,如下公式所示:
y = 1 1 + e - ( β 0 + β 1 x 1 + β 2 x 2 + . . . β n x n )
我将为每个变量sex
、age
、promotions
和years_employed
创建β 系数。输出变量did_quit
是二进制的,这将驱动我们正在预测的逻辑回归结果。因为我们处理多个维度,所以很难可视化我们的逻辑曲线所代表的曲线超平面。因此,我们将避免可视化。
让我们来点有趣的。我们将使用 scikit-learn,但创建一个交互式 shell,我们可以用来测试员工。示例 6-9 展示了代码,当我们运行它时,将执行逻辑回归,然后我们可以输入新员工以预测他们是否会离职。会出什么问题呢?我相信没有。我们只是根据人们的个人属性进行预测并做出相应决策。我相信一切都会好的。
(如果不清楚,我是在开玩笑)。
示例 6-9。对员工数据进行多变量逻辑回归
import pandas as pd
from sklearn.linear_model import LogisticRegression
employee_data = pd.read_csv("https://tinyurl.com/y6r7qjrp" )
inputs = employee_data.iloc[:, :-1 ]
output = employee_data.iloc[:, -1 ]
fit = LogisticRegression(penalty='none' ).fit(inputs, output)
print ("COEFFICIENTS: {0}" .format (fit.coef_.flatten()))
print ("INTERCEPT: {0}" .format (fit.intercept_.flatten()))
def predict_employee_will_stay (sex, age, promotions, years_employed ):
prediction = fit.predict([[sex, age, promotions, years_employed]])
probabilities = fit.predict_proba([[sex, age, promotions, years_employed]])
if prediction == [[1 ]]:
return "WILL LEAVE: {0}" .format (probabilities)
else :
return "WILL STAY: {0}" .format (probabilities)
while True :
n = input ("Predict employee will stay or leave {sex},
{age},{promotions},{years employed}: " )
(sex, age, promotions, years_employed) = n.split("," )
print (predict_employee_will_stay(int (sex), int (age), int (promotions),
int (years_employed)))
图 6-9 显示了员工是否被预测会离职的结果。员工的性别为“1”,年龄为 34 岁,晋升 1 次,公司工作了 5 年。果然,预测是“将离开”。
图 6-9。预测 34 岁员工,1 次晋升和 5 年工作经验是否会离职
请注意,predict_proba()
函数将输出两个值,第一个是 0(假)的概率,第二个是 1(真)的概率。
您会注意到sex
、age
、promotions
和years_employed
的系数按照这个顺序显示。通过系数的权重,您可以看到sex
和age
在预测中起到很小的作用(它们的权重接近 0)。然而,promotions
和years_employed
具有显著的权重分别为-2.504
和0.97
。这个玩具数据集的一个秘密是,如果员工每两年没有晋升就会离职,我制造了这个模式,我的逻辑回归确实捕捉到了这个模式,您也可以尝试对其他员工进行测试。然而,如果您超出了它训练的数据范围,预测可能会开始失效(例如,如果放入一个 70 岁的员工,三年没有晋升,很难说这个模型会做出什么,因为它没有关于那个年龄的数据)。
当然,现实生活并不总是如此干净。一个在公司工作了八年,从未晋升过的员工很可能对自己的角色感到满意,不会很快离开。如果是这种情况,年龄等变量可能会发挥作用并被赋予权重。然后当然我们可能会担心其他未被捕捉到的相关变量。查看以下警告以了解更多信息。
谨慎对人进行分类!
一个快速而肯定地让自己陷入困境的方法是收集关于人们的数据,并随意地用它来做预测。不仅可能引发数据隐私问题,还可能出现法律和公关问题,如果模型被发现具有歧视性。像种族和性别这样的输入变量可能在机器学习训练中被赋予权重。之后,这些人口统计学数据可能会导致不良结果,比如不被录用或被拒绝贷款。更极端的应用包括被监视系统错误标记或被拒绝刑事假释。还要注意,看似无害的变量,比如通勤时间,已被发现与歧视性变量相关联。
在撰写本文时,已有多篇文章引用机器学习歧视作为一个问题:
随着数据隐私法律的不断发展,谨慎处理个人数据是明智的。考虑自动决策将如何传播以及如何造成伤害。有时最好的做法是让“问题”保持原样,继续手动处理。
最后,在这个员工留存的例子中,想想这些数据是从哪里来的。是的,我虚构了这个数据集,但在现实世界中,你总是要质疑数据是如何生成的。这个样本是从多长时间内得出的?我们要回溯多久来寻找已经离职的员工?什么构成了留下的员工?他们现在是当前员工吗?我们怎么知道他们不会马上离职,从而成为一个假阴性?数据科学家很容易陷入分析数据所说的内容,但不质疑数据来源和内置的假设。
获取这些问题的答案的最佳方法是了解预测用途。是用来决定何时提升人员以留住他们吗?这会产生一种循环偏见,促使具有一组属性的人员晋升吗?当这些晋升开始成为新的训练数据时,这种偏见会得到确认吗?
这些都是重要的问题,甚至可能是令人不快的问题,会导致不必要的范围渗入项目中。如果你的团队或领导不欢迎对项目进行这种审查,考虑让自己担任一个不同的角色,让好奇心成为一种优势。
理解对数几率
此时,是时候讨论逻辑回归及其数学构成了。这可能有点令人眩晕,所以在这里要花点时间。如果你感到不知所措,随时可以稍后回顾这一部分。
从 20 世纪开始,数学家一直对将线性函数的输出缩放到 0 和 1 之间感兴趣,因此对于预测概率是有用的。对数几率,也称为对数函数,适用于逻辑回归的这一目的。
记得之前我指出指数值 β 0 + β 1 x 是一个线性函数吗?再看看我们的逻辑函数:
p = 1.0 1.0 + e - ( β 0 + β 1 x )
这个被提升到 e 的线性函数被称为 对数几率 函数,它取得了感兴趣事件的对数几率。你可能会说,“等等,我看不到 log()
或几率。我只看到一个线性函数!” 请耐心等待,我会展示隐藏的数学。
举个例子,让我们使用之前的逻辑回归,其中Β [0] = -3.17576395,Β [1] = 0.69267212。在六小时后出现症状的概率是多少,其中x = 6 ?我们已经知道如何做到这一点:将这些值代入我们的逻辑函数:
p = 1.0 1.0 + e - ( - 3.17576395 + 0.69267212 ( 6 ) ) = 0.727161542928554
我们将这些值代入并输出概率为 0.72716。但让我们从赔率的角度来看这个问题。回想一下,在第二章中我们学习了如何从概率计算赔率:
赔率 = p 1 - p 赔率 = .72716 1 - .72716 = 2.66517246407876
因此,在六小时时,患者出现症状的可能性是不出现症状的 2.66517 倍。
当我们将赔率函数包装在一个自然对数(以e 为底的对数)中时,我们称之为对数几率函数 。这个公式的输出就是我们所说的对数几率 ,之所以这样命名...令人震惊...是因为我们取了赔率的对数:
对数几率 = log ( p 1 - p ) 对数几率 = log ( .72716 1 - .72716 ) = 0.98026877
我们在六小时时的对数几率为 0.9802687。这意味着什么,为什么我们要关心呢?当我们处于“对数几率领域”时,比较一组赔率相对容易。我们将大于 0 的任何值视为支持事件发生的赔率,而小于 0 的任何值则反对事件发生。对数几率为-1.05 与 0 的距离与 1.05 相同。然而,在普通赔率中,这些等价值分别为 0.3499 和 2.857,这并不容易解释。这就是对数几率的便利之处。
赔率和对数
对数和赔率有着有趣的关系。当赔率在 0.0 和 1.0 之间时,事件是不利的,但大于 1.0 的任何值都支持事件,并延伸到正无穷。这种缺乏对称性很尴尬。然而,对数重新调整了赔率,使其完全线性,其中对数几率为 0.0 表示公平的赔率。对数几率为-1.05 与 0 的距离与 1.05 相同,因此比较赔率更容易。
Josh Starmer 有一个很棒的视频 讲述了赔率和对数之间的关系。
记得我说过我们逻辑回归公式中的线性函数β 0 + β 1 x 是我们的对数几率函数。看看这个:
log-odds = β 0 + β 1 x log-odds = - 3.17576395 + 0.69267212 ( 6 ) log-odds = 0.98026877
这与我们先前计算的值 0.98026877 相同,即在x = 6 时逻辑回归的赔率,然后取其log()
!那么这之间有什么联系?是什么将所有这些联系在一起?给定逻辑回归p 的概率和输入变量x ,就是这样:
l o g ( p 1 - p ) = β 0 + β 1 x
让我们将对数几率线与逻辑回归一起绘制,如图 6-10 所示。
图 6-10。对数几率线转换为输出概率的逻辑函数
每个逻辑回归实际上都由一个线性函数支持,而该线性函数是一个对数几率函数。请注意,在图 6-10 中,当对数几率在线上为 0.0 时,逻辑曲线的概率为 0.5。这是有道理的,因为当我们的赔率为 1.0 时,概率将为 0.50,如逻辑回归所示,而对数几率将为 0,如线所示。
从赔率的角度看逻辑回归的另一个好处是我们可以比较一个 x 值和另一个 x 值之间的效果。假设我想了解暴露于化学物质六小时和八小时之间我的赔率变化有多大。我可以取六小时和八小时的赔率,然后将两个赔率相对于彼此进行比较,得到一个赔率比 。这不应与普通赔率混淆,是的,它是一个比率,但不是赔率比。
让我们首先找出分别为六小时和八小时的症状概率:
p = 1.0 1.0 + e - ( β 0 + β 1 x ) p 6 = 1.0 1.0 + e - ( - 3.17576395 + 0.69267212 ( 6 ) ) = 0.727161542928554 p 8 = 1.0 1.0 + e - ( - 3.17576395 + 0.69267212 ( 8 ) ) = 0.914167258137741
现在让我们将这些转换为几率,我们将其声明为o x :
o = p 1 - p o 6 = 0.727161542928554 1 - 0.727161542928554 = 2.66517246407876 o 8 = 0.914167258137741 1 - 0.914167258137741 = 10.6505657200694
最后,将两个几率相互对比作为一个几率比值,其中 8 小时的几率为分子,6 小时的几率为分母。我们得到一个约为 3.996 的数值,这意味着我们的症状出现的几率随着额外两小时的暴露增加了近四倍:
odds ratio = 10.6505657200694 2.66517246407876 = 3.99620132040906
你会发现这个几率比值为 3.996 的数值在任何两小时范围内都成立,比如 2 小时到 4 小时,4 小时到 6 小时,8 小时到 10 小时等等。只要是两小时的间隔,你会发现几率比值保持一致。对于其他范围长度,它会有所不同。
R-平方
我们在第五章中已经涵盖了线性回归的许多统计指标,我们将尝试对 logistic 回归做同样的事情。我们仍然担心许多与线性回归相同的问题,包括过拟合和方差。事实上,我们可以借鉴并调整几个线性回归的指标,并将它们应用于 logistic 回归。让我们从R 2 开始。
就像线性回归一样,给定 logistic 回归也有一个R 2 。如果你回忆一下第五章,R 2 表示一个给定自变量解释因变量的程度。将这应用于我们的化学暴露问题,我们想要衡量化学暴露小时数解释症状出现的程度是有意义的。
实际上并没有关于如何计算 logistic 回归的R 2 的最佳方法的共识,但一种被称为麦克法登伪R 2 的流行技术紧密模仿了线性回归中使用的R 2 。我们将在以下示例中使用这种技术,以下是公式:
R 2 = ( log likelihood ) - ( log likelihood fit ) ( log likelihood )
我们将学习如何计算“对数似然拟合”和“对数似然”,以便计算R 2 。
我们不能像线性回归那样在这里使用残差,但我们可以将结果投影回逻辑曲线,如图 6-11 所示,并查找它们在 0.0 和 1.0 之间的相应可能性。
图 6-11. 将输出值投影回逻辑曲线
然后我们可以取这些可能性的log()
并将它们相加。这将是拟合的对数似然(示例 6-10)。就像我们计算最大似然一样,我们通过从 1.0 中减去“假”可能性来转换“假”可能性。
示例 6-10. 计算拟合的对数似然
from math import log, exp
import pandas as pd
patient_data = pd.read_csv('https://bit.ly/33ebs2R' , delimiter="," ).itertuples()
b0 = -3.17576395
b1 = 0.69267212
def logistic_function (x ):
p = 1.0 / (1.0 + exp(-(b0 + b1 * x)))
return p
log_likelihood_fit = 0.0
for p in patient_data:
if p.y == 1.0 :
log_likelihood_fit += log(logistic_function(p.x))
elif p.y == 0.0 :
log_likelihood_fit += log(1.0 - logistic_function(p.x))
print (log_likelihood_fit)
使用一些巧妙的二进制乘法和 Python 推导,我们可以将那个for
循环和if
表达式整合成一行,返回log_likelihood_fit
。类似于我们在最大似然公式中所做的,我们可以使用一些真假病例之间的二进制减法来在数学上消除其中一个。在这种情况下,我们乘以 0,因此相应地将真或假情况应用于总和(示例 6-11)。
示例 6-11. 将我们的对数似然逻辑整合成一行
log_likelihood_fit = sum (log(logistic_function(p.x)) * p.y +
log(1.0 - logistic_function(p.x)) * (1.0 - p.y)
for p in patient_data)
如果我们要用数学符号来表达拟合的可能性,它会是这个样子。注意f ( x i ) 是给定输入变量x i 的逻辑函数:
log likelihood fit = ∑ i = 1 n ( log ( f ( x i ) ) × y i ) + ( log ( 1.0 - f ( x i ) ) × ( 1 - y i ) )
如在示例 6-10 和 6-11 中计算的,我们的拟合对数似然为-9.9461。我们需要另一个数据点来计算R 2 :即估计不使用任何输入变量,只使用真实病例数除以所有病例数(实际上只留下截距)。请注意,我们可以通过将所有 y 值相加∑ y i 来计算症状病例的数量,因为只有 1 会计入总和,而 0 不会。这是公式:
log likelihood = ∑ y i n × y i + ( 1 - ∑ y i n ) × ( 1 - y i )
这是应用于示例 6-12 中的公式的 Python 等效展开。
示例 6-12. 患者的对数似然
import pandas as pd
from math import log, exp
patient_data = list (pd.read_csv('https://bit.ly/33ebs2R' , delimiter="," ) \
.itertuples())
likelihood = sum (p.y for p in patient_data) / len (patient_data)
log_likelihood = 0.0
for p in patient_data:
if p.y == 1.0 :
log_likelihood += log(likelihood)
elif p.y == 0.0 :
log_likelihood += log(1.0 - likelihood)
print (log_likelihood)
为了整合这个逻辑并反映公式,我们可以将那个for
循环和if
表达式压缩成一行,使用一些二进制乘法逻辑来处理真假病例(示例 6-13)。
示例 6-13. 将对数似然整合成一行
log_likelihood = sum (log(likelihood)*p.y + log(1.0 - likelihood)*(1.0 - p.y) \
for p in patient_data)
最后,只需将这些值代入并获得你的R 2 :
R 2 = (对数似然) - (拟合对数似然) ( 对数似然 ) R 2 = - 0.5596 - ( - 9.9461 ) - 0.5596 R 2 = 0.306456
下面是 Python 代码,显示在示例 6-14 中,完整计算R 2 。
示例 6-14. 计算逻辑回归的R 2
import pandas as pd
from math import log, exp
patient_data = list (pd.read_csv('https://bit.ly/33ebs2R' , delimiter="," ) \
.itertuples())
b0 = -3.17576395
b1 = 0.69267212
def logistic_function (x ):
p = 1.0 / (1.0 + exp(-(b0 + b1 * x)))
return p
log_likelihood_fit = sum (log(logistic_function(p.x)) * p.y +
log(1.0 - logistic_function(p.x)) * (1.0 - p.y)
for p in patient_data)
likelihood = sum (p.y for p in patient_data) / len (patient_data)
log_likelihood = sum (log(likelihood) * p.y + log(1.0 - likelihood) * (1.0 - p.y) \
for p in patient_data)
r2 = (log_likelihood - log_likelihood_fit) / log_likelihood
print (r2)
好的,所以我们得到一个R 2 = 0.306456,那么化学暴露时间是否能解释某人是否出现症状?正如我们在第五章中学到的线性回归一样,拟合不好的情况下R 2 会接近 0.0,而拟合较好的情况下会接近 1.0。因此,我们可以得出结论,暴露时间对于预测症状是一般般的,因为R 2 为 0.30645。除了时间暴露之外,肯定还有其他变量更好地预测某人是否会出现症状。这是有道理的,因为我们观察到的大多数数据中,有很多患者出现症状和没有出现症状的混合,如图 6-12 所示。
图 6-12. 我们的数据中间有很多方差,因此我们的数据有一个中等的R 2 为 0.30645
但是如果我们的数据中有一个明确的分界线,其中 1 和 0 的结果被清晰地分开,如图 6-13 所示,我们将有一个完美的R 2 为 1.0。
图 6-13. 这个逻辑回归有一个完美的R 2 为 1.0,因为根据暴露时间预测的结果有一个清晰的分界线
P-值
就像线性回归一样,我们并不因为有一个R 2 就结束了。我们需要调查我们看到这些数据是因为偶然还是因为实际关系的可能性。这意味着我们需要一个 p 值。
为了做到这一点,我们需要学习一个称为卡方分布 的新概率分布,标记为χ 2 分布。它是连续的,在统计学的几个领域中使用,包括这个!
如果我们取标准正态分布中的每个值(均值为 0,标准差为 1)并平方,那将给我们一个自由度为 1 的χ 2 分布。对于我们的目的,自由度将取决于我们逻辑回归中有多少参数n ,这将是n - 1 。你可以在图 6-14 中看到不同自由度的例子。
图 6-14。不同自由度的χ 2 分布
由于我们有两个参数(暴露时间和是否出现症状),我们的自由度将为 1,因为2 - 1 = 1 。
我们需要在前一小节关于 R²中计算的对数似然拟合和对数似然。这是产生我们需要查找的χ 2 值的公式:
χ 2 = 2 ( log likelihood fit ) - ( log likelihood )
然后我们取该值并从χ 2 分布中查找概率。这将给我们我们的 p 值:
p-value = chi ( 2 ( ( log likelihood fit ) - ( log likelihood ) )
示例 6-15 展示了我们拟合的逻辑回归的 p 值。我们使用 SciPy 的chi2
模块使用卡方分布。
示例 6-15。计算给定逻辑回归的 p 值
import pandas as pd
from math import log, exp
from scipy.stats import chi2
patient_data = list (pd.read_csv('https://bit.ly/33ebs2R' , delimiter="," ).itertuples())
b0 = -3.17576395
b1 = 0.69267212
def logistic_function (x ):
p = 1.0 / (1.0 + exp(-(b0 + b1 * x)))
return p
log_likelihood_fit = sum (log(logistic_function(p.x)) * p.y +
log(1.0 - logistic_function(p.x)) * (1.0 - p.y)
for p in patient_data)
likelihood = sum (p.y for p in patient_data) / len (patient_data)
log_likelihood = sum (log(likelihood) * p.y + log(1.0 - likelihood) * (1.0 - p.y) \
for p in patient_data)
chi2_input = 2 * (log_likelihood_fit - log_likelihood)
p_value = chi2.pdf(chi2_input, 1 )
print (p_value)
所以我们有一个 p 值为 0.00166,如果我们的显著性阈值为 0.05,我们说这些数据在统计上是显著的,不是随机事件。
训练/测试拆分
如第五章中所述,我们可以使用训练/测试拆分来验证机器学习算法。这是评估逻辑回归性能的更机器学习方法。虽然依赖传统统计指标如R 2 和 p 值是个好主意,但当你处理更多变量时,这变得不太实际。这时再次用到了训练/测试拆分。回顾一下,图 6-15 展示了一个三折交叉验证交替测试数据集。
图 6-15。一个三折交叉验证,交替将数据集的每个第三部分作为测试数据集
在示例 6-16 中,我们对员工留存数据集执行逻辑回归,但将数据分成三部分。然后我们交替将每个部分作为测试数据。最后,我们用平均值和标准差总结三个准确性。
示例 6-16。执行带有三折交叉验证的逻辑回归
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import KFold, cross_val_score
df = pd.read_csv("https://tinyurl.com/y6r7qjrp" , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
kfold = KFold(n_splits=3 , random_state=7 , shuffle=True )
model = LogisticRegression(penalty='none' )
results = cross_val_score(model, X, Y, cv=kfold)
print ("Accuracy Mean: %.3f (stdev=%.3f)" % (results.mean(), results.std()))
我们还可以使用随机折叠验证、留一交叉验证以及我们在第五章中执行的所有其他折叠变体。说完这些,让我们谈谈为什么准确率是分类的一个糟糕的度量。
混淆矩阵
假设一个模型观察到名为“迈克尔”的人离职。捕获名字作为输入变量的原因确实值得怀疑,因为一个人的名字是否会影响他们是否离职是可疑的。然而,为了简化例子,让我们继续。该模型随后预测任何名为“迈克尔”的人都会离职。
现在这就是准确率失效的地方。我有一百名员工,其中一个名叫“迈克尔”,另一个名叫“山姆”。迈克尔被错误地预测会离职,而实际上是山姆离职了。我的模型准确率是多少?是 98%,因为在一百名员工中只有两次错误预测,如图 6-16 所示。
图 6-16. 预测名为“迈克尔”的员工会离职,但实际上是另一名员工离职,给我们带来了 98%的准确率
尤其对于数据不平衡的情况,其中感兴趣的事件(例如,员工离职)很少见,准确率指标对于分类问题是极其误导的。如果供应商、顾问或数据科学家试图通过准确性来销售分类系统,请要求一个混淆矩阵。
混淆矩阵 是一个网格,将预测与实际结果进行对比,显示出真正的正例、真正的负例、假正例(I 型错误)和假负例(II 型错误)。这里是一个在图 6-17 中呈现的混淆矩阵。
图 6-17. 一个简单的混淆矩阵
通常,我们希望对角线值(从左上到右下)更高,因为这些反映了正确的分类。我们想评估有多少被预测会离职的员工实际上确实离职(真正的正例)。相反,我们也想评估有多少被预测会留下的员工实际上确实留下(真正的负例)。
其他单元格反映了错误的预测,其中一个被预测会离职的员工最终留下(假正例),以及一个被预测会留下的员工最终离职(假负例)。
我们需要将准确率指标分解为针对混淆矩阵不同部分的更具体的准确率指标。让我们看看图 6-18,它添加了一些有用的指标。
从混淆矩阵中,我们可以得出除准确率之外的各种有用的指标。我们可以清楚地看到精确度(正面预测的准确性)和灵敏度(识别出的正面率)都为 0,这意味着这个机器学习模型在正面预测上完全失败。
图 6-18. 在混淆矩阵中添加有用的指标
示例 6-17 展示了如何在 SciPy 中使用混淆矩阵 API 对具有训练/测试拆分的逻辑回归进行操作。请注意,混淆矩阵仅应用于测试数据集。
示例 6-17。在 SciPy 中为测试数据集创建混淆矩阵
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
df = pd.read_csv('https://bit.ly/3cManTi' , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
model = LogisticRegression(solver='liblinear' )
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=.33 ,
random_state=10 )
model.fit(X_train, Y_train)
prediction = model.predict(X_test)
"""
The confusion matrix evaluates accuracy within each category.
[[truepositives falsenegatives]
[falsepositives truenegatives]]
The diagonal represents correct predictions,
so we want those to be higher
"""
matrix = confusion_matrix(y_true=Y_test, y_pred=prediction)
print (matrix)
贝叶斯定理和分类
你还记得第二章中的贝叶斯定理吗?你可以使用贝叶斯定理引入外部信息来进一步验证混淆矩阵的发现。图 6-19 展示了对一千名患者进行疾病测试的混淆矩阵。
图 6-19。用于识别疾病的医学测试的混淆矩阵
我们被告知对于存在健康风险的患者,将成功识别 99%(敏感性)。使用混淆矩阵,我们可以数学上验证这一点:
sensitivity = 198 198 + 2 = .99
但是如果我们颠倒条件呢?那么测试结果为阳性的人中有多少百分比存在健康风险(精确度)?虽然我们在颠倒条件概率,但在这里我们不必使用贝叶斯定理,因为混淆矩阵为我们提供了所有我们需要的数字:
precision = 198 198 + 50 = .798
好吧,79.8%并不算糟糕,这是测试为阳性的人中实际患有疾病的百分比。但请问自己...我们对数据做了什么假设?它是否代表人口?
一些快速研究发现 1%的人口实际上患有这种疾病。在这里有机会使用贝叶斯定理。我们可以考虑实际患有疾病的人口比例,并将其纳入我们的混淆矩阵结果中。然后我们发现了一些重要的东西。
P ( 如果阳性则存在风险 ) = P ( 如果存在风险则阳性 ) × P (存在风险) P ( 阳性 ) P ( 如果阳性则存在风险 ) = .99 × .01 .248 P ( 如果阳性则存在风险 ) = .0339
当我们考虑到只有 1%的人口存在风险,并且我们测试的患者中有 20%存在风险时,接受阳性测试的人存在风险的概率为 3.39%!为什么从 99%下降到了这个数字?这只是展示了我们如何容易被高概率欺骗,这种概率只在特定样本中高,比如供应商的一千名测试患者。因此,如果这个测试只有 3.39%的概率成功识别真正的阳性,我们可能不应该使用它。
接收器操作特性/曲线下面积
当我们评估不同的机器学习配置时,可能会得到数十、数百或数千个混淆矩阵。这些可能很繁琐,因此我们可以用一个接收器操作特性(ROC)曲线 来总结所有这些,如图 6-20 所示。这使我们能够看到每个测试实例(每个由一个黑点表示)并找到真正例和假正例之间的一个令人满意的平衡。
我们还可以通过为每个模型创建单独的 ROC 曲线来比较不同的机器学习模型。例如,如果在图 6-21 中,我们的顶部曲线代表逻辑回归,底部曲线代表决策树(这是本书中没有涵盖的一种机器学习技术),我们可以并排看到它们的性能。曲线下面积(AUC) 是选择使用哪个模型的良好指标。由于顶部曲线(逻辑回归)的面积更大,这表明它是一个更优秀的模型。
图 6-20. 接收器操作特性曲线
图 6-21. 通过它们各自的 ROC 曲线比较两个模型的曲线下面积(AUC)
要将 AUC 作为评分指标使用,请在 scikit-learn API 中将scoring
参数更改为使用roc_auc
,如在示例 6-18 中所示进行交叉验证。
示例 6-18. 使用 AUC 作为 scikit-learn 参数
results = cross_val_score(model, X, Y, cv=kfold, scoring='roc_auc' )
print ("AUC: %.3f (%.3f)" % (results.mean(), results.std()))
类别不平衡
在我们结束本章之前,还有一件事情需要讨论。正如我们之前在讨论混淆矩阵时看到的那样,类别不平衡 ,即当数据在每个结果类别中没有得到平等表示时,是机器学习中的一个问题。不幸的是,许多感兴趣的问题都存在不平衡,比如疾病预测、安全漏洞、欺诈检测等等。类别不平衡仍然是一个没有很好解决方案的问题。然而,你可以尝试一些技术。
首先,你可以做一些明显的事情,比如收集更多数据或尝试不同的模型,以及使用混淆矩阵和 ROC/AUC 曲线。所有这些都将有助于跟踪糟糕的预测并主动捕捉错误。
另一种常见的技术是复制少数类别中的样本,直到它在数据集中得到平等的表示。你可以在 scikit-learn 中这样做,如示例 6-19 所示,在进行训练-测试拆分时传递包含类别值的列的stratify
选项,它将尝试平等地表示每个类别的数据。
示例 6-19. 在 scikit-learn 中使用stratify
选项来平衡数据中的类别
X, Y = ...
X_train, X_test, Y_train, Y_test = \
train_test_split(X, Y, test_size=.33 , stratify=Y)
还有一类算法称为 SMOTE,它会生成少数类的合成样本。然而,最理想的方法是以一种利用异常检测模型的方式解决问题,这些模型专门设计用于寻找罕见事件。它们寻找异常值,但不一定是分类,因为它们是无监督算法。所有这些技术超出了本书的范围,但值得一提,因为它们可能 为给定问题提供更好的解决方案。
结论
逻辑回归是预测数据概率和分类的主力模型。逻辑回归可以预测不止一个类别,而不仅仅是真/假。你只需构建单独的逻辑回归模型,模拟它是否属于该类别,产生最高概率的模型即为胜者。你可能会发现,大部分情况下,scikit-learn 会为你完成这一点,并在数据具有两个以上类别时进行检测。
在本章中,我们不仅介绍了如何使用梯度下降和 scikit-learn 拟合逻辑回归,还涉及了统计学和机器学习方法的验证。在统计学方面,我们涵盖了R 2 和 p 值,而在机器学习方面,我们探讨了训练/测试拆分、混淆矩阵和 ROC/AUC。
如果你想了解更多关于逻辑回归的知识,可能最好的资源是 Josh Starmer 的 StatQuest 播放列表关于逻辑回归的视频。我必须感谢 Josh 在协助本章某些部分方面的工作,特别是如何计算逻辑回归的R 2 和 p 值。如果没有其他,观看他的视频也是为了那些出色的开场曲 !
一如既往,你将发现自己在统计学和机器学习两个世界之间徘徊。目前许多书籍和资源都从机器学习的角度讨论逻辑回归,但也尝试寻找统计学资源。两种思维方式各有优劣,只有适应两者才能取得胜利!
练习
提供了一个包含三个输入变量RED
、GREEN
和BLUE
以及一个输出变量LIGHT_OR_DARK_FONT_IND
的数据集,链接在这里 。这将用于预测给定背景颜色(由 RGB 值指定)是否适合使用浅色/深色字体(分别为 0/1)。
对上述数据执行逻辑回归,使用三折交叉验证和准确率作为评估指标。
生成一个混淆矩阵,比较预测和实际数据。
选择几种不同的背景颜色(你可以使用像这个 这样的 RGB 工具),看看逻辑回归是否明智地为每种颜色选择浅色(0)或深色(1)字体。
根据前面的练习,你认为逻辑回归对于预测给定背景颜色的浅色或深色字体有效吗?
答案在附录 B 中。
第七章:神经网络
在过去的 10 年里,一种经历复兴的回归和分类技术是神经网络。在最简单的定义中,神经网络 是一个包含权重、偏差和非线性函数层的多层回归,位于输入变量和输出变量之间。深度学习 是神经网络的一种流行变体,利用包含权重和偏差的多个“隐藏”(或中间)层节点。每个节点在传递给非线性函数(称为激活函数)之前类似于一个线性函数。就像我们在第五章中学到的线性回归一样,优化技术如随机梯度下降被用来找到最优的权重和偏差值以最小化残差。
神经网络为计算机以前难以解决的问题提供了令人兴奋的解决方案。从识别图像中的物体到处理音频中的单词,神经网络已经创造了影响我们日常生活的工具。这包括虚拟助手和搜索引擎,以及我们 iPhone 中的照片工具。
鉴于媒体的炒作和大胆宣称主导着关于神经网络的新闻头条,也许令人惊讶的是,它们自上世纪 50 年代以来就存在了。它们在 2010 年后突然变得流行的原因是由于数据和计算能力的不断增长。2011 年至 2015 年之间的 ImageNet 挑战赛可能是复兴的最大推动力,将对 140 万张图像进行一千个类别的分类准确率提高到了 96.4%。
然而,就像任何机器学习技术一样,它只适用于狭义定义的问题。即使是创建“自动驾驶”汽车的项目也不使用端到端的深度学习,主要使用手工编码的规则系统,卷积神经网络充当“标签制造机”来识别道路上的物体。我们将在本章后面讨论这一点,以了解神经网络实际上是如何使用的。但首先我们将在 NumPy 中构建一个简单的神经网络,然后使用 scikit-learn 作为库实现。
何时使用神经网络和深度学习
神经网络和深度学习可用于分类和回归,那么它们与线性回归、逻辑回归和其他类型的机器学习相比如何?你可能听说过“当你手中只有一把锤子时,所有事情看起来都像钉子”。每种算法都有其特定情况下的优势和劣势。线性回归、逻辑回归以及梯度提升树(本书未涵盖)在结构化数据上做出了相当出色的预测。将结构化数据视为可以轻松表示为表格的数据,具有行和列。但感知问题如图像分类则不太结构化,因为我们试图找到像素组之间的模糊相关性以识别形状和模式,而不是表格中的数据行。尝试预测正在输入的句子中的下四五个单词,或者解密音频剪辑中的单词,也是感知问题,是神经网络用于自然语言处理的例子。
在本章中,我们将主要关注只有一个隐藏层的简单神经网络。
使用神经网络是否有些大材小用?
对于即将介绍的例子来说,使用神经网络可能有些大材小用,因为逻辑回归可能更实用。甚至可以使用公式方法 。然而,我一直是一个喜欢通过将复杂技术应用于简单问题来理解的人。你可以了解技术的优势和局限性,而不会被大型数据集所分散注意力。因此,请尽量不要在更实用的情况下使用神经网络。为了理解技术,我们将在本章中打破这个规则。
一个简单的神经网络
这里有一个简单的例子,让你对神经网络有所了解。我想要预测给定颜色背景下的字体应该是浅色(1)还是深色(0)。以下是不同背景颜色的几个示例,见图 7-1。顶部一行最适合浅色字体,底部一行最适合深色字体。
图 7-1. 浅色背景颜色最适合深色字体,而深色背景颜色最适合浅色字体
在计算机科学中,表示颜色的一种方式是使用 RGB 值,即红色、绿色和蓝色值。每个值都介于 0 和 255 之间,表示这三种颜色如何混合以创建所需的颜色。例如,如果我们将 RGB 表示为(红色,绿色,蓝色),那么深橙色的 RGB 值为(255,140,0),粉色为(255,192,203)。黑色为(0,0,0),白色为(255,255,255)。
从机器学习和回归的角度来看,我们有三个数值输入变量red
、green
和blue
来捕捉给定背景颜色。我们需要对这些输入变量拟合一个函数,并输出是否应该为该背景颜色使用浅色(1)或深色(0)字体。
通过 RGB 表示颜色
在线有数百种颜色选择器调色板可供尝试 RGB 值。W3 Schools 有一个这里 。
请注意,这个例子与神经网络识别图像的工作原理并不相去甚远,因为每个像素通常被建模为三个数值 RGB 值。在这种情况下,我们只关注一个“像素”作为背景颜色。
让我们从高层次开始,把所有的实现细节放在一边。我们将以洋葱的方式来处理这个主题,从更高的理解开始,然后慢慢剥离细节。目前,这就是为什么我们简单地将一个接受输入并产生输出的过程标记为“神秘数学”。我们有三个数值输入变量 R、G 和 B,这些变量被这个神秘的数学处理。然后它输出一个介于 0 和 1 之间的预测,如图 7-2 所示。
图 7-2。我们有三个数值 RGB 值用于预测使用浅色或深色字体
这个预测输出表示一个概率。输出概率是使用神经网络进行分类的最常见模型。一旦我们用它们的数值替换 RGB,我们会发现小于 0.5 会建议使用深色字体,而大于 0.5 会建议使用浅色字体,如图 7-3 所示。
图 7-3。如果我们输入一个粉色的背景色(255,192,203),那么神秘的数学会推荐使用浅色字体,因为输出概率 0.89 大于 0.5
那个神秘的数学黑匣子里到底发生了什么?让我们在图 7-4 中看一看。
我们还缺少神经网络的另一个部分,即激活函数,但我们很快会讨论到。让我们先了解这里发生了什么。左侧的第一层只是三个变量的输入,这些变量在这种情况下是红色、绿色和蓝色值。在隐藏(中间)层中,请注意我们在输入和输出之间产生了三个节点 ,或者说是权重和偏置的函数。每个节点本质上是一个线性函数,斜率为W i ,截距为B i ,与输入变量X i 相乘并求和。每个输入节点和隐藏节点之间有一个权重W i ,每个隐藏节点和输出节点之间有另一组权重。每个隐藏和输出节点都会额外添加一个偏置B i 。
图 7-4。神经网络的隐藏层对每个输入变量应用权重和偏置值,输出层对该输出应用另一组权重和偏置
注意,输出节点重复执行相同的操作,将隐藏层的加权和求和输出作为输入传递到最终层,其中另一组权重和偏置将被应用。
简而言之,这是一个回归问题,就像线性回归或逻辑回归一样,但需要解决更多参数。权重和偏置值类似于m 和b ,或者线性回归中的β 1 和β 0 参数。我们使用随机梯度下降和最小化损失,就像线性回归一样,但我们需要一种称为反向传播的额外工具来解开权重W i 和偏置B i 值,并使用链式法则计算它们的偏导数。我们将在本章后面详细讨论这一点,但现在让我们假设我们已经优化了权重和偏置值。我们需要先讨论激活函数。
激活函数
接下来让我们介绍激活函数。激活函数 是一个非线性函数,它转换或压缩节点中的加权和值,帮助神经网络有效地分离数据,以便进行分类。让我们看一下图 7-5。如果没有激活函数,你的隐藏层将毫无生产力,表现不会比线性回归好。
图 7-5. 应用激活函数
ReLU 激活函数 将隐藏节点的任何负输出归零。如果权重、偏置和输入相乘并求和得到负数,它将被转换为 0。否则输出保持不变。这是使用 SymPy (示例 7-1) 绘制的 ReLU 图(图 7-6)。
示例 7-1. 绘制 ReLU 函数
from sympy import *
x = symbols('x' )
relu = Max(0 , x)
plot(relu)
图 7-6. ReLU 函数图
ReLU 是“修正线性单元”的缩写,但这只是一种将负值转换为 0 的花哨方式。ReLU 在神经网络和深度学习中的隐藏层中变得流行,因为它速度快,并且缓解了梯度消失问题 。梯度消失发生在偏导数斜率变得非常小,导致过早接近 0 并使训练停滞。
输出层有一个重要的任务:它接收来自神经网络隐藏层的大量数学,并将其转换为可解释的结果,例如呈现分类预测。对于这个特定的神经网络,输出层使用逻辑激活函数 ,这是一个简单的 S 形曲线。如果您阅读第六章,逻辑(或 S 形)函数应该很熟悉,它表明逻辑回归在我们的神经网络中充当一层。输出节点的权重、偏置和从隐藏层传入的每个值求和。之后,它通过逻辑函数传递结果值,以便输出介于 0 和 1 之间的数字。就像第六章中的逻辑回归一样,这代表了输入到神经网络的给定颜色建议使用浅色字体的概率。如果大于或等于 0.5,则神经网络建议使用浅色字体,否则建议使用深色字体。
这是使用 SymPy (示例 7-2) 绘制的逻辑函数图(图 7-7)。
示例 7-2. SymPy 中的逻辑激活函数
from sympy import *
x = symbols('x' )
logistic = 1 / (1 + exp(-x))
plot(logistic)
图 7-7. 逻辑激活函数
当我们通过激活函数传递节点的加权、偏置和求和值时,我们现在称之为激活输出 ,意味着它已通过激活函数进行了过滤。当激活输出离开隐藏层时,信号准备好被馈送到下一层。激活函数可能会增强、减弱或保持信号不变。这就是神经网络中大脑和突触的比喻的来源。
鉴于复杂性的潜在可能性,您可能想知道是否还有其他激活函数。一些常见的激活函数显示在表 7-1 中。
表 7-1. 常见激活函数
名称
典型使用层
描述
注释
线性
输出
保持值不变
不常用
Logistic
输出层
S 形 sigmoid 曲线
将值压缩在 0 和 1 之间,通常用于二元分类
双曲正切
隐藏层
tanh,在 -1 和 1 之间的 S 形 sigmoid 曲线
通过将均值接近 0 来“居中”数据
ReLU
隐藏层
将负值转换为 0
比 sigmoid 和 tanh 更快的流行激活函数,缓解消失梯度问题,计算成本低廉
Leaky ReLU
隐藏层
将负值乘以 0.01
ReLU 的有争议变体,边缘化而不是消除负值
Softmax
输出层
确保所有输出节点加起来为 1.0
适用于多类别分类,重新缩放输出使其加起来为 1.0
这不是激活函数的全面列表,理论上神经网络中任何函数都可以是激活函数。
尽管这个神经网络表面上支持两类(浅色或深色字体),但实际上它被建模为一类:字体是否应该是浅色(1)或不是(0)。如果您想支持多个类别,可以为每个类别添加更多输出节点。例如,如果您试图识别手写数字 0-9,将有 10 个输出节点,代表给定图像是这些数字的概率。当有多个类别时,您可能还考虑在输出时使用 softmax 激活。图 7-8 展示了一个以像素化图像为输入的示例,其中像素被分解为单独的神经网络输入,然后通过两个中间层,最后一个输出层,有 10 个节点代表 10 个类别的概率(数字 0-9)。
图 7-8. 一个神经网络,将每个像素作为输入,并预测图像包含的数字
在神经网络上使用 MNIST 数据集的示例可以在 附录 A 中找到。
我不知道要使用什么激活函数!
如果不确定要使用哪些激活函数,当前最佳实践倾向于在中间层使用 ReLU,在输出层使用 logistic(sigmoid)。如果输出中有多个分类,可以在输出层使用 softmax。
前向传播
让我们使用 NumPy 捕获到目前为止学到的知识。请注意,我尚未优化参数(我们的权重和偏置值)。我们将用随机值初始化它们。
示例 7-3 是创建一个简单的前馈神经网络的 Python 代码,尚未进行优化。前馈 意味着我们只是将一种颜色输入到神经网络中,看看它输出什么。权重和偏置是随机初始化的,并将在本章后面进行优化,因此暂时不要期望有用的输出。
示例 7-3. 一个具有随机权重和偏置值的简单前向传播网络
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
all_data = pd.read_csv("https://tinyurl.com/y2qmhfsr" )
all_inputs = (all_data.iloc[:, 0 :3 ].values / 255.0 )
all_outputs = all_data.iloc[:, -1 ].values
X_train, X_test, Y_train, Y_test = train_test_split(all_inputs, all_outputs,
test_size=1 /3 )
n = X_train.shape[0 ]
w_hidden = np.random.rand(3 , 3 )
w_output = np.random.rand(1 , 3 )
b_hidden = np.random.rand(3 , 1 )
b_output = np.random.rand(1 , 1 )
relu = lambda x: np.maximum(x, 0 )
logistic = lambda x: 1 / (1 + np.exp(-x))
def forward_prop (X ):
Z1 = w_hidden @ X + b_hidden
A1 = relu(Z1)
Z2 = w_output @ A1 + b_output
A2 = logistic(Z2)
return Z1, A1, Z2, A2
test_predictions = forward_prop(X_test.transpose())[3 ]
test_comparisons = np.equal((test_predictions >= .5 ).flatten().astype(int ), Y_test)
accuracy = sum (test_comparisons.astype(int ) / X_test.shape[0 ])
print ("ACCURACY: " , accuracy)
这里有几点需要注意。 包含 RGB 输入值以及输出值(1 代表亮,0 代表暗)的数据集包含在此 CSV 文件 中。 我将输入列 R、G 和 B 的值缩小了 1/255 的因子,使它们介于 0 和 1 之间。 这将有助于后续的训练,以便压缩数字空间。
请注意,我还使用 scikit-learn 将数据的 2/3 用于训练,1/3 用于测试,我们在第五章中学习了如何做到这一点。 n
简单地是训练数据记录的数量。
现在请注意示例 7-4 中显示的代码行。
示例 7-4。 NumPy 中的权重矩阵和偏置向量
w_hidden = np.random.rand(3 , 3 )
w_output = np.random.rand(1 , 3 )
b_hidden = np.random.rand(3 , 1 )
b_output = np.random.rand(1 , 1 )
这些声明了我们神经网络隐藏层和输出层的权重和偏置。 这可能还不明显,但矩阵乘法将使我们的代码变得强大简单,使用线性代数和 NumPy。
权重和偏置将被初始化为介于 0 和 1 之间的随机值。 让我们首先看一下权重矩阵。 当我运行代码时,我得到了这些矩阵:
W h i d d e n = 0.034535 0.5185636 0.81485028 0.3329199 0.53873853 0.96359003 0.19808306 0.45422182 0.36618893 W o u t p u t = 0.82652072 0.30781539 0.93095565
请注意,W h i d d e n 是隐藏层中的权重。第一行代表第一个节点的权重 W 1 ,W 2 和 W 3 。第二行是第二个节点,带有权重 W 4 ,W 5 和 W 6 。第三行是第三个节点,带有权重 W 7 ,W 8 和 W 9 。
输出层只有一个节点,意味着其矩阵只有一行,带有权重 W 10 ,W 11 和 W 12 。
看到规律了吗?每个节点在矩阵中表示为一行。如果有三个节点,就有三行。如果只有一个节点,就只有一行。每一列保存着该节点的权重值。
让我们也看看偏差。由于每个节点有一个偏差,隐藏层将有三行偏差,输出层将有一行偏差。每个节点只有一个偏差,所以只有一列:
B h i d d e n = 0.41379442 0.81666079 0.07511252 B o u t p u t = 0.58018555
现在让我们将这些矩阵值与我们在 图 7-9 中展示的神经网络进行比较。
图 7-9。将我们的神经网络与权重和偏差矩阵值进行可视化
那么除了紧凑难懂之外,这种矩阵形式中的权重和偏差有什么好处呢?让我们把注意力转向 示例 7-5 中的这些代码行。
示例 7-5。我们神经网络的激活函数和前向传播函数
relu = lambda x: np.maximum(x, 0 )
logistic = lambda x: 1 / (1 + np.exp(-x))
def forward_prop (X ):
Z1 = w_hidden @ X + b_hidden
A1 = relu(Z1)
Z2 = w_output @ A1 + b_output
A2 = logistic(Z2)
return Z1, A1, Z2, A2
这段代码很重要,因为它使用矩阵乘法和矩阵-向量乘法简洁地执行我们整个神经网络。我们在第四章中学习了这些操作。它仅用几行代码将三个 RGB 输入的颜色通过权重、偏置和激活函数运行。
首先,我声明relu()
和logistic()
激活函数,它们分别接受给定的输入值并返回曲线的输出值。forward_prop()
函数为包含 R、G 和 B 值的给定颜色输入X
执行整个神经网络。它返回四个阶段的矩阵输出:Z1
、A1
、Z2
和A2
。数字“1”和“2”表示操作属于第 1 层和第 2 层。“Z”表示来自该层的未激活输出,“A”是来自该层的激活输出。
隐藏层由Z1
和A1
表示。Z1
是应用于X
的权重和偏置。然后A1
获取来自Z1
的输出,并通过激活 ReLU 函数。Z2
获取来自A1
的输出,并应用输出层的权重和偏置。该输出依次通过激活函数,即逻辑曲线,变为A2
。最终阶段,A2
,是输出层的预测概率,返回一个介于 0 和 1 之间的值。我们称之为A2
,因为它是来自第 2 层的“激活”输出。
让我们更详细地分解这个,从Z1
开始:
Z 1 = W h i d d e n X + B h i d d e n
首先,我们在W h i d d e n 和输入颜色X
之间执行矩阵-向量乘法。我们将W h i d d e n 的每一行(每一行都是一个节点的权重集)与向量X
(RGB 颜色输入值)相乘。然后将偏置添加到该结果中,如图 7-10 所示。
图 7-10。将隐藏层权重和偏置应用于输入X
,使用矩阵-向量乘法以及向量加法
那个Z 1 向量是隐藏层的原始输出,但我们仍然需要通过激活函数将Z 1 转换为A 1 。很简单。只需将该向量中的每个值通过 ReLU 函数传递,就会给我们A 1 。因为所有值都是正值,所以不应该有影响。
A 1 = R e L U ( Z 1 ) A 1 = R e L U ( 1.36054964190909 ) R e L U ( 2.15471757888247 ) R e L U ( 0.719554393391768 ) = 1.36054964190909 2.15471757888247 0.719554393391768
现在让我们将隐藏层输出A 1 传递到最终层,得到Z 2 ,然后A 2 。 A 1 成为输出层的输入。
Z 2 = W o u t p u t A 1 + B o u t p u t Z 2 = 0.82652072 0.3078159 0.93095565 1.36054964190909 2.15471757888247 0.719554393391768 + 0.58018555 Z 2 = 2.45765202842636 + 0.58018555 Z 2 = 3.03783757842636
最后,将这个单个值Z 2 通过激活函数传递,得到A 2 。这将产生一个约为 0.95425 的预测:
A 2 = l o g i s t i c ( Z 2 ) A 2 = l o g i s t i c ( 3.0378364795204 ) A 2 = 0.954254478103241
执行我们整个神经网络,尽管我们尚未对其进行训练。但请花点时间欣赏,我们已经将所有这些输入值、权重、偏差和非线性函数转化为一个将提供预测的单个值。
再次强调,A2
是最终输出,用于预测背景颜色是否需要浅色(1)或深色(1)字体。尽管我们的权重和偏差尚未优化,让我们按照示例 7-6 计算准确率。取测试数据集X_test
,转置它,并通过forward_prop()
函数传递,但只获取A2
向量,其中包含每个测试颜色的预测。然后将预测与实际值进行比较,并计算正确预测的百分比。
示例 7-6. 计算准确率
test_predictions = forward_prop(X_test.transpose())[3 ]
test_comparisons = np.equal((test_predictions >= .5 ).flatten().astype(int ), Y_test)
accuracy = sum (test_comparisons.astype(int ) / X_test.shape[0 ])
print ("ACCURACY: " , accuracy)
当我运行示例 7-3 中的整个代码时,我大致获得 55%到 67%的准确率。请记住,权重和偏差是随机生成的,因此答案会有所不同。虽然这个准确率似乎很高,考虑到参数是随机生成的,但请记住,输出预测是二元的:浅色或深色。因此,对于每个预测,随机抛硬币也可能产生这种结果,所以这个数字不应令人惊讶。
不要忘记检查是否存在数据不平衡!
正如在第六章中讨论的那样,不要忘记分析数据以检查是否存在不平衡的类别。整个背景颜色数据集有点不平衡:512 种颜色的输出为 0,833 种颜色的输出为 1。这可能会使准确率产生偏差,也可能是我们的随机权重和偏差倾向于高于 50%准确率的原因。如果数据极度不平衡(例如 99%的数据属于一类),请记住使用混淆矩阵来跟踪假阳性和假阴性。
到目前为止,结构上一切都合理吗?在继续之前,随时可以回顾一切。我们只剩下最后一个部分要讲解:优化权重和偏差。去冲杯浓缩咖啡或氮气咖啡吧,因为这是本书中我们将要做的最复杂的数学!
反向传播
在我们开始使用随机梯度下降来优化我们的神经网络之前,我们面临的挑战是如何相应地改变每个权重和偏差值,尽管它们都纠缠在一起以创建输出变量,然后用于计算残差。我们如何找到每个权重W i 和偏差B i 变量的导数?我们需要使用我们在第一章中讨论过的链式法则。
计算权重和偏差的导数
我们还没有准备好应用随机梯度下降来训练我们的神经网络。我们需要求出相对于权重W i 和偏差B i 的偏导数,而且我们有链式法则来帮助我们。
虽然过程基本相同,但在神经网络上使用随机梯度下降存在一个复杂性。一层中的节点将它们的权重和偏置传递到下一层,然后应用另一组权重和偏置。这创建了一个类似洋葱的嵌套结构,我们需要从输出层开始解开。
在梯度下降过程中,我们需要找出应该调整哪些权重和偏置,以及调整多少,以减少整体成本函数。单个预测的成本将是神经网络的平方输出A 2 减去实际值Y :
C = ( A 2 - Y ) 2
但让我们再往下一层。那个激活输出A 2 只是带有激活函数的Z 2 :
A 2 = s i g m o i d ( Z 2 )
转而,Z 2 是应用于激活输出A 1 的输出权重和偏置,这些输出来自隐藏层:
Z 2 = W 2 A 1 + B 2
A 1 是由通过 ReLU 激活函数传递的Z 1 构建而成:
A 1 = R e L U ( Z 1 )
最后,Z 1 是由隐藏层加权和偏置的输入 x 值:
Z 1 = W 1 X + B 1
我们需要找到包含在矩阵和向量W 1 ,B 1 ,W 2 和B 2 中的权重和偏置,以最小化我们的损失。通过微调它们的斜率,我们可以改变对最小化损失影响最大的权重和偏置。然而,对权重或偏置进行微小调整会一直传播到外层的损失函数。这就是链式法则可以帮助我们找出这种影响的地方。
让我们专注于找到输出层权重W 2 和成本函数C 之间的关系。权重W 2 的变化导致未激活的输出Z 2 的变化。然后改变激活输出A 2 ,进而改变成本函数C 。利用链式法则,我们可以定义关于W 2 的导数如下:
d C d W 2 = d Z 2 d W 2 d A 2 d Z 2 d C d A 2
当我们将这三个梯度相乘在一起时,我们得到了改变W 2 将如何改变成本函数C 的度量。
现在我们将计算这三个导数。让我们使用 SymPy 计算在示例 7-7 中关于A 2 的成本函数的导数。
d C d A 2 = 2 A 2 - 2 y
示例 7-7. 计算成本函数关于A 2 的导数
from sympy import *
A2, y = symbols('A2 Y' )
C = (A2 - Y)**2
dC_dA2 = diff(C, A2)
print (dC_dA2)
接下来,让我们找到关于Z 2 的导数与A 2 的导数(示例 7-8)。记住A 2 是激活函数的输出,在这种情况下是逻辑函数。因此,我们实际上只是在求取 S 形曲线的导数。
d A 2 d Z 2 = e - Z 2 1 + e - Z 2 2
示例 7-8. 找到关于Z 2 的导数与A 2
from sympy import *
Z2 = symbols('Z2' )
logistic = lambda x: 1 / (1 + exp(-x))
A2 = logistic(Z2)
dA2_dZ2 = diff(A2, Z2)
print (dA2_dZ2)
Z 2 关于W 2 的导数将会得到A 1 ,因为它只是一个线性函数,将返回斜率(示例 7-9)。
d Z 2 d W 1 = A 1
示例 7-9. 关于W 2 的导数Z 2
from sympy import *
A1, W2, B2 = symbols('A1, W2, B2' )
Z2 = A1*W2 + B2
dZ2_dW2 = diff(Z2, W2)
print (dZ2_dW2)
将所有内容整合在一起,这里是找到改变W 2 中的权重会如何影响成本函数C 的导数:
d C d w 2 = d Z 2 d w 2 d A 2 d Z 2 d C d A 2 = ( A 1 ) ( e - Z 2 1 + e - Z 2 2 ) ( 2 A 2 - 2 y )
当我们运行一个输入X
与三个输入 R、G 和 B 值时,我们将得到A 1 、A 2 、Z 2 和y 的值。
不要在数学中迷失!
在这一点很容易在数学中迷失并忘记你最初想要实现的目标,即找到成本函数关于输出层中的权重(W 2 )的导数。当你发现自己陷入困境并忘记你要做什么时,那就退一步,出去走走,喝杯咖啡,提醒自己你要达成的目标。如果你做不到,你应该从头开始,一步步地找回迷失的地方。
然而,这只是神经网络的一个组成部分,对于W 2 的导数。以下是我们在示例 7-10 中使用 SymPy 计算的其余部分偏导数,这些是我们在链式求导中需要的。
示例 7-10。计算我们神经网络所需的所有偏导数
from sympy import *
W1, W2, B1, B2, A1, A2, Z1, Z2, X, Y = \
symbols('W1 W2 B1 B2 A1 A2 Z1 Z2 X Y' )
C = (A2 - Y)**2
dC_dA2 = diff(C, A2)
print ("dC_dA2 = " , dC_dA2)
logistic = lambda x: 1 / (1 + exp(-x))
_A2 = logistic(Z2)
dA2_dZ2 = diff(_A2, Z2)
print ("dA2_dZ2 = " , dA2_dZ2)
_Z2 = A1*W2 + B2
dZ2_dA1 = diff(_Z2, A1)
print ("dZ2_dA1 = " , dZ2_dA1)
dZ2_dW2 = diff(_Z2, W2)
print ("dZ2_dW2 = " , dZ2_dW2)
dZ2_dB2 = diff(_Z2, B2)
print ("dZ2_dB2 = " , dZ2_dB2)
relu = lambda x: Max(x, 0 )
_A1 = relu(Z1)
d_relu = lambda x: x > 0
dA1_dZ1 = d_relu(Z1)
print ("dA1_dZ1 = " , dA1_dZ1)
_Z1 = X*W1 + B1
dZ1_dW1 = diff(_Z1, W1)
print ("dZ1_dW1 = " , dZ1_dW1)
dZ1_dB1 = diff(_Z1, B1)
print ("dZ1_dB1 = " , dZ1_dB1)
注意,ReLU 是手动计算的,而不是使用 SymPy 的diff()
函数。这是因为导数适用于平滑曲线,而不是 ReLU 上存在的锯齿角。但通过简单地声明斜率为正数为 1,负数为 0,可以轻松解决这个问题。这是有道理的,因为负数具有斜率为 0 的平坦线。但正数保持不变,具有 1:1 的斜率。
这些偏导数可以链接在一起,以创建相对于权重和偏置的新偏导数。让我们为W 1 、W 2 、B 1 和B 2 相对于成本函数的四个偏导数。我们已经讨论了d C d w 2 。让我们将其与其他三个链式求导一起展示:
d C d W 2 = d Z 2 d W 2 d A 2 d Z 2 d C d A 2 = ( A 1 ) ( e - Z 2 1 + e - Z 2 2 ) ( 2 A 2 - 2 y ) d C d B 2 = d Z 2 d B 2 d A 2 d Z 2 d C d A 2 = ( 1 ) ( e - Z 2 1 + e - Z 2 2 ) ( 2 A 2 - 2 y ) d C d W 1 = d C D A 2 D A 2 d Z 2 d Z 2 d A 1 d A 1 d Z 1 d Z 1 d W 1 = ( 2 A 2 - 2 y ) ( e - Z 2 1 + e - Z 2</mn
我们将使用这些链式梯度来计算成本函数C 相对于W 1 ,B 1 ,W 2 和B 2 的斜率。
随机梯度下降
现在我们准备整合链式法则来执行随机梯度下降。为了保持简单,我们每次迭代只对一个训练记录进行采样。在神经网络和深度学习中通常使用批量梯度下降和小批量梯度下降,但是在每次迭代中只处理一个样本就足够了,因为其中涉及足够的线性代数和微积分。
让我们来看看我们的神经网络的完整实现,使用反向传播的随机梯度下降,在示例 7-11 中。
示例 7-11. 使用随机梯度下降实现神经网络
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
all_data = pd.read_csv("https://tinyurl.com/y2qmhfsr" )
L = 0.05
all_inputs = (all_data.iloc[:, 0 :3 ].values / 255.0 )
all_outputs = all_data.iloc[:, -1 ].values
X_train, X_test, Y_train, Y_test = train_test_split(all_inputs, all_outputs,
test_size=1 / 3 )
n = X_train.shape[0 ]
w_hidden = np.random.rand(3 , 3 )
w_output = np.random.rand(1 , 3 )
b_hidden = np.random.rand(3 , 1 )
b_output = np.random.rand(1 , 1 )
relu = lambda x: np.maximum(x, 0 )
logistic = lambda x: 1 / (1 + np.exp(-x))
def forward_prop (X ):
Z1 = w_hidden @ X + b_hidden
A1 = relu(Z1)
Z2 = w_output @ A1 + b_output
A2 = logistic(Z2)
return Z1, A1, Z2, A2
d_relu = lambda x: x > 0
d_logistic = lambda x: np.exp(-x) / (1 + np.exp(-x)) ** 2
def backward_prop (Z1, A1, Z2, A2, X, Y ):
dC_dA2 = 2 * A2 - 2 * Y
dA2_dZ2 = d_logistic(Z2)
dZ2_dA1 = w_output
dZ2_dW2 = A1
dZ2_dB2 = 1
dA1_dZ1 = d_relu(Z1)
dZ1_dW1 = X
dZ1_dB1 = 1
dC_dW2 = dC_dA2 @ dA2_dZ2 @ dZ2_dW2.T
dC_dB2 = dC_dA2 @ dA2_dZ2 * dZ2_dB2
dC_dA1 = dC_dA2 @ dA2_dZ2 @ dZ2_dA1
dC_dW1 = dC_dA1 @ dA1_dZ1 @ dZ1_dW1.T
dC_dB1 = dC_dA1 @ dA1_dZ1 * dZ1_dB1
return dC_dW1, dC_dB1, dC_dW2, dC_dB2
for i in range (100_000 ):
idx = np.random.choice(n, 1 , replace=False )
X_sample = X_train[idx].transpose()
Y_sample = Y_train[idx]
Z1, A1, Z2, A2 = forward_prop(X_sample)
dW1, dB1, dW2, dB2 = backward_prop(Z1, A1, Z2, A2, X_sample, Y_sample)
w_hidden -= L * dW1
b_hidden -= L * dB1
w_output -= L * dW2
b_output -= L * dB2
test_predictions = forward_prop(X_test.transpose())[3 ]
test_comparisons = np.equal((test_predictions >= .5 ).flatten().astype(int ), Y_test)
accuracy = sum (test_comparisons.astype(int ) / X_test.shape[0 ])
print ("ACCURACY: " , accuracy)
这里涉及很多内容,但是建立在我们在本章学到的一切基础之上。我们进行了 10 万次随机梯度下降迭代。将训练和测试数据分别按 2/3 和 1/3 划分,根据随机性的不同,我在测试数据集中获得了大约 97-99%的准确率。这意味着训练后,我的神经网络能够正确识别 97-99%的测试数据,并做出正确的浅色/深色字体预测。
backward_prop()
函数在这里起着关键作用,实现链式法则,将输出节点的误差(平方残差)分配并向后传播到输出和隐藏权重/偏差,以获得相对于每个权重/偏差的斜率。然后我们在for
循环中使用这些斜率,分别通过乘以学习率L
来微调权重/偏差,就像我们在第五章和第六章中所做的那样。我们进行一些矩阵-向量乘法,根据斜率向后传播误差,并在需要时转置矩阵和向量,以使行和列之间的维度匹配起来。
如果你想让神经网络更具交互性,这里有一段代码片段在示例 7-12 中,我们可以输入不同的背景颜色(通过 R、G 和 B 值),看看它是否预测为浅色或深色字体。将其附加到之前的代码示例 7-11 的底部,然后试一试!
示例 7-12. 为我们的神经网络添加一个交互式 shell
def predict_probability (r, g, b ):
X = np.array([[r, g, b]]).transpose() / 255
Z1, A1, Z2, A2 = forward_prop(X)
return A2
def predict_font_shade (r, g, b ):
output_values = predict_probability(r, g, b)
if output_values > .5 :
return "DARK"
else :
return "LIGHT"
while True :
col_input = input ("Predict light or dark font. Input values R,G,B: " )
(r, g, b) = col_input.split("," )
print (predict_font_shade(int (r), int (g), int (b)))
从头开始构建自己的神经网络需要大量的工作和数学知识,但它让你深入了解它们的真实本质。通过逐层工作、微积分和线性代数,我们对像 PyTorch 和 TensorFlow 这样的深度学习库在幕后做了什么有了更深刻的理解。
从阅读本整章节中你已经了解到,要使神经网络正常运转有很多要素。在代码的不同部分设置断点可以帮助你了解每个矩阵操作在做什么。你也可以将代码移植到 Jupyter Notebook 中,以获得更多对每个步骤的视觉洞察。
3Blue1Brown 关于反向传播的视频
3Blue1Brown 有一些经典视频讨论反向传播 和神经网络背后的微积分 。
使用 scikit-learn
scikit-learn 中有一些有限的神经网络功能。如果你对深度学习感兴趣,你可能会想学习 PyTorch 或 TensorFlow,并购买一台配备强大 GPU 的计算机(这是购买你一直想要的游戏电脑的绝佳借口!)。我被告知现在所有酷炫的孩子都在使用 PyTorch。然而,scikit-learn 中确实有一些方便的模型可用,包括MLPClassifier
,它代表“多层感知器分类器”。这是一个用于分类的神经网络,默认使用逻辑输出激活。
示例 7-13 是我们开发的背景颜色分类应用的 scikit-learn 版本。activation
参数指定了隐藏层。
示例 7-13. 使用 scikit-learn 神经网络分类器
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
df = pd.read_csv('https://bit.ly/3GsNzGt' , delimiter="," )
X = (df.values[:, :-1 ] / 255.0 )
Y = df.values[:, -1 ]
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=1 /3 )
nn = MLPClassifier(solver='sgd' ,
hidden_layer_sizes=(3 , ),
activation='relu' ,
max_iter=100_000 ,
learning_rate_init=.05 )
nn.fit(X_train, Y_train)
print (nn.coefs_ )
print (nn.intercepts_)
print ("Training set score: %f" % nn.score(X_train, Y_train))
print ("Test set score: %f" % nn.score(X_test, Y_test))
运行这段代码,我在测试数据上获得了约 99.3% 的准确率。
使用 scikit-learn 的 MNIST 示例
要查看一个使用 MNIST 数据集预测手写数字的 scikit-learn 示例,请参阅附录 A。
神经网络和深度学习的局限性
尽管神经网络具有许多优势,但在某些类型的任务上仍然存在困难。这种对层、节点和激活函数的灵活性使其能够以非线性方式拟合数据...可能太灵活了。为什么?它可能对数据过拟合。深度学习教育的先驱、谷歌大脑前负责人安德鲁·吴在 2021 年的一次新闻发布会上提到这是一个问题。在被问及为什么机器学习尚未取代放射科医生时,这是他在IEEE Spectrum 文章 中的回答:
结果表明,当我们从斯坦福医院收集数据,然后在同一家医院的数据上进行训练和测试时,确实可以发表论文,显示[算法]在发现某些病况方面与人类放射科医生相媲美。
结果表明,当你将同样的模型,同样的人工智能系统,带到街对面的一家老医院,使用一台老机器,技术人员使用稍有不同的成像协议时,数据漂移会导致人工智能系统的性能显著下降。相比之下,任何一名人类放射科医生都可以走到街对面的老医院并做得很好。
因此,即使在某个特定数据集上的某个时间点上,我们可以展示这是有效的,临床现实是这些模型仍然需要大量工作才能投入生产。
换句话说,机器学习过度拟合了斯坦福医院的训练和测试数据集。当应用到其他设备不同的医院时,由于过度拟合,性能显著下降。
自动驾驶汽车和无人驾驶汽车也面临相同的挑战。仅仅在一个停车标志上训练神经网络是不够的!它必须在围绕该停车标志的无数条件下进行训练:晴天、雨天、夜晚和白天、有涂鸦、被树挡住、在不同的地点等等。在交通场景中,想象所有不同类型的车辆、行人、穿着服装的行人以及将遇到的无限数量的边缘情况!简单地说,通过在神经网络中增加更多的权重和偏差来捕捉在道路上遇到的每种事件是没有效果的。
这就是为什么自动驾驶汽车本身不会以端到端的方式使用神经网络。相反,不同的软件和传感器模块被分解,其中一个模块可能使用神经网络在物体周围画一个框。然后另一个模块将使用不同的神经网络对该框中的物体进行分类,比如行人。从那里,传统的基于规则的逻辑将尝试预测行人的路径,硬编码逻辑将从不同的条件中选择如何反应。机器学习仅限于标记制作活动,而不涉及车辆的战术和机动。此外,像雷达这样的基本传感器在车辆前方检测到未知物体时将会停止,这只是技术堆栈中另一个不使用机器学习或深度学习的部分。
尽管媒体头条报道神经网络和深度学习在诸如国际象棋和围棋等游戏中击败人类 ,甚至胜过战斗飞行模拟中的飞行员 ,这可能令人惊讶。在这样的强化学习环境中,需要记住模拟是封闭的世界,在这里可以生成无限量的标记数据,并通过虚拟有限世界进行学习。然而,现实世界并非我们可以生成无限量数据的模拟环境。此外,这不是一本哲学书,所以我们将不讨论我们是否生活在模拟中。抱歉,埃隆!在现实世界中收集数据是昂贵且困难的。除此之外,现实世界充满了无限的不可预测性和罕见事件。所有这些因素驱使机器学习从业者转向数据录入劳动来标记交通物体的图片 和其他数据。自动驾驶汽车初创公司通常必须将这种数据录入工作与模拟数据配对,因为需要生成训练数据的里程数和边缘情况场景太过庞大,无法简单地通过驾驶数百万英里的车队来收集。
这些都是人工智能研究喜欢使用棋盘游戏和视频游戏的原因,因为可以轻松干净地生成无限标记数据。谷歌的著名工程师弗朗西斯·乔勒特(Francis Chollet)为 TensorFlow 开发了 Keras(还写了一本很棒的书,Python 深度学习 ),在一篇Verge 文章 中分享了一些见解:
问题在于,一旦你选择了一个度量标准,你就会采取任何可用的捷径来操纵它。例如,如果你将下棋作为智能的度量标准(我们从上世纪 70 年代开始一直持续到 90 年代),你最终会得到一个只会下棋的系统,仅此而已。没有理由认为它对其他任何事情都有好处。你最终会得到树搜索和极小化,这并不能教会你任何关于人类智能的东西。如今,将追求在像 Dota 或 StarCraft 这样的视频游戏中的技能作为一种普遍智能的替代品,陷入了完全相同的智力陷阱...
如果我着手使用深度学习以超人水平“解决”《魔兽争霸 III》,只要我有足够的工程人才和计算能力(这类任务需要数千万美元的资金),你可以确信我会成功。但一旦我做到了,我会学到关于智能或泛化的什么?嗯,什么也没有。充其量,我会开发关于扩展深度学习的工程知识。所以我并不认为这是科学研究,因为它并没有教会我们任何我们不已经知道的东西。它没有回答任何未解之谜。如果问题是,“我们能以超人水平玩 X 吗?”,答案肯定是,“是的,只要你能生成足够密集的训练情境样本,并将它们输入到一个足够表达力的深度学习模型中。” 这一点我们已经知道有一段时间了。
换句话说,我们必须小心,不要混淆算法在游戏中的表现与尚未解决的更广泛能力。机器学习、神经网络和深度学习都是狭义地解决定义明确的问题。它们不能广泛推理或选择自己的任务,也不能思考之前未见过的对象。就像任何编码应用程序一样,它们只会执行它们被编程要执行的任务。
无论使用什么工具,解决问题是最重要的。不应该偏袒神经网络或其他任何可用工具。在这一点上,使用神经网络可能不是你面临的任务的最佳选择。重要的是要始终考虑你正在努力解决的问题,而不是把特定工具作为主要目标。深度学习的使用必须是策略性的和有充分理由的。当然有使用案例,但在你的日常工作中,你可能会更成功地使用简单和更偏向的模型,如线性回归、逻辑回归或传统的基于规则的系统。但如果你发现自己需要对图像中的对象进行分类,并且有预算和人力来构建数据集,那么深度学习将是你最好的选择。
结论
神经网络和深度学习提供了一些令人兴奋的应用,我们在这一章中只是触及了表面。从识别图像到处理自然语言,继续应用神经网络及其不同类型的深度学习仍然有用武之地。
从零开始,我们学习了如何构建一个具有一个隐藏层的简单神经网络,以预测在背景颜色下是否应该使用浅色或深色字体。我们还应用了一些高级微积分概念来计算嵌套函数的偏导数,并将其应用于随机梯度下降来训练我们的神经网络。我们还涉及到了像 scikit-learn 这样的库。虽然在本书中我们没有足够的篇幅来讨论 TensorFlow、PyTorch 和更高级的应用,但有很多优秀的资源可以扩展你的知识。
3Blue1Brown 有一个关于神经网络和反向传播的精彩播放列表 ,值得多次观看。Josh Starmer 的 StatQuest 播放列表关于神经网络 也很有帮助,特别是在将神经网络可视化为流形操作方面。关于流形理论和神经网络的另一个优秀视频可以在Art of the Problem 这里找到 。最后,当你准备深入研究时,可以查看 Aurélien Géron 的使用 Scikit-Learn、Keras 和 TensorFlow 进行实践机器学习 (O’Reilly)和 Francois Chollet 的Python 深度学习 (Manning)。
如果你读到了本章的结尾,并且觉得你合理地吸收了一切,恭喜!你不仅有效地学习了概率、统计学、微积分和线性代数,还将其应用于线性回归、逻辑回归和神经网络等实际应用。我们将在下一章讨论你如何继续前进,并开始你职业成长的新阶段。
练习
将神经网络应用于我们在第六章中处理的员工留存数据。您可以在这里 导入数据。尝试构建这个神经网络,使其对这个数据集进行预测,并使用准确率和混淆矩阵来评估性能。这对于这个问题是一个好模型吗?为什么?
虽然欢迎您从头开始构建神经网络,但考虑使用 scikit-learn、PyTorch 或其他深度学习库以节省时间。
答案在附录 B 中。
第八章:职业建议与前行之路
随着本书的结束,评估接下来该怎么做是个不错的主意。你学习并整合了广泛的应用数学主题:微积分、概率论、统计学和线性代数。然后,你将这些技术应用于实际应用,包括线性回归、逻辑回归和神经网络。在本章中,我们将讨论如何在前进的同时利用这些知识,航行在数据科学职业这个奇特、激动人心且多样化的领域中。我将强调拥有方向和明确的目标的重要性,而不是仅仅死记工具和技术,而缺乏实际问题的解决方案。
由于我们正在远离基础概念和应用方法,这一章的语调将与书中其余部分有所不同。你可能期待了解如何将这些数学建模技能以专注和实际的方式应用于你的职业生涯。然而,如果你希望在数据科学职业中取得成功,你将需要学习一些更硬的技能,比如 SQL 和编程,以及发展专业意识的软技能。后者尤为重要,这样你就不会在数据科学这个变幻莫测、看不见的市场力量盲目地中迷失方向。
我不会假设我知道你的职业目标或你希望通过这些信息实现什么。但我会打几个保险,因为你正在阅读这本书。我想象你可能对数据科学职业感兴趣,或者你已经在数据分析方面工作,并希望系统化你的分析知识。也许你来自软件工程背景,希望掌握人工智能和机器学习。也许你是某种项目经理,发现你需要了解数据科学或人工智能团队的能力,以便据此进行范围确定。也许你只是一个好奇的专业人士,想知道数学如何在实际层面上有用,而不仅仅是学术上的。
我将尽力满足所有这些群体的关切,并希望总结出一些对大多数读者有用的职业建议。让我们从重新定义数据科学开始。我们已经客观地研究了它,并将在职业发展和该领域的未来的背景下进行讨论。
作者是否在讲故事?
在像这样的任务中很难不显得像轶事,我在这里分享了自己的经验(以及他人的经验),而不是像我在第三章中提倡的大规模、控制的调查和研究。但是我要说的是:我在财富 500 强世界工作了十多年,看到了数据科学运动对组织的转变。我在世界各地的许多技术会议上发表演讲,并听取了无数同行的反应,“这种情况也发生在我们身上!”我阅读了许多博客和受人尊敬的出版物,从华尔街日报 到福布斯 ,我学会了认识到流行期望与现实之间的脱节。我特别关注各行各业的权威人士、领导者和追随者,以及他们如何通过数据科学和人工智能来制定或跟随市场。目前,我在南加州大学教授和指导人工智能在安全关键应用中的利益相关者,涉及航空安全和安全项目。
我只是把简历放在这里,来展示虽然我没有进行正式的调查和研究,也许我正在整理轶事信息,但我在所有这些信息源中找到了持续的叙述。Vicki Boykis,Tumblr 的敏锐的机器学习工程师,写了一篇博客文章分享了与我类似的发现 ,我强烈建议你阅读一下。你可以带着怀疑的心态接受我的发现,但要密切关注你自己工作环境中正在发生的事情,并识别你的管理层和同事们认同的假设。
重新定义数据科学
数据科学是分析数据以获得可操作洞见。实际上,它是将不同与数据相关的学科融合在一起的一个整体:统计学、数据分析、数据可视化、机器学习、运筹学、软件工程……只是举几个例子。几乎任何涉及数据的学科都可以被标志为“数据科学”。这种缺乏明确定义对该领域造成了问题。毕竟,任何缺乏定义的事物都容易被解释,就像一幅抽象艺术品一样。这就是为什么人力资源部门在“数据科学家”职位发布时感到困惑,因为它们往往七零八落 。图 8-1 显示了一个涵盖不同学科和工具的伞形图,可以归入数据科学的范畴。
图 8-1. 数据科学的伞形图
我们是如何到达这一步的?一个像数据科学这样缺乏定义的东西如何在企业世界中成为如此有吸引力的力量?最重要的是,它的定义(或缺乏定义)如何影响你的职业生涯?这些都是我们在本章讨论的重要问题。
这就是为什么我告诉我的客户,数据科学 的更好定义是具备统计学、机器学习和优化技能的软件工程。如果去掉这四个学科中的任何一个(软件工程、统计学、机器学习和优化),数据科学家的表现就会受到威胁。大多数组织在界定使数据科学家有效的技能集方面一直存在困难,但提供的定义应该能带来清晰度。尽管有些人可能认为软件工程是一个有争议的要求,但考虑到行业发展的方向,我认为这是非常必要的。我们将在后面讨论这一点。
但首先,理解数据科学的最佳方式是追溯这个术语的历史。
数据科学的简史
我们可以追溯数据科学一直到统计学的起源,早在17 世纪甚至 8 世纪 。为了简洁起见,让我们从 1990 年代开始。分析师、统计学家、研究员、“量化分析师”和数据工程师通常拥有不同的角色。工具堆栈通常包括电子表格、R、MATLAB、SAS 和 SQL。
当然,在 2000 年后,事情开始迅速改变。互联网和连接设备开始产生大量数据。随着 Hadoop 的诞生,谷歌推动分析和数据收集达到了前所未有的高度。随着 2010 年的临近,谷歌的高管们坚信统计学家将在接下来的十年中拥有“性感”的工作 。接下来发生的事情证明了他们的预见。
2012 年,《哈佛商业评论 》推广了这个称为数据科学的概念,并宣称它是“21 世纪最性感的工作” 。在《哈佛商业评论 》文章之后,许多公司和企业工作者争相填补数据科学的空白。管理顾问们则准备好向财富 500 强领导者传授如何将数据科学引入他们的组织中。SQL 开发人员、分析师、研究员、量化分析师、统计学家、工程师、物理学家以及众多其他专业人士纷纷将自己重新打造为“数据科学家”。科技公司感到传统的角色名称如“分析师”、“统计学家”或“研究员”听起来过时,因此将这些角色改称为“数据科学家”。
自然地,财富 500 强公司的管理层受到 C 级高管的压力,要跟上数据科学的潮流。最初的理由是正在收集大量数据,因此大数据正在成为一种趋势,需要数据科学家从中获取洞见。在此期间,“数据驱动”这个词成为各行各业的格言。企业界相信,与人不同,数据是客观和无偏的。
提醒:数据不是客观和无偏的!
直到今天,许多专业人士和管理者都陷入了数据客观和无偏见的谬误中。希望在阅读本书后,您知道这简直是不可能的,需要有关此事的提醒,请参阅第三章。
公司的管理层和人力资源,在无法与 FAANG(Facebook、Amazon、Apple、Netflix 和 Google)抢购的专业深度学习博士竞争的情况下,仍然承受着数据科学的压力,他们做出了一个有趣的举动。他们把现有的分析师、SQL 开发人员和 Excel 高手重新打造成“数据科学家”。Google 的首席决策科学家 Cassie Kozyrkov 在 2018 年的 Hackernoon 博客文章中描述了这个公开的秘密:
在我担任每一个数据科学家 职称的时候,实际上我早在重新品牌的 HR 官员对员工数据库进行小小的整形手术之前,已经在用另一种名字做同样的工作了。我的职责丝毫没有改变。我不是个例外;我的社交圈子里到处都是曾经的统计学家、决策支持工程师、量化分析师、数学教授、大数据专家、商业智能专家、分析主管、研究科学家、软件工程师、Excel 高手、小众博士幸存者……所有这些人都是今天自豪的数据科学家。
从技术上讲,数据科学并不排除这些专业人士中的任何一个,因为他们都在使用数据来获取洞见。当然,科学界也有些反对意见,他们不愿意把数据科学正式认定为一门真正的科学。毕竟,你能想到一门不使用数据的科学吗?2011 年,现任 Google TensorFlow 主管的 Pete Warden 在一篇 O’Reilly 文章中对数据科学运动进行了有趣的辩护。他也清楚地阐述了反对者对缺乏定义的论点:
[关于数据科学缺乏定义的问题] 可能是最深刻的反对意见,也是最有力的一条。对于数据科学的范围内外界限,尚无广泛接受的界定。它只是统计学的时尚重新包装吗?我不这么认为,但我也没有一个完整的定义。我相信最近数据的丰富使世界上发生了一些新的事情,当我四处看时,我看到有共同特征的人们,他们不适合传统的分类。这些人往往超越主导企业和机构世界的狭窄专业领域,处理从查找数据、大规模处理、可视化到撰写成故事的一切。他们似乎首先从数据告诉他们的内容开始,并选择追随有趣的线索,而不是传统科学家的方法,先选择问题然后找数据来阐明。
Pete 讽刺地也无法给数据科学提供一个定义,但他清楚地阐述了为什么数据科学是有缺陷但有用的。他还强调了研究放弃科学方法,转而采用曾经被摒弃的数据挖掘等实践的转变,我们在第三章中谈到过。
数据科学在《哈佛商业评论》文章发表几年后发生了有趣的转变。也许这更像是与人工智能和机器学习的合并,而不是一个转变。无论如何,当机器学习和深度学习在 2014 年左右成为头条新闻时,数据被称为创造人工智能的“燃料”。这自然地扩大了数据科学的范围,并与 AI/机器学习运动相结合。特别是,ImageNet 挑战激发了对 AI 的新兴兴趣,并引发了机器学习和深度学习的复兴。Waymo 和特斯拉等公司承诺几年内推出自动驾驶汽车,这得益于深度学习的进步,进一步推动了媒体头条新闻和夏令营的报名。
这种对神经网络和深度学习的突然兴趣产生了一个有趣的副作用。像决策树、支持向量机和逻辑回归这样的回归技术,几十年来一直隐藏在学术界和专业统计领域中,随着深度学习的光环走进了公众的视野。与此同时,像 scikit-learn 这样的库降低了进入该领域的门槛。这带来了一个隐藏的成本,即创建了一些不理解这些库或模型如何工作但仍然使用它们的数据科学专业人士。
由于数据科学的学科发展速度比对其定义的感知需求快得多,一个数据科学家的角色往往是完全不确定的。我曾经遇到过几位在财富 500 强公司担任数据科学家职位的人。有些人在编码方面非常熟练,甚至可能有软件工程背景,但对统计显著性一无所知。还有一些人局限于使用 Excel,几乎不懂 SQL,更不用说 Python 或 R 了。我曾经遇到过自学了一些 scikit-learn 函数的数据科学家,很快发现自己陷入困境,因为那是他们所知道的全部。
那么这对你意味着什么呢?在这样一个充斥着术语和混乱的环境中,你如何才能蓬勃发展呢?一切都取决于你对什么类型的问题或行业感兴趣,并且不要急于依赖雇主来定义角色。你不必成为数据科学家才能从事数据科学。有很多领域可以利用你现在拥有的知识为你带来优势。你可以成为分析师、研究员、机器学习工程师、顾问、咨询师,以及许多其他不一定称为数据科学家的角色。
但首先,让我们讨论一些你可以继续学习并在数据科学就业市场上找到优势的方法。
发现你的优势
实际数据科学专业人士需要的不仅仅是统计和机器学习的理解。在大多数情况下,期望数据能够轻松地用于机器学习和其他项目是不现实的。相反,你会发现自己在追寻数据来源、编写脚本和软件、抓取文档、抓取 Excel 工作簿,甚至创建自己的数据库。至少 95%的编码工作与机器学习或统计建模无关,而是创建、移动和转换数据以便使用。
此外,你还需要了解你的组织的全局视角和动态。你的管理者可能会在定义你的角色时做出一些假设,识别这些假设非常重要,这样你才能意识到它们对你的影响。当你依赖你的客户和领导的行业专业知识时,你应该在提供技术知识并表达可行性方面发挥作用。让我们来看看你可能需要的一些硬技能和软技能。
SQL 熟练度
SQL ,也称为结构化查询语言 ,是一种用于检索、转换和写入表数据的查询语言。关系数据库 是组织数据的最常见方式,将数据存储到表中,这些表像 Excel 中的 VLOOKUP 一样相互连接。MySQL、Microsoft SQL Server、Oracle、SQLite 和 PostgreSQL 等关系数据库平台都支持 SQL。正如你可能注意到的,SQL 和关系数据库紧密耦合,以至于“SQL”经常用于关系数据库的品牌营销,例如“MySQL”和“Microsoft SQL Server”。
示例 8-1 是一个简单的 SQL 查询,从CUSTOMER
表中检索CUSTOMER_ID
和NAME
字段,条件是STATE
为'TX'
。
示例 8-1. 一个简单的 SQL 查询
SELECT CUSTOMER_ID, NAME
FROM CUSTOMER
WHERE STATE = 'TX'
简而言之,作为数据科学专业人士,没有 SQL 的熟练技能很难有所成就。企业使用数据仓库,而 SQL 几乎总是检索数据的手段。SELECT
、WHERE
、GROUP BY
、ORDER BY
、CASE
、INNER JOIN
和 LEFT JOIN
这些 SQL 关键字都应该很熟悉。更好的是,了解子查询、派生表、公共表表达式和窗口函数,以充分利用你的数据。
厚颜无耻的自荐:作者有一本 SQL 书!
我曾为 O'Reilly 写过一本入门级的 SQL 书,名为Getting Started with SQL 。它只有一百多页,可以在一天内完成。书中涵盖了基本内容,包括连接和聚合,以及创建自己的数据库。该书使用 SQLite,可以在不到一分钟内设置好。
还有 O'Reilly 出版的其他出色的 SQL 书籍,包括 Alan Beaulieu 的《Learning SQL, 3rd Ed.》和 Alice Zhao 的《SQL Pocket Guide, 4th Ed.》。在速读完我那一百页的入门手册之后,也可以查看这两本书。
SQL 在让 Python 或其他编程语言轻松访问数据库方面也非常关键。如果你想从 Python 向数据库发送 SQL 查询,你可以将数据作为 Pandas DataFrames、Python 集合和其他结构返回。
示例 8-2 展示了使用 SQLAlchemy 库在 Python 中运行简单 SQL 查询。它以命名元组的形式返回记录。只需确保下载这个SQLite 数据库文件 并放置在你的 Python 项目中,然后运行pip install sqlalchemy
。
示例 8-2. 使用 SQLAlchemy 在 Python 中运行 SQL 查询
from sqlalchemy import create_engine, text
engine = create_engine('sqlite:///thunderbird_manufacturing.db' )
conn = engine.connect()
stmt = text("SELECT * FROM CUSTOMER" )
results = conn.execute(stmt)
for customer in results:
print (customer)
关于 Pandas 和 NoSQL 呢?
我经常收到关于与 SQL 的“替代品”(如 NoSQL 或 Pandas)的问题。事实上,这些并不是替代品,而是数据科学工具链中其他地方的不同工具。以 Pandas 为例,在示例 8-3 中,我可以创建一个 SQL 查询,从表CUSTOMER
中提取所有记录并将它们放入 Pandas 的DataFrame
中。
示例 8-3. 将 SQL 查询导入到 Pandas DataFrame 中
from sqlalchemy import create_engine, text
import pandas as pd
engine = create_engine('sqlite:///thunderbird_manufacturing.db' )
conn = engine.connect()
df = pd.read_sql("SELECT * FROM CUSTOMER" , conn)
print (df)
SQL 被用来在关系数据库和我的 Python 环境之间建立桥梁,并将数据加载到 Pandas 的DataFrame
中。如果我有需要使用 SQL 处理的复杂计算任务,通过 SQL 在数据库服务器上执行要比在本地计算机上使用 Pandas 更高效。简单来说,Pandas 和 SQL 可以共同工作,而不是竞争技术。
NoSQL 也是如此,包括像 Couchbase 和 MongoDB 这样的平台。虽然一些读者可能会有不同意见并提出合理的论点,但我认为将 NoSQL 与 SQL 进行比较就像是在比较苹果和橙子。是的,它们都存储数据并提供查询功能,但我不认为它们是竞争关系。它们针对不同的用例具有不同的特性。NoSQL 代表“不仅仅是 SQL”,更适合存储非结构化数据,如图片或自由格式的文本文章。SQL 更适合存储结构化数据。SQL 比 NoSQL 更积极地维护数据完整性,尽管以计算开销和较低的可扩展性为代价。
编程熟练度
通常,许多数据科学家并不擅长编程,至少不像软件工程师那样。然而,掌握编程技能变得越来越重要。这提供了获得优势的机会。学习面向对象编程、函数式编程、单元测试、版本控制(例如 Git 和 GitHub)、Big-O 算法分析、密码学以及其他你遇到的相关计算机科学概念和语言特性。
为什么呢?假设你创建了一个有前途的回归模型,比如逻辑回归或神经网络,基于一些给定的样本数据。你请求内部 IT 部门的程序员将其“插入”到现有软件中。
他们警惕地看着你的提议。“我们需要用 Java 重写这个,而不是 Python,”他们不情愿地说道。“你的单元测试在哪里?”另一个问道。“你没有定义任何类或类型吗?我们必须重新设计这段代码使其面向对象。”除此之外,他们不理解你的模型的数学,并担心它在未见过的数据上表现不佳。由于你没有定义单元测试(这在机器学习中并不直接),他们不确定如何验证你的模型的质量。他们还问两个代码版本(Python 和 Java)如何管理?
你开始感到不在自己的领域内,并说,“我不明白为什么 Python 脚本不能直接插入。”其中一人沉思片刻后回答道,“我们可以使用 Flask 创建一个 web 服务,避免重新用 Java 编写。但是,其他问题并没有消失。我们接下来必须担心可扩展性和高访问量对 web 服务的影响。等等…也许我们可以部署到微软 Azure 云上作为一个虚拟机规模集,但我们仍然必须设计后端。看,无论你如何处理,这都必须重新设计。”
这正是为什么许多数据科学家的工作从不离开他们的笔记本电脑。事实上,将机器学习投入生产已经变得如此难以捉摸,以至于它已经成为近年来的一个独角兽和热门话题。数据科学家和软件工程师之间存在巨大的鸿沟,因此自然而然地,数据科学专业人员现在也要成为软件工程师。
这可能听起来令人不知所措,因为数据科学的范围已经很广泛,涵盖了许多学科和要求。但是,这并不意味着你需要学习 Java。你可以成为一名有效的软件工程师,使用 Python(或者你喜欢的任何就业语言),但你必须擅长它。学习面向对象编程、数据结构、函数式编程、并发编程和其他设计模式。两本解决这些问题的好书包括 Luciano Ramalho 的《流畅的 Python,第二版》(O’Reilly)和 Al Sweigart 的《Python 基础进阶》(No Starch)。
之后,学习解决实际任务,包括数据库 API、web 服务 、JSON 解析 、正则表达式 、网页抓取 、安全和加密 、云计算(Amazon Web Services、Microsoft Azure)以及其他任何有助于你在建立系统时提高生产力的东西。
正如前文所述,你精通的编程语言不一定非得是 Python。它可以是其他语言,但建议选择那些普遍使用和具有就业能力的语言。目前具有较高就业能力的语言包括 Python、R、Java、C# 和 C++。Swift 和 Kotlin 在苹果和安卓设备上占据主导地位,它们都是出色的、得到很好支持的语言。尽管这些语言大多数不是数据科学的主流,学习至少另外一门语言以增加曝光是有帮助的。
锚定偏差与首选编程语言
对技术专业人士而言,对技术和平台产生偏爱和情感投入是很常见的,特别是对编程语言。请不要这样做!这种部落主义是没有生产力的,它忽略了每种编程语言服务不同质量和用例的现实。另一个现实是,一些编程语言流行而另一些不流行,通常原因与语言设计的优点无关。如果一家大公司不支付其支持费用,它的生存机会就很渺茫。
我们在第三章讨论了不同类型的认知偏差。另一个是锚定偏差 ,它指出我们可能会偏爱第一次学到的东西,比如一门编程语言。如果你感觉有义务学习一门新语言,要开放心态,给它一个机会!没有完美的语言,重要的是它能完成任务。
但是,如果语言的支持情况堪忧,比如它处于维护状态,没有更新,或者缺乏企业支持者,就要小心了。例如,微软的VBA ,Red Hat 的Ceylon ,以及Haskell 。
数据可视化
另一项你应该具备一定程度熟练度的技术技能是数据可视化。能够轻松制作图表、图形和绘图,不仅能向管理层讲述故事,还能帮助自己的数据探索工作。你可以用 SQL 命令总结数据,但有时柱状图或散点图能够更快地让你了解数据。
当涉及到选择数据可视化工具时,这个问题就变得更加难以回答,因为选择太多,而且分散。如果你在传统办公环境中工作,Excel 和 PowerPoint 通常是首选的可视化工具,你知道吗?它们完全可以胜任!虽然我不是所有事情都用它们,但它们确实能完成绝大多数任务。需要在小/中型数据集上绘制散点图?或者直方图?没问题!只需将数据复制/粘贴到 Excel 工作簿中,几分钟内就能生成一个。这对于一次性图形可视化非常方便,在这种情况下使用 Excel 毫无羞耻感。
然而,有些情况下,你可能希望脚本化创建图表,以便重复使用或与你的 Python 代码集成。matplotlib 已经是一段时间以来的首选,当你的平台是 Python 时,很难避免。Seaborn 在 matplotlib 之上提供了一个包装器,使其更易于使用常见的图表类型。SymPy,在本书中我们经常使用的库,使用 matplotlib 作为其后端。然而,有些人认为 matplotlib 非常成熟,已经接近遗留状态。类似Plotly 的库已经崭露头角,使用起来非常愉快,它基于 JavaScript 的D3.js 库 。就个人而言,我在Manim 方面取得了成功。它产生的 3Blue1Brown 风格的可视化效果非常出色,给客户带来了“哇!”的感觉,而且考虑到它具有的动画功能,其 API 使用起来令人惊讶地简单。然而,这是一个年轻的库,尚未成熟,这意味着每个发布版本的演变可能会导致代码变更。
探索所有这些解决方案不会有错,如果雇主/客户没有偏好,可以找到最适合自己的一个。
商业许可证平台如Tableau 在某种程度上还算不错。他们致力于创建专门用于可视化的专有软件,并创建了一个拖放界面,使其对非技术用户也可访问。Tableau 甚至有一篇标题为“让你的组织中每个人都成为数据科学家” 的白皮书,这并没有解决之前提到的数据科学家定义问题。我发现 Tableau 的挑战在于它只擅长可视化,并需要昂贵的许可证。虽然你可以在TabPy 中部分集成 Python,但除非雇主想使用 Tableau,否则你可能会选择使用之前提到的功能强大的开源库。
软件许可证可能涉及政治问题。
想象一下,你创建了一个 Python 或 Java 应用程序,请求用户输入一些信息,检索和处理不同的数据源,运行一些高度定制的算法,然后呈现可视化和显示结果的表格。经过几个月的辛苦工作后,在会议上展示出来,但是其中一位经理举手发问:“为什么不直接在 Tableau 中完成这个任务呢?”
对于一些经理来说,这是一个难以接受的现实,他们花费了数千美元购买企业软件许可证,而您来了,使用一个更强大(尽管使用起来更复杂)且没有许可成本的开源解决方案。您可以强调 Tableau 不支持您必须创建的这些算法或集成工作流程。毕竟,Tableau 只是可视化软件。它不是一个从头开始编码的平台,无法创建定制的高度量身定制的解决方案。
领导层通常被告知 Tableau、Alteryx 或其他商业工具可以做到一切。毕竟,他们为此花了大量的钱,而且可能供应商给了他们一个很好的销售演示。自然地,他们想要为这些成本辩护,并希望尽可能多的人使用许可证。他们可能还花了额外的预算培训员工使用该软件,并希望其他人能够维护您的工作。
对此要敏感。如果管理层要求您使用他们支付的工具,请探索是否可以让其发挥作用。但如果在您特定的任务中存在限制或严重的可用性妥协,请在一开始就礼貌地提出来。
了解您的行业
让我们比较两个行业:视频流媒体(例如 Netflix)和航空航天防务(例如洛克希德·马丁)。它们有共同之处吗?几乎没有!两者都是技术驱动型公司,但一个是为消费者提供电影流媒体,另一个是制造带弹药的飞机。
当我在人工智能和系统安全方面提供建议时,我首先指出这两个行业对风险的容忍度有很大不同。一个电影流媒体公司可能会吹嘘他们拥有一个能学习推荐给消费者电影的 AI 系统,但当它给出一个糟糕的推荐时,情况有多严重?最坏的情况是消费者可能会稍感失望,浪费了两个小时观看他们不喜欢的电影。
但是航空航天防务公司呢?如果一架战斗机上搭载了自动射击目标的 AI,如果它出错了会有多严重?现在我们谈论的是人类生命,而不是电影推荐!
这两个行业的风险容忍度差距很大。自然地,航空航天防务公司在实施任何实验性系统时都会更为保守。这意味着官僚主义和安全工作组会评估并阻止他们认为风险不可接受的任何项目,这是理所当然的。然而,有趣的是,在硅谷初创公司中,AI 在像电影推荐这样的低风险应用中取得了成功,这引发了防务工业高管和领导层的 FOMO(“错失良机的恐惧”)。这可能是因为这两个领域之间的风险容忍度差距没有被充分强调。
当然,在这两个行业之间的风险严重程度之间有着广泛的谱系,“恼怒的用户”和“人类生命的丧失”之间。银行可能会使用 AI 来确定谁有资格获得贷款,但这带来了在歧视某些人口统计学特征方面的风险。刑事司法系统在假释和监视系统中尝试使用 AI,但也遇到了同样的歧视问题。社交媒体可能使用 AI 来确定哪些用户发布的内容是可接受的,但当它压制“无害”内容(虚假阳性)时,会激怒其用户,也会让立法者感到不满,因为“有害”内容没有被压制(虚假阴性)。
这表明了了解自己所在行业的必要性。如果你想要做大量的机器学习,你可能会希望在低风险行业工作,其中虚假阳性和虚假阴性不会危及或激怒任何人。但如果这些都不吸引你,你想要从事像自动驾驶汽车 、航空和医学等更大胆的应用,那么期望你的机器学习模型会被频繁拒绝。
在这些高风险行业中,如果需要特定的博士学位或其他正式证书,不要感到意外。即使拥有专业的博士学位,虚假阳性和虚假阴性也不会神奇地消失。如果你不想追求这种专业化,你可能最好学习除了机器学习之外的其他工具,包括软件工程、优化、统计学和业务规则系统/启发式方法。
有效的学习
在 2008 年的单口喜剧特别节目中,喜剧演员布莱恩·雷根将自己对不读报纸的人的缺乏好奇心与那些读报纸的人进行了对比。指出头版新闻故事永远不会完结,他说他没有兴趣翻到指定的页面去找出它的结局。“经过九年的审判,陪审团最终做出了继续在第 22 页第 C 栏上继续的裁决……我想我永远也不会知道,”他轻蔑地开玩笑说。然后他与那些翻页的人进行对比,喊道,“我想学习!我想成为一个学习者!”
尽管布莱恩·雷根可能本意是自嘲,也许他在某些方面是对的。仅仅为了学习而学习一个主题几乎没有动力,而且对于缺乏兴趣并不总是一件坏事。如果你拿起一本微积分教科书,没有学习它的目的,你可能最终会感到沮丧和挫败。你需要有一个项目或目标,如果你觉得某个主题无趣,为什么要去学习它呢?就我个人而言,当我允许自己对我认为无关紧要的主题失去兴趣时,这是非常解放的。更令人惊讶的是,我的工作效率飙升了。
这并不意味着你不应该保持好奇心。然而,那里有如此多的信息,优先考虑你学习的内容是一项无价的技能。你可以问为什么某些东西有用,如果你得不到一个直接的答案,那就允许自己继续前进!所有人都在谈论自然语言处理吗?这并不意味着你非得去做!大多数企业根本不需要自然语言处理,所以说它不值得你的努力和时间是可以的。
无论你在工作中有项目还是为了自学而自主创建项目,都应该有具体的目标。只有你 可以决定什么值得学习,你可以摒弃在追求你感兴趣和相关的事物时可能产生的焦虑。
从业者与顾问
这可能是一种概括,但有两种类型的知识专家:从业者和顾问。为了找到你的优势,辨别你想成为哪种类型,并相应地调整你的职业发展。
在数据科学和分析领域,从业者们编写代码、创建模型、搜寻数据,并试图直接创造价值。顾问就像顾问,告诉管理层他们的目标是否合理,帮助制定战略,并提供方向。有时候,从业者可以逐步成为顾问。有时候,顾问从未是从业者。每种角色都有其利弊。
从业者可能喜欢编码、进行数据分析,并执行直接能够创造价值的具体工作。从业者的好处在于他们实际上开发并拥有硬技能。然而,沉迷于代码、数学和数据很容易让人失去大局观,并与组织和行业的其他部分脱节。我经常听到经理抱怨的一个常见问题是,他们的数据科学家想要解决他们认为有趣但对组织没有价值的问题。我也听到从业者抱怨,他们想要曝光和上升空间,但感觉在组织中被束缚和隐藏。
顾问在某些方面确实有一个更轻松的工作。他们向经理们提供建议和信息,并帮助企业提供战略方向。他们通常不是编写代码或搜寻数据的人,但他们帮助管理层雇佣那些从事这些工作的人。他们的职业风险是不同的,因为他们不必担心满足冲刺期限、处理代码错误或者模型运行不良的问题,就像从业者们那样。但是他们确实需要担心保持知识更新、可信和相关性。
要成为一个有效的顾问,您必须真正了解并掌握其他人不了解的知识。这必须是与客户需求密切相关的关键信息。要保持相关性,您每天都必须阅读,寻找并综合他人忽视的信息。仅仅熟悉机器学习、统计学和深度学习是不够的。您还必须关注客户所在的行业以及其他行业,追踪谁在成功谁在失败。您还必须学会将正确的解决方案与正确的问题匹配,在一个许多人寻找银弹的商业环境中。为了做到这一切,您必须成为一个有效的沟通者,并以帮助客户为目标分享信息,而不仅仅是展示自己的知识。
顾问面临的最大风险是提供错误的信息。一些顾问非常善于将责任推向外部因素,比如“行业内没有人预见到这一点”或“这是一个六西格玛事件!”意味着一个不希望发生的事件出现的概率是五亿分之一,但却发生了。另一个风险是没有从业者的硬技能,并且与业务技术层面脱节。这就是为什么定期在家练习编码和建模,或者至少将技术书籍作为阅读的一部分是个好主意。
最后,一个好的顾问努力成为客户与他们最终目标之间的桥梁,经常填补存在的巨大知识空白。这不是为了计费最大小时数和编造繁忙工作,而是真正识别客户的困扰,并帮助他们安心入睡。
当项目计划基于工具而非问题时,项目成功的可能性很低。这意味着作为顾问,您必须磨练自己的倾听技巧,并识别客户难以提出甚至回答的问题。如果一家主要快餐连锁店聘请您协助“AI 战略”,而您看到他们的人力资源部门急于聘请深度学习人才,您的工作就是问,“您试图解决深度学习中的哪些问题?”如果得不到明确答案,您需要鼓励管理层退后一步,评估他们真正面临的行业问题。他们是否存在员工排班效率低下的问题?嗯,他们不需要深度学习,他们需要线性规划!对于一些读者而言,这可能显得很基础,但如今许多管理人员往往难以进行这些区分。我曾多次遇到将其线性规划解决方案品牌化为 AI 的供应商和顾问,这在语义上可以与深度学习混淆。
数据科学职位需注意事项
要理解数据科学职位市场,可能需要将其与一部深刻的美国电视作品进行比较。
在 2010 年,有一部美国电视剧Better Off Ted 。其中一集名为“Jabberwocky”(第 1 季,第 12 集),深刻揭示了企业行话的某种本质。在节目中,主角泰德(Ted)在公司虚构了一个名为“Jabberwocky”的项目来隐藏资金。由此引发了一连串滑稽的结果,他的经理、CEO,最终整个公司都开始“参与”这个“Jabberwocky”项目,甚至都不知道这究竟是什么。局面逐渐升级,成千上万的员工假装在“Jabberwocky”上工作,而没有人停下来问他们究竟在做什么。原因在于:没有人愿意承认自己不了解这个重要事务,处于不在圈内、无知的状态。
Jabberwocky 效应 是一个轶事性理论,即一个行业或组织可以不定义清楚一个流行词或项目,却能持续推广它。组织会周期性地陷入这种行为中,允许术语在没有明确定义的情况下循环流传,并且群体行为使得模糊性得以存在。常见的例子包括区块链、人工智能、数据科学、大数据、比特币、物联网、量子计算、NFT、以“数据驱动”为核心的技术、云计算和“数字颠覆”。即使是具体、备受关注且特定的项目,也可能变成只有少数人理解却被广泛讨论的神秘流行词。
要阻止 Jabberwocky 效应,你必须成为促进有效对话的催化剂。对项目或倡议的方法和手段感到好奇(而不仅仅是品质或结果)。在角色选择时,公司是因为担心错过机会(FOMO)而雇佣你去做“Jabberwocky”,还是因为他们实际上有特定和实用的需求?做出这种判断可以决定你是否与公司匹配良好,顺利前行,还是在职业生涯中遇到沉闷的障碍。
在这种背景下,让我们现在来考虑一下在数据科学岗位中需要注意的几个事项,从角色定义开始。
角色定义
假设你被聘为数据科学家。面试进行得很顺利。你问了一些关于角色的问题,得到了直接的回答。你被提供了一份工作,而最重要的是,你应该知道自己将要从事哪些项目。
你总是希望进入一个角色清晰定义、目标明确的岗位。不应该猜测自己该做什么。更好的情况是,你应该有一个明确愿景的领导层,理解业务需求。你成为明确定义目标的执行者,并了解你的客户。
相反,如果你被聘用是因为部门想要“数据驱动”或在“数据科学”中拥有竞争优势,这是一个警告信号。很有可能你将被负担着寻找问题并销售任何低成本的解决方案。当你寻求战略指导时,你会被告知将“机器学习”应用于业务。但当然,当你只有一把锤子时,一切都开始看起来像钉子。数据科学团队感到压力要在甚至没有目标或问题的情况下提供解决方案(例如机器学习)。一旦发现问题,获得利益相关者的支持和资源对齐就变得困难,重点开始从一个低成本的解决方案转移到另一个低成本的解决方案。
问题在于你是基于流行词被聘用的,而不是基于功能。不良的角色定义往往会传播到下面讨论的其他问题。让我们转向组织的关注点。
组织的专注和支持
另一个需要注意的因素是组织在特定目标上的对齐程度以及所有各方是否全力支持。
自从数据科学的兴起以来,许多组织进行了重组,成立了一个中央数据科学团队。高管的愿景是让数据科学团队流动,提供建议,并帮助其他部门实现数据驱动,并采用机器学习等创新技术。他们还可能被要求消除部门之间的数据孤岛。虽然这在理论上听起来是个好主意,但许多组织发现这充满挑战。
原因在于:管理层成立了一个数据科学团队,但缺乏明确的目标。因此,这个团队的任务是寻找需要解决的问题,而不是有权力解决已知问题。正如所述,这就是为什么数据科学团队以先有解决方案(例如机器学习),而后有目标而闻名。他们尤其不适合成为消除数据孤岛的推动力,因为这完全超出了他们的专业领域。
打破数据孤岛是 IT 的工作!
在组织中使用数据科学团队“打破数据孤岛”是错误的。数据孤岛往往是由于缺乏数据仓库基础设施,各部门将其数据存储在电子表格和秘密数据库中,而不是集中和支持的数据库中。
如果数据孤岛被视为问题,你需要服务器、云实例、认证的数据库管理员、安全协议以及 IT 工作组来把所有这些整合起来。数据科学团队通常没有必要的专业知识、预算和组织权力来完成这项任务,除非是在非常小的公司里。
一旦发现问题,获得利益相关者的支持和资源对齐是困难的。如果发现机会,需要强大的领导力来做以下几点:
明确定义目标和路线图
获得预算来收集数据并支持基础设施
获得数据访问权限并协商数据所有权
包括利益相关者的支持和领域知识
从利益相关者那里预算时间和会议
在数据科学团队被聘用之后,这些要求要比之前更难实现,因为数据科学团队的角色是在对现有情况做出反应的基础上确定和预算的。如果高层领导没有对所有必要方方面面的资源和支持做好对齐,数据科学项目就不会成功。这就是为什么有无数的文章指责组织没有准备好数据科学,从哈佛商业评论 到MIT 斯隆评论 。
最好是在组织上与其客户处于同一部门的数据科学团队进行工作。信息、预算和沟通更自由和紧密地共享。这样,减少了跨部门政治的紧张局势,因为将所有人置于同一团队而非政治竞争之中。
数据访问是具有政治性的
组织对其数据保持保护并非秘密,但这并不仅仅是出于安全或不信任的考虑。数据本身是一种极具政治性的资产,许多人甚至不愿意向自己的同事提供访问权限。同一组织内部的部门出于这个原因也不愿意共享数据:他们不希望其他人来做他们的工作,更不希望他们做错。解释数据可能需要他们 全职专业知识,也需要他们 的领域知识。毕竟,他们的数据就是他们的业务!如果你请求访问他们 的数据,那就是在要求介入他们 的业务。
此外,数据科学家可能高估了解释外部数据集所需的能力和领域专业知识。要克服这一障碍,你必须与每个具有专业知识的合作伙伴建立信任和支持,协商知识转移,如果需要,让他们在项目中扮演重要角色。
足够的资源
另一个需要警惕的风险是没有得到足够的资源来完成工作。被投入到一个角色中而没有必要的支持是很困难的。当然,适应性和善于利用资源是一种宝贵的特质。但即使是最能打的软件工程师/数据科学家明星也很快会发现自己超出了自己的能力范围。有时候你需要花钱来购买必需的东西,而你的雇主却没有预算。
假设你需要一个数据库来进行预测工作。你与第三方数据库的连接很差,经常出现宕机和断开连接的情况。你最不想听到的就是“让它工作”,但这就是你的处境。你考虑在本地复制数据库,但为此你需要每天存储 40 GB 的数据,因此需要一个服务器或云实例。现在,你显然已经超出了自己的能力范围,一个数据科学家成为一个没有 IT 预算的 IT 部门!
在这些情况下,你必须考虑如何在不损害项目的情况下省去一些步骤。你能否仅保留最新的滚动数据并删除其余数据?你能否创建一些错误处理的 Python 脚本,在断开连接时重新连接,并将数据分批处理,以便从上一批次的最后成功点重新开始?
如果这个问题和解决方案听起来很具体,是的,我确实遇到过这种情况,而且这确实有效!能够提出解决方案并简化流程,而不增加更多成本,是令人满意的事情。但不可避免地,对于许多数据项目,你可能需要数据管道、服务器、集群、基于 GPU 的工作站以及其他桌面电脑无法提供的计算资源。换句话说,这些东西是需要成本支持的,而你的组织可能无法为它们预算。
数学建模在哪里?
如果你想知道为什么你被聘请来做回归、统计、机器学习和其他应用数学,结果却发现自己在做独行的 IT 工作,那在当前的企业氛围中并不罕见。
然而,你正在处理数据,这本质上可能会导致类似 IT 的工作。重要的是确保你的技能仍然与工作和所需的结果相匹配。我们将在本章的其余部分讨论这一点。
合理的目标
这是一个需要特别注意的问题。在充满炒作和远大承诺的环境中,很容易遇到不切实际的目标。
有些情况下,经理聘请数据科学家,并期望他们毫不费力地为组织增加指数级的价值。如果组织仍在进行手工操作,并且到处都有自动化的机会,这当然是可能的。例如,如果组织所有工作都在电子表格中进行,并且预测纯粹是猜测,那么对于数据科学专业人员来说,将流程优化为数据库,并使用简单的回归模型取得进展,这是一个很好的机会。
另一方面,如果组织聘请数据科学家将机器学习应用到他们的软件中,特别是识别图像中的对象,那就更加困难了。一个了解情况的数据科学家必须向管理层解释,这是一个至少会花费数十万美元的努力!不仅需要收集图片,还必须雇佣人工来标记图像中的对象 。而这仅仅是收集数据的第一步!
数据科学家通常要花费他们最初的 18 个月向管理层解释为什么他们尚未交付成果,因为他们仍在努力收集和清洗数据,这占据了机器学习工作的 95%。管理层可能对此感到幻灭,因为他们陷入了一个普遍的叙事中,即机器学习和人工智能将消除手工流程,但事实上他们发现自己只是将一套手工流程换成了另一套:获得标记数据。
因此要警惕那些設置不合理目標的環境,尤其是當其他人向管理層承諾了“EASY 按鈕”的時候,要找到外交的方式來管理期望。來自其他值得信賴的商業期刊和高額管理咨詢公司的聲稱認為超智能 AI 即將來臨。缺乏技術專業知識的經理人可能會成為這種炒作敘述的受害者。
惠誰?
拉丁語表達“cui bono”意思是“誰受益?”當您試圖理解 Jabberwocky 效應時,這是一個很好的問題。當媒體宣傳有關人工智能的故事時,誰從中受益?無論您的答案是什麼,媒體也會從點擊和廣告收入中受益。高額管理咨詢公司圍繞“AI 戰略”創建更多計費時間。晶片製造商可以推廣深度學習以銷售更多顯卡,而雲平台可以為機器學習項目銷售更多數據存儲和 CPU 時間。
所有這些方面有什麼共同點?不僅是他們將 AI 作為銷售產品的手段,而且他們對客戶的長期成功沒有利益。他們在賣的是單位,而不是項目結果,就像在淘金熱時賣鏟子一樣。
不過,我不是在說這些媒體和供應商的動機是不道德的。他們的員工的工作是為公司賺錢並養活家庭。宣傳其產品的主張甚至可能是合法和可實現的。但是,不能否認一旦宣傳了一個主張,要撤回它是很困難的,即使它被認為是無法實現的。許多企業可能會轉向並重新指定他們的努力,而不是承認他們的主張沒有實現。因此,只需注意這種動態,並始終問“cui bono?”
與現有系統競爭
這種警告可能屬於“合理目標”的一部分,但我認為這種情況非常普遍,足以獨立成類別。一種微妙但存在問題的角色類型是與實際上沒有問題的現有系統競爭。這些情況可能出現在工作環境中,缺乏工作且需要忙碌起來的地方。
幾年前,您的雇主與供應商簽訂合同安裝了一個銷售預測系統。現在您的經理要求您通過增加 1%的準確性來改進預測系統。
您看到這裡的統計問題了嗎?如果您讀過第三章,1%不應該在統計上感到顯著,隨機性可以輕易給您帶來這 1%,而您本人無需任何努力。相反,隨機性可能會朝相反的方向擺動,市場力量超出您的控制範圍,可以抵消您實施的任何努力。一個糟糕的銷售季度和公司市場中的競爭對手等因素可能使收入減少-3%,而不是您無可避免的 p-hack 的 1%。
这里的主要问题除了工作的冗余外,还有结果不在你的影响范围内。这可能会变得不太理想。如果你要与一个没有问题、没有自动化的现有系统竞争,那么你将会面临困难时期。如果可能的话,当你遇到这种项目时,最好逃之夭夭。
一个角色并不是你期望的那样
当你开始一个角色,并发现它并不是你期望的那样时,你会怎么做?例如,你被告知你的角色将是统计和机器学习,但实际上你发现自己在做类似 IT 工作,因为组织的数据基础根本不足以进行机器学习。
你也许能够制作柠檬水。当然,你可以接受你的数据科学家角色转变为 IT 角色,并且在此过程中获得一些数据库和编程技能。你甚至可能成为驻场 SQL 专家或技术大师,这样职业上会更加舒适。当你简化企业的数据操作和工作流程时,你正在为未来更复杂的应用做好准备。当你的运营顺利进行时,你可以分配时间学习和在你感兴趣的领域中职业生涯成长。
另一方面,如果你期望做统计分析和机器学习,但最终发现自己在调试损坏的电子表格、Microsoft Access 和 VBA 宏,那么你可能会感到失望。在这种情况下,至少要成为变革的倡导者。推动现代化工具的使用,主张使用 Python 和现代数据库平台,如 MySQL 甚至 SQLite。如果你能做到这一点,那么至少你将站在一个允许更多创新的平台上,并且离应用本书中的概念更近一步。这也将有益于组织,因为支持和工具的灵活性将会增加,而 Python 人才比 Microsoft Access 和 VBA 等过时技术更容易找到。
你梦想的工作不存在吗?
虽然你总是可以放弃一个角色,但一定要评估你的期望是否真实可行。你正在追求的尖端技术也许太尖端了吗?
以自然语言处理为例。例如,你想使用深度学习构建聊天机器人。然而,实际上做这种工作的公司并不多,因为大多数公司并不真正需要聊天机器人。为什么呢?因为聊天机器人目前还不够成熟。虽然像OpenAI 进行了有趣的研究,如 GPT-3 ,但大部分仍然只是有趣的研究。最终,GPT-3 只是基于概率的模式识别器,将单词链接在一起,因此它没有常识。有研究证明了这一点,包括纽约大学的 Gary Marcus 的一些研究 。
这意味着,创建具有更广泛应用的聊天机器人仍然是一个未解决的问题,并且对大多数企业来说还没有形成价值主张。如果自然语言处理是你真正想追求的领域,而且你发现与职业机会存在脱节,你最好的选择可能是进入学术界进行研究。虽然像 Alphabet 这样的公司进行类似学术研究,但许多员工都是从学术界过来的。
所以,在你探索职场时,请保持对期望的现实态度。如果你的期望超出了职场能提供的范围,强烈考虑学术路线。当你所追求的工作类型经常要求博士学位或特定的学术证书,并且这成为你实现梦想工作的障碍时,你也应该考虑这条路线。
Where Do I Go Now?
现在我们已经覆盖了数据科学的格局,接下来我们该去哪里?数据科学的未来又是什么?
首先,考虑担任数据科学家职称的负担。这意味着隐含要求具备知识的无边界性,主要是由于定义缺乏标准化的限制性。如果我们从过去 10 年观察数据科学运动中学到了任何东西,那就是定义很重要。数据科学家正在演变成为精通统计学、优化和机器学习的软件工程师。甚至可能不再拥有“数据科学家”这个职称。尽管这比“21 世纪最性感职位”宣布时的要求更为广泛,但具备这些技能正在变得必要。
另一种选择是专注于更专业的职称,过去几年这种情况越来越多见。像计算机视觉工程师、数据工程师、数据分析师、研究员、运筹分析师和顾问/咨询师这样的角色正在回归。我们看到数据科学家角色的数量减少,这一趋势可能在未来 10 年继续,主要是由于角色专业化。遵循这种趋势当然是一个选择。
需要注意的是,劳动市场已经发生了巨大变化,这就是为什么你需要本章列出的竞争优势。尽管在 2014 年,数据科学家被视为独角兽,拥有六位数的薪水,但如今任何公司的数据科学家职位很容易收到数百甚至数千份申请,而薪水可能只有五位数。数据科学学位和短期培训班已经创造了大量的数据科学专业人才供给。因此,争取市场标榜为数据科学家或数据科学家总体职位的工作非常具有竞争性。这就是为什么追求分析师、运筹研究和软件开发者等角色并不一定是一个坏主意!Vicki Boykis,Tumblr 的机器学习工程师,在她的博客文章“数据科学现在不同了” 中或许表达得最好:
记住最终目标……是要超过那些正在攻读数据科学学位、参加训练营和通过教程学习的人群。
你想要踏入门槛,获得与数据相关的职位 ,并朝着你梦寐以求的工作迈进,同时尽可能多地了解科技行业的情况。
不要陷入分析的瘫痪。选择一小部分内容并从那里开始。做一些小事。学到一些小东西,建立一些小东西。告诉其他人。记住,你在数据科学方面的第一份工作可能不会是数据科学家。
结论
这是本书中与其他章节不同的一章,但如果你想要在数据科学职场中航行并有效地应用本书中的知识,它是很重要的。当你发现大部分工作将把你引向其他工作时,可能会感到困惑,因为你已经学习了统计工具和机器学习。当这种情况发生时,抓住机会继续学习和提升技能。当你将基本的数学知识与编程和软件工程的熟练程度结合起来时,你的价值将因为理解 IT 和数据科学之间的差距而增加数十倍。
记住要摒弃炒作,倾向于实际解决方案,不要陷入技术视野而被市场力量所蒙蔽。了解管理和领导动机,以及人们普遍的动机。理解事物的为什么 而不仅仅是如何 。要好奇一个技术或工具是如何解决问题的为什么 ,而不仅仅是它的技术方面是如何 操作的。
学习不是为了学习本身,而是为了发展能力,并将正确的工具与正确的问题匹配起来。学习的最有效方法之一是选择一个你觉得有趣的问题(而不是工具!)。解决这个问题会引发对其他事物的好奇心,然后又是另一个,再接着是另一个。你心中有一个目标,所以继续朝着正确的兔子洞深入,并知道何时从其他兔子洞中抽身而出。采取这种方法绝对是有益的,你会惊讶于在短时间内能获得多少专业知识。
附录 A. 补充主题
使用 SymPy 进行 LaTeX 渲染
当你对数学符号更加熟悉时,将你的 SymPy 表达式显示为数学符号可能会很有帮助。
最快的方法是在 SymPy 中使用 latex()
函数对你的表达式,然后复制结果到 LaTeX 数学查看器中。
示例 A-1 是一个将简单表达式转换为 LaTeX 字符串的示例。当然,我们也可以对导数、积分和其他 SymPy 操作的结果进行渲染成 LaTeX。但让我们保持示例简单。
示例 A-1. 使用 SymPy 将表达式转换为 LaTeX
from sympy import *
x,y = symbols('x y' )
z = x**2 / sqrt(2 *y**3 - 1 )
print (latex(z))
这个 \frac{x^{2}}{\sqrt{2 y^{3} - 1}}
字符串是格式化的 mathlatex,有多种工具和文档格式可以适应它。但为了简单地渲染 mathlatex,可以去 LaTeX 方程编辑器。这里有两个我在线使用的不同的:
在 图 A-1 中,我使用 Lagrida 的 LaTeX 编辑器来渲染数学表达式。
图 A-1. 使用数学编辑器查看 SymPy LaTeX 输出
如果你想要省略复制/粘贴步骤,你可以将 LaTeX 直接附加为 CodeCogs LaTeX 编辑器 URL 的参数,就像示例 A-2 中展示的那样,它将在你的浏览器中显示渲染后的数学方程。
示例 A-2. 使用 CodeCogs 打开一个 mathlatex 渲染。
import webbrowser
from sympy import *
x,y = symbols('x y' )
z = x**2 / sqrt(2 *y**3 - 1 )
webbrowser.open ("https://latex.codecogs.com/png.image?\dpi{200}" + latex(z))
如果你使用 Jupyter,你也可以使用插件来渲染 mathlatex 。
从头开始的二项分布
如果你想从头实现一个二项分布,这里是所有你需要的部分在 示例 A-3 中。
示例 A-3. 从头构建一个二项分布
def factorial (n: int ):
f = 1
for i in range (n):
f *= (i + 1 )
return f
def binomial_coefficient (n: int , k: int ):
return factorial(n) / (factorial(k) * factorial(n - k))
def binomial_distribution (k: int , n: int , p: float ):
return binomial_coefficient(n, k) * (p ** k) * (1.0 - p) ** (n - k)
n = 10
p = 0.9
for k in range (n + 1 ):
probability = binomial_distribution(k, n, p)
print ("{0} - {1}" .format (k, probability))
使用 factorial()
和 binomial_coefficient()
,我们可以从头构建一个二项分布函数。阶乘函数将一系列整数从 1 到n
相乘。例如,5! 的阶乘将是 1 2 3 4 5 = 120 。
二项式系数函数允许我们从 n 个可能性中选择 k 个结果,而不考虑顺序。如果你有 k = 2 和 n = 3,那将产生集合 (1,2) 和 (1,2,3)。在这两组之间,可能的不同组合将是 (1,3),(1,2) 和 (2,3)。因此,这将是一个二项式系数为 3。当然,使用 binomial_coefficient()
函数,我们可以避免所有这些排列工作,而是使用阶乘和乘法来实现。
在实现binomial_distribution()
时,请注意我们如何取二项式系数,并将其乘以成功概率p
发生k
次(因此指数)。然后我们乘以相反情况:失败概率1.0 - p
在n - k
次中发生。这使我们能够跨多次试验考虑事件发生与否的概率p
。
从头开始构建贝塔分布
如果你想知道如何从头开始构建贝塔分布,你将需要重新使用我们用于二项分布的factorial()
函数,以及我们在第二章中构建的approximate_integral()
函数。
就像我们在第一章中所做的那样,我们在感兴趣的范围内根据曲线包装矩形,如图 A-2 所示。
图 A-2. 包装矩形以找到面积/概率
这只是使用六个矩形;如果我们使用更多矩形,将会获得更高的准确性。让我们从头实现beta_distribution()
并在 0.9 到 1.0 之间使用 1,000 个矩形进行积分,如示例 A-4 所示。
示例 A-4. 从头开始的贝塔分布
def factorial (n: int ):
f = 1
for i in range (n):
f *= (i + 1 )
return f
def approximate_integral (a, b, n, f ):
delta_x = (b - a) / n
total_sum = 0
for i in range (1 , n + 1 ):
midpoint = 0.5 * (2 * a + delta_x * (2 * i - 1 ))
total_sum += f(midpoint)
return total_sum * delta_x
def beta_distribution (x: float , alpha: float , beta: float ) -> float :
if x < 0.0 or x > 1.0 :
raise ValueError("x must be between 0.0 and 1.0" )
numerator = x ** (alpha - 1.0 ) * (1.0 - x) ** (beta - 1.0 )
denominator = (1.0 * factorial(alpha - 1 ) * factorial(beta - 1 )) / \
(1.0 * factorial(alpha + beta - 1 ))
return numerator / denominator
greater_than_90 = approximate_integral(a=.90 , b=1.0 , n=1000 ,
f=lambda x: beta_distribution(x, 8 , 2 ))
less_than_90 = 1.0 - greater_than_90
print ("GREATER THAN 90%: {}, LESS THAN 90%: {}" .format (greater_than_90,
less_than_90))
请注意,使用beta_distribution()
函数时,我们提供一个给定的概率x
,一个衡量成功的alpha
值,以及一个衡量失败的beta
值。该函数将返回观察到给定概率x
的可能性。但是,要获得观察到概率x
的概率,我们需要在x
值范围内找到一个区域。
幸运的是,我们已经定义并准备好使用来自第二章的approximate_integral()
函数。我们可以计算成功率大于 90%和小于 90%的概率,如最后几行所示。
推导贝叶斯定理
如果你想理解为什么贝叶斯定理有效而不是听信我的话,让我们进行一个思想实验。假设我有一个 10 万人口的人群。将其与我们给定的概率相乘,以得到喝咖啡的人数和患癌症的人数:
N = 100,000 P ( 喝咖啡的人 ) = .65 P ( 癌症 ) = .005 喝咖啡的人数 = 65,000 癌症患者数 = 500
我们有 65,000 名咖啡饮用者和 500 名癌症患者。现在,这 500 名癌症患者中,有多少是咖啡饮用者?我们提供了条件概率P ( 癌症|咖啡 ) ,我们可以将其乘以这 500 人,得到应有 425 名喝咖啡的癌症患者:
P ( 喝咖啡的人|癌症 ) = .85 喝咖啡的癌症患者数 = 500 × .85 = 425
现在,喝咖啡的人中得癌症的百分比是多少?我们需要除以哪两个数字?我们已经知道喝咖啡的人数 和 得癌症的人数。因此,我们将这个比例与总的喝咖啡的人数进行比较:
P ( 癌症|喝咖啡的人 ) = 喝咖啡的癌症患者数 喝咖啡的人数 P ( 癌症|喝咖啡的人 ) = 425 65,000 P ( 癌症|喝咖啡的人 ) = 0.006538
稍等片刻,我们是不是刚刚颠倒了条件概率?是的!我们从P ( Coffee Drinker|Cancer ) 开始,最终得到P ( Cancer|Coffee Drinker ) 。通过取两个子集(65,000 名咖啡饮用者和 500 名癌症患者),然后应用我们拥有的条件概率来应用联合概率,我们最终在我们的人口中得到了既饮咖啡又患癌症的 425 人。然后我们将这个数字除以咖啡饮用者的数量,得到患癌症的概率,假如一个人是咖啡饮用者。
但是贝叶斯定理在哪里呢?让我们专注于P ( Cancer|Coffee Drinker ) 表达式,并用我们先前计算的所有表达式扩展它:
P ( Cancer|Coffee Drinker ) = 100,000 × P ( Cancer ) × P ( Coffee Drinker|Cancer ) 100,000 × P ( Coffee Drinker )
注意人口N 为 10 万,在分子和分母中都存在,所以可以取消。这看起来现在熟悉吗?
P ( Cancer|Coffee Drinker ) = P ( Cancer ) × P ( Coffee Drinker|Cancer ) P ( Coffee Drinker )
毫无疑问,这应该与贝叶斯定理相符!
P ( A|B ) = P ( B|A ) * P ( B ) P ( A ) P ( Cancer|Coffee Drinker ) = P ( Cancer ) × P ( Coffee Drinker|Cancer ) P ( Coffee Drinker )
如果你对贝叶斯定理感到困惑或者在其背后的直觉上挣扎,尝试基于提供的概率数据获取固定人口的子集。然后你可以追踪你的方式来颠倒一个条件概率。
从头开始的 CDF 和逆 CDF
要计算正态分布的面积,我们当然可以使用我们在第一章中学到的矩形填充法,之前在附录中应用于 beta 分布。它不需要累积密度函数(CDF),而只需在概率密度函数(PDF)下填充矩形。使用这种方法,我们可以找到金毛寻回犬体重在 61 至 62 磅之间的概率,如示例 A-5 所示,使用 1,000 个填充的矩形对正态 PDF 进行估算。
示例 A-5. Python 中的正态分布函数
import math
def normal_pdf (x: float , mean: float , std_dev: float ) -> float :
return (1.0 / (2.0 * math.pi * std_dev ** 2 ) ** 0.5 ) *
math.exp(-1.0 * ((x - mean) ** 2 / (2.0 * std_dev ** 2 )))
def approximate_integral (a, b, n, f ):
delta_x = (b - a) / n
total_sum = 0
for i in range (1 , n + 1 ):
midpoint = 0.5 * (2 * a + delta_x * (2 * i - 1 ))
total_sum += f(midpoint)
return total_sum * delta_x
p_between_61_and_62 = approximate_integral(a=61 , b=62 , n=7 ,
f= lambda x: normal_pdf(x,64.43 ,2.99 ))
print (p_between_61_and_62)
这将为我们提供大约 8.25%的概率,即金毛犬重量在 61 至 62 磅之间。如果我们想利用已经为我们集成且不需要任何矩形包装的 CDF,我们可以像在示例 A-6 中所示从头开始声明它。
示例 A-6. 在 Python 中使用称为ppf()
的逆 CDF
import math
def normal_cdf (x: float , mean: float , std_dev: float ) -> float :
return (1 + math.erf((x - mean) / math.sqrt(2 ) / std_dev)) / 2
mean = 64.43
std_dev = 2.99
x = normal_cdf(66 , mean, std_dev) - normal_cdf(62 , mean, std_dev)
print (x)
math.erf()
被称为误差函数,通常用于计算累积分布。最后,要从头开始做逆 CDF,您需要使用名为erfinv()
的erf()
函数的逆函数。示例 A-7 使用从头编码的逆 CDF 计算了一千个随机生成的金毛犬重量。
示例 A-7. 生成随机金毛犬重量
import random
from scipy.special import erfinv
def inv_normal_cdf (p: float , mean: float , std_dev: float ):
return mean + (std_dev * (2.0 ** 0.5 ) * erfinv((2.0 * p) - 1.0 ))
mean = 64.43
std_dev = 2.99
for i in range (0 ,1000 ):
random_p = random.uniform(0.0 , 1.0 )
print (inv_normal_cdf(random_p, mean, std_dev))
使用 e 预测随时间发生的事件概率
让我们看看您可能会发现有用的e 的另一个用例。假设您是丙烷罐的制造商。显然,您不希望罐子泄漏,否则可能会在开放火焰和火花周围造成危险。测试新罐设计时,您的工程师报告说,在给定的一年中,有 5%的机会它会泄漏。
你知道这已经是一个不可接受的高数字,但你想知道这种概率如何随时间复合。现在你问自己,“在 2 年内发生泄漏的概率是多少?5 年?10 年?”随着时间的推移,暴露的时间越长,看到罐子泄漏的概率不是越来越高吗?欧拉数再次派上用场!
P l e a k = 1.0 - e - λ T
此函数建模随时间的事件发生概率,或者在本例中是罐子在T 时间后泄漏的概率。 e 再次是欧拉数,lambda λ 是每个单位时间(每年)的失效率,T 是经过的时间量(年数)。
如果我们绘制此函数,其中T 是我们的 x 轴,泄漏的概率是我们的 y 轴,λ = .05,图 A-3 显示了我们得到的结果。
图 A-3. 预测随时间泄漏概率
这是我们在 Python 中为λ = . 05 和T = 5 年建模此函数的方式,参见示例 A-8。
示例 A-8. 用于预测随时间泄漏概率的代码
from math import exp
p_leak = .05
t = 5
p_leak_5_years = 1.0 - exp(-p_leak * t)
print ("PROBABILITY OF LEAK WITHIN 5 YEARS: {}" .format (p_leak_5_years))
2 年后罐子失效的概率约为 9.5%,5 年约为 22.1%,10 年约为 39.3%。随着时间的推移,罐子泄漏的可能性越来越大。我们可以将此公式推广为预测任何在给定期间内具有概率的事件,并查看该概率在不同时间段内的变化。
爬坡和线性回归
如果您发现从头开始构建机器学习中的微积分令人不知所措,您可以尝试一种更加蛮力的方法。让我们尝试一种 爬山 算法,其中我们通过添加一些随机值来随机调整 m 和 b ,进行一定次数的迭代。这些随机值将是正或负的(这将使加法操作有效地成为减法),我们只保留能够改善平方和的调整。
但我们随机生成任何数字作为调整吗?我们更倾向于较小的移动,但偶尔也许会允许较大的移动。这样,我们主要是微小的调整,但偶尔如果需要的话会进行大幅跳跃。最好的工具来做到这一点是标准正态分布,均值为 0,标准差为 1。回想一下第三章 中提到的标准正态分布在 0 附近有大量的值,而远离 0 的值(无论是负向还是正向)的概率较低,如 图 A-4 所示。
图 A-4. 标准正态分布中大多数值都很小且接近于 0,而较大的值在尾部出现的频率较低
回到线性回归,我们将从 0 或其他起始值开始设置 m
和 b
。然后在一个 for
循环中进行 150,000 次迭代,我们将随机调整 m
和 b
,通过添加从标准正态分布中采样的值。如果随机调整改善/减少了平方和,我们将保留它。但如果平方和增加了,我们将撤消该随机调整。换句话说,我们只保留能够改善平方和的调整。让我们在 示例 A-9 中看一看。
示例 A-9. 使用爬山算法进行线性回归
from numpy.random import normal
import pandas as pd
points = [p for p in pd.read_csv("https://bit.ly/2KF29Bd" ).itertuples()]
m = 0.0
b = 0.0
iterations = 150000
n = float (len (points))
best_loss = 10000000000000.0
for i in range (iterations):
m_adjust = normal(0 ,1 )
b_adjust = normal(0 ,1 )
m += m_adjust
b += b_adjust
new_loss = 0.0
for p in points:
new_loss += (p.y - (m * p.x + b)) ** 2
if new_loss < best_loss:
print ("y = {0}x + {1}" .format (m, b))
best_loss = new_loss
else :
m -= m_adjust
b -= b_adjust
print ("y = {0}x + {1}" .format (m, b))
您将看到算法的进展,但最终您应该得到一个大约为 y = 1.9395722046562853x + 4.731834051245578
的拟合函数。让我们验证这个答案。当我使用 Excel 或 Desmos 进行线性回归时,Desmos 给出了 y = 1.93939x + 4.73333
。还不错!我几乎接近了!
为什么我们需要一百万次迭代?通过实验,我发现这足够多的迭代次数使得解决方案不再显著改善,并且收敛到了接近于最优的 m
和 b
值以最小化平方和。您将发现许多机器学习库和算法都有一个迭代次数的参数,它确切地做到这一点。您需要足够多的迭代次数使其大致收敛到正确的答案,但不要太多以至于在已经找到可接受的解决方案时浪费计算时间。
你可能会问为什么我将 best_loss
设置为一个极大的数字。我这样做是为了用我知道会被覆盖的值初始化最佳损失,并且它将与每次迭代的新损失进行比较,以查看是否有改进。我也可以使用正无穷 float('inf')
而不是一个非常大的数字。
爬山算法与逻辑回归
就像之前的线性回归示例一样,我们也可以将爬山算法应用于逻辑回归。如果你觉得微积分和偏导数一次性学习太过于复杂,可以再次使用这一技术。
爬山方法完全相同:用正态分布的随机值调整 m
和 b
。然而,我们确实有一个不同的目标函数,即最大似然估计,在 第六章 中讨论过。因此,我们只采用增加似然估计的随机调整,并在足够的迭代之后应该会收敛到一个拟合的逻辑回归模型。
所有这些都在 示例 A-10 中演示。
示例 A-10. 使用爬山算法进行简单的逻辑回归
import math
import random
import numpy as np
import pandas as pd
points = [p for p in pd.read_csv("https://tinyurl.com/y2cocoo7" ).itertuples()]
best_likelihood = -10_000_000
b0 = .01
b1 = .01
def predict_probability (x ):
p = 1.0 / (1.0001 + math.exp(-(b0 + b1 * x)))
return p
for i in range (1_000_000 ):
random_b = random.choice(range (2 ))
random_adjust = np.random.normal()
if random_b == 0 :
b0 += random_adjust
elif random_b == 1 :
b1 += random_adjust
true_estimates = sum (math.log(predict_probability(p.x)) \
for p in points if p.y == 1.0 )
false_estimates = sum (math.log(1.0 - predict_probability(p.x)) \
for p in points if p.y == 0.0 )
total_likelihood = true_estimates + false_estimates
if best_likelihood < total_likelihood:
best_likelihood = total_likelihood
elif random_b == 0 :
b0 -= random_adjust
elif random_b == 1 :
b1 -= random_adjust
print ("1.0 / (1 + exp(-({0} + {1}*x))" .format (b0, b1))
print ("BEST LIKELIHOOD: {0}" .format (math.exp(best_likelihood)))
参见 第六章 获取更多关于最大似然估计、逻辑函数以及我们为什么使用 log()
函数的详细信息。
线性规划简介
每个数据科学专业人士都应熟悉的技术是线性规划 ,它通过使用“松弛变量”来适应方程组来解决不等式系统。当你在线性规划系统中有离散整数或二进制变量(0 或 1)时,它被称为整数规划 。当使用线性连续和整数变量时,它被称为混合整数规划 。
尽管比数据驱动更多地依赖算法,线性规划及其变体可用于解决广泛的经典人工智能问题。如果将线性规划系统称为人工智能听起来有些靠不住,但许多供应商和公司都将其视为一种常见做法,因为这会增加其感知价值。
在实践中,最好使用众多现有的求解器库来为你执行线性规划,但是在本节末尾将提供如何从头开始执行的资源。在这些示例中,我们将使用 PuLP ,虽然 Pyomo 也是一个选择。我们还将使用图形直觉,尽管超过三个维度的问题不能轻易地进行可视化。
这是我们的例子。你有两条产品线:iPac 和 iPac Ultra。iPac 每件产品盈利 200 , 而 i P a c U l t r a 每 件 产 品 盈 利 200 , 而 i P a c U l t r a 每 件 产 品 盈 利 300。
然而,装配线只能工作 20 小时,而制造 iPac 需要 1 小时,制造 iPac Ultra 需要 3 小时。
一天只能提供 45 套装备,而 iPac 需要 6 套,而 iPac Ultra 需要 2 套。
假设所有供应都将被销售,我们应该销售多少个 iPac 和 iPac Ultra 以实现利润最大化?
让我们首先注意第一个约束条件并将其拆分:
…装配线只能工作 20 小时,生产一个 iPac 需要 1 小时,生产一个 iPac Ultra 需要 3 小时。
我们可以将其表示为一个不等式,其中x 是 iPac 单元的数量,y 是 iPac Ultra 单元的数量。两者必须为正,并且图 A-5 显示我们可以相应地绘制图形。
x + 3 y ≤ 20 ( x ≥ 0 , y ≥ 0 )
图 A-5. 绘制第一个约束条件
现在让我们看看第二个约束条件:
一天只能提供 45 个套件,其中 iPac 需要 6 个套件,而 iPac Ultra 需要 2 个套件。
我们还可以根据图 A-6 进行建模和绘图。
6 x + 2 y ≤ 45 ( x ≥ 0 , y ≥ 0 )
图 A-6. 绘制第二个约束条件
注意在图 A-6 中,现在这两个约束条件之间存在重叠。我们的解决方案位于该重叠区域内,我们称之为可行区域 。最后,我们正在最大化我们的利润Z ,下面给出了 iPac 和 iPac Ultra 的利润金额。
Z = 200 x + 300 y
如果我们将这个函数表示为一条线,我们可以尽可能地增加Z ,直到这条线不再位于可行区域内。然后我们注意在图 A-7 中可视化的 x 和 y 值。
Objective Function 的 Desmos 图
如果您需要以更交互式和动画的方式看到这个图形,请查看Desmos 上的这张图 。
图 A-7. 将我们的目标线增加到不再位于可行区域内
当利润Z 尽可能地增加时,当那条线“刚好触及”可行区域时,您将落在可行区域的一个顶点或角上。该顶点提供了将最大化利润所需的 x 和 y 值,如图 A-8 所示。
尽管我们可以使用 NumPy 和一堆矩阵运算来进行数值求解,但使用 PuLP 会更容易,如示例 A-11 所示。请注意,LpVariable
定义了要解决的变量。LpProblem
是线性规划系统,使用 Python 操作符添加约束和目标函数。然后通过在LpProblem
上调用solve()
来求解变量。
A-8. 线性规划系统的最大化目标
A-11. 使用 Python PuLP 解决线性规划系统
from pulp import *
x = LpVariable("x" , 0 )
y = LpVariable("y" , 0 )
prob = LpProblem("factory_problem" , LpMaximize)
prob += x + 3 *y <= 20
prob += 6 *x +2 *y <= 45
prob += 200 *x + 300 *y
status = prob.solve()
print (LpStatus[status])
print (value(x))
print (value(y))
也许你会想知道是否有意义构建 5.9375 和 4.6875 单位。如果您的变量能容忍连续值,线性规划系统会更高效,也许您可以在之后对它们进行四舍五入处理。但某些类型的问题绝对需要处理整数和二进制变量。
要将x
和y
变量强制为整数,可以使用cat=LpInteger
参数,如示例 A-12 所示。
A-12. 强制变量作为整数解决
x = LpVariable("x" , 0 , cat=LpInteger)
y = LpVariable("y" , 0 , cat=LpInteger)
从图形上来看,这意味着我们用离散点填充我们的可行区域,而不是连续的区域。我们的解决方案不一定会落在一个顶点上,而是接近顶点的点,如图 A-9 所示。
线性规划中有一些特殊情况,如图 A-10 所示。有时可能有多个解决方案。有时可能根本没有解决方案。
这只是线性规划的一个快速介绍示例,不幸的是,这本书里没有足够的空间来充分讨论这个话题。它可以用于一些出人意料的问题,包括调度受限资源(如工人、服务器任务或房间)、解决数独和优化金融投资组合。
A-9. 离散线性规划系统
A-10. 线性规划的特殊情况
如果您想了解更多,有一些很好的 YouTube 视频,包括PatrickJMT 和Josh Emmanuel 。如果您想深入研究离散优化,Pascal Van Hentenryck 教授在 Coursera 上组织了一门极好的课程,请点击这里 。
使用 scikit-learn 进行 MNIST 分类器
示例 A-13 展示了如何使用 scikit-learn 的神经网络进行手写数字分类。
A-13. scikit-learn 中的手写数字分类器神经网络示例
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
df = pd.read_csv('https://bit.ly/3ilJc2C' , compression='zip' , delimiter="," )
X = (df.values[:, :-1 ] / 255.0 )
Y = df.values[:, -1 ]
print (df.groupby(["class" ]).agg({"class" : [np.size]}))
X_train, X_test, Y_train, Y_test = train_test_split(X, Y,
test_size=.33 , random_state=10 , stratify=Y)
nn = MLPClassifier(solver='sgd' ,
hidden_layer_sizes=(100 , ),
activation='logistic' ,
max_iter=480 ,
learning_rate_init=.1 )
nn.fit(X_train, Y_train)
print ("Training set score: %f" % nn.score(X_train, Y_train))
print ("Test set score: %f" % nn.score(X_test, Y_test))
import matplotlib.pyplot as plt
fig, axes = plt.subplots(4 , 4 )
vmin, vmax = nn.coefs_[0 ].min (), nn.coefs_[0 ].max ()
for coef, ax in zip (nn.coefs_[0 ].T, axes.ravel()):
ax.matshow(coef.reshape(28 , 28 ), cmap=plt.cm.gray, vmin=.5 * vmin, vmax=.5 * vmax)
ax.set_xticks(())
ax.set_yticks(())
plt.show()
附录 B. 练习答案
第一章
62.6738 是有理数,因为它的小数位数有限,可以表示为分数 626738 / 10000。
10 7 10 - 5 = 10 7 + - 5 = 10 2 = 100
81 1 2 = ( 81 ) = 9
25 3 2 = ( 25 1 / 2 ) 3 = 5 3 = 125
结果金额为$1,161.47。Python 脚本如下:
from math import exp
p = 1000
r = .05
t = 3
n = 12
a = p * (1 + (r/n))**(n * t)
print (a)
结果金额为$1161.83。Python 脚本如下:
from math import exp
p = 1000
r = .05
t = 3.0
a = p * exp(r*t)
print (a)
导数计算为 6x ,这使得x =3 时的斜率为 18。SymPy 代码如下:
from sympy import *
x = symbols('x' )
f = 3 *x**2 + 1
dx_f = diff(f)
print (dx_f)
print (dx_f.subs(x,3 ))
曲线在 0 到 2 之间的面积为 10。SymPy 代码如下:
from sympy import *
x = symbols('x' )
f = 3 *x**2 + 1
area = integrate(f, (x, 0 , 2 ))
print (area)
第二章
0.3 × 0.4 = 0.12;参见“条件概率和贝叶斯定理”。
(1 - 0.3) + 0.4 - (.03 × 0.4) = 0.98;参见“联合概率”,请记住我们在寻找没有雨 ,因此从 1.0 中减去该概率。
0.3 × 0.2 = 0.06;参见“条件概率和贝叶斯定理”。
以下 Python 代码计算出 0.822 的答案,将 50 名以上未出现乘客的概率相加:
from scipy.stats import binom
n = 137
p = .40
p_50_or_more_noshows = 0.0
for x in range (50 ,138 ):
p_50_or_more_noshows += binom.pmf(x, n, p)
print (p_50_or_more_noshows)
使用 SciPy 中显示的 Beta 分布,获取到 0.5 的面积并从 1.0 中减去。结果约为 0.98,因此这枚硬币极不可能是公平的。
from scipy.stats import beta
heads = 8
tails = 2
p = 1.0 - beta.cdf(.5 , heads, tails)
print (p)
第三章
平均值为 1.752,标准差约为 0.02135。Python 代码如下:
from math import sqrt
sample = [1.78 , 1.75 , 1.72 , 1.74 , 1.77 ]
def mean (values ):
return sum (values) /len (values)
def variance_sample (values ):
mean = sum (values) / len (values)
var = sum ((v - mean) ** 2 for v in values) / len (values)
return var
def std_dev_sample (values ):
return sqrt(variance_sample(values))
mean = mean(sample)
std_dev = std_dev_sample(sample)
print ("MEAN: " , mean)
print ("STD DEV: " , std_dev)
使用 CDF 获取 30 到 20 个月之间的值,大约是 0.06 的面积。Python 代码如下:
from scipy.stats import norm
mean = 42
std_dev = 8
x = norm.cdf(30 , mean, std_dev) - norm.cdf(20 , mean, std_dev)
print (x)
有 99%的概率,卷筒的平均丝径在 1.7026 到 1.7285 之间。Python 代码如下:
from math import sqrt
from scipy.stats import norm
def critical_z_value (p, mean=0.0 , std=1.0 ):
norm_dist = norm(loc=mean, scale=std)
left_area = (1.0 - p) / 2.0
right_area = 1.0 - ((1.0 - p) / 2.0 )
return norm_dist.ppf(left_area), norm_dist.ppf(right_area)
def ci_large_sample (p, sample_mean, sample_std, n ):
lower, upper = critical_z_value(p)
lower_ci = lower * (sample_std / sqrt(n))
upper_ci = upper * (sample_std / sqrt(n))
return sample_mean + lower_ci, sample_mean + upper_ci
print (ci_large_sample(p=.99 , sample_mean=1.715588 ,
sample_std=0.029252 , n=34 ))
营销活动的 p 值为 0.01888。Python 代码如下:
from scipy.stats import norm
mean = 10345
std_dev = 552
p1 = 1.0 - norm.cdf(11641 , mean, std_dev)
p2 = p1
p_value = p1 + p2
print ("Two-tailed P-value" , p_value)
if p_value <= .05 :
print ("Passes two-tailed test" )
else :
print ("Fails two-tailed test" )
第四章
向量落在[2, 3]。Python 代码如下:
from numpy import array
v = array([1 ,2 ])
i_hat = array([2 , 0 ])
j_hat = array([0 , 1.5 ])
basis = array([i_hat, j_hat])
w = basis.dot(v)
print (w)
向量落在[0, -3]。Python 代码如下:
from numpy import array
v = array([1 ,2 ])
i_hat = array([-2 , 1 ])
j_hat = array([1 , -2 ])
basis = array([i_hat, j_hat])
w = basis.dot(v)
print (w)
行列式为 2.0。Python 代码如下:
import numpy as np
from numpy.linalg import det
i_hat = np.array([1 , 0 ])
j_hat = np.array([2 , 2 ])
basis = np.array([i_hat,j_hat]).transpose()
determinant = det(basis)
print (determinant)
是的,因为矩阵乘法允许我们将多个矩阵合并为一个表示单一转换的矩阵。
x = 19.8,y = –5.4,z = –6。代码如下:
from numpy import array
from numpy.linalg import inv
A = array([
[3 , 1 , 0 ],
[2 , 4 , 1 ],
[3 , 1 , 8 ]
])
B = array([
54 ,
12 ,
6
])
X = inv(A).dot(B)
print (X)
是的,它是线性相关的。尽管在 NumPy 中存在一些浮点不精确性,行列式有效地为 0。
from numpy.linalg import det
from numpy import array
i_hat = array([2 , 6 ])
j_hat = array([1 , 3 ])
basis = array([i_hat, j_hat]).transpose()
print (basis)
determinant = det(basis)
print (determinant)
为了解决浮点问题,使用 SymPy,你将得到 0:
from sympy import *
basis = Matrix([
[2 ,1 ],
[6 ,3 ]
])
determinant = det(basis)
print (determinant)
第五章
有很多工具和方法可以执行线性回归,就像我们在第五章中学到的一样,但是这里使用 scikit-learn 进行解决。斜率是 1.75919315,截距是 4.69359655。
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
df = pd.read_csv('https://bit.ly/3C8JzrM' , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
fit = LinearRegression().fit(X, Y)
m = fit.coef_.flatten()
b = fit.intercept_.flatten()
print ("m = {0}" .format (m))
print ("b = {0}" .format (b))
plt.plot(X, Y, 'o' )
plt.plot(X, m*X+b)
plt.show()
我们得到高达 0.92421 的相关性和测试值 23.8355,显著统计范围为±1.9844。这种相关性绝对有用且统计上显著。代码如下:
import pandas as pd
df = pd.read_csv('https://bit.ly/3C8JzrM' , delimiter="," )
correlations = df.corr(method='pearson' )
print (correlations)
from scipy.stats import t
from math import sqrt
n = df.shape[0 ]
print (n)
lower_cv = t(n - 1 ).ppf(.025 )
upper_cv = t(n - 1 ).ppf(.975 )
r = correlations["y" ]["x" ]
test_value = r / sqrt((1 - r ** 2 ) / (n - 2 ))
print ("TEST VALUE: {}" .format (test_value))
print ("CRITICAL RANGE: {}, {}" .format (lower_cv, upper_cv))
if test_value < lower_cv or test_value > upper_cv:
print ("CORRELATION PROVEN, REJECT H0" )
else :
print ("CORRELATION NOT PROVEN, FAILED TO REJECT H0 " )
if test_value > 0 :
p_value = 1.0 - t(n - 1 ).cdf(test_value)
else :
p_value = t(n - 1 ).cdf(test_value)
p_value = p_value * 2
print ("P-VALUE: {}" .format (p_value))
"""
TEST VALUE: 23.835515323677328
CRITICAL RANGE: -1.9844674544266925, 1.984467454426692
CORRELATION PROVEN, REJECT H0
P-VALUE: 0.0 (extremely small)
"""
在x = 50 时,预测区间在 50.79 到 134.51 之间。代码如下:
import pandas as pd
from scipy.stats import t
from math import sqrt
points = list (pd.read_csv('https://bit.ly/3C8JzrM' , delimiter="," ) \
.itertuples())
n = len (points)
m = 1.75919315
b = 4.69359655
x_0 = 50
x_mean = sum (p.x for p in points) / len (points)
t_value = t(n - 2 ).ppf(.975 )
standard_error = sqrt(sum ((p.y - (m * p.x + b)) ** 2 for p in points) / \
(n - 2 ))
margin_of_error = t_value * standard_error * \
sqrt(1 + (1 / n) + (n * (x_0 - x_mean) ** 2 ) / \
(n * sum (p.x ** 2 for p in points) - \
sum (p.x for p in points) ** 2 ))
predicted_y = m*x_0 + b
print (predicted_y - margin_of_error, predicted_y + margin_of_error)
将测试数据集分成三分,并使用 k-fold(其中k = 3)评估时表现还不错。在这三个数据集中,均方误差大约为 0.83,标准偏差为 0.03。
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import KFold, cross_val_score
df = pd.read_csv('https://bit.ly/3C8JzrM' , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
kfold = KFold(n_splits=3 , random_state=7 , shuffle=True )
model = LinearRegression()
results = cross_val_score(model, X, Y, cv=kfold)
print (results)
print ("MSE: mean=%.3f (stdev-%.3f)" % (results.mean(), results.std()))
"""
[0.86119665 0.78237719 0.85733887]
MSE: mean=0.834 (stdev-0.036)
"""
第六章
当你在 scikit-learn 中运行这个算法时,准确率非常高。我运行时,平均在测试折叠上至少获得 99.9%的准确率。
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import KFold, cross_val_score
df = pd.read_csv("https://bit.ly/3imidqa" , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
kfold = KFold(n_splits=3 , shuffle=True )
model = LogisticRegression(penalty='none' )
results = cross_val_score(model, X, Y, cv=kfold)
print ("Accuracy Mean: %.3f (stdev=%.3f)" % (results.mean(),
results.std()))
混淆矩阵将产生大量的真阳性和真阴性,以及很少的假阳性和假阴性。运行这段代码,你会看到:
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
df = pd.read_csv("https://bit.ly/3imidqa" , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
model = LogisticRegression(solver='liblinear' )
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=.33 )
model.fit(X_train, Y_train)
prediction = model.predict(X_test)
"""
The confusion matrix evaluates accuracy within each category.
[[truepositives falsenegatives]
[falsepositives truenegatives]]
The diagonal represents correct predictions,
so we want those to be higher
"""
matrix = confusion_matrix(y_true=Y_test, y_pred=prediction)
print (matrix)
下面展示了一个用于测试用户输入颜色的交互式 shell。考虑测试黑色(0,0,0)和白色(255,255,255),看看是否能正确预测暗色和浅色字体。
import pandas as pd
from sklearn.linear_model import LogisticRegression
import numpy as np
from sklearn.model_selection import train_test_split
df = pd.read_csv("https://bit.ly/3imidqa" , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
model = LogisticRegression(solver='liblinear' )
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=.33 )
model.fit(X_train, Y_train)
prediction = model.predict(X_test)
while True :
n = input ("Input a color {red},{green},{blue}: " )
(r, g, b) = n.split("," )
x = model.predict(np.array([[int (r), int (g), int (b)]]))
if model.predict(np.array([[int (r), int (g), int (b)]]))[0 ] == 0.0 :
print ("LIGHT" )
else :
print ("DARK" )
是的,逻辑回归在预测给定背景颜色的浅色或暗色字体方面非常有效。准确率不仅极高,而且混淆矩阵在主对角线的右上到左下有很高的数字,其他单元格的数字则较低。
第七章
显然,你可以尝试不同的隐藏层、激活函数、不同大小的测试数据集等进行大量的实验和尝试。我尝试使用一个有三个节点的隐藏层,使用 ReLU 激活函数,但在我的测试数据集上难以得到良好的预测。混淆矩阵和准确率一直很差,我运行的任何配置更是表现不佳。
神经网络可能失败的原因有两个:1)测试数据集对于神经网络来说太小(神经网络对数据需求极大),2)对于这类问题,像逻辑回归这样更简单和更有效的模型存在。这并不是说你不能找到一个适用的配置,但你必须小心,避免通过 p-hack 的方式使得结果过拟合到你所拥有的少量训练和测试数据。
这是我使用的 scikit-learn 代码:
import pandas as pd
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
df = pd.read_csv('https://tinyurl.com/y6r7qjrp' , delimiter="," )
X = df.values[:, :-1 ]
Y = df.values[:, -1 ]
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=1 /3 )
nn = MLPClassifier(solver='sgd' ,
hidden_layer_sizes=(3 , ),
activation='relu' ,
max_iter=100_000 ,
learning_rate_init=.05 )
nn.fit(X_train, Y_train)
print ("Training set score: %f" % nn.score(X_train, Y_train))
print ("Test set score: %f" % nn.score(X_test, Y_test))
print ("Confusion matrix:" )
matrix = confusion_matrix(y_true=Y_test, y_pred=nn.predict(X_test))
print (matrix)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示