学号:SA16225140
姓名:李豪俊
前言:
为期7周的“网络程序设计”课程即将告一段落,在此提笔写下对于这门课的总结。很遗憾这门课程的时间太短暂,个人感觉还有很多需要学习了解的地方,特别希望自己能够完整地研究透彻课程项目中所涉及地这些技术——对于基础薄弱的我来说有点眼睛不够用了。
对于这一学期的课程,老师将研究方向转向了神经网络方向。这令我有些措手不及,选课之初以为是关于网络开发的学习研究,得知研究方向的变化其实压力很大,但是也很兴奋,希望能够学到新的知识去丰富自己。本科的时候周围一些同学在进行机器学习相关实验的时候,一直就很好奇,也有些畏惧,感觉艰涩难懂。可以说,学习这门课之前,神经网络对我而言是一个非常高深艰涩的学科。所幸在老师的带领下一点点入门,揭开了这个领域的神秘面纱,让我有勇气去探索发现更多不一样的知识,丰富自己的认知。
1 课程目标
基于深度学习神经网络等机器学习技术实现一个医学辅助诊断的专家系统原型,具体切入点为课程项目(项目地址):对血常规检验报告的OCR识别,深度学习与分析,进行诸如预测患者的年龄和性别等工作,研究其中可能存在的联系。通过该项目,学习机器学习的常见算法框架,重点分析神经网路,理解和掌握常用算法框架工具的使用。
2 神经网路
2.1 简介(摘自百度百科)
** 人工神经网络 **(Artificial Neural Networks,简写为ANNs)也简称为神经网络(NNs)或称作连接模型(Connection Model),它是一种模仿动物神经网络行为特征,进行分布式并行信息处理的算法数学模型。这种网络依靠系统的复杂程度,通过调整内部大量节点之间相互连接的关系,从而达到处理信息的目的。
** 人工神经网络 **:是一种应用类似于大脑神经突触联接的结构进行信息处理的数学模型。在工程与学术界也常直接简称为“神经网络”或类神经网络。
2.2 个人认识
提到神经网络,不可避免得说到机器学习。想必都已经听说,AlphaGo大战围棋高手,近日Master在野狐平台斩获50连胜,快棋击败了无数围棋高手,彰显出机器学习的巨大进步与实力。而神经网络作为机器学习一种应用相当广泛的算法,具有非常重要的意义。
对于当前机器学习发展如火如荼的现状,课程中引入神经网络是非常有远见和胆魄的,相当考验教学者与学生。在老师和同学一致努力下,一个复杂高难度高要求的项目在短短7周内从无到有,亲眼见证这一切我感到无比震惊与自豪。当然我们这门课扩展了不只是神经网络的学习,同学们分享了诸如决策树、支持向量积、贝叶斯分类、集成学习等等都让人大开眼界。
神经网络,其计算模型灵感来自动物的中枢神经系统(尤其是脑),并且被用于估计或可以依赖于大量的输入和一般的未知近似函数。在我看来,即是在模仿脑部神经元突触的形成过程,通过不断地刺激(模型训练),发现新的认识(形成模型)。神经网络的一个重要特性是它能够从环境中学习,并把学习的结果分布存储于网络的突触连接中。神经网络的学习是一个过程,在其所处环境的激励下,相继给网络输入一些样本模式,并按照一定的规则(学习算法)调整网络各层的权值矩阵,待网络各层权值都收敛到一定值,学习过程结束。然后我们就可以用生成的神经网络来对真实数据做分类。
如下图,最左一列节点是输入节点,最右列节点是输出节点,中间节点是隐藏节点。该图结构是分层的,隐藏的部分有时候也会分为多个隐藏层。使用的层数非常多就会变成我们平常说的深度学习了。
3 项目实践
3.1 神经网络实现手写字符识别
这个课程项目的学习算是对于神经网络初步入门,借以了解OCR识别和神经网络的基本概念。在网页端200x200的画布上监听鼠标按下操作绘制图形,点击按钮,获得画布结果,将其抽象为一个20x20的矩阵,并转为1x400的向量作为输入层。
这里主要运用前馈神经网络,该结构中不存在回路。而有输出反馈给输入的神经网络称作递归神经网络(RNN)。在这个实验中,我们使用前馈神经网络中经典的BP神经网络来实现手写识别系统。在数据前向传播时候用sigmoid函数作为激发函数。反向传播是数据的训练关键,需要通过计算误差率然后系统根据误差改变网络的权值矩阵和偏置向量。
详情见 实验楼课程
进一步的学习 反向传播神经网络极简入门
3.2 对血常规检验报告的OCR识别、深度学习与分析
3.2.1 环境配置
- 操作系统
优先选择 Ubuntu 14.04 - 开发配置
# 安装numpy,
sudo apt-get install python-numpy # http://www.numpy.org/
# 安装opencv
sudo apt-get install python-opencv # http://opencv.org/
# 安装OCR和预处理相关依赖
sudo apt-get install tesseract-ocr
sudo pip install pytesseract
sudo apt-get install python-tk
sudo pip install pillow
# 安装Flask框架
sudo pip install Flask
# 安装mongoDB(如果找不到可以先sudo apt-get update)
sudo apt-get install mongodb
sudo service mongodb started
sudo pip install pymongo
# 安装tensorflow,keras框架
sudo pip install tensorflow
sudo pip install keras
- 运行
cd BloodTestReportOCR
python view.py # upload图像,在浏览器打开http://yourip:8080
3.2.2 结构说明
- view.py
使用Flask(轻量级 Web 应用框架),搭配MongoDB,快速构建Web应用
只通过一个view.py
即可处理对于该项目所需提供响应的Web请求
Web 端上传图片到服务器,通过ImageFilter进行图片优化处理并切割,进行OCR识别,如果是可以识别的图片文件,存入mongodb并返回其fid
- imageFilter.py
对图像透视裁剪和OCR识别进行了简单的封装,以便于模块间的交互,规定适当的接口- ocr函数 - 模块主函数返回识别数据
用于对img进行ocr识别,他会先进行剪切,之后进一步做ocr识别,返回一个json对象 如果剪切失败,则返回None @num 规定剪切项目数 - perspect函数做 - 初步的矫正图片
用于透视image,他会缓存一个透视后的opencv numpy矩阵,并返回该矩阵 透视失败,则会返回None,并打印不是报告 @param 透视参数
关于param: 参数的形式为[p1, p2, p3 ,p4 ,p5]。 p1,p2,p3,p4,p5都是整型,其中p1必须是奇数。
- ocr函数 - 模块主函数返回识别数据
p1是高斯模糊的参数,p2和p3是canny边缘检测的高低阈值,p4和p5是和筛选有关的乘数。
如果化验报告单放在桌子上时,有的边缘会稍微翘起,产生比较明显的阴影,这种阴影有可能被识别出来,导致定位失败。 解决的方法是调整p2和p3,来将阴影线筛选掉。但是如果将p2和p3调的比较高,就会导致其他图里的黑线也被筛选掉了。 参数的选择是一个问题。 在getinfo.default中设置的是一个较低的阈值,p2=70,p3=30,这个阈值不会屏蔽阴影线。 如果改为p2=70,p3=50则可以屏蔽,但是会导致其他图片识别困难。
就现在来看,得到较好结果的前提主要有三个
化验单尽量平整
图片中应该包含全部的三条黑线
图片尽量不要包含化验单的边缘,如果有的话,请尽量避开有阴影的边缘。
- filter函数 - 过滤掉不合格的或非报告图片
返回img经过透视过后的PIL格式的Image对象,如果缓存中有PerspectivImg则直接使用,没有先进行透视 过滤失败则返回None @param filter参数 - autocut函数 - 将图片中性别、年龄、日期和各项目名称数据分别剪切出来
用于剪切ImageFilter中的img成员,剪切之后临时图片保存在out_path, 如果剪切失败,返回-1,成功返回0 @num 剪切项目数 @param 剪切参数
剪切出来的图片在BloodTestReportOCR/temp_pics/ 文件夹下
函数输出为data0.jpg,data1.jpg……等一系列图片,分别是白细胞计数,中性粒细胞记数等的数值的图片。
- imgproc.py
将识别的图像进行处理二值化等操作,提高识别率(包括对中文和数字的处理)
依次进行了,灰度化,二值化,腐蚀,放大,腐蚀,膨胀,直方图均衡化,中值滤波去噪点等操作,返回一个经过处理的图片
- tf_predict.py
使用Tensorflow框架进行年龄、性别预测 - caffe_predict.py
使用Caffe框架进行年龄、性别预测
- classifier.py
使用pHash.py
判定裁剪矫正后的报告是否为血常规检验报告,判定裁剪出检测项目的编号 - pHash.py
利用离散余弦变换(DCT)获取图片的低频成分,与已有的一张已经裁剪好的标准图片做对比,如果相似度>60%则接受,判定为血常规检测报告 - config.py
用于设定数据,当前包括:可识别图片的类型,数据库的地址与端口号,服务器配置地址与端口号,Flask框架Debug模式开关
3.2.3 项目演示
3.2.4 项目要点(Code Review)
- 网页配置
- Flask轻量级框架,便于快速开发
运行方式简洁
app = Flask(__name__, static_url_path="")
if __name__ == '__main__':
app.run(host=app.config['SERVER_HOST'], port=app.config['SERVER_PORT'])
HTTP(与 Web 应用会话的协议)有许多不同的访问URL方法。默认情况下,路由只回应GET请求,但是通过route()
装饰器传递methods
参数可以改变这个行为。
@app.route('/', methods=['GET', 'POST'])
def index():
return redirect('/index.html')
关于Flask Restful参见这里
- MongoDB非关系型数据库
操作简单快捷,为WEB应用提供可扩展的高性能数据存储解决方案
# 连接数据库,并获取数据库对象
db = MongoClient(app.config['DB_HOST'], app.config['DB_PORT']).test
c = dict(report_data=report_data,
content=bson.binary.Binary(content.getvalue()),
filename=secure_filename(f.name),
mime=mime)
db.files.save(c)
- Vue
由大牛尤雨溪开发的JavaScript框架,目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件
代码示例如下,数据绑定非常简洁
<table id= "table_left" class="table table-inverse table-hover table-bordered">
<thead>
<tr>
<th> </th>
<th>检测项目</th>
<th>结果</th>
<th>参考范围</th>
<th>单位</th>
</tr>
</thead>
<tbody>
<tr v-for="item in report_items_left">
<td>{{ item.count }}</td>
<td>{{ item.name }}</td>
<td>
<input type="text" v-model="item.value" class="form-control" placeholder="检测值" />
</td>
<td>{{ item.range }}</td>
<td>{{ item.unit }}</td>
</tr>
</tbody>
</table>
新建Vue对象,在methods自定义方法
var report = new Vue({
el: '#report',
data: {
report_items_left: new Array(),
report_items_right: new Array(),
},
methods: {………………
- AJAX
AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新
示例代码如下
$.ajax({
url: $(this).attr('action'),
type: 'POST',
data: new FormData(this),
processData: false,
contentType: false
}).done(function(data) {
//console.log(data.templates);
if(data.error == 1)
{
alert("图片不合格!");
}else
{
$("#filtered-image").empty().append(data.templates);
}
});
- CDN
值得注意的是,我们开发的时候采用CDN加载外部css,js等资源文件
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BloodTestOCR</title>
<!-- Jquey load frist-->
<script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js" type="text/javascript"></script>
<!-- Bootstrap -->
<link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css">
<!-- bootstrap.js below is needed if you wish to zoom and view file content
in a larger detailed modal dialog -->
<!-- https://unpkg.com/vue/dist/vue.js -->
<script src="http://cdn.bootcss.com/bootstrap/3.3.0/js/bootstrap.min.js"></script>
<script src="http://static.runoob.com/assets/vue/1.0.11/vue.min.js"></script>
</head>
在我阅读代码,运行测试的时候发现一些载入异常,对于这样的数据源````在某些网络下无法正常加载,会发生功能不能如期运行的BUG,当前使用<script src="http://static.runoob.com/assets/vue/1.0.11/vue.min.js">
替换,也可以考虑使用http://cdn.bootcss.com/bootstrap/
数据源
- OCR识别
主要使用opencv2包进行处理
1 对输入的图像进行处理,采用Canny算子描绘边缘
# 载入图像,灰度化,开闭运算,描绘边缘
img_sp = self.img.shape
ref_lenth = img_sp[0] * img_sp[1] * ref_lenth_multiplier
img_gray = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
img_gb = cv2.GaussianBlur(img_gray, (gb_param, gb_param), 0)
closed = cv2.morphologyEx(img_gb, cv2.MORPH_CLOSE, kernel)
opened = cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel)
edges = cv2.Canny(opened, canny_param_lower , canny_param_upper)
![Canny描绘边缘.jpg](http://upload-images.jianshu.io/upload_images/4255156-d262ee72354be2e5.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
2 调用CV2模块的findContours提取矩形轮廓,筛选对角线大于阈值的轮廓
# 调用findContours提取轮廓
contours, hierarchy = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 筛选出对角线足够大的几个轮廓
found = []
for i in range(len(contours)):
box = getbox(i)
distance_arr = distance(box)
if distance_arr > ref_lenth:
found.append([i, box])
3 将轮廓变成线,并去除不合适的线
# 将轮廓变为线
line = []
for i in found:
box = i[1]
point1, point2, lenth = getline(box)
line.append([point1, point2, lenth])
# 把不合适的线删去
if len(line)>3:
for i in line:
for j in line:
if i is not j:
rst = linecmp(i, j)
if rst > 0:
deleteline(line, j)
elif rst < 0:
deleteline(line, i)
#检测出的线数量不对就返回-1跳出
if len(line) != 3:
print "it is not a is Report!,len(line) =",len(line)
return None
# 由三条线来确定表头的位置和表尾的位置
line_upper, line_lower = findhead(line[2],line[1],line[0])
4 透视变换
#由于需要表格外的数据,所以变换区域需要再向上和向下延伸
left_axis = line_lower[0] - line_upper[0]
right_axis = line_lower[1] - line_upper[1]
line_upper[0] = line_upper[0] - left_axis * 2 / 15
line_upper[1] = line_upper[1] - right_axis * 2 / 15
line_lower[0] = line_lower[0] + left_axis * 2 / 15
line_lower[1] = line_lower[1] + right_axis * 2 / 15
#设定透视变换的矩阵
points = np.array([[line_upper[0][0], line_upper[0][1]], [line_upper[1][0], line_upper[1][1]],
[line_lower[0][0], line_lower[0][1]], [line_lower[1][0], line_lower[1][1]]],np.float32)
standard = np.array([[0,0], [1000, 0], [0, 760], [1000, 760]],np.float32)
#使用透视变换将表格区域转换为一个1000*760的图
PerspectiveMatrix = cv2.getPerspectiveTransform(points,standard)
self.PerspectiveImg = cv2.warpPerspective(self.img, PerspectiveMatrix, (1000, 760))
#输出透视变换后的图片
cv2.imwrite(self.output_path + 'region.jpg', self.PerspectiveImg)
- 数据分析(Tensorflow预测性别为例)
- 设置参数
learning_rate = 0.005
display_step = 100
n_input = 22
n_hidden_1_sex = 16
n_hidden_2_sex = 8
n_classes_sex = 2
- 建立模型
'''
建立性别模型
'''
x_sex = tf.placeholder("float", [None, n_input])
y_sex = tf.placeholder("float", [None, n_classes_sex])
def multilayer_perceptron_sex(x_sex, weights_sex, biases_sex):
# Hidden layer with RELU activation
layer_1 = tf.add(tf.matmul(x_sex, weights_sex['h1']), biases_sex['b1'])
layer_1 = tf.nn.relu(layer_1)
# Hidden layer with RELU activation
layer_2 = tf.add(tf.matmul(layer_1, weights_sex['h2']), biases_sex['b2'])
layer_2 = tf.nn.relu(layer_2)
# Output layer with linear activation
out_layer = tf.matmul(layer_2, weights_sex['out']) + biases_sex['out']
return out_layer
weights_sex = {
'h1': tf.Variable(tf.random_normal([n_input, n_hidden_1_sex])),
'h2': tf.Variable(tf.random_normal([n_hidden_1_sex, n_hidden_2_sex])),
'out': tf.Variable(tf.random_normal([n_hidden_2_sex, n_classes_sex]))
}
biases_sex = {
'b1': tf.Variable(tf.random_normal([n_hidden_1_sex])),
'b2': tf.Variable(tf.random_normal([n_hidden_2_sex])),
'out': tf.Variable(tf.random_normal([n_classes_sex]))
}
pred_sex = multilayer_perceptron_sex(x_sex, weights_sex, biases_sex)
- 运行测试
saver = tf.train.Saver()
init = tf.global_variables_initializer()
with tf.Session() as sess:
saver.restore(sess, "./model.ckpt")
print ("load model success!")
p_sex = sess.run(pred_sex, feed_dict={x_sex: data_predict})
if p_sex[0][0] > p_sex[0][1]:
sex_result = 1
else:
sex_result = 0
return sex_result
4 总结反思
近70人共同完成这一个项目,相当考验项目管理者的集成能力,这么一个庞大的开发团体,也非常需要软件工程经验对我们进行指导。幸而,孟宁老师坐镇中枢,集成管理整个项目,保证了项目的进展与成功。可以毫不夸张地说,缺少孟宁老师这样的项目管理者,我们几乎无法完成这门课的学习。
正是因为项目的复杂、高难度以及参与人数的庞大,项目的可扩展性相当高。我看到针对项目的每个环节,都有同学提出了各种各样不同的实现方式,没有说给出一个定式。一个开放的实践课程,让我大开眼界。例如,对于预测,同学们提出了CNN、RNN、决策树、向量积等等不一样的实现方式;在对于框架的选择上,也有各种不一样的偏好:Caffee、Tensorflow、Karas。可以说是,“八仙过海,各显神通”,让我见识到了周围同学出色的代码编程以及学习能力,实在佩服。
这门课程对于机器学习的知识面狩猎非常之广,个人零基础,一开始听大神们侃侃而谈就有点迷糊跟不上节奏,因此买了一本周志华老师的《机器学习》自己默默啃着,然而因为下学期5门课程的重压,精力实在有点捉襟见肘,直到现在还没有看完。但是,随着课程的学习,我也简单了解了很多关于机器学习的知识,极大地开拓了眼界,与人谈论机器学习时至少能够搭得上话了吧。(不知道是不是也算有所得了)
需要反省的是,自己做事比较磨蹭,每次有新的想法总会犹豫一下,又对于自己能力有点不自信,故而未曾pr,最后零贡献实在有点难看,希望这些缺点能够改正。
最后在检查的时候,对于页面改善提出了一点看法,根据老师的要求进行了修改,集成Bootstrap上传控件,提交了一个pr #270 完成对于Bootstrap上传插件的集成,总算也不是零贡献了。