前言
在深度学习中,分类任务是最常见的任务类型之一,几乎涉及到所有的图像识别、文本分类、情感分析、语音识别、目标检测等应用场景中,在本篇文章中,我将带领各位小伙伴们系统理解分类任务的定义,以及神经网络中常用的分类任务损失函数的原理及使用场景。
一、什么是分类任务?
在深度学习中,分类任务是一类非常常见的问题,其目标就是让模型根据输入的特征,判断该输入是属于哪个“类别”。
简单理解就是:分类任务的本质就是“判断标签”,就像我们用肉眼看图识物一样,比如我们看到一只动物,我们就会自动判断它是属于“猫”或者是“狗”。
总结来说,分类任务就是:
- 将输入样本映射到一个或多个预定义类别中的过程
分类任务的常见类型:
任务类型 | 特点 | 例子 |
二分类 | 仅有两个类别 | 邮件是否为垃圾邮件 |
多分类 | 多个互斥类别,每个样本仅属于一类 | 数字识别 |
多标签分类 | 每个样本可同时属于多个类别 | 一张图中既有“人”又有“车” |
二、分类任务的数学表达
在分类任务中,有一套约定俗成的数学表达形式:
输入特征:
标签:
,其中
是类别数
模型输出
:类别的概率分布(通常是经过
之后得到)
分类模型的训练关键就是:通过损失函数衡量预测概率与真实标签的差距,然后反向传播优化。
三、常用分类任务损失函数
接下来,我将介绍几种常用的分类任务损失函数,每种都有其应用场景及特点。
1. 0-1损失
0-1 损失函数是分类任务中最直观的一种损失函数,它只关心预测是否准确:
举个例子小伙伴们就懂了:
真是标签 | 预测标签 | 损失 |
1 | 1 | 0 |
2 | 1 | 1 |
通过这个例子我们可以知道,0-1损失更像是一种对于“是否分类成功的统计”,类似于准确率,而不是连续的损失值。
0-1损失具有以下特点:
- 直观:判断是否预测正确即可
- 与准确率相关:它与最终的分类准确率对应
- 不可导:0-1损失是不连续函数,无法求导,无法用于梯度下降优化模型
下面,我手写一下0-1损失函数的伪代码:
pred = torch.argmax(outputs, dim=1)
0_1_loss = (pred != labels).float().mean().item()
print(f'0-1 Loss:{0_1_loss:.4f}')
2. 二分类的BCELoss(Binary Cross Entropy)
BCELoss损失常用于二分类任务,是衡量模型预测概率与真实标签之前差异的一种方法:
其中, 是模型预测的概率,通常使用Sigmoid激活函数将输出映射到[0,1]区间
使用场景如下:
任务类型 | 是否适用 |
二分类 | 是 |
多标签分类 | 是(每个标签看成一个二分类) |
多分类 | 否 |
BCELoss与BCEWithLogitsLoss的区别:
函数名 | 输入要求 | 是否自动Sigmoid | 稳定性 |
BCELoss | 概率值(0~1) | 需要自己加Sigmoid | 较差 |
BCEWithLogitsLoss | 原始输出logitis | 内容自动加Sigmoid | 较稳定 |
接下来,我简单实现一下BCELoss:
import torch
import torch.nn as nn
labels = torch.tensor([1.0, 0.0, 1.0])
pred = torch.tensor([0.9, 0.1, 0.4]) # 模型输出的概率,经过Sigmoid处理过的
loss_fn = nn.BCELoss()
loss = loss_fn(pred, labels)
print(f'BCELoss:{loss.item():.4f}')
3. 交叉熵损失(Cross Entropy Loss)
交叉熵的由来,我在本系列的前面的文章里已经详细的解释了它的由来和原理,在这里我只简单的回归一下:交叉熵是衡量两个概率分差异的一种方法,广泛应用在多分类问题中。其公式如下:
可能小伙伴们看到上面的式子还是不知道其在网络中是如何计算损失的,那么我来举个例子各位就明白了。假设我们要对一个样本进行猫、狗、鸟三分类识别:
- 类别数:3(猫、狗、鸟)
- 该样本的真实标签:狗,对应的one-hot为[0, 1, 0]
- 模型预测结果(Softmax输出)为[0.2,0.7,0.1]
- 则该损失为:
下面,我将简单的实现以下交叉熵的应用,需要主要的是,pytroch中内置的交叉熵函数内部会自动执行log(Softmax(logits)),因此无需手动添加Softmax:
import torch
import torch.nn as nn
outputs = torch.tensor([[2.0, 1.0, 0.1]])
labels = torch.tensor([0]) # 标签为整数索引,索引0是正确的类别
loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(outputs, labels)
print(f'Loss:{loss.item():.4f}')
4. Focal Loss(用于类别不平衡)
Focal Loss 是为了解决类别不平衡问题而提出的一种改进的分类损失函数。其实它的原理很简单,只是在交叉熵损失函数的基础上添加了一个调节因子,用来减少简单样本对总损失的影响,聚焦于难分类样本的学习。这个调节因子设置的初心就是降低简单样本的权重,相当于增加了困难样本的权重。公式如下:
其中:
,表示预测为真是标签的概率
,用于平衡正负样本的权重
,聚焦因子,控制“容易分类样本”的抑制程度,通常为2
,调节因子,当样本预测准确率为
,整个项趋近于0,及对容易分类的样本惩罚更小
可能光看上面的公式很难理解,那么我们举个列子来看一下:假设我们要做一个三分类问题,类别0,1,2:
- 真实标签
:对应的one-hot表示为[0,1,0]
- 参数设定为:
假设第一次模型预测概率为:[0.5,0.3,0.2],那么对应的真实样本的预测概率为0.3,显然预测的非常不准,是困难样本
- 那么交叉熵损失为:
- Focal Loss为:
假设第二次模型预测概率为:[0.1,0.8,0.1],那么对应的真实样本的预测概率为0.8,显然预测的比较准确,是简单样本
- 那么交叉熵损失为:
- Focal Loss为:
通过这个例子说明,对比交叉熵损失来说,使用Focal Loss后,简单样本的损失缩小了近100倍,困难样本的权重缩小了两倍左右,也就是降低了简单样本的权重,增加了困难样本的权重。即,当模型预测正确(0.8)时,Focal Loss 大幅降低对该样本的惩罚,从而更加关注那些预测困难(0.3)的样本。
好了,相信小伙伴们通过对上面例子的理解,已经明白了Focal Loss对样本权重控制的原理:即降低简单样本权重,增加困难样本权重。接下来,我将简答实现以下Focal Loss函数:
def focal_loss(preds, targets, alpha=0.25, gamma=2.0, eps=1e-9):
preds = troch.clamp(preds, eps, 1.-eps)
a_loss = -(target * torch.log(preds) + (1 - targets) * torch.log(1 - preds))
b_loss = (1 - preds) ** gamma
loss = alpha * b_loss * a_loss
return loss.mean()
四、分类任务简答流程实现
接下来,我将以经典的数字识别任务为例,构建一个简单的卷积神经网络,并使用交叉熵作为损失函数,来完成分类任务的搭建:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# 数据预处理
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,),(0.5,))
])
# 使用MNIST数据集
train_dataset = torchvision.dataset.MNIST(root='./data', train=True, download=True, transform=transforms)
test_dataset = torchvision.dataset.MNIST(root='./data', train=False, download=True, transform=transforms)
train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=False)
# 构建CNN
class MyCNN(nn.Module):
def __init__(self, ):
super(MyCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, Padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(16 * 14 * 14 * 14, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.pool(x)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
return x
# Training
device = 'cuda' if torch.cuda.is_aviailable() else 'cpu'
model = MyCNN().to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for epoch in range(100):
model.train()
total_loss = 0.0
for image, lable in train_dataloader:
image, label = image.to(device), label.to(device)
outputs = model(image)
loss = loss_fn(outputs, label)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f'epoch:{epoch} | Loss:{total_loss/len(train_dataloader):.4f}')
# test
model.eval()
correct = 0
total = 0
with torch.no_grad():
for image, label in test_dataloader:
image, label = image.to(device), label.to(device)
outputs = model(image)
_, pred = torch.argmax(outputs, dim=1)
total += label.size(0)
correct += (pred == label).sum().item()
print(f'test accuracy:{100 * correct/totalL.3f}%')
总结
以上就是本文的全部内容,相信小伙伴们读到这里已经过分类任务损失函数有了更全面、深刻的理解与掌握:分类任务是神经网络的核心任务之一,损失函数的选择直接决定模型的学习效率和泛化能力,在具体的应用场景中,小伙伴们应当结合具体的数据特性与训练要求来选择。
🧡 如果小伙伴们觉得本文有帮助,欢迎点赞👍、收藏⭐、关注🔔本专栏「从认识AI开始」,帮我将持续在本专栏中跟新人工智能知识,帮助各位小伙伴们打好扎实的理论知识与操作基础。欢迎订阅本专栏,系统掌握深度学习基础知识!