用TensorFlow訓練CNN模型判別剪刀石頭布

還記得上次的口罩辨識嗎?在這次的教程中,我們會教你如何運用威盛Pixetto製作「判別剪刀石頭布」教程。透過人工智慧與威盛Pixetto「神經網路辨識」功能,撰寫Python程式碼並且訓練CNN模型,使威盛 Pixetto 正確偵測出剪刀、石頭、布。

這次的教程是運用Python程式撰寫,製作客製化的「神經網路辨識」,你也可以依據自己的需求撰寫生活上或工作所需的應用。那我們就開始吧!

步驟:

  1. 開啟機器學習加速器平台
  2. Python 程式碼撰寫
  3. 連結威盛 Pixetto 至電腦
  4. 上傳Tflite至威盛 Pixetto

步驟 1

首先,進入機器學習加速器平台,登入帳號,並點擊「 Python 」 。

在右上角點選「New」,新增 「Python 3」 檔案。

當開啟完成後,我們就可以進入 Python程式撰寫了!

步驟 2

接下來,我們要進行最辛苦的一部分「撰寫Python程式碼」,共可分為以下步驟:

  1. 匯入套件
  2. 匯入資料
  3. 資料視覺化
  4. 資料處理
  5. 模型訓練
  6. 輸出模型

1. 匯入套件

我們這次會使用到的套件包含:

  • tensorflow==2.2.0
  • tensorflow-addons==0.11.2
  • tensorflow_datasets==4.0.1+nightly
  • matplotlib==3.1.2
  • numpy==1.18.5
import tensorflow as tf
import tensorflow_datasets as tfds
import tensorflow_addons as tfa

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

import gc
gc.collect()

2. 匯入資料

當匯入完套件後,我們將使用 tensorflow_datasets 裡的 rock_paper_scissors 資料集。利用tfds.load()匯入,分別將資料集和資料集資訊存入dataset、dataset_info。

dataset, dataset_info = tfds.load(
    name='rock_paper_scissors',
    data_dir='tmp',
    with_info=True,
    as_supervised=True,
)
dataset_train = dataset['train']
dataset_test = dataset['test']

我們可以檢視rock_paper_scissors 資料集的詳細資料:

並將rock_paper_scissors 資料集的重要資訊把它print出來:
可以看到資料集是由 (300, 300, 3) 的圖片檔組成。訓練資料集有 2,520 張圖片,測試資料集有 372 張圖片。總共有3種類別:石頭、布、剪刀。

train_size = dataset_info.splits['train'].num_examples
test_size = dataset_info.splits['test'].num_examples
dataset_classes = dataset_info.features['label'].num_classes

print('dataset name:', dataset_info.name)
print('train dataset:', dataset_train)
print('test dataset:', dataset_test)
print('train dataset size:', train_size)
print('test dataset size:', test_size)
print('number of classes in train and test dataset:', dataset_classes, dataset_info.features['label'].names)
print('shape of images in train and test dataset:', dataset_info.features['image'].shape)

3. 資料視覺化

簡單運用plot_image()觀察其中一張圖片,你也可以嘗試看看改變plot_image()中n的數字,改變觀看的圖片。

def plot_image(n=1):
    for image, label in dataset_train.take(n):
        image = image.numpy()
        label = label.numpy()

    image_label = dataset_info.features['label'].int2str(label)

    plt.imshow(image)
    plt.title(image_label)
    plt.colorbar()
    
plot_image(5)
gc.collect()

一張一張看太慢了嗎?不如我們一次看5張!

這也會是我們之後再做資料處理後,會使用的視覺化方式。

def plot_dataset(dataset, num=5):
    plt.figure(figsize=(15, 15))
    plot_index = 0
    for image, label in dataset.take(num):
        image = image.numpy()
        label = label.numpy()
        
        image_label = dataset_info.features['label'].int2str(label)
        
        plot_index+=1
        plt.subplot(3, 5, plot_index)
        plt.title(image_label)
        plt.imshow(image)

