話者分離の評価指標を完全解説|DER・JER・Purity・Boundary Errorの計算方法と使い分け

この記事の結論(まず読んでほしいポイント)

話者分離(Speaker Diarization)の評価には「何を測りたいか」によって使うべき指標が異なる。DERは業界標準だが「なぜ失敗しているか」の診断には使えない。本番システムを改善するには、DER単体ではなくConfusion Matrix・Purity・Boundary Errorを組み合わせて診断することが重要だ。本記事では各指標の計算式・実装コード・実験的解釈まで一次情報をもとに解説する。


話者分離とは「誰がいつ話したか」を自動検出するタスクだ。ASR(自動音声認識)の前処理として、また議事録・コールセンター分析・医療面談記録など実用場面で広く使われている。しかし、いざ自作システムを評価しようとすると「DERって何?JERとの違いは?」と混乱することが多い。

本記事では、話者分離評価で登場する6つの主要指標について、計算方法・実装・実験での解釈を体系的に解説する。


1. DER(Diarization Error Rate)とJER(Jaccard Error Rate)

DERとは

DERは話者分離の最も標準的な評価指標で、1997年のNISTベンチマークから使われてきた。計算式は以下の通り。

DER=FA+MISS+CONFTotal Reference Duration\text{DER} = \frac{\text{FA} + \text{MISS} + \text{CONF}}{\text{Total Reference Duration}}
成分意味
FA(False Alarm)音声がないのに話者を検出した時間
MISS(Miss)実際に話しているのに検出できなかった時間
CONF(Confusion)話者ラベルを取り違えた時間

用語解説

  • 話者分離(Speaker Diarization): 複数話者の音声から「誰がどの区間で話しているか」を自動推定するタスク
  • 参照ラベル(Reference / Ground Truth): 人手でアノテーションされた正解データ
  • 仮説(Hypothesis): システムが出力した推定結果

Pythonでの計算例

python
from pyannote.metrics.diarization import DiarizationErrorRate
from pyannote.core import Annotation, Segment

# 正解アノテーション
reference = Annotation()
reference[Segment(0, 5)] = 'SPEAKER_A'
reference[Segment(5, 10)] = 'SPEAKER_B'
reference[Segment(10, 15)] = 'SPEAKER_A'

# システム出力
hypothesis = Annotation()
hypothesis[Segment(0, 4)] = 'SPEAKER_1'   # SPEAKER_Aに対応
hypothesis[Segment(4, 11)] = 'SPEAKER_2'  # 一部混在
hypothesis[Segment(11, 15)] = 'SPEAKER_1'

metric = DiarizationErrorRate()
der = metric(reference, hypothesis)
print(f"DER: {der:.3f}")  # 例: 0.133 (13.3%)

# 成分ごとの詳細
detail = metric(reference, hypothesis, detailed=True)
print(f"  Miss: {detail['missed detection']:.3f}")
print(f"  FA:   {detail['false alarm']:.3f}")
print(f"  Conf: {detail['confusion']:.3f}")

Collar(カラー)について

DER計算では通常、発話境界の前後 250ms を評価から除外する「collar」設定が使われる。NAISTやCHiME-6などデータセットによって collar=0 の厳格設定もある。pyannote.metrics では collar=0.25 がデフォルト。比較する際は必ずcollar設定を統一すること。

JER(Jaccard Error Rate)とは

JERはCHiME-6チャレンジで提案された指標で、話者ごとの誤りを均等に扱う点がDERと異なる。

JER=11SsSHsRsHsRs\text{JER} = 1 - \frac{1}{|S|} \sum_{s \in S} \frac{|H_s \cap R_s|}{|H_s \cup R_s|}
  • SS: 全話者の集合
  • RsR_s: 話者 ss の正解区間
  • HsH_s: 話者 ss にマッピングされた仮説区間
python
# DERは長い話者の誤りが総合スコアを支配する
# 例: SPEAKER_Aが900秒、SPEAKER_Bが100秒
# SPEAKER_Bを完全に誤っても DER への影響は小さい

# JERはすべての話者を等重みで扱う
# → 少ない発話量の話者の精度も公平に評価される

from pyannote.metrics.diarization import JaccardErrorRate

