RSNA 2024 腰椎退行性分类
- 概述
- 描述
- 评估
- 文件与数据
- 项目正文
- 导入第三方包
- 读取项目数据
- 查看数据
- 生成训练和测试图像的路径列表。
- 显示DICOM图像。
- 加载并显示DICOM图像,并在图像上标注特定的坐标点。
- 多列数据的DataFrame行进行重塑
- 重塑与合并
- 查看特定行的合并结果
- 最终合并后的DataFrame添加两列:row_id 和 image_path
- 特定严重程度(severity)的记录数量
- 生成一个扩展的测试描述数据框
- severity 列的值进行映射
- 检查训练数据
- 加载DICOM图像并进行预处理
- 加载对应的DICOM图像并进行预处理
- 创建自定义数据集和数据加载器
- 可视化数据加载器中的图像批次
- 从 trainloader_t2 数据加载器中获取一个批次的图像和标签
- 自定义的EfficientNetV2模型类
- 计算并打印出 sagittal_t1_model 模型中可训练参数的总数
- 获取一个批次的图像和标签
- 训练神经网络模型
- 遍历 models 字典中的每个模型
- 遍历 train_data 数据框中的 'level' 列
- 更新 expanded_test_desc 数据框中的 row_id 列
- TestDataset 的自定义数据集类
- predict_test_data 函数对测试数据进行预测
- 预测的概率值添加到 expanded_test_desc 数据框中
- 存储在 submission 数据框中
- 保存预测结果
- 查看预测结果
- 完整代码--请在jupyter NoteBook上运行
- 总结
概述
本次项目的目标是创建可用于帮助使用腰椎 MR 图像检测和分类退行性脊柱状况的模型。
描述
根据世界卫生组织的数据,腰痛是全球残疾的主要原因,2020 年影响了 6.19 亿人。大多数人在一生中的某个时刻都会经历腰痛,并且频率会随着年龄的增长而增加。疼痛和活动受限通常是脊椎病的症状,脊椎病是一组退行性脊柱疾病,包括椎间盘退化和随后的椎管狭窄(椎管狭窄)、关节下隐窝或神经孔,并伴有腰部神经的压迫或刺激。
磁共振成像 (MRI) 提供腰椎、椎间盘和神经的详细视图,使放射科医生能够评估这些疾病的存在和严重程度。对这些疾病进行正确诊断和分级有助于指导治疗和可能的手术,以帮助缓解背痛并改善患者的整体健康状况和生活质量。
RSNA 与美国神经放射学会 (ASNR) 合作举办了这项竞赛,探索人工智能是否可用于帮助使用腰椎 MR 图像检测和分类退行性脊柱疾病。
项目将侧重于五种腰椎退行性疾病的分类:左侧神经孔狭窄、右侧神经孔狭窄、左侧关节下狭窄、右侧关节下狭窄和椎管狭窄。对于数据集中的每项影像学研究,我们提供了椎间盘水平 L1/L2、L2/L3、L3/L4、L4/L5 和 L5/S1 的五种情况中的每一种的严重程度评分(正常/轻度、中度或重度)。
为了创建地面实况数据集,RSNA 挑战规划工作组收集了来自五大洲八个站点的成像数据。这个多机构、专业策划的数据集有望改进退行性腰椎疾病的标准化分类,并支持开发工具以自动准确和快速地进行疾病分类。
评估
使用样本加权对数损失的平均值和指标生成的预测来评估提交的内容。
样本权重如下:
1 表示正常/轻度。
2 表示中等。
4 表示严重。
row_id,normal_mild,moderate,severe
123456_left_neural_foraminal_narrowing_l1_l2,0.333,0.333,0.333
123456_left_neural_foraminal_narrowing_l2_l3,0.333,0.333,0.333
123456_left_neural_foraminal_narrowing_l3_l4,0.333,0.333,0.333
etc.
文件与数据
train.csv 训练集的标签。
-study_id- 研究 ID。每个研究可能包括多个系列的图像。
-[condition]_[level]- 目标标签(如 ,)的严重性级别某些条目的标签不完整。spinal_canal_stenosis_l1_l2Normal/MildModerateSevere
train_label_coordinates.csv
-study_id
-series_id- 影像系列 ID。
-instance_number- 图像在 3D 堆栈中的订单号。
-condition- 有三种核心疾病:椎管狭窄、neural_foraminal_narrowing 和 --subarticular_stenosis。后两者考虑用于脊柱的每一侧。
-level- 相关的椎骨,例如l3_l4
-[x/y]- 定义标签的区域中心的 x/y 坐标。
sample_submission.csv
-row_id- 研究 ID、条件和级别的 slug,例如 。12345_spinal_canal_stenosis_l3_l4
[normal_mild/moderate/severe]- 三个 prediction 列。
[train/test]_images/[study_id]/[series_id]/[instance_number].dcm影像数据。
[训练/测试]_series_descriptions.csv
-study_id
-series_id
-series_description扫描的方向。
项目正文
导入第三方包
import os # 提供对操作系统进行访问的功能,例如文件和目录操作
import json # 用于解析和生成JSON数据
import time # 提供时间相关的函数,例如获取当前时间
import timm # 提供预训练的PyTorch模型
import torch # PyTorch深度学习框架
import glob # 用于查找符合特定规则的文件路径
import random # 提供随机数生成和随机选择功能
import warnings # 用于控制警告信息的显示
import numpy as np # 提供科学计算功能,特别是多维数组操作
import pandas as pd # 提供数据分析和操作功能
import seaborn as sns # 基于matplotlib的数据可视化库
import matplotlib.pyplot as plt # 提供绘图功能
import collections # 提供容器数据类型
import torch.nn as nn # 提供神经网络模块
import torchvision.models as models # 提供预训练的计算机视觉模型
import pydicom as dicom # 用于处理DICOM格式的医学图像
import matplotlib.patches as patches # 提供绘制图形的功能
from matplotlib import animation, rc # 提供动画和渲染功能
from pydicom.pixel_data_handlers.util import apply_voi_lut # 用于应用DICOM图像的VOI LUT
from sklearn.model_selection import train_test_split # 用于将数据集划分为训练集和测试集
from torch.utils.data import Dataset, DataLoader # 提供数据集和数据加载器
from torchvision import transforms # 提供图像变换功能
from tqdm import tqdm # 提供进度条功能
from copy import deepcopy # 提供深拷贝功能
warnings.filterwarnings("ignore", category=UserWarning, module="albumentations") # 忽略特定模块的警告信息
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 设置设备为CUDA(如果可用),否则为CPU
读取项目数据
# 指定的路径读取多个CSV文件,并将它们加载到Pandas数据框中。
# 数据框将用于后续的数据处理和分析。
train_path = '请自定义你的文件路径:rsna-2024-lumbar-spine-degenerative-classification/' # 设置训练数据的路径
# 读取训练数据集的CSV文件
train = pd.read_csv(train_path + 'train.csv') # 加载训练数据集
label = pd.read_csv(train_path + 'train_label_coordinates.csv') # 加载训练标签坐标数据集
train_desc = pd.read_csv(train_path + 'train_series_descriptions.csv') # 加载训练系列描述数据集
test_desc = pd.read_csv(train_path + 'test_series_descriptions.csv') # 加载测试系列描述数据集
sub = pd.read_csv(train_path + 'sample_submission.csv') # 加载样本提交数据集
查看数据
# 训练数据集
train
# 训练标签坐标数据集
label
# 训练系列描述数据集
train_desc
# 测试系列描述数据集
test_desc
# 样本提交数据集
sub
train
label
train_desc
test_desc
sub
生成训练和测试图像的路径列表。
通过遍历数据框中的每个研究ID和系列ID,构建图像的完整路径,并将这些路径存储在列表中。
def generate_image_paths(df, data_dir):
"""
生成图像路径列表。
参数:
df (pd.DataFrame): 包含研究ID和系列ID的数据框。
data_dir (str): 数据目录的路径。
返回:
list: 图像路径的列表。
"""
image_paths = [] # 初始化图像路径列表
for study_id, series_id in zip(df['study_id'], df['series_id']): # 遍历每个研究ID和系列ID
study_dir = os.path.join(data_dir, str(study_id)) # 构建研究目录路径
series_dir = os.path.join(study_dir, str(series_id)) # 构建系列目录路径
images = os.listdir(series_dir) # 获取系列目录中的所有图像文件名
image_paths.extend([os.path.join(series_dir, img) for img in images]) # 将每个图像的完整路径添加到列表中
return image_paths # 返回图像路径列表
# 生成训练和测试图像的路径列表
train_image_paths = generate_image_paths(train_desc, f'{train_path}/train_images') # 生成训练图像路径
test_image_paths = generate_image_paths(test_desc, f'{train_path}/test_images') # 生成测试图像路径
显示DICOM图像。
def display_dicom_images(image_paths):
"""
显示DICOM图像。
参数:
image_paths (list): DICOM图像路径的列表。
"""
plt.figure(figsize=(15, 5)) # 设置图像显示的大小
for i, path in enumerate(image_paths[:3]): # 遍历前三个图像路径
ds = dicom.dcmread(path) # 读取DICOM文件
plt.subplot(1, 3, i+1) # 设置子图布局
plt.imshow(ds.pixel_array, cmap=plt.cm.bone) # 显示图像,使用灰度颜色映射
plt.title(f"Image {i+1}") # 设置图像标题
plt.axis('off') # 关闭坐标轴显示
plt.show() # 显示图像
# 显示训练图像路径中的DICOM图像
display_dicom_images(train_image_paths)
# 显示测试图像路径中的DICOM图像
display_dicom_images(test_image_paths)
display_dicom_images(train_image_paths)
display_dicom_images(test_image_paths)
加载并显示DICOM图像,并在图像上标注特定的坐标点。
display_dicom_with_coordinates函数:该函数用于显示DICOM图像,并在图像上标注从数据框中提取的坐标点。每个图像的标题会显示其对应的study_id和series_id。
load_dicom_files函数:该函数用于从指定文件夹中加载DICOM文件,并按文件名排序。
根据指定的study_id找到对应的DICOM图像文件夹,然后加载每个系列(series)的第一个DICOM文件,并调用display_dicom_with_coordinates函数显示这些图像。
def display_dicom_with_coordinates(image_paths, label_df):
# 创建一个子图,子图的数量等于DICOM图像的数量
fig, axs = plt.subplots(1, len(image_paths), figsize=(18, 6))
# 遍历每个DICOM图像路径
for idx, path in enumerate(image_paths):
# 从路径中提取study_id和series_id
study_id = int(path.split('/')[-3])
series_id = int(path.split('/')[-2])
# 根据study_id和series_id过滤标签数据框
filtered_labels = label_df[(label_df['study_id'] == study_id) & (label_df['series_id'] == series_id)]
# 读取DICOM文件
ds = dicom.dcmread(path)
# 显示DICOM图像
axs[idx].imshow(ds.pixel_array, cmap='gray')
axs[idx].set_title(f"Study ID: {study_id}, Series ID: {series_id}")
axs[idx].axis('off')
# 在图像上标注坐标点
for _, row in filtered_labels.iterrows():
axs[idx].plot(row['x'], row['y'], 'ro', markersize=5)
# 调整布局并显示图像
plt.tight_layout()
plt.show()
# 从指定文件夹加载DICOM文件的函数
def load_dicom_files(path_to_folder):
# 获取文件夹中所有以.dcm结尾的文件路径
files = [os.path.join(path_to_folder, f) for f in os.listdir(path_to_folder) if f.endswith('.dcm')]
# 按文件名排序
files.sort(key=lambda x: int(os.path.splitext(os.path.basename(x))[0].split('-')[-1]))
return files
# 指定study_id
study_id = "100206310"
# 构建study文件夹路径
study_folder = f'{train_path}/train_images/{study_id}'
# 初始化图像路径列表
image_paths = []
# 遍历study文件夹中的每个series文件夹
for series_folder in os.listdir(study_folder):
# 构建series文件夹路径
series_folder_path = os.path.join(study_folder, series_folder)
# 加载该series文件夹中的DICOM文件
dicom_files = load_dicom_files(series_folder_path)
# 如果该series文件夹中有DICOM文件,则添加第一个文件到图像路径列表中
if dicom_files:
image_paths.append(dicom_files[0])
# 调用函数显示DICOM图像并标注坐标点
display_dicom_with_coordinates(image_paths, label)
display_dicom_with_coordinates(image_paths, label)
多列数据的DataFrame行进行重塑
将其转换为一个新的DataFrame。具体来说,代码会将每一行中的特定列(不包括study_id, series_id, instance_number, x, y, series_description)进行拆分和重组,生成一个新的DataFrame,其中每一行代表一个特定的条件、级别和严重程度。
def reshape_row(row):
# 初始化一个字典,用于存储重塑后的数据
data = {'study_id': [], 'condition': [], 'level': [], 'severity': []}
# 遍历行的每一列
for column, value in row.items():
# 跳过不需要处理的列
if column not in ['study_id', 'series_id', 'instance_number', 'x', 'y', 'series_description']:
# 将列名拆分为多个部分
parts = column.split('_')
# 将前几个部分组合成条件,并将每个单词首字母大写
condition = ' '.join([word.capitalize() for word in parts[:-2]])
# 将最后两个部分组合成级别,并将每个单词首字母大写
level = parts[-2].capitalize() + '/' + parts[-1].capitalize()
# 将study_id、条件、级别和严重程度添加到字典中
data['study_id'].append(row['study_id'])
data['condition'].append(condition)
data['level'].append(level)
data['severity'].append(value)
# 将字典转换为DataFrame并返回
return pd.DataFrame(data)
# 遍历原始DataFrame的每一行,调用reshape_row函数进行重塑,并将结果合并为一个新DataFrame
new_train_df = pd.concat([reshape_row(row) for _, row in train.iterrows()], ignore_index=True)
new_train_df
重塑与合并
将重塑后的DataFrame与标签数据和描述数据进行合并,生成一个包含所有相关信息的最终DataFrame。
# 将重塑后的DataFrame与标签数据进行合并,基于study_id、condition和level进行内连接
merged_df = pd.merge(new_train_df, label, on=['study_id', 'condition', 'level'], how='inner')
# 将合并后的DataFrame与训练描述数据进行合并,基于series_id进行内连接
final_merged_df = pd.merge(merged_df, train_desc, on='series_id', how='inner')
# 将合并后的DataFrame与训练描述数据进行合并,基于series_id和study_id进行内连接
final_merged_df = pd.merge(merged_df, train_desc, on=['series_id', 'study_id'], how='inner')
# 显示最终合并后的DataFrame的前5行
final_merged_df.head(5)
final_merged_df.head(5)
查看特定行的合并结果
# 从最终合并后的DataFrame中筛选出study_id为100206310的行,并按照x和y列的值进行升序排序
final_merged_df[final_merged_df['study_id'] == 100206310].sort_values(['x', 'y'], ascending=True)
通过筛选和排序操作,提取出特定系列ID的所有记录,并按实例编号进行排序,以便进一步分析或可视化。
# 从最终合并后的DataFrame中筛选出series_id为1012284084的行,并按照instance_number列的值进行排序
final_merged_df[final_merged_df['series_id'] == 1012284084].sort_values("instance_number")
# 从最终合并后的DataFrame中筛选出study_id为1013589491的行,并按照instance_number列的值进行排序
filtered_df = final_merged_df[final_merged_df['study_id'] == 1013589491].sort_values("instance_number")
sorted_final_merged_df = final_merged_df[final_merged_df['study_id'] == 1013589491].sort_values(by=['series_id', 'series_description', 'instance_number'])
最终合并后的DataFrame添加两列:row_id 和 image_path
row_id 列:通过将 study_id、condition 和 level 列的值组合起来,生成一个唯一的标识符。这个标识符有助于在后续分析中唯一标识每一行数据。
image_path 列:通过将 train_path、study_id、series_id 和 instance_number 列的值组合起来,生成每个图像的完整路径。这个路径可以用于后续的图像加载和处理。
# 为最终合并后的DataFrame添加image_path列
final_merged_df['image_path'] = (
f'{train_path}/train_images/' + # 添加训练图像路径前缀
final_merged_df['study_id'].astype(str) + '/' + # 将study_id转换为字符串并添加斜杠
final_merged_df['series_id'].astype(str) + '/' + # 将series_id转换为字符串并添加斜杠
final_merged_df['instance_number'].astype(str) + '.dcm' # 将instance_number转换为字符串并添加.dcm后缀
)
特定严重程度(severity)的记录数量
# 统计最终合并后的DataFrame中severity为"Normal/Mild"的记录数量
normal_mild_count = final_merged_df[final_merged_df["severity"] == "Normal/Mild"].value_counts().sum()
# 统计最终合并后的DataFrame中severity为"Moderate"的记录数量
moderate_count = final_merged_df[final_merged_df["severity"] == "Moderate"].value_counts().sum()
final_merged_df[final_merged_df[“severity”] == “Normal/Mild”]:
从最终合并后的DataFrame final_merged_df 中筛选出 severity 列值为 “Normal/Mild” 的所有行。
.value_counts().sum():
value_counts():计算筛选出的行中每个唯一值的频数。
sum():对频数进行求和,得到 “Normal/Mild” 的总记录数。
final_merged_df[final_merged_df[“severity”] == “Moderate”]:
从最终合并后的DataFrame final_merged_df 中筛选出 severity 列值为 “Moderate” 的所有行。
.value_counts().sum():
value_counts():计算筛选出的行中每个唯一值的频数。
sum():对频数进行求和,得到 “Moderate” 的总记录数。
生成一个扩展的测试描述数据框
base_path:
设置测试图像的基础路径。
get_image_paths 函数:
该函数接收一行数据(row),并根据 study_id 和 series_id 构建系列路径。
如果系列路径存在,返回该路径下的所有文件路径;否则返回空列表。
condition_mapping 字典:
该字典定义了不同系列描述(series_description)对应的条件映射。
例如,‘Sagittal T1’ 对应 ‘left_neural_foraminal_narrowing’ 和 ‘right_neural_foraminal_narrowing’。
expanded_rows 列表:
初始化一个空列表,用于存储扩展后的行数据。
遍历 test_desc 数据框的每一行:
获取图像路径。
根据系列描述获取条件映射。
如果条件是字符串,将其转换为字典。
遍历条件和图像路径,将扩展行添加到 expanded_rows 列表中。
expanded_test_desc DataFrame:
将 expanded_rows 列表转换为 DataFrame,生成扩展后的测试描述数据框。
base_path = '请自定义你的文件路径:rsna-2024-lumbar-spine-degenerative-classification/test_images'
def get_image_paths(row):
# 构建系列路径
series_path = os.path.join(base_path, str(row['study_id']), str(row['series_id']))
# 如果系列路径存在,返回该路径下的所有文件路径
if os.path.exists(series_path):
return [os.path.join(series_path, f) for f in os.listdir(series_path) if os.path.isfile(os.path.join(series_path, f))]
# 如果系列路径不存在,返回空列表
return []
# 条件映射字典
condition_mapping = {
'Sagittal T1': {'left': 'left_neural_foraminal_narrowing', 'right': 'right_neural_foraminal_narrowing'},
'Axial T2': {'left': 'left_subarticular_stenosis', 'right': 'right_subarticular_stenosis'},
'Sagittal T2/STIR': 'spinal_canal_stenosis'
}
# 初始化扩展行列表
expanded_rows = []
# 遍历测试描述数据框的每一行
for index, row in test_desc.iterrows():
# 获取图像路径
image_paths = get_image_paths(row)
# 获取条件映射
conditions = condition_mapping.get(row['series_description'], {})
# 如果条件是字符串,将其转换为字典
if isinstance(conditions, str):
conditions = {'left': conditions, 'right': conditions}
# 遍历条件和图像路径
for side, condition in conditions.items():
for image_path in image_paths:
# 将扩展行添加到列表中
expanded_rows.append({
'study_id': row['study_id'],
'series_id': row['series_id'],
'series_description': row['series_description'],
'image_path': image_path,
'condition': condition,
'row_id': f"{row['study_id']}_{condition}"
})
# 将扩展行列表转换为DataFrame
expanded_test_desc = pd.DataFrame(expanded_rows)
severity 列的值进行映射
# 将最终合并后的DataFrame中的severity列的值进行映射
final_merged_df['severity'] = final_merged_df['severity'].map({
'Normal/Mild': 'normal_mild', # 将"Normal/Mild"映射为"normal_mild"
'Moderate': 'moderate', # 将"Moderate"映射为"moderate"
'Severe': 'severe' # 将"Severe"映射为"severe"
})
test_data = expanded_test_desc
train_data = final_merged_df
检查训练数据
检查训练数据中的 study_id、series_id 和 image_path 是否存在,并将检查结果存储在新的列中。然后,筛选出所有 study_id、series_id 和 image_path 都存在的行,生成一个新的训练数据集。
check_exists 函数:
该函数接收一个路径作为参数,并返回该路径是否存在。
check_study_id 函数:
该函数接收一行数据(row),并根据 study_id 构建路径。
调用 check_exists 函数检查该路径是否存在,并返回结果。
check_series_id 函数:
该函数接收一行数据(row),并根据 study_id 和 series_id 构建路径。
调用 check_exists 函数检查该路径是否存在,并返回结果。
check_image_exists 函数:
该函数接收一行数据(row),并根据 image_path 检查路径是否存在。
调用 check_exists 函数检查该路径是否存在,并返回结果。
检查并存储结果:
使用 apply 方法遍历 train_data 中的每一行,调用 check_study_id、check_series_id 和 check_image_exists 函数,并将结果存储在新的列 study_id_exists、series_id_exists 和 image_exists 中。
筛选数据:
使用布尔索引筛选出所有 study_id_exists、series_id_exists 和 image_exists 都为 True 的行,生成一个新的训练数据集 train_data。
def check_exists(path):
return os.path.exists(path)
# 检查study_id是否存在的函数
def check_study_id(row):
study_id = row['study_id']
path = f'{train_path}/train_images/{study_id}'
return check_exists(path)
# 检查series_id是否存在的函数
def check_series_id(row):
study_id = row['study_id']
series_id = row['series_id']
path = f'{train_path}/train_images/{study_id}/{series_id}'
return check_exists(path)
# 检查image_path是否存在的函数
def check_image_exists(row):
image_path = row['image_path']
return check_exists(path)
# 检查训练数据中的study_id、series_id和image_path是否存在,并将结果存储在新的列中
train_data['study_id_exists'] = train_data.apply(check_study_id, axis=1)
train_data['series_id_exists'] = train_data.apply(check_series_id, axis=1)
train_data['image_exists'] = train_data.apply(check_image_exists, axis=1)
# 筛选出所有study_id、series_id和image_path都存在的行,生成一个新的训练数据集
train_data = train_data[(train_data['study_id_exists']) & (train_data['series_id_exists']) & (train_data['image_exists'])]
加载DICOM图像并进行预处理
将其像素值归一化到0到255之间,并转换为8位无符号整数(uint8)。具体来说,定义了一个函数 load_dicom,该函数接收一个DICOM文件路径作为输入,读取DICOM文件,提取像素数据,并进行归一化和类型转换。
def load_dicom(path):
# 读取DICOM文件
dicom = pyd.read_file(path)
# 提取像素数据
data = dicom.pixel_array
# 将像素数据减去最小值,使最小值为0
data = data - np.min(data)
# 如果像素数据的最大值不为0,将其归一化到0到1之间
if np.max(data) != 0:
data = data / np.max(data)
# 将像素数据乘以255,并转换为8位无符号整数(uint8)
data = (data * 255).astype(np.uint8)
# 返回预处理后的像素数据
return data
加载对应的DICOM图像并进行预处理
从训练数据集中随机选择两行,加载对应的DICOM图像并进行预处理,然后显示这两张图像。具体来说,通过随机选择索引,加载对应的DICOM图像,并将其显示在子图中,同时显示每张图像对应的 row_id。
# 从训练数据集中随机选择两行,加载对应的DICOM图像并进行预处理,然后显示这两张图像
# 初始化图像列表和row_id列表
images = []
row_ids = []
# 从训练数据集中随机选择两个索引
selected_indices = random.sample(range(len(train_data)), 2)
# 遍历选中的索引
for i in selected_indices:
# 加载DICOM图像并进行预处理
image = load_dicom(train_data['image_path'][i])
# 将预处理后的图像添加到图像列表中
images.append(image)
# 将对应的row_id添加到row_id列表中
row_ids.append(train_data['row_id'][i])
# 创建一个包含两个子图的图形
fig, ax = plt.subplots(1, 2, figsize=(8, 4))
# 遍历两个子图
for i in range(2):
# 显示图像,使用灰度颜色映射
ax[i].imshow(images[i], cmap='gray')
# 设置图像标题为对应的row_id
ax[i].set_title(f'Row ID: {row_ids[i]}', fontsize=8)
# 关闭坐标轴显示
ax[i].axis('off')
# 调整布局并显示图像
plt.tight_layout()
plt.show()
train_data = train_data.dropna() # 删除含有空值的行
创建自定义数据集和数据加载器
创建自定义数据集和数据加载器,用于训练和验证深度学习模型。具体来说,定义了一个 CustomDataset 类,用于加载和预处理DICOM图像数据,并将其与标签一起返回。然后,定义了一个函数 create_datasets_and_loaders,用于根据指定的 series_description 创建训练和验证数据集,并生成相应的数据加载器。最后,使用这些数据加载器来加载不同类型的图像数据,并将它们存储在字典中,以便后续的模型训练和验证。
CustomDataset 类:
该类继承自 torch.utils.data.Dataset,用于加载和预处理DICOM图像数据。
init 方法:初始化数据框和图像变换。
len 方法:返回数据集的长度。
getitem 方法:根据索引加载并预处理DICOM图像,并返回图像和标签。
create_datasets_and_loaders 函数:
该函数根据指定的 series_description 筛选数据框,并将数据集划分为训练集和验证集。
创建训练和验证数据集,并生成相应的数据加载器。
返回训练和验证数据加载器以及数据集的长度。
transform 变换:
定义了一系列图像变换操作,包括将图像数据转换为 uint8 类型、转换为 PIL 图像、调整图像大小、转换为灰度图像并输出3个通道、转换为张量。
创建数据集和数据加载器:
调用 create_datasets_and_loaders 函数,创建不同 series_description 的训练和验证数据集,并生成相应的数据加载器。
将数据加载器和数据集长度存储在字典中,以便后续的模型训练和验证。
label_map 标签映射:
定义了标签映射字典,将标签 ‘Mild’、‘Moderate’ 和 ‘Severe’ 分别映射为 0、1 和 2。
class CustomDataset(Dataset):
def __init__(self, dataframe, transform=None):
self.dataframe = dataframe # 存储数据框
self.transform = transform # 存储图像变换
def __len__(self):
return len(self.dataframe) # 返回数据集的长度
def __getitem__(self, index):
image_path = self.dataframe['image_path'][index] # 获取图像路径
image = load_dicom(image_path) # 加载并预处理DICOM图像
label = self.dataframe['severity'][index] # 获取标签
if self.transform:
image = self.transform(image) # 应用图像变换
return image, label # 返回图像和标签
# 创建数据集和数据加载器的函数
def create_datasets_and_loaders(df, series_description, transform, batch_size=8):
filtered_df = df[df['series_description'] == series_description] # 根据series_description筛选数据框
train_df, val_df = train_test_split(filtered_df, test_size=0.2, random_state=42) # 将数据集划分为训练集和验证集
train_df = train_df.reset_index(drop=True) # 重置训练集的索引
val_df = val_df.reset_index(drop=True) # 重置验证集的索引
train_dataset = CustomDataset(train_df, transform) # 创建训练数据集
val_dataset = CustomDataset(val_df, transform) # 创建验证数据集
trainloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # 创建训练数据加载器
valloader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False) # 创建验证数据加载器
return trainloader, valloader, len(train_df), len(val_df) # 返回训练和验证数据加载器以及数据集的长度
# 定义图像变换
transform = transforms.Compose([
transforms.Lambda(lambda x: (x * 255).astype(np.uint8)), # 将图像数据转换为uint8类型
transforms.ToPILImage(), # 将图像数据转换为PIL图像
transforms.Resize((224, 224)), # 调整图像大小为224x224
transforms.Grayscale(num_output_channels=3), # 将图像转换为灰度图像,并输出3个通道
transforms.ToTensor(), # 将图像转换为张量
])
# 初始化数据加载器和数据集长度的字典
dataloaders = {}
lengths = {}
# 创建不同series_description的数据集和数据加载器
trainloader_t1, valloader_t1, len_train_t1, len_val_t1 = create_datasets_and_loaders(train_data, 'Sagittal T1', transform)
trainloader_t2, valloader_t2, len_train_t2, len_val_t2 = create_datasets_and_loaders(train_data, 'Axial T2', transform)
trainloader_t2stir, valloader_t2stir, len_train_t2stir, len_val_t2stir = create_datasets_and_loaders(train_data, 'Sagittal T2/STIR', transform)
# 将数据加载器存储在字典中
dataloaders['Sagittal T1'] = (trainloader_t1, valloader_t1)
dataloaders['Axial T2'] = (trainloader_t2, valloader_t2)
dataloaders['Sagittal T2/STIR'] = (trainloader_t2stir, valloader_t2stir)
# 将数据集长度存储在字典中
lengths['Sagittal T1'] = (len_train_t1, len_val_t1)
lengths['Axial T2'] = (len_train_t2, len_val_t2)
lengths['Sagittal T2/STIR'] = (len_train_t2stir, len_val_t2stir)
# 定义标签映射
label_map = {'Mild': 0, 'Moderate': 1, 'Severe': 2}
可视化数据加载器中的图像批次
定义了一个 visualize_batch 函数,该函数从数据加载器中获取一个批次的图像和标签,并将这些图像显示在子图中,同时显示每个图像对应的标签。然后,分别对 Sagittal T1、Axial T2 和 Sagittal T2/STIR 的训练数据加载器调用 visualize_batch 函数,以可视化这些数据集中的图像批次。
visualize_batch 函数:
该函数接收一个数据加载器 dataloader 作为输入。
images, labels = next(iter(dataloader)):从数据加载器中获取一个批次的图像和标签。
fig, axes = plt.subplots(1, len(images), figsize=(20, 5)):创建一个包含子图的图形,每个子图的尺寸为 20x5。
for i, (img, lbl) in enumerate(zip(images, labels))::遍历图像和标签。
ax = axes[i]:获取当前子图。
img = img.permute(1, 2, 0):调整图像维度顺序,使其适合显示。
ax.imshow(img):在当前子图中显示图像。
ax.set_title(f"Label: {lbl}"):设置当前子图的标题为对应的标签。
ax.axis(‘off’):关闭当前子图的坐标轴显示。
plt.show():显示图形。
可视化不同数据集的样本:
print(“Visualizing Sagittal T1 samples”):打印提示信息。
visualize_batch(trainloader_t1):调用 visualize_batch 函数,可视化 Sagittal T1 训练数据加载器中的图像批次。
print(“Visualizing Axial T2 samples”):打印提示信息。
visualize_batch(trainloader_t2):调用 visualize_batch 函数,可视化 Axial T2 训练数据加载器中的图像批次。
print(“Visualizing Sagittal T2/STIR samples”):打印提示信息。
visualize_batch(trainloader_t2stir):调用 visualize_batch 函数,可视化 Sagittal T2/STIR 训练数据加载器中的图像批次。
def visualize_batch(dataloader):
images, labels = next(iter(dataloader)) # 从数据加载器中获取一个批次的图像和标签
fig, axes = plt.subplots(1, len(images), figsize=(20, 5)) # 创建一个包含子图的图形
for i, (img, lbl) in enumerate(zip(images, labels)): # 遍历图像和标签
ax = axes[i] # 获取当前子图
img = img.permute(1, 2, 0) # 调整图像维度顺序,使其适合显示
ax.imshow(img) # 显示图像
ax.set_title(f"Label: {lbl}") # 设置图像标题为对应的标签
ax.axis('off') # 关闭坐标轴显示
plt.show() # 显示图形
# 可视化Sagittal T1样本
print("Visualizing Sagittal T1 samples")
visualize_batch(trainloader_t1)
# 可视化Axial T2样本
print("Visualizing Axial T2 samples")
visualize_batch(trainloader_t2)
# 可视化Sagittal T2/STIR样本
print("Visualizing Sagittal T2/STIR samples")
visualize_batch(trainloader_t2stir)
从 trainloader_t2 数据加载器中获取一个批次的图像和标签
从 trainloader_t2 数据加载器中获取一个批次的图像和标签,并显示其中的一张图像及其对应的标签。从数据加载器中获取一个批次的图像和标签,然后选择其中一个图像,调整其维度顺序,并使用 Matplotlib 显示该图像及其标签。
# 从trainloader_t2数据加载器中获取一个批次的图像和标签
image, label = next(iter(trainloader_t2))
# 选择其中一个图像,并调整其维度顺序,使其适合显示
sample = image[1].permute(1, 2, 0)
# 设置图形尺寸
plt.figure(figsize=(8, 4))
# 显示图像,使用灰度颜色映射
plt.imshow(sample, cmap='gray')
# 设置图像标题为对应的标签
plt.title(label[1])
# 关闭坐标轴显示
plt.axis('off')
# 调整布局
plt.tight_layout()
# 显示图像
plt.show()
自定义的EfficientNetV2模型类
自定义的EfficientNetV2模型类,并初始化三个不同类型的EfficientNetV2模型,分别用于处理 Sagittal T1、Axial T2 和 Sagittal T2/STIR 图像数据。定义一个 CustomEfficientNetV2 类,这个类继承自 nn.Module,并加载预训练的EfficientNetV2模型权重,然后修改模型的最后一层以适应新的分类任务。接着,初始化了三个模型实例,并将它们移动到GPU(如果可用),然后冻结模型的特征提取部分,只训练分类器部分。最后,定义损失函数和优化器,并将模型和优化器存储在字典中,以便后续的训练和验证。
CustomEfficientNetV2 类:
该类继承自 nn.Module,用于定义自定义的EfficientNetV2模型。
init 方法:初始化EfficientNetV2模型,并加载预训练的模型权重(如果提供)。然后修改模型的最后一层以适应新的分类任务。
forward 方法:定义前向传播。
unfreeze_model 方法:解冻模型特征提取部分的最后20层(不包括BatchNorm层)和分类器部分的参数。
设置设备:
设置设备为CUDA(如果可用),否则为CPU。
预训练权重文件路径:
定义预训练权重文件的路径。
初始化模型:
初始化三个不同类型的EfficientNetV2模型实例,并加载预训练的模型权重。
冻结模型特征提取部分的参数:
冻结 Sagittal T1、Axial T2 和 Sagittal T2/STIR 模型的特征提取部分的参数,使其在训练过程中不更新。
解冻分类器部分的参数:
解冻 Sagittal T1、Axial T2 和 Sagittal T2/STIR 模型的分类器部分的参数,使其在训练过程中更新。
定义损失函数:
定义交叉熵损失函数 nn.CrossEntropyLoss()。
定义优化器:
定义Adam优化器,用于优化 Sagittal T1、Axial T2 和 Sagittal T2/STIR 模型的分类器部分的参数。
将模型和优化器存储在字典中:
将三个模型实例和对应的优化器存储在字典中,以便后续的训练和验证。
import torchvision.models as models # 导入PyTorch的预训练模型
import torch.optim.lr_scheduler as lr_scheduler # 导入学习率调度器
import torch.nn as nn # 导入神经网络模块
import torch # 导入PyTorch库
# 自定义EfficientNetV2模型类
class CustomEfficientNetV2(nn.Module):
def __init__(self, num_classes=3, pretrained_weights=None):
super(CustomEfficientNetV2, self).__init__()
self.model = models.efficientnet_v2_s(weights=None) # 初始化EfficientNetV2模型
if pretrained_weights:
self.model.load_state_dict(torch.load(pretrained_weights)) # 加载预训练的模型权重
num_ftrs = self.model.classifier[-1].in_features # 获取分类器最后一层的输入特征数
self.model.classifier[-1] = nn.Linear(num_ftrs, num_classes) # 修改分类器最后一层以适应新的分类任务
def forward(self, x):
return self.model(x) # 定义前向传播
def unfreeze_model(self):
# 解冻模型特征提取部分的最后20层(不包括BatchNorm层)
for layer in list(self.model.features.children())[-20:]:
if not isinstance(layer, nn.BatchNorm2d):
for param in layer.parameters():
param.requires_grad = True
# 解冻分类器部分
for param in self.model.classifier.parameters():
param.requires_grad = True
# 设置设备为CUDA(如果可用),否则为CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 预训练权重文件路径
weights_path = '请自定义你的文件路径:efficientnet_v2_s-dd5fe13b.pth'
# 初始化模型
sagittal_t1_model = CustomEfficientNetV2(num_classes=3, pretrained_weights=weights_path).to(device)
axial_t2_model = CustomEfficientNetV2(num_classes=3, pretrained_weights=weights_path).to(device)
sagittal_t2stir_model = CustomEfficientNetV2(num_classes=3, pretrained_weights=weights_path).to(device)
# 冻结模型特征提取部分的参数
for param in sagittal_t1_model.model.features.parameters():
param.requires_grad = False
for param in axial_t2_model.model.features.parameters():
param.requires_grad = False
for param in sagittal_t2stir_model.model.features.parameters():
param.requires_grad = False
# 解冻分类器部分的参数
for param in sagittal_t1_model.model.classifier.parameters():
param.requires_grad = True
for param in axial_t2_model.model.classifier.parameters():
param.requires_grad = True
for param in sagittal_t2stir_model.model.classifier.parameters():
param.requires_grad = True
# 定义损失函数
criterion = nn.CrossEntropyLoss()
# 定义优化器
optimizer_sagittal_t1 = torch.optim.Adam(sagittal_t1_model.model.classifier.parameters(), lr=0.001)
optimizer_axial_t2 = torch.optim.Adam(axial_t2_model.model.classifier.parameters(), lr=0.001)
optimizer_sagittal_t2stir = torch.optim.Adam(sagittal_t2stir_model.model.classifier.parameters(), lr=0.001)
# 将模型和优化器存储在字典中
models = {
'Sagittal T1': sagittal_t1_model,
'Axial T2': axial_t2_model,
'Sagittal T2/STIR': sagittal_t2stir_model,
}
optimizers = {
'Sagittal T1': optimizer_sagittal_t1,
'Axial T2': optimizer_axial_t2,
'Sagittal T2/STIR': optimizer_sagittal_t2stir,
}
运行完了自定义的EfficientNetV2模型类之后,会出现下图的提示,这个提示是来自 torch.load 函数的一个 FutureWarning,它提醒用户在使用 torch.load 加载模型权重时,当前默认的 weights_only=False 可能会导致安全风险。使用默认的 weights_only=False 加载模型权重时,可能会执行任意代码,从而带来潜在的安全隐患。
计算并打印出 sagittal_t1_model 模型中可训练参数的总数
# 计算并打印出 sagittal_t1_model 模型中可训练参数的总数
trainable_params = sum(p.numel() for p in sagittal_t1_model.parameters() if p.requires_grad)
# 打印可训练参数的总数
print(f"Number of parameters: {trainable_params}")
# 映射值
label_map = {'normal_mild': 0, 'moderate': 1, 'severe': 2}
获取一个批次的图像和标签
从 trainloader_t2 数据加载器中获取一个批次的图像和标签,并将标签转换为对应的整数形式,然后将标签移动到指定的设备(如 GPU 或 CPU),并打印出这些标签。最后,通过 break 语句终止循环,只处理一个批次的图像和标签。
# 从 trainloader_t2 数据加载器中获取一个批次的图像和标签
for images, labels in trainloader_t2:
# 将标签转换为对应的整数形式
labels = torch.tensor([label_map[label] for label in labels])
# 将标签移动到指定的设备(如 GPU 或 CPU)
labels = labels.to(device)
# 打印标签
print(labels)
# 终止循环,只处理一个批次的图像和标签
break
训练神经网络模型
定义了一个名为 train_model 的函数,用于训练一个神经网络模型。该函数接受模型、训练数据加载器、验证数据加载器、训练数据集长度、验证数据集长度、优化器、训练轮数(epochs)和早停耐心值(patience)作为输入参数。函数的主要作用是训练模型,并在验证集上评估模型的性能,同时实现早停机制以防止过拟合。
scheduler = lr_scheduler.StepLR(optimizer, step_size=2, gamma=0.1):
定义一个学习率调度器,每2个epoch将学习率乘以0.1。
best_val_acc = 0.0:
初始化最佳验证准确率为0.0。
best_model_wts = deepcopy(model.state_dict()):
初始化最佳模型权重为当前模型的权重。
counter = 0:
初始化早停计数器为0。
for epoch in range(num_epochs)::
开始训练循环,遍历每个epoch。
model.train():
设置模型为训练模式。
with tqdm(trainloader, unit=“batch”) as tepoch:
使用 tqdm 显示训练进度。
for images, labels in tepoch:
遍历训练数据加载器中的每个批次。
images, labels = images.to(device), torch.tensor([label_map[label] for label in labels]).to(device):
将图像和标签移动到指定的设备,并将标签转换为整数形式。
optimizer.zero_grad():
清零梯度。
outputs = model(images):
前向传播,获取模型输出。
loss = criterion(outputs, labels):
计算损失。
loss.backward():
反向传播,计算梯度。
optimizer.step():
更新权重。
train_loss += loss.item():
累加训练损失。
probabilities = torch.softmax(outputs, dim=1):
计算预测概率。
predicted = torch.max(probabilities, 1):
获取预测类别。
correct_train += (predicted == labels).sum().item():
累加正确预测数。
tepoch.set_postfix(epoch=epoch+1):
更新 tqdm 进度条。
scheduler.step():
更新学习率。
train_loss /= len(trainloader):
计算平均训练损失。
train_acc = 100 * correct_train / len_train:
计算训练准确率。
model.eval():
设置模型为评估模式。
with torch.no_grad():
关闭梯度计算,节省内存。
with tqdm(valloader, unit=“batch”) as vepoch:
使用 tqdm 显示验证进度。
for images, labels in vepoch:
遍历验证数据加载器中的每个批次。
images, labels = images.to(device), torch.tensor([label_map[label] for label in labels]).to(device):
将图像和标签移动到指定的设备,并将标签转换为整数形式。
outputs = model(images):
前向传播,获取模型输出。
loss = criterion(outputs, labels):
计算损失。
val_loss += loss.item():
累加验证损失。
probabilities = torch.softmax(outputs, dim=1).squeeze(0):
计算预测概率。
predicted = torch.max(probabilities, 1):
获取预测类别。
correct_val += (predicted == labels).sum().item():
累加正确预测数。
vepoch.set_postfix(epoch=epoch+1):
更新 tqdm 进度条。
val_loss /= len(valloader):
计算平均验证损失。
val_acc = 100 * correct_val / len_val:
计算验证准确率。
print(f"Epoch {epoch+1}, Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%, Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%"):
打印当前epoch的训练和验证结果。
if val_acc > best_val_acc:
如果当前验证准确率优于最佳验证准确率。
best_val_acc = val_acc:
更新最佳验证准确率。
best_model_wts = deepcopy(model.state_dict()):
保存最佳模型权重。
counter = 0:
重置早停计数器。
torch.save(best_model_wts, f’best_model_{epoch+1}.pth’):
保存最佳模型。
else:
如果当前验证准确率不优于最佳验证准确率。
counter += 1:
增加早停计数器。
if counter >= patience::
如果早停计数器达到耐心值。
print(f"Early stopping triggered after {epoch+1} epochs"):
打印早停信息。
break:
终止训练循环。
model.load_state_dict(best_model_wts):
加载最佳模型权重。
return model, best_val_acc:
返回训练好的模型和最佳验证准确率。
def train_model(model, trainloader, valloader, len_train, len_val, optimizer, num_epochs=10, patience=3):
"""
训练模型的函数。
参数:
model: 要训练的模型
trainloader: 训练数据加载器
valloader: 验证数据加载器
len_train: 训练数据集的长度
len_val: 验证数据集的长度
optimizer: 优化器
num_epochs: 训练轮数,默认为10
patience: 早停耐心值,默认为3
返回:
model: 训练好的模型
best_val_acc: 最佳验证准确率
"""
# 定义学习率调度器,每2个epoch学习率乘以0.1
scheduler = lr_scheduler.StepLR(optimizer, step_size=2, gamma=0.1)
# 初始化最佳验证准确率和最佳模型权重
best_val_acc = 0.0
best_model_wts = deepcopy(model.state_dict())
counter = 0 # 早停计数器
# 开始训练
for epoch in range(num_epochs):
model.train() # 设置模型为训练模式
train_loss = 0 # 初始化训练损失
correct_train = 0 # 初始化训练正确预测数
# 使用tqdm显示训练进度
with tqdm(trainloader, unit="batch") as tepoch:
for images, labels in tepoch:
images, labels = images.to(device), torch.tensor([label_map[label] for label in labels]).to(device)
optimizer.zero_grad() # 清零梯度
outputs = model(images) # 前向传播
loss = criterion(outputs, labels) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新权重
train_loss += loss.item() # 累加训练损失
probabilities = torch.softmax(outputs, dim=1) # 计算预测概率
_, predicted = torch.max(probabilities, 1) # 获取预测类别
correct_train += (predicted == labels).sum().item() # 累加正确预测数
tepoch.set_postfix(epoch=epoch+1) # 更新tqdm进度条
scheduler.step() # 更新学习率
train_loss /= len(trainloader) # 计算平均训练损失
train_acc = 100 * correct_train / len_train # 计算训练准确率
model.eval() # 设置模型为评估模式
val_loss, correct_val = 0, 0 # 初始化验证损失和正确预测数
# 使用tqdm显示验证进度
with torch.no_grad():
with tqdm(valloader, unit="batch") as vepoch:
for images, labels in vepoch:
images, labels = images.to(device), torch.tensor([label_map[label] for label in labels]).to(device)
outputs = model(images) # 前向传播
loss = criterion(outputs, labels) # 计算损失
val_loss += loss.item() # 累加验证损失
probabilities = torch.softmax(outputs, dim=1).squeeze(0) # 计算预测概率
_, predicted = torch.max(probabilities, 1) # 获取预测类别
correct_val += (predicted == labels).sum().item() # 累加正确预测数
vepoch.set_postfix(epoch=epoch+1) # 更新tqdm进度条
val_loss /= len(valloader) # 计算平均验证损失
val_acc = 100 * correct_val / len_val # 计算验证准确率
print(f"Epoch {epoch+1}, Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%, Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%")
# 如果当前验证准确率优于最佳验证准确率
if val_acc > best_val_acc:
best_val_acc = val_acc # 更新最佳验证准确率
best_model_wts = deepcopy(model.state_dict()) # 保存最佳模型权重
counter = 0 # 重置早停计数器
torch.save(best_model_wts, f'best_model_{epoch+1}.pth') # 保存最佳模型
else:
counter += 1 # 增加早停计数器
# 如果早停计数器达到耐心值,触发早停
if counter >= patience:
print(f"Early stopping triggered after {epoch+1} epochs")
break
# 加载最佳模型权重
model.load_state_dict(best_model_wts)
return model, best_val_acc # 返回训练好的模型和最佳验证准确率
遍历 models 字典中的每个模型
遍历 models 字典中的每个模型,并为每个模型选择相应的训练数据加载器、验证数据加载器、训练数据集长度和验证数据集长度。然后,调用 train_model 函数来训练每个模型。
# 遍历 models 字典中的每个模型
for desc, model in models.items():
# 根据描述选择相应的训练数据加载器、验证数据加载器、训练数据集长度和验证数据集长度
if desc == 'Sagittal T1':
trainloader, valloader, len_train, len_val = trainloader_t1, valloader_t1, len_train_t1, len_val_t1
elif desc == 'Axial T2':
trainloader, valloader, len_train, len_val = trainloader_t2, valloader_t2, len_train_t2, len_val_t2
elif desc == 'Sagittal T2/STIR':
trainloader, valloader, len_train, len_val = trainloader_t2stir, valloader_t2stir, len_train_t2stir, len_val_t2stir
# 打印正在训练的模型描述
print(f"Training model for {desc}")
# 调用 train_model 函数训练模型
train_model(model, trainloader, valloader, len_train, len_val, optimizers[desc])
遍历 train_data 数据框中的 ‘level’ 列
遍历 train_data 数据框中的 ‘level’ 列,并返回该列中所有唯一的值。然后,它将 expanded_test_desc 数据框打印出来。
# 获取 train_data 数据框中 'level' 列的所有唯一值
unique_levels = train_data['level'].unique()
# 打印 expanded_test_desc 数据框
print(expanded_test_desc)
更新 expanded_test_desc 数据框中的 row_id 列
根据 levels 列表中的值和 row.name 的索引,为每一行生成一个新的 row_id。新的 row_id 由 study_id、condition 和 level 组成。
# 定义 levels 列表,包含不同层次的标签
levels = ['l1_l2', 'l2_l3', 'l3_l4', 'l4_l5', 'l5_s1']
# 定义更新 row_id 的函数
def update_row_id(row, levels):
# 根据 row.name 的索引获取对应的 level
level = levels[row.name % len(levels)]
# 返回新的 row_id,由 study_id、condition 和 level 组成
return f"{row['study_id']}_{row['condition']}_{level}"
# 应用 update_row_id 函数,更新 expanded_test_desc 数据框中的 row_id 列
expanded_test_desc['row_id'] = expanded_test_desc.apply(lambda row: update_row_id(row, levels), axis=1)
TestDataset 的自定义数据集类
定义 TestDataset 的自定义数据集类,并使用该类创建了一个测试数据集 test_dataset。然后,它使用 DataLoader 创建了一个测试数据加载器 testloader。
# 定义 TestDataset 类,继承自 Dataset
class TestDataset(Dataset):
def __init__(self, dataframe, transform=None):
self.dataframe = dataframe # 存储数据框
self.transform = transform # 存储图像变换
def __len__(self):
return len(self.dataframe) # 返回数据集的长度
def __getitem__(self, index):
image_path = self.dataframe['image_path'][index] # 获取图像路径
image = load_dicom(image_path) # 加载并预处理DICOM图像(假设 load_dicom 函数已定义)
if self.transform:
image = self.transform(image) # 应用图像变换
return image # 返回图像
# 定义图像变换
transform = transforms.Compose([
transforms.ToPILImage(), # 将图像数据转换为PIL图像
transforms.Resize((224, 224)), # 调整图像大小为224x224
transforms.Grayscale(num_output_channels=3), # 将图像转换为灰度图像,并输出3个通道
transforms.ToTensor(), # 将图像转换为张量
])
# 创建测试数据集
test_dataset = TestDataset(expanded_test_desc, transform)
# 创建测试数据加载器
testloader = DataLoader(test_dataset, batch_size=1, shuffle=False)
predict_test_data 函数对测试数据进行预测
# 使用 predict_test_data 函数对测试数据进行预测
normal_mild_probs, moderate_probs, severe_probs, test_predictions = predict_test_data(testloader, expanded_test_desc)
# 获取 test_predictions 列表中的第 6 个元素(索引为 5)
prediction = test_predictions[5]
预测的概率值添加到 expanded_test_desc 数据框中
# 将 normal_mild_probs 列表中的概率值添加到 expanded_test_desc 数据框的 'normal_mild' 列中
expanded_test_desc['normal_mild'] = normal_mild_probs
# 将 moderate_probs 列表中的概率值添加到 expanded_test_desc 数据框的 'moderate' 列中
expanded_test_desc['moderate'] = moderate_probs
# 将 severe_probs 列表中的概率值添加到 expanded_test_desc 数据框的 'severe' 列中
expanded_test_desc['severe'] = severe_probs
存储在 submission 数据框中
从 expanded_test_desc 数据框中选择特定的列,并将其存储在一个新的数据框 submission 中。选择了 “row_id”、“normal_mild”、“moderate” 和 “severe” 这四列,并将它们存储在 submission 数据框中。
# 从 expanded_test_desc 数据框中选择 "row_id"、"normal_mild"、"moderate" 和 "severe" 列,并存储在 submission 数据框中
submission = expanded_test_desc[["row_id", "normal_mild", "moderate", "severe"]]
保存预测结果
import os # 导入 os 模块,用于文件操作
# 按 row_id 对 submission 数据框进行分组,并取每组中 normal_mild、moderate 和 severe 列的最大值,然后重置索引
grouped_submission = submission.groupby('row_id').max().reset_index()
# 将 grouped_submission 数据框中的 normal_mild、moderate 和 severe 列的值更新到 sub 数据框中
sub[['normal_mild', 'moderate', 'severe']] = grouped_submission[['normal_mild', 'moderate', 'severe']]
# 将 sub 数据框保存为 CSV 文件,文件名为 "submission.csv",不保存索引列
sub.to_csv("你自定义的文件路径:submission.csv", index=False)
查看预测结果
sub
完整代码–请在jupyter NoteBook上运行
import os # 提供对操作系统进行访问的功能,例如文件和目录操作
import json # 用于解析和生成JSON数据
import time # 提供时间相关的函数,例如获取当前时间
import timm # 提供预训练的PyTorch模型
import torch # PyTorch深度学习框架
import glob # 用于查找符合特定规则的文件路径
import random # 提供随机数生成和随机选择功能
import warnings # 用于控制警告信息的显示
import numpy as np # 提供科学计算功能,特别是多维数组操作
import pandas as pd # 提供数据分析和操作功能
import seaborn as sns # 基于matplotlib的数据可视化库
import matplotlib.pyplot as plt # 提供绘图功能
import collections # 提供容器数据类型
import torch.nn as nn # 提供神经网络模块
import torchvision.models as models # 提供预训练的计算机视觉模型
import pydicom as dicom # 用于处理DICOM格式的医学图像
import matplotlib.patches as patches # 提供绘制图形的功能
from matplotlib import animation, rc # 提供动画和渲染功能
from pydicom.pixel_data_handlers.util import apply_voi_lut # 用于应用DICOM图像的VOI LUT
from sklearn.model_selection import train_test_split # 用于将数据集划分为训练集和测试集
from torch.utils.data import Dataset, DataLoader # 提供数据集和数据加载器
from torchvision import transforms # 提供图像变换功能
from tqdm import tqdm # 提供进度条功能
from copy import deepcopy # 提供深拷贝功能
warnings.filterwarnings("ignore", category=UserWarning, module="albumentations") # 忽略特定模块的警告信息
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 设置设备为CUDA(如果可用),否则为CPU
# 从指定的路径读取多个CSV文件,并将它们加载到Pandas数据框中。
# 这些数据框将用于后续的数据处理和分析。
train_path = 'rsna-2024-lumbar-spine-degenerative-classification/' # 设置训练数据的路径
# 读取训练数据集的CSV文件
train = pd.read_csv(train_path + 'train.csv') # 加载训练数据集
label = pd.read_csv(train_path + 'train_label_coordinates.csv') # 加载训练标签坐标数据集
train_desc = pd.read_csv(train_path + 'train_series_descriptions.csv') # 加载训练系列描述数据集
test_desc = pd.read_csv(train_path + 'test_series_descriptions.csv') # 加载测试系列描述数据集
sub = pd.read_csv(train_path + 'sample_submission.csv') # 加载样本提交数据集
# 生成训练和测试图像的路径列表。
# 通过遍历数据框中的每个研究ID和系列ID,构建图像的完整路径,并将这些路径存储在列表中。
def generate_image_paths(df, data_dir):
"""
生成图像路径列表。
参数:
df (pd.DataFrame): 包含研究ID和系列ID的数据框。
data_dir (str): 数据目录的路径。
返回:
list: 图像路径的列表。
"""
image_paths = [] # 初始化图像路径列表
for study_id, series_id in zip(df['study_id'], df['series_id']): # 遍历每个研究ID和系列ID
study_dir = os.path.join(data_dir, str(study_id)) # 构建研究目录路径
series_dir = os.path.join(study_dir, str(series_id)) # 构建系列目录路径
images = os.listdir(series_dir) # 获取系列目录中的所有图像文件名
image_paths.extend([os.path.join(series_dir, img) for img in images]) # 将每个图像的完整路径添加到列表中
return image_paths # 返回图像路径列表
# 生成训练和测试图像的路径列表
train_image_paths = generate_image_paths(train_desc, f'{train_path}/train_images') # 生成训练图像路径
test_image_paths = generate_image_paths(test_desc, f'{train_path}/test_images') # 生成测试图像路径
# 显示DICOM图像。
# 读取DICOM文件并使用Matplotlib显示图像,最多显示前三个图像。
def display_dicom_images(image_paths):
"""
显示DICOM图像。
参数:
image_paths (list): DICOM图像路径的列表。
"""
plt.figure(figsize=(15, 5)) # 设置图像显示的大小
for i, path in enumerate(image_paths[:3]): # 遍历前三个图像路径
ds = dicom.dcmread(path) # 读取DICOM文件
plt.subplot(1, 3, i + 1) # 设置子图布局
plt.imshow(ds.pixel_array, cmap=plt.cm.bone) # 显示图像,使用灰度颜色映射
plt.title(f"Image {i + 1}") # 设置图像标题
plt.axis('off') # 关闭坐标轴显示
plt.show() # 显示图像
# 显示训练图像路径中的DICOM图像
display_dicom_images(train_image_paths)
import os
import pydicom as dicom
import matplotlib.pyplot as plt
import pandas as pd
# 显示DICOM图像并标注坐标点的函数
def display_dicom_with_coordinates(image_paths, label_df):
# 创建一个子图,子图的数量等于DICOM图像的数量
fig, axs = plt.subplots(1, len(image_paths), figsize=(18, 6))
# 遍历每个DICOM图像路径
for idx, path in enumerate(image_paths):
# 从路径中提取study_id和series_id
study_id = int(path.split('/')[-3])
series_id = int(path.split('/')[-2])
# 根据study_id和series_id过滤标签数据框
filtered_labels = label_df[(label_df['study_id'] == study_id) & (label_df['series_id'] == series_id)]
# 读取DICOM文件
ds = dicom.dcmread(path)
# 显示DICOM图像
axs[idx].imshow(ds.pixel_array, cmap='gray')
axs[idx].set_title(f"Study ID: {study_id}, Series ID: {series_id}")
axs[idx].axis('off')
# 在图像上标注坐标点
for _, row in filtered_labels.iterrows():
axs[idx].plot(row['x'], row['y'], 'ro', markersize=5)
# 调整布局并显示图像
plt.tight_layout()
plt.show()
# 从指定文件夹加载DICOM文件的函数
def load_dicom_files(path_to_folder):
# 获取文件夹中所有以.dcm结尾的文件路径
files = [os.path.join(path_to_folder, f) for f in os.listdir(path_to_folder) if f.endswith('.dcm')]
# 按文件名排序
files.sort(key=lambda x: int(os.path.splitext(os.path.basename(x))[0].split('-')[-1]))
return files
# 指定study_id
study_id = "100206310"
# 构建study文件夹路径
study_folder = f'{train_path}/train_images/{study_id}'
# 初始化图像路径列表
image_paths = []
# 遍历study文件夹中的每个series文件夹
for series_folder in os.listdir(study_folder):
# 构建series文件夹路径
series_folder_path = os.path.join(study_folder, series_folder)
# 加载该series文件夹中的DICOM文件
dicom_files = load_dicom_files(series_folder_path)
# 如果该series文件夹中有DICOM文件,则添加第一个文件到图像路径列表中
if dicom_files:
image_paths.append(dicom_files[0])
# 调用函数显示DICOM图像并标注坐标点
display_dicom_with_coordinates(image_paths, label)
import pandas as pd
# 重塑行的函数
def reshape_row(row):
# 初始化一个字典,用于存储重塑后的数据
data = {'study_id': [], 'condition': [], 'level': [], 'severity': []}
# 遍历行的每一列
for column, value in row.items():
# 跳过不需要处理的列
if column not in ['study_id', 'series_id', 'instance_number', 'x', 'y', 'series_description']:
# 将列名拆分为多个部分
parts = column.split('_')
# 将前几个部分组合成条件,并将每个单词首字母大写
condition = ' '.join([word.capitalize() for word in parts[:-2]])
# 将最后两个部分组合成级别,并将每个单词首字母大写
level = parts[-2].capitalize() + '/' + parts[-1].capitalize()
# 将study_id、条件、级别和严重程度添加到字典中
data['study_id'].append(row['study_id'])
data['condition'].append(condition)
data['level'].append(level)
data['severity'].append(value)
# 将字典转换为DataFrame并返回
return pd.DataFrame(data)
# 遍历原始DataFrame的每一行,调用reshape_row函数进行重塑,并将结果合并为一个新DataFrame
new_train_df = pd.concat([reshape_row(row) for _, row in train.iterrows()], ignore_index=True)
# 将重塑后的DataFrame与标签数据进行合并,基于study_id、condition和level进行内连接
merged_df = pd.merge(new_train_df, label, on=['study_id', 'condition', 'level'], how='inner')
# 将合并后的DataFrame与训练描述数据进行合并,基于series_id进行内连接
final_merged_df = pd.merge(merged_df, train_desc, on='series_id', how='inner')
# 将合并后的DataFrame与训练描述数据进行合并,基于series_id和study_id进行内连接
final_merged_df = pd.merge(merged_df, train_desc, on=['series_id', 'study_id'], how='inner')
# 显示最终合并后的DataFrame的前5行
final_merged_df.head(5)
# 从最终合并后的DataFrame中筛选出study_id为100206310的行,并按照x和y列的值进行升序排序
final_merged_df[final_merged_df['study_id'] == 100206310].sort_values(['x', 'y'], ascending=True)
# 从最终合并后的DataFrame中筛选出series_id为1012284084的行,并按照instance_number列的值进行排序
final_merged_df[final_merged_df['series_id'] == 1012284084].sort_values("instance_number")
# 从最终合并后的DataFrame中筛选出study_id为1013589491的行,并按照instance_number列的值进行排序
filtered_df = final_merged_df[final_merged_df['study_id'] == 1013589491].sort_values("instance_number")
# 为最终合并后的DataFrame添加row_id列
final_merged_df['row_id'] = (
final_merged_df['study_id'].astype(str) + '_' + # 将study_id转换为字符串并添加下划线
final_merged_df['condition'].str.lower().str.replace(' ', '_') + '_' + # 将condition转换为小写,替换空格为下划线,并添加下划线
final_merged_df['level'].str.lower().str.replace('/', '_') # 将level转换为小写,替换斜杠为下划线
)
# 为最终合并后的DataFrame添加image_path列
final_merged_df['image_path'] = (
f'{train_path}/train_images/' + # 添加训练图像路径前缀
final_merged_df['study_id'].astype(str) + '/' + # 将study_id转换为字符串并添加斜杠
final_merged_df['series_id'].astype(str) + '/' + # 将series_id转换为字符串并添加斜杠
final_merged_df['instance_number'].astype(str) + '.dcm' # 将instance_number转换为字符串并添加.dcm后缀
)
# 统计最终合并后的DataFrame中severity为"Normal/Mild"的记录数量
normal_mild_count = final_merged_df[final_merged_df["severity"] == "Normal/Mild"].value_counts().sum()
# 统计最终合并后的DataFrame中severity为"Moderate"的记录数量
moderate_count = final_merged_df[final_merged_df["severity"] == "Moderate"].value_counts().sum()
# 设置基础路径
base_path = 'rsna-2024-lumbar-spine-degenerative-classification/test_images/'
# 获取图像路径的函数
def get_image_paths(row):
# 构建系列路径
series_path = os.path.join(base_path, str(row['study_id']), str(row['series_id']))
# 如果系列路径存在,返回该路径下的所有文件路径
if os.path.exists(series_path):
return [os.path.join(series_path, f) for f in os.listdir(series_path) if
os.path.isfile(os.path.join(series_path, f))]
# 如果系列路径不存在,返回空列表
return []
# 条件映射字典
condition_mapping = {
'Sagittal T1': {'left': 'left_neural_foraminal_narrowing', 'right': 'right_neural_foraminal_narrowing'},
'Axial T2': {'left': 'left_subarticular_stenosis', 'right': 'right_subarticular_stenosis'},
'Sagittal T2/STIR': 'spinal_canal_stenosis'
}
# 初始化扩展行列表
expanded_rows = []
# 遍历测试描述数据框的每一行
for index, row in test_desc.iterrows():
# 获取图像路径
image_paths = get_image_paths(row)
# 获取条件映射
conditions = condition_mapping.get(row['series_description'], {})
# 如果条件是字符串,将其转换为字典
if isinstance(conditions, str):
conditions = {'left': conditions, 'right': conditions}
# 遍历条件和图像路径
for side, condition in conditions.items():
for image_path in image_paths:
# 将扩展行添加到列表中
expanded_rows.append({
'study_id': row['study_id'],
'series_id': row['series_id'],
'series_description': row['series_description'],
'image_path': image_path,
'condition': condition,
'row_id': f"{row['study_id']}_{condition}"
})
# 将扩展行列表转换为DataFrame
expanded_test_desc = pd.DataFrame(expanded_rows)
# 将最终合并后的DataFrame中的severity列的值进行映射
final_merged_df['severity'] = final_merged_df['severity'].map({
'Normal/Mild': 'normal_mild', # 将"Normal/Mild"映射为"normal_mild"
'Moderate': 'moderate', # 将"Moderate"映射为"moderate"
'Severe': 'severe' # 将"Severe"映射为"severe"
})
# 检查路径是否存在的函数
def check_exists(path):
return os.path.exists(path)
# 检查study_id是否存在的函数
def check_study_id(row):
study_id = row['study_id']
path = f'{train_path}/train_images/{study_id}'
return check_exists(path)
# 检查series_id是否存在的函数
def check_series_id(row):
study_id = row['study_id']
series_id = row['series_id']
path = f'{train_path}/train_images/{study_id}/{series_id}'
return check_exists(path)
# 检查image_path是否存在的函数
def check_image_exists(row):
image_path = row['image_path']
return check_exists(path)
# 检查训练数据中的study_id、series_id和image_path是否存在,并将结果存储在新的列中
train_data['study_id_exists'] = train_data.apply(check_study_id, axis=1)
train_data['series_id_exists'] = train_data.apply(check_series_id, axis=1)
train_data['image_exists'] = train_data.apply(check_image_exists, axis=1)
# 筛选出所有study_id、series_id和image_path都存在的行,生成一个新的训练数据集
train_data = train_data[
(train_data['study_id_exists']) & (train_data['series_id_exists']) & (train_data['image_exists'])]
import pydicom as pyd # 导入pydicom库,用于处理DICOM文件
import numpy as np # 导入numpy库,用于数组操作
# 加载DICOM图像并进行预处理的函数
def load_dicom(path):
# 读取DICOM文件
dicom = pyd.read_file(path)
# 提取像素数据
data = dicom.pixel_array
# 将像素数据减去最小值,使最小值为0
data = data - np.min(data)
# 如果像素数据的最大值不为0,将其归一化到0到1之间
if np.max(data) != 0:
data = data / np.max(data)
# 将像素数据乘以255,并转换为8位无符号整数(uint8)
data = (data * 255).astype(np.uint8)
# 返回预处理后的像素数据
return data
# 从训练数据集中随机选择两行,加载对应的DICOM图像并进行预处理,然后显示这两张图像
# 初始化图像列表和row_id列表
images = []
row_ids = []
# 从训练数据集中随机选择两个索引
selected_indices = random.sample(range(len(train_data)), 2)
# 遍历选中的索引
for i in selected_indices:
# 加载DICOM图像并进行预处理
image = load_dicom(train_data['image_path'][i])
# 将预处理后的图像添加到图像列表中
images.append(image)
# 将对应的row_id添加到row_id列表中
row_ids.append(train_data['row_id'][i])
# 创建一个包含两个子图的图形
fig, ax = plt.subplots(1, 2, figsize=(8, 4))
# 遍历两个子图
for i in range(2):
# 显示图像,使用灰度颜色映射
ax[i].imshow(images[i], cmap='gray')
# 设置图像标题为对应的row_id
ax[i].set_title(f'Row ID: {row_ids[i]}', fontsize=8)
# 关闭坐标轴显示
ax[i].axis('off')
# 调整布局并显示图像
plt.tight_layout()
plt.show()
train_data = train_data.dropna() # 删除含有空值的行
import torch # 导入PyTorch库
from torch.utils.data import Dataset, DataLoader # 导入数据集和数据加载器
from torchvision import transforms # 导入图像变换功能
from sklearn.model_selection import train_test_split # 导入数据集划分功能
import numpy as np # 导入numpy库,用于数组操作
import random # 导入随机数生成功能
# 自定义数据集类
class CustomDataset(Dataset):
def __init__(self, dataframe, transform=None):
self.dataframe = dataframe # 存储数据框
self.transform = transform # 存储图像变换
def __len__(self):
return len(self.dataframe) # 返回数据集的长度
def __getitem__(self, index):
image_path = self.dataframe['image_path'][index] # 获取图像路径
image = load_dicom(image_path) # 加载并预处理DICOM图像
label = self.dataframe['severity'][index] # 获取标签
if self.transform:
image = self.transform(image) # 应用图像变换
return image, label # 返回图像和标签
# 创建数据集和数据加载器的函数
def create_datasets_and_loaders(df, series_description, transform, batch_size=8):
filtered_df = df[df['series_description'] == series_description] # 根据series_description筛选数据框
train_df, val_df = train_test_split(filtered_df, test_size=0.2, random_state=42) # 将数据集划分为训练集和验证集
train_df = train_df.reset_index(drop=True) # 重置训练集的索引
val_df = val_df.reset_index(drop=True) # 重置验证集的索引
train_dataset = CustomDataset(train_df, transform) # 创建训练数据集
val_dataset = CustomDataset(val_df, transform) # 创建验证数据集
trainloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # 创建训练数据加载器
valloader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False) # 创建验证数据加载器
return trainloader, valloader, len(train_df), len(val_df) # 返回训练和验证数据加载器以及数据集的长度
# 定义图像变换
transform = transforms.Compose([
transforms.Lambda(lambda x: (x * 255).astype(np.uint8)), # 将图像数据转换为uint8类型
transforms.ToPILImage(), # 将图像数据转换为PIL图像
transforms.Resize((224, 224)), # 调整图像大小为224x224
transforms.Grayscale(num_output_channels=3), # 将图像转换为灰度图像,并输出3个通道
transforms.ToTensor(), # 将图像转换为张量
])
# 初始化数据加载器和数据集长度的字典
dataloaders = {}
lengths = {}
# 创建不同series_description的数据集和数据加载器
trainloader_t1, valloader_t1, len_train_t1, len_val_t1 = create_datasets_and_loaders(train_data, 'Sagittal T1',
transform)
trainloader_t2, valloader_t2, len_train_t2, len_val_t2 = create_datasets_and_loaders(train_data, 'Axial T2', transform)
trainloader_t2stir, valloader_t2stir, len_train_t2stir, len_val_t2stir = create_datasets_and_loaders(train_data,
'Sagittal T2/STIR',
transform)
# 将数据加载器存储在字典中
dataloaders['Sagittal T1'] = (trainloader_t1, valloader_t1)
dataloaders['Axial T2'] = (trainloader_t2, valloader_t2)
dataloaders['Sagittal T2/STIR'] = (trainloader_t2stir, valloader_t2stir)
# 将数据集长度存储在字典中
lengths['Sagittal T1'] = (len_train_t1, len_val_t1)
lengths['Axial T2'] = (len_train_t2, len_val_t2)
lengths['Sagittal T2/STIR'] = (len_train_t2stir, len_val_t2stir)
# 定义标签映射
label_map = {'Mild': 0, 'Moderate': 1, 'Severe': 2}
import matplotlib.pyplot as plt # 导入Matplotlib库,用于绘图
# 可视化数据加载器中的图像批次的函数
def visualize_batch(dataloader):
images, labels = next(iter(dataloader)) # 从数据加载器中获取一个批次的图像和标签
fig, axes = plt.subplots(1, len(images), figsize=(20, 5)) # 创建一个包含子图的图形
for i, (img, lbl) in enumerate(zip(images, labels)): # 遍历图像和标签
ax = axes[i] # 获取当前子图
img = img.permute(1, 2, 0) # 调整图像维度顺序,使其适合显示
ax.imshow(img) # 显示图像
ax.set_title(f"Label: {lbl}") # 设置图像标题为对应的标签
ax.axis('off') # 关闭坐标轴显示
plt.show() # 显示图形
# 可视化Sagittal T1样本
print("Visualizing Sagittal T1 samples")
visualize_batch(trainloader_t1)
# 可视化Axial T2样本
print("Visualizing Axial T2 samples")
visualize_batch(trainloader_t2)
# 可视化Sagittal T2/STIR样本
print("Visualizing Sagittal T2/STIR samples")
visualize_batch(trainloader_t2stir)
import matplotlib.pyplot as plt # 导入Matplotlib库,用于绘图
# 从trainloader_t2数据加载器中获取一个批次的图像和标签
image, label = next(iter(trainloader_t2))
# 选择其中一个图像,并调整其维度顺序,使其适合显示
sample = image[1].permute(1, 2, 0)
# 设置图形尺寸
plt.figure(figsize=(8, 4))
# 显示图像,使用灰度颜色映射
plt.imshow(sample, cmap='gray')
# 设置图像标题为对应的标签
plt.title(label[1])
# 关闭坐标轴显示
plt.axis('off')
# 调整布局
plt.tight_layout()
# 显示图像
plt.show()
import torchvision.models as models # 导入PyTorch的预训练模型
import torch.optim.lr_scheduler as lr_scheduler # 导入学习率调度器
import torch.nn as nn # 导入神经网络模块
import torch # 导入PyTorch库
# 自定义EfficientNetV2模型类
class CustomEfficientNetV2(nn.Module):
def __init__(self, num_classes=3, pretrained_weights=None):
super(CustomEfficientNetV2, self).__init__()
self.model = models.efficientnet_v2_s(weights=None) # 初始化EfficientNetV2模型
if pretrained_weights:
self.model.load_state_dict(torch.load(pretrained_weights)) # 加载预训练的模型权重
num_ftrs = self.model.classifier[-1].in_features # 获取分类器最后一层的输入特征数
self.model.classifier[-1] = nn.Linear(num_ftrs, num_classes) # 修改分类器最后一层以适应新的分类任务
def forward(self, x):
return self.model(x) # 定义前向传播
def unfreeze_model(self):
# 解冻模型特征提取部分的最后20层(不包括BatchNorm层)
for layer in list(self.model.features.children())[-20:]:
if not isinstance(layer, nn.BatchNorm2d):
for param in layer.parameters():
param.requires_grad = True
# 解冻分类器部分
for param in self.model.classifier.parameters():
param.requires_grad = True
# 设置设备为CUDA(如果可用),否则为CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 预训练权重文件路径
weights_path = 'weight/efficientnet_v2_s-dd5fe13b.pth'
# 初始化模型
sagittal_t1_model = CustomEfficientNetV2(num_classes=3, pretrained_weights=weights_path).to(device)
axial_t2_model = CustomEfficientNetV2(num_classes=3, pretrained_weights=weights_path).to(device)
sagittal_t2stir_model = CustomEfficientNetV2(num_classes=3, pretrained_weights=weights_path).to(device)
# 冻结模型特征提取部分的参数
for param in sagittal_t1_model.model.features.parameters():
param.requires_grad = False
for param in axial_t2_model.model.features.parameters():
param.requires_grad = False
for param in sagittal_t2stir_model.model.features.parameters():
param.requires_grad = False
# 解冻分类器部分的参数
for param in sagittal_t1_model.model.classifier.parameters():
param.requires_grad = True
for param in axial_t2_model.model.classifier.parameters():
param.requires_grad = True
for param in sagittal_t2stir_model.model.classifier.parameters():
param.requires_grad = True
# 定义损失函数
criterion = nn.CrossEntropyLoss()
# 定义优化器
optimizer_sagittal_t1 = torch.optim.Adam(sagittal_t1_model.model.classifier.parameters(), lr=0.001)
optimizer_axial_t2 = torch.optim.Adam(axial_t2_model.model.classifier.parameters(), lr=0.001)
optimizer_sagittal_t2stir = torch.optim.Adam(sagittal_t2stir_model.model.classifier.parameters(), lr=0.001)
# 将模型和优化器存储在字典中
models = {
'Sagittal T1': sagittal_t1_model,
'Axial T2': axial_t2_model,
'Sagittal T2/STIR': sagittal_t2stir_model,
}
optimizers = {
'Sagittal T1': optimizer_sagittal_t1,
'Axial T2': optimizer_axial_t2,
'Sagittal T2/STIR': optimizer_sagittal_t2stir,
}
# 计算并打印出 sagittal_t1_model 模型中可训练参数的总数
trainable_params = sum(p.numel() for p in sagittal_t1_model.parameters() if p.requires_grad)
# 打印可训练参数的总数
print(f"Number of parameters: {trainable_params}")
# 从 trainloader_t2 数据加载器中获取一个批次的图像和标签
for images, labels in trainloader_t2:
# 将标签转换为对应的整数形式
labels = torch.tensor([label_map[label] for label in labels])
# 将标签移动到指定的设备(如 GPU 或 CPU)
labels = labels.to(device)
# 打印标签
print(labels)
# 终止循环,只处理一个批次的图像和标签
break
def train_model(model, trainloader, valloader, len_train, len_val, optimizer, num_epochs=10, patience=3):
"""
训练模型的函数。
参数:
model: 要训练的模型
trainloader: 训练数据加载器
valloader: 验证数据加载器
len_train: 训练数据集的长度
len_val: 验证数据集的长度
optimizer: 优化器
num_epochs: 训练轮数,默认为10
patience: 早停耐心值,默认为3
返回:
model: 训练好的模型
best_val_acc: 最佳验证准确率
"""
# 定义学习率调度器,每2个epoch学习率乘以0.1
scheduler = lr_scheduler.StepLR(optimizer, step_size=2, gamma=0.1)
# 初始化最佳验证准确率和最佳模型权重
best_val_acc = 0.0
best_model_wts = deepcopy(model.state_dict())
counter = 0 # 早停计数器
# 开始训练
for epoch in range(num_epochs):
model.train() # 设置模型为训练模式
train_loss = 0 # 初始化训练损失
correct_train = 0 # 初始化训练正确预测数
# 使用tqdm显示训练进度
with tqdm(trainloader, unit="batch") as tepoch:
for images, labels in tepoch:
images, labels = images.to(device), torch.tensor([label_map[label] for label in labels]).to(device)
optimizer.zero_grad() # 清零梯度
outputs = model(images) # 前向传播
loss = criterion(outputs, labels) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新权重
train_loss += loss.item() # 累加训练损失
probabilities = torch.softmax(outputs, dim=1) # 计算预测概率
_, predicted = torch.max(probabilities, 1) # 获取预测类别
correct_train += (predicted == labels).sum().item() # 累加正确预测数
tepoch.set_postfix(epoch=epoch + 1) # 更新tqdm进度条
scheduler.step() # 更新学习率
train_loss /= len(trainloader) # 计算平均训练损失
train_acc = 100 * correct_train / len_train # 计算训练准确率
model.eval() # 设置模型为评估模式
val_loss, correct_val = 0, 0 # 初始化验证损失和正确预测数
# 使用tqdm显示验证进度
with torch.no_grad():
with tqdm(valloader, unit="batch") as vepoch:
for images, labels in vepoch:
images, labels = images.to(device), torch.tensor([label_map[label] for label in labels]).to(device)
outputs = model(images) # 前向传播
loss = criterion(outputs, labels) # 计算损失
val_loss += loss.item() # 累加验证损失
probabilities = torch.softmax(outputs, dim=1).squeeze(0) # 计算预测概率
_, predicted = torch.max(probabilities, 1) # 获取预测类别
correct_val += (predicted == labels).sum().item() # 累加正确预测数
vepoch.set_postfix(epoch=epoch + 1) # 更新tqdm进度条
val_loss /= len(valloader) # 计算平均验证损失
val_acc = 100 * correct_val / len_val # 计算验证准确率
print(
f"Epoch {epoch + 1}, Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%, Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%")
# 如果当前验证准确率优于最佳验证准确率
if val_acc > best_val_acc:
best_val_acc = val_acc # 更新最佳验证准确率
best_model_wts = deepcopy(model.state_dict()) # 保存最佳模型权重
counter = 0 # 重置早停计数器
torch.save(best_model_wts, f'best_model_{epoch + 1}.pth') # 保存最佳模型
else:
counter += 1 # 增加早停计数器
# 如果早停计数器达到耐心值,触发早停
if counter >= patience:
print(f"Early stopping triggered after {epoch + 1} epochs")
break
# 加载最佳模型权重
model.load_state_dict(best_model_wts)
return model, best_val_acc # 返回训练好的模型和最佳验证准确率
# 遍历 models 字典中的每个模型
for desc, model in models.items():
# 根据描述选择相应的训练数据加载器、验证数据加载器、训练数据集长度和验证数据集长度
if desc == 'Sagittal T1':
trainloader, valloader, len_train, len_val = trainloader_t1, valloader_t1, len_train_t1, len_val_t1
elif desc == 'Axial T2':
trainloader, valloader, len_train, len_val = trainloader_t2, valloader_t2, len_train_t2, len_val_t2
elif desc == 'Sagittal T2/STIR':
trainloader, valloader, len_train, len_val = trainloader_t2stir, valloader_t2stir, len_train_t2stir, len_val_t2stir
# 打印正在训练的模型描述
print(f"Training model for {desc}")
# 调用 train_model 函数训练模型
train_model(model, trainloader, valloader, len_train, len_val, optimizers[desc])
# 获取 train_data 数据框中 'level' 列的所有唯一值
unique_levels = train_data['level'].unique()
# 打印 expanded_test_desc 数据框
print(expanded_test_desc)
# 定义 levels 列表,包含不同层次的标签
levels = ['l1_l2', 'l2_l3', 'l3_l4', 'l4_l5', 'l5_s1']
# 定义更新 row_id 的函数
def update_row_id(row, levels):
# 根据 row.name 的索引获取对应的 level
level = levels[row.name % len(levels)]
# 返回新的 row_id,由 study_id、condition 和 level 组成
return f"{row['study_id']}_{row['condition']}_{level}"
# 应用 update_row_id 函数,更新 expanded_test_desc 数据框中的 row_id 列
expanded_test_desc['row_id'] = expanded_test_desc.apply(lambda row: update_row_id(row, levels), axis=1)
# 定义 TestDataset 类,继承自 Dataset
class TestDataset(Dataset):
def __init__(self, dataframe, transform=None):
self.dataframe = dataframe # 存储数据框
self.transform = transform # 存储图像变换
def __len__(self):
return len(self.dataframe) # 返回数据集的长度
def __getitem__(self, index):
image_path = self.dataframe['image_path'][index] # 获取图像路径
image = load_dicom(image_path) # 加载并预处理DICOM图像(假设 load_dicom 函数已定义)
if self.transform:
image = self.transform(image) # 应用图像变换
return image # 返回图像
# 定义图像变换
transform = transforms.Compose([
transforms.ToPILImage(), # 将图像数据转换为PIL图像
transforms.Resize((224, 224)), # 调整图像大小为224x224
transforms.Grayscale(num_output_channels=3), # 将图像转换为灰度图像,并输出3个通道
transforms.ToTensor(), # 将图像转换为张量
])
# 创建测试数据集
test_dataset = TestDataset(expanded_test_desc, transform)
# 创建测试数据加载器
testloader = DataLoader(test_dataset, batch_size=1, shuffle=False)
# 使用 predict_test_data 函数对测试数据进行预测
normal_mild_probs, moderate_probs, severe_probs, test_predictions = predict_test_data(testloader, expanded_test_desc)
# 获取 test_predictions 列表中的第 6 个元素(索引为 5)
prediction = test_predictions[5]
# 将 normal_mild_probs 列表中的概率值添加到 expanded_test_desc 数据框的 'normal_mild' 列中
expanded_test_desc['normal_mild'] = normal_mild_probs
# 将 moderate_probs 列表中的概率值添加到 expanded_test_desc 数据框的 'moderate' 列中
expanded_test_desc['moderate'] = moderate_probs
# 将 severe_probs 列表中的概率值添加到 expanded_test_desc 数据框的 'severe' 列中
expanded_test_desc['severe'] = severe_probs
# 从 expanded_test_desc 数据框中选择 "row_id"、"normal_mild"、"moderate" 和 "severe" 列,并存储在 submission 数据框中
submission = expanded_test_desc[["row_id", "normal_mild", "moderate", "severe"]]
import os # 导入 os 模块,用于文件操作
# 按 row_id 对 submission 数据框进行分组,并取每组中 normal_mild、moderate 和 severe 列的最大值,然后重置索引
grouped_submission = submission.groupby('row_id').max().reset_index()
# 将 grouped_submission 数据框中的 normal_mild、moderate 和 severe 列的值更新到 sub 数据框中
sub[['normal_mild', 'moderate', 'severe']] = grouped_submission[['normal_mild', 'moderate', 'severe']]
# 将 sub 数据框保存为 CSV 文件,文件名为 "submission.csv",不保存索引列
sub.to_csv("submission.csv", index=False)
总结
用于处理和分析医学图像数据,主要包括数据加载、图像预处理、模型训练和预测。
数据加载:
从指定路径加载多个CSV文件,包括训练数据、标签坐标、训练系列描述和测试系列描述等。
生成训练和测试图像的路径列表。
图像显示:
显示DICOM图像,并在图像上标注坐标点。
数据重塑和合并:
将原始数据框中的行重塑为新的格式,并将其与标签数据和训练描述数据进行合并。
数据预处理:
加载DICOM图像并进行预处理,包括归一化和转换为8位无符号整数。
数据集和数据加载器:
创建自定义数据集类和数据加载器,用于加载和预处理图像数据。
模型定义和训练:
定义自定义的EfficientNetV2模型,并使用预训练权重进行初始化。
冻结模型特征提取部分的参数,只训练分类器部分。
定义损失函数和优化器,并进行模型训练。
模型预测:
创建测试数据集和数据加载器,对测试数据进行预测。
将预测结果保存为CSV文件,用于提交。