vocal_helper.exemplar module

Regroupement incrémental sans centroïde : des exemplaires, et le meilleur point.

Pourquoi abandonner le centroïde

IncrementalClusterer tient un centroïde par locuteur, c’est-à-dire une moyenne mobile de ses empreintes. Une moyenne est un point, et un point ne peut pas représenter un étalement. Or c’est l’étalement qui pose problème sur du matériel réel : la même voix embarquée près du micro et loin du micro, de face et de profil, dans le silence et sous le brouhaha, occupe une région — parfois deux régions franchement séparées. Le centroïde se pose alors entre les deux modes, là où le locuteur ne se trouve jamais, et il est à la fois trop loin de ses propres observations et trop près de celles du voisin.

La mesure dit exactement cela. Sur AMI, avec le seuil qui minimise le taux d’erreur, le regroupement à centroïdes laisse vingt étiquettes pour quatre locuteurs, et aucun réglage de seuil ne le corrige — quatre paramétrages ont été essayés et réfutés (voir le commentaire en tête de separating_threshold()). Pendant ce temps, l’espace d’empreintes lui-même est sain : la variation d’une même voix tourne autour de 0,12 quand la distance entre voix différentes tourne autour de 0,83, soit un facteur sept. Ce n’est donc pas l’empreinte qui limite, c’est sa réduction à un point.

Ce que fait ce module

Chaque locuteur n’est plus un point mais un ensemble borné d’exemplaires, chacun gardé avec sa confiance. La ressemblance à un locuteur est la distance à son meilleur point, pas à sa moyenne — un locuteur bimodal est alors reconnu par celui de ses deux modes qui correspond, au lieu d’être manqué par une moyenne qui n’en est aucun.

Quatre choses y gagnent par rapport au schéma à centroïdes :

  1. Pas de moyenne. Une voix à deux régimes est représentée par les deux, pas par leur milieu.

  2. La fusion globale devient définissable. Comparer deux ensembles d’exemplaires a un sens ; comparer deux moyennes mobiles n’en avait pas, car une moyenne a déjà perdu de quoi savoir si deux groupes se recouvrent. C’est ce qui permet de rattraper la sur-segmentation après coup, sans rien recalculer — le seul levier que le diagnostic laissait ouvert.

  3. La confiance remplace la moyenne mobile aveugle. Un centroïde était mis à jour au même taux par une empreinte tirée de huit secondes de parole propre et par une tirée de quatre dixièmes. Ici la durée propre est la confiance, elle décide de l’admission et elle survit à l’éviction.

  4. Le chaînage est réglable. Le meilleur point pur est un lien simple, donc sensible à un exemplaire aberrant qui ferait pont entre deux locuteurs. neighbours permet de demander le deuxième ou le troisième plus proche au lieu du premier, ce qui coupe le pont sans perdre la multimodalité.

Ce qui est conservé du schéma précédent, parce que c’est ce qui marchait : la contrainte d’injectivité par algorithme hongrois, qui interdit à deux voix simultanées de recevoir la même étiquette ; le plancher de confiance avant d’écrire quoi que ce soit ; et la création explicite d’un locuteur au-delà d’un seuil, qui est ce qui fait deviner le compte.

class vocal_helper.exemplar.Exemplar(vector, confidence)[source]

Bases: object

Une observation gardée, et ce qu’elle vaut.

Parameters:
  • vector (ndarray[tuple[int, ...], dtype[float32]])

  • confidence (float)

vector

L’empreinte, normalisée L2.

Type:

NDArray[np.float32]

confidence

Les secondes de parole propre dont elle a été tirée. C’est la seule mesure de qualité disponible sans vérité terrain, et elle est juste : une empreinte calculée sur huit secondes où le locuteur est seul vaut mieux qu’une calculée sur quatre dixièmes de seconde.

Type:

float

confidence: float
vector: ndarray[tuple[int, ...], dtype[float32]]
class vocal_helper.exemplar.ExemplarClusterer(policy=<factory>)[source]

Bases: object

Rattache les locuteurs locaux d’une fenêtre, sans jamais moyenner.

