本次竞赛为2023年初的一场kaggle竞赛,最终成绩铜牌。

RSNA Screening Mammography Breast Cancer Detection

1.赛题分析

对于北美放射学会给出的乳腺癌数据集进行图像分类,判断对于患者而言患癌症的问题,本质为图像分类问题。本次赛题的难点在于数据集的高度不平衡(2%左右的正类样本)以及数据集十分庞大(300G+)的dicom文件,所以数据的预处理是本次竞赛中重要的部分。

在这次比赛中有许多数据处理步骤十分巧妙,有的参赛选手使用CBIS外部数据集训练Unet+的分割辅助本次模型的训练,还有的参赛选手使用yolov5(v7)辅助提取CT图片中特定区域,或者使用Opencv库对图片进行roi区域提取。同时,数据增强也是非常重要的方法,因为正类样本数量及其少,一切预处理方法都是为了让模型学习到更多的分类特征。

本赛题评分标准为pf1指标。

2.数据集特征

除了图片数据,赛题还提供了更多额外信息,下面依次对其含义进行研究:

site_id:代表图片数据来源的医院

patient_id:代表患者的身份编号

image_id:代表目标图片的编号

laterality:代表乳房图片的左侧或者右侧

view:代表图片拍摄的方向

age:代表目标患者的年龄,以年为单位

implant:代表患者乳房是否有植入物信息

density:代表患者乳房的密度分级指标(只在training的csv数据中存在)

machine_id:代表图片来源的拍摄机器编号

cancer:关键标签,代表患者是否患有癌症

biopsy:代表患者是否进行了后续的活检(只在training的csv数据中存在)

invasive:代表乳房是否对于检验呈阳性,无论是否有侵袭性的癌症(只在training的csv中存在)

BIRADS:代表患者需要后续随访的标签

prediction_id:在提交时用于区分预测值的标签(只存在于测试csv中)

difficult_negative_case:代表是否为极难区分的图片,(只在training的csv中存在)

3.思路分析

首先对于图片数据,对于dicom格式是直接送入模型进行训练的,所以我们首先考虑将图片转换为png或jpeg格式的,而对于图片转换应当考虑如下几个问题:

(1).图片转换为png(jpeg)图片的尺寸问题,对于高分辨率图片转换为低分辨率图片的过程,必然存在像素损失,对于阳性样本本来就很少的样本,如果调整为过小的尺寸,则可能会损失大量关键信息。但考虑到gpu设备的内存问题,过大的尺寸必然导致训练速度慢甚至难以进行训练,经过折中考虑,首先选择 512 ∗ 512 512*512 512∗512图片进行训练,大致对于训练数据进行探索,后续采用 1024 ∗ 1024 1024*1024 1024∗1024以及更大尺寸进行训练。

(2).图片转换库的选择问题:常见的dicom转换为png格式的库为pydicom和dicomsdl,而采用gpu转换的库还有nvidia支持的dali库,使用gpu对于转换过程进行加速。对于本次比赛,竞赛要求为笔记本运行时长不允许超过9h,所以选择合适的库就变成了很关键的问题,经过实验,三者的速度为dali>dicomsdl>pydicom,由于对于dali处理图片并不熟悉且所需要的代码量不小,同时dicomsdl转换所有图片大致需要5h,已可以满足<9H的时间需求,故本次竞赛的提交过程和训练过程的数据准备均采用dicomsdl进行编写。

第二是对于数据不平衡问题的处理,常见的思路有:1.自定义采样器,对于样本少的正类样本进行多次采样。2.调整损失函数,对于二分类问题使用带权重的交叉熵损失函数。3.对于损失函数,还可以采用FocalLoss抑制样本不均匀问题。具体使用哪种方法应当采取实验决定,对于不同的预处理方法,可能需要采用的处理不平衡方法也不同,对于不同的模型,FocalLoss和带权重的交叉熵损失函数可能也无绝对的好坏之分,一切处理方法都应当适应实际数据。

第三是对于图片的增强问题,对于pytorch编写的pipeline,常用的图像增强库有torchvision(基于pytorch官方实现),albumentations(基于torchvision和opencv实现),Opencv。在kaggle竞赛中albumentations是常用的图像增强库,包含内容十分全面。具体的图像增强同样应取决于数据。

