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

网友投稿 714 2022-05-30

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

赛题背景

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

赛题描述及数据说明

sample数据

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

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

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

Training数据

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

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

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

Test数据

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

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

提交说明

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

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

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

评估标准

在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以外了,加上没有思路我就放弃了,错过一次进决赛的机会。

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

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

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如下:

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下载

医疗 机器学习

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Excel图表移动的正确操作是怎样(移动excel图表的方法是什么)
下一篇:redhat 7更换使用centos 7的yum源(redhat Linux)
相关文章