Deep Clustering for Unsupervised Learning of Visual Features

Xiaohang Zhan∗ 1 , Jiahao Xie∗2 , Ziwei Liu1 , Yew Soon Ong2,3, Chen Change Loy2 1CUHK - SenseTime Joint Lab, The Chinese University of Hong Kong 2Nanyang Technological University 3AI3, A*STAR, Singapore

1 {zx017, zwliu}@ie.cuhk.edu.hk 2 {jiahao003, asysong, ccloy}@ntu.edu.sg

摘要

​  联合聚类和特征学习方法在无监督表示学习中表现出了显著的性能。然而,在特征聚类和网络参数更新之间交替的训练计划会导致视觉表示的学习不稳定。为了克服这一挑战,我们提出了在线深度聚类(ODC),它同时执行聚类和网络更新,而不是交替执行。我们的关键观点是,聚类质心应该稳定地进化,以保持分类器的稳定更新。具体来说,我们设计并维护了两个动态内存模块,即用于存储样本标签和特征的样本内存,以及用于质心进化的质心内存。我们将突然的全局聚类分解为稳定的内存更新和批量标签重新分配。该过程被集成到网络更新迭代中。通过这种方式,标签和网络并肩发展,而不是交替发展。大量实验表明,ODC稳定了训练过程,有效地提高了性能。

1.介绍

​  无监督表示学习[1,2,3,4,5,6,7,8,9]的目的是学习可转移的图像或视频表示,而无需手动注释。其中,基于聚类的表示学习方法[10,11,12,13,14]成为这一领域的一个很有前途的方向。与基于恢复的方法[2,3,4,8]不同,基于聚类的方法只需要很少的领域知识[13],同时获得了令人鼓舞的性能。与仅捕获图像内不变性的对比性表示学习[15,16,17]相比,基于聚类的方法能够探索图像间的相似性。与传统的通常在固定特征[18,19]上执行的聚类不同,这些工作共同优化聚类和特征学习。
​  虽然早期工作[11,12]的评估大多是在小数据集上进行的,但Caron等人提出的Deep Clustering[13](DC)是首次尝试扩大基于聚类的表示学习。DC在深度特征聚类和CNN参数更新之间交替进行。特别是,在每个历元开始时,它对整个数据集执行离线聚类算法,以获得伪标签作为下一个历元的监督。离线聚类不可避免地会在不同的时代排列所分配的标签,即即使某些聚类没有变化,聚类后的索引也会随机排列。因此,分类器中的参数不能从上一个历元继承,它们必须在每个历元之前进行随机初始化。该机制引入了训练的不稳定性,并使表象暴露在表象腐败的高风险之中。如图1 (a)所示,DC中的网络更新在每个时期都被特征提取和聚类中断。这与传统的监督分类不同,后者使用固定的标签,其迭代由网络的前向和向后传播组成。

image-20250217152428929
image-20250217152524157

​  在这项工作中,我们寻求设计一个联合聚类和高稳定性的特征学习范式。为了减少DC和监督学习之间的训练机制的差异,我们将聚类过程分解为小批量的标签更新,并将该更新过程集成到网络更新的迭代中。基于这种直觉,我们提出了在线深度聚类(ODC,Online Deep Clustering)的联合聚类和特征学习。具体来说,ODC迭代包括正向和向后传播、标签重新分配和质心更新。对于标签更新,ODC在前向传播中重用了这些特征,从而避免了额外的特征提取。为了方便在线标签重新分配和质心更新,我们设计并维护了两个动态内存模块,即样本存储以存储样本的标签和特征,质心内存用于质心进化。通过这种方式,ODC以一种类似于监督分类的不间断方式进行训练,而不需要手动注释。在训练过程中,标签和网络参数是肩并肩的,而不是交替的。由于标签在每次迭代中都不断即时更新,CNN中的分类器也更加稳定,从而产生更稳定的损失曲线,如图1 (b)所示。

image-20250217153059962
image-20250217153117089
image-20250217153130638

​  虽然ODC单独在各种基准测试上实现了引人注目的无监督表示学习性能,但它可以自然地用于对使用其他无监督学习方法训练过的模型进行微调。大量的实验表明,ODC的稳定性帮助它作为一个无监督的微调工具,执行优于DC。我们总结出了我们的贡献如下:

  1. 我们提出了ODC,以一种无监督的方式学习图像表示与高稳定性。
    2. ODC也作为一个统一的无监督微调方案,进一步改进了以前的自监督表示学习方法。
    3. 在不同的基准上观察到良好的性能,表明联合聚类和特征学习的巨大潜力。

2.相关工作

3.方法论

