医学影像报告异常检测线上0.895开源

简介: 医学影像报告异常检测线上0.895开源

赛题:全球人工智能技术创新大赛赛道一: 医学影像报告异常检测

赛题背景

影像科医生在工作时会观察医学影像(如CT、核磁共振影像),并对其作出描述,这些描述中包含了大量医学信息,对医疗AI具有重要意义。本任务需要参赛队伍根据医生对CT的影像描述文本数据,判断身体若干目标区域是否有异常以及异常的类型。初赛阶段仅需判断各区域是否有异常,复赛阶段除了判断有异常的区域外,还需判断异常的类型。判断的结果按照指定评价指标进行评测和排名,得分最优者获胜。


赛题描述及数据说明

sample数据

医生对若干CT的影像描述的明文数据,及描述中有异常区域与异常类型的label。样本数量为10份,以便使参赛队伍对比赛数据有直观的了解(Sample数据只是为了增进参赛选手对医疗影像描述的直观了解,实际训练与测试数据不一定与Sample数据具有相同特征或分布)。


每份样本占一行,使用分隔符“|,|”分割为3列,为不带表头的CSV数据格式。


列名 类型 示例

report_ID int 1

description string,影像描述 右下肺野见小结节样影与软组织肿块影

label 由两部分组成。第一部分为若干异常区域ID,用空格分割。第二部分为若干异常类型ID,用空格分割。两部分用逗号“,”分割。若定义中所有区域均无异常,则两部分均为空,此项为“,”。 4,1 2

需要预测的人体区域有17个,复赛中需要判断的异常类型有12种。由于数据安全需要,不会告知具体区域与类型的名称,只会以ID表示,区域ID为0到16,类型ID为0到11。每个影像描述中可能有零个、一个或多个区域存在异常;若此描述有异常区域,则可能包含一个或多个异常类型。


Training数据

脱敏后的影像描述与对应label。影像描述以字为单位脱敏,使用空格分割。初赛只进行各区域有无异常的判断,label只有异常区域ID。复赛除了判断各区域有无异常,还需要判断各区域异常的类型,因此label包含异常区域ID与异常类型ID。初赛Training集规模为10000例样本,复赛Training集规模为20000例样本。Training数据用于参赛选手的模型训练与预估。


初赛Training数据格式(不同列使用分隔符“|,|”分割):

列名 类型 示例

report_ID int 1

description 脱敏后的影像描述,以字为单位使用空格分割 101 47 12 66 74 90 0 411 234 79 175

label 由多个异常区域ID组成,以空格分隔。若此描述中无异常区域,则为空 3 4

复赛Training数据格式(不同列使用分隔符“|,|”分割):

列名 类型 示例

report_ID int 1

description 脱敏后的影像描述,以字为单位使用空格分割 101 47 12 66 74 90 0 411 234 79 175

label string,由两部分组成。第一部分为若干异常区域ID,用空格分割。第二部分为若干异常类型ID,用空格分割。两部分用逗号“,”分割。若定义中所有区域均无异常,则两部分均为空,此项为“,”。 3 4,0 2

Test数据

脱敏后的影像描述,脱敏方法和Training相同。Test数据用于参赛选手的模型评估和排名。初赛Test集分为AB榜,规模均为3000。复赛Test集规模为5000。


Test数据格式(不同列使用分隔符“|,|”分割):


列名 类型 示例

report_ID int 1

description 脱敏后的影像描述,以字为单位使用空格分割 101 47 12 66 74 90 0 411 234 79 175

提交说明

对于Test数据report_ID,description,选手应提交report_ID,prediction,其中prediction是预测结果。初赛中prediction是17维向量,值在0到1之间,表示各区域有异常的概率,使用空格分割。复赛中prediction是29维向量,值在0到1之间,前17个值表示17个区域有异常的概率,后12个值表示此描述包含各异常类型的概率。


初赛提交数据格式(不同列使用分隔符“|,|”分割):

列名 类型 示例

report_ID int 1

Prediction 17维向量 0.68 0.82 0.92 0.59 0.71 0.23 0.45 0.36 0.46 0.64 0.92 0.66 0.3 0.5 0.94 0.7 0.38

复赛提交数据格式(不同列使用分隔符“|,|”分割):

列名 类型 示例

report_ID int 1

Prediction 29维向量(中间不需要使用逗号分隔) 0.68 0.82 0.92 0.59 0.71 0.23 0.45 0.36 0.46 0.64 0.92 0.66 0.3 0.5 0.94 0.7 0.38 0.05 0.97 0.71 0.5 0.64 0.0 0.54 0.5 0.49 0.41 0.06 0.07

