- Зачем: - исследовательское решение должно однозначно задавать повторное использование эмбеддингов и проверку счетчиков. - Что: - выбран внутренний интерфейс подготовки и финализации sherpa-onnx. - закреплены входы, вариант NME и критерии качества. - измерения CPU и памяти отделены от будущих гейтов карты. - Проверка: - `uv run pytest -q` (334 passed, 2 skipped). - `git diff --check`. - 31 внешняя ссылка отвечает HTTP 200.
35 KiB
Оценка числа говорящих для быстрых эмбеддингов
Дата: 2026-08-30
Статус: исследовательская записка. Production-код и конфигурация не менялись.
Вопрос
Как отделить оценку числа говорящих от порога FastClustering, чтобы
использовать быстрые CAMPPlus или TitaNet и не запускать сегментацию и
извлечение эмбеддингов второй раз?
Краткий вывод
Нужна двухшаговая схема: один раз получить ровно те эмбеддинги, которые
подготовил конвейер sherpa-onnx, отдельно оценить по ним число голосовых
кластеров N, затем передать N штатному FastClustering. Для первого
прототипа рекомендован счетчик NME, без полной спектральной кластеризации:
ядро алгоритма из статьи реализовать на NumPy, а итоговые метки по-прежнему
получить через FastClustering(num_clusters=N).
Счетчик eigengap с фиксированным прореживанием нужен как дешевый контрольный
вариант. Пока это гипотеза для прототипа, а не выбор production-алгоритма.
Публичного промежуточного интерфейса для этого в sherpa-onnx 1.13.5 нет.
SpeakerEmbeddingExtractor и FastClustering публичны, но
OfflineSpeakerDiarization.process() возвращает только готовые интервалы.
Матрица эмбеддингов и соответствие строк локальным говорящим остаются внутри
реализации. После set_config() повторный process() снова выполнит модельный
инференс. Python-адаптер проекта сам по себе не может дать дешевую повторную
кластеризацию; будущему прототипу нужен узкий исследовательский интерфейс на
уровне sherpa-onnx.
Что подтверждено про sherpa-onnx
FastClustering уже умеет второй шаг
В версии 1.13.5 алгоритм нормирует строки, строит все попарные косинусные расстояния, выполняет AHC методом полной связи и затем выбирает один из двух способов разрезать одно и то же дерево:
- при
num_clusters > 0: ровно на заданное число кластеров; - иначе: по высоте
threshold.
Так работает
fast-clustering.cc
и закреплено в комментарии к
FastClusteringConfig:
при известном числе кластеров порог игнорируется. Python-модуль публично
принимает C-contiguous матрицу float32 размера M x D и возвращает метку для
каждой строки
(исходник Python-модуля).
Значит, передавать оцененное N в существующий кластеризатор не требует новой
реализации распределения эмбеддингов по кластерам.
Порог FastClustering не принадлежит модели эмбеддингов: он задает высоту
дерева полной связи в шкале косинусного расстояния. Официальный пример CAMPPlus
для проверки говорящего лишь вычисляет косинусное сходство и не задает
универсальной границы решения
(infer_sv.py).
Официальный рецепт диаризации 3D-Speaker отдельно задает pval=0.012 для
спектральной кластеризации и mer_cos=0.8 для последующего слияния центров
(infer_diarization.py).
Эти величины имеют другой смысл и не могут быть перенесены в sherpa как
порог модели.
Публичных деталей недостаточно для повтора всего конвейера
Публичный SpeakerEmbeddingExtractor позволяет создать поток, подать в него
аудио и получить один эмбеддинг
(Python-модуль,
C/C++ API).
Интерфейс пригоден для самостоятельно выбранного фрагмента, но не выдает
материал, который выбрал конвейер диаризации.
Внутри OfflineSpeakerDiarization после сегментации:
- из перекрывающихся окон строятся пары
окно x локальный говорящий; - речь с одновременными голосами исключается;
- для каждой пары собирается свой поток и вычисляется эмбеддинг;
- вся матрица передается в
FastClustering; - метки строк переводятся обратно в разметку говорящих.
Последовательность и временная матрица видны в
OfflineSpeakerDiarizationPyannoteImpl::Process,
а сборка строк описана в
GetChunkSpeakerSampleIndexes и ComputeEmbeddings.
Оба метода закрыты внутри реализации.
Python-класс OfflineSpeakerDiarization экспортирует только sample_rate,
set_config и process; process возвращает готовый результат, а не признаки
(Python-модуль 1.13.5).
Внутренний SetConfig заменяет только объект кластеризации, тогда как каждый
Process заново вызывает сегментацию и ComputeEmbeddings
(реализация).
Публичная C API также документирует только создание, настройку и получение
итоговых сегментов
(официальная страница).
Итог: публичный API позволяет кластеризовать имеющуюся матрицу повторно. Извлечь или повторно обработать промежуточную матрицу штатного конвейера диаризации через него нельзя.
Варианты оценки N
1. Отдельный порог для каждой модели эмбеддингов
Автоматический режим FastClustering дешев, не добавляет зависимостей и за один
проход оценивает число кластеров и назначает метки. На порог влияют архитектура
модели эмбеддингов, длительность и состав выбранных окон, акустика и
распределение голосов. Локальная калибровка уже не нашла общего рабочего порога
для CAMPPlus, а TitaNet не прошел автоматическую сетку
(исходное исследование,
повторный CPU-тест).
Вводить еще один официальный порог модели оснований нет: исходные рецепты
публикуют параметры конкретного конвейера кластеризации, а не переносимую
границу для метода полной связи в sherpa. Вариант остается контрольным, но не
решает тикет.
2. Счетчик eigengap
Официальный CAMPPlus-конвейер 3D-Speaker строит матрицу косинусного сходства.
В каждой строке он оставляет фиксированную долю ближайших соседей,
симметризует матрицу, затем вычисляет ненормированный лапласиан и несколько
наименьших собственных пар.
Число говорящих задает позиция максимального разрыва собственных значений
(eigengap) в заданных пределах, после чего собственные векторы
кластеризуются k-means
(SpectralCluster).
Для нашей двухшаговой схемы нужны только собственные значения и оценка N;
k-means можно не выполнять. Одного спектрального разложения достаточно для
минимального контрольного варианта, но фиксированный pval остается новым
порогом. Официальная реализация задает его для своего способа нарезки; пары
окно x локальный говорящий sherpa устроены иначе. Вариант отделяет N от
порога AHC, но сохраняет калибруемый параметр.
Если брать реализацию 3D-Speaker как зависимость, она импортирует NumPy, SciPy,
scikit-learn, fastcluster, UMAP и HDBSCAN, причем отсутствие UMAP или HDBSCAN
роняет уже импорт модуля. Корневой requirements.txt при этом не перечисляет
fastcluster, UMAP и HDBSCAN
(исходник,
requirements).
Для production это несоразмерно счетчику.
3. NME-SC
NME-SC создан именно для совместной оценки числа кластеров и параметра
прореживания без отдельной выборки для настройки. Для каждого числа соседей
p алгоритм бинаризует матрицу косинусного сходства, симметризует ее, строит
ненормированный лапласиан и считает разрывы собственных значений. Нормированный
разрыв равен максимальному разрыву, деленному на наибольшее собственное
значение; выбирается p, минимизирующий отношение доли соседей p/M к
нормированному разрыву, а N задает позиция максимального разрыва
(статья авторов, раздел III).
Авторы ограничивали поиск p диапазоном до четверти числа строк, а число
говорящих числом восемь. Результаты статьи получены на x-vector и англоязычных
корпусах, поэтому перенос на наши эмбеддинги еще предстоит проверить.
Поддерживаемая реализация NeMo разделяет оценку и назначение меток. NMESC
принимает готовую матрицу сходства и возвращает est_num_of_spk вместе с
выбранным p; следующий слой отдельно выбирает оцененное или известное число
говорящих перед спектральной кластеризацией
(NMESC и выбор числа кластеров,
SpeakerClustering.forward_unit_infer).
Более высокий интерфейс NeMo получает многомасштабные эмбеддинги вместе с
временной разметкой и сам строит матрицу сходства
(forward_infer).
Для ускорения NME NeMo задает целевой размер матрицы 512 строк, перебирает
разреженную сетку из 30 значений p и предупреждает, что меньше 20 может
ухудшить оценку
(класс NMESC).
Предел не жесткий: регулярный шаг вычисляется целочисленно, поэтому для
некоторых размеров после прореживания остается больше 512 строк. Исходник также
предупреждает о риске пропустить говорящего с малой долей речи
(subsampleAffinityMat).
Для коротких записей NeMo добавляет случайные якорные эмбеддинги и повторяет
оценку. Такое расширение выходит за пределы минимального ядра статьи
(исходник).
Полная NME-SC после оценки N еще раз раскладывает лапласиан, строит
спектральные эмбеддинги и запускает k-means. Для этого тикета такая замена
итогового кластеризатора не нужна: локальные тесты уже показывают полезный путь
FastClustering с известным числом говорящих. Нужна только функция
embeddings -> N.
Рекомендуемая граница ответственности
Здесь речь не об отдельном счетчике, а о способе соединить оценку N со
штатным кластеризатором:
segmentation -> embeddings +-> счетчик NME -> N
+-> FastClustering(features, num_clusters=N)
-> штатная финализация sherpa
Плюсы:
- порог метода полной связи больше не определяет число голосовых кластеров;
- CAMPPlus и TitaNet используют один модельный проход;
- итоговое назначение меток остается в уже принятом
FastClustering; - на одной матрице можно независимо сравнить несколько счетчиков.
Минус один, но архитектурный: без нового промежуточного интерфейса текущий
process() не позволяет вставить счетчик между вычислением матрицы и
кластеризацией.
Зависимости, лицензии и цена
Штатная установка NeMo требует среди прочего NumPy, scikit-learn и PyTorch
(pyproject.toml).
| Вариант | Зависимости прототипа | Работа после эмбеддингов | Ограничение |
|---|---|---|---|
Порог FastClustering |
новых нет | существующие попарные расстояния и AHC | порог зависит от модели и материала |
| Счетчик eigengap | NumPy; официальный модуль 3D-Speaker целиком не импортируется | матрица M x M, одно собственное разложение |
фиксированное прореживание остается параметром |
| Полная NME-SC NeMo | nemo-toolkit, PyTorch, scikit-learn и другие зависимости NeMo |
несколько разложений, затем спектральные эмбеддинги и k-means | меняет итоговый кластеризатор и слишком тяжела для счетчика |
Счетчик NME + FastClustering |
NumPy | несколько разложений, затем штатный AHC по известному N |
нужен внутренний интерфейс sherpa и проверка переноса |
FastClustering уже хранит M(M-1)/2 расстояний и строит AHC. Счетчик eigengap
добавляет плотную матрицу сходства и одно собственное разложение. NME
повторяет анализ для набора p. Полная спектральная кластеризация также хранит
собственные векторы и запускает k-means, поэтому для одной оценки N избыточна.
В проектных замерах основной расход пока приходится на извлечение эмбеддингов,
а не на кластеризацию
(профиль стоимости,
CPU-тест); это не
заменяет отдельного замера NME на фактическом числе строк.
NumPy сейчас есть только в lock-файле проекта, а не в прямых
зависимостях local-transcriber. Для стенда версию 2.4.3 нужно закрепить явно.
Если счетчик дойдет до поставляемого кода, NumPy потребуется объявить прямой
зависимостью времени выполнения.
sherpa-onnx, 3D-Speaker и NeMo Speech распространяют код под Apache-2.0 (sherpa-onnx, 3D-Speaker, NeMo Speech). Авторская реализация NME-SC опубликована под MIT и направляет пользователей к поддерживаемой версии NeMo (репозиторий авторов). Файл кластеризации NeMo дополнительно сохраняет BSD-уведомление scikit-learn, NumPy 2.4.3 использует BSD-3-Clause (лицензия). Лицензионного запрета на прототип нет. Прямое заимствование кода потребует сохранить его уведомления; реализация по формулам статьи должна явно назвать происхождение алгоритма.
Ограничения вывода
- NME-SC проверялась авторами на другой нарезке и эмбеддингах x-vector. Строки sherpa представляют локальных говорящих в перекрывающихся окнах Pyannote после исключения пересечений речи; переносимость нельзя считать доказанной.
- Правильное
Nне гарантирует качественную разметку говорящих. Для TitaNet в режиме известного числа кластеров уже наблюдался малый кластер, поэтому нужны прежние прокси-метрики и слуховая проверка. - У sherpa есть отдельный путь для единственного окна, который обходится без общей матрицы эмбеддингов. Стенд должен сохранить этот частный случай, а также текущую обработку пустого результата и нечисловых эмбеддингов.
- Python-модуль
FastClusteringнормирует переданную матрицу на месте; сравниваемым вариантам нужны отдельные C-contiguous копии. - Оцененное
Nнужно проверять до вызоваFastClustering:1 <= N <= min(8, M), гдеMобозначает число валидных строк. Некорректное значение считается ошибкой ячейки, а не молча ограничивается. - Прореживание строк может потерять голосовой кластер с малой долей речи. Поэтому полный набор строк задает основной результат, а сокращенная матрица годится только для отдельной проверки чувствительности.
numpy.linalg.eigvalshиспользует BLAS/LAPACK. Без закрепленного числа потоков его процессорное и фактическое время нельзя сравнивать между машинами.
Точная граница будущего прототипа
Production-код, CLI, модели по умолчанию и ADR не менять. Сделать отдельный исследовательский стенд на sherpa-onnx 1.13.5 и сохранить точный хеш коммита экспериментальной сборки рядом с результатом.
Внутренний интерфейс sherpa
В экспериментальной ветке sherpa разделить внутренний Process в одном месте:
после удаления нечисловых строк и синхронной фильтрации пар
окно x локальный говорящий, до clustering_->Cluster.
Prepare(audio)либо возвращает готовый результат существующего частного пути, либо владеющий объектPreparedDiarization. Объект хранит матрицуfloat32, выровненные пары, локальные покадровые метки, число говорящих по кадрам и длину аудио, необходимые текущей финализации.- Экспериментальный Python-модуль отдает C-contiguous копию матрицы и непрозрачный дескриптор подготовленного состояния. Дескриптор живет только в памяти процесса.
Finalize(handle, row_labels)проверяет число меток и выполняет существующиеConvertChunkSpeakerToCluster,ReLabel,FinalizeLabelsиComputeResult. Так все счетчики используют одну подготовку и одну штатную финализацию.- Пустая речь, одно окно, отсутствие валидных эмбеддингов и одна валидная строка сохраняют нынешнее поведение. Аудио, текст, матрица и дескриптор не сериализуются.
Закрепленные входы и счетчики
Использовать только модель Pyannote Segmentation 3.0 FP32 model.onnx
(220ad67ca923bef2fa91f2390c786097bf305bceb5e261d4af67b38e938e1079) и две
модели эмбеддингов из предыдущих измерений:
- CAMPPlus zh/en
(
aa3cfc16963a10586a9393f5035d6d6b57e98d358b347f80c2a30bf4f00ceba2); - TitaNet small
(
ad4a1802485d8b34c722d2a9d04249662f2ece5d28a7a039063ca22f515a789e).
Полные SHA-256 и происхождение уже записаны в калибровке CAMPPlus и исследовании TitaNet. INT8-сегментация в эту матрицу не входит: она меняет локальные покадровые метки и уже отклонена по результатам отдельного CPU-теста.
На каждой исходной матрице сравнить:
- диагностический контроль
FastClustering: для CAMPPlus пороги1,05и1,10; для TitaNet использовать наименее плохую, но отклоненную точку1,10; - счетчик eigengap по схеме 3D-Speaker с
pval=0.012,min_pnum=6, без спектральных эмбеддингов и k-means; - счетчик NME по контракту ниже.
Контрольные пороги не становятся допустимыми настройками. Они только воспроизводят уже наблюдавшийся переход между лишними кластерами и склейкой.
Контракт счетчика NME
Контракт следует формулам статьи и варианту Sparse-Search-20 из кода авторов:
| Параметр | Значение |
|---|---|
| Вход | все M валидных строк; строки конечны и имеют ненулевую норму |
| Матрица сходства | нормировать строки на копии, затем A = E @ E.T |
Диапазон p |
1..floor(M/4); при M < 6 ячейка счетчика помечается пропуском, а диагностический FastClustering выполняется независимо |
| Разреженный поиск | все значения, если верхняя граница не больше 20; иначе 20 целых точек linspace с обоими концами и удалением дублей |
| Бинаризация | сохранить top-p в каждой строке, включая диагональ; равные значения разрешать по меньшему индексу столбца |
| Граф | (A_p + A_p.T) / 2, затем обнулить диагональ |
| Лапласиан | ненормированный L = D - A, D_ii = sum_j(abs(A_ij)) |
| Собственные значения | numpy.linalg.eigvalsh, по возрастанию; epsilon = 1e-10 |
Оценка N |
N = 1 + argmax(gaps[:min(8, M - 1)]); при равенстве брать наименьший индекс |
Выбор p |
argmin по (p/M) / (max_gap / (lambda_max + epsilon) + epsilon); при равенстве брать наименьшее p |
| Связность | если выбранный граф несвязен, взять первое большее p из той же сетки, которое делает его связным; отсутствие такого p считать ошибкой ячейки |
После коррекции связности использовать N, рассчитанное для итогового p.
Статья и код авторов не задают стабильное разрешение равных значений или
поведение при слишком малой матрице; таблица фиксирует эти два локальных
правила воспроизводимости. Основной расчет идет по всем строкам, как в
авторской реализации.
До прогона на рабочих записях закрепить две синтетические фикстуры с
блочными матрицами сходства и ожидаемыми N=2 и N=3. Они проверяют
сортировку и разрешение равных значений в argmax и argmin.
Якорные эмбеддинги, Torch, спектральные эмбеддинги и k-means в этот счетчик не
входят.
Основной результат считать на всех строках. Если M > 512, дополнительно
повторить NME с регулярным прореживанием NeMo, записать M до и после и
сравнить N. Сокращенная матрица не заменяет полный результат даже при
совпадении.
Проверка и граница решения
Для каждой модели один раз подготовить матрицу на тех же трех пятиминутных контрольных записях и на ее независимых копиях выполнить все счетчики. Критерии качества:
- оцененное число голосовых кластеров точно равно
3 / 2 / 2на трех контрольных записях для каждой модели отдельно; содержательные кластеры не компенсируют ошибку счета; - метки строк и финальный результат для оцененного
Nсовпадают с отдельным вызовом известногоNна той же матрице; - повторный счет на той же матрице дает те же
p,Nи метки; - сохранены прежняя метрика
mapped speaker purity, число слов без говорящего и диагностика малых кластеров; прогон CAMPPlus и TitaNet заканчивается слуховой проверкой.
Для каждого счетчика отдельно записать фактическое и процессорное время,
peak RSS, форму матрицы, конфигурацию NumPy/BLAS и эффективное число потоков.
Процесс запускать с одним потоком BLAS/LAPACK; способ ограничения и вывод
numpy.show_config() входят в результат.
Числовой гейт времени и памяти эта записка не задает: его определит Зафиксировать аппаратную матрицу и бюджет памяти, а общую матрицу соберет Собрать минимальную матрицу следующего CPU-эксперимента. До этих решений время и память служат измерениями, но не основанием объявить счетчик принятым.
Если счетчики eigengap и NME проходят критерии качества, в следующую матрицу идет более дешевый. Если проходит только NME, отдельно решается перенос чистой NumPy-реализации в production. Если оба ошибаются, CAMPPlus и TitaNet остаются кандидатами только для явно заданного числа говорящих.
Прототип проверяет переносимость оценки N на внутренние строки sherpa и цену
счетчика. Он не выбирает новую модель по умолчанию, не заменяет
FastClustering и не расширяет продуктовый контракт.