Source code for pycsamt.ai.nets.cnn1d

# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""
1-D CNN for EM inversion — Puzyrev (2019/2021) architecture.

The input feature vector (log-scaled apparent resistivity + phase, or
log-scaled TEM dBz/dt) is treated as a 1-D sequence over
frequency/time channels.  Three convolutional blocks encode local
frequency dependencies; a small fully-connected head maps to the
output model parameter vector.

References
----------
Puzyrev, V. et al. (2019). Deep CNNs for 1D inversion of EM data.
*EAGE Conference 2019*.

Puzyrev, V. & Swidinsky, A. (2021). Inversion of 1D frequency- and
time-domain EM data with CNNs. *Computers & Geosciences*, 149, 104681.
"""

from __future__ import annotations

from collections.abc import Sequence

__all__ = ["CNN1DNet"]


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"
        )


[docs] class CNN1DNet: """ Factory wrapper — call :func:`build` to get an ``nn.Module``. Use :class:`~pycsamt.ai.inversion.inv1d.EMInverter1D` instead of instantiating this class directly. Parameters ---------- n_features : int Length of the input feature vector. n_out : int Length of the output parameter vector (``2*n_layers - 1``). channels : sequence of int Number of filters in each convolutional block. kernel_size : int Convolutional kernel width (same padding applied). dropout : float Dropout probability in the FC head. """ def __init__( self, n_features: int, n_out: int, *, channels: Sequence[int] = (32, 64, 128), kernel_size: int = 5, dropout: float = 0.3, ): self.n_features = n_features self.n_out = n_out self.channels = tuple(channels) self.kernel_size = kernel_size self.dropout = dropout
[docs] def build(self): """Return the ``nn.Module``.""" torch, nn = _require_torch() return _CNN1DModule( self.n_features, self.n_out, channels=self.channels, kernel_size=self.kernel_size, dropout=self.dropout, )
def __repr__(self): return ( f"CNN1DNet(n_features={self.n_features}, n_out={self.n_out}, " f"channels={self.channels})" )
# ─── Internal nn.Module ─────────────────────────────────────────────────────── def _build_cnn1d(n_features, n_out, channels, kernel_size, dropout): """Build and return a CNN1D nn.Module (called lazily).""" torch, nn = _require_torch() class _CNN1DModule(nn.Module): def __init__(self): super().__init__() pad = kernel_size // 2 blocks = [] in_ch = 1 length = int(n_features) for out_ch in channels: blocks += [ nn.Conv1d(in_ch, out_ch, kernel_size, padding=pad), nn.BatchNorm1d(out_ch), nn.ReLU(inplace=True), ] # pooling a length-1 sequence would produce size 0; # skip it once the sequence cannot be halved if length >= 2: blocks.append(nn.MaxPool1d(2)) length //= 2 in_ch = out_ch self.encoder = nn.Sequential(*blocks) # Compute flattened size after encoder. The probe must # run in eval mode: BatchNorm cannot normalise a # single-sample batch in training mode. with torch.no_grad(): self.encoder.eval() dummy = torch.zeros(1, 1, n_features) flat = self.encoder(dummy).view(1, -1).shape[1] self.encoder.train() self.head = nn.Sequential( nn.Linear(flat, 256), nn.ReLU(inplace=True), nn.Dropout(dropout), nn.Linear(256, 128), nn.ReLU(inplace=True), nn.Linear(128, n_out), ) def forward(self, x): # x: (batch, n_features) x = x.unsqueeze(1) # (batch, 1, n_features) x = self.encoder(x) x = x.reshape(x.size(0), -1) return self.head(x) return _CNN1DModule() # Monkey-patch build to use the lazy factory def _cnn1d_build(self): return _build_cnn1d( self.n_features, self.n_out, self.channels, self.kernel_size, self.dropout, ) CNN1DNet.build = _cnn1d_build