jer = JaccardErrorRate()
jer_score = jer(reference, hypothesis)
print(f"JER: {jer_score:.3f}")

DER は時間ベースなので、「大部分の時間は合っていて、一部でラベルが割れる」ケースだと比較的低く出やすい。
一方で JER は話者ごとの集合一致を見るから、話者分裂やラベルの不整合に厳しい。


2. Speaker Confusion Matrix(話者混同行列)

混同行列は「どの話者がどの話者と間違えられているか」を可視化する診断ツールだ。DERの CONF 成分の内訳を把握できる。

計算方法

実装の核心は「collar/skip_overlapで加工済みのアノテーション」に対して計算することだ。DERの内部で uemify() によって評価範囲が整形されており、混同行列もその後の ref_eval / hyp_eval に対して計算する必要がある。

python
def compute_speaker_confusion_matrix(
    reference: Annotation,
    hypothesis: Annotation,
) -> dict:
    ref_labels = sorted(map(str, reference.labels()))
    hyp_labels = sorted(map(str, hypothesis.labels()))

    # 全ペアの重複時間(秒)を計算
    overlap_by_pair = {
        ref_label: {hyp_label: 0.0 for hyp_label in hyp_labels}
        for ref_label in ref_labels
    }
    for ref_seg, _, ref_label in reference.itertracks(yield_label=True):
        for hyp_seg, _, hyp_label in hypothesis.itertracks(yield_label=True):
            intersection = ref_seg & hyp_seg
            if intersection:
                overlap_by_pair[str(ref_label)][str(hyp_label)] += float(
                    intersection.duration
                )

    # 正解話者ごとにシェアを計算してサマリーを作る
    per_reference_speaker = {}
    for ref_label, row in overlap_by_pair.items():
        ref_duration = sum(
            float(seg.duration)
            for seg, _, lbl in reference.itertracks(yield_label=True)
            if str(lbl) == ref_label
        )
        overlaps = sorted(
            [
                {"hyp_speaker": h, "overlap_sec": s,
                 "share": s / ref_duration if ref_duration > 0 else 0.0}
                for h, s in row.items() if s > 0.0
            ],
            key=lambda x: -x["overlap_sec"],
        )
        dominant = overlaps[0] if overlaps else None
        per_reference_speaker[ref_label] = {
            "ref_duration_sec": ref_duration,
            "hyp_count": len(overlaps),
            "dominant": dominant["hyp_speaker"] if dominant else None,
            "dominant_share": dominant["share"] if dominant else 0.0,
            "overlaps": overlaps,
        }

    return {"seconds": overlap_by_pair, "per_reference_speaker": per_reference_speaker}

出力の読み方

実際の評価ツールが出力する gt_speaker_confusion_all はこの per_reference_speaker を整形したものだ。

text
GT 6: hyp_count=4 dominant=speaker_9 share=49.6%
      overlaps=[speaker_9:49.6%, speaker_7:43.9%, speaker_10:3.4%, speaker_1:1.3%]

dominant_share が50%未満の場合、その正解話者は2つ以上の仮説クラスタに引き裂かれている。hyp_count が大きいほど fragmentation が深刻だ。


3. Fragmentation / Purity / Coverage

DERが「量」を測るのに対し、これらの指標は「質」を測る。

用語定義

  • Fragmentation(断片化): 1人の話者の発話が複数のクラスタに分散している度合い
  • Purity(純粋度): 1つのクラスタが単一話者で構成されている度合い
  • Coverage(カバレッジ): 正解の各話者区間がどれだけシステムに捉えられているか

計算式

Cluster Purity

Purity=1TkmaxsCkRs\text{Purity} = \frac{1}{T} \sum_{k} \max_{s} |C_k \cap R_s|
  • TT: 総音声時間
  • CkC_k: クラスタ kk に属する時間区間の集合
  • RsR_s: 話者 ss の正解区間

Coverage

Coverage=1TsmaxkRsCk\text{Coverage} = \frac{1}{T} \sum_{s} \max_{k} |R_s \cap C_k|
python
from pyannote.metrics.diarization import (
    DiarizationPurity,
    DiarizationCoverage,
    DiarizationErrorRate,
)

