第8讲:自监督视觉大模型——DINO与DINOv2

一、从"教小孩认动物"开始

1.1 监督学习的困境

想象你要教一个小孩认识世界上的动物:

监督学习方式:
  你指着图片说:"这是猫" → 小孩记住
  你指着图片说:"这是狗" → 小孩记住
  ...
  问题:
    1. 你需要给每张图打标签(累死人!)
    2. 标签可能出错(你把"猞猁"标成"猫")
    3. 没见过"袋鼠"?小孩不认识!

深度学习同样的问题

问题 说明 后果
标注成本高 ImageNet-1k有130万张图,人工标注耗资数百万美元 小团队玩不起
标注错误 人也会标错,错误标签误导模型 模型学到错误关联
泛化性差 只能识别训练时见过的类别 遇到新类别就傻眼
语义局限 标签是离散的(猫/狗),不是连续的语义 无法理解"像猫但更大的动物"

1.2 自监督学习:让模型"自学"

小孩的自学方式:
  不需要你告诉它"这是猫"
  它自己观察:
    - "这张图和那张图有相似的毛茸茸纹理"
    - "这个动物和那个动物都有尖耳朵"
    - "这个物体从正面看和侧面看,还是同一个东西"
  
  自己总结规律 → 形成"视觉常识"

自监督学习的核心思想

从数据本身构造"伪任务",让模型自己学。不需要人工标签!

经典伪任务

伪任务 怎么做 模型学到什么
拼图 把图切成9块,打乱,让模型还原 空间关系、物体结构
着色 把彩色图变灰度,让模型还原颜色 颜色与物体的关联
预测缺失块 遮住图的一部分,让模型填充 上下文理解、纹理连续性
对比学习 同一张图做不同变换,让模型认出来 不变性特征、语义理解

二、DINO的核心:知识蒸馏的自监督

2.1 什么是知识蒸馏?

传统老师教学生:
  老师知道正确答案 → 直接告诉学生
  学生死记硬背

知识蒸馏:
  老师不直接给答案,而是给"软标签"
  
  例子:识别"猫"
    硬标签:这是猫(100%)
    软标签:80%猫 + 15%狐狸 + 5%狗
    
  软标签包含更多信息:
    "猫和狐狸有点像(尖耳朵、毛茸茸)"
    "猫和狗也有点像(四足、宠物)"
  
  学生从软标签中学到"相似性关系",比死记硬背更好

2.2 DINO的自蒸馏:自己教自己

DINO = DIstillation with NO labels(无标签蒸馏)

架构:
  ┌─────────────────┐     ┌─────────────────┐
  │   Student(学生) │ ←── │   Teacher(老师) │
  │   (在线网络)     │     │   (动量网络)     │
  │                 │     │                 │
  │  输入: 局部裁剪图   │     │  输入: 全局裁剪图   │
  │  输出: 特征 + 概率  │     │  输出: 特征 + 概率  │
  │                 │     │                 │
  │  参数: 正常反向传播  │     │  参数: Student的指数移动平均 │
  │  训练目标: 匹配Teacher│     │  (不直接训练,只复制)     │
  └─────────────────┘     └─────────────────┘
           │                        │
           └────────┬───────────────┘
                    ↓
              损失 = Student输出 vs Teacher输出的差异
                    ↓
              Student参数更新(反向传播)
              Teacher参数 = 0.9996 × Teacher + 0.0004 × Student

关键设计

组件 作用 为什么有效
Student 在线学习的主网络 直接训练,快速适应
Teacher 动量更新的目标网络 提供更稳定的"软标签",避免Student自举
多Crop 同一张图的不同裁剪/变换 让模型学会"不同视角看同一物体"
Centering 对输出做中心化处理 防止模型崩溃(所有输出都一样)

2.3 多Crop策略:让模型"见多识广"

一张原图 → 生成多个"视角":

全局视图(2个):
  ┌─────────────────────┐
  │                     │
  │    原图完整视图       │
  │    (224×224)       │
  │                     │
  └─────────────────────┘
  
  变换1:随机裁剪 + 颜色抖动 + 高斯模糊
  变换2:随机裁剪 + 灰度化 + 锐化

