VGG16遷移學習(他山之石,可以攻玉)_分辨人和馬_使用 TensorFlow Keras 實作 VGG16 遷移學習_part1.圖檔格式轉換前處理&生成自訂數據集

絕大多數教科書或是網上課程、教學文章,都會是拿內建的toy dataset(鳶尾花、手寫字...等)在做CNN訓練分類。不過這十分不接地氣,真實場景是我們會有自己前處理過的圖像資料集,要怎麼把它封裝成tensorflow可以讀取的dataset格式,是我們需掌握的基礎。

這邊就拿這個二元分類的資料集做一個示範筆記

Horses Or Humans Dataset
  • 資料集包含 500 張以不同姿勢、出現在不同地點的各類馬匹物種渲染影像,另包含 527 張以不同姿勢、出現在不同地點的人物渲染影像。
  • 資料集特別納入男性與女性,以及亞洲人、黑人、南亞裔與白人。驗證集則新增 6 位性別、族裔與姿勢各異的人物,以確保資料涵蓋範圍廣泛。
  • 人、馬影像大小是300x300x4(RGBA四個通道)的png圖檔


遷移學習(Transfer Learning)是一種機器學習技術,將你在「某個任務或領域」學到的知識和權重,拿來應用到「另一個相關但不同」的新任務上。

「他山之石,可以攻玉。」這名句最早出自《詩經·小雅·鶴鳴》,意思是別的山上的石頭,能夠用來打磨玉器或用來製成加工玉石的磨石。既比喻別國的賢才可為本國效力,也比喻能幫助自己改正缺點的人或意見。遷移學習正是如此,透過借用其他模型已經學習到的知識,幫助新模型更有效率地完成新的分類任務。


以下是VGG16原本的神經網路架構,整個神經網路都只使用了1x1、3x3的卷積核。
  • 採用了16層(分別為13個卷積層和3個全連接層)。
  • 在VGG中,所有使用的卷積、池化的配置都統一固定。
    • 卷積配置:3×3的卷積核,填滿1,stride:1
    • 池化配置:2×2的池化窗口,stride:2,池化方式:max  。
    • 所有隱藏層的激活函數都採用ReLU函數。

https://datahacker.rs/deep-learning-vgg-16-vs-vgg-19/

3x224x224影像輸入
Convolutional Blocks可分為C1、C2、C3、C4、C5
Fully Connected Layers可分為F6、F7、F8。
C1. conv(3,64)->ReLu->conv(64,64)->ReLu->Pooling
輸出Size:64×112×112
C2. conv(64,128)->ReLu->conv(128,128)->ReLu->Pooling
輸出Size:128×56×56
C3. conv(128,256)->ReLu->conv(256,256)->ReLu->conv(256,256)->ReLu->Pooling
輸出Size:256×28×28
C4. conv(256,512)->ReLu->conv(512,512)->ReLu->conv(512,512)->ReLu->Pooling
輸出Size:512×14×14
C5. conv(512,512)->ReLu->conv(512,512)->ReLu->conv(512,512)->ReLu->Pooling
輸出Size:512×7×7
F6.全連接層,神經元個數4096,激活函數ReLu(使用p=0.5的Dropout)
F7.全連接層,神經元個數4096,激活函數ReLu(使用p=0.5的Dropout)
F8.全連接層,神經元個數1000(即類別的個數)。VGG-16的輸出,也就是將F8的輸出進行soft-max作為最終的輸出。


論文原始對照表格,最常被拿來做研究德是D(16層)、E(19層)




最一開始輸入要求是224*224*3的大小單張圖片。
可以看到13個conv都是3x3最後尾巴的三個fc layer


測試程式碼1.建立一個VGG16網路架構,並載入預訓練權重。
  • 載入已經在 ImageNet 上訓練好的 VGG16,保留其卷積特徵提取能力,使 VGG16 不需要從零開始學習邊緣、紋理、形狀等影像特徵。
  • 移除原本的分類器,建立自己的全連接層進行新任務的分類。(自訂 Classification Head)
  • 原本的 VGG16 用來辨識 ImageNet 的 1,000 個類別。
  • 但在新任務可能只需要辨識兩個類別,例如 人 / 馬,因此保留原本的 1,000 類分類器並不合適。