评估标准

在Test数据上将对选手提交结果计算mlogloss作为评估标准,最终分数为1-mlogloss。

在初赛阶段,一个样本对应M(M=17)个预测值,N个样本共MN个预测值。对此MN个值的真实值与预测值计算mlogloss,计算方式如下:




其中y_{n,m}yn,m 和\hat{y}_{n,m}y^n,m分别是第n个样本第m个标签的真实值和预测值。


初赛分数 S=1-mlogloss。为了让分数区间更合理,复赛阶段调整为S=1-2*mlogloss。


在复赛阶段,分数由两部分组成。第一部分与初赛相同,对预测值的前17维结合真实值计算S_1S1得到 。第二部分为对所有实际存在异常区域的测试样本,对其预测值后12维结合真实异常类型进行计算,方法与第一部分相同,若N个测试样本中有K个实际有异常区域,则将对12K个值进行计算(实际无异常的样本不参与第二部分计算),得到S_2S2。复赛最终分数S=0.6S_1+0.4S_2S=0.6S1+0.4S2。


开源方案

 本方案根据线上84.7的开源分享(基于textCNN)-天池技术圈-天池技术讨论区,提供的Baseline做的修改,主要修改该了Mode、数据处理部分。baseline的特点:


本baselien基于textCNN构建


构建词汇表,总共858个词语,编号为0-857。

统一样本的长度,这里选择50个词语作为样本长度,多的截断,少的补齐(用858补齐)

textCNN的第一层是对原始序列进行enmbeding,对每一个词都enmbed到固定维度,然后使用CNN来进行特征提取。

最后的输出采取BECWithlogitLoss()

线下验证指标采取auc和logloss两种方案

我主要做的修改:


统一样本的长度,这里选择64个词语作为样本长度,多的截断,少的补齐(用0补齐,用0补齐后大约有0.05的提高)

textCNN的第一层是对原始序列进行enmbeding,对每一个词都enmbed到固定维度,然后使用CNN来进行特征提取,enmbeding改为64.

最终的初赛成绩是88,这个很奇怪,我记着当时已经下滑到100以外了,加上没有思路我就放弃了,错过一次进决赛的机会。

tt.png




Net

详见:net.py


我总结一下我的Net的特点:


1、引入了3个TransformerEncoder编码器,我看到有人在kaggle的一个比赛top1方案中使用了,所以就拿过试试。


2、使用CNN的主力机制Coordinate Attention ,参考这篇: 注意力机制在CNN中使用总结_AI浩-CSDN博客


3、使用Mish激活函数。


4、构架多尺度的网络融合,和残差。


模型代码:

