Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
我們的前端智能化框架: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教程: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教程:https://nodejs.org/zh-cn/
MacOS:https://nodejs.org/dist/v12.16.2/node-v12.16.2.pkg
Windows:
https://nodejs.org/dist/v12.16.2/node-v12.16.2-x64.msi
https://nodejs.org/dist/v12.16.2/node-v12.16.2-x86.msi
Linux:https://nodejs.org/dist/v12.16.2/node-v12.16.2-linux-x64.tar.xz
下載頁面:https://nodejs.org/zh-cn/download/
模塊網站:https://www.npmjs.com/
安裝方法:
$ npm install -g @pipcook/pipcook-cli
確保你的 Python 版本為 > 3.6 ,你的 Node.js 版本為 > 12.x 的最新穩定版本,執行上面的安裝命令,就可以在電腦里擁有 Pipcook 的完整開發環境了。
命令行:
$ mkdir pipcook-example && cd pipcook-example
$ pipcook init
$ pipcook board
輸出:
> @pipcook/pipcook-board-server@1.0.0 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/gm 在 http://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
最后,分享一下如何在 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 工作原理、了解數據集格式、準備訓練數據、重新訓練模型、模型預測,下面分別介紹這些關鍵步驟:
對于 Pipcook 內置的 Example ,分為三類:機器視覺、自然語言處理、強化學習。機器視覺和自然語言處理,代表“看見”和“理解”,強化學習代表決策和生成,這些內容可類比于一個程序員,從看到、理解、編寫代碼的過程。在不同的編程任務中組合使用不同的能力,這就是 Pipline 的使命。
對于 mnist 手寫數字識別這種簡單的任務,只需要使用部分機器視覺的能力即可,對于 imgcook.com 這種復雜的應用場景,就會涉及很多復雜的能力。針對不同的任務,通過 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 自然語言處理的方法,也會按照:快速實驗、實踐方法、原理解析這種模式來做,敬請期待。
整代碼,忽略樣式:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script> </head> <body> <div id="app"> <free-tree @select="onselect" :alldata="mydata" class="trees"></free-tree> </div> <script> var freeTreeList={ template: ` <li> <span @click="toggle(model.id)"> <i v-if="isFolder" class="icon" :class="[open ? 'folder-open': 'folder']"></i> <i v-if="!isFolder" class="icon file-text"></i> {{ model.menuName }} </span> <ul style="padding-left: 16px" :class="{slideDown:!open,slideUp:open}" v-show="open" v-if="isFolder"> <free-tree-list @select="onselect" v-for="item in model.children" :model="item" :key="item.id"></free-tree-list> </ul> </li>`, name: 'free-tree-list', // 遞歸組件,name屬性不能少 props: ['model'], data() { return { open: false } }, computed: { isFolder: function() { return this.model.children && this.model.children.length } }, methods: { toggle: function(id) { this.$emit('select', id) if (this.isFolder) { this.open=!this.open } }, onselect(id) { this.$emit('select', id); } } }; var freeTree={ props: { alldata: { type: Array, // default: [] } }, data() { return { } }, template: ` <div class="tree-menu"> <ul v-for="menuItem in alldata" :key="menuItem.id"> <free-tree-list @select="onselect" :model="menuItem"></free-tree-list> </ul> </div>`, components: { freeTreeList }, methods: { onselect(id) { this.$emit('select', id); } } }; var mydata=[{ 'id': '1', 'menuName': '一級-01', 'menuCode': '10', 'children': [{ 'menuName': '二級-01', 'menuCode': '11', 'id': '11' }, { 'menuName': '二級-02', 'menuCode': '12', 'id': '12', 'children': [{ 'menuName': '三級-02', 'menuCode': '121', 'id': '121' }, { 'menuName': '三級-03', 'menuCode': '122', 'id': '122' } ] }, { 'menuName': '二級-03', 'menuCode': '13', 'id': '13' } ] }, { 'id': '2', 'menuName': '一級-02', 'menuCode': '' }, { 'id': '3', 'menuName': '一級-03', 'menuCode': '30', 'children': [{ 'menuName': '二級-01', 'menuCode': '31', 'id': '31' }, { 'menuName': '二級-02', 'menuCode': '32', 'id': '32', 'children': [] } ] }, { 'id': '4', 'menuName': '一級-04', 'menuCode': '', 'children': [] } ]; new Vue({ el: '#app', data: { mydata: mydata }, methods: { onselect(id) { console.log(id); } }, components: { freeTree } }) </script> </body> </html>
點擊每一項,如果有數據的,則展開。
一級樹:
二級樹:
三級樹:
于項目的需要,需要在項目的WinForm系統的一個模塊中集成手寫簽名的功能,一開始對這塊不是很了解,只是了解他能夠替代鼠標進行簽名。既然是簽名,一般就是需要記錄手稿圖片,作為一個記錄核實的憑證,因為有效的簽名是很難模擬的。市場上也存在很多類型的電子簽名筆,一時間還真不知道那種適合。
WinForm應用的界面開發有個很好用的界面控件推薦——DevExpress WinForm,擁有180+組件和UI庫,能為Windows Forms平臺創建具有影響力的業務解決方案,點擊下方按鈕鏈接可直接獲取產品體驗!
DevExpress WinForms Subscription官方最新版免費下載試用,歷史版本下載,在線文檔和幫助文件下載-慧都網
電子手寫簽名,其實就是模擬真實的筆進行簽名的過程,我這里主要是介紹使用外部設備來記錄手稿圖片的需求,這樣其實就是類似于把我們真實在紙張上的簽名內容,放到了電腦記錄面板上進行操作了,而這個操作模擬,其實就是利用了類似鼠標功能的接觸筆來實現的。
有些筆是在紙上或者電腦屏幕上進行模擬簽名,通過一個接收器方式接受筆的接觸信號,一般要先設定紙張或者屏幕的范圍,然后進行簽名書寫。
由于簽名筆淘寶上也有很多,開始淘到的就是類似這種,不過效果不理想,好像總是定位不準,而且和鼠標發生嚴重沖突,基本上操作不了,商家客服說很少有這種現象發生,但是卻發生在我身上,于是只有退貨。然后淘到的是一款漢王手寫板筆,開始用的還可以,其實就是代替了鼠標進行操作,試過可以后,就擱置起來。
一直用鼠標模擬簽名進行開發,寫該篇隨筆的時候,本來想用來展示下效果,可惜又用不了,不知道什么原因。
做簽名功能開發的時候,其實我是不關注手寫筆功能的,因為我想其實如果鼠標能操作就可以,手寫筆其實也就應該可以操作。因此只需要在輸入的地方記錄鼠標操作的痕跡,類似手寫簽名的效果即可,大概如下所示。
從上圖可以看到,只需要提供一個類似繪圖的面板即可記錄鼠標的軌跡,也就是功能有點類似Windows自帶的白板(或者繪圖板)軟件即可。
其實要模擬鼠標簽名的效果,只需要利用功能強大的GraphicsPath對象就差不多了,剩下的就是記錄點和繪制點,設置繪圖筆的寬度和顏色等方面。下面我們看看具體的實現代碼吧。
首先要申明幾個必要的對象,來承載相關的信息。
//記錄直線或者曲線的對象
private System.Drawing.Drawing2D.GraphicsPath mousePath=new System.Drawing.Drawing2D.GraphicsPath();
//畫筆透明度
private int myAlpha=100;
//畫筆顏色對象
private Color myUserColor=new Color();
//畫筆寬度
private int myPenWidth=3;
//簽名的圖片對象
public Bitmap SavedBitmap;
從上圖效果圖上,我們看到,我們在其中放置了一個繪圖面板,其實就是一個PictureBox對象而已,我們只需要在PictureBox對象,記錄鼠標的移動、鼠標按下,以及對象刷新操作事件即可實現模擬簽名的效果了,如下代碼所示。
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button==System.Windows.Forms.MouseButtons.Left)
{
try
{
mousePath.AddLine(e.X, e.Y, e.X, e.Y);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
pictureBox1.Invalidate();
}
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button==System.Windows.Forms.MouseButtons.Left)
{
mousePath.StartFigure();
}
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
try
{
myUserColor=System.Drawing.Color.Blue;
myAlpha=255;
Pen CurrentPen=new Pen(Color.FromArgb(myAlpha, myUserColor), myPenWidth);
e.Graphics.DrawPath(CurrentPen, mousePath);
}
catch { }
}
保存及清空操作,其實很簡單,清空的時候,記得把繪圖面板清空,并重置路徑對象即可。保存也是記錄PictureBox對象的大小寬度,把圖片存儲到圖片對象里面,供控件使用。
具體實現如下所示。
private void btnClear_Click(object sender, EventArgs e)
{
pictureBox1.CreateGraphics().Clear(Color.White);
mousePath.Reset();
}
private void btnSave_Click(object sender, EventArgs e)
{
SavedBitmap=new Bitmap(pictureBox1.Width, pictureBox1.Height);
pictureBox1.DrawToBitmap(SavedBitmap, new Rectangle(0, 0, pictureBox1.Width, pictureBox1.Height));
this.DialogResult=System.Windows.Forms.DialogResult.OK;
}
上面實現還不能完成一個最終的簽名效果,有時候,我們需要把這些圖片放到數據庫里面,如果是把上面的操作的圖片記錄下來,發現很大,一般我們簽名的效果顯示,不會很大,一方面只需要辨認其筆跡即可,另外一方面也不會過于增大數據庫的存儲空間。那么我們就需要對圖片大小進行一定的處理了。其實可能在詳細信息里面,我們就這樣記錄顯示即可。那么就一定要裁剪圖片的大小。
以上的醫生簽名,我們觸發的操作就是彈出一個簽名窗體,在其中繪制簽名,確認后返回,并把圖片進行顯示在詳細信息窗體里面。
private void btnDoctorSign_Click(object sender, EventArgs e)
{
FrmSignicture dlg=new FrmSignicture();
if (dlg.ShowDialog()==System.Windows.Forms.DialogResult.OK)
{
this.picDoctor.Image=dlg.SavedBitmap;
}
}
為了安裝最終顯示的PictureBox的大小保存圖片,我們需要裁減,裁減就是重新根據圖片大小進行保存Bitmap對象,這種可以從全屏最初的幾M縮小到十幾K,方便存儲。
至于圖片對象存儲到數據庫,這個由于利用了我Winform開發框架里面的數據庫基礎對象,基本上不需要特別對待及處理,只需要把圖片字段的Byte數組獲得即可了。這里就不在贅述Winform開發框架的功能及相關的基類處理了。
public Bitmap SaveImage(PictureBox pictureBox1)
{
Bitmap SavedBitmap=new Bitmap(pictureBox1.Width, pictureBox1.Height);
pictureBox1.DrawToBitmap(SavedBitmap, new Rectangle(0, 0, pictureBox1.Width, pictureBox1.Height));
return SavedBitmap;
}
最后,有時候,簽名還需要在列表里面顯示,這樣方便對一些關鍵信息進行查看核對。如下圖所示:
那么對于列表中顯示圖片,我們在DevExpress界面的分頁控件中應該如何處理呢,這估計也是DevExpress開發中很多常見問題之一?
其實也很簡單,就是在DataSourceChanged 變化的事件中改變單元格的對象屬性即可。
this.winGridViewPager1.OnRefresh +=new EventHandler(winGridViewPager1_OnRefresh);
this.winGridViewPager1.ShowLineNumber=true;
this.winGridViewPager1.gridView1.Appearance.Row.TextOptions.HAlignment=DevExpress.Utils.HorzAlignment.Center;
this.winGridViewPager1.gridView1.Appearance.HeaderPanel.TextOptions.HAlignment=DevExpress.Utils.HorzAlignment.Center;
this.winGridViewPager1.AppendedMenu=this.contextMenuStrip1;
this.winGridViewPager1.gridView1.DataSourceChanged +=new EventHandler(gridView1_DataSourceChanged);
this.winGridViewPager1.gridView1.RowHeight=44;
void gridView1_DataSourceChanged(object sender, EventArgs e)
{
RepositoryItemPictureEdit pic1=new RepositoryItemPictureEdit();
pic1.SizeMode=DevExpress.XtraEditors.Controls.PictureSizeMode.Zoom;
pic1.NullText=" ";
pic1.CustomHeight=44;
pic1.BestFitWidth=100;
this.winGridViewPager1.gridView1.Columns["Signature"].ColumnEdit=picFlight;
this.winGridViewPager1.gridView1.Columns["Signature"].MaxWidth=100;
this.winGridViewPager1.gridView1.Columns["Signature"].MinWidth=100;
RepositoryItemPictureEdit picDoctor=new RepositoryItemPictureEdit();
picDoctor.SizeMode=DevExpress.XtraEditors.Controls.PictureSizeMode.Zoom;
picDoctor.NullText=" ";
picDoctor.CustomHeight=44;
picDoctor.BestFitWidth=100;
this.winGridViewPager1.gridView1.Columns["DoctorSignature"].ColumnEdit=picDoctor;
this.winGridViewPager1.gridView1.Columns["DoctorSignature"].MaxWidth=100;
this.winGridViewPager1.gridView1.Columns["DoctorSignature"].MinWidth=100;
}
以上就基本上解決了,簽名,以及圖片保存,以及列表顯示圖片效果的問題了。
本文轉載自:博客園 - 伍華聰
*請認真填寫需求信息,我們會在24小時內與您取得聯系。