前言

手上有個未來將利用 Tensorflow 做語音辨識的案子,趁著暑假的空檔研究並整理了思維。

也著手做了影像辨識的範例,辨識學校學生入口的四位數驗證碼。

本文所使用的 Tensorflow 版本為 1.13,已獲 2.0 版本更新的小節可參考連結。


關於 Tensorflow

TensorFlow 是一個開源軟體庫,用於各種感知和語言理解任務的機器學習。

被用於研究和生產許多 Google 商業產品,如語音辨識、Gmail、Google 相簿和搜尋。

最初由 Google 大腦團隊開發,用於 Google 的研究和生產,於 2015 年 11 月開源發布。

TensorFlow 提供了 Python,以及 C++、Java 等 API,底層核心引擎由 C++ 實現。

目前只有 Python API 較為豐富的實現了反向傳播部分。

所以大多數人使用 Python 進行模型訓練,但是可以選擇使用其它語言進行線上推理。


關於 機器學習

單眼的人臉辨識、Siri 的語音辨識看似神奇,但是電腦其實不是真的「看」得懂或「聽」得懂。

電腦只是將輸入(數字、影像、聲音)一律轉化為數字陣列,並套用公式計算。

例如:一張數字 7 的圖片,對電腦而言是一堆數字組合的陣列。

簡單來說,電腦其實就是在「算數學」而已。

至於,公式是什麼?電腦又計算了些什麼?這些本文不深究,我們只要學會如何使用 API 就好了。


影像辨識-訓練

事前準備

用 pip 即可安裝 TensorFlow 以及其他所需的軟體包:

pip install tensorflow
pip install opencv-python
pip install numpy

進行訓練前需要準備大量的樣本,我將驗證碼的每個數字擷取下來並放入對應名稱的目錄:


影像陣列化

這次簡單地使用只有一個隱藏層的 NN 標準神經網路,影像須先處理過,處理流程如下:

  1. 彩色影像轉成黑白。
  2. 黑白影像轉成二維陣列。
  3. 二維陣列扁平化。

原本彩色影像會有 RGB 三個通道,直接轉換會有三個二維陣列,為了降低複雜度,先將影像轉成黑白。

黑白影像轉成陣列就只剩一個二維陣列了,為了更精簡,我又將二維陣列再轉成一維陣列。

也就是說,一張樣本轉換為一條一維陣列,如果載入 50 個樣本的話就是 50 條。

再將這 50 條裝入一個陣列中,最終變成這種形態:[[1],[2],...,[49],[50]]

但是,電腦根本不知道這 50 條分別代表什麼數字,所以需要再將每條貼上標籤。

標籤就用一條一維陣列裝就可以了:[0,0,0,0,0,1,1,1,1,1,...,9,9,9,9,9]

def read_pic():
    digits = []
    labels = []
    for folder in range(2,10):
        for img in os.listdir("訓練集/{}/".format(folder)):
            # 影像轉成黑白(.convert("L")保留灰階;.convert("1")非黑即白)
            img_read = Image.open("訓練集/{}/{}".format(folder,img)).convert("L")
            # 影像轉成二維陣列並扁平化
            img_read = np.array(img_read, dtype=np.int32).flatten()
            # 將每條裝入陣列 digits
            digits.append([pixel for pixel in iter(img_read)])
            # 貼上標籤
            labels.append(folder)
    return np.array(digits), np.array(labels)

影像陣列標準化

影像陣列中的每個數字,從 0 到 255 都有可能是,由於使用 NN 標準神經網路,範圍太大難以收斂。

於是要先將影像陣列標準化,讓範圍縮小但又不喪失特徵,我使用 Z-score 方法。

Z-score 標準化後是浮點數,但影像已轉成黑白,數值差距明顯,所以我再次轉成整數,加快收斂速度。

順帶一提,只有影像陣列需要標準化,標籤陣列不需要。

def z_score(np_array):
    data = []
    for row in np_array:
        mean = np.mean(row, axis=0)
        std = np.std(row, axis=0)
        row = row - mean
        row = row / std
        row = np.nan_to_num(row) # NaN 置 0
        data.append(row)

    return np.array(data, dtype=np.int32) # 再次轉成整數

建構神經網路

(此節已有 TF2 版本的更新貼文)

直接使用 TensorFlow 提供的 API 建構就可以了,很方便。

首先,保留兩個輸入的位置 x, y,分別是影像陣列與標籤陣列。

