本文整理自往期
MixLab无界社区
文章。
网上大量教程都是教如何训练模型,
往往我们只学会了训练模型,
而实际应用的环节是缺失的。
def AIFullstack( ):
本文从「全栈」的角度,通过训练模型、部署成后端服务、前端页面开发等内容的介绍,帮大家更快地把深度学习的模型应用到实际场景中。
用到的技术:
keras+tensorflow+flask
web开发相关
指南分为5篇。
第一篇
介绍开发环境--训练模型--保存至本地;
第二篇
介绍导入训练好的模型--识别任意的手写数字图片;
第三篇
介绍用Flask整合keras训练好的模型,并开发后端服务;
第四篇
介绍前端web单页应用的开发。
第五篇
介绍图像处理相关知识。
return学以致用
第一篇
介绍开发环境--训练模型--保存至本地
为了方便入门,下面采用docker的方式进行实验。
01/01
采用docker部署开发环境
首先安装好docker,本指南使用的是mac系统,window用户请查阅官方安装教程,运行docker,终端输入:
在本地电脑新建一个目录,我这边是kerasStudy,路径是
大家可以改成自己本机对应的路径。
终端运行:
-p 6006:6006,表示将Docker主机的6006端口与容器的6006接口绑定;
-v参数中,冒号":"前面的目录是宿主机目录,后面的目录是容器内目录。
记得,还需要在docker中配置宿主机的与镜像共享的目录地址
将新建一个容器,并在容器中开启一个交互模式的终端,结果如下:
01/02
启动jupyter notebook
终端输入:
键盘按 i ,按回车及方向键控制光标,把floydhub/dl-docker:cpu镜像默认的使用Theano作为后端,改为如下:
按下esc键;输入:wq,保存修改结果。
终端输入:
显示jupyter notebook已经运行成功,如下图:
打开浏览器,在地址栏中输入:
即可访问jupyter,如下图:
01/03
Hello Jupyter Notebook
上文提到的jupyter notebook到底是什么东西?
Jupyter Notebook 是一款集编程和写作于一体的效率工具,优点:
分享便捷
远程运行
交互式展现
在浏览器可以访问Jupyter Notebook,也就是说,我可以部署成web应用的形式,用户可以分享,通过域名访问,并且可以利用web的任何交互方式。
继续我们的教程,在浏览器打开Jupyter Notebook后,找到我们与本地共享的项目目录kerasStudy,点击进入,然后点击jupyter右上角的new,选择python2,如下图所示:
新建一个notebook。
先来做个小实验:
输入:
然后在菜单中,选择Cell--Run Cells,运行代码:
如下图所示,输出了一些结果:
第一行代码:
使得随机数据可预测。相当于给随机数赋了个id,下次调用随机数的时候,只要再次取这个id,再调用随机数,即可产生相同的随机数
可以做下这个练习:
练习1
#控制台输出结果(随机生成,每次生成的都不一样)
练习2
#控制台输出结果
练习3
#控制台输出结果
01/04
Keras训练模型
这里结合keras的官方案例,训练一个多层感知器。
步骤1
重新建一个notebook,
输入:
步骤2
步骤3
步骤4
步骤5
步骤6
步骤7
步骤8
步骤9
步骤10
步骤11
第二篇
介绍导入训练好的模型--识别任意的手写数字图片
02/01
再次进入docker容器
接着上一篇,我们继续使用上次新建好的容器,可以终端输入 :
显示如下图,找到上次run的容器:
我这边是容器名(NAMES)为suspicious_cori,启动它,可以终端输入:
然后,终端再输入:
即可在容器中开启一个交互模式的终端。
然后终端输入
新建一个notebook
02/02
加载训练好的模型
加载上一篇训练好的模型,在新建的notebook里输入:
02/03
读取需要识别的手写字图片
引入用于读取图片的库:
读取位于kerasStudy目录下的图片:
matplotlib只支持PNG图像,读取和代码处于同一目录下的 test.png ,注意,读取后的img 就已经是一个 np.array 了,并且已经归一化处理。
上文的png图片是单通道图片(灰度),如果test.png是rgb通道的图片,可以rgb2gray进行转化,代码如下:
关于图片的通道,我们可以在photoshop里直观的查看:
先查看下读取的图片数组维度:
输出是(28, 28)
转化成正确的输入格式:
打印出来看看:
输出是(1, 784)
02/04
识别的手写字图片
输入:
打印出来即可:
识别出来是6:
至此,你已经学会了从训练模型到使用模型进行识别任务的全过程啦。
有兴趣可以试着替换其他的手写字图片进行识别看看。
当然也可以写个后端服务,部署成web应用。
第三篇
介绍用Flask整合keras训练好的模型,并开发后端服务
03/01
目录结构
新建一个web全栈项目的文件夹,我在kerasStudy下建了个app的文件夹,app下的文件构成如下:
app.py是项目的主入口,主要是用flask写的一些路由;
predict.py是识别手写字的python模块;
static是放置前端页面的目录;
model存放训练好的模型;
test是一些测试图片;
tmp是前端上传到服务器的图片存放地址。
03/02
前端代码
新建一个简单的index.html文件,放置于static目录下,写一个form表单:
这里的前端代码比较简单,只是一个把手写字图片提交到服务器的表单,下一篇文章将实现一个手写字的输入工具。
03/03
后端代码
app.py里,用flask设置路由,返回静态html页面:
其余flask的相关配置代码可以参考往期文章:
这个时候,我们启动docker,把镜像启动,并进入docker镜像的终端中(查看第2篇),找到app目录,终端输入:
等终端提示相关的启动信息后,在浏览器里试下,输入:
成功打开index.html页面:
再次编辑app.py文件,写一个predict的接口,接受前端提交的图片,并返回识别结果给前端:
其中predict.img2class(imgurl)是一个python模块。
接下来,我们编写识别手写字的python模块。
03/04
编写识别手写字的python模块
在Python中,每个Python文件都可以作为一个模块,模块的名字就是文件的名字。比如有这样一个文件test.py,在test.py中定义了函数add:
那么在其他文件中就可以先import test,然后通过test.add(a,b)来调用了,当然也可以通过from test import add来引入。
回到本篇的例子,我们在第2篇中已经写过识别手写字的代码了,现在只需稍微调整下就可以形成一个python模块,供其他文件调用了。
如本篇中,在app.py中通过:
引入predict.py模块,使用的时候调用:
#predict.py文件
详情可以参考第2篇内容
这边把上次实现过的代码,书写出一个python模块,以供其他文件调用:
在docker镜像中启动伪终端,进入app目录,输入:
上传测试图片试试:
成功返回识别结果,至此,一个迷你的识别手写字web全栈应用已经完成。
第四篇
介绍前端web单页应用的开发
如果你练习里前面三篇,相信你已经熟悉了Docker和Keras,以及Flask了,接下来我们实现一个提供给用户输入手写字的前端web页面。
前端画板我们可以自己用最基本的canvas写,也可以选择封装好的开源库:
下面介绍2个比较好的模拟手写效果的画板库:
signature_pad
https://github.com/szimek/signature_pad/
drawingboard.js
https://github.com/Leimi/drawingboard.js
这边我选择的是signature_pad。
HTML代码:
mnist demo
识别结果:
清除识别
移动端注意要写这句标签,把屏幕缩放设为no,比例设为1:
CSS代码:
body { display: flex; justify-content: center; align-items: center; height:100vh; width:100%; user-select: none; margin:; padding:;}
h5 { margin:; padding:
}
#mnist-pad { position: relative; display: flex; flex-direction: column; font-size:1em; width:100%; height:100%; background-color: #fff; box-shadow:1px5px rgba(,,,0.27),40px rgba(,,,0.08) inset; padding:16px;}
.mnist-pad-body { position: relative; flex:1; border:1px solid #f4f4f4;}
.mnist-pad-body canvas { position: absolute; left:; top:; width:100%; height:100%; border-radius:4px; box-shadow:5px rgba(,,,0.02) inset;}
.mnist-pad-footer { color: #C3C3C3; font-size:1.2em; margin-top:8px; margin-bottom:8px;}
.mnist-pad-result { display: flex; justify-content: center; align-items: center; margin-bottom:8px;}
.mnist-pad-actions { display: flex; justify-content: space-between; margin-bottom:8px;}
#mnist-pad-clear { height:44px; background-color: #eeeeee; width:98px; border: none; font-size:16px; color: #4a4a4a;}
#mnist-pad-save { height:44px; background-color: #3b3b3b; width:98px; border: none; font-size:16px; color: #ffffff;}
CSS样式都是一些常用的,有兴趣可以自己实现个简单的UI。
JS代码,有3个文件:
signature_pad.js这是引用的开源库;
mnist.js这是我们给开源库写的一些扩展,下文会介绍;
app.js主要是一些初始化,事件绑定,请求后端接口的处理。
先来看看app.js:
步骤1
初始化画板,绑定按钮事件;
varclearBtn =document.getElementById("mnist-pad-clear");
varsaveBtn =document.getElementById("mnist-pad-save");
varcanvas =document.querySelector("canvas");
varmnistPad =newSignaturePad(canvas, {
backgroundColor:'transparent',
minWidth:6,
maxWidth:8
});
clearBtn.addEventListener("click",function(event) { mnistPad.clear();});
saveBtn.addEventListener("click",function(event) {
if(mnistPad.isEmpty()) { alert("请书写一个数字"); }else{ mnistPad.getMNISTGridBySize(true,28,img2text); }});
注意minWidth及MaxWidth的设置,我试验下来,比较好的数值是6跟8,识别效果较好,也可以自行试验修改。
ministPad的方法,getMNISTGridBySize将把截取画板上的手写数字,并缩放成28x28的尺寸,然后调用img2text函数。
img2text主要是把28x28的图片传给后端,获取识别结果,这边由于canvas的数据是base64,需要用到转化为blob的函数,dataURItoBlob(github上有写好的),转化后通过构造一个表单,注意文件名predictImg一定要与后端flask接受函数里的写的一致。调用XMLHttpRequest请求后端接口即可。
步骤2
这一步“如何把canvas生成的图片上传至后端”是个很典型的问题。
functionimg2text(b64img){
varformData =newFormData();
varblob = dataURItoBlob(b64img);
formData.append("predictImg", blob);
varrequest =newXMLHttpRequest();
request.onreadystatechange =function() {
if(request.readyState ==4) {
if((request.status >=200&& request.status
console.log(request.response)
document.querySelector('#mnist-pad-result').innerHTML=request.response; }; } }; request.open("POST","./predict"); request.send(formData);};
步骤3
还有一个比较重要的函数:
画板根据屏幕尺寸自适应的代码(尤其是PC端,记得加):
functionresizeCanvas() {
varratio =Math.max(window.devicePixelRatio ||1,1); canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight;
// canvas.getContext("2d").scale(ratio, ratio);mnistPad.clear();};
window.onresize = resizeCanvas;resizeCanvas();
到这一步可以试一下前端的输入效果先:
接下来完成mnist.js
步骤4
signature_pad有个方法是toData,可以获取所有手写输入的坐标点。
varps=mnistPad.toData()[];
mnistPad._ctx.strokeStyle='red';
ps.forEach((p,i)=>{ mnistPad._ctx.beginPath(); mnistPad._ctx.arc(p.x, p.y,4,,2*Math.PI); mnistPad._ctx.stroke();})
我们可以在chrome的控制台直接试验。
红色的圈圈就是所有的坐标点,只要求出如下图所示的紫色框,第一步也就完成了。
步骤5
给signature_pad扩展个getArea方法:
SignaturePad.prototype.getArea =function() {
varxs = [], ys = [];
varorign =this.toData();
for(vari =; i
varorignChild = orign[i];
for(varj =; j
varpaddingNum =30;
varmin_x =Math.min.apply(null, xs) - paddingNum;
varmin_y =Math.min.apply(null, ys) - paddingNum;
varmax_x =Math.max.apply(null, xs) + paddingNum;
varmax_y =Math.max.apply(null, ys) + paddingNum;
varwidth = max_x - min_x, height = max_y - min_y;
vargrid = { x: min_x, y: min_y, w: width, h: height };
returngrid;
};
测试下:
注意paddingNum,我设置了个30的值,把边框稍微放大了下,原因见mnist手写字训练集的图片就知道啦。
到这一步,我们的手写字数据集是下图这样的:
步骤6
我们还需要把边框变成方形。
再写个转换函数:
原理如下图,判断下长边是哪个,然后计算出x,y,width,height即可。
写好代码后,试一下:
红框是最后要提交的范围。
这个时候,还要处理下,把图片变成黑底白字的图片,因为MNIST数据集是这样的。
步骤7
主要代码如下:
ctx.fillStyle ="white";ctx.fillRect(,, grid.w, grid.h);
ctx.drawImage(img, grid.x, grid.y, grid.w, grid.h,,, size, size);
varimgData = ctx.getImageData(,, size, size);
for(vari =; i
ctx.putImageData(imgData,,);
画上背景,遍历像素,把颜色反色下就ok啦。
最后都测试下:
最后,注意下MNIST数据集里的数据,对应的是灰度图,28x28的尺寸,黑底白字,并且数字是像素的重心居中处理的。本文没有介绍如何把web前端的手写字根据重心居中处理这一内容,将会挑选合适时机介绍,用上了可以提高识别率哦!
第5篇
图像处理
再回顾下MNIST手写字数据集的特点:每个数据经过归一化处理,对应一张灰度图片,图片以像素的重心居中处理,28x28的尺寸。
上一篇中,对canvas手写对数字仅做了简单对居中处理,严格来说,应该做一个重心居中的处理。
本篇主要介绍:
如何实现前端的手写数字按重心居中处理成28x28的图片格式。
我们先把前端canvas中的手写数字处理成二值图,求重心主要运用了二值图的一阶矩,先来看下零阶矩:
二值图在某点上的灰度值只有0或者1两个值,因此零阶矩为二值图的白色面积总和。
只要把上文的公式转为JS代码,即可求出重心坐标:
SignaturePad.prototype.getGravityCenter =function() {
varw =this._ctx.canvas.width, h =this._ctx.canvas.height;
varmM =, mX =, mY =;
varimgData =this._ctx.getImageData(,, w, h);
for(vari =; i
vart = imgData.data[i +3] /255;
varpos =this.pixel2Pos(i);
mM = mM + t; mX = pos.x * t + mX; mY = pos.y * t + mY;
};
varcenter = { x: mX / mM, y: mY / mM }
returncenter
};
pixel2Pos是我另外写的根据i求出点坐标的函数:
SignaturePad.prototype.pixel2Pos =function(p) {
varw =this._ctx.canvas.width, h =this._ctx.canvas.height;
vary =Math.ceil((p +1) /4/ w);
varx =Math.ceil((p +1) /4- (y -1) * w);
return{ x: x, y: y }
}
这里要注意下:
getImageData() 方法返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。
对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:
R - 红色 (0-255)
G - 绿色 (0-255)
B - 蓝色 (0-255)
A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的)
根据以上的代码就可以找出重心,如下图红点所示位置:
以重心为中心,把数字放置于28x28的正方形中,剪切出来,传给后端即可。
以上为指南全文。MixLab无界社区是一所面向未来的实验室
领取专属 10元无门槛券
私享最新 技术干货