VGG16遷移學習(他山之石,可以攻玉)_分辨人和馬_使用 TensorFlow Keras 實作 VGG16 遷移學習_part1.圖檔格式轉換前處理&生成自訂數據集
絕大多數教科書或是網上課程、教學文章,都會是拿內建的toy dataset(鳶尾花、手寫字...等)在做CNN訓練分類。不過這十分不接地氣,真實場景是我們會有自己前處理過的圖像資料集,要怎麼把它封裝成tensorflow可以讀取的dataset格式,是我們需掌握的基礎。
這邊就拿這個二元分類的資料集做一個示範筆記
Horses Or Humans Dataset
- 資料集包含 500 張以不同姿勢、出現在不同地點的各類馬匹物種渲染影像,另包含 527 張以不同姿勢、出現在不同地點的人物渲染影像。
- 資料集特別納入男性與女性,以及亞洲人、黑人、南亞裔與白人。驗證集則新增 6 位性別、族裔與姿勢各異的人物,以確保資料涵蓋範圍廣泛。
- 人、馬影像大小是300x300x4(RGBA四個通道)的png圖檔
「他山之石,可以攻玉。」這名句最早出自《詩經·小雅·鶴鳴》,意思是別的山上的石頭,能夠用來打磨玉器或用來製成加工玉石的磨石。既比喻別國的賢才可為本國效力,也比喻能幫助自己改正缺點的人或意見。遷移學習正是如此,透過借用其他模型已經學習到的知識,幫助新模型更有效率地完成新的分類任務。
以下是VGG16原本的神經網路架構,整個神經網路都只使用了1x1、3x3的卷積核。
- 採用了16層(分別為13個卷積層和3個全連接層)。
- 在VGG中,所有使用的卷積、池化的配置都統一固定。
- 卷積配置:3×3的卷積核,填滿1,stride:1
- 池化配置:2×2的池化窗口,stride:2,池化方式:max 。
- 所有隱藏層的激活函數都採用ReLU函數。
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
- 載入已經在 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()
運行結果
測試程式碼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("--------------------------------")
執行結果












留言
張貼留言