首页 > 其他分享 >xView2 比赛冠军代码解读

xView2 比赛冠军代码解读

时间:2024-05-19 12:51:24浏览次数:23  
标签:py img 模型 self random 解读 msk 冠军 xView2

CSDN搬家失败,手动导出markdown后再导入博客园

代码地址:https://github.com/vdurnov/xview2_1st_place_solution

模型训练中用到了混合精度训练工具 Nvidia apex图像增强工具 imgaug

目录

1、readme

权重文件

数据清洗

数据处理

模型细节

2、代码结构

3、定位模型

3.1 数据集

3.2 模型结构

3.3 优化器

3.4 损失函数

4 分类模型

4.1 数据集

4.2 模型结构

4.3 损失函数

5 预测阶段(待完善)

5.1 定位模型

5.2 分类模型

5.3 模型结果整合

1、readme

权重文件

https://vdurnov.s3.amazonaws.com/xview2_1st_weights.zip

数据清洗

本次比赛的数据集非常完善,未发现有任何问题。使用 json 文件创建 mask 图像,将 “un-classified” 标签归到 “no-damage” 类别(create_masks.py)。

灾前和灾后图像,由于拍摄时的天底(nadir)不同,导致有微小的偏移。这个问题在模型层面被解决:

  1. 定位模型只使用灾前图像,以忽略灾后图像产生的这些偏移噪声。这里使用了 Unet-like 这种编码 - 解码结构的神经网络。
  2. 训练完成的定位模型转化为用于分类的孪生神经网络。如此,灾前和灾后的图像共享定位模型的权重,把最后一层解码层输出的特征 concat 来预测每个像素的损坏等级。这种方式使得神经网络使用相同的方式分别观察灾前和灾后图像,有助于忽略这些漂移。
  3. 使用 5*5 的 kernel 对分类 mask 做形态学膨胀,这使得预测结果更 “bold”(理解为边界更宽一点),这有助于提升边界精度,并消除漂移和天底影响。

数据处理

模型输入的尺寸从(448,448)到(736,736),越重的编码器结构采用越小的尺寸。训练所用的数据增强手段有:

  • 翻转(often)
  • 旋转(often)
  • 缩放(often)
  • 颜色 shift(rare)
  • 直方图、模糊、噪声(rare)
  • 饱和度、亮度、对比度(rare)
  • 弹性变换(rare)

推理阶段使用全尺寸图像(1024,1024),使用 4 中简单的测试阶段增强(原图,左右反转,上下翻转,180 度旋转)

模型细节

所有模型训练集 / 验证集比例为 9:1,每个模型使用 3 个随机数种子训练 3 次,保存最高验证集精度的 checkpoint,结果保存在 3 个文件夹中。

定位模型使用了 torchvision 中预训练的 4 个 encoder 模型:

  • ResNet34
  • se_resnext50_32x4d
  • SeNet154
  • Dpn92

定位模型在灾前图像上训练,灾后图像只在极少数情况下作为额外的数据增强手段添加。

定位模型训练阶段参数:

  • 损失函数:Dice+Focal
  • 验证指标:Dice
  • 优化器:AdamW

分类模型使用对应的定位模型(和随机数种子)初始化。分类模型实际上是使用了整个定位模型的孪生神经网络,同时输入灾前和灾后图像。解码器最后一层的特征结合后用于分类。预训练的权重并不冻结。使用定位模型的预训练权重使得分类模型训练更快,精度更高。从灾前和灾后图像提取的特征在解码器的最后连接(bottleneck),这有助于防止过拟合,生成适用性更强的模型。

分类模型训练阶段参数:

  • 损失函数:Dice+Focal + 交叉熵。交叉熵损失函数中 2-4 级破坏的系数更大一些。
  • 验证指标:比赛提供的 metric
  • 优化器:AdamW
  • 采样:2-4 级破坏采样 2 次,对其更关注

所有 checkpoint 最后在整个训练集上做少次微调,使用低学习率和少量数据增强。

最终预测结果是把定位和分类模型的输出分别做平均。

定位模型对受损和未受损的类别使用不同的阈值(受损的更高)。

Pytorch 预训练模型: https://github.com/Cadene/pretrained-models.pytorch