L’interface est celle de IncrementalClusterer — assign prend des empreintes et des durées propres, rend des étiquettes — donc les deux sont interchangeables dans IncrementalDiarStage, et comparables à extraction identique.

Examples

Deux voix orthogonales ouvrent le flux, chacune reçoit son étiquette, et la correspondance tient quand elles se présentent dans l’autre sens.

>>> import numpy as np
>>> e0 = np.array([1.0, 0.0, 0.0], dtype=np.float32)
>>> e1 = np.array([0.0, 1.0, 0.0], dtype=np.float32)
>>> c = ExemplarClusterer()
>>> c.assign(np.stack([e0, e1]), [5.0, 5.0])
['S0', 'S1']
>>> c.assign(np.stack([e1, e0]), [5.0, 5.0])
['S1', 'S0']

Voici ce qu’un centroïde ne peut pas faire. La même voix revient dans un autre régime, assez loin pour être gardée comme second exemplaire mais assez près pour être reconnue :

>>> other = np.array([0.8, 0.6, 0.0], dtype=np.float32)
>>> c.assign(other[None, :], [5.0])
['S0']
>>> c.exemplar_counts
[2, 1]

Ce second régime est désormais reconnu exactement, parce que la mesure est la distance au meilleur point et non à une moyenne qui serait tombée entre les deux :

>>> float(round(c.distances(other[None, :])[0, 0], 6))
0.0

Un quasi-doublon ne prend pas de place, il renforce la confiance :

>>> c.assign((other + 0.01 * e0)[None, :], [5.0])
['S0']
>>> c.exemplar_counts
[2, 1]

Et la fusion globale, que l’absence de moyenne rend définissable. Deux voix quasi identiques vues en même temps sont forcément séparées par la contrainte d’injectivité — c’est le mécanisme même de la sur-segmentation :

>>> d = ExemplarClusterer()
>>> near = np.array([1.0, 0.1, 0.0], dtype=np.float32)
>>> d.assign(np.stack([e0, near]), [5.0, 5.0])
['S0', 'S1']
>>> d.merge()
{'S1': 'S0'}
>>> d.speaker_count
1
Parameters:

policy (ExemplarPolicy)

assign(embeddings, clean_durations)[source]

Étiqueter les locuteurs locaux d’une fenêtre.

Parameters:
  • embeddings (NDArray[np.float32]) – (k, d) — une empreinte par locuteur local actif.

  • clean_durations (list of float) – k durées de parole propre, qui servent de confiance.

Returns:

k étiquettes, dans l’ordre des lignes reçues.

Return type:

list of str

Raises:

ValueError – Si les deux entrées n’ont pas la même longueur, ou si les empreintes ne sont pas une matrice.

distances(embeddings)[source]

Distance de chaque empreinte au meilleur point de chaque locuteur.

Avec neighbours > 1, c’est le k-ième plus proche et non le plus proche : un seul exemplaire aberrant ne suffit alors plus à rapprocher deux locuteurs.

Parameters:

embeddings (NDArray[np.float32]) – (k, d), pas nécessairement normalisées.

Returns:

(k, m) de distances cosinus, m étant le nombre de locuteurs.

Return type:

NDArray[np.float32]

property exemplar_counts: list[int]

Combien d’exemplaires chaque locuteur garde.

merge()[source]

Réunir les locuteurs que la mémoire révèle identiques.

C’est ce que l’absence de moyenne rend possible. Deux ensembles d’exemplaires se comparent — on regarde si leurs observations se recouvrent — là où deux moyennes mobiles avaient déjà perdu de quoi le savoir. La passe est exacte, ne recalcule aucune empreinte, et répare après coup la sur-segmentation que la décision locale ne pouvait pas éviter : la contrainte d’injectivité doit séparer deux locuteurs locaux simultanés, même quand la segmentation s’est trompée et qu’il n’y en avait qu’un.

Returns:

{ancienne étiquette: étiquette survivante} pour les locuteurs absorbés. Vide si rien ne fusionne. À l’appelant de réécrire ses propres sorties avec cette table.

Return type:

dict

