mirror of
https://github.com/hanzoai/jin.git
synced 2026-10-30 18:33:17 +00:00
461 lines
18 KiB
Python
461 lines
18 KiB
Python
import torch
|
|
import numpy as np
|
|
from patcher import ConvPatcher, ImagePatcher, HybridPatcher, ConvPatcher3d
|
|
from transformer import Transformer
|
|
from copy import deepcopy
|
|
|
|
def min_scale(x):
|
|
x = x - x.min() + 1e-6
|
|
return x / x.sum()
|
|
|
|
def normalized_image_loss(x, x_out, loss_f):
|
|
overall_mean = x.mean(dim = 0, keepdim = True)
|
|
overall_std = x.std(dim = 0, keepdim = True)
|
|
|
|
return loss_f((x - overall_mean) / overall_std,
|
|
(x_out - overall_mean) / overall_std)
|
|
|
|
class MaskedAutoencoder(torch.nn.Module):
|
|
def __init__(self,
|
|
h,
|
|
w = None,
|
|
in_channels = 1,
|
|
dim = 384,
|
|
depth = 8,
|
|
patch_size = 8,
|
|
cls_token = True,
|
|
mask_prob = 0.6,
|
|
mask_drop = 0.1,
|
|
loss = torch.nn.MSELoss(),
|
|
final_activation = torch.nn.GELU(),
|
|
patcher_type = "hybrid"):
|
|
super().__init__()
|
|
|
|
if w is None:
|
|
w = h
|
|
self.h = h
|
|
self.w = w
|
|
self.dim = dim
|
|
self.depth = depth
|
|
self.in_channels = in_channels
|
|
self.patch_size = patch_size
|
|
self.mask_prob = mask_prob
|
|
self.mask_drop = mask_drop
|
|
self.has_cls = cls_token
|
|
|
|
self.loss = loss
|
|
|
|
if patcher_type == "conv":
|
|
self.patcher = ConvPatcher(h, w = w,
|
|
patch_size = patch_size,
|
|
in_channels = in_channels,
|
|
out_channels = dim)
|
|
elif patcher_type == "hybrid":
|
|
self.patcher = HybridPatcher(h, w = w,
|
|
patch_size = patch_size,
|
|
in_channels = in_channels,
|
|
out_channels = dim)
|
|
else:
|
|
self.patcher = ImagePatcher(h, w = w,
|
|
patch_size = patch_size,
|
|
in_channels = in_channels,
|
|
out_channels = dim)
|
|
self.num_patches = self.patcher.num_patches
|
|
|
|
if cls_token:
|
|
self.register_buffer("cls_token", torch.randn(1, dim))
|
|
self.num_patches += 1
|
|
|
|
self.transformer = Transformer(dim = dim,
|
|
depth = depth,
|
|
context = self.num_patches)
|
|
self.final_activation = final_activation
|
|
|
|
self.register_buffer("mask_token", torch.randn(1, dim))
|
|
|
|
def forward(self, x, mask_indices = None, transformer_dropout = None):
|
|
x = self.embed(x,
|
|
mask_indices = mask_indices,
|
|
transformer_dropout = transformer_dropout)
|
|
if self.has_cls:
|
|
x = x[:, 1:]
|
|
x = self.patcher.inverse(x)
|
|
x = self.final_activation(x)
|
|
return x
|
|
|
|
def project_deproject(self, x):
|
|
x = self.patcher(x)
|
|
x = self.patcher.inverse(x)
|
|
return x
|
|
|
|
def embed(self, x, mask_indices = None, transformer_dropout = None):
|
|
x = self.patcher(x)
|
|
if not mask_indices is None:
|
|
x[mask_indices] = self.mask_token
|
|
if self.has_cls:
|
|
cls_tokens = self.cls_token.expand(x.shape[0], -1, -1)
|
|
x = torch.cat((cls_tokens, x), dim = 1)
|
|
x = self.transformer(x, stop_at = transformer_dropout)
|
|
return x
|
|
|
|
def get_loss(self, x):
|
|
cls_token_int = int(self.has_cls)
|
|
mask_sample = torch.rand(x.shape[0], self.num_patches - cls_token_int)
|
|
mask = (mask_sample < self.mask_prob)
|
|
# trains better when some mask tokens aren't applied
|
|
mask_dropped_out = mask_sample < self.mask_prob * (1 - self.mask_drop)
|
|
|
|
x_out = self.forward(x, mask_indices = mask_dropped_out)
|
|
|
|
# convert mask to image space
|
|
img_mask = self.patcher.mask_inverse(mask).bool()
|
|
img_mask = img_mask.repeat(1, self.in_channels, 1, 1)
|
|
# compute loss only on masked pixels
|
|
loss = normalized_image_loss(x[img_mask],
|
|
x_out[img_mask],
|
|
self.loss)
|
|
return loss, x_out
|
|
|
|
def update_mask_prob(self, amount = None, percent = None):
|
|
current_prob = self.mask_prob
|
|
if percent is not None:
|
|
amount = current_prob * percent
|
|
assert not amount is None, "Must provide either amount or percent"
|
|
# mask shouldn't exceed 80% imo
|
|
self.mask_prob = max(current_prob + amount, 0.8)
|
|
|
|
def save(self, path):
|
|
torch.save(self.state_dict(), path)
|
|
|
|
class SelfDistillMAE(torch.nn.Module):
|
|
def __init__(self,
|
|
h,
|
|
w = None,
|
|
d = None,
|
|
in_channels = 1,
|
|
dim = 384,
|
|
encoder_depth = 4,
|
|
regressor_depth = 2,
|
|
decoder_depth = 4,
|
|
patch_size = 8,
|
|
cls_token = False,
|
|
mask_prob = 0.6,
|
|
flash_attention = False,
|
|
loss = torch.nn.MSELoss(),
|
|
patcher_type = "conv",
|
|
student_temp = 0.1,
|
|
teacher_temp = 0.04,
|
|
center_alpha = 0.996,
|
|
downsample_3d = (2, 2, 2),
|
|
n_layers_3d = 3):
|
|
super().__init__()
|
|
if w is None:
|
|
w = h
|
|
self.h = h
|
|
self.w = w
|
|
if d is None:
|
|
d = 1
|
|
self.d = d
|
|
self.dim = dim
|
|
self.encoder_depth = encoder_depth
|
|
self.regressor_depth = regressor_depth
|
|
self.decoder_depth = decoder_depth
|
|
self.in_channels = in_channels
|
|
self.patch_size = patch_size
|
|
self.mask_prob = mask_prob
|
|
self.has_cls = cls_token
|
|
self.student_temp = student_temp
|
|
self.teacher_temp = teacher_temp
|
|
self.center_alpha = center_alpha
|
|
|
|
self.loss = loss
|
|
if patcher_type == "conv":
|
|
self.patcher = ConvPatcher(h, w = w,
|
|
d = d,
|
|
patch_size = patch_size,
|
|
in_channels = in_channels,
|
|
out_channels = dim)
|
|
elif patcher_type == "hybrid":
|
|
assert d is None, "Hybrid patcher doesn't support 3D images"
|
|
self.patcher = HybridPatcher(h, w = w,
|
|
patch_size = patch_size,
|
|
in_channels = in_channels,
|
|
out_channels = dim)
|
|
elif patcher_type == "conv3d":
|
|
assert d is not None, "Conv3d patcher requires d to be specified"
|
|
ds_factor = [x ** n_layers_3d for x in downsample_3d]
|
|
h_patcher = h * ds_factor[1]
|
|
w_patcher = w * ds_factor[2]
|
|
d_patcher = d * ds_factor[0]
|
|
self.patcher = ConvPatcher3d(h_patcher, w = w_patcher,
|
|
d = d_patcher,
|
|
in_channels = in_channels,
|
|
out_channels = dim,
|
|
layer_mults = downsample_3d,
|
|
n_layers= n_layers_3d ,)
|
|
|
|
else:
|
|
self.patcher = ImagePatcher(h, w = w,
|
|
d = d,
|
|
patch_size = patch_size,
|
|
in_channels = in_channels,
|
|
out_channels = dim,
|
|
activation = torch.nn.GELU())
|
|
self.num_patches = self.patcher.num_patches
|
|
|
|
#TODO: not actually implemented
|
|
# if cls_token:
|
|
# self.register_buffer("cls_token", torch.randn(1, dim))
|
|
# self.num_patches += 1
|
|
|
|
self.pos_emb = torch.nn.Parameter(torch.randn(1, self.num_patches, dim))
|
|
|
|
self.encoder= Transformer(dim = dim,
|
|
depth = encoder_depth,
|
|
positional_embedding = False,
|
|
context = None,
|
|
flash_attention = flash_attention)
|
|
self.encoder_ema = deepcopy(self.encoder).requires_grad_(False)
|
|
|
|
self.regressor = Transformer(dim = dim,
|
|
depth = regressor_depth,
|
|
context = None,
|
|
positional_embedding = False,
|
|
cross = True,
|
|
flash_attention = flash_attention)
|
|
self.decoder = Transformer(dim = dim,
|
|
depth = decoder_depth,
|
|
context = None,
|
|
positional_embedding = False,
|
|
flash_attention = flash_attention)
|
|
|
|
self.mask_token = torch.nn.Parameter(torch.randn(1, dim))
|
|
self.register_buffer("center", torch.zeros(1, 1, dim))
|
|
|
|
def tokenize(self, x):
|
|
x = self.patcher(x)
|
|
#TODO : implement cls token
|
|
#if self.has_cls:
|
|
# cls_tokens = self.cls_token.expand(x.shape[0], -1, -1)
|
|
# x = torch.cat((cls_tokens, x), dim = 1)
|
|
return x
|
|
|
|
def project_deproject(self, x):
|
|
x = self.patcher(x)
|
|
x = self.patcher.inverse(x)
|
|
return x
|
|
|
|
def embed(self, x, type = "ema"):
|
|
if type == "ema":
|
|
return self.encoder_ema(self.tokenize(x),
|
|
pos_embedding = self.pos_emb)
|
|
elif type == "normal":
|
|
return self.encoder(self.tokenize(x),
|
|
pos_embedding = self.pos_emb)
|
|
else:
|
|
raise ValueError(f"Unknown type {type}")
|
|
|
|
def decode(self, x):
|
|
x = self.decoder(x)
|
|
return self.patcher.inverse(x)
|
|
|
|
def mask_strategy(self, random = False):
|
|
roll = np.random.rand()
|
|
h_patches = self.patcher.h_patched
|
|
w_patches = self.patcher.w_patched
|
|
d = self.d
|
|
# random patches
|
|
if (not random) or roll < 0.5:
|
|
mask_sample = torch.rand(self.num_patches)
|
|
# 2x2 block patches
|
|
elif roll < 0.8:
|
|
mask_sample = torch.rand(d, h_patches // 2, w_patches // 2)
|
|
mask_sample = torch.nn.functional.interpolate(mask_sample.unsqueeze(0),
|
|
size = (h_patches, w_patches),
|
|
mode = "nearest").squeeze()
|
|
mask_sample = mask_sample.flatten()
|
|
# 4x4 block patches
|
|
else:
|
|
mask_sample = torch.rand(d, h_patches // 4, w_patches // 4)
|
|
mask_sample = torch.nn.functional.interpolate(mask_sample.unsqueeze(0),
|
|
size = (h_patches, w_patches),
|
|
mode = "nearest").squeeze()
|
|
mask_sample = mask_sample.flatten()
|
|
# trains better when some mask tokens aren't applied
|
|
mask = mask_sample < self.mask_prob
|
|
if mask.sum() == 0:
|
|
# at least one mask token
|
|
mask[0] = True
|
|
elif mask.sum() == mask.shape[0]:
|
|
# at least one non-mask token
|
|
mask[0] = False
|
|
mask_inverse = ~mask
|
|
return mask, mask_inverse
|
|
|
|
#TODO: look into not repeating same mask for batch
|
|
def get_loss(self, x, rep_loss_weight = 2):
|
|
batch_size = x.shape[0]
|
|
x_toks = self.tokenize(x)
|
|
#cls_token_int = int(self.has_cls)
|
|
mask, mask_inverse = self.mask_strategy()
|
|
|
|
pos_emb_mask = self.pos_emb.repeat(batch_size, 1, 1)[:, mask, :].clone()
|
|
pos_emb_inverse = self.pos_emb.repeat(batch_size, 1, 1)[:, mask_inverse, :].clone()
|
|
|
|
encoding = self.encoder(x_toks[:, mask_inverse, :],
|
|
pos_embedding = pos_emb_inverse)
|
|
with torch.no_grad():
|
|
encoding_ema = self.encoder_ema(x_toks[:, mask, :],
|
|
pos_embedding = pos_emb_mask)
|
|
|
|
masked_query = self.mask_token.expand_as(encoding_ema).clone()
|
|
|
|
encoding_ema_hat = self.regressor(masked_query,
|
|
y = encoding,
|
|
pos_embedding = pos_emb_mask)
|
|
|
|
rep_loss = torch.nn.functional.cross_entropy(((encoding_ema - self.center) / self.teacher_temp).softmax(dim = -1),
|
|
(encoding_ema_hat / self.student_temp).softmax(dim = -1))
|
|
rep_loss *= rep_loss_weight
|
|
|
|
self.center = self.center * self.center_alpha + encoding_ema.mean(dim = (0, 1), keepdim = True) * (1 - self.center_alpha)
|
|
|
|
last_embedding = self.decoder(encoding_ema_hat,
|
|
pos_embedding = pos_emb_mask)
|
|
# detach here so that encoder only gets grad through regressor
|
|
with torch.no_grad():
|
|
encoder_decode = self.decoder(encoding,
|
|
pos_embedding = pos_emb_inverse)
|
|
|
|
# reconstruct image
|
|
full_embedding = torch.zeros(batch_size, self.num_patches, self.dim,
|
|
device = x.device)
|
|
full_embedding[:, mask, :] = last_embedding
|
|
full_embedding[:, mask_inverse, :] = encoder_decode
|
|
x_out = self.patcher.inverse(full_embedding)
|
|
# convert mask to image space
|
|
img_mask = self.patcher.mask_inverse(mask.repeat(batch_size, 1)).bool()
|
|
img_mask = img_mask.repeat(1, self.in_channels, 1, 1)
|
|
# compute loss only on masked pixels
|
|
loss = normalized_image_loss(x[img_mask],
|
|
x_out[img_mask],
|
|
self.loss)
|
|
return loss + rep_loss, x_out
|
|
|
|
def update_mask_prob(self, amount = None, percent = None):
|
|
current_prob = self.mask_prob
|
|
if percent is not None:
|
|
amount = current_prob * percent
|
|
assert not amount is None, "Must provide either amount or percent"
|
|
# mask shouldn't exceed 80% imo
|
|
self.mask_prob = max(current_prob + amount, 0.8)
|
|
|
|
def save(self, path):
|
|
torch.save(self.state_dict(), path)
|
|
|
|
if __name__ == "__main__":
|
|
import torchvision
|
|
import os
|
|
from tqdm import tqdm
|
|
from einops import rearrange
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
from utils import ema_update
|
|
|
|
im2tensor = torchvision.transforms.ToTensor()
|
|
|
|
def collate(x, im2tensor = im2tensor):
|
|
x = [im2tensor(x_i[0]) for x_i in x]
|
|
return torch.stack(x, dim = 0)
|
|
|
|
def tensor2im(x):
|
|
return torchvision.transforms.ToPILImage()(x)
|
|
|
|
def save_im(x, path):
|
|
tensor2im(x).save(path)
|
|
|
|
# making a tmp folder to store the images
|
|
os.makedirs("tmp/", exist_ok = True)
|
|
|
|
# data root
|
|
data_root = "D:/Projects/" # you should only need to change this
|
|
test_mae = False
|
|
# image export frequency
|
|
output_every = 100
|
|
|
|
# data and patcher params
|
|
h, w = 32, 32
|
|
in_channels = 3
|
|
patch_size = 1
|
|
patch_dim = patch_size**2 * 3
|
|
embed_dim = patch_dim * 2
|
|
depth = 6
|
|
n_patches = (h // patch_size) * (w // patch_size)
|
|
batch_size = 256
|
|
mask_prob = 0.6
|
|
flash_attention = False
|
|
|
|
if test_mae:
|
|
model = MaskedAutoencoder(h,
|
|
w = w,
|
|
in_channels = in_channels,
|
|
depth = depth,
|
|
dim = embed_dim,
|
|
patch_size = patch_size,
|
|
mask_prob = mask_prob).to(device)
|
|
else:
|
|
model = SelfDistillMAE(h,
|
|
w = w,
|
|
in_channels = in_channels,
|
|
encoder_depth = depth,
|
|
dim = embed_dim,
|
|
patch_size = patch_size,
|
|
mask_prob = mask_prob,
|
|
flash_attention = flash_attention).to(device)
|
|
|
|
optimizer = torch.optim.Adam(model.parameters(),
|
|
lr = 1e-3,
|
|
weight_decay = 1e-6)
|
|
|
|
# training params
|
|
n_epochs = 5
|
|
lr = 1e-3
|
|
|
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
cifar = torchvision.datasets.CIFAR100(root = data_root, train = True, download = True)
|
|
|
|
losses = []
|
|
for epoch in range(n_epochs):
|
|
print(f"Epoch {epoch}")
|
|
dataloader = torch.utils.data.DataLoader(cifar,
|
|
batch_size = batch_size,
|
|
shuffle = True,
|
|
collate_fn = collate)
|
|
epoch_losses = []
|
|
for i, x in enumerate(tqdm(dataloader)):
|
|
optimizer.zero_grad()
|
|
x = x.to(device)
|
|
loss, x_out = model.get_loss(x)
|
|
loss.backward()
|
|
torch.nn.utils.clip_grad_norm_(model.parameters(), 1)
|
|
|
|
optimizer.step()
|
|
|
|
if not test_mae:
|
|
model.encoder_ema = ema_update(model.encoder_ema,
|
|
model.encoder)
|
|
|
|
epoch_losses.append(loss.item())
|
|
|
|
if i % output_every == 0:
|
|
str_i = str(i).zfill(5)
|
|
b, c, h, w = x.shape
|
|
|
|
x_out = torch.stack([x, x_out], dim = 0)
|
|
x_out = rearrange(x_out, "n b c h w -> (b n) c h w")
|
|
|
|
x_out = torchvision.utils.make_grid(x_out, nrow = 8, padding = 2, pad_value = 1)
|
|
save_im(x_out, f"tmp/epoch_{epoch}_{str_i}.png")
|
|
losses.extend(epoch_losses)
|
|
print(f"Loss: {np.mean(epoch_losses)}")
|
|
plt.plot(losses) |