DINO 就像在让一个“学生”模型通过观察无数物体的局部特写,去向一个更博学、更稳定的“老师”模型学习,最终让学生自己也变成一个能够“窥一斑而知全豹”的特征提取大师。
它通过这种“自我蒸馏”的方式,完美地实现了无监督学习,学到的特征可以直接用于图像分类、目标检测、分割等各种下游任务,并且效果极佳。
DINO 这个非常巧妙的自监督学习算法。
想象一下,你要教一个刚学说话的双胞胎宝宝认识什么是“猫”。
传统方法(有监督学习):
你拿出一大堆猫的照片,每一张都指着说“这是猫”。这方法有效,但给海量图片打标签太费劲了。
之前的对比学习(如MoCo):
你拿出一张猫的照片,然后给它看:
一张同一只猫的不同角度的照片(正样本)。
和一堆狗、汽车、树木的照片(负样本)。
然后告诉宝宝:“找出和第一张最像的那张”。宝宝通过对比,学会了猫的特征。
DINO 的妙招:一个更聪明的“双胞胎学习法”
现在,假设你有一对心灵感应的双胞胎宝宝,一个叫学生,一个叫老师。但他们还不会说话,你也没法直接教他们。
你的训练方法如下:
准备“考卷”:
你随便拿出一张猫的照片。
用两种不同的“滤镜”处理这张照片,生成两张看起来有点不一样但都是同一只猫的图片。
一张细节丰富的“全局视图”给老师看。
一张被裁剪过的“局部特写”(比如只看到猫耳朵和一部分脸)给学生看。
第一轮“考试”:
学生看到局部特写,挠挠头说:“我觉得……这应该是一个毛茸茸的、尖耳朵的东西。”(输出一个预测概率分布)
老师看到全局图,思考后说:“我认为,这毫无疑问是一只猫。”(也输出一个预测概率分布)
一开始,他们俩的答案可能天差地别。
关键的学习规则:
你的目标是:让“学生”的答案向“老师”的答案看齐。你只更新学生的大脑,让他努力去理解:“为什么我看到一个局部,老师就能看出是整只猫?”
“老师”是怎么变的? —— “老师”的智慧,是“学生”智慧的缓慢影子。
一开始,老师和学生的知识水平一样。
每次学生学完一点新东西,老师并不会立刻全盘接受,而是非常缓慢地向学生靠近一点。
用公式表示就是:老师的新参数 = 0.99 * 老师的旧参数 + 0.01 * 学生的新参数
这保证了老师的知识更稳定、更全面,不会因为学生看到一个局部特写(比如一个猫爪子)就突然认为所有东西都是猫爪子。
反复训练:
你给双胞胎看了成千上万张猫、狗、车……的照片,每次都遵循这个流程。
学生为了能回答上“老师”的问题,被迫学会了“管中窥豹,可见一斑” 的能力。即使只看到一个局部,它也必须去推理和想象出这个物体的全局特征和本质结构。
老师因为总是看到更完整的图片,并且更新缓慢,其知识代表了更稳定、更全局的“真理”。
DINO 为什么强大?
它不需要“负样本”:不像MoCo需要一堆“不像”的图片来对比。DINO只关心学生和老师对同一事物不同视角的看法是否一致。这简化了训练流程。
它学会了优秀的“特征表示”:经过这种训练,学生模型成了一个“火眼金睛”。它学会了抓住图像中最本质的特征,因此它提取的特征质量非常高。
惊人的“涌现属性”:研究人员发现,DINO模型在训练过程中,自然而然地学会了图像的语义分割能力。也就是说,它能自动把图片里的物体(比如猫)和背景分离开,而没有任何人教过它什么叫“分割”!这正是因为它为了完成“从局部推理全局”的任务,必须自己去理解物体的边界。总结一下 DINO 的核心思想:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
from torchvision.models import resnet50
import numpy as np
class DINOHead(nn.Module):
"""DINO 的投影头 - 将特征映射到对比学习空间"""
def __init__(self, in_dim, out_dim=65536, hidden_dim=2048, bottleneck_dim=256):
super().__init__()
self.mlp = nn.Sequential(
nn.Linear(in_dim, hidden_dim),
nn.GELU(),
nn.Linear(hidden_dim, hidden_dim),
nn.GELU(),
nn.Linear(hidden_dim, bottleneck_dim),
)
self.last_layer = nn.Linear(bottleneck_dim, out_dim, bias=False)
def forward(self, x):
x = self.mlp(x)
# L2 归一化
x = F.normalize(x, dim=-1, p=2)
x = self.last_layer(x)
return x
class DINO(nn.Module):
def __init__(self, backbone, embedding_dim=2048, student_temp=0.1,
teacher_temp=0.04, momentum_teacher=0.996):
super().__init__()
self.student_temp = student_temp
self.teacher_temp = teacher_temp
self.momentum_teacher = momentum_teacher
# 创建学生和教师网络
self.student_backbone = backbone(pretrained=False)
self.teacher_backbone = backbone(pretrained=False)
# 替换分类头为投影头
in_features = self.student_backbone.fc.in_features
self.student_backbone.fc = nn.Identity()
self.teacher_backbone.fc = nn.Identity()
self.student_projector = DINOHead(in_features, embedding_dim)
self.teacher_projector = DINOHead(in_features, embedding_dim)
# 初始化教师网络与学生相同
self._init_teacher()
# 冻结教师网络 - 只通过动量更新
for param in self.teacher_backbone.parameters():
param.requires_grad = False
for param in self.teacher_projector.parameters():
param.requires_grad = False
def _init_teacher(self):
"""初始化教师网络参数与学生相同"""
for param_s, param_t in zip(self.student_backbone.parameters(),
self.teacher_backbone.parameters()):
param_t.data.copy_(param_s.data)
for param_s, param_t in zip(self.student_projector.parameters(),
self.teacher_projector.parameters()):
param_t.data.copy_(param_s.data)
@torch.no_grad()
def _update_teacher(self):
"""动量更新教师网络"""
for param_s, param_t in zip(self.student_backbone.parameters(),
self.teacher_backbone.parameters()):
param_t.data = self.momentum_teacher * param_t.data + (1 - self.momentum_teacher) * param_s.data
for param_s, param_t in zip(self.student_projector.parameters(),
self.teacher_projector.parameters()):
param_t.data = self.momentum_teacher * param_t.data + (1 - self.momentum_teacher) * param_s.data
def forward(self, student_views, teacher_views):
"""
student_views: 给学生网络的多个裁剪视图 [view1, view2, ...]
teacher_views: 给教师网络的多个裁剪视图 [view1, view2, ...]
"""
# 教师网络前向传播 (不计算梯度)
teacher_outputs = []
with torch.no_grad():
for view in teacher_views:
features = self.teacher_backbone(view)
output = self.teacher_projector(features)
# 应用教师温度系数并中心化
output = F.softmax(output / self.teacher_temp, dim=-1)
teacher_outputs.append(output)
# 学生网络前向传播
student_outputs = []
for view in student_views:
features = self.student_backbone(view)
output = self.student_projector(features)
# 应用学生温度系数
output = F.log_softmax(output / self.student_temp, dim=-1)
student_outputs.append(output)
return student_outputs, teacher_outputs
class DINOLoss(nn.Module):
"""DINO 损失函数 - 让学生预测匹配教师预测"""
def __init__(self):
super().__init__()
def forward(self, student_outputs, teacher_outputs):
total_loss = 0
n_views = len(student_outputs)
# 计算所有学生视图与所有教师视图之间的交叉熵损失
for i, student_out in enumerate(student_outputs):
for j, teacher_out in enumerate(teacher_outputs):
if i != j: # 避免相同视图对
loss = -torch.sum(teacher_out * student_out, dim=-1)
total_loss += loss.mean()
# 平均损失
total_loss /= (n_views * (n_views - 1))
return total_loss
def get_dino_augmentations():
"""获取 DINO 专用的多裁剪数据增强"""
# 全局视图 (大尺寸裁剪)
global_transform = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.4, 1.0)),
transforms.RandomHorizontalFlip(),
transforms.RandomApply([
transforms.ColorJitter(0.4, 0.4, 0.2, 0.1)
], p=0.8),
transforms.RandomGrayscale(p=0.2),
transforms.RandomApply([transforms.GaussianBlur(3)], p=0.5),
transforms.RandomSolarize(threshold=0.5, p=0.1),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# 局部视图 (小尺寸裁剪)
local_transform = transforms.Compose([
transforms.RandomResizedCrop(96, scale=(0.05, 0.4)), # 更小的裁剪
transforms.RandomHorizontalFlip(),
transforms.RandomApply([
transforms.ColorJitter(0.4, 0.4, 0.2, 0.1)
], p=0.8),
transforms.RandomGrayscale(p=0.2),
transforms.RandomApply([transforms.GaussianBlur(3)], p=0.5),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
return global_transform, local_transform
def train_dino_epoch(model, dataloader, optimizer, criterion, device, n_global_views=2, n_local_views=6):
"""训练一个 epoch"""
model.train()
total_loss = 0
for batch_idx, (images, _) in enumerate(dataloader):
images = images.to(device)
batch_size = images.shape[0]
# 准备多裁剪视图
global_transform, local_transform = get_dino_augmentations()
# 生成全局视图 (给教师和学生)
teacher_views = []
student_views = []
# 教师网络使用全局视图
for _ in range(n_global_views):
teacher_views.append(global_transform(images))
# 学生网络使用全局视图 + 局部视图
for _ in range(n_global_views):
student_views.append(global_transform(images))
for _ in range(n_local_views):
student_views.append(local_transform(images))
# 前向传播
student_outputs, teacher_outputs = model(student_views, teacher_views)
# 计算损失
loss = criterion(student_outputs, teacher_outputs)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 动量更新教师网络
model._update_teacher()
total_loss += loss.item()
if batch_idx % 50 == 0:
print(f'Batch [{batch_idx}/{len(dataloader)}] Loss: {loss.item():.6f}')
return total_loss / len(dataloader)
# 使用示例
if __name__ == "__main__":
# 设置设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
# 创建 DINO 模型
model = DINO(resnet50, embedding_dim=65536).to(device)
# 创建优化器 (使用 LARS 优化器效果更好,这里用 Adam 简化)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
criterion = DINOLoss()
# 创建假数据加载器 (实际使用时替换为真实数据集)
class DummyDataset(torch.utils.data.Dataset):
def __init__(self, length=1000):
self.length = length
def __len__(self):
return self.length
def __getitem__(self, idx):
# 返回随机图像
dummy_img = torch.randn(3, 224, 224)
return dummy_img, 0 # 返回图像和伪标签
dataset = DummyDataset(length=1000)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=32, shuffle=True)
# 训练几个 epoch
print("开始训练 DINO...")
for epoch in range(5):
avg_loss = train_dino_epoch(model, dataloader, optimizer, criterion, device)
print(f'Epoch [{epoch+1}/5] Average Loss: {avg_loss:.6f}')
# 训练完成后,可以使用教师网络作为特征提取器
feature_extractor = model.teacher_backbone
print("DINO 训练完成!可以使用教师网络进行下游任务。")
# 特征提取示例
def extract_features(model, images):
"""使用教师网络提取特征"""
model.eval()
with torch.no_grad():
features = model.teacher_backbone(images)
return features
# 测试特征提取
test_images = torch.randn(4, 3, 224, 224).to(device)
features = extract_features(model, test_images)
print(f"提取的特征形状: {features.shape}")
关键组件详解
1. 教师-学生架构
python
复制
下载
# 学生网络 - 通过梯度更新
features = self.student_backbone(view)
output = self.student_projector(features)
# 教师网络 - 通过动量更新
with torch.no_grad():
features = self.teacher_backbone(view)
output = self.teacher_projector(features)
2. 动量更新机制
python
复制
下载
@torch.no_grad()
def _update_teacher(self):
"""教师网络缓慢跟踪学生网络"""
for param_s, param_t in zip(self.student_backbone.parameters(),
self.teacher_backbone.parameters()):
param_t.data = self.momentum_teacher * param_t.data + (1 - self.momentum_teacher) * param_s.data
3. 多裁剪策略
python
复制
下载
# 全局视图 - 大尺度裁剪,给教师网络 global_transform = transforms.RandomResizedCrop(224, scale=(0.4, 1.0)) # 局部视图 - 小尺度裁剪,给学生网络 local_transform = transforms.RandomResizedCrop(96, scale=(0.05, 0.4))
4. 损失计算
python
复制
下载
def forward(self, student_outputs, teacher_outputs):
total_loss = 0
# 计算所有视图对之间的交叉熵损失
for i, student_out in enumerate(student_outputs):
for j, teacher_out in enumerate(teacher_outputs):
if i != j: # 避免相同视图
loss = -torch.sum(teacher_out * student_out, dim=-1)
total_loss += loss.mean()
return total_loss
实际应用
训练完成后,你可以这样使用学到的特征:
python
复制
下载
# 1. 图像分类
class LinearClassifier(nn.Module):
def __init__(self, feature_dim, num_classes):
super().__init__()
self.fc = nn.Linear(feature_dim, num_classes)
def forward(self, x):
return self.fc(x)
# 2. 特征相似度计算
def compute_similarity(features1, features2):
"""计算特征之间的余弦相似度"""
features1 = F.normalize(features1, dim=1)
features2 = F.normalize(features2, dim=1)
similarity = torch.mm(features1, features2.t())
return similarity
# 3. 语义分割 (利用 DINO 的注意力图)
def get_attention_maps(model, images):
"""获取 DINO 的注意力图 - 用于分割"""
model.eval()
with torch.no_grad():
# 对于 ViT backbone,可以提取注意力权重
# 这里以 ResNet 为例,返回高级特征图
features = model.teacher_backbone(images)
return features
这个实现包含了 DINO 的核心思想:
-
教师-学生蒸馏:学生向教师学习
-
动量更新:教师网络缓慢演化
-
多裁剪训练:全局+局部视图
-
无负样本对比:通过分布匹配学习
实际使用时,建议:
-
使用更大的 batch size
-
使用 LARS 优化器
-
训练更多 epoch
-
使用 Vision Transformer 作为 backbone 效果更好

4

被折叠的 条评论
为什么被折叠?