# DERと同じcollar/skip_overlap設定を揃えることが重要
collar = 0.0
skip_overlap = False

purity_metric   = DiarizationPurity(collar=collar, skip_overlap=skip_overlap)
coverage_metric = DiarizationCoverage(collar=collar, skip_overlap=skip_overlap)

purity   = float(purity_metric(reference, hypothesis))
coverage = float(coverage_metric(reference, hypothesis))

print(f"Purity:   {purity:.3f}")    # 1.0 が理想
print(f"Coverage: {coverage:.3f}")  # 1.0 が理想

# Purityが高くCoverageが低い → split / fragmentation が支配的
# Purityが低くCoverageが高い → merge が支配的(複数話者が1クラスタに混在)

Fragmentation の定量化

Purity/Coverageはグローバルな集計値だが、話者ごとの断片化の深刻さは segment_split_count(正解話者の発話区間内での仮説ラベル切り替わり数)で表現できる。実際の評価スクリプトでは、正解話者ごとの区間を時系列に走査し、仮説ラベルが変化するたびにカウントを増やすことで計算している。

python
# 実測値(15話者会議音声)
# GT 1: split_count=57, hyp_count=8  ← 最長話者(144s)が8クラスタに分裂
# GT 2: split_count=42, hyp_count=4
# GT 6: split_count=17, hyp_count=4

# duration_weighted_mean_fragment_count で全体傾向を把握する
# → 平均的に1人が何クラスタに分裂しているか

Purity vs DERの使い分け

  • デバッグ時 → Purity/Coverageで「過分割か統合不足か」を先に判断する
  • 論文比較 → DER(標準的でベンチマーク間で比較しやすい)
  • マルチスピーカー公平性 → JER

4. Count Error(話者数誤り)

Count Errorはシステムが推定した話者数と正解話者数の差を評価する。ポイントは「ファイル全体の話者数の比較」ではなく、タイムライン上の各フレームでの活性話者数の差分を時間加重平均する点だ。

計算式

Count Error=tΔtNref(t)Nhyp(t)tΔt\text{Count Error} = \frac{\sum_t \Delta t \cdot |N_{\text{ref}}(t) - N_{\text{hyp}}(t)|}{\sum_t \Delta t}
  • Nref(t)N_{\text{ref}}(t): 時刻 tt における正解の活性話者数
  • Nhyp(t)N_{\text{hyp}}(t): 時刻 tt における仮説の活性話者数
python
def compute_count_error(
    reference: Annotation,
    hypothesis: Annotation,
    timeline,  # DER の uemify() が返す評価タイムライン
) -> dict:
    """
    話者数誤りをタイムライン上で時間加重計算する。
    単純な ref/hyp 話者総数の差ではなく、
    各時刻フレームでの活性話者数のずれを集計する。
    """
    total_duration   = 0.0
    weighted_abs_err = 0.0
    weighted_sgn_err = 0.0
    exact_match_dur  = 0.0

    for segment in timeline:
        duration  = float(segment.duration)
        ref_count = len(reference.get_labels(segment, unique=False))
        hyp_count = len(hypothesis.get_labels(segment, unique=False))
        diff = hyp_count - ref_count

        total_duration   += duration
        weighted_abs_err += duration * abs(diff)
        weighted_sgn_err += duration * diff
        if diff == 0:
            exact_match_dur += duration

    return {
        "mean_abs_speaker_count_error": weighted_abs_err / total_duration,
        "mean_signed_speaker_count_error": weighted_sgn_err / total_duration,
        "exact_count_match_ratio": exact_match_dur / total_duration,
    }

# DERの評価タイムラインを取得してから呼ぶ
der_metric = DiarizationErrorRate(collar=0.0, skip_overlap=False)
ref_proj, hyp_proj, timeline = der_metric.uemify(
    reference, hypothesis,
    collar=0.0, skip_overlap=False, returns_timeline=True,
)
result = compute_count_error(ref_proj, hyp_proj, timeline)
print(f"mean_abs:          {result['mean_abs_speaker_count_error']:.4f}")
print(f"exact_match_ratio: {result['exact_count_match_ratio']:.4f}")

Count Errorが低くてもDERが高いケースに注意