2、代码结构

首先看 train.sh 脚本。

echo "Creating masks..."
python create_masks.py
echo "Masks created"
 
echo "training seresnext50 localization model with seeds 0-2"
python train50_loc.py 0
python train50_loc.py 1
python train50_loc.py 2
python tune50_loc.py 0
python tune50_loc.py 1
python tune50_loc.py 2

该脚本使用 3 个不同的随机数种子将每个模型训练 3 次,然后微调,先训练定位模型,在训练分类模型,所有权重保存在 weights 文件夹中。

然后使用 predict.sh 脚本预测

python predict34_loc.py
python predict50_loc.py
python predict92_loc.py
python predict154_loc.py
python predict34cls.py 0
python predict34cls.py 1
python predict34cls.py 2
python predict50cls.py 0
python predict50cls.py 1
python predict50cls.py 2
python predict92cls.py 0
python predict92cls.py 1
python predict92cls.py 2
python predict154cls.py 0
python predict154cls.py 1
python predict154cls.py 2
echo "submission start!"
python create_submission.py
echo "submission created!"

使用所有模型进行定位和预测后,使用平均方式整合结果。

3、定位模型

下面以 ResNet34_Unet 为例讲解(train34_loc.py),其他模型除了结构以外几乎一样。首先看主函数,主要重点在于数据集、模型结构、优化器、损失函数

cv2.setNumThreads(0)
cv2.ocl.setUseOpenCL(False)
 
train_dirs = ['train', 'tier3']
 
models_folder = 'weights'
 
input_shape = (736, 736)
 
if __name__ == '__main__':
    t0 = timeit.default_timer()
 
    makedirs(models_folder, exist_ok=True)
 # 命令行接受一个参数,作为随机数种子
    seed = int(sys.argv[1])
 # vis_dev = sys.argv[2]
 # os.environ['CUDA_DEVICE_ORDER'] = 'PCI_BUS_ID'
 # os.environ["CUDA_VISIBLE_DEVICES"] = vis_dev
 
    cudnn.benchmark = True
 
    batch_size = 16
    val_batch_size = 8
 
    snapshot_name = 'res34_loc_{}_1'.format(seed)
 
    train_idxs, val_idxs = train_test_split(np.arange(len(all_files)), test_size=0.1, random_state=seed)
 
    np.random.seed(seed + 545)
    random.seed(seed + 454)
 
    steps_per_epoch = len(train_idxs) // batch_size
    validation_steps = len(val_idxs) // val_batch_size
 
    print('steps_per_epoch', steps_per_epoch, 'validation_steps', validation_steps)
 # 重点1:数据增强
    data_train = TrainData(train_idxs)
    val_train = ValData(val_idxs)
 
    train_data_loader = DataLoader(data_train, batch_size=batch_size, num_workers=6, shuffle=True, pin_memory=False, drop_last=True)
    val_data_loader = DataLoader(val_train, batch_size=val_batch_size, num_workers=6, shuffle=False, pin_memory=False)
 # 重点2:模型结构
    model = Res34_Unet_Loc()
    
    params = model.parameters()
 # 重点3:优化器
    optimizer = AdamW(params, lr=0.00015, weight_decay=1e-6)
 
    scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[5, 11, 17, 25, 33, 47, 50, 60, 70, 90, 110, 130, 150, 170, 180, 190], gamma=0.5)
 
    model = nn.DataParallel(model).cuda()
 # 重点4:损失函数
    seg_loss = ComboLoss({'dice': 1.0, 'focal': 10.0}, per_image=False).cuda() #True
 
    best_score = 0
    _cnt = -1
    torch.cuda.empty_cache()
    for epoch in range(55):
        train_epoch(epoch, seg_loss, model, optimizer, scheduler, train_data_loader)
        if epoch % 2 == 0:
            _cnt += 1
            torch.cuda.empty_cache()
            best_score = evaluate_val(val_data_loader, best_score, model, snapshot_name, epoch)
 
    elapsed = timeit.default_timer() - t0
    print('Time: {:.3f} min'.format(elapsed / 60))

3.1 数据集

以训练集为例,添加注释