第四是对于模型的选择,对于分类模型,截止目前常用的有resnet系列及其变体,efficientnet系列及其变体,在比赛后期排行榜前列使用很多的Convnext新版本。在竞赛后期经过各个参赛者的交流讨论,模型选择对于比赛成绩的影响并没有很大,数据处理对于结果的影响远大于模型的选择,故在本次比赛中主要采用了efficientnetv2s进行训练,efficientnetv2s优点在于相较于efficientnet有了速度上的提升,同时所需的gpu空间减少,相较resnet有性能上的提升,由于gpu配置有限,没有进行Convnextv2的实验,但从最终结果来看,Convnetxt的性能应略优于efficientnetv2s。

4.图片预处理(通过dicomsdl将原始图片转换为png图像)

此处以提交过程的图片处理作为例子:

#对于图片处理需要用到的库进行安装
try:import dicomsdl
else:!pip install dicomsdl
try:import gdcm
else:!pip install gdcm
try:import pylibjpeg
else:!pip install pylibjpeg
#对用到的其他库进行载入
import glob
import shutil
import os
from PIL import Image
import cv2
#对于目标图片进行路径筛选
test_images = glob.glob("/kaggle/input/rsna-breast-cancer-detection/test_images/*/*.dcm")
save_path = "/kaggle/temp/"#临时保存temp文件夹中,便于训练过程的图片载入
INPUT_SIZE = 1024#目标图片尺寸
os.makedirs(save_path, exist_ok = True)#创建temp文件夹,如果存在则不操作
df = pd.read_csv('/kaggle/input/rsna-breast-cancer-detection/test.csv')#载入test过程所需的csv
#编写区域提取函数
def img2roi(img):# Binarize the imagebin_img = cv2.threshold(img, 20, 255, cv2.THRESH_BINARY)[1]# Make contours around the binarized image, keep only the largest contourcontours, _ = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)contour = max(contours, key=cv2.contourArea)# Find ROI from largest contourys = contour.squeeze()[:, 0]xs = contour.squeeze()[:, 1]roi =  img[np.min(xs):np.max(xs), np.min(ys):np.max(ys)]return roi
def process(path, size):patient = path.split('/')[-2]image = path.split('/')[-1][:-4]dicom = dicomsdl.open(path)img = dicom.pixelData(storedvalue=False)img = (img - img.min()) / (img.max()-img.min())#对于不同的背景颜色,转换为相同的背景if dicom.PhotometricInterpretation == "MONOCHROME1":img = 1.0- imgimg = (img*255).astype(np.uint8)img = img2roi(img)final_img = Image.fromarray(img)#将图片大小转换为1024*512,便于统一输入模型的图片大小final_img = final_img.resize((int(INPUT_SIZE/2),int(INPUT_SIZE)),Image.Resampling.LANCZOS)final_img.save(save_path+f"{patient}_{image}.png")
_ = Parallel(n_jobs=4)(delayed(process)(uid, size=INPUT_SIZE)for uid in tqdm(test_images)
)#采取多线程处理,加快处理速度
#len(os.listdir(save_path))
#查看是否所有图片都转换完成

实际训练过程中使用kaggle制作好的dataset直接进行训练,减少大量时间,但提交过程因为不允许访问互联网,故应当采取上述编写方式进行编写。

5.不平衡问题csv的处理

方法1:

#自定义采样器
from torch.utils.data import WeightedRandomSampler
def getweight(cur):cancer_weight = (cur.shape[0]-cur.cancer.sum())/cur.cancer.sum()/8normal_weight = 1weights = []for i in range(len(cur)):if cur.iloc[i]['cancer'] == 1:weights.append(cancer_weight)else:weights.append(normal_weight)return weights

在加载数据时,使用实例化后的WeightedRandomSampler作为sampler参数传入dataloader,注意这里dataloader中的shuffle应当为false,否则报错。

方法2:

人为重复癌症数据N次,这里使用3次

df_train = pd.read_csv('/kaggle/input/rsna-breast-cancer-detection/train.csv')
pos = df_train[df_train['cancer'] == 1]
for i in range(3):df_train = df_train.append(pos)
df_train.index = range(len(df_train))

对于上述两种方法,本质差别不大,仅在于对于重复次数(本质为权重问题)的不同处理,在本次竞赛中使用法1实验发现过拟合验证,考虑参数调整不合适,同时考虑到shuffle和测试集数据分布大致为2%,使用法2稍微进行阳性样本的过采样。

6.csv数据的后续处理

使用患者patient_id作为分组条件,分为N_FOLDS组,便于后续进行交叉验证:

