人脸识别损失函数之Center Loss

人脸识别的难点在于:
1、不同类人脸类别之间的分类界限不明显;
2、人脸之间的相似度很高,人类也难以区分;
3、人脸的特征分类困难。

解决途径:
1、网络模型:
残差 深度可分类 稠密网络 densenet alexet inception net 等等。。
2、损失函数:
对于回归问题,常用的损失函数是均方误差(MSE,Mean Squared Error)。
对于分类问题,常用的损失函数为交叉熵(CE,Cross Entropy)。交叉熵一般与one-hot和softmax在一起使用。
改进多分类损失函数,对细微特征的能达到好的分类效果。

一、One-Hot

在分类问题中,one-hot编码是目标类别的表达方式。目标类别需要由文字标签,转换为one-hot编码的标签。one-hot向量,在目标类别的索引位置是1,在其他位置是0。类别的数量就是one-hot向量的维度。在one-hot编码中,假设类别变量之间相互独立。同时,在多分类问题中,one-hot与softmax组合使用。

import numpy as np

def one_hot(arr):
    """
    概率矩阵转换为One-Hot矩阵
    arr = np.array([[0.1, 0.5, 0.4], [0.2, 0.1, 0.6]])
    :param arr: 概率矩阵
    :return: One-Hot矩阵
    """
    arr_size = arr.shape[1]  # 类别数
    arr_max = np.argmax(arr, axis=1)  # 最大值位置
    oh_arr = np.eye(arr_size)[arr_max]  # One-Hot矩阵
    return oh_arr

二、Softmax

softmax使得神经网络的多个输出值的总和为1,softmax的输出值就是概率分布,应用于多分类问题。softmax也属于激活函数。softmax、one-hot和cross-entropy,一般组合使用。
人脸识别损失函数之Center Loss_第1张图片

import numpy as np
def softmax(x):
    orig_shape=x.shape
    if len(x.shape)>1:
        #矩阵
        tmp=np.max(x,axis=1)  # 取最大值
        x-=tmp.reshape((x.shape[0],1))  # 减等最大值,防止指数爆炸
        x=np.exp(x)
        tmp=np.sum(x,axis=1)  # 重定义tmp为公式分母
        x/=tmp.reshape((x.shape[0],1))
        print("matrix")
    else:
        #向量
        tmp=np.max(x)
        x-=tmp
        x=np.exp(x)
        tmp=np.sum(x)
        x/=tmp
        print("vector")
    return x

三、Cross-entropy

熵,热力学中表征物质状态的参量之一,用符号S表示,其物理意义是体系混乱程度的度量。香农提出了“信息熵”的概念,才解决了对信息的量化度量问题。
给定两个概率分布:p(理想结果)和q(随机分布),则通过q来表示p的交叉熵为:
人脸识别损失函数之Center Loss_第2张图片
交叉熵刻画的是通过概率分布q来表达概率分布p的困难程度,其中p是正确答案,q是预测值,也就是交叉熵值越小,两个概率分布越接近。这样我们可以用交叉熵来比较经过softmax输出和one-hot编码(标签)之间的距离,即模型的输出和真值,再对得到的损失进行优化。

四、Softmax Loss

多分类激活函数 softmax:将输入范围正负无穷,输出为:0~1的概率值,可以扩大相邻数值间的差距,所有概率的和为一。对softmax激活后的结果再使用交叉熵就是Softmax loss,使用Softmax loss可以刚好把人脸分开,但不能进行很好的应用。因为softmax经过了独热编码,标签为正时,yj=1,负yj=0。公式简化为:
人脸识别损失函数之Center Loss_第3张图片
softmax输出为0~1之间的值,求log之后:值域为负无穷到0,求负后为0到正无穷。当损失为0时,softmax输出值为1,刚好满足交叉熵的定义。
下图中,当Pj为softmax输出的(0~1)概率时,两者等价
人脸识别损失函数之Center Loss_第4张图片
人脸识别损失函数之Center Loss_第5张图片
在使用Softmax Loss对手写数字识别的分类时,可视化效果如下图。贴个论文地址。
人脸识别损失函数之Center Loss_第6张图片
对于可以看出的10个分类而言,我们希望分类效果好意味着,让每个类之间的距离变得更大,分的更开。而且:类的中心处没有分开,我们要在增加类间距的同时,还要减小类内距离,这里引入Center loss来配合原损失函数达到给每个类规划一个中心并使其在分类的同时,类内距离减小。注意:Center loss不可以单独去使用,要配合Softmax loss。
在这段发展过程中还有两个插曲:Siamese Network 、Triplet Loss。这里不展开讨论了。

