我们证明了可以利用深度学习方法来端到端训练模型,实现从单个输入图像自动生成针对三种不同平台(即 iOS,Android 和基于 Web 的平台)的源代码,且准确性超过 77%。
前言
最近这三年来,随着深度学习的发展以及不断尝试,给前端开发也带来一些冲击。在一些研究者的实验中表明,Deep Learning 手段可以提高网页的原型制作速度,降低构建软件的障碍。2017 年,当 Tony Beltramelli 发表了 pix2code 论文、Airbnb 推出了 sketch2code 模型,利用深度学习自动生成 HTML 代码这个话题受到越来越多的期待。目前来说,实现前端开发自动化的最大障碍是计算能力。但是使用深度学习算法和合成的训练数据也可能是一种很好的解决办法。刚好笔者读研期间有接触过 Deep Learning ,现在从事的又是前端开发工作,所以对这个话题比较关注。那今天我们就来研究一下吧!
pix2code
2017 年,uizard公开了一篇比较有意思的论文(demo 演示地址)。这篇论文实现的目标很简单:试图从设计原型图直接生成源代码(github 项目地址)。这篇文章一出来就引起了很多关注,甚至有很多公众号、科技专栏发文章说前端开发马上就要被取代了,取得名字也是一个比一个夸张,当时笔者也是刚入前端的坑,听到这消息的时候也是被震惊得不要不要的。那现在我们就来看看他到底是怎么实现的吧!
pix2code 中的神经网络是一种卷积神经网络,是一种能够同时以许多不同尺度观看图像的网络。这允许网络通过检查用户界面和底层代码来学习,从而查看每行代码对设计的每个元素的影响。pix2code 现在可以保证约 77%的代码正确,但随着数据的增多,准确率应该越来越高。
概述
在企业的一般流程中,将图形用户界面(GUI,Graphical User Interface)的原型图或者是设计图转化为网页或者是客户端页面通常是开发人员的工作。但是这部分工作比较耗时,而且除了这部分工作开发人员还需要大量时间实现需求的逻辑和功能。所以这篇文章就尝试构建了一个随机梯度下降的端到端训练模型,将单个 GUI 图像作为输入,可以同时学习对序列和时空视觉特征进行建模然后生成一个可变长度的 token 字符串。
uizard 团队利用卷积神经网络和递归神经网络的来计算输入 GUI 的 token 字符串。也就是说他们没有设计专门的特征提取流程或者是专家方法来处理输入数据,而是利用神经网络自适应渐进式学习特性进行学习。这样的方法还有一个好处就是只需要更改训练数据就可以实现不同平台的需求。该团队已经将其训练所用数据集公开,可以从项目github 地址获取。
pix2code 模型概述
现在在计算机视觉领域已经有很多方法可以解决图像和视频的字母问题了,具体试验和论文请看文末参考资料。这些研究表明深度神经网络能够学习描述图像中对象的潜在变量及其与相应的变长文本描述的关系。所有这些方法都依赖于两个主要组成部分。 首先,利用一个卷积神经网络(CNN)进行无监督特征学习将原始输入图像映射到深度学习之后的表示形式。 然后,利用一个递归神经网络(RNN)对由输入图片得到的文字描述进行语言建模。 这些方法具有端到端可区分的优势,因此允许使用梯度下降进行优化。那这篇文章是怎么做的呢?我们先来看一下 pix2code 的模型架构:
该模型一共可以分为两个部分:训练和采样。在训练阶段,会利用一个基于 CNN 的视觉模型对 GUI(Graphical User Interface) 图片进行编码。而输入图像对应的上下文(context,即与 DSL 代码相对应的 one-hot 编码 tokens 序列)则由包含多个 LSTM(Long short-term memory) 结构的语言模型进行编码。将得到的两个特征向量(feture vectors)进行级联然后喂进第二个具有多个 LSTM 结构的进行解码。最后由 softmax 层一次生成一个用于采样的 token。softmax 层输出的尺寸和 DSL 的词汇数量保持一致。 对于给定图像和 token 序列,改模型(即包含在灰色框中的模型)是可区分的,因此可以通过梯度下降进行端到端优化以预测序列中的下一个 token。在采样阶段,最开始出入的是空白的上下文(context)和 GUI 图像,上下文(context)通过在每一次预测之后进行更新来包含最后预测的 token。最后会生成一个与该 GUI 图像最相关的 DSL 代码,并使用传统的编译器设计技术将生成的 DSL token 序列编译为所需的目标语言。
从 GUI 生成给定编程语言编写的计算机代码这一任务可以与从场景摄影生成英语文本描述的任务进行比较。在这两种情况下,都是希望根据像素值生成可变长度的令牌(token)字符串。因此,作者将问题分为了三个子问题:首先,计算机视觉问题是理解给定的场景( GUI 图像)并推断存在的对象,它们的身份,位置和姿势(即按钮,标签,元素容器)。其次,语言建模问题是理解文本(源代码)并生成句法和语义上正确的样本。第三,最后的挑战是通过利用从场景理解中推断出的潜在变量来生成由这些变量表示的对象的相应文本描述(即计算机代码而不是英语),从而将解决方案用于先前的两个子问题。
利用 Vision Model(视觉模型)生成 DSL(领域专用语言)
使用机器学习技术自动生成程序是一个相对较新的研究领域,以人类可读的格式进行程序合成只是在最近才得到解决。目前方法大多数都依赖于领域特定语言(DSL,Domain Specific Languages)。专为特定领域而设计的计算机语言(例如标记语言,编程语言,建模语言),但比全功能计算机语言更具限制性。
论文中是使用一个 CNN 模型通过无监督特征学习方式将输入图像映射到固定长度矢量,也就是将这个 CNN 作为编码器。
- 预处理
将输入图像调整为 256×256 像素(不保留宽高比),并且对像素值进行归一化处理。通过归一化处理之后的像素值会在[0,1]的范围内,可以防止仿射变换的影响并且减小几何变换的影响,最重要的是还可以加快梯度下降求最优解的速度。 - CNN 模型
模型定义的源码片段如下:
class pix2code(AModel):
def __init__(self, input_shape, output_size, output_path):
AModel.__init__(self, input_shape, output_size, output_path)
self.name = "pix2code"
image_model = Sequential()
# 第一部分
# 卷积层(32,(3,3)) * 2 + 最大池化层 + Dropout
image_model.add(Conv2D(32, (3, 3), padding='valid', activation='relu', input_shape=input_shape))
image_model.add(Conv2D(32, (3, 3), padding='valid', activation='relu'))
image_model.add(MaxPooling2D(pool_size=(2, 2)))
image_model.add(Dropout(0.25))
# 第二部分
# 卷积层(64,(3,3)) * 2 + 最大池化层 + Dropout
image_model.add(Conv2D(64, (3, 3), padding='valid', activation='relu'))
image_model.add(Conv2D(64, (3, 3), padding='valid', activation='relu'))
image_model.add(MaxPooling2D(pool_size=(2, 2)))
image_model.add(Dropout(0.25))
# 第三部分
# 卷积层(128,(3,3)) * 2 + 最大池化层 + Dropout
image_model.add(Conv2D(128, (3, 3), padding='valid', activation='relu'))
image_model.add(Conv2D(128, (3, 3), padding='valid', activation='relu'))
image_model.add(MaxPooling2D(pool_size=(2, 2)))
image_model.add(Dropout(0.25))
# 第四部分
# (全连接层 + Dropout) * 2
image_model.add(Flatten())
image_model.add(Dense(1024, activation='relu'))
image_model.add(Dropout(0.3))
image_model.add(Dense(1024, activation='relu'))
image_model.add(Dropout(0.3))
image_model.add(RepeatVector(CONTEXT_LENGTH))
# 后面部分省略
可以看出为了将每一张输入图片编码成定长的输出向量,文中的 CNN 采用的是步长为 1 的尺寸为 3X3 感受野(卷积核)进行卷积(详情见VGGNet)。卷积阶段,三个阶段卷积核的数量分别为 32、64 和 128,在每一个阶段卷积之后会进行一次最大池化处理,来减小 feature map 的尺寸并且保留主要特征方式过拟合,最后进行一次 Dropout 处理,通过使卷积核以一定的概率停止工作来防止过拟合并且增强网络的泛化性。卷积完毕之后,将卷积得到的 feature map 展开通过两层全连接层和 Dropout 的处理后得到最终编码的结果。
下图就是一个根据原生 IOS 用户图形界面生成的 DSL 代码。图中左边是 GUI 图片,右边是 DSL。
可以看到右边的 DSL 是描述了 GUI 的格式化语言,可以看出图中的语言描述的是左边的 GUI 中第一行是一个 label(标签)和一个 switch(开关),第二行是一个 label(标签)和一个 btn-add(增加按钮),第三行是两个 label(标签)和一个 slider(滑动条),第四行是一张 img(图片)和 label(标签)。然后下面的 footer 里面的内容就是页面底部的内容描述。接下来就是要把 DSL 语言编程成源代码。在 github 上拉取代码后,进入 compiler 文件夹,根据不同的需求运行不同的命令,目前 pix2code 提供三种目标语言。
cd compiler
# compile .gui file to Android XML UI
./android-compiler.py <input file path>.gui
# compile .gui file to iOS Storyboard
./ios-compiler.py <input file path>.gui
# compile .gui file to HTML/CSS (Bootstrap style)
./web-compiler.py <input file path>.gui
Language Model
除了减小搜索空间的大小之外,DSL 简单性还减小了词汇表的大小(即 DSL 支持的 token 总数)。所以,文中的语言模型可以通过利用用 one-hot 编码向量对来离散的输入进行 token 级语言建模。在大多数编程语言和标记语言中,元素是用一个开头标记声明的。 如果子元素或指令包含在块中,则解释器或编译器通常需要关闭标记。 在这种情况下,父元素中包含的子元素的数量是可变的,因此需要对长期依赖性进行建模以能够关闭打开的块。 像这种处理有前后关系的序列信息,通常是采用循环神经网络(RNN)。文中采用的是 LSTM 网络结构,LSTM 是一种特殊的 RNN,主要是为了解决长序列训练过程中的梯度消失和梯度爆炸问题。简单来说,就是相比普通的 RNN,LSTM 能够在更长的序列中有更好的表现。
LSTM 和 RNN
在普通的全连接层或 CNN 中,每一层神经元的信号只能向上一层传播,每一层的输出都是独立的,因此又被称为前向神经网络(Feed-forward Neural Networks)。但是实际上很多时候,人类并不是每时每刻都从一片空白的大脑开始他们的思考。在你阅读这篇文章时候,你都是基于自己已经拥有的对先前所见词的理解来推断当前词的真实含义,而不是将所有的东西都全部丢弃,然后用空白的大脑进行思考。人类的思想拥有持久性,而作为仿人类大脑神经网络的卷积神经网络来说也应该需要这种持久性,能够对样本时序上的变化进行建模,所以,出现了 RNN(递归神经网络)。RNN 网络结构如下图所示(按时间展开):
由图和公式可以看出,在 RNN 中是将过去的输出和当前的输入 concatenate 到一起,通过 tanh 来控制两者的输出,它只考虑最近时刻的状态。也就是说,RNN 本时刻的隐藏层信息只来源于当前输入和上一时刻的隐藏层信息,没有记忆功能。而 LSTM 专门设计用来避免长期依赖问题的 RNN 网络,对 LSTM 来说记住长期的信息是一种默认行为。LSTM 网络结构如下图所示:
可以看出,LSTM 的网络结构相比于 RNN 复杂了很多,而且包含多种图标,每一个图标的意义如下图所示。每一条黑线传输着一整个向量,从一个节点的输出到其他节点的输入。粉色的圈代表按位 pointwise 的操作,诸如向量的和,而黄色的矩阵就是学习到的神经网络层。合在一起的线表示向量的连接,分开的线表示内容被复制,然后分发到不同的位置。
LSTM 的关键就是细胞状态,水平线在图上方贯穿运行。细胞状态类似于传送带。直接在整个链上运行,只有一些少量的线性交互。信息在上面流传并保持不变会很容易,也就是和传统的 RNN 一样,通过使用递归连接来学习记忆信息。另外 LSTM 有通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。他们包含一个 sigmoid 神经网络层和一个按位的乘法操作。
LSTM 中一共有三种门:输入门、遗忘门、输出门,利用这三种门来保持和控制信息。LSTM 中某个 timstep t 的计算公式如下所示。那么 LSTM 是如何做到保持和控制信息的呢?- 遗忘门
遗忘门决定我们会从细胞状态中丢弃什么信息。实现方式是结合上一层隐藏层的状态值和当前层的输入,通过 sigmoid 函数(σ),决定舍弃哪些旧信息。sigmoid 输出的是(0,1)范围内的数值,用于描述每个部分有多少量可以通过。0 代表“不许任何量通过”,1 就指“允许任意量通过”。遗忘门结构和计算方式如图所示。 - 输入门
输入门是确定什么样的新信息被存放在细胞状态中。这里包含两个部分。第一,sigmoid 层称 “输入门层” 决定什么值我们将要更新。然后,tanh 层创建一个新的候选值向量$\tilde{C}_t$,会被加入到状态中。输入门结构和计算方式如图所示: - 输出门
输出门是根据当前的状态和现在的输入,决定输出的应该是什么。把旧状态$\tilde{C}_{t-1}$与遗忘门的值$f_t$ 相乘,丢弃掉我们确定需要丢弃的信息。接着加上 $i_t$ 和 $\tilde{C}_t$的乘积,这就是我们要更新的内容。
输出门结构和计算方式如图所示:
最后,基于细胞状态决定最终输出什么值。首先,运行一个 sigmoid 层来确定细胞状态的哪个部分将输出出去。接着把细胞状态通过 tanh 进行处理(得到一个在 -1 到 1 之间的值)并将它和 sigmoid 的输出相乘,最终仅仅会输出确定输出的那部分。
- 遗忘门
Decoder
这一部分的神经网络采用的是监督学习方式,将一张图片 I 和其对应的 T 个 tokens 的上下文序列 X 作为输入,即$x_t,t \epsilon \{0,…,T-1 \}$,token $x_T$则作为目标标签。如 pix2code(Figure 1) 模型图所示,基于 CNN 的视觉模型将输入图片编码成一个向量表示 p,而输入的 token $x_t$则被一个 基于 LSTM 的语言模型编码成中间表示 $q_t$这样使得模型可以更关注某些 token。pix2code 模型中一共有两个基于 LSTM 的语言模型。第一个语言模型一共有两个 LSTM 层构成,每一层都有 128 个单元。第二个语言模型的则是将视觉编码向量 p 和语言编码向量$x_t$串联之后的单个特征向量$r_t$作为输入来对视觉模型和语言模型中学习到的表示进行解码。因此,解码器是对输入 GUI 图像中的对象与 DSL 代码中相应的 token(令牌)之间的关系进行建模。解码器也是由两个 LSTM 层(每个 512 个单元)的组成。其数学表达式如下图所示:
该架构允许整个 pix2code 模型在梯度下降的情况下进行端到端优化,每次在看到图像和序列中的前一个标记后预测一个标记。又由于该模型输出的离散性(即 DSL 中固定大小的标记词汇表)可以将任务简化为分类问题。也就是说,该模型的输出层的神经元数量与词汇表单词的数量相同,从而在每个时间步长使用 softmax 层执行多类分类生成候选 token(令牌)的概率分布。
论文具体实验过程就不详细说了,展示一下论文中的实验结果,下图分别是 IOS、web 和 Android 三种平台上的实验结果:
参考资料
- 让 AI 替你写代码
- Long-term recurrent convolutional networks for visual recognition and description
- Deep visual-semantic alignments for generating image descriptions
- 人人都能看懂的 LSTM
- 理解 LSTM
- DNN、RNN、CNN.…..一文带你读懂这些绕晕人的名词
结语
这是笔者第一次正儿八经的解读一篇论文,自己都没有想到能坚持下来。由于笔者英语比较渣,很多地方都是靠谷歌翻译,可能有不准确的地方,希望大家可以指出!有时间的话,也会再做校正!另外,这边文章主要是讲论文内容,那 pix2code 这个模型具体怎么运行,运行之后的结果如何,又如何进行训练等这些问题放到下一篇《深度学习生成 HTML 探索之 pix2code(实践)》!