用深度学习分离音乐曲目
如何使用 Deezer 的 library spleeter 开始创建您的卡拉 ok。
如果你是一名 DJ,一名音乐采样员或类似的人,通常你会花几个小时从原始音乐中创作歌曲,甚至花上几千美元购买软件来帮助你。这个过程中的一个步骤是能够分离人声和音乐,有时是人声、低音、鼓等等。
Deezer 的出色人员刚刚发布了一个包,它将帮助您快速、轻松且免费地做到这一点!!
它是如何工作的
在论文“歌声分离:训练数据研究”中,Deezer 的人员解释了使用深度神经网络分离音轨的过程。这是要点:
在本文中,我们将重点放在深度神经网络来执行分离。我们选择的基准模型是 U-Net[……]U-Net 与卷积自动编码器采用相同的架构,具有额外的跳跃连接,可将编码阶段丢失的详细信息带回解码阶段。它在编码器中有 5 个步长 2D 卷积层,在解码器中有 5 个步长 2D 去卷积层。
所以他们使用一个 U 型网络,看起来像这样:
https://arxiv.org/pdf/1505.04597.pdf
这个架构是由 Ronneberger 和其他人创建的,用于医学领域的图像分类任务,现在我们将它用于音乐和更多。该网络是从完全卷积网络中构思出来的,经过修改和扩展,它可以使用非常少的训练图像进行工作,并产生更精确的分割。
U-Net 架构还依赖于对数据扩充的大量使用,以便更有效地使用可用的带注释的样本。如果你没有那么多信息,这是很棒的。
Deezer 的人做的主要修改是集成立体声处理:
我们使用 3D 张量(通道、时间步长、频率仓)作为网络的输入和输出。
用于训练网络的数据集:MUSBD 和 Bean。MUSDB 是用于源分离的最大和最新的公共数据集,而 Bean 是包含大部分流行/摇滚歌曲的私有数据集,并且包括作为独立录音的声乐和器乐音轨。
然后他们重建了数据集并做了一些数据扩充,具体来说他们做了:
- 频道交换。
- 时间拉伸。
- 音高移位
- 使再混合
- 逆高斯滤波
- 响度标度
以及更多的组合。最后一句话:
预训练模型的性能非常接近已发布的技术水平,并且据作者所知,在即将公开发布的通用 musdb18 基准测试中,这是性能最好的 4 茎分离模型。Spleeter 的速度也非常快,因为它可以使用预训练的 4 茎模型在单个图形处理单元(GPU)上将混合音频文件分成 4 个茎,比实时 1 快 100 倍。
如何使用它
https://deezer.io/releasing-spleeter-deezer-r-d-source-separation-engine-2b88985e797e
为了使用这个包,我创建了一个 MatrixDS repo,这样你就可以试一试了:
[## MatrixDS |数据项目工作台
MatrixDS 是一个构建、共享和管理任何规模的数据项目的地方。
community.platform.matrixds.com](https://community.platform.matrixds.com/community/project/5dc40ffa9550a3c2d251bcb8/files)
要开始使用该库,首先要做的是安装它:
pip install --user spleeter
对我来说,最好使用康达方式:
git clone https://github.com/deezer/spleeter
conda env create -f spleeter/conda/spleeter-cpu.yaml
conda activate spleeter-cpu
您还需要安装 ffmpeg:
sudo apt-get update
sudo apt-get install ffmpeg
如果您正在运行 mac:
brew install ffmpeg
我将用这些歌曲来测试:
免责声明:我不拥有这些歌曲的权利,版权属于官方音乐创作者和唱片公司。
然后对第一首歌这样做:
spleeter separate -i songs/breath.mp3 -o output/
这将下载用于分离人声和音乐的预训练模型。
对于第一首歌,“你的每一次呼吸”,这些是分离人声和音乐的结果:
[## 伴奏. wav
编辑描述
drive.google.com](https://drive.google.com/file/d/1nQX9c_gkr0ulofnbF2aG2GNS0d8fHAm1/view?usp=sharing) [## vocals.wav
编辑描述
drive.google.com](https://drive.google.com/open?id=1tym_4LCTj0Jt6-cQ8Ui7GdyHK3pfk6td)
以下是“懒歌”的搜索结果:
[## 伴奏(1)。声音资源文件
编辑描述
drive.google.com](https://drive.google.com/file/d/1_Wkdnr4dpxndDAXv9e-zzkXnZf_3GRje/view?usp=sharing) [## 人声(1)。声音资源文件
编辑描述
drive.google.com](https://drive.google.com/file/d/1D62XMBDzvsGAdNfI0UuCj_3KTWSttxjZ/view?usp=sharing)
现在让我们用“不要让我失望”这首歌的 4 茎模型来分离人声/低音/鼓/其他:
spleeter separate -i songs/dont.mp3 -o audio_output -p spleeter:4stems
这将下载用于分离所有音乐源的预训练模型。
以下是这首歌的结果:
编辑描述
drive.google.com](https://drive.google.com/drive/folders/1qA3xsUf71qrk23pwdf65roWvGM_yhsm6?usp=sharing)
你会在那里找到足迹。
这个库的强大给我留下了非常深刻的印象,所有这些只需要几秒钟就可以运行,而且输出非常好。我知道可以改进,你也可以!因为代码是开源的,你可以去那里看看哪里可以让声音更清晰。你甚至可以用特定音乐来训练你自己的模型,这样它就能被更好地识别。
所有相关信息都在这里:
Spleeter 是 Deezer 源分离库,具有用 Python 编写的预训练模型,并使用 Tensorflow。
github.com](https://github.com/deezer/spleeter)
还有这里:
Deezer 源分离库,包括预训练模型。- deezer/spleeter
github.com](https://github.com/deezer/spleeter/wiki/2.-Getting-started)
感谢 Deezer 的出色人员发布了这个库。
如果你想更多地了解我并看到其他文章,请点击这里关注我:
Favio Vázquez 的最新推文(@FavioVaz)。数据科学家。物理学家和计算工程师。我有一个…
twitter.com](https://twitter.com/FavioVaz)
用独立分量分析分离混合信号
Image modified from garageband
周围的世界是各种来源信号的动态混合体。就像上图中的颜色相互融合,产生新的色调,我们所感知的一切都是简单成分的融合。大多数时候,我们甚至没有意识到我们周围的世界是独立过程的混乱混合体。只有在不同的刺激,没有很好地混合,竞争我们的注意力的情况下,我们才意识到这种混乱。一个典型的例子是在鸡尾酒会上,一个人在听另一个人的声音,同时过滤掉所有其他客人的声音。根据房间的响度,这可能是一项简单或困难的任务,但不知何故,我们的大脑能够将信号与噪音分开。虽然还不清楚我们的大脑是如何进行这种分离的,但有几种计算技术旨在将信号分成其基本成分。其中一种方法被称为In 独立 C 组件 A 分析( ICA ),在这里我们将仔细看看这个算法是如何工作的,以及如何用 Python 代码写下来。如果你对代码比对解释更感兴趣,你也可以直接查看 Github 上这篇文章的 Jupyter 笔记本。
什么是独立分量分析?
现在让我们继续以鸡尾酒会为例。想象有两个人在说话,你可以听到他们两个,但是其中一个比另一个离你更近。两个声源的声波将混合在一起,作为一个组合信号到达你的耳朵。你的大脑会将两个声音源分开,你会分别听到两个客人的声音,离你近的那个声音更大。现在让我们用一种更抽象和简化的方式来描述它。每个源都是频率恒定的正弦波。这两种来源的混合取决于你的立场。这意味着离你更近的信号源比离你更远的信号源在混合信号中占主导地位。我们可以用向量矩阵符号将它写下来如下:
其中 x 为观察信号, s 为源信号, A 为混合矩阵。换句话说,我们的模型假设信号 x 是通过源信号的线性组合产生的。在 Python 代码中,我们的示例如下所示:
>> import numpy as np>>> # Number of samples
>>> ns = np.linspace(0, 200, 1000)>>> # Sources with (1) sine wave, (2) saw tooth and (3) random noise
>>> S = np.array([np.sin(ns * 1),
signal.sawtooth(ns * 1.9),
np.random.random(len(ns))]).T>>> # Quadratic mixing matrix
>>> A = np.array([[0.5, 1, 0.2],
[1, 0.5, 0.4],
[0.5, 0.8, 1]])>>> # Mixed signal matrix
>>> X = S.dot(A).T
从图 1 的图中可以看出,代码产生一个正弦波信号、一个锯齿信号和一些随机噪声。这三个信号是我们独立的来源。在下图中,我们还可以看到源信号的三种线性组合。此外,我们看到第一混合信号受锯齿分量支配,第二混合信号受正弦波分量影响更大,最后混合信号受噪声分量支配。
Figure 1: Source signals (upper plots) and linear combinations of the source signals (lower plots).
现在,根据我们的模型,我们可以通过将 x 乘以 A 的倒数,再次从混合信号中检索源信号:
这意味着为了找到源信号,我们需要计算 W 。因此,这篇文章剩余部分的任务将是找到 W 并从三个混合信号中检索三个独立的源信号。
ICA 发挥作用的先决条件
现在,在我们继续之前,我们需要多考虑一下源信号需要具备什么样的属性,以便 ICA 能够成功估计出 W 。算法工作的第一个先决条件是混合信号是任意数量的源信号的线性组合。第二个前提条件是源信号独立。那么独立是什么意思呢?如果信号 s1 中的信息没有给出关于信号 s2 的任何信息,则两个信号是独立的。这意味着它们不相关,这意味着它们的协方差是 0。然而,这里必须小心,因为不相关并不自动意味着独立。第三个前提条件是独立分量是非高斯的。这是为什么呢?两个独立的非高斯信号的联合密度分布在正方形上将是均匀的;参见下面图 2 中的左上图。用正交矩阵混合这两个信号将导致两个信号不再独立,而是在平行四边形上均匀分布;参见图 2 中的左下图。这意味着,如果我们处于一个混合信号的最小值或最大值,我们就知道另一个信号的值。因此他们不再独立。对两个高斯信号进行同样的操作将会产生其他结果(参见图 2 中的右图)。源信号的联合分布是完全对称的,混合信号的联合分布也是如此。因此,它不包含任何关于混合矩阵的信息,我们想要计算混合矩阵的逆矩阵。因此,在这种情况下,ICA 算法将会失败。
Figure 2: Gaussian and non-Gaussian sources and their mixtures
因此,总之,为了使 ICA 算法工作,需要满足以下先决条件:我们的源是( 2 )独立的( 3 )非高斯信号的( 1 )线性混合。
所以让我们快速检查一下上面的测试信号是否满足这些前提条件。在下面的左图中,我们看到正弦波信号与锯齿波信号的关系,而每个点的颜色代表噪声成分。如非高斯随机变量所预期的,信号分布在正方形上。同样,混合信号在图 3 的右图中形成平行四边形,这表明混合信号不再是独立的。
Figure 3: Scatter plots of source and mixed signals
预处理步骤
现在,将混合信号直接馈入 ICA 并不是一个好主意。为了获得独立分量的最佳估计,建议对数据进行一些预处理。下面将更详细地解释两种最重要的预处理技术。
定中心
我们在这里讨论的第一个预处理步骤是定心。这是从我们的输入数据中减去平均值的简单方法。结果,居中的混合信号将具有零均值,这意味着我们的源信号 s 也具有零均值。这简化了 ICA 计算,均值可以在以后加回去。Python 中的居中功能如下。
>>> def center(x):
>>> return x - np.mean(x, axis=1, keepdims=True)
白粉
我们需要的第二个预处理步骤是信号 X 的白化。这里的目标是对 X 进行线性变换,从而消除信号之间的潜在相关性,使其方差等于 1。结果,白化信号的协方差矩阵将等于单位矩阵:
其中 I 是单位矩阵。因为我们还需要在白化过程中计算协方差,所以我们将为它编写一个小的 Python 函数。
>>> def covariance(x):
>>> mean = np.mean(x, axis=1, keepdims=True)
>>> n = np.shape(x)[1] - 1
>>> m = x - mean
>>> return (m.dot(m.T))/n
白化步骤的代码如下所示。它基于 X 的协方差矩阵的奇异值分解(SVD)。如果你对这个程序的细节感兴趣,我推荐这篇文章。
>>> def whiten(x):
>>> # Calculate the covariance matrix
>>> coVarM = covariance(X) >>> # Singular value decoposition
>>> U, S, V = np.linalg.svd(coVarM)
>>> # Calculate diagonal matrix of eigenvalues
>>> d = np.diag(1.0 / np.sqrt(S))
>>> # Calculate whitening matrix
>>> whiteM = np.dot(U, np.dot(d, U.T))
>>> # Project onto whitening matrix
>>> Xw = np.dot(whiteM, X)
>>> return Xw, whiteM
FastICA 算法的实现
好了,现在我们已经有了预处理函数,我们终于可以开始实现 ICA 算法了。基于测量独立性的对比度函数,有几种实现 ICA 的方法。在这里,我们将在名为 FastICA 的 ICA 版本中使用 负熵的近似值。
那么它是如何工作的呢?如上所述,ICA 工作的一个先决条件是我们的源信号是非高斯的。关于两个独立的非高斯信号,有趣的是它们的和比任何源信号都更高斯。因此,我们需要优化 W ,使得 Wx 的结果信号尽可能是非高斯的。为了做到这一点,我们需要一种高斯度量。最简单的测量方法是峰度,它是数据的四阶矩,用于测量分布的“尾部”。正态分布的值为 3,像我们在图 2 中使用的均匀分布的峰度为< 3。从下面的代码可以看出,Python 中的实现非常简单,它还计算数据的其他矩。第一个矩是均值,第二个是方差,第三个是偏度,第四个是峰度。这里从四阶矩中减去 3,使得正态分布的峰度为 0。
>>> def kurtosis(x):
>>> n = np.shape(x)[0]
>>> mean = np.sum((x**1)/n) # Calculate the mean
>>> var = np.sum((x-mean)**2)/n # Calculate the variance
>>> skew = np.sum((x-mean)**3)/n # Calculate the skewness
>>> kurt = np.sum((x-mean)**4)/n # Calculate the kurtosis
>>> kurt = kurt/(var**2)-3>>> return kurt, skew, var, mean
然而,对于 ICA 的实现,我们不会使用峰度作为对比函数,但我们可以在以后使用它来检查我们的结果。相反,我们将使用以下对比函数 g(u) 及其一阶导数 g’(u) :
FastICA 算法在定点迭代方案中以如下方式使用上述两个函数:
因此,根据上述内容,我们要做的是随机猜测每个组件的重量。随机权重和混合信号的点积被传递到两个函数 g 和g’。然后我们从 g 中减去*g’*的结果,并计算平均值。结果是我们新的权重向量。接下来,我们可以将新的权重向量直接除以其范数,并重复上述操作,直到权重不再变化。这没什么不对的。然而,我们在这里面临的问题是,在第二个组件的迭代中,我们可能会识别出与第一次迭代中相同的组件。为了解决这个问题,我们必须从先前识别的权重中去相关新的权重。这是在更新权重和除以它们的范数之间的步骤中发生的事情。在 Python 中,实现如下所示:
>>> def fastIca(signals, alpha = 1, thresh=1e-8, iterations=5000):
>>> m, n = signals.shape>>> # Initialize random weights
>>> W = np.random.rand(m, m)>>> for c in range(m):
>>> w = W[c, :].copy().reshape(m, 1)
>>> w = w/ np.sqrt((w ** 2).sum())>>> i = 0
>>> lim = 100
>>> while ((lim > thresh) & (i < iterations)):>>> # Dot product of weight and signal
>>> ws = np.dot(w.T, signals)>>> # Pass w*s into contrast function g
>>> wg = np.tanh(ws * alpha).T>>> # Pass w*s into g'
>>> wg_ = (1 - np.square(np.tanh(ws))) * alpha>>> # Update weights
wNew = (signals * wg.T).mean(axis=1) -
>>> wg_.mean() * w.squeeze()>>> # Decorrelate weights
>>> wNew = wNew -
np.dot(np.dot(wNew, W[:c].T), W[:c])
>>> wNew = wNew / np.sqrt((wNew ** 2).sum())>>> # Calculate limit condition
>>> lim = np.abs(np.abs((wNew * w).sum()) - 1)
>>> # Update weights
>>> w = wNew
>>> # Update counter
>>> i += 1>>> W[c, :] = w.T
>>> return W
现在我们已经写好了所有的代码,让我们运行整个程序吧!
>>> # Center signals
>>> Xc, meanX = center(X)>>> # Whiten mixed signals
>>> Xw, whiteM = whiten(Xc)>>> # Run the ICA to estimate W
>>> W = fastIca(Xw, alpha=1)>>> #Un-mix signals using W
>>> unMixed = Xw.T.dot(W.T)>>> # Subtract mean from the unmixed signals
>>> unMixed = (unMixed.T - meanX).T
ICA 的结果显示在下面的图 4 中,其中上图表示原始源信号,下图表示 ICA 实现检索到的独立分量。结果看起来非常好。我们把三个来源都拿回来了!
Figure 4: Results of the ICA analysis. Above true sources; below recovered signals.
最后让我们检查最后一件事:信号的峰度。正如我们在图 5 中看到的,我们所有的混合信号的峰度都≤ 1,而所有恢复的独立分量的峰度都为 1.5,这意味着它们的高斯性低于它们的源。这是必须的,因为 ICA 试图最大化非高斯性。它也很好地说明了上面提到的事实,即非高斯信号的混合将比源更高斯。
Figure 5: Kernel Density Estimates of the three mixed and source signals.
总结一下:我们看到了 ICA 是如何工作的,以及如何用 Python 从头开始实现它。当然,有许多 Python 实现可以直接使用。然而,了解该方法的基本原理以知道何时以及如何使用它总是明智的。如果你有兴趣深入研究 ICA 并了解其细节,我向你推荐这篇论文,作者是 Aapo hyvrinen 和 Erkki Oja,2000 年。
否则你可以在这里查看完整的代码,在 Twitter 上关注我或者通过 LinkedIn 联系我。
这个项目的代码可以在 Github 上找到。
快速 ICA 算法的 Python 实现——akcarsten/Independent _ Component _ Analysis
github.com](https://github.com/akcarsten/Independent_Component_Analysis)
九月版:部署机器学习模型
机器学习的应用似乎是无穷无尽的(正如胡安·德·迪奥斯·桑托斯所展示的,构建一个皮卡丘探测应用)。但是,尽管越来越多的数据科学家开始熟悉这项技术,但找到大规模且健壮的成功用例仍然很困难。
这至少在一定程度上是因为,建立一个准确的、无偏见的模型仅仅被认为是成功的一半。其他人需要用!正如 Ian Xiao 在我们 Edition 的另一篇文章中所讨论的,即使你可以将模型发布到世界上,也会有交互、执行和反馈问题等待着破坏你的人工智能抱负……我们本月的选择在这里帮助你选择正确的栈来发布你的模型。
约书亚·弗莱明——编辑助理
部署机器学习模型作为 REST API
由阮 Ngo — 6 分钟读完
作为一名 Python 开发人员和数据科学家,我渴望构建 web 应用程序来展示我的工作。尽管我喜欢设计前端,但同时学习机器学习和应用程序开发变得非常困难。
在 Python 中部署 Keras 深度学习模型作为 Web 应用
通过将 Koehrsen — 7 分钟读取
深度学习、web 应用、Flask、HTML 和 CSS 在一个项目中
部署深度学习模型
艾萨克·戈弗雷德(is AAC Godfried)5 分钟阅读
最近,学术界和行业研究人员在深度学习领域进行了大量令人兴奋和开创性的研究。
使用 TensorFlow 服务和烧瓶部署 Keras 模型
通过 Himanshu Rawlani — 8 分钟阅读
通常需要抽象出机器学习模型的细节,并将其部署或集成到易于使用的 API 端点中。
使用 Tensorflow 对象检测在 Android 上检测皮卡丘
由胡安·德·迪奥斯·桑多斯 — 12 分钟阅读
在 TensorFlow 的众多功能和工具中,有一个名为 TensorFlow 对象检测 API 的组件。
解决在现实世界中部署人工智能系统的最后一英里问题
Ian Xiao — 10 分钟阅读
最后一英里的问题是实现人工智能承诺的价值的最后障碍。
用 Flask 部署 Keras 深度学习模型
通过本·韦伯 — 7 分钟读取
这篇文章演示了如何使用用 Keras 构建的深度学习模型来设置端点以服务于预测。
大规模部署 scikit-learn 模型
由郁风 G — 4 分钟读出
Scikit-learn 非常适合组装一个快速模型来测试数据集。但是,如果您想对传入的实时数据运行它,该怎么办呢?
我们也感谢最近加入我们的所有伟大的新作家,迈克尔·伯乐斯,阿卡什·坦登,伊夫·佩尔斯曼,扬·范·泽格布鲁克,安德鲁·E·布雷顿,维维维安·林登伯格,西蒙·海尔曼·弗拉奇斯,尼古拉·柳比莫夫,塞拉菲姆·巴佐格洛 理查德·廖,贾汉吉尔·马迈多夫,鲍里斯·克尼亚泽夫,肖尔·乔尔,丹尼尔·霍根,亚历克斯·穆尔,安迪·帕特森,SJ·波特等等。 我们邀请你看看他们的简介,看看他们的工作。
用于聚类和分类的序列嵌入
在这里,我们将学习一种方法来获得字符串序列的矢量嵌入。这些嵌入可用于聚类和分类。
序列建模一直是一个挑战。这是因为序列数据固有的非结构化。就像自然语言处理(NLP)中的文本一样,序列是任意的字符串。对于计算机来说,这些字符串没有任何意义。因此,建立数据挖掘模型非常困难。
对于文本,我们已经提出了嵌入,如 word2vec,它将一个单词转换为一个 n 维向量。本质上,把它放在欧几里得空间。在这篇文章中,我们将学习对序列做同样的事情。
在这里,我们将介绍一种为序列创建嵌入的方法,这种方法将序列带入欧几里得空间。通过这些嵌入,我们可以在序列数据集上执行传统的机器学习和深度学习,例如 kmeans、PCA 和多层感知器。我们提供并处理两个数据集——蛋白质序列和博客。
序列数据集在我们身边随处可见。例如,科技行业的点击流、音乐收听历史和博客。在生物信息学中,我们有大型的蛋白质序列数据库。蛋白质序列由 20 种氨基酸的某种组合组成。典型的蛋白质序列如下所示,其中每个字母对应一种氨基酸。
Fig. 1. Example of a protein sequence.
一个蛋白质序列不一定包含所有的 20 个氨基酸,而是它的一个子集。为了清楚起见,我们将定义一些在这篇文章中使用的关键词。
字母表*:组成一个序列的离散元素。例如氨基酸。*
alphabet-set :将在语料库中构成序列的所有字母表的集合。例如,语料库中的所有蛋白质序列都由一组 20 个氨基酸组成。
序列*:离散字母的有序序列。语料库中的序列包含字母表的子集。*
序列语料库通常包含数千到数百万个序列。给定我们已标记或未标记的数据,通常需要聚类和分类。然而,由于序列的非结构化——任意长度的任意字符串,这样做并不简单。
为了克服这一点,可以使用序列嵌入。这里我们将使用 SGT 嵌入,将序列中的长期和短期模式嵌入到一个有限维向量中。SGT 嵌入的优点是,我们可以很容易地调整长期/短期模式的数量,而不会增加计算量。
下面的源代码和数据是这里是。在继续之前,我们需要安装sgt
包。
$ pip install sgt
使聚集
蛋白质序列聚类
这里使用的数据取自www.uniprot.org。这是一个蛋白质的公共数据库。该数据包含蛋白质序列及其功能。在这一节中,我们将对蛋白质序列进行聚类,在下一节中,我们将使用它们的功能作为构建分类器的标签。
我们首先读取序列数据,并将其转换为一个列表列表。如下所示,每个序列都是一个字母列表。
>>> protein_data = pd.DataFrame.from_csv('../data/protein_classification.csv')
>>> X = protein_data['Sequence']
>>> def split(word):
>>> return [char for char in word]>>> sequences = [split(x) for x in X]
>>> print(sequences[0])
['M', 'E', 'I', 'E', 'K', 'T', 'N', 'R', 'M', 'N', 'A', 'L', 'F', 'E', 'F', 'Y', 'A', 'A', 'L', 'L', 'T', 'D', 'K', 'Q', 'M', 'N', 'Y', 'I', 'E', 'L', 'Y', 'Y', 'A', 'D', 'D', 'Y', 'S', 'L', 'A', 'E', 'I', 'A', 'E', 'E', 'F', 'G', 'V', 'S', 'R', 'Q', 'A', 'V', 'Y', 'D', 'N', 'I', 'K', 'R', 'T', 'E', 'K', 'I', 'L', 'E', 'D', 'Y', 'E', 'M', 'K', 'L', 'H', 'M', 'Y', 'S', 'D', 'Y', 'I', 'V', 'R', 'S', 'Q', 'I', 'F', 'D', 'Q', 'I', 'L', 'E', 'R', 'Y', 'P', 'K', 'D', 'D', 'F', 'L', 'Q', 'E', 'Q', 'I', 'E', 'I', 'L', 'T', 'S', 'I', 'D', 'N', 'R', 'E']
接下来,我们生成序列嵌入。
>>> from sgt import Sgt
>>> sgt = Sgt(kappa = 10, lengthsensitive = False)
>>> embedding = sgt.fit_transform(corpus=sequences)
嵌入是在 400 维空间。我们先对它做 PCA,降维到两个。这也将有助于集群的可视化。
>>> pca = PCA(n_components=2)
>>> pca.fit(embedding)
>>> X = pca.transform(embedding)>>> print(np.sum(pca.explained_variance_ratio_))
0.6019403543806409
前两个电脑解释了大约 60%的差异。我们将把它们分成 3 组。
>>> kmeans = KMeans(n_clusters=3, max_iter =300)
>>> kmeans.fit(df)>>> labels = kmeans.predict(df)
>>> centroids = kmeans.cluster_centers_>>> fig = plt.figure(figsize=(5, 5))
>>> colmap = {1: 'r', 2: 'g', 3: 'b'}
>>> colors = list(map(lambda x: colmap[x+1], labels))
>>> plt.scatter(df['x1'], df['x2'], color=colors, alpha=0.5, edgecolor=colors)
继续构建分类器。
分类
蛋白质序列分类
我们将从在前面使用的相同蛋白质数据集上构建分类器开始。数据集中的蛋白质有两个功能。因此,我们将构建一个二元分类器。
我们将首先将数据中的function [CC]
列转换成标签,这些标签可以在keras
中内置的 MLP 模型中获取。
>>> y = protein_data['Function [CC]']
>>> encoder = LabelEncoder()
>>> encoder.fit(y)
>>> encoded_y = encoder.transform(y)
在下文中,我们建立了 MLP 分类器,并运行 10 重交叉验证。
>>> kfold = 10
>>> X = pd.DataFrame(embedding)
>>> y = encoded_y>>> random_state = 1>>> test_F1 = np.zeros(kfold)
>>> skf = KFold(n_splits = kfold, shuffle = True, random_state = random_state)
>>> k = 0
>>> epochs = 50
>>> batch_size = 128>>> for train_index, test_index in skf.split(X, y):
>>> X_train, X_test = X.iloc[train_index], X.iloc[test_index]
>>> y_train, y_test = y[train_index], y[test_index]
>>> X_train = X_train.as_matrix(columns = None)
>>> X_test = X_test.as_matrix(columns = None)
>>>
>>> model = Sequential()
>>> model.add(Dense(64, input_shape = (X_train.shape[1],), init = 'uniform'))
>>> model.add(Activation('relu'))
>>> model.add(Dropout(0.5))
>>> model.add(Dense(32, init='uniform'))
>>> model.add(Activation('relu'))
>>> model.add(Dropout(0.5))
>>> model.add(Dense(1, init='uniform'))
>>> model.add(Activation('sigmoid'))
>>> model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
>>>
>>> model.fit(X_train, y_train ,batch_size=batch_size, epochs=epochs, verbose=0)
>>>
>>> y_pred = model.predict_proba(X_test).round().astype(int)
>>> y_train_pred = model.predict_proba(X_train).round().astype(int)>>> test_F1[k] = sklearn.metrics.f1_score(y_test, y_pred)
>>> k+=1
>>> print ('Average test f1-score', np.mean(test_F1))
Average test f1-score 1.0
这个数据对分类器来说太好了。我们还有另一个更具挑战性的数据集。让我们过一遍。
网络日志数据分类
这个数据样本取自这里的。这是一个网络入侵数据,包含审计日志和任何攻击作为正面标签。由于,网络入侵是一个罕见的事件,数据是不平衡的。此外,我们有一个只有 111 条记录的小数据集。这里我们将建立一个序列分类模型来预测网络入侵。
每个序列中包含的数据是一系列的活动,例如,{login, password, …}
。输入数据序列中的字母已经被编码成整数。原始序列数据文件出现在这里。
与之前类似,我们将首先为分类器准备数据。
>>> darpa_data = pd.DataFrame.from_csv('../data/darpa_data.csv')
>>> X = darpa_data['seq']
>>> sequences = [x.split('~') for x in X]>>> y = darpa_data['class']
>>> encoder = LabelEncoder()
>>> encoder.fit(y)
>>> y = encoder.transform(y)
在该数据中,序列嵌入应该是长度敏感的。长度在这里很重要,因为模式相似但长度不同的序列可以有不同的标签。考虑两个会话的简单例子:{login, pswd, login, pswd,…}
和{login, pswd,…(repeated several times)…, login, pswd}
。虽然第一个会话可能是普通用户输错了一次密码,但另一个会话可能是猜测密码的攻击。因此,序列长度和模式一样重要。
>>> sgt_darpa = Sgt(kappa = 5, lengthsensitive = True)
>>> embedding = sgt_darpa.fit_transform(corpus=sequences)
我们在这里发现的embedding
很稀少。因此,在训练分类器之前,我们将使用 PCA 进行降维。
>>> from sklearn.decomposition import PCA
>>> pca = PCA(n_components=35)
>>> pca.fit(embedding)
>>> X = pca.transform(embedding)
>>> print(np.sum(pca.explained_variance_ratio_))
0.9862350164327149
所选的前 35 个主成分分析解释了 98%以上的差异。我们现在将继续使用keras
构建一个多层感知器。
由于数据量很小,阳性标记点的数量也很少,我们将进行三重验证。
>>> kfold = 3
>>> random_state = 11>>> test_F1 = np.zeros(kfold)
>>> time_k = np.zeros(kfold)
>>> skf = StratifiedKFold(n_splits=kfold, shuffle=True, random_state=random_state)
>>> k = 0
>>> epochs = 300
>>> batch_size = 15>>> class_weight = {0 : 0.12, 1: 0.88,} # The weights can be changed and made inversely proportional to the class size to improve the accuracy.>>> for train_index, test_index in skf.split(X, y):
>>> X_train, X_test = X[train_index], X[test_index]
>>> y_train, y_test = y[train_index], y[test_index]
>>>
>>> model = Sequential()
>>> model.add(Dense(128, input_shape=(X_train.shape[1],), init='uniform'))
>>> model.add(Activation('relu'))
>>> model.add(Dropout(0.5))
>>> model.add(Dense(1, init='uniform'))
>>> model.add(Activation('sigmoid'))
>>> model.summary()
>>> model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
>>>
>>> start_time = time.time()
>>> model.fit(X_train, y_train ,batch_size=batch_size, epochs=epochs, verbose=1, class_weight=class_weight)
>>> end_time = time.time()
>>> time_k[k] = end_time-start_time>>> y_pred = model.predict_proba(X_test).round().astype(int)
>>> y_train_pred = model.predict_proba(X_train).round().astype(int)
>>> test_F1[k] = sklearn.metrics.f1_score(y_test, y_pred)
>>> k += 1
>>> print ('Average Test f1-score', np.mean(test_F1))
Average Test f1-score 0.5236467236467236>>> print ('Average Run time', np.mean(time_k))
Average Run time 9.076935768127441
这是一个很难分类的数据。为了有一个宽松的基准,让我们在相同的数据上建立一个更好的 LSTM 分类器。
>>> X = darpa_data['seq']
>>> encoded_X = np.ndarray(shape=(len(X),), dtype=list)
>>> for i in range(0,len(X)):
>>> encoded_X[i]=X.iloc[i].split("~")
>>> max_seq_length = np.max(darpa_data['seqlen'])
>>> encoded_X = sequence.pad_sequences(encoded_X, maxlen=max_seq_length)>>> kfold = 3
>>> random_state = 11>>> test_F1 = np.zeros(kfold)
>>> time_k = np.zeros(kfold)>>> epochs = 50
>>> batch_size = 15
>>> skf = StratifiedKFold(n_splits=kfold, shuffle=True, random_state=random_state)
>>> k = 0>>> for train_index, test_index in skf.split(encoded_X, y):
>>> X_train, X_test = encoded_X[train_index], encoded_X[test_index]
>>> y_train, y_test = y[train_index], y[test_index]
>>>
>>> embedding_vecor_length = 32
>>> top_words=50
>>> model = Sequential()
>>> model.add(Embedding(top_words, embedding_vecor_length, input_length=max_seq_length))
>>> model.add(LSTM(32))
>>> model.add(Dense(1, init='uniform'))
>>> model.add(Activation('sigmoid'))
>>> model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])>>> start_time = time.time()
>>> model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=1)
>>> end_time=time.time()
>>> time_k[k]=end_time-start_time>>> y_pred = model.predict_proba(X_test).round().astype(int)
>>> y_train_pred=model.predict_proba(X_train).round().astype(int)
>>> test_F1[k]=sklearn.metrics.f1_score(y_test, y_pred)
>>> k+=1>>> print ('Average Test f1-score', np.mean(test_F1))
Average Test f1-score 0.0>>> print ('Average Run time', np.mean(time_k))
Average Run time 425.68603706359863
我们发现 LSTM 分类器给出的 F1 值为 0。这可以通过改变模型来改善。然而,我们发现 SGT 嵌入可以在不需要复杂分类器的情况下对少量且不平衡的数据起作用。此外,在运行时,SGT 嵌入式网络明显更快。平均耗时 9.1 秒,而 LSTM 模型耗时 425.6 秒。
结束语
- 我们学习了使用序列嵌入进行序列聚类和分类。
- 这个嵌入是这个论文的一个实现。这篇文章中没有涉及到,但是可以参考这篇文章来看看 SGT 嵌入相对于其他嵌入的准确性比较。
- 由于 SGT 嵌入捕捉长期和短期模式的能力,它比大多数其他序列建模方法工作得更好。
- 建议您使用调谐参数
kappa
,查看其对精确度的影响。
学分:
- 萨曼内·易卜拉希米博士,他是 SGT 论文的合著者,对上述代码做出了重大贡献。
- Yassine Khelifi ,为 SGT 编写第一个 Python 版本的数据科学专家。
吴恩达的序列模型——11 个经验教训
我最近在 Coursera 上完成了吴恩达深度学习专业化的第五门也是最后一门课程: 序列模型 。Ng 很好地描述了创建你自己的递归神经网络所涉及的各种建模复杂性。这门课程中我最喜欢的部分是编程练习。特别是,最后的编程练习让您实现一个触发词检测系统。这些系统可以用来预测一个人何时说“Alexa”,或者预测金融触发事件的时间。
在监督学习中,序列模型可以用于解决各种应用,包括金融时间序列预测、语音识别、音乐生成、情感分类、机器翻译和视频活动识别。唯一的约束是输入或输出是一个序列。换句话说,您可以使用序列模型来解决任何类型的监督学习问题,该问题在输入层或输出层包含时间序列。
在这篇文章中,我将讨论我在课程中学到的 11 个关键经验。我还写了一些文章,详细介绍了我从专业领域的前 4 门课程中学到的其他重要经验。对于我的计算机视觉文章,点击这里。其他深度学习课程,点击这里。
第 1 课:为什么不是标准网络?
传统的前馈神经网络不在网络的不同位置共享特征。换句话说,这些模型假设所有的输入(和输出)都是相互独立的。该模型在序列预测中不起作用,因为先前的输入在预测下一个输出中固有地重要。例如,如果您要预测文本流中的下一个单词,您可能希望至少知道目标单词之前的几个单词。
传统的神经网络要求输入和输出序列长度在所有预测中保持不变。正如第 2 课所讨论的,序列模型网络可以直接解决这个问题。
第二课:RNN 建筑有哪些类型?
如引言中所讨论的,序列模型可以处理各种序列预测应用。在本课程中,讲师将讨论各种网络类型,包括一对一、一对多、多对一和多对多网络。
在音乐生成中,输入可以是空集,输出可以是歌曲(一对多)。在高频金融波动性预测中,输入可能是过去 3 分钟的报价和交易流,输出将是波动性预测(多对一)。最有趣的是,多对多架构可以处理输入和输出序列长度不相同的应用,使用上图右下角所示的编码器/解码器设置。在文献中,多对多模型通常被称为序列对序列模型。
第 3 课:语言模型和序列生成是如何工作的?
语言模型通过估计下一个单词出现的概率来做出预测。在你训练了一个语言模型之后,你所估计的条件分布可能被用来对新的序列进行采样。
在家庭作业练习中,你将根据莎士比亚文本训练一个语言模型,并生成新颖的莎士比亚句子。虽然本课程只讨论基于语言的序列生成,但在其他领域还有各种其他应用。例如,在金融领域,您可以使用这种类型的模型来生成样本股票路径。您可以针对单个名称以不同的 3 分钟时间间隔训练网络,然后使用网络生成样本路径。
第四课:用 RNNs 消失渐变
rnn 可能具有以指数速度快速消失的梯度,使得网络难以学习长期相关性。分解渐变问题不大,因为你可以很容易地应用一个简单的渐变裁剪算法。消失的渐变也很难发现,这使得在将系统部署到生产环境中时更加危险。
第五课:捕捉长期依赖
门控递归单元(gru)可用于通过添加两个门(更新和重置门)来解决消失梯度问题,这两个门跟踪与预测最相关的信息。更新门用于确定有多少过去的信息需要传递到下一个时间步。重置门用于确定有多少信息是不相关的,应该被遗忘。LSTM 细胞也可用于解决长期依赖。
第六课:GRU vs LSTM 细胞
LSTM 细胞有一个额外的门,因此更复杂,需要更长的时间来训练。理论上,LSTM 细胞应该能够记住更长的序列,代价是增加训练时间;然而,没有明确的经验证据表明任何一个网络在所有情况下都优于另一个。讲师建议从 gru 开始,因为它们比 LSTM 单元更简单,可扩展性更强。
第 7 课:使用单词嵌入进行迁移学习
单词嵌入可以被认为是给定单词的向量表示。可以使用 Word2Vec、负采样或 Glove 算法来训练它们。单词嵌入模型可以在非常大的文本语料库(比如 100 个单词)上被训练,然后可以在具有较少数量的训练示例(比如 10,000 个单词)的序列预测任务上被使用。例如,情感分类可以使用单词嵌入来大大减少生成准确模型所需的训练示例的数量。在下图中,E 代表单词嵌入矩阵。
第 8 课:设计一个没有不良偏差的算法
预测任务正被用于做出越来越重要的决策。那么,我们如何设计一个不受性别、种族和社会经济偏见影响的算法呢?一些研究人员同意,这个问题在计算机程序中比在人类中更容易解决。使用一种类似于主成分分析的方法,我们可以识别偏差和非偏差子空间。那么对于每一个非定义性的单词,我们可以将这个单词投影为零偏差。最后,我们可以均衡成对的单词来中和剩余的偏差。例如,我们希望祖母和保姆之间以及祖父和保姆之间的距离相等。
第九课:使用搜索算法的机器翻译
给定一个英语句子,搜索算法可以用来生成最可能的法语句子。波束搜索是一种常用于此任务的算法。该算法的贪婪性由波束宽度参数定义。如果你想不那么贪婪,你可以将波束宽度设置为一个更大的正整数。在诊断错误时,可以确定错误是由于波束搜索算法不准确还是您训练的 RNN 模型。
第十课:序列对序列模型中的注意力模型
不严格地说,注意力模型是基于人类的视觉注意力机制。该算法试图根据目前看到的输入序列来学习应该注意什么。注意力模型在诸如神经机器翻译的任务中非常有用。有一个家庭作业,让你自己实现这个模型。
第 11 课:序列对序列模型的语音识别
序列到序列模型允许从业者对语音识别应用采取更简单的端到端方法。从前,语音识别系统是用音素构建的。然而,随着大数据的兴起,我们可以使用端到端的深度学习,并完全去除人工音素和特征工程步骤。
触发词检测系统是本课程中描述的语音识别的最终应用。你会在自己的作业练习中实现这样的算法。我亲自训练了一个网络来开关我的灯。触发检测算法在其他领域也有各种应用,例如金融经济学;也许你可以训练一个算法来检测股票时间序列中的事件/峰值。
结论
总的来说,序列模型的大量应用使本课程非常值得你花时间。家庭作业也让你练习自己实现实际的系统。我上面解释的课程仅仅代表了本课程中材料的一个子集。通过学习这门课程,你将仅仅触及序列模型的表面,但它可能足以启动一个人工智能的机会或职业生涯。我写这篇文章没有得到 deeplearning.ai 的背书。
如果你对序列模型有任何有趣的应用想分享,请在下面的评论中告诉我。我很乐意讨论新项目的潜在合作。
这就是所有人——如果你已经做到了这一步,请在下面评论并在 LinkedIn 上加我。
我的 Github 是这里的。
其他深度学习课程博客
其他有趣的文章
Python 中的序列匹配器
一个人性化的 最长连续&无垃圾 序列比较器
SequenceMatcher 是 python 模块中可用的类,名为**“diff lib”。**可用于比较成对的输入序列。本文的目的是通过一个示例来解释 SequenceMatcher 算法。由于可用的文档有限,我想通过一步一步的例子来分享这个概念,这可以帮助读者以清晰的方式理解整个过程。
引用原始文件:
基本思想是找到不包含 【垃圾】 元素的最长连续匹配子序列***【LCS】**。这不会产生最小的编辑序列,但是会产生与人的 【向右看】 匹配的序列。*
等一下。上一段描述了这么多专业术语。我们按顺序解码吧。我希望任何阅读这篇文章的人都知道算法 LCS 。基本上,LCS 的目标是:给定两个序列,找出两个序列中最长的子序列的长度。在这里,我们也试图在一对序列之间找到类似于 LCS 的东西,但它们在数学上不是 100%优雅的。换句话说,结果应该让用户更满意,这就是为什么它被称为对人们来说“看起来合适”的匹配。为了避免任何早期的混淆,让我们观察一个我在 堆栈溢出 上找到的例子来理解这两者之间的区别。
输入字符串: my stackoverflow mysteries
和 mystery
对任何人来说,自然的搭配是"myster"
如下:
*my stackoverflow* ***myster****ies .................****myster****y..*
事实上,这就是 SequenceMatcher 算法在处理上述字符串时返回的输出。
然而,LCS 将输出"mystery"
***my******st****ackov****er****flow m****y****steries* ***my****.****st****.....****er****......****y****.......*
因为对于人类专家来说,较长的公共子序列可能比较短的公共子序列显得不那么自然。因此,我们认为 SequenceMatcher 试图找出对人类更友好的输出。
继续向前,接下来是 【垃圾】 元素的概念。【垃圾】是你不希望算法匹配的东西:比如普通文本文件中的空行,或者可能是 HTML 文件中的“< P >”行等。
SequenceMatcher 对象初始化为:
init(isjunk=None,a= ’ ',b= ’ ')
isjunk 参数为您提供了定义您希望算法视为垃圾的任何元素的选项。
>如果是垃圾没有定义:
>如果是垃圾被定义为认为空白是垃圾:
将空白视为垃圾,可以防止“abcd”与第二个序列末尾的“abcd”直接匹配。相反,只有“abcd”可以匹配,并且匹配第二个序列中最左边的“abcd”。
序列匹配器流程图
给定两个输入字符串 a 和 b,
- ratio( ) 返回输入字符串之间的相似性得分(float in [0,1])。它对函数 get_matching_blocks 返回的所有匹配序列的大小求和,并计算比率为:ratio = 2.0 * M/T,其中 M =匹配,T =两个序列中的元素总数
- get_matching_blocks( ) 返回描述匹配子序列的三元组列表。最后一个三元组是哑元,(len(a),len(b),0)。它通过重复应用 find_longest_match()来工作
- find_longest_match( ) 返回包含 a[aLow:aHigh]和 b[bLow:bHigh]中最长匹配块的三元组
再解释一下
在深入这个例子之前,让我们多了解一下上面两个重要的函数,这肯定会帮助我们完美地理解这些概念。我试图以一种简单的方式只解释重要的部分。关于完整的 python 代码,建议读者访问 官方网站
get_matching_blocks()解释
Code Snippet of function get_matching_blocks( )
基本上,维护一个队列,该队列用包含两个输入字符串的上限和下限的索引的四元组来初始化。直到队列中有一个四元组可用,它被弹出并传递给 find_longest_match( ) 函数,该函数返回描述匹配子序列的三元组。三元组被添加到 matching_blocks 列表中。
每个三元组的形式为(I,j,n),表示 a[i:i+n] == b[j:j+n]
匹配子序列左边和右边的序列片段被进一步添加到队列中。重复该过程,直到队列为空。
然后对 matching_blocks 列表进行排序,并作为输出返回。
find_longest_match()解释
Code Snippet of find_longest_match( )
这将包含字符串的上限和下限索引的三元组作为输入,并返回包含最长匹配块的三元组。
首先,定义一个字典b2j其中对于字符串 b 中的 x,b2j[x]是 x 出现的索引(到 b 中)的列表。
现在在外循环中逐字符扫描第一个字符串,我们使用 b2j 检查该字符在字符串 b 中的出现。如果有匹配,我们更新另一个字典 newj2len,它有助于记录到目前为止匹配的字符串的长度。并且相应地,变量 besti、bestj 和 bestsize 被更新,这考虑了迄今为止获得的最长匹配块数据。
在所有最大匹配块中,该算法返回在 a 中最早开始的块,在所有最大匹配块中,它返回在 b 中最早开始的块。
我想这些理论已经足够了,现在让我们深入一个例子,它将帮助我们详细地理解整个工作。
一个有插图的例子
让我们通过使用一对输入字符串实现算法来描述算法的一步一步的过程。
根据经验,ratio()值超过 0.6 意味着序列非常匹配。这里,通过计算,我们已经获得了 0.8 的相似性得分比率,因此输入的序列对被视为相似的。
为了交叉验证我们的计算结果,让我们用 python 运行 SequenceMatcher。
万岁!!!我们已经达到了预期的产量。
我希望浏览上面的例子能够帮助读者了解序列相似性算法的工作原理。除了相似性,SequenceMatcher 类中还有许多其他功能。建议读者访问 官方代号 。
到那时,干杯!!
参考文献:
https://github . com/python/cpython/blob/master/Lib/difflib . py
无服务器:它能简化数据科学项目吗?
你认为数据科学家在开发和测试模型上花了多少时间?根据谷歌的 Josh Cogan 的这篇文章,只有 10%。他们的大部分时间都花在数据收集、构建基础设施、开发运维以及集成上。当您最终构建一个模型时,将它交付到产品中的过程有多长、多复杂(假设您已经完成了这一步)?当您最终将模型整合到一些有用的商业应用程序中时,您如何再现或解释其结果?你监控它的准确性吗?而持续的应用和模型升级呢?
简化数据科学开发和加快生产时间的一种方法是采用无服务器架构进行数据收集、探索、模型培训和服务。
这篇文章将解释无服务器及其局限性,并提供一个使用无服务器解决数据科学挑战的实际例子。
无服务器概述
“无服务器”一词是几年前由亚马逊创造的,用来描述其λ功能,一种开发者编写一些代码和规范并点击“部署”的服务 Lambda 自动构建容器化的应用程序,将其部署在生产集群上,并提供自动监控、日志记录、扩展和滚动升级。无服务器的其他好处是更低成本的按使用付费模式及其与平台资源和事件的本机集成。
总体而言,无服务器解决了三个主要的数据科学挑战:
1.它减少了开发、部署和监控代码的开销和复杂性。无服务器带来了更快的生产时间,并允许数据科学家和开发人员专注于业务逻辑,而不是工具和基础设施相关的任务。
2.它提供了一个定义明确、版本化的运行时,其中包含了代码、软件包依赖、ML 模型、环境变量、数据源和运行时配置。这可以保证一致和可再现的结果。
3.Serverelss 允许将功能移动、扩展和复制到多个实施点。我们在 Iguazio 看到的一个很好的例子是在云中开发和测试功能,然后在边缘网关或多个区域运行它们,或者只是扩展它们以满足不断增长的流量。
无服务器已经在应用程序和前端开发人员中引起了广泛关注和广泛采用。我们看到它在一些数据科学口袋中被投机性地使用,但它尚未在数据科学家中得到广泛采用。
无服务器可以解决上面概述的许多数据科学挑战,所以我很惊讶“无服务器”的发明者亚马逊没有在亚马逊自己的数据科学平台 SageMaker 中使用 Lambda。你想过为什么吗?
原因很简单!第一波无服务器功能,如 Amazon Lambda,不太适合数据科学,因为:
- 它们是无状态的,数据科学通常是关于大量数据(状态)的。
- 它们速度慢且是单线程的,而数据处理需要并行性。
- 它们限制了函数图像的大小,ML 模型往往很大(大于 Lambda 的最大图像大小限制)。
- 它们不支持 GPU,而 GPU 在 AI/ML 应用中可能相当高效。
- 数据科学工具/框架和无服务器之间没有本机集成。
但是上述所有限制都与云供应商特定的无服务器实现相关,而不是一般的无服务器实现。其他实施确实在满足数据科学需求的同时提供了无服务器优势。
Nuclio,一个针对数据科学优化的无服务器框架
Nuclio 从一开始就被设计为一个开源项目,旨在解决数据科学应用并消除开发和运营的复杂性。Nuclio 提供了极致的性能,支持有状态和数据密集型工作负载,支持并最大限度地提高 GPU 的效率,并且没有任何阻碍它的“包袱”(参见: Nuclio 与 Lambda 比较了解详细信息)。
Nuclio 运行在广泛采用的 Kubernetes 编排层上,并与各种数据科学和分析框架相集成。
Nuclio 使用高性能并发执行引擎(每个进程支持多达 400,000 个事件)。它是目前唯一一个具有 GPU 支持和快速文件访问的无服务器框架,可以加速机器学习代码的性能,并最大限度地利用 CPU 和 GPU(使用资源池模型),以便代码在 Nuclio 上运行更快。
Nuclio 最酷的特性之一是它与 Jupyter 的集成,Jupyter 是最广泛使用的数据科学工作台。这使得开发人员可以自动将数据科学代码和笔记本转变为可扩展的实时功能,这些功能提供数据集成、版本控制、日志记录、监控、安全性和开箱即用的可扩展性,而无需任何额外的编码。
Nuclio 广泛用于各种数据科学任务:
- 数据收集器、ETL、流处理
- 数据准备和分析
- 超参数模型训练
- 实时模型服务
- 特征向量组合(实时数据准备)
以下示例演示了如何从笔记本开发、测试和部署功能。源代码、更详细的文档和各种示例可在 Nuclio-Jupyter git 中找到。
示例:实现自然语言处理功能
完整笔记本见本 Github 链接。
第一步是在 Docker 或 Kubernetes 上部署 Nuclio。参见说明或者直接使用托管版本。
现在,使用以下命令在您的笔记本中安装 python 和 Jupyter SDK:
!pip install nuclio-jupyter
当使用 Nuclio-Jupyter 时,我们在笔记本中编写和测试我们的代码,就像任何其他数据科学代码一样。我们添加了一些“神奇的”命令来描述额外的配置,例如要安装哪些包、代码将如何被触发(http、cron、stream)、环境变量、我们想要捆绑的额外文件(例如 ML 模型、库)、版本控制等。
我们使用 deploy 命令或 API 将笔记本自动转换为代码,然后以容器化的功能进行部署。我们希望从函数中排除的代码单元格标有“# nuclio: ignore”注释。
接下来的部分演示了 Nuclio 库的导入(在一个“被忽略”的单元中),接着是要安装的包的规范、环境变量的设置和构建配置(例如,指示基本 Docker 映像)。
下一部分将实现我们的代码,该代码接受来自触发器的一些文本(例如 HTTP POST 请求),并使用“TextBlob”库来更正、分析情感,并用翻译成另一种语言的文本进行响应(语言由环境变量指定)。
一旦我们实现了我们的代码,我们就使用模拟的上下文和事件对象在笔记本中测试它。请注意,我们的代码使用了日志,这些日志也会在调试时显示。
下一步是将该功能部署到我们的集群上,并服务于真实的用户请求——使用单个“deploy”命令。一旦部署过程完成,它还将为函数提供一个 URL 分配,然后我们可以用它来测试我们的新函数。
该示例演示了如何在熟悉的数据科学环境中创建代码。我们使用一些神奇的装饰来指定我们的生产需求(包依赖关系、环境和配置),并将其部署到生产集群中,而没有考虑底层基础设施。
这是一个简单的例子。更高级的功能示例可以在下面的存储库中找到,包括 Twitter 和股票数据收集和分析、图像识别和预测基础设施监控的实现。
Nuclio 并不局限于 Python 代码。它支持 8 种编码语言,允许我们为工作选择正确和最熟悉的工具。参见这个例子,了解如何通过几个简单的步骤,在不了解 Docker 的情况下,将一个 bash 脚本变成一个交互式的无服务器函数。
摘要
无服务器让我们专注于数据科学逻辑!它使我们的代码生产准备就绪。函数使用结构化日志记录。它们针对安全性进行了强化,并提供自动横向扩展和纵向扩展,以最大限度地提高性能和资源效率。假设功能是容器化的工件,我们可以在一个地方开发和测试它们,然后将它们部署到一个或多个其他位置(例如,在边缘设备或区域集群上)。在另一篇文章中会有更多的介绍。
尝试一下,如果你遇到任何问题,可以去纽克利奥的社区休闲频道。
使用 Dask、Amazon ECS 和 Python 的分布式数据预处理(下)
Source: pixabay.com
使用 Dask 进行 EDA 和超参数优化(HPO)
在本系列的第 1 部分中,我解释了如何在 AWS Fargate 上构建 Dask 调度器和工作器的无服务器集群。上下调整工人数量相当简单。您可以通过运行以下 AWS CLI 命令来实现这一点:
bash~# aws ecs update-service — service Dask-Workers — desired-count 10 — cluster Fargate-Dask-Cluster > /dev/nullbash~# aws ecs update-service — service Dask-Scheduler — desired-count 1 — cluster Fargate-Dask-Cluster > /dev/null
既然我已经扩展了无服务器 Fargate 集群,让我们尝试一些探索性数据分析(EDA)。我使用的是著名的 NY Yellow Taxi 2017 数据集。
首先,让我们将数据帧装入集群内存。为此,只需从 dask 库中导入 dataframe 类,然后使用 read_csv() 函数加载文件:
***client . persist()***函数的工作原理是将数据异步保存到内存中,然后立即返回。它将任务图提交给集群,并返回一个 Future 对象。然后,我们可以在输出数据帧上运行快速查询。让我们进行一些分析:
1.1 数据框架分析
让我们在数据框架上得到一些描述性的统计数据。代码看起来应该完全一样,只是对于 Dask dataframes,您需要添加一个 compute() 函数,以便在笔记本上立即获得结果。如您所见,在数据帧上进行一些复杂的计算需要大约 7 秒钟,例如:(计算分位数、平均值、计数、最小值、最大值和标准偏差)。
1.2 计算总行程距离,并为每个供应商计算:
完成这条 groupby() 语句大概花了 5.5 秒。如您所见,供应商 2 的行程比供应商 1 多得多,行驶的距离也长得多。
1.3 计算每个特征的缺失值:
数据集中没有缺失值。
1.4 显示特征之间的相关性:
有一些明显的相关性,如票价金额和行程距离、小费金额和票价金额之间的高度正相关。我需要移除高度相关的要素(total_amount)以避免类似于多重共线性问题的问题。
让我们再试一次 1.2 但是这一次,我将把数据保存在内存中,并让它在后台工作,同时我做一些其他的任务。我还将打印出进度条,以查看任务何时完成。
Persist function progress bar
请注意,当使用集群工作线程在后台加载/计算数据时,提示符会立即返回。
当进度条完成后(全部为绿色),可以快速查询数据的输出。请注意,当使用集群工作线程在后台加载/计算数据时,提示符会立即返回。
使用 Dask 进行机器学习:
机器学习中计算量最大的任务之一是超参数优化(HPO)。HPO 是一种用于调整在训练过程中没有学习到的 ML 参数的技术,例如学习速率、优化器、正则化或神经网络中的隐藏层数。它的工作原理是为您要调优的每个超参数探索预定义范围的搜索空间。有许多库和技术可以实现 HPO 过程,但是在本文中,我将重点关注使用网格搜索技术和 Python 中的 Scikit-Learn 库来调优超参数。
HPO 使用网格搜索:
网格搜索技术是一种穷举搜索,通过手动指定学习算法的超参数空间的子集。然后,该算法将遍历每个超参数的组合,旨在(最大化/最小化)客观度量(准确性/损失)。它最终会给出最好的结果,但是优化的超参数越多,优化过程就越复杂。
2-dimensional Grid Search for HPO
HPO 在 Dask 集群上使用网格搜索:
由于 HPO 过程计算量大,我们将在 Dask 集群上运行它,以便利用其规模和弹性。Scikit-learn 使用一个名为 joblib 的非常强大的库来跨多个 CPU 内核并行化进程。
Joblib 也为其他并行系统提供接口,成为执行引擎。我们可以通过使用parallel_backend
上下文管理器在集群中运行数千个内核来实现这一点:
首先,我们需要从 sklearn externals 导入 joblib ,然后注册 Dask Distributed 作为 joblib 的并行后端引擎。
from sklearn.externals.joblib import _dask, parallel_backend
from sklearn.utils import register_parallel_backend
from sklearn.externals.joblib import parallel_backendregister_parallel_backend('distributed',_dask.DaskDistributedBackend)
然后,我们需要运行下面一行来开始使用集群作为执行引擎:
with parallel_backend('distributed', scheduler_host='dask-Scheduler.local-dask:8786'):
<Normal sklearn Code>
然后你的 sklearn 代码逻辑保持完全一样,没有变化。
以下是使用 HPO 网格搜索为随机森林分类器查找最佳超参数的完整代码,该分类器将对 MNIST 数据集中的手写数字进行分类:
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifierimport numpy as np
from time import timefrom sklearn.externals.joblib import _dask, parallel_backend
from sklearn.utils import register_parallel_backend
register_parallel_backend('distributed', _dask.DaskDistributedBackend)# Loading the Digits dataset
digits = datasets.load_digits()# To apply an classifier on this data, we need to flatten the image, to
# turn the data in a (samples, feature) matrix:
n_samples = len(digits.images)
X = digits.images.reshape((n_samples, -1))
y = digits.target# Split the dataset in two equal parts
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)clf = RandomForestClassifier(n_estimators=20)# use a full grid over all parameters
param_grid = {"max_depth": [3,4,5,6, None],
"max_features": [1, 3, 10, None],
"min_samples_split": [2, 3, 10],
"bootstrap": [True, False],
"criterion": ["gini", "entropy"]}# run grid search
grid_search = GridSearchCV(clf, param_grid=param_grid, cv=8, iid=True)start = time()
with parallel_backend('distributed', scheduler_host='dask-Scheduler.local-dask:8786'):
grid_search.fit(X, y)
clf.fit(X, y)print("GridSearchCV took %.2f seconds for %d candidate parameter settings."
% (time() - start, len(grid_search.cv_results_['params'])))results = grid_search.cv_results_
# Return the index of the best validation score
idx = np.flatnonzero(results['rank_test_score'] == 1 )
print("The best score is: " + str(results['mean_test_score'][idx[0]]))
#print the parameters for the best job
print("Parameters: {0}".format(results['params'][idx[0]]))
The output of the above code
在一个 10 节点集群上,找到分类器的最佳超参数组合需要大约 40 秒,而在一台机器上(即使有多个内核/多个 CPU),如果没有并行化特性,将需要许多分钟。
总结:
根据我使用 Dask 的经验,它是一个很好的以分布式方式预处理大型数据集的库。如果你是熊猫和 Numpy 的粉丝,并且很难将你的数据存储到内存中,那么 Dask 绝对是一个不错的选择。对于时间和成本敏感的机器学习任务,如 HPO、数据插补、数据预处理和探索性分析,这绝对是一个很好的解决方案。
无服务器 ML
AWS Lambdas 让您的模型栩栩如生。
©Philip Massie 2019
介绍
日复一日地处理争论不休的数据集,设计有趣的功能,测试和训练大量的模型,会让你感觉有点脱离现实世界的系统。嗯,至少对我来说是这样。最近,我花时间和软件工程师在一起,他们帮助我理解了如何通过利用无服务器架构,以很低的成本或零成本在云中部署 ML 模型。因此,模型或模型集合可以通过简单的 API 调用适应更大的系统。
使用 AWS Lambdas,我们不需要保持昂贵的 EC2 实例全天候运行。兰姆达斯按照要求旋转得非常快。(Google Cloud 也有类似的云功能,但这里我将重点介绍 AWS 产品。)当然,在后台,它们都运行在 EC2 服务器上,但是 Lambdas 是如此抽象,以至于你不需要担心 EC2。有很多很好的理由认为去无服务器,但我不想运行广告。你可以在这里阅读更多内容
Lambda 本质上是一个小型的快速容器,预先配置了许多不同的运行时之一。其中包括:
作为一名数据科学家,这些天我主要从事 Python 的工作,在本教程中,我将带您在 AWS Lambda 中部署一个简单的 ML 模型。这里有一个附带的 github repo 这里包括你需要的所有代码以及模型、数据和训练数据。
型号
这不是在训练一个伟大的模特。作为一个用例,我使用开普敦的历史天气数据来训练一个模型,根据开普敦的历史来预测明天下雨的可能性。你可以在世界上任何一个城市喂它,它会根据开普敦的历史预测那里明天是否会下雨。没关系,这不是重点。
点穴
如果您不熟悉 Lambdas,或者对在 EC2 实例上存放 Docker 容器的成本不抱幻想,希望这将让您了解考虑更大更复杂的模型和代码系统是多么便宜和容易。幸运的话,你的创造力会被激发。
要求
这看起来有点乏味,但是值得一试。大部分你只需要做一次。我将在这里忽略一些东西,比如 AWS 执行角色和密钥管理。此外,本教程将使用 AWS web 控制台和 CLI。如果您不熟悉 CLI,希望这将是一个简单的介绍。
Python 3.6
我们将使用 Python 3.6 Lambda 运行时环境,因为这个运行时环境附带了一个包含 scipy/numpy 的预配置层。因为这些包具有默认情况下 Lambda 环境中不存在的操作系统依赖性,所以这一层对于我们使用任何低级数学函数(如 SKLearn 中的函数)都是至关重要的。我看到 Python 3.7 运行时现在也有了 scipy/numpy 层!
码头工人
为了构建层,我们需要在与 Lambda 的远程环境相匹配的环境中本地安装 Python 包。
解决方案:Docker ( 如何安装)
如果你在使用 Ubuntu,这应该能让你开始:
sudo apt-get remove docker docker-engine docker.io
sudo apt install docker.iosudo
systemctl start dockersudo
systemctl enable docker
Github 回购
本教程有一个 github repo 可用。目录结构很重要,我假设您从项目根目录运行命令。
- project root/
├-layers/
| ├-02_dependencies/
| | └-requirements.txt
| |-03_rain_model/
| | ├-models/
| | | └-rf_rain.pkl
| | └-keys.csv.template
| ├-zips/
| └-model_training/
├-readme.md
└-rain_forecast.py
你需要一个免费账户来查询天气数据。在https://darksky.net/dev报名。注册和登录后,您的主页应该显示您的密钥。将此存储在layers/03_rain_model/keys.csv
中。
location IQ
在 https://locationiq.com 的位置 IQ 注册一个免费的地理编码账户。创建一个访问令牌并将其存储在layers/03_rain_model/keys.csv
中。
AWS 设置
账号
如果您还没有,请设置您的免费 AWS 帐户。
安装 AWS CLI
AWS CLI 提供对 AWS 的命令行访问。您需要安装它,通常使用 pip,并将其配置为使用您的 AWS 帐户。
TLDR Ubuntu 版本检查其他系统的这些说明和故障排除:
# install
pip3 install awscli --upgrade --user
# check (might need to log in to ubuntu again)
aws --version
配置
我们需要使用控制台来配置 AWS CLI。
登录到 AWS 控制台:
- 右上角,单击您的姓名>我的安全凭证下的下拉菜单
- 选择“访问密钥(访问密钥 ID 和秘密访问密钥)”
- “单击创建新的访问密钥”
- 下载带有您的密钥和秘密密钥的 csv 文件,并保存在安全的地方
回到你的终端(详细说明):
aws configure
> AWS Access Key ID [None]: ABABABABAEXAMPLE
> AWS Secret Access Key [None]:CDCDCDCDCDCDCDCDCDCDCEXAMPLEKEY
> Default region name [None]: us-west-2
> Default output format [None]: json
为项目 创建一个 S3 时段
回到 web 控制台的 S3 部分,为此项目创建一个空存储桶。在所有存储桶中,存储桶名称必须是唯一的,因此请记住这个名称。当然,您也可以使用 CLI 来完成这项工作。
我会把我在美国西俄勒冈的名字叫做severless-ml-tutorial
(以匹配我们的配置)。您需要调整下面的代码来匹配这个名称。
我认为这是唯一一个可能会产生成本的地方。S3 存储不是免费的,尽管在这个规模上它是非常便宜的。构建图层后,删除此桶以节省潜在成本。我有几个里面有几兆字节的桶,我还没有发生费用。有点混乱。
还在吗?干得好!好,让我们建立第一个简单的 Lambda。我们稍后将对此进行详述。
创建一个λ
- 在 AWS 控制台中,单击“服务”>“Lambda”。
- 单击“创建函数”创建一个新的 Lambda 函数。
- 称之为
severless-ml-lambda
,选择 Python 3.6 运行时。将权限选项设置为默认值。 - 点击底部的“创建函数”。
您的 Lambda 将被创建,其属性屏幕将打开。
添加 API 网关触发器
现在我们将配置一个 http 网关,在这里我们可以与新的 Lambda 进行交互。
- 选择“API 网关”
- API 下拉菜单>“创建新 API”
- 从“选择模板”中选择“Rest API”
- 安全性下拉菜单>打开
- 附加设置—默认
- 点击“添加”
添加一个测试事件
现在我们添加一个示例查询,这样我们就可以测试 Lambda 将如何响应 API 调用。我们将向 Lambda 发送一个城市的名称,因此让我们设置一个测试事件来完成这项工作。
- 在屏幕顶部的“选择测试事件”下拉列表中,选择配置测试事件
- 输入名称,例如“天气测试”
- 输入以下 json,模拟提交“城市”字符串“洛杉矶”的 https 事件。
{
"httpMethod": "GET",
"queryStringParameters": {
"city": "Los Angeles"
}
}
添加一些代码
现在我们终于可以添加一些代码了。这非常简单,它将传入的字符串赋给一个变量,打印出来并返回一个包含该字符串的格式化的 HTML 字符串。
设计师面板
- 突出显示您的 Lambda 名称
向下滚动到功能代码面板
-
将以下代码粘贴到代码块中(我们稍后将需要所有这些库):
-
再次靠近页面顶部,单击 Save,然后单击 Test。
-
幸运的话,你的 Lambda 刚刚执行了你用 WeatherTest json 发送的有效载荷。
-
结果应该显示在页面顶部附近的绿色面板中。
-
您也可以在那里看到 Python 打印函数的结果。
- 在设计器面板中,单击 API 网关块,向下滚动到 API 网关面板 url,然后单击 API 端点旁边的 url。
- 将
?city=Hong Kong
添加到 url 的末尾。点击 enter,您应该看到页面内容更新,以反映这个新的 querystring。
现在您已经有了一个简单的 Lambda,可以通过 URL 访问,您可以向它发送一个有效负载,它可以处理并返回有效负载,在这种情况下作为 HTTP 响应。
扩展 Lambda
层
在我们添加更多代码来做更有趣的事情之前,让我们让 Lambda 架构更强大一点。我们可以通过添加层来做到这一点。层只是你包含在 Lambda 容器中的额外资源——有大小限制要处理,所以不要太疯狂。
在这一节之后,我们可以回到代码并扩展 Lambda。如果你有 github repo ,你会看到我有构建嵌套在层目录中的每个层所需的文件。
通常,我们需要整理每一层的内容,将它们上传到 S3,将内容编译成 Lambda 层,然后将这些层附加到我们的 Lambda 上。
01 科学/数字层
这一层由 AWS 预编译,不仅包括 python 库,还包括我们需要使用的系统库。这对于你自己来说是一个非常困难的层次,但是感谢 AWS 为我们做了大量的工作。我们不需要建立这一层。
02 依赖层
这一层将包括默认安装的所有必需的 Python 库。我们将在本地机器上使用一个类似 Lambda 环境的 docker 容器(即 lambci)来整理这一层的内容。在 lambci 容器中,我们可以使用 pip 将库安装到容器外的本地目录中。然后,我们使用这个目录来构建我们的新层。
在终端中,从项目根目录:
docker run --rm -it -v ${ PWD }/layers/02_dependencies:/var/task lambci/lambda:build-python3.6 bash
# From the docker container, install the python packages
pip install -r requirements.txt --target python/lib/python3.6/site-packages/ --no-deps --ignore-installed exit
如果工作正常,你应该会在你的“02_dependencies”图层目录中看到一个“python”目录。
压缩图层内容
我们需要压缩图层内容,为上传到 S3 做准备。
# clear any old versions first
rm layers/zips/02_dependencies.zip
pushd
layers/02_dependencies zip -r ../zips/02_dependencies.zip python popd
上传图层到 S3
使用 AWS CLI 而不是 web 控制台,将您的 zip 目录的内容与 S3 的一个位置同步。保持 web 控制台打开是一个好主意,这样可以看到您正在做的事情是否达到了预期的效果。
aws s3 sync layers/zips s3://severless-ml-tutorial/lambdas/layers --delete
现在我们可以使用上传的 zip 文件来更新或创建一个 Lambda 层,它可以在任何 Python 3.6 Lambda 中使用。记得在 S3Bucket=旁边更改您的存储桶的名称。
aws lambda publish-layer-version --layer-name weather_02_dependencies --description "dependencies layer" --content S3Bucket =severless-ml-tutorial,S3Key=lambdas/layers/02_dependencies.zip --compatible-runtimes python3.6
03 雨水模型层
该层的文件已经存在于layers/03_rain_model/
目录中,所以这里不需要 Docker hijinks。你需要把你的 api 密匙放到名为keys.csv.template
的文件中,并把它重命名为keys.csv
。
不管这个层的名称和主要目的是什么,除了模型对象之外,我还将包括 API 键。注意,这不是管理云中密钥的正确/安全方式。
重要的钥匙不要这样!
压缩图层内容
我们需要压缩图层内容,为上传到 S3 做准备。
# clear any old versions first
rm layers/zips/03_rain_model.zip
pushd layers/03_rain_model/
zip -r ../zips/03_rain_model.zip .
popd
上传图层到 S3
像以前一样,上传图层内容到 S3…
aws s3 sync layers/zips s3://severless-ml-tutorial/lambdas/layers --delete
构建/更新图层 03_rain_model
…并构建图层。请记住在此处更改您的存储桶名称。
aws lambda publish-layer-version --layer-name weather_03_model --description "model layer" --content S3Bucket=severless-ml-tutorial,S3Key=lambdas/layers/03_rain_model.zip --compatible-runtimes python3.6
检查图层是否已成功创建
使用 AWS web 控制台确保图层存在。
S3 部分
- 确认图层 zip 文件存在于 S3。
兰姆达斯区
- 确认已创建新层。
- 服务> Lambda >层>…
- 每次你更新一个现有的层,它会得到一个新的版本。
将新层添加到您的 Lambda 中
我们将把所有需要的层添加到我们的 Lambda 中,然后我们就完成了设置。
在 web 控制台中,打开您的 Lambda
- 服务> Lambda >单击无服务器-ml-lambda
在设计器窗格中,单击“层”。
- 向下滚动到图层窗格,使用“添加图层”按钮,添加以下 3 个图层的最新版本:
- AWSLambda-Python36-SciPy1x 版本 2
Python 3.6 numpy/scipy+OS deps 层) - weather_02_dependencies
我们的 Python 库依赖 weather_02_dependencies - 天气 _ 03 _ 模型
我们的随机森林模型和密钥文件层天气 _ 02 _ 模型
别忘了点击保存!
这就是我们的计划。如果你已经做得很好了!把这个写出来,好像很长。现在剩下的就是构建代码了!
构建代码
下面的每一步都是对上一步代码的扩展。要跟进并查看功能如何增长,请将每个代码块添加到前一个代码块的底部,覆盖前一个代码的返回括号。在每一步更新和保存之后,重新加载 API 端点 URL 并提交一个不同的城市。
或者,在 severless-ml-lambda.py 中找到完整的代码。
添加地理位置
这个模块加载 API 密钥,并使用 LoationIQ 密钥对您提交的城市进行地理编码。这些结果被打印出来并通过 http 返回。
添加天气查询
该代码采用 DarkSky 键和 LocationIQ GPS 坐标,并返回您提交的城市的当前天气情况。
添加模型预测
添加此代码块以加载您在第 3 层中推送的模型,并预测明天是否会下雨。
注意,当你测试这个的时候,你会遇到内存错误。向下滚动到基本设置面板,将 Lambda 的内存增加到 1024 Mb。当你在那里时,把超时时间增加到 10 秒。
向 S3 添加文字
最后一步是将每个查询的结果写到 S3。这只是为了展示与 Python 和 Lambdas 的 AWS 基础设施进行交互是多么简单。
结论
诚然,这是一个相当长的教程。希望它已经阐明了如何通过使用无服务器函数将一个训练好的模型合并到一个更大的代码库中。享受吧。
使用 PySpark 和 GCP 的无服务器推荐系统
我的在线电影推荐系统的幕后以及如何与谷歌云平台交互。
Photo by Noom Peerapong on Unsplash
“网飞如何预测我的品味?”这是我进入数据科学之前想到的问题。让我对这个领域产生了好奇。
最近我觉得是时候回答这个问题了。所以,我决定创建一个向注册用户推荐电影的 web 应用程序。
我的主要目标是让它在线并且完全没有服务器。在这段旅程中,我面临了几个挑战。
首先,我将简要提及这些挑战,然后我们可以深入了解该项目的细节。
挑战:
- 由于我使用了 ALS 矩阵分解协同过滤算法,主要问题是冷启动问题。也就是说,如果没有之前关于用户的数据,算法根本无法向用户推荐产品(这里指的是电影)。因此,为了避免冷启动问题,我应该要求用户在我的应用程序中指定以前看过的电影。
Asking the user to specify 5 movies.
- 由于这是一个有趣的项目,我决定使用谷歌云平台免费层。因此,Apache Spark 集群不允许我使用超过 24 个 vCPUs。我选择了 1 个主节点和 2 个工作节点,主节点有 8 个 vCPUs,内存为 30 GB,工作节点的属性与主节点相同。使用该集群生成建议大约需要 20 分钟。我必须在等待这些推荐的同时向用户提供快速推荐和最佳电影(我将在下面给出更多关于我如何确定最佳电影以及我如何生成快速推荐的信息,请耐心等待)。
- 我选择用 App Engine 的标准环境,比灵活环境便宜。标准环境不允许使用用 Python 以外的编程语言实现的库。所以,当我需要它的功能时,我不能在我的应用程序中使用熊猫。我应该在 python 的标准 CSV 库中复制我的应用程序所需的一些功能。
深入细节。
首先,ALS 矩阵分解是如何工作的?让我们深入了解一下:
自网飞价格挑战以来,这种算法广为人知。这是一种用于稀疏评级数据的最先进的协同过滤算法。简而言之,矩阵分解是将矩阵分解成具有较低维度的矩形因子矩阵的乘积(在这种情况下是用户和电影矩阵的乘积)。在用户矩阵中,列是潜在特征,行是用户。在项目(电影)矩阵中,行代表潜在特征,列是项目(电影)。
Factorization of user-movie sparse matrix. “d” is latent features.
矩阵分解有其假设:
- 每个用户可以用 f 个潜在特征来描述(潜在特征的数量是我们应该确定的该算法的超参数)。例如,第一个潜在特征可能是表示每个用户喜欢恐怖电影多过喜欢惊险电影的比率,或者可能是表示每个用户喜欢浪漫电影多的数字。
- 每部电影都可以用一组与用户矩阵中相似的潜在特征来描述。例如,第一个潜在特征可能是一个数字,表示特定电影的类型与恐怖电影有多接近。
- 我们可以将用户的每个潜在特征乘以电影的相应特征,然后将所有特征加在一起,这应该会给我们一个用户对特定电影的评级的良好近似值(这就是我如何为我的应用程序生成快速推荐)。
我将用户的因子矩阵称为**“X”,电影的因子矩阵称为“Y”**。对了,我们不知道潜在特征代表什么,也不用知道。—我们只需要计算出潜在特征的值,然后确定每部电影的未知用户评级只是简单的矩阵乘法问题。
为了预测潜在特征的值,我们将使用交替最小二乘法。
最小平方法的基本形式是用某条线来拟合数据,测量所有点到这条线的平方距离的总和,并尝试得到缺失点的最佳拟合。
使用交替最小二乘法,想法是相同的,但是我们在优化 Y 和固定 X 和 v.v .之间交替迭代。使用这种迭代优化过程的主要目标是尝试更接近原始数据的分解表示。
通过这样做,我们首先使用 X 估计 Y (我们优化 Y 的值并保持 X 固定)并使用 Y 估计 X (我们优化 X 的值并保持 Y 固定)。经过足够次数的迭代(这是我们应该确定的模型的超参数),我们的目标是达到一个收敛点,在这里 X 和 Y 矩阵足够接近我们原始数据的因式分解表示,并且这些矩阵的损失函数最小化。
然而,有一个关于数据的小问题。我们的数据很少,这意味着我们没有每部电影的完整用户评级对:)。这就是我们试图预测那些缺失的评分并建立推荐引擎的原因。所以,我们应该惩罚那些没有分级的电影。在我们这样做之后,我们不必处理没有来自用户的评级的电影,并且不围绕推荐中没有评级的电影做出任何假设。因此,为了惩罚未分级电影,我们需要一个权重矩阵,其中未分级电影的权重为 0,分级电影的权重为 1。我将这个权重矩阵称为“W”。
因为我们必须优化 X 和 Y 矩阵。将有两个对应于 X 和 Y. 的成本函数
J(Xu) is a cost function for the user factor matrix and J(Yi) is a cost function for the movie factor matrix.
请注意上述成本函数中的最后一项。这些是正则项,也是这个算法的最后一个超参数。它们在那里是为了避免过度配合。换句话说,他们在优化过程中不让权重有高值。顺便说一下,您应该通过使用交叉验证集(而不是测试集)来调整 lambda(正则项),使算法能够更好地对看不见的数据进行归纳。
X 和 Y 矩阵的优化解决方案如下所示。
Xu is a solution for the X matrix, Yi is a solution for the Y matrix. u is an u-th user in X matrix and i is an i-th movie in Y matrix.
在上述优化方案中, W u 表示用户潜在特征的权重, W i 表示电影潜在特征的权重。 q u 和 q i 将分别等于 r u 和 r i 当 u 个用户已经对 i- 个电影进行了评级时。 r u 和 r i 分别代表第 u 个用户对应的评分值和第 I 部电影对应的评分值。
我们可以用伪代码显示上述算法,如下所示:
我在这个笔记本里用 Numpy 实现了这个算法的一个幼稚版本。
movielens 数据集中用户认为哪些电影最好看?
为了确定这些电影,我使用 Keras 创建了简单的神经网络嵌入,如下面的片段所示。
这段代码主要做的是处理用于嵌入模型的收视率和电影数据帧,创建用户和电影嵌入向量,将它们彼此相乘,并将相应的偏差嵌入向量添加到获得的矩阵中。
通过这样做,我们得到了一个模型,试图通过使用电影和用户嵌入向量来预测每个用户和电影组合的评级。当用 SGD optimizer 训练该模型时,它通过调整嵌入矩阵的相应值来最小化预测和实际评级之间的均方误差。
在几个时代之后,我们将为每部电影训练电影偏见,我们需要它来确定最好的电影。完美!
我们可以得到最好的电影,如下面的片段所示。
在这里,这个片段中的“best_movies”代表了 movielens 数据集中用户认为最好的 48 部电影。我们用最好的电影向用户展示我们的应用程序,以使它不那么无聊。
我们能快速向用户展示一些推荐吗?
的确,我们可以。通过使用 movielens 小数据集。尽管这些推荐可能不如通过使用完整数据集生成的实际推荐准确。
在我项目的 Github 库中有 quick_reco_engine.py 文件。哪个具有与 engine.py 相同的“推荐者”对象
唯一的区别是,quick_reco_engine.py 不将任何东西写入数据库,它从 ALS 模型中提取“产品特征”(电影的潜在因素),并将这些保存到 Google 云存储中。
在 main.py 文件中,我们从 GCS 中获取那些产品特性,并将这些电影的潜在特性与新用户的评分相乘(未评分的电影将 0 作为评分值)。通过这样做,我们得到了用户对电影评价的一个很好的近似值。然后,我们将这些评级按降序排序,并向用户推荐排名前 12 位的高评级电影。检查 main.py 中的 这几行
让我们看看这个在线推荐系统的幕后,以及它是如何与 GCP 互动的。
在 Google Dataproc 中运行的主要推荐引擎是 engine.py
我的应用程序的数据集是 movielens 的完整数据集(显然),其中包括 280,000 名用户对 58,000 部电影的 27,000,000 个评级。
首先,我在 Google CloudSQL 中创建了以下表格。
- 等级
- 电影
- 链接
- 用户
- 推荐
- 快速推荐
然后导入 ratigs.csv 到评分表,movies.csv 到 movies 表,links.csv 到 links 表。
main.py 和 engine.py 将向应用程序写入新注册用户的用户数据、推荐和 QUICK_RECOMMENDATIONS 表。
这个降价文件详细描述了上述推荐系统如何工作,以及它如何与 GCP 交互。markdown 文件中每一个突出显示部分都表示到相应脚本中的一个或多个特定行的永久链接。请查看!
申请在线 这里 。
主要建议如下所示。
注:App 为每个注册用户创建这些推荐,他们可以随时访问推荐的电影。
There are 48 of those movies.
你也可以查一下这个 Jupyter 笔记本 ,我在这里实现了香草 ALS,做了一些 EDA 来回答那些让我无法入睡的问题,直到我回答完这些问题并确定了最佳电影。
这个项目的源代码可以在下面的 Github 库中找到。
[## badalnabizade/movie hunter-推荐-引擎
1 个用户注册 1.1。main.py 为注册用户分配一个新的 id。1.2.py 写用户的 id,名字,邮件和散列…
github.com](https://github.com/badalnabizade/MovieHunter-Recommendation-Engine)
谢谢你的时间。希望你喜欢它!
带 EC2 自动训练的无服务器 TensorFlow 工作流
机器学习培训工作通常是时间和资源密集型的,因此将这一过程纳入实时自动化工作流可能是一项挑战。
在之前的一篇文章中,我展示了一个在 AWS Lambda 上运行无服务器 TensorFlow 训练作业的原型。这种情况的激励用例是当有有限的标记数据,并且新的输入条目需要非常快速地合并到模型中时。
虽然可以在 Lambda 上运行标准的 Python TensorFlow 库,但很可能许多应用程序很快就会遇到部署包大小和/或执行时间的限制,或者需要额外的计算选项。
本文将介绍如何保持数据管理和预测无服务器,但是将培训任务卸载到一个临时 EC2 实例。这种创建实例的模式将建立在为在云中运行经济高效的超参数优化而开发的模式之上。
在 Lambda 中保留预测功能意味着由于加载 TensorFlow,仍然可能存在大小约束。为了减轻这一点,所有的 Lambda 函数都将为 Node.js 编写,这也将允许我们使用 TensorFlow.js 而不是标准的 Python 库。
TensorFlow.js 有浏览器版本和节点版本,后者包含 C++绑定以提高性能。节点版本似乎是显而易见的选择,但是它解压缩到 690MB(!)这使得它立即不适用于 Lambda。考虑到我们不会在 Lambda 函数中进行训练,预测的性能损失是可以接受的,因此我们将使用解压缩到 55MB 的浏览器版本。
对于底层机器学习模型,我们将尝试基于以下输入参数来预测一个人的舒适度:
- 温度(°F)
- 相对湿度(%)
- 衣服隔热(以“克罗”为单位)
- 风速(米/秒)
实际模型将使用使用 TensorFlow 的 Keras API 构建的简单(非优化)神经网络。
对于数据存储,我们将在 DynamoDB 中创建两个表:
data
—将为训练保留带标签的输入数据model
—存储培训作业的元数据和指标
环境设置
初始化
由于我们的项目将与 Node Lambda 文件和 Python EC2 文件混合在一起,我们将在如下所示的文件夹结构中将它们分开。我们还将利用无服务器框架,它将保留在顶层,而节点和 Python 部分将在各自的文件夹中初始化。
├── LambdaAutoTraining
│ ├── js
│ │ ├── ...
│ ├── py
│ │ ├── ...
首先,安装 Serverless 并使用节点模板初始化一个新项目。应该会出现一个样板处理程序(handler.js
)和配置文件(serverless.yml
)。
$ npm install -g serverless
$ mkdir -p LambdaAutoTraining/{js,py}
$ cd LambdaAutoTraining
$ serverless create --template aws-nodejs
节点设置
导航到js
文件夹,初始化一个新的节点项目,安装 Tensorflow.js(仅限浏览器版本!).
$ cd js
$ npm init
...follow prompts
$ npm install @tensorflow/tfjs
接下来,使用架构图作为指南,创建必要的 JavaScript 文件,这些文件将映射到最终的 Lambda 函数。
$ touch test.js upload.js train.js infer.js s3proxy.js
最后,将handler.js
中的样板代码复制到每个文件中,然后删除handler.js
。
Python 设置
导航到py
文件夹并创建一个新的虚拟环境。为了创建培训方案,我们将使用 Jupyter 笔记本,我们还需要tensorflowjs
模块,以便我们可以将保存的模型转换为 TensorFlow.js 可以理解的格式。
$ cd ../py
$ pyenv virtualenv 3.7.3 autotraining
$ pyenv activate autotraining
$ pip install tensorflow tensorflowjs jupyter
$ pip freeze > requirements.txt
在这一部分,我们只需要创建 Jupyter 笔记本文件和 Dockerfile。Python 文件将作为 Docker 构建过程的一部分创建。
$ touch train.ipynb Dockerfile
您的项目结构现在应该如下所示:
├── LambdaAutoTraining
│ ├── serverless.yml
│ ├── js
│ │ ├── test.js
│ │ ├── upload.js
│ │ ├── train.js
│ │ ├── infer.js
│ │ ├── s3proxy.js
│ │ ├── package.json
│ │ ├── package_lock.json
| │ ├── node_modules
| │ │ ├── ...
│ ├── py
│ │ ├── requirements.txt
│ │ ├── train.ipynb
│ │ ├── Dockerfile
无服务器设置
serverless.yml
文件是项目的主要配置。首先删除文件中的所有样板文本(如果需要,您可以稍后参考文档中的所有选项),然后开始构建提供者部分。
与大多数 AWS 无服务器示例的一个关键区别是,我们将定义自己的 IAM 角色。通常情况下,role
将替换为iamRoleStatements
部分,该部分允许无服务器与其自身的整体 IAM 角色合并的定制策略。然而,我们需要将 EC2 作为可信实体,这在iamRoleStatements
中是不可用的。稍后将在参考资料一节中进行构建。
环境部分让我们可以访问 Lambda 函数中的部署相关变量。创建 EC2 实例策略需要用到IAM_ROLE
,而test.js
和infer.js
都将使用API_URL
来调用我们的 API 网关端点。
接下来,使用图表和创建的文件作为指导来定义每个功能。为了简单起见,每个处理函数名和 API 端点将与文件名相同。
upload
、infer
和s3proxy
将通过 API 网关调用,因此将有http
事件。对于s3proxy
,我们将使用路径参数来定义所请求的文件,其中key
是 S3 桶中的文件夹。
对于train
函数,我们将使用 DynamoDB 流触发器,它将包含在参考资料部分。当至少有一个新事件并且满足以下任一限制时,该事件将被触发:
batchSize
—创建的最大项目数batchWindow
—创建第一个项目后的最长时间
由于train
将主要负责启动 EC2 实例,我们还将定义一些额外的特定环境变量。在本例中,我们的 Docker 图像将存储在 AWS Docker 注册表(ECR)中,但是也可以使用其他图像。
AMI_ID
—我们将在本例中使用 ami-0f812849f5bc97db5,因为它是为 Docker 预先构建的KEY_NAME
—这是 SSH 访问实例所需的 pem 文件的名称;请确保您可以访问私钥!INSTANCE_TYPE
—有效值是该图像可用的 EC2 风格SPOT_DURATION
—spot 实例被中断前的最短时间(分钟)VALID_HRS
—如果没有实现,现货请求将持续的最长时间ECR_ID
—应该与您的 AWS 帐户 id 相同ECR_REPO
—ECR 储存库和项目的名称
最后,test
将仅用于手动触发,因此没有关联事件。
接下来,创建 S3 存储桶和两个 DynamoDB 表(在此阶段提供的吞吐量有限)。请注意,data
表还包含用于触发train
功能的StreamSpecification
。
要创建的最后一个资源是我们的自定义 IAM 角色,它将被所有功能使用,无服务器的文档提供了一个很好的起点模板。为了将角色从 Lambda 转移到 EC2,我们需要两件事情:
- 将
ec2.amazonaws.com
添加到AssumeRolePolicyDocument
部分 - 在
Policies
部分为iam:PassRole
添加一个允许动作
对于Policies
部分,我们将首先复制默认的无服务器日志策略和 S3 部署桶(通常这些是自动创建的)。接下来,我们将为之前定义的 S3 桶和 DynamoDB 表添加自定义语句。请注意,在创建自定义策略时,不会自动创建 DynamoDB 流策略,因此我们需要显式地定义它。
此外,我们将添加创建 EC2 实例所需的策略:
- EC2 —创建并运行实例。
- CloudWatch —创建、描述和启用警报,以便我们可以在训练完成时自动终止实例。
- ECR —允许提取 Docker 图像(这将仅由 EC2 使用,而不由 Lambda 函数使用)。
- IAM 获取、创建角色并将其添加到实例配置文件中。当从控制台启动 EC2 实例并选择 IAM 角色时,会自动创建这个概要文件,但是我们需要在我们的函数中手动创建。
安全注意事项:在部署到生产环境之前,这些策略应该仅限于所需的资源
因为您已经向每个函数添加了样板代码,所以现在可以部署和测试所有的配置是否正确。
$ serverless deploy --stage dev
...
$ curl -X POST "https://<api_id>.execute-api.<region>.amazonaws.com/dev/upload"Go Serverless v1.0! Your function executed successfully!
现在我们已经准备好构建应用程序了!
Photo by Susan Holt Simpson on Unsplash
λ:upload . js
upload
函数将接受一组新标记的数据作为输入,并将其存储在 DynamoDB 表中。该更新将启动一个流触发器来启动train
功能。
在upload.js
中,首先导入并设置 AWS SDK。由于这个函数是由一个 HTTP 事件触发的,我们将读取body
字段,然后构造一个表示各个 DynamoDB 插入项的对象数组。注意,即使字段有不同的类型(例如“N”或“S”分别代表数字和字符串),实际值也需要作为字符串传入。
如果有新的项目要写,我们将构造一个新的对象,然后使用来自 DynamoDB AWS SDK 的batchWriteItem
来写新的项目。batchWriteItem
比一系列putItem
请求更高效,并且也是原子性的。
现在我们已经构建了upload
函数,您还可以构建test.js
来生成随机数据,以测试工作流并填充数据库。详见 Github 文件。
重新部署到dev
阶段并测试端点。此时,开始用数据填充 DynamoDB 是有价值的,这可以通过手动调用test.js
函数来完成。
$ severless deploy --stage dev
...
$ curl -X POST "https://<api_id>.execute-api.<region>.amazonaws.com/dev/upload" -d '[{"created": 1570323309012, "temp": 75, "rh": 60, "wind": 0.6, "clo": 1.0, "label": "ok", "score": -1}]'1 records created successfully
尽管train.js
函数还没有构建出来,但是一旦达到批处理窗口或批处理大小,您仍然会看到它被调用。
EC2: train.py
上传新数据的功能完成后,我们现在将重点转移到 Python 训练部分。这里转移焦点而不是完成 JavaScript Lambda 函数的动机是,如果 EC2/ECR 集成完成了,验证train
函数会容易得多,否则我们将无法验证启动脚本是否工作。
要开始使用 TensorFlow 模型,请打开 Jupyter 笔记本(您的虚拟环境应该已经激活)。
$ cd py
$ jupyter notebook
打开之前创建为空文件的train.ipynb
。我们希望将关键字段作为环境参数传递给 Docker 容器,但是为了便于测试,我们将提供这些值。接下来创建代表两个 DynamoDB 表的变量。
对于输入数据,我们将对 DynamoDB 数据表执行一次扫描。如果结果被分页,那么LastEvaluatedKey
就会出现,这种情况发生在响应大于 1MB 的时候。
DynamoDB 返回一个 Decimal 数据类型,所以我们将遍历数据集,转换为 floats,并对标签数据进行一次性编码。最后,这个列表被转换成 numpy 数组,以便输入到 TensorFlow 模型中。
为了创建模型,我们将使用 TensorFlow 的 Keras API ,更具体地说,是允许我们构建神经网络层的顺序模型。本文的重点不是超参数优化,因此我们将使用一个非常简单的配置。请务必注意,必须定义输入形状,以便稍后导入 TensorFlow.js。
一旦模型完成,我们将使用来自tfjs
模块的转换器直接将其保存为可由 TensorFlow.js 导入的格式。然后,这些文件被上传到 S3 的一个新文件夹中,并以当前纪元为关键字。我们还将维护一个“最新”文件夹,以定义客户应该使用哪个模型进行预测。最后,每个模型拟合的结果将存储在 DynamoDB 的model
表中。
由于应该填充了data
表,现在您可以在本地运行这个笔记本并验证其功能。
随着模型开发的完成,我们现在将开始准备 Docker 映像,从提供构建映像的指令的 Docker 文件开始。
打开 docker 文件进行编辑,并按如下所示进行更新,目的如下:
- 从标准 Python 3.7 基础映像开始
- 创建新用户
lambdaautotraining
- 复制到 Jupyter 笔记本和需求文件中
- 从需求文件安装 Python 库
- 将 Jupyter 笔记本转换为标准 Python 文件,并在图像启动时运行
FROM python:3.7RUN echo $(python3 --version)RUN useradd -ms /bin/bash lambdaautotrainingWORKDIR /home/lambdaautotrainingRUN apt-get update -yCOPY train.ipynb requirements.txt ./RUN pip install -r requirements.txtRUN chown -R lambdaautotraining:lambdaautotraining ./USER lambdaautotrainingRUN jupyter nbconvert --to script train.ipynbCMD ["python3","-u","train.py"]
接下来,在本地构建 Docker 容器,用 ECR URI 标记,登录 ECR,然后推送到存储库。
$ docker build -t lambda-auto-training-dev .
...
$ docker tag \
lambda-auto-training-dev:latest \
<ecr_id>.dkr.ecr.<region>.amazonaws.com/lambda-auto-training/lambda-auto-training-dev:latest$ $(aws ecr get-login --no-include-email --region <region>)
...
$ docker push \
<ecr_id>.dkr.ecr.<region>.amazonaws.com/lambda-auto-training/lambda-auto-training-dev:latest
您可以手动启动 EC2 实例并执行命令来运行这个映像,但是我们将创建触发 Lambda 函数并一起测试它。
Lambda: train.js
train
Lambda 函数的主要目的是对新的一批标记数据做出反应,然后启动一个新的 EC2 实例来完全执行训练工作流。与使用回调样式处理程序的upload
函数不同,这里我们将使用 async/await 模式。
该函数中定义的第一个变量是初始化脚本,它将被传递给 EC2 实例进行启动。这值得作为一个单独的 shell 脚本进行测试,但是为了简单起见,这里仅以字符串形式显示。该脚本的主要职责如下:
- 下载并安装 AWS CLI
- 登录到 ECR
- 下拉所需的 Docker 图像
- 运行 Docker 映像
注意,run
命令有一系列我们通过 replace 语句定义的环境属性。这些将在我们的训练 Python 脚本中使用,以与 DynamoDB 和 S3 进行交互。
最后,该字符串需要按照 EC2 要求进行 base64 编码。
接下来,检索实例配置文件,它定义了 EC2 实例将使用的 IAM 角色。需要阻塞的每个调用都使用带有 await 关键字的 promise 形式。
有了实例概要文件,我们将为 spot 实例定义完整的 EC2 参数集。另一种选择是单独创建一个模板并直接启动它。我们还将在关闭时终止实例,这里的一个额外优化是根据需要停止/启动一个持久性实例。
我们现在准备开始创建 EC2。成功后,我们将创建并启用一个警报,当 CPU 下降到某个阈值以下时,该警报将自动终止实例,我们将该阈值用作训练完成的代理。
我们现在可以用新功能更新开发环境。
$ cd ../js
$ serverless deploy --stage dev
已经验证了train.js
的触发工作,我们将使用控制台测试培训工作流程。在 AWS 中,打开 Lambda、DynamoDB、S3 和 EC2 的服务页面,并执行以下操作:
- λ:用空输入触发训练函数
- EC2:验证实例是使用正确的警报创建的
- DynamoDB:验证模型信息是否更新
- S3:确认模型文件已经上传
- EC2:大约 10 分钟后,验证实例是否已终止
Lambda: infer.js
完整的培训工作流完成后,我们现在准备构建预测/推理部分。infer
的主要目的是下载模型,加载到 TensorFlow.js 中,然后根据 HTTP 触发器提供给它的一组输入进行预测。该函数期望输入是一个对象数组,其中的键表示所需的模型输入字段。
TensorFlow.js 的浏览器版本使用的fetch
在 Node.js 中不是标准的。为了解决这个问题,我们将安装node-fetch
,并在全局范围内使用它来代替fetch
。
$ npm install node-fetch
接下来,必须下载模型。我们将再次需要解决这样一个事实,即我们使用的是浏览器版本,它不期望访问标准的本地文件系统。我们可以将必要的模块从tfjs-node
提取到我们的项目中,但是在这个例子中,我们将利用loadLayersModel
中的直接 HTTP 下载选项。
然而,由于我们的 S3 存储桶没有对世界开放,我们需要确定如何允许这种访问。对于对 S3 的 HTTP 访问,使用签名的 url 是一个合理的选择,但是在下载步骤中,TensorFlow 实际上做了两件事:
- 下载 model . JSON——我们可以在这里传入签名的 url
- 使用 url 根目录下载模型拓扑—步骤 1 中签名的 url 将不再有效!
为了解决这个问题,我们将使用一个单独的代理来接收每个请求,并将其重定向到一个适当的签名 url。更新s3proxy
以支持此功能,如下所示:
回到infer
函数,加载模型后,我们将输入转换为 2D 张量并运行预测。arraySync
会将结果转换为标准浮点数,每组输入被转换为跨输出维度的一组预测。通过寻找最大值,这个预测被转换成一个简单的标签映射,然后在一个新的 JSON 对象中返回。
测试整个工作流程
如果您创建了test
函数,那么您可以设置一个 cron 作业,在一个定义的时间间隔执行,这将模拟真实的流量。为此,我们需要在我们的serverless.yml
配置中添加一个 CloudWatch 事件触发器(默认禁用):
test:
handler: js/test.test
events:
- schedule:
rate: rate(2 minutes)
enabled: false
手动启用触发器可能有点令人困惑,因为 Lambda UI 会显示为“enabled ”,但实际上您需要转到 CloudWatch 来启用底层事件:
问题是在 Lambda UI 中既有
AWS::Events::Rule
(可以启用/禁用)也有触发器(可以启用/禁用)。Lambda UI 显示触发器状态,该状态已启用。然而,我们实际上不能用云的形成来触摸它。AWS::Events::Rule
设置为 disabled,这是我们用 CloudFormation 设置的。如果触发器或规则被禁用,它将不会触发您的 Lambda。
对于预测方面,我们可以像以前一样手动测试,或者扩展我们的测试功能策略,以包括推理。
准备就绪后,您现在可以部署到“生产”阶段。对于 Docker 映像,我们只需向现有映像添加一个新标签,并将其推入生产存储库。
$ serverless deploy --stage prod
...
$ docker tag \
lambda-auto-training-dev:latest \
<ecr_id>.dkr.ecr.<region>.amazonaws.com/lambda-auto-training/lambda-auto-training-prod:latest
...
$ docker push \
<ecr_id>.dkr.ecr.<region>.amazonaws.com/lambda-auto-training/lambda-auto-training-prod:latest
最后的想法
鉴于这是一个原型,在部署到真实的生产环境之前,应该考虑许多方面:
- 持久 API 端点的域集成(见
serverless-domain-manager
插件)。 - HTTP 事件输入应该经过验证并包含错误处理。
- 可以向面向客户端的端点添加预热功能,以限制冷启动时的长调用时间。
- 应该加强 IAM 资源权限。将这个环境封装在 VPC 中是一个不错的选择,它还提供了允许 HTTP 访问 S3 的代理的替代方案。
- DynamoDB 流触发器相对简单,在高容量环境中可能会过于激进。一个更健壮的解决方案可能是将新事件附加到一个文件中,并单独对新事件进行计数,这也可以减少每次训练运行时扫描整个表的工作量。
- 如果 EC2 实例在每次运行后被终止,最终未使用的警报将需要被清除。如果使用停止/启动一个实例的替代模式,警报也可以重复使用。
- 对于生产中的保护,应在培训工作中应用阈值,以便不引入表现不佳的模型进行预测。
感谢阅读!
你可以在 GitHub 上查看所有代码:https://github.com/mikepm35/LambdaAutoTraining
羽毛球比赛中的发球、得分领先和连续得分
羽毛球分为 5 类:男子单打、女子单打、男子双打、女子双打和混合双打。众所周知,羽毛球运动员的战术、战略和强度在这些类别中有很大的不同。一般来说,双打比赛节奏更快,更注重进攻而不是防守,而单打比赛更注重多功能性,更具活力。考虑到游戏风格的差异,本文着眼于 在 的每个类别中,发球、连续得分和得分领先如何不同地影响玩家的心态/表现。这些发现将从统计学以及游戏性的角度进行解释。
总结观察结果,产生了两个问题来引导本文的范围:
- 发球给玩家的优势是多还是少?单打和双打有什么不同?
- 球员的表现会受到连续得分或大比分领先的影响吗?
本文考察的数据是 3 月 6 日至 3 月 11 日在伯明翰举行的 2019 年全英公开赛的结果。许多羽毛球爱好者飞到英国去看顶尖选手,如、史、等男单排名前三的选手。
bwf 网站上的原始数据记录了比赛在单个分数方面的进步(第一分后的分数,第二分,一直到比赛结束)。有了这些信息,我们就能够将每场比赛的分数处理成单点的实例。每个实例将包括以下信息:
- 玩家 _ 领先:玩家在一个点开始时的领先。可以是正的也可以是负的。(负表示分数落后于对手)
- Player_score:玩家在一个点开始时的得分。
- 玩家 _ 发球:如果玩家赢了最后一分,那么玩家发球。
- Point_won:如果当前玩的点是玩家赢的。
- 玩家在一个点开始时所拥有的连续点数。
Diagram 0: A snippet of processed data
这些处理的实例包括我们将检查的关键因素,即玩家领先、玩家连续得分和玩家服务。
描述性数据
为了让领先、连续得分、发球的影响更容易被形象化,我创造了三组情节。
Diagram 1: service descriptive data
第一组图显示了发球和得分结果之间的关系,得分可以是得分,也可以是失分。纵轴是事件发生的频率,横轴是事件发生的分数。绿色/红色线条显示球员发球得分/失分的频率。
在除“ms”之外的所有类别中,发球和失球的发生率似乎比发球和赢球的发生率高(也就是说红线似乎比绿线高)。从发球的描述数据来看,发球似乎有劣势。
Diagram 2: lead descriptive data
第二组图显示了积分领先和积分结果之间的关系。纵轴是事件发生的频率,横轴是事件发生的分数。绿/红线显示了玩家赢得/失去具有不同点数领先的点数的频率。
在类别“ms”、“ws”和“xd”中,有一小部分获胜的频率高于失败。但总的来说,图表的视觉效果**似乎无法确定因积分领先而赢得和失去积分之间的任何差异。**需要更多的分析才能有进一步的发言权。
Diagram 3: consecutive points descriptive data
第三组图显示了连续点(条纹)和一个点的结果之间的关系。绿/红线显示了玩家赢得/失去具有不同连续点数的点数的频率。
在类别“md”、“wd”、“xd”和“总体”中,连续点数达到 5 似乎会导致玩家输多于赢。从观察连续点的描述性数据来看,拥有连续点似乎有一个弱劣势。
直观地绘制这些因素当然有助于我们发现任何相关性。综上所述,发球似乎是一个劣势,连续得分似乎是一个弱劣势,领先似乎没有提供任何优势/劣势。要做出合理的结论,需要对数据进行进一步的分析。
逻辑回归
Point_won 在处理的数据集中表示为二进制列。1 指点赢,0 指点输。这使得逻辑回归成为一个很好的模型,可以用来可视化发球、领先、连续得分与赢得一分之间的相关性。
不同类别的逻辑回归绘制在同一图上,以便更容易看出差异。
Plot 4: Logistic regressions and coefficients on player serve for multiple categories
根据上面的图表和系数,发球对得分结果的影响在所有游戏类别中都是负面的。从最负到最小负排列系数,我们得到 md,xd,wd,ws,ms 。双打和单打比赛有非常明显的模式和区别。这个服务用最快的反弹伤害了游戏。 md 以快速驱动和强力扣杀闻名,因此在 md 比赛中发球绝对是个坏主意。 xd 排在 wd 之前是因为 xd 里有个男玩家,使得进攻还是很厉害的。ws 和 ms 受到的影响较小,因为每边只有一名球员击球,所以击球组合通常较慢。因此发球不会有太大的负面影响。
为了看看相关性是否真的很重要,我进行了一个排列测试来推断每个类别的 p 值。测试结果如下所示。
由 ms 反映的相关性不显著,而所有其他类别的相关性显著,p 值非常小。尽管男子单打与发球的相关性并不显著,但其余的相关性仍然强化了关于拉力赛速度与发球的相同概念。更新后的回归图如下所示,仅包括显著相关性(移除 ms )。
Plot 5: Significant Logistic regressions on player serve for multiple categories
同样的方法也适用于连续得分对赢得一分机会的影响。
Plot 6: Logistic regressions on consecutive points’ impact on scoring for multiple categories
我们从系数中看到,只有对于 ms ,连续点数与中奖概率之间存在正相关关系。所有其他类别都具有负相关性,按照从最敏感到最不敏感的顺序排列为“md、xd、wd、ws”。从游戏性的角度来解释这一点,在更高的连续点数上更高的失败几率意味着游戏更具竞争力,更少“滑坡”。同样,双打比赛也受到连续得分的负面影响。发生这种情况的原因很可能是由于球员的水平差异和比赛的接近程度之间的事实,在单打比赛中,即使一名球员比另一名球员好一点点,也可能反映出很大的分数差异,但在双打比赛中,即使两个队的水平非常不同,他们仍然可能有一场接近的比赛。因此,在双打比赛中,与单打比赛相比,这场比赛似乎总是“势均力敌”。
再次进行排列测试,结果如下:
有趣的是,排列测试的结果表明,对于 ms 和 ws,相关性是不显著的。只有在双打比赛中才能得出有意义的结论。这验证了单打比赛和双打比赛之间的实际差异,进一步巩固了双打比赛更“激烈”、“快速”和“接近”的结论。
Plot 7: Significant correlations from player consecutive points’ impact on chances of winning
要考虑的最后一个因素是球员的领先优势及其对赢得一分的机会的影响
从描述性数据来看,这并没有产生任何明确的相关性,希望逻辑回归能给我们更多的具体信息。
Plot 8: Logistic regressions and coefficients on player lead for multiple categories
从系数和情节来看,领先有利于赢得除男子双打以外的所有类别的比赛。Md 脱颖而出,可能再次是同样的“竞争”和“密切”的游戏性质。这可以解释为 md 的比赛如此接近,以至于很难取得大的领先优势,因此当领先优势变大时,就更容易输掉比赛。Wd、ms、ws、xd 的顺序是通过点导致租赁最受益。没有非常明确的迹象和见解来解释这一点,让我们在推测可能的解释之前,先看看这些相关性的显著性检验结果。
在对关于玩家领先的逻辑回归进行排列测试后,我们发现除了将所有类别作为一个整体来看,它们都是不重要的,这不会给类别之间的差异增加太多洞察力。这很重要,因为它否定了我们从玩家领先的影响中推断出的潜在关系。这个测试的结论是玩家的领先和获胜的几率没有关系。
结论
本文利用逻辑回归和线性回归来评估发球、球员领先和球员连续得分的影响,总体上的重要发现是发球降低了赢得一分的机会,连续得分可能是风向转变的迹象。积分领先和赢得积分之间没有显著的相关性。
本文仅绘制了 300 多场羽毛球比赛的非常小的数据集,包含更多比赛统计数据的更大的数据集可以帮助巩固当前的相关性,并进一步探索积分领先的潜在影响。
感谢您的阅读!
如果你有任何意见或者你只是喜欢羽毛球,请联系我们!
用烧瓶服务先知模型——预测未来
演示如何使用 Flask 在 Web 上提供 Prophet 模型 API 的解决方案。prophet——由脸书开发的用于预测时间序列数据的开源 Python 库。
Source: Pixabay
几乎对任何企业来说,准确的预测和未来的预测都是至关重要的。这是显而易见的事情,不需要解释。有一个时间序列数据的概念,这个数据是按日期排序的,通常每个日期都被赋予一个或多个特定于该日期的值。机器学习驱动的模型可以基于时间序列数据生成预测。这种预测可能是商业决策的重要信息来源。
我在研究时间序列预测的 LSTM 模型。LSTM 神经网络擅长预测序列数据,时间序列也是序列数据,这就是为什么 LSTM 网络被用来预测时间序列的原因。LSTM 的实施很复杂,很难为 LSTM 的培训准备输入数据。可能所有 LSTM 的例子都是基于当前数据集的数据进行预测,而不是真实的未来数据-这使得在实践中很难应用 LSTM。
当我在 GitHub 上找到并评估了 Prophet Python 库用于未来预测时,我对它的简单性和实用性印象深刻。prophet——由脸书开发的开源预测库。
我将与 Prophet forecast 实现共享一个示例 Python 笔记本,并展示如何通过 Flask API 保存/加载 Prophet 模型并提供服务。
源代码和数据可在我的 GitHub repo 中获得。
首先,您需要导入 Prophet 库:
import pandas as pd
import matplotlib.pyplot as plt
from fbprophet import Prophet
%matplotlib inline
如果您的环境中没有安装 Prophet,您需要安装它。我把它安装在 Docker 容器中运行的 Ubuntu 中。用 pip 安装没有效果。但是我可以用康达安装:
conda install gcc
conda install -c conda-forge fbprophet
通常下一步是加载数据。对于这个例子,我使用铁/钢价格数据集(从这里下载)。数据被加载到带有熊猫库的框架中。熊猫数据框架允许操纵和争论数据。我们正在删除未使用的列,设置索引并重新排序时间序列数据:
df = pd.read_csv('Dow Jones Iron & Steel Historical Data.csv')
df = df[['Date', 'Price']].dropna()df['Date'] = pd.to_datetime(df['Date'])
df = df.set_index('Date')daily_df = df.resample('D').mean()
d_df = daily_df.reset_index().dropna()
Prophet 操作 ds/y 列,我们应该重命名数据框中的列:
d_df.columns = ['ds', 'y']fig = plt.figure(facecolor='w', figsize=(20, 6))
plt.plot(d_df.ds, d_df.y)
我们将用于 Prophet 模型培训的钢铁价格数据:
Iron/steel price data
关键部分来了——先知模型训练:
m = Prophet()
m.fit(d_df)future = m.make_future_dataframe(periods=90)
forecast = m.predict(future)
forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail()
通过调用 fit 函数并传递 Pandas 数据帧来执行模型训练。未来预测由 predict 函数执行,并传递描述未来多少天要预测的参数(上例中为 90 天)。Prophet 返回一个包含各种参数的数据框来描述预测。其中最重要的是:
- ds —预测日期
- yhat —给定日期的预测值
- yhat _ lower 给定日期的预测下限
- yhat _ uppet 给定日期的预测上限
调用 Prophet 模型的 plot 函数,显示模型是如何根据训练数据(黑点—训练数据,蓝线—预测值,浅蓝色区域—预测边界)进行训练的:
Prophet model
此图表显示未来 90 天的模型预测。但是很难看出来,图表显示了所有的数据。我们可以放大数据,并使用从训练数据中分离预测的垂直线来绘制图表:
from datetime import datetime, timedeltafig1 = m.plot(forecast)#datenow = datetime.now()
datenow = datetime(2019, 7, 2)
dateend = datenow + timedelta(days=90)
datestart = dateend - timedelta(days=450)plt.xlim([datestart, dateend])
plt.title("Iron/steel forecast", fontsize=20)
plt.xlabel("Day", fontsize=20)
plt.ylabel("Iron/steel price", fontsize=20)
plt.axvline(datenow, color="k", linestyle=":")
plt.show()
这有助于更清楚地看到 90 天的预测。我们可以看到价格预测的下降趋势:
Forecast trend
打印预测值有一种简单的方法,即从预测数据框中访问最近 90 天的预测值:
forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']][-90:]
First 6 entries from 90 days forecast
为了评估模型,显示学习到的趋势是有用的:
fig2 = m.plot_components(forecast)
Trends
Prophet 了解到从三月到十月价格通常会下降。
当预测一定天数时,您应该检查预期误差是多少。这被称为模型交叉验证:
from fbprophet.diagnostics import cross_validation, performance_metrics
df_cv = cross_validation(m, horizon='90 days')
df_p = performance_metrics(df_cv)
df_p.head(5)
结果:
Cross-validation results
我们可以绘制这些指标来直观地观察模型的表现。百分比误差(MAPE)更容易理解,我们可以这样绘制:
from fbprophet.plot import plot_cross_validation_metric
fig3 = plot_cross_validation_metric(df_cv, metric='mape')
它表明 10 天的预测导致大约 10%的误差,然后误差增长到大约 18%:
Forecast percentage error
当新数据可用时,应该重新训练该模型。如果数据没有改变,就没有必要重新训练模型。当用户想要调用预测功能时,保存模型并再次使用。将 pickle 功能用于:
import pickle
with open('forecast_model.pckl', 'wb') as fout:
pickle.dump(m, fout)with open('forecast_model.pckl', 'rb') as fin:
m2 = pickle.load(fin)
这将把模型保存到磁盘上的一个物理文件中。在本例中,我展示了如何保存和加载模型,为您提供方便。在实际实施中,load 函数将在不同的位置调用(在负责处理用户预测请求的函数中)。
Flask 完美地通过 REST API 将 Prophet 模型暴露给外部世界。导入烧瓶库:
from flask import Flask, jsonify, request
from flask_cors import CORS, cross_origin
在带有 Flask 注释的函数内调用预测:
app = Flask(__name__)
CORS(app)[@app](http://twitter.com/app).route("/katana-ml/api/v1.0/forecast/ironsteel", methods=['POST'])
def predict():
horizon = int(request.json['horizon'])
future2 = m2.make_future_dataframe(periods=horizon)
forecast2 = m2.predict(future2)
data = forecast2[['ds', 'yhat', 'yhat_lower', 'yhat_upper']][-horizon:]
ret = data.to_json(orient='records', date_format='iso')
return ret# running REST interface, port=3000 for direct test
if __name__ == "__main__":
app.run(debug=False, host='0.0.0.0', port=3000)
通过 Postman 调用 REST API 的例子。预测范围参数被传递给 API,我们得到带有预测数据的 JSON 响应:
Prophet forecast REST API response example
源代码和示例数据可以在我的 GitHub repo 中获得。
在 Kubernetes 上提供 Vowpal Wabbit 模型
Vowpal Wabbit Logo
在这篇文章中,我将展示一个构建 Kubernetes 服务的例子,将使用没有“本地”服务 API 的库训练的机器学习模型投入生产。我选择了 Vowpal Wabbit,因为虽然它不像其他机器学习框架那样受欢迎,但它通常会提供非常好的开箱即用的结果,并且在数据准备、优化器选择和超参数调整方面需要最少的努力。我个人认为,学习 vow pal Wabbit T1(VW)是一个(初级)数据科学家的良好起点,因为初学者可以轻松、快速、迭代地在相当大的数据集上构建原型。另一方面,从机器学习工程师的角度来看,服务于用 VW 训练的模型是一项有趣的任务,因为该库提供的接口很少,并且没有官方的服务 API。
在这篇文章中,我将带你了解这项服务的发展。有几个 GitHub gists,你可以在这里找到完整的库。
推出 VowpalWabbit
VW 是微软研究院赞助的快速核外机器学习系统。第一个公开版本出现在 2007 年,多年来已经增加了许多功能和改进。大众的设计是学术研究和软件开发之间良性循环的一个例子,论文中出现的算法被纳入库中,框架的速度和性能可以催生进一步的学术研究。
为了更具体地说明 VW 如何工作,让我们考虑一个预测任务,其中我们想要建立一个线性模型 y = X𝛃,其中 y 是要预测的变量, X 是特征向量,𝛃是模型参数的向量。
第一个关键想法是大众如何使用所谓的散列技巧来表示特征。为了简单起见,首先考虑分类特征 𝓕 的情况,其可以取许多可能的值,例如在 (LAX,AMS) 中的对(出发机场,到达机场)。大众不需要用户预先指定所有可能的级别,或者让它知道𝓕 可以假设的不同值的数量;用户需要做的是指定特征的总最大数量𝓓,即𝛃.的行数在幕后,大众将使用一个散列函数将所有特征存储到𝓓值中。具体来说,在 𝓕 = (LAX,AMS) 的情况下,VW 会将字符串“ 𝓕 = *(LHR,AMS)”*哈希成一个介于 0 和𝓓 -1 之间的整数 i ,并设置𝛃( i ) = 1 ( 一热编码)。在数字特征的情况下,比如说 𝓕 = 2.5,整数 i 将通过散列字符串“ 𝓕 ”来计算,然后大众将设置𝛃( i ) = 2.5。
第二个关键思想是大众使用在线梯度下降(一种在线学习的形式)来优化模型权重𝛃;具体来说,不需要将数据保存在存储器中,可以一次处理一个例子;这使得 VW 能够在非常大的数据集上很好地扩展,因为内存需求现在基本上被绑定到为𝛃.分配内存例如,您可以通过在网络上传输数据,使用 Hadoop 集群中存储的万亿字节数据在您的笔记本电脑上训练一个大众模型。此外,小心实施梯度下降以处理不同重要性的特征,并降低算法对学习率(学习率衰减)精确设置的敏感性。
在进入下一步之前,让我们先看一下大众要求的数据格式。我首先以我们将用于服务的 JSON 格式呈现它:
Training data: JSON format
这里我们处理一个分类任务,因此目标可以是 0 或1;标签为示例的可选唯一标识;特性被组织在名称空间中,其中每个名称空间都有自己的哈希函数,减少了哈希冲突的可能性;例如,在名称空间 a 中,我们发现一个分类特征 x1 取值 a 和一个数值特征 x2 取值 0.814 。在 VW 中接收这个示例的方法是将所有内容放在同一个字符串中,使用“|”字符分隔名称空间:
Training data: VW native format
注意分类特征如何使用“*=”字符,数字特征如何使用“:*字符;进一步的细节可以在这里找到。
分解问题和设计考虑
让我们首先集思广益,为大众服务。我很快想到了两个解决方案:
- 哈希反转 : VW 提供了以人类可读格式存储模型权重𝛃的可能性;通过额外的工作,VW 还可以对哈希进行反转,从而生成训练示例中出现的原始特征的𝛃索引,例如上面 gists 中的 x2 、 x1 、…。通过这种方式,人们可以获得模型权重的文本文件,并使用任何可以对线性模型进行评分的函数(例如,参见此处采用的为大众模型构建 TensorFlow 包装器的方法)。
- Java 原生接口 ( JNI ): 这个项目旨在提供一个 API 从 Java 调用 VW;因此,这可以用来将 VW 集成到任何基于 Java 的服务库中。
稍加思考,我们就可以很容易地发现这两种方法都有重大缺陷,并且本质上都不令人满意:
- 哈希反转是一种开销很大的操作;它需要进一步遍历所有的训练数据集,并且不能很好地扩展大量的特征。此外,它占用了数据科学家的操作工作时间,而这些时间本可以更好地用于创建和改进算法。此外,在提供权重时总是可能存在翻译错误,这可能导致生产中的模型不同于经过训练的模型。
- 在撰写本文时,大众 Java 项目似乎并不活跃,而且落后于大众的最新版本。此外,使用 JNI 可能会带来一些风险,即引入一个可能导致产品中的模型崩溃的错误。
因此,我们的目标是 1)以尽可能接近原生大众环境的方式提供模型,2)易于扩展。
码头工人将允许满足 1);Docker 是一个在操作系统级别执行虚拟化的软件。这意味着我们可以在操作系统上构建应用程序,同时隔离它所需的特定库和依赖项,也就是说,我们不需要在操作系统级别安装它们。该应用程序将在它自己的容器中运行,这将给人一种独立虚拟机的错觉。具体来说,我们将围绕 VW shell 命令构建一个 Python 包装器,它将模型加载到容器的内存中,并在预测请求到来时对其进行评分。请注意,这种方法可以很容易地推广到其他机器学习库加以必要的修改。
Kubernetes 将允许通过扩展集群上的容器来满足 2)并将模型公开为服务。Kubernetes 是容器编排系统的一个例子,它在过去几年中获得了相当大的流行。顺便提一下,我一直认为这个名字意味着类似“立方体”的东西,因为容器通常被描绘成在机器内部运行的小立方体;然而,事实证明它来源于希腊语,意思是船长。
以下是后续步骤的分类:
- 创建 Docker 映像的原型,以在“本地”环境中隔离大众
- 构建一个操场测试模型,以更熟悉大众,并对最终结果进行健全性检查
- 创建一个 Python 包装器来为模型服务
- 开发一个 Flask 应用程序来处理评分请求
- 在 Kubernetes 上将模型部署为服务
这里值得注意的遗漏是:
- 监控培训和生产环境之间的功能对等性。
- 监控需要在生产中加速运行的请求和实例的数量。
理想情况下,最后两个问题的解决方案应该是独立的服务,这些服务可以使用公共 API 与每个单独的模型服务进行对话。
在 Docker 上开发
第一个概念验证是构建一个能够运行大众的 docker 映像。如果我们从一个 Ubuntu 镜像开始,这很容易,因为 VW 已经作为一个包被维护了:
Prototype Docker Image
使用 apt-get 我们安装 Python 3.6 和 VW;然后我们在 requirements.txt 中安装附加包;然后,当我们将在 Kubernetes 上部署时,我们创建了一个用户 vwserver ,由于安全限制,我们可能无法在 Kubernetes 集群上以 root 身份运行容器。最后,我们创建一个挂载点目录 vw_models 来 1)存储生产中的模型,2)允许我们在开发时在笔记本电脑和容器之间交换脚本和代码。
您可以从标签为andrejschioppa/VW _ serving _ flask:tryubuntu的 DockerHub 中提取此图像;如果你更喜欢 CentOS 图像,我也提供andrejschioppa/VW _ serving _ flask:trycentos。我想指出的是,对于 CentOS,我必须从其原始库编译 VW,由于过时的 GCC / g++编译器, Dockerfile 变得更加棘手。
创建操场测试模型
下一步,我们将更加熟悉 VW,并创建一个模型,用于测试我们的服务应用程序的正确性。让我们初始化一个本地卷目录并启动容器;然后我们将 numpy 安装到容器中:
Running the prototype container
我们现在创建一个测试模型;我们有这个文件test _ model . py坐在 localVolume :
Training data generator
这个模型是一个简单的线性模型,有一个数字特征 x2 和三个分类特征 x1,x2,x3;模型是完全确定的:如果分数是 > 0 我们预测正标签 1 ,否则负标签 0 。
我们现在生成训练示例,并将模型训练到完美拟合(毕竟这里的一切都是确定的,因此我们应该期待一个完美的分类器;返回的概率可能不是精确校准的):
Training a VW model
我们看到,正面例子的概率为 73.11% ,负面例子的概率为 50%。在这些 10 的例子中,模型看起来像一个完美的分类器,尽管概率可能没有被校准。我们将保留文件 test_data.json、test_data.vw 和 test_model.model 以备将来测试之用。**
用于提供预测的 Python 模块
我写了一个类 VWModel 来服务 Python 中的 VW 模型。让我们来看看构造函数:
constructor of VWModel
关键思想是启动一个 shell 进程,运行 VW 并通过 Python 控制标准流 : 标准输入,标准输出和标准错误*。第一步是获得正确的 vw 命令(第 26 行);注意,我们首先停止训练更新(选项仅测试),即我们不再更新权重𝛃;其次,我们通过选项 initial_regressor 设置用于评分的模型,并将预测重定向到 stdout 。选项链接(第 32 行)允许在广义线性模型(如使用逻辑链接的二元分类器)的情况下指定一个链接函数,如果在训练期间使用了特征交互,选项 sort_features 强制 VW 对特征进行分类。*
现在让我们来看看模型实际上是如何启动的:
How VWModel starts a model
这里,我们只是用上面构建的命令启动一个 shell 子进程,并通过子进程控制标准流。管道*。关于处理标准流和管道的精彩阅读请看这个精彩的系列。*
最后让我们看看实际的评分函数:
How VWModel handles a prediction request
我们使用一个静态方法, parse_example ,将输入 json 转换成 VW 输入格式的字符串。然后,我们将字符串刷新到正在运行的进程的 stdin 中,并收集 stdout 来提取预测。
完整的 Python 模块请看这里的;请注意,在测试目录中,您可以找到一个基于示例和我们上面构建的模型的单元测试;注意这个单元测试意味着在将 vw_model 模块添加到 PYTHONPATH 之后在容器内部运行。
烧瓶应用程序
为了处理模型服务的预测请求,我们将开发一个简单的 Flask 应用程序。Flask 是一个非常流行的用于开发 web 应用程序的 Python 微框架。服务应用程序变得非常简单:
The base Flask application
一个服务请求将通过对 /serve/ 的 POST 请求使用REST API;具体地说,这相当于将 JSON 格式的特性发送到 /serve/ URL。上面的代码所做的只是将预测请求转发给一个名为 FlaskServer 的对象,我们可以在这里检查它:
The interface between Flask application and VWModel
这个 FlaskServer 类只是提供了一个到 VWModel 的接口;有两个关键的环境变量与容器相关联:
- 模型文件,它指向我们想要服务的模型文件
- 模型 _CONF ,它指向一个配置文件,该文件目前支持链接规范和选项来分类特征。
让我们把所有东西放在一起,在一个容器中测试 Flask 应用程序。您可以在这里找到最终的 docker 文件:
- CentOS:tagT25andrejschioppa/VW _ serving _ flask:CentOS _ v1**
- Ubuntu:tagandrejschioppa/VW _ serving _ flask:Ubuntu _ v1
让我们提取映像并将测试模型和数据复制到本地卷*😗
preparing to run a test
让我们从一个具有相关配置的容器开始:
starting a container for testing
现在,我们在笔记本电脑中打开另一个 shell,检查预测与测试数据是否一致:
the test
一切看起来都很好,我们开始在 Kubernetes 上部署。
在 Kubernetes 部署
最后一步,我们将在 Kubernetes 上部署该模型。这里的基本概念是:
- 持久卷 :这是一个我们存储模型的卷;这可以在服务于相同模型的容器之间共享;这样,我们只需要上传模型一次。在 Kubernetes 的术语中,具有共享网络、存储和如何运行它们的指令的一个或多个容器的组被称为 pod 。在我们的讨论中,pod 与容器是一样的,因为我们只需要一个容器来为模型服务。
- 部署 :这控制给定种类的多少个 pod 正在运行;它确保达到目标吊舱数量,如果某个吊舱崩溃,它会被一个新的吊舱所取代。通过这种方式,可以以可靠的方式扩展服务模型,因为我们可以确保始终有足够的 pod 来处理预测请求。
- 服务 :我们将使用它使我们部署中的 pod 共享相同的 IP 地址;通过这种方式,预测请求只发送到一个 IP 地址,而不必担心负载如何在 pod 之间平衡。
Kubernetes 可以在配置了不同配置的集群上运行。对于这篇文章,我将使用一个本地集群,即我的笔记本电脑上的集群。这可以通过使用 minikube 来实现,这是一个允许在本地运行 Kubernetes 的工具。安装 minikube 并不总是一个简单的过程,请参考文档。在我的例子中,我使用的是 Linux 发行版,所以我能够运行 minikube 而不需要安装管理程序,但是如果你使用的是 MacOS,你就需要安装一个。这里你可以用。我将使用的 yaml* 配置文件。*
让我们启动 Kubernetes 集群并创建一个目录 /mnt/data ,它将作为我们的持久卷。
starting minikube
然后,我们为永久卷和永久卷声明创建配置文件:
creating a persistent volume
claiming a persistent volume
我们现在可以创建体积和体积索赔
persistent volumes on the cluster
为了复制模型和配置文件,我们将使用一个“假部署”,它只是挂载持久卷。
a fake deployment to access the persistent volume
下一步是识别运行假部署的 pod,并复制模型和配置文件;我们终于可以登录到 pod 中检查一切是否正常:
copying the model in the persistent volume
部署只是运行具有所需环境变量的模型服务容器,这些环境变量指定了模型文件、模型配置和服务端口(在本例中为6025*);副本的数量指定了要保持运行多少个容器来分配负载:*
the application deployment
该服务将 pod 包装在同一个 IP 地址下;这样,我们就不需要担心如何在副本之间平衡请求:
the service
我们最终可以部署豆荚和服务;然后定位服务的 IP,并通过提交预测请求来运行测试:
deploying and testing
结论
通过一点点努力和设计考虑,我们已经将在没有本地服务 API 的库中训练的生产模型。
如果你面临一个类似的任务,我建议不要急于寻找快速的解决方案,不要在纸上探索设计思想及其结果;给予一些思考和反思通常可以避免走进死胡同。在这种情况下,理想的解决方案非常简单:只需使用一个 shell 进程,并通过 Python 控制它。
我非常感谢 Antonio Castelli 让我注意到 JNI 的问题,并测试了这个库的早期原型。
我将非常感谢对 GitHub 代码的改进/扩展的评论、建议和批评以及合并请求。