【深度学习】基于NNCLR模型的计算机视觉自监督学习过程

1.引言

1.1.自监督学习研究的背景

进行自监督学习的研究具有深远的意义:

首先,自监督学习能够有效减少对标注数据的依赖。在实际应用中,获取大量标注数据往往需要消耗大量的时间和人力资源,特别是在一些专业领域,如医疗图像分析或遥感图像识别,标注数据更是稀缺。自监督学习通过利用未标注的数据进行训练,能够挖掘数据中的潜在信息,并将其转化为有用的特征表示,从而降低对标注数据的依赖,降低数据标注的成本和难度。

其次,自监督学习有助于提升模型的泛化能力和鲁棒性。传统的监督学习主要依赖于标注数据,而自监督学习则通过设计各种自监督任务来训练模型,使模型能够学习到数据的内在结构和规律。这种学习方式使得模型不仅仅局限于特定任务的标注信息,而是能够学习到更加泛化和鲁棒的特征表示。这种泛化能力使得模型在面临新的任务或数据时,能够更快地适应并表现出较好的性能。

最后,自监督学习为深度学习理论的发展提供了新的思路和方法。自监督学习的研究涉及到如何设计有效的自监督任务、如何构建和优化对比损失函数、如何设计更有效的网络结构等多个方面。这些问题的研究不仅有助于推动深度学习理论的发展,还能够为其他领域的研究提供有益的启示。同时,自监督学习也为多模态学习、跨模态分析等领域提供了新的解决方案,有助于实现更准确的跨模态分析和理解。

综上所述,自监督学习的研究在降低数据标注成本、提升模型性能以及推动深度学习理论发展等方面都具有重要的意义。随着技术的不断进步和应用的不断拓展,自监督学习将在未来发挥更加重要的作用。

1.2 自监督学习的概念

自监督学习是一种创新的机器学习范式,它突破了传统监督学习的局限,通过巧妙地设计任务来从大量未标记的数据中自动提取有价值的信息。以下是对自监督学习概念的扩展和改写:

  1. 定义:自监督学习不仅是一种无监督学习方法,更是一种智能的数据驱动学习方式。它通过构造具有挑战性的任务,促使模型在没有明确指导的情况下自主学习数据的内在结构和模式。

  2. 核心特点深化
    - 数据驱动的监督信号:自监督学习的核心在于利用数据内在的属性来生成监督信号,这些信号指导模型学习到有效的特征表示。
    - 数据的多样性和丰富性:自监督学习能够处理各种类型的数据,包括图像、文本、音频等,这使得它在不同领域都有广泛的应用前景。
    - 创造性的预设任务设计:设计预设任务需要创造性思维,这些任务不仅要能够引导模型学习到有用的特征,还要能够应对数据的不确定性和复杂性。

  3. 实现方式丰富化
    - 在计算机视觉领域,除了预测图像的缺失部分或旋转角度,还可以设计任务如颜色化灰度图像、风格迁移等,以促使模型学习到更深层次的视觉特征。
    - 在自然语言处理中,除了预测遮蔽词或句子重排列,还可以设计任务如语言模型的连续性预测、情感分析的上下文理解等,以提高模型对语言的深入理解。

  4. 自监督学习的优势
    - 自监督学习能够充分利用未标记数据的潜力,降低了对昂贵且耗时的数据标注过程的依赖。
    - 由于减少了人工标签的介入,自监督学习在一定程度上避免了标签错误对模型性能的负面影响,提高了模型的鲁棒性。
    - 在多种下游任务中,自监督学习预训练的模型已经展现出与甚至超越传统监督学习的性能,证明了其强大的表示学习能力。

  5. 应用领域扩展
    - 自监督学习不仅在计算机视觉和自然语言处理领域取得了突破,还在语音识别、推荐系统、异常检测等多个领域展现出其独特的价值。
    - 随着研究的深入,自监督学习在跨模态学习中的应用也逐渐增多,例如利用图像和文本的联合表示来提高多模态数据的理解和处理能力。

自监督学习通过创造性地设计预设任务,有效地从无标签数据中提取信息,为机器学习领域带来了新的视角和方法。它的应用不仅限于传统的监督学习任务,还在不断拓展到新的领域和场景中,展现出广阔的发展前景。

1.3.NNCLR模型概述

NNCLR(Nearest Neighbor Contrastive Learning of Representations)是一种先进的计算机视觉自监督学习模型,它通过巧妙地利用图像的增强版本来学习强大的特征表示。以下是对NNCLR模型概念的改写和扩展:

1.模型背景
-

  • 自监督学习的重要性:NNCLR体现了自监督学习在处理大规模未标记数据时的重要性,它通过内在的数据结构来引导模型学习,无需依赖外部的标注信息。
  • 对比学习的创新性:该模型采用了对比学习的创新方法,通过对比样本间的相似度和差异度,推动模型学习到区分度高的特征表示。
  1. 工作原理
  • 数据增强的多样性:NNCLR通过应用多样化的数据增强技术如随机裁剪、颜色变换、仿射变换等,生成丰富的图像视图,增加了学习过程的复杂性和鲁棒性。
  • 特征提取的深度:利用深度学习模型,尤其是卷积神经网络,从增强后的图像中提取深层次的特征,这些特征捕捉了图像的高级语义信息。
  • 样本对的策略性构建:通过策略性地构建正负样本对,模型学习到区分不同图像的能力,正样本对的相似度被优化以提高模型的判别能力。
  • 对比损失的优化:采用如InfoNCE等对比损失函数,优化模型以识别和拉近正样本对的特征表示,同时推远负样本对,增强特征的区分性。
  1. 模型特点
  • 最近邻对比的高效性:NNCLR利用最近邻方法选择正样本,这种方法能够适应数据的分布,提供更丰富的语义变化,从而提高学习效率。
  • 性能的显著提升:在多个视觉识别任务中,NNCLR通过其自监督学习框架显著提升了模型性能,如在ImageNet分类任务中取得的显著提升。
  • 数据增强策略的灵活性:与其他对比学习方法相比,NNCLR在数据增强方面展现出更大的灵活性,减少了对特定数据增强技术的依赖。
  1. 应用场景
  • 预训练模型的通用性:NNCLR训练得到的模型不仅适用于计算机视觉任务,还可以迁移到其他领域,如医疗图像分析、卫星图像处理等,显示出其强大的通用性。
  • 迁移学习的广泛适用性:在不同的数据集和应用场景中,NNCLR模型均能快速适应并提供优异的性能,尤其在需要少量标注数据的迁移学习场景中。

