作者:vivo 互联网前端团队- Su Ning
在设计稿转网页中运用基于self-attention机制设计的机器学习模型进行设计稿的布局,能够结合dom节点的上下文得出合理的方案。
切图作为前端的传统手艺却是大多数前端开发者都不愿面对的工作。
为了解决切图的各种问题,人们绞尽脑汁开发了各种各样的设计稿转代码(D2C)工具,这些D2C工具随着设计师使用的软件的变更又在不断地迭代。
从Photoshop时代,前端需要手动标记节点进行单独的样式导出(如图1),到sketch measure,可以整体页面输出(如图2),其效率、结果都已经有了一个质的提升。
但是还是未能彻底解决切图的问题,因为设计稿所包含的信息只负责输出样式,而没有办法输出网页布局,我们还是没有办法直接copy生成的代码到我们的项目中直接使用。

图1

图2
在学习现有的D2C案例的过程中,我们发现很多成熟的方案中引用了机器学习辅助代码的生成,其中绝大多数的工作是用于视觉识别和语义识别,于是我们想,机器学习是否能够应用到网页的布局中呢?
为了验证我们的猜想,也为了解决我们工作中的实际痛点,我们决定自己开发一个D2C工具,希望能够通过机器帮我们实现网页布局的过程,整体工作流程大致如图3,首先获取设计稿的元数据,然后对设计稿的数据进行一系列的处理导出自由的dsl,然后根据这个dsl生成相应端的代码。

图3
要处理网页的布局需要解决两个问题,节点的父子关系以及节点之间的位置关系。
节点的父子关系指的是一个节点包含了哪些子节点,又被哪个节点所包含。这部分的内容可以直接使用规则处理,通过节点的顶点位置和中心点的位置信息判断一个节点是否是另一个节点的子节点。这里我们就不展开讨论了。

同一层级节点之间的位置关系,才是我们这次着重需要解决的问题。
网页的布局有很多种,线性布局,流式布局,网格布局,还有随意定位的绝对定位等等,而我们在导出样式的时候,无非需要确认两件事情,节点的定位方式(relative、absolute、fixed)和节点的布局方向(纵向、横向)。

线性布局

流式布局

网格布局
按照*时切图的习惯,我们会首先识别一组*级节点之间有没有明显的上下或者左右的位置关系,然后将他们放入到网格中,最后独立在这些节点外面的节点就是绝对定位。
让机器识别节点之间的位置关系,就成了解决问题的关键一环。

判断节点之间的位置关系只需要节点的位置和宽高信息,因此我们的输入数据设计如下:
[
{
width:200,
height:50,
x:0,
y:0
},
{
width:200,
height:100,
x:0,
y:60
},
{
width:200,
height:100,
x:210,
y:60
}
]
同时我们希望获得的输出结果是每一个节点是上下排列,左右排列,还是绝对定位的,输出的数据设计如下:
[
{
layout:'col'
},
{
layout:'row'
},
{
layout:'absolute'
}
]
起初我们是希望通过书写一定的规则进行布局的判断,通过判断前后两个节点的位置关系是上下还是左右来进行布局,然而这样只关注两个节点位置关系的规则却很难判断绝对定位的节点,并且固定的规则总是不够灵活。于是我们想到了善于处理分类问题的机器学习。
很显然通过大量数据训练的机器学习模型可以很好的模仿我们*时切图的习惯,在处理各种边缘场景的时候也能够更加的灵活。只要进行合理的模型设计,就可以辅助我们进行布局的处理。
从上文我们可以看出,我们需要训练一个模型,输入一个节点的列表,输出一个节点的布局信息,是不是有点像文本识别里面的词性翻译呢?

对于具体的一个节点,我们是没有办法判断其真正的布局的,只有将其放到文档流中结合上下文来看才能体现出其实际的意义。
在处理词性标记这块,RNN(循环神经网络)、LSTM(长短期记忆网络)都比较合适此类场景,RNN作为经典的神经网络模型通过将前一次训练的权重带入到下一次训练中建立上下文的关联,LSTM作为RNN的一种变体解决了RNN难以解决的长期依赖问题,用来训练网页布局看起来是一个不错的选择。

RNN(循环神经网络)

665px LSTM(长短期记忆网络)
使用LSTM的确可以解决我们的问题,但是由于此类神经网络对于时序的依赖导致上下文的数据没有办法进行并行运算,这使得我们的计算机没有办法更高效的训练模型,并且网页布局只需要获取不同节点的定位信息,对于装载的顺序并没有强要求。
随着2017年的一篇文章《Attention is All you Need》的横空出世,整个机器学习领域迎来了新一轮的革命,目前最主流的框架transformer、BERT、GPT全是在attention的基础上发展起来的。
self-attention自注意力机制是attention机制的变体,通过全局关联权重得出单个向量在全局中的加权信息,因为每一个节点都采用相同的运算方式,所以同一个序列中的节点可以同时进行上下文计算,极大地提升了模型训练的效率,也更方便我们优化和回归模型。

