原文:
annas-archive.org/md5/a52efc405cb495a26c2bdcb2c25f51df
译者:飞龙
第三部分
高级主题
正如标题所示,本节将讨论与算法相关的一些精选高级主题。密码学和大规模算法是本节的关键亮点。我们还将探讨与训练复杂算法所需的大规模基础设施相关的问题。本节的最后一章将探讨在实施算法时应考虑的实际问题。本节包括的章节有:
-
第十二章,推荐引擎
-
第十三章,数据处理的算法策略
-
第十四章,密码学
-
第十五章,大规模算法
-
第十六章,实际考虑事项
第十二章:推荐引擎
我能得到的最好推荐是我自己的才华,和我自己的努力成果,而别人无法为我做的事,我会尽力为自己去做。
—18 至 19 世纪科学家约翰·詹姆斯·奥杜邦
推荐引擎利用可用的用户偏好和物品详细数据,提供量身定制的建议。推荐引擎的核心目标是识别不同物品之间的共性,并理解用户与物品互动的动态。推荐系统不仅仅专注于产品,还考虑任何类型的物品——无论是歌曲、新闻文章还是产品——并根据这些来定制推荐。
本章首先介绍推荐引擎的基础知识。接着,讨论推荐引擎的各种类型。在本章的后续部分,我们将探讨推荐系统的内部工作原理。这些系统能够为用户推荐量身定制的物品或产品,但也面临着挑战。我们将讨论它们的优势和局限性。最后,我们将学习如何使用推荐引擎解决现实世界中的问题。
本章将涵盖以下内容:
-
推荐引擎概述
-
不同类型的推荐系统
-
认识到推荐方法的限制
-
实际应用领域
-
一个实际的例子
到本章结束时,你应该能够理解如何利用推荐引擎根据某些偏好标准推荐不同的物品。
让我们从推荐引擎的背景概念开始了解。
介绍推荐系统
推荐系统是强大的工具,最初由研究人员设计,但现在在商业环境中得到广泛应用,能够预测用户可能感兴趣的物品。它们通过提供个性化的物品推荐,使得推荐系统成为一个不可或缺的资产,尤其在数字购物领域中。
当推荐引擎应用于电子商务时,使用复杂的算法来改善购物体验,允许服务提供商根据用户的偏好定制产品。
这些系统的重要性的经典例子是 2009 年的 Netflix 奖挑战。Netflix 为了优化其推荐算法,提供了 100 万美元的奖金,奖励任何能够将现有推荐系统 Cinematch 提升 10%的团队。这个挑战吸引了全球研究人员的参与,BellKor 的 Pragmatic Chaos 团队最终获胜。他们的成就突显了推荐系统在商业领域中至关重要的作用和潜力。关于这个有趣挑战的更多内容,可以在本章中了解。
推荐引擎的类型
我们可以将推荐引擎大致分为三大类:
-
基于内容的推荐引擎:它们关注物品属性,将一个产品的特征与另一个产品进行匹配。
-
协同过滤引擎:它们根据用户行为预测偏好。
-
混合推荐引擎:这是一种融合了基于内容和协同过滤方法优点的推荐引擎,用于优化建议。
在建立了类别之后,让我们逐一深入了解这三种推荐引擎的细节:
基于内容的推荐引擎
基于内容的推荐引擎基于一个简单的原理:它们推荐用户以前互动过的类似物品。这些系统的关键在于准确地衡量物品之间的相似性。
举个例子,想象一下图 12.1中描述的场景:
图 12.1:基于内容的推荐系统
假设用户 1阅读了文档 1。由于文档之间的相似性,我们可以向用户 1推荐文档 2。
这种方法只有在我们能够识别并量化这些相似性时才有效。因此,识别物品之间的相似性对于推荐至关重要。接下来我们将探讨如何量化这些相似性。
确定非结构化文档中的相似性
确定不同文档之间相似性的一种方法是使用共现矩阵,它的前提是经常一起购买的物品可能共享相似性或属于互补类别。
例如,购买剃须刀的人可能还需要购买剃须膏。我们可以通过四个用户的购买习惯数据来解码这一点:
剃须刀 | 苹果 | 剃须膏 | 自行车 | 鹰嘴豆泥 | |
---|---|---|---|---|---|
Mike | 1 | 1 | 1 | 0 | 1 |
Taylor | 1 | 0 | 1 | 1 | 1 |
Elena | 0 | 0 | 0 | 1 | 0 |
Amine | 1 | 0 | 1 | 0 | 0 |
要构建共现矩阵,请按照以下步骤操作:
-
初始化一个NxN矩阵,其中N是物品的数量。该矩阵将存储共现计数。
-
对于用户-物品矩阵中的每个用户,通过增加用户与物品对交互的单元格值来更新共现矩阵。
-
最终矩阵展示了基于用户交互的物品关联性。
上表的共现矩阵如下所示:
剃须刀 | 苹果 | 剃须膏 | 自行车 | 鹰嘴豆泥 | |
---|---|---|---|---|---|
剃须刀 | - | 1 | 3 | 1 | 2 |
Apple | 1 | - | 1 | 0 | 1 |
剃须膏 | 3 | 1 | - | 1 | 2 |
自行车 | 1 | 0 | 1 | - | 1 |
鹰嘴豆泥 | 2 | 1 | 2 | 1 | - |
本矩阵本质上展示了两种物品一起购买的可能性。它是推荐系统中的一种有价值的工具。
协同过滤推荐引擎
协同过滤的推荐基于用户历史购买模式的分析。基本假设是,如果两位用户对大部分相同的项目表现出兴趣,我们可以将这两位用户归为相似用户。换句话说,我们可以假设以下情况:
-
如果两位用户的购买历史重叠度超过某个阈值,我们可以将他们归类为相似用户。
-
通过查看相似用户的历史,购买历史中没有重叠的项目将成为协同过滤推荐的基础。
例如,让我们看一个具体的例子。我们有两个用户,User1和User2,如下面的图所示:
图 12.2:协同过滤推荐引擎
请注意以下几点:
-
User1和User2都对完全相同的文档,Doc1和Doc2,表现出了兴趣。
-
根据他们相似的历史模式,我们可以将两者归类为相似用户。
-
如果User1现在阅读Doc3,我们也可以将Doc3推荐给User2。
基于用户历史记录向其推荐项目的策略并不总是有效。让我们更详细地了解与协同过滤相关的问题。
与协同过滤相关的问题
协同过滤涉及的三个潜在问题是:
-
由于样本量有限而导致的不准确性
-
容易受到孤立分析的影响
-
对历史的过度依赖
让我们更详细地了解这些局限性。
由于样本量有限而导致的不准确性
协同过滤系统的准确性和有效性还取决于样本量。例如,如果仅分析三份文档,那么做出准确推荐的可能性就会受到限制。
然而,如果系统拥有数百或数千个文档和交互数据,它的预测能力将变得更加可靠。这就像是基于少量数据点做出预测与拥有全面数据集以进行洞察分析之间的区别。
即使拥有大量数据,协同过滤也并非万无一失。原因在于它完全依赖于用户和项目之间的历史交互,而忽视了任何外部因素。
容易受到孤立分析的影响
协同过滤关注的是用户行为及其与项目互动形成的模式。这意味着它通常会忽视那些可能影响用户选择的外部因素。例如,用户选择某本书可能并非出于个人兴趣,而是因为学术需要或朋友推荐。协同过滤模型无法识别这些细微差别。
对历史的过度依赖
因为系统依赖历史数据,它有时会加强刻板印象,或者跟不上用户不断变化的口味。想象一下,如果一个用户曾经有一段时间喜欢科幻电影,但现在转而喜欢浪漫影片。如果他们过去看了大量的科幻电影,系统可能仍然主要推荐这些电影,而忽视了他们当前的偏好。
从本质上讲,尽管协同过滤在数据量大时非常强大,但理解其固有的局限性非常重要,因为它是通过孤立的操作方式来工作的。
接下来,我们来看看混合推荐引擎。
混合推荐引擎
到目前为止,我们讨论了基于内容和基于协同过滤的推荐引擎。这两种推荐引擎可以结合起来创建一个混合推荐引擎。为此,我们遵循以下步骤:
-
生成物品的相似度矩阵。
-
生成用户的偏好矩阵。
-
生成推荐。
让我们逐步分析这些步骤。
生成物品的相似度矩阵
在混合推荐中,我们首先通过内容推荐创建物品的相似度矩阵。这可以通过使用共现矩阵或任何距离度量来量化物品之间的相似度。
假设我们目前有五个物品。通过基于内容的推荐,我们生成一个矩阵,捕捉物品之间的相似度,如图 12.3所示:
图 12.3:相似度矩阵
让我们看看如何将相似度矩阵与偏好矩阵结合,以生成推荐。
生成用户的参考向量
根据系统中每个用户的历史记录,我们将生成一个捕捉这些用户兴趣的偏好向量。
假设我们要为一家名为KentStreetOnline的在线商店生成推荐,该商店销售 100 种独特的商品。KentStreetOnline非常受欢迎,拥有 100 万活跃订阅者。需要注意的是,我们只需要生成一个尺寸为 100×100 的相似度矩阵。同时,我们还需要为每个用户生成一个偏好向量;这意味着我们需要为 100 万用户生成 100 万个偏好向量。
性能向量的每一项代表了对某个物品的偏好。第一行的值表示Item 1的偏好权重为4。偏好分数并不是购买次数的直接反映,而是一个加权指标,可能考虑浏览历史、过去的购买、物品评分等因素。
一个4的分数可能代表了对Item 1的兴趣和过去的互动,表明用户很可能会喜欢这个物品。
这在图 12.4中有图示:
图 12.4:用户偏好矩阵
现在,让我们来看看如何基于相似度矩阵S和用户偏好矩阵U生成推荐。
生成推荐
为了进行推荐,我们可以对矩阵进行相乘。用户更可能对与他们给出高评分的物品频繁共现的物品感兴趣:
矩阵[S] × 矩阵[U] = 矩阵[R]
这个计算在图 12.5中以图形方式展示:
图 12.5:推荐矩阵的生成
为每个用户生成一个单独的结果矩阵。推荐矩阵*Matrix[R]*中的数字量化了用户对每个项目的预测兴趣。例如,在结果矩阵中,第四个项目的数字最大,达到 58。所以这个项目被强烈推荐给这个特定的用户。
发展推荐系统
推荐系统不是静态的;它们依赖于不断的优化。这个演变是如何发生的呢?通过将推荐的项目(预测)与用户的实际选择进行对比。通过分析差异,系统可以识别出需要改进的地方。随着时间的推移,通过基于用户反馈和观察到的行为进行重新校准,系统提高了推荐的准确性,确保用户始终能收到最相关的建议。
现在,让我们来看看不同推荐系统的局限性。
理解推荐系统的局限性
推荐引擎使用预测算法向一群用户推荐内容。这是一项强大的技术,但我们也应当了解其局限性。让我们来探讨推荐系统的各种限制。
冷启动问题
协同过滤的核心是一个至关重要的依赖性:历史用户数据。没有用户偏好的记录,生成准确的推荐将变得困难。对于一个新加入系统的用户,由于缺乏数据,我们的算法主要基于假设,这可能导致不精确的推荐。同样,在基于内容的推荐系统中,新项目可能缺乏全面的细节,这使得推荐过程不太可靠。这种数据依赖性——需要有一定的用户和项目数据才能产生可靠的推荐——就是所谓的冷启动问题。
有几种策略可以应对冷启动问题:
-
混合系统:将协同过滤与基于内容的过滤相结合,可以通过利用另一种系统的优势来弥补某个系统的局限性。
-
基于知识的推荐:如果历史数据稀缺,依靠关于用户和项目的显式知识可以帮助弥补这一空白。
-
新手问卷:对于新用户,可以通过简短的偏好问卷为系统提供初步数据,从而引导早期推荐。
理解并应对这些挑战,确保推荐系统在用户参与策略中始终是一种有效且可靠的工具。
元数据需求
虽然基于内容的推荐系统可以在没有元数据的情况下运行,但纳入这些细节可以提升其精度。值得注意的是,元数据不仅限于文本描述。在我们多元化的数字生态系统中,商品涵盖了各种媒体类型,如图像、音频或电影。对于这些媒体,“内容”可以从其固有属性中提取。例如,图像相关的元数据可能来自视觉模式;音频的元数据可以来自波形或频谱特征;对于电影,可以考虑如类型、演员阵容或场景结构等方面。
将这些多样的内容维度进行整合,可以使推荐系统更加适应,提供跨广泛商品范围的精确建议。
数据稀疏问题
在大量商品中,用户可能只会评价少数商品,从而导致一个非常稀疏的用户/商品评分矩阵。
亚马逊拥有大约十亿用户和十亿个商品。亚马逊的推荐引擎被认为是全球数据最稀疏的推荐引擎之一。
为了应对这种稀疏性,采用了多种技术。例如,矩阵分解方法可以预测这些稀疏区域中的潜在评分,从而提供更完整的用户-项目交互景观。此外,混合推荐系统结合了基于内容的过滤和协同过滤的元素,即使在用户-项目交互有限的情况下,也能生成有意义的推荐。通过整合这些及其他方法,推荐系统能够有效应对和缓解稀疏数据集带来的挑战。
推荐系统中的社交影响力的双刃剑
推荐系统可以受到社交动态的显著影响。事实上,我们的社交圈常常对我们的偏好和选择产生重要影响。例如,朋友们往往做出相似的购买决策,并以相似的方式对产品或服务进行评价。
从积极的方面来看,利用社交连接可以提升推荐的相关性。如果系统发现某个特定社交群体中的个体喜欢某部电影或某个产品,那么推荐同样的商品给群体中的其他成员可能是有意义的。这可能会带来更高的用户满意度,并且有可能提高转化率。
然而,也存在一个缺点。过度依赖社交影响可能会在推荐中引入偏见。它可能会无意间创建回音室效应,让用户只接触到他们社交圈内认可的项目,从而限制了多样性,并可能错过那些更符合个人需求的产品或服务。此外,这还可能导致自我强化的反馈循环,同样的项目不断被推荐,压倒其他潜在的有价值项目。
因此,尽管社交影响力是塑造用户偏好的强大工具,推荐系统仍需要将其与用户个体行为和更广泛的趋势平衡,以确保多样化和个性化的用户体验。
实际应用领域
推荐系统在我们日常的数字互动中扮演着关键角色。为了真正理解它们的重要性,我们将探讨它们在各个行业中的应用。
根据提供的关于 Netflix 使用数据科学及其推荐系统的全面信息,让我们来看一下调整后的陈述,涵盖了这些要点。
Netflix 在数据驱动推荐方面的精通
作为流媒体行业的领导者,Netflix 通过数据分析优化内容推荐,硅谷的 800 名工程师在推动这一努力。它们对数据驱动策略的重视在 Netflix Prize 挑战中得到了体现。获胜团队 BellKor’s Pragmatic Chaos 使用了 107 种不同的算法,从矩阵分解到限制玻尔兹曼机,投入了 2,000 小时的开发时间。
结果是他们的“Cinematch”系统显著提高了 10.06%。这转化为更多的流媒体观看时长、更少的订阅取消,以及 Netflix 的巨大节省。有趣的是,现在大约 75%的用户观看内容是由推荐决定的。Töscher 等人(2009 年)提出了一种有趣的“1 天效应”,暗示共享账户或用户情绪波动可能影响推荐。
尽管这个挑战展示了 Netflix 对数据的承诺,但它也暗示了集成技术在平衡推荐多样性和准确性方面的潜力。
今天,获胜模型的元素仍然是 Netflix 推荐引擎的核心,但随着技术的不断发展,仍有进一步改进的潜力,例如集成强化算法和改进的 A/B 测试。
以下是 Netflix 统计数据的来源:towardsdatascience.com/netflix-recommender-system-a-big-data-case-study-19cfa6d56ff5
。
亚马逊推荐系统的演变
在 2000 年代初期,亚马逊通过将推荐引擎从基于用户的协同过滤转变为物品与物品之间的协同过滤,彻底改变了其推荐系统。正如林登、史密斯和约克在 2003 年的开创性论文中所详细介绍的,这一策略从基于相似用户推荐产品转变为根据个别产品购买情况推荐相关产品。
这种“相关性”的本质来自于观察到的客户购买模式。如果《哈利·波特》书籍的买家经常购买哈利·波特书签,那么这些物品就被认为是相关的。然而,初始系统存在缺陷。对于高频购买者,推荐不够精细,这促使史密斯和他的团队对算法进行了必要的调整。
快进到几年后——在 2019 年的 re:MARS 大会上,亚马逊强调了其在为 Prime Video 客户提供电影推荐方面的重大进展,实现了两倍的提升。
该技术灵感来源于矩阵补全问题。这种方法涉及将 Prime Video 用户和电影表示在一个网格中,并预测用户观看特定电影的概率。亚马逊随后应用深度神经网络来解决这个矩阵问题,从而实现了更准确和个性化的电影推荐。
未来充满潜力。随着持续的研究和进步,亚马逊团队计划进一步优化和革新推荐算法,不断提升客户体验。
你可以在这里找到亚马逊的统计数据:www.amazon.science/the-history-of-amazons-recommendation-algorithm
。
现在,让我们尝试使用推荐引擎来解决一个实际问题。
实际案例 – 创建一个推荐引擎
让我们构建一个推荐引擎,能够为一群用户推荐电影。我们将使用由明尼苏达大学 GroupLens 研究小组整理的数据。
1. 搭建框架
我们的第一项任务是确保拥有正确的工具。对于 Python 来说,这意味着导入必要的库:
import pandas as pd
import numpy as np
2. 数据加载:导入评论和标题
现在,让我们导入 df_reviews
和 df_movie_titles
数据集:
df_reviews = pd.read_csv('https://storage.googleapis.com/neurals/data/data/reviews.csv')
df_reviews.head()
reviews.csv
数据集包含了丰富的用户评论集合。每个条目包含用户的 ID、他们评论过的电影 ID、评分以及评论时间戳。
图 12.6:reviews.csv 数据集的内容
movies.csv
数据集是电影标题及其详细信息的汇编。每条记录通常包含一个独特的电影 ID、电影标题以及其相关的类别或类别。
图 12.7:movies.csv 数据集的内容
3. 合并数据:打造全面视图
从全局角度看,我们需要合并这些数据集。'movieId'
是我们连接它们的桥梁:
df = pd.merge(df_reviews, df_movie_titles, on='movieId')
df.head()
合并后的数据集应包含以下信息:
图 12.8:合并的电影数据
以下是每一列的简要说明:
-
userId
:每个用户的唯一标识符。 -
movieId
:每部电影的唯一标识符。 -
rating
:表示用户对电影的评分,范围从 1 到 5。 -
timestamp
:表示特定电影被评分的时间。 -
title
:电影的标题 -
genres
:与电影相关的类型。
4. 描述性分析:从评分中获取洞察
让我们深入了解数据的核心:评分。一个好的起点是计算每部电影的平均评分。同时,了解评分人数也能提供有关电影受欢迎程度的线索:
df_ratings = pd.DataFrame(df.groupby('title')['rating'].mean())
df_ratings['number_of_ratings'] = df.groupby('title')['rating'].count()
df_ratings.head()
每部电影的 mean
评分应如下所示:
图 12.9:计算平均评分
通过这些聚合指标,我们可以识别出评分较高的热门电影,具有大量评分的潜在大片,或者可能有较少评论但平均评分较高的隐藏佳作。
这个基础将为后续步骤铺平道路,我们将深入构建实际的推荐引擎。随着进展,我们对用户偏好的理解将不断深化,从而使我们能够推荐更符合个人口味的电影。
5. 为推荐构建结构:构建矩阵
下一步是将我们的数据集转换为适合推荐的结构。可以将这个结构想象成一个矩阵:
-
行表示我们的用户(按
userId
索引) -
列表示电影标题
-
矩阵中的单元格填充了评分,显示了用户对特定电影的评价
Pandas 中的 pivot_table
函数是一个多功能工具,帮助重塑或透视 DataFrame 中的数据,以提供总结视图。该函数本质上是从原始表创建一个新的派生表:
movie_matrix = df.pivot_table(index='userId', columns='title', values='rating')
请注意,前面的代码将生成一个非常稀疏的矩阵。
6. 测试引擎:推荐电影
让我们看看引擎如何工作。假设一个用户刚刚看过 阿凡达(2009)。我们如何找到他们可能喜欢的其他电影呢?
我们的第一个任务是隔离所有评分过 阿凡达(2009)的用户:
avatar_ratings = movie_matrix['Avatar (2009)']
avatar_ratings = avatar_ratings.dropna()
print("\nRatings for 'Avatar (2009)':")
print(avatar_ratings.head())
userId
10 2.5
15 3.0
18 4.0
21 4.0
22 3.5
Name: Avatar (2009), dtype: float64
从前面的代码中,注意以下几点:
-
userId:表示我们数据集中每个用户的唯一标识符。
userId
列表包含10
、15
、18
、21
和22
——我们数据快照中前五个评分 阿凡达(2009)的用户。 -
评分:与每个
userId
(2.5
、3.0
、4.0
、4.0
、3.5
)相邻的数字代表这些用户给阿凡达(2009)电影的评分。评分范围为1
到5
,其中更高的评分表示用户对电影的评价更高。例如,用户 10给阿凡达(2009)的评分是2.5
,意味着他们觉得电影一般,甚至略低于预期,而用户 22给出了3.5
的评分,表明他们对电影有略高于平均水平的评价。
让我们构建一个推荐引擎,能够为一群用户推荐电影。
寻找与《阿凡达》(2009)相关的电影
通过确定其他电影与阿凡达(2009)的评分模式的相关性,我们可以建议可能吸引阿凡达粉丝的电影。
为了简洁地呈现我们的发现:
similar_to_Avatar=movie_matrix.corrwith(Avatar_user_rating)
corr_Avatar = pd.DataFrame(similar_to_Avatar, columns=['correlation'])
corr_Avatar.dropna(inplace=True)
corr_Avatar = corr_Avatar.join(df_ratings['number_of_ratings'])
corr_Avatar.head()
correlation number_of_ratings
title
'burbs, The (1989) 0.353553 17
(500) Days of Summer (2009) 0.131120 42
*batteries not included (1987) 0.785714 7
10 Things I Hate About You (1999) 0.265637 54
《公元 10000 年》(2008) -0.075431 理解相关性
较高的相关性(接近 1)表示一部电影的评分模式与阿凡达(2009)相似。负值则表示相反的情况。
然而,必须谨慎处理推荐。例如,电影**《电池未 included》(1987)**成了阿凡达(2009)粉丝的热门推荐,这看起来并不准确。可能是因为仅依赖用户评分而忽视其他因素(如类型或电影主题)会导致问题。为了更精确的推荐系统,必须进行调整和优化。
最终的表格展示了与阿凡达的用户评分行为相关的电影。我们分析结束时生成的表格按与阿凡达的相关性列出了电影。但是,这到底意味着什么呢?
在这里,相关性指的是一种统计度量,用来解释一组数据相对于另一组数据的变化关系。具体来说,我们使用了皮尔逊相关系数,它的取值范围是从-1 到 1:
-
1:完美正相关。这意味着如果阿凡达收到某用户的高评分,那么同一用户给的其他电影也会获得高评分。
-
-1:完美负相关。如果用户给阿凡达打了高分,那么同一用户给的其他电影评分会很低。
-
0:无相关性。阿凡达与其他电影的评分彼此独立。
在我们的电影推荐情境中,与阿凡达相关性较高(接近 1)的电影被认为更适合推荐给喜欢阿凡达的用户。这是因为这些电影展示了与阿凡达相似的评分模式。
通过检查表格,你可以识别出哪些电影的评分行为与阿凡达类似,因此它们可能成为阿凡达粉丝的潜在推荐。
这意味着我们可以将这些电影作为推荐项提供给用户。
评估模型
测试和评估至关重要。评估我们模型的一种方法是使用诸如训练-测试分割等方法,将部分数据留作测试。然后将模型在测试集上的推荐与实际用户评分进行比较。像平均绝对误差(MAE)或均方根误差(RMSE)等指标可以量化这些差异。
随着时间的推移重新训练:融入用户反馈
用户偏好会不断变化。定期用新数据重新训练推荐模型,确保其推荐结果始终相关。引入反馈循环,让用户对推荐进行评分或评论,进一步提升模型的准确性。
总结
本章我们学习了推荐引擎。我们研究了如何根据我们要解决的问题选择合适的推荐引擎。我们还探讨了如何为推荐引擎准备数据,以创建相似度矩阵。我们还学习了推荐引擎如何用于解决实际问题,例如根据用户过去的行为模式推荐电影。
在下一章中,我们将重点介绍用于理解和处理数据的算法。
在 Discord 上了解更多
要加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问以及了解新版本——请扫描下面的二维码:
第十三章:数据处理的算法策略
数据是数字经济的新石油。
—《连线杂志》
在这个数据驱动的时代,从大量数据集中提取有意义的信息的能力正在从根本上塑造我们的决策过程。我们在本书中探讨的算法在很大程度上依赖于数据。因此,开发旨在创建强大且高效的数据存储基础设施的工具、方法和战略计划变得尤为重要。
本章的重点是以数据为中心的算法,用于高效地管理数据。这些算法的核心操作包括高效存储和数据压缩。通过运用这些方法,以数据为中心的架构能够实现数据管理和高效的资源利用。通过本章的学习,你应该能够很好地理解设计和实施各种数据中心算法时涉及的概念和权衡。
本章讨论以下概念:
-
数据算法简介
-
数据分类
-
数据存储算法
-
数据压缩算法
让我们首先介绍基本概念。
数据算法简介
数据算法专门用于管理和优化数据存储。除了存储,它们还处理数据压缩等任务,确保高效的存储空间利用率,并简化快速的数据检索,这在许多应用中至关重要。
理解数据算法,特别是在分布式系统中,关键的一点是 CAP 定理。它的重要性在于:该定理阐明了在一致性、可用性和分区容忍性之间的平衡。在任何分布式系统中,同时实现这三者中的两项保障是我们所能期望的。理解 CAP 的微妙之处有助于识别现代数据算法中的挑战和设计决策。
在数据治理的范围内,这些算法是非常宝贵的。它们确保分布式系统中所有节点的数据一致性,从而保证数据的完整性。它们还确保数据的高效可用性,并管理数据分区容忍度,从而增强系统的弹性和安全性。
CAP 定理在数据算法中的重要性
CAP 定理不仅设定了理论上的极限,它在现实场景中也具有实际的意义,尤其是在数据被操作、存储和检索的过程中。例如,假设一个算法必须从分布式系统中检索数据。关于一致性、可用性和分区容忍性的选择会直接影响该算法的效率和可靠性。如果一个系统优先考虑可用性,数据可能很容易被检索,但可能不是最新的版本。相反,优先考虑一致性的系统可能会延迟数据检索,以确保访问的始终是最新的数据。
我们在这里讨论的数据中心算法在许多方面都受到这些 CAP 约束的影响。通过将我们对 CAP 定理的理解与数据算法相结合,我们可以在处理数据挑战时做出更明智的决策。
分布式环境中的存储
单节点架构适用于较小的数据集。然而,随着数据集规模的激增,分布式环境存储已成为大规模问题的标准解决方案。在此类环境中,确定合适的数据存储策略取决于多个因素,包括数据的性质和预期的使用模式。
CAP 定理为开发这些存储策略提供了一个基础性原理,帮助我们应对与管理庞大数据集相关的挑战。
连接 CAP 定理与数据压缩
起初,CAP 定理与数据压缩似乎没有什么重叠。但考虑到实际的影响,如果我们在系统中优先考虑一致性(按照 CAP 的考虑),那么我们的数据压缩方法需要确保数据在所有节点间始终保持一致的压缩状态。在一个以可用性为优先的系统中,即便这可能导致轻微的不一致,压缩方法可能会为了速度进行优化。这种相互作用表明,我们在 CAP 方面的选择甚至会影响到数据的压缩和检索方式,展示了定理在数据中心算法中的广泛影响。
展示 CAP 定理
1998 年,Eric Brewer 提出了一个定理,后来被称为 CAP 定理。它突出了设计分布式服务系统中涉及的各种权衡。为了理解 CAP 定理,首先,我们需要定义分布式服务系统的以下三个特性:一致性、可用性和分区容忍性。CAP 实际上是由这三种特性组成的首字母缩写:
-
一致性(或简称C):分布式服务由多个节点组成。任何一个节点都可以用来读取、写入或更新数据仓库中的记录。一致性保证了在某个特定时间 t1,无论我们使用哪个节点来读取数据,都能得到相同的结果。每次读取操作要么返回最新的数据(在分布式数据仓库中保持一致),要么返回错误信息。
-
可用性(或简称A):在分布式系统中,可用性意味着系统整体始终对请求作出响应。这确保了用户每次查询系统时都会得到回复,即使这可能不是最新的数据。因此,重点不在于每个节点是否都保持最新,而在于整个系统的响应能力。它保证了即使系统的某些部分包含过时的信息,用户的请求也永远不会没有回复。
-
分区容忍性(简称P):在分布式系统中,多个节点通过通信网络连接。分区容忍性保证在发生部分节点(一个或多个)之间的通信故障时,系统仍然能够正常运行。需要注意的是,为了保证分区容忍性,数据需要在足够数量的节点之间进行复制。
利用这三个特性,CAP 定理仔细总结了分布式服务系统架构和设计中涉及的权衡。具体来说,CAP 定理指出,在分布式存储系统中,我们只能拥有以下三个特性中的两个:一致性或C,可用性或A,分区容忍性或P。
这一点在下面的图表中得以展示:
图 13.1:在分布式系统中可视化选择:CAP 定理
分布式数据存储正日益成为现代 IT 基础设施的重要组成部分。设计分布式数据存储时应根据数据的特性和我们要解决的问题的需求来仔细考虑。当应用于分布式数据库时,CAP 定理有助于指导设计和决策过程,确保开发人员和架构师理解在创建分布式数据库系统时涉及的基本权衡和限制。平衡这三个特性对于实现分布式数据库系统的期望性能、可靠性和可扩展性至关重要。当应用于分布式数据库时,CAP 定理有助于指导设计和决策过程,确保开发人员和架构师理解基本的权衡。平衡这三个特性在实现分布式数据库系统的期望性能、可靠性和可扩展性方面至关重要。在 CAP 定理的背景下,我们可以假设有三种类型的分布式存储系统:
-
CA系统(实现一致性-可用性)
-
AP系统(实现可用性-分区容忍性)
-
CP系统(实现一致性-分区容忍性)
将数据存储分类为CA、AP和CP系统有助于我们理解在设计数据存储系统时涉及的各种权衡。
让我们逐一了解它们。
CA 系统
传统的单节点系统是 CA 系统。在非分布式系统中,分区容忍性不是一个问题,因为无需管理多个节点之间的通信。因此,这些系统可以专注于同时维护一致性和可用性。换句话说,它们是 CA 系统。
一个系统可以通过在单个节点或服务器上存储和处理数据来实现没有分区容错的功能。虽然这种方法可能不适用于处理大规模数据集或高速数据流,但对于较小的数据规模或对性能要求不高的应用来说,它是有效的。
传统的单节点数据库,如 Oracle 或 MySQL,是 CA 系统的典型例子。这些系统非常适合数据量和数据流速相对较低且分区容错不是关键因素的使用场景。例子包括中小型企业、学术项目或具有有限用户和数据源的应用。
AP 系统
AP 系统是设计用来优先考虑可用性和分区容错的分布式存储系统,即使需要牺牲一致性。这些高响应系统可以在必要时牺牲一致性,以适应高速数据流。在这样做的过程中,这些分布式存储系统能够立即处理用户请求,即使这会导致不同节点间暂时提供稍微过时或不一致的数据。
当 AP 系统牺牲一致性时,用户可能会偶尔获取稍微过时的信息。在某些情况下,这种临时的不一致性是可以接受的,因为能够快速处理用户请求并保持高可用性被认为比严格的数据一致性更为关键。
典型的 AP 系统用于实时监控系统,如传感器网络。高速度的分布式数据库,如 Cassandra,是 AP 系统的典型例子。
在需要高可用性、响应性和分区容错的场景中,推荐使用 AP 系统来实现分布式数据存储。
例如,如果加拿大交通运输部希望通过在渥太华高速公路上不同位置安装的传感器网络来监控交通状况,那么 AP 系统将是首选。在这种情况下,优先考虑实时数据处理和可用性对于确保交通监控能够有效运行至关重要,即使存在网络分区或临时的不一致性。因此,尽管可能会牺牲一致性,AP 系统仍然是这种应用的推荐选择。
CP 系统
CP 系统优先考虑一致性和分区容错,确保分布式存储系统在读取过程中获取值之前保证一致性。这些系统专门设计用于维护数据一致性,并在存在网络分区的情况下继续有效运行。
CP 系统的理想数据类型是那些需要严格一致性和准确性的数据,即使这意味着牺牲系统的即时可用性。例子包括财务交易、库存管理和关键业务操作数据。在这些情况下,确保数据在分布式环境中的一致性和准确性至关重要。
CP 系统的典型使用案例是我们想要以 JSON 格式存储文档文件。像 MongoDB 这样的文档数据存储是为分布式环境中的一致性而调整的 CP 系统。
通过了解不同类型的分布式存储系统,我们现在可以继续探索数据压缩算法。
解码数据压缩算法
数据压缩是用于数据存储的重要方法。它不仅提高了存储效率,减少了数据传输时间,而且在成本节省和性能提升方面具有重要意义,特别是在大数据和云计算领域。本节介绍了详细的数据压缩技术,特别关注无损算法哈夫曼和 LZ77,以及它们对现代压缩方案(如 Gzip、LZO 和 Snappy)的影响。
无损压缩技术
无损压缩围绕着消除数据中的冗余,以减少存储需求,同时确保完美的可逆性。哈夫曼和 LZ77 是两个基础算法,它们在这一领域产生了深远影响。
哈夫曼编码侧重于可变长度编码,使用较少的位表示频繁出现的字符,而 LZ77 是一种基于字典的算法,利用重复的数据序列并用较短的引用表示它们。让我们一一来看。
哈夫曼编码:实现可变长度编码
哈夫曼编码,一种熵编码形式,广泛应用于无损数据压缩。哈夫曼编码的基本原理是为频繁出现的字符分配较短的编码,从而减少整体数据大小。
该算法使用一种特定类型的二叉树,称为哈夫曼树,其中每个叶子节点对应一个数据元素。元素出现的频率决定了在树中的位置:频繁出现的元素更靠近树根。这种策略确保最常见的元素具有最短的编码。
一个简单的示例
假设我们有包含字母A、B和C的数据,其频率分别为5
、9
和12
。在哈夫曼编码中:
-
C,最常见的,可能会得到像
0
这样的短编码。 -
B,下一个最常见的,可能会得到
10
。 -
A,最不常见的,可能会得到
11
。
为了全面理解,我们来通过一个 Python 示例。
在 Python 中实现哈夫曼编码
我们通过为每个字符创建一个节点开始,其中节点包含字符及其频率。然后,将这些节点添加到优先队列中,频率最低的元素具有最高优先级。为此,我们创建一个Node
类来表示霍夫曼树中的每个字符。每个Node
对象包含字符、其频率,以及指向其左子节点和右子节点的指针。__lt__
方法用于根据频率比较两个Node
对象。
import functools
@functools.total_ordering
class Node:
def __init__(self, char, freq):
self.char = char
self.freq = freq
self.left = None
self.right = None
def __lt__(self, other):
return self.freq < other.freq
def __eq__(self, other):
return self.freq == other.freq
接下来,我们构建霍夫曼树。构建霍夫曼树涉及在优先队列中进行一系列的插入和删除操作,通常实现为二叉堆。为了构建霍夫曼树,我们创建一个Node
对象的最小堆。最小堆是一种特殊的树状结构,满足一个简单而重要的条件:父节点的值小于或等于其子节点的值。这个属性确保最小的元素始终位于根节点,使得优先操作更加高效。我们反复弹出两个频率最低的节点,将它们合并,并将合并后的节点推回堆中。这个过程持续进行,直到只剩下一个节点,它成为霍夫曼树的根节点。树的构建通过以下定义的build_tree
函数来实现:
import heapq
def build_tree(frequencies):
heap = [Node(char, freq) for char, freq in frequencies.items()]
heapq.heapify(heap)
while len(heap) > 1:
node1 = heapq.heappop(heap)
node2 = heapq.heappop(heap)
merged = Node(None, node1.freq + node2.freq)
merged.left = node1
merged.right = node2
heapq.heappush(heap, merged)
return heap[0] # the root node
一旦霍夫曼树构建完成,我们可以通过遍历树来生成霍夫曼编码。从根节点开始,每走一条左分支就附加一个0
,每走一条右分支就附加一个1
。当我们到达一个叶子节点时,沿着从根到该叶子节点路径上累积的0
和1
序列就是该叶子节点对应字符的霍夫曼编码。这个功能通过如下定义的generate_codes
函数来实现。
def generate_codes(node, code='', codes=None):
if codes is None:
codes = {}
if node is None:
return {}
if node.char is not None:
codes[node.char] = code
return codes
generate_codes(node.left, code + '0', codes)
generate_codes(node.right, code + '1', codes)
return codes
现在让我们使用霍夫曼树。我们首先定义用于霍夫曼编码的数据。
data = {
'L': 0.45,
'M': 0.13,
'N': 0.12,
'X': 0.16,
'Y': 0.09,
'Z': 0.05
}
然后,我们打印出每个字符的霍夫曼编码。
# Build the Huffman tree and generate the Huffman codes
root = build_tree(data)
codes = generate_codes(root)
# Print the root of the Huffman tree
print(f'Root of the Huffman tree: {root}')
# Print out the Huffman codes
for char, code in codes.items():
print(f'{char}: {code}')
Root of the Huffman tree: <__main__.Node object at 0x7a537d66d240>
L: 0
M: 101
N: 100
X: 111
Y: 1101
Z: 1100
现在,我们可以推断出以下内容:
-
固定长度编码:此表的固定长度编码为
3
。这是因为,对于六个字符,固定长度的二进制表示需要最多三个位(2³ = 8 种可能的组合,可以涵盖我们的 6 个字符)。 -
可变长度编码:此表的可变长度编码为
45(1) + .13(3) + .12(3) + .16(3) + .09(4) + .05(4) = 2.24.
以下图示显示了从前述示例创建的霍夫曼树:
图 13.2:霍夫曼树:可视化压缩过程
请注意,霍夫曼编码是将数据转换为霍夫曼树,从而实现压缩。解码或解压缩则将数据恢复为原始格式。
在了解了霍夫曼编码后,我们接下来将探索另一种基于字典的无损压缩技术。
接下来,我们讨论基于字典的压缩。
理解基于字典的压缩 LZ77
LZ77 属于一种称为字典编码器的压缩算法家族。与霍夫曼编码通过维持静态的代码词典不同,LZ77 会动态构建一个输入数据中已出现的子字符串字典。这个字典不会单独存储,而是通过一个滑动窗口隐式引用已经编码的输入数据,从而提供一种优雅且高效的表示重复序列的方法。
LZ77 算法的原理是将重复的数据替换为指向单一副本的引用。它保持一个“滑动窗口”,用于处理最近的数据。当它遇到已经出现过的子字符串时,它不会存储实际的子字符串;而是存储一对值——指向重复子字符串开始位置的距离,以及重复子字符串的长度。
通过一个示例来理解
想象一下,你正在阅读以下字符串:
data_string = "ABABCABABD"
当你从左到右处理这个字符串时,当遇到子字符串 “CABAB
” 时,你会注意到 “ABAB
” 之前已经出现过,紧接着最初的 “AB
” 后面。LZ77 利用了这种重复现象。
LZ77 不会再写 “ABAB
”,它会建议:“嘿,回溯两个字符并复制接下来的两个字符!”从技术角度讲,这是指回溯两个字符并复制两个字符。
所以,使用 LZ77 压缩我们的 data_string
,它可能看起来像这样:
ABABC<2,2>D
这里,<2,2>
是 LZ77 的符号表示,意味着“回溯两个字符并复制接下来的两个字符”。
与霍夫曼的比较
为了更好地理解 LZ77 和霍夫曼之间的强大功能和差异,使用相同的数据是有帮助的。让我们继续使用 data_string = "ABABCABABD"
。
虽然 LZ77 识别数据中的重复序列并加以引用,但霍夫曼编码更多的是将频繁出现的字符表示为更短的编码。
例如,如果你使用霍夫曼算法压缩我们的 data_string
,你可能会看到一些字符,比如 ‘A
’ 和 ‘B
’,它们出现频率较高,会用比较少出现的 ‘C
’ 和 ‘D
’ 更短的二进制代码表示。
这个比较展示了,尽管霍夫曼编码基于频率来表示字符,但 LZ77 则是通过识别和引用模式来进行压缩。根据数据的类型和结构,某一种可能比另一种更高效。
高级无损压缩格式
由霍夫曼和 LZ77 提出的原则催生了高级压缩格式。本章将探讨三种高级格式。
-
LZO
-
Snappy
-
gzip
让我们一一来看它们。
LZO 压缩:优先考虑速度
LZO 是一种无损数据压缩算法,强调快速的压缩和解压缩。它将重复的数据替换为指向单一副本的引用。在经过这一次 LZ77 压缩后,数据会被传递到霍夫曼编码阶段。
尽管其压缩比可能不是最高的,但其处理速度明显快于许多其他算法。这使得 LZO 成为在实时数据处理和流媒体应用等需要快速数据访问的场景中非常理想的选择。
Snappy 压缩:寻求平衡
Snappy 是另一个由 Google 最初开发的快速压缩和解压缩库。Snappy 的主要关注点是实现高速和合理的压缩,但不一定是最大压缩比。
Snappy 的压缩方法基于 LZ77,但侧重于速度,并且没有像霍夫曼编码那样的额外熵编码步骤。相反,Snappy 使用了一种更简单的编码算法,确保压缩和解压缩过程的快速执行。该算法采用基于复制的策略,寻找数据中重复的序列,并将其编码为长度和对先前位置的引用。
应该注意,由于这种速度上的权衡,Snappy 的数据压缩效率不如使用霍夫曼编码或其他形式的熵编码的算法。然而,在速度比压缩比更为关键的使用场景中,Snappy 可以是一个非常有效的选择。
GZIP 压缩:最大化存储效率
GZIP
是一种文件格式和软件应用程序,用于文件压缩和解压缩。GZIP
数据格式结合了 LZ77 算法和霍夫曼编码。
实际示例:AWS 中的数据管理:聚焦于 CAP 定理和压缩算法
让我们考虑一个全球电子商务平台的例子,该平台运行在全球多个云服务器上。这个平台每秒处理成千上万的交易,来自这些交易的数据需要高效地存储和处理。我们将看到 CAP 定理和压缩算法如何引导平台数据管理系统的设计。
1. 应用 CAP 定理
CAP 定理指出,分布式数据存储不能同时提供以下三种保证中的两种:一致性、可用性和分区容错性。
在我们的电子商务平台场景中,可用性和分区容错性可能会被优先考虑。高可用性确保即使一些服务器发生故障,系统仍能继续处理交易。分区容错性意味着即使网络故障导致某些服务器被隔离,系统仍然可以继续运行。
虽然这意味着系统可能无法始终提供强一致性(每次读取都获得最新的写入),但它可以使用最终一致性(更新通过系统传播,最终所有副本显示相同的值)来确保良好的用户体验。在实践中,轻微的不一致是可以接受的,例如,当用户的购物车在所有设备上更新需要几秒钟时。
在 AWS 生态系统中,我们有多种数据存储服务可供选择,可以根据 CAP 定理定义的需求进行选择。对于我们的电商平台,我们更倾向于选择可用性和分区容忍度,而非一致性。亚马逊 DynamoDB 作为一款键值型 NoSQL 数据库,十分契合这一需求。它内建支持多区域复制和自动分片,确保高可用性和分区容忍度。
为了保持一致性,DynamoDB 提供了“最终一致性”和“强一致性”选项。在我们的案例中,我们会选择最终一致性,以优先考虑可用性和性能。
2. 使用压缩算法
平台会生成大量的数据,包括交易详情、用户行为日志和产品信息。存储和传输这些数据可能会既昂贵又耗时。
在这里,像 gzip、Snappy 或 LZO 这样的压缩算法可以提供帮助。例如,平台可能会使用 gzip 来压缩那些被归档存储的交易日志。考虑到 gzip 通常能将文本文件压缩到原始大小的约 30%,这可以显著降低存储成本。
另一方面,对于用户行为数据的实时分析,平台可能会使用 Snappy 或 LZO。虽然这些算法的压缩比可能不如 gzip,但它们更快速,能够让分析系统更快地处理数据。
AWS 提供了多种实现压缩的方法,具体取决于数据的类型和使用场景。对于长时间存储的交易日志,我们可以使用亚马逊 S3(简单存储服务)结合 gzip 压缩。S3 支持在上传文件时自动进行 gzip 压缩,这可以显著降低存储成本。对于用户行为数据的实时分析,我们可以使用亚马逊 Kinesis 数据流结合 Snappy 或 LZO 压缩。Kinesis 可以捕获、处理并存储数据流以进行实时分析,并支持压缩以处理大容量数据。
3. 量化效益
其效益的量化方式与前述类似。
让我们通过一个实际的例子来演示潜在的成本节省。假设我们的平台每天生成 1 TB 的交易日志。通过利用 gzip 压缩与 S3,我们可能将存储需求缩小到大约 300 GB。截至 2023 年 8 月,S3 对前 50 TB 每月收费约为每 GB $0.023。算一下,这每月可以节省约 $485,年节省约 $5,820,仅从日志存储方面来看。值得注意的是,引用的 AWS 定价仅供参考,具体数据可能因 2023 年 8 月而异,使用时请务必查看最新的定价。
使用 Snappy 或 LZO 与 Kinesis 进行实时分析可以提高数据处理速度。这可能会导致更及时和个性化的用户推荐,进而有可能增加销售额。财务收益可以根据通过提高推荐速度所带来的转化率提升来计算。
最后,通过使用 DynamoDB 并遵循 CAP 定理,我们可以确保即使在发生网络分区或单个服务器故障的情况下,用户仍能享受到流畅的购物体验。这一选择的价值可以体现在平台的用户留存率和整体客户满意度上。
总结
在本章中,我们探讨了以数据为中心的算法设计,重点关注三个关键组成部分:数据存储、数据治理和数据压缩。我们研究了与数据治理相关的各种问题。我们分析了数据的不同属性如何影响数据存储的架构决策。我们探讨了不同的数据压缩算法,每种算法在效率和性能方面提供了特定的优势。在下一章中,我们将研究密码学算法。我们将了解如何利用这些算法的力量来确保交换和存储的信息安全。
在 Discord 上了解更多
要加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问,并了解新版本的发布——请扫描下面的二维码:
第十四章:密码学
我把我未写的诗隐藏在面部的密码中!
—乔治·艾略特
本章将向你介绍与密码学相关的算法。我们将首先介绍背景知识,然后讨论对称加密算法。接着,我们将解释消息摘要 5(MD5)算法和安全哈希算法(SHA),并展示对称算法的局限性和弱点。然后,我们将讨论非对称加密算法,以及它们如何用于创建数字证书。最后,我们将呈现一个实际示例,总结所有这些技术。
到本章结束时,你将对与密码学相关的各种问题有一个基本的理解。
本章讨论了以下主题:
-
密码学简介
-
理解密码学技术的种类
-
示例 – 部署机器学习模型时的安全问题
让我们从基础概念开始。
密码学简介
保护秘密的技术已经存在了几个世纪。最早尝试保护和隐藏数据以防敌人窃取可以追溯到古埃及在纪念碑上发现的古老铭文,其中使用了一种只有少数可信人员才知道的特殊字母。这种早期的安全形式叫做“模糊性”,至今仍以不同形式使用。为了使这种方法有效,保护秘密是至关重要的,在上述例子中就是保护字母的秘密含义。后来,在第一次世界大战和第二次世界大战中,找到可靠的保护重要信息的方法变得尤为重要。进入 20 世纪后期,随着电子技术和计算机的出现,发展出了一些复杂的算法来保护数据,这促成了密码学这一新领域的出现。本章讨论了密码学的算法方面。密码学算法的一种用途是允许两个过程或用户之间进行安全的数据交换。密码算法通过使用数学函数来确保既定的安全目标。
首先,我们将看一下基础设施中“最弱链条”的重要性。
理解最弱链条的重要性
有时候,在设计数字基础设施的安全性时,我们过于强调单个实体的安全,而没有足够关注端到端的安全性。这可能导致我们忽视系统中的某些漏洞和脆弱点,后来这些漏洞可能被黑客利用,从而访问敏感数据。需要记住的重要一点是,一个数字基础设施整体的强度取决于它最薄弱的环节。对于黑客来说,这个最弱的环节可能为他们提供进入数字基础设施的后门,访问敏感数据。超过某个点之后,如果没有关闭所有后门,再加固前门就没有太大的意义。
随着用于保护数字基础设施的算法和技术变得越来越复杂,攻击者也不断升级他们的技术。我们始终需要记住,攻击者攻破数字基础设施的最简单方法之一,就是通过利用这些漏洞来访问敏感信息。
2014 年,对加拿大一个联邦研究机构——国家研究委员会(NRC)的网络攻击估计造成了数亿美元的损失。攻击者能够窃取数十年的研究数据和知识产权材料。他们利用了 Web 服务器上使用的 Apache 软件中的一个漏洞,成功获得了敏感数据。
本章将重点讲解各种加密算法的漏洞。
让我们首先来看一下使用的基本术语。
基本术语
让我们来看一下与密码学相关的基本术语:
-
密码:执行特定密码学功能的算法。
-
明文:原始数据,可以是文本文件、视频、位图或数字化的语音。在本章中,我们将明文表示为 P。
-
密文:在对明文应用密码学后得到的加密文本。在本章中,我们将其表示为 C。
-
密码套件:一组或一套密码学软件组件。当两个独立的节点希望使用密码学交换信息时,他们首先需要就密码套件达成一致。这一点对于确保他们使用完全相同的密码学功能实现非常重要。
-
加密:将明文 P 转换为密文 C 的过程称为加密。从数学上讲,它表示为 encrypt§ = C。
-
解密:将密文转换回明文的过程。数学上,它表示为 decrypt© = P。
-
密码分析:分析密码算法强度的方法。分析者试图在没有密钥的情况下恢复明文。
-
个人身份信息(PII):PII 是指那些可以单独或与其他相关数据一起用于追溯个人身份的信息。举例来说,保护性信息如社会安全号码、出生日期或母亲的娘家姓。
让我们首先了解系统的安全需求。
理解安全需求
了解一个系统的确切安全需求非常重要。理解这一点将帮助我们使用正确的密码学技术,并发现系统中的潜在漏洞。
更好地理解系统安全需求的一种方法是通过回答以下四个问题:
-
哪些个人或流程需要受到保护?
-
我们保护这些个人和流程免受哪些威胁?
-
我们应该在哪些地方进行保护?
-
我们为什么要保护这些信息?
让我们以 AWS 云中的虚拟私有云(VPC)为例。VPC 允许我们创建一个逻辑隔离的网络,在其中添加虚拟机等资源。为了理解 VPC 的安全要求,首先需要通过回答以下四个问题来识别身份:
-
有多少人计划使用这个系统?
-
需要保护的是什么样的信息?
-
我们是只保护 VPC,还是需要将消息加密并传递给系统,再与 VPC 进行通信?
-
数据的安全分类是什么?潜在的风险有哪些?为什么有人会有动机尝试攻击系统?
这些问题的大多数答案将通过执行以下三个步骤来获得:
-
识别实体。
-
建立安全目标。
-
理解数据的敏感性。
让我们逐一看看这些步骤。
第 1 步:识别实体
实体可以定义为个人、过程或信息系统中的资源。我们首先需要识别在运行时用户、资源和过程如何存在。然后,我们将量化这些识别出的实体的安全需求,既可以单独考虑,也可以作为一个整体来考虑。
一旦我们更好地理解了这些要求,就可以建立我们数字系统的安全目标。
第 2 步:建立安全目标
设计安全系统的目标是保护信息免受盗窃、破坏或攻击。通常使用加密算法来实现一个或多个安全目标:
-
认证:认证是我们确认用户、设备或系统身份的机制,确保他们确实是他们所声称的身份。
-
授权:授权是指给予用户访问特定资源或功能的权限的过程。
-
机密性:需要保护的数据被称为敏感数据。机密性是指将敏感数据限制给授权用户的概念。为了在数据传输或存储过程中保护敏感数据的机密性,需要将数据加密,使其只有授权用户才能读取。这个过程是通过使用加密算法来实现的,我们将在本章稍后讨论这些算法。
-
完整性:完整性是指在数据传输或存储过程中,确保数据未被篡改的过程。例如,TCP/IP(传输控制协议/互联网协议)使用校验和或循环冗余检查(CRC)算法来验证数据完整性。
-
不可否认性:不可否认性是指能够提供不可伪造且无可辩驳的证据,证明一条消息已被发送或接收。这些证据可以在之后用来证明数据的接收。
第 3 步:理解数据的敏感性
理解数据的机密性质非常重要。数据由监管机构如政府、机构或组织根据其泄露后的后果的严重程度进行分类。数据的分类有助于我们选择正确的加密算法。根据数据所包含信息的敏感性,数据的分类方法不止一种。让我们来看看数据分类的典型方式:
-
公开数据或未分类数据:任何可供公众消费的数据,例如公司网站或政府信息门户网站上的信息。
-
内部数据或机密数据:虽然不供公众消费,但将这些数据公开可能不会产生严重后果。例如,如果一个员工抱怨经理的电子邮件被公开,可能会让公司感到尴尬,但这可能不会带来严重后果。
-
敏感数据或机密数据:不应公开的数据显示,公开这些数据可能会对个人或组织造成严重后果。例如,泄露未来 iPhone 的细节可能会损害苹果的商业目标,并且可能给竞争对手,如三星,带来优势。
-
高度敏感数据:也称为绝密数据。如果这些信息被泄露,可能会对组织造成极大损害。高度敏感数据的例子包括专有研究、战略商业计划或内部财务数据。
绝密数据通过多层安全保护,需要特别的权限才能访问。
通常,更复杂的安全设计比简单的算法更慢。重要的是在安全性和系统性能之间找到正确的平衡。
理解密码的基本设计
设计密码是制定一个算法,将敏感数据进行混淆,以便恶意进程或未经授权的用户无法访问。尽管随着时间的推移,密码变得越来越复杂,但密码所基于的基本原理依然保持不变。
让我们从一些相对简单的密码开始,帮助我们理解设计加密算法时使用的基本原理。
介绍替代密码
替代密码已经使用了几百年,并且以不同形式出现。顾名思义,替代密码基于一个简单的概念——将明文中的字符按照预定的、组织好的方式替换为其他字符。
让我们看看这个过程的具体步骤:
-
首先,我们将每个字符映射到一个替代字符。
-
然后,我们通过使用替代映射将明文中的每个字符替换为密码文本中的另一个字符,从而对明文进行编码和转换成密码文本。
-
为了解码,我们通过使用替代映射将明文还原回来。
以下是基于替换的密码示例:
-
凯撒密码
-
旋转 13
让我们更详细地研究一下它们。
凯撒密码
凯撒密码基于替换映射。替换映射通过应用一个保密的简单公式,以确定性方式改变实际字符串。
替换映射是通过将每个字符替换为它右边第三个字符来创建的。这个映射在下图中进行了说明:
图 13.1:凯撒密码的替换映射
让我们看看如何用 Python 实现凯撒密码:
rotation = 3
P = 'CALM'; C=''
for letter in P:
C = C+ (chr(ord(letter) + rotation))
我们可以看到我们对明文 CALM
应用了凯撒密码。
让我们在加密明文后打印出密文:
print(C)
FDOP
据说凯撒密码曾被尤利乌斯·凯撒用来与他的顾问通信。
凯撒密码是一种简单的密码,且易于实现。缺点是它并不难破解,因为黑客可以简单地通过遍历字母表的所有可能位移(共 2626 种)来查看是否出现任何连贯的消息。鉴于现代计算机的处理能力,这个组合数是相对较小的。因此,它不应被用来保护高度敏感的数据。
旋转 13 (ROT13)
ROT13 是凯撒密码的一种特殊情况,其中替换映射是通过将每个字符替换为它右边第 13 个字符来创建的。下图演示了这一点:
图 14.2:ROT13 的工作原理
这意味着如果 ROT13()
是实现 ROT13 的函数,那么以下内容适用:
rotation = 13
P = 'CALM'; C=''
for letter in P:
C = C+ (chr(ord(letter) + rotation))
现在,让我们打印 C
的编码值:
print(c)
PNYZ
ROT13 实际上并不是用来实现数据保密的。它更多是用来掩盖文本,例如隐藏可能令人反感的文本。它也可以用来避免泄露谜题的答案,以及其他类似的使用场景。
替换密码的密码分析
替换密码易于实现和理解。不幸的是,它们也容易破解。对替换密码的简单密码分析表明,如果我们使用英语字母表,那么破解密码所需做的就是确定我们旋转了多少。我们可以一一尝试英语字母表中的每个字母,直到我们能够解密文本为止。这意味着需要大约 25 次尝试来恢复明文。
现在,让我们看一下另一种简单的密码——换位密码。
理解换位密码
在换位密码中,明文的字符通过换位加密。换位是一种加密方法,我们通过使用确定性逻辑将字符的位置打乱。换位密码将字符写入矩阵中的行,然后按列读取作为输出。让我们来看一个例子。
让我们来看一下 Ottawa Rocks
的明文(P)。
首先,让我们对 P 进行编码。为此,我们将使用一个 3 x 4 的矩阵,并水平书写明文:
O | t | t | a |
---|---|---|---|
w | a | R | o |
c | k | s |
read
过程将会垂直读取字符串,这将生成密文——OwctaktRsao
。密钥将是 {1,2,3,4},表示列的读取顺序。使用不同的密钥加密,比如 {2,4,3,1},将会得到不同的密文,此时为 takaotRsOwc
。
德国人在第一次世界大战中使用了一种名为 ADFGVX 的密码,这种密码结合了换位和替代密码。几年后,这个密码被乔治·潘文破解。
所以,这些是一些常见的密码。通常,密码使用密钥来加密明文。现在,让我们看看一些目前使用的密码学技术。密码学通过加密和解密过程来保护信息,这在下一节中会进一步讨论。
理解密码学技术的类型
不同类型的密码学技术使用不同的算法,并在不同的情况下应用。由于不同的情况和使用场景对安全性有不同的需求,且这些需求依据业务要求和数据分类的不同而有所差异,因此选择合适的技术对于一个设计良好的架构至关重要。
广义上,密码学技术可以分为以下三种类型:
-
哈希
-
对称
-
非对称
让我们逐一来看。
使用密码学哈希函数
密码学哈希函数是一种数学算法,可以用来创建消息的唯一指纹。它从明文生成一个输出,称为哈希。输出的大小通常是固定的,但某些特定算法的输出大小可能会有所不同。
从数学角度来看,这样表示:
C[1] = hashFunction(P[1])
这可以解释为:
-
P[1] 是表示输入数据的明文
-
C[1] 是由密码学哈希函数生成的固定长度哈希
这在以下图表中有所显示。通过单向哈希函数,变长的数据被转换为固定长度的哈希:
图 14.3:单向哈希函数
哈希函数是一种数学算法,它将任意数量的数据转换为固定大小的字节串。它在确保数据的完整性和真实性方面起着至关重要的作用。以下是定义密码学哈希函数的关键特征:
-
确定性:哈希函数是确定性的,这意味着相同的输入(或“明文”)总是会产生相同的输出(或“哈希”)。无论你对某一数据进行多少次哈希,结果将始终保持一致。
-
唯一性:理想情况下,不同的输入应始终产生唯一的哈希输出。如果两个不同的输入产生相同的哈希,这被称为碰撞。优质的哈希函数旨在尽量减少碰撞的可能性。
-
固定长度:哈希函数的输出具有固定的长度,无论输入数据的大小如何。无论你是在哈希一个字符还是整个小说,结果哈希的大小都将相同,且特定于所使用的哈希算法(例如,MD5 为 128 位,SHA-256 为 256 位)。
-
对输入变化敏感:即使在明文中进行微小的修改,也会导致结果哈希值发生显著且不可预测的变化。这一特性确保了无法推导出原始输入,也无法找到一个不同的输入产生相同的哈希,从而增强了哈希函数的安全性。其效果是,即使在大文档中改变一个字母,也会导致哈希值看起来完全不同。
-
单向函数:哈希函数是单向的,意味着计算上不可行地逆转这个过程,从哈希(C[1])生成原始明文(P[1])。这确保了即使未经授权的方获得了哈希值,他们也无法用它来确定原始数据。
如果我们遇到每个唯一的消息没有唯一的哈希值的情况,我们称之为碰撞。换句话说,碰撞是指哈希算法对两个不同的输入值产生相同的哈希值。对于安全应用程序,碰撞是一种潜在的漏洞,其概率应该非常低。也就是说,如果我们有两个文本,P1 和 P2,在碰撞的情况下,意味着hashFunction(P[1]) = hashFunction(P[2])。
无论使用何种哈希算法,碰撞是罕见的。否则,哈希就不会有用。然而,对于某些应用程序来说,碰撞是不能容忍的。在这些情况下,我们需要使用一种更复杂的哈希算法,但它生成碰撞的可能性要小得多。
实现加密哈希函数
加密哈希函数可以通过使用各种算法来实现。让我们深入了解其中的两种:
-
MD5
-
安全哈希算法 (SHA)
理解 MD5 容忍性
MD5 由 Poul-Henning Kamp 于 1994 年开发,用于替代 MD4。它生成一个 128 位的哈希值。生成一个 128 位的哈希值意味着结果哈希值由 128 个二进制数字(位)组成。
这意味着固定长度为 16 字节或 32 个十六进制字符。固定长度确保了无论原始数据的大小如何,哈希值始终为 128 位长。这个固定长度输出的目的是为了创建原始数据的“指纹”或“摘要”。MD5 是一个相对简单的算法,但它容易受到碰撞攻击。在无法容忍碰撞的应用场景中,不应使用 MD5。例如,它可以用于检查从互联网下载文件的完整性。
让我们来看一个示例。为了在 Python 中生成 MD5 哈希值,我们将从使用hashlib
模块开始,hashlib
是 Python 标准库的一部分,提供多种不同的加密哈希算法:
import hashlib
接下来,我们定义一个名为generate_md5_hash()
的工具函数,该函数以input_string
作为参数。该字符串将被该函数哈希处理:
def generate_md5_hash(input_string):
# Create a new md5 hash object
md5_hash = hashlib.md5()
# Encode the input string to bytes and hash it
md5_hash.update(input_string.encode())
# Return the hexadecimal representation of the hash
return md5_hash.hexdigest()
请注意,hashlib.md5()
会创建一个新的哈希对象。这个对象使用 MD5 算法,md5_hash.update(input_string.encode())
会用输入字符串的字节更新哈希对象。该字符串会使用默认的 UTF-8 编码转换为字节。所有数据更新到哈希对象后,我们可以调用hexdigest()
方法返回摘要的十六进制表示。这就是输入字符串的 MD5 哈希值。
在这里,我们使用generate_md5_hash()
函数来获取字符串"Hello, World!"
的 MD5 哈希值,并将结果打印到控制台:
def verify_md5_hash(input_string, correct_hash):
# Generate md5 hash for the input_string
computed_hash = generate_md5_hash(input_string)
# Compare the computed hash with the provided hash
return computed_hash == correct_hash
# Test
input_string = "Hello, World!"
hash_value = generate_md5_hash(input_string)
print(f"Generated hash: {hash_value}")
correct_hash = hash_value
print(verify_md5_hash(input_string, correct_hash))# This should return True
Generated hash: 65a8e27d8879283831b664bd8b7f0ad4
True
在verify_md5_hash
函数中,我们接受一个输入字符串和一个已知的正确 MD5 哈希值。我们使用generate_md5_hash
函数生成输入字符串的 MD5 哈希值,并将其与已知的正确哈希值进行比较。
何时使用 MD5
回顾历史,MD5 的弱点是在 1990 年代末被发现的。尽管存在一些问题,MD5 的使用仍然很普遍。它非常适合用于数据的完整性检查。请注意,MD5 消息摘要并未唯一地将哈希值与其所有者关联,因为 MD5 摘要不是一个签名哈希。MD5 用于证明自哈希计算以来,文件没有被更改。它并不用于证明文件的真实性。现在,我们来看看另一种哈希算法——SHA。
理解安全哈希算法(SHA)
SHA 是由美国国家标准与技术研究院(NIST)开发的。它被广泛用于验证数据的完整性。在其变种中,SHA-512 是一种流行的哈希函数,Python 的hashlib
库中包括了它。让我们看看如何使用 Python 创建一个使用 SHA 算法的哈希值。为此,我们首先需要导入hashlib
库:
import hashlib
然后我们将定义盐值和消息。加盐是将随机字符添加到密码中以进行哈希的做法。它通过使哈希碰撞变得更加困难来增强安全性:
salt = "qIo0foX5"
password = "myPassword"
接下来,我们将把盐值与密码结合,应用加盐过程:
salted_password = salt + password
接下来,我们将使用sha512
函数来生成盐值密码的哈希值:
sha512_hash = hashlib.sha512()
sha512_hash.update(salted_password.encode())
myHash = sha512_hash.hexdigest()
让我们打印myHash
:
myHash
2e367911b87b12f73b135b1a4af9fac193a8064d3c0a52e34b3a52a5422beed2b6276eabf9
5abe728f91ba61ef93175e5bac9a643b54967363ffab0b35133563
请注意,当我们使用 SHA 算法时,生成的哈希值为 512 字节。这个特定的大小并非随意,而是算法安全性特征的关键组成部分。更大的哈希大小对应着更多的潜在组合,从而减少了“碰撞”的概率——即两个不同的输入产生相同的哈希输出。碰撞会损害哈希算法的可靠性,而 SHA-512 的 512 字节输出大大降低了这一风险。
密码哈希函数的应用
哈希函数用于在复制文件后检查文件的完整性。为此,当文件从源复制到目标(例如从 Web 服务器下载时),相应的哈希也会一并复制。这个原始哈希值horiginal作为原文件的指纹。在复制文件后,我们从复制版本的文件生成哈希——即hcopied。如果horiginal = hcopied——即生成的哈希与原始哈希匹配——这就验证了文件没有发生变化,下载过程中没有丢失任何数据。我们可以使用任何密码哈希函数,如 MD5 或 SHA,来生成哈希值。
在 MD5 和 SHA 之间选择
MD5 和 SHA 都是哈希算法。MD5 简单且快速,但它的安全性较差。相比 MD5,SHA 更复杂,提供了更高的安全性。
现在,让我们看看对称加密。
使用对称加密
在密码学中,密钥是用来通过选择的算法对明文进行编码的一组数字。在对称加密中,我们使用相同的密钥进行加密和解密。如果用于对称加密的密钥是K,那么对于对称加密,以下公式成立:
EK§ = C
在这里,P是明文,C是密文。
对于解密,我们使用相同的密钥K,将其转换回P:
DK© = P
这个过程如图所示:
图 14.4:对称加密
现在,让我们看看如何使用 Python 进行对称加密。
编码对称加密
在本节中,我们将探讨如何使用 Python 内置的hashlib
库处理哈希函数。hashlib
是 Python 的预装库,提供了多种哈希算法。首先,我们导入hashlib
库:
import hashlib
我们将使用 SHA-256 算法来创建我们的哈希值。其他算法如 MD5、SHA-1 等也可以使用:
sha256_hash = hashlib.sha256()
让我们为消息"Ottawa is really cold"
创建一个哈希值:
message = "Ottawa is really cold".encode()
sha256_hash.update(message)
哈希的十六进制表示可以通过以下方式打印出来:
print(sha256_hash.hexdigest())
b6ee63a201c4505f1f50ff92b7fe9d9e881b57292c00a3244008b76d0e026161
让我们看看对称加密的一些优点。
对称加密的优点
以下是对称加密的优点:
-
简单:使用对称加密进行加密和解密实现起来更简单。
-
快速:对称加密比非对称加密更快。
-
安全:美国政府指定的最广泛使用的对称密钥加密系统是高级加密标准(AES)。当使用像 AES 这样的安全算法时,对称加密至少和非对称加密一样安全。
对称加密的问题
当两个用户或进程计划使用对称加密进行通信时,他们需要通过安全通道交换密钥。这就产生了以下两个问题:
-
密钥保护:如何保护对称加密密钥
-
密钥分发:如何通过安全通道从源头到目的地共享对称加密密钥
现在,让我们来看看非对称加密。
非对称加密
在 1970 年代,非对称加密被发明出来,以解决我们在上一节讨论的对称加密的一些弱点。
非对称加密的第一步是生成两个看起来完全不同但在算法上有关联的密钥。一个被选为私钥,Kpr,另一个被选为公钥,Kpu。选择哪一个密钥作为公钥或私钥是任意的。数学上,我们可以表示为:
EKpr§ = C
这里,P是明文,C是密文。
我们可以如下解密:
DKpu© = P
公钥应该是可以自由分发的,而私钥则由密钥对的拥有者保密。例如,在 AWS 中,密钥对用于确保与虚拟实例的连接安全,并管理加密资源。公钥由他人用来加密数据或验证签名,而私钥则由拥有者安全存储,用于解密数据或签署数字内容。通过遵循将私钥保密、公钥可访问的原则,AWS 用户可以确保云环境中的通信安全和数据完整性。公钥和私钥的分离是 AWS 及其他云服务中安全性和信任机制的基石。
基本原理是,如果你用其中一个密钥加密,唯一的解密方法就是使用另一个密钥。例如,如果我们用公钥加密数据,那么我们需要用另一个密钥——即私钥——来解密。
现在,让我们来看一下非对称加密的一个基本协议——安全套接字层(SSL)/传输层安全(TLS)握手协议——它负责通过非对称加密在两个节点之间建立连接。
SSL/TLS 握手算法
SSL 最初是为了为 HTTP 添加安全性而开发的。随着时间的推移,SSL 被一个更高效、更安全的协议所替代,称为 TLS。TLS 握手是 HTTP 如何创建安全通信会话的基础。TLS 握手发生在两个参与实体之间——客户端和服务器。此过程如下图所示:
图 14.5:客户端和服务器之间的安全会话
TLS 握手在参与节点之间建立安全连接。以下是此过程中涉及的步骤:
-
客户端向服务器发送一个
client hello
消息。该消息还包含以下内容:-
使用的 TLS 版本
-
客户端支持的密码套件列表
-
压缩算法
-
一个随机字节串,标识为
byte_client
-
-
服务器向客户端发送一个
server hello
消息。该消息还包含以下内容:-
由服务器从客户端提供的列表中选择的密码套件。
-
会话 ID。
-
一个随机字节串,标识为
byte_server
。 -
服务器数字证书,标识为
cert_server
,包含服务器的公钥。 -
如果服务器需要数字证书来进行客户端身份验证或请求客户端证书,客户端-服务器请求还包括以下内容:
-
可接受 CA 的区分名称
-
支持的证书类型
-
-
客户端验证
cert_server
。 -
客户端生成一个随机字节串,标识为
byte_client2
,并使用通过cert_server
提供的服务器公钥加密。 -
客户端生成一个随机字节串,并使用其私钥对其进行标识和加密。
-
服务器验证客户端证书。
-
客户端向服务器发送一个
finished
消息,该消息使用秘密密钥进行加密。 -
为了从服务器端确认这一点,服务器向客户端发送一个
finished
消息,该消息使用秘密密钥进行加密。 -
服务器和客户端现在已经建立了一个安全通道。它们现在可以交换使用共享秘密密钥对称加密的消息。整个方法论如下所示:
-
图 14.6:客户端和服务器之间的安全会话
现在,让我们讨论如何使用非对称加密来创建公钥基础设施(PKI),该基础设施旨在满足组织的一个或多个安全目标。
公钥基础设施
非对称加密用于实现公钥基础设施(PKI)。PKI 是管理组织加密密钥的最流行且可靠的方法之一。所有参与者信任一个称为证书授权机构(CA)的中央信任机构。CA 验证个人和组织的身份,然后向他们颁发数字证书(数字证书包含个人或组织的公钥及其身份),验证与该个人或组织关联的公钥确实属于该个人或组织。
它的工作原理是,证书授权机构(CA)要求用户证明其身份。基本验证称为域验证,这可能仅仅是验证域名的所有权。扩展验证(如有需要)涉及更严格的流程,需要根据用户尝试获得的数字证书类型提供身份的物理证明。如果 CA 确认用户确实是他们所声称的人,用户将通过安全通道向 CA 提供他们的公钥。
CA 利用这些信息创建包含用户身份和公钥的数字证书。该证书由 CA 进行数字签名。证书是公开的实体,因为用户可以将其证书展示给任何需要验证其身份的人,而无需通过安全通道传送,因为证书本身不包含任何敏感信息。接收证书的人不需要直接验证用户的身份。那个人只需通过验证 CA 的数字签名来验证证书是否有效,从而确认证书中包含的公钥确实属于证书上所列的个人或组织。
组织的 CA 私钥是 PKI 信任链中的最弱环节。如果冒充者掌握了微软的私钥,例如,他们可以通过冒充 Windows 更新在全球数百万台计算机上安装恶意软件。
区块链与加密学
毫无疑问,近年来区块链和加密货币引起了大量关注。区块链被认为是有史以来最安全的技术之一。关于区块链的热潮始于比特币和数字货币。数字货币最早在 1980 年被开发,但随着比特币的出现,它们开始走向主流。比特币的崛起归功于分布式系统的广泛应用。它有两个重要特点,使其成为游戏规则的改变者:
-
它在设计上是去中心化的。它利用了一种矿工网络和一种被称为区块链的分布式算法。
-
比特币基于矿工为了将区块添加到区块链而竞争的固有激励机制,通过尝试解答非常复杂的计算难题。获胜的矿工有资格要求不同比特币作为他们努力的奖励。
尽管区块链最初是为比特币开发的,但它已经找到了更广泛的用途和应用。区块链基于分布式共识算法,使用分布式账本技术(DLT)。它具有以下特点:
-
去中心化:它基于分布式而非集中式架构。没有中央机构。区块链系统中的每个节点都参与维护 DLT 的完整性。所有参与节点之间存在共识。在这种分布式架构中,交易存储在组成节点的节点上,形成 P2P 网络。
注意,“P2P”术语代表“点对点”,这意味着网络中的每个节点或“对等方”直接与其他节点通信,而无需经过中央服务器或机构。
-
链状形成:所有区块链的交易都累积在一个区块列表中。当添加多个区块时,它形成链状结构,这也是其名称“区块链”的原因。
-
不可变性:数据是安全的,复制的,并以不可变的区块存储。
-
可靠性:每笔交易都维护了一个血统或历史。使用密码学技术验证和记录每笔交易。
在底层,区块链交易使用链中每个先前区块的加密哈希。哈希函数用于创建任意数据块的单向指纹。默克尔树或哈希树用于验证存储、处理和在不同参与节点之间传输的数据。它使用 SHA-2 进行哈希。下面显示了一个特定交易的图示:
图 14.7:区块链的默克尔树
图 13.7总结了区块链的工作原理。它显示了如何将交易转换为区块,然后再转换为链。左侧显示了四笔交易,A、B、C 和 D。接下来,通过应用哈希函数创建了默克尔根。默克尔根可以视为区块头的一部分数据结构。由于交易是不可改变的,先前记录的交易不能被更改。
注意,前一个区块头的哈希值也成为区块的一部分,从而整合交易记录。这创建了链状处理结构,并是“区块链”名称的原因。
每个区块链用户通过加密技术进行身份验证和授权,从而消除了对第三方身份验证和授权的需求。数字签名也用于确保交易的安全性。交易的接收者拥有一个公钥。区块链技术消除了第三方参与交易验证,依赖加密证明来实现这一点。交易通过数字签名来确保安全。每个用户都有一个唯一的私钥,在系统中建立其数字身份。
示例:部署机器学习模型时的安全性问题
在第六章《无监督机器学习算法》中,我们讨论了跨行业数据挖掘标准过程(CRISP-DM)生命周期,该生命周期定义了训练和部署机器学习模型的不同阶段。一旦模型训练并评估完成,最终阶段就是部署。如果是关键性的机器学习模型,我们希望确保其所有安全目标都得到满足。
让我们分析在部署这样的模型时面临的常见挑战,并讨论如何利用本章中讨论的概念来解决这些挑战。我们将讨论保护训练模型免受以下三大挑战的策略:
-
中间人攻击(MITM)
-
冒充
-
数据篡改
让我们逐一分析它们。
中间人攻击
我们希望保护模型免受的一种可能攻击是中间人攻击(MITM)。中间人攻击发生在入侵者试图窃听本应私密的通信时。
让我们通过一个示例场景来逐步理解中间人攻击。
假设 Bob 和 Alice 想通过公钥基础设施(PKI)交换信息:
-
Bob 使用{Pr[Bob], Pu[Bob]},而 Alice 使用{Pr[Alice], Pu[Alice]}。Bob 创建了消息M[Bob],Alice 创建了消息M[Alice]。他们希望以安全的方式相互交换这些消息。
-
最初,他们需要交换公钥,以建立彼此之间的安全连接。这意味着 Bob 使用Pu[Alice]来加密M[Bob],然后将消息发送给 Alice。
-
假设我们有一个窃听者,通常称为 Eve X,Eve X 使用{Pr[X], Pu[X]}。攻击者能够拦截 Bob 和 Alice 之间的公钥交换,并将其替换为自己的公钥证书。
-
Bob 将M[Bob]发送给 Alice,用Pu[X]加密,而不是用Pu[Alice],错误地认为这是 Alice 的公钥证书。窃听者X拦截了这次通信。它拦截了M[Bob]消息,并使用Pr[Bob]解密它。
这个中间人攻击在以下图表中展示:
图 14.8:中间人攻击
现在,让我们看一下如何防止中间人攻击。
如何防止中间人攻击(MITM)
让我们探讨如何通过引入 CA(证书授权中心)来防止 MITM 攻击。假设这个 CA 的名称是 myTrustCA。数字证书中嵌入了它的公钥,名为PumyTrustCA
。myTrustCA 负责为组织中的所有人签发证书,包括 Alice 和 Bob。这意味着 Bob 和 Alice 的证书都由 myTrustCA 签署。在签发证书时,myTrustCA 验证他们确实是他们所声称的身份。
现在,随着这一新安排的到位,让我们重新审视 Bob 和 Alice 之间的顺序交互:
-
Bob 使用 {Pr[Bob], Pu[Bob]},Alice 使用 {Pr[Alice], Pu[Alice]}。他们的公钥都嵌入在由 myTrustCA 签署的数字证书中。Bob 创建了一个消息* M*[Bob],Alice 创建了一个消息* M*[Alice]。他们希望以安全的方式交换这些消息。
-
他们交换包含公钥的数字证书。只有当公钥嵌入在由他们信任的 CA 签署的证书中时,他们才会接受这些公钥。他们需要交换公钥以建立安全连接。这意味着 Bob 将使用Pu[Alice]来加密M[Bob],然后将消息发送给 Alice。
-
假设我们有一个窃听者 X,他使用 {Pr[X], Pu[X]}。攻击者能够拦截 Bob 和 Alice 之间的公钥交换,并用自己公钥证书Pu[X]替换它们。
-
Bob 拒绝了X的尝试,因为坏人的数字证书没有被 Bob 信任的 CA 签署。安全握手被中止,攻击尝试的时间戳和所有细节被记录,并触发了安全异常。
在部署训练好的机器学习模型时,替代 Alice 的是一个部署服务器。Bob 在建立安全通道后才部署模型,使用之前提到的步骤。
避免伪装
攻击者 X 伪装成授权用户 Bob,获得对敏感数据的访问权限,在这个案例中是已训练的模型。我们需要保护模型不受未经授权的更改。
保护训练模型免受伪装的一种方式是使用授权用户的私钥加密模型。加密后,任何人都可以通过授权用户的公钥解密模型并使用它,而该公钥可以在他们的数字证书中找到。没有人能够对模型进行未经授权的更改。
数据和模型加密
一旦模型部署完成,作为输入提供给模型的实时无标签数据也可能被篡改。训练好的模型用于推理,并为这些数据提供标签。为了防止数据被篡改,我们需要保护静态数据和传输中的数据。为了保护静态数据,可以使用对称加密对其进行编码。
为了传输数据,可以建立基于 SSL/TLS 的安全通道来提供一个安全隧道。这个安全隧道可以用来传输对称密钥,数据可以在服务器上解密,然后再提供给训练好的模型。
这是保护数据不被篡改的更高效且万无一失的方法之一。
对称加密也可以用来加密训练好的模型,在将其部署到服务器之前。这将防止在模型部署之前任何未经授权的访问。
让我们看看如何使用对称加密加密源端的训练模型,按照以下步骤操作,然后在目标端解密它并使用:
-
让我们首先使用 Iris 数据集训练一个简单的模型:
import pickle from joblib import dump, load from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.datasets import load_iris from cryptography.fernet import Fernet iris = load_iris() X = iris.data y = iris.target X_train, X_test, y_train, y_test = train_test_split(X, y) model = LogisticRegression(max_iter=1000) # Increase max_iter for convergence model.fit(X_train, y_train)
-
现在,我们定义将存储模型的文件名:
filename_source = "unencrypted_model.pkl" filename_destination = "decrypted_model.pkl" filename_sec = "encrypted_model.pkl"
-
请注意,
filename_source
是存储源端训练未加密模型的文件,filename_destination
是存储目标端训练未加密模型的文件,而filename_sec
是加密的训练模型。 -
我们将使用
pickle
将训练好的模型存储在文件中:from joblib import dump dump(model, filename_source)
-
我们定义一个名为
write_key()
的函数,它将生成一个对称密钥并将其存储在名为key.key
的文件中:def write_key(): key = Fernet.generate_key() with open("key.key", "wb") as key_file: key_file.write(key)
-
现在,让我们定义一个名为
load_key()
的函数,它可以从key.key
文件中读取存储的密钥:def load_key(): return open("key.key", "rb").read()
-
接下来,我们定义一个
encrypt()
函数,它可以加密并训练模型,并将其存储在名为filename_sec
的文件中:def encrypt(filename, key): f = Fernet(key) with open(filename,"rb") as file: file_data = file.read() encrypted_data = f.encrypt(file_data) with open(filename_sec,"wb") as file: file.write(encrypted_data)
-
我们将使用这些函数来生成一个对称密钥并将其存储在一个文件中。然后,我们将读取这个密钥并使用它将训练好的模型存储在名为
filename_sec
的文件中:write_key() key = load_key() encrypt(filename_source, key)
现在模型已经加密。它将被传输到目标端,在那里它将用于预测:
-
首先,我们定义一个名为
decrypt()
的函数,可以用来使用存储在key.key
文件中的密钥将模型从filename_sec
解密到filename_destination
:def decrypt(filename, key): f = Fernet(key) with open(filename, "rb") as file: encrypted_data = file.read() decrypted_data = f.decrypt(encrypted_data) with open(filename_destination, "wb") as file: file.write(decrypted_data)
-
现在让我们使用这个函数来解密模型并将其存储在名为
filename_destination
的文件中:decrypt(filename_sec, key)
-
现在让我们使用这个未加密的文件来加载模型并用它进行预测:
loaded model = load(filename_destination) result = loaded_model.score(X_test, y_test) print(result)
0.9473684210526315
请注意,我们使用了对称加密来编码模型。如果需要,相同的技术也可以用来加密数据。
总结
在本章中,我们学习了加密算法。我们首先确定了一个问题的安全目标。然后我们讨论了各种加密技术,还深入了解了公钥基础设施(PKI)的细节。最后,我们看了如何保护训练好的机器学习模型免受常见攻击。现在,你应该能够理解用于保护现代 IT 基础设施的安全算法的基础。
在下一章中,我们将研究设计大规模算法。我们将学习设计和选择大算法时面临的挑战和权衡。我们还将探讨使用 GPU 和集群来解决复杂问题。
在 Discord 上了解更多
要加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问并了解新版本——请扫描下面的二维码:
第十五章:大规模算法
大规模算法专门设计用于解决庞大且复杂的问题。它们的特点在于需要多个执行引擎来应对大量的数据和处理需求。此类算法的例子包括大语言模型(LLMs),如 ChatGPT,它们需要分布式模型训练来应对深度学习固有的巨大计算需求。此类复杂算法的资源密集型特性突显了强大并行处理技术的必要性,这对于训练模型至关重要。
本章将从介绍大规模算法的概念开始,然后讨论支撑它们所需的高效基础设施。此外,我们还将探索管理多资源处理的各种策略。在本章中,我们将研究由 Amdahl 定律提出的并行处理局限性,并探讨图形处理单元(GPUs)的使用。完成本章后,您将掌握设计大规模算法所需的基本策略,并打下坚实的基础。
本章涉及的主题包括:
-
大规模算法简介
-
大规模算法的高效基础设施
-
多资源处理策略
-
利用集群/云的力量运行大规模算法
让我们从简介开始。
大规模算法简介
在历史上,人类一直在解决复杂问题,从预测蝗虫群体的位置到发现最大的质数。我们的好奇心和决心推动了问题解决方法的不断创新。计算机的发明是这一历程中的一个关键时刻,使我们能够处理复杂的算法和计算。如今,计算机使我们能够以惊人的速度和精确度处理海量数据集、执行复杂计算并模拟各种场景。
然而,随着我们遇到越来越复杂的挑战,单台计算机的资源往往不足以应对。这时,大规模算法便派上了用场,它们利用多台计算机共同工作的强大计算力。大规模算法设计是计算机科学中的一个动态且广泛的领域,致力于创建和分析能够高效利用多台机器计算资源的算法。这些大规模算法支持两种类型的计算——分布式计算和并行计算。在分布式计算中,我们将一个任务拆分到多台计算机上,每台计算机处理任务的一部分,最后将结果汇总。可以把它想象成组装一辆汽车:不同的工人处理不同的部件,但共同完成整辆车的组装。相比之下,并行计算则是多台处理器同时执行多个任务,类似于流水线,每个工人同时进行不同的工作。
LLM,如 OpenAI 的 GPT-4,在这个广阔的领域中占据着至关重要的地位,因为它们代表了一种大规模算法的形式。LLM 旨在通过处理大量数据并识别语言中的模式来理解和生成类似人类的文本。然而,训练这些模型是一项重负荷的任务。它涉及处理数十亿,甚至数万亿的数据单元,称为“标记”(tokens)。这个训练过程包含需要逐步完成的步骤,比如准备数据,也有一些步骤可以同时进行,比如确定模型不同层次所需的变化。
这项工作并非夸大其词。由于其规模庞大,通常的做法是同时使用多台计算机来训练 LLM。我们称这些为“分布式系统”。这些系统使用多个 GPU——这些是计算机中负责重负载工作的部件,用于创建图像或处理数据。更准确地说,LLM 几乎总是在多台计算机协同工作来训练单一模型的情况下进行训练。
在这种背景下,我们首先要描述一个设计良好的大规模算法,它能够充分利用现代计算基础设施的潜力,如云计算、集群和 GPU/TPU。
描述大规模算法的高效基础设施
为了高效运行大规模算法,我们需要高性能的系统,因为这些系统被设计成通过增加计算资源来处理更大的工作负载,从而分配处理任务。水平扩展是实现分布式系统可扩展性的关键技术,它使系统能够通过将任务分配给多个资源来扩展其处理能力。这些资源通常是硬件(如 中央处理单元(CPU)或 GPU)或软件元素(如内存、磁盘空间或网络带宽),系统可以利用这些资源执行任务。为了让可扩展的系统高效地满足计算需求,它应具备弹性和负载均衡,以下部分将详细讨论这一点。
弹性
弹性是指基础设施根据变化的需求动态扩展资源的能力。实现这一特性的常见方法是自动扩展,这是云计算平台(如 Amazon Web Services(AWS))中常见的策略。在云计算的背景下,服务器组是由一组虚拟服务器或实例组成,这些服务器或实例经过编排协同工作以处理特定的工作负载。这些服务器组可以组织成集群,以提供高可用性、容错能力和负载均衡。组内的每台服务器可以配置特定的资源,如 CPU、内存和存储,以优化执行预定任务的性能。自动扩展允许服务器组通过修改运行中的节点(虚拟服务器)数量来适应波动的需求。在弹性系统中,可以通过增加资源(横向扩展)来应对需求增加,同样,当需求减少时,也可以释放资源(横向缩减)。这种动态调整能有效利用资源,帮助在性能需求与成本效益之间保持平衡。
AWS 提供了一项自动扩展服务,它与其他 AWS 服务(如 EC2(弹性计算云)和 ELB(弹性负载均衡))集成,能够自动调整组内服务器实例的数量。这确保了在流量高峰或系统故障期间,资源能够得到优化分配,性能保持稳定。
描述一个设计良好的大规模算法
一个设计良好的大规模算法能够处理海量信息,并且被设计为具备适应性、弹性和高效性。它具有弹性,能够适应大规模环境中不断变化的动态。
一个设计良好的大规模算法具有以下两个特点:
-
并行性:并行性是让算法一次性处理多个任务的特性。对于大型计算任务,算法应能够将任务分配到多台计算机上。因为这些计算是同时进行的,计算速度得以加快。在大规模计算的背景下,算法应能将任务拆分到多台机器上,从而通过并行处理加速计算过程。
-
容错性:由于大规模环境中组件数量庞大,系统故障的风险增大,因此构建能够承受这些故障的算法至关重要。算法应具备在不丧失大量数据或输出精度的情况下从故障中恢复的能力。
三大云计算巨头,谷歌、亚马逊和微软,提供高度弹性的基础设施。由于它们共享资源池的庞大规模,几乎没有公司能够与这三家公司基础设施的弹性匹敌。
大规模算法的性能与底层基础设施的质量密切相关。这个基础设施应提供充足的计算资源、广泛的存储能力、高速的网络连接和可靠的性能,以确保算法的最佳运行。让我们描述一个适合大规模算法的基础设施。
负载均衡
负载均衡是大规模分布式计算算法中的一项核心实践。通过均衡地管理和分配工作负载,负载均衡避免了资源过载,并保持了系统的高性能。它在确保高效运作、优化资源利用和实现高吞吐量方面发挥着重要作用,尤其是在分布式深度学习领域。
图 15.1 直观地展示了这一概念。它展示了一个用户与负载均衡器交互,负载均衡器则管理多个节点的负载。在此例中,有四个节点,节点 1、节点 2、节点 3 和 节点 4。负载均衡器不断监控所有节点的状态,将传入的用户请求在它们之间分配。将任务分配给某个特定节点的决定,取决于该节点的当前负载以及负载均衡器的算法。通过防止任何单一节点被压垮,而其他节点仍处于低负载状态,负载均衡器确保系统性能的最佳化:
图 15.1:负载均衡
在云计算的更广泛背景下,AWS 提供了一个名为**弹性负载均衡(ELB)**的功能。ELB 会自动将传入的应用流量分配到 AWS 生态系统内的多个目标,如 Amazon EC2 实例、IP 地址或 Lambda 函数。通过这种方式,ELB 可以防止资源过载,并保持应用的高可用性和性能。
ELB:结合弹性和负载均衡
ELB 代表一种先进的技术,它将弹性和负载均衡的元素结合到一个单一的解决方案中。它利用服务器集群来增强计算基础设施的响应性、效率和可扩展性。其目标是在所有可用资源之间保持均匀的工作负载分配,同时使基础设施能够根据需求波动动态调整其规模。
图 15.2 显示了一个负载均衡器管理四个服务器组。请注意,服务器组是一个节点集合,负责执行特定的计算功能。在此,服务器组指的是由节点组成的一个集合,每个节点被赋予一个独特的计算任务。
服务器组的关键特性之一是其弹性——根据情况灵活地添加或移除节点的能力:
图 15.2:智能负载均衡服务器自动扩展
负载均衡器通过实时监控工作负载指标来运行。当计算任务变得越来越复杂时,处理能力的需求也相应增加。为应对这种需求激增,系统会触发“扩容”操作,将额外的节点集成到现有的服务器组中。在此背景下,“扩容”是指增加计算能力以适应扩展的工作负载。相反,当需求下降时,基础设施可以启动“缩容”操作,移除一些节点。通过这种跨服务器组的动态节点重新分配,确保了资源的最优利用比率。通过将资源分配调整以匹配当前的工作负载,系统可以避免资源过度配置或不足配置。这种动态资源管理策略提高了运营效率和成本效益,同时保持了高水平的性能。
策略化多资源处理
在多资源处理策略的早期,大规模算法是在被称为超级计算机的强大机器上执行的。这些单体机器具有共享内存空间,能够让不同的处理器之间进行快速通信,并通过相同的内存访问共享变量。随着运行大规模算法的需求增长,超级计算机转变为分布式共享内存(DSM)系统,在这种系统中,每个处理节点拥有一段物理内存。随后,集群应运而生,构成了松散连接的系统,这些系统依赖于处理节点之间的消息传递。
有效运行大规模算法需要多个执行引擎并行工作以解决复杂的挑战。可以利用三种主要策略来实现这一点:
-
向内看:通过利用计算机上的现有资源,使用 GPU 上成百上千的核心来执行大规模算法。例如,一位数据科学家希望训练一个复杂的深度学习模型时,可以利用 GPU 的计算能力来增强计算能力。
-
向外看:实施分布式计算以访问补充的计算资源,这些资源可以协同解决大规模问题。例子包括集群计算和云计算,它们通过利用分布式资源,使得运行复杂且资源需求高的算法成为可能。
-
混合策略:将分布式计算与每个节点上的 GPU 加速结合,以加快算法执行。一些处理大量数据并进行复杂模拟的科研组织可能会采用这种方法。如图 15.3所示,计算负载被分配到多个节点(节点 1、节点 2 和 节点 3)上,每个节点都配备有自己的 GPU。该图有效展示了混合策略,展示了如何利用分布式计算和 GPU 加速的优势,推动每个节点中的模拟和计算的加速:
图 15.3:多资源处理的混合策略
在我们探索并行计算在运行大规模算法中的潜力时,理解支配其效率的理论限制同样重要。
在接下来的部分,我们将深入探讨并行计算的基本约束,揭示影响其性能的因素,以及它可以被优化的程度。
理解并行计算的理论限制
需要注意的是,并行算法并不是万能的。即使是设计最好的并行架构,也可能无法达到我们预期的性能。并行计算的复杂性,如通信开销和同步问题,使得实现最佳效率变得具有挑战性。为了帮助我们应对这些复杂性,并更好地理解并行算法的潜在收益和局限性,提出了阿姆达尔定律。
阿姆达尔定律
基因·阿姆达尔(Gene Amdahl)是最早研究并行处理的人之一,他在 20 世纪 60 年代提出了阿姆达尔定律,该定律至今仍然适用,并成为理解设计并行计算解决方案时涉及的各种权衡的基础。阿姆达尔定律提供了一个理论极限,即在给定可并行化部分的算法下,使用并行化版本的算法能够实现的最大执行时间改进。
它基于这样一个概念:在任何计算过程中,并非所有过程都可以并行执行。总会有一部分过程是顺序执行的,无法并行化。
推导阿姆达尔定律
考虑一个可以分为并行部分(f)和串行部分(1 - f)的算法或任务。并行部分指的是可以在多个资源或处理器上同时执行的任务部分。这些任务相互独立,可以并行执行,因此称为“并行可分”。另一方面,串行部分是任务中无法拆分的部分,必须按顺序依次执行,因此称为“串行”。
令 Tp(1) 表示在单个处理器上处理该任务所需的时间。可以表达为:
T[p](1) = N(1 - f)τ[p] *+ N(f)τ[p] *= Nτ[p]
在这些方程中,N 和 τ[p] 表示:
-
N:算法或任务必须执行的任务总数或迭代次数,在单个处理器和并行处理器上是一致的。
-
τ[p]:处理器完成单个工作单元、任务或迭代所需的时间,不论使用多少个处理器,这个时间保持不变。
前面的方程计算了在单个处理器上处理所有任务所需的总时间。现在,让我们看看任务在N个并行处理器上执行的情况。
执行所需的时间可以表示为 T[p](N)。在下面的图示中,X 轴表示处理器数量,即执行我们程序所使用的计算单元或核心的数量。当我们沿着 X 轴向右移动时,使用的处理器数量增加。Y 轴表示加速比,这是衡量使用多个处理器时程序运行速度相比仅使用一个处理器时的提升程度。当我们沿着 Y 轴向上移动时,程序的速度成比例地增加,导致任务执行效率更高。
图 15.4 中的图表和阿姆达尔定律向我们展示了,更多的处理器可以提高性能,但由于代码中的串行部分,性能提升是有限的。这个原理是并行计算中收益递减的经典例子。
*N = N(1 - f)*τ[p] *+ (f)*τ[p]
在这里,RHS(右侧)的第一项表示处理任务的串行部分所需的时间,而第二项表示处理并行部分所需的时间。
在这种情况下,加速比是由于任务的并行部分分布到N个处理器上。阿姆达尔定律定义了使用N个处理器时实现的加速比S(N),公式为:
对于显著的加速,必须满足以下条件:
1 - f << f / N (4.4)
这个不等式表示并行部分(f)必须非常接近于 1,尤其是在N很大的时候。
现在,让我们来看一个典型的图表,解释阿姆达尔定律:
图 15.4:并行处理中的收益递减:可视化阿姆达尔定律
在 图 15.4 中,X 轴表示处理器的数量 (N),对应用于执行程序的计算单元或核心。沿着 X 轴向右移动,N 增加。Y 轴表示加速比 (S),这是一个衡量程序在多个处理器上执行时相较于只使用一个处理器执行的时间 T[p] 改进的指标。沿 Y 轴向上移动表示程序执行速度的提升。
该图展示了四条线,每条线代表不同并行化比例 (f) 下获得的加速比 S,分别为 50%、75%、90% 和 95%:
-
50% 并行 (f = 0.5): 这一行展示了最小的加速比 S。尽管添加了更多的处理器 (N),程序的一半仍然按顺序执行,限制了加速比的最大值为 2。
-
75% 并行 (f = 0.75): 与 50% 的情况相比,加速比 S 更高。然而,程序的 25% 仍然是顺序执行,这限制了整体加速比。
-
90% 并行 (f = 0.9): 在这种情况下,观察到显著的加速比 S。然而,程序的 10% 顺序部分对加速比施加了限制。
-
95% 并行 (f = 0.95): 这一行展示了最高的加速比 S。然而,顺序执行的 5% 仍然对加速比施加了上限。
该图与阿姆达尔定律结合,强调了虽然增加处理器数量 (N) 可以提高性能,但由于代码中的顺序部分 (1 - f),仍然存在一个固有的限制。这个原则是并行计算中收益递减的经典示例。
阿姆达尔定律提供了有关多处理器系统中可以实现的性能增益的宝贵见解,并强调并行化部分 (f) 在决定系统整体加速比中的重要性。在讨论了并行计算的理论限制之后,重要的是要介绍并探讨另一种强大且广泛使用的并行处理技术:GPU 及其相关的编程框架 CUDA。
CUDA: 释放 GPU 架构在并行计算中的潜力
GPU 最初是为图形处理设计的,但后来发展演变,展现出与 CPU 不同的独特特点,形成了完全不同的计算范式。
与处理器核心数量有限的 CPU 不同,GPU 由数千个核心组成。然而,需要注意的是,这些核心单独来看并不像 CPU 核心那样强大,但 GPU 在并行执行大量相对简单的计算任务时非常高效。
由于 GPU 最初是为图形处理设计的,GPU 架构非常适合图形处理,其中多个操作可以独立执行。例如,渲染图像涉及对每个像素的颜色和亮度进行计算。这些计算彼此基本独立,因此可以同时进行,充分利用 GPU 的多核架构。
表格底部
这种设计选择使得 GPU 在它们所设计的任务上变得极其高效,比如渲染图形和处理大规模数据集。下面是图 15.5所示的 GPU 架构:
图 15.5:GPU 架构
这种独特的架构不仅有利于图形处理,还对其他类型的计算问题具有显著的优势。任何可以分解成较小、独立任务的问题,都可以利用这种架构进行更快速的处理。这包括像科学计算、机器学习,甚至是加密货币挖矿等领域,这些领域的数据集庞大且计算复杂。
在 GPU 成为主流之后,数据科学家开始探索它们在高效执行并行操作方面的潜力。由于典型的 GPU 拥有成千上万的算术逻辑单元(ALUs),它有潜力产生成千上万的并发进程。需要注意的是,ALU 是核心的主力部件,负责执行大部分实际的计算。大量的 ALU 使得 GPU 非常适合执行需要对许多数据点同时进行相同操作的任务,例如数据科学和机器学习中常见的向量和矩阵运算。因此,能够执行并行计算的算法最适合在 GPU 上运行。例如,在 GPU 上进行视频中的对象搜索,速度至少比 CPU 快 20 倍。第五章中讨论的图算法,已知在 GPU 上的运行速度远快于 CPU。
2007 年,NVIDIA 开发了一个名为计算统一设备架构(CUDA)的开源框架,以便数据科学家能够利用 GPU 的强大计算能力来处理他们的算法。CUDA 将 CPU 和 GPU 分别抽象为主机和设备。
主机指的是 CPU 和主内存,负责执行主程序并将数据并行计算任务卸载到 GPU 上。
设备指的是 GPU 及其内存(VRAM),负责执行执行数据并行计算的内核。
在典型的 CUDA 程序中,主机在设备上分配内存,传输输入数据并调用内核。设备执行计算,结果存储回其内存。主机随后检索结果。通过这种劳动分工,充分发挥了每个组件的优势,CPU 处理复杂逻辑,GPU 负责大规模数据并行计算。
CUDA 在 NVIDIA GPU 上运行,并需要操作系统内核的支持,最初从 Linux 开始,后来扩展到 Windows。CUDA 驱动 API 连接了编程语言 API 和 CUDA 驱动,支持 C、C++和 Python。
LLM 中的并行处理:一个关于阿姆达尔定律和收益递减的案例研究
像 ChatGPT 这样的 LLM 是复杂的系统,能够根据给定的初始提示生成与人类写作非常相似的文本。这项任务涉及一系列复杂的操作,可以大致分为顺序任务和可并行化任务。
顺序任务是指那些必须按照特定顺序依次进行的任务。这些任务可能包括像分词这样的预处理步骤,其中输入文本被拆分成更小的部分,通常是单词或短语,模型能够理解。它还可能包括像解码这样的后处理任务,在这些任务中,模型的输出(通常是以词元概率的形式呈现)被转化回人类可读的文本。这些顺序任务对模型的功能至关重要,但由于其本质,它们无法拆分并同时执行。
另一方面,能够并行化的任务是那些可以拆分并同时运行的任务。一个典型的例子是模型神经网络中的前向传播阶段。在这里,网络中每一层的计算可以并行执行。这个操作构成了模型大部分的计算时间,正是在这里可以发挥并行处理的强大优势。
现在,假设我们使用的是一款拥有 1000 个核心的 GPU。在语言模型的背景下,任务中可以并行化的部分可能涉及前向传播阶段,在这个阶段中,神经网络每一层的计算可以并行执行。我们假设这占总计算时间的 95%。其余 5%的任务可能涉及诸如分词和解码之类的操作,这些是顺序进行的,无法并行化。
将阿姆达尔定律应用于此场景给出了以下结果:
加速 = 1 / ((1 - 0.95) + 0.95/1000) = 1 / (0.05 + 0.00095) = 19.61
在理想情况下,这表明我们的语言处理任务在 1000 核心的 GPU 上比在单核 CPU 上要快约 19.61 倍。
为了进一步说明并行计算的收益递减,我们来调整核心数量,分别为 2、50 和 100:
-
对于 2 个核心:加速 = 1 / ((1 - 0.95) + 0.95/2) = 1.67
-
对于 50 个核心:加速 = 1 / ((1 - 0.95) + 0.95/50) = 14.71
-
对于 100 个核心:加速 = 1 / ((1 - 0.95) + 0.95/100) = 16.81
从我们的计算结果来看,向并行计算环境中添加更多核心并不会导致速度的等效提升。这是并行计算中收益递减概念的一个典型例子。即使将核心数量从 2 增加到 4,或者从 2 增加到 100 时增加 50 倍,速度提升也不会翻倍或增加 50 倍。相反,速度提升会根据阿姆达尔定律达到一个理论限制。
这种收益递减的主要原因是任务中存在无法并行化的部分。在我们的例子中,像标记化和解码这样的操作构成了这一顺序部分,占总计算时间的 5%。无论我们向系统添加多少核心,或者我们能多高效地执行并行化部分,这一顺序部分都会对可实现的加速造成上限。它将始终存在,要求占用其计算时间份额。
阿姆达尔定律优雅地捕捉了并行计算的这一特性。它指出,使用并行处理的最大潜在加速由任务中无法并行化的部分决定。该定律提醒算法设计师和系统架构师,尽管并行性可以显著加速计算,但它并不是一个可以无限利用来提高速度的资源。它强调了识别和优化算法中顺序部分的重要性,以便最大化并行处理的优势。
在大规模语言模型(LLM)的背景下,这一理解尤为重要,因为计算的庞大规模使得高效的资源利用成为一个关键问题。它强调了需要一种平衡的方法,将并行计算策略与优化任务中顺序部分的性能的努力结合起来。
重新思考数据局部性
在传统的并行和分布式处理中,数据局部性原理在决定最优资源分配方面至关重要。它从根本上表明,在分布式基础设施中应尽量避免数据移动。只要可能,数据应在其所在节点上本地处理,而不是移动数据;否则,它将减少并行化和水平扩展的好处,其中水平扩展是通过增加更多机器或节点来分配工作负载,从而提高系统容量,使其能够处理更高的流量或数据量。
随着网络带宽多年来的提升,数据局部性带来的限制变得不再那么显著。更高的数据传输速度使得分布式计算环境中节点之间的通信更加高效,从而减少了对数据局部性进行性能优化的依赖。网络带宽可以通过网络分段带宽来量化,分段带宽是指网络中两部分之间的带宽。这在资源物理分布的分布式计算中尤为重要。如果我们在分布式网络中的两组资源之间画一条线,分段带宽就是指一侧的服务器与另一侧的服务器之间的通信速度,如 图 15.6 所示。为了使分布式计算高效运行,这是需要考虑的最重要参数。如果没有足够的网络分段带宽,分布式计算中多个执行引擎所带来的好处将被缓慢的通信链路所掩盖。
图 15.6:分段带宽
高分段带宽使我们能够在数据所在的地方进行处理,而无需复制数据。如今,主要的云计算提供商提供卓越的分段带宽。例如,在 Google 数据中心,分段带宽高达每秒 1 petabyte。其他主要云供应商也提供类似的带宽。相比之下,典型的企业网络可能只提供每秒 1 到 10 gigabytes 的分段带宽。
这种速度上的巨大差异展示了现代云基础设施的卓越能力,使其非常适合大规模数据处理任务。
增加的宠物比特分段带宽为高效存储和处理大数据开辟了新的选项和设计模式。这些新选项包括由于网络容量的增加而变得可行的替代方法和设计模式,使得数据处理变得更快速、更高效。
利用 Apache Spark 进行集群计算的优势
Apache Spark 是一个广泛使用的平台,用于管理和利用集群计算。在这个背景下,“集群计算”是指将多台机器组合在一起,使它们作为一个单一的系统共同工作以解决问题。Spark 不仅仅实现了这一点,它还创建并控制这些集群以实现高速数据处理。
在 Apache Spark 中,数据会转换成被称为 弹性分布式数据集 (RDDs) 的形式。这些实际上是 Apache Spark 数据抽象的核心。
RDD 是不可变的,意味着一旦创建后,它们无法被更改,是可以并行处理的元素集合。换句话说,这些数据集的不同部分可以同时进行处理,从而加速数据处理过程。
当我们说“容错”时,指的是 RDD 具有从执行过程中的潜在失败或错误中恢复的能力。这使得它们在大数据处理任务中具有强大的可靠性和稳健性。RDD 被划分为多个较小的块,称为“分区”,然后分布在集群中的多个节点或独立计算机上。这些分区的大小可以变化,主要由任务的性质和 Spark 应用的配置决定。
Spark 的分布式计算框架使得任务可以分布在多个节点上,从而显著提高处理速度和效率。
Spark 架构由多个主要组件组成,包括驱动程序、执行器、工作节点和集群管理器。
-
驱动程序:驱动程序是 Spark 应用中的关键组件,功能类似于操作的控制中心。它存在于一个独立的进程中,通常位于称为驱动机器的机器上。驱动程序的角色类似于管弦乐队的指挥;它运行主 Spark 程序,并监督其中的众多任务。
驱动程序的主要任务之一是处理和运行 SparkSession。SparkSession 对于 Spark 应用至关重要,因为它封装了 SparkContext。SparkContext 就像 Spark 应用的中枢神经系统——它是应用与 Spark 计算生态系统交互的门户。
为了简化理解,可以将 Spark 应用比作一栋办公大楼。驱动程序就像大楼管理员,负责整体运作和维护。在这栋大楼中,SparkSession 代表一个独立的办公室,而 SparkContext 是通往该办公室的主要入口。关键是,这些组件——驱动程序、SparkSession 和 SparkContext——协同工作,以协调任务并管理 Spark 应用中的资源。SparkContext 包含应用启动时预加载的基本功能和上下文信息。此外,它还携带关于集群的重要细节,如配置和状态,这对于应用的运行和任务的有效执行至关重要。
-
集群管理器:驱动程序与集群管理器无缝互动。集群管理器是一个外部服务,负责提供和管理集群中的资源,如计算能力和内存。驱动程序和集群管理器密切合作,以识别集群中可用的资源,进行有效分配,并在 Spark 应用的生命周期内管理其使用。
-
执行器:执行器是指专门为在集群中某个节点上运行的单个 Spark 应用程序而启动的计算进程。每个执行器进程都运行在工作节点上,实际上充当着 Spark 应用程序背后的计算“肌肉”。
-
以这种方式共享内存和全局参数可以显著提高任务执行的速度和效率,使得 Spark 成为一个高性能的大数据处理框架。
-
工作节点:工作节点,顾名思义,负责在分布式 Spark 系统中执行任务的实际操作。
每个工作节点能够托管多个执行器,这些执行器又可以为多个 Spark 应用程序提供服务:
图 15.7:Spark 的分布式架构
Apache Spark 如何支持大规模算法处理
Apache Spark 已成为处理和分析大数据的领先平台,这得益于其强大的分布式计算能力、容错特性和易用性。在本节中,我们将探讨 Apache Spark 如何支持大规模算法处理,使其成为复杂、资源密集型任务的理想选择。
分布式计算
Apache Spark 架构的核心概念是数据分区,这使得数据可以在集群中的多个节点之间分配。这个特性使得并行处理和高效的资源利用成为可能,而这两者对于运行大规模算法至关重要。Spark 的架构由一个驱动程序和分布在工作节点上的多个执行器进程组成。驱动程序负责管理并分配任务到各个执行器,而每个执行器则在多个线程中并行运行任务,从而实现高吞吐量。
内存处理
Spark 的一大亮点是其内存处理能力。与传统的基于磁盘的系统不同,Spark 可以将中间数据缓存到内存中,显著加速需要多次遍历数据的迭代算法。
- 这种内存处理能力对于大规模算法尤为有利,因为它最小化了磁盘 I/O 的时间,从而加快了计算速度,并更高效地利用资源。
在云计算中使用大规模算法
数据的快速增长以及机器学习模型日益复杂,使得分布式模型训练成为现代深度学习管道中不可或缺的一部分。大规模算法需要大量的计算资源,并且需要高效的并行处理来优化其训练时间。云计算提供了一系列服务和工具,支持分布式模型训练,使你能够充分发挥资源密集型大规模算法的潜力。
使用云计算进行分布式模型训练的一些关键优势包括:
-
可扩展性:云计算提供几乎无限的资源,使你能够根据大规模算法的需求扩展模型训练工作负载。
-
灵活性:云计算支持多种机器学习框架和库,使你能够选择最适合你特定需求的工具。
-
性价比:使用云计算,你可以通过选择合适的实例类型和利用抢占实例来优化培训成本,从而降低开支。
示例
随着我们深入研究机器学习模型,尤其是处理自然语言处理(NLP)任务的模型时,我们发现对计算资源的需求越来越大。例如,像 GPT-3 这样的变压器模型,用于大规模语言建模任务,可能拥有数十亿个参数,需求巨大的处理能力和内存。在庞大的数据集上训练这样的模型,如包含数十亿网页的 Common Crawl,进一步加剧了这些需求。
云计算在这里成为了一种强有力的解决方案。它提供了分布式模型训练的服务和工具,使我们能够访问几乎无限的资源池,扩展工作负载,并选择最适合的机器学习框架。更重要的是,云计算通过提供灵活的实例类型和抢占实例来促进成本优化——本质上是在竞标空闲的计算能力。通过将这些资源密集型任务委托给云计算,我们可以更加专注于创新工作,加快训练过程,并开发更强大的模型。
总结
本章我们探讨了大规模并行算法设计的概念和原理。我们分析了并行计算的重要作用,特别是它在将计算任务有效地分配到多个处理单元方面的能力。详细研究了 GPU 的非凡能力,展示了它们在并发执行大量线程时的实用性。此外,我们还讨论了分布式计算平台,特别是 Apache Spark 和云计算环境。它们在促进大规模算法的开发和部署方面的重要性被强调,为高性能计算提供了强大、可扩展且具成本效益的基础设施。
在 Discord 上了解更多
要加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问,并了解新版本——请扫描下面的二维码:
第十六章:实际考虑因素
本书中介绍了许多可用于解决现实世界问题的算法。在这一章中,我们将探讨本书中算法的实用性。我们的重点将放在它们的现实世界适用性、潜在挑战和整体主题上,包括效用和伦理影响。
本章的组织结构如下:我们将从引言开始。接着,我们将介绍算法可解释性的问题,即算法的内部机制能够以可理解的术语进行解释的程度。然后,我们将讨论使用算法的伦理问题以及实施算法时可能产生的偏见。接下来,我们将讨论处理 NP-hard 问题的技术。最后,我们将研究在选择算法之前应考虑的因素。
到本章结束时,你将了解在使用算法解决现实世界问题时,必须牢记的实际考虑因素。
本章将涵盖以下主题:
-
引入实际考虑因素
-
算法的可解释性
-
理解伦理与算法
-
减少模型中的偏差
-
何时使用算法
让我们从一些算法解决方案面临的挑战开始。
算法解决方案面临的挑战
除了设计、开发和测试算法外,在许多情况下,考虑开始依赖机器来解决现实世界问题时的某些实际因素也是非常重要的。对于某些算法,我们可能需要考虑如何可靠地加入新信息,而这些信息预计在我们部署算法之后仍会持续变化。例如,全球供应链的突发中断可能会使我们用于训练模型以预测产品利润率的某些假设失效。我们需要仔细考虑是否加入这些新信息会以某种方式改变我们经过充分测试的算法的质量。如果会,那么我们的设计如何处理这种变化?
预见意外情况
许多使用算法开发的现实世界问题解决方案都是基于某些假设的。这些假设在模型部署后可能会意外地发生变化。一些算法使用的假设可能会受到全球地缘政治情况变化的影响。例如,考虑一个训练好的模型,它预测一家在全球各地都有办事处的国际公司的财务利润。像战争或突如其来的致命病毒蔓延这样的意外破坏事件,可能会根本性地改变这个模型的假设和预测的质量。对于这种应用场景,建议是“预见意外”并为意外情况做好应对策略。对于某些数据驱动的模型,意外可能来自于解决方案部署后法规政策的变化。
当我们使用算法来解决现实世界的问题时,某种程度上,我们是在依赖机器来进行问题解决。即使是最复杂的算法也都建立在简化和假设的基础上,无法应对突发情况。我们仍然离将关键决策完全交给我们自己设计的算法的目标相距甚远。
例如,谷歌的推荐引擎算法由于隐私问题,最近面临欧盟的监管限制。这些算法可能是该领域最先进的技术之一,但如果被禁止,这些算法可能会变得毫无用处,因为它们将无法解决它们原本应解决的问题。
然而,事实是,遗憾的是,算法的实际考虑往往是事后的思考,通常在初期设计阶段并未得到充分的考虑。
对于许多用例来说,一旦算法被部署,短期内的解决方案带来的兴奋感过去后,使用算法的实际问题和影响将在时间的推移中被发现,并最终决定项目的成败。
让我们来看看一个实际的例子,分析一下没有关注实际考虑导致一个全球顶尖 IT 公司设计的高调项目失败的原因。
Tay 的失败,推特 AI 机器人
让我们来看一个经典的例子——Tay,这是微软在 2016 年创建的首个 AI 推特机器人。通过使用 AI 算法,Tay 被训练成一个能够根据特定话题回复推文的自动化推特机器人。为了实现这一目标,它具备根据对话上下文构建简单消息的能力,利用现有的词汇。一旦部署,它本应从实时的在线对话中不断学习,并通过增强其词汇库,吸收在重要对话中频繁使用的单词。然而,Tay 在网络空间中生活了几天后,开始学习新词汇。除了学习了一些新词,遗憾的是,Tay 还从正在进行的推文中学到了一些种族主义和粗俗的词语。它很快开始使用新学到的词语发布自己的推文。尽管其中绝大多数推文并无恶意,但其中少数推文足够冒犯人们,迅速引发了警报。尽管它展现出了智能,并且迅速学会了如何根据实时事件创作定制的推文,但与此同时,它也严重冒犯了人们。微软将其下线并尝试进行重新调整,但未能成功。最终,微软不得不终止了这个项目。这是一个雄心勃勃的项目的悲惨结局。
请注意,尽管微软在其内置的人工智能方面取得了令人印象深刻的成绩,但公司忽视了部署自学习 Twitter 机器人的实际影响。尽管自然语言处理和机器学习算法可能是业内最先进的,但由于显而易见的缺陷,该项目几乎是毫无用处的。如今,Tay 已经成为一个由于忽视让算法实时学习的实际后果而导致失败的教科书式例子。Tay 的失败所带来的教训无疑影响了后来的 AI 项目。数据科学家们也开始更加关注算法的透明性。
为了更深入了解,以下是关于 Tay 的全面研究:spectrum.ieee.org/in-2016-microsofts-racist-chatbot-revealed-the-dangers-of -online-conversation
。
这引出了下一个话题,探讨了为何需要让算法透明以及如何实现这一点。
算法的可解释性
首先,让我们区分黑盒算法和白盒算法:
-
黑盒算法是指其逻辑无法被人类解释的算法,原因可能是其复杂性或其逻辑以复杂的方式呈现。
-
白盒算法是指其逻辑对人类可见且可以理解的算法。
在机器学习的背景下,可解释性指的是我们理解和表达算法特定输出背后原因的能力。从本质上讲,它衡量的是一个算法的内部工作原理和决策路径对于人类认知的可理解程度。
许多算法,特别是在机器学习领域,由于其不透明性,通常被称为“黑盒”。例如,考虑我们在第八章中讨论的神经网络算法,神经网络算法。这些算法是许多深度学习应用的基础,是典型的黑盒模型。它们的复杂性和多层结构使得它们本质上不直观,导致其内部决策过程对人类理解来说是谜一样的。
然而,必须注意,“黑盒”和“白盒”这两个术语是明确的分类,分别表示完全不透明和完全透明。它们不是一个渐变或光谱,一个算法不可能是“有点黑”或“有点白”。当前的研究正致力于让这些黑盒算法(如神经网络)变得更加透明和可解释。然而,由于其复杂的架构,它们仍然主要属于黑盒类别。
如果算法用于关键决策中,理解算法生成结果的原因可能是非常重要的。避免使用黑箱算法,转而使用白箱算法,也能更好地洞察模型的内部运作。第七章中讨论的决策树算法,传统监督学习算法,就是这种白箱算法的一个例子。例如,一个可解释的算法将指导医生了解哪些特征实际被用来将病人分类为生病或未生病。如果医生对结果有任何疑问,他们可以回去重新检查这些特征的准确性。
机器学习算法和可解释性
在机器学习领域,可解释性的概念至关重要。那么,到底我们指的是什么是可解释性呢?从本质上讲,可解释性指的是我们能够理解和解释机器学习模型决策的清晰度。
这意味着揭开模型预测背后的面纱,理解其背后的“原因”。
在利用机器学习,特别是在决策场景中,个人往往需要信任模型的输出。如果模型的过程和决策是透明的并且可以解释的,那么这种信任可以得到显著增强。为了说明可解释性的重要性,让我们考虑一个现实场景。
假设我们想利用机器学习根据房屋的特征预测波士顿地区的房价。假设当地的城市规定允许我们使用机器学习算法,但前提是每当需要时,我们必须提供详细的信息来证明任何预测的合理性。这些信息是审计的需要,以确保住房市场的某些细分不会被人为操控。让我们的训练模型具有可解释性将提供这些额外的信息。
让我们深入了解实现已训练模型可解释性的不同选项。
解释性策略展示
对于机器学习,提供算法可解释性的基本策略有两种:
-
全球可解释性策略:这是指提供整个模型制定过程的细节。例如,我们可以考虑一个用于批准或拒绝个人贷款的机器学习模型的案例。可以使用全球可解释性策略来量化该模型决策的透明度。全球可解释性策略并不是针对单个决策的透明度,而是关于整体趋势的透明度。假设媒体对该模型中的性别偏见进行猜测,全球可解释性策略将提供必要的信息来验证或否定这一猜测。
-
局部可解释策略:这是为了提供由我们的训练模型做出的单一预测的依据。其目的是为每个单独的决策提供透明度。例如,考虑我们之前的例子,预测波士顿地区的房价。如果一位房主质疑为什么他们的房子被模型评估为特定价格,那么局部可解释策略将提供有关该具体估价的详细推理,明确指出各种因素及其权重,帮助理解模型如何做出该预测。
对于全局可解释性,我们有一些技术,如使用概念激活向量进行测试(TCAV),它用于为图像分类模型提供可解释性。TCAV 通过计算方向导数来量化用户定义的概念与图像分类之间的关系程度。例如,它会量化分类一个人是男性的预测对面部毛发存在的敏感度。还有其他全局可解释性策略,例如部分依赖图和计算排列重要性,它们有助于解释我们训练模型中的公式。全局和局部可解释性策略可以是模型特定的,也可以是模型无关的。模型特定策略适用于某些类型的模型,而模型无关策略可以应用于各种不同的模型。
以下图示总结了可用于机器学习可解释性的不同策略:
图 16.1:机器学习可解释性方法
现在,让我们来看看如何使用这些策略之一实现可解释性。
实现可解释性
局部可解释模型无关解释(LIME)是一种模型无关的方法,可以解释经过训练的模型所做出的单个预测。由于其模型无关的特性,它可以解释大多数类型训练过的机器学习模型的预测。
LIME 通过对每个实例的输入进行小幅度变化来解释决策。它可以收集这些变化对该实例的局部决策边界的影响。它会遍历循环,提供每个变量的详细信息。通过查看输出,我们可以看到哪个变量对该实例的影响最大。
让我们看看如何使用 LIME 使我们的房价预测模型的个别预测变得可解释:
-
如果你之前从未使用过
LIME
,你需要使用pip
安装该软件包:!pip install lime
-
然后,让我们导入我们需要的 Python 包:
import sklearn import requests import pickle import numpy as np from lime.lime_tabular import LimeTabularExplainer as ex
-
我们将训练一个模型,该模型可以预测某个特定城市的房价。为此,我们首先将导入存储在
housing.pkl
文件中的数据集。然后,我们将探索它所包含的特征:# Define the URL url = "https://storage.googleapis.com/neurals/data/data/housing.pkl" # Fetch the data from the URL response = requests.get(url) data = response.content # Load the data using pickle housing = pickle.loads(data) housing['feature_names']
array(['crime_per_capita', 'zoning_prop', 'industrial_prop', 'nitrogen oxide', 'number_of_rooms', 'old_home_prop', 'distance_from_city_center', 'high_way_access', 'property_tax_rate', 'pupil_teacher_ratio', 'low_income_prop', 'lower_status_prop', 'median_price_in_area'], dtype='<U25')
基于这些特征,我们需要预测一套房屋的价格。
-
现在,让我们训练模型。我们将使用随机森林回归器来训练模型。首先,我们将数据分为测试集和训练集,然后使用它来训练模型:
from sklearn.ensemble import RandomForestRegressor X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(housing.data, housing.target) regressor = RandomForestRegressor() regressor.fit(X_train, y_train)
RandomForestRegressor()
-
接下来,让我们识别类别列:
cat_col = [i for i, col in enumerate(housing.data.T) if np.unique(col).size < 10]
-
现在,让我们使用所需的配置参数来实例化 LIME 解释器。请注意,我们指定我们的标签是
'price'
,表示波士顿的房价:myexplainer = ex(X_train, feature_names=housing.feature_names, class_names=['price'], categorical_features=cat_col, mode='regression')
-
让我们尝试深入了解预测的细节。为此,首先让我们从
matplotlib
导入pyplot
作为绘图工具:exp = myexplainer.explain_instance(X_test[25], regressor.predict, num_features=10) exp.as_pyplot_figure() from matplotlib import pyplot as plt plt.tight_layout()
图 16.2:房价预测的特征逐项解释
-
由于 LIME 解释器是针对个别预测工作的,我们需要选择要分析的预测。我们已要求解释器对索引为
1
和35
的预测提供理由:for i in [1, 35]: exp = myexplainer.explain_instance(X_test[i], regressor.predict, num_features=10) exp.as_pyplot_figure() plt.tight_layout()
图 16.3:突出显示关键特征:解读测试实例 1 和 35 的预测
让我们尝试分析 LIME 之前的解释,它告诉我们以下内容:
-
在个别预测中使用的特征列表:它们在前面的截图中标示在y轴上。
-
特征在决定中的相对重要性:条形线越大,重要性越大。数字的值显示在x轴上。
-
每个输入特征对标签的正负影响:红色条表示负面影响,绿色条表示特定特征的正面影响。
理解伦理学与算法
算法伦理学,也称为计算伦理学,探讨算法的道德维度。这个关键领域的目标是确保基于这些算法运行的机器遵守伦理标准。算法的开发和部署可能无意中导致不道德的结果或偏见。设计算法时,预测其所有道德后果是一项挑战。当我们在这个背景下讨论大规模算法时,指的是那些处理大量数据的算法。然而,当多个用户或设计者共同参与算法的设计时,复杂性进一步增加,因为这会引入不同的人为偏见。算法伦理学的总体目标是突出并解决这些领域中出现的问题:
-
偏见与歧视:有许多因素会影响算法所创建的解决方案的质量。一个主要的关注点是无意中的算法偏见。其原因可能在于算法的设计导致某些数据比其他数据更重要。或者,原因可能出在数据的收集和选择上。这可能会导致应由算法计算的数据被遗漏,或者本不应包含的数据被纳入。例如,一家保险公司使用算法计算风险时,可能会使用包含驾驶者性别的车祸数据。根据现有的数据,算法可能会认为女性驾驶员涉及更多的事故,因此女性驾驶员自动获得更高费用的保险报价。
-
隐私:算法使用的数据可能包含个人信息,并且可能以侵犯个人隐私的方式被使用。例如,启用面部识别的算法就是隐私问题的一个例子。目前,全球许多城市和机场都在使用面部识别系统。挑战在于如何以保护个人隐私不受侵犯的方式使用这些算法。
越来越多的公司将算法的伦理分析纳入其设计过程中。但事实是,问题可能直到我们发现某个有问题的应用场景时才会显现出来。
学习算法的问题
能够根据变化的数据模式进行自我调整的算法被称为学习算法。它们在实时的学习模式中工作,但这种实时学习能力可能带来伦理上的问题。这就有可能导致它们的学习过程做出从伦理角度来看有问题的决策。由于它们被设计为处于持续进化的阶段,几乎不可能对它们进行持续的伦理分析。
例如,让我们研究一下亚马逊在其招聘算法中发现的问题。亚马逊从 2015 年开始使用 AI 算法来招聘员工。在部署之前,它经过了严格的测试,以确保其满足功能和非功能要求,并且没有任何偏见或其他伦理问题。由于它是一个学习算法,因此随着新数据的不断出现,它会不断地自我调优。部署几周后,亚马逊发现 AI 算法意外地发展出了性别偏见。亚马逊将算法下线并进行了调查。结果发现,性别偏见是由于新数据中的一些特定模式引入的。具体而言,最近的数据中男性数量远多于女性,而这些男性恰好有更相关的背景适合该职位。实时的自我调优学习带来了一些无意的后果,导致算法开始偏向男性而非女性,从而引入了偏见。该算法开始将性别作为招聘的决定因素之一。之后,模型重新训练,并添加了必要的安全防护措施,确保不会再次引入性别偏见。
随着算法复杂性的增加,全面理解它们对社会中个人和群体的长期影响变得越来越困难。
了解伦理考量
算法解决方案是数学公式。开发算法的人员有责任确保其符合我们试图解决问题的伦理敏感性。一旦解决方案部署,它们可能需要定期监控,以确保随着新数据的到来和基础假设的变化,算法不会开始产生伦理问题。
这些算法的伦理考量取决于算法的类型。例如,让我们来看一下以下算法及其伦理考量。需要仔细考虑伦理问题的一些强大算法示例如下:
-
分类算法和回归算法在机器学习中各自有不同的用途。分类算法将数据分为预定义的类别,并且可以直接用于决策过程。例如,它们可能决定签证审批或识别城市中的特定人群。另一方面,回归算法基于输入数据预测数值,这些预测确实可以用于决策。例如,回归模型可能预测在市场上列出房子的最佳价格。本质上,分类提供了类别结果,而回归提供了定量预测;两者在不同场景下都对知情决策有价值。
-
算法在推荐引擎中的应用可以将简历与求职者匹配,无论是针对个人还是群体。对于这种使用场景,算法应该在局部和全局层面都实现可解释性。局部层面的可解释性会提供特定个人简历与可用职位匹配时的可追溯性。全局层面的可解释性则提供了匹配简历和职位所使用的整体逻辑的透明度。
-
数据挖掘算法可以用来从各种数据源中挖掘关于个人的信息,这些信息可能被政府用来进行决策。例如,芝加哥警察局使用数据挖掘算法来识别城市中的犯罪热点和高风险个体。确保这些数据挖掘算法的设计和使用符合所有伦理要求,需要通过精心设计和持续监控来实现。
因此,算法的伦理考量将取决于其使用的具体场景以及它们直接或间接影响的实体。在开始使用算法进行关键决策之前,需要从伦理角度进行仔细分析。这些伦理考量应当是设计过程的一部分。
影响算法解决方案的因素
以下是我们在分析算法解决方案优劣时应当牢记的因素。
考虑不确定证据
在机器学习中,数据集的质量和广度在模型结果的准确性和可靠性中起着至关重要的作用。通常,数据可能显得有限,或者缺乏提供决定性结果所需的全面深度。
例如,考虑临床试验:如果一款新药在一小部分人群中进行测试,结果可能无法全面反映其疗效。同样,如果我们在某个城市的特定邮政编码区域检查欺诈模式,有限的数据可能会暗示一个趋势,但这个趋势在更广泛的范围内并不一定准确。
关键在于区分“有限数据”和“不确定证据”。虽然大多数数据集本质上都是有限的(没有数据集能捕捉到所有可能性),但“不确定证据”是指数据未能提供明确或决定性的趋势或结果。这一区分至关重要,因为基于不确定模式做出的决策可能会导致判断错误。特别是在使用基于此类数据训练的算法时,决策时必须保持批判性的眼光。
基于不确定证据做出的决策容易导致不合理的行动。
可追溯性
机器学习算法通常有单独的开发和生产环境。这可能导致训练阶段与推理阶段之间的脱节。这意味着,如果算法造成了某种伤害,追踪和调试非常困难。而且,当算法发现问题时,实际上很难确定受到影响的人群。
误导性证据
算法是数据驱动的公式。垃圾进,垃圾出(GIGO)原则意味着算法的结果只会与其所基于的数据一样可靠。如果数据中存在偏见,那么这些偏见也会反映在算法中。
不公平的结果
算法的使用可能会对已经处于不利地位的弱势群体造成伤害。
此外,使用算法来分配研究资金已经被证明多次对男性群体存在偏见。用于移民审批的算法有时无意间对弱势群体存在偏见。
尽管使用高质量的数据和复杂的数学公式,如果结果是不公平的,整个努力可能带来的伤害大于收益。
让我们看看如何减少模型中的偏差。
减少模型中的偏差
正如我们所讨论的,模型中的偏见是指特定算法的某些属性导致它产生不公平的结果。在当今世界,基于性别、种族和性取向的偏见是已知的,并且有文献记载。这意味着我们收集的数据可能会表现出这些偏见,除非我们在收集数据之前做出了努力,去消除这些偏见。
大多数时候,算法中的偏见是由人类直接或间接引入的。人类通过疏忽无意中引入偏见,或者通过主观性故意引入偏见。人类偏见的一个原因是人类大脑容易受到认知偏见的影响,这种偏见反映了一个人在数据处理和算法逻辑创建过程中的主观性、信仰和意识形态。人类偏见可以反映在算法使用的数据中,也可以反映在算法本身的制定中。对于一个典型的机器学习项目,遵循CRISP-DM(即跨行业标准过程)生命周期,正如在第五章中所解释的,图算法,偏见通常呈现如下:
图 16.4:偏见可以在 CRISP-DM 生命周期的不同阶段被引入
减少偏差最棘手的部分是首先识别和定位潜在的无意识偏见。
让我们看看何时使用算法。
何时使用算法
算法就像是从业者工具箱中的工具。首先,我们需要了解在给定的情况下,哪种工具是最合适的。有时,我们需要问自己,是否已经有解决我们正在尝试解决的问题的方案,何时是部署解决方案的最佳时机。我们需要确定,使用算法是否能提供一个实际有用的解决方案,而不是替代方案。我们需要从三个方面分析使用算法的效果:
-
成本:使用算法的成本是否值得?
-
时间:我们的解决方案是否使整体过程比更简单的替代方案更高效?
-
准确性:我们的解决方案是否比更简单的替代方案产生更准确的结果?
选择合适的算法时,我们需要找到以下问题的答案:
-
我们能否通过做出假设来简化问题?
-
我们将如何评估我们的算法?
-
关键的度量标准是什么?
-
它将如何被部署和使用?
-
它需要被解释吗?
-
我们是否理解三项重要的非功能性需求——安全性、性能和可用性?
-
是否有预期的截止日期?
在根据上述标准选择算法后,值得考虑的是,尽管大多数事件或挑战都可以预见并加以解决,但仍然有一些例外事件,它们违背了我们传统的理解和预测能力。让我们更详细地探讨这一点。
理解黑天鹅事件及其对算法的影响
在数据科学和算法解决方案领域,一些不可预测且罕见的事件可能会带来独特的挑战。“黑天鹅事件”这一术语由纳西姆·塔勒布在《随机的愚弄》(2001)中提出,形象地代表了那些罕见且不可预测的事件。
要被认为是黑天鹅事件,它必须满足以下标准:
-
意外性:该事件令大多数观察者感到惊讶,就像广岛原子弹轰炸那样。
-
事件的重大性:该事件具有颠覆性和重大意义,就像西班牙流感的爆发。
-
事件后可预测性:在事件发生后,很明显,如果之前注意到某些线索,事件是可以预见的,就像在西班牙流感成为大流行之前被忽视的迹象。
-
并非所有人都感到惊讶:一些人可能早已预见到事件,就像参与曼哈顿计划的科学家们预见到原子弹的爆炸一样。
在黑天鹅事件首次在野外被发现之前,几个世纪以来,它们一直用来代表一些不可能发生的事情。发现之后,这个词依然流行,但其代表的意义发生了变化。它现在代表的是一些极其罕见,无法预测的事件。
黑天鹅事件对算法的挑战和机会:
-
预测困境:虽然有许多预测算法,从 ARIMA 到深度学习方法,但预测黑天鹅事件仍然是一个难题。使用标准技术可能会给人一种虚假的安全感。例如,预测像 COVID-19 这样的事件的确切发生时间,由于历史数据不足,面临着许多挑战。
-
预测影响:一旦黑天鹅事件发生,预测其广泛的社会影响就变得复杂。我们可能缺乏相关的数据和对事件影响下的社会关系的理解。
-
预测潜力:虽然黑天鹅事件看似随机,但它们通常是由于被忽视的复杂前兆所引起的。算法在此提供了机会:制定预测和检测这些前兆的策略,可能有助于预见潜在的黑天鹅事件。
实际应用的相关性:
让我们考虑最近的 COVID-19 大流行,这是一个典型的黑天鹅事件。一种潜在的实际应用可能涉及利用先前大流行的相关数据、全球旅行模式和当地健康指标。然后,一个算法可以监控疾病的异常激增或其他潜在的早期迹象,提示可能的全球健康威胁。然而,黑天鹅事件的独特性使得这一过程变得更加困难。
总结
在本章中,我们学习了设计算法时应考虑的实际方面。我们探讨了算法可解释性的概念,以及如何在不同层次上提供可解释性。我们还探讨了算法中可能出现的伦理问题。最后,我们描述了在选择算法时需要考虑的因素。
算法是我们今天所见证的这个新自动化世界中的引擎。了解、实验和理解使用算法的影响至关重要。理解它们的优点和局限性,以及使用算法的伦理影响,将对改善我们生活的世界产生深远的影响,本书的目标就是在这个不断变化和发展的世界中实现这一重要目标。
在 Discord 上了解更多
要加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问并了解新版本——请扫描下面的二维码: