AI識萬物:從0搭建和部署手語識別系統 ⛵

💡 作者:韓信子@ShowMeAI
📘 深度學習實戰系列//www.showmeai.tech/tutorials/42
📘 電腦視覺實戰系列: //www.showmeai.tech/tutorials/46
📘 本文地址//www.showmeai.tech/article-detail/292
📢 聲明:版權所有,轉載請聯繫平台與作者並註明出處
📢 收藏ShowMeAI查看更多精彩內容

據北京聽力協會預估數據,中國聽障人群數量已過千萬。而在全球範圍內有4.66億人患有殘疾性聽力損失,約佔全世界人口的5%。聾啞人士很特殊,他們需要使用手語進行交流,其他與常人無異,中國存在特殊教育水平在各城市中發展力度具有較大差異,國家通用手語推廣程度淺,但不懂手語,與聽力障礙者交流會非常困難。

在本篇內容中,ShowMeAI 藉助深度學習與神經網路技術,針對這個問題從 0 構建 1 個應用程式,檢測手語並將其翻譯給其他人進而打破手語隔閡。

搭建和部署完成後,你可以通過攝影機,輕鬆測試模型,如下圖所示,快來一起試試吧。這個動圖中的手勢代表的單詞,見文末哦!

💡 手語介紹

我們先來簡單了解一下手語,它由 3 個主要部分組成:

  • 手指拼寫:這是一種手動的交流方式,用雙手和手指拼寫單詞。每個字母都用指定的手位置表示。
  • 單詞級符號辭彙:這是一個大型影片數據集,用於識別單詞或字母的整個手勢。
  • 非手部特徵:包括任何面部表情、嘴巴、舌頭或身體姿勢。

在本文中,我們先解決第①個部分的問題。我們準備使用的解決方案是基於視覺數據的神經網路

💡 深度學習與電腦視覺

人工智慧和電腦視覺的最典型的模型是卷積神經網路(CNN),它在典型的電腦視覺應用中(如影像識別、目標檢測等)應用廣泛。我們在本次應用的核心技術也將採用 CNN。

CNN 網路有著如上圖所示的網路結構,典型的結構包括卷積層、池化層、激活層、全連接層等,對於輸入影像,可以有效抽取影像內容表徵,並進行分類或其他處理。卷積層等特殊結構,可以在控制參數量的前提下,保證良好的影像特徵提取能力。

關於卷積神經網路的詳細知識可以參考ShowMeAI下述教程:

💡 小試牛刀,打通流程

我們來構建一個 CNN 識別的流程,會分成以下基礎步驟:

  • 數據讀取與切分
  • 數據可視化及預處理
  • CNN網路構建與訓練

① 導入相關庫

我們在這裡主要使用 TensorFlow 構建網路與訓練,會使用 Numpy 做數據計算與處理,以及使用 Matplotlib 進行簡單可視化。

對於這些工具庫,ShowMeAI都製作了快捷即查即用的速查表手冊,大家可以在下述位置獲得:

我們先把這些工具庫導入。

# 導入工具庫
import string
import pandas as pd
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow import keras
from functools import partial
from tensorflow.keras.preprocessing.image import ImageDataGenerator, array_to_img

② 讀取數據集

本數據集為手語字母對應的數據集,圖片 size 不大,所以也叫做 sign_mnist 數據集(類比手寫數字數據集 mnist),部分示例圖片如下

數據集大家可以在 🏆Kaggle平台對應數據集頁面 下載,也可以通過ShowMeAI的百度網盤地址下載。

🏆 實戰數據集下載(百度網盤):公眾號『ShowMeAI研究中心』回復『實戰』,或者點擊 這裡 獲取本文 [5] 從0搭建基於神經網路的手語識別系統sign_mnist 數據集

ShowMeAI官方GitHub//github.com/ShowMeAI-Hub

下面我們載入訓練集與測試集並切分特徵與標籤:

# 讀取數據
test = pd.read_csv("sign_mnist_test.csv")
train = pd.read_csv("sign_mnist_train.csv")

# 輸出基本資訊
print("訓練集維度", train.shape)
print("測試集維度", train.shape)
# 輸出標籤資訊
labels = train["label"].value_counts().sort_index(ascending=True)
labels
# 切分特徵與標籤
train_x = train.drop(labels = "label", axis = 1)
train_y = train["label"]
test_x = test.drop(labels = "label", axis = 1)
test_y = test["label"]
train_x.head()
# 數據預處理與可視化