class TrainData(Dataset):
    def __init__(self, train_idxs):
        super().__init__()
        self.train_idxs = train_idxs
        self.elastic = iaa.ElasticTransformation(alpha=(0.25, 1.2), sigma=0.2)
 
    def __len__(self):
        return len(self.train_idxs)
 
    def __getitem__(self, idx):
        _idx = self.train_idxs[idx]
 
        fn = all_files[_idx]
 
        img = cv2.imread(fn, cv2.IMREAD_COLOR)
 # 少量使用灾后图像
        if random.random() > 0.985:
            img = cv2.imread(fn.replace('_pre_disaster', '_post_disaster'), cv2.IMREAD_COLOR)
 
        msk0 = cv2.imread(fn.replace('/images/', '/masks/'), cv2.IMREAD_UNCHANGED)
 # 水平翻转
        if random.random() > 0.5:
            img = img[::-1, ...]
            msk0 = msk0[::-1, ...]
 # 旋转
        if random.random() > 0.05:
            rot = random.randrange(4)
            if rot > 0:
                img = np.rot90(img, k=rot)
                msk0 = np.rot90(msk0, k=rot)
 # 仿射变换偏移
        if random.random() > 0.8:
            shift_pnt = (random.randint(-320, 320), random.randint(-320, 320))
            img = shift_image(img, shift_pnt)
            msk0 = shift_image(msk0, shift_pnt)
 # 旋转缩放
        if random.random() > 0.2:
            rot_pnt =  (img.shape[0] // 2 + random.randint(-320, 320), img.shape[1] // 2 + random.randint(-320, 320))
            scale = 0.9 + random.random() * 0.2
            angle = random.randint(0, 20) - 10
            if (angle != 0) or (scale != 1):
                img = rotate_image(img, angle, scale, rot_pnt)
                msk0 = rotate_image(msk0, angle, scale, rot_pnt)
 # 裁剪
        crop_size = input_shape[0]
        if random.random() > 0.3:
            crop_size = random.randint(int(input_shape[0] / 1.2), int(input_shape[0] / 0.8))
 
        bst_x0 = random.randint(0, img.shape[1] - crop_size)
        bst_y0 = random.randint(0, img.shape[0] - crop_size)
        bst_sc = -1
        try_cnt = random.randint(1, 5)
        for i in range(try_cnt):
            x0 = random.randint(0, img.shape[1] - crop_size)
            y0 = random.randint(0, img.shape[0] - crop_size)
            _sc = msk0[y0:y0+crop_size, x0:x0+crop_size].sum()
            if _sc > bst_sc:
                bst_sc = _sc
                bst_x0 = x0
                bst_y0 = y0
        x0 = bst_x0
        y0 = bst_y0
        img = img[y0:y0+crop_size, x0:x0+crop_size, :]
        msk0 = msk0[y0:y0+crop_size, x0:x0+crop_size]
 
        
        if crop_size != input_shape[0]:
            img = cv2.resize(img, input_shape, interpolation=cv2.INTER_LINEAR)
            msk0 = cv2.resize(msk0, input_shape, interpolation=cv2.INTER_LINEAR)
 # RGB通道变换
        if random.random() > 0.97:
            img = shift_channels(img, random.randint(-5, 5), random.randint(-5, 5), random.randint(-5, 5))
        elif random.random() > 0.97:
            img = change_hsv(img, random.randint(-5, 5), random.randint(-5, 5), random.randint(-5, 5))
 # 直方图、高斯噪声、滤波
        if random.random() > 0.93:
            if random.random() > 0.97:
                img = clahe(img)
            elif random.random() > 0.97:
                img = gauss_noise(img)
            elif random.random() > 0.97:
                img = cv2.blur(img, (3, 3))
 # 饱和度、亮度、对比度
        elif random.random() > 0.93:
            if random.random() > 0.97:
                img = saturation(img, 0.9 + random.random() * 0.2)
            elif random.random() > 0.97:
                img = brightness(img, 0.9 + random.random() * 0.2)
            elif random.random() > 0.97:
                img = contrast(img, 0.9 + random.random() * 0.2)
 # 弹性变换 
        if random.random() > 0.97:
            el_det = self.elastic.to_deterministic()
            img = el_det.augment_image(img)
 
        msk = msk0[..., np.newaxis]
 # msk二值化
        msk = (msk > 127) * 1
 # 像素归一化到(-1,1)
        img = preprocess_inputs(img)
 
        img = torch.from_numpy(img.transpose((2, 0, 1))).float()
        msk = torch.from_numpy(msk.transpose((2, 0, 1))).long()
 
        sample = {'img': img, 'msk': msk, 'fn': fn}
        return sample

3.2 模型结构

# w = w - wd * lr * w
if group['weight_decay'] != 0:
    p.data.add_(-group['weight_decay'] * group['lr'], p.data)
 
# w = w - lr * w.grad
p.data.addcdiv_(-step_size, exp_avg, denom)

3.3 优化器

这里采用了修正后的 Adam 优化器

AdamW 原文:https://arxiv.org/abs/1711.05101

大体意思是说现在常用的深度学习框架,在实现权重衰减的时候,都是使用直接在损失函数上 L2 正则来近似的。而实际上只有在使用 SGD 优化器的时候,L2 正则和权重衰减才等价。

在使用 Adam 优化器时,由于 L2 正则项里面要除以 w 的平方,导致越大的权重实际衰减的越小,不符合预期,因此采用直接做权重衰减的方式来修正。

class ComboLoss(nn.Module):
    def __init__(self, weights, per_image=False):
        super().__init__()
        self.weights = weights
        self.bce = StableBCELoss()
        self.dice = DiceLoss(per_image=False)
        self.jaccard = JaccardLoss(per_image=False)
        self.lovasz = LovaszLoss(per_image=per_image)
        self.lovasz_sigmoid = LovaszLossSigmoid(per_image=per_image)
        self.focal = FocalLoss2d()
        self.mapping = {'bce': self.bce,
                        'dice': self.dice,
                        'focal': self.focal,
                        'jaccard': self.jaccard,
                        'lovasz': self.lovasz,
                        'lovasz_sigmoid': self.lovasz_sigmoid}
        self.expect_sigmoid = {'dice', 'focal', 'jaccard', 'lovasz_sigmoid'}
        self.values = {}

3.4 损失函数

损失函数这里先介绍下作者自己定义的一个组合损失函数类

def forward(self, x):
        dec10_0 = self.forward1(x[:, :3, :, :])
        dec10_1 = self.forward1(x[:, 3:, :, :])
        dec10 = torch.cat([dec10_0, dec10_1], 1)
        return self.res(dec10)

里面定义了多个损失函数,使用的时候给不同的 loss 赋予权重即可,如

loss0 = seg_loss(out[:, 0, ...], msks[:, 0, ...])
        loss1 = seg_loss(out[:, 1, ...], msks[:, 1, ...])
        loss2 = seg_loss(out[:, 2, ...], msks[:, 2, ...])
        loss3 = seg_loss(out[:, 3, ...], msks[:, 3, ...])
        loss4 = seg_loss(out[:, 4, ...], msks[:, 4, ...])
 
        loss = 0.05 * loss0 + 0.2 * loss1 + 0.8 * loss2 + 0.7 * loss3 + 0.4 * loss4

对于定位模型来说,输出就是一个单通道的图像,直接与 mask 做像素级别的分类损失即可,后面分类模型会复杂一些。

4 分类模型

分类模型与定位模型相比,其实整体上差别不大,只不过是复制了 2 个定位模型构造孪生神经网络,同时输入灾前和灾后的图像,下面具体讲一下(train34_cls.py)。

4.1 数据集

数据增强方法与前面基本一致,主要区别在于 mask 的构造,前面的定位模型是一个单类别的图像分割(有无建筑),这里变成了对建筑进行更高细粒度的 4 类别分割。

数据构造:img 灾前,img2 灾后

  1. msk0 灾前
  2. lbl_msk1 灾后 1~4 都有,原始 mask
  3. msk1~msk4:分别对应灾后的 1~4
  4. msk:msk0~msk4 组合,形成 5 通道 mask
  5. 对 msk 的 1~4 通道做形态学膨胀,然后调整多边形:
    1. 对于 msk1 层,有任何 2~4 级标签的地方均设为 0
    2. 对于 msk3 层,有 2 级的像素点设为 0
    3. 对于 msk4 层,有 2 级和 3 级的像素点均设为 0
    4. 对于 msk0 层,有 1~4 级的像素点均设为 1(0 层用于定位,有建筑就为 1)
  6. lbl_msk:1~4 标签
  7. img 和 img2 组合成 6 通道图像,归一化为 - 1 到 1
  8. 训练集 lbl_msk = msk.argmax(axis=2)
  9. 验证集 lbl_msk = msk[..., 1:].argmax(axis=2)

4.2 模型结构

这里采用孪生神经网络,Res34_Unet_Double,网络结构跟前面单个的 Res34_Unet 完全一致,只是在输出层把两个分支整合

def forward(self, x):
        dec10_0 = self.forward1(x[:, :3, :, :])
        dec10_1 = self.forward1(x[:, 3:, :, :])
        dec10 = torch.cat([dec10_0, dec10_1], 1)
        return self.res(dec10)

最后经过 1*1 卷积 self.res 输出了一个 5 通道的特征图。

在训练时,使用上一步定位模型的权重作为预训练权重。

4.3 损失函数

损失函数的设计跟定位模型一样,但是使用方法不同。

loss0 = seg_loss(out[:, 0, ...], msks[:, 0, ...])
        loss1 = seg_loss(out[:, 1, ...], msks[:, 1, ...])
        loss2 = seg_loss(out[:, 2, ...], msks[:, 2, ...])
        loss3 = seg_loss(out[:, 3, ...], msks[:, 3, ...])
        loss4 = seg_loss(out[:, 4, ...], msks[:, 4, ...])
 
        loss = 0.05 * loss0 + 0.2 * loss1 + 0.8 * loss2 + 0.7 * loss3 + 0.4 * loss4

输出的 5 个通道分别和 mask 的 5 个通道做损失,其中 loss0 为灾前图像上的定位损失,1-4 为灾后图像上 4 个级别的分类损失。

5 预测阶段(待完善)

5.1 定位模型

  1. 每张图片采样 4 次,每张图片做 x,y,xy 翻转,组合成 batchsize=4 的图像输入(4,3,1024,1024)
  2. 模型输出后在经过一个 sigmoid 层,输出 4 个 msk,将每个 mask 按翻转方式还原,然后取平均值作为最终输出
  3. 保存为 part1 图像,单通道

5.2 分类模型

  1. 灾前灾后图像叠加,采样 4 次,组合成(4,6,1024,1024)
  2. 输出经过 sigmoid,输出 4 个 mask,做平均
  3. 由于输出是 5 通道,将前三个通道保存一次 part1,后三个通道保存一次 part2

5.3 模型结果整合

  1. 读取分类输出的 part1 和 part2,重新组合成 5 通道 mask,所有 12 个模型做平均,除以 255 归一化为 0 到 1

  2. 读取定位输出的 part1,做平均,归一化

  3. msk_dmg 为每个通道上输出最大值的位置,即把每个通道上的概率值转化为 1~4 的标签

  4. msk_loc 定位阈值筛选条件: _thr=[0.38,0.13,0.14]

    1. 定位大于阈值 0
    2. 或定位结果大于阈值 1 且 msk_dmg 对应位置大于 1 且小于 4(即 2~3 级破坏)
    3. 或定位结果大于阈值 2 且 msk_dmg 对应位置大于 1(即所有等级破坏)
  5. msk_dmg 更新为 msk_dmg 和 msk_loc 取交集

  6. 对于 2 级破坏,命名为_msk,如果有 2 级破坏  if _msk.sum()> 0,则对其进行膨胀,将膨胀后的_msk 与原始 msk_dmg==1 的位置做交集,即 2 级破坏膨胀后和 1 级破坏做交集,相交(重合)的地方设为 2 级破坏

标签:py,img,模型,self,random,解读,msk,冠军,xView2
From: https://www.cnblogs.com/algorithmSpace/p/18200241

相关文章

  • 振弦采集仪在岩土工程监测中的数据解读与分析
    振弦采集仪在岩土工程监测中的数据解读与分析岩土工程监测是在岩土工程施工过程中进行的一项重要工作,旨在对岩土体的变形和应力变化进行实时监测和分析,以保障工程的安全和稳定。而河北稳控科技振弦采集仪作为岩土工程监测中常用的一种仪器,具有快速、准确、高精度的特点,被广泛应用......
  • 深度解读《深度探索C++对象模型》之C++虚函数实现分析(三)
    “深度解读《深度探索C++对象模型》”系列已经在CSDN上和我的公众号上更新完毕,请有需要的同学移步到我的CSDN主页里去阅读,主页地址:https://blog.csdn.net/iShare_Carlos?spm=1010.2135.3001.5421或者敬请关注我的公众号:iShare爱分享前面两篇请从这里阅读:深度解读《深度探索C+......
  • QT5.0_TensorBoard相关曲线解读
    TensorBoard生成的各种可视化图表可以帮助你解读和分析训练过程中的不同指标。以下是对一些常见图表的解释:1.损失曲线(LossCurve)损失曲线显示了训练过程中的损失(loss)随时间的变化情况。一般会有两条曲线:训练损失和验证损失。训练损失(TrainingLoss):反映模型在训练数据上的表......
  • Oracle-HWM(High Water Mark) 高水位解读
    转自:https://cloud.tencent.com/developer/article/1861861Oracle的逻辑存储管理ORACLE在逻辑存储上分4个粒度,由大到小为:表空间,段,区和块.块Block块:是粒度最小的存储单位,现在标准的块大小是8K,ORACLE每一次I/O操作也是按块来操作的,也就是说当ORACLE从数据文件读数......
  • 顶配将近3万是割韭菜还是真强 新iPad Pro/Air全面解读
    苹果举行了“放飞吧”Apple特别活动,发布了首发M4芯片的iPadPro,两种尺寸的iPadAir,以及全新ApplePencilPro、妙控键盘。值得一提的是,这次新款的iPadPro在将所有配置拉满后,售价接近3万块,苹果到底是在割韭菜还是真的配置强?全新的iPadAir又值得哪些朋友入手?下面就跟随我们一起,全......
  • 第六届·2024 MindSpore 量子计算黑客松热身赛赛题解读
    第六届·2024MindSpore量子计算黑客松火热进行中。本次大赛由量子信息网络产业联盟主办,昇思MindSporeQuantum社区承办,多所高校和单位联合举办。开发者将全面体验全新一代通用量子计算框架MindSporeQuantum。热身赛为量子计算基础学习和编程演练。完成热身赛的前100名选手将有......
  • [智能网联汽车] 解读理想汽车的整车EEA电子电气架构 | 2020 [转]
    0序本文首发于华夏EV网、2020年。时至今日,这篇文章虽已过去了2年,今天的我们,对了解当前整车电子电气架构(EEA)的技术演进仍有一定的参考意义。关键词:整车电子电气架构(EEA)分布式架构域集中式架构中央集中式架构中央计算平台(CCU)导读2020年,当我们谈论整车电子电......
  • JUC源码解析:深入解读偏向锁
    JUC源码解析:深入解读偏向锁本文使用jdk8几种锁状态介绍先介绍一下锁状态吧看偏向锁这一栏,它的内存存储了线程ID和Epoch,这一点尤为关键,意味着偏向锁没有内存可以存储对象头的hashCode,而其他锁是有地方存的.。也就意味着,,当锁对象被隐式(父类)或显试调用了has......
  • 探究职业发展的关键:能力模型解读
    为什么要了解能力模型王阳明曾在《传习录》中提到过一个思想:以终为始。所谓“以终为始”,意味着在行动的开始阶段就要考虑到最终的目标和结果,以此来指导自己的行动和选择。那么如果我们想在自己的行业内获取好的职业发展,第一步不是要努力,而是要知道如何努力。人对于自己未来的恐惧......
  • AdaZoom: Towards Scale-Aware Large Scene Object Detection 论文解读
    《AdaZoom:TowardsScale-AwareLargeSceneObjectDetection》笔记1.研究动机1.1挑战与困难小目标检测和对象尺度差异存在挑战现有研究方法对于大场景中如此极端尺度变化的物体缺乏灵活性,缺乏对不同尺度物体的适应性。1.2解决方案构建了一个自适应缩放网络(简称AdaZoom),对......