再來建立神經網路層,一個隱藏層(10 個神經元、激勵函數 relu)。

一個輸出層(10 個神經元),數字要分類 0-9,共 10 個類別,所以神經元必須為 10。

接著建立最佳化器(optimizer)去降低誤差,學習效率為 0.05。

最後建立會話,會話指向初始化所有變數。

tf_x = tf.placeholder(tf.float32, x.shape) # 輸入 x
tf_y = tf.placeholder(tf.int32, y.shape)   # 輸入 y

l1 = tf.layers.dense(tf_x, 10, tf.nn.relu) # 隱藏層
output = tf.layers.dense(l1, 10)           # 輸出層

loss = tf.losses.sparse_softmax_cross_entropy(labels=tf_y, logits=output) # 計算誤差
accuracy = tf.metrics.accuracy(labels=tf.squeeze(tf_y), predictions=tf.argmax(output, axis=1),)[1] # 計算準確性
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.05) # 最佳化器
train_op = optimizer.minimize(loss) # 降低誤差

sess = tf.Session() # 建立會話
init_op = tf.group(tf.global_variables_initializer(), tf.local_variables_initializer())
sess.run(init_op) # 初始化所有變數

訓練並儲存結果

(此節已有 TF2 版本的更新貼文)

將影像陣列與標籤陣列傳值進會話,訓練 5000 步,每 5 步輸出準確性。

最後儲存訓練後的參數結果。

import os
import tensorflow as tf
import numpy as np
from PIL import Image


x, y = read_pic()
x = z_score(x)

for step in range(5000):
    _, acc, pred = sess.run([train_op, accuracy, output], {tf_x: x, tf_y: y})
    if step % 5 == 0:
        print('Accuracy=%.2f' % acc)

saver = tf.train.Saver()
saver.save(sess, './params', write_meta_graph=False)
print("Saved.")

影像辨識-推理

影像陣列化

與訓練時大同小異,訓練時如何處理,推理時就要比照辦理。

下載驗證碼,轉黑白,轉陣列,分割,扁平化。

url = "https://eap10.nuu.edu.tw/CommonPages/Captcha.aspx"

def load_pic():
    # 下載驗證碼
    res = requests.get(url)

    # 讀取驗證碼並轉為陣列
    #.convert("L")保留灰階;.convert("1")非黑即白
    pil_image = Image.open(BytesIO(res.content)).convert("L")
    np_image = np.array(pil_image, dtype=np.int32)

    # 儲存驗證碼
    pil_image.save("tmp.jpg")

    # 將陣列分割成四個數字並攤平成一維
    cut_area = [(7, 7, 14, 19), (21, 7, 14, 19), (34, 7, 14, 19), (48, 7, 14, 19)]
    digits = []
    for x,y,w,h in cut_area:
        img_read = np_image[y:y+h, x:x+w]
        digits.append([pixel for pixel in iter(img_read.flatten())])

    return np.array(digits)

影像陣列標準化

同訓練。

def z_score(np_array):
    data = []
    for row in np_array:
        mean = np.mean(row, axis=0)
        std = np.std(row, axis=0)
        row = row - mean
        row = row / std
        row = np.nan_to_num(row) # NaN 置 0
        data.append(row)

    return np.array(data, dtype=np.int32) # 再次轉成整數

建構神經網路

(此節已有 TF2 版本的更新貼文)

訓練儲存的只有參數結果不是模型,所以神經網路需重建。

用於推理就無法輸入標籤陣列了,因為目標即是推理出標籤。

tf_x = tf.placeholder(tf.float32, x.shape, name="tf_x") # 輸入 x

l1 = tf.layers.dense(tf_x, 10, tf.nn.relu) # 隱藏層
output = tf.layers.dense(l1, 10)           # 輸出層

sess = tf.Session() # 建立會話
saver = tf.train.Saver()
saver.restore(sess, "./params") # 讀取載入參數

推理並輸出預測值

(此節已有 TF2 版本的更新貼文)

輸出的預測值是四個一維陣列,每個陣列有 10 個元素,分別代表 0-9 的相似度。

例如:

[0.1, 0.6, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]即代表數字 1。

[0.1, 0.3, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.8]即代表數字 9。

故用argmax(1)取最大值的索引即可。

import os, requests
import tensorflow as tf
import numpy as np
from PIL import Image
from io import BytesIO


x = load_pic()
x = z_score(x)

pred = sess.run([output], {tf_x: x})

print("=====")
print(pred[0].argmax(1))
print("=====")