[深度学习初识 - 实操笔记] 卷积神经网络-MTCNN人脸侦测

卷积神经网络

1. 全连接与卷积神经网络的区别

(1)全连接神经网络:一个神经元看一个数据,一个图片有CWH个数据,计算量大,而且前一层的信息全盘接收。
(2)卷积神经网络:每个神经元可通过看一部分的局部信息得到一个结果,这些局部信息可以相互重叠。相比全连接,任务减轻了,计算量也减小了。(卷积核是通过学习自动生成的)

2. 卷积神经网络参数

nn.Conv2d(3,16,3,1,padding=1) #——pytorch.nn: 

输入三通道,输出16通道,卷积核大小3*3,步长为1,padding-扩展原图像大小(用来保证输出图像大小)。
参数:
(1)核大小: kernel_size
卷积核尺寸,(H,W) ,卷积核内容通过学习自动生成。
(2)步长:stride
卷积的步长。可作下采样。
(3)补充:padding
希望填充后的特征图大小符合预期。
(4)空洞:dilation
在卷积核内部加入空洞,不做计算。初始化值为1.。

3. 卷积神经网络计算

(1)输出特征图的大小:Lout = (Lin − kernel_size + 2*padding )/stride+1
Lout 输出图大小; Lin输入图大小;kernel_size核大小;padding补充值;stride步长。
(2)网络计算量:FLOPs = 2 * Ci * k^2 * Co * W * H
(3)参数量计算,权重数量:params = Co * (k^2 * Ci +1)
FLOPs 网络计算量;Ci 输入的通道量; k 卷积核大小; Co 输出的通道量。
(4)代码 thop:只支持torch.nn内的网络结构。