NNCLR模型通过结合自监督学习和对比学习的优势,成功地从未标记的图像数据中提取出鲁棒且具有区分性的特征表示。它在多个视觉识别基准上的表现证明了其在计算机视觉领域的应用潜力和实际效用。随着研究的深入,NNCLR有望在更广泛的领域中发挥其强大的自监督学习能力。

1.4.研究内容

  1. 自监督学习:自监督表征学习致力于从原始数据中提取样本的稳健特征,无需依赖成本高昂的标签或注释。在这一领域的早期工作中,研究者们专注于创建预训练任务,这些任务通常涉及在具有丰富弱监督标签的领域内执行代理任务。通过解决这些任务,编码器能够学习到一般性特征,这些特征可能对那些需要昂贵标注的下游任务,例如图像分类,具有潜在的实用价值。

2.对比学习:在自监督学习技术中,对比损失方法是一个广泛应用的类别,它在计算机视觉的多个领域,如图像相似性比较、降维(DrLIM)和面部验证/识别等任务中发挥了重要作用。这类方法通过学习潜在空间,使得正样本在空间中聚集,同时将负样本推至远处。

  1. NNCLR:本文介绍了由Google Research和DeepMind提出的NNCLR模型。NNCLR通过自监督学习获取的表征超越了单一实例的正样本,这有助于学习对不同视角、变形以及类内变化具有不变性的特征。虽然基于聚类的方法可以超越单一实例的正样本,但如果将整个聚类视为正样本,可能会因为过早泛化而影响性能。NNCLR通过在已学习的特征空间中使用最近邻作为正样本来避免这一问题。此外,NNCLR不仅提升了如SimCLR等现有对比学习方法的性能,还减少了自监督学习对数据增强策略的依赖。

在SimCLR中,同一图像的两个视图通过随机数据增强生成,然后输入编码器以获得正样本嵌入对。NNCLR则采用了不同的方法,它维持了一个代表整个数据分布的支持集嵌入,并利用最近邻来构建正样本对。这个支持集在训练过程中充当记忆库,其运作方式类似于MoCo中的队列机制,即遵循先进先出的原则。

2. 自监督学习过程

2.1. 安装及设置

安装TensorFlow 数据表处理包

!pip install tensorflow-datasets

引用软件库

# 导入matplotlib的pyplot模块,用于绘图  
import matplotlib.pyplot as plt  
  
# 导入tensorflow库,用于构建和训练神经网络  
import tensorflow as tf  
  
# 导入tensorflow_datasets库,用于加载数据集  
import tensorflow_datasets as tfds  
  
# 导入os模块,用于处理操作系统相关的功能  
import os  
  
# 设置环境变量,确保keras使用tensorflow作为后端  
os.environ["KERAS_BACKEND"] = "tensorflow"  
  
# 导入keras库,keras是一个用于构建和训练深度学习模型的高级API  
import keras  
  
# 导入keras_cv库(注意:keras_cv不是Keras或TensorFlow的官方库,可能是某个第三方库或自定义库)  
import keras_cv  
  
# 从keras中导入ops模块,ops模块包含了一些操作函数和类  
from keras import ops  
  
# 从keras中导入layers模块,layers模块包含了构建神经网络所需的层(如卷积层、池化层等)  
from keras import layers

2.2.设置超参数

队列大小(queue_size):在实践中,较大的队列大小很可能意味着更好的性能,但这也会引入显著的计算开销。实验中,使用98,304(他们实验中最大的队列大小)时,NNCLR获得了最佳结果。在本文这里,我们使用10,000作为队列大小,以展示一个工作示例。

  • 队列大小的重要性:队列大小是自监督学习中一个关键的超参数,它直接影响模型的性能和计算成本。较大的队列可以提供更多的正样本选择,有助于模型学习到更鲁棒的特征表示。然而,这也意味着需要更多的内存和计算资源来维护和更新队列。

  • 实验结果:在NNCLR的原始研究中,作者通过一系列实验确定了队列大小与性能之间的关系。他们发现,当队列大小增加到98,304时,模型的性能得到了显著提升,这表明更大的队列可以提供更丰富的正样本,从而改善学习效果。

  • 实际应用中的权衡:虽然理论上更大的队列可以带来更好的性能,但在实际应用中,我们需要在性能和计算资源之间找到平衡。例如,在这个示例中,我们选择了10,000作为队列大小,这是一个折衷的选择,既可以在有限的计算资源下展示模型的工作流程,又能够获得相对满意的性能。

  • 队列大小的调整策略:在实际应用中,我们可以根据可用的计算资源和性能需求来调整队列大小。如果资源充足,可以尝试使用更大的队列来进一步提升性能。如果资源有限,可以通过优化数据增强策略或调整其他超参数来补偿较小队列可能带来的性能损失。

队列大小是影响自监督学习模型性能的一个重要因素。通过合理设置队列大小,我们可以在有限的计算资源下实现模型的最佳性能。同时,我们也需要考虑其他超参数和策略,以实现模型性能和计算效率的最佳平衡。

import tensorflow as tf  
  
# 设置AUTOTUNE为TensorFlow的自动调优参数,用于数据加载时的并行处理  
AUTOTUNE = tf.data.AUTOTUNE  
  
# 设置打乱数据集的缓冲区大小  
shuffle_buffer = 5000  
  
# STL10数据集的训练集带标签图片数量  
# 这两个值来自https://www.tensorflow.org/datasets/catalog/stl10  
labelled_train_images = 5000  
# STL10数据集的未标记图片数量  
unlabelled_images = 100000  
  
# 对比学习中的温度系数  
temperature = 0.1  
  
# 对比学习中的队列大小,用于构建正样本和负样本  
queue_size = 10000  
  
# 对比学习中的数据增强设置  
contrastive_augmenter = {  
    "brightness": 0.5,  # 亮度调整的范围  
    "name": "contrastive_augmenter",  # 增强器的名称  
    "scale": (0.2, 1.0),  # 其他可能的缩放或变换的范围  
}  
  
# 分类任务中的数据增强设置  
classification_augmenter = {  
    "brightness": 0.2,  # 亮度调整的范围  
    "name": "classification_augmenter",  # 增强器的名称  
    "scale": (0.5, 1.0),  # 其他可能的缩放或变换的范围  
}  
  
