Bootstrap

开集图像识别(open-set recognition)

开集识别介绍

开放集识别:开放集识别通过考虑模型在测试过程中可能遇到以前看不见或未知类的场景,扩展了传统的图像识别范式。与闭集识别不同,闭集识别在固定的类集上训练和测试模型,开集识别解决了模型需要区分已知类和未知类的更现实的情况。
在开集识别中,模型不仅必须将图像分类到预定义的类中,而且能够从训练集中不存在的类中检测实例。开集识别的常见方法包括测量预测的不确定性,例如使用熵或softmax分数来识别模型缺乏信心或遇到不熟悉模式的情况。
开集识别的评估通常包括绘制受试者工作特征(ROC)曲线和计算曲线下面积(AUC)等指标。这些指标深入了解了模型区分已知类和未知类的能力,为不可避免地存在未知类的现实世界应用程序提供了有价值的信息。
总之,虽然图像识别侧重于对已知类别中的对象进行分类,但开集识别将这一能力扩展到处理模型遇到以前看不见或未定义的类的情况,使其在动态环境中更加稳健和适用。

用深度学习方法实现开集识别

数据集

这里我将CIFAR10数据集作为闭集,将Fashion-MNIST数据集作为开集。我还会对这两个数据集可视化以便于观察他们的图像特点。首先,我将给出两个数据集的加载方式:

train_data = torchvision.datasets.CIFAR10("./dataset", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
test_data = torchvision.datasets.CIFAR10("./dataset", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
train_loader = DataLoader(dataset=train_data, batch_size=64, shuffle=True, num_workers=0)
test_loader = DataLoader(dataset=test_data, batch_size=64, shuffle=False, num_workers=0)

transform = transforms.Compose([
    transforms.Resize((32, 32)),  # 调整图像大小
    transforms.ToTensor(),  # 转换为张量
])

train_fashion_data = torchvision.datasets.FashionMNIST("./dataset", train=True, transform=transform,
                                               download=True)
test_fashion_data = torchvision.datasets.FashionMNIST("./dataset", train=True, transform=transform,
                                                download=True)
fashion_dataset = ConcatDataset([train_fashion_data, test_fashion_data])
fashion_loader = DataLoader(dataset=fashion_dataset, batch_size=64, num_workers=0, drop_last=True)

可以看到这里我使用了ConcatDataset类去加载Fashion-MNIST数据集,因为我将其作为开集,所以我将该数据集的训练集和测试集的图片合并为一个整体数据集。然后这两个数据集的可视化代码如下:

class_names = train_data.classes
print(class_names)

def imshow(inp, title=None):
    """Display image for Tensor."""
    inp = inp.numpy().transpose((1, 2, 0))
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.pause(0.001)  # pause a bit so that plots are updated

def visualize():
    # Get a batch of training data
    inputs, classes = next(iter(train_loader))

    # Make a grid from batch
    out = torchvision.utils.make_grid(inputs)

    imshow(out, title=[class_names[x] for x in classes])
    plt.savefig("cifar10.jpg")

在这里插入图片描述在这里插入图片描述

模型

这里我使用了两种不同的简单的神经网络模型,MLP和resnet18。以上两个模型的实现代码如下:

num_i = 3 * 32 * 32  # 输入层节点数
num_h = 128  # 隐含层节点数
num_o = 10  # 输出层节点数
batch_size = 64
N_CLASSES = 10

class MLP(torch.nn.Module):

    def __init__(self, num_i, num_h, num_o):
        super(MLP, self).__init__()

        self.linear1 = torch.nn.Linear(num_i, num_h)
        self.relu = torch.nn.ReLU()
        self.linear2 = torch.nn.Linear(num_h, num_h)  # 2个隐层
        self.relu2 = torch.nn.ReLU()
        self.linear3 = torch.nn.Linear(num_h, num_o)

    def forward(self, x):
        x = x.view(-1, 3*32*32)
        x = self.linear1(x)
        x = self.relu(x)
        x = self.linear2(x)
        x = self.relu2(x)
        x = self.linear3(x)
        return x
        
def load_modify_resnet(model_name='resnet18', num_classes=10):
    if model_name == 'resnet18':
        model = models.resnet18(pretrained=False).to(device)
    elif model_name == 'resnet50':
        model = models.resnet50(pretrained=False).to(device)
    else:
        raise ValueError("Unsupported model name")

    # 修改第一层以适应CIFAR-10图像尺寸(3x32x32)
    model.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False).to(device)
    model.maxpool = nn.Identity().to(device)  # CIFAR-100图像太小,不需要最大池化

    # 修改最后的全连接层以匹配CIFAR-10的类别数
    num_ftrs = model.fc.in_features
    model.fc = nn.Linear(num_ftrs, num_classes).to(device)

    return model

实现完模型之后,接下来我将给出模型的训练和测试代码:

num_epochs = 30  # 50轮
learning_rate = 0.01  # 学习率0.01
threshold = 0.5
wandb.init(project='homework2',
            name='name')

model = MLP(num_i,num_h,num_o).to(device)

model = load_modify_resnet('resnet18', num_classes=10)
# 损失函数
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

# 更新学习率
def update_lr(optimizer, lr):
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr

def get_confidence(output):
    return torch.max(output, 1)[0]


def train():
    # 训练数据集
    total_step = len(train_loader)
    curr_lr = learning_rate

    for epoch in range(num_epochs):
        for i, (images, labels) in enumerate(train_loader):
            images = images.to(device)
            labels = labels.to(device)

            # Forward pass
            outputs = model(images)
            loss = criterion(outputs, labels)
            # Backward and optimize
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            if (i + 1) % 100 == 0:
                print("Epoch [{}/{}], Step [{}/{}] Loss: {:.4f}"
                        .format(epoch + 1, num_epochs, i + 1, total_step, loss.item()))
            wandb.log({'loss': loss, 'curr_lr': curr_lr, 'epoch': epoch})
            # 延迟学习率
        if (epoch + 1) % 20 == 0:
            curr_lr /= 3
            update_lr(optimizer, curr_lr)

    # 测试网络模型
    model.eval()
    with torch.no_grad():
        correct = 0
        total = 0
        class_correct = list(0. for i in range(N_CLASSES))
        class_total = list(0. for i in range(N_CLASSES))
        for images, labels in test_loader:
            images = images.to(device)
            labels = labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

            c = (predicted == labels).squeeze()

            for i in range(len(labels)):
                _label = labels[i]
                class_correct[_label] += c[i].item()
                class_total[_label] += 1
        #
        print('Accuracy of the model on the test images of cifar10: {} %'.format(100 * correct / total))
        for i in range(N_CLASSES):
            print('Accuracy of {} : {} %'.format(
                class_names[i], 100 * class_correct[i] / class_total[i]))
    # S将模型保存
    torch.save(model.state_dict(), 'resnet.pth')
    wandb.finish()

上述代码只是简单地用模型在CIFAR10上面训练,并且给出每个类地准确率。这里我还使用了wandb去记录损失函数的变化,wandb是一个非常好的记录代码种各种变量变化的工具(推荐大家去使用),使用前只需要在wandb里面创建一个project,然后将project的名字并且加一个当前训练的名字就可以初始化了,具体初始化代码参考上面,这里我提供了一个最简单的初始化代码,只包含了project和name两个参数,下图是我用wandb记录的多次训练的loss函数的变化。
在这里插入图片描述
接下来,我将进入开集识别的流程,代码如下:

import torch
import torch.nn as nn
import torch.optim as optim
from torch.hub import load_state_dict_from_url

import wandb
from torch.optim import lr_scheduler
import torch.backends.cudnn as cudnn
import numpy as np
import torchvision
from torch.utils.data import DataLoader
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
from torch.nn import functional as F, CrossEntropyLoss
import time
import os
from torch.utils.data import ConcatDataset

model = models.resnet18(pretrained=True)
model.eval()
cifar_outputs = []
fashion_outputs = []
    
# 关闭梯度计算,节省内存
with torch.no_grad():
    # 获取CIFAR-10测试集的预测分数

    for images, labels in test_loader:
        predictions = model(images)
        cifar_outputs.append(predictions)

cifar_outputs = torch.cat(cifar_outputs)
# cifar_scores = F.softmax(cifar_outputs, dim=1)
# cifar_scores = torch.max(cifar_outputs, dim=1).values.cpu().detach().numpy()  # 最大softmax分数
# cifar_scores = cifar_outputs.max(1)[0]  # 最大logits分数
cifar_scores = -(cifar_outputs.softmax(-1) * cifar_outputs.log_softmax(-1)).sum(1)  # 熵

model.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3,
                               bias=False)
