Source code for speechbrain.utils.filter_analysis

"""Implements utils to model and combine filter properties, i.e. compute how
window size, stride, etc. behave, which may be useful for certain usecases such
as streaming.

Authors:
 * Sylvain de Langen 2024
"""

from dataclasses import dataclass
from typing import Any, Iterable


[docs] @dataclass class FilterProperties: """Models the properties of something that behaves like a filter (e.g. convolutions, fbanks, etc.) over time.""" window_size: int """Size of the filter, i.e. the number of input frames on which a single output depends. Other than dilation, it is assumed that the window operates over a contiguous chunk of frames. Example: -------- .. code-block:: text size = 3, stride = 3 out <-a-> <-b-> <-c-> in 1 2 3 4 5 6 7 8 9 """ stride: int = 1 """Stride of the filter, i.e. how many input frames get skipped over from an output frame to the next (regardless of window size or dilation). Example: -------- .. code-block:: text size = 3, stride = 2 <-a-> <-b-> <-d-> out <-c-> in 1 2 3 4 5 6 7 8 9 """ dilation: int = 1 """Dilation rate of the filter. A window will consider every n-th (n=dilation) input frame. With dilation, the filter will still observe `size` input frames, but the window will span more frames. Dilation is mostly relevant to "a trous" convolutions. A dilation rate of 1, the default, effectively performs no dilation. Example: -------- .. code-block:: text size = 3, stride = 1, dilation = 3 <-------> dilation - 1 == 2 skips a a a | b | b | b | | c | | c | | c | | | d | | d | | d | | | | e | | e | | .. in 1 2 3 4 5 6 7 8 9 10 .. <-> stride == 1 """ causal: bool = False """Whether the filter is causal, i.e. whether an output frame only depends on past input frames (of a lower or equal index). In certain cases, such as 1D convolutions, this can simply be achieved by inserting padding to the left of the filter prior to applying the filter to the input tensor. Example: -------- .. code-block:: text size = 3, stride = 1, causal = true <-e-> <-d-> <-c-> b-> a in 1 2 3 4 5 """ def __post_init__(self): assert self.window_size > 0 assert self.stride > 0 assert ( self.dilation > 0 ), "Dilation must be >0. NOTE: a dilation of 1 means no dilation."
[docs] @staticmethod def pointwise_filter() -> "FilterProperties": """Returns filter properties for a trivial filter whose output frames only ever depend on their respective input frame.""" return FilterProperties(window_size=1, stride=1)
[docs] def get_effective_size(self): """The number of input frames that span the window, including those ignored by dilation.""" return 1 + ((self.window_size - 1) * self.dilation)
[docs] def get_convolution_padding(self): """The number of frames that need to be inserted on each end for a typical convolution.""" if self.window_size % 2 == 0: raise ValueError("Cannot determine padding with even window size") if self.causal: return self.get_effective_size() - 1 return (self.get_effective_size() - 1) // 2
[docs] def get_noncausal_equivalent(self): """From a causal filter definition, gets a compatible non-causal filter definition for which each output frame depends on the same input frames, plus some false dependencies.""" if not self.causal: return self return FilterProperties( # NOTE: valid even on even window sizes e.g. (2-1)*2+1 == 3 window_size=(self.window_size - 1) * 2 + 1, stride=self.stride, dilation=self.dilation, causal=False, )
[docs] def with_on_top( self, other: "FilterProperties", allow_approximate: bool = True ) -> "FilterProperties": """Considering the chain of filters `other_filter(self(x))`, returns recalculated properties of the resulting filter. Arguments --------- other_filter: FilterProperties The filter to combine `self` with. allow_approximate: bool, optional If `True` (the default), the resulting properties may be "pessimistic" and express false dependencies instead of erroring out when exact properties cannot be determined. This might be the case when stacking non-causal and causal filters. Depending on the usecase, this might be fine, but functions like `has_overlap` may erroneously start returning `True`. """ self_size = self.window_size if other.window_size % 2 == 0: if allow_approximate: other_size = other.window_size + 1 else: raise ValueError( "The filter to append cannot have an uneven window size. " "Specify `allow_approximate=True` if you do not need to " "analyze exact dependencies." ) else: other_size = other.window_size if (self.causal or other.causal) and not (self.causal and other.causal): if allow_approximate: return self.get_noncausal_equivalent().with_on_top( other.get_noncausal_equivalent() ) else: raise ValueError( "Cannot express exact properties of causal and non-causal " "filters. " "Specify `allow_approximate=True` if you do not need to " "analyze exact dependencies." ) out_size = self_size + (self.stride * (other_size - 1)) stride = self.stride * other.stride dilation = self.dilation * other.dilation causal = self.causal return FilterProperties(out_size, stride, dilation, causal)
[docs] def stack_filter_properties( filters: Iterable[Any], allow_approximate: bool = True ) -> FilterProperties: """Returns the filter properties of a sequence of stacked filters. If the sequence is empty, then a no-op filter is returned (with a size and stride of 1). Arguments --------- filters: FilterProperties | any The filters to combine, e.g. `[a, b, c]` modelling `c(b(a(x)))`. If an item is not an instance of :class:`FilterProperties`, then this attempts to call `.get_filter_properties()` over it. allow_approximate: bool, optional See `FilterProperties.with_on_top`.""" ret = FilterProperties.pointwise_filter() for prop in filters: if not isinstance(prop, FilterProperties): prop = prop.get_filter_properties() ret = ret.with_on_top(prop, allow_approximate) return ret