flops, params = thop.profile(conv,(x,)
#传入模型和输入参数(输入为元组),返回计算量和参数量
flops,params = thop.clever_format((flops,params),"%.3f")
#转换显示格式

4. 感受野

(1)某一层的特征图的某个特征点位置代表前一层(前面某层的特征/数据)的范围。
(2)感受野计算公式:RFi = (RFj-1)* stridei + Ksizei
RF:感受野;i :第i层;j :第 i+1 层;stridei : 第 i 层的步长;Ksizei : 第 i 层的核大小。

5. 池化

(1)池化类型:最大池化;平均池化;自适应池化:某块区域根据某种规则进行降维。

nn.MaxPool2d(kernel_size=2, stride=2)#用最大池化下采样

(2)池化作用:最大池化是为了更加关注某些特征;特征降维;在一定程度上防止过拟合;平均池化可用来缩放。

6. 空洞 dilation

在低计算量的情况下,扩大感受野。

7. 交叉熵

(1)输出结果为概率时候,使用交叉熵。
(2)计算比MSE快。
(3)pytorch 自带softmax和onehot转换:

self.loos_fn = nn.CrossEntropLoss()

8. 卷积神经网络的优化

(1)残差网络(用来加深网络):
将初值加入结果——输出输入的结果大小(维度)必须相等(可以通过输出时,加卷积层调整输出矩阵形状。)
torch内置有常用的resnet。
Densenet:稠密网络。每个稠密快内部都是各层残差累加。
(2)BatchNormal:将输出的权重标准正态分布。
可以使用更大的学习率;
可以不需要偏值;
对权重初始化和尺寸不再敏感,激活函数有更多选择;
不需要dropout就可以减少过拟合。
(3)权重初始化:防止梯度弥散和梯度暴涨。
主要功能:使权重有一定堆积选;对权重的方差有所控制(不同激活层方差相同);

nn.init.kaiming_normal_()#凯明/xavier初始化
nn.init.zeros_(m.bias)#初始化偏值为0

(4)正则化:(防止过拟合)
在loss后面加上正则化,可控制在只在某一层使用正则化。

optim.Adam(self.net.parameters(),weight_decay=0.1)#对定义为参数的数用L2正则化。

(5)Dropout :(随机屏蔽掉一些神经元)

nn.Dropout2d(0.5) #丢掉50%的神经元 代码里卷积dropout后缀2d..全连接没有

(6)通道优化:将低计算量、提高精度。
① 分离卷积/分组卷积:把通道分组连接(降低计算量,但是感受野分离,网络会变笨,性能降低——解决方式:通道混洗。)
② 通道混洗:将通道C拆分,转置,融合恢复。(使通道充分混合)
③ 卷积分解:将33卷积核分解成13和3*1。(通道多的时候速度快,增加了层次)

9. 常用的卷积网络

(1)MobileNet (V1/V2) :可以在移动设备上运行。使用了分离卷积的优化方法,将像素上的数据和通道上的数据充分融合。
(2)ShuffleNet :通道混洗和分离卷积。
(3)ResNet:将通道变小,在复原。(有损压缩,但是能控制失去的数据一般是不重要的特征。)
(4)Inception:(逐渐被淘汰。)
(5)SqueezeNet:第一个能用到移动设备上的网络。用到了通道分离。
(6)EfficientNet:让AI自己在通道数、网络深度、图片尺寸中找到一个最优的参数。

MTCNN 人脸侦测项目

人脸识别主要包括:人脸侦测、特征提取、特征对比。——这里做第一步,人脸侦测。

1. MTCNN主要用到的技巧

(1)图像金字塔
主要解决:目标识别中,建议框大小的选择问题。(即多大的建议框可以将目标包围 / 多目标的情况下,如何确定不同目标使用不同大小建议框)
图像金字塔,将图像不断按比例缩放构建金字塔。建议框大小不变,在图像金字塔每一层中分别进行检测。总有一层合适的金字塔,使得建议框的大小适合目标大小。
(2)IOU:评价当前框的好坏。
建议框(假设为当前可能检测到目标的建议框)与实际框的IOU。
① 交集/并集:值越大,证明该建议框包围住的目标占本身目标的比例大。(但是可能出现,框太小,且在目标内部,即值为1的情况)
②交集/最小框面积:可排除①的弊端。
(3)NMS 非极大值抑制:去掉多余的线框(解决重叠框的问题)
步骤:①先取IOU最大/(或置信度最大) 的框(与目标框的IOU)
②另外的建议框与①取得的框作IOU,除去与上一步的框重叠多(IOU大)的框。
③在剩下的框中重复步骤①。
④直到所有可能目标都找到。
(4)特征映射:将图片中的一个建议框(区域)映射成一个像素。(区域可重叠)
(5)将多目标检测变成单目标检测:每个建议框都进行一个卷积神经网络,都有一个单目标任务;即这个建议框内是否有目标。

2. MTCNN输出结构

(1)置信度:判断区域是否有目标的概率。(取值0-1:sigmoid)
(2)目标坐标偏移量:是将建议框作参照物的偏移量。(建议框可通过卷积步长和建议框大小求出建议框位置,加上偏移量即为目标位置)——要通过反算得到目标在原图上的位置。

3. MTCNN代码

(1)数据处理

① Celeba数据集:抽查样本(看看样本标注情况和人脸情况);人脸目标框误差大,可以做偏移矫正;查看标签文本的读入格式。(样本框对左上和右下对角点数据)

#由于样本不好 box做的矫正
x1,y1 = int(x + w * 0.12),int(y + h *0.1)
x2,y2 = int(x + w * 0.9),int(y + h *0.85)
x,y,w,h = x1,y1,x2-x1,y2-y1

② 生成正样本、负样本、部分人脸样本。
样本做偏移,用IOU判别是正样本还是负样本还是部分人脸样本。(这里设置:IOU大于0.65为正样本,IOU大于0.4位部分样本,IOU小于0.3为负样本。)
生成正样本

# 中心和宽高做偏移 制作人脸、部分人脸和地标样本
cx,cy = int(x+w/2),int(y+h/2)
_cx,_cy = cx + np.random.randint(-w*0.2,w*0.2),cy + np.random.randint(-h*0.2,h*0.2)

# 让人脸形成正方形(12*12,24*24,48*48),并且让坐标也有少许的偏离
side_len = np.random.randint(int(min(w, h) * 0.8), np.ceil(1.25 * max(w, h))) # 边长偏移的随机数的范围;ceil大于等于该值的最小整数(向上取整);原0.8
_x1 = np.max(_cx - side_len / 2, 0) # 坐标点随机偏移
_y1 = np.max(_cy - side_len / 2, 0)
x2 = _x1 + side_len
_y2 = _y1 + side_len

生成负样本

img_w, img_h = img.size
if max(w, h) < 40 or x1 < 0 or y1 < 0 or w < 0 or h < 0 or min(img_w, img_h) / 2 <= 50:
	continue

side_len = np.random.randint(self.img_size, min(img_w, img_h) / 2)
                
_x1,_y1 = np.random.randint(0, img_w - side_len),np.random.randint(0, img_h - side_len)
_x2, _y2 = _x1 + side_len, _y1 + side_len

_x1_off, _y1_off, _x2_off, _y2_off = (x1 - _x1)/side_len, (y1 - _y1)/side_len, (x2 - _x2)/side_len, (y2 - _y2)/side_len

裁剪出偏移后的框与目标框作IOU,然后按不同类型保存样本以及标签(这里有加上人脸关键点数据)。
裁剪后,按样本需求缩放,分别存储不同大小的样本。(这里需要尺寸为12,24,48的样本)

iou = utils.iou(np.array([x1,y1,x2,y2]),np.array([[_x1,_y1,_x2,_y2]]))
if iou>0.65:
	clip_img.save(f"{self.positive_image_dir}/{positive_count}.jpg")
                    
	positive_label_txt.write(f"{positive_count}.jpg 1 {_x1_off} {_y1_off} {_x2_off} {_y2_off} {_lefteye_x_off} {_lefteye_y_off} {_righteye_x_off} {_righteye_y_off} {_nose_x_off} {_nose_y_off} {_leftmouth_x_off} {_leftmouth_y_off} {_rightmouth_x_off} {_rightmouth_y_off}\n")
	positive_label_txt.flush()
	positive_count += 1
elif iou>0.4:
	clip_img.save(f"{self.part_image_dir}/{part_count}.jpg")
	part_label_txt.write(f"{part_count}.jpg 2 {_x1_off} {_y1_off} {_x2_off} {_y2_off} {_lefteye_x_off} {_lefteye_y_off} {_righteye_x_off} {_righteye_y_off} {_nose_x_off} {_nose_y_off} {_leftmouth_x_off} {_leftmouth_y_off} {_rightmouth_x_off} {_rightmouth_y_off}\n")
	part_label_txt.flush()
	part_count += 1
elif iou<0.3:
	clip_img.save(f"{self.negative_image_dir}/{negative_count}.jpg")
	negative_label_txt.write(f"{negative_count}.jpg 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n")
	negative_label_txt.flush()
	negative_count += 1

(2)生成数据集

from torch.utils.data import Dataset
from PIL import Image
import numpy as np
from torchvision import transforms

tf = transforms.Compose([
    transforms.ToTensor()
])

class MyDataset(Dataset):

    def __init__(self,root,img_size):

        self.dataset = []

        #positive_file = open(f"{root}/img_size/positive.txt","r")
        #negative_file = open(f"{root}/img_size/negative.txt","r")
        #part_file = open(f"{root}/img_size/part.txt","r")

        #self.dataset.extend(positive_file.readlines())
        #self.dataset.extend(negative_file.readlines())
        #self.dataset.extend(part_file.readlines())

        #positive_file.close()
        #negative_file.close()
        #part_file.close()

        self.img_root_dir =f"{root}/{img_size}"

        with open(f"{self.img_root_dir}/positive.txt","r") as f:
            self.dataset.extend(f.readlines())
        with open(f"{self.img_root_dir}/negative.txt","r") as f:
            self.dataset.extend(f.readlines())
        with open(f"{self.img_root_dir}/part.txt","r") as f:
            self.dataset.extend(f.readlines())

    def __len__(self):
        return len(self.dataset)

    def __getitem__(self,index):
        data = self.dataset[index]
        strs = data.split()

        img_path = None
        
        if strs[1] == "1":
            img_path = f"{self.img_root_dir}/positive/{strs[0]}"
        elif strs[2] == "2":
            img_path = f"{self.img_root_dir}/part/{strs[0]}"
        else:
            img_path = f"{self.img_root_dir}/negative/{strs[0]}"

        img_data = tf(Image.open(img_path))
        #print(type(img_data))

        c,x1,y1,x2,y2 = float(strs[1]),float(strs[2]),float(strs[3]),float(strs[4]),float(strs[5])

        px1,py1 = float(strs[6]),float(strs[7])
        px2,py2 = float(strs[8]),float(strs[9])
        px3,py3 = float(strs[10]),float(strs[11])
        px4,py4 = float(strs[12]),float(strs[13])
        px5,py5 = float(strs[14]),float(strs[15])

        return img_data, np.array([c,x1,y1,x2,y2,px1,py1,px2,py2,px3,py3,px4,py4,px5,py5], dtype=np.float32)

#测试
if __name__ == '__main__':
    dataset = MyDataset(r"F:\AIStudyCode\MTCNN\MTCNN",12)
    print(dataset[0])

(3)网络创建

卷积层要求,P网络训练12尺寸,R网络训练24尺寸,O网络训练48尺寸。输出为一个像素大小的特征图。
① P网络 :训练12尺寸的图片。
P网络末尾没有全连接层。网络层次小,但是最终得到的数据最多。(利用较小的置信度阈值与nms阈值,尽可能不放过每一个有目标的区域。)

class PNet(nn.Module):

    def __init__(self):
        super().__init__()

        self.sequential = nn.Sequential(
            nn.Conv2d(3,10,3,stride=1,padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3,stride=2),

            nn.Conv2d(10,16,3,stride=1),
            nn.ReLU(),

            nn.Conv2d(16,32,3,stride=1),
            nn.ReLU(),

            nn.Conv2d(32,15,1,stride=1)
            )

② R网络:训练24尺寸的图片
将P网络得到的建议框进一步筛选,得到毕竟少的建议框,进入下一层网络。

class RNet(nn.Module):
    def __init__(self):
        super().__init__()

        self.backbone = nn.Sequential(
            nn.Conv2d(3,28,3),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),

            nn.Conv2d(28,48,3),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),

            nn.Conv2d(48,64,2),
            nn.ReLU(),
            )

        self.ouput_layer = nn.Sequential(
            nn.Linear(3*3*64,128),
            nn.ReLU(),
            nn.Linear(128,15)
            )

