MiVOLO / mivolo /model /cross_bottleneck_attn.py
admin
sync
4c4ff57
raw
history blame
4.12 kB
"""
Code based on timm https://github.com/huggingface/pytorch-image-models
Modifications and additions for mivolo by / Copyright 2023, Irina Tolstykh, Maxim Kuprashevich
"""
import torch
import torch.nn as nn
from timm.layers.bottleneck_attn import PosEmbedRel
from timm.layers.helpers import make_divisible
from timm.layers.mlp import Mlp
from timm.layers.trace_utils import _assert
from timm.layers.weight_init import trunc_normal_
class CrossBottleneckAttn(nn.Module):
def __init__(
self,
dim,
dim_out=None,
feat_size=None,
stride=1,
num_heads=4,
dim_head=None,
qk_ratio=1.0,
qkv_bias=False,
scale_pos_embed=False,
):
super().__init__()
assert feat_size is not None, "A concrete feature size matching expected input (H, W) is required"
dim_out = dim_out or dim
assert dim_out % num_heads == 0
self.num_heads = num_heads
self.dim_head_qk = dim_head or make_divisible(dim_out * qk_ratio, divisor=8) // num_heads
self.dim_head_v = dim_out // self.num_heads
self.dim_out_qk = num_heads * self.dim_head_qk
self.dim_out_v = num_heads * self.dim_head_v
self.scale = self.dim_head_qk**-0.5
self.scale_pos_embed = scale_pos_embed
self.qkv_f = nn.Conv2d(dim, self.dim_out_qk * 2 + self.dim_out_v, 1, bias=qkv_bias)
self.qkv_p = nn.Conv2d(dim, self.dim_out_qk * 2 + self.dim_out_v, 1, bias=qkv_bias)
# NOTE I'm only supporting relative pos embedding for now
self.pos_embed = PosEmbedRel(feat_size, dim_head=self.dim_head_qk, scale=self.scale)
self.norm = nn.LayerNorm([self.dim_out_v * 2, *feat_size])
mlp_ratio = 4
self.mlp = Mlp(
in_features=self.dim_out_v * 2,
hidden_features=int(dim * mlp_ratio),
act_layer=nn.GELU,
out_features=dim_out,
drop=0,
use_conv=True,
)
self.pool = nn.AvgPool2d(2, 2) if stride == 2 else nn.Identity()
self.reset_parameters()
def reset_parameters(self):
trunc_normal_(self.qkv_f.weight, std=self.qkv_f.weight.shape[1] ** -0.5) # fan-in
trunc_normal_(self.qkv_p.weight, std=self.qkv_p.weight.shape[1] ** -0.5) # fan-in
trunc_normal_(self.pos_embed.height_rel, std=self.scale)
trunc_normal_(self.pos_embed.width_rel, std=self.scale)
def get_qkv(self, x, qvk_conv):
B, C, H, W = x.shape
x = qvk_conv(x) # B, (2 * dim_head_qk + dim_head_v) * num_heads, H, W
q, k, v = torch.split(x, [self.dim_out_qk, self.dim_out_qk, self.dim_out_v], dim=1)
q = q.reshape(B * self.num_heads, self.dim_head_qk, -1).transpose(-1, -2)
k = k.reshape(B * self.num_heads, self.dim_head_qk, -1) # no transpose, for q @ k
v = v.reshape(B * self.num_heads, self.dim_head_v, -1).transpose(-1, -2)
return q, k, v
def apply_attn(self, q, k, v, B, H, W, dropout=None):
if self.scale_pos_embed:
attn = (q @ k + self.pos_embed(q)) * self.scale # B * num_heads, H * W, H * W
else:
attn = (q @ k) * self.scale + self.pos_embed(q)
attn = attn.softmax(dim=-1)
if dropout:
attn = dropout(attn)
out = (attn @ v).transpose(-1, -2).reshape(B, self.dim_out_v, H, W) # B, dim_out, H, W
return out
def forward(self, x):
B, C, H, W = x.shape
dim = int(C / 2)
x1 = x[:, :dim, :, :]
x2 = x[:, dim:, :, :]
_assert(H == self.pos_embed.height, "")
_assert(W == self.pos_embed.width, "")
q_f, k_f, v_f = self.get_qkv(x1, self.qkv_f)
q_p, k_p, v_p = self.get_qkv(x2, self.qkv_p)
# person to face
out_f = self.apply_attn(q_f, k_p, v_p, B, H, W)
# face to person
out_p = self.apply_attn(q_p, k_f, v_f, B, H, W)
x_pf = torch.cat((out_f, out_p), dim=1) # B, dim_out * 2, H, W
x_pf = self.norm(x_pf)
x_pf = self.mlp(x_pf) # B, dim_out, H, W
out = self.pool(x_pf)
return out