mathsnap-deblur / nafnet.py
Monishcloud8's picture
Upload 5 files
21f5137 verified
Raw History Blame Contribute Delete
4.33 kB
"""NAFNet (megvii-research, MIT) rebuilt in PyTorch to load the mlx-community NHWC safetensors."""
import json
import numpy as np
import torch
import torch.nn as nn
from safetensors.numpy import load_file
class LayerNorm2d(nn.Module):
def __init__(self, c, eps=1e-6):
super().__init__()
self.weight = nn.Parameter(torch.ones(c))
self.bias = nn.Parameter(torch.zeros(c))
self.eps = eps
def forward(self, x):
mu = x.mean(1, keepdim=True)
var = (x - mu).pow(2).mean(1, keepdim=True)
y = (x - mu) / torch.sqrt(var + self.eps)
return self.weight.view(1, -1, 1, 1) * y + self.bias.view(1, -1, 1, 1)
class SimpleGate(nn.Module):
def forward(self, x):
a, b = x.chunk(2, dim=1)
return a * b
class NAFBlock(nn.Module):
def __init__(self, c):
super().__init__()
dw = c * 2
self.conv1 = nn.Conv2d(c, dw, 1)
self.conv2 = nn.Conv2d(dw, dw, 3, padding=1, groups=dw)
self.conv3 = nn.Conv2d(dw // 2, c, 1)
# Simplified channel attention; global average pooling (the tile size equals the TLC window).
self.sca = nn.Conv2d(dw // 2, dw // 2, 1)
self.sg = SimpleGate()
self.conv4 = nn.Conv2d(c, 2 * c, 1)
self.conv5 = nn.Conv2d(c, c, 1)
self.norm1 = LayerNorm2d(c)
self.norm2 = LayerNorm2d(c)
self.beta = nn.Parameter(torch.zeros(1, c, 1, 1))
self.gamma = nn.Parameter(torch.zeros(1, c, 1, 1))
def forward(self, inp):
x = self.sg(self.conv2(self.conv1(self.norm1(inp))))
x = x * self.sca(x.mean(dim=(2, 3), keepdim=True))
y = inp + self.conv3(x) * self.beta
x = self.conv5(self.sg(self.conv4(self.norm2(y))))
return y + x * self.gamma
class NAFNet(nn.Module):
def __init__(self, img_channel=3, width=64, middle_blk_num=1, enc_blk_nums=(1, 1, 1, 28), dec_blk_nums=(1, 1, 1, 1), **_):
super().__init__()
self.intro = nn.Conv2d(img_channel, width, 3, padding=1)
self.ending = nn.Conv2d(width, img_channel, 3, padding=1)
self.encoders, self.decoders, self.ups, self.downs = nn.ModuleList(), nn.ModuleList(), nn.ModuleList(), nn.ModuleList()
chan = width
for num in enc_blk_nums:
self.encoders.append(nn.Sequential(*[NAFBlock(chan) for _ in range(num)]))
self.downs.append(nn.Conv2d(chan, 2 * chan, 2, 2))
chan *= 2
self.middle_blks = nn.Sequential(*[NAFBlock(chan) for _ in range(middle_blk_num)])
for num in dec_blk_nums:
self.ups.append(nn.Sequential(nn.Conv2d(chan, chan * 2, 1, bias=False), nn.PixelShuffle(2)))
chan //= 2
self.decoders.append(nn.Sequential(*[NAFBlock(chan) for _ in range(num)]))
def forward(self, inp):
x = self.intro(inp)
skips = []
for enc, down in zip(self.encoders, self.downs):
x = enc(x)
skips.append(x)
x = down(x)
x = self.middle_blks(x)
for dec, up, skip in zip(self.decoders, self.ups, skips[::-1]):
x = dec(up(x) + skip)
return self.ending(x) + inp
def load(weights, config=None):
cfg = config or {"width": 64, "middle_blk_num": 1, "enc_blk_nums": [1, 1, 1, 28], "dec_blk_nums": [1, 1, 1, 1]}
model = NAFNet(**cfg)
raw = load_file(weights)
state = {}
target = model.state_dict()
for key, value in raw.items():
name = key
if key.startswith("ups.") and key.count(".") == 2:
name = key.replace(".weight", ".0.weight") # ups.N is Sequential(conv, PixelShuffle)
if value.ndim == 4 and name.endswith("weight"):
value = np.transpose(value, (0, 3, 1, 2)) # MLX (O,kH,kW,I) -> PyTorch (O,I,kH,kW)
elif name.endswith(("beta", "gamma")):
value = np.transpose(value, (0, 3, 1, 2))
state[name] = torch.from_numpy(np.ascontiguousarray(value))
missing = set(target) - set(state)
unused = set(state) - set(target)
bad = [k for k in target if k in state and tuple(target[k].shape) != tuple(state[k].shape)]
if missing or unused or bad:
raise RuntimeError(f"missing={sorted(missing)[:8]} unused={sorted(unused)[:8]} shape={bad[:8]}")
model.load_state_dict(state, strict=True)
return model.eval()