plot_dataset(dataset_train, 15)
gc.collect()

你也可以依據喜好選擇顯示5、10、15張。

4. 資料處理

因為訓練成本的關係,我們必須透過資料處理改變圖片的大小,這裡我們將圖片縮小至(64, 64, 3)。同時設置批次大小為64。

batch_size = 64
image_size = 64

def format_image(image, label):
    image = tf.cast(image, tf.float32)
    image = tf.image.resize(image, (image_size, image_size))
    image /= 255
    return image, label

dataset_train = dataset_train.map(format_image)
dataset_test = dataset_test.map(format_image)

# Explore preprocessed training dataset images.
plot_dataset(dataset_train)

接下來,進入最重要的圖片預處理;首先要製作圖片處理函數,分為以下部分:

  • 圖片轉置:將圖片隨機轉置
  • 圖片翻轉:將圖片隨機上下、左右翻轉
  • 圖片旋轉:隨機旋轉圖片0-360度
  • 圖片顏色調整:隨機改變圖片飽和度、亮度、對比度、色相
  • 圖片顏色反轉:將圖片顏色隨機反轉,為了增加機器學習模型的泛化能力,我們增加一點noise(20%),避免過擬合
  • 圖片縮放:將圖片隨機縮放
def image_transpose(image):
    rand = tf.random.uniform(shape=[], minval=0.0, maxval=1.0, dtype=tf.float32) 
    image = tf.cond(rand < 0.5, 
                    lambda: tf.identity(image), 
                    lambda: tf.image.transpose(image)) 
    return image

def image_flip(image: tf.Tensor) -> tf.Tensor:
    image = tf.image.random_flip_left_right(image)
    image = tf.image.random_flip_up_down(image)
    return image

def image_rotate(image):
    image = tf.image.rot90(image, tf.random.uniform(shape=[], minval=0, maxval=4, dtype=tf.int32))
    rand = tf.random.uniform(shape=[], minval=0.0, maxval=1.0, dtype=tf.float32) 
    def random_rotate(image):
        image = tfa.image.rotate(
            image, tf.random.uniform(shape=[], minval=0 * np.pi / 180, maxval=360 * np.pi / 180, dtype=tf.float32))
        return image
    
    image = tf.cond(rand < 0.5, 
                    lambda: tf.identity(image), 
                    lambda: random_rotate(image)) 
    return image  

def image_color(image: tf.Tensor) -> tf.Tensor:
    image = tf.image.random_saturation(image, lower=0.5, upper=3)
    image = tf.image.random_brightness(image, max_delta=0.2)
    image = tf.image.random_contrast(image, lower=0.8, upper=1)
    image = tf.image.random_hue(image, max_delta=0.03)
    image = tf.clip_by_value(image, clip_value_min=0, clip_value_max=1)
    return image

def image_inversion(image: tf.Tensor) -> tf.Tensor:
    rand = tf.random.uniform(shape=[], minval=0.0, maxval=1.0, dtype=tf.float32)
    image = tf.cond(rand < 0.8, 
                    lambda: tf.identity(image), 
                    lambda: tf.math.add(tf.math.multiply(image, -1), 1))
    return image

def image_zoom(image: tf.Tensor, min_zoom=0.8, max_zoom=1.0) -> tf.Tensor:
    image_width, image_height, image_colors = image.shape
    crop_size = (image_width, image_height)

    # Generate crop settings, ranging from a 1% to 20% crop.
    scales = list(np.arange(min_zoom, max_zoom, 0.01))
    boxes = np.zeros((len(scales), 4))

    for i, scale in enumerate(scales):
        x1 = y1 = 0.5 - (0.5 * scale)
        x2 = y2 = 0.5 + (0.5 * scale)
        boxes[i] = [x1, y1, x2, y2]

    def random_crop(img):
        # Create different crops for an image
        crops = tf.image.crop_and_resize(
            [img],
            boxes=boxes,
            box_indices=np.zeros(len(scales)),
            crop_size=crop_size
        )
        # Return a random crop
        return crops[tf.random.uniform(shape=[], minval=0, maxval=len(scales), dtype=tf.int32)]

    choice = tf.random.uniform(shape=[], minval=0., maxval=1., dtype=tf.float32)

    # Only apply cropping 50% of the time
    return tf.cond(choice < 0.5, lambda: image, lambda: random_crop(image))