import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
import tensorflow as tf
def transfer_model(input_shape=(224, 224, 3)):
    vgg16 = tf.keras.applications.vgg16.VGG16(
        include_top=False,  # 不包含頂層(移除原始全連接分類層)
        weights='imagenet',# 載入預訓練權重(已經在 ImageNet 大型影像資料集上訓練過的權重)
input_shape=input_shape ) #這些層中的可訓練權重在後續訓練過程中不會被梯度更新。 for layer in vgg16.layers: layer.trainable = False # 設置VGG16預訓練模型不可訓練 pre_model = vgg16.output # 添加自定義全連接層 x = tf.keras.layers.Flatten()(pre_model) x = tf.keras.layers.Dense(128, activation='relu')(x) x = tf.keras.layers.Dropout(0.3)(x) x = tf.keras.layers.Dense(32, activation='relu')(x) x = tf.keras.layers.Dropout(0.3)(x) x = tf.keras.layers.Dense(2, activation='softmax')(x) # 生成遷移模型 model = tf.keras.models.Model(inputs=vgg16.input, outputs=x) model.summary() return model transfer_model()


輸出結果
在flatten層以前,都是維持原先的VGGNet架構。

後半部就是客製化的全連接層

Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 224, 224, 3)]     0         
                                                                 
 block1_conv1 (Conv2D)       (None, 224, 224, 64)      1792      
                                                                 
 block1_conv2 (Conv2D)       (None, 224, 224, 64)      36928     
                                                                 
 block1_pool (MaxPooling2D)  (None, 112, 112, 64)      0         
                                                                 
 block2_conv1 (Conv2D)       (None, 112, 112, 128)     73856     
                                                                 
 block2_conv2 (Conv2D)       (None, 112, 112, 128)     147584    
                                                                 
 block2_pool (MaxPooling2D)  (None, 56, 56, 128)       0         
                                                                 
 block3_conv1 (Conv2D)       (None, 56, 56, 256)       295168    
                                                                 
 block3_conv2 (Conv2D)       (None, 56, 56, 256)       590080    
                                                                 
 block3_conv3 (Conv2D)       (None, 56, 56, 256)       590080    
                                                                 
 block3_pool (MaxPooling2D)  (None, 28, 28, 256)       0         
                                                                 
 block4_conv1 (Conv2D)       (None, 28, 28, 512)       1180160   
                                                                 
 block4_conv2 (Conv2D)       (None, 28, 28, 512)       2359808   
                                                                 
 block4_conv3 (Conv2D)       (None, 28, 28, 512)       2359808   
                                                                 
 block4_pool (MaxPooling2D)  (None, 14, 14, 512)       0         
                                                                 
 block5_conv1 (Conv2D)       (None, 14, 14, 512)       2359808   
                                                                 
 block5_conv2 (Conv2D)       (None, 14, 14, 512)       2359808   
                                                                 
 block5_conv3 (Conv2D)       (None, 14, 14, 512)       2359808   
                                                                 
 block5_pool (MaxPooling2D)  (None, 7, 7, 512)         0         
                                                                 
 flatten (Flatten)           (None, 25088)             0         
                                                                 
 dense (Dense)               (None, 128)               3211392   
                                                                 
 dropout (Dropout)           (None, 128)               0         
                                                                 
 dense_1 (Dense)             (None, 32)                4128      
                                                                 
 dropout_1 (Dropout)         (None, 32)                0         
                                                                 
 dense_2 (Dense)             (None, 2)                 66        
                                                                 
=================================================================
Total params: 17,930,274
Trainable params: 3,215,586
Non-trainable params: 14,714,688
_________________________________________________________________


測試程式碼2.打印某一張圖片資訊確認

from PIL import Image
from pathlib import Path
image_dir = Path(r'E:\論文研究\遷移學習\horse-or-human\horse-or-human\train\horses')
image_dir = image_dir / 'horse01-0.png'
img = Image.open(image_dir)
print("圖片模式:", img.mode)
print("圖片尺寸:", img.size)
img.show()
img.close()

運行結果

這邊建立一個train2的空目錄,底下要再分別創建horses跟humans的目錄,等回兒要分門別類,存放轉至RGB的人、馬圖檔。



測試程式碼3.將所有訓練圖檔資料從RGBA轉換到RGB

from PIL import Image
import os
import numpy as np

image_dir = r'E:\論文研究\遷移學習\horse-or-human\horse-or-human\train' #原始RGBA圖檔所在路徑
image_dir2 = r'E:\論文研究\遷移學習\horse-or-human\horse-or-human\train2' #預計存放轉成RGB的圖檔
for img_horse in os.listdir(image_dir + '/horses' ):
    img = Image.open(image_dir + '/horses' +'/' + img_horse)
    img2 = img.convert('RGB')
    img2.save(image_dir2 + '/horses' +'/' + img_horse)
    img.close()
    img2.close()