import torch
import torch.nn as nn
import torch.nn.functional as F
from collections import OrderedDict
channelNum = 64
class CA_Block(nn.Module):
    def __init__(self, channel, h, w, reduction=16):
        super(CA_Block, self).__init__()
        self.h = h
        self.w = w
        self.avg_pool_x = nn.AdaptiveAvgPool2d((h, 1))
        self.avg_pool_y = nn.AdaptiveAvgPool2d((1, w))
        self.conv_1x1 = nn.Conv2d(in_channels=channel, out_channels=channel // reduction, kernel_size=1, stride=1,
                                  bias=False)
        self.relu = nn.ReLU()
        self.bn = nn.BatchNorm2d(channel // reduction)
        self.F_h = nn.Conv2d(in_channels=channel // reduction, out_channels=channel, kernel_size=1, stride=1,
                             bias=False)
        self.F_w = nn.Conv2d(in_channels=channel // reduction, out_channels=channel, kernel_size=1, stride=1,
                             bias=False)
        self.sigmoid_h = nn.Sigmoid()
        self.sigmoid_w = nn.Sigmoid()
    def forward(self, x):
        x_h = self.avg_pool_x(x).permute(0, 1, 3, 2)
        x_w = self.avg_pool_y(x)
        x_cat_conv_relu = self.relu(self.conv_1x1(torch.cat((x_h, x_w), 3)))
        x_cat_conv_split_h, x_cat_conv_split_w = x_cat_conv_relu.split([self.h, self.w], 3)
        s_h = self.sigmoid_h(self.F_h(x_cat_conv_split_h.permute(0, 1, 3, 2)))
        s_w = self.sigmoid_w(self.F_w(x_cat_conv_split_w))
        out = x * s_h.expand_as(x) * s_w.expand_as(x)
        return out
class Mish(torch.nn.Module):
    def __init__(self):
        super().__init__()
    def forward(self, x):
        x = x * (torch.tanh(torch.nn.functional.softplus(x)))
        return x
class ConvBN(nn.Sequential):
    def __init__(self, in_planes, out_planes, kernel_size, stride=1, groups=1):
        if not isinstance(kernel_size, int):
            padding = [(i - 1) // 2 for i in kernel_size]
        else:
            padding = (kernel_size - 1) // 2
        super(ConvBN, self).__init__(OrderedDict([
            ('conv', nn.Conv2d(in_planes, out_planes, kernel_size, stride,
                               padding=padding, groups=groups, bias=False)),
            ('bn', nn.BatchNorm2d(out_planes)),
            # ('Mish', Mish())
            ('Mish', nn.LeakyReLU(negative_slope=0.3, inplace=False))
        ]))
class ResBlock(nn.Module):
    """
    Sequential residual blocks each of which consists of \
    two convolution layers.
    Args:
        ch (int): number of input and output channels.
        nblocks (int): number of residual blocks.
        shortcut (bool): if True, residual tensor addition is enabled.
    """
    def __init__(self, ch, nblocks=1, shortcut=True):
        super().__init__()
        self.shortcut = shortcut
        self.module_list = nn.ModuleList()
        for i in range(nblocks):
            resblock_one = nn.ModuleList()
            resblock_one.append(ConvBN(ch, ch, 1))
            resblock_one.append(Mish())
            resblock_one.append(ConvBN(ch, ch, 3))
            resblock_one.append(Mish())
            self.module_list.append(resblock_one)
    def forward(self, x):
        for module in self.module_list:
            h = x
            for res in module:
                h = res(h)
            x = x + h if self.shortcut else h
        return x
class Encoder_conv(nn.Module):
    def __init__(self, in_planes=128, blocks=2, h=32, w=64):
        super().__init__()
        self.conv2 = ConvBN(in_planes, in_planes * 2, [1, 9])
        self.conv3 = ConvBN(in_planes * 2, in_planes * 4, [9, 1])
        self.conv4 = ConvBN(in_planes * 4, in_planes, 1)
        self.resBlock = ResBlock(ch=in_planes, nblocks=blocks)
        self.conv5 = ConvBN(in_planes, in_planes * 2, [1, 7])
        self.conv6 = ConvBN(in_planes * 2, in_planes * 4, [7, 1])
        self.conv7 = ConvBN(in_planes * 4, in_planes, 1)
        self.eca = CA_Block(in_planes, h=h, w=w)
        self.relu = Mish()
    def forward(self, input):
        x2 = self.conv2(input)
        x3 = self.conv3(x2)
        x4 = self.conv4(x3)
        r1 = self.resBlock(x4)
        x5 = self.conv5(r1)
        x6 = self.conv6(x5)
        x7 = self.conv7(x6)
        x8 = self.relu(x7 + x4)
        e = self.eca(x8)
        return e
class TransformerEncoder(torch.nn.Module):
    def __init__(self, embed_dim, num_heads, dropout, feedforward_dim):
        super().__init__()
        self.attn = torch.nn.MultiheadAttention(embed_dim, num_heads, dropout=dropout)
        self.linear_1 = torch.nn.Linear(embed_dim, feedforward_dim)
        self.linear_2 = torch.nn.Linear(feedforward_dim, embed_dim)
        self.layernorm_1 = torch.nn.LayerNorm(embed_dim)
        self.layernorm_2 = torch.nn.LayerNorm(embed_dim)
    def forward(self, x_in):
        attn_out, _ = self.attn(x_in, x_in, x_in)
        x = self.layernorm_1(x_in + attn_out)
        ff_out = self.linear_2(torch.nn.functional.relu(self.linear_1(x)))
        x = self.layernorm_2(x + ff_out)
        return x
class CNN_Text(nn.Module):
    def __init__(self, embed_num, static=False):
        super(CNN_Text, self).__init__()
        embed_dim = 128
        class_num = 17
        Ci = 1
        self.embed = nn.Embedding(embed_num, embed_dim)  # 词嵌入
        self.tram = TransformerEncoder(embed_dim, 8, 0.5, 512)
        self.encoder_2 = TransformerEncoder(embed_dim, 8, 0.5, 512)
        self.encoder_3 = TransformerEncoder(embed_dim, 8, 0.5, 512)
        self.encoder1 = nn.Sequential(OrderedDict([
            ("conv3_bn3", ConvBN(Ci, channelNum, 1)),
            ("encoder_conv1", Encoder_conv(channelNum, blocks=2, h=64, w=128)),
        ]))
        self.encoder2 = nn.Sequential(OrderedDict([
            ("conv3_bn_3,3", ConvBN(Ci, channelNum * 2, 3)),
            ("conv3_bn1,1", ConvBN(channelNum * 2, channelNum // 2, 1)),
            ('se', CA_Block(channelNum // 2, h=64, w=128)),
        ]))
        self.encoder3 = nn.Sequential(OrderedDict([
            ("conv3_bn3", ConvBN(Ci, channelNum * 2, [1, 3])),
            ("conv3_bn31", ConvBN(channelNum * 2, channelNum // 2, [3, 1])),
            ('se', CA_Block(channelNum // 2, h=64, w=128)),
        ]))
        self.encoder_conv = Encoder_conv(channelNum * 2)
        self.encoder_conv1 = nn.Sequential(OrderedDict([
            ("conv1x1_bn", ConvBN(channelNum * 2, 1, 1)),
        ]))
        self.con1 = ConvBN(Ci, channelNum * 2, 1, stride=2)
        self.relu = Mish()
        self.pool = nn.AvgPool2d(2);
        self.fc1 = nn.Linear(2048, class_num)
        self.dp = nn.Dropout(0.5)
        self.sg = nn.Sigmoid()
        if static:
            self.embed.weight.requires_grad = False
    def forward(self, x):
        x = self.embed(x)  # (N, W, D)-batch,单词数量,维度
        x1=self.tram(x)
        x2=self.encoder_2(x1)
        x = self.encoder_3(x2)
        x = x.unsqueeze(1)  # (N, Ci, W, D)
        x = self.sg(x)
        x0 = self.con1(x)
        encode1 = self.encoder1(x)
        encode2 = self.encoder2(x)
        encode3 = self.encoder3(x)
        x = torch.cat((encode1, encode2, encode3), dim=1)
        x = self.relu(x)
        x = self.pool(x)
        x = self.encoder_conv(x)
        x = self.relu(x + x0)
        x = self.encoder_conv1(x)
        x = x.contiguous().view(-1, 2048)
        x = self.dp(x)
        logit = self.fc1(x)  # (N, C)
        return logit
if __name__ == "__main__":
    net = CNN_Text(embed_num=1000)
    x = torch.LongTensor([[1, 2, 4, 5, 2, 35, 43, 113, 111, 451, 455, 22, 45, 55],
                          [14, 3, 12, 9, 13, 4, 51, 45, 53, 17, 57, 954, 156, 23]])
    logit = net(x)
    print(net)

数据分析

详见eda.py

我在数据分析上做的主要工作有:

1、数据长度的分析,数据最长是104,最短是4,平均值是41.

2、找出高频词,并在加载数据时,将高频词去除。(此操作不但没有提升分数,反而下降了,不学科啊)

import pandas as pd
import numpy as np
from collections import Counter
#
train_df=pd.read_csv('data/track1_round1_train_20210222.csv',header=None)
test_df=pd.read_csv('data/track1_round1_testA_20210222.csv',header=None) 
#
train_df.columns=['report_ID','description','label']
test_df.columns=['report_ID','description']
train_df.drop(['report_ID'],axis=1,inplace=True)
test_df.drop(['report_ID'],axis=1,inplace=True)
print("train_df:{},test_df:{}".format(train_df.shape,test_df.shape))
#
new_des=[i.strip('|').strip() for i in train_df['description'].values]
new_label=[i.strip('|').strip() for i in train_df['label'].values]
train_df['description']=new_des
train_df['label']=new_label
new_des_test=[i.strip('|').strip() for i in test_df['description'].values]
test_df['description']=new_des_test
#
word_all=[]
len_list=[]
for i in range(len(new_des)):
    tmp=[int(i) for i in new_des[i].split(' ')]
    word_all+=tmp
    len_list.append(len(tmp))
for i in range(len(new_des_test)):
    tmp=[int(i) for i in new_des_test[i].split(' ')]
    word_all+=tmp
    len_list.append(len(tmp))
#
print(train_df['label'].unique())
a=Counter(word_all)
print(len(a))
a=dict(a)
a=sorted(a)#0-857 
#print(a)
print(np.max(len_list),np.min(len_list),np.mean(len_list))

训练

这里面我对loss做了修改,本次loss如下:

tt.png

pytorch中最接近的的loss是torch.nn.BCEWithLogitsLoss(),但是在计算有差别,所以我尝试自己写loss。

def logloss(y_true, y_pred):
    # Clip y_pred between eps and 1-eps
    p = torch.clamp(y_pred, 1e-5, 1-1e-5)
    loss = torch.sum(y_true * torch.log(p) + (1 - y_true) * torch.log(1 - p))
    return loss / len(y_true)
class Muti_logloss(torch.nn.Module):
    def __init__(self):
        super(Muti_logloss, self).__init__()
    def forward(self, y, y_p):
        allloss = []
        for i in range(y.shape[1]):
            loss = logloss(y[:, i], y_p[:, i])
            allloss.append(loss)
        allloss = torch.tensor(allloss, dtype=torch.float)
        alllosssum = torch.sum(allloss)
        lossre = alllosssum / (y.shape[1])
        lossre = -Variable(lossre, requires_grad=True)
        return lossre

写完loss后,接着训练,发现loss一直不收敛,找了几个大佬帮我核对loss没有问题。不知道哪里出问题了,先记录。最后还是用的 BCEWithLogitsLoss()。

完整的代码连接:

医学影像报告异常检测0.8956.zip-深度学习文档类资源-CSDN下载v

目录
相关文章
|
Ubuntu 安全 Unix
Ubuntu内核OverlayFS权限逃逸漏洞(CVE-2021-3493)
Linux Kernel 一般指Linux内核。Linux是一种开源电脑操作系统内核。它是一个用C语言写成,符合POSIX标准的类Unix操作系统。
388 2
|
机器学习/深度学习 PyTorch 算法框架/工具
【实操】涨点神器你还不会,快点进来学习Label Smooth
【实操】涨点神器你还不会,快点进来学习Label Smooth
398 1
|
机器学习/深度学习 算法 测试技术
Python中实现多层感知机(MLP)的深度学习模型
Python中实现多层感知机(MLP)的深度学习模型
717 0
|
人工智能 运维 数据安全/隐私保护
【阿里天池赛题】2021年赛道一:医学影像报告异常检测
本文介绍了2021年阿里天池全球人工智能技术创新大赛的医学影像报告异常检测赛题,包括赛题背景、数据说明、评估标准和比赛规则。
221 8
|
IDE Java 开发工具
Spring Boot:加速Java后端开发的现代化利器
在当今快速迭代的软件开发环境中,Spring Boot 已成为Java后端开发领域的首选框架。作为Spring家族的一员,它以“约定优于配置”的设计理念,极大地简化了传统Spring应用的配置和部署过程,让开发者能够更加专注于业务逻辑的实现。本文将探讨Spring Boot的核心优势,并通过一个简单的示例展示如何快速启动一个基于Spring Boot的Java Web应用。
443 1
|
存储 安全 Java
Qt线程池+生产者消费者模型
Qt线程池+生产者消费者模型
653 5
ly~
|
供应链 监控 搜索推荐
大数据的应用场景
大数据在众多行业中的应用场景广泛,涵盖金融、零售、医疗保健、交通物流、制造、能源、政府公共服务及教育等领域。在金融行业,大数据用于风险评估、精准营销、反欺诈以及决策支持;零售业则应用于商品推荐、供应链管理和门店运营优化等;医疗保健领域利用大数据进行疾病预测、辅助诊断和医疗质量评估;交通物流业通过大数据优化物流配送、交通管理和运输安全;制造业则在生产过程优化、设备维护和供应链协同方面受益;能源行业运用大数据提升智能电网管理和能源勘探效率;政府和公共服务部门借助大数据改善城市管理、政务服务及公共安全;教育行业通过大数据实现个性化学习和资源优化配置;体育娱乐业则利用大数据提升赛事分析和娱乐制作水平。
ly~
2535 2
|
JavaScript 前端开发 API
Vue 使用vue完成登录+注册前后端交互的实现
Vue 使用vue完成登录+注册前后端交互的实现
740 0
|
物联网 数据处理
LLM-05 大模型 15分钟 FineTuning 微调 ChatGLM3-6B(微调实战1) 官方案例 3090 24GB实战 需22GB显存 LoRA微调 P-TuningV2微调
LLM-05 大模型 15分钟 FineTuning 微调 ChatGLM3-6B(微调实战1) 官方案例 3090 24GB实战 需22GB显存 LoRA微调 P-TuningV2微调
336 0
|
计算机视觉 Python
【Python实战】——Python+Opencv是实现车牌自动识别
【Python实战】——Python+Opencv是实现车牌自动识别