本文记述了座头鲸挑战赛0.78563提交成绩的算法策略。
本文应该与 Bounding Box模型同时使用,它描述了如何将目标从图像中裁剪出来的策略1。
为了加快运行速度,一些较慢的计算结果已经作为数据集包含在内,而不是重新计算。不过尽管这部分代码不执行,我们依然会将代码提供出来2。
本文的方法是训练一个Siamese网络,稍后会详细介绍一些修改的部分。对精度提升帮助最大的部分是在训练过程中生成图像对。每次训练都使用一系列图像对(A,B),规则如下:
本文记录了提交成绩的所有细节。显然,要涵盖一切,它必须相当长。我鼓励大家直接跳到最感兴趣的地方,而不必经历一切。
本节介绍用于识别重复图像的启发式算法。 训练和测试集具有重复图像的事实是在文档中有提到的。有些图像是完美的二进制复制,二有些则有所改变:对比度和亮度,大小,屏蔽图例等。
如果两个图像符合以下条件,则认为它们是重复的:
1. 两张图片具有相同的感知哈希(phash);或者
2. 两张图片具有:
相差最多6位的phash,并且
具有相同的尺寸,并且
标准化图像之间的像素均方误差低于给定阈值。
字典 p2h 为每张图片关联唯一图像ID(phash),即pic2hash。 字典 h2p 将每个图像id与要用于该哈希的首选图像相关联,即hash2pic。
# 读取图片描述
from pandas import read_csv
tagged = dict([(p,w) for _,p,w in read_csv('../input/whale-categorization-playground/train.csv').to_records()])
submit = [p for _,p,_ in read_csv('../input/whale-categorization-playground/sample_submission.csv').to_records()]
join = list(tagged.keys()) + submit
len(tagged),len(submit),len(join),list(tagged.items())[:5],submit[:5]
(9850,
15610,
25460,
[(‘00022e1a.jpg’, ‘w_e15442c’),
(‘000466c4.jpg’, ‘w_1287fbc’),
(‘00087b01.jpg’, ‘w_da2efe0’),
(‘001296d5.jpg’, ‘w_19e5482’),
(‘0014cfdf.jpg’, ‘w_f22f3e3’)],
[‘00029b3a.jpg’,
‘0003c693.jpg’,
‘000bc353.jpg’,
‘0010a672.jpg’,
‘00119c3f.jpg’])
# 确定每个图像的大小
from os.path import isfile
from PIL import Image as pil_image
from tqdm import tqdm_notebook
def expand_path(p):
if isfile('../input/whale-categorization-playground/train/' + p): return '../input/whale-categorization-playground/train/' + p
if isfile('../input/whale-categorization-playground/test/' + p): return '../input/whale-categorization-playground/test/' + p
return p
p2size = {}
for p in tqdm_notebook(join):
size = pil_image.open(expand_path(p)).size
p2size[p] = size
len(p2size), list(p2size.items())[:5]
(25460,
[(‘00022e1a.jpg’, (699, 500)),
(‘000466c4.jpg’, (1050, 700)),
(‘00087b01.jpg’, (1050, 368)),
(‘001296d5.jpg’, (397, 170)),
(‘0014cfdf.jpg’, (700, 398))])
# 读取或者生成 p2h(picture to hash)
import pickle
import numpy as np
from imagehash import phash
from math import sqrt
# 对所有图像对,如果满足下列条件,则认为是重复的:
# 1) 它们具有相同的模式和大小;
# 2) 在将像素归一化为零均值和一方差之后,均方误差不超过0.1
def match(h1,h2):
for p1 in h2ps[h1]:
for p2 in h2ps[h2]:
i1 = pil_image.open(expand_path(p1))
i2 = pil_image.open(expand_path(p2))
if i1.mode != i2.mode or i1.size != i2.size: return False
a1 = np.array(i1)
a1 = a1 - a1.mean()
a1 = a1/sqrt((a1**2).mean())
a2 = np.array(i2)
a2 = a2 - a2.mean()
a2 = a2/sqrt((a2**2).mean())
a = ((a1 - a2)**2).mean()
if a > 0.1: return False
return True
if isfile('../input/humpback-whale-identification-model-files/p2h.pickle'):
with open('../input/humpback-whale-identification-model-files/p2h.pickle', 'rb') as f:
p2h = pickle.load(f)
else:
# 计算训练和测试集中每个图像的phash。
p2h = {}
for p in tqdm_notebook(join):
img = pil_image.open(expand_path(p))
h = phash(img)
p2h[p] = h
# 查找与给定phash值关联的所有图像。
h2ps = {}
for p,h in p2h.items():
if h not in h2ps: h2ps[h] = []
if p not in h2ps[h]: h2ps[h].append(p)
# 找到所有不同的phash值
hs = list(h2ps.keys())
# 如果图像足够接近,则关联两个phash值 (这部分非常慢: 算法复杂度 n^2 )
h2h = {}
for i,h1 in enumerate(tqdm_notebook(hs)):
for h2 in hs[:i]:
if h1-h2 <= 6 and match(h1, h2):
s1 = str(h1)
s2 = str(h2)
if s1 < s2: s1,s2 = s2,s1
h2h[s1] = s2
# 将相同phash的图像组合在一起,并用字符串格式的phash替换(更快,更可读)
for p,h in p2h.items():
h = str(h)
if h in h2h: h = h2h[h]
p2h[p] = h
len(p2h), list(p2h.items())[:5]
(25460,
[(‘00022e1a.jpg’, ‘b362cc79b1a623b8’),
(‘000466c4.jpg’, ‘b3cccc3331cc8733’),
(‘00087b01.jpg’, ‘bc4ed0f2a7e168a8’),
(‘001296d5.jpg’, ‘93742d9a28b35b87’),
(‘0014cfdf.jpg’, ‘d4a1dab1c49f6352’)])
# 对于每个图像ID,生成图像列表
h2ps = {}
for p,h in p2h.items():
if h not in h2ps: h2ps[h] = []
if p not in h2ps[h]: h2ps[h].append(p)
#注意到25460张图像是如何仅使用20913个不同的图像ID。
len(h2ps),list(h2ps.items())[:5]
(20913,
[(‘b362cc79b1a623b8’, [‘00022e1a.jpg’]),
(‘b3cccc3331cc8733’, [‘000466c4.jpg’]),
(‘bc4ed0f2a7e168a8’, [‘00087b01.jpg’, ‘7c72d707.jpg’]),
(‘93742d9a28b35b87’, [‘001296d5.jpg’]),
(‘d4a1dab1c49f6352’, [‘0014cfdf.jpg’, ‘89c94943.jpg’])])
# 展示一些重复图像
import matplotlib.pyplot as plt
def show_whale(imgs, per_row=2):
n = len(imgs)
rows = (n + per_row - 1)//per_row
cols = min(per_row, n)
fig, axes = plt.subplots(rows,cols, figsize=(24//per_row*cols,24//per_row*rows))
for ax in axes.flatten(): ax.axis('off')
for i,(img,ax) in enumerate(zip(imgs, axes.flatten())): ax.imshow(img.convert('RGB'))
for h, ps in h2ps.items():
if len(ps) > 2:
print('Images:', ps)
imgs = [pil_image.open(expand_path(p)) for p in ps]
show_whale(imgs, per_row=len(ps))
break
Images: [‘0c35fcb4.jpg’, ‘2d6610b9.jpg’, ‘a98bfd97.jpg’]
# 对于每个图像ID,选择首选的图像
def prefer(ps):
if len(ps) == 1: return ps[0]
best_p = ps[0]
best_s = p2size[best_p]
for i in range(1, len(ps)):
p = ps[i]
s = p2size[p]
if s[0]*s[1] > best_s[0]*best_s[1]: # Select the image with highest resolution
best_p = p
best_s = s
return best_p
h2p = {}
for h,ps in h2ps.items(): h2p[h] = prefer(ps)
len(h2p),list(h2p.items())[:5]
(20913,
[(‘b362cc79b1a623b8’, ‘00022e1a.jpg’),
(‘b3cccc3331cc8733’, ‘000466c4.jpg’),
(‘bc4ed0f2a7e168a8’, ‘00087b01.jpg’),
(‘93742d9a28b35b87’, ‘001296d5.jpg’),
(‘d4a1dab1c49f6352’, ‘0014cfdf.jpg’)])
训练前对图像进行以下操作:
我注意到有些照片中鲸鱼的尾巴指向下方而不是往常一样向上。每当我在训练集中遇到这样的实例(而不是在测试集中)时,我会将它添加到列表中。在训练过程中,将这些图像旋转180°使它们向上标注化。这个清单并不详尽,可能还有更多我没注意到的情况。
with open('../input/humpback-whale-identification-model-files/rotate.txt', 'rt') as f: rotate = f.read().split('\n')[:-1]
rotate = set(rotate)
rotate
{‘2b792814.jpg’,
‘2bc459eb.jpg’,
‘3401bafe.jpg’,
‘56fafc52.jpg’,
‘a492ab72.jpg’,
‘d1502267.jpg’,
‘e53d2b96.jpg’,
‘ed4f0cd5.jpg’,
‘f2ec136c.jpg’,
‘f966c073.jpg’}
def read_raw_image(p):
img = pil_image.open(expand_path(p))
if p in rotate: img = img.rotate(180)
return img
p = list(rotate)[0]
imgs = [pil_image.open(expand_path(p)), read_raw_image(p)]
show_whale(imgs)
在我早期的实验中,我注意到我的模型在比较两个彩色图像或两个黑白图像时达到了大致相同的精度。 然而,将彩色图像与黑白图像进行比较精度则低得多。 最简单的解决方案是将所有图像转换为黑白图像,即使与原始彩色图像比较也不会降低精度。
仿射变换将原始图像的矩形区域映射到分辨率为384x384x1的正方形图像(仅黑色和白色的一个通道)。 矩形区域的宽度高度纵横比为2.15,接近平均图像的宽高比。裁剪的矩形比另外一个kernel中计算出来的bounding box略大一些,因为削减获得的边缘比精确拟合获得的增益相比更有害,因此留一些空白是必须的(即用目标检测的方法得到鲸鱼的bbox时,可能会丢失边缘的一些信息,而为了保留这些这些信息而增加了一些额外的噪声是值得的 )。
在训练期间,通过缩放,移位,旋转和剪切的随机变换来进行数据增强。 测试时跳过随机变换。
最后,将图像归一化为零均值和单位方差。
# 从bounding box kernel中读取边界框数据(参见上面的参考资料)
with open('../input/humpback-whale-identification-model-files/bounding-box.pickle', 'rb') as f:
p2bb = pickle.load(f)
list(p2bb.items())[:5]
[(‘00022e1a.jpg’, (34, 45, 682, 317)),
(‘000466c4.jpg’, (263, 309, 591, 412)),
(‘00087b01.jpg’, (-6, 2, 1028, 363)),
(‘001296d5.jpg’, (9, 21, 387, 135)),
(‘0014cfdf.jpg’, (36, 129, 636, 299))]
# 抑制导入keras时烦人的stderr输出
import sys
import platform
old_stderr = sys.stderr
sys.stderr = open('/dev/null' if platform.system() != 'Windows' else 'nul', 'w')
import keras
sys.stderr = old_stderr
import random
from keras import backend as K
from keras.preprocessing.image import img_to_array,array_to_img
from scipy.ndimage import affine_transform
img_shape = (384,384,1) # 模型使用的图像形状
anisotropy = 2.15 # 水平压缩比
crop_margin = 0.05 # 在边界框周围添加余量以补偿边界框的不精确性
def build_transform(rotation, shear, height_zoom, width_zoom, height_shift, width_shift):
"""
构建具有指定特征的变换矩阵
"""
rotation = np.deg2rad(rotation)
shear = np.deg2rad(shear)
rotation_matrix = np.array([[np.cos(rotation), np.sin(rotation), 0], [-np.sin(rotation), np.cos(rotation), 0], [0, 0, 1]])
shift_matrix = np.array([[1, 0, height_shift], [0, 1, width_shift], [0, 0, 1]])
shear_matrix = np.array([[1, np.sin(shear), 0], [0, np.cos(shear), 0], [0, 0, 1]])
zoom_matrix = np.array([[1.0/height_zoom, 0, 0], [0, 1.0/width_zoom, 0], [0, 0, 1]])
shift_matrix = np.array([[1, 0, -height_shift], [0, 1, -width_shift], [0, 0, 1]])
return np.dot(np.dot(rotation_matrix, shear_matrix), np.dot(zoom_matrix, shift_matrix))
def read_cropped_image(p, augment):
"""
@param p : 要读取的图片的名称
@param augment: 是否需要做图像增强
@返回变换后的图像
"""
# 如果给出了图像ID,则转换为文件名
if p in h2p: p = h2p[p]
size_x,size_y = p2size[p]
# 根据边界框确定要捕获的原始图像的区域。
x0,y0,x1,y1 = p2bb[p]
if p in rotate: x0, y0, x1, y1 = size_x - x1, size_y - y1, size_x - x0, size_y - y0
dx = x1 - x0
dy = y1 - y0
x0 -= dx*crop_margin
x1 += dx*crop_margin + 1
y0 -= dy*crop_margin
y1 += dy*crop_margin + 1
if (x0 < 0 ): x0 = 0
if (x1 > size_x): x1 = size_x
if (y0 < 0 ): y0 = 0
if (y1 > size_y): y1 = size_y
dx = x1 - x0
dy = y1 - y0
if dx > dy*anisotropy:
dy = 0.5*(dx/anisotropy - dy)
y0 -= dy
y1 += dy
else:
dx = 0.5*(dy*anisotropy - dx)
x0 -= dx
x1 += dx
# 生成变换矩阵
trans = np.array([[1, 0, -0.5*img_shape[0]], [0, 1, -0.5*img_shape[1]], [0, 0, 1]])
trans = np.dot(np.array([[(y1 - y0)/img_shape[0], 0, 0], [0, (x1 - x0)/img_shape[1], 0], [0, 0, 1]]), trans)
if augment:
trans = np.dot(build_transform(
random.uniform(-5, 5),
random.uniform(-5, 5),
random.uniform(0.8, 1.0),
random.uniform(0.8, 1.0),
random.uniform(-0.05*(y1 - y0), 0.05*(y1 - y0)),
random.uniform(-0.05*(x1 - x0), 0.05*(x1 - x0))
), trans)
trans = np.dot(np.array([[1, 0, 0.5*(y1 + y0)], [0, 1, 0.5*(x1 + x0)], [0, 0, 1]]), trans)
# 读取图像,转换为黑白再转换为numpy数组
img = read_raw_image(p).convert('L')
img = img_to_array(img)
# 使用仿射变换
matrix = trans[:2,:2]
offset = trans[:2,2]
img = img.reshape(img.shape[:-1])
img = affine_transform(img, matrix, offset, output_shape=img_shape[:-1], order=1, mode='constant', cval=np.average(img))
img = img.reshape(img_shape)
# 归一化为零均值和单位方差
img -= np.mean(img, keepdims=True)
img /= np.std(img, keepdims=True) + K.epsilon()
return img
def read_for_training(p):
"""
使用数据增强(随机变换)读取和预处理图像。
"""
return read_cropped_image(p, True)
def read_for_validation(p):
"""
在没有数据增强的情况下读取和预处理图像(用于测试)。
"""
return read_cropped_image(p, False)
p = list(tagged.keys())[312]
imgs = [
read_raw_image(p),
array_to_img(read_for_validation(p)),
array_to_img(read_for_training(p))
]
show_whale(imgs, per_row=3)
左图是原始图片。 中心图像进行测试转换。 右图增加了随机数据增强转换。
Siamese网络通过比较两个图像来决定这两个图像是出自同一条鲸鱼还是不同的鲸鱼。 通过测试每个来自测试集的图像,与训练集中每个图片进行比较,就可以通过相似性进行排序来识别最匹配的鲸鱼。
Siamese网络有两部分组成。一个CNN将输入图像转化为描述鲸鱼的特征向量。具有相同权重的相同CNN作用于两个图像,我称这个CNN为branch model。我使用的是一个类似于ResNet的模型。
另一个模型称作head model,用于比较来自CNN的特征向量并确定鲸鱼是否匹配。
Head model比较来自branch model的特征向量,判断图片是否来自同一条鲸鱼。典型的方法是使用距离测度(如 L1 L 1 范数)作为损失函数,但这里有几个理由去尝试不同的东西:
为了解决这些问题,我做了以下处理:
Branch model是常规的CNN模型。 以下是其设计的关键要素:
Branch model由6个Block组成,由于中间具有池化层,每个Block处理的分辨率越来越小。
Block 1 具有单个stride为2的卷积层,接着是2 × × 2最大池化。由于分辨率高,它使用了大量的存储空间,因此为了节省后续Block的存储空间,这里做了最少的工作。
Block 2 有两个类似于VGG的3 × × 3卷积。这些卷积比后续的ResNet模块更节省存储空间。请注意,在此之后,张量的尺寸为96 × × 96 × × 64,与初始的384 × × 384 × × 1图像的体积相同,因此我们可以假设没有丢失重要信息。
Block3到6执行ResNet类型的卷积,我建议阅读原始论文,其想法是使用1 × × 1卷积的子块来减少特征数量,3 × × 3卷积核另一个1 × × 1卷积用来恢复原始特征的数量。然后将这些卷积的输出添加到原始张量(旁路连接),我再每一个block都使用这样的子块,再加上一个1 × × 1卷积来增加每个池化层后的特征数。
Branch model的最后一步是全局最大池化,这可以使模型鲁棒地可以忽略侥幸的不够好的特征。
以下是该模型的Keras代码
from keras import regularizers
from keras.optimizers import Adam
from keras.engine.topology import Input
from keras.layers import Activation, Add, BatchNormalization, Concatenate, Conv2D, Dense, Flatten, GlobalMaxPooling2D, Lambda, MaxPooling2D, Reshape
from keras.models import Model
def subblock(x, filter, **kwargs):
x = BatchNormalization()(x)
y = x
y = Conv2D(filter, (1, 1), activation='relu', **kwargs)(y) # 减少特征数量
y = BatchNormalization()(y)
y = Conv2D(filter, (3, 3), activation='relu', **kwargs)(y) # 扩展特征域
y = BatchNormalization()(y)
y = Conv2D(K.int_shape(x)[-1], (1, 1), **kwargs)(y) # 无激活函数 # 恢复原始特征的数量
y = Add()([x,y]) # Add the bypass connection
y = Activation('relu')(y)
return y
def build_model(lr, l2, activation='sigmoid'):
##############
# BRANCH MODEL
##############
regul = regularizers.l2(l2)
optim = Adam(lr=lr)
kwargs = {'padding':'same', 'kernel_regularizer':regul}
inp = Input(shape=img_shape) # 384x384x1
x = Conv2D(64, (9,9), strides=2, activation='relu', **kwargs)(inp)
x = MaxPooling2D((2, 2), strides=(2, 2))(x) # 96x96x64
for _ in range(2):
x = BatchNormalization()(x)
x = Conv2D(64, (3,3), activation='relu', **kwargs)(x)
x = MaxPooling2D((2, 2), strides=(2, 2))(x) # 48x48x64
x = BatchNormalization()(x)
x = Conv2D(128, (1,1), activation='relu', **kwargs)(x) # 48x48x128
for _ in range(4): x = subblock(x, 64, **kwargs)
x = MaxPooling2D((2, 2), strides=(2, 2))(x) # 24x24x128
x = BatchNormalization()(x)
x = Conv2D(256, (1,1), activation='relu', **kwargs)(x) # 24x24x256
for _ in range(4): x = subblock(x, 64, **kwargs)
x = MaxPooling2D((2, 2), strides=(2, 2))(x) # 12x12x256
x = BatchNormalization()(x)
x = Conv2D(384, (1,1), activation='relu', **kwargs)(x) # 12x12x384
for _ in range(4): x = subblock(x, 96, **kwargs)
x = MaxPooling2D((2, 2), strides=(2, 2))(x) # 6x6x384
x = BatchNormalization()(x)
x = Conv2D(512, (1,1), activation='relu', **kwargs)(x) # 6x6x512
for _ in range(4): x = subblock(x, 128, **kwargs)
x = GlobalMaxPooling2D()(x) # 512
branch_model = Model(inp, x)
############
# HEAD MODEL
############
mid = 32
xa_inp = Input(shape=branch_model.output_shape[1:])
xb_inp = Input(shape=branch_model.output_shape[1:])
x1 = Lambda(lambda x : x[0]*x[1])([xa_inp, xb_inp])
x2 = Lambda(lambda x : x[0] + x[1])([xa_inp, xb_inp])
x3 = Lambda(lambda x : K.abs(x[0] - x[1]))([xa_inp, xb_inp])
x4 = Lambda(lambda x : K.square(x))(x3)
x = Concatenate()([x1, x2, x3, x4])
x = Reshape((4, branch_model.output_shape[1], 1), name='reshape1')(x)
# 使用合适的步幅,让2D卷积实现具有共享权重的特征神经网络.
x = Conv2D(mid, (4, 1), activation='relu', padding='valid')(x)
x = Reshape((branch_model.output_shape[1], mid, 1))(x)
x = Conv2D(1, (1, mid), activation='linear', padding='valid')(x)
x = Flatten(name='flatten')(x)
# Dense layer的实现为加权和.
x = Dense(1, use_bias=True, activation=activation, name='weighted-average')(x)
head_model = Model([xa_inp, xb_inp], x, name='head')
########################
# SIAMESE NEURAL NETWORK
########################
# 通过在每个输入图像上调用branch model来构建完整模型,
# 然后是生成512个向量的head model.
img_a = Input(shape=img_shape)
img_b = Input(shape=img_shape)
xa = branch_model(img_a)
xb = branch_model(img_b)
x = head_model([xa, xb])
model = Model([img_a, img_b], x)
model.compile(optim, loss='binary_crossentropy', metrics=['binary_crossentropy', 'acc'])
return model, branch_model, head_model
model, branch_model, head_model = build_model(64e-5,0)
head_model.summary()
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_2 (InputLayer) (None, 512) 0
input_3 (InputLayer) (None, 512) 0
lambda_3 (Lambda) (None, 512) 0 input_2[0][0]
input_3[0][0]
lambda_1 (Lambda) (None, 512) 0 input_2[0][0]
input_3[0][0]
lambda_2 (Lambda) (None, 512) 0 input_2[0][0]
input_3[0][0]
lambda_4 (Lambda) (None, 512) 0 lambda_3[0][0]
concatenate_1 (Concatenate) (None, 2048) 0 lambda_1[0][0]
lambda_2[0][0]
lambda_3[0][0]
lambda_4[0][0]
reshape1 (Reshape) (None, 4, 512, 1) 0 concatenate_1[0][0]
conv2d_56 (Conv2D) (None, 1, 512, 32) 160 reshape1[0][0]
reshape_1 (Reshape) (None, 512, 32, 1) 0 conv2d_56[0][0]
conv2d_57 (Conv2D) (None, 512, 1, 1) 33 reshape_1[0][0]
flatten (Flatten) (None, 512) 0 conv2d_57[0][0]
weighted-average (Dense) (None, 1) 513 flatten[0][0]
==================================================================================================
Total params: 706
Trainable params: 706
Non-trainable params: 0
from keras.utils import plot_model
plot_model(head_model, to_file='head-model.png')
pil_image.open('head-model.png')
branch_model.summary()
这部分太长了,就不贴了,直接看可视化的网络
# Oops, this is HUGE!
plot_model(branch_model, to_file='branch-model.png')
img = pil_image.open('branch-model.png')
img.resize([x//2 for x in img.size])
正如摘要部分所强调的那样,这是提升模型精度最重要的部分。
我们希望Siamese网络从训练集中的所有可能的鲸鱼中挑选一条正确的鲸鱼。虽然需要给正确的鲸鱼打较高的分,但它必须同时给所有其他鲸鱼的较低的得分。将随机的鲸鱼得分降低是不够的。为了迫使所有其他鲸鱼达到低概率,训练算法需要提供给模型难度逐渐增加的图像对,这个难度是模型在任意给定的时间评估出来的。从本质上讲,我们把模型的训练当成是一种对抗训练的形式。
同时,我们希望模型识别鲸鱼而不是图片鉴于训练数据集中的图片数量很少,要模型识别波浪的形状或者飞鸟是不切实际的。为防止这种情况,提交给模型的数据必须是无偏的。如果某一张图片在负例中频繁地出现,那么模型会简单地学习,在任何图片出现时都猜错,而不是学习如何正确地比较鲸鱼。通过提供相同出现次数的具有50%正例和50%负例的每个图像,该模型就没有学习识别特定图片的动机,一次会更关注与需要识别的鲸鱼。(这里指的是不平衡的数据导致模型预测时会偏向多的那一类
首先,我们减少训练集中图像的数量:
黑名单是通过发现对训练无益的图像手动构建的。比如尾巴的底面不可见。或者我们在海滩上看到只有尾巴的碎片,图中有两条鲸鱼等等,这份清单并不详尽。
with open('../input/humpback-whale-identification-model-files/exclude.txt', 'rt') as f: exclude = f.read().split('\n')[:-1]
len(exclude)
34
show_whale([read_raw_image(p) for p in exclude], per_row=5)
训练时使用的一半样本是一对图像。对应训练集中的每只鲸鱼,计算其图片的Derangement,使用原始顺序图片作为图片A,将Derangement作为图片B。这会创建一个随机数量的匹配图像对,每个图像只采样两次。
通过计算来自训练集的所有图片的Derangement来生成不同的鲸鱼示例,但须满足:
总结:模型同时接收4张图片,即2个图像对(A, B), (A’, B’),前者属于同一个鲸鱼,label为1,后者属于不同鲸鱼,label为0
以下算法用于生成图像对:
上述逻辑基本由TrainingData类实现,该类实时地执行数据增强和匹配计算。
# 找到与图像ID关联的所有鲸鱼。 它可能不明确,因为重复的图像可能有不同的鲸鱼ID。
h2ws = {}
new_whale = 'new_whale'
for p,w in tagged.items():
if w != new_whale: # 仅使用已识别的鲸鱼
h = p2h[p]
if h not in h2ws: h2ws[h] = []
if w not in h2ws[h]: h2ws[h].append(w)
for h,ws in h2ws.items():
if len(ws) > 1:
h2ws[h] = sorted(ws)
len(h2ws)
8412
# 对于每条鲸鱼,找到明确的图像ID。
w2hs = {}
for h,ws in h2ws.items():
if len(ws) == 1: # 仅使用明确的图片
if h2p[h] in exclude:
print(h) # 跳过排除的图像
else:
w = ws[0]
if w not in w2hs: w2hs[w] = []
if h not in w2hs[w]: w2hs[w].append(h)
for w,hs in w2hs.items():
if len(hs) > 1:
w2hs[w] = sorted(hs)
len(w2hs)
ebf094854a2bb1d6
f86bcf9487653848
d931c4768ebf9098
807b19b6766d09ce
bc984f67a31b48a6
aa557d0ad40f807f
c5313ec3c0343bcf
9dc39bb4833cc3c8
fb8c85f18c67131c
afa994d4416b2ab6
c0352f2194b7fcca
c4cc196f46bc8cce
c0c0753e9fcf4368
e5d11a1e86c47979
cdc0363cc23c3ecb
d8dc91b13fae8a18
f3ad8c8cb2b38c8c
f18c966fb836c90c
f8908e4ee223f758
a2d5d5eae64f2a01
96c949b632e90d3e
88d1a0eee07ce9da
e8d1960f60b13fca
d08f9d61729e8d61
90376cc843f6b9a3
e92d90d2616f0f9c
c96d1296e96b16b4
c7882c3359ec7327
9467679c9b638c98
f43183739a8f53c4
4239
# 获取训练图像列表,里面只保留至少有两个图像的鲸鱼
train = [] # A list of training image ids
for hs in w2hs.values():
if len(hs) > 1:
train += hs
random.shuffle(train)
train_set = set(train)
w2ts = {} # 将训练中的图像ID与每个鲸鱼ID相关联。
for w,hs in w2hs.items():
for h in hs:
if h in train_set:
if w not in w2ts: w2ts[w] = []
if h not in w2ts[w]: w2ts[w].append(h)
for w,ts in w2ts.items(): w2ts[w] = np.array(ts)
t2i = {} # 训练图像ID在训练集中的位置
for i,t in enumerate(train): t2i[t] = i
len(train),len(w2ts)
(6038, 1905)
from keras.utils import Sequence
# 首先尝试使用更快的lapjv解决Linear Assignment Problem。
# 在我写这篇文章时,带有自定义包的kaggle kernel无法提交。
# scipy可以当做备用,但在时间限制下运行此kernel太慢
# 使用scipy进行数据分区来作为一种解决方法。
# 因为算法复杂度为O(n^3), 分成小块会快的多,但生成的不是真正得解决方案。
try:
from lap import lapjv
segment = False
except ImportError:
print('Module lap not found, emulating with much slower scipy.optimize.linear_sum_assignment')
segment = True
from scipy.optimize import linear_sum_assignment
class TrainingData(Sequence):
def __init__(self, score, steps=1000, batch_size=32):
"""
@param score 图片匹配的cost matrix
@param steps epoch数,用来设计score matrix
"""
super(TrainingData, self).__init__()
self.score = -score # 最大化分数与最小化负分数相同。
self.steps = steps
self.batch_size = batch_size
for ts in w2ts.values():
idxs = [t2i[t] for t in ts]
for i in idxs:
for j in idxs:
self.score[i,j] = 10000.0 # 为匹配鲸鱼设置一个很大的值 - 消除了这种潜在的配对
self.on_epoch_end()
def __getitem__(self, index):
start = self.batch_size*index
end = min(start + self.batch_size, len(self.match) + len(self.unmatch))
size = end - start
assert size > 0
a = np.zeros((size,) + img_shape, dtype=K.floatx())
b = np.zeros((size,) + img_shape, dtype=K.floatx())
c = np.zeros((size,1), dtype=K.floatx())
j = start//2
for i in range(0, size, 2):
a[i, :,:,:] = read_for_training(self.match[j][0])
b[i, :,:,:] = read_for_training(self.match[j][1])
c[i, 0 ] = 1 # This is a match
a[i+1,:,:,:] = read_for_training(self.unmatch[j][0])
b[i+1,:,:,:] = read_for_training(self.unmatch[j][1])
c[i+1,0 ] = 0 # Different whales
j += 1
return [a,b],c
def on_epoch_end(self):
if self.steps <= 0: return # 跳过最后一个epoch
self.steps -= 1
self.match = []
self.unmatch = []
if segment:
# 使用较慢的scipy,用较小的batch
# 因为算法复杂度为O(n^3), 小batch更快
# 然而,这并不能找到真正的最优解,只是近似值。
tmp = []
batch = 512
for start in range(0, score.shape[0], batch):
end = min(score.shape[0], start + batch)
_, x = linear_sum_assignment(self.score[start:end, start:end])
tmp.append(x + start)
x = np.concatenate(tmp)
else:
_,_,x = lapjv(self.score) # 解决 linear assignment problem
y = np.arange(len(x),dtype=np.int32)
# 计算匹配鲸鱼的derangement
for ts in w2ts.values():
d = ts.copy()
while True:
random.shuffle(d)
if not np.any(ts == d): break
for ab in zip(ts,d): self.match.append(ab)
# Construct unmatched whale pairs from the LAP solution.
for i,j in zip(x,y):
if i == j:
print(self.score)
print(x)
print(y)
print(i,j)
assert i != j
self.unmatch.append((train[i],train[j]))
# Force a different choice for an eventual next epoch.
self.score[x,y] = 10000.0
self.score[y,x] = 10000.0
random.shuffle(self.match)
random.shuffle(self.unmatch)
# print(len(self.match), len(train), len(self.unmatch), len(train))
assert len(self.match) == len(train) and len(self.unmatch) == len(train)
def __len__(self):
return (len(self.match) + len(self.unmatch) + self.batch_size - 1)//self.batch_size
# 对一批32个随机cost matrix进行测试。
score = np.random.random_sample(size=(len(train),len(train)))
data = TrainingData(score)
(a, b), c = data[0]
a.shape, b.shape, c.shape
((32, 384, 384, 1), (32, 384, 384, 1), (32, 1))
# 第一对为匹配的鲸鱼
imgs = [array_to_img(a[0]), array_to_img(b[0])]
show_whale(imgs, per_row=2)
# 第二对为不匹配的鲸鱼
imgs = [array_to_img(a[1]), array_to_img(b[1])]
show_whale(imgs, per_row=2)
本节介绍用于训练模型的过程。训练持续400个epoch,随着训练的进行,以下数值会发生变化:
该程序本身是从早期版本的模型中的许多实验,试验和错误演变而来。
用随机权重训练大型模型很困难。实际上,如果该模型最初被提供的实例太难,则它根本不会收敛。在本文中,难区分的样本属于不同鲸鱼的类似图像。更极端地说,构建一个训练数据集,其中不同鲸鱼的图片对出现比来自同一条鲸鱼的图片对更相似,使模型学会将类似的图像分类为不同的鲸鱼,和不同的图像一样的鲸鱼。
为了防止这种情况,早期训练K的值较大,使得负面实例基本上是随机的不同鲸鱼图片对。由于模型区分鲸鱼的能力增加,K逐渐减少,呈现更难的训练案列。同样,训练从没有 L2 L 2 正则化开始。在250个epoch后,训练的准确性非常好,但也开始过拟合。此时,应用 L2 L 2 正则化,将学习率重置为较大值,再训练150个epoch。
下表显示了学习率, L2 L 2 正则化和随机score matrix的确切时间表。
还要注意,Linear Assignment Problem的score matrix是从第10个epoch后,每5个epoch计算一次。
Epochs | LR | K | L2 L 2 |
---|---|---|---|
1-10 | 64e-5 | +∞ + ∞ | 0 |
11-15 | 64e-5 | 100.00 | 0 |
16-20 | 64e-5 | 63.10 | 0 |
21-25 | 64e-5 | 39.81 | 0 |
26-30 | 64e-5 | 25.12 | 0 |
31-35 | 64e-5 | 15.85 | 0 |
36-40 | 64e-5 | 10.0 | 0 |
41-45 | 64e-5 | 6.31 | 0 |
46-50 | 64e-5 | 3.98 | 0 |
51-55 | 64e-5 | 2.51 | 0 |
56-60 | 64e-5 | 1.58 | 0 |
61-150 | 64e-5 | 1.00 | 0 |
150-200 | 16e-5 | 0.50 | 0 |
201-240 | 4e-5 | 0.25 | 0 |
241-250 | 1e-5 | 0.25 | 0 |
251-300 | 64e-5 | 1.00 | 2e-4 |
301-350 | 16e-5 | 0.50 | 2e-4 |
351-390 | 4e-5 | 0.25 | 2e-4 |
391-400 | 1e-5 | 0.25 | 2e-4 |
# Keras生成器,仅评估branch model
class FeatureGen(Sequence):
def __init__(self, data, batch_size=64, verbose=1):
super(FeatureGen, self).__init__()
self.data = data
self.batch_size = batch_size
self.verbose = verbose
if self.verbose > 0: self.progress = tqdm_notebook(total=len(self), desc='Features')
def __getitem__(self, index):
start = self.batch_size*index
size = min(len(self.data) - start, self.batch_size)
a = np.zeros((size,) + img_shape, dtype=K.floatx())
for i in range(size): a[i,:,:,:] = read_for_validation(self.data[start + i])
if self.verbose > 0:
self.progress.update()
if self.progress.n >= len(self): self.progress.close()
return a
def __len__(self):
return (len(self.data) + self.batch_size - 1)//self.batch_size
# Keras生成器,用于评估head model上已预先计算的特征。
# 如果y为None,则仅计算cost matrix的上三角矩阵。
class ScoreGen(Sequence):
def __init__(self, x, y=None, batch_size=2048, verbose=1):
super(ScoreGen, self).__init__()
self.x = x
self.y = y
self.batch_size = batch_size
self.verbose = verbose
if y is None:
self.y = self.x
self.ix, self.iy = np.triu_indices(x.shape[0],1)
else:
self.iy, self.ix = np.indices((y.shape[0],x.shape[0]))
self.ix = self.ix.reshape((self.ix.size,))
self.iy = self.iy.reshape((self.iy.size,))
self.subbatch = (len(self.x) + self.batch_size - 1)//self.batch_size
if self.verbose > 0: self.progress = tqdm_notebook(total=len(self), desc='Scores')
def __getitem__(self, index):
start = index*self.batch_size
end = min(start + self.batch_size, len(self.ix))
a = self.y[self.iy[start:end],:]
b = self.x[self.ix[start:end],:]
if self.verbose > 0:
self.progress.update()
if self.progress.n >= len(self): self.progress.close()
return [a,b]
def __len__(self):
return (len(self.ix) + self.batch_size - 1)//self.batch_size
from keras_tqdm import TQDMNotebookCallback
def set_lr(model, lr):
K.set_value(model.optimizer.lr, float(lr))
def get_lr(model):
return K.get_value(model.optimizer.lr)
def score_reshape(score, x, y=None):
"""
将packed matrix的'得分'转换为方阵。
@param score the packed matrix
@param x 第一张图像的特征张量
@param y 第二张图像的张量,如果与x不同
@结果为方阵
"""
if y is None:
# 当y为None时,得分是packed matrix的上三角矩阵。
# 解包, 并转置形成对称的下三角矩阵。
m = np.zeros((x.shape[0],x.shape[0]), dtype=K.floatx())
m[np.triu_indices(x.shape[0],1)] = score.squeeze()
m += m.transpose()
else:
m = np.zeros((y.shape[0],x.shape[0]), dtype=K.floatx())
iy,ix = np.indices((y.shape[0],x.shape[0]))
ix = ix.reshape((ix.size,))
iy = iy.reshape((iy.size,))
m[iy,ix] = score.squeeze()
return m
def compute_score(verbose=1):
"""
Compute the score matrix by scoring every pictures from the training set against every other picture O(n^2).
"""
features = branch_model.predict_generator(FeatureGen(train, verbose=verbose), max_queue_size=12, workers=6, verbose=0)
score = head_model.predict_generator(ScoreGen(features, verbose=verbose), max_queue_size=12, workers=6, verbose=0)
score = score_reshape(score, features)
return features, score
def make_steps(step, ampl):
"""
执行训练
@param step 训练的epoch数。
@param ampl K值, score matrix的随机分量。
"""
global w2ts, t2i, steps, features, score, histories
# 打乱训练图片
random.shuffle(train)
# 将鲸鱼id映射到相关的训练图片的hash表上去。
w2ts = {}
for w,hs in w2hs.items():
for h in hs:
if h in train_set:
if w not in w2ts: w2ts[w] = []
if h not in w2ts[w]: w2ts[w].append(h)
for w,ts in w2ts.items(): w2ts[w] = np.array(ts)
# 将训练图片hash值映射到'train'数组中的索引
t2i = {}
for i,t in enumerate(train): t2i[t] = i
# 计算每个图片对的匹配分数
features, score = compute_score()
# 训练模型'step'个epoch
history = model.fit_generator(
TrainingData(score + ampl*np.random.random_sample(size=score.shape), steps=step, batch_size=32),
initial_epoch=steps, epochs=steps + step, max_queue_size=12, workers=6, verbose=0,
callbacks=[
TQDMNotebookCallback(leave_inner=True, metric_format='{value:0.3f}')
]).history
steps += step
# 收集历史数据
history['epochs'] = steps
history['ms' ] = np.mean(score)
history['lr' ] = get_lr(model)
print(history['epochs'],history['lr'],history['ms'])
histories.append(history)
model_name = 'mpiotte-standard'
histories = []
steps = 0
if isfile('../input/humpback-whale-identification-model-files/mpiotte-standard.model'):
tmp = keras.models.load_model('../input/humpback-whale-identification-model-files/mpiotte-standard.model')
model.set_weights(tmp.get_weights())
else:
# epoch -> 10
make_steps(10, 1000)
ampl = 100.0
for _ in range(10):
print('noise ampl. = ', ampl)
make_steps(5, ampl)
ampl = max(1.0, 100**-0.1*ampl)
# epoch -> 150
for _ in range(18): make_steps(5, 1.0)
# epoch -> 200
set_lr(model, 16e-5)
for _ in range(10): make_steps(5, 0.5)
# epoch -> 240
set_lr(model, 4e-5)
for _ in range(8): make_steps(5, 0.25)
# epoch -> 250
set_lr(model, 1e-5)
for _ in range(2): make_steps(5, 0.25)
# epoch -> 300
weights = model.get_weights()
model, branch_model, head_model = build_model(64e-5,0.0002)
model.set_weights(weights)
for _ in range(10): make_steps(5, 1.0)
# epoch -> 350
set_lr(model, 16e-5)
for _ in range(10): make_steps(5, 0.5)
# epoch -> 390
set_lr(model, 4e-5)
for _ in range(8): make_steps(5, 0.25)
# epoch -> 400
set_lr(model, 1e-5)
for _ in range(2): make_steps(5, 0.25)
model.save('mpiotte-standard.model')
对于测试集中的每张图片,基本策略是这样的:
假设没有标记错误,那么重复图像就是免费的答案。对于new_whale,算法首先选择高置信度预测,然后插入new_whale,然后插入低置信度预测。通过反复使用选择阈值,尽管大多数情况模型选择阈值作为最佳值,导致7100多个图像以new_whale类作为第一选择。以上结果可以在不向Kaggle提交预测的情况下测量。
# 在本文中不进行计算,因为它有点慢。 使用GTX 1080大约需要15分钟。
import gzip
def prepare_submission(threshold, filename):
"""
Generate a Kaggle submission file.
@param threshold the score given to 'new_whale'
@param filename the submission file name
"""
vtop = 0
vhigh = 0
pos = [0,0,0,0,0,0]
with gzip.open(filename, 'wt', newline='\n') as f:
f.write('Image,Id\n')
for i,p in enumerate(tqdm_notebook(submit)):
t = []
s = set()
a = score[i,:]
for j in list(reversed(np.argsort(a))):
h = known[j]
if a[j] < threshold and new_whale not in s:
pos[len(t)] += 1
s.add(new_whale)
t.append(new_whale)
if len(t) == 5: break;
for w in h2ws[h]:
assert w != new_whale
if w not in s:
if a[j] > 1.0:
vtop += 1
elif a[j] >= threshold:
vhigh += 1
s.add(w)
t.append(w)
if len(t) == 5: break;
if len(t) == 5: break;
if new_whale not in s: pos[5] += 1
assert len(t) == 5 and len(s) == 5
f.write(p + ',' + ' '.join(t[:5]) + '\n')
return vtop,vhigh,pos
if False:
# Find elements from training sets not 'new_whale'
h2ws = {}
for p,w in tagged.items():
if w != new_whale: # Use only identified whales
h = p2h[p]
if h not in h2ws: h2ws[h] = []
if w not in h2ws[h]: h2ws[h].append(w)
known = sorted(list(h2ws.keys()))
# Dictionary of picture indices
h2i = {}
for i,h in enumerate(known): h2i[h] = i
# Evaluate the model.
fknown = branch_model.predict_generator(FeatureGen(known), max_queue_size=20, workers=10, verbose=0)
fsubmit = branch_model.predict_generator(FeatureGen(submit), max_queue_size=20, workers=10, verbose=0)
score = head_model.predict_generator(ScoreGen(fknown, fsubmit), max_queue_size=20, workers=10, verbose=0)
score = score_reshape(score, fknown, fsubmit)
# Generate the subsmission file.
prepare_submission(0.99, 'mpiotte-standard.csv.gz')
mpiotte-standard.model的得分为0.766。
由于训练数据集较小,且测试集较大,因此bootstrapping算法是提高分数的良好方案。在本文中,bootstrapping意味着使用该模型自动生成额外的训练示例,并在较大的数据集上重新训练模型。在这个实验中,我选择了测试集中预测单个鲸鱼得分大于0.999999的图像(得分只是用于对鲸鱼进行排序的数字,它不是概率)。
with open('../input/humpback-whale-identification-model-files/bootstrap.pickle', 'rb') as f:
bootstrap = pickle.load(f)
len(bootstrap), list(bootstrap.items())[:5]
(1885,
[(‘ea8f94e03ced18d2’, ‘w_0b775c1’),
(‘bd84d2b265199e65’, ‘w_e8bce8a’),
(‘afdad0a5e024bd23’, ‘w_3461d6d’),
(‘b61cc9598e4fea12’, ‘w_34c8690’),
(‘a1be8e613c8c5379’, ‘w_7554f44’)])
提交这些1885张照片的结果,显示准确率超过93%
将这些文件添加到训练集并从头开始重新运行训练会生成mpiotte-bootstrap.model。 该模型的得分略高于0.774,这里阈值为 0.989。
最佳分数是通过mpiotte-standard.model和mpiotte-bootstrap.model集合而获得的。这两种模型由于它们的性质而产生不同的错误,这使它们成为合奏的良好候选者:
分配策略包括计算一个score matrix(或者通过训练的测试尺度),它是standard和bootstrap模型的线性组合。使用score matrix生成提交的策略不变,试验表明standard model的权重为0.45,bootstrap model的权重为0.55。
得到的整体的精度为0.78563,阈值为0.92。 值得注意的是,为什么整体的“阈值”值如此的低,这与两个模型产生不同误差的事实一致,因此整体分数通常低于单个模型,这些模型对它们的猜测非常乐观。
本节通过一些可视化来探索模型。
如模型描述中所讨论的,head model对特征进行加权求和,允许负权重。我们可以验证我们是否看到了正负权重的组合,这些权重确认某些特征在匹配时会降低匹配鲸鱼的概率。 这可能让我们匹配单一的,单色的尾巴,这可能不太正确,因为匹配鲸鱼会涉及到多重的特征。
w = head_model.layers[-1].get_weights()[0]
w = w.flatten().tolist()
w = sorted(w)
fig, axes = plt.subplots(1,1)
axes.bar(range(len(w)), w)
plt.show()
我们还可以检查“每个特征”网络对不同功能值的行为方式。
我们期望看到的是,相等的零特征应该产生比类似的大值更小的输出。 同时,非常不同的值必须受到惩罚。
# 用线性激活函数构造head model
_, _, tmp_model = build_model(64e-5,0, activation='linear')
tmp_model.set_weights(head_model.get_weights())
# 用常数向量评估模型。
a = np.ones((21*21,512),dtype=K.floatx())
b = np.ones((21*21,512),dtype=K.floatx())
for i in range(21):
for j in range(21):
a[21*i + j] *= float(i)/10.0
b[21*i + j] *= float(j)/10.0
x = np.arange(0.0, 2.01, 0.1, dtype=K.floatx())
x, y = np.meshgrid(x, x)
z = tmp_model.predict([a,b], verbose=0).reshape((21,21))
x.shape, y.shape, z.shape
((21, 21), (21, 21), (21, 21))
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(x, y, z, cmap=cm.coolwarm)
plt.show()
不是很容易看到,但仍然是匹配具有大值的特征的最大输出(最佳鲸鱼匹配)。 匹配零获得更低的值。
只是色彩图可能更容易看到。 这证实了初步假设。
from matplotlib.colors import BoundaryNorm
from matplotlib.ticker import MaxNLocator
levels = MaxNLocator(nbins=15).tick_values(z.min(), z.max())
fig = plt.figure()
ax = fig.add_subplot(111)
cf = ax.contourf(x, y, z, levels=levels, cmap=cm.coolwarm)
plt.show()
本节尝试重建最大化激活特征的图像。 这提供了有关特征提取过程的一些见解。
生成图像的代码是根据Francois Chollet在Deep Learning with Python中的示例进行修改而成的。
from scipy.ndimage import gaussian_filter
def show_filter(filter, blur):
np.random.seed(1)
noise = 0.1 # Initial noise
step = 1 # Gradient step
# 构造函数
inp = branch_model.layers[0].get_input_at(0)
loss = K.mean(branch_model.layers[-3].output[0,2:4,2:4,filter]) # Stimulate the 4 central cells
grads = K.gradients(loss, inp)[0]
grads /= K.sqrt(K.mean(K.square(grads))) + K.epsilon()
iterate = K.function([inp],[grads])
img = (np.random.random(img_shape) -0.5)*noise
img = np.expand_dims(img, 0)
# 使用梯度下降来形成图像
for i in range(200):
grads_value = iterate([img])[0]
# Blurring a little creates nicer images by reducing reconstruction noise
img = gaussian_filter(img + grads_value*step, sigma=blur)
# 剪辑图像以提高对比度
avg = np.mean(img)
std = sqrt(np.mean((img - avg)**2))
low = avg - 5*std
high = avg + 5*std
return array_to_img(np.minimum(high, np.maximum(low, img))[0])
# 显示前25个特征 (全部为512个)
show_whale([show_filter(i, 0.5) for i in tqdm_notebook(range(25))], per_row=5)
如上所述,使用i7-8700 CPU和GTX 1080 GPU,训练基础模型大约需要2天,而bootstrap版本需要3天时间。超过50%的时间用于Linear Assignment Problem,因为所使用的算法具有 O(n3) O ( n 3 ) 复杂性并且提供了精确的解决方案。然而,score matrix是随机的,因此投入大量时间来计算随机输入的精确解决方案是浪费的。 在本次竞赛的背景下,对运行时和小数据集没有约束,这是一个实用的选择。 然而,为了扩展这种方法,较低成本的随机匹配启发式将更有效。
训练可扩展性的另一种方法是将训练数据划分为不同的子集,每个子集被单独处理以匹配图像对。 每次计算cost matrix时,可以随机重建子集。 这不仅对 Linear Assignment Problem部分有效,而且在计算仍具有复杂度 O(n2) O ( n 2 ) 的cost matrix时也是有效的。 通过将子集大小固定为合理的值,复杂度随着子集的数量线性增长,从而允许更大的训练数据集。
分数 | 描述 |
---|---|
0.786 | 通过standard model与bootstrap model的线性组合获得的最佳分数 |
0.774 | bootstrapped model |
0.766 | standard model |
0.752 | VGG架构的standard model |
0.728 | 没有 L2 L 2 正则化的standard model(250个epoch后的结果) |
0.714 | 没有排除列表,旋转列表和bbox模型的标准模型(即没有对训练集进行手动判断) |
0.423 | 提交结果只有重复图像和new_whale |
0.325 | 提交结果只有new_whale |
0.107 | 提交结果只有重复图像 |
到目前为止,我还没有讨论验证数据集。 在研究过程中,我使用了由训练集中的570个图像组成的验证集来测试想法并调整训练过程。 但是,通过使用所有数据重新训练模型,重复在验证集上成功的过程,可以实现更高的准确性。 本文基本上描述了这种最终的再训练,因此没有涉及验证集。