for img_human in os.listdir(image_dir + '/humans' ):
    img = Image.open(image_dir + '/humans' +'/' + img_human)
    img2 = img.convert('RGB')
    img2.save(image_dir2 + '/humans' +'/' + img_human)
    img.close()
    img2.close()

運行效果如下,可明確看到變成RGB只占用24位元深度。



測試程式碼4.將外部圖片資料集轉為tensorflow使用的Dataset格式

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"# 隱藏 CUDA GPU,明確使用 CPU
import tensorflow as tf
# 讀取影像檔案
def read_images(path):
    all_images = []
    # 讀取 horses 影像
    horse_path = path + "/horses/"
    for f in os.listdir(horse_path):
        img_string = tf.io.read_file(horse_path + f)  # 讀取原始檔案
        img_graph = tf.image.decode_png(img_string)  # 解碼 PNG 圖片
        # 調整影像大小,與後續模型的輸入要求一致,並進行標準化
        img_final = tf.image.resize(img_graph, [224, 224]) / 255.0
        all_images += [img_final]
    horse_files_num = len(all_images)
    # 讀取 humans 影像
    human_path = path + "/humans/"
    for f in os.listdir(human_path):
        img_string = tf.io.read_file(human_path + f)  # 讀取原始檔案
        img_graph = tf.image.decode_png(img_string)  # 解碼 PNG 圖片
        # 調整影像大小,並進行標準化
        img_final = tf.image.resize(img_graph, [224, 224]) / 255.0
        all_images += [img_final]
    human_files_num = len(all_images) - horse_files_num

    print('read files:', len(all_images))
    print(horse_files_num, 'horses files in', 'files')
    print(human_files_num, 'humans files in', 'files')
    # 取得所有標籤。標籤使用 One-hot 編碼表示, horse 標籤為 [1, 0],human 標籤為 [0, 1]
    all_labels = [[1,0]] * horse_files_num + [[0,1]] * human_files_num
    return all_images, all_labels

def generate_dataset(data_path,batch_size=8):
    images,labels = read_images(data_path)
    dataset = tf.data.Dataset.from_tensor_slices((images, labels))
    dataset = dataset.shuffle(len(images))#->洗牌打亂,避免前面固定都馬,後面都人。
    dataset = dataset.batch(batch_size)
    dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)
    return dataset

data_path = r"E:\論文研究\遷移學習\horse-or-human\horse-or-human\train2"
dataset_train = generate_dataset(data_path)
# 1. Dataset 本身
print("Dataset:", dataset_train)
print("Type:", type(dataset_train))#Type:<class 'tensorflow.python.data.ops.dataset_ops.PrefetchDataset'>
# 2. 元素規格
print("Element Spec:", dataset_train.element_spec)
# 分別查看影像和標籤的規格
image_spec, label_spec = dataset_train.element_spec
print("Image Shape:", image_spec.shape)
print("Image dtype:", image_spec.dtype)
print("Label Shape:", label_spec.shape)
print("Label dtype:", label_spec.dtype)
print("\n========== Dataset 數量 ==========")

# 3. Dataset 的 Batch 數量
cardinality = dataset_train.cardinality().numpy()
print("Cardinality:", cardinality)
# 4. 逐批統計實際影像數量
total_images = 0
for images, labels in dataset_train:
    total_images += images.shape[0]
print("Total Images:", total_images)




from matplotlib import pyplot as plt
x_train, y_train = read_images(r'E:\論文研究\遷移學習\horse-or-human\horse-or-human\train2')
#0~499 ->都是馬的類別
plt.figure(figsize=(6, 6))
print(x_train[0].shape,y_train[0])
plt.title("Image 0")
plt.imshow(x_train[0])
#500~1026 ->都是人的類別
plt.figure(figsize=(6, 6))
print(x_train[500].shape,y_train[500])
plt.title("Image 500")
plt.imshow(x_train[500])
plt.show()


在tensorflow框架中,具體有這幾個步驟
1.讀取影像檔案,設置Label,準備好的訓練資料與對應標準答案合併。
2.將上一步結果後丟給tf.Dataset.from_tensor_slices轉dataset(適合小型數據集,一次獨入內存。)
  • 可先理解為跟一般python內建的list或numpy的array一樣,在RAM中產一個tensorflow專用的切片資料型別,會沿著數據集元素的第一個維度產生切片。
  • 這邊傳入的是一個tuple型別,也就是將images,labels組成(images,labels)。
  • 如果是大型數據集,建議改用TFRecord格式。