# 输入图片的形状  
input_shape = (96, 96, 3)  
  
# 嵌入层的宽度(例如,对比学习中的特征维度)  
width = 128  
  
# 训练周期数,为了获得更好的结果,可以使用25个周期  
num_epochs = 5  # Use 25 for better results  
  
# 每个周期的训练步数,为了获得更好的结果,可以使用200步  
steps_per_epoch = 50  # Use 200 for better results  

2.3.数据预处理

2.3.1.加载数据

加载STL-10数据集:我们从TensorFlow Datasets加载STL-10数据集,这是一个用于开发无监督特征学习、深度学习和自我指导学习算法的图像识别数据集。STL-10数据集受CIFAR-10数据集的启发,但进行了一些修改。

  • 数据集特点:STL-10数据集是一个广泛用于机器学习研究的数据集,特别是在无监督学习和自监督学习领域。它包含了多种类别的图像,提供了丰富的视觉特征,适合用来训练和评估深度学习模型。

  • 与CIFAR-10的比较:虽然STL-10在设计上受到了CIFAR-10的影响,但它在数据分布和图像复杂性上做了一些调整。例如,STL-10的图像分辨率更高,图像内容也更加多样化,这为研究者提供了更具挑战性的任务。

  • 数据集结构:STL-10数据集由训练集、测试集和未标记的挑战集组成。训练集包含了5000个类别的图像,每个类别有500个样本。测试集则包含了100个类别,每个类别有100个样本。此外,还有未标记的挑战集,用于评估模型在未知类别上的表现。

  • 数据集的应用:STL-10数据集不仅适用于图像分类任务,还可以用于其他视觉任务,如特征提取、目标检测和图像分割等。由于其无监督的特性,STL-10为研究者提供了一个理想的平台来探索和开发新的学习算法。

  • 数据集的加载和预处理:在使用STL-10数据集之前,需要从TensorFlow Datasets库中加载数据,并进行适当的预处理,如图像大小调整、归一化和数据增强等。这些步骤对于确保模型能够从数据中有效学习至关重要。

STL-10是一个多用途的图像数据集,为无监督学习和自监督学习提供了丰富的资源。通过合理利用这个数据集,研究者可以在多种视觉任务中测试和改进他们的算法,推动机器学习领域的发展。

# 数据集名称  
dataset_name = "stl10"  
  
  
def prepare_dataset():  
    """  
    准备STL10数据集,包括未标记和标记的训练集以及测试集。  
    """  
    # 计算未标记和标记数据的批次大小  
    unlabeled_batch_size = unlabelled_images // steps_per_epoch  
    labeled_batch_size = labelled_train_images // steps_per_epoch  
    # 合并两者的批次大小作为总批次大小  
    batch_size = unlabeled_batch_size + labeled_batch_size  
  
    # 加载未标记的训练数据集  
    unlabeled_train_dataset = (  
        tfds.load(  
            name=dataset_name,  # 数据集名称  
            split="unlabelled",  # 划分类型为未标记  
            as_supervised=True,  # 加载数据和标签  
            shuffle_files=True   # 打乱文件顺序  
        )  
        .shuffle(buffer_size=shuffle_buffer)  # 打乱数据  
        .batch(unlabeled_batch_size, drop_remainder=True)  # 批次处理  
    )  
  
    # 加载标记的训练数据集  
    labeled_train_dataset = (  
        tfds.load(  
            name=dataset_name,  # 数据集名称  
            split="train",  # 划分类型为训练集  
            as_supervised=True,  # 加载数据和标签  
            shuffle_files=True   # 打乱文件顺序  
        )  
        .shuffle(buffer_size=shuffle_buffer)  # 打乱数据  
        .batch(labeled_batch_size, drop_remainder=True)  # 批次处理  
    )  
  
    # 加载测试数据集  
    test_dataset = (  
        tfds.load(  
            name=dataset_name,  # 数据集名称  
            split="test",  # 划分类型为测试集  
            as_supervised=True  # 加载数据和标签  
        )  
        .batch(batch_size)  # 批次处理  
        .prefetch(buffer_size=AUTOTUNE)  # 预取数据以加速数据加载  
    )  
  
    # 将未标记和标记的训练数据集合并为一个数据集  
    train_dataset = tf.data.Dataset.zip(  
        (unlabeled_train_dataset, labeled_train_dataset)  
    ).prefetch(buffer_size=AUTOTUNE)  # 预取数据以加速数据加载  
  
    # 返回批次大小、训练数据集、标记的训练数据集和测试数据集  
    return batch_size, train_dataset, labeled_train_dataset, test_dataset  
  
  
# 调用函数准备数据集  
batch_size, train_dataset, labeled_train_dataset, test_dataset = prepare_dataset()
2.3.2.数据增强

自监督学习技术的数据增强依赖性:像SimCLR、BYOL、SwAV等其他自监督学习技术在获取最佳性能时,严重依赖于精心设计的数据增强流程。然而,NNCLR对复杂增强的依赖性较小,因为最近邻已经在样本变化中提供了丰富的信息。

  • 常见的数据增强技术:通常包含在增强流程中的一些常见技术包括:
    - 随机调整大小的裁剪:通过随机调整图像的尺寸并进行裁剪,增加样本的多样性。
    - 多种颜色失真:通过改变图像的颜色空间属性,模拟不同的光照和颜色条件。
    - 高斯模糊:应用高斯模糊来模拟图像的模糊效果,增加模型对模糊的鲁棒性。
    NNCLR的数据增强策略:由于NNCLR对复杂增强的依赖性较小,我们将只使用随机裁剪和随机亮度调整来增强输入图像。

  • 数据增强的重要性:尽管NNCLR不像其他自监督学习方法那样依赖复杂的数据增强,但适当的数据增强仍然是提高模型泛化能力和性能的关键因素。数据增强通过模拟不同的视觉变化,帮助模型学习到更加鲁棒的特征。

  • 简化的数据增强流程:在NNCLR中,我们选择使用更简单的数据增强技术,如随机裁剪和随机亮度调整。这种方法减少了计算成本,同时仍然能够提供足够的样本变化,以支持有效的学习过程。

  • 数据增强与模型性能的关系:尽管简化了数据增强流程,但NNCLR依然能够实现良好的性能,这表明模型能够从最近邻的样本中学习到丰富的特征表示,即使在数据增强较少的情况下。

  • 灵活性与适应性:NNCLR的这种灵活性允许研究者根据具体的应用场景和计算资源来调整数据增强的复杂性。在资源受限的情况下,可以采用更简单的增强策略;在需要进一步提升性能时,可以考虑引入更复杂的数据增强技术。

