整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

用原生Javascript制作一个随机移动的图片动画

eb Animation API 介绍

当我们谈及网页动画时,自然联想到的是 CSS3 动画、JS 动画、SVG 动画 等技术以及 jQuery.animate() 等动画封装库,根据实际动画内容设计去选择不同的实现方式,然而,每个现行的动画技术都存在一定的缺点,如 CSS3动画必须通过JS去获取动态改变的值,一个动画效果分散在css文件和js文件里不好维护,setInterval 的时间往往是不精确的而且还会卡顿,引入额外的动画封装库也并非对性能敏感的业务适用。

Web Animation API 的历史也应该有几年了,但是每当做动画效果时,笔者就是依赖各种库,很少想着去原生实现,最终造成了我们的项目各种依赖库,体积也不断变大,性能如何也不得而知,作为前端开发的我们多么希望原生的JS去支持通用的动画解决方案, Web Animation API 可能就是一个不错的解决方案。

W3C 提出 Web Animation API(简称 WAAPI)正缘于此,它致力于集合 CSS3 动画的性能、JavaScript 的灵活、动画库的丰富等各家所长,将尽可能多的动画控制由原生浏览器实现,并添加许多 CSS 不具备的变量、控制以及或调的选项。它为我们提供了一种通用语言来描述DOM元素的动画,主要方法有:Animation,KeyframeEffect,AnimationEvent,DocumentTimeline,EffectTiming。关于这个API的详细介绍,可以参照MDN的这篇文档,链接地址:https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API。

使用Web Animations API,我们可以将交互式动画从样式表移动到JavaScript,将表示与行为分开。我们不再需要依赖DOM的技术,例如编写CSS属性作用于元素以控制方向。为了构建自定义动画库和创建交互式动画,Web Animations API可能是完成工作的完美工具,你无需借助第三方动画库,就可以轻松实现一个效果不错的动画。

为了让大家对这个API有个清晰的认识,笔者在接下来的系列文章里,用五六个例子让大家理解这个API,今天笔者将用此API实现一个随机移动的图片开始进行介绍,比如用这个效果我们可以制作一个随机飘浮移动的广告位,游戏里随机走动的怪物等等,本例中的特点就是为了体现Web Animation API的灵活性和强大性,我没有引用任何第三方类库,比如(JQ)以及也没有使用setTimeout和requestAnimationFrame()函数。

本篇文章预计时间 5 分钟

动画效果

开始前,我们先来看看完成后的动画效果,示例如下效果:

页面布局

无论图片怎么随机移动,我们都希望在指定的容器里,而不是漫无边际,首先我们在html页面定义容器:

<div id="container">
</div>

接下来定义容器的样式:

body {
 margin: 0;
}
div#container {
 height:500px;
 width:100%;
 background: #C6CEF7;
}
#target {
 position: absolute;
 filter: drop-shadow(-12px 12px 7px rgba(0,0,0,0.5));
}

脚本部分

获取容器

var container = document.getElementById("container");

加载动画

为了更加直观性,我选择一个走动的gif图片,由于图片的加载需要一些时间,为了不破坏动画的连贯性,确保图片加载完了我们在执行动画,相关代码如下:

var target = document.createElement("img");
target.id = "target";
target.onload = function() {
 floatHead();
}
target.src = "walk.gif";
container.appendChild(target);

大家都看到了,onload部分我们加载了floatHead()函数,接下来我们来进行相关实现,此函数主要包含以下功能:创建一个随机位置,计算移动时间,封装移动动画。

随机位置

我们利用Math.floor函数实现了其随机位置的变化,示例代码如下:

function makeNewPosition() {
 var containerVspace = container.offsetHeight - target.offsetHeight,
 containerHspace = container.offsetWidth - target.offsetWidth,
 newX = Math.floor(Math.random() * containerVspace),
 newY = Math.floor(Math.random() * containerHspace);
 return [newX, newY];
}

这里的随机位置,我们返回了一个数组,描述的是图片相对容器的位置,即top,left。这里你需要理解offsetHeight,offsetWidth,可理解为div的可视高度或宽度,样式的height或Width+上下padding或左右padding+上下border-width或左右border-width。

计算时间

动画是有时间属性的,我们进行位置的移动,需要花多久时间,假设运动速度为0.1个单位/毫秒。这个函数包含两个数组:prev为当前目标的原始X和Y位置,next为移动目标的位置。此函数没有进行进行精确的距离计算,只是判断了x和y轴上移动的距离大小用最大的距离除以速度,示例代码如下:

function velocity(prev, next) { 
 var x = Math.abs(prev[1] - next[1]),
 y = Math.abs(prev[0] - next[0]),
 larger = x > y ? x : y,
 speedModifier = 0.1,
 time = Math.ceil(larger / speedModifier);
 return time; 
}

