Faster_RCNN 血液细胞检测项目实战(二)

上一节项目从数据集认识到数据加载器的制作。其实对于任何一个已确定网络框架的深度学习任务,后面的训练并不是最难的,最难的还是如何将数据加载进网络,这就需要将数据制作成适合网络的形状,这一点很难,因为我们无法预料到我们的原始数据集是怎样的,对于越复杂的任务,其数据集越难处理。不过对于血细胞检测的分类回归任务包含了对标签,标签框的处理,由于图像这块数据集已经提前进行过数据增强所以就不需要我们操作。

有了以上的总结,从本节开始,逐渐走进网络,但是在此之前还有一个小内容,对于本项目。

即如何绘制检测框。

1.绘制检测框

这块使用的是opencv库,库的安装就不在我们讲的内容之内,但是要提一嘴,在安装opencv之前,如果你的环境里有numpy,那么就要主要版本是否对应,否则简单点就是删了numpy库,在安装opencv库时就会直接安装numpy。

好了,废话不多说,看主题,先上代码。

#绘制bbox
#重新获取图片信息
img,label = next(iter(dl_train))
#iter将数据加载器转换为一个迭代器,允许诸葛批次遍历数据
#next 用于获取迭代器的下一个元素,即下一个批次的数据。返回的是两个数据的元组,第一个元素是图像,第二个元素是标签数据

#做反向字典
names= {'0':'WBC','1':'RBC','2':'Platelets'}
#从tenser转成numpy
src_img = img[0].permute(1,2,0).numpy()
#因为数据是压缩在0-1,所以成255,转成uint8格式
src_img = (src_img*255).astype(np.uint8)
src_img = src_img.copy()  #做拷贝

#查找img[0]对应box
boxes = label[0]['boxes'].numpy()
labels = label[0]['labels'].numpy()

#循环从boxes与labels中提取标签名和框的位置
#print(boxes.shape[0])

for idx in range(boxes.shape[0]):
    x1,y1,x2,y2  =int(boxes[idx][0]),int(boxes[idx][1]),int(boxes[idx][2]),int(boxes[idx][3])
    name = names[str(labels[idx])]
    #绘制框,参数为img图,左上角坐标,右下角坐标,框的颜色(绿色),框的厚度2
    cv2.rectangle(src_img,(x1,y1),(x2,y2),(0,255,0),2)
    #绘制标签,img,text内容,位置,字体,字体大小,厚度,线性,颜色
    cv2.putText(src_img,text=name,org=(x1,y1+10),fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                fontScale=0.5,thickness=1,lineType=cv2.LINE_AA,color=(0,0,255))
plt.imshow(src_img)
plt.show()

上一节我们制作了数据加载器,我们可以尝试验证下,将数据从数据集中读出来。

img,label = next(iter(dl_train))

代码img, label = next(iter(dl_train))通常用于从一个数据加载器(data loader)中获取下一批次的数据。

  • dl_train 是一个数据加载器,通常用于批量加载训练数据。
  • iter(dl_train) 创建了一个数据加载器的迭代器,你可以使用它来逐个获取数据批次。
  • next(iter(dl_train)) 从迭代器中获取下一个元素,即下一批次的数据。

next函数与iter函数通常结合使用。

首先看看我们train_data的数据结构:

将其加载进DateLoader后得到dl_train,这样通过 next(iter(dl_train))读取数据。

由于我们在上CellDetection是这样的:

所以最后返回时是图像img的tensor,以及包含boxes,labels的target。

这里我们在使用dataLoader是batch_size为2,即每一批次,为网络输送2个图像的数据包。

好了,越说越远了,开始正题。

我们从dl中读出来的数据还需要解析来显示。

#从tenser转成numpy
src_img = img[0].permute(1,2,0).numpy()
#因为数据是压缩在0-1,所以成255,转成uint8格式
src_img = (src_img*255).astype(np.uint8)
src_img = src_img.copy()  #做拷贝

