項目 | 內容 |
---|---|
課程 | 人工智能實戰2019 |
作業要求 | AI2019課程第四次作業(團隊) |
團隊成員 | 李大,許駿鵬,陳澤寅,鄒鎮洪,宋知遇,藺立萱 |
本次作業作用 | 熟悉mini_batch的實現、作用 |
參考文獻 | 谷歌CNN教程,tensorflow CNN教程, kivy doc ,kivy畫板教程 |
組隊名稱: 人工智能小分隊
NABCD
- N(Need,需求)
手寫體識別是經典計算機視覺項目,旨在最基本的字符類型(數字)上測試機器視覺算法的實用性。本項目通過編寫一個交互式的手寫數字識別小軟件,可以幫助同學們練習動手實踐一個小型AI demo,鍛煉代碼編寫能力、團隊編程能力和感受基本的機器視覺算法。
但當前手寫數字識別早已達到非常高的水准,進而衍生出對於更加復雜的字符和字符組合的識別的需求,比如連續輸入、復雜字符識別、表達式識別等,其中數學算式識別被認為是一個基礎但具有代表性的拓展,可以幫助人們簡答而快捷的輸入數學算式,而不必通過繁瑣的Math語言編碼。其重點應用在於論文編寫中,是科研人員的重要助手。當前已經存在的競品有Mathpix(圖像轉Math代碼)、Word公式編輯器(手寫輸入字符)等,但鮮有單次輸入完整算式的程序。這樣的情況一方面說明了這類軟件的需求,另一方面說明了開發的必要性。
- A(Approach,做法)
搭建手寫數字識別軟件。
- 模型實現
- 我們使用tensorflow實現了一個CNN進行數字識別
- 卷積層相比於由全連接層在構成神經網絡時通過參數共享使網絡不需要圖片中識別目標的相對位置,減少了參數,提高訓練效率,是使用神經網絡做圖片識別時常用的網絡架構
- CNN相關教程網上很多,我們僅簡單介紹一下
- CNN將二維圖像通過卷積和pooling逐漸展成一維向量,一維向量再送進全連接層做分類最終輸出每個數字對應的識別概率
- 卷積核是m*m的矩陣
- stride是其掃描時的步長
- padding是掃描到邊緣時是否向外補全的設定
- relu函數是一個激活函數,用於加速收斂,和sigmoid類似
- AdamOptimizer是一個常用的步長調整梯度下降優化器,隨着迭代次數的增加步長減小,以達到更精細的權值調整
- 我們使用Mnist數據集進行訓練
擴展軟件功能,支持連續輸入連續識別,支持算式識別。
- B(Benefit,好處)
可以幫助人們簡答而快捷的輸入數學算式,而不必通過繁瑣的Math語言編碼。其重點應用在於論文編寫中,是科研人員的重要助手。
- C(Competition,競爭)
目前主要有兩類競品:圖像轉公式、手寫公式
- 圖像轉公式:以Mathpix為代表,這列工具的主要功能在於將現有文件上的公式以截圖的形式存儲,再從圖像中識別公式並轉為Math代碼,適用於以代碼編輯公式的場景,便於用戶直接利用其它位置獲得的公式,但對於自建公式的編輯無效用。
- 手寫公式:以微軟的Math Input Panel為代表,這類工具的主要功能在於獲取用戶在輸入面板上繪制的公式圖形,再從圖形中識別公式並轉為公式類型文本(不是代碼),適用於比較復雜的公式編輯、自建公式的編輯和無法使用代碼編輯公式的情景,缺點是識別效率較低,容易誤識別。
- 我們的主要競品是第二類。
- D(Delivery, 交付)
初期:實現單個數字的識別。
中期:實現連續輸入字符的識別。
后期:擴展識別字符的類型(包含基本的算式字符),實現不太復雜的算式識別。
團隊成員&&分工
- 陳澤寅:模型的建立以及算法實現、文檔撰寫
- 李大: 算法的實現以及參數的調節、文檔撰寫
- 鄒鎮洪:算法的實現以及參數的調節、需求分析
- 宋知遇:神經網絡的搭建數據的搜集
- 藺立萱:神經網絡算法以及界面設計
- 許駿鵬:數據搜集以及數據預處理
項目時間預估以及項目期望
- 希望在期末結束之前,我們能夠做出類似\(a/b+c/d\)這類的手寫算式的計算實現。
模型實現
- 我們使用tensorflow實現了一個CNN進行數字識別
- 卷積層相比於由全連接層在構成神經網絡時通過參數共享使網絡不需要圖片中識別目標的相對位置,減少了參數,提高訓練效率,是使用神經網絡做圖片識別時常用的網絡架構
- CNN相關教程網上很多,我們僅簡單介紹一下
- CNN將二維圖像通過卷積和pooling逐漸展成一維向量,一維向量再送進全連接層做分類最終輸出每個數字對應的識別概率
- 卷積核是m*m的矩陣
- stride是其掃描時的步長
- padding是掃描到邊緣時是否向外補全的設定
- relu函數是一個激活函數,用於加速收斂,和sigmoid類似
- AdamOptimizer是一個常用的步長調整梯度下降優化器,隨着迭代次數的增加步長減小,以達到更精細的權值調整
- 我們使用Mnist數據集進行訓練
# coding: utf-8
import tensorflow as tf
def conv_net(input_x_dict, reuse, is_training):
with tf.variable_scope('ConvNet', reuse=reuse):
# TF Estimator input is a dict, in case of multiple inputs
# 為了適應有多個輸入變量的情況,TF Estimator要求輸入是一個字典
input_x = input_x_dict['images']
# Input layer 輸入層 [28*28*1]
input_x_images = tf.reshape(input_x, [-1, 28, 28, 1])
# The reason why the first dimension should be -1, is that we don't know the size of input,
conv1 = tf.layers.conv2d(
inputs=input_x_images,
filters=32,
kernel_size=[5, 5],
strides=1,
padding='same',
activation=tf.nn.relu
)
pool1 = tf.layers.max_pooling2d(
inputs=conv1,
pool_size=[2, 2],
strides=2
)
conv2 = tf.layers.conv2d(
inputs=pool1,
filters=64,
kernel_size=[5, 5],
strides=1,
padding='same',
activation=tf.nn.relu
)
pool2 = tf.layers.max_pooling2d(
inputs=conv2,
pool_size=[2, 2],
strides=2
)
flat = tf.reshape(pool2, [-1, 7 * 7 * 64])
dense = tf.layers.dense(
inputs=flat,
units=1024,
activation=tf.nn.relu
)
# Dropout層
# tf.layers.dropout
# inputs 張量
# rate 丟棄率
# training 是否是在訓練的時候丟棄
dropout = tf.layers.dropout(
inputs=dense,
rate=0.5,
training=is_training
)
# Output Layer, activation is not needed (actually a dense layer)
# 輸出層,不用激活函數(本質就是一個全連接層)
logits = tf.layers.dense(
inputs=dropout,
units=10
)
# Output size 輸出形狀 [?,10]
return logits
def model_fn(features, labels, mode):
# 因為Dropout對於訓練和測試/預測有不同的行為,我們需要建立兩個獨立的網絡,但它們共享相同的權重
logits_train = conv_net(features, reuse=False, is_training=True) # Net for training 對於訓練
logits_test = conv_net(features, reuse=True, is_training=False) # Net for evaluation and prediction 對於評估和預測
# Predictions 預測
pred_classes = tf.argmax(logits_test, axis=1)
pred_probas = tf.nn.softmax(logits_test)
# If prediction mode, early return 如果是預測模式,則提前退出
if mode == tf.estimator.ModeKeys.PREDICT:
predictions = {
'class_ids': pred_classes,
'probabilities': pred_probas
}
return tf.estimator.EstimatorSpec(mode, predictions=predictions)
# Define loss 定義損失函數
loss = tf.losses.softmax_cross_entropy(onehot_labels=labels, logits=logits_train)
if mode == tf.estimator.ModeKeys.TRAIN:
learning_rate = 0.001 # 學習速率
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
train_op = optimizer.minimize(loss, global_step=tf.train.get_global_step())
return tf.estimator.EstimatorSpec(mode, loss=loss, train_op=train_op)
# Calculate accuracy 計算准確率
acc_op = tf.metrics.accuracy(labels=tf.argmax(labels, axis=1), predictions=pred_classes)
# Calculate recall 計算召回率
rec_op = tf.metrics.recall(labels=tf.argmax(labels, axis=1), predictions=pred_classes)
eval_metrics = {
'accuracy': acc_op,
'recall': rec_op
}
# For tensorboard display 用於tensorboard顯示
tf.summary.scalar('accuracy', acc_op[1])
tf.summary.scalar('recall', rec_op[1])
# Evaluate the model 評估模型
if mode == tf.estimator.ModeKeys.EVAL:
# TF Estimators requires to return a EstimatorSpec, that specify
# the different ops for training, evaluating, ...
estim_specs = tf.estimator.EstimatorSpec(
mode=mode,
loss=loss,
eval_metric_ops=eval_metrics
)
return estim_specs
- trainer.py
# coding: utf-8
import tensorflow as tf
import CNN
import os
import shutil
from tensorflow.examples.tutorials.mnist import input_data
# Set the hyper params 設置超參數
num_step = 2000 # number of the training step 訓練迭代數
train_batch_size = 50 # batch size for training 訓練的batch大小
test_batch_size = 50 # batch size for test 測試所有的batch大小
# If a trained model exists, delete it and train a new model from the beginning
# 如果有已經訓練好的模型存在,刪除它,從頭開始訓練
if os.path.exists('saved_model'):
shutil.rmtree('saved_model')
# Display the tensorflow log
# 顯示tensorflow日志
tf.logging.set_verbosity(tf.logging.INFO)
# Get data from MNIST dataset
# 從MNIST數據集中獲取數據
mnist = input_data.read_data_sets('mnist_data/', one_hot=True)
train_x = mnist.train.images
train_y = mnist.train.labels
test_x = mnist.test.images
test_y = mnist.test.labels
# =============Training 訓練模型=============
# Build the Estimator 創建一個tensorflow estimator
model = tf.estimator.Estimator(CNN.model_fn, model_dir=r'saved_model/')
# Define the input function for training # 定義訓練的數據輸入函數
train_input_fn = tf.estimator.inputs.numpy_input_fn(
x={'images':train_x}, y=train_y,
batch_size=train_batch_size, num_epochs=None, shuffle=True
)
# Begin the training 開始訓練
model.train(train_input_fn, steps=num_step)
# =============Evaluate 測試評估模型=============
# Define the input function for evaluating # 定義測試的數據輸入函數
test_input_fn = tf.estimator.inputs.numpy_input_fn(
x={'images':test_x}, y=test_y,
batch_size=test_batch_size, shuffle=False
)
# Use the Estimator 'evaluate' method 開始測試
model.evaluate(test_input_fn)
- predictor.py
class CNNPredictor:
# Initialize the saved trained CNN model
# 初始化保存的訓練好的CNN模型
def __init__(self):
self.model = tf.estimator.Estimator(CNN.model_fn, model_dir=r'saved_model/')
print('獲取模型')
# Process the image
# 處理圖片
def process_img(self, filepath):
img = Image.open(filepath) # Open the file 打開文件
img = img.resize((28, 28))
img = img.convert('L') # Transfer the image into a grey image 轉換成灰度圖
imgarr = np.array(img, dtype=np.float32)
imgarr = imgarr.reshape([1, 28*28])/255.0
return imgarr
# Do predictions and return the result
# 進行預測,返回預測結果
def get_predictions(self, filepath):
imgarr = self.process_img(filepath)
predict_input_fn = tf.estimator.inputs.numpy_input_fn(
x={'images':imgarr}, batch_size=1, shuffle=False
)
predictions = list(self.model.predict(predict_input_fn))
return predictions[0]
界面實現
- 我們使用kivy繪制頁面
- 核心畫圖部分是在convas上跟蹤鼠標坐標隨着鼠標移動畫線
- 涉及on_touch_down(手指觸下),on_touch_move(觸摸點移動),on_touch_up(手指離開)三個事件
class PaintWidget(Widget):
color = (254, 254, 254, 1)
thick = 13
def __init__(self, root, **kwargs):
super().__init__(**kwargs)
self.parent_widget = root
def on_touch_down(self, touch):
with self.canvas:
Color(*self.color, mode='rgba')
if touch.x > self.width or touch.y < self.parent_widget.height - self.height:
return
touch.ud['line'] = Line(points=(touch.x, touch.y), width=self.thick)
def on_touch_move(self, touch):
with self.canvas:
if touch.x > self.width or touch.y < self.parent_widget.height - self.height:
return
touch.ud['line'].points += [touch.x, touch.y]
def on_touch_up(self, touch):
if touch.x > self.width or touch.y < self.parent_widget.height - self.height:
return
self.export_to_png('r.png')
self.parent.parent.do_predictions()
- 其余的界面繪制邏輯只是簡單的kivy使用
- 輸入圖片被保存為r.png后送入predictor進行識別,結果和概率顯示在界面上
class Recognizer(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.predictor = CNNPredictor() # Initialize the CNN model from the trained model 從保存的訓練好的模型中初始化CNN模型
self.number = -1 # Variable to store the predicted number 保存識別的數字的變量
self.orientation = 'horizontal' # UI related UI相關
self.draw_window()
# function to declare the components of the application, and add them to the window
# 聲明程序UI組件的函數,並且將它們添加到窗口上
def draw_window(self):
# Clear button 清除按鈕
# Painting board 畫板
self.painter = PaintWidget(self, size_hint=(1, 8 / 9))
# Label for hint text 提示文字標簽
self.hint_label = Label(font_name=CNN_Handwritten_Digit_RecognizerApp.font_name, size_hint=(1, 1 / 45))
# Label for predicted number 識別數字展示標簽
self.result_label = Label(font_size=200, size_hint=(1, 1 / 3))
# Label for some info 展示一些信息的標簽
self.info_board = Label(font_size=24, size_hint=(1, 26 / 45))
# BoxLayout 盒子布局
first_column = BoxLayout(orientation='vertical', size_hint=(2 / 3, 1))
second_column = BoxLayout(orientation='vertical', size_hint=(1 / 3, 1))
# Add widgets to the window 將各個組件加到應用窗口上
first_column.add_widget(self.painter)
first_column.add_widget(self.hint_label)
second_column.add_widget(self.result_label)
second_column.add_widget(self.info_board)
second_column.add_widget(self.clear_button)
self.add_widget(first_column)
self.add_widget(second_column)
# motion binding 動作綁定
# Bind the click of the clear button to the clear_paint function
# 將清除按鈕的點擊事件綁定到clear_paint函數上
self.clear_button.bind(on_release=self.clear_paint)
self.clear_paint() # Initialize the state of the app 初始化應用狀態
# Clear the painting board and initialize the state of the app.
def clear_paint(self, obj=None):
self.painter.canvas.clear()
self.number = -1
self.result_label.text = '^-^'
self.hint_label.text = 'Please draw a digit on the board~'
self.info_board.text = 'Info Board'
# Extract info from the predictions, and display them on the window
# 從預測結果中提取信息,並展示在窗口上
def show_info(self, predictions):
self.number = predictions['class_ids']
self.result_label.text = str(self.number)
self.hint_label.text = 'The predicted digit is displayed.'
probabilities = predictions['probabilities']
template = '''Probabilities
0: %.4f%%
1: %.4f%%
2: %.4f%%
3: %.4f%%
4: %.4f%%
5: %.4f%%
6: %.4f%%
7: %.4f%%
8: %.4f%%
9: %.4f%%'''
self.info_board.text = template % tuple(probabilities * 100.0)
# Use CNN predictor to do prediction, and call show_info to display the result
# 使用CNN預測器做預測,並調用show_info函數將結果顯示出來
def do_predictions(self):
pre = self.predictor.get_predictions('r.png')
self.show_info(pre)
手寫界面和識別結果展示
- 0
- 1
- 2
- 3
- 4
-
5
-
6
- 7
- 8
- 9