將所有的函數統整後,利用map()就可以對整個資料集進行處理了。

def augment_data(image, label):
    image = image_flip(image)
    image = image_color(image)
    image = image_zoom(image)
    image = image_transpose(image)
    image = image_inversion(image)
    image = image_rotate(image)
    return image, label

dataset_train_augmented = dataset_train.map(augment_data)

plot_dataset(dataset_train_augmented)
gc.collect()

5. 模型訓練

在正式訓練模型之前,我們必須將資料進行打亂,增加樣本的隨機性。先前批次大小設置為64,buffer_size設置為推薦的tf.data.experimental.AUTOTUNE。

dataset_train_batches = dataset_train_augmented.shuffle(
    buffer_size=train_size).batch(batch_size=batch_size).prefetch(buffer_size=tf.data.experimental.AUTOTUNE)
dataset_test_batches = dataset_test.batch(batch_size)

print(dataset_train_batches)
print(dataset_test_batches)

最後,我們進行CNN模型建立。

TensorFlow的模型建立主要分為兩種,一種是直接在Sequential API中設定不同的卷積層,另一種是利用add()加入不同的卷積層。這裡我們採用第一種。
以下列出不同的卷積層:

  • tf.keras.layers.Convolution2D()
  • tf.keras.layers.MaxPooling2D()
  • tf.keras.layers.Dense()
  • tf.keras.layers.Flatten()
  • tf.keras.layers.Dropout()
