import torch import torch.nn as nn import torch.nn.functional as F from torchvision import ops from basicsr.utils.registry import ARCH_REGISTRY from einops import rearrange from basicsr.archs.arch_util import trunc_normal_ from itertools import repeat import collections.abc from typing import Tuple from pdb import set_trace as st import numpy as np f""" cust_arch.py """ # df2k download : https://github.com/dslisleedh/Download_df2k/blob/main/download_df2k.sh # dataset prepare : https://github.com/XPixelGroup/BasicSR/blob/master/docs/DatasetPreparation.md #################################################### ## LN and ConvFFN class LayerNorm(nn.Module): def __init__(self, normalized_shape, eps=1e-6, channel_first=True): super().__init__() self.channel_first = channel_first self.normalized_shape = normalized_shape self.eps = eps self.norm = nn.LayerNorm(normalized_shape, eps=eps) def forward(self, x): if self.channel_first == False: return self.norm(x) elif self.channel_first == True: x = x.permute(0, 2, 3, 1) x = self.norm(x) x = x.permute(0, 3, 1, 2) return x class dwconv(nn.Module): def __init__(self, hidden_features, kernel_size=5): super(dwconv, self).__init__() self.depthwise_conv = nn.Conv2d( hidden_features, hidden_features, kernel_size=kernel_size, stride=1, padding=(kernel_size - 1) // 2, groups=hidden_features, ) def forward(self, x, x_size): # x: [B, L, C] B, L, C = x.shape H, W = x_size x = x.transpose(1, 2).reshape(B, C, H, W) x = self.depthwise_conv(x) x = x.view(B, C, -1).transpose(1, 2) return x class ConvFFN(nn.Module): def __init__(self, in_features, hidden_features=None, out_features=None, kernel_size=5): super().__init__() out_features = out_features or in_features hidden_features = hidden_features or in_features self.fc1 = nn.Linear(in_features, hidden_features) self.dwconv = dwconv(hidden_features=hidden_features, kernel_size=kernel_size) self.fc2 = nn.Linear(hidden_features, out_features) self.act = nn.GELU() def forward(self, x, x_size): x = self.fc1(x) x = self.act(x) x = x + self.dwconv(x, x_size) x = self.fc2(x) return x ############################################################## ## Inter-Window Attn class CUSTAttention(nn.Module): def __init__(self, dim, window_size=8, group_size=9): super().__init__() ### var self.window_size = window_size # 8 -> 64 self.group_size = group_size # 9 -> 81 self.scale = dim ** -0.5 hidden_dim = dim self.to_q = nn.Linear(dim, hidden_dim) self.to_k = nn.Linear(dim, hidden_dim) self.to_v = nn.Linear(dim, dim) self.proj = nn.Linear(dim, dim) # gate before proj self.gate_proj = nn.Linear(dim, dim) self.act = nn.Sigmoid() ############ window group partition and reverse Start ############ def window_group_partition(self, x): B, C, H, W = x.shape ws, gs = self.window_size, self.group_size #################### # Pad #################### target_unit = ws * gs pad_h = (target_unit - H % target_unit) % target_unit pad_w = (target_unit - W % target_unit) % target_unit if pad_h > 0 or pad_w > 0: x = F.pad(x, (0, pad_w, 0, pad_h), mode='reflect') H_pad, W_pad = x.shape[2], x.shape[3] gh, gw = H_pad // target_unit, W_pad // target_unit # 각 h, w 별 그룹 개수 ######################################################## # Partition and Grouping # [B C (gh gs ws) (gw gs ws)] 8차원으로 한 번에 쪼개고 # [B gh gw gs gs ws ws C]로 이동. ######################################################## x = x.view(B, C, gh, gs, ws, gw, gs, ws) x = x.permute(0, 2, 5, 3, 6, 4, 7, 1) x = x.contiguous().view(B, gh * gw, gs * gs, ws * ws, C) return x, pad_h, pad_w def window_group_reverse(self, x, original_shape, padded_size): b, ng, gs_sq, ws_sq, chan = x.shape ws, gs = self.window_size, self.group_size _, _, H, W = original_shape ######################## # Pad 크기 계산 ######################## H_pad, W_pad = H + padded_size[0], W + padded_size[1] gh, gw = H_pad // (ws * gs), W_pad // (ws * gs) ########################################################## # 8차원으로 복원 후 재배치([b, c, (gh gs ws), (gw gs ws)]) # 이후 [B C H_pad W_pad]로 복구 ########################################################## x = x.view(b, gh, gw, gs, gs, ws, ws, chan) x = x.permute(0, 7, 1, 3, 5, 2, 4, 6) x = x.contiguous().view(b, chan, H_pad, W_pad) if padded_size[0] > 0 or padded_size[1] > 0: x = x[:, :, :H, :W] return x ############ window group partition and reverse End ############ def cana(self, x_grouped, sim): f""" 다음 청크를 키/밸류에 추가. 단, 다른 window를 높은 유사도로 갖는 패치는 -inf 처리 sim : [B, ng, gs, ws, gs] : 각 패치들(ws)과, 그룹 내의 윈도우들(gs) 간의 유사도 """ B, ng, gs, ws, chan = x_grouped.shape device = x_grouped.device x_grouped = x_grouped.view(B*ng, gs*ws, chan) # [배치*그룹수, 그룹사이즈*윈도우사이즈, 채널] assign_id = sim.argmax(dim=-1).view(B*ng, gs*ws) sorting_indices = torch.argsort(assign_id, dim=1) ### x와 id를 정렬된 순서대로 재배열 gather_idx = sorting_indices.unsqueeze(-1).expand(-1, -1, chan) # [B*ng, gs*ws, C]: 채널 방향으로 expand x_sorted = torch.gather(x_grouped, 1, gather_idx) # [B*ng, gs*ws, C] id_sorted = torch.gather(assign_id, 1, sorting_indices) # [B*ng, gs*ws] cs = self.window_size ** 2 # chunk_size nc = (gs*ws) // cs # num_chunk # A) Query q_chunks = x_sorted.view(B * ng, nc, cs, chan) q_ids = id_sorted.view(B * ng, nc, cs) ########################################################################### # B-2) (전 반 청크 + 현 청크 + 다음 반 청크) pad_x = torch.zeros(B*ng, cs//2, chan, device=device) pad_x = torch.cat([pad_x, x_sorted, pad_x], dim=1) pad_id = torch.full((B*ng, cs//2), -1, device=device) pad_id = torch.cat([pad_id, id_sorted, pad_id], dim=1) # [B*ng, gs*ws+64] ########################################################################### # Unfold 통해 슬라이딩 윈도우 생성(win=128, stride=64) kv_chunks = pad_x.unfold(1, cs*2, cs).permute(0, 1, 3, 2) kv_ids = pad_id.unfold(1, cs*2, cs) # [B*ng, 128, nc] ############################################################################### # Attn with Masking ############################################################################### q = self.to_q(q_chunks) # [BG, Chunks, 64, C] k = self.to_k(kv_chunks) # [BG, Chunks, 128, C] v = self.to_v(kv_chunks) # [BG, Chunks, 128, C] attn = (q @ k.transpose(-2, -1)) * self.scale # [BG, Chunks, 64, 128] ### Masking ### # Query의 ID와 Key의 ID가 같을 때만 True (같은 그룹끼리만) # q_ids: [..., 64, 1], kv_ids: [..., 1, 128] mask = (q_ids.unsqueeze(-1) == kv_ids.unsqueeze(-2)) # False인 부분(ID 불일치)을 작은값으로 마스킹 min_val = -1e4 attn = attn.masked_fill(~mask, min_val) attn = attn.softmax(dim=-1) out = attn @ v # [b*ng, Chunks, 64, C] gate = self.act(self.gate_proj(x_sorted)).view(B*ng, gs, ws, -1) # [B*ng, gs*ws, 1] out = out * gate ##################################### # Unsort & Restore(원래 순서로 복구) ##################################### out = out.view(B * ng, gs*ws, chan) out = self.proj(out) inverse_indices = torch.argsort(sorting_indices, dim=1) inverse_indices = inverse_indices.unsqueeze(-1).expand(-1, -1, chan) out = torch.gather(out, 1, inverse_indices) out = out.view(B, ng, gs, ws, chan) return out def forward(self, x): # x: [B, C, H, W] batch, chan, H, W = x.shape ################### # 이미지 -> 그룹화된 윈도우 텐서로 변환([B, num_group, group_size, win_size, c]) 뒤 키 풀링 ################### x_grouped, pad_h, pad_w = self.window_group_partition(x) ##################################### # 유사도 계산 및 다른 패치와 엮기 계산 ##################################### sim = x_grouped.detach().mean(dim=3) # [B, num_group, group_size, c] sim = torch.einsum('b g w p c, b g k c -> b g w p k', x_grouped, sim) # [B, ng, gs, ws, gs] cana_out = self.cana(x_grouped, sim) ##################### # 원래 shape로 되돌림 ##################### x = self.window_group_reverse(cana_out, x.shape, (pad_h, pad_w)) return x class CUSTBlock(nn.Module): def __init__(self, dim, window_size=8, group_size=9, ffn_scale=2.0,): super().__init__() self.pe = nn.Conv2d(dim, dim, kernel_size=3, padding=1, groups=dim) # Attention Path self.norm1 = LayerNorm(dim) self.attn = CUSTAttention(dim, window_size, group_size) # FFN Path self.norm2 = LayerNorm(dim) self.ffn = ConvFFN(dim, int(dim * ffn_scale)) def forward(self, x): x = x + self.pe(x) # 2. Attention (Pre-Norm & Residual) x = x + self.attn(self.norm1(x)) # 3. FFN (Pre-Norm & Residual) shortcut = x x = self.norm2(x) B, C, H, W = x.shape x = rearrange(x, 'b c h w -> b (h w) c') x = self.ffn(x, (H, W)) x = rearrange(x, 'b (h w) c -> b c h w', h=H, w=W) x = shortcut + x return x ############################################################## ## Intra-Window Attn def patch_divide(x, step, ps): """Crop image into patches(이미지를 지정된 크기(ps)로 자르되, 서로 겹치게 자른다.) Args: x (Tensor): Input feature map of shape(b, c, h, w). step (int): Divide step. 'ps-2' ps (int): Patch size. [16, 20, 24, 28, 16, 20, 24, 28] Returns: crop_x (Tensor): Cropped patches. nh (int): Number of patches along the horizontal direction. nw (int): Number of patches along the vertical direction. """ b, c, h, w = x.size() if h == ps and w == ps: # h==w==patch_size일 경우, step은 ps-2가 아닌 ps step = ps crop_x = [] nh = 0 ########################################################################## # if h == 100 : range(0, 98, 14) --> i = [0, 14, 28, ... 84] 이렇게 루프를 돔. # top, down = (0, 16), ... (84, 100) # down > h : 이미지 끝(h)가 패치 이동간격(step)으로 딱 떨어지지 않을 때, 자투리 공간이 남을 때 T # h=101일 때, range(0,99,14)이므로 i=[0,14,...98], 즉 i=98이 추가됨. # 이 때, down=98+16>h로, 범위를 벗어남. 그러면 (top,down)=(85,101)로, 이미지 맨 끝을 down으로 갖게 됨. # right > w : 마찬가지로, 이미지 너비(w)가 패치 이동간격(step)으로 떨어지지 않을 때 # w=75일 때, range(0, 73, 14)이므로 j=[0,14,...70], 즉 j=70이 추가됨. # 그러면 right=70+16>w이 되므로, (right,left) = (56,70)로, 이미지 맨 끝을 right로 갖게 됨. # 이렇게 하나의 높이에서 여러 개의 이미지를 crop으로 잘라냄. # nh =세로방향으로 잘라낸 개수 / nw = 총 크롭된 이미지 / 세로로 잘라낸 수 = 가로로 잘라낸 수 ########################################################################## for i in range(0, h + step - ps, step): top = i down = i + ps if down > h: top = h - ps down = h nh += 1 for j in range(0, w + step - ps, step): left = j right = j + ps if right > w: left = w - ps right = w crop_x.append(x[:, :, top:down, left:right]) nw = len(crop_x) // nh ##################################### # crop_x : [(총 crop된 횟수) x (B, dim, ps, ps)] = 42 x [B 40 16 16] # stack 및 permute로, [b 42 40 16 16]으로 만들고, nh, nw와 함께 반환 ##################################### crop_x = torch.stack(crop_x, dim=0) # (n, b, c, ps, ps) crop_x = crop_x.permute(1, 0, 2, 3, 4).contiguous() # (b, n, c, ps, ps) return crop_x, nh, nw def patch_reverse(crop_x, x, step, ps): """Reverse patches into image. Args: crop_x (Tensor): Cropped patches. [B, num_crop, dim, ps, ps] x (Tensor): Feature map of shape(b, c, h, w). step (int): Divide step. ps (int): Patch size. Returns: output (Tensor): Reversed image. [B, dim(40), H, W] """ b, c, h, w = x.size() output = torch.zeros_like(x) index = 0 #################################################### # 크롭된 이미지를 순서대로 다시 집어넣기(output에). # 순서가 range(crop_x[1]=num_crop)이 아닌, # 집어넣을 간격을 먼저 정하고, 거기에 crop_x[:,index]를 더함 #################################################### for i in range(0, h + step - ps, step): top = i down = i + ps if down > h: top = h - ps down = h for j in range(0, w + step - ps, step): left = j right = j + ps if right > w: left = w - ps right = w output[:, :, top:down, left:right] += crop_x[:, index] index += 1 #################################################### # patch overlap으로 인해, 중첩되어 2번 더해진 영역들을 2로 나눔. # [height, 2]만큼, 또는 [2, width]만큼 더해진 영역은 2번 더해졌음. # [2, 2] 영역은 4번 더해졌음. 이는 for문 2개를 돌면서 4로 나눠짐. #################################################### for i in range(step, h + step - ps, step): top = i down = i + ps - step if top + ps > h: top = h - ps output[:, :, top:down, :] /= 2 for j in range(step, w + step - ps, step): left = j right = j + ps - step if left + ps > w: left = w - ps output[:, :, :, left:right] /= 2 return output class Attention(nn.Module): """Attention module. Args: dim (int): Base channels. heads (int): Head numbers. qk_dim (int): Channels of query and key. """ def __init__(self, dim, heads, qk_dim): super().__init__() self.heads = heads self.dim = dim self.qk_dim = qk_dim self.scale = qk_dim ** -0.5 # attn self.qkv = nn.Linear(dim, dim*3, bias=False) self.gate = nn.Linear(dim, dim) self.proj = nn.Linear(dim, dim, bias=False) self.act = nn.GELU() self.pe = nn.Conv2d(dim, dim, kernel_size=3, padding=1, groups=dim) def forward(self, x): B, N, C = x.shape ws = int(N**0.5) qkv = self.qkv(x) q, k, v = qkv.split([self.qk_dim, self.qk_dim, self.dim], dim=-1) z = self.act(self.gate(x)) # attn pe = self.pe(q.transpose(1,2).view(B, C, ws, ws)).view(B, C, N).transpose(1,2) # q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h=self.heads), (q, k, v)) attn = (q @ k.transpose(-2, -1)) * self.scale attn = attn.softmax(dim=-1) out = (attn @ v) + pe # gate out = out * z # out = rearrange(out, 'b h n d -> b n (h d)') return self.proj(out) ### Frequency Modulation class Low_to_high_MS_v2(nn.Module): def __init__(self, dim): super().__init__() self.error_refiner = nn.Sequential( nn.Conv2d(dim, dim, kernel_size=3, padding=2, dilation=2, groups=dim, bias=False), nn.GELU(), nn.Conv2d(dim, dim, 1) ) self.gate_gen = nn.Sequential( nn.Conv2d(dim * 2, dim // 4, kernel_size=1), nn.GELU(), nn.Conv2d(dim // 4, 1, kernel_size=1), nn.Sigmoid() ) self.scale = nn.Parameter(torch.zeros(1, dim, 1, 1)) def forward(self, x): B, C, H, W = x.shape ################################### # 1. 계층적 오차 추출 ################################### x_d2 = F.adaptive_avg_pool2d(x, (H // 2, W // 2)) x_u2 = F.interpolate(x_d2, size=(H, W), mode='bilinear', align_corners=False) err2 = x - x_u2 x_d4 = F.adaptive_avg_pool2d(x_d2, (H // 4, W // 4)) x_u4 = F.interpolate(x_d4, size=(H, W), mode='bilinear', align_corners=False) err4 = x_u2 - x_u4 ################################# # 2. Refiner & Gate ################################# refined_error = self.error_refiner(err2 + err4) error_energies = torch.cat([err2.abs(), err4.abs()], dim=1) spatial_gate = self.gate_gen(error_energies) return x + (self.scale * refined_error * spatial_gate) class MEDA(nn.Module): """Attention module. Args: dim (int): Base channels. num (int): Number of blocks. qk_dim (int): Channels of query and key in Attention. mlp_dim (int): Channels of hidden mlp in Mlp. heads (int): Head numbers of Attention. patch_divide 및 reverse (with overlapping)의 목표 : step(stride)를 ps(patch_size)보다 작게 해서, 경계면에 있는 애들의 정보를 더 잘 파악하기 위함. """ def __init__(self, dim, qk_dim, ffn_scale=2.0, heads=1): super().__init__() self.norm1 = LayerNorm(dim, channel_first=False) self.norm2 = LayerNorm(dim, channel_first=False) self.lth = Low_to_high_MS_v2(dim) self.attn = Attention(dim, heads, qk_dim) self.ffn = ConvFFN(dim, int(dim * ffn_scale)) def forward(self, x, ps): B, C, H, W = x.shape step = ps - 2 x = self.lth(x) ############################ # Patch Divide - LN - ATTN # ps(patch_size) : [16, 20, 24, 28, 16, 20, 24, 28] # 만들어진 q,k,v(=[크롭된 이미지 개수xB, head, ps*ps, head_dim]) 간의 attn 진행 ############################ crop_x, nh, nw = patch_divide(x, step, ps) # (b, n, c, ps, ps) b, n, c, ph, pw = crop_x.shape crop_x = rearrange(crop_x, 'b n c h w -> (b n) (h w) c') crop_x = self.attn(self.norm1(crop_x)) + crop_x crop_x = rearrange(crop_x, '(b n) (h w) c -> b n c h w', n=n, w=pw) ################################ # Patch Reverse - LN - MLP(ConvFFN) # patch_reverse input : crop_x, x(첫 input), step(=patch_size - 2), ps) ################################ x = patch_reverse(crop_x, x, step, ps) _, _, h, w = x.shape x = rearrange(x, 'b c h w-> b (h w) c') x = self.ffn(self.norm2(x), x_size=(h, w)) + x x = rearrange(x, 'b (h w) c->b c h w', h=h) return x ############################################################## ## Block class MainBlock(nn.Module): def __init__(self, dim, ffn_scale=2.0, drop=0., attn_drop=0., drop_path=0., patch_size=16, window_size=8, group_size=9,): super().__init__() self.patch_size = patch_size ### Multiscale Block self.cust = CUSTBlock(dim, ffn_scale=ffn_scale, window_size=window_size, group_size=group_size,) self.meda = MEDA(dim, dim, # qk_dim ffn_scale=ffn_scale, ) ### Feedforward layer self.mid_conv = nn.Conv2d(dim, dim, 3, 1, 1) def forward(self, x): residual = x x = self.cust(x) x = self.meda(x, self.patch_size) x = self.mid_conv(x) + residual return x ############################################################## ## Overall Architecture # @ARCH_REGISTRY.register() class CUSTNet(nn.Module): def __init__(self, dim, ffn_scale=2.0, upscaling_factor=4, drop_rate=0., attn_drop_rate=0., drop_path_rate=0., patch_size=[12, 16, 20, 24, 12, 16, 20, 24], window_size=8, group_size=10,): super().__init__() self.to_feat = nn.Conv2d(3, dim, 3, 1, 1) self.dim = dim n_blocks = len(patch_size) self.pos_drop = nn.Dropout(p=drop_rate) dpr = [x.item() for x in torch.linspace(0, drop_path_rate, n_blocks)] # stochastic depth decay rule self.feats = nn.Sequential(*[MainBlock(dim, ffn_scale, drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[i], patch_size=patch_size[i], window_size=window_size, group_size=group_size, ) for i in range(n_blocks)]) # self.to_img = nn.Sequential( # nn.Conv2d(dim, 3 * upscaling_factor**2, 3, 1, 1), # nn.PixelShuffle(upscaling_factor) #) self.upscale = upscaling_factor if self.upscale == 4: self.upconv1 = nn.Conv2d(self.dim, self.dim * 4, 3, 1, 1, bias=True) self.upconv2 = nn.Conv2d(self.dim, self.dim * 4, 3, 1, 1, bias=True) self.pixel_shuffle = nn.PixelShuffle(2) elif self.upscale == 2 or self.upscale == 3: self.upconv = nn.Conv2d(self.dim, self.dim * (self.upscale ** 2), 3, 1, 1, bias=True) self.pixel_shuffle = nn.PixelShuffle(self.upscale) self.last_conv = nn.Conv2d(self.dim, 3, 3, 1, 1) if self.upscale != 1: self.lrelu = nn.LeakyReLU(negative_slope=0.1, inplace=True) self.apply(self._init_weights) def _init_weights(self, m): if isinstance(m, nn.Linear): trunc_normal_(m.weight, std=.02) if isinstance(m, nn.Linear) and m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, nn.LayerNorm): nn.init.constant_(m.bias, 0) nn.init.constant_(m.weight, 1.0) def check_img_size(self, x): _, _, h, w = x.size() downsample_scale = 8 scaled_size = self.window_size * downsample_scale mod_pad_h = (scaled_size - h % scaled_size) % scaled_size mod_pad_w = (scaled_size - w % scaled_size) % scaled_size x = F.pad(x, (0, mod_pad_w, 0, mod_pad_h), 'reflect') return x def forward(self, x): B, C, H, W = x.shape # patch embed x_feat = self.to_feat(x) # module, and return to original shape x_feat = self.feats(x_feat) + x_feat x_feat = x_feat[:, :, :H, :W] ## reconstruction if self.upscale == 4: x_feat = self.lrelu(self.pixel_shuffle(self.upconv1(x_feat))) x_feat = self.lrelu(self.pixel_shuffle(self.upconv2(x_feat))) elif self.upscale == 1: x_feat = x_feat else: x_feat = self.lrelu(self.pixel_shuffle(self.upconv(x_feat))) x_feat = self.last_conv(x_feat) if self.upscale != 1: base = F.interpolate(x, scale_factor=self.upscale, mode='bilinear', align_corners=False) else: base = x x_out = x_feat + base return x_out if __name__== '__main__': #############Test Model Complexity ############# from fvcore.nn import flop_count_table, FlopCountAnalysis, ActivationCountAnalysis # x, upscaling_factor = torch.randn(1, 3, 640, 360), 2 # x, upscaling_factor = torch.randn(1, 3, 427, 240), 3 x, upscaling_factor = torch.randn(1, 3, 320, 180), 4 # x = torch.randn(1, 3, 256, 256) window_size, group_size = 8, 10 # large # patch_size = [12,14,16,18,12,14,16,18,12,14,16,18] # branch_dim = [40] # tiny branch_dim = [30] patch_size = [18,18,18,18,18,18,18,18] dim = sum(branch_dim) model = CUSTNet(dim=dim, ffn_scale=2.0, upscaling_factor=upscaling_factor, window_size=window_size, patch_size=patch_size,) # print(model) print(f'params: {sum(map(lambda x: x.numel(), model.parameters()))}') print(flop_count_table(FlopCountAnalysis(model, x), activations=ActivationCountAnalysis(model, x))) # output = model(x) # print(output.shape)