前端智能化:實現自己的控件識別模型

用我們的前端智能化框架:http://github.com/alibaba/pipcook 內置實驗,可以方便的進行手寫數字識別和圖像分類任務,這裏按照環境準備、快速實驗、實戰方法、原理解析的順序,分四個部分進行介紹。完成本教程,你可以開始進行自己的前端智能化項目,用機器學習解決編程過程中遇到的問題。

環境準備

開始之前

臺式機和筆記本

首先,初學者我更推薦筆記本,因爲其便攜性和初期實驗的運算量並不是很大,可以保證在咖啡館或戶外立即開始學習和實踐。其次,現在的輕薄筆記本如小米的 Pro 款配備了 Max 150 滿血版,基本可以滿足常用的機器學習實驗。Mac Book Pro 的用戶可以考慮帶 AMD 顯卡的筆記本,因爲在 PlaidML(intel提供的機器學習後端)支持下,Keras 的大部分 OP 都是具備 GPU 硬件加速的。需要注意,PlaidML 對很多神經網絡支持不太好,比如對 RNN 的支持就不好,具體可以看 Issue。在我的16寸 Mac Book Pro上,PlaidML 對 RNN 無硬件加速效果,GPU 監視器未有負載且模型編譯過程冗長。

最後,對於有條件的朋友建議準備臺式機,因爲在學習實驗中將會遇到越來越多複雜模型,這些模型一半都需要訓練數天,臺式機能夠提供更好的散熱性能來保證運行的穩定性。

組裝臺式機的時候對CPU的主頻要求不用太高,一般 AMD 的中低端 CPU 即可勝任,只要核心數達到6個以上的AMD 12 線程CPU基本就夠用了。內存方面最好是 32GB ,16GB 只能說夠用,對海量數據尤其是圖片類型進行加工處理的時候,最容易爆的就是內存。

GPU方面由於ROCM的完善,喜歡折騰的人選擇 AMD GPU 完全沒問題,不喜歡折騰可以選擇 Nvidia GPU,需要指出的是顯存容量和顯存帶寬在預算允許的範圍內越大越好,尤其是顯存容量,海量參數的大模型沒有大顯存根本無法訓練。

硬盤方面選擇高速 SSD 作爲系統盤 512GB 起步,掛載一個混合硬盤作爲數據存儲和模型參數存儲即可。電源儘量選擇大一點兒,除了考慮峯值功耗之外,未來可能要考慮多 GPU 來加速訓練過程、應對海量參數。機箱作爲硬件的家,電磁屏蔽性能好、板材厚重、空間大便於散熱即可,用水冷打造性能小鋼炮的除外。

選擇的依據很簡單:喜歡折騰的按上述內容 DIY ,喜歡簡單的按上述內容買帶售後的品牌機。兩者的區別就是花時間省點兒錢?還是花錢省點兒時間?

操作系統

對於筆記本自帶 Windows 操作系統的,直接使用 Windows 並沒有問題,Anaconda 基本可以搞定和研發環境的所有問題,而且其自帶的 NPM 管理工具很方便。有條件愛折騰的上一個 Ubuntu Linux 系統最好,因爲在 Linux 下能夠更加原生支持機器學習相關技術生態,幾乎不會遇到兼容性問題。

對於臺式機建議安裝 Ubuntu Linux 系統,否則,這麼好的顯卡很容易裝個 Windows 玩遊戲去了……Ubuntu 的安裝盤製作很簡單,一個U盤搞定,一路回車安裝即可。裝好系統後在自己的“~”根目錄下建一個“Workspace”存放代碼文件,製作一個軟鏈接把混合硬盤作爲數據盤引入即可,未來還可以把 Keras、NLTK 等框架的數據集文件夾也以軟鏈接的方式保存在數據盤裏。

Ubuntu 會自動進行更新,這個很重要,很多框架和庫的 Bug 在這個過程中被修復,需要注意的是在這個過程中出現長時間無響應或網絡問題的情況,可以考慮用阿里雲的源來進行加速,然後在命令行手動執行更新。

Python環境

教程

Python教程:https://docs.python.org/zh-cn/3.8/tutorial/index.html

安裝包:

MacOS:https://www.python.org/ftp/python/3.7.7/python-3.7.7-macosx10.9.pkg

Windows:https://www.python.org/ftp/python/3.7.7/python-3.7.7-embed-amd64.zip

安裝模塊:

https://docs.python.org/zh-cn/3.8/installing/index.html

Node環境

教程

Node教程:https://nodejs.org/zh-cn/

安裝包:

MacOS:https://nodejs.org/dist/v12.16.2/node-v12.16.2.pkg

Windows:

64-bit