NNCLR通过减少对复杂数据增强的依赖,展示了自监督学习模型在不同条件下的适应性和灵活性。简化的数据增强流程不仅降低了计算成本,还为模型提供了足够的样本变化,使其能够在各种视觉任务中表现出色。

def augmenter(brightness_factor, name, scale_factor):  
    """  
    创建一个数据增强模型序列,用于图像增强。  
  
    参数:  
    brightness_factor (float): 亮度变化的因子。  
    name (str): 增强模型序列的名称。  
    scale_factor (tuple or float): 裁剪面积的比例因子,如果是tuple,则包含两个浮点数(min, max)。  
  
    返回:  
    keras.Sequential: 包含多个数据增强层的模型序列。  
    """  
    # 假设 input_shape 已经在外部定义,代表输入图像的尺寸  
    return keras.Sequential(  
        [  
            # 输入层,需要指定输入图像的尺寸  
            layers.Input(shape=input_shape),  
            # 将像素值缩放到0到1之间  
            layers.Rescaling(1 / 255),  
            # 随机水平翻转图像  
            layers.RandomFlip("horizontal"),  
            # 随机裁剪并调整图像大小  
            keras_cv.layers.RandomCropAndResize(  
                target_size=(input_shape[0], input_shape[1]),  # 目标尺寸与输入尺寸相同  
                crop_area_factor=scale_factor,  # 裁剪面积的比例因子  
                aspect_ratio_factor=(3 / 4, 4 / 3),  # 裁剪区域的长宽比因子  
            ),  
            # 随机改变图像的亮度  
            keras_cv.layers.RandomBrightness(  
                factor=brightness_factor,  # 亮度变化的因子  
                value_range=(0.0, 1.0)  # 亮度值的范围  
            ),  
        ],  
        name=name,  # 序列名称  
    )  
  
# 注意:在使用此函数之前,需要确保已经导入了相应的库,例如 keras, keras.layers, keras_cv.layers,  
# 并且 input_shape 需要在外部定义

2.4建立模型

2.4.1.编码器架构

本示例使用ResNet-50作为编码器:在文献中,使用ResNet-50作为编码器架构是标准做法。在原始论文中,作者使用ResNet-50作为编码器,并对其输出进行空间平均处理。但请注意,更强大的模型不仅会增加训练时间,还会需要更多的内存,并且会限制您可以使用的最大的批量大小。

  • 简化的编码器架构:为了本示例的目的,我们只使用四层卷积网络。

  • 编码器选择的重要性:编码器架构的选择对于自监督学习模型的性能至关重要。ResNet-50因其强大的特征提取能力而在多个研究中被广泛采用。然而,选择更复杂的模型需要权衡训练效率和资源消耗。

  • 空间平均的作用:对ResNet-50的输出进行空间平均处理有助于减少特征的空间维度,同时保留重要的信息,这对于后续的特征比较和学习过程是有益的。

  • 简化架构的实用性:在本示例中,为了展示NNCLR模型的基本概念和工作流程,我们采用了简化的编码器架构,即四层卷积网络。这种简化不仅降低了实现的复杂性,也使得模型更加轻量化,便于理解和实验。

  • 资源与性能的平衡:在实际应用中,我们需要根据可用的计算资源和所需的性能来选择合适的编码器架构。对于资源受限的情况,简化的编码器可以是一个有效的选择。对于追求更高性能的应用,可以考虑使用更复杂的模型,如ResNet-50或其他更深的网络。

  • 编码器深度对批量大小的影响:编码器的深度直接影响模型的内存消耗和训练时间。更深的网络可能需要更大的批量大小来实现有效学习,但这也可能受到硬件资源的限制。

编码器架构的选择需要综合考虑性能、资源和应用场景。虽然简化的编码器架构可以快速展示模型的工作流程,但在资源允许的情况下,使用更强大的模型如ResNet-50可能会带来更好的学习效果和性能。

2.4.2.NNCLR模型用于对比性预训练
  • 使用对比损失训练编码器:我们在未标记的图像上训练一个编码器,使用对比损失函数来优化模型。这种训练方式鼓励编码器学习到能够区分不同图像的特征表示。

  • 非线性投影头:在编码器的顶部附加一个非线性投影头,这样做可以提升编码器表示的质量。投影头通常包含一些全连接层或非线性激活函数,有助于进一步提取和转换特征,使其更适合对比学习的目标。

  • 对比性预训练的目标:NNCLR模型通过对比性预训练,旨在从大量未标记的数据中学习到丰富的特征表示。这种预训练方法为后续的下游任务提供了一个强大的起点,使得模型能够更快地适应特定任务。

  • 对比损失的作用:对比损失通过比较正样本和负样本之间的相似度来指导学习过程。在NNCLR中,正样本通常来自于同一图像的不同增强视图,而负样本则来自于其他图像。这种损失函数的设计有助于模型学习到区分不同图像的能力。

  • 非线性投影头的重要性:非线性投影头是NNCLR架构中的一个关键组件。它不仅增加了模型的表示能力,还通过引入非线性变换,使得模型能够捕捉更复杂的特征模式。

  • 编码器与投影头的协同:编码器负责从图像中提取特征,而投影头则对这些特征进行进一步的处理和优化。这种协同工作的方式使得模型能够学习到更加精细和鲁棒的特征表示。

  • 预训练与下游任务的关联:通过对比性预训练得到的编码器,可以在多种下游任务中发挥作用。例如,在图像分类、目标检测或图像分割等任务中,预训练的编码器可以作为特征提取器,提供高质量的输入特征。

NNCLR模型通过结合对比性损失和非线性投影头,有效地从未标记的图像中学习到有用的特征表示。这种预训练方法不仅提高了模型的表示能力,还为解决各种视觉任务提供了一个坚实的基础。随着自监督学习领域的不断发展,NNCLR及其变体有望在更多应用中展现出其潜力。

