2018年5月1日星期二

微软提出基于程序图简化程序分析,直接从源代码中学习

程序分析通常有两种方法,分别基于数理逻辑和自然语言理解。通过将程序表示成图结构,来自微软研究院和西门菲莎大学的研究者展示了一种结合二者的新方法,可以直接从源代码中学习,且更准确地查找已发布软件中的 bug。

过去五年,基于深度学习的方法给大量应用带来了变革,如需要理解图像、话语和自然语言的应用。对于计算机科学家而言,一个自然出现的问题是:计算机是否能够学会理解源代码。乍一看这个问题似乎很简单,因为编程语言的设计初衷就是被计算机理解。但是,很多软件 bug 的出现是因为我想让软件这么做,但是写出来却是另外一回事。也就是说,小的拼写错误可能导致严重后果。

看一下以下这个简单示例:

float getHeight { return this.width; }.

该示例中,人类或者理解「height」和「width」意思的系统可以很快发现问题所在。源代码具备两种功能。首先,它与计算机进行准确交流,以执行硬件指令。其次,它与其他程序员(或源代码作者)针对程序的运行情况进行交流。后者通过选择代号、代码布局和代码注释来实现。在发现两种交流渠道似乎可以分离后,一个自动发现软件 bug 的系统出现了。

之前的程序分析主要关注程序的正式、机器可理解语义或将程序看作(有点奇怪的)自然语言。前者的方法来自于数理逻辑,要求对每个需要处理的新案例进行大量的工程工作。而自然语言方法需要在纯句法任务上性能优越但尚无法学习程序语义的自然语言处理工具。

在 ICLR 2018 的一篇论文《Learning to Represent Programs with Graphs》中,来自微软研究院和西门菲莎大学的研究者展示了一种结合二者的新方法,并展示了如何查找已发布软件中的 bug。

程序图

为了学习源代码中的大量结构,研究者首先把源代码转换成程序图( program graph)。图中的节点包括程序的 token(即变量、运算子、方法名等)及其抽象句法树的节点(定义语言句法的语法元素,如 IfStatement)。程序图包含两种不同类型的边:句法边,表示代码解析方式,如 while loop 和 if block;语义边,即简单程序分析的结果。

图 1:句法边

句法边包括简单的「NextToken」边、用于表示抽象句法树的「Child」边,以及连接一个 token 和源代码中它最后一次出现的「LastLexicalUse」边。图 1 展示了此类边用于 statement Assert.NotNull(clazz) 的部分示例,其中对应 token 的节点是灰色框,对应程序语法的非终端的节点是蓝色椭圆形框。Child 边是蓝色的实线边,而 NextToken 边是黑色的双线边。

语义边包括连接一个变量和它在程序执行中最后一次使用的「LastUse」边(如果是循环案例,则变量在程序执行中最后一次使用的情况出现得更晚一些)、连接一个变量和它最后一次写入的「LastWrite」边,以及连接一个变量和它据此计算的值的「ComputedFrom」边。也可能有更多语义边,利用程序分析工具箱的其他工具,如 aliasing、points-to 分析,以及程序条件。图 2 是在一个小代码段(黑色)上形成的一些语义边。

图 2:语义边

LastUse 关系用绿色边表示,y 与循环前 y 最后一次使用的情况连接。类似地,LastWrite 关系用红色边表示,while 条件中的 x 的使用与循环前 x 的分配和循环中 x 的分配连接起来。最后,ComputedFrom 关系用蓝色边表示,变量与其据此计算的变量连接起来。

句法边大概对应程序员在阅读源代码时所看到的。语义边对应程序如何执行。通过在一个图中结合二者,该系统可以比单一方法学习到更多的信息。

从图中学习

由于图通常作为表征数据和数据关系的标准方式,从图结构数据中学习的方法近期受到了一定程度的关注。一个组织可以用图的形式展现出来,正如药物分子可以看成是原子构成的图。近期成功的应用深度学习的图方法是图卷积网络(卷积神经网络的一种扩展)和门控图神经网络(循环神经网络的一种扩展)。

这两种方法都是首先独立地处理每个节点,以获取节点本身的内部表征(即低维空间中的一个向量),然后将互相连接的节点的表征进行重复连接(两种方法的组合方式不同)。因此,经过一个步骤之后,每个节点拥有自身的信息和它的直接近邻节点的信息;经过两个步骤之后,每个节点将获得距离两个节点的信息,以此类推。由于所有的步骤都使用(小型)神经网络,因此这些方法可以被训练用于从数据中提取整个任务相关的信息。

搜索 bug

在程序图上学习可以用于搜索 bug,例如本文开头描述的那个例子。给定一个程序、程序中的某个位置以及在该位置上可以使用的一系列变量。然后模型被询问应该使用哪些变量。为了执行这项任务,程序被变换为程序图,某个特定节点对应所考虑的位置。通过考虑该特定节点的计算表征,以及对应可用变量的节点表征,网络可以计算每个变量的可能性。这样的模型可以很容易地通过几百万行已有代码来训练,并且不需要专门标注的数据集。

当模型在新代码上运行,并以很高的概率预测出 var1,然而程序员选择的是 var2,这可能就是一个 bug。通过标记这些问题让人类专家审核,可以发现真正的 bug。例如,以下来自 Roslyn(微软 C# 编译器)的例子:

注意参数 filepath 和字段_filePath 的使用,二者很容易被混淆。然而,_filePath 只是一个打字错误,开发者在研究员报告这个问题和类似问题之后将其修改了。相似的 bug 在很多其它 C# 项目中也被找到、报告和修改。

在一个更大规模的定量评估中,新方法远远超越了传统的机器学习技术。作为基线方法,双向循环神经网络(BiRNN)直接在源代码上执行,BiRNN 的简单扩展可以访问数据流的某些信息。为了评估不同的模型,微软分析了包含 290 万行源代码的开源 C# 项目。在测试时,源代码的某个变量被遮盖,然后让模型找出原始使用的变量(假定源代码是准确并经过良好测试的)。在第一个实验中,模型在项目的留出文件上进行测试(其他文件用于训练)。在第二个实验中,模型在全新项目的数据上测试。结果如下表所示,在新的程序图上学习的模型得到了明显更好的结果。

未来应用

程序图对于在程序上应用深度学习方法是很通用的工具,微软将继续朝这个方向探索。

原文地址:https://ift.tt/2JIKB2o

]]> 原文: https://ift.tt/2IagCn2
RSS Feed