封装移动动画

接下来是我们Web Animations API的核心部分,我们使用其核心API在加上上述我们完成的两个函数让其动起来,示例代码如下:

function floatHead() {
 var newPos = makeNewPosition(),
 oldTop = target.offsetTop,
 oldLeft = target.offsetLeft,
 target.animate([
 { top: oldTop+"px", left: oldLeft+"px" },
 { top: newPos[0]+"px", left: newPos[1]+"px" }
 ], {
 duration: velocity([oldTop, oldLeft],newPos),
 fill: "forwards"
 }).onfinish = function() {
 floatHead();
 }
}

该Animation的animate函数有两个参数,一个是KeyframeEffects数组和AnimationEffectTimingPropertiesoptions 的对象。基本上,第一个参数映射到您将放入CSS中的内容@keyframes,你可以想象成css中的@keyframes内容,比如以下代码:

@keyframes emphasis {
 0% {
 transform: scale(1); 
 opacity: 1; 
 }
 30% {
 transform: scale(.5); 
 opacity: .5; 
 }
 78.75% {
 transform: scale(.667); 
 opacity: .667; 
 }
 100% {
 transform: scale(.6);
 opacity: .6; 
 }
}

你可以将“{}”里的信息顺序依次放到一个数组里;第二个参数是时间控制 timing,包括有 duration 持续时间、iterations 执行次数、direction 动画方向、easing 缓动函数等属性。比如以下代码:

#toAnimate {
 animation: emphasis 700ms ease-in-out 10ms infinite alternate forwards;
}

你还可能注意到我们使用了onfinish事件完成了floatHead函数的反复调用,其是Animation的属性,监听动画完成事件,如果动画完成继续执行floatHead(),相当不断的递归调用。

最终完成的代码

<!DOCTYPE html>
<html lang="en">
<head>
 <style>
 body {
 margin: 0;
 }
 div#container {
 height:500px;
 width:100%;
 background: #C6CEF7;
 }
 #target {
 position: absolute;
 filter: drop-shadow(-12px 12px 7px rgba(0,0,0,0.5));
 }
 </style>
 <meta charset="UTF-8">
 <title>前端达人示例展示——图片随机移动</title>
</head>
<body>
<div id="container"></div>
<script>
 function makeNewPosition() {
 var containerVspace = container.offsetHeight - target.offsetHeight,
 containerHspace = container.offsetWidth - target.offsetWidth,
 newX = Math.floor(Math.random() * containerVspace),
 newY = Math.floor(Math.random() * containerHspace);
 return [newX, newY];
 }
 function velocity(prev, next) {
 var x = Math.abs(prev[1] - next[1]),
 y = Math.abs(prev[0] - next[0]),
 larger = x > y ? x : y,
 speedModifier = 0.2,
 time = Math.ceil(larger / speedModifier);
 return time;
 }
 function floatHead() {
 var newPos = makeNewPosition(),
 oldTop = target.offsetTop,
 oldLeft = target.offsetLeft;
 target.animate([
 { top: oldTop+"px", left: oldLeft+"px" },
 { top: newPos[0]+"px", left: newPos[1]+"px" }
 ], {
 duration: velocity([oldTop, oldLeft],newPos),
 fill: 'forwards'
 }).onfinish = function() {
 floatHead();
 }
 }
 var container = document.getElementById("container"),
 target = document.createElement("img");
 target.id = "target";
 target.onload = function() {
 floatHead();
 }
 target.src = "walk.gif";
 target.width="200";
 container.appendChild(target);
</script>
</body>
</html>

兼容情况

最后聊聊你关心的各浏览器兼容问题,如下所示显示了各个浏览器的兼容情况:

看来好多都是部分支持,没有完全支持,笔者也亲自测试了下,在pc端最新版的谷歌浏览器和Firefox是没有任何问题的可以完美运行,笔者的safari还是运行不起来,在iPhone XS Max无法运行。

作为一名前端开发者,在移动端大行其道怎么能容忍在手机端没有效果,为了在现代浏览器厂商还没完全跟进到位的时候抢先用上 WAAPI(Web Animation API简称),我们可以选择引入针对 Web Animation API 的 Polyfill 库 [https://github.com/web-animations/web-animations-js],从而在 IE/Firefox/Safari 等浏览器上体验到 WAAPI 的精彩。

因此我们只需要文件里引入以下js,就可以完美体验:

<script src="https://cdn.jsdelivr.net/web-animations/latest/web-animations.min.js"></script>

移动端浏览器,Android 5.0 以上的 Android Browser 和 Chrome for Android 本身就已经支持 WAAPI 了,加上 Polyfill 之后,笔者的手机终于可以看到运行效果了,微信里的QQ内核浏览器也能完美运行,pc端的safari也可以完美运行。可以说是全平台支持了,有了这个库你可以放心大胆的使用了。

小节

好了今天的代码撸完了,js代码还不到50行(注:为了在手机端运行,引入了web-animations.min.js),您可以点击"https://www.qianduandaren.com/demo/walk/"行预览,笔者亲测在iPhone XS Max运行良好,其他手机没有,有待亲们的测试,欢迎到留言区告知。下一篇文章我们用不到20行的原生js代码纯手工撸一个漂亮的时钟,敬请期待。

更多精彩内容,请微信关注“前端达人”公众号

深度学习自动生成HTML代码

选自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 层。

代码地址:

  • github.com/emilwallner…
  • www.floydhub.com/emilwallner…

所有 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=' ')
复制代码