class NNCLR(keras.Model):
    def __init__(self, temperature, queue_size):
        super().__init__()
        # 初始化评价指标
        self.probe_accuracy = keras.metrics.SparseCategoricalAccuracy()
        self.correlation_accuracy = keras.metrics.SparseCategoricalAccuracy()
        self.contrastive_accuracy = keras.metrics.SparseCategoricalAccuracy()
        self.probe_loss = keras.losses.SparseCategoricalCrossentropy(from_logits=True)

        # 初始化数据增强器
        self.contrastive_augmenter = augmenter(**contrastive_augmenter)
        self.classification_augmenter = augmenter(**classification_augmenter)

        # 初始化编码器
        self.encoder = encoder()

        # 初始化非线性投影头
        self.projection_head = keras.Sequential(
            [
                layers.Input(shape=(width,)),
                layers.Dense(width, activation="relu"),
                layers.Dense(width),
            ],
            name="projection_head",
        )

        # 初始化线性探针
        self.linear_probe = keras.Sequential(
            [layers.Input(shape=(width,)), layers.Dense(10)], name="linear_probe"
        )

        # 温度参数,用于控制对比损失中的缩放因子
        self.temperature = temperature

        # 特征维度
        feature_dimensions = self.encoder.output_shape[1]
        # 初始化特征队列
        self.feature_queue = keras.Variable(
            keras.utils.normalize(
                keras.random.normal(shape=(queue_size, feature_dimensions)),
                axis=1,
                order=2,
            ),
            trainable=False,
        )

    def compile(self, contrastive_optimizer, probe_optimizer, **kwargs):
        super().compile(**kwargs)
        # 对比优化器和探针优化器
        self.contrastive_optimizer = contrastive_optimizer
        self.probe_optimizer = probe_optimizer

    # 计算最近邻特征
    def nearest_neighbour(self, projections):
        support_similarities = tf.matmul(
            projections, tf.transpose(self.feature_queue))
        nn_projections = tf.gather(
            self.feature_queue, tf.argmax(support_similarities, axis=1), axis=0
        )
        return projections + tf.stop_gradient(nn_projections - projections)

    # 更新对比准确率
    def update_contrastive_accuracy(self, features_1, features_2):
        # 归一化特征
        features_1 = tf.nn.l2_normalize(features_1, axis=1)
        features_2 = tf.nn.l2_normalize(features_2, axis=1)
        # 计算相似度
        similarities = tf.matmul(features_1, tf.transpose(features_2))
        # 构建对比标签
        batch_size = tf.shape(features_1)[0]
        contrastive_labels = tf.range(batch_size)
        self.contrastive_accuracy.update_state(
            tf.concat([contrastive_labels, contrastive_labels], axis=0),
            tf.concat([similarities, tf.transpose(similarities)], axis=0),
        )

    # 更新相关性准确率
    def update_correlation_accuracy(self, features_1, features_2):
        # 标准化特征
        features_1 = (features_1 - tf.reduce_mean(features_1, axis=0)) / tf.math.reduce_std(
            features_1, axis=0
        )
        features_2 = (features_2 - tf.reduce_mean(features_2, axis=0)) / tf.math.reduce_std(
            features_2, axis=0
        )
        # 计算交叉相关性
        batch_size = tf.shape(features_1)[0]
        cross_correlation = (
            tf.matmul(tf.transpose(features_1), features_2) / batch_size
        )
        feature_dim = tf.shape(features_1)[1]
        correlation_labels = tf.range(feature_dim)
        self.correlation_accuracy.update_state(
            tf.concat([correlation_labels, correlation_labels], axis=0),
            tf.concat(
                [cross_correlation, tf.transpose(cross_correlation)], axis=0
            ),
        )

    # 对比损失函数
    def contrastive_loss(self, projections_1, projections_2):
        # 归一化特征
        projections_1 = tf.nn.l2_normalize(projections_1, axis=1)
        projections_2 = tf.nn.l2_normalize(projections_2, axis=1)

        # 计算相似度
        similarities_1_2_1 = (
            tf.matmul(
                self.nearest_neighbour(projections_1), tf.transpose(projections_2)
            )
            / self.temperature
        )
        # 此处省略了其他相似度计算的代码以简化展示...

        # 构建对比标签
        batch_size = tf.shape(projections_1)[0]
        contrastive_labels = tf.range(batch_size)
        # 计算对比损失
        loss = keras.losses.sparse_categorical_crossentropy(
            tf.concat([
                contrastive_labels,
                # 此处省略了其他相似度标签的代码以简化展示...
            ], axis=0),
            tf.concat([
                similarities_1_2_1,
                # 此处省略了其他相似度的代码以简化展示...
            ], axis=0),
            from_logits=True,
        )

        # 更新特征队列
        self.feature_queue.assign(
            tf.concat([projections_1, self.feature_queue[:-batch_size]], axis=0)
        )
        return loss

    # 训练步骤
    def train_step(self, data):
        # 此处省略了数据增强和编码器的代码以简化展示...
        # 计算梯度和更新参数
        with tf.GradientTape() as tape:
            # 此处省略了特征提取和投影头的代码以简化展示...
            contrastive_loss = self.contrastive_loss(projections_1, projections_2)
        # 此处省略了梯度计算和优化器应用的代码以简化展示...

        # 更新评价指标
        self.update_contrastive_accuracy(features_1, features_2)
        self.update_correlation_accuracy(features_1, features_2)
        # 返回训练结果
        return {
            "c_loss": contrastive_loss,
            "c_acc": self.contrastive_accuracy.result(),
            "r_acc": self.correlation_accuracy.result(),
            # 此处省略了探针损失和准确率的返回代码以简化展示...
        }

    # 测试步骤
    def test_step(self, data):
        # 此处省略了数据预处理和特征提取的代码以简化展示...
        # 计算探针损失和更新评价指标
        probe_loss = self.probe_loss(labels, class_logits)
        self.probe_accuracy.update_state(labels, class_logits)
        # 返回测试结果
        return {"p_loss": probe_loss, "p_acc": self.probe_accuracy.result()}

2.5.预训练 NNCLR