例えば「正解3人→推定3人」でもラベルがすべて混同されていればDERは高い。Count Errorは「活性話者数の推定精度」を測るものであり、「正しく分離できているか」は別問題だ。mean_signed_speaker_count_error が正ならover-counting(多め検出)、負ならunder-counting(少なめ検出)の傾向が分かる。


5. Boundary Error(境界誤り)

Boundary Errorは話者の切り替わり点(境界)をどれだけ正確に検出できているかを評価する。

定義

text
正解: A----A----A|B-----B|A---A
推定: A----A------|B----B-----|A---
                ↑           ↑
              境界の時間ずれ(Boundary Error)

境界誤りには2種類ある:

  • Detection Error: 境界自体を見逃す・余計に検出する
  • Localization Error: 境界を検出したが時刻がずれている

計算方法

実装のポイントは境界点の一対一マッチングだ。単純な「tolerance以内の境界を全部カウント」ではなく、正解1点につき仮説1点だけをマッチさせる。マッチした仮説境界はリストから除去し、近接した多重検出による過大評価を防ぐ。

python
def extract_boundaries(annotation: Annotation) -> list[float]:
    """アノテーション内の全セグメントの start/end を境界として返す"""
    boundaries: set[float] = set()
    for segment in annotation.itersegments():
        boundaries.add(round(float(segment.start), 9))
        boundaries.add(round(float(segment.end), 9))
    return sorted(boundaries)


def compute_boundary_error(
    reference: Annotation,
    hypothesis: Annotation,
    tolerance: float = 0.5,  # デフォルト 500ms
) -> dict:
    ref_boundaries = extract_boundaries(reference)
    hyp_boundaries = list(extract_boundaries(hypothesis))  # mutable: マッチしたら除去

    matched_pairs = []
    hyp_idx = 0

    for ref_b in ref_boundaries:
        # スキャン位置を tolerance 範囲の左端まで進める
        while (hyp_idx < len(hyp_boundaries)
               and hyp_boundaries[hyp_idx] < ref_b - tolerance):
            hyp_idx += 1

        # 近傍候補の中で最も近い仮説境界を1つ選ぶ
        candidates = []
        for ci in (hyp_idx - 1, hyp_idx, hyp_idx + 1):
            if 0 <= ci < len(hyp_boundaries):
                dist = abs(hyp_boundaries[ci] - ref_b)
                if dist <= tolerance:
                    candidates.append((dist, ci))

        if not candidates:
            continue

        _, best_ci = min(candidates)
        matched_pairs.append((ref_b, hyp_boundaries[best_ci]))
        hyp_boundaries.pop(best_ci)      # 使用済み境界を除去
        if best_ci < hyp_idx:
            hyp_idx -= 1

    matched   = len(matched_pairs)
    ref_total = len(ref_boundaries)
    hyp_total = matched + len(hyp_boundaries)  # 残りはFP(過剰検出)

    precision = matched / hyp_total if hyp_total else 1.0
    recall    = matched / ref_total if ref_total else 1.0
    f1        = 2 * precision * recall / (precision + recall) if precision + recall else 0.0
    errors    = [abs(r - h) for r, h in matched_pairs]

    return {
        "precision": precision,
        "recall": recall,
        "f1": f1,
        "mean_abs_error_sec": sum(errors) / matched if matched else None,
        "max_abs_error_sec": max(errors) if errors else None,
    }

result = compute_boundary_error(reference, hypothesis, tolerance=0.5)
print(f"Precision: {result['precision']:.3f}")
print(f"Recall:    {result['recall']:.3f}")
print(f"F1:        {result['f1']:.3f}")
if result["mean_abs_error_sec"]:
    print(f"Mean loc error: {result['mean_abs_error_sec']*1000:.1f}ms")

なぜBoundary Errorが重要か

DERでは「境界のズレ」が直接見えない。例えばcollar=250msの設定では250ms以内のずれは無視されるため、DERが低くてもすべての境界が200msずれているケースが存在する。

実用的には

  • 字幕生成・リアルタイム議事録では100ms以内の精度が求められる
  • オフライン処理の話者ラベリングなら500ms程度の誤差は許容される