局部视图(4-8个):
  ┌─────┐ ┌─────┐ ┌─────┐
  │ 局部1│ │ 局部2│ │ 局部3│
  │ 96×96│ │ 96×96│ │ 96×96│
  └─────┘ └─────┘ └─────┘
  
  只包含物体的部分区域
  变换:更激进的裁剪 + 颜色抖动

Teacher看全局视图(理解整体)
Student看局部视图(理解细节)
目标:Student从局部猜全局,被迫学语义特征

为什么这样设计?

如果Student只看到"猫耳朵"的局部,却要预测整张图的特征,它必须学会"耳朵→猫"的语义关联,而不是死记硬背像素。


2.4 防止模型崩溃:Centering和Sharpening

崩溃现象:模型偷懒,对所有输入输出相同的向量。

崩溃前:
  猫图 → [0.8, 0.1, 0.1]  (识别为猫)
  狗图 → [0.1, 0.8, 0.1]  (识别为狗)

崩溃后:
  猫图 → [0.33, 0.33, 0.33]  (和稀泥)
  狗图 → [0.33, 0.33, 0.33]  (和稀泥)
  
  损失=0,但什么都没学到!

DINO的解决方案

1. Centering(中心化):
   Teacher的输出减去均值
   防止某个维度 dominate

2. Sharpening(锐化):
   用低温度Softmax(temperature < 1)
   让概率分布更"尖锐",强迫模型做选择
   
   Softmax(x/T), T=0.1时:
     [2, 1, 0.5] → [0.90, 0.09, 0.01]  (更尖锐)
   
   Softmax(x/T), T=1时:
     [2, 1, 0.5] → [0.60, 0.30, 0.10]  (更平缓)

三、DINOv2的升级:从好到更好

3.1 DINO的问题

问题 说明
特征质量有限 在ImageNet上线性分类不错,但下游任务(检测、分割)提升有限
分辨率固定 训练用224×224,高分辨率图像特征质量下降
数据规模 用ImageNet-1k,数据量相对较小
训练效率 需要多GPU长时间训练,成本高

3.2 DINOv2的四大升级

升级1:数据规模爆炸
  DINO:   ImageNet-1k(130万张,1000类)
  DINOv2: LVD-142M(1.42亿张,无标签!)
  
  LVD来源:
    - ImageNet(去标签)
    - Google Landmarks(地标图)
    - 内部数据集(约1亿张)
    
  效果:见过更多视觉模式,特征更通用

升级2:更强大的正则化
  DINO:   只用Centering + Sharpening
  DINOv2: 加入Sinkhorn-Knopp分配(类似聚类)
           + 特征归一化改进
  
  效果:训练更稳定,特征质量更高

升级3:多分辨率训练
  DINO:   固定224×224
  DINOv2: 随机分辨率(224到518)
  
  效果:高分辨率图像的特征质量大幅提升

升级4:更长的训练 + 更大的模型
  DINO:   ViT-Base/16,300 epoch
  DINOv2: ViT-g/14(1.1B参数),500+ epoch
  
  效果:特征提取能力达到SOTA

3.3 DINOv2的性能:线性探测SOTA

线性探测(Linear Probing):冻结预训练特征,只训练最后一层线性分类器。

模型 预训练数据 参数量 ImageNet Top-1
监督ResNet-50 ImageNet-1k 25M 76.1%
SimCLR v2 ImageNet-1k 152M 80.3%
DINO ImageNet-1k 86M 78.2%
DINOv2 LVD-142M 86M 84.5%
DINOv2-g LVD-142M 1.1B 86.4%

DINOv2用无标签数据,超越了用标签数据训练的模型!


四、动手实验:用DINOv2提取特征

4.1 环境准备

# 安装DINOv2(Meta官方实现)
pip install dinov2

# 或者用Hugging Face的便捷版本
pip install transformers

# 如果需要本地运行,下载预训练权重
# 从 https://github.com/facebookresearch/dinov2 获取

4.2 实验1:提取图像特征

import torch
import torchvision.transforms as transforms
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

print("=" * 60)
print("【实验1】DINOv2特征提取")
print("=" * 60)

