chore: add missing scripts of PyTorch module

This commit is contained in:
SWHL committed 2026-07-02 17:58:08 +08:00
1 parent 370f01d0a8
commit 28490093df
10 files changed
+3084

No files matched your search

@@ -0,0 +1,689 @@
# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# PyTorch implementation of PPLCNetV4 backbone for PP-OCRv6.
# Converted from PaddlePaddle: PaddleOCR/ppocr/modeling/backbones/rec_lcnetv4.py
import torch
import torch.nn as nn
import torch.nn.functional as F
# ---------------------------------------------------------------------------
# Network configuration tables (identical to PaddleOCR)
# ---------------------------------------------------------------------------
NET_CONFIG_DET = {
"tiny": {
# stem(mid=16, out=32) channels: 32 -> 48 -> 64 -> 160
"stem": (16, 32),
"blocks_s1": [[3, 32, 32, 1, True], [3, 32, 32, 1, False]],
"blocks_s2": [
[3, 32, 48, 2, False],
[3, 48, 48, 1, True],
[3, 48, 48, 1, False],
],
"blocks_s3": [
[3, 48, 64, 2, False],
[3, 64, 64, 1, True],
[3, 64, 64, 1, False],
[3, 64, 64, 1, True],
[3, 64, 64, 1, False],
],
"blocks_s4": [
[3, 64, 160, 2, False],
[3, 160, 160, 1, True],
[3, 160, 160, 1, False],
],
},
"small": {
# stem(mid=24, out=48) channels: 48 -> 96 -> 192 -> 384
"stem": (24, 48),
"blocks_s1": [[3, 48, 48, 1, True], [3, 48, 48, 1, False]],
"blocks_s2": [
[3, 48, 96, 2, False],
[3, 96, 96, 1, True],
[3, 96, 96, 1, False],
],
"blocks_s3": [
[3, 96, 192, 2, False],
[3, 192, 192, 1, True],
[3, 192, 192, 1, False],
[3, 192, 192, 1, True],
[3, 192, 192, 1, False],
],
"blocks_s4": [
[3, 192, 384, 2, False],
[3, 384, 384, 1, True],
[3, 384, 384, 1, False],
],
},
"medium": {
# stem(mid=64, out=128) channels: 128 -> 256 -> 512 -> 896
"stem": (64, 128),
"blocks_s1": [[3, 128, 128, 1, True], [3, 128, 128, 1, False]],
"blocks_s2": [
[3, 128, 256, 2, False],
[3, 256, 256, 1, True],
[3, 256, 256, 1, False],
],
"blocks_s3": [
[3, 256, 512, 2, False],
[3, 512, 512, 1, True],
[3, 512, 512, 1, False],
[3, 512, 512, 1, True],
[3, 512, 512, 1, False],
],
"blocks_s4": [
[3, 512, 896, 2, False],
[3, 896, 896, 1, True],
[3, 896, 896, 1, False],
],
},
}
NET_CONFIG_REC = {
"tiny": {
# stem: simple (2xConv2D_BN+GELU, mid=24, out=48) channels: 48 -> 96 -> 160
"stem": (24, 48),
"stem_type": "simple",
"blocks2": [[3, 48, 48, 1, True]],
"blocks3": [[3, 48, 48, 1, False]],
"blocks4": [
[3, 48, 96, (2, 1), False],
[3, 96, 96, 1, True],
[3, 96, 96, 1, False],
],
"blocks5": [
[3, 96, 160, (2, 1), False],
[3, 160, 160, 1, True],
[3, 160, 160, 1, False],
[3, 160, 160, 1, False],
],
"blocks6": [],
},
"small": {
# stem: branch StemBlock (mid=48, out=96) channels: 96 -> 192 -> 384
"stem": (48, 96),
"stem_type": "branch",
"blocks2": [[3, 96, 96, 1, True]],
"blocks3": [[3, 96, 96, 1, False], [3, 96, 96, 1, False]],
"blocks4": [
[3, 96, 192, (2, 1), False],
[3, 192, 192, 1, True],
[3, 192, 192, 1, False],
[3, 192, 192, 1, True],
[3, 192, 192, 1, False],
[3, 192, 192, 1, True],
[3, 192, 192, 1, False],
],
"blocks5": [
[3, 192, 384, (2, 1), False],
[3, 384, 384, 1, True],
[3, 384, 384, 1, False],
],
"blocks6": [],
},
"medium": {
# stem: branch StemBlock (mid=64, out=128) channels: 128 -> 256 -> 512 -> 768
"stem": (64, 128),
"stem_type": "branch",
"blocks2": [[3, 128, 128, 1, True]],
"blocks3": [
[3, 128, 256, 1, False],
[3, 256, 256, 1, False],
[3, 256, 256, 1, True],
],
"blocks4": [
[3, 256, 512, (2, 1), False],
[3, 512, 512, 1, True],
[3, 512, 512, 1, False],
[3, 512, 512, 1, True],
[3, 512, 512, 1, False],
[3, 512, 512, 1, True],
[3, 512, 512, 1, False],
],
"blocks5": [
[3, 512, 768, (2, 1), False],
[3, 768, 768, 1, True],
[3, 768, 768, 1, False],
],
"blocks6": [],
},
}
# ---------------------------------------------------------------------------
# PyTorch building blocks
# ---------------------------------------------------------------------------
class Conv2D_BN(nn.Sequential):
"""Conv2D + BatchNorm2d (bias=False). Supports fuse() for deployment."""
def __init__(
self,
in_channels,
out_channels,
kernel_size=1,
stride=1,
padding=0,
groups=1,
bn_weight_init=1.0,
):
super().__init__()
self.add_module(
"conv",
nn.Conv2d(
in_channels,
out_channels,
kernel_size,
stride,
padding,
groups=groups,
bias=False,
),
)
bn = nn.BatchNorm2d(out_channels)
nn.init.constant_(bn.weight, 1.0 if bn_weight_init == 1.0 else 0.0)
nn.init.constant_(bn.bias, 0.0)
self.add_module("bn", bn)
@torch.no_grad()
def fuse(self):
"""Fuse Conv2d + BatchNorm2d into a single Conv2d (bias=True)."""
c, bn = self.conv, self.bn
w = bn.weight / (bn.running_var + bn.eps) ** 0.5
w = c.weight * w[:, None, None, None]
b = bn.bias - bn.running_mean * bn.weight / (bn.running_var + bn.eps) ** 0.5
# Handle 'same' padding
if isinstance(c.padding, str):
padding = (c.kernel_size[0] - 1) // 2
else:
padding = c.padding
m = nn.Conv2d(
w.shape[1] * c.groups,
w.shape[0],
w.shape[2:],
stride=c.stride,
padding=padding,
groups=c.groups,
)
m.weight.data.copy_(w)
m.bias.data.copy_(b)
return m
class ConvBNAct(nn.Module):
"""Conv2d + BN + ReLU. Supports rep() that fuses Conv+BN into one Conv."""
def __init__(
self,
in_channels,
out_channels,
kernel_size=3,
stride=1,
padding=1,
groups=1,
use_act=True,
lr_mult=1.0,
):
super().__init__()
self.use_act = use_act
self.is_repped = False
if isinstance(padding, str) and padding.upper() == "SAME":
# Use PyTorch's native 'same' padding (handles asymmetric case correctly)
padding = "same"
self.conv = nn.Conv2d(
in_channels,
out_channels,
kernel_size,
stride,
padding=padding,
groups=groups,
bias=False,
)
self.bn = nn.BatchNorm2d(out_channels)
if self.use_act:
self.act = nn.ReLU(inplace=True)
def forward(self, x):
x = self.conv(x)
if not self.is_repped:
x = self.bn(x)
if self.use_act:
x = self.act(x)
return x
@torch.no_grad()
def rep(self):
"""Fuse Conv2d + BatchNorm2d into a single Conv2d (bias=True)."""
if self.is_repped:
return
c, bn = self.conv, self.bn
w = bn.weight / (bn.running_var + bn.eps) ** 0.5
fused_w = c.weight * w[:, None, None, None]
fused_b = (
bn.bias - bn.running_mean * bn.weight / (bn.running_var + bn.eps) ** 0.5
)
# Handle 'same' padding
if isinstance(c.padding, str):
padding = (
c.padding[0]
if isinstance(c.padding, tuple)
else (c.kernel_size[0] - 1) // 2
)
else:
padding = c.padding
m = nn.Conv2d(
c.in_channels,
c.out_channels,
c.kernel_size,
stride=c.stride,
padding=padding,
dilation=c.dilation,
groups=c.groups,
)
m.weight.data.copy_(fused_w)
m.bias.data.copy_(fused_b)
self.conv = m
del self.bn
self.is_repped = True
class StemBlock(nn.Module):
"""Multi-branch stem with total stride 4 (stem1 stride=2 + stem3 stride=2)."""
def __init__(self, in_channels=3, mid_channels=48, out_channels=96, lr_mult=1.0):
super().__init__()
self.is_repped = False
self.stem1 = ConvBNAct(
in_channels, mid_channels, 3, 2, use_act=True, lr_mult=lr_mult
)
self.stem2a = ConvBNAct(
mid_channels,
mid_channels // 2,
2,
1,
padding="SAME",
use_act=True,
lr_mult=lr_mult,
)
self.stem2b = ConvBNAct(
mid_channels // 2,
mid_channels,
2,
1,
padding="SAME",
use_act=True,
lr_mult=lr_mult,
)
self.stem3 = ConvBNAct(
mid_channels * 2, mid_channels, 3, 2, use_act=True, lr_mult=lr_mult
)
self.stem4 = ConvBNAct(
mid_channels, out_channels, 1, 1, padding=0, use_act=True, lr_mult=lr_mult
)
self.pool = nn.MaxPool2d(kernel_size=2, stride=1, padding=0)
# Note: Paddle uses padding="SAME" (asymmetric), we use explicit F.pad
def forward(self, x):
x = self.stem1(x)
x2 = self.stem2b(self.stem2a(x))
# Explicit padding to match Paddle's MaxPool2D with padding="SAME"
# kernel=2, stride=1, "SAME" -> pad (right=1, bottom=1) only
x_pool = F.pad(x, [0, 1, 0, 1])
x1 = self.pool(x_pool)
x = self.stem4(self.stem3(torch.cat([x1, x2], dim=1)))
return x
def rep(self, fuse_lab=None):
if self.is_repped:
return
for attr in ("stem1", "stem2a", "stem2b", "stem3", "stem4"):
getattr(self, attr).rep()
self.is_repped = True
class SELayer(nn.Module):
"""Squeeze-and-Excitation layer (reduction=4, Hardsigmoid gating)."""
def __init__(self, channel, reduction=4, lr_mult=1.0):
super().__init__()
self.conv1 = nn.Conv2d(channel, channel // reduction, 1)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(channel // reduction, channel, 1)
self.hardsigmoid = nn.Hardsigmoid()
def forward(self, x):
identity = x
x = x.mean(dim=[2, 3], keepdim=True)
x = self.relu(self.conv1(x))
x = self.hardsigmoid(self.conv2(x))
return x * identity
class RepDWConv(nn.Module):
"""Reparameterizable depthwise convolution.
Training: 3-branch (3x3 DW + 1x1 DW + identity BN)
Inference: fused into a single 3x3 DW Conv
"""
def __init__(self, channels, kernel_size=3):
super().__init__()
self.channels = channels
self.kernel_size = kernel_size
padding = (kernel_size - 1) // 2
self.conv = Conv2D_BN(
channels, channels, kernel_size, 1, padding, groups=channels
)
self.conv1 = nn.Conv2d(channels, channels, 1, 1, 0, groups=channels, bias=False)
self.bn = nn.BatchNorm2d(channels)
nn.init.constant_(self.bn.weight, 1.0)
nn.init.constant_(self.bn.bias, 0.0)
self.is_repped = False
self.reparam_conv = None
def forward(self, x):
if self.is_repped:
return self.reparam_conv(x)
return self.bn(self.conv(x) + self.conv1(x) + x)
def rep(self, fuse_lab=None):
if self.is_repped:
return
fused = self._fuse_conv()
padding = (self.kernel_size - 1) // 2
self.reparam_conv = nn.Conv2d(
self.channels,
self.channels,
self.kernel_size,
1,
padding,
groups=self.channels,
)
self.reparam_conv.weight.data.copy_(fused.weight)
self.reparam_conv.bias.data.copy_(fused.bias)
del self.conv
del self.conv1
del self.bn
self.is_repped = True
@torch.no_grad()
def _fuse_conv(self):
conv = self.conv.fuse()
pad_size = self.kernel_size // 2
conv1_w = F.pad(self.conv1.weight, [pad_size, pad_size, pad_size, pad_size])
identity = F.pad(
torch.ones(
[self.conv1.weight.shape[0], self.conv1.weight.shape[1], 1, 1],
device=self.conv1.weight.device,
dtype=self.conv1.weight.dtype,
),
[pad_size, pad_size, pad_size, pad_size],
)
w = conv.weight + conv1_w + identity
conv.weight.data.copy_(w)
bn = self.bn
scale = bn.weight / (bn.running_var + bn.eps) ** 0.5
conv.weight.data.copy_(conv.weight * scale[:, None, None, None])
conv.bias.data.copy_(bn.bias + (conv.bias - bn.running_mean) * scale)
return conv
class LCNetV4Block(nn.Module):
"""LCNetV4 block for detection and recognition.
Token mixer: RepDWConv when stride=1 and in==out, else plain Conv2D_BN DW conv.
Channel mixer: expand -> act -> compress (+ residual when stride=1 and in==out)
rep() fuses all Conv2D_BN layers (mathematically exact, no accuracy change).
"""
def __init__(
self,
in_channels,
out_channels,
stride,
dw_size,
use_se=False,
lr_mult=1.0,
expand_ratio=2,
act_type="gelu",
):
super().__init__()
self.is_repped = False
self.has_residual = in_channels == out_channels and stride == 1
self.use_rep_dw = stride == 1 and in_channels == out_channels
self.token_mixer = nn.Sequential()
if self.use_rep_dw:
self.token_mixer.add_module("rep_dw", RepDWConv(in_channels, dw_size))
else:
padding = (dw_size - 1) // 2
self.token_mixer.add_module(
"dw_conv",
Conv2D_BN(
in_channels,
in_channels,
dw_size,
stride,
padding,
groups=in_channels,
),
)
if use_se:
self.token_mixer.add_module("se", SELayer(in_channels, lr_mult=lr_mult))
hidden_channels = int(in_channels * expand_ratio)
compress_bn_init = 0.0 if self.has_residual else 1.0
self.channel_mixer = nn.Sequential()
self.channel_mixer.add_module(
"expand", Conv2D_BN(in_channels, hidden_channels, 1, 1, 0)
)
if act_type == "gelu":
self.channel_mixer.add_module("act", nn.GELU())
elif act_type == "hswish":
self.channel_mixer.add_module("act", nn.Hardswish())
elif act_type == "relu":
self.channel_mixer.add_module("act", nn.ReLU(inplace=True))
self.channel_mixer.add_module(
"compress",
Conv2D_BN(
hidden_channels,
out_channels,
1,
1,
0,
bn_weight_init=compress_bn_init,
),
)
def forward(self, x):
x = self.token_mixer(x)
if self.has_residual:
return x + self.channel_mixer(x)
return self.channel_mixer(x)
def rep(self, fuse_lab=None):
if self.is_repped:
return
if self.use_rep_dw:
self.token_mixer.rep_dw.rep(fuse_lab=fuse_lab)
else:
self.token_mixer.dw_conv = self.token_mixer.dw_conv.fuse()
for name in ("expand", "compress"):
m = getattr(self.channel_mixer, name, None)
if isinstance(m, Conv2D_BN):
setattr(self.channel_mixer, name, m.fuse())
self.is_repped = True
# ---------------------------------------------------------------------------
# PPLCNetV4 – unified backbone for det & rec
# ---------------------------------------------------------------------------
class PPLCNetV4(nn.Module):
"""Unified PPLCNetV4 backbone for text detection and recognition.
Detection (det=True):
model_size in {'tiny', 'small', 'medium'} -- see NET_CONFIG_DET.
Returns 4-level feature list [s1_out, s2_out, s3_out, s4_out].
Recognition (det=False):
model_size in {'tiny', 'small', 'medium'} -- see NET_CONFIG_REC.
Returns pooled feature tensor [B, C, 1, W].
"""
def __init__(
self,
det=False,
model_size="small",
in_channels=3,
lr_mult_list=None,
**kwargs,
):
super().__init__()
if lr_mult_list is None:
lr_mult_list = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
self.det = det
self.is_repped = False
if det:
assert (
model_size in NET_CONFIG_DET
), "det model_size must be one of {} but got '{}'".format(
list(NET_CONFIG_DET.keys()), model_size
)
cfg = NET_CONFIG_DET[model_size]
stem_mid, stem_out = cfg["stem"]
self.stem = StemBlock(in_channels, stem_mid, stem_out)
def make_stage(key):
return nn.Sequential(
*[
LCNetV4Block(in_c, out_c, s, k, se, expand_ratio=2)
for k, in_c, out_c, s, se in cfg[key]
]
)
self.blocks_s1 = make_stage("blocks_s1")
self.blocks_s2 = make_stage("blocks_s2")
self.blocks_s3 = make_stage("blocks_s3")
self.blocks_s4 = make_stage("blocks_s4")
self.out_channels = [
cfg["blocks_s1"][-1][2],
cfg["blocks_s2"][-1][2],
cfg["blocks_s3"][-1][2],
cfg["blocks_s4"][-1][2],
]
else:
assert isinstance(lr_mult_list, (list, tuple)) and len(lr_mult_list) == 6
assert (
model_size in NET_CONFIG_REC
), "rec model_size must be one of {} but got '{}'".format(
list(NET_CONFIG_REC.keys()), model_size
)
self.lr_mult_list = lr_mult_list
cfg = NET_CONFIG_REC[model_size]
stem_mid, stem_out = cfg["stem"]
if cfg["stem_type"] == "branch":
self.conv1 = StemBlock(3, stem_mid, stem_out, lr_mult=lr_mult_list[0])
else:
self.conv1 = nn.Sequential(
Conv2D_BN(3, stem_mid, 3, 2, 1),
nn.GELU(),
Conv2D_BN(stem_mid, stem_out, 3, 2, 1),
)
def make_stage(stage_name, lr_idx):
return nn.Sequential(
*[
LCNetV4Block(
in_c,
out_c,
s,
k,
se,
lr_mult=lr_mult_list[lr_idx],
expand_ratio=2,
)
for k, in_c, out_c, s, se in cfg.get(stage_name, [])
]
)
self.blocks2 = make_stage("blocks2", 1)
self.blocks3 = make_stage("blocks3", 2)
self.blocks4 = make_stage("blocks4", 3)
self.blocks5 = make_stage("blocks5", 4)
self.blocks6 = make_stage("blocks6", 5)
for sname in reversed(
["blocks2", "blocks3", "blocks4", "blocks5", "blocks6"]
):
if cfg.get(sname):
self.out_channels = cfg[sname][-1][2]
break
def forward(self, x):
if self.det:
x = self.stem(x)
o1 = self.blocks_s1(x)
o2 = self.blocks_s2(o1)
o3 = self.blocks_s3(o2)
o4 = self.blocks_s4(o3)
return [o1, o2, o3, o4]
else:
x = self.conv1(x)
x = self.blocks2(x)
x = self.blocks3(x)
x = self.blocks4(x)
x = self.blocks5(x)
x = self.blocks6(x)
if self.training:
x = F.adaptive_avg_pool2d(x, [1, 40])
else:
assert x.shape[2] >= 3, f"Feature height {x.shape[2]} < pool kernel 3."
x = F.avg_pool2d(x, [3, 2])
return x
def rep(self, fuse_lab=None):
if self.is_repped:
return
if self.det:
self.stem.rep()
for stage in [
self.blocks_s1,
self.blocks_s2,
self.blocks_s3,
self.blocks_s4,
]:
for block in stage:
block.rep(fuse_lab=fuse_lab)
else:
if hasattr(self.conv1, "rep"):
self.conv1.rep()
for stage in [
self.blocks2,
self.blocks3,
self.blocks4,
self.blocks5,
self.blocks6,
]:
for block in stage:
block.rep()
self.is_repped = True
@@ -0,0 +1,287 @@
# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve.
# copyright (c) 2024 PytorchOCR Authors. All Rights Reserve.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
PP-LCNet v1 backbone implementation for PyTorch.
PP-LCNet: A Lightweight CPU Convolutional Neural Network
Paper: https://arxiv.org/abs/2109.15099
This backbone implements the original PP-LCNet (v1) architecture used in
PaddleOCR's textline_ori and doc_ori models.
The backbone includes the GAP + 1280-dim 1x1 conv + H-Swish + Dropout
that is characteristic of PP-LCNet, allowing ClsHead to work directly.
"""
from __future__ import absolute_import, division, print_function
import torch.nn as nn
# PP-LCNet network configuration (original ImageNet style)
# Format: blocks{stage}: [kernel_size, in_channels, out_channels, stride, use_se]
NET_CONFIG = {
"blocks2": [
[3, 16, 32, 1, False],
],
"blocks3": [
[3, 32, 64, 2, False],
[3, 64, 64, 1, False],
],
"blocks4": [
[3, 64, 128, 2, False],
[3, 128, 128, 1, False],
],
"blocks5": [
[3, 128, 256, 2, False],
[5, 256, 256, 1, False],
[5, 256, 256, 1, False],
[5, 256, 256, 1, False],
[5, 256, 256, 1, False],
[5, 256, 256, 1, False],
],
"blocks6": [
[5, 256, 512, 2, True],
[5, 512, 512, 1, True],
],
}
def make_divisible(v, divisor=8, min_value=None):
"""Make channel divisible by divisor (same as PaddleClas)."""
if min_value is None:
min_value = divisor
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
if new_v < 0.9 * v:
new_v += divisor
return new_v
class DepthSepConv(nn.Module):
"""
Depthwise Separable Convolution block (MobileNetV1 style).
PP-LCNet uses simple DepthSepConv without residual connections.
DW Conv(kxk, groups=in_ch) → BN → H-Swish → PW Conv(1x1) → BN → H-Swish
"""
def __init__(self, in_channels, out_channels, kernel_size, stride=1):
super().__init__()
self.dw_conv = nn.Conv2d(
in_channels=in_channels,
out_channels=in_channels,
kernel_size=kernel_size,
stride=stride,
padding=(kernel_size - 1) // 2,
groups=in_channels,
bias=False,
)
self.dw_bn = nn.BatchNorm2d(in_channels)
self.dw_act = nn.Hardswish(inplace=True)
self.pw_conv = nn.Conv2d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=1,
stride=1,
padding=0,
bias=False,
)
self.pw_bn = nn.BatchNorm2d(out_channels)
self.pw_act = nn.Hardswish(inplace=True)
def forward(self, x):
x = self.dw_conv(x)
x = self.dw_bn(x)
x = self.dw_act(x)
x = self.pw_conv(x)
x = self.pw_bn(x)
x = self.pw_act(x)
return x
class SELayer(nn.Module):
"""Squeeze-and-Excitation module (PP-LCNet uses reduction=4)."""
def __init__(self, channel, reduction=4):
super().__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.conv1 = nn.Conv2d(
in_channels=channel,
out_channels=channel // reduction,
kernel_size=1,
stride=1,
padding=0,
bias=True,
)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(
in_channels=channel // reduction,
out_channels=channel,
kernel_size=1,
stride=1,
padding=0,
bias=True,
)
self.hardsigmoid = nn.Hardsigmoid(inplace=True)
def forward(self, x):
identity = x
x = self.avg_pool(x)
x = self.conv1(x)
x = self.relu(x)
x = self.conv2(x)
x = self.hardsigmoid(x)
return identity * x
class PPLCNetBlock(nn.Module):
"""PP-LCNet basic block: DW Conv → BN → H-Swish → (SE) → PW Conv → BN → H-Swish."""
def __init__(self, in_channels, out_channels, kernel_size, stride=1, use_se=False):
super().__init__()
self.use_se = use_se
# Depthwise conv
self.dw_conv = nn.Conv2d(
in_channels=in_channels,
out_channels=in_channels,
kernel_size=kernel_size,
stride=stride,
padding=(kernel_size - 1) // 2,
groups=in_channels,
bias=False,
)
self.dw_bn = nn.BatchNorm2d(in_channels)
self.dw_act = nn.Hardswish(inplace=True)
# SE on intermediate features (after DW, before PW)
if use_se:
self.se = SELayer(in_channels, reduction=4)
# Pointwise conv
self.pw_conv = nn.Conv2d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=1,
stride=1,
padding=0,
bias=False,
)
self.pw_bn = nn.BatchNorm2d(out_channels)
self.pw_act = nn.Hardswish(inplace=True)
def forward(self, x):
x = self.dw_conv(x)
x = self.dw_bn(x)
x = self.dw_act(x)
if self.use_se:
x = self.se(x)
x = self.pw_conv(x)
x = self.pw_bn(x)
x = self.pw_act(x)
return x
class PPLCNet(nn.Module):
"""
PP-LCNet v1 backbone for classification tasks.
Used in PaddleOCR's textline_ori and doc_ori models.
Outputs a 1280-dim feature vector (after GAP + 1x1 Conv + H-Swish + Dropout),
suitable for direct use with ClsHead.
Args:
scale (float): Channel scaling factor (0.25, 0.35, 0.5, 0.75, 1.0, etc.)
class_num (int): Number of output classes (NOT used here; ClsHead handles classification).
Included for compatibility but ignored.
"""
def __init__(self, scale=1.0, **kwargs):
super().__init__()
self.scale = scale
self.net_config = NET_CONFIG
# Stem: Conv2D(3→16*scale, k=3, s=2)
stem_out = make_divisible(16 * scale)
self.conv1 = nn.Conv2d(
in_channels=3,
out_channels=stem_out,
kernel_size=3,
stride=2,
padding=1,
bias=False,
)
self.conv1_bn = nn.BatchNorm2d(stem_out)
self.conv1_act = nn.Hardswish(inplace=True)
# Blocks 2-6 (stages)
self.blocks2 = self._make_stage(self.net_config["blocks2"])
self.blocks3 = self._make_stage(self.net_config["blocks3"])
self.blocks4 = self._make_stage(self.net_config["blocks4"])
self.blocks5 = self._make_stage(self.net_config["blocks5"])
self.blocks6 = self._make_stage(self.net_config["blocks6"])
# Last stage output channels (after scale)
last_out = make_divisible(512 * scale)
# PP-LCNet characteristic: GAP → 1x1 Conv(→1280) → H-Swish → Dropout
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.last_conv = nn.Conv2d(
in_channels=last_out,
out_channels=1280,
kernel_size=1,
stride=1,
padding=0,
bias=False,
)
self.last_act = nn.Hardswish(inplace=True)
self.dropout = nn.Dropout(p=0.2)
self.out_channels = 1280
def _make_stage(self, stage_config):
"""Build a sequential stage from config list."""
layers = []
for k, in_c, out_c, s, se in stage_config:
layers.append(
PPLCNetBlock(
in_channels=make_divisible(in_c * self.scale),
out_channels=make_divisible(out_c * self.scale),
kernel_size=k,
stride=s,
use_se=se,
)
)
return nn.Sequential(*layers)
def forward(self, x):
# Stem
x = self.conv1(x)
x = self.conv1_bn(x)
x = self.conv1_act(x)
# Feature extraction stages
x = self.blocks2(x)
x = self.blocks3(x)
x = self.blocks4(x)
x = self.blocks5(x)
x = self.blocks6(x)
# Head projection (PP-LCNet characteristic)
x = self.avg_pool(x) # → [B, C, 1, 1]
x = self.last_conv(x) # → [B, 1280, 1, 1]
x = self.last_act(x)
x = self.dropout(x)
return x
@@ -0,0 +1,163 @@
import torch
import torch.nn.functional as F
from torch import nn
from torch.nn import Linear
from torch.nn.init import xavier_uniform_
class MultiheadAttention(nn.Module):
"""Allows the model to jointly attend to information
from different representation subspaces.
See reference: Attention Is All You Need
.. math::
\text{MultiHead}(Q, K, V) = \text{Concat}(head_1,\dots,head_h)W^O
\text{where} head_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)
Args:
embed_dim: total dimension of the model
num_heads: parallel attention layers, or heads
"""
def __init__(
self,
embed_dim,
num_heads,
dropout=0.0,
bias=True,
add_bias_kv=False,
add_zero_attn=False,
):
super(MultiheadAttention, self).__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.dropout = dropout
self.head_dim = embed_dim // num_heads
assert (
self.head_dim * num_heads == self.embed_dim
), "embed_dim must be divisible by num_heads"
self.scaling = self.head_dim**-0.5
self.out_proj = Linear(embed_dim, embed_dim, bias=bias)
self._reset_parameters()
self.conv1 = torch.nn.Conv2d(
in_channels=embed_dim, out_channels=embed_dim, kernel_size=(1, 1)
)
self.conv2 = torch.nn.Conv2d(
in_channels=embed_dim, out_channels=embed_dim, kernel_size=(1, 1)
)
self.conv3 = torch.nn.Conv2d(
in_channels=embed_dim, out_channels=embed_dim, kernel_size=(1, 1)
)
def _reset_parameters(self):
xavier_uniform_(self.out_proj.weight)
def forward(
self,
query,
key,
value,
key_padding_mask=None,
incremental_state=None,
attn_mask=None,
):
"""
Inputs of forward function
query: [target length, batch size, embed dim]
key: [sequence length, batch size, embed dim]
value: [sequence length, batch size, embed dim]
key_padding_mask: if True, mask padding based on batch size
incremental_state: if provided, previous time steps are cashed
need_weights: output attn_output_weights
static_kv: key and value are static
Outputs of forward function
attn_output: [target length, batch size, embed dim]
attn_output_weights: [batch size, target length, sequence length]
"""
q_shape = query.shape
src_shape = key.shape
q = self._in_proj_q(query)
k = self._in_proj_k(key)
v = self._in_proj_v(value)
q *= self.scaling
# q = paddle.transpose(
# paddle.reshape(
# q, [q_shape[0], q_shape[1], self.num_heads, self.head_dim]),
# [1, 2, 0, 3])
q = torch.reshape(q, (q_shape[0], q_shape[1], self.num_heads, self.head_dim))
q = q.permute(1, 2, 0, 3)
# k = paddle.transpose(
# paddle.reshape(
# k, [src_shape[0], q_shape[1], self.num_heads, self.head_dim]),
# [1, 2, 0, 3])
k = torch.reshape(k, (src_shape[0], q_shape[1], self.num_heads, self.head_dim))
k = k.permute(1, 2, 0, 3)
# v = paddle.transpose(
# paddle.reshape(
# v, [src_shape[0], q_shape[1], self.num_heads, self.head_dim]),
# [1, 2, 0, 3])
v = torch.reshape(v, (src_shape[0], q_shape[1], self.num_heads, self.head_dim))
v = v.permute(1, 2, 0, 3)
if key_padding_mask is not None:
assert key_padding_mask.shape[0] == q_shape[1]
assert key_padding_mask.shape[1] == src_shape[0]
attn_output_weights = torch.matmul(q, k.permute(0, 1, 3, 2))
if attn_mask is not None:
attn_mask = torch.unsqueeze(torch.unsqueeze(attn_mask, 0), 0)
attn_output_weights += attn_mask
if key_padding_mask is not None:
attn_output_weights = torch.reshape(
attn_output_weights,
[q_shape[1], self.num_heads, q_shape[0], src_shape[0]],
)
key = torch.unsqueeze(torch.unsqueeze(key_padding_mask, 1), 2)
key = key.type(torch.float32)
y = torch.full(
size=key.shape, fill_value=float("-Inf"), dtype=torch.float32
)
y = torch.where(key == 0.0, key, y)
attn_output_weights += y
attn_output_weights = F.softmax(
attn_output_weights.type(torch.float32),
dim=-1,
dtype=torch.float32
if attn_output_weights.dtype == torch.float16
else attn_output_weights.dtype,
)
attn_output_weights = F.dropout(
attn_output_weights, p=self.dropout, training=self.training
)
attn_output = torch.matmul(attn_output_weights, v)
attn_output = torch.reshape(
attn_output.permute(2, 0, 1, 3), [q_shape[0], q_shape[1], self.embed_dim]
)
attn_output = self.out_proj(attn_output)
return attn_output
def _in_proj_q(self, query):
query = query.permute(1, 2, 0)
query = torch.unsqueeze(query, dim=2)
res = self.conv1(query)
res = torch.squeeze(res, dim=2)
res = res.permute(2, 0, 1)
return res
def _in_proj_k(self, key):
key = key.permute(1, 2, 0)
key = torch.unsqueeze(key, dim=2)
res = self.conv2(key)
res = torch.squeeze(res, dim=2)
res = res.permute(2, 0, 1)
return res
def _in_proj_v(self, value):
value = value.permute(1, 2, 0) # (1, 2, 0)
value = torch.unsqueeze(value, dim=2)
res = self.conv3(value)
res = torch.squeeze(res, dim=2)
res = res.permute(2, 0, 1)
return res
@@ -0,0 +1,838 @@
import copy
import math
import numpy as np
import torch
import torch.nn.functional as F
from torch import nn
from torch.nn import Dropout, LayerNorm
from torch.nn import ModuleList as LayerList
from torch.nn.init import xavier_normal_, xavier_uniform_
from .multiheadAttention import MultiheadAttention
class Transformer(nn.Module):
"""A transformer model. User is able to modify the attributes as needed. The architechture
is based on the paper "Attention Is All You Need". Ashish Vaswani, Noam Shazeer,
Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, Lukasz Kaiser, and
Illia Polosukhin. 2017. Attention is all you need. In Advances in Neural Information
Processing Systems, pages 6000-6010.
Args:
d_model: the number of expected features in the encoder/decoder inputs (default=512).
nhead: the number of heads in the multiheadattention models (default=8).
num_encoder_layers: the number of sub-encoder-layers in the encoder (default=6).
num_decoder_layers: the number of sub-decoder-layers in the decoder (default=6).
dim_feedforward: the dimension of the feedforward network model (default=2048).
dropout: the dropout value (default=0.1).
custom_encoder: custom encoder (default=None).
custom_decoder: custom decoder (default=None).
"""
def __init__(
self,
d_model=512,
nhead=8,
num_encoder_layers=6,
beam_size=0,
num_decoder_layers=6,
max_len=25,
dim_feedforward=1024,
attention_dropout_rate=0.0,
residual_dropout_rate=0.1,
custom_encoder=None,
custom_decoder=None,
in_channels=0,
out_channels=0,
scale_embedding=True,
):
super(Transformer, self).__init__()
self.out_channels = out_channels # out_channels + 1
self.max_len = max_len
self.embedding = Embeddings(
d_model=d_model,
vocab=self.out_channels,
padding_idx=0,
scale_embedding=scale_embedding,
)
self.positional_encoding = PositionalEncoding(
dropout=residual_dropout_rate,
dim=d_model,
)
if custom_encoder is not None:
self.encoder = custom_encoder
else:
if num_encoder_layers > 0:
encoder_layer = TransformerEncoderLayer(
d_model,
nhead,
dim_feedforward,
attention_dropout_rate,
residual_dropout_rate,
)
self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers)
else:
self.encoder = None
if custom_decoder is not None:
self.decoder = custom_decoder
else:
decoder_layer = TransformerDecoderLayer(
d_model,
nhead,
dim_feedforward,
attention_dropout_rate,
residual_dropout_rate,
)
self.decoder = TransformerDecoder(decoder_layer, num_decoder_layers)
self._reset_parameters()
self.beam_size = beam_size
self.d_model = d_model
self.nhead = nhead
self.tgt_word_prj = nn.Linear(d_model, self.out_channels, bias=False)
w0 = np.random.normal(
0.0, d_model**-0.5, (self.out_channels, d_model)
).astype(np.float32)
self.tgt_word_prj.weight.data = torch.from_numpy(w0)
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Conv2d):
xavier_normal_(m.weight)
if m.bias is not None:
torch.nn.init.zeros_(m.bias)
def forward_train(self, src, tgt):
tgt = tgt[:, :-1]
tgt_key_padding_mask = self.generate_padding_mask(tgt)
tgt = self.embedding(tgt).permute(1, 0, 2)
tgt = self.positional_encoding(tgt)
tgt_mask = self.generate_square_subsequent_mask(tgt.shape[0], tgt.device)
if self.encoder is not None:
src = self.positional_encoding(src.permute(1, 0, 2))
memory = self.encoder(src)
else:
memory = src.squeeze(2).permute(2, 0, 1)
output = self.decoder(
tgt,
memory,
tgt_mask=tgt_mask,
memory_mask=None,
tgt_key_padding_mask=tgt_key_padding_mask,
memory_key_padding_mask=None,
)
output = output.permute(1, 0, 2)
logit = self.tgt_word_prj(output)
return logit
def forward(self, src, targets=None):
"""Take in and process masked source/target sequences.
Args:
src: the sequence to the encoder (required).
tgt: the sequence to the decoder (required).
Shape:
- src: :math:`(S, N, E)`.
- tgt: :math:`(T, N, E)`.
Examples:
>>> output = transformer_model(src, tgt)
"""
if self.training:
max_len = targets[1].max()
tgt = targets[0][:, : 2 + max_len]
return self.forward_train(src, tgt)
else:
if self.beam_size > 0:
return self.forward_beam(src)
else:
return self.forward_test(src)
def forward_test(self, src):
bs = src.shape[0]
if self.encoder is not None:
src = self.positional_encoding(src.permute(1, 0, 2))
memory = self.encoder(src)
else:
memory = torch.squeeze(src, 2).permute(2, 0, 1)
dec_seq = torch.full((bs, 1), 2, dtype=torch.int64)
dec_prob = torch.full((bs, 1), 1.0, dtype=torch.float32)
for len_dec_seq in range(1, 25):
dec_seq_embed = self.embedding(dec_seq).permute(1, 0, 2)
dec_seq_embed = self.positional_encoding(dec_seq_embed)
tgt_mask = self.generate_square_subsequent_mask(dec_seq_embed.shape[0])
output = self.decoder(
dec_seq_embed,
memory,
tgt_mask=tgt_mask,
memory_mask=None,
tgt_key_padding_mask=None,
memory_key_padding_mask=None,
)
dec_output = output.permute(1, 0, 2)
dec_output = dec_output[:, -1, :]
tgt_word_prj = self.tgt_word_prj(dec_output)
word_prob = F.softmax(tgt_word_prj, dim=1)
preds_idx = word_prob.argmax(dim=1)
if torch.equal(
preds_idx, torch.full(preds_idx.shape, 3, dtype=torch.int64)
):
break
preds_prob = torch.max(word_prob, dim=1).values
dec_seq = torch.cat([dec_seq, torch.reshape(preds_idx, (-1, 1))], dim=1)
dec_prob = torch.cat([dec_prob, torch.reshape(preds_prob, (-1, 1))], dim=1)
return [dec_seq, dec_prob]
def forward_beam(self, images):
"""Translation work in one batch"""
def get_inst_idx_to_tensor_position_map(inst_idx_list):
"""Indicate the position of an instance in a tensor."""
return {
inst_idx: tensor_position
for tensor_position, inst_idx in enumerate(inst_idx_list)
}
def collect_active_part(
beamed_tensor, curr_active_inst_idx, n_prev_active_inst, n_bm
):
"""Collect tensor parts associated to active instances."""
beamed_tensor_shape = beamed_tensor.shape
n_curr_active_inst = len(curr_active_inst_idx)
new_shape = (
n_curr_active_inst * n_bm,
beamed_tensor_shape[1],
beamed_tensor_shape[2],
)
beamed_tensor = beamed_tensor.reshape([n_prev_active_inst, -1])
beamed_tensor = beamed_tensor.index_select(curr_active_inst_idx, axis=0)
beamed_tensor = beamed_tensor.reshape(new_shape)
return beamed_tensor
def collate_active_info(
src_enc, inst_idx_to_position_map, active_inst_idx_list
):
# Sentences which are still active are collected,
# so the decoder will not run on completed sentences.
n_prev_active_inst = len(inst_idx_to_position_map)
active_inst_idx = [
inst_idx_to_position_map[k] for k in active_inst_idx_list
]
active_inst_idx = torch.tensor(active_inst_idx, dtype=torch.int64)
active_src_enc = collect_active_part(
src_enc.permute(1, 0, 2), active_inst_idx, n_prev_active_inst, n_bm
).permute(1, 0, 2)
active_inst_idx_to_position_map = get_inst_idx_to_tensor_position_map(
active_inst_idx_list
)
return active_src_enc, active_inst_idx_to_position_map
def beam_decode_step(
inst_dec_beams,
len_dec_seq,
enc_output,
inst_idx_to_position_map,
n_bm,
memory_key_padding_mask,
):
"""Decode and update beam status, and then return active beam idx"""
def prepare_beam_dec_seq(inst_dec_beams, len_dec_seq):
dec_partial_seq = [
b.get_current_state() for b in inst_dec_beams if not b.done
]
dec_partial_seq = torch.stack(dec_partial_seq)
dec_partial_seq = dec_partial_seq.reshape([-1, len_dec_seq])
return dec_partial_seq
def predict_word(
dec_seq, enc_output, n_active_inst, n_bm, memory_key_padding_mask
):
dec_seq = self.embedding(dec_seq).permute(1, 0, 2)
dec_seq = self.positional_encoding(dec_seq)
tgt_mask = self.generate_square_subsequent_mask(dec_seq.shape[0])
dec_output = self.decoder(
dec_seq,
enc_output,
tgt_mask=tgt_mask,
tgt_key_padding_mask=None,
memory_key_padding_mask=memory_key_padding_mask,
)
dec_output = dec_output.permute(1, 0, 2)
dec_output = dec_output[:, -1, :] # Pick the last step: (bh * bm) * d_h
word_prob = F.softmax(self.tgt_word_prj(dec_output), dim=1)
word_prob = torch.reshape(word_prob, (n_active_inst, n_bm, -1))
return word_prob
def collect_active_inst_idx_list(
inst_beams, word_prob, inst_idx_to_position_map
):
active_inst_idx_list = []
for inst_idx, inst_position in inst_idx_to_position_map.items():
is_inst_complete = inst_beams[inst_idx].advance(
word_prob[inst_position]
)
if not is_inst_complete:
active_inst_idx_list += [inst_idx]
return active_inst_idx_list
n_active_inst = len(inst_idx_to_position_map)
dec_seq = prepare_beam_dec_seq(inst_dec_beams, len_dec_seq)
word_prob = predict_word(dec_seq, enc_output, n_active_inst, n_bm, None)
# Update the beam with predicted word prob information and collect incomplete instances
active_inst_idx_list = collect_active_inst_idx_list(
inst_dec_beams, word_prob, inst_idx_to_position_map
)
return active_inst_idx_list
def collect_hypothesis_and_scores(inst_dec_beams, n_best):
all_hyp, all_scores = [], []
for inst_idx in range(len(inst_dec_beams)):
scores, tail_idxs = inst_dec_beams[inst_idx].sort_scores()
all_scores += [scores[:n_best]]
hyps = [
inst_dec_beams[inst_idx].get_hypothesis(i)
for i in tail_idxs[:n_best]
]
all_hyp += [hyps]
return all_hyp, all_scores
with torch.no_grad():
# -- Encode
if self.encoder is not None:
src = self.positional_encoding(images.permute(1, 0, 2))
src_enc = self.encoder(src)
else:
src_enc = images.squeeze(2).transpose([0, 2, 1])
n_bm = self.beam_size
src_shape = src_enc.shape
inst_dec_beams = [Beam(n_bm) for _ in range(1)]
active_inst_idx_list = list(range(1))
# Repeat data for beam search
# src_enc = paddle.tile(src_enc, [1, n_bm, 1])
src_enc = src_enc.repeat(1, n_bm, 1)
inst_idx_to_position_map = get_inst_idx_to_tensor_position_map(
active_inst_idx_list
)
# Decode
for len_dec_seq in range(1, 25):
src_enc_copy = src_enc.clone()
active_inst_idx_list = beam_decode_step(
inst_dec_beams,
len_dec_seq,
src_enc_copy,
inst_idx_to_position_map,
n_bm,
None,
)
if not active_inst_idx_list:
break # all instances have finished their path to <EOS>
src_enc, inst_idx_to_position_map = collate_active_info(
src_enc_copy, inst_idx_to_position_map, active_inst_idx_list
)
batch_hyp, batch_scores = collect_hypothesis_and_scores(inst_dec_beams, 1)
result_hyp = []
hyp_scores = []
for bs_hyp, score in zip(batch_hyp, batch_scores):
l = len(bs_hyp[0])
bs_hyp_pad = bs_hyp[0] + [3] * (25 - l)
result_hyp.append(bs_hyp_pad)
score = float(score) / l
hyp_score = [score for _ in range(25)]
hyp_scores.append(hyp_score)
return [
torch.tensor(np.array(result_hyp), dtype=torch.int64),
torch.tensor(hyp_scores),
]
def generate_square_subsequent_mask(self, sz):
"""Generate a square mask for the sequence. The masked positions are filled with float('-inf').
Unmasked positions are filled with float(0.0).
"""
mask = torch.zeros([sz, sz], dtype=torch.float32)
mask_inf = torch.triu(
torch.full(size=[sz, sz], fill_value=float("-Inf"), dtype=torch.float32),
diagonal=1,
)
mask = mask + mask_inf
return mask
def generate_padding_mask(self, x):
# padding_mask = paddle.equal(x, paddle.to_tensor(0, dtype=x.dtype))
padding_mask = x == torch.tensor(0, dtype=x.dtype)
return padding_mask
def _reset_parameters(self):
"""Initiate parameters in the transformer model."""
for p in self.parameters():
if p.dim() > 1:
xavier_uniform_(p)
class TransformerEncoder(nn.Module):
"""TransformerEncoder is a stack of N encoder layers
Args:
encoder_layer: an instance of the TransformerEncoderLayer() class (required).
num_layers: the number of sub-encoder-layers in the encoder (required).
norm: the layer normalization component (optional).
"""
def __init__(self, encoder_layer, num_layers):
super(TransformerEncoder, self).__init__()
self.layers = _get_clones(encoder_layer, num_layers)
self.num_layers = num_layers
def forward(self, src):
"""Pass the input through the endocder layers in turn.
Args:
src: the sequnce to the encoder (required).
mask: the mask for the src sequence (optional).
src_key_padding_mask: the mask for the src keys per batch (optional).
"""
output = src
for i in range(self.num_layers):
output = self.layers[i](output, src_mask=None, src_key_padding_mask=None)
return output
class TransformerDecoder(nn.Module):
"""TransformerDecoder is a stack of N decoder layers
Args:
decoder_layer: an instance of the TransformerDecoderLayer() class (required).
num_layers: the number of sub-decoder-layers in the decoder (required).
norm: the layer normalization component (optional).
"""
def __init__(self, decoder_layer, num_layers):
super(TransformerDecoder, self).__init__()
self.layers = _get_clones(decoder_layer, num_layers)
self.num_layers = num_layers
def forward(
self,
tgt,
memory,
tgt_mask=None,
memory_mask=None,
tgt_key_padding_mask=None,
memory_key_padding_mask=None,
):
"""Pass the inputs (and mask) through the decoder layer in turn.
Args:
tgt: the sequence to the decoder (required).
memory: the sequnce from the last layer of the encoder (required).
tgt_mask: the mask for the tgt sequence (optional).
memory_mask: the mask for the memory sequence (optional).
tgt_key_padding_mask: the mask for the tgt keys per batch (optional).
memory_key_padding_mask: the mask for the memory keys per batch (optional).
"""
output = tgt
for i in range(self.num_layers):
output = self.layers[i](
output,
memory,
tgt_mask=tgt_mask,
memory_mask=memory_mask,
tgt_key_padding_mask=tgt_key_padding_mask,
memory_key_padding_mask=memory_key_padding_mask,
)
return output
class TransformerEncoderLayer(nn.Module):
"""TransformerEncoderLayer is made up of self-attn and feedforward network.
This standard encoder layer is based on the paper "Attention Is All You Need".
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez,
Lukasz Kaiser, and Illia Polosukhin. 2017. Attention is all you need. In Advances in
Neural Information Processing Systems, pages 6000-6010. Users may modify or implement
in a different way during application.
Args:
d_model: the number of expected features in the input (required).
nhead: the number of heads in the multiheadattention models (required).
dim_feedforward: the dimension of the feedforward network model (default=2048).
dropout: the dropout value (default=0.1).
"""
def __init__(
self,
d_model,
nhead,
dim_feedforward=2048,
attention_dropout_rate=0.0,
residual_dropout_rate=0.1,
):
super(TransformerEncoderLayer, self).__init__()
self.self_attn = MultiheadAttention(
d_model, nhead, dropout=attention_dropout_rate
)
self.conv1 = nn.Conv2d(
in_channels=d_model, out_channels=dim_feedforward, kernel_size=(1, 1)
)
self.conv2 = nn.Conv2d(
in_channels=dim_feedforward, out_channels=d_model, kernel_size=(1, 1)
)
self.norm1 = LayerNorm(d_model)
self.norm2 = LayerNorm(d_model)
self.dropout1 = Dropout(residual_dropout_rate)
self.dropout2 = Dropout(residual_dropout_rate)
def forward(self, src, src_mask=None, src_key_padding_mask=None):
"""Pass the input through the endocder layer.
Args:
src: the sequnce to the encoder layer (required).
src_mask: the mask for the src sequence (optional).
src_key_padding_mask: the mask for the src keys per batch (optional).
"""
src2 = self.self_attn(
src, src, src, attn_mask=src_mask, key_padding_mask=src_key_padding_mask
)
src = src + self.dropout1(src2)
src = self.norm1(src)
src = src.permute(1, 2, 0)
src = torch.unsqueeze(src, 2)
src2 = self.conv2(F.relu(self.conv1(src)))
src2 = torch.squeeze(src2, 2)
src2 = src2.permute(2, 0, 1)
src = torch.squeeze(src, 2)
src = src.permute(2, 0, 1)
src = src + self.dropout2(src2)
src = self.norm2(src)
return src
class TransformerDecoderLayer(nn.Module):
"""TransformerDecoderLayer is made up of self-attn, multi-head-attn and feedforward network.
This standard decoder layer is based on the paper "Attention Is All You Need".
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez,
Lukasz Kaiser, and Illia Polosukhin. 2017. Attention is all you need. In Advances in
Neural Information Processing Systems, pages 6000-6010. Users may modify or implement
in a different way during application.
Args:
d_model: the number of expected features in the input (required).
nhead: the number of heads in the multiheadattention models (required).
dim_feedforward: the dimension of the feedforward network model (default=2048).
dropout: the dropout value (default=0.1).
"""
def __init__(
self,
d_model,
nhead,
dim_feedforward=2048,
attention_dropout_rate=0.0,
residual_dropout_rate=0.1,
):
super(TransformerDecoderLayer, self).__init__()
self.self_attn = MultiheadAttention(
d_model, nhead, dropout=attention_dropout_rate
)
self.multihead_attn = MultiheadAttention(
d_model, nhead, dropout=attention_dropout_rate
)
self.conv1 = nn.Conv2d(
in_channels=d_model, out_channels=dim_feedforward, kernel_size=(1, 1)
)
self.conv2 = nn.Conv2d(
in_channels=dim_feedforward, out_channels=d_model, kernel_size=(1, 1)
)
self.norm1 = LayerNorm(d_model)
self.norm2 = LayerNorm(d_model)
self.norm3 = LayerNorm(d_model)
self.dropout1 = Dropout(residual_dropout_rate)
self.dropout2 = Dropout(residual_dropout_rate)
self.dropout3 = Dropout(residual_dropout_rate)
def forward(
self,
tgt,
memory,
tgt_mask=None,
memory_mask=None,
tgt_key_padding_mask=None,
memory_key_padding_mask=None,
):
"""Pass the inputs (and mask) through the decoder layer.
Args:
tgt: the sequence to the decoder layer (required).
memory: the sequnce from the last layer of the encoder (required).
tgt_mask: the mask for the tgt sequence (optional).
memory_mask: the mask for the memory sequence (optional).
tgt_key_padding_mask: the mask for the tgt keys per batch (optional).
memory_key_padding_mask: the mask for the memory keys per batch (optional).
"""
tgt2 = self.self_attn(
tgt, tgt, tgt, attn_mask=tgt_mask, key_padding_mask=tgt_key_padding_mask
)
tgt = tgt + self.dropout1(tgt2)
tgt = self.norm1(tgt)
tgt2 = self.multihead_attn(
tgt,
memory,
memory,
attn_mask=memory_mask,
key_padding_mask=memory_key_padding_mask,
)
tgt = tgt + self.dropout2(tgt2)
tgt = self.norm2(tgt)
# default
tgt = tgt.permute(1, 2, 0)
tgt = torch.unsqueeze(tgt, 2)
tgt2 = self.conv2(F.relu(self.conv1(tgt)))
tgt2 = torch.squeeze(tgt2, 2)
tgt2 = tgt2.permute(2, 0, 1)
tgt = torch.squeeze(tgt, 2)
tgt = tgt.permute(2, 0, 1)
tgt = tgt + self.dropout3(tgt2)
tgt = self.norm3(tgt)
return tgt
def _get_clones(module, N):
return LayerList([copy.deepcopy(module) for i in range(N)])
class PositionalEncoding(nn.Module):
"""Inject some information about the relative or absolute position of the tokens
in the sequence. The positional encodings have the same dimension as
the embeddings, so that the two can be summed. Here, we use sine and cosine
functions of different frequencies.
.. math::
\text{PosEncoder}(pos, 2i) = sin(pos/10000^(2i/d_model))
\text{PosEncoder}(pos, 2i+1) = cos(pos/10000^(2i/d_model))
\text{where pos is the word position and i is the embed idx)
Args:
d_model: the embed dim (required).
dropout: the dropout value (default=0.1).
max_len: the max. length of the incoming sequence (default=5000).
Examples:
>>> pos_encoder = PositionalEncoding(d_model)
"""
def __init__(self, dropout, dim, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros([max_len, dim])
position = torch.arange(0, max_len, dtype=torch.float32).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, dim, 2).type(torch.float32) * (-math.log(10000.0) / dim)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = torch.unsqueeze(pe, 0)
pe = pe.permute(1, 0, 2)
self.register_buffer("pe", pe)
def forward(self, x):
"""Inputs of forward function
Args:
x: the sequence fed to the positional encoder model (required).
Shape:
x: [sequence length, batch size, embed dim]
output: [sequence length, batch size, embed dim]
Examples:
>>> output = pos_encoder(x)
"""
x = x + self.pe[: x.shape[0], :]
return self.dropout(x)
class PositionalEncoding_2d(nn.Module):
"""Inject some information about the relative or absolute position of the tokens
in the sequence. The positional encodings have the same dimension as
the embeddings, so that the two can be summed. Here, we use sine and cosine
functions of different frequencies.
.. math::
\text{PosEncoder}(pos, 2i) = sin(pos/10000^(2i/d_model))
\text{PosEncoder}(pos, 2i+1) = cos(pos/10000^(2i/d_model))
\text{where pos is the word position and i is the embed idx)
Args:
d_model: the embed dim (required).
dropout: the dropout value (default=0.1).
max_len: the max. length of the incoming sequence (default=5000).
Examples:
>>> pos_encoder = PositionalEncoding(d_model)
"""
def __init__(self, dropout, dim, max_len=5000):
super(PositionalEncoding_2d, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros([max_len, dim])
position = torch.arange(0, max_len, dtype=torch.float32).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, dim, 2).type(torch.float32) * (-math.log(10000.0) / dim)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = torch.unsqueeze(pe, 0).permute(1, 0, 2)
self.register_buffer("pe", pe)
self.avg_pool_1 = nn.AdaptiveAvgPool2d((1, 1))
self.linear1 = nn.Linear(dim, dim)
self.linear1.weight.data.fill_(1.0)
self.avg_pool_2 = nn.AdaptiveAvgPool2d((1, 1))
self.linear2 = nn.Linear(dim, dim)
self.linear2.weight.data.fill_(1.0)
def forward(self, x):
"""Inputs of forward function
Args:
x: the sequence fed to the positional encoder model (required).
Shape:
x: [sequence length, batch size, embed dim]
output: [sequence length, batch size, embed dim]
Examples:
>>> output = pos_encoder(x)
"""
w_pe = self.pe[: x.shape[-1], :]
w1 = self.linear1(self.avg_pool_1(x).squeeze()).unsqueeze(0)
w_pe = w_pe * w1
w_pe = w_pe.permute(1, 2, 0)
w_pe = torch.unsqueeze(w_pe, 2)
h_pe = self.pe[: x.shape[-2], :]
w2 = self.linear2(self.avg_pool_2(x).squeeze()).unsqueeze(0)
h_pe = h_pe * w2
h_pe = h_pe.permute(1, 2, 0)
h_pe = torch.unsqueeze(h_pe, 3)
x = x + w_pe + h_pe
x = torch.reshape(x, [x.shape[0], x.shape[1], x.shape[2] * x.shape[3]]).permute(
2, 0, 1
)
return self.dropout(x)
class Embeddings(nn.Module):
def __init__(self, d_model, vocab, padding_idx, scale_embedding):
super(Embeddings, self).__init__()
self.embedding = nn.Embedding(vocab, d_model, padding_idx=padding_idx)
w0 = np.random.normal(0.0, d_model**-0.5, (vocab, d_model)).astype(np.float32)
self.embedding.weight.data = torch.from_numpy(w0)
self.d_model = d_model
self.scale_embedding = scale_embedding
def forward(self, x):
if self.scale_embedding:
x = self.embedding(x)
return x * math.sqrt(self.d_model)
return self.embedding(x)
class Beam:
"""Beam search"""
def __init__(self, size, device=False):
self.size = size
self._done = False
# The score for each translation on the beam.
self.scores = torch.zeros((size,), dtype=torch.float32)
self.all_scores = []
# The backpointers at each time-step.
self.prev_ks = []
# The outputs at each time-step.
self.next_ys = [torch.full((size,), 0, dtype=torch.int64)]
self.next_ys[0][0] = 2
def get_current_state(self):
"Get the outputs for the current timestep."
return self.get_tentative_hypothesis()
def get_current_origin(self):
"Get the backpointers for the current timestep."
return self.prev_ks[-1]
@property
def done(self):
return self._done
def advance(self, word_prob):
"Update beam status and check if finished or not."
num_words = word_prob.shape[1]
# Sum the previous scores.
if len(self.prev_ks) > 0:
beam_lk = word_prob + self.scores.unsqueeze(1).expand_as(word_prob)
else:
beam_lk = word_prob[0]
flat_beam_lk = beam_lk.reshape([-1])
best_scores, best_scores_id = flat_beam_lk.topk(
self.size, 0, True, True
) # 1st sort
self.all_scores.append(self.scores)
self.scores = best_scores
# bestScoresId is flattened as a (beam x word) array,
# so we need to calculate which word and beam each score came from
prev_k = best_scores_id // num_words
self.prev_ks.append(prev_k)
self.next_ys.append(best_scores_id - prev_k * num_words)
# End condition is when top-of-beam is EOS.
if self.next_ys[-1][0] == 3:
self._done = True
self.all_scores.append(self.scores)
return self._done
def sort_scores(self):
"Sort the scores."
return self.scores, torch.tensor(
[i for i in range(int(self.scores.shape[0]))], dtype=torch.int32
)
def get_the_best_score_and_idx(self):
"Get the score of the best in the beam."
scores, ids = self.sort_scores()
return scores[1], ids[1]
def get_tentative_hypothesis(self):
"Get the decoded sequence for the current timestep."
if len(self.next_ys) == 1:
dec_seq = self.next_ys[0].unsqueeze(1)
else:
_, keys = self.sort_scores()
hyps = [self.get_hypothesis(k) for k in keys]
hyps = [[2] + h for h in hyps]
dec_seq = torch.tensor(hyps, dtype=torch.int64)
return dec_seq
def get_hypothesis(self, k):
"""Walk back to construct the full hypothesis."""
hyp = []
for j in range(len(self.prev_ks) - 1, -1, -1):
hyp.append(self.next_ys[j + 1][k])
k = self.prev_ks[j][k]
return list(map(lambda x: x.item(), hyp[::-1]))
@@ -0,0 +1,31 @@
# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__all__ = ["build_transform"]
def build_transform(config):
from .tps import TPS
from .stn import STN_ON
from .tsrn import TSRN
from .tbsrn import TBSRN
support_dict = ["TPS", "STN_ON", "TSRN", "TBSRN"]
module_name = config.pop("name")
assert module_name in support_dict, Exception(
"transform only support {}".format(support_dict)
)
module_class = eval(module_name)(**config)
return module_class
@@ -0,0 +1,121 @@
"""
This code is refer from:
https://github.com/ayumiymk/aster.pytorch/blob/master/lib/models/stn_head.py
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import math
import torch
from torch import nn
from torch.nn import functional as F
import numpy as np
from .tps_spatial_transformer import TPSSpatialTransformer
def conv3x3_block(in_channels, out_channels, stride=1):
n = 3 * 3 * out_channels
w = math.sqrt(2.0 / n)
conv_layer = nn.Conv2d(
in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=True
)
block = nn.Sequential(conv_layer, nn.BatchNorm2d(out_channels), nn.ReLU())
return block
class STN(nn.Module):
def __init__(self, in_channels, num_ctrlpoints, activation="none"):
super(STN, self).__init__()
self.in_channels = in_channels
self.num_ctrlpoints = num_ctrlpoints
self.activation = activation
self.stn_convnet = nn.Sequential(
conv3x3_block(in_channels, 32), # 32x64
nn.MaxPool2d(kernel_size=2, stride=2),
conv3x3_block(32, 64), # 16x32
nn.MaxPool2d(kernel_size=2, stride=2),
conv3x3_block(64, 128), # 8*16
nn.MaxPool2d(kernel_size=2, stride=2),
conv3x3_block(128, 256), # 4*8
nn.MaxPool2d(kernel_size=2, stride=2),
conv3x3_block(256, 256), # 2*4,
nn.MaxPool2d(kernel_size=2, stride=2),
conv3x3_block(256, 256),
) # 1*2
self.stn_fc1 = nn.Sequential(
nn.Linear(2 * 256, 512, bias=True),
nn.BatchNorm1d(512),
nn.ReLU(inplace=True),
)
fc2_bias = self.init_stn()
self.stn_fc2 = nn.Linear(512, num_ctrlpoints * 2, bias=True)
def init_stn(self):
margin = 0.01
sampling_num_per_side = int(self.num_ctrlpoints / 2)
ctrl_pts_x = np.linspace(margin, 1.0 - margin, sampling_num_per_side)
ctrl_pts_y_top = np.ones(sampling_num_per_side) * margin
ctrl_pts_y_bottom = np.ones(sampling_num_per_side) * (1 - margin)
ctrl_pts_top = np.stack([ctrl_pts_x, ctrl_pts_y_top], axis=1)
ctrl_pts_bottom = np.stack([ctrl_pts_x, ctrl_pts_y_bottom], axis=1)
ctrl_points = np.concatenate([ctrl_pts_top, ctrl_pts_bottom], axis=0).astype(
np.float32
)
if self.activation == "none":
pass
elif self.activation == "sigmoid":
ctrl_points = -np.log(1.0 / ctrl_points - 1.0)
ctrl_points = torch.Tensor(ctrl_points)
# fc2_bias = ctrl_points.view(-1)
fc2_bias = torch.reshape(
ctrl_points, shape=[ctrl_points.shape[0] * ctrl_points.shape[1]]
)
return fc2_bias
def forward(self, x):
x = self.stn_convnet(x)
batch_size, _, h, w = x.shape
# x = x.view(batch_size, -1)
x = torch.reshape(x, shape=(batch_size, -1))
img_feat = self.stn_fc1(x)
x = self.stn_fc2(0.1 * img_feat)
if self.activation == "sigmoid":
x = F.sigmoid(x)
# x = x.view(-1, self.num_ctrlpoints, 2)
x = torch.reshape(x, shape=[-1, self.num_ctrlpoints, 2])
return img_feat, x
class STN_ON(nn.Module):
def __init__(
self,
in_channels,
tps_inputsize,
tps_outputsize,
num_control_points,
tps_margins,
stn_activation,
):
super(STN_ON, self).__init__()
self.tps = TPSSpatialTransformer(
output_image_size=tuple(tps_outputsize),
num_control_points=num_control_points,
margins=tuple(tps_margins),
)
self.stn_head = STN(
in_channels=in_channels,
num_ctrlpoints=num_control_points,
activation=stn_activation,
)
self.tps_inputsize = tps_inputsize
self.out_channels = in_channels
def forward(self, image):
stn_input = torch.nn.functional.interpolate(
image, self.tps_inputsize, mode="bilinear", align_corners=True
)
stn_img_feat, ctrl_points = self.stn_head(stn_input)
x, _ = self.tps(image, ctrl_points)
return x
@@ -0,0 +1,305 @@
# copyright (c) 2022 PaddlePaddle Authors. All Rights Reserve.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This code is refer from:
https://github.com/FudanVI/FudanOCR/blob/main/scene-text-telescope/model/tbsrn.py
"""
import math
import warnings
import numpy as np
import torch
from torch import nn
import string
warnings.filterwarnings("ignore")
from .tps_spatial_transformer import TPSSpatialTransformer
from .stn import STN as STNHead
from .tsrn import GruBlock, mish, UpsampleBLock
from pytorchocr.modeling.heads.sr_rensnet_transformer import (
Transformer,
LayerNorm,
PositionwiseFeedForward,
MultiHeadedAttention,
)
def positionalencoding2d(d_model, height, width):
"""
:param d_model: dimension of the model
:param height: height of the positions
:param width: width of the positions
:return: d_model*height*width position matrix
"""
if d_model % 4 != 0:
raise ValueError(
"Cannot use sin/cos positional encoding with "
"odd dimension (got dim={:d})".format(d_model)
)
pe = torch.zeros([d_model, height, width])
# Each dimension use half of d_model
d_model = int(d_model / 2)
div_term = torch.exp(torch.arange(0.0, d_model, 2) * -(math.log(10000.0) / d_model))
pos_w = torch.arange(0.0, width, dtype=torch.float32).unsqueeze(1)
pos_h = torch.arange(0.0, height, dtype=torch.float32).unsqueeze(1)
pe[0:d_model:2, :, :] = (
torch.sin(pos_w * div_term).transpose(0, 1).unsqueeze(1).repeat(1, height, 1)
)
pe[1:d_model:2, :, :] = (
torch.cos(pos_w * div_term).transpose(0, 1).unsqueeze(1).repeat(1, height, 1)
)
pe[d_model::2, :, :] = (
torch.sin(pos_h * div_term).transpose(0, 1).unsqueeze(2).repeat(1, 1, width)
)
pe[d_model + 1 :: 2, :, :] = (
torch.cos(pos_h * div_term).transpose(0, 1).unsqueeze(2).repeat(1, 1, width)
)
return pe
class FeatureEnhancer(nn.Module):
def __init__(self):
super(FeatureEnhancer, self).__init__()
self.multihead = MultiHeadedAttention(h=4, d_model=128, dropout=0.1)
self.mul_layernorm1 = LayerNorm(features=128)
self.pff = PositionwiseFeedForward(128, 128)
self.mul_layernorm3 = LayerNorm(features=128)
self.linear = nn.Linear(128, 64)
def forward(self, conv_feature):
"""
text : (batch, seq_len, embedding_size)
global_info: (batch, embedding_size, 1, 1)
conv_feature: (batch, channel, H, W)
"""
batch = conv_feature.shape[0]
if torch.cuda.is_available():
position2d = (
positionalencoding2d(64, 16, 64)
.float()
.cuda()
.unsqueeze(0)
.reshape([1, 64, 1024])
)
else:
position2d = (
positionalencoding2d(64, 16, 64)
.float()
.unsqueeze(0)
.reshape([1, 64, 1024])
)
position2d = position2d.repeat(batch, 1, 1)
conv_feature = torch.cat(
[conv_feature, position2d], 1
) # batch, 128(64+64), 32, 128
result = conv_feature.permute(0, 2, 1).contiguous()
origin_result = result
result = self.mul_layernorm1(
origin_result + self.multihead(result, result, result, mask=None)[0]
)
origin_result = result
result = self.mul_layernorm3(origin_result + self.pff(result))
result = self.linear(result)
return result.permute(0, 2, 1).contiguous()
def str_filt(str_, voc_type):
alpha_dict = {
"digit": string.digits,
"lower": string.digits + string.ascii_lowercase,
"upper": string.digits + string.ascii_letters,
"all": string.digits + string.ascii_letters + string.punctuation,
}
if voc_type == "lower":
str_ = str_.lower()
for char in str_:
if char not in alpha_dict[voc_type]:
str_ = str_.replace(char, "")
str_ = str_.lower()
return str_
class TBSRN(nn.Module):
def __init__(
self,
in_channels=3,
scale_factor=2,
width=128,
height=32,
STN=True,
srb_nums=5,
mask=False,
hidden_units=32,
infer_mode=False,
):
super(TBSRN, self).__init__()
in_planes = 3
if mask:
in_planes = 4
assert math.log(scale_factor, 2) % 1 == 0
upsample_block_num = int(math.log(scale_factor, 2))
self.block1 = nn.Sequential(
nn.Conv2d(in_planes, 2 * hidden_units, kernel_size=9, padding=4),
nn.PReLU()
# nn.ReLU()
)
self.srb_nums = srb_nums
for i in range(srb_nums):
setattr(self, "block%d" % (i + 2), RecurrentResidualBlock(2 * hidden_units))
setattr(
self,
"block%d" % (srb_nums + 2),
nn.Sequential(
nn.Conv2d(2 * hidden_units, 2 * hidden_units, kernel_size=3, padding=1),
nn.BatchNorm2d(2 * hidden_units),
),
)
# self.non_local = NonLocalBlock2D(64, 64)
block_ = [UpsampleBLock(2 * hidden_units, 2) for _ in range(upsample_block_num)]
block_.append(nn.Conv2d(2 * hidden_units, in_planes, kernel_size=9, padding=4))
setattr(self, "block%d" % (srb_nums + 3), nn.Sequential(*block_))
self.tps_inputsize = [height // scale_factor, width // scale_factor]
tps_outputsize = [height // scale_factor, width // scale_factor]
num_control_points = 20
tps_margins = [0.05, 0.05]
self.stn = STN
self.out_channels = in_channels
if self.stn:
self.tps = TPSSpatialTransformer(
output_image_size=tuple(tps_outputsize),
num_control_points=num_control_points,
margins=tuple(tps_margins),
)
self.stn_head = STNHead(
in_channels=in_planes,
num_ctrlpoints=num_control_points,
activation="none",
)
self.infer_mode = infer_mode
self.english_alphabet = (
"-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
self.english_dict = {}
for index in range(len(self.english_alphabet)):
self.english_dict[self.english_alphabet[index]] = index
transformer = Transformer(alphabet="-0123456789abcdefghijklmnopqrstuvwxyz")
self.transformer = transformer
for param in self.transformer.parameters():
param.trainable = False
def label_encoder(self, label):
batch = len(label)
length = [len(i) for i in label]
length_tensor = torch.Tensor(length).type(torch.int64)
max_length = max(length)
input_tensor = np.zeros((batch, max_length))
for i in range(batch):
for j in range(length[i] - 1):
input_tensor[i][j + 1] = self.english_dict[label[i][j]]
text_gt = []
for i in label:
for j in i:
text_gt.append(self.english_dict[j])
text_gt = torch.Tensor(text_gt).type(torch.int64)
input_tensor = torch.Tensor(input_tensor).type(torch.int64)
return length_tensor, input_tensor, text_gt
def forward(self, x):
output = {}
if self.infer_mode:
output["lr_img"] = x
y = x
else:
output["lr_img"] = x[0]
output["hr_img"] = x[1]
y = x[0]
if self.stn and self.training:
_, ctrl_points_x = self.stn_head(y)
y, _ = self.tps(y, ctrl_points_x)
block = {"1": self.block1(y)}
for i in range(self.srb_nums + 1):
block[str(i + 2)] = getattr(self, "block%d" % (i + 2))(block[str(i + 1)])
block[str(self.srb_nums + 3)] = getattr(self, "block%d" % (self.srb_nums + 3))(
(block["1"] + block[str(self.srb_nums + 2)])
)
sr_img = torch.tanh(block[str(self.srb_nums + 3)])
output["sr_img"] = sr_img
if self.training:
hr_img = x[1]
# add transformer
label = [str_filt(i, "lower") + "-" for i in x[2]]
length_tensor, input_tensor, text_gt = self.label_encoder(label)
hr_pred, word_attention_map_gt, hr_correct_list = self.transformer(
hr_img, length_tensor, input_tensor
)
sr_pred, word_attention_map_pred, sr_correct_list = self.transformer(
sr_img, length_tensor, input_tensor
)
output["hr_img"] = hr_img
output["hr_pred"] = hr_pred
output["text_gt"] = text_gt
output["word_attention_map_gt"] = word_attention_map_gt
output["sr_pred"] = sr_pred
output["word_attention_map_pred"] = word_attention_map_pred
return output
class RecurrentResidualBlock(nn.Module):
def __init__(self, channels):
super(RecurrentResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(channels)
self.gru1 = GruBlock(channels, channels)
# self.prelu = nn.ReLU()
self.prelu = mish()
self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(channels)
self.gru2 = GruBlock(channels, channels)
self.feature_enhancer = FeatureEnhancer()
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
def forward(self, x):
residual = self.conv1(x)
residual = self.bn1(residual)
residual = self.prelu(residual)
residual = self.conv2(residual)
residual = self.bn2(residual)
size = residual.shape
residual = residual.reshape([size[0], size[1], -1])
residual = self.feature_enhancer(residual)
residual = residual.reshape([size[0], size[1], size[2], size[3]])
return x + residual
@@ -0,0 +1,292 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from pytorchocr.modeling.common import Activation
# import paddle
# from paddle import nn, ParamAttr
# from paddle.nn import functional as F
import numpy as np
class ConvBNLayer(nn.Module):
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
groups=1,
act=None,
name=None,
):
super(ConvBNLayer, self).__init__()
self.conv = nn.Conv2d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=(kernel_size - 1) // 2,
groups=groups,
bias=False,
)
bn_name = "bn_" + name
self.bn = nn.BatchNorm2d(
out_channels,
)
self.act = act
if act is not None:
self._act = Activation(act)
def forward(self, x):
x = self.conv(x)
x = self.bn(x)
if self.act is not None:
x = self._act(x)
return x
class LocalizationNetwork(nn.Module):
def __init__(self, in_channels, num_fiducial, loc_lr, model_name):
super(LocalizationNetwork, self).__init__()
self.F = num_fiducial
F = num_fiducial
if model_name == "large":
num_filters_list = [64, 128, 256, 512]
fc_dim = 256
else:
num_filters_list = [16, 32, 64, 128]
fc_dim = 64
# self.block_list = []
self.block_list = nn.Sequential()
for fno in range(0, len(num_filters_list)):
num_filters = num_filters_list[fno]
name = "loc_conv%d" % fno
# conv = self.add_sublayer(
# name,
# ConvBNLayer(
# in_channels=in_channels,
# out_channels=num_filters,
# kernel_size=3,
# act='relu',
# name=name))
conv = ConvBNLayer(
in_channels=in_channels,
out_channels=num_filters,
kernel_size=3,
act="relu",
name=name,
)
# self.block_list.append(conv)
self.block_list.add_module(name, conv)
if fno == len(num_filters_list) - 1:
pool = nn.AdaptiveAvgPool2d(1)
else:
# pool = nn.MaxPool2D(kernel_size=2, stride=2, padding=0)
pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
in_channels = num_filters
# self.block_list.append(pool)
self.block_list.add_module("{}_pool".format(name), pool)
name = "loc_fc1"
stdv = 1.0 / math.sqrt(num_filters_list[-1] * 1.0)
self.fc1 = nn.Linear(
in_channels,
fc_dim,
bias=True,
)
# Init fc2 in LocalizationNetwork
initial_bias = self.get_initial_fiducials()
initial_bias = initial_bias.reshape(-1)
name = "loc_fc2"
self.fc2 = nn.Linear(fc_dim, F * 2, bias=True)
self.out_channels = F * 2
def forward(self, x):
"""
Estimating parameters of geometric transformation
Args:
image: input
Return:
batch_C_prime: the matrix of the geometric transformation
"""
B = x.shape[0]
i = 0
for block in self.block_list:
x = block(x)
x = x.squeeze(dim=2).squeeze(dim=2)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
x = x.reshape(shape=[-1, self.F, 2])
return x
def get_initial_fiducials(self):
"""see RARE paper Fig. 6 (a)"""
F = self.F
ctrl_pts_x = np.linspace(-1.0, 1.0, int(F / 2))
ctrl_pts_y_top = np.linspace(0.0, -1.0, num=int(F / 2))
ctrl_pts_y_bottom = np.linspace(1.0, 0.0, num=int(F / 2))
ctrl_pts_top = np.stack([ctrl_pts_x, ctrl_pts_y_top], axis=1)
ctrl_pts_bottom = np.stack([ctrl_pts_x, ctrl_pts_y_bottom], axis=1)
initial_bias = np.concatenate([ctrl_pts_top, ctrl_pts_bottom], axis=0)
return initial_bias
class GridGenerator(nn.Module):
def __init__(self, in_channels, num_fiducial):
super(GridGenerator, self).__init__()
self.eps = 1e-6
self.F = num_fiducial
name = "ex_fc"
self.fc = nn.Linear(in_channels, 6, bias=True)
def forward(self, batch_C_prime, I_r_size):
"""
Generate the grid for the grid_sampler.
Args:
batch_C_prime: the matrix of the geometric transformation
I_r_size: the shape of the input image
Return:
batch_P_prime: the grid for the grid_sampler
"""
C = self.build_C_paddle()
P = self.build_P_paddle(I_r_size)
inv_delta_C_tensor = self.build_inv_delta_C_paddle(C).type(torch.float32)
P_hat_tensor = self.build_P_hat_paddle(C, torch.as_tensor(P)).type(
torch.float32
)
inv_delta_C_tensor.stop_gradient = True
P_hat_tensor.stop_gradient = True
batch_C_ex_part_tensor = self.get_expand_tensor(batch_C_prime)
batch_C_ex_part_tensor.stop_gradient = True
batch_C_prime_with_zeros = torch.cat(
[batch_C_prime, batch_C_ex_part_tensor], dim=1
)
inv_delta_C_tensor = inv_delta_C_tensor.to(batch_C_prime_with_zeros.device)
batch_T = torch.matmul(inv_delta_C_tensor, batch_C_prime_with_zeros)
P_hat_tensor = P_hat_tensor.to(batch_T.device)
batch_P_prime = torch.matmul(P_hat_tensor, batch_T)
return batch_P_prime
def build_C_paddle(self):
"""Return coordinates of fiducial points in I_r; C"""
F = self.F
ctrl_pts_x = torch.linspace(-1.0, 1.0, int(F / 2), dtype=torch.float64)
ctrl_pts_y_top = -1 * torch.ones([int(F / 2)], dtype=torch.float64)
ctrl_pts_y_bottom = torch.ones([int(F / 2)], dtype=torch.float64)
ctrl_pts_top = torch.stack([ctrl_pts_x, ctrl_pts_y_top], dim=1)
ctrl_pts_bottom = torch.stack([ctrl_pts_x, ctrl_pts_y_bottom], dim=1)
C = torch.cat([ctrl_pts_top, ctrl_pts_bottom], dim=0)
return C # F x 2
def build_P_paddle(self, I_r_size):
I_r_height, I_r_width = I_r_size
I_r_grid_x = (
torch.arange(-I_r_width, I_r_width, 2, dtype=torch.float64) + 1.0
) / torch.as_tensor(np.array([I_r_width]).astype(np.float64))
I_r_grid_y = (
torch.arange(-I_r_height, I_r_height, 2, dtype=torch.float64) + 1.0
) / torch.as_tensor(np.array([I_r_height]).astype(np.float64))
# P: self.I_r_width x self.I_r_height x 2
P = torch.stack(torch.meshgrid([I_r_grid_x, I_r_grid_y]), dim=2)
# P = paddle.transpose(P, perm=[1, 0, 2])
P = P.permute(1, 0, 2)
# n (= self.I_r_width x self.I_r_height) x 2
return P.reshape([-1, 2])
def build_inv_delta_C_paddle(self, C):
"""Return inv_delta_C which is needed to calculate T"""
F = self.F
hat_C = torch.zeros((F, F), dtype=torch.float64) # F x F
for i in range(0, F):
for j in range(i, F):
if i == j:
hat_C[i, j] = 1
else:
r = torch.norm(C[i] - C[j])
hat_C[i, j] = r
hat_C[j, i] = r
hat_C = (hat_C**2) * torch.log(hat_C)
delta_C = torch.cat( # F+3 x F+3
[
torch.cat(
[torch.ones((F, 1), dtype=torch.float64), C, hat_C], dim=1
), # F x F+3
torch.cat(
[torch.zeros((2, 3), dtype=torch.float64), C.permute(1, 0)], dim=1
), # 2 x F+3
torch.cat(
[
torch.zeros((1, 3), dtype=torch.float64),
torch.ones((1, F), dtype=torch.float64),
],
dim=1,
), # 1 x F+3
],
dim=0,
)
inv_delta_C = torch.inverse(delta_C)
return inv_delta_C # F+3 x F+3
def build_P_hat_paddle(self, C, P):
F = self.F
eps = self.eps
n = P.shape[0] # n (= self.I_r_width x self.I_r_height)
# P_tile: n x 2 -> n x 1 x 2 -> n x F x 2
# P_tile = paddle.tile(paddle.unsqueeze(P, axis=1), (1, F, 1))
P_tile = torch.unsqueeze(P, dim=1).repeat(1, F, 1)
C_tile = torch.unsqueeze(C, dim=0) # 1 x F x 2
P_diff = P_tile - C_tile # n x F x 2
# rbf_norm: n x F
rbf_norm = torch.norm(P_diff, p=2, dim=2, keepdim=False)
# rbf: n x F
# rbf = torch.mul(
# torch.square(rbf_norm), torch.log(rbf_norm + eps))
rbf = torch.mul(rbf_norm**2, torch.log(rbf_norm + eps))
P_hat = torch.cat([torch.ones((n, 1), dtype=torch.float64), P, rbf], dim=1)
return P_hat # n x F+3
def get_expand_tensor(self, batch_C_prime):
B, H, C = batch_C_prime.shape
batch_C_prime = batch_C_prime.reshape([B, H * C])
batch_C_ex_part_tensor = self.fc(batch_C_prime)
batch_C_ex_part_tensor = batch_C_ex_part_tensor.reshape([-1, 3, 2])
return batch_C_ex_part_tensor
class TPS(nn.Module):
def __init__(self, in_channels, num_fiducial, loc_lr, model_name):
super(TPS, self).__init__()
self.loc_net = LocalizationNetwork(
in_channels, num_fiducial, loc_lr, model_name
)
self.grid_generator = GridGenerator(self.loc_net.out_channels, num_fiducial)
self.out_channels = in_channels
def forward(self, image):
image.stop_gradient = False
batch_C_prime = self.loc_net(image)
batch_P_prime = self.grid_generator(batch_C_prime, image.shape[2:])
batch_P_prime = batch_P_prime.reshape([-1, image.shape[2], image.shape[3], 2])
if torch.__version__ < "1.3.0":
batch_I_r = F.grid_sample(image, grid=batch_P_prime)
else:
batch_I_r = F.grid_sample(image, grid=batch_P_prime, align_corners=True)
return batch_I_r
@@ -0,0 +1,138 @@
"""
This code is refer from:
https://github.com/ayumiymk/aster.pytorch/blob/master/lib/models/tps_spatial_transformer.py
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import torch
from torch import nn
from torch.nn import functional as F
import numpy as np
import itertools
def grid_sample(input, grid, canvas=None):
input.stop_gradient = False
output = (
F.grid_sample(input, grid, align_corners=True)
if torch.__version__ >= "1.3.0"
else F.grid_sample(input, grid)
)
if canvas is None:
return output
else:
# input_mask = paddle.ones(shape=input.shape)
input_mask = input.data.new(input.size()).fill_(1)
output_mask = F.grid_sample(input_mask, grid)
padded_output = output * output_mask + canvas * (1 - output_mask)
return padded_output
# phi(x1, x2) = r^2 * log(r), where r = ||x1 - x2||_2
def compute_partial_repr(input_points, control_points):
N = input_points.shape[0]
M = control_points.shape[0]
# pairwise_diff = input_points.view(N, 1, 2) - control_points.view(1, M, 2)
pairwise_diff = torch.reshape(input_points, shape=[N, 1, 2]) - torch.reshape(
control_points, shape=[1, M, 2]
)
# original implementation, very slow
# pairwise_dist = torch.sum(pairwise_diff ** 2, dim = 2) # square of distance
pairwise_diff_square = pairwise_diff * pairwise_diff
pairwise_dist = pairwise_diff_square[:, :, 0] + pairwise_diff_square[:, :, 1]
repr_matrix = 0.5 * pairwise_dist * torch.log(pairwise_dist)
# fix numerical error for 0 * log(0), substitute all nan with 0
# mask = np.array(repr_matrix != repr_matrix)
# repr_matrix[mask] = 0
mask = repr_matrix != repr_matrix
repr_matrix.masked_fill_(mask, 0)
return repr_matrix
# output_ctrl_pts are specified, according to our task.
def build_output_control_points(num_control_points, margins):
margin_x, margin_y = margins
num_ctrl_pts_per_side = num_control_points // 2
ctrl_pts_x = np.linspace(margin_x, 1.0 - margin_x, num_ctrl_pts_per_side)
ctrl_pts_y_top = np.ones(num_ctrl_pts_per_side) * margin_y
ctrl_pts_y_bottom = np.ones(num_ctrl_pts_per_side) * (1.0 - margin_y)
ctrl_pts_top = np.stack([ctrl_pts_x, ctrl_pts_y_top], axis=1)
ctrl_pts_bottom = np.stack([ctrl_pts_x, ctrl_pts_y_bottom], axis=1)
output_ctrl_pts_arr = np.concatenate([ctrl_pts_top, ctrl_pts_bottom], axis=0)
output_ctrl_pts = torch.Tensor(output_ctrl_pts_arr)
return output_ctrl_pts
class TPSSpatialTransformer(nn.Module):
def __init__(self, output_image_size=None, num_control_points=None, margins=None):
super(TPSSpatialTransformer, self).__init__()
self.output_image_size = output_image_size
self.num_control_points = num_control_points
self.margins = margins
self.target_height, self.target_width = output_image_size
target_control_points = build_output_control_points(num_control_points, margins)
N = num_control_points
# create padded kernel matrix
forward_kernel = torch.zeros(N + 3, N + 3)
target_control_partial_repr = compute_partial_repr(
target_control_points, target_control_points
)
forward_kernel[:N, :N].copy_(target_control_partial_repr)
forward_kernel[:N, -3].fill_(1)
forward_kernel[-3, :N].fill_(1)
forward_kernel[:N, -2:].copy_(target_control_points)
forward_kernel[-2:, :N].copy_(target_control_points.transpose(0, 1))
# compute inverse matrix
inverse_kernel = torch.inverse(forward_kernel)
# create target cordinate matrix
HW = self.target_height * self.target_width
target_coordinate = list(
itertools.product(range(self.target_height), range(self.target_width))
)
target_coordinate = torch.Tensor(target_coordinate) # HW x 2
Y, X = target_coordinate.split(1, dim=1)
Y = Y / (self.target_height - 1)
X = X / (self.target_width - 1)
target_coordinate = torch.cat([X, Y], dim=1) # convert from (y, x) to (x, y)
target_coordinate_partial_repr = compute_partial_repr(
target_coordinate, target_control_points
)
target_coordinate_repr = torch.cat(
[target_coordinate_partial_repr, torch.ones(HW, 1), target_coordinate],
dim=1,
)
# register precomputed matrices
self.inverse_kernel = inverse_kernel
self.padding_matrix = torch.zeros(3, 2)
self.target_coordinate_repr = target_coordinate_repr
self.target_control_points = target_control_points
def forward(self, input, source_control_points):
assert source_control_points.ndimension() == 3
assert source_control_points.shape[1] == self.num_control_points
assert source_control_points.shape[2] == 2
batch_size = source_control_points.size(0)
Y = torch.cat(
[source_control_points, self.padding_matrix.expand(batch_size, 3, 2)], 1
)
mapping_matrix = torch.matmul(self.inverse_kernel, Y)
source_coordinate = torch.matmul(self.target_coordinate_repr, mapping_matrix)
# grid = source_coordinate.view(-1, self.target_height, self.target_width, 2)
grid = torch.reshape(
source_coordinate, shape=[-1, self.target_height, self.target_width, 2]
)
grid = torch.clamp(
grid, 0, 1
) # the source_control_points may be out of [0, 1].
# the input to grid_sample is normalized [-1, 1], but what we get is [0, 1]
grid = 2.0 * grid - 1.0
output_maps = grid_sample(input, grid, canvas=None)
return output_maps, source_coordinate
@@ -0,0 +1,220 @@
# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This code is refer from:
https://github.com/FudanVI/FudanOCR/blob/main/text-gestalt/model/tsrn.py
"""
import math
import torch
import torch.nn.functional as F
from torch import nn
import warnings
import math
warnings.filterwarnings("ignore")
from .tps_spatial_transformer import TPSSpatialTransformer
from .stn import STN as STN_model
from pytorchocr.modeling.heads.sr_rensnet_transformer import Transformer
class TSRN(nn.Module):
def __init__(
self,
in_channels,
scale_factor=2,
width=128,
height=32,
STN=False,
srb_nums=5,
mask=False,
hidden_units=32,
infer_mode=False,
**kwargs,
):
super(TSRN, self).__init__()
in_planes = 3
if mask:
in_planes = 4
assert math.log(scale_factor, 2) % 1 == 0
upsample_block_num = int(math.log(scale_factor, 2))
self.block1 = nn.Sequential(
nn.Conv2d(in_planes, 2 * hidden_units, kernel_size=9, padding=4), nn.PReLU()
)
self.srb_nums = srb_nums
for i in range(srb_nums):
setattr(self, "block%d" % (i + 2), RecurrentResidualBlock(2 * hidden_units))
setattr(
self,
"block%d" % (srb_nums + 2),
nn.Sequential(
nn.Conv2d(2 * hidden_units, 2 * hidden_units, kernel_size=3, padding=1),
nn.BatchNorm2d(2 * hidden_units),
),
)
block_ = [UpsampleBLock(2 * hidden_units, 2) for _ in range(upsample_block_num)]
block_.append(nn.Conv2d(2 * hidden_units, in_planes, kernel_size=9, padding=4))
setattr(self, "block%d" % (srb_nums + 3), nn.Sequential(*block_))
self.tps_inputsize = [height // scale_factor, width // scale_factor]
tps_outputsize = [height // scale_factor, width // scale_factor]
num_control_points = 20
tps_margins = [0.05, 0.05]
self.stn = STN
if self.stn:
self.tps = TPSSpatialTransformer(
output_image_size=tuple(tps_outputsize),
num_control_points=num_control_points,
margins=tuple(tps_margins),
)
self.stn_head = STN_model(
in_channels=in_planes,
num_ctrlpoints=num_control_points,
activation="none",
)
self.out_channels = in_channels
self.r34_transformer = Transformer()
for param in self.r34_transformer.parameters():
param.trainable = False
self.infer_mode = infer_mode
def forward(self, x):
output = {}
if self.infer_mode:
output["lr_img"] = x
y = x
else:
output["lr_img"] = x[0]
output["hr_img"] = x[1]
y = x[0]
if self.stn and self.training:
_, ctrl_points_x = self.stn_head(y)
y, _ = self.tps(y, ctrl_points_x)
block = {"1": self.block1(y)}
for i in range(self.srb_nums + 1):
block[str(i + 2)] = getattr(self, "block%d" % (i + 2))(block[str(i + 1)])
block[str(self.srb_nums + 3)] = getattr(self, "block%d" % (self.srb_nums + 3))(
(block["1"] + block[str(self.srb_nums + 2)])
)
sr_img = torch.tanh(block[str(self.srb_nums + 3)])
output["sr_img"] = sr_img
if self.training:
hr_img = x[1]
length = x[2]
input_tensor = x[3]
# add transformer
sr_pred, word_attention_map_pred, _ = self.r34_transformer(
sr_img, length, input_tensor
)
hr_pred, word_attention_map_gt, _ = self.r34_transformer(
hr_img, length, input_tensor
)
output["hr_img"] = hr_img
output["hr_pred"] = hr_pred
output["word_attention_map_gt"] = word_attention_map_gt
output["sr_pred"] = sr_pred
output["word_attention_map_pred"] = word_attention_map_pred
return output
class RecurrentResidualBlock(nn.Module):
def __init__(self, channels):
super(RecurrentResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(channels)
self.gru1 = GruBlock(channels, channels)
self.prelu = mish()
self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(channels)
self.gru2 = GruBlock(channels, channels)
def forward(self, x):
residual = self.conv1(x)
residual = self.bn1(residual)
residual = self.prelu(residual)
residual = self.conv2(residual)
residual = self.bn2(residual)
residual = (
self.gru1(residual.permute(0, 1, 3, 2).contiguous())
.permute(0, 1, 3, 2)
.contiguous()
)
return self.gru2(x + residual).contiguous()
class UpsampleBLock(nn.Module):
def __init__(self, in_channels, up_scale):
super(UpsampleBLock, self).__init__()
self.conv = nn.Conv2d(
in_channels, in_channels * up_scale**2, kernel_size=3, padding=1
)
self.pixel_shuffle = nn.PixelShuffle(up_scale)
self.prelu = mish()
def forward(self, x):
x = self.conv(x)
x = self.pixel_shuffle(x)
x = self.prelu(x)
return x
class mish(nn.Module):
def __init__(
self,
):
super(mish, self).__init__()
self.activated = True
def forward(self, x):
if self.activated:
x = x * (torch.tanh(F.softplus(x)))
return x
class GruBlock(nn.Module):
def __init__(self, in_channels, out_channels):
super(GruBlock, self).__init__()
assert out_channels % 2 == 0
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, padding=0)
self.gru = nn.GRU(
out_channels,
out_channels // 2,
bidirectional=True,
batch_first=True,
)
def forward(self, x):
# x: b, c, w, h
x = self.conv1(x)
x = x.permute(0, 2, 3, 1).contiguous() # b, w, h, c
batch_size, w, h, c = x.size()
x = x.view(batch_size * w, h, c) # b*w, h, c
x, _ = self.gru(x)
x = x.view(batch_size, w, h, c)
x = x.permute(0, 3, 1, 2).contiguous()
return x