首先是图像,图像的数据类型tensor。像素数据类型float32,而压缩进网络时,图像数据被压缩在0~1之间,为了显示,还需要*255转为uint8,

#查找img[0]对应box
boxes = label[0]['boxes'].numpy()
labels = label[0]['labels'].numpy()

同样对于boxes,labels,数据也要从tensor转为numpy。

#做反向字典
names= {'0':'WBC','1':'RBC','2':'Platelets'}

最后由于细胞种类这块我们编码成了0,1,2,所以为了解码显示。还需要做一个反向字典。

这样就将在网络中运行的数据都转成了在外面可以解读的数据形式了。

最后进一步解读,拿到标签名,以及bbox的位置信息,利用opencv函数进行绘制。

for idx in range(boxes.shape[0]):
    x1,y1,x2,y2  =int(boxes[idx][0]),int(boxes[idx][1]),int(boxes[idx][2]),int(boxes[idx][3])
    name = names[str(labels[idx])]
    #绘制框,参数为img图,左上角坐标,右下角坐标,框的颜色(绿色),框的厚度2
    cv2.rectangle(src_img,(x1,y1),(x2,y2),(0,255,0),2)
    #绘制标签,img,text内容,位置,字体,字体大小,厚度,线性,颜色
    cv2.putText(src_img,text=name,org=(x1,y1+10),fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                fontScale=0.5,thickness=1,lineType=cv2.LINE_AA,color=(0,0,255))
plt.imshow(src_img)
plt.show()

以上就是显示结果。

2.Faster_RCNN网络简介

其实在制作好数据加载器dataloder之后就可以进网络训练了,但在此之前还要对Faster-RCNN网络有一个大概的认识。其实现在已经有很多关于Faster-RCNN网络的解读,这里只大概讲一下结构,感兴趣的可以看原论文或者看其他博主的博客。

Faster R-CNN(Region-based Convolutional Neural Network)是一种用于目标检测的深度学习模型。它是一种改进的RCNN系列模型,用于在图像中检测和定位物体。Faster R-CNN引入了RPN(Region Proposal Network),这是一个用于生成候选物体边界框的网络,然后使用这些边界框进行目标检测。这种方法相对于以前的RCNN模型更快速和准确。

而RCNN即为区域卷积神经网络主要包括:

RPN,区域建议网络,RPN用于提取可能包含目标的候选区域。它生成一组边界框建议,这些建议通常是不同尺寸和长宽比的边界框。

CNN,卷积特征提取,RCNN使用卷积神经网络(CNN)来提取图像特征。这些特征用于后续的目标分类和边界框回归。

同样的。Faster R-CNN的架构包括卷积神经网络(CNN)用于特征提取和RPN网络用于生成候选区域。它已经在计算机视觉领域取得了重大突破,广泛用于物体检测和图像分割任务。

从图可以看到,faster-RCNN主要架构,卷积层,RPN,ROIPooling,以及最后两个,目标分类和边界框回归。

卷积层主要用来提取特征,conv layers。即特征提取网络,用于提取特征。通过一组conv+relu+pooling层来提取图像的feature maps,用于后续的RPN层和取proposal。

RPN生成候选框,筛选候选框,修正候选框。即区域候选网络,这里任务有两部分,一个是分类:判断所有预设anchor是属于positive还是negative(即anchor内是否有目标,二分类);还有一个bounding box regression:修正anchors得到较为准确的proposals。因此,RPN网络相当于提前做了一部分检测,即判断是否有目标(具体什么类别这里不判),以及修正anchor使框的更准一些。

而RoI Pooling层则用于收集RPN生成的proposals(每个框的坐标),并从(1)中的feature maps中提取出来(从对应位置扣出来),生成proposals feature maps送入后续全连接层继续做分类(具体是哪一类别)和回归。

后面两个,一个是目标分类和边界框回归,利用proposals feature maps计算出具体类别,同时再做一次bounding box regression获得检测框最终的精确位置。

由于现在faster-rcnn已经有许多版本,主要改变是将卷积层换成不同的backbone(即主干网络),