# 使用Hugging Face的DINOv2(无需额外安装)
try:
    from transformers import AutoImageProcessor, AutoModel
    DINO_AVAILABLE = True
except ImportError:
    DINO_AVAILABLE = False
    print("请安装transformers: pip install transformers")

if DINO_AVAILABLE:
    # 加载DINOv2模型
    model_name = "facebook/dinov2-base"  # 可选: dinov2-small/base/large/giant
    
    print(f"正在加载 {model_name}...")
    processor = AutoImageProcessor.from_pretrained(model_name)
    model = AutoModel.from_pretrained(model_name)
    
    device = "cuda" if torch.cuda.is_available() else "cpu"
    model = model.to(device)
    model.eval()
    
    print(f"✅ 模型加载完成!设备:{device}")
    print(f"参数量:{sum(p.numel() for p in model.parameters())/1e6:.0f}M")
    
    # 准备测试图像
    # 创建模拟图像(实际使用时替换为真实图片)
    np.random.seed(42)
    
    # 模拟3张不同类别的图
    images = []
    
    # 图1:模拟"猫"(暖色调,有纹理)
    img1 = np.zeros((224, 224, 3), dtype=np.uint8)
    img1[:, :, 0] = 200  # 红色通道
    img1[50:150, 50:150] = [255, 200, 150]  # 中心亮斑
    images.append(Image.fromarray(img1))
    
    # 图2:模拟"狗"(冷色调,不同纹理)
    img2 = np.zeros((224, 224, 3), dtype=np.uint8)
    img2[:, :, 2] = 200  # 蓝色通道
    for i in range(0, 224, 20):
        img2[i:i+10, :] = [100, 100, 255]  # 条纹
    images.append(Image.fromarray(img2))
    
    # 图3:模拟"风景"(绿色调)
    img3 = np.zeros((224, 224, 3), dtype=np.uint8)
    img3[:, :, 1] = 180  # 绿色通道
    img3[100:, :] = [100, 200, 100]  # 下半部分深绿
    images.append(Image.fromarray(img3))
    
    # 提取特征
    features = []
    
    print("\n提取特征...")
    for i, img in enumerate(images):
        # 预处理
        inputs = processor(images=img, return_tensors="pt")
        inputs = {k: v.to(device) for k, v in inputs.items()}
        
        # 前向传播
        with torch.no_grad():
            outputs = model(**inputs)
        
        # 取[CLS]标记的特征(或全局平均池化)
        # DINOv2通常用patch tokens的平均
        feature = outputs.last_hidden_state.mean(dim=1)  # [1, 768]
        features.append(feature.cpu().numpy()[0])
        
        print(f"  图像{i+1}特征形状: {feature.shape}")
        print(f"    前5维: {feature[0, :5].cpu().numpy().round(3)}")
    
    features = np.array(features)  # [3, 768]
    print(f"\n特征矩阵形状: {features.shape}")
    
    # 可视化特征相似度
    from sklearn.metrics.pairwise import cosine_similarity
    
    similarity = cosine_similarity(features)
    
    print("\n余弦相似度矩阵:")
    print("        图1    图2    图3")
    for i in range(3):
        print(f"  图{i+1}: {similarity[i, 0]:.3f}  {similarity[i, 1]:.3f}  {similarity[i, 2]:.3f}")
    
    # 可视化
    fig, axes = plt.subplots(1, 3, figsize=(12, 4))
    titles = ['Image 1 (Warm)', 'Image 2 (Cool)', 'Image 3 (Green)']
    
    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img)
        axes[i].set_title(title)
        axes[i].axis('off')
    
    plt.tight_layout()
    plt.savefig('/mnt/agents/output/dinov2_feature_images.png', dpi=150)
    plt.show()
    
    # 热力图
    fig, ax = plt.subplots(figsize=(6, 5))
    im = ax.imshow(similarity, cmap='hot', vmin=0, vmax=1)
    ax.set_xticks([0, 1, 2])
    ax.set_yticks([0, 1, 2])
    ax.set_xticklabels(['Img1', 'Img2', 'Img3'])
    ax.set_yticklabels(['Img1', 'Img2', 'Img3'])
    ax.set_title('Feature Similarity (Cosine)')
    plt.colorbar(im, ax=ax, label='Similarity')
    
    # 标注数值
    for i in range(3):
        for j in range(3):
            ax.text(j, i, f'{similarity[i, j]:.2f}', 
                   ha='center', va='center', 
                   color='white' if similarity[i, j] > 0.5 else 'black',
                   fontsize=12, weight='bold')
    
    plt.tight_layout()
    plt.savefig('/mnt/agents/output/dinov2_similarity_heatmap.png', dpi=150)
    plt.show()
    
    print("\n📊 特征提取结果已保存!")
    print("""
解读:
  对角线=1.0(自己和自己最像)
  图1 vs 图2:颜色差异大,相似度应该较低
  图1 vs 图3:都是暖/绿色调,可能略高
  (注意:模拟图的特征可能不明显,真实图像效果更显著)
    """)

