Image Classification
vision
self-supervised
DINO-Classification / prepare_model.py
mathmanu's picture
Add dino model files
5d6cefd verified
Raw
History Blame Contribute Delete
19.4 kB
#!/usr/bin/env python3
"""
Export DINO classification models (backbone + linear head) from PyTorch Hub
to ONNX with fixed input shapes for TI EdgeAI hardware deployment.
Each exported model includes the full DINO backbone and the pretrained linear
classification head, outputting 1000-class ImageNet logits [1, 1000].
Feature extraction follows DINO's eval_linear.py conventions:
ViT-S models : last 4 blocks' CLS tokens concatenated → [B, 384×4 = 1536]
ViT-B models : CLS token + averaged patch tokens (interleaved) → [B, 768×2 = 1536]
ResNet-50 : avgpool output → [B, 2048]
Supported models (backbone + linear head, 1000-class ImageNet):
dino_vits16 - ViT-S/16, 21M params, 77.0% linear top-1, 74.5% k-NN top-1
dino_vits8 - ViT-S/8, 21M params, 79.7% linear top-1, 78.3% k-NN top-1
dino_vitb16 - ViT-B/16, 85M params, 78.2% linear top-1, 76.1% k-NN top-1
dino_vitb8 - ViT-B/8, 85M params, 80.1% linear top-1, 77.4% k-NN top-1
dino_resnet50 - ResNet-50, 23M params, 75.3% linear top-1, 67.5% k-NN top-1
Usage:
python prepare_model.py --model dino_vits16
python prepare_model.py --model dino_vitb16 --no-simplifier
python prepare_model.py --model all
"""
import sys
import subprocess
import tempfile
from pathlib import Path
# n_last_blocks / avgpool follow DINO's eval_linear.py default args per arch:
# ViT-S: n_last_blocks=4, avgpool=False → linear_in = 384 * 4 = 1536
# ViT-B: n_last_blocks=1, avgpool=True → linear_in = 768 * 2 = 1536
# ResNet: direct avgpool output → linear_in = 2048
SUPPORTED_MODELS = {
'dino_vits16': {
'arch': 'ViT-S/16', 'params': '21M', 'accuracy_top1': 77.0, 'knn_top1': 74.5,
'n_last_blocks': 4, 'avgpool': False, 'linear_in': 384 * 4,
},
'dino_vits8': {
'arch': 'ViT-S/8', 'params': '21M', 'accuracy_top1': 79.7, 'knn_top1': 78.3,
'n_last_blocks': 4, 'avgpool': False, 'linear_in': 384 * 4,
},
'dino_vitb16': {
'arch': 'ViT-B/16', 'params': '85M', 'accuracy_top1': 78.2, 'knn_top1': 76.1,
'n_last_blocks': 1, 'avgpool': True, 'linear_in': 768 * 2,
},
'dino_vitb8': {
'arch': 'ViT-B/8', 'params': '85M', 'accuracy_top1': 80.1, 'knn_top1': 77.4,
'n_last_blocks': 1, 'avgpool': True, 'linear_in': 768 * 2,
},
'dino_resnet50': {
'arch': 'ResNet-50', 'params': '23M', 'accuracy_top1': 75.3, 'knn_top1': 67.5,
'n_last_blocks': None, 'avgpool': False, 'linear_in': 2048,
},
}
_BASE_URL = 'https://dl.fbaipublicfiles.com/dino/'
LINEAR_WEIGHTS_URLS = {
'dino_vits16': _BASE_URL + 'dino_deitsmall16_pretrain/dino_deitsmall16_linearweights.pth',
'dino_vits8': _BASE_URL + 'dino_deitsmall8_pretrain/dino_deitsmall8_linearweights.pth',
'dino_vitb16': _BASE_URL + 'dino_vitbase16_pretrain/dino_vitbase16_linearweights.pth',
'dino_vitb8': _BASE_URL + 'dino_vitbase8_pretrain/dino_vitbase8_linearweights.pth',
'dino_resnet50': _BASE_URL + 'dino_resnet50_pretrain/dino_resnet50_linearweights.pth',
}
def _ensure_dependencies():
required = {
'onnx': 'onnx',
'onnxsim': 'onnx-simplifier',
'torch': 'torch',
}
for module, package in required.items():
try:
__import__(module)
except ImportError:
print(f"Installing missing dependency: {package}")
subprocess.check_call([sys.executable, '-m', 'pip', 'install', package])
_ensure_dependencies()
import torch
import torch.nn as nn
import onnx
from onnx import shape_inference
import argparse
class _LinearClassifier(nn.Module):
"""Linear head matching DINO's eval_linear.py LinearClassifier structure."""
def __init__(self, in_features, num_classes=1000):
super().__init__()
self.linear = nn.Linear(in_features, num_classes)
def forward(self, x):
return self.linear(x)
class _DinoViTClassifier(nn.Module):
"""
DINO ViT backbone + linear head for classification.
Feature extraction matches eval_linear.py:
- Collects CLS tokens from the last n_last_blocks transformer blocks
- For ViT-B (avgpool=True): interleaves CLS with averaged patch tokens
using the same stack+flatten as the original code, preserving weight
compatibility: [CLS[0], patch[0], CLS[1], patch[1], ...]
"""
def __init__(self, backbone, linear_head, n_last_blocks, avgpool):
super().__init__()
self.backbone = backbone
self.linear_head = linear_head
self.n = n_last_blocks
self.avgpool = avgpool
def forward(self, x):
intermediate = self.backbone.get_intermediate_layers(x, self.n)
feat = torch.cat([layer[:, 0] for layer in intermediate], dim=-1)
if self.avgpool:
# Interleave CLS and patch-average as in eval_linear.py:
# stack → [B, embed, 2] → flatten(1) → [B, embed*2]
patch_avg = torch.mean(intermediate[-1][:, 1:], dim=1)
feat = torch.stack([feat, patch_avg], dim=-1).flatten(1)
return self.linear_head(feat)
class _DinoResNetClassifier(nn.Module):
"""DINO ResNet-50 backbone + linear head for classification."""
def __init__(self, backbone, linear_head):
super().__init__()
self.backbone = backbone
self.linear_head = linear_head
def forward(self, x):
return self.linear_head(self.backbone(x))
def _build_classifier(model_name, info):
"""
Load DINO backbone from PyTorch Hub, load pretrained linear weights,
and return a combined classifier module ready for ONNX export.
Returns the combined nn.Module or None on failure.
"""
print(f"\nLoading backbone from PyTorch Hub:")
print(f" torch.hub.load('facebookresearch/dino:main', '{model_name}')")
try:
backbone = torch.hub.load('facebookresearch/dino:main', model_name, pretrained=True)
except Exception as e:
print(f"✗ Failed to load backbone: {e}")
print(" Ensure you have an internet connection and PyTorch installed.")
return None
backbone.eval()
print(f"\nDownloading linear weights:")
print(f" URL: {LINEAR_WEIGHTS_URLS[model_name]}")
try:
ckpt = torch.hub.load_state_dict_from_url(
LINEAR_WEIGHTS_URLS[model_name], map_location='cpu', progress=True
)
state_dict = ckpt['state_dict']
# Saved under DDP → strip 'module.' prefix
state_dict = {k.replace('module.', ''): v for k, v in state_dict.items()}
except Exception as e:
print(f"✗ Failed to download linear weights: {e}")
return None
linear_head = _LinearClassifier(info['linear_in'])
try:
linear_head.load_state_dict(state_dict, strict=True)
print(f"✓ Linear weights loaded ({info['linear_in']} → 1000 classes)")
except Exception as e:
print(f"✗ Failed to load linear weights into head: {e}")
return None
linear_head.eval()
if info['n_last_blocks'] is None:
model = _DinoResNetClassifier(backbone, linear_head)
else:
model = _DinoViTClassifier(backbone, linear_head, info['n_last_blocks'], info['avgpool'])
model.eval()
return model
def export_to_onnx(model_name, output_path, height=224, width=224):
"""
Build the DINO backbone + linear head and export to ONNX (opset 17).
"""
info = SUPPORTED_MODELS[model_name]
print(f"\nDINO Model Export")
print("=" * 80)
print(f"Model: {model_name} ({info['arch']})")
print(f"Params: {info['params']}")
print(f"Top-1 (lin): {info['accuracy_top1']}%")
print(f"Top-1 (k-NN): {info['knn_top1']}%")
print(f"Input shape: [1, 3, {height}, {width}]")
print(f"Output shape: [1, 1000]")
model = _build_classifier(model_name, info)
if model is None:
return False
dummy_input = torch.randn(1, 3, height, width)
# Sanity-check output shape before export
with torch.no_grad():
out = model(dummy_input)
if list(out.shape) != [1, 1000]:
print(f"✗ Unexpected output shape: {list(out.shape)}, expected [1, 1000]")
return False
print(f"\n✓ Output shape verified: {list(out.shape)}")
print(f"\nExporting to ONNX (opset 17):")
print(f" Output: {output_path}")
try:
with tempfile.TemporaryDirectory() as tmpdir:
tmp_onnx = Path(tmpdir) / f"{model_name}.onnx"
torch.onnx.export(
model,
dummy_input,
str(tmp_onnx),
export_params=True,
opset_version=17,
do_constant_folding=True,
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'},
},
)
probe = onnx.load(str(tmp_onnx), load_external_data=False)
has_external = any(
t.data_location == onnx.TensorProto.EXTERNAL
for t in probe.graph.initializer
)
if has_external:
# Should not happen for DINO models (<2 GB), but handle gracefully
print("\nMerging external tensor data...")
exported = onnx.load(str(tmp_onnx))
else:
exported = onnx.load(str(tmp_onnx))
onnx.save(exported, str(output_path))
except Exception as e:
print(f"✗ ONNX export failed: {e}")
return False
if not output_path.exists():
print("✗ Export failed: output file not created")
return False
file_size = output_path.stat().st_size
print(f"✓ Export completed: {file_size:,} bytes ({file_size / 1024 / 1024:.2f} MB)")
return True
def fix_model_shape(model_path, output_path, batch_size=1, channels=3, height=224, width=224, use_simplifier=True):
"""
Convert dynamic ONNX model input shape to fixed shape in all layers.
"""
print(f"\nFixing Model Shapes:")
print("=" * 80)
print(f"Input model: {model_path}")
print(f"Output model: {output_path}")
model = onnx.load(str(model_path))
graph = model.graph
input_tensor = None
for inp in graph.input:
if any(init.name == inp.name for init in graph.initializer):
continue
input_tensor = inp
break
if input_tensor is None:
print("✗ Error: No input tensor found!")
return False
original_shape = []
for dim in input_tensor.type.tensor_type.shape.dim:
if dim.dim_value:
original_shape.append(str(dim.dim_value))
elif dim.dim_param:
original_shape.append(f"'{dim.dim_param}'")
else:
original_shape.append("?")
print(f"Original shape: [{', '.join(original_shape)}]")
new_shape = [batch_size, channels, height, width]
print(f"Fixed shape: {new_shape}")
input_tensor.type.tensor_type.shape.ClearField('dim')
for dim_value in new_shape:
dim = input_tensor.type.tensor_type.shape.dim.add()
dim.dim_value = dim_value
try:
model = shape_inference.infer_shapes(model)
print(f"✓ Propagated shapes through {len(model.graph.value_info)} intermediate tensors")
except Exception as e:
print(f"⚠ Warning: Shape inference issue: {e}")
try:
onnx.checker.check_model(model)
print("✓ Model validation passed")
except Exception as e:
print(f"✗ Model validation failed: {e}")
return False
if use_simplifier:
try:
import onnxsim
model_simplified, check = onnxsim.simplify(
model,
check_n=3,
perform_optimization=True,
skip_fuse_bn=False,
overwrite_input_shapes={input_tensor.name: new_shape},
)
if check:
orig_nodes = len(graph.node)
simp_nodes = len(model_simplified.graph.node)
model = model_simplified
print(f"✓ Model simplified ({orig_nodes} → {simp_nodes} nodes)")
else:
print("⚠ Simplification validation failed, using non-simplified version")
except ImportError:
print("⚠ onnx-simplifier not installed, skipping")
except Exception as e:
print(f"⚠ Simplification failed: {e}, continuing without")
onnx.save(model, str(output_path))
output_size = output_path.stat().st_size
print(f"\n✓ Saved: {output_path} ({output_size / 1024 / 1024:.2f} MB)")
try:
verified = onnx.load(str(output_path))
onnx.checker.check_model(verified)
for inp in verified.graph.input:
if any(init.name == inp.name for init in verified.graph.initializer):
continue
shape = [dim.dim_value for dim in inp.type.tensor_type.shape.dim]
if all(isinstance(s, int) and s > 0 for s in shape):
print(f"✓ Input '{inp.name}': {shape}")
else:
print(f"⚠ Input '{inp.name}' has dynamic dimensions: {shape}")
print("✨ Success! Fixed model ready for deployment")
return True
except Exception as e:
print(f"✗ Final verification failed: {e}")
return False
def _prepare_single_model(model_name, args, script_dir):
"""Export backbone+head and fix shapes for one model. Returns True on success."""
info = SUPPORTED_MODELS[model_name]
final_output = script_dir / f"{model_name}.onnx"
print(f"\n{'=' * 80}")
print(f"DINO Model Preparation: {model_name}")
print(f"{'=' * 80}")
print(f"Architecture: {info['arch']} | Params: {info['params']}")
print(f"Top-1: {info['accuracy_top1']}% | k-NN: {info['knn_top1']}%")
print(f"Input shape: [{args.batch_size}, {args.channels}, {args.height}, {args.width}]")
if args.skip_export:
if not final_output.exists():
print(f"\n✗ Error: ONNX file not found: {final_output}")
print(" Run without --skip-export to export it first.")
return False
print(f"\nUsing existing ONNX file: {final_output.name}")
else:
if final_output.exists() and not args.force_export:
print(f"\nONNX file already exists: {final_output.name}")
print(f"File size: {final_output.stat().st_size / 1024 / 1024:.2f} MB")
print("Use --force-export to re-export.")
return True
success = export_to_onnx(model_name, final_output, args.height, args.width)
if not success:
return False
success = fix_model_shape(
final_output,
final_output,
batch_size=args.batch_size,
channels=args.channels,
height=args.height,
width=args.width,
use_simplifier=not args.no_simplifier,
)
if success:
print(f"\n{'=' * 80}")
print("COMPLETE!")
print(f"{'=' * 80}")
print(f"Model: {model_name}")
print(f"Output: {final_output.name} ({final_output.stat().st_size / 1024 / 1024:.2f} MB)")
print(f"Config: {model_name}_config.yaml")
else:
print("\n✗ Shape fixing failed")
return success
def main():
parser = argparse.ArgumentParser(
description=(
'Export DINO classification models (backbone + linear head) from PyTorch Hub\n'
'to ONNX format with fixed static shapes for TI EdgeAI hardware deployment.\n'
'\n'
'Each model outputs 1000-class ImageNet logits [1, 1000].\n'
'\n'
'Processing pipeline:\n'
' 1. Load pretrained backbone from torch.hub (facebookresearch/dino:main)\n'
' 2. Download pretrained linear classification weights from Meta AI\n'
' 3. Combine backbone + linear head into a single module\n'
' 4. Export to ONNX (opset 17) with dynamic batch axis\n'
' 5. Fix dynamic input shapes to static [batch, channels, height, width]\n'
' 6. Run ONNX shape inference and onnxsim simplification\n'
' 7. Validate the final model'
),
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Export default model (ViT-S/16)
%(prog)s
# Export a specific variant
%(prog)s --model dino_vitb16
# Export all supported models in sequence
%(prog)s --model all
# Skip onnxsim (faster, larger output file)
%(prog)s --model dino_vits16 --no-simplifier
# Re-run shape inference + onnxsim on an already-exported ONNX file
%(prog)s --model dino_vits16 --skip-export
# Force re-export even if ONNX file exists
%(prog)s --model dino_vits16 --force-export
Available models:
dino_vits16 - ViT-S/16, 21M params, 77.0%% linear top-1 (recommended for edge)
dino_vits8 - ViT-S/8, 21M params, 79.7%% linear top-1
dino_vitb16 - ViT-B/16, 85M params, 78.2%% linear top-1
dino_vitb8 - ViT-B/8, 85M params, 80.1%% linear top-1
dino_resnet50 - ResNet-50, 23M params, 75.3%% linear top-1
"""
)
parser.add_argument(
'--model', type=str, default='dino_vits16',
choices=list(SUPPORTED_MODELS.keys()) + ['all'],
help='Model variant to export, or "all" to export every model (default: dino_vits16)',
)
parser.add_argument('--batch-size', type=int, default=1,
help='Fixed batch size (default: 1)')
parser.add_argument('--channels', type=int, default=3,
help='Number of channels (default: 3)')
parser.add_argument('--height', type=int, default=224,
help='Image height (default: 224)')
parser.add_argument('--width', type=int, default=224,
help='Image width (default: 224)')
parser.add_argument('--force-export', action='store_true',
help='Force re-export even if ONNX file already exists')
parser.add_argument('--skip-export', action='store_true',
help='Skip export, only re-run shape inference + onnxsim on existing ONNX')
parser.add_argument('--no-simplifier', action='store_true',
help='Skip onnx-simplifier (onnxsim) step; shape inference still runs')
args = parser.parse_args()
script_dir = Path(__file__).parent
if args.model == 'all':
models = list(SUPPORTED_MODELS.keys())
print(f"Exporting {len(models)} DINO models...")
results = {}
for model_name in models:
results[model_name] = _prepare_single_model(model_name, args, script_dir)
print(f"\n{'=' * 80}")
print("ALL MODELS SUMMARY")
print(f"{'=' * 80}")
succeeded = [m for m, ok in results.items() if ok]
failed = [m for m, ok in results.items() if not ok]
for m in succeeded:
print(f" ✓ {m}")
for m in failed:
print(f" ✗ {m}")
print(f"\n{len(succeeded)}/{len(models)} models completed successfully.")
sys.exit(0 if not failed else 1)
else:
ok = _prepare_single_model(args.model, args, script_dir)
sys.exit(0 if ok else 1)
if __name__ == '__main__':
main()