比如我们接下来使用的就是faster-rcnn restnet50版本。

backbone意思是主干网络,或者重要组件,目前常见的深度学习backbone有VGG,GoogleNet,ResNet,MobileNet,EfficientNet,Darknet,Transformer系列等等。后期可以大概学习一下。

【精选】深度学习常用的backbone有哪些_backbone深度学习_万里鹏程转瞬至的博客-CSDN博客

感兴趣的看上面这个博客。

可以细看下RPN网络

分为两条路,上面一条对假设框分类是否含有目标,下面一条用于计算假设框的回归偏移量。所以这里已经有一次损失函数。

这个假设框的生成是对于得到feturemap上每个像素点生成9个框,这个框按照三种长宽比ratio[1:1,1:2,2:1]设置。

后续对检测框使用softmax进行判断是不是目标。

而下面一条网络是用来计算坐标的偏移量,后续对原坐标进行修整。

后续就生成proposal,在这里对检测框坐标修正,剔除一些不良的边界框,在使用NMS非极大值抑制剔除一部分重叠的框。最后输出框的坐标值,这里采用的是框的左上角与右下角坐标值。

总结:生成anchors–>softmax分类器提取positive anchors–>bbox regression回归positive anchors生成偏移量–>生成最终Proposals

ROIpooling,是为了统一最后输入全连接层的featuremap;RoI pooling会有一个预设的pooled_w和pooled_h,表明要把每个proposal特征都统一为这么大的feature map
(1)由于proposals坐标是基于MxN尺度的,先映射回(M/16)x(N/16)尺度
(2)再将每个proposal对应的feature map区域分为pooled_w x pooled_h的网格
(3)对网格的每一部分做max pooling
(4)这样处理后,即使大小不同的proposal输出结果都是pooled_w x pooled_h固定大小,实现了固定长度输出
————————————————
版权声明:本文为CSDN博主「风中一匹狼v」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_42310154/article/details/119889682

大概是这么个意思。

关于损失函数这块我们只要知道RPN网络有两个loss,分类框(二分类),修正框的位置(回归)。以及最后网络还有两个loss,多目标分类,以及框位置的回归。

Faster RCNN与SSD的anchor区别在于:
(1)Faster RCNN在一个特征图上预设了anchor,先进行初步修正与筛选,之后再进行分类与回归(two-stage)
(2)SSD在多个特征图上预设了anchor(多尺度),并直接在所有这些anchor上进行分类与回归(one-stage)

其实目前常用的目标检测网络是Yolo,Yolo快,且准确率差不多。

好了以上就是Faster-RCNN的简介下面看看怎么使用,怎么训练。

3.Faster-RCNN模型导入

这里我们使用的是torchvision中的模型,fasterrcnn_resnet50_fpn。其详细信息可以在Faster R-CNN — Torchvision 0.16 documentation

这里看到,也有使用方法。

如何进行训练与推理:

model = torchvision.models.detection.fasterrcnn_resnet50_fpn(weights = None,progress = True,num_classes = 4)
#train 模式
img,label = next(iter(dl_train))  #现在是元组
label = list(label)  #转成list
print('label:',label)

model.train()  #开启模型train模式
loss_dict = model(img,label)
print('loss_dict:',loss_dict)
#虽然有4个loss,但最后是加一块进行优化的。
losses = sum(loss for loss in loss_dict.values())
print('losses:',losses)

model.eval()
pred = model(img)
print('pred',pred)

for i in range(len(pred)):
    pic_pred = pred[i]['boxes']
    label_boxes = label[i]['boxes']
    iou_tensor = torchvision.ops.box_iou(label_boxes,pic_pred)    #第一个box是真实值的box
    iou_total = np.mean(torch.max(iou_tensor,dim=1)[0].detach().numpy())
    #detach() 用于分离张量,将张量与梯度不再关联
    print('iou_total',iou_total)

来看看输出结果:

第一个print是打印dl_train的数据label,这里label包含真实框boxes的位置信息,以及其种类标签labels。

