为我的定制笔记本服务器构建单元数据格式
Jockey.jl
在 Jockey.jl 中为我的单元格构建新的数据格式
介绍
我最近参与的一个相当酷的项目,让我非常兴奋的是新的 Jockey 笔记本编辑器。该编辑器可用于处理整个技术堆栈范围内的笔记本电脑,并计划提供各种功能,远远超出标准单元环境。有几个组件需要为后端构建,今天我将为我们的单元编辑器制作后端。这也将与我之前编程的骑师会议和配置设置相关联,因此如果您想阅读我编程所有这些内容的文章,您可以在此处进行:
另外,这里有一个 Github 上 Jockey 的链接。Github 可能会让您对这个项目的构建方向、包组成和目标有更好的了解。
https://github.com/ChifiSource/Jockey.jl
今天,我们正在为后端会话制作一个关键组件,它将成为一种基础数据格式,我们将把它加载到文件中,并在将来使用它。还会有一篇后续文章介绍我如何为这些数据创建保存/加载算法。
参考点
为了确保这种格式正是我想要的,我将参考 Pluto.jl 文件。这是因为我认为将它作为第一种读/写文件格式是最有意义的。将来,我可能会编写一个算法,通过。IPYNB 文件,但现在是常规的。Pluto 笔记本自带的 jl Julia 格式可能是最佳选择。记住这一点,在下一篇文章中,我将创建一个阅读器来将这些类型的文件解析到我的新结构中。本文将只讨论如何创建结构。
所以让我们快速创建一个新环境,放一个冥王星笔记本进去。
([@v1](http://twitter.com/v1).6) pkg> activate PLTO
Activating new environment at `~/dev/nbs/PLTO/Project.toml`(PLTO) pkg> add Pluto
Updating registry at `~/.julia/registries/General`
我将我的环境命名为 PLTO,我们真正需要添加的是 Pluto.jl。我们的目标只是获得一个. jl 文件,其中包含一些可以读取的特性。我对 Pluto 如何处理保存 Julia 文件有一些痛苦的怀疑,让我们看看它可能是下面的哪一个(或者可能是别的什么东西)。
- 文档字符串?
- 评论?
- 真正的朱利安类型?
让我们开始一个快速的笔记本会话,看看!
julia> using Plutojulia>julia> Pluto.run()
当我写这篇文章并启动这台笔记本服务器时,有一件事我肯定会注意到——服务器慢得令人难以置信。这不是执行,这只是在不同的端点之间导航,尝试得到一个示例笔记本,我注意到页面加载需要多长时间。对我来说,我讨厌等待加载东西——这就是为什么我编写加载时间更短的东西,我可以肯定地告诉你,jl 会比这更快。我跑题了,因为谢天谢地,我现在把它做成笔记本了!
(图片由作者提供)
另一个注意是,我认为管理文件的 Pluto.jl 很奇怪。很奇怪,我不喜欢。最终,我觉得在某些方面,很多焦点从来没有传递到这样的事情上,我想要一个 GUI 或终端中的目录浏览器,而不是一个放 URI 的文本框。现在应该注意的是,这个方法没有障碍,默认情况下目录实际上是 Julia 的目录(服务器的目录),我仍然认为可以有一个更好的方法,比如 IJulia/Jupyter 如何处理它——这只是一个例子。
现在,我可以关闭这个会话及其合作服务器,我们可以继续了解这样做的全部目的,也就是更好地了解 Pluto 文件以及代码和 markdown。
(图片由作者提供)
哦,我的天啊—
看来正确的答案是答案 3:这些细胞使用朱莉娅类型的一切。我认为这可能会面临一些问题。首先也是最重要的一点,很明显,UUID 在评论中被用于某种阅读算法。解析器使用注释来跟踪 UUID,以及那些单元格内的内容。对于我的实际格式,我想改变这种方法的很多东西。另一件我觉得奇怪的事情是,输出细胞也有 UUID 和各自的朱莉娅代码。
当然,我有能力简单地匹配这种格式,但我认为这最终弊大于利。我的计划更像是将数据保存到文本中,然后加载数据的典型路线——不需要 UUID,只需要代码。问题是我想让我的单元格以这种数据格式可读——尽管这不是我最喜欢的。或者,我也可以创建自己的程序来使用它,但是考虑到这两种类型的笔记本都使用。jl 扩展名,我们可能希望寻找某种标识符来区分这两个文件。
回到骑师
对这种格式做了一些研究后,我们准备对 Jockey.jl 包本身做一些工作。我要做的第一件事是为新文件中的单元添加一个新的代码文件,我将把这个文件称为“Cells.jl”。鉴于这个组件是 Jockey 会话的一部分,我们将把它包含在相应的目录中。之后,我将创建另一个名为“Formats.jl”的新文件
(PLTO) pkg> activate Jockey
Activating environment at `~/dev/Jockey/Project.toml`shell> cd Jockey
/home/emmac/dev/Jockeyshell> cd src
/home/emmac/dev/Jockey/srcshell> ls
Jockey.jl ServerControllershell> touch Formats.jlshell> touch ServerController/Cells.jl
目前,我们不打算使用 Formats.jl,这将在未来的文章和提交中迭代。在这段代码中,我们要做的是编写新的单元结构,这实际上会有点复杂。因为我们要保存并加载到 Pluto.jl 格式中,所以我们要跟踪 UUID 的。也就是说,在我们创建这个小的附加物之前,让我们添加依赖关系:
(Jockey) pkg> add UUIDs
Resolving package versions...
Updating `~/dev/Jockey/Project.toml`
[cf7118a7] + UUIDs
No Changes to `~/dev/Jockey/Manifest.toml`
在新的 Cells.jl 文件中,我要做的第一件事是为模块的这一部分定义类型层次的顶层。让我们继续这样做:
abstract type Cell end
现在我们将为这个类型添加两个子类型,分别是输入和输出单元格。
abstract type Cell end
mutable struct InputCellendmutable struct OutputCellend
我们可能需要在这些单元格中添加一些数据。我要添加第一个东西是 UUID。添加了这些基本 ID 后,我将把注意力转向输入单元格。我想给这个单元格添加一些不同的东西,最好一次只关注一个构造函数。
mutable struct InputCell
ID::
end
我必须承认,我相信类型将是 UUID,但我们可能会测试它或在添加它之前在文档中查找它。我最终决定,至少对于这个迭代,我应该有以下内容:
abstract type AbstractCell end
mutable struct InputCell <: AbstractCell
ID::UUID
Text::String
env::Dict
hidden::bool
end
注意这只是针对输入,不是针对输出。输出非常简单,只包含一个 ID 和一些输出:
mutable struct OutputCell <: AbstractCell
ID::UUID
output::String
end
最终,无论选择什么,输出都是 HTML。我认为这是给定的,所以输出单元格在这个例子中并没有太大的关系。最后,我将创建顶层单元格。还需要注意的是,我将抽象类型 Cell 更改为抽象 Cell。然后将这些细胞装入更大的细胞类型中,这将非常方便地容纳它们。
using UUIDs, TOMLabstract type AbstractCell end
mutable struct InputCell <: AbstractCell
ID::UUID
Text::String
env::Dict
endmutable struct OutputCell <: AbstractCell
ID::UUID
output::String
endmutable struct Cell <: AbstractCell
input::Cell
output::Cell
hidden::bool
end
所以有一些功能需要添加到这个单元中… Jockey.jl 意味着有一些非常好的环境跟踪,我也将在这里添加一些这样的空实现。以下是我得出的这些类型的最终版本:
using UUIDs, TOMLabstract type AbstractCell end
mutable struct InputCell <: AbstractCell
ID::UUID
Text::String
env::Dict
endmutable struct OutputCell <: AbstractCell
ID::UUID
output::String
endmutable struct Cell <: AbstractCell
input::Cell
output::Cell
hidden::bool
lang::Symbol
env::UUID
end
结论
还有一件事我们必须要做,那就是将这一点纳入到一揽子计划的其余部分。一些简单的代码,但是这些新的类型将成为我们 Jockey.jl 会议如何处理代码的基础。我们需要修改的代码与我们上次使用的代码相同。我们正在寻找的文件被称为 Session.jl。这将是在这个类型之上的另一个类型,它将控制单元格。我们只是将单元格添加到那个会话对象中,作为一个字典,这样我们就可以按正确的顺序排列单元格。
include("Cells.jl")
mutable struct Identifier
id::Int64
auth::Int64
username::Int64
status::Int64
end
mutable struct JSession
ID::Identifier
KeyBindings::Dict
Extensions::Dict
username::String
Cells::Dict
end
非常感谢您的阅读!我希望这个项目对我的文章来说是有趣的,也许这给了我一个坚实的视角来看这个东西是如何开发的,以及我在这样的开发过程中所看到的。我真的很感谢你的阅读,并希望在这个项目的下一部分看到你!
用 Rasa 构建聊天机器人
入门指南
作者图片
聊天机器人是模拟人类对话的程序。这些机器人可以是简单的基于规则的聊天机器人,用户只能点击按钮或机器人提供的建议回复,也可以是成熟的机器人,可以处理上下文、聊天和其他复杂的事情,这些在人类对话中非常常见。
从数量上来说,对话式人工智能有 5 个层次,从简单的基于规则的系统到真正自适应的聊天机器人,它们可以处理复杂的场景,比如当用户甚至不确定他们想要什么时,可以根据他们想要的详细程度来回复用户。
**Table of Contents**
- Rasa
- Intents, Entities, Slots and Responses
- Setting up Rasa
- Creating training data
- Building our bot -- training and testing
罗砂
Rasa 是一个开源框架,用于构建基于文本和语音的聊天机器人。它在对话式人工智能的第 3 层工作,机器人可以理解上下文。第三级对话代理可以处理诸如用户改变主意、处理上下文甚至意外查询之类的事情。
如果你想建立一个聊天机器人,Rasa 不是唯一可用的工具,但它是最好的工具之一。还有其他几个,像 DialogFlow,虽然我们不会在这篇文章中讨论它们。
意图、实体、位置和响应
这些是任何聊天机器人对话的主要组成部分。我们将通过一个示例对话来更好地理解这些术语。假设我们正在构建一个收集用户联系信息的机器人。
意图
用户所暗示的被称为intent
。例如,如果用户说出以下内容之一来开始聊天:
- 早上好!
- 嗨!
- 嘿!
- 你好。
用户本质上是在说问候。因此,我们可以将这些组合成一个称为greet
的单一意图。现在,每当机器人收到类似于greet
中其他短语的用户消息,机器人就会将其归类为属于greet
意图。
意图示例—作者提供的图片
**user**: Hi. (bot classifies this message as "greet")
实体
实体是可以从用户消息中提取的数据片段。
继续我们的例子,在用户问候机器人之后,机器人询问他们的联系信息。大概是这样的:
**user**: Hi. (bot classifies this message as "greet")
**bot**: Hello! Could you please provide your contact information?
然后,用户将输入他们的详细信息,如他们的姓名和电子邮件 id。
**user**: Sure. It's John Doe. My email is johndoe@email.com.
上面的消息包含两条信息——姓名和电子邮件。这些可以被提取为实体。你的机器人将根据你训练数据的质量提取它们。
假设您已经为姓名和电子邮件定义了两个实体。这是机器人将提取的内容。
**name**: John Doe
**email**: johndoe@email.com
这里的一个重要问题是:机器人如何知道要提取什么?用户可以潜在地输入信息的任意组合。所有这些都是有效的用户输入:
- 我叫约翰·多伊。电子邮件:johndoe@gmail.com
- 姓名:无名氏电子邮件:johndoe@gmail.com
- 是啊,当然。我是无名氏。我的电子邮件是 johndoe@gmail.com。
- 约翰·多伊,johndoe@gmail.com
人类可以很容易地提取姓名和电子邮件 id。但是聊天机器人是怎么做到的呢?
这个问题的答案在于我们的训练数据有多好,我们稍后会谈到这一点。
时间
插槽是机器人的记忆。任何需要在整个对话中保持的信息,比如用户的名字或者他们的目的地(如果你正在构建一个机票预订机器人),都应该存储为 slots。
由于我们已经有了两个实体(姓名和电子邮件),我们可以创建具有相同名称的槽,因此当提取姓名或电子邮件 id 时,它们会自动存储在各自的槽中。这是因为默认情况下有两个属性是True
。
auto_fill
store_entities_as_slots
我们将如何设置这些位置,我们将在稍后讨论。
反应
响应是机器人对用户说的话。自然,对于一个机器人来说,要给出一个适当的响应,它必须弄清楚用户想要说什么。
在我们的例子中,当用户向机器人打招呼时,它理解消息本身,并通过问好和询问他们的联系方式做出适当的响应。
继续我们的示例对话,在机器人提供他们的信息后,假设我们对机器人进行编程,让它在用户提供他们的详细信息后说“谢谢”。
**user:** Hi. (bot classifies this message as "greet")
**bot**: Hello! Could you please provide your contact information?
**user**: Sure. It's John. My email is john@email.com.
**bot**: Thanks John for the info!
一些先决条件
- 使用
pip install rasa==2.6.2
安装 Rasa 开源
你可以安装最新的版本,但是这篇文章是基于 v2.6.2 的,所以任何 v2.x 都应该可以很好地适应这里介绍的内容。
2.运行rasa init
来设置演示项目。
创建培训数据
回到我们的问题,如何创建能够以多种形式提取有用信息的机器人。
这对我们来说很容易,因为我们得到了自然语言的语义,但聊天机器人做不到这一点。机器人不知道“电子邮件”是什么。它也不知道约翰可能是一个名字,电子邮件包含“@”符号。
我们的目标是提供各种数据,以便机器人能够理解单词之间的一些关联,比如单词“email”后面的内容可能是电子邮件 id,或者形式为<some_text>@<some_text>.com
的单词可能是电子邮件。
当然,不可能涵盖所有场景,但我们可以教会聊天机器人最常见的场景,确保我们添加了通用的单词和短语,这些单词和短语可能代表了机器人可能看到的很大一部分信息。
正如你可能猜到的,这更多的是一个迭代过程,我们评估我们的机器人在现实世界中的性能,并使用它来提高其性能。
密码
我们会一个文件一个文件的来,这样更容易理解。出于本文的目的,我们将讨论三个文件:
nlu.yml
domain.yml
stories.yml
NLU 培训数据
为了supply_contact_info
的意图,我们来处理下面的案例。
- 我叫约翰·多伊。电子邮件:johndoe@gmail.com
- 姓名:无名氏电子邮件:johndoe@gmail.com
- 是啊,当然。我是无名氏。我的电子邮件是 johndoe@gmail.com。
- 约翰·多伊,johndoe@gmail.com
- 当然可以。是无名氏。我的电子邮件是 johndoe@email.com。
这个训练数据被称为 NLU 数据,它包含了我们期望从用户那里得到的短语和对话(intents
)。请注意,这不包括机器人的响应或我们的对话流程。这将存在于我们项目的一个单独的部分。
YAML 文件用于此目的。
添加意图— nlu.yml
接下来,我们将为这个意图命名。姑且称之为supply_contact_info
。
用这个替换nlu.yml
的内容。改变名称和电子邮件 id 本身是一个好主意,这样机器人可以更好地概括。
version: "2.0"
nlu:- intent: supply_contact_info
examples: |
- My name is John. email's john@gmail.com
- name: David email: david@email.com
- Yeah sure. I’m Barbara. My email is barbara@email.com.
- Susan, susan@email.com
- Sure. It's Fred. My email is fred@email.com.
version
键是指 rasa 支持的训练数据格式。所有2.x
版本的 Rasa 都支持2.0
。
标记实体 nlu.yml
上面的 NLU 数据会让机器人知道用户可以说什么。但是我们仍然没有标记任何entities
,这是一个快速提醒,是机器人应该收集的关键信息。
首先,让我们定义一些实体。让我们创造性地将表示客户名称的实体称为name
。类似地,对于电子邮件,我们将使用一个名为email
的实体。
标记实体的语法是[entity_value](entity_name)
。所以对于这条线:
My name is John. email's john@email.com
我们把它写成:
My name is [John](name). email's [john@email.com](email)
标记实体-按作者分类的图像
这样,我们最终得到了用于supply_contact_info
目的的最终 NLU 数据。
version: "2.0"
nlu:- intent: supply_contact_info
examples: |
- My name is [John](name). email's [john@email.com](email)
- name: [David](name) email: [david@email.com](email)
- Yeah sure. I'm [Barbara](name). My email is [barbara@email.com](email)
- [Susan](name), [susan@email.com](email)
- Sure. It's [Fred](name). My email is [fred@email.com](email).
我们还有另一个意图,叫做greet
意图,当用户开始对话并说“嗨”或“你好”之类的话时。最后, **nlu.yml**
应该是这个样子。
nlu:- intent: greet
examples: |
- hi
- hello- intent: supply_contact_info
examples: |
- My name is [John](name). email's [john@email.com](email)
- name: [David](name) email: [david@email.com](email)
- Yeah sure. I'm [Barbara](name). My email is [barbara@email.com](email)
- [Susan](name), [susan@email.com](email)
- Sure. It's [Fred](name). My email is [fred@email.com](email).
添加插槽— domain.yml
现在我们有了意图和实体,我们可以添加我们的插槽。
头到文件domain.yml
。用这个替换slots
键的内容。
slots:
name:
type: text
email:
type: text
因为姓名和电子邮件都是字符串,所以我们将类型设置为text
。
添加响应 domain.yml
就像我们想要抽象出用户想说什么一样,我们也有反应来代表机器人会说什么。
简单的响应是基于文本的,尽管 Rasa 允许您添加更复杂的功能,如按钮、备选响应、特定于频道的响应,甚至自定义操作,我们将在后面介绍。
在我们的例子中,在用户问候机器人(intent: greet
)之后,机器人向用户询问他们的联系信息。这可以表示为机器人响应或发声。按照惯例,我们在每个机器人发声前添加一个“绝对”前缀——就像这样:
utter_ask_for_contact_info
用我们的响应替换domain.yml
中responses
键的内容。
responses:utter_ask_for_contact_info:
- text: Hello! Could you please provide your contact information?
类似地,我们可以在用户提供他们的信息后添加响应。姑且称之为:utter_acknowledge_provided_info
(粗体,如下)。
responses:utter_ask_for_contact_info:
- text: Hello! Could you please provide your contact information?**utter_acknowledge_provided_info****:
- text: Thanks for provided your info!**
我们可以通过在确认信息中提到用户的名字来让用户体验稍微好一点,比如“谢谢 John 提供的信息!”
为此,我们将修改上面的utter_acknowledge_provided_info
,为name
插槽添加一个占位符,如下所示:
utter_acknowledge_provided_info:
- text: Thanks {name} for provided your info!
对话示例—作者提供的图片
故事
故事让我们的机器人知道对话应该如何进行。回到我们的例子,机器人向用户询问他们的联系方式,对话是这样的:
**user:** Hi. (bot classifies this message as "greet")
**bot**: Hello! Could you please provide your contact information?
**user**: Sure. It's John. My email is john@email.com.
**bot**: Thanks John for the info!
这可以分解为如下的意图和回应:
intent: greet
action: utter_ask_for_contact_info
intent: supply_contact_info
action: utter_acknowledge_provided_info
添加故事— stories.yml
让我们把这个变成 rasa 能理解的语法。用这里将要讨论的内容替换文件stories.yml
的内容。让我们称这个故事为“用户提供客户信息”。
故事名称不会影响机器人的工作方式。
version: "2.0"stories:
- story: user supplies customer info
steps:
- intent: greet
- action: utter_ask_for_contact_info
- intent: supply_contact_info
- action: utter_acknowledge_provided_info
但是还有一件事。
我们还必须指出用户意图可能提供的实体,这样机器人就更容易知道如何响应。
在意图下,只需列出可能出现在该意图中的实体。
version: "2.0"stories:
- story: user supplies customer info
steps:
- intent: greet
- action: utter_ask_for_contact_info
**- intent: supply_contact_info
entities:
- name
- email** - action: utter_acknowledge_provided_info
建造我们的机器人
现在我们已经准备好了数据和故事,我们必须遵循一些步骤来让我们的机器人运行起来。
代码设置
我们对演示机器人中的文件做了一些修改。让我们回顾一下。
**nlu.yml**
—保存我们模型的 NLU 训练数据
在这个文件中,我们为我们的两个目的保留标记数据:
supply_contact_info
greet
**stories.yml**
—让机器人知道对话应该如何进行
我们的单story
就设在这里。
**domain.yml**
—所有意图、响应和实体的完整信息
这个文件比其他两个文件稍微复杂一点。如果你看看演示机器人(post running rasa init
)提供的domain.yml
文件,你会注意到intents
、actions
、responses
、entities
等键。
我们已经在我们的domain.yml
中添加了slots
和responses
。我们现在需要做的就是提到我们的intents
和entities
。最后,该文件将如下所示:
version: '2.0'intents:
- greet
- supply_contact_infoentities:
- name
- emailslots:
name:
type: text
email:
type: textresponses:
utter_ask_for_contact_info:
- text: Hello! Could you please provide your contact information?
utter_acknowledge_provided_info:
- text: Thanks {name}, for the info!
训练机器人
验证数据
在训练机器人之前,一个好的做法是检查故事和规则中的任何不一致,尽管在这么简单的项目中,这不太可能发生。
$ rasa data validate
截断的输出如下所示。没有发现冲突,所以我们可以很好地训练我们的模型。
The configuration for policies and pipeline was chosen automatically. It was written into the config file at 'config.yml'.
2021-09-12 18:36:07 INFO rasa.validator - Validating intents...
2021-09-12 18:36:07 INFO rasa.validator - Validating uniqueness of intents and stories...
...
2021-09-12 18:36:08 INFO rasa.validator - No story structure conflicts found.
注意:您可能会收到以下警告:
UserWarning: model_confidence 设置为“softmax”。建议尝试使用“model_confidence=linear_norm”来更容易地调整回退阈值。
这只是一个建议,可以忽略。
培训
为了训练机器人,我们只需使用rasa train
命令。为了更好地组织,我们将为模型提供一个名称,但这不是必需的。
$ rasa train --fixed-model-name contact_bot
这个的输出要大得多,所以我不会在这里展示。
注意:您可能会收到以下警告:
用户警告:在您的管道中找到基于规则的策略,但没有基于规则的培训数据。请将基于规则的案例添加到您的培训数据中,或者从您的 pipeline 中删除基于规则的策略(
RulePolicy
)。我们不会在这篇文章中讨论规则,但它们本质上就是它们听起来的样子。它们需要一个称为 RulePolicy 的东西,默认情况下,它会添加到您的 bot 管道中。暂时可以忽略。这将在以后的帖子中讨论。
和我们的机器人聊天
与我们的新机器人聊天很简单。打开一个新的终端窗口并启动一个 rasa shell。
$ rasa shell
这将让你在终端上与你的机器人聊天。但是如果你想清理 UI 和更多的信息,比如识别了什么意图,提取了什么实体,你可以使用 Rasa X.
关于 Rasa X 有一点很简单:它可以让你测试你的机器人,修复机器人的错误响应,更重要的是,你可以和其他人分享你的机器人,以更好地了解它在现实世界中的表现。
要使用它,请在本地模式下安装它。
$ pip3 install rasa-x --extra-index-url https://pypi.rasa.com/simple
然后,运行它。
$ rasa x
前往左边菜单中的“与你的机器人交谈”,开始与你的机器人交谈。
下面,你可以看到我们的机器人表现如何。
在 Rasa X 中与我们的机器人对话——图片由作者提供
注意
在上面的对话中,除了对话,你会注意到一些灰色的信息。在每条用户消息下面,您可以看到用户消息的意图和可信度,以及提取了哪些实体。
在每条 bot 消息的上方,您可以看到 bot 以什么样的信心决定采取什么样的行动,以及设置了什么样的槽。
action_listen
是一个内置动作,意味着聊天机器人需要用户输入。
示例代码和资源
本文中使用的所有代码都可以在以下位置获得:
https://github.com/Polaris000/BlogCode/tree/main/RasaChatbot
链接
最后
Rasa 是对话式人工智能的一个伟大工具。它的灵活性和定制深度使它成为一个很好的工具选择。在这篇文章中,我们制作的机器人非常简单。
有很多东西我无法在一篇文章中描述,比如动作、表单、规则、正则表达式、同义词、交互式学习、配置文件、管道等等。但是这篇文章中的内容应该足以让你开始。
我们将在以后的文章中介绍其余的内容。我会在这里添加所有未来帖子的链接。希望有帮助!
该系列的下一部分
第二部分:槽和实体一样吗?
第三部分:处理聊天机器人故障
第四部分:聊天机器人如何理解?
更新
20.3.2022
向该系列的其余部分添加链接
构建一个从经验中学习的国际象棋人工智能
不需要数据!
拉胡尔·帕博卢在 Unsplash 上拍摄的照片
在像国际象棋这样的规则定义的环境中应用机器学习技术已经成为掌握像国际象棋或围棋这样的策略游戏的稳定手段。这些技术通常涉及使用某种数据来训练模型,以做出最佳举措。
作为一个挑战,我想尝试创建一个完全不需要任何数据的引擎。尽管这会减慢模型的训练过程,但它会解决基于数据的模型所面临的一个关键问题。
模型的好坏取决于它的数据。在这种情况下,根据数据训练的象棋模型只能和数据源一样好。通过从方程中移除数据,算法的理论上限将被移除,这将给予模型(理论上)无限的潜力。
概念:
我打算怎么做?这可以通过结合我以前用过的两种技术来实现。该模型将由两部分组成:一个支配性的遗传算法和一个用于遗传算法中每个主体的蒙特卡罗搜索树。
遗传算法:
遗传算法将由一定数量的代理组成。代理人将相互竞争,获胜者将获得一定数量的分数。
前 20%的玩家将被保留,而其余玩家的数据将被删除。前 20%的玩家之间的权重会发生交叉,从而产生一组新的玩家。这应该有希望产生越来越好的球员。
将遗传算法用于该项目的原因有两个:遗传算法可以在没有数据的情况下进行优化,并且还具有非常灵活的适应度函数。该适应度函数可以被改变以微调模型的结果,并且它还可以涉及抽象的非数值运算。
蒙特卡罗搜索树:
蒙特卡洛搜索树用于进行每一步棋:引擎从给定的位置随机进行一系列的移动。最终位置然后由神经网络评估,其返回单个值。这个过程重复一定次数。汇总每次迭代的所有评估将导致位置分析。从初始位置开始的每一次合法移动都要重复这个过程。
然后将进行具有最高评估分数的移动。理论上,随着评估模型的改进,这些举措将是最优的。
当使用蒙特卡罗树时,需要在灵活性和速度之间进行权衡。理论上,可以使用单个深度神经网络直接评估电路板。然而,这个模型将更加难以优化,因为它需要自己“学习”许多概念,这对于遗传算法来说尤其缓慢。
蒙特卡洛搜索树是一种具有内置深度概念的神经网络,允许训练更快。
实施:
我提供的代码只是片段,肯定会出错。该项目的完整代码可以在我的 Github 上的链接中找到。
1.健身功能:
这是用于评估每个代理的适应度的适应度函数。
最初,我想让每个代理扮演每个其他代理,但发现这将花费太长时间。一些快速的计算让我得出结论,用那个设置,我需要 400 多个小时才能得到像样的结果。
原始算法的一个关键变化是在评估函数中。Model(input)比 model.predict(input)快 106 倍,每代节省 9 个多小时。
通过让每个代理只玩一个其他代理,它将减少每一代(p+1)/2 倍(p 是玩家的数量)所用的时间。
为了简单起见,我只是将代理的当前适应度乘以某个值来改变他们的适应度。如果我决定做一个 Elo 系统,这将会很有趣,并且可能会产生更好的结果。然而,我决定反对它,因为实现它会降低算法的速度。
2.评估网络
评估网络是每个代理背后的大脑。左边的代码展示了两种模型架构:
complex_eval 定义了一个复数卷积模型,常用于 Pix2Pix GAN 的编码部分。然而,这种复杂性使其难以部署,因为进行预测需要太多时间。
simple_eval 是一个基本的概念验证模型,它需要大约 0.004 秒来进行预测。低数量的权重也将导致更快的收敛。
模型的输入形状是(8,8,12),因为棋盘是 8 乘 8,并且有 12 个可能的棋子(每边 6 个)
3.遗传算法
遗传算法的大脑是适应度函数,心脏是交叉和变异函数。
交叉函数很容易理解,但实现起来有点困难。
从人口的前 20%中随机选择两个父母代理。这两个父代理的基因被合并以形成子代理。以下是对的逐步解释
- 它们的权重是扁平的,因此值可以改变。
- 找到一个随机的交点。这个点是父母一方的遗传信息结束的地方,也是父母一方的遗传信息开始的地方。
- 父母的基因结合在一起,然后一个新的子代理拥有这个操作产生的权重。
这有望让好父母的优良品质传递给他们的后代。
变异过程非常相似:随机代理的权重变平,其中一个权重变为随机值。实施遗传算法的突变,以便最终形成不是在第一种群中随机产生的有利性状。
结论:
这个项目非常有趣:没有算法理论限制的成本是由成功训练算法所花费的时间来支付的。
我的想法的实现并不完美,只是我未来工作的一个框架。
以下是可以改善/改变它的一些事情:
1.更改健身功能:
即使蒙特卡罗搜索树是最快的移动,最现实的搜索算法是找到哪一步移动结束时对手的平均评价最低。
2.改变评估模式:
尽管这看起来很容易,但它必须足够快,以允许执行时间存在于可能性的范围内
3.使用并行:
对于足够强大的 CPU,并行性(同时运行脚本)可以大大加快处理速度。如果这个问题得到解决,项目的限制将被移除,从而允许更好的结果。
我的链接:
如果你想看更多我的内容,点击这个 链接 。
构建象棋引擎:第 2 部分
结合 AlphaGo 和变形金刚学习象棋
大家好,这将是我构建象棋引擎系列教程的第二部分。这一课将着重于建立一个我们可以玩的人工智能代理。这一课比第一部分更专业,所以请耐心听我说。我试图提供方程式和图表来帮助使事情变得简单一点。
既然我们已经完成了构建我们的国际象棋游戏,我们可以开始设计一个玩它的人工智能了。国际象棋人工智能已经成功很多年了,从深蓝到 stockfish 。然而,有一个新的程序已经成为使用深度学习的最先进的技术。 AlphaZero 是一个由 DeepMind 开发的强化学习代理,它已经在众多游戏(象棋、松木、围棋)中取得了超人级别的结果。
由于这一成功,我们将建立在 AlphaZero 的基础上,并使用其强大的思想来指导具有深度学习模型的蒙特卡罗树搜索 (MCTS)算法。我们将使用一个转换器,而不是像他们论文中使用的那样使用卷积神经网络 (CNN)。变形金刚最近在自然语言处理(NLP)和计算机视觉(CV)等许多领域取得了巨大成功。因此,我们可以尝试在这里使用一个。
蒙特卡洛树搜索(MCTS)
在所有的游戏中,都有一系列的动作。在博弈论中,在博弈树中表示这些动作是很常见的。
游戏树示例|作者图片
在简单的游戏中,游戏树很小,可以被暴力搜索。对于小博弈树,我们可以使用贪婪算法,比如" MiniMax "搜索算法。大多数游戏都不简单,尽管这导致暴力方法在现代计算极限下被认为是不可行的。正因为如此,像 MCTS 这样的其他搜索算法被证明是更有益的。
MCTS 首先检查当前状态是否是叶节点(游戏结束)。如果当前状态是叶节点,搜索算法返回游戏结果。
游戏结果|作者图片
- sw =白色胜利(0 或 1)
- st =平局(0 或 1)
- sb =黑色胜利(0 或 1)
如果动作不是叶节点,并且这是第一次看到它,则使用神经网络(NN)来确定当前状态的值(v)和所有可能动作的概率§。当前状态节点奖励值是新预测的值。使用预测的概率分布来更新所有可能的有效动作的概率。我们返回要更新的先前节点的值(v)和概率§。
节点反向传播示例|作者图片
如果我们以前见过当前状态,我们递归地搜索具有最高 UCB 的动作。
UCB 是一个试图平衡开发和探索的强大公式。等式的前半部分利用了人工智能的游戏知识。后半部分通过将超参数©乘以概率§来增强探索。
置信上限(UCB) |作者图片
- v =结束游戏值
- c =探索超参数[0–1]
- p =行动概率
- N =父节点访问
- n =行动节点访问
当搜索游戏树时,如果当前深度等于最大允许深度,则返回当前深度处的值(v)和概率(p ),以更新之前的节点。
MCTS 流程图|作者图片
模型架构
了解了 MCTS 之后,让我们来谈谈我们的模型。我们的模型将获得游戏状态作为输入,并返回预期的游戏结果以及动作概率分布。
模型公式|作者图片
- vs =基于当前状态的游戏结果的概率分布
- psa =当前状态下所有行为的概率分布
我们模型的主要组件是变压器。变压器最近被谈论了很多,并越来越多地出现在最先进的系统中。通过使用注意力机制(多头比例点积注意力),变形金刚非常有效。
注意力是一个强大的等式,允许模型学习哪些事情是重要的,值得更多的关注。它通过获取一个查询和一个关键字的比例点积来确定两者的相似性。点积通过找出输入之间的角度来确定相似性。然后 Softmax 在 0-1 之间缩放结果。对结果和值进行点积,以确定两者的相似性。第二个点积有另一个有用的特性,有助于输出保持其初始形状。
缩放的点积注意力|作者图片
- Q = Q(E) =查询(向量/矩阵)
- K = K(E) = key(向量/矩阵)
- V = V(E) =值(向量/矩阵)
- E =嵌入
多头注意力将输入分成多个头,并行处理不同的组块。人们认为多头注意力通过创建不同的子空间来获得其优势。然后将每个头的结果连接在一起,并传递到一个线性层,该层将输入映射回其原始维度。
多头缩放点积关注|图片来自https://arxiv.org/abs/1706.03762
普通变压器具有简单的编码器和解码器架构。
变压器架构(编码器-解码器)|图片来自 https://arxiv.org/abs/1706.03762
然而,我们不打算使用这种架构,而是打算使用一个仅支持解码器的转换器。一个只有解码器的变压器看起来就像“注意是你需要的全部”论文中的变压器架构图中的编码器。当您比较上面的编码器模块和解码器模块时,您会发现只有一个区别,即编码器中缺少编码器-解码器关注层。在一个只有解码器的转换器中不需要这个编码器-解码器关注层,因为没有编码器,这意味着我们可以利用编码器块来创建一个只有解码器的模型。
纯解码器变压器架构|图片来自https://arxiv.org/abs/1706.03762
在变压器模块之后,我们模型的其余部分相当简单。我们添加一个线性层来映射我们的转换器层的输出到我们需要的维度(1D)。
线性图层|作者提供的图像
- x =输入(向量/矩阵)
- A =层权重(矢量/矩阵)
- b =层偏差(矢量/矩阵)
该模型然后分叉成两个平行的线性层(v,p)。这些线性图层用于将每个输出映射到所需的大小。其中一层将是游戏结果的大小(v),而另一层是动作状态的大小§。在线性层之后,softmax 独立应用于两个输出,以将其结果转换为概率分布。
Softmax |作者图片
- x =输入(向量/矩阵)
输入编码
既然我们知道了我们正在使用的模型架构,我们可能应该讨论一下输入数据。该模型将接收游戏的状态作为输入,但不能直接处理它。对于要处理的游戏状态,需要对其进行编码。为此,我们将通过标记化的方法使用嵌入。这些嵌入是代表每一部分的唯一向量。嵌入允许我们的模型获得每个游戏片段之间的相关性的强表示。
符号化嵌入|作者图像
转换器模型并行处理输入序列,导致它们丢弃位置信息。幸运的是,我们可以在输入中添加一个位置编码来帮助恢复这些附加信息。位置编码允许模型根据其在输入序列中的位置从每个嵌入中提取额外的信息。这些额外的信息代表了每个棋子和它在游戏棋盘上的位置之间的关系。
嵌入示例|作者图片
位置编码公式|作者图片
输出编码
有了对输入的这种理解,我们来谈谈输出。该模型将有两个输出值。一个代表可能结果的概率分布(奖励),另一个代表行动的概率分布(政策)。
在我们的国际象棋游戏中,可能的结果是白棋赢,平手和黑棋赢。由于这些结果是在名义规模上,我们知道这是一个分类问题。独热编码在分类问题中很流行,因为每一位可以代表不同的结果。为了确定游戏最可能结果的指数,我们可以取输出的 argmax。由此,我们现在知道了游戏最有可能的结果和预测的可信度。
预测的游戏结果|作者图片
- sw =白色胜利(0 或 1)
- st =平局(0 或 1)
- sb =黑色胜利(0 或 1)
在国际象棋中,棋子不断移动。这种移动使得我们很难将有效的动作映射回当前状态。为了缓解这个问题,我们将把动作想象成哪个方块移动到哪个方块。这里我们有效地简化了解码过程,因为棋盘上的方块在整个游戏中保持不变。我们可以用一个扁平的 64x64 矩阵来表示。展平矩阵是必要的,因为这又是一个分类问题,因为我们的行动是名义上的。正因为如此,我们将希望使用一键编码来表示我们的动作空间。为了确定最可能采取的动作的索引,我们过滤掉无效的动作,并取输出的 argmax。由此,我们现在知道了最有可能的有效动作和该预测的置信度。
游戏动作编码|作者图片
动作概率分布|作者图片
- a=行动概率(0–1)
培训模式
与 AlphaZero 类似,我们的模型完全通过自我游戏来学习。在我们的版本中,我们通过让两个模型(活动的,新的)在循环赛系列中相互竞争,为自我游戏添加了一个进化的方面。在我们的循环赛系列赛中,新型号在每场比赛后都要训练。如果新模型是循环赛的获胜者,它将成为活动模型。这样,我们可以将自我游戏导致的潜在收益递减最小化。
该模型根据每场比赛的结果进行训练,以使预测的比赛结果和预测的行动(奖励和政策网络)的损失最小化。为此,该模型使用两个输出的二元交叉熵(BCE)。BCE 是损失函数,因为它能很好地处理多变量分类问题。
损失函数公式|作者图片
- p =行动概率分布
- pi =所采取行动的一次性编码
- v =游戏结果概率分布
- z =游戏结果的一次性编码
谢谢
就这样,我们成功地创造了一个国际象棋人工智能。这个 AI 会在没有任何知识的情况下开始学习国际象棋的游戏。它玩的训练游戏越多,表现就越好。我希望您喜欢阅读本系列的第 2 部分。你可以在我的 GitHub 上查看完整版本的代码。你也可以看到这个系列的第一部分在这里和我的新机器人结合穆泽罗和感知者 IO 在这里。
感谢阅读。如果你喜欢这样,可以考虑订阅我的账户,以便在我最近发帖时得到通知。
参考
- https://en . Wikipedia . org/wiki/Deep _ Blue _(chess _ computer)
- https://en . Wikipedia . org/wiki/stock fish _(chess)
- https://deep mind . com/blog/article/alpha zero-shedding-new-light-grand-games-chess-shogi-and-go
- https://deepmind.com/
- https://en.wikipedia.org/wiki/Monte_Carlo_tree_search
- https://en.wikipedia.org/wiki/Convolutional_neural_network
- https://en.wikipedia.org/wiki/Minimax
- https://jalammar.github.io/illustrated-transformer/
- https://kazemnejad . com/blog/transformer _ architecture _ positional _ encoding/
- https://en.wikipedia.org/wiki/One-hot
- https://arxiv.org/abs/1706.03762
通过写作构建引人注目的数据科学产品组合
数据科学写作不仅可以在你的旅程中,也可以在你的职业生涯中产生变革性的影响。
照片由 Aaron Burden 在 Unsplash
我参加了由Weights&bias组织的快速阅读会议,讨论数据科学写作的好处。我写这篇文章是为了总结我在那里讨论的内容。这篇文章最初发表在他们的论坛上,但是我在这里分享了一个编辑过的版本。它主要讨论了为什么写作在数据科学中很重要,以及如何将其作为一种工具来利用您的投资组合。
学习任何概念的最佳方式,尤其是在数据科学中,是通过写下它。它有助于你详细理解这个主题,反过来,你的工作也可以帮助别人。但这说起来容易做起来难。即使许多人想写作,他们也需要几个月甚至几年的时间来克服最初的自我怀疑。雷切尔·托马斯的博客文章《为什么你(是的,你)应该写博客》非常恰当地触及了这个问题。事实上,它涵盖了你开始写作时应该记住的所有要点。我的职业生涯是从写作开始的,在这篇文章中,我将分享你如何运用你的写作技巧来创作一个引人注目的作品集。
写作为什么有用?
写作,尤其是在数据科学领域,是一项重要的技能。它给你声音和能见度。依我看,写作的好处可以总结如下:
写作的好处|作者图片
- 记忆:写作帮助你记住一个新概念。
- 研究:写作有助于培养研究思维。
- 声誉:写作有助于在社区中建立声誉。人们引用你的文章,在他们的谈话中提到你,等等。
- 收入:写作本身也可以是一种自给自足的职业。随着创作者经济的蓬勃发展,写作可以带来潜在的工作机会。
如何开始你的写作之旅
当你决定写一篇文章时,你就成功了一半。然而,第二部分是决定写什么,在哪里写,主题,长度等等。最好是自己创造自己的写作道路。从最舒服的概念开始;这会给你必要的信心开始。逐渐多样化你的写作组合。开始接触新事物,并尝试写下它们。写作是一个反复的过程。写得越多,学得越多,写得越好。
你可以写的东西
这些只是一小部分,但可以作为一个很好的起点。
展示你的作品
无论你是为一个人还是为一千个人写一篇文章,付出的努力是一样的。因此,一定要在其他平台上分享它们,比如 Linkedin、Twitter 和其他你所在的社区团体。请不要要求他们喜欢或分享本身。这是因为在分享你的作品和发送垃圾邮件之间只有一线之隔。如果人们喜欢它,觉得它有趣,相信我,他们会想自己分享它。
展示你作品的另一种方式是在聚会、演示和会议中使用它们。内容创建很有挑战性,但是一旦完成,就可以以多种方式重用。
创建公共投资组合
你可以在开放的博客平台上写作或者创建自己的网站。这完全取决于你。但是确保从一开始就建立一个好的投资组合。Github 页面、Kaggle 配置文件、堆栈溢出等。,可以支持你的简历。
为社区做贡献
社区是数据科学的支柱。加入当地的 Meetup 社区。在会议上发言——从地方到地区,甚至国家。你甚至可以指导这个领域的新手。在论坛回答和帮助别人。尝试为开源库的文档做贡献。
这些都很好,但是有人会读我的文章吗?
这是我遇到的最常见的问题。不仅在数据科学领域,而且在几乎所有领域,甚至在开始之前就自我怀疑是相当普遍的。人们的一些常见疑虑是:
已经有很多关于这个话题的文章了。我的文章会有什么不同?
假设你想写一个图书馆。你快速搜索了一下,发现还有十多篇关于这个话题的好文章。所以你会放弃这个想法吗?不,绝对不是。尝试通过使用有趣的数据集和关注库中鲜为人知的方面来对您的文章进行不同的处理。例如,我写了一篇关于Lux——一个 Python 库 的文章,在那里我展示了 Simpson 使用这个库的效果。库的创建者喜欢它,并把它作为文档的一部分。
来自 Lux 的 Github 知识库的片段| 来源
这是另一个例子。我最近写了一篇文章可解读还是准确?为什么不两者都要?,在那里我解释了可解释的助推机器的概念——模型被设计成具有与助推树相当的准确性,同时具有高度的可理解性和可解释性。 EBM 是微软的开源库。我收到了团队写这篇文章的个人信息。
作者图片
这些只是几个例子,突出了写作不仅可以给你的简历带来巨大的价值,而且可以在社区内建立人际网络。
入门资源
既然你已经决定冒险一试,这里有一些开源工具可以帮助你开始。
- blog site:fast pages—一个易于使用的博客平台,支持 Jupyter 笔记本、Word 文档和 Markdown。另一个选项可以是中。
- Images : Unsplash 、 Pixabay 和 Pexels 是一些提供免费图片和照片的网站,你可以下载并用于任何项目。
- 插图: unDraw 为可定制的开源插图。
- 自定义图像 : Canva 用于设计和发布几乎任何内容。
- 校对 : Grammarly 的免费增值版,用于拼写检查和捕捉最基本的语法错误。小型 SEO 工具则提供除了检查语法之外的免费抄袭检查。
反馈是必不可少的,但不要气馁。
领导力专家肯·布兰查德恰如其分地创造了这个短语:“反馈是冠军的早餐。”。获得批评性的反馈是提高的必要条件,这同样适用于写作。然而,给予反馈是重要的,但是谴责某人的工作(尤其是公开地)是完全错误的。可悲的是,很多时候,你会遇到这样的情况。有些人在你的文章上留下非常令人反感的评论,没有给出任何理由。
然而,我给你的建议是不要让消极情绪战胜你的激情。我和上面苏珊娜的推文产生共鸣。创造任何东西都是困难的,需要大量的耐心、毅力和坚持。
结论
在我看来,最重要的是找到你感兴趣的话题,这个话题产生了你想要回答的问题。我们倾向于拖延很多事情,比如写作。我们相信我们的第一个博客一定是最好的。就像我上面提到的,写作是一个迭代的过程,我们写的每一篇文章都会让我们成为更好的作家。所以,走出去,弄脏你的手,享受写作的乐趣。
构建机密数据网
数据网格架构和差分隐私技术如何帮助您以安全的方式实现对机密数据的访问民主化
里卡多·戈麦斯·安吉尔在 Unsplash 上的照片
最常见的数据工程架构是数据湖和数据仓库。两者都是集中式系统:来自每个数据产生实体的数据被集中到一个位置,由一个数据工程师团队负责。它带来了许多挑战和限制:
- 数据工程师没有特定的领域知识。产生数据的系统可能变化很快,由一个单独的团队负责集成到一个集中的系统中会增加复杂性和延迟。数据湖/仓库不太灵活。
- 很难添加大量数据源,因为集中式工程团队很难扩展。
- 将数据移动到一个中央存储库在法律上可能具有挑战性,尤其是跨业务线或地理边界。
- 当数据被复制到中央存储库时,数据泄露的风险就更大了
为什么数据网格现在如此受关注?
数据网格架构是 Zhamak Dehghani ( @zhamakd )(在这篇论文)在 2019 年 5 月提出的。这是数据架构设计中的一个重大转变,克服了集中式方法的局限性,并提供了可伸缩性和敏捷性。
Zhamak Dehghani 介绍她关于数据网格的论文(来源:Twitter)
数据网格的核心原则是:
— 分布式、面向领域的数据:与许多实体(比如营销、运营、R&D……)推送数据的集中式存储库相反,数据网格意味着数据是按领域组织的。这意味着数据工程师拥有领域知识,因此可以更加高效和敏捷
— 数据即产品:数据消费者的用户体验必须是域名所有者的主要关注点。任何人(当然是经过授权的)都应该能够自主地消费数据,而不需要领域数据团队的帮助。数据应该是高质量的、可发现的、自我描述的,并且通常易于消费
— 自助式数据平台:为了支持领域数据团队,组织必须提供自助式数据基础架构,为数据团队提供工具,如大数据存储、联合身份管理或数据访问日志记录
— 分散化&联合治理:全球(在组织级别)标准应该通过联合机制定义,并有领域代表。这些标准是在平台中通过计算实现的。
围绕数据网格有一个活跃的社区,你可以在这里加入。
文化和组织的变化:当数据成为产品
现在,每个数据生产实体都应该通过一个定义明确、记录完善的标准来提供其数据。数据工程师现在就来自这个领域,所以更容易扩展:领域的数量不会影响集中的数据团队,变得无关紧要。
领域数据团队现在必须考虑他们的资产,他们的产品,以及数据的消费者,他们的客户。必须衡量和跟踪客户满意度,以推动数据团队的工作。
软件和数据工程架构和哲学的可喜融合
数据网格的引入与软件工程中从单片到微服务的转变有一些相似之处。它强调了域所有者和互操作性的重要性,旨在在高度复杂的环境中进行扩展。
机密数据网格简介
在大多数组织中,许多数据集是高度敏感的,并受到限制(出于监管或商业原因)。例如,可能无法将个人数据移出法律实体或国家/地区。这些数据集不能添加到网格中,原始域之外的分析师也不能利用它们。
这是一个重大的错失机会。传统的补救策略依赖于数据屏蔽,但它需要定制的数据争论,无法解决大规模的合规性挑战,并可能对数据效用产生重大影响(更多关于数据屏蔽缺点的信息此处)。为了充分利用数据网格的力量,我们需要大规模解决机密数据访问。
输入隐私保护技术,尤其是差分隐私。
差分隐私是定义数据分析中隐私风险的数学框架。它提供了在处理数据时保护私有信息的机制和算法。它是由 Cynthia Dwork 在 2006 年推出的,并在商业应用中慢慢部署(还有很多工作要做)。它主要被谷歌、苹果或微软用于内部需求,抓住机会利用数据,同时保护隐私。这里有一个很好的资源,不用太多数学就可以学习 DP。为了更系统地回顾它,这是参考书:差分隐私的算法基础(德沃克,罗斯)。
差分隐私可以被认为是匿名计算结果的定义。它被认为是匿名的,因为它不会透露任何特定个人的重要信息。它适用于任何类型的计算,但机器学习或 SQL 分析是显而易见的候选。对一行数据应用差分隐私没有太大意义,因为结果不应该取决于数据行本身!但幸运的是,没有人在一行数据上做 ML 或 BI。在数据网格体系结构中,整个数据集位于一个位置,因此可以对所有行执行每个分析,这是差异隐私的完美匹配。
有了差分隐私,我们现在可以设计一个真正可扩展的机密数据网格。当一些域具有不能在整个组织内共享的敏感或管控数据时,这是传统数据网格的扩展。每个域都实施与其监管约束相关的隐私政策。
在机密数据网格中,不能从节点中提取敏感数据,但是仍然可以在其上执行计算。因为它实现了与经典数据网格相同的互操作性需求,所以数据源可以被同样地编目。需要提取数据样本甚至流式传输数据的应用程序总是可以访问模拟源属性的合成数据,这些数据是通过差分私有深度学习算法生成的。可以使用 SQL 查询的不同私有实现来查询节点级数据库。机器学习可以在原始数据上进行训练,仍然具有差分隐私保证。
最后,从数据从业者的角度来看,机密数据网格的行为与原始数据网格完全一样。主要区别在于所有权原则还扩展到了遵从标准和隐私政策的实现。
一个机密数据网混合了敏感域和开放域。敏感域必须只公开不同私有 API(由 Sarus technologies 绘制)
构建您自己的机密数据网(CDM)
清洁发展机制的组成部分是:
- 在 SQL 查询或 ML 作业中实现不同隐私的远程执行框架
- 跟踪数据集上所有影响隐私的查询的会计师
- 差分专用合成数据发生器
- 实现互操作性标准的 API(包括目录、数据导出、SQL 驱动程序……)
- 还有把所有东西粘在一起的胶水。
没有一种开源解决方案可以一键部署 CDM,但幸运的是,一些基本的构建模块是可用的。大多数开源项目仍处于试验阶段,但它们是一个很好的开始资源。最有趣的是:
- OpenDP :这是哈佛和微软的共同努力,为差分私有计算提供了主要的工具箱。主要贡献是一组 DP 原语( smartnoise-core )和一个运行 SQL 查询的 SDK(smart noise-SDK)
- Google Differential Privacy:与 OpenDP 类似,Google DP 提供了 DP 原语,可以在 Apache Beam、Differential-private SQL 引擎和隐私会计师之上运行
- TensorFlow/privacy :允许训练具有差分隐私的 TensorFlow 模型。请注意,这段代码已经打包在 OpenMined 中。
- PyTorch Opacus :来自脸书的一个库,用来训练 PyTorch 模型的不同隐私
通过将这些库安装在机密域上,用户可以在不同的隐私保证下对机密数据进行查询。
这是一个良好的开端,但我们仍远未找到可行的解决方案,因为缺少许多关键部分:
- API:数据网格非常强大,因为所有与外部用户的通信都是通过标准化的 API 进行的。CDM 提供与其他节点的互操作性也是如此。
- 加强隐私合规性:开源库是为科学家设计的,他们可以访问原始数据,并希望发布私人成果。用户有责任使用差分隐私的安全参数化。在 CDM 中,这应该是强制的,这样用户就不能执行任何暴露隐私的查询。
- 优化隐私消费 : DP 伴随着隐私预算的概念。但是如果孤立地考虑每个查询,这个预算会很快用完。为了获得最佳的准确性,人们希望利用所有的公共信息来建立有用的先验知识,以及之前已经发布的任何信息。实现记忆化是执行连续查询或学习作业的重要部分。
- 合成数据:数据从业者会希望看到一些原始数据的样本。因为揭示原始数据不是一个选项,所以合成数据是必须的。它必须用差分隐私来生成,以保持隐私保证。
- 使用策略和会计的隐私治理:如何控制多个请求的隐私泄露?(提示:这不是微不足道的,因为差异隐私的关键指标ε不是可加的:如果你花了ε= 1,那么ε= 2,你没有花ε= 3:()。管理员如何为不同的用户设置不同的策略?
如果没有单一的开源库可以将 CDM 带入生活,一些初创公司和大型组织正在慢慢地将所有的碎片汇集在一起,使 CDM 成为大规模利用敏感数据的最有效方式。
在 Sarus,我们让以数据为中心的组织在以完全合规的方式处理敏感数据时更加高效和敏捷。如果您想了解更多关于机密数据网格、Sarus 如何提供帮助、获取演示并亲自尝试,请联系 hello@sarus.tech.
如果你对用最先进的技术解决隐私问题感兴趣,我们正在招聘,所以快来加入我们吧!
构建卷积神经网络模型以理解场景
R 中的数据科学
一个完整的循序渐进的数据扩充和转移学习教程
照片由索拉萨克、迈克尔·克拉恩、肖恩·皮尔斯、纪尧姆·布里亚德、希法兹·沙蒙和岩田良治在 Unsplash
**Table of Contents**· [Library](#dcd8)
· [Dataset](#44f0)
· [Exploratory Data Analysis](#7f82)
· [Data Preprocessing](#e848)
· [Modeling](#8635)
∘ [Simple CNN](#a08c)
∘ [Deeper CNN](#161c)
∘ [Deeper CNN with Pretrained Weights](#0f40)
· [Conclusion](#fb48)
自从我开始在介质上写作,我就非常依赖 Unsplash 。这是一个美丽的地方,创造高品质的图像。但是你知道 Unsplash 用机器学习来帮助标记照片吗?
对于上传到 Unsplash […]的每张图片,我们都会通过一系列机器学习算法来理解照片的内容,从而消除了投稿人手动标记照片的需要。— Unsplash 博客
给照片贴标签是一项非常重要的任务,可以由机器快速完成。因此,我们将建立一个模型,可以从图像中提取信息,并给出正确的标签,以根据主题位置对图像数据库进行分类。我们将使用卷积神经网络(CNN)进行预测,以对图像是关于“建筑物”、“森林”、“冰川”、“山”、“海”还是“街道”进行分类。所以,这是一个图像分类问题。
图书馆
除了我们通常在 R 中使用的循环库,我们还将利用 keras 。Keras 是一个高级神经网络 API,开发的目的是实现快速实验。
library(keras) # deep learning
library(tidyverse) # data wrangling
library(imager) # image manipulation
library(caret) # model evaluation
library(grid) # display images in a grid
library(gridExtra) # display images in a gridRS <- 42 # random state constant
注意,我们创建了一个名为RS
的变量,它只是一个数字,用于未来随机过程的再现性。
资料组
数据由带有 6 个不同标签的图像组成:“建筑物”、“森林”、“冰川”、“山”、“海”和“街道”。不像上一篇文章中的图像像素数据已经被转换成一个.csv
文件的列,这次我们使用数据生成器直接读取图像。
https://medium.com/data-folks-indonesia/hand-gesture-recognition-8c0e2927a8bb [## 手势识别
medium.com](https://medium.com/data-folks-indonesia/hand-gesture-recognition-8c0e2927a8bb)
为此,我们需要知道图像文件夹结构,如下所示。
seg_train
└── seg_train
├── buildings
├── forest
├── glacier
├── mountain
├── sea
└── streetseg_test
└── seg_test
├── buildings
├── forest
├── glacier
├── mountain
├── sea
└── street
在每个buildings
、forest
、glacier
、mountain
、sea
和street
子文件夹中,保存有相应的图像。顾名思义,我们将使用seg_train
进行模型训练,使用seg_test
进行模型验证。
探索性数据分析
首先,我们需要定位每个类别的父文件夹地址。
folder_list <- list.files("seg_train/seg_train/")
folder_path <- paste0("seg_train/seg_train/", folder_list, "/")
folder_path#> [1] "seg_train/seg_train/buildings/" "seg_train/seg_train/forest/" "seg_train/seg_train/glacier/" "seg_train/seg_train/mountain/"
#> [5] "seg_train/seg_train/sea/" "seg_train/seg_train/street/"
然后,列出来自每个父文件夹地址的所有seg_train
图像地址。
file_name <-
map(folder_path, function(x) paste0(x, list.files(x))) %>%
unlist()
下面我们可以看到总共有 14034 张seg_train
图片。
cat("Number of train images:", length(file_name))#> Number of train images: 14034
作为理智检查,让我们看几张seg_train
图片。
set.seed(RS)
sample_image <- sample(file_name, 18)
img <- map(sample_image, load.image)
grobs <- lapply(img, rasterGrob)
grid.arrange(grobs=grobs, ncol=6)
拿第一张图片来说。
img <- load.image(file_name[1])
img#> Image. Width: 150 pix Height: 150 pix Depth: 1 Colour channels: 3
下面可以看到,这张图的尺寸是 150 × 150 × 1 × 3。这意味着这个特定的图像具有 150 像素的宽度、150 像素的高度、1 像素的深度和 3 个颜色通道(用于红色、绿色和蓝色,也称为 RGB)。
dim(img)#> [1] 150 150 1 3
现在,我们将构建一个函数来获取图像的宽度和高度,并将该函数应用于所有图像。
get_dim <- function(x){
img <- load.image(x)
df_img <- data.frame(
width = width(img),
height = height(img),
filename = x
)
return(df_img)
}
file_dim <- map_df(file_name, get_dim)
head(file_dim)#> width height filename
#> 1 150 150 seg_train/seg_train/buildings/0.jpg
#> 2 150 150 seg_train/seg_train/buildings/10006.jpg
#> 3 150 150 seg_train/seg_train/buildings/1001.jpg
#> 4 150 150 seg_train/seg_train/buildings/10014.jpg
#> 5 150 150 seg_train/seg_train/buildings/10018.jpg
#> 6 150 150 seg_train/seg_train/buildings/10029.jpg
我们得到了图像的宽度和高度的如下分布。
hist(file_dim$width, breaks = 20)
hist(file_dim$height, breaks = 20)
summary(file_dim)#> width height filename
#> Min. :150 Min. : 76.0 Length:14034
#> 1st Qu.:150 1st Qu.:150.0 Class :character
#> Median :150 Median :150.0 Mode :character
#> Mean :150 Mean :149.9
#> 3rd Qu.:150 3rd Qu.:150.0
#> Max. :150 Max. :150.0
正如我们所见,数据集包含不同维度的图像。所有的宽度都是 150 像素。但是,最大和最小高度分别为 150 和 76 像素。在适合模型之前,所有这些图像必须在相同的维度上。这一点至关重要,因为:
- 每个图像像素值所适合的模型的输入层具有固定数量的神经元,
- 如果图像尺寸太高,训练模型可能会花费太长时间
- 如果图像尺寸太低,就会丢失太多信息。
数据预处理
神经网络模型可能出现的一个问题是,它们倾向于记忆seg_train
数据集中的图像,以至于当新的seg_test
数据集进来时,它们无法识别它。数据扩充是解决这一问题的众多技术之一。给定一幅图像,数据增强将对其进行轻微转换,以创建一些新图像。然后将这些新图像放入模型中。这样,模型知道原始图像的许多版本,并且希望理解图像的意思,而不是记住它。我们将只使用一些简单的转换,例如:
- 随机水平翻转图像
- 随机旋转 10 度
- 随机放大 0.1 倍
- 随机水平移动总宽度的 0.1 分之一
- 随机水平移动总高度的 0.1 分之一
我们不使用垂直翻转,因为在我们的情况下,他们可以改变图像的意义。这种数据扩充可以使用image_data_generator()
功能完成。将生成器保存到名为train_data_gen
的对象中。注意train_data_gen
仅在训练时应用,我们在预测时不使用它。
在train_data_gen
中,我们还进行了归一化处理,以减少光照差异的影响。此外,CNN 模型在[0…1]数据比[0…255].为此,只需将每个像素值除以 255。
train_data_gen <- image_data_generator(
rescale = 1/255, # scaling pixel value
horizontal_flip = T, # flip image horizontally
vertical_flip = F, # flip image vertically
rotation_range = 10, # rotate image from 0 to 45 degrees
zoom_range = 0.1, # zoom in or zoom out range
width_shift_range = 0.1, # shift horizontally to the width
height_shift_range = 0.1, # shift horizontally to the height
)
我们将使用 150 × 150 像素作为输入图像的形状,因为 150 像素是所有图像中最常见的宽度和高度(再次查看 EDA),并将大小存储为target_size
。此外,我们将分批训练模型,每批 32 个观测值,存储为batch_size
。
target_size <- c(150, 150)
batch_size <- 32
现在,构建生成器以从各自的目录中流动训练和验证数据集。填写参数target_size
和batch_size
。由于我们有彩色的 rgb 图像,将color_mode
设置为“RGB”。最后,使用train_data_gen
作为generator
来应用之前创建的数据扩充。
# for training dataset
train_image_array_gen <- flow_images_from_directory(
directory = "seg_train/seg_train/", # folder of the data
target_size = target_size, # target of the image dimension
color_mode = "rgb", # use RGB color
batch_size = batch_size , # number of images in each batch
seed = RS, # set random seed
generator = train_data_gen # apply data augmentation
)
# for validation dataset
val_image_array_gen <- flow_images_from_directory(
directory = "seg_test/seg_test/",
target_size = target_size,
color_mode = "rgb",
batch_size = batch_size ,
seed = RS,
generator = train_data_gen
)
接下来,我们将看到目标变量中标签的比例,以检查类不平衡。如果有的话,分类器倾向于做出有偏差的学习模型,与多数类相比,该模型对少数类具有较差的预测准确性。我们可以通过对训练数据集进行上采样或下采样来以最简单的方式解决这个问题。
output_n <- n_distinct(train_image_array_gen$classes)
table("Frequency" = factor(train_image_array_gen$classes)) %>%
prop.table()#> Frequency
#> 0 1 2 3 4 5
#> 0.1561208 0.1618213 0.1712983 0.1789939 0.1620351 0.1697307
幸运的是,从上面可以看出,所有的职业都相对平衡!我们不需要进一步的治疗。
建模
首先,让我们保存我们使用的训练和验证图像的数量。除了训练数据之外,我们还需要不同的数据来进行验证,因为我们不希望我们的模型只擅长预测它已经看到的图像,而且还可以推广到未看到的图像。这种对看不见的图像的需求正是我们也必须在验证数据集上看到模型性能的原因。
因此,我们可以在下面观察到,我们有 14034 个图像用于训练(如前所述),3000 个图像用于验证模型。
train_samples <- train_image_array_gen$n
valid_samples <- val_image_array_gen$n
train_samples#> [1] 14034valid_samples#> [1] 3000
我们将从最简单的开始逐步构建三个模型。
简单 CNN
该模型只有 4 个隐藏层,包括最大池化和展平,以及 1 个输出层,详情如下:
- 卷积层:滤波器 16,核大小 3 × 3,相同填充,relu 激活函数
- 最大池层:池大小 2 × 2
- 展平图层
- 密集层:16 节点,relu 激活功能
- 密集层(输出):6 节点,softmax 激活功能
请注意,我们使用扁平化层作为从网络的卷积部分到密集部分的桥梁。基本上,展平层所做的就是——顾名思义——将最后一个卷积层的维度展平为单个密集层。例如,假设我们有一个大小为(8,8,32)的卷积层。这里,32 是过滤器的数量。展平层将把这个张量整形为大小为 2048 矢量。
在输出层,我们使用 softmax 激活函数,因为这是一个多类分类问题。最后,我们需要指定 CNN 输入层所需的图像大小。如前所述,我们将使用存储在target_size
中的 150 × 150 像素的图像尺寸和 3 个 RGB 通道。
现在,我们准备好了。
# Set Initial Random Weight
tensorflow::tf$random$set_seed(RS)
model <- keras_model_sequential(name = "simple_model") %>%
# Convolution Layer
layer_conv_2d(filters = 16,
kernel_size = c(3,3),
padding = "same",
activation = "relu",
input_shape = c(target_size, 3)
) %>%
# Max Pooling Layer
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# Flattening Layer
layer_flatten() %>%
# Dense Layer
layer_dense(units = 16,
activation = "relu") %>%
# Output Layer
layer_dense(units = output_n,
activation = "softmax",
name = "Output")
summary(model)#> Model: "simple_model"
#> _________________________________________________________________
#> Layer (type) Output Shape Param #
#> =================================================================
#> conv2d (Conv2D) (None, 150, 150, 16) 448
#> _________________________________________________________________
#> max_pooling2d (MaxPooling2D) (None, 75, 75, 16) 0
#> _________________________________________________________________
#> flatten (Flatten) (None, 90000) 0
#> _________________________________________________________________
#> dense (Dense) (None, 16) 1440016
#> _________________________________________________________________
#> Output (Dense) (None, 6) 102
#> =================================================================
#> Total params: 1,440,566
#> Trainable params: 1,440,566
#> Non-trainable params: 0
#> _________________________________________________________________
建立后,我们编译和训练模型。我们对损失函数使用分类交叉熵,因为这又是一个多类分类问题。我们使用默认学习率为 0.001 的 adam 优化器,因为 adam 是最有效的优化器之一。我们还使用准确性作为简单性的衡量标准。更重要的是,因为我们不喜欢一个类高于其他类,并且每个类都是平衡的,所以与精确度、灵敏度或特异性相比,准确性更受青睐。我们将训练 10 个时期的模型。
model %>%
compile(
loss = "categorical_crossentropy",
optimizer = optimizer_adam(lr = 0.001),
metrics = "accuracy"
)
# fit data into model
history <- model %>%
fit_generator(
# training data
train_image_array_gen,
# training epochs
steps_per_epoch = as.integer(train_samples / batch_size),
epochs = 10,
# validation data
validation_data = val_image_array_gen,
validation_steps = as.integer(valid_samples / batch_size)
)plot(history)
从第十个时期的最终训练和验证精度,我们可以看到它们具有相似的值并且相对较高,这意味着没有发生过拟合。接下来,我们将对验证数据集上的所有图像执行预测(而不是像在训练中那样对每批图像执行预测)。首先,让我们将每个图像的路径和它对应的类列表。
val_data <- data.frame(file_name = paste0("seg_test/seg_test/", val_image_array_gen$filenames)) %>%
mutate(class = str_extract(file_name, "buildings|forest|glacier|mountain|sea|street"))
head(val_data)#> file_name class
#> 1 seg_test/seg_test/buildings\\20057.jpg buildings
#> 2 seg_test/seg_test/buildings\\20060.jpg buildings
#> 3 seg_test/seg_test/buildings\\20061.jpg buildings
#> 4 seg_test/seg_test/buildings\\20064.jpg buildings
#> 5 seg_test/seg_test/buildings\\20073.jpg buildings
#> 6 seg_test/seg_test/buildings\\20074.jpg buildings
然后,我们将每个图像转换成一个数组。不要忘记将像素值归一化,即除以 255。
image_prep <- function(x, target_size) {
arrays <- lapply(x, function(path) {
img <- image_load(
path,
target_size = target_size,
grayscale = F
)
x <- image_to_array(img)
x <- array_reshape(x, c(1, dim(x)))
x <- x/255
})
do.call(abind::abind, c(arrays, list(along = 1)))
}
test_x <- image_prep(val_data$file_name, target_size)
dim(test_x)#> [1] 3000 150 150 3
接下来,预测。
pred_test <- predict_classes(model, test_x)
head(pred_test)#> [1] 4 0 0 0 4 3
现在,将每个预测解码到相应的类别中。
decode <- function(x){
case_when(
x == 0 ~ "buildings",
x == 1 ~ "forest",
x == 2 ~ "glacier",
x == 3 ~ "mountain",
x == 4 ~ "sea",
x == 5 ~ "street",
)
}
pred_test <- sapply(pred_test, decode)
head(pred_test)#> [1] "sea" "buildings" "buildings" "buildings" "sea" "mountain"
最后,分析混淆矩阵。
cm_simple <- confusionMatrix(as.factor(pred_test), as.factor(val_data$class))
acc_simple <- cm_simple$overall['Accuracy']
cm_simple#> Confusion Matrix and Statistics
#>
#> Reference
#> Prediction buildings forest glacier mountain sea street
#> buildings 348 24 14 20 35 106
#> forest 8 418 3 4 4 19
#> glacier 7 5 357 53 38 5
#> mountain 19 6 98 381 61 5
#> sea 13 1 75 65 363 6
#> street 42 20 6 2 9 360
#>
#> Overall Statistics
#>
#> Accuracy : 0.7423
#> 95% CI : (0.7263, 0.7579)
#> No Information Rate : 0.1843
#> P-Value [Acc > NIR] : < 0.00000000000000022
#>
#> Kappa : 0.6909
#>
#> Mcnemar's Test P-Value : 0.0000000001327
#>
#> Statistics by Class:
#>
#> Class: buildings Class: forest Class: glacier Class: mountain Class: sea Class: street
#> Sensitivity 0.7963 0.8819 0.6456 0.7257 0.7118 0.7186
#> Specificity 0.9224 0.9850 0.9559 0.9236 0.9357 0.9684
#> Pos Pred Value 0.6362 0.9167 0.7677 0.6684 0.6941 0.8200
#> Neg Pred Value 0.9637 0.9780 0.9227 0.9407 0.9407 0.9449
#> Prevalence 0.1457 0.1580 0.1843 0.1750 0.1700 0.1670
#> Detection Rate 0.1160 0.1393 0.1190 0.1270 0.1210 0.1200
#> Detection Prevalence 0.1823 0.1520 0.1550 0.1900 0.1743 0.1463
#> Balanced Accuracy 0.8593 0.9334 0.8007 0.8247 0.8238 0.8435
从混淆矩阵中可以看出,该模型很难区分每个类别。我们在验证数据集上获得了 74%的准确率。有 106 个街道图像被预测为建筑物,这超过了所有街道图像的 20%。这是有意义的,因为在许多街道图像中,建筑物也存在。
我们可以通过各种方式来提高模型的性能。但是现在,让我们通过简单地改变架构来改进它。
深度 CNN
现在我们用更多的卷积层制作一个更深的 CNN。这是它的架构:
- 块 1: 2 个卷积层和 1 个最大汇集层
- 块 2: 1 个卷积层和 1 个最大池层
- 块 3: 1 个卷积层和 1 个最大池层
- 块 4: 1 个卷积层和 1 个最大池层
- 展平图层
- 一个致密层
- 输出层
tensorflow::tf$random$set_seed(RS)
model_big <- keras_model_sequential(name = "model_big") %>%
# First convolutional layer
layer_conv_2d(filters = 32,
kernel_size = c(5,5), # 5 x 5 filters
padding = "same",
activation = "relu",
input_shape = c(target_size, 3)
) %>%
# Second convolutional layer
layer_conv_2d(filters = 32,
kernel_size = c(3,3), # 3 x 3 filters
padding = "same",
activation = "relu"
) %>%
# Max pooling layer
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# Third convolutional layer
layer_conv_2d(filters = 64,
kernel_size = c(3,3),
padding = "same",
activation = "relu"
) %>%
# Max pooling layer
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# Fourth convolutional layer
layer_conv_2d(filters = 128,
kernel_size = c(3,3),
padding = "same",
activation = "relu"
) %>%
# Max pooling layer
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# Fifth convolutional layer
layer_conv_2d(filters = 256,
kernel_size = c(3,3),
padding = "same",
activation = "relu"
) %>%
# Max pooling layer
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# Flattening layer
layer_flatten() %>%
# Dense layer
layer_dense(units = 64,
activation = "relu") %>%
# Output layer
layer_dense(name = "Output",
units = output_n,
activation = "softmax")
summary(model_big)#> Model: "model_big"
#> _________________________________________________________________
#> Layer (type) Output Shape Param #
#> =================================================================
#> conv2d_5 (Conv2D) (None, 150, 150, 32) 2432
#> _________________________________________________________________
#> conv2d_4 (Conv2D) (None, 150, 150, 32) 9248
#> _________________________________________________________________
#> max_pooling2d_4 (MaxPooling2D) (None, 75, 75, 32) 0
#> _________________________________________________________________
#> conv2d_3 (Conv2D) (None, 75, 75, 64) 18496
#> _________________________________________________________________
#> max_pooling2d_3 (MaxPooling2D) (None, 37, 37, 64) 0
#> _________________________________________________________________
#> conv2d_2 (Conv2D) (None, 37, 37, 128) 73856
#> _________________________________________________________________
#> max_pooling2d_2 (MaxPooling2D) (None, 18, 18, 128) 0
#> _________________________________________________________________
#> conv2d_1 (Conv2D) (None, 18, 18, 256) 295168
#> _________________________________________________________________
#> max_pooling2d_1 (MaxPooling2D) (None, 9, 9, 256) 0
#> _________________________________________________________________
#> flatten_1 (Flatten) (None, 20736) 0
#> _________________________________________________________________
#> dense_1 (Dense) (None, 64) 1327168
#> _________________________________________________________________
#> Output (Dense) (None, 6) 390
#> =================================================================
#> Total params: 1,726,758
#> Trainable params: 1,726,758
#> Non-trainable params: 0
#> _________________________________________________________________
剩下的和之前做的一样。
model_big %>%
compile(
loss = "categorical_crossentropy",
optimizer = optimizer_adam(lr = 0.001),
metrics = "accuracy"
)
history <- model_big %>%
fit_generator(
train_image_array_gen,
steps_per_epoch = as.integer(train_samples / batch_size),
epochs = 10,
validation_data = val_image_array_gen,
validation_steps = as.integer(valid_samples / batch_size)
)plot(history)
pred_test <- predict_classes(model_big, test_x)
pred_test <- sapply(pred_test, decode)
cm_big <- confusionMatrix(as.factor(pred_test), as.factor(val_data$class))
acc_big <- cm_big$overall['Accuracy']
cm_big#> Confusion Matrix and Statistics
#>
#> Reference
#> Prediction buildings forest glacier mountain sea street
#> buildings 390 3 24 24 11 34
#> forest 3 465 11 7 8 11
#> glacier 2 0 367 35 9 1
#> mountain 0 2 82 415 17 1
#> sea 3 1 57 42 461 6
#> street 39 3 12 2 4 448
#>
#> Overall Statistics
#>
#> Accuracy : 0.8487
#> 95% CI : (0.8353, 0.8613)
#> No Information Rate : 0.1843
#> P-Value [Acc > NIR] : < 0.00000000000000022
#>
#> Kappa : 0.8185
#>
#> Mcnemar's Test P-Value : < 0.00000000000000022
#>
#> Statistics by Class:
#>
#> Class: buildings Class: forest Class: glacier Class: mountain Class: sea Class: street
#> Sensitivity 0.8924 0.9810 0.6637 0.7905 0.9039 0.8942
#> Specificity 0.9625 0.9842 0.9808 0.9588 0.9562 0.9760
#> Pos Pred Value 0.8025 0.9208 0.8865 0.8027 0.8088 0.8819
#> Neg Pred Value 0.9813 0.9964 0.9281 0.9557 0.9798 0.9787
#> Prevalence 0.1457 0.1580 0.1843 0.1750 0.1700 0.1670
#> Detection Rate 0.1300 0.1550 0.1223 0.1383 0.1537 0.1493
#> Detection Prevalence 0.1620 0.1683 0.1380 0.1723 0.1900 0.1693
#> Balanced Accuracy 0.9275 0.9826 0.8222 0.8746 0.9301 0.9351
这个结果总体上比之前的model
要好,因为model_big
更复杂,因此能够捕捉更多的特征。我们在验证数据集上获得了 85%的准确率。虽然对街道图像的预测已经变得更好,但对冰川图像的预测仍然存在。
预训练权重的深度 CNN
实际上,研究人员已经为图像分类问题开发了许多模型,从 VGG 模型家族到谷歌开发的最新艺术级 EfficientNet。出于学习目的,在本节中,我们将使用 VGG16 模型,因为它是所有模型中最简单的模型之一,仅由卷积层、最大池层和密集层组成,如我们之前介绍的那样。这个过程叫做迁移学习,将预先训练好的模型的知识进行迁移,来解决我们的问题。
最初的 VGG16 模型是在 1000 个类上训练的。为了使它适合我们的问题,我们将排除模型的顶部(密集)层,并插入我们版本的预测层,该预测层由一个全局平均池层(作为扁平化层的替代)、一个具有 64 个节点的密集层和一个具有 6 个节点的输出层(用于 6 个类)组成。
我们来看看整体架构。
# load original model without top layers
input_tensor <- layer_input(shape = c(target_size, 3))
base_model <- application_vgg16(input_tensor = input_tensor,
weights = 'imagenet',
include_top = FALSE)
# add our custom layers
predictions <- base_model$output %>%
layer_global_average_pooling_2d() %>%
layer_dense(units = 64, activation = 'relu') %>%
layer_dense(units = output_n, activation = 'softmax')
# this is the model we will train
vgg16 <- keras_model(inputs = base_model$input, outputs = predictions)
summary(vgg16)#> Model: "model"
#> _________________________________________________________________
#> Layer (type) Output Shape Param #
#> =================================================================
#> input_1 (InputLayer) [(None, 150, 150, 3)] 0
#> _________________________________________________________________
#> block1_conv1 (Conv2D) (None, 150, 150, 64) 1792
#> _________________________________________________________________
#> block1_conv2 (Conv2D) (None, 150, 150, 64) 36928
#> _________________________________________________________________
#> block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
#> _________________________________________________________________
#> block2_conv1 (Conv2D) (None, 75, 75, 128) 73856
#> _________________________________________________________________
#> block2_conv2 (Conv2D) (None, 75, 75, 128) 147584
#> _________________________________________________________________
#> block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
#> _________________________________________________________________
#> block3_conv1 (Conv2D) (None, 37, 37, 256) 295168
#> _________________________________________________________________
#> block3_conv2 (Conv2D) (None, 37, 37, 256) 590080
#> _________________________________________________________________
#> block3_conv3 (Conv2D) (None, 37, 37, 256) 590080
#> _________________________________________________________________
#> block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
#> _________________________________________________________________
#> block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160
#> _________________________________________________________________
#> block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808
#> _________________________________________________________________
#> block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808
#> _________________________________________________________________
#> block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
#> _________________________________________________________________
#> block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808
#> _________________________________________________________________
#> block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808
#> _________________________________________________________________
#> block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808
#> _________________________________________________________________
#> block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
#> _________________________________________________________________
#> global_average_pooling2d (GlobalAveragePooling2D) (None, 512) 0
#> _________________________________________________________________
#> dense_3 (Dense) (None, 64) 32832
#> _________________________________________________________________
#> dense_2 (Dense) (None, 6) 390
#> =================================================================
#> Total params: 14,747,910
#> Trainable params: 14,747,910
#> Non-trainable params: 0
#> _________________________________________________________________
哇,这么多层次啊!我们可以直接使用vgg16
进行训练和预测,但同样,出于学习的目的,让我们自己从头开始制作 VGG16 模型。
model_bigger <- keras_model_sequential(name = "model_bigger") %>%
# Block 1
layer_conv_2d(filters = 64,
kernel_size = c(3, 3),
activation='relu',
padding='same',
input_shape = c(94, 94, 3),
name='block1_conv1') %>%
layer_conv_2d(filters = 64,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block1_conv2') %>%
layer_max_pooling_2d(pool_size = c(2, 2),
strides=c(2, 2),
name='block1_pool') %>%
# Block 2
layer_conv_2d(filters = 128,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block2_conv1') %>%
layer_conv_2d(filters = 128,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block2_conv2') %>%
layer_max_pooling_2d(pool_size = c(2, 2),
strides=c(2, 2),
name='block2_pool') %>%
# Block 3
layer_conv_2d(filters = 256,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block3_conv1') %>%
layer_conv_2d(filters = 256,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block3_conv2') %>%
layer_conv_2d(filters = 256,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block3_conv3') %>%
layer_max_pooling_2d(pool_size = c(2, 2),
strides=c(2, 2),
name='block3_pool') %>%
# Block 4
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block4_conv1') %>%
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block4_conv2') %>%
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block4_conv3') %>%
layer_max_pooling_2d(pool_size = c(2, 2),
strides=c(2, 2),
name='block4_pool') %>%
# Block 5
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block5_conv1') %>%
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block5_conv2') %>%
layer_conv_2d(filters = 512,
kernel_size = c(3, 3),
activation='relu',
padding='same',
name='block5_conv3') %>%
layer_max_pooling_2d(pool_size = c(2, 2),
strides=c(2, 2),
name='block5_pool') %>%
# Dense
layer_global_average_pooling_2d() %>%
layer_dense(units = 64, activation = 'relu') %>%
layer_dense(units = output_n, activation = 'softmax')
model_bigger#> Model
#> Model: "model_bigger"
#> _________________________________________________________________
#> Layer (type) Output Shape Param #
#> =================================================================
#> block1_conv1 (Conv2D) (None, 94, 94, 64) 1792
#> _________________________________________________________________
#> block1_conv2 (Conv2D) (None, 94, 94, 64) 36928
#> _________________________________________________________________
#> block1_pool (MaxPooling2D) (None, 47, 47, 64) 0
#> _________________________________________________________________
#> block2_conv1 (Conv2D) (None, 47, 47, 128) 73856
#> _________________________________________________________________
#> block2_conv2 (Conv2D) (None, 47, 47, 128) 147584
#> _________________________________________________________________
#> block2_pool (MaxPooling2D) (None, 23, 23, 128) 0
#> _________________________________________________________________
#> block3_conv1 (Conv2D) (None, 23, 23, 256) 295168
#> _________________________________________________________________
#> block3_conv2 (Conv2D) (None, 23, 23, 256) 590080
#> _________________________________________________________________
#> block3_conv3 (Conv2D) (None, 23, 23, 256) 590080
#> _________________________________________________________________
#> block3_pool (MaxPooling2D) (None, 11, 11, 256) 0
#> _________________________________________________________________
#> block4_conv1 (Conv2D) (None, 11, 11, 512) 1180160
#> _________________________________________________________________
#> block4_conv2 (Conv2D) (None, 11, 11, 512) 2359808
#> _________________________________________________________________
#> block4_conv3 (Conv2D) (None, 11, 11, 512) 2359808
#> _________________________________________________________________
#> block4_pool (MaxPooling2D) (None, 5, 5, 512) 0
#> _________________________________________________________________
#> block5_conv1 (Conv2D) (None, 5, 5, 512) 2359808
#> _________________________________________________________________
#> block5_conv2 (Conv2D) (None, 5, 5, 512) 2359808
#> _________________________________________________________________
#> block5_conv3 (Conv2D) (None, 5, 5, 512) 2359808
#> _________________________________________________________________
#> block5_pool (MaxPooling2D) (None, 2, 2, 512) 0
#> _________________________________________________________________
#> global_average_pooling2d_1 (GlobalAveragePooling2D) (None, 512) 0
#> _________________________________________________________________
#> dense_5 (Dense) (None, 64) 32832
#> _________________________________________________________________
#> dense_4 (Dense) (None, 6) 390
#> =================================================================
#> Total params: 14,747,910
#> Trainable params: 14,747,910
#> Non-trainable params: 0
#> _________________________________________________________________
注意model_bigger
与vgg16
每层的参数数量完全相同。迁移学习的优势在于,我们不必从随机权重开始训练模型,而是从原始模型的预训练权重开始。这些预先训练的权重已经针对图像分类问题进行了优化,我们只需要对它们进行微调以符合我们的目的。因此有了这个比喻:
我们正站在巨人的肩膀上。
话虽如此,我们还是把vgg16
的权重全部赋给model_bigger
吧。
set_weights(model_bigger, get_weights(vgg16))
这里是我们的model_bigger
层的总结:
layers <- model_bigger$layers
for (i in 1:length(layers))
cat(i, layers[[i]]$name, "\n")#> 1 block1_conv1
#> 2 block1_conv2
#> 3 block1_pool
#> 4 block2_conv1
#> 5 block2_conv2
#> 6 block2_pool
#> 7 block3_conv1
#> 8 block3_conv2
#> 9 block3_conv3
#> 10 block3_pool
#> 11 block4_conv1
#> 12 block4_conv2
#> 13 block4_conv3
#> 14 block4_pool
#> 15 block5_conv1
#> 16 block5_conv2
#> 17 block5_conv3
#> 18 block5_pool
#> 19 global_average_pooling2d_1
#> 20 dense_5
#> 21 dense_4
请注意,第 19-21 层仍然具有随机权重,因为它们是由我们创建的,而不是来自原始模型。因此,我们需要先冻结所有层,然后只训练这些层。
freeze_weights(model_bigger, from = 1, to = 18)
为了训练这些预测层,我们将简单地使用先前的设置。
# compile the model (should be done *after* setting base layers to non-trainable)
model_bigger %>% compile(loss = "categorical_crossentropy",
optimizer = optimizer_adam(lr = 0.001),
metrics = "accuracy")
history <- model_bigger %>%
fit_generator(
train_image_array_gen,
steps_per_epoch = as.integer(train_samples / batch_size),
epochs = 10,
validation_data = val_image_array_gen,
validation_steps = as.integer(valid_samples / batch_size)
)
现在,微调模型。要做到这一点,我们应该对优化器应用一个低的学习率,这样就不会打乱预先训练好的权重。我们将使用 0.00001 的学习率。此外,为了节省时间,我们只训练 4 个时期的模型。
在微调之前,不要忘记解冻要训练的层。在我们的例子中,我们将解冻所有层。
unfreeze_weights(model_bigger)
# compile again with low learning rate
model_bigger %>% compile(loss = "categorical_crossentropy",
optimizer = optimizer_adam(lr = 0.00001),
metrics = "accuracy")
history <- model_bigger %>%
fit_generator(
train_image_array_gen,
steps_per_epoch = as.integer(train_samples / batch_size),
epochs = 4,
validation_data = val_image_array_gen,
validation_steps = as.integer(valid_samples / batch_size)
)plot(history)
pred_test <- predict_classes(model_bigger, test_x)
pred_test <- sapply(pred_test, decode)
cm_bigger <- confusionMatrix(as.factor(pred_test), as.factor(val_data$class))
acc_bigger <- cm_bigger$overall['Accuracy']
cm_bigger#> Confusion Matrix and Statistics
#>
#> Reference
#> Prediction buildings forest glacier mountain sea street
#> buildings 396 0 2 1 2 13
#> forest 1 469 2 2 4 0
#> glacier 1 2 479 61 5 0
#> mountain 0 0 50 452 4 0
#> sea 1 1 16 7 492 2
#> street 38 2 4 2 3 486
#>
#> Overall Statistics
#>
#> Accuracy : 0.9247
#> 95% CI : (0.9146, 0.9339)
#> No Information Rate : 0.1843
#> P-Value [Acc > NIR] : < 0.0000000000000002
#>
#> Kappa : 0.9095
#>
#> Mcnemar's Test P-Value : 0.00281
#>
#> Statistics by Class:
#>
#> Class: buildings Class: forest Class: glacier Class: mountain Class: sea Class: street
#> Sensitivity 0.9062 0.9895 0.8662 0.8610 0.9647 0.9701
#> Specificity 0.9930 0.9964 0.9718 0.9782 0.9892 0.9804
#> Pos Pred Value 0.9565 0.9812 0.8741 0.8933 0.9480 0.9084
#> Neg Pred Value 0.9841 0.9980 0.9698 0.9707 0.9927 0.9939
#> Prevalence 0.1457 0.1580 0.1843 0.1750 0.1700 0.1670
#> Detection Rate 0.1320 0.1563 0.1597 0.1507 0.1640 0.1620
#> Detection Prevalence 0.1380 0.1593 0.1827 0.1687 0.1730 0.1783
#> Balanced Accuracy 0.9496 0.9929 0.9190 0.9196 0.9769 0.9752
我们已经找到了赢家。model_bigger
在验证数据集上有 92%的准确率!尽管如此,还是有一些错误的分类,因为没有模型是完美的。以下是预测的摘要:
- 有些建筑被误预测为街道,反之亦然。再次,这是由于一些建筑物的图像也包含街道,这使模型混淆。
- 森林预测几乎是完美的。
- 许多冰川被预测为山脉和海洋,也有许多山脉被预测为冰川。这可能来自冰川、山脉和海洋图像的相同蓝色。
- 海上的好预测。
结论
rbind(
"Simple CNN" = acc_simple,
"Deeper CNN" = acc_big,
"Fine-tuned VGG16" = acc_bigger
)#> Accuracy
#> Simple CNN 0.7423333
#> Deeper CNN 0.8486667
#> Fine-tuned VGG16 0.9246667
我们已经成功地用 6 个类别进行了图像分类:“建筑物”、“森林”、“冰川”、“山”、“海”和“街道”。由于图像是非结构化数据,这个问题可以通过使用神经网络的机器学习来解决,神经网络在没有人工干预的情况下自动进行特征提取。为了获得更好的性能,我们使用卷积神经网络来实现这一点,并继续使用密集层进行预测。最后,我们使用 VGG16 模型和 IMAGENET 权重初始化,达到 92%的准确率。
使用图像分类,拥有大型可视化数据库网站的公司可以轻松地组织和分类他们的数据库,因为它允许对大量图像进行自动分类。这有助于他们将视觉内容货币化,而无需投入大量时间进行手动分类和标记。
🔥你好!如果你喜欢这个故事,想支持我这个作家,可以考虑 成为会员 。每月只需 5 美元,你就可以无限制地阅读媒体上的所有报道。如果你注册使用我的链接,我会赚一小笔佣金。
🔖想了解更多关于经典机器学习模型如何工作以及如何优化其参数的信息?或者 MLOps 大型项目的例子?有史以来最优秀的文章呢?继续阅读:
从零开始的机器学习
View list8 stories
高级优化方法
View list7 stories
MLOps 大型项目
View list6 stories
我最好的故事
View list24 stories
R 中的数据科学
View list7 stories
在 PyTorch 构建卷积 VAE
用神经网络生成新图像?
深度学习在计算机视觉中的应用已经从图像分类等简单任务扩展到自动驾驶等高级任务——神经网络揭示的最迷人的领域之一是图像生成。
随着生成对抗网络(GANs)在内容生成方面的能力和成功,我们经常忽略另一种类型的生成网络:变分自动编码器(VAE)。本文讨论了 VAE 的基本概念,包括体系结构和损失设计背后的直觉,并提供了一个简单卷积 VAE 的基于 PyTorch 的实现,以基于 MNIST 数据集生成图像。
什么是 VAE?
自动编码器
为了理解 VAE 的概念,我们首先描述一个传统的自动编码器及其应用。
图一。传统自动编码器的示意图。
在传统的计算机科学中,我们总是试图找到最佳的方法来将某个文件(无论是图像还是文档)压缩成更小的表示形式。自动编码器是一种特殊类型的神经网络,具有用于降维的瓶颈层,即潜在表示:
其中 x 为原始输入, z 为潜在表示,*x’*为重构输入,函数 f 和 g 分别为编码器和解码器。目标是最小化重构输出 g(f(x)) 和原始 x 之间的差异,以便我们知道较小尺寸的潜在表示 f(x) 实际上保留了足够的特征用于重构。
除了满足降维的需要,自动编码器还可以用于去噪等目的,即,将扰动的 x 输入自动编码器,并让潜在表示学习仅检索图像本身,而不检索噪声。当去噪自动编码器用深度网络构建时,我们称之为堆叠去噪自动编码器。
在简单的词语中添加“变化”
在对自动编码器进行简短描述后,人们可能会问,如何改变这种网络设计来生成内容——这就是“变化”概念的来源。
当我们正则化自动编码器,使其潜在表示不会过度拟合到单个数据点,而是整个数据分布时(关于防止过度拟合的技术,请参考本文),我们可以从潜在空间执行随机采样,从而从分布中生成看不见的图像,使我们的自动编码器变得“可变”。为此,我们将 KL 散度的思想融入到我们的损失函数设计中(关于 KL 散度的更多细节,请参考这篇文章)。下面几节将深入介绍使用 PyTorch 从头开始构建 VAE 的具体过程。
计算环境
图书馆
整个程序仅通过 PyTorch 库(包括 torchvision)构建。在评估结果时,我们还使用 Matplotlib 和 NumPy 库进行数据可视化。这些库可以按如下方式导入:
资料组
为了简化演示,我们从最简单的视觉数据集 MNIST 训练了整个 VAE。MNIST 包含 60000 幅训练图像和 10000 幅测试图像,显示从 0 到 9 的手写数字字符。
硬件要求
由于 MNIST 是一个相当小的数据集,因此可以纯粹在 CPU 上训练和评估网络。然而,当在其他更大的数据集上使用时,建议使用 GPU 进行计算。为了确定是否使用 GPU 进行训练,我们可以首先根据可用性创建一个可变的设备 CPU/GPU:
网络体系结构
图二。VAE 的示意图。
我们的 VAE 结构如上图所示,它包括一个编码器,一个解码器,在两者之间的潜在表示被重新参数化。
编码器— 编码器由两个卷积层组成,后面是两个独立的全连接层,两个层都将卷积后的特征图作为输入。两个全连接层在我们预期的潜在空间的维度上输出两个向量,其中一个是均值,另一个是方差。这是 VAEs 和传统自动编码器之间的主要结构差异。
重新参数化— 通过计算平均值和方差,我们随机抽取一个可能出现在给定分布中的点,该点将被用作潜在表示,并输入解码阶段。
解码器 —该解码器类似于传统的自动编码器,具有一个全连接层,后跟两个卷积层,以基于给定的潜在表示来重建图像。
我们可以使用 PyTorch 构建 VAE 结构的上述组件,如下所示:
培训程序
损失函数
VAE 的核心概念之一是其损失函数的设计。简而言之,我们试图设计这样的损失,即它基于给定的图像重建得很好,但也适合整个分布,而不是仅过度适合图像本身。因此,VAE 损失是以下因素的组合:
二进制交叉熵(BCE)损失— 计算重建图像与原始图像的像素间差异,以最大化重建的相似性。BCE 损失的计算方法如下:
其中 xᵢ 和 x’ᵢ 分别表示原始和重建图像像素(总共 n 个像素)。
KL-散度损失— KL 散度衡量两个分布的相似性。在这种情况下,我们假设分布为正态分布,因此损耗设计如下:
这是通过我们预测的潜在向量(大小为 m )中每个值的平均值和 sigma 来计算的。
培养
下面的代码显示了培训过程。我们将批量大小设置为 128,学习速率设置为 1e-3,总的时期数设置为 10。
注意,为了简单起见,我们在这里只进行纯训练。然而,建议在每个时期之后,我们在测试集上计算有效性,以防止在训练期间的任何过度拟合。当验证损失达到最低点时,也应该保存检查点。
(英)可视化(= visualization)
训练之后,我们可以用下面的代码来可视化结果:
图 3。重建结果。左边是原始图像,右边是生成的图像。
从可视化中我们可以看到,我们已经成功地在原始图形的基础上生成了略有不同的数字图形,这就是 VAE 最终要实现的目标!
结论
所以你有它!希望这篇文章给你一个基本的概述和指导,告诉你如何从头开始构建你的第一个 VAE。完整的实现可以在下面的 Github 资源库中找到:
https://github.com/ttchengab/VAE.git
感谢您坚持到现在🙏!我会在计算机视觉/深度学习的不同领域发布更多内容。一定要看看我的另一篇关于一次性学习的文章!
使用 Plotly 和币安 API 构建加密货币仪表板
轻松跟踪您的投资组合
资料来源:unsplash.com
根据 CoinMarketCap 的数据,截至目前,全球加密货币市场价值超过 1.5 万亿美元。最近,几家银行和信用卡公司批准将加密货币作为其金融产品之一,这预示着加密市场的光明前景。在过去的几年里,越来越多的人开始交易加密货币,全球有许多支持不同加密货币的交易所。在本文中,我们将关注币安交易所,它是当今世界上排名第一的 T2 交易所。它支持几乎所有的加密货币,并在许多国家可用。
在本文中,我们将讨论如何从币安 API 访问数据,并用 Plotly 创建一个仪表板。
下面是我们将要讲述的内容:
- 如何设置币安 API
- 如何使用币安 API 获取数据
- 用 Plotly 构建仪表板
1.如何设置币安 API
币安提供两种 API 访问:1)实际的币安 API 和 2)测试币安 API。
1.1 设置实际的币安 API
实际 API 提供了对您实际账户的直接访问,使用此 API 进行的任何交易都将反映到您的实际账户中。这意味着我们在使用这个 API 时需要小心。
首先,你需要在https://www.binance.com/en/register?ref=AG3W30LV注册币安(如果你没有账户的话)(这个链接包括我的推荐代码,会给你 10%的交易费折扣。更多细节可以在https://www.binance.com/en/activity/referral找到
注册后,您将被要求(或者您可以从安全设置中完成)设置双重身份验证以获得额外的安全性。我会推荐选择 Google Authenticator。
设置好 2FA 后,您可以转到“设置”下的“API 管理”选项卡。您将被要求为您的 API 键提供一个标签(当您有多个键与一个帐户相关联时,这将非常有用)。
为您的 API 键提供一个标签后,单击 create API。您将被要求再次认证,然后您将能够看到您的“API 密钥”和“秘密密钥”这是你唯一一次看到它,所以把你的钥匙复制到某个安全的地方。默认情况下,这些键将被授予以下访问权限,这些权限是可以更改的。
我们将使用实际的 API 和测试 API 键来理解如何使用它们。为此,我们不想搞乱我们的实际帐户,所以我们将把实际的 API keys 权限改为只读。
我们将把实际的 API 密匙保存到下面提到的secret.cfg
文件中(记住,永远不要共享或发布你的secret.cfg
文件)
1.2 设置测试币安 API
测试币安 API 为您提供了与实际 API 交互时相同的感觉。我建议从这个(交易)开始,直到你确信你的应用程序运行良好。
首先,你需要在https://testnet.binance.vision/登录(目前,只有 GitHub 支持登录)
登录后,点击“生成 HMAC_SHA256 密钥”,你将再次被要求提供一个密钥标签。提供标签后,点击 generate,您应该能够看到您的 API 键。把它们复制到某个安全的地方。此外,请阅读主页上关于测试 API 的所有详细信息。
现在,我们将修改secret.cfg
文件以包含测试 API 键,如下所述
我们已经成功设置了实际和测试 API 密钥,并将其保存到secret.cfg
文件中。在下一节中,我们将关注使用这些 API 键获取数据。
2.如何使用币安 API 获取数据
2.1 安装 python-币安库
币安没有提供与 API 交互的 python 库,但是有一个非常著名的第三方库叫做python-binance
,我们将用它来与 API 交互。
安装python-binance
库
$ pip install python-binance
2.2 获取账户信息
默认情况下,您将在您的测试帐户中以不同加密货币的形式获得一些余额,我们将在这一部分使用测试 API(因为我不想分享我的帐户信息)。另外,python-binance
不能访问测试 API,所以我们需要更改端点 URL。
下面是获取测试帐户信息的代码
从上面的输出可以看出,它显示了一些重要的东西,比如 accountType、balances、permissions 等。
现在,让我们得到 ETH 平衡。
你可以用python-binance
库做很多事情,详细的文档可以在这里找到。
2.3 获取历史数据
根据我的观察,测试 API 不提供真实的历史数据;相反,它提供虚拟数据。因此,为了访问真实数据,我们将使用实际的 API 和实际的 API 键。
获取从最早可用日期(币安)到现在的 ETH 价格
上述输出代表了币安 API 文档中提到的以下参数
将输出转换为数据框并保存为 CSV 文件
2.4 获取实时数据
我们可以使用币安的 WebSocket 来传输实时数据。这是你怎么做的
现在,要停止数据流并关闭 WebSocket
看起来我们已经找到了获取数据和信息的不同方法。你可以用python-binance
库和币安 API 做很多其他的事情。我鼓励你看一看这个和这个。现在,我们可以在下一节构建一个 Plotly 仪表板了。
3.用 Plotly 构建仪表板
在本节中,我们将使用 Plotly 构建一个仪表板,该仪表板将实时跟踪我们的测试帐户组合,并根据实时流数据更改帐户总价值。
这是我们最终的仪表板的样子(不要介意美观,因为您可以在以后更改它)
GIF:最终仪表板
如您所见,我们在控制面板中包括了以下功能。
- 指标-USDT 的投资组合总价值
- 指标-BTC 的投资组合总价值
- 指标- BNB/USDT 换算
- 饼图-投资组合分布(在 USDT)
- 条形图-令牌分布
让我们看看代码。
首先,我们将导入所有需要的库。
其次,我们将读取我们的密钥,建立连接,并获得帐户信息。
第三,我们将定义一些函数来处理流数据并基于实时数据计算指标。
第四,我们将开始流式传输数据。
第五,我们将定义我们的仪表板布局、图表并托管它
就这样,你应该能够跟踪你的测试账户投资组合。您可以使用实际的密钥轻松地为实际帐户设置它,而无需更改端点 URL。
这就把我们带到了本文的结尾。你可以访问这个 GitHub repo 中的所有代码。请随意标记或收藏,以供将来参考。
最近开始用 Python 和机器学习写算法交易系列。你可以在下面找到第一条。
https://pub.towardsai.net/algorithmic-trading-with-python-and-machine-learning-part-1-47c56706c182
更多文章关注我。请随时在 LinkedIn 上联系我。谢谢大家!
注来自《走向数据科学》的编辑: 虽然我们允许独立作者根据我们的 规则和指导方针 发表文章,但我们不认可每个作者的贡献。你不应该在没有寻求专业建议的情况下依赖一个作者的作品。详见我们的 读者术语 。