來源:Alibaba F2E
作者:業楓

Hi~ 我是前端學徒業楓,今天為大家帶來一篇硬核前端智能化教程,真·手把手教你用機器學習打造一個純前端運作的圖示智能識别工具。并附上完整代碼,一起來體驗前端智能化的魅力吧~
背景
目前的前端元件庫都使用 Iconfont 來管理圖示,随着時間推移,圖示越來越多,圖示的命名也五花八門,很難限制。開發者還原設計稿時,經常要人肉從幾百個圖示中尋找對應的圖示。有時候連設計師都找不到,導緻重複添加圖示。
最近發現在 AntDesign 官網有以圖搜圖示的功能,使用者對設計稿或任意圖檔中的圖示截圖,點選/拖拽/粘貼上傳,就可以搜尋到比對度最高的幾個圖示:
這個功能很好的解決了上面提到的問題,但還有些不足:
- 截圖最好是正方形的,否則拉伸後識别率會下降(後面會解釋)。
- 隻能識别 AntDesign 的圖示。
為了解決這些問題,我們決定自己打造一個前端圖示識别工具。下面将以我們團隊的開源元件庫 Cloud Design為例,手把手教你打造純前端的專屬圖示識别工具。(完整代碼放在文末)
術語簡介
簡單介紹幾個術語,了解的同學可以直接跳過。
機器學習
機器學習研究和建構的是一種特殊算法(而非某一個特定的算法),能夠讓計算機自己在資料中學習進而進行預測。
是以,機器學習不是某種具體的算法,而是很多算法的統稱。
機器學習包含:線性回歸、貝葉斯、聚類、決策樹、深度學習等等。前面 AntDesign 的模型是通過深度學習的代表算法 CNN 訓練得到的。
CNN 卷積神經網絡
卷積神經網絡(Convolutional Neural Networks, CNN)是一類包含卷積計算且具有深度結構的前饋神經網絡(Feedforward Neural Networks),最常用于分析視覺圖像。
CNN 能有效的将大資料量圖檔降維到小資料量,且保留圖像特征,非常适合處理圖像資料。即使圖像翻轉、旋轉或變換位置也能有效識别,常用來解決:圖像分類檢索、目标定位監測、人臉識别等等。
開始行動吧
我們要對圖示進行識别,屬于機器學習中經典的“圖像分類”問題。CNN(卷積神經網絡) 可以有效的識别圖示,但是無法适應拉伸變形的場景。因為模型輸入時要先把圖像變換為正方形尺寸,截圖尺寸不對會導緻圖像拉伸變形,降低識别率,甚至識别錯誤。
常用的解法有兩種:
1、純機器學習:通過增加不同拉伸狀态的樣本,讓模型适應變形的圖像。
2、機器學習 + 圖像處理:用圖像處理算法對資料進行裁剪,保證圖像接近正方形。
第一種方法需要生成大量的訓練資料,訓練速度變慢,而且拉伸變形的情況很難周遊。第二種方法隻需要進行簡單的圖像處理就可以有效提高識别率,是以我選擇了它。那最終工作流應該是這樣的:
接下來我會從 樣本生成、模型訓練、模型使用 三部分來介紹完整的過程。
樣本生成
圖像分類的訓練樣本都是圖檔,我們的圖示則是 iconfont 渲染在頁面中的。可以自然想到用 樣本頁面 + Puppeteer 截圖來生成樣本。但截圖速度很慢,我也不想用 Faas 服務,于是想了個本地生成的方法:
首先人工把圖示庫的css部分轉為js:
這樣就能把圖示當作文本繪制在 canvas 上,并用圖像算法裁剪四周的空白區域:
// 用離屏 canvas 繪制圖示
offscreenCtx.font = `20px NextIcon`;
offscreenCtx.fillText(labelMap[labelName]);
// 用 getImageData 擷取圖檔資料,計算需裁剪的坐标
const { x, y, width: w, height: h } = getCutPosition(canvasSize, canvasSize, offscreenCtx.getImageData(0, 0, canvasSize, canvasSize).data);
// 計算需裁剪的坐标
function getCutPosition(width, height, imgData) {
let lOffset = width; let rOffset = 0; let tOffset = height; let bOffset = 0;
// 周遊像素,擷取最小的非空白矩形區域
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
const pos = (i + width * j) * 4;
if (notEmpty(imgData[pos], imgData[pos + 1], imgData[pos + 2], imgData[pos + 3])) {
// 調整 lOffset、rOffset、tOffset、bOffset
// 略
}
}
}
// 如果形狀不是正方形,将其擴充為正方形
const r = (rOffset - lOffset) / (bOffset - tOffset);
if (r !== 1) {
// 略
}
return { x: lOffset, y: tOffset, width: rOffset - lOffset, height: bOffset - tOffset };
}
// 門檻值 0 - 255
const d = 5;
// 判斷是否非空白像素
function notEmpty(r, g, b, a) {
return r < 255 - d && g < 255 - d && b < 255 - d;
}
// 用 canvas 裁剪 & 縮放圖像,導出為 base64
ctx.drawImage(offscreenCanvas, x, y, w, h, 0, 0, 96, 96);
canvas.toDataURL('image/jpeg');
生成一張圖檔的邏輯就寫完了。改造一下,周遊不同圖示、不同字号,可以得到全量的樣本:
const fontStep = 1;
const fontSize = [20, 96];
labels.map((labelName) => {
// 周遊不同的字号繪制圖示
for (let i = fontSize[0]; i <= fontSize[1]; i += fontStep) {
// ...before
offscreenCtx.font = `${i}px NextIcon`;
// 其它邏輯
}
});
通過 Blob 将資料作為一個 json 下載下傳:
const resultData = /* 生成全量資料 */;
const aLink = document.createElement('a');
const blob = new Blob([JSON.stringify(resultData, null, 2)], { type : 'application/json' });
aLink.download = 'icon.json';
aLink.href = URL.createObjectURL(blob);
aLink.click();
這樣就得到了包含幾萬張(350個圖示,每個分類約70張圖)樣本圖檔的大 json,大概長這樣:
[
{
"name": "smile",
"data": [
{
"url": "data:image/jpeg;base64,/9j/4AA...IkB//9k=",
"size": 20
},
{
"url": "data:image/jpeg;base64,/9j/4AA...JAf//Z",
"size": 21
},
...
]
},
]
最後寫一個簡單的 node 程式,把每個分類的樣本按照訓練集70%,驗證集20%,測試集10%的比例拆分打散并存儲為圖檔檔案。
--- train
|-- smile
|-- smile_3.jpg
|-- smile_7.jpg
|-- cry
|-- cry_2.jpg
|-- cry_8.jpg
...
--- validation
|-- smile
|-- cry
...
--- test
|-- smile
|-- cry
...
這樣我們就得到了完整的訓練樣本,而且生成速度很快,運作一遍隻要1分鐘左右。然後把三個目錄一起打包成一個 zip 檔案即可,因為下一步訓練隻支援 zip 格式。
模型訓練
機器學習工具有很多種,作為一個前端,我最終選擇使用 Pipcook 來訓練。
Pipcook 項目是一個開源工具集,它能讓 Web 開發者更好地使用機器學習,進而開啟和加速前端智能化時代!(
https://alibaba.github.io/pipcook/#/zh-cn/)Pipcook 的安裝和教程看官網即可,要注意目前隻支援 Mac & Linux,Windows 暫時無法使用(Windows 可以使用 Tensorflow.js 訓練)。
寫一份 pipcook 的配置項:
{
"plugins": {
"dataCollect": {
"package": "@pipcook/plugins-image-classification-data-collect",
"params": {
"url": "file://絕對路徑,指向上一步打包的檔案.zip"
}
},
"dataAccess": {
"package": "@pipcook/plugins-pascalvoc-data-access"
},
"dataProcess": {
"package": "@pipcook/plugins-tfjs-image-classification-process",
"params": {
"resize": [224, 224]
}
},
"modelDefine": {
"package": "@pipcook/plugins-tfjs-mobilenet-model-define",
"params": {}
},
"modelTrain": {
"package": "@pipcook/plugins-image-classification-tfjs-model-train",
"params": {
"batchSize": 64,
"epochs": 12
}
},
"modelEvaluate": {
"package": "@pipcook/plugins-image-classification-tfjs-model-evaluate"
}
}
}
使用 Pipcook 配套的 Cli 工具開始訓練:
$ pipcook run 上面寫的配置項.json
看到出現 Epochs 和 Iteration 字樣說明訓練成功開始了。
...
i [job] running modelTrain start
i start loading plugin @pipcook/plugins-image-classification-tfjs-model-train
i @pipcook/plugins-image-classification-tfjs-model-train plugin is loaded
i Epoch 0/12 start
i Iteration 0/303 result --- loss: 5.969481468200684 accuracy: 0
i Iteration 30/303 result --- loss: 5.65574312210083 accuracy: 0.015625
i Iteration 60/303 result --- loss: 5.293442726135254 accuracy: 0.0625
i Iteration 90/303 result --- loss: 4.970404624938965 accuracy: 0.03125
...
兩萬多張樣本以上面的參數在我的 Mac 上訓練大約需要兩個小時,期間電腦的 cpu 資源都會被占用,是以要找好空閑的時間訓練。如果中途要停下來,用 control + c 是沒用的,需要先用
pipcook job list
檢視任務清單,再用
pipcook job stop <jobId>
來停止訓練。
訓練的時長與:樣本的資料量、epochs 和 batchSize 有關。
/ ======= 兩個小時後... ======= /
訓練完成,能看到最終的損失率(越低越好)和準确率(越高越好):
...
i [job] running modelEvaluate start
i start loading plugin @pipcook/plugins-image-classification-tfjs-model-evaluate
i @pipcook/plugins-image-classification-tfjs-model-evaluate plugin is loaded
i Evaluate Result: loss: 0.05339580587460659 accuracy: 0.9850694444444444
...
如果損失率大于 0.2,準确率低于 0.8,那訓練的效果就不太好了,需要調整參數或樣本,然後重新訓練。
同時 pipcook 會在配置項 json 同目錄下建立一個 output 檔案夾,裡面包含了我們需要的模型:
output
|-- logs # 訓練日志檔案夾
|-- model # 模型檔案夾,裡面兩個檔案就是最終需要的産物
|-- weights.bin
|-- model.json
|-- metadata.json # 元資訊
|-- package.json # 項目資訊
|-- index.js # 預設入口檔案
|-- boapkg.js # 輔助檔案
模型使用
因為用的 Pipcook 插件底層調用 Tensorflow.js 進行訓練,是以模型可以直接在前端頁面運作。
我們先把生成的
model.json
和
weights.bin
放在同一目錄下存好。然後找到
metadata.json
中的
output.dataset
字段,是個 Json 字元串,反序列化後找到的
labelArray
屬性的值并且存下來:
// 目前這個順序是随機生成的,和樣本生成時的順序不一樣,不要混淆了
const labelArray = ["col-before","h1","solidDown","add-test",...];
準備就緒,隻要再寫一些 Tensorflow.js 代碼就可以進行識别了。
import * as tf from '@tensorflow/tfjs';
const modelUrl = 'model.json 的通路位址';
// 加載模型
model = await tf.loadLayersModel(modelUrl);
// 對輸入圖像裁剪
const { x, y, width: w, height: h } = getCutPosition(imgW, imgH, offscreenCtx.getImageData(0, 0, imgW, imgH).data, 'white');
ctx.drawImage(offscreenCanvas, x, y, w, h, 0, 0, cutSize, cutSize);
// 圖像轉化為 tensor
const imgTensor = tf.image
.resizeBilinear(tf.browser.fromPixels(canvas), [224, 224])
.reshape([1, 224, 224, 3]);
// 模型識别
const pred = model.predict(imgTensor).arraySync()[0];
// 找出相似度最高的 5 項
const result = pred.map((score, i) => ({ score, label: labelArray[i] }))
.sort((a, b) => b.score - a.score)
.slice(0, 5);
大功告成
現在可以開始體驗圖示識别的能力,享受機器學習帶來的便利了。這是一個純前端工具,無需額外後端服務,可以在靜态網站上部署,非常适合在元件庫網站中查找圖示的場景。團隊有自己的圖示庫也完全沒問題,隻要按照步驟走,就能訓練出專屬的模型。
完整代碼見:
https://github.com/maplor/iconcook總結
從開始寫代碼到模型能用花了一個周末加兩個晚上,而搭建環境和訓練模型的時間占了很大比例。Pipcook 雖然使用簡單,省去了很多工作,但入門也有不少坑:文檔稀少,插件的參數隻有看源碼才明白,運作過程有一些潛規則需要不斷試錯。希望 Pipcook 的文檔能及時更新和維護。
如果有什麼疑問可以在評論指出,歡迎大家體驗交流~
常見問題
圖示庫如果有 新增/修改 圖示怎麼辦?答:需要重新訓練模型。
參考資料
- 斯坦福《機器學習》課程: https://www.coursera.org/learn/machine-learning
- 《Tensorflow.js 海量圖示,毫秒級識别!》: https://zhuanlan.zhihu.com/p/128669062
- Tensorflow.js 官網: https://tensorflow.google.cn/js
- Pipcook 官網: https://alibaba.github.io/pipcook/#/zh-cn/
- 一文看懂機器學習: https://easyai.tech/ai-definition/machine-learning/
- 一文看懂卷積神經網絡 CNN: https://easyai.tech/ai-definition/cnn/
關于我們
本文作者來自于阿裡雲TxD團隊,是由技術開發、體驗設計、品牌創意構成的綜合體驗團隊,我們一直緻力于雲産品基礎建設的探索與發展。坐标:杭州/北京。聯系我們:[email protected]。