"纸上得来终觉浅,绝知此事要躬行。"这是老祖宗传下来的一句话,与其对应的一句英文便是"Get your hands dirty",他们都表达同样一个意思,任何事情,只有实践才能够真正出真知,如果只懂嘴上功夫,那么永远无法真正成为大师。老祖宗的智慧是非常大的,这时刻提醒着我们需要亲自动手,要自己踩过一些坑,才能够明白一些原理。为什么要在开篇要说这个呢?因为这正是我用Gluon炼丹中得到的一点启发,我想这也是沐神开设动手学深度学习课程的初衷之一,理论不仅要看,也要自己动手写代码,调参,这样才能真正理解deep learning。
介绍
学习了一段时间沐神的课程,动手学深度学习,也动手调了一下cifar10数据集,这里有一个简单的结果展示,确实感受到了调参的魔力。这里要强烈安利一下gluon的论坛,里面的小伙伴都非常棒,而且aws还为大家参加比赛提供计算支持,这里有cifar10的奖金情况,可以看到基本上参与的人都拿到了至少50刀的aws credit,如果稍微调一下,就能够拿到至少100刀的奖励,也就是说只要不管你有没有GPU,你都可以轻松地玩转kaggle,体会深度学习的魅力,是不是特别棒呢?如果错过了cifar10的比赛,没有关系,现在又开始了新的比赛,ImageNet的子集比赛120狗分类,看到了大家的奖金是不是特别心动呢?赶快来参加吧,既可以玩深度学习,还能顺便赚点aws credit花,何乐而不为呢。
广告说完了,接下来正式进入到炼丹环节。这次要分享的炼丹过程是ai challenger比赛场景分类项目,总的图片是80000张,一共是80个场景分类,70%用于训练集,10%作为验证集,20%作为测试集A和B。
我们可视化其中一张图片如下,大小是(531, 800)。
数据预处理
均值和方差的计算
在定义网络之前,首先我们需要进行数据预处理,首先想到的就是需要做标准化,也就是减去均值除以标准差,所以我们首先要在训练集上进行均值和方差的计算,方法非常简单,遍历每一张图片,然后计算每个channel上的均值和方差即可。
r = 0 # r mean
g = 0 # g mean
b = 0 # b mean
r_2 = 0 # r^2
g_2 = 0 # g^2
b_2 = 0 # b^2
total = 0
for img_name in img_list:
img = mx.image.imread(path + img_name) # ndarray, width x height x 3
img = img.astype('float32') / 255.
total += img.shape[0] * img.shape[1]
r += img[:, :, 0].sum().asscalar()
g += img[:, :, 1].sum().asscalar()
b += img[:, :, 2].sum().asscalar()
r_2 += (img[:, :, 0]**2).sum().asscalar()
g_2 += (img[:, :, 1]**2).sum().asscalar()
b_2 += (img[:, :, 2]**2).sum().asscalar()
r_mean = r / total
g_mean = g / total
b_mean = b / total
r_var = r_2 / total - r_mean ** 2
g_var = g_2 / total - g_mean ** 2
b_var = b_2 / total - b_mean ** 2
数据增强
一个非常好的处理过拟合的方法就是数据增强,这里对训练集使用数据增强分为以下几步。首先我们随机将图片较短的边按比例resize到[256, 480]之间的一个整数,然后在resize之后的图片上做随机crop到(224, 224)的大小,然后在按0.5的概率做随机翻转。对于验证集,我们就简单地将数据resize到(224, 224)。
def transform_train(img):
'''
img is the mx.image.imread object
'''
img = img.astype('float32') / 255
random_shape = int(np.random.uniform() * 224 + 256)
# random samplely in [256, 480]
aug_list = mx.image.CreateAugmenter(
data_shape=(3, 224, 224), resize=random_shape,
rand_mirror=True, rand_crop=True,
mean=np.array([0.4960, 0.4781, 0.4477]),
std=np.array([0.2915, 0.2864, 0.2981]))
for aug in aug_list:
img = aug(img)
img = nd.transpose(img, (2, 0, 1))
return img
def transform_valid(img):
img = img.astype('float32') / 255.
aug_list = mx.image.CreateAugmenter(
data_shape=(3, 224, 224),
mean=np.array([0.4960, 0.4781, 0.4477]),
std=np.array([0.2915, 0.2864, 0.2981]))
for aug in aug_list:
img = aug(img)
img = nd.transpose(img, (2, 0, 1))
return img
数据读入
接下来需要写数据读入,这里gluon和pytorch几乎是一样的,只需要定义一个dataset就好了,比赛的数据集label是放在一个json文件中的,打开之后大概是这样
image_id
是图片名字,url就是图片的网站,不用管,label_id
就是图片的label,知道了这些我们就能够写一个自定义的dataset来读入数据集。
class SceneDataSet(gl.data.Dataset):
def __init__(self, json_file, img_path, transform):
self._img_path = img_path
self._transform = transform
with open(json_file, 'r') as f:
annotation_list = json.load(f)
self._img_list = [[i['image_id'], i['label_id']]
for i in annotation_list]
def __getitem__(self, idx):
img_name = self._img_list[idx][0]
label = np.float32(self._img_list[idx][1])
img = mx.image.imread(os.path.join(self._img_path, img_name))
img = self._transform(img)
return img, label
def __len__(self):
return len(self._img_list)
然后我们可以使用gluon中的DataLoader来构成一个迭代器。
train_data = gl.data.DataLoader(train_set, batch_size=64, shuffle=True, last_batch='keep')
模型训练
定义好了数据预处理和数据读入之后,我们可以定义模型,然后定义好loss函数,epoch数目,学习率,权重衰减等参数就可以开始训练了。
超参数的定义如下。
ctx = mx.gpu(0)
num_epochs = 10
lr = 0.1
wd = 1e-4
lr_decay = 0.1
loss和优化函数定义如下。
criterion = gl.loss.SoftmaxCrossEntropyLoss()
trainer = gl.Trainer(
net.collect_params(), 'sgd', {'learning_rate': lr, 'momentum': 0.9, 'wd': wd})
这里的模型使用了gluon model zoo里面的resnet50。
net = gl.model_zoo.vision.resnet50_v2(classes=80)
net.initialize(init=mx.init.Xavier(), ctx=ctx)
net.hybridize()
初始化参数使用Xavier方法,net.hybridize()
是gluon特有的,可以将动态图转换成静态图加快训练速度。
接着我们就可以开始训练了,训练的主体如下,跟pytorch很类似。
for epoch in range(num_epochs):
for data, label in train_data:
bs = data.shape[0]
data = data.as_in_context(ctx)
label = label.as_in_context(ctx)
with mx.autograd.record():
output = net(data)
loss = criterion(output, label)
loss.backward()
trainer.step(bs)
在每个epoch中进行数据迭代,然后使用as_in_context
将data放到gpu上,然后在mx.autograd.record()
中建立计算图,loss.backward()
反向传播,计算梯度,最后使用trainer.step(bs)
更新参数。
我的显卡是titan x,训练一个epoch大概需要13分钟,我就随便跑了100次作为baseline,没有调过learning rate decay,最后的训练记录结果如下。
可以看到train loss还是很大的,并没有经过充分训练,训练完成之后使用net.save_params('./net.params')
保存模型。
提交结果
我们需要对测试集进行预测,然后提交top3的结果。这里我们采取的策略是取图片四个角和正中心的patch以及他们的镜面对称,一共构成10个patch,每个patch大小都是224,对这10个patch进行预测,然后取10个结果的softmax求和作为最后的结果,完整的代码在文章最后的github地址中。
最后我们将结果提交到官网上就能看到我们的排名了。
因为我们就是简单地跑一下baseline,所以得到的结果并不是特别好,如果想得到更好的结果,可以训练更长的时间,同时使用多个模型做ensemble。
以上就是初步对gluon炼丹体验的小结,得到的结果并不算太好,抛砖引玉,希望大家使用gluon能够在深度学习的世界里面玩得开心。
欢迎查看我的知乎专栏,深度炼丹
欢迎访问我的博客