用途に応じて tolerance を調整し、Boundary F1スコアをDERと組み合わせて報告することを推奨する。


6. Utterance Length Recall(発話長再現率)

Utterance Length Recallは「各発話区間をどれだけ正確に再現できているか」を発話長ビンごとに評価する指標で、特に短い発話の検出精度に焦点を当てる。

定義と計算

実装のポイントは最適マッピング後の仮説を使うことだ。DERが計算した optimal_mapping(ハンガリアン法による最適ラベル割り当て)を適用してから、正解発話ごとに「同一話者ラベルとしてカバーされた時間 / 発話長」をRecallとして計算する。

python
UTTERANCE_LENGTH_BINS = [
    ("0_to_1s",   0.0,  1.0),
    ("1_to_2s",   1.0,  2.0),
    ("2_to_5s",   2.0,  5.0),
    ("5_to_10s",  5.0, 10.0),
    ("10s_plus", 10.0, None),
]

def compute_utterance_length_recall(
    reference: Annotation,
    mapped_hypothesis: Annotation,  # optimal_mapping 適用済みの仮説
) -> dict:
    """
    発話長ビンごとの Recall を計算する。
    Recall の定義:
      その発話区間のうち「同一話者として」カバーされた時間 / 発話長
    """
    # 仮説側を話者ラベルでインデックス化(区間リストに整形)
    hyp_by_label: dict[str, list[tuple[float, float]]] = {}
    for seg, _, lbl in mapped_hypothesis.itertracks(yield_label=True):
        hyp_by_label.setdefault(str(lbl), []).append(
            (float(seg.start), float(seg.end))
        )

    grouped = {name: {"count": 0, "ref_dur": 0.0, "matched_dur": 0.0}
               for name, *_ in UTTERANCE_LENGTH_BINS}

    for ref_seg, _, ref_lbl in reference.itertracks(yield_label=True):
        dur = float(ref_seg.duration)
        lbl = str(ref_lbl)

        # 仮説側の同一話者ラベル区間との重複を計算
        matched = sum(
            max(0.0, min(float(ref_seg.end), h_end)
                   - max(float(ref_seg.start), h_start))
            for h_start, h_end in hyp_by_label.get(lbl, [])
        )
        bin_name = next(
            name for name, lo, hi in UTTERANCE_LENGTH_BINS
            if dur >= lo and (hi is None or dur < hi)
        )
        grouped[bin_name]["count"]       += 1
        grouped[bin_name]["ref_dur"]     += dur
        grouped[bin_name]["matched_dur"] += matched

    results = {}
    total_ref = total_matched = 0.0
    for name, stats in grouped.items():
        rd = stats["ref_dur"]
        results[name] = {
            "utterance_count": stats["count"],
            "duration_weighted_recall": stats["matched_dur"] / rd if rd > 0 else 0.0,
        }
        total_ref     += rd
        total_matched += stats["matched_dur"]

    results["overall_duration_weighted"] = total_matched / total_ref if total_ref else 0.0
    return results

実際の出力(実測値):

text
utterance_length_recall:
  duration_weighted = 0.8473  ← 全体の時間ベース再現率
  macro             = 0.8071  ← 発話本数で等重みの再現率

duration_weighted > macro という差は「長い発話は比較的うまく取れているが、短い発話の精度が引き下げている」ことを示す。

なぜUterance Length Recallが重要か

多くのDiarization評価はDERで総合スコアを報告するが、短い発話(0〜2秒)の検出率はシステムによって大きく異なる。会議音声では相槌・割り込み・短い返答が頻繁に登場し、これらが「短い発話」に該当する。

DERだけではこの差が隠れる。Utterance Length Recallを分析することで:

  • VADのチューニング指針が得られる(短発話でmissが多ければVADの感度問題)
  • クラスタリングの問題と切り分けられる(短発話でconfが多ければ割り当ての問題)

指標の組み合わせと診断フロー

