原文:https://www.freecodecamp.org/news/how-you-can-train-an-ai-to-convert-your-design-mockups-into-html-and-css-cc7afd82fed4/
译者:zhicheng
校对者:shendengnian
题图:由 Unsplash 的 Wesson Wang 提供
提示:文中的蓝色字体大家可以点击文末“阅读原文”在 freeCodeCamp 中文论坛访问链接
在未来几年,深度学习会改变前端开发。它会提高基于原型的开发速度,降低开发软件的成本。
随着 Tony Beltramelli 发起的 pix2code 和 Airbnb 发起的 sketch2code 项目,这一领域正在飞速发展。
目前,前端开发智能化的最大阻碍是计算能力。不管怎样,我们可以使用当下流行的深度学习算法,以及现成的训练数据,来探索一下机器开发前端。
[h3]1)把设计图片输入给训练过的神经网络[/h3]![]()
[h3]2)神经网络把图片转换成 HTML 代码[/h3]
![]()
[h3]3)渲染输出[/h3]![]()
我们将会分三步构建神经网络。
首先,我们将会实现一个基础功能版本熟悉一下流程。第二个是 HTML 版本,会将所有的操作自动化,这里会介绍神经网络层。在最终的 Booststrap 版本里,我们将创建一个通用的 LSTM 层模型。
代码托管在 GitHub 和 FloyHub Jupyter 的 notebooks 上。所有的 FloydHub notebooks 在目录下,本地文件在目录下。
模型基于 Beltramelli 的 pix2code paper 以及 Jason Brownlee 的图片识别教程。代码是 Python 和 Keras 写的,Keras 是一个基于 TensorFlow 的框架。
如果你是深度学习的新手,建议你从 Python 、backpropagation 和卷积神经网络开始入门。我在 FloyHub 的三篇早期博客(英文)可以作为参考:
- 我学习深度学习的第一个周末
- 深度学习的代码记录
- 用神经网络给 B&W 照片上色
[h2]核心概念[/h2]让我们来看一下我们的目标。我们想要构建一个神经网络,它可以根据网页截图生成相应的 HTML/CSS 代码。
通过和截图匹配的 HTML 代码来训练神经网络。
机器通过把 HTML 代码标签和相应的图片对比来学习。它分析截图以找到合适的 HTML 标签。
这里是 Google Sheet 上的一组简单的训练数据例子。
创建逐字预测的模型是普遍做法。当然还有其它的方法,为了便于理解,我们在教程里采用普遍做法。
注意每次预测都是读取同一个图片。所以如果预测 20 个单词,会读取 20 次原型图。目前为止,不用太纠结神经网络是如何工作的,了解神经网络的输入和输出就好了。
![]()
先从上一个标签开始处理。训练机器来识别句子 “I can code” 。当输入 “I”,它会预测出 “can”。下一次输入变成 “I can” 然后预测出 “code”。输入一个单词就能预测出下一个单词。
![]()
神经网络从数据里寻找特征,然后通过特征建立输入和输出的联系。它会创建画像来了解每一个截图是什么,然后预测 HTML 语句。从而构建出知识体系来预测标签。
把训练好的模型应用于真实世界,和模型训练的原理是相同的。文字也是根据截图逐个生成。区别是它根据已经学习过的标签来预测代码,而不是给定相应的 HTML 代码来学习。接着,预测下一个代码标签。预测从 “开始标签” 预测,在 “结束标签” 或者达到最大限制截止。这里是在 Google Sheet 上的另一个例子。
![]()
[h2]“hello world” 版本[/h2]让我们先来构建一个 “hello world” 版本。先让神经网络学习一个显示 “Hello World” 的网站截图,教他生成相应标签。
![]()
首先,神经网络把设计原型图转换为像素值列表。值范围是 0 - 255,包含红蓝绿三个通道。
![]()
这里使用了 one hot encoding 把标签转换成神经网络可以理解的方式。例如,句子 “I can code” 可以转换成下面的样子。
![]()
在上面的图片里,还包含了开始和结束标记。这些标记代表了机器什么时候开始识别什么时候终止识别。
我们使用句子作为数据输入,从第一个单词开始,接着一个单词一个单词的添加。输出数据也是单词。
句子的逻辑和单词是一样的,也同样需要输入长度。句子受最大句子长度约束,而不是受最低词汇量限制。如果句子长度比最大长度短,会使用 0 字符填充。
![]()
如你所见,单词从右到左打印。每轮训练单词的位置都不固定。这样模型就可以学习句子而不是简单记住每个单词的位置。
在下面的图片里有四个预测。每一行是一个预测。左边的图片表示三个颜色通道: 红、绿、蓝,以及当前的单词。括号外面代表每次的预测,红色的方块代表结束。
![]()
green blocks = start tokens | red block = end token
- #Length of longest sentencemax_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 modelimages = preprocess_input(images)#Turn start tokens into one-hot encodinghtml_input = np.array( [[[0., 0., 0.], #start [0., 0., 0.], [1., 0., 0.]], [[0., 0., 0.], #start Hello World! [1., 0., 0.], [0., 1., 0.]]])#Turn next word into one-hot encodingnext_words = np.array( [[0., 1., 0.], # Hello World! [0., 0., 1.]]) # end# Load the VGG16 model trained on imagenet and output the classification featureVGG = VGG16(weights='imagenet', include_top=True)# Extract the features from the imagefeatures = VGG.predict(images)#Load the feature to the network, apply a dense layer, and repeat the vectorvgg_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 inputdecoder = concatenate([vgg_feature_repeat, language_model])# Extract information from the concatenated outputdecoder = LSTM(5, return_sequences=False)(decoder)# Predict which word comes nextdecoder_output = Dense(vocab_size, activation='softmax')(decoder)# Compile and run the neural networkmodel = Model(inputs=[vgg_feature, language_input], outputs=decoder_output)model.compile(loss='categorical_crossentropy', optimizer='rmsprop')# Train the neural networkmodel.fit([features, html_input], next_words, batch_size=2, shuffle=False, epochs=1000)
复制代码
在 hello world 版本里,用到了 3 个 tokens:、和。token 可以是任何东西。可以是字符、单词或者句子。字符版本需要较少的词汇量,但是受限于神经网络。单词版本则性更优。
下面是做出的预测:
- # Create an empty sentence and insert the start tokensentence = np.zeros((1, 3, 3)) # [[0,0,0], [0,0,0], [0,0,0]]start_token = [1., 0., 0.] # startsentence[0][2] = start_token # place start in empty sentence# Making the first prediction with the start tokensecond_word = model.predict([np.array([features[1]]), sentence])# Put the second word in the sentence and make the final predictionsentence[0][1] = start_tokensentence[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_tokensentence[0][1] = np.round(second_word)sentence[0][2] = np.round(third_word)# Transform our one-hot predictions into the final tokensvocabulary = ["start", "Hello World!", "end"]for i in sentence[0]:print(vocabulary[np.argmax(i)], end=' ')
复制代码 输出
- 10 epochs:
- 100 epochs:
- start Hello World! Hello World!
复制代码 - 300 epochs:
[h2]我犯的错误[/h2]- 构建前先收集数据。 在这个项目初期,我设法获取了 Geocities 托管网站的一个旧的副本。上面有三千八百万个网站。由于过于盲目,我忽略了分析 10 万词汇量所需的巨大工作。
- 处理 TB 级别的数据需要一个好的设备以及很大的耐心。 在 mac 运行力不从心后,我开始使用更强大的远程服务器。租用的 8 核 CPU 1G 带宽让我有了一个像样的调试环境。
- 在我熟悉数据输入输出之前一切都没用。 输入 X,是一个截图以及前一个标签代码。输出,Y,是下一个标签代码。当我理清这些时,事情变得简单多了。切换不同的架构也变得很方便。
- 当心未知世界。 由于这个项目涉及到深度学习的很多领域,在学习过程中我在好多不必要的地方浪费了大量时间。我花了一周时间从零开始学习 RNNs,研究了一段时间嵌入向量空间,又了解了各种非主流实现。
- 机器图片转代码是一种图片识别模型。 当我了解到这一点时,我仍然忽略了很多图片识别资料,因为它们太枯燥了。一旦我意识到这一点,我的进步飞快。
[h2]在 FloydHub 上运行代码[/h2]FloydHub 是一个深度学习训练平台。我第一次接触深度学习的时候就开始用它,现在我用它来处理机器学习并记录我的学习过程。你可以点击下面的按钮在 30s 内快速上手:
![]()
这个链接在 FloydHub 上打开了一个工作空间,里面包含了 Bootstrap 版本所使用的环境和数据集。以及测试用的训练好的模型。
或者你可以通过以下这两步手动安装: 2 分钟安装、5 分钟上手教程。
克隆仓库- git clone https://github.com/emilwallner/Screenshot-to-code-in-Keras.git
复制代码 登录并初始化 FloydHub 命令行工具- cd Screenshot-to-code-in-Kerasfloyd loginfloyd init s2c
复制代码 在 FloydHub 云 GPU 机器上运行 Jupyter 笔记本- floyd run --gpu --env tensorflow-1.4 --data emilwallner/datasets/imagetocode/2:data --mode jupyter
复制代码 所有的 notebooks 都在 FloydHub 路径下。local 代表本地。一旦运行,你可以在这里找到第一个 notebook:floydub/Helloworld/helloworld.ipynd。
如果你想要更详细的介绍,这里有一篇我早期的文章。
[h2]HTML 版本[/h2]在这个版本里,我们在 Hello World 模型基础上做了很多自动化工作。这节将会专注于用神经网络创建一个可扩展、可移植的实现。
这个版本还不能从一个随机的截图来生成 HTML,但却是对动态解决问题的更深探索。
![]()
概述
如果我们展开前一个图片的组件,看起来如下:
![]()
有两个主要的部分。首先,encoder。这是我们处理图片特征(Image features)和前一个标签特征(Previous markup features)的地方。特征就是机器创建的把设计原型图和代码联系起来的语句块。在 encoder 的末尾,我们把图片特征和前一个标签的每个单词连在一起。
decoder 接收设计和标签特征合体然后创建了下一个标签特性。这个特性通过完全连接的神经网络运行,以预测下一个标签。
[h3]原型图特性[/h3]由于我们需要给每一个单词插入一个截图,这在我们训练机器的时候成为了瓶颈 (例子)。所以我们直接提取代码所需要的信息,而不是使用图片。
信息编码成图片特性。这里用到了一个预训练的卷积神经网络 (CNN)。模型已经在 Imagenet 上面训练过了。
我们在最终分类前在图层上提取了特征。
![]()
我们最终获得了 1536 个 8*8 像素的图片做为已知特征。虽然它们对于人脑来说很难理解,神经网络却可以从这些特征里提取所需的对象以及元素的位置信息。
[h3]Markup 特性[/h3]在 hello world 版本里,我们使用 one-hot encoding 来描述代码。在这个版本里,我们使用嵌入单词做为输入,使用 one-hot encoding 做为输出。
我们划分每个句子的方式还是相同,但是 token 的映射改变了。One-hot encoding 把每个单词当成了一个独立的部分。我们把输入数据中的每个单词转换成数字列表。列表代表了代码标签间的关系。
![]()
嵌入单词的参数是 8 ,但是经常在 50-500 之间浮动,和词汇的大小有关。
单词的 8 类似于 BP 神经网络里的权重。它们需要动态调整,代表了单词之前的相关性 (Mikolov et al., 2013)。
这就是开发代码特性的过程。特性通过神经网络把输入数据和输出数据连接起来。目前为止,不用在意它们是什么,我们将要在下一节里讨论它们。
[h2]编码[/h2]我们输入嵌入单词然后运行在 LSTM 上并返回一系列代码特性。它们运行在 Time 分发密集层—把它想成一个有多个输入和输出的密集层。
![]()
平行的,图片特性在第一层。忽略数字结构,它们把图片转换成了一个大的数字列表。然后我们在这层上又应用了一个密集层组成了一个高阶特性,把这些图像特性和标签特性组合起来。
这可能很难理解–让我们分解它。
[h3]代码特性[/h3]这里我们通过 LSTM 层运行嵌入单词。在这个图片里,所有的句子都被填充以达到三个 token 的最大大小。
![]()
为了混合信号以及找到更高级的模式,我们给代码特性应用了一个 TimeDistributed 密集层。TimeDistributed 密集和普通密集层相同,只不过有多个输入和输出。
[h3]图片特性[/h3]平行的,我们准备了图片。我们获取到所有的小图片特征然后把它们转换成一个长列表。信息没有改变,只是被重新整理。
![]()
再次,为了混合信号以及抽出更高的概念,我们应用了一个密集层。由于我们只处理了一路输入值,所以我们可以使用一个普通的密集层。我们复制了图片特性,以便把图片特性和标记特性组合起来。
[h3]将图片和标签特性联系起来[/h3]所有的句子都被扩充以创造三个代码特性。由于已经准备好了图片特征,现在能给每个代码特性添加一个图片特征。
![]()
在把一个图片特征粘贴到每个代码标签后,我们最终得到三个图片代码特性。这就是我们提供给解码器的输入。
[h3]解码[/h3]在这里我们使用组合图片代码特性来预测下一个标签。
![]()
在下面的例子里,我们使用三个图片代码特性配对,然后输出给下一个标签特性。
请注意 LSTM 图层序列设置为 false。它只预测一个特性,而不是返回输入序列的长度。在我们的用例里,是下一个标签的特性。它包含了最终预测的信息。
![]()
[h3]最终预测[/h3]密集层的原理有点像前馈神经网络。它在含有 4 个最终预测的下一个标签里连接了 512 个数字。在我们的词汇里有四个单词:start、hello、world 和 end。
预测的词汇应该是 [0.1, 0.1, 0.1, 0.7]。在密集层的 softmax 激活分配了值为从 0-1 之间的概率,预测值的总和是 1。在这里,它在下一个标签里预测了第四个单词。然后把 one-hot encoding [0, 0, 0, 1] 翻译成映射的值,也就是 “end”。
- # Load the images and preprocess them for inception-resnetimages = []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 layerIR2 = InceptionResNetV2(weights='imagenet', include_top=False)features = IR2.predict(images)# We will cap each input sequence to 100 tokensmax_caption_len = 100# Initialize the function that will create our vocabulary tokenizer = Tokenizer(filters='', split=" ", lower=False)# Read a document and return a stringdef load_doc(filename): file = open(filename, 'r') text = file.read() file.close() return text# Load all the HTML filesX = []all_filenames = listdir('html/')all_filenames.sort()for filename in all_filenames: X.append(load_doc('html/'+filename))# Create the vocabulary from the html filestokenizer.fit_on_texts(X)# Add +1 to leave space for empty wordsvocab_size = len(tokenizer.word_index) + 1# Translate each word in text file to the matching vocabulary indexsequences = tokenizer.texts_to_sequences(X)# The longest HTML filemax_length = max(len(s) for s in sequences)# Intialize our final input to the modelX, 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 encoderimage_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 decoderdecoder = concatenate([ir2_out, language_model])decoder = LSTM(512, return_sequences=False)(decoder)decoder_output = Dense(vocab_size, activation='softmax')(decoder)# Compile the modelmodel = Model(inputs=[image_features, language_input], outputs=decoder_output)model.compile(loss='categorical_crossentropy', optimizer='rmsprop')# Train the neural networkmodel.fit([image_data, X], y, batch_size=64, shuffle=False, epochs=2)# map an integer to a worddef 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 imagedef 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 HTMLtest_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)
复制代码 [h2]输出[/h2]![]()
[h3]生成好的页面[/h3]- 250 epochs
- 350 epochs
- 450 epochs
- 550 epochs
如果你点击上面的链接后看不到内容,那么需要右击选择 “显示网页源代码”。这是对应的原始网站。
[h2]我犯的错误[/h2]- 在我的认知里 LSTMs 比 CNNs 更重。 当我展开所有的 LSTMs,它们变得更容易理解。Fast.ai’s video on RNNs 里写的很棒。另外,不要尝试去了解它们怎样工作,专注于输入和输出功能就好了。
- 着手构建一个词汇表比弄一个巨大的词汇库简单多了。 这也适用于字体、div 尺寸、hex colors 、变量名和普通的单词。
- 大部分库都是用来解析文本文档而非代码的。 在文档里,一切东西都以空格分隔,但是在代码里,你需要定制解析。
- 可以使用在 Imagenet 训练好的模型来提取特性。 由于 Imagenet 里只有少量的 web 图片所以这可能有悖于常理。虽然和由 scratch 训练的 pix2code 模型相比错误率高达 30% ,但使用基于 web 截图的由 inception-resnet 训练的模型是一件很有趣的事。
[h2]Bootstrap 版本[/h2]在最终版本里,使用 pix2code paper 上面生成好的 bootstrap 网站数据集。通过引入 Twitter 的 bootstrap, 可以合并 HTML 和 CSS,减少词汇的体积。
我们将要为之前没有见过的截图生成标记。我们还会深入关于怎样将截图转换成代码的知识。
我们将使用 17 个简化的 tokens 来转换成 HTML 和 CSS,而不是在 bootstrap 标签上训练。数据集包含了1500 个测试截图和 250 个符合要求的图片。每个截图平均有 65 个 tokens,共有 96925 个训练例子。
通过在 pix2code paper 里定制模型,可以预测 97% 的 web 组件 (使用 BLEU 4-ngram 贪婪搜索模式后,还会更高)。
![]()
[h3]端到端接近[/h3]在图片识别模型抽出的预训练模型效果很不错。但是经过一些尝试后,我意识到 pix2code 的端到端方式在这个问题能上表现的更出色。训练好的模型还没有在 web 数据上训练,它们需要手动来分类。
在这个模型里,我们把预训练图片特性替换成了一个轻量的卷积神经网络。我们使用了 strides 而不是 max-pooling 来提高信息密度。这样就可以获得前端元素的位置和颜色。
![]()
有两个核心模型可以做到这点:卷积神经网络 (CNN)和递归神经网络 (RNN)。最常见的递归神经网络就是 long-short term memory (LSTM),这也是我更倾向的模型。
有很多的出色的 CNN 教程,我在之前的文章里也有提到过。在这里,我将会专注于 LSTMs。
[h3]了解 LSTMs 里面的 timesteps[/h3]LSTMs 里面最难理解的就是 timesteps。BP 神经网络可以看成是两个 timesteps。 如果给出 "Hello, " 它会预测出 “World.”。但是它很难预测更多的 timesteps。在下面的例子里,输入有 4 个timesteps,每一个对应一个单词。
LSTMs 由包含 timesteps 的输入组成。它是为有序信息定制的神经网络。如果展开模型看起来像下面这样。对于每个向下的 step,具有同样的权重。用一组权重来做为前一个输出,另一组做为新的输入。
![]()
加权的输入和输出被合并添加到 activation 里。这是这个 timestep 的输出。由于我们复用了权重,它们从多个输入里重组了信息构建了新的知识系列。
下面是在 LSTM 里处理每个 timestep 的过程的简单描述:
![]()
如果想更好的理解这一逻辑,我建议通过 Andrew Trask 的教程来亲自用 scratch 构建一个 RNN。
[h3]了解 LSTM 层里面的单元[/h3]每个 LSTM 层里面的单元数量决定了存储的能力。这也反应了每个输出特性的大小。再次,特性是一个用来在不同层之间传输信息的长列表的数字。
LSTM 层里的单元跟踪学习不同的语句。下面是一个单元跟踪 row div 信息的可视化表述。这是一个简化过了的训练 bootstrap 模型的代码。
![]()
每个 LSTM 单元维护了一个 cell 的状态。可以把 cell 状态理解为内存。权重和 activations 用不同的方式修改状态。这可以让 LSTM 层在信息保持和丢弃的时候更好的契合。
除了给每个输入传入一个输出特性,还可以转发 cell 状态,cell 状态就是 LSTM 里每个单元的一个值。如果需要了解更多 LSTM 组件的交互,建议参考 Golah 的教程,Jayasiri 的 Nummpy 实现,以及 [Karphay 的讲座](Karphay’s lecture)和资料。
- dir_name = 'resources/eval_light/'# Read a file and return a stringdef load_doc(filename): file = open(filename, 'r') text = file.read() file.close() return textdef 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 = ' ' + load_doc(data_dir+filename) + ' ' # 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, texttrain_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 indexestrain_sequences = tokenizer.texts_to_sequences(texts)# The longest set of boostrap tokensmax_sequence = max(len(s) for s in train_sequences)# Specify how many tokens to have in each input sentencemax_length = 48def 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 encoderimage_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 decoderdecoder = 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 modelmodel = 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 epochfilepath="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 modelmodel.fit([image_data, X], y, batch_size=64, shuffle=False, validation_split=0.1, callbacks=callbacks_list, verbose=1, epochs=50)
复制代码 精确测试
找到合适的方式测量准确性很难。你可能会说可以一个单词一个单词对比呀。如果一个单词一个单词对比,可能会是 0% 的准确率。但是如果移动一个单词,可能准确率就变成了 99%。
我使用的是 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-grams 的数目来让它更难。四个 n-gram 模型是人工翻译最好的测试模型。我建议用下面的代码多运行一些测试或者读一读它的文档。
- #Create a function to read a file and return its contentdef load_doc(filename): file = open(filename, 'r') text = file.read() file.close() return textdef 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 = ' ' + load_doc(data_dir+filename) + ' ' #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 vocabularytokenizer = Tokenizer(filters='', split=" ", lower=False)#Create the vocabulary in a specific ordertokenizer.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 modelloaded_model.load_weights("../../../../weights.hdf5")print("Loaded model from disk")# map an integer to a worddef word_for_id(integer, tokenizer): for word, index in tokenizer.word_index.items(): if index == integer: return word return Noneprint(word_for_id(17, tokenizer))# generate a description for an imagedef generate_desc(model, tokenizer, photo, max_length): photo = np.array([photo]) # seed the generation process in_text = ' ' # iterate over the whole length of the sequence print('\nPrediction---->\n\n ', 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 == '': break return in_textmax_length = 48# evaluate the skill of the modeldef 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, predictedbleu, actual, predicted = evaluate_model(loaded_model, texts, train_features, tokenizer, max_length)#Compile the tokens into HTML and cssdsl_path = "compiler/assets/web-dsl-mapping.json"compiler = Compiler(dsl_path)compiled_website = compiler.compile(predicted[0], 'index.html')print(compiled_website )print(bleu)
复制代码 输出
![]()
输出结果的例子的链接
- 生成的网站 1—原始1
- 生成的网站 2—原始2
- 生成的网站 3—原始3
- 生成的网站 4—原始4
- 生成的网站 5—原始5
[h2]我犯的错误[/h2]- 理解模型的弱点而不是测试随机的模型。 最初我使用了一个随机的模型类似于 batch normalization 和 bidirectional networks,尝试花更多精力在上面。后来查看了测试数据发现它无法准确预测颜色和位置等信息,我意识到了 CNN 在某些放面有缺陷。所以我把 mapooling 替换成了更新的 strides。validation 损失从 0.12 降到了 0.02,BLEU 分数从 85% 增加到了 97%。
- 如果有现成的就使用预训练的数据。 用一个小数据集的图片预训练模型会提高性能。根据我的经验,端到端模型训练起来很慢并且需要更多的内存,只提高了 30% 的准确性。
- 如果把你的模型运行在远程服务器上的话需要注意一些细节。 在我的 mac 上,文件按字母顺序读取。可是在服务器上读取顺序是随机的。这样截图和代码之间就不能匹配了。虽然可能有相交,但是数据比我修复这一问题后糟糕了 50%。
- 确保你了解了库的功能。 比如在你的词汇里面的空格产生的空 token。当我添加时,它并没有添加这些 tokens。在调试之后我只注意到了最终输出了多次,并没有预测出 “single” token。在排查后,我意识它甚至到不在词汇表中。因此,最好按和词汇表里相同的顺序训练测试。
- 体验的时候尽量使用轻量级的库。 使用 GRUs 而不是 LSTMs,这样每个 epoch 能减少 30% ,同时对性能又不会有太大的影响。
[h2]下一步[/h2]前端开发是深度学习的一个重要的应用场景。生成数据很容易,当前的深度学习算法能覆盖到大部分逻辑。
另一个更令人振奋的领域是 在 LSTMs 里使用 attention。这不仅能提升准确度,还能可视化。CNN 则更专注于生成代码。
Attention 是代码标签、样式、脚本和后端间沟通的桥梁。Attention 层可以打通变量,能够在不同编程语言间通讯。
在最终版本里,需要考虑下怎样用一个可扩展的方式来生成数据。接下来就可以添加字体、颜色、单词甚至是动画。
目前为止,大部分的流程都是通过 sketches 设计然后把它们转换成模板 app。在未来两年时间里,我们在纸上画一个 APP,前端会在 1 秒内得到页面。现在 Airbnb 的设计团队和 Uizard 已经有了两个原型。
这里是更多的玩法。
玩法
起步
- 运行所有的方法
- 尝试不同的 hyper 参数
- 测试不同的 LSTM 模型
- 使用不同的数据集实现模型。(可以通过
- --data emilwallner/datasets/100k-html:data
复制代码 flag 方便的在 FloydHub 上挂载这一工具集)
更多玩法
- 创建一个固定的随机的 app/web 自动代码生成器。
- sketch 的 app model 数据。自动转换 app/web 截图到 sketches,使用 GAN 来创建分类。
- 应用 attention 层来可视化每个预测焦的预测,类似于这个模型。
- 创建一个模块化功能的框架。已经有了字体、颜色、布局的编码模块,把它们组合成一个解码器。建议从实现图像特性开始。
- 为机器提供简单的 HTML 组件,教会它使用 CSS 生成动画。采用 attention 方法专注于两个输入源一定很有趣。
推荐阅读:
FCC DevTalk 003丨徐子达:真实的你如此可爱
通过配置 VS Code 来提高生产力
实用指南: 编写函数式 JavaScript
点击 “阅读原文” 查看 freeCodeCamp 论坛的更多内容
|
|