3.前處理(shuffle、batch、pretetch)
  • shuffle:當訓練集中的每筆record相互獨立且分布均勻時,梯度下降效果會最佳。
    因此需要透過shuffle來打亂record的順序,其中buffer_size參數代表打亂時要用的緩存空間大小,通常指定的參數會很直觀設置微數據集總長度。
  • pretetch:可由字面直接翻譯理解叫做「預取」,當我們訓練的演算法過程正在處理某一個批次,數據集已併行工作準備下一批次了。我們可以在CPU中利用多個內核,在訓練時,同時讀取下一批資料,並作轉換。
  • batch:會將多個元素組合成batch,方便模型訓練。

執行結果如下


read files: 1027
500 horses files in files
527 humans files in files
Dataset: <PrefetchDataset element_spec=(TensorSpec(shape=(None, 224, 224, 3), dtype=tf.float32, name=None), TensorSpec(shape=(None, 2), dtype=tf.int32, name=None))>
Type: <class 'tensorflow.python.data.ops.dataset_ops.PrefetchDataset'>
Element Spec: (TensorSpec(shape=(None, 224, 224, 3), dtype=tf.float32, name=None), TensorSpec(shape=(None, 2), dtype=tf.int32, name=None))
Image Shape: (None, 224, 224, 3)
Image dtype: <dtype: 'float32'>
Label Shape: (None, 2)
Label dtype: <dtype: 'int32'>

========== Dataset 數量 ==========
Cardinality: 129
Total Images: 1027
read files: 1027
500 horses files in files
527 humans files in files
(224, 224, 3) [1, 0]
(224, 224, 3) [0, 1]

測試程式碼5.遍歷轉微Tensor slices型別的Dataset

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"# 隱藏 CUDA GPU,明確使用 CPU
import tensorflow as tf
# 讀取影像檔案
def read_images(path):
    all_images = []
    # 讀取 horses 影像
    horse_path = path + "/horses/"
    for f in os.listdir(horse_path):
        img_string = tf.io.read_file(horse_path + f)  # 讀取原始檔案
        img_graph = tf.image.decode_png(img_string)  # 解碼 PNG 圖片
        # 調整影像大小,與後續模型的輸入要求一致,並進行標準化
        img_final = tf.image.resize(img_graph, [224, 224]) / 255.0
        all_images += [img_final]
    horse_files_num = len(all_images)
    # 讀取 humans 影像
    human_path = path + "/humans/"
    for f in os.listdir(human_path):
        img_string = tf.io.read_file(human_path + f)  # 讀取原始檔案
        img_graph = tf.image.decode_png(img_string)  # 解碼 PNG 圖片
        # 調整影像大小,並進行標準化
        img_final = tf.image.resize(img_graph, [224, 224]) / 255.0
        all_images += [img_final]
    human_files_num = len(all_images) - horse_files_num

    print('read files:', len(all_images))
    print(horse_files_num, 'horses files in', 'files')
    print(human_files_num, 'humans files in', 'files')
    # 取得所有標籤。標籤使用 One-hot 編碼表示, horse 標籤為 [1, 0],human 標籤為 [0, 1]
    all_labels = [[1,0]] * horse_files_num + [[0,1]] * human_files_num
    return all_images, all_labels

def generate_dataset(data_path,batch_size=8):
    images,labels = read_images(data_path)
    dataset = tf.data.Dataset.from_tensor_slices((images, labels))
    dataset = dataset.shuffle(len(images))#->洗牌打亂,避免前面固定都馬,後面都人。
    dataset = dataset.batch(batch_size)
    dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)
    return dataset

data_path = r"E:\論文研究\遷移學習\horse-or-human\horse-or-human\train2"
dataset_train = generate_dataset(data_path)
print("Dataset:", dataset_train)
print("-----------------------------------------------------")
# for item in dataset_train:
#     print(item)
#使用 TensorFlow 的 .take(3) 方法,只取出 Dataset 前 3 筆資料。
#由於目前的 dataset_train 已經經過 batch(batch_size=8),所以這邊是指前3個批次的資料,每個批次會涵蓋8張圖資。
for i, (images, labels) in enumerate(dataset_train.take(3)):
    print(f"Batch {i+1}")
    print("Images Shape:", images.shape)
    print("Labels Shape:", labels.shape)
    print("Labels:")
    print(labels.numpy())
    print("--------------------------------")




執行結果


















留言

這個網誌中的熱門文章

何謂淨重(Net Weight)、皮重(Tare Weight)與毛重(Gross Weight)

外貿Payment Term 付款條件(方式)常見的英文縮寫與定義

Architecture(架構) 和 Framework(框架) 有何不同?_軟體設計前的事前規劃的藍圖概念