医学影像报告异常检测线上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

目录
相关文章
|
3天前
|
机器学习/深度学习 数据采集 算法
基于机器学习的糖尿病风险预警分析系统是一个非常有用的应用
基于机器学习的糖尿病风险预警分析系统是一个非常有用的应用
12 1
|
5天前
|
机器学习/深度学习 数据采集 监控
基于机器学习的糖尿病风险预警分析系统
基于机器学习的糖尿病风险预警分析系统
17 1
|
1月前
|
机器学习/深度学习 传感器 算法
构建未来:基于机器学习的智能健康监测系统
【5月更文挑战第12天】 在数字医疗领域,智能健康监测系统的出现正在革新我们对健康管理和疾病预防的理解。本文将探讨一个基于机器学习技术的智能健康监测系统的设计与实现,它能够实时跟踪个体的健康指标并通过预测性分析提前警示潜在的健康问题。通过融合生物统计学、数据挖掘及模式识别等先进技术,该系统旨在为个人用户提供量身定制的健康建议,并为医疗专业人员提供决策支持。文章首先概述了系统框架和关键技术,随后详细讨论了机器学习模型的建立过程以及如何优化这些模型以提高预测的准确性。最后,我们通过实验结果验证了系统的有效性,并讨论了未来的发展方向。
|
9月前
|
机器学习/深度学习 数据采集 数据处理
如何做一个诊断系统
如何做一个诊断系统
113 0
|
机器学习/深度学习 存储 运维
论文阅读--异常检测中实时大数据处理的研究挑战
论文阅读--异常检测中实时大数据处理的研究挑战
|
运维
《智能运维里的时间序列:异常检测、根源分析、预测》电子版地址
智能运维里的时间序列:异常检测、根源分析、预测
207 0
《智能运维里的时间序列:异常检测、根源分析、预测》电子版地址
|
机器学习/深度学习 存储 Web App开发
机器学习如何做到疫情可视化——疫情数据分析与预测实战
机器学习如何做到疫情可视化——疫情数据分析与预测实战
483 0
|
机器学习/深度学习 数据安全/隐私保护 Python
利用机器学习进行金融数据风险评估(1)
利用机器学习进行金融数据风险评估(1)
465 0
利用机器学习进行金融数据风险评估(1)
|
机器学习/深度学习 数据格式
利用机器学习进行金融数据风险评估(2)
利用机器学习进行金融数据风险评估(2)
202 0
利用机器学习进行金融数据风险评估(2)
|
机器学习/深度学习 测试技术
利用机器学习进行金融数据风险评估(4)
利用机器学习进行金融数据风险评估(4)
301 0
利用机器学习进行金融数据风险评估(4)