这里有两个 boxes,以及labels,是因为我们数据加载器中的batch_size是设置为2。所以就是包含两幅图的boxes,labels信息。

可以看到这两幅图中的boxes,labels的数量不一样。

在打开模型的训练模式后,将数据喂入,进行寻训练。输出其loss字典。

loss_dict: {

'loss_classifier': tensor(1.3580, grad_fn=<NllLossBackward0>),

'loss_box_reg': tensor(0.1118, grad_fn=<DivBackward0>),

'loss_objectness': tensor(0.6904, grad_fn=<BinaryCrossEntropyWithLogitsBackward0>),

'loss_rpn_box_reg': tensor(0.0526, grad_fn=<DivBackward0>)}

以上是输出结果,可以看到包含4个loss,分别是多分类器(一般使用交叉熵损失,衡量预测的类别与真实类别之间的差异),box边界框回归(衡量预测的边界框与真实边界框之间的差异),object二分类(是否包含对象),rpnbox回归(rpn边界框回归,用于候选框位置调整)。

下面是对几个loss求和,输出

对一个批次训练完之后,开启了模型推理模式,输入图片,得到推理结果。

其中包含:

boxes的预测很多,label,以及score得分,即预测准确的概率。

后面的for循环中提取了推理得到的预测框信息,以及真实标签框信息,计算其IOU,并计算。

IOU称作交并比,可以用来判断预测的好坏。这里使用的是torch自带的函数。

这里由于真实框与预测框的大小并不一样,所以对相互计算可以得到IOU矩阵,列表示真实框数,行表示预测狂数量,这样按列提取最大值,即可得到有效IOU数字。最后对所有IOU取均值得到最终输出结果。

两个结果。

以上就是如何使用该网络进行训练以及推理。

下一节就是完整训练数据集。

4.训练网络

先看完整代码:

import numpy as np
import glob
from PIL import Image
import torch
import torch.nn as nn
from torch.utils.data import Dataset,DataLoader
from torchvision import transforms
import xml.etree.ElementTree as ET
import torchvision
from tqdm import tqdm

#定义xml提取函数
class_idx = {'WBC':0,'RBC':1,'Platelets':2}
def get_LabelFromXml(xml_file):
    an_file = open(xml_file, encoding='utf-8')  # 用utf-8的格式打开该文件
    tree = ET.parse(an_file)  # 用ET来解析an_file,得到文件内容树格式
    root = tree.getroot()  # 获取树的根目录,抓取xml中的数据,与html的爬取是一样的
    label = []
    bbox_list = []
    for object in root.findall('object'):
        cell = object.find('name').text  # 同理拿到object的name属性,可以得到细胞的类名
        cell_id = class_idx[cell]  #根据字典将类名变成类序号
        xmin = object.find('bndbox').find('xmin').text  # 拿到候选框的位置
        ymin = object.find('bndbox').find('ymin').text
        xmax = object.find('bndbox').find('xmax').text
        ymax = object.find('bndbox').find('ymax').text
        #1 位于边界的框筛选不要
        #2 边界框无大小的筛选不邀
        if int(xmin)== 0 or int(xmax)== 0 or(ymin)== 0 or(ymax)== 0:
            pass #或者continue
        elif int(xmin) ==int(xmax) or(ymin) == (ymax):
            pass
        else:
            label.append(cell_id)   #保存标签名
            bbox_list.append([int(xmin),int(ymin),int(xmax),int(ymax)])  #保存候选框位置 左上角(x1,y1)和右下角(x2,y2)

    return  label,bbox_list