print("\n" + "=" * 60)

4.3 实验2:以图搜图(Image Retrieval)

print("\n" + "=" * 60)
print("【实验2】以图搜图:DINOv2特征检索")
print("=" * 60)

if DINO_AVAILABLE:
    # 构建模拟数据库
    print("构建图像数据库(10张模拟图)...")
    
    np.random.seed(123)
    database_images = []
    database_features = []
    
    # 生成10张不同特征的图
    for i in range(10):
        img = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
        # 给每张图独特特征
        img[:, :, i % 3] = np.clip(img[:, :, i % 3] + 100, 0, 255)
        
        pil_img = Image.fromarray(img)
        database_images.append(pil_img)
        
        # 提取特征
        inputs = processor(images=pil_img, return_tensors="pt")
        inputs = {k: v.to(device) for k, v in inputs.items()}
        
        with torch.no_grad():
            outputs = model(**inputs)
        feat = outputs.last_hidden_state.mean(dim=1).cpu().numpy()[0]
        database_features.append(feat)
    
    database_features = np.array(database_features)  # [10, 768]
    
    print(f"数据库: {len(database_images)} 张图, 特征维度: {database_features.shape[1]}")
    
    # 查询图(用数据库中的第3张,或新图)
    query_idx = 3
    query_img = database_images[query_idx]
    query_feat = database_features[query_idx]
    
    print(f"\n查询图: 数据库第{query_idx+1}张")
    
    # 计算与数据库所有图的相似度
    similarities = cosine_similarity(query_feat.reshape(1, -1), database_features)[0]
    
    # 排序
    top_k = 5
    top_indices = np.argsort(similarities)[::-1][:top_k]
    
    print(f"\nTop-{top_k} 检索结果:")
    for rank, idx in enumerate(top_indices, 1):
        marker = " ← 查询图" if idx == query_idx else ""
        print(f"  第{rank}名: 数据库图{idx+1}, 相似度={similarities[idx]:.4f}{marker}")
    
    # 可视化检索结果
    fig, axes = plt.subplots(2, top_k, figsize=(15, 6))
    
    # 查询图
    axes[0, 0].imshow(query_img)
    axes[0, 0].set_title('Query Image', fontsize=12, weight='bold')
    axes[0, 0].axis('off')
    
    # 隐藏其他查询行
    for j in range(1, top_k):
        axes[0, j].axis('off')
    
    # 检索结果
    for j, idx in enumerate(top_indices):
        axes[1, j].imshow(database_images[idx])
        color = 'green' if idx == query_idx else 'blue'
        axes[1, j].set_title(f'Rank {j+1}\nSim: {similarities[idx]:.3f}', 
                            color=color, fontsize=10)
        axes[1, j].axis('off')
    
    plt.suptitle('Image Retrieval with DINOv2 Features', fontsize=14, weight='bold')
    plt.tight_layout()
    plt.savefig('/mnt/agents/output/dinov2_image_retrieval.png', dpi=150)
    plt.show()
    
    print("\n📊 以图搜图结果已保存!")
    print("""
实际应用:
  1. 电商:用户上传照片,找相似商品
  2. 相册:自动归类相似照片
  3. 版权:检测相似/重复图片
  4. 医学:找相似病例影像
    """)

print("\n" + "=" * 60)

4.4 实验3:特征可视化(PCA降维)