五、Center Loss
论文链接:https://ydwen.github.io/papers/WenECCV16.pdf
原文片段:增加类间距的同时,还要减小类内距离。
在这里插入图片描述
人脸识别损失函数之Center Loss_第7张图片
类中心c:

每一个样本的特征需要通过一个好的网络到达特征层获得,这样计算完后所有样本的特征的平均值为类中心c,而好的网络需要是在有类中心加入的情况下才能得到。

优化过程:

每个类别的中心c无法直接获得,我们将其放到网络里根据输入的feature和label的形状自己生成,在每一个batch里更新center.即随机初始化center,而后每一个batch里计算当前数据与center的距离,而后将这个梯度形式的距离加到center上。类似于参数修正。同样的类似于梯度下降法,这里再增加一个scale度量α,使得center不会抖动。一般设置为0.5。可以视其为步长或者学习率。
人脸识别损失函数之Center Loss_第8张图片
如何配合使用呢?看以下公式:
人脸识别损失函数之Center Loss_第9张图片
红色框是Softmax loss,蓝色框是Center loss,其中的λ是人为给定的平衡两个损失的权重,见下图中不同λ对应的分类效果有所不同。看在训练时更加侧重于哪个损失。在训练人脸数据集时,一般取值0.001,在训练数字10分类时一般给1或者2都行。
人脸识别损失函数之Center Loss_第10张图片
如何将Center loss应用到实际项目中呢?见下图中,我们需要将倒数第二层经过最大值池化的输出通过全连接层输出,这部分是用来做Center loss的,另外一部分,经过最后一层卷积输出的再经过全连接层输出,用来做Softmax loss。在提取人脸特征时,需要经过池化后输出的维度N大于128,因为这里维度越高,学习的信息也就越全面,在进行手写数字分类任务中,N=2,这里是为了可视化我们的分类效果。
人脸识别损失函数之Center Loss_第11张图片
综上所述,我们使用Softmax loss 来达到增大类间距离的目的,使用Center loss 来达到使类内距离减小的目的。下篇讲一下ArcSoftmax。
Center loss的缺点:
1、因为他在使用时为每个类都要确定一个中心,相当于在正常分类的同时,增加了类别中心的计算和更新,对我们的硬件要求较高
2、因为正则化方式L2范数的平方问题,而Center loss又是用所有点对中心点进行计算后取均值,其中的离群点会对整体结果有较大影响,导致离群点不容易回归
3、Center loss不适合用于对类别差异较大数据分类,而用于单类多目标(同一类,类间相似,类外不同)的数据适用于人脸,手写数字的分类。
手写数字10分类Center loss效果代码,目前Pytorch没有封装Center loss的函数需要自己写。第一部分代码是单独Center loss损失的设计测试,第二部分是使用两个优化器单独优化softmax loss和center loss,第三部分是训练。
在这里补上center loss求解形状变换图:
人脸识别损失函数之Center Loss_第12张图片

Part 1

import torch
import torch.nn as nn

