概述
比赛信息:
- Name: Planet: Understanding the Amazon from Space.
- *Goal: *Use satellite data to track the human footprint in the Amazon rainforest
- Kaggle 比赛地址: https://www.kaggle.com/c/planet-understanding-the-amazon-from-space
- 个人GitHub仓库(存放相关代码):https://github.com/Kinpzz/Kaggle-Planet-Competition
主要工作
进行亚马逊热带雨林卫星图像的分类,其中每张图片可能包含多个标签
进度
Previous work
- May 30th 使用单个vgg16、ResNet提交baseline,发现存在问题
weeks
- Day 1, June 26th Mon. 完成数据集分析
- Day 2, June 27th Tue. 完成天气分类训练
- Day 3, June 28th Wed. 完成校验集划分输出F beta score观察,完成陆地类训练,观察各类精度
- Day 4, June 29th Thurs. 完成阈值搜索,完成初次提交和改进,成绩LB 0.91816
- Day 5, June 30th Fri. 对LB 0.91816预测结果进行分析,发现问题,尝试改进
- Day 6, July 3th Mon. 改用Inception v3模型,使用Keras进行修改和fine-tune
- Day 7, July 4th Tue. 使用旋转、对称方法预处理图像,增加训练样本
- Day 8, July 5th, Wed. 考试,剩余时间在训练Inception v3 模型 (LB 0.91815)
- Day 9, July 6th Thurs. 使用投票的方法,将不同方法预处理的结果进行投票综合, 准备答辩
具体进展
Day 1 : 数据集分析(June 26th. Mon)
一、先前进展
先前采用了一个Vgg 16网络对多个标签同时进行训练,并采用阈值设定的方法来进行标签的筛选,准确率为78%左右。
后将Vgg 16模型改为ResNet并进行上述相同的训练策略,效果不佳,准确率为72%左右
二、问题概览
- 数据集存在的问题
- 数据集不均衡,有的样本量多有的样本量少
- 直接在同一个网络里对多标签进行训练效果不佳、存在天气、地形等标签
- 不同标签之间存在着依赖关系,比如出现cloudy就不会出现其他的标签
- 对应改进方法
- 采用随机批量训练的方法,每次训练都从所训练的类中取相同数目的训练集进行训练,若该标签的训练集不足,则重复。
- 先对样本进行标签分类,针对每个类别建立一个神经网络,观察是单标签还是多标签
- 建立共生矩阵,查看相互之间的关系
- 训练方法:validation set + training set 训练出基础模型,再对基础模型训练一遍training set
- 待改进
- 图片格式jpg和tiff有什么区别?
- 各个类别的阈值设定:他人的最佳实践、动态计算
三、数据集分析
训练样本:
- 数目:40479
- 样例文件名:
train_1.jpg
标签:
Header: image_name,tags
样子行: train_0, haze primary
标签类别: 17类
['haze', 'primary', 'agriculture', 'clear', 'water', 'habitation', 'road', 'cultivation', 'slash_burn', 'cloudy', 'partly_cloudy', 'conventional_mine', 'bare_ground', 'artisinal_mine', 'blooming', 'selective_logging', 'blow_down']
注:具体标签释义、样例图片请参考官方数据集描述
标签组合: 431种
类别:
- 天气:haze、clear、partly_cloudy、cloudy(关系:互斥?)
天气标签 | 数目 |
---|---|
haze | 2679 |
clear | 28431 |
partly_cloudy | 7261 |
cloudy | 2089 |
* 地形:余下13类(关系:非互斥,具体有什么关系?) |
地形标签 | 数目 |
---|---|
primary | 37513 |
agriculture | 12315 |
water | 7411 |
habitation | 3660 |
road | 8071 |
cultivation | 4547 |
slash_burn | 209 |
conventional mine | 100 |
bare_ground | 862 |
artisinal_mine | 339 |
blooming | 332 |
selective_logging | 340 |
blow_down | 101 |
共生矩阵:
陆地的类型与天气的类型应该是两个分析数据的维度,天气不同短时间内基本不会影响陆地的类型,所以打算将两种类别分在不同的网络中进行训练,暂不做相关性考虑。
天气类别共生矩阵(4类)
clear | partly_cloudy | haze | cloudy | |
---|---|---|---|---|
clear | 28431 | 0 | 0 | 0 |
partly_cloudy | 0 | 7261 | 0 | 0 |
haze | 0 | 0 | 2697 | 0 |
cloudy | 0 | 0 | 0 | 2089 |
$$28431+7261+2697+2089=40478$$ 总数 40479,只有一张图片没有天气标签,哪一张?通过运算发现是第24448张。
但是通过观察全部的共生矩阵可以发现,water除了跟cloudy无任何相关性,都与其他的天气情况有过共同出现的情况。
注:共生矩阵显示:cloudy与其他所有类都无任何共同出现的情况,一旦标签有cloudy就不会有其他标签。
water | |
---|---|
clear | 5502 |
partly_cloudy | 1295 |
haze | 613 |
cloudy | 0 |
由此可以推出,可以忽略那张只有water的训练数据,得出天气之间的条件是互斥关系,并且每张图片都应该有一个天气标签。
陆地类共生矩阵(13类)
样本丰富的陆地类别(6类)
<img src=”https://ws1.sinaimg.cn/large/6177e8b1gy1fgyoksvlaej20qy0eomyx.jpg"/ width=”500px”>
可以发现这些样本重叠的标签是比较多的,这说明这些场景出现的数目比较多,且常常伴有同时出现的情况。
样本稀有的陆地类别(7类)
可以发现这几类比较稀有的陆地类之间的相关关系是比较小的,只有artisinal_mine和bare_ground之间同时出现的概率还比较大一点,其他类标签同时出现的概率是比较小的。
样本丰富和稀有的陆地类别
稀有地形标签 | 数目 |
---|---|
slash_burn | 209 |
conventional mine | 100 |
bare_ground | 862 |
artisinal_mine | 339 |
blooming | 332 |
selective_logging | 340 |
blow_down | 101 |
从中可以发现slash_burn、selective_logging、blooming、blow_down总是伴随着primary出现的,其余类别伴随primary出现的概率也非常高,基本可以判断这些陆地类型是在primary rain forest中出现的。还有其余的陆地类型也是,可以见得本次的遥感位置大多拍摄的是雨林中的图片。
jpg图片和tiff图片的区别:
官方说法: tiff图片包含了红外频段信息可以帮助判断一些情况且包含更准确的卫星遥感信息,而jpg图片只包含了相对符合的颜色、结构信息不那么科学严谨。其中jpg图片包含三个通道(R,G,B),而tiff包含了(R,G,B,NIR)四个通道,两种格式的图片都是从raw image中提取而来的。从理论上来说jpg图片应该等同于tiff的(R,G,B)三个通道。
但是从论坛上的反应来看,tiff图片与jpg图片存在着约11%的不一致性,存在位置、颜色的偏移,以及有文件名的错误,需要进行修改。官方也给出了相关说明Test tif vs. jpg - filenames & image shifting
而标签是通过jpg图片来打的,参照其他人实践的结果,未修正错误之前,采用jpg格式训练效果更好。所以目前阶段先只考虑使用jpg图片来做数据集,暂不考虑NIR通道。
四、分析总结
初步设想:划分成天气类和陆地类两类,每一类各在一个网络中进行训练。
其中天气类比较好训练,可以视单标签训练,选出训练中概率最高的一类即可。其中需要相应提升样本量较少者。
陆地类仍然存在着多标签的问题,其中各类伴随着primary的概率很高且primary的样本量最多。考虑将其再划分成两个模型来进行训练 ,一个训练不考虑primary的陆地标签,一个只对primary的陆地标签进行二分类训练。primary类可以相应减少正样本数量,与负样本数量均衡。
五、数据集处理
待完成:筛选出每一类的训练样本名称即可得出含primary、不含primary两类。天气类、陆地类训练样本集合都相同。(因为共存、除一张特殊情况)
六、参考:
- 远程访问Jupyter notebook
- difference between tiff and jpg
- Data analysis
- Getting started with the data - now with docs!
Day2, Day3 : 分类训练
通过上述的分析发现,天气类是比较好训练的,可以看做是一个单标签的训练问题。不需要设定阈值,直接取概率最高者作为预测标签即可。
解决样本不均衡
经过上述的数据分析之后,发现样本仍然存在着较大的不均衡性。这里采用的策略是在随机批量读取数据的时候,均匀地覆盖到每一个类。
每一个次都读取不同类的一张图片。例如每次随机批量训练的随机样本数量为n,通过一个current_sample_index
来记录读取到哪个类的样本,每读取一次下标加一,弱超过样本类别数则归0。而从每一个标签类里进行读取的时候,都从随机取这个标签类里取一个index。这样子在多次训练之后,在样本量较多的类里抽取到重复的样本数量较少,在样本量较少的类里抽取到重复样本数量会较多。(注:在多标签的样本里,可能要避免同一批抽取到同下标的样本,但是影响可能也不大)
1 | class Reader(): |
2 | def __init__(self, config): |
3 | |
4 | self.imgnames = list() |
5 | self.labels = pd.read_csv(config.labels_file, skiprows=[0], usecols=config.usecols, header=None) |
6 | self.labels = np.float32(self.labels.values) |
7 | |
8 | self.imgnames = pd.read_csv(config.labels_file, skiprows=[0], usecols=[0], header=None).values |
9 | |
10 | self.size = len(self.imgnames) |
11 | self.batch_size = config.batch_size |
12 | self.imgs_path = config.imgs_path |
13 | self.lineidx = 0 |
14 | self.lable_tpyes_num = len(config.usecols) |
15 | self.current_sample_index = 0 |
16 | self.current_sample = list() |
17 | for i in xrange(self.lable_tpyes_num): |
18 | self.current_sample.append([index for index, value in enumerate(np.transpose(self.labels)[i]) if value == 0]) |
19 | |
20 | # this method can return the index of every type of sample one by one when fetch random batch |
21 | def get_one_random_balance_index(self): |
22 | rand_index = random.sample(self.current_sample[self.current_sample_index], 1) |
23 | self.current_sample_index = (self.current_sample_index + 1) % self.lable_tpyes_num |
24 | return rand_index |
25 | |
26 | def random_batch(self): |
27 | rand = list() |
28 | for i in xrange(self.batch_size): |
29 | rand.append(self.get_one_random_balance_index()[0]) |
30 | batch_imgnames = list() |
31 | for idx in rand: |
32 | batch_imgnames.append(self.imgnames[idx]) |
33 | batch_labels = self.labels[rand] |
34 | |
35 | img_list = list() |
36 | for imgname in batch_imgnames: |
37 | path = self.imgs_path + imgname[0] +".jpg" |
38 | img = utils.load_image(path) |
39 | img_list.append(img) |
40 | |
41 | batch_imgs = np.reshape(np.stack(img_list), [-1,224,224,3]) |
42 | batch_labels = np.reshape(batch_labels, [-1, self.labels.shape[1]]) |
43 | return batch_imgs, batch_labels |
设定损失函数
这里使用交叉熵损失(Cross-entroy loss)来衡量softmax之后输出的概率分布vgg.prob
与true_out
是否接近
$$
H(p,q)=-\sum_x p(x) \log q(x)
$$
注:
1 | total_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(y_hat, y_true)) |
等同于:
1 | y_hat_softmax = tf.nn.softmax(y_hat) |
2 | total_loss = tf.reduce_mean(-tf.reduce_sum(y_true * tf.log(y_hat_softmax), [1])) |
设定校验集
在使用随机批量训练的时候,直接观察cost的值效果其实不好,因为不同批次的样本cost可能差异较大,这样子我们对模型好坏的观察和把握其实是比较不直观的。所以这里可以划分出校验集,在一定训练次数之后对校验集合的数据进行验证计算,有助于我们直观地比较模型训练的好坏,避免过拟合。做inside test可能会出现过拟合,且需要验证的样本量比较大。
通过校验集衡量出来的模型,可以再对整个训练数据集合(或校验集)进行训练,可以起到一定的提升。
校验集数据,按照一定比例进行抽取,取每类约10%~25%的数据作校验集。
这里通过sklean.model_selection.train_test_split
方法来进行校验集的划分:
1 | # validation set |
2 | # set 10% samples to validation set |
3 | from sklearn.model_selection import train_test_split |
4 | img_set = labels_df['image_name'].values |
5 | labels_set = labels_df[label_list].values |
6 | img_train, img_test, labels_train, labels_test = train_test_split(img_set, labels_set, test_size=0.1, random_state=42) |
将划分好的训练集和校验集输出为新的one hot标签文件
1 | # write train set |
2 | import csv |
3 | with open( './train_validation_v2_bin.csv', 'wb') as f: |
4 | writer = csv.writer(f) |
5 | writer.writerow(['image_name'] + label_list) |
6 | for i in xrange(len(img_train)): |
7 | data = [img_train[i]] + labels_train[i].tolist() |
8 | writer.writerow(data) |
9 | # write validation set |
10 | with open( './validation_train_v2_bin.csv', 'wb') as f: |
11 | writer = csv.writer(f) |
12 | writer.writerow(['image_name'] + label_list) |
13 | for i in xrange(len(img_test)): |
14 | data = [img_test[i]] + labels_test[i].tolist() |
15 | writer.writerow(data) |
由于采用的是随机取样的方法来进行划分,所以每个类的分布基本是按照原先该类别的比例分布的。所以直接采用随机取样的方法即可,不用再对每一类进行单独的按比例取样。
Mean F Score
Kaggle对这次模型的训练结果的评分标准就是Mean F Score,且使用其中F beta score这种定义方法来进行训练。这个定义可以输出一个平均的精度值,1为最高值,0位最低值。Sklearn提供了相关的接口可以用来帮助我们观察模型训练结果的好坏。
参考:
训练
划分校验集训练
天气类:
通过观察校验集的F2 score可以帮助我们来筛选观察是否发生了过拟合,这里使用sklearn.metrics.fbeta_score
来计算F beta score。训练校验集结果如下:
1 | 0 cost: 1.11878 f2_score: 0.625 |
2 | validation_f2_score: 0.730731225296 |
3 | |
4 | 1000 cost: 0.191897 f2_score: 1.0 |
5 | validation_f2_score: 0.877470355731 |
6 | |
7 | 2000 cost: 0.0831444 f2_score: 1.0 |
8 | validation_f2_score: 0.923913043478 |
9 | |
10 | 3000 cost: 0.106504 f2_score: 0.875 |
11 | validation_f2_score: 0.937994071146 |
12 | |
13 | 4000 cost: 0.0934441 f2_score: 1.0 |
14 | validation_f2_score: 0.930335968379 |
15 | |
16 | 5000 cost: 0.025127 f2_score: 1.0 |
17 | validation_f2_score: 0.93354743083 |
18 | |
19 | 6000 cost: 0.0247073 f2_score: 1.0 |
20 | validation_f2_score: 0.937994071146 |
21 | |
22 | 7000 cost: 0.100279 f2_score: 1.0 |
23 | validation_f2_score: 0.930088932806 |
24 | |
25 | 8000 cost: 0.205701 f2_score: 0.875 |
26 | validation_f2_score: 0.926136363636 |
27 | |
28 | 9000 cost: 0.232889 f2_score: 0.875 |
29 | validation_f2_score: 0.941205533597 |
30 | |
31 | 10000 cost: 0.094468 f2_score: 1.0 |
32 | validation_f2_score: 0.9375 |
33 | |
34 | 11000 cost: 0.0641046 f2_score: 1.0 |
35 | validation_f2_score: 0.913290513834 |
36 | |
37 | 12000 cost: 0.0119296 f2_score: 1.0 |
38 | validation_f2_score: 0.937252964427 |
39 | |
40 | 13000 cost: 0.0160865 f2_score: 1.0 |
41 | validation_f2_score: 0.933794466403 |
42 | |
43 | 14000 cost: 0.486503 f2_score: 0.875 |
44 | validation_f2_score: 0.938488142292 |
对模型进行了14000次训练,每1000次验证一下校验集的F2 score,发现最后F2 score基本稳定与0.93+, 其中最好一次为9000次训练之后的0.9412,所以做出初步判断,训练3000~4000次,即可到达极值附近。后续再对模型训练的时候只训练3000次左右。
陆地类
仍使用sklearn.metrics.fbeta_score
来计算校验集的f beta score,然后使用sklearn.metrics.accuracy_score
来计算每一类的准确度。
这里暂时未采用搜索最佳阈值的方法,直接采用固定阈值来测试,发现训练一定次数之后可以发现基本稳定在0.85左右,取其中一次输出来观察:
1 | 4000 cost: 0.762351 |
2 | validation_f2_score: 0.859302211777 |
3 | agriculture acy_score: 0.85128458498 |
4 | artisinal_mine acy_score: 0.994071146245 |
5 | bare_ground acy_score: 0.978260869565 |
6 | blooming acy_score: 0.964673913043 |
7 | blow_down acy_score: 0.995800395257 |
8 | conventional_mine acy_score: 0.996541501976 |
9 | cultivation acy_score: 0.903409090909 |
10 | habitation acy_score: 0.933300395257 |
11 | primary acy_score: 0.931571146245 |
12 | road acy_score: 0.927124505929 |
13 | selective_logging acy_score: 0.994565217391 |
14 | slash_burn acy_score: 0.993577075099 |
15 | water acy_score: 0.900691699605 |
具体看每个类,可以发现样本量比较大的agriculture、
反而是比较低,像一些稀有样本量反而准确率更高一些。考虑可能跟阈值设定有关,且正样本量多可能容易误判,在后续调整成搜索阈值之后再作进一步的观察。
不划分校验集训练
通过上面训练出基础模型之后再对整个训练集进行少量训练,包括校验集。精度应该获得少量的提升。
参考
StackOverflow: cost function : cross-entropy and softmax
Day 4:阈值设定
阈值搜索
相比与固定的阈值在空间内搜索最佳的阈值往往可以达到一个更好的效果。这里采用了F beta score的值作为衡量标准,对每一类的阈值进行了搜索。总体来说可以有一个0.01~0.03的提升。
参考了Kaggle上了一篇讨论,该讨论对每一类的阈值做了搜索,但是不是全局空间,未考虑到所有可能的组合方式,有点采用贪婪的思想,先找每一类中最好的阈值,再来找其他类。作者认为,每一类中最好的阈值一定是最好的,无论其他类的阈值如何。但作者还未进行数学上的证明。
我对下面的代码做了一定的修改,添加了阈值搜索的起始值,将阈值搜索的起始值,设为上一次的最优值,再来对每一个类重新寻找阈值的最佳值进行搜索。
1 | def optimise_f2_thresholds(y, p, thres, verbose=True, resolution=100): |
2 | |
3 | type_num = y.shape[1] |
4 | def mf(x): |
5 | p2 = np.zeros_like(p) |
6 | for i in range(type_num): |
7 | p2[:, i] = (p[:, i] > x[i]).astype(np.int) |
8 | score = fbeta_score(y, p2, beta=2, average='samples') |
9 | return score |
10 | |
11 | x = thres |
12 | for i in range(type_num): |
13 | best_i2 = 0 |
14 | best_score = 0 |
15 | for i2 in range(resolution): |
16 | i2 = i2 / float(resolution) |
17 | x[i] = i2 |
18 | score = mf(x) |
19 | if score > best_score: |
20 | best_i2 = i2 |
21 | best_score = score |
22 | x[i] = best_i2 |
23 | if verbose: |
24 | print(i, best_i2, best_score) |
25 | return x |
作者说明:
The arguments
y
andp
are both Nx17 numpy array arrays, where N is the number of rows in the validation set. I wouldn’t recommend using this if you’re not doing validation, as the threshold on the training set could be vastly different. It returns a list of size 17x
, which contains the best thresholds for each class.
作者推荐在校验集上进行最佳阈值的搜索。
效果
1 | validation_f2_score: 0.875932907368 |
2 | acy_score: agriculture 0.889575098814 |
3 | acy_score: artisinal_mine 0.97233201581 |
4 | acy_score: bare_ground 0.970849802372 |
5 | acy_score: blooming 0.992588932806 |
6 | acy_score: blow_down 0.995800395257 |
7 | acy_score: conventional_mine 0.994318181818 |
8 | acy_score: cultivation 0.892045454545 |
9 | acy_score: habitation 0.903162055336 |
10 | acy_score: primary 0.933053359684 |
11 | acy_score: road 0.912055335968 |
12 | acy_score: selective_logging 0.994812252964 |
13 | acy_score: slash_burn 0.993577075099 |
14 | acy_score: water 0.851037549407 |
经过一定次数的训练之后,可以发现校验集的F beta score稳定在0.87左右,有了0.02左右的提升。但是arigiculture, cultivation, primary, road, water, habitation
等丰富的土地类从精度来看仍比较差。
单独拆开丰富陆地类训练F2 beta可达0.88左右。原先单类精度较高的稀有陆地类F2 score的分数反而很低,由此可以推测,是因为之前校验集中稀有样本太少了,负样本过多,导致判断都是负样本,单方面造成精度高,但是实际遇到正样本识别能力仍然很差。
所以新增一个校验集合,单独划分出稀有类的校验集和训练集。训练集与校验集各占50%,新增校验集rare_validation_train_v2_bin.csv
, 训练集rare_train_validation_v2_bin.csv
再训练
通过校验集的动态搜索找到一个较好的阈值之后,对全部的样本再进行一定训练。
陆地类分类训练效果不佳
发现陆地类进行分类训练就算加上了动态阈值搜索,仍值在0.87左右。下面采用了对17个类一起训练,vgg 16 + 阈值搜索,就达到了0.91521的成绩,超出预期值。
初步提交(LB 0.91521)
对17个类进行训练
由于分类效果不佳,所以尝试使用了对17个类不分类进行训练,加上阈值搜索,发现得到了比预期要好的结果
采用阈值搜索的方法加上解决样本失衡的方法进行训练,取最后一次推导出来的最佳阈值再对校验集进行一定训练,取最后最佳阈值来进行预测,得到结果:成绩0.91521。相比于之前同样采用vgg训练的0.78的结果,有了大幅度的改进。
改进提交(+0.003 LB)
通过在阈值搜索的过程中,加入对cloudy类的处理,如果判断为cloudy类,则其他类全部设定为0来计算f beta score。因为在上述情况下仍会出现cloudy与其他类同时存在的矛盾情况。这样子就会提升cloudy类判断的阈值门槛,因为如果误判为cloudy类,其他类全部为0,这样子f beta score会很低。从而加入cloudy类与其他类之间的关系。
1 | def optimise_f2_thresholds(y, p, thres, isAll, verbose=True, resolution=100): |
2 | |
3 | type_num = y.shape[1] |
4 | def mf(x): |
5 | p2 = np.zeros_like(p) |
6 | for i in range(type_num): |
7 | p2[:,i] = (p[:,i] > x[i]).astype(np.int) |
8 | # consider relation between classes in all classes |
9 | if isAll: |
10 | for i in range(p2.shape[0]): |
11 | # if cloudy others are all zero |
12 | if p2[i,6] == 1: |
13 | p2[i,:] = np.zeros(p2.shape[1]).astype(np.int) |
14 | p2[i,6] = 1 |
15 | # if slash_burn, blooming, selective_logging, primary are 1 |
16 | #if (p2[i,3] + p2[i,14] + p2[i,15]) > 0: |
17 | # p2[i,13] = 1 |
18 | score = fbeta_score(y, p2, beta=2, average='samples') |
19 | return score |
其余同上述步骤,得到最后最佳阈值thres = [0.07, 0.01, 0.07, 0.05, 0.05, 0.06, 0.48, 0.01, 0.05, 0.05, 0.09, 0.04, 0.08, 0.08, 0.04, 0.02, 0.06]
进行预测。结果:0.91816, 有了0.03的提升。
参考:
Day 5:针对LB0.918结果分析改进
LB0.918结果分析
对0.918的提交结果进行分析,得到如下结果:
1 | muti weather samples number: 6085 |
2 | none weather samples number: 7 |
3 | test_4 cloudy partly_cloudy primary |
4 | test_6 agriculture clear cultivation habitation haze partly_cloudy primary water |
5 | test_9 agriculture clear haze primary |
6 | test_18 clear haze primary |
7 | test_35 clear partly_cloudy primary |
8 | ... |
9 | file_9967 cloudy partly_cloudy primary |
10 | file_9968 cloudy partly_cloudy primary |
11 | file_9975 clear partly_cloudy primary |
问题
发现仍存在存在着一些问题:
- cloudy与其他类别同时出现
- 有的预测结果有多个天气标签,与之前数据分析不符
- 有的预测结果没有天气标签
以上问题都违反了之前输出分析所得出来的结果,只在阈值搜索的时候加入了对cloudy的考量,但是没有强制规则,下面将探讨如何针对以上问题进行改进。
困难
在改进过程中也发现其实存在着一定的困难:
- 若设置cloudy为1,则其他类强制设为0,cloudy误判的时候对整体的影响结果太大
- 如何选出唯一一个天气类,17类带阈值一起训练,单纯取最大似乎不可取
- 分出天气类进行单独训练,效果不够好,F beta score 最高也只接近0.94,综合起来效果可能更差
尝试解决
Strategy 1:
在搜索阈值空间和预测的时候加入一定的策略:
- 从天气类中,选出一类大于其阈值最多的类,作为该类的天气类
- 若在上述天气步骤中判断为cloudy,则其他标签均设为0
- 若陆地类全为0,则该天气类应设为cloudy
从目前训练的结果来看:
- 对训练集训练,校验集的f2 score可以达到0.92+。
- 对校验集训练,校验集的f2 score可以达到0.93+(inside test容易过拟合)。
提交结果:目前仍未超过0.918,无改进,考虑更换其他网络结构,如ResNet50
TODO: 提高训练速度,校验集label只读取一次保存起来,不用每次做校验都重新读取
Day 6: Keras + Inception_v3
模型更换
经过长时间的vgg 调试发现结果很难提升了,所以打算更换一下网络的模型,同组的同学使用ResNet 50,大概可以到LB 0.927。所以我考虑用一种不一样的模型,最后决定先尝试一下Inception_v3 在论文中,Image Net的分类上,它可以可以达到比ResNet 50更好的效果。
由于之前使用的Vgg16,ResNet都是用网上别人写好的tensorflow模板,几乎所有的代码都是源码出现,比如添加层数之类的,修改起来异常繁琐和复杂。后来发现了Keras其实可以以tensorflow为后端进行神经网络的设计,而且官方给了很多API和pre-trained model,很适合做快速的原型验证。用了一个早上的时间就上手了,确实很好用,接口清晰方便,修改起来也相对容易,代码的条理性更强,方便我们做修改。
Inception v3的论文可以参考 Rethinking the Inception Architecture for Computer Vision,在Keras下可以直接当成一个黑盒来使用了,具体的神经网络设计后面再继续了解。先跑起来训练看效果如何。
Keras
Keras的上手主要参考Fine-tune InceptionV3 on a new set of classes,非常简单的几行代码,只要添加上对图片的读取和处理即可使用。
这里使用了fit_generator(self, generator, steps_per_epoch, epochs=1, verbose=1, callbacks=None, validation_data=None, validation_steps=None, class_weight=None, max_queue_size=10, workers=1, use_multiprocessing=False, initial_epoch=0)
来进行训练。
我们可以在generator调用读取图片的方法,最后用yield
使得这个函数变为一个generator。下面是我写的两个generator,一个是随机批量读取一个是顺序批量读取,两个都非常简洁。直接调用我之前在reader中写好的读取图像的方法即可,只是在这里包装为一个生成器。
1 | def random_batch_generator(config): |
2 | reader = Reader(config) |
3 | while True: |
4 | batch_features, batch_labels = reader.random_batch() |
5 | yield batch_features, batch_labels |
6 | def batch_generator(config): |
7 | reader = Reader(config) |
8 | while True: |
9 | batch_features, batch_labels = reader.batch() |
10 | yield batch_features, batch_labels |
模型的保存和读取
当我们训练到一定的次数可以用model.save(path)
的方法将神经网络的结构和权重保存在.h5
文件中,当要重新载入模型和权重进行训练的时候用keras.models.load_model(path)
进行重新读取即可。
训练过程
1 | 500/500 [==============================] - 125s - loss: 4.0212 - val_loss: 4.3960 |
2 | Epoch 2/50 |
3 | 500/500 [==============================] - 120s - loss: 4.0317 - val_loss: 4.3766 |
4 | Epoch 3/50 |
5 | 500/500 [==============================] - 120s - loss: 3.9788 - val_loss: 4.3717 |
6 | Epoch 4/50 |
7 | 500/500 [==============================] - 119s - loss: 4.0052 - val_loss: 4.3587 |
8 | Epoch 5/50 |
9 | 500/500 [==============================] - 119s - loss: 4.0394 - val_loss: 4.3478 |
这里可以看到,在训练过程中,每一个epoch的validation loss都是呈下降趋势,通过观察每个epoch都会对校验集做一次检验,这样就可以清楚地观察到模型有没有过拟合了。比我之前vgg的写法要清楚很多。
参考
- Implement fit_generator( ) in Keras
- Keras: Model class API
- Rethinking the Inception Architecture for Computer Vision
- 如何保存Keras深度学习模型?
Day 7:预处理图像
卫星图像进行过翻转,镜像处理之后,其标签的值应该不会发生改变。将这一点条件加入模型中进行训练,就可以增大我们训练的样本量了,可以帮助模型更好地学到一些与空间位置无关的信息,从而来帮助更好地预测。
这里主要运用了图像的旋转,翻转,镜像等方法。使用了numpy的方法,还有open cv的方法。
1 | def image_preprocess(x, case): |
2 | rows = x.shape[0] |
3 | cols = x.shape[1] |
4 | if case == 1: |
5 | M = cv2.getRotationMatrix2D((rows/2,cols/2), 90, 1) |
6 | x = cv2.warpAffine(x,M,(rows,cols)) |
7 | elif case == 2: |
8 | M = cv2.getRotationMatrix2D((rows/2,cols/2), 180, 1) |
9 | x = cv2.warpAffine(x,M,(rows,cols)) |
10 | elif case == 3: |
11 | M = cv2.getRotationMatrix2D((rows/2,cols/2), 270, 1) |
12 | x = cv2.warpAffine(x,M,(rows,cols)) |
13 | elif case == 4: |
14 | x = np.fliplr(x) |
15 | elif case == 5: |
16 | x = np.flipud(x) |
17 | elif case == 6: |
18 | for i in range(3): |
19 | x[:,:,i] = np.transpose(x[:,:,i]) |
20 | return x |
加入这个策略之后,模型的损失有一定的降低,但是会出现一定的过拟合问题,考虑加入Dropout。
Day 8:Inception v3 模型 (LB 0.91815)
对模型加入了Dropout,Dropout rate使用0.5,然后继续采用对图像预处理的方法进行训练。这里仍然采用随机批量,还有随机预处理的方法。
可以看到训练时候,损失已经有了一定的下降。但是仍非常难训练,损失下降很慢。
1 | Epoch 6/20 |
2 | 500/500 [==============================] - 188s - loss: 3.7685 - val_loss: 4.1716 |
3 | Epoch 7/20 |
4 | 500/500 [==============================] - 193s - loss: 3.7367 - val_loss: 4.1844 |
5 | Epoch 8/20 |
6 | 500/500 [==============================] - 193s - loss: 3.7128 - val_loss: 4.2051 |
7 | Epoch 9/20 |
8 | 500/500 [==============================] - 192s - loss: 3.6974 - val_loss: 4.1923 |
9 | Epoch 10/20 |
10 | 500/500 [==============================] - 192s - loss: 3.6406 - val_loss: 4.1910 |
11 | Epoch 11/20 |
12 | 500/500 [==============================] - 192s - loss: 3.7149 - val_loss: 4.1881 |
提交之后相对于之前单单在训练集上的Inception v3有所改进,提交之后成绩为LB 0.9185 与之前vgg 模型的最好成绩持平,但是仍未突破0.92
Day 9:Voting(LB 0.9218,0.9294)
将不同处理的方法结合进行投票,得到一个综合的结果。得到结果(LB 0.9218)
将不同模型vgg、ResNet、Inception预测结果进行投票。得到结果(LB 0.9299)
结果比平均好,但是相对于训练最好的预测结构ResNet(0.9303)(同组成员所做)仍是有所降低了的。看得出来投票是一种不错的方法。
改进:可以针对每一种不同处理的方法进行训练,可能单种处理方法可以训练得到一个更好的效果。最后再将结果综合起来。