import torch from torch import nn from collections import OrderedDict import numpy as np from typing import Tuple, List, Dict import torch import torch.nn as nn import torch.nn.functional as F ### from src.model.base import ODBinaryMetricClassifier from src.model.clip import VideoAttrExtractor from src.utility.loss import focal_loss ''' QuickGELU and LayerNorm w/ fp16 from official CLIP repo (https://github.com/openai/CLIP/blob/3b473b0e682c091a9e53623eebc1ca1657385717/clip/model.py) ''' class QuickGELU(nn.Module): def forward(self, x: torch.Tensor): return x * torch.sigmoid(1.702 * x) class LayerNorm(nn.LayerNorm): """Subclass torch's LayerNorm to handle fp16.""" def forward(self, x: torch.Tensor): orig_type = x.dtype ret = super().forward(x.type(torch.float32)) return ret.type(orig_type) class Attention(nn.Module): ''' A generalized attention module with more flexibility. ''' def __init__( self, q_in_dim: int, k_in_dim: int, v_in_dim: int, qk_proj_dim: int, v_proj_dim: int, num_heads: int, out_dim: int, return_all_features: bool = False, ): super().__init__() self.q_proj = nn.Linear(q_in_dim, qk_proj_dim) self.k_proj = nn.Linear(k_in_dim, qk_proj_dim) self.v_proj = nn.Linear(v_in_dim, v_proj_dim) self.out_proj = nn.Linear(v_proj_dim, out_dim) self.num_heads = num_heads self.return_all_features = return_all_features assert qk_proj_dim % num_heads == 0 and v_proj_dim % num_heads == 0 self._initialize_weights() def _initialize_weights(self): for m in (self.q_proj, self.k_proj, self.v_proj, self.out_proj): nn.init.xavier_uniform_(m.weight) nn.init.constant_(m.bias, 0.) def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor): assert q.ndim == 3 and k.ndim == 3 and v.ndim == 3 N = q.size(0) assert k.size(0) == N and v.size(0) == N Lq, Lkv = q.size(1), k.size(1) assert v.size(1) == Lkv q, k, v = self.q_proj(q), self.k_proj(k), self.v_proj(v) H = self.num_heads Cqk, Cv = q.size(-1) // H, v.size(-1) // H q = q.view(N, Lq, H, Cqk) k = k.view(N, Lkv, H, Cqk) v = v.view(N, Lkv, H, Cv) aff = torch.einsum('nqhc,nkhc->nqkh', q / (Cqk ** 0.5), k) aff = aff.softmax(dim=-2) mix = torch.einsum('nqlh,nlhc->nqhc', aff, v) out = self.out_proj(mix.flatten(-2)) if self.return_all_features: return dict(q=q, k=k, v=v, aff=aff, out=out) else: return out class PatchEmbed2D(nn.Module): def __init__( self, patch_size: Tuple[int, int] = (16, 16), in_channels: int = 3, embed_dim: int = 768, ): super().__init__() self.patch_size = patch_size self.in_channels = in_channels self.proj = nn.Linear(np.prod(patch_size) * in_channels, embed_dim) def _initialize_weights(self, x): nn.init.kaiming_normal_(self.proj.weight, 0.) nn.init.constant_(self.proj.bias, 0.) def forward(self, x: torch.Tensor): B, C, H, W = x.size() pH, pW = self.patch_size assert C == self.in_channels and H % pH == 0 and W % pW == 0 x = x.view(B, C, H // pH, pH, W // pW, pW).permute(0, 2, 4, 1, 3, 5).flatten(3).flatten(1, 2) x = self.proj(x) return x class TransformerEncoderLayer(nn.Module): def __init__( self, in_feature_dim: int = 768, qkv_dim: int = 768, num_heads: int = 12, mlp_factor: float = 4.0, mlp_dropout: float = 0.0, act: nn.Module = QuickGELU, return_all_features: bool = False, ): super().__init__() self.return_all_features = return_all_features self.attn = Attention( q_in_dim=in_feature_dim, k_in_dim=in_feature_dim, v_in_dim=in_feature_dim, qk_proj_dim=qkv_dim, v_proj_dim=qkv_dim, num_heads=num_heads, out_dim=in_feature_dim, return_all_features=return_all_features, ) mlp_dim = round(mlp_factor * in_feature_dim) self.mlp = nn.Sequential(OrderedDict([ ('fc1', nn.Linear(in_feature_dim, mlp_dim)), ('act', act()), ('dropout', nn.Dropout(mlp_dropout)), ('fc2', nn.Linear(mlp_dim, in_feature_dim)), ])) self.norm1 = LayerNorm(in_feature_dim) self.norm2 = LayerNorm(in_feature_dim) self._initialize_weights() def _initialize_weights(self): for m in (self.mlp[0], self.mlp[-1]): nn.init.xavier_uniform_(m.weight) nn.init.normal_(m.bias, std=1e-6) def forward(self, x: torch.Tensor): if self.return_all_features: ret_dict = {} x_norm = self.norm1(x) attn_out = self.attn(x_norm, x_norm, x_norm) ret_dict['q'] = attn_out['q'] ret_dict['k'] = attn_out['k'] ret_dict['v'] = attn_out['v'] ret_dict['attn_out'] = attn_out['out'] x = x + attn_out['out'] x = x + self.mlp(self.norm2(x)) ret_dict['out'] = x return ret_dict else: x_norm = self.norm1(x) x = x + self.attn(x_norm, x_norm, x_norm) x = x + self.mlp(self.norm2(x)) return x class TransformerDecoderLayer(nn.Module): def __init__( self, in_feature_dim: int = 768, qkv_dim: int = 768, num_heads: int = 12, mlp_factor: float = 4.0, mlp_dropout: float = 0.0, act: nn.Module = QuickGELU, ): super().__init__() self.attn = Attention( q_in_dim=in_feature_dim, k_in_dim=in_feature_dim, v_in_dim=in_feature_dim, qk_proj_dim=qkv_dim, v_proj_dim=qkv_dim, num_heads=num_heads, out_dim=in_feature_dim, ) mlp_dim = round(mlp_factor * in_feature_dim) self.mlp = nn.Sequential(OrderedDict([ ('fc1', nn.Linear(in_feature_dim, mlp_dim)), ('act', act()), ('dropout', nn.Dropout(mlp_dropout)), ('fc2', nn.Linear(mlp_dim, in_feature_dim)), ])) self.norm1 = LayerNorm(in_feature_dim) self.norm2 = LayerNorm(in_feature_dim) self.norm3 = LayerNorm(in_feature_dim) self._initialize_weights() def _initialize_weights(self): for m in (self.mlp[0], self.mlp[-1]): nn.init.xavier_uniform_(m.weight) nn.init.normal_(m.bias, std=1e-6) def forward(self, x: torch.Tensor, y: torch.Tensor): y_norm = self.norm3(y) x = x + self.attn(self.norm1(x), y_norm, y_norm) x = x + self.mlp(self.norm2(x)) return x class VisionTransformer2D(nn.Module): def __init__( self, feature_dim: int = 768, input_size: Tuple[int, int] = (224, 224), patch_size: Tuple[int, int] = (16, 16), num_heads: int = 12, num_layers: int = 12, mlp_factor: float = 4.0, act: nn.Module = QuickGELU, return_all_features: bool = False, ln_pre: bool = False, ): super().__init__() self.return_all_features = return_all_features self.patch_embed = PatchEmbed2D(patch_size=patch_size, embed_dim=feature_dim) self.num_patches = np.prod([x // y for x, y in zip(input_size, patch_size)]) + 1 self.cls_token = nn.Parameter(torch.zeros([feature_dim])) self.pos_embed = nn.Parameter(torch.zeros([self.num_patches, feature_dim])) self.blocks = nn.ModuleList([ TransformerEncoderLayer( in_feature_dim=feature_dim, qkv_dim=feature_dim, num_heads=num_heads, mlp_factor=mlp_factor, act=act, return_all_features=return_all_features, ) for _ in range(num_layers) ]) if ln_pre: self.ln_pre = LayerNorm(feature_dim) else: self.ln_pre = nn.Identity() self._initialize_weights() def _initialize_weights(self): nn.init.normal_(self.cls_token, std=0.02) nn.init.normal_(self.pos_embed, std=0.02) def forward(self, x: torch.Tensor): dtype = self.patch_embed.proj.weight.dtype x = x.to(dtype) x = self.patch_embed(x) x = torch.cat([self.cls_token.view(1, 1, -1).repeat(x.size(0), 1, 1), x], dim=1) x = x + self.pos_embed x = self.ln_pre(x) if self.return_all_features: all_features = [] for blk in self.blocks: x = blk(x) all_features.append(x) x = x['out'] return all_features else: for blk in self.blocks: x = blk(x) return x vit_presets = { 'ViT-B/16-lnpre': dict( feature_dim=768, input_size=(224, 224), patch_size=(16, 16), num_heads=12, num_layers=12, mlp_factor=4.0, ln_pre=True, ), 'ViT-L/14-lnpre': dict( feature_dim=1024, input_size=(224, 224), patch_size=(14, 14), num_heads=16, num_layers=24, mlp_factor=4.0, ln_pre=True, ), } class TransformerDecoderLayer(nn.Module): def __init__( self, in_feature_dim: int = 768, qkv_dim: int = 768, num_heads: int = 12, mlp_factor: float = 4.0, mlp_dropout: float = 0.0, act: nn.Module = QuickGELU, ): super().__init__() self.attn = Attention( q_in_dim=in_feature_dim, k_in_dim=in_feature_dim, v_in_dim=in_feature_dim, qk_proj_dim=qkv_dim, v_proj_dim=qkv_dim, num_heads=num_heads, out_dim=in_feature_dim, ) mlp_dim = round(mlp_factor * in_feature_dim) self.mlp = nn.Sequential(OrderedDict([ ('fc1', nn.Linear(in_feature_dim, mlp_dim)), ('act', act()), ('dropout', nn.Dropout(mlp_dropout)), ('fc2', nn.Linear(mlp_dim, in_feature_dim)), ])) self.norm1 = LayerNorm(in_feature_dim) self.norm2 = LayerNorm(in_feature_dim) self.norm3 = LayerNorm(in_feature_dim) self._initialize_weights() def _initialize_weights(self): for m in (self.mlp[0], self.mlp[-1]): nn.init.xavier_uniform_(m.weight) nn.init.normal_(m.bias, std=1e-6) def forward(self, x: torch.Tensor, y: torch.Tensor): y_norm = self.norm3(y) x = x + self.attn(self.norm1(x), y_norm, y_norm) x = x + self.mlp(self.norm2(x)) return x class TemporalCrossAttention(nn.Module): def __init__( self, spatial_size: Tuple[int, int] = (14, 14), feature_dim: int = 768, ): super().__init__() self.spatial_size = spatial_size w_size = np.prod([x * 2 - 1 for x in spatial_size]) self.w1 = nn.Parameter(torch.zeros([w_size, feature_dim])) self.w2 = nn.Parameter(torch.zeros([w_size, feature_dim])) idx_tensor = torch.zeros([np.prod(spatial_size) for _ in (0, 1)], dtype=torch.long) for q in range(np.prod(spatial_size)): qi, qj = q // spatial_size[1], q % spatial_size[1] for k in range(np.prod(spatial_size)): ki, kj = k // spatial_size[1], k % spatial_size[1] i_offs = qi - ki + spatial_size[0] - 1 j_offs = qj - kj + spatial_size[1] - 1 idx_tensor[q, k] = i_offs * (spatial_size[1] * 2 - 1) + j_offs self.idx_tensor = idx_tensor def forward_half(self, q: torch.Tensor, k: torch.Tensor, w: torch.Tensor) -> torch.Tensor: q, k = q[:, :, 1:], k[:, :, 1:] # remove cls token assert q.size() == k.size() assert q.size(2) == np.prod(self.spatial_size) attn = torch.einsum('ntqhd,ntkhd->ntqkh', q / (q.size(-1) ** 0.5), k) attn = attn.softmax(dim=-2).mean(dim=-1) # L, L, N, T self.idx_tensor = self.idx_tensor.to(w.device) w_unroll = w[self.idx_tensor] # L, L, C ret = torch.einsum('ntqk,qkc->ntqc', attn, w_unroll) return ret def forward(self, q: torch.Tensor, k: torch.Tensor): N, T, L, H, D = q.size() assert L == np.prod(self.spatial_size) + 1 ret = torch.zeros([N, T, L, self.w1.size(-1)], device='cuda') ret[:, 1:, 1:, :] += self.forward_half(q[:, 1:, :, :, :], k[:, :-1, :, :, :], self.w1) ret[:, :-1, 1:, :] += self.forward_half(q[:, :-1, :, :, :], k[:, 1:, :, :, :], self.w2) return ret class EVLDecoder(nn.Module): def __init__( self, num_frames: int = 8, spatial_size: Tuple[int, int] = (14, 14), num_layers: int = 4, in_feature_dim: int = 768, qkv_dim: int = 768, num_heads: int = 12, mlp_factor: float = 4.0, enable_temporal_conv: bool = True, enable_temporal_pos_embed: bool = True, enable_temporal_cross_attention: bool = True, mlp_dropout: float = 0.5, ): super().__init__() self.enable_temporal_conv = enable_temporal_conv self.enable_temporal_pos_embed = enable_temporal_pos_embed self.enable_temporal_cross_attention = enable_temporal_cross_attention self.num_layers = num_layers self.decoder_layers = nn.ModuleList( [TransformerDecoderLayer(in_feature_dim, qkv_dim, num_heads, mlp_factor, mlp_dropout) for _ in range(num_layers)] ) if enable_temporal_conv: self.temporal_conv = nn.ModuleList( [nn.Conv1d(in_feature_dim, in_feature_dim, kernel_size=3, stride=1, padding=1, groups=in_feature_dim) for _ in range(num_layers)] ) if enable_temporal_pos_embed: self.temporal_pos_embed = nn.ParameterList( [nn.Parameter(torch.zeros([num_frames, in_feature_dim])) for _ in range(num_layers)] ) if enable_temporal_cross_attention: self.cross_attention = nn.ModuleList( [TemporalCrossAttention(spatial_size, in_feature_dim) for _ in range(num_layers)] ) self.cls_token = nn.Parameter(torch.zeros([in_feature_dim])) def _initialize_weights(self): nn.init.normal_(self.cls_token, std=0.02) def forward(self, in_features: List[Dict[str, torch.Tensor]]): N, T, L, C = in_features[0]['out'].size() assert len(in_features) == self.num_layers x = self.cls_token.view(1, 1, -1).repeat(N, 1, 1) for i in range(self.num_layers): frame_features = in_features[i]['out'] if self.enable_temporal_conv: feat = in_features[i]['out'] feat = feat.permute(0, 2, 3, 1).contiguous().flatten(0, 1) # N * L, C, T feat = self.temporal_conv[i](feat) feat = feat.view(N, L, C, T).permute(0, 3, 1, 2).contiguous() # N, T, L, C frame_features += feat if self.enable_temporal_pos_embed: frame_features += self.temporal_pos_embed[i].view(1, T, 1, C) if self.enable_temporal_cross_attention: frame_features += self.cross_attention[i](in_features[i]['q'], in_features[i]['k']) frame_features = frame_features.flatten(1, 2) # N, T * L, C x = self.decoder_layers[i](x, frame_features) return x.squeeze(1) class EVLVideoAttrExtractor(VideoAttrExtractor): def __init__( self, architecture, num_frames=1, decoder_layers=4, attn_record=False ): super(EVLVideoAttrExtractor, self).__init__( architecture=architecture, text_embed=False, store_attrs=["q", "k", "out"], attn_record=attn_record ) self.decoder = EVLDecoder( num_frames=num_frames, spatial_size=(self.n_patch, self.n_patch), num_layers=decoder_layers, in_feature_dim=self.embed_dim, qkv_dim=self.embed_dim, num_heads=self.n_heads, mlp_factor=4.0, enable_temporal_conv=True, enable_temporal_pos_embed=True, enable_temporal_cross_attention=True, mlp_dropout=0.5 ) self.decoder_layers = decoder_layers def forward(self, x): results = super(EVLVideoAttrExtractor, self).forward(x) # remove cls_token attrs # layer_attrs = results["layer_attrs"] # for i in range(len(layer_attrs)): # for k in layer_attrs[i]: # layer_attrs[i][k] = layer_attrs[i][k][:, :, 1:] reps = self.decoder(results["layer_attrs"][-self.decoder_layers:]) results["reps"] = reps return results def train(self, mode=True): super().train(mode) if (mode): self.model.eval() return self class BinaryLinearClassifier(nn.Module): def __init__( self, *args, **kargs, ): super().__init__() self.encoder = EVLVideoAttrExtractor( *args, **kargs ) self.head = self.make_linear(self.encoder.embed_dim) def make_linear(self, embed_dim): linear = nn.Linear( embed_dim, 2 ) return nn.Sequential( nn.LayerNorm(embed_dim), linear ) @property def transform(self): return self.encoder.transform @property def n_px(self): return self.encoder.model.input_resolution def forward(self, x, *args, **kargs): results = self.encoder(x) logits = self.head(results["reps"]) return dict( logits=logits, ** results ) class EfficientVideoLearner(ODBinaryMetricClassifier): def __init__( self, architecture: str = 'ViT-B/16', num_frames: int = 1, attn_record: bool = False, decoder_layers: int = 4, is_focal_loss: bool = True, cls_weight: float = 10.0, label_weights: List[float] = [1, 1], ): super().__init__() self.save_hyperparameters() params = dict( architecture=architecture, attn_record=attn_record, num_frames=num_frames, decoder_layers=decoder_layers ) self.model = BinaryLinearClassifier(**params) self.label_weights = torch.tensor(label_weights) self.cls_weight = cls_weight self.is_focal_loss = is_focal_loss @property def transform(self): return self.model.transform @property def n_px(self): return self.model.n_px def shared_step(self, batch, stage): x, y, z = batch["xyz"] indices = batch["indices"] dts_name = batch["dts_name"] names = batch["names"] output = self(x, **z) logits = output["logits"] loss = 0 # classification loss if (stage == "train"): if self.is_focal_loss: cls_loss = focal_loss( logits, y, gamma=4, weight=( self.label_weights.to(y.device) if stage == "train" else None ) ) else: cls_loss = nn.functional.cross_entropy( logits, y, reduction="none", weight=( self.label_weights.to(y.device) if stage == "train" else None ) ) loss += cls_loss.mean() * self.cls_weight self.log( f"{stage}/{dts_name}/loss", cls_loss.mean(), batch_size=logits.shape[0] ) else: # classification loss cls_loss = nn.functional.cross_entropy( logits, y, reduction="none", weight=None ) loss += cls_loss.mean() return { "logits": logits, "labels": y, "loss": loss, "dts_name": dts_name, "indices": indices, "output": output } if __name__ == "__main__": frames = 5 model = EfficientVideoLearner(num_frames=frames, attn_record=True) model.to("cuda") logit = model(torch.randn(9, frames, 3, 224, 224).to("cuda"))["logits"] logit.sum().backward() print([k for k, v in model.named_parameters() if v.requires_grad]) print("done")