def center_loss(output=None,feature, label, lambdas):
    """
    :param output: shape[N,10] 网络输出,用于做BCEloss 这里用来生成center的shape[0],如手写数字为10个类
    :param feature: shape[N,2] 特征层数据,用于做损失
    :param label: shape[N] 标签
    :param lambdas: λ超参数(学习率、步长)
    :return: loss
    """
    label = label.unsqueeze(0)
    center = nn.Parameter(torch.randn(output.shape[1], feature.shape[1]), requires_grad=True).cuda()
    #center: torch.Size([10, 2]) 其中10为类别数,2为center的维度,可视化过程中为2用于绘图。
    #注意:feature与label对应的为一类,例如 feature的第4个[N,2]对应的类别为label中的[N],这样我们就要为每一个feature的相同的类规定一个中心,使用centreloss把这些相同类的拉到一堆。
    #首先随机给定中心,但是要确定center的shape,因为特征点要与中心点相减,但是现在维度形状不一样
    #使用index_select让center按照标签的形式扩张
    #torch.index_select()函数:
    label = label.squeeze()
    center_exp = center.index_select(dim=0, index=label.long())
    #center_exp: torch.Size([N, 2]) 原来center为[10,2]: 10为总的类别数,2为每个类的中心点,center_exp为根据label索引排列为[N,2],这样相当于为N个数据都对应了一个中心点。
    #bins=int(max(label).item() + 1):当前批次label中包含的类别,不能用长度
    #max=int(max(label).item() 
    count = torch.histc(label, bins=int(max(label).item() + 1), min=0, max=int(max(label).item()))
    #做统计直方图,统计每个类别出现了几次
    #count: torch.size([10]):10为这批次里有10个类,其中每个对应的数字就是该类别出现的次数
    count_exp = count.index_select(dim=0, index=label.long())
    #按照label去扩张,同样得到的是每个label的元素按照N个去对应的该类别所对应的出现的次数。
    #下面做个图
    #count_exp: torch.size([N])
    loss = lambdas/2*torch.mean(torch.div(torch.sum(torch.pow(feature - center_exp, 2), dim=1), count_exp))
    return loss

Part 2 网络及损失

import torch.nn as nn
import torch
import matplotlib.pyplot as plt

class CenterLoss(nn.Module):
    def __init__(self, cls_num, feature_num):
        super().__init__()
        self.cls_num = cls_num
        self.center = nn.Parameter(torch.randn(cls_num, feature_num))
    
    def forward(self, xs, ys):
        center_exp = self.center.index_select(dim=0, index=ys.long())
        count = torch.histc(ys, bins=self.cls_num, min=0, max=self.cls_num - 1)
        count_dis = count.index_select(dim=0, index=ys.long())
        return torch.mean(torch.div(torch.sum(torch.pow(xs - center_exp, 2), dim=1), count_dis))

class Net2(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv_layer = nn.Sequential(
            nn.Conv2d(1, 32, 5, 1, 2),  # 28*28
            nn.BatchNorm2d(32),
            nn.PReLU(),
            nn.Conv2d(32, 32, 5, 1, 2),  # 28*28
            nn.BatchNorm2d(32),
            nn.PReLU(),
            
            nn.MaxPool2d(2, 2),  # 14*14
            nn.Conv2d(32, 64, 5, 1, 2),  # 14*14
            nn.BatchNorm2d(64),
            nn.PReLU(),
            nn.Conv2d(64, 64, 5, 1, 2),  # 14*14
            nn.BatchNorm2d(64),
            nn.PReLU(),
            
            nn.MaxPool2d(2, 2),  # 7*7
            nn.Conv2d(64, 128, 5, 1, 2),  # 7*7
            nn.BatchNorm2d(128),
            nn.PReLU(),
            nn.Conv2d(128, 128, 5, 1, 2),  # 7*7
            nn.BatchNorm2d(128),
            nn.PReLU(),
            nn.MaxPool2d(2, 2)  # 3*3
        )
        
        self.feature = nn.Linear(128 * 3 * 3, 2)  # 2:(x,y)
        self.output_layer = nn.Linear(2, 10)

        self.center_loss_layer = CenterLoss(10, 2)
        # self.center_loss = self.center_loss_layer()

    def forward(self, xs):
        _feature = self.conv_layer(xs)
        y_conv = torch.reshape(_feature, [-1, 128 * 3 * 3])
        y_feature = self.feature(y_conv)  # [N 2]
        y_output = torch.log_softmax(self.output_layer(y_feature), dim=1)
        # print(y_feature.shape, y_output.shape)
        return y_feature, y_output
    
    def get_loss(self, features, labels):
        loss_center = self.center_loss_layer(features, labels)
        return loss_center

    def visualize(self, feat, labels, epoch):
        # plt.ion()
        color = ['#ff0000', '#ffff00', '#00ff00', '#00ffff', '#0000ff',
                 '#ff00ff', '#990000', '#999900', '#009900', '#009999']
        plt.clf()
        for i in range(10):
            plt.plot(feat[labels == i, 0], feat[labels == i, 1], '.', c=color[i])
        plt.legend(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], loc='upper right')
        # plt.xlim(xmin=-5, xmax=5)
        # plt.ylim(ymin=-5, ymax=5)
        plt.title("epoch=%d" % epoch)
        plt.savefig('./images/epoch=%d.jpg' % epoch)
        # plt.draw()
        # plt.pause(0.001)