为了有效地预训练NNCLR模型,我们采取了以下策略和方法:

  1. 温度参数的选择:我们遵循论文的建议,选择了0.1作为温度值。温度参数在对比学习中起着至关重要的作用,它控制了相似度分布的平滑程度,进而影响模型学习特征表示的能力。

  2. 队列大小的设定:队列大小设置为10,000,这为模型提供了足够的样本多样性,有助于学习更加鲁棒的特征。队列大小的选择需要平衡计算资源和模型性能。

  3. 优化器的选用:我们选择了Adam优化器,它因其自适应学习率的特性而被广泛应用于深度学习中。在本例中,Adam优化器同时用于对比损失和探针损失的优化。

  4. 训练周期的安排:虽然示例中只训练了30个周期,但为了获得更好的性能,我们建议根据验证集上的性能进行更多的训练周期,直至收敛。

  5. 预训练效果的监控:我们使用了以下两个关键指标来监控预训练的效果:

  • 对比准确率:这是一个衡量自监督学习效果的重要指标,它反映了模型是否能够识别出同一图像的不同增强视图的相似性。即使在没有标签的样本情况下,这个指标也能提供有价值的反馈,帮助我们进行超参数的调整和模型优化。

  • 线性探测准确率:这是一种评估自监督学习模型性能的方法,通过在冻结的编码器之上训练一个简单的分类器(如逻辑回归模型),来评估特征的分类能力。与常规的预训练后训练分类器不同,我们在预训练过程中就同时训练线性探测分类器。这种方法虽然可能会对最终的准确率产生一定影响,但它允许我们在训练过程中实时监控分类性能,为实验和调试提供了便利。

  1. 模型性能的进一步优化:为了进一步提升模型性能,我们可以考虑以下策略:
  • 数据增强的多样性:探索不同类型的数据增强技术,以增加模型对输入数据变化的鲁棒性。
  • 超参数的调整:细致调整包括学习率、批大小等在内的超参数,以找到最优的模型配置。
  • 正则化技术:应用如dropout、权重衰减等正则化技术,以防止模型过拟合。
  1. 模型的下游任务适应性:在预训练完成后,模型通常需要针对特定的下游任务进行微调。这可能包括调整或替换最后的全连接层,以及在有标签的数据集上进行额外的训练。

通过精心选择的温度参数、队列大小、优化器,以及对关键指标的监控,我们可以有效地预训练NNCLR模型。此外,通过进一步的策略调整和优化,我们的模型能够在多种视觉识别任务中展现出卓越的性能。

# 定义温度参数和队列大小
temperature = 0.5  # 这通常需要根据实验进行调整
queue_size = 65536  # 特征队列的大小

# 初始化NNCLR模型
model = NNCLR(temperature=temperature, queue_size=queue_size)

# 编译模型,使用Adam优化器进行对比损失和探针损失的优化
model.compile(
    contrastive_optimizer=keras.optimizers.Adam(),  # 对比损失的优化器
    probe_optimizer=keras.optimizers.Adam(),        # 探针损失的优化器
    jit_compile=False,                             # 是否使用JIT编译,False表示不使用
)

# 训练模型
pretrain_history = model.fit(
    train_dataset,  # 训练数据集
    epochs=num_epochs,  # 训练的轮数
    validation_data=test_dataset  # 验证数据集,用于评估模型在未见过的数据上的表现
)

自监督学习是一种在标记数据稀缺但未标记数据丰富的情况下特别有效的学习范式。这一领域的先驱方法,如SEER、SimCLR、SwAV等,已经证明了通过在大量未标记数据集上进行预训练,然后迁移到小规模标记数据集上进行微调,可以显著提升模型在有限类别标签下的性能。

这些方法的成功不仅在于它们能够利用未标记数据来学习通用特征表示,而且还在于它们为模型提供了一种强大的预训练基础,这有助于在面对标记数据不足时,依然能够实现有效的迁移学习。以下是一些扩展和建议,以帮助您更深入地了解自监督学习的实际应用和效果:

  1. 深入研究现有方法:通过阅读有关SEER、SimCLR、SwAV等方法的原始论文和博客文章,您可以获得关于这些方法的工作原理、优势以及局限性的深刻理解。

  2. 探索实际案例:阅读博客文章和教程,了解如何将自监督学习应用于实际问题,例如图像分类、去噪、特征提取等。

  3. 实践和实验:通过在Keras等深度学习框架中实现自监督学习算法,您可以亲自体验这些方法的效果,并根据项目需求进行调整和优化。

  4. 关注领域专家:关注像Debidatta Dwibedi这样的领域专家,他们不仅在自监督学习领域做出了重要贡献,而且通过分享他们的知识和经验,帮助社区的其他成员学习和进步。

  5. 参与社区讨论:加入机器学习和自监督学习的在线社区,参与讨论和交流,这可以帮助您获取最新的研究动态和技术进展。

  6. 跨领域应用:考虑将自监督学习应用于不同领域,如自然语言处理、语音识别、时间序列分析等,探索其在这些领域的潜力和挑战。

  7. 创新和改进:在现有方法的基础上,尝试提出新的自监督学习算法或改进现有算法,以解决特定问题或提高性能。

自监督学习提供了一种强大的工具,可以在有限的标记数据条件下,充分利用未标记数据来提升模型性能。通过不断学习和实践,您可以掌握这一领域的核心技术,并将其应用于您的研究和项目中,以解决实际问题。

3. 总结与展望

总结

自监督学习作为深度学习领域的一个重要分支,已经在图像识别、自然语言处理等多个领域展现出了巨大的潜力和价值。通过在大量未标记数据集上的预训练,模型能够学习到丰富的特征表示,这些特征在后续的下游任务中表现出了强大的泛化能力和鲁棒性。NNCLR模型作为自监督学习中的一个突出代表,通过结合对比学习和最近邻方法,有效地提高了特征学习的质量和效率。

在本研究中,我们探讨了自监督学习的理论基础、关键技术和实际应用。我们分析了自监督学习的重要性、核心特点、实现方式和优势,并以NNCLR模型为例,详细介绍了其工作原理、模型特点和应用场景。我们还提供了一个基于STL-10数据集的自监督学习实现示例,包括数据预处理、模型构建、训练和评估等步骤。

展望

尽管自监督学习已经取得了显著的进展,但仍有许多挑战和机遇值得进一步探索:

  1. 算法创新:当前的自监督学习方法仍有改进空间。研究者可以探索新的自监督任务设计、损失函数优化和网络结构创新,以提高模型的性能和适用性。

  2. 多模态学习:自监督学习在多模态数据上的潜力尚未完全挖掘。结合图像、文本、音频等多种数据类型,可以为多模态学习提供新的视角和方法。

  3. 跨领域应用:自监督学习在不同领域的应用仍处于起步阶段。将其应用于医疗、金融、工业等专业领域,有望解决更多实际问题。

  4. 小样本学习:在标记数据极其有限的情况下,自监督学习如何与小样本学习相结合,提高模型的学习能力和适应性,是一个值得研究的方向。

  5. 理论分析:自监督学习的理论研究相对不足。深入分析其数学原理、统计特性和学习机制,可以为算法设计和应用提供更坚实的理论基础。

  6. 计算效率:随着模型规模的增长,自监督学习算法的计算效率和资源消耗问题日益突出。研究如何提高算法的计算效率和可扩展性,对于实际应用具有重要意义。

  7. 鲁棒性与安全性:在实际应用中,自监督学习模型可能面临对抗攻击、数据偏差等挑战。提高模型的鲁棒性和安全性,确保其在各种环境下的稳定运行,是未来研究的重要方向。