text
DERが高い
    │
    ├─ MISS成分が大 → VADの感度を上げる
    │
    ├─ FA成分が大  → VADの閾値を上げる・ノイズフィルタ強化
    │
    └─ CONF成分が大
           │
           ├─ Count Errorも大 → 話者数推定の改善(クラスタリングのk選択)
           │
           ├─ Purityが低い  → クラスタが複数話者を含む → クラスタリング粒度を細かく
           │
           ├─ Coverageが低い → 1話者が複数クラスタに分散 → マージ戦略を見直す
           │
           ├─ Boundary F1が低い → 発話境界検出の精度問題
           │
           └─ Short recall が低い → VAD・セグメンテーションの短発話対応

実験テンプレート

markdown
| Metric            | System A | System B | Baseline |
|-------------------|----------|----------|----------|
| DER (%)           | 12.3     | 10.8     | 18.5     |
| JER (%)           | 15.6     | 13.2     | 22.1     |
| Purity            | 0.921    | 0.943    | 0.872    |
| Coverage          | 0.884    | 0.901    | 0.821    |
| Count Error (avg) | 0.4      | 0.3      | 1.2      |
| Boundary F1       | 0.823    | 0.871    | 0.741    |
| Short ULR (<1s)   | 0.51     | 0.68     | 0.38     |
| Collar (sec)      | 0.25     | 0.25     | 0.25     |
| Dataset           | AMI      | AMI      | AMI      |

論文比較での重大注意事項

  1. Collarを明記する: collar=0 と collar=0.25 では DER が 3〜5ポイント変わることがある
  2. 重複発話の扱いを明記する: overlapping speech を除外して評価しているシステムは多い
  3. 最適マッピング(Hungarian algorithm)の使用有無を明記する: 話者ラベルの割り当て方によってスコアが変わる
  4. 評価ライブラリのバージョンを固定する: pyannote.metrics はバージョンによって挙動が変わる場合がある

実データで読み解く:15話者会議音声の診断例

ここからは実際のシステム出力結果を使って、各指標がどう連携して問題を教えてくれるかを示す。以下の数値はすべて実測値だ。評価条件は collar=0.0(strictモード)、skip_overlap=False

Step 1:全体像を眺める

まずすべての数値を一覧で並べる。

text
DER:       17.69%  → 内訳: confusion 12.61% / missed 2.67% / FA 2.42%
JER:       37.36%
Purity:    97.31%
Coverage:  88.63%
ref/hyp:   15人 / 17人
Count Error: mean_abs=0.050  exact_match=95.0%
Boundary:  precision=0.785  recall=0.886  f1=0.833
ULR:       duration_weighted=0.847  macro=0.807

この段階でもう重要なことが2つ読める。

① DER 17.7% なのに JER 37.4%。この乖離が最初の重要シグナルだ。DERは発話時間の長い話者に引きずられる。GT 1(144.61s)とGT 2(106.72s)の2話者だけで全体の56%を占めており、ここがうまく分離できていればDERは低く出る。一方JERは全15話者を等重みで評価するため、発話量の少ない話者(GT 11: 2.38s、GT 14: 2.76s)の壊滅的な精度がそのままスコアに反映される。

② DERの内訳で confusion が約7割を占めている。miss・FAはどちらも 2〜3%台と小さい。これが最重要の事実で、**「誰かが話していること自体は捉えられているが、誰なのかを間違えている」**という状態を示している。

この時点で「VADは主な問題ではない」と切り分けられる

missed detection と false alarm がともに低いということは、発話区間の検出自体は機能している。改善の矢は VAD ではなく、話者の割り当て(speaker assignment)側に向けるべきだ。


Step 2:Purity と Coverage の非対称性から主因を特定する

text
Purity:   97.31%  ← 非常に高い
Coverage: 88.63%  ← やや低い

この組み合わせは診断上のパターンが決まっている。

  • Purity が高い = 各クラスタの中身はほぼ1人だけ(複数話者が混ざっていない)
  • Coverage が低い = GT話者1人が複数のクラスタに分裂している

つまり問題は merge(統合しすぎ)ではなく split(分割しすぎ) だ。もし merge が強いなら Purity がもっと下がる。Purity 97% は「クラスタ内の純粋さ」を保証しており、「1人を2人・3人に分けてしまう fragmentation」が主因という仮説と完全に整合する。

fragmentation データがこれを数値で裏付ける:

text
GT 1:  split_count=57  hyp_count=8   ← 最長話者(144s)が8クラスタに散らばる
GT 2:  split_count=42  hyp_count=4
GT 6:  split_count=17  hyp_count=4

