mirror of
https://github.com/macaodha/batdetect2.git
synced 2025-06-29 22:51:58 +02:00
Add decode functions to classes module
This commit is contained in:
parent
04ed669c4f
commit
a2ec190b73
@ -11,13 +11,6 @@ from batdetect2.targets.labels import (
|
|||||||
generate_heatmaps,
|
generate_heatmaps,
|
||||||
load_label_config,
|
load_label_config,
|
||||||
)
|
)
|
||||||
from batdetect2.targets.targets import (
|
|
||||||
TargetConfig,
|
|
||||||
build_decoder,
|
|
||||||
build_target_encoder,
|
|
||||||
get_class_names,
|
|
||||||
load_target_config,
|
|
||||||
)
|
|
||||||
from batdetect2.targets.terms import (
|
from batdetect2.targets.terms import (
|
||||||
TagInfo,
|
TagInfo,
|
||||||
TermInfo,
|
TermInfo,
|
||||||
@ -51,23 +44,18 @@ __all__ = [
|
|||||||
"ReplaceRule",
|
"ReplaceRule",
|
||||||
"SoundEventTransformation",
|
"SoundEventTransformation",
|
||||||
"TagInfo",
|
"TagInfo",
|
||||||
"TargetConfig",
|
|
||||||
"TermInfo",
|
"TermInfo",
|
||||||
"TransformConfig",
|
"TransformConfig",
|
||||||
"build_decoder",
|
|
||||||
"build_target_encoder",
|
|
||||||
"build_transform_from_rule",
|
"build_transform_from_rule",
|
||||||
"build_transformation_from_config",
|
"build_transformation_from_config",
|
||||||
"call_type",
|
"call_type",
|
||||||
"derivation_registry",
|
"derivation_registry",
|
||||||
"generate_heatmaps",
|
"generate_heatmaps",
|
||||||
"get_class_names",
|
|
||||||
"get_derivation",
|
"get_derivation",
|
||||||
"get_tag_from_info",
|
"get_tag_from_info",
|
||||||
"get_term_from_key",
|
"get_term_from_key",
|
||||||
"individual",
|
"individual",
|
||||||
"load_label_config",
|
"load_label_config",
|
||||||
"load_target_config",
|
|
||||||
"load_transformation_config",
|
"load_transformation_config",
|
||||||
"load_transformation_from_config",
|
"load_transformation_from_config",
|
||||||
"register_term",
|
"register_term",
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Callable, List, Literal, Optional, Set
|
from typing import Callable, Dict, List, Literal, Optional, Set, Tuple
|
||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
from soundevent import data
|
from soundevent import data
|
||||||
|
|
||||||
from batdetect2.configs import BaseConfig, load_config
|
from batdetect2.configs import BaseConfig, load_config
|
||||||
from batdetect2.targets.terms import (
|
from batdetect2.targets.terms import (
|
||||||
|
GENERIC_CLASS_KEY,
|
||||||
TagInfo,
|
TagInfo,
|
||||||
TermRegistry,
|
TermRegistry,
|
||||||
get_tag_from_info,
|
get_tag_from_info,
|
||||||
@ -15,12 +16,17 @@ from batdetect2.targets.terms import (
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"SoundEventEncoder",
|
"SoundEventEncoder",
|
||||||
|
"SoundEventDecoder",
|
||||||
"TargetClass",
|
"TargetClass",
|
||||||
"ClassesConfig",
|
"ClassesConfig",
|
||||||
"load_classes_config",
|
"load_classes_config",
|
||||||
"build_encoder_from_config",
|
|
||||||
"load_encoder_from_config",
|
"load_encoder_from_config",
|
||||||
|
"load_decoder_from_config",
|
||||||
|
"build_encoder_from_config",
|
||||||
|
"build_decoder_from_config",
|
||||||
|
"build_generic_class_tags_from_config",
|
||||||
"get_class_names_from_config",
|
"get_class_names_from_config",
|
||||||
|
"DEFAULT_SPECIES_LIST",
|
||||||
]
|
]
|
||||||
|
|
||||||
SoundEventEncoder = Callable[[data.SoundEventAnnotation], Optional[str]]
|
SoundEventEncoder = Callable[[data.SoundEventAnnotation], Optional[str]]
|
||||||
@ -33,63 +39,184 @@ rules, the function returns None.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
SoundEventDecoder = Callable[[str], List[data.Tag]]
|
||||||
|
"""Type alias for a sound event class decoder function.
|
||||||
|
|
||||||
|
A decoder function takes a class name string (as predicted by the model or
|
||||||
|
assigned during encoding) and returns a list of `soundevent.data.Tag` objects
|
||||||
|
that represent that class according to the configuration. This is used to
|
||||||
|
translate model outputs back into meaningful annotations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_SPECIES_LIST = [
|
||||||
|
"Barbastella barbastellus",
|
||||||
|
"Eptesicus serotinus",
|
||||||
|
"Myotis alcathoe",
|
||||||
|
"Myotis bechsteinii",
|
||||||
|
"Myotis brandtii",
|
||||||
|
"Myotis daubentonii",
|
||||||
|
"Myotis mystacinus",
|
||||||
|
"Myotis nattereri",
|
||||||
|
"Nyctalus leisleri",
|
||||||
|
"Nyctalus noctula",
|
||||||
|
"Pipistrellus nathusii",
|
||||||
|
"Pipistrellus pipistrellus",
|
||||||
|
"Pipistrellus pygmaeus",
|
||||||
|
"Plecotus auritus",
|
||||||
|
"Plecotus austriacus",
|
||||||
|
"Rhinolophus ferrumequinum",
|
||||||
|
"Rhinolophus hipposideros",
|
||||||
|
]
|
||||||
|
"""A default list of common bat species names found in the UK."""
|
||||||
|
|
||||||
|
|
||||||
class TargetClass(BaseConfig):
|
class TargetClass(BaseConfig):
|
||||||
"""Defines the criteria for assigning an annotation to a specific class.
|
"""Defines criteria for encoding annotations and decoding predictions.
|
||||||
|
|
||||||
Each instance represents one potential output class for the classification
|
Each instance represents one potential output class for the classification
|
||||||
model. It specifies the class name and the tag conditions an annotation
|
model. It specifies:
|
||||||
must meet to be assigned this class label.
|
1. A unique `name` for the class.
|
||||||
|
2. The tag conditions (`tags` and `match_type`) an annotation must meet to
|
||||||
|
be assigned this class name during training data preparation (encoding).
|
||||||
|
3. An optional, alternative set of tags (`output_tags`) to be used when
|
||||||
|
converting a model's prediction of this class name back into annotation
|
||||||
|
tags (decoding).
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
name : str
|
name : str
|
||||||
The unique name assigned to this target class (e.g., 'pippip',
|
The unique name assigned to this target class (e.g., 'pippip',
|
||||||
'myodau', 'noise'). This name will be used as the label during model
|
'myodau', 'noise'). This name is used as the label during model
|
||||||
training and output. Should be unique across all TargetClass
|
training and is the expected output from the model's prediction.
|
||||||
definitions in a configuration.
|
Should be unique across all TargetClass definitions in a configuration.
|
||||||
tag : List[TagInfo]
|
tags : List[TagInfo]
|
||||||
A list of one or more tags (defined using `TagInfo`) that an annotation
|
A list of one or more tags (defined using `TagInfo`) used to identify
|
||||||
must possess to potentially match this class.
|
if an existing annotation belongs to this class during encoding (data
|
||||||
|
preparation for training). The `match_type` attribute determines how
|
||||||
|
these tags are evaluated.
|
||||||
match_type : Literal["all", "any"], default="all"
|
match_type : Literal["all", "any"], default="all"
|
||||||
Determines how the `tag` list is evaluated:
|
Determines how the `tags` list is evaluated during encoding:
|
||||||
- "all": The annotation must have *all* the tags listed in the `tag`
|
- "all": The annotation must have *all* the tags listed to match.
|
||||||
field to match this class definition.
|
|
||||||
- "any": The annotation must have *at least one* of the tags listed
|
- "any": The annotation must have *at least one* of the tags listed
|
||||||
in the `tag` field to match this class definition.
|
to match.
|
||||||
|
output_tags: Optional[List[TagInfo]], default=None
|
||||||
|
An optional list of tags (defined using `TagInfo`) to be assigned to a
|
||||||
|
new annotation when the model predicts this class `name`. If `None`
|
||||||
|
(default), the tags listed in the `tags` field will be used for
|
||||||
|
decoding. If provided, this list overrides the `tags` field for the
|
||||||
|
purpose of decoding predictions back into meaningful annotation tags.
|
||||||
|
This allows, for example, training on broader categories but decoding
|
||||||
|
to more specific representative tags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
tags: List[TagInfo] = Field(default_factory=list, min_length=1)
|
tags: List[TagInfo] = Field(min_length=1)
|
||||||
match_type: Literal["all", "any"] = Field(default="all")
|
match_type: Literal["all", "any"] = Field(default="all")
|
||||||
|
output_tags: Optional[List[TagInfo]] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_classes() -> List[TargetClass]:
|
||||||
|
"""Generate a list of default target classes.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[TargetClass]
|
||||||
|
A list of TargetClass objects, one for each species in
|
||||||
|
DEFAULT_SPECIES_LIST. The class names are simplified versions of the
|
||||||
|
species names.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
TargetClass(
|
||||||
|
name=_get_default_class_name(value),
|
||||||
|
tags=[TagInfo(key=GENERIC_CLASS_KEY, value=value)],
|
||||||
|
)
|
||||||
|
for value in DEFAULT_SPECIES_LIST
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_class_name(species: str) -> str:
|
||||||
|
"""Generate a default class name from a species name.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
species : str
|
||||||
|
The species name (e.g., "Myotis daubentonii").
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
A simplified class name (e.g., "myodau").
|
||||||
|
The genus and species names are converted to lowercase,
|
||||||
|
the first three letters of each are taken, and concatenated.
|
||||||
|
"""
|
||||||
|
genus, species = species.strip().split(" ")
|
||||||
|
return f"{genus.lower()[:3]}{species.lower()[:3]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_generic_class() -> List[TagInfo]:
|
||||||
|
"""Generate the default list of TagInfo objects for the generic class.
|
||||||
|
|
||||||
|
Provides a default set of tags used to represent the generic "Bat" category
|
||||||
|
when decoding predictions that didn't match a specific class.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[TagInfo]
|
||||||
|
A list containing default TagInfo objects, typically representing
|
||||||
|
`call_type: Echolocation` and `order: Chiroptera`.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
TagInfo(key="call_type", value="Echolocation"),
|
||||||
|
TagInfo(key="order", value="Chiroptera"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ClassesConfig(BaseConfig):
|
class ClassesConfig(BaseConfig):
|
||||||
"""Configuration model holding the list of target class definitions.
|
"""Configuration defining target classes and the generic fallback category.
|
||||||
|
|
||||||
|
Holds the ordered list of specific target class definitions (`TargetClass`)
|
||||||
|
and defines the tags representing the generic category for sounds that pass
|
||||||
|
filtering but do not match any specific class.
|
||||||
|
|
||||||
The order of `TargetClass` objects in the `classes` list defines the
|
The order of `TargetClass` objects in the `classes` list defines the
|
||||||
priority for classification. When encoding an annotation, the system checks
|
priority for classification during encoding. The system checks annotations
|
||||||
against the class definitions in this sequence and assigns the name of the
|
against these definitions sequentially and assigns the name of the *first*
|
||||||
*first* matching class.
|
matching class.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
classes : List[TargetClass]
|
classes : List[TargetClass]
|
||||||
An ordered list of target class definitions. The order determines
|
An ordered list of specific target class definitions. The order
|
||||||
matching priority (first match wins).
|
determines matching priority (first match wins). Defaults to a
|
||||||
|
standard set of classes via `get_default_classes`.
|
||||||
|
generic_class : List[TagInfo]
|
||||||
|
A list of tags defining the "generic" or "unclassified but relevant"
|
||||||
|
category (e.g., representing a generic 'Bat' call that wasn't
|
||||||
|
assigned to a specific species). These tags are typically assigned
|
||||||
|
during decoding when a sound event was detected and passed filtering
|
||||||
|
but did not match any specific class rule defined in the `classes` list.
|
||||||
|
Defaults to a standard set of tags via `get_default_generic_class`.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
ValueError
|
ValueError
|
||||||
If validation fails (e.g., non-unique class names).
|
If validation fails (e.g., non-unique class names in the `classes`
|
||||||
|
list).
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
It is crucial that the `name` attribute of each `TargetClass` in the
|
- It is crucial that the `name` attribute of each `TargetClass` in the
|
||||||
`classes` list is unique. This configuration includes a validator to
|
`classes` list is unique. This configuration includes a validator to
|
||||||
enforce this uniqueness.
|
enforce this uniqueness.
|
||||||
|
- The `generic_class` tags provide a baseline identity for relevant sounds
|
||||||
|
that don't fit into more specific defined categories.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
classes: List[TargetClass] = Field(default_factory=list)
|
classes: List[TargetClass] = Field(default_factory=_get_default_classes)
|
||||||
|
|
||||||
|
generic_class: List[TagInfo] = Field(
|
||||||
|
default_factory=_get_default_generic_class
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator("classes")
|
@field_validator("classes")
|
||||||
def check_unique_class_names(cls, v: List[TargetClass]):
|
def check_unique_class_names(cls, v: List[TargetClass]):
|
||||||
@ -108,37 +235,7 @@ class ClassesConfig(BaseConfig):
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
def load_classes_config(
|
def _is_target_class(
|
||||||
path: data.PathLike,
|
|
||||||
field: Optional[str] = None,
|
|
||||||
) -> ClassesConfig:
|
|
||||||
"""Load the target classes configuration from a file.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
path : data.PathLike
|
|
||||||
Path to the configuration file (YAML).
|
|
||||||
field : str, optional
|
|
||||||
If the classes configuration is nested under a specific key in the
|
|
||||||
file, specify the key here. Defaults to None.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
ClassesConfig
|
|
||||||
The loaded and validated classes configuration object.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
FileNotFoundError
|
|
||||||
If the config file path does not exist.
|
|
||||||
pydantic.ValidationError
|
|
||||||
If the config file structure does not match the ClassesConfig schema
|
|
||||||
or if class names are not unique.
|
|
||||||
"""
|
|
||||||
return load_config(path, schema=ClassesConfig, field=field)
|
|
||||||
|
|
||||||
|
|
||||||
def is_target_class(
|
|
||||||
sound_event_annotation: data.SoundEventAnnotation,
|
sound_event_annotation: data.SoundEventAnnotation,
|
||||||
tags: Set[data.Tag],
|
tags: Set[data.Tag],
|
||||||
match_all: bool = True,
|
match_all: bool = True,
|
||||||
@ -185,6 +282,38 @@ def get_class_names_from_config(config: ClassesConfig) -> List[str]:
|
|||||||
return [class_info.name for class_info in config.classes]
|
return [class_info.name for class_info in config.classes]
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_with_multiple_classifiers(
|
||||||
|
sound_event_annotation: data.SoundEventAnnotation,
|
||||||
|
classifiers: List[Tuple[str, Callable[[data.SoundEventAnnotation], bool]]],
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Encode an annotation by checking against a list of classifiers.
|
||||||
|
|
||||||
|
Internal helper function used by the `SoundEventEncoder`. It iterates
|
||||||
|
through the provided list of (class_name, classifier_function) pairs.
|
||||||
|
Returns the name associated with the first classifier function that
|
||||||
|
returns True for the given annotation.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
sound_event_annotation : data.SoundEventAnnotation
|
||||||
|
The annotation to encode.
|
||||||
|
classifiers : List[Tuple[str, Callable[[data.SoundEventAnnotation], bool]]]
|
||||||
|
An ordered list where each tuple contains a class name and a function
|
||||||
|
that returns True if the annotation matches that class. The order
|
||||||
|
determines priority.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str or None
|
||||||
|
The name of the first matching class, or None if no classifier matches.
|
||||||
|
"""
|
||||||
|
for class_name, classifier in classifiers:
|
||||||
|
if classifier(sound_event_annotation):
|
||||||
|
return class_name
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def build_encoder_from_config(
|
def build_encoder_from_config(
|
||||||
config: ClassesConfig,
|
config: ClassesConfig,
|
||||||
term_registry: TermRegistry = term_registry,
|
term_registry: TermRegistry = term_registry,
|
||||||
@ -221,7 +350,7 @@ def build_encoder_from_config(
|
|||||||
(
|
(
|
||||||
class_info.name,
|
class_info.name,
|
||||||
partial(
|
partial(
|
||||||
is_target_class,
|
_is_target_class,
|
||||||
tags={
|
tags={
|
||||||
get_tag_from_info(tag_info, term_registry=term_registry)
|
get_tag_from_info(tag_info, term_registry=term_registry)
|
||||||
for tag_info in class_info.tags
|
for tag_info in class_info.tags
|
||||||
@ -232,31 +361,170 @@ def build_encoder_from_config(
|
|||||||
for class_info in config.classes
|
for class_info in config.classes
|
||||||
]
|
]
|
||||||
|
|
||||||
def encoder(
|
return partial(
|
||||||
sound_event_annotation: data.SoundEventAnnotation,
|
_encode_with_multiple_classifiers,
|
||||||
) -> Optional[str]:
|
classifiers=binary_classifiers,
|
||||||
"""Assign a class name to an annotation based on configured rules.
|
)
|
||||||
|
|
||||||
Iterates through pre-compiled classifiers in priority order. Returns
|
|
||||||
the name of the first matching class, or None if no match is found.
|
|
||||||
|
|
||||||
Parameters
|
def _decode_class(
|
||||||
----------
|
name: str,
|
||||||
sound_event_annotation : data.SoundEventAnnotation
|
mapping: Dict[str, List[data.Tag]],
|
||||||
The annotation to encode.
|
raise_on_error: bool = True,
|
||||||
|
) -> List[data.Tag]:
|
||||||
|
"""Decode a class name into a list of representative tags using a mapping.
|
||||||
|
|
||||||
Returns
|
Internal helper function used by the `SoundEventDecoder`. Looks up the
|
||||||
-------
|
provided class `name` in the `mapping` dictionary.
|
||||||
str or None
|
|
||||||
The name of the matched class, or None.
|
|
||||||
"""
|
|
||||||
for class_name, classifier in binary_classifiers:
|
|
||||||
if classifier(sound_event_annotation):
|
|
||||||
return class_name
|
|
||||||
|
|
||||||
return None
|
Parameters
|
||||||
|
----------
|
||||||
|
name : str
|
||||||
|
The class name to decode.
|
||||||
|
mapping : Dict[str, List[data.Tag]]
|
||||||
|
A dictionary mapping class names to lists of `soundevent.data.Tag`
|
||||||
|
objects.
|
||||||
|
raise_on_error : bool, default=True
|
||||||
|
If True, raises a ValueError if the `name` is not found in the
|
||||||
|
`mapping`. If False, returns an empty list if the `name` is not found.
|
||||||
|
|
||||||
return encoder
|
Returns
|
||||||
|
-------
|
||||||
|
List[data.Tag]
|
||||||
|
The list of tags associated with the class name, or an empty list if
|
||||||
|
not found and `raise_on_error` is False.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If `name` is not found in `mapping` and `raise_on_error` is True.
|
||||||
|
"""
|
||||||
|
if name not in mapping and raise_on_error:
|
||||||
|
raise ValueError(f"Class {name} not found in mapping.")
|
||||||
|
|
||||||
|
if name not in mapping:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return mapping[name]
|
||||||
|
|
||||||
|
|
||||||
|
def build_decoder_from_config(
|
||||||
|
config: ClassesConfig,
|
||||||
|
term_registry: TermRegistry = term_registry,
|
||||||
|
raise_on_unmapped: bool = False,
|
||||||
|
) -> SoundEventDecoder:
|
||||||
|
"""Build a sound event decoder function from the classes configuration.
|
||||||
|
|
||||||
|
Creates a callable `SoundEventDecoder` that maps a class name string
|
||||||
|
back to a list of representative `soundevent.data.Tag` objects based on
|
||||||
|
the `ClassesConfig`. It uses the `output_tags` field if provided in a
|
||||||
|
`TargetClass`, otherwise falls back to the `tags` field.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
config : ClassesConfig
|
||||||
|
The loaded and validated classes configuration object.
|
||||||
|
term_registry : TermRegistry, optional
|
||||||
|
The TermRegistry instance used to look up term keys. Defaults to the
|
||||||
|
global `batdetect2.targets.terms.registry`.
|
||||||
|
raise_on_unmapped : bool, default=False
|
||||||
|
If True, the returned decoder function will raise a ValueError if asked
|
||||||
|
to decode a class name that is not in the configuration. If False, it
|
||||||
|
will return an empty list for unmapped names.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
SoundEventDecoder
|
||||||
|
A callable function that takes a class name string and returns a list
|
||||||
|
of `soundevent.data.Tag` objects.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
KeyError
|
||||||
|
If a term key specified in the configuration (`output_tags`, `tags`, or
|
||||||
|
`generic_class`) is not found in the provided `term_registry`.
|
||||||
|
"""
|
||||||
|
mapping = {}
|
||||||
|
for class_info in config.classes:
|
||||||
|
tags_to_use = (
|
||||||
|
class_info.output_tags
|
||||||
|
if class_info.output_tags is not None
|
||||||
|
else class_info.tags
|
||||||
|
)
|
||||||
|
mapping[class_info.name] = [
|
||||||
|
get_tag_from_info(tag_info, term_registry=term_registry)
|
||||||
|
for tag_info in tags_to_use
|
||||||
|
]
|
||||||
|
|
||||||
|
return partial(
|
||||||
|
_decode_class,
|
||||||
|
mapping=mapping,
|
||||||
|
raise_on_error=raise_on_unmapped,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_generic_class_tags_from_config(
|
||||||
|
config: ClassesConfig,
|
||||||
|
term_registry: TermRegistry = term_registry,
|
||||||
|
) -> List[data.Tag]:
|
||||||
|
"""Extract and build the list of tags for the generic class from config.
|
||||||
|
|
||||||
|
Converts the list of `TagInfo` objects defined in `config.generic_class`
|
||||||
|
into a list of `soundevent.data.Tag` objects using the term registry.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
config : ClassesConfig
|
||||||
|
The loaded classes configuration object.
|
||||||
|
term_registry : TermRegistry, optional
|
||||||
|
The TermRegistry instance for term lookups. Defaults to the global
|
||||||
|
`batdetect2.targets.terms.registry`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[data.Tag]
|
||||||
|
The list of fully constructed tags representing the generic class.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
KeyError
|
||||||
|
If a term key specified in `config.generic_class` is not found in the
|
||||||
|
provided `term_registry`.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
get_tag_from_info(tag_info, term_registry=term_registry)
|
||||||
|
for tag_info in config.generic_class
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_classes_config(
|
||||||
|
path: data.PathLike,
|
||||||
|
field: Optional[str] = None,
|
||||||
|
) -> ClassesConfig:
|
||||||
|
"""Load the target classes configuration from a file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path : data.PathLike
|
||||||
|
Path to the configuration file (YAML).
|
||||||
|
field : str, optional
|
||||||
|
If the classes configuration is nested under a specific key in the
|
||||||
|
file, specify the key here. Defaults to None.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ClassesConfig
|
||||||
|
The loaded and validated classes configuration object.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FileNotFoundError
|
||||||
|
If the config file path does not exist.
|
||||||
|
pydantic.ValidationError
|
||||||
|
If the config file structure does not match the ClassesConfig schema
|
||||||
|
or if class names are not unique.
|
||||||
|
"""
|
||||||
|
return load_config(path, schema=ClassesConfig, field=field)
|
||||||
|
|
||||||
|
|
||||||
def load_encoder_from_config(
|
def load_encoder_from_config(
|
||||||
@ -298,3 +566,53 @@ def load_encoder_from_config(
|
|||||||
"""
|
"""
|
||||||
config = load_classes_config(path, field=field)
|
config = load_classes_config(path, field=field)
|
||||||
return build_encoder_from_config(config, term_registry=term_registry)
|
return build_encoder_from_config(config, term_registry=term_registry)
|
||||||
|
|
||||||
|
|
||||||
|
def load_decoder_from_config(
|
||||||
|
path: data.PathLike,
|
||||||
|
field: Optional[str] = None,
|
||||||
|
term_registry: TermRegistry = term_registry,
|
||||||
|
raise_on_unmapped: bool = False,
|
||||||
|
) -> SoundEventDecoder:
|
||||||
|
"""Load a class decoder function directly from a configuration file.
|
||||||
|
|
||||||
|
This is a convenience function that combines loading the `ClassesConfig`
|
||||||
|
from a file and building the final `SoundEventDecoder` function.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path : data.PathLike
|
||||||
|
Path to the configuration file (e.g., YAML).
|
||||||
|
field : str, optional
|
||||||
|
If the classes configuration is nested under a specific key in the
|
||||||
|
file, specify the key here. Defaults to None.
|
||||||
|
term_registry : TermRegistry, optional
|
||||||
|
The TermRegistry instance used for term lookups. Defaults to the
|
||||||
|
global `batdetect2.targets.terms.registry`.
|
||||||
|
raise_on_unmapped : bool, default=False
|
||||||
|
If True, the returned decoder function will raise a ValueError if asked
|
||||||
|
to decode a class name that is not in the configuration. If False, it
|
||||||
|
will return an empty list for unmapped names.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
SoundEventDecoder
|
||||||
|
The final decoder function ready to convert class names back into tags.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FileNotFoundError
|
||||||
|
If the config file path does not exist.
|
||||||
|
pydantic.ValidationError
|
||||||
|
If the config file structure does not match the ClassesConfig schema
|
||||||
|
or if class names are not unique.
|
||||||
|
KeyError
|
||||||
|
If a term key specified in the configuration is not found in the
|
||||||
|
provided `term_registry` during the build process.
|
||||||
|
"""
|
||||||
|
config = load_classes_config(path, field=field)
|
||||||
|
return build_decoder_from_config(
|
||||||
|
config,
|
||||||
|
term_registry=term_registry,
|
||||||
|
raise_on_unmapped=raise_on_unmapped,
|
||||||
|
)
|
||||||
|
@ -1,147 +0,0 @@
|
|||||||
from collections.abc import Iterable
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Callable, List, Optional
|
|
||||||
|
|
||||||
from pydantic import Field
|
|
||||||
from soundevent import data
|
|
||||||
|
|
||||||
from batdetect2.configs import BaseConfig, load_config
|
|
||||||
from batdetect2.targets.terms import TagInfo, get_tag_from_info
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"TargetConfig",
|
|
||||||
"load_target_config",
|
|
||||||
"build_target_encoder",
|
|
||||||
"build_decoder",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ReplaceConfig(BaseConfig):
|
|
||||||
"""Configuration for replacing tags."""
|
|
||||||
|
|
||||||
original: TagInfo
|
|
||||||
replacement: TagInfo
|
|
||||||
|
|
||||||
|
|
||||||
class TargetConfig(BaseConfig):
|
|
||||||
"""Configuration for target generation."""
|
|
||||||
|
|
||||||
classes: List[TagInfo] = Field(
|
|
||||||
default_factory=lambda: [
|
|
||||||
TagInfo(key="class", value=value) for value in DEFAULT_SPECIES_LIST
|
|
||||||
]
|
|
||||||
)
|
|
||||||
generic_class: Optional[TagInfo] = Field(
|
|
||||||
default_factory=lambda: TagInfo(key="class", value="Bat")
|
|
||||||
)
|
|
||||||
|
|
||||||
include: Optional[List[TagInfo]] = Field(
|
|
||||||
default_factory=lambda: [TagInfo(key="event", value="Echolocation")]
|
|
||||||
)
|
|
||||||
|
|
||||||
exclude: Optional[List[TagInfo]] = Field(
|
|
||||||
default_factory=lambda: [
|
|
||||||
TagInfo(key="class", value=""),
|
|
||||||
TagInfo(key="class", value=" "),
|
|
||||||
TagInfo(key="class", value="Unknown"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
replace: Optional[List[ReplaceConfig]] = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_tag_label(tag_info: TagInfo) -> str:
|
|
||||||
# TODO: Review this
|
|
||||||
return tag_info.value
|
|
||||||
|
|
||||||
|
|
||||||
def get_class_names(classes: List[TagInfo]) -> List[str]:
|
|
||||||
return sorted({get_tag_label(tag) for tag in classes})
|
|
||||||
|
|
||||||
|
|
||||||
def build_replacer(
|
|
||||||
rules: List[ReplaceConfig],
|
|
||||||
) -> Callable[[data.Tag], data.Tag]:
|
|
||||||
mapping = {
|
|
||||||
get_tag_from_info(rule.original): get_tag_from_info(rule.replacement)
|
|
||||||
for rule in rules
|
|
||||||
}
|
|
||||||
|
|
||||||
def replacer(tag: data.Tag) -> data.Tag:
|
|
||||||
return mapping.get(tag, tag)
|
|
||||||
|
|
||||||
return replacer
|
|
||||||
|
|
||||||
|
|
||||||
def build_target_encoder(
|
|
||||||
classes: List[TagInfo],
|
|
||||||
replacement_rules: Optional[List[ReplaceConfig]] = None,
|
|
||||||
) -> Callable[[Iterable[data.Tag]], Optional[str]]:
|
|
||||||
target_tags = set([get_tag_from_info(tag) for tag in classes])
|
|
||||||
|
|
||||||
tag_mapping = {
|
|
||||||
tag: get_tag_label(tag_info)
|
|
||||||
for tag, tag_info in zip(target_tags, classes)
|
|
||||||
}
|
|
||||||
|
|
||||||
replacer = (
|
|
||||||
build_replacer(replacement_rules) if replacement_rules else lambda x: x
|
|
||||||
)
|
|
||||||
|
|
||||||
def encoder(
|
|
||||||
tags: Iterable[data.Tag],
|
|
||||||
) -> Optional[str]:
|
|
||||||
sanitized_tags = {replacer(tag) for tag in tags}
|
|
||||||
|
|
||||||
intersection = sanitized_tags & target_tags
|
|
||||||
|
|
||||||
if not intersection:
|
|
||||||
return None
|
|
||||||
|
|
||||||
first = intersection.pop()
|
|
||||||
return tag_mapping[first]
|
|
||||||
|
|
||||||
return encoder
|
|
||||||
|
|
||||||
|
|
||||||
def build_decoder(
|
|
||||||
classes: List[TagInfo],
|
|
||||||
) -> Callable[[str], List[data.Tag]]:
|
|
||||||
target_tags = set([get_tag_from_info(tag) for tag in classes])
|
|
||||||
tag_mapping = {
|
|
||||||
get_tag_label(tag_info): tag
|
|
||||||
for tag, tag_info in zip(target_tags, classes)
|
|
||||||
}
|
|
||||||
|
|
||||||
def decoder(label: str) -> List[data.Tag]:
|
|
||||||
tag = tag_mapping.get(label)
|
|
||||||
return [tag] if tag else []
|
|
||||||
|
|
||||||
return decoder
|
|
||||||
|
|
||||||
|
|
||||||
def load_target_config(
|
|
||||||
path: Path, field: Optional[str] = None
|
|
||||||
) -> TargetConfig:
|
|
||||||
return load_config(path, schema=TargetConfig, field=field)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_SPECIES_LIST = [
|
|
||||||
"Barbastellus barbastellus",
|
|
||||||
"Eptesicus serotinus",
|
|
||||||
"Myotis alcathoe",
|
|
||||||
"Myotis bechsteinii",
|
|
||||||
"Myotis brandtii",
|
|
||||||
"Myotis daubentonii",
|
|
||||||
"Myotis mystacinus",
|
|
||||||
"Myotis nattereri",
|
|
||||||
"Nyctalus leisleri",
|
|
||||||
"Nyctalus noctula",
|
|
||||||
"Pipistrellus nathusii",
|
|
||||||
"Pipistrellus pipistrellus",
|
|
||||||
"Pipistrellus pygmaeus",
|
|
||||||
"Plecotus auritus",
|
|
||||||
"Plecotus austriacus",
|
|
||||||
"Rhinolophus ferrumequinum",
|
|
||||||
"Rhinolophus hipposideros",
|
|
||||||
]
|
|
@ -1,148 +1,141 @@
|
|||||||
# Step 4: Defining Target Classes for Training
|
# Step 4: Defining Target Classes and Decoding Rules
|
||||||
|
|
||||||
## Purpose and Context
|
## Purpose and Context
|
||||||
|
|
||||||
You've prepared your data by defining your annotation vocabulary (Step 1: Terms), removing irrelevant sounds (Step 2: Filtering), and potentially cleaning up or modifying tags (Step 3: Transforming Tags).
|
You've prepared your data by defining your annotation vocabulary (Step 1: Terms), removing irrelevant sounds (Step 2: Filtering), and potentially cleaning up or modifying tags (Step 3: Transforming Tags).
|
||||||
Now, it's time to tell `batdetect2` **exactly what categories (classes) your model should learn to identify**.
|
Now, it's time for a crucial step with two related goals:
|
||||||
|
|
||||||
This step involves defining rules that map the final tags on your sound event annotations to specific **class names** (like `pippip`, `myodau`, or `noise`).
|
1. Telling `batdetect2` **exactly what categories (classes) your model should learn to identify** by defining rules that map annotation tags to class names (like `pippip`, `myodau`, or `noise`).
|
||||||
These class names are the labels the machine learning model will be trained to predict.
|
This process is often called **encoding**.
|
||||||
Getting this definition right is essential for successful model training.
|
2. Defining how the model's predictions (those same class names) should be translated back into meaningful, structured **annotation tags** when you use the trained model.
|
||||||
|
This is often called **decoding**.
|
||||||
|
|
||||||
|
These definitions are essential for both training the model correctly and interpreting its output later.
|
||||||
|
|
||||||
## How it Works: Defining Classes with Rules
|
## How it Works: Defining Classes with Rules
|
||||||
|
|
||||||
You define your target classes in your main configuration file (e.g., your `.yaml` training config), typically under a section named `classes`.
|
You define your target classes and their corresponding decoding rules in your main configuration file (e.g., your `.yaml` training config), typically under a section named `classes`.
|
||||||
This section contains a **list** of class definitions.
|
This section contains:
|
||||||
Each item in the list defines one specific class your model should learn.
|
|
||||||
|
1. A **list** of specific class definitions.
|
||||||
|
2. A definition for the **generic class** tags.
|
||||||
|
|
||||||
|
Each item in the `classes` list defines one specific class your model should learn.
|
||||||
|
|
||||||
## Defining a Single Class
|
## Defining a Single Class
|
||||||
|
|
||||||
Each class definition rule requires a few key pieces of information:
|
Each specific class definition rule requires the following information:
|
||||||
|
|
||||||
1. `name`: **(Required)** This is the unique, simple name you want to give this class (e.g., `pipistrellus_pipistrellus`, `myotis_daubentonii`, `echolocation_noise`).
|
1. `name`: **(Required)** This is the unique, simple name for this class (e.g., `pipistrellus_pipistrellus`, `myotis_daubentonii`, `noise`).
|
||||||
This is the label the model will actually use.
|
This label is used during training and is what the model predicts.
|
||||||
Choose names that are clear and distinct.
|
Choose clear, distinct names.
|
||||||
**Each class name must be unique.**
|
**Each class name must be unique.**
|
||||||
2. `tags`: **(Required)** This is a list containing one or more specific tags that identify annotations belonging to this class.
|
2. `tags`: **(Required)** This list contains one or more specific tags (using `key` and `value`) used to identify if an _existing_ annotation belongs to this class during the _encoding_ phase (preparing training data).
|
||||||
Remember, each tag is specified using its term `key` (like `species` or `sound_type`, defaulting to `class` if omitted) and its specific `value` (like `Pipistrellus pipistrellus` or `Echolocation`).
|
3. `match_type`: **(Optional, defaults to `"all"`)** Determines how the `tags` list is evaluated during _encoding_:
|
||||||
3. `match_type`: **(Optional, defaults to `"all"`)** This tells the system how to use the list of tags you provided in the `tag` field:
|
- `"all"`: The annotation must have **ALL** listed tags to match.
|
||||||
- `"all"`: An annotation must have **ALL** of the tags listed in the `tags` section to be considered part of this class.
|
(Default).
|
||||||
(This is the default if you don't specify `match_type`).
|
- `"any"`: The annotation needs **AT LEAST ONE** listed tag to match.
|
||||||
- `"any"`: An annotation only needs to have **AT LEAST ONE** of the tags listed in the `tags` section to be considered part of this class.
|
4. `output_tags`: **(Optional)** This list specifies the tags that should be assigned to an annotation when the model _predicts_ this class `name`.
|
||||||
|
This is used during the _decoding_ phase (interpreting model output).
|
||||||
|
- **If you omit `output_tags` (or set it to `null`/~), the system will default to using the same tags listed in the `tags` field for decoding.** This is often what you want.
|
||||||
|
- Providing `output_tags` allows you to specify a different, potentially more canonical or detailed, set of tags to represent the class upon prediction.
|
||||||
|
For example, you could match based on simplified tags but output standardized tags.
|
||||||
|
|
||||||
**Example: Defining two specific bat species classes**
|
**Example: Defining Species Classes (Encoding & Default Decoding)**
|
||||||
|
|
||||||
|
Here, the `tags` used for matching during encoding will also be used for decoding, as `output_tags` is omitted.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# In your main configuration file
|
# In your main configuration file
|
||||||
classes:
|
classes:
|
||||||
# Definition for the first class
|
# Definition for the first class
|
||||||
- name: pippip # Simple name for Pipistrellus pipistrellus
|
- name: pippip # Simple name for Pipistrellus pipistrellus
|
||||||
tags:
|
tags: # Used for BOTH encoding match and decoding output
|
||||||
- key: species # Term key (could also default to 'class')
|
- key: species
|
||||||
value: Pipistrellus pipistrellus # Specific tag value
|
value: Pipistrellus pipistrellus
|
||||||
# match_type defaults to "all" (which is fine for a single tag)
|
# match_type defaults to "all"
|
||||||
|
# output_tags is omitted, defaults to using 'tags' above
|
||||||
|
|
||||||
# Definition for the second class
|
# Definition for the second class
|
||||||
- name: myodau # Simple name for Myotis daubentonii
|
- name: myodau # Simple name for Myotis daubentonii
|
||||||
tags:
|
tags: # Used for BOTH encoding match and decoding output
|
||||||
- key: species
|
- key: species
|
||||||
value: Myotis daubentonii
|
value: Myotis daubentonii
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example: Defining a class requiring multiple conditions (`match_type: "all"`)**
|
**Example: Defining a Class with Separate Encoding and Decoding Tags**
|
||||||
|
|
||||||
|
Here, we match based on _either_ of two tags (`match_type: any`), but when the model predicts `'pipistrelle'`, we decode it _only_ to the specific `Pipistrellus pipistrellus` tag plus a genus tag.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
classes:
|
classes:
|
||||||
- name: high_quality_pippip # Name for high-quality P. pip calls
|
- name: pipistrelle # Name for a Pipistrellus group
|
||||||
match_type: all # Annotation must match BOTH tags below
|
match_type: any # Match if EITHER tag below is present during encoding
|
||||||
tags:
|
|
||||||
- key: species
|
|
||||||
value: Pipistrellus pipistrellus
|
|
||||||
- key: quality # Assumes 'quality' term key exists
|
|
||||||
value: Good
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example: Defining a class matching multiple alternative tags (`match_type: "any"`)**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
classes:
|
|
||||||
- name: pipistrelle # Name for any Pipistrellus species in this list
|
|
||||||
match_type: any # Annotation must match AT LEAST ONE tag below
|
|
||||||
tags:
|
tags:
|
||||||
- key: species
|
- key: species
|
||||||
value: Pipistrellus pipistrellus
|
value: Pipistrellus pipistrellus
|
||||||
- key: species
|
- key: species
|
||||||
value: Pipistrellus pygmaeus
|
value: Pipistrellus pygmaeus # Match pygmaeus too
|
||||||
|
output_tags: # BUT, when decoding 'pipistrelle', assign THESE tags:
|
||||||
- key: species
|
- key: species
|
||||||
value: Pipistrellus nathusii
|
value: Pipistrellus pipistrellus # Canonical species
|
||||||
|
- key: genus # Assumes 'genus' key exists
|
||||||
|
value: Pipistrellus # Add genus tag
|
||||||
```
|
```
|
||||||
|
|
||||||
## Handling Overlap: Priority Order Matters
|
## Handling Overlap During Encoding: Priority Order Matters
|
||||||
|
|
||||||
Sometimes, an annotation might have tags that match the rules for _more than one_ class definition.
|
As before, when preparing training data (encoding), if an annotation matches the `tags` and `match_type` rules for multiple class definitions, the **order of the class definitions in the configuration list determines the priority**.
|
||||||
For example, an annotation tagged `species: Pipistrellus pipistrellus` would match both a specific `'pippip'` class rule and a broader `'pipistrelle'` genus rule (like the examples above) if both were defined.
|
|
||||||
|
|
||||||
How does `batdetect2` decide which class name to assign? It uses the **order of the class definitions in your configuration list**.
|
- The system checks rules from the **top** of the `classes` list down.
|
||||||
|
- The annotation gets assigned the `name` of the **first class rule it matches**.
|
||||||
|
- **Place more specific rules before more general rules.**
|
||||||
|
|
||||||
- The system checks an annotation against your class rules one by one, starting from the **top** of the `classes` list and moving down.
|
_(The YAML example for prioritizing Species over Noise remains the same as the previous version)_
|
||||||
- As soon as it finds a rule that the annotation matches, it assigns that rule's `name` to the annotation and **stops checking** further rules for that annotation.
|
|
||||||
- **The first match wins!**
|
|
||||||
|
|
||||||
Therefore, you should generally place your **most specific rules before more general rules** if you want the specific category to take precedence.
|
## Handling Non-Matches & Decoding the Generic Class
|
||||||
|
|
||||||
**Example: Prioritizing Species over Noise**
|
What happens if an annotation passes filtering/transformation but doesn't match any specific class rule during encoding?
|
||||||
|
|
||||||
|
- **Encoding:** As explained previously, these annotations are **not ignored**.
|
||||||
|
They are typically assigned to a generic "relevant sound" category, often called the **"Bat"** class in BatDetect2, intended for all relevant bat calls not specifically classified.
|
||||||
|
- **Decoding:** When the model predicts this generic "Bat" category (or when processing sounds that weren't assigned a specific class during encoding), we need a way to represent this generic status with tags.
|
||||||
|
This is defined by the `generic_class` list directly within the main `classes` configuration section.
|
||||||
|
|
||||||
|
**Defining the Generic Class Tags:**
|
||||||
|
|
||||||
|
You specify the tags for the generic class like this:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
classes:
|
# In your main configuration file
|
||||||
# --- Specific Species Rules (Checked First) ---
|
classes: # Main configuration section for classes
|
||||||
- name: pippip
|
# --- List of specific class definitions ---
|
||||||
tags:
|
classes:
|
||||||
- key: species
|
- name: pippip
|
||||||
value: Pipistrellus pipistrellus
|
tags:
|
||||||
|
- key: species
|
||||||
|
value: Pipistrellus pipistrellus
|
||||||
|
# ... other specific classes ...
|
||||||
|
|
||||||
- name: myodau
|
# --- Definition of the generic class tags ---
|
||||||
tags:
|
generic_class: # Define tags for the generic 'Bat' category
|
||||||
- key: species
|
- key: call_type
|
||||||
value: Myotis daubentonii
|
value: Echolocation
|
||||||
|
- key: order
|
||||||
# --- General Noise Rule (Checked Last) ---
|
value: Chiroptera
|
||||||
- name: noise # Catch-all for anything tagged as Noise
|
# These tags will be assigned when decoding the generic category
|
||||||
match_type: any # Match if any noise tag is present
|
|
||||||
tags:
|
|
||||||
- key: sound_type # Assume 'sound_type' term key exists
|
|
||||||
value: Noise
|
|
||||||
- key: quality # Assume 'quality' term key exists
|
|
||||||
value: Low # Maybe low quality is also considered noise for training
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, an annotation tagged with `species: Myotis daubentonii` _and_ `quality: Low` would be assigned the class name `myodau` because that rule comes first in the list.
|
This `generic_class` list provides the standard tags assigned when a sound is identified as relevant (passed filtering) but doesn't belong to one of the specific target classes you defined.
|
||||||
It would not be assigned `noise`, even though it also matches the second condition of the noise rule.
|
Like the specific classes, sensible defaults are often provided if you don't explicitly define `generic_class`.
|
||||||
|
|
||||||
Okay, that's a very important clarification about how BatDetect2 handles sounds that don't match specific class definitions.
|
**Crucially:** Remember, if sounds should be **completely excluded** from training (not even considered "generic"), use **Filtering rules (Step 2)**.
|
||||||
Let's refine that section to accurately reflect this behavior.
|
|
||||||
|
|
||||||
## What if No Class Matches?
|
### Outcome
|
||||||
|
|
||||||
It's important to understand what happens if a sound event annotation passes through the filtering (Step 2) and transformation (Step 3) steps, but its final set of tags doesn't match _any_ of the specific class definitions you've listed in this section.
|
By defining this list of prioritized class rules (including their `name`, matching `tags`, `match_type`, and optional `output_tags`) and the `generic_class` tags, you provide `batdetect2` with:
|
||||||
|
|
||||||
These annotations are **not ignored** during training.
|
1. A clear procedure to assign a target label (`name`) to each relevant annotation for training.
|
||||||
Instead, they are typically assigned to a **generic "relevant sound" class**.
|
2. A clear mapping to convert predicted class names (including the generic case) back into meaningful annotation tags.
|
||||||
Think of this as a category for sounds that you considered important enough to keep after filtering, but which don't fit into one of your specific target classes for detailed classification (like a particular species).
|
|
||||||
This generic class is distinct from background noise.
|
|
||||||
|
|
||||||
In BatDetect2, this default generic class is often referred to as the **"Bat"** class.
|
This complete definition prepares your data for the final heatmap generation (Step 5) and enables interpretation of the model's results.
|
||||||
The goal is generally that all relevant bat echolocation calls that pass the initial filtering should fall into _either_ one of your specific defined classes (like `pippip` or `myodau`) _or_ this generic "Bat" class.
|
|
||||||
|
|
||||||
**In summary:**
|
|
||||||
|
|
||||||
- Sounds passing **filtering** are considered relevant.
|
|
||||||
- If a relevant sound matches one of your **specific class rules** (in priority order), it gets that specific class label.
|
|
||||||
- If a relevant sound does **not** match any specific class rule, it gets the **generic "Bat" class** label.
|
|
||||||
|
|
||||||
**Crucially:** If you want certain types of sounds (even if they are bat calls) to be **completely excluded** from the training process altogether (not even included in the generic "Bat" class), you **must remove them using rules in the Filtering step (Step 2)**.
|
|
||||||
Any sound annotation that makes it past filtering _will_ be used in training, either under one of your specific classes or the generic one.
|
|
||||||
|
|
||||||
## Outcome
|
|
||||||
|
|
||||||
By defining this list of prioritized class rules, you provide `batdetect2` with a clear procedure to assign a specific target label (your class `name`) to each relevant sound event annotation based on its tags.
|
|
||||||
This labelled data is exactly what the model needs for training (Step 5).
|
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Iterable, List, Optional
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
from soundevent import data, terms
|
from soundevent import data, terms
|
||||||
|
|
||||||
from batdetect2.targets import (
|
from batdetect2.targets import call_type
|
||||||
TargetConfig,
|
|
||||||
build_target_encoder,
|
|
||||||
call_type,
|
|
||||||
get_class_names,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -201,23 +196,3 @@ def clip_annotation(
|
|||||||
non_relevant_sound_event,
|
non_relevant_sound_event,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def target_config() -> TargetConfig:
|
|
||||||
return TargetConfig()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def class_names(target_config: TargetConfig) -> List[str]:
|
|
||||||
return get_class_names(target_config.classes)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def encoder(
|
|
||||||
target_config: TargetConfig,
|
|
||||||
) -> Callable[[Iterable[data.Tag]], Optional[str]]:
|
|
||||||
return build_target_encoder(
|
|
||||||
classes=target_config.classes,
|
|
||||||
replacement_rules=target_config.replace,
|
|
||||||
)
|
|
||||||
|
@ -7,11 +7,14 @@ from pydantic import ValidationError
|
|||||||
from soundevent import data
|
from soundevent import data
|
||||||
|
|
||||||
from batdetect2.targets.classes import (
|
from batdetect2.targets.classes import (
|
||||||
|
DEFAULT_SPECIES_LIST,
|
||||||
ClassesConfig,
|
ClassesConfig,
|
||||||
TargetClass,
|
TargetClass,
|
||||||
build_encoder_from_config,
|
build_encoder_from_config,
|
||||||
get_class_names_from_config,
|
get_class_names_from_config,
|
||||||
is_target_class,
|
_get_default_class_name,
|
||||||
|
_get_default_classes,
|
||||||
|
_is_target_class,
|
||||||
load_classes_config,
|
load_classes_config,
|
||||||
load_encoder_from_config,
|
load_encoder_from_config,
|
||||||
)
|
)
|
||||||
@ -149,7 +152,7 @@ def test_is_target_class_match_all(
|
|||||||
),
|
),
|
||||||
data.Tag(term=sample_term_registry["quality"], value="Good"),
|
data.Tag(term=sample_term_registry["quality"], value="Good"),
|
||||||
}
|
}
|
||||||
assert is_target_class(sample_annotation, tags, match_all=True) is True
|
assert _is_target_class(sample_annotation, tags, match_all=True) is True
|
||||||
|
|
||||||
tags = {
|
tags = {
|
||||||
data.Tag(
|
data.Tag(
|
||||||
@ -157,14 +160,14 @@ def test_is_target_class_match_all(
|
|||||||
value="Pipistrellus pipistrellus",
|
value="Pipistrellus pipistrellus",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
assert is_target_class(sample_annotation, tags, match_all=True) is True
|
assert _is_target_class(sample_annotation, tags, match_all=True) is True
|
||||||
|
|
||||||
tags = {
|
tags = {
|
||||||
data.Tag(
|
data.Tag(
|
||||||
term=sample_term_registry["species"], value="Myotis daubentonii"
|
term=sample_term_registry["species"], value="Myotis daubentonii"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
assert is_target_class(sample_annotation, tags, match_all=True) is False
|
assert _is_target_class(sample_annotation, tags, match_all=True) is False
|
||||||
|
|
||||||
|
|
||||||
def test_is_target_class_match_any(
|
def test_is_target_class_match_any(
|
||||||
@ -178,7 +181,7 @@ def test_is_target_class_match_any(
|
|||||||
),
|
),
|
||||||
data.Tag(term=sample_term_registry["quality"], value="Good"),
|
data.Tag(term=sample_term_registry["quality"], value="Good"),
|
||||||
}
|
}
|
||||||
assert is_target_class(sample_annotation, tags, match_all=False) is True
|
assert _is_target_class(sample_annotation, tags, match_all=False) is True
|
||||||
|
|
||||||
tags = {
|
tags = {
|
||||||
data.Tag(
|
data.Tag(
|
||||||
@ -186,14 +189,14 @@ def test_is_target_class_match_any(
|
|||||||
value="Pipistrellus pipistrellus",
|
value="Pipistrellus pipistrellus",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
assert is_target_class(sample_annotation, tags, match_all=False) is True
|
assert _is_target_class(sample_annotation, tags, match_all=False) is True
|
||||||
|
|
||||||
tags = {
|
tags = {
|
||||||
data.Tag(
|
data.Tag(
|
||||||
term=sample_term_registry["species"], value="Myotis daubentonii"
|
term=sample_term_registry["species"], value="Myotis daubentonii"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
assert is_target_class(sample_annotation, tags, match_all=False) is False
|
assert _is_target_class(sample_annotation, tags, match_all=False) is False
|
||||||
|
|
||||||
|
|
||||||
def test_get_class_names_from_config():
|
def test_get_class_names_from_config():
|
||||||
@ -279,3 +282,17 @@ def test_load_encoder_from_config_invalid(
|
|||||||
temp_yaml_path,
|
temp_yaml_path,
|
||||||
term_registry=sample_term_registry,
|
term_registry=sample_term_registry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_default_class_name():
|
||||||
|
assert _get_default_class_name("Myotis daubentonii") == "myodau"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_default_classes():
|
||||||
|
default_classes = _get_default_classes()
|
||||||
|
assert len(default_classes) == len(DEFAULT_SPECIES_LIST)
|
||||||
|
first_class = default_classes[0]
|
||||||
|
assert isinstance(first_class, TargetClass)
|
||||||
|
assert first_class.name == _get_default_class_name(DEFAULT_SPECIES_LIST[0])
|
||||||
|
assert first_class.tags[0].key == "class"
|
||||||
|
assert first_class.tags[0].value == DEFAULT_SPECIES_LIST[0]
|
||||||
|
Loading…
Reference in New Issue
Block a user