总之,自监督学习作为一种新兴的学习范式,其发展前景广阔。通过不断的技术创新和应用探索,我们有理由相信,自监督学习将在人工智能领域发挥更加关键的作用,推动技术的进一步发展和应用。

参考文献

[1]Keras团队. “NNCLR 示例.” Keras官方网站, 2024-1-22. https://keras.io/examples/vision/nnclr/. 访问日期: 2024年6月14日.

示例代码

import matplotlib.pyplot as plt
import tensorflow as tf
import tensorflow_datasets as tfds
import os

# 设置Keras后端为TensorFlow
os.environ["KERAS_BACKEND"] = "tensorflow"
# 导入Keras和其他所需库
import keras
import keras_cv
from keras import ops
from keras import layers

# 超参数设置
# 队列大小queue_size越大,性能可能越好,但也会增加计算开销
AUTOTUNE = tf.data.AUTOTUNE  # TensorFlow数据加载自动调整参数
shuffle_buffer = 5000  # 数据洗牌缓冲区大小
# STL-10数据集的图像数量
labelled_train_images = 5000  # 带标签的训练图像数量
unlabelled_images = 100000  # 未标记的图像数量

# 其他超参数
temperature = 0.1  # 对比学习中的温度参数
queue_size = 10000  # 特征队列大小
contrastive_augmenter = {"brightness": 0.5, "name": "contrastive_augmenter", "scale": (0.2, 1.0)}  # 对比学习增强器参数
classification_augmenter = {"brightness": 0.2, "name": "classification_augmenter", "scale": (0.5, 1.0)}  # 分类学习增强器参数
input_shape = (96, 96, 3)  # 输入图像的尺寸
width = 128  # 特征宽度
num_epochs = 5  # 训练周期数,实际应使用更多周期以达到更好性能
steps_per_epoch = 50  # 每个训练周期的步数

# 加载STL-10数据集
dataset_name = "stl10"

def prepare_dataset():
    # 准备数据集的函数
    # 计算批次大小
    unlabeled_batch_size = unlabelled_images // steps_per_epoch
    labeled_batch_size = labelled_train_images // steps_per_epoch
    batch_size = unlabeled_batch_size + labeled_batch_size

    # 加载和处理未标记训练数据集
    unlabeled_train_dataset = (
        tfds.load(
            dataset_name, split="unlabelled", as_supervised=True, shuffle_files=True
        )
        .shuffle(buffer_size=shuffle_buffer)
        .batch(unlabeled_batch_size, drop_remainder=True)
    )
    # 加载和处理标记训练数据集
    labeled_train_dataset = (
        tfds.load(dataset_name, split="train", as_supervised=True, shuffle_files=True)
        .shuffle(buffer_size=shuffle_buffer)
        .batch(labeled_batch_size, drop_remainder=True)
    )
    # 加载测试数据集
    test_dataset = (
        tfds.load(dataset_name, split="test", as_supervised=True)
        .batch(batch_size)
        .prefetch(buffer_size=AUTOTUNE)
    )
    # 组合训练数据集
    train_dataset = tf.data.Dataset.zip(
        (unlabeled_train_dataset, labeled_train_dataset)
    ).prefetch(buffer_size=AUTOTUNE)

    return batch_size, train_dataset, labeled_train_dataset, test_dataset

# 调用函数准备数据集
batch_size, train_dataset, labeled_train_dataset, test_dataset = prepare_dataset()

# 数据增强模块准备
def augmenter(brightness, name, scale):
    # 创建数据增强序列模型
    return keras.Sequential(
        [
            layers.Input(shape=input_shape),
            layers.Rescaling(1 / 255),
            layers.RandomFlip("horizontal"),
            keras_cv.layers.RandomCropAndResize(
                target_size=(input_shape[0], input_shape[1]),
                crop_area_factor=scale,
                aspect_ratio_factor=(3 / 4, 4 / 3),
            ),
            keras_cv.layers.RandomBrightness(factor=brightness, value_range=(0.0, 1.0)),
        ],
        name=name,
    )

# 编码器架构
def encoder():
    # 构建编码器序列模型
    return keras.Sequential(
        [
            layers.Input(shape=input_shape),
            layers.Conv2D(width, kernel_size=3, strides=2, activation="relu"),
            layers.Conv2D(width, kernel_size=3, strides=2, activation="relu"),
            layers.Conv2D(width, kernel_size=3, strides=2, activation="relu"),
            layers.Conv2D(width, kernel_size=3, activation="relu"),
            layers.Flatten(),
            layers.Dense(width, activation="relu"),
        ],
        name="encoder",
    )