输出

  • 10 epochs: start start start
  • 100 epochs: start <HTML><center><H1>Hello World!</H1></center></HTML> <HTML><center><H1>Hello World!</H1></center></HTML>
  • 300 epochs: start <HTML><center><H1>Hello World!</H1></center></HTML> end

我走过的坑:

  • 在收集数据之前构建第一个版本。在本项目的早期阶段,我设法获得 Geocities 托管网站的旧版存档,它有 3800 万的网站。但我忽略了减少 100K 大小词汇所需要的巨大工作量。
  • 训练一个 TB 级的数据需要优秀的硬件或极其有耐心。在我的 Mac 遇到几个问题后,最终用上了强大的远程服务器。我预计租用 8 个现代 CPU 和 1 GPS 内部链接以运行我的工作流。
  • 在理解输入与输出数据之前,其它部分都似懂非懂。输入 X 是屏幕的截图和以前标记的标签,输出 Y 是下一个标记的标签。当我理解这一点时,其它问题都更加容易弄清了。此外,尝试其它不同的架构也将更加容易。
  • 图片到代码的网络其实就是自动描述图像的模型。即使我意识到了这一点,但仍然错过了很多自动图像摘要方面的论文,因为它们看起来不够炫酷。一旦我意识到了这一点,我对问题空间的理解就变得更加深刻了。

在 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)
复制代码

输出

训练不同轮数所生成网站的地址:

  • 250 epochs:emilwallner.github.io/html/250_ep…
  • 350 epochs:emilwallner.github.io/html/350_ep…
  • 450 epochs:emilwallner.github.io/html/450_ep…
  • 550 epochs:emilwallner.github.io/html/550_ep…

我走过的坑:

  • 我认为理解 LSTM 比 CNN 要难一些。当我展开 LSTM 后,它们会变得容易理解一些。此外,我们在尝试理解 LSTM 前,可以先关注输入与输出特征。
  • 从头构建一个词汇表要比压缩一个巨大的词汇表容易得多。这样的构建包括字体、div 标签大小、变量名的 hex 颜色和一般单词。
  • 大多数库是为解析文本文档而构建。在库的使用文档中,它们会告诉我们如何通过空格进行分割,而不是代码,我们需要自定义解析的方式。
  • 我们可以从 ImageNet 上预训练的模型抽取特征。然而,相对于从头训练的 pix2code 模型,损失要高 30% 左右。此外,我对于使用基于网页截屏预训练的 inception-resnet 网络很有兴趣。

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)
复制代码

输出

样本输出的链接:

  • Generated website 1 - Original 1 (emilwallner.github.io/bootstrap/r…)
  • Generated website 2 - Original 2 (emilwallner.github.io/bootstrap/r…)
  • Generated website 3 - Original 3 (emilwallner.github.io/bootstrap/r…)
  • Generated website 4 - Original 4 (emilwallner.github.io/bootstrap/r…)
  • Generated website 5 - Original 5 (emilwallner.github.io/bootstrap/r…)

我走过的坑:

  • 理解模型的弱点而不是测试随机模型。首先我使用随机的东西,比如批归一化、双向网络,并尝试实现注意力机制。在查看测试数据,并知道其无法高精度地预测颜色和位置之后,我意识到 CNN 存在一个弱点。这致使我使用增加的步幅来取代最大池化。验证损失从 0.12 降至 0.02,BLEU 分值从 85% 增加至 97%。
  • 如果它们相关,则只使用预训练模型。在小数据的情况下,我认为一个预训练图像模型将会提升性能。从我的实验来看,端到端模型训练更慢,需要更多内存,但是精确度会提升 30%。
  • 当你在远程服务器上运行模型,我们需要为一些不同做好准备。在我的 mac 上,它按照字母表顺序读取文档。但是在服务器上,它被随机定位。这在代码和截图之间造成了不匹配。

下一步

