本文内容旨在说明如何构建一个用于图像分类的神经网络,并对模型进行评估,预测。
这里使用公开数据集与resnet18经行演示。
文章目录
一、数据准备与预处理
1、收集数据集
选择或收集包含所需图像类别的数据集。数据集应包含足够的图像样本,以覆盖各种可能的图像变化(如光照、角度、遮挡等)。
本文采用的是源于kaggle的乳腺超声图像公开数据集
链接:数据集链接
2、图像预处理
图像缩放:将图像缩放到统一的大小,以适应CNN的输入要求。
归一化:将图像的像素值归一化到一定的范围内(如0-1或-1到1),以加快训练速度和提高模型性能。
数据增强(可选):通过旋转、翻转、缩放、裁剪等方式增加数据集的多样性,以提高模型的泛化能力。
这里使用的是将标记图与原始图经行叠加处理
input_dir = 'Dataset_BUSI_with_GT' # 定义输入目录
output_dir = 'working/OverlayedImages' # 定义输出目录,保存叠加图像
labels = ['benign', 'malignant', 'normal']
for label in labels:
os.makedirs(os.path.join(output_dir, label), exist_ok=True) # 创建输出目录
def overlay_and_save(image_path, mask_path): # 定义图像叠加和保存函数
try:
if os.path.exists(image_path) and os.path.exists(mask_path):
image = Image.open(image_path)
mask = Image.open(mask_path)
if image.mode != mask.mode:
mask = mask.convert(image.mode)
if image.size != mask.size:
image = image.resize(mask.size)
overlayed = Image.blend(image, mask, alpha=0.5)
label = os.path.basename(os.path.dirname(image_path))
output_path = os.path.join(output_dir, label, os.path.basename(image_path))
overlayed.save(output_path)
else:
pass
except Exception as e:
print(f"An error occurred for: {image_path} or {mask_path}. Error: {str(e)}")
for label in labels: # 遍历目录并处理图像
label_dir = os.path.join(input_dir, label)
if os.path.isdir(label_dir):
for image_filename in os.listdir(label_dir):
if image_filename.endswith('.png'):
image_path = os.path.join(label_dir, image_filename)
mask_filename = image_filename.replace('.png', '_mask.png')
mask_path = os.path.join(label_dir, mask_filename)
overlay_and_save(image_path, mask_path)
print("Overlayed images have been saved to /kaggle/working/OverlayedImages directory.")
详解
定义输入和输出目录:
input_dir = ‘Dataset_BUSI_with_GT’: 输入目录,包含原始图像和对应的掩码图像。
output_dir = ‘working/OverlayedImages’: 输出目录,用于保存叠加后的图像。
创建输出目录:
labels = [‘benign’, ‘malignant’, ‘normal’]: 三种标签类别,分别表示良性、恶性和正常。
os.makedirs(os.path.join(output_dir, label), exist_ok=True): 为每个标签创建对应的输出目录。如果目录已经存在,则不会报错。
定义图像叠加和保存函数overlay_and_save:
检查文件存在性:
检查图像文件和掩码文件是否存在,确保后续操作能够进行。
读取图像和掩码:
使用Image.open函数打开图像和掩码文件。
模式匹配:
如果图像和掩码的模式(如RGB, L等)不匹配,将掩码转换为与图像相同的模式。
尺寸匹配:
如果图像和掩码的尺寸不一致,将图像调整为掩码的尺寸。
图像叠加:
使用Image.blend函数将图像和掩码按指定的透明度alpha=0.5进行叠加。
保存叠加图像:
根据图像所属的类别(从其父目录获取),将叠加后的图像保存到对应的输出目录。
错误处理:
如果在处理过程中发生任何错误,打印错误信息并继续处理下一个文件。
遍历目录并处理图像:
遍历标签类别:
对每个类别(benign, malignant, normal)的文件夹进行遍历。
找到并处理图像文件:
对于每个文件,如果它是以.png结尾的图像文件,则查找对应的掩码文件(文件名类似,但结尾为_mask.png),然后调用overlay_and_save函数进行处理。
输出信息:
当所有图像处理完成后,打印信息,表示叠加后的图像已保存到指定的输出目录。
叠加后的图像展示
叠加处理有以下优点
突出特征:标记后的图像通常会对目标物体进行标注,如边界框、关键点等。将这些标注信息叠加到原始图像上,可以使得目标特征在视觉上更加突出。这样,在训练过程中,网络模型能够更容易地注意到这些特征,从而加强对这些特征的学习。
引导注意力:叠加后的图像通过明确指示目标物体的位置和范围,引导网络模型在训练时将更多的注意力集中在这些关键区域上。这有助于模型更快地学习到有效的特征表示,提高检测或识别的精度。
3、划分数据集
将数据集划分为训练集、验证集和测试集。通常的比例为70%训练集、15%验证集和15%测试集,但具体比例可根据实际情况调整。
这里采用通常比例
def data_processing(subdirectories=['train', 'validation', 'test']):
# 数据预处理,数据增强正则化,数据分类
class_names = ['malignant', 'normal', 'benign']
minority_classes = ['malignant', 'normal']
minority_class_transforms = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.9),
transforms.RandomRotation(15, expand=False),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
])
data_transforms = {
'train': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.RandomApply([minority_class_transforms], p=0.5),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'validation': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'test': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}
# 训练数据来源地址
# data_dir = 'working/OverlayedImages' # 使用标记过的数据
data_dir = 'Dataset_BUSI_with_GT_nomask' # 使用没有标记过的数据
file_paths = []
labels = []
for label in os.listdir(data_dir):
label_dir = os.path.join(data_dir, label)
if os.path.isdir(label_dir):
for image_file in os.listdir(label_dir):
if image_file.endswith('.png') and not (image_file.endswith('_mask.png') or
image_file.endswith('_mask_1.png') or
image_file.endswith('_mask_2.png')):
image_path = os.path.join(label_dir, image_file)
labels.append(label)
file_paths.append(image_path)
data = pd.DataFrame({'Image_Path': file_paths, 'Label': labels})
# 分割数据集为训练集、验证集和测试集
train_data, test_data = train_test_split(data, test_size=0.15, random_state=42, stratify=data['Label'])
train_data, val_data = train_test_split(train_data, test_size=0.15, random_state=42, stratify=train_data['Label'])
train_dir = 'working/train'
val_dir = 'working/validation'
test_dir = 'working/test'
for label in class_names:
os.makedirs(os.path.join(train_dir, label), exist_ok=True)
os.makedirs(os.path.join(val_dir, label), exist_ok=True)
os.makedirs(os.path.join(test_dir, label), exist_ok=True)
# 复制图像文件到相应目录
for _, row in train_data.iterrows():
image_path = row['Image_Path']
label = row['Label']
shutil.copy(image_path, os.path.join(train_dir, label))
for _, row in val_data.iterrows():
image_path = row['Image_Path']
label = row['Label']
shutil.copy(image_path, os.path.join(val_dir, label))
for _, row in test_data.iterrows():
image_path = row['Image_Path']
label = row['Label']
shutil.copy(image_path, os.path.join(test_dir, label))
# 打印每个类别在每个数据集中的文件数量
for split_dir, split_name in [(train_dir, 'Train'), (val_dir, 'Validation'), (test_dir, 'Test')]:
file_counts = {label: len(os.listdir(os.path.join(split_dir, label))) for label in class_names}
for category, count in file_counts.items():
print(f"{split_name} {category}: {count}")
# 创建数据集对象
image_datasets = {
x: datasets.ImageFolder(root=os.path.join('working', x), transform=data_transforms[x])
for x in subdirectories
}
# 创建数据加载器
dataloaders = {
x: DataLoader(image_datasets[x], batch_size=32, shuffle=True, num_workers=4)
for x in subdirectories
}
dataset_sizes = {x: len(image_datasets[x]) for x in subdirectories}
class_names = image_datasets['train'].classes
return dataloaders, dataset_sizes, class_names
详解:
定义类别和数据增强:
class_names: 列出所有分类标签,包括’malignant’(恶性)、‘normal’(正常)和’benign’(良性)。
minority_classes: 指定需要进行数据增强的小类,这里包括’malignant’和’normal’。
minority_class_transforms: 定义小类的图像增强方法,包括随机水平翻转、随机旋转、颜色抖动等。
定义数据增强和预处理管道:
data_transforms: 为训练、验证和测试集分别定义数据预处理操作。对于训练集,还包括条件应用的少数类增强。
数据加载和过滤:
data_dir: 数据的根目录,这里设置为’Dataset_BUSI_with_GT_nomask’(未标记的数据)。
file_paths和labels: 用于存储所有图像文件的路径及其对应的标签。通过遍历数据目录,将符合条件的图像文件路径和标签加入列表中。
数据集划分:
使用train_test_split函数将数据分割为训练集、验证集和测试集。数据划分是基于标签的分层抽样(stratify),以确保每个子集中的标签分布与原始数据集一致。
创建目录并复制文件:
创建存放训练集、验证集和测试集图像的目录,并根据标签将对应的图像文件复制到相应的子目录中。
打印每个类别的文件数量:
统计并打印每个数据集(训练、验证、测试)中每个类别的图像数量,以便检查数据分布。
创建数据集对象和数据加载器:
使用datasets.ImageFolder创建图像数据集对象,这些对象根据前面定义的data_transforms进行数据预处理。
使用DataLoader为每个数据集创建数据加载器,这些加载器用于在训练过程中批量加载数据。设置batch_size为32,并在训练时打乱数据(shuffle=True)。
返回值:
返回dataloaders, dataset_sizes, class_names三个对象:
dataloaders: 包含训练集、验证集和测试集的加载器。
dataset_sizes: 每个子集的数据量。
class_names: 训练集中的分类标签。
二、构建CNN模型
1、定义模型结构:
使用深度学习框架(如TensorFlow、PyTorch、MATLAB等)中的工具或函数定义CNN模型结构。
CNN模型通常包括多个卷积层、池化层、激活函数(如ReLU)、全连接层和输出层(如Softmax层)。
2、初始化模型参数:
初始化卷积核、偏置等参数。这些参数将在训练过程中通过反向传播算法进行优化。
def train_model(model, criterion, optimizer, dataloaders, dataset_sizes, num_epochs=25,
save_path='work/best_model.pth'):
# 确定设备是否可用GPU,否则使用CPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)
# 复制模型的最佳权重
best_model_wts = copy.deepcopy(model.state_dict())
best_acc = 0.0
# 用于记录训练和验证的损失和准确率的历史
train_loss_history = []
val_loss_history = []
train_acc_history = []
val_acc_history = []
for epoch in range(num_epochs):
print(f'Epoch {epoch}/{num_epochs - 1}')
print('-' * 10)
# 每一轮训练有两个阶段:训练和验证
for phase in ['train', 'validation']:
if phase == 'train':
model.train() # 设置模型为训练模式
else:
model.eval() # 设置模型为评估模式
running_loss = 0.0
running_corrects = 0
# 迭代数据
for inputs, labels in dataloaders[phase]:
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
# 前向传递和计算梯度
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels)
if phase == 'train':
loss.backward()
optimizer.step()
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
epoch_loss = running_loss / dataset_sizes[phase]
epoch_acc = running_corrects.double() / dataset_sizes[phase]
# 记录损失和准确率
if phase == 'train':
train_loss_history.append(epoch_loss)
train_acc_history.append(epoch_acc.item())
else:
val_loss_history.append(epoch_loss)
val_acc_history.append(epoch_acc.item())
print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')
# 深度复制模型
if phase == 'validation' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
print()
print(f'Best val Acc: {best_acc:.4f}')
model.load_state_dict(best_model_wts)
# 可视化训练结果
visualize_training_results(train_loss_history, val_loss_history, train_acc_history, val_acc_history)
# 保存最好的模型权重到工作目录
if not os.path.exists('work'):
os.makedirs('work')
torch.save(model.state_dict(), save_path)
print(f'Model saved to {save_path}')
return model
详解:
设备选择:
检查是否可以使用GPU,如果可以,则使用GPU,否则使用CPU。
初始化:
深拷贝模型的当前权重以保存最佳权重。初始化最佳准确率best_acc为0。还初始化了四个列表,用于记录每一轮训练和验证的损失(loss)和准确率(accuracy)。
训练循环:
1.epoch循环:
外层循环运行num_epochs轮训练。
2.阶段循环:
每个epoch中有两个阶段:训练(‘train’)和验证(‘validation’)。
在训练阶段,模型处于训练模式(model.train()),在验证阶段,模型处于评估模式(model.eval())。
3.数据迭代:
对于每个阶段的每个批次数据,将输入和标签转移到设备(GPU或CPU),然后在训练阶段前向传播并计算梯度(loss.backward()),最后通过优化器更新模型权重(optimizer.step())。
4.损失和准确率计算:
在每个阶段结束时,计算平均损失和准确率,并将结果存储在相应的列表中。如果在验证阶段取得了更好的准确率,则深度复制当前模型权重。
打印和保存结果:
打印每个epoch的损失和准确率,并在所有epoch结束后打印最佳验证准确率。加载最佳模型权重,并通过visualize_training_results函数可视化训练过程中的损失和准确率曲线。
模型保存:
如果目录work不存在,则创建它,并将最佳模型权重保存到指定路径save_path。
返回值:
最终返回训练好的模型。
三、训练模型
1、设置训练选项:
设置学习率、优化器(如SGD、Adam等)、损失函数(如交叉熵损失)等训练选项。
2、前向传播:
将预处理后的图像输入CNN模型,通过逐层卷积、池化、激活等操作,得到最终的输出结果。
3、计算损失函数:
将模型输出结果与真实标签进行比较,计算损失函数值。
4、反向传播:
根据损失函数值计算梯度,并通过反向传播算法更新模型参数。
5、迭代训练:
重复前向传播、计算损失函数和反向传播的过程,直到达到预设的训练轮次或满足其他停止条件(如验证集上的性能不再提升)。
# 数据处理,返回数据加载器、数据集大小和类别名称
dataloaders, dataset_sizes, class_names = file_pretreatment.data_processing()
# 加载预训练的ResNet18模型
model = models.resnet18(weights='IMAGENET1K_V1')
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, len(class_names))
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
# 训练模型并保存
model = train_model(model, criterion, optimizer, dataloaders, dataset_sizes, num_epochs=20,save_path='work/best_model_nomask.pth')
详解
数据处理:
调用file_pretreatment.data_processing()函数对数据进行预处理,并返回数据加载器(dataloaders)、数据集大小(dataset_sizes)和类别名称(class_names)。
dataloaders: 包含训练集、验证集和测试集的数据加载器,用于在训练过程中批量加载数据。
dataset_sizes: 包含训练集、验证集和测试集的样本数量。
class_names: 数据集中的类别名称,这里可能是[‘benign’, ‘malignant’, ‘normal’]。
加载预训练的ResNet18模型
加载一个预训练的ResNet18模型,使用在ImageNet上预训练的权重IMAGENET1K_V1。
num_ftrs = model.fc.in_features:获取模型最后一个全连接层的输入特征数量。
将模型的最后一层(全连接层)替换为一个新的线性层,该层的输出单元数等于类别数量(len(class_names))。这是因为原始的ResNet18模型是为ImageNet任务设计的,输出层具有1000个分类,而这里需要与当前任务的分类数相匹配。
定义损失函数和优化器
使用交叉熵损失函数CrossEntropyLoss(),这是分类任务中常用的损失函数。
定义随机梯度下降(SGD)优化器optim.SGD,设置学习率为0.001,动量为0.9。动量有助于加速梯度下降并减少震荡。
训练模型并保存
调用train_model函数训练模型,传入模型、损失函数、优化器、数据加载器和数据集大小。训练轮数(num_epochs)设置为20。
训练过程中,train_model会在每个epoch后评估模型性能,并保存验证集上表现最好的模型权重到指定路径’work/best_model_nomask.pth’。
最终,训练完成后返回的模型是验证集上表现最好的模型。
四、模型评估与优化
1、模型评估:
使用测试集对训练好的CNN模型进行评估,计算分类准确率、召回率、F1分数等指标。
2、模型优化(可选):
根据评估结果调整模型结构、参数或训练选项,以进一步提高模型性能。
可以使用fine-tuning技术在已有的CNN模型基础上进行微调,以适应新的数据集或任务。
def evaluate_model(model, dataloaders, criterion):
# 确定设备是否可用GPU,否则使用CPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)
model.eval()
running_loss = 0.0
running_corrects = 0
# 迭代测试数据
for inputs, labels in dataloaders['test']:
inputs = inputs.to(device)
labels = labels.to(device)
with torch.no_grad():
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels)
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
total_loss = running_loss / len(dataloaders['test'].dataset)
total_acc = running_corrects.double() / len(dataloaders['test'].dataset)
print(f'Test Loss: {total_loss:.4f} Acc: {total_acc:.4f}')
详解
确定设备并设置模型模式
检查是否有可用的GPU,如果有则使用GPU,否则使用CPU。
将模型移到选定的设备(GPU或CPU)上。
设置模型为评估模式(model.eval()),以确保模型在评估时不会进行参数更新,也不会使用dropout等训练专用的技术。
迭代测试数据
遍历测试数据集,将输入数据和标签移动到选定的设备上。
使用torch.no_grad()来禁用梯度计算,因为在评估阶段不需要反向传播,这样可以节省内存和计算资源。
前向传播:将输入数据传递给模型,得到输出(outputs)。
通过torch.max(outputs, 1)找到每个输出中最大值的索引,即模型预测的类别(preds)。
计算损失:使用传入的损失函数计算当前批次的损失。
累积损失和正确预测数量
累积当前批次的损失值,并乘以输入的样本数量以保持与数据集规模一致。
计算当前批次中正确预测的样本数量,并将其累积到running_corrects中。
计算测试集的总损失和准确率
total_loss: 通过将累积的损失除以测试集的总样本数,得到平均损失。
total_acc: 将累积的正确预测数量除以测试集的总样本数,得到准确率。
五、用训练好的模型经行预测
def predict_and_visualize(model_path='work/best_model_nomask.pth', selected_images_path='work/Selected_Images',
save_path='work/predictions.png'):
# 加载预训练的模型
model = models.resnet18(weights=None)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 3) # 假设有3个类别
model.load_state_dict(torch.load(model_path))
model.eval()
# 定义数据变换
data_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
# 定义类别名称
class_names = ['benign', 'malignant', 'normal']
# 预测结果
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)
predictions = []
correct_predictions = 0
total_images = 0
# 定义正则表达式来去掉圆括号及其内容
def extract_label(filename):
match = re.match(r'([a-zA-Z]+)', filename)
return match.group(0) if match else 'unknown'
# 读取选中的图片进行预测
for image_file in os.listdir(selected_images_path):
if image_file.endswith(('.png', '.jpg', '.jpeg')):
image_path = os.path.join(selected_images_path, image_file)
image = Image.open(image_path)
image = data_transform(image).unsqueeze(0)
image = image.to(device)
with torch.no_grad():
outputs = model(image)
_, preds = torch.max(outputs, 1)
predicted_class = class_names[preds.item()]
# 获取实际类别(去掉圆括号及其内容)
actual_class = extract_label(image_file.split('_')[0])
predictions.append((image_file, actual_class, predicted_class))
# 更新准确率统计
if actual_class == predicted_class:
correct_predictions += 1
total_images += 1
# 计算准确率
accuracy = (correct_predictions / total_images) * 100
# 打印预测报告和准确率
print(f'Prediction Report:')
for img_name, actual, pred in predictions:
print(f'Image: {img_name}, Actual Class: {actual}, Predicted Class: {pred}')
print(f'Accuracy: {accuracy:.2f}%')
# 可视化预测结果
plt.figure(figsize=(20, 20))
for i, (img_name, actual, pred) in enumerate(predictions):
img_path = os.path.join(selected_images_path, img_name)
img = Image.open(img_path)
plt.subplot(5, 5, i + 1)
plt.imshow(img)
plt.title(f'Actual: {actual}\nPredicted: {pred}')
plt.axis('off')
# 保存可视化结果
plt.savefig(save_path)
print(f'Predictions visualized and saved to {save_path}')
详解
加载预训练模型
加载ResNet18模型,不使用预训练的权重(weights=None)。
获取模型最后一层(全连接层)的输入特征数,并将其替换为一个新的线性层,输出维度为3,表示模型有3个分类。
加载指定路径的模型权重model.load_state_dict(torch.load(model_path))。
将模型设置为评估模式(model.eval()),以确保模型在预测时不会进行参数更新。
定义数据变换
定义一系列图像变换步骤,用于在将图像输入模型之前进行预处理:
Resize(256): 将图像缩放至256像素大小。
CenterCrop(224): 将缩放后的图像中心裁剪为224x224像素。
ToTensor(): 将图像转换为张量。
Normalize(…): 使用ImageNet数据集的均值和标准差对图像进行归一化。
设置类别名称
定义类别名称列表,用于在预测结果中显示
预测和统计
设置设备(GPU或CPU),并将模型移动到该设备。
初始化存储预测结果、正确预测数量和总图像数量的变量。
辅助函数
extract_label函数使用正则表达式从文件名中提取实际类别标签,这里假设文件名的格式可以通过这种方式得到实际标签。
迭代图像文件进行预测
遍历选定的图像文件夹,将每个图像文件加载并应用数据变换,然后将其转移到设备上。
在不计算梯度的情况下,使用模型对图像进行预测,获取预测类别索引,并转换为类别名称。
提取实际类别标签,并将文件名、实际类别和预测类别存储到predictions列表中。
比较实际类别与预测类别,如果相同,则更新正确预测数量,并增加总图像数量。
可视化和保存预测结果
创建一个大的图形窗口,用于展示每张图像的实际和预测类别。
遍历预测结果,将图像和标题(包括实际和预测类别)显示在图形中。
将可视化结果保存到指定路径,并打印保存的消息。