print("\n" + "=" * 60)
print("【实验3】特征空间可视化(PCA)")
print("=" * 60)

if DINO_AVAILABLE:
    from sklearn.decomposition import PCA
    
    # 生成更多样化的模拟数据
    np.random.seed(456)
    
    # 3个"类别",每类5张图
    n_per_class = 5
    n_classes = 3
    
    all_images = []
    all_features = []
    all_labels = []
    
    for cls in range(n_classes):
        for _ in range(n_per_class):
            # 每类有独特颜色特征
            img = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
            
            if cls == 0:  # "红色类"
                img[:, :, 0] = np.clip(img[:, :, 0] + 150, 0, 255)
            elif cls == 1:  # "蓝色类"
                img[:, :, 2] = np.clip(img[:, :, 2] + 150, 0, 255)
            else:  # "绿色类"
                img[:, :, 1] = np.clip(img[:, :, 1] + 150, 0, 255)
            
            pil_img = Image.fromarray(img)
            all_images.append(pil_img)
            all_labels.append(cls)
            
            # 提取特征
            inputs = processor(images=pil_img, return_tensors="pt")
            inputs = {k: v.to(device) for k, v in inputs.items()}
            
            with torch.no_grad():
                outputs = model(**inputs)
            feat = outputs.last_hidden_state.mean(dim=1).cpu().numpy()[0]
            all_features.append(feat)
    
    all_features = np.array(all_features)  # [15, 768]
    all_labels = np.array(all_labels)
    
    # PCA降维到2D
    pca = PCA(n_components=2)
    features_2d = pca.fit_transform(all_features)
    
    print(f"PCA解释方差比: {pca.explained_variance_ratio_.round(3)}")
    print(f"  第1主成分: {pca.explained_variance_ratio_[0]:.1%}")
    print(f"  第2主成分: {pca.explained_variance_ratio_[1]:.1%}")
    
    # 可视化
    colors = ['red', 'blue', 'green']
    class_names = ['Red Class', 'Blue Class', 'Green Class']
    
    plt.figure(figsize=(10, 8))
    
    for cls in range(n_classes):
        mask = all_labels == cls
        plt.scatter(
            features_2d[mask, 0], 
            features_2d[mask, 1],
            c=colors[cls],
            label=class_names[cls],
            s=200,
            alpha=0.7,
            edgecolors='black',
            linewidths=1.5
        )
    
    plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%})')
    plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%})')
    plt.title('DINOv2 Feature Space (PCA Visualization)\nSame Color Images Cluster Together!')
    plt.legend(fontsize=12)
    plt.grid(True, alpha=0.3)
    
    # 标注每个点
    for i, (x, y) in enumerate(features_2d):
        plt.annotate(f'{i+1}', (x, y), fontsize=8, ha='center', va='center')
    
    plt.tight_layout()
    plt.savefig('/mnt/agents/output/dinov2_feature_pca.png', dpi=150)
    plt.show()
    
    print("\n📊 特征空间可视化已保存!")
    print("""
解读:
  相同颜色的图(相同"类别")在特征空间中聚集
  这说明DINOv2学到了"颜色/纹理"的语义特征
  即使没有标签,模型自动把相似的图聚到一起!
  
  实际应用中:
    - 红色类 = 猫图,蓝色类 = 狗图,绿色类 = 风景图
    - 聚类效果会更明显
    """)

print("\n" + "=" * 60)

4.5 实验4:自蒸馏原理演示

print("\n" + "=" * 60)
print("【实验4】自蒸馏原理演示(简化版)")
print("=" * 60)

import torch.nn as nn
import torch.nn.functional as F

