Source code for zensols.deeplearn.layer.conv

"""Convolution network creation utilities.

"""
__author__ = 'Paul Landes'

from typing import Tuple, Any
from dataclasses import dataclass, field
from abc import abstractmethod, ABCMeta
import logging
import copy as cp
from functools import reduce
import math
from torch import nn
from . import LayerError

logger = logging.getLogger(__name__)


[docs] class Flattenable(object): """A class with a :obj:`flatten_dim` and :obj:`out_shape` properties. """ @property def out_shape(self) -> Tuple[int]: """Return the shape of the layer after flattened in to one dimension. """ pass @property def flatten_dim(self) -> int: """Return the number or neurons of the layer after flattening in to one dimension. """ return reduce(lambda x, y: x * y, self.out_shape) def __str__(self): sup = super().__str__() return f'{sup}, out: {self.out_shape}'
[docs] class Im2DimCalculator(Flattenable): """Convolution matrix dimension calculation utility. Implementation as Matrix Multiplication section. Example (im2col):: W_in = H_in = 227 Ch_in = D_in = 3 Ch_out = D_out = 3 K = 96 F = (11, 11) S = 4 P = 0 W_out = H_out = 227 - 11 + (2 * 0) / 4 = 55 output locations X_col = Fw^2 * D_out x W_out * H_out = 11^2 * 3 x 55 * 55 = 363 x 3025 Example (im2row):: W_row = 96 filters of size 11 x 11 x 3 => K x 11 * 11 * 3 = 96 x 363 Result of convolution: transpose(W_row) dot X_col. Must reshape back to 55 x 55 x 96 :see: `Stanford <http://cs231n.github.io/convolutional-networks/#conv>`_ """
[docs] def __init__(self, W: int, H: int, D: int = 1, K: int = 1, F: Tuple[int, int] = (2, 2), S: int = 1, P: int = 0): """Initialize. :param W: width :param H: height :param D: depth [of volume] (usually same as K) :param K: number of filters :param F: tuple of kernel/filter (width, height) :param S: stride :param P: padding """ self.W = W self.H = H self.D = D self.K = K self.F = F self.S = S self.P = P
[docs] def validate(self): W, H, F, P, S = self.W, self.H, self.F, self.P, self.S if ((W - F[0] + (2 * P)) % S): raise LayerError('Incongruous convolution width layer parameters') if ((H - F[1] + (2 * P)) % S): raise LayerError('Incongruous convolution height layer parameters') if (F[0] > (W + (2 * P))): raise LayerError(f'Kernel/filter {F} must be <= width {W} + 2 * padding {P}') if (F[1] > (H + (2 * P))): raise LayerError(f'Kernel/filter {F} must be <= height {H} + 2 * padding {P}') if self.W_row[1] != self.X_col[0]: raise LayerError(f'Columns of W_row {self.W_row} do not match ' + f'rows of X_col {self.X_col}')
@property def W_out(self): return int(((self.W - self.F[0] + (2 * self.P)) / self.S) + 1) @property def H_out(self): return int(((self.H - self.F[1] + (2 * self.P)) / self.S) + 1) @property def X_col(self): # TODO: not supported for non-square filters return (self.F[0] ** 2 * self.D, self.W_out * self.H_out) @property def W_row(self): # TODO: not supported for non-square filters return (self.K, (self.F[0] ** 2) * self.D) @property def out_shape(self): return (self.K, self.W_out, self.H_out)
[docs] def flatten(self, axis: int = 1): fd = self.flatten_dim W, H = (1, fd) if axis else (fd, 1) return self.__class__(W, H, F=(1, 1), D=1, K=1)
def __str__(self): attrs = 'W H D K F S P W_out H_out W_row X_col out_shape'.split() return ', '.join(map(lambda x: f'{x}={getattr(self, x)}', attrs)) def __repr__(self): return self.__str__()
[docs] @dataclass class ConvolutionLayerFactory(object): """Create convolution layers. Each attribute maps a corresponding attribuate variable in :class:`.Im2DimCalculator`, which documented in the parenthesis in the parameter documentation below. :param width: the width of the image/data (``W``) :param height: the height of the image/data (``H``) :param depth: the volume, which is usually same as ``n_filters`` (``D``) :param n_filters: the number of filters, aka the filter depth/volume (``K``) :param kernel_filter: the kernel filter dimension in width X height (``F``) :param stride: the stride, which is the number of cells to skip for each convolution (``S``) :param padding: the zero'd number of cells on the ends of the image/data (``P``) :see: `Stanford <http://cs231n.github.io/convolutional-networks/#conv>`_ """ width: int = field(default=1) height: int = field(default=1) depth: int = field(default=1) n_filters: int = field(default=1) kernel_filter: Tuple[int, int] = field(default=(2, 2)) stride: int = field(default=1) padding: int = field(default=0) @property def calc(self) -> Im2DimCalculator: return Im2DimCalculator(**{ 'W': self.width, 'H': self.height, 'D': self.depth, 'K': self.n_filters, 'F': self.kernel_filter, 'S': self.stride, 'P': self.padding})
[docs] def copy_calc(self, calc: Im2DimCalculator): self.width = calc.W self.height = calc.H self.depth = calc.D self.n_filters = calc.K self.kernel_filter = calc.F self.stride = calc.S self.padding = calc.P
[docs] def flatten(self) -> Any: """Return a new flattened instance of this class. """ clone = self.clone() calc = clone.calc.flatten() clone.copy_calc(calc) return clone
@property def flatten_dim(self) -> int: """Return the dimension of a flattened array of the convolution layer represented by this instance. """ return self.calc.flatten_dim
[docs] def clone(self, **kwargs) -> Any: """Return a clone of this factory instance. """ clone = cp.deepcopy(self) clone.__dict__.update(kwargs) return clone
[docs] def conv1d(self) -> nn.Conv1d: """Return a convolution layer in one dimension. """ c = self.calc return nn.Conv1d(c.D, c.K, c.F, padding=c.P, stride=c.S)
[docs] def conv2d(self) -> nn.Conv2d: """Return a convolution layer in two dimensions. """ c = self.calc return nn.Conv2d(c.D, c.K, c.F, padding=c.P, stride=c.S)
[docs] def batch_norm2d(self) -> nn.BatchNorm2d: """Return a 2D batch normalization layer. """ return nn.BatchNorm2d(self.calc.K)
def __str__(self): return str(self.calc)
[docs] @dataclass class PoolFactory(Flattenable, metaclass=ABCMeta): """Create a 2D max pool and output it's shape. :see: `Stanford <https://cs231n.github.io/convolutional-networks/#pool>`_ """ layer_factory: ConvolutionLayerFactory = field(repr=False, default=None) stride: int = field(default=1) padding: int = field(default=0) @abstractmethod def _calc_out_shape(self) -> Tuple[int]: pass
[docs] @abstractmethod def create_pool(self) -> nn.Module: pass
@property def out_shape(self) -> Tuple[int]: """Calculates the dimensions for a max pooling filter and creates a layer. :param F: the spacial extent (kernel filter) :param S: the stride """ return self._calc_out_shape() def __call__(self) -> nn.Module: """Return the pooling layer. """ return self.create_pool()
[docs] @dataclass class MaxPool1dFactory(PoolFactory): """Create a 1D max pool and output it's shape. """ kernel_filter: Tuple[int] = field(default=2) """The filter used for max pooling.""" def _calc_out_shape(self) -> Tuple[int]: calc = self.layer_factory.calc L = calc.flatten_dim F = self.kernel_filter S = self.stride P = self.padding Lo = math.floor((((L + (2 * P) - (F - 1) - 1)) / S) + 1) return (1, Lo)
[docs] def create_pool(self) -> nn.Module: return nn.MaxPool1d( self.kernel_filter, stride=self.stride, padding=self.padding)
[docs] @dataclass class MaxPool2dFactory(PoolFactory): """Create a 2D max pool and output it's shape. """ kernel_filter: Tuple[int, int] = field(default=(2, 2)) """The filter used for max pooling.""" def _calc_out_shape(self) -> Tuple[int]: calc = self.layer_factory.calc K, W, H = calc.out_shape F = self.kernel_filter S = self.stride P = self.padding W_2 = ((W - F[0] + (2 * P)) / S) + 1 H_2 = ((H - F[1] + (2 * P)) / S) + 1 return (K, int(W_2), int(H_2))
[docs] def create_pool(self) -> nn.Module: return nn.MaxPool2d( self.kernel_filter, stride=self.stride, padding=self.padding)