长尾图像分类问题

长尾问题介绍

长尾问题是指在现实世界中经常出现的一种分布模式,其中少数类别(“头”)的出现频率很高,而大量其他类别(“尾”)的发生频率很低。绘制时,这种分布类似于长尾形状,尾部代表绝大多数类别,这些类别单独占总数的一小部分,但共同构成数据集的重要部分。
在机器学习的背景下,特别是在与数据分类、推荐系统和自然语言处理相关的任务中,长尾问题提出了重大挑战。出现这个问题的原因是,标准模型往往在数据丰富的分布头部表现良好,但很难准确预测或分类属于数据稀疏的尾部的实例。长尾问题带来了一些挑战。
1.数据不平衡:最直接的挑战是头部和尾部类别之间的不平衡,导致模型偏向头部类别。这种不平衡使得模型很难学习尾部类别的有效表示。
2.泛化:由于尾部类别的数据稀缺,模型往往无法很好地泛化这些类别。这种缺乏通用性的情况会显著影响模型的整体性能,尤其是在覆盖所有类别非常重要的应用程序中。
3.评估指标:传统的评估指标可能无法准确反映模型在长尾分布上的性能。对所有类别给予同等权重的指标,无论其频率如何,都可以提供对头部和尾部类别的模型性能的更平衡的视图
为了解决这些问题,研究人员提出了各种策略,如下所示:
1.重新采样:对尾部类别进行过采样或对头部类别进行欠采样等技术可以帮助缓解数据失衡。这些方法旨在创建一个更平衡的数据集,帮助模型更好地学习尾部类别的表示。
2.数据增强:对于数据有限的尾部类别,通过增强技术生成合成数据可以帮助增加训练示例的多样性和数量。这种方法有助于更好的模型泛化。
3.成本敏感学习:将更高的成本分配给尾部类别的错误分类会鼓励模型更加关注它们。这种方法可以帮助在训练过程中平衡头部和尾部类别之间的注意力。
4.迁移学习和少镜头学习:迁移学习允许模型利用从相关任务或头部类别中学到的知识来提高尾部类别的性能。很少有镜头学习技术是专门为从少量例子中学习而设计的,这使得它们适合尾部类别。
5.集合方法:将来自多个模型的预测相结合,每个模型可能在不同的数据子集上训练或具有不同的目标,可以提高长尾分布的整体性能。
解决长尾问题需要仔细考虑数据分布和应用程序上下文。通过采用其中一种或多种策略,可以构建更稳健的模型,并在整个分布中表现更好,包括头部和长尾。

深度学习方法解决长尾问题

模型

这里我们使用了一个简单的CNN结构和resnet结构去处理长尾分类问题。模型的代码如下:

class SimpleCNN(nn.Module):
    def __init__(self, num_classes=100):
        super(SimpleCNN, self).__init__()
        # 第一个卷积层,输入通道数1或3(取决于图像是灰度的还是彩色的),输出通道数16,卷积核大小3x3
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1)
        # 第二个卷积层,输入通道数16,输出通道数32,卷积核大小3x3
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
        # 最大池化层,使用2x2窗口进行池化
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
        # 第一个全连接层,输入特征数32*8*8(这取决于输入图像的大小和之前层的结构),输出特征数512
        self.fc1 = nn.Linear(32 * 8 * 8, 512)
        # 第二个全连接层,输出最终的分类数
        self.fc2 = nn.Linear(512, num_classes)

    def forward(self, x):
        # 通过卷积层->激活层->池化层
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        # 展平特征图,为全连接层准备
        x = x.view(-1, 32 * 8 * 8)
        # 通过全连接层->激活层
        x = F.relu(self.fc1(x))
        # 通过最后的全连接层得到输出
        x = self.fc2(x)
        return x