class SimpleDINO(nn.Module):
    """
    极度简化的DINO演示
    实际DINO用ViT,这里用MLP演示原理
    """
    def __init__(self, input_dim=768, hidden_dim=512, output_dim=256):
        super().__init__()
        
        # Student网络(在线)
        self.student = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, output_dim)
        )
        
        # Teacher网络(动量更新)
        self.teacher = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, output_dim)
        )
        
        # 初始化Teacher = Student
        self._update_teacher(m=1.0)
        
        self.momentum = 0.999  # 动量系数
    
    def _update_teacher(self, m=0.999):
        """动量更新:Teacher = m * Teacher + (1-m) * Student"""
        for param_t, param_s in zip(self.teacher.parameters(), 
                                     self.student.parameters()):
            param_t.data = m * param_t.data + (1 - m) * param_s.data
    
    def forward(self, x_student, x_teacher):
        """
        x_student: 局部裁剪的特征
        x_teacher: 全局裁剪的特征
        """
        # Student预测
        s_out = self.student(x_student)
        
        # Teacher预测(不计算梯度)
        with torch.no_grad():
            t_out = self.teacher(x_teacher)
        
        return s_out, t_out
    
    def compute_loss(self, s_out, t_out, temperature=0.1):
        """
        损失:Student输出 vs Teacher输出的交叉熵
        """
        # 归一化
        s_out = F.normalize(s_out, dim=-1)
        t_out = F.normalize(t_out, dim=-1)
        
        # 温度缩放
        s_logits = s_out / temperature
        t_logits = t_out / temperature
        
        # Teacher生成软标签
        t_probs = F.softmax(t_logits, dim=-1)
        
        # Student的log softmax
        s_log_probs = F.log_softmax(s_logits, dim=-1)
        
        # 交叉熵损失
        loss = -(t_probs * s_log_probs).sum(dim=-1).mean()
        
        return loss

# 演示训练过程
print("创建简化DINO模型...")
dino = SimpleDINO()

# 模拟数据:同一张图的两个视角
torch.manual_seed(42)
x_global = torch.randn(4, 768)  # 全局视图(4张图)
x_local = torch.randn(4, 768)   # 局部视图(对应的4张图)

optimizer = torch.optim.SGD(dino.student.parameters(), lr=0.01)

print("\n训练10步...")
losses = []

for step in range(10):
    # 前向
    s_out, t_out = dino(x_local, x_global)
    
    # 计算损失
    loss = dino.compute_loss(s_out, t_out, temperature=0.1)
    losses.append(loss.item())
    
    # 反向传播(只更新Student)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    # 动量更新Teacher
    dino._update_teacher(m=dino.momentum)
    
    if (step + 1) % 2 == 0:
        print(f"  Step {step+1}: Loss = {loss.item():.4f}")

# 可视化损失曲线
plt.figure(figsize=(8, 5))
plt.plot(range(1, 11), losses, 'o-', linewidth=2, markersize=8)
plt.xlabel('Training Step')
plt.ylabel('Distillation Loss')
plt.title('DINO Self-Distillation: Student Learning from Teacher')
plt.grid(True, alpha=0.3)

# 标注
plt.annotate('Student matches Teacher', 
            xy=(10, losses[-1]), 
            xytext=(7, losses[-1] + 0.5),
            arrowprops=dict(arrowstyle='->', color='red'),
            fontsize=10, color='red')

plt.tight_layout()
plt.savefig('/mnt/agents/output/dino_distillation_loss.png', dpi=150)
plt.show()

print("\n📊 自蒸馏训练曲线已保存!")
print("""
观察:
  损失逐渐下降 → Student逐渐"模仿"Teacher
  Teacher也在缓慢进化(动量更新)
  最终两者形成"共识":对同一张图的不同视角,输出相似特征
""")

# 验证:同一张图的两个视角,特征是否相似?
print("\n验证:局部视图 vs 全局视图特征相似度")
dino.eval()
with torch.no_grad():
    s_feat, t_feat = dino(x_local, x_global)
    
    # 计算余弦相似度
    s_norm = F.normalize(s_feat, dim=-1)
    t_norm = F.normalize(t_feat, dim=-1)
    similarities = (s_norm * t_norm).sum(dim=-1)

print(f"4个样本的相似度: {similarities.numpy().round(3)}")
print(f"平均相似度: {similarities.mean().item():.3f}")
print("  → 相似度高说明模型学到了'视角不变性'!")

print("\n" + "=" * 60)
print("✅ DINO自蒸馏原理演示完成!")
print("=" * 60)

五、应用场景

5.1 以图搜图

流程:
  1. 用DINOv2提取数据库所有图片的特征(离线)
  2. 用户上传查询图,提取特征
  3. 计算与数据库特征的相似度
  4. 返回Top-K相似图片

