小新 编译自 Insight Data Blog
量子位 出品 | 公众号 QbitAI
写个网页能有多麻烦?在大多数公司里,这项工作分为三步:
1. 产品经理完成用户调研任务后,列出一系列技术要求;
2. 设计师根据这些要求来设计低保真原型,逐渐修改得到高保真原型和UI设计图;
3. 工程师将这些设计图实现为代码,最终变成用户使用的产品。
这么多环节,任何地方出一点问题,都会拉长开发周期。因此,不少公司,比如Airbnb已经开始用机器学习来提高这个过程的效率。
△ Airbnb内部的AI工具,从图纸到代码一步到位
看起来很美好,但Airbnb还没公开该模型中端到端训练的细节,以及手工设计的图像特征对该模型的贡献度。这是该公司特有的闭源解决方案专利,可能不会进行公开。
好在,一个叫Ashwin Kumar的程序员创建了一个开源版本,让开发者/设计师的工作变得更简单。
以下内容翻译自他的博客:
理想上,这个模型可以根据网站设计的简单手绘原型,很快地生成一个可用的HTML网站:
△ SketchCode模型利用手绘线框图来生成HTML网站
事实上,上面例子就是利用训练好的模型在测试集上生成的一个实际网站,代码请访问:https://github.com/ashnkumar/sketch-code。
目前要解决的问题属于一种更广泛的任务,叫做程序综合(program synthesis),即自动生成工作源代码。尽管很多程序综合研究通过自然语言规范或执行追踪法来生成代码,但在当前任务中,我会充分利用源图像,即给出的手绘线框图来展开工作。
在机器学习中有一个十分热门的研究领域,称为图像标注(image caption),目的是构建一种把图像和文本连接在一起的模型,特别是用于生成源图像内容的描述。
△ 图像标注模型生成源图像的文本描述
我从一篇pix2code论文和另一个应用这种方法的相关项目中获得灵感,决定把我的任务按照图像标注方式来实现,把绘制的网站线框图作为输入图像,并将其相应的HTML代码作为其输出内容。
注:上段提到的两个参考项目分别是
pix2code论文:https://arxiv.org/abs/1705.07962
floydhub教程:https://blog.floydhub.com/turning-design-mockups-into-code-with-deep-learning/?source=techstories.org
确定图像标注方法后,理想中使用的训练数据集会包含成千上万对手绘线框图和对应的HTML输出代码。但是,目前还没有我想要的相关数据集,我只好为这个任务来创建数据集。
最开始,我尝试了pix2code论文给出的开源数据集,该数据集由1750张综合生成网站的截图及其相应源代码组成。
△ pix2code数据集中的生成网站图片和源代码
这是一个很好的数据集,有几个有趣的地方:
该数据集中的每个生成网站都包含几个简单的辅助程序元素,如按钮、文本框和DIV对象。尽管这意味着这个模型受限于将这些少数元素作为它的输出内容,但是这些元素可通过选择生成网络来修改和扩展。这种方法应该很容易地推广到更大的元素词汇表。
每个样本的源代码都是由领域专用语言(DSL)的令牌组成,这是该论文作者为该任务所创建的。每个令牌对应于HTML和CSS的一个片段,且加入编译器把DSL转换为运行的HTML代码。
为了修改我的任务数据集,我要让网站图像看起来像手工绘制出的。我尝试使用Python中的OpenCV库和PIL库等工具对每张图像进行修改,包括灰度转换和轮廓检测。
最终,我决定直接修改原始网站的CSS样式表,通过执行以下操作:
1. 更改页面上元素的边框半径来平滑按钮和DIV对象的边缘;
2. 模仿绘制的草图来调整边框的粗细,并添加阴影;
3. 将原有字体更改为类似手写的字体;
最终实现的流程中还增加了一个步骤,通过添加倾斜、移动和旋转来实现图像增强,来模拟实际绘制草图中的变化。
现在,我已经处理好数据集,接下来是构建模型。
我利用了图像标注中使用的模型架构,该架构由三个主要部分组成:
1. 一种使用卷积神经网络(CNN)的计算机视觉模型,从源图像提取图像特征;
2. 一种包含门控单元GRU的语言模型,对源代码令牌序列进行编码;
3. 一个解码器模型,也属于GRU单元,把前两个步骤的输出作为输入,并预测序列中的下一个令牌。
△ 以令牌序列为输入来训练模型
为了训练模型,我将源代码拆分为令牌序列。模型的输入为单个部分序列及它的源图像,其标签是文本中的下一个令牌。该模型使用交叉熵函数作为损失函数,将模型的下个预测令牌与实际的下个令牌进行比较。
在模型从头开始生成代码的过程中,该推理方式稍有不同。图像仍然通过CNN网络进行处理,但文本处理开始时仅采用一个启动序列。在每个步骤中,模型对序列中输出的下个预测令牌将会添加到当前输入序列,并作为新的输入序列送到模型中;重复此操作直到模型的预测令牌为,或该过程达到每个文本中令牌数目的预定义值。
当模型生成一组预测令牌后,编译器就会将DSL令牌转换为HTML代码,这些HTML代码可以在任何浏览器中运行。
我决定使用BLEU分数来评估模型。这是机器翻译任务中常用的一种度量标准,通过在给定相同输入的情况下,衡量机器生成的文本与人类可能产生内容的近似程度。
实际上,BLEU通过比较生成文本和参考文本的N元序列,以创建修改后的准确版本。它非常适用于这个项目,因为它会影响生成HTML代码中的实际元素,以及它们之间的相互关系。
最棒的是,我还可以通过检查生成的网站来比较当前的实际BLEU分数。
△ 观察BLEU分数
当BLEU分数为1.0时,则说明给定源图像后该模型能在正确位置设置合适的元素,而较低的BLEU分数这说明模型预测了错误元素或是把它们放在相对不合适的位置。我们最终模型在评估数据集上的BLEU分数为0.76。
后来,我还想到,由于该模型只生成当前页面的框架,即文本的令牌,因此我可以在编译过程中添加一个定制的CSS层,并立刻得到不同风格的生成网站。
△ 一个手绘图生成多种风格的网页
把风格定制和模型生成两个过程分开,在使用模型时带来了很多好处:
1.如果想要将SketchCode模型应用到自己公司的产品中,前端工程师可以直接使用该模型,只需更改一个CSS文件来匹配该公司的网页设计风格;
2. 该模型内置的可扩展性,即通过单一源图像,模型可以迅速编译出多种不同的预定义风格,因此用户可以设想出多种可能的网站风格,并在浏览器中浏览这些生成网页。
受到图像标注研究的启发,SketchCode模型能够在几秒钟内将手绘网站线框图转换为可用的HTML网站。
但是,该模型还存在一些问题,这也是我接下来可能的工作方向:
1. 由于这个模型只使用了16个元素进行训练,所以它不能预测这些数据以外的令牌。下一步方向可能是使用更多元素来生成更多的网站样本,包括网站图片,下拉菜单和窗体,可参考启动程序组件(https://getbootstrap.com/docs/4.0/components/buttons/)来获得思路;
2. 在实际网站构建中,存在很多变化。创建一个能更好反映这种变化的训练集,是提高生成效果的一种好方法,可以通过获取更多网站的HTML/CSS代码以及内容截图来提高;
3. 手绘图纸也存在很多CSS修改技巧无法捕捉到的变化。解决这个问题的一种好方法是使用生成对抗网络GAN来创建更逼真的绘制网站图像。
代码:https://github.com/ashnkumar/sketch-code
原文:https://blog.insightdatascience.com/automated-front-end-development-using-deep-learning-3169dd086e82
— 完 —
诚挚招聘
量子位正在招募编辑/记者,工作地点在北京中关村。期待有才气、有热情的同学加入我们!相关细节,请在量子位公众号(QbitAI)对话界面,回复“招聘”两个字。
量子位 QbitAI · 头条号签约作者
վ'ᴗ' ի 追踪AI技术和产品新动态
文共2416字,预计学习时长5分钟
图片来源:Unsplash/Luca Bravo
函数是编程中的关键部件之一。这些函数可执行特定任务,并可反复调用执行。将Javascript中的函数和其他编程语言中的函数相比较,其中最大的区别在于Javascript函数为一级对象,这意味着Javascript中的函数表现类似于对象,可作为变量、数组和其他对象。
本文就定义函数的三种不同方法进行讨论:
1. 函数声明
2. 函数表达式
3. 生成器函数
1. 函数声明
函数声明可能是最常见的方式了。观察如下语句:
function name (parameter) { statements }
函数声明语句
函数由关键字函数、函数的强制名称和括号内的参数组成(定义函数也可无参数)。最后,大括号内则是函数的主体,用以执行实际任务。
理解形参和实参的区别很重要。形参是定义函数时的一个变量。在调用函数时,实参则是输入函数形参的实际数据。
首先,看一个简单实例:
function hello(name) { console.log("Hello "+ name) } hello('stuti') // Hello stuti hello(12) // Hello 12
名为hello的函数以实际参数名称声明,在控制台记录消息。从本例中,可以看出,因为实际参数上无指定类型,所以其既适用于string函数,也适用于number函数。倘若只是希望该函数是一个名称而不是数值呢?很遗憾,由于Javascript中没有预建法来实现这一点,必须手动确定输入函数的实际参数类型,如下所示:
function hello(name) { if (typeof name === 'string') console.log("Hello " + name) else console.log("Please input a Name") } hello(12) // Plese input a Name
默认情况下,函数返回未定义变量。若要返回任何其他值,则函数必须具备返回语句,而由该语句指定返回值。
function something(){ } console.log(something()) // undefined function notSomething(){ return 1 } console.log(notSomething()) // 1
函数声明中提升是什么?
简单地说,这意味着在执行代码时,无论何处声明函数或变量,它们均会移动至作用域的顶部。这也就是所谓的提升。
观察下方实例:
myName() // My name is Stuti Chahuhan function myName() { console.log(`My name is Stuti Chauhan`) }
函数声明中的提升
此处甚至是在声明之前就调用了这个函数——提升。
2. 函数表达式
图片来源:unsplash.com/@creativegangsters
函数表达式与函数声明极为相似,连语句都不甚相同。最主要的区别在于函数表达式不以关键字函数开头,且函数名称可选,在此情况下,该函数表达式则可以成为一个匿名函数。如果函数有名称,则将其称为命名函数表达式。
let checkNumber = function check (num){ return (num %2==0)?"even" : "odd" } console.log(checkNumber(50)) // even
函数表达式的例子
现在来观察一下定义上述函数的匿名法。
//Anonymous Function let checkNumber = function (num){ return (num %2==0)?"even" : "odd" } console.log(checkNumber(50)) // even
匿名函数表达式
函数声明和函数表达式最关键的区别就在于函数表达式无提升功能。若在函数表达式中尝试与提升函数声明相同的项目,则会出现报错。
myName() // ReferenceError: myName is not defined let myName =function () { console.log(`My name is Stuti Chauhan`) }
函数表达式中无提升
IIFE(立即执行函数表达式)
这意味着函数一旦定义就开始运行,而函数表达式附在分组操作符()中,
( function () { let num = 4 return num }) //4
为什么典型的函数表达式需要IIFE?一般定义一个函数,随后多次调用。但倘若只想调用一次用来生成输出,并且不再使用该函数呢?由此就出现了IIFE。该函数会立即执行,并在以后不再经程序访问。因为不需再调用该函数,所以它也不需要名称,因此匿名函数表达式首选IIFE。
3. 生成器函数
图片来源:unsplash.com/@roman_lazygeek
普通函数遵循运行-完成的模型,不能在执行最后一行之前停止运行。若想在执行过程中退出,必须返回或抛出一个错误。
而生成器函数可以在执行过程中停止。当其调回时,仍然从停止的地方继续。
该函数与普通函数一样进行声明,但不同之处在于生成器函数在function关键字后面带有星号*,其间可包含任意数量的空格。
还有一点需要注意:在JavaScript中,生成器是一个函数,该函数返回一个对象,在该对象上可调用next(),每次调用next()都会返回一个结构如下的对象:
{ value: Any, done: true|false }
此对象包含两个属性:value和done。Value是对象的实际值,而done规定了函数终止的属性——默认值为false,当其变为true时,则函数停止。
通过一个简单的例子来理解这点:
function * generatorFunction() { console.log('first to execute'); yield 'takes a pause'; console.log(' printed after the pause'); yield 'end of the function'; } const gen = generatorFunction(); console.log(gen.next().value); // first to execute // takes a pause console.log(gen.next().value); // printed after the pause // end of the function console.log(gen.next().value); // undefined
函数本体中,不使用return关键字——而是使用yeild,如果使用了return,则会将done属性更改为true,函数将结束——之后的任何操作都不会执行。Yield给出为其分配的值。
首先,在调用函数前先定义函数,同时调用一个生成器函数,形成一个生成器对象,存储在变量中。
然后在使用next()和value属性时调用对象。第一次使用next()时,函数执行开始。首先,运行console.log (“首次执行”)并将其记录在控制台中,然后遇到yield—输出 “暂停”值,执行停止。
第二次调用next()时,它从上次暂停处继续。同样,首先运行console.log(),再次遇到yield,生成“函数结束”值,函数停止。
第三次调用next()时,结果是未定义。这是因为由生成器函数生成的对象只能迭代一次——该对象现在是无用的,必须生成一个新对象才能重新启动程序。
function* generator() { yield "some value"; return "ends here"; yield "will never execute"; yield "never reach here" } var gen = generator() console.log(gen.next()); // { value: "some value", done: false } console.log(gen.next()); // { value: "ends here", done: true } console.log(gen.next()); // { value: undefined, done: true } console.log(gen.next()); // { value: undefined, done: true }
如果使用return而不是yield,那么数据的值就会被更改为true,之后就不会执行任何操作。与Promise结合使用时,就异步编程而言,生成器是一个非常强大的工具。该函数即使不能完全消除回调的问题,也可以减轻其问题的程度。
结论:哪种方法更好?
事实上,我们不能将其进行比较,然后说一个比另一个好——声明的类型还是取决于具体情况或条件。
为实现更好的异步功能则使用生成器函数,针对立即执行函数的情况,则使用匿名函数表达式。
留言 点赞 关注
我们一起分享AI学习与发展的干货
编译组:柳玥、温媛
相关链接:
https://medium.com/better-programming/three-ways-to-define-functions-in-javascript-750a908e51d9
如需转载,请后台留言,遵守转载规范
JavaScript 函数有 4 种调用方式。
每种方式的不同方式在于 this 的初始化。
this 关键字
一般而言,在Javascript中,this指向函数执行时的当前对象。
注意 this 是保留关键字,你不能修改 this 的值。 |
调用 JavaScript 函数
我们知道如何创建函数。
函数中的代码在函数被调用后执行。
作为一个函数调用
实例
function myFunction(a, b) {
return a * b;
}
myFunction(10, 2); // myFunction(10, 2) 返回 20
以上函数不属于任何对象。但是在 JavaScript 中它始终是默认的全局对象。
在 HTML 中默认的全局对象是 HTML 页面本身,所以函数是属于 HTML 页面。
在浏览器中的页面对象是浏览器窗口(window 对象)。以上函数会自动变为 window 对象的函数。
myFunction() 和 window.myFunction() 是一样的:
实例
function myFunction(a, b) {
return a * b;
}
window.myFunction(10, 2); // window.myFunction(10, 2) 返回 20
这是调用 JavaScript 函数常用的方法, 但不是良好的编程习惯 全局变量,方法或函数容易造成命名冲突的bug。 |
全局对象
当函数没有被自身的对象调用时, this 的值就会变成全局对象。
在 web 浏览器中全局对象是浏览器窗口(window 对象)。
该实例返回 this 的值是 window 对象:
实例
function myFunction() {
return this;
}
myFunction(); // 返回 window 对象
函数作为全局对象调用,会使 this 的值成为全局对象。使用 window 对象作为一个变量容易造成程序崩溃。 |
函数作为方法调用
在 JavaScript 中你可以将函数定义为对象的方法。
以下实例创建了一个对象 (myObject), 对象有两个属性 (firstName 和 lastName), 及一个方法 (fullName):
实例
var myObject = {
firstName:"John",
lastName: "Doe",
fullName: function () {
return this.firstName + " " + this.lastName;
}
}
myObject.fullName(); // 返回 "John Doe"
fullName 方法是一个函数。函数属于对象。 myObject 是函数的所有者。
this对象,拥有 JavaScript 代码。实例中 this 的值为 myObject 对象。
测试以下!修改 fullName 方法并返回 this 值:
实例
var myObject = {
firstName:"John",
lastName: "Doe",
fullName: function () {
return this;
}
}
myObject.fullName(); // 返回 [object Object] (所有者对象)
函数作为对象方法调用,会使得 this 的值成为对象本身。 |
使用构造函数调用函数
如果函数调用前使用了 new 关键字, 则是调用了构造函数。
这看起来就像创建了新的函数,但实际上 JavaScript 函数是重新创建的对象:
实例
// 构造函数:
function myFunction(arg1, arg2) {
this.firstName = arg1;
this.lastName = arg2;
}
// Thiscreates a new object
var x = new myFunction("John","Doe");
x.firstName; // 返回 "John"
构造函数的调用会创建一个新的对象。新对象会继承构造函数的属性和方法。
构造函数中 this 关键字没有任何的值。this 的值在函数调用时实例化对象(new object)时创建。 |
作为函数方法调用函数
在 JavaScript 中, 函数是对象。JavaScript 函数有它的属性和方法。
call() 和 apply() 是预定义的函数方法。 两个方法可用于调用函数,两个方法的第一个参数必须是对象本身。
实例
function myFunction(a, b) {
return a * b;
}
myFunction.call(myObject, 10, 2); // 返回 20
实例
function myFunction(a, b) {
return a * b;
}
myArray = [10,2];
myFunction.apply(myObject, myArray); // 返回 20
两个方法都使用了对象本身作为第一个参数。 两者的区别在于第二个参数: apply传入的是一个参数数组,也就是将多个参数组合成为一个数组传入,而call则作为call的参数传入(从第二个参数开始)。
在 JavaScript 严格模式(strict mode)下, 在调用函数时第一个参数会成为 this 的值, 即使该参数不是一个对象。
在 JavaScript 非严格模式(non-strict mode)下, 如果第一个参数的值是 null 或 undefined, 它将使用全局对象替代。
通过 call() 或 apply() 方法你可以设置 this 的值, 且作为已存在对象的新方法调用。 |
如您还有不明白的可以在下面与我留言或是与我探讨QQ群308855039,我们一起飞!
*请认真填写需求信息,我们会在24小时内与您取得联系。