前端开发是深度学习应用的理想空间。数据容易生成,并且当前深度学习算法可以映射绝大部分逻辑。一个最让人激动的领域是注意力机制在 LSTM 上的应用。这不仅会提升精确度,还可以使我们可视化 CNN 在生成标记时所聚焦的地方。注意力同样是标记、可定义模板、脚本和最终端之间通信的关键。注意力层要追踪变量,使网络可以在编程语言之间保持通信。

但是在不久的将来,最大的影响将会来自合成数据的可扩展方法。接着你可以一步步添加字体、颜色和动画。目前为止,大多数进步发生在草图(sketches)方面并将其转化为模版应用。在不到两年的时间里,我们将创建一个草图,它会在一秒之内找到相应的前端。Airbnb 设计团队与 Uizard 已经创建了两个正在使用的原型。下面是一些可能的试验过程:

实验

开始

  • 运行所有模型
  • 尝试不同的超参数
  • 测试一个不同的 CNN 架构
  • 添加双向 LSTM 模型
  • 用不同数据集实现模型

进一步实验

  • 使用相应的语法创建一个稳定的随机应用/网页生成器
  • 从草图到应用模型的数据。自动将应用/网页截图转化为草图,并使用 GAN 创建多样性。
  • 应用注意力层可视化每一预测的图像聚焦,类似于这个模型
  • 为模块化方法创建一个框架。比如,有字体的编码器模型,一个用于颜色,另一个用于排版,并使用一个解码器整合它们。稳定的图像特征是一个好的开始。
  • 馈送简单的 HTML 组件到神经网络中,并使用 CSS 教其生成动画。使用注意力方法并可视化两个输入源的聚焦将会很迷人。

原文链接:blog.floydhub.com/turning-des…

本文为机器之心编译,转载请联系本公众号获得授权。

JavaScript 画一棵树?

产品说要让前端用 JavaScript 画一棵树出来,但是这难道不能直接让 UI 给一张图片吗?

后来一问才知道,产品要的是一颗 随机树,也就是树的茂盛程度、长度、枝干粗细都是随机的,那这确实没办法叫 UI 给图,毕竟 UI 不可能给我 10000 张树的图片吧?

所以第一时间想到的就是 Canvas,用它来画这棵随机树(文末有完整代码)

Canvas 画一颗随机树

接下来使用 Canvas 去画这棵随机树

基础页面

我们需要在页面上写一个 canvas 标签,并设置好宽高,同时需要获取它的 Dom 节点、绘制上下文,以便后续的绘制

坐标调整

默认的 Canvas 坐标系是这样的

但是我们现在需要从中间去向上去画一棵树,所以坐标得调整成这样:

  • X 轴从最上面移动到最下面
  • Y 轴的方向由往下调整成往上,并且从最左边移动到画布中间

这些操作可以使用 Canvas 的方法

  • ctx.translate: 坐标系移动
  • ctx.scale: 坐标系缩放

绘制一棵树的要素

绘制一棵树的要素是什么呢?其实就是树枝果实,但是其实树枝才是第一要素,那么树枝又有哪些要素呢?无非就这几个点

  • 起始点
  • 树枝长度、树枝粗细
  • 生长角度
  • 终点

开始绘制

所以我们可以写一个 drawBranch 来进行绘制,并且初始调用肯定是绘制树干,树干的参数如下:

  • 起始点:(0, 0)
  • 树枝长度、树枝粗细:这些可以自己自定义
  • 生长角度:90度
  • 终点:需要算

这个终点应该怎么算呢?其实很简单,根据树枝长度、生长角度就可以算出来了,这是初高中的知识

于是我们可以使用 Canvas 的绘制方法,去绘制线段,其实树枝就是一个一个的线段

到现在我绘制出了一个树干 出来

但是我们是想让这棵树开枝散叶,所以需要继续递归继续去绘制更多的树枝出来~

递归绘制

其实往哪开枝散叶呢?无非就是往左或者往右

所以需要递归画左边和右边的树枝,并且子树枝肯定要比父树枝更短、比父树枝更细,比如我们可以定义一个比例

  • 子树枝是父树枝长度的 0.8
  • 子树枝是父树枝粗细的 0.75

而子树枝的生长角度,其实可以随机,我们可以在 0° - 30° 之间随机选一个角度,于是增加了递归调用的代码

但是这个时候会发现,报错了,爆栈了,因为我们只递归开始,但却没有在某个时刻递归停止

我们可以自己定义一个停止规则(规则可以自己定义,这会决定你这棵树的茂盛程度):

  • 粗细小于 2 时马上停止
  • (粗细小于 10 时 + 随机数)决定是否停止

现在可以看到我们已经大致绘制出一棵树了

不过还少了树的果实

绘制果实

绘制果实很简单,只需要在绘制树枝结束的时候,去把果实绘制出来就行,其实果实就是一个个的白色实心圆

至此这棵树完整绘制完毕

绘制部分的代码如下

完整代码