优势:
  ✅ 无需标签,任何图片都能入库
  ✅ 语义相似(找"看起来像"的,不只是颜色像)
  ✅ 支持跨模态(配合CLIP可以做文本搜图)

5.2 图像聚类

流程:
  1. 提取所有图片的DINOv2特征
  2. 用K-Means或DBSCAN聚类
  3. 自动分组(无需知道类别名)

应用:
  - 相册整理:自动把"猫照"、"旅游照"、"美食照"分开
  - 数据清洗:找出重复/相似图片
  - 异常检测:聚类外的图片可能是异常

5.3 下游任务预训练

DINOv2作为"视觉骨干网络":

图像分类:
  冻结DINOv2 → 只训练分类头 → 少量数据即可高精度

目标检测:
  DINOv2替代ResNet做Backbone → 特征质量更高 → AP提升

语义分割:
  DINOv2特征 + 轻量分割头 → 边界更清晰

深度估计:
  DINOv2特征包含几何信息 → 深度预测更准确

六、核心总结

概念 一句话解释
自监督学习 从数据本身构造任务,无需人工标签
知识蒸馏 老师给软标签,学生学相似性关系
DINO 自蒸馏:Student学Teacher,Teacher是Student的动量平均
多Crop 同一张图的不同视角,强迫学语义特征
Centering/Sharpening 防止模型崩溃,强迫做选择
DINOv2 更大规模数据 + 更强正则化 + 多分辨率 → SOTA特征
线性探测 冻结特征,只训分类头,验证特征质量

七、面试高频题

Q1:DINO和对比学习(SimCLR/MoCo)的区别?

:对比学习需要"正样本对"(同图的不同变换)和"负样本对"(不同图),通过拉大正样本相似度、缩小负样本相似度来学习。DINO是自蒸馏:Student预测Teacher的输出,不需要显式负样本,通过动量Teacher提供稳定目标,Centering防止崩溃。DINO更简洁,且特征更适合下游任务。

Q2:为什么DINOv2的特征质量远超监督预训练?

:1)数据量:LVD-142M vs ImageNet-1k,见过更多视觉模式;2)目标不同:自监督学"视觉本质"(纹理、形状、结构),监督学习只学"分类边界";3)特征丰富度:DINOv2特征包含多尺度信息,监督特征可能过拟合到特定类别。

Q3:动量编码器(Momentum Encoder)的作用?

:Teacher网络用Student的指数移动平均更新,而不是直接训练。这保证了:1)Teacher变化缓慢,提供稳定的优化目标;2)避免Student和Teacher"同流合污"(同时崩溃);3)Teacher集成了Student的历史信息,类似模型集成效果。

Q4:DINOv2在实际部署中的挑战?

:1)模型大(ViT-g/14有1.1B参数),推理慢;2)高分辨率输入(518×518)计算量大;3)特征维度高(1536维),存储和检索成本高。优化方向:模型蒸馏(DINOv2-Small)、量化、特征降维(PCA)、近似最近邻检索(FAISS)。


八、课后作业

作业1:真实图像实验

# 用真实图片(CIFAR-10或你自己的照片)替换模拟图
# 观察:
# 1. 同类图片的特征相似度是否更高?
# 2. 不同角度/光照的同物体,特征是否稳定?
# 3. 语义相似但外观不同的图(如"猫"和"狮子"),特征距离如何?

作业2:特征降维对比

# 对比PCA、t-SNE、UMAP三种降维方法
# 观察DINOv2特征在低维空间的聚类效果
# t-SNE通常效果最好,但计算慢

作业3:思考DINOv2的局限

问题:DINOv2有什么做不到?

提示:
  1. 需要细粒度区分的任务(如"分辨不同品种的狗")?
  2. 需要空间精度的任务(如关键点检测)?
  3. 视频/时序特征?
  4. 与语言结合的多模态理解?

九、下讲预告

第9讲:多模态的"罗塞塔石碑"——CLIP

我们将:

  • 理解"把图像和文本映射到同一空间"的革命思想
  • 探索双塔架构和对比学习
  • 动手用CLIP做零样本图像分类
  • 实现图文相似度计算和图像检索
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