這是在kaggle上的一個練習比賽,使用的是ImageNet數據集的子集。
注意,mxnet
版本要高於0.12.1b2017112
。
下載數據集。
1. 數據
1.1 整理數據
將解壓后的數據整理成Gluon能夠讀取的形式,這里我直接使用了zh.gluon.ai教程上的代碼
導入各種庫
import math
import os
import shutil
from collections import Counter
設置一些變量
data_dir = './data'
label_file = 'labels.csv'
train_dir = 'train'
test_dir = 'test'
input_dir = 'train_valid_test'
batch_size = 128
valid_ratio = 0.1
定義整理數據函數
def reorg_dog_data(data_dir, label_file, train_dir, test_dir, input_dir,
valid_ratio):
# 讀取訓練數據標簽。
with open(os.path.join(data_dir, label_file), 'r') as f:
# 跳過文件頭行(欄名稱)。
lines = f.readlines()[1:]
tokens = [l.rstrip().split(',') for l in lines]
idx_label = dict(((idx, label) for idx, label in tokens))
labels = set(idx_label.values())
num_train = len(os.listdir(os.path.join(data_dir, train_dir)))
# 訓練集中數量最少一類的狗的數量。
min_num_train_per_label = (
Counter(idx_label.values()).most_common()[:-2:-1][0][1])
# 驗證集中每類狗的數量。
num_valid_per_label = math.floor(min_num_train_per_label * valid_ratio)
label_count = dict()
def mkdir_if_not_exist(path):
if not os.path.exists(os.path.join(*path)):
os.makedirs(os.path.join(*path))
# 整理訓練和驗證集。
for train_file in os.listdir(os.path.join(data_dir, train_dir)):
idx = train_file.split('.')[0]
label = idx_label[idx]
mkdir_if_not_exist([data_dir, input_dir, 'train_valid', label])
shutil.copy(os.path.join(data_dir, train_dir, train_file),
os.path.join(data_dir, input_dir, 'train_valid', label))
if label not in label_count or label_count[label] < num_valid_per_label:
mkdir_if_not_exist([data_dir, input_dir, 'valid', label])
shutil.copy(os.path.join(data_dir, train_dir, train_file),
os.path.join(data_dir, input_dir, 'valid', label))
label_count[label] = label_count.get(label, 0) + 1
else:
mkdir_if_not_exist([data_dir, input_dir, 'train', label])
shutil.copy(os.path.join(data_dir, train_dir, train_file),
os.path.join(data_dir, input_dir, 'train', label))
# 整理測試集。
mkdir_if_not_exist([data_dir, input_dir, 'test', 'unknown'])
for test_file in os.listdir(os.path.join(data_dir, test_dir)):
shutil.copy(os.path.join(data_dir, test_dir, test_file),
os.path.join(data_dir, input_dir, 'test', 'unknown'))
調用這個函數整理數據集
reorg_dog_data(data_dir, label_file, train_dir, test_dir, input_dir,
valid_ratio)
1.2 載入數據
數據整理好之后,需要載入到gluon中,首先需要定義轉換函數,因為需要模型融合,所以需要兩個輸入,分別經過兩個不同的模型
導入各種包
from mxnet import gluon
from mxnet import image
import numpy as np
from mxnet import nd
在訓練數據里,開啟了一些數據增強,並且做了一些預處理。需要說明的是,這里的圖像均值和方差是ImageNet數據集的。這是因為預訓練模型的數據集是ImageNet,那么我們就必須按照模型訓練的時候處理的方式來處理我們的數據,這樣才能保證最好的效果。圖像尺寸同樣,這里使用兩個不同的尺寸就是為了兩個不同的網絡准備的。
def transform_train(data, label):
im1 = image.imresize(data.astype('float32') / 255, 224, 224)
im2 = image.imresize(data.astype('float32') / 255, 299, 299)
auglist1 = image.CreateAugmenter(data_shape=(3, 224, 224), resize=0,
rand_crop=False, rand_resize=False, rand_mirror=True,
mean=np.array([0.485, 0.456, 0.406]), std=np.array([0.229, 0.224, 0.225]),
brightness=0, contrast=0,
saturation=0, hue=0,
pca_noise=0, rand_gray=0, inter_method=2)
auglist2 = image.CreateAugmenter(data_shape=(3, 299, 299), resize=0,
rand_crop=False, rand_resize=False, rand_mirror=True,
mean=np.array([0.485, 0.456, 0.406]), std=np.array([0.229, 0.224, 0.225]),
brightness=0, contrast=0,
saturation=0, hue=0,
pca_noise=0, rand_gray=0, inter_method=2)
for aug in auglist1:
im1 = aug(im1)
for aug in auglist2:
im2 = aug(im2)
# 將數據格式從"高*寬*通道"改為"通道*高*寬"。
im1 = nd.transpose(im1, (2,0,1))
im2 = nd.transpose(im2, (2,0,1))
return (im1,im2, nd.array([label]).asscalar().astype('float32'))
def transform_test(data, label):
im1 = image.imresize(data.astype('float32') / 255, 224, 224)
im2 = image.imresize(data.astype('float32') / 255, 299, 299)
auglist1 = image.CreateAugmenter(data_shape=(3, 224, 224),
mean=np.array([0.485, 0.456, 0.406]),
std=np.array([0.229, 0.224, 0.225]))
auglist2 = image.CreateAugmenter(data_shape=(3, 299, 299),
mean=np.array([0.485, 0.456, 0.406]),
std=np.array([0.229, 0.224, 0.225]))
for aug in auglist1:
im1 = aug(im1)
for aug in auglist2:
im2 = aug(im2)
# 將數據格式從"高*寬*通道"改為"通道*高*寬"。
im1 = nd.transpose(im1, (2,0,1))
im2 = nd.transpose(im2, (2,0,1))
return (im1,im2, nd.array([label]).asscalar().astype('float32'))
轉換函數定義好之后,就可以載入到gluon里了
batch_size = 32
train_ds = gluon.data.vision.ImageFolderDataset(input_str + train_dir, flag=1,
transform=transform_train)
valid_ds = gluon.data.vision.ImageFolderDataset(input_str + valid_dir, flag=1,
transform=transform_test)
train_valid_ds = gluon.data.vision.ImageFolderDataset(input_str + train_valid_dir,
flag=1, transform=transform_train)
test_ds = gluon.data.vision.ImageFolderDataset(input_str + test_dir, flag=1,
transform=transform_test)
loader = gluon.data.DataLoader
train_data = loader(train_ds, batch_size, shuffle=True, last_batch='keep')
valid_data = loader(valid_ds, batch_size, shuffle=True, last_batch='keep')
train_valid_data = loader(train_valid_ds, batch_size, shuffle=True,
last_batch='keep')
2. 設計網絡
這里為了得到一個更好的名次,並且減少我們的工作量,使用了遷移學習加模型融合。遷移學習就是使用預訓練模型里的特征層,然后再自己填補后面的輸出層。因為與訓練模型都是老司機了,見多識廣,所以這些數據也不在話下。
模型融合這里使用兩個模型,分別是resnet152_v1
和inception_v3
導入各種包
from mxnet import init
from mxnet.gluon import nn
2.1 雙模型合並
為了讓兩個網絡合並,就需要自定義一個合並兩個網絡的層。
在這里,我為每個網絡添加了一個GlobalAvgPool2D
層,這是為了讓兩個網絡輸出的尺寸可以合並。
class ConcatNet(nn.HybridBlock):
def __init__(self,net1,net2,**kwargs):
super(ConcatNet,self).__init__(**kwargs)
self.net1 = nn.HybridSequential()
self.net1.add(net1)
self.net1.add(nn.GlobalAvgPool2D())
self.net2 = nn.HybridSequential()
self.net2.add(net2)
self.net2.add(nn.GlobalAvgPool2D())
def hybrid_forward(self,F,x1,x2):
return F.concat(*[self.net1(x1),self.net2(x2)])
這樣就可以構造出一個特征提取層
def get_features2(ctx):
resnet = gluon.model_zoo.vision.inception_v3(pretrained=True,ctx=ctx)
return resnet.features
def get_features1(ctx):
resnet = gluon.model_zoo.vision.resnet152_v1(pretrained=True,ctx=ctx)
return resnet.features
def get_features(ctx):
features1 = get_features1(ctx)
features2 = get_features2(ctx)
net = ConcatNet(features1,features2)
return net
2.3 輸出層
輸出層比較簡單,兩層全鏈接,中間加了一層Dropout
def get_output(ctx,ParamsName=None):
net = nn.HybridSequential()
with net.name_scope():
net.add(nn.Dense(256, activation="relu"))
net.add(nn.Dropout(.7))
net.add(nn.Dense(120))
if ParamsName is not None:
net.collect_params().load(ParamsName,ctx)
else:
net.initialize(init = init.Xavier(),ctx=ctx)
return net
2.4 連接成一個網絡
有了可以合並兩個網絡的層,還有一個輸出層,那么我們需要將這兩個網絡連接起來。
class OneNet(nn.HybridBlock):
def __init__(self,features,output,**kwargs):
super(OneNet,self).__init__(**kwargs)
self.features = features
self.output = output
def hybrid_forward(self,F,x1,x2):
return self.output(self.features(x1,x2))
這樣就可以構造出一個完整的網絡
def get_net(ParamsName,ctx):
output = get_output(ctx,ParamsName)
features = get_features(ctx)
net = OneNet(features,output)
return net
3. 訓練
有着前面的准備,就可以開始干活了。首先第一步是提取特征,因為是遷移學習,會鎖定特征層。那干脆讓所有訓練數據都過一遍特征網絡,這樣既節約時間,有節省顯存。何樂而不為。
導入各種包
from tqdm import tqdm
import datetime
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
from mxnet import autograd
import mxnet as mx
import pickle
3.1 提取特征
提取特征我們使用上面定義好的特征提取網絡
net = get_features(mx.gpu())
net.hybridize()
def SaveNd(data,net,name):
x =[]
y =[]
print('提取特征 %s' % name)
for fear1,fear2,label in tqdm(data):
fear1 = fear1.as_in_context(mx.gpu())
fear2 = fear2.as_in_context(mx.gpu())
out = net(fear1,fear2).as_in_context(mx.cpu())
x.append(out)
y.append(label)
x = nd.concat(*x,dim=0)
y = nd.concat(*y,dim=0)
print('保存特征 %s' % name)
nd.save(name,[x,y])
SaveNd(train_data,net,'train_r152i3.nd')
SaveNd(valid_data,net,'valid_r152i3.nd')
SaveNd(train_valid_data,net,'input_r152i3.nd')
為了最后輸出提交文件做准備,保存一下需要的東西
ids = ids = sorted(os.listdir(os.path.join(data_dir, input_dir, 'test/unknown')))
synsets = train_valid_ds.synsets
f = open('ids_synsets','wb')
pickle.dump([ids,synsets],f)
f.close()
3.2 載入預訓練后的數據
3.3 訓練模型
訓練之前先把各種參數設置一下
num_epochs = 100
batch_size = 128
learning_rate = 1e-4
weight_decay = 1e-4
pngname='train.png'
modelparams='r152i3.params'
然后載入特征提取后的數據
train_nd = nd.load('train_r152i3.nd')
valid_nd = nd.load('valid_r152i3.nd')
input_nd = nd.load('input_r152i3.nd')
f = open('ids_synsets','rb')
ids_synsets = pickle.load(f)
f.close()
train_data = gluon.data.DataLoader(gluon.data.ArrayDataset(train_nd[0],train_nd[1]), batch_size=batch_size,shuffle=True)
valid_data = gluon.data.DataLoader(gluon.data.ArrayDataset(valid_nd[0],valid_nd[1]), batch_size=batch_size,shuffle=True)
input_data = gluon.data.DataLoader(gluon.data.ArrayDataset(input_nd[0],input_nd[1]), batch_size=batch_size,shuffle=True)
設置訓練函數和loss函數
def get_loss(data, net, ctx):
loss = 0.0
for feas, label in data:
label = label.as_in_context(ctx)
output = net(feas.as_in_context(ctx))
cross_entropy = softmax_cross_entropy(output, label)
loss += nd.mean(cross_entropy).asscalar()
return loss / len(data)
def train(net, train_data, valid_data, num_epochs, lr, wd, ctx):
trainer = gluon.Trainer(
net.collect_params(), 'adam', {'learning_rate': lr, 'wd': wd})
train_loss = []
if valid_data is not None:
test_loss = []
prev_time = datetime.datetime.now()
for epoch in range(num_epochs):
_loss = 0.
for data, label in train_data:
label = label.as_in_context(ctx)
with autograd.record():
output = net(data.as_in_context(ctx))
loss = softmax_cross_entropy(output, label)
loss.backward()
trainer.step(batch_size)
_loss += nd.mean(loss).asscalar()
cur_time = datetime.datetime.now()
h, remainder = divmod((cur_time - prev_time).seconds, 3600)
m, s = divmod(remainder, 60)
time_str = "Time %02d:%02d:%02d" % (h, m, s)
__loss = _loss/len(train_data)
train_loss.append(__loss)
if valid_data is not None:
valid_loss = get_loss(valid_data, net, ctx)
epoch_str = ("Epoch %d. Train loss: %f, Valid loss %f, "
% (epoch,__loss , valid_loss))
test_loss.append(valid_loss)
else:
epoch_str = ("Epoch %d. Train loss: %f, "
% (epoch, __loss))
prev_time = cur_time
print(epoch_str + time_str + ', lr ' + str(trainer.learning_rate))
plt.plot(train_loss, 'r')
if valid_data is not None:
plt.plot(test_loss, 'g')
plt.legend(['Train_Loss', 'Test_Loss'], loc=2)
plt.savefig(pngname, dpi=1000)
net.collect_params().save(modelparams)
接下來就可以訓練了
softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()
ctx = mx.gpu()
net = get_output(ctx)
net.hybridize()
train(net, train_data,valid_data, num_epochs, learning_rate, weight_decay, ctx)
輸出測試結果
訓練之后,就可以把測試集的數據跑出來了
首先定義一些變量
netparams = 'r152i3.params'
csvname = 'kaggle.csv'
ids_synsets_name = 'ids_synsets'
f = open(ids_synsets_name,'rb')
ids_synsets = pickle.load(f)
f.close()
從原始圖像載入數據,並定義測試輸出函數
test_ds = vision.ImageFolderDataset(input_str + test_dir, flag=1,
transform=transform_test)
def SaveTest(test_data,net,ctx,name,ids,synsets):
outputs = []
for data1,data2, label in tqdm(test_data):
data1 =data1.as_in_context(ctx)
data2 =data2.as_in_context(ctx)
output = nd.softmax(net(data1,data2))
outputs.extend(output.asnumpy())
with open(name, 'w') as f:
f.write('id,' + ','.join(synsets) + '\n')
for i, output in zip(ids, outputs):
f.write(i.split('.')[0] + ',' + ','.join(
[str(num) for num in output]) + '\n')
開跑
net = get_net(netparams,mx.gpu())
net.hybridize()
SaveTest(test_data,net,mx.gpu(),csvname,ids_synsets[0],ids_synsets[1])
最后就可以把輸出的csv
文件提交到kaggle上了。
使用kaggle提供的數據,最后拿到了0.27760
的分數。如果更進一步,那就是用Stanford dogs dataset數據集。
感悟
首先這次的kaggle比賽算是我第一次結束正式的圖像分類比賽,在Gluon論壇里也學到了好多東西。
使用遷移學習的話,那前期就先把數據過一遍特征網絡,省時省力。如需要訓練前面特征網絡的時候,再連起來訓練就可以了。
大多數圖像分類都可以使用預訓練模型進行遷移訓練,因為經過ImageNet的模型都是老司機了,見多識廣。
使用預訓練模型進行遷移學習,那么數據處理要和原模型的一致,比如圖像尺寸,歸一化等。
最后感謝沐神的直播課程,和論壇里的大神楊培文提供的思路和借鑒代碼。
完整代碼: https://github.com/fierceX/Dog-Breed-Identification-Gluon