前言
用深度學習做圖像分類任務摸索中踩了很多坑,也總結出了一些經驗。現在將一些自己覺得非常實用的模型訓練經驗寫下來作爲記錄,也方便後來者借鑑驗證。
調參經驗
- 模型選擇
通常我會使用一個簡單的CNN模型(這個模型一般包含5個卷積層)將數據扔進去訓練跑出一個baseline,這一步工作主要是爲了驗證數據集的質量。如果這個模型訓練結果很差就不要先調試模型,需要檢查一下你的訓練集數據,看看圖像的質量,圖像標籤是否正確,模型的代碼是否正確等等,否則就是在做無用功,畢竟:garbage in,garbage out。 - 超參數的選擇
調參是項技術活,調得好CVPR,調不好下海搬磚。
通常要選的超參數有卷積核大小和數目,批訓練(batch size)大小,優化函數(optimizer),學習率等等,一般來說卷積核用33或者55,batch szie 用16或者32不會過擬合,optimizer用Adam(學習率建議用論文中默認的,我試過調整Adam的學習率,效果或都沒有默認的好),激活函數用relu這個應該是大家的共識吧。還有就是先跑幾百個epoch看loss的變化趨勢。 - 數據預處理
訓練數據對模型的影響是決定性的,提高訓練數據的質量,就是在提高模型的準確率。
圖像預處理的時候一般我會抽出部分圖像觀察,對圖像中的噪聲進行濾波,圖像標籤要驗證一下,其他的預處理就結合實際情況來看了。一般來說,數據清洗的工作佔比是多於寫模型的工作(通常是7:3)。 - 數據增強
數據增強已經是訓練深度網絡的常規操作了,這味丹藥有利於增加訓練數據量,減少網絡過擬合程度,男女老少,居家旅行必備良藥。
常用的數據增強方法包括:圖像縮放,圖像翻轉,圖像裁剪,圖像色彩的飽和度、亮度和對比度變換。
海康威視在ImageNet上曾經用過PCA Jittering的方法,但是由於這個方法的計算量過大,我沒有在自己的訓練中使用過。他們還使用了有監督的數據增強的方法,有興趣的同學可以研究一下。
- 數據不平衡的處理
如果訓練數據中各類樣本數目差距較大,很有可能會導致部分類別的準確率很低,從根本上解決樣本不平衡的問題就是要把樣本變平衡。
一種是增加樣本少的類別的圖像數目,可以用上述數據增強的方法。
另一種就是直接將樣本多的類別圖像數目減少,可以說是非常簡單粗暴了。
當然,也有人提出類別權重的方法,增加少樣本在訓練時的權重,間接地增強了圖像數目。 - 自己的數據生成器
當任務變得複雜,數據規模變大時,框架提供的接口不能滿足你的需求,這時你需要有自己的data generation function。例如,我使用keras時需要對輸入圖片進行多標籤任務的訓練,而keras本身不包含這樣的接口,所以需要自己實現一個data generation function。通過查看官方文檔和相關接口實現了一個多標籤數據生成器(寫這個數據生成器的時候被官方文檔坑了一次,暫且不表,下次另起一文詳說),代碼如下:
# 訓練集/測試集數據生成器,替換flow_from_directory()
def flow_from_2DList(directory=None, target_size=(256, 256),
color_mode='rgb', classes=None, class_mode='categorical',
batch_size=1, shuffle=True, seed=None, save_to_dir=None,
save_prefix='', save_format='png', follow_links=False,
subset=None, interpolation='nearest'):
"""
A DirectoryIterator yielding tuples of (x, y)
where x is a numpy array containing a batch of images
with shape (batch_size, *target_size, channels) and
y is a numpy array of corresponding labels.
"""
# 每個epoch都要shuffle數據集
random.shuffle(directory)
# 參數初始化
if directory is None: # python函數的默認參數如果是list這種可變類型,
# 需要在函數體內進行初始化,
# 否則會在上次的結果後繼續使用list
directory = [ [ 99999 for x in range(4) ] for y in range(batch_size) ]
list_len = len(directory)
print('\nlength of directory:', list_len, '\n\n')
print('\nbatch_size:', batch_size, '\n\n')
step = list_len//batch_size # 向下取整得到一個epoch需要多少個step
print('\nsetp:',step,'\n\n')
for i in range(step):
# 每行一個記錄讀取訓練/測試數據,返回(x,[y1,y2,y3])
batch_images = []
y_label_age = np.zeros((batch_size, 100))
y_label_sex = np.zeros((batch_size, 2))
y_label_sick = np.zeros((batch_size, 2))
batch_directory = directory[i*batch_size : (i+1)*batch_size].copy()
batch_size_num = 0 # 循環計數器
for record in batch_directory:
file_path = record[0]
image = cv2.imread(file_path)
image = cv2.resize(image, target_size)
batch_images.append(image)
age = record[1]
sex = record[2]
sick = record[3]
# 將age,sex,sick轉換成one-hot編碼
if age != 0:
age -= 1
age = to_categorical(age, num_classes = 100)
sex = to_categorical(sex-1, num_classes = 2)
sick = to_categorical(sick-1, num_classes = 2)
y_label_age[batch_size_num,:] = age
y_label_sex[batch_size_num,:] = sex
y_label_sick[batch_size_num,:] = sick
batch_size_num += 1
batch_images = np.array(batch_images)
y_labels = [y_label_age, y_label_sex, y_label_sick]
data = (batch_images, y_labels)
yield data