Part 3 训练

import torch
import torch.nn as nn
import torch.utils.data as data
import torchvision
import torchvision.transforms as transforms
from center.Center_loss_Net import CenterLoss, Net2
import os
import numpy as np

if __name__ == '__main__':
    
    save_path1 = "./params3/net_center1.pth"
    train_data = torchvision.datasets.MNIST(root=r"C:\Projects", download=False, train=True,
                                            transform=transforms.Compose([transforms.ToTensor(),
                                                                          transforms.Normalize(mean=[0.5, ],
                                                                                               std=[0.5, ])]))
    train_loader = data.DataLoader(dataset=train_data, shuffle=True, batch_size=100)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    net = Net2().to(device)
    if os.path.exists(save_path1):
        net.load_state_dict(torch.load(save_path1))
    else:
        print("NO Param")
    lossfn_cls = nn.NLLLoss()
    optimzer = torch.optim.Adam(net.parameters())
    optimzer2 = torch.optim.SGD(net.center_loss_layer.parameters(), lr=0.5, momentum=0.9)
    epoch = 0
    while True:
        feat_loader = []
        label_loader = []
        for i, (x, y) in enumerate(train_loader):
            x = x.to(device)
            y = y.to(device)
            # x = torch.reshape(x, [-1, 28*28])
            feature, output = net.forward(x)
            # feature2, output2 = net2.forward(x)
            # print(feature.shape)  # [N,2]
            # print(feature.shape)#[N,2]
            # print(output.shape)#[N,10]
            # center = nn.Parameter(torch.randn(output.shape[1], feature.shape[1]))
            # print(center.shape)#[10,2]
            loss_cls = lossfn_cls(output, y)
            # y = y.float()
            # loss_cls, loss_center = net.get_loss(output, feature)
            # loss_center = lossfn_2(feature2, feature2, y)
            # print(feature2, y)
            loss_center = net.get_loss(features=feature, labels=y)
            
            loss = loss_cls + loss_center
            optimzer.zero_grad()
            optimzer2.zero_grad()
            # loss_cls.backward(retain_graph)
            # loss_center.backward()
            loss.backward()
            optimzer.step()
            optimzer2.step()
            # feature.shape=[100,2]
            # y.shape=[100]
            feat_loader.append(feature)
            label_loader.append(y)
            if i % 20 == 0:
                print("epoch:", epoch, "i:", i, "total:", loss.item(), "softmax_loss:", loss_cls.item(), "center_loss:",
                      loss_center.item())
        feat = torch.cat(feat_loader, 0)
        labels = torch.cat(label_loader, 0)
        '---------------'
        # print(feat.shape)#feat.shape=[60000,2]
        # print(labels.shape)#feat.shape=[60000]
        '-------------------'
        net.visualize(feat.data.cpu().numpy(), labels.data.cpu().numpy(), epoch)
        epoch += 1
        torch.save(net.state_dict(), save_path1)
        # torch.save(net2.state_dict(), save_path2)
        if epoch == 150:
            break

你可能感兴趣的:(深度学习,人脸识别,深度学习,pytorch,神经网络,人工智能)