#定义dataset
#pytorch 数据增强时,resize,旋转之类的,目标位置,目标类别都需改变,
transformer = transforms.Compose([transforms.ToTensor(),])
class CellDetection(Dataset):
    def __init__(self,img,xml,transformer = None):
        self.img = img
        self.xml = xml
        self.transformer = transformer
    def __getitem__(self, index):
        img = self.img[index]
        xml = self.xml[index]

        img_open = Image.open(img)
        img_tensor = self.transformer(img_open)
        label, bbox = get_LabelFromXml(xml)
        #将列表转换为tensor   label int64   box float32
        bbox_tensor = torch.as_tensor(bbox,dtype=torch.float32)
        label_tensor = torch.as_tensor(label,dtype=torch.int64)
        #打包表为字典
        target = {}
        target['boxes'] = bbox_tensor
        target['labels'] = label_tensor

        return img_tensor,target
    def __len__(self):
        return len(self.img)

#定义训练函数
def train_one_epoch(model,optimizer,dl_train,dl_test,device,epochs):

    for epoch in range(epochs):
        loss_epoch = []
        iou_epoch = []

        for images,targets in tqdm(dl_train):
            model.train()
            images = list(image.to(device) for image in images)
            targets = [{k:v.to(device) for k,v in t.items()} for t in targets]
            #k,key,v,balue
            loss_dict = model(images,targets)
            losses = sum(loss for loss in loss_dict.values())
            #优化
            optimizer.zero_grad()
            losses.backward()
            optimizer.step()

            #评估IOU与loss
            with torch.no_grad():
                model.eval()
                loss_epoch.append(losses.cpu().numpy())
                label = list(targets)
                try:
                    pred = model(images)
                    for i in range(len(pred)):
                        pic_boxes = pred[i]['boxes']
                        label_boxes = label[i]['boxes']
                        iou_tensor = torchvision.ops.box_iou(label_boxes,pic_boxes)
                        iou_total = np.mean(torch.max(iou_tensor,dim=1)[0].cpu().numpy())
                        iou_epoch.append(iou_total)
                except:
                    continue

        #测试
        test_loss_epoch = []
        test_iou_epoch = []

        for images,targets in tqdm(dl_test):
            model.train()
            img_input = []
            for im in images:
                im = im.to('cuda')
                img_input.append(im)

            label_input = []
            for lb in targets:
                lb_dic = {}
                for k,v in lb.items():
                    v = v.to('cuda')
                    lb_dic[k] = v
                label_input.append(lb_dic)
            loss_dict = model(img_input,label_input)
            losses = sum(loss for loss in loss_dict.values())

            optimizer.zero_grad()
            losses.backward()
            optimizer.step()

            with torch.no_grad():
                model.eval()
                test_loss_epoch.append(losses.cpu().numpy())
                label = label_input
                try:
                    pred = model(img_input)
                    for i in range(len(pred)):
                        pic_boxes = pred[i]['boxes']
                        label_boxes = label[i]['boxes']
                        iou_tensor = torchvision.ops.box_iou(label_boxes, pic_boxes)
                        iou_total = np.mean(torch.max(iou_tensor, dim=1)[0].cpu().numpy())
                        test_iou_epoch.append(iou_total)
                except:
                    continue
        mIou = np.mean(iou_epoch)
        epochloss = np.mean(loss_epoch)
        test_mIou = np.mean(test_iou_epoch)
        test_epochloss = np.mean(test_loss_epoch)

        static_dict=model.state_dict()
        torch.save(static_dict,'./data/checkpoint/{}_train_mIou_{}_test_mIou_{}.pth'.format(epoch,round(mIou, 3),round(test_mIou,3)))
        print('epoch:', epoch,
              'epoch_mIou:', mIou,
              'epoch_loss:', epochloss,
              'test_epoch_mIou:', test_mIou,
              'test_epoch_loss:', test_epochloss)

#训练数据
train_path = './data/train/'  #设置需要爬取的文件路径,当前文件下data下的train文件
test_path = './data/test/'  #设置需要爬取的文件路径,当前文件下data下的train文件
train_xml_file = glob.glob(train_path+'*.xml') #通过glob来爬取文件中.xml结尾的文件,文件名
test_xml_file = glob.glob(test_path+'*.xml') #通过glob来爬取文件中.xml结尾的文件,文件名
train_img_file = glob.glob(train_path+'*.jpg') #通过glob来爬取文件中.jpg结尾的图片,文件名
test_img_file = glob.glob(test_path+'*.jpg') #通过glob来爬取文件中.jpg结尾的图片,文件名
#乱序文件调整顺序
train_xml_list = []
train_img_list = []

