Source code for pycsamt.ai.nets.resnet

# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""
1-D Residual CNN for EM inversion — Liu et al. (2021) I8RFCN style.

Skip (residual) connections allow training very deep 1-D CNNs without
vanishing gradients, enabling the network to learn both coarse
resistivity contrasts (long-range dependencies) and fine-grained
frequency patterns (short-range) simultaneously.

Three residual stages progressively double the channel width while
halving the spatial resolution.  A global average-pool collapses the
spatial dimension before the final linear output head.

References
----------
Liu, W. et al. (2021). Deep learning AMT inversion using
residual-based deep convolutional neural network.
*Journal of Geophysics and Engineering*, 18(6), 876-888.
"""

from __future__ import annotations

from collections.abc import Sequence

__all__ = ["ResNet1DNet"]


def _require_torch():
    try:
        import torch
        import torch.nn as nn

        return torch, nn
    except ImportError:
        raise ImportError(
            "PyTorch is required for pycsamt.ai.nets. "
            "Install with: pip install torch"
        )


# ── Module-level class definitions ───────────────
# Defined at module scope so pickle can locate them
# when coordinator.py checkpoints AgentResult dicts.

try:
    import torch.nn as nn
    import torch.nn.functional as F

    class _ResBlock(nn.Module):
        """Basic 1-D residual block."""

        def __init__(self, in_ch, out_ch, stride=1):
            super().__init__()
            self.conv1 = nn.Conv1d(
                in_ch,
                out_ch,
                3,
                stride=stride,
                padding=1,
                bias=False,
            )
            self.bn1 = nn.BatchNorm1d(out_ch)
            self.conv2 = nn.Conv1d(
                out_ch,
                out_ch,
                3,
                padding=1,
                bias=False,
            )
            self.bn2 = nn.BatchNorm1d(out_ch)
            if stride != 1 or in_ch != out_ch:
                self.shortcut = nn.Sequential(
                    nn.Conv1d(
                        in_ch,
                        out_ch,
                        1,
                        stride=stride,
                        bias=False,
                    ),
                    nn.BatchNorm1d(out_ch),
                )
            else:
                self.shortcut = nn.Identity()

        def forward(self, x):
            out = F.relu(
                self.bn1(self.conv1(x)),
                inplace=True,
            )
            out = self.bn2(self.conv2(out))
            return F.relu(
                out + self.shortcut(x),
                inplace=True,
            )

    class _ResNet1DModule(nn.Module):
        def __init__(
            self,
            channels,
            n_blocks,
            dropout,
            n_out,
        ):
            super().__init__()
            self.stem = nn.Sequential(
                nn.Conv1d(
                    1,
                    channels[0],
                    7,
                    padding=3,
                    bias=False,
                ),
                nn.BatchNorm1d(channels[0]),
                nn.ReLU(inplace=True),
            )
            stage_list = []
            in_ch = channels[0]
            for si, out_ch in enumerate(channels):
                for bi in range(n_blocks):
                    stride = 2 if (si > 0 and bi == 0) else 1
                    stage_list.append(_ResBlock(in_ch, out_ch, stride))
                    in_ch = out_ch
            self.stages = nn.Sequential(*stage_list)
            self.pool = nn.AdaptiveAvgPool1d(1)
            self.dropout = nn.Dropout(dropout)
            self.fc = nn.Linear(channels[-1], n_out)

        def forward(self, x):
            x = x.unsqueeze(1)
            x = self.stem(x)
            x = self.stages(x)
            x = self.pool(x).squeeze(-1)
            x = self.dropout(x)
            return self.fc(x)

except ImportError:
    pass  # torch not installed


# ── Factory class ─────────────────────────────────


[docs] class ResNet1DNet: r""" Factory wrapper for the 1-D residual CNN. Parameters ---------- n_features : int Input feature-vector length. n_out : int Output parameter vector length (``2*n_layers - 1``). channels : sequence of int Filter counts for the three residual stages. Default ``(64, 128, 256)`` replicates the I8RFCN paper. dropout : float Dropout before the final linear layer. n_blocks : int Number of residual blocks per stage (default 2). """ def __init__( self, n_features: int, n_out: int, *, channels: Sequence[int] = (64, 128, 256), dropout: float = 0.3, n_blocks: int = 2, ): self.n_features = n_features self.n_out = n_out self.channels = tuple(channels) self.dropout = dropout self.n_blocks = n_blocks
[docs] def build(self): """Return the ``nn.Module``.""" return _build_resnet1d( self.n_features, self.n_out, channels=self.channels, dropout=self.dropout, n_blocks=self.n_blocks, )
def __repr__(self): return ( f"ResNet1DNet(" f"n_features={self.n_features}, " f"n_out={self.n_out}, " f"channels={self.channels}, " f"n_blocks={self.n_blocks})" )
# ── Internal build ──────────────────────────────── def _build_resnet1d(n_features, n_out, channels, dropout, n_blocks): _require_torch() return _ResNet1DModule(tuple(channels), n_blocks, dropout, n_out)