GT 1(144.61秒)が57区間・8クラスタに断片化されているのは深刻だ。発話量が多いにもかかわらず話者埋め込みが安定しない——声質変化・感情変動・マイク距離変化などで埋め込みベクトルが一貫した空間位置を保てていない可能性が高い。


Step 3:hyp 話者数の多さも同じ仮説を支持する

text
ref_speakers: 15
hyp_speakers: 17
count_error:  mean_abs=0.050  exact_match_ratio=0.950

ファイル単位での話者数推定は95%で正解と非常に良い。しかしトータルで hyp=17(ref=15)、つまり2クラスタ余分に生成している。これは fragmentation 仮説と一致する。

merge が強いシステムなら逆に hyp < ref になりやすい。hyp > ref という方向性は「同一人物を別人として分けてしまう傾向」の傍証だ。

Count Error が良いのに DER が高い:この組み合わせが意味すること

「人数の推定は正確、でもラベルの割り当てが不安定」という状態。問題は「何人いるか分からない」ではなく「誰を誰として束ねるか」のクラスタリング品質にある。


Step 4:confusion matrix で混同の構造を読む

gt_speaker_confusion_all を見ると、混同のパターンが具体的に見えてくる:

text
GT 6:  dominant=speaker_9 share=49.6%
        overlaps=[speaker_9:49.6%, speaker_7:43.9%, ...]
        → 1人の話者が speaker_9 と speaker_7 の2クラスタに引き裂かれている

GT 9:  dominant=speaker_15 share=57.7%
        overlaps=[speaker_15:57.7%, speaker_10:21.3%, ...]
        → GT 6 と GT 9 がともに speaker_10 を共有
          (speaker_10 が複数GT話者の「受け皿」になっている)

GT 11: dominant=speaker_14 share=36.1%  ← dominant share が最低
        detected=53.8%  ← 音声の半分近くが未検出
        → 発話総量 2.38s という極端な短さが要因

GT 11 は発話量が少なすぎて、そもそも「この人専用のクラスタ」が形成されにくい。これが JER を大きく押し上げている要因の一つだ。


Step 5:Boundary Error の precision < recall が示すもの

text
boundary_error: precision=0.785  recall=0.886  f1=0.833

Recall > Precision、つまり境界を多めに打ちすぎている。本当は連続した同一話者の発話なのに、途中で余計な区切りを入れてしまっている傾向だ。これも fragmentation と連動している——境界を余計に入れるたびに speaker ID が揺れやすくなる。

さらに boundary_focus_analysis を見ると:

text
start+0.5s: coverage=76.1%  conf=18.1%
end-0.5s:   coverage=67.1%  conf=25.0%  ← 終端の方が一貫して悪い

発話の終端付近の方が開始付近より精度が低い。発話終了後のフェードアウト・余韻・短い無音区間の扱いが不安定で、終端のセグメント境界が揺れていると読める。


Step 6:短発話の confusion が「VADではなくクラスタリング」の問題を確定する

text
utterance_length_conditioned_errors:
  0.5〜1s:  coverage=43.5%  conf=56.5%  miss=0.0%   (n=4)
  1〜2s:    coverage=70.7%  conf=23.8%  miss=5.5%   (n=16)
  2s+:      coverage=85.9%  conf=11.6%  miss=2.5%   (n=78)

ここが最も重要な切り分けポイントだ。0.5〜1秒の発話で miss=0% ——VADは短い発話を検出できている。しかし conf=56.5%——検出した発話の半分以上で話者ラベルが間違っている。

発話が短いと埋め込みベクトルの信頼度が低下し、近隣クラスタへの誤割り当てが増える。短発話への対策はVADではなく、クラスタリングの割り当てロジック(例:短区間は近傍の時系列コンテキストを参照する、confidence-weighted assignment)の改善になる。


総合診断:このシステムの本質的な問題

全指標を統合すると、一貫したストーリーが見える。

「検出はできている。人数感も大きく外していない。しかし同一話者の一貫性が弱く、話者ラベルの安定性が足りない。」

各指標がどの仮説を支持しているかを整理するとこうなる:

