深度学习记录–02–微调FasterRCNN
【目的】学会对视觉模型的微调;学会构建对应的目标检测的数据集;对静态图片中的物体的检测;
【其他】会用就行,除非要发论文创新,否则原理那些可以不深入理解,有个大概印象即可。
【免责声明】笔者也是初学者,可能有些地方理解不全面🤖😋。
项目结构
| 目标检测项目1/
| ----datasets/
| --------test/
| lol3.png
| test.png
| --------train/
| annotations.json
| ------------images/
| lol10.png
| lol11.png
| lol12.png
| ------------xml/
| lol10.xml
| lol11.xml
| lol12.xml
| ----models/
| Fine-tuning.py
| __ init __.py
| --------outputs/
| predicted_image.png
| predicted_image1.png
| predicted_image2.png
| --------weight/
| model.pth
| model2.pth
| model3.pth
| ----utils/
| drawXMJG.py
| dtcLoader.py
| dtcXml2Json.py
整体流程
①准备数据,并用labelImg进行数据标注。
②得到的labelImg标注数据的XML文件,运行dtcXml2Json文件,生成对应的annotations.json文件。
③然后直接运行models/Fine-tunning.py运行得到对应的结果权重,然后用test/test.png进行预测。
运行结果
数据准备
数据集
数据集的话,随便截几张图就行,Faster-RCNN对图的宽高没有要求。
数据标注
❗ 使用labelImg进行目标检测数据的标注。
得到的数据保存到对应的文件夹下。
工具类的编写
dtc = detection
dtcXml2json
- 这个就是将xml文件转换为json的一个工具方法
import xml.etree.ElementTree as ET
import json
import os
def xml2json(xml_file_path,annotations):
# 确保文件存在
if not os.path.exists(xml_file_path):
print(f'The file {xml_file_path} was not found.')
else:
# 解析XML文件
tree = ET.parse(xml_file_path)
root = tree.getroot()
# 读取annotation信息
filename = root.find('filename').text
# 创建图像字典
image_dict = {
"filename": filename,
"boxes": [],
"labels": []
}
# 读取object信息
for obj in root.findall('object'):
# 获取类别名称
name = obj.find('name').text
# 创建类别ID
class_id = name # 如果需要映射到特定的ID,可以在这里进行映射
# 读取bndbox信息
bndbox = obj.find('bndbox')
xmin = int(bndbox.find('xmin').text)
ymin = int(bndbox.find('ymin').text)
xmax = int(bndbox.find('xmax').text)
ymax = int(bndbox.find('ymax').text)
# 添加边界框和类别ID
image_dict["boxes"].append([xmin, ymin, xmax, ymax])
image_dict["labels"].append(class_id)
return image_dict
def saveJson(annotations):
# 将annotations列表转换为JSON格式的字符串
annotations_json_str = json.dumps(annotations, indent=4)
# 打印JSON字符串
print(annotations_json_str)
# 如果需要保存到文件
with open('../datasets/train/annotations.json', 'w') as f:
json.dump(annotations, f, indent=4)
if __name__ == '__main__':
# XML文件路径
annotations = []
xml_list = list(os.listdir('../datasets/train/xml'))
for xml_file_path in xml_list:
image_dict = xml2json('../datasets/train/xml/'+xml_file_path,annotations=[])
# 返回图像字典,最后一次性压入
annotations.append(image_dict)
saveJson(annotations)
dtcLoader
- 通过对应的json文件创建dataloader
"""
name:01、自学代码 & dtcLoader
time:2024/9/15 12:04
author:yxy
content:
"""
import json
import os
import numpy as np
import torch
from tqdm import tqdm
from torch.utils.data import Dataset, DataLoader
from PIL import Image
# 构建目标检测的数据集
# 构建对应的数据集需要将标注数据进行一一对应起来;
class ObjectDetectionDataset(Dataset):
def __init__(self, root, annotations, transform=None):
# 获取对应路径和标注的数据
self.root = root
self.annotations = annotations
# 定义对应的数据转换格式
self.transform = transform
def __len__(self):
# 获取标注数据的长度
return len(self.annotations)
def __getitem__(self, idx):
# 获取图片的路径,方便后续打开
## 图像标注的话,filename是字典里面的一个key
### 拼接对应的路径
img_path = os.path.join(self.root, self.annotations[idx]['filename'])
image = Image.open(img_path).convert('RGB')
# 获取对应的坐标和labels
boxes = self.annotations[idx]['boxes']
# 对标签进行转换
## 需要一个背景类别
labels1 = ['background']
labels2 = self.annotations[idx]['labels']
labels = labels1 + labels2
from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
integer_encoded = label_encoder.fit_transform(labels)
label_list = integer_encoded
# 将图像、检测框、标签按照一定的规则进行格式转换
if self.transform:
image, boxes, labels = self.transform(image, boxes, label_list)
# target表示数据的标签。
## 说白了,目标检测本质上还是进行对应的线性回归,只不过这种线性回归的输出不再是简单的分类,而是检测框位置坐标,类别和背景的六种输出。
## 如果判断是背景,那么就不再进行对应的检测框坐标预测
target = {'boxes': torch.tensor(boxes, dtype=torch.float32),
'labels': torch.tensor(labels, dtype=torch.int64)} # 字符串型的label需要转换
return image, target
# 定义数据转换
class ToTensor(object):
def __call__(self, image, boxes, labels):
# 将所有的数据进行张量化
image = torch.from_numpy(np.array(image)).permute(2, 0, 1)
return image, boxes, labels
# 加载注释
def load_annotations(json_path):
with open(json_path, 'r') as f:
annotations = json.load(f)
return annotations
if __name__ == '__main__':
# 加载对应的json数据
## 这个json数据,可以通过对应的idx进行遍历
annotations = load_annotations('../datasets/train/annotations.json')
dataset = ObjectDetectionDataset(root='../datasets/train/images/', annotations=annotations, transform=ToTensor())
# num_workers是CPU核心数
dataloader = DataLoader(dataset, batch_size=2, shuffle=True, num_workers=0)
【解释】
构建dataloader前需要构建一个dataset类,这个类里面包含了指定文件夹下面的相关数据;这个类也是构建dataloader的必要参数之一。
【代码理解】
1)①dataset用到了一些进阶的Python知识,也就是类的相关方法。其中init就是python中类的初始化,有点类似于java的构造器一样和vue里面的this钩子函数一样,可以对一些类属性进行赋初值,方便在当前类这个域里面使用对应的变量(思考一下如果没有这个一个类里面无法使用传递进来的参数。所以,笔者认为这里应该是开辟了一个可以在这个类里面访问的一个全局变量空间);②len也就是获取dataset长度的一个方法,用于获取json文件中的元素的长度。当然,这个函数也可以写一些其他的逻辑取决于任务;③getitem是一种让类可以通过索引进行访问的操作,这一步的目的也是方便后续的DataLoader进行遍历。
2)①dataLoader会遍历整个dataset,如果最后一批数据量不满足dataloader定义的batch_size也是不会报错的,只不过不是完整的情况,如果想要丢弃不完整的bach_size可以将参数drop_last设置为True;shuffle随机打乱这些图像,保证数据的随机分布;num_workers是子进程的任务数,通常情况下和CPU有关,num_workers越多处理的进程数越多,数据加载的越快;构建完的dataloader就可以在后续训练中使用。❗❗②其中的标签类必须为n+1类,其中+1为背景类,如果没有+1那么在实现二分类目标检测时会出只检测一个类别的情况,因为没有背景类的话第一个标签就会被当作是背景类。
3)ToTensor的作用是将图像数据转换为张量类型的图像数据,torch提供了可以直接将numpy数组转换为Tensor数组的方法。.permute(2,0,1)是为了交换对应的通道,PIL打开的Image是np类型的为(HWC),所以要通过这种方式调整为torch中(CHW),不然后续可能会有问题。
(思考:permute底层怎么实现的)
【可能存在的问题】
1)dataLoader中的batchsize尽量改成1,不然可能出现图片形状不匹配的错误。如下所示。
RuntimeError❌: stack expects each tensor to be equal size, but got [3, 467, 973] at entry 0 and [3, 699, 476] at entry 1
训练代码
import torch
from torch.utils.data import DataLoader
from torchvision.models.detection import fasterrcnn_resnet50_fpn
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from torch.optim import SGD
from torchvision.transforms import functional as F
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
import os
## 导入外部自写库
import sys
sys.path.append('../utils') # 向当前文件添加对应的文件路径
import dtcLoader
def get_dataloaer():
annotations = dtcLoader.load_annotations('../datasets/train/annotations.json')
dataset = dtcLoader.ObjectDetectionDataset(root='../datasets/train/images/', annotations=annotations,
transform=dtcLoader.ToTensor())
dataloader = dtcLoader.DataLoader(dataset, batch_size=1, shuffle=True, num_workers=0) # num_workers是子任务数,加速数据读取。
return dataloader
def get_model(config):
# 构建resnet_50的网络结构
model = fasterrcnn_resnet50_fpn(weights=True)
in_features = model.roi_heads.box_predictor.cls_score.in_features
num_classes = config["num_classes"] # 2个类别 + 1个背景类
## 获取FastRCNNPredictor的输出
### 这个就有两个输出,一个是类别的输出,一个是bounding_box
### 将模型的预测器改为,自己的预测器。但这里还是用的已有的预测器。
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
return model
# 加载预训练的Faster R-CNN模型
def model_train(model, dataloader, config):
# 定义损失函数
## 定义评判规则
# criterion = torch.nn.CrossEntropyLoss()
# 选择优化器
optimizer = SGD(model.parameters(), lr=config['lr'], momentum=0.9)
# for param in model.backbone.parameters():
# param.requires_grad = False
# # 如果你还想冻结 RPN 或者 RoIHeads 的参数,可以这样做:
# for param in model.rpn.parameters():
# param.requires_grad = False
# for param in model.roi_heads.parameters():
# param.requires_grad = False
# 训练模型
num_epochs = config["epochs"]
# 损失绘图用
losses_per_epoch = []
for epoch in range(num_epochs):
# 存储每一个epoch的损失
epoch_losses = []
# 开启训练模式
for images, targets in dataloader:
# 将图像和目标转换为模型所需的格式
## 如何构建对应的联系
images = images.float() # 转换为浮点数
images = images / 255.0 # 归一化到 [0, 1]
images = list(image for image in images)
# targets有两种情况:1)类别;2)预测框的位置
# Q3:这里还存在着一定的问题
# 压缩对应的维度
if len(targets['boxes'].shape) == 3:
targets['boxes'] = targets['boxes'].squeeze(0)
targets['labels'] = targets['labels'].squeeze(0)
targets = [targets]
# 要分别遍历出检测框和标签
# targets_boxes = [i.get('boxes') for i in targets if i.get('boxes') is not None]
# targets_labels = [i.get('labels') for i in targets if i.get('labels') is not None]
# print(targets_boxes)
# 前向传播
## 这里实质上还是调用的FastRCNNPredictor里面的cls和box的损失。
## 这里直接将images放到模型当中,还需要其他的一些变化吗,比如将图片调整到指定大小。
predictions = model(images,targets)
# 计算4个损失
loss_classifier = predictions['loss_classifier'] # 分类损失
loss_box_reg = predictions['loss_box_reg'] # 检测框坐标损失
loss_objectness = predictions['loss_objectness'] # 物体检测损失
loss_rpn_box_reg = predictions['loss_rpn_box_reg'] # rpn损失
# 计算总损失(微调情况)
losses = loss_classifier + loss_box_reg + loss_objectness + loss_rpn_box_reg
# 计算损失(一般情况)
# criterion(predictions,targets) # criterion这是一般情况下的损失计算,faster-RCNN封装好了,在输入的时候就把targets和images传入模型当中。
# 反向传播
optimizer.zero_grad()
losses.backward()
optimizer.step()
# 打印训练信息
print(f"Epoch: {epoch}, Loss: {losses.item()}")
# 损失计算
losses = loss_classifier + loss_box_reg + loss_objectness + loss_rpn_box_reg
epoch_losses.append(losses.item()) # 记录当前epoch的损失值
# 每一个epoch带来的情况
average_loss = sum(epoch_losses) / len(epoch_losses)
losses_per_epoch.append(average_loss)
print(f"Epoch: {epoch}, Average Loss: {average_loss}")
# 每个epoch结束后在验证集上评估模型
# model.eval()
# validate_model(model, validation_dataloader)
plot_loss(losses_per_epoch)
# 保存模型
torch.save(model.state_dict(), f'weight/modele{num_epochs}.pth')
def plot_loss(losses_per_epoch):
plt.figure()
plt.plot(losses_per_epoch, label='Training Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Loss vs. Epochs')
plt.legend()
plt.show()
plt.close()
def digital_prediction(model):
# 加载模型
model.load_state_dict(torch.load('./weight/model3.pth'))
# 获取图像的image_tensor
origin_image, image_tensor = get_pre_img()
with torch.no_grad():
model.eval()
predictions = model(image_tensor)
# predictions 包含了预测的边界框、类别标签和置信度分数
# 打印预测的边界框
predicted_boxes = predictions[0]['boxes']
print(predicted_boxes)
# 打印预测的类别标签
predicted_labels = predictions[0]['labels']
print(predicted_labels)
# 打印预测的置信度分数
predicted_scores = predictions[0]['scores']
print(predicted_scores)
# 可选:将预测的边界框转换为图像中的绝对坐标
original_image_size = origin_image.size
for box in predicted_boxes:
# 将边界框坐标转换为整数并转换为绝对坐标
box = box.to('cpu').numpy().astype(int)
box[0], box[1], box[2], box[3] = box[0] * original_image_size[0], box[1] * original_image_size[1], box[2] * \
original_image_size[0], box[3] * original_image_size[1]
print(box)
def img_prediction(model, weights_path, img_path, is_save=False):
# 加载模型
model.load_state_dict(torch.load(weights_path))
# 获取图像的image_tensor
origin_image, image_tensor = get_pre_img()
with torch.no_grad():
model.eval()
predictions = model(image_tensor)
# print(predictions)
# predictions 包含了预测的边界框、类别标签和置信度分数
predicted_boxes = predictions[0]['boxes'].to('cpu').numpy()
predicted_labels = predictions[0]['labels'].to('cpu').numpy()
predicted_scores = predictions[0]['scores'].to('cpu').numpy()
# 加载图像为matplotlib的格式
image_np = np.array(origin_image)
# 绘制图像和边界框
plt.figure()
plt.imshow(image_np)
# 反标签化
label_name_list = ["background","vn","ejt"]
for box, score, label in zip(predicted_boxes, predicted_scores, predicted_labels):
label_name = label_name_list[label]
if score > 0.3: # 只绘制置信度大于0.3的边界框
box = box.astype(int)
plt.gca().add_patch(
plt.Rectangle((box[0], box[1]), box[2] - box[0], box[3] - box[1], linewidth=1, edgecolor='r',
facecolor='none'))
plt.gca().text(box[0], box[1] - 10, f'{label_name} {score:.2f}', color='white', backgroundcolor='darkred',
fontsize=12)
# 保存图像到指定文件夹
output_folder = './outputs'
if not os.path.exists(output_folder):
os.makedirs(output_folder)
output_path = os.path.join(output_folder,img_path)
# 判断是否保存
if is_save:
plt.savefig(output_path)
print(f'Image saved at {output_path}')
# 保存图片要先于plt.show()
plt.show()
# 清理资源
plt.close()
def get_pre_img():
image_path = '../datasets/test/test.png'
image = Image.open(image_path)
if image.mode == 'RGBA':
image = image.convert('RGB')
image_tensor = F.to_tensor(image).unsqueeze(0) # 增加批次维度
image_tensor = image_tensor.to('cuda' if torch.cuda.is_available() else 'cpu')
return image,image_tensor
def get_config():
config = {
"num_classes":3,
"epochs":20,
"lr":0.005,
}
return config
# 定义数据集和数据加载器
# 假设你已经有了一个适合目标检测的自定义数据集
if __name__ == '__main__':
# 获取超参数
config = get_config()
# 训练
# 获取dataloader
data_loader = get_dataloaer()
model = get_model(config)
model_train(model,data_loader,config)
# 预测
# digital_prediction(get_model())
img_prediction(get_model(config),weights_path='./weight/modele20.pth', img_path='predicted_image6.png', is_save=True)
【微调的理解】
①所谓的模型微调,就是说利用某些深度学习官网中下载好的权重进行自己任务的定义。微调的核心在于,利用其他人利用大量的数据集训练出来的具有极强泛化性的模型权重。这个权重可以快速的应用在自己的任务上,进行更准确特征的提取。
②微调可以利用很少的数据集做出较好的效果,自己不必再进行从头训练。只需要调整对应输出层的逻辑即可,网络权重的下载本质上就是下载特征提取网络的权重,通过特征提取网络提取出较好的特征,然后放到全连接层做输出。微调也就是调整最后做预测的网络的权重,即调整到适合自己的输出。
③微调的时候可以冻结特征提取网络的权重,保证特征提取网络权重的不变。训练的过程也只是训练全连接层。
【代码的理解】
1)get_model函数:获取对应的网络模型:
①fasterrcnn_resnet50_fpn就是微调好的一个模型,weights参数为True表示使用预训练参数
②model.roi.box_predictor.cls_score.in_features获取特征网络的输出数,只有确定了这个才能进行后续网络修改时输入数。
③num_classes一定是要分类的类别上 + 1个背景类。
④model.roi_heads.box_predictor表示改模型的边界框预测器(也就是输出模块),使用FasterRCNNPredictor,即Faster-RCNN的输出器(预测的置信度 + 预测的类别 + 预测的检测框位置)。
2)model_train:模型训练相关的
①注意图像和标签的对齐:即一个图像要对应可能存在的多个类别和多个检测框。
②一些维度上的调整,维度的调整是为了将盒子坐标和标签降低一维,方便后续合并成一个维度。
③该模型是封装好的,对于输入和输出是同时输入到模型当中,最后直接返回对应的损失。
④最后选择合适的优化器和损失函数进行训练。
3)其他函数:其他的函数就是一些绘图相关的和预测相关的函数。
总结
如果遇到错误了就上ChatGPT搜索错误或上B站看看系统教程。尝试自己解释和理解,有一些自己的思考。