# 获取Fashion-MNIST测试集的预测分数
with torch.no_grad():
    for images, labels in fashion_loader:
        fashion_outputs.append(model(images))

fashion_outputs = torch.cat(fashion_outputs)
# fashion_scores = F.softmax(fashion_outputs, dim=1)
# fashion_scores = torch.max(fashion_outputs, dim=1).values.cpu().detach().numpy()  # 最大softmax分数
# fashion_scores = fashion_outputs.max(1)[0]  # 最大logits分数
fashion_scores = -(fashion_outputs.softmax(-1) * fashion_outputs.log_softmax(-1)).sum(1)  # 熵

# 将真实标签组合起来
cifar_labels = np.ones(len(cifar_scores))  # CIFAR-10作为正类,标签为1
fashion_labels = np.zeros(len(fashion_scores))  # Fashion-MNIST作为负类,标签为0

all_scores = np.concatenate([cifar_scores, fashion_scores])
all_labels = np.concatenate([cifar_labels, fashion_labels])

fpr, tpr, _ = roc_curve(all_labels, all_scores)
roc_auc = auc(fpr, tpr)

plt.figure()
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (area = {:.2f})'.format(roc_auc))
plt.plot([0,1], [0,1],color='navy', lw=2, linestyle='--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc='lower right')
plt.savefig('ROC2.jpg')
plt.show()

为了测试方便,我直接使用resnet18的预训练模型进行开集识别测试。然后,我使用三种开集识别的评估方式,比如最大softmax分数,最大logits分数和熵。然后,我们将两个数据集的标签整合起来,CIFAR10作为已知的数据集(开集),所以将CIFAR10的标签都初始化为1,反之,Fashion-MNIST数据集作为闭集,将它的标签初始化为0,然后将两个的标签合并。同时也将我们获得两个score分数进行合并。合并之后,我使用两者汇出开集识别的ROC回归曲线。可视化代码也在上面给出来了,下面我将给出以上三种评估方法得出的ROC回归曲线的可视化结果,以下三个图像分别对应最大softmax分数,最大logits分数和熵:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

;