Alexandre Attia是美国卡通“辛普森一家”的狂热粉丝。他看了一系列辛普森电视卡通剧集,然后想建立一个能识别其中人物角色的神经网络。
我们的任务是训练一个卷积神经网络來识别"辛普森一家"的卡通视频的20个角色。
Alexandre仍然持续在建立与更新这个数据集(标签圖片)数据集:辛普森
文件 simpson-set.tar.gz:這是一个图像数据集:20个文件夾(每个字符一個),每个文件夾中有400-2000不等的图像。
文件 simpson-test-set.zip:用來测试模型预测結果的图像数据集 文件 weights.best.h5:已经計算过的权重 文件 annotation.txt:每个字符的边界框的注释文件
训练集包括每个角色約1000张图像。角色在图像像中的位置不一定都是位于中央,有時可能图像中除了主要角色外也会有其他角色(但主角应該是图片中最重要的部分)。
在這篇文章中只會训练识别18个角色(別外2个角色的图像資料仍然在持续收集中)。
数据准备从此处下载图像数据集"the-simpsons-characters-dataset.zip"。
在这个Jupyter Notebook所在的目录下新建一个新的子目录"data"。
解压缩图像数据到"data"的目录里。
将"kaggle_simpson_testset"目录重新命名为"test"。
在"data"的目录里新建子目录"train"。
把压缩包"simpsons_dataset.tar.gz"搬移到"data/train"的目录然后進行解压缩。
最后你的目录结果看起來像这样:
import numpy as np import cv2 import matplotlib.pyplot as plt import pickle import h5py import glob import time from random import shuffle from collections import Counter from sklearn.model_selection import train_test_split import keras from keras.preprocessing.image import ImageDataGenerator from keras.callbacks import LearningRateScheduler, ModelCheckpoint from keras.models import Sequential from keras.layers import Dense, Dropout, Activation, Flatten from keras.layers import Conv2D, MaxPooling2D from keras.optimizers import SGD, Adam数据预处理 (Data Preprocessing)
预处理图片的第一步是调整它们的大小。我们需要有相同大小的所有照片进行训练。
我会将数据资料转换型別为float32來节省一些ram用量并对它们进行归一化(除以255)。
然后,我用数字代替每一個卡通每一個角色的名称,并感謝Keras,我可以快速地将這些类別装换为向量:
# 卡通角色的Label-encoding
map_characters = {0: 'abraham_grampa_simpson', 1: 'apu_nahasapeemapetilon', 2: 'bart_simpson',
3: 'charles_montgomery_burns', 4: 'chief_wiggum', 5: 'comic_book_guy', 6: 'edna_krabappel',
7: 'homer_simpson', 8: 'kent_brockman', 9: 'krusty_the_clown', 10: 'lisa_simpson',
11: 'marge_simpson', 12: 'milhouse_van_houten', 13: 'moe_szyslak',
14: 'ned_flanders', 15: 'nelson_muntz', 16: 'principal_skinner', 17: 'sideshow_bob'}
img_width = 42
img_height = 42
num_classes = len(map_characters) # 要辨識的角色種類
pictures_per_class = 1000 # 每個角色會有接近1000張訓練圖像
test_size = 0.15
imgsPath = "data/train"
# 將訓練資料圖像從檔案系統中取出並進行
def load_pictures():
pics = []
labels = []
for k, v in map_characters.items(): # k: 數字編碼 v: 角色label
# 把某一個角色在檔案夾裡的所有圖像檔的路徑捉出來
pictures = [k for k in glob.glob(imgsPath + "/" + v + "/*")]
print(v + " : " + str(len(pictures))) # 看一下每個角色有多少訓練圖像
for i, pic in enumerate(pictures):
tmp_img = cv2.imread(pic)
# 由於OpenCv讀圖像時是以BGR (Blue-Green-Red), 我們把它轉置成RGB (Red-Green-Blue)
tmp_img = cv2.cvtColor(tmp_img, cv2.COLOR_BGR2RGB)
tmp_img = cv2.resize(tmp_img, (img_height, img_width)) # 進行大小歸一位
pics.append(tmp_img)
labels.append(k)
return np.array(pics), np.array(labels)
# 取得訓練資料集與驗證資料集
def get_dataset(save=False, load=False):
if load:
# 從檔案系統中載入之前處理保存的訓練資料集與驗證資料集
h5f = h5py.File('dataset.h5','r')
X_train = h5f['X_train'][:]
X_test = h5f['X_test'][:]
h5f.close()
# 從檔案系統中載入之前處理保存的訓練資料標籤與驗證資料集籤
h5f = h5py.File('labels.h5', 'r')
y_train = h5f['y_train'][:]
y_test = h5f['y_test'][:]
h5f.close()
else:
# 從最原始的圖像檔案開始處理
X, y = load_pictures()
y = keras.utils.to_categorical(y, num_classes) # 目標的類別種類數
# 將資料切分為訓練資料集與驗證資料集 (85% vs. 15%)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size)
if save: # 保存尚未進行歸一化的圖像數據
h5f = h5py.File('dataset.h5', 'w')
h5f.create_dataset('X_train', data=X_train)
h5f.create_dataset('X_test', data=X_test)
h5f.close()
h5f = h5py.File('labels.h5', 'w')
h5f.create_dataset('y_train', data=y_train)
h5f.create_dataset('y_test', data=y_test)
h5f.close()
# 進行圖像每個像素值的型別轉換與歸一化處理
X_train = X_train.astype('float32') / 255.
X_test = X_test.astype('float32') / 255.
print("Train", X_train.shape, y_train.shape)
print("Test", X_test.shape, y_test.shape)
return X_train, X_test, y_train, y_test
# 取得訓練資料集與驗證資料集 X_train, X_test, y_train, y_test = get_dataset(save=True, load=False)网络模型 (Model)
现在我们来定义我们的模型架构。我们将使用具有6個卷积层的前馈网络,然后是完全连接的隐藏层。
我们也将在两者之间使用Dropout层来防止网络"过拟合(overfitting)"。
def create_model_six_conv(input_shape):
model = Sequential()
model.add(Conv2D(32, (3, 3), padding='same', activation='relu', input_shape=input_shape))
model.add(Conv2D(32, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Conv2D(64, (3, 3), padding='same', activation='relu'))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Conv2D(128, (3, 3), padding='same', activation='relu'))
model.add(Conv2D(128, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
return model;
#圖像的shape是 (42,42,3)
model = create_model_six_conv((img_height, img_width, 3)) # 初始化一個模型
model.summary() # 秀出模型架構
在训练模型之前,我们需要将模型配置为学习算法并进行编译。我们需要指定:
loss: 损失函数,我们要优化。我们不能使用MSE,因为它是不连续的数值。因此,我们使用:categorical_crossentropy
optimizer: 我们使用标准随机梯度下降(Stochastic gradient descent)与聂士杰洛夫动量(Nesterov momentum)
metric: 由于我们正在处理一个分类问題,我们用度量是accuracy。
# 讓我們先配置一個常用的組合來作為後續優化的基準點
lr = 0.01
sgd = SGD(lr=lr, decay=1e-6, momentum=0.9, nesterov=True)
model.compile(loss='categorical_crossentropy',
optimizer=sgd,
metrics=['accuracy'])
训练 (Training)
現在,我们的模型已經准备好了。在训练期間,我们的模型将进行迭代批量训练,每次的训练图片的大小为batch_size。对于每批次,模型將会计算出梯度(gradient),並自动更新网络的权重。对所有训练集的一次迭代被称为一次的循环(epoch)。训练通常会一直进行到损失收敛于一个常数。
我们将增加一些功能到我们的训练设定:
Learning rate scheduler: 随着训练循环的次数逐渐增加的过程中对权重调整的学习率进行衰減通常有助于让模型学习更好 Model checkpoint: 我们将比比每个epoch的验证准确度并只保存模型表现最好的模型。这对深度学习來說是很有用的設定,因为我們的网络可能在一定数量的训练循环后开始过拟合(overfitting),但是我们需要在整个训练过程中表現最好的模型留下來。
这些设定不是必須的,但它们的確可以提高模型的准确性。这些功能是通過Keras的callback功能來实现的。callback是一組函式,将在训练過程的特定阶段被应用,比如将训练結束。 Keras提供內置的学习速率調度(learning rate scheduling)和模型检查点功能(model checkpointing)。
def lr_schedule(epoch):
return lr*(0.1**int(epoch/10))
batch_size = 32
epochs = 30
history = model.fit(X_train, y_train,
batch_size=batch_size,
epochs=epochs,
validation_data=(X_test, y_test),
shuffle=True,
callbacks=[LearningRateScheduler(lr_schedule),
ModelCheckpoint('model.h5', save_best_only=True)
])
# 透過趨勢圖來觀察訓練與驗證的走向 (特別去觀察是否有"過擬合(overfitting)"的現象)
import matplotlib.pyplot as plt
def plot_train_history(history, train_metrics, val_metrics):
plt.plot(history.history.get(train_metrics),'-o')
plt.plot(history.history.get(val_metrics),'-o')
plt.ylabel(train_metrics)
plt.xlabel('Epochs')
plt.legend(['train', 'validation'])
plt.figure(figsize=(12,4))
plt.subplot(1,2,1)
plot_train_history(history, 'loss','val_loss')
plt.subplot(1,2,2)
plot_train_history(history, 'acc','val_acc')
plt.show()
从以上两个图看来我们的训练模式的表現不錯, 也沒有明显的过拟合(overfitting)的現象。
加载测试数据
让我们加载测试数据并评估我们的模型
import os
from pathlib import PurePath # 處理不同作業系統file path的解析問題 (*nix vs windows)
# 載入要驗證模型的數據
def load_test_set(path):
pics, labels = [], []
reverse_dict = {v:k for k,v in map_characters.items()}
for pic in glob.glob(path+'*.*'):
char_name = "_".join(os.path.basename(pic).split('_')[:-1])
if char_name in reverse_dict:
temp = cv2.imread(pic)
temp = cv2.cvtColor(temp, cv2.COLOR_BGR2RGB)
temp = cv2.resize(temp, (img_height,img_width)).astype('float32') / 255.
pics.append(temp)
labels.append(reverse_dict[char_name])
X_test = np.array(pics)
y_test = np.array(labels)
y_test = keras.utils.to_categorical(y_test, num_classes) # 進行one-hot編碼
print("Test set", X_test.shape, y_test.shape)
return X_test, y_test
imgsPath = "data/test/"
#載入數據
X_valtest, y_valtest = load_test_set(imgsPath)
计算预测的正確率
# 預測與比對
from keras.models import load_model
# 把訓練時val_loss最小的模型載入
model = load_model('model.h5')
# 預測與比對
y_pred = model.predict_classes(X_valtest)
acc = np.sum(y_pred==np.argmax(y_valtest, axis=1))/np.size(y_pred)
print("Test accuracy = {}".format(acc))
96%的正确率 ~~ 太神奇了!!
图像增強 (Data Augmentation)在原有的数据集的训练里, 我們达到了将近96.7%的准确率。让我们试着使用图像增強的手法來看看是否可以再提高一些效能。
让我们直接使用keras的內置功能來完成数据增強 (Data Augmentation)。
from keras.preprocessing.image import ImageDataGenerator
datagen = datagen = ImageDataGenerator(
featurewise_center=False, # set input mean to 0 over the dataset
samplewise_center=False, # set each sample mean to 0
featurewise_std_normalization=False, # divide inputs by std of the dataset
samplewise_std_normalization=False, # divide each input by its std
zca_whitening=False, # apply ZCA whitening
rotation_range=10, # randomly rotate images in the range (degrees, 0 to 180)
width_shift_range=0.1, # randomly shift images horizontally (fraction of total width)
height_shift_range=0.1, # randomly shift images vertically (fraction of total height)
horizontal_flip=True, # randomly flip images
vertical_flip=False) # randomly flip images
datagen.fit(X_train)
# 保存在訓練過程中比較好的模型
filepath="model-dtaug.h5"
# 保留"val_acc"最好的那個模型
checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=0, save_best_only=True, mode='max')
# 定義學習率遞減的原則
def lr_schedule(epoch):
return lr*(0.1**int(epoch/10))
#圖像的shape是 (42,42,3)
model = create_model_six_conv((img_height, img_width, 3)) # 初始化一個模型
lr = 0.01
sgd = SGD(lr=lr, decay=1e-6, momentum=0.9, nesterov=True)
model.compile(loss='categorical_crossentropy',
optimizer=sgd,
metrics=['accuracy'])
callbacks_list = [LearningRateScheduler(lr_schedule) ,checkpoint]
history = model.fit_generator(datagen.flow(X_train, y_train, batch_size=batch_size),
steps_per_epoch=X_train.shape[0] // batch_size,
epochs=epochs,
validation_data=(X_test, y_test),
callbacks=callbacks_list)
计算预测的正確率
# 預測與比對
from keras.models import load_model
# 把訓練時val_loss最小的模型載入
model = load_model('model-dtaug.h5')
# 預測與比對
y_pred = model.predict_classes(X_valtest)
acc = np.sum(y_pred==np.argmax(y_valtest, axis=1))/np.size(y_pred)
print("Test accuracy = {}".format(acc))
将预测結果可视化
預測與比對
from keras.models import load_model
from mpl_toolkits.axes_grid1 import AxesGrid
model = load_model('model.h5')
F = plt.figure(1, (15,20))
grid = AxesGrid(F, 111, nrows_ncols=(4, 4), axes_pad=0, label_mode="1")
for i in range(16):
char = map_characters[i]
image = cv2.imread(np.random.choice([k for k in glob.glob('data/test/*.*') if char in k]))
img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
pic = cv2.resize(image, (42, 42)).astype('float32') / 255.
a = model.predict(pic.reshape(1, 42, 42,3))[0]
actual = char.split('_')[0].title()
text = sorted(['{:s} : {:.1f}%'.format(map_characters[k].split('_')[0].title(), 100*v) for k,v in enumerate(a)],
key=lambda x:float(x.split(':')[1].split('%')[0]), reverse=True)[:3]
img = cv2.resize(img, (352, 352))
cv2.rectangle(img, (0,260),(215,352),(255,255,255), -1)
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(img, 'Actual : %s' % actual, (10, 280), font, 0.7,(0,0,0),2,cv2.LINE_AA)
for k, t in enumerate(text):
cv2.putText(img, t,(10, 300+k*18), font, 0.65,(0,0,0),2,cv2.LINE_AA)
grid[i].imshow(img)
plt.show()
每一个角色的预测正確率
import sklearn
# 使用sklearn的分類報告來看預測結果
y_pred = model.predict(X_test)
print('n', sklearn.metrics.classification_report(np.where(y_test > 0)[1],
np.argmax(y_pred, axis=1),
target_names=list(map_characters.values())), sep='')
混淆矩阵可视化
import seaborn as sns; sns.set()
import pandas as pd
from sklearn.metrics import confusion_matrix
conf_mat = confusion_matrix(np.where(y_test > 0)[1], np.argmax(y_pred, axis=1))
classes = list(map_characters.values())
df = pd.DataFrame(conf_mat, index=classes, columns=classes)
fig = plt.figure(figsize = (10,10))
sns.heatmap(df, annot=True, square=True, fmt='.0f', cmap="Blues")
plt.title('Simpson characters classification')
plt.xlabel('ground_truth')
plt.ylabel('prediction')
plt.show()
我们可以看到,有一些角色与其角色混在一起:
Abraham : 他和Homer(他的儿子)混淆了,这很正常, 儿子长的像爸爸。 Bart : 他与Lisa(他的妹妹)混在一起,可能是因为他们有一些共同的照片:也许我们需要清理一点数据集。总结 (Conclusion)
在这篇文章中有一些个人学习到的一些有趣的重点:
深度学习也可以很生活化
通过自己常接触的、喜欢的或有有趣的图像资料来学习卷积神经网络会更能够掌握这个技术的应用
人人都可以拥有自己的训练图像数据集
数据增强法(Data Augmentation)在這次的训练里并沒有让模型效能好, 或许需要增加训练的epoch