仮説支持する指標
主因は fragmentation(1人を複数IDに分割)Purity高・Coverage低、hyp>ref、split_count大、Boundary precision<recall
VADは主な問題ではないmiss/FA ともに低、短発話のmiss=0%
クラスタリングの speaker assignment が弱いconfusion成分が支配的、短発話のconf高、Count Errorは良好
短発話・発話終端でさらに悪化utterance_length conditioned errors、end-0.5s の coverage 低

改善の優先順位はシンプルだ。

  1. クラスタリングのマージ戦略を強化する(oversegmentation の直接対策)
  2. 短区間の speaker assignment に時系列コンテキストを活用する(短発話 conf の改善)
  3. 発話終端のセグメント境界精度を上げる(fragmentation の二次的要因の除去)

ノイズ除去や VAD の感度チューニングは、少なくともこのデータにおいては優先度が低い。

collar=0.0 という厳格設定の影響

この結果は collar=0(境界の猶予なし)で計算されている。collar=0.25s に変更すると DER は概ね 13〜15% 程度まで改善すると予想される。これはシステムの改善ではなく評価基準の変更なので、論文ベンチマークとの比較時は必ずcollar設定を揃えること。


FAQ

Q1. DERが0%になることはありますか?

理論上は可能だが、実際には注釈の揺らぎ(アノテーターが境界をどこに置くかのばらつき)があるため、人手アノテーションどうしを比較しても数%のDERが出ることが多い。AMIコーパスでの人間同士の一致率は DER 約5〜7% 程度とされる。

Q2. pyannoteとdscore(md-eval)で結果が違うのはなぜですか?

md-eval(NIST公式ツール)とpyannote.metricsではcollarのデフォルト設定や重複区間の処理が異なる。特にmd-evalは発話単位で処理し、pyannoteはタイムライン単位で処理するため、完全には一致しない。ベンチマーク投稿先のルールに従ったツールを使うこと。

Q3. 話者が2人しかいない場合にJERを使う意味はありますか?

話者数が少ないほどDERとJERの差は小さくなる。2話者の場合はほぼ同等のスコアになることが多い。JERが特に有効なのは4人以上のマルチスピーカー会議音声で、発話量の不均衡(例:司会者が全体の70%話す)が大きいケースだ。

Q4. Boundary Errorのcollarは何秒に設定すればいいですか?

用途による。字幕・リアルタイム書き起こし用途なら100ms。論文ベンチマーク的な評価なら慣例として250msか500ms。AMI/DIPCOのチャレンジでは500msが多い。必ず設定値を報告すること。

Q5. 話者が途中で出入りする会議(遅刻・早退)の評価はどうすればいいですか?

DERはその話者が発話していない区間も評価対象に含める実装が多いため、「存在しない話者を誤検出」がFAとして計上される。この場合、ファイル全体ではなく各話者の発話区間を明示的にマスクして評価するか、発話のある区間に限定したcount-based評価を行うことを検討する。

Q6. PurityとCoverageの値が両方0.9台でもDERが高いことがあります。なぜですか?

PurityとCoverageは「時間ベースの割合」で計算されるため、短い混同が多く発生する(Confusionが高い)ケースでDERが高くなる。また、DERにはFAとMISSも含まれるため、それらの成分が高い場合もある。DERの成分を分解して原因を特定するのが先決だ。


まとめ

指標何を測るか主な用途
DER総合的な誤り率論文比較・ベンチマーク
JER話者を均等に扱った誤り率多話者公平評価
Confusion Matrix話者の混同パターンデバッグ・分析
Purity / Coverageクラスタの純粋度と網羅性過/未分割の診断
Count Error話者数推定精度クラスタリング評価
Boundary Error境界検出精度セグメンテーション評価
Utterance Length Recall短い発話の再現率VAD・実用性評価

DER単体でシステムを評価するのは「体温だけで健康診断する」ようなものだ。本記事で紹介した6つの指標を組み合わせることで、システムのどこに問題があるかを正確に診断し、改善の方向性を絞り込める。次のステップとして、自分のデータセットで各指標を計算し、診断フローに従って改善サイクルを回してみてほしい。

関連するブログ

この記事に近いテーマのブログをピックアップしています。