Skip to content

Commit fb0d774

Browse files
authored
Merge pull request #694 from yzhao062/development
Release v3.6.0: Audio Anomaly Detection Modality
2 parents 476f0d1 + 5ccb802 commit fb0d774

21 files changed

Lines changed: 719 additions & 19 deletions

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

README.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ PyOD 3 is the most comprehensive Python library for anomaly detection. Four pill
6262
=========================== ========================================================================================
6363
Pillar What it means
6464
=========================== ========================================================================================
65-
Multi-Modal 60 detectors across **tabular, time series, graph, text, and image** data, one API
65+
Multi-Modal 61 detectors across **tabular, time series, graph, text, image, and audio** data, one API
6666
Full Lifecycle From raw data to explained anomalies and next-step guidance in a single call
6767
Agentic ``od-expert`` turns natural-language requests into ADEngine workflows; MCP exposes structured tools for other agents
6868
Most Used 38+ million downloads; benchmark-backed routing (ADBench, TSB-AD, BOND, NLP-ADBench)
@@ -253,7 +253,7 @@ Additional Topics
253253
Implemented Algorithms
254254
^^^^^^^^^^^^^^^^^^^^^^
255255

256-
PyOD is organized into two functional groups: **(i) Detection Algorithms**, with dedicated subsections for tabular, time series, and graph data (EmbeddingOD inside the tabular table adds multi-modal support for text and image via foundation model encoders); and **(ii) Utility Functions** for data generation, evaluation, and lifecycle orchestration.
256+
PyOD is organized into two functional groups: **(i) Detection Algorithms**, with dedicated subsections for tabular, time series, and graph data (EmbeddingOD inside the tabular table adds multi-modal support for text, image, and audio via foundation model or handcrafted encoders); and **(ii) Utility Functions** for data generation, evaluation, and lifecycle orchestration.
257257

258258
**(i-a) Tabular & Multi-Modal Detection Algorithms** :
259259

@@ -508,7 +508,7 @@ PyOD is organized into two functional groups: **(i) Detection Algorithms**, with
508508
- [#Goodge2022Lunar]_
509509
* - Embedding-based
510510
- EmbeddingOD
511-
- Multi-modal anomaly detection via foundation model embeddings, text and image (`example <https://github.com/yzhao062/pyod/blob/development/examples/embedding_od_example.py>`__)
511+
- Multi-modal anomaly detection via foundation model embeddings, text, image, and audio (`example <https://github.com/yzhao062/pyod/blob/development/examples/embedding_od_example.py>`__)
512512
- 2025
513513
- [#Li2024NLPADBench]_
514514

docs/index.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ PyOD 3 is the most comprehensive Python library for anomaly detection. Four pill
7171
=========================== ========================================================================================
7272
Pillar What it means
7373
=========================== ========================================================================================
74-
Multi-Modal 60 detectors across **tabular, time series, graph, text, and image** data, one API
74+
Multi-Modal 61 detectors across **tabular, time series, graph, text, image, and audio** data, one API
7575
Full Lifecycle From raw data to explained anomalies and next-step guidance in a single call
7676
Agentic Ask in plain English, and AI agents run the full detection workflow without OD expertise
7777
Most Used `38+ million downloads <https://pepy.tech/project/pyod>`_; benchmark-backed routing (ADBench, TSB-AD, BOND, NLP-ADBench)
@@ -208,7 +208,7 @@ Benchmarks
208208
Implemented Algorithms
209209
======================
210210

211-
PyOD is organized into two functional groups: **(i) Detection Algorithms**, with dedicated subsections for tabular, time series, and graph data (EmbeddingOD inside the tabular table adds multi-modal support for text and image via foundation model encoders); and **(ii) Utility Functions** for data generation, evaluation, and lifecycle orchestration.
211+
PyOD is organized into two functional groups: **(i) Detection Algorithms**, with dedicated subsections for tabular, time series, and graph data (EmbeddingOD inside the tabular table adds multi-modal support for text, image, and audio via foundation model or handcrafted encoders); and **(ii) Utility Functions** for data generation, evaluation, and lifecycle orchestration.
212212

213213
**(i-a) Tabular & Multi-Modal Detection Algorithms** :
214214

@@ -263,7 +263,7 @@ Neural Networks DevNet Deep Anomaly Detection with Deviation Net
263263
Neural Networks AE1SVM Autoencoder-based One-class Support Vector Machine 2019 :class:`pyod.models.ae1svm.AE1SVM` :cite:`a-nguyen2019scalable`
264264
Graph-based R-Graph Outlier detection by R-graph 2017 :class:`pyod.models.rgraph.RGraph` :cite:`a-you2017provable`
265265
Graph-based LUNAR LUNAR: Unifying Local Outlier Detection Methods via Graph Neural Networks 2022 :class:`pyod.models.lunar.LUNAR` :cite:`a-goodge2022lunar`
266-
Embedding-based EmbeddingOD Multi-modal anomaly detection via foundation model embeddings (text, image) 2025 :class:`pyod.models.embedding.EmbeddingOD` :cite:`a-li2024nlp`
266+
Embedding-based EmbeddingOD Multi-modal anomaly detection via foundation model embeddings (text, image, audio) 2025 :class:`pyod.models.embedding.EmbeddingOD` :cite:`a-li2024nlp`
267267
=================== ================ ====================================================================================================== ===== =================================================== ======================================================
268268

269269

docs/install.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Example output:
114114
.. code-block:: text
115115
116116
PyOD version: 3.1.0
117-
Detectors (ADEngine): 60 total (43 tabular, 7 time-series, 8 graph, 2 text, 2 image, 1 multimodal)
117+
Detectors (ADEngine): 61 total (43 tabular, 7 time-series, 8 graph, 2 text, 2 image, 1 multimodal, 3 audio)
118118
Classic API: OK
119119
ADEngine (Layer 2): OK
120120
MCP extra: OK (run: pyod mcp serve)

examples/agentic_demo.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ <h1>Any AI Agent Becomes an <em>OD Expert</em></h1>
731731
</div>
732732

733733
<div class="footer">
734-
<strong>PyOD V3.2.0</strong> &middot; 60 detectors &middot; 5 modalities &middot; od-expert skill (1000 lines: SKILL.md + 6 references)<br>
734+
<strong>PyOD V3.2.0</strong> &middot; 61 detectors &middot; 6 modalities &middot; od-expert skill (1000 lines: SKILL.md + 6 references)<br>
735735
<a href="https://github.com/yzhao062/pyod">github.com/yzhao062/pyod</a> &middot;
736736
<a href="https://pyod.readthedocs.io">pyod.readthedocs.io</a> &middot;
737737
<code style="font-size: 12px;">pip install pyod &amp;&amp; pyod install skill</code>

examples/agentic_example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
"""Example: Agent-driven anomaly detection with PyOD 3 (Layer 3).
33
44
Demonstrates what makes PyOD distinctive in the agentic workflow:
5-
- 60 detectors across 5 data modalities
5+
- 61 detectors across 6 data modalities
66
- Benchmark-backed detector selection (ADBench, TSB-AD, BOND)
77
- Multi-detector consensus with per-detector scores
88
- Result quality assessment
9-
- Multi-modal: same API for tabular, time series, graph, text, image
9+
- Multi-modal: same API for tabular, time series, graph, text, image, audio
1010
1111
Dataset: UCI Cardiotocography (1,831 recordings, 21 clinical
1212
features). Shipped with PyOD at examples/data/cardio.csv.

pyod/models/audio_ae.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# -*- coding: utf-8 -*-
2+
"""AudioAE: a log-mel reconstruction autoencoder for audio anomaly detection.
3+
4+
Each clip is turned into overlapping log-mel context windows; a dense
5+
autoencoder is fit on the windows of the (mostly normal) training clips,
6+
and each clip is scored by its mean per-window reconstruction error. This
7+
is the DCASE-style audio anomaly detection baseline, expressed through
8+
PyOD's ``AutoEncoder`` so the training loop and preprocessing are shared
9+
with the rest of the library.
10+
"""
11+
# Author: Yue Zhao <yzhao062@gmail.com>
12+
# License: BSD 2 clause
13+
14+
import numpy as np
15+
from sklearn.utils.validation import check_is_fitted
16+
17+
from .base import BaseDetector
18+
from ..utils.encoders.audio import _to_mono_waveform
19+
20+
_DEFAULT_SR = 22050
21+
22+
23+
def _logmel_windows(y, sr, n_mels, context, hop_length):
24+
"""Return overlapping log-mel context windows for one waveform.
25+
26+
Output shape is ``(n_windows, n_mels * context)``. Clips shorter than
27+
one context window are padded so at least one window is produced.
28+
"""
29+
import librosa
30+
31+
spec = librosa.power_to_db(
32+
librosa.feature.melspectrogram(
33+
y=y, sr=sr, n_mels=n_mels, hop_length=hop_length))
34+
n_frames = spec.shape[1]
35+
if n_frames < context:
36+
pad = np.zeros((n_mels, context - n_frames), dtype=spec.dtype)
37+
spec = np.concatenate([spec, pad], axis=1)
38+
n_frames = context
39+
windows = [spec[:, t:t + context].T.reshape(-1)
40+
for t in range(n_frames - context + 1)]
41+
return np.stack(windows).astype(np.float32)
42+
43+
44+
class AudioAE(BaseDetector):
45+
"""Log-mel reconstruction autoencoder for audio anomaly detection.
46+
47+
The detector extracts overlapping log-mel context windows from each
48+
clip, fits a dense autoencoder (PyOD's :class:`AutoEncoder`) on the
49+
windows of the training clips, and scores each clip by its mean
50+
per-window reconstruction error. Training assumes the input is mostly
51+
normal, the usual unsupervised setting.
52+
53+
Requires ``torch`` (for the autoencoder) and ``pyod[audio]``
54+
(``librosa``, ``soundfile``).
55+
56+
Parameters
57+
----------
58+
n_mels : int, optional (default=64)
59+
Number of mel bands in the spectrogram.
60+
61+
context : int, optional (default=5)
62+
Number of consecutive frames stacked into one autoencoder input
63+
window. The window dimensionality is ``n_mels * context``.
64+
65+
hop_length : int, optional (default=512)
66+
STFT hop length in samples.
67+
68+
sr : int, optional (default=22050)
69+
Target sample rate. File inputs are loaded at this rate;
70+
``(waveform, sample_rate)`` tuples are resampled to it.
71+
72+
contamination : float, optional (default=0.1)
73+
Expected proportion of outliers, used for the clip-level
74+
threshold and labels.
75+
76+
epoch_num : int, optional (default=40)
77+
Autoencoder training epochs.
78+
79+
batch_size : int, optional (default=1024)
80+
Autoencoder mini-batch size (over frames, not clips).
81+
82+
lr : float, optional (default=1e-3)
83+
Learning rate.
84+
85+
hidden_neuron_list : list of int or None, optional (default=None)
86+
Encoder hidden sizes. ``None`` uses ``[128, 32, 8]``, which gives
87+
the DCASE-style 320-128-32-8 contraction for the default
88+
320-dimensional window (``n_mels=64``, ``context=5``).
89+
90+
device : str or None, optional (default=None)
91+
Torch device. ``None`` auto-selects.
92+
93+
random_state : int, optional (default=42)
94+
Seed forwarded to the autoencoder.
95+
96+
verbose : int, optional (default=0)
97+
Autoencoder verbosity.
98+
99+
Attributes
100+
----------
101+
decision_scores_ : numpy array of shape (n_clips,)
102+
Clip-level outlier scores of the training data.
103+
104+
threshold_ : float
105+
Score threshold based on ``contamination``.
106+
107+
labels_ : numpy array of shape (n_clips,)
108+
Binary labels of training clips (0: inlier, 1: outlier).
109+
110+
ae_ : AutoEncoder
111+
The fitted frame-level autoencoder.
112+
113+
Examples
114+
--------
115+
>>> import numpy as np
116+
>>> from pyod.models.audio_ae import AudioAE
117+
>>> clips = [np.random.RandomState(s).randn(22050) for s in range(20)]
118+
>>> clf = AudioAE(epoch_num=5)
119+
>>> clf.fit(clips) # doctest: +SKIP
120+
>>> scores = clf.decision_function(clips) # doctest: +SKIP
121+
"""
122+
123+
def __init__(self, n_mels=64, context=5, hop_length=512, sr=_DEFAULT_SR,
124+
contamination=0.1, epoch_num=40, batch_size=1024, lr=1e-3,
125+
hidden_neuron_list=None, device=None, random_state=42,
126+
verbose=0):
127+
super(AudioAE, self).__init__(contamination=contamination)
128+
self.n_mels = n_mels
129+
self.context = context
130+
self.hop_length = hop_length
131+
self.sr = sr
132+
self.epoch_num = epoch_num
133+
self.batch_size = batch_size
134+
self.lr = lr
135+
self.hidden_neuron_list = hidden_neuron_list
136+
self.device = device
137+
self.random_state = random_state
138+
self.verbose = verbose
139+
140+
def _extract(self, X):
141+
"""Return (frames, clip_idx) over all clips in X."""
142+
try:
143+
import librosa # noqa: F401
144+
import soundfile # noqa: F401
145+
except ImportError:
146+
raise ImportError(
147+
"AudioAE requires 'librosa' and 'soundfile'. "
148+
"Install with: pip install pyod[audio]")
149+
if len(X) == 0:
150+
raise ValueError("AudioAE received an empty input.")
151+
frames_list, clip_idx = [], []
152+
for i, item in enumerate(X):
153+
y = _to_mono_waveform(item, self.sr)
154+
windows = _logmel_windows(y, self.sr, self.n_mels,
155+
self.context, self.hop_length)
156+
frames_list.append(windows)
157+
clip_idx.append(np.full(len(windows), i, dtype=np.int64))
158+
return np.concatenate(frames_list, axis=0), np.concatenate(clip_idx)
159+
160+
@staticmethod
161+
def _aggregate(frame_scores, clip_idx, n_clips):
162+
"""Mean per-frame score within each clip."""
163+
out = np.zeros(n_clips, dtype=np.float64)
164+
for i in range(n_clips):
165+
mask = clip_idx == i
166+
if mask.any():
167+
out[i] = float(frame_scores[mask].mean())
168+
return out
169+
170+
def fit(self, X, y=None):
171+
"""Fit the frame autoencoder and score the training clips.
172+
173+
Parameters
174+
----------
175+
X : list
176+
Audio clips as file paths, waveform arrays, or
177+
``(waveform, sample_rate)`` tuples.
178+
179+
y : Ignored
180+
Not used, present for API consistency.
181+
182+
Returns
183+
-------
184+
self : object
185+
"""
186+
try:
187+
import torch # noqa: F401
188+
except ImportError:
189+
raise ImportError(
190+
"AudioAE requires torch (for the autoencoder) and "
191+
"pyod[audio] (librosa, soundfile). Install with: "
192+
"pip install pyod[torch,audio]")
193+
from .auto_encoder import AutoEncoder
194+
195+
frames, clip_idx = self._extract(X)
196+
dim = frames.shape[1]
197+
hidden = self.hidden_neuron_list or [128, 32, 8]
198+
# Drop hidden layers that are not smaller than the input so the
199+
# autoencoder stays a contraction for unusually small windows.
200+
hidden = [h for h in hidden if h < dim] or [max(dim // 2, 2)]
201+
202+
# Cap the batch size to the frame count. PyOD's AutoEncoder drops
203+
# the last incomplete batch, so a batch larger than the dataset
204+
# would drop every frame and leave the training loop with nothing.
205+
batch_size = max(1, min(self.batch_size, frames.shape[0]))
206+
207+
self.ae_ = AutoEncoder(
208+
contamination=self.contamination, epoch_num=self.epoch_num,
209+
batch_size=batch_size, lr=self.lr,
210+
hidden_neuron_list=hidden, device=self.device,
211+
random_state=self.random_state, verbose=self.verbose)
212+
self.ae_.fit(frames)
213+
214+
frame_scores = self.ae_.decision_function(frames)
215+
self._set_n_classes(y)
216+
self.decision_scores_ = self._aggregate(frame_scores, clip_idx, len(X))
217+
self._process_decision_scores()
218+
return self
219+
220+
def decision_function(self, X):
221+
"""Predict clip-level anomaly scores for X.
222+
223+
Parameters
224+
----------
225+
X : list
226+
Audio clips in the same formats accepted by ``fit``.
227+
228+
Returns
229+
-------
230+
anomaly_scores : numpy array of shape (n_clips,)
231+
"""
232+
check_is_fitted(self, ['decision_scores_', 'threshold_', 'labels_'])
233+
frames, clip_idx = self._extract(X)
234+
frame_scores = self.ae_.decision_function(frames)
235+
return self._aggregate(frame_scores, clip_idx, len(X))

pyod/models/embedding.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,55 @@ def for_image(cls, quality='balanced', **kwargs):
431431
config = {**presets[quality], **kwargs}
432432
return cls(**config)
433433

434+
@classmethod
435+
def for_audio(cls, quality='balanced', **kwargs):
436+
"""Create an EmbeddingOD configured for audio anomaly detection.
437+
438+
Uses a handcrafted 74-dim acoustic feature encoder (20 MFCC,
439+
12 chroma, and 5 spectral descriptors, each as mean and standard
440+
deviation over frames) followed by a classical PyOD detector. This
441+
embed-then-detect pattern with classical detectors is competitive
442+
on standard audio anomaly detection benchmarks and needs no GPU.
443+
Requires ``pyod[audio]`` (librosa, soundfile).
444+
445+
Input clips may be file paths, waveform arrays, or
446+
``(waveform, sample_rate)`` tuples.
447+
448+
Parameters
449+
----------
450+
quality : str, optional (default='balanced')
451+
- 'fast': handcrafted features + IForest.
452+
- 'balanced': handcrafted features + KNN.
453+
- 'best': handcrafted features + LUNAR (requires torch).
454+
455+
**kwargs
456+
Override any EmbeddingOD parameter.
457+
458+
Returns
459+
-------
460+
clf : EmbeddingOD
461+
"""
462+
presets = {
463+
'fast': {
464+
'encoder': 'audio-mfcc',
465+
'detector': 'IForest',
466+
},
467+
'balanced': {
468+
'encoder': 'audio-mfcc',
469+
'detector': 'KNN',
470+
},
471+
'best': {
472+
'encoder': 'audio-mfcc',
473+
'detector': 'LUNAR',
474+
},
475+
}
476+
if quality not in presets:
477+
raise ValueError(
478+
"quality must be 'fast', 'balanced', or 'best', "
479+
"got '%s'" % quality)
480+
config = {**presets[quality], **kwargs}
481+
return cls(**config)
482+
434483

435484
class MultiModalOD(BaseDetector):
436485
"""Multi-modal anomaly detection via score fusion.

pyod/skills/od_expert/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Fire this skill when:
1919

2020
## What you have access to
2121

22-
PyOD ships <!-- KB-snapshot count -->60<!-- /KB-snapshot --> detectors across five modalities (43 tabular, 7 time series, 8 graph, 2 text, 2 image, 1 multimodal). Use the `ADEngine` session API to drive the full workflow:
22+
PyOD ships <!-- KB-snapshot count -->61<!-- /KB-snapshot --> detectors across six modalities (43 tabular, 7 time series, 8 graph, 2 text, 2 image, 1 multimodal, 3 audio). Use the `ADEngine` session API to drive the full workflow:
2323

2424
```python
2525
from pyod.utils.ad_engine import ADEngine

pyod/skills/od_expert/references/tabular.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Tabular anomaly detection reference
22

3-
PyOD's largest modality (43 of 60 buildable detectors). The agent loads this file when the master decision tree (in SKILL.md) routes to tabular.
3+
PyOD's largest modality (43 of 61 buildable detectors). The agent loads this file when the master decision tree (in SKILL.md) routes to tabular.
44

55
## Decision table by data shape (expert heuristics)
66

0 commit comments

Comments
 (0)