DriftFox's picture
Release synthetic-pretrained Voronoi ResNets with BSD-3-Clause and original attribution
293b0b9 verified
Raw History Blame Contribute Delete
3.87 kB
"""Load synthetic-pretrained Voronoi microstructure ResNets without pickle."""
from pathlib import Path
import torch
from torch import nn
from torchvision import models
from safetensors.torch import load_file
TARGET_NAMES = [
'log_grains', 'mean_ecd', 'area_weighted_ecd', 'ecd_cv',
'hall_petch_mean_ecd', 'phase_area_fraction', 'phase_grain_fraction',
'log_stretch', 'log_defects', 'defect_area_fraction',
'mean_defect_radius', 'boundary_fraction', 'interior_grain_fraction',
]
VARIANTS = {
'resnet18_seed1019': 'resnet18',
'resnet18_seed2026': 'resnet18',
'resnet34_seed1019': 'resnet34',
'resnet50_seed1019': 'resnet50',
}
class VoronoiResNet(nn.Module):
def __init__(self, architecture):
super().__init__()
if architecture not in {'resnet18', 'resnet34', 'resnet50'}:
raise ValueError('Expected resnet18, resnet34, or resnet50')
self.encoder = getattr(models, architecture)(weights=None)
dimension = self.encoder.fc.in_features
self.encoder.fc = nn.Identity()
self.head = nn.Sequential(nn.Linear(dimension, 128), nn.SiLU(), nn.Linear(128, 26))
self.register_buffer('mean', torch.tensor([.485, .456, .406])[None, :, None, None])
self.register_buffer('scale', torch.tensor([.229, .224, .225])[None, :, None, None])
self.register_buffer('target_mean', torch.zeros(13))
self.register_buffer('target_scale', torch.ones(13))
def forward(self, images):
"""Return normalized means, clamped log variances, and encoder embeddings."""
if images.ndim != 4 or images.shape[1:] != (1, 256, 256):
raise ValueError('Expected floating point images shaped [N, 1, 256, 256]')
if not images.is_floating_point():
raise ValueError('Convert image pixels to floating point in [0, 1] first')
z = self.encoder((images.repeat(1, 3, 1, 1) - self.mean) / self.scale)
p = self.head(z)
return p[:, :13], p[:, 13:].clamp(-4, 3), z
@torch.inference_mode()
def predict(self, images, tta=True):
"""Return parameter estimates, standard deviations, and mean embeddings.
Log-transformed targets stay in their named log domain. Uncertainty is
estimated by the synthetic model and is not calibrated on real images.
"""
if self.training:
raise ValueError('Call model.eval() before inference')
means, variances, embeddings = [], [], []
views = ((flip, rotation) for flip in (False, True) for rotation in range(4)) if tta else [(False, 0)]
for flip, rotation in views:
x = images.flip(-1) if flip else images
p, lv, z = self(torch.rot90(x, rotation, (-2, -1)))
means.append(p.float())
variances.append(lv.float().exp())
embeddings.append(z.float())
p = torch.stack(means)
parameters = p.mean(0) * self.target_scale + self.target_mean
variance = torch.stack(variances).mean(0) + p.var(0, unbiased=False)
uncertainty = variance.sqrt() * self.target_scale
return parameters, uncertainty, torch.stack(embeddings).mean(0)
def load_model(directory, variant='resnet18_seed1019', device='cpu'):
"""Load one variant from a downloaded repository directory."""
model = VoronoiResNet(VARIANTS[variant])
model.load_state_dict(load_file(str(Path(directory) / f'{variant}.safetensors')), strict=True)
return model.to(device).eval()
if __name__ == '__main__':
torch.set_num_threads(2)
model = load_model(Path(__file__).resolve().parent)
p, u, z = model.predict(torch.full((1, 1, 256, 256), .6), tta=False)
assert p.shape == u.shape == (1, 13) and z.shape == (1, 512)
assert torch.isfinite(p).all() and torch.isfinite(u).all() and (u > 0).all()
print('Load and inference check passed')