作者 | 甄子
用我们的 前端智能化框架 内置实验,可以方便的进行手写数字识别和图像分类任务,这里按照环境准备、快速实验、实战方法、原理解析的顺序,分四个部分进行介绍。完成本教程,你可以开始进行自己的前端智能化项目,用机器学习解决编程过程中遇到的问题。长文预警,请耐心阅读。
环境准备
开始之前
台式机和笔记本
首先,初学者我更推荐笔记本,因为其便携性和初期实验的运算量并不是很大,可以保证在咖啡馆或户外立即开始学习和实践。其次,现在的轻薄笔记本如小米的 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(https://nodejs.org/dist/v12.16.2/node-v12.16.2-x64.msi)
32-bit(https://nodejs.org/dist/v12.16.2/node-v12.16.2-x86.msi)
Linux:64-bit(https://nodejs.org/dist/v12.16.2/node-v12.16.2-linux-x64.tar.xz)
下载页面:https://nodejs.org/zh-cn/download/
安装模块:
安装方法:
$ 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/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');
"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 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 自然语言处理的方法,也会按照:快速实验、实践方法、原理解析这种模式来做,敬请期待。
关注「Alibaba F2E」
把握阿里巴巴前端新动向