机器知心

IFTTT

用摄像头和Tensorflow.js在浏览器上实现目标检测

Tensorflow.js 是一个能在你的浏览器里运行的全新深度学习库。本文将会介绍从原生 Tiny YOLO Darknet 模型到 Keras 的转换,再到 Tensorflow.js 的转换,如何利用其作一些预测,在编写 Tensorflow.js 遇到的一些问题,以及介绍使用联网摄像头/图像轻松地进行预测检测。

项目地址:https://ift.tt/2EAH1V0

YOLO9000:更好,更快,更强

严格意义上来讲这才是原文的标题。我们将会使用 Tiny YOLO,一个快速的、能在 200 FPS 下运行的目标检测模型。我们将会使用 Tiny YOLO 而非完整的 YOLOv2。为什么?首先,YOLOv2 只在功能强大的桌面级设备上以 40 帧每秒的速度运行,而这个条件大多数用户都达不到。同时,YOLOv2 的模型文件是 Tiny YOLO 的五倍大,这将会导致网络卡死。最后,YOLOv2 有一个暂时不被原生 Tensorflow.js 支持的重组层(reorg layer)。下一步,我们将重点放在 Tiny YOLO 的网络配置(神经网络结构)以及权重上。

YAD2K:又一个 DARKNET 2 KERAS(转换器)