device = "cuda:0"
def load_modify_resnet(model_name='resnet18', num_classes=100):
    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-100图像尺寸(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-100的类别数
    num_ftrs = model.fc.in_features
    model.fc = nn.Linear(num_ftrs, num_classes).to(device)

    return model

然后将构建的模型在CIFAR100的长尾数据集上训练。为了创建CIFAR100数据集的长尾版本,我可以通过减少某些类别中的样本数量来手动修改数据集的分布。在这里,我将提供MyLongTailedCIFAR100类的实现,它继承自PyTorch的Dataset类。该实现的核心思想是在加载数据集后,根据预定义的比例减少每个类别的样本数量,从而模拟长尾分布。长尾数据集的创建方式如下:

class MyLongTailedCIFAR100(CIFAR100):
    def __init__(self, root, train=True, transform=None, target_transform=None, download=False, alpha=0.01):
        super(MyLongTailedCIFAR100, self).__init__(root, train=train, transform=transform,
                                                   target_transform=target_transform, download=download)
        if train:  # 只在训练集上应用长尾分布
            self.adjust_to_long_tail(alpha)

    def adjust_to_long_tail(self, alpha):
        num_classes = 100
        # 计算每个类别的样本数,按照长尾分布调整
        class_counts = np.array([len(np.where(np.array(self.targets) == i)[0]) for i in range(100)])
        # 使用浮点数进行幂律分布计算以避免错误
        weights = [class_counts[i] * (alpha ** (i / (num_classes - 1))) for i in range(num_classes)]
        new_targets = []
        new_data = []

        for i in range(100):
            class_indices = np.where(np.array(self.targets) == i)[0]
            np.random.shuffle(class_indices)
            # 按照权重计算每个类别应该保留的样本数
            keep_num = max(1, int(weights[i]))
            new_indices = class_indices[:keep_num]

            new_data.append(self.data[new_indices])
            new_targets.extend([i] * keep_num)

        self.data = np.vstack(new_data)
        self.targets = new_targets

使用长尾CIFAR100数据集,我将可视化原始CIFAR100和长尾版本的数据分布,以更好地理解长尾数据的数据特征。对于两种类型的数据集的数据分布的可视化,关键步骤是获得两种数据集中每个类别的图像数量。下面是可视化代码:

train_set = datasets.CIFAR100(root='data',train=True, download=False, transform=transform)
train_set1 = MyLongTailedCIFAR100(root='data',train=True, download=False, transform=transform)
test_set = datasets.CIFAR100(root='data',train=False, download=False, transform=transform)

num_per_cls = Counter(np.array(train_set.targets))
num_per_cls1 = Counter(np.array(train_set1.targets))

plt.figure(figsize=(10, 6))
keys = range(num_classes)
values = [num_per_cls[k] for k in keys]  # 原始分布
target_values = [num_per_cls1[k] for k in keys]  # 目标(长尾)分布

for k in keys:
    print("class {}, {} samples".format(class_name[k], num_per_cls1[k]))
plt.plot(keys, class_sample_count, color='skyblue', label='Re-sampling data Distribution')
plt.plot(keys, target_values, color='red', label='Target Long-tail Distribution', linewidth=2)
plt.xlabel('Class ID')
plt.ylabel('Number of samples')
plt.title('CIFAR-100 Class Distribution: resample vs Long-tail')
plt.legend()
plt.savefig('long-tail2.png')
plt.show()

在这里插入图片描述
获得CIFAR100的长尾分布数据集之后,我们使用上面构建的模型对该数据集进行训练,训练的代码如下:

net = load_modify_resnet('resnet18', num_classes=100)
net = SimpleCNN(num_classes=100).to(device)
# net = ResNet(pretrained=False).to(device)
criterion = nn.CrossEntropyLoss()
# criterion = FocalLoss()
# criterion = LDAMLoss(target_values)
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
num_epochs = 20

for epoch in range(num_epochs):
    net.train()
    running_loss = 0.0
    for images, labels in train_loader:
        optimizer.zero_grad()
        images, labels = images.to(device), labels.to(device)
        outputs = net(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    print(f"Epoch {epoch+1}/{num_epochs}, Loss: {running_loss/len(train_loader)}")

# 测试模型
net.eval()
correct = 0
total = 0
class_correct = list(0. for i in range(num_classes))
class_total = list(0. for i in range(num_classes))

with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = net(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        c = (predicted == labels).squeeze()

        for i in range(labels.size(0)):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] = 100

    print(f'Accuracy of the model on the 10000 test images: {100 * correct / total} %')
    # for i in range(num_classes):
    #     print('Accuracy of {} : {} %'.format(class_name[i], 100 * class_correct[i] / class_total[i]))
    accuracy_per_class = [100 * class_correct[i] / class_total[i] for i in range(100)]

    fig, ax1 = plt.subplots(figsize=(10, 5))
    color = 'tab:red'
    classes = list(range(100))
    ax1.set_xlabel('class')
    ax1.set_ylabel('Accuracy(%)', color = color)
    ax1.plot(classes, accuracy_per_class, color = color)
    ax1.tick_params(axis='y', labelcolor=color)

    ax2 = ax1.twinx()  # 初始化第二个y轴
    color = 'tab:blue'
    ax2.set_ylabel('Training samples', color=color)
    ax2.bar(classes, target_values , alpha=0.5, color=color)
    ax2.tick_params(axis='y', labelcolor=color)
    plt.savefig('cnn+acc.png', bbox_inches='tight', dpi=300)
    plt.show()

    torch.save(net.state_dict(), 'CNN.pth')

上面的代码除了基本的训练和测试代码之外,还有一个每个类的准确率可视化的代码,可视化的图像如下:
在这里插入图片描述
而且值得注意的是,我在上面的训练代码中定义了多种损失函数比如Dice Loss和Label Distribution Aware Margin (LDAM) Loss。这里我将给出这两个损失函数的具体实现代码:

class FocalLoss(nn.Module):
    def __init__(self, alpha=None, gamma=2, reduction='mean'):
        super(FocalLoss, self).__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.reduction = reduction

    def forward(self, inputs, targets):
        CE_loss = F.cross_entropy(inputs, targets, reduction='none')
        pt = torch.exp(-CE_loss)
        F_loss = (1 - pt) ** self.gamma * CE_loss
        if self.alpha is not None:
            if torch.cuda.is_available():
                self.alpha = self.alpha.cuda()
            at = self.alpha[targets]
            F_loss = at * F_loss

        if self.reduction == 'mean':
            return F_loss.mean()
        elif self.reduction == 'sum':
            return F_loss.sum()
        else:
            return F_loss


class LDAMLoss(nn.Module):
    def __init__(self, cls_num_list, max_m=0.5, s=30):
        super(LDAMLoss, self).__init__()
        m_list = 1.0 / np.sqrt(np.sqrt(cls_num_list))
        m_list = m_list * (max_m / np.max(m_list))
        self.m_list = torch.FloatTensor(m_list).cuda()
        assert s > 0
        self.s = s

    def forward(self, x, target):
        index = torch.zeros_like(x, dtype=torch.uint8)
        index.scatter_(1, target.data.view(-1, 1), 1)

        index_float = index.type(torch.FloatTensor)
        if torch.cuda.is_available():
            index_float = index_float.cuda()
        batch_m = torch.matmul(self.m_list[None, :], index_float.transpose(0, 1))

        batch_m = batch_m.view((-1, 1))
        x_m = x - batch_m

        output = torch.where(index, x_m, x)
        return F.cross_entropy(self.s * output, target)

此外,除了使用更强的损失函数解决长尾分布数据集中的数据不平衡问题,我还使用了其他的方法去解决该问题,比如数据增强,重采样以及对损失函数进行加权,下面我将分别给出这三类策略的实现代码:

# 数据增强
transform = transforms.Compose([
    transforms.RandomResizedCrop(32),  # 随机裁剪到32*32
    transforms.RandomHorizontalFlip(),  # 随机水平翻转
    transforms.RandomRotation(10),
    transforms.ColorJitter(),
    transforms.ToTensor(),
    transforms.Normalize([0.5,0.5,0.5], [0.5,0.5,0.5])
])

# 给损失函数加权
class_counts = torch.zeros(100)
for _, index in train_set1:
    class_counts[index] += 1
# class_weights = 1. / torch.sqrt(class_counts)
class_weights = 1. / class_counts
class_weights = class_weights / class_weights.sum()  # Normalize to sum to 1
# class_weights = (class_weights + 1).cuda()
class_weights = class_weights.cuda()
criterion = nn.CrossEntropyLoss(weight=class_weights)

# 重采样
class_counts = torch.zeros(100)
for _, index in train_set1:
    class_counts[index] += 1
# 计算每个类别的权重
class_weights = 1. / class_counts
class_weights = 1. / torch.sqrt(class_counts)
# 生成每个样本的权重
sample_weights = class_weights[train_set1.targets]

# 创建一个WeightedRandomSampler对象
sampler = WeightedRandomSampler(weights=sample_weights, num_samples=len(sample_weights), replacement=True)
# 注意dataloader里面使用了sampler就不能使用shffule
train_loader = DataLoader(train_set1, batch_size=64, sampler=sampler)
  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值