# 存儲標籤數據
test_classes= test_y
train_clasees = train_y

# 特徵轉為numpy格式
train_x = train_x.to_numpy()
test_x = test_x.to_numpy()

# 把數據轉為3維影像數據(圖片數量*寬*高,這裡如果是灰度圖,顏色通道為1,省略)
train_x = train_x.reshape(-1,28,28)
test_x = test_x.reshape(-1,28,28)
# 在訓練集中取樣30張圖片,做可視化查看
def plot_categories(training_images, training_labels):
    fig, axes = plt.subplots(3, 10, figsize=(16, 15))
    axes = axes.flatten()
    letters = list(string.ascii_lowercase)

    for k in range(30):
        img = training_images[k]
        img = np.expand_dims(img, axis=-1)
        img = array_to_img(img)
        ax = axes[k]
        ax.imshow(img, cmap="Greys_r")
        ax.set_title(f"{letters[int(training_labels[k])]}")
        ax.set_axis_off()

    plt.tight_layout()
    plt.show()

plot_categories(train_x, train_y)

③ 卷積神經網路CNN搭建

我們使用 TensorFlow 的 high level API(即keras)搭建一個簡易CNN神經網路,並擬合一下數據

def create_model():
    model = tf.keras.models.Sequential([
    # 卷積層
    tf.keras.layers.Conv2D(32, (3,3), activation='relu', input_shape=(28, 28, 1)),
    # 池化層
    tf.keras.layers.MaxPooling2D(2,2),
    # 卷積層
    tf.keras.layers.Conv2D(32, (3,3), activation='relu'),
    # 池化層
    tf.keras.layers.MaxPooling2D(2,2),
    # 展平
    tf.keras.layers.Flatten(),
    # 全連接層
    tf.keras.layers.Dense(512, activation='relu'),
    # softmax分類
    tf.keras.layers.Dense(26, activation='softmax')])

    model.compile(
    optimizer='adam',  #優化器
    loss='sparse_categorical_crossentropy',  #損失函數
    metrics=['accuracy']) #評估準則
  
    return model
# 初始化模型
model = create_model()
# 擬合數據
history = model.fit(train_x, train_y, epochs=20, validation_data=(test_x, test_y))

我們這裡在全量數據集上迭代20個輪次,結果如下:

我們可以看到,這裡的數據並不特別複雜,在自己從頭搭建的 CNN 模型上,經過訓練可以達到訓練集 100% 驗證集 92% 的準確率。

我們再對訓練過程中的「準確率」及「損失函數」變化值進行繪製,以了解模型狀態。

# 獲取準確率與損失函數情況
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']

# matplotlib繪製訓練過程中指標的變化狀況
epochs = range(len(acc))

plt.plot(epochs, acc, 'r', label='Training accuracy')
plt.plot(epochs, val_acc, 'b', label='Validation accuracy')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()