(32-bit)[https://nodejs.org/dist/v12.16.2/node-v12.16.2-x86.msi]

Linux:64-bit

下載頁面:https://nodejs.org/zh-cn/download/

安裝模塊:

模塊網站:https://www.npmjs.com/

安裝方法:

$ npm install -g @pipcook/pipcook-cli

確保你的 Python 版本爲 > 3.6 ,你的 Node.js 版本爲 > 12.x 的最新穩定版本,執行上面的安裝命令,就可以在電腦裏擁有 Pipcook 的完整開發環境了。

快速實驗

啓動可視化實驗環境:Pipboard

啓動

命令行:

$ mkdir pipcook-example && cd pipcook-example
$ pipcook init
$ pipcook board

輸出:

> @pipcook/[email protected] dev /Users/zhenyankun.zyk/work/node/pipcook/example/.server
> egg-bin dev
[egg-ts-helper] create typings/app/controller/index.d.ts (2ms)
[egg-ts-helper] create typings/config/index.d.ts (9ms)
[egg-ts-helper] create typings/config/plugin.d.ts (2ms)
[egg-ts-helper] create typings/app/service/index.d.ts (1ms)
[egg-ts-helper] create typings/app/index.d.ts (1ms)
2020-04-16 11:52:22,053 INFO 26016 [master] node version v12.16.2
2020-04-16 11:52:22,054 INFO 26016 [master] egg version 2.26.0
2020-04-16 11:52:22,839 INFO 26016 [master] agent_worker#1:26018 started (782ms)
2020-04-16 11:52:24,262 INFO 26016 [master] egg started on http://127.0.0.1:7001 (2208ms)

在瀏覽器內選擇實驗:

想進行手寫數字識別實驗,選擇 MNIST Handwritten Digit Recognition (手寫數字識別)點擊 Try Here 按鈕。想進行圖像分類實驗,選擇 Image Classifiaction for Front-end Assets。

體驗機器學習的魅力:手寫數字識別

從瀏覽器內進入手寫數字識別的實驗:

按照:
1、鼠繪;
2、點擊預測按鈕“Predict”;
3、查看預測結果“7” 的順序進行實驗,就能看到模型預測出手寫的圖像是數字 “7” 。

體驗機器學習的魅力:圖像分類

從瀏覽器進入圖像分類的實驗:

選擇一張圖片後可以看到:

提示正在進行預測,這個過程會加載模型並進行圖像分類的預測,當選擇 “依更美” 的商標圖片並等待一小會兒後,在 Result 區域可以看到 Json 結構的預測結果:

模型可以識別出這個圖像是 “brandLogo” (品牌logo)。

實踐方法

偷天換日:改造現有的工程。就像學畫畫、學書法……,從臨摹開始可以極大平滑學習曲線。因此,先從改造一個現有的 Pipcook mnist pipline 開始,藉助這個過程來實現一個自己的控件識別模型。完成後續的教程後,你就擁有了一個可以從圖片中識別出 “button” 的模型。

問題定義

如果你之前看過我的一些文章,基本可以瞭解 imgcook.com 的原理:通過機器視覺對設計稿進行前端代碼重構。這裏定義的問題就是:用機器視覺對設計稿進行代碼重構。但是這個問題太大,作爲實戰入門可以簡化一下:用機器視覺對控件進行識別。

爲了讓模型可以進行控件識別,首先要定義什麼是控件:在計算機編程當中,控件(或部件,widget或control)是一種圖形用戶界面元素,其顯示的信息排列可由用戶改變,例如視窗或文本框。控件定義的特點是爲給定數據的直接操作(direct manipulation)提供單獨的互動點。控件是一種基本的可視構件塊,包含在應用程序中,控制着該程序處理的所有數據以及關於這些數據的交互操作。(引用自維基百科)

問題分析

根據問題定義,控件屬於:圖形用戶界面,層級:元素,邊界:提供單獨的互動點。因此,在圖形界面中找到的,提供單獨的互動點的元素,就是控件。對於機器視覺的模型來說,“在圖形界面中找到元素”類似於“在圖像中找到元素”的任務,“在圖像中找到元素”的任務可以用:目標檢測模型來完成。

“Segmenting Nuclei in Microscopy Images”

這裏推薦使用 MaskRCNN 地址在:https://github.com/matterport/Mask_RCNN ,可以看到細胞的語義化分割再對分割後的圖像進行分類,就完成了目標檢測任務。總結一下 Mask RCNN 的目標檢測過程是:使用PRN網絡產生候選區(語義化分割),再對候選區進行圖像分類(掩碼預測多任務損失)。所謂的語義化,其實就是以語義爲基礎來確定數據之間的關係。比如用機器學習摳圖,不能把人的胳膊腿、頭髮絲兒扣掉了,這裏就應用到語義化來確定人像的組成部分。

做個語義分割的機器視覺任務可能有點兒複雜,手寫數字識別這種圖像分類相對簡單。Mask RCNN 只是用 Bounding Box 把圖像切成一塊兒、一塊兒的,然後對每一塊兒圖像進行分類,如果把圖像分類做好了就等於做好了一半兒,讓我們開始吧。

數據組織

數據組織就是根據問題定義和訓練任務給模型準備“標註樣本”。之前在《前端智能化:思維轉變之路》裏介紹過,智能化開發的方法就是告訴機器正確答案(正樣本)、錯誤答案(負樣本)這種標註數據,機器通過對數據的分析理解,學習到形成答案的解題思路。因此,數據組織非常關鍵,高質量的數據才能讓機器學到正確的解題思路。

通過分析 mnist 數據集的數據組織方式,可以快速複用 mnist 的例子:

可以看到,Mnist 手寫數字識別的訓練樣本,其實就真的是手寫了一些數字,給他們打上對應的標籤(label),寫了“0”就標註“0”、寫了“1”就標註“1”……這樣,模型訓練之後就能夠知道標籤“0”對應的圖像長什麼樣?

其次,要探求一下 Pipcook 在訓練模型的時候,對數據組織的要求是怎樣的?可以在這裏看到:

https://github.com/alibaba/pipcook/blob/master/example/pipelines/mnist-image-classification.json

{
  "plugins": {
    "dataCollect": {
      "package": "@pipcook/plugins-mnist-data-collect",
      "params": {
        "trainCount": 8000,
        "testCount": 2000
      }
    },

根據線索:@pipcook/plugins-mnist-data-collect 找到:https://github.com/alibaba/pipcook/blob/master/packages/plugins/data-collect/mnist-data-collect/src/index.ts 裏:

const mnist = require('mnist');

於是,在:https://github.com/alibaba/pipcook/blob/master/packages/plugins/data-collect/mnist-data-collect/package.json 裏找到了:

"dependencies": {
    "@pipcook/pipcook-core": "^0.5.9",
    "@tensorflow/tfjs-node-gpu": "1.7.0",
    "@types/cli-progress": "^3.4.2",
    "cli-progress": "^3.6.0",
    "jimp": "^0.10.0",
    "mnist": "^1.1.0"
  },

在:https://www.npmjs.com/package/mnist 裏看到了相關的信息。

從npm包的信息來到:https://github.com/cazala/mnist 源碼站點,在README裏找到:

The goal of this library is to provide an easy-to-use way for training and testing MNIST digits for neural networks (either in the browser or node.js). It includes 10000 different samples of mnist digits. I built this in order to work out of the box with Synaptic.
You are free to create any number (from 1 to 60 000) of different examples c via MNIST Digits data loader

這裏提到:想要創建不同的樣本可以使用 MNIST Digits datta loader,點進去一探究竟:https://github.com/ApelSYN/mnist_dl 這裏有詳細的步驟:

Installation
for node.js: npm install mnist_dl
Download from LeCun’s website and unpack two files:

train-images-idx3-ubyte.gz:  training set images (9912422 bytes) 
train-labels-idx1-ubyte.gz:  training set labels (28881 bytes)

You need to place these files in the “./data” directory.

先去Clone項目:

git clone https://github.com/ApelSYN/mnist_dl.git
正克隆到 'mnist_dl'...
remote: Enumerating objects: 36, done.
remote: Total 36 (delta 0), reused 0 (delta 0), pack-reused 36
展開對象中: 100% (36/36), 完成.

對項目做一下:npm install,然後創建數據源和數據集目標目錄:

# 數據源目錄,用來下載 LeCun 大神的數據
$ mkdir data
# 數據集目錄,用來存放 mnist_dl 處理後的 Json 數據
$ mkdir digits

然後在機器學習大牛 LeCun 的網站上下載數據,保存到"./data"目錄下:
http://yann.lecun.com/exdb/mnist/

Mnist的訓練樣本圖片數據:train-images-idx3-ubyte.gz

Mnist的訓練樣本標籤數據:train-labels-idx1-ubyte.gz

然後用 mnist_dl 進行測試:

node mnist_dl.js --count 10000
DB digits Version: 2051
Total digits: 60000
x x y: 28 x 28
60000
47040000
Pass 0 items...
Pass 1000 items...
Pass 2000 items...
Pass 3000 items...
Pass 4000 items...
Pass 5000 items...
Pass 6000 items...
Pass 7000 items...
Pass 8000 items...
Pass 9000 items...
Finish processing 10000 items...
Start make "0.json with 1001 images"
Start make "1.json with 1127 images"
Start make "2.json with 991 images"
Start make "3.json with 1032 images"
Start make "4.json with 980 images"
Start make "5.json with 863 images"
Start make "6.json with 1014 images"
Start make "7.json with 1070 images"
Start make "8.json with 944 images"
Start make "9.json with 978 images"

接着 Clone mnist項目進行數據集替換測試:

$ git clone https://github.com/cazala/mnist.git
正克隆到 'mnist'...
remote: Enumerating objects: 143, done.
remote: Total 143 (delta 0), reused 0 (delta 0), pack-reused 143
接收對象中: 100% (143/143), 18.71 MiB | 902.00 KiB/s, 完成.
處理 delta 中: 100% (73/73), 完成.
$ npm install
$ cd src
$ cd digits
$ ls
0.json 1.json 2.json 3.json 4.json 5.json 6.json 7.json 8.json 9.json

下面先試試原始數據集,使用:mnist/visualizer.html 文件在瀏覽器中打開可以看到:

下面,把數據文件替換成剛纔處理的文件:

# 進入工作目錄
$ cd src
# 先備份一下
$ mv digits digits-bk
# 再拷貝之前處理的json數據
$ cp -R ../mnist_dl/digits ./
$ ls
digits    digits-bk mnist.js

強制刷新一下瀏覽器裏的:mnist/visualizer.html 文件,可以看到生成的文件完全可用,因此,一個解決方案漸漸浮現:替換原始Mnist文件裏的內容和Mnist標籤的內容來實現自己的圖片分類檢測模型。

爲了能夠替換文件:

Mnist的訓練樣本圖片數據:train-images-idx3-ubyte.gz

Mnist的訓練樣本標籤數據:train-labels-idx1-ubyte.gz

成爲我們自定義的數據集,首先需要了解這兩個文件的格式。通過文件名裏 xx-xx-idx3-ubyte 可以看出,文件是按照 idx-ubyte 的方式組織的:

在train-images.idx3-ubyte文件中,偏移量0位置32位的整數是魔數(magic number),偏移量位置4爲圖片總數(圖片樣本數量),偏移量位置8、12爲圖片尺寸(存放圖片像素信息的高、寬),偏移量位置16之後的都是像素信息(存放圖片像素值,值域爲0~255)。經過分析後,只需要依次獲取魔數和圖片的個數,然後獲取圖片的高和寬,最後逐個像素讀取就可以了。因此,在 MNIST_DL 項目的 lib 文件夾中的 digitsLoader.js 內容:

stream.on('readable', function () {
        let buf = stream.read();
        if (buf) {
            if (ver != 2051) {
                ver = buf.readInt32BE(0);
                console.log(`DB digits Version: ${ver}`);
                digitCount = buf.readInt32BE(4);
                console.log(`Total digits: ${digitCount}`);
                x = buf.readInt32BE(8);
                y = buf.readInt32BE(12);
                console.log(`x x y: ${x} x ${y}`);
                start = 16;
            }
            for (let i = start; i< buf.length; i++) {
                digits.push(buf.readUInt8(i));
            }
            start = 0;
        }
    });

就非常容易理解了,需要做的就是把圖片按照這個過程進行 “逆運算” ,反向把準備好的圖片樣本組織成這個格式即可。知道如何組織數據,那麼如何生產樣本呢?

樣本製造

在問題分析裏,我們瞭解到 “圖像分類” 是做好控件識別的基礎,就像手寫的數字 “0” 的圖像被標記上數字 “0” 一樣,我們也要對控件進行樣本標註。因爲樣本標註是一個繁瑣冗長的工作,所以機器學習的興起催生了一個全新的職業:樣本標註工程師。樣本標註工程師人工對圖片打標籤:

標註之後的樣本就可以組織成數據集(Dataset)給模型進行訓練,因此,良好的標註質量(準確傳遞信息給模型)和豐富(從不同視角和不同條件下描述信息)的數據集是優質模型的基礎。後續會介紹 pipcook 裏的樣本製造機,我們會很快開源這部分內容,現在,先把樣本製造過程分享一下。

Web 控件以 HTML 標籤的形式書寫,然後 HTML 頁面被瀏覽器渲染成圖像,可以利用這個過程和前端流行的 Puppeteer 工具,完成樣本的自動化生成。爲了方便,這裏用 bootstrap 寫一個簡單的Demo:

<link rel="stylesheet" href="t1.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<div align="middle">
<p>
    <button class="btn btn-primary">Primary</button>
</p>
<p>
    <button class="btn btn-info">Info</button>
</p>
<p>
    <button class="btn btn-success">Success</button>
</p>
<p>
    <button class="btn btn-warning">Warning</button>
</p>
<p>
    <button class="btn btn-danger">Danger</button>
</p>
<p>
    <button class="btn btn-lg btn-primary" type="button">Large button</button>
</p>
<p>
    <button class="btn btn-primary" type="button">Default button</button>
</p>
<p>
    <button class="btn btn-sm btn-primary" type="button">Mini button</button>
</p>
<p>
    <a href="#" class="btn btn-xs btn-primary disabled">Primary link disabled state</a>
</p>
<p>
    <button class="btn btn-lg btn-block btn-primary" type="button">Block level button</button>
</p>
<p>
    <button type="button" class="btn btn-primary">Primary</button>
</p>
<p>
    <button type="button" class="btn btn-secondary">Secondary</button>
</p>
<p>
    <button type="button" class="btn btn-success">Success</button>
</p>
<p>
    <button type="button" class="btn btn-danger">Danger</button>
</p>
<p>
    <button type="button" class="btn btn-warning">Warning</button>
</p>
<p>
    <button type="button" class="btn btn-info">Info</button>
</p>
<p>
    <button type="button" class="btn btn-light">Light</button>
</p>
<p>
    <button type="button" class="btn btn-dark">Dark</button>
</p>
</div>

在瀏覽器打開 HTML 用調試工具模擬Mobile iPhoneX顯示:

可以從:https://startbootstrap.com/themes/ 裏找到很多 Themes,用這些不同的主題來使我們的樣本具備 “多樣性”,讓模型更加容易從圖像中找到 “Button” 的特徵。
這樣手工截圖效率太差還不精準,下面就輪到 Puppeteer 工具出場了。首先是初始化一個 node.js 項目並安裝:

$ mkdir pupp && cd pupp
$ npm init --yes
$ npm i puppeteer --save
# or "yarn add puppeteer"

爲了能夠處理圖像,需要安裝 https://www.npmjs.com/package/gmhttp://www.graphicsmagick.org/ 有GM的安裝方法。

$ brew install graphicsmagick
$ npm i gm --save

安裝完成後打開 IDE 添加一個 shortcut.js 文件(依舊會在文末附上全部源碼):

const puppeteer = require("puppeteer");
const fs = require("fs");
const Q = require("Q");
function delay(ms) {
  var deferred = Q.defer();
  setTimeout(deferred.resolve, ms);
  return deferred.promise;
}
const urls = [
  "file:///Users/zhenyankun.zyk/work/node/pipcook/pupp/htmlData/page1.html",
  "file:///Users/zhenyankun.zyk/work/node/pipcook/pupp/htmlData/page2.html",
  "file:///Users/zhenyankun.zyk/work/node/pipcook/pupp/htmlData/page3.html",
  "file:///Users/zhenyankun.zyk/work/node/pipcook/pupp/htmlData/page4.html",
  "file:///Users/zhenyankun.zyk/work/node/pipcook/pupp/htmlData/page5.html",
];
(async () => {
  // Launch a headful browser so that we can see the page navigating.
  const browser = await puppeteer.launch({
    headless: true,
    args: ["--no-sandbox", "--disable-gpu"],
  });
  const page = await browser.newPage();
  await page.setViewport({
    width: 375,
    height: 812,
    isMobile: true,
  }); //Custom Width
  //start shortcut every page
  let counter = 0;
  for (url of urls) {
    await page.goto(url, {
      timeout: 0,
      waitUntil: "networkidle0",
    });
    await delay(100);
    let btnElements = await page.$$("button");
    for (btn of btnElements) {
      const btnData = await btn.screenshot({
        encoding: "binary",
        type: "jpeg",
        quality: 90,
      });
      let fn = "data/btn" + counter + ".jpg";
      Q.nfcall(fs.writeFileSync, fn, btnData);
      counter++;
    }
  }
  await page.close();
  await browser.close();
})();

通過上述腳本,可以循環把五種Themes的Button都渲染出來,並利用Puppeteer截圖每個Button:

生成的圖片很少,只有80多張,這裏就輪到之前安裝的GM: https://www.npmjs.com/package/gm 出場了:

用GM庫把圖片進行處理,讓他和Mnist的手寫數字圖片一致,然後,通過對圖片上添加一些隨機文字,讓模型忽略這些文字的特徵。這裏的原理就是“打破規律”,模型記住Button特徵的方式和人識別事物的方式非常相似。人在識別事物的時候,會記住那些重複的部分用於分辨。比如我想記住一個人,需要記住這個人不變的特徵,例如:眼睛大小、瞳孔顏色、眉距、臉寬、顴骨……,而不會去記住他穿什麼衣服、什麼鞋子,因爲,如果分辨一個人是依靠衣服鞋子,換個衣服鞋子就認不出來了,無異於:刻舟求劍。

下面,看一下具體處理圖片的代碼,請注意,這裏並沒有增強,真正使用的時候需要“舉一反三”,用一張圖片生成更多圖片,這就是“數據增強”的方法:

const gm = require("gm");
const fs = require("fs");
const path = require("path");
const basePath = "./data/";
const chars = [
  "0",
  "1",
  "2",
  "3",
  "4",
  "5",
  "6",
  "7",
  "8",
  "9",
  "A",
  "B",
  "C",
  "D",
  "E",
  "F",
  "G",
  "H",
  "I",
  "J",
  "K",
  "L",
  "M",
  "N",
  "O",
  "P",
  "Q",
  "R",
  "S",
  "T",
  "U",
  "V",
  "W",
  "X",
  "Y",
  "Z",
];
let randomRange = (min, max) => {
  return Math.random() * (max - min) + min;
};
let randomChars = (rangeNum) => {
  let tmpChars = "";
  for (let i = 0; i < rangeNum; i++) {
    tmpChars += chars[Math.ceil(Math.random() * 35)];
  }
  return tmpChars;
};
//獲取此文件夾下所有的文件(數組)
const files = fs.readdirSync(basePath);
for (let file of files) {
  let filePath = path.join(basePath, file);
  gm(filePath)
    .quality(100)
    .gravity("Center")
    .drawText(randomRange(-5, 5), 0, randomChars(5))
    .channel("Gray")
    // .monochrome()
    .resize(28)
    .extent(28, 28)
    .write(filePath, function (err) {
      if (!err) console.log("At " + filePath + " done! ");
      else console.log(err);
    });
}

我們對以下代碼稍加修改就可以達到增強的效果:

for (let file of files) {
  for (let i = 0; i < 3; i++) {
    let rawfilePath = path.join(basePath, file);
    let newfilePath = path.join(basePath, i + file);
    gm(rawfilePath)
      .quality(100)
      .gravity("Center")
      .drawText(randomRange(-5, 5), 0, randomChars(5))
      .channel("Gray")
      // .monochrome()
      .resize(28)
      .extent(28, 28)
      .write(newfilePath, function (err) {
        if (!err) console.log("At " + newfilePath + " done! ");
        else console.log(err);
      });
  }
}

這樣就把圖片數量增強擴展到三倍了。

完成了數據增強,下一步將圖片組織乘 idx-ubyte 文件,保證 mnist-ld 能夠正常處理。爲了組織 idx-ubyte 文件,需要對圖片進行一些特殊處理:提取像素信息、加工成類似 Mnist 數據集一樣的數據向量等工作。在 JavaScript 裏處理會比較痛苦,Python 卻很擅長處理這類問題,那麼,用 Python 的技術生態來解決問題就需要請 Boa 出場了:https://zhuanlan.zhihu.com/p/128993125 (具體可以看這裏的介紹)。

Boa 是我們爲 Pipcook 開發的底層核心功能,負責在 JavaScript 裏 Bridge Python 技術生態,整個過程幾乎是性能無損耗的:

首先是安裝:

$ npm install @pipcook/boa --save

其次是安裝 opencv-python :

 $ ./node_modules/@pipcook/boa/.miniconda/bin/pip install opencv-python pillow

最後,分享一下如何在 JavaScript 裏使用 Boa bridge Python 的能力:

const boa = require("@pipcook/boa");
// 引入一些 python 語言內置的數據結構
const { int, tuple, list } = boa.builtins();
// 引入 OpenCV
const cv2 = boa.import("cv2");
const np = boa.import("numpy");
const Image = boa.import("PIL.Image");
const ImageFont = boa.import("PIL.ImageFont");
const ImageDraw = boa.import("PIL.ImageDraw");
let img = np.zeros(tuple([28, 28, 3]), np.uint8);
img = Image.fromarray(img);
let draw = ImageDraw.Draw(img);
draw.text(list([0, 0]), "Shadow");
img.save("./test.tiff");

來對比一下 Python 的代碼:

import numpy as np
import cv2
from PIL import ImageFont, ImageDraw, Image
img = np.zeros((150,150,3),np.uint8)
img = Image.fromarray(img)
draw = ImageDraw.Draw(img)
draw.text((0,0),"Shadow")
img.save()

可以看到 Python 的代碼和 JavaScript 代碼的差異點主要是:

1、引入包的方式:

Python:import cv2

JavaScript:const cv2 = boa.import(“cv2”);

Python:from PIL import ImageFont, ImageDraw, Image

JavaScript:

const Image = boa.import("PIL.Image");
const ImageFont = boa.import("PIL.ImageFont");
const ImageDraw = boa.import("PIL.ImageDraw");

2、使用 Tuple 等數據結構:

Python:(150,150,3)

JavaScript:tuple([28, 28, 3])

可以看到,從 github.com 開源機器學習項目,移植到 Pipcook 和 Boa 是一件非常簡單的事兒,只要掌握上述兩個方法即可。

課後習題:

#/usr/bin/env python2.7
#coding:utf-8
import os
import cv2
import numpy
import sys
import struct
DEFAULT_WIDTH = 28
DEFAULT_HEIGHT = 28
DEFAULT_IMAGE_MAGIC = 2051
DEFAULT_LBAEL_MAGIC = 2049
IMAGE_BASE_OFFSET = 16
LABEL_BASE_OFFSET = 8
def usage_generate():
    print "python mnist_helper generate path_to_image_dir"
    print "\t path_to_image_dir/subdir, subdir is the label"
    print ""
    pass
def create_image_file(image_file):
    fd = open(image_file, 'w+b')
    buf = struct.pack(">IIII", DEFAULT_IMAGE_MAGIC, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT)
    fd.write(buf)
    fd.close()
    pass
def create_label_file(label_file):
    fd = open(label_file, 'w+b')
    buf = struct.pack(">II", DEFAULT_LBAEL_MAGIC, 0)
    fd.write(buf)
    fd.close()
    pass
def update_file(image_file, label_file, image_list, label_list):
    ifd = open(image_file, 'r+')
    ifd.seek(0)
    image_magic, image_count, rows, cols = struct.unpack(">IIII", ifd.read(IMAGE_BASE_OFFSET))
    image_len = rows * cols
    image_offset = image_count * rows * cols + IMAGE_BASE_OFFSET
    ifd.seek(image_offset)
    for image in image_list:
        ifd.write(image.astype('uint8').reshape(image_len).tostring())
    image_count += len(image_list)
    ifd.seek(0, 0)
    buf = struct.pack(">II", image_magic, image_count)
    ifd.write(buf)
    ifd.close()
    lfd = open(label_file, 'r+')
    lfd.seek(0)
    label_magic, label_count = struct.unpack(">II", lfd.read(LABEL_BASE_OFFSET))
    buf = ''.join(label_list)
    label_offset = label_count + LABEL_BASE_OFFSET
    lfd.seek(label_offset)
    lfd.write(buf)
    lfd.seek(0)
    label_count += len(label_list)
    buf = struct.pack(">II", label_magic, label_count)
    lfd.write(buf)
    lfd.close()
def mnist_generate(image_dir):
    if not os.path.isdir(image_dir):
        raise Exception("{0} is not exists!".format(image_dir))
    image_file = os.path.join(image_dir, "user-images-ubyte")
    label_file = os.path.join(image_dir, "user-labels-ubyte")
    create_image_file(image_file)
    create_label_file(label_file)
    for i in range(10):
        path = os.path.join(image_dir, "{0}".format(i))
        if not os.path.isdir(path):
            continue
        image_list = []
        label_list = []
        for f in os.listdir(path):
            fn = os.path.join(path, f)
            image = cv2.imread(fn, 0)
            w, h = image.shape
            if w and h and (w <> 28) or (h <> 28):
                simg = cv2.resize(image, (28, 28))
                image_list.append(simg)
                label_list.append(chr(i))
        update_file(image_file, label_file, image_list, label_list)
    print "user data generate successfully"
    print "output files: \n\t {0}\n\t {1}".format(image_file, label_file)
    pass

上面是用 python 寫的一個工具,可以組裝 idx 格式的 mnist 數據集,用之前的 mnist-ld 進行處理,就可以替換成我們生成的數據集了。

樣本增強

使用樣本平臺更方便:

`

特徵工程

特徵分析和處理可以幫助我們更好的優化數據集,爲了得到圖像的特徵,可以採用 Keypoint、SIFT 等特徵來表徵圖像,這種高階的特徵具有各自的優勢,例如 SIFT 可以克服旋轉、Keypoint 可以克服形變……等等。

模型訓練

Pipline配置:

{
  "plugins": {
    "dataCollect": {
      "package": "@pipcook/plugins-mnist-data-collect",
      "params": {
        "trainCount": 8000,
        "testCount": 2000
      }
    },
    "dataAccess": {
      "package": "@pipcook/plugins-pascalvoc-data-access"
    },
    "dataProcess": {
      "package": "@pipcook/plugins-image-data-process",
      "params": {
        "resize": [28,28]
      }
    },
    "modelDefine": {
      "package": "@pipcook/plugins-tfjs-simplecnn-model-define"
    },
    "modelTrain": {
      "package": "@pipcook/plugins-image-classification-tfjs-model-train",
      "params": {
        "epochs": 15
      }
    },
    "modelEvaluate": {
      "package": "@pipcook/plugins-image-classification-tfjs-model-evaluate"
    }
  }
}

模型訓練:

$ pipcook run examples/pipelines/mnist-image-classification.json

模型預測:

$ pipcook board

原理解析

回顧整個工程改造的過程:理解 Pipline 的任務、理解 Pipline 工作原理、瞭解數據集格式、準備訓練數據、重新訓練模型、模型預測,下面分別介紹這些關鍵步驟:

理解 Pipline 的任務

對於 Pipcook 內置的 Example ,分爲三類:機器視覺、自然語言處理、強化學習。機器視覺和自然語言處理,代表“看見”和“理解”,強化學習代表決策和生成,這些內容可類比於一個程序員,從看到、理解、編寫代碼的過程。在不同的編程任務中組合使用不同的能力,這就是 Pipline 的使命。

對於 mnist 手寫數字識別這種簡單的任務,只需要使用部分機器視覺的能力即可,對於 imgcook.com 這種複雜的應用場景,就會涉及很多複雜的能力。針對不同的任務,通過 Pipline 管理機器學習能力使用的方式,就可以把不同的機器學習能力組合起來。

最後,需要理解 “機器學習應用工程” 和 “機器學習算法工程” 的區別。機器學習算法工程中,主要是算法工程師在設計、調整、訓練模型。機器學習應用工程中,主要是選擇、訓練模型。前者是爲了創造、改造模型,後者是爲了應用模型和算法能力。未來,在研讀機器學習資料和教材時,可針對上述原則側重於模型思想和模型應用,不要被書裏的公式嚇到,那些公式只是用數學方法描述模型思想而已。

理解 Pipline 工作原理

這就是Pipline的工作原理,主要由圖中 7 類插件構成了整個算法工程鏈路。由於引入了 plugin 的開放模式,對於自己的前端工程,可以在遇到問題的時候,自己開發 plugin 來完成工程接入。Plugin 開發文檔在:https://alibaba.github.io/pipcook/#/tutorials/how-to-develop-a-plugin

瞭解數據集格式

瞭解數據集格式是爲了讓 Pipline 跑起來,更具體一點兒是讓模型可以識別並使用數據。不同的任務對應不同類型的模型,不同的模型對應不同類型的數據集,這種對應關係保證了模型能夠正確被訓練。在 Pipcook 裏定義的數據集格式也針對了不同的任務和模型,對於機器視覺的數據集是 VOC,對於 NLP Pipcook 定義的數據集是 CSV 。具體的數據集格式,可以按照文檔:https://alibaba.github.io/pipcook/#/spec/dataset 的說明來分析和理解。也可以採用本文介紹的方法,從相關處理程序和代碼裏進行分析。

準備訓練數據

數據爲什麼是最重要的部分?因爲數據的準確性、分佈合理性、數據對特徵描述的充分性……直接決定了最終的模型效果。爲了準備高質量的數據,還需要掌握 Puppeteer 等工具和爬蟲……等。還可以在傳統機器學習理論和工具基礎上,藉助 PCA 算法等方式評估數據質量。還可以用數據可視化工具,來直觀的感受數據分佈情況:

具體可以查看:https://www.yuque.com/zhenzishadow/tx7xtl/xhol3k 我的這篇文章。

訓練和預測、部署

訓練模型沒有太多可說的,因爲今天的模型超參數並不想以前那麼敏感,調參不如調數據。那麼,參數在訓練的時候還有什麼意義呢? 遷就GPU和顯存大小。因爲訓練的時候,除了模型的複雜度外,超參數適當的調小雖然會犧牲訓練速度(也可能影響模型準確率),但起碼可以保證模型能夠被訓練。因此,在 Pipcook 的模型配置中,一旦發現顯卡OOM了,可以通過調整超參數來解決。

預測的時候唯一需要注意的是:輸入模型訓練的數據格式和輸入模型預測的格式必須一致。

部署的時候需要注意的是對容器的選擇,如果只是簡單的模型,其實 CPU 容器足夠用了,畢竟預測不像訓練那樣消耗算力。如果部署的模型很複雜,預測時間很長無法接受,則可以考慮 GPU 或 異構運算容器。GPU 容器比較通用的是 NVIDIA 的 CUDA 容器,可以參考:https://github.com/NVIDIA/nvidia-docker。如果要使用異構運算容器,比如阿里雲提供的賽靈思容器等,可以參考阿里雲相關的文檔。

寫在最後

這篇文章斷斷續續寫了很久,主要還是平時比較忙,後續會努力帶來更多文章,分享更多自己在實踐中的一些方法和思考。下一篇會系統完整的介紹一下 NLP 自然語言處理的方法,也會按照:快速實驗、實踐方法、原理解析這種模式來做,敬請期待。

原文鏈接:https://juejin.im/post/5e9667f6f265da47e1594952

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章