CATEGORY_AUX_TARGETS = ['site_id', 'laterality', 'view', 'implant', 'biopsy', 'invasive', 'BIRADS', 'density', 'difficult_negative_case', 'machine_id', 'age']
TARGET = 'cancer'
ALL_FEAT = [TARGET] + CATEGORY_AUX_TARGETS
N_FOLDS = 5
FOLDS = np.array(os.environ.get('FOLDS', '0,1,2,3,4').split(',')).astype(int)
from sklearn.model_selection import StratifiedGroupKFold
import numpy as np
import pandas as pd
from sklearn.model_selection import GroupKFold
from sklearn.preprocessing import LabelEncoder
#分为5组,便于后续5折交叉验证
split = StratifiedGroupKFold(N_FOLDS)
for k, (_, test_idx) in enumerate(split.split(df_train, df_train.cancer, groups=df_train.patient_id)):df_train.loc[test_idx, 'split'] = k
df_train.split = df_train.split.astype(int)#将数据属于哪一组标记在csv文件最后一列
df_train.groupby('split').cancer.mean()#查看每一组的癌症患者占比
df_train.age.fillna(df_train.age.mean(), inplace=True)#补充年龄数据中缺少项
df_train['age'] = pd.qcut(df_train.age, 10, labels=range(10), retbins=False).astype(int)#将年龄分为10组进行编码
#对其他行进行int类型的编码转换
df_train[CATEGORY_AUX_TARGETS] = df_train[CATEGORY_AUX_TARGETS].apply(LabelEncoder().fit_transform)

7.图像增强

import albumentations as A
from albumentations.pytorch.transforms import ToTensorV2
#对于训练集的图像增强
augmentation = A.Compose([#A.Transpose(p=0.5),#考虑到测试集与训练集的数据分布,不采用transpose变换A.HorizontalFlip(p=0.5),A.VerticalFlip(p=0.5),A.RandomBrightness(p=0.5, limit=0.2),A.RandomContrast(p=0.5, limit=0.2),   A.OneOf([A.MotionBlur(blur_limit=5),A.MedianBlur(blur_limit=5),A.GaussianBlur(blur_limit=5),A.GaussNoise(var_limit=(0.5, 30))],p=0.5),A.CLAHE(clip_limit=4.0, p=0.5),A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=10, p=0.5),A.Resize(1024, 512),A.Cutout(max_h_size=int(1024 * 0.1), max_w_size=int(512 * 0.1), num_holes=4, p=0.5),  A.Normalize(),ToTensorV2(),
])
#对于验证集的图像增强
aug_resize_norm = A.Compose([A.Resize(1024, 512),A.Normalize(),ToTensorV2(),
])

8.模型的导入

try:import timm
except:!pip install timm -q

9.模型的结构改进

class BreastCancerModel(torch.nn.Module):def __init__(self, aux_classes, model_type=Config.MODEL_TYPE, dropout=0.):super().__init__()self.model = create_model(model_type, pretrained=True, drop_rate = 0.4, drop_path_rate = 0.3)self.backbone_dim = self.model(torch.randn(1, 3, 1024, 512)).shape[-1]#cancer标签的输出头self.nn_cancer = torch.nn.Sequential(torch.nn.Linear(self.backbone_dim, 1),)#其他辅助标签的输出头self.nn_aux = torch.nn.ModuleList([torch.nn.Linear(self.backbone_dim, n) for n in aux_classes])def forward(self, x):# returns logitsx = self.model(x)cancer = self.nn_cancer(x).squeeze()aux = []for nn in self.nn_aux:aux.append(nn(x).squeeze())return cancer, auxdef predict(self, x):cancer, aux = self.forward(x)sigaux = []for a in aux:sigaux.append(torch.softmax(a, dim=-1))return torch.sigmoid(cancer), sigauxAUX_TARGET_NCLASSES = df_train[CATEGORY_AUX_TARGETS].max() + 1

这里我们使用竞赛中给的额外数据作为辅助损失训练模型,具体操作方法如下:模型对于imagenet最后的fc层为1280->1000,我们将后面跟上1000->1作为癌症标签的输出头,同理1000->mi作为第i个标签的输出头,在后续计算损失时同时计算癌症加其他标签的损失,然后梯度回传训练模型参数,这样相当于通过辅助标签辅助了主要的癌症标签的训练,而对于loss中两种损失比例的调整便可以调整两者的梯度回传力度。

10.DataSet的重写