# NNCLR模型定义
class NNCLR(keras.Model):
    # NNCLR模型初始化
    def __init__(self, temperature, queue_size):
        super().__init__()
        # 初始化评价指标和损失函数
        self.probe_accuracy = keras.metrics.SparseCategoricalAccuracy()
        self.correlation_accuracy = keras.metrics.SparseCategoricalAccuracy()
        self.contrastive_accuracy = keras.metrics.SparseCategoricalAccuracy()
        self.probe_loss = keras.losses.SparseCategoricalCrossentropy(from_logits=True)

        # 初始化数据增强器、编码器和投影头
        self.contrastive_augmenter = augmenter(**contrastive_augmenter)
        self.classification_augmenter = augmenter(**classification_augmenter)
        self.encoder = encoder()
        self.projection_head = keras.Sequential(
            [
                layers.Input(shape=(width,)),
                layers.Dense(width, activation="relu"),
                layers.Dense(width),
            ],
            name="projection_head",
        )
        self.linear_probe = keras.Sequential(
            [layers.Input(shape=(width,)), layers.Dense(10)], name="linear_probe"
        )
        self.temperature = temperature

        # 初始化特征队列
        feature_dimensions = self.encoder.output_shape[1]
        self.feature_queue = keras.Variable(
            keras.utils.normalize(
                keras.random.normal(shape=(queue_size, feature_dimensions)),
                axis=1,
                order=2,
            ),
            trainable=False,
        )

    # 编译模型
    def compile(self, contrastive_optimizer, probe_optimizer, **kwargs):
        super().compile(**kwargs)
        self.contrastive_optimizer = contrastive_optimizer
        self.probe_optimizer = probe_optimizer

    # 计算最近邻特征
    def nearest_neighbour(self, projections):
        # 计算最近邻的函数
        support_similarities = ops.matmul(projections, ops.transpose(self.feature_queue))
        nn_projections = ops.take(
            self.feature_queue, ops.argmax(support_similarities, axis=1), axis=0
        )
        return projections + ops.stop_gradient(nn_projections - projections)

    # 更新对比准确率
    def update_contrastive_accuracy(self, features_1, features_2):
        # 更新对比准确率的函数
        features_1 = keras.utils.normalize(features_1, axis=1, order=2)
        features_2 = keras.utils.normalize(features_2, axis=1, order=2)
        similarities = ops.matmul(features_1, ops.transpose(features_2))
        batch_size = ops.shape(features_1)[0]
        contrastive_labels = ops.arange(batch_size)
        self.contrastive_accuracy.update_state(
            ops.concatenate([contrastive_labels, contrastive_labels], axis=0),
            ops.concatenate([similarities, ops.transpose(similarities)], axis=0),
        )

    # 更新相关性准确率
    def update_correlation_accuracy(self, features_1, features_2):
        # 更新相关性准确率的函数
        features_1 = (features_1 - ops.mean(features_1, axis=0)) / ops.std(features_1, axis=0)
        features_2 = (features_2 - ops.mean(features_2, axis=0)) / ops.std(features_2, axis=0)

        batch_size = ops.shape(features_1)[0]
        cross_correlation = (
            ops.matmul(ops.transpose(features_1), features_2) / batch_size
        )

        feature_dim = ops.shape(features_1)[1]
        correlation_labels = ops.arange(feature_dim)
        self.correlation_accuracy.update_state(
            ops.concatenate([correlation_labels, correlation_labels], axis=0),
            ops.concatenate(
                [cross_correlation, ops.transpose(cross_correlation)], axis=0
            ),
        )

    # 对比损失函数
    def contrastive_loss(self, projections_1, projections_2):
        # 计算对比损失的函数
        projections_1 = keras.utils.normalize(projections_1, axis=1, order=2)
        projections_2 = keras.utils.normalize(projections_2, axis=1, order=2)

        # 计算相似度和损失
        # ...(省略部分代码以简化展示)

    # 训练步骤
    def train_step(self, data):
        # 单步训练函数
        # ...(省略部分代码以简化展示)

    # 测试步骤
    def test_step(self, data):
        # 单步测试函数
        # ...(省略部分代码以简化展示)

# 使用指定的温度和队列大小预训练NNCLR模型
model = NNCLR(temperature=temperature, queue_size=queue_size)
model.compile(
    contrastive_optimizer=keras(optimizers.Adam(),
 probe_optimizer=keras.optimizers.Adam(),
 jit_compile=False,
)
# 执行模型预训练
pretrain_history = model.fit(
    train_dataset, 
    epochs=num_epochs, 
    validation_data=test_dataset
)

# 使用预训练得到的模型进行微调
# 构建微调模型,这里我们在预训练的编码器之上添加一个新的全连接分类层
finetuning_model = keras.Sequential(
    [
        layers.Input(shape=input_shape),
        augmenter(**classification_augmenter),  # 使用定义好的数据增强模块
        model.encoder,  # 使用预训练的编码器
        layers.Dense(10),  # 添加一个新的分类层,用于分类任务
    ],
    name="finetuning_model",
)
# 编译微调模型,使用Adam优化器和稀疏分类交叉熵损失函数
finetuning_model.compile(
    optimizer=keras.optimizers.Adam(),
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=[keras.metrics.SparseCategoricalAccuracy(name="acc")],
    jit_compile=False,
)

# 执行模型微调
finetuning_history = finetuning_model.fit(
    labeled_train_dataset, 
    epochs=num_epochs, 
    validation_data=test_dataset
)

# 可视化预训练和微调过程中的准确率和损失
def plot_training_curves(pretraining_history, finetuning_history, baseline_history):
    for metric_key, metric_name in zip(["acc", "loss"], ["accuracy", "loss"]):
        plt.figure(figsize=(8, 5), dpi=100)
        plt.plot(
            baseline_history.history[f"val_{metric_key}"],
            label="监督学习基线",
        )
        plt.plot(
            pretraining_history.history[f"val_p_{metric_key}"],
            label="自监督预训练",
        )
        plt.plot(
            finetuning_history.history[f"val_{metric_key}"],
            label="监督微调",
        )
        plt.legend()
        plt.title(f"训练过程中的分类{metric_name}")
        plt.xlabel("周期")
        plt.ylabel(f"验证{metric_name}")

# 绘制训练曲线
plot_training_curves(pretraining_history, finetuning_history, baseline_history)

# 评估模型性能
# 通常,评估自监督学习方法的一种流行方法是在训练好的模型特征上学习线性分类器,并在未见过的图像上评估分类器。
# 其他方法通常包括在源数据集或目标数据集上进行微调,这些数据集有5%或10%的标签存在。
# 您可以使用我们刚刚训练的骨干网络进行任何下游任务,例如图像分类、分割或检测,其中骨干模型通常使用监督学习进行预训练。

最近更新

  1. TCP协议是安全的吗?

    2024-06-15 05:08:02       14 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-15 05:08:02       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-15 05:08:02       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-15 05:08:02       18 阅读

热门阅读

  1. 大数据开发语言Scala(一) - Scala入门

    2024-06-15 05:08:02       5 阅读
  2. C# 事件(Event)定义及其使用

    2024-06-15 05:08:02       4 阅读
  3. 一文搞懂OPC质量码

    2024-06-15 05:08:02       11 阅读
  4. MySQL(7)

    2024-06-15 05:08:02       8 阅读
  5. 1606 - 求一个两位数倒序的结果

    2024-06-15 05:08:02       8 阅读
  6. LeetCode 2848. Points That Intersect With Cars

    2024-06-15 05:08:02       7 阅读