白何谓Margin Collapse
不同于其他很多属性,盒模型中垂直方向上的Margin会在相遇时发生崩塌,也就是说当某个元素的底部Margin与另一个元素的顶部Margin相邻时,只有二者中的较大值会被保留下来,可以从下面这个简单的例子来学习:
.square { width: 80px; height: 80px; }.red { background-color: #F44336; margin-bottom: 40px; }.blue { background-color: #2196F3; margin-top: 30px; }
在上述例子中我们会发现,红色和蓝色方块的外边距并没有相加得到70px,而是只有红色的下外边距保留了下来。我们可以使用一些方法来避免这种行为,不过建议来说还是尽量统一使用margin-bottom
属性,这样就显得和谐多了。
使用Flexbox进行布局
在传统的布局中我们习惯使用Floats或者inline-blocks,不过它们更适合于格式化文档,而不是整个网站。而Flexbox则是专门的用于进行布局的工具。Flexbox模型允许开发者使用很多便捷可扩展的属性来进行布局,估计你一旦用上就舍不得了:
.container { display: flex; /* Don't forget to add prefixes for Safari */display: -webkit-flex; }
我们已经在Tutorialzine上提供了很多的关于Flexbox的介绍与小技巧,譬如5 Flexbox Techniques You Need to Know About。
使用CSS Reset
虽然这些年来随着浏览器的迅速发展与规范的统一,浏览器特性碎片化的情况有所改善,但是在不同的浏览器之间仍然存在着很多的行为差异。而解决这种问题的最好的办法就是使用某个CSS Reset来为所有的元素设置统一的样式,保证你能在相对统一干净的样式表的基础上开始工作。目前流行的Reset库有 normalize.css, minireset以及 ress ,它们都可以修正很多已知的浏览器之间的差异性。而如果你不打算用某个外在的库,那么建议可以使用如下的基本规则:
* { margin: 0; padding: 0; box-sizing: border-box; }
上面的规则看起来没啥用,不过如果不同的浏览器在默认情况下为你设置了不同的外边距/内边距的默认值,还是会挺麻烦的。
一切应为Border-box
虽然很多初学者并不了解box-sizing
这个属性,但是它确实相当的重要。而最好的理解它的方式就是看看它的两种取值:
默认值为content-box,即当我们设置某个元素的heght/width属性时,仅仅会作用于其内容尺寸。而所有的内边距与边都是在其之上的累加,譬如某个<div>
标签设置为宽100,内边距为10,那么最终元素会占用120(100 + 2*10)的像素。
border-box:内边距与边是包含在了width/height之内,譬如设置了width:100px
的<div>
无论其内边距或者边长设置为多少,其占有的大小都是100px。
将元素设置为border-box会很方便你进行样式布局,这样的话你就可以在父元素设置高宽限制而不担心子元素的内边距或者边打破了这种限制。
以背景图方式使用Images
如果需要在响应式的环境下展示图片,有个简单的小技巧就是使用该图片作为某个<div>
的背景图而不是直接使用img标签。基于这种方式配合上background-size
与background-position
这两个属性,可以很方便地按比例缩放:
img { width: 300px; height: 200px; }div { width: 300px; height: 200px; background: url('http://cdn.tutorialzine.com/wp-content/uploads/2016/08/bicycle.jpg'); background-position: center center; background-size: cover; }section{ float: left; margin: 15px; }
不过这种方式也是存在缺陷的,譬如你无法设置图片的懒加载、图片无法被搜索引擎或者其他类似的工具抓取到,有个不错的属性叫object-fit可以解决这个问题,不过该属性目前的浏览器支持并不是很完善。
Better Table Borders
HTML中使用Tables进行布局一直是个很头疼的问题,它们使用起来很简单,但是无法进行响应式操作,并且也不方便进行全局样式设置。譬如,如果你打算为Table的边与单元的边添加样式,可能得到的结果如下:
table { width: 600px; border: 1px solid #505050; margin-bottom: 15px; color:#505050; }td{ border: 1px solid #505050; padding: 10px; }
这里存在的问题是出现了很多的重复的边,会导致视觉上不协调的情况,那么我们可以通过设置border-collapse:collapse
来进行处理:
注释格式优化
CSS虽然谈不上一门编程语言但是其仍然需要添加注释以保障整体代码的可读性,只要添加些简单的注释不仅可以方便你更好地组织整个样式表还能够让你的同事或者未来的自己更好地理解。对于CSS中整块的注释或者使用在Media-Query中的注释,建议是使用如下形式:
/*--------------- #Header ---------------*/header { }header nav { }/*--------------- #Slideshow ---------------*/.slideshow { }
而设计的细节说明或者一些不重要的组件可以用如下单行注释的方式:
/* Footer Buttons */.footer button { }.footer button:hover { }
同时,不要忘了CSS中是没有//
这种注释方式的:
/* Do */p { padding: 15px; /*border: 1px solid #222;*/}/* Don't */p { padding: 15px; // border: 1px solid #222; }
使用Kebab-case命名变量
对于样式类名或者ID名的命名都需要在多个单词之间添加-
符号,CSS本身是大小写不敏感的因此你是用不了camelCase的,另一方面,很久之前也不支持下划线,所以现在的默认的命名方式就是使用-
:
/* Do */.footer-column-left { }/* Don't */.footerColumnLeft { }.footer_column_left { }
而涉及到具体的变量命名规范时,建议是使用BEM规范,只要遵循一些简单的原则即可以保证基于组件风格的命名一致性。你也可以参考CSS Tricks来获得更多的细节描述。
避免重复代码
大部分元素的CSS属性都是从DOM树根部继承而来,这也是其命名为级联样式表的由来。我们以font
属性为例,该属性往往是继承自父属性,因此我们并不需要再单独地为元素设置该属性。我们只需要在html
或者body
中添加该属性然后使其层次传递下去即可:
html { font: normal 16px/1.4 sans-serif; }
使用transform添加CSS Animations
不建议直接改变元素的width
与height
属性或者left/top/bottom/right
这些属性来达到动画效果,而应该优先使用transform()
属性来提供更平滑的变换效果,并且能使得代码的可读性会更好:
.ball { left: 50px; transition: 0.4s ease-out; }/* Not Cool*/.ball.slide-out { left: 500px; }/* Cool*/.ball.slide-out { transform: translateX(450px); }
Transform的几个属性translate
、rotate
、scale
都具有比较好的浏览器兼容性可以放心使用。
不要重复造轮子
现在CSS社区已经非常庞大,并且不断地有新的各式各样的库开源出来。这些库可以帮助我们解决从小的代码片到用于构建完整的响应式应用的全框架。所以如果下次你再碰到什么CSS问题的时候,在打算撸起袖子自己上之前可以尝试在GitHUB或者CodePen上搜索可行方案。
尽可能使用低优先级的选择器
并不是所有的CSS选择器的优先级都一样,很多初学者在使用CSS选择器的时候都是考虑以新的特性去复写全部的继承特性,不过这一点在某个元素多状态时就麻烦了,譬如下面这个例子:
a{ color: #fff; padding: 15px; }a#blue-btn { background-color: blue; }a.active { background-color: red; }
我们本来希望将.active
类添加到按钮上然后使其显示为红色,不过在上面这个例子中很明显起不了作用,因为button
已经以ID选择器设置过了背景色,也就是所谓的Higher Selector Specificity。一般来说,选择器的优先级顺序为:ID(#id) > Class(.class) > Type(header)
避免使用!important
认真的说,千万要避免使用!important,这可能会导致你在未来的开发中无尽的属性重写,你应该选择更合适的CSS选择器。而唯一的可以使用!important
属性的场景就是当你想去复写某些行内样式的时候,不过行内样式本身也是需要避免的。
使用text-transform属性设置文本大写
<div class="movie-poster">Star Wars: The Force Awakens</div>.movie-poster { text-transform: uppercase; }
Em, Rem, 以及 Pixel
已经有很多关于人们应该如何使用em,rem,以及px作为元素尺寸与文本尺寸的讨论,而笔者认为,这三个尺寸单位都有其适用与不适用的地方。不同的开发与项目都有其特定的设置,因此并没有通用的规则来决定应该使用哪个单位,这里是我总结的几个考虑:
em – 其基本单位即为当前元素的font-size
值,经常适用于media-queries中,em是特别适用于响应式开发中。
rem – 其是相对于html
属性的单位,可以保证文本段落真正的响应式尺寸特性。
px – Pixels 并没有任何的动态扩展性,它们往往用于描述绝对单位,并且可以在设置值与最终的显示效果之间保留一定的一致性。
在大型项目中使用预处理器
估计你肯定听说过 Sass, Less, PostCSS, Stylus这些预处理器与对应的语法。Preprocessors可以允许我们将未来的CSS特性应用在当前的代码开发中,譬如变量支持、函数、嵌套式的选择器以及很多其他的特性,这里我们以Sass为例:
$accent-color: #2196F3;a { padding: 10px 15px; background-color: $accent-color; }a:hover { background-color: darken($accent-color,10%); }
使用Autoprefixers来提升浏览器兼容性
使用特定的浏览器前缀是CSS开发中常见的工作之一,不同的浏览器、不同的属性对于前缀的要求也不一样,这就使得我们无法在编码过程中记住所有的前缀规则。并且在写样式代码的时候还需要加上特定的浏览器前缀支持也是个麻烦活,幸亏现在也是有很多工具可以辅助我们进行这样的开发:
Online tools: Autoprefixer
Text editor plugins: Sublime Text, Atom
Libraries: Autoprefixer (PostCSS)
在生产环境下使用Minified代码
为了提升页面的加载速度,在生产环境下我们应该默认使用压缩之后的资源代码。在压缩的过程中,会将所有的空白与重复剔除掉从而减少整个文件的体积大小。当然,经过压缩之后的代码毫无可读性,因此在开发阶段我们还是应该使用普通的版本。对于CSS的压缩有很多的现行工具:
Online tools – CSS Minifier (API included), CSS Compressor
Text editor plugins: Sublime Text, Atom
Libraries: Minfiy (PHP), CSSO and CSSNano (PostCSS, Grunt, Gulp)
选择哪个工具肯定是依赖于你自己的工作流啦~
多参阅Caniuse
不同的浏览器在兼容性上差异很大,因此如果我们可以针对我们所需要适配的浏览器,在caniuse上我们可以查询某个特性的浏览器版本适配性,是否需要添加特定的前缀或者在某个平台上是否存在Bug等等。不过光光使用caniuse肯定是不够的,我们还需要使用些额外的服务来进行检测。
Validate:校验
对于CSS的校验可能不如HTML校验或者JavaScript校验那么重要,不过在正式发布之前用Lint工具校验一波你的CSS代码还是很有意义的。它会告诉你代码中潜在的错误,提示你一些不符合最佳实践的代码以及给你一些提升代码性能的建议。就像Minifers与Autoprefixers,也有很多可用的工具:
Online tools: W3 Validator, CSS Lint
Text editor plugins: Sublime Text, Atom
Libraries: lint (Node.js, PostCSS), css-validator (Node.js)
(作者:Danny Markov,翻译:王下邀月熊_Chevalier)
英语原文:20 Protips For Writing Modern CSS
年云栖大会期间,阿里云 Hands on Labs 动手实验室推出有奖体验活动,立即参加体验从设计稿自动生成 H5 应用的神奇。
? 来源:yunqi.aliyun.com ? 作者:Imgcook ?
(本文字数:3960,阅读时长大约:6 分钟)
文末有惊喜,不要忘了去文末看哦~
imgcook 可以使用 Sketch、PSD、静态图片等形式作为输入,通过智能化技术一键生成可维护的前端代码,包含视图代码、数据字段绑定、组件代码、部分业务逻辑代码等。
Img Cook 期望能够利用智能化手段,让自己成为一位前端工程师,在对设计稿轻约束的前提下实现高度还原,释放前端生产力,助力前端与设计师高效协作,让工程师们专注于更具挑战性的工作!
视图代码研发,一般是根据视觉稿编写 HTML 和 CSS 代码。如何提效,当面对 UI 视图开发重复性的工作时,自然想到组件化、模块化等封装复用物料的解决方案,基于此解决方案会有各种 UI 库的沉淀,甚至是可视化拼装搭建的更高阶的产品化封装,但复用的物料不能解决所有场景问题。个性化业务、个性化视图遍地开花,直面问题本身,直接生成可用的 HTML 和 CSS 代码是否可行?
这是业界一直在不断尝试的命题,通过设计工具的开发插件可以导出图层的基本信息,但这里的主要难点还是对设计稿的要求高、生成代码可维护性差,这是核心问题,我们来继续拆解。
设计稿要求高
对设计稿的要求高,会导致设计师的成本加大,相当于前端的工作量转嫁给了设计师,导致推广难度会非常大。一种可行的办法是采用 CV(ComputerVision, 计算机视觉) 结合导出图层信息的方式,以去除设计稿的约束,当然对设计稿的要求最好是直接导出一张图片,那样对设计师没有任何要求,也是我们梦寐以求的方案,我们也一直在尝试从静态图片中分离出各个适合的图层,但目前在生产环境可用度不够(小目标识别精准度问题、复杂背景提取问题仍待解决),毕竟设计稿自带的元信息,比一张图片提取处理的元信息要更多更精准。
代码可维护性
生成的代码结构一般都会面临可维护性方面的挑战:
基于上述的概述和问题分解后,我们对现有的 D2C(Design 2 Code) 智能化技术体系做了能力概述分层,主要分为以下三部分:
(前端智能化 D2C 能力概要分层)
在整个方案里,我们用同一套**数据协议规范(D2C Schema)**来连接各层的能力,确保其中的识别能够映射到具体对应的字段上,在表达阶段也能正确地通过出码引擎等方案生成代码。
智能识别技术分层
在整个 D2C 项目中,最核心的是上述识别能力部分的机器智能识别部分,这层的具体再分解如下:
(D2C 识别能力技术分层)
技术痛点
当然,这其中的识别不全面、识别准确度不高一直是 D2C 老生常谈的话题,也是 imgcook 的核心技术痛点。我们尝试从这几个角度来分析引起这个问题的因素:
在了解了 imgcook 大致思路之后,那么为什么会选择在云开发平台上集成 imgcook 呢?那就是 imgcook 和云开发平台通过彼此的打通,将能够为双方解决彼此的痛点,无论是为云上开发者,还是 imgcook 开发者都提供了全新的用户体验。
对于 imgcook 开发者来说,其中一个痛点就来自于对于设计稿的管理,以及前后端交互的逻辑,然而通过云开发平台,开发者不再需要在本地安装 Sketch,通过云开发平台直接上传设计稿即可开始生成代码,真正做到了0成本一键生成。
另外云开发平台上直接提供了 Midway Serverless 框架,我们通过云开发平台的插件定制化,可以让开发者直接选择某个页面所使用的函数(Function),这样就节省掉编写一些前后端交互的基础逻辑或请求代码。
对于云开发平台的开发者来说,最想得到的便是极速的上线体验和更加便捷的开发体验,imgcook 可以降低云开发平台的使用门槛,比如一位 FaaS 应用工程师不再需要学习如何切图,如何写 CSS,而只需要编写 FaaS 函数的逻辑即可,剩下的前端逻辑代码都可以通过 imgcook 插件在开发平台内即可完成,这是多么棒的体验啊!
那么,接下来就看看如何快速地从 0 到 1 生成代码吧。
首先需要先打开云开发平台创建应用,选择 imgcook 创建应用:
然后在应用的 WebIDE 中通过右键打开 imgcook 云插件,就可以正式开始使用了。
第一步,在插件中选择“导入”,打开上传设计稿界面:
第二步,imgcook 可视化编辑器:
第三步,生成代码:
第四步,导出代码到应用:
第五步,上线应用:
$ npm install
$ npm run dev
正在启动,请稍后...
---------------------------------------
开发服务器已成功启动
请打开 >>> http://*****-3000.xide.aliyun.com/
---------------------------------------
感谢使用 Midway Serverless,欢迎 Star!
https://github.com/midwayjs/midway
---------------------------------------
启动成功后通过命令行的地址打开页面效果如下,是不是很简单呢?
本文通过介绍前端智能化的背景,imgcook 的问题定义以及技术方案,以及如何在云开发平台上使用 imgcook 开始智能开发,总的来说,还是希望让业内的前端工程师们从使用 imgcook 开始,将日常工作中的一些繁琐、耗时的工作交给 AI 来完成,这样能关注工程师本身更感兴趣,也更有价值的事情,也相信不久的将来,前端工程师将借助于 AI 能更加快乐与从容地工作!
今年云栖大会期间,阿里云 Hands on Labs 动手实验室推出有奖体验活动,立即参加体验从设计稿自动生成 H5 应用的神奇。
同时,由 Hands-on Labs 发起的万人云上 Hello World 实验上线
选自Floydhub
作者:Emil Wallner
机器之心编译
如何用前端页面原型生成对应的代码一直是我们关注的问题,本文作者根据 pix2code 等论文构建了一个强大的前端代码生成模型,并详细解释了如何利用 LSTM 与 CNN 将设计原型编写为 HTML 和 CSS 网站。
项目链接:github.com/emilwallner…
在未来三年内,深度学习将改变前端开发。它将会加快原型设计速度,拉低开发软件的门槛。
Tony Beltramelli 在去年发布了论文《pix2code: Generating Code from a Graphical User Interface Screenshot》,Airbnb 也发布Sketch2code(airbnb.design/sketching-i…)。
目前,自动化前端开发的最大阻碍是计算能力。但我们已经可以使用目前的深度学习算法,以及合成训练数据来探索人工智能自动构建前端的方法。在本文中,作者将教神经网络学习基于一张图片和一个设计模板来编写一个 HTML 和 CSS 网站。以下是该过程的简要概述:
1)向训练过的神经网络输入一个设计图
2)神经网络将图片转化为 HTML 标记语言
3)渲染输出
我们将分三步从易到难构建三个不同的模型,首先,我们构建最简单地版本来掌握移动部件。第二个版本 HTML 专注于自动化所有步骤,并简要解释神经网络层。在最后一个版本 Bootstrap 中,我们将创建一个模型来思考和探索 LSTM 层。
代码地址:
所有 FloydHub notebook 都在 floydhub 目录中,本地 notebook 在 local 目录中。
本文中的模型构建基于 Beltramelli 的论文《pix2code: Generating Code from a Graphical User Interface Screenshot》和 Jason Brownlee 的图像描述生成教程,并使用 Python 和 Keras 完成。
核心逻辑
我们的目标是构建一个神经网络,能够生成与截图对应的 HTML/CSS 标记语言。
训练神经网络时,你先提供几个截图和对应的 HTML 代码。网络通过逐个预测所有匹配的 HTML 标记语言来学习。预测下一个标记语言的标签时,网络接收到截图和之前所有正确的标记。
这里是一个简单的训练数据示例:docs.google.com/spreadsheet…。
创建逐词预测的模型是现在最常用的方法,也是本教程使用的方法。
注意:每次预测时,神经网络接收的是同样的截图。也就是说如果网络需要预测 20 个单词,它就会得到 20 次同样的设计截图。现在,不用管神经网络的工作原理,只需要专注于神经网络的输入和输出。
我们先来看前面的标记(markup)。假如我们训练神经网络的目的是预测句子「I can code」。当网络接收「I」时,预测「can」。下一次时,网络接收「I can」,预测「code」。它接收所有之前单词,但只预测下一个单词。
神经网络根据数据创建特征。神经网络构建特征以连接输入数据和输出数据。它必须创建表征来理解每个截图的内容和它所需要预测的 HTML 语法,这些都是为预测下一个标记构建知识。把训练好的模型应用到真实世界中和模型训练过程差不多。
我们无需输入正确的 HTML 标记,网络会接收它目前生成的标记,然后预测下一个标记。预测从「起始标签」(start tag)开始,到「结束标签」(end tag)终止,或者达到最大限制时终止。
Hello World 版
现在让我们构建 Hello World 版实现。我们将馈送一张带有「Hello World!」字样的截屏到神经网络中,并训练它生成对应的标记语言。
首先,神经网络将原型设计转换为一组像素值。且每一个像素点有 RGB 三个通道,每个通道的值都在 0-255 之间。
为了以神经网络能理解的方式表征这些标记,我使用了 one-hot 编码。因此句子「I can code」可以映射为以下形式。
在上图中,我们的编码包含了开始和结束的标签。这些标签能为神经网络提供开始预测和结束预测的位置信息。以下是这些标签的各种组合以及对应 one-hot 编码的情况。
我们会使每个单词在每一轮训练中改变位置,因此这允许模型学习序列而不是记忆词的位置。在下图中有四个预测,每一行是一个预测。且左边代表 RGB 三色通道和之前的词,右边代表预测结果和红色的结束标签。
#Length of longest sentence max_caption_len=3 #Size of vocabulary vocab_size=3 # Load one screenshot for each word and turn them into digits images=[] for i in range(2): images.append(img_to_array(load_img('screenshot.jpg', target_size=(224, 224)))) images=np.array(images, dtype=float) # Preprocess input for the VGG16 model images=preprocess_input(images) #Turn start tokens into one-hot encoding html_input=np.array( [[[0., 0., 0.], #start [0., 0., 0.], [1., 0., 0.]], [[0., 0., 0.], #start <HTML>Hello World!</HTML> [1., 0., 0.], [0., 1., 0.]]]) #Turn next word into one-hot encoding next_words=np.array( [[0., 1., 0.], # <HTML>Hello World!</HTML> [0., 0., 1.]]) # end # Load the VGG16 model trained on imagenet and output the classification feature VGG=VGG16(weights='imagenet', include_top=True) # Extract the features from the image features=VGG.predict(images) #Load the feature to the network, apply a dense layer, and repeat the vector vgg_feature=Input(shape=(1000,)) vgg_feature_dense=Dense(5)(vgg_feature) vgg_feature_repeat=RepeatVector(max_caption_len)(vgg_feature_dense) # Extract information from the input seqence language_input=Input(shape=(vocab_size, vocab_size)) language_model=LSTM(5, return_sequences=True)(language_input) # Concatenate the information from the image and the input decoder=concatenate([vgg_feature_repeat, language_model]) # Extract information from the concatenated output decoder=LSTM(5, return_sequences=False)(decoder) # Predict which word comes next decoder_output=Dense(vocab_size, activation='softmax')(decoder) # Compile and run the neural network model=Model(inputs=[vgg_feature, language_input], outputs=decoder_output) model.compile(loss='categorical_crossentropy', optimizer='rmsprop') # Train the neural network model.fit([features, html_input], next_words, batch_size=2, shuffle=False, epochs=1000) 复制代码
在 Hello World 版本中,我们使用三个符号「start」、「Hello World」和「end」。字符级的模型要求更小的词汇表和受限的神经网络,而单词级的符号在这里可能有更好的性能。
以下是执行预测的代码:
# Create an empty sentence and insert the start token sentence=np.zeros((1, 3, 3)) # [[0,0,0], [0,0,0], [0,0,0]] start_token=[1., 0., 0.] # start sentence[0][2]=start_token # place start in empty sentence # Making the first prediction with the start token second_word=model.predict([np.array([features[1]]), sentence]) # Put the second word in the sentence and make the final prediction sentence[0][1]=start_token sentence[0][2]=np.round(second_word) third_word=model.predict([np.array([features[1]]), sentence]) # Place the start token and our two predictions in the sentence sentence[0][0]=start_token sentence[0][1]=np.round(second_word) sentence[0][2]=np.round(third_word) # Transform our one-hot predictions into the final tokens vocabulary=["start", "<HTML><center><H1>Hello World!</H1></center></HTML>", "end"] for i in sentence[0]: print(vocabulary[np.argmax(i)], end=' ') 复制代码
输出
我走过的坑:
在 FloydHub 上运行代码
FloydHub 是一个深度学习训练平台,我自从开始学习深度学习时就对它有所了解,我也常用它训练和管理深度学习试验。我们能安装它并在 10 分钟内运行第一个模型,它是在云 GPU 上训练模型最好的选择。若果读者没用过 FloydHub,可以花 10 分钟左右安装并了解。
FloydHub 地址:www.floydhub.com/
复制 Repo:
https://github.com/emilwallner/Screenshot-to-code-in-Keras.git 复制代码
登录并初始化 FloydHub 命令行工具:
cd Screenshot-to-code-in-Keras floyd login floyd init s2c 复制代码
在 FloydHub 云 GPU 机器上运行 Jupyter notebook:
floyd run --gpu --env tensorflow-1.4 --data emilwallner/datasets/imagetocode/2:data --mode jupyter 复制代码
所有的 notebook 都放在 floydbub 目录下。一旦我们开始运行模型,那么在 floydhub/Helloworld/helloworld.ipynb 下可以找到第一个 Notebook。更多详情请查看本项目早期的 flags。
HTML 版本
在这个版本中,我们将关注与创建一个可扩展的神经网络模型。该版本并不能直接从随机网页预测 HTML,但它是探索动态问题不可缺少的步骤。
概览
如果我们将前面的架构扩展为以下右图展示的结构,那么它就能更高效地处理识别与转换过程。
该架构主要有两个部,即编码器与解码器。编码器是我们创建图像特征和前面标记特征(markup features)的部分。特征是网络创建原型设计和标记语言之间联系的构建块。在编码器的末尾,我们将图像特征传递给前面标记的每一个单词。随后解码器将结合原型设计特征和标记特征以创建下一个标签的特征,这一个特征可以通过全连接层预测下一个标签。
设计原型的特征
因为我们需要为每个单词插入一个截屏,这将会成为训练神经网络的瓶颈。因此我们抽取生成标记语言所需要的信息来替代直接使用图像。这些抽取的信息将通过预训练的 CNN 编码到图像特征中,且我们将使用分类层之前的层级输出以抽取特征。
我们最终得到 1536 个 8*8 的特征图,虽然我们很难直观地理解它,但神经网络能够从这些特征中抽取元素的对象和位置。
标记特征
在 Hello World 版本中,我们使用 one-hot 编码以表征标记。而在该版本中,我们将使用词嵌入表征输入并使用 one-hot 编码表示输出。我们构建每个句子的方式保持不变,但我们映射每个符号的方式将会变化。one-hot 编码将每一个词视为独立的单元,而词嵌入会将输入数据表征为一个实数列表,这些实数表示标记标签之间的关系。
上面词嵌入的维度为 8,但一般词嵌入的维度会根据词汇表的大小在 50 到 500 间变动。以上每个单词的八个数值就类似于神经网络中的权重,它们倾向于刻画单词之间的联系(Mikolov alt el., 2013)。这就是我们开始部署标记特征(markup features)的方式,而这些神经网络训练的特征会将输入数据和输出数据联系起来。
编码器
我们现在将词嵌入馈送到 LSTM 中,并期望能返回一系列的标记特征。这些标记特征随后会馈送到一个 Time Distributed 密集层,该层级可以视为有多个输入和输出的全连接层。
和嵌入与 LSTM 层相平行的还有另外一个处理过程,其中图像特征首先会展开成一个向量,然后再馈送到一个全连接层而抽取出高级特征。这些图像特征随后会与标记特征相级联而作为编码器的输出。
标记特征
如下图所示,现在我们将词嵌入投入到 LSTM 层中,所有的语句都会用零填充以获得相同的向量长度。
为了混合信号并寻找高级模式,我们运用了一个 TimeDistributed 密集层以抽取标记特征。TimeDistributed 密集层和一般的全连接层非常相似,且它有多个输入与输出。
图像特征
对于另一个平行的过程,我们需要将图像的所有像素值展开成一个向量,因此信息不会被改变,它们只会用来识别。
如上,我们会通过全连接层混合信号并抽取更高级的概念。因为我们并不只是处理一个输入值,因此使用一般的全连接层就行了。
级联图像特征和标记特征
所有的语句都被填充以创建三个标记特征。因为我们已经预处理了图像特征,所以我们能为每一个标记特征添加图像特征。
如上,在复制图像特征到对应的标记特征后,我们得到了新的图像-标记特征(image-markup features),这就是我们馈送到解码器的输入值。
解码器
现在,我们使用图像-标记特征来预测下一个标签。
在下面的案例中,我们使用三个图像-标签特征对来输出下一个标签特征。注意 LSTM 层不应该返回一个长度等于输入序列的向量,而只需要预测预测一个特征。在我们的案例中,这个特征将预测下一个标签,它包含了最后预测的信息。
最后的预测
密集层会像传统前馈网络那样工作,它将下一个标签特征中的 512 个值与最后的四个预测连接起来,即我们在词汇表所拥有的四个单词:start、hello、world 和 end。密集层最后采用的 softmax 函数会为四个类别产生一个概率分布,例如 [0.1, 0.1, 0.1, 0.7] 将预测第四个词为下一个标签。
# Load the images and preprocess them for inception-resnet images=[] all_filenames=listdir('images/') all_filenames.sort() for filename in all_filenames: images.append(img_to_array(load_img('images/'+filename, target_size=(299, 299)))) images=np.array(images, dtype=float) images=preprocess_input(images) # Run the images through inception-resnet and extract the features without the classification layer IR2=InceptionResNetV2(weights='imagenet', include_top=False) features=IR2.predict(images) # We will cap each input sequence to 100 tokens max_caption_len=100 # Initialize the function that will create our vocabulary tokenizer=Tokenizer(filters='', split=" ", lower=False) # Read a document and return a string def load_doc(filename): file=open(filename, 'r') text=file.read() file.close() return text # Load all the HTML files X=[] all_filenames=listdir('html/') all_filenames.sort() for filename in all_filenames: X.append(load_doc('html/'+filename)) # Create the vocabulary from the html files tokenizer.fit_on_texts(X) # Add +1 to leave space for empty words vocab_size=len(tokenizer.word_index) + 1 # Translate each word in text file to the matching vocabulary index sequences=tokenizer.texts_to_sequences(X) # The longest HTML file max_length=max(len(s) for s in sequences) # Intialize our final input to the model X, y, image_data=list(), list(), list() for img_no, seq in enumerate(sequences): for i in range(1, len(seq)): # Add the entire sequence to the input and only keep the next word for the output in_seq, out_seq=seq[:i], seq[i] # If the sentence is shorter than max_length, fill it up with empty words in_seq=pad_sequences([in_seq], maxlen=max_length)[0] # Map the output to one-hot encoding out_seq=to_categorical([out_seq], num_classes=vocab_size)[0] # Add and image corresponding to the HTML file image_data.append(features[img_no]) # Cut the input sentence to 100 tokens, and add it to the input data X.append(in_seq[-100:]) y.append(out_seq) X, y, image_data=np.array(X), np.array(y), np.array(image_data) # Create the encoder image_features=Input(shape=(8, 8, 1536,)) image_flat=Flatten()(image_features) image_flat=Dense(128, activation='relu')(image_flat) ir2_out=RepeatVector(max_caption_len)(image_flat) language_input=Input(shape=(max_caption_len,)) language_model=Embedding(vocab_size, 200, input_length=max_caption_len)(language_input) language_model=LSTM(256, return_sequences=True)(language_model) language_model=LSTM(256, return_sequences=True)(language_model) language_model=TimeDistributed(Dense(128, activation='relu'))(language_model) # Create the decoder decoder=concatenate([ir2_out, language_model]) decoder=LSTM(512, return_sequences=False)(decoder) decoder_output=Dense(vocab_size, activation='softmax')(decoder) # Compile the model model=Model(inputs=[image_features, language_input], outputs=decoder_output) model.compile(loss='categorical_crossentropy', optimizer='rmsprop') # Train the neural network model.fit([image_data, X], y, batch_size=64, shuffle=False, epochs=2) # map an integer to a word def word_for_id(integer, tokenizer): for word, index in tokenizer.word_index.items(): if index==integer: return word return None # generate a description for an image def generate_desc(model, tokenizer, photo, max_length): # seed the generation process in_text='START' # iterate over the whole length of the sequence for i in range(900): # integer encode input sequence sequence=tokenizer.texts_to_sequences([in_text])[0][-100:] # pad input sequence=pad_sequences([sequence], maxlen=max_length) # predict next word yhat=model.predict([photo,sequence], verbose=0) # convert probability to integer yhat=np.argmax(yhat) # map integer to word word=word_for_id(yhat, tokenizer) # stop if we cannot map the word if word is None: break # append as input for generating the next word in_text +=' ' + word # Print the prediction print(' ' + word, end='') # stop if we predict the end of the sequence if word=='END': break return # Load and image, preprocess it for IR2, extract features and generate the HTML test_image=img_to_array(load_img('images/87.jpg', target_size=(299, 299))) test_image=np.array(test_image, dtype=float) test_image=preprocess_input(test_image) test_features=IR2.predict(np.array([test_image])) generate_desc(model, tokenizer, np.array(test_features), 100) 复制代码
输出
训练不同轮数所生成网站的地址:
我走过的坑:
Bootstrap 版本
在最终版本中,我们使用 pix2code 论文中生成 bootstrap 网站的数据集。使用 Twitter 的 Bootstrap 库(getbootstrap.com/),我们可以结合 HTML 和 CSS,降低词汇表规模。
我们将使用这一版本为之前未见过的截图生成标记。我们还深入研究它如何构建截图和标记的先验知识。
我们不在 bootstrap 标记上训练,而是使用 17 个简化 token,将其编译成 HTML 和 CSS。数据集(github.com/tonybeltram…)包括 1500 个测试截图和 250 个验证截图。平均每个截图有 65 个 token,一共有 96925 个训练样本。
我们稍微修改一下 pix2code 论文中的模型,使之预测网络组件的准确率达到 97%。
端到端方法
从预训练模型中提取特征在图像描述生成模型中效果很好。但是几次实验后,我发现 pix2code 的端到端方法效果更好。在我们的模型中,我们用轻量级卷积神经网络替换预训练图像特征。我们不使用最大池化来增加信息密度,而是增加步幅。这可以保持前端元素的位置和颜色。
存在两个核心模型:卷积神经网络(CNN)和循环神经网络(RNN)。最常用的循环神经网络是长短期记忆(LSTM)网络。我之前的文章中介绍过 CNN 教程,本文主要介绍 LSTM。
理解 LSTM 中的时间步
关于 LSTM 比较难理解的是时间步。我们的原始神经网络有两个时间步,如果你给它「Hello」,它就会预测「World」。但是它会试图预测更多时间步。下例中,输入有四个时间步,每个单词对应一个时间步。
LSTM 适合时序数据的输入,它是一种适合顺序信息的神经网络。模型展开图示如下,对于每个循环步,你需要保持同样的权重。
加权后的输入与输出特征在级联后输入到激活函数,并作为当前时间步的输出。因为我们重复利用了相同的权重,它们将从一些输入获取信息并构建序列的知识。下面是 LSTM 在每一个时间步上的简化版处理过程:
理解 LSTM 层级中的单元
每一层 LSTM 单元的总数决定了它记忆的能力,同样也对应于每一个输出特征的维度大小。LSTM 层级中的每一个单元将学习如何追踪句法的不同方面。以下是一个 LSTM 单元追踪标签行信息的可视化,它是我们用来训练 bootstrap 模型的简单标记语言。
每一个 LSTM 单元会维持一个单元状态,我们可以将单元状态视为记忆。权重和激活值可使用不同的方式修正状态值,这令 LSTM 层可以通过保留或遗忘输入信息而得到精调。除了处理当前输入信息与输出信息,LSTM 单元还需要修正记忆状态以传递到下一个时间步。
dir_name='resources/eval_light/' # Read a file and return a string def load_doc(filename): file=open(filename, 'r') text=file.read() file.close() return text def load_data(data_dir): text=[] images=[] # Load all the files and order them all_filenames=listdir(data_dir) all_filenames.sort() for filename in (all_filenames): if filename[-3:]=="npz": # Load the images already prepared in arrays image=np.load(data_dir+filename) images.append(image['features']) else: # Load the boostrap tokens and rap them in a start and end tag syntax='<START> ' + load_doc(data_dir+filename) + ' <END>' # Seperate all the words with a single space syntax=' '.join(syntax.split()) # Add a space after each comma syntax=syntax.replace(',', ' ,') text.append(syntax) images=np.array(images, dtype=float) return images, text train_features, texts=load_data(dir_name) # Initialize the function to create the vocabulary tokenizer=Tokenizer(filters='', split=" ", lower=False) # Create the vocabulary tokenizer.fit_on_texts([load_doc('bootstrap.vocab')]) # Add one spot for the empty word in the vocabulary vocab_size=len(tokenizer.word_index) + 1 # Map the input sentences into the vocabulary indexes train_sequences=tokenizer.texts_to_sequences(texts) # The longest set of boostrap tokens max_sequence=max(len(s) for s in train_sequences) # Specify how many tokens to have in each input sentence max_length=48 def preprocess_data(sequences, features): X, y, image_data=list(), list(), list() for img_no, seq in enumerate(sequences): for i in range(1, len(seq)): # Add the sentence until the current count(i) and add the current count to the output in_seq, out_seq=seq[:i], seq[i] # Pad all the input token sentences to max_sequence in_seq=pad_sequences([in_seq], maxlen=max_sequence)[0] # Turn the output into one-hot encoding out_seq=to_categorical([out_seq], num_classes=vocab_size)[0] # Add the corresponding image to the boostrap token file image_data.append(features[img_no]) # Cap the input sentence to 48 tokens and add it X.append(in_seq[-48:]) y.append(out_seq) return np.array(X), np.array(y), np.array(image_data) X, y, image_data=preprocess_data(train_sequences, train_features) #Create the encoder image_model=Sequential() image_model.add(Conv2D(16, (3, 3), padding='valid', activation='relu', input_shape=(256, 256, 3,))) image_model.add(Conv2D(16, (3,3), activation='relu', padding='same', strides=2)) image_model.add(Conv2D(32, (3,3), activation='relu', padding='same')) image_model.add(Conv2D(32, (3,3), activation='relu', padding='same', strides=2)) image_model.add(Conv2D(64, (3,3), activation='relu', padding='same')) image_model.add(Conv2D(64, (3,3), activation='relu', padding='same', strides=2)) image_model.add(Conv2D(128, (3,3), activation='relu', padding='same')) 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(max_length)) visual_input=Input(shape=(256, 256, 3,)) encoded_image=image_model(visual_input) language_input=Input(shape=(max_length,)) language_model=Embedding(vocab_size, 50, input_length=max_length, mask_zero=True)(language_input) language_model=LSTM(128, return_sequences=True)(language_model) language_model=LSTM(128, return_sequences=True)(language_model) #Create the decoder decoder=concatenate([encoded_image, language_model]) decoder=LSTM(512, return_sequences=True)(decoder) decoder=LSTM(512, return_sequences=False)(decoder) decoder=Dense(vocab_size, activation='softmax')(decoder) # Compile the model model=Model(inputs=[visual_input, language_input], outputs=decoder) optimizer=RMSprop(lr=0.0001, clipvalue=1.0) model.compile(loss='categorical_crossentropy', optimizer=optimizer) #Save the model for every 2nd epoch filepath="org-weights-epoch-{epoch:04d}--val_loss-{val_loss:.4f}--loss-{loss:.4f}.hdf5" checkpoint=ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_weights_only=True, period=2) callbacks_list=[checkpoint] # Train the model model.fit([image_data, X], y, batch_size=64, shuffle=False, validation_split=0.1, callbacks=callbacks_list, verbose=1, epochs=50) 复制代码
测试准确率
找到一种测量准确率的优秀方法非常棘手。比如一个词一个词地对比,如果你的预测中有一个词不对照,准确率可能就是 0。如果你把百分百对照的单词移除一个,最终的准确率可能是 99/100。
我使用的是 BLEU 分值,它在机器翻译和图像描述模型实践上都是最好的。它把句子分解成 4 个 n-gram,从 1-4 个单词的序列。在下面的预测中,「cat」应该是「code」。
为了得到最终的分值,每个的分值需要乘以 25%,(4/5) × 0.25 + (2/4) × 0.25 + (1/3) × 0.25 + (0/2) ×0.25=0.2 + 0.125 + 0.083 + 0=0.408。然后用总和乘以句子长度的惩罚函数。因为在我们的示例中,长度是正确的,所以它就直接是我们的最终得分。
你可以增加 n-gram 的数量,4 个 n-gram 的模型是最为对应人类翻译的。我建议你阅读下面的代码:
#Create a function to read a file and return its content def load_doc(filename): file=open(filename, 'r') text=file.read() file.close() return text def load_data(data_dir): text=[] images=[] files_in_folder=os.listdir(data_dir) files_in_folder.sort() for filename in tqdm(files_in_folder): #Add an image if filename[-3:]=="npz": image=np.load(data_dir+filename) images.append(image['features']) else: # Add text and wrap it in a start and end tag syntax='<START> ' + load_doc(data_dir+filename) + ' <END>' #Seperate each word with a space syntax=' '.join(syntax.split()) #Add a space between each comma syntax=syntax.replace(',', ' ,') text.append(syntax) images=np.array(images, dtype=float) return images, text #Intialize the function to create the vocabulary tokenizer=Tokenizer(filters='', split=" ", lower=False) #Create the vocabulary in a specific order tokenizer.fit_on_texts([load_doc('bootstrap.vocab')]) dir_name='../../../../eval/' train_features, texts=load_data(dir_name) #load model and weights json_file=open('../../../../model.json', 'r') loaded_model_json=json_file.read() json_file.close() loaded_model=model_from_json(loaded_model_json) # load weights into new model loaded_model.load_weights("../../../../weights.hdf5") print("Loaded model from disk") # map an integer to a word def word_for_id(integer, tokenizer): for word, index in tokenizer.word_index.items(): if index==integer: return word return None print(word_for_id(17, tokenizer)) # generate a description for an image def generate_desc(model, tokenizer, photo, max_length): photo=np.array([photo]) # seed the generation process in_text='<START> ' # iterate over the whole length of the sequence print('\nPrediction---->\n\n<START> ', end='') for i in range(150): # integer encode input sequence sequence=tokenizer.texts_to_sequences([in_text])[0] # pad input sequence=pad_sequences([sequence], maxlen=max_length) # predict next word yhat=loaded_model.predict([photo, sequence], verbose=0) # convert probability to integer yhat=argmax(yhat) # map integer to word word=word_for_id(yhat, tokenizer) # stop if we cannot map the word if word is None: break # append as input for generating the next word in_text +=word + ' ' # stop if we predict the end of the sequence print(word + ' ', end='') if word=='<END>': break return in_text max_length=48 # evaluate the skill of the model def evaluate_model(model, descriptions, photos, tokenizer, max_length): actual, predicted=list(), list() # step over the whole set for i in range(len(texts)): yhat=generate_desc(model, tokenizer, photos[i], max_length) # store actual and predicted print('\n\nReal---->\n\n' + texts[i]) actual.append([texts[i].split()]) predicted.append(yhat.split()) # calculate BLEU score bleu=corpus_bleu(actual, predicted) return bleu, actual, predicted bleu, actual, predicted=evaluate_model(loaded_model, texts, train_features, tokenizer, max_length) #Compile the tokens into HTML and css dsl_path="compiler/assets/web-dsl-mapping.json" compiler=Compiler(dsl_path) compiled_website=compiler.compile(predicted[0], 'index.html') print(compiled_website ) print(bleu) 复制代码
输出
样本输出的链接:
我走过的坑:
下一步
前端开发是深度学习应用的理想空间。数据容易生成,并且当前深度学习算法可以映射绝大部分逻辑。一个最让人激动的领域是注意力机制在 LSTM 上的应用。这不仅会提升精确度,还可以使我们可视化 CNN 在生成标记时所聚焦的地方。注意力同样是标记、可定义模板、脚本和最终端之间通信的关键。注意力层要追踪变量,使网络可以在编程语言之间保持通信。
但是在不久的将来,最大的影响将会来自合成数据的可扩展方法。接着你可以一步步添加字体、颜色和动画。目前为止,大多数进步发生在草图(sketches)方面并将其转化为模版应用。在不到两年的时间里,我们将创建一个草图,它会在一秒之内找到相应的前端。Airbnb 设计团队与 Uizard 已经创建了两个正在使用的原型。下面是一些可能的试验过程:
实验
开始
进一步实验
原文链接:blog.floydhub.com/turning-des…
本文为机器之心编译,转载请联系本公众号获得授权。
*请认真填写需求信息,我们会在24小时内与您取得联系。