class BreastCancerDataSet(torch.utils.data.Dataset):def __init__(self, df, path, transforms=None):super().__init__()self.df = dfself.path = pathself.transforms = transformsdef __getitem__(self, i):path = f'{self.path}/{self.df.iloc[i].patient_id}_{self.df.iloc[i].image_id}.png'try:img = cv2.imread(path)except Exception as ex:print(path, ex)return Noneif self.transforms is not None:img = self.transforms(image=img)['image']else:img = img.astype(np.float32)if TARGET in self.df.columns:cancer_target = torch.as_tensor(self.df.iloc[i].cancer)cat_aux_targets = torch.as_tensor(self.df.iloc[i][CATEGORY_AUX_TARGETS])return img, cancer_target, cat_aux_targetsreturn imgdef __len__(self):return len(self.df)

11.损失函数的选择

法1:BCEFocalLoss

class BCEFocalLoss(torch.nn.Module):def __init__(self, gamma=2, alpha=0.94, reduction='mean'):super(BCEFocalLoss, self).__init__()self.gamma = gammaself.alpha = alphaself.reduction = reductiondef forward(self, pt, target):loss = - self.alpha * ((1 - pt) ** self.gamma) * target * torch.log(pt) - (1 - self.alpha) * (pt ** self.gamma) * (1 - target) * torch.log(1 - pt)if self.reduction == 'mean':loss = torch.mean(loss)elif self.reduction == 'sum':loss = torch.sum(loss)return loss

法2:

 cancer_loss = torch.nn.functional.binary_cross_entropy_with_logits(y_cancer_pred,y_cancer.to(float).to(DEVICE),pos_weight=torch.tensor([config.POSITIVE_TARGET_WEIGHT]).to(DEVICE))

两者其实差别不大,discussion区讨论后认为binary_cross_entropy_with_logits已足以完成本次竞赛工作,通过实验我癌症标签使用Focalloss,而在辅助标签时使用普通cross_entropy作为损失函数,同时两者使用超参数调整总损失。

12.其他需要的辅助函数

1.验证指标相关

from sklearn.metrics import accuracy_score
def accuracy(labels, predictions, thr):acc = accuracy_score(labels, predictions>thr)return acc
def pfbeta(labels, predictions, beta=1.):y_true_count = 0ctp = 0cfp = 0for idx in range(len(labels)):prediction = min(max(predictions[idx], 0), 1)if (labels[idx]):y_true_count += 1ctp += predictionelse:cfp += predictionbeta_squared = beta * betac_precision = ctp / (ctp + cfp)c_recall = ctp / max(y_true_count, 1)  # avoid / 0if (c_precision > 0 and c_recall > 0):result = (1 + beta_squared) * (c_precision * c_recall) / (beta_squared * c_precision + c_recall)return resultelse:return 0def optimal_f1(labels, predictions):thres = np.linspace(0, 1, 201)f1s = [pfbeta(labels, predictions > thr) for thr in thres]idx = np.argmax(f1s)return f1s[idx], thres[idx]

2.模型的保存与加载

def save_model(name, model, thres, model_type):torch.save({'model': model.state_dict(), 'threshold': thres, 'model_type': model_type}, save_path+f'{name}')
def load_model(path, model=None):data = torch.load(path, map_location=DEVICE)if model is None:model = BreastCancerModel(AUX_TARGET_NCLASSES, data['model_type'])model.load_state_dict(data['model'])return model, data['threshold'], data['model_type']

3.内存释放相关

def gc_collect():gc.collect()torch.cuda.empty_cache()

13.其他相关说明

1.使用AdamW进行参数更新,使用weight_decay抑制过拟合现象

2.使用梯度累积以获得较大的batchsize,因为显卡只有一块TITANXp,最大单次batchsize只能为8

3.使用半精训练,提高训练速度

4.使用OneCycleLR,设置预热占比为0.1-0.15

5.使用的timm模型加载预训练参数(经实验如果pretrained=False,图像极难学习到内容,pf1在0.04左右)

6.对efficientnetv2s模型使用dropout和drop path,抑制过拟合现象

7.使用wandb实时检测训练进度,同时便于可视化模型表现

8.考虑使用TTA获取更高的测试分数

9.考虑赛题使用pf1作为打分依据,考虑使用二进制化预测值提高验证分数,采取启发式搜索寻找最佳阈值