plt.plot(epochs, loss, 'r', label='Training Loss')
plt.plot(epochs, val_loss, 'b', label='Validation Loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

💡 問題與優化

① 深度網路與梯度消失

一般來說,隨著 CNN 網路層數變深,模型的學習能力會變強,也能學到更多的資訊。但訓練深度CNN存在梯度消失的問題。

梯度消失和梯度爆炸部分內容也可以參考ShowMeAI的對吳恩達老師課程的總結文章 📘 深度學習教程 | 深度學習的實用層面

非常深的神經網路的梯度會很快變為零(反向傳播的梯度連乘帶來的問題),這最終會使整個梯度下降變慢。有一些特殊結構的神經網路,可以大程度緩解這個問題,比如最著名的 ResNet,當然,大家可以藉助 ResNet 預訓練模型快速遷移學習應用在我們當前的手語識別問題上,為了讓大家對ResNet 細節更清晰,我們在這裡手動搭建 ResNet-50(即50層的ResNet網路)來訓練和做效果對比。

ResNet的詳細講解也可以參考ShowMeAI的 📘 深度學習教程 | 吳恩達專項課程 · 全套筆記解讀中的文章 📘 深度學習教程 | 經典CNN網路實例詳解

② ResNet 模型簡介

ResNet 是 Residual Networks 的簡稱,是迄今為止我們看到的最流行和最成功的深度學習模型之一。ResNets 由殘差塊組成,殘差塊的核心組件是『跳躍連接/skip-connection』。跳躍連接,也稱為快捷連接,讓神經網路跳過某些層並將一層的輸出饋送到神經網路中另一層的輸入。它能幫助模型避免乘以中間跳過的那些層的權重,從而有助於解決梯度消失的問題。

然而,使用 ResNet 和跳躍連接,由於中間有卷積層和池化層,一層輸出的維度可能與另一層的輸出維度不同。為了解決這個問題,可以使用兩種方法:

  • 快捷連接填充多個零實體以增加其維度
  • 添加 1X1 卷積層來匹配維度。

但是,對於第二種方法,我們需要在輸出中添加一個額外的參數,而第一種方法不需要。

③ ResNet為何有效

ResNet的效果核心有2點:

  • ① 它使用我們上面提到的跳躍連接,它跳過層來解決梯度消失的問題。
  • ② 它通過讓模型學習恆等函數來確保最高層的性能至少與最低層一樣好。

④ 構建ResNet-50

下面我們參考 keras 官方 ResNet 構建方式,構建一個 ResNet-50,如下所示,我們先構建基本模組,再組裝成最終的網路。

# Defining the identity block of the Resnet-50 Model. 
def identity_block(X, f, filters, training=True):
    # filter of the three convs 
    f1,f2,f3 = filters
    X_shortcut = X 
    
    # First Component 
    X = tf.keras.layers.Conv2D(filters = f1, kernel_size = 1, strides = (1,1), padding = 'valid')(X)
    X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
    X = tf.keras.layers.Activation('relu')(X)
   
    # Second Component 
    X = tf.keras.layers.Conv2D(filters = f2, kernel_size = f, strides = (1,1), padding = 'same')(X)
    X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
    X = tf.keras.layers.Activation('relu')(X)
   
    # Third Component 
    X = tf.keras.layers.Conv2D(filters = f3, kernel_size = 1, strides = (1,1), padding = 'valid')(X)
    X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
    
    # Adding the two tensors 
    X = tf.keras.layers.Add()([X_shortcut,X])
    X = tf.keras.layers.Activation('relu')(X)
    
    # Returning the last output
    return X
# Defining the Convolution Block of the Resnet-50 Model. 
def convolutional_block(X, f, filters, s=2,training=True):
    # filter of the three convs 
    f1,f2,f3 = filters
    X_shortcut = X 
    
    # First Component 
    X = tf.keras.layers.Conv2D(filters = f1, kernel_size = 1, strides = (1,1), padding = 'valid')(X)
    X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
    X = tf.keras.layers.Activation('relu')(X)
    
    # Second Component 
    X = tf.keras.layers.Conv2D(filters = f2, kernel_size = f, strides = (s,s), padding = 'same')(X)
    X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
    X = tf.keras.layers.Activation('relu')(X)
    
    # Third Component 
    X = tf.keras.layers.Conv2D(filters = f3, kernel_size = 1, strides = (1,1), padding = 'valid')(X)
    X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
    
    # Converting the Input Volume to the match the last output for addition. 
    X_shortcut =tf.keras.layers.Conv2D(filters = f3, kernel_size = 1, strides = (s,s), padding = 'valid')(X_shortcut)
    X_shortcut = tf.keras.layers.BatchNormalization(axis = 3)(X_shortcut, training = training)
    X = tf.keras.layers.Add()([X_shortcut,X])
    X = tf.keras.layers.Activation('relu')(X)
    
    # Adding the last two tensors
    X = tf.keras.layers.Add()([X, X_shortcut])
    X = tf.keras.layers.Activation('relu')(X)
    
    # Returning the output tensor
    return X
# Defining a modified Resnet-50 Model using the Identity and Convolution Blocks. 
def ResNet50(input_shape = (28, 28, 1), classes = 26):
    
    # Defining the input as a tensor with shape input_shape
    X_input = tf.keras.Input(input_shape)
    
    # Zero-Padding
    X = tf.keras.layers.ZeroPadding2D((3, 3))(X_input)
    
    # Stage 1
    X = tf.keras.layers.Conv2D(64, (5, 5), strides = (1, 1))(X)
    X = tf.keras.layers.BatchNormalization(axis = 3)(X)
    X = tf.keras.layers.Activation('relu')(X)
    X = tf.keras.layers.MaxPooling2D((3, 3), strides=(2, 2))(X)

    # Stage 2
    X = convolutional_block(X, f = 3, filters = [64, 64, 256], s = 1)
    X = identity_block(X, 3, [64, 64, 256])
    X = identity_block(X, 3, [64, 64, 256])
    
    # Add an Average Pool Layer
    X = tf.keras.layers.AveragePooling2D((2,2))(X)

    # Output Layer
    X = tf.keras.layers.Flatten()(X)
    X = tf.keras.layers.Dense(classes, activation='softmax')(X)
    
    # Create Model
    model = tf.keras.Model(inputs = X_input, outputs = X)

    return model

⑤ 訓練ResNet-50

下面我們在數據集上,使用 ResNet-50 網路進行訓練

# 初始化模型
model = ResNet50()

# 編譯
model.compile(optimizer="adam",metrics=["accuracy"],loss = "sparse_categorical_crossentropy")

# 訓練
history = model.fit(train_x, train_y, validation_data = (test_x, test_y), epochs =10)

得到如下結果

💡 優化效果對比

我們對ResNet-50也繪製訓練過程中準確率和損失函數的變化,如下

# 獲取準確率與損失函數情況
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']

# matplotlib繪製訓練過程中指標的變化狀況
epochs = range(len(acc))

plt.plot(epochs, acc, 'r', label='Training accuracy')
plt.plot(epochs, val_acc, 'b', label='Validation accuracy')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()

plt.plot(epochs, loss, 'r', label='Training Loss')
plt.plot(epochs, val_loss, 'b', label='Validation Loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

對比圖如下:

我們觀察到,從簡單的 CNN 模型換到 ResNet 模型時,測試集的準確率從92% 到 97% 。也說明了,ResNet 的結構確實能夠帶來效果上的提升。

💡 部署與實時測試

在這裡我們做一個簡單的測試,使用 OpenCV 的影片錄製功能,通過 python 收集我們的攝影機的鏡頭採集的影像並進行實時預測。

ShowMeAI給OpenCV工具庫製作了快捷即查即用的 OpenCV 速查表手冊,大家可以點擊查看和下載。

具體的過程是,我們解析捕獲的每一幀影像,將其處理為灰度圖(類似於我們模型的訓練集),在影像中心抓取一個 400*400 像素的正方形區域(參見 x0,x1,y0,y1),將正方形調整為我們最初的 28×28 大小並使用我們的模型進行測試(之前保存到 .h5 文件)。

# 導入工具庫
import keras
import numpy as np
from PIL import Image
import string
import pandas as pd
import tensorflow as tf
# 導入OpenCV
import cv2
from matplotlib import pyplot

# 設定維度
dim = (28, 28) # 影像維度
letters = list(string.ascii_lowercase) # 識別的字母

x0 = 1920 // 2 - 400 # 400px left of center
x1 = 1920 // 2 + 400 # 400px right of center
y0 = 1080 // 2 - 400 # 400px right of center
y1 = 1080 // 2 + 400 # 400px right of center

# 初始化影片捕獲
video=cv2.VideoCapture(0)

cv2.namedWindow('Webcam') # 構建1個窗口
cv2.moveWindow('Webcam',40,30) # 放置窗口

while video.isOpened(): # 只要沒有關掉實時攝影機
    ret,capture = video.read() # 抓取每個影片幀
    cropped = capture[y0:y1, x0:x1] # 截取
    img = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY) # 轉成灰度圖
    img = cv2.GaussianBlur(img, (5, 5), 0) # 影像平滑
    img = cv2.resize(img, dim) # 影像大小縮放
    pyplot.imshow(img, cmap='gray') # 可視化展示圖片
    pyplot.show() # 展示
    img = np.reshape(img, (1,img.shape[0],img.shape[1],1))
    img = tf.cast(img, tf.float32)
    pred=model.predict(img)

    # 可視化實時效果
    cv2.rectangle(capture, (x0,y0),(x1,y1),(255,0,0),2) # 為圖片添加矩形框
    cv2.putText(capture,'{} res50'.format(letters[np.argmax(pred[0])]),(x0+25,y0+50),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,255,0),1) # 預測字母
    cv2.imshow('Webcam', capture) # 展示影片
    
    # 結果輸出
    print(pred)
    print(letters[np.argmax(pred[0])])
    
    # 退出影片輸入
    key = cv2.waitKey(1)
    if key == ord('q'):
        break
video.release()
cv2.destroyAllWindows()

為了更輕鬆地對預估結果查看,我們把將預測的字母顯示在實時畫面上(請參閱下面的 gif 以測試單詞 hello)。

💡 參考資料