你可能已经注意到 YOLO 是用 Darknet 编写的,而 Darknet 听起来和 Tensorflow 没什么关系。所以我们的第一步就是将 YOLO 模型转换为更加 Tensorflow 式的东西,在我们的例子中,这个东西是 Keras。Keras 是一个更高级的深度学习框架。我们推荐使用此种方式(https://js.tensorflow.org/tutorials/import-keras.html)将你的权重转换为 Tensorflow.js 的格式。

我们将会使用 YAD2K 来将 Darknet 模型转换为 Keras 模型。请按照此处的教程来安装 YAD2K(https://github.com/allanzelener/YAD2K#installation)。

当然了,你也可以假装你已经安装好了然后使用我已经给出的最终权重文件(https://modeldepot.io/mikeshi/tiny-yolo-in-javascript),但是这样的话你会失去不少乐趣!

现在,我们将要修复 YAD2K 里的一个 Bug 来正确加载 Tiny YOLO。

用你最喜欢的文本编辑器打开 yad2k.py,在第 83 行,将 buffer=weights_file.read(16) 改为 buffer=weights_file.read(20)。为什么?现在毫无头绪。

现在在你的电脑终端运行下列指令。这些指令会下载 Tiny YOLO 权重以及配置文件同时会将转换后的模型文件输出到 model_data/yolov2-tiny.h5。

wget https://pjreddie.com/media/files/yolov2-tiny.weights wget https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov2-tiny.cfg ./yad2k.py yolov2-tiny.cfg yolov2-tiny.weights model_data/yolov2-tiny.h5

之后,我们要保证新模型能运转起来。一个有趣的事实是:之前我忘了做这一步然后我花了一整天纠结为什么 Tensorflow.js 不工作。

./test_yolo.py model_data/yolov2-tiny.h5

这个模型会输出一些预测。

TENSORFLOW.JS 转换器:我们最后的权重转换方式

这一步更加简单,我保证!查看此处的官方指导(https://js.tensorflow.org/tutorials/import-keras.html)。我已经把所有的难题解决了,所以如果你还觉得这些看起来太多了的话,你可以直接跳过这一步。

我们将要安装官方转换工具。在终端运行 pip install tensorflowjs 指令来安装转换器。

现在要转换我们的模型,再次使用终端!

tensorflowjs_converter --input_format keras \    model_data/yolov2-tiny.h5 \    tfjs_model_data

现在我们终于将模型文件转移到 tfjs_model_data 中去了!注意,那个文件夹还包含了 model.json 以及一些其他的碎片文件。model.json 告诉 Tensorflow.js 神经网络的结构是怎样的以及哪些碎片文件与哪些权重相对应。这些碎片文件包含了模型的权重。要保证这些碎片文件和 model.json 在同一个目录下,不然你的模型将会无法正确加载。

Tensorflow.js 

现在,我们到了有趣的部分。我们将同时处理 ML 和 JS 代码!

首先,我们导入 Tensorflow 然后加载模型。

import * as tf from '@tensorflow/tfjs'; const model = await tf.loadModel(url);

等等,什么是'url'?你可以使用此处的托管模型文件(https://raw.githubusercontent.com/MikeShi42/yolo-tiny-tfjs/master/model2.json)或者把路径加到你转换后的文件中去。

完美,现在我们做些真正的深度学习了:

function yolo (input) {   return model.predict(input); }

啊哈。这看起来好像也没什么特别的,等等。我们忘了将输出转换成边框、分类标签以及概率!

将数字转换为边框以及数字

我不准备深入探讨这个话题,因为对 YOLO 的后处理可以拿来单独写几篇博客了。相反,我会专注于我在将 Python 版本转换到 Javascript 时遇到的难题。

1. 一部分张量操作不可行。

Tensorflow.js 还很年轻,因此有一些功能暂时不可用,例如 boolean mask 以及 NMS。你可能也会遇到这样的问题,不过幸运的是你可以绕过它,通过下列方式:

const expected_shape = tensor.shape; const data = await tensor.data(); // Async transfer from GPU to CPU // Note: data is a flattened TypedArray of the original tensor //... data manipulation in JS on CPU tf.tensor1d(data).reshape(expected_shape); // Shape it back into where we were before

2. 你不能像 Python 那样使用 -1 索引

在 python 中,你可以使用 -1 作为「最后一个元素」。不幸的是,在 Tensorflow.js 中,如果你在 tf.slice 上尝试这样操作,它只会默默地失败。

3. 你不能使用 5d 张量

注意,Tensorflow,js 的 WebGL 后端不支持 5d 张量。既然无法想象五维的样子,为什么还要使用它们?在 YOLO 中我们将输出重塑为 [batch_size,xy,wh,box_confidence,box_class_pred]。这个很显然是五维的。幸运的是,为了避免这些麻烦,我删除了 batch_size。另一种做法是不将输出重塑为 5d 张量。但是有些细节还需要谨慎处理。

除此之外,我的使用体验是连贯的。我只需要重新实现两个通常情况下 Tensorflow 不使用的 CPU 算法(传统 JS)。

NPM 安装

现在把我们的深度学习应用到程序中去吧!

幸运的是,有人已经写好了代码,所以只需在终端安装 NPM 即可:

npm i tfjs-yolo-tiny

然后我们再稍微利用一下 Javascript:

import yolo, { downloadModel } from 'tfjs-yolo-tiny'; const model = await downloadModel(); const inputImage = webcam.capture(); const boxes = await yolo(inputImage, model);

哇,你说的这个 webcam.capture() 是什么?

除了 NPM 的安装之外...

你可能已经注意到我们还没有关注过输入到我们的 YOLO 中的到底是什么。这也是 Tensorflow.js 最棒的部分之一。

我们可以从文件(DOM)中取出视频或者图像然后将其转换为一个张量!

我们可以用 Javascript 写下这些:

import yolo, { downloadModel } from 'tfjs-yolo-tiny'; const model = await downloadModel(); const inputImage = webcam.capture(); const boxes = await yolo(inputImage, model);

棒!现在可以将图像作为张量输入!之后,从图像切换到网络摄像头,你只需将其指向正确的元素即可。这对我来说很神奇。

在这之后,我们要做一些预处理。在这种情况下,将其裁剪为正方形,调整大小为 416x416,然后除以 255 得到大小范围从 0 到 1 的像素值。

最后的一些想法

我们已经介绍了如何将模型转换为 Tensorflow.js 格式,加载模型并用它进行预测。然后,我们探讨了在 Tensorflow.js 中编写后处理代码的一些难题,但我们解决了这些问题。现在我们知道如何通过静态图像或网络摄像头抓取数据,可以将大多数 ML 模型从 Python 转换为 Tensorflow.js 并在浏览器中运行它们。

原文地址:https://ift.tt/2HhxDrn


]]> 原文: https://ift.tt/2FAwBoO
RSS Feed

机器知心

IFTTT

CVPR 2018:解耦神经网络DCNet,性能优于标准CNN

本论文提出一个通用的解耦学习框架,并构建了解耦神经网络 DCNet,实验表明解耦操作可大大提高模型性能,加速收敛,提升稳健性。这篇论文已被 CVPR 2018 接收,是大会的 Spotlight 论文。

卷积神经网络(CNN)大大拓展了视觉任务的边界,如目标识别 [24, 25, 5]、目标检测 [2, 23, 22]、语义分割 [16] 等。最近,CNN 领域一个重要研究方向是:通过引入捷径连接(shortcut connection)[5, 8] 、多分支卷积(multi-branch convulsion)[25, 30] 等改进架构,来增加网络的深度和表征能力。但另一方面,尽管 CNN 有了诸多改进和提升,对于卷积本身为何能够实现判别表征和优秀的泛化能力,这依然是一个有趣的、值得探索的问题。

如今,CNN 中通常利用内积来编码表示 patch x 和卷积核 w 之间的相似性。但内积公式 将语义差异(即类间差异)和类内差异耦合到同一个度量中。因此,当两个样本间的内积很大时,我们很难确定是由于两个样本间存在很大的语义/标签差异,还是由于存在很大的类内差异。为了更好地研究 CNN 表征的性质,进而改善现有的 CNN 框架,本文作者提出明确地解耦(decouple)语义差异和类内差异。具体而言,研究者使用范数和角度(angle)将内积重新参数化为:该直觉来自图 1 中的观察,其中角度表示语义/标签差异,而特征范数(feature norm)则表示类内差异。特征范数越大,则预测越可信。这种直观的解耦方法启发研究者提出了解耦卷积算子(decoupled convolution operator)。研究者希望,通过将内积解耦为范数和角度,能够更好地对深度网络中的类内差异和语义差异进行建模。

基于将内积解耦为范数和角度这一思路,研究者通过将传统的基于内积的卷积算子(||w|| ||x|| cos(θ_(w,x)))扩展至解耦算子,提出了一个全新的解耦神经网络 DCNet。为此,研究者将此类解耦算子定义为:某个范数函数 h(||w||, ||x||) 与某个角度函数 g(θ_(w,x)) 的乘积形式。解耦算子为更好地建模类内差异和语义差异提供了一个通用框架,原始的 CNN 等价于将 h(||w||, ||x||) 设置为 ||w|| ||x||,将 g(θ_(w,x)) 设置为 cos(θ_(w,x))。(在解耦算子中),幅度函数(magnitude function)h(||w||, ||x||) 建模类内差异,而角度函数(angular function)g(θ_(w,x)) 则建模语义差异。

从解耦的角度看,原始 CNN 包含了一个很强大的假设:类内差异可通过范数乘积的形式进行线性建模,而语义差异可利用角度余弦值刻画。但这个建模方法并非在所有任务中都是最优的,而通过解耦学习框架,我们可以根据任务本身设计解耦算子,或者直接从数据中「学习」出来。DCNet 共有以下四个优点:一,DCNet 不仅允许我们使用一些替代函数更好地建模类内差异和语义差异,还允许我们直接学习这些函数,而不是修复它们。二,通过使用有界幅度函数,DCNet 可以改善 [14] 中分析的问题,进而实现更快的收敛,同时取得和原始 CNN 相当甚至更好的准确率。三,DCNet 的一些实例展现出了面对对抗攻击时更强的稳健性:通过一个有界函数 h(·) 压缩各类别的特征空间,可以获得一定的稳健性。四,解耦算子具有很强的灵活性,且是架构不敏感的(architecture-agnostic),因此我们可以很轻松地将其引入各种网络架构,如 VGG [24]、GooleNet [25] 以及 ResNet [5]。

具体而言,研究者提出了两种不同的解耦卷积算子:有界算子和无界算子,并利用两种算子完成多个实例。结果显示,有界算子具有更快的收敛速度,且在对抗攻击中具有更好的稳健性;而无界算子则具有更好的表征能力。解耦算子可以是平滑或非平滑的,平滑与否会影响其表现。另外,研究者针对解耦算子提出了一个全新的概念:算子半径(operator radius)。算子半径刻画了幅度函数 h(·) 对输入 ||x|| 的导数的重大变化。通过利用反向传播算法联合学习算子半径,研究者还提出了可学习的解耦算子。最后,研究者展示了多种通过改进标准反向传播算法优化解耦算子的替代方式。本论文的主要贡献如下:

  • CNN 学得的特征是天然解耦的,受此启发,研究者提出了一个明确解耦的框架来研究神经网络。

  • 研究者展示了 CNN 中包含了一个很强大的假设,以完成对类内差异和类间差异的建模,而此假设可能不是最优的。通过对内积解耦,研究者能够针对不同任务设计出更有效的幅度函数和角度函数,而非使用原始的卷积。

  • 和标准 CNN 相比,DCNet 能够更容易地收敛,且具有更好的准确率和稳健性。

图 1:CNN 学得的特征天然是解耦的。图中的 2D 特征是通过将 CNN 特征维度设置为 2 直接得到的输出。

图 2:解耦卷积算子的几何解释。绿线表示原始向量,红线表示投影向量。

表 1:加权算子(TanhConv)在 CIFAR-100 上的评估结果。

表 2:未使用反向传播的原始 CNN-9 在 CIFAR-100 上的测试误差(%)。「N/C」表示模型未收敛,「-」表示没有结果。不同列中的结果来自于不同的角度激活函数(angular activation)。

表 6:标准 ResNet-18 和修正 ResNet-18 在 ImageNet-2012 上的 Center-crop Top-5 误差(%)。「*」表示使用了原始 CNN 在 ImageNet-2012 上的预训练模型作为初始模型(见 4.3)。

表 7:CIFAR-10 上的白盒(White-box)攻击,性能用准确率(%)度量。前三行是标准训练模型的结果,后三行是对抗训练模型的结果。

表 8:CIFAR-10 上的黑盒(Black-box)攻击,性能用准确率(%)度量。前三行是标准训练模型的结果,后三行是对抗训练模型的结果。

论文:Decoupled Networks

论文链接:https://ift.tt/2JEqUZo

摘要:长期以来,基于内积的卷积都是卷积神经网络(CNN)中的核心成分,也是学习视觉表征的关键所在。而 CNN 学得的特征天然能够解耦为特征范数(对应类内差异)和角度(对应语义差异),受此启发,我们提出了一个通用的解耦学习框架,对类内差异和语义差异分别进行建模。具体而言,我们首先将内积重新参数化为解耦形式,然后将其扩展至解耦卷积算子,并作为解耦神经网络的重要组成成分。我们展示了解耦卷积算子的多个有效实例,其中每个解耦算子都有很好的根据以及直观的几何解释。基于这些解耦算子,我们进一步提出直接从数据中学习算子。大量实验表明,这种解耦重新参数化操作极大地提升了模型性能,加快了模型的收敛速度,显著提升了模型的稳健性。

]]> 原文: https://ift.tt/2JMur8j
RSS Feed

机器知心

IFTTT

Google开放最大目标检测数据集,还要为它举办AI挑战赛

梅想好 发自 SSJQ

量子位 报道 | 公众号 QbitAI

Google推出的Open Images数据集又更新了。

最新发布的Open Images V4包含190万图像、600个种类,1540万个bounding-box标注,是当前最大的带物体位置标注信息的数据集

Open Images数据集的标注图像

Google方面称,为了保持标注的高质量,这些图像大部分由专业标注人员手动完成,图像丰富多样,通常还包含了具有多个对象的复杂场景——平均每张图像8个以上。

Open Images挑战赛

同时,结合Open Images V4版本的发布,Google还将举办一个Open Images挑战赛,该挑战赛将在ECCV 2018(2018年欧洲计算机视觉会议)上进行。

Google方面还说,Open Images挑战赛将遵循PASCAL VOC、ImageNet和COCO的挑战赛模式,但规模会更大,挑战也将是独一无二的。

比如:

  • 训练集包含500类170万张图片,带有1220万处bounding-box标注;
  • 比其他挑战赛有更广泛的检测类别,比如"雪人"等新对象;
  • 除了物体检测,还设置了视觉关系检测,用来识别特定关系中的物体对,如"女孩弹吉他"。

好消息是训练集已经开放,训练现在就可以开始了。

Kaggle将在今年7月1日发布一组10万规模的测试集,并于2018年9月1日截止提交结果。

Google表示很期待有更多令人惊喜的检测模型诞生,也认为大量带物体位置的标准图像,可以对视觉关系检测有帮助——他们坚信这会是一个越来越热的研究课题。

需要说明的是,Open Images V4中还包含了19,794类3010万个人工审核的图片级标签,但此次不"参赛"。

传送门

Open Images数据集&挑战赛:

Open Images Dataset V4

欢迎大家关注我们的专栏:量子位 - 知乎专栏

诚挚招聘

量子位正在招募编辑/记者,工作地点在北京中关村。期待有才气、有热情的同学加入我们!相关细节,请在量子位公众号(QbitAI)对话界面,回复"招聘"两个字。

量子位 QbitAI · 头条号签约作者

վ'ᴗ' ի 追踪AI技术和产品新动态



via 量子位 - 知乎专栏 https://ift.tt/2HF07Qp
RSS Feed

RSS5

IFTTT

Google开放最大目标检测数据集,还要为它举办AI挑战赛

梅想好 发自 SSJQ

量子位 报道 | 公众号 QbitAI

Google推出的Open Images数据集又更新了。

最新发布的Open Images V4包含190万图像、600个种类,1540万个bounding-box标注,是当前最大的带物体位置标注信息的数据集

Open Images数据集的标注图像

Google方面称,为了保持标注的高质量,这些图像大部分由专业标注人员手动完成,图像丰富多样,通常还包含了具有多个对象的复杂场景——平均每张图像8个以上。

Open Images挑战赛

同时,结合Open Images V4版本的发布,Google还将举办一个Open Images挑战赛,该挑战赛将在ECCV 2018(2018年欧洲计算机视觉会议)上进行。

Google方面还说,Open Images挑战赛将遵循PASCAL VOC、ImageNet和COCO的挑战赛模式,但规模会更大,挑战也将是独一无二的。

比如:

  • 训练集包含500类170万张图片,带有1220万处bounding-box标注;
  • 比其他挑战赛有更广泛的检测类别,比如"雪人"等新对象;
  • 除了物体检测,还设置了视觉关系检测,用来识别特定关系中的物体对,如"女孩弹吉他"。

好消息是训练集已经开放,训练现在就可以开始了。

Kaggle将在今年7月1日发布一组10万规模的测试集,并于2018年9月1日截止提交结果。

Google表示很期待有更多令人惊喜的检测模型诞生,也认为大量带物体位置的标准图像,可以对视觉关系检测有帮助——他们坚信这会是一个越来越热的研究课题。

需要说明的是,Open Images V4中还包含了19,794类3010万个人工审核的图片级标签,但此次不"参赛"。

传送门

Open Images数据集&挑战赛:

Open Images Dataset V4

欢迎大家关注我们的专栏:量子位 - 知乎专栏

诚挚招聘

量子位正在招募编辑/记者,工作地点在北京中关村。期待有才气、有热情的同学加入我们!相关细节,请在量子位公众号(QbitAI)对话界面,回复"招聘"两个字。

量子位 QbitAI · 头条号签约作者

վ'ᴗ' ի 追踪AI技术和产品新动态



via 量子位 - 知乎专栏 https://ift.tt/2HF07Qp
RSS Feed

RSS5

IFTTT

2018年4月30日星期一

如何写一手漂亮的模型:面向对象编程的设计原则综述

面向对象的编程在实现想法乃至系统的过程中都非常重要,我们不论是使用 TensorFlow 还是 PyTorch 来构建模型都或多或少需要使用类和方法。而采用类的方法来构建模型会令代码非常具有可读性和条理性,本文介绍了算法实现中使用类和方法来构建模型所需要注意的设计原则,它们可以让我们的机器学习代码更加美丽迷人。

大多数现代编程语言都支持并且鼓励面向对象编程(OOP)。即使我们最近似乎看到了一些偏离,因为人们开始使用不太受 OOP 影响的编程语言(例如 Go, Rust, Elixir, Elm, Scala),但是大多数还是具有面向对象的属性。我们在这里概括出的设计原则也适用于非 OOP 编程语言。

为了成功地写出清晰的、高质量的、可维护并且可扩展的代码,我们需要以 Python 为例了解在过去数十年里被证明是有效的设计原则。

对象类型

因为我们要围绕对象来建立代码,所以区分它们的不同责任和变化是有用的。一般来说,面向对象的编程有三种类型的对象。

1. 实体对象

这类对象通常对应着问题空间中的一些现实实体。比如我们要建立一个角色扮演游戏(RPG),那么简单的 Hero 类就是一个实体对象。

class Hero:    def __init__(self, health, mana):        self._health = health        self._mana = mana    def attack(self) -> int:        """        Returns the attack damage of the Hero        """        return 1    def take_damage(self, damage: int):        self._health -= damage    def is_alive(self):        return self._health > 0

这类对象通常包含关于它们自身的属性(例如 health 或 mana),这些属性根据具体的规则都是可修改的。

2. 控制对象(Control Object)

控制对象(有时候也称作管理对象)主要负责与其它对象的协调,这是一些管理并调用其它对象的对象。我们上面的 RPG 案例中有一个很棒的例子,Fight 类控制两个英雄,并让它们对战。

class Fight:    class FightOver(Exception):        def __init__(self, winner, *args, **kwargs):            self.winner = winner            super(*args, **kwargs)    def __init__(self, hero_a: Hero, hero_b: Hero):        self._hero_a = hero_a        self._hero_b = hero_b        self.fight_ongoing = True        self.winner = None    def fight(self):        while self.fight_ongoing:            self._run_round()        print(f'The fight has ended! Winner is #{self.winner}')    def _run_round(self):        try:            self._run_attack(self._hero_a, self._hero_b)            self._run_attack(self._hero_b, self._hero_a)        except self.FightOver as e:            self._finish_round(e.winner)    def _run_attack(self, attacker: Hero, victim: Hero):        damage = attacker.attack()        victim.take_damage(damage)        if not victim.is_alive():            raise self.FightOver(winner=attacker)    def _finish_round(self, winner: Hero):        self.winner = winner        self.fight_ongoing = False

在这种类中,为对战封装编程逻辑可以给我们提供多个好处:其中之一就是动作的可扩展性。我们可以很容易地将参与战斗的英雄传递给非玩家角色(NPC),这样它们就能利用相同的 API。我们还可以很容易地继承这个类,并复写一些功能来满足新的需要。

3. 边界对象(Boundary Object)

这些是处在系统边缘的对象。任何一个从其它系统获取输入或者给其它系统产生输出的对象都可以被归类为边界对象,无论那个系统是用户,互联网或者是数据库。

class UserInput:    def __init__(self, input_parser):        self.input_parser = input_parser    def take_command(self):        """        Takes the user's input, parses it into a recognizable command and returns it        """        command = self._parse_input(self._take_input())        return command    def _parse_input(self, input):        return self.input_parser.parse(input)    def _take_input(self):        raise NotImplementedError() class UserMouseInput(UserInput):    pass class UserKeyboardInput(UserInput):    pass class UserJoystickInput(UserInput):    pass

这些边界对象负责向系统内部或者外部传递信息。例如对要接收的用户指令,我们需要一个边界对象来将键盘输入(比如一个空格键)转换为一个可识别的域事件(例如角色的跳跃)。

Bonus:值对象(Value Object)

价值对象代表的是域(domain)中的一个简单值。它们无法改变,不恒一。

如果将它们结合在我们的游戏中,Money 类或者 Damage 类就表示这种对象。上述的对象让我们容易地区分、寻找和调试相关功能,然而仅使用基础的整形数组或者整数却无法实现这些功能。

class Money:    def __init__(self, gold, silver, copper):        self.gold = gold        self.silver = silver        self.copper = copper    def __eq__(self, other):        return self.gold == other.gold and self.silver == other.silver and self.copper == other.copper    def __gt__(self, other):        if self.gold == other.gold and self.silver == other.silver:            return self.copper > other.copper        if self.gold == other.gold:            return self.silver > other.silver        return self.gold > other.gold    def __add__(self, other):        return Money(gold=self.gold + other.gold, silver=self.silver + other.silver, copper=self.copper + other.copper)    def __str__(self):        return f'Money Object(Gold: {self.gold}; Silver: {self.silver}; Copper: {self.copper})'    def __repr__(self):        return self.__str__() print(Money(1, 1, 1) == Money(1, 1, 1)) # => True print(Money(1, 1, 1) > Money(1, 2, 1)) # => False print(Money(1, 1, 0) + Money(1, 1, 1)) # => Money Object(Gold: 2; Silver: 2; Copper: 1)

它们可以归类为实体对象的子类别。

关键设计原则

设计原则是软件设计中的规则,过去这些年里已经证明它们是有价值的。严格地遵循这些原则有助于软件达到一流的质量。

抽象(Abstraction)

抽象就是将一个概念在一定的语境中简化为原始本质的一种思想。它允许我们拆解一个概念来更好的理解它。

上面的游戏案例阐述了抽象,让我们来看一下 Fight 类是如何构建的。我们以尽可能简单的方式使用它,即在实例化的过程中给它两个英雄作为参数,然后调用 fight() 方法。不多也不少,就这些。

代码中的抽象过程应该遵循最少意外(POLA)的原则,抽象不应该用不必要和不相关的行为/属性。换句话说,它应该是直观的。

注意,我们的 Hero#take_damage() 函数不会做一些异常的事情,例如在还没死亡的时候删除角色。但是如果他的生命值降到零以下,我们可以期望它来杀死我们的角色。

封装

封装可以被认为是将某些东西放在一个类以内,并限制了它向外部展现的信息。在软件中,限制对内部对象和属性的访问有助于保证数据的完整性。

将内部编程逻辑封装成黑盒子,我们的类将更容易管理,因为我们知道哪部分可以被其它系统使用,哪些不行。这意味着我们在保留公共部分并且保证不破坏任何东西的同时能够重用内部逻辑。此外,我们从外部使用封装功能变得更加简单,因为需要考虑的事情也更少。

在大多数编程语言中,封装都是通过所谓的 Access modifiers(访问控制修饰符)来完成的(例如 private,protected 等等)。Python 并不是这方面的最佳例子,因为它不能在运行时构建这种显式修饰符,但是我们使用约定来解决这个问题。变量和函数前面的_前缀就意味着它们是私有的。

举个例子,试想将我们的 Fight#_run_attack 方法修改为返回一个布尔变量,这意味着战斗结束而不是发生了意外。我们将会知道,我们唯一可能破坏的代码就是 Fight 类的内部,因为我们是把这个函数设置为私有的。

请记住,代码更多的是被修改而不是重写。能够尽可能清晰、较小影响的方式修改代码对开发的灵活性很重要。

分解

分解就是把一个对象分割为多个更小的独立部分,这些独立的部分更易于理解、维护和编程。

试想我们现在希望 Hero 类能结合更多的 RPG 特征,例如 buffs,资产,装备,角色属性。

class Hero:    def __init__(self, health, mana):        self._health = health        self._mana = mana        self._strength = 0        self._agility = 0        self._stamina = 0        self.level = 0        self._items = {}        self._equipment = {}        self._item_capacity = 30        self.stamina_buff = None        self.agility_buff = None        self.strength_buff = None        self.buff_duration = -1    def level_up(self):        self.level += 1        self._stamina += 1        self._agility += 1        self._strength += 1        self._health += 5    def take_buff(self, stamina_increase, strength_increase, agility_increase):        self.stamina_buff = stamina_increase        self.agility_buff = agility_increase        self.strength_buff = strength_increase        self._stamina += stamina_increase        self._strength += strength_increase        self._agility += agility_increase        self.buff_duration = 10  # rounds    def pass_round(self):        if self.buff_duration > 0:            self.buff_duration -= 1        if self.buff_duration == 0:  # Remove buff            self._stamina -= self.stamina_buff            self._strength -= self.strength_buff            self._agility -= self.agility_buff            self._health -= self.stamina_buff * 5            self.buff_duration = -1            self.stamina_buff = None            self.agility_buff = None            self.strength_buff = None    def attack(self) -> int:        """        Returns the attack damage of the Hero        """        return 1 + (self._agility * 0.2) + (self._strength * 0.2)    def take_damage(self, damage: int):        self._health -= damage    def is_alive(self):        return self._health > 0    def take_item(self, item: Item):        if self._item_capacity == 0:            raise Exception('No more free slots')        self._items[item.id] = item        self._item_capacity -= 1    def equip_item(self, item: Item):        if item.id not in self._items:            raise Exception('Item is not present in inventory!')        self._equipment[item.slot] = item        self._agility += item.agility        self._stamina += item.stamina        self._strength += item.strength        self._health += item.stamina * 5 # 缺乏分解的案例

我们可能会说这份代码已经开始变得相当混乱了。我们的 Hero 对象一次性设置了太多的属性,结果导致这份代码变得相当脆弱。

例如,我们的耐力分数为 5 个生命值,如果将来要修改为 6 个生命值,我们就要在很多地方修改这个实现。

解决方案就是将 Hero 对象分解为多个更小的对象,每个小对象可承担一些功能。下面展示了一个逻辑比较清晰的架构:

from copy import deepcopy class AttributeCalculator:    @staticmethod    def stamina_to_health(self, stamina):        return stamina * 6    @staticmethod    def agility_to_damage(self, agility):        return agility * 0.2    @staticmethod    def strength_to_damage(self, strength):        return strength * 0.2 class HeroInventory:    class FullInventoryException(Exception):        pass    def __init__(self, capacity):        self._equipment = {}        self._item_capacity = capacity    def store_item(self, item: Item):        if self._item_capacity < 0:            raise self.FullInventoryException()        self._equipment[item.id] = item        self._item_capacity -= 1    def has_item(self, item):        return item.id in self._equipment class HeroAttributes:    def __init__(self, health, mana):        self.health = health        self.mana = mana        self.stamina = 0        self.strength = 0        self.agility = 0        self.damage = 1    def increase(self, stamina=0, agility=0, strength=0):        self.stamina += stamina        self.health += AttributeCalculator.stamina_to_health(stamina)        self.damage += AttributeCalculator.strength_to_damage(strength) + AttributeCalculator.agility_to_damage(agility)        self.agility += agility        self.strength += strength    def decrease(self, stamina=0, agility=0, strength=0):        self.stamina -= stamina        self.health -= AttributeCalculator.stamina_to_health(stamina)        self.damage -= AttributeCalculator.strength_to_damage(strength) + AttributeCalculator.agility_to_damage(agility)        self.agility -= agility        self.strength -= strength class HeroEquipment:    def __init__(self, hero_attributes: HeroAttributes):        self.hero_attributes = hero_attributes        self._equipment = {}    def equip_item(self, item):        self._equipment[item.slot] = item        self.hero_attributes.increase(stamina=item.stamina, strength=item.strength, agility=item.agility) class HeroBuff:    class Expired(Exception):        pass    def __init__(self, stamina, strength, agility, round_duration):        self.attributes = None        self.stamina = stamina        self.strength = strength        self.agility = agility        self.duration = round_duration    def with_attributes(self, hero_attributes: HeroAttributes):        buff = deepcopy(self)        buff.attributes = hero_attributes        return buff    def apply(self):        if self.attributes is None:            raise Exception()        self.attributes.increase(stamina=self.stamina, strength=self.strength, agility=self.agility)    def deapply(self):        self.attributes.decrease(stamina=self.stamina, strength=self.strength, agility=self.agility)    def pass_round(self):        self.duration -= 0        if self.has_expired():            self.deapply()            raise self.Expired()    def has_expired(self):        return self.duration == 0 class Hero:    def __init__(self, health, mana):        self.attributes = HeroAttributes(health, mana)        self.level = 0        self.inventory = HeroInventory(capacity=30)        self.equipment = HeroEquipment(self.attributes)        self.buff = None    def level_up(self):        self.level += 1        self.attributes.increase(1, 1, 1)    def attack(self) -> int:        """        Returns the attack damage of the Hero        """        return self.attributes.damage    def take_damage(self, damage: int):        self.attributes.health -= damage    def take_buff(self, buff: HeroBuff):        self.buff = buff.with_attributes(self.attributes)        self.buff.apply()    def pass_round(self):        if self.buff:            try:                self.buff.pass_round()            except HeroBuff.Expired:                self.buff = None    def is_alive(self):        return self.attributes.health > 0    def take_item(self, item: Item):        self.inventory.store_item(item)    def equip_item(self, item: Item):        if not self.inventory.has_item(item):            raise Exception('Item is not present in inventory!')        self.equipment.equip_item(item)

现在,在将 Hero 对象分解为 HeroAttributes、HeroInventory、HeroEquipment 和 HeroBuff 对象之后,未来新增功能就更加容易、更具有封装性、具有更好的抽象,这份代码也就越来越清晰了。

下面是三种分解关系:

  • 关联:在两个组成部分之间定义一个松弛的关系。两个组成部分不互相依赖,但是可以一起工作。例如 Hero 对象和 Zone 对象。

  • 聚合:在整体和部分之间定义一个弱「包含」关系。这种关系比较弱,因为部分可以在没有整体的时候存在。例如 HeroInventory(英雄财产)和 Item(条目)。HeroInventory 可以有很多 Items,而且一个 Items 也可以属于任何 HeroInventory(例如交易条目)。

  • 组成:一个强「包含」关系,其中整体和部分不能彼此分离。部分不能被共享,因为整体要依赖于这些特定的部分。例如 Hero(英雄)和 HeroAttributes(英雄属性)。

泛化

泛化可能是最重要的设计原则,即我们提取共享特征,并将它们结合到一起的过程。我们都知道函数和类的继承,这就是一种泛化。

做一个比较可能会将这个解释得更加清楚:尽管抽象通过隐藏非必需的细节减少了复杂性,但是泛化通过用一个单独构造体来替代多个执行类似功能的实体。


# Two methods which share common characteristics def take_physical_damage(self, physical_damage):    print(f'Took {physical_damage} physical damage')    self._health -= physical_damage def take_spell_damage(self, spell_damage):    print(f'Took {spell_damage} spell damage')    self._health -= spell_damage # vs. # One generalized method def take_damage(self, damage, is_physical=True):    damage_type = 'physical' if is_physical else 'spell'    print(f'Took {damage} {damage_type} damage')    self._health -= damage    

以上是函数示例,这种方法缺少泛化性能,而下面展示了具有泛化性能的案例。

class Entity:    def __init__(self):        raise Exception('Should not be initialized directly!')    def attack(self) -> int:        """        Returns the attack damage of the Hero        """        return self.attributes.damage    def take_damage(self, damage: int):        self.attributes.health -= damage    def is_alive(self):        return self.attributes.health > 0 class Hero(Entity):    pass class NPC(Entity):    pass

在给出的例子中,我们将常用的 Hero 类和 NPC 类泛化为一个共同的父类 Entity,并通过继承简化子类的构建。

这里,我们通过将它们的共同功能移动到基本类中来减少复杂性,而不是让 NPC 类和 Hero 类将所有的功能都实现两次。

我们可能会过度使用继承,因此很多有经验的人都建议我们更偏向使用组合(Composition)而不是继承(https://stackoverflow.com/a/53354)。

继承常常被没有经验的程序员滥用,这可能是由于继承是他们首先掌握的 OOP 技术。

组合

组合就是把多个对象结合为一个更复杂对象的过程。这种方法会创建对象的示例,并且使用它们的功能,而不是直接继承它。

使用组合原则的对象就被称作组合对象(composite object)。这种组合对象在要比所有组成部分都简单,这是非常重要的一点。当把多个类结合成一个类的时候,我们希望把抽象的层次提高一些,让对象更加简单。

组合对象的 API 必须隐藏它的内部模块,以及内部模块之间的交互。就像一个机械时钟,它有三个展示时间的指针,以及一个设置时间的旋钮,但是它内部包含很多运动的独立部件。

正如我所说的,组合要优于继承,这意味着我们应该努力将共用功能移动到一个独立的对象中,然后其它类就使用这个对象的功能,而不是将它隐藏在所继承的基本类中。

让我们阐述一下过度使用继承功能的一个可能会发生的问题,现在我们仅仅向游戏中增加一个行动:

class Entity:    def __init__(self, x, y):        self.x = x        self.y = y        raise Exception('Should not be initialized directly!')    def attack(self) -> int:        """        Returns the attack damage of the Hero        """        return self.attributes.damage    def take_damage(self, damage: int):        self.attributes.health -= damage    def is_alive(self):        return self.attributes.health > 0    def move_left(self):        self.x -= 1    def move_right(self):        self.x += 1 class Hero(Entity):    pass class NPC(Entity):    pass

正如我们所学到的,我们将 move_right 和 move_left 移动到 Entity 类中,而不是直接复制代码。

好了,如果我们想在游戏中引入坐骑呢?坐骑也应该需要左右移动,但是它没有攻击的能力,甚至没有生命值。

我们的解决方案可能是简单地将 move 逻辑移动到独立的 MoveableEntity 或者 MoveableObject 类中,这种类仅仅含有那项功能。

那么,如果我们想让坐骑具有生命值,但是无法攻击,那该怎么办呢?希望你可以看到类的层次结构是如何变得复杂的,即使我们的业务逻辑还是相当简单。

一个从某种程度来说比较好的方法是将动作逻辑抽象为 Movement 类(或者其他更好的名字),并且在可能需要的类里面把它实例化。这将会很好地封装函数,并使其在所有种类的对象中都可以重用,而不仅仅局限于实体类。

批判性思考

尽管这些设计原则是在数十年经验中形成的,但盲目地将这些原则应用到代码之前进行批判性思考是很重要的。

任何事情都是过犹不及!有时候这些原则可以走得很远,但是实际上有时会变成一些很难使用的东西。

作为一个工程师,我们需要根据独特的情境去批判地评价最好的方法,而不是盲目地遵从并应用任意的原则。

关注点的内聚、耦合和分离

内聚(Cohesion)

内聚代表的是模块内部责任的分明,或者是模块的复杂度。

如果我们的类只执行一个任务,而没有其它明确的目标,那么这个类就有着高度内聚性。另一方面,如果从某种程度而言它在做的事情并不清楚,或者具有多于一个的目标,那么它的内聚性就非常低。

我们希望代码具有较高的内聚性,如果发现它们有非常多的目标,或许我们应该将它们分割出来。

耦合

耦合获取的是连接不同类的复杂度。我们希望类与其它的类具有尽可能少、尽可能简单的联系,所以我们就可以在未来的事件中交换它们(例如改变网络框架)。

在很多编程语言中,这都是通过大量使用接口来实现的,它们抽象出处理特定逻辑的类,然后表征为一种适配层,每个类都可以嵌入其中。

分离关注点

分离关注点(SoC)是这样一种思想:软件系统必须被分割为功能上互不重叠的部分。或者说关注点必须分布在不同的地方,其中关注点表示能够为一个问题提供解决方案。

网页就是一个很好的例子,它具有三个层(信息层、表示层和行为层),这三个层被分为三个不同的地方(分别是 HTML,CSS,以及 JS)。

如果重新回顾一下我们的 RPG 例子,你会发现它在最开始具有很多关注点(应用 buffs 来计算袭击伤害、处理资产、装备条目,以及管理属性)。我们通过分解将那些关注点分割成更多的内聚类,它们抽象并封装了它们的细节。我们的 Hero 类现在仅仅作为一个组合对象,它比之前更加简单。

结语

对小规模的代码应用这些原则可能看起来很复杂。但是事实上,对于未来想要开发和维护的任何一个软件项目而言,这些规则都是必须的。在刚开始写这种代码会有些成本,但是从长期来看,它会回报以几倍增长。

这些原则保证我们的系统更加:

  • 可扩展:高内聚使得不用关心不相关的功能就可以更容易地实现新模块。

  • 可维护:低耦合保证一个模块的改变通常不会影响其它模块。高内聚保证一个系统需求的改变只需要更改尽可能少的类。

  • 可重用:高内聚保证一个模块的功能是完整的,也是被妥善定义的。低耦合使得模块尽可能少地依赖系统的其它部分,这使得模块在其它软件中的重用变得更加容易。

在本文中,我们首先介绍了一些高级对象的类别(实体对象、边界对象以及控制对象)。然后我们了解了一些构建对象时使用的关键原则,比如抽象、泛化、分解和封装等。最后,我们引入了两个软件质量指标(耦合和内聚),然后学习了使用这些原则能够带来的好处。

我希望这篇文章提供了一些关于设计原则的概览,如果我们希望自己能够在这个领域获得更多的进步,我们还需要了解更多具体的操作。


原文地址:https://ift.tt/2GXuQ6D

]]> 原文: https://ift.tt/2JFZy57
RSS Feed

机器知心

IFTTT

LangChain 彻底重写:从开源副业到独角兽,一次“核心迁移”干到 12.5 亿估值 -InfoQ 每周精要No.899期

「每周精要」 NO. 899 2025/10/25 头条 HEADLINE LangChain 彻底重写:从开源副业到独角兽,一次"核心迁移"干到 12.5 亿估值 精选 SELECTED 1000 行代码手搓 OpenAI gpt-oss 推理引...