在动作/冒险类型中塑造有毒的男子气概
使用 NLTK,Gensim,Spacy 和 pyLDAvis 揭示行动主角之间的说话模式。
Photo by Jakob Owens on Unsplash
动作/冒险电影多年来一直吸引着观众。从《Bullit》(1968 年)中的惊险汽车追逐到《John Wick 3》(2019 年)中的激烈战斗,电影观众一次又一次地涌向电影院,寻求肾上腺素的刺激。
虽然这种类型确实有很多乐趣,但也有一些非常明显的问题,我很好奇想了解一下。首先,绝大多数动作/冒险电影都有一个白人男性主角。其次,所说的主角往往把女性和配角当作被征服的对象。这一点在《詹姆斯·邦德》系列中表现得很明显,但我会引用范·迪塞尔主演的《XXX》系列中的一句话来总结它:
“让我为你简化一下。踢一些屁股,得到女孩,并试图看起来像毒品,而你做到了这一点。”
考虑到这一切,我没有必要去发现我在这个项目中最终发现了什么。事实上,我最初试图通过对话提取个性,试图通过降维将五大特征映射到口语中。虽然我无法使用下面的技术来揭示人格特征,但我能够在几个主题中找到清晰的有毒的演讲模式。所以…让我们开始吧!
数据
为了这个项目,我利用了来自加州大学圣克鲁斯分校的大型电影语料库。语料库按类型细分,包含 960 个电影剧本,其中电影中的对话与场景描述分离。在这里,我选择特别关注动作/冒险类型,包括从《兰博》到《虎胆龙威》的 143 部电影。
我最近完成了一个项目,打印出了语料库中每个电影主角的个性特征,在这个项目中,我将更详细地介绍文本预处理,所以在这篇文章中,我将重点介绍一些更详细的 NLP 技术和主题建模。
可以说,在我最初的预处理之后,我有了一个熊猫数据帧,其中有两列(人物和对话)用于语料库中的每个主角,每行对话作为单独的行,总共 127K 个话语,跨越大约 150 个人物。
处理自然语言
Photo by Patrick Tomasso on Unsplash
准备好数据后,现在是时候引入自然语言工具包并为主题建模准备好语料库了。
关于 NLP(自然语言处理)有趣的部分是,对于过程或工具没有硬性的和固定的规则。通常,在预处理/标准化之后是标记化、词干化/词干化、向量化、维度缩减和可视化。但是,根据上下文的不同,步骤和细微差别会有很大的不同。例如,在一种情况下,对计数矢量器和 LSA 使用二元模型可能有意义,而对 TFIDF 和 NMF 使用三元模型可能有意义。你可以把这想象成一个工作室里的艺术家,用模型和雕塑做实验,直到你有了美丽的东西。
为了这篇文章的目的,我不会在整个项目中经历许多迭代(和失败),而是会遍历产生最佳结果的步骤和模型。
首先,我将每个句子标记成一个单词列表,使用 Gensim 的 simple_preprocess()函数删除标点符号和不必要的字符,如下面的代码所示:
接下来,我定义了二元模型和三元模型。实质上,这捕获了两个相邻的单词,并确保应该在一起的单词(如“旧金山”)仍然是一个单元。换句话说:二元模型是在文档中频繁出现的两个词。三元模型是三个经常出现的词。
Gensim 的Phrases
模型可以构建和实现二元模型、三元模型、四元模型等等。Phrases
的两个重要参数是min_count
和threshold
。这些参数的值越高,单词就越难组合成二元模型。这方面的代码如下:
接下来,我定义了一些函数。也就是说,我对单词进行了词汇化,去掉了停用词,词汇化就是把一个单词转换成它的词根。例如:单词‘trees’的引理是‘tree’。同样,“说话”–>“说话”,“鹅”–>“鹅”等等。停用词本质上是英语中非常常见的词,我们不想将它们包括在我们的分析中。像“the”和“for”等词。对于下面的代码,我使用 Spacy 导入了停用词和词汇化。
从单词到词典/语料库到 LDA
既然我已经越来越接近能够将这些高维数据提取到主题中,我接下来使用 id2word 将我的单词列表转换为字典和语料库。原因是 LDA 主题模型的两个主要输入是(你猜对了!)词典(id2word
)和文集:
这让我非常接近做好模型的准备!难题的最后一块是确定我想探索多少主题。这里有许多技术,包括剪影方法,但我选择生成所谓的肘图:本质上是一种启发式方法,以帮助找到运行模型所需的集群数量。这种方法将方差的百分比解释为聚类数的函数:应该选择多个聚类,这样添加另一个聚类不会提供更好的数据建模。在图上,这通常是看起来像肘的点。
在这种情况下,我没有一个强大的“肘”来形象化。也许是 3 或 4 岁,但还不确定。这必须是另一个例子,我尝试了一些事情,并解释了结果的质量。
好了,终于到模型了!接下来,我将词典和语料库以及我想要探索的主题数量放入我的 LDA 模型中。我找到了 4 个主题最容易理解的结果。除了主题的数量,alpha
和eta
是影响主题稀疏度的超参数。根据 Gensim 文档,两者都默认为 1.0/num _ topics previous。
chunksize
是每个训练组块中要使用的文档数。update_every
决定模型参数更新的频率,而passes
是训练通过的总次数。以下是该模型的代码:
太好了。现在来看一些结果。虽然简单地打印出模型主题是有效的,但有一种更直观、更具交互性的方式来显示结果:pyLDAvis。左侧图中的每个气泡代表一个主题。泡沫越大,这个话题就越流行。对于 4 个主题,我能够得到相当不同的(非重叠的)气泡,并得到有意义的结果,如下所示:
调查结果+结论性想法
在语料库的所有四个主题中,我看到了命令和厌恶女性的词汇。在更“驯服”的一边,有像“去”“得到”“想要”“拿走”这样的动作词,一直到只能被描述为“不那么驯服”的词,像“干”“宝贝”“杀死”等等。
当然,有许多电影的主角需要简单地…
……但是像“得到”这样的词似乎反复暗示着对他人的占有和强制命令,而不仅仅是去某个地方的需要。撇开幽默不谈,这种形象化表明,从 60 年代到现在,动作主角的写作确实存在问题,如果我不得不说出我上面的视觉主题,我会说它们可以归结为:
- 命令和强烈的要求
- 威胁
- 作为物品的女人
- 武器
我们希望这种趋势会改变,我们不仅会看到动作片中男女主角的多元化表现,还会看到他们的言行更具同理心。
这就是这个项目的全部,但展望未来,我有兴趣对语料库中的其他体裁进行同样的处理。与此同时,如果你有兴趣更深入地研究代码,请随时查看我的项目回购。
附录
强化学习建模
概念和用例
强化学习包括弄清楚在哪种情况下做什么。这可能很棘手。所有可能的情况中只有极小一部分可能被经历过。如果那样的话。即使在熟悉的情况下,在特定的情况下,一个可靠的行动可能会产生意想不到的结果。环境可能会抛出一个曲线球。
行动有直接的和延迟的后果,可能是相互冲突的。一些延迟的后果可能是未知的。有些人即使在同样的情况下也不会重复。有些可能依赖于初始动作之后的动作。代理试图对所有这些进行分类,以发现并执行适当平衡这两者的行动策略。
在这篇文章中,我们将介绍强化学习的主要概念。我们的视角是一种建模视角。我们考虑各种用例。我们模拟了试图将它们建模为强化学习问题的过程。有趣的问题出现了。获得洞察力。
故事是这样的。我们从框架和基本概念开始。接下来,我们看一些例子来巩固我们的理解。接下来,我们将介绍更多的概念。最后,我们更详细地看一个例子。
框架
强化学习包括一个代理人在一个环境中行动,这个环境从某些状态传递奖励。(有时奖励是通过行动来实现的。我们稍后将对此进行深入研究。)在任何给定时间,代理都处于特定状态。她可以选择她能执行的动作。她选择了一个,这通常受到她当前所处状态的影响。她的长期目标是访问提供奖励的州,尽可能多地积累奖励。
然而,在许多用例中,会出现各种各样的挑战。第一个是“环境”并不总是合作。它通过移动到另一个状态来对她的动作做出反应。这一步甚至可能不是决定性的。在同样的情况下——也就是说,在同一个州采取同样的行动——环境可能会做出不同的反应。更糟糕的是,这种反应可能是对抗性的。它可能会故意阻止代理人访问奖励国。
第二个挑战是状态空间可能很大。代理访问一小部分甚至是不可行的。
第三个挑战是状态可能不是完全可观察的。也就是说,代理并不知道当前状态的所有信息。这可能会妨碍她选择最佳行动的能力。
第四种可能性是,同一个州可能会对不同的访问给予不同的回报。也就是说,任何特定状态下的奖励本身就是一个随机变量。
我们需要更多的概念和改进。在进一步讨论之前,我们先停下来看一些具体的例子。
象棋
你是代理人。板配置就是状态。有些代表获胜的州,有些代表失败的州。行动是你可以采取的行动。你想达到一个胜利的状态。也就是赢。
这些状态是完全可观测的。当前的棋盘配置包含了你选择下一步棋所需的所有信息。(你如何利用这些信息是另一回事。)
州的数量是巨大的。大约
10000000000000000000000000000000000000000000000
环境是对抗性的。你的对手想让你输。
更多例子
扑克。你是代理人。这些状态只是部分可观测的。如果你能看到其他玩家的牌,你可能会做得更好。但是你不能。
你正在努力减肥。状态就是你现在的体重。这是完全可以观察到的。你可能的行动是各种锻炼,各种饮食,药物治疗,什么都不做。它们的效果会延迟。你不会马上减肥。甚至不确定你会不会。有些方法可能根本不起作用。
你是一个渴望成为职业高尔夫球手的机器人。让我们集中精力打好一杆。状态的一些关键要素是球到球瓶的距离,以及你是在球道上还是在草地上还是在沙坑里。有些动作是用哪个球杆。包括挥杆在内的动作显然也很重要。想象一个机器人在荡秋千。
你是一个试图学习如何驾驶汽车的机器人。(我指的是传统型,有方向盘、刹车、油门踏板……)状态的某些方面是你当前的速度和方向,因为它们与你所处的道路有关。如果你走得太快,你可能要慢下来。如果你在高速公路上曲折行驶,你可能想“直起腰来”。你动作的一些要素是踩哪个踏板(刹车?,加速器?),按多少,方向盘的朝向等等。很明显,行动既有(激烈的)直接后果,也有延迟的后果(开得太快有被开罚单或出事故的风险,开得太慢意味着你会迟到,再加上很多车对你鸣喇叭)。
你正试图从购物中心的 A 店走到 B 店。便利的是,购物中心的地图就在商店 a 的旁边。如果购物中心不拥挤,你可能会选择最短的路线。即使弄清楚这一点也可能会涉及到,这取决于 B 离 A 有多远,以及商场的走道是如何安排的。如果商场很拥挤,你还有一个复杂的问题要处理。如果一条最短的路线上的某些路段很拥挤,或者有人朝与你相反的方向移动,那么这条路线可能不是一条好路线。
接下来,前面提到的附加概念。
行走和奖励
遍历是一个特定的状态-动作对交替序列,从特定的状态开始,到特定的状态结束。这在下面描述。
W: s0 -> a0 -> s1 -> a1 -> ... -> sk
代理从状态 s0 开始并执行动作 a0。这导致移动到状态 s1。在 s1 中,代理执行 a1。诸如此类。最终代理结束于状态 sk。如前所述,虽然代理可以完全控制在任何给定的状态下要做的动作,但她通常不能完全控制下一个要访问的状态。环境会做出它选择的任何反应。
步行的(累积)奖励是被访问的州的折扣奖励的总和。
R(W) = r0 + a*r1 + a^2 * r2 + ... + a^k * rk
这里 0 和 1 之间的 a 是折扣因子,ri 是奖励状态 si 提供的。为什么是贴现因子?时光飞逝。有一个短期。有一个长期的。为了寻求即时的满足,将 a 设为零。寻求长期回报——即使短期回报不会到来——将 a 设为正数。
代理人的政策
代理寻求体验产生高累积回报的行走。她怎样才能达到这个目标呢?通过她选择的行动。
让我们把这正式定为代理的政策。策略指定代理从任何给定状态采取的操作。该策略可以是确定性的,即代理总是在每次访问某个状态时执行相同的动作。还是概率性的。在不同的访问中,代理人有不同的反应,尽管有些反应比其他反应更受欢迎。
一般来说,当代理人知道在某个特定的状态下该做什么及其后果时,她会想要利用她的知识。选择她的下一步行动。在不熟悉的状态下,她更倾向于探索。尝试不同的事情,看看会发生什么。这就是强化学习中所谓的探索 vs 开发权衡。
状态值(在策略下)
某个状态有多好?这通常取决于代理遵循的策略。对熟练的代理人有利的状态可能对不熟练的代理人不利。不熟练的代理人可能只是不知道如何利用其固有的优点。
让我们把这个正式化。在特定策略下,状态的值是代理可以从执行该策略的行走中期望得到的回报。单词“expect”提醒我们,即使在确定性策略下,行走也可能不完全在代理的控制之下。
在国际象棋中,棋盘配置的价值是代理人从它开始时获胜的可能性。这项政策是含蓄的。代理人想赢。
接下来,我们的最后一个例子。这一个更充实。部分是为了加强刚刚描述的概念。与此同时,也带出新鲜的微妙之处。
通勤
假设你从家通勤到公司(或学校)。想象一下你的通勤很“复杂”。不难想象。你在城市街道,高速公路,甚至泥路上行驶。你有很多选择。你刚刚读到过持续使用全球定位系统如何把你的大脑变成一个“沙发土豆”。所以你要练习用你的大脑来路由。嗯,加上你的边缘系统…
状态和动作
国家应该是什么样的?时空听起来很合理。即特定时间的特定位置。你什么时候出发可能会影响你从 A 到 B 的路线。
你的行动应该是什么?在时间 T 你能从你的当前位置选择的路线段?所谓路段,我们指的是从你当前的状态 A 出发,带你到某个特定位置 b 的道路。类似于地图应用程序将其建议的路线分成路段(转弯方向)。
奖励和优化标准
你想优化什么?找到最快的路线?还是最短的,风景最好的,最不丑的,坑洞最少的,红绿灯最少的,卡车流量最少的,海风最好的,树木最多的,…
嗯……又一个用脑的理由。不需要预先指定目标,甚至不需要明确地指定。你的大脑会“感知”你喜欢和不喜欢的路线。即使当你很难准确表达你喜欢什么,更不用说将你的偏好整合成一个多标准的目标函数时,这种方法仍然有效。
你应该选择什么作为你所在州的奖励?嗯……上一段列出的标准适用于路线而非位置。对你来说路线就是行动。所以看起来你想要的是行动上的奖励而不是状态上的奖励。实际上,想得更多一点,你可能也有在通勤期间你想要参观(或避免)的地点的偏好。所以你既想要行动上的回报,也想要状态上的回报。
嗯,一个路段上的奖励可以同时获取该路段的行驶奖励和该路段的目的地奖励。这是因为操作明确指定了目的地。也就是说,我们假设状态转换是确定的。如果我们在时间 T 从 A 取路段A->R->B
,我们将在稍后的某个时间到达 B。
这种奖励机制描述如下。
reward(route-segment, T) = reward(route-segment.drive, T) + reward(route-segment.destination, destination.arrival-time)
到达时间是你到达目的地的时间。这个时间既取决于你什么时候开始这个路段,也取决于沿途发生的其他事情。
如果你的主要兴趣是最快的路线,reward(route-segment.drive,T)
是一个随机变量。任何特定情况下的驾驶时间取决于交通状况和其他因素。开始时间 T 抓住了这种依赖性的一部分,但不是全部。
状态值
你在 t 时刻位于 A 点,你想要从这里开始工作的最快路线。你当前状态的价值是你期望从这里到工作地点需要的时间。如果没有流量,这只是最短路径的长度,长度是以时间而不是距离为单位定义的。引入流量(和其他因素)会注入一些可变性。这就是为什么我们将状态值建模为预期时间。
你的政策
再说一遍,假设你在优化通勤时间。假设没有交通堵塞,您的最佳策略是从当前位置到目的地走最短的路径。(和以前一样,长度是以时间而不是距离来衡量的。)找到这个策略需要解决一个最短路径问题。如果有交通或其他因素注入可变性,你的任务就更复杂了。如果这些因素取决于你的开始时间,那就更是如此。您可能需要在不同的时间尝试不同的起始状态,并从中采取不同的行动,即下一个路段。这将有助于您估计各种条件下不同路段的行驶时间。当然,你仍然需要从 A 到 B 在一个特定的时间从所有这些开始缝合一个好的多段路线。
上述过程可以用先验知识来播种。也就是说,你可能对不同路段的行程时间有合理的预估。这可以指导你早期的探索。
总结
我们已经介绍了强化学习的主题和其中的关键概念。我们的观点是一个模型。我们已经研究了稳定环境、非稳定环境、确定性行为和反应(由环境决定),以及确定性和随机策略。
我们已经在许多环境中看到了这样的例子:游戏(国际象棋、扑克)、控制(机器人尝试打高尔夫球、开车)、健康(减肥)和路由(逛商场、通勤)。
我们希望读者能够更好地理解强化学习是什么,以及它能解决什么类型的问题。
基于机器学习的高效军事部署建模——R
(如果使用智能手机,这篇文章最好在横向模式下查看)
拉丁美洲和加勒比海地区的武装部队面临着必须执行多方面任务的挑战。在内乱加剧的时候,他们需要开展维持和平行动,武器换毒品交易引发的帮派战争要求进行反叛乱式的部署,季节性自然灾害往往需要他们的服务,以支持极端条件下的基本服务。在资源有限的情况下,需要利用一切机会防止不必要的开支,同时保持效力。
在本帖中,我将展示如何在拉丁美洲和加勒比海的海军部队中应用 K-means 聚类算法,以安排高效的海军部署并减少不必要的行动。
准备数据
对于这个例子,我模拟了 200 个数据点,这些数据点代表了需要在加勒比海部署海军资源的事件的位置。这些数据有一个时间戳,指示每个事件在 24 小时时钟周期中发生的时间。重点关注的特定区域约为 7500 平方英里,位于以下区域之间:
纬度— N1800 '和 N1720’
,经度— W7820 '和 W7600 ’
(牙买加南海岸附近的加勒比海)
我们的任务是确定任何容易发生事故的地点和时间,如果是这样的话,向这些地区分配更多的军事资源,同时减少向很少发生事故的地点的部署。
#You will require these packages to execute the code in this example
#effectively.
library("plotly", lib.loc="~/R/win-library/3.6")
library("dplyr", lib.loc="~/R/win-library/3.6")
library("ggplot2", lib.loc="~/R/win-library/3.6")
现在来模拟数据点。
#Generate data for example
set.seed(11)
LatCL1 <- round(rnorm(20, mean = 17.9, sd = 0.1), 2)set.seed(12)
LonCL1 <- round(rnorm(20, mean = -78.37, sd = 0.2), 2)
CL1 <- data.frame(LonCL1, LatCL1)
names(CL1) <- c('Longitude', 'Latitude')set.seed(21)
LatCL2 <- round(rnorm(50, mean = 17.73, sd = 0.033), 2)set.seed(22)
LonCL2 <- round(rnorm(50, mean = -78, sd = 0.25), 2)
CL2 <- data.frame(LonCL2, LatCL2)
names(CL2) <- c('Longitude', 'Latitude')set.seed(31)
LatCL3 <- round(rnorm(130, mean = 17.64, sd = 0.075), 2)set.seed(32)
LonCL3 <- round(rnorm(130, mean = -76.64, sd = 0.3), 2)
CL3 <- data.frame(LonCL3, LatCL3)
names(CL3) <- c('Longitude', 'Latitude')CL <- rbind(CL1, CL2, CL3)
我们已经生成了 200 个数据点,这些数据点代表了记录事件的位置,例如遇险呼叫、海盗船目击报告(是的,加勒比海真的有海盗)、海上失踪的水手(请求搜索和救援服务)、拦截毒品/武器走私者等…
将数据可视化
我们现在可以将它们绘制在图表上,看看我们是否可以直观地识别数据点之间的任何明显趋势。
CL <- rbind(CL1, CL2, CL3)
plot(CL, xlim = c(-78.3, -76), ylim = c(17.3, 18), main = 'Events')
似乎有两组事件聚集在一起(分别位于-77.4 经度线的两侧)。
接下来,我们将实现 K-means 算法来验证我们的观察是否在数学上得到支持。由于聚类数“K”是模型的一个超参数,因此遵循我们对 K = 2 的初始观察是合理的。但是,我们知道运营部署安排在三个轮班日左右,我们将设置 K = 3 来测试我们是否可以识别三个相应的集群,每个轮班一个集群。之后,我们将使用 ggplot2 可视化集群。
#Clustering with K=three
set.seed(101)
TwoD_Clust <- kmeans(CL, 3, iter.max = 10)#Assign the cluster to the varibale
CL$Group <- as.factor(TwoD_Clust$cluster)ggplot(CL, aes(x=CL$Longitude, y=CL$Latitude, color=CL$Group)) +
labs(x ='Longitude', y ='Latitude', title = 'Tri-Clustered Events') +
geom_point()
在 K = 3 的情况下,该算法仍然能够识别高度可区分的组,但是这两个组(1 & 3)看起来是强制的。让我们回到 K = 2,并把结果可视化。
#Clustering with K=two
set.seed(100)
TwoD_Clust <- kmeans(CL, 2, iter.max = 10)#Assign the cluster to the varibaleCL$Group <- as.factor(TwoD_Clust$cluster)ggplot(CL, aes(x=CL$Latitude, y=CL$Longitude, color=CL$Group)) +
labs(x ='Longitude', y ='Latitude', title = 'Bi-Clustered Events') +
geom_point()
探索数据并获得创造力
这些组看起来仍然更合适,幸运的是我们还没有将第三个变量(事件发生的时间)添加到我们的数据中。可能添加第三个变量将有助于我们能够更好地区分各组。接下来,我们将把所有三个变量经度、纬度和时间编译成一个数据帧,并使用 Plotly 软件包进行 3D 可视化,以制作一个交互式 3D 绘图。
#Adding the third variable 'Time'
set.seed(41)
TCL1 <- data.frame(round(rnorm(20, mean = 10, sd = 1), 2))
names(TCL1) <- c('ETime')set.seed(42)
TCL2 <- data.frame(round(rnorm(50, mean = 23.5, sd = 1), 2))
names(TCL2) <- c('ETime')set.seed(43)
TCL3 <- data.frame(round(rnorm(130, mean = 3, sd = 1), 2))
names(TCL3) <- c('ETime')Event_Times <- rbind(TCL1, TCL2, TCL3)#Compiling all the data into a dataframe
Event_Data <- data.frame(rbind(CL1, CL2, CL3), Event_Times)#Using Plotly to visualise three dimensions
plot_ly(x =Event_Data$Longitude, y = Event_Data$Latitude, z= Event_Data$ETime,
type = 'scatter3d', mode = 'markers')
Rotate the Graph to view from different perspectives. Interactive 3D Scatter-plot
您可以与可视化交互并旋转可视化,以从不同的角度查看集群的外观。使用这个交互式 3D 散点图,我们现在可以清楚地识别三组事件。虽然 2D 图掩盖了数据的可变性,并建议部署应集中在两个特定区域,但现在很明显,在规划常规部署时,“部署时间”应是一个重要的考虑因素,理想情况下,可用的人力资源应分成三个小组,每个小组在特定时间部署。
为了进一步验证我们的观察,我们将再次进行另一个 K 均值分析,K= 3。利用 K-means 函数的输出,我们还能够对数据中可能出现的任何趋势进行更详细的分析。
#Clustering the data with three variables and K=3
ThreeD_Clust <- kmeans(Event_Data, 3, iter.max = 10)Event_Data$Group <- as.factor(ThreeD_Clust$cluster)D32<- plot_ly(x = Event_Data$Longitude, y = Event_Data$Latitude, z= Event_Data$ETime,
type = 'scatter3d', mode = 'markers', color = Event_Data$Group) %>%
layout(title = '3D Clusters', scene = list(
xaxis = list(title ='Longitude'),
yaxis = list(title ='Latitude'),
zaxis = list(title ='Time Axis')
))
Rotate the Graph to view from different perspectives. Interactive Output of K-Means Cluster with K=3
您可以与可视化交互并旋转可视化,以从不同的角度查看集群的外观。该算法很好地确定了三个不同的集群,可用于确定有效作战部署的最佳时间和位置。
分析和解释
这项工作还没有完成。接下来,我们必须对算法的输出进行分析,以将其结果正式化,并将其转换为可用于自信地部署军事资源的可操作信息。
#Viewing the output of the kmeans functionThreeD_ClustK-means clustering with 3 clusters of sizes 130, 20, 50Cluster means: Longitude Latitude ETime Group
1 -76.64654 17.64231 3.068308 1
2 -78.43600 17.86550 10.427000 3
3 -77.96940 17.73420 23.464600 2Clustering vector:
[1] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
[42] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 1 1 1 1 1 1 1 1 1
[83] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
[124] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
[165] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1Within cluster sum of squares by cluster:
[1] 134.22748 19.67019 68.32914(between_SS / total_SS = 98.6 %)Available components:
[1] "cluster" "centers" "totss" "withinss" "tot.withinss"
[6] "betweenss" "size" "iter" "ifault"
从输出/结果中,三条信息对我们的用例特别重要:1)聚类平均值或“质心”。2)每个聚类的大小 3)平方和。
- 我们已经确定了三个集群,它们确定了资源应该部署到的适当时间和地点,以便期望最高的产出回报。以下是要点:
W-76*.64,N17*.64 凌晨 3 点左右
W-78*.43,N17*.86 上午 10:45 左右
W-77*.96,N17*.73 晚上 11:45 左右。
我们现在可以在这些地点和时间规划部署,以获得最大的效率。
2.集群 1、2 和 3 的大小分别为 130、20 和 50(比率为 65:10:25)。这些数字表示每个集群中可能发生的事件的相对数量。有了这些信息,我们就能够将一个 100 人的军事人员小组适当地分配如下:第 1 组 65 人,第 2 组 20 人,第 3 组 25 人。这减少了因疏忽造成的人员不足和过度部署。
3.类内平方和表示类的可变性。基本上,模型与数据的吻合程度,以及特定值与平均值相差很大的可能性。大面积的平方表明,尽管部署将集中在特定的地点和时间,但他们仍然必须在这些中心区域进行长时间的巡逻。我们的组间 SS /总 SS = 98.6%,表明该模型非常适合!
这种技术在现实生活中应用的一个例子可以在这里找到
边注
通常建议在执行机器学习算法之前对数据进行归一化/缩放,因为变量范围的差异会影响模型的性能。我可以证实,在这种情况下,情况并非如此,也没有证据表明发生了这种情况。事实上,缩放后的模型往往表现更差!(你可以自己重新运行代码,评估缩放后的结果)——这是对机器学习科学中存在的艺术的微妙提醒。此外,将质心的初始随机迭代次数设置为 10 似乎足以产生一个稳定的模型,该模型在多次重新运行算法后具有一致的结果。时间戳被转换为表示一天 24 小时中的小时,纬度/经度值从度分钟格式转换为度和小数。
用 Python 在经典力学中模拟三体
引力基础概述,Scipy 中的 odeint 解算器和 Matplotlib 中的 3D 绘图
Image by Kevin Gill on Flickr
1.介绍
我最近读了中国作家刘的科幻小说《三体》。在书中,他描述了一个虚构的外星文明,生活在一个被三颗恒星环绕的名为 Trisolaris 的星球上。由于三颗恒星的存在,你认为他们的存在会与我们的存在有多大的不同?耀眼的阳光?持续的夏天?事实证明,事情要糟糕得多。
我们很幸运生活在一个只有一颗主要恒星的太阳系中,因为这使得恒星(我们的太阳)的轨道是可预测的。将恒星的数量增加到两颗,系统仍然保持稳定。它有,我们称之为的解析解——也就是说,我们可以求解描述它的方程组,得到一个精确给出系统从 1 秒到一百万年的时间演化的函数。
然而,当你添加第三个身体时,一些不寻常的事情发生了。系统变得混乱不堪,高度不可预测。它没有解析解(除了少数特殊情况),它的方程只能在计算机上数值求解。它们会突然从稳定变为不稳定,反之亦然。生活在这样一个混乱世界中的三索拉人发展出了在“混乱时代”让自己“脱水”并冬眠,在“稳定时代”醒来并平静生活的能力。
书中对恒星系统有趣的形象化描述启发我阅读了引力中的 n 体类问题以及用于解决它们的数值方法。这篇文章涉及到理解这个问题所需的引力的几个核心概念和求解描述这个系统的方程所需的数值方法。
通过本文,您将了解到以下工具和概念的实现:
- 使用 Scipy 模块中的 odeint 函数在 Python 中解微分方程。
- 使方程无量纲化
- 在 Matplotlib 中制作 3D 图
2.引力基础
2.1 牛顿引力定律
牛顿万有引力定律说,任意两个点质量之间都有引力(称为万有引力),其大小与它们质量的乘积成正比,**与它们之间距离的平方成反比。**下面的等式用向量的形式表示这个定律。
这里, G 是万有引力常数, m₁ 和 m₂ 是两个物体的质量, r 是它们之间的距离。单位矢量远离物体 m₁ 指向 m₂ ,力也作用在同一个方向。
2.2 运动方程
根据牛顿第二运动定律,物体上的净力产生了物体动量的净变化——简单来说,力就是质量乘以加速度。因此,将上面的方程应用到质量为 m₁ 的物体上,我们得到下面的物体运动微分方程。
请注意,我们将单位向量分解为向量 r 除以其大小 |r| ,从而将分母中的 r 项的幂增加到 3。
现在,我们有了一个二阶微分方程来描述两个物体由于重力而产生的相互作用。为了简化它的解,我们可以把它分解成两个一阶微分方程。
物体的加速度是物体速度随时间的变化,因此位置的二阶微分可以用速度的一阶微分代替。类似地,速度可以表示为位置的一阶微分。****
索引 i 用于待计算位置和速度的物体,而索引 j 用于与物体 i 相互作用的另一物体。因此,对于一个两体系统,我们将求解这两个方程的两个集合。
2.3 质心
另一个需要记住的有用概念是系统的质心。质心是系统所有质量矩的总和为零的点——简单来说,你可以把它想象成系统整体质量平衡的点。
有一个简单的公式可以求出系统的质心和速度。它包括位置和速度向量的质量加权平均值。
在建立三体系统的模型之前,让我们首先建立一个两体系统的模型,观察它的行为,然后将代码扩展到三体系统。
3.两体模型
3.1 半人马座阿尔法星系
两体系统的一个著名的现实世界例子可能是半人马座阿尔法星系。它包含三颗恒星——半人马座α星 A、半人马座α星 B 和半人马座α星 C (通常称为比邻星)。然而,由于比邻星与其他两颗恒星相比质量小得可以忽略不计,半人马座阿尔法星被认为是一个双星系统。这里要注意的重要一点是,在多体系统中考虑的物体都有相似的质量。因此,太阳-地球-月亮不是一个三体系统,因为它们没有相等的质量,地球和月亮也不会显著影响太阳的路径。
The Alpha Centauri binary star system captured from the Paranal Observatory in Chile by John Colosimo
3.2 无量纲化
在我们开始解这些方程之前,我们必须先把它们无量纲化。那是什么意思?我们将方程中所有有量纲(分别像 m、m/s、kg 的量)的量(像位置、速度、质量等等)转换成大小接近 1 的无量纲量。这样做的原因是:
- 在微分方程中,不同的项可能有不同的数量级(从 0.1 到 10 ⁰).如此巨大的差距可能导致数值方法收敛缓慢。
- 如果所有项的幅度变得接近于 1,那么所有的计算将变得比幅度不对称地大或小时更便宜。
- 您将获得一个相对于标尺的参考点。例如,如果我给你一个量,比如 4×10 ⁰千克,你可能无法计算出它在宇宙尺度上是大还是小。然而,如果我说是太阳质量的 2 倍,你将很容易理解这个量的意义。
为了无量纲化方程,将每个量除以一个固定的参考量。例如,将质量项除以太阳的质量,位置(或距离)项除以半人马座阿尔法星系统中两颗恒星之间的距离,时间项除以半人马座阿尔法星的轨道周期,速度项除以地球围绕太阳的相对速度。
当你用参考量除每一项时,你也需要乘以它以避免改变等式。所有这些项连同 G 可以组合成一个常数,比如说等式 1 的 K₁ 和等式 2 的 K₂ 。因此,无量纲化方程如下:
术语上的横条表示术语是无量纲的。这些是我们将在模拟中使用的最终方程。
3.3 代码
让我们从导入模拟所需的所有模块开始。
#Import scipy
import scipy as sci#Import matplotlib and associated modules for 3D and animations
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import animation
接下来,让我们定义用于无量纲化方程的常数和参考量以及净常数 K₁ 和 K₂ 。
#Define universal gravitation constant
G=6.67408e-11 #N-m2/kg2#Reference quantities
m_nd=1.989e+30 #kg #mass of the sun
r_nd=5.326e+12 #m #distance between stars in Alpha Centauri
v_nd=30000 #m/s #relative velocity of earth around the sun
t_nd=79.91*365*24*3600*0.51 #s #orbital period of Alpha Centauri#Net constants
K1=G*t_nd*m_nd/(r_nd**2*v_nd)
K2=v_nd*t_nd/r_nd
是时候定义一些参数来定义我们试图模拟的两颗恒星了——它们的质量、初始位置和初始速度。注意这些参数都是无量纲的,所以半人马座阿尔法星 A 的质量定义为 1.1(表示太阳质量的 1.1 倍,这是我们的参考量)。速度是任意定义的,没有一个物体能逃脱彼此的引力。
#Define masses
m1=1.1 #Alpha Centauri A
m2=0.907 #Alpha Centauri B#Define initial position vectors
r1=[-0.5,0,0] #m
r2=[0.5,0,0] #m#Convert pos vectors to arrays
r1=sci.array(r1,dtype="float64")
r2=sci.array(r2,dtype="float64")#Find Centre of Mass
r_com=(m1*r1+m2*r2)/(m1+m2)#Define initial velocities
v1=[0.01,0.01,0] #m/s
v2=[-0.05,0,-0.1] #m/s#Convert velocity vectors to arrays
v1=sci.array(v1,dtype="float64")
v2=sci.array(v2,dtype="float64")#Find velocity of COM
v_com=(m1*v1+m2*v2)/(m1+m2)
我们现在已经定义了模拟所需的大部分主要量。我们现在可以继续准备 scipy 中的 odeint 解算器来解我们的方程组。
为了求解任何一个 ODE,你需要方程(当然!)、一组初始条件和时间跨度来求解方程。 odeint 解算器也需要这三个基本要素。这些方程是通过函数来定义的。该函数接受一个包含所有因变量(这里是位置和速度)的数组和一个包含所有自变量(这里是时间)的数组。它返回数组中所有微分的值。
#A function defining the equations of motion
def TwoBodyEquations(w,t,G,m1,m2):
r1=w[:3]
r2=w[3:6]
v1=w[6:9]
v2=w[9:12] r=sci.linalg.norm(r2-r1) #Calculate magnitude or norm of vector dv1bydt=K1*m2*(r2-r1)/r**3
dv2bydt=K1*m1*(r1-r2)/r**3
dr1bydt=K2*v1
dr2bydt=K2*v2 r_derivs=sci.concatenate((dr1bydt,dr2bydt))
derivs=sci.concatenate((r_derivs,dv1bydt,dv2bydt))
return derivs
从代码片段中,您可能能够非常容易地识别微分方程。其他零零碎碎的是什么?请记住,我们正在解决 3 维的方程,所以每个位置和速度矢量将有 3 个组成部分。现在,如果考虑上一节给出的两个向量微分方程,需要求解向量的所有三个分量。因此,对于单个物体,你需要解 6 个标量微分方程。对于两个物体,你得到了,12 个标量微分方程。所以我们制作了一个大小为 12 的数组 w ,用来存储两个物体的位置和速度坐标。
在函数的末尾,我们连接或加入所有不同的导数,并返回一个大小为 12 的数组deriv。
困难的工作现在完成了!剩下的就是将函数、初始条件和时间跨度输入到 odeint 函数中。
#Package initial parameters
init_params=sci.array([r1,r2,v1,v2]) #create array of initial params
init_params=init_params.flatten() #flatten array to make it 1D
time_span=sci.linspace(0,8,500) #8 orbital periods and 500 points#Run the ODE solver
import scipy.integratetwo_body_sol=sci.integrate.odeint(TwoBodyEquations,init_params,time_span,args=(G,m1,m2))
变量 two_body_sol 包含关于两体系统的所有信息,包括位置矢量和速度矢量。为了创建我们的情节和动画,我们只需要位置向量,所以让我们把它们提取到两个不同的变量。
r1_sol=two_body_sol[:,:3]
r2_sol=two_body_sol[:,3:6]
是时候剧情了!这就是我们将利用 Matplotlib 的 3D 绘图功能的地方。
#Create figure
fig=plt.figure(figsize=(15,15))#Create 3D axes
ax=fig.add_subplot(111,projection="3d")#Plot the orbits
ax.plot(r1_sol[:,0],r1_sol[:,1],r1_sol[:,2],color="darkblue")
ax.plot(r2_sol[:,0],r2_sol[:,1],r2_sol[:,2],color="tab:red")#Plot the final positions of the stars
ax.scatter(r1_sol[-1,0],r1_sol[-1,1],r1_sol[-1,2],color="darkblue",marker="o",s=100,label="Alpha Centauri A")
ax.scatter(r2_sol[-1,0],r2_sol[-1,1],r2_sol[-1,2],color="tab:red",marker="o",s=100,label="Alpha Centauri B")#Add a few more bells and whistles
ax.set_xlabel("x-coordinate",fontsize=14)
ax.set_ylabel("y-coordinate",fontsize=14)
ax.set_zlabel("z-coordinate",fontsize=14)
ax.set_title("Visualization of orbits of stars in a two-body system\n",fontsize=14)
ax.legend(loc="upper left",fontsize=14)
最后的图非常清楚地表明,轨道遵循一种可预测的模式,正如两体问题的解决方案所预期的那样。
A Matplotlib plot showing the time evolution of the orbits of the two stars
这里有一个动画展示了轨道的逐步演变。
An animation made in Matplotlib that shows the time evolution step-by-step (code not given in article)
我们还可以做一个可视化,那是从质心的参考系。上面的图像是从空间中某个任意的静止点拍摄的,但是如果我们从系统的质量中心观察这两个物体的运动,我们将会看到一个更加清晰的图像。
首先让我们在每个时间步找到质心的位置,然后从两个物体的位置向量中减去这个向量,找到它们相对于质心的位置。
#Find location of COM
rcom_sol=(m1*r1_sol+m2*r2_sol)/(m1+m2)#Find location of Alpha Centauri A w.r.t COM
r1com_sol=r1_sol-rcom_sol#Find location of Alpha Centauri B w.r.t COM
r2com_sol=r2_sol-rcom_sol
最后,我们可以使用用于绘制前一个视图的代码,通过改变变量来绘制后面的视图。
A Matplotlib plot showing the time evolution of the orbits of the two stars as seen from the COM
如果你坐在通讯器前观察这两个天体,你会看到上面的轨道。从这个模拟中还不清楚,因为时间尺度非常小,但即使这些轨道保持旋转非常轻微。
现在很清楚,它们遵循非常可预测的路径,你可以使用一个函数——也许是椭球的方程——来描述它们在空间中的运动,正如两体系统所预期的那样。
4.三体模型
4.1 代码
现在,为了将我们先前的代码扩展到三体系统,我们必须对参数进行一些添加——添加第三体的质量、位置和速度向量。让我们假设第三颗星的质量等于太阳的质量。
#Mass of the Third Star
m3=1.0 #Third Star#Position of the Third Star
r3=[0,1,0] #m
r3=sci.array(r3,dtype="float64")#Velocity of the Third Star
v3=[0,-0.01,0]
v3=sci.array(v3,dtype="float64")
我们需要更新规范中的质心公式和质心速度公式。
#Update COM formula
r_com=(m1*r1+m2*r2+m3*r3)/(m1+m2+m3)#Update velocity of COM formula
v_com=(m1*v1+m2*v2+m3*v3)/(m1+m2+m3)
对于一个三体系统,我们将需要修改运动方程,以包括另一个物体的存在所施加的额外引力。因此,我们需要在 RHS 上添加一个力项,以表示每一个其他物体对所讨论的物体施加的力。在三体系统的情况下,一个物体将受到其余两个物体施加的力的影响,因此两个力项将出现在 RHS 上。它在数学上可以表示为。
为了在代码中反映这些变化,我们需要创建一个新函数来提供给 odeint 求解器。
def ThreeBodyEquations(w,t,G,m1,m2,m3):
r1=w[:3]
r2=w[3:6]
r3=w[6:9]
v1=w[9:12]
v2=w[12:15]
v3=w[15:18] r12=sci.linalg.norm(r2-r1)
r13=sci.linalg.norm(r3-r1)
r23=sci.linalg.norm(r3-r2)
dv1bydt=K1*m2*(r2-r1)/r12**3+K1*m3*(r3-r1)/r13**3
dv2bydt=K1*m1*(r1-r2)/r12**3+K1*m3*(r3-r2)/r23**3
dv3bydt=K1*m1*(r1-r3)/r13**3+K1*m2*(r2-r3)/r23**3
dr1bydt=K2*v1
dr2bydt=K2*v2
dr3bydt=K2*v3 r12_derivs=sci.concatenate((dr1bydt,dr2bydt))
r_derivs=sci.concatenate((r12_derivs,dr3bydt))
v12_derivs=sci.concatenate((dv1bydt,dv2bydt))
v_derivs=sci.concatenate((v12_derivs,dv3bydt))
derivs=sci.concatenate((r_derivs,v_derivs))
return derivs
最后,我们需要调用 odeint 函数,并向其提供上述函数和初始条件。
#Package initial parameters
init_params=sci.array([r1,r2,r3,v1,v2,v3]) #Initial parameters
init_params=init_params.flatten() #Flatten to make 1D array
time_span=sci.linspace(0,20,500) #20 orbital periods and 500 points#Run the ODE solver
import scipy.integratethree_body_sol=sci.integrate.odeint(ThreeBodyEquations,init_params,time_span,args=(G,m1,m2,m3))
与两体模拟一样,我们需要提取所有三体的位置坐标以进行绘制。
r1_sol=three_body_sol[:,:3]
r2_sol=three_body_sol[:,3:6]
r3_sol=three_body_sol[:,6:9]
可以使用上一节中给出的代码进行一些修改来制作最终的绘图。这些轨道没有可预测的模式,你可以从下面混乱的图表中观察到。
A Matplotlib plot showing the time evolution of the orbits of the three stars
一个动画会让混乱的情节变得更容易理解。
An animation made in Matplotlib that shows the time evolution step-by-step (code not given in article)
这是另一个初始配置的解决方案,你可以观察到,这个解决方案最初似乎是稳定的,但后来突然变得不稳定。
An animation made in Matplotlib that shows the time evolution step-by-step (code not given in article)
你可以试着改变初始条件,看看不同的解决方案。近年来,由于更强大的计算能力,人们发现了许多有趣的三体解,其中一些似乎是周期性的——就像 8 字形解,其中所有三个物体都在平面 8 字形路径上移动。
一些供进一步阅读的参考资料:
- 一篇关于三体的数学描述的小论文。
- 一篇关于平面受限三体解研究的论文(包括图 8 和希尔解)。
我没有在本文中包括动画的代码。如果你想了解更多,你可以发邮件给我或者在推特上联系我。
用潮汐模型和防风草建模
一种解决分类问题的简洁方法
Photo by Karim Ghantous on Unsplash
概观
最近,我完成了与 R 合作的 业务分析在线课程,该课程侧重于与 R 合作的应用数据和商业科学,向我介绍了一些新的建模概念和方法。特别引起我注意的一个是parsnip
,它试图实现一个统一的建模和分析接口(类似于 python 的 scikit-learn
)来无缝访问 r 中的几个建模平台
parsnip
是 RStudio 的 Max Khun (因caret
成名)和 Davis Vaughan 的产物,是tidymodels
的一部分,这是一个不断发展的工具集合,用于探索和迭代建模任务,与tidyverse
有着共同的理念(和一些库)。
尽管有许多软件包处于开发的不同阶段,我还是决定用tidymodels
来“兜一圈”,也就是说,创建并执行一个“整洁的”建模工作流来解决一个分类问题。我的目的是展示在 R 的glm
中安装一个简单的逻辑回归是多么容易,并且只需更改几行代码,就可以使用ranger
引擎快速切换到一个交叉验证的随机森林。
对于这篇文章,我特别关注来自tidymodels
套件的四个不同的库:rsample
用于数据采样和交叉验证,recipes
用于数据预处理,parsnip
用于模型建立和估计,以及yardstick
用于模型评估。
注意重点是建模工作流和库交互。出于这个原因,我将数据探索和特性工程保持在最低限度。
建立
首先,我加载了这个分析所需的包。
**library**(tidymodels)
**library**(skimr)
**library**(tibble)
对于这个项目,我使用来自 IBM 分析社区之一的 IBM Watson Analytics 的 电信客户流失 。该数据包含 7,043 行,每行代表一个客户,21 列代表潜在预测者,提供预测客户行为的信息,并帮助制定有针对性的客户保留计划。
Churn
是因变量,显示上个月离开的客户。数据集还包括每个客户已经注册的服务的详细信息,以及客户账户和人口统计信息。
telco <- readr::**read_csv**("WA_Fn-UseC_-Telco-Customer-Churn.csv")telco %>%
skimr::**skim**()## Skim summary statistics
## n obs: 7043
## n variables: 21
##
## -- Variable type:character -------------
## variable missing complete n min max empty n_unique
## Churn 0 7043 7043 2 3 0 2
## Contract 0 7043 7043 8 14 0 3
## customerID 0 7043 7043 10 10 0 7043
## Dependents 0 7043 7043 2 3 0 2
## DeviceProtection 0 7043 7043 2 19 0 3
## gender 0 7043 7043 4 6 0 2
## InternetService 0 7043 7043 2 11 0 3
## MultipleLines 0 7043 7043 2 16 0 3
## OnlineBackup 0 7043 7043 2 19 0 3
## OnlineSecurity 0 7043 7043 2 19 0 3
## PaperlessBilling 0 7043 7043 2 3 0 2
## Partner 0 7043 7043 2 3 0 2
## PaymentMethod 0 7043 7043 12 25 0 4
## PhoneService 0 7043 7043 2 3 0 2
## StreamingMovies 0 7043 7043 2 19 0 3
## StreamingTV 0 7043 7043 2 19 0 3
## TechSupport 0 7043 7043 2 19 0 3
##
## -- Variable type:numeric ---------------
## variable missing complete n mean sd p0 p25 p50
## MonthlyCharges 0 7043 7043 64.76 30.09 18.25 35.5 70.35
## SeniorCitizen 0 7043 7043 0.16 0.37 0 0 0
## tenure 0 7043 7043 32.37 24.56 0 9 29
## TotalCharges 11 7032 7043 2283.3 2266.77 18.8 401.45 1397.47
## p75 p100
## 89.85 118.75
## 0 1
## 55 72
## 3794.74 8684.8
这里有几件事需要注意:
- customerID 是每一行的唯一标识符。因此,它没有描述或预测能力,需要删除。
- 鉴于 TotalCharges 中缺失值相对较少(只有 11 个),我将它们从数据集中删除。
telco <-
telco %>%
**select**(-customerID) %>%
**drop_na**()
用tidymodels
建模
为了展示tidymodels
框架中的基本步骤,我正在拟合和评估一个简单的逻辑回归模型。
训练和测试分割
rsample
提供了一种简化的方法来创建原始数据的随机训练和测试分割。
**set.seed**(seed = 1972) train_test_split <-
rsample::**initial_split**(
data = telco,
prop = 0.80
) train_test_split
## <5626/1406/7032>
在总共 7,043 个客户中,5,626 个被分配给训练集,1,406 个被分配给测试集。我将它们保存为train_tbl
和test_tbl
。
train_tbl <- train_test_split %>% **training**()
test_tbl <- train_test_split %>% **testing**()
简单的食谱
recipes
包使用了一个烹饪隐喻来处理所有的数据预处理,比如缺失值插补、移除预测值、居中和缩放、一次性编码等等。
首先,我创建了一个recipe
,在这里我定义了我想要应用于我的数据的转换。在这种情况下,我创建了一个简单的方法,将所有的字符变量转换为因子。
然后,我用prep
混合配料*“准备食谱”*。为了简洁起见,这里我在 recipe 函数中包含了 prep 位。
recipe_simple <- **function**(dataset) {
**recipe**(Churn ~ ., data = dataset) %>%
**step_string2factor**(**all_nominal**(), -**all_outcomes**()) %>%
**prep**(data = dataset)
}
注意为了避免数据泄漏(例如:将信息从列车组传输到测试组),数据应仅使用train_tbl
进行“准备”。
recipe_prepped <- **recipe_simple**(dataset = train_tbl)
最后,继续烹饪的比喻,我*“烘焙食谱”*将所有预处理应用于数据集。
train_baked <- **bake**(recipe_prepped, new_data = train_tbl)
test_baked <- **bake**(recipe_prepped, new_data = test_tbl)
符合模型
parsnip
是tidymodels
套件中相对较新的一个,可能是我最喜欢的一个。这个包提供了一个统一的 API,允许访问几个机器学习包,而不需要学习每个单独包的语法。
通过 3 个简单的步骤,您可以:
- 设置您想要安装的型号(这里是一个
logistic regression
)及其模式 (classification
) - 决定使用哪种计算型引擎(本例中为
glm
) - 拼出精确的模型规格以适合(我在这里使用所有变量)以及使用什么数据(烘焙的训练数据集)
logistic_glm <-
**logistic_reg**(mode = "classification") %>%
**set_engine**("glm") %>%
**fit**(Churn ~ ., data = train_baked)
如果您想使用另一个引擎,您可以简单地切换set_engine
参数(对于逻辑回归,您可以从glm
、glmnet
、stan
、spark
和keras
中选择),而parsnip
将负责在幕后为您更改所有其他内容。
性能评价
yardstick
软件包提供了一种简单的方法来计算多个评估指标。但是在我评估我的模型的性能之前,我需要通过将test_baked
数据传递给predict
函数来计算一些预测。
predictions_glm <- logistic_glm %>%
**predict**(new_data = test_baked) %>%
**bind_cols**(test_baked %>% **select**(Churn))head(predictions_glm)
## # A tibble: 6 x 2
## .pred_class Churn
## <fct> <fct>
## 1 Yes No
## 2 No No
## 3 No No
## 4 No No
## 5 No No
## 6 No No
有几个度量可以用来调查分类模型的性能,但为了简单起见,我只关注其中的一个选择:准确性、精确度、召回和 F1_Score 。
所有这些度量(以及更多)都可以通过 混淆矩阵 导出,该表用于描述分类模型对一组真实值已知的测试数据的性能。
就其本身而言,混淆矩阵是一个相对容易理解的概念,因为它显示了假阳性、假阴性、真阳性和真阴性的数量。然而,从它派生的一些度量可能需要一些推理来完全理解它们的意义和用途。
predictions_glm %>%
**conf_mat**(Churn, .pred_class) %>%
**pluck**(1) %>%
**as_tibble**() %>%
**ggplot**(**aes**(Prediction, Truth, alpha = n)) +
**geom_tile**(show.legend = FALSE) +
**geom_text**(**aes**(label = n), colour = "white", alpha = 1, size = 8)
模型的精度是模型正确预测的分数,可以通过将predictions_glm
传递给metrics
函数来轻松计算。然而,准确性并不是一个非常可靠的指标,因为如果数据集不平衡,它将提供误导性的结果。
仅通过基本的数据操作和特征工程,简单的逻辑模型已经达到 80%的准确度。
predictions_glm %>%
**metrics**(Churn, .pred_class) %>%
**select**(-.estimator) %>%
**filter**(.metric == "accuracy") ## .metric .estimate
## accuracy 0.8058321
精度显示模型对误报的敏感程度(即预测客户在他/她实际逗留时离开),而召回查看模型对误报的敏感程度(即预测客户在他/她实际离开时仍在逗留)。
这些是非常相关的商业指标,因为组织对准确预测哪些客户真正有流失的风险特别感兴趣,这样他们就可以有针对性地制定保留策略。与此同时,他们希望尽最大努力留住那些被错误归类为离开的客户,因为他们会留下来。
**tibble**(
"precision" =
**precision**(predictions_glm, Churn, .pred_class) %>%
**select**(.estimate),
"recall" =
**recall**(predictions_glm, Churn, .pred_class) %>%
**select**(.estimate)
) %>%
**unnest**() %>%
**kable**()## precision recall
## 0.8466368 0.9024857
另一个流行的性能评估指标是 F1 得分 ,它是精度和召回的调和平均值。F1 分数在 1 时达到最佳值,具有完美的精度和召回。
predictions_glm %>%
**f_meas**(Churn, .pred_class) %>%
**select**(-.estimator) %>%
**kable**()## .metric .estimate
## f_meas 0.8736696
随机森林
这就是tidymodels
真正的魅力所在。现在,我可以使用这个整洁的建模框架,用ranger
引擎装配一个随机森林模型。
交叉验证设置
为了进一步完善模型的预测能力,我使用来自rsample
的vfold_cv
实现了一个 10 重交叉验证,它再次分割了初始训练数据。
cross_val_tbl <- **vfold_cv**(train_tbl, v = 10)cross_val_tbl
## # 10-fold cross-validation
## # A tibble: 10 x 2
## splits id
## <list> <chr>
## 1 <split [5.1K/563]> Fold01
## 2 <split [5.1K/563]> Fold02
## 3 <split [5.1K/563]> Fold03
## 4 <split [5.1K/563]> Fold04
## 5 <split [5.1K/563]> Fold05
## 6 <split [5.1K/563]> Fold06
## 7 <split [5.1K/562]> Fold07
## 8 <split [5.1K/562]> Fold08
## 9 <split [5.1K/562]> Fold09
## 10 <split [5.1K/562]> Fold10
如果我们进一步观察,我们应该认识到 5626 这个数字,这是初始train_tbl
中的观察总数。在每一轮中,将依次从估计中保留 563 个观察值,并用于验证该折叠的模型。
cross_val_tbl$splits %>%
**pluck**(1)## <5063/563/5626>
为了避免混淆并区分初始训练/测试分割和那些用于交叉验证的分割,Max Kuhn的作者创造了两个新术语:analysis
和assessment
集合。前者是用于递归估计模型的训练数据部分,而后者是用于验证每个估计的部分。
更新配方
注意一个随机森林需要所有数字变量被居中并缩放,所有字符/因子变量被*【虚拟化】*。这很容易通过用这些转换更新配方来完成。
recipe_rf <- **function**(dataset) {
**recipe**(Churn ~ ., data = dataset) %>%
**step_string2factor**(**all_nominal**(), -**all_outcomes**()) %>%
**step_dummy**(**all_nominal**(), -**all_outcomes**()) %>%
**step_center**(**all_numeric**()) %>%
**step_scale**(**all_numeric**()) %>%
**prep**(data = dataset)
}
评估模型
切换到另一种模式再简单不过了!我所需要做的就是将型号改为random_forest
并添加其超参数,将 set_engine 参数改为ranger
,我就准备好了。
我将所有步骤捆绑到一个函数中,该函数估计所有褶皱的模型,运行预测,并返回一个包含所有结果的方便表格。我需要在配方“准备”之前添加一个额外的步骤,将交叉验证分割映射到analysis
和assessment
函数。这将引导 10 次折叠的迭代。
rf_fun <- **function**(split, id, try, tree) {
analysis_set <- split %>% **analysis**()
analysis_prepped <- analysis_set %>% **recipe_rf**()
analysis_baked <- analysis_prepped %>% **bake**(new_data = analysis_set) model_rf <-
**rand_forest**(
mode = "classification",
mtry = try,
trees = tree
) %>%
**set_engine**("ranger",
importance = "impurity"
) %>%
**fit**(Churn ~ ., data = analysis_baked) assessment_set <- split %>% **assessment**()
assessment_prepped <- assessment_set %>% **recipe_rf**()
assessment_baked <- assessment_prepped %>% **bake**(new_data = assessment_set) **tibble**(
"id" = id,
"truth" = assessment_baked$Churn,
"prediction" = model_rf %>%
**predict**(new_data = assessment_baked) %>%
**unlist**()
)
}
性能评价
我剩下要做的就是将公式映射到数据框。
pred_rf <- **map2_df**(
.x = cross_val_tbl$splits,
.y = cross_val_tbl$id,
~ **rf_fun**(split = .x, id = .y, try = 3, tree = 200)
)**head**(pred_rf)
## # A tibble: 6 x 3
## id truth prediction
## <chr> <fct> <fct>
## 1 Fold01 Yes Yes
## 2 Fold01 Yes No
## 3 Fold01 Yes Yes
## 4 Fold01 No No
## 5 Fold01 No No
## 6 Fold01 Yes No
我发现yardstick
有一个非常方便的混淆矩阵summary
函数,它返回一个由 13 个不同指标组成的数组,但是在这种情况下,我想看看我在glm
模型中使用的四个指标。
pred_rf %>%
**conf_mat**(truth, prediction) %>%
**summary**() %>%
**select**(-.estimator) %>%
**filter**(.metric %in%
c("accuracy", "precision", "recall", "f_meas")) %>%
**kable**()## .metric .estimate
## accuracy 0.7979026
## precision 0.8250436
## recall 0.9186301
## f_meas 0.8693254
random forest
型号的性能与简单的logistic regression
不相上下。鉴于我已经完成的非常基本的特征工程,还有进一步改进模型的空间,但这超出了本文的范围。
成交注意事项
tidymodels
的最大优势之一是对分析工作流程每个阶段的灵活性和易用性。创建建模管道轻而易举,通过使用parsnip
改变模型类型和使用recipes
进行数据预处理,您可以轻松重用初始框架,并且很快就可以使用yardstick
检查新模型的性能。
在任何分析中,你通常会审计几个模型,而parsnip
让你不必学习每个建模引擎的独特语法,这样你就可以专注于寻找手头问题的最佳解决方案。
代码库
完整的 R 代码可以在我的 GitHub 简介中找到
参考
- 非常感谢 Bruno Rodrigues 的文章,这篇文章为大评估公式提供了灵感,这是一篇关于与 R 进行整齐交叉验证的教程
- 更感谢本杰明·索伦森用
[parsnip](https://www.benjaminsorensen.me/post/modeling-with-parsnip-and-tidymodels/)
和[tidymodels](https://www.benjaminsorensen.me/post/modeling-with-parsnip-and-tidymodels/)
对造型的深思熟虑 - 关于
[parsnip](https://www.tidyverse.org/articles/2018/11/parsnip-0-0-1/)
的介绍
原载于 2019 年 6 月 22 日https://diegousei . io。
作为无服务器功能的模型
Source: Wikimedia
“生产中的数据科学”第 3 章
我最近出版了我正在编写的关于 leanpub 的书的第三章。本章的目标是让数据科学家能够利用托管服务将模型部署到生产中,并拥有更多开发运维。
用 Python 构建可扩展的模型管道
towardsdatascience.com](/data-science-in-production-13764b11d68e)
无服务器技术使开发人员能够编写和部署代码,而无需担心配置和维护服务器。这种技术最常见的用途之一是无服务器功能,这使得编写可伸缩以适应不同工作负载的代码变得更加容易。在无服务器函数环境中,您编写一个运行时支持的函数,指定一个依赖项列表,然后将该函数部署到生产环境中。云平台负责供应服务器、扩展更多机器以满足需求、管理负载平衡器以及处理版本控制。因为我们已经探索了作为 web 端点的托管模型,所以当您想要快速地将预测模型从原型转移到生产时,无服务器函数是一个很好的工具。
无服务器功能于 2015 年和 2016 年首次在 AWS 和 GCP 上推出。这两个系统都提供了各种可以调用函数的触发器,以及函数可以作为响应触发的大量输出。虽然可以使用无服务器功能来避免编写复杂的代码来将云平台中的不同组件粘合在一起,但我们将在本章中探索一个更窄的用例。我们将编写由 HTTP 请求触发的无服务器函数,为传入的特征向量计算倾向得分,并将预测作为 JSON 返回。对于这个特定的用例,GCP 的云功能更容易启动和运行,但我们将探索 AWS 和 GCP 的解决方案。
在本章中,我们将介绍托管服务的概念,其中云平台负责供应服务器。接下来,我们将介绍使用云功能托管 sklearn 和 Keras 模型。最后,我们将展示如何在 AWS 中使用 Lambda 函数为 sklearn 模型实现相同的结果。我们还将谈到模型更新和访问控制。
3.1 托管服务
自 2015 年以来,云计算领域出现了一场运动,将开发人员从手动配置服务器转变为使用抽象出服务器概念的托管服务。这种新模式的主要优势在于,开发人员可以在试运行环境中编写代码,然后将代码推向生产环境,最大限度地减少运营开销,并且可以根据需要自动扩展与所需工作负载相匹配的基础架构。这使得工程师和数据科学家在 DevOps 中更加活跃,因为基础架构的许多运营问题都由云提供商管理。
手动供应服务器,您ssh
进入机器以设置库和代码,这通常被称为托管的部署,而管理的解决方案则由云平台负责从用户那里抽象出这种担忧。在本书中,我们将涵盖这两个类别的例子。以下是我们将涉及的一些不同的使用案例:
- **Web 端点:**单个 EC2 实例(托管)与 AWS Lambda(托管)
- Docker: 单个 EC2 实例(托管)与 ECS(托管)
- 消息: Kafka(托管)vs AWS Kinesis(托管)
本章将介绍第一个用例,将 web 端点从一台机器迁移到一个弹性环境。我们还将通过一些例子来说明这种区别,比如使用特定的机器配置和手动集群管理来部署 Spark 环境。
无服务器技术和托管服务是数据科学家的强大工具,因为它们使单个开发人员能够构建可扩展到大规模工作负载的数据管道。这是数据科学家可以使用的强大工具,但是在使用托管服务时需要考虑一些权衡。以下是在选择托管和托管解决方案时要考虑的一些主要问题:
- **迭代:**您是在产品上快速原型化还是在生产中迭代系统?
- **延迟:**对于您的 SLA 来说,几秒钟的延迟是可以接受的吗?
- **扩展:**您的系统能否扩展以满足峰值工作负载需求?
- **成本:**您是否愿意为无服务器云支付更多成本?
在初创公司,无服务器技术非常棒,因为你的流量很低,并且有能力快速迭代和尝试新的架构。在一定规模下,当您已经有了调配云服务的内部专业知识时,动态变化和使用无服务器技术的成本可能不再那么有吸引力。在我过去的项目中,最担心的问题是延迟,因为它会影响客户体验。在第 8 章中,我们将触及这个主题,因为托管解决方案通常不能很好地扩展到大型工作负载。
即使您的组织在日常运营中不使用托管服务,作为数据科学家,这也是一项有用的技能,因为这意味着您可以将模型培训与模型部署问题分开。本书的主题之一是模型不需要复杂,但是部署模型可能会很复杂。无服务器功能是展示大规模服务模型能力的一个很好的方法,我们将介绍两个提供这种能力的云平台。
3.2 云功能(GCP)
谷歌云平台为称为云功能的无服务器功能提供了一个环境。该工具的一般概念是,您可以编写针对 Flask 的代码,但利用 GCP 的托管服务为您的 Python 代码提供弹性计算。GCP 是开始使用无服务器功能的绝佳环境,因为它非常符合标准的 Python 开发生态系统,在这里您可以指定需求文件和应用程序代码。
我们将构建可扩展的端点,通过云功能服务于 sklearn 和 Keras 模型。在这种环境下编写函数时,需要注意几个问题:
- **存储:**云函数在只读环境下运行,但是可以写入
/tmp
目录。 - 标签:空格和标签会导致云函数出现问题,如果你使用的是 web 编辑器而不是 Sublime Text 等熟悉的工具,这些可能很难发现。
- 当使用一个需求文件时,根据你的导入来区分
sklearn
和scikit-learn
是很重要的。本章我们会用到 sklearn。
云平台总是在变化,因此本章概述的具体步骤可能会根据这些平台的发展而变化,但部署功能的一般方法应该适用于这些更新。一如既往,我提倡的方法是从一个简单的例子开始,然后根据需要扩展到更复杂的解决方案。在这一节中,我们将首先构建一个 echo 服务,然后探索 sklearn 和 Keras 模型。
3.2.1 回声服务
GCP 为创作云函数提供了一个 web 界面。这个 UI 提供了为函数设置触发器、为 Python 函数指定需求文件以及创作满足请求的 Flask 函数实现的选项。首先,我们将设置一个简单的 echo 服务,它从 HTTP 请求中读入一个参数,并将传入的参数作为结果返回。
在 GCP,您可以直接将云功能设置为 HTTP 端点,而无需配置额外的触发器。要开始设置 echo 服务,请在 GCP 控制台中执行以下操作:
- 搜索“云功能”
- 点击“创建功能”
- 选择“HTTP”作为触发器
- 选择“允许未经验证的调用”
- 为源代码选择“内联编辑器”
- 选择 Python 3.7 作为运行时
该过程的一个示例如图 3.1 所示。在执行这些步骤之后,UI 将为 main.py 和 requirements.txt 文件提供选项卡。需求文件是我们指定库的地方,比如flask >= 1.1.1
,主文件是我们实现函数行为的地方。
图 3.1:创建云函数。
我们将从创建一个简单的 echo 服务开始,该服务从传入的请求中解析出msg
参数,并将该参数作为 JSON 响应返回。为了使用jsonify
功能,我们需要在需求文件中包含flask
库。简单 echo 服务的requirements.txt
文件和main.py
文件如下面的代码片段所示。这里的 echo 函数类似于我们在 2.1.1 节中编写的 echo 服务,主要区别在于我们不再使用注释来指定端点和允许的方法。相反,这些设置现在是使用云功能 UI 指定的。
***# requirements.txt*** flask ***#main.py*** def echo(request):
from flask import jsonify data = {"success": False}
params = request.get_json() if "msg" in params:
data["response"] = str(params['msg'])
data["success"] = True
return jsonify(data)
我们可以通过执行以下步骤将该功能部署到生产环境中:
- 将“要执行的功能”更新为“回显”
- 单击“创建”进行部署
一旦部署了该功能,您可以单击“Testing”选项卡来检查该功能的部署是否如预期的那样工作。您可以指定一个 JSON 对象传递给函数,通过点击“测试函数”来调用函数,如图 3.2 所示。运行这个测试用例的结果是在Output
对话框中返回的 JSON 对象,这表明调用 echo 函数工作正常。
图 3.2:测试云函数。
既然已经部署了该函数,并且我们启用了对该函数的未经身份验证的访问,我们就可以使用 Python 通过 web 调用该函数了。要获取该函数的 URL,请单击“trigger”选项卡。我们可以使用requests
库将 JSON 对象传递给无服务器函数,如下面的代码片段所示。
import requestsresult = **requests.post**(
"https://us-central1-gameanalytics.cloudfunctions.net/echo"
,json = { 'msg': 'Hello from Cloud Function' })
**print**(**result.json**())
运行这个脚本的结果是从无服务器函数返回一个 JSON 有效负载。调用的输出是如下所示的 JSON。
{
'response': 'Hello from Cloud Function',
'success': True
}
我们现在有了一个无服务器功能,提供了一个 echo 服务。为了使用云函数为模型提供服务,我们需要将模型规范保存在无服务器函数可以访问的地方。为了实现这一点,我们将使用云存储将模型存储在分布式存储层中。
云存储(GCS)
GCP 提供了一个名为谷歌云存储(GCS)的弹性存储层,可以用于分布式文件存储,也可以扩展到其他用途,如数据湖。在这一节中,我们将探索利用该服务存储和检索文件以在无服务器功能中使用的第一个用例。GCS 类似于 AWS 的 S3,它在游戏行业被广泛用于构建数据平台。
虽然 GCP 确实提供了与 GCS 交互的 UI,但我们将在这一部分探索命令行界面,因为这种方法对于构建自动化工作流非常有用。GCP 要求与此服务交互时进行身份验证,如果您还没有设置 JSON 凭证文件,请重新阅读 1.1 节。为了使用 Python 与云存储交互,我们还需要安装 GCS 库,使用如下所示的命令:
pip install --user google-cloud-storage
export GOOGLE_APPLICATION_CREDENTIALS=**/**home/ec2-user/dsdemo.json
现在我们已经安装了必备库并设置了凭证,我们可以使用 Python 以编程方式与 GCS 进行交互。在存储文件之前,我们需要在 GCS 上设置一个存储桶。bucket 是分配给存储在 GCS 上的所有文件的前缀,每个 bucket 名称必须是全局唯一的。我们将创建一个名为dsp_model_store
的存储桶,在这里我们将存储模型对象。下面的脚本展示了如何使用create_bucket
函数创建一个新的存储桶,然后使用list_buckets
函数遍历所有可用的存储桶。在运行这个脚本之前,您需要将bucket_name
变量改为一个独特的变量。
from google.cloud import storage
bucket_name = "dsp_model_store"storage_client = **storage.Client**()
**storage_client.create_bucket**(bucket_name)**for** bucket **in** **storage_client.list_buckets**():
**print**(bucket.name)
运行这段代码后,脚本的输出应该是一个单独的 bucket,其名称分配给了bucket_name
变量。我们现在在 GCS 上有了一个可以用来保存文件的路径:gs://dsp_model_storage
。
我们将重用我们在第 2.2.1 节中训练的模型来部署具有云函数的逻辑回归模型。为了将文件保存到 GCS,我们需要分配一个到目的地的路径,如下面的bucket.blob
命令所示,并选择一个要上传的本地文件,该文件被传递给上传函数。
from google.cloud import storagebucket_name = "dsp_model_store"
storage_client = **storage.Client**()
bucket = **storage_client.get_bucket**(bucket_name)blob = **bucket.blob**("serverless/logit/v1")
**blob.upload_from_filename**("logit.pkl")
运行该脚本后,本地文件logit.pkl
现在可以在 GCS 的以下位置获得:
gs:**//**dsp_model_storage/serverless/logit/v1/logit.pkl
虽然像这样直接使用 URIs 来访问文件是可能的,正如我们将在第 6 章用 Spark 探索的,在这一节我们将使用桶名和 blob 路径来检索文件。下面的代码片段显示了如何将模型文件从 GCS 下载到本地存储。我们将模型文件下载到本地路径local_logit.pkl
,然后用这个路径调用pickle.load
来加载模型。
import pickle
from google.cloud import storagebucket_name = "dsp_model_store"
storage_client = **storage.Client**()
bucket = **storage_client.get_bucket**(bucket_name)blob = **bucket.blob**("serverless/logit/v1")
**blob.download_to_filename**("local_logit.pkl")
model = **pickle.load**(**open**("local_logit.pkl", 'rb'))
model
我们现在可以使用 Python 以编程方式将模型文件存储到 GCS 中,并且还可以检索它们,从而使我们能够在云函数中加载模型文件。我们将结合上一章的 Flask 示例,将 sklearn 和 Keras 模型作为云函数。
模型功能
我们现在可以设置一个云函数,通过 web 服务于逻辑回归模型预测。我们将在 2.3.1 节中探索的 Flask 示例的基础上进行构建,并对服务进行一些修改,以便在 GCP 上运行。第一步是在requirements.txt
文件中指定服务请求所需的 Python 库,如下所示。我们还需要 pandas 来建立一个数据框架以进行预测,sklearn 用于应用模型,云存储用于从 GCS 中检索模型对象。
google-cloud-storage
sklearn
pandas
flask
下一步是在main.py
文件中实现我们的模型函数。与之前的一个小变化是现在使用request.get_json()
而不是flask.request.args
来获取params
对象。主要的变化是,我们现在从 GCS 下载模型文件,而不是直接从本地存储中检索文件,因为在使用 UI 工具编写云函数时,本地文件是不可用的。与先前函数的另一个变化是,我们现在为每个请求重新加载模型,而不是在启动时加载一次模型文件。在后面的代码片段中,我们将展示如何使用全局对象来缓存加载的模型。
def **pred**(request):
from google.cloud import storage
import pickle as pk
import sklearn
import pandas as pd
from flask import jsonify data = {"success": False}
params = **request.get_json**() **if** "G1" **in** params:
new_row = { "G1": **params.get**("G1"),"G2": **params.get**("G2"),
"G3": **params.get**("G3"),"G4": **params.get**("G4"),
"G5": **params.get**("G5"),"G6": **params.get**("G6"),
"G7": **params.get**("G7"),"G8": **params.get**("G8"),
"G9": **params.get**("G9"),"G10":**params.get**("G10")} new_x = **pd.DataFrame.from_dict**(new_row,
orient = "index")**.transpose**()
*# set up access to the GCS bucket*
bucket_name = "dsp_model_store"
storage_client = **storage.Client**()
bucket = **storage_client.get_bucket**(bucket_name) *# download and load the model*
blob = **bucket.blob**("serverless/logit/v1")
**blob.download_to_filename**("/tmp/local_logit.pkl")
model = **pk.load**(**open**("/tmp/local_logit.pkl", 'rb'))
data["response"] = **str**(**model.predict_proba**(new_x)[0][1])
data["success"] = True
return **jsonify**(data)
上面的代码片段中需要注意的一点是,/tmp
目录用于存储下载的模型文件。在云函数中,除了这个目录之外,不能写入本地磁盘。一般来说,最好是将对象直接读入内存,而不是将对象拖到本地存储,但是用于从 GCS 读取对象的 Python 库目前需要这种方法。
对于这个函数,我创建了一个名为pred
的新云函数,将要执行的函数设置为pred
,并将该函数部署到生产环境中。我们现在可以从 Python 调用该函数,使用 2.3.1 中的相同方法,URL 现在指向云函数,如下所示:
import requestsresult = **requests.post**(
"https://us-central1-gameanalytics.cloudfunctions.net/pred"
,json = { 'G1':'1', 'G2':'0', 'G3':'0', 'G4':'0', 'G5':'0'
,'G6':'0', 'G7':'0', 'G8':'0', 'G9':'0', 'G10':'0'})
**print**(**result.json**())
Python web 对该函数的请求的结果是一个 JSON 响应,其中包含响应值和模型预测,如下所示:
{
'response': '0.06745113592634559',
'success': True
}
为了提高函数的性能,使响应时间从几秒缩短到几毫秒,我们需要在两次运行之间缓存模型对象。最好避免在函数范围之外定义变量,因为托管函数的服务器可能会因不活动而终止。当用于在函数调用之间缓存对象时,全局变量是对该规则的执行。下面的代码片段展示了如何在 pred 函数的范围内定义一个全局模型对象,以提供一个跨调用的持久对象。在第一次函数调用期间,将从 GCS 检索模型文件,并通过 pickle 加载。在接下来的运行中,模型对象将被加载到内存中,从而提供更快的响应时间。
model = None
def **pred**(request):
global model
**if** not model:
*# download model from GCS*
model = **pk.load**(**open**("/tmp/local_logit.pkl", 'rb')) **if** "G1" **in** params:
*# apply model* return **jsonify**(data)
缓存对象对于创作按需延迟加载对象的响应式模型非常重要。它对于更复杂的模型也很有用,比如 Keras,它需要在调用之间持久化一个张量流图。
Keras 模型
由于云函数提供了一个需求文件,可以用来向函数添加额外的依赖项,因此也可以用这种方法来服务 Keras 模型。我们将能够重用上一节中的大部分代码,我们还将使用 2.3.2 节中介绍的 Keras 和 Flask 方法。考虑到 Keras 库和依赖项的大小,我们需要将该函数可用的内存从 256 MB 升级到 1GB。我们还需要更新需求文件以包含 Keras:
google-cloud-storage
tensorflow
keras
pandas
flask
Keras 模型作为云函数的完整实现如下面的代码片段所示。为了确保用于加载模型的 TensorFlow 图可用于模型的未来调用,我们使用全局变量来缓存模型和图对象。为了加载 Keras 模型,我们需要重新定义在模型训练期间使用的auc
函数,我们将它包含在predict
函数的范围内。我们重用前一节中的相同方法从 GCS 下载模型文件,但是现在使用 Keras 中的load_model
将模型文件从临时磁盘位置读入内存。其结果是一个 Keras 预测模型,它可以延迟获取模型文件,并可以作为一个无服务器功能进行扩展以满足各种工作负载。
model = None
graph = Nonedef **predict**(request):
global model
global graph
from google.cloud import storage
import pandas as pd
import flask
import tensorflow as tf
import keras as k
from keras.models import load_model
from flask import jsonify
def **auc**(y_true, y_pred):
auc = **tf.metrics.auc**(y_true, y_pred)[1]
**k.backend.get_session**()**.run**(
**tf.local_variables_initializer**())
return auc
data = {"success": False}
params = **request.get_json**() *# download model if now cached*
**if** not model:
graph = **tf.get_default_graph**()
bucket_name = "dsp_model_store_1"
storage_client = **storage.Client**()
bucket = **storage_client.get_bucket**(bucket_name) blob = **bucket.blob**("serverless/keras/v1")
**blob.download_to_filename**("/tmp/games.h5")
model = **load_model**('/tmp/games.h5',
custom_objects={'auc':auc})
*# apply the model*
**if** "G1" **in** params:
new_row = { "G1": **params.get**("G1"),"G2": **params.get**("G2"),
"G3": **params.get**("G3"),"G4": **params.get**("G4"),
"G5": **params.get**("G5"),"G6": **params.get**("G6"),
"G7": **params.get**("G7"),"G8": **params.get**("G8"),
"G9": **params.get**("G9"),"G10":**params.get**("G10")} new_x = **pd.DataFrame.from_dict**(new_row,
orient = "index")**.transpose**()
with **graph.as_default**():
data["response"]= **str**(**model.predict_proba**(new_x)[0][0])
data["success"] = True
return **jsonify**(data)
为了测试部署的模型,我们可以重用前一节中的 Python web 请求脚本,并将请求 URL 中的pred
替换为predict
。我们现在已经将深度学习模型部署到生产中。
访问控制
我们在本章中介绍的云功能对 web 是开放的,这意味着任何人都可以访问它们,并有可能滥用端点。一般来说,最好不要启用未经身份验证的访问,而是锁定该功能,以便只有经过身份验证的用户和服务才能访问它们。这个建议也适用于我们在上一章中部署的 Flask 应用程序,在这种情况下,最佳实践是限制对可以使用 AWS 私有 IP 到达端点的服务的访问。
有几种不同的方法来锁定云功能,以确保只有经过身份验证的用户才能访问这些功能。最简单的方法是在函数设置中禁用“允许未经验证的调用”,以防止在开放的 web 上托管该函数。要使用该函数,您需要为该函数设置 IAM 角色和凭证。这一过程包括若干步骤,并可能随着 GCP 的发展而改变。与其走一遍这个过程,不如参考 GCP 文档。
另一种设置强制认证功能的方法是使用 GCP 内的其他服务。我们将在第 8 章探索这种方法,该章介绍了 GCP 的 PubSub 系统,用于在 GCP 的生态系统中生成和消费消息。
模型更新
我们已经使用云函数将 sklearn 和 Keras 模型部署到生产中,但这些函数的当前实现使用静态模型文件,不会随时间而改变。通常有必要随着时间的推移对模型进行更改,以确保模型的准确性不会偏离预期性能太远。我们可以采用几种不同的方法来更新云函数正在使用的模型规范:
- Redeploy: 覆盖 GCS 上的模型文件并重新部署函数将导致函数加载更新的文件。
- **超时:**我们可以在函数中添加一个超时,在经过一定的时间阈值后,比如 30 分钟,重新下载模型。
- **新功能:**我们可以部署一个新功能,比如
pred_v2
并更新调用服务的系统所使用的 URL,或者使用一个负载均衡器来自动化这个过程。 - **模型触发器:**我们可以给函数添加额外的触发器,强制函数手动重新加载模型。
虽然第一种方法最容易实现,并且适用于小规模部署,但是第三种方法(使用负载平衡器将调用指向最新的可用函数)可能是生产系统中最健壮的方法。最佳实践是将日志记录添加到函数中,以便随着时间的推移跟踪预测,从而可以记录模型的性能并识别潜在的偏差。
3.3λ函数(AWS)
AWS 还提供了一个名为 Lambda 的无服务器功能生态系统。AWS Lambda 对于将 AWS 部署中的不同组件粘合在一起非常有用,因为它支持一组丰富的函数输入和输出触发器。虽然 Lambda 确实为构建数据管道提供了一个强大的工具,但当前的 Python 开发环境比 GCP 稍显笨拙。
在这一节中,我们将通过 Lambda 设置一个 echo 服务和一个 sklearn 模型端点。我们不会讨论 Keras,因为在用 AWS 部署函数时,库的大小会导致问题。与过去我们使用 UI 定义函数的部分不同,我们将使用命令行工具为 Lambda 提供函数定义。
回声功能
对于一个简单的函数,您可以使用 Lambda 为创作函数提供的内联代码编辑器。您可以通过在 AWS 控制台中执行以下步骤来创建新功能:
- 在“查找服务”下,选择“Lambda”
- 选择“创建功能”
- 使用“从头开始创作”
- 指定一个名称(例如 echo)
- 选择 Python 运行时
- 点击“创建功能”
运行完这些步骤后,Lambda 会生成一个名为lambda_function.py
的文件。该文件定义了一个名为lambda_handler
的函数,我们将使用它来实现 echo 服务。我们将对文件做一个小的修改,如下所示,它将msg
参数作为响应对象的主体。
def **lambda_handler**(event, context): return {
'statusCode': 200,
'body': event['msg']
}
单击“保存”部署功能,然后单击“测试”测试文件。如果使用默认的测试参数,那么在运行该函数时将会返回一个错误,因为在事件对象中没有msg
键可用。点击“配置测试事件”,并定义使用以下配置:
{
"msg": "Hello from Lambda!"
}
点击“Test”后,您应该会看到执行结果。响应应该是返回状态代码为 200 的回显消息。还有关于函数执行时间(25.8 毫秒)、计费持续时间(100 毫秒)和使用的最大内存(56 MB)的细节。
我们现在有一个运行在 AWS Lambda 上的简单函数。对于向外部系统公开的这个功能,我们需要设置一个 API 网关,这将在 3.3.3 节中介绍。如果需要,该功能将扩展以满足需求,并且部署后不需要服务器监控。要设置一个部署模型的函数,我们需要使用不同的工作流来创作和发布函数,因为 AWS Lambda 目前不支持用内嵌代码编辑器编写函数时定义依赖关系的requirements.txt
文件。为了存储我们想要用 Lambda 函数服务的模型文件,我们将使用 S3 作为模型工件的存储层。
3.3.2 简单存储服务(S3)
AWS 提供了一个名为 S3 的高性能存储层,可用于托管网站的单个文件,存储用于数据处理的大文件,甚至托管成千上万的文件来构建数据湖。现在,我们的用例将存储一个单独的 zip 文件,我们将使用它来部署新的 Lambda 函数。然而,还有许多更广泛的使用案例,许多公司使用 S3 作为数据平台中数据接收的初始端点。
为了使用 S3 来存储我们要部署的功能,我们需要设置一个新的 S3 存储桶,定义一个访问该存储桶的策略,并为设置对 S3 的命令行访问配置凭证。S3 的桶类似于 GCP 的 GCS 桶。
要设置一个存储桶,请浏览到 AWS 控制台并选择“查找服务”下的“S3”。接下来,选择“创建存储桶”在 S3 上设置存储文件的位置。为 S3 桶创建一个唯一的名称,如图 3.3 所示,点击“下一步”,然后点击“创建桶”,完成桶的设置。
图 3.3:在 AWS 上创建一个 S3 桶。
我们现在有了一个在 S3 上存储对象的位置,但是我们仍然需要设置一个用户,然后才能使用命令行工具来读写桶。浏览到 AWS 控制台,在“查找服务”下选择“IAM”。接下来,单击“用户”,然后单击“添加用户”来设置新用户。创建用户名,选择“程序化访问”,如图 3.4 所示。
图 3.4:设置具有 S3 访问权限的用户。
下一步是为用户提供对 S3 的完全访问权限。使用附加现有策略选项并搜索 S3 策略,以便找到并选择AmazonS3FullAccess
策略,如图 3.5 所示。单击“下一步”继续该过程,直到定义了新用户。在此过程结束时,将显示一组凭证,包括访问密钥 ID 和秘密访问密钥。将这些值存储在安全的位置。
图 3.5:为完全 S3 访问选择策略。
设置命令行访问 S3 所需的最后一步是从 EC2 实例运行aws configure
命令。您将被要求提供我们刚刚设置的用户的访问和密钥。为了测试凭据配置是否正确,您可以运行以下命令:
aws configure
aws s3 ls
结果应该包括我们在本节开始时设置的 S3 存储桶的名称。现在我们已经用命令行访问设置了一个 S3 存储桶,我们可以开始编写 Lambda 函数,使用额外的库,比如 pandas 和 sklearn。
模型功能
为了创作一个使用基本 Python 发行版之外的库的 Lambda 函数,您需要设置一个本地环境来定义该函数并包含所有的依赖项。一旦定义了函数,就可以通过创建本地环境的 zip 文件来上传函数,将结果文件上传到 S3,并从上传到 S3 的文件中配置 Lambda 函数。
这个过程的第一步是创建一个目录,其中所有的依赖项都安装在本地。虽然可以在本地机器上执行这个过程,但是我使用了 EC2 实例来提供一个干净的 Python 环境。下一步是安装功能所需的库,分别是熊猫和 sklearn。这些库已经安装在 EC2 实例上,但是需要重新安装在当前目录中,以便包含在我们将上传到 S3 的 zip 文件中。为此,我们可以在 pip 命令的末尾添加-t .
,以便将库安装到当前目录中。在命令行上运行的最后步骤是将我们的逻辑回归模型复制到当前目录中,并创建一个将实现 Lambda 函数的新文件。
mkdir lambda
cd lambda
pip install pandas -t .
pip install sklearn -t .
cp ../logit.pkl logit.pkl
vi logit.py
为我们的逻辑回归模型服务的 Lambda 函数的完整源代码显示在下面的代码片段中。文件的结构应该看起来很熟悉,我们首先全局定义一个模型对象,然后实现一个服务于模型请求的函数。该函数首先解析响应以提取模型的输入,然后在结果数据帧上调用predict_proba
以获得模型预测。然后结果作为包含一个body
键的 dictionary 对象返回。在body
键中定义函数响应很重要,否则 Lambda 在通过 web 调用函数时会抛出异常。
from sklearn.externals import joblib
import pandas as pd
import json
model = **joblib.load**('logit.pkl')
def **lambda_handler**(event, context): *# read in the request body as the event dict*
**if** "body" **in** event:
event = event["body"]
**if** event is not None:
event = **json.loads**(event)
**else**:
event = {}
**if** "G1" **in** event:
new_row = { "G1": event["G1"],"G2": event["G2"],
"G3": event["G3"],"G4": event["G4"],
"G5": event["G5"],"G6": event["G6"],
"G7": event["G7"],"G8": event["G8"],
"G9": event["G9"],"G10":event["G10"]} new_x = **pd.DataFrame.from_dict**(new_row,
orient = "index")**.transpose**()
prediction = **str**(**model.predict_proba**(new_x)[0][1])
return { "body": "Prediction " + prediction }
return { "body": "No parameters" }
与云函数不同,用 Python 编写的 Lambda 函数不是建立在 Flask 库之上的。Lambda 函数不需要单个参数(request
),而是需要将event
和context
对象作为函数参数传入。事件包括请求的参数,上下文提供关于函数执行环境的信息。当使用 Lambda 控制台中的“Test”功能测试 Lambda 函数时,测试配置将作为字典直接传递给该函数的event
对象。但是,当从 web 调用函数时,事件对象是描述 web 请求的字典,请求参数存储在这个 dict 中的body
键中。上面 Lambda 函数的第一步检查函数是直接从控制台调用,还是通过 web 调用。如果该函数是从 web 上调用的,那么该函数会用请求体中的内容覆盖事件字典。
这种方法与 GCP 云函数的一个主要区别是,我们不需要显式定义延迟定义的全局变量。使用 Lambda 函数,您可以定义函数范围之外的变量,这些变量在函数被调用之前保持不变。在模型服务函数之外加载模型对象是很重要的,因为在处理大型工作负载时,每次请求时重新加载模型会变得很昂贵。
为了部署模型,我们需要创建当前目录的 zip 文件,并将文件上传到 S3 上的一个位置。下面的代码片段展示了如何执行这些步骤,然后使用s3 ls
命令确认上传成功。您需要修改路径,以使用您在上一节中定义的 S3 存储桶名称。
zip -r logitFunction.zip .
aws s3 cp logitFunction.zip s3:**//**dsp-ch3-logit/logitFunction.zip
aws s3 ls s3:**//**dsp-ch3-logit/
一旦您的函数以 zip 文件的形式上传到 S3,您就可以返回 AWS 控制台并设置一个新的 Lambda 函数。像以前一样选择“从头开始创作”,并在“代码输入类型”下选择从 S3 上传的选项,从上面的cp
命令指定位置。您还需要定义Handler
,它是 Python 文件名和 Lambda 函数名的组合。logit 功能的配置示例如图 3.6 所示。
图 3.6:在 AWS Lambda 上定义 logit 函数。
确保选择 Python 运行时与用于在 EC2 实例上运行 pip 命令的 Python 版本相同。一旦通过按“Save”部署了该功能,我们就可以使用下面的测试事件定义来测试该功能。
{
"G1": "1", "G2": "1", "G3": "1",
"G4": "1", "G5": "1",
"G6": "1", "G7": "1", "G8": "1",
"G9": "1", "G10": "1"
}
由于模型是在部署功能时加载的,因此测试功能的响应时间应该相对较快。测试功能的输出示例如图 3.7 所示。该函数的输出是一个字典,其中包含一个主体键和作为值的模型输出。该功能执行时间为 110 毫秒,计费持续时间为 200 毫秒
图 3.7:在 AWS Lambda 上测试 logit 函数。
到目前为止,我们只使用 Lambda 的内置测试功能调用了这个函数。为了托管该函数,以便其他服务可以与该函数交互,我们需要定义一个 API 网关。在“设计器”选项卡下,单击“添加触发器”并选择“API 网关”。接下来,选择“创建新的 API”并选择“打开”作为安全设置。设置好触发器后,在设计器布局中应该可以看到一个 API 网关,如图 3.8 所示。
图 3.8:为函数设置 API 网关。
在从 Python 代码调用函数之前,我们可以使用 API 网关测试功能来确保函数设置正确。我在测试这个 Lambda 函数时遇到的一个挑战是,当从 web 和控制台调用这个函数时,请求的结构会发生变化。这就是为什么该函数首先检查event
对象是 web 请求还是带参数的字典。当您使用 API 网关测试该函数时,结果调用将模拟作为 web 请求调用该函数。logit 功能的测试示例如图 3.9 所示。
图 3.9:在 Lambda 函数上测试 post 命令。
既然网关已经设置好了,我们可以使用 Python 从远程主机调用该函数。下面的代码片段展示了如何使用 POST 命令调用函数并显示结果。因为该函数返回一个字符串作为响应,所以我们使用text
属性而不是 json 函数来显示结果。
import requestsresult = **requests.post**("https://3z5btf0ucb.execute-api.us-east-1.
amazonaws.com/default/logit",
json = { 'G1':'1', 'G2':'0', 'G3':'0', 'G4':'0', 'G5':'0',
'G6':'0', 'G7':'0', 'G8':'0', 'G9':'0', 'G10':'0' })**print**(result.text)
我们现在在 AWS Lambda 上部署了一个预测模型,该模型将根据需要自动扩展以匹配工作负载,并且只需最少的维护开销。
与云功能类似,有几种不同的方法可以用来更新部署的模型。然而,对于我们在本节中使用的方法,更新模型需要在开发环境中更新模型文件,重新构建 zip 文件并将其上传到 S3,然后部署模型的新版本。这是一个手动过程,如果您期望频繁的模型更新,那么最好重写函数,以便它直接从 S3 获取模型定义,而不是期望文件已经在本地上下文中可用。最具伸缩性的方法是为函数设置额外的触发器,通知函数是时候加载新模型了。
3.4 结论
无服务器功能是一种托管服务,使开发人员能够部署生产规模的系统,而无需担心基础设施。为了提供这种抽象,不同的云平台确实对必须如何实现功能进行了限制,但这种权衡通常值得这些工具在 DevOps 中实现的改进。虽然像 Cloud Functions 和 Lambda 这样的无服务器技术在运营上可能很昂贵,但它们提供的灵活性可以抵消这些成本。
在本章中,我们使用 GCP 的云功能和 AWS 的 Lambda 产品实现了 echo 服务和 sklearn 模型端点。通过 AWS,我们创建了一个包含所有依赖项的本地 Python 环境,然后将生成的文件上传到 S3 来部署功能,而在 GCP,我们直接使用在线代码编辑器创作功能。最佳系统的使用可能取决于您的组织正在使用的云提供商,但在构建新系统原型时,拥有使用多个无服务器功能生态系统的实践经验是非常有用的。
本·韦伯是 Zynga 的一名杰出的数据科学家。我们正在招聘!
作为 Web 端点的模型
Source: https://www.maxpixel.net/Internet-Hexagon-Icon-Networks-Honeycomb-Hexagons-3143432
《生产中的数据科学》摘录
在生产中的数据科学的第二章中,我将讨论如何将预测模型设置为 web 端点。这是一项有用的技能,因为它使数据科学家能够从批处理模型应用程序(如输出 CSV 文件)转移到托管其他应用程序可以实时使用的模型。我以前使用 Keras 写过这个过程,但是结果不是一个可伸缩的解决方案。本文的目标是关注 Python 堆栈中可以用来托管预测模型的工具,该模型可以扩展以满足需求。
这篇文章演示了如何使用 Keras 构建的深度学习模型来设置端点以服务于预测。它…
towardsdatascience.com](/deploying-keras-deep-learning-models-with-flask-5da4181436a2)
在这篇文章中,我将展示如何使用一个简单的 Flask 应用程序,并将其作为服务部署在开放的 web 上,这对于构建数据科学组合非常有用。为了实现这个目标,我们将使用 Gunicorn 和 Heroku。我写这本书的目的之一是展示如何从头开始设置环境,这通常包括在 EC2 上设置一个实例。我已经讨论过在前一篇文章和书籍样本中设置机器。
瓶
Flask 是一个多功能的工具,用于使用 Python 构建 web 应用程序和服务。虽然可以直接使用 Flask 创建完整的 web 应用程序,但我通常使用它来创建消费和生成 JSON 响应的端点,而使用破折号来创建更多的 UI 应用程序。
在这篇文章中,我们将创建一个返回 JSON 响应的最小服务,如果指定的话,它将回显粘贴的 in msg
参数。返回的字典包括回显的消息和一个指定消息是否被传递给服务的success
值。第一步是安装 Flask 和 Gunicorn。
pip install flask
pip install gunicorn
接下来,我们将创建一个名为 echo.py 的文件,它使用 Flask 来实现这个 echo 功能。这个 web 应用程序的完整代码如下面的代码片段所示。该脚本首先加载 Flask 库,然后实例化一个 Flask 对象。接下来,@app.route
注释用于定义一个被分配了路由位置和一组 HTTP 方法的函数。在这种情况下,我们在根 URL 处定义函数,/
。该函数定义了一个用于返回 JSON 响应的 dictionary 对象。该函数使用request.json
和request.args
对象检查传入的参数。如果提供了传入的消息,则将它添加到响应字典中,并将成功值设置为 true。最后一步调用应用程序对象上的run
来启动 Flask 会话。这是我们在直接使用 Flask 作为端点时想要做的事情,而不是在使用 Gunicorn 时。我们将主机设置为0.0.0.0
来启用远程连接。
**# load Flask** import flask
app = flask.Flask(__name__)**# define a predict function as an endpoint** [@app](http://twitter.com/app).route("/", methods=["GET","POST"])
def predict():
data = {"success": False}
**# check for passed in parameters** params = flask.request.json
if params is None:
params = flask.request.args
**# if parameters are found, echo the msg parameter** **if** **"msg"** **in** **params****.keys():** data["response"] = params.get("msg")
data["success"] = True
**# return a response in json format** return flask.jsonify(data)
**# start the flask app, allow remote connections** if __name__ == '__main__':
app.run(host='0.0.0.0')
我们现在可以使用python echo.py
运行应用程序。运行该命令的结果如下所示。
python3 echo.py
* Serving Flask app "echo" (lazy loading)
* Environment: production
WARNING: This is a development server.
Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on [http://0.0.0.0:5000/](http://0.0.0.0:5000/) (Press CTRL+C to quit)
您可以使用 web 浏览器或 Python 与服务进行交互。如果使用 AMI 实例,您需要使用机器的公共 IP 并打开端口 5000。该服务可以与 get 和 post 方法一起使用,如下所示。如果您需要向服务发送许多参数,那么 post 方法是生产环境的首选方法。
import requestsresult = requests.get("[http://localhost:5000/?msg=Hello](http://52.90.199.190:5000/?msg=Hello) from URL!")
print(result.json())result = requests.get("[http://](http://52.90.199.190:5000/)[localhost](http://52.90.199.190:5000/?msg=Hello)[:5000/](http://52.90.199.190:5000/)",
params = { 'msg': 'Hello from params' })
print(result.json())result = requests.post("[http://localhost:5000/](http://52.90.199.190:5000/)",
json = { 'msg': 'Hello from data' })
print(result.json())
该脚本的结果如下所示:
{‘response’: ‘Hello from URL!’, ‘success’: True}
{‘response’: ‘Hello from params’, ‘success’: True}
{‘response’: ‘Hello from data’, ‘success’: True}
我们现在有了一个可用于托管预测模型的 web 端点,但我们当前的方法无法扩展。开发模式中的 Flask 使用单线程方法,并建议使用 WSGI 服务器来处理生产工作负载。
格尼科恩
我们可以使用 Gunicorn 为 Flask 应用程序提供一个 WSGI 服务器。使用 gunicorn 有助于将我们在 Flask 中实现的应用程序的功能与应用程序的部署分开。Gunicorn 是一个轻量级的 WSGI 实现,与 Flask 应用程序配合良好。
直接从使用 Flask 切换到使用 Gunicorn 来运行 web 服务是很简单的。运行应用程序的新命令如下所示。请注意,我们正在传入一个绑定参数来启用到服务的远程连接。
gunicorn --bind 0.0.0.0 echo:app
命令行上的结果如下所示。与以前的主要区别是,我们现在在端口 8000 而不是端口 5000 上连接服务。如果您想测试服务,您需要在端口 8000 上启用远程访问。
gunicorn --bind 0.0.0.0 echo:app
[INFO] Starting gunicorn 19.9.0
[INFO] Listening at: [http://0.0.0.0:8000](http://0.0.0.0:8000) (10310)
[INFO] Using worker: sync
[INFO] Booting worker with pid: 10313
同样,我们可以用 Python 测试服务,如下所示。
result = requests.get("[http://](http://52.90.199.190:5000/)[localhost](http://52.90.199.190:5000/?msg=Hello)[:8000/](http://52.90.199.190:5000/)",
params = { 'msg': 'Hello from Gunicorn' })
print(result.json())
赫罗库
现在我们有了一个 gunicorn 应用程序,我们可以使用 Heroku 将它托管在云中。Python 是这种云环境支持的核心语言之一。使用 Heroku 的好处在于,你可以免费托管应用程序,这对于展示数据科学项目来说非常棒。第一步是在网站上建立一个账户:https://www.heroku.com/
接下来,我们将通过运行如下所示的命令,为 Heroku 设置命令行工具。在 AMI EC2 实例上设置 Heroku 时,可能会有些复杂。这些步骤下载一个版本,提取它,并安装一个附加的依赖项。
wget [https://cli-assets.heroku.com/heroku-linux-x64.tar.gz](https://cli-assets.heroku.com/heroku-linux-x64.tar.gz)
unzip heroku-linux-x64.tar.gz
tar xf heroku-linux-x64.tar
sudo yum -y install glibc.i686
/home/ec2-user/heroku/bin/heroku --version
最后一步输出安装的 Heroku 版本。我得到了以下输出:heroku/7.29.0 linux-x64 node-v11.14.0
接下来,我们将使用 CLI 设置一个新的 Heroku 项目:
/home/ec2-user/heroku/bin/heroku login
/home/ec2-user/heroku/bin/heroku create
这将创建一个唯一的应用程序名称,如obscure-coast-69593
。在部署到生产环境之前,最好在本地测试设置。为了测试设置,您需要安装django
和django-heroku
包。
pip install --user django
sudo yum install gcc python-setuptools python-devel postgresql-devel
sudo easy_install psycopg2
pip install --user django-heroku
要开始设置项目,我们可以从 Heroku 提供的示例 Python 项目开始。
sudo yum install git
git clone [https://github.com/heroku/python-getting-started.git](https://github.com/heroku/python-getting-started.git)
cd python-getting-started
接下来,我们将对项目进行更改。我们将我们的 echo.py 文件复制到目录中,将 Flask 添加到 requirements.txt 文件的依赖项列表中,覆盖在 Procfile 中运行的命令,然后调用heroku local
在本地测试配置。
cp ../echo.py echo.py
echo 'flask' >> requirements.txt
echo "web: gunicorn echo:app" > Procfile
/home/ec2-user/heroku/bin/heroku local
您应该会看到如下所示的结果:
/home/ec2-user/heroku/bin/heroku local
[OKAY] Loaded ENV .env File as KEY=VALUE Format
[INFO] Starting gunicorn 19.9.0
[INFO] Listening at: [http://0.0.0.0:5000](http://0.0.0.0:5000) (10485)
[INFO] Using worker: sync
[INFO] Booting worker with pid: 10488
和以前一样,我们可以使用浏览器或 Python 调用来测试端点,如下所示。在测试配置中,默认情况下使用端口 5000。
result = requests.get("[http://](http://52.90.199.190:5000/)[localhost](http://52.90.199.190:5000/?msg=Hello)[:5000/](http://52.90.199.190:5000/)",
params = { 'msg': 'Hello from Heroku Local' })
print(result.json())
最后一步是将服务部署到生产环境中。git 命令用于将结果推送到 Heroku,Heroku 会自动发布应用程序的新版本。最后一个命令告诉 Heroku 扩展到单个工人,这是免费的。
git add echo.py
git commit .
git push heroku master
/home/ec2-user/heroku/bin/heroku ps:scale web=1
现在我们可以调用端点,它有一个正确的 URL,是安全的,可以用来公开共享数据科学项目。
result = requests.get("[https://obscure-coast-69593.herokuapp.com](https://obscure-coast-69593.herokuapp.com)",
params = { 'msg': 'Hello from Heroku Prod' })
print(result.json())
我想展示的最后一步是将图像传递到这个端点的能力,这在构建深度学习模型时是一项有用的任务。下面的代码使用 Python 图像库将图像编码为字符串,将结果传递给 echo 服务,然后使用 matplotlib 呈现结果。
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
import io
import base64image = open("luna.png", "rb").read()
encoded = base64.b64encode(image)
result = requests.get("[https://obscure-coast-69593.herokuapp.com](https://obscure-coast-69593.herokuapp.com)", json = {'msg': encoded})encoded = result.json()['response']
imgData = base64.b64decode(encoded)
plt.imshow( np.array(Image.open(io.BytesIO(imgData)) ))
结果如下图所示。我们能够对我姻亲的猫的图像进行编码,将其发送到 echo 服务,并呈现返回的图像。这种方法可以用来建立图像分类器作为网络端点。
结论
这篇文章展示了如何使用 Heroku 将 Flask web 应用程序部署到生产环境中。虽然该应用程序是一个简单的 echo 服务,但它确实提供了构建更复杂应用程序所需的脚手架,例如使用 Keras 来识别图像中是否包含猫或狗。下一次,我将介绍使用 AWS Lambda 和 Google Cloud 函数作为构建可伸缩模型端点的替代方法。
现代并行和分布式 Python:Ray 快速教程
一个快速、简单的分布式应用框架
Ray 是并行分布式 Python 的开源项目。
并行和分布式计算是现代应用的主要内容。我们需要利用多个内核或多台机器来加速应用程序或大规模运行它们。用于抓取网络和响应搜索查询的基础设施不是在某人的笔记本电脑上运行的单线程程序,而是相互通信和交互的服务集合。
The cloud promises unlimited scalability in all directions (memory, compute, storage, etc). Realizing this promise requires new tools for programming the cloud and building distributed applications.
这篇文章将描述如何使用 Ray 轻松构建可以从笔记本电脑扩展到大型集群的应用程序。
为什么是雷?
很多教程解释了如何使用 Python 的多处理模块。不幸的是,多处理模块在处理现代应用需求的能力上受到严重限制。这些要求包括以下内容:
雷解决了所有这些问题,让简单的事情变得简单,让复杂的行为成为可能。
必要的概念
传统编程依赖于两个核心概念:函数和类。使用这些构建模块,编程语言允许我们构建无数的应用程序。
然而,当我们将应用程序迁移到分布式环境时,概念通常会发生变化。
一方面,我们有像 OpenMPI 、 Python 多处理和 ZeroMQ 这样的工具,它们提供了发送和接收消息的底层原语。这些工具非常强大,但它们提供了不同的抽象,因此单线程应用程序必须从头开始重写才能使用它们。
另一方面,我们有特定领域的工具,如用于模型训练的 TensorFlow ,用于数据处理和 SQL 的 Spark ,以及用于流处理的 Flink 。这些工具提供了更高层次的抽象,如神经网络、数据集和流。然而,因为它们不同于串行编程所用的抽象,所以应用程序必须从头开始重新编写才能利用它们。
Tools for distributed computing on an axis from low-level primitives to high-level abstractions.
雷占据了独特的中间地带。而不是引入新的概念。Ray 采用现有的函数和类的概念,并将它们作为任务和角色翻译到分布式设置中。这种 API 选择允许串行应用并行化,而无需重大修改。
起始光线
ray.init()
命令启动所有相关的光线过程。在集群上,这是唯一需要更改的行(我们需要传入集群地址)。这些流程包括以下内容:
- 并行执行 Python 函数的多个工作进程(大约每个 CPU 内核一个工作进程)。
- 将“任务”分配给工人(和其他机器)的调度程序。任务是由 Ray 调度的工作单元,对应于一个函数调用或方法调用。
- 一个共享内存对象存储库,用于在工作人员之间高效地共享对象(无需创建副本)。
- 一个内存数据库,用于存储在机器出现故障时重新运行任务所需的元数据。
与线程相反,Ray workers 是独立的进程,因为由于全局解释器锁,Python 中对多线程的支持非常有限。
任务并行
为了将 Python 函数f
转换成“远程函数”(一个可以远程异步执行的函数),我们用@ray.remote
装饰器声明该函数。然后通过f.remote()
的函数调用将立即返回 future(一个 future 是对最终输出的引用),实际的函数执行将在后台进行(我们将这个执行称为任务)。
Code for running parallel tasks in Python.
因为对f.remote(i)
的调用会立即返回,所以只需运行那行代码四次,就可以并行执行f
的四个副本。
任务相关性
任务也可以依赖于其他任务。下面,multiply_matrices
任务使用两个create_matrix
任务的输出,所以直到前两个任务执行完之后它才会开始执行。前两个任务的输出将自动作为参数传递给第三个任务,未来将被替换为它们相应的值)。以这种方式,任务可以与任意的 DAG 依赖项组合在一起。
Code illustrating three tasks, where the third task depends on the outputs of the first two.
有效地聚合值
任务相关性可以用更复杂的方式来使用。例如,假设我们希望将 8 个值聚合在一起。这个例子使用了整数加法,但是在许多应用程序中,在多台机器上聚合大向量可能是一个瓶颈。在这种情况下,更改一行代码就可以将聚合的运行时间从线性聚合更改为对数聚合。
The dependency graph on the left has depth 7. The dependency graph on the right has depth 3. The computations yield the same result, but the one on the right is much faster.
如上所述,要将一个任务的输出作为后续任务的输入,只需将第一个任务返回的未来作为参数传递给第二个任务。Ray 的调度程序会自动考虑这种任务依赖性。在第一个任务完成之前,第二个任务不会执行,第一个任务的输出会自动发送到正在执行第二个任务的机器上。
Code for aggregating values in a linear fashion versus in a tree-structured fashion.
上面的代码非常清晰,但是请注意,这两种方法都可以使用while
循环以更简洁的方式实现。
A more concise implementation of the two aggregation schemes. The only difference between the two blocks of code is whether the output of “add.remote” is placed at the front or the back of the list.
从班级到演员
在不使用类的情况下编写有趣的应用程序是很有挑战性的,这在分布式环境下和在单核环境下都是如此。
Ray 允许你获取一个 Python 类并用@ray.remote
decorator 声明它。每当实例化该类时,Ray 都会创建一个新的“actor ”,它是一个在集群中某个地方运行的进程,并保存着该对象的副本。对该角色的方法调用变成了在角色进程上运行的任务,并且可以访问和改变角色的状态。以这种方式,参与者允许在多个任务之间共享可变状态,而远程函数不能。
个体参与者串行执行方法(每个个体方法都是原子的),因此不存在竞争条件。并行可以通过创建多个参与者来实现。
Code example for instantiating a Python class as an actor.
上面的例子是 actors 最简单的用法。第Counter.remote()
行创建了一个新的 actor 进程,它有一个Counter
对象的副本。对c.get_value.remote()
和c.inc.remote()
的调用在远程角色进程上执行任务,并改变角色的状态。
演员处理
在上面的例子中,我们只调用了主 Python 脚本中 actor 的方法。actor 最强大的方面之一是我们可以将句柄传递给 actor ,这允许其他 actor 或其他任务调用同一个 actor 上的所有方法。
以下示例创建了一个存储消息的执行元。几个 worker 任务重复地将消息推送到 actor,主 Python 脚本定期读取消息。
Code for invoking methods on an actor from multiple concurrent tasks.
演员是极其厉害的。它们允许您获取一个 Python 类,并将其实例化为一个微服务,可以从其他参与者和任务甚至其他应用程序查询该微服务。
任务和参与者是 Ray 提供的核心抽象。这两个概念非常通用,可以用来实现复杂的应用程序,包括 Ray 的内置库,用于强化学习、超参数调整、加速熊猫等等。
了解更多关于 Ray 的信息
通过将时序数据库与机器学习相结合,实现 IT 基础设施监控的现代化
让我们探讨一下 IT 基础设施的复杂性和脆弱性,以及如何使用时间序列数据库和机器学习的组合来构建现代 IT 基础设施监控解决方案。
IT 基础设施:复杂而脆弱
iCloud 最近加入了谷歌、脸书、亚马逊等遭遇大规模云中断的主要公司的行列。查看 ZDNet 的系列文章详细描述了断电情况。此次中断导致 YouTube、Snapchat 和 Gmail 等网站中断。
iCloud 的失败也影响了他们所有的第三方应用和 ApplePay,响彻全球。我们已经很快接受了云,认为它比内部基础设施更有弹性,所以这个消息发人深省。它还显示了 It 基础架构的脆弱性,包括基于云的基础架构和内部基础架构,这些基础架构为我们依赖软件的世界提供了动力,这个世界现在包括娱乐和个人以及专业联系。
IT 基础设施包含所有相关组件,包括网络、安全、存储、操作系统、集线器链接和计算机。每个组件都有许多子组件,如内存、中央处理器等。除此之外,云的采用和虚拟化增加了复杂性。软件定义的网络可以快速自动地更改基础架构,这使得跟踪哪个工作负载驻留在哪个虚拟机上以及将它们与物理服务器相关联变得更加困难。测量一台机器在任何给定时刻的性能影响都是一个严峻的挑战!
在这个数字时代,公司(和人!)依赖于良好的基础设施来支持其关键功能,如通信、融资等。停机时间对企业来说代价高昂且具有破坏性。这增加了各种规模的公司重新考虑其基础设施监控策略的压力。您如何有效地监控随着业务增长而不断扩展的分散 IT 组件?
利用时序数据库和机器学习进行预测性基础设施监控
当前的监控工具通常是特定于供应商的孤立工具,缺乏对整个基础架构环境的全面了解。随着数据量和数据种类的不断增长,它们可能会产生瓶颈和盲点。这些挑战需要一种新的设计,一种为分布式计算、数据收集和大规模并行处理而构建的设计,一种可以从历史中学习并预测停机时间的设计。
设计支持预测分析的现代 IT 基础设施监控平台的构建模块包括以下多步流程:
- 数据采集和预处理:
a.从各种来源实时收集数据,包括系统日志、网络流量日志、事件日志以及吞吐量、IOPS 和延迟等指标。
这一步很有挑战性,因为应用程序会生成各种格式的数据,以不同的频率存储数据,而且获取数据的 API 也因应用程序而异。您将需要一个支持大规模数据收集的框架,支持多种协议和数据格式。像 Nuclio 、 OpenFaas 、 AWS Lambda 、 Azure Functions 等事件驱动框架通过处理数据收集所需的所有操作性繁重工作来应对这些挑战。请记住,除了实时数据之外,还需要为查找表(如设备表)连续收集批量数据。
b.动态丰富数据,为更快的分析准备数据,并通过利用外部数据源提供更多见解。这需要一个非常快速的机制来运行实时流数据和额外的数据集连接,同时保持每秒数百万个事件。
c.在时序数据库中存储大量时序数据(TSDB)。当每秒钟的事件数量很大时,这种方法特别有效。数据是不可变的,记录是插入的,而不是更新的,为系统中的每次更改创建一个新行。TSDBs 是处理大量实时数据的有效解决方案,还提供了针对基于时间的查询而优化的查询引擎。也就是说,并非所有数据都应该存储在 TSDB 中。客户通常有用于数据丰富的查找表,并且需要在两种格式之间连接数据。在这些情况下,将查找表存储在关系表或键值表结构中更有意义,同时为键访问优化实时运行的连接,以便保持负载。
2。浏览数据 你可以用各种方式浏览数据。使用 Prometheus(流行的开源 TSDB)来利用其查询引擎进行基于时间的查询是非常常见的。然而,Prometheus 不可扩展,无法存储或分析大型数据集。需要具有可扩展数据层的分布式数据平台来应对这些挑战,在使用 Prometheus 接口的同时存储和分析数据。然后,您可以使用可视化工具在 Prometheus 上生成交互式报告(例如 Grafana)。
3。使用 ML: 获得可行的见解最终目标是获得服务于业务需求的见解。传统上,这是通过基于规则(例如基于规则)的工具来实现的,这些工具根据特定的事件集触发警报和事件。例如,如果 5 分钟时间范围内的平均温度超过某个阈值,则通知管理员。但客户正在寻找更先进的机器学习和预测分析解决方案,这些解决方案可以根据大量指标来识别异常,以找到相关性。虽然机器学习和预测分析支持的不仅仅是基于规则的系统,但企业仍然面临着涉及规模和性能的运营挑战。
这听起来很复杂吗?对我们来说幸运的是,有些人提前考虑了现代解决方案。今天的端到端数据科学平台使得创建您自己的智能解决方案的过程变得更加容易。强大的数据科学平台,如 Iguazio ,提供集成工具来设计您自己的实时基础设施监控解决方案,这种解决方案超越了传统的反应式 TSDB。
能够监控现代 IT 基础设施的数据科学平台将提供:
●复杂的实时预测
●关联时序数据、运行算法、生成交互式仪表盘和行动建议的工具
●强劲的 TSDB 发动机,兼容普罗米修斯和其他流行的 TSDB API
●在云中、内部或边缘运行的多功能性
●用户可以不受限制地灵活使用喜爱的分析框架(如 Spark)或仪表板工具(如 Grafana)
●以更便宜、简化的方式容纳大量数据并添加相关历史数据
●支持多种数据模型
●在不损害隐私和数据管理的情况下实现安全数据共享;使用身份验证、数据安全、“暗站点”和离线部署
云中断的混乱消息强调了为您的 IT 基础设施制定复杂的监控策略的重要性。一篇商业内幕文章暗示谷歌高调的停机将影响他们的市场份额。谷歌在可靠性上推销自己,这次中断可能会让该公司落后于其劲敌亚马逊和微软。
摩丁:通过改变一行代码来加速你的熊猫功能
想获得灵感?快来加入我的 超级行情快讯 。😎
Pandas 是用 Python 处理数据的首选库。在处理不同类型和大小的数据时,它易于使用并且非常灵活。它有大量不同的功能,使得处理数据变得轻而易举。
Popularity of various Python packages over time. Source
但是有一个缺点:Pandas 对于大型数据集来说速度很慢。
默认情况下,Pandas 使用单个 CPU 内核作为单个进程执行其功能。这对于较小的数据集来说很好,因为您可能不会注意到速度上的很大差异。但是,随着数据集越来越大,要进行的计算越来越多,当只使用单核时,速度开始受到严重影响。它一次只对一个可能有百万甚至十亿行的数据集进行一次计算。
然而,大多数为数据科学制造的现代机器至少有两个 CPU 内核。这意味着,以 2 个 CPU 内核为例,当使用 Pandas 时,默认情况下,50%或更多的计算机处理能力不会做任何事情。当您使用 4 核(现代英特尔 i5)或 6 核(现代英特尔 i7)时,情况会变得更糟。熊猫的设计根本就不能有效利用这种计算能力。
Modin 是一个新的库,旨在通过在系统的所有可用 CPU 核心之间自动分配计算来加速 Pandas。有了它,摩丁声称能够让任何大小的熊猫数据帧的系统上的 CPU 核心数接近线性加速。
让我们看看它是如何工作的,并浏览几个代码示例。
摩丁是如何与熊猫进行并行处理的
给定 Pandas 中的数据帧,我们的目标是以最快的方式对其执行某种计算或处理。这可能是用.mean()
取每一列的平均值,用groupby
分组数据,用drop_duplicates()
删除所有重复项,或者任何其他内置的熊猫函数。
在上一节中,我们提到了 Pandas 如何只使用一个 CPU 内核进行处理。自然,这是一个很大的瓶颈,特别是对于较大的数据帧,资源的缺乏会真正显现出来。
理论上,并行化计算就像在每个可用 CPU 内核的不同数据点上应用该计算一样简单。对于 Pandas 数据帧,一个基本的想法是将数据帧分成几个部分,你有多少个 CPU 核就有多少个部分,让每个 CPU 核在它自己的部分上运行计算。最后,我们可以汇总结果,这是一个计算量很小的操作。
How a multi-core system can process data faster. For a single-core process (left), all 10 tasks go to a single node. For the dual-core process (right), each node takes on 5 tasks, thereby doubling the processing speed
这正是摩丁所做的。它将你的数据帧分割成不同的部分,这样每个部分都可以发送到不同的 CPU 内核。Modin 在行和列上划分数据帧。这使得摩丁的并行处理可以扩展到任何形状的数据帧。
想象一下,如果给你一个多列少行的数据帧。有些库只执行跨行的分区,在这种情况下效率会很低,因为我们的列比行多。但是对于 Modin,由于分区是在两个维度上完成的,所以并行处理对于所有形状的数据帧都是有效的,无论它们是更宽(许多列)、更长(许多行),还是两者都是。
A Pandas DataFrame (left) is stored as one block and is only sent to one CPU core. A Modin DataFrame (right) is partitioned across rows and columns, and each partition can be sent to a different CPU core up to the max cores in the system
上图是一个简单的例子。Modin 实际上使用了一个分区管理器,它可以根据操作类型改变分区的大小和形状。例如,可能有一个操作需要整行或整列。在这种情况下,分区管理器将以它能找到的最佳方式执行分区并分配给 CPU 内核。它很灵活。
为了执行并行处理,摩丁可以使用 Dask 或者 Ray。它们都是带有 Python APIs 的并行计算库,你可以选择其中一个在运行时与 Modin 一起使用。Ray 将是目前最安全的,因为它更稳定 Dask 后端是实验性的。
但是,嘿,这是足够的理论。让我们来看看代码和速度基准!
基准测试摩丁速度
安装和运行 Modin 最简单的方法是通过 pip。以下命令安装 Modin、Ray 和所有相关的依赖项:
pip install modin[ray]
对于我们下面的例子和基准,我们将使用 Kaggle 的 CS:GO 竞争匹配数据 。CSV 的每一行都包含关于 CS:GO 比赛中一轮比赛的数据。
我们将坚持使用目前最大的 CSV 文件(有几个)进行实验,名为esea_master_dmg_demos.part1.csv
,1.2GB。有了这样的大小,我们应该能够看到熊猫如何变慢,以及摩丁如何帮助我们。对于测试,我将使用一个i7–8700k CPU,它有 6 个物理内核和 12 个线程。
我们要做的第一个测试就是用我们的好工具read_csv()
简单地读取数据。熊猫和摩丁的密码完全一样。
为了测量速度,我导入了time
模块,并在read_csv()
前后放了一个time.time()
。结果,熊猫从 CSV 加载数据到内存用了 8.38 秒,而摩丁用了 3.22 秒。这是 2.6 倍的加速。对于仅仅改变导入语句来说,这还不算太糟糕!
让我们在数据帧上做几个更复杂的处理。在 Pandas 中,连接多个数据帧是一个常见的操作——我们可能有几个或更多包含我们的数据的 CSV 文件,然后我们必须一次读取一个并进行连接。我们可以通过熊猫和摩丁中的pd.concat()
函数轻松做到这一点。
我们希望 Modin 能很好地处理这种操作,因为它处理了大量的数据。代码如下所示。
在上面的代码中,我们将数据帧连接了 5 次。熊猫能够在 3.56 秒内完成拼接操作,而摩丁在 0.041 秒内完成,加速 86.83 倍!虽然我们只有 6 个 CPU 内核,但数据帧的分区对速度有很大帮助。
常用于数据帧清理的 Pandas 函数是.fillna()
函数。此函数查找数据帧中的所有 NaN 值,并用您选择的值替换它们。那里有很多行动。Pandas 必须遍历每一行和每一列来找到 NaN 值并替换它们。这是一个应用摩丁的绝佳机会,因为我们正在多次重复一个非常简单的操作。
这一次,熊猫用了 1.8 秒跑完了.fillna()
,而摩丁用了 0.21 秒,加速了 8.57 倍!
警告和最终基准
所以摩丁总是这么快吗?
嗯,不总是这样。
在某些情况下,Pandas 实际上比 Modin 更快,即使在这个具有 5,992,097(近 600 万)行的大数据集上。下表显示了我进行的一些实验中熊猫和摩丁的运行时间。
正如你所看到的,在一些操作中,Modin 的速度明显更快,通常是读入数据和查找值。熊猫的其他操作,如执行统计计算要快得多。
使用摩丁的实用技巧
摩丁仍然是一个相当年轻的图书馆,并在不断发展和扩大。因此,熊猫的所有功能还没有完全加速。如果你尝试使用一个尚未加速的函数,它将默认为 Pandas,这样就不会有任何代码错误。关于 Modin 支持的 Pandas 方法的完整列表,请参见本页。
默认情况下,Modin 将使用机器上所有可用的 CPU 内核。在某些情况下,您可能希望限制 Modin 可以使用的 CPU 内核的数量,特别是如果您希望在其他地方使用该计算能力。我们可以通过 Ray 中的初始化设置来限制 Modin 可以访问的 CPU 内核的数量,因为 Modin 在后端使用它。
import ray
ray.init(num_cpus=4)
import modin.pandas as pd
处理大数据时,数据集的大小超过系统内存(RAM)的情况并不少见。摩丁有一个特定的标志,我们可以设置为true
,这将使其脱离核心模式。核心外基本上意味着 Modin 将使用您的磁盘作为内存的溢出存储,允许您处理远远大于 RAM 大小的数据集。我们可以设置以下环境变量来启用此功能:
export MODIN_OUT_OF_CORE=true
结论
所以你有它!使用摩丁加速熊猫功能指南。这很容易做到,只需修改导入语句。希望你发现摩丁至少在一些情况下对加速你的熊猫功能是有用的。
喜欢学习?
在推特上关注我,我会在这里发布所有最新最棒的人工智能、技术和科学!也请在 LinkedIn上与我保持联系!
分子性质:多元线性回归之旅
这是我对分子性质的 Kaggle 竞赛的版本。目标是使用各种可用的特征来预测标量耦合常数。
自从完成我的数据科学训练营以来,我还没有做过包含我所学内容的个人项目。在看到 Kaggle 比赛和我对生物的热爱后,我决定尝试一下。不幸的是,当时我对自己在时限内完成比赛的能力没有信心,所以我只是下载并开始一点一点地工作,试图更好地理解这个过程。我在这里发表了多篇关于这些过程的文章。这些文章可以在下面找到:
以上四篇文章概括了我解决这个问题的每一种不同的方法。在上一篇文章之后,我决定休息一会儿,然后回来从一个新的角度看这个项目。我还使用了其他一些不同的方法,但我将只关注最终笔记本并在此介绍整个过程。为了理解这个过程,我建议你通读上面的文章。它们都是短文,通读它们不应该超过半小时。
现在让我们开始加载我将使用的所有库:
**import** **pandas** **as** **pd**
**import** **numpy** **as** **np**
**import** **matplotlib.pyplot** **as** **plt**
%matplotlib inline
**import** **networkx** **as** **nx**
**import** **seaborn** **as** **sns**
**from** **sklearn** **import** preprocessing, tree
**from** **sklearn.ensemble** **import** RandomForestRegressor
**from** **sklearn.model_selection** **import** cross_validate, cross_val_score, GridSearchCV
**from** **sklearn.linear_model** **import** LinearRegression, Ridge, Lasso
**import** **lightgbm**
**from** **sklearn.model_selection** **import** KFold
**from** **sklearn** **import** linear_model
**from** **sklearn.model_selection** **import** train_test_split
**from** **keras.models** **import** Sequential
**from** **keras.layers** **import** Dense
**from** **keras** **import** optimizers
**import** **keras**
**import** **warnings**
warnings.filterwarnings('ignore')
然后让我们加载库,看看一些基本的统计数据:
df= pd.read_csv('molecule_complete.csv')
df.columns #the names of all the columnsIndex(['molecule_name', 'atom_index_0', 'atom_index_1', 'type',
'scalar_coupling_constant', 'potential_energy', 'X', 'Y', 'Z',
'XX_atom1', 'YX_atom1', 'ZX_atom1', 'XY_atom1', 'YY_atom1', 'ZY_atom1',
'XZ_atom1', 'YZ_atom1', 'ZZ_atom1', 'XX', 'YX', 'ZX', 'XY', 'YY', 'ZY',
'XZ', 'YZ', 'ZZ', 'mulliken_charge_atom1', 'mulliken_charge',
'type_scc', 'fc', 'sd', 'pso', 'dso', 'atom_atom1_structure',
'x_atom1_structure', 'y_atom1_structure', 'z_atom1_structure', 'atom',
'x', 'y', 'z'],
dtype='object')df.select_dtypes(include=[object]) #this shows the categorical variables
The categorical variables among the features
现在来看一些图片。这要感谢 Kaggle 的竞争对手安德鲁:
fig, ax = plt.subplots(figsize = (20, 12)) **for** i, t **in** enumerate(df[‘type’].unique()): df_type = df.loc[df[‘type’] == t] G = nx.from_pandas_edgelist(df_type, ‘atom_index_0’, ‘atom_index_1’, [‘scalar_coupling_constant’]) plt.subplot(2, 4, i + 1); nx.draw(G, with_labels=**True**); plt.title(f’Graph for type **{t}**’)
这有助于创建与标量耦合常数相关的所有分类变量的网络图。我还为 fc、muliken_charge、pso、sd 和 dso 创建了更多的关系图,因为它们也被确定为我之前工作中的重要特性。
This is just for the Scalar Constant. The rest of the graph can be found in the final notebook linked above
这可能有点难以解释。然而,我们看到一些类型更多地聚集在一起,而其他类型如 2JHH 与标量形成了相当独特的关系。使用 Tableau Visual 有一个稍微好一点的方法来可视化每种类型对目标原子的影响:
This shows how much weight each atom type has on different atoms.
While this shows how many times each type shows up altogether.
结合上面两张图片,我们可以理解为什么 2JHH 具有如此独特的图式,而像 1JHC 和 3JHC 这样的原子是相似的,并且更加聚集在一起。现在让我们使用类型和标量常数创建一个标量常数库和一个 violin 图:
#this was done for all important features from the LGBM feature extraction model, but I will only show scalar here.
fig, ax = plt.subplots(figsize = (18, 6))
plt.subplot(1, 2, 1);
plt.hist(df['scalar_coupling_constant'], bins=20);
plt.title('Basic scalar_coupling_constant histogram');
plt.subplot(1, 2, 2);
sns.violinplot(x='type', y='scalar_coupling_constant', data=df);
plt.title('Violinplot of scalar_coupling_constant by type');
The bin shows most scalar numbers are rather small. The violin plot however shows another great visual on how each type plays a critical role on our target variables
在此之后,我使用 LabelEncoder 将所有分类变量转换为虚拟变量,创建一个训练和测试集,最后运行 sci-kit learn 的 train-test-split。
**for** f **in** ['type', 'type_scc', 'atom_atom1_structure', 'atom']:
lbl = preprocessing.LabelEncoder()
lbl.fit(list(df[f].values))
df[f] = lbl.transform(list(df[f].values))train= df.drop(['molecule_name', 'scalar_coupling_constant'], axis=1)
test= df['scalar_coupling_constant']feature_train, feature_test, target_train, target_test= train_test_split(train, test, test_size=0.12)
这给了我以下信息:
total feature training features: 4099169
total feature testing features: 558978
total target training features: 4099169
total target testing features: 558978
在我以前的模型中,我没有使用任何分类变量。因此,我决定再次运行 LGBM 模型,但包括虚拟变量:
train_data = lightgbm.Dataset(feature_train, label=target_train)
test_data = lightgbm.Dataset(feature_test, label=target_test)#Create some parameters:
n_fold = 7 folds = KFold(n_splits=n_fold, shuffle=**True**, random_state=4) #The formatting below is a bit messy but please be aware of proper indentation. Copy paste into medium is messing up proper indentationparameters = {'num_leaves': 250,
'min_child_samples': 75,
'objective': 'regression',
'max_depth': 24,
'learning_rate': 0.001,
"boosting_type": "gbdt",
"subsample_freq": 3,
"subsample": 0.9,
"bagging_seed": 15,
"metric": 'mae',
"verbosity": -1,
'reg_alpha': 0.01,
'reg_lambda': 0.3,
'colsample_bytree': 1.0}#Now to run the model:
model = lightgbm.train(parameters, train_data, valid_sets=test_data, num_boost_round=5000, early_stopping_rounds=100) #I should have used 10,000 but 5000 was still great#Time to visualize the new feature importance change:
ax = lightgbm.plot_importance(model, max_num_features=40, figsize=(15,15))
plt.show()
We see virtually no change compare to the previous LGBM model I made
我又做了一个视觉效果,它关注的是准确性而不是平均绝对误差,它给出了一个稍微不同的特征重要性,但它只影响了前 5 个特征的顺序。然后我意识到,由于目标变量包含的数字大多很小,但并不是所有的特性都是如此,这就对数字较大的特性产生了某种形式的偏差。然后我决定将数据标准化:
normal_feature=df.drop(['molecule_name', 'scalar_coupling_constant'], axis=1) target= df['scalar_coupling_constant']x= normal_feature.values
min_max_scaler = preprocessing.MinMaxScaler()x_scaled = min_max_scaler.fit_transform(x) #this gave an array instead of a dataframe. Now to convert this array into a dataframe and then properly change all column names back to the originalnf = pd.DataFrame(x_scaled)#This will reassign the names. It would have been easier to create a loop function but since I did it once already, I just copy pasted and added the additional dummy variables innormfeat= nf.rename(columns={0: 'atom_index_0', 1:'atom_index_1', 2: 'type', 3:'potential_energy', 4: 'X', 5: 'Y', 6: 'Z', 7: 'XX_atom1', 8: 'YX_atom1', 9: 'ZX_atom1', 10: 'XY_atom1', 11: 'YY_atom1', 12: 'ZY_atom1',13: 'XZ_atom1', 14: 'YZ_atom1', 15: 'ZZ_atom1', 16: 'XX', 17: 'YX', 18: 'ZX', 19: 'XY', 20: 'YY', 21:'ZY', 22:'XZ', 23:'YZ', 24:'ZZ', 25: 'mulliken_charge_atom1', 26: 'mulliken_charge', 27: 'type_scc', 28: 'fc', 29: 'sd', 30:'pso',31: 'dso', 32: 'atom_atom1_structure', 33:'x_atom1_structure', 34:'y_atom1_structure', 35:'z_atom1_structure', 36: 'atom', 37:'x', 38:'y', 39:'z' })
现在创建一个新的训练-测试-分割,因为这些本质上是新的变量。
features= normfeat
target= df[[‘scalar_coupling_constant’]] feature_train, feature_test, target_train, target_test= train_test_split(features, target, test_size=0.1)#Then to convert it all to numpy array for easier calculations:
feature_array= feature_train.to_numpy()
target_array= target_train.to_numpy()
做完这些,我决定进入真正的机器建模。我没有做另一个 LGBM 模型,因为我不认为它会改变更多的功能,即使在正常化后,但你可以尝试一下,让我知道。我从基本的线性回归模型开始,使用负均方误差进行测量。这意味着数字越小,结果越好:
linear= LinearRegression()
mse= cross_val_score(linear, feature_array, target_array, scoring='neg_mean_squared_error', cv= 20)
mean_mse= np.mean(mse)mse= -6.683008610663098e-09
我专门为目标测试变量创建了一个新的数据框架,用于将来比较 ridge 和 lasso 模型:
target_test= target_test.reset_index()target_test=target_test.drop(['index'], axis=1)
#Only run the above codes once or it will give you errortarget_test=target_test.rename(columns={'scalar_coupling_constant': 'Actual Test Scalar'})
现在创建一个新的山脊模型,并根据标准化的特征阵列对其进行测试。我选择 GridSearchCV 用于山脊和套索,因为根据我的研究,它是最佳的。它使用交叉验证,而不是创建一个新的训练测试拆分案例。这最小化了数据泄漏的可能性,因为该算法不会看到实际的测试集。
ridge= Ridge()
ridgereg= GridSearchCV(ridge, param_grid=parameters, scoring='neg_mean_squared_error', cv= 15)
ridgereg.fit(feature_array, target_array)
print('ridge param: ', ridgereg.best_params_)
print('ridge score: ', ridgereg.best_score_)
ridgereg#The answers:
ridge param: {'alpha': 1e-25}
ridge score: -6.683015092064162e-09#Again, sorry about the formatting.GridSearchCV(cv=15, error_score='raise-deprecating',
estimator=Ridge(alpha=1.0, copy_X=True, fit_intercept=True, max_iter=None, normalize=False, random_state=None, solver='auto', tol=0.001),
iid='warn', n_jobs=None,
param_grid={'alpha': [1e-25, 1e-20, 1e-15, 1e-10, 1e-05, 0.01, 1,
10]},
pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
scoring='neg_mean_squared_error', verbose=0) # Finally to fit the best ridge parameters to our training set:ridgereg= GridSearchCV(ridge, param_grid=parameters, scoring='neg_mean_squared_error', cv= 20) ridgereg.fit(feature_array, target_array)#To check the actual score:
ridgereg.score(feature_test, target_test)-6.836977895342504e-09 #this is a very small mean squared error.
现在对未知的测试用例进行一些预测:
ridgepredict=ridgereg.predict(feature_test)
actualtest=np.array(target_test)
plt.rcParams["figure.figsize"] = (8, 8)
fig, ax = plt.subplots()
ax.scatter(actualtest, ridgepredict)
ax.set(title="Ridge Actual vs Predict")
ax.set(xlabel="Actual", ylabel="Predict");
This shows how well my model is predicting against the actual test.
这让我担心过度拟合,我不明白为什么数据之间有差距。所以我把山脊线的分数保存到一个数据框中,然后转移到 lasso 上。
#To save the ridge data into a dataframe
ridgedata= pd.concat([target_test, pd.DataFrame(ridgepredict)], axis=1)
ridgecomparison= ridgedata.rename(columns={0:'Predicted Ridge Scalar'})
套索的计算方法与山脊相同:
lasso= Lasso()
lassoreg= GridSearchCV(lasso, param_grid=parameters, scoring='neg_mean_squared_error', cv= 20)
lassoreg.fit(feature_array, target_array)lassoreg.score(feature_test, target_test)-1.3148967532843286e-05 #the score is worse than Ridge. This showed that Ridge is better for this problem than Lasso#To create the Lasso graph:
lassopredict=lassoreg.predict(feature_test)
plt.rcParams["figure.figsize"] = (8, 8)
fig, ax = plt.subplots()
ax.scatter(actualtest, lassopredict)
ax.set(title="Lasso Actual vs Predict")
ax.set(xlabel="Actual", ylabel="Predict");
The same issue persist.
此时,我将 lasso 分数保存到一个数据帧中,并决定使用 Tableau 作为视觉媒介,直接将结果与实际测试分数进行比较:
lassodata= pd.concat([target_test, pd.DataFrame(lassopredict)], axis=1)
lassocomparison= lassodata.rename(columns={0:'Predicted Lasso Scalar'})#Combining the test dataframe with the Ridge and Lasso and then saving it as a CSV to check with Tableauaccuracy_check= pd.concat([ridgecomparison, lassocomparison], axis=1)
accuracy_check= accuracy_check.loc[:,~accuracy_check.columns.duplicated()] #this drops any duplicated columnsaccuracy_check.to_csv('accuracy_check.csv', index=**False**)# You can run the following to see if it saved properly:
acc_chk= pd.read_csv('accuracy_check.csv')
acc_chk.head()
The Tableau visual (sorry, there is no codes for this)
这表明了我的山脊和套索模型的准确性。由于使用 tableau 检查三个连续列的准确性有点棘手,所以我对所有列使用 COUNT 函数作为一种过滤形式,然后创建一个 SUM 线性图,其中实际的测试标量是主要部分。大小显示了大多数数字的集中,它在更大的项目计划中并没有真正发挥任何作用。然而,很容易画出一条清晰的最佳拟合线,表明我使用的算法可以用来预测分子标量常数电荷。这也有助于解释一些差距。大多数标量一开始都是很小的数字,边界之外的几千个可能造成了线性图形中的缺口。然而,我们可以清楚地看到一条最佳拟合线。
我还想尝试一种算法。一个神经网络。在不同的笔记本上尝试了几个不同的版本后,我决定用 Relu 和 adadelta 作为优化器。我选择 adadelta 是因为来自 Keras 文档的解释:
学习率基于梯度更新的移动窗口,而不是累积所有过去的梯度。这样,即使已经进行了多次更新,Adadelta 也能继续学习。
earlystop=keras.callbacks.callbacks.EarlyStopping(monitor='val_loss', min_delta=0, patience=0, verbose=0, mode='auto', baseline=**None**, restore_best_weights=**False**)terminate= keras.callbacks.callbacks.TerminateOnNaN()adadelta= optimizers.Adadelta(learning_rate=1.0, rho=0.95)
在设置了基本的边界之后,我创建了一个带有早停和早停的模型。它们的描述和用法可以在 keras 文档中找到。
m4 = Sequential() m4.add(Dense(64, input_dim=40, activation='relu'))
m4.add(Dense(32, activation='relu'))
m4.add(Dense(16, activation='relu'))
m4.add(Dense(8, activation='relu'))
m4.add(Dense(4, activation='relu'))
m4.add(Dense(1, activation='linear'))
m4.summary()Model: "sequential_4" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_19 (Dense) (None, 64) 2624 _________________________________________________________________ dense_20 (Dense) (None, 32) 2080 _________________________________________________________________ dense_21 (Dense) (None, 16) 528 _________________________________________________________________ dense_22 (Dense) (None, 8) 136 _________________________________________________________________ dense_23 (Dense) (None, 4) 36 _________________________________________________________________ dense_24 (Dense) (None, 1) 5 ================================================================= Total params: 5,409
Trainable params: 5,409
Non-trainable params: 0*#early callback*
m4.compile(optimizer=adadelta, loss='mean_squared_error', metrics=['mae', 'acc'])
his4=m4.fit(feature_array, target_array, validation_split=0.3, verbose=1, callbacks=[earlystop, terminate],
epochs=300, batch_size=5000)
由于提前回调和终止,模型结束得相当快。它显示出高 mae 和低精度,因为收敛发生得相对较早。但我知道这是一个错误,因为下图。
*#with early and termination callback established*
acc = his4.history['acc']
val_acc = his4.history['val_acc']
loss = his4.history['loss']
val_loss = his4.history['val_loss']
epochs = range(len(acc))
plt.plot(epochs, acc, 'r', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend() plt.figure()
plt.plot(epochs, loss, 'r', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend()
plt.show()
The convergence occurs due to the Loss but not accuracy. The accuracy is far too low to be valid.
然后我运行了一个新的模型,但是这次我没有使用提前回调或者终止。我想穿越所有的时代:
m4.compile(optimizer=adadelta, loss='mean_squared_error', metrics=['mae', 'acc'])his4=m4.fit(feature_array, target_array, validation_split=0.3, verbose=1,
epochs=300, batch_size=5000)#The graphacc = his4.history['acc']
val_acc = his4.history['val_acc']
loss = his4.history['loss']
val_loss = his4.history['val_loss']
epochs = range(len(acc))
plt.plot(epochs, acc, 'r', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend() plt.figure()
plt.plot(epochs, loss, 'r', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend()
plt.show()
With no early callback
现在我看到精度和损耗都收敛了。然而,准确性似乎收敛在一个非常低的点,并不是这个模型的一个合适的度量。由于 adadelta 使用衰减作为均值点,我也决定研究平均绝对误差而不是精度。由于精度如此之小,并且没有适当的梯度,因此无法使用。
acc = his4.history['mae']
val_acc = his4.history['val_mae']
loss = his4.history['loss']
val_loss = his4.history['val_mae']
epochs = range(len(mae))
plt.plot(epochs, mae, 'r', label='Training mae')
plt.plot(epochs, val_mae, 'b', label='Validation mae') plt.title('Training and validation mae')
plt.ylabel('mae')
plt.xlabel('epoch')
plt.legend() plt.figure()
plt.plot(epochs, loss, 'r', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend()
plt.show()
Mean Absolute Error
这显示了更好的收敛和梯度下降。如果我运行一个更高的纪元计数,mae 很可能会下降更多。但此时,mae 位于 0.6403,亏损位于 0.6871。虽然它没有山脊模型 mae 小,但它仍然是一个下降模型。但是,就定量分析而言,岭模型仍然是用于线性回归问题的最佳模型,该问题包含多个要素,每个要素都有数百万个数据集。此外,每个特性的值都很稀疏,有些特性甚至可能不起任何作用。也有可能一些特征对目标变量产生相同的影响,导致多重共线性和权重重要性的差异,从而导致模型中的偏差和过度拟合。考虑到所有这些因素,我认为使用岭模型来计算平均误差而不是精确度对于这个挑战来说是完美的。我这个项目的 github url 将在下面链接,所以你可以看看,也许可以改进我的模型。
来自 Kaggle 竞争预测分子性质的数据。所有的数据都可以在那里找到。由于 git 大小的限制…
github.com](https://github.com/imamun93/Molecular_Properties) [## 预测分子性质
你能测量一对原子之间的磁相互作用吗?
www.kaggle.com](https://www.kaggle.com/c/champs-scalar-coupling) [## 优化器- Keras 文档
优化器是编译 Keras 模型所需的两个参数之一:from Keras import optimizer model =…
keras.io](https://keras.io/optimizers/)
人工智能的分子合成
一步一步指导使用氮化镓寻找新的药物和材料
Progressive GAN generated molecule structures (Three molecules are real)
分子合成是从简单的前体构建复杂化学分子的过程。这是开发下一代药物、智能材料、杀虫剂和电子产品的关键。迄今为止,分子合成是手工的、昂贵的、耗时的、多步骤的过程。它主要由化学直觉和化学反应的可靠知识指导。人工智能(AI)有潜力随意制造任何分子,成本低廉,时间跨度有意义,这将为未来的科学进步开启想象不到的机遇。
生成对抗网(GAN)是一个由“生成器”和“鉴别器”组成的人工智能模型。生成器捕获训练数据分布并从中生成样本。鉴别器估计样本来自生成器而不是训练数据的概率。这个两人游戏一直玩到鉴别器出错的概率最大化。
在本文中,我将演示如何训练 GAN 来生成分子结构。这是自动化分子合成的第一步。类似的工作正在由药物发现和合成联盟 ( 人工智能能创造分子吗?由麻省理工学院-IBM 沃森人工智能实验室。我举这个例子是为了强调任何人都可以轻松地使用人工智能进行尖端研究。唯一的限制因素是想象力。
数据
任何人工智能(AI)项目的第一步都是数据。有多个网站包含各种格式的化学数据库。 PubChem 包含大约 96M 的化合物列表,以 SMILES 符号表示。SMILES 符号可以分为功能组,并使用 python rdkit 库转换为分子结构图像。在 52 个官能团中,只有醇脂肪族官能团用于限制这项工作的范围。首先将来自醇脂肪族的 50,000 种化合物转换成 128×128 大小的图像。将 SMILES 条目分类并转换成图像的代码可以在 github 获得。
培养
下一步是选择 GAN 型号。在几次失败的尝试后,我选择了进步的 GAN 的()py torch 实现,由 Facebook research(py torch _ GAN _ zoo)。渐进式 GAN 的新颖性在于,它从低分辨率的图像开始训练,并随着训练的进行添加新的层来引入更高分辨率的细节。据其作者称,PGAN 的这些特点使它更稳定,训练速度更快。
训练的第一步是克隆 pytorch_GAN_zoo:
git clone [https://github.com/facebookresearch/pytorch_GAN_zoo.git](https://github.com/facebookresearch/pytorch_GAN_zoo.git)
PGAN 的训练过程与其他甘斯略有不同。在开始训练之前,必须将数据调整为低分辨率图像。这可以通过发出以下命令来完成:
python datasets.py celeba $PATH_TO_IMAGES -o $OUTPUT_DIR -f
在该命令中,“celeba”是预训练数据集的名称。pytorch_GAN_zoo 在此模型上预先训练了多个数据集。“celeba”数据集对应于 128x128 像素的图像,这与本项目中使用的图像大小相同。除非用户是超参数向导,否则建议使数据适应预训练模型的超参数,而不是根据数据调整超参数。首次运行后,可以更改超参数来微调模型。下表包含预训练的型号名称和支持的图像大小。
Dataset name and image size
Datasets.py 还会生成一个配置文件。这个配置文件将包含 1)调整大小的图像和原始图像的路径。2)每个尺度的迭代次数。“规模”是 PGAN 独有的概念。每个比例都与模型中的层数、迭代次数和图像分辨率相关联。使用以下公式计算最大比例。
image_size = 2**(2+max_scale)
在该公式中,常数 2 被添加到最大比例,因为训练层从 4x4 的分辨率开始。Datasets.py 将以(64,128,512,1024)为步长调整图像大小,直到达到数据的图像大小。“$PATH_TO_IMAGES”是训练图像的位置。“OUTPUT_DIR”这是保存已调整大小的图像的位置。“-f”选项是在训练开始前生成调整大小的图像。创建了调整图像大小和配置文件后,下一步是通过发出以下命令开始训练:
python train.py PGAN -c $CONFIG_FILE -n $DATASET_NAME -d $WEIGHTS_DIR
在这个命令中,“PGAN”指的是进步的甘。pytorch_GAN_zoo 也支持 DCGAN。“配置文件”是 datasets.py 生成的配置文件的路径。“数据集名称”是自定义数据集的名称。“权重 _ 目录”是存储权重的位置。train.py 中定义了更多选项。我使用了“np _ vis”选项来使用基于 numpy 的可视化,而不是安装“visdom”包。我还将“-e”和“-s”选项设置为 2000。
培训分析
该培训在基于 Tesla K80 的服务器上进行了 9 天。在上一次规模的 62000 次迭代后,我停止了训练。可以使用此链接下载训练模型的权重。GAN 训练时间长的一个主要原因是生成模型中缺乏迁移学习。迁移学习减少了判别模型中的数据需求和收敛时间。最近有一篇论文提出在 GANs 中解决这个问题。减少训练时间的一种方法是使用与数据相关的最小尺寸的图像。
在 PGAN,训练层从 4x4 分辨率开始,一直到数据集的图像大小。对于 128 的图像尺寸,以 4、8、16、32、64、128 的比例添加层。绘制训练时间与迭代的关系清楚地显示了新层的添加如何在每个尺度上指数地增加了训练时间。
Training time vs iterations
下面的视频显示了训练进度。生成的图像中的大部分改进发生在上次缩放期间。这个视频是通过拼接每 2000 次迭代后保存的评估图像创建的。
Training progress on Progressive GAN
pytorch_GAN_zoo 提供了在生成的图像上评估模型性能的工具。最流行的是盗梦空间评分。通过给出以下命令来计算生成的图像的初始分数。
python eval.py inception -c $CONFIGURATION_FILE -n $modelName -m $modelType -d $WEIGHTS_DIR
切片 Wasserstein 距离(SWD)是另一种用于评估高分辨率 GANs 的方法。拉普拉斯 SWD 分数通过给出以下命令来计算。关于评估 GAN 性能的更多信息,请参见的论文。
python eval.py laplacian_SWD -c $CONFIGURATION_FILE -n $modelName -m $modelType -d $WEIGHTS_DIR
pytorch_GAN_zoo 实现了另一个工具,“励志 Adverserial Image generation ”。该工具将图像作为输入,并使用梯度下降提取输入向量。该输入向量用于生成共享输入图像特征的新图像。鼓舞人心的一代是一个两步的过程。
python save_feature_extractor.py {vgg16, vgg19}\ $PATH_TO_THE_OUTPUT_FEATURE_EXTRACTOR --layers 3 4 5
在该命令中,vgg16/vgg19 指定用于输入矢量生成的模型。下载特征提取器后,使用以下命令根据以下内容生成分子结构
python eval.py inspirational_generation -n $modelName -m $modelType\ --inputImage $pathTotheInputImage -f \ $PATH_TO_THE_OUTPUT_FEATURE_EXTRACTOR -d $WEIGHTS_DIR
下图显示了输入和生成的分子结构。令人钦佩的是,ProGAN 已经很好地学习了输入向量和生成模式。
Inspiration Generation
未来的工作
上面训练的模型将产生随机的分子结构。这项工作可以通过使用 LC GAN 很容易地扩展,通过用在潜在空间中训练的单独 GAN 约束变分自动编码器(VAE)的潜在空间来产生具有某些属性的分子。这个模型也可以在 3D 分子结构上进行训练。该模型的输出可以被馈送到另一个模型,该模型可以验证和预测所生成的分子结构的特性。所有这些方法目前都在由麻省理工学院-IBM 沃森人工智能实验室进行。
由于信息、数据和资源的开放性,任何个人都可以从事新技术的研究。
确认
我非常感谢开源社区,这篇文章依赖于他们的工作和知识。我感谢化学教授 Preeti Gupta 给我上了官能团分类的速成课。