policy: ExemplarPolicy
property speaker_count: int

Nombre de locuteurs découverts.

property speaker_labels: list[str]

Les étiquettes, dans l’ordre de découverte.

class vocal_helper.exemplar.ExemplarPolicy(rho_update_s=0.4, delta_new=0.6, reference='meilleur', neighbours=1, max_exemplars=12, diversity_margin=0.1, max_speakers=None, merge=True, merge_threshold=None)[source]

Bases: object

Les réglages du regroupement par exemplaires.

Parameters:
  • rho_update_s (float)

  • delta_new (float)

  • reference (str)

  • neighbours (int)

  • max_exemplars (int)

  • diversity_margin (float)

  • max_speakers (int | None)

  • merge (bool)

  • merge_threshold (float | None)

rho_update_s

Confiance minimale, en secondes de parole propre, pour qu’une observation soit gardée. En dessous, elle est quand même étiquetée — on ne perd personne — mais elle n’entre pas dans la mémoire du locuteur.

Type:

float

delta_new

Distance au meilleur point au-delà de laquelle on crée un locuteur plutôt que de forcer une jointure. Reste à 0,6 : trois réunions d’AMI donnent 0,750 / 0,600 / 0,600 comme optimum, et aucune normalisation n’a réduit cette dispersion.

Type:

float

reference

Comment mesurer la ressemblance à un locuteur, sans jamais moyenner.

"meilleur"

Distance au neighbours-ième exemplaire le plus proche. Garde la multimodalité, mais c’est un minimum sur un ensemble, donc un estimateur optimiste : il diminue mécaniquement quand l’ensemble grandit, si bien qu’un seul exemplaire bruité suffit à capturer un locuteur. Mesuré sur AMI : taux d’erreur 0,529 contre 0,299 pour le centroïde, et dérive 0,859. C’est ce biais que neighbours corrige partiellement, 0,529 puis 0,378 puis 0,363 pour k=1,2,3.

"confiance"

Distance à l’unique exemplaire de meilleure confiance. Un point observé, donc aucune moyenne, et pas de biais optimiste non plus puisque le nombre d’exemplaires n’entre pas dans la mesure.

"médoïde"

Distance au médoïde — l’exemplaire le plus central de l’ensemble, celui de plus forte similarité totale aux autres. Également un point observé, et robuste à un intrus isolé, qui ne peut pas devenir le plus central.

Type:

str

neighbours

Combien de voisins regarder, pour reference="meilleur" seulement. 1 est le meilleur point pur, donc un lien simple, sensible à un exemplaire aberrant faisant pont entre deux locuteurs. 2 ou 3 coupent le pont tout en gardant la multimodalité.

Type:

int

max_exemplars

Taille de la mémoire par locuteur. C’est ce qui garde le coût borné en direct : la distance à un locuteur coûte au plus max_exemplars produits scalaires.

Type:

int

diversity_margin

En dessous de cette distance à un exemplaire déjà gardé, une observation est un quasi-doublon : elle renforce la confiance de l’existant au lieu de prendre une place. C’est ce qui empêche la mémoire de se remplir de dix copies du même instant.

Type:

float

max_speakers

Plafond dur du nombre de locuteurs. None laisse le compte libre, c’est-à-dire deviné.

Type:

int, optional

merge

Faire ou non la reprise globale. True par défaut, parce que c’est l’apport principal de la représentation par exemplaires. Le mettre à False sert à isoler les deux effets dans une mesure : l’absence de moyenne d’une part, la reprise globale de l’autre.

Type:

bool

merge_threshold

Distance entre deux ensembles d’exemplaires en dessous de laquelle ExemplarClusterer.merge() les réunit. None reprend delta_new, ce qui est cohérent : deux locuteurs plus proches que le seuil de création n’auraient jamais dû être deux.

Type:

float, optional

delta_new: float = 0.6
diversity_margin: float = 0.1
max_exemplars: int = 12
max_speakers: int | None = None
merge: bool = True
merge_threshold: float | None = None
neighbours: int = 1
reference: str = 'meilleur'
rho_update_s: float = 0.4