项目体验地址:http://at.iunitv.cn/javascript
效果预览:前端
不少小伙伴嘴上说着学不动了,其实身体仍是很诚实的。java
毕竟读书仍是有不少好处的:好比让你的脑门散发智慧的光芒,再或者让你有理由说由于读书太忙了因此没有女友等等。因此在这个特殊的日子里,你这一年的图书咱们承包了。不为别的,只为帮助在座的各位在2020年可以碰见更好的本身!node
今天的主题仅仅是送图书,咱们也想要借助这个特殊的机会,普及一下Tensorflow相关的知识,咱们会用TensorFlow.js作一个图书识别的模型,并在Vue Application中运行,赋予网页识别图书的能力。python
本文讲述了AI相关的概念知识和如何运用SSD Mobile Net V1模型进行迁移学习的方法,从而帮助你们完成一个能够在网页上运行的图书识别模型。git
迁移学习和域适应指的是在一种环境中学到的知识被用在另外一个领域中来提升它的泛化性能。——《深度学习》,第 526 页
再简单一点理解,以今天图书识别模型训练为例,咱们利用前人训练好的具有图片识别能力的AI模型,保留AI模型中对图片特征提取的能力的基础上再训练,使AI模型具有识别图书的能力。程序员
迁移学习可以大大提升模型训练的速度,并达到相对不错的正确率。github
而咱们今天所要迁移学习的对象就是SSD Mobile Net V1模型,初次接触神经网络的同窗能够将其理解为一种具有图片识别的轻便小巧的AI模型,它可以在移动设备上高效地运行。对这个模型具体的神经网络设计结构感兴趣的同窗能够自行搜索。web
了解了基本的概念以后,咱们便开始动手吧!咱们能够基于SSD Mobile Net模型去设计一个属于本身的AI模型,并让它在Vue Application中运行。npm
本次项目是为了训练一个Object Detection的模型,即目标识别的模型,该模型可以识别并圈选出图片中相应的目标对象。
为了不小伙伴由于环境问题遇到各类各样的坑,在工做开展以前,咱们先跟你们同步一下运行的环境。你们若是要动手去作,也尽可能跟咱们的运行环境保持一致,这样能够有效避免踩坑,规避“从入门到放弃”的现象。
开发环境:
同步完开发环境后,终于要开始动工了。首先咱们须要在Github上下载几个项目:
咱们能够经过搜索引擎收集有关图书的图片素材:
其次,咱们能够在Github上克隆LabelImg项目,并根据Github的使用说明,按照不一样的环境安装运行LabelImg项目,运行后的页面以下:
而后咱们按照如下步骤,将图片格式转换为圈选区域后的XML文件:
存放完后咱们在存放的目录下会看到许多XML格式的文件,这个文件记录了图片的位置信息、圈选信息和标签信息等,用于后续的模型训练。
从Github克隆迁移模型训练的项目迁移模型训练项目,注意要在r1.5分支运行,并用PyCharm打开项目。
项目的目录环境为上图,首先咱们须要下载TensorFlow1.15.2版本:
pip install tensorflow==1.15.2
其次安装依赖包:
sudo pip install pillow sudo pip install lxml sudo pip install jupyter sudo pip install matplotlib
而后经过终端切换到research目录,并执行几行配置命令,具体请参考Github的使用说明:
cd ./research protoc object_detection/protos/*.proto --python_out=. export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
最后咱们运行model_builder_test.py
文件,若是在终端中看到OK字样,表示配置成功。
python object_detection/builders/model_builder_test.py
克隆并打开图片格式转换项目,而后咱们对该项目加以小改造:
改造文件目录:
annotations
、data
、training
目录中的内容xmls
目录,用以存放xml文件
改造文件:
接着,咱们再改造如下2个文件并新增一个文件,方便咱们转换图片格式
改造xml_to_csv.py
为:
import os import glob import pandas as pd import xml.etree.ElementTree as ET import random import time import shutil class Xml2Cvs: def __init__(self): self.xml_filepath = r'./xmls' self.save_basepath = r"./annotations" self.trainval_percent = 0.9 self.train_percent = 0.85 def xml_split_train(self): total_xml = os.listdir(self.xml_filepath) num = len(total_xml) list = range(num) tv = int(num * self.trainval_percent) tr = int(tv * self.train_percent) trainval = random.sample(list, tv) train = random.sample(trainval, tr) print("train and val size", tv) print("train size", tr) start = time.time() test_num = 0 val_num = 0 train_num = 0 for i in list: name = total_xml[i] if i in trainval: if i in train: directory = "train" train_num += 1 xml_path = os.path.join(os.getcwd(), 'annotations/{}'.format(directory)) if (not os.path.exists(xml_path)): os.mkdir(xml_path) filePath = os.path.join(self.xml_filepath, name) newfile = os.path.join(self.save_basepath, os.path.join(directory, name)) shutil.copyfile(filePath, newfile) else: directory = "validation" xml_path = os.path.join(os.getcwd(), 'annotations/{}'.format(directory)) if (not os.path.exists(xml_path)): os.mkdir(xml_path) val_num += 1 filePath = os.path.join(self.xml_filepath, name) newfile = os.path.join(self.save_basepath, os.path.join(directory, name)) shutil.copyfile(filePath, newfile) else: directory = "test" xml_path = os.path.join(os.getcwd(), 'annotations/{}'.format(directory)) if (not os.path.exists(xml_path)): os.mkdir(xml_path) test_num += 1 filePath = os.path.join(self.xml_filepath, name) newfile = os.path.join(self.save_basepath, os.path.join(directory, name)) shutil.copyfile(filePath, newfile) end = time.time() seconds = end - start print("train total : " + str(train_num)) print("validation total : " + str(val_num)) print("test total : " + str(test_num)) total_num = train_num + val_num + test_num print("total number : " + str(total_num)) print("Time taken : {0} seconds".format(seconds)) def xml_to_csv(self, path): xml_list = [] for xml_file in glob.glob(path + '/*.xml'): tree = ET.parse(xml_file) root = tree.getroot() print(root.find('filename').text) for object in root.findall('object'): value = (root.find('filename').text, int(root.find('size').find('width').text), int(root.find('size').find('height').text), object.find('name').text, int(object.find('bndbox').find('xmin').text), int(object.find('bndbox').find('ymin').text), int(object.find('bndbox').find('xmax').text), int(object.find('bndbox').find('ymax').text) ) xml_list.append(value) column_name = ['filename', 'width', 'height', 'class', 'xmin', 'ymin', 'xmax', 'ymax'] xml_df = pd.DataFrame(xml_list, columns=column_name) return xml_df def main(self): for directory in ['train', 'test', 'validation']: xml_path = os.path.join(os.getcwd(), 'annotations/{}'.format(directory)) xml_df = self.xml_to_csv(xml_path) xml_df.to_csv('data/mask_{}_labels.csv'.format(directory), index=None) print('Successfully converted xml to csv.') if __name__ == '__main__': Xml2Cvs().xml_split_train() Xml2Cvs().main()
generate_tfrecord.py
文件,将csv格式转换为TensorFlow须要的record格式:将该区域的row_label改为咱们LabelImg中的标签名,由于咱们只有一个标签,因此直接修改为book
便可。
新增一个generate_tfrecord.sh
脚本,方便执行generate_tfrecord.py
文件
#!/usr/bin/env bash python generate_tfrecord.py --csv_input=data/mask_train_labels.csv --output_path=data/mask_train.record --image_dir=images python generate_tfrecord.py --csv_input=data/mask_test_labels.csv --output_path=data/mask_test.record --image_dir=images python generate_tfrecord.py --csv_input=data/mask_validation_labels.csv --output_path=data/mask_validation.record --image_dir=images
配置Object Decation的环境:
export PYTHONPATH=$PYTHONPATH:你的models/research/slim所在的全目录路径
最后咱们将图片文件复制到images
目录,将xml文件复制到xmls
目录下,再执行xml_to_csv.py
文件,咱们会看到data目录下产生了几个csv格式结尾的文件;这时,咱们在终端执行generate_tfrecord.sh
文件,TensorFlow所须要的数据格式就大功告成啦。
在这个环节咱们要作如下几件事:
book.pbtxt
文件和book.config
文件为了方便我直接将models/research/object_detection/test_data
下的目录清空,放置迁移训练的文件。
首先咱们下载SSD Mobile Net V1模型文件:
咱们下载第一个ssd_mobilenet_v1_coco模型便可,下载完毕后,咱们解压下载的模型压缩包文件,并将模型相关的文件放在test_data
的model
目录下。并将咱们刚刚生成的record文件放置在test_data
目录下。
咱们在test_data
目录下,新建一个book.pbtxt
文件,并完成配置内容:
item { id: 1 name: 'book' }
因为咱们只有一个标签,咱们就直接配置一个id值为1,name为book的item
对象。
因为咱们使用SSD Mobile Net V1模型进行迁移学习,所以咱们到sample\configs
目录下复制一份ssd_mobilenet_v1_coco.config
文件并重命名为book.config
文件。
接着咱们修改book.config
中的配置文件:
将num_classes修改成当前的标签数量:
因为咱们只有一个book标签,所以修改为1便可。
修改全部PATH_TO_BE_CONFIGURED
的路径:
<center>
<img src="https://user-gold-cdn.xitu.io/2020/4/23/171a5d916b7213e0?w=1410&h=982&f=png&s=147351" style="zoom:50%;" />
</center>
咱们将此处的模型文件地址设置成testdata/model/model.ckpt
的全路径地址。
咱们将train_input_reader
的input_path
设置成mask_train.record
的全路径地址;将label_map_path
设置成book.pbtxt
的全路径地址;将eval_input_reader
的input_path
设置成mask_test.record
的全路径地址。
到目前为止咱们全部配置都已经完成啦。接下来就是激动人心的训练模型的时刻。
咱们在终端中运行train.py
文件,开始迁移学习、训练模型。
python3 train.py --logtostderr --train_dir=./test_data/training/ --pipeline_config_path=./test_data/book.config
其中train_dir
为咱们训练后的模型存放的目录,pipeline_config_path为咱们book.config
文件所在的相对路径。
运行命令后,咱们能够看到模型在进行一步一步的训练:
并在/test_data/training
目录下存放训练后的模型文件:
<center>
<img src="https://user-gold-cdn.xitu.io/2020/4/23/171a5db65704e6e0?w=610&h=842&f=png&s=140510" style="zoom:50%;" />
</center>
咱们经过export_inference_graph.py
文件,将训练好的模型转换为pb格式的文件,这个文件格式在后面咱们要用来转换为TensorFlow.js可以识别的文件格式。终于咱们见到TensorFlow.js的影子啦。
<center>
<img src="https://user-gold-cdn.xitu.io/2020/4/23/171a5dbee720cc2c?w=440&h=439&f=jpeg&s=20529" style="zoom:50%;" />
</center>
咱们执行命令,运行export_inference_graph.py
文件:
python export_inference_graph.py --input_type image_tensor --pipeline_config_path ./test_data/book.config --trained_checkpoint_prefix ./test_data/training/model.ckpt-1989 --output_directory ./test_data/training/book_model_test
其中pipeline_config_path
为book.config
的相对文件路径,trained_checkpoint_prefix
为模型文件的路径,例如咱们选择训练了1989步的模型文件,output_directory
为咱们输出pb文件的目标目录。
运行完后,咱们能够看到一个生成了book_model_test
目录:
首先咱们须要依赖TensorFlowjs的依赖包
pip install tensorflowjs
而后经过命令行转换刚刚生成的pb文件
tensorflowjs_converter --input_format=tf_saved_model --output_node_names='detection_boxes,detection_classes,detection_features,detection_multiclass_scores,detection_scores,num_detections,raw_detection_boxes,raw_detection_scores' --saved_model_tags=serve --output_format=tfjs_graph_model ./saved_model ./web_model
其中咱们设置最后两个参数,即saved_model
的目录与TensorFlow.js识别模型的输出目录。
运行结束后,咱们能够看到一个新生成的web_model
目录,其中包括了咱们迁移学习训练后的模型。
到这里,模型训练的阶段终于结束了。
新建Vue项目,在Vue项目的public目录下放入咱们训练好的模型,即web_model目录。
接着咱们借助Tensorflow.js的依赖包,在package.json
的dependencies
中加入:
"@tensorflow/tfjs": "^1.7.2", "@tensorflow/tfjs-core": "^1.7.2", "@tensorflow/tfjs-node": "^1.7.2",
而后经过npm命令安装依赖包。
在咱们的JS代码部分引入TensorFlow的依赖包:
import * as tf from '@tensorflow/tfjs'; import {loadGraphModel} from '@tensorflow/tfjs-converter';
接着第一步,咱们先加载模型文件中的model.json
文件:
const MODEL_URL = process.env.BASE_URL+"web_model/model.json"; this.model = await loadGraphModel(MODEL_URL);
经过loadGraphModel
方法,咱们加载好训练的模型,再将模型对象打印出来:
随后,咱们能够看到模型会输出一个长度为4的数组:
detection_scores
:表示识别对象模型的置信度,置信度越高,则表明模型认为对应区域识别为书本的可能性越高detection_classes
:表示模型识别的区域对应的标签,例如在本案例中,识别出来的是booknum_detections
:表示模型识别出目标对象的个数detection_boxes
:表示模型识别出来目标对象的区域,为一个长度为4的数组,分别是:[x_pos,y_pos,x_width,y_height] 。第一个位表明圈选区域左上角的x坐标,第二位表明圈选左上角的y坐标,第三位表明圈选区域的宽度,第四位表明圈选区域的长度。知道了输出值,咱们就能够开始将图片输入到模型中,从而获得模型预测的结果:
const img = document.getElementById('img'); let modelres =await this.model.executeAsync(tf.browser.fromPixels(img).expandDims(0));
咱们经过model.executeAsync
方法,将图片输入到模型中,从而获得模型的输出值。
结果是咱们前文提到的一个长度为4的数组。接着咱们经过自定义方法,将获得的结果进行整理,从而输出一个想要的结果格式:
buildDetectedObjects:function(scores, threshold, imageWidth, imageHeight, boxes, classes, classesDir) { const detectionObjects = []; scores.forEach((score, i) => { if (score > threshold) { const bbox = []; const minY = boxes[i * 4] * imageHeight; const minX = boxes[i * 4 + 1] * imageWidth; const maxY = boxes[i * 4 + 2] * imageHeight; const maxX = boxes[i * 4 + 3] * imageWidth; bbox[0] = minX; bbox[1] = minY; bbox[2] = maxX - minX; bbox[3] = maxY - minY; detectionObjects.push({ class: classes[i], label: classesDir[classes[i]].name, score: score.toFixed(4), bbox: bbox }); } }); return detectionObjects }
咱们经过调用buildDetectedObjects
来整理和返回最后的结果。
detection_scores
数组detectionObjects
中detection_boxes
数组detection_classes
数组调用buildDetectedObjects
方法示例:
let classesDir = { 1: { name: 'book', id: 1, } }; let res=this.buildDetectedObjects(modelres[0].dataSync(),0.20,img.width,img.height,modelres[3].dataSync(),modelres[1].dataSync(),classesDir);
咱们经过modelres[0].dataSync()
,来获取对应结果的数组对象,再输入到方法中,从而最终得到res结果对象。
最后咱们经过Canvas的API,将图片根据bbox返回的数组对象,画出对应的区域便可。因为篇幅缘由,就不赘述了,最终效果以下:
本案例的模型存在必定的不足,因为训练时间较短,图书的封面类型众多,存在人像、风景图等等的样式,致使模型在识别过程当中可能会将少部分的人脸、风景照等图片错误地识别成图书封面。各位小伙伴在训练本身模型的过程当中能够考虑优化此问题。
固然,本案例的模型在识别非图书的场景会存在识别不许确的状况,一方面这是由于本案例从网络收集的图书样本具备必定局限性,并且图书的封面类型千差万别,存在人像、风景图等等的样式;另外一方面由于本文在仅为起到抛砖引玉的做用,为各位前端小伙伴普及TensorFlow.js相关的知识,并提供训练本身的模型的解决方案,因此在收集样本和模型训练时间较短。感兴趣的小伙伴能够本身琢磨琢磨如何优化样本和在避免过拟合的状况下提升训练时长,从而提升模型对被识别物体的准确性。
咱们写下本文仅为起到抛砖引玉的做用,为各位前端小伙伴普及TensorFlow.js相关知识并提供一种AI的解决方案。
在世界读书日,咱们但愿和广大程序员一块儿学习新知、共同进步,个推技术学院也特意为你们准备了微信读书卡,愿每位热爱学习的开发者都能畅游书海,碰见更好的本身!
活动奖品:
一等奖 :极客时间充值卡1张,1名
二等奖:获得电子书vip年卡1张,3名
三等奖5名:《深度学习》1本,5名
抽奖方式:
扫描下方二维码关注个推技术学院公众号,后台回复“我爱读书”获取抽奖入口,点击便可参与抽奖。
开奖时间:2020年4月27日 16:00,系统将随机抽取出幸运粉丝。
领取方式:请中奖者于24小时内在抽奖助手中填写收件信息,咱们会在7个工做日以内为您寄出。
注:活动解释权归个推全部。