kaggle竞赛RSNA Screening Mammography Breast Cancer Detection相关推荐

  1. Breast cancer detection in rotational thermography images using texture feature

    基于纹理特征的旋转热成像图像乳腺癌检测 Abstract 乳腺癌是发展中国家年轻妇女死亡的主要原因.早期诊断是提高癌症患者生存率的关键.乳腺热成像是一种诊断程序,它对乳腺表面的红外辐射进行非侵入性成像 ...

  2. 论文阅读2018-Deep Convolutional Neural Networks for breast cancer screening 重点:利用迁移学习三个网络常规化进行分类

    论文阅读2018-Deep Convolutional Neural Networks for breast cancer screening 摘要:我们探讨了迁移学习的重要性,并通过实验确定了在训练 ...

  3. 《Kaggle Histopathologic Cancer Detection癌症图像分类比赛》之Keras/Generator实现

    项目地址:https://www.kaggle.com/c/histopathologic-cancer-detection/overview 本文记录了自己使用纯Keras以及Keras标准的Gen ...

  4. 【数据竞赛】kaggle竞赛宝典-多分类相关指标优化​

    ↑↑↑关注后"星标"kaggle竞赛宝典 kaggle竞赛宝典 作者: 尘沙杰少.谢嘉嘉.DOTA.有夕 赛题理解,分析,规划之多分类相关指标优化 这是一个系列篇,后续我们会按照我 ...

  5. 【竞赛相关】Kaggle竞赛宝典国内外竞赛方案汇总

    本文汇总了kaggle竞赛宝典将近50个竞赛的Top方案&思路&代码. 2019年度所有国内外竞赛汇总 干货 | 2019 数据竞赛TOP方案合集 数据竞赛2019年度年鉴发布(250 ...

  6. 【数据竞赛】Kaggle竞赛宝典国内外竞赛方案汇总

    本文汇总了kaggle竞赛宝典将近50个竞赛的Top方案&思路&代码.关注"kaggle竞赛宝典"并在后台输入"竞赛宝典",还将获得5个kagg ...

  7. 太强了!Kaggle竞赛宝典方案汇总

    kaggle竞赛宝典公众号建立的初衷是希望将很多经典比赛的方案写成案例的形式,每个步骤都配有一定的阐述,讲述某些操作为什么有效等,希望可以帮助更多的新手快速入门(太多的开源只有代码,看代码的时间是巨大 ...

  8. Kaggle竞赛宝典方案汇总

    kaggle竞赛宝典公众号建立的初衷是希望将很多经典比赛的方案写成案例的形式,每个步骤都配有一定的阐述,讲述某些操作为什么有效等,希望可以帮助更多的新手快速入门(太多的开源只有代码,看代码的时间是巨大 ...

  9. Python视觉深度学习系列教程 第三卷 第9章 Kaggle竞赛:情绪识别

            第三卷 第九章 Kaggle竞赛:情绪识别 在本章中,我们将解决Kaggle的面部表情识别挑战.为了完成这项任务,我们将在训练数据上从头开始训练一个类似VGG的网络,同时考虑到我们的网 ...

最新文章

  1. Android之异步消息处理机制Handler源码解析
  2. 通过IIS操作修改服务器文件没有权限的解决办法
  3. 图像检索:几类基于内容的图像分类技术
  4. java反射的优化_请问Java反射的性能为什么比直接调用慢一个数量级左右?
  5. matlab pca求曲率和法向量_实践课堂 已给出实例的MATLAB求解大汇总(线性规划)...
  6. 前端学习(1267):axios的post传参
  7. 动手学深度学习(PyTorch实现)(五)--多层感知机
  8. PyTorch 1.0 中文官方教程:数据加载和处理教程
  9. day13、1 - 抓包--科来软件使用
  10. oracle大写数字转小写,求助oracle小写金额转换大写金额的函数
  11. 如何判断一个数是素数(质数)
  12. Spring学习02-Spring中的设计模式(一)
  13. Win8企业版如何升级至win10专业版
  14. 分布式任务调度平台201902
  15. IT人士如何提高软件下载的速度
  16. 研发流程中的产品测试
  17. 全网最详细elasticsearch7.10.2安装手册
  18. Flutter iOS调用系统相机里面的文字显示英文
  19. 基于VB的Picture绘图
  20. 解决“错误0x80071ac3”文件无法写入移动硬盘

热门文章

  1. android 串口一直打开_Android串口使用总结
  2. 搭建webrtc视频服务rtcmulticonnection-server完整教程
  3. 其境遇与文章中描写的现实恰恰相反科技的
  4. TCP三次握手四次挥手及time_wait状态解析
  5. 【BUG】AttributeError: module ‘gast‘ has no attribute ‘Index’
  6. js实现鼠标滚轮放大缩小页面指定区域(css3 scale)
  7. 计算机系统英语作文结尾万能句子,英语作文万能句子:结尾时作总结的10个万能句...
  8. 基于物品 的协同过滤推荐算法(ItemCF)
  9. Excel字符串替换(replace)
  10. Linux安装miniconda