|
|
|
"""Contains the Inception V3 model, which is used for inference ONLY. |
|
|
|
This file is mostly borrowed from `torchvision/models/inception.py`. |
|
|
|
Inception model is widely used to compute FID or IS metric for evaluating |
|
generative models. However, the pre-trained models from torchvision is slightly |
|
different from the TensorFlow version |
|
|
|
http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz |
|
|
|
which is used by the official FID implementation |
|
|
|
https://github.com/bioinf-jku/TTUR |
|
|
|
In particular: |
|
|
|
(1) The number of classes in TensorFlow model is 1008 instead of 1000. |
|
(2) The avg_pool() layers in TensorFlow model does not include the padded zero. |
|
(3) The last Inception E Block in TensorFlow model use max_pool() instead of |
|
avg_pool(). |
|
|
|
Hence, to align the evaluation results with those from TensorFlow |
|
implementation, we modified the inception model to support both versions. Please |
|
use `align_tf` argument to control the version. |
|
""" |
|
|
|
import warnings |
|
|
|
import torch |
|
import torch.nn as nn |
|
import torch.nn.functional as F |
|
import torch.distributed as dist |
|
|
|
from utils.misc import download_url |
|
|
|
__all__ = ['InceptionModel'] |
|
|
|
|
|
|
|
_MODEL_URL_SHA256 = { |
|
|
|
'torchvision_official': ( |
|
'https://download.pytorch.org/models/inception_v3_google-1a9a5a14.pth', |
|
'1a9a5a14f40645a370184bd54f4e8e631351e71399112b43ad0294a79da290c8' |
|
), |
|
|
|
|
|
'tf_inception_v3': ( |
|
'https://github.com/mseitzer/pytorch-fid/releases/download/fid_weights/pt_inception-2015-12-05-6726825d.pth', |
|
'6726825d0af5f729cebd5821db510b11b1cfad8faad88a03f1befd49fb9129b2' |
|
) |
|
} |
|
|
|
|
|
class InceptionModel(object): |
|
"""Defines the Inception (V3) model. |
|
|
|
This is a static class, which is used to avoid this model to be built |
|
repeatedly. Consequently, this model is particularly used for inference, |
|
like computing FID. If training is required, please use the model from |
|
`torchvision.models` or implement by yourself. |
|
|
|
NOTE: The pre-trained model assumes the inputs to be with `RGB` channel |
|
order and pixel range [-1, 1], and will also resize the images to shape |
|
[299, 299] automatically. If your input is normalized by subtracting |
|
(0.485, 0.456, 0.406) and dividing (0.229, 0.224, 0.225), please use |
|
`transform_input` in the `forward()` function to un-normalize it. |
|
""" |
|
models = dict() |
|
|
|
@staticmethod |
|
def build_model(align_tf=True): |
|
"""Builds the model and load pre-trained weights. |
|
|
|
If `align_tf` is set as True, the model will predict 1008 classes, and |
|
the pre-trained weight from `https://github.com/mseitzer/pytorch-fid` |
|
will be loaded. Otherwise, the model will predict 1000 classes, and will |
|
load the model from `torchvision`. |
|
|
|
The built model supports following arguments when forwarding: |
|
|
|
- transform_input: Whether to transform the input back to pixel range |
|
(-1, 1). Please disable this argument if your input is already with |
|
pixel range (-1, 1). (default: False) |
|
- output_logits: Whether to output the categorical logits instead of |
|
features. (default: False) |
|
- remove_logits_bias: Whether to remove the bias when computing the |
|
logits. The official implementation removes the bias by default. |
|
Please refer to |
|
`https://github.com/openai/improved-gan/blob/master/inception_score/model.py`. |
|
(default: False) |
|
- output_predictions: Whether to output the final predictions, i.e., |
|
`softmax(logits)`. (default: False) |
|
""" |
|
if align_tf: |
|
num_classes = 1008 |
|
model_source = 'tf_inception_v3' |
|
else: |
|
num_classes = 1000 |
|
model_source = 'torchvision_official' |
|
|
|
fingerprint = model_source |
|
|
|
if fingerprint not in InceptionModel.models: |
|
|
|
model = Inception3(num_classes=num_classes, |
|
aux_logits=False, |
|
init_weights=False, |
|
align_tf=align_tf) |
|
|
|
|
|
if dist.is_initialized() and dist.get_rank() != 0: |
|
dist.barrier() |
|
|
|
url, sha256 = _MODEL_URL_SHA256[model_source] |
|
filename = f'inception_model_{model_source}_{sha256}.pth' |
|
model_path, hash_check = download_url(url, |
|
filename=filename, |
|
sha256=sha256) |
|
state_dict = torch.load(model_path, map_location='cpu') |
|
if hash_check is False: |
|
warnings.warn(f'Hash check failed! The remote file from URL ' |
|
f'`{url}` may be changed, or the downloading is ' |
|
f'interrupted. The loaded inception model may ' |
|
f'have unexpected behavior.') |
|
|
|
if dist.is_initialized() and dist.get_rank() == 0: |
|
dist.barrier() |
|
|
|
|
|
model.load_state_dict(state_dict, strict=False) |
|
del state_dict |
|
|
|
|
|
model.eval().requires_grad_(False).cuda() |
|
InceptionModel.models[fingerprint] = model |
|
|
|
return InceptionModel.models[fingerprint] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Inception3(nn.Module): |
|
|
|
def __init__(self, num_classes=1000, aux_logits=True, inception_blocks=None, |
|
init_weights=True, align_tf=True): |
|
super(Inception3, self).__init__() |
|
if inception_blocks is None: |
|
inception_blocks = [ |
|
BasicConv2d, InceptionA, InceptionB, InceptionC, |
|
InceptionD, InceptionE, InceptionAux |
|
] |
|
assert len(inception_blocks) == 7 |
|
conv_block = inception_blocks[0] |
|
inception_a = inception_blocks[1] |
|
inception_b = inception_blocks[2] |
|
inception_c = inception_blocks[3] |
|
inception_d = inception_blocks[4] |
|
inception_e = inception_blocks[5] |
|
inception_aux = inception_blocks[6] |
|
|
|
self.aux_logits = aux_logits |
|
self.align_tf = align_tf |
|
self.Conv2d_1a_3x3 = conv_block(3, 32, kernel_size=3, stride=2) |
|
self.Conv2d_2a_3x3 = conv_block(32, 32, kernel_size=3) |
|
self.Conv2d_2b_3x3 = conv_block(32, 64, kernel_size=3, padding=1) |
|
self.Conv2d_3b_1x1 = conv_block(64, 80, kernel_size=1) |
|
self.Conv2d_4a_3x3 = conv_block(80, 192, kernel_size=3) |
|
self.Mixed_5b = inception_a(192, pool_features=32, align_tf=self.align_tf) |
|
self.Mixed_5c = inception_a(256, pool_features=64, align_tf=self.align_tf) |
|
self.Mixed_5d = inception_a(288, pool_features=64, align_tf=self.align_tf) |
|
self.Mixed_6a = inception_b(288) |
|
self.Mixed_6b = inception_c(768, channels_7x7=128, align_tf=self.align_tf) |
|
self.Mixed_6c = inception_c(768, channels_7x7=160, align_tf=self.align_tf) |
|
self.Mixed_6d = inception_c(768, channels_7x7=160, align_tf=self.align_tf) |
|
self.Mixed_6e = inception_c(768, channels_7x7=192, align_tf=self.align_tf) |
|
if aux_logits: |
|
self.AuxLogits = inception_aux(768, num_classes) |
|
self.Mixed_7a = inception_d(768) |
|
self.Mixed_7b = inception_e(1280, align_tf=self.align_tf) |
|
self.Mixed_7c = inception_e(2048, use_max_pool=self.align_tf) |
|
self.fc = nn.Linear(2048, num_classes) |
|
if init_weights: |
|
for m in self.modules(): |
|
if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear): |
|
import scipy.stats as stats |
|
stddev = m.stddev if hasattr(m, 'stddev') else 0.1 |
|
X = stats.truncnorm(-2, 2, scale=stddev) |
|
values = torch.as_tensor(X.rvs(m.weight.numel()), dtype=m.weight.dtype) |
|
values = values.view(m.weight.size()) |
|
with torch.no_grad(): |
|
m.weight.copy_(values) |
|
elif isinstance(m, nn.BatchNorm2d): |
|
nn.init.constant_(m.weight, 1) |
|
nn.init.constant_(m.bias, 0) |
|
|
|
@staticmethod |
|
def _transform_input(x, transform_input=False): |
|
if transform_input: |
|
x_ch0 = torch.unsqueeze(x[:, 0], 1) * (0.229 / 0.5) + (0.485 - 0.5) / 0.5 |
|
x_ch1 = torch.unsqueeze(x[:, 1], 1) * (0.224 / 0.5) + (0.456 - 0.5) / 0.5 |
|
x_ch2 = torch.unsqueeze(x[:, 2], 1) * (0.225 / 0.5) + (0.406 - 0.5) / 0.5 |
|
x = torch.cat((x_ch0, x_ch1, x_ch2), 1) |
|
return x |
|
|
|
def _forward(self, |
|
x, |
|
output_logits=False, |
|
remove_logits_bias=False, |
|
output_predictions=False): |
|
|
|
if x.shape[2] != 299 or x.shape[3] != 299: |
|
if self.align_tf: |
|
theta = torch.eye(2, 3).to(x) |
|
theta[0, 2] += theta[0, 0] / x.shape[3] - theta[0, 0] / 299 |
|
theta[1, 2] += theta[1, 1] / x.shape[2] - theta[1, 1] / 299 |
|
theta = theta.unsqueeze(0).repeat(x.shape[0], 1, 1) |
|
grid = F.affine_grid(theta, |
|
size=(x.shape[0], x.shape[1], 299, 299), |
|
align_corners=False) |
|
x = F.grid_sample(x, grid, |
|
mode='bilinear', |
|
padding_mode='border', |
|
align_corners=False) |
|
else: |
|
x = F.interpolate( |
|
x, size=(299, 299), mode='bilinear', align_corners=False) |
|
if x.shape[1] == 1: |
|
x = x.repeat((1, 3, 1, 1)) |
|
|
|
if self.align_tf: |
|
x = (x * 127.5 + 127.5 - 128) / 128 |
|
|
|
|
|
x = self.Conv2d_1a_3x3(x) |
|
|
|
x = self.Conv2d_2a_3x3(x) |
|
|
|
x = self.Conv2d_2b_3x3(x) |
|
|
|
x = F.max_pool2d(x, kernel_size=3, stride=2) |
|
|
|
x = self.Conv2d_3b_1x1(x) |
|
|
|
x = self.Conv2d_4a_3x3(x) |
|
|
|
x = F.max_pool2d(x, kernel_size=3, stride=2) |
|
|
|
x = self.Mixed_5b(x) |
|
|
|
x = self.Mixed_5c(x) |
|
|
|
x = self.Mixed_5d(x) |
|
|
|
x = self.Mixed_6a(x) |
|
|
|
x = self.Mixed_6b(x) |
|
|
|
x = self.Mixed_6c(x) |
|
|
|
x = self.Mixed_6d(x) |
|
|
|
x = self.Mixed_6e(x) |
|
|
|
if self.training and self.aux_logits: |
|
aux = self.AuxLogits(x) |
|
else: |
|
aux = None |
|
|
|
x = self.Mixed_7a(x) |
|
|
|
x = self.Mixed_7b(x) |
|
|
|
x = self.Mixed_7c(x) |
|
|
|
|
|
x = F.adaptive_avg_pool2d(x, (1, 1)) |
|
|
|
x = F.dropout(x, training=self.training) |
|
|
|
x = torch.flatten(x, 1) |
|
|
|
if output_logits or output_predictions: |
|
x = self.fc(x) |
|
|
|
if remove_logits_bias: |
|
x = x - self.fc.bias.view(1, -1) |
|
if output_predictions: |
|
x = F.softmax(x, dim=1) |
|
return x, aux |
|
|
|
def forward(self, |
|
x, |
|
transform_input=False, |
|
output_logits=False, |
|
remove_logits_bias=False, |
|
output_predictions=False): |
|
x = self._transform_input(x, transform_input) |
|
x, aux = self._forward( |
|
x, output_logits, remove_logits_bias, output_predictions) |
|
if self.training and self.aux_logits: |
|
return x, aux |
|
else: |
|
return x |
|
|
|
|
|
class InceptionA(nn.Module): |
|
|
|
def __init__(self, in_channels, pool_features, conv_block=None, align_tf=False): |
|
super(InceptionA, self).__init__() |
|
if conv_block is None: |
|
conv_block = BasicConv2d |
|
self.branch1x1 = conv_block(in_channels, 64, kernel_size=1) |
|
|
|
self.branch5x5_1 = conv_block(in_channels, 48, kernel_size=1) |
|
self.branch5x5_2 = conv_block(48, 64, kernel_size=5, padding=2) |
|
|
|
self.branch3x3dbl_1 = conv_block(in_channels, 64, kernel_size=1) |
|
self.branch3x3dbl_2 = conv_block(64, 96, kernel_size=3, padding=1) |
|
self.branch3x3dbl_3 = conv_block(96, 96, kernel_size=3, padding=1) |
|
|
|
self.branch_pool = conv_block(in_channels, pool_features, kernel_size=1) |
|
self.pool_include_padding = not align_tf |
|
|
|
def _forward(self, x): |
|
branch1x1 = self.branch1x1(x) |
|
|
|
branch5x5 = self.branch5x5_1(x) |
|
branch5x5 = self.branch5x5_2(branch5x5) |
|
|
|
branch3x3dbl = self.branch3x3dbl_1(x) |
|
branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl) |
|
branch3x3dbl = self.branch3x3dbl_3(branch3x3dbl) |
|
|
|
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1, |
|
count_include_pad=self.pool_include_padding) |
|
branch_pool = self.branch_pool(branch_pool) |
|
|
|
outputs = [branch1x1, branch5x5, branch3x3dbl, branch_pool] |
|
return outputs |
|
|
|
def forward(self, x): |
|
outputs = self._forward(x) |
|
return torch.cat(outputs, 1) |
|
|
|
|
|
class InceptionB(nn.Module): |
|
|
|
def __init__(self, in_channels, conv_block=None): |
|
super(InceptionB, self).__init__() |
|
if conv_block is None: |
|
conv_block = BasicConv2d |
|
self.branch3x3 = conv_block(in_channels, 384, kernel_size=3, stride=2) |
|
|
|
self.branch3x3dbl_1 = conv_block(in_channels, 64, kernel_size=1) |
|
self.branch3x3dbl_2 = conv_block(64, 96, kernel_size=3, padding=1) |
|
self.branch3x3dbl_3 = conv_block(96, 96, kernel_size=3, stride=2) |
|
|
|
def _forward(self, x): |
|
branch3x3 = self.branch3x3(x) |
|
|
|
branch3x3dbl = self.branch3x3dbl_1(x) |
|
branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl) |
|
branch3x3dbl = self.branch3x3dbl_3(branch3x3dbl) |
|
|
|
branch_pool = F.max_pool2d(x, kernel_size=3, stride=2) |
|
|
|
outputs = [branch3x3, branch3x3dbl, branch_pool] |
|
return outputs |
|
|
|
def forward(self, x): |
|
outputs = self._forward(x) |
|
return torch.cat(outputs, 1) |
|
|
|
|
|
class InceptionC(nn.Module): |
|
|
|
def __init__(self, in_channels, channels_7x7, conv_block=None, align_tf=False): |
|
super(InceptionC, self).__init__() |
|
if conv_block is None: |
|
conv_block = BasicConv2d |
|
self.branch1x1 = conv_block(in_channels, 192, kernel_size=1) |
|
|
|
c7 = channels_7x7 |
|
self.branch7x7_1 = conv_block(in_channels, c7, kernel_size=1) |
|
self.branch7x7_2 = conv_block(c7, c7, kernel_size=(1, 7), padding=(0, 3)) |
|
self.branch7x7_3 = conv_block(c7, 192, kernel_size=(7, 1), padding=(3, 0)) |
|
|
|
self.branch7x7dbl_1 = conv_block(in_channels, c7, kernel_size=1) |
|
self.branch7x7dbl_2 = conv_block(c7, c7, kernel_size=(7, 1), padding=(3, 0)) |
|
self.branch7x7dbl_3 = conv_block(c7, c7, kernel_size=(1, 7), padding=(0, 3)) |
|
self.branch7x7dbl_4 = conv_block(c7, c7, kernel_size=(7, 1), padding=(3, 0)) |
|
self.branch7x7dbl_5 = conv_block(c7, 192, kernel_size=(1, 7), padding=(0, 3)) |
|
|
|
self.branch_pool = conv_block(in_channels, 192, kernel_size=1) |
|
self.pool_include_padding = not align_tf |
|
|
|
def _forward(self, x): |
|
branch1x1 = self.branch1x1(x) |
|
|
|
branch7x7 = self.branch7x7_1(x) |
|
branch7x7 = self.branch7x7_2(branch7x7) |
|
branch7x7 = self.branch7x7_3(branch7x7) |
|
|
|
branch7x7dbl = self.branch7x7dbl_1(x) |
|
branch7x7dbl = self.branch7x7dbl_2(branch7x7dbl) |
|
branch7x7dbl = self.branch7x7dbl_3(branch7x7dbl) |
|
branch7x7dbl = self.branch7x7dbl_4(branch7x7dbl) |
|
branch7x7dbl = self.branch7x7dbl_5(branch7x7dbl) |
|
|
|
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1, |
|
count_include_pad=self.pool_include_padding) |
|
branch_pool = self.branch_pool(branch_pool) |
|
|
|
outputs = [branch1x1, branch7x7, branch7x7dbl, branch_pool] |
|
return outputs |
|
|
|
def forward(self, x): |
|
outputs = self._forward(x) |
|
return torch.cat(outputs, 1) |
|
|
|
|
|
class InceptionD(nn.Module): |
|
|
|
def __init__(self, in_channels, conv_block=None): |
|
super(InceptionD, self).__init__() |
|
if conv_block is None: |
|
conv_block = BasicConv2d |
|
self.branch3x3_1 = conv_block(in_channels, 192, kernel_size=1) |
|
self.branch3x3_2 = conv_block(192, 320, kernel_size=3, stride=2) |
|
|
|
self.branch7x7x3_1 = conv_block(in_channels, 192, kernel_size=1) |
|
self.branch7x7x3_2 = conv_block(192, 192, kernel_size=(1, 7), padding=(0, 3)) |
|
self.branch7x7x3_3 = conv_block(192, 192, kernel_size=(7, 1), padding=(3, 0)) |
|
self.branch7x7x3_4 = conv_block(192, 192, kernel_size=3, stride=2) |
|
|
|
def _forward(self, x): |
|
branch3x3 = self.branch3x3_1(x) |
|
branch3x3 = self.branch3x3_2(branch3x3) |
|
|
|
branch7x7x3 = self.branch7x7x3_1(x) |
|
branch7x7x3 = self.branch7x7x3_2(branch7x7x3) |
|
branch7x7x3 = self.branch7x7x3_3(branch7x7x3) |
|
branch7x7x3 = self.branch7x7x3_4(branch7x7x3) |
|
|
|
branch_pool = F.max_pool2d(x, kernel_size=3, stride=2) |
|
outputs = [branch3x3, branch7x7x3, branch_pool] |
|
return outputs |
|
|
|
def forward(self, x): |
|
outputs = self._forward(x) |
|
return torch.cat(outputs, 1) |
|
|
|
|
|
class InceptionE(nn.Module): |
|
|
|
def __init__(self, in_channels, conv_block=None, align_tf=False, use_max_pool=False): |
|
super(InceptionE, self).__init__() |
|
if conv_block is None: |
|
conv_block = BasicConv2d |
|
self.branch1x1 = conv_block(in_channels, 320, kernel_size=1) |
|
|
|
self.branch3x3_1 = conv_block(in_channels, 384, kernel_size=1) |
|
self.branch3x3_2a = conv_block(384, 384, kernel_size=(1, 3), padding=(0, 1)) |
|
self.branch3x3_2b = conv_block(384, 384, kernel_size=(3, 1), padding=(1, 0)) |
|
|
|
self.branch3x3dbl_1 = conv_block(in_channels, 448, kernel_size=1) |
|
self.branch3x3dbl_2 = conv_block(448, 384, kernel_size=3, padding=1) |
|
self.branch3x3dbl_3a = conv_block(384, 384, kernel_size=(1, 3), padding=(0, 1)) |
|
self.branch3x3dbl_3b = conv_block(384, 384, kernel_size=(3, 1), padding=(1, 0)) |
|
|
|
self.branch_pool = conv_block(in_channels, 192, kernel_size=1) |
|
self.pool_include_padding = not align_tf |
|
self.use_max_pool = use_max_pool |
|
|
|
def _forward(self, x): |
|
branch1x1 = self.branch1x1(x) |
|
|
|
branch3x3 = self.branch3x3_1(x) |
|
branch3x3 = [ |
|
self.branch3x3_2a(branch3x3), |
|
self.branch3x3_2b(branch3x3), |
|
] |
|
branch3x3 = torch.cat(branch3x3, 1) |
|
|
|
branch3x3dbl = self.branch3x3dbl_1(x) |
|
branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl) |
|
branch3x3dbl = [ |
|
self.branch3x3dbl_3a(branch3x3dbl), |
|
self.branch3x3dbl_3b(branch3x3dbl), |
|
] |
|
branch3x3dbl = torch.cat(branch3x3dbl, 1) |
|
|
|
if self.use_max_pool: |
|
branch_pool = F.max_pool2d(x, kernel_size=3, stride=1, padding=1) |
|
else: |
|
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1, |
|
count_include_pad=self.pool_include_padding) |
|
branch_pool = self.branch_pool(branch_pool) |
|
|
|
outputs = [branch1x1, branch3x3, branch3x3dbl, branch_pool] |
|
return outputs |
|
|
|
def forward(self, x): |
|
outputs = self._forward(x) |
|
return torch.cat(outputs, 1) |
|
|
|
|
|
class InceptionAux(nn.Module): |
|
|
|
def __init__(self, in_channels, num_classes, conv_block=None): |
|
super(InceptionAux, self).__init__() |
|
if conv_block is None: |
|
conv_block = BasicConv2d |
|
self.conv0 = conv_block(in_channels, 128, kernel_size=1) |
|
self.conv1 = conv_block(128, 768, kernel_size=5) |
|
self.conv1.stddev = 0.01 |
|
self.fc = nn.Linear(768, num_classes) |
|
self.fc.stddev = 0.001 |
|
|
|
def forward(self, x): |
|
|
|
x = F.avg_pool2d(x, kernel_size=5, stride=3) |
|
|
|
x = self.conv0(x) |
|
|
|
x = self.conv1(x) |
|
|
|
|
|
x = F.adaptive_avg_pool2d(x, (1, 1)) |
|
|
|
x = torch.flatten(x, 1) |
|
|
|
x = self.fc(x) |
|
|
|
return x |
|
|
|
|
|
class BasicConv2d(nn.Module): |
|
|
|
def __init__(self, in_channels, out_channels, **kwargs): |
|
super(BasicConv2d, self).__init__() |
|
self.conv = nn.Conv2d(in_channels, out_channels, bias=False, **kwargs) |
|
self.bn = nn.BatchNorm2d(out_channels, eps=0.001) |
|
|
|
def forward(self, x): |
|
x = self.conv(x) |
|
x = self.bn(x) |
|
return F.relu(x, inplace=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|