model = tf.keras.Sequential([
    tf.keras.layers.Convolution2D(input_shape=(image_size, image_size, 3), filters=64, kernel_size=3, activation='relu'),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Convolution2D(input_shape=(image_size, image_size, 3), filters=64, kernel_size=3, activation='relu'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Convolution2D(input_shape=(image_size, image_size, 3), filters=128, kernel_size=3, activation='relu'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Convolution2D(input_shape=(image_size, image_size, 3), filters=128, kernel_size=3, activation='relu'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(units=512, activation=tf.keras.activations.relu),
    tf.keras.layers.Dense(units=dataset_classes, activation=tf.keras.activations.softmax)
])

我們可以利用model.summary查看完整的CNN模型。除了呈現不同層神經網路以外,還可以查看總參數為2,621,507。

model.summary()

我們採用的優化器是RMSprop,設置完成後,將模型加入compiler。

這裡我們利用訓練資料集大小除去批次大小來獲得我們的訓練回合,回合控制我們模型訓練速度。除此之外,我們也計算出驗證訓練的測試回合。

值得注意的是,我們有設置early stopping的功能,在訓練超過5回val_accuracy沒有提升,模型就會停止訓練。

rmsprop_optimizer = tf.keras.optimizers.RMSprop(learning_rate=0.001)

model.compile(optimizer=rmsprop_optimizer,
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

steps_per_epoch = train_size // batch_size
validation_steps = test_size // batch_size

print('steps_per_epoch:', steps_per_epoch)
print('validation_steps:', validation_steps)

early_stopping = tf.keras.callbacks.EarlyStopping(patience=5, monitor='val_accuracy')

這裡我們開始訓練過程,經過了15回合的訓練可以發現:

  • loss 降至0.2103
  • accuray提升至0.9287
  • val_loss降至0.1569
  • val_accuracy提升至0.9500
training_history = model.fit(x=dataset_train_batches.repeat(),
                             validation_data=dataset_test_batches.repeat(),
                             epochs=15, 
                             steps_per_epoch=steps_per_epoch,
                             validation_steps=validation_steps,
                             callbacks=[early_stopping],
                             verbose=1)

大家可能會覺得很意外,訓練accuracy回合14到15下降了一個百分點,這樣不是模型預測變差了嗎?但值得注意的是val_accuracy卻因此獲得顯著提升(5個百分點)。

這部分也可以從視覺化圖形觀察出相同的概念。左邊的圖表顯示在回合15時訓練loss達到最低點,同時和測試loss重合。右邊的圖表accuracy也有異曲同工之妙的概念。

def plot_training_history(training_history):
    loss = training_history.history['loss']
    val_loss = training_history.history['val_loss']

    accuracy = training_history.history['accuracy']
    val_accuracy = training_history.history['val_accuracy']

    plt.figure(figsize=(18, 6))

    plt.subplot(1, 2, 1)
    plt.title('Training and Test Loss')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.plot(loss, label='Training set')
    plt.plot(val_loss, label='Test set', linestyle='--')
    plt.legend()
    plt.grid(linestyle='--', linewidth=1, alpha=0.5)

    plt.subplot(1, 2, 2)
    plt.title('Training and Test Accuracy')
    plt.xlabel('Epoch')
    plt.ylabel('Accuracy')
    plt.plot(accuracy, label='Training set')
    plt.plot(val_accuracy, label='Test set', linestyle='--')
    plt.legend()
    plt.grid(linestyle='--', linewidth=1, alpha=0.5)

    plt.show()

plot_training_history(training_history)
gc.collect()
train_loss, train_accuracy = model.evaluate(dataset_train.batch(batch_size).take(train_size))
test_loss, test_accuracy = model.evaluate(dataset_test.batch(batch_size).take(test_size))

print('Training Loss: ', train_loss)
print('Training Accuracy: ', train_accuracy)
print('Test Loss: ', test_loss)
print('Test Accuracy: ', test_accuracy)

6. 輸出模型

完成訓練後我們就可以存取CNN模型了!這裡存成能與威盛 Pixetto 相容的Tflite。大家可以看出這裡分別存取了兩個模型,以下簡單說明差異:

  • model.tflite是最原始的model,因為有10.5MB所以我們要把它縮小
  • quant_model.tflite是縮小版的model.tflite,運用Quantization演算法來縮小資料格式,在可接受範圍內,達到儲存記憶體優化效果,這裡檔案大小轉為2.63MB
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open('model.tflite', 'wb') as f:
    f.write(tflite_model)

converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_quant_model = converter.convert()
with open('quant_model.tflite', 'wb') as f:
    f.write(tflite_quant_model)

這樣複雜的模型建立就完成囉!

趕緊將quant_model.tflite下載至電腦中,準備上傳至威盛 Pixetto 。

步驟 3

接下來我們要將威盛 Pixetto 連結至電腦。

使用Micro USB 2.0傳輸線將威盛Pixetto連至個人電腦。當看到綠色、藍色、紅色LED燈點亮時,代表威盛Pixetto已成功連結。

別忘記將鏡頭蓋子取下!

步驟 4

上傳Tflite至威盛 Pixetto 的步驟和先前幾次的教程大同小異,只不過這次我們將會使用「神經網路辨識」功能。

首先,打開威盛 Utility,在功能區選擇「神經網路辨識」。

接下來,選擇下方「模型路徑」,上傳quant_model.tflite。完成後記得點選確認鍵。

上傳完成後,我們要改變標籤名稱。

點選左上角「工具」中的「標籤編輯」,分別在索引0、1、2,輸入石頭、布、剪刀。

Reference:

Rock Paper Scissors (MobilenetV2)

實際操作:

當完成以上步驟,就是開始玩「判別剪刀石頭布辨識」的時候!

將威盛Pixetto對準自己的手,開始出拳吧!你也可以將這項「判別剪刀石頭布」進階應用在自己的生活之中!

恭喜你完成了!

祝你玩得愉快,別忘了分享自己的創作至社群上並標註 #VIAPixetto!

你也可以關注威盛Pixetto粉絲專頁獲得更多第一手資訊!

分享貼文!

Share on linkedin
Share on twitter
Share on facebook

Leave a Reply