test_xml_list = []
test_img_list = []

for i in train_xml_file:
    img = i[:-3]+'jpg'    #将xml文件名后三位改成jpg,就是图像的文件名
    if img in train_img_file:   #判断该文件名是不是在img_file中,如果在,那就加入img_list
        train_img_list.append(img)
        train_xml_list.append(i)

train_data = CellDetection(train_img_list,train_xml_list,transformer)

for i in test_xml_file:
    img = i[:-3]+'jpg'    #将xml文件名后三位改成jpg,就是图像的文件名
    if img in test_img_file:   #判断该文件名是不是在img_file中,如果在,那就加入img_list
        test_img_list.append(img)
        test_xml_list.append(i)

test_data = CellDetection(test_img_list,test_xml_list)
#测试
#print(train_data.xml[5])

#定义打包函数
def detection_collate(x):
    return list(tuple(zip(*x)))
"""
当你的数据集包含不同维度或结构的数据时,你可能需要自定义collate_fn来确保每个batch的数据可以被正确处理。
例如,在目标检测任务中,每个数据样本可能包含图像和与图像相关的目标框(bounding boxes),
而这些数据的结构可能会有所不同,因此你需要编写自定义的函数来将它们组合成一个batch。
"""

dl_train = DataLoader(train_data,batch_size = 1,shuffle=True,collate_fn= detection_collate)
dl_test = DataLoader(test_data,batch_size = 1,shuffle=True,collate_fn= detection_collate)

#模型

model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained = False,progress = True,num_classes = 4)
model = model.to('cuda')
optimizer=torch.optim.Adam(model.parameters(),lr=0.0001)
train_one_epoch(model,optimizer,dl_train,dl_test,device='cuda',epochs=100)

这里定义了一个训练函数。train_one_epoch,里面包含train与test,可以随时观察loss以及IOU情况。由于我的电脑显卡一般,这里我实在Autodl中租了一台机子训练。

这里需要主要的一些点就是test_data这块需要加transfomer,将数据变为tensor格式,要不然会报错。

除此之外还有就是model的参数,pretrain这一块,由于torch版本不同,我是11.6需要改成pretrain = False,老版本的则需要将其改为参数weight = None。

这样就可以进行训练加测试了。在大概训练20次后loss。

在每个epoch中是将模型的权重值都保留了下来,有具体的loss数值,方便后其寻找,推理。

优化器选用的是Adam,学习率是0.0001,先别问为什么,就是这个值的loss好一些,感兴趣的也可以改改这些参数自己训练。这是学习的深度学习网络的第一步。

对于初学者,聊一下神经网络训练,数据传入网络中,在进行每次正向传播后,需要使用优化器来更新网络中的权重参数。

在使用PyTorch进行神经网络训练时,通常会按照以下步骤来执行优化器的更新,这通常发生在每个训练迭代(或称为epoch)中:

  1. optimizer.zero_grad():首先,你需要清零(zero_grad)优化器中的梯度信息。这是因为PyTorch会累积梯度,所以在每次训练迭代之前都需要将梯度重置为零,以避免梯度信息的叠加。

  2. losses.backward():接下来,你会计算损失函数(loss)并调用backward()方法来计算损失相对于网络参数的梯度。这将根据反向传播算法计算出每个参数的梯度。

  3. optimizer.step():最后,你会调用step()方法来更新模型的参数,以最小化损失。optimizer.step()将使用计算出的梯度来更新模型的权重。不同的优化算法(如SGD、Adam、RMSprop等)会根据梯度信息来更新参数,以降低损失函数的值。

这一组步骤通常在每个训练迭代中重复执行,直到达到所需的训练轮次或其他停止条件。整体的神经网络训练过程会重复这些步骤,以不断优化模型参数以逼近最佳性能。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值