目标
在之前的笔记本中,我们已经探索了息肉图像和掩模,并将数据分割为训练集、验证集和测试集。
在这个笔记本中,我们将取出数据集,并生成批量的张量图像数据,通过实时数据增强,然后将它们用于训练我们的分割模型。涵盖这个领域前沿的算法理解和设计实现。
参考论文如下:
目录
Import Modules
# TensorFlow的Keras API,用于图像数据增强和预处理
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# 用于绘图和可视化的Matplotlib和Seaborn
import matplotlib.pyplot as plt
import seaborn as sns
# 用于数值操作的NumPy
import numpy as np
# 用于生成随机数的Random
import random
# 用于机器学习操作的TensorFlow
import tensorflow as tf
# 用于构建U-Net架构的Keras模型和层
from keras.models import Model, load_model
from keras.layers import Input, Conv2D, MaxPooling2D, concatenate, Conv2DTranspose, BatchNormalization, Dropout, Activation, MaxPool2D, Concatenate
# 用于模型的正则化器和优化器
from keras.regularizers import l2
from keras.optimizers import Adam, Nadam
# 用于模型训练的回调
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau
# 用于评估的Scikit-learn指标
from sklearn.metrics import confusion_matrix, classification_report
连接Google Drive
我们将在Google Colab上训练我们的模型,因此我们需要将图像数据上传到Google Drive,并将这个笔记本连接到存储图像的路径。
# 从Google Colab导入必要的模块
from google.colab import drive
# 挂载Google Drive以访问文件
drive.mount('/content/drive')
# 将当前目录更改为Polyp分割文件夹
%cd '/content/drive/MyDrive/Polyp segmentation'
在模型训练过程中,通常会将模型分批次地在训练数据的子集上进行训练,这称为小批量(mini-batches)。ImageDataGenerator
可以通过随机抽样和增强图像来生成这些小批量。它简化了为训练准备数据的过程,通过自动生成增强图像的批次。
为了正确设置生成器,我们的图像数据需要组织成以下结构。请注意,确保图像和对应的掩膜(mask)存储在相应的子文件夹中,并具有相同的名称是很重要的。例如,测试图像和测试掩膜分别存储在具有相同名称“test”的子文件夹中。
new_data
├── test
│ ├── images
│ │ ├── test
│ │ │ ├── 0011.jpg
│ │ │ ├── 0024.jpg
│ │ │ ├── ...
│ ├── masks
│ │ ├── test
│ │ │ ├── 0011.jpg
│ │ │ ├── 0024.jpg
│ │ │ ├── ...
│
├── train
│ ├── images
│ │ ├── train
│ │ │ ├── 0001.jpg
│ │ │ ├── 0002.jpg
│ │ │ ├── ...
│ ├── masks
│ │ ├── train
│ │ │ ├── 0001.jpg
│ │ │ ├── 0002.jpg
│ │ │ ├── ...
│
├── valid
│ ├── images
│ │ ├── valid
│ │ │ ├── 0008.jpg
│ │ │ ├── 0009.jpg
│ │ │ ├── ...
│ ├── masks
│ │ ├── valid
│ │ │ ├── 0008.jpg
│ │ │ ├── 0009.jpg
│ │ │ ├── ...
生成数据批次
def generate_data_batches(data_dir, batch_size, target_size, seed=None, train_augmentation=False):
"""
生成用于图像分割任务的数据批处理。
参数:
data_dir (str): 包含包含训练、验证和测试集子目录的数据集目录。
batch_size (int): 数据生成器使用的批大小。
target_size (tuple): 调整图像和掩模大小的目标尺寸,指定为 (高度, 宽度)。
seed (int or None, optional): 用于可重现性的随机种子。默认为 None。
train_augmentation (bool, optional): 是否对训练集启用数据增强的标志。默认为 False。
返回:
tuple: 包含以下数据生成器的元组:
- train_images_generator (DirectoryIterator): 训练集图像数据生成器。
- train_masks_generator (DirectoryIterator): 训练集掩模数据生成器。
- valid_images_generator (DirectoryIterator): 验证集图像数据生成器。
- valid_masks_generator (DirectoryIterator): 验证集掩模数据生成器。
- test_images_generator (DirectoryIterator): 测试集图像数据生成器。
- test_masks_generator (DirectoryIterator): 测试集掩模数据生成器。
"""
# 所有数据集的重新缩放因子
rescale_factor = 1.0 / 255.0
# 如果提供了种子,则设置随机种子
if seed is not None:
np.random.seed(seed)
# 训练集的数据增强
if train_augmentation:
train_data_generator = ImageDataGenerator(
rescale=rescale_factor,
horizontal_flip=True,
vertical_flip=True,
rotation_range=20,
zoom_range=0.2,
shear_range=0.2,
width_shift_range=0.2,
height_shift_range=0.2,
brightness_range=[0.7, 1.3],
fill_mode='reflect',
)
else:
train_data_generator = ImageDataGenerator(rescale=rescale_factor)
# 验证集和测试集的数据生成器(不进行增强)
valid_data_generator = ImageDataGenerator(rescale=rescale_factor)
test_data_generator = ImageDataGenerator(rescale=rescale_factor)
# 从目录中流式传输数据,并在适用时应用增强
train_images_generator = train_data_generator.flow_from_directory(
data_dir + '/train/images',
target_size=target_size,
batch_size=batch_size,
class_mode=None,
#color_mode = 'grayscale',
shuffle=True, # 为了更好的泛化能力而随机打乱顺序
seed=seed
)
train_masks_generator = train_data_generator.flow_from_directory(
data_dir + '/train/masks',
target_size=target_size,
batch_size=batch_size,
class_mode=None,
color_mode='grayscale',
shuffle=True, # 为了更好的泛化能力而随机打乱顺序
seed=seed
)
valid_images_generator = valid_data_generator.flow_from_directory(
data_dir + '/valid/images',
target_size=target_size,
batch_size=batch_size,
class_mode=None,
#color_mode = 'grayscale',
shuffle=False, # 为了正确评估模型性能而保持其原始顺序
seed=seed
)
valid_masks_generator = valid_data_generator.flow_from_directory(
data_dir + '/valid/masks',
target_size=target_size,
batch_size=batch_size,
class_mode=None,
color_mode='grayscale',
shuffle=False, # 为了正确评估模型性能而保持其原始顺序
seed=seed
)
test_images_generator = test_data_generator.flow_from_directory(
data_dir + '/test/images',
target_size=target_size,
batch_size=100, # 设置测试批大小为完整测试样本大小
class_mode=None,
#color_mode = 'grayscale',
shuffle=False, # 为了正确预测模型性能而保持其原始顺序
seed=seed
)
test_masks_generator = test_data_generator.flow_from_directory(
data_dir + '/test/masks',
target_size=target_size,
batch_size=100, # 设置测试批大小为完整测试样本大小
class_mode=None,
color_mode='grayscale',
shuffle=False, # 为了正确预测模型性能而保持其原始顺序
seed=seed
)
return (
train_images_generator, train_masks_generator,
valid_images_generator, valid_masks_generator,
test_images_generator, test_masks_generator
)
部分核心代码:
# Pack train generator as `(x, y)` and validation genereators as `(x_val, y_val)`
train_generator = zip(train_images_gen, train_masks_gen)
val_generator = zip(validation_images_gen, validation_masks_gen)
检查生成的批次
一旦我们为训练集、验证集和测试集设置了数据生成器,我们就可以查看数据批次的内容。
让我们从训练数据批次开始。
Train batches
如果我们将可用的样本数量除以指定的批次大小,我们将得到步骤数,该步骤数将在训练我们的分割模型时作为参数传递。
# Get the number of samples in the train images
train_samples = train_images_gen.samples
# Get the batch size
batch_size = train_images_gen.batch_size
# Calculate the number of steps per epoch (number of batches)
train_steps = train_samples / batch_size
# Print the number of samples, batch size, and number of batches
print("Train Images")
print("Number of samples:", train_samples)
print("Batch size:", batch_size)
print("Number of batches (train steps):", round(train_steps))
# Get the number of samples in the train masks
train_samples = train_masks_gen.samples
# Get the batch size
batch_size = train_masks_gen.batch_size
# Calculate the number of steps per epoch (number of batches)
train_steps = train_samples / batch_size
# Print the number of samples, batch size, and number of batches
print("Train Masks")
print("Number of samples:", train_samples)
print("Batch size:", batch_size)
print("Number of batches (train steps):", round(train_steps))
可视化一些训练批次的示例。我们将查看前3个批次中的前5张图像。
# 设置要绘制的批次数量和每个批次的图像数量
num_batches = 3 # 绘制前3个批次
images_per_batch = 5 # 每个批次绘制5张图像
# ----------------------------------------------------
# 遍历批次并绘制图像和掩码
for i in range(num_batches):
# 获取当前批次的图像和掩码
batch_images = train_images_gen.next()[:images_per_batch]
batch_masks = train_masks_gen.next()[:images_per_batch]
# 注意:这里使用 next() 方法从生成器中获取数据,但在新版本的Python中通常使用 __next__() 或者在生成器对象上直接调用 next()
for j in range(images_per_batch):
# 绘制图像
axes[i, j].imshow(batch_images[j], cmap='gray') # 使用灰度图显示图像
axes[i, j].axis('off') # 关闭坐标轴
# 绘制掩码
axes[i, j + images_per_batch].imshow(batch_masks[j], cmap='gray') # 使用灰度图显示掩码
axes[i, j + images_per_batch].axis('off') # 关闭坐标轴
# 调整子图之间的间距
plt.subplots_adjust(wspace=0.1, hspace=0.1) # wspace 控制子图之间的宽度间距,hspace 控制高度间距
# 显示绘制的图形
plt.show()
print("Minimum train image pixel value:", batch_images[0].min())
print("Maximum train image pixel value:", batch_images[0].max())
print("Minimum train mask pixel value:", batch_masks[0].min())
print("Maximum train mask pixel value:", batch_masks[0].max())
我们可以看到,在之前的笔记本中,我们图像的像素值最小为0,最大为255。在重新缩放后,我们图像的像素值介于0和1之间
Validation batches
对于验证批次,由于验证集中的样本数量少于训练集,因此与训练批次相比,步骤数量较少。
# Get the number of samples in the validation images
validation_samples = validation_images_gen.samples
# Get the batch size
batch_size = validation_images_gen.batch_size
# Calculate the number of steps per epoch (number of batches)
validation_steps = validation_samples / batch_size
# Print the number of samples, batch size, and number of batches
print("Validation Images")
print("Number of samples:", validation_samples)
print("Batch size:", batch_size)
print("Number of batches (validation steps):", round(validation_steps))
# Get the number of samples in the validation masks
validation_samples = validation_masks_gen.samples
# Get the batch size
batch_size = validation_masks_gen.batch_size
# Calculate the number of steps per epoch (number of batches)
validation_steps = validation_samples / batch_size
# Print the number of samples, batch size, and number of batches
print("Validation Masks")
print("Number of validation samples:", validation_samples)
print("Batch size:", batch_size)
print("Number of validation batches (validation steps):", round(validation_steps))
可视化一些验证批次的示例。我们将查看前3个批次中的前5张图像。
# 设置要绘制的批次数量和每个批次的图像数量
num_batches = 3 # 绘制前3个批次
images_per_batch = 5 # 每个批次绘制5张图像
# 创建图形和坐标轴用于绘制图像和掩码
fig, axes = plt.subplots(num_batches, 2 * images_per_batch, figsize=(15, 7))
# 创建一个 num_batches 行,2 * images_per_batch 列的子图网格,并设置图形大小为 15x7
# 遍历每个批次并绘制图像和掩码
for i in range(num_batches):
# 从验证图像生成器中获取当前批次的图像
batch_images = validation_images_gen.next()[:images_per_batch]
# 从验证掩码生成器中获取当前批次的掩码
batch_masks = validation_masks_gen.next()[:images_per_batch]
# 遍历每个批次的每张图像和对应的掩码
for j in range(images_per_batch):
# 在坐标轴上绘制图像
axes[i, j].imshow(batch_images[j], cmap='gray') # 使用灰度模式显示图像
axes[i, j].axis('off') # 关闭坐标轴
# 在相邻的坐标轴上绘制掩码
axes[i, j + images_per_batch].imshow(batch_masks[j], cmap='gray') # 使用灰度模式显示掩码
axes[i, j + images_per_batch].axis('off') # 关闭坐标轴
# 调整子图之间的间距
plt.subplots_adjust(wspace=0.1, hspace=0.1) # 调整子图之间的宽度和高度间距
# 显示绘制的图形
plt.show()
# 设置网格的行数和列数
num_rows = 3
num_cols = 5
total_images = num_rows * num_cols # 总共要显示的图像数量,即15张图像
# 在网格中绘制图像和掩码
fig, axes = plt.subplots(num_rows, 2 * num_cols, figsize=(15, 7))
# 创建一个num_rows行,2 * num_cols列的子图网格,并设置图形大小为15x7
# 获取测试批次的数据
test_batch_images = test_images_gen.next()
test_batch_masks = test_masks_gen.next()
# 注意:在较新版本的Python中,建议使用next(test_images_gen) 和 next(test_masks_gen)
# 遍历测试批次中的图像,并在网格中绘制它们
for i in range(num_rows):
for j in range(num_cols):
index = i * num_cols + j
# 计算当前图像在测试批次中的索引
# 绘制图像
axes[i, j].imshow(test_batch_images[index], cmap='gray')
axes[i, j].axis('off') # 关闭坐标轴
# 绘制掩码
axes[i, j + num_cols].imshow(test_batch_masks[index], cmap='gray')
axes[i, j + num_cols].axis('off') # 关闭坐标轴
# 调整子图之间的间距
plt.subplots_adjust(wspace=0.1, hspace=0.1)
# 调整子图之间的宽度间距和高度间距
# 显示绘制的图形
plt.show()
和训练集以及验证集批次一样,在重新缩放后,我们图像数据的最小像素值和最大像素值都在0和1之间。
建立和训练(肿瘤,息肉)分割模型:U-Net
什么是 U-Net?
U-Net 结构是一种专为快速而精准地分割生物医学图像而设计的卷积神经网络(CNN)。最初,它是针对生物医学图像分割任务而开发的,例如电子显微镜堆栈中的神经结构。然而,其多功能性和有效性使其在生物医学应用之外的各种图像分割任务中得到了广泛应用。它的一些优点包括:
- 数据效率: U-Net 以非常高的数据效率著称,这意味着即使训练数据有限,它也能产生良好的结果。
- 高分辨率: 该架构旨在产生高分辨率的输出,使其非常适合需要识别图像中细节的任务。
- 多功能性: 虽然最初是为了生物医学图像分割而开发的,但 U-Net 已成功地适应了各种其他类型的图像分割任务。
许多后续的架构要么建立在 U-Net 的基础上,要么受其启发,用于各种分割甚至一些非分割任务。
模型结构
U-Net 结构可以被可视化为一个“U”形,这也是它名字的由来。以下是它结构的详细说明:
-
编码器(下采样路径): 编码器捕获图像中的上下文信息。它由一系列卷积层、批量归一化层、激活函数(通常为 ReLU)和最大池化层组成。沿着 U 形结构向下的每一步都包括两个卷积操作,然后是一个最大池化操作,以减小特征图的维度。这有助于网络学习越来越抽象的特征。
-
解码器(上采样路径): 解码器使用转置卷积实现精确的定位。它由一系列上卷积层(或转置卷积层)、与编码器的特征图进行连接(跳跃连接),然后是常规卷积操作。转置卷积会增加特征图的维度。
-
跳跃连接: 这些是编码器和解码器之间的“桥梁”。它们帮助解码器恢复编码过程中丢失的空间信息,这对于实现精确的分割是至关重要的。
-
瓶颈层: 这是 U-Net 结构中最深的层,连接着编码器和解码器。它通常由卷积和激活函数组成,但没有池化操作。这一层负责最抽象的特征表示。
-
输出层: 最终层是一个 1x1 卷积,后面跟着一个类似 sigmoid(用于二值分割)或 softmax(用于多类分割)的激活函数。
# 中文注释:
def conv_block(input, num_filters):
"""
创建一个由两个卷积层组成的块,每个卷积层后面跟着批量归一化和ReLU激活函数。
Args:
input (Tensor): 输入张量。
num_filters (int): 卷积层的滤波器数量。
Returns:
x (Tensor): 应用两个卷积层、批量归一化和ReLU激活函数后的输出张量。
"""
# 第一个卷积层
x = Conv2D(num_filters, 3,
#kernel_initializer='he_uniform',
kernel_regularizer=l2(1e-5),
padding="same")(input)
x = BatchNormalization()(x) # 不在原始网络中。
x = Activation("relu")(x)
# 第二个卷积层
x = Conv2D(num_filters, 3,
#kernel_initializer='he_uniform',
kernel_regularizer=l2(1e-5),
padding="same")(x)
x = BatchNormalization()(x) # 不在原始网络中。
x = Activation("relu")(x)
# 在激活函数之后添加Dropout
#x = Dropout(0.25)(x)
return x
def encoder_block(input, num_filters):
"""
创建一个编码器块,其中包含一个卷积块,然后是最大池化。
Args:
input (Tensor): 输入张量。
num_filters (int): 卷积层的滤波器数量。
Returns:
x (Tensor): 经过卷积块后的输出张量。
p (Tensor): 经过最大池化后的输出张量。
"""
# 卷积块
# 卷积输出可以用于与解码器中的跳跃连接进行拼接
x = conv_block(input, num_filters)
# 最大池化
p = MaxPool2D((2, 2))(x)
return x, p
def decoder_block(input, skip_features, num_filters):
"""
创建一个解码器块,其中包含一个转置卷积层和一个卷积块。
Args:
input (Tensor): 输入张量。
skip_features (Tensor): 来自编码器的张量,将与输入拼接。
num_filters (int): 卷积层的滤波器数量。
Returns:
x (Tensor): 解码器块后的输出张量。
"""
# 转置卷积层
x = Conv2DTranspose(num_filters, (2, 2),
strides=2,
#kernel_initializer='he_uniform',
kernel_regularizer=l2(1e-5),
padding="same")(input)
# 与跳跃连接特征拼接
x = Concatenate()([x, skip_features])
# 卷积块
x = conv_block(x, num_filters)
return x
# 使用这些块构建Unet
def build_unet(input_shape, n_classes):
"""
构建用于图像分割的U-Net模型。
Args:
input_shape (tuple): 输入图像的形状(高度、宽度、通道)。
n_classes (int): 分割的类别数。
Returns:
model (Model): U-Net模型。
"""
inputs = Input(input_shape)
# 编码器块
s1, p1 = encoder_block(inputs, 64)
s2, p2 = encoder_block(p1, 128)
s3, p3 = encoder_block(p2, 256)
s4, p4 = encoder_block(p3, 512)
# 桥接部分
b1 = conv_block(p4, 1024)
# 解码器块
d1 = decoder_block(b1, s4, 512)
d2 = decoder_block(d1, s3, 256)
d3 = decoder_block(d2, s2, 128)
d4 = decoder_block(d3, s1, 64)
# 输出层:根据类别数选择激活函数
if n_classes == 1: # 二分类
activation = 'sigmoid'
else:
activation = 'softmax'
outputs = Conv2D(n_classes, 1, padding="same", activation=activation)(d4) # 根据类别数更改激活函数
print(f"输出激活函数: {activation}")
model = Model(inputs, outputs, name="U-Net")
return model
在你们的实现中,添加了 Batch Normalization 层以稳定训练。此外,还尝试添加 Dropout 层,并尝试不同的 L2 正则化强度(从 1e-6 到 1e-3)以及不同的初始化器(He 正态分布和 Glorot 均匀分布 - 默认值)。
对于你们的任务表现最佳的组合是没有 Dropout 层,使用默认的初始化器,并且将 L2 正则化因子设为 1e-5。这个调参结果给予了你们最佳的模型性能。
Model configuration
在这一部分中,我们将为训练配置我们的 U-Net 模型。在这之前,我们将设置参数来构建我们的 U-Net 模型。
以下是一些关键参数,您可以考虑设置以构建您的 U-Net 模型:
- 输入形状: 定义图像的输入形状。
- 类别数量: 指定分割任务的类别数量。
- 滤波器数量: 决定在卷积层中使用的滤波器数量。
- 核大小: 选择卷积操作的核大小。
- 激活函数: 选择要使用的激活函数,例如 ReLU。
- 优化器: 选择用于训练的优化器,如 Adam 或 SGD。
- 学习率: 设置优化器的学习率。
- 损失函数: 定义在训练期间优化的损失函数,例如分类交叉熵。
- 评估指标: 选择评估指标,如准确性或交并比(IoU)。
通过适当配置这些参数,您可以根据特定的任务和数据集定制您的 U-Net 模型,然后开始训练。
# Get the shape of the input images
x = train_images_gen.__next__()
IMG_HEIGHT = x.shape[1]
IMG_WIDTH = x.shape[2]
IMG_CHANNELS = x.shape[3]
input_shape = (IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS)
# Number of classes (binary segmentation)
n_classes = 1
我们可以进行快速检查,确认批处理大小、输入形状和输出形状。
为了成功编译模型,我们需要正确输入优化器、损失函数和指标。我们将使用典型的准确度指标,该指标计算预测与标签相等的频率,并尝试不同组合的优化器(Adam 和 Nadam)和损失函数(二元交叉熵损失、Dice 损失和 loU 损失)。
# 用于评估分割性能的Dice系数定义
def dice_coefficient(y_true, y_pred, smooth=1):
intersection = tf.reduce_sum(y_true * y_pred)
union = tf.reduce_sum(y_true) + tf.reduce_sum(y_pred)
dice = (2.0 * intersection + smooth) / (union + smooth)
return dice
# 基于Dice系数定义的Dice损失
def dice_loss(y_true, y_pred):
loss = 1 - dice_coefficient(y_true, y_pred)
return loss
# 定义IoU(Intersection over Union)系数
def iou_coefficient(y_true, y_pred, smooth=1):
intersection = tf.reduce_sum(y_true * y_pred)
union = tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) - intersection
iou = (intersection + smooth) / (union + smooth)
return iou
# 基于IoU系数定义的IoU损失
def iou_loss(y_true, y_pred):
loss = 1 - iou_coefficient(y_true, y_pred)
return loss
# 学习率
lr = 1e-4
# 构建并编译U-Net模型
model = build_unet(input_shape, n_classes=n_classes)
# 使用不同的损失函数和优化器来编译模型
model.compile(optimizer=Adam(lr), loss='binary_crossentropy', metrics=['accuracy'])
#model.compile(optimizer=Adam(lr), loss=dice_loss, metrics=['accuracy'])
#model.compile(optimizer=Adam(lr), loss=iou_loss, metrics=['accuracy'])
#model.compile(optimizer=Nadam(lr), loss='binary_crossentropy', metrics=['accuracy'])
#model.compile(optimizer=Nadam(lr), loss=dice_loss, metrics=['accuracy'])
#model.compile(optimizer=Nadam(lr), loss=iou_loss, metrics=['accuracy'])
# 显示模型的架构
model.summary()
Output activation: sigmoid Model: "U-Net" __________________________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ================================================================================================== input_1 (InputLayer) [(None, 256, 256, 3 0 [] )] conv2d (Conv2D) (None, 256, 256, 64 1792 ['input_1[0][0]'] ) batch_normalization (BatchNorm (None, 256, 256, 64 256 ['conv2d[0][0]'] alization) ) activation (Activation) (None, 256, 256, 64 0 ['batch_normalization[0][0]'] ) conv2d_1 (Conv2D) (None, 256, 256, 64 36928 ['activation[0][0]'] ) batch_normalization_1 (BatchNo (None, 256, 256, 64 256 ['conv2d_1[0][0]'] rmalization) ) activation_1 (Activation) (None, 256, 256, 64 0 ['batch_normalization_1[0][0]'] ) max_pooling2d (MaxPooling2D) (None, 128, 128, 64 0 ['activation_1[0][0]'] ) conv2d_2 (Conv2D) (None, 128, 128, 12 73856 ['max_pooling2d[0][0]'] 8) batch_normalization_2 (BatchNo (None, 128, 128, 12 512 ['conv2d_2[0][0]'] rmalization) 8) activation_2 (Activation) (None, 128, 128, 12 0 ['batch_normalization_2[0][0]'] 8) conv2d_3 (Conv2D) (None, 128, 128, 12 147584 ['activation_2[0][0]'] 8) batch_normalization_3 (BatchNo (None, 128, 128, 12 512 ['conv2d_3[0][0]'] rmalization) 8) activation_3 (Activation) (None, 128, 128, 12 0 ['batch_normalization_3[0][0]'] 8) max_pooling2d_1 (MaxPooling2D) (None, 64, 64, 128) 0 ['activation_3[0][0]'] conv2d_4 (Conv2D) (None, 64, 64, 256) 295168 ['max_pooling2d_1[0][0]'] batch_normalization_4 (BatchNo (None, 64, 64, 256) 1024 ['conv2d_4[0][0]'] rmalization) activation_4 (Activation) (None, 64, 64, 256) 0 ['batch_normalization_4[0][0]'] conv2d_5 (Conv2D) (None, 64, 64, 256) 590080 ['activation_4[0][0]'] batch_normalization_5 (BatchNo (None, 64, 64, 256) 1024 ['conv2d_5[0][0]'] rmalization) activation_5 (Activation) (None, 64, 64, 256) 0 ['batch_normalization_5[0][0]'] max_pooling2d_2 (MaxPooling2D) (None, 32, 32, 256) 0 ['activation_5[0][0]'] conv2d_6 (Conv2D) (None, 32, 32, 512) 1180160 ['max_pooling2d_2[0][0]'] batch_normalization_6 (BatchNo (None, 32, 32, 512) 2048 ['conv2d_6[0][0]'] rmalization) activation_6 (Activation) (None, 32, 32, 512) 0 ['batch_normalization_6[0][0]'] conv2d_7 (Conv2D) (None, 32, 32, 512) 2359808 ['activation_6[0][0]'] batch_normalization_7 (BatchNo (None, 32, 32, 512) 2048 ['conv2d_7[0][0]'] rmalization) activation_7 (Activation) (None, 32, 32, 512) 0 ['batch_normalization_7[0][0]'] max_pooling2d_3 (MaxPooling2D) (None, 16, 16, 512) 0 ['activation_7[0][0]'] conv2d_8 (Conv2D) (None, 16, 16, 1024 4719616 ['max_pooling2d_3[0][0]'] ) batch_normalization_8 (BatchNo (None, 16, 16, 1024 4096 ['conv2d_8[0][0]'] rmalization) ) activation_8 (Activation) (None, 16, 16, 1024 0 ['batch_normalization_8[0][0]'] ) conv2d_9 (Conv2D) (None, 16, 16, 1024 9438208 ['activation_8[0][0]'] ) batch_normalization_9 (BatchNo (None, 16, 16, 1024 4096 ['conv2d_9[0][0]'] rmalization) ) activation_9 (Activation) (None, 16, 16, 1024 0 ['batch_normalization_9[0][0]'] ) conv2d_transpose (Conv2DTransp (None, 32, 32, 512) 2097664 ['activation_9[0][0]'] ose) concatenate (Concatenate) (None, 32, 32, 1024 0 ['conv2d_transpose[0][0]', ) 'activation_7[0][0]'] conv2d_10 (Conv2D) (None, 32, 32, 512) 4719104 ['concatenate[0][0]'] batch_normalization_10 (BatchN (None, 32, 32, 512) 2048 ['conv2d_10[0][0]'] ormalization) activation_10 (Activation) (None, 32, 32, 512) 0 ['batch_normalization_10[0][0]'] conv2d_11 (Conv2D) (None, 32, 32, 512) 2359808 ['activation_10[0][0]'] batch_normalization_11 (BatchN (None, 32, 32, 512) 2048 ['conv2d_11[0][0]'] ormalization) activation_11 (Activation) (None, 32, 32, 512) 0 ['batch_normalization_11[0][0]'] conv2d_transpose_1 (Conv2DTran (None, 64, 64, 256) 524544 ['activation_11[0][0]'] spose) concatenate_1 (Concatenate) (None, 64, 64, 512) 0 ['conv2d_transpose_1[0][0]', 'activation_5[0][0]'] conv2d_12 (Conv2D) (None, 64, 64, 256) 1179904 ['concatenate_1[0][0]'] batch_normalization_12 (BatchN (None, 64, 64, 256) 1024 ['conv2d_12[0][0]'] ormalization) activation_12 (Activation) (None, 64, 64, 256) 0 ['batch_normalization_12[0][0]'] conv2d_13 (Conv2D) (None, 64, 64, 256) 590080 ['activation_12[0][0]'] batch_normalization_13 (BatchN (None, 64, 64, 256) 1024 ['conv2d_13[0][0]'] ormalization) activation_13 (Activation) (None, 64, 64, 256) 0 ['batch_normalization_13[0][0]'] conv2d_transpose_2 (Conv2DTran (None, 128, 128, 12 131200 ['activation_13[0][0]'] spose) 8) concatenate_2 (Concatenate) (None, 128, 128, 25 0 ['conv2d_transpose_2[0][0]', 6) 'activation_3[0][0]'] conv2d_14 (Conv2D) (None, 128, 128, 12 295040 ['concatenate_2[0][0]'] 8) batch_normalization_14 (BatchN (None, 128, 128, 12 512 ['conv2d_14[0][0]'] ormalization) 8) activation_14 (Activation) (None, 128, 128, 12 0 ['batch_normalization_14[0][0]'] 8) conv2d_15 (Conv2D) (None, 128, 128, 12 147584 ['activation_14[0][0]'] 8) batch_normalization_15 (BatchN (None, 128, 128, 12 512 ['conv2d_15[0][0]'] ormalization) 8) activation_15 (Activation) (None, 128, 128, 12 0 ['batch_normalization_15[0][0]'] 8) conv2d_transpose_3 (Conv2DTran (None, 256, 256, 64 32832 ['activation_15[0][0]'] spose) ) concatenate_3 (Concatenate) (None, 256, 256, 12 0 ['conv2d_transpose_3[0][0]', 8) 'activation_1[0][0]'] conv2d_16 (Conv2D) (None, 256, 256, 64 73792 ['concatenate_3[0][0]'] ) batch_normalization_16 (BatchN (None, 256, 256, 64 256 ['conv2d_16[0][0]'] ormalization) ) activation_16 (Activation) (None, 256, 256, 64 0 ['batch_normalization_16[0][0]'] ) conv2d_17 (Conv2D) (None, 256, 256, 64 36928 ['activation_16[0][0]'] ) batch_normalization_17 (BatchN (None, 256, 256, 64 256 ['conv2d_17[0][0]'] ormalization) ) activation_17 (Activation) (None, 256, 256, 64 0 ['batch_normalization_17[0][0]'] ) conv2d_18 (Conv2D) (None, 256, 256, 1) 65 ['activation_17[0][0]'] ================================================================================================== Total params: 31,055,297 Trainable params: 31,043,521 Non-trainable params: 11,776
在尝试不同的损失函数和优化器后,对于我们的任务,表现最佳的组合是使用 Adam 优化器和二元交叉熵损失函数。
Model training
一旦模型成功编译,我们现在准备训练模型。以下是设置训练的一些关键要点:
-
Epochs 和 Steps: 我们将 epoch 数量设置为 120,并基于训练数据计算每个 epoch 的步数。
-
模型检查点: 我们根据验证损失保存最佳模型,这使我们可以稍后恢复训练或进行预测。
-
提前停止: 我们实现了带有 10 个 epoch 容忍度的提前停止,以防止过拟合并减少训练时间。
-
学习率降低: 我们使用
ReduceLROnPlateau
来降低学习率,如果验证损失出现平稳期,这有助于模型收敛到更好的解决方案。 -
训练: 我们使用
fit
方法与我们的训练和验证数据生成器,这遵循了训练 Keras 模型的标准方式。 -
回调函数: 我们包括多个回调函数(
model_checkpoint
、early_stopping
和reduce_lr
)来监视和调整训练过程。
# 设置训练参数
epochs = 120
steps_per_epoch = train_images_gen.samples // train_images_gen.batch_size
validation_steps = validation_images_gen.samples // validation_images_gen.batch_size
# 设置模型检查点、early stopping和学习率调整的回调函数
checkpoint_path = './model/unet.h5'
model_checkpoint = ModelCheckpoint(checkpoint_path, monitor='val_loss', save_best_only=True, verbose=1)
early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=8, min_lr=1e-5)
# 使用数据生成器来拟合模型
history = model.fit(
train_generator,
steps_per_epoch=steps_per_epoch,
epochs=epochs,
validation_data=val_generator,
validation_steps=validation_steps,
callbacks=[model_checkpoint, early_stopping, reduce_lr]
)
当训练完成后,fit
方法会返回一个 History 对象,其中记录了连续的 epoch 的训练损失值和指标值,以及验证损失值和验证指标值。
现在让我们将它们绘制出来,看看模型的表现如何。
# 从`history`对象中获取准确率和损失的历史记录
accuracy = history.history['accuracy']
val_accuracy = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
# 绘制准确率图表
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(range(1, len(accuracy) + 1), accuracy, label='Training Accuracy')
plt.plot(range(1, len(val_accuracy) + 1), val_accuracy, label='Validation Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Training and Validation Accuracy')
plt.legend()
# 绘制损失图表
plt.subplot(1, 2, 2)
plt.plot(range(1, len(loss) + 1), loss, label='Training Loss')
plt.plot(range(1, len(val_loss) + 1), val_loss, label='Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training and Validation Loss')
plt.legend()
# 调整布局并显示图表
plt.tight_layout()
plt.show()
根据训练历史,我们可以得出以下结论:
-
验证损失通常在下降,这表明模型正在学会很好地泛化到未见数据。最低的验证损失是在第 89 个 Epoch 时达到的,为 0.15967。
-
训练准确率在 88-90% 左右波动,这表明模型在训练数据上表现得相当不错。
-
验证准确率一般高于训练准确率,经常超过 94%。这是一个积极的迹象,表明模型没有过拟合,并且对新数据有很好的泛化能力。
总体来说,该模型表现良好。验证指标较强,没有明显的过拟合迹象。然而,在图像分割中,准确率并不总是评估模型性能的最具信息性的指标,因为类别通常是不平衡的。
例如,感兴趣的对象(如息肉)可能只占图像的很小部分。一个将每个像素预测为多数类(背景)的模型可能仍然可以实现很高的准确率,但在预测少数类(感兴趣的对象)上表现不佳。
因此,在接下来的部分,我们将研究其他更适合评估分割性能的指标。
评估已经训练好的 U-Net 模型
除了准确率之外,通常用于评估分割性能的其他指标包括:
- 精确度(Precision)
- 召回率(Recall)
- IoU(交并比,Intersection over Union)
- Dice 系数(Dice Coefficie 要评估已经训练好的 U-Net 模型,您可以使用测试数据集进行预测,并计算各种指标来评估模型的性能。以下是一般步骤:
- 加载已经训练好的 U-Net 模型。
- 使用测试数据集对模型进行预测。
- 计算精确度(Precision)、召回率(Recall)、IoU(交并比,Intersection over Union)、Dice 系数等指标来评估模型性能。
- 根据评估结果分析模型在图像分割任务中的表现。
如果您有测试数据集和相应的标签,您可以按照上述步骤进行评估。如果您需要关于特定代码实现的帮助,请随时告诉我,我将乐意协助您。nt)
所有这些指标的值范围从 0(最差)到 1(最佳),我们将在接下来的部分详细讨论每个指标。现在让我们首先加载我们训练好的模型。
Confusion matrix
上述所有提到的指标都是基于计算混淆矩阵得出的,混淆矩阵表示被正确或错误分类为感兴趣对象或背景的像素数量。
让我们进行混淆矩阵分析,并查看 U-Net 模型在测试集上的表现。
test_image = test_images_gen[0]
test_mask = test_masks_gen[0]
# 为测试图像生成预测
prediction = loaded_model.predict(test_image)
# 将预测结果进行阈值处理以获得二值化的mask
binary_prediction = (prediction > 0.5).astype(np.uint8)
# 将mask转换为1D数组并展平
test_mask_flat = test_mask.ravel()
binary_prediction_flat = binary_prediction.ravel()
# 将二值化的mask转换为二进制标签(0或1)
test_mask_labels = (test_mask_flat > 0).astype(np.uint8)
binary_prediction_labels = (binary_prediction_flat > 0).astype(np.uint8)
# 创建混淆矩阵
conf_mat = confusion_matrix(test_mask_labels, binary_prediction_labels)
print(conf_mat)
# 将混淆矩阵作为热力图绘制
plt.figure(figsize=(12, 8))
sns.set(font_scale=1.2)
sns.heatmap(conf_mat, annot=True, fmt="d", cmap="Blues", cbar=False,
xticklabels=["Background", "Foreground"], yticklabels=["Background", "Foreground"])
plt.xlabel("Predicted Labels")
plt.ylabel("True Labels")
plt.title("Confusion Matrix")
plt.show()
4/4 [==============================] - 5s 97ms/step [[5361958 89861] [ 222039 879742]]
根据混淆矩阵,我们可以观察到:
- 真正例(True Positive,TP): 879,742 个像素被正确识别为感兴趣对象。
- 真负例(True Negative,TN): 5,361,958 个像素被正确识别为背景。
- 假正例(False Positive,FP): 89,861 个像素被错误识别为感兴趣对象。
- 假负例(False Negative,FN): 222,039 个像素被错误识别为背景。
正如前面讨论的,我们可以利用混淆矩阵中的 TP、TN、FP 和 FN 的值来计算精确度、召回率、IoU 和 Dice 系数等评估指标,从而全面评估模型的分割性能。
让我们来看一下下面的分类报告,它总结了我们感兴趣的两个指标:精确度和召回率。
模型在预测前景类别时更加保守,这表现为高精确度但较低的召回率。这意味着模型漏掉了一些应该被分类为前景的像素。
模型在对背景进行分类时表现出色,如各项指标得分较高所。
总的来说,模型表现相当不错,但可以通过旨在提高前景类别召回率的技术来进一步改善。
IoU(Intersection over Union)和Dice系数(F1分数)
F-测量值,也称为F-分数,在医学图像分割中是最常用的性能评估之一。基于F-测量值,医学图像分割中有两个常用的度量标准:
- 交并比(IoU)
- Dice系数
# 初始化变量以存储Dice系数的总交集、并集和求和
total_intersection = 0
total_union = 0
total_dice_numerator = 0
total_dice_denominator = 0
# 获取测试集中样本的总数
num_samples = len(test_masks_gen)
# 定义一个计算Dice系数的函数
def dice_coefficient(y_true, y_pred):
intersection = np.sum(y_true * y_pred)
dice_numerator = 2.0 * intersection
dice_denominator = np.sum(y_true) + np.sum(y_pred)
return dice_numerator, dice_denominator
# 计算测试集中每个批次的交并比(IoU)和Dice系数
for i in range(num_samples):
# 获取测试图像和mask的下一个批次
batch_images = test_images_gen[i]
batch_masks = test_masks_gen[i]
# 为批次生成预测
batch_predictions = loaded_model.predict(batch_images, verbose=0)
# 将预测结果进行阈值处理以获得二值化的mask
binary_predictions = (batch_predictions > 0.5).astype(np.uint8)
# 计算批次的交集和并集
intersection = np.logical_and(batch_masks, binary_predictions)
union = np.logical_or(batch_masks, binary_predictions)
# 计算批次的Dice系数
dice_numerator, dice_denominator = dice_coefficient(batch_masks, binary_predictions)
# 更新总和
total_intersection += np.sum(intersection)
total_union += np.sum(union)
total_dice_numerator += dice_numerator
total_dice_denominator += dice_denominator
# 计算所有批次的平均IoU和Dice系数
mean_iou = total_intersection / total_union
mean_dice = total_dice_numerator / total_dice_denominator
# 打印平均IoU和Dice系数
print(f"Mean IoU: {mean_iou}")
print(f"Mean Dice Coefficient: {mean_dice}")
Mean IoU: 0.7382603164373193 Mean Dice Coefficient: 0.856693324347813
IoU值为0.738是相当不错的,表明预测分割与地面实况有相当大部分重叠。
Dice系数为0.857同样很高,表明预测结果与地面实况之间有很高程度的重叠。接近1的Dice系数表明模型表现优秀。
Mask prediction
我们已经调查了4个评估指标:精确度、召回率、IoU和Dice系数,以了解我们的模型在分割息肉方面的性能如何。现在,我们可以进行定性评估(视觉检查),以全面了解我们模型的表现。
# 为了保证可重现性,设定随机种子
random.seed(123)
# 获取对应的测试图像和mask(仅来自1个批次)
test_image = test_images_gen[0]
test_mask = test_masks_gen[0]
# 随机选择5个样本的索引
random_indices = random.sample(range(len(test_image)), 5)
# 为测试图像生成预测
prediction = loaded_model.predict(test_image)
# 对预测结果进行阈值处理以获得二值化的mask
binary_prediction = (prediction > 0.5).astype(np.uint8)
# 打印随机索引
print(f"随机索引: {random_indices}")
# 初始化子图
plt.figure(figsize=(12, 17))
for i, random_index in enumerate(random_indices):
# 将原始图像、真实mask和预测mask并排绘制
plt.subplot(5, 3, i*3 + 1)
plt.imshow(test_image[random_index]) # test_image是一个批次,我们提取第一张图像
plt.title('原始图像')
plt.axis('off')
plt.subplot(5, 3, i*3 + 2)
plt.imshow(test_mask[random_index], cmap='gray') # test_mask是一个批次,我们提取第一个mask
plt.title('真实Mask')
plt.axis('off')
plt.subplot(5, 3, i*3 + 3)
plt.imshow(binary_prediction[random_index], cmap='viridis') # binary_prediction是一个批次,我们提取第一个预测
plt.title('预测Mask')
plt.axis('off')
plt.tight_layout()
plt.show()
4/4 [==============================] - 0s 140ms/step Random indices: [6, 34, 11, 98, 52]
预测的分割掩模显示出与地面实况具有高度的形态相似性,捕捉到了目标区域的整体形状和轮廓。然而,在边界划定方面存在细微的差异,表明模型的空间精度可能需要进一步优化。与其使用标准的U-Net模型,我们可以尝试其他专门设计用于改善分割任务中边界划定的模型,比如V-Net、DeepLab、Mask R-CNN等。
我们还可以观察到在预测掩模中存在虚假检测或假阳性,这对模型的精确度产生了不利影响。这些不准确性表明模型在识别息肉方面过于乐观。为解决这一问题,我们可以通过增加预测阈值来减少假阳性的数量,但这也可能降低模型对真阳性的敏感性。在医学背景下,这是一个需要谨慎平衡的问题,最好在与临床专家协商后再做决定。
Conclusion
当然,我们可以总结讨论关于模型性能的内容,包括其优势、改进空间和建议。
优势
- 高准确性:模型表现出95%的高整体准确性,这是令人鼓舞的。
- 良好的精确度和召回率:两个指标均超过80%,表明该模型通常可靠地识别和正确分类目标区域。
- 形态相似性:模型很好地捕捉了目标的整体形状和轮廓。
改进空间
- 边界划定:模型在空间精度方面有待提高,特别是在目标区域的边界处。
- 假阳性:存在虚假检测,表明精确度有待提高。
改进方向
- 阈值调整:微调预测阈值以平衡假阳性和假阴性。
- 损失函数:可以使用自定义损失函数,如边界损失或Hausdorff距离损失,训练模型更注重边界。
- 数据增强:使用更多样化的训练数据或在训练过程中应用边界抖动等技术,使模型对边界变化更加稳健。
- 注意力机制:空间注意力机制可以帮助模型集中在边界区域。
U-Net架构的局限性
标准的U-Net架构在许多分割任务中表现有效,但也存在一些局限性:
-
有限的上下文信息:U-Net能很好地捕获局部特征,但在某些分割任务中可能难以处理全局上下文信息,而这对于任务至关重要。
-
固定架构:该架构通常是固定的,不容易适应不同规模或类型的数据。
-
边界勾画:U-Net有时可能生成的分割掩模在边界处不够精确,这在医学成像中是一个重要问题,因为准确的边界对诊断通常至关重要。
-
内存消耗:该架构可能会消耗大量内存,特别是对于3D分割任务。
-
泛化能力:U-Net可能无法很好地泛化到超出分布范围的数据,或者对于与训练数据显著不同的情况。
-
小目标检测:对于U-Net来说,检测小型或细小目标可能具有挑战性,因为该架构可能更多地关注主导类别。
-
类别不平衡:U-Net本身不能很好地处理类别不平衡的情况,在医学成像中往往会出现这种问题,因为感兴趣的对象远小于背景。
-
缺乏注意力机制:标准的U-Net不包括注意力机制,这可能帮助它集中关注图像中更重要的区域。
-
纹理和细节:虽然U-Net擅长捕捉整体形状,但在捕捉纹理和细节方面可能效果不佳。
-
类别间变异性:U-Net在处理高度类别间变异性时可能会遇到困难,即同一类别的对象具有不同的外观。
-
计算效率:虽然U-Net相对简单,但对于所有任务来说可能并不是最具计算效率的模型,特别是在需要实时推理时。
###当前的最先进模型:Meta-Polyp 根据Papers with Code网站的信息,Kvasir-SEG数据集上目前最先进的模型是Meta-Polyp,这是2023年的最新研究成果。该模型提出了融合了MetaFormer和UNet的新模型,据称在许多方面优于标准的UNet架构,尤其是在处理诸如分布不均匀的数据集、缺失边界和小型息肉等具有挑战性的任务时表现更好。作者声称他们的方法在多个数据集上取得了顶尖结果,包括CVC-300、Kvasir和CVC-ColonDB。
彩蛋1:讨论与联系
微信 | |