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

目录
相关文章
|
2月前
|
机器学习/深度学习 数据采集 TensorFlow
使用Python实现深度学习模型:智能健康监测与预警
使用Python实现深度学习模型:智能健康监测与预警
204 9
|
2月前
|
人工智能 自然语言处理 数据库
从数据洞察到智能决策:合合信息&infiniflow RAG技术的实战案例分享
【9月更文挑战第3天】从数据洞察到智能决策:合合信息&infiniflow RAG技术的实战案例分享
|
3月前
|
人工智能 运维 数据挖掘
【阿里天池-医学影像报告异常检测】1 数据分析和探索
关于医学影像报告异常检测竞赛的数据分析和探索。文中详细介绍了数据读取、统计词数、词频统计、句子长度统计、标签类别分布、正负样本分布、缺失值检查和句子最后一个字符的词频统计。通过这些分析,文章为理解数据集的特征提供了全面的视角,并为后续的模型训练和优化奠定了基础。
44 3
|
3月前
|
机器学习/深度学习 算法 搜索推荐
基于机器学习的用户行为分析:深入洞察与精准决策
【8月更文挑战第3天】基于机器学习的用户行为分析为企业提供了深入了解用户需求、优化产品设计和制定精准营销策略的有力工具。随着人工智能和大数据技术的不断发展,用户行为分析将更加智能化和个性化。未来,我们可以期待更加高效、精准的机器学习算法和模型的出现,以及更多创新性的应用场景的拓展。同时,也需要关注数据隐私和安全性问题,确保用户数据的安全和合规使用。
|
3月前
|
机器学习/深度学习 运维 算法
【阿里天池-医学影像报告异常检测】2 特征工程
本文详细介绍了在医学影像报告异常检测任务中进行特征工程的步骤和方法。
33 1
|
4月前
|
机器学习/深度学习 数据采集 人工智能
探索机器学习在医疗诊断中的应用
【7月更文挑战第23天】随着人工智能技术的飞速发展,机器学习已经成为推动现代医学革新的关键力量。本文将深入探讨机器学习如何在医疗诊断领域发挥作用,包括疾病预测、影像分析以及个性化治疗等方面。通过具体案例,我们将展示机器学习技术如何提高诊断的准确性和效率,同时讨论其在实际应用中面临的挑战与限制。
|
5月前
|
机器学习/深度学习 数据采集 算法
基于机器学习的糖尿病风险预警分析系统是一个非常有用的应用
基于机器学习的糖尿病风险预警分析系统是一个非常有用的应用
64 1
|
5月前
|
机器学习/深度学习 数据采集 监控
基于机器学习的糖尿病风险预警分析系统
基于机器学习的糖尿病风险预警分析系统
59 1
|
4月前
|
机器学习/深度学习 运维 监控
智能运维的崛起:机器学习在故障预测中的应用
随着信息技术的快速发展,传统的运维模式已无法满足现代企业的需求。本文将探讨如何利用机器学习技术进行故障预测,以实现智能运维。我们将通过数据分析和实验结果来展示机器学习在故障预测中的有效性,并讨论其在实际运维中的应用前景。
91 0
|
5月前
|
机器学习/深度学习 监控 算法
【机器学习】农田智能监控系统的实践探索
【机器学习】农田智能监控系统的实践探索
123 0
下一篇
无影云桌面