③ O网络:训练尺寸为48的图片。
利用较大的置信度阈值与nms阈值,筛选出最可能为目标的建议框。要用交集/并集,筛选重叠框,也要用 交集/最小框, 除去较小框。)

class ONet(nn.Module):
    def __init__(self):
        super().__init__()

        self.backbone = nn.Sequential(
            nn.Conv2d(3,32,3),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),

            nn.Conv2d(32,64,3),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),

            nn.Conv2d(64,64,3),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),

            nn.Conv2d(64,128,2),
            nn.ReLU(),
            )

        self.ouput_layer = nn.Sequential(
            nn.Linear(3*3*128,256),
            nn.ReLU(),
            nn.Linear(256,15)
            )

(4)网络训练

① 根据不同尺寸的图片,选择不同的网络训练。
② 损失:置信度损失用正样本和负样本训练,坐标点的损失用正样本和部分样本训练。

#损失:目标框的数据与网络训练得到的数据作平方差
c_mask = tag[:,0] < 2
c_predict = predict[c_mask]
c_tag = tag[c_mask]
loss_c = torch.mean((c_predict[:,0] - c_tag[:,0])**2)

off_mask = tag[:,0] > 0
off_predict = predict[off_mask]
off_tag = tag[off_mask]
loss_off = torch.mean((off_predict[:,1:] - off_tag[:,1:])**2)

