厲害了!神經(jīng)網(wǎng)絡(luò)替你寫前端代碼
在未來(lái)的三年內(nèi),深度學(xué)習(xí)將改變前端開發(fā)現(xiàn)狀——它會(huì)提高原型設(shè)計(jì)的速度,并將降低軟件開發(fā)的門檻。
繼去年Tony Beltramelli和Airbnb推出了pix2code和ketch2code的論文之后,這個(gè)領(lǐng)域就開始騰飛?,F(xiàn)在自動(dòng)化前端開發(fā)的最大瓶頸是計(jì)算能力,但是通過深度學(xué)習(xí)算法以及綜合的訓(xùn)練數(shù)據(jù)集,我們已然可以開始探索人工前端自動(dòng)化。
本文我們將通過訓(xùn)練一個(gè)神經(jīng)網(wǎng)絡(luò),使它可以直接將網(wǎng)頁(yè)的設(shè)計(jì)原型圖轉(zhuǎn)換成基本的HTML和CSS網(wǎng)頁(yè)。
以下是這個(gè)訓(xùn)練過程的簡(jiǎn)要概述:
1)將網(wǎng)頁(yè)設(shè)計(jì)圖導(dǎo)入到訓(xùn)練后的神經(jīng)網(wǎng)絡(luò)中
2)HTML標(biāo)記
3)展示結(jié)果
“我們將構(gòu)建三個(gè)版本的神經(jīng)網(wǎng)絡(luò)”
在第一個(gè)版本,我們將實(shí)現(xiàn)一個(gè)最低限度版本,了解動(dòng)態(tài)這部分的竅門。
HTML版本著重于全過程自動(dòng)化,并且會(huì)解釋各個(gè)神經(jīng)網(wǎng)絡(luò)層。最后Bootstrap版中,我們會(huì)創(chuàng)建基于LSTM的模型。
這篇文章里的模型都是以Beltramelli的pix2code論文和Brownlee的圖像標(biāo)注教程。這篇文章的代碼用Python和Keras(基于TensorFlow)完成的。
如果你還是個(gè)深度學(xué)習(xí)領(lǐng)域的新手,那我建議你先了解一下Python,反向傳播算法和卷積神經(jīng)網(wǎng)絡(luò)。我之前的三篇文章可以幫助你開始了解。
核心邏輯
讓我們重新回顧一下我們的目標(biāo):我們想要構(gòu)建一個(gè)能夠?qū)⒕W(wǎng)頁(yè)截圖轉(zhuǎn)換成相應(yīng)HTML/CSS代碼的神經(jīng)網(wǎng)絡(luò)。
我們提供給神經(jīng)網(wǎng)絡(luò)網(wǎng)頁(yè)截圖和相對(duì)應(yīng)的HTML代碼來(lái)訓(xùn)練它。.它逐個(gè)預(yù)測(cè)匹配的HTML標(biāo)簽來(lái)學(xué)習(xí)。當(dāng)它要預(yù)測(cè)下一個(gè)標(biāo)簽時(shí),會(huì)收到網(wǎng)頁(yè)截圖及對(duì)應(yīng)的完整標(biāo)簽,直到下一個(gè)標(biāo)簽開始。谷歌表格提供了一個(gè)簡(jiǎn)單的訓(xùn)練數(shù)據(jù)的例子創(chuàng)建一個(gè)逐詞預(yù)測(cè)模型是當(dāng)今最常見的方法。當(dāng)然還有其他的方法(https://machinelearningmastery.com/deep-learning-caption-generation-models/),但在整個(gè)教程中我們還是會(huì)使用的逐詞預(yù)測(cè)的模型。
請(qǐng)注意,由于對(duì)它的每個(gè)預(yù)測(cè)我們都使用同樣的網(wǎng)頁(yè)截圖,所以如果我們需要它預(yù)測(cè)20個(gè)詞,那么就需要給它看20遍這個(gè)設(shè)計(jì)原型(即網(wǎng)頁(yè)截圖)。現(xiàn)在不要在意神經(jīng)網(wǎng)絡(luò)是如何工作的,我們的重點(diǎn)應(yīng)該放在神經(jīng)網(wǎng)絡(luò)的輸入輸出參數(shù)上。
讓我們把重點(diǎn)放在前面的標(biāo)簽。假設(shè)我們訓(xùn)練神經(jīng)網(wǎng)絡(luò)來(lái)預(yù)測(cè)“I can code"這句話。當(dāng)它收到"I"時(shí),它會(huì)預(yù)測(cè)到”can" 。接著它會(huì)收到"I can“并預(yù)測(cè)到”code"。它接收之前所有的單詞然后只需要預(yù)測(cè)下一個(gè)單詞。
數(shù)據(jù)讓神經(jīng)網(wǎng)絡(luò)可以創(chuàng)建特征。特征讓輸入數(shù)據(jù)和輸出數(shù)據(jù)有了聯(lián)系。神經(jīng)網(wǎng)絡(luò)需要將學(xué)習(xí)的網(wǎng)頁(yè)截圖、HTML語(yǔ)法構(gòu)建出來(lái),用來(lái)預(yù)測(cè)下一個(gè)HTML標(biāo)簽。
無(wú)論什么用途,訓(xùn)練模型的方式總是相似的。在每一次迭代中使用相同的圖片生成一段段代碼。我們并不會(huì)輸入正確的HTML標(biāo)簽給神經(jīng)網(wǎng)絡(luò),它使用自已生成的標(biāo)簽來(lái)預(yù)測(cè)下段標(biāo)簽。預(yù)測(cè)在“開始標(biāo)簽”時(shí)初始化,當(dāng)它預(yù)測(cè)到“結(jié)束標(biāo)簽”或達(dá)到上限時(shí)結(jié)束。Google Sheet上有另一個(gè)示例。
“Hello World!"版本
讓我們來(lái)構(gòu)建”Hello World"版本。我們提供給神經(jīng)網(wǎng)絡(luò)一張顯示"Hello World"的網(wǎng)頁(yè)截圖并教它如何生成對(duì)應(yīng)的標(biāo)簽。
首先,神經(jīng)網(wǎng)絡(luò)將設(shè)計(jì)模型映射成像素值列表。像素值為0-255,其三個(gè)通道為紅、黃、藍(lán)。
為了使神經(jīng)網(wǎng)絡(luò)能夠理解標(biāo)簽,我使用獨(dú)熱編碼,所以”I can code"會(huì)被映射成下圖這個(gè)樣子:
如上圖所示,我們引入了開始標(biāo)簽和結(jié)束標(biāo)簽,它們能夠幫助神經(jīng)網(wǎng)絡(luò)預(yù)測(cè)從哪里開始到哪里結(jié)束。我們使用順序詞組做為輸入,它是從第一個(gè)詞開始,順序連接后面的詞。輸出總是一個(gè)詞。
順序詞組的邏輯和詞是一樣的,不過它們需要相同的詞組長(zhǎng)度。
它們不受詞匯表的限制,但受到最長(zhǎng)詞組長(zhǎng)度的限制。如果它比最長(zhǎng)詞組長(zhǎng)度短,你需要用空詞去補(bǔ)全它,空詞是內(nèi)容全為0的詞。
如你所見,空詞是填充在左側(cè)的。這樣每次訓(xùn)練都會(huì)改變?cè)~的位置,使得模型能夠?qū)W會(huì)這句話而不是記住每個(gè)單詞的位置。下圖每一行均表示一次預(yù)測(cè),共有四次預(yù)測(cè)。逗號(hào)左邊是用RGB表示的圖片,逗號(hào)右邊是前面詞,括號(hào)外從上到下分別是每個(gè)預(yù)測(cè)的結(jié)果,其中結(jié)束標(biāo)簽用紅色方塊表示。
#Length of longest sentence max_caption_len = 3 #Size of vocabularyvocab_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 Hello World! [1., 0.,0.0., 1., 0.]]]) #Turn next word into one-hot encoding next_words = np.array( [[0., 1., 0.], # Hello World! [0., 0., 1.]]) # end# Load the VGG16 model trained on imagenet and output the classification feature VGG = VGG16(weights='imagenet', include_top=True) # Extract thefeatures 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 nextdecoder_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"版本中,我們使用三個(gè)詞條:"start"、"Hello World!"和"end"。
用字符、單詞或者句子作為詞條都是可以的。使用字符作為詞條需要小量的詞匯表,但是會(huì)壓垮神經(jīng)網(wǎng)絡(luò)。三者中用單詞作為詞條最佳。
讓我們開始預(yù)測(cè)吧:
- # 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", "
- Hello World!
- ", "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
走過的彎路:
-
在收集數(shù)據(jù)之前構(gòu)建第一個(gè)運(yùn)行的版本。在這個(gè)項(xiàng)目的早期,我設(shè)法得到Geocities托管網(wǎng)站的一個(gè)舊版本。它有3800萬(wàn)個(gè)網(wǎng)站。我只顧著這個(gè)數(shù)據(jù)的巨大的可能性,忽視了減少100K大小詞匯所需的巨大工作量。
-
處理一個(gè)TB級(jí)的數(shù)據(jù)需要很好的硬件或者很強(qiáng)的耐心。在我的Mac遇到幾個(gè)問題后,我最終使用了一個(gè)功能強(qiáng)大的遠(yuǎn)程服務(wù)器。要想獲得順暢的工作流程,估計(jì)你得租一個(gè)8核CPU的設(shè)備,再加上1GPS的網(wǎng)速
-
直到我理解了輸入和輸出數(shù)據(jù)之后,一切才變得有意義起來(lái)。輸入數(shù)據(jù)X是網(wǎng)頁(yè)截圖和之前的標(biāo)簽。輸出數(shù)據(jù)Y是下一個(gè)標(biāo)簽。當(dāng)我明白了這個(gè)時(shí),理解它們之間的一切變得更容易了。嘗試不同的體系結(jié)構(gòu)也變得更加容易。
-
別鉆牛角尖。因?yàn)檫@個(gè)項(xiàng)目在深度學(xué)習(xí)中與很多領(lǐng)域相交叉,所以我一路鉆了很多牛角尖。我花了一個(gè)星期從頭開始編寫RNN,也對(duì)嵌入向量空間感到非常著迷,并且被它獨(dú)特的實(shí)現(xiàn)方法誘惑了。
-
圖片到編碼的網(wǎng)絡(luò)是偽裝了的圖像描述模型。即使當(dāng)我了解到這一點(diǎn),我仍然忽視了許多圖像描述的論文,只是因?yàn)樗鼈儾惶帷5且坏┪伊私庖恍┻@方面的觀點(diǎn),我就加快了對(duì)問題空間的了解。
在FloygdHub上運(yùn)行代碼
FloydHub是一個(gè)深度學(xué)習(xí)的培訓(xùn)平臺(tái)。我在剛開始學(xué)習(xí)深度學(xué)習(xí)的時(shí)候發(fā)現(xiàn)了這個(gè)平臺(tái),而且用它來(lái)訓(xùn)練和管理我的深度學(xué)習(xí)實(shí)驗(yàn)。你可以安裝FloydHub,在10分鐘內(nèi)你就可以運(yùn)行你的第一個(gè)模型了。這是云端GPU運(yùn)行模型的最佳選擇。
如果你是FloydHub的新手,那么做2分鐘的安裝(https://www.floydhub.com/)或5分鐘的演練(https://www.youtube.com/watch?v=byLQ9kgjTdQ&t=21s)。
克隆存儲(chǔ)庫(kù)
git clone https://github.com/emilwallner/Screenshot-to-code-in-Keras.git
登錄并啟動(dòng)FloydHub命令行工具
- cd Screenshot-to-code-in-Keras
- floyd login
- floyd init s2c:
在FloydHub云GPU機(jī)器上運(yùn)行一個(gè)Jupyter Notebook:
floyd run --gpu --env tensorflow-1.4 --data emilwallner/datasets/imagetocode/2:data --mode jupyter
所有筆記本都在floydhub目錄中。本地東西在本地。運(yùn)行之后,你可以在這個(gè)地址找到第一個(gè)筆記本:
floydhub / Hello world / hello world.ipynb。
如果你想要更詳細(xì)的說(shuō)明和標(biāo)志的解釋,請(qǐng)關(guān)注我以前的帖子(https://blog.floydhub.com/colorizing-b&w-photos-with-neural-networks/)。
HTML版本
在這個(gè)版本中,我們將自動(dòng)執(zhí)行Hello World模型的多個(gè)步驟。本部分將著重于創(chuàng)建一個(gè)可擴(kuò)展的實(shí)現(xiàn)和神經(jīng)網(wǎng)絡(luò)中的活動(dòng)部分。
這個(gè)版本達(dá)不到隨便給一個(gè)網(wǎng)站就能推演HTML代碼,但它仍然為我們探索動(dòng)態(tài)問題提供了一個(gè)不錯(cuò)的思路。
概覽
如果我們展開先前圖形的組件,看起來(lái)就會(huì)像這樣。
這個(gè)版本主要由兩部分組成:
1.首先用于創(chuàng)建圖像特征和之前標(biāo)簽特征的編碼器。特征是神經(jīng)網(wǎng)絡(luò)創(chuàng)建的用來(lái)連接設(shè)計(jì)原型和標(biāo)簽的構(gòu)建塊。在編碼器的結(jié)束部分,我們將圖像特征和對(duì)應(yīng)的標(biāo)簽串起來(lái)。
2.然后解碼器將用合并后的設(shè)計(jì)原型特征和標(biāo)簽特征來(lái)創(chuàng)建下一個(gè)標(biāo)簽特征。這個(gè)特征運(yùn)行在一個(gè)全連接神經(jīng)網(wǎng)絡(luò)上。
設(shè)計(jì)原型特征
由于每一個(gè)詞前我們都需要插入一張網(wǎng)頁(yè)截圖,它成了訓(xùn)練神經(jīng)網(wǎng)絡(luò)的瓶頸 (example)(https://docs.google.com/spreadsheets/d/1xXwarcQZAHluorveZsACtXRdmNFbwGtN3WMNhcTdEyQ/edit#gid=0)。所以我們不使用圖像,我們僅提取我們需要的信息來(lái)生成標(biāo)簽。
這是通過一個(gè)已經(jīng)預(yù)先在Imagenet上訓(xùn)練好的卷積神經(jīng)網(wǎng)絡(luò)(CNN)來(lái)完成的。在最后的分類前我們需要從神經(jīng)網(wǎng)絡(luò)層中提取特征。
最終的特征是1536個(gè)8*8像素的圖片集。雖然我們很難理解這些內(nèi)容,但是神經(jīng)網(wǎng)絡(luò)可以從這里面提取物體的位置和元素。
HTML標(biāo)簽特征
在“Hello World”版本中,我們使用獨(dú)熱編碼來(lái)表示標(biāo)簽。在這個(gè)版本中,我們使用詞嵌入作為輸入,輸出數(shù)據(jù)仍保持獨(dú)熱編碼格式。
句子的數(shù)據(jù)結(jié)構(gòu)是一樣的,不過映射詞條的方式是不一樣的。獨(dú)熱編碼將每個(gè)詞都當(dāng)作獨(dú)立部分。相反,我們將輸入的詞轉(zhuǎn)化成數(shù)字列表來(lái)表示標(biāo)簽之間的關(guān)系。
詞嵌入是8維的,根據(jù)詞匯量的大小,變化通常在50-500之間。代表詞的8個(gè)數(shù)字代表權(quán)重就像原始神經(jīng)網(wǎng)絡(luò)(vanilla neural network)一樣。它們會(huì)不斷地調(diào)整以表示詞與詞之間的關(guān)系。
這就是我們開始開發(fā)標(biāo)簽特征的方法。在神經(jīng)網(wǎng)絡(luò)中特征被開發(fā)出來(lái)用來(lái)表示輸入數(shù)據(jù)和輸出數(shù)據(jù)之間的關(guān)系。先不用擔(dān)心它們是什么,我們?cè)诤竺鏁?huì)深入講解。
編碼器
我們會(huì)將詞嵌入傳入到LTSM中,它會(huì)返回連續(xù)的標(biāo)簽特征。它們通過時(shí)序全連接層(time dense layer)運(yùn)行 -把它想成有多個(gè)輸入輸出的全連接層。
同時(shí)圖像特征也被提取出來(lái)。無(wú)論圖像以哪種數(shù)據(jù)結(jié)構(gòu)表示,它們都會(huì)被展開成一個(gè)很長(zhǎng)的向量,再傳送到全連接層,提取出高級(jí)特征。然后將這些特征與標(biāo)簽特征級(jí)聯(lián)起來(lái)。
這個(gè)過程比較復(fù)雜 -讓我們一步步來(lái)
標(biāo)簽特征
這里我們把詞嵌入傳輸?shù)絃STM層。如下圖所示,所有的句子都被填充成長(zhǎng)度為3的詞符。
為了混合信號(hào)及發(fā)現(xiàn)高級(jí)模式,我們引入一個(gè)TimeDistributed全連接層到標(biāo)簽特征中。TimeDistributed全連接層和通常的全連接層相似,只不過有很多的輸入和輸出。
圖像特征
同時(shí)我們會(huì)準(zhǔn)備好圖片。我們將這些圖片特征轉(zhuǎn)換成一個(gè)長(zhǎng)列表。這些信息并沒有發(fā)生變化,只是結(jié)構(gòu)不同罷了。
同樣的,為了混合信號(hào)并且提取高級(jí)概念我們?cè)僖肴B接層。由于我們只需要處理一個(gè)輸入值,所以一個(gè)普通的全連接層就行了。為了連接圖像特征和標(biāo)簽特征,我們復(fù)制圖像特征。
級(jí)聯(lián)圖像特征和標(biāo)簽特征
所有的句子都被填充以創(chuàng)建3個(gè)標(biāo)簽特征。由于我們已經(jīng)準(zhǔn)備好了圖像特征,現(xiàn)在我們可以為每個(gè)標(biāo)簽特征添加圖像特征。
在為每個(gè)標(biāo)簽特征添加圖像特征后,我們得到3個(gè)圖像-標(biāo)簽特征。我們會(huì)把它們傳輸?shù)浇獯a器中。
解碼器
這里我們使用剛才得到的圖像-標(biāo)記結(jié)合特征來(lái)預(yù)測(cè)下一個(gè)標(biāo)簽。
在下面的例子里,我們用這三個(gè)圖像-標(biāo)記特征對(duì)來(lái)輸出下一個(gè)標(biāo)簽特征。請(qǐng)注意LSTM層將序列設(shè)置為“否”,也就是說(shuō)它僅會(huì)預(yù)測(cè)出一個(gè)合并特性,而不是返回同輸入序列同樣長(zhǎng)度的特性描述序列。在我們的實(shí)際案例中,這就是下一個(gè)標(biāo)簽的特性。它包含了最終預(yù)測(cè)結(jié)果所需要的信息。
最終預(yù)測(cè)
全連接層所起的作用就相當(dāng)于一個(gè)傳統(tǒng)的前饋型神經(jīng)網(wǎng)絡(luò)。
它將下個(gè)標(biāo)簽特性所包含的512個(gè)數(shù)位與4個(gè)最終預(yù)測(cè)值聯(lián)系起來(lái)。假設(shè)詞匯表中有四個(gè)單詞:start,hello,world以及end。
對(duì)于詞匯表中詞匯的預(yù)測(cè)值可能是[0.1, 0.1, 0.1, 0.7],在全連接層啟用softmax函數(shù),將概率在0-1的區(qū)間內(nèi)進(jìn)行離散,同時(shí)所有預(yù)測(cè)值和為1。在本例中,預(yù)測(cè)第四個(gè)單詞即為下一個(gè)標(biāo)簽。之后使用解碼器,將獨(dú)熱編碼的結(jié)果[0,0,0,1]譯為映射值當(dāng)中的“end”。
# Load the images and preprocess them for inception-resnet images = all_filenames = listdir('images/') all_filenames.sort for filename inall_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 tokensmax_caption_len = 100 # Initialize the function that will create our vocabulary tokenizer = Tokenizer(filters='', split=" ", lower=False) # Read a document and return a string defload_doc(filename): file = open(filename, 'r') text = file.read file.close return text # Load all the HTML files X = all_filenames = listdir('html/'for filename inall_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 indexsequences = tokenizer.texts_to_sequences(X) # The longest HTML filemax_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 inenumerate(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 dataX.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 worddefword_for_id(integer, tokenizer): for word, index intokenizer.word_index.items: if index == integer: return word returnNone # generate a description for an image defgenerate_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 wordisNone: 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)
輸出
生成的網(wǎng)站鏈接:
-
250 epochs(https://emilwallner.github.io/html/250_epochs/)
-
350 epochs(https://emilwallner.github.io/html/350_epochs/)
-
450 epochs(https://emilwallner.github.io/html/450_epochs/)
-
550 epochs(https://emilwallner.github.io/html/550_epochs/)
如果點(diǎn)擊鏈接無(wú)法顯示任何結(jié)果,你可以右擊選擇“查看網(wǎng)頁(yè)源代碼”。以下是案例中用于識(shí)別分析的原網(wǎng)站。
如果上面的鏈接打開后沒有內(nèi)容顯示,你可以右鍵選擇"查看網(wǎng)頁(yè)源代碼“。這里是這些網(wǎng)頁(yè)的本來(lái)的源代碼。
走過的彎路:
-
(對(duì)我來(lái)說(shuō))LSTM網(wǎng)絡(luò)的學(xué)習(xí)困難程度遠(yuǎn)高于卷積神經(jīng)網(wǎng)絡(luò)。在我全面認(rèn)識(shí)理解LSTM網(wǎng)絡(luò)之后,這一結(jié)構(gòu)對(duì)我來(lái)說(shuō)變得容易了一些。Fast.ai的循環(huán)神經(jīng)網(wǎng)絡(luò)視頻有極大的幫助。同時(shí)在你試圖理解這個(gè)網(wǎng)絡(luò)結(jié)構(gòu)如何運(yùn)行的前,也要仔細(xì)觀察那些輸入和輸出的特征。
-
從頭構(gòu)建一個(gè)詞庫(kù)比起壓縮一個(gè)巨大的詞庫(kù)要容易太多,因?yàn)楹笳呱婕暗阶煮w、DIV大小、十六進(jìn)制顏色編碼、變量名稱和網(wǎng)頁(yè)內(nèi)容。
-
通常文本文件內(nèi)容是用空格分開的,但是在代碼文件中,你需要自定義解析方法。
-
你可以提取用Imagenet上已經(jīng)訓(xùn)練好的模型來(lái)提取特征。Imagenet幾乎沒有什么網(wǎng)頁(yè)圖片,這可能有違直覺。但是同pix2code的模型比起來(lái),它的損失要高30%。當(dāng)然我也對(duì)基于網(wǎng)頁(yè)截圖來(lái)預(yù)訓(xùn)練的inception-resnet模型很有興趣。
Bootstrap版本
在我們的最終版本中,我們會(huì)使用pix2code論文中搭建的bootstrap網(wǎng)站的一個(gè)數(shù)據(jù)集。通過使用Twitter的bootstrap,可以將HML和CSS相結(jié)合,并且壓縮詞庫(kù)的大小。
我們將讓它為一副之前沒有見過的網(wǎng)頁(yè)截圖生成標(biāo)簽,同時(shí)也會(huì)深入探討它是如何建立關(guān)于截圖與標(biāo)記的認(rèn)知。
我們將會(huì)使用17個(gè)經(jīng)過簡(jiǎn)化的詞條將這些記號(hào)轉(zhuǎn)換為HTML和CSS,而不是利用bootstrap標(biāo)簽進(jìn)行訓(xùn)練。這一套數(shù)據(jù)集包括1500個(gè)測(cè)試截圖以及250幅驗(yàn)證圖像。平均每個(gè)截圖都有65個(gè)詞條,結(jié)果總計(jì)產(chǎn)生96925個(gè)訓(xùn)練樣本。
通過對(duì)pix2code論文中提及模型的微調(diào),我們的模型對(duì)于網(wǎng)頁(yè)組件預(yù)測(cè)的準(zhǔn)確度可以高達(dá)97%(基于BLEU飽和搜索測(cè)評(píng),詳解見后)。
端到端方法
利用預(yù)訓(xùn)練過的模型提取特征對(duì)于圖像標(biāo)注模型效果的確很好。但經(jīng)過幾次試驗(yàn)之后,我發(fā)現(xiàn)pix2code的端到端方法對(duì)于這類問題效果更佳。預(yù)訓(xùn)練模型并不是通過網(wǎng)頁(yè)數(shù)據(jù)訓(xùn)練,而是通過定制的分類器訓(xùn)練。
在這一模型中,我們用一個(gè)輕量的卷積神經(jīng)網(wǎng)絡(luò)替換了預(yù)訓(xùn)練得到的圖像特征。我們通過增加步長(zhǎng)來(lái)增加信息密度,而不是最大池化函數(shù)。這樣能最大程度保留前端各元素的位置和顏色信息。
有兩個(gè)核心模型可以做到這一點(diǎn),卷積神經(jīng)網(wǎng)絡(luò)(CNN)和循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)。最常用的循環(huán)神經(jīng)網(wǎng)絡(luò)是長(zhǎng)短期記憶網(wǎng)絡(luò)(LSTM),接下來(lái)我將會(huì)用到。在我之前的文章中,也總結(jié)過許多非常棒的CNN教程,在本案例中,我們重點(diǎn)使用LSTM。
理解LSTM的時(shí)間步
掌握LSTM的難點(diǎn)在于時(shí)間步。一個(gè)原始神經(jīng)網(wǎng)絡(luò)(vanilla neural network)可以看做有兩個(gè)時(shí)間步。如果你輸入“Hello”,它將預(yù)測(cè)“World”。但是它嘗試去預(yù)測(cè)更多的時(shí)間步。
在以下的示例中,輸入包含了四個(gè)時(shí)間步,每一個(gè)都對(duì)應(yīng)一個(gè)詞。
LSTM適用于含有時(shí)間步的輸入,適用于有序信息的神經(jīng)網(wǎng)絡(luò)。如果你對(duì)于模型進(jìn)行解析,它的工作原理看起來(lái)類似這樣:每一次向下遞推都保持相同的權(quán)重。你可以對(duì)于之前的輸出設(shè)置一套權(quán)重,然后對(duì)于新的輸入再設(shè)置另一套權(quán)重。
被賦予權(quán)重的輸入和輸出將通過激活被聯(lián)結(jié)并且組合,也就形成了那一時(shí)間步的輸出。因?yàn)槲覀儠?huì)反復(fù)使用這些權(quán)重,也就會(huì)從若干次的輸入中得出信息,并形成對(duì)于結(jié)果的認(rèn)知。加權(quán)后的輸入和輸出級(jí)聯(lián)后傳輸給一個(gè)激活函數(shù),作為相應(yīng)的時(shí)間步的輸出。這些被復(fù)用的權(quán)重集描述了輸入的信息,并構(gòu)建序列的知識(shí)。
以下是LSTM模型中每一個(gè)時(shí)間步的簡(jiǎn)化版本。
建議大家參考Andrew Trask的神教程(https://iamtrask.github.io/2015/11/15/anyone-can-code-lstm/),從無(wú)到有搭建一個(gè)RNN網(wǎng)絡(luò)來(lái)理解背后的邏輯
理解LSTM層的單元
每一層LSTM的單元數(shù)量決定了它的記憶力,也同樣決定了輸出特征的大小。需要再次指出,我們這里的特征是層與層之間傳遞信息的一長(zhǎng)串?dāng)?shù)字。
LSTM層中的每個(gè)單元都保留對(duì)于語(yǔ)法不同方面的記錄。下面展示了如何實(shí)現(xiàn)一個(gè)單元對(duì)于div行信息進(jìn)行保留。這也是我們用于訓(xùn)練bootstrap模型的一種簡(jiǎn)化標(biāo)記。
每一個(gè)LSTM單元都存儲(chǔ)了一個(gè)細(xì)胞狀態(tài)。把細(xì)胞狀態(tài)看作記憶,權(quán)重和激活函數(shù)使用不同的方式改變這個(gè)狀態(tài)。使得LSTM層對(duì)于每一次的輸入,可以很好的調(diào)整那些信息,決定哪些需要保留,而哪些需要丟棄。
除了傳遞每一次輸入的輸出特征外,層單元也會(huì)傳遞細(xì)胞狀態(tài),每一個(gè)LSTM細(xì)胞都對(duì)應(yīng)一個(gè)不同的值。如果想了解LSTM層中的要素之間都是如何相互作用的,我強(qiáng)烈推薦Colah的教程,Jayasiri的Numpy實(shí)現(xiàn),Karphay的講座以及評(píng)論。
dir_name = 'resources/eval_light/' # Read a file and return a stringdefload_doc(filename): file = open(filename, 'r') text = file.read file.close return text defload_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' '.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 vocabularytokenizer = 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 intrain_sequences) # Specify how many tokens to have in each input sentencemax_length = 48 defpreprocess_data(sequences, features): X, y, image_data = list, list, list for img_no, seq in enumerate(sequences): for i inrange(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) returnnp.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, (33), activation='relu', padding='same', strides=2)) image_model.add(Conv2D(32, (33), activation='relu', padding='same'32, (33), activation='relu', padding='same', strides=264, (33), activation='relu', padding='same'64, (33), activation='relu', padding='same', strides=2128, (33), 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'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)
準(zhǔn)確率測(cè)試
用一種公平合理的方式測(cè)試正確率是比較困難的。假設(shè)逐詞對(duì)照,如果在同步時(shí)有一個(gè)詞錯(cuò)位,也許只能收獲0%的正確率。而如果你刪掉一個(gè)符合同步預(yù)測(cè)的詞,準(zhǔn)確率也可能高達(dá)99%。
我使用了BLEU測(cè)評(píng),這一測(cè)評(píng)在機(jī)器翻譯和圖像標(biāo)注模型上有很好表現(xiàn)。測(cè)評(píng)將句子打散為四個(gè)n-gram,也就是1-4個(gè)詞組成的字符串。在以下的預(yù)測(cè)中,應(yīng)該是“code”而不是‘cat’。
最終評(píng)分需要將每次打散的成績(jī)乘以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
求和結(jié)果再乘以句子長(zhǎng)度做取值補(bǔ)償。因?yàn)槲覀円陨系睦又虚L(zhǎng)度取值正確,所以這就是我們最終測(cè)評(píng)分?jǐn)?shù)。
你可以通過增加n-gram的組數(shù)來(lái)增加測(cè)評(píng)難度,分為4組n-gram的模型是最符合人類的直覺。建議大家利用以下的代碼運(yùn)行一些案例,然后讀一讀維基百科對(duì)于BLEU測(cè)評(píng)的描述。
#Create a function to read a file and return its contentdefload_doc(filename): file = open(filename, 'r') text = file.read file.close return text defload_data(data_dir): text = images = files_in_folder = os.listdir(data_dir) files_in_folder.sort for filenamein 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' '.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 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 word defword_for_id(integer, tokenizer):for word, index in tokenizer.word_index.items: if index == integer: returnword returnNone print(word_for_id(17, tokenizer)) # generate a description for an image defgenerate_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(' Prediction----> ', end='') for i in range(150): # integer encode input sequence sequence = tokenizer.texts_to_sequences([in_text])[0] # pad inputsequence = 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 wordisNone: 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_text max_length = 48 # evaluate the skill of the model defevaluate_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 predictedprint(' Real----> ' + 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
(https://emilwallner.github.io/bootstrap/pred_1/) (https://emilwallner.github.io/bootstrap/real_1/)
-
Generated website 2 - Original 2
() ()
-
Generated website 3 - Original 3
() ()
-
Generated website 4 - Original 4
() ()
-
Generated website 5 - Original 5
() ()
走過的彎路:
-
理解每個(gè)模型的不足,而不是隨機(jī)選擇模型測(cè)試。一開始我采用的方法比較隨機(jī),比如批量歸一化,雙向網(wǎng)絡(luò),甚至嘗試實(shí)現(xiàn)注意力??吹綔y(cè)試數(shù)據(jù)后,我才明白這些方法并不能準(zhǔn)確的預(yù)測(cè)顏色和位置,這是我意識(shí)到卷積神經(jīng)網(wǎng)絡(luò)當(dāng)中存在一些不足。這導(dǎo)致我使用增加步長(zhǎng)的辦法去代替最大池化方法。損失從0.12降到了0.02,同時(shí)BLEU評(píng)分從85%升到97%。
-
如果具有相關(guān)性的話,僅考慮使用經(jīng)過預(yù)訓(xùn)練的模型。對(duì)于小型的數(shù)據(jù)集,我認(rèn)為一個(gè)經(jīng)過訓(xùn)練的圖像模型可以改善表現(xiàn)。以我個(gè)人的經(jīng)驗(yàn)看來(lái),一個(gè)端到端的模型訓(xùn)練費(fèi)時(shí),并且需要更多的內(nèi)存,但是準(zhǔn)確率會(huì)提高30%。
-
如果使用遠(yuǎn)程服務(wù)器運(yùn)行模型,需要考慮到輕微的偏差。我的MAC以字母表順序讀取文件,但是在服務(wù)器上,文件是隨機(jī)讀取的。這會(huì)導(dǎo)致截圖和代碼之間的不匹配。雖然預(yù)測(cè)結(jié)果趨同,但是有效數(shù)據(jù)比起重新匹配前要糟糕50%。
-
掌握引用的庫(kù)函數(shù)。包括詞匯表中的空詞條里的填充空格。如果不進(jìn)行特別添加,識(shí)別中將不包括這一標(biāo)記。我是通過幾次觀察到最終結(jié)果中無(wú)法預(yù)測(cè)出“單個(gè)”標(biāo)記,才注意到這一點(diǎn)。快速檢查一遍后,我意識(shí)到這并不包含在詞庫(kù)中。同時(shí)也需要注意,訓(xùn)練和檢測(cè)時(shí),需要使用同樣順序的詞庫(kù)。
-
實(shí)驗(yàn)時(shí)使用輕量的模型。利用GRU而不是LSTM可以讓每光華迭代循環(huán)的時(shí)間減少30%,并且對(duì)于結(jié)果不會(huì)有太大影響。
接下會(huì)發(fā)生什么?
-
前端開發(fā)是應(yīng)用深度學(xué)習(xí)理想的空間。生成數(shù)據(jù)容易,并且現(xiàn)在的深度學(xué)習(xí)算法可以實(shí)現(xiàn)絕大部分的邏輯。
-
其中很有意思的地方是“通過LSTM實(shí)現(xiàn)注意力”。它不僅可以用來(lái)提高準(zhǔn)確率,而且讓我們可以讓CNN將它的注意力放在生成標(biāo)簽上。
-
注意力也是標(biāo)簽、樣式、腳本甚至后端之間交流的關(guān)鍵。注意力層可以追蹤變量,使神經(jīng)網(wǎng)格可以在不同的編程語(yǔ)言中交流。
-
但是在不久的將來(lái),最大的影響來(lái)自于建立生成數(shù)據(jù)的可擴(kuò)展方法。那時(shí)你可以一步步地添加字體、顏色、內(nèi)容和動(dòng)畫。
-
目前大部分的進(jìn)步在是將草圖轉(zhuǎn)換成模板。在兩年內(nèi),我們可以在紙上畫上應(yīng)用的模板,然后瞬間生成對(duì)應(yīng)的前端代碼。事實(shí)上Airbnb’s design team(https://airbnb.design/sketching-interfaces/) andUizard(https://www.uizard.io/)已經(jīng)建立了基本可以使用的原型了。
進(jìn)一步的實(shí)驗(yàn)
-
根據(jù)相應(yīng)的語(yǔ)法創(chuàng)建一個(gè)穩(wěn)定的隨機(jī)應(yīng)用/網(wǎng)站生成器。
-
生成從草圖到應(yīng)用的數(shù)據(jù)。自動(dòng)轉(zhuǎn)換應(yīng)用/網(wǎng)頁(yè)截圖到草圖并用GAN來(lái)構(gòu)建多樣性。
-
添加注意力層,可視化每一次預(yù)測(cè)的焦點(diǎn),像這個(gè)模型一樣。
-
為模塊化方法創(chuàng)建一個(gè)框架。比如字體編碼器、顏色編碼器、結(jié)構(gòu)編碼器,然后用一個(gè)解碼器將它們整合起來(lái)。從穩(wěn)定的圖像特征開始似乎不錯(cuò)。
-
讓神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)簡(jiǎn)單的HTML組件,然后將它生成CSS動(dòng)畫。注意力機(jī)制和可視化輸入源真的很神奇。
關(guān)于作者: Emil Wallner
深度學(xué)習(xí)鄰域的資深博客撰寫者,投資人,曾就職于牛津大學(xué)商學(xué)院,現(xiàn)長(zhǎng)期居住在法國(guó),是非盈利組織42(écoles)項(xiàng)目組成員。我們已獲得授權(quán)翻譯