人工智能(AI)是一门致力于以某种方式自动化任务的领域,以展示对人类观众具有某种智能形式。这种表现的智能可能类似于人类智能,或者只是机器或程序给我们带来的一些见解性行动。随着我们对世界的理解随着我们的工具而改善,我们对于什么会让我们感到惊讶或认为具有智能的期望不断提高。AI领域的知名研究人员RodneyBrooks表达了这种效应(通常称为AI效应):
每当我们揭开其中一部分的面纱,它就不再神秘;我们会说,“哦,那只是一种计算。”我们曾开玩笑说,AI的意思是“几乎实现”。
近年来,AI在硬件方面取得了巨大进展,例如图形处理单元(GPUs)和现在的张量处理单元(TPUs),这些硬件可以支持更强大的模型,如具有数十万、百万甚至数十亿参数的深度学习模型。这些模型在基准测试中表现越来越好,通常达到人类甚至超人类水平。对于任何涉足该领域的人来说,一些经过数千小时训练的模型,如果在亚马逊网络服务(AWS)上运行,其价值可能达到数十万美元,现在可以下载使用和扩展。
这种在图像处理、音频处理和越来越多的自然语言处理中表现出来的性能飞跃特别显著。在游戏领域中,这种表现在媒体上尤为突出。虽然1997年卡斯帕罗夫与深蓝的国际象棋比赛仍深入人心,但可以说,机器对人类国际象棋冠军的成功主要归因于强大超级计算机每秒搜索和分析2亿个位置的蛮力。然而,此后,算法和计算能力的结合使得机器在更复杂的游戏中表现出了熟练和掌握。
下表展示了AI的进展:
因此,现在比以往任何时候更及时地查看并学习使用AI的最先进方法,这就是本书的内容。你将找到精心选择的配方,这些配方将帮助你更新你的知识并了解最新的算法。
如果你希望为工作或甚至是业余项目构建AI解决方案,你会发现这本食谱书很有用。借助易于遵循的配方,本书将带你了解构建解决问题的智能模型所需的AI算法。通过本书,你将能够确定解决应用问题的AI方法,实现和测试算法,并处理模型版本控制、报告和监控。
这本AI机器学习书籍适用于Python开发者、数据科学家、机器学习工程师和深度学习从业者,他们想要学习如何用简单的配方构建人工智能解决方案。如果你希望在各种使用情境中找到执行不同机器学习任务的最新解决方案,本书也会对你有所帮助。掌握Python编程语言和机器学习概念的基本工作知识将有助于你在本书中有效地处理代码。
第一章,Python中的人工智能入门,描述了使用Python进行数据处理和AI的基本设置。我们将在pandas中进行数据加载、绘图,并在scikit-learn和Keras中编写第一个模型。由于数据准备是一项耗时的活动,我们将介绍最先进的技术来简化这一活动。
第三章,模式、异常值和推荐,通过一个涉及真实世界聚类的示例,讲述了如何使用sklearn和Keras检测数据中的异常和离群值。接着我们将介绍如何构建一个模糊字符串匹配的最近邻搜索、通过构建潜在空间进行协同过滤以及在图网络中进行欺诈检测。
第四章,概率建模,解释了如何构建预测股票价格的概率模型,以及如何在不确定条件下估计客户寿命,诊断疾病和量化信用风险。
第五章,启发式搜索技术和逻辑推理,介绍了广泛的问题解决工具类,从本体论和基于知识的推理开始,到在满足性背景下的优化,以及使用粒子群算法、遗传算法等方法的组合优化。我们将模拟多主体系统中的疫情传播,为棋盘引擎实现蒙特卡洛树搜索,编写基本的逻辑求解器,并通过图算法嵌入知识。
第六章,深度强化学习,应用多臂老丨虎丨机优化网站,实施REINFORCE算法用于控制任务和简单游戏的深度Q网络。
第七章,高级图像应用,带您从基础到最先进的图像识别方法之旅。然后,我们将学习如何使用生成对抗网络创建图像样本,以及如何使用对抗自编码器进行风格转移。
第八章,移动图像处理,从视频流中的图像检测开始,然后使用深度伪造模型创建视频。
第九章,音频与语音深度学习,对不同的语音命令进行分类,然后介绍了文本到语音架构,并以递归神经网络模型和生成音乐序列的配方结束。
第十章,自然语言处理,解释了如何分类情感,创建聊天机器人,并使用序列到序列模型翻译文本。最后,我们将尝试使用最先进的文本生成模型撰写一本流行小说。
第十一章,生产中的人工智能,涵盖了监控和模型版本控制,可视化为仪表板,并解释了如何保护模型免受可能泄露用户数据的恶意黑客攻击。
本书中涵盖的一些最显著的软件和库列在以下表中:
本书中使用了多种文本约定。
CodeInText:指示文本中的代码字词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和Twitter句柄。例如:“这里是RangeTransformer的简化代码。”
代码块设置如下:
importoperatoroperator.sub(1,2)==1-2#True任何命令行输入或输出如下所示:
$mkdircss$cdcss粗体:表示新术语、重要词汇或屏幕上显示的文字。例如,菜单或对话框中的单词会以这种方式出现在文本中。这里有一个例子:“从管理面板中选择系统信息。”
警告或重要提示会以这种方式出现。
提示和技巧会以这种方式出现。
在本书中,您将经常看到几个标题(准备就绪、如何操作、工作原理、还有更多和另请参阅)。
要清晰地说明如何完成配方,请使用以下章节:
本节告诉您配方中可以期待什么,并描述如何设置配方所需的任何软件或初步设置。
本节包含了跟随配方所需的步骤。
本节通常详细解释了前一节中发生的事情。
本节包含有关配方的附加信息,以进一步增加你对其的了解。
我们的读者反馈一直受欢迎。
一般反馈:如果您对本书的任何方面有疑问,请在消息主题中提及书名,并发送邮件至customercare@packtpub.com。
盗版:如果您在互联网上发现我们作品的任何形式的非法拷贝,请向我们提供位置地址或网站名称,我们将不胜感激。请通过链接copyright@packt.com联系我们。
在本章中,我们将首先设置一个Jupyter环境来运行我们的实验和算法,我们将介绍不同的巧妙Python和Jupyter的AI技巧,我们将在scikit-learn、Keras和PyTorch中进行一个玩具示例,然后在Keras中进行稍微复杂的示例以完成这些事情。本章主要是介绍性的,本章中看到的许多内容将在后续章节中进一步构建,因为我们将涉及更高级的应用。
在本章中,我们将涵盖以下示例:
正如你所知,自从你获取了这本书以来,Python已经成为人工智能领域的主导编程语言。它拥有所有编程语言中最丰富的生态系统,包括许多最先进算法的实现,使用它们通常只需简单导入并设置少量参数。无需多言,在许多情况下我们将超越基本用法,并在我们深入实例时讨论很多潜在的思想和技术。
我们无法强调快速原型设计和查看其作为解决方案的效果的重要性。这通常是AI或数据科学工作的主要部分。当将想法转化为原型时,读取-求值-打印循环(REPL)是快速迭代的关键,您希望具有编辑历史、图形化等功能。这就解释了为什么JupyterNotebook(其中Jupyter简称Julia,Python,R)在AI工作中如此重要。
请注意,尽管我们将专注于JupyterNotebook或GoogleColab(它在云中运行Jupyter笔记本),但还有几个功能上类似的替代方案,例如JupyterLab或者使用远程解释器运行的PyCharm。然而,JupyterNotebook仍然是最流行(可能也是最受支持)的选择。
首先确保您已安装Python,并有安装库的方法。根据以下两种情况使用和安装库有不同的方法:
让我们设置我们的Python环境!
正如我们提到的,我们将看两种情况:
在第一种情况下,我们无需在服务器上设置任何东西,只需安装几个额外的库。在第二种情况下,我们将安装Anaconda发行版的环境,并查看Jupyter的设置选项。
在这两种情况下,我们将有一个交互式的Python笔记本,通过它我们将运行大部分的实验。
GoogleColab是JupyterNotebook的修改版本,在Google硬件上运行,并提供访问支持硬件加速(如TPU和GPU)的运行时。
在GoogleColab中,您可以将模型保存到Google服务器的远程磁盘并重新加载。从那里,您可以将模型下载到自己的计算机或与GoogleDrive同步。ColabGUI为这些用例提供了许多有用的代码片段。以下是如何从Colab下载文件的方法:
fromjoblibimportdump
dump(
my_model,
'my_model_auc0.84.joblib'
)
files.download('my_model_auc0.84.joblib')
Anaconda是一个Python发行版,带有自己的包安装程序和环境管理器,称为conda。这使得保持你的库更新更容易,它还处理系统依赖性管理以及Python依赖性管理。稍后我们会提到一些Anaconda/conda的替代方案;现在,我们将快速浏览本地安装的说明。在线材料中,您会找到说明如何为团队中的其他人提供类似安装,例如在使用docker化设置的公司中,这有助于管理设置一个或一组机器上的Python环境。
如果您的计算机已经设置好,并且您熟悉conda和pip,请随意跳过此部分。
对于Anaconda安装,我们需要下载一个安装程序,然后选择一些设置:
Anaconda支持Linux、macOS和Windows安装程序。
对于macOS和Windows,您也可以选择图形安装程序。这在Anaconda文档中都有详细说明;但是,我们将快速浏览终端安装。
bashAnaconda3-2019.10-Linux-x86_64.sh你需要阅读并确认许可协议。你可以通过按下空格键直到看到要求同意的问题。你需要按下Y然后按Enter。
您可以选择建议的下载位置或选择在您计算机上共享的目录。完成后,您可以享用美味的咖啡或观看Python和许多Python库的安装。
最后,您可以决定是否运行condainit程序。这将在您的终端上设置PATH变量,以便在输入python、pip、conda或jupyter时,conda版本将优先于计算机上安装的任何其他版本。
请注意,在基于Unix/Linux的系统上,包括macOS,您始终可以按如下方式检查您正在使用的Python二进制文件的位置:
>whichPython在Windows上,您可以使用where.exe命令。
如果看到类似以下内容,则知道您正在使用正确的Python运行时:
/home/ben/anaconda3/bin/Python如果未看到正确的路径,可能需要运行以下命令:
source~/.bashrc这将设置您的环境变量,包括PATH。在Windows上,您需要检查您的PATH变量。
同一台机器上也可以设置和切换不同的环境。Anaconda默认带有Jupyter/iPython,因此您可以通过终端启动Jupyter笔记本,如下所示:
如果您从访问的服务器运行此操作,请确保使用GNUscreen或tmux等屏幕复用器,以确保您的JupyterNotebook客户端在终端断开连接后不会停止。
我们将在本书中使用许多库,如pandas、NumPy、scikit-learn、TensorFlow、Keras、PyTorch、Dash、Matplotlib等,因此在逐步介绍配方时,我们将经常进行安装,通常如下所示:
pipinstall
condainstall
pipinstallscikit-learnpandasnumpytensorflow-gputorch请注意,对于tensorflow-gpu库,您需要有可用且准备好使用的GPU。如果没有,将其更改为tensorflow(即去掉-gpu)。
这将使用Anaconda提供的pip二进制文件来安装前述库。请注意,Keras是TensorFlow库的一部分。
或者,您可以如下运行conda包安装程序:
condainstallscikit-learnpandasnumpytensorflow-gpupytorch干得好!您已成功设置好计算机,准备开始使用即将介绍的许多精彩的配方。
Conda是一个环境和包管理器。与本书中将使用的许多其他库以及Python语言本身一样,conda是开源的,因此我们总是可以准确了解算法的操作并轻松修改它。Conda还是跨平台的,不仅支持Python,还支持R和其他语言。
有数百个专门的渠道可以与conda一起使用。这些是包含数百甚至数千种不同包的子仓库。其中一些由开发特定库或软件的公司维护。
例如,您可以按如下方式从PyTorch渠道安装pytorch包:
condainstall-cpytorchpytorch虽然尝试启用许多渠道以获取所有领域的最新技术是非常诱人的。然而,这里有一个要注意的地方。如果您启用了许多渠道,或者是非常大的渠道,conda的依赖解析可能会变得非常慢。因此,在使用许多额外的渠道时要小心,特别是如果它们包含大量的库。
您可能应该熟悉一些Jupyter的选项。这些选项在文件$HOME/.jupyter/jupyter_notebook_config.py中。如果您还没有这个文件,可以使用以下命令创建它:
>jupyternotebook--generate-config这里是/home/ben/.jupyter/jupyter_notebook_config.py的一个示例配置:
importrandom,stringfromnotebook.authimportpasswdc=get_config()c.NotebookApp.ip='*'password=''.join(random.SystemRandom().choice(string.ascii_letters+string.digits+string.punctuation)for_inrange(8))print('Thepasswordis{}'.format(password))c.NotebookApp.password=passwd(password)c.NotebookApp.open_browser=Falsec.NotebookApp.port=8888如果您将Python环境安装在希望从笔记本电脑访问的服务器上(我把我的本地计算服务器放在了阁楼上),您首先需要确保可以从其他计算机(如笔记本电脑)远程访问计算服务器(c.NotebookApp.ip='*')。
然后我们创建一个随机密码并进行配置。我们禁用在运行JupyterNotebook时自动打开浏览器的选项,然后将默认端口设置为8888。
如果你是单独工作,你不需要这个工具;但是,如果你在团队中工作,你可以使用Docker或JupyterHub将每个人放入独立的环境中。在线上,你可以找到设置Jupyter环境与Docker的设置指南。
让我们看看一些简单但非常实用的技巧,使在笔记本中工作更加舒适和高效。这些技巧适用于无论你是在本地环境还是托管的Python环境中工作。
以下简短的示例涵盖了以下内容:
如果您使用自己的安装,无论是直接在您的系统上还是在Docker环境中,都要确保它正在运行。然后将您的Colab或Jupyter笔记本实例的地址放入浏览器中,然后按下Enter键。
我们将使用以下库:
我们可以像之前一样使用pip来安装它们:
pipinstallswiftertqdmrayjoblibjaxjaxlibseabornnumbacython完成这些后,让我们来看看一些提高在Jupyter中工作效率的技巧。
这里的小贴士简洁明了,都提供了在Jupyter和Python中提高生产力的方法。
如果没有特别说明,所有的代码都需要在笔记本中运行,或者更准确地说,在一个笔记本单元格中。
让我们来看看这些小贴士吧!
在Jupyter单元格中,有很多不同的方式可以通过程序来获取代码。除了这些输入之外,还可以查看生成的输出。我们将介绍这两个方面,并可以使用全局变量来实现。
为了获取您的单元格执行历史记录,_ih列表保存了已执行单元格的代码。要获取完整的执行历史记录并将其写入文件,可以按以下步骤操作:
withopen('command_history.py','w')asfile:forcell_inputin_ih[:-1]:file.write(cell_input+'\n')如果到目前为止,我们只运行了一个单元格,其中包含print('hello,world!'),那么在我们新创建的文件command_history.py中正好会看到这样的内容:
!catcommand_history.pyprint('hello,world!')在Windows上,要打印文件的内容,可以使用type命令。
我们可以使用_ih的简写来代替最近三个单元格的内容。_i表示刚刚执行的单元格的代码,_ii表示上一个执行的单元格的代码,而_iii则表示再上一个。
为了获取最近的输出,可以使用_(单个下划线)、__(双下划线)和___(三个下划线),分别对应最近的、第二个和第三个最近的输出。
autoreload是一个内置扩展,当您对磁盘上的模块进行更改时会重新加载该模块。一旦您保存了模块,它就会自动重新加载模块。
不需要手动重新加载您的包或重新启动笔记本,使用autoreload,您只需要加载并启用该扩展,它将自动完成其工作。
我们首先加载扩展,如下所示:
%load_extautoreload然后我们按如下方式启用它:
如果您找不到错误并且错误的回溯信息不足以找到问题,调试可以大大加快错误搜索过程。让我们快速看一下调试魔法:
defnormalize(x,norm=10.0):returnx/normnormalize(5,1)你应该看到单元格输出为5.0。
然而,在函数中有一个错误,我相信细心的读者已经注意到了。让我们进行调试!
让我们看看几个更有用的魔术命令。
%%timeit-n10-r1importtimetime.sleep(1)我们看到以下输出:
pipinstalliPython-autotime请注意,这种语法适用于Colab,但不适用于标准的JupyterNotebook。安装库的永远有效的方法是使用pip或conda的魔术命令,分别是%pip和%conda。此外,如果您在行首使用感叹号,如下所示,还可以从笔记本中执行任何shell命令:
!pipinstalliPython-autotime%load_extautotimesum([iforiinrange(10)])我们将看到这个输出:time:5.62ms。
希望您能看到这在比较不同实现时有多有用。特别是在处理大量数据或复杂处理的情况下,这非常有用。
fromtqdm.notebookimporttrangefromtqdm.notebookimporttqdmtqdm.pandas()tqdmpandas集成(可选)意味着您可以看到pandasapply操作的进度条。只需将apply替换为progress_apply。
global_sum=0.0foriintrange(1000000):global_sum+=1.0tqdm提供了不同的方法来实现这一点,它们都需要最小的代码更改-有时仅仅是一个字母,正如您在前面的例子中所看到的。更通用的语法是像这样将您的循环迭代器用tqdm包装起来:
for_intqdm(range(10)):print()您应该看到类似以下截图的进度条:
Python是一种解释性语言,这对于进行实验是一个很大的优势,但对速度可能是不利的。有不同的方法来编译您的Python代码,或者使用从Python编译的代码。
%load_extCython%%cythondefmultiply(floatx,floaty):returnx*ymultiply(10,5)#50这可能不是编译代码最有用的例子。对于这样一个小函数,编译的开销太大了。您可能希望编译一些更复杂的内容。
fromnumbaimportjit@jitdefadd_numbers(N):a=0foriinrange(N):a+=iadd_numbers(10)使用autotime激活后,您应该看到类似以下的输出:
add_numbers(10)您应该看到类似以下的输出:
time:867μs还有其他提供JIT编译的库,包括TensorFlow、PyTorch和JAX,这些都可以帮助您获得类似的好处。
importjax.numpyasnpfromjaximportjitdefslow_f(x):returnx*x+x*2.0x=np.ones((5000,5000))fast_f=jit(slow_f)fast_f(x)因此,有不同的方法可以通过使用JIT或预编译来获得速度优势。我们将在接下来的部分看到一些加快代码速度的其他方法。
在本书中最重要的库之一将是pandas,一个用于表格数据的库,非常适用于提取、转换、加载(ETL)任务。Pandas是一个很棒的库,但是一旦涉及到更复杂的任务,您可能会遇到一些限制。Pandas是处理数据加载和转换的首选库。数据处理的一个问题是,即使您向量化函数或使用df.apply(),它也可能速度较慢。
通过并行化apply可以进一步提升效率。某些库,如swifter,可以通过为您选择计算后端来帮助您,或者您可以自行选择:
如我们所提到的,swifter可以为您选择后端,而不改变语法。这里是使用pandas与swifter的快速设置:
importpandasaspdimportswifterdf=pd.read_csv('some_big_dataset.csv')df['datacol']=df['datacol'].swifter.apply(some_long_running_function)通常情况下,apply()要比循环DataFrame快得多。
通过直接使用底层的NumPy数组并访问NumPy函数,例如使用df.values.apply(),您可以进一步提高执行速度。NumPy向量化真是太方便了。以下是在pandasDataFrame列上应用NumPy向量化的示例:
squarer=lambdat:t**2vfunc=np.vectorize(squarer)df['squared']=vfunc(df[col].values)这只是两种方法之一,但如果您查看下一个子配方,应该能够编写并行映射函数作为另一种选择。
要更快地完成某些事情,一种方法是同时做多件事情。有不同的方法可以使用并行处理来实现您的例程或算法。Python有许多支持此功能的库。让我们看看使用multiprocessing、Ray、joblib的几个示例,以及如何利用scikit-learn的并行处理。
multiprocessing库作为Python标准库的一部分。让我们首先来看一下它。我们这里没有提供数百万点的数据集–关键是展示一个使用模式–不过,请想象一个大数据集。以下是使用我们的伪数据集的代码片段:
#runonmultiplecoresimportmultiprocessingdataset=[{'data':'largearraysandpandasDataFrames','filename':'path/to/files/image_1.png'},#...100,000datapoints]defget_filename(datapoint):returndatapoint['filename'].split('/')[-1]pool=multiprocessing.Pool(64)result=pool.map(get_filename,dataset)使用Ray,除了多核心,还可以在多台机器上并行化,几乎不需要更改代码。Ray通过共享内存(和零拷贝序列化)高效处理数据,并使用具有容错性的分布式任务调度器:
#runonmultiplemachinesandtheircoresimportrayray.init(ignore_reinit_error=True)@ray.remotedefget_filename(datapoint):returndatapoint['filename'].split('/')[-1]result=[]fordatapointindataset:result.append(get_filename.remote(datapoint))Scikit-learn,我们之前安装的机器学习库,内部使用joblib进行并行化。以下是一个例子:
PyTorch和Keras都支持多GPU和多CPU执行。多核并行化是默认的。Keras中的多机执行在每个TensorFlow发布中都变得更加容易作为默认后端。
还有很多有用的扩展可以在不同的地方找到:
我们还想强调以下扩展:
本配方中使用或提到的其他库包括以下内容:
在本节中,我们将探讨数据探索和在三个最重要的库中建模。因此,我们将将事物分解为以下子食谱:
在这些食谱和随后的几个食谱中,我们将专注于首先涵盖Python中AI三个最重要库的基础:scikit-learn、Keras和PyTorch。通过这些,我们将介绍监督机器学习和深度神经网络等中级和基础技术。
我们将依次通过scikit-learn、Keras和PyTorch进行一个简单的分类任务。我们将同时运行这两个深度学习框架的离线模式。
这些食谱旨在介绍三个库的基础知识。但即使你已经使用过它们所有,你可能仍会发现一些感兴趣的内容。
鸢尾花数据集是仍在使用的最古老的机器学习数据集之一。它由罗纳德·费希尔于1936年发布,用于说明线性判别分析。问题是基于萼片和花瓣的宽度和长度的测量来分类三种鸢尾花物种中的一种。
尽管这是一个非常简单的问题,但基本工作流程如下:
我们假设您之前已安装了这三个库,并且您的JupyterNotebook或Colab实例正在运行。此外,我们还将使用seaborn和scikit-plot库进行可视化,因此我们也会安装它们:
!pipinstallseabornscikit-plot使用一个如此广为人知的数据集的便利之处在于,我们可以从许多包中轻松加载它,例如:
importseabornassnsiris=sns.load_dataset('iris')让我们直接开始,从数据可视化开始。
让我们首先查看数据集。
在这个示例中,我们将介绍数据探索的基本步骤。理解问题的复杂性和数据中的任何潜在问题通常是很重要的:
%matplotlibinline#this^isnotnecessaryonColabimportseabornassnssns.set(style="ticks",color_codes=True)g=sns.pairplot(iris,hue='species')这里是(在seaborn愉悦的间距和颜色中呈现):
在seaborn中绘制一对图可视化数据集中的成对关系。每个子图显示一个散点图中的一个变量与另一个变量。对角线上的子图显示变量的分布。颜色对应于三个类别。
从这个图中,特别是沿着对角线看,我们可以看到弗吉尼亚和变色鸢尾品种并不是(线性)可分的。这是我们将要努力解决的问题,我们将不得不克服它。
iris.head()我们只看到setosa,因为花的种类是按顺序列出的:
classes={'setosa':0,'versicolor':1,'virginica':2}X=iris[['sepal_length','sepal_width','petal_length','petal_width']].valuesy=iris['species'].apply(lambdax:classes[x]).values最后一行将三个对应于三个类别的字符串转换为数字-这称为序数编码。多类别机器学习算法可以处理这个问题。对于神经网络,我们将使用另一种编码方式,稍后您将看到。
经过这些基本步骤,我们准备开始开发预测模型。这些模型可以根据特征预测花的类别。我们将依次看到Python中最重要的三个机器学习库中的每一个。让我们从scikit-learn开始。
在这个示例中,我们将在scikit-learn中创建一个分类器,并检查其性能。
Scikit-learn(也称为sklearn)是自2007年以来开发的Python机器学习框架。它也是可用的最全面的框架之一,与pandas、NumPy、SciPy和Matplotlib库兼容。Scikit-learn的大部分都经过了Cython、C和C++的优化,以提高速度和效率。
请注意,并非所有的scikit-learn分类器都能处理多类问题。所有分类器都能进行二元分类,但并非所有都能处理超过两类的情况。幸运的是,随机森林模型能够处理。随机森林模型(有时称为随机决策森林)是一种可以应用于分类和回归任务的算法,是决策树的集成。其主要思想是通过在数据集的自助采样上创建决策树,并对这些树进行平均,以提高精度。
以下几行代码中的一些应该对你来说是样板代码,并且我们会一遍又一遍地使用它们:
作为良好实践的一部分,我们应该始终在未用于训练的数据样本上测试模型的性能(称为保留集或验证集)。我们可以这样做:
fromsklearn.ensembleimportRandomForestClassifierfromsklearn.model_selectionimporttrain_test_splitX_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.33,random_state=0)在这里,我们定义我们的模型超参数,并使用这些超参数创建模型实例。在我们的情况下,过程如下:
超参数是不属于学习过程但控制学习的参数。在神经网络的情况下,这包括学习率、模型架构和激活函数。
params=dict(max_depth=20,random_state=0,n_estimators=100,)clf=RandomForestClassifier(**params)在这里,我们将训练数据集传递给我们的模型。在训练过程中,调整模型的参数以获得更好的结果(其中“更好”由一个称为成本函数或损失函数的函数定义)。
对于训练,我们使用fit方法,该方法适用于所有与sklearn兼容的模型:
clf.fit(X_train,y_train)尽管模型内部有一个度量(成本函数),我们可能希望查看额外的度量。在建模的背景下,这些被称为指标。在scikit-learn中,我们可以方便地使用许多度量。对于分类问题,我们通常会查看混淆矩阵,并经常希望将其绘制出来:
fromsklearn.metricsimportplot_confusion_matrixplot_confusion_matrix(clf,X_test,y_test,display_labels=['setosa','versicolor','virginica'],normalize='true')混淆矩阵相对直观,特别是在类似于sklearn的plot_confusion_matrix()中呈现得如此清晰的情况下。基本上,我们可以看到我们的类预测与实际类别的对应情况。我们可以看到预测值与实际标签之间的对比,按类别分组,因此每个条目对应于在给定实际类别B的情况下预测为类别A的次数。在这种情况下,我们对矩阵进行了归一化处理,使每行(实际标签)的总和为一。
这是混淆矩阵:
在命中率方面表现非常好,然而,正如预期的那样,我们在区分变色鸢尾和弗吉尼亚鸢尾之间有一点小问题。
让我们继续使用Keras。
在这个示例中,我们将使用Keras预测花的种类。
Keras是一个高级接口,用于(深度)神经网络模型,可以使用TensorFlow作为后端,但也可以使用MicrosoftCognitiveToolkit(CNTK)、Theano或PlaidML。Keras是开发AI模型的接口,而不是一个独立的框架。Keras已作为TensorFlow的一部分进行了集成,因此我们从TensorFlow导入Keras。TensorFlow和Keras都是由Google开发的开源工具。
fromtensorflow.keras.modelsimportSequentialfromtensorflow.keras.layersimportDenseimporttensorflowastfdefcreate_iris_model():'''Createtheirisclassificationmodel'''iris_model=Sequential()iris_model.add(Dense(10,activation='selu',input_dim=4))iris_model.add(Dense(3,activation='softmax'))iris_model.compile(optimizer='rmsprop',loss='categorical_crossentropy',metrics=['accuracy'])iris_model.summary()returniris_modeliris_model=create_iris_model()这导致以下模型构建:
我们可以用不同的方式可视化这个模型。我们可以使用内置的Keras功能如下:
dot=tf.keras.utils.model_to_dot(iris_model,show_shapes=True,show_layer_names=True,rankdir="TB",expand_nested=True,dpi=96,subgraph=False,)dot.write_png('iris_model_keras.png')这将把网络的可视化写入一个名为iris_model_keras.png的文件中。生成的图像如下所示:
这显示我们有4个输入神经元,10个隐藏神经元和3个输出神经元,完全连接以前馈方式。这意味着输入中的所有神经元都馈送到隐藏层中的所有神经元,然后馈送到输出层中的所有神经元。
我们使用顺序模型构建(与图形相对)。顺序模型类型比图形类型更容易构建。层的构建方式相同;然而,对于顺序模型,您必须定义输入维度,input_dim。
我们使用两个密集层,中间层使用SELU激活函数,最后一层使用softmax激活函数。我们将在工作原理……部分解释这两者。至于SELU激活函数,现在可以说它提供了必要的非线性,使得神经网络能够处理更多线性不可分的变量,正如我们的情况。实际上,在隐藏层中很少使用线性(恒等函数)激活。
最终层中的每个单元(或神经元)对应于三个类别中的一个。softmax函数将输出层归一化,使其神经激活总和为1。我们使用分类交叉熵作为损失函数进行训练。交叉熵通常用于神经网络的分类问题。二元交叉熵损失用于两类问题,而分类交叉熵损失用于两类或更多类问题(交叉熵将在工作原理...部分详细解释)。
这意味着我们有三列,每一列代表一个物种,其中一个将被设为1对应于相应的类别:
y_categorical=tf.keras.utils.to_categorical(y,3)因此,我们的y_categorical的形状为(150,3)。这意味着,为了表示类别0作为标签,而不是使用0(有时称为标签编码或整数编码),我们使用了一个向量[1.0,0.0,0.0]。这被称为独热编码。每行的总和等于1。
对于神经网络,我们的特征应该以一种激活函数可以处理整个输入范围的方式进行归一化——通常是到标准分布,其平均值为0.0,标准差为1.0:
X=(X-X.mean(axis=0))/X.std(axis=0)X.mean(axis=0)此单元格的输出如下:
array([-4.73695157e-16,-7.81597009e-16,-4.26325641e-16,-4.73695157e-16])我们看到每列的均值非常接近零。我们还可以通过以下命令查看标准差:
X.std(axis=0)输出如下:
array([1.,1.,1.,1.])标准差正好为1,与预期一致。
TensorBoard是一种神经网络学习的可视化工具,用于跟踪和可视化指标、模型图、特征直方图、投影嵌入等等:
%load_exttensorboardimportoslogs_base_dir="./logs"os.makedirs(logs_base_dir,exist_ok=True)%tensorboard--logdir{logs_base_dir}此时,应在笔记本中弹出一个TensorBoard小部件。我们只需确保它获得所需的信息:
importdatetimelogdir=os.path.join(logs_base_dir,datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))tensorboard_callback=tf.keras.callbacks.TensorBoard(logdir,histogram_freq=1)X_train,X_test,y_train,y_test=train_test_split(X,y_categorical,test_size=0.33,random_state=0)iris_model.fit(x=X_train,y=y_train,epochs=150,callbacks=[tensorboard_callback])这会运行我们的训练。一个epoch是整个数据集通过神经网络的一次完整遍历。我们在这里使用了150,这有点任意。我们可以使用停止准则,在验证和训练误差开始发散,或者换句话说,过拟合发生时自动停止训练。
为了像以前一样使用plot_confusion_matrix()进行比较,我们必须将模型包装在一个实现predict()方法的类中,并具有classes_列表和一个等于分类器的_estimator_type属性。我们将在在线资料中展示这一点。
在这里,使用scikitplot函数会更容易:
importscikitplotasskplty_pred=iris_model.predict(X_test).argmax(axis=1)skplt.metrics.plot_confusion_matrix(y_test.argmax(axis=1),y_pred,normalize=True)同样地,我们归一化矩阵,以便得到分数。输出应该类似于以下内容:
这比我们在scikit-learn中之前的尝试稍逊一筹,但通过一些调整,我们可以达到一个可比较的水平,甚至可能有更好的表现。调整的例子包括改变模型的任何超参数,比如隐藏层中的神经元数,对网络架构的任何更改(添加新层),或者改变隐藏层的激活函数。
这些图显示了整个训练过程中的准确率和损失。我们还可以在TensorBoard中获得另一种对网络的可视化:
因此,分类准确性随着训练周期的增加而提高,损失逐渐减少。最终的图表显示了网络和训练架构,包括两个密集层、损失和指标,以及优化器。
在这个示例中,我们将描述一个与之前在Keras中展示的网络等效的网络,训练它,并绘制其性能。
importtorchfromtorchimportnniris_model=nn.Sequential(torch.nn.Linear(4,10),#equivalenttoDenseinkerastorch.nn.SELU(),torch.nn.Linear(10,3),torch.nn.Softmax(dim=1))print(iris_model)这与我们之前在Keras中定义的架构相同:这是一个前馈式、两层神经网络,隐藏层采用SELU激活函数,第一层有10个神经元,第二层有3个神经元。
fromtorch.autogradimportVariableX_train=Variable(torch.Tensor(X_train).float())y_train=Variable(torch.Tensor(y_train.argmax(axis=1)).long())X_test=Variable(torch.Tensor(X_test).float())y_test=Variable(torch.Tensor(y_test.argmax(axis=1)).long())y_train是我们之前创建的one-hot编码的目标矩阵。由于PyTorch交叉熵损失函数期望这样,我们将其转换回整数编码。
criterion=torch.nn.CrossEntropyLoss()#crossentropylossoptimizer=torch.optim.RMSprop(iris_model.parameters(),lr=0.01)forepochinrange(1000):optimizer.zero_grad()out=iris_model(X_train)loss=criterion(out,y_train)loss.backward()optimizer.step()ifepoch%10==0:print('numberofepoch',epoch,'loss',loss)importscikitplotasskplty_pred=iris_model(X_test).detach().numpy()skplt.metrics.plot_confusion_matrix(y_test,y_pred.argmax(axis=1),normalize=True)labels=['setosa','versicolor','virginica']ax.set_xticklabels(labels)ax.set_yticklabels(labels)这是我们得到的图表:
你的绘图可能有所不同。神经网络学习不是确定性的,因此你可能得到更好或更差的数字,或者完全不同的数字。
我们首先看看神经网络训练背后的直觉,然后稍微深入探讨一些我们将在PyTorch和Keras配方中使用的技术细节。
机器学习的基本思想是通过改变模型的参数来尽量减少误差。这种参数的调整被称为学习。在监督学习中,误差由模型预测与目标之间的损失函数计算得出。这种误差在每一步计算,并且相应地调整模型参数。
神经网络是由可调的仿射变换(f)和激活函数(sigma)组成的可组合函数逼近器:
简而言之,在具有线性激活函数的单层前馈神经网络中,模型预测由系数与所有维度输入的乘积之和给出:
这被称为感知器,它是一个线性二元分类器。下图展示了具有四个输入的简单示例:
我们也可以使用非常简单的线性代数来定义二元分类器,如下所示:
为了说明,让我们用jax来写下这些内容:
importjax.numpyasnpfromjaximportgrad,jitimportnumpy.randomasnprdefpredict(params,inputs):forW,binparams:outputs=np.dot(inputs,W)+binputs=np.tanh(outputs)returnoutputsdefconstruct_network(layer_sizes=[10,5,1]):'''Pleasemakesureyourfinallayercorrespondstothetargetdimensionality.'''definit_layer(n_in,n_out):W=npr.randn(n_in,n_out)b=npr.randn(n_out,)returnW,breturnlist(map(init_layer,layer_sizes[:-1],layer_sizes[1:]))params=construct_network()如果您看这段代码,您会发现我们可以同样地在NumPy、TensorFlow或PyTorch中进行操作。您还会注意到construct_network()函数接受一个layer_sizes参数。这是网络的超参数之一,在学习之前需要决定的内容。我们可以选择仅输出[1]来得到感知器,或者[10,1]来得到一个两层感知器。这展示了如何将网络作为一组参数以及如何从该网络获得预测。我们还没有讨论如何学习这些参数,这将引出我们的错误。
有一句古语说,“所有的模型都是错误的,但有些是有用的”。我们可以测量我们模型的误差,这可以帮助我们计算我们可以对参数进行的变化的大小和方向,以减少误差。
然后为了得到我们权重的变化,我们将使用训练中点上损失的导数:
defmse(preds,targets):returnnp.sum((preds-targets)**2)defpropagate_and_error(loss_fun):deferror(params,inputs,targets):preds=predict(params,inputs)returnloss_fun(preds,targets)returnerrorerror_grads=jit(grad(propagate_and_error(mse)))PyTorch和JAX都具有autograd功能,这意味着我们可以自动获取各种函数的导数(梯度)。
我们将在本书中遇到许多不同的激活和损失函数。在本章中,我们使用SELU激活函数。
作为我们神经网络输出层的激活函数,我们使用softmax函数。这作为输出层神经激活的归一化,总和为1.0的类激活。因此,输出可以解释为类概率。softmax激活函数定义如下:
在使用神经网络进行多类训练时,通常会训练交叉熵。多类别情况下的二元交叉熵如下所示:
您可以在每个库的网站上找到本教程中使用的更多详细信息:
我们在这里使用了一些其他库,而且在本书中我们将会遇到以下内容:
在接下来的示例中,我们将通过Keras了解一个更实际的例子。
在这个示例中,我们将加载一个数据集,然后进行探索性数据分析(EDA),如可视化分布。
我们将执行典型的预处理任务,如编码分类变量,并进行归一化和缩放以用于神经网络训练。然后我们将在Keras中创建一个简单的神经网络模型,使用生成器进行训练,绘制训练和验证性能。我们将查看一个仍然相当简单的数据集:来自UCI机器学习库的成人收入数据集(也称为人口普查收入数据集)。在这个数据集中,目标是根据人口普查数据预测某人是否年收入超过5万美元。
由于我们有一些分类变量,我们还将处理分类变量的编码。
由于这仍然是一个入门的示例,我们将详细介绍这个问题。我们将包括以下部分:
除了之前安装的库外,这个示例还需要一些其他库:
我们之前用过Seaborn进行可视化。
我们可以按以下方式安装这些库:
!pipinstallcategory_encodersminepyeli5seaborn作为给您读者的一条注:如果您同时使用pip和conda,可能会导致一些库不兼容,造成环境混乱。我们建议在有conda版本的情况下使用conda,尽管通常使用pip更快。
这个数据集已经分成了训练集和测试集。让我们从UCI下载数据集,如下所示:
我们从UCI数据集描述页面获得以下信息:
fnlwgt实际上代表最终权重;换句话说,构成条目的总人数。
请记住,这个数据集是一个知名的数据集,已经多次在科学出版物和机器学习教程中使用。我们在这里使用它来回顾一些Keras基础知识,而不需要专注于数据集。
正如我们之前提到的,我们首先加载数据集,进行一些探索性数据分析,然后在Keras中创建模型,训练它,并查看性能。
我们将这个配方分成数据加载和预处理,以及模型训练两部分。
我们将从加载训练集和测试集开始:
importpandasaspdcols=['age','workclass','fnlwgt','education','education-num','marital-status','occupation','relationship','race','sex','capital-gain','capital-loss','hours-per-week','native-country','50k']train=pd.read_csv('adult.data',names=cols)test=pd.read_csv('adult.test',names=cols)现在让我们看看数据!
train.head()这导致以下输出:
接下来,我们将查看测试数据:
test.head()看起来如下:
第一行有15列中的14个空值和1列不可用列。我们将丢弃这一行:
test.drop(0,axis=0,inplace=True)然后它消失了。
importcategory_encodersasceX=train.drop('50k',axis=1)encoder=ce.OrdinalEncoder(cols=list(X.select_dtypes(include='object').columns)[:])encoder.fit(X,train['50k'])X_cleaned=encoder.transform(X)X_cleaned.head()我们在这里分离X,即特征,和y,即目标。特征不包含标签;这就是drop()方法的目的——我们也可以使用deltrain['50k']。
这是结果:
在开始新任务时,最好进行探索性数据分析(EDA)。让我们绘制一些这些变量。
fromscipyimportstatsimportseabornassnssns.set(color_codes=True)sns.set_context('notebook',font_scale=1.5,rc={"lines.linewidth":2.0})sns.distplot(train['age'],bins=20,kde=False,fit=stats.gamma)我们将得到以下绘图:
接下来,我们再次看一下配对图。我们将所有数值变量相互绘制:
importnumpyasnpnum_cols=list(set(train.select_dtypes(include='number').columns)-set(['education-num']))+['50k']]g=sns.pairplot(train[num_cols],hue='50k',height=2.5,aspect=1,)fori,jinzip(*np.triu_indices_from(g.axes,1)):g.axes[i,j].set_visible(False)如前所述,在对角线上的配对图中,显示了单变量的直方图——即变量的分布——其色调由类别定义。这里我们有橙色与蓝色(请参见图右侧的图例)。对角线上的子图显示了两个变量之间的散点图:
如果我们看一下对角线上的年龄变量(第二行),我们会发现两个类别有不同的分布,尽管它们仍然重叠。因此,年龄似乎在我们的目标类别方面具有区分性。
我们可以看到在分类图中也是如此:
sns.catplot(x='50k',y='age',kind='box',data=train)这是生成的图形:
mask=np.zeros_like(corrs,dtype=np.bool)mask[np.triu_indices_from(mask)]=Truecmap=sns.diverging_palette(h_neg=220,h_pos=10,n=50,as_cmap=True)sns.set_context('notebook',font_scale=1.1,rc={'lines.linewidth':2.0})sns.heatmap(corrs,square=True,mask=mask,cmap=cmap,vmax=1.0,center=0.5,linewidths=.5,cbar_kws={"shrink":.5})如下所示:
corrs.loc['education-num','education']输出为0.9995095286140694。
让我们看看education中每个值的education-num的方差:
train.groupby(by='education')['education-num'].std()我们只看到零。没有变异。换句话说,education中的每个值都恰好对应于education-num中的一个值。这些变量完全相同!我们应该能够删除其中一个,例如通过deltrain['education'],或者在训练期间忽略其中一个。
UCI描述页面提到了缺失变量。现在让我们来寻找缺失的变量:
train.isnull().any()对于每个变量,我们只看到False,因此在这里我们看不到任何缺失值。
在神经网络训练中,对于分类变量,我们可以选择使用嵌入(我们将在第十章,自然语言处理中讨论)或者将它们作为一位热编码来进行输入;这意味着每个因子,每个可能的值,都被编码为一个二进制变量,指示其是否存在。让我们试试一位热编码以简化问题。
因此,首先让我们重新编码变量:
encoder=ce.OneHotEncoder(cols=list(X.select_dtypes(include='object').columns)[:])encoder.fit(X,train['50k'])X_cleaned=encoder.transform(X)x_cleaned_cols=X_cleaned.columnsx_cleaned_cols我们的x_cleaned_cols如下所示:
完成后,是时候对我们的标签进行编码了。
在以下代码块中,我们只是做了一个选择并坚持了下来:
y=np.zeros((len(X_cleaned),2))y[:,0]=train['50k'].apply(lambdax:x=='<=50K')y[:,1]=train['50k'].apply(lambdax:x=='>50K')fromsklearn.preprocessingimportStandardScalerstandard_scaler=StandardScaler()X_cleaned=standard_scaler.fit_transform(X_cleaned)X_test=standard_scaler.transform(encoder.transform(test[cols[:-1]]))importjoblibjoblib.dump([encoder,standard_scaler,X_cleaned,X_test],'adult_encoder.joblib')我们现在准备好进行训练了。
我们将创建模型,训练它,绘制性能,然后计算特征重要性。
fromtensorflow.kerasimportSequentialfromtensorflow.keras.layersimportDensemodel=Sequential()model.add(Dense(20,activation='selu',input_dim=108))model.add(Dense(2,activation='softmax'))model.compile(optimizer='rmsprop',loss='categorical_hinge',metrics=['accuracy'])model.summary()这是Keras模型的摘要:
我们将使用fit_generator()函数如下:
defadult_feed(X_cleaned,y,batch_size=10,shuffle=True):definit_batches():return(np.zeros((batch_size,X_cleaned.shape[1])),np.zeros((batch_size,y.shape[1])))batch_x,batch_y=init_batches()batch_counter=0whileTrue:#thisisforeveryepochindexes=np.arange(X_cleaned.shape[0])ifshuffle==True:np.random.shuffle(indexes)forindexinindexes:batch_x[batch_counter,:]=X_cleaned[index,:]batch_y[batch_counter,:]=y[index,:]batch_counter+=1ifbatch_counter>=batch_size:yield(batch_x,batch_y)batch_counter=0batch_x,batch_y=init_batches()如果我们还没有进行预处理,我们可以将其放入此函数中。
history=model.fit_generator(adult_feed(X_cleaned,y,10),steps_per_epoch=len(X_cleaned)//10,epochs=50)因为这是一个小数据集,所以应该相对快速;然而,如果发现这太耗时,您可以减少数据集大小或训练周期数。
我们有来自训练的输出,如损失和指标,保存在我们的history变量中。
importmatplotlib.pyplotaspltplt.plot(history.history['accuracy'])plt.plot(history.history['loss'])plt.title('ModelTraining')plt.ylabel('Accuracy')plt.xlabel('Epoch')plt.legend(['Accuracy','Loss'],loc='centerleft')请注意,在某些Keras版本中,准确率存储为accuracy而不是acc在历史记录中。
这是生成的图表:
在训练过程中,准确率在增加而损失在减少,这是一个好兆头。
fromsklearn.metricsimportroc_auc_scorepredictions=model.predict(X_test)#Pleasenotethatthetargetshaveslightlydifferentnamesinthetestsetthaninthetrainingdataset.We'llneedtotakecareofthishere:target_lookup={'<=50K.':0,'>50K.':1}y_test=test['50k'].apply(lambdax:target_lookup[x]).valuesroc_auc_score(y_test,predictions.argmax(axis=1))我们获得了0.7579310072282265作为AUC得分。76%的AUC得分可以根据任务的难度而有好坏之分。对于这个数据集来说并不差,但我们可能通过进一步调整模型来提高性能。不过,目前我们会保留它如此。
为了使其工作,我们需要一个评分函数,如下所示:
fromeli5.permutation_importanceimportget_score_importancesdefscore(data,y=None,weight=None):returnmodel.predict(data).argmax(axis=1)base_score,score_decreases=get_score_importances(score,X_test,y_test)feature_importances=np.mean(score_decreases,axis=0).mean(axis=1)现在我们可以按排序顺序打印特征重要性:
importoperatorfeature_importances_annotated={col:impforcol,impinzip(x_cleaned_cols,feature_importances)}sorted_feature_importances_annotated=sorted(feature_importances_annotated.items(),key=operator.itemgetter(1),reverse=True)fori,(k,v)inenumerate(sorted_feature_importances_annotated):print('{i}:{k}:{v}'.format(i=i,k=k,v=v))ifi>9:break我们获得类似以下列表的内容:
您的最终列表可能会与此列表不同。神经网络训练并不确定性,尽管我们可以尝试固定随机生成器种子。在这里,正如我们所预料的,年龄是一个重要因素;然而,在关系状态和婚姻状况的某些类别中,年龄之前的因素也显现出来。
我们经历了机器学习中的典型流程:加载数据集,绘制和探索数据,对分类变量进行编码和归一化预处理。然后在Keras中创建和训练神经网络模型,并绘制训练和验证性能。让我们更详细地讨论我们所做的事情。
如果你熟悉Python生成器,你就不需要解释这是什么,但也许需要一些澄清。使用生成器可以按需或在线加载数据,而不是一次性加载。这意味着你可以处理比可用内存大得多的数据集。
神经网络和Keras生成器的一些重要术语如下所示
有多种方法可以使用Keras实现生成器,例如以下方式:
对于第一个选项,我们可以使用任何生成器,但这里使用了一个带有yield函数。这意味着我们为Keras的fit_generator()函数提供了steps_per_epoch参数。
至于第二个选项,我们编写了一个继承自tensorflow.keras.utils.Sequence的类,该类实现了以下方法:
为简单起见,我们采用了前一种方法。
我们稍后将看到,使用生成器进行批数据加载通常是在线学习的一部分,即我们根据数据增量地训练模型。
关于Keras、底层TensorFlow库、在线学习和生成器,我们将在接下来的示例中详细讨论。我建议您熟悉层类型、数据加载器和预处理器、损失、指标和训练选项。所有这些都可以转移到其他框架,例如PyTorch,其应用程序编程接口(API)不同,但基本原则相同。
以下是TensorFlow/Keras文档的链接:
对于更多数据集,以下三个网站是您的好帮手:
在前一章节中跟随scikit-learn、Keras和PyTorch的尝试后,我们将进入更多端到端示例。这些示例更加先进,因为它们包括更复杂的转换和模型类型。
在线学习在这个上下文中(与基于互联网的学习相对),指的是一种包含顺序接收的训练数据的模型更新策略。这在数据集非常庞大(通常出现在图像、视频和文本中)或者由于数据变化的性质需要保持模型更新时非常有用。
在许多这些示例中,我们已经缩短了描述以突出特定概念的最显著细节。有关完整详情,请参阅GitHub上的笔记本。
在这一章中,我们将涵盖以下几个示例:
或许这些示例将在多方面为我们提供信息,并且我们将了解到关于人类交配选择机制的一些有用机制。
在OpenML网站上的数据集描述如下:
这些数据是从2002年至2004年实验性速配活动的参与者中收集的。在活动中,与异性的每位参与者进行四分钟的第一次约会。在他们的4分钟结束时,参与者被问及是否愿意再见对方。他们还被要求评价约会对象的六个属性:吸引力、诚实、智力、趣味、野心和共同兴趣。数据集还包括在过程不同阶段收集的参与者问卷数据。这些字段包括人口统计信息、约会习惯、跨关键属性的自我感知、对伴侣所认为有价值的东西的信念以及生活方式信息。
问题是根据我们对参与者及其配对的了解来预测伴侣选择。这个数据集呈现了一些可以作为说明用途的挑战:
它还包括以下内容:
在解决预测伴侣选择问题的过程中,我们将在scikit-learn中构建自定义编码器和包含所有特征及其预处理步骤的管道。
本示例的主要焦点将放在管道和转换器上。特别是,我们将为处理范围特征构建一个自定义转换器,以及为数值特征构建另一个转换器。
对于这个示例,我们将需要以下库:
为了安装它们,我们可以再次使用pip:
pipinstall-qopenmlopenml_speed_dating_pipeline_steps==0.5.5imbalanced_learncategory_encodersshapOpenML是一个旨在使数据科学和机器学习可复制的组织,因此更有利于研究。OpenML网站不仅托管数据集,还允许将机器学习结果上传到公共排行榜,条件是实现必须完全依赖开源。有兴趣的任何人都可以查看这些结果及其详细获取方式。
为了检索数据,我们将使用OpenMLPythonAPI。get_dataset()方法将下载数据集;使用get_data(),我们可以获取特征和目标的pandasDataFrames,并且方便地获取分类和数值特征类型的信息:
数据集加载完毕,并安装了库,我们已经准备好开始了。
管道是描述机器学习算法如何按顺序进行转换的一种方式,包括预处理步骤,然后应用最终预测器之前的原始数据集。我们将在本配方和本书中的其他概念示例中看到这些概念。
查看此数据集很快就会发现几个显著的特点。我们有很多分类特征。因此,在建模时,我们需要对它们进行数字编码,如第一章中的Keras中的建模和预测配方中所述,Python人工智能入门。
其中一些实际上是编码范围。这意味着这些是有序的序数类别;例如,d_interests_correlate特征包含如下字符串:
[[0-0.33],[0.33-1],[-1-0]]如果我们把这些范围视为分类变量,我们将失去有关顺序的信息,以及有关两个值之间差异的信息。但是,如果我们将它们转换为数字,我们将保留这些信息,并能够在其上应用其他数值转换。
我们将实现一个转换器插入到sklearn管道中,以便将这些范围特征转换为数值特征。转换的基本思想是提取这些范围的上限和下限,如下所示:
defencode_ranges(range_str):splits=range_str[1:-1].split('-')range_max=splits[-1]range_min='-'.join(splits[:-1])returnrange_min,range_maxexamples=X['d_interests_correlate'].unique()[encode_ranges(r)forrinexamples]我们将以示例为例:
这里是RangeTransformer的简化代码:
请注意如何使用fit()和transform()方法。在fit()方法中,我们不需要做任何事情,因为我们总是应用相同的静态规则。transform()方法应用这个规则。我们之前已经看过例子。在transform()方法中,我们会迭代列。这个转换器还展示了典型的scikit-learn并行化模式的使用。另外,由于这些范围经常重复,并且数量并不多,我们将使用缓存,以便不必进行昂贵的字符串转换,而是可以在处理完范围后从内存中检索范围值。
在scikit-learn中自定义转换器的一个重要事项是,它们应该继承自BaseEstimator和TransformerMixin,并实现fit()和transform()方法。稍后,我们将需要get_feature_names()方法来获取生成特征的名称。
让我们实现另一个转换器。您可能已经注意到,我们有不同类型的特征,看起来涉及相同的个人属性:
似乎很明显,任何这些特征之间的差异可能是显著的,比如真诚的重要性与某人评估潜在伴侣的真诚程度之间的差异。因此,我们的下一个转换器将计算数值特征之间的差异,这有助于突出这些差异。
这些特征是从其他特征派生的,并结合了来自两个(或更多特征)的信息。让我们看看NumericDifferenceTransformer特征是什么样的:
这个转换器与RangeTransformer结构非常相似。请注意列之间的并行化。__init__()方法的一个参数是用于计算差异的函数,默认情况下是operator.sub()。operator库是Python标准库的一部分,它将基本运算符实现为函数。sub()函数做的就是它听起来像:
importoperatoroperator.sub(1,2)==1-2#True这为我们提供了标准操作符的前缀或功能语法。由于我们可以将函数作为参数传递,这使我们能够指定列之间的不同操作符。
这一次的fit()方法只是收集数值列的名称,我们将在transform()方法中使用这些名称。
我们将使用ColumnTransformer和管道将这些转换器组合在一起。但是,我们需要将列与它们的转换关联起来。我们将定义不同的列组:
range_cols=[colforcolinX.select_dtypes(include='category')ifX[col].apply(lambdax:x.startswith('[')ifisinstance(x,str)elseFalse).any()]cat_columns=list(set(X.select_dtypes(include='category').columns)-set(range_cols))num_columns=list(X.select_dtypes(include='number').columns)现在我们有范围列、分类列和数值列,我们可以为它们分配管道步骤。
在我们的案例中,我们将其组合如下,首先是预处理器:
fromimblearn.ensembleimportBalancedRandomForestClassifierfromsklearn.feature_selectionimportSelectKBest,f_classiffromsklearn.composeimportColumnTransformerfromsklearn.pipelineimportPipelinefromsklearn.preprocessingimportFunctionTransformerimportcategory_encodersasceimportopenml_speed_dating_pipeline_stepsaspipeline_stepspreprocessor=ColumnTransformer(transformers=[('ranges',Pipeline(steps=[('impute',pipeline_steps.SimpleImputerWithFeatureNames(strategy='constant',fill_value=-1)),('encode',pipeline_steps.RangeTransformer())]),range_cols),('cat',Pipeline(steps=[('impute',pipeline_steps.SimpleImputerWithFeatureNames(strategy='constant',fill_value='-1')),('encode',ce.OneHotEncoder(cols=None,#allfeaturesthatitgivenbyColumnTransformerhandle_unknown='ignore',use_cat_names=True))]),cat_columns),('num',pipeline_steps.SimpleImputerWithFeatureNames(strategy='median'),num_columns),],remainder='drop',n_jobs=-1)然后,我们将预处理放入管道中,与估算器一起:
defcreate_model(n_estimators=100):returnPipeline(steps=[('preprocessor',preprocessor),('numeric_differences',pipeline_steps.NumericDifferenceTransformer()),('feature_selection',SelectKBest(f_classif,k=20)),('rf',BalancedRandomForestClassifier(n_estimators=n_estimators,))])这里是测试集的表现:
fromsklearn.metricsimportroc_auc_score,confusion_matrixfromsklearn.model_selectionimporttrain_test_splitX_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.33,random_state=42,stratify=y)clf=create_model(50)clf.fit(X_train,y_train)y_predicted=clf.predict(X_test)auc=roc_auc_score(y_test,y_predicted)print('auc:{:.3f}'.format(auc))我们得到以下性能作为输出:
auc:0.779这是一个非常好的性能,您可以将其与OpenML排行榜进行比较看到。
有几点需要指出关于我们的方法。正如我们之前所说,我们有缺失值,因此必须用其他值填充(意思是替换)缺失值。在这种情况下,我们用-1替换缺失值。对于分类变量来说,这将成为一个新类别,而对于数值变量来说,这将成为分类器必须处理的特殊值。
ColumnTransformer是scikit-learn版本0.20中引入的一个期待已久的功能。从那时起,ColumnTransformer经常可以像这样看到,例如:
fromsklearn.composeimportColumnTransformer,make_column_transformerfromsklearn.preprocessingimportStandardScaler,OneHotEncoderfeature_preprocessing=make_column_transformer((StandardScaler(),['column1','column2']),(OneHotEncoder(),['column3','column4','column5']))feature_preprocessing可以像往常一样使用fit()、transform()和fit_transform()方法:
processed_features=feature_preprocessing.fit_transform(X)这里,X意味着我们的特征。
或者,我们可以像这样将ColumnTransformer作为管道的一步:
由于NumericDifferenceTransformer可以提供大量特征,我们将增加一步基于模型的特征选择。
RangeTransformer和NumericDifferenceTransformer也可以使用scikit-learn中的FunctionTransformer实现。
ColumnTransformer对于pandasDataFrames或NumPy数组特别方便,因为它允许为不同特征子集指定不同操作。然而,另一个选项是FeatureUnion,它允许将来自不同转换的结果连接在一起。要了解另一种方法如何将我们的操作链在一起,请查看我们存储库中的PandasPicker。
在这个食谱中,问题的目标是预测艾奥瓦州埃姆斯的房价,给定描述房屋、区域、土地、基础设施、公用设施等81个特征。埃姆斯数据集具有良好的分类和连续特征组合,适量适度,并且最重要的是,不像其他类似数据集(例如波士顿房价数据集)那样受潜在的红线问题或数据输入问题的困扰。我们将在此处集中讨论PyTorch建模的主要方面。我们将进行在线学习,类似于Keras,在第一章中的Keras中的建模和预测食谱中。如果您想查看某些步骤的更多详细信息,请查看我们在GitHub上的笔记本。
作为一个额外的内容,我们还将演示在PyTorch中开发的模型的神经元重要性。您可以在PyTorch中尝试不同的网络架构或模型类型。这个食谱的重点是方法论,而不是对最佳解决方案的详尽搜索。
为了准备这个食谱,我们需要做一些准备工作。我们将像之前的食谱一样下载数据,在scikit-learn中转换数据,并按以下步骤进行一些预处理:
让我们看看这些特征:
importpandasaspddata_ames=pd.DataFrame(data.data,columns=data.feature_names)data_ames['SalePrice']=data.targetdata_ames.info()这是DataFrame的信息:
不过,我们还会使用一个库,captum,它允许检查PyTorch模型的特征和神经元重要性:
!pipinstallcaptum还有一件事。我们假设您的计算机有GPU。如果您的计算机没有GPU,我们建议您在Colab上尝试此方法。在Colab中,您需要选择一个带GPU的运行时类型。
在所有这些准备工作之后,让我们看看如何预测房屋价格。
Ames房屋数据集是一个小到中等规模的数据集(1,460行),包含81个特征,既包括分类特征又包括数值特征。没有缺失值。
在之前的Keras配方中,我们已经看到了如何缩放变量。在这里缩放很重要,因为所有变量具有不同的尺度。分类变量需要转换为数值类型,以便将它们输入到我们的模型中。我们可以选择独热编码,其中我们为每个分类因子创建虚拟变量,或者序数编码,其中我们对所有因子进行编号,并用这些编号替换字符串。我们可以像处理任何其他浮点变量一样输入虚拟变量,而序数编码则需要使用嵌入,线性神经网络投影,重新排列多维空间中的类别。
我们选择嵌入路线:
现在,我们可以将数据分割为训练集和测试集,就像我们在之前的示例中所做的那样。在这里,我们还添加了一个数值变量的分层。这确保了不同部分(五个部分)在训练集和测试集中的等量包含:
np.random.seed(12)fromsklearn.model_selectionimporttrain_test_splitbins=5sale_price_bins=pd.qcut(X['SalePrice'],q=bins,labels=list(range(bins)))X_train,X_test,y_train,y_test=train_test_split(X.drop(columns='SalePrice'),X['SalePrice'],random_state=12,stratify=sale_price_bins)在继续之前,让我们使用一个与模型无关的技术来查看特征的重要性。
然而,在运行任何东西之前,让我们确保我们在GPU上运行:
device=torch.device('cuda')torch.backends.cudnn.benchmark=True让我们构建我们的PyTorch模型,类似于《Python中的人工智能入门》第一章中的在scikit-learn、Keras和PyTorch中分类的配方。
我们将使用PyTorch实现一个带批量输入的神经网络回归。这将涉及以下步骤:
没有进一步的序言,让我们开始吧:
fromtorch.autogradimportVariablenum_features=list(set(num_cols)-set(['SalePrice','Id']))X_train_num_pt=Variable(torch.cuda.FloatTensor(X_train[num_features].values))X_train_cat_pt=Variable(torch.cuda.LongTensor(X_train[cat_cols].values))y_train_pt=Variable(torch.cuda.FloatTensor(y_train.values)).view(-1,1)X_test_num_pt=Variable(torch.cuda.FloatTensor(X_test[num_features].values))X_test_cat_pt=Variable(torch.cuda.LongTensor(X_test[cat_cols].values).long())y_test_pt=Variable(torch.cuda.FloatTensor(y_test.values)).view(-1,1)这确保我们将数值和分类数据加载到不同的变量中,类似于NumPy。如果在单个变量(数组/矩阵)中混合数据类型,它们将变成对象。我们希望将数值变量作为浮点数加载,并将分类变量作为长整型(或整型)索引类别。我们还要将训练集和测试集分开。
criterion=torch.nn.MSELoss().to(device)optimizer=torch.optim.SGD(house_model.parameters(),lr=0.001)data_batch=torch.utils.data.TensorDataset(X_train_num_pt,X_train_cat_pt,y_train_pt)dataloader=torch.utils.data.DataLoader(data_batch,batch_size=10,shuffle=True)我们设置批量大小为10。现在我们可以进行训练。
由于这似乎比我们在《Python人工智能入门第一章中看到的Keras中的分类》一书中的示例要冗长得多,我们对此代码进行了详细的注释。基本上,我们必须在每个epoch上进行循环,并在每个epoch内执行推断、计算误差,并根据误差应用优化器进行调整。
这是没有内部训练循环的epoch循环:
fromtqdm.notebookimporttrangetrain_losses,test_losses=[],[]n_epochs=30forepochintrange(n_epochs):train_loss,test_loss=0,0#trainingcodewillgohere:#<...>#printtheerrorsintrainingandtest:ifepoch%10==0:print('Epoch:{}/{}\t'.format(epoch,1000),'TrainingLoss:{:.3f}\t'.format(train_loss/len(dataloader)),'TestLoss:{:.3f}'.format(test_loss/len(dataloader)))训练是在所有训练数据的批次循环内执行的。它看起来如下所示:
for(x_train_num_batch,x_train_cat_batch,y_train_batch)indataloader:#predictybypassingxtothemodel(x_train_num_batch,x_train_cat_batch,y_train_batch)=(x_train_num_batch.to(device),x_train_cat_batch.to(device),y_train_batch.to(device))pred_ytrain=house_model.forward(x_train_num_batch,x_train_cat_batch)#calculateandprintloss:loss=torch.sqrt(criterion(pred_ytrain,y_train_batch))#zerogradients,performabackwardpass,#andupdatetheweights.optimizer.zero_grad()loss.backward()optimizer.step()train_loss+=loss.item()withtorch.no_grad():house_model.eval()pred_ytest=house_model.forward(X_test_num_pt,X_test_cat_pt)test_loss+=torch.sqrt(criterion(pred_ytest,y_test_pt))train_losses.append(train_loss/len(dataloader))test_losses.append(test_loss/len(dataloader))这是我们得到的输出。TQDM为我们提供了一个有用的进度条。在每个第十个epoch,我们打印一个更新,显示训练和验证性能:
请注意,我们对nn.MSELoss取平方根,因为PyTorch中的nn.MSELoss定义如下:
((input-target)**2).mean()让我们绘制模型在训练和验证数据集上的表现情况:
plt.plot(np.array(train_losses).reshape((n_epochs,-1)).mean(axis=1),label='Trainingloss')plt.plot(np.array(test_losses).reshape((n_epochs,-1)).mean(axis=1),label='Validationloss')plt.legend(frameon=False)plt.xlabel('epochs')plt.ylabel('MSE')以下图表显示了结果绘图:
我们在验证损失停止减少之前及时停止了训练。
我们还可以对目标变量进行排名和分箱,并将预测结果绘制在其上,以便查看模型在整个房价范围内的表现。这是为了避免在回归中出现的情况,特别是当损失函数为MSE时,只能在接近均值的中等价值范围内进行良好预测,而对其他任何值都表现不佳。您可以在GitHub笔记本中找到此代码。这称为提升图表(这里有10个分箱):
深度学习神经网络框架使用不同的优化算法。其中流行的有随机梯度下降(SGD)、均方根传播(RMSProp)和自适应矩估计(ADAM)。
我们将随机梯度下降定义为我们的优化算法。或者,我们也可以定义其他优化器:
opt_SGD=torch.optim.SGD(net_SGD.parameters(),lr=LR)opt_Momentum=torch.optim.SGD(net_Momentum.parameters(),lr=LR,momentum=0.6)opt_RMSprop=torch.optim.RMSprop(net_RMSprop.parameters(),lr=LR,alpha=0.1)opt_Adam=torch.optim.Adam(net_Adam.parameters(),lr=LR,betas=(0.8,0.98))SGD与梯度下降的工作方式相同,只是每次只对单个示例进行操作。有趣的是,其收敛性类似于梯度下降,并且对计算机内存的要求更低。
RMSProp的工作原理是根据梯度的符号来调整算法的学习率。最简单的变体检查最后两个梯度的符号,然后根据它们是否相同增加或减少学习率的一小部分。
ADAM是最流行的优化器之一。它是一种自适应学习算法,根据梯度的一阶和二阶矩来调整学习率。
Captum是一个工具,可以帮助我们理解在数据集上学习的神经网络模型的细枝末节。它可以协助学习以下内容:
这在学习可解释神经网络中非常重要。在这里,使用了集成梯度来理解特征重要性。后来,还通过使用层导纳方法展示了神经元的重要性。
鉴于我们已经定义并训练了神经网络,让我们使用captum库找出重要的特征和神经元:
fromcaptum.attrimport(IntegratedGradients,LayerConductance,NeuronConductance)house_model.cpu()forembeddinginhouse_model.embeddings:embedding.cpu()house_model.cpu()ing_house=IntegratedGradients(forward_func=house_model.forward,)#X_test_cat_pt.requires_grad_()X_test_num_pt.requires_grad_()attr,delta=ing_house.attribute(X_test_num_pt.cpu(),target=None,return_convergence_delta=True,additional_forward_args=X_test_cat_pt.cpu())attr=attr.detach().numpy()现在,我们有一个特征重要性的NumPy数组。
也可以使用这个工具获取层和神经元的重要性。让我们看看我们第一层神经元的重要性。我们可以传递house_model.act1,这是第一线性层上的ReLU激活函数:
cond_layer1=LayerConductance(house_model,house_model.act1)cond_vals=cond_layer1.attribute(X_test,target=None)cond_vals=cond_vals.detach().numpy()df_neuron=pd.DataFrame(data=np.mean(cond_vals,axis=0),columns=['NeuronImportance'])df_neuron['Neuron']=range(10)它看起来是这样的:
图表显示神经元的重要性。显然,有一个神经元并不重要。
我们还可以通过对我们之前获得的NumPy数组进行排序来查看最重要的变量:
df_feat=pd.DataFrame(np.mean(attr,axis=0),columns=['featureimportance'])df_feat['features']=num_featuresdf_feat.sort_values(by='featureimportance',ascending=False).head(10)这里是最重要的10个变量的列表:
特征重要性通常有助于我们理解模型,并且剪枝模型以使其变得不那么复杂(希望也不那么过拟合)。
我们将通过下载数据集和安装几个库来准备我们的配方。
再次,我们将从OpenML获取数据:
对于每一行,描述一个人,我们有不同的特征,数值和分类的,告诉我们关于人口统计和顾客历史的信息。
为了模拟顾客签署我们的产品的可能性,我们将使用专门用于在线模型的scikit-multiflow包。我们还将再次使用category_encoders包:
!pipinstallscikit-multiflowcategory_encoders有了这两个库,我们可以开始这个配方了。
我们需要实现一个探索策略和一个正在不断更新的模型。我们正在使用在线版本的随机森林,HoeffdingTree,作为我们的模型。我们正在估计每一步的不确定性,并基于此返回下一个要呼叫的候选人。
与往常一样,我们需要定义一些预处理步骤:
fromsklearn.composeimportColumnTransformerfromsklearn.preprocessingimportFunctionTransformerimportcategory_encodersasceordinal_encoder=ce.OrdinalEncoder(cols=None,#allfeaturesthatitencountershandle_missing='return_nan',handle_unknown='ignore').fit(X)preprocessor=ColumnTransformer(transformers=[('cat',ordinal_encoder,categorical_features),('num',FunctionTransformer(validate=False),numeric_features)])preprocessor=preprocessor.fit(X)然后我们来到我们的主动学习方法本身。这受到了modAL.models.ActiveLearner的启发:
创建主动学习管道的方法如下:
active_pipeline=ActivePipeline(HoeffdingTreeClassifier(),preprocessor,class_weights.to_dict())active_pipeline.model.classes=[0,1,2]我们可以使用这个设置在我们的数据集上运行不同的模拟。例如,我们可以比较大量实验(0.5开发利用)与仅开发利用(1.0),或者在第一批之后根本不学习。基本上我们通过一个循环进行:
您可以在GitHub的笔记本中看到一个例子。
值得深入探讨这个配方中使用的一些概念和策略。
主动学习意味着我们可以积极查询更多信息;换句话说,探索是我们策略的一部分。在我们必须主动决定学习什么以及我们学到了什么不仅影响我们的模型学习量和质量,还影响我们可以获得的投资回报的场景中,这是非常有用的。
霍夫丁边界定义如下:
由于我们处理的是不平衡数据集,让我们使用类权重。这基本上意味着我们在少数类(注册)上采样,而在多数类(未注册)上进行下采样。
类权重的公式如下:
同样,在Python中,我们可以编写以下内容:
class_weights=len(X)/(y.astype(int).value_counts()*2)然后我们可以使用这些类权重进行抽样。
我们将以几个额外的指针来结束本文。
替代制裁的矫正罪犯管理剖析系统(COMPAS)是一种商业算法,根据犯罪案件记录为刑事被告分配风险分数。该风险分数对应于再犯(累犯)和犯下暴力犯罪的可能性,并且此分数用于法庭上帮助确定判决。ProPublica组织在佛罗里达州的一个县获得了约7,000人的分数和数据。等待2年后,他们审计了2016年的COMPAS模型,并发现了模型存在非常令人担忧的问题。ProPublica的发现之一是在性别、种族和族裔方面存在歧视问题,特别是在对少数民族过度预测累犯率的情况下。
歧视对AI系统构成重大问题,说明了审核模型及其输入数据的重要性。如果忽视了这种偏见,基于人类决策建立的模型将放大人类偏见。我们不仅从法律角度考虑,而且从道德角度来说,我们希望构建不会给某些群体带来不利的模型。这为模型构建提出了一个有趣的挑战。
在我们开始之前,我们首先会下载数据,提及预处理中的问题,并安装所需的库。
让我们获取数据:
我们可以突出几个数据集中的问题:
importdatetimeindexes=data.compas_screening_date<=pd.Timestamp(datetime.date(2014,4,1))assertindexes.sum()==6216data=data[indexes]在本教程中我们将使用几个库,可以按以下方式安装:
!pipinstallcategory-encoderscategory-encoders是一个提供超出scikit-learn所提供的分类编码功能的库。
让我们先了解一些基本术语。我们需要为公平性制定度量标准。但是公平性(或者,如果我们看不公平性,偏见)意味着什么?
公平性可以指两个非常不同的概念:
第一个也被称为等几率,而后者指的是等假阳性率。平等机会意味着每个群体都应该有同样的机会,而平等结果策略则意味着表现不佳的群体应该相对其他群体更加宽容或者有更多机会。
我们将采用假阳性率的概念,这在直觉上具有吸引力,并且在许多司法管辖区的平等就业机会案例中被确立为法律。关于这些术语,我们将在参见部分提供一些资源。
因此,影响计算的逻辑基于混淆矩阵中的数值,最重要的是假阳性,我们刚刚提到的。这些情况被预测为阳性,实际上却是阴性;在我们的情况下,被预测为再犯者的人,实际上不是再犯者。让我们为此编写一个函数:
defconfusion_metrics(actual,scores,threshold):y_predicted=scores.apply(lambdax:x>=threshold).valuesy_true=actual.valuesTP=((y_true==y_predicted)&(y_predicted==1)).astype(int)FP=((y_true!=y_predicted)&(y_predicted==1)).astype(int)TN=((y_true==y_predicted)&(y_predicted==0)).astype(int)FN=((y_true!=y_predicted)&(y_predicted==0)).astype(int)returnTP,FP,TN,FN现在我们可以使用这个函数来总结特定群体的影响,代码如下:
这表达了一个期望,即受保护群体(非裔美国人)的指标应该与常规群体(白种人)的指标相同。在这种情况下,我们将得到1.0.如果受保护群体的指标与常规群体相差超过20个百分点(即低于0.8或高于1.2),我们将标记它为显著的歧视。
规范组:一个规范组,也称为标准化样本或规范组,是代表统计数据意图比较的人群样本。在偏见的语境中,其法律定义是具有最高成功率的群体,但在某些情况下,整个数据集或最频繁的群体被作为基线。实用上,我们选择白人群体,因为他们是最大的群体,也是模型效果最好的群体。
在前述函数中,我们按敏感群体计算假阳性率。然后,我们可以检查非洲裔美国人与白人之间的假阳性率是否不成比例,或者非洲裔美国人的假阳性率是否高得多。这意味着非洲裔美国人被频繁标记为再次犯罪者的情况。我们发现确实如此:
这张表格的简短解释如下:
让我们进行一些预处理,然后我们将构建模型:
fromsklearn.feature_extraction.textimportCountVectorizerfromcategory_encoders.one_hotimportOneHotEncoderfromsklearn.model_selectionimporttrain_test_splitfromsklearn.preprocessingimportStandardScalercharge_desc=data['c_charge_desc'].apply(lambdax:xifisinstance(x,str)else'')count_vectorizer=CountVectorizer(max_df=0.85,stop_words='english',max_features=100,decode_error='ignore')charge_desc_features=count_vectorizer.fit_transform(charge_desc)one_hot_encoder=OneHotEncoder()charge_degree_features=one_hot_encoder.fit_transform(data['c_charge_degree'])data['race_black']=data['race'].apply(lambdax:x=='African-American').astype(int)stratification=data['race_black']+(data['is_recid']).astype(int)*2CountVectorizer计算单词的词汇量,指示每个单词的使用频率。这被称为词袋表示法,并且我们将其应用于被指控描述列。我们排除英语停用词,这些词非常常见,例如介词(如on或at)和人称代词(例如I或me);我们还将词汇限制为100个词以及不出现在超过85%字段中的词汇。
我们对被指控程度应用虚拟编码(单热编码)。
为什么我们要使用两种不同的转换方式?基本上,描述是关于为什么某人被指控犯罪的文字描述。每个字段都是不同的。如果我们使用单热编码,每个字段将获得自己的虚拟变量,我们就无法看到字段之间的任何共性。
最后,我们创建一个新变量以分层化,以确保在训练和测试数据集中,再犯(我们的目标变量)和某人是否是非洲裔美国人的比例相似。这将帮助我们计算指标以检查歧视性:
y=data['is_recid']X=pd.DataFrame(data=np.column_stack([data[['juv_fel_count','juv_misd_count','juv_other_count','priors_count','days_b_screening_arrest']],charge_degree_features,charge_desc_features.todense()]),columns=['juv_fel_count','juv_misd_count','juv_other_count','priors_count','days_b_screening_arrest']\+one_hot_encoder.get_feature_names()\+count_vectorizer.get_feature_names(),index=data.index)X['jailed_days']=(data['c_jail_out']-data['c_jail_in']).apply(lambdax:abs(x.days))X['waiting_jail_days']=(data['c_jail_in']-data['c_offense_date']).apply(lambdax:abs(x.days))X['waiting_arrest_days']=(data['c_arrest_date']-data['c_offense_date']).apply(lambdax:abs(x.days))X.fillna(0,inplace=True)columns=list(X.columns)X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.33,random_state=42,stratify=stratification)#westratifybyblackandthetarget我们进行一些数据工程,导出变量来记录某人在监狱中度过了多少天,等待了多久的审判,或者等待了多久的逮捕。
我们将使用类似于我们在《Python中的人工智能入门》第一章中遇到的方法来构建一个jax神经网络模型。这次,我们将进行一个完整的实现:
让我们训练它并检查性能。请注意,我们输入X、y和敏感训练,它是用于训练数据集的非洲裔美国人的指示变量:
sensitive_train=X_train.join(data,rsuffix='_right')['race_black']jax_learner=JAXLearner([X.values.shape[1],100,1])jax_learner.fit(X_train.values,y_train.values,sensitive_train.values)我们将统计数据可视化如下:
X_predicted=pd.DataFrame(data=jax_learner.predict(X_test.values)*10,columns=['score'],index=X_test.index).join(data[['sex','race','is_recid']],rsuffix='_right')calculate_impacts(X_predicted,score_col='score')这是我们得到的表格:
我们可以看到非洲裔美国人的不成比例的误报率非常接近(甚至低于)1.0,这正是我们想要的。测试集很小,没有足够的样本来计算亚裔和美洲原住民的有意义统计数据。然而,如果我们希望确保这两个群体的误报率相等,我们可以扩展我们的方法来涵盖这两个群体。
这种方法能起作用的关键是自定义的目标函数或损失函数。在scikit-learn中,这远非易事,尽管我们将在接下来的部分展示一个实现。
通常,有不同的可能性来实现自己的成本或损失函数。
对于scikit-learn,通常没有公共API来定义自己的损失函数。对于许多算法,只有一个选择,有时候还有几个备选项。在使用树的分裂标准时,损失函数必须是高效的,只有Cython实现能保证这一点。这仅在非公共API中可用,这意味着使用起来更加困难。
最后,当没有(直接)方法来实现自定义损失时,可以将算法包装在如遗传算法等通用优化方案中。
在神经网络中,只要提供一个可微的损失函数,你可以插入任何你想要的东西。
基本上,我们能够将不利影响编码为具有均方误差(MSE)函数的惩罚项。这基于我们之前提到的MSE,但具有不利影响的惩罚项。让我们再看一下损失函数:
计算方法如下:
对于2,MSE可以简单地通过将预测和目标分别乘以sensitive来计算。这将取消所有sensitive等于0的点。
对于4,看似可以取消总体误差,但实际上我们发现它似乎确实有效。我们可能也可以添加这两项,以便给两种错误赋予类似的重要性。
我们使用Jax中的autograd功能来进行微分。
在接下来的内容中,我们将使用非公开的scikit-learnAPI来为决策树实现自定义分裂标准。我们将使用这个来训练一个带有COMPAS数据集的随机森林模型:
ensemble=[DecisionTreeClassifier(criterion=PenalizedHellingerDistanceCriterion(2,np.array([2,2],dtype='int64')),max_depth=100)foriinrange(100)]formodelinensemble:model.fit(X_train,X_train.join(data,rsuffix='_right')[['is_recid','race_black']])Y_pred=np.array([model.predict(X_test)formodelinensemble])predictions2=Y_pred.mean(axis=0)这给了我们一个AUC值为0.62:
我们可以看到,尽管我们已经取得了很大进展,但我们并没有完全消除所有偏见。30%(非洲裔美国人的DFP)仍然被认为是不可接受的。我们可以尝试不同的改进或采样策略来改善结果。不幸的是,我们不能在实践中使用这个模型。
例如,解决此问题的一种方法是在随机森林中进行模型选择。由于每棵树都有其独特的分类方式,我们可以计算每棵单独树或树组合的不利影响统计数据。我们可以删除树,直到剩下一组满足我们不利影响条件的树。这超出了本章的范围。
您可以在不同的地方进一步了解算法公平性。有大量关于公平性的文献可供参考:
有不同的Python库可用于解决偏差(或反算法公平性)问题:
为了准备这个示例,我们将安装库并下载数据集。
我们将使用statsmodels库和Prophet:
该数据集是关于大气中二氧化碳早期记录之一。正如稍后将观察到的那样,这些数据呈现出正弦模式,冬季二氧化碳浓度上升,夏季由于植物和植被减少而下降:
X,y=load_mauna_loa_atmospheric_co2()数据集包含从1958年到2001年在夏威夷毛纳罗亚观测站测量的平均CO[2]浓度。我们将根据这些数据对CO[2]浓度进行建模。
df_CO2=pd.DataFrame(data=X,columns=['Year'])df_CO2['CO2inppm']=ylm=sns.lmplot(x='Year',y='CO2inppm',data=df_CO2,height=4,aspect=4)fig=lm.figfig.suptitle('CO2conc.mauna_loa1958-2001',fontsize=12)这里是图表:
importstatsmodels.apiasstmdd=stm.datasets.co2.load_pandas()co2=d.dataco2.head()y=co2['co2']y=y.fillna(y.interpolate())#Fillmissingvaluesbyinterpolation现在我们已经对分解数据进行了预处理,让我们继续进行:
我们将对数据集拟合ARIMA和SARIMA模型。
我们将定义我们的两个模型,并将其应用于测试数据集中的每个点。在这里,我们迭代地在所有点上拟合模型并预测下一个点,作为一步预测。
#takinga90/10splitbetweentrainingandtesting:future=int(len(y)*0.9)print('numberoftrainsamples:%dtestsamples%d'(future,len(y)-future))train,test=y[:future],y[future:]这使我们得到了468个样本用于训练和53个用于测试。
我们可以使用阿卡信息准则(AIC)进行参数探索,它反映了模型质量相对于模型中参数数量的情况。statsmodels中拟合函数返回的模型对象包括AIC值,因此我们可以在一系列参数上进行网格搜索,选择最小化AIC的模型。
为了解释我们使用的模型ARIMA和SARIMA,我们将逐步进行,并依次解释每个模型:
ARIMA和SARIMA基于ARMA模型,这是一个自回归移动平均模型。让我们简要地了解一些基础知识。
ARMA是一个线性模型,分为两部分。首先是自回归线性模型:
不同的ARMA扩展用于解决前两个限制,这就是ARIMA和SARIMA发挥作用的地方。
ARIMA(p,d,q)代表自回归积分移动平均。它带有三个参数:
积分是指差异化。为了稳定均值,我们可以取连续观测之间的差异。这也可以去除趋势或消除季节性。可以写成如下形式:
SARIMA代表季节性ARIMA,是ARIMA的扩展,因为它还考虑了数据的季节性。
SARIMA(p,d,q)(P,D,Q)m包含ARIMA的非季节性参数和额外的季节性参数。大写字母P、D和Q注释了季节性移动平均和自回归分量,其中m是每个季节中的周期数。通常这是一年中的周期数;例如m=4表示季度季节效应,意味着D表示观测Xt和Xt-m之间的季节性差异,P和Q表示具有m个滞后的线性模型。
在Python中,statsmodels库提供了基于数据季节性的信号分解方法。
让我们看看如何使用它:
fromfbprophetimportProphettrain_df=df_CO2_fb['1958':'1997']test_df=df_CO2_fb['1998':'2001']train_df=train_df.reset_index()test_df=test_df.reset_index()Co2_model=Prophet(interval_width=0.95)Co2_model.fit(train_df)train_forecast=Co2_model.predict(train_df)test_forecast=Co2_model.predict(test_df)fut=Co2_model.make_future_DataFrame(periods=12,freq='M')forecast_df=Co2_model.predict(fut)Co2_model.plot(forecast_df)这里是我们的模型预测:
我们得到与ARIMA/SARIMA模型相似的分解,即趋势和季节性组成部分:
我们在这个示例中使用了以下库:
为了从数据中获取知识,理解数据集背后的结构非常重要。我们对数据集的表示方式可以使其更直观地在某种方式下工作,并因此更容易从中获得洞察力。工具的法则指出,当手持锤子时,一切似乎都像是钉子(基于安德鲁·马斯洛的《科学心理学》,1966年),这是关于适应工具的趋势。然而,并没有银弹,因为所有方法都有它们在特定问题下的缺点。因此,了解可用工具库中的基本方法对于识别应该使用锤子而不是螺丝刀的情况至关重要。
在本章中,我们将探讨不同的数据表示方法,无论是为了可视化客户群体以及找到异常模式,还是投射数据以强调差异,根据客户自己以及其他客户的先前选择向客户推荐产品,并通过相似性识别欺诈者社区。
具体而言,我们将提供以下配方:
对于这个配方,我们将使用信用风险数据集,通常被完整称为德国信用风险数据集。每行描述了一位借款人的信息,给出了关于这个人的几个属性,并告诉我们这个人是否还了贷款(即信用是否良好或不良风险)。
我们需要按如下步骤下载和加载德国信用数据:
!pipinstalldython现在我们可以使用dython库玩弄德国信用数据集,将其可视化,并看看如何将内部的人群聚合到不同的群组中。
我们首先将可视化数据集,进行一些预处理,并应用聚类算法。我们将试图从这些聚类中获得见解,并在新的见解基础上重新进行聚类。
我们将从可视化特征开始:
我们无法真正看到清晰的聚类界限;然而,如果你沿对角线观察,似乎有几个群体。
作为聚类的第一步,我们将一些变量转换为虚拟变量;这意味着我们将对分类变量进行独热编码。
catvars=['existingchecking','credithistory','purpose','savings','employmentsince','statussex','otherdebtors','property','otherinstallmentplans','housing','job','telephone','foreignworker']numvars=['creditamount','duration','installmentrate','residencesince','age','existingcredits','peopleliable','classification']dummyvars=pd.get_dummies(customers[catvars])transactions=pd.concat([customers[numvars],dummyvars],axis=1)不幸的是,当我们将数据集可视化以突出客户差异时,结果并不理想。你可以在线查看笔记本以了解一些尝试。
fromsklearn.clusterimportKMeansfrommatplotlibimportpyplotaspltsse={}forkinrange(1,15):kmeans=KMeans(n_clusters=k).fit(transactions)sse[k]=kmeans.inertia_plt.figure()plt.plot(list(sse.keys()),list(sse.values()))plt.xlabel("Numberofcluster")plt.ylabel("SSE")plt.show()惯性是所有数据点到最近聚类中心的距离总和。在k-means聚类算法中选择最佳聚类数(超参数k)的一种视觉标准称为肘部准则。
让我们可视化不同聚类数下的惯性:
“肘部准则”的基本思想是选择误差或惯性变平的聚类数。根据肘部准则,我们可能会选择4个聚类。让我们重新获取这些聚类:
kmeans=KMeans(n_clusters=4).fit(transactions)y=kmeans.labels_clusters=transactions.join(pd.DataFrame(data=y,columns=['cluster'])).groupby(by='cluster').agg(age_mean=pd.NamedAgg(column='age',aggfunc='mean'),age_std=pd.NamedAgg(column='age',aggfunc='std'),creditamount=pd.NamedAgg(column='creditamount',aggfunc='mean'),duration=pd.NamedAgg(column='duration',aggfunc='mean'),count=pd.NamedAgg(column='age',aggfunc='count'),class_mean=pd.NamedAgg(column='classification',aggfunc='mean'),class_std=pd.NamedAgg(column='classification',aggfunc='std'),).sort_values(by='class_mean')clusters这是聚类的汇总表。我们包括了营销特征,如年龄,以及其他让我们了解客户带来的收益的因素。我们展示了一些标准偏差,以了解这些群体的一致性程度:
有了这个想法,我们将进行新的尝试:
fromscipy.spatial.distanceimportpdist,squareformfromsklearn.preprocessingimportStandardScalerfromsklearn.clusterimportAgglomerativeClusteringdistances=squareform(pdist(StandardScaler().fit_transform(transactions[['classification','creditamount','duration']])))clustering=AgglomerativeClustering(n_clusters=5,affinity='precomputed',linkage='average').fit(distances)y=clustering.labels_现在我们可以再次生成概览表,以查看群体统计信息:
clusters=transactions.join(pd.DataFrame(data=y,columns=['cluster'])).groupby(by='cluster').agg(age_mean=pd.NamedAgg(column='age',aggfunc='mean'),age_std=pd.NamedAgg(column='age',aggfunc='std'),creditamount=pd.NamedAgg(column='creditamount',aggfunc='mean'),duration=pd.NamedAgg(column='duration',aggfunc='mean'),count=pd.NamedAgg(column='age',aggfunc='count'),class_mean=pd.NamedAgg(column='classification',aggfunc='mean'),class_std=pd.NamedAgg(column='classification',aggfunc='std'),).sort_values(by='class_mean')clusters接下来是新的摘要:
在商业智能中,聚类是一种非常常见的可视化技术。在营销中,你会针对不同的人群进行定位,比如青少年与退休人员,某些群体比其他群体更有价值。通常作为第一步,会通过降维方法或特征选择减少维度,然后通过应用聚类算法将群体分离。例如,你可以首先应用主成分分析(PCA)来降低维度(特征数量),然后用k-均值找到数据点的群体。
由于视觉化很难客观评判,在前一节中,我们所做的是退后一步,看看实际目的,即我们想要达成的业务目标。我们采取以下步骤来实现这个目标:
基于这个前提,我们尝试了不同的方法,并根据我们的业务目标对它们进行了评估。
如果你在查看配方时留意到,你可能已经注意到我们不对输出(z-分数)进行标准化。在使用z-分数进行标准化时,原始分数x通过减去均值并除以标准偏差被转换为标准分数,因此每个标准化变量的平均值为0,标准偏差为1:
我们不应用标准化,因为已经进行虚拟转换的变量在因素数量上具有更高的重要性。简单来说,z分数意味着每个变量具有相同的重要性。独热编码为每个可以采用的值提供了一个单独的变量。如果我们在进行虚拟转换后计算和使用z分数,一个被转换为许多新(虚拟)变量的变量,因为它有许多值,会比另一个具有较少值和因此较少虚拟列的变量更不重要。我们希望避免这种情况,所以我们不应用z分数。
然而,需要记住的重要一点是,我们必须专注于我们能够理解和描述的差异。否则,我们可能会得到用途有限的聚类。
在下一节中,我们将更详细地讨论k-均值算法。
PCA提出于1901年(由卡尔·皮尔逊在《关于空间中一组点的最佳适合线和平面》中提出),k-均值提出于1967年(由詹姆斯·麦克昆在《关于多元观测分类和分析的一些方法》中提出)。虽然这两种方法在数据和计算资源稀缺时有其用武之地,但今天存在许多替代方法可以处理数据点和特征之间更复杂的关系。作为本书的作者,我们个人经常感到沮丧,看到依赖正态性或变量之间一种非常有限关系的方法,例如经典的PCA或k-均值方法,尤其是在存在更多更好的方法时。
还有其他强健的非线性方法可用,例如亲和传播、模糊c-均值、凝聚聚类等。然而,重要的是要记住,尽管这些方法将数据点分组,以下陈述也是正确的:
令人惊讶的是,这样一个简单的想法竟然可以得出对人类观察者看起来有意义的结果。这里有一个使用它的例子。让我们试试用我们已经在第一章中Python中的人工智能入门中知道的鸢尾花数据集来尝试它:
importmatplotlib.pyplotaspltfromsklearn.datasetsimportload_irisX,y=load_iris(return_X_y=True)kmeans=KMeans(k=3)kmeans.fit(X)最终我们得到可以可视化或检查的簇,类似于之前。
异常是任何偏离预期或正常结果的情况。在工业过程监控(IPM)中,检测异常可以帮助实现更高水平的安全性、效率和质量。
我们将应用基于相似性的自动编码器方法,并使用适用于查找数据流事件的在线学习方法。
本文将专注于寻找异常值。我们将演示如何使用pyOD库包括自动编码器方法来做到这一点。我们还将概述不同方法的优缺点。
让我们按照以下步骤下载并加载它:
!pipinstallpyOD请注意,一些pyOD方法有依赖关系,如TensorFlow和Keras,因此您可能需要确保这些也已安装。如果您收到NoModulenamedKeras的消息,您可以单独安装Keras如下:
!pipinstallkeras请注意,通常最好使用与TensorFlow一起提供的Keras版本。
让我们看一下我们的数据集,然后应用不同的异常检测方法。
我们将在本节中涵盖不同的步骤和方法。它们如下:
让我们从探索和可视化我们的数据集开始:
让我们使用以下命令查看我们的数据集:
cpu_data.head()它看起来是这样的:
fromdatetimeimportdatetimeimportseabornassnscpu_data['datetime']=cpu_data.timestamp.astype(int).apply(datetime.fromtimestamp)#Useseabornstyledefaultsandsetthedefaultfiguresizesns.set(rc={'figure.figsize':(11,4)})time_data=cpu_data.set_index('datetime')time_data.loc[time_data['label']==1.0,'value'].plot(linewidth=0.5,marker='o',linestyle='')time_data.loc[time_data['label']==0.0,'value'].plot(linewidth=0.5)这是结果图,其中点代表异常值:
或者,我们可以看到异常值在关键绩效指标谱中的位置,以及它们与正常数据的区别有多明显,使用以下代码:
importnumpyasnpfrommatplotlibimportpyplotaspltmarkers=['r--','b-^']defhist2d(X,by_col,n_bins=10,title=None):bins=np.linspace(X.min(),X.max(),n_bins)vals=np.unique(by_col)formarker,valinzip(markers,vals):n,edges=np.histogram(X[by_col==val],bins=bins)n=n/np.linalg.norm(n)bin_centers=0.5*(edges[1:]+edges[:-1])plt.plot(bin_centers,n,marker,alpha=0.8,label=val)plt.legend(loc='upperright')iftitleisnotNone:plt.title(title)plt.show()hist2d(cpu_data.value,cpu_data.label,n_bins=50,title='Valuesbylabel')使用上述代码,我们绘制两个直方图的线图对比。或者,我们可以使用hist()函数并设置透明度。
我们将为所有后续方法使用相同的可视化效果,以便进行图形比较。
异常值(用虚线表示)与正常数据点(方块)几乎无法区分,因此我们不会期望完美的表现。
在继续测试异常值检测方法之前,让我们制定一个比较它们的过程,这样我们就有了测试方法相对性能的基准。
我们像往常一样将数据分为训练集和测试集:
fromsklearn.model_selectionimporttrain_test_splitX_train,X_test,y_train,y_test=train_test_split(cpu_data[['value']].values,cpu_data.label.values)现在让我们编写一个测试函数,可以用不同的异常值检测方法进行测试:
frompyod.utils.dataimportevaluate_printfrompyod.models.knnimportKNNdeftest_outlier_detector(X_train,y_train,X_test,y_test,only_neg=True,basemethod=KNN()):clf=basemethodifonly_neg:clf.fit(X_train[y_train==0.0],np.zeros(shape=((y_train==0.0).sum(),1)))else:clf.fit(X_train,y_train)#mostalgorithmsignoreyy_train_pred=clf.predict(X_train)#labels_y_train_scores=clf.decision_scores_y_test_pred=clf.predict(X_test)y_test_scores=clf.decision_function(X_test)print("\nOnTestData:")evaluate_print(type(clf).__name__,y_test,y_test_scores)hist2d(X_test,y_test_pred,title='Predictedvaluesbylabel')此函数在数据集上测试异常值检测方法。它训练一个模型,从模型中获取性能指标,并绘制可视化结果。
它接受这些参数:
我们可以选择仅在正常点上进行训练(即不包括异常值的所有点),以便学习这些点的分布或一般特征,然后异常值检测方法可以决定新点是否符合这些特征。
现在这一步完成后,让我们测试两种异常值检测方法:孤立森林和自编码器。
孤立森林排名第一。
我们运行基准测试方法,并使用孤立森林检测方法:
frompyod.models.iforestimportIForesttest_outlier_detector(X_train,y_train,X_test,y_test,only_neg=True,basemethod=IForest(contamination=0.01),)#OnTestData:#IForestROC:0.867,precision@rankn:0.1孤立森林预测的接收器操作特征曲线(ROC)性能相对于测试数据约为0.86,因此表现相当不错。
然而,从下图可以看出,在关键绩效指标谱的较低范围内没有1(预测的异常值)。该模型错过了较低范围的异常值:
它仅识别具有更高值(>=1.5)的点作为异常值。
frompyod.models.auto_encoderimportAutoEncodertest_outlier_detector(X_train,y_train,X_test,y_test,only_neg=False,basemethod=AutoEncoder(hidden_neurons=[1],epochs=10))我们可以看到Keras网络结构以及测试函数的输出:
Layer(type)OutputShapeParam#=================================================================dense_39(Dense)(None,1)2_________________________________________________________________dropout_30(Dropout)(None,1)0_________________________________________________________________dense_40(Dense)(None,1)2_________________________________________________________________dropout_31(Dropout)(None,1)0_________________________________________________________________dense_41(Dense)(None,1)2_________________________________________________________________dropout_32(Dropout)(None,1)0_________________________________________________________________dense_42(Dense)(None,1)2=================================================================Totalparams:8Trainableparams:8Non-trainableparams:0...OnTestData:AutoEncoderROC:0.8174,precision@rankn:0.1自编码器的性能与孤立森林非常相似;然而,自编码器在关键绩效指标谱的较低和较高范围都能找到异常值。
此外,我们在只提供正常数据或正常数据和异常值时并未获得明显差异。我们可以通过下图了解自动编码器的工作方式:
实际上这看起来并不太糟糕——中间范围内的值被分类为正常,而在谱外的值则被分类为异常值。
请记住,这些方法是无监督的;当然,如果我们使用自己的数据集使用监督方法,这将需要我们额外的工作来注释异常,而无监督方法则无需如此。
异常值是偏离数据中其他观测值的极端值。在许多领域中,包括网络安全、金融、交通、社交媒体、机器学习、机器模型性能监控和监视中,异常值检测都是重要的。已提出了许多领域中的异常检测算法。最突出的算法包括k-最近邻(kNN)、局部异常因子(LOF)和隔离森林,以及最近的自动编码器、长短期记忆网络(LSTM)和生成对抗网络(GANs)。我们将在后续的实例中探索其中一些方法。在这个实例中,我们使用了kNN、自动编码器和隔离森林算法。让我们简要地谈谈这三种方法。
kNN分类器是由ThomasCover和PeterHart提出的非参数分类器(见最近邻模式分类,1967)。其主要思想是一个新点很可能属于与其邻居相同的类。超参数k是要比较的邻居数。还有基于新点与其邻居相对距离的加权版本。
隔离森林的思想相对简单:创建随机决策树(这意味着每个叶子节点使用随机选择的特征和随机选择的分割值),直到只剩下一个点。穿过树获取到终端节点的路径长度指示出点是否是异常值。
自动编码器是一种神经网络架构,通过学习数据集的表示来实现。通常通过一个较小的隐藏层(瓶颈)来实现,从而可以恢复原始数据。这与许多其他降维方法类似。
自编码器由两部分组成:编码器和解码器。我们真正想学习的是编码器的转换,它给我们一个我们寻找的数据的代码或表示。
自动编码器在中间网络层中表示数据,它们在基于中间表示的重建越接近时,异常程度越低。
Python中有许多异常检测的公开实现:
在这个案例中,我们希望找到一种方法来确定两个字符串是否相似,给定这两个字符串的表示。我们将尝试改进字符串的表示方式,以便在字符串之间进行更有意义的比较。但首先,我们将使用更传统的字符串比较算法来建立基准。
我们将进行以下操作:给定一组成对字符串匹配的数据集,我们将尝试不同的函数来测量字符串的相似性,然后是基于字符n-gram频率的字符袋表示,最后是共孪神经网络(也称为双胞胎神经网络)的维度减少字符串表示。我们将设置一个双网络方法,通过字符n-gram频率学习字符串的潜在相似空间。
暹罗神经网络,有时也被称为双胞胎神经网络,是以联合双胞胎的类比来命名的。这是一种训练投影或度量空间的方法。两个模型同时训练,比较的是两个模型的输出而不是模型本身的输出。
如往常一样,我们需要下载或加载数据集并安装必要的依赖项。
我们将使用一组配对字符串的数据集,根据它们的相似性来确定它们是否匹配:
importpandasaspddata=pd.read_csv('forbes_freebase_goldstandard_train.csv',names=['string1','string2','matched'])数据集包含一对相匹配或不匹配的字符串。它的开始如下:
同一GitHub仓库还提供了一个测试数据集:
test=pd.read_csv('forbes_freebase_goldstandard_test.csv',names=['string1','string2','matched'])最后,我们将在本示例中使用一些库,可以像这样安装:
让我们开始吧。
让我们先实现几个标准字符串比较函数。
我们首先需要确保清理我们的字符串:
defclean_string(string):return''.join(map(lambdax:x.lower()ifstr.isalnum(x)else'',string)).strip()我们将在接下来的代码中的每个字符串比较函数中使用这个清理函数。我们将使用这个函数在进行任何字符串距离计算之前去除特殊字符。
现在我们可以实现简单的字符串比较函数。首先做Levenshtein距离:
importLevenshteindeflevenstein_distance(s1_,s2_):s1,s2=clean_string(s1_),clean_string(s2_)len_s1,len_s2=len(s1),len(s2)returnLevenshtein.distance(s1,s2)/max([len_s1,len_s2])现在让我们来计算Jaro-Winkler距离,它是最小单字符转置次数:
defjaro_winkler_distance(s1_,s2_):s1,s2=clean_string(s1_),clean_string(s2_)return1-Levenshtein.jaro_winkler(s1,s2)我们还将使用被比较对之间的最长公共子串。我们可以使用SequenceMatcher来完成这一点,它是Python标准库的一部分:
fromdifflibimportSequenceMatcherdefcommon_substring_distance(s1_,s2_):s1,s2=clean_string(s1_),clean_string(s2_)len_s1,len_s2=len(s1),len(s2)match=SequenceMatcher(None,s1,s2).find_longest_match(0,len_s1,0,len_s2)len_s1,len_s2=len(s1),len(s2)norm=max([len_s1,len_s2])return1-min([1,match.size/norm])现在我们可以对所有字符串对进行遍历,并基于每种方法计算字符串距离。对于这三种算法,我们可以计算曲线下面积(AUC)分数,以查看它们在分离匹配字符串和非匹配字符串方面的表现如何:
importnumpyasnpfromsklearn.metricsimportroc_auc_scoredists=np.zeros(shape=(len(data),3))foralgo_i,algoinenumerate([levenstein_distance,jaro_winkler_distance,common_substring_distance]):fori,string_pairindata.iterrows():dists[i,algo_i]=algo(string_pair['string1'],string_pair['string2'])print('AUCfor{}:{}'.format(algo.__name__,roc_auc_score(data['matched'].astype(float),1-dists[:,algo_i])))#AUCforlevenstein_distance:0.9508904955034385#AUCforjaro_winkler_distance:0.9470992770234525#AUCforcommon_substring_distance:0.9560042320578381所有算法的AUC分数约为95%,看起来很不错。这三种距离方法表现已经相当不错了。让我们尝试超过这个水平。
现在我们将实现一种基于字符包的字符串相似性方法。
字符包表示意味着我们将创建一个字符的直方图,或者换句话说,我们将计算每个单词中字符的出现次数:
fromsklearn.feature_extraction.textimportCountVectorizer#Wecleanthestringsasbeforeandwetakengrams.ngram_featurizer=CountVectorizer(min_df=1,analyzer='char',ngram_range=(1,1),#thisistherangeofngramsthataretobeextracted!preprocessor=clean_string).fit(np.concatenate([data['string1'],data['string2']],axis=0))我们已将ngrams的范围设置为1,这意味着我们只希望单个字符。然而,如果你希望包括字符之间更长范围的依赖关系而不仅仅是字符频率,这个参数可能会有趣。
让我们看看我们可以通过这个方法获得什么样的性能:
string1cv=ngram_featurizer.transform(data['string1'])string2cv=ngram_featurizer.transform(data['string2'])defnorm(string1cv):returnstring1cv/string1cv.sum(axis=1)similarities=1-np.sum(np.abs(norm(string1cv)-norm(string2cv)),axis=1)/2roc_auc_score(data['matched'].astype(float),similarities)#0.9298183741844471如你在大约93%的AUC分数中所见,这种方法整体表现还不如上面的方法好,尽管表现并非完全糟糕。因此,让我们尝试调整一下。
现在我们将实现一个Siamese网络,学习表示字符串之间相似性(或差异)的投影。
如果你对Siamese网络方法感到有些陌生,可能会觉得有点令人畏惧。我们将在工作原理...部分进一步讨论它。
让我们从字符串特征化函数开始:
fromtensorflow.keras.modelsimportSequential,Modelfromtensorflow.keras.layersimportDense,Lambda,Inputimporttensorflowastffromtensorflow.kerasimportbackendasKdefcreate_string_featurization_model(feature_dimensionality,output_dim=50):preprocessing_model=Sequential()preprocessing_model.add(Dense(output_dim,activation='linear',input_dim=feature_dimensionality))preprocessing_model.summary()returnpreprocessing_modelcreate_string_featurization_model函数返回一个字符串特征化模型。特征化模型是字符包输出的非线性投影。
函数有以下参数:
接下来,我们需要创建这两个模型的联合孪生体。为此,我们需要一个比较函数。我们使用归一化的欧氏距离。这是两个L2归一化投影向量之间的欧氏距离。
向量x的L2范数定义如下:
L2归一化是将向量x除以其范数。
我们可以如下定义距离函数:
defeuclidean_distance(vects):x,y=vectsx=K.l2_normalize(x,axis=-1)y=K.l2_normalize(y,axis=-1)sum_square=K.sum(K.square(x-y),axis=1,keepdims=True)returnK.sqrt(K.maximum(sum_square,K.epsilon()))现在Siamese网络可以通过将其包装为Lambda层来使用该函数。让我们定义如何联接孪生体,或者换句话说,我们如何将其包装成一个更大的模型,以便我们可以训练字符串对及其标签(即相似和不相似)。
defcreate_siamese_model(preprocessing_models,#initial_bias=input_shapes=(10,)):ifnotisinstance(preprocessing_models,(list,tuple)):raiseValueError('preprocessingmodelsneedstobealistortupleofmodels')print('{}modelstobetrainedagainsteachother'.format(len(preprocessing_models)))ifnotisinstance(input_shapes,list):input_shapes=[input_shapes]*len(preprocessing_models)inputs=[]intermediate_layers=[]forpreprocessing_model,input_shapeinzip(preprocessing_models,input_shapes):inputs.append(Input(shape=input_shape))intermediate_layers.append(preprocessing_model(inputs[-1]))layer_diffs=[]foriinrange(len(intermediate_layers)-1):layer_diffs.append(Lambda(euclidean_distance)([intermediate_layers[i],intermediate_layers[i+1]]))siamese_model=Model(inputs=inputs,outputs=layer_diffs)siamese_model.summary()returnsiamese_model这是一种冗长的说法:取两个网络,计算归一化欧氏距离,并将距离作为输出。
让我们创建双网络并进行训练:
defcompile_model(model):model.compile(optimizer='rmsprop',loss='mse',)feature_dims=len(ngram_featurizer.get_feature_names())string_featurization_model=create_string_featurization_model(feature_dims,output_dim=10)siamese_model=create_siamese_model(preprocessing_models=[string_featurization_model,string_featurization_model],input_shapes=[(feature_dims,),(feature_dims,)],)compile_model(siamese_model)siamese_model.fit([string1cv,string2cv],1-data['matched'].astype(float),epochs=1000)创建一个输出为10维的模型;从n-gram特征提取器中获得了41维,这意味着我们总共有420个参数(41*10+10)。
正如之前提到的,我们组合网络的输出是两个输出之间的欧氏距离。这意味着我们必须反转我们的目标(匹配)列,以便从相似变为不同,这样1对应不同,0对应相同。我们可以通过简单地从1中减去来轻松实现这一点。
现在我们可以获得这种新投影的性能:
fromscipy.spatial.distanceimporteuclideanstring_rep1=string_featurization_model.predict(ngram_featurizer.transform(data['string1']))string_rep2=string_featurization_model.predict(ngram_featurizer.transform(data['string2']))dists=np.zeros(shape=(len(data),1))fori,(v1,v2)inenumerate(zip(string_rep1,string_rep2)):dists[i]=euclidean(v1,v2)roc_auc_score(data['matched'].astype(float),1-dists)0.9802944806912361我们已经成功地击败了其他方法。甚至在调整任何超参数之前,我们的投影显然在突出显示对字符串相似性比较重要的差异方面起作用。
scikit-learn的CountVectorizer计算字符串中特征的出现次数。一个常见的用例是计算句子中的单词数——这种表示称为词袋,在这种情况下,特征将是单词。在我们的情况下,我们对基于字符的特征感兴趣,因此我们只计算a**出现的次数、b**出现的次数,依此类推。我们可以通过表示连续字符元组如ab或ba来使这种表示更加智能化;然而,这超出了我们当前的范围。
Siamese网络训练是指两个(或更多)神经网络相互训练,通过比较给定一对(或元组)输入的网络输出以及这些输入之间的差异的知识。通常,Siamese网络由相同的网络组成(即相同的权重)。两个网络输出之间的比较函数可以是诸如欧氏距离或余弦相似度之类的度量。由于我们知道两个输入是否相似,甚至知道它们有多相似,我们可以根据这些知识训练目标。
下图说明了信息流和我们将使用的不同构建块:
我们实际上可以直接训练这个完整的模型,给定一个字符串比较模型和一个由字符串对和目标组成的数据集。这种训练将调整字符串特征化模型,使其表示更加有用。
在这个步骤中,我们将构建一个推荐系统。推荐系统是一个信息过滤系统,通过将内容和社交连接在一起预测排名或相似性。
为了准备我们的步骤,我们将下载数据集并安装所需的依赖项。
让我们获取数据集,并在这里安装我们将使用的两个库–spotlight和lightfm是推荐系统库:
fromspotlight.datasets.goodbooksimportget_goodbooks_datasetfromspotlight.cross_validationimportrandom_train_test_splitimportnumpyasnpinteractions=get_goodbooks_dataset()train,test=random_train_test_split(interactions,random_state=np.random.RandomState(42))数据集以交互对象的形式呈现。根据spotlight的文档,交互对象可以定义如下:
对于隐性反馈场景,只应提供已观察到交互的用户-项目对的用户ID和项目ID。未提供的所有对都被视为缺失观察,并且通常被解释为(隐性)负信号。
对于显性反馈场景,应提供数据集中观察到的所有用户-项目-评分三元组的用户ID、项目ID和评分。
我们有以下训练和测试数据集:
importpandasaspdbooks=pd.read_csv('books.csv',index_col=0)defget_book_titles(book_ids):'''Getbooktitlesbybookids'''ifisinstance(book_ids,int):book_ids=[book_ids]titles=[]forbook_idinbook_ids:titles.append(books.loc[book_id,'title'])returntitlesbook_labels=get_book_titles(list(train.item_ids))现在我们可以按以下方式使用这个函数:
get_book_titles(1)['TheHungerGames(TheHungerGames,#1)']现在我们已经获取了数据集并安装了所需的库,我们可以开始我们的步骤了。
我们必须设置许多参数,包括潜在维度的数量和周期数:
importtorchfromspotlight.factorization.explicitimportExplicitFactorizationModelfromspotlight.evaluationimport(rmse_score,precision_recall_score)model=ExplicitFactorizationModel(loss='regression',embedding_dim=128,n_iter=10,batch_size=1024,l2=1e-9,learning_rate=1e-3,use_cuda=torch.cuda.is_available())model.fit(train,verbose=True)train_rmse=rmse_score(model,train)test_rmse=rmse_score(model,test)print('TrainRMSE{:.3f},testRMSE{:.3f}'.format(train_rmse,test_rmse))结果显示在以下截图中:
我们得到以下的推荐:
现在我们将使用lightfm推荐算法:
fromlightfmimportLightFMfromlightfm.evaluationimportprecision_at_k#Instantiateandtrainthemodelmodel=LightFM(loss='warp')model.fit(train.tocoo(),epochs=30,num_threads=2)test_precision=precision_at_k(model,test.tocoo(),k=5)print('meantestprecisionat5:{:.3f}'.format(test_precision.mean()))meantestprecisionat5:0.114我们也可以查看推荐内容,如下所示:
两个推荐系统都有它们的应用。基于精度在k(k=5)的基础上,我们可以得出结论,第二个推荐系统lightfm表现更好。
推荐系统向用户推荐产品。
他们可以根据不同的原则提出建议,例如以下内容:
混合模型可以以不同方式组合方法,例如分别进行基于内容和基于协同的预测,然后将分数相加,或者将这些方法统一到一个单一模型中。
我们尝试的两个模型都基于这样一种想法,即我们可以分离用户和物品的影响。我们将依次解释每个模型及其如何结合方法,但首先让我们解释一下我们正在使用的度量标准:k的精度。
精度在k上不考虑在前k个结果中的排序,也不包括我们绝对应该捕捉到的真正好的结果的数量:这将是召回率。尽管如此,精度在k上是一个合理的度量标准,而且很直观。
spotlight中的显式模型基于YehudaKoren等人在《推荐系统的矩阵分解技术》(2009)中提出的矩阵分解技术。基本思想是将用户-物品(交互)矩阵分解为表示用户潜在因素和物品潜在因素的两个组成部分,以便根据给定物品和用户进行推荐,计算如下:
矩阵分解或矩阵因子分解是将矩阵分解为矩阵乘积的过程。存在许多不同的这类分解方法,用途各不相同。
一个相对简单的分解是奇异值分解(SVD),但现代推荐系统使用其他分解方法。spotlight矩阵分解和lightfm模型都使用线性整合。
lightfm模型是由Kula在《用户和物品冷启动推荐的元数据嵌入》(2015)中介绍的。更具体地说,我们使用的是WARP损失,该损失在JasonWeston等人于2011年的《WSABIE:大规模词汇图像注释的扩展》中有详细解释。
在lightfm算法中,用于预测的函数如下:
模型训练最大化数据在给定参数条件下的似然性表达如下:
有多种方法可以衡量推荐系统的表现,而我们选择使用哪种方法取决于我们试图实现的目标。
同样,有很多库可以轻松启动和运行。首先,我想强调这两个已经在本配方中使用过的库:
但还有一些其他很有前景的方法:
为了准备好制作这个配方,我们将安装所需的库并下载数据集。
此外,我们将使用SciPy,但这是在Anaconda分发中包含的:
信用卡欺诈数据集包含了2013年9月欧洲持卡人使用信用卡的交易记录。该数据集包含了284,807笔交易,其中492笔是欺诈交易。该数据集非常不平衡:正类(欺诈)占所有交易的0.172%。
让我们导入数据集,然后将其分割为训练集和测试集:
importpandasaspdfromsklearn.datasetsimportfetch_openmlimportrandomX,y=fetch_openml(data_id=1597,return_X_y=True)samples=random.choices(list(range(X.shape[0])),k=int(X.shape[0]*0.33))X_train=X[(samples),:]我们准备好了!让我们来做这道菜吧!
首先我们会创建一个邻接矩阵,然后我们可以对其应用社区检测方法,最后我们将评估生成社区的质量。整个过程由于大数据集而增加了难度,这意味着我们只能应用某些算法。
首先,我们需要计算所有点的距离。对于这样一个大数据集,这是一个真正的问题。你可以在网上找到几种方法。
我们使用Spotify的annoy库进行此目的,这是非常快速和内存高效的:
fromannoyimportAnnoyIndext=AnnoyIndex(X_train.shape[1],'euclidean')#Lengthofitemvectorthatwillbeindexedfori,vinenumerate(X_train):t.add_item(i,v)t.build(10)#10trees然后我们可以根据索引给出的距离初始化我们的邻接矩阵:
fromtqdmimporttrangefromscipy.sparseimportlil_matrixMAX_NEIGHBORS=10000#Careful:thisparameterdeterminestherun-timeoftheloop!THRESHOLD=6.0defget_neighbors(i):neighbors,distances=t.get_nns_by_item(i,MAX_NEIGHBORS,include_distances=True)return[nforn,dinzip(neighbors,distances)ifd 我们矩阵的大小限制了我们的选择。我们将应用以下两个算法: 我们可以直接在邻接矩阵上应用SCC算法,如下所示: fromscipy.sparse.csgraphimportconnected_componentsn_components,labels=connected_components(A,directed=False,return_labels=True)对于第二个算法,我们首先需要将邻接矩阵转换为图形;这意味着我们将矩阵中的每个点视为节点之间的边。为了节省空间,我们使用了一个简化的图形类: importnetworkxasnxclassThinGraph(nx.Graph):all_edge_dict={'weight':1}defsingle_edge_dict(self):returnself.all_edge_dictedge_attr_dict_factory=single_edge_dictG=ThinGraph(A)然后我们可以按以下方式应用Louvain算法: importcommunity#thisisthepython-louvainpackagepartition=community.best_partition(G)现在我们有数据集的两个不同分区。让我们看看它们是否有价值! 在理想情况下,我们期望一些社区只有欺诈者,而其他(大多数)社区则完全没有。这种纯度是我们在完美社区中寻找的。然而,由于我们可能也希望得到一些其他可能是欺诈者的建议,我们预计一些点会在大多数非欺诈组中被标记为欺诈者,反之亦然。 我们可以从观察每个社区的欺诈频率直方图开始。Louvain欺诈者分布如下: 这表明社区中有很多非欺诈者,而其他数值很少。但我们能量化这有多好吗? 我们可以通过计算每个群集中的类熵来描述欺诈者分布。我们将在它的工作原理...部分解释熵。 然后我们可以创建适当选择的随机实验,以查看任何其他社区分配是否会导致更好的类熵。如果我们随机重新排序欺诈者,然后计算跨社区的熵,我们将得到一个熵分布。这将为我们提供Louvain社区熵的p值,统计显著性。 p值是我们仅凭偶然性就能获得这种(或更好)分布的概率。 您可以在GitHub上的笔记本中找到采样的实现。 我们得到了非常低的显著性,这意味着几乎不可能通过偶然得到类似的结果,这使我们得出结论,我们在识别欺诈者方面找到了有意义的聚类。 我们使用稀疏矩阵,其中大多数邻接都是0,如果它们不超过给定的阈值。我们将每个点之间的每个连接表示为布尔值(1位),并且我们仅采用33%的样本,即93,986个点,而不是全部数据集。 让我们了解两个图形社区算法的工作原理。 直到模块度再无改善为止,这两个步骤将被重复进行。 算法工作如下: 结果是一个树状图,显示了算法步骤中聚类的排列方式。 这通常被视为随机变量中的意外性、不确定性或混乱程度。 如果变量不是离散的,我们可以应用分箱(例如,通过直方图)或者使用非离散版本的公式。 我们也可以应用其他算法,如2005年由DavidPearce发布的SCC算法(在《找到有向图的强连通分量的改进算法》中)。 我们也可以尝试这种方法: fromscipy.sparse.csgraphimportconnected_componentsn_components,labels=connected_components(A,directed=False,return_labels=True)SCC社区欺诈分布如下: 再次得到一个p值,显示出非常高的统计显著性。这意味着这不太可能仅仅是偶然发生的,表明我们的方法确实是欺诈的良好分类器。 我们也可以应用更传统的聚类算法。例如,亲和传播算法接受一个邻接矩阵,如下所示: fromsklearn.clusterimportAffinityPropagationap=AffinityPropagation(affinity='precomputed').fit(A)还有许多其他方法可供我们应用。对于其中一些方法,我们需要将邻接矩阵转换为距离矩阵。 Python有一些非常好的图库,具有许多用于社区检测或图分析的实现: 大多数Python库适用于小到中型邻接矩阵(大约高达1,000条边)。适用于更大数据规模的库包括以下内容: Cdlib还包含BigClam算法,适用于大图。 本章讨论不确定性和概率方法。现代机器学习系统存在两个显著的缺点。 首先,它们在预测中可能过于自信(有时过于不自信)。在实践中,鉴于嘈杂的数据,即使我们观察到使用未见数据集进行交叉验证的最佳实践,这种信心也可能不合理。特别是在受监管或敏感环境中,如金融服务、医疗保健、安全和情报领域,我们需要非常谨慎地对待我们的预测及其准确性。 其次,机器学习系统越复杂,我们需要更多的数据来拟合我们的模型,过拟合的风险也越严重。 概率模型是使用随机采样技术产生概率推断的模型。通过参数化分布和固有的不确定性,我们可以克服这些问题,并获得否则需要更多数据才能获得的准确性。 在本章中,我们将使用不同的插件方法建立股票价格预测模型,并进行置信度估计。然后,我们将涵盖估计客户生命周期,这是为服务客户的企业所共有的问题。我们还将研究诊断疾病,并量化信用风险,考虑不同类型的不确定性。 本章涵盖以下配方: 在本章中,我们主要使用以下内容: 在这个示例中,我们将在scikit-learn中构建一个简单的股票预测管道,并使用不同的方法生成概率估计。然后,我们将评估我们的不同方法。 我们将使用yfinance库检索历史股票价格。 这是我们安装它的方式: pipinstallyfinanceyfinance将帮助我们下载历史股票价格。 在实际情况中,我们希望回答以下问题:在价格水平给定的情况下,它们会上涨还是下跌,以及幅度如何? 为了朝着这个目标取得进展,我们将按以下步骤进行: 特别地,我们将比较以下生成置信度值的方法: 我们将讨论这些方法及其背景在它是如何工作...部分。 让我们试一下! importyfinanceasyfmsft=yf.Ticker('MSFT')hist=msft.history(period='max')现在我们有我们的股票价格作为pandasDataFramehist可用。 fromtypingimportTupleimportnumpyasnpimportpandasaspdimportscipydefgenerate_data(data:pd.DataFrame,window_size:int,shift:int)->Tuple[np.array,np.array]:y=data.shift(shift+window_size)observation_window=[]foriinrange(window_size):observation_window.append(data.shift(i))X=pd.concat(observation_window,axis=1)y=(y-X.values[:,-1])/X.values[:,-1]X=X.pct_change(axis=1).values[:,1:]inds=(~np.isnan(X).any(axis=1))&(~np.isnan(y))X,y=X[inds],y[inds]returnX,y然后,我们将使用我们的新函数generate_data()生成我们的训练和测试数据集: fromsklearn.model_selectionimporttrain_test_splitX,y=generate_data(hist.Close,shift=1,window_size=30)X_train,X_test,y_train,y_test=train_test_split(X,y)当然,这是一个常见的模式,我们在前面的示例中已经见过几次了:我们生成我们的数据集,然后将其拆分为训练和验证集,其中训练集用于训练(正如其名称所示),验证集用于检查我们的算法的工作效果(特别是我们是否过度拟合)。 我们的数据集大致符合正态分布。这是我们在训练中目标的样子: 我们可以看到有一个向左的偏斜,即比零更多的值在下方(大约49%)而不是在上方(大约43%)。这意味着在训练中,价格会下降而不是上涨。 我们的数据集还没有完成,但是我们需要进行另一次转换。我们的场景是,我们想应用这个模型来帮助我们决定是否购买股票,假设价格会上涨。我们将分离三个不同的类别: 在下面的代码块中,我们根据threshold参数应用了x给定的截止值: defthreshold_vector(x,threshold=0.02):defthreshold_scalar(f):iff>threshold:return1eliff<-threshold:return-1return0returnnp.vectorize(threshold_scalar)(x)y_train_classes,y_test_classes=threshold_vector(y_train),threshold_vector(y_test)在此之后,我们对训练和测试(验证)的阈值化y值进行了处理。 fromsklearnimportmetricsdefto_one_hot(a):"""convertfromintegerencodingtoone-hot"""b=np.zeros((a.size,3))b[np.arange(a.size),a+1]=1returnbdefmeasure_perf(model,y_test_classes):y_pred=model.predict(X_test)auc=metrics.roc_auc_score(to_one_hot(y_test_classes),to_one_hot(y_pred),multi_class='ovo')print('AUC:{:.3f}'.format(auc))现在我们可以使用我们的新方法来评估模型训练后的性能。 fromsklearn.ensembleimportRandomForestClassifierfromsklearn.calibrationimportCalibratedClassifierCVrf=RandomForestClassifier(n_estimators=500,n_jobs=-1).fit(X_train,y_train_classes)platt=CalibratedClassifierCV(rf,method='sigmoid').fit(X_train,y_train_classes)isotonic=CalibratedClassifierCV(rf,method='isotonic').fit(X_train,y_train_classes)print('Platt:')measure_perf(platt,y_test_classes)print('Isotonic:')measure_perf(isotonic,y_test_classes)#Platt:#AUC:0.504#Isotonic:#AUC:0.505对于朴素贝叶斯,我们尝试不同的变体:分类朴素贝叶斯和补集朴素贝叶斯: fromsklearn.ensembleimportStackingClassifierfromsklearn.naive_bayesimportComplementNB,CategoricalNBdefcreate_classifier(final_estimator):estimators=[('rf',RandomForestClassifier(n_estimators=100,n_jobs=-1))]returnStackingClassifier(estimators=estimators,final_estimator=final_estimator,stack_method='predict_proba').fit(X_train,y_train_classes)measure_perf(create_classifier(CategoricalNB()),y_test_classes)measure_perf(create_classifier(ComplementNB()),y_test_classes)#CategoricalNB:#AUC:0.500#ComplementNB:#AUC:0.591我们发现,无论是Platt缩放(逻辑回归)还是等渗回归都无法很好地处理我们的数据集。朴素贝叶斯回归不比随机选择好多少,这让我们不愿意拿来押注,即使稍微比随机选择好一点。然而,补集朴素贝叶斯分类器的表现要好得多,达到了59%的AUC。 我们已经看到,我们可以创建一个股票价格预测器。我们将这个过程分解为创建数据、验证和训练模型。最终,我们找到了一种方法,可以让我们对其实际使用抱有希望。 让我们先生成数据,然后再使用我们的不同方法。 这对于任何人工智能工作都是至关重要的。在处理任何工作或首次查看数据集之前,我们应该问自己我们选择什么作为我们观察单位的单元,并且我们如何以有意义且可以被算法捕捉的方式描述我们的数据点。这是一种随着经验变得自动化的事情。 特征被标准化为平均值为0,然后作为百分比变化差异(每个窗口中的每个值到前一个值的差异)。差异步骤是为了引入平稳性度量。特别地,目标被表达为相对于窗口中最后一个值的百分比变化,即特征。 接下来我们将看一下Platt缩放,这是一种将模型预测缩放为概率输出的最简单方法之一。 Platt缩放(JohnPlatt,1999年,支持向量机的概率输出及其与正则化似然方法的比较)是我们使用的第一种模型结果缩放方法。简单来说,它是在我们的分类器预测之上应用逻辑回归。逻辑回归可以表达为以下形式(方程式1): 这里的A和B是通过最大似然方法学习得到的。 我们正在寻找A和B,如下所示: 这里的p指的是前述的方程式1。 作为梯度下降,我们可以迭代应用以下两个步骤之一: 在接下来的小节中,我们将探讨使用保序回归进行概率校准的替代方法。 保序回归(Zadrozny和Elkan,2001年,当成本和概率均未知时的学习和决策)是使用保序函数进行回归的方法,即作为函数逼近时,最小化均方误差的同时保持单调递增或非减。 我们可以表达为以下形式: 这里m是我们的保序函数,x和y是特征和目标,f是我们的分类器。 接下来,我们将看一个最简单的概率模型之一,朴素贝叶斯。 朴素贝叶斯分类器基于贝叶斯定理。 贝叶斯定理是关于条件概率的定理,表示事件A发生在B给定的条件下: P(A)是观察到A的概率(A的边际概率)。考虑到公式中的P(B)在分母中,不能为零。这背后的推理值得一读。 朴素贝叶斯分类器涉及到给定特征的类别概率。我们可以将类别k和特征x插入到贝叶斯定理中,如下所示: 它被称为朴素是因为它假设特征彼此独立,因此提名者可以简化如下: 在接下来的部分中,我们将查看额外的材料。 这里有一些资源供您参考: 我们需要lifetimes包来完成这个配方。让我们按照以下代码安装它: pipinstalllifetimes现在我们可以开始了。 用于客户生命周期价值的数据集可以是交易性的,也可以是客户汇总的。 摘要数据应包括以下统计信息: 让我们从第一步开始! fromlifetimes.datasetsimportload_cdnow_summary_data_with_monetary_valuefromlifetimesimportBetaGeoFitterbgf=BetaGeoFitter(penalizer_coef=0.0)bgf.fit(data['frequency'],data['recency'],data['T'])fromlifetimesimportGammaGammaFitterdata_repeat=data[data.frequency>0]ggf=GammaGammaFitter(penalizer_coef=0.0)ggf.fit(data_repeat.frequency,data_repeat.monetary_value)print(ggf.customer_lifetime_value(bgf,data['frequency'],data['recency'],data['T'],data['monetary_value'],time=12,discount_rate=0.01).head(5))输出显示了客户生命周期价值: 让我们看看这个配方中的一些方法。 在这个配方中,我们根据他们的购买模式估计了客户的生命周期价值。 最后,我们可以按照以下公式结合预测以获取生命周期价值: 让我们看看我们在这里使用的两个子模型。 这考虑了客户的购买频率和客户的退出概率。 它包含以下假设: 可以根据最大似然估计来估计λ和p参数。 此模型用于估计客户终身的平均交易价值,E(M),我们对其有一个不完美的估计,如下所示: 在这里,x是客户终身内(未知的)总购买次数,z是每次购买的价值。 我们假设z是从伽玛分布中抽样的,因此,模型的拟合涉及在个体水平上找到形状和比例参数。 Lifetimes库提供了一系列模型(称为fitters),您可能想要深入了解。关于本示例中两种方法的更多详细信息,请参阅Fader等人的CountingyourCustomerstheEasyWay:AnAlternativetothePareto/NBDModel,2005年以及Batislam等人的Empiricalvalidationandcomparisonofmodelsforcustomerbaseanalysis,2007年。您可以在Fader和Hardi的报告Gamma-GammaModelofMonetaryValue(2013年)中找到关于伽玛-伽玛模型的详细信息。 对于概率建模,实验性库不胜枚举。运行概率网络可能比算法(非算法)方法慢得多,直到不久前,它们几乎对除了非常小的数据集外的任何事物都不实用。事实上,大多数教程和示例都是关于玩具数据集的。 然而,由于硬件更快和变分推断的进步,在近年来这种情况发生了变化。利用TensorFlowProbability,即使进行了概率抽样并且完全支持GPU,也通常可以简单地定义架构、损失和层,并支持最新的实现来进行快速训练。 在这个示例中,我们将实现一个在医疗保健领域的应用程序-我们将诊断一种疾病。 我们已经在之前的章节中安装了scikit-learn和TensorFlow。 对于这个示例,我们还需要安装tensorflow-probability。 pipinstalltensorflow-probability现在安装了tensorflow-probability,我们将在下一节中广泛使用它。 我们将把这个过程分解为几个步骤: 我们将从Python中获取数据集的步骤开始: 如之前一样,我们将从OpenML下载数据集。您可以在那里看到完整的描述。原始目标编码了不同的状态,其中0表示健康,其他数字表示疾病。因此,我们将在健康和非健康之间分开,并将其视为二元分类问题。我们应用标准缩放器,以便将z分数馈送给神经网络。所有这些在Chapter1、GettingStartedwithArtificialIntelligenceinPython、Chapter2、AdvancedTopicsinSupervisedMachineLearning、Chapter3、Patterns,Outliers,andRecommendations和Chapter4、ProbabilisticModeling等几个早期的章节中应该是熟悉的: fromsklearn.datasetsimportfetch_openmlfromsklearn.preprocessingimportStandardScalerfromsklearn.model_selectionimporttrain_test_splitX,y=fetch_openml(data_id=1565,return_X_y=True,as_frame=True)target=(y.astype(int)>1).astype(float)scaler=StandardScaler()X_t=scaler.fit_transform(X)Xt_train,Xt_test,y_train,y_test=train_test_split(X_t,target,test_size=0.33,random_state=42)现在,我们已经预处理并将数据集拆分为训练集和测试集。 importtensorflowastfimporttensorflow_probabilityastfptfd=tfp.distributionsfromtensorflowimportkerasnegloglik=lambday,p_y:-p_y.log_prob(y)model=keras.Sequential([keras.layers.Dense(12,activation='relu',name='hidden'),keras.layers.Dense(1,name='output'),tfp.layers.DistributionLambda(lambdat:tfd.Bernoulli(logits=t)),])model.compile(optimizer=tf.optimizers.Adagrad(learning_rate=0.05),loss=negloglik)需要注意的是,我们不像在二元分类任务中那样最后以Dense(2,activation='softmax'层结束,而是将输出减少到我们概率分布需要的参数数量,对于伯努利分布而言,仅需一个参数,即二进制结果的期望平均值。 我们使用的是一个只有181个参数的相对较小的模型。在Howitworks...节中,我们将解释损失函数。 fromscipy.statsimportnormimportmatplotlib.pyplotasplty_pred=model(Xt_test)a=y_pred.mean().numpy()[10]b=y_pred.variance().numpy()[10]fig,ax=plt.subplots(1,1)x=np.linspace(norm.ppf(0.001,a,b),norm.ppf(0.999,a,b),100)pdf=norm.pdf(x,a,b)ax.plot(x,pdf/np.sum(pdf),'r-',lw=5,alpha=0.6,label='normpdf')plt.ylabel('probabilitydensity')plt.xlabel('predictions')此预测如下所示: 因此,每个预测都是来自伯努利过程的样本。我们可以使用累积分布函数将每个预测转换为类别概率: importsklearndefto_one_hot(a):"""convertfromintegerencodingtoone-hot"""b=np.zeros((a.size,2))b[np.arange(a.size),np.rint(a).astype(int)]=1returnbsklearn.metrics.roc_auc_score(to_one_hot(y_test),class_probs)print('{:.3f}'.format(sklearn.metrics.roc_auc_score(to_one_hot(y_test),class_probs)))0.85985%的AUC听起来不错。但我们在医疗领域工作,因此我们需要检查召回率(也称为敏感性)和精度;换句话说,我们是否检测到了所有患病的患者,并且如果我们诊断某人,他们是否确实患病?如果我们漏掉某人,他们可能会死于未治疗的情况。如果我们将所有人都诊断为患病,将会给资源带来压力。 在以下代码段中,我们将更详细地查看我们得到的结果: fromsklearn.metricsimportplot_precision_recall_curveimportmatplotlib.pyplotaspltfromsklearn.metricsimportaverage_precision_scoreclassModelWrapper(sklearn.base.ClassifierMixin):_estimator_type='classifier'classes_=[0,1]defpredict_proba(self,X):pred=model(X)returnto_classprobs(pred)model_wrapper=ModelWrapper()average_precision=average_precision_score(to_one_hot(y_test),class_probs)fig=plot_precision_recall_curve(model_wrapper,Xt_test,y_test)fig.ax_.set_title('2-classPrecision-Recallcurve:''AP={0:0.2f}'.format(average_precision))这通过可视化我们的结果来更好地理解精度和召回率之间的权衡。 我们得到以下图表: 这条曲线展示了我们模型固有的召回率和精度之间的权衡。在我们的置信度(或类别概率)上采用不同的截断时,我们可以判断某人是否患病。如果我们想要找到所有人(召回率=100%),精度会下降到40%以下。另一方面,如果我们希望在诊断某人患病时始终正确(精度=100%),那么我们将会错过所有人(召回率=0%)。 现在的问题是,分别错过人员或诊断过多的成本,以便对是否某人患病做出决定。考虑到治疗人员的重要性,也许在召回率约为90%和精度约为65%左右之间存在一个最佳点。 我们训练了一个神经网络来进行概率预测,用于诊断疾病。让我们分析一下这个过程,并了解我们在这里使用的内容。 TensorFlowProbability提供了多种用于建模不同类型不确定性的层。随机不确定性指的是在给定相同输入的情况下,结果的随机变化性——换句话说,我们可以学习数据中的分布情况。 我们可以通过在Keras和TensorFlowProbability中参数化描述预测的分布,而不是直接预测输入来实现这一点。基本上,DistributionLambda从分布中抽取样本(在我们的情况下是伯努利分布)。 我们使用负对数似然作为我们的损失函数。这种损失函数通常用于最大似然估计。 我们定义的是这样的: negloglik=lambday,p_y:-p_y.log_prob(y)损失函数接受两个值:y,目标值,以及提供log_prob()方法的概率分布。该方法返回y处概率密度的对数。由于高值代表好结果,我们希望通过取负值来反转该函数。 伯努利分布(有时称为硬币翻转分布)是一个具有两个结果的离散事件分布,其发生概率为p和q=1-p。它有一个单一的参数p。我们也可以选择其他建模选项,例如在softmax激活层之上的分类分布。 最后,我们涉及了召回率和精确率。 它们的定义如下: 我们之前见过真阳性(tp)、假阳性(fp)和假阴性(fn)。作为提醒,真阳性指的是正确的预测,假阳性指的是错误地预测为正面的值,假阴性指的是错误地预测为负面的值。 我们将使用一个相对较小的信用卡申请数据集;然而,它仍然可以为我们提供一些关于如何使用神经网络模型进行信用评分的见解。我们将实现一个包含权重分布和输出分布的模型。这被称为认知不确定性和随机不确定性,将为我们提供更可靠的预测信息。 我们将使用tensorflow-probability。以防您跳过了前面的配方诊断疾病,这里是如何安装它: pipinstalltensorflow-probability现在,我们应该准备好使用Keras和tensorflow-probability了。 让我们获取数据集并进行预处理,然后创建模型,训练模型并验证它: 我们将使用scikit-learn的实用函数通过openml下载数据: importnumpyasnpfromsklearn.datasetsimportfetch_openmlopenml_frame=fetch_openml(data_id=42477,as_frame=True)data=openml_frame['data']这给我们提供了关于客户人口统计信息及其申请的特征。 我们将使用一个非常标准的预处理过程,这在本书中我们已经看过很多次,并且我们将轻松处理。我们本可以更深入地检查特征,或者在转换和特征工程上做更多工作,但这与本示例无关。 所有特征都注册为数值,因此我们只应用标准缩放。这里是预处理步骤,然后我们将其分为训练和测试数据集: fromsklearn.preprocessingimportStandardScalerfromsklearn.model_selectionimporttrain_test_splitscaler=StandardScaler()X=scaler.fit_transform(data)target_dict={val:numfornum,valinenumerate(list(openml_frame['target'].unique()))}y=openml_frame['target'].apply(lambdax:target_dict[x]).astype('float').valuesX_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.33,random_state=42)现在,工作完成了,让我们创建模型。 importtensorflowastfimporttensorflow_probabilityastfpimportmatplotlib.pyplotasplttfd=tfp.distributions%matplotlibinlinenegloglik=lambday,rv_y:-rv_y.log_prob(y)defprior_trainable(kernel_size,bias_size=0,dtype=None):n=kernel_size+bias_sizereturntf.keras.Sequential([tfp.layers.VariableLayer(n,dtype=dtype),tfp.layers.DistributionLambda(lambdat:tfd.Independent(tfd.Normal(loc=t,scale=1),reinterpreted_batch_ndims=1)),])defposterior_mean_field(kernel_size,bias_size=0,dtype=None):n=kernel_size+bias_sizec=np.log(np.expm1(1.))returntf.keras.Sequential([tfp.layers.VariableLayer(2*n,dtype=dtype),tfp.layers.DistributionLambda(lambdat:tfd.Independent(tfd.Normal(loc=t[...,:n],scale=1e-5+tf.nn.softplus(c+t[...,n:])),reinterpreted_batch_ndims=1)),])请注意DenseVariational。 现在来看主模型,在这里我们将使用先验和后验分布。你会认出DistributionLambda。我们从之前的示例中替换了Binomial,诊断疾病,用Normal,这将给我们一个预测方差的估计: model=tf.keras.Sequential([tfp.layers.DenseVariational(2,posterior_mean_field,prior_trainable,kl_weight=1/X.shape[0]),tfp.layers.DistributionLambda(lambdat:tfd.Normal(loc=t[...,:1],scale=1e-3+tf.math.softplus(0.01*t[...,1:]))),])model.compile(optimizer=tf.optimizers.Adam(learning_rate=0.01),loss=negloglik)callback=tf.keras.callbacks.EarlyStopping(monitor='val_loss',patience=5)model.fit(X_train,y_train,validation_data=(X_test,y_test),epochs=1000,verbose=False,callbacks=[callback])拟合后,我们将模型应用于测试数据集并对其进行预测。 fromsklearn.metricsimportroc_auc_scorepreds=model(X_test)roc_auc_score(y_test,preds.mean().numpy())我们得到了约70%的AUC。由于这个摘要数字通常不能完全揭示全部情况,让我们也看看混淆矩阵: fromsklearn.metricsimportconfusion_matriximportpandasaspdimportseabornassnscm=confusion_matrix(y_test,preds.mean().numpy()>=0.5)cm=pd.DataFrame(data=cm/cm.sum(axis=0),columns=['False','True'],index=['False','True'])sns.heatmap(cm,fmt='.2f',cmap='Blues',annot=True,annot_kws={'fontsize':18})此代码给出了以下混淆矩阵: 这个混淆矩阵列出了对实际违约的预测。在点对角线上,假-假和真-真是正确的预测(真正例和真反例)。我们可以看到,正确预测的数量比错误预测高,这让人感到安慰。 这给了我们一个非常有用的实践信心估计。我们发现,对于测试点,误差较高的方差较大,而在预期误差较低的地方,方差较小。我们可以在散点图中查看绝对误差和方差: 这就结束了我们的配方。这个配方作为一个练习,让你尝试更好的预处理,调整模型,或者切换分布。 信用评分模型通常使用logistic回归模型,我们在本章中的预测股票价格和置信度配方中遇到过。另外,提升模型或可解释的决策树也在使用中。鉴于在线学习的能力和表示残差不确定性的能力,tensorflow-probability提供了另一个实用的选择。 在这个配方中,我们创建了一个与认知不确定性一起工作的概率性信用违约预测模型。现在是时候解释这意味着什么了。 在TensorFlowProbability中,这可以建模为权重不确定性。在这个配方中,我们使用了贝叶斯神经网络,其中权重是概率分布而不是标量估计。这种权重不确定性转化为预测中的不确定性,这正是我们想要看到的。 作为我们最终的网络层,我们加入了来自正态分布的随机性,以模拟阿莱托里克不确定性。相反,我们也可以假设这种变异性是已知的。 还有其他探索途径,比如库或额外的材料,我们将在这里列出。 在线可以找到类似的问题,比如以下内容: 至于库,我们建议您看看这些: 在本章中,我们将介绍一系列问题解决工具。我们将从本体论和基于知识的推理开始,然后转向布尔可满足性(SAT)和组合优化的优化,其中我们将模拟个体行为和社会协调的结果。最后,我们将实现蒙特卡洛树搜索以找到国际象棋中的最佳着法。 在本章中,我们将涉及各种技术,包括逻辑求解器、图嵌入、遗传算法(GA)、粒子群优化(PSO)、SAT求解器、模拟退火(SA)、蚁群优化、多主体系统和蒙特卡洛树搜索。 在本章中,我们将涵盖以下配方: 让我们开始吧! 当关于一个主题有大量背景知识可用时,为什么不在做出决策时使用它?这被称为基于知识的系统。专家系统中的推理引擎和逻辑求解器中的统一化就是其中的例子。 在做出决策时,另一种检索知识的方式是基于在图中表示知识。图中的每个节点表示一个概念,而每条边表示一种关系。两者都可以嵌入并表示为表达它们与图中其他元素位置关系的数值特征。 在本配方中,我们将为每种可能性举例两次。 在这个配方中,我们将使用从Python的nltk(自然语言工具包)库接口的逻辑求解器,然后使用被称为networkx和karateclub的图库。 您需要使用的pip命令如下: 正如我们在这个配方的介绍中解释的那样,我们将从两种不同的方法来看待两个不同的问题。 我们将从使用逻辑求解器开始进行逻辑推理。 在这个配方的这一部分,我们将使用nltk库捆绑的一些库来简单展示逻辑推理的一个示例。还有许多其他方法可以处理逻辑推理,我们将在配方末尾的参考资料部分中看到一些。 我们将使用一个非常简单的玩具问题,你可以在任何101–逻辑入门书籍中找到,尽管解决这类问题的方法可以更复杂。 我们的问题是众所周知的:如果所有人类都是可死的,苏格拉底是一个人类,那么苏格拉底是可死的吗? 我们可以在nltk中非常自然地表达这个过程,如下所示: fromnltkimport*fromnltk.semimportExpressionp1=Expression.fromstring('man(socrates)')p2=Expression.fromstring('allx.(man(x)->mortal(x))')c=Expression.fromstring('mortal(socrates)')ResolutionProver().prove(c,[p1,p2],verbose=True)前面的代码给出了以下输出: [1]{-mortal(socrates)}A[2]{man(socrates)}A[3]{-man(z2),mortal(z2)}A[4]{-man(socrates)}(1,3)[5]{mortal(socrates)}(2,3)[6]{}(1,5)True求解器提供的推理也可以很自然地阅读,所以我们不会在这里解释这个过程。我们将在如何工作...部分中学习其内部工作原理。 接下来,我们将看看知识嵌入。 在这个配方的这一部分,我们将尝试利用信息如何相互关联,将其嵌入到一个可以作为特征化一部分的多维空间中。 这里,我们将加载数据,预处理数据,嵌入数据,然后通过对其新特征进行分类来测试我们的嵌入效果。让我们开始吧: importpandasaspdzoo=pd.read_csv('zoo.csv')binary_cols=zoo.columns[zoo.nunique()==2]forcolinbinary_cols:zoo[col]=zoo[col].astype(bool)labels=['Mammal','Bird','Reptile','Fish','Amphibian','Bug','Invertebrate']training_size=int(len(zoo)*0.8)动物园数据集包含101种动物,每种动物都有描述其是否有毛发或产奶等特征。这里,目标类别是动物的生物学类别。 all_labels={i+1:cfori,cinenumerate(labels)}cols=list(zoo.columns)triplets=[]defget_triplet(row,col):ifcol=='class_type':return(all_labels[row[col]],'is_a',row['animal_name'],)#intproperties:ifcolin['legs']:#ifrow[col]>0:return(row['animal_name'],'has'+col,str(row[col])+'_legs')#else:#return()#binaryproperties:ifrow[col]:return(row['animal_name'],'has',str(col))else:return()fori,rowinzoo.iterrows():forcolincols:ifcol=='animal_name':continueifcol=='class_type'andi>training_size:continuetriplet=get_triplet(row,col)iftriplet:triplets.append(triplet)前面的代码将创建我们的三元组。让我们看一些示例,了解它们的样子。以下是我们得到的前20个条目;我们使用triplets[:20]来获取它们: [('aardvark','has','hair'),('aardvark','has','milk'),('aardvark','has','predator'),('aardvark','has','toothed'),('aardvark','has','backbone'),('aardvark','has','breathes'),('aardvark','haslegs','4_legs'),('aardvark','has','catsize'),('Mammal','is_a','aardvark'),('antelope','has','hair'),('antelope','has','milk'),('antelope','has','toothed'),('antelope','has','backbone'),('antelope','has','breathes'),('antelope','haslegs','4_legs'),('antelope','has','tail'),('antelope','has','catsize'),('Mammal','is_a','antelope'),('bass','has','eggs'),('bass','has','aquatic')]前面的代码块展示了一些结果三元组的示例。总共,我们从101行中得到了842个三元组。 现在,我们可以使用networkxAPI将这个数据集加载到图中: importnetworkxasnxclassVocabulary:label2id={}id2label={}deflookup(self,word):"""getwordid;ifnotpresent,insert"""ifwordinself.label2id:returnself.label2id[word]ind=len(self.label2id)self.label2id[word]=indreturninddefinverse_lookup(self,index):iflen(self.id2label)==0:self.id2label={ind:labelforlabel,indinself.label2id.items()}returnself.id2label.get(index,None)vocab=Vocabulary()nx_graph=nx.Graph()for(a,p,b)intriplets:id1,id2=vocab.lookup(a),vocab.lookup(b)nx_graph.add_edge(id1,id2)Vocabulary类是label2id和id2label字典的包装器。我们需要这个类是因为一些图嵌入算法不接受节点或关系的字符串名称。在这里,我们在将概念标签存储到图中之前将其转换为ID。 现在,我们可以用不同的算法对图进行数值嵌入。这里我们将使用Walklets: fromkarateclub.node_embedding.neighbourhoodimportWalkletsmodel_w=Walklets(dimensions=5)model_w.fit(nx_graph)embedding=model_w.get_embedding()前面的代码显示了图中每个概念将由一个5维向量表示。 现在,我们可以测试这些特征是否对预测目标(动物)有用: trainamals=[vocab.label2id[animal]foranimalinzoo.animal_name.values[:training_size]]testimals=[vocab.label2id[animal]foranimalinzoo.animal_name.values[training_size:]]clf=SVC(random_state=42)clf.fit(embedding[trainamals,:],zoo.class_type[:training_size])test_labels=zoo.class_type[training_size:]test_embeddings=embedding[testimals,:]print(end='SupportVectorMachine:Accuracy:')print('{:.3f}'.format(accuracy_score(test_labels,clf.predict(test_embeddings)))print(confusion_matrix(test_labels,clf.predict(test_embeddings)))输出如下所示: SupportVectorMachine:Accuracy=0.809[[5000000][0400000][2001000][0003000][1000000][0000020][0000003]]看起来很不错,尽管这项技术只有在我们拥有超越训练集的知识库时才会变得真正有趣。在不加载数百万个三元组或庞大图表的情况下,很难展示图嵌入。我们将在接下来的小节中提到一些大型知识库。 在本节中,我们将首先涵盖逻辑推理和逻辑证明器,然后再看知识嵌入和Walklets图嵌入的基本概念。 逻辑推理是一个涵盖逻辑推断技术如演绎、归纳和引导的术语。引导推理,经常在专家系统中使用,是从现有观察中检查并推导可能结论(最佳解释)的过程。 专家系统是一种模拟人类专家决策能力的推理系统。专家系统通过推理处理知识体系,主要以if-then-else规则表示(这称为知识库)。 归纳推理是在遵循初始前提和规则的情况下确定结论。在演绎推理中,我们从观察中推断出一个规则。 要应用逻辑推理,语言陈述必须编码为良好形式的逻辑公式,以便我们可以应用逻辑演算。良好形式的公式可以包含以下实体: 例如,推理Socrates是一个人。人是有限的。因此,苏格拉底是有限的,可以用命题逻辑的逻辑陈述来表达,如下: 接下来,我们将看一下逻辑证明器。 在下一小节中,我们将看一下知识嵌入。 知识嵌入(KE)指的是从概念关系中导出的分布式表示。这些通常在知识图谱(KG)中表示。 知识图谱可以在自然语言处理(NLP)应用中使用,以支持决策,它们可以有效地作为查找引擎或推理的工具。 知识嵌入是概念关系的低维表示,可以使用嵌入或更通用的降维方法提取。在下一小节中,我们将看看Walklet嵌入方法。 Walklet算法基本上将Word2Vecskipgram算法应用于图中的顶点,因此我们将根据它们的连接而不是单词(Word2Vec的原始应用)获得概念的嵌入。Walklet算法在图的顶点上对短随机行走进行子采样,作为路径传递给浅层神经网络(见下图),用于skipgram训练。 下图说明了skipgram网络架构,包括输入层、隐藏层和单词预测的输出层: w(t)指的是当前单词(或概念),而w(t-2)、w(t-1)、w(t+1)和w(t+2)指的是当前单词之前和之后的两个单词。我们根据当前单词预测单词上下文。正如我们已经提到的,上下文的大小(窗口大小)是skipgram算法的超参数。 下面是用于Python中逻辑推理的库: 一些用于图嵌入的其他库如下: KarateClub由爱丁堡大学的博士生BenedekRozemberczki维护,包含许多无监督图嵌入算法的实现。 一些关于推理真实世界和/或常识的资源如下: 还有几个大型的现实世界知识数据库可供使用,例如以下内容: 在数理逻辑中,可满足性是关于一个公式在某些解释(参数)下是否有效的问题。如果一个公式在任何解释下都不能成立,我们称其为不可满足。布尔可满足性问题(SAT)是关于一个布尔公式在其参数的任何值下是否有效(可满足)的问题。由于许多问题可以归约为SAT问题,并且存在针对它的求解器和优化方法,SAT问题是一个重要的问题类别。 在这个配方中,我们将以多种方式解决SAT问题。我们将以一个相对简单且深入研究的案例来解释,即n-皇后问题,其中我们尝试在一个n乘n的棋盘上放置皇后,以使得任何列、行和对角线最多只能放置一个皇后。 首先,我们将应用遗传算法(GA),然后是粒子群优化(PSO),最后使用专门的SAT求解器。 我们在这个配方中的一个方法中将使用dd求解器。要安装它,我们还需要omega库。我们可以使用pip命令获取这两个库,如下所示: pipinstallddomega我们稍后将使用ddSAT求解器库,但首先我们将研究一些其他的算法方法。 我们将从遗传算法(GA)开始。 首先,我们将定义染色体的表示方式和如何进行变异。然后,我们将定义一个反馈循环来测试和改变这些染色体。我们将在最后的工作原理部分详细解释算法本身。让我们开始吧: importrandomfromtypingimportOptional,List,TupleclassChromosome:def__init__(self,configuration:Optional[List]=None,nq:Optional[int]=None):ifconfigurationisNone:self.nq=nqself.max_fitness=np.sum(np.arange(nq))self.configuration=[random.randint(1,nq)for_inrange(nq)]else:self.configuration=configurationself.nq=len(configuration)self.max_fitness=np.sum(np.arange(self.nq))deffitness(self):returncost_function(self.configuration)/self.max_fitnessdefmutate(self):ind=random.randint(0,self.nq-1)val=random.randint(1,self.nq)self.configuration[ind]=val上述代码创建了我们的基本数据结构,其中包含一个候选解决方案,可以复制和突变。此代码涉及成本函数。 我们需要一个成本函数,以便知道如何适应我们的基因: defcost_function(props):res=0fori1,q1inenumerate(props[:-1]):fori2,q2inenumerate(props[i1+1:],i1+1):if(q1!=q2)and(abs(i1-i2)!=abs(q1-q2)):res+=1returnres我们可以根据这个成本函数(见fitness()方法)选择基因。 defga_solver(nq):fitness_trace=[]gq=GeneticQueen(nq=nq)generation=0whilenotgq.solution:gq.iterate()if(generation%100)==0:print('Generation{}'.format(generation))print('MaximumFitness:{:.3f}'.format(gq.best_fitness))fitness_trace.append(gq.best_fitness)generation+=1gq.visualize_solution()returnfitness_trace最后,我们可以可视化解决方案。 如果我们运行上述代码,将得到一个看起来像这样的单次运行结果(您的结果可能会有所不同): Generation0MaximumFitness:0.857Generation100MaximumFitness:0.821Generation200MaximumFitness:0.892Generation300MaximumFitness:0.892Generation400MaximumFitness:0.892上述代码给出了以下输出: 这个操作接近8秒才完成。 下图显示了算法每次迭代中最佳染色体的适应性: 在这里,我们可以看到算法的适应性并不总是改善;它也可能下降。我们本可以选择在此处保留最佳染色体。在这种情况下,我们不会看到任何下降(但潜在的缺点是我们可能会陷入局部最小值)。 现在,让我们继续PSO! 在这个配方的这一部分,我们将从头开始实现N皇后问题的PSO算法。让我们开始吧: classParticle:best_fitness:int=0def__init__(self,N=None,props=None,velocities=None):ifpropsisNone:self.current_particle=np.random.randint(0,N-1,N)self.best_state=np.random.randint(0,N-1,N)self.velocities=np.random.uniform(-(N-1),N-1,N)else:self.current_particle=propsself.best_state=propsself.velocities=velocitiesself.best_fitness=cost_function(self.best_state)defset_new_best(self,props:List[int],new_fitness:int):self.best_state=propsself.best_fitness=new_fitnessdef__repr__(self):returnf'{self.__class__.__name__}(\n'+\f'\tcurrent_particle={self.current_particle}\n'+\f'\best_state={self.best_state}\n'+\f'\tvelocities={self.velocities}\n'+\f'\best_fitness={self.best_fitness}\n'+\')'这是我们将要处理的主数据结构。它包含一个候选解决方案。应用PSO将涉及更改一堆这些粒子。我们将在它的工作原理...部分详细解释Particle的工作原理。 我们将使用与我们为GA定义的相同成本函数。该成本函数告诉我们我们的粒子如何适应给定问题-换句话说,一个性质向量有多好。 我们将初始化和主算法封装到一个类中: 我们还想展示我们的解决方案。显示棋盘位置的代码如下: importchessimportchess.svgfromIPython.displayimportdisplaydefshow_board(queens):fen='/'.join([queen_to_str(q)forqinqueens])display(chess.svg.board(board=chess.Board(fen),size=300))下面是PSO的主算法: defparticle_swarm_optimization(N:int,omega:float,phip:float,phig:float,n_particles:int,visualize=False,max_iteration=999999)->List[int]:defprint_best():print(f'iteration{iteration}-bestparticle:{best_particle},score:{best_score}')solved_cost=np.sum(np.arange(N))pso=ParticleSwarm(N,n_particles,omega,phip,phig)iteration=0best_particle,best_score=get_best_particle(particles)scores=[best_score]ifvisualize:print('iteration:',iteration)show_board(best_particle)whilebest_score 正如我们之前提到的,我们将在工作原理...部分解释所有这些内容的工作方式。 我们在这里使用棋盘库进行可视化。 在下面的图表中,您可以看到解决方案在迭代中的质量: 由于所有粒子都保持其最佳解的记录,分数永远不会下降。在第1,323次迭代时,我们找到了一个解决方案,算法停止了。 在Python中的现代SAT求解器中,我们可以将约束定义为简单的函数。 基本上,有一个公式包含所有约束条件。一旦所有约束条件满足(或所有约束条件的合取),就找到了解决方案: defqueens_formula(n):present=at_least_one_queen_per_row(n)rows=at_most_one_queen_per_line(True,n)cols=at_most_one_queen_per_line(False,n)slash=at_most_one_queen_per_diagonal(True,n)backslash=at_most_one_queen_per_diagonal(False,n)s=conj([present,rows,cols,slash,backslash])returns这是at_least_one_queen_per_row的约束条件: defat_least_one_queen_per_row(n):c=list()foriinrange(n):xijs=[_var_str(i,j)forjinrange(n)]s=disj(xijs)c.append(s)returnconj(c)在这里,我们对每行上的皇后进行析取。 主运行如下所示: 下面是八皇后问题的示例解决方案: 文本输出如下所示: queens:8time:4.775595426559448(sec)node:-250797totalnodes:250797numbersolutions:92此求解器不仅获得了所有的解决方案(我们只显示了其中一个),而且比遗传算法快大约两倍! 在本节中,我们将解释在此配方中使用的不同方法,从遗传算法开始。 在本质上,遗传算法很简单:我们维护一组候选解决方案(称为染色体),并且我们有两种操作可以用来改变它们: 一个染色体存储在configuration中的候选解决方案。在初始化染色体时,我们必须给它皇后的数量或者初始配置。在本章的前文中,我们已经讨论了染色体的实际含义。如果没有给定配置,则需要使用列表推导创建一个,比如[random.randint(1,nq)for_inrange(nq)]。 一个染色体可以计算自己的适应度;在这里,我们使用了先前使用的相同成本函数,但这次我们将其缩放到0到1之间,其中1表示我们找到了一个解决方案,介于其中的任何值显示我们距离解决方案有多接近。染色体也可以对自己进行突变;也就是说,它可以随机改变其值之一。 算法的每一次迭代,我们都通过这两个操作创建新的染色体代。 我们在这里非常宽泛地表达了最后一步。基本上,我们可以决定何时适应度足够高以及我们想要迭代多少次。这些是我们的停止标准。 这在我们的GeneticQueen.iterate()的实现中非常清晰,因此为了可视化目的,让我们再看一眼(仅稍微简化): defiterate(self):new_population=[]foriinrange(len(self.population)):p1,p2=self.get_parents()child=Chromosome(self.cross_over(p1,p2))ifrandom.random() 我们通过按其适应度加权随机选择父母,其中适应度最高的被选择的可能性更大。在我们的实现中,cross-over函数会随机在每个参数中的两个父母之间做出决策。 为GA必须做出的主要超参数和主要决策如下: 正如我们所见,遗传算法非常灵活且直观。在接下来的部分,我们将看看PSO。 我们以Particle数据结构开始我们的实现。要初始化一个粒子,我们传入皇后数量(N)或者我们的速度和参数向量。基本上,一个粒子有一个配置,或者说一组参数-在这种情况下是一个向量,它与问题的某个程度匹配(current_particle),以及一个速度(类似于学习率)。每个粒子的属性向量表示皇后的位置。 PSO然后以特定的方式对粒子应用变化。PSO结合了局部搜索和全局搜索;也就是说,在每个粒子处,我们试图将搜索引导向全局最佳粒子和过去最佳粒子。一个粒子保持其最佳实例的记录;也就是说,其最佳参数的向量和相应的得分。我们还保持参数的相应速度。这些速度可以根据正在使用的公式而减慢、增加或改变方向。 PSO需要一些参数,如下所示(大多数这些在我们的实现中已命名;这里省略了那些特定于我们的九皇后问题的参数): 在我们的PSO问题中,有两个增量,delta_p和delta_g,其中p和g分别代表粒子(particle)和全局(global)。这是因为其中一个是根据粒子的历史最佳计算的,另一个是根据粒子的全局最佳计算的。 更新根据以下代码计算: delta_p=particle.best_state-particle.current_particledelta_g=best_particle-particle.current_particleupdate=(rp*phip*delta_p+\rg*phig*delta_g)#localvsglobal这里,rp和rg是随机数,phip和phig分别是局部和全局因子。它们分别指一个唯一的粒子或所有粒子,如delta_p和delta_g变量所示。 还有另一个参数omega,它调节当前速度的衰减。在每次迭代中,根据以下公式计算新的速度: particle.velocities=omega*particle.velocities+update接着,根据它们的速度递增粒子参数。 请注意,算法对于phip、phig和omega的选择非常敏感。 我们的成本函数(或好度函数)根据给定的皇后配置为每个粒子计算分数。这个配置被表示为在范围]0,N-1.中的索引列表对于每对皇后,函数检查它们是否在对角线、垂直或水平方向上重叠。每个不冲突的检查都给予一个点,因此最大的得分是![。这对于8皇后问题是28。 我们在配方中使用的dd求解器,使用二进制决策图(BDD)工作,这些图是由RandalBryant(基于图的布尔函数操作算法,1986年)引入的。二进制决策图(有时称为分支程序)将约束表示为布尔函数,而不是其他编码方式,如否定范式。 在BDD中,一个算法或一组约束被表示为在维度为n的布尔域上的布尔函数,其评估为真或假: 这意味着我们可以将问题表示为二叉树或等效地表示为真值表。 为了说明这一点,让我们看一个例子。我们可以枚举所有关于我们的二进制变量(x1,x2和x3)的状态,然后得出一个最终状态,即f的结果。以下真值表总结了我们变量的状态,以及我们的函数评估: 这对应于以下二叉树: 二叉树和真值表具有高度优化的库实现,因此它们可以运行非常快。这解释了我们如何如此快速地得到结果。 Python中还有许多其他SAT求解器,其中一些如下所示: 除了像scipy和numpy这样的标准依赖项外,我们还将使用scikit-opt库,该库实现了许多不同的群体智能算法。 群体智能是分散式、自组织系统的集体行为,这种行为在观察者眼中表现出明显的智能性。这个概念在基于人工智能的工作中被使用。自然系统,如蚂蚁群、鸟群、鹰的捕猎、动物群集和细菌生长,在全局层面展示出一定水平的智能,尽管蚂蚁、鸟类和鹰通常表现出相对简单的行为。受生物学启发的群体算法包括遗传算法、粒子群优化、模拟退火和蚁群优化。 我们可以使用pip安装scikit-opt,如下所示: pipinstallscikit-opt现在,我们准备解决旅行推销员问题。 正如我们之前提到的,我们将以两种不同的方式解决最短公交路线问题。 首先,我们需要为公交车站创建一组坐标(经度,纬度)。问题的难度取决于站点的数量(N)。在这里,我们将N设置为15: importnumpyasnpN=15stops=np.random.randint(0,100,(N,2))我们还可以预先计算站点之间的距离矩阵,如下所示: 我们将从模拟退火开始。 deffind_tour(stops,distance_matrix,iterations=10**5):defcalc_distance(i,j):"""sumofdistancetoandfromiandjintour"""returnsum(distance_matrix[tour[k],tour[k+1]]forkin[j-1,j,i-1,i])n=len(stops)tour=np.random.permutation(n)lengths=[]fortemperatureinnp.logspace(4,0,num=iterations):i=np.random.randint(n-1)#city1j=np.random.randint(i+1,n)#city2old_length=calc_distance(i,j)#swapiandj:tour[[i,j]]=tour[[j,i]]new_length=calc_distance(i,j)ifnp.exp((old_length-new_length)/temperature) 我们还可以绘制算法的内部距离度量。请注意,这个内部成本函数在约800,000次迭代之前一直下降: 现在,让我们尝试蚁群优化算法。 在这里,我们正在从库中加载实现。我们将在它的工作原理...部分解释细节: fromsko.ACAimportACA_TSPdefcal_total_distance(tour):returnsum([distance_matrix[tour[i%N],tour[(i+1)%N]]foriinrange(N)])aca=ACA_TSP(func=cal_total_distance,n_dim=N,size_pop=N,max_iter=200,distance_matrix=distance_matrix)best_x,best_y=aca.run()我们使用基于我们之前获取的点距离的距离计算(distance_matrix)。 再次,我们可以绘制最佳路径和路径距离随迭代次数的变化情况,如下所示: 再次,我们可以看到最终路径,这是我们优化的结果(左侧子图),以及随着算法迭代距离逐渐减少的路径(右侧子图)。 最短巴士路线问题是旅行商问题(TSP)的一个示例,而TSP又是组合优化的一个众所周知的示例。 组合优化是指使用组合技术来解决离散优化问题。换句话说,它是在一组对象中找到解决方案的行为。在这种情况下,“离散”意味着有限数量的选项。组合优化的智能部分在于减少搜索空间或加速搜索。旅行商问题、最小生成树问题、婚姻问题和背包问题都是组合优化的应用。 TSP可以表述如下:给定要访问的城镇列表,找出遍历所有城镇并回到起点的最短路径是什么?TSP在规划、物流和微芯片设计等领域有应用。 现在,让我们更详细地看一下模拟退火和蚁群优化。 在这个示例中,我们随机初始化了我们的城市旅游路线,然后进行了模拟退火的迭代。SA的主要思想是,变化的速率取决于一定的温度。在我们的实现中,我们从4逻辑地降低了温度到0。在每次迭代中,我们尝试交换(也可以尝试其他操作)路径(旅游路线)中两个随机巴士站点的索引i和j,其中i 温度在我们需要决定是否接受交换时发挥作用。我们计算路径长度变化前后的指数差: 然后,我们生成一个随机数。如果这个随机数小于我们的表达式,我们就接受这个变化;否则,我们撤销它。 正如其名称所示,蚁群优化受到蚂蚁群体的启发。让我们使用蚂蚁分泌的信息素作为类比:这里,代理人具有候选解决方案,越接近解决方案,越有吸引力。 总体而言,蚂蚁编号k从状态x转移到状态y的概率如下: Tau是在x和y之间沉积的信息素路径。eta参数控制信息素的影响,其中eta的beta次幂是状态转换(例如转换成本的倒数)。信息素路径根据包括状态转换在内的整体解决方案的好坏而更新。 在这里,scikit-opt函数起到了重要作用。我们只需传递几个参数,如距离函数、点数、种群中的蚂蚁数量、迭代次数和距离矩阵,然后调用run()。 scikit-opt是一个强大的启发式算法库。它包括以下算法: 诸如天花、结核病和黑死病等大流行病,长期以来显著影响了人类群体。截至2020年,新冠肺炎正在全球范围内传播,关于如何在尽可能少的伤亡情况下控制病毒的政治和经济问题已广泛讨论。 在英国,最初的反应是依赖群体免疫,只有在其他国家已经实施封锁数周后才宣布封锁。由于无法应对,国民健康服务系统(NHS)使用临时床位并租用商业医院的床位。 多代理系统(MAS)是由参与者(称为代理)组成的计算机模拟。这些个体代理可以根据启发式或基于强化学习作出响应。此外,这些代理相互响应以及对环境的响应的系统行为可以应用于研究以下主题: 在这个食谱中,一个相对简单的多代理模拟将展示不同的响应如何导致疫情的致命人数和传播方式上的差异。 我们将使用mesa多代理建模库来实现我们的多代理模拟。 用于此操作的pip命令如下: pipinstallmesa现在,我们已经准备好了! 首先,我们将通过Person类来定义我们的代理: classPerson(Agent):def__init__(self,unique_id,model):super().__init__(unique_id,model)self.alive=Trueself.infected=Falseself.hospitalized=Falseself.immune=Falseself.in_quarantine=False#self-quarantineself.time_infected=0此定义将代理定义为拥有健康和隔离状态的人。 我们仍然需要一些方法来改变其他属性的变化方式。我们不会详细介绍所有这些方法,只是介绍那些足以让你理解所有内容如何结合在一起的方法。我们需要理解的核心是代理在感染时做什么。基本上,在感染期间,我们需要了解代理是否会传染给其他人,是否会因感染而死亡,或者是否会康复: 现在,我们需要一种方法让我们的代理记录它们的位置,这在mesa中被称为MultiGrid: defmove_to_next(self):possible_steps=self.model.grid.get_neighborhood(self.pos,moore=True,include_center=False)new_position=self.random.choice(possible_steps)self.model.grid.move_agent(self,new_position)这是相对直接的。如果代理移动,它们只在它们的邻域内移动;也就是说,下一个相邻的单元。 被称为step()方法的入口方法在每个周期(迭代)都会被调用: defstep(self):ifself.alive:self.move()如果代理活着,它们在每一步都会移动。这是它们移动时会发生的事情: defmove(self):ifself.in_quarantineorself.model.lockdown:passelse:self.move_to_next()ifself.infected:self.while_infected()这结束了我们的代理,也就是Person的主要逻辑。现在,让我们看看在模型层面上如何将所有内容整合在一起。这可以在model.py中的Simulation类中找到。 让我们看看代理是如何创建的: defcreate_agents(self):foriinrange(self.num_agents):a=Person(i,self)ifself.random.random() 我们还需要定义一些数据收集器,如下所示: defset_reporters(self):self.datacollector=DataCollector(model_reporters={'ActiveCases':active_cases,'Deaths':total_deaths,'Immune':total_immune,'Hospitalized':total_hospitalized,'Lockdown':get_lockdown,})此字典列表中的变量在每个周期中都会追加,以便我们可以绘图或进行统计评估。例如,让我们看看active_cases函数是如何定义的: defactive_cases(model):returnsum([1foragentinmodel.schedule.agentsifagent.infected])当被调用时,该函数会迭代模型中的代理,并计算状态为infected的代理数量。 同样地,就像对Person一样,Simulation的主要逻辑在step()方法中,该方法推进模型一个周期: defstep(self):self.datacollector.collect(self)self.hospital_takeup=self.datacollector.model_vars['Hospitalized'][-1] 我们将在这些模拟中使用与之前相同的一组变量。我们设置它们以便它们大致对应于英国,按照1/1,000的因子: scale_factor=0.001area=242495#km2ukside=int(math.sqrt(area))#492sim_params={'grid_x':side,'grid_y':side,'density':259*scale_factor,#populationdensityuk,'initial_infected':0.05,'infect_rate':0.1,'recovery_period':14*12,'critical_rate':0.05,'hospital_capacity_rate':.02,'active_ratio':8/24.0,'immunity_chance':1.0,'quarantine_rate':0.6,'lockdown_policy':lockdown_policy,'cycles':200*12,'hospital_period':21*12,}我们将在它是如何工作的……部分解释网格的动机。 首先,让我们看看在没有引入封锁的情况下的数据。如果我们的policy函数始终返回False,我们可以创建这个策略: 总体而言,我们有8,774例死亡。 在这里,我们可以看到随着这一政策早期解除封锁,多次感染的波动: deflockdown_policy(infected,deaths,population_size):if((max(infected[-5*10:])/population_size)>0.6and(len(deaths)>2anddeaths[-1]>deaths[-2])):return7*12return0当我们运行这个模拟时,我们得到完全不同的结果,如下所示: 让我们将这与一个非常谨慎的政策进行比较,即每次死亡率上升或者感染率在(大致)3周内超过20%时宣布封锁: deflockdown_policy(infected,deaths,population_size):ifinfected[-1]/population_size>0.2:return21*12return0仅有一次封锁,我们得到了以下的图表,显示了大约600人的总死亡人数: 您可以更改这些参数或者调整逻辑,以创建更复杂和/或更真实的模拟。 模拟非常简单:它由代理组成,并且在迭代(称为周期)中进行。每个代理代表人群的一部分。 在这里,某个群体被这种疾病感染。在每个周期(对应1小时)内,被感染者可以去医院(如果有空位)、死亡、或者朝着康复迈进。他们还可以进入隔离状态。在尚未康复、未隔离且尚未死亡的情况下,他们可以在与他们空间接近的人群中传播疾病。恢复时,代理可以获得免疫力。 在每个周期内,代理可以移动。如果他们不在隔离中或者国家实施了封锁,他们将会移动到一个新的位置;否则,他们将保持原地。如果一个人被感染,他们可以死亡、去医院、康复、传染给其他人,或者进入隔离状态。 根据死亡和感染率,可以宣布国家封锁,这是我们模拟的主要焦点:国家封锁的引入如何影响死亡人数? 我们需要考虑不同的变量。其中一个是人口密度。我们可以通过将我们的代理放在地图或网格上来引入人口密度,网格大小由grid_x和grid_y定义。infect_rate参数必须根据网格大小和人口密度进行调整。 我们在这里需要考虑更多的参数,比如以下的参数: 在Simulation的step()方法中,我们进行了数据收集。然后,根据free_beds变量检查医院是否可以接收更多患者。接着,我们运行了代理器self.schedule.step()。如果我们处于封锁状态,我们开始倒计时。封锁状态由False变量到lockdown_period变量设置(在Python的鸭子类型中有所改动)。 为了获得更快的反馈,让我们实时绘制模拟循环,如下所示: %matplotlibinlinefromcollectionsimportdefaultdictfrommatplotlibimportpyplotaspltfromIPython.displayimportclear_outputdeflive_plot(data_dict,figsize=(7,5),title=''):clear_output(wait=True)plt.figure(figsize=figsize)forlabel,dataindata_dict.items():plt.plot(data,label=label)plt.title(title)plt.grid(True)plt.xlabel('iteration')plt.legend(loc='best')plt.show()model=Simulation(sim_params)cycles_to_run=sim_params.get('cycles')print(sim_params)forcurrent_cycleinrange(cycles_to_run):model.step()if(current_cycle%10)==0:live_plot(model.datacollector.model_vars)print('Totaldeaths:{}'.format(model.datacollector.model_vars['Deaths'][-1]))这将持续(每10个周期)更新我们的模拟参数绘图。如果没有达到预期效果,我们可以中止它,而不必等待完整模拟。 国际象棋是一种两人对弈的棋盘游戏,自15世纪以来作为智力游戏而广受欢迎。在20世纪50年代,计算机击败了第一个人类玩家(一个完全的新手),然后在1997年击败了人类世界冠军。此后,它们已经发展到拥有超人类的智能。编写国际象棋引擎的主要困难之一是搜索许多变化和组合并选择最佳策略。 在这个示例中,我们将使用蒙特卡洛树搜索来创建一个基本的国际象棋引擎。 我们将使用python-chess库进行可视化,获取有效移动,并知道状态是否终止。我们可以使用pip命令安装它,如下所示: pipinstallpython-chess我们将使用这个库进行可视化,生成每个位置的有效移动,并检查是否达到了最终位置。 首先,我们将查看我们将用来定义我们的树搜索类的代码,然后看看搜索是如何工作的。之后,我们将学习如何将其适应于国际象棋。 树搜索是一种利用搜索树作为数据结构的搜索方法。通常情况下,在搜索树中,节点(或叶子)表示一个概念或情况,这些节点通过边(分支)连接。树搜索遍历树以得出最佳解决方案。 让我们首先实现树搜索类: importrandomclassMCTS:def__init__(self,exploration_weight=1):self.Q=defaultdict(int)self.N=defaultdict(int)self.children=dict()self.exploration_weight=exploration_weight我们将在它的工作原理...部分更详细地讨论这些变量。我们很快将向这个类添加更多方法。 我们的树搜索中的不同步骤在我们的do_rollout方法中执行: defdo_rollout(self,node):path=self._select(node)leaf=path[-1]self._expand(leaf)reward=self._simulate(leaf)self._backpropagate(path,reward)每个rollout()调用都会向我们的树中添加一层。 让我们依次完成四个主要步骤: def_select(self,node):path=[]whileTrue:path.append(node)ifnodenotinself.childrenornotself.children[node]:returnpathunexplored=self.children[node]-self.children.keys()ifunexplored:n=unexplored.pop()path.append(n)returnpathnode=self._select(random.choice(self.children[node]))这是递归定义的,因此如果我们找不到未探索的节点,我们就会探索当前节点的一个子节点。 def_expand(self,node):ifnodeinself.children:returnself.children[node]=node.find_children()此函数使用后代(或子节点)更新children字典。这些节点是从当前节点通过单个移动可以到达的任何有效棋盘位置。 def_simulate(self,node):invert_reward=TruewhileTrue:ifnode.is_terminal():reward=node.reward()return1-rewardifinvert_rewardelserewardnode=node.find_random_child()invert_reward=notinvert_reward此函数执行模拟直到游戏结束。 def_backpropagate(self,path,reward):fornodeinreversed(path):self.N[node]+=1self.Q[node]+=rewardreward=1-reward最后,我们需要一种选择最佳移动的方法,可以简单地通过查看Q和N字典并选择具有最大效用(奖励)的后代来实现: defchoose(self,node):ifnodenotinself.children:returnnode.find_random_child()defscore(n):ifself.N[n]==0:returnfloat('-inf')returnself.Q[n]/self.N[n]returnmax(self.children[node],key=score)我们将看不见的节点的分数设置为-infinity,以避免选择未见过的移动。 现在,让我们学习如何为我们的国际象棋实现使用一个节点。 因为这基于python-chess库,所以实现起来相对容易: importhashlibimportcopyclassChessGame:deffind_children(self):ifself.is_terminal():returnset()return{self.make_move(m)forminself.board.legal_moves}deffind_random_child(self):ifself.is_terminal():returnNonemoves=list(self.board.legal_moves)m=choice(moves)returnself.make_move(m)defplayer_win(self,turn):ifself.board.result()=='1-0'andturn:returnTrueifself.board.result()=='0-1'andnotturn:returnTruereturnFalsedefreward(self):ifself.board.result()=='1/2-1/2':return0.5ifself.player_win(notself.board.turn):return0.0defmake_move(self,move):child=self.board.copy()child.push(move)returnChessGame(child)defis_terminal(self):returnself.board.is_game_over()我们在这里省略了一些方法,但不要担心-我们将在工作原理...部分进行覆盖。 现在一切准备就绪,我们终于可以下棋了。 让我们下一盘国际象棋! 以下只是一个简单的循环,带有一个图形提示显示棋盘位置: fromIPython.displayimportdisplayimportchessimportchess.svgdefplay_chess():tree=MCTS()game=ChessGame(chess.Board())display(chess.svg.board(board=game.board,size=300))whileTrue:move_str=input('entermove:')move=chess.Move.from_uci(move_str)ifmovenotinlist(game.board.legal_moves):raiseRuntimeError('Invalidmove')game=game.make_move(move)display(chess.svg.board(board=game.board,size=300))ifgame.is_terminal():breakfor_inrange(50):tree.do_rollout(game)game=tree.choose(game)print(game)ifgame.is_terminal():break然后,您应该被要求输入一个移动到棋盘上某个位置的移动。每次移动后,将显示一个棋盘,显示当前棋子的位置。这可以在以下截图中看到: 注意移动必须以UCI符号输入。如果以“squaretosquare”格式输入移动,例如a2a4,它应该总是有效。 这里使用的游戏强度并不是非常高,但是在玩弄它时应该仍然很容易看到一些改进。请注意,此实现没有并行化。 在蒙特卡洛树搜索(MCTS)中,我们应用蒙特卡洛方法-基本上是随机抽样-以获取关于玩家所做移动强度的概念。对于每个移动,我们随机进行移动直到游戏结束。如果我们做得足够频繁,我们将得到一个很好的估计。 树搜索维护不同的变量: 这些字典很重要,因为我们通过奖励对节点(棋盘状态)的效用进行平均,并根据它们被访问的频率(或者更确切地说,它们被访问的次数越少)对节点进行抽样。 搜索的每次迭代包括四个步骤: 选择步骤,在其最基本的形式下,寻找一个尚未探索过的节点(例如一个棋盘位置)。 扩展步骤将children字典更新为所选节点的子节点。 模拟步骤很简单:我们执行一系列随机移动,直到到达终止位置,并返回奖励。由于这是一个两人零和棋盘游戏,当轮到对手时,我们必须反转奖励。 反向传播步骤按照反向方向的路径将奖励与探索路径中的所有节点关联起来。_backpropagate()方法沿着一系列移动(路径)回溯所有节点,赋予它们奖励,并更新访问次数。 至于节点的实现,由于我们将它们存储在之前提到的字典中,所以节点必须是可散列且可比较的。因此,在这里,我们需要实现__hash__和__eq__方法。我们以前没有提到它们,因为我们不需要它们来理解算法本身,所以我们在这里补充了它们以保持完整性: def__hash__(self):returnint(hashlib.md5(self.board.fen().encode('utf-8')).hexdigest()[:8],16)def__eq__(self,other):returnself.__hash__()==other.__hash__()def__repr__(self):return'\n'+str(self.board)当你在调试时,__repr__()方法可能非常有用。 对于ChessGame类的主要功能,我们还需要以下方法: 请再次查看ChessGame的实现,以了解它的运作方式。 MCTS的一个重要扩展是上置信树(UCTs),用于平衡探索和利用。在9x9棋盘上达到段位的第一个围棋程序使用了带有UCT的MCTS。 要实现UCT扩展,我们需要回到我们的MCTS类,并进行一些更改: def_uct_select(self,node):log_N_vertex=math.log(self.N[node])defuct(n):returnself.Q[n]/self.N[n]+self.exploration_weight*math.sqrt(log_N_vertex/self.N[n])returnmax(self.children[node],key=uct)uct()函数应用上置信界限(UCB)公式,为一个移动提供一个得分。节点n的得分是从节点n开始的所有模拟中赢得的模拟数量的总和,加上一个置信项: 在这里,c是一个常数。 接下来,我们需要替换代码的最后一行,以便使用_uct_select()代替_select()进行递归。在这里,我们将替换_select()的最后一行,使其陈述如下: node=self._uct_select(node)进行此更改应该会进一步增强代理程序的游戏强度。 强化学习是指通过优化它们在环境中的行动来自动化问题解决的目标驱动代理的发展。这涉及预测和分类可用数据,并训练代理成功执行任务。通常,代理是一个能够与环境进行交互的实体,学习是通过将来自环境的累积奖励的反馈来指导未来的行动。 可以区分三种不同类型的强化学习: 在本章中,我们将从多臂赌博机的角度开始介绍强化学习在网站优化中的相对基础的用例,我们将看到一个代理和一个环境以及它们的交互。然后,我们将进入控制的简单演示,这时情况会稍微复杂一些,我们将看到一个代理环境和基于策略的方法REINFORCE。最后,我们将学习如何玩二十一点,我们将使用深度Q网络(DQN),这是一个基于价值的算法,2015年由DeepMind创建用于玩Atari游戏的AI。 在本章中,我们将涵盖以下步骤: 在这个步骤中,我们将处理网站优化。通常,需要对网站进行变更(或者更好的是,单一变更)来观察其效果。在所谓的A/B测试的典型情况下,将系统地比较两个版本的网页。A/B测试通过向预定数量的用户展示网页版本A和版本B来进行。之后,计算统计显著性或置信区间,以便量化点击率的差异,目的是决定保留哪种网页变体。 这个网站优化的使用案例将帮助我们介绍代理和环境的概念,并展示探索与利用之间的权衡。我们将在工作原理...部分解释这些概念。 为了实施我们的方案,我们需要两个组件: 由于我们仅使用标准的Python,无需安装任何东西,我们可以直接开始实施我们的方案: importrandomimportnumpyasnpclassBandit:def__init__(self,K=2,probs=None):self.K=KifprobsisNone:self.probs=[random.random()for_inrange(self.K)]else:assertlen(probs)==Kself.probs=probsself.probs=list(np.array(probs)/np.sum(probs))self.best_probs=max(self.probs)defplay(self,i):ifrandom.random() classAgent:def__init__(self,env):self.env=envself.listeners={}self.metrics={}self.reset()defreset(self):forkinself.metrics:self.metrics[k]=[]defadd_listener(self,name,fun):self.listeners[name]=funself.metrics[name]=[]defrun_metrics(self,i):forkey,funinself.listeners.items():fun(self,i,key)defrun_one_step(self):raiseNotImplementedErrordefrun(self,n_steps):raiseNotImplementedError任何代理都需要一个环境来进行交互。它需要做出单一的决策(run_one_step(self)),为了看到其决策的好坏,我们需要运行一个模拟(run(self,n_steps))。 代理将包含一个指标函数的查找列表,并且还会继承一个指标收集功能。我们可以通过run_metrics(self,i)函数来运行指标收集。 我们在这里使用的策略称为UCB1。我们将在如何做...部分解释这个策略: 我们应该看一下run_one_step(self)方法,它通过选择最佳的乐观选择来做出单一选择。run(self,n_step)方法运行一系列选择,并从环境中获取反馈。 我们的跟踪函数是update_regret()和update_rank_corr(): random.seed(42.0)bandit=Bandit(20)agent=UCB1(bandit,alpha=2.0)agent.add_listener('regret',update_regret)agent.add_listener('corr',update_rank_corr)agent.run(5000)因此,我们有20个不同的网页选择,并收集定义的regret和corr,并进行5000次迭代。如果我们绘制这个,我们可以了解这个代理的表现如何: 对于第二次运行,我们将alpha更改为0.5,因此我们将进行较少的探索: 因此,较少的探索使得我们的代理模型对环境的真实参数了解程度较差。这是因为较少的探索使得较低排名特征的排序没有收敛。尽管它们被排名为次优,但它们还没有被选择足够多次来确定它们是最差还是次差,例如。这就是我们在较少探索时看到的情况,这也可能是可以接受的,因为我们可能只关心知道哪种选择是最佳的。 在这个示例中,我们处理了网站优化问题。我们模拟用户对不同版本网页的选择,同时实时更新每个变体的统计数据,以及应该显示的频率。此外,我们比较了探索性场景和更加利用性场景的优缺点。 运行方式如下: UCB算法遵循在不确定性面前保持乐观的原则,通过选择在其置信区间上UCB最高的臂而不是估计奖励最高的臂来执行动作。它使用简单的均值估计器来估算动作奖励。 前述方程式中的第二项量化了不确定性。不确定性越低,我们越依赖Q(a)。不确定性随着动作播放次数的增加而线性减少,并随着轮数的对数增加而对数增加。 有许多变体的赌博算法来处理更复杂的场景,例如,选择之间切换的成本,或者具有有限生命周期的选择,例如秘书问题。秘书问题的基本设置是你想从一个有限的申请者池中雇佣一名秘书。每位申请者按随机顺序进行面试,面试后立即做出明确的决定(是否雇佣)。秘书问题也被称为婚姻问题。 作为阅读材料,我们建议这些书籍: 倒立摆是OpenAIGym中的一个控制任务,已经研究了多年。虽然与其他任务相比相对简单,但它包含了我们实施强化学习算法所需的一切,我们在这里开发的一切也可以应用于其他更复杂的学习任务。它还可以作为在模拟环境中进行机器人操作的示例。选择一个不那么苛刻的任务的好处在于训练和反馈更快。 在OpenAIGym环境的以下截图中展示了倒立摆任务,通过将购物车向左或向右移动来平衡一个立杆: 在这个示例中,我们将使用PyTorch实现REINFORCE策略梯度方法来解决倒立摆任务。让我们开始吧。 有许多库提供了测试问题和环境的集合。其中一个集成最多的库是OpenAIGym,我们将在这个示例中使用它: pipinstallgym现在我们可以在我们的示例中使用OpenAIGym了。 OpenAIGym为我们节省了工作——我们不必自己定义环境,确定奖励信号,编码环境或说明允许哪些动作。 首先,我们将加载环境,定义一个深度学习策略用于动作选择,定义一个使用此策略来选择执行动作的代理,最后我们将测试代理在我们的任务中的表现: 我们可以加载环境并打印这些参数如下: importgymenv=gym.make('CartPole-v1')print('observationspace:{}'.format(env.observation_space))print('actions:{}'.format(env.action_space.n))#observationspace:Box(4,)#actions:2因此,我们确认我们有四个输入和两个动作,我们的代理将类似于前面的示例优化网站定义,只是这次我们会在代理外部定义我们的神经网络。 代理将创建一个策略网络,并使用它来做出决策,直到达到结束状态;然后将累积奖励馈送到网络中进行学习。让我们从策略网络开始。 importtorchasTimporttorch.nnasnnimporttorch.nn.functionalasFimportnumpyasnpclassPolicyNetwork(nn.Module):def__init__(self,lr,n_inputs,n_hidden,n_actions):super(PolicyNetwork,self).__init__()self.lr=lrself.fc1=nn.Linear(n_inputs,n_hidden)self.fc2=nn.Linear(n_hidden,n_actions)self.optimizer=optim.Adam(self.parameters(),lr=self.lr)self.device=T.device('cuda:0'ifT.cuda.is_available()else'cpu:0')self.to(self.device)defforward(self,observation):x=T.Tensor(observation.reshape(-1).astype('float32'),).to(self.device)x=F.relu(self.fc1(x))x=F.softmax(self.fc2(x),dim=0)returnx这是一个神经网络模块,用于学习策略,换句话说,从观察到动作的映射。它建立了一个具有一层隐藏层和一层输出层的两层神经网络,其中输出层中的每个神经元对应于一个可能的动作。我们设置了以下参数: classAgent:eps=np.finfo(np.float32).eps.item()def__init__(self,env,lr,params,gamma=0.99):self.env=envself.gamma=gammaself.actions=[]self.rewards=[]self.policy=PolicyNetwork(lr=lr,**params)defchoose_action(self,observation):output=self.policy.forward(observation)action_probs=T.distributions.Categorical(output)action=action_probs.sample()log_probs=action_probs.log_prob(action)action=action.item()self.actions.append(log_probs)returnaction,log_probs代理人评估策略以执行动作并获得奖励。gamma是折扣因子。 使用choose_action(self,observation)方法,我们的代理根据观察选择动作。动作是根据我们网络的分类分布进行抽样。 我们省略了run()方法,其内容如下: defrun(self):state=self.env.reset()probs=[]rewards=[]done=Falseobservation=self.env.reset()t=0whilenotdone:action,prob=self.choose_action(observation.reshape(-1))probs.append(prob)observation,reward,done,_=self.env.step(action)rewards.append(reward)t+=1policy_loss=[]returns=[]R=0forrinrewards[::-1]:R=r+self.gamma*Rreturns.insert(0,R)returns=T.tensor(returns)returns=(returns-returns.mean())/(returns.std()+self.eps)forlog_prob,Rinzip(probs,returns):policy_loss.append(-log_prob*R)if(len(policy_loss))>0:self.policy.optimizer.zero_grad()policy_loss=T.stack(policy_loss,0).sum()policy_loss.backward()self.policy.optimizer.step()returntrun(self)方法类似于之前的示例,优化网站,在环境中运行完整的模拟直到结束。这是直到杆几乎倒下或达到500步(即env._max_episode_steps的默认值)为止。 env._max_episode_steps=10000input_dims=env.observation_space.low.reshape(-1).shape[0]n_actions=env.action_space.nagent=Agent(env=env,lr=0.01,params=dict(n_inputs=input_dims,n_hidden=10,n_actions=n_actions),gamma=0.99,)update_interval=100scores=[]score=0n_episodes=25000stop_criterion=1000foriinrange(n_episodes):mean_score=np.mean(scores[-update_interval:])if(i>0)and(i%update_interval)==0:print('Iteration{},averagescore:{:.3f}'.format(i,mean_score))T.save(agent.policy.state_dict(),filename)score=agent.run()scores.append(score)ifscore>=stop_criterion:print('Stopping.Iteration{},averagescore:{:.3f}'.format(i,mean_score))break我们应该看到以下输出: 我们可以看到我们的策略正在持续改进——网络正在成功学习如何操作杆车。请注意,您的结果可能会有所不同。网络可能学习得更快或更慢。 在下一节中,我们将深入了解这个算法的实际工作原理。 在这个案例中,我们看了一个杆车控制场景中的基于策略的算法。让我们更详细地看看其中的一些内容。 策略梯度方法通过给定的梯度上升找到一个策略,以最大化相对于策略参数的累积奖励。我们实现了一种无模型的基于策略的方法,即REINFORCE算法(R.Williams,《简单的统计梯度跟随算法用于连接主义强化学习》,1992年)。 这就是我们在策略网络中所做的,这有助于我们做出我们的动作选择。 在参数初始化之后,REINFORCE算法通过每次执行动作时应用此更新函数来进行。 还有一些其他的事情可以让我们玩得更开心。首先,我们想看到代理与杆互动,其次,我们可以使用库来避免从零开始实施代理。 我们可以玩数百场游戏或尝试不同的控制任务。如果我们真的想在Jupyter笔记本中观看我们的代理与环境的互动,我们可以做到: fromIPythonimportdisplayimportmatplotlib.pyplotasplt%matplotlibinlineobservation=env.reset()img=plt.imshow(env.render(mode='rgb_array'))for_inrange(100):img.set_data(env.render(mode='rgb_array'))display.display(plt.gcf())display.clear_output(wait=True)action,prob=agent.choose_action(observation)observation,_,done,_=agent.env.step(action)ifdone:break现在我们应该看到我们的代理与环境互动了。 如果您正在远程连接(例如在GoogleColab上运行),您可能需要做一些额外的工作: !sudoapt-getinstall-yxvfbffmpeg!pipinstall'gym==0.10.11'!pipinstall'imageio==2.4.0'!pipinstallPILLOW!pipinstall'pyglet==1.3.2'!pipinstallpyvirtualdisplaydisplay=pyvirtualdisplay.Display(visible=0,size=(1400,900)).start()在接下来的部分,我们将使用一个实现在库中的强化学习算法,RLlib。 我们可以使用Python库和包中的实现,而不是从头开始实现算法。例如,我们可以训练PPO算法(Schulman等人,《ProximalPolicyOptimizationAlgorithms》,2017),该算法包含在RLlib包中。RLlib是我们在《Python人工智能入门》第一章中遇到的Ray库的一部分。PPO是一种政策梯度方法,引入了一个替代目标函数,可以通过梯度下降进行优化: importrayfromrayimporttunefromray.rllib.agents.ppoimportPPOTrainerray.init()trainer=PPOTraineranalysis=tune.run(trainer,stop={'episode_reward_mean':100},config={'env':'CartPole-v0'},checkpoint_freq=1,)这将开始训练。您的代理将存储在本地目录中,以便稍后加载它们。RLlib允许您使用'torch':True选项来使用PyTorch和TensorFlow。 一些强化学习库提供了许多深度强化学习算法的实现: 在这个示例中,我们开始使用最简单的游戏环境之一:21点游戏。21点游戏与现实世界有一个有趣的共同点:不确定性。 Blackjack是一种纸牌游戏,在其最简单的形式中,您将与一名纸牌荷官对战。您面前有一副牌,您可以"hit",意味着您会得到一张额外的牌,或者"stick",这时荷官会继续抽牌。为了赢得比赛,您希望尽可能接近21分,但不能超过21分。 在这个教程中,我们将使用Keras实现一个模型来评估给定环境配置下不同动作的价值函数。我们将实现的变体称为DQN,它在2015年的Atari里程碑成就中被使用。让我们开始吧。 如果您尚未安装依赖项,则需要进行安装。 我们将使用OpenAIGym,并且我们需要安装它: pipinstallgym我们将使用Gym环境来进行21点游戏。 我们需要一个代理人来维护其行为影响的模型。这些行动是从其内存中回放以进行学习的。我们将从一个记录过去经验以供学习的内存开始: 最后一点值得强调:我们不是使用所有的记忆来进行学习,而是只取其中的一部分。 在sample()方法中,我们做了一些修改以使我们的数据符合正确的形状。 该代理人带有一些用于配置的超参数: 我们发现这三个参数可以显著改变我们学习的轨迹。 我们从列表中省略了一个方法,该方法定义了神经网络模型: def_build_model(self):model=tf.keras.Sequential([layers.Dense(100,input_shape=(4,),kernel_initializer=initializers.RandomNormal(stddev=5.0),bias_initializer=initializers.Ones(),activation='relu',name='state'),layers.Dense(2,activation='relu'),layers.Dense(1,name='action',activation='tanh'),])model.summary()model.compile(loss='hinge',optimizer=optimizers.RMSprop(lr=self.lr))returnmodel这是一个三层神经网络,有两个隐藏层,一个有100个神经元,另一个有2个神经元,采用ReLU激活函数,并带有一个输出层,有1个神经元。 importgymenv=gym.make('Blackjack-v0')agent=DQNAgent(env=env,epsilon=0.01,lr=0.1,batch_size=100)这加载了BlackjackOpenAIGym环境和我们在本教程的第2步中实现的DQNAgent。 epsilon参数定义了代理的随机行为。我们不希望将其设置得太低。学习率是我们在实验后选择的值。由于我们在进行随机卡牌游戏,如果设置得太高,算法将会非常快速地遗忘。批处理大小和记忆大小参数分别决定了每一步的训练量以及关于奖励历史的记忆。 我们可以看到这个网络的结构(由Keras的summary()方法显示)。 _________________________________________________________________Layer(type)OutputShapeParam#=================================================================state(Dense)(None,100)500_________________________________________________________________dense_4(Dense)(None,2)202_________________________________________________________________action(Dense)(None,1)3=================================================================Totalparams:705Trainableparams:705Non-trainableparams:0对于模拟来说,我们的关键问题之一是epsilon参数的值。如果设置得太低,我们的代理将无法学到任何东西;如果设置得太高,我们将会因为代理做出随机动作而亏钱。 num_rounds=5000exploit_runs=num_rounds//5best_100=-1.0payouts=[]epsilons=np.hstack([np.linspace(0.5,0.01,num=num_rounds-exploit_runs),np.zeros(exploit_runs)])这是实际开始玩21点的代码: fromtqdm.notebookimporttrangeforsampleintrange(num_rounds):epsilon=epsilons[sample]agent.epsilon=epsilontotal_payout=0state=agent.env.reset()for_inrange(10):state,payout,done=agent.play(state)total_payout+=payoutifdone:breakifepsilon>0:agent.learn()mean_100=np.mean(payouts[-100:])ifmean_100>best_100:best_100=mean_100payouts.append(total_payout)if(sample%100)==0andsample>=100:print('averagepayout:{:.3f}'.format(mean_100))print(agent.losses[-1])print('best100average:{:.3f}'.format(best_100))在模拟过程中,我们收集了网络训练损失的统计数据,并且收集了连续100次游戏中的最大津贴。 由于巨大的可变性,我们并未展示原始数据,而是绘制了移动平均线,分别为100和1,000,结果呈现两条线:一条高度变化,另一条平滑,如图所示。 在这个案例中,我们看到了强化学习中更高级的算法,更具体地说是一种基于价值的算法。在基于价值的强化学习中,算法构建了价值函数的估计器,进而让我们选择策略。 最优Q值函数定义如下: 因此,可以根据以下公式确定最佳策略: 在神经拟合Q学习(NFQ)(Riedmiller,《神经拟合Q迭代——数据高效的神经强化学习方法的第一次经验》,2005年),神经网络对给定状态进行前向传播,输出对应的可用动作。神经Q值函数可以通过梯度下降根据平方误差进行更新: DQN(Mnih等人,《使用深度强化学习玩Atari游戏》,2015年)基于NFQ进行了一些改进。这些改进包括仅在几次迭代中的小批量更新参数,基于来自重播记忆的随机样本。由于在原始论文中,该算法从屏幕像素值学习,网络的第一层是卷积层(我们将在第七章,高级图像应用中介绍)。 计算机视觉中的人工智能应用包括机器人技术、自动驾驶汽车、人脸识别、生物医学图像中的疾病识别以及制造业的质量控制等。 在本章中,我们将从图像识别(或图像分类)开始,我们将探讨基本模型和更高级的模型。然后,我们将使用生成对抗网络(GANs)创建图像。 在本章中,我们将涵盖以下内容: 图像分类的一个流行例子是MNIST数据集,其中包含不同风格的数字从0到9。在这里,我们将使用一种称为Fashion-MNIST的可替换数据集,其中包含不同的服装。 这里是数据集中的一些示例: 在这个配方中,我们将使用不同的模型识别服装项目——我们将从通用图像特征(高斯差分或DoG)和支持向量机开始;然后我们将转向前馈多层感知器(MLP);接着我们将使用卷积神经网络(ConvNet);最后,我们将使用MobileNet进行迁移学习。 在我们开始之前,我们必须安装一个库。在这个配方中,我们将使用scikit-image,这是一个用于图像变换的库,因此我们将快速设置它: pipinstallscikit-image现在我们已经准备好进入配方了! 我们将首先加载和准备数据集,然后我们将使用不同的方法学习Fashion-MNIST数据集中服装项目的分类模型。让我们从加载Fashion-MNIST时尚数据集开始。 我们可以通过keras工具函数直接获取数据集: fromtensorflowimportkerasfrommatplotlibimportpyplotasplt(train_images,train_labels),(test_images,test_labels)=keras.datasets.fashion_mnist.load_data()train_images=train_images/255.0test_images=test_images/255.0plt.imshow(train_images[0])plt.colorbar()plt.grid(False)我们还将图像标准化为0到1的范围,通过除以最大像素强度(255.0),并且我们可视化第一张图像。 我们应该看到一张运动鞋的图片,这是训练集中的第一张图片: 正如我们在本配方介绍中提到的,我们将在接下来的几节中应用不同的方法: 让我们从DoG开始。 在深度学习在图像识别中取得突破之前,图像是使用来自拉普拉斯差分或高斯的滤波器进行特征化的。这些功能在scikit-image中实现,因此我们可以采用现成的实现。 让我们编写一个函数,使用高斯金字塔提取图像特征: importskimage.transformimportnumpyasnpdefget_pyramid_features(img):returnnp.hstack([layer.reshape(-1)forlayerinskimage.transform.pyramids.pyramid_gaussian(img)])get_pyramid_features()函数应用高斯金字塔并将这些特征展平为一个向量返回。我们将在它是如何工作...部分解释什么是高斯金字塔。 我们几乎准备好开始学习了。我们只需迭代所有图像并提取我们的高斯金字塔特征。让我们创建另一个执行此操作的函数: x_train,y_train=featurize(train_images,train_labels)clf=LinearSVC(C=1,loss='hinge').fit(x_train,y_train)x_val,y_val=featurize(test_images,test_labels)print('accuracy:{:.3f}'.format(clf.score(x_val,y_val)))使用这些特征的线性支持向量机在验证数据集上获得了84%的准确率。通过调整滤波器,我们可以达到更高的性能,但这超出了本文的范围。在2012年AlexNet发布之前,这种方法是图像分类的最先进方法之一。 训练模型的另一种方法是将图像展平,并直接将归一化的像素值输入到分类器中,例如MLP。这是我们现在要尝试的。 用MLP对图像进行分类的一个相对简单的方法是。在这种情况下,使用了一个具有10个神经元的两层MLP,你可以将隐藏层看作是10个特征检测器的特征提取层。 在本书中我们已经多次看到MLP的示例,因此我们将跳过此处的细节;可能感兴趣的是,我们将图像从28x28展平为784的向量。至于其余部分,可以说我们训练分类交叉熵,并监视准确性。 你将在以下代码块中看到这一点: importtensorflowastffromtensorflow.keras.lossesimportSparseCategoricalCrossentropydefcompile_model(model):model.summary()model.compile(optimizer='adam',loss=SparseCategoricalCrossentropy(from_logits=True),metrics=['accuracy'])defcreate_mlp():model=tf.keras.Sequential([tf.keras.layers.Flatten(input_shape=(28,28)),tf.keras.layers.Dense(128,activation='relu'),tf.keras.layers.Dense(10)])compile_model(model)returnmodel这个模型在两层及其连接之间有101,770个可训练参数。 我们将使用以下函数封装我们的训练集。这应该是相当容易理解的: deftrain_model(model,train_images,test_images):model.fit(train_images,train_labels,epochs=50,verbose=1,validation_data=(test_images,test_labels))loss,accuracy=model.evaluate(test_images,test_labels,verbose=0)print('loss:',loss)print('accuracy:',accuracy)经过50个周期,我们在验证集上的准确率为0.886。 下一个模型是经典的ConvNet,为MNIST提出,使用卷积、池化和全连接层。 LeNet5是一个前馈神经网络,具有卷积层和最大池化,以及用于导致输出的特征的全连接前馈层。让我们看看它的表现: fromtensorflow.keras.layersimport(Conv2D,MaxPooling2D,Flatten,Dense)defcreate_lenet():model=tf.keras.Sequential([Conv2D(filters=6,kernel_size=(5,5),padding='valid',input_shape=(28,28,1),activation='tanh'),MaxPooling2D(pool_size=(2,2)),Conv2D(filters=16,kernel_size=(5,5),padding='valid',activation='tanh'),MaxPooling2D(pool_size=(2,2)),Flatten(),Dense(120,activation='tanh'),Dense(84,activation='tanh'),Dense(10,activation='softmax')])compile_model(model)returnmodelcreate_lenet()函数构建我们的模型。我们只需调用它,并使用它运行我们的train_model()函数,以适应训练数据集并查看我们的测试表现: train_model(create_lenet(),train_images.reshape(train_images.shape+(1,)),test_images.reshape(test_images.shape+(1,)),)经过50集数,我们的验证准确率为0.891。 我们还可以查看混淆矩阵,以查看我们如何区分特定的服装类别: 让我们继续进行我们最后一次尝试对服装项进行分类。 MobileNetV2模型是在ImageNet上训练的,这是一个包含1400万张图像的数据库,已手动注释为WordNet分类系统的类别。 MobileNet可以下载用于迁移学习的权重。这意味着我们保持大部分或所有MobileNet的权重固定。在大多数情况下,我们只需添加一个新的输出投影来区分MobileNet表示之上的新类别集: base_model=tf.keras.applications.MobileNetV2(input_shape=(224,224,3),include_top=False,weights='imagenet')MobileNet包含2,257,984个参数。下载MobileNet时,我们有方便的选项可以省略输出层(include_top=False),这样可以节省工作量。 对于我们的迁移模型,我们必须附加一个池化层,然后像前两个神经网络一样附加一个输出层: defcreate_transfer_model():base_model=tf.keras.applications.MobileNetV2(input_shape=(224,224,3),include_top=False,weights='imagenet')base_model.trainable=Falsemodel=tf.keras.Sequential([base_model,tf.keras.layers.GlobalAveragePooling2D(),tf.keras.layers.Dense(10)])compile_model(model)returnmodel请注意,在MobileNet模型中,我们会冻结或固定权重,只学习我们添加在顶部的两个层。 当我们下载MobileNet时,您可能已经注意到一个细节:我们指定了224x224x3的尺寸。MobileNet有不同的输入形状,224x224x3是最小的之一。这意味着我们必须重新缩放我们的图像,并将它们转换为RGB(通过串联灰度)。您可以在GitHub上的在线笔记本中找到详细信息。 MobileNet迁移学习的验证准确率与LeNet和我们的MLP非常相似:0.893。 图像分类包括为图像分配标签,这是深度学习革命开始的地方。 我们使用skimage的实用函数来提取特征,然后在顶部应用线性支持向量机作为分类器。为了提高性能,我们本可以尝试其他分类器,如随机森林或梯度提升。 CNN或ConvNet是至少包含一个卷积层的神经网络。LeNet是ConvNet的经典示例,最初由YannLeCun等人提出,1989年的原始形式是(应用于手写邮政编码识别的反向传播),1998年的修订形式(称为LeNet5)是(应用于文档识别的基于梯度的学习)。 卷积在图像识别中是非常重要的转换,也是非常深的神经网络中图像识别的重要组成部分。卷积包括前馈连接,称为过滤器或卷积核,应用于图像的矩形补丁(上一层)。然后,每个生成的映射是核在整个图像上滑动的结果。这些卷积映射通常后面跟随由池化层进行的子采样(在LeNet的情况下,是从每个核中提取的最大值)。 在Keras中,加载模型只需一条命令。tf.keras.applications包提供了许多模型的架构和权重,例如DenseNet、EfficientNet、Inception-ResNetV2、InceptionV3、MobileNetV1、MobileNetV2、NASNet-A、ResNet、ResNetv2、VGG16、VGG19和XceptionV1。在我们的案例中,我们有一个预训练模型,这意味着它具有使用tf.keras.applications.MobileNetV2()函数的架构和权重。 我们可以重新训练(微调)模型以提高应用性能,或者我们可以使用原始模型,并在其上添加额外的层以对新类进行分类。 我们加载模型的操作是这样的: base_model=tf.keras.applications.MobileNetV2(input_shape=(224,224,3),include_top=False,weights='imagenet')正如之前提到的,我们可以从多个选择中指定不同的输入形状。include_top指定是否包含分类层。如果我们想要使用在ImageNet数据集上训练过的原始模型输出,我们会将其设置为True。由于我们的数据集中有不同的类别,我们想将其设置为False。 2014年由IanGoodfellow等人引入的GAN对抗学习,是一种通过两个网络相互对抗的框架来拟合数据集的分布,其中一个模型生成示例,另一个模型区分这些示例是真实的还是虚假的。这可以帮助我们使用新的训练示例扩展我们的数据集。使用GAN的半监督训练可以在使用少量标记训练示例的同时,实现更高的监督任务性能。 本篇重点是在MNIST数据集上实现深度卷积生成对抗网络(DCGAN)和鉴别器,这是最知名的数据集之一,包含60,000个0到9之间的数字。我们将在工作原理...部分解释术语和背景。 对于这个步骤,我们不需要任何特殊的库。我们将使用TensorFlow与Keras,NumPy和Matplotlib,这些都是我们之前见过的。为了保存图像,我们将使用Pillow库,你可以按以下方式安装或升级: pipinstall--upgradePillow让我们马上开始吧。 对于我们的GAN方法,我们需要一个生成器——一个接受某些输入(可能是噪声)的网络——以及一个鉴别器,一个图像分类器,例如本章中识别服装项目食谱中看到的那样。 生成器和鉴别器都是深度神经网络,并将一起进行训练。训练后,我们将看到训练损失、各个时期的示例图像以及最后时期的复合图像。 首先,我们将设计鉴别器。 这是一个经典的ConvNet示例。它是一系列卷积和池化操作(通常是平均或最大池化),接着是平坦化和输出层。更多详情,请参见本章的识别服装项目示例: defdiscriminator_model():model=Sequential([Conv2D(64,(5,5),padding='same',input_shape=(28,28,1),activation='tanh'),MaxPooling2D(pool_size=(2,2)),Conv2D(128,(5,5),activation='tanh'),MaxPooling2D(pool_size=(2,2)),Flatten(),Dense(1024,activation='tanh'),Dense(1,activation='sigmoid')])returnmodel这与我们在本章的识别服装项目示例中介绍的LeNet卷积块非常相似。 接下来,我们设计生成器。 虽然鉴别器通过卷积和池化操作对其输入进行下采样,生成器则进行上采样。我们的生成器接受一个100维的输入向量,并通过执行与ConvNet相反方向的操作生成图像。因此,这种类型的网络有时被称为DeconvNet。 fromtensorflow.keras.modelsimportSequentialfromtensorflow.keras.layersimport(Dense,Reshape,Activation,Flatten,BatchNormalization,UpSampling2D,Conv2D,MaxPooling2D)defcreate_generator_model():model=Sequential([Dense(input_dim=100,units=1024,activation='tanh'),Dense(128*7*7),BatchNormalization(),Activation('tanh'),Reshape((7,7,128),input_shape=(128*7*7,)),UpSampling2D(size=(2,2)),Conv2D(64,(5,5),padding='same'),Activation('tanh'),UpSampling2D(size=(2,2)),Conv2D(1,(5,5),padding='same'),Activation('tanh'),])model.summary()returnmodel调用此函数,我们将使用summary()获取我们网络架构的输出。我们将看到有6,751,233个可训练参数。我们建议在强大的系统上运行此示例,例如GoogleColab。 为了训练网络,我们加载并标准化了MNIST数据集: fromtensorflow.keras.datasetsimportmnist(X_train,y_train),(X_test,y_test)=mnist.load_data()X_train=(X_train.astype(np.float32)-127.5)/127.5X_train=X_train[:,:,:,None]X_test=X_test[:,:,:,None]图像以灰度形式呈现,像素值范围在0–255之间。我们将其标准化到-1到+1的范围内,然后重新调整为在末尾具有单例维度。 为了将误差传递给生成器,我们将生成器与鉴别器链在一起,如下所示: defchain_generator_discriminator(g,d):model=Sequential()model.add(g)model.add(d)returnmodel作为我们的优化器,我们将使用Keras的随机梯度下降: fromtensorflow.keras.optimizersimportSGDdefoptim():returnSGD(lr=0.0005,momentum=0.9,nesterov=True)现在,让我们创建并初始化我们的模型: d=discriminator_model()g=generator_model()d_on_g=chain_generator_discriminator(g,d)d_on_g.compile(loss='binary_crossentropy',optimizer=optim())d.compile(loss='binary_crossentropy',optimizer=optim())单个训练步骤包括三个步骤: 让我们依次进行这些步骤。首先是从噪声生成图像: importnumpyasnpdefgenerate_images(g,batch_size):noise=np.random.uniform(-1,1,size=(batch_size,100))image_batch=X_train[index*batch_size:(index+1)*batch_size]returng.predict(noise,verbose=0)然后,鉴别器在给定假和真实图像时进行学习: deflearn_discriminate(d,image_batch,generated_images,batch_size):X=np.concatenate((image_batch,generated_images))y=np.array([1]*batch_size+[0]*batch_size)loss=d.train_on_batch(X,y)returnloss我们将真实的1和假的0图像串联起来,作为鉴别器的输入。 最后,生成器从鉴别器的反馈中学习: deflearn_generate(d_on_g,d,batch_size):noise=np.random.uniform(-1,1,(batch_size,100))d.trainable=Falsetargets=np.array([1]*batch_size)loss=d_on_g.train_on_batch(noise,targets)d.trainable=Truereturnloss请注意,在这个函数中,鉴别器目标的反转。与以前的假0不同,我们现在输入1。同样重要的是,在生成器学习期间,鉴别器的参数是固定的(否则我们将再次忘记)。 我们可以在训练中加入额外的命令,以保存图像,以便可以通过视觉方式欣赏我们生成器的进展: fromPILimportImagedefsave_images(generated_images,epoch,index):image=combine_images(generated_images)image=image*127.5+127.5Image.fromarray(image.astype(np.uint8)).save('{}_{}.png'.format(epoch,index))我们的训练通过交替进行以下步骤来进行: 在100个epochs中,我们的训练错误看起来如下: 我们已经保存了图像,因此我们也可以查看生成器在epochs中的输出。以下是每个100个epochs中单个随机生成的数字的画廊: 我们可以看到图像通常变得越来越清晰。有趣的是,生成器的训练误差似乎在前几个epochs之后保持在相同的基线水平。这是一个显示最后一个epoch期间生成的100个图像的图像: 图像并不完美,但大部分可以识别为数字。 在GAN技术中,生成网络学习将一个种子(例如,随机输入)映射到目标数据分布,而鉴别网络评估并区分生成器产生的数据和真实数据分布。 生成网络生成数据,鉴别网络评估数据。这两个神经网络相互竞争,生成网络的训练增加了鉴别网络的错误率,而鉴别器的训练增加了生成器的错误率,从而进行武器竞赛,迫使彼此变得更好。 在训练中,我们将随机噪声输入生成器,然后让鉴别器学习如何对生成器输出与真实图像进行分类。然后,给定鉴别器的输出,或者说其反向的生成器进行训练。鉴别器判断图像为假的可能性越小,对生成器越有利,反之亦然。 自编码器在有效地表示输入方面非常有用。在2016年的一篇论文中,Makhazani等人展示了对抗自编码器可以比变分自编码器创建更清晰的表示,并且——与我们在前一示例中看到的DCGAN类似——我们获得了学习创建新示例的额外好处,这对半监督或监督学习场景有帮助,并且允许使用更少的标记数据进行训练。以压缩方式表示还有助于基于内容的检索。 在这个示例中,我们将在PyTorch中实现对抗自编码器。我们将实现监督和无监督两种方法,并展示结果。在无监督方法中,类别之间有很好的聚类效果;在监督方法中,我们的编码器-解码器架构能够识别风格,从而使我们能够进行风格转移。在这个示例中,我们将使用计算机视觉的helloworld数据集MNIST。 对于这个示例,我们将需要使用torchvision。这将帮助我们下载我们的数据集。我们将快速安装它: !pipinstalltorchvision对于PyTorch,我们需要先进行一些准备工作,比如启用CUDA并设置tensor类型和device: use_cuda=Trueuse_cuda=use_cudaandtorch.cuda.is_available()print(use_cuda)ifuse_cuda:dtype=torch.cuda.FloatTensordevice=torch.device('cuda:0')else:dtype=torch.FloatTensordevice=torch.device('cpu')与其他示例风格不同,我们还会先导入必要的库: importnumpyasnpimporttorchfromtorchimportautogradimporttorch.nnasnnimporttorch.nn.functionalasFfromtorch.utils.dataimportDataLoader,datasetfromtorchvision.datasetsimportMNISTimporttorchvision.transformsasTfromtqdm.notebookimporttrange现在,让我们开始吧。 首先我们将导入必要的库。然后,我们将加载我们的数据集,定义模型组件,包括编码器、解码器和判别器,然后进行训练,最后我们将可视化生成的表示。 首先是加载数据集。 我们需要设置一些全局变量,这些变量将定义训练和数据集。然后,我们加载我们的数据集: EPS=torch.finfo(torch.float32).epsbatch_size=1024n_classes=10batch_size=1024n_classes=10train_loader=torch.utils.data.DataLoader(MNIST('Data/',train=True,download=True,transform=T.Compose([T.transforms.ToTensor(),T.Normalize((0.1307,),(0.3081,))])),batch_size=batch_size,shuffle=True)val_loader=torch.utils.data.DataLoader(MNIST('Val/',train=False,download=True,transform=T.Compose([T.transforms.ToTensor(),T.Normalize((0.1307,),(0.3081,))])),batch_size=batch_size,shuffle=False)规范化中的转换对应于MNIST数据集的均值和标准差。 接下来是定义自编码器模型。 自编码器由编码器、解码器和判别器组成。如果您熟悉自编码器,这对您来说不是什么新东西。在下一节工作原理……中,我们将对其进行详细解释和分解。 首先,我们将定义我们的编码器和解码器: dims=10classEncoder(nn.Module):def__init__(self,dim_input,dim_z):super(Encoder,self).__init__()self.dim_input=dim_input#imagesizeself.dim_z=dim_zself.network=[]self.network.extend([nn.Linear(self.dim_input,self.dim_input//2),nn.Dropout(p=0.2),nn.ReLU(),nn.Linear(self.dim_input//2,self.dim_input//2),nn.Dropout(p=0.2),nn.ReLU(),nn.Linear(self.dim_input//2,self.dim_z),])self.network=nn.Sequential(*self.network)defforward(self,x):z=self.network(x)returnz请注意dim参数,它表示表示层的大小。我们选择10作为我们编码层的大小。 然后,我们将定义我们的解码器: classDecoder(nn.Module):def__init__(self,dim_input,dim_z,supervised=False):super(Decoder,self).__init__()self.dim_input=dim_inputself.dim_z=dim_zself.supervised=supervisedself.network=[]self.network.extend([nn.Linear(self.dim_z,self.dim_input//2)ifnotself.supervisedelsenn.Linear(self.dim_z+n_classes,self.dim_input//2),nn.Dropout(p=0.2),nn.ReLU(),nn.Linear(self.dim_input//2,self.dim_input//2),nn.Dropout(p=0.2),nn.ReLU(),nn.Linear(self.dim_input//2,self.dim_input),nn.Sigmoid(),])self.network=nn.Sequential(*self.network)defforward(self,z):x_recon=self.network(z)returnx_recon顺便说一下,我们还可以定义我们的判别器来与我们的编码器竞争: classDiscriminator(nn.Module):def__init__(self,dims,dim_h):super(Discriminator,self).__init__()self.dim_z=dimsself.dim_h=dim_hself.network=[]self.network.extend([nn.Linear(self.dim_z,self.dim_h),nn.Dropout(p=0.2),nn.ReLU(),nn.Dropout(p=0.2),nn.Linear(self.dim_h,self.dim_h),nn.ReLU(),nn.Linear(self.dim_h,1),nn.Sigmoid(),])self.network=nn.Sequential(*self.network)defforward(self,z):disc=self.network(z)returndisc请注意,为了保持在0和1的范围内,我们压缩了我们的输出。这对我们的损失函数非常重要。 然后是训练模型。 对抗自编码器可以在有监督的方式下使用,其中标签被输入到解码器中,除了编码输出之外,我们还需要一个实用函数,将变量进行独热编码: defone_hot_encoding(labels,n_classes=10):cat=np.array(labels.data.tolist())cat=np.eye(n_classes)[cat].astype('float32')cat=torch.from_numpy(cat)returnautograd.Variable(cat)我们将展示如何在有标签和无标签情况下使用对抗自编码器: 现在,让我们初始化我们的模型和优化器: encoder=Encoder(784,dims).to(device)decoder=Decoder(784,dims,supervised=True).to(device)Disc=Discriminator(dims,1500).to(device)lr=0.001optim_encoder=torch.optim.Adam(encoder.parameters(),lr=lr)optim_decoder=torch.optim.Adam(decoder.parameters(),lr=lr)optim_D=torch.optim.Adam(Disc.parameters(),lr=lr)optim_encoder_reg=torch.optim.Adam(encoder.parameters(),lr=lr*0.1)现在我们可以开始训练: train_loss=[]val_loss=[]forepochintrange(n_epochs):l1,l2,l3=train_validate(encoder,decoder,Disc,train_loader,optim_encoder,optim_decoder,optim_D,train=True)print('epoch:{}----trainingloss:{:.8f}'.format(epoch,l1))train_loss.append(l1)if(epoch%5)==0:l1,l2,l3=train_validate(encoder,decoder,Disc,val_loader,optim_encoder,optim_decoder,optim_D,False)print('epoch:{}----validationloss:{:.8f}'.format(epoch,l1))val_loss.append(l1)这没什么大不了的,除了之前定义的train_validate()函数的调用,一次是train=True选项,一次是train=False选项。从这两个调用中,我们分别收集用于训练和验证的错误。 训练和验证错误持续下降,正如我们在下面的图表中所见: 如果您运行此代码,比较生成器和判别器的损失——看到生成器和判别器的损失如何相互影响是很有趣的。 下一步是可视化表示: 在有监督条件下,编码器空间的投影与类别关系不大,正如您在下面的tsne图中所见: 这是编码器数字表示空间的二维可视化。颜色(或者如果您在黑白显示器上看的话,阴影)代表不同的数字,它们都被集中在一起,而不是分成群集。编码器根本不区分不同的数字。 编码的东西完全是别的,那就是风格。事实上,我们可以分别在两个维度上变化输入到解码器中,以展示这一点: 前五行对应第一个维度的线性范围,第二个维度保持恒定,然后在接下来的五行中,第一个维度固定,第二个维度变化。我们可以看到第一个维度对应厚度,第二个维度对应倾斜度。这被称为风格转移。 我们还可以尝试无监督训练,通过设置supervised=False。我们应该看到这样的投影,其中类别在tsne投影的2D空间中聚类: 这是数字编码器表示空间的二维可视化。每种颜色(或阴影)代表不同的数字。我们可以看到不同的聚类将同一数字的实例组合在一起。编码器区分不同的数字。 在下一节中,我们将讨论其工作原理。 自编码器是一个由两部分组成的网络-编码器和解码器-其中编码器将输入映射到潜在空间,解码器重建输入。自编码器可以通过重建损失进行训练,该损失通常是原始输入与恢复输入之间的平方误差。 由于对抗自编码器是GAN,因此依赖于生成器和鉴别器之间的竞争,训练比普通自编码器更复杂一些。我们计算三种不同类型的错误: 在我们的情况下,我们强制先验分布和解码器输出在0和1之间的范围内,并且因此可以使用交叉熵作为重建误差。 可能有助于突出显示负责计算不同类型误差的代码段。 重建误差如下所示: ifdecoder.supervised:categories=one_hot_encoding(labels,n_classes=10).to(device)decoded=decoder(torch.cat((categories,encoding),1))else:decoded=decoder(encoding)reconstruction_loss=F.binary_cross_entropy(decoded,real_data_v)有一个额外的标志用于将标签作为监督训练输入到解码器中。我们发现在监督设置中,编码器并不表示数字,而是表示风格。我们认为这是因为在监督设置中,重建误差不再依赖于标签。 鉴别器损失计算如下: #i)latentrepresentation:encoder.eval()z_real_gauss=autograd.Variable(torch.randn(data.size()[0],dims)*5.0).to(device)z_fake_gauss=encoder(real_data_v)#ii)feedintodiscriminatorD_real_gauss=Disc(z_real_gauss)D_fake_gauss=Disc(z_fake_gauss)D_loss=-torch.mean(torch.log(D_real_gauss+EPS)+torch.log(1-D_fake_gauss+EPS))请注意,这是为了训练鉴别器,而不是编码器,因此有encoder.eval()语句。 生成器损失计算如下: iftrain:encoder.train()else:encoder.eval()z_fake_gauss=encoder(real_data_v)D_fake_gauss=Disc(z_fake_gauss)G_loss=-torch.mean(torch.log(D_fake_gauss+EPS))total_gen_loss+=G_loss.item()在下一节中,我们将查看更多的材料和背景。 谈论视频时应该考虑到很多应用程序,例如目标跟踪、事件检测(监视)、深度伪造、3D场景重建和导航(自动驾驶汽车)。 在本章中,我们将看到以下示例: 我们将使用许多标准库,包括keras和opencv,但在每个示例开始之前我们会提到更多的库。 对象检测是指在图像和视频中识别特定类别的对象。例如,在自动驾驶汽车中,必须识别行人和树木以避让。 在这个示例中,我们将在Keras中实现一个对象检测算法。我们将首先将其应用于单个图像,然后应用于我们的笔记本摄像头。在工作原理...部分,我们将讨论理论和更多关于对象检测的算法。 对于这个示例,我们将需要开放计算机视觉库(OpenCV)和scikit-image的Python绑定: !pipinstall-Uopencv-pythonscikit-image作为我们的示例图像,我们将从一个对象检测工具箱中下载一张图像: 我们将使用基于keras-yolo3库的代码,只需进行少量更改即可快速设置。我们也可以快速下载这个: 在本节中,我们将使用Keras实现一个物体检测算法。 我们将导入keras-yolo3库,加载预训练的权重,然后对给定的图像或摄像头视频流进行物体检测: fromkeras_yolo3importload_model,detectyolov3=load_model('yolov3.weights')我们的模型现在可作为Keras模型使用。 frommatplotlibimportpyplotaspltplt.imshow(detect(yolov3,'demo.jpg'))我们应该看到我们的示例图像标注了每个边界框的标签,如下截图所示: 我们可以使用OpenCV库扩展此功能以处理视频。我们可以逐帧捕获连接到计算机的摄像头的图像,运行物体检测,并显示带标注的图像。 请注意,此实现未经优化,可能运行比较慢。要获取更快的实现,请参考参考资料部分中链接的darknet实现。 当你运行以下代码时,请注意你可以按q键停止摄像头: importcv2fromskimageimportcolorcap=cv2.VideoCapture(0)while(True):ret,frame=cap.read()img=cv2.cvtColor(frame,cv2.COLOR_BGR2RGB)img=detect(yolov3,img)cv2.imshow('frame',img)ifcv2.waitKey(1)&0xFF==ord('q'):breakcap.release()cv2.destroyAllWindows()我们以灰度方式捕获图像,但随后必须使用scikit-image将其转换回RGB,通过堆叠图像来检测对象并显示带标注的帧。 这是我们获得的图像: 在下一节中,我们将讨论这个配方及其背景解释。 我们已经使用Keras实现了一个物体检测算法。这是一个标准库的开箱即用功能,但我们将其连接到摄像头,并应用到了一个示例图像上。 图像检测的主要算法如下: 物体检测的主要需求之一是速度–你不希望在识别前等待撞上树。 图像检测是基于图像识别的基础上,增加了在图像中搜索候选位置的复杂性。 FastR-CNN是R-CNN的改进(2014年同一作者)。每个感兴趣区域,即由边界框定义的矩形图像块,通过图像金字塔进行尺度归一化。卷积网络然后可以通过一次前向传递处理这些对象提议(从几千到成千上万)。作为实现细节,FastR-CNN使用奇异值分解压缩完全连接层以提高速度。 YOLO是一个单一网络,直接从图像中提出边界框和类别。在其实验中,作者以每秒45帧和155帧的速度运行了不同版本的YOLO。 SSD是一种单阶段模型,摒弃了独立对象提议生成的需要,而选择通过网络传递一组离散的边界框。然后在不同分辨率和边界框位置上组合预测结果。 YOLOv4在其CNN中引入了几种新的网络特性,展示了快速的处理速度,同时保持了显著优于YOLOv3的精度水平(43.5%平均精度(AP),在TeslaV100GPU上实时速度约为每秒65帧,针对MSCOCO数据集)。 与网络摄像头交互的方式有多种,并且甚至有一些移动应用程序允许您流式传输摄像头内容,这意味着您可以将其插入在云上运行的应用程序中(例如Colab笔记本)或服务器上。 最常见的库之一是matplotlib,也可以从网络摄像头实时更新matplotlib图形,如下所示的代码块: %matplotlibnotebookimportcv2importmatplotlib.pyplotaspltdefgrab_frame(cap):ret,frame=cap.read()ifnotret:print('Noimagecaptured!')exit()returncv2.cvtColor(frame,cv2.COLOR_BGR2RGB)cap=cv2.VideoCapture(0)fig,ax=plt.subplots(1,1)im=ax.imshow(grab_frame(cap))plt.tick_params(top=False,bottom=False,left=False,right=False,labelleft=False,labelbottom=False)plt.show()whileTrue:try:im.set_data(grab_frame(cap))fig.canvas.draw()exceptKeyboardInterrupt:cap.release()break这是初始化视频源并在matplotlib子图中显示的基本模板。我们可以通过中断内核来停止。 我们将在下一节中提到更多的库以供玩耍。 有关对象检测,有几个库可用: 让我们继续下一个步骤吧! 深度伪造技术存在一些道德应用,其中一些非常有趣。您是否想过史泰龙可能会在《终结者》中看起来如何?今天您可以实现! 在本教程中,我们将学习如何使用Python创建深度伪造。我们将下载两部电影的公共领域视频,并通过替换其中一个人物的脸部来制作深度伪造。《Charade》是一部1963年由斯坦利·多南导演的电影,风格类似于希区柯克的影片。影片中50多岁的凯瑞·格兰特与30多岁的奥黛丽·赫本搭档。我们认为让这对配对年龄更合适。在一番搜索后,我们找到了1963年约翰·韦恩主演的《McLintock!》中的莫琳·奥哈拉,以取代奥黛丽·赫本。 我们需要做的是下载faceit仓库并安装必需的库。 你可以用git克隆(clone)这个仓库(如果在ipython笔记本中输入此命令,请加上感叹号): 请注意,尽管有一个轻量级模型可供使用,我们强烈建议您在配备GPU的机器上运行深度伪造技术。 最后,我们可以这样进入我们的容器: ./dockerrun.sh在容器内部,我们可以运行Python3.6.以下所有命令都假定我们在容器内并且在/project目录下。 我们需要将视频和面部定义为深度伪造过程的输入。 fromfaceitimport*faceit=FaceIt('hepburn_to_ohara','hepburn','ohara')这清楚地表明我们想要用ohara替换hepburn(这是我们在进程内部对它们命名的方式)。我们必须将图像放在data/persons目录下分别命名为hepburn.jpg和ohara.jpg。我们已经在仓库的便利性部分提供了这些图像。 如果我们没有提供图像,faceit将提取所有的面部图像,不论它们显示的是谁。然后我们可以将其中两张图像放在persons目录下,并删除data/processed/目录下面部的目录。 请注意,只能从允许下载或不禁止下载的网站下载视频,即使是公共领域的视频: faceit.add('ohara','mclintock.mp4')faceit.add('hepburn','who_trust.mp4')FaceIt.add_model(faceit)这定义了我们的模型使用的数据为一对视频。Faceit允许一个可选的第三参数,它可以是一个链接到视频的链接,从中可以自动下载。但是,在您从YouTube或其他网站下载视频之前,请确保这在其服务条款中允许,并且在您的司法管辖区内合法。 faceit.preprocess()faceit.train()faceit.convert('who_trust.mp4',face_filter=True,photos=False)预处理步骤包括下载视频,提取所有帧作为图像,最后提取面部。我们已经提供了这些面部,因此您不必执行预处理步骤。 下面的图片显示奥黛丽·赫本在左边,莫琳·奥哈拉在右边扮演奥黛丽·赫本: 这些变化可能看起来很微妙。如果你想要更清晰的东西,我们可以使用同样的模型将凯瑞·格兰特替换为莫琳·奥哈拉: 实际上,我们可以通过在转换中禁用面部过滤器来制作一部名为《成为莫琳·奥哈拉》的电影。 典型的深度伪造流程包括我们在配方中方便地忽略的一些步骤,因为Faceit提供了的抽象。这些步骤是以下,给定人物A和人物B,其中A将被B替换: 在我们的情况下,面部识别库(face-recognition)在检测和识别方面表现非常出色。然而,它仍然存在高假阳性和假阴性。这可能导致体验不佳,特别是在有几个面部的帧中。 每个步骤都需要非常注意。整个操作的核心是模型。可以有不同的模型,包括生成对抗自编码器和其他模型。faceswap中的原始模型是带有变化的自编码器。我们之前在第七章中使用过自编码器,高级图像应用。这个相对传统,我们可以从那里的自编码器实现中获取我们的自编码器实现。但是为了完整起见,我们将展示其基于keras/tensorflow的实现(简化): 现在,自编码器更有趣的部分在于训练是如何进行的,作为两个模型: optimizer=Adam(lr=5e-5,beta_1=0.5,beta_2=0.999)x=Input(shape=IMAGE_SHAPE)encoder=Encoder()decoder_A,decoder_B=Decoder(),Decoder()autoencoder_A=Model(x,decoder_A(encoder(x)))autoencoder_B=Model(x,decoder_B(encoder(x)))autoencoder_A.compile(optimizer=optimizer,loss='mean_absolute_error')autoencoder_B.compile(optimizer=optimizer,loss='mean_absolute_error')我们有两个自编码器,一个用于训练A脸部,另一个用于训练B脸部。两个自编码器都在最小化输出与输入之间的重建误差(以平均绝对误差度量)。如前所述,我们有一个单一编码器,它是两个模型的一部分,并且因此将在A脸部和B脸部上都进行训练。解码器模型在两个脸部之间是分开的。这种架构确保我们在A脸部和B脸部之间有一个共同的潜在表示。在转换中,我们可以从A中取出一个脸部,表示它,然后应用B的解码器以获得对应于潜在表示的B脸部。 我们整理了一些关于玩弄视频和深度伪造以及检测深度伪造的进一步参考资料。 这里有一些简单视频伪造应用的不错示例: 对于更复杂的视频操作,如深度伪造,有很多可用的工具,我们将重点介绍两个: 提出并实现了许多不同的模型,包括以下内容: 论文《DeepFakesandBeyond:ASurveyof**FaceManipulationandFakeDetection*》(RubenTolosana等人,2020)提供了更多的链接和数据集资源及方法。 在本章中,我们将处理声音和语音。声音数据以波形的形式出现,因此需要与其他类型的数据进行不同的预处理。 在音频信号的机器学习中,商业应用包括语音增强(例如助听器)、语音到文本和文本到语音、噪声取消(例如耳机)、根据用户喜好推荐音乐(如Spotify)以及生成音频。在音频中可以遇到许多有趣的问题,包括音乐流派的分类、音乐的转录、生成音乐等等。 在本章中,我们将看看以下的配方: !pipinstalllibrosaLibrosa在Colab中默认安装。 对于本章中的配方,请确保您有一个可用的GPU。在GoogleColab上,请确保您激活了GPU运行时。 在这个配方中,我们将在谷歌的语音命令数据集上解决一个简单的声音识别问题。我们将把声音命令分类到不同的类别中。然后,我们将建立一个深度学习模型并进行训练。 对于本配方,我们需要在章节开头提到的librosa库。我们还需要下载语音命令数据集,为此我们首先需要安装wget库: !pipinstallwget或者,我们可以在Linux和macOS中使用!wget系统命令。我们将创建一个新目录,下载带有数据集的存档文件,并提取tarfile: _background_noise_fivemarvinrighttreebedfournineseventwobirdgonosheilaupcathappyoffsixvalidation_list.txtdoghouseonstopwowdownleftonetesting_list.txtyeseightLICENSEREADME.mdthreezero大多数指的是语音命令;例如,bed目录包含了bed命令的示例。 有了这些,我们现在准备好开始了。 首先进行数据探索,然后导入和预处理数据集进行训练,接着创建模型,训练并在验证中检查其性能: importlibrosax,sr=librosa.load('data/train/bed/58df33b5_nohash_0.wav')我们还可以获得一个Jupyter小部件来听声音文件或加载的向量: importIPython.displayasipdipd.Audio(x,rate=sr)小部件看起来像这样: 按下播放,我们听到声音。请注意,即使在远程连接上(例如使用GoogleColab),这也可以工作。 现在让我们看一下声音波形: %matplotlibinlineimportmatplotlib.pyplotaspltimportlibrosa.displayplt.figure(figsize=(14,5))librosa.display.waveplot(x,sr=sr,alpha=0.8)波形看起来像这样: 我们可以如下绘制频谱: X=librosa.stft(x)Xdb=librosa.amplitude_to_db(abs(X))plt.figure(figsize=(14,5))librosa.display.specshow(Xdb,sr=sr,x_axis='time',y_axis='log')plt.colorbar()频谱看起来像这样: 请注意,我们在y轴上使用了对数尺度。 最后,我们需要将Python特征列表转换为NumPy数组,并且需要分割训练和验证数据: importnumpyasnpfromsklearn.model_selectionimporttrain_test_splitfeatures=np.concatenate([f.reshape(1,-1)forfinfeatures],axis=0)labels=np.array(labels)X_train,X_test,y_train,y_test=train_test_split(features,labels,test_size=0.33,random_state=42)现在我们需要对我们的训练数据做一些处理。我们需要一个可以训练的模型。 importtensorflow.kerasaskerasfromtensorflow.keras.layersimport*fromtensorflow.keras.regularizersimportl2fromtensorflow.keras.modelsimportModelimporttensorflow.keras.backendasKdefpreprocess(x):x=(x+0.8)/7.0x=K.clip(x,-5,5)returnxPreprocess=Lambda(preprocess)接下来是以下内容: defrelu6(x):returnK.relu(x,max_value=6)defconv_layer(x,num_filters=100,k=3,strides=2):x=Conv1D(num_filters,(k),padding='valid',use_bias=False,kernel_regularizer=l2(1e-6))(x)x=BatchNormalization()(x)x=Activation(relu6)(x)x=MaxPool1D(pool_size=num_filters,strides=None,padding='valid')(x)returnxdefcreate_model(classes,nlayers=1,filters=100,k=100):input_layer=Input(shape=[features.shape[1]])x=Preprocess(input_layer)x=Reshape([features.shape[1],1])(x)for_inrange(nlayers):x=conv_layer(x,num_filters=filters,k=k)x=Reshape([219*filters])(x)x=Dense(units=len(classes),activation='softmax',kernel_regularizer=l2(1e-2))(x)model=Model(input_layer,x,name='conv1d_sound')model.compile(optimizer=keras.optimizers.Adam(lr=3e-4),loss=keras.losses.SparseCategoricalCrossentropy(),metrics=[keras.metrics.sparse_categorical_accuracy])model.summary()returnmodelmodel=create_model(classes请注意conv_layer()函数,它提供了网络的核心部分。在视觉中可以使用非常类似的一维卷积模块,这里只是我们在这里使用了一维卷积。 这给了我们一个相对较小的模型,仅约有75,000个参数: 现在我们可以进行训练和验证: importsklearnmodel.fit(X_train,y_train,epochs=30)predicted=model.predict(X_test)print('accuracy:{:.3f}'.format(sklearn.metrics.accuracy_score(y_test,predicted.argmax(axis=1))))在验证集中,模型的准确率输出应该大约为0.805。 对于机器学习,我们可以从波形中进行特征提取,并在原始波形上使用1D卷积,或在声谱图表示(例如,Mel声谱图–Davis和Mermelstein,连续语音中基于音节的识别实验,1980年)上使用2D卷积。我们之前在第七章中处理过卷积,高级图像应用。简而言之,卷积是对层输入上的矩形补丁应用的前向滤波器。生成的映射通常在池化层之后进行子采样。 还有一些有趣的库和存储库值得探索: 一个文本到语音程序,对人类来说很容易理解,可以让有视觉或阅读障碍的人听到家用电脑上的书写文字,或者在驾车时让您享受阅读一本书。在这个示例中,我们将加载一个文本到语音模型,并让它朗读给我们听。在它是如何工作的……部分,我们将介绍模型实现和模型架构。 对于这个示例,请确保您有一个可用的GPU。在GoogleColab上,请确保您激活了GPU运行时。我们还需要安装wget库,可以在笔记本中如下安装: !pipinstallwget我们还需要从GitHub克隆pytorch-dc-tts存储库并安装其要求。请从笔记本运行此命令(或在终端中运行,不带前导感叹号): 我们已经准备好处理主要示例了。 我们将下载Torch模型文件,加载它们到Torch中,然后从句子中合成语音: importsyssys.path.append('pytorch-dc-tts')importnumpyasnpimporttorchimportIPythonfromIPython.displayimportAudiofromhparamsimportHParamsashpfromaudioimportsave_to_wavfrommodelsimportText2Mel,SSRNfromdatasets.lj_speechimportvocab,idx2char,get_test_data现在我们可以加载模型: torch.set_grad_enabled(False)text2mel=Text2Mel(vocab)text2mel.load_state_dict(torch.load('ljspeech-text2mel.pth').state_dict())text2mel=text2mel.eval()ssrn=SSRN()ssrn.load_state_dict(torch.load('ljspeech-ssrn.pth').state_dict())ssrn=ssrn.eval()最后,我们可以大声朗读这些句子。 以下句子是花园路径句子的例子——这些句子会误导听众关于单词如何相互关联的理解。我们选择它们是因为它们既简短又有趣。您可以在学术文献中找到这些及更多花园路径句子,比如在《UptheGardenPath》(TomáGráf;2013年发表于ActaUniversitatisCarolinaePhilologica)中: SENTENCES=['Thehorseracedpastthebarnfell.','Theoldmantheboat.','Thefloristsenttheflowerswaspleased.','ThecottonclothingismadeofgrowsinMississippi.','Thesourdrinkfromtheocean.','Havethestudentswhofailedtheexamtakethesupplementary.','Wepaintedthewallwithcracks.','Thegirltoldthestorycried.','Theraftfloateddowntheriversank.','Fatpeopleeataccumulates.']我们可以按照以下步骤从这些句子生成语音: foriinrange(len(SENTENCES)):sentences=[SENTENCES[i]]max_N=len(sentences[0])L=torch.from_numpy(get_test_data(sentences,max_N))zeros=torch.from_numpy(np.zeros((1,hp.n_mels,1),np.float32))Y=zerosA=Nonefortinrange(hp.max_T):_,Y_t,A=text2mel(L,Y,monotonic_attention=True)Y=torch.cat((zeros,Y_t),-1)_,attention=torch.max(A[0,:,-1],0)attention=attention.item()ifL[0,attention]==vocab.index('E'):#EOSbreak_,Z=ssrn(Y)Z=Z.cpu().detach().numpy()save_to_wav(Z[0,:,:].T,'%d.wav'%(i+1))IPython.display.display(Audio('%d.wav'%(i+1),rate=hp.sr))在还有更多...部分,我们将看一下如何为不同数据集训练模型。 语音合成是通过程序产生人类语音的过程,称为语音合成器。从自然语言到语音的合成称为文本到语音(TTS)。合成的语音可以通过连接来自录制片段的音频生成,这些片段以单位如独特的声音、音素和音素对(双音素)出现。 让我们稍微深入了解两种方法的细节。 该架构由两个子网络组成,可以分别训练,一个用于从文本合成频谱图,另一个用于从频谱图创建波形。文本到频谱图部分包括以下模块: Donahue等人在无监督环境中训练了一个GAN以合成原始音频波形。他们尝试了两种不同的策略: 对于第一种策略,他们必须开发一个能够将频谱图转换回文本的方法。 我们还可以使用WaveGAN模型从文本合成语音。 我们将下载在前一教程中遇到的语音命令上训练的模型检查点。然后我们将运行模型生成语音: importtensorflowastftf.reset_default_graph()saver=tf.train.import_meta_graph('infer.meta')graph=tf.get_default_graph()sess=tf.InteractiveSession()saver.restore(sess,'model.ckpt');现在我们可以生成语音。 importnumpyasnpimportPIL.ImagefromIPython.displayimportdisplay,Audioimporttimeastime_z=(np.random.rand(2,100)*2.)-1.z=graph.get_tensor_by_name('z:0')G_z=graph.get_tensor_by_name('G_z:0')[:,:,0]G_z_spec=graph.get_tensor_by_name('G_z_spec:0')start=time.time()_G_z,_G_z_spec=sess.run([G_z,G_z_spec],{z:_z})print('Finished!(Took{}seconds)'.format(time.time()-start))foriinrange(2):display(Audio(_G_z[i],rate=16000))这应该展示了两个使用Jupyter小部件的生成声音示例: 如果这些听起来并不特别自然,不要担心。毕竟,我们使用了潜在空间的随机初始化。 在这个教程中,我们将生成一个旋律。更具体地说,我们将使用MagentaPython库中的功能继续一首歌曲。 我们需要安装Magenta库以及一些系统库作为依赖。请注意,为了安装系统依赖项,您需要管理员权限。如果您不是在Linux(或*nix)上,您将需要找到与您的系统对应的依赖项。 在macOS上,这应该相对简单。否则,在Colab环境中运行可能更容易: !apt-getupdate-qq&&apt-getinstall-qqlibfluidsynth1fluid-soundfont-gmbuild-essentiallibasound2-devlibjack-dev!pipinstall-qUpyfluidsynthpretty_midi!pipinstall-qUmagenta如果您在Colab上,您需要另一种调整以允许Python找到您的系统库: 是时候发挥创造力了! 我们首先组合一段旋律的开头,然后从Magenta加载MelodyRNN模型让它继续旋律: importnote_seqnote_seq.plot_sequence(twinkle_twinkle)note_seq.play_sequence(twinkle_twinkle,synth=note_seq.fluidsynth)看起来如下: 我们可以听歌的前9秒。 frommagenta.models.melody_rnnimportmelody_rnn_sequence_generatorfrommagenta.models.sharedimportsequence_generator_bundlefromnote_seq.protobufimportgenerator_pb2fromnote_seq.protobufimportmusic_pb2note_seq.notebook_utils.download_bundle('attention_rnn.mag','/content/')bundle=sequence_generator_bundle.read_bundle_file('/content/basic_rnn.mag')generator_map=melody_rnn_sequence_generator.get_generator_map()melody_rnn=generator_map'basic_rnn'melody_rnn.initialize()这应该只需几秒钟。与本书中遇到的其他模型相比,Magenta模型非常小。 现在,我们可以将之前的旋律和一些参数一起输入,以继续这首歌曲: defget_options(input_sequence,num_steps=128,temperature=1.0):last_end_time=(max(n.end_timefornininput_sequence.notes)ifinput_sequence.noteselse0)qpm=input_sequence.tempos[0].qpmseconds_per_step=60.0/qpm/melody_rnn.steps_per_quartertotal_seconds=num_steps*seconds_per_stepgenerator_options=generator_pb2.GeneratorOptions()generator_options.args['temperature'].float_value=temperaturegenerate_section=generator_options.generate_sections.add(start_time=last_end_time+seconds_per_step,end_time=total_seconds)returngenerator_optionssequence=melody_rnn.generate(input_sequence,get_options(twinkle_twinkle))现在我们可以绘制和播放新音乐: note_seq.plot_sequence(sequence)note_seq.play_sequence(sequence,synth=note_seq.fluidsynth)再次,我们得到了Bokeh库的绘图和播放小部件: 我们可以像这样从我们的音符序列创建一个MIDI文件: note_seq.sequence_proto_to_midi_file(sequence,'twinkle_continued.mid')这将在磁盘上创建一个新的MIDI文件。 在GoogleColab上,我们可以这样下载MIDI文件: LSTM层的基本单元是LSTM单元,由几个调节器组成,我们可以在下面的示意图中看到: 监管机构包括以下内容: 我们可以解释这些门背后的直觉,而不陷入方程的细节。一个输入门调节输入对单元的影响力,一个输出门减少传出单元的激活,而遗忘门则衰减单元的活动。 在MelodyRNN中,注意力掩码a的应用如下: 我们首先将完成一个简单的监督任务,确定段落的情感,然后我们将设置一个响应命令的Alexa风格聊天机器人。接下来,我们将使用序列到序列模型翻译文本。最后,我们将尝试使用最先进的文本生成模型写一本流行小说。 在本章中,我们将进行以下配方: 与迄今为止大多数章节一样,我们将尝试基于PyTorch和TensorFlow的模型。我们将在每个配方中应用不同的更专业的库。 您可能还想比较在《第二章》中算法偏差对抗的配方中,使用词袋法(在scikit-learn中的CountVectorizer)来解决这个问题的方法。在这个配方中,我们将使用词嵌入和使用词嵌入的深度学习模型。 在这个配方中,我们将使用scikit-learn和TensorFlow(Keras),正如本书的许多其他配方一样。此外,我们将使用需要下载的词嵌入,并且我们将使用Gensim库的实用函数在我们的机器学习管道中应用它们: !pipinstallgensim我们将使用来自scikit-learn的数据集,但我们仍然需要下载词嵌入。我们将使用Facebook的fastText词嵌入,该词嵌入是在Wikipedia上训练的: 新闻组数据集是大约20,000个新闻组文档的集合,分为20个不同的组。20个新闻组集合是在NLP中测试机器学习技术(如文本分类和文本聚类)的流行数据集。 我们将把一组新闻组分类为三个不同的主题,并且我们将使用三种不同的技术来解决这个任务,以便进行比较。首先获取数据集,然后应用词袋法技术,使用词嵌入,在深度学习模型中训练定制的词嵌入。 首先,我们将使用scikit-learn的功能下载数据集。我们将新闻组数据集分两批下载,分别用于训练和测试。 fromsklearn.datasetsimportfetch_20newsgroupscategories=['alt.atheism','soc.religion.christian','comp.graphics','sci.med']twenty_train=fetch_20newsgroups(subset='train',categories=categories,shuffle=True,random_state=42)twenty_test=fetch_20newsgroups(subset='test',categories=categories,shuffle=True,random_state=42)这方便地为我们提供了训练和测试数据集,我们可以在这三种方法中使用。 让我们开始覆盖第一个方法,使用词袋法。 我们将构建一个单词计数和根据它们的频率重新加权的管道。最终的分类器是一个随机森林。我们在训练数据集上训练这个模型: importnumpyasnpfromsklearn.pipelineimportPipelinefromsklearn.feature_extraction.textimportCountVectorizerfromsklearn.feature_extraction.textimportTfidfTransformerfromsklearn.ensembleimportRandomForestClassifiertext_clf=Pipeline([('vect',CountVectorizer()),('tfidf',TfidfTransformer()),('clf',RandomForestClassifier()),])text_clf.fit(twenty_train.data,twenty_train.target)CountVectorizer计算文本中的标记,而tfidfTransformer重新加权这些计数。我们将在工作原理...部分讨论词项频率-逆文档频率(TFIDF)重新加权。 训练后,我们可以在测试数据集上测试准确率: predicted=text_clf.predict(twenty_test.data)np.mean(predicted==twenty_test.target)我们的准确率约为0.805。让我们看看我们的另外两种方法表现如何。下一步是使用词嵌入。 我们将加载我们之前下载的词嵌入: fromgensim.modelsimportKeyedVectorsmodel=KeyedVectors.load_word2vec_format('wiki.en.vec',binary=False,encoding='utf8')将文本的最简单策略向量化是对单词嵌入进行平均。对于短文本,这通常至少效果还不错: importnumpyasnpfromtensorflow.keras.preprocessing.textimporttext_to_word_sequencedefembed_text(text:str):vector_list=[model.wv[w].reshape(-1,1)forwintext_to_word_sequence(text)ifwinmodel.wv]iflen(vector_list)>0:returnnp.mean(np.concatenate(vector_list,axis=1),axis=1).reshape(1,300)else:returnnp.zeros(shape=(1,300))assertembed_text('trainingrun').shape==(1,300)然后我们将这种向量化应用于我们的数据集,然后在这些向量的基础上训练一个随机森林分类器: train_transformed=np.concatenate([embed_text(t)fortintwenty_train.data])rf=RandomForestClassifier().fit(train_transformed,twenty_train.target)然后我们可以测试我们方法的性能: test_transformed=np.concatenate([embed_text(t)fortintwenty_test.data])predicted=rf.predict(test_transformed)np.mean(predicted==twenty_test.target)我们的准确率约为0.862。 让我们看看我们的最后一种方法是否比这个更好。我们将使用Keras的嵌入层构建定制的词嵌入。 嵌入层是在神经网络中即时创建自定义词嵌入的一种方式: fromtensorflow.kerasimportlayersembedding=layers.Embedding(input_dim=5000,output_dim=50,input_length=500)我们必须告诉嵌入层希望存储多少单词,词嵌入应该具有多少维度,以及每个文本中有多少单词。我们将整数数组输入嵌入层,每个数组引用字典中的单词。我们可以将创建嵌入层输入的任务委托给TensorFlow实用函数: fromtensorflow.keras.preprocessing.textimportTokenizertokenizer=Tokenizer(num_words=5000)tokenizer.fit_on_texts(twenty_train.data)这创建了字典。现在我们需要对文本进行分词并将序列填充到适当的长度: fromtensorflow.keras.preprocessing.sequenceimportpad_sequencesX_train=tokenizer.texts_to_sequences(twenty_train.data)X_test=tokenizer.texts_to_sequences(twenty_test.data)X_train=pad_sequences(X_train,padding='post',maxlen=500)X_test=pad_sequences(X_test,padding='post',maxlen=500)现在我们准备构建我们的神经网络: fromtensorflow.keras.modelsimportSequentialfromtensorflow.keras.lossesimportSparseCategoricalCrossentropyfromtensorflow.kerasimportregularizersmodel=Sequential()model.add(embedding)model.add(layers.Flatten())model.add(layers.Dense(10,activation='relu',kernel_regularizer=regularizers.l1_l2(l1=1e-5,l2=1e-4)))model.add(layers.Dense(len(categories),activation='softmax'))model.compile(optimizer='adam',loss=SparseCategoricalCrossentropy(),metrics=['accuracy'])model.summary()我们的模型包含50万个参数。大约一半位于嵌入层,另一半位于前馈全连接层。 我们对网络进行了几个epoch的拟合,然后可以在测试数据上测试我们的准确性: model.fit(X_train,twenty_train.target,epochs=10)predicted=model.predict(X_test).argmax(axis=1)np.mean(predicted==twenty_test.target)我们获得约为0.902的准确率。我们还没有调整模型架构。 这完成了我们使用词袋模型、预训练词嵌入和自定义词嵌入进行新闻组分类的工作。现在我们来探讨一些背景。 我们已经根据三种不同的特征化方法对文本进行了分类:词袋模型、预训练词嵌入和自定义词嵌入。让我们简要地深入研究词嵌入和TFIDF。 在第五章的基于知识做决策配方中,我们已经讨论了Skipgram和ContinuousBagofWords(CBOW)算法,在启发式搜索技术与逻辑推理(在使用Walklets进行图嵌入子节中)。 简而言之,词向量是一个简单的机器学习模型,可以根据上下文(CBOW算法)预测下一个词,或者可以根据一个单词预测上下文(Skipgram算法)。让我们快速看一下CBOW神经网络。 CBOW算法是一个两层前馈神经网络,用于从上下文中预测单词(更确切地说是稀疏索引向量): 我们还没有讨论这些词嵌入的含义,这些词嵌入在它们出现时引起了轰动。这些嵌入是单词的网络激活,并具有组合性质,这为许多演讲和少数论文赋予了标题。我们可以结合向量进行语义代数或进行类比。其中最著名的例子是以下内容: 直观地说,国王和王后是相似的社会职位,只是一个由男人担任,另一个由女人担任。这在数十亿个单词学习的嵌入空间中得到了反映。从国王的向量开始,减去男人的向量,最后加上女人的向量,我们最终到达的最接近的单词是王后。 嵌入空间可以告诉我们关于我们如何使用语言的很多信息,其中一些信息有些令人担忧,比如当单词向量展现出性别刻板印象时。 让我们快速看看在这个方法的词袋法中采用的重新加权。 有些词可能出现在每个文档中;其他词可能只出现在文档的一个小子集中,这表明它们更为特定和精确。这就是TFIDF的直觉,即如果一个词在语料库(文档集合)中的频率低,则提升计数(矩阵中的列)的重要性。 在本章的下一个示例中,我们将超越单词的编码,研究更复杂的语言模型。 我们将简要介绍如何使用Gensim学习自己的词嵌入,构建更复杂的深度学习模型,并在Keras中使用预训练的词嵌入: 让我们读入一个文本文件,以便将其作为fastText的训练数据集: 训练本身很简单,并且由于我们的文本文件很小,所以相对快速: 您可以像这样从训练好的模型中检索向量: model.wv['axe']Gensim具有丰富的功能,我们建议您阅读一些其文档。 x=Conv1D(128,5,activation='relu')(embedded_sequences)x=MaxPooling1D(5)(x)卷积层具有非常少的参数,这是使用它们的另一个优点。 word_index={i:wfori,winenumerate(model.wv.vocab.keys())}然后我们可以将这些向量馈送到嵌入层中: fromtensorflow.keras.layersimportEmbeddingembedding_layer=Embedding(len(word_index)+1,300,weights=[list(model.wv.vectors)],input_length=500,trainable=False)为了训练和测试,您必须通过在我们的新词典中查找它们来提供单词索引,并像之前一样将它们填充到相同的长度。 这就结束了我们关于新闻组分类的配方。我们应用了三种不同的特征化方法:词袋模型、预训练词嵌入以及简单神经网络中的自定义词嵌入。 在这个配方中,我们使用了词嵌入。已经介绍了许多不同的嵌入方法,并且已经发布了许多训练自数百亿字词和数百万文档的词嵌入矩阵。如果在租用的硬件上进行大规模训练,这可能会耗费数十万美元。最流行的词嵌入包括以下内容: 处理词嵌入的流行库包括以下内容: 在这个教程中,我们将构建一个AI助手。这其中的困难在于,人们表达自己的方式有无数种,而且根本不可能预料到用户可能说的一切。在这个教程中,我们将训练一个模型来推断他们想要什么,并且我们会相应地做出回应。 对于这个教程,我们将使用FarizRahman开发的名为Eywa的框架。我们将从GitHub使用pip安装它: 我们还将通过pyOWM库使用OpenWeatherMapWebAPI,因此我们也将安装这个库: 让我们看看我们如何实现这一点。 我们的代理将处理用户输入的句子,解释并相应地回答。它将首先预测用户查询的意图,然后提取实体,以更准确地了解查询的内容,然后返回答案: fromeywa.nluimportClassifierCONV_SAMPLES={'greetings':['Hi','hello','Howareyou','heythere','hey'],'taxi':['bookacab','needaride','findmeacab'],'weather':['whatistheweatherintokyo','weathergermany','whatistheweatherlikeinkochi','whatistheweatherlike','isithotoutside'],'datetime':['whatdayistoday','todaysdate','whattimeisitnow','timenow','whatisthetime'],'music':['playtheBeatles','shufflesongs','makeasound']}CLF=Classifier()forkeyinCONV_SAMPLES:CLF.fit(CONV_SAMPLES[key],key)我们已经创建了基于对话样本的分类器。我们可以使用以下代码块快速测试其工作原理: fromeywa.nluimportEntityExtractorX_WEATHER=['whatistheweatherintokyo','weathergermany','whatistheweatherlikeinkochi']Y_WEATHER=[{'intent':'weather','place':'tokyo'},{'intent':'weather','place':'germany'},{'intent':'weather','place':'kochi'}]EX_WEATHER=EntityExtractor()EX_WEATHER.fit(X_WEATHER,Y_WEATHER)这是为了检查天气预测的特定位置。我们也可以测试天气的实体提取: EX_WEATHER.predict('whatistheweatherinLondon')我们询问伦敦的天气,并且我们的实体提取成功地返回了地点名称: {'intent':'weather','place':'London'}frompyowmimportOWMmgr=OWM('YOUR-API-KEY').weather_manager()defget_weather_forecast(place):observation=mgr.weather_at_place(place)returnobservation.get_weather().get_detailed_status()print(get_weather_forecast('London'))介绍中提到的原始ELIZA有许多语句-响应对,例如以下内容: overcastclouds没有问候和日期,没有一个聊天机器人是完整的: 在匹配正则表达式的情况下,会随机选择一种可能的响应,如果需要,动词会进行转换,包括使用如下逻辑进行缩写: X_GREETING=['Hii','helllo','Howdy','heythere','hey','Hi']Y_GREETING=[{'greet':'Hii'},{'greet':'helllo'},{'greet':'Howdy'},{'greet':'hey'},{'greet':'hey'},{'greet':'Hi'}]EX_GREETING=EntityExtractor()EX_GREETING.fit(X_GREETING,Y_GREETING)X_DATETIME=['whatdayistoday','datetoday','whattimeisitnow','timenow']Y_DATETIME=[{'intent':'day','target':'today'},{'intent':'date','target':'today'},{'intent':'time','target':'now'},{'intent':'time','target':'now'}]EX_DATETIME=EntityExtractor()EX_DATETIME.fit(X_DATETIME,Y_DATETIME)_EXTRACTORS={'taxi':None,'weather':EX_WEATHER,'greetings':EX_GREETING,'datetime':EX_DATETIME,'music':None}Eywa,一个用于对话代理的框架,具有三个主要功能: importdatetime_EXTRACTORS={'taxi':None,'weather':EX_WEATHER,'greetings':EX_GREETING,'datetime':EX_DATETIME,'music':None}defquestion_and_answer(u_query:str):q_class=CLF.predict(u_query)print(q_class)if_EXTRACTORS[q_class]isNone:return'Sorry,youhavetoupgradeyoursoftware!'q_entities=_EXTRACTORS[q_class].predict(u_query)print(q_entities)ifq_class=='greetings':returnq_entities.get('greet','hello')ifq_class=='weather':place=q_entities.get('place','London').replace('_','')return'Theforecastfor{}is{}'.format(place,get_weather_forecast(place))ifq_class=='datetime':return'Today\'sdateis{}'.format(datetime.datetime.today().strftime('%B%d,%Y'))return'Icouldn\'tunderstandwhatyousaid.Iamsorry.'在我们详细讨论这些内容之前,看一下介绍中提到的ELIZA聊天机器人可能会很有趣。这将希望我们了解需要理解更广泛语言集的改进。 whileTrue:query=input('\nHowcanIhelpyou')print(question_and_answer(query))实体提取器-从句子中提取命名实体 你应该能够询问不同地方的日期和天气情况,但如果你询问出租车或音乐,它会告诉你需要升级你的软件。 对于机器而言,在开始阶段,硬编码一些规则会更容易,但如果您想处理更多复杂性,您将构建解释意图和位置等参考的模型。 ... 这总结了我们的配方。我们实现了一个简单的聊天机器人,首先预测意图,然后基于规则提取实体回答用户查询。 “感谢您的来电,我的名字是_。今天我能为您做什么?” [r'Isthere(.*)',["Doyouthinkthereis%1","It'slikelythatthereis%1.","Wouldyouliketheretobe%1"]],我们可以使用PythonOpenWeatherMap库(pyOWM)请求给定位置的天气预报。在撰写本文时,调用新功能get_weather_forecast(),将London作为参数传入,结果如下: gReflections={#..."i'd":"youwould",}ELIZA是如何工作的? 如果你有兴趣,你应该能够自己实现和扩展这个功能。 让我们基于分类器和实体提取创建一些互动。我们将编写一个响应函数,可以问候,告知日期和提供天气预报: Eywa 请注意,如果您想执行此操作,您需要使用您自己的(免费)OpenWeatherMapAPI密钥。 我们已经为基本任务实现了一个非常简单但有效的聊天机器人。很明显,这可以扩展和定制以处理更多或其他任务。 question_and_answer()函数回答用户查询。 这是如何运作的… 这三者使用起来非常简单,但功能强大。我们在“如何做...”部分看到了前两者的功能。让我们看看基于语义上下文的食品类型模式匹配: fromeywa.nluimportPatternp=Pattern('Iwanttoeat[food:pizza,banana,yogurt,kebab]')p('i\'dliketoeatsushi')我们创建一个名为food的变量,并赋予样本值:pizza、banana、yogurt和kebab。在类似上下文中使用食品术语将匹配我们的变量。这个表达式应该返回这个: {'food':'sushi'}使用看起来与正则表达式非常相似,但正则表达式基于单词及其形态学,eywa.nlu.Pattern则在语义上锚定在词嵌入中工作。 正则表达式(简称:regex)是定义搜索模式的字符序列。它由StevenKleene首次形式化,并由KenThompson和其他人在Unix工具(如QED、ed、grep和sed)中实现于1960年代。这种语法已进入POSIX标准,因此有时也称为POSIX正则表达式。在1990年代末,随着Perl编程语言的出现,出现了另一种标准,称为Perl兼容正则表达式(PCRE),已在包括Python在内的不同编程语言中得到采用。 这些模型如何工作? 分类器通过存储的对话项目并根据这些嵌入选择具有最高相似度分数的匹配项。请注意,eywa还有另一个基于递归神经网络的模型实现。 创建聊天机器人的库和框架非常丰富,包括不同的想法和集成方式: 在这个食谱中,我们将从头开始实现一个Transformer网络,并将其用于从英语到德语的翻译任务。在它是如何工作的...部分,我们将详细介绍很多细节。 我们建议使用带有GPU的机器。强烈推荐使用Colab环境,但请确保您正在使用启用了GPU的运行时。如果您想检查是否可以访问GPU,可以调用NVIDIA系统管理接口: !nvidia-smi你应该看到类似这样的东西: TeslaT4:0MiB/15079MiB这告诉您正在使用NVIDIATeslaT4,已使用1.5GB的0MB(1MiB大约相当于1.049MB)。 我们需要一个相对较新版本的torchtext,这是一个用于pytorch的文本数据集和实用工具库。 !pipinstalltorchtext==0.7.0对于还有更多...部分,您可能需要安装额外的依赖项: !pipinstallhydra-core我们正在使用spaCy进行标记化。这在Colab中预先安装。在其他环境中,您可能需要pip-install它。我们确实需要安装德语核心功能,例如spacy的标记化,在这个食谱中我们将依赖它: !python-mspacydownloadde我们将在食谱的主要部分加载此功能。 我们将首先准备数据集,然后实现Transformer架构,接着进行训练,最后进行测试: importtorchimporttorch.nnasnnimporttorch.optimasoptimimporttorchtextfromtorchtext.datasetsimportMulti30kfromtorchtext.dataimportField,BucketIteratorimportmatplotlib.pyplotaspltimportmatplotlib.tickerastickerimportspacyimportnumpyasnpimportmath我们将要训练的数据集是Multi30k数据集。这是一个包含约30,000个平行英语、德语和法语短句子的数据集。 我们将加载spacy功能,实现函数来标记化德语和英语文本: spacy_de=spacy.load('de')spacy_en=spacy.load('en')deftokenize_de(text):return[tok.textfortokinspacy_de.tokenizer(text)]deftokenize_en(text):return[tok.textfortokinspacy_en.tokenizer(text)]这些函数将德语和英语文本从字符串标记化为字符串列表。 Field定义了将文本转换为张量的操作。它提供了常见文本处理工具的接口,并包含一个Vocab,将标记或单词映射到数值表示。我们正在传递我们的前面的标记化方法: SRC=Field(tokenize=tokenize_en,init_token=' train_data,valid_data,test_data=Multi30k.splits(exts=('.en','.de'),fields=(SRC,TRG))SRC.build_vocab(train_data,min_freq=2)TRG.build_vocab(train_data,min_freq=2)然后我们可以定义我们的数据迭代器,覆盖训练、验证和测试数据集: device=torch.device('cuda'iftorch.cuda.is_available()else'cpu')BATCH_SIZE=128train_iterator,valid_iterator,test_iterator=BucketIterator.splits((train_data,valid_data,test_data),batch_size=BATCH_SIZE,device=device)现在我们可以在训练此数据集之前构建我们的变压器架构。 classPositionwiseFeedforwardLayer(nn.Module):def__init__(self,hid_dim,pf_dim,dropout):super().__init__()self.fc_1=nn.Linear(hid_dim,pf_dim)self.fc_2=nn.Linear(pf_dim,hid_dim)self.dropout=nn.Dropout(dropout)defforward(self,x):x=self.dropout(torch.relu(self.fc_1(x)))x=self.fc_2(x)returnx我们需要Encoder和Decoder部分,每个部分都有自己的层。然后我们将这两者连接成Seq2Seq模型。 这是编码器的外观: classEncoder(nn.Module):def__init__(self,input_dim,hid_dim,n_layers,n_heads,pf_dim,dropout,device,max_length=100):super().__init__()self.device=deviceself.tok_embedding=nn.Embedding(input_dim,hid_dim)self.pos_embedding=nn.Embedding(max_length,hid_dim)self.layers=nn.ModuleList([EncoderLayer(hid_dim,n_heads,pf_dim,dropout,device)for_inrange(n_layers)])self.dropout=nn.Dropout(dropout)self.scale=torch.sqrt(torch.FloatTensor([hid_dim])).to(device)defforward(self,src,src_mask):batch_size=src.shape[0]src_len=src.shape[1]pos=torch.arange(0,src_len).unsqueeze(0).repeat(batch_size,1).to(self.device)src=self.dropout((self.tok_embedding(src)*self.scale)+self.pos_embedding(pos))forlayerinself.layers:src=layer(src,src_mask)returnsrc它由多个编码器层组成。它们看起来如下所示: classEncoderLayer(nn.Module):def__init__(self,hid_dim,n_heads,pf_dim,dropout,device):super().__init__()self.self_attn_layer_norm=nn.LayerNorm(hid_dim)self.ff_layer_norm=nn.LayerNorm(hid_dim)self.self_attention=MultiHeadAttentionLayer(hid_dim,n_heads,dropout,device)self.positionwise_feedforward=PositionwiseFeedforwardLayer(hid_dim,pf_dim,dropout)self.dropout=nn.Dropout(dropout)defforward(self,src,src_mask):_src,_=self.self_attention(src,src,src,src_mask)src=self.self_attn_layer_norm(src+self.dropout(_src))_src=self.positionwise_feedforward(src)src=self.ff_layer_norm(src+self.dropout(_src))returnsrc解码器与编码器并没有太大的不同,但是它附带了两个多头注意力层。解码器看起来像这样: classDecoder(nn.Module):def__init__(self,output_dim,hid_dim,n_layers,n_heads,pf_dim,dropout,device,max_length=100):super().__init__()self.device=deviceself.tok_embedding=nn.Embedding(output_dim,hid_dim)self.pos_embedding=nn.Embedding(max_length,hid_dim)self.layers=nn.ModuleList([DecoderLayer(hid_dim,n_heads,pf_dim,dropout,device)for_inrange(n_layers)])self.fc_out=nn.Linear(hid_dim,output_dim)self.dropout=nn.Dropout(dropout)self.scale=torch.sqrt(torch.FloatTensor([hid_dim])).to(device)defforward(self,trg,enc_src,trg_mask,src_mask):batch_size=trg.shape[0]trg_len=trg.shape[1]pos=torch.arange(0,trg_len).unsqueeze(0).repeat(batch_size,1).to(self.device)trg=self.dropout((self.tok_embedding(trg)*self.scale)+self.pos_embedding(pos))forlayerinself.layers:trg,attention=layer(trg,enc_src,trg_mask,src_mask)output=self.fc_out(trg)returnoutput,attention在序列中,解码器层执行以下任务: 自注意力层中的掩码是为了避免模型在预测中包含下一个标记(这将是作弊)。 让我们实现解码器层: classDecoderLayer(nn.Module):def__init__(self,hid_dim,n_heads,pf_dim,dropout,device):super().__init__()self.self_attn_layer_norm=nn.LayerNorm(hid_dim)self.enc_attn_layer_norm=nn.LayerNorm(hid_dim)self.ff_layer_norm=nn.LayerNorm(hid_dim)self.self_attention=MultiHeadAttentionLayer(hid_dim,n_heads,dropout,device)self.encoder_attention=MultiHeadAttentionLayer(hid_dim,n_heads,dropout,device)self.positionwise_feedforward=PositionwiseFeedforwardLayer(hid_dim,pf_dim,dropout)self.dropout=nn.Dropout(dropout)defforward(self,trg,enc_src,trg_mask,src_mask):_trg,_=self.self_attention(trg,trg,trg,trg_mask)trg=self.self_attn_layer_norm(trg+self.dropout(_trg))_trg,attention=self.encoder_attention(trg,enc_src,enc_src,src_mask)trg=self.enc_attn_layer_norm(trg+self.dropout(_trg))_trg=self.positionwise_feedforward(trg)trg=self.ff_layer_norm(trg+self.dropout(_trg))returntrg,attention最后,在Seq2Seq模型中一切都汇聚在一起: classSeq2Seq(nn.Module):def__init__(self,encoder,decoder,src_pad_idx,trg_pad_idx,device):super().__init__()self.encoder=encoderself.decoder=decoderself.src_pad_idx=src_pad_idxself.trg_pad_idx=trg_pad_idxself.device=devicedefmake_src_mask(self,src):src_mask=(src!=self.src_pad_idx).unsqueeze(1).unsqueeze(2)returnsrc_maskdefmake_trg_mask(self,trg):trg_pad_mask=(trg!=self.trg_pad_idx).unsqueeze(1).unsqueeze(2)trg_len=trg.shape[1]trg_sub_mask=torch.tril(torch.ones((trg_len,trg_len),device=self.device)).bool()trg_mask=trg_pad_mask&trg_sub_maskreturntrg_maskdefforward(self,src,trg):src_mask=self.make_src_mask(src)trg_mask=self.make_trg_mask(trg)enc_src=self.encoder(src,src_mask)output,attention=self.decoder(trg,enc_src,trg_mask,src_mask)returnoutput,attention现在我们可以用实际参数实例化我们的模型: INPUT_DIM=len(SRC.vocab)OUTPUT_DIM=len(TRG.vocab)HID_DIM=256ENC_LAYERS=3DEC_LAYERS=3ENC_HEADS=8DEC_HEADS=8ENC_PF_DIM=512DEC_PF_DIM=512ENC_DROPOUT=0.1DEC_DROPOUT=0.1enc=Encoder(INPUT_DIM,HID_DIM,ENC_LAYERS,ENC_HEADS,ENC_PF_DIM,ENC_DROPOUT,device)dec=Decoder(OUTPUT_DIM,HID_DIM,DEC_LAYERS,DEC_HEADS,DEC_PF_DIM,DEC_DROPOUT,device)SRC_PAD_IDX=SRC.vocab.stoi[SRC.pad_token]TRG_PAD_IDX=TRG.vocab.stoi[TRG.pad_token]model=Seq2Seq(enc,dec,SRC_PAD_IDX,TRG_PAD_IDX,device).to(device)整个模型共有9,543,087个可训练参数。 definitialize_weights(m):ifhasattr(m,'weight')andm.weight.dim()>1:nn.init.xavier_uniform_(m.weight.data)model.apply(initialize_weights);我们需要将学习率设置得比默认值低得多: LEARNING_RATE=0.0005optimizer=torch.optim.Adam(model.parameters(),lr=LEARNING_RATE)在我们的损失函数CrossEntropyLoss中,我们必须确保忽略填充的标记: criterion=nn.CrossEntropyLoss(ignore_index=TRG_PAD_IDX)我们的训练函数如下所示: deftrain(model,iterator,optimizer,criterion,clip):model.train()epoch_loss=0fori,batchinenumerate(iterator):src=batch.srctrg=batch.trgoptimizer.zero_grad()output,_=model(src,trg[:,:-1])output_dim=output.shape[-1]output=output.contiguous().view(-1,output_dim)trg=trg[:,1:].contiguous().view(-1)loss=criterion(output,trg)loss.backward()torch.nn.utils.clip_grad_norm_(model.parameters(),clip)optimizer.step()epoch_loss+=loss.item()returnepoch_loss/len(iterator)然后在循环中执行训练: N_EPOCHS=10CLIP=1best_valid_loss=float('inf')forepochinrange(N_EPOCHS):train_loss=train(model,train_iterator,optimizer,criterion,CLIP)print(f'\tTrainLoss:{train_loss:.3f}|TrainPPL:{math.exp(train_loss):7.3f}')我们在这里略微简化了事情。您可以在GitHub上找到完整的笔记本。 这个模型训练了10个时期。 为了翻译一个句子,我们必须使用之前创建的源词汇表将其数值化编码,并在将其馈送到我们的模型之前附加停止标记。然后,必须从目标词汇表中解码模型输出: example_idx=8src=vars(train_data.examples[example_idx])['src']trg=vars(train_data.examples[example_idx])['trg']print(f'src={src}')print(f'trg={trg}')我们得到了以下对: src=['a','woman','with','a','large','purse','is','walking','by','a','gate','.']trg=['eine','frau','mit','einer','groen','geldbrse','geht','an','einem','tor','vorbei','.']我们可以将其与我们模型获得的翻译进行比较: translation,attention=translate_sentence(src,SRC,TRG,model,device)print(f'predictedtrg={translation}')这是我们的翻译句子: predictedtrg=['eine','frau','mit','einer','groen','handtasche','geht','an','einem','tor','vorbei','.',' 然后,我们可以计算我们模型与黄金标准的BLEU分数的指标: 在翻译中,一个有用的度量标准是双语评估助手(BLEU)分数,其中1是最佳值。它是候选翻译部分与参考翻译(黄金标准)部分的比率,其中部分可以是单个词或一个词序列(n-grams)。 这就是我们的翻译模型了。我们可以看到实际上创建一个翻译模型并不是那么困难。然而,其中有很多理论知识,我们将在下一节中进行介绍。 在这个示例中,我们为英语到德语的翻译任务从头开始训练了一个变压器模型。让我们稍微了解一下变压器是什么,以及它是如何工作的。 变压器架构最初是为机器翻译而创建的(AshishVaswani等人的注意力机制就是你所需要的,2017年)。变压器网络摒弃了递归和卷积,训练和预测速度大大加快,因为单词可以并行处理。变压器架构提供了通用的语言模型,在许多任务中推动了技术发展,如神经机器翻译(NMT),问答系统(QA),命名实体识别(NER),文本蕴涵(TE),抽象文本摘要等。变压器模型通常被拿来即插即用,并针对特定任务进行微调,以便从长期和昂贵的训练过程中获得的通用语言理解中受益。 Transformers由两部分组成,类似于自动编码器: 我们的示例中实现与原始变压器实现(AshishVaswani等人的注意力机制就是你所需要的,2017年)之间的差异如下: 这些变化与现代转换器(如BERT)保持同步。 然后编码器通过堆叠模块传递,每个模块包括注意力、前馈全连接层和归一化。注意力层是缩放的乘法(点积)注意力层的线性组合(多头注意力)。 一些转换器架构只包含其中的一部分。例如,OpenAIGPT转换器架构(AlecRadfor等人,《通过生成预训练改进语言理解》,2018年),生成了非常连贯的文本,由堆叠的解码器组成,而Google的BERT架构(JacobDevlin等人,《BERT:深度双向转换器的预训练用于语言理解》,2019年),也由堆叠的编码器组成。 Torch和TensorFlow都有预训练模型的存储库。我们可以从TorchHub下载一个翻译模型并立即使用它。这就是我们将快速展示的内容。对于pytorch模型,我们首先需要安装一些依赖项: !pipinstallfairseqfastBPEsacremoses完成之后,我们可以下载模型。它非常大,这意味着它会占用大量磁盘空间: importtorchen2de=torch.hub.load('pytorch/fairseq','transformer.wmt19.en-de',checkpoint_file='model1.pt:model2.pt:model3.pt:model4.pt',tokenizer='moses',bpe='fastbpe')en2de.translate('Machinelearningisgreat!')我们应该得到这样的输出: MaschinellesLernenistgroartig!这个模型(NathanNg等人,《FacebookFAIR的WMT19新闻翻译任务提交》,2019年)在翻译方面处于技术领先地位。它甚至在精度(BLEU分数)上超越了人类翻译。fairseq附带了用于在您自己的数据集上训练翻译模型的教程。 TorchHub提供了许多不同的翻译模型,还有通用语言模型。 至于支持翻译任务的库,pytorch和tensorflow都提供预训练模型,并支持在翻译中有用的架构: OpenAIGPT-3拥有1750亿个参数,显著推动了语言模型领域的发展,学习了物理学的事实,能够基于描述生成编程代码,并能够撰写娱乐性和幽默性的散文。 在撰写本文时,简·奥斯汀的浪漫19世纪初的小说傲慢与偏见在过去30天内下载量最高(超过47,000次)。我们将以纯文本格式下载这本书: 我们将在Colab中工作,您将可以访问NvidiaT4或NvidiaK80GPU。但是,您也可以使用自己的计算机,使用GPU甚至CPU。 我们将使用一个称为gpt-2-simple的OpenAIGPT-2的包装库,由BuzzFeed的数据科学家MaxWoolf创建和维护: %tensorflow_version1.x!pipinstall-qgpt-2-simple此库将使模型对新文本进行微调并在训练过程中显示文本样本变得更加容易。 然后我们可以选择GPT-2模型的大小。OpenAI已经发布了四种大小的预训练模型: 我们将选择小模型: importgpt_2_simpleasgpt2gpt2.download_gpt2(model_name='124M')让我们开始吧! 我们已经下载了一本流行小说傲慢与偏见的文本,并将首先对模型进行微调,然后生成类似傲慢与偏见的文本: 我们将挂载GoogleDrive。gpt-2-simple库提供了一个实用函数: gpt2.copy_file_from_gdrive(file_name)然后我们可以基于我们下载的文本开始微调: sess=gpt2.start_tf_sess()gpt2.finetune(sess,dataset=file_name,model_name='124M',steps=1000,restore_from='fresh',run_name='run1',print_every=10,sample_every=200,save_every=500)我们应该看到训练损失在至少几个小时内下降。我们在训练过程中会看到生成文本的样本,例如这个: gpt2.copy_checkpoint_to_gdrive(run_name='run1')如果我们希望在Colab重新启动后继续训练,我们也可以这样做: #1\.copycheckpointfromgoogledrive:gpt2.copy_checkpoint_from_gdrive(run_name='run1')#2\.continuetraining:sess=gpt2.start_tf_sess()gpt2.finetune(sess,dataset=file_name,model_name='124M',steps=500,restore_from='latest',run_name='run1',print_every=10,overwrite=True,sample_every=200,save_every=100)#3\.let'sbackupthemodelagain:gpt2.copy_checkpoint_to_gdrive(run_name='run1')现在我们可以生成我们的新小说了。 gpt2.copy_checkpoint_from_gdrive(run_name='run1')sess=gpt2.start_tf_sess()gpt2.load_gpt2(sess,run_name='run1')请注意,您可能需要再次重启笔记本(Colab),以避免TensorFlow变量冲突。 gen_file='gpt2_gentext_{:%Y%m%d_%H%M%S}.txt'.format(datetime.utcnow())gpt2.generate_to_file(sess,destination_path=gen_file,temperature=0.7,nsamples=100,batch_size=20)files.download(gen_file)gpt_2_simple.generate()函数接受一个可选的prefix参数,这是要继续的文本。 傲慢与偏见——传奇继续;阅读文本时,有时可以看到一些明显的连续性缺陷,然而,有些段落令人着迷。我们总是可以生成几个样本,这样我们就可以选择我们小说的继续方式。 在这个示例中,我们使用了GPT-2模型来生成文本。这被称为神经故事生成,是神经文本生成的一个子集。简而言之,神经文本生成是构建文本或语言的统计模型,并应用该模型生成更多文本的过程。 可以通过最小化预测令牌与实际令牌的交叉熵来近似这一过程。例如,LSTM、生成对抗网络(GANs)或自回归变换器架构已经用于此目的。 在文本生成中,我们需要做出的一个主要选择是如何抽样,我们有几种选择: 在贪婪搜索中,每次选择评分最高的选择,忽略其他选择。相比之下,束搜索(beamsearch)并行跟踪几个选择的分数,以选择最高分序列,而不是选择高分令牌。Top-k抽样由AngelaFan等人提出(HierarchicalNeuralStoryGeneration,2018)。在top-k抽样中,除了最可能的k个词语外,其他词语都被丢弃。相反,在top-p(也称为核心抽样)中,选择高分词汇超过概率阈值p,而其他词语则被丢弃。可以结合使用top-k和top-p以避免低排名词汇。 尽管huggingfacetransformers库为我们提供了所有这些选择,但是使用gpt-2-simple时,我们可以选择使用top-k抽样和top-p抽样。 在涉及人工智能(AI)的系统创建中,实际上AI通常只占总工作量的一小部分,而实施的主要部分涉及周围基础设施,从数据收集和验证开始,特征提取,分析,资源管理,到服务和监控(DavidSculley等人,《机器学习系统中隐藏的技术债务》,2015年)。 在本章中,我们将处理监控和模型版本控制、作为仪表板的可视化以及保护模型免受可能泄露用户数据的恶意黑客攻击。 在本章中,我们将介绍以下配方: 对于Python库,我们将使用在TensorFlow和PyTorch中开发的模型,并在每个配方中应用不同的、更专业的库。 与业务股东的频繁沟通是获取部署AI解决方案的关键,并应从思路和前提到决策和发现开始。结果如何传达可以决定在商业环境中成功或失败的关键因素。在这个配方中,我们将为一个机器学习(ML)模型构建一个可视化解决方案。 pipinstallstreamlitaltair在这个配方中,我们不会使用笔记本。因此,在这个代码块中,我们已经省略了感叹号。我们将从终端运行所有内容。 让我们继续构建一个交互式数据应用程序。 我们将构建一个简单的应用程序用于模型构建。这旨在展示如何轻松创建一个面向浏览器的视觉交互式应用程序,以向非技术或技术观众展示发现。 作为对streamlit的快速实用介绍,让我们看看如何在Python脚本中的几行代码可以提供服务。 我们将以Python脚本形式编写我们的streamlit应用程序,而不是笔记本,并且我们将使用streamlit执行这些脚本以进行部署。 我们将在我们喜爱的编辑器(例如vim)中创建一个新的Python文件,假设名为streamlit_test.py,并编写以下代码行: importstreamlitasstchosen_option=st.sidebar.selectbox('Hello',['A','B','C'])st.write(chosen_option)这将显示一个选择框或下拉菜单,标题为Hello,并在选项A、B和C之间进行选择。这个选择将存储在变量chosen_option中,在浏览器中可以输出。 我们可以从终端运行我们的简介应用程序如下: streamlitrunstreamlit_test.py--server.port=80服务器端口选项是可选的。 这应该在新标签页或窗口中打开我们的浏览器,显示带有三个选项的下拉菜单。我们可以更改选项,新值将显示出来。 这应该足够作为介绍。现在我们将进入实际的步骤。 我们数据应用的主要思想是将建模选择等决策纳入我们的应用程序,并观察其后果,无论是用数字总结还是在图表中直观显示。 我们将从实现核心功能开始,如建模和数据集加载,然后创建其界面,首先是侧边栏,然后是主页面。我们将所有代码写入一个单独的Python脚本中,可以称之为visualizing_model_results.py: 让我们从一些预备工作开始,如导入: importnumpyasnpimportpandasaspdimportaltairasaltimportstreamlitasstfromsklearn.datasetsimport(load_iris,load_wine,fetch_covtype)fromsklearn.model_selectionimporttrain_test_splitfromsklearn.ensembleimport(RandomForestClassifier,ExtraTreesClassifier,)fromsklearn.treeimportDecisionTreeClassifierfromsklearn.metricsimportroc_auc_scorefromsklearn.metricsimportclassification_report如果您注意到了hello-world介绍,您可能会想知道界面如何与Python通信。这由streamlit处理,每次用户点击某处或输入字段时重新运行您的脚本。 dataset_lookup={'Iris':load_iris,'Wine':load_wine,'Covertype':fetch_covtype,}@st.cachedefload_data(name):iris=dataset_lookup[name]()X_train,X_test,y_train,y_test=train_test_split(iris.data,iris.target,test_size=0.33,random_state=42)feature_names=getattr(iris,'feature_names',[str(i)foriinrange(X_train.shape[1])])target_names=getattr(iris,'target_names',[str(i)foriinnp.unique(iris.target)])return(X_train,X_test,y_train,y_test,target_names,feature_names)这实现了建模和数据集加载器的功能。 我们正在使用scikit-learn数据集API下载数据集。由于scikit-learn的load_x()类型函数(如load_iris(),主要用于玩具数据集)包括target_names和feature_names等属性,但是scikit-learn的fetch_x()函数(如fetch_covtype())用于更大、更严肃的数据集,我们将为这些分别生成特征和目标名称。 训练过程同样被装饰成可以缓存。但请注意,为了确保缓存与模型类型、数据集以及所有超参数都是唯一的,我们必须包含我们的超参数: @st.cachedeftrain_model(dataset_name,model_name,n_estimators,max_depth):model=[mforminmodelsifm.__class__.__name__==model_name][0]withst.spinner('Buildinga{}modelfor{}...'.format(model_name,dataset_name)):returnmodel.fit(X_train,y_train)建模函数接受模型列表,我们将根据超参数的选择进行更新。我们现在将实施这个选择。 在侧边栏中,我们将呈现数据集、模型类型和超参数的选择。让我们首先选择数据集: st.sidebar.title('Modelanddatasetselection')dataset_name=st.sidebar.selectbox('Dataset',list(dataset_lookup.keys()))(X_train,X_test,y_train,y_test,target_names,feature_names)=load_data(dataset_name)这将在我们在iris、wine和covertype之间做出选择后加载数据集。 对于模型的超参数,我们将提供滑动条: n_estimators=st.sidebar.slider('n_estimators',1,100,25)max_depth=st.sidebar.slider('max_depth',1,150,10)最后,我们将再次将模型类型暴露为一个下拉菜单: models=[DecisionTreeClassifier(max_depth=max_depth),RandomForestClassifier(n_estimators=n_estimators,max_depth=max_depth),ExtraTreesClassifier(n_estimators=n_estimators,max_depth=max_depth),]model_name=st.sidebar.selectbox('Model',[m.__class__.__name__forminmodels])model=train_model(dataset_name,model_name,n_estimators,max_depth)最后,在选择后,我们将调用train_model()函数,参数为数据集、模型类型和超参数。 此截图显示了侧边栏的外观: 这显示了浏览器中的菜单选项。我们将在浏览器页面的主要部分展示这些选择的结果。 在主面板上,我们将展示重要的统计数据,包括分类报告、几个图表,应该能够揭示模型的优势和劣势,以及数据本身的视图,在这里模型错误的决策将被突出显示。 我们首先需要一个标题: predictions=model.predict(X_test)probs=model.predict_proba(X_test)st.subheader('Modelperformanceintest')st.write('AUC:{:.2f}'.format(roc_auc_score(y_test,probs,multi_class='ovo'iflen(target_names)>2else'raise',average='macro'iflen(target_names)>2elseNone)))st.write(pd.DataFrame(classification_report(y_test,predictions,target_names=target_names,output_dict=True)))然后,我们将展示一个混淆矩阵,表格化每个类别的实际和预测标签: test_df=pd.DataFrame(data=np.concatenate([X_test,y_test.reshape(-1,1),predictions.reshape(-1,1)],axis=1),columns=feature_names+['target','predicted'])target_map={i:nfori,ninenumerate(target_names)}test_df.target=test_df.target.map(target_map)test_df.predicted=test_df.predicted.map(target_map)confusion_matrix=pd.crosstab(test_df['target'],test_df['predicted'],rownames=['Actual'],colnames=['Predicted'])st.subheader('ConfusionMatrix')st.write(confusion_matrix)我们还希望能够滚动查看被错误分类的测试数据样本: defhighlight_error(s):ifs.predicted==s.target:return['background-color:None']*len(s)return['background-color:red']*len(s)ifst.checkbox('Showtestdata'):st.subheader('Testdata')st.write(test_df.style.apply(highlight_error,axis=1))错误分类的样本将以红色背景突出显示。我们将这种原始数据探索设为可选项,需要通过点击复选框激活。 最后,我们将展示变量在散点图中相互绘制的面板图。这部分将使用altair库: ifst.checkbox('Showtestdistributions'):st.subheader('Distributions')row_features=feature_names[:len(feature_names)//2]col_features=feature_names[len(row_features):]test_df_with_error=test_df.copy()test_df_with_error['error']=test_df.predicted==test_df.targetchart=alt.Chart(test_df_with_error).mark_circle().encode(alt.X(alt.repeat("column"),type='quantitative'),alt.Y(alt.repeat("row"),type='quantitative'),color='error:N').properties(width=250,height=250).repeat(row=row_features,column=col_features).interactive()st.write(chart)这些图中突出显示了错误分类的例子。同样,我们将这部分设为可选项,通过标记复选框激活。 主页上部分用于Covetype数据集的样式如下: 您可以看到分类报告和混淆矩阵。在这些内容之下(不在截图范围内),将是数据探索和数据图表。 这结束了我们的演示应用程序。我们的应用程序相对简单,但希望这个方法能作为构建这些应用程序以进行清晰沟通的指南。 如果你愿意,Streamlit提供了一个本地服务器,可以通过浏览器远程访问。因此,你可以在Azure、GoogleCloud、AWS或你公司的云上运行你的Streamlit应用服务器,并在本地浏览器中查看你的结果。 重要的是理解Streamlit的工作流程。小部件的值由Streamlit存储。其他值在用户与小部件交互时每次都会根据Python脚本从头到尾重新计算。为了避免昂贵的计算,可以使用@st.cache装饰器进行缓存,就像我们在这个示例中看到的那样。 Streamlit的API集成了许多绘图和图表库。这些包括Matplotlib、Seaborn、Plotly、Bokeh,以及Altair、VegaLite、用于地图和3D图表的deck.gl等交互式绘图库,以及graphviz图形。其他集成包括Keras模型、SymPy表达式、pandasDataFrames、图像、音频等。 你可以玩转不同的输入和输出,可以从头开始,也可以改进这个食谱中的代码。我们可以扩展它以显示不同的结果,构建仪表板,连接到数据库进行实时更新,或者为专业主题专家建立用户反馈表单,例如注释或批准。 AI和统计可视化是一个广泛的领域。FernandaViégas和MartinWattenberg在NIPS2018上进行了一场名为机器学习可视化的概述演讲,并且你可以找到他们的幻灯片和演讲录像。 这是一些有趣的Streamlit演示列表: 除了Streamlit,还有其他可以帮助创建交互式仪表板、演示和报告的库和框架,比如Bokeh、JupyterVoilà、Panel和PlotlyDash。 在这个示例中,我们将从头开始构建一个小型推断服务器,并专注于将人工智能引入生产环境的技术挑战。我们将展示如何通过稳健性、按需扩展、及时响应的软件解决方案将POC开发成适合生产的解决方案,并且可以根据需要快速更新。 在这个示例中,我们将在终端和Jupyter环境之间切换。我们将在Jupyter环境中创建和记录模型。我们将从终端控制mlflow服务器。我们会注意哪个代码块适合哪个环境。 我们将在这个示例中使用mlflow。让我们从终端安装它: pipinstallmlflow我们假设您已经安装了conda。如果没有,请参考《Python人工智能入门》中的设置Jupyter环境一章,以获取详细说明。 我们可以像这样从终端启动我们的本地mlflow服务器,使用sqlite数据库作为后端存储: 这是我们可以从浏览器访问此服务器的地方。在浏览器中,我们可以比较和检查不同的实验,并查看我们模型的指标。 在更多内容...部分,我们将快速演示如何使用FastAPI库设置自定义API。我们也将快速安装此库: !pipinstallfastapi有了这个,我们就准备好了! 我们将从一个逗号分隔值(CSV)文件中构建一个简单的模型。我们将尝试不同的建模选项,并进行比较。然后我们将部署这个模型: 我们将下载一个数据集作为CSV文件并准备进行训练。在这个示例中选择的数据集是葡萄酒数据集,描述了葡萄酒样本的质量。我们将从UCIML存档中下载并读取葡萄酒质量的CSV文件: train_x,test_x,train_y,test_y=train_test_split(data.drop(['quality'],axis=1),data['quality'])我们可以在mlflow服务器中跟踪我们喜欢的任何东西。我们可以创建一个用于性能指标报告的函数,如下所示: fromsklearn.metricsimportmean_squared_error,mean_absolute_error,r2_scoredefeval_metrics(actual,pred):rmse=np.sqrt(mean_squared_error(actual,pred))mae=mean_absolute_error(actual,pred)r2=r2_score(actual,pred)returnrmse,mae,r2在运行训练之前,我们需要将mlflow库注册到服务器上: 在我们的训练函数中,我们在训练数据上训练,在测试数据上提取我们的模型指标。我们需要选择适合比较的适当超参数和指标: fromsklearn.linear_modelimportElasticNetimportmlflow.sklearnnp.random.seed(40)deftrain(alpha=0.5,l1_ratio=0.5):withmlflow.start_run():lr=ElasticNet(alpha=alpha,l1_ratio=l1_ratio,random_state=42)lr.fit(train_x,train_y)predicted=lr.predict(test_x)rmse,mae,r2=eval_metrics(test_y,predicted)model_name=lr.__class__.__name__print('{}(alpha={},l1_ratio={}):'.format(model_name,alpha,l1_ratio))print('RMSE:%s'%rmse)print('MAE:%s'%mae)print('R2:%s'%r2)mlflow.log_params({key:valueforkey,valueinlr.get_params().items()})mlflow.log_metric('rmse',rmse)mlflow.log_metric('r2',r2)mlflow.log_metric('mae',mae)mlflow.sklearn.log_model(lr,model_name)我们拟合模型,提取我们的指标,将它们打印到屏幕上,在mlflow服务器上记录它们,并将模型工件存储在服务器上。 我们还可以计算更多伴随模型的工件,例如变量重要性。 我们还可以尝试使用不同的超参数,例如以下方式: train(0.5,0.5)我们应该得到性能指标作为输出: ElasticNet(alpha=0.5,l1_ratio=0.5):RMSE:0.7325693777577805MAE:0.5895721434715478R2:0.12163690293641838在我们使用不同参数多次运行之后,我们可以转到我们的服务器,比较模型运行,并选择一个模型进行部署。 然后我们可以在概述表中比较不同模型运行,或者获取不同超参数的概述图,例如这样: 这个等高线图向我们展示了我们针对平均绝对误差(MAE)改变的两个超参数。 我们可以选择一个模型进行部署。我们可以看到我们最佳模型的运行ID。可以通过命令行将模型部署到服务器,例如像这样: mlflowmodelsserve-m/Users/ben/mlflow/examples/sklearn_elasticnet_wine/mlruns/1/208e2f5250814335977b265b328c5c49/artifacts/ElasticNet/我们可以将数据作为JSON传递,例如使用curl,同样是从终端。这可能看起来像这样: 将模型产品化的基本工作流程如下: 这通常导致一个通过JSON通信的微服务(通常这被称为RESTful服务)或GRPC(通过Google的协议缓冲区进行远程过程调用)。这具有将决策智能从后端分离出来,并让ML专家完全负责他们的解决方案的优势。 微服务是一个可以独立部署、维护和测试的单个服务。将应用程序结构化为一组松散耦合的微服务称为微服务架构。 另一种方法是将您的模型和粘合代码打包部署到公司现有企业后端中。此集成有几种替代方案: MLflow具有命令行、Python、R、Java和RESTAPI接口,用于将模型上传到模型库,记录模型结果(实验),再次下载以便在本地使用,控制服务器等等。它提供了一个服务器,还允许部署到AzureML、AmazonSagemaker、ApacheSparkUDF和RedisAI。 如果我们想要访问模型,我们需要从默认的后端存储(这是存储指标的地方)切换到数据库后端,并且我们必须使用URI中的协议定义我们的工件存储,例如对于本地mlruns/目录,使用file://$PWD/mlruns。我们已经启用了后端的SQLite数据库,这是最简单的方式(但可能不适合生产环境)。我们也可以选择MySQL、Postgres或其他数据库后端。 以下指标可以作为监控决策过程的一部分: 要了解检测异常值的方法,请参阅第三章中的发现异常配方,模式、异常值和推荐。 可以从头开始构建独立的监控,类似于本章中可视化模型结果的模板,或者与更专业的监控解决方案集成,如Prometheus、Grafana或Kibana(用于日志监控)。 这是一个非常广泛的话题,在本文档的工作原理……部分中提到了许多生产化方面。在ML和深度学习(DL)模型中有许多竞争激烈的工业级解决方案,考虑到空间限制,我们只能尝试给出一个概述。像往常一样,在本书中,我们主要集中于避免供应商锁定的开源解决方案: 虽然某些工具只支持一个或少数几个建模框架,但其他工具,特别是BentoML和MLflow,支持部署在所有主要ML训练框架下训练的模型。这两者提供了在Python中创建的任何东西的最大灵活性,并且它们都具有用于监控的跟踪功能。 其他工具包括以下内容: 此外,还有许多库可用于创建自定义微服务。其中最受欢迎的两个库是: 使用这些,您可以创建端点,该端点可以接收像图像或文本这样的数据,并返回预测结果。 对抗攻击在机器学习中指的是通过输入欺骗模型的行为。这种攻击的示例包括通过改变几个像素向图像添加扰动,从而导致分类器误分类样本,或者携带特定图案的T恤以逃避人物检测器(对抗T恤)。一种特定的对抗攻击是隐私攻击,其中黑客可以通过成员推理攻击和模型反演攻击获取模型的训练数据集知识,从而可能暴露个人或敏感信息。 在医疗或金融等领域,隐私攻击尤为危险,因为训练数据可能涉及敏感信息(例如健康状态),并且可能可以追溯到个人身份。在本配方中,我们将构建一个免受隐私攻击的模型,因此无法被黑客攻击。 我们将实现一个PyTorch模型,但我们将依赖由NicolasPapernot和其他人创建和维护的TensorFlow/privacy存储库中的脚本。我们将按以下步骤克隆存储库: 我们必须为教师模型和学生模型定义数据加载器。在我们的情况下,教师和学生架构相同。我们将训练教师,然后从教师响应的聚合训练学生。我们将最终进行隐私分析,执行来自隐私存储库的脚本。 请参阅第七章中的识别服装项目配方,简要讨论MNIST数据集,以及同一章节中的生成图像配方,用于使用该数据集的另一个模型。 请注意,我们将在整个配方中以临时方式定义一些参数。其中包括num_teachers和standard_deviation。您将在工作原理...部分看到算法的解释,希望那时这些参数会变得合理。 另一个参数,num_workers,定义了用于数据加载的子进程数。batch_size定义了每批加载的样本数。 我们将为教师定义数据加载器: num_teachers=100defget_data_loaders(train_data,num_teachers=10):teacher_loaders=[]data_size=len(train_data)//num_teachersforiinrange(num_teachers):indices=list(range(i*data_size,(i+1)*data_size))subset_data=Subset(train_data,indices)loader=torch.utils.data.DataLoader(subset_data,batch_size=batch_size,num_workers=num_workers)teacher_loaders.append(loader)returnteacher_loadersteacher_loaders=get_data_loaders(train_data,num_teachers)get_data_loaders()函数实现了一个简单的分区算法,返回给定教师所需的数据部分。每个教师模型将获取训练数据的不相交子集。 我们为学生定义一个训练集,包括9000个训练样本和1000个测试样本。这两个集合都来自教师的测试数据集,作为未标记的训练点-将使用教师的预测进行标记: importtorchfromtorch.utils.dataimportSubsetstudent_train_data=Subset(test_data,list(range(9000)))student_test_data=Subset(test_data,list(range(9000,10000)))student_train_loader=torch.utils.data.DataLoader(student_train_data,batch_size=batch_size,num_workers=num_workers)student_test_loader=torch.utils.data.DataLoader(student_test_data,batch_size=batch_size,num_workers=num_workers)importtorch.nnasnnimporttorch.nn.functionalasFimporttorch.optimasoptimclassNet(nn.Module):def__init__(self):super(Net,self).__init__()self.conv1=nn.Conv2d(1,10,kernel_size=5)self.conv2=nn.Conv2d(10,20,kernel_size=5)self.conv2_drop=nn.Dropout2d()self.fc1=nn.Linear(320,50)self.fc2=nn.Linear(50,10)defforward(self,x):x=F.relu(F.max_pool2d(self.conv1(x),2))x=F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)),2))x=x.view(-1,320)x=F.relu(self.fc1(x))x=F.dropout(x,training=self.training)x=self.fc2(x)returnF.log_softmax(x)这是用于图像处理的卷积神经网络。请参阅第七章,高级图像应用,了解更多图像处理模型。 让我们为预测创建另一个工具函数,给定一个dataloader: defpredict(model,dataloader):outputs=torch.zeros(0,dtype=torch.long).to(device)model.to(device)model.eval()forimages,labelsindataloader:images,labels=images.to(device),labels.to(device)output=model.forward(images)ps=torch.argmax(torch.exp(output),dim=1)outputs=torch.cat((outputs,ps))returnoutputs现在我们可以开始训练教师。 首先,我们将实现一个训练模型的函数: device=torch.device('cuda:0'iftorch.cuda.is_available()else'cpu')deftrain(model,trainloader,criterion,optimizer,epochs=10,print_every=120):model.to(device)steps=0running_loss=0foreinrange(epochs):model.train()forimages,labelsintrainloader:images,labels=images.to(device),labels.to(device)steps+=1optimizer.zero_grad()output=model.forward(images)loss=criterion(output,labels)loss.backward()optimizer.step()running_loss+=loss.item()现在我们准备训练我们的教师: fromtqdm.notebookimporttrangedeftrain_models(num_teachers):models=[]fortintrange(num_teachers):model=Net()criterion=nn.NLLLoss()optimizer=optim.Adam(model.parameters(),lr=0.003)train(model,teacher_loaders[t],criterion,optimizer)models.append(model)returnmodelsmodels=train_models(num_teachers)这将实例化并训练每个教师的模型。 对于学生,我们需要一个聚合函数。您可以在工作原理...部分看到聚合函数的解释: importnumpyasnpdefaggregated_teacher(models,data_loader,standard_deviation=1.0):preds=torch.torch.zeros((len(models),9000),dtype=torch.long)print('Runningteacherpredictions...')fori,modelinenumerate(models):results=predict(model,data_loader)preds[i]=resultsprint('Calculatingaggregates...')labels=np.zeros(preds.shape[1]).astype(int)fori,image_predsinenumerate(np.transpose(preds)):label_counts=np.bincount(image_preds,minlength=10).astype(float)label_counts+=np.random.normal(0,standard_deviation,len(label_counts))labels[i]=np.argmax(label_counts)returnpreds.numpy(),np.array(labels)standard_deviation=5.0teacher_models=modelspreds,student_labels=aggregated_teacher(teacher_models,student_train_loader,standard_deviation)aggregated_teacher()函数为所有教师做出预测,计数投票,并添加噪声。最后,它通过argmax返回投票和结果的聚合。 standard_deviation定义了噪声的标准差。这对隐私保证至关重要。 学生首先需要一个数据加载器: defstudent_loader(student_train_loader,labels):fori,(data,_)inenumerate(iter(student_train_loader)):yielddata,torch.from_numpy(labels[i*len(data):(i+1)*len(data)])这个学生数据加载器将被提供聚合的教师标签: student_model=Net()criterion=nn.NLLLoss()optimizer=optim.Adam(student_model.parameters(),lr=0.001)epochs=10student_model.to(device)steps=0running_loss=0foreinrange(epochs):student_model.train()train_loader=student_loader(student_train_loader,student_labels)forimages,labelsintrain_loader:images,labels=images.to(device),labels.to(device)steps+=1optimizer.zero_grad()output=student_model.forward(images)loss=criterion(output,labels)loss.backward()optimizer.step()running_loss+=loss.item()# 由于简洁起见,本代码中的某些部分已从训练循环中省略。验证如下所示: ifsteps%50==0:test_loss=0accuracy=0student_model.eval()withtorch.no_grad():forimages,labelsinstudent_test_loader:images,labels=images.to(device),labels.to(device)log_ps=student_model(images)test_loss+=criterion(log_ps,labels).item()ps=torch.exp(log_ps)top_p,top_class=ps.topk(1,dim=1)equals=top_class==labels.view(*top_class.shape)accuracy+=torch.mean(equals.type(torch.FloatTensor))student_model.train()print('TrainingLoss:{:.3f}..'.format(running_loss/len(student_train_loader)),'TestLoss:{:.3f}..'.format(test_loss/len(student_test_loader)),'TestAccuracy:{:.3f}'.format(accuracy/len(student_test_loader)))running_loss=0最终的训练更新如下: Epoch:10/10..TrainingLoss:0.026..TestLoss:0.190..TestAccuracy:0.952我们看到这是一个好模型:在测试数据集上准确率为95.2%。 他们提供了一个基于投票计数和噪声标准差的分析脚本。我们之前克隆了这个仓库,因此可以切换到其中一个目录,并执行分析脚本: %cdprivacy/research/pate_2018/ICLR2018我们需要将聚合后的教师计数保存为一个NumPy文件。然后可以通过分析脚本加载它: clean_votes=[]forimage_predsinnp.transpose(preds):label_counts=np.bincount(image_preds,minlength=10).astype(float)clean_votes.append(label_counts)clean_votes=np.array(counts)withopen('clean_votes.npy','wb')asfile_obj:np.save(file_obj,clean_votes)这将counts矩阵放在一起,并将其保存为文件。 最后,我们调用隐私分析脚本: 我们从数据集中创建了一组教师模型,然后从这些教师模型中引导出了一个能提供隐私保证的学生模型。在本节中,我们将讨论机器学习中隐私问题的一些背景,差分隐私,以及PATE的工作原理。 尽管几列的组合可能泄露特定个体的太多信息,例如,地址或邮政编码再加上年龄,对于试图追踪数据的人来说是一个线索,但是建立在这些数据集之上的机器学习模型也可能不安全。当遭受成员推断攻击和模型反演攻击等攻击时,机器学习模型可能会泄漏敏感信息。 成员攻击大致上是识别目标模型在训练输入上的预测与在未经训练的输入上的预测之间的差异。您可以从论文针对机器学习模型的成员推断攻击(RezaShokri等人,2016)了解更多信息。他们表明,Google等公司提供的现成模型可能容易受到这些攻击的威胁。 差分隐私(DP)机制可以防止模型反演和成员攻击。接下来的部分,我们将讨论差分隐私,然后是关于PATE的内容。 差分隐私的概念,最初由CynthiaDwork等人在2006年提出(在私有数据分析中校准噪声和灵敏度),是机器学习中隐私的金标准。它集中在个体数据点对算法决策的影响上。大致而言,这意味着,模型的任何输出都不会泄露是否包含了个人信息。在差分隐私中,数据会被某种分布的噪声干扰。这不仅可以提供防止隐私攻击的安全性,还可以减少过拟合的可能性。 在这个表述中,epsilon参数是乘法保证,delta参数是概率几乎完全准确结果的加法保证。这意味着个人由于其数据被使用而产生的差异隐私成本是最小的。Delta隐私可以被视为epsilon为0的子集或特殊情况,而epsilon隐私则是delta为0的情况。 这些保证是通过掩盖输入数据中的微小变化来实现的。例如,斯坦利·L·沃纳在1965年描述了这种掩盖的简单程序(随机化响应:消除逃避性答案偏差的调查技术)。调查中的受访者对敏感问题如你有过堕胎吗?以真实方式或根据硬币翻转决定: 这提供了合理的否认能力。 基于NicolasPapernot等人(2017)的论文来自私有训练数据的半监督知识转移,教师集合的私有聚合(PATE)技术依赖于教师的嘈杂聚合。2018年,NicolasPapernot等人(具有PATE的可扩展私有学习)改进了2017年的框架,提高了组合模型的准确性和隐私性。他们进一步展示了PATE框架在大规模、真实世界数据集中的适用性。 PATE训练遵循这个过程: 正如提到的,每个教师都在数据集的不相交子集上训练。直觉上,如果教师们就如何对新的输入样本进行分类达成一致意见,那么集体决策不会透露任何单个训练样本的信息。 直观上,准确性随噪声的方差增加而降低,因此方差必须选择足够紧密以提供良好的性能,但又足够宽以保护隐私。 ε值取决于聚合,特别是噪声水平,还取决于数据集及其维度的上下文。请参阅JaewooLee和ChrisClifton的《多少是足够的?选择差分隐私的敏感性参数》(2011年),以进行讨论。