self-attention
综上,考虑到attention对于上下文更好的关联和更好的并行性能,我们决定基于此进行建模。
我们起初设计了一个输出的向量用于识别数据处理的结果,[1,0,0,0]代表是正常的竖向排列,[0,1,0,0]代表了横向排列,以此类推依次代表了absolute和fixed定位方式,但是后来我们发现fixed由于是相对于整个页面的定位方式,所以在单一层级下很难做标记,所以我们将输出值精简成了竖向排列,横向排列和绝对定位三种情况。

模型的整体设计如下,通过self-attention将节点转化为上下文信息,再通过前馈神经网络将上下文信息训练成具体的布局。

(1)在获取到一组数据后,为了去掉数值的大小的影响,我们首先对数据进行一次归一处理,将输入数据的每一个值分别除以这一组数据中的最大值。
(2)将每一组数据分别进行三次线性变换, 得到每组数据相应的Q,K,V,接下来我们就可以进行self-attention运算了。
(3)我们以node1为例,如果需要计算node1和其他节点的相关性,则需要使用Q1分别和每一个节点的key进行点积操作,将他们的和与V1相乘,为了防止权重值相乘以后过大,最后进行一次softmax计算,就得到了node1和其他节点的上下文信息。

我们将一组数据中所有QKV看成三个矩阵,得到的上下文context的集合就可以看成一个矩阵计算。

(4)为了提升训练的效果,每个节点的上下文信息输入到前馈神经网络进行最后的布局结果的训练,将得到的结果进行softmax计算就可以得到单个节点在一组数据中布局的概率分布了,由于同一组节点的运算没有前后顺序,故单个节点的运算可以同时进行,配合GPU加速极大地提升了训练的效率。
由于机器学习需要海量的数据,数据的数量和质量都会极大地影响模型最终的训练效果,所以数据的数量和质量都非常重要,我们采用了三种数据源用于数据的训练,分别是标记过的设计稿,抓取的真实网页数据,以及自动生成的数据。
在获取到设计稿数据以后只取每个节点的定位和宽高数据,通过上文的父子关系处理后获取每一层的节点数据,为了防止出现过拟合的情况我们去掉节点数量相对较少的层级,并对每一层的布局进行手动标记。设计稿标记数据是质量最好,但也是最费时费力的工作,所以需要其他的数据源进行数量的补充。
作为标记设计稿的补充,网页中的真实数据也是可靠的数据源,但是抓取网页的过程中最大的难点在于判断页面中的节点属于横向还是纵向。
由于实现横向排列的方式千奇百怪,可以通过float,inline-block,flex等等方法,我们如果只获取网页中节点的定位和宽高信息,还是需要手动标记他的布局,所以还是要从节点的css入手,在批量获取之后进行手动筛选,去掉低质量的数据。相对于标记的设计稿是一个有效的补充。
为了更快的生成大量的数据,我们写了一个网页生成的算法,在一开始就决定节点的定位方式,然后将节点渲染成网页,最后在抓取节点的定位信息,但是随机生成的数据存在一些不稳定的边界场景,譬如生成的绝对定位的节点会正好定位到横向布局的右边。这时就需要手动进行甄别。

我们最终收集了大约两万多条数据,经过反复的训练与调试,最终准确率稳定在99.4%左右,达到一个相对可用的水*。

使用真实的dom进行回归验证,可以看出准确的识别出了网页的横向竖向布局以及绝对定位的节点。

设计稿中会出现列表一行放不下然后换行的情况,这些节点应该是属于横向的位置关系,但是机器面对两行会将每一行的首个元素识别成纵向排布,这就需要对重复节点进行相似度检测,对相似节点采用相同的布局策略。
基于规则的分组会导致两个不相交的节点不会分配到一个组里面,比如网格中的图标和文字,导致布局的时候会分成两个独立的组,可以通过训练常见的布局结合内容识别进行更精准的分组。
通过self-attention机制我们不仅可以判断单个节点在其层级的布局信息,我们也可以进行发散,通过训练整个层级的节点数据得出当前节点属于卡片流,标签,个人信息页等功能性的标记,进一步推导出每个节点的功能,再结合节点具体的布局信息,不仅可以更好的实现网页的排布,还可以节点的功能推到实现标签的语义化。
为了解决更多的网页布局问题,同时减少我们人工标记的工作量,可以运用强化学习模型开发一套网页生成工具,让我们的数据更接*真实的网页布局,从而使布局模型训练的结果更加贴*生产的场景。
机器学习非常擅长处理分类问题,相比于传统的手写规则布局,机器学习是基于我们现有的开发习惯进行训练,最终生成的代码也更加贴*我们*时的切图习惯,代码的可读性和可维护性都更上一层楼。
而机器生成的静态页面相较于不同的人手写的静态页面,遵循一致的代码规范,代码风格也更加统一。
在模型搭建的过程中可以将具体的使用场景类比为文本或者图像领域的内容,便于寻找现有的模型进行迁移学习。
运用机器学习解决网页布局问题的核心在于建立节点的上下文的关联,通过了解各种经典的神经网络模型的运行原理我们选出了循环神经网络和自注意力机制这种能够建立上下文关联的模型,而通过对于其运行原理的进一步了解我们选择了更贴*网页布局场景以及运行效率更高的self-attention模型,由此也可以看出了解模型的运行机制可以更好的帮助我们解决实际的应用场景。
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时
作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代
Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer
我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此
我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r