loss = loss_c + loss_off

self.opt.zero_grad()
loss.backward()
self.opt.step()

(5)工具(iou&nms)

import numpy as np

#[x1,y1,x2,y2]
def iou(box,boxes, is_min = False):
    box_area = (box[2]-box[0]) * (box[3] - box[1])
    boxes_area = (boxes[:,2]-boxes[:,0]) * (boxes[:,3] - boxes[:,1])

    inter_x1 = np.maximum(box[0],boxes[:,0])
    inter_y1 = np.maximum(box[1],boxes[:,1])
    inter_x2 = np.minimum(box[2],boxes[:,2])
    inter_y2 = np.minimum(box[3],boxes[:,3])

    inter_w = np.maximum(0, inter_x2 - inter_x1)
    inter_h = np.maximum(0, inter_y2 - inter_y1)

    inter = inter_w * inter_h

    if is_min:
        return inter / np.minimum(box_area,boxes_area)
    else:
        return inter / (box_area + boxes_area - inter)

def nms(boxes,threshold, is_min = False):
    if boxes.shape[0] == 0: return np.array([])
    _boxes = boxes[(-boxes[:,4]).argsort()]
    r_boxes = []

    while _boxes.shape[0] > 1:
        a_box = _boxes[0]

        b_boxes = _boxes[1:]

        r_boxes.append(a_box)

        _boxes = b_boxes[iou(a_box,b_boxes,is_min)<threshold]

    if _boxes.shape[0] == 1:
        r_boxes.append(_boxes[0])

    return np.stack(r_boxes)

(6)人脸侦测

侦测中网络步骤:
① 读入侦测图片。
② 将原图按比例缩放。(图像金字塔)
③ 将缩放后的图片放入P网络训练。(图片不小于12)
④ 反算建议框:实际框 = (建议框位置+偏移量(网络得到的)) / 缩放比例 (反算的实际步骤,根据样本设计时候的计算公式进行反算)
⑤ 用 nms 筛选框。
⑥ 放入R&O网络,得到最可能为目标的建议框,在调整置信度阈值和nms阈值筛选。(这两个网络步骤相似,除了缩放比例、nms和置信度阈值不同)

你可能感兴趣的:(深度学习)