完整的深度学习组合项目第 2 部分
用 Flask 和 HTML 创建可部署的 Web 应用程序
沙哈达特·拉赫曼在 Unsplash 上拍摄的照片
介绍
本文是系列文章的第二部分。在这里,我着重于构建使用在第一篇文章中开发的 birds 分类器的 web 应用程序。正如我在第一部分提到的,成为一名机器学习工程师不仅意味着在 Jupyter 笔记本上训练模型,还意味着能够创建一个可以由最终用户直接部署和使用的应用程序。此外,这对你的投资组合也很有帮助,因为你现在有了一份可以展示给招聘人员的申请。这非常有帮助,因为他们通常没有技术背景,因此不想通过 Jupyter 笔记本来了解您在这个项目中开发了什么。
在本文的第一部分,我想引导您使用 html 创建前端部分。在第二部分,我将向您展示如何使用 Python 库 Flask 开发后端。
你可以在我的 Github 资源库中找到与这个项目相关的所有代码。图 1 展示了运行中的最终应用程序。
图 1:开发的 web 应用程序使用情况的 GIF
创建前端
每个应用程序都需要一个前端。前端部分非常重要,因为这是用户将要与之交互的部分。这个项目的前端是使用 html 开发的。html 代码在代码笔中创建。Code Pen 是一个网页,用户可以在交互式环境中在线创建和可视化他们的 html 代码。这非常适合快速制作网页原型!
这里重要的部分是从第 37 行到第 62 行。在这里,一个 base64 图像被创建并可视化,以防用户点击“选择文件”按钮。然后,base64 字符串被格式化,以便稍后可以发送到后端应用程序。当用户按下“预测”按钮时,用 POST 命令调用托管网页的预测端点。然后,预测端点对输入图像进行预处理,并将其提供给鸟类分类器模型。这将在下一节中进一步解释。
predict.html 文件存储在一个名为“模板”的文件夹中。这是以后烧瓶应用所需要的。
如果你不熟悉开发 html 代码,我真的可以向你推荐这个关于 html 编码的 Youtube 速成班。
代码 1:创建前端应用程序的 Html 代码。
创建后端
后端应用程序负责后台发生的“魔术”。birds 分类器的后端应用程序是使用 Python 库 Flask 开发的,这是一个易于使用的 web 框架。代码 2 展示了后端应用程序的完整 Python 代码。在第 19 行,应用程序本身被创建。
第 21 行是 Python 修饰符,定义了默认路由。该功能在应用程序启动时执行。在“我的表单”功能中,前端 html 文件链接到这个应用程序。
“get_model”函数加载经过训练的 Keras 模型,并将其存储在全局模型变量中。此外,还提供了预处理功能。此功能预处理输入图像,使其具有网络预期的输入格式。“load_classes”函数仅用于在以后将预测的标签链接到相应的类名。
在第 49 行,定义了预测路线。当前端应用程序中的预测按钮被调用时,这个路由被调用。在这里,图像被提取、预处理并馈送到网络。然后网络进行预测。预测的类别标签和输出分数然后被发送回前端应用程序,使得它可以在网站上显示。
main 函数加载模型和类,并在端口 5000 启动应用程序。
启动 Python 应用程序后,您可以在浏览器中打开链接 http://127.0.0.1 :5000 下的网站。
代码 2:用于创建后端应用程序的 Python 代码。
结论
所以现在已经开发了一个成熟的机器学习应用。在本系列的第一篇文章中,找到了最佳的训练策略,并训练了最终的模型。本文开发了相应的 web 应用程序。这个应用程序现在可以部署到像 AWS 这样的云提供商。当你想阅读我如何创建一个 AWS Elastic Beanstalk 的持续部署管道,这样每个人都可以访问创建的 web 应用程序,那么我可以推荐你阅读我的媒体文章。
谢谢你把我的文章看完!我希望你喜欢这篇文章和我参与的项目。如果你想在未来阅读更多类似的文章,请关注我,保持更新。
在服务器上全面部署 Jupyter 笔记本电脑,包括 TLS/SSL 和加密功能
在服务器或云上完成 Jupyter 笔记本电脑或 JupyterLab 的设置和安装
图片来自维基共享资源
Jupyter Notebook 是一个强大的工具,但是你如何在服务器上使用它呢?在本教程中,你将看到如何在服务器上设置 Jupyter notebook,如数字海洋、海兹纳、 AWS 或其他大多数可用的主机提供商。此外,您将看到如何通过 SSH 隧道或 TLS/SSL 使用 Jupyter notebooks 和让我们加密。本文原载此处。
Jupyter 是一个开源的 web 应用程序,支持从浏览器进行交互式计算。您可以创建包含实时代码的文档、带有 Markdown 的文档、等式、可视化甚至小部件和其他有趣的功能。Jupyter 来自支持的三种核心语言:Julia、Python 和 r。Jupyter 连接到使用特定语言的内核,最常见的是 IPython 内核。它支持各种各样的内核,你应该能找到你需要的大多数语言。本教程写于 JupyterLab ,Jupyter 笔记本的下一步发展:
在本教程中,我们将使用 Ubuntu 18.04/20.04 服务器,但大多数步骤应该与 Debian 9/10 发行版相当相似。我们将首先创建 SSH 密钥,在服务器上添加一个新用户,并使用 Anaconda 安装 Python 和 Jupyter。接下来,您将设置 Jupyter 在服务器上运行。最后,你可以选择在 SSH 隧道上运行 Jupyter 笔记本,或者在 SSL 上运行让我们加密。
创建 SSH 密钥
我们从一个全新的服务器开始,为了在访问您的服务器时增加更多的安全性,您应该考虑使用 SSH 密钥对。这些密钥对由上传到服务器的公钥和保存在机器上的私钥组成。一些主机提供商要求您在创建服务器实例之前上传公钥。要创建新的 SSH 密钥,您可以使用 ssh-keygen 工具。要创建密钥对,您只需键入以下命令:
ssh-keygen
如果您愿意,这将提示您添加文件路径和密码。还有其他选项参数可供选择,如公钥算法或文件名。你可以在这里找到一个非常好的教程关于如何用 ssh-keygen 为 Linux 或 macOS 创建一个新的 SSH 密钥。如果您使用的是 Windows,您可以使用 PuTTYgen 创建 SSH-keys,如这里的所述。如果您的主机提供商在创建前不需要公钥,您可以使用 ssh-copy-id 工具复制公钥:
ssh-copy-id -i ~/.ssh/jupyter-cloud-key user@host
最后,您可以通过以下方式连接到您的服务器:
ssh -i ~/.ssh/id_rsa root@host
其中~/.ssh/id_rsa
是您的 ssh 私有密钥的路径,而host
是您的服务器实例的主机地址或 IP 地址。
添加新用户
在一些服务器中,您是作为根用户开始的。直接使用根用户被认为是一种不好的做法,因为它有很多特权,如果某些命令是意外执行的,这些特权可能是破坏性的。如果您已经有用户,可以跳过这一部分。请注意,您可以将以下所有命令中的cloud-user
替换为您想要的用户名。首先创建一个新用户:
adduser cloud-user
这个命令会问你几个问题,包括密码。接下来,您需要向该用户授予管理权限。您可以通过键入以下命令来完成此操作:
usermod -aG sudo cloud-user
现在你可以用su cloud-user
切换到新用户,或者用ssh cloud-user@host
连接到你的服务器。或者,您可以将 root 用户的 SSH 密钥添加到新用户中,以提高安全性。否则,您可以跳到下一节如何安装 Anaconda。现在,如果您有根用户的现有 SSH 密钥,您可以将公钥从根主文件夹复制到用户主文件夹,如下所示:
mkdir /home/cloud-user/.ssh
cp /root/.ssh/authorized_keys /home/cloud-user/.ssh/
接下来,您需要更改文件夹和公钥的权限:
cd /home/user/
chmod 700 .ssh/
chmod 600 .ssh/authorized_keys
如果您正在为您的用户使用密码,您需要更新/etc/ssh/sshd_config
:
nano /etc/ssh/sshd_config
在这里,您希望找到行PasswordAuthentication no
,并将no
更改为yes
,以允许密码验证。最后,您希望通过键入service ssh restart
来重启 SSH 服务。对于其他发行版,请看一下这个指南,在那里你也会看到如何设置防火墙。
安装 Anaconda
Anaconda 是 Python(和 R)的开源发行版,用于科学计算,包括包管理和部署。有了它,你就有了你需要的大部分工具,包括 Jupyter。要安装 Anaconda,请转到 linux 的下载,并复制最新 Python 3.x 版本的 Linux 安装程序链接。然后你可以用wget
下载安装程序:
wget [https://repo.anaconda.com/archive/Anaconda3-5.2.0-Linux-x86_64.sh](https://repo.anaconda.com/archive/Anaconda3-5.2.0-Linux-x86_64.sh)
接下来,您可以使用bash
安装 Anaconda,如下所示:
bash Anaconda3-5.2.0-Linux-x86_64.sh
在安装过程中,当安装过程中出现以下提示时,键入yes
非常重要:
Do you wish the installer to prepend the Anaconda3 install location
to PATH in your /home/user/.bashrc ? [yes|no]
安装完成后,您希望通过 Anaconda 用以下命令初始化conda
命令行工具和包管理器:
source .bashrc
conda update conda
这两个命令在您的服务器上设置 Anaconda。如果您已经用 sudo 运行了 Anaconda bash 文件,您将得到一个Permission denied
错误。你可以通过输入sudo chown -R $$USER:$$USER /home/user/anaconda3
来解决这个问题所示的问题。这将使用 chown 命令将该文件夹的所有者更改为当前用户。
正在启动 Jupyter 笔记本服务器
Jupyter 与 Anaconda 一起安装,但是我们需要做一些配置以便在服务器上运行它。首先,您需要为 Jupyter 笔记本创建一个密码。您可以通过用ipython
启动 IPython shell 并生成一个密码散列来实现这一点:
from IPython.lib import passwd
passwd()
暂时保存这个结果散列,我们稍后会用到它。接下来,您想要生成一个配置文件,您可以通过键入。
jupyter-notebook --generate-config
现在用sudo nano ~/.jupyter/jupyter_notebook_config.py
打开配置文件,将下面的代码复制到文件中,并用您之前生成的代码替换这个代码片段中的散列:
c = get_config() # get the config object
# do not open a browser window by default when using notebooks
c.NotebookApp.open_browser = False
# this is the password hash that we generated earlier.
c.NotebookApp.password = 'sha1:073bb9acaa67:b367308802ab66cb1d7654b6684eafefbd61d004'
现在你应该设置好了。接下来,您可以决定是使用 SSH 隧道还是使用 SSL 加密并通过您自己的域名访问您的 jupyter 笔记本。
Linux 或 MacOS 上的 SSH 隧道
您可以通过在负责端口转发的ssh
命令中添加-L
参数来隧道连接到您的服务器。第一个8888
是您将在本地机器上访问的端口(如果您已经为另一个 juypter 实例使用了这个端口,那么您可以使用端口 8889 或不同的开放端口)。你可以用localhost:8888
在你的浏览器上访问它。第二部分localhost:8888
指定从服务器访问的跳转服务器地址。因为我们希望在服务器上本地运行笔记本,所以这也是 localhost。这意味着我们从服务器通过端口转发到我们机器上的localhost:8888
来访问localhost:8888
。下面是该命令的样子:
ssh -L 8888:localhost:8888 cloud-user@host
如果您的本地机器上已经运行了另一个 Jupyter 笔记本,您可以将端口更改为8889
,这将导致命令:
ssh -L 8889:localhost:8888 cloud-user@host
现在,您可以在服务器上为您的项目创建一个笔记本文件夹,并在其中运行 Jupyter notebook:
mkdir notebook
cd notebook/
jupyter-notebook
你也可以使用 JupyterLab 来代替,这是一个更强大的接口,它也预装了 Anaconda。你可以通过输入jupyter-lab
而不是juypter-notebook
来启动它。
让我们加密的 SSL 加密
也可以对你的 jupyter 笔记本使用 SSL 加密。这使您能够通过互联网访问您的 Jupyter 笔记本,从而方便地与您的同事分享结果。要做到这一点,您可以使用让我们加密,这是一个免费的认证中心(CA) ,它为 TLS/SSL 证书提供了一种简单的方法。这可以用他们的 certbot 工具全自动完成。要找到您系统的安装指南,请查看此列表。对于 Ubuntu 18.04,安装如下所示:
sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository universe
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot python-certbot-apache
现在,您可以为您拥有的域运行 certbot:
sudo certbot certonly -d example.com
完成提示后,您应该会看到以下输出:
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/example.com/privkey.pem
Your cert will expire on 2019-05-09\. To obtain a new or tweaked
version of this certificate in the future, simply run certbot again
with the "certonly" option. To non-interactively renew *all* of
your certificates, run "certbot renew"
- If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: [https://eff.org/donate-le](https://eff.org/donate-le)
太好了!您已经准备好了证书和密钥文件。现在,您可以使用 jupyter 笔记本配置文件中的证书和密钥文件。在此之前,您需要更改证书和密钥文件的所有者(用您自己的用户名更改user
):
sudo chown user /usr/local/etc/letsencrypt/live
sudo chown user /usr/local/etc/letsencrypt/archive
接下来,您可以将以下代码添加到~/.jupyter/jupyter_notebook_config.py
配置文件中:
# Path to the certificate
c.NotebookApp.certfile = '/etc/letsencrypt/live/example.com/fullchain.pem'
# Path to the certificate key we generated
c.NotebookApp.keyfile = '/etc/letsencrypt/live/example.com/privkey.pem'
# Serve the notebooks for all IP addresses
c.NotebookApp.ip = '0.0.0.0'
最后,你可以通过https://example.com:8888
安全地访问 Jupyter 笔记本。只是确保使用https://
而不是http://
。如果您犯了任何错误,您可以用sudo certbot delete
或sudo certbot delete --cert-name example.com
删除 certbot 证书。如果您使用的是防火墙,请确保端口8888
是打开的。这里有一个关于使用简单防火墙(UFW) 防火墙的很好的指南。
结论
您已经从头到尾学习了如何为服务器设置 Jupyter。随着您设置的每一台服务器,这项任务变得越来越容易。请务必深入研究 Linux 服务器管理的相关主题,因为一开始使用服务器可能会令人生畏。使用 Jupyter,您可以访问各种各样的内核,这些内核使您能够使用其他语言。所有可用内核的列表可以在这里找到。我希望这是有帮助的,如果你有任何进一步的问题或意见,请在下面的评论中分享。
我在之前的教程中提到了如何在 Jupyter 笔记本中使用虚拟环境。还有一个将 Jupyter 作为 Docker 容器运行的选项。例如,您可以使用jupyter/data science-notebook容器。你可以在本指南的中阅读更多关于如何使用 Jupyter 和 Docker 的信息。关于进一步的安全考虑,请看一下 Jupyter 笔记本服务器中的安全性。以下是我从中学到的更多链接,可能对你也有用:
- 初始服务器设置
- 运行笔记本服务器
- 如何为 Python 3 设置 Jupyter 笔记本
- 如何使用 Certbot 独立模式检索加密 SSL 证书
- UFW 基础:通用防火墙规则和命令
- 在 Jupyter 笔记本和 Python 中使用虚拟环境
- 用 Jupyter 笔记本制作幻灯片
更多参考
梯度下降的完全实现
深入研究梯度下降算法的数学。
这篇文章推导了实现梯度下降的算法。它又脏又丑又乏味!自己推导它是一个有趣的练习,但最终,它只是识别模式。不要让这个页面让你脑袋爆炸,让你放弃 AI!仅仅知道如何使用算法是最重要的(即使这样也可以认为你可以只使用 Keras),但是为什么不自己尝试一下并从中获得一些乐趣呢!我在这里包括推导,只是因为我知道当我学习人工智能时,我会喜欢看它。
我们现在知道,我们必须找到误差函数相对于每个权重的梯度,以最小化我们的误差(如果你不知道我刚才说了什么,请学习/复习如何训练神经网络这里)。这很好,但是我们如何用代码来实现呢?你可以想象,如果我们有一个 5、6 层或更多层的神经网络,每层都有数百或数千个神经元,找到梯度会有多困难。
我们知道要找什么(梯度),现在我们只需要弄清楚如何找到它。
我们可以用导数的定义。仔细检查每一个重量,并稍微增加一点,然后将误差的变化除以这个微小的量。这将给出这个特定重量的∂E/∂W。如果我们对所有的重量都这样做,我们就会得到梯度。
但是客观地看,我训练用来玩雅达利游戏的神经网络有 1,685,667 个权重,我必须更新它们大约 1000 万次。对于这样的网络来说,使用导数的定义太费时间了。
由于神经网络具有非常高的重复率,看起来我们可以找到一种算法,以一种有效的方式为我们找到梯度。为了尝试创建这种算法,我们将首先手动写出一个相对较小但较深的神经网络的前进和更新阶段。然后,我们将在这个过程中寻找模式,尽可能地简化它。
这是我们将用来推导梯度下降算法的神经网络:
*这一次我们实施的是偏向。请注意,只要我们将偏置“输入”保持为 1,上述实现与仅将偏置添加到每个神经元的加权和上完全相同。偏向是蓝色的,任何与偏向相关的东西在这个页面上都是蓝色的。*图片作者
正向阶段
首先,非常重要的是要记住,我们绘制神经网络作为这个计算图,是为了帮助我们可视化正在发生的事情。但是,如果我们要找到我们的梯度下降算法,我们将需要写出完成的计算。嗯,我们这里有很多参数!我们真的为丑陋的索引做了大量的参数!
让我写出这个神经网络中所有的参数。然后让你了解我的索引惯例。
图片作者
上面的 Xbar 是每一层的*【输入】*列表。注意,Xbar 中每个列表的大小对应于每层中神经元的数量。Xbar[0]是带有偏差的初始输入数据,Xbar[1]是带有级联偏差的 X[0]的激活加权和,Xbar[2]是带有级联偏差的 X[1]的激活加权和,依此类推。
Wbar 是连接每一层的所有权重的列表。每个列表都应该有大小:
(不包括偏差的下一层神经元的数量,包括偏差的当前层神经元的数量)。
例如,第一个权重列表有 3 行,因为在下一层中有 3 个神经元(不包括偏差)。该列表有 3 列,因为有 2 个输入和 1 个偏差。
现在,左上角的索引显示了特定元素所在的层。右上角的指数告诉我们正在谈论的层中的输入神经元,右下角的指数告诉我们将要输入什么。
*X 是层的输入,w 是层的权重,z 是通过激活函数之前的加权和。*图片作者
现在我们在记谱法上达成了一致。这是神经网络的前向阶段。从左到右,从上到下,写出计算结果:
*a(z)是我们应用于 z 的激活函数。退一步,看看在高层次上发生了什么。不要让你的脑袋爆炸,试图同时意识到每个细节!*图片作者
最上面一行计算将我们从第一层输入带到第二层输入。第二行将我们从第二层输入带到第三层输入,依此类推,直到我们到达一个输出。
反向相位
好了,现在让我把我们要找的东西写出来;
图片作者
我们将试图找到足够的第三和第二个导数列表来确定一个模式。利用链式法则,以及大量盯着的问题,我们可以证明这些导数等于下列;
我突出显示了我认识的任何模式,这将有助于简化。第二梯度矩阵只是部分完成。我们只需要足够的证据来确定模式。图片作者
如果上面的照片有点模糊,建议你下载一份,研究一段时间。我首先计算导数,然后突出矩阵中的相似之处。模式识别是这个游戏的名字。
这可能很难理解你自己。经常回头看看前进阶段的照片。如果你从这个错误开始,然后用链式法则让你自己回到你试图找到的重量,你会发现这很简单,但是很乏味。
现在,我们试图找到一种更简单的方法来表达上述方程。如果你记得你的线性代数,你会发现上面的方程可以简化为:
那个里面有 X 的圆圈表示我们正在对行进行乘法运算。矩阵之间的点表示我们正在应用点积。图片作者
看多漂亮!你可以注意到所有红色的东西都和上面一行中高亮显示的一样。这意味着我们可以从最后一层开始,初始化一个术语,称之为ψ,就是绿色的。然后,当我们回到之前的层时,我们可以用该层中的绿色来更新ψ(红色的就是ψ的当前值)。现在,要找到该层的梯度,我们只需将ψ乘以∂Z/∂w 行。当你插入数值时,这是非常漂亮的,下面我假设均方误差函数和 sigmoid 激活函数。
图片作者
现在,我们将更新打包成一个非常简单的 for 循环:
图片作者
摘要
好吧,我知道你在想什么。刚刚发生了什么事!?我所做的只是识别模式!没有比链式法则更复杂的东西被用到,我只是用了很多。如果你只是相信我的结果,而不是自己推导出来的,我不会怪你。在实践中,我们无论如何都将使用 Keras 来实现,但是现在您已经知道如何自己做了,您不必为这样做而感到内疚。
一如既往,我们还没有真正理解这一点!直到我们应用它,我们才会理解它。我在这里用 python 实现了一个简单的实现,在这个链接中我还构建了一个有用的 python 类,我们稍后会用它来识别手写数字。
感谢您的阅读!如果这篇文章在某种程度上帮助了你,或者你有什么意见或问题,请在下面留下回复,让我知道!此外,如果你注意到我在某个地方犯了错误,或者我可以解释得更清楚一些,那么如果你能通过回复让我知道,我会很感激。
这是一系列文章的继续,这些文章从头开始对神经网络进行了直观的解释。其他文章请参见下面的链接:
第 3 部分:梯度下降的完全实现
满秩联合分析
用于洞察消费者偏好的技术
由吉纳·戈麦斯
与格雷戈·佩奇
我们为这个练习建立了数据集,通过模拟一项调查,要求回答者对一系列 24 个独立的手机月套餐进行排序,1 是最好的,24 是最差的。在向 1000 名受访者展示调查后,每组选项都会根据其在所有受访者中的平均排名获得一个排名。
这些计划包括互联网数据的两个选项、通话时间的三个选项、音乐的两个选项和社交网络接入的两个选项。这些功能基于一家哥伦比亚电信公司提供的真实电话套餐。价格大致基于实际功能价格,从哥伦比亚比索兑换回美元。
数据集有 7 个变量,变量的规格见下表:
bundleID: 数据集每一行的 ID。
**千兆字节:**在互联网上导航的千兆字节数。选项是 4 和 10。
**分钟:**人们必须拨打电话的分钟数。选项 100、200 和 400。
**音乐:**计划是否包含免费获取音乐(“是”或“否”值)。
social_networks: 计划是否包括无限制访问社交网络(“是”或“否”值)。
**价格:**计划在哥伦比亚的价格,以美元表示。
**排名:**根据近 1000 名受访者的调查答案,对 1 至 24 个首选预付费计划进行排名。
**flip 恶作剧:**每个捆绑包的“翻转”等级,呈现为受青睐的捆绑包采用较高的值,而不是较低的值。这将使线性回归系数更容易解释。
当受访者评估捆绑包时,他们会考虑成本—否则,我们可以预期受访者会简单地选择“最佳”或功能最多的选项。这里显示了增加的功能成本:
通过添加上表中的功能,可以找到任何特定捆绑包的成本。例如,一个 10gb 的套餐,400 分钟的通话时间,没有音乐,可以访问社交网络,价格为 2.50 美元+3.35 美元+0.00 美元+1.55 美元,或 7.40 美元。一个 4.5 的套餐,有 200 分钟的通话时间,可以访问音乐,但不能访问社交网络,价格为 1.50 美元+2.95 美元+1.60 美元+ 0.00 美元,即 6.05 美元。
为了运行线性回归并能够更容易地读取结果,我们修改了 rank 变量,将#1 包的 CorrectedRank 值设为 24,将#2 包的 CorrectedRank 值设为 23,将#3 包的 CorrectedRank 值设为 22,依此类推。
为什么要使用完整的排名系统?
在之前的一篇文章中,我们写了一个基于评级的联合分析系统,在这个系统中,调查受访者考虑游乐园乘坐的功能,从 1 到 10 对各种功能组合进行评级。
虽然这种系统非常适合线性建模,但它有一个明显的缺陷——当要求在 1-10 的范围内赋值时,并非所有受访者都使用相同的评级“基线”。例如,一个电影评论者可能会给出 7.2 分(满分 10 分),而另一个看过同一组电影的评论者给出的平均分接近 5.0 分。
有了完整的排名系统,基线问题就解决了,因为所有回答者都被简单地要求确定每个选项的相对吸引力。然而,这种系统的一个可能的缺点是调查疲劳的风险——如果有太多的捆绑包需要排序,受访者可能会不知所措。随着更多功能和更多级别的添加,对包总数的影响是倍增的。在这里,只有 2 个数据选项、3 个通话时间选项、2 个音乐选项和 2 个社交网络选项,我们已经有了 24 个组合(2 x 3 x 2 x 2)。
满秩场景的线性建模
如上所述,为了提高线性回归建模的适用性,我们“翻转”了排名,以便更高的值将与更受青睐的包相关联。这使得线性回归系数的解释变得更容易——正值现在与更好的选项相关联,而负值与更差的选项相关联。
下面您可以看到在环境中读取的干净数据集:
在运行这样一个模型之前,所有的输入特性都应该被虚拟化,包括数据计划和每月通话分钟数等数值。这样,模型结果会将每个特征选项显示为一个离散的选项。这样做意味着我们可以给每一个选项附加一个精确的系数值,而不是用一个单一的数字系数来试图表达整个连续的可能选项范围内的偏好。
pandas 的 get_dummies()函数帮助我们为线性建模准备变量:
然后,使用 scikit-learn 的 LinearRegression 模块,以 fli 恶作剧作为结果变量,以千兆字节、分钟、音乐和社交网络的虚拟化水平作为输入,构建线性模型:
至于使用普通最小二乘线性回归的适用性,有几个问题值得注意。
首先,这个数据集中的结果值是均匀分布的,范围是 1 到 24。如果我们将这个模型用于预测目的,这个范围约束可能会出现问题(我们如何解释小于 1 或大于 24 的值?),但这并不妨碍我们出于解释的目的使用该模型。我们的目标只是更好地理解功能选项和客户偏好之间的关系,我们可以在这里完成。
其次,考虑到响应变量的非正态分布,异方差的风险可能会增加。我们使用回归诊断图来检查这一点,并在模型结果中没有发现异方差的证据。
解读结果
模型系数揭示了一些有价值的模式:
例如,他们向我们表明,调查受访者非常强烈地支持社交网络接入和 10gb 的数据计划。
较大的通话分钟数和音乐选项的负值不一定意味着客户根本不想要这些选项。相反,它可能表明消费者不希望支付与这些功能相关的增量成本。电信运营商可能希望在未来的调查中修改这些选项——如果相关成本降低,受访者可能会更倾向于这些选项。
或者,对这些特征缺乏兴趣可能表明消费者口味的其他方面。也许这个群体中的消费者习惯于通过其他格式听音乐,并且根本不打算每月使用超过 100 分钟的通话时间。通过功能和定价选项的不断迭代,以及对实际用户数据的分析,电信公司可以不断努力为其消费者群找到一组理想的选择。
福克斯尔斯,摄影师。(2019 年 11 月 5 日)。拿着手机的人。从 Pexels 检索。
在 Docker 上使用 Node.js 和 ElasticSearch 进行全文搜索
让我们基于 Node.js、ElasticSearch 和 Docker 构建一个真实世界的应用程序
全文搜索既令人害怕又令人兴奋。一些流行的数据库,如 MySql 和 Postgres,是存储数据的惊人解决方案…但当谈到全文搜索性能时,没有与 ElasticSearch 竞争。
对于那些不知道的人来说, ElasticSearch 是一个建立在 Lucene 之上的搜索引擎服务器,具有惊人的分布式架构支持。根据 db-engines.com 的说法,它是目前使用最多的搜索引擎。
在这篇文章中,我们将构建一个简单的 REST 应用程序,称为报价数据库,它将允许我们存储和搜索尽可能多的报价!
我准备了一个 JSON 文件,其中包含 5000 多条作者引用;我们将用它作为填充 ElasticSearch 的初始数据。
您可以在这里找到这个项目的资源库。
设置 Docker
首先,我们不想在我们的机器上安装 ElasticSearch。我们将使用 Docker 在一个容器上编排 Node.js 服务器和 ES 实例,这将允许我们部署一个生产就绪的应用程序以及它需要的所有依赖项!
让我们在项目根文件夹中创建一个Dockerfile
:
如你所见,我们告诉 Docker 我们将运行 Node.js 10.15.3-alpine 运行时。我们还将在/usr/src/app
下创建一个新的工作目录,在那里我们将复制package.json
和package-lock.json
文件。这样,Docker 将能够在我们的WORKDIR
中运行npm install
,安装我们需要的依赖项。
我们还将通过运行RUN npm install -g pm2
在全球范围内安装 PM2 。Node.js 运行时是单线程的,因此如果一个进程崩溃,整个应用程序都需要重启…PM2 检查 Node.js 进程状态,并在应用程序因任何原因关闭时重新启动它。
安装 PM2 后,我们将在我们的WORKDIR
( COPY . ./
)中复制我们的代码库,我们告诉 Docker 公开两个端口:3000
,这将公开我们的 RESTful 服务,和9200
,这将公开 ElasticSearch 服务(EXPOSE 3000
和EXPOSE 9200
)。
最后,我们告诉 Docker 哪个命令将启动 Node.js 应用程序:npm run start
。
设置 docker 撰写
现在你可能在想,“太好了,我明白了!但是我如何在 Docker 中处理 ElasticSearch 实例呢?我在我的文档里找不到!_“…你说得对!这就是 docker-compose 派上用场的地方。它允许我们编排多个 Docker 容器,并在它们之间创建连接。因此,让我们写下docker-compose.yml
文件,它将存储在我们的项目根目录中:
这比我们的 docker 文件要复杂一些,但是让我们来分析一下:
- 我们声明我们使用的是哪个版本的文件(
3.6
) - 我们声明我们的服务:
api
这是我们的 Node.js 应用程序。就像在我们的 docker 文件中一样,它需要node:10.15.3-alpine
图像。我们还为这个容器指定了一个名字:tqd-node
,在这里,我们使用build .
命令调用之前创建的 Dockerfile。然后,我们需要公开3000
端口:如您所见,我们将这些语句编写如下:3000:3000
。这意味着我们从端口3000
(在我们的容器内)映射到端口3000
(可以从我们的机器访问)。然后我们将设置一些环境变量。值elasticsearch
是一个变量,它引用我们的docker-compose.yml
文件中的elasticsearch
服务。我们还想挂载一个卷:/usr/src/app/quotes
。这样,一旦我们重启我们的容器,我们将维护我们的数据而不丢失它。我们再次告诉 Docker,一旦容器启动,我们需要执行哪个命令,然后我们设置一个到elasticsearch
服务的链接。我们还告诉 Docker 在elasticsearch
服务启动后启动api
服务(使用depends_on
指令)。最后,我们告诉 Docker 在esnet
网络下连接api
服务。这是因为每个容器都有自己的网络:这样,我们说api
和elasticsearch
服务共享同一个网络,所以它们将能够用相同的端口相互调用。这是(你可能已经猜到了)我们的 ES 服务。它的配置与api
服务非常相似。我们将把logging
指令设置为driver: none
来删除它的详细日志。 - 我们声明存储 es 数据的卷
- 我们宣布我们的网络,
esnet
引导 Node.js 应用程序
现在我们需要创建 Node.js 应用程序,所以让我们开始设置我们的package.json
文件:
npm init -y
现在我们需要安装一些依赖项:
npm i -s @elastic/elasticsearch body-parser cors dotenv express
太好了!我们的package.json
文件应该是这样的:
让我们在 Node.js 中实现我们的 ElasticSearch 连接器。首先,我们需要创建一个新的/src/elastic.js
文件:
如你所见,这里我们设置了一些非常有用的常量。首先,我们使用其官方 Node.js SDK 创建一个到 ElasticSearch 的新连接;然后,我们定义一个索引("quotes"
)和一个索引类型("quotes"
),我们稍后会看到它们的含义。
现在我们需要在 ElasticSearch 上创建一个索引。您可以将“索引”视为 SQL“数据库”的等价物。ElasticSearch 是一个 NoSQL 数据库,这意味着它没有表——它只存储 JSON 文档。索引是映射到一个或多个主碎片的逻辑名称空间,可以有零个或多个副本碎片。你可以在这里阅读更多关于弹性搜索指数的信息。
现在让我们定义一个创建索引的函数:
现在我们需要另一个函数来为我们的报价创建映射。映射定义了我们文档的模式和类型:
如您所见,我们正在为文档定义模式,并将它插入到我们的index
中。
现在让我们考虑一下,ElasticSearch 是一个庞大的系统,可能需要几秒钟才能启动。在 ES 准备好之前,我们无法连接到 ES,因此我们需要一个函数来检查 ES 服务器何时准备好:
如你所见,我们正在回报一个承诺。这是因为通过使用,async/await
,我们能够停止整个 Node.js 进程,直到这个承诺得到解决,并且它不会这样做,直到它连接到 es。这样,我们强制 Node.js 在启动前等待 ES。
我们已经完成了 ElasticSearch!现在,让我们导出我们的函数:
太好了!让我们看看整个elastic.js
档案:
用报价填充弹性搜索
现在我们需要用我们的报价填充我们的 ES 实例。这听起来很容易,但是相信我,这可能是我们应用程序中很棘手的一部分!
让我们在/src/data/index.js
中创建新文件:
正如您所看到的,我们正在导入刚刚创建的elastic
模块和来自存储在/src/data/quotes.json
中的 JSON 文件的报价。我们还创建了一个名为esAction
的对象,一旦我们插入一个文档,它将告诉 ES 如何索引它。
现在我们需要一个脚本来填充我们的数据库。我们还需要创建一个具有以下结构的对象数组:
如您所见,对于我们将要插入的每个报价,我们需要将其映射设置为 ElasticSeaech。这就是我们要做的:
太好了!现在让我们创建我们的主文件/src/main.js
,看看我们将如何组织我们到目前为止所写的所有内容:
我们来分析一下上面的代码。我们创建一个自动执行的 main 函数来检查 ES 连接。在 ES 连接之前,代码不会执行。当 ES 准备好时,我们将检查quotes
索引是否存在:如果不存在,我们将创建它,我们将设置它的映射,并将填充数据库。显然,我们只有在第一次启动服务器时才会这样做!
创建 RESTful API
现在我们需要创建 RESTful 服务器。我们将使用 Express.js,它是构建服务器最流行的 Node.js 框架。
我们将从/src/server/index.js
文件开始:
如你所见,它只是一个标准的 Express.js 服务器;我们不会在那上面花太多时间。让我们看看我们的/src/server/routes/index.js
档案:
我们创建两个端点:
GET /
将返回与我们的查询字符串参数匹配的报价列表。POST /new/
将允许我们发布一个新的报价存储在弹性搜索。
现在让我们看看我们的/src/server/controllers/index.js
文件:
这里我们基本上定义了两个函数:
getQuotes
,需要至少一个查询字符串参数:text
addQuote
,需要两个参数:author
和quote
ElasticSearch 接口委托给我们的/src/server/models/index.js
。这种结构有助于我们维护一个类似 MVC 的架构。让我们看看我们的模型:
如您所见,我们通过选择包含给定单词或短语的每个报价来构建我们的 ElasticSearch 查询。然后,我们生成查询,设置page
和limit
值:我们可以在查询字符串中传递它们,例如:http://localhost:3000/quotes?text=love&page=1&limit=100
。如果这些值没有通过查询字符串传递,我们将使用它们的默认值。
ElasticSearch 返回大量数据,但我们需要四样东西:
- 报价 ID
- 引用本身
- 引用作者
- 得分
分数代表报价与我们的搜索词的接近程度;一旦我们有了这些值,我们就将它们和总结果数一起返回,这在前端对结果进行分页时可能会很有用。
现在我们需要为模型创建最后一个函数:insertNewQuote
:
这个函数很简单:我们将引文和作者发布到我们的索引中,并将查询结果返回给控制器。现在,完整的/src/server/models/index.js
文件应该如下所示:
我们完事了。我们需要从里到外设置我们的启动脚本package.json
文件,我们已经准备好了:
一旦连接了 ElasticSearch,我们还需要更新我们的/src/main.js
脚本来启动我们的 Express.js 服务器:
启动应用程序
我们现在准备使用 docker-compose 启动我们的应用程序!只需运行以下命令:
$ docker-compose up
您需要等到 Docker 下载了 ElasticSearch 和 Node.js 图像,然后它将启动您的服务器,您就可以对 REST 端点进行查询了!
让我们用几个 cURL 调用进行测试:
$ curl localhost:3000/quotes?text=love&limit=3
{
"success": true,
"data": {
"results": 716,
"values": [
{
"id": "JDE3kGwBuLHMiUvv1itT",
"quote": "There is only one happiness in life, to love and be loved.",
"author": "George Sand",
"score": 6.7102118
},
{
"id": "JjE3kGwBuLHMiUvv1itT",
"quote": "Live through feeling and you will live through love. For feeling is the language of the soul, and feeling is truth.",
"author": "Matt Zotti",
"score": 6.2868223
},
{
"id": "NTE3kGwBuLHMiUvv1iFO",
"quote": "Genuine love should first be directed at oneself if we do not love ourselves, how can we love others?",
"author": "Dalai Lama",
"score": 5.236455
}
]
}
}
如你所见,我们决定将结果限制在3
,但是还有其他 713 个引用!我们可以通过调用以下命令轻松获得接下来的三个报价:
$ curl localhost:3000/quotes?text=love&limit=3&page=2{
"success": true,
"data": {
"results": 716,
"values": [
{
"id": "SsyHkGwBrOFNsaVmePwE",
"quote": "Forgiveness is choosing to love. It is the first skill of self-giving love.",
"author": "Mohandas Gandhi",
"score": 4.93597
},
{
"id": "rDE3kGwBuLHMiUvv1idS",
"quote": "Neither a lofty degree of intelligence nor imagination nor both together go to the making of genius. Love, love, love, that is the soul of genius.",
"author": "Wolfgang Amadeus Mozart",
"score": 4.7821507
},
{
"id": "TjE3kGwBuLHMiUvv1h9K",
"quote": "Speak low, if you speak love.",
"author": "William Shakespeare",
"score": 4.6697206
}
]
}
}
如果您需要插入新的报价呢?就叫/quotes/new
端点吧!
$ curl --request POST \
--url http://localhost:3000/quotes/new \
--header 'content-type: application/json' \
--data '{
"author": "Michele Riva",
"quote": "Using Docker and ElasticSearch is challenging, but totally worth it."
}'
答案会是:
{
"success": true,
"data": {
"id": "is2QkGwBrOFNsaVmFAi8",
"author": "Michele Riva",
"quote": "Using Docker and ElasticSearch is challenging, but totally worth it."
}
}
结论
Docker 使得管理我们的依赖项和它们的部署变得非常容易。从那时起,我们可以轻松地在 Heroku 、 AWS ECS 、 Google Cloud Container 或任何其他基于 Docker 的服务上托管我们的应用程序,而无需费力地用它们的超级复杂的配置来设置我们的服务器。
下一步?
- 了解如何使用 Kubernetes 来扩展您的容器并编排更多的弹性搜索实例!
- 创建一个允许您更新现有报价的新端点。错误是会发生的!
- 那么删除一个报价呢?您将如何实现该端点?
- 用标签保存你的引语会很棒(例如,关于爱情、健康、艺术的引语)…试着更新你的
quotes
索引!
软件开发很有趣。有了 Docker,Node,和 ElasticSearch,就更好了!
使用 AWS CI/CD 工具完全自动化您的 ML 管道
利用可信的 DevOps 工具集构建自动化 MLOps 管道的指南。
在过去的几年里,由于谷歌和脸书等公司的进步,以及开源社区的贡献,机器学习(ML)的受欢迎程度呈指数级增长。由于它可以应用于非常广泛的用例,几乎世界上的每个公司都开始利用 ML 并将其集成到其流程中。
这种大规模采用 ML 最初缺少一个关键部分:自动化。即使 ML 系统基本上是软件系统,它们之间的细微差别,例如 ML 本质上是实验性的,使得一些最初的采用者在构建他们的 ML 系统时放弃了 DevOps 原则——即使 DevOps 已经是其他软件系统的标准。
这为人工智能平台向其客户提供 MLOps 功能创造了空间,允许他们通过应用 CI/CD 原则来自动化其 ML 管道。在本文中,我们的目标是展示如何通过依赖大多数云提供商提供的工具,将我们在 DevOps 管道中日常使用的 CI/CD 原则应用到 ML 管道中。我们将在本文中讨论 AWS 工具,但大多数其他云提供商也提供类似的功能。
首先:把代码放在一个地方
是的,ML 本质上是实验性的,所以应用源代码控制可能不像对其他软件系统那样简单。但是,一旦你开始将你的 ML 代码库的不同组件(从预处理、训练或后处理脚本,到你的实验笔记本)集中在一个主要服务中,你将开始收获好处:协作将变得更加无缝,并且你将确保代码质量。
与其他软件系统不同,对 ML 来说,对代码进行版本控制只是工作的一半,因为管道也依赖于数据集。理想情况下,你不希望通过你的源代码控制服务来版本化你的数据集,所以你可以利用S3提供的版本化功能。这样,无论何时新的数据集到达,您都可以确定您将在管道中使用它,同时还可以跟踪数据的确切版本。至于笔记本,你可以使用像 Jupytext 这样的工具,让它们更容易在你的版本控制服务上工作。
这一步还将允许您定义最适合您的项目的分支策略,以及您决定如何组织它们。无论您决定使用 AWS CodeCommit 还是其他提供者,如 GitHub 或 GitLab,都不会真正影响管道的其余部分,因为我们将要讨论的工具提供了大多数主流源代码控制服务的连接器。
管道的当前版本:版本化代码和数据集
AWS 代码管道:更大的图景
构建我们的 MLOps 管道的最佳起点实际上是定义管道本身,即使不同的组件仍然没有准备好。AWS CodePipeline 是 AWS 持续交付产品中经常被忽视的核心部分,有了它,您将能够构建一个实际的 MLOps 管道,在不同的 AWS 服务上运行和编排步骤。
除了与大多数 AWS 服务(包括我们将为该管道利用的所有工具)集成之外,CodePipeline 还提供了与大多数源代码控制服务的连接器,这意味着您可以配置一个 webhook,允许它在推送到主分支之后或合并拉请求时(取决于您希望管道运行的频率)检索项目的新版本。
既然我们可以基于适合用例的事件来触发管道,我们就已经可以定义管道的不同步骤了:
- 当有我们想要触发管道的事件时,从我们的源代码控制服务中检索代码(这已经由 CodePipeline 在一个专用的源代码步骤中处理了)。
- 对数据运行预处理脚本(这是可选的,可以独立于此管道发生)。
- 使用最新版本的训练脚本来训练 ML 模型。
- 评估我们的新模型并测试其性能。
- 如果模型在性能方面满足一定的标准,则部署该模型。
在这个阶段,我们已经定义了 CodePipeline 中的不同步骤,现在让我们看看如何实现它们。
管道的当前版本:通过 AWS 代码管道定义的步骤,初始源步骤包括检索代码和数据
AWS 代码构建:您的瑞士军刀
我们模型的训练、评估和部署需要在一个包含所有必要包(无论是开源库还是定制的内部包)的环境中进行,这样我们不同的脚本就可以运行而不会出现任何问题或意外。
使用容器是对这种需求的直接回答,它允许我们为管道中的不同步骤高效地构建可预测的环境。幸运的是,AWS 提供了 CodeBuild,这是一种持续集成服务,我们可以利用它为我们的三个主要步骤中的每一个步骤推送 Docker 容器。
这种设计为我们的不同步骤提供了极大的灵活性,因为我们可以指定每个容器的包列表以及我们希望如何执行我们的脚本。CodeBuild 需要做的唯一额外工作是为每个作业提供一个 [buildspec](https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html)
脚本,其中我们指定了构建 Docker 映像要执行的命令。在buildspec
脚本中,我们还将图像推送到 ECR ,AWS 容器注册表。
现在,对于我们的三个主要步骤中的每一个,我们都有一个容器映像来表示执行该步骤的合适环境——但是我们可以在哪里运行这些容器呢?
管道的当前版本:CodeBuild 允许我们为管道的每一步构建一个专用的 Docker 映像
AWS SageMaker:奇迹发生的地方
SageMaker,Amazon 的通用 ML 平台,允许我们在 ML 优化的实例上运行不同的容器。SageMaker 还为我们的不同步骤提供专用组件:
- SageMaker 培训任务:它们允许我们无缝运行培训任务并生成模型。
- SageMaker 加工作业:非常适合模型评估任务。例如,我们可以使用测试数据集评估模型,然后在我们的跟踪工具上记录一组指标。
- SageMaker 端点:它们允许我们轻松地部署我们的服务模型。
多亏了这些工具,我们现在可以运行我们专用的容器映像来训练模型、评估它,然后部署它来提供服务。我们的 ML 难题中唯一剩下的部分是增加从 CodePipeline 触发这些不同的 SageMaker 组件的能力。
管道的当前版本:我们运行培训作业、模型评估,然后在 SageMaker 上部署
AWS Lambda:将点连接起来
Lambda 函数是 AWS 上使用最广泛的服务之一。这主要归功于它们的多功能性、简单性以及作为无服务器计算服务提供的便利性。
对于我们的 MLOps 管道,可以有效地利用 Lambda 函数来触发不同的 SageMaker 作业,在必要时执行附加操作,并在管道内将参数从一个步骤传递到另一个步骤。
例如,在评估步骤中,我们可以使用 Lambda 函数来执行以下操作:
- 检索由训练步骤产生的模型的位置(在 S3 上)。
- 为我们的评估环境检索完整的 Docker 图像名称和标签(图像将由 SageMaker 从 ECR 中提取)。
- 通过传递所有必要的参数,触发 SageMaker 处理作业来运行评估。
为了将变量从一个步骤传递到另一个步骤,我们可以通过 CodePipeline 本身定义想要传递的元数据,然后我们可以从不同的 Lambda 函数中检索变量。
最终的管道:我们使用 Lambda 函数来触发不同的 SageMaker 组件,依赖于通过 CodePipeline 传递的变量
结论
建立一个自动化的 MLOps 管道是工业化 ML 工作流程和确保模型及时到达生产的必要条件。在本文中,我们展示了 AWS 提供的 CI/CD 工具是构建这种管道的合适选择。
与开箱即用的解决方案相比,这种方法所需的额外工作是为极度灵活性(因为管道可以轻松修改或更新)和与其他 AWS 服务的自然集成(例如,我们可以在几分钟内通过 SNS 添加通知)所付出的代价。
流水线本身也应该通过 IaC(作为代码的基础设施)自动化,以确保可再现性和效率。文章中提到的所有资源都可以通过 Terraform 或 CloudFormation 创建。
最后,以下资源提供了关于该设计不同组件的更多见解:
- MLOps:机器学习中的连续交付和自动化管道
- AWS re:Invent 2018:使用 CI/CD 技术工业化机器学习
- 在 SageMaker 短暂实例上调度 Jupyter 笔记本
- 机器学习团队在生产中使用 CI/CD 的 4 种方式
要了解更多数据工程内容,您可以订阅我的双周刊时事通讯 Data Espresso,我将在其中讨论与数据工程和技术相关的各种主题:
https://dataespresso.substack.com/
Python 中的函数装饰器
为什么您可能需要它们以及如何实现一个定制的
约翰·安维克在 Unsplash上的照片
装饰者在第一次与它们打交道时可能会显得有点神秘,但毫无疑问,这是增强功能行为的一个很好的工具。
事实上,Python 中有两种类型的装饰器——类装饰器和函数装饰器——但我在这里将重点介绍函数装饰器。
在我们进入基本装饰器如何工作以及如何实现你自己的装饰器的有趣细节之前,让我们先看看为什么我们需要它们。在一些情况下,装饰者会有很大的帮助。
他们可以添加日志记录或工具代码,并以封装的方式从应用程序组件中提取各种指标,例如计时。
Decorators 可能是验证输入的完美机制,这在使用像 Python 这样的动态类型的语言时尤其重要。
内置装饰器在 Python 库中被广泛使用。典型的例子包括 web 框架中用于路由和授权的 decorator,例如, Flask 中的[@route()](https://flask.palletsprojects.com/en/2.0.x/api/#flask.Flask.route)
decorator。
既然我们看到了学习编写和应用装饰器是一个好主意,那么让我们弄清楚什么是装饰器,以及如果需要的话,如何实现自定义装饰器。
为了使用 decorators,你需要知道的基本知识是,函数也是 Python 中的对象,它们可以作为参数传递给其他函数,就像其他对象一样,比如字符串。所以 Python 中的函数是一级对象。
以其他函数为自变量返回一个函数的函数是 高阶函数 。这就是 Python decorators 的情况。
一个函数可以在另一个函数中定义。在这种情况下,它被称为嵌套函数。另一个与嵌套函数直接相关并且你需要理解的概念是 闭包——一个“记住”数据的函数对象,例如来自其封闭范围的变量及其值。这将是我们稍后将看到的装饰器示例中的wrapper()
函数。
装饰器通常被定义为一个修改另一个函数行为的函数。最常见的情况是,装饰者给参数函数的行为添加了一些东西。重要的是要记住,装饰器只是在一定程度上改变被装饰函数的行为,而不是永久地或完全地改变它。这就是为什么有时人们更喜欢将装饰器定义为一个函数,这个函数扩展了另一个更准确的函数的行为。
接下来,让我们看一个装饰函数的例子。假设我们有一个简单的函数,它返回两个整数的和:
def sum_up(n, m):
return n + m
那么我们可以这样运行它:
print(sum_up(3, 7))
输出:
10
现在,假设我们想要记录代码库的这一部分发生了什么——为了便于练习,我们将简单地把它打印到控制台上。我们可以为此编写一个装饰器:
现在我们需要将装饰器添加到代码库中的函数中:
[@log_computation](http://twitter.com/log_computation)
def sum_up(n, m):
return n + m
因此,当我们在代码中调用此函数时,我们将在控制台中看到以下输出:
Adding up 3 and 7\. The result equals to 10.
让我们一行一行地分解它,以理解装饰器中发生了什么。
在第 1 行,decorator 方法的签名显示它接受一个函数作为参数。在第 2 行,我们定义了一个嵌套的包装函数,它将调用修饰函数并在第 6 行返回它的输出。在第 3 行,我们获得了修饰函数的输出。在第 4 行和第 5 行,装饰者的修改行为开始生效:在这里,它使输出更加冗长。在第 7 行,装饰者返回包装器。
如果我们不知道修饰函数的参数的很多细节怎么办?如果我们想概括装饰者呢?
我们可以使用 *[args](https://docs.python.org/3.7/glossary.html?highlight=kwarg#term-parameter)
和/或**[kwargs](https://docs.python.org/3.7/glossary.html?highlight=kwarg#term-parameter)
来实现:
def log_computation(func):
def wrapper(*args):
numbers = args
res = func(*args)
print('Adding up the following numbers:\n'
'{}\n'
'The result equals to {}.'.format(numbers, res))
return res
return wrapper
因此,如果将sum_up()
函数推广到累加任意数量的整数,我们的装饰器仍然可以工作:
sum_up(3, 7, 11, 50)
输出:
Adding up the following numbers:
(3, 7, 11, 50)
The result equals to 71.
这是一个自定义装饰的基本模板。自定义装饰器可能要复杂得多。嵌套函数可以不接受参数,也可以接受位置参数、关键字参数或两者都接受。他们可以在调用 main 函数之前完成一些步骤,以确保它可以被安全地调用,或者在调用之后完成(就像我们的例子一样),或者两者都完成(例如,在记录函数的执行或计时时)。我们也可以对一个函数应用多个装饰器——这被称为链接装饰器。
Decorators 是使您的代码更健壮(特别是如果您将它们用于验证目的的话)并且更简单、更通用的好帮手。
如果你对使用 decorators 来跟踪你的 Python 代码的性能感兴趣,可以随意查看我关于代码优化和并发和并行的帖子。
人脑的功能连接和相似性分析(下)
人脑的空间分析
材料
这是该系列的第三篇文章,即“腹侧颞叶皮层时空功能磁共振成像的认知计算模型”。如果你想了解整个系列,请点击下面的链接。
我将介绍功能连接和相似性分析的主题,它们在大脑解码研究中的用例。让我们开始吧。
所有相关资料都放在我的 GitHub 页面上。别忘了去看看。如果你是一个纸质爱好者,你可以阅读这一系列文章的纸质版,也可以在我的回购中找到。
在深入分析和编码之前,让我们稍微操作一下 Haxby 数据集,并应用腹侧时间掩模从人脑的感兴趣区域提取信号。安装和导入部分在前一篇文章中已经完成。如果你没有看我以前的文章,我想就此打住,快速浏览一下。但是,不用担心。所有部分“几乎”相互独立。开始编码吧。
在这里,我们获取 Haxby 数据集。接下来,我们将理解数据结构,并将其转换为 NumPy 数组,以便进一步处理。
正如我们所看到的,有 8 个类别。(我们将忽略“rest”类别,因为它没有提供额外的信息。)
让我们删除“休息”条件数据,并探索数据的形状如下。
因此,有 864 个样本在时间上相连,即时间序列数据。固定时间的 fMRI 数据具有 40×64×64 的维度,其中 40 是指 3-D 图像的深度,64 是指空间维度。因此,我们是四维时间序列图像数据。回想一下,实验中有 6 名受试者,让我们按如下方式查看所有受试者。
所以,所有受试者都有 864 个时间序列数据。有关数据集的详细描述,请参考第一部分。
然后,让我们执行掩蔽来提取感兴趣的区域,以降低 fMRI 的维度。掩蔽的 fMRI 样本代表神经活动可能发生的区域。
是的,这个脚本使我们能够
- 获取 fMRI 数据并将其转换为 NumPy 矩阵
- 创建并应用时空掩膜来提取感兴趣区域
- 准备监督(目标/标签)
那么,让我们运行这个函数并获取数据。
是啊!最后,我们准备了数据集。现在,我们可以进行任何我们想要的分析。在本文中,我们将对人脑进行功能连接和相似性分析。
功能连接和相似性分析
功能连接性被定义为解剖学上分离的大脑区域的神经元激活模式的时间依赖性,在过去的几年中,越来越多的神经成像研究开始通过测量大脑区域之间静息状态 fMRI 时间序列的共激活水平来探索功能连接性[23]。这些功能联系对于建立大脑区域的统计联系非常重要。可以通过估计来自分解的不同大脑区域的信号的协方差(或相关性)矩阵来获得功能连接,例如在静息状态或自然刺激数据集上。这里,我们基于相关性、精确度和部分相关性进行了功能连接性分析。然后,进行基于余弦、闵可夫斯基和欧几里德距离的相似性分析,以进一步扩展掩蔽的 fMRI 数据中的统计发现。
功能连接:关联
对受试者 1 进行基于皮尔逊相关性的功能连接。我们可以看到,在受试者 1 的腹侧颞叶皮层中,当呈现面孔的刺激时,存在很强的相关性。
相关矩阵(图片由作者提供)
功能连接:精度
如文献[20,24]所示,使用逆协方差矩阵,即精度矩阵更有意义。它只给出区域之间的直接联系,因为它包含部分协方差,即两个区域之间的协方差取决于所有其他区域。此外,我们基于精确评分进行了功能性连接组,以提取受试者 1 的 RoI 信号。这里,随着连接性测量的改变,我们看到受试者 1 的腹侧皮层中空间相关性的直接变化。通过精确测量,我们进一步了解了大脑组织和大脑网络。
精度矩阵(图片由作者提供)
功能连接:部分相关
在一系列网络建模方法中,偏相关在准确检测真实的大脑网络连接方面显示出巨大的前景[25]。因此,我们基于偏相关进行了功能连接性分析。RoI fMRI 数据中的部分相关性的可视化表明受试者 1 的腹侧颞叶皮层没有太多相关性。
部分相关矩阵(图片由作者提供)
相似性分析:余弦距离
为了便于在大脑中统计连接的背景下进行测地线理解,我们对受试者 1 进行了余弦相似性分析,并将获得的矩阵可视化。结果表明,当视觉刺激出现时,在神经活动方面存在高度重叠的区域。
余弦矩阵(图片作者提供)
相似性分析:闵可夫斯基距离
为了试验不同的相似性度量,我们利用了闵可夫斯基距离,它是欧几里得距离和曼哈顿距离的推广。因此,它在 fMRI 时间相似性分析中是有用的。
闵可夫斯基矩阵(图片作者提供)
相似性分析:欧几里德距离
最后,我们基于经典的欧氏距离进行相似性分析。这是一个非常经典的使用勾股定理[13]根据点的笛卡尔坐标测量距离的方法。从功能连接和相似性分析所揭示的统计和结构模式中,我们可以得出结论,在人脑腹侧颞叶皮层诱发的神经活动是高度重叠和分布的。
欧几里德矩阵(图片由作者提供)
耶!本文到此为止。我深入讨论了 fMRI 数据的功能连接和相似性分析技术。
恭喜你!您已经完成了第三篇文章,并通过认知计算方法对人脑解码迈出了一步。
在下一篇文章中,我们将执行无监督表示学习,以提取人脑中的潜伏期。
文章链接
- 发表文章
https://cankocagil.medium.com/discovery-neuroimaging-analysis-part-ii-b2cdbdc6e6c3
2.在路上(即将到来…)
- 第五部分的占位符
进一步阅读
- 【https://www.hindawi.com/journals/cmmm/2012/961257/
我在机器学习和神经科学方面的研究中使用了以下参考文献列表。我强烈建议复制粘贴参考资料,并简要回顾一下。
参考
[1]巴、基罗斯和辛顿。图层归一化,2016。
[2] L. Buitinck,G. Louppe,M. Blondel,F. Pedregosa,A. Mueller,O. Grisel,V. Niculae,P. Prettenhofer,A. Gramfort,J. Grobler,R. Layton,J. VanderPlas,a .乔利,B. Holt,10 和 G. Varoquaux。机器学习软件的 API 设计:scikit-learn 项目的经验。在 ECML PKDD 研讨会:数据挖掘和机器学习的语言,第 108–122 页,2013。
[3]褚,田,王,张,任,魏,夏,沈。双胞胎:重新审视《视觉变形金刚》中空间注意力的设计,2021。
[4] K .克拉默、o .德克、j .凯舍特、s .沙莱夫-施瓦兹和 y .辛格。在线被动攻击算法。2006.
[5] K. J .弗里斯顿。统计参数映射。1994.
[6]格罗斯、罗查-米兰达和本德。猕猴下颞皮质神经元的视觉特性。神经生理学杂志,35(1):96–111,1972。
[7] S. J .汉森、t .松坂和 J. V .哈克斯比。用于物体识别的腹侧颞叶组合编码。
[8]哈克斯比、戈比尼、富里、伊沙伊、斯豪滕和彼得里尼。《视觉物体识别》,2018。
[9]赫克曼、哈伊纳尔、贾巴尔、吕克特和哈默斯。结合标记传播和决策融合的自动解剖脑 mri 分割。神经影像,33(1):115–126,2006。
10d .亨德里克斯和 k .金佩尔。高斯误差线性单位(gelus),2020。
[11]黄少华,邵文伟,王明林,张德庆.人脑活动视觉信息的功能解码:简要综述。国际自动化和计算杂志,第 1-15 页,2021。
[12] R. Koster、M. J. Chadwick、Y. Chen、D. Berron、A. Banino、E. Duzel、D. Hassabis 和 D. Kumaran。海马系统内的大循环复发支持跨发作的信息整合。神经元,99(6):1342–1354,2018。
[13]马奥尔。勾股定理:4000 年的历史。普林斯顿大学出版社,2019。
[14] K. A. Norman、S. M. Polyn、G. J. Detre 和 J. V. Haxby 超越读心术:功能磁共振成像数据的多体素模式分析。认知科学趋势,10(9):424–430,2006。
[15]奥图尔、江、阿卜迪和哈克斯比。腹侧颞叶皮层中物体和面孔的部分分布表征。认知神经科学杂志,17(4):580–590,2005。
[16] F .佩德雷戈萨、g .瓦洛夸、a .格拉姆福特、v .米歇尔、b .蒂里翁、o .格里塞尔、m .布隆德尔、p .普雷登霍弗、r .魏斯、v .杜伯格、j .范德普拉斯、a .帕索斯、d .库尔纳波、m .布鲁彻、m .佩罗特和 e .杜切斯内。sci kit-learn:Python 中的机器学习。机器学习研究杂志,12:2825–2830,2011。
17 r . a .波尔德拉克。功能磁共振成像的感兴趣区域分析。社会认知和情感神经科学,2(1):67–70,2007。
[18] M. Poustchi-Amin、S. A. Mirowitz、J. J. Brown、R. C. McKinstry 和 T. Li。回波平面成像的原理和应用:普通放射科医师回顾。放射学,21(3):767–779,2001。
[19] R. P. Reddy,A. R. Mathulla 和 J. Rajeswaran。心理健康专家的观点采择和情绪传染的初步研究:移情的玻璃脑观点。印度心理医学杂志,0253717620973380,2021 页。
[20]史密斯、米勒、萨利米-科尔希迪、韦伯斯特、贝克曼、尼科尔斯、拉姆齐和伍尔利奇。功能磁共振成像的网络建模方法。神经影像,54(2):875–891,2011。
21 田中先生。下颞叶皮层和物体视觉。神经科学年度评论,19(1):109–139,1996。
[22] M. S .特雷德。Mvpa-light:一个多维数据的分类和回归工具箱。神经科学前沿,14:289,2020。
[23] M. P .范登赫维尔和 H. E .波尔。探索大脑网络:静息态功能磁共振成像功能连接综述。欧洲神经精神药理学,20(8):519–534,2010。
[24] G. Varoquaux,A. Gramfort,J. B. Poline 和 B. Thirion。大脑协方差选择:使用群体先验的更好的个体功能连接模型。arXiv 预印本 arXiv:1008.5071,2010。
[25] Y. Wang,J. Kang,P. B. Kemmer 和 Y. Guo。一种利用偏相关估计大规模脑网络功能连接的有效可靠的统计方法。神经科学前沿,10:123,2016。
26s . Wold、K. Esbensen 和 P. Geladi。主成分分析。化学计量学和智能实验室系统,2(1–3):37–52,1987。
27s . Wold、K. Esbensen 和 P. Geladi。主成分分析。化学计量学和智能实验室系统,2(1–3):37–52,1987。
功能“控制流”——编写没有循环的程序
小窍门
控制流的函数式编程特性概述—没有循环和 if-else
概述
在我上一篇关于函数式编程的关键原则的文章中,我解释了函数式编程范例与命令式编程的不同之处,并讨论了幂等性和避免副作用的概念是如何与函数式编程中支持等式推理的引用透明性联系起来的。
在我们深入了解函数式编程的一些特性之前,让我们从我写 Scala 代码的前三个月的个人轶事开始。
函数代码中没有“如果-否则”
我正在为一个定制的 Spark UDF 编写一个纯 Scala 函数,它基于一个用 JSON 字符串表示的定制分级调整来计算收入调整。当我试图用纯功能代码来表达业务逻辑时(因为这是团队的编码风格),我对我感觉到的生产力下降感到非常沮丧,以至于我在代码中引入了“if-else”逻辑,试图“完成工作”。
这么说吧,我在对那个特定的合并请求进行代码审查的过程中学到了一个非常艰难的教训。
“函数代码中没有 if-else,这不是命令式编程… 没有 if,就没有 else。
没有“if-else”,我们如何在函数式编程中写出“控制流”?
简答:功能构成。
最长的答案是:函数组合和函数数据结构的结合。
由于对每个功能设计模式的深入探究可能相当冗长,本文的重点是提供一个函数组合的概述,以及它如何实现一个更直观的方法来设计数据管道。
函数合成简介
功能构成(图片由作者提供)
在数学中,函数合成是将两个函数 f 和 g 依次取值,形成一个复合函数 h ,使得h(x)= g(f(x)】—函数 g 应用于将函数 f 应用于一个泛型输入 x 的结果。在数学上,该操作可以表示为:
f : X → Y,g:y→z⟹g♀f:x→z
其中g♀f是复合函数。
直观地说,对于域 X 中的所有值,复合函数将域 Z 中的XX 映射到域 g(f(x)) 。
用一个有用的类比来说明函数组合的概念,就是用一片面包和冷黄油在烤箱里做黄油吐司。有两种可能的操作:
- 在烤箱中烘烤(操作 f)
- 将黄油涂在最宽的表面上(操作 g)
如果我们先在烤箱里烤面包,然后把冷黄油涂在从烤箱里出来的面包的最宽表面上,我们会得到一片涂有冷黄油的烤面包。
如果我们先将冷黄油涂在面包最宽的表面上,然后在烤箱中烘烤涂有冷黄油的面包,我们会得到一片涂有热黄油的烤面包*(f♀g)。而且我们知道*“冷黄油涂抹”!= “温黄油涂抹”。
从这些例子中,我们可以直观地推断出函数应用的顺序在函数合成中很重要。(g♀f≠f♀g)
类似地,在设计数据管道时,我们经常通过将函数应用于其他函数的结果来编写数据转换。组合函数的能力鼓励将重复的代码段重构成函数,以实现可维护性和可重用性。
充当一级对象
函数式编程中的核心思想是:函数就是值。
这个特征意味着一个函数可以是[2,3]:
- 赋给变量
- 作为参数传递给其他函数
- 作为其他函数的值返回
为此,函数必须是运行时环境中的一级对象(并存储在数据结构中)——就像数字、字符串和数组一样。包括 Scala 在内的所有函数式语言以及 Python 等一些解释型语言都支持一级函数。
高阶函数
函数作为一级对象的概念所产生的一个关键含义是,函数组合可以自然地表达为一个高阶函数。
高阶函数至少具有下列特性之一:
- 接受函数作为参数
- 将函数作为值返回
高阶函数的一个例子是map
。
当我们查看 Python 内置函数map
的文档时,发现map
函数接受另一个函数和一个 iterable 作为输入参数,并返回一个产生结果的迭代器[4]。
在 Scala 中,包scala.collections
中的每个集合类及其子集都包含由 ScalaDoc [5]上的以下函数签名定义的map
方法:
*def map[B](f: (A) => B): Iterable[B] // for collection classes
def map[B](f: (A) => B): Iterator[B] // for iterators that access elements of a collection*
函数签名的意思是map
接受一个函数输入参数f
,而f
将一个A
类型的通用输入转换成一个B
类型的结果值。
为了对整数集合中的每个值求平方,迭代方法是遍历集合中的每个元素,对元素求平方,并将结果附加到随着每次迭代而长度扩展的结果集合中。
- 在 Python 中:
*def square(x):
return x * x
def main(args): collection = [1,2,3,4,5]
# initialize list to hold results
squared_collection = []
# loop till the end of the collection
for num in collection:
# square the current number
squared = square(num)
# add the result to list
squared_collection.append(squared)
print(squared_collection)*
在迭代方法中,循环中的每次迭代都会发生两种状态变化:
- 保存从
square
函数返回的结果的squared
变量;和 - 保存 square 函数结果的集合。
为了使用函数方法执行相同的操作(即不使用可变变量),可以使用map
函数将集合中的每个元素“映射”到一个新集合,该新集合具有与输入集合相同数量的元素——通过对每个元素应用平方运算并将结果收集到新集合中。
- 在 Python 中:
*def square(x):
return x * x
def main(args):
collection = [1,2,3,4,5]
squared = list(map(square, collection))
print(squared)*
- 在 Scala 中:
*object MapSquare {
def square(x: Int): Int = {
x * x
}
def main(args: Array[String]) {
val collection = List[1,2,3,4,5]
val squared = collection.map(square)
println(squared)
}
}*
在这两个实现中,map
函数接受应用于值集合中每个元素的输入函数,并返回包含结果的新集合。由于map
具有接受另一个函数作为参数的属性,所以它是一个高阶函数。
关于 Python 和 Scala 实现之间的差异,有一些简短的补充说明:
- Python
map
vs Scalamap
:需要一个像list
这样的可迭代函数将 Pythonmap
函数返回的迭代器转换成可迭代函数。在 Scala 中,不需要将来自map
函数的结果显式转换为 iterable,因为Iterable
trait 中的所有方法都是根据抽象方法iterator
定义的,抽象方法iterator
返回Iterator
trait 的一个实例,该实例一个接一个地产生集合的元素[6]。 - 如何从函数中返回值:虽然在 Python 中使用了
return
关键字来返回函数结果,但是在 Scala 中很少使用return
关键字。相反,在 Scala 中定义函数时,会计算函数声明中的最后一行,并返回结果值。事实上,在 Scala 中使用return
关键字对于函数式编程来说并不是一个好的实践,因为它放弃了当前的计算,并且不是引用透明的[7-8]。
匿名函数
当使用高阶函数时,能够用函数文字或匿名函数调用输入函数参数通常是方便的,而不必在它们可以在高阶函数中使用之前将其定义为命名函数对象。
在 Python 中,匿名函数也称为 lambda 表达式,因为它们源于 lambda 演算。一个匿名函数是用关键字lambda
创建的,它不使用关键字def
或return
包装一个表达式。例如,Python 中前面示例中的square
函数可以表示为map
函数中的匿名函数,其中 lambda 表达式lambda x: x * x
用作map
的函数输入参数:
*def main(args):
collection = [1,2,3,4,5]squared = map(lambda x: x * x, collection)
print(squared)*
在 Scala 中,匿名函数是按照=>
符号定义的——其中函数参数定义在=>
箭头的左边,函数表达式定义在=>
箭头的右边。例如,Scala 中前面示例中的square
函数可以用(x: Int) => x * x
语法表示为匿名函数,并用作map
的函数输入参数:
*object MapSquareAnonymous {
def main(args: Array[String]) {
val collection = List[1,2,3,4,5]
val squared = collection.map((x: Int) => x * x)
println(squared)
}
}*
在高阶函数中使用匿名函数的一个关键好处是,单次使用的单表达式函数不需要显式包装在命名函数定义中,因此优化了代码行和提高了代码的可维护性。
递归作为“函数迭代”的一种形式
递归是自引用函数组合*的一种形式——递归函数获取自身(较小实例)的结果,并将它们作为自身另一个实例的输入。为了防止递归调用的无限循环,需要一个基本用例作为终止条件,以便在不使用递归的情况下返回结果。*
递归的一个经典例子是阶乘函数,它被定义为所有小于或等于整数 n 的正整数的乘积:
n!= n ⋅ (n-1) ⋅ (n-2)⋅⋯⋅3⋅2⋅1
实现阶乘函数有两种可能的迭代方法:使用for
循环和使用while
循环。
- 在 Python 中:
*def factorial_for(n):
# initialize variable to hold factorial
fact = 1
# loop from n to 1 in decrements of 1
for num in range(n, 1, -1):
# multiply current number with the current product
fact = fact * num
return fact
def factorial_while(n):
# initialize variable to hold factorial
fact = 1
# loop till n reaches 1
while n >= 1:
# multiply current number with the current product
fact = fact * n
# subtract the number by 1
n = n - 1
return fact*
在阶乘函数的两种迭代实现中,循环中的每次迭代都会发生两种状态变化:
- 存储当前产品的阶乘变量;和
- 被相乘的数字。
为了使用函数方法实现阶乘函数,递归在将问题分成相同类型的子问题时非常有用——在本例中,子问题是 n 和 (n-1)的乘积!。
阶乘函数的基本递归方法如下所示:
- 在 Python 中:
*def factorial(n):
# base case to return value
if n <= 0: return 1
# recursive function call with another set of inputs
return n * factorial(n-1)*
- 在 Scala 中:
*def factorial(n: Int): Long = {
if (n <= 0) 1 else n * factorial(n-1)
}*
对于基本递归方法,5 的阶乘按以下方式计算:
*factorial(5)
if (5 <= 0) 1 else 5 * factorial(5 - 1)
5 * factorial(4) // factorial(5) is added to call stack
5 * (4 * factorial(3)) // factorial(4) is added to call stack
5 * (4 * (3 * factorial(2))) // factorial(3) is added to call stack
5 * (4 * (3 * (2 * factorial(1)))) // factorial(2) is added to call stack
5 * (4 * (3 * (2 * (1 * factorial(0))))) // factorial(1) is added to call stack
5 * (4 * (3 * (2 * (1 * 1)))) // factorial(0) returns 1 to factorial(1)
5 * (4 * (3 * (2 * 1))) // factorial(1) return 1 * factorial(0) = 1 to factorial(2)
5 * (4 * (3 * 2)) // factorial(2) return 2 * factorial(1) = 2 to factorial(3)
5 * (4 * 6) // factorial(3) return 3 * factorial(2) = 6 to factorial(4)
5 * 24 // factorial(4) returns 4 * factorial(3) = 24 to factorial(5)
120 // factorial(5) returns 5 * factorial(4) = 120 to global execution context*
对于 n = 5 ,阶乘函数的评估涉及对阶乘函数的 6 次递归调用,包括基本情况。
虽然与迭代方法相比,基本递归方法更接近于阶乘函数的定义(也更自然地)来表达阶乘函数,但它也使用更多的内存,因为每个函数调用都作为堆栈帧被推送到调用堆栈,并在函数调用返回值时从调用堆栈中弹出。
对于更大的 n 值,递归会随着对自身的更多函数调用而变得更深,并且更多的空间必须分配给调用栈。当存储函数调用所需的空间超过调用堆栈的容量时,就会发生堆栈溢出!
尾部递归和尾部调用优化
为了防止无限递归导致堆栈溢出和程序崩溃,必须对递归函数进行一些优化,以减少调用堆栈中堆栈帧的消耗。优化递归函数的一种可能方法是将其重写为一个尾递归函数。
尾部递归函数递归调用自身,并且在递归调用返回后不执行任何计算。当一个函数调用除了返回函数调用的值之外什么也不做时,它就是一个尾调用。
在 Scala 等函数式编程语言中,尾调用优化通常包含在编译器中,以识别尾调用,并将递归编译为迭代循环,每次迭代都不会消耗堆栈帧。事实上,堆栈帧可以被递归函数和递归函数中被调用的函数重用[1]。
通过这种优化,递归函数的空间性能可以从 O(N) 减少到O(1)——从每次调用一个堆栈帧减少到所有调用一个堆栈帧[8]。在某种程度上,尾部递归函数是“函数迭代”的一种形式,其性能与循环相当。
例如,阶乘函数可以在 Scala 中以尾部递归的形式表示:
*def factorialTailRec(n: Int): Long = {
def fact(n: Int, product: Long): Long = {
if (n <= 0) product
else fact(n-1, n * product)
}
fact(n, 1)
}*
虽然在 Scala 中尾调用优化是在编译期间自动执行的,但 Python 却不是这样。此外,Python 中有一个递归限制(缺省值为 1000),作为防止 CPython 实现的 C 调用堆栈溢出的措施。
接下来是什么:高阶函数
在本帖中,我们将了解:
- 功能组成
- 高阶函数是函数式编程的关键含义
- 递归作为“函数迭代”的一种形式
我们找到“如果-否则”的替代词了吗?不完全是,但是我们现在知道如何使用高阶函数和尾部递归在函数式编程中编写“循环”。
在下一篇文章中,我将更多地探讨高阶函数,以及如何将它们用于设计函数式数据管道。
想要更多关于我作为数据专业人员的学习历程的幕后文章吗?查看我的网站 https://ongchinhwee.me !
参考
[1]保罗·丘萨诺和罗纳·比雅纳松,Scala 中的函数式编程 (2014)
[2]阿尔文·亚历山大,《函数也是变量》 (2018),函数式编程简化版
[3] Steven F. Lott,函数式 Python 编程(第二版 ) (2018)
[5] Scala 标准库 2 . 13 . 6—Scala . collections . iterable
[6]Trait Iterable | Collections | Scala 文档
【8】Scala 中不使用 Return?—问题— Scala 用户
[9] Michael R. Clarkson,尾部递归 (2021),OCaml 中的函数式编程
原载于 2021 年 7 月 4 日https://ongchinhwee . me*。*
Python 中的功能建模和定量系统分析
实践教程
使系统框图可执行的编码模式
本文介绍了一种实用的编码模式来对系统进行建模和分析。这由一个的现实例子来说明。该示例是完整开发的,包括完整代码清单。这旨在成为系统架构师和工程师的快速入门指南。
介绍
功能建模是一种建立工程系统模型的技术。模型化的系统被视为一组相互关联的功能。
每个功能都将输入转换为输出,其行为可能取决于参数。在图形上,这通常由功能框图来描述。
为了定量分析这种功能框图,需要一种可执行的数学表示。Python 非常适合这项任务。不仅仅是编码和执行数学模型,而是实际执行分析,绘制结果,并通过可复制的计算环境共享工作。本文介绍了 Python 中的一种编码模式,以简明地对功能框图建模,作为定量系统分析的一部分。编码模式应用于简单的工程分析环境中:信号转换链中的误差源分析。
功能链的示例
我们仅限于没有反馈回路的单向功能链的情况。反馈循环引入了需要扩展模式的动态效果——这将在后续文章中讨论。考虑下面说明性的功能框图。
说明性功能框图(图片由作者提供)。
它显示了从极坐标( θi,A )到笛卡尔坐标 (u,v) 以及反过来的变换链。需要注意的是,这个链中有一些错误源,用红色突出显示。为了举例,假设分析旨在量化由各种误差源产生的最终角度误差。这个例子的灵感来自于作者作为磁传感器架构师的职业经历[1]。基本原则是通用的。
假设极坐标到笛卡尔坐标的变换存在偏移。这些是一次分析运行的固定参数。进一步假设笛卡尔坐标被噪声污染,随机样本从具有给定标准偏差(另一个固定参数)的正态分布中抽取。最后一个功能块简单地用 2 参数反正切值重新计算输出角度(Numpy 中的 np.atan2 )。直观上,如果偏移和噪声相对于振幅 A 相对较小,则角度误差 θe 应该较小。
Python 中的函数定义
对于每个功能块,我们定义一个将输出与输入相关联的数学函数: *Y= f(X,P)。*通常,输入或输出可能是多维变量,因此使用大写符号。在功能输入中,我们区分在块之间流动的变量信号 X ,以及由设计或环境条件设置的具有固定值的参数 P 。对于给定的分析运行,这些参数是恒定的。Python 中有偏移误差的极坐标到笛卡尔坐标变换的直接对应函数定义如下:
def f(A, theta_i, offset_u, offset_v):
return (A * np.array([np.cos(theta_i), np.sin(thetat_i)])
+ np.array([offset_u, offset_v]))
注意,上面的定义没有区分可变输入和固定参数。它还返回匿名输出:这两个输出被简单地分组在一个数组中,没有标记。这种编码方案是一个很好的起点,但缺乏模块化。
为了模块化,我们希望能够链接功能块,而不管它们的功能签名。为了获得统一的函数签名并允许链接,我们引入以下编码模式。
- 首先,我们将所有固定参数分组到一个公共数据字典[1] dd 中。这使得确定用于分析运行的确切参数集变得更加容易。为了方便起见,我们将这个数据字典打包成一个 Pandas 系列,以允许使用更短的点符号快速访问底层元素: dd.parameter_1 (与 Python 中更冗长的 dd[‘parameter_1’] 相比)。
- 其次,我们还将变量输入分组到另一个系列 X 。
- 最后,输出也用命名标签分组。
按照这种约定,函数签名的形式总是:*Y = f(X,dd)。*这与输入和输出的数量无关。以下是 polar2cart 的改编定义:
dd=pd.Series({
'offset_u' : 0.01,
'offset_v' : 0.0,
'noise_std': 0.01,
})def polar2cart(X, dd):
return ({
'u': X.A * np.cos(X.theta_i) + dd.offset_u,
'v': X.A * np.sin(X.theta_i) + dd.offset_v,
})
可以按如下方式调用:
polar2cart(pd.Series({'A': 1, 'theta_i': 0}), dd=dd)
在现阶段,人们可能想知道上述公约有什么好处。好像挺啰嗦的。
所提出的模式的主要优点是易于扩展到表格数据集,在 Pandas 中称为数据框架。数据帧是用离散时间变化信号进行的完整模拟运行的简明表示,其中一行表示在给定时刻所有信号值的一个快照。
编码信号波形的数据帧(图片由作者提供)。
让我们从主要输入(模拟的刺激)开始:输入角度 θi 和振幅 A 。让我们针对两个幅度 A =1 和 A =2,生成对应于完整角度扫描(从 0 到 2π)的数据帧。
import itertools
itertools.product([1,2], [3])
df = pd.DataFrame(itertools.product(
np.arange(0,2*np.pi,np.pi/100),
[1,2]),
columns=['theta_i', 'A'])
display(df)
我们现在可以包装我们之前的函数(在 Python 中这被称为“修饰”函数),这样它就可以处理数据框输入和输出。
这可以通过将函数应用于每一行,并连接输出和输入数据帧来实现。使用这种编码模式,在输入变量的表格数据集上执行任何函数 f 只需调用: df=f(df,dd) 。
下面是 polar2cart 函数的定义,这次它被包装成数据帧友好的,并调用它。
def apply_func_df(func):
def wrapper(df, dd):
Y = df.apply(func, dd=dd, axis=1, result_type='expand')
return df.drop(columns=Y.columns, errors='ignore').join(Y)
wrapper.__wrapped__ = func
return wrapper[@apply_func_df](http://twitter.com/apply_func_df)
def polar2cart(X, dd):
return {
'u': X.A * np.cos(X.theta_i) + dd.offset_u,
'v': X.A * np.sin(X.theta_i) + dd.offset_v,
}df=polar2cart(df, dd)
display(df)
对数据帧的操作
由于所有变量都在一个数据框中,我们可以利用 Pandas 库的广泛功能进行快速内联操作。例如,可以很容易地应用移动平均:
df[['u_avg', 'v_avg']]=df[['u', 'v']].rolling(2, center=True).mean()
像这样的简单操作可以直接在完整的数据帧上执行,而不需要调用我们的装饰器。这是因为 Pandas 内置的广播机制,默认情况下,它将算术运算扩展到整列。例如,要添加噪声,我们可以定义下面的函数,绕过我们的装饰器:
def add_noise(df, dd):
df[['u', 'v']] += np.random.randn(len(df),2)*dd.noise_std
return df
df=add_noise(df, dd)
对于复杂的操作(例如,控制逻辑,或者不同的输出),我们提出的编码模式仍然有效:定义一个初等函数并用我们的装饰器包装它。
对链条的其余部分进行建模
回到功能链,我们仍然需要对角度计算块进行建模。我们还可以计算考虑相位缠绕的角度误差(-π = +π,以角度表示)。使用我们的编码模式,我们将两个额外的函数 calc_angle 和 calc_angle_err 定义如下:
[@apply_func_df](http://twitter.com/apply_func_df)
def calc_angle(X, dd):
return {
'theta_o': np.arctan2(X.v, X.u)
}[@apply_func_df](http://twitter.com/apply_func_df)
def calc_angle_err(X, dd):
e = X.theta_o — X.theta_i
# account for phase wrapping
if e > np.pi:
e-=2*np.pi
elif e < -np.pi:
e+=2*np.pi
return {
'theta_err': e
}
最后的修饰操作可能是将弧度转换为角度,以便更容易地解释绘图。因为这是单个 Numpy 操作,所以我们可以直接对数据框列进行操作。我们在名称包含 *theta_* 的所有列上就地操作。
def convert_to_deg(df, dd):
df[df.filter(like=‘theta_’).columns]=np.degrees(
df[df.filter(like=‘theta_’).columns])
return df
# 完整管道
所有功能块的功能都已定义,并在需要时进行包装,以使它们都与数据帧兼容。我们现在可以使用 pipe [3]操作符将所有操作链接起来,如下所示:
df=(df
.pipe(f1, dd)
.pipe(f2, dd))
完整的工作流程如下图所示。
* (a)我们从单向信号流的功能模型开始。
* (b)我们用 python 函数 *Y=f(X,dd)* 对每个功能块建模。如果需要的话,我们包装了这个函数,这样包装的函数就可以直接在数据帧上操作。
* (c)我们用主要输出填充数据框。
* (d)我们按照与信号流相同的顺序依次调用管道中的每个函数。这将逐步扩展数据帧。

Python 中函数建模的工作流(图片由作者提供)。
汇总统计也很容易计算,因为我们在单个数据帧中有所有信号值。下面我们展示了如何通过提取两个不同振幅的均方根(RMS)误差,在数据透视表中轻松总结结果。我们最后通过调用 *df.plot(…)* 或 *df.hvplot* 来绘制关键曲线,这是一个带有 Holoviews 的交互版本【4】。

总结分析的交互式绘图和表格(图片由作者提供)。
附录中提供了生成上述图的完整代码清单。
# 结论
我们提出了一个通用的编码模式,适合于用 Python 实现一个单向框图的可执行模型。该模式基于初等函数,这些初等函数直接作用于表格数据集,如 Pandas 数据框。然后简单地通过从刺激开始链接这些操作来调用端到端模拟。我们在定量系统分析中演示了这种编码模式,以说明其优势。主要优势是:
1. 我们利用 Pandas 内置的大量数据框操作(切片、过滤、查询、数据透视表、绘图……)。
2. 我们获得了可直接追溯到框图的模块化可执行模型。
# 参考
[1] N. Dupré、Y. Bidaux、O. Dubrulle 和 G. Close,“具有 1%精度的杂散磁场免疫磁位移传感器”,IEEE Sens. J,2020 年 5 月。可用:[https://doi.org/10.1109/JSEN.2020.2998289](https://doi.org/10.1109/JSEN.2020.2998289)
[2] Mathworks,“什么是数据字典?”。Mathworks 帮助中心。可用:[https://ch . mathworks . com/help/Simulink/ug/what-is-a-data-dictionary . html](https://ch.mathworks.com/help/simulink/ug/what-is-a-data-dictionary.html)。访问日期:2021 年 1 月 24 日。
[3] B. Chen,“使用 Pandas 方法链接提高代码可读性”,走向数据科学,2020 年 8 月。可用:[https://towards data science . com/using-pandas-method-chaining-to-improve-code-readability-d 8517 c 5626 AC](/using-pandas-method-chaining-to-improve-code-readability-d8517c5626ac)
[4] A. Rilley,“使用全息视图在 Python 中实现高级数据可视化”,走向数据科学,2020 年 6 月。可用:[https://towards data science . com/advanced-data-visualization-with-holo views-e 7263 ad 202 e](/advanced-data-visualization-with-holoviews-e7263ad202e)
# 附录:完整的代码清单
完整的代码笔记本可以在[那里](https://gist.github.com/gael-close/3dd0ef09a22def02ec97246543d9d4ae)找到,还有一个可执行版本的链接。
# 函数主成分分析和函数数据
> 原文:<https://towardsdatascience.com/functional-principal-component-analysis-and-functional-data-91d21261ab7f?source=collection_archive---------7----------------------->
## [实践教程](https://towardsdatascience.com/tagged/hands-on-tutorials)
## 简要介绍函数数据,尤其是函数主成分分析。

功能数据的典型示例(图片由作者提供)
# 功能数据
功能数据到底是什么?
假设您连续多年每天都在测量家乡的温度。如果你接着查看累积的数据,你可能会得出结论,温度在一年内的表现有些相似。嗯,这很明显,我们毕竟是按照这种模式生活的。天气受时间控制。事实上,有人可以说,有一个潜在的时间函数,导致了你的测量。现在在函数数据分析中,我们不把我们的测量数据解释为一个观察序列,而是一个单一的函数或曲线。这比传统的时间序列视图有多方面的优势。
1. 一个函数通常可以在任何时间点进行求值。
2. 我们可以分析函数的导数。
3. 功能可以很容易地注册到一个共同的时间表。
4. 函数可以为我们的数据提供更加自然和直观的视图。
5. 不必在相同的时间点进行测量才能进行比较。
第五点其实是一个普遍的问题。经常发生的情况是,您想要比较时间序列,但是它们的测量值不是同时进行的,或者一个测量值比另一个测量值多。比较这些会有问题。如果我们把数据看作函数,比较就容易多了。
# 我们是不是忘了什么?
函数是连续的,测量是不连续的。
是的,事实上测量是不连续的。但是我们可以估计数据的基本函数。通过这种方式,我们还能够进行平滑处理,这对于降低噪声来说是理想的。你可能会问,如何做到这一点?一种常见的方法是使用基展开。这里,我们将通过基函数的线性组合来表示我们的测量值。

(图片由作者提供)
常见的基函数是 B 样条、小波或傅立叶基。我们使用的基函数越多,我们得到的平滑就越多。这可能是为了减少噪音。在上面的图中,我使用了 20 个 B 样条来估计我的数据的基本函数。
现在你知道了什么是函数数据,以及我们如何从我们的离散测量中估计出潜在的函数。你现在是某种专家了。
# 功能主成分分析
主成分分析的老大哥。
有大量的技术来分析功能数据。其中许多都有一个非功能性的对应物,您可能已经使用过了。FPCA 是基于主成分分析,一种著名的降维技术。为了理解 FPCA,我们至少应该简要地谈谈 PCA。
## 主成分分析
函数主成分分析的小兄弟。
PCA 解决了数据分析中的一个常见问题。很多时候,我们必须降低大型数据集的维度。这是很有问题的,因为每个维度都存储着信息,如果我们忽视它们,我们就会丢失这些信息。也就是说,我们需要在降低维度的同时测量信息。在 PCA 的情况下,这个度量是我们的数据内的变化。哪里有变异,哪里就有信息。
PCA 找到所谓的主成分(向量),其沿着它们的方向最大化数据的方差。每个主成分解释了数据中总方差的一部分。他们建立了一个标准正交基,这意味着每个数据点是我们的主成分的线性组合。现在,我们可以通过使用 PC 的子集来表示数据点,从而降低数据的维数。例如,我们将选择最小数量的 PC,它们的总方差超过 95%。这样,我们减少了数据集的维度,但仍然保留了大部分信息。
如果您不熟悉 PCA,我鼓励您阅读这篇文章,以便更好地了解这项技术。
</a-one-stop-shop-for-principal-component-analysis-5582fb7e0a9c>
## 真正的交易
FPCA 本质上与 PCA 做完全相同的事情,但是有一些小的不同。在 PCA 中,我们处理向量,而在 FPCA 中,我们处理函数。这意味着我们的主要组成部分也是函数,或曲线。在下文中,我们称它们为功能主成分(fPC)。现在我们如何找到这些 fPC?

给定 n 条曲线,通过最大化第一个 fPC 分数的方差来找到第一个 fPC。我们将第一个 fPC 限制为一个,因为我们可以通过增加第一个 fPC 来无限地最大化。这样我们可以找到最大化的明确答案。第一个 fPC 是我们数据变化的主要来源。

现在,与第一个 fPC 相同的过程适用于第二个 fPC(以及以下所有 fPC)。只是这次我们有了一个额外的约束。也就是说,第二 fPC 与第一 fPC 正交。事实上,所有 fPC 都必须相互垂直。现在,每条曲线都可以表示为 fPC 的线性组合。fPC 分数告诉我们一条曲线在多大程度上由它各自的函数主成分组成。
现在你也知道了功能主成分背后的理论,但是如何应用这种技术呢?
# 一个典型的用例
也是一个相当温和的人。
可以探索性地使用 FPCA 来更深入地了解您的数据。现在我想给你们举个例子。我们想要分析的数据是过去 200 年来法国男性的对数死亡率。

(图片由作者提供)
首先,我们估计数据的基本函数。如前所述,我们使用 B 样条基展开来实现这一点。这额外导致平滑,这是伟大的,因为我们减少噪音。

(图片由作者提供)
现在我们运行功能主成分分析,看看前两个功能主成分。这两个解释了我们数据中大约 97%的变化。

(图片由作者提供)
这些功能性主成分的解释可能相当困难,因为在数据中可能没有明显的对应物。在我们的例子中,这种解释幸运地非常直观。
第一个 fPC 似乎是我们数据的均值函数的变化。这种变化会随着时间的推移而减少。也可以说,我们越老,我们的死亡就越确定。这个 fPC 解释了我们数据中 94%的变化。
第二个 fPC 似乎与 20-40 岁的人有关。这暗示了在我们的数据中,这个年龄组的死亡率与其他年龄组有很大的不同。我立即想到的是两次大战,它们造成了如此多年轻人不必要的死亡。
还记得我们说过如何找到函数的主成分吗?当时,我们了解到数据中的每条曲线都可以表示为 fPC 的线性组合,其中每个 fPC 分数决定了曲线在多大程度上由哪个成分解释。我们现在可以做的是将每条曲线缩减为一个二维点,由前两个 fPC 分数组成。

(图片由作者提供)
这很有趣。通过颜色编码,我们可以看到战争年代的曲线在很大程度上是由第二 fPC 解释的。这是一个好消息,因为我们的解释是正确的。我们也可以很清楚地看到,死亡率在 1950 年左右有一个很大的转变。这在原始数据中并不清楚。我们不仅深入了解了数据,还在不丢失太多信息的情况下降低了数据的(高维)维度。我们现在能够进一步使用这种表示并应用传统的聚类算法。
最后我想提一下,我从[2]中获得了这个分析的灵感。如果你对 FPCA 更深入的解释和示范性的使用感兴趣,我鼓励你去看看。
在[3]中还可以找到 scikit-fda python 包的文档。它为多功能分析工具提供了许多易于理解的例子。
[1] *人类死亡率数据库*。加州大学伯克利分校(美国)和马克斯·普朗克人口研究所(德国)。可在 www.mortality.org 的[或](http://www.mortality.org) [www.humanmortality.de](http://www.humanmortality.de) 获得(数据于【01.06.2021】下载)。
[2]尚,H.."功能主成分分析综述."AStA 统计分析进展 98(2014):121–142。
[[3]https://FDA . readthe docs . io/en/latest/auto _ examples/index . html](https://fda.readthedocs.io/en/latest/auto_examples/index.html)
# 功能时间序列
> 原文:<https://towardsdatascience.com/functional-time-series-83b717cca12?source=collection_archive---------12----------------------->
## [实践教程](https://towardsdatascience.com/tagged/hands-on-tutorials)
## 当我们更频繁地测量数据时,我们如何才能最好地分析它?功能数据分析(FDA)可以大大简化分析。

由[罗斯蒂斯拉夫·萨维钦](https://unsplash.com/@ross_savchyn?utm_source=medium&utm_medium=referral)在 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral) 上拍摄的照片
随着内存空间的增长,存储数据变得越来越便宜,这反过来意味着存储越来越多的数据。在时间序列的情况下,这意味着更频繁地收集数据。
然而,还不清楚如何对以(非常)高的频率记录的时间序列建模,尤其是当涉及(多个)季节性时。
我们应该把数据看作一元时间序列还是高维的多元时间序列?在许多情况下,最好将观察值视为时间的函数,并分析这一函数时间序列本身。例如,我们可以将股票的日内价格划分为每日观察值,我们观察每天的价格随时间的变化。(听起来很专业,但是我们后面会看到一个例子!)
当函数数据是连续的时,后一种方法特别有用,因为它意味着比高维向量更多的结构(对于连续函数,如果 *x* 接近 *y* , *f(x)* 接近*f(y)*; *x(i)* 对于一个向量 *(x(1),…,x(d))* ,不需要靠近 *x(i+1)* 。
功能数据在不同的环境中自然出现,如医学(脑电图数据)、金融(股票价格)或气象学(温度)。
在这篇博文中,我们将通过例子来尝试对函数时间序列有一个直观的理解。此外,假设检验的独立性和平稳性的假设函数时间序列介绍。
注:自始至终,我们都假定你熟悉时间序列分析的基本概念。如果想刷新一下知识或者入门题目,可以查看我的 [*以前的博文*](/time-series-analysis-part-i-3be41995d9ad) *。*
# 介绍
先说个例子。假设我们在一段时间内测量了特定位置的温度,并且每天收集一个观测值,那么对于年份 *t* 和日期 *i* 我们有观测值 *X(t,i)* ,其中 *i ∈ {1,…,d},t ∈ {1,…,n}* 和 *d = 365* (每年的天数),如图 1 所示。

图 1:2013-2017 年悉尼每日气温;x 轴:时间,y 轴:摄氏温度;作者图片
现在我们有不同的选择来处理数据。首先,我们可以将其视为一个单变量时间序列,并简单地按时间顺序连接数据,因此在技术上 *Y(t) = X(j,i)* 其中 *t = (j-1) d + i* 。在这种情况下,我们有季节性,这使得分析更加困难。
我们还可以将时间序列建模为一个多变量时间序列,其维数与每年的观测值一样多,这样时间序列的每个观测值都对应于全年收集的数据: *Y(t) = ( X(t,1),…,X(t,d) )* 。现在不用考虑季节性,但是维度很高(准确的说是 365 维)。当然,我们可以通过降低观测频率来降低维数。然而,在这种情况下,我们丢失了信息,并且不清楚如何选择频率(每周还是每月?).
最后一种方法是将数据视为函数时间序列 *Y(t,x)* ,其中我们有一个函数 *Y(。,x)* 为每年 *t* 用 *Y(t,i/d) = X(t,i)* 。在这种情况下,年温度被视为时间的函数,每个观测值对应于一个描述年温度的函数。在图 2 中,来自图 1 的数据被视为函数时间序列。

图 2:悉尼不同年份的气温;x 轴:时间,y 轴:摄氏温度;作者图片
数据的功能视角具有重要的优势。在这个例子中,全年的平均温度显然不是常数。然而,夏季的平均温度高于冬季的平均温度。因此,当建模为函数时间序列时,非平稳单变量时间序列可能是平稳的,因为我们比较的数据是合理的(例如,将 2016 年 1 月的温度与 2017 年 1 月而不是 2016 年 7 月的温度进行比较)。
此外,将数据建模为函数时间序列通常比使用高维时间序列更自然,因为它增加了额外的结构。例如,如果我们观察连续函数,如温度或股票价格,两个相近时刻的值是相似的,而这种结构对于任意多元时间序列是不存在的。
这在精神上类似于卷积神经网络。由于其特定的体系结构,CNN 的使用更多地局限于一组特定的问题,然而对于这组问题,它们工作得非常好。
功能数据分析(FDA)是一个活跃的研究领域,可用于各种应用。在下文中,我们将重点关注一种特定类型的函数数据,即函数时间序列。然而,请注意,许多来自统计的经典结果被推广到功能数据,例如比较不同组的期望值的 t 检验。
# 功能时间序列—基础知识
好的,我提到了函数时间序列,我们看到了一个例子,但是数学上什么是函数时间序列呢?它与单变量时间序列有什么不同?
数学上,只有很小的差别。单变量实时序列是由时间索引的真实数据的集合(见[此处](/time-series-analysis-part-i-3be41995d9ad))。所以对于时刻 *1,2,…,n* ,我们观察实值数据 *Y(1),Y(2),…,Y(n)* ,比如特定地点的温度或者某个股票的价格。一个*泛函时间序列*基本相同,只是我们观察的是函数而不是实值数据。在这种情况下, *Y(1),Y(2),…,Y(n)* 是函数,可以写成 *x* 中的函数,即 *Y(1)(x),Y(2)(x),…,Y(n)(x)* 。为了简单起见,我们经常假设函数定义在区间*【0,1】*上,必要时重新调整区间(如果 *f(x)* 定义在区间*【0,T】*, *g(x)=f(xT)* 定义在区间*【0,1】*)。
*技术说明:我们在一个函数空间而不是实数空间中工作,不清楚如何为函数数据定义诸如期望值或协方差之类的量。幸运的是,在大多数情况下,均值和协方差可以逐点定义,因此等式 E[Y(i)](x) = E[Y(i)(x)]和 Cov(Y(i)(x),Y(j)(z)) = Cov(Y(i),Y(j))(x,z)成立。
根据观察值的连续性或 L-可积性等假设,我们可能有额外的结构(连续函数的空间是 Banach 空间,L-空间是 Hilbert 空间),在这些情况下,逐点定义是合理的。*
# 平稳性和独立性测试
至于单变量时间序列,平稳性和独立性的概念大大简化了任何进一步的分析,所以我们想知道它们是否是合理的假设。在概念层面上,想法与单变量的情况相同:如果概率因子分解,两个函数观察是独立的;如果时间序列的分布在一段时间内保持不变,则时间序列是稳定的(参见[这篇博客文章](/time-series-analysis-part-i-3be41995d9ad)的严格定义)。
在[这篇博文](/time-series-analysis-part-ii-ii-bece7ecc9647)中,我们已经看到了如何验证单变量时间序列的两个假设。对于函数数据,我们可以做完全相同的事情,即使用累积和统计量来确定时间序列是否是(弱)平稳的,并使用组合测试来验证(或拒绝)独立性的零假设。
## 弱平稳性测试
当处理时间序列时,我们想知道它们是否是平稳的,因为在这种情况下,我们不需要考虑时间的变化。然而,平稳性是难以衡量的,我们经常使用时间序列的时刻作为代理。直观上,如果矩不随时间变化,我们可以忽略潜在分布的时间变化。因此,我们想测试给定时间序列的均值和(自)协方差是否是时不变的,而不是测试平稳性。对于一个函数时间序列 *Y(1)(x),Y(2)(x),…,Y(n)(x)* ,这转化为假设

对于某些 *i ∈ { 2,…,n }* 和

对于某些 *i ∈ { 2,…,n-h}* 。
如果零假设对任意正整数 *h* 有效,则时间序列 *Y(i)* 是弱平稳的。注意 *Y(i)* 的一阶和二阶矩本身就是函数。所以两个函数相等依赖于函数空间。在连续函数空间中,比如两个函数 *f* 和 *g* 在任意一点 *x* 相等,那么如果 *f(x)=g(x)* 。相反,在平方可积函数空间 *L* 中,两个函数相等,如果它们几乎在每个点都重合(w.r.t .勒贝格测度)。
我们通常将注意力限制在第一个 *H* 滞后上(对于所有 *1 ≤ h ≤ H* 的 *h* ),而不是对所有滞后 *h* 测试后面的假设,因为它们从根本上决定了分布的行为。在下文中,我们将只测试 *H₀* ,因为我们可以类似地测试关于二阶矩的零假设。此外,我们假设数据是平方可积的,因此它属于范数由 *||表示的空间 *L ([0,1])* 。||* 。在这种情况下,( 1)中的测试问题等价于

在单变量方案中,我们可以使用累积和统计量,它基本上将第一个观测值的平均值与其余观测值的平均值进行比较。(函数)累积和统计量定义为

在零假设(和弱假设)下, *√n C(u,x)* 弱收敛于空间中协方差函数未知的一个中心高斯过程 *B(u,x)**L([0,1])**| |。||₂* 。反之, *√n C(u,x)* 在选择下偏向 *+∞* 或 *-∞* 。所以如果 *√n C(u,x)* 偏离其极限 *B(u,x)* 太多,我们可以拒绝 *H₀* 。
不幸的是,我们不知道 *B(u,x)* 的分布,因为我们不知道协方差,需要估计它。有不同的方法可以做到这一点,时间序列分析中的一种常见方法是使用块乘数自举近似,这基本上是一种考虑时间相关性的重采样方案。
如果 *q(α)* 表示 *||B||₂* 的 *α* 分位数(例如,通过 bootstrap 过程或协方差的直接估计获得),我们可以拒绝 *H₀* 每当 **√n ||C||₂ > q(1-α)** 。这为 *H₀* 定义了一个渐近一致水平α测试。
## 组合式测验
与平稳性类似,随机独立性很难测量,我们使用时间序列(自)协方差结构作为代理来评估其依赖程度。为简单起见,我们假设时间序列是平稳的和居中的,即 *E[Y(i)]=0* (这个假设可以用类似于给出的过程来检验)。同样,我们主要对小滞后的自协方差感兴趣。根据经典的组合测试,我们考虑假设

像以前一样,函数时间序列的二阶矩本身就是函数,所以我们根据它们的范数来制定假设。
为了避免多重测试问题,我们同时比较所有的矩,而不是通过考虑它们的最大值来单独测试它们。
作为检验统计量,我们可以使用(最大的)经验矩

估计器 *Mₙ(h)* 以概率收敛于*e[y(1)y(1+h)】*,滞后时的自协方差 *h* 。因此,我们可以拒绝零假设,如果 *Mₙ(h)* 明显偏离其极限。
在零假设下,对于某个居中的高斯变量 *B(h)* ,认为 *√n ||Mₙ(h)||₂* 弱收敛于 *||B(h)||₂* ,而在选择下发散到无穷。
同样, *B(h)* 的协方差结构是未知的,它的分布可以用 bootstrap 程序来近似,就像在平稳性测试的情况下一样。
如果 *q(α)* 表示*最大{||B(h)||₂: 1 ≤ h ≤ H}* 的(近似) *α* 分位数,我们可以随时拒绝 *H₀*

其为 h₀*定义了渐近一致水平α测试。*
# 用 Python 实现
对于实施,我们使用来自澳大利亚的气候数据。更具体的说是澳大利亚政府气象局[提供的悉尼(站号 066062)1859-2017 年的日最低气温。](http://www.bom.gov.au/climate/data/)
首先,我们需要加载所需的包并准备数据:
## 均值平稳性的检验
为了测试均值的平稳性,我们定义了三个辅助函数来计算累积和统计量、 *L -* 范数和 bootstrap 复制来逼近分位数。
Test Statistic: 2.11
Approximated quantile: 1.84
The null hypothesis can be rejected
输出表明,我们可以拒绝常数均值函数的零假设。因此,从 1859 年到 2017 年,悉尼的温度不太可能是稳定的,这表明气候发生了变化。
## 非相关性测试
对于前面介绍的组合体类型测试,我们定义了两个辅助函数。第一个函数计算(函数)观测值的乘积,这些乘积稍后将用于更有效地计算经验矩。第二个函数生成 bootstrap 复制来逼近分位数。
*请注意,计算可能需要一些时间,因为分位数近似值的计算开销很大。*
*还要注意的是,为了简单起见,我们假设时间序列是居中的和平稳的。在给定的例子中,这两个假设显然都不满足。通过减去估计的(局部)平均值,我们可以将该方法推广到非中心时间序列,但它将是不稳定的,如前面的测试所示。无论如何,我们可以用这些数据来说明这种方法。*
Test Statistic: 2587.612
Approximated quantile: 67.463
The null hypothesis can be rejected
结果表明,不相关的零假设可以被拒绝。如前所述,这一结果是不可解释的,因为它既不合理假设时间序列的中心性也不平稳。
# 结论
函数数据分析比单变量数据分析更具技术性,但具有一些重要的优势,可用于许多应用。在概念层面上,这两种方法是相似的,我们可以使用(几乎)相同的思想和技术。
除了功能时间序列,FDA 还有许多其他重要的应用——它甚至可以用于维度缩减,这乍一看似乎违反直觉——这是我们作为数据科学家应该知道的一个话题。
*如果你对时间序列分析监控机器学习模型的应用感兴趣,这篇文章可能适合你:*
</monitoring-ml-models-in-production-768b6a74ee51>
*如果你对单变量时间序列感兴趣,可以看看这两篇入门帖子:*
</time-series-analysis-part-i-3be41995d9ad> </time-series-analysis-part-ii-ii-bece7ecc9647>
# 在 Pandas 中生成多重索引的函数以及如何删除级别
> 原文:<https://towardsdatascience.com/functions-that-generate-a-multiindex-in-pandas-and-how-to-remove-the-levels-7aa15ac7ca95?source=collection_archive---------6----------------------->
## groupby 和 unstack 操作如何创建多重索引,以及如何在不损害数据完整性的情况下删除多重索引

由[凯利·西克玛](https://unsplash.com/@kellysikkema?utm_source=medium&utm_medium=referral)在 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral) 上拍摄的照片
**简介**
在本文中,我们将了解什么是多重索引,何时何地使用它,生成多重索引的函数,以及如何将它折叠成一个单一的索引。
但是首先,让我们弄清楚一些基本的定义。
**索引**是数据帧中“唯一”标识每行的一列。可以把它想象成行标签。

作者图片
**多指标**是指有一个以上的指标。其他名称有*多重指标*和*分级指标*。

作者图片
Multiindex 也可以指**多个标题级别**,或者当你有一个列名的层次结构时。

作者图片
**多指标的优势**
* 用于保存包含层次结构或级别的高维数据。
* 以表格形式直观显示层次结构级别,非常有用。
* 它允许使用诸如 [df.xs()](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.xs.html) 和 [df.unstack()](https://pandas.pydata.org/docs/user_guide/reshaping.html#reshaping-by-stacking-and-unstacking) 之类的函数有效地选择和操作层次数据。
**多指标的缺点**
* 这种格式不允许直接绘制图表。
* 使用 multiindex 没有性能优势。
我们将使用来自 [seaborn](https://seaborn.pydata.org/) 的 [diamonds 数据集](https://github.com/mwaskom/seaborn-data/blob/master/diamonds.csv)来演示导致多索引情况的各种场景,以及如何将多索引折叠回单个索引数据框架。
import pandas as pd
import seaborn as snsdiamonds = sns.load_dataset(‘diamonds’)
那么,我们如何得到多个指数呢?
## 第一部分:生成多重指数
## A.行的多索引
**A1。使用** `**df.set_index(col_list)**`
下面的代码手动将索引设置为两列(`cut`和`clarity`)。
sorted_df = diamonds.sort_values([ 'cut’, ’clarity’])multiind_df = sorted_df.set_index([ ‘cut’,‘clarity’])multiind_df

按作者分类的多索引数据框架
需要注意的事项:
* 层次结构按预期显示。如果没有,记得对这些列的数据帧进行排序。
* 得到的数据帧的行数不变,而是重新排列数据帧,使得层次结构可见。
diamonds.shape###Results
(53940, 10)multiind_df.shape###Results
(53940, 8)
* 结果列的数量现在少了两个,因为索引丢失了一些列(参见上面的`df.shape`的结果)。
* 该索引现在是多索引。运行`df.index`显示一个多索引列表,其中每个元素都是一个[元组](/ultimate-guide-to-lists-tuples-arrays-and-dictionaries-for-beginners-8d1497f9777c)。
multiind_df.index

作者图片
* 默认情况下,先前的索引已被删除。当我们将索引设置为新数据帧中的另一列时,请看下面发生的情况。
multiind_df.set_index(‘carat’)

作者图片
如果您想保留以前的索引,首先使用`df.reset_index()`使索引成为现有列的一部分,然后使用`df.set_index(col_list)`。
**A2。由多列**的 `**groupby**` **产生的多索引**
`[df.groupby](https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html)`根据所选列的类别汇总列(特征)。
例如,我们可以通过`cut` 和`color`对菱形进行分组,以查看其他特性在这些类别中的分布情况。我们使用`max()`作为[聚合函数](https://cmdlinetips.com/2019/10/pandas-groupby-13-functions-to-aggregate/)。
grouped_df = diamonds.groupby([’cut’, 'color’]).max()grouped_df

按作者分组图片
需要注意的事项:
* 行数将大大减少。这是因为只显示唯一的索引(这里是唯一的`cut`和`color` 组合)。指定的聚合函数(`max`)将这些组中的其他值组合成一个值。
diamonds.shape###Results
(53940, 10)grouped_df.shape###Results
(35, 7)
* 列数也减少到 7 列,因为现在有两列作为索引,而`clarity`被删除,因为聚合函数`max` 不能处理非数字特性。
## B.列的多索引(多个标题级别)
现在让我们来演示一下我们是如何得到多个标题级别的。
**B1。** `**Groupby**` **超过两列则** `**unstack**`
我们将继续使用上一节的代码。我们用`cut`和`color.`进行分组
现在让我们将它分解,这样`‘cut’`类别就显示为列标题。这是通过将它们从行索引翻转到列标题来实现的。
grouped_df = diamonds.groupby([‘cut’,‘color’]).max()unstacked_df = grouped_df.unstack(‘cut’)unstacked_df

作者未堆叠 df 的图像
我们现在在原来的标题下面有了一个新的标题级别— `carat`、`depth`、`price,`等等。
B2。 `**Groupby**` **使用几个聚合函数**
在我们之前的分组中,我们只使用了 `max()`作为[聚合函数](https://cmdlinetips.com/2019/10/pandas-groupby-13-functions-to-aggregate/)。但是,我们可以包含几个聚合函数,它们的名称将保持在一个新的级别。
下面的代码将数据按一列分组— `cut` —但是使用了 3 个聚合函数— `median`、`max`和`mean`。
diamonds.groupby( ‘cut’).agg( [‘median’,‘max’,‘mean’] )

作者图片
B3。使用 `**pivot_table**` **将行转换成列**
一个`pivot_table`提供了一种方便的方法来将列的值重新整形为列标题,就像我们上面使用的 unstack 方法一样。在这里,我们将关注我们的原始钻石数据集。
diamonds.pivot_table(
values = ‘price’,
index = ‘color’,
columns = [‘clarity’,‘cut’],
aggfunc=‘median’)

作者数据透视表 _ 表格
**B4。熊猫交叉标签**
`pandas.crosstab`功能允许我们创建数据频率表。在下面的代码中,我们希望找到每个`color`的`clarity`和`cut`的分布。我们使用`normalize=’columns’`来显示每列的百分比分布。
pd.crosstab(
index = diamonds[’color’],
columns = [diamonds[’clarity’],
diamonds[’cut’]],
normalize = 'columns’)

作者的交叉表插图
## 第二部分:删除多重索引
## **C .删除多行索引(每行多个索引)**
**C1。使用** `**df.reset_index()**`
`df.reset_index()`通过将现有索引转换为普通列来重置索引。生成一个[范围索引](https://pandas.pydata.org/docs/reference/api/pandas.RangeIndex.html)作为新的索引。
grouped_df.reset_index()

作者图片
在多索引的情况下,我们可以通过包含`level=n`来选择要重置的索引的名称(或位置)。
grouped_df.reset_index(
level=‘cut’)

作者图片
我们还可以重置索引,并仍然保持多个标题级别。
df = diamonds.groupby(
'cut’).agg(
[’median’,’max’,’mean’])df.reset_index()

作者图片
**C2。使用** `**df.droplevel(level = level_to_drop, axis=0)**`删除多索引
当您想要完全删除一个索引时,可以使用这种方法。
使用前面生成的`grouped_df`,让我们删除`color`索引。注意,我们既可以用`level=index_name`也可以用`level=position`(从 0 开始计数为最外层)。该方法返回修改后的数据帧。
grouped_df.droplevel(
level = 1,
axis=0)

作者图片
**C3。使用** `**df.index.droplevel(level = level_to_drop)**`删除多索引
该函数从索引中删除指定的级别,并返回剩余的索引列表。下一步是将这个列表指定为数据帧的索引。
grouped_df.index = grouped_df.index.droplevel(‘color’)
打印数据帧,显示与上一节中的图像相同的结果。
## D.删除列标题中的多索引
**D1。合并每列的级别名称**
合并标题级别是一种常见的解决方案,因为两个级别都可能有用,并且在这里不可能重置索引。

多个标题级别

按作者列出的列名元组列表
***方法 1:使用*** `***map***` ***和*** `***join***` ***功能***
函数使用给定的函数修改列表中的每个元素。这里,我们为每个列名准备了一个元组列表。地图用途[。连接](https://www.jquery-az.com/3-ways-convert-python-list-string-join-map-str/)将元组合并成一个用下划线分隔的名称。
df.columns.map(‘_’.join)

上面的代码返回一个索引对象。我们需要将它分配给列名,然后打印出数据帧。
df.columns = df.columns.map(‘_’.join)

按作者合并标题级别
***方法二:利用列表理解***
一个[列表理解](https://realpython.com/list-comprehension-python/#using-list-comprehensions)也接受一个列表,通过一些操作修改每个元素,并返回一个新的列表。我们使用。join 使用不同的连接符号(|)将每个元组合并为一个名称。
df.columns = [‘|’.join(s) for s in df.columns]

作者图片
另一个列表理解示例使用了 [f 字符串格式](https://zetcode.com/python/fstring/)。这有助于在合并后更改名称的顺序。在下面的代码中,低级别的名称排在最前面。
df.columns = [f’{j}#{i}’ for i,j in df.columns]

按作者合并后反转名称
**D2。使用** `**df.droplevel(level, axis=1)**`删除每列的多索引
如果标题级别对标识列没有用,您可以选择删除它。我们使用`axis=1`(或`axis= ’columns’`)来表示列标题级别。
让我们来演示一下。首先,我们通过`cut`对数据集进行*分组,并使用**四个**聚合函数。然后我们使用`df.xs()`只选择`price`列。*
df = diamonds.groupby(
‘cut’).agg(
[‘max’, ‘median’, ‘min’,‘mean’])df.xs(
key=‘price’,
axis=1,
level=0,
drop_level=False)

作者图片
现在我们可以放弃顶层`‘price’`,因为我们已经知道所有的值都代表价格。
df_price.droplevel(
level=0,
axis=1)

作者图片
**D3。** `**df.columns.droplevel(level_to_drop)**`
我们还可以删除一个标题级别,并以列表形式返回所需的级别。(这与前面的`df.index.droplevel`类似,但用*列*代替*索引*)。然后,我们将这个列表分配给列名。
df.columns = df.columns.droplevel(0)display(df)
打印数据帧,显示与上一节中的图像相同的结果。
D4。 `**df.columns.get_level_values(level_to_return)**`
该函数返回所需的级别并删除其余的级别。下面的代码产生的结果与上一节中的图像相同。
df.columns = df.columns.get_level_values(1)df
## 结论
在本文中,我们探讨了生成多索引的各种函数,以及如何将它折叠回只有一个索引的基本数据框架。
从头开始创建多索引数据框架还有其他方式和几种复杂的方式[访问和选择数据](https://stackoverflow.com/questions/18835077/selecting-from-multi-index-pandas)。我鼓励您使用包含许多显著分类特征的多维数据进行实验和实践,并尝试使用多索引。
在这里找到这篇博文[中使用的代码](https://github.com/suemnjeri/medium-articles/blob/main/multiindex/Multiindex%20notebook.ipynb)。
如果你喜欢这个内容,并希望得到更多类似的通知,请在这里订阅。如果你还不是一个中等会员,在这里加入。感谢您的阅读。
## 参考
1.[熊猫多指数教程](http://zaxrosenberg.com/pandas-multiindex-tutorial/)作者 [Zax Rosenberg,CFA](https://zaxrosenberg.com/)
2.[访问熊猫多索引数据框架](/accessing-data-in-a-multiindex-dataframe-in-pandas-569e8767201d)中的数据 [B. Chen](https://medium.com/u/563d09da62a?source=post_page-----7aa15ac7ca95--------------------------------)
3.[层次索引](https://jakevdp.github.io/PythonDataScienceHandbook/03.05-hierarchical-indexing.html#The-Better-Way:-Pandas-MultiIndex)摘自杰克·范德普拉斯的 [Python 数据科学手册](http://shop.oreilly.com/product/0636920034919.do)
# FuncTools:一个被低估的 Python 包
> 原文:<https://towardsdatascience.com/functools-an-underrated-python-package-405bbef2dd46?source=collection_archive---------4----------------------->
## 使用 functools 将您的 Python 函数提升到一个新的水平

([https://unsplash.com/photos/gnyA8vd3Otc](https://unsplash.com/photos/gnyA8vd3Otc)
# 介绍
上个月,我写了一篇关于 Python 标准库中一些模块的文章,我发现这些模块在我的编程和数据科学生涯中非常有用。这篇文章获得了高质量的反馈,因此我决定再写一篇文章,讨论相同的主题,使用大家都应该熟悉的更多标准库工具。事实证明,Python 编程语言的基础实际上是相当包容的,包括了许多应对各种编程挑战的优秀工具。如果你想读这些文章中的任何一篇,你可以在这里查阅:
</10-surprisingly-useful-base-python-functions-822d86972a23> </15-more-surprisingly-useful-python-base-modules-6ff1ee89b018>
当浏览这些工具时,似乎有些工具应该有一整篇文章专门介绍它们,而不仅仅是对它所提供的大多数其他模块的概述。我认为最能体现我这种想法的工具是 functools 模块。这是一个非常强大的模块,通过使用简单和经典的方法,可以用来改进 Python 中的几乎任何功能,例如利用处理器速度上的堆栈。虽然在某些情况下,我可以看到这是一个很大的负面影响,但当然也有例外。
> [笔记本](https://github.com/emmettgb/Emmetts-DS-NoteBooks/blob/master/Python3/Functools%20examples.ipynb)
# 隐藏物
functools 模块提供的最酷的东西可能是能够在内存中缓存某些计算,而不是为了以后重新计算而丢弃它们。这是节省处理时间的一个很好的方法,特别是如果您发现自己处于 Python3 超时的情况下,您的代码无法被解释。虽然这伴随着使用更多内存的代价,但是在许多不同的情况下使用它肯定是有意义的。Python 编程语言本身是相当声明性的,通常解释器为我们处理所有的内存管理。虽然这是一种效率较低的编程方法,但它也消除了许多分配内存和类似事情的麻烦。使用 functools,我们可以通过决定什么在堆栈中,什么将被重新计算来改变这一点。
functools 提供的缓存的好处在于,它既易于使用,又允许用户更好地控制代码下的解释器。利用这一令人敬畏的特性就像在函数上方调用它一样简单。这里有一个关于阶乘计算的例子,我真的认为它很好地利用了这一点:
def factorial(n):
return n * factorial(n-1) if n else 1
为了在这个函数中使用缓存,我们将从 functools 中导入 lru_cache,并在我们的函数之前调用它:
@lru_cache
def factorial(n):
return n * factorial(n-1) if n else 1
现在,让我们评估一下,仅仅通过做这个简单的加法,我们可能已经获得的性能优势。让我们看看没有它阶乘函数计算阶乘有多快:

现在,我将运行重新启动内核,以确保没有奇怪的内存问题发生,并运行我们使用 lru_cache 的新函数。

从这个例子中,我们可以看到使用这种缓存技术的巨大好处。众所周知,阶乘很难用计算机计算。在大多数情况下,这是因为阶乘是递归的自然数学示例。结果,二项分布的累积分布函数(CDF)让我做噩梦。这种计算非常密集,以至于编程语言的基本阶乘函数通常会使用查找表,而不是计算数字。
也就是说,如果你打算像这样使用递归,开始熟悉 functools 可能是个好主意。这个标准的库工具可以大大加快 Python 通常很难解决的问题的解决速度。在某种程度上,它真的把我带回了 Numba Python 编译器,在那里一个简单的调用就能让你的代码变得更快。如果你想看我不久前写的一篇文章,你可以在这里看看:
</numba-jit-compilation-but-for-python-373fc2f848d6>
# 关键功能
有没有这样的时候,你真的想使用一些非常旧的 Python 代码中的函数,但是这个函数被编译成了一个比较函数?在现代 Python 3 中,这些类型的函数不再得到很好的支持,甚至不再被使用,并且将一种函数类型转换成另一种函数类型可能非常困难。也就是说,functools 可以通过另一个简单的方法调用轻松解决这个问题:
newfn = cmp_to_key(myfunction)
# 部分的
partial 函数将返回一个新的 partial 对象,稍后可以使用完全相同的参数调用该对象,并且它的行为将与我们之前的 func 完全一样。代码中的函数最终看起来有点像这样:
def partial(func, /, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = {**keywords, **fkeywords}
return func(*args, *fargs, **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
我们可以在之前使用阶乘函数创建一个分部:
from functools import partial
fact = partial(factorial)
fact(10)
partial()用于部分函数应用程序,它“冻结”函数参数和/或关键字的某个部分,从而产生具有简化签名的新对象。这样做的结果是,通过创建一个包装在全新对象中的函数的简化版本,可以轻松地节省内存和提高速度。
# 减少
reduce 函数将通过累积迭代应用两个参数的函数。记住这一点,我们需要我们的论点是可重复的。在这个例子中,我将使用一个生成器,range。这将使我们非常容易地构造出我们想要的任意长度的列表数据类型。
from functools import reduce
reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])
这有什么用?这将把可迭代式简化成它的最简形式。reduce 的名字来自于它的数学等价物。这可以方便地节省运行时性能,并且像这个列表中的许多其他调用一样,没有一个好的借口不使用!
# 派遣
> 好吧——这真的真的很酷。
我博客的读者可能会注意到我是 Julia 编程语言的超级粉丝。这不仅是因为 Julia 是一种令人敬畏的高性能科学编程语言,还因为我对多重调度有着特殊的吸引力。当这个概念不在的时候,我真的很怀念它,它让编程感觉更自然。在某种程度上,让函数更加基于类型,而不是仅仅通过不同的调用来处理不同的情况,这很好。
Python 的 functools 模块实际上提供了一种方法,可以有效地将 Python 变成一种具有多重调度的编程语言。最重要的是,它像以前一样简单,只需添加一个装饰器。为了做到这一点,我们将首先用 singledispatch 修饰器来修饰一个函数,它不需要任何参数的特定类型:
@singledispatch
def fun(arg, verbose=False):
if verbose:
print(“Let me just say,”, end=" ")
print(arg)
现在我们将使用我们的 function.register 来注册新类型的调度:
@fun.register
def _(arg: int, verbose=False):
if verbose:
print(“Strength in numbers, eh?”, end=" ")
print(arg)
@fun.register
def _(arg: list, verbose=False):
if verbose:
print(“Enumerate this:”)
for i, elem in enumerate(arg):
print(i, elem)
# 结论
Functools 是一个非常棒的软件包!此外,我认为这个包对于任何 Python 程序员来说都非常方便。这个包允许你用最少的努力来简化你的代码和数学。此外,更深入地了解语言及其缓存也相对容易,这对于提高某些方面的性能非常方便。几乎任何你想做的有趣的函数都可以用这个模块来完成,这太棒了!
# func tools——Python 中高阶函数的威力
> 原文:<https://towardsdatascience.com/functools-the-power-of-higher-order-functions-in-python-8e6e61c6e4e4?source=collection_archive---------9----------------------->
## 浏览 Python 的 functools 模块,了解如何使用它的高阶函数来实现缓存、重载等等

Joel Filipe 在 [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 上拍摄的照片
Python 标准库包括许多伟大的模块,可以帮助你使你的代码更干净、更简单,而`functools`绝对是其中之一。这个模块提供了许多有用的高阶函数,这些函数作用于或返回其他函数,我们可以利用这些函数来实现函数缓存、重载、创建装饰器,并在总体上使我们的代码更具功能性,所以让我们浏览一下它,看看它能提供的所有东西...
# 贮藏
让我们从`functools`模块最简单却非常强大的功能开始。这些是缓存函数(也是装饰器)- `lru_cache`,`cache`和`cached_property`。其中第一个- `lru_cache`提供*最近最少使用的*函数结果缓存,或者换句话说- *结果存储*:
在这个例子中,我们使用`@lru_cache` decorator 获取请求并缓存它们的结果(最多 32 个缓存结果)。为了查看缓存是否真正工作,我们可以使用`cache_info`方法检查函数的缓存信息,它显示缓存命中和未命中的数量。装饰器还提供了`clear_cache`和`cache_parameters`方法,分别用于使缓存的结果无效和检查参数。
如果你想有一个更细粒度的缓存,那么你也可以包含可选的`typed=true`参数,这样不同类型的参数可以分别缓存。
`functools`中的另一个缓存装饰器是一个简单称为`cache`的函数。它是在`lru_cache`之上的一个简单的包装器,省略了`max_size`参数,使它更小,因为它不需要驱逐旧值。
还有一个装饰器可以用于缓存,它叫做`cached_property`。这个——正如您可能猜到的——用于缓存类属性的结果。如果你有一个计算起来很昂贵同时又是不可变的属性,这是非常有用的。
这个简单的例子展示了我们如何使用缓存属性来缓存呈现的 HTML 页面,这些页面会一遍又一遍地返回给用户。对于某些数据库查询或长时间的数学计算也是如此。
`cached_property`的好处是它只在查找时运行,因此允许我们修改属性。修改属性后,将不使用以前缓存的值,而是计算并缓存新值。也可以清除缓存,我们需要做的就是删除属性。
在本节的最后,我要对上述所有装饰器提出警告——如果您的函数有任何副作用或者每次调用都会创建可变对象,请不要使用它们,因为这些不是您想要缓存的函数类型。
# 比较和排序
你可能已经知道在 Python 中可以使用`__lt__`、`__gt__`或`__eq__`实现比较操作符,比如`<`、`>=`或`==`。尽管实现`__eq__`、`__lt__`、`__le__`、`__gt__`或`__ge__`中的每一个都很烦人。幸运的是,`functools`模块包含了`@total_ordering`装饰器,可以帮助我们完成这个任务——我们需要做的就是实现`__eq__`,剩下的方法和 rest 将由装饰器自动提供:
上面显示了即使我们只实现了`__eq__`和`__lt__`我们也能够使用所有丰富的比较操作。这样做最明显的好处是不必编写所有额外的神奇方法,但更重要的可能是减少了代码并提高了可读性。
# 过载
可能我们都被告知函数重载在 Python 中是不可能的,但是实际上有一个简单的方法来实现它,使用 `functools`模块中的两个函数- `singledispatch`和/或`singledispatchmethod`。这些函数帮助我们实现我们所谓的*多重分派*算法,这是 Python 等动态类型编程语言在运行时区分类型的一种方式。
考虑到函数重载本身就是一个很大的话题,我专门为 Python 的`singledispatch`和`singledispatchmethod`写了一篇文章,所以如果你想了解更多,你可以在这里阅读更多:
</the-correct-way-to-overload-functions-in-python-b11b50ca7336> [## Python 中重载函数的正确方法
towardsdatascience.com](/the-correct-way-to-overload-functions-in-python-b11b50ca7336)
# 部分的
我们都使用各种外部库或框架,其中许多提供了需要我们传入回调函数的函数和接口——例如用于异步操作或事件监听器。这没什么新鲜的,但是如果我们还需要在回调函数中传递一些参数呢?这就是`functools.partial`派上用场的地方- `partial`可以用来*冻结*函数的一些(或全部)参数,用简化的函数签名创建新对象。迷惑?让我们看一些实际的例子:
上面的代码片段演示了我们如何使用`partial`来传递函数(`output_result`)及其参数(`log=logger`)作为回调函数。在这种情况下,我们使用`multiprocessing.apply_async`,它异步计算提供的函数(`concat`)的结果,并将其结果返回给回调函数。然而,`apply_async`总是将结果作为第一个参数传递,如果我们想要包含任何额外的参数,就像在本例中的`log=logger`一样,我们必须使用`partial`。
这是一个相当高级的用例,所以一个更基本的例子可能是简单地创建打印到`stderr`而不是`stdout`的函数:
通过这个简单的技巧,我们创建了一个新的 callable(函数),它总是将`file=sys.stderr`关键字参数传递给`print`,这样我们就不必每次都指定关键字参数,从而简化了代码。
最后一个好的衡量标准的例子。我们还可以使用`partial`来利用`iter`函数鲜为人知的特性——可以通过向`iter`传递 callable 和 sentinel 值来创建一个迭代器,这在下面的应用程序中很有用:
通常,当读取一个文件时,我们希望遍历所有行,但是对于二进制数据,我们可能希望遍历固定大小的记录。这可以通过使用读取指定数据块的`partial`创建 callable 并将其传递给`iter`来完成,然后由后者创建迭代器。这个迭代器然后调用`read`函数,直到到达文件末尾,总是只取指定的数据块(`RECORD_SIZE`)。最后,当到达文件结尾时*返回标记值* ( `b''`),迭代停止。
# 装修工
我们已经在前面的章节中讨论了一些装饰者,但是没有讨论创造更多装饰者的装饰者。一个这样的装饰器是`functools.wraps`,为了理解我们为什么需要它,让我们首先看一下下面的例子:
这个例子展示了如何实现一个简单的装饰器——我们用外部的`decorator`函数包装执行实际任务的函数(`actual_func`),外部的`decorator`函数成为我们可以附加到其他函数的装饰器——例如这里的`greet`函数。当`greet`函数被调用时,你会看到它打印了来自`actual_func`的消息以及它自己的消息。一切看起来都很好,这里没有问题,对不对?但是,如果我们尝试以下方法会怎么样:
当我们检查修饰函数的 name 和 docstring 时,我们发现它被 decorator 函数内部的值替换了。这并不好——我们不能在每次使用 decorator 时覆盖所有的函数名和文档。那么,我们如何解决这个问题呢?—带`functools.wraps`:
`wraps`函数唯一的工作就是复制名称、文档字符串、参数列表等。以防止它们被覆盖。考虑到`wraps`也是一个装饰者,我们可以把它放到我们的`actual_func`上,问题就解决了!
# 减少
在`functools`模块中最后但同样重要的是`reduce`。你可能从其他语言中知道它是`fold` (Haskell)。这个函数的作用是获取一个 iterable,然后*将*(或折叠)它的所有值变成一个值。这有许多不同的应用,以下是其中一些:
从上面的代码中可以看出,`reduce`可以简化并经常将代码压缩成单行,否则代码会很长。也就是说,仅仅为了缩短代码、使*【聪明】*或使*更实用*而过度使用这个函数通常是个坏主意,因为它会很快变得难看和不可读,所以在我看来——少用它。
此外,考虑到使用`reduce`通常会产生一行程序,它是`partial`的理想候选:
最后,如果你不仅需要最终的*简化的*结果,还需要中间结果,那么你可以使用`accumulate`来代替——来自另一个伟大模块`itertools`的函数。这就是你如何使用它来计算运行最大值:
# 结束语
正如你在这里看到的,`functools`提供了许多有用的函数和装饰器,可以让你的生活更轻松,但是这个模块只是冰山一角。正如我在开始时提到的,Python 标准库包括许多可以帮助你构建更好代码的模块,所以除了我们在这里探索的`functools`,你可能还想检查其他模块,比如`operator`或`itertools`(我也写过关于这个的文章👇)或者直接进入 [Python 模块索引](https://docs.python.org/3/py-modindex.html),点击任何引起你注意的东西,我相信你会在那里找到有用的东西。
*本文最初发布于*[*martinheinz . dev*](https://martinheinz.dev/blog/52?utm_source=medium&utm_medium=referral&utm_campaign=blog_post_52)
</tour-of-python-itertools-2af84db18a5e> </making-python-programs-blazingly-fast-c1cd79bd1b32> </ultimate-guide-to-python-debugging-854dea731e1b>