​  在下面的小节中,我们首先在3.1节讨论在秒中提出的ODC与传统的DC [13]之间的区别。然后,在3.2节,我们推荐一些有用的策略在使用ODC时保持稳定的集群大小。最后,我们解释如何使用ODC进行无监督微调(在3.3节)和ODC的实现细节(在3.4节)。

3.1.在线深度聚类

​  我们首先讨论了DC[13]的基本思想,然后详细介绍了所提出的ODC。为了学习表示方法,DC在离线特征聚类和使用伪标签的网络反向传播之间交替进行。离线聚类过程需要对整个训练集进行深度特征提取,然后采用全局聚类算法,如K-Means聚类。全局聚类使伪标签的排列幅度较大,要求网络在随后的时代快速适应新的标签。

3.1.1.框架概述

​  与DC不同,ODC不需要额外的特征提取过程。此外,标签也会随着网络参数的更新而平稳地演化。这是由于新引入的采样和质心记忆而实现的。

image-20250217154520065

​  如图2所示,样本存储器存储了整个数据集的特征和伪标签,而质心存储器存储了类质心的特征,即一个类中所有样本的平均特征。这里的“类”表示在训练过程中不断发展的临时集群。在ODC的不间断迭代期间,标签和网络参数同时更新。引入了特定的技术,包括损失重新加权和处理小集群,以避免ODC陷入琐碎的解决方案。

3.1.2.ODC迭代

​  假设我们有一个随机初始化的网络\(f_{\theta}\left(*\right)\)和一个线性分类器\(g_{w}\left(*\right)\),其目标是训练主干参数θ来产生高度判别的表示。为了准备ODC,样本和质心记忆通过一个全局聚类过程进行初始化,例如,K-Means。接下来,我们可以迭代地执行不间断的ODC。
​  一个ODC迭代包含四个步骤。首先,给定一批输入图像{x},网络将图像映射到紧凑的特征向量\(F=f_{\theta}(x)\)中。其次,我们从样本内存中读取这批处理的伪标签。利用伪标签,我们用随机梯度下降法更新网络,以解决以下问题: \[\operatorname*{min}_{\theta,w}\frac{1}{B}\sum_{n=1}^{B}l\left(g_{w}\left(f_{\theta}\left(x_{n}\right)\right),y_{n}\right),\] ​  其中yn是样本内存中的当前伪标签,B表示每个小批的大小。第三,重用L2归一化后的\(f_{\theta}(x)\)来更新样本内存: \[F_{m}\left(x\right)\leftarrow m\frac{f_{\theta}\left(x\right)}{\left|\left|f_{\theta}\left(x\right)\right|\right|_{2}}+\left(1-m\right)F_{m}\left(x\right),\] ​  其中Fm (x)为样本内存中x在存储器中的特征,m∈(0,1]为动量系数。同时,通过找到最近的质心,为每个涉及的样本分配一个新的标签: \[\operatorname*{min}_{y\in\left\{1,..,C\right\}}\|F_{m}\left(x\right)-C_{y}\|_{2}^{2}\] ​  其中,Cy表示y类的质心特征。最后,记录所涉及的中心,包括新成员加入的中心和旧成员离开的中心。通过平均属于相应质心的所有样本的特征,每k次迭代都更新。

3.2.处理ODC中的集群分布

3.2.1.损失重新加权

​  为了避免训练崩溃成几个巨大的簇,DC在每个纪元之前采用均匀采样。然而,对于ODC,集群上的样本数量在每次迭代中都会发生变化。使用统一采样需要在每次迭代中重新采样整个数据集,这一过程被认为是冗余的和昂贵的。我们提出了另一种方法,即根据每个类中的样本数量来重新加权损失。为了验证它们的等价性,我们实现了一个具有损失重新加权的直流模型,并通过经验发现,当权重遵循\(w_{c}\ \propto\ {\frac{1}{\sqrt{N_{c}}}}\)时,性能保持不变,其中Nc表示第c类中的样本数量。因此,我们对ODC采用相同的损失重加权公式。随着损失重加权,小簇中的样本对反向传播的贡献更大,从而推动决策边界进一步接受更多的潜在样本。

3.2.2.处理小集群

​  损失重新加权有助于防止巨大的簇的形成。然而,我们仍然面临着有一些小集群崩溃成空集群的风险。为了克服这个问题,我们建议在非常小的集群崩溃之前预先处理和消除它们。将正常簇表示大小大于阈值的Cn,小簇表示大小不大于的C,对于∈,我们首先将c中的样本分配给Cn中最近的质心,使c为空。接下来,我们用k-均值将最大的簇cmax∈Cn分成两个子簇,并随机选择其中一个子簇作为新的c。我们重复这个过程,直到所有的集群都属于Cn。尽管这个过程突然改变了一些集群,但这只影响参与这一过程的一小部分样品。

3.2.3.尺寸减少

​  一些主干网络将图像映射到高维向量,例如,AlexNet产生4096维特征,ResNet- 50产生2048维特征,导致后续聚类中较高的空间和时间复杂性。DC对整个数据集的特征进行了主成分分析,以减少维度。然而,对于ODC,不同样本的特征有不同的时间戳,导致样本之间的统计数据不兼容。因此,PCA已不再适用。在每次迭代中执行PCA的成本也很高。因此,我们添加了一个非线性的头层,以将高维特征减少到256维。它是在ODC迭代期间联合调优的。为下游任务删除头部层。

3.3.ODC用于无监督的微调

​  与倾向于捕获图像内语义的自监督学习方法相比,基于聚类的方法更多地关注图像间的信息。因此,DC和ODC自然地是对以前的自我监督学习方法的补充。由于DC和ODC并不局限于一个专门设计的目标,如旋转角度或颜色预测,它们很容易作为一种无监督的微调方案来提高现有的自监督方法的性能。在本文中,我们研究了DC和ODC作为一个从不同的自监督学习方法中初始化的微调过程的有效性。

3.4.实施细节

​  数据预处理。
​  我们使用ImageNet,它包含128万张没有标签的图像进行训练。图像首先被随机裁剪,使其分辨率为224x224,其增强功能包括随机翻转和旋转(±2◦)。DC对图像采用Sobel滤波器,避免利用颜色作为快捷方式。这样的预处理步骤要求下游任务也包括Sobel层,这可能会限制其应用程序。我们发现,强烈的颜色抖动显示出与Sobel过滤器相同的效果,而它允许正常的RGB图像作为输入。具体来说,我们采用PyTorch风格的颜色抖动变换,包括亮度因子(0.6、1.4)、对比度系数(0.6、1.4)、饱和度因子(0、2)和色调因子(−0.5、0.5)。此外,我们将图像随机转换为灰度,概率为0.2。应用于训练样本的随机颜色抖动和灰度随机化了以颜色测量的相似度。这阻碍了网络利用颜色中的琐碎信息。

​  ODC的训练。
​  我们使用ResNet-50作为骨干。考虑到大多数早期的作品都使用了AlexNet,我们也在AlexNet上进行了实验以进行比较。在[13]之后,我们使用不需要本地响应规范化的AlexNet架构,并添加批处理规范化层。AlexNet和ResNet-50的ODC模型是从零开始训练的。批处理大小是512个,分配给8个gpu。400个时代,AlexNet的学习率一直为0.01,ResNet-50为0.03,80个时代的学习率衰减0.1。在DC之后,集群的数量设置为10000,是ImageNet注释的类数的10倍。动量系数m设为0.5。识别小集群的阈值设置为20。改变这个阈值不会显著影响结果,前提是它不超过集群中样本的平均数量。质心存储器每10次迭代更新一次。质心更新频率构成了学习效率和学习效率之间的权衡。在我们的实验中,我们观察到,只要频率被限制在一个合理的范围内,ODC的性能就对它不敏感。

4.实验

5.结论

​  我们提出了一种有效的联合聚类和特征学习范式的无监督表示学习。该方法采用在线深度聚类(ODC),通过对特征聚类进行分解并将过程整合到网络更新迭代中,实现了有效稳定的深度神经网络无监督训练。ODC单独作为一种无监督的表示学习方案,表现得令人信服。它还可以用于微调和大大改进以前的自监督学习方法。

6.代码

​  其代码整合在https://github.com/open-mmlab/mmselfsup/tree/main/configs/selfsup/odc。

6.1.配置代码

_base_ = [
'../_base_/models/odc.py',
'../_base_/datasets/imagenet_odc.py',
'../_base_/schedules/sgd_steplr-200e_in1k.py',
'../_base_/default_runtime.py',
]

# model settings
model = dict(
head=dict(num_classes={{_base_.num_classes}}),
memory_bank=dict(num_classes={{_base_.num_classes}}),
)

# optimizer
optimizer = dict(type='SGD', lr=0.06, weight_decay=1e-5, momentum=0.9)
optim_wrapper = dict(
type='OptimWrapper',
optimizer=optimizer,
paramwise_cfg=dict(custom_keys={'head': dict(momentum=0.)}))

# learning rate scheduler
param_scheduler = [
dict(type='MultiStepLR', by_epoch=True, milestones=[400], gamma=0.4)
]

# runtime settings
train_cfg = dict(max_epochs=440)
# the max_keep_ckpts controls the max number of ckpt file in your work_dirs
# if it is 3, when CheckpointHook (in mmcv) saves the 4th ckpt
# it will remove the oldest one to keep the number of total ckpts as 3
default_hooks = dict(
checkpoint=dict(type='CheckpointHook', interval=10, max_keep_ckpts=3))

6.2.模型配置

6.2.1.总览

odc.py如下:

# model settings
model = dict(
type='ODC',
data_preprocessor=dict(
mean=(123.675, 116.28, 103.53),
std=(58.395, 57.12, 57.375),
bgr_to_rgb=True),
backbone=dict(
type='ResNet',
depth=50,
in_channels=3,
out_indices=[4], # 0: conv-1, x: stage-x
norm_cfg=dict(type='SyncBN')),
neck=dict(
type='ODCNeck',
in_channels=2048,
hid_channels=512,
out_channels=256,
with_avg_pool=True),
head=dict(
type='ClsHead',
loss=dict(type='mmcls.CrossEntropyLoss'),
with_avg_pool=False,
in_channels=256,
num_classes=10000),
memory_bank=dict(
type='ODCMemory',
length=1281167,
feat_dim=256,
momentum=0.5,
num_classes=10000,
min_cluster=20,
debug=False))

6.2.2.backbone

​  模型的backbone为ResNet,其代码在https://github.com/open-mmlab/mmselfsup/blob/db861f30487c9a0b0cb3e35e176d6058887660bc/mmselfsup/models/backbones/resnet.py。

​  模型的输入通道为3,输出通道为2048。

6.2.2.neck网络

​  模型的backbone为odc_neck,其代码在https://github.com/open-mmlab/mmselfsup/blob/db861f30487c9a0b0cb3e35e176d6058887660bc/mmselfsup/models/necks/odc_neck.py

@MODELS.register_module()
class ODCNeck(BaseModule):
"""The non-linear neck of ODC: fc-bn-relu-dropout-fc-relu.

Args:
in_channels (int): Number of input channels.
hid_channels (int): Number of hidden channels.
out_channels (int): Number of output channels.
with_avg_pool (bool): Whether to apply the global
average pooling after backbone. Defaults to True.
norm_cfg (dict): Dictionary to construct and config norm layer.
Defaults to dict(type='SyncBN').
init_cfg (dict or list[dict], optional): Initialization config dict.
"""

def __init__(
self,
in_channels: int,
hid_channels: int,
out_channels: int,
with_avg_pool: bool = True,
norm_cfg: dict = dict(type='SyncBN'),
init_cfg: Optional[Union[dict, List[dict]]] = [
dict(type='Constant', val=1, layer=['_BatchNorm', 'GroupNorm'])
]
) -> None:
super().__init__(init_cfg)
self.with_avg_pool = with_avg_pool
if with_avg_pool:
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc0 = nn.Linear(in_channels, hid_channels)
self.bn0 = build_norm_layer(
dict(**norm_cfg, momentum=0.001, affine=False), hid_channels)[1]
self.fc1 = nn.Linear(hid_channels, out_channels)
self.relu = nn.ReLU(inplace=True)
self.dropout = nn.Dropout()

def forward(self, x: List[torch.Tensor]) -> List[torch.Tensor]:
"""Forward function.

Args:
x (List[torch.Tensor]): The feature map of backbone.

Returns:
List[torch.Tensor]: The output features.
"""
assert len(x) == 1
x = x[0]
if self.with_avg_pool:
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc0(x)
x = self.bn0(x)
x = self.relu(x)
x = self.dropout(x)
x = self.fc1(x)
x = self.relu(x)
return [x]

6.3.3.head网络

​  使用的是ClsHead,CrossEntropyLoss损失

6.3.4.memory_bank网络

6.3.运行时配置文件

default_runtime.py如下:

default_scope = 'mmselfsup'

default_hooks = dict(
runtime_info=dict(type='RuntimeInfoHook'),
timer=dict(type='IterTimerHook'),
logger=dict(type='LoggerHook', interval=50),
param_scheduler=dict(type='ParamSchedulerHook'),
checkpoint=dict(type='CheckpointHook', interval=10),
sampler_seed=dict(type='DistSamplerSeedHook'),
)

env_cfg = dict(
cudnn_benchmark=False,
mp_cfg=dict(mp_start_method='fork', opencv_num_threads=0),
dist_cfg=dict(backend='nccl'),
)

log_processor = dict(
window_size=10,
custom_cfg=[dict(data_src='', method='mean', window_size='global')])

vis_backends = [dict(type='LocalVisBackend')]
visualizer = dict(
type='SelfSupVisualizer', vis_backends=vis_backends, name='visualizer')
# custom_hooks = [dict(type='SelfSupVisualizationHook', interval=1)]

log_level = 'INFO'
load_from = None
resume = False