我们在进行神经网络的学习的时候,我们经常会使用到编码结构和解码结构。我们将数据编码成一个固定维度的向量。那么我们为什么要这样做呢,我们希望编码模块能够将信息中的特征较好的提取出来,并且有很好的区分效果。两个特征即使非常相似也能够进行特征提取。
例如,我们在人脸识别项目中,人脸与人脸的相似度极高,那么神经网络又如何识别出一个人呢?我们不可能用传统的分类思想,因为人类的数量是数以亿计。并且,系统中随时会有人员的增减,那么如何解决人脸识别的问题呢?换句话说,就是我们如何能够使得神经网络提取特征的能力增强呢?
我们希望有一个特征提取器能够将人脸上的所有特征能够非常好的提取出来,即使两张脸有细微的区别也能够识别。下面我们以MNIST手写数字识别数据集作为例子,将28*28的图像编码为包含两个数字的一个向量。
一、Center Loss——以MNIST手写数字识别为例
我们设计一个网络,首先是特征提取器,也可以叫做编码器。将输入的图片最终压缩为一个含有两个元素的向量。压缩后的特征经过解码后,输出为10个类别。根据我们的理解,这个含有两个元素的向量,应该同类别能汇聚到一起,最终将训练集的所有数据编码成十个类别。实验开始:
实现代码如下:
import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
from torchvision.transforms import ToTensor
from torch.utils.data import Dataset, DataLoader
from torchvision.datasets import MNIST
from torch.nn.functional import one_hot
import matplotlib.pyplot as plt
device = "cuda" if torch.cuda.is_available() else "cpu"
dataset = MNIST(root="./dataset", download=True, train=True, transform=ToTensor())
train_loader = DataLoader(dataset, shuffle=True, batch_size=512)
class Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.layers = nn.Sequential(
nn.Linear(28*28, 512),nn.ReLU(),
nn.Linear(512, 256),nn.ReLU(),
nn.Linear(256, 128),nn.ReLU(),
nn.Linear(128, 2)
)
self.out = nn.Sequential(
nn.Linear(2, 10)
)
def forward(self, x):
x = x.reshape(-1, 28*28)
y = self.layers(x)
out = self.out(y)
return y, out
def visualize(feature, label, title):
plt.ion() # 开启交互模式
color = ["#f8d99e", "#c3b5d4", "#a58e61", "#6a7f7a", "#568e88", "#9db92c", "#4a754a","#674668", "#f94d00", "#eee600"]
plt.clf()
for i in range(10):
# plt.plot(feature[label == i, 0], feature[label == i, 1], ".", c=color[i])
plt.scatter(feature[label == i, 0], feature[label == i, 1], c=color[i],alpha=0.6,s=5)
legend = [str(i) for i in range(10)]
plt.legend(legend, loc="upper right")
plt.title(f"epochs : {title}")
plt.savefig("./img/res.jpg")
plt.draw()
plt.pause(0.001)
# plt.ioff()
if __name__ =="__main__":
net = Net().to(device)
net.train()
loss_fn = nn.MSELoss()
optimizer = optim.Adam(net.parameters())
epochs = 0
while epochs < 30:
codes = []
labels = []
for i,(img, label) in enumerate(train_loader):
img = img.to(device)
target = one_hot(label, 10).float().to(device)
code, out = net(img)
loss = loss_fn(target, out)
optimizer.zero_grad()
loss.backward()
optimizer.step()
codes.append(code)
labels.append(label)
if i % 10 == 0:
print(f"epoch {epochs}, batch {i}, loss: {loss.item()}")
codes = torch.cat(codes,0).detach().cpu().numpy()
labels = torch.cat(labels,0).detach().cpu().numpy()
visualize(codes, labels, epochs)
epochs +=1
根据上面的实验,我们可以看出,随着时间的进行,神经网络的能力越来越强,二维特征也逐渐分开。最后,所有的数据被大致的分为十团。十个类别得到了一定的聚集,说明这个编码器是有一定效果的。
但是我们的要求远不止于此,我们希望同类别的特征应该更加汇聚,不同类别的数据不能交叉。那么我们又该如何设计呢?
(1)加深网络层数
我们思考,会不会是模型的深度不够导致的模型提取特征的能力不够呢?那么我们试着加深一下网络模型试试看呢?于是我们接着增加了三层网络:
class Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.layers = nn.Sequential(
nn.Linear(28*28, 512),nn.ReLU(),
nn.Linear(512, 256),nn.ReLU(),
nn.Linear(256, 128),nn.ReLU(),
nn.Linear(128, 64),nn.ReLU(),
nn.Linear(64, 32),nn.ReLU(),
nn.Linear(32, 16),nn.ReLU(),
nn.Linear(16, 2)
)
self.out = nn.Sequential(
nn.Linear(2, 10)
)
def forward(self, x):
x = x.reshape(-1, 28*28)
y = self.layers(x)
out = self.out(y)
return y, out
实验结果如下:
与上图相比,似乎也没有明显的变化呢?那么就说明加深网络层数没办法将特征提取能力增强。那是不是损失函数呢?因为损失函数的反向更新能够指导参数更新,参数是提取特征的关键,那么我们试试修改损失函数。
(2)修改损失函数
于是我们对损失函数进行修改,对于十分类任务,我们还有一个常用的损失函数。就是交叉熵损失函数,那么我们不妨试试看:
代码实现:
import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
from torchvision.transforms import ToTensor
from torch.utils.data import Dataset, DataLoader
from torchvision.datasets import MNIST
from torch.nn.functional import one_hot
import matplotlib.pyplot as plt
device = "cuda" if torch.cuda.is_available() else "cpu"
dataset = MNIST(root="./dataset", download=True, train=True, transform=ToTensor())
train_loader = DataLoader(dataset, shuffle=True, batch_size=512)
class Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.layers = nn.Sequential(
nn.Linear(28*28, 512),nn.ReLU(),
nn.Linear(512, 256),nn.ReLU(),
nn.Linear(256, 128),nn.ReLU(),
nn.Linear(128, 2)
)
self.out = nn.Sequential(
nn.Linear(2, 10)
)
def forward(self, x):
x = x.reshape(-1, 28*28)
y = self.layers(x)
out = self.out(y)
return y, out
def visualize(feature, label, title):
plt.ion() # 开启交互模式
color = ["#f8d99e", "#c3b5d4", "#a58e61", "#6a7f7a", "#568e88", "#9db92c", "#4a754a","#674668", "#f94d00", "#eee600"]
plt.clf()
for i in range(10):
# plt.plot(feature[label == i, 0], feature[label == i, 1], ".", c=color[i])
plt.scatter(feature[label == i, 0], feature[label == i, 1], c=color[i],alpha=0.6,s=5)
legend = [str(i) for i in range(10)]
plt.legend(legend, loc="upper right")
plt.title(f"epochs : {title}")
plt.savefig("./img/res.jpg")
plt.draw()
plt.pause(0.001)
if __name__ =="__main__":
net = Net().to(device)
net.train()
# loss_fn = nn.MSELoss()
loss_fn = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters())
epochs = 0
while epochs < 100:
codes = []
labels = []
for i,(img, label) in enumerate(train_loader):
img = img.to(device)
# target = one_hot(label, 10).float().to(device)
target = label.to(device)
code, out = net(img)
loss = loss_fn( out, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
codes.append(code)
labels.append(label)
if i % 10 == 0:
print(f"epoch {epochs}, batch {i}, loss: {loss.item()}")
codes = torch.cat(codes,0).detach().cpu().numpy()
labels = torch.cat(labels,0).detach().cpu().numpy()
visualize(codes, labels, epochs)
epochs +=1
我们发现每个类别似乎区分的更好了,大部分数据的特征都能够很好的分开,但是在中心点的特征却还是聚集在一起,那有什么办法呢?
我们希望,能够找到每个类别的中心点,并且让每个类别往自己的中心点去靠近就好了。
(3)center loss
于是这就是我们的center loss算法,不仅要让不同类别分开,并且要每个类别的数据往自己的中心点靠拢。
- 减少类内聚、增大类间距
具体的做法就是,在原有的损失函数的基础之上,增加一个距离损失Center Loss,来增强模型的特征提取能力。
k代表k个类别,n代表每个类别的数量,center loss也就是每个类别的平均距离之和。距离公式就用的欧氏距离,计算每个类别中每个点和中心点之间的距离。
代码实现:
import torch
import torchvision
from torch import nn
device = "cuda" if torch.cuda.is_available else "cpu"
# 自定义centerloss损失函数
class CenterLoss(nn.Module):
def __init__(self, class_num, feat_num) -> None:
super().__init__()
self.cls_num = class_num
self.feat_num = feat_num
self.center = nn.Parameter(torch.randn(self.cls_num, self.feat_num)) # 中心点随机产生
def forward(self, x, labels):
center_exp = self.center.index_select(dim=0, index=labels.long()) # [N, 2]
count = torch.histc(labels.float(), bins=self.cls_num, min=0, max=self.cls_num-1) # [10]
count_exp = count.index_select(dim=0, index=labels.long())+1 # [N]
# loss = torch.sum(torch.div(torch.sqrt(torch.sum(torch.pow(x - center_exp,2), dim=1)), count_exp)) # 求损失, 原公式
loss = torch.sum(torch.div(torch.sum(torch.pow(x - center_exp,2), dim=1), 2*count_exp)) # 求损失,略不同
return loss
import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
from torchvision.transforms import ToTensor
from torch.utils.data import Dataset, DataLoader
from torchvision.datasets import MNIST
from torch.nn.functional import one_hot
import matplotlib.pyplot as plt
from Center_Loss import CenterLoss
device = "cuda" if torch.cuda.is_available() else "cpu"
dataset = MNIST(root="./dataset", download=True, train=True, transform=ToTensor())
train_loader = DataLoader(dataset, shuffle=True, batch_size=128)
class Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.convs = nn.Sequential(
nn.Conv2d(1,32,3,2,1,bias=False),
nn.BatchNorm2d(32),
nn.PReLU(),
nn.Conv2d(32,128,3,2,1,bias=False),
nn.BatchNorm2d(128),
nn.PReLU(),
nn.Conv2d(128,128,3,1,1,bias=False),
nn.BatchNorm2d(128),
nn.PReLU(),
nn.Conv2d(128,16,3,2,1,bias=False)
)
self.layers = nn.Sequential(
nn.Linear(16*4*4, 2),
)
self.out = nn.Sequential(
nn.Linear(2, 10),
)
def forward(self, x):
y = self.convs(x)
y = y.reshape(-1, 16*4*4)
y = self.layers(y)
out = self.out(y)
return y, out
def visualize(feature, label, title):
plt.ion() # 开启交互模式
color = ["#f8d99e", "#c3b5d4", "#a58e61", "#6a7f7a", "#568e88", "#9db92c", "#4a754a","#674668", "#f94d00", "#eee600"]
plt.clf()
for i in range(10):
# plt.plot(feature[label == i, 0], feature[label == i, 1], ".", c=color[i])
plt.scatter(feature[label == i, 0], feature[label == i, 1], c=color[i],alpha=0.6,s=5)
legend = [str(i) for i in range(10)]
plt.legend(legend, loc="upper right")
plt.title(f"epochs : {title}")
plt.savefig(f"./img/res_{epochs}.jpg")
plt.draw()
plt.pause(0.001)
if __name__ =="__main__":
net = Net().to(device)
net.train()
# loss_fn = nn.MSELoss()
loss_fn = nn.CrossEntropyLoss()
center_loss_fn = CenterLoss(10, 2).to(device)
optmizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9, weight_decay=0.0005)
scheduler = optim.lr_scheduler.StepLR(optmizer, 10, gamma=0.8)
optmizercenter = optim.SGD(center_loss_fn.parameters(), lr=0.5)
epochs = 0
while epochs < 5000:
codes = []
labels = []
for i,(img, label) in enumerate(train_loader):
img = img.to(device)
# target = one_hot(label, 10).float().to(device)
target = label.to(device)
code, out = net(img)
loss1 = loss_fn( out, target)
loss2 = center_loss_fn(code.to(device), label.to(device))
loss = loss1 + loss2
optmizer.zero_grad()
optmizercenter.zero_grad()
loss.backward()
optmizercenter.step()
optmizer.step()
codes.append(code)
labels.append(label)
if i % 100 == 0:
print(f"epoch {epochs}, batch {i}, loss: {loss.item()}")
scheduler.step()
codes = torch.cat(codes,0).detach().cpu().numpy()
labels = torch.cat(labels,0).detach().cpu().numpy()
visualize(codes, labels, epochs)
epochs +=1
实验结果:
证明了center loss有较好的表现,并且能够将各个特征进行很好的区分。
(3)思考一:
我们这里利用的是欧氏距离来判断两个点是否具有相似性,那么我们其实是项将数据编码成一个向量,我们希望向量具有相似度。然而我们通常使用余弦相似度来衡量两个向量是否相似,那我们可以使用centerloss来作为损失函数吗?
答案是可以的,欧氏距离和余弦相似度在一定条件下能够等价,那就是将所有的向量划分到同一个单位圆中。体现在代码中,就是我们对所有的特征向量都要进行归一化操作,如果我们对向量进行归一化操作之后,我们的结果就会变成这个样子:
我们观察,归一化使得每一个类别变得细长了。那这又是为什么呢?我认为,cross entropy给每一类特征分开一个方向,每个类别在一个方向,然而每一个特征点散布在这个方向的两边波动,然而归一化的作用,就是让离线比较远的点的欧氏距离相对较大,靠近线的点欧式距离短,所以centerloss就表示那些离线比较远的点距离线的距离,那么在反向传播中减小loss,那么也就使得这些点更加集中了,所有的点往中间的线靠近。
(4)思考二:
接下来,我们再次思考,当我们使用了cross entropy之后,我们的特征向量就会分别朝着不同的方向散开。然而cross entropy是Softmax和NLLLoss损失函数的结合。那么到底是哪个因素造成了特征的散开呢?
这个好办,我们将损失函数改为原来的MseLoss,在神经网络的输出后面加上softmax激活函数,看看结果如何?
结果发现,特征散开了!那就说明,真正起作用的是softmax激活函数。那我们得思考,为什么softmax能够造成这样的现象呢?
softmax又叫做指数归一化函数:
softmax相当于是给神经元中的w进行了一个规范,w向量所指向的方向在图中的表现就是那条中心线,也被称之为决策线。w向量的方向固定了使得x向量往w向量的方向靠拢。两个向量越靠拢,角度减小,相似度就会增大,损失就会变小。那么softmax说到底,其实就是在减小w和x之间的角度,那正好验证了上面为什么特征会聚集成一条的线。当然最后不可能拟合成一条线。
二、ArcFace Loss
我们对softmax进行修改,就变成了arcface loss,其公式如下:
代码实现:
import torch
import torchvision
from torch import nn
from torch.nn.functional import normalize
device = "cuda" if torch.cuda.is_available else "cpu"
"""
Arc Face Loss,代替原来的输出层和softmax操作
"""
class Arc(nn.Module):
def __init__(self, feat_num, cls_num) -> None:
super().__init__()
self.w = nn.Parameter(torch.randn((feat_num, cls_num))) # [2, 10]
def forward(self, x, m=1, s=10):
x_norm = normalize(x, p=2, dim=1) # [N, 2]
w_norm = normalize(self.w, p=2, dim=0) # [2, 10]
cosa = torch.matmul(x_norm, w_norm) / s # 防止梯度爆炸 [N, 10]
a = torch.arccos(cosa) # 0-90°,因为除以了十,所以达不到 0-90° [N, 10]
top = torch.exp(s*torch.cos(a+m)) # [N, 10]
# top = torch.exp(s*(torch.cos(a)-m)) # [N, 10]
down = top + torch.sum(torch.exp(s*cosa), dim=1, keepdim=True) - torch.exp(s*cosa)
arc_softmax = top/(down+1e-10) # 加起来不等于1
return arc_softmax
if __name__ == "__main__":
x = torch.randn((3,2))
arc = Arc(2,10)
y = arc(x)
print(y.shape)
import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
from torchvision.transforms import ToTensor
from torch.utils.data import Dataset, DataLoader
from torchvision.datasets import MNIST
from torch.nn.functional import one_hot
import matplotlib.pyplot as plt
from ArcFace_Loss import Arc
device = "cuda" if torch.cuda.is_available() else "cpu"
dataset = MNIST(root="./dataset", download=True, train=True, transform=ToTensor())
train_loader = DataLoader(dataset, shuffle=True, batch_size=512)
class Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.convs = nn.Sequential(
nn.Conv2d(1,32,3,2,1,bias=False),
nn.BatchNorm2d(32),
nn.PReLU(),
nn.Conv2d(32,128,3,2,1,bias=False),
nn.BatchNorm2d(128),
nn.PReLU(),
nn.Conv2d(128,128,3,1,1,bias=False),
nn.BatchNorm2d(128),
nn.PReLU(),
nn.Conv2d(128,16,3,2,1,bias=False)
)
self.layers = nn.Sequential(
nn.Linear(16*4*4, 2),
)
self.out = nn.Sequential(
nn.Linear(2, 10),
)
def forward(self, x):
y = self.convs(x)
y = y.reshape(-1, 16*4*4)
y = self.layers(y)
out = self.out(y)
return y
def visualize(feature, label, title):
plt.ion() # 开启交互模式
color = ["#f8d99e", "#c3b5d4", "#a58e61", "#6a7f7a", "#568e88", "#9db92c", "#4a754a","#674668", "#f94d00", "#eee600"]
plt.clf()
for i in range(10):
# plt.plot(feature[label == i, 0], feature[label == i, 1], ".", c=color[i])
plt.scatter(feature[label == i, 0], feature[label == i, 1], c=color[i],alpha=0.6,s=5)
legend = [str(i) for i in range(10)]
plt.legend(legend, loc="upper right")
plt.title(f"epochs : {title}")
plt.savefig(f"./img/res1_{epochs}.jpg")
plt.draw()
plt.pause(0.001)
if __name__ =="__main__":
net = Net().to(device)
net.train()
loss_fn = nn.NLLLoss(reduction="sum")
arc_loss_fn = Arc(2, 10).to(device)
# optmizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9, weight_decay=0.0005)
optmizer = optim.Adam(net.parameters())
# scheduler = optim.lr_scheduler.StepLR(optmizer, 20, gamma=0.8)
# optmizerarc =optim.SGD(net.parameters(), lr=0.01, momentum=0.9, weight_decay=0.0005)
optmizerarc =optim.Adam(net.parameters())
# arcscheduler = optim.lr_scheduler.StepLR(optmizerarc, 20, gamma=0.8)
epochs = 0
while epochs < 5000:
codes = []
labels = []
for i,(img, label) in enumerate(train_loader):
img = img.to(device)
# target = one_hot(label, 10).float().to(device)
target = label.to(device)
y = net(img)
out = arc_loss_fn(y)
loss = loss_fn(out, target)
optmizer.zero_grad()
optmizerarc.zero_grad()
loss.backward()
optmizerarc.step()
optmizer.step()
codes.append(y)
labels.append(label)
if i % 100 == 0:
print(f"epoch {epochs}, batch {i}, loss: {loss.item()}")
# scheduler.step()
# arcscheduler.step()
codes = torch.cat(codes,0).detach().cpu().numpy()
labels = torch.cat(labels,0).detach().cpu().numpy()
visualize(codes, labels, epochs)
epochs +=1
实验结果如下:
网友评论