- Зачем: - быстрым моделям эмбеддингов нужен автоматический подсчет без повторного инференса. - Что: - проверены возможности повторного использования эмбеддингов в публичном API sherpa-onnx. - сопоставлены порог, eigengap, NME-SC и двухшаговая кластеризация. - зафиксирована граница NumPy-прототипа без production-зависимости от NeMo. - Проверка: - git diff --cached --check.
24 KiB
Оценка числа говорящих для быстрых эмбеддингов
Дата: 2026-08-30
Статус: исследовательская записка. Production-код и конфигурация не менялись.
Вопрос
Как отделить оценку числа говорящих от порога FastClustering, чтобы
использовать быстрые CAMPPlus или TitaNet и не запускать сегментацию и
извлечение эмбеддингов второй раз?
Краткий вывод
Нужна двухшаговая схема: один раз получить ровно те эмбеддинги, которые
подготовил конвейер sherpa-onnx, отдельно оценить по ним число голосовых
кластеров N, затем передать N штатному FastClustering. Для первого
прототипа рекомендован только счетчик NME, без полной spectral clustering:
ядро алгоритма из статьи реализовать на уже доступном NumPy, а итоговые метки
по-прежнему получить через FastClustering(num_clusters=N). Простой eigengap с
фиксированным прореживанием нужен как дешевый контрольный вариант.
Публичного промежуточного интерфейса для этого в sherpa-onnx 1.13.5 нет.
SpeakerEmbeddingExtractor и FastClustering публичны, но
OfflineSpeakerDiarization.process() возвращает только готовые интервалы.
Матрица эмбеддингов и соответствие строк локальным говорящим остаются внутри
реализации. После set_config() повторный process() снова выполнит модельный
инференс. Python-адаптер проекта сам по себе не может дать дешевую повторную
кластеризацию; будущему прототипу нужен узкий исследовательский интерфейс на
уровне sherpa-onnx.
Что подтверждено про sherpa-onnx
FastClustering уже умеет второй шаг
В версии 1.13.5 алгоритм нормирует строки, строит все попарные cosine dissimilarity, выполняет complete-linkage AHC и затем выбирает один из двух способов разрезать одно и то же дерево:
- при
num_clusters > 0- ровно на заданное число кластеров; - иначе - по высоте
threshold.
Это видно непосредственно в
fast-clustering.cc
и закреплено в комментарии к
FastClusteringConfig:
при известном числе кластеров порог игнорируется. Python binding публично
принимает C-contiguous матрицу float32 размера M x D и возвращает метку для
каждой строки
(исходник binding).
Значит, передавать оцененное N в существующий кластеризатор не требует новой
реализации распределения эмбеддингов по кластерам.
Порог FastClustering не принадлежит модели эмбеддингов. Это высота
complete-linkage дерева в шкале cosine dissimilarity. Официальный пример
CAMPPlus для speaker verification лишь вычисляет cosine score и не задает
универсальной границы решения
(infer_sv.py).
Официальный diarization-рецепт 3D-Speaker отдельно задает pval=0.012 для
spectral clustering и mer_cos=0.8 для последующего слияния центров
(infer_diarization.py).
Эти величины имеют другой смысл и не могут быть перенесены в sherpa как
порог модели.
Публичных деталей недостаточно для повтора всего конвейера
Публичный SpeakerEmbeddingExtractor позволяет создать stream, подать в него
аудио и получить один embedding
(Python binding,
C/C++ API).
Это пригодно для самостоятельно выбранного фрагмента, но не выдает материал,
который выбрал diarization-конвейер.
Внутри OfflineSpeakerDiarization после сегментации:
- из перекрывающихся окон строятся пары
окно x локальный говорящий; - речь с одновременными голосами исключается;
- для каждой пары собирается свой stream и вычисляется embedding;
- вся матрица передается в
FastClustering; - метки строк переводятся обратно в разметку говорящих.
Последовательность и временная матрица видны в
OfflineSpeakerDiarizationPyannoteImpl::Process,
а сборка строк - в
GetChunkSpeakerSampleIndexes и ComputeEmbeddings.
Это private-методы реализации.
Python-класс OfflineSpeakerDiarization экспортирует только sample_rate,
set_config и process; process возвращает готовый результат, а не признаки
(binding 1.13.5).
Внутренний SetConfig заменяет только объект кластеризации, тогда как каждый
Process заново вызывает segmentation и ComputeEmbeddings
(реализация).
Актуальная публичная C API также документирует только создание, настройку и
получение итоговых сегментов
(официальная страница).
Итог: публичный API позволяет кластеризовать имеющуюся матрицу повторно. Извлечь или переиграть промежуточную матрицу штатного diarization-конвейера через него нельзя.
Варианты оценки N
1. Отдельный порог для каждой embedding-модели
Это текущий FastClustering в автоматическом режиме. Он дешев, не добавляет
зависимостей и на одном проходе сразу оценивает число кластеров и назначает
метки. На порог влияют архитектура модели эмбеддингов, длительность и состав
выбранных фрагментов, акустика и распределение голосов. Локальная калибровка
уже не нашла общего рабочего порога для CAMPPlus,
а TitaNet не прошел автоматическую сетку
(исходное исследование,
повторный CPU-тест).
Вводить еще один официальный порог модели оснований нет: upstream-рецепты
публикуют параметры конкретного clustering pipeline, а не переносимую границу
для complete-linkage sherpa. Вариант остается контрольным, но не решает тикет.
2. Spectral clustering с простым eigengap
Официальный CAMPPlus-конвейер 3D-Speaker строит cosine affinity, оставляет в
каждой строке фиксированную долю ближайших соседей, симметризует матрицу,
вычисляет ненормированный Laplacian и несколько наименьших собственных пар.
Число говорящих - позиция максимального eigengap в заданных пределах, после
чего собственные векторы кластеризуются k-means
(SpectralCluster).
Для нашей двухшаговой схемы нужны только собственные значения и оценка N;
k-means можно не выполнять. Это одна spectral decomposition и потому хороший
минимальный контрольный вариант. Однако фиксированный pval остается новым
порогом.
Официальная реализация задает его для своего способа нарезки; пары
окно x локальный говорящий sherpa устроены иначе. Вариант отделяет N от
порога AHC, но сохраняет калибруемый параметр.
Если брать реализацию 3D-Speaker как зависимость, она импортирует NumPy, SciPy, scikit-learn, fastcluster, UMAP и HDBSCAN, причем последние два обязательны уже при импорте модуля (исходник, requirements). Для production это несоразмерно счетчику.
3. NME-SC
NME-SC создан именно для совместной оценки числа кластеров и параметра
прореживания без dev-set. Для каждого числа соседей p алгоритм бинаризует
cosine affinity, симметризует ее, строит ненормированный Laplacian и считает
eigengap. Нормированный gap равен максимальному gap, деленному на наибольшее
собственное значение; выбирается p, минимизирующий отношение p / gap, а N
задает позиция максимального gap
(статья авторов, раздел III).
Авторы ограничивали поиск p диапазоном до четверти числа фрагментов и число
говорящих - восемью; результаты статьи получены на x-vector и англоязычных
корпусах, поэтому перенос на наши эмбеддинги еще предстоит проверить.
Поддерживаемая реализация NeMo принимает готовые эмбеддинги, строит cosine
affinity и отдельно получает est_num_of_spk; известное число говорящих затем
подменяет эту оценку перед spectral clustering
(offline_clustering.py).
Для ускорения NME она по умолчанию ограничивает матрицу анализа 512 строками,
перебирает разреженную сетку из 30 значений p и предупреждает, что меньше 20
может ухудшить оценку
(класс NMESC).
Для коротких записей NeMo добавляет случайные anchor embeddings и повторяет
оценку. Это отдельное расширение за пределами минимального ядра статьи
(исходник).
Полная NME-SC после оценки N еще раз раскладывает Laplacian, строит spectral
embeddings и запускает k-means. Для этого тикета такая замена итогового
кластеризатора не нужна: локальные тесты уже показывают полезный путь
FastClustering с известным числом говорящих. Нужна только функция
embeddings -> N.
4. Оценить N, затем передать известное число кластеров
Это не отдельный счетчик. Это рекомендуемая граница ответственности:
segmentation -> embeddings +-> count-only NME -> N
+-> FastClustering(features, num_clusters=N)
-> штатная финализация sherpa
Плюсы:
- порог complete-linkage больше не определяет число голосовых кластеров;
- CAMPPlus и TitaNet используют один модельный проход;
- итоговое назначение меток остается в уже принятом FastClustering;
- на одной матрице можно независимо сравнить несколько счетчиков.
Минус один, но архитектурный: без нового промежуточного интерфейса текущий
process() не позволяет вставить счетчик между вычислением матрицы и
кластеризацией.
Зависимости, лицензии и цена
Штатная установка NeMo требует среди прочего NumPy, scikit-learn и PyTorch
(pyproject.toml).
| Вариант | Новые runtime-зависимости | Дополнительная работа после эмбеддингов | Ограничение |
|---|---|---|---|
Порог FastClustering |
нет | уже существующие попарные расстояния и AHC | порог калибруется для модели и материала |
| Простой eigengap | SciPy в официальной реализации; либо только уже доступный NumPy | affinity M x M, одно собственное разложение |
фиксированный параметр прореживания |
| Полная NME-SC NeMo | nemo-toolkit, PyTorch, scikit-learn и другие зависимости NeMo |
до нескольких десятков eigenvalue-разложений, затем еще spectral embedding и k-means | самый тяжелый и меняет итоговый кластеризатор |
| Счетчик NME + FastClustering | только NumPy при чистой реализации ядра статьи | NME на ограниченной матрице, затем штатный AHC по известному N |
нужен промежуточный интерфейс и локальная проверка переноса |
FastClustering уже хранит M(M-1)/2 расстояний и строит AHC. Простой
eigengap добавляет еще одну плотную affinity-матрицу. NME повторяет eigenvalue-
анализ для набора p; ограничение размера и разреженный поиск контролируют
цену, но их влияние на качество нужно измерить. Полная spectral clustering
также хранит собственные векторы и запускает k-means, поэтому для одной оценки
N избыточна. В проектных замерах основной расход по-прежнему приходится на
извлечение эмбеддингов, а не на clustering
(профиль стоимости,
CPU-тест); это не
заменяет отдельного замера NME на целевой матрице.
sherpa-onnx, 3D-Speaker и NeMo Speech распространяют код под Apache-2.0 (sherpa-onnx, 3D-Speaker, NeMo Speech). Авторская реализация NME-SC опубликована под MIT и сама направляет пользователей к поддерживаемой версии NeMo (репозиторий авторов). Файл NeMo с clustering дополнительно сохраняет BSD-уведомление scikit-learn. NumPy имеет BSD-3-Clause (лицензия 2.4.3) и уже присутствует в lock-файле проекта. Лицензионного запрета на прототип нет; прямое заимствование кода потребует сохранить его уведомления. Новые модели для счетчика не нужны.
Ограничения вывода
- NME-SC проверялась авторами на другой нарезке и x-vector. Строки sherpa - это локальные говорящие перекрывающихся Pyannote-окон после исключения overlap; переносимость нельзя считать доказанной.
- Правильное
Nне гарантирует качественную разметку говорящих. Для TitaNet в режиме известного числа кластеров уже наблюдался малый кластер, поэтому нужны прежние proxy-метрики и слуховая проверка. - У sherpa есть отдельный путь для единственного окна, который обходится без общей матрицы эмбеддингов. Прототип не должен ломать этот special case.
- Python binding
FastClusteringнормирует переданную матрицу на месте; сравниваемым вариантам нужно выдавать отдельные C-contiguous копии. - Цена NME на числе строк, которое реально создает sherpa, здесь не измерялась.
Точная граница будущего прототипа
Production-код, CLI, модели по умолчанию и ADR не менять. Сделать отдельный исследовательский стенд на закрепленном sherpa-onnx 1.13.5:
- Узко разделить внутренний
Processна подготовку и финализацию либо добавить исследовательский callback послеComputeEmbeddings. За один проход сохранить в памяти матрицуfloat32, список валидных парокно x локальный говорящийи локальные frame labels; аудио и текст не сериализовать. - На одной и той же матрице сравнить три счетчика:
текущий порог как контроль, простой eigengap с фиксированным прореживанием и
count-only NME. Ограничить
Nдиапазоном1..8. - Минимальный NME реализовать по статье на NumPy: cosine affinity,
top-
pbinarization, симметризация, ненормированный Laplacian,numpy.linalg.eigvalsh, выборpиNпо NME. Для первого прогона взять детерминированное ограничение 512 строк и sparse search из 20 значений; anchor embeddings, Torch, spectral embeddings и k-means не включать. - Для каждого оцененного
Nвызвать публичныйFastClusteringна копии той же матрицы сnum_clusters=N, затем выполнить существующую финализацию без повторной сегментации или извлечения эмбеддингов. - На тех же трех контрольных фрагментах измерить: оцененное
N, общее и содержательное число кластеров, mapped speaker purity, слова без говорящего, время счетчика/кластеризации и peak RSS. Прогон с CAMPPlus и TitaNet должен завершиться слуховой проверкой. - Если простой eigengap и NME одинаково проходят проверку, выбрать более дешевый eigengap. Если проходит только NME, отдельно решить, переносить ли чистую NumPy-реализацию в production. Если оба ошибаются, быстрые embedding- модели оставить только для явно заданного числа говорящих.
Этот прототип отвечает ровно на два неизвестных: можно ли стабильно оценить
N по внутренним эмбеддингам CAMPPlus/TitaNet и укладывается ли count-only NME
в приемлемую CPU-цену. Он не выбирает новую модель по умолчанию, не заменяет
FastClustering и не расширяет продуктовый контракт.