MarkTechPost
Điểm AI 44/100

Hướng dẫn

Hướng dẫn từ Google Research: Xây dựng và đánh giá Sound Encoder trên chuẩn MSEB

(giờ Việt Nam)

Tóm tắt AI

Bài hướng dẫn chi tiết cách lập trình Sound Encoder theo chuẩn MSEB 0.1.0 và thực hiện đánh giá đa nhiệm trên các tác vụ phân loại, phân cụm, truy xuất và phân đoạn âm thanh.

Chính văn · Bản dịch AI

A Coding Guide to Google Research’s MSEB: Writing Sound Encoders to the Benchmark Contract and Scoring Them Across Classification, Clustering, Retrieval and Segmentation

Trong hướng dẫn này, chúng ta làm việc với MSEB, Massive Sound Embedding Benchmark từ Google Research, và tiếp cận nó từ góc độ ý nghĩa thực sự của một con số trên bảng xếp hạng: bề mặt đánh giá (evaluator surface). Chúng ta cài đặt gói và ánh xạ ba lớp của nó, sau đó viết hai bộ mã hóa (encoder) cố tình khác biệt dựa trên lớp cơ sở trừu tượng của khung làm việc: một bộ đo độ lớn theo thời gian và một bộ đo âm sắc, rồi mã hóa một tập dữ liệu tổng hợp nhỏ mà chúng ta tạo ra trong notebook để không cần phải tải xuống bất cứ thứ gì. Chúng ta vận hành các bộ đánh giá phân loại, phân cụm, truy xuất và phân đoạn trên các embedding đó, gọi trực tiếp các hàm đo lường để xem mỗi bộ đánh giá ưu tiên điều gì, và kết thúc bằng việc lắp ráp TaskMetadata mà một bài nộp thực tế cần có. Kết quả là một sự so sánh trong đó hai bộ mã hóa hoán đổi vị trí tùy thuộc vào bộ đánh giá nào được sử dụng, đây chính là lập luận cho một tiêu chuẩn đa nhiệm được thể hiện bằng con số thay vì văn bản.

Mã
import os
import sys
import json
import math
import traceback
import subprocess
import numpy as np
 
RESULTS = {}
BENCH = {}
 
 
def banner(title):
    print("\n" + "=" * 78)
    print(title)
    print("=" * 78)
 
 
def section(name):
    def wrap(fn):
        def run(*a, **kw):
            banner(name)
            try:
                out = fn(*a, **kw)
                RESULTS[name] = out if isinstance(out, str) else "ok"
                return out
            except Exception as e:
                RESULTS[name] = f"SKIPPED / FAILED -> {type(e).__name__}: {e}"
                print(f"\n[!] {name} did not complete: {type(e).__name__}: {e}")
                traceback.print_exc(limit=3)
                return None
        return run
    return wrap
 
 
banner("0. Install MSEB and map the three layers we will use")
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "mseb==0.1.0"], check=True)
 
import mseb
from mseb import types, encoder as encoder_lib, evaluator as evaluator_lib, metrics
from mseb.evaluators import (
    classification_evaluator,
    clustering_evaluator,
    retrieval_evaluator,
    segmentation_evaluator,
)
 
print(f"  mseb {mseb.__version__}  |  Python {sys.version.split()[0]}  |  numpy {np.__version__}")
print("\n  MSEB is three layers, and a benchmark run walks down them:")
print("    types      -> Sound, SoundEmbedding, Score, TaskMetadata: the shapes every task speaks")
print("    encoder    -> MultiModalEncoder: the contract YOUR model implements")
print("    evaluators -> classification, clustering, retrieval, reranking, transcription, segmentation, ...")
print("\n  evaluator entry points we will drive:")
for module, cls in [(classification_evaluator, "ClassificationEvaluator"),
                    (clustering_evaluator, "ClusteringEvaluator"),
                    (retrieval_evaluator, "RetrievalEvaluator"),
                    (segmentation_evaluator, "SegmentationEvaluator")]:
    print(f"    {module.__name__.split('.')[-1]:28s} {cls}")
print("\n  Everything below runs on CPU with no dataset download: we synthesise the audio.")

Chúng ta cài đặt mseb và nhập ba lớp mà một lần chạy benchmark sẽ đi qua. Mô-đun types chứa các hình dạng (shapes) mà mọi tác vụ đều sử dụng, bao gồm Sound, SoundEmbedding, Score và TaskMetadata; mô-đun encoder chứa MultiModalEncoder, hợp đồng mà mô hình của chúng ta thực thi; và gói evaluators chứa một mô-đun cho mỗi nhóm tác vụ. Chúng ta chỉ nhập bốn bộ đánh giá mà notebook này sử dụng, vì các mô-đun phân loại, phân cụm, truy xuất và phân đoạn không phụ thuộc vào bất kỳ thư viện nào nặng hơn NumPy và scikit-learn. Ngược lại, các bộ đánh giá xếp hạng lại (reranking) và phiên âm (transcription) kéo theo Whisper, còn trình chạy tác vụ kéo theo TensorFlow và apache-beam. Do đó, mọi thứ bên dưới đều chạy trên môi trường CPU miễn phí mà không cần tải xuống tập dữ liệu hay sử dụng bộ tăng tốc.

Mã
SR = 16000
 
 
@section("1. The type contract: Sound, SoundEmbedding, Score")
def type_contract():
    t = np.arange(SR) / SR
    waveform = (0.5 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)
    sound = types.Sound(
        waveform=waveform,
        context=types.SoundContextParams(id="demo_000", sample_rate=SR, length=len(waveform),
                                         language="en_us", text="a 440 Hz tone"),
    )
    print(f"  Sound          id={sound.context.id!r}  {sound.waveform.shape} @ {sound.context.sample_rate} Hz"
          f"  -> {sound.size_bytes:,} bytes")
 
    embedding = types.SoundEmbedding(
        embedding=np.zeros((1, 16), dtype=np.float32),            # (N, D): one utterance-level vector
        timestamps=np.array([[0.0, 1.0]], dtype=np.float32),      # (M, 2): [start, end] in seconds
        context=sound.context,
        encoding_stats=types.EncodingStats(input_size_bytes=sound.size_bytes, embedding_size_bytes=16 * 4),
    )
    print(f"  SoundEmbedding embedding{embedding.embedding.shape}  timestamps{embedding.timestamps.shape}"
          f"  -> {embedding.size_bytes} bytes")
    print(f"                 compression_ratio = {embedding.encoding_stats.compression_ratio:.5f}"
          f"  ({1 / embedding.encoding_stats.compression_ratio:,.0f}x smaller than the audio)")
    print("  N embeddings and M timestamps: M == N is frame-aligned, M == 1 is utterance-level.")
    print("  `embedding` may also hold N strings instead of vectors - step 8 uses exactly that.")
 
    score = types.Score(metric="Accuracy", description="Overall classification accuracy",
                        value=0.875, min=0.0, max=1.0)
    print(f"\n  Score          {score.metric}={score.value} in [{score.min}, {score.max}] :: {score.description}")
    for bad, why in [(dict(metric="", description="d", value=0.5, min=0.0, max=1.0), "empty metric name"),
                     (dict(metric="m", description="d", value=0.5, min=1.0, max=0.0), "min > max")]:
        try:
            types.Score(**bad)
        except Exception as e:
            print(f"  rejected at construction ({why}): {type(e).__name__}: {e}")
    return f"Sound {sound.size_bytes:,} B -> embedding {embedding.size_bytes} B"
 
 
type_contract()

Chúng ta bắt đầu với hợp đồng kiểu dữ liệu (type contract), vì mọi lớp khác đều được thể hiện trong đó. Một Sound mang theo dạng sóng (waveform), cùng với SoundContextParams, định danh, tốc độ lấy mẫu, độ dài, ngôn ngữ và bản phiên âm tùy chọn, những thứ này theo sát âm thanh trong suốt toàn bộ pipeline. Một SoundEmbedding mang theo một mảng gồm N embedding và một mảng gồm M cặp dấu thời gian, và mối quan hệ giữa N và M chính là từ vựng của benchmark: M bằng N nghĩa là một vector cho mỗi khung hình, trong khi M bằng một nghĩa là một vector cấp độ phát âm duy nhất, đây là kết quả mà các bộ mã hóa của chúng ta tạo ra. EncodingStats ghi lại kích thước đầu vào và embedding, đồng thời hiển thị compression_ratio, ở đây là mức giảm gấp nghìn lần từ âm thanh sang vector. Một Score là tên chỉ số, giá trị và giới hạn của nó, và nó tự xác thực khi khởi tạo, từ chối tên chỉ số trống hoặc giá trị tối thiểu lớn hơn tối đa, vì vậy một con số sai định dạng không thể lọt vào bảng xếp hạng. Trường embedding cũng chấp nhận N chuỗi thay vì N vector, đây là cánh cửa mà bước 8 sẽ đi qua.

Mã
class EnergyEnvelopeEncoder(encoder_lib.MultiModalEncoder):
    """Baseline: average energy in `n_bins` equal time slices. Loud/quiet, nothing about timbre."""
 
    def __init__(self, n_bins: int = 16):
        super().__init__()
        self.n_bins = n_bins
 
    def _setup(self):
        self._ready = True                                    # a real encoder loads weights here
 
    def _check_input_types(self, batch):
        for item in batch:
            if not isinstance(item, types.Sound):
                raise ValueError(f"{type(self).__name__} takes types.Sound, got {type(item).__name__}")
 
    def _encode(self, batch) -> list[types.SoundEmbedding]:
        out = []
        for sound in batch:
            slices = np.array_split(sound.waveform.astype(np.float32), self.n_bins)
            vec = np.array([[float(np.sqrt(np.mean(s ** 2) + 1e-12)) for s in slices]], dtype=np.float32)
            vec /= np.linalg.norm(vec) + 1e-9
            out.append(types.SoundEmbedding(
                embedding=vec,
                timestamps=np.array([[0.0, sound.context.length / sound.context.sample_rate]], dtype=np.float32),
                context=sound.context))
        return out
 
 
class SpectralProfileEncoder(encoder_lib.MultiModalEncoder):
    """Contender: mean log-magnitude spectrum pooled into `n_bands` bands. Describes timbre."""
 
    def __init__(self, n_bands: int = 16, frame: int = 512):
        super().__init__()
        self.n_bands, self.frame = n_bands, frame
 
    def _setup(self):
        self._window = np.hanning(self.frame).astype(np.float32)
 
    def _check_input_types(self, batch):
        for item in batch:
            if not isinstance(item, types.Sound):
                raise ValueError(f"{type(self).__name__} takes types.Sound, got {type(item).__name__}")
 
    def _encode(self, batch) -> list[types.SoundEmbedding]:
        out = []
        for sound in batch:
            w = sound.waveform.astype(np.float32)
            n_frames = max(1, len(w) // self.frame)
            spectra = [np.abs(np.fft.rfft(w[i * self.frame:(i + 1) * self.frame] * self._window))
                       for i in range(n_frames)]
            mean_spectrum = np.log1p(np.mean(spectra, axis=0))
            vec = np.array([[float(b.mean()) for b in np.array_split(mean_spectrum, self.n_bands)]],
                           dtype=np.float32)
            vec /= np.linalg.norm(vec) + 1e-9
            out.append(types.SoundEmbedding(
                embedding=vec,
                timestamps=np.array([[0.0, sound.context.length / sound.context.sample_rate]], dtype=np.float32),
                context=sound.context))
        return out
 
 
@section("2. The encoder contract: three methods, and the framework does the rest")
def encoder_contract():
    print("  MultiModalEncoder abstract methods a subclass must implement:")
    for name in sorted(encoder_lib.MultiModalEncoder.__abstractmethods__):
        print(f"    {name}")
    print("  final (framework-owned, do not override): setup(), encode()")
 
    t = np.arange(SR) / SR
    fade = np.exp(-2.5 * t).astype(np.float32)                # a decaying note, so the envelope is not flat
    sound = types.Sound(waveform=(0.5 * fade * np.sin(2 * np.pi * 440 * t)).astype(np.float32),
                        context=types.SoundContextParams(id="demo_000", sample_rate=SR, length=SR))
    for enc in (EnergyEnvelopeEncoder(), SpectralProfileEncoder()):
        enc.setup()
        emb = enc.encode([sound])[0]
        stats = emb.encoding_stats                            # attached by encode(), not by our code
        print(f"\n  {type(enc).__name__:24s} -> {emb.embedding.shape} {emb.embedding.dtype}"
              f"   output_type={enc.output_type().__name__}")
        print(f"  {'':24s}    EncodingStats(input={stats.input_size_bytes:,} B, "
              f"embedding={stats.embedding_size_bytes} B, flops={stats.flops})")
        print(f"  {'':24s}    first 6 dims: {np.round(emb.embedding[0][:6], 3)}")
    print("\n  The envelope encoder sees the note decay; the spectral encoder sees one peak at 440 Hz.")
 
    try:
        EnergyEnvelopeEncoder().encode(["not a Sound"])
    except ValueError as e:
        print(f"\n  wrong input type is caught by _check_input_types: {e}")
    return "two encoders satisfying MultiModalEncoder"
 
 
encoder_contract()

Chúng ta viết hai bộ mã hóa bằng cách kế thừa MultiModalEncoder, với đúng ba phương thức trừu tượng: _setup tải bất cứ thứ gì mô hình cần, _check_input_types từ chối bất kỳ thứ gì không phải là Sound, và _encode biến một lô (batch) thành các đối tượng SoundEmbedding. Khung làm việc sở hữu setup và encode, và encode là thứ gắn EncodingStats vào mọi kết quả, vì vậy mã của chúng ta không bao giờ phải tự điền thủ công. EnergyEnvelopeEncoder tính trung bình năng lượng trong mười sáu lát thời gian bằng nhau và do đó chỉ mô tả cách độ lớn thay đổi; SpectralProfileEncoder gộp phổ log-magnitude trung bình thành mười sáu dải tần và do đó mô tả âm sắc. Cả hai đều chuẩn hóa L2 đầu ra của chúng để tích vô hướng trở thành cosine. Mã hóa một nốt nhạc đang tắt dần qua từng bộ cho thấy sự khác biệt ngay lập tức: bộ mã hóa bao (envelope) thấy sự tắt dần, và bộ mã hóa phổ (spectral) thấy một đỉnh duy nhất tại 440 Hz.

Mã
CLASSES = ["tone", "chirp", "noise"]
N_PER_CLASS = 12
 
 
def synthesize(kind: str, index: int, take: int) -> types.Sound:
    """One second of audio. `take` 0 is the document, take 1 is a noisier recording of the SAME clip.
    Two cues are deliberately separated: the spectrum says which class it is, and the amplitude
    envelope - drawn per item, independent of class - says which item it is.
    """
    item = np.random.default_rng(1000 + CLASSES.index(kind) * 100 + index)
    control = 0.25 + 0.75 * item.random(8)
    envelope = np.interp(np.linspace(0, 7, SR), np.arange(8), control).astype(np.float32)
 
    t = np.arange(SR) / SR
    if kind == "tone":
        w = np.sin(2 * np.pi * (380 + 80 * item.random()) * t)
    elif kind == "chirp":
        f0, f1 = 200 + 50 * item.random(), 3200 + 400 * item.random()
        w = np.sin(2 * np.pi * (f0 * t + 0.5 * (f1 - f0) * t ** 2))
    else:
        w = item.standard_normal(SR)
    w /= np.sqrt(np.mean(w ** 2)) + 1e-9                      # unit RMS: the envelope is the only loudness cue
 
    take_rng = np.random.default_rng(50_000 + take * 10_000 + CLASSES.index(kind) * 100 + index)
    w = (0.4 + 0.2 * take_rng.random()) * envelope * (w + 0.02 * take_rng.standard_normal(SR))
    return types.Sound(waveform=w.astype(np.float32), context=types.SoundContextParams(
        id=f"{kind}_{index:02d}" + ("" if take == 0 else "_take2"), sample_rate=SR,
        length=SR, language="en_us", text=kind))
 
 
@section("3. A synthetic corpus, encoded into MSEB embedding caches")
def build_corpus():
    corpus = [synthesize(k, i, 0) for k in CLASSES for i in range(N_PER_CLASS)]
    queries = [synthesize(k, i, 1) for k in CLASSES for i in range(N_PER_CLASS)]
    labels = {s.context.id: s.context.text for s in corpus + queries}
    print(f"  {len(corpus)} documents + {len(queries)} second takes of the same clips,"
          f" {len(CLASSES)} classes, 1.0s each @ {SR} Hz")
 
    caches, query_caches = {}, {}
    for enc in (EnergyEnvelopeEncoder(), SpectralProfileEncoder()):
        enc.setup()
        embeddings = enc.encode(corpus)                        # one batched call, like a real runner
        caches[type(enc).__name__] = {e.context.id: e for e in embeddings}
        query_caches[type(enc).__name__] = {e.context.id: e for e in enc.encode(queries)}
 
        matrix = np.vstack([e.embedding for e in embeddings])
        within, between = [], []
        for i in range(len(corpus)):
            for j in range(i + 1, len(corpus)):
                sim = float(matrix[i] @ matrix[j])
                (within if labels[corpus[i].context.id] == labels[corpus[j].context.id] else between).append(sim)
        print(f"  {type(enc).__name__:24s} cache of {len(embeddings)} embeddings, dim {matrix.shape[1]}"
              f"   mean cosine: same-class {np.mean(within):.3f} vs other-class {np.mean(between):.3f}"
              f"   (gap {np.mean(within) - np.mean(between):+.3f})")
 
    print("\n  Read that gap as a prediction: only the spectral encoder separates the classes at all.")
    print("  Steps 4-6 check whether the evaluators agree - and whether the gap is the whole story.")
    globals().update(CORPUS=corpus, QUERIES=queries, LABELS=labels, CACHES=caches, QCACHES=query_caches)
    return f"{len(corpus)} documents + {len(queries)} queries encoded by 2 encoders"
 
 
build_corpus()

Chúng ta tổng hợp một tập dữ liệu trong đó hai tín hiệu được tách biệt một cách cố ý. Phổ cho biết một đoạn âm thanh thuộc lớp nào, như âm sắc, tiếng rít hay tiếng ồn, trong khi bao biên độ (amplitude envelope) được vẽ cho từng mục và độc lập với lớp, vì vậy nó xác định đó là đoạn âm thanh nào mà không nói gì về việc nó là cái gì. Chúng ta chuẩn hóa mọi dạng sóng về RMS đơn vị trước khi áp dụng bao biên độ, để bao biên độ trở thành tín hiệu độ lớn duy nhất. Chúng ta kết xuất mỗi mục trong số ba mươi sáu mục hai lần, một lần là tài liệu gốc và một lần là bản thu thứ hai có nhiều nhiễu hơn của cùng đoạn đó, và mã hóa cả hai tập hợp bằng cả hai bộ mã hóa vào các bộ nhớ đệm embedding MSEB, các từ điển đơn giản từ id âm thanh đến SoundEmbedding mà mọi bộ đánh giá đều tiêu thụ. Độ tương đồng cosine trung bình cùng lớp và khác lớp được in ra ở đây đóng vai trò như một dự đoán cho ba bước tiếp theo: chỉ bộ mã hóa phổ mới phân tách được các lớp.

Mã
def class_prototypes(cache, labels):
    """Class embedding table (C, D): the mean unit vector of each class, as the evaluator's `weights`."""
    rows = []
    for name in CLASSES:
        vecs = np.vstack([cache[i].embedding for i in cache if labels[i] == name])
        mean = vecs.mean(axis=0)
        rows.append(mean / (np.linalg.norm(mean) + 1e-9))
    return np.vstack(rows).astype(np.float32)
 
 
@section("4. ClassificationEvaluator: prototypes in, Score objects out")
def classification():
    table, example = {}, None
    for name, cache in CACHES.items():
        evaluator = classification_evaluator.ClassificationEvaluator(
            class_labels=CLASSES,
            weights=class_prototypes(cache, LABELS),
            distance_fn=evaluator_lib.dot_product,             # embeddings are L2-normalised -> cosine
            top_k_value=2,
        )
        predictions = evaluator.compute_predictions(cache)     # {id: per-class score vector}
        references = [classification_evaluator.ClassificationReference(i, LABELS[i]) for i in cache]
        table[name] = {s.metric: s.value for s in evaluator.compute_metrics(predictions, references)}
        if name == "SpectralProfileEncoder":
            key = next(iter(predictions))
            example = (key, np.round(list(predictions[key]), 3))
 
    metric_names = list(next(iter(table.values())))[:6]
    print(f"  {'encoder':26s}" + "".join(f"{m[:14]:>16s}" for m in metric_names))
    for name, row in table.items():
        print(f"  {name:26s}" + "".join(f"{row[m]:16.3f}" for m in metric_names))
 
    print(f"\n  compute_predictions returns one raw score per class, e.g. {example[0]!r} -> {example[1]}")
    print(f"  ({CLASSES} - the argmax is the prediction, and top_k_value=2 also scores Top-2 Accuracy.)")
    print("  compute_metrics turns those into types.Score objects, which is what the leaderboard stores.")
    for name, row in table.items():
        BENCH.setdefault(name, {})["Accuracy"] = row["Accuracy"]
    winner = max(table, key=lambda k: table[k]["Accuracy"])
    return "Accuracy: " + ", ".join(f"{k} {v['Accuracy']:.3f}" for k, v in table.items()) + f" (winner {winner})"
 
 
classification()

ClassificationEvaluator lấy một bảng các embedding lớp làm trọng số và một hàm khoảng cách, và chúng ta xây dựng các trọng số dưới dạng các nguyên mẫu lớp (class prototypes), là vector đơn vị trung bình của mỗi lớp. Hai phương thức của nó tách biệt rõ ràng: compute_predictions trả về điểm thô cho mỗi lớp đối với mọi embedding trong bộ nhớ đệm, và compute_metrics biến chúng cùng với các nhãn ClassificationReference thành danh sách các đối tượng Score mà bảng xếp hạng lưu trữ. Việc đặt top_k_value thành hai sẽ thêm Top-2 Accuracy bên cạnh độ chính xác (accuracy), độ chính xác cân bằng (balanced accuracy) và precision, recall, F1 có trọng số. Bộ mã hóa phổ phân loại tập dữ liệu một cách hoàn hảo, và bộ mã hóa bao đạt kết quả cao hơn mức ngẫu nhiên nhưng thấp hơn nhiều, đây chính là thứ tự mà khoảng cách cosine đã dự đoán.

Mã
@section("5. ClusteringEvaluator: no labels at encode time, V-measure at score time")
def clustering():
    evaluator = clustering_evaluator.ClusteringEvaluator()
    examples = [clustering_evaluator.ClusteringExample(sound_id=i, label=LABELS[i])
                for i in next(iter(CACHES.values()))]
    print(f"  {len(examples)} examples, KMeans with k = {len(CLASSES)} (inferred from the labels)")
    for name, cache in CACHES.items():
        np.random.seed(0)                                      # MiniBatchKMeans takes no random_state here:
        scores = evaluator(cache, examples)                    # it falls back to NumPy's global RNG, so pin that
                                                               # or an unstructured embedding space scores 0.01-0.08
                                                               # at random. The evaluator is callable.
        BENCH.setdefault(name, {})["VMeasure"] = scores[0].value
        print(f"  {name:26s} {scores[0].metric:12s} {scores[0].value:6.3f}"
              f"   [{scores[0].min}, {scores[0].max}]  :: {scores[0].description}")
    print("\n  V-measure is the harmonic mean of homogeneity and completeness: 1.0 means the clusters")
    print("  recover the classes exactly, 0.0 means they carry no information about them. Note how much")
    print("  harsher it is on the envelope encoder than accuracy was - clustering gets no labels to lean on.")
    return ", ".join(f"{k} V={v['VMeasure']:.3f}" for k, v in BENCH.items())
 
 
clustering()

ClusteringEvaluator đặt ra câu hỏi khó hơn tương tự, vì nó không bao giờ thấy nhãn tại thời điểm mã hóa: nó chạy KMeans trên bộ nhớ đệm. Nó chấm điểm các cụm dựa trên các nhãn bằng V-measure, trung bình điều hòa của độ thuần nhất (homogeneity) và độ đầy đủ (completeness). Khoảng cách giữa hai bộ mã hóa nới rộng đáng kể ở đây so với phân loại, vì việc đọc nguyên mẫu có giám sát có thể khai thác một tín hiệu mờ nhạt mà phân cụm không giám sát không thể tự tìm thấy. Một chi tiết thực tế đáng để sao chép vào bất kỳ lần chạy benchmark có thể tái lập nào: bộ đánh giá xây dựng MiniBatchKMeans mà không có random_state, vì vậy nó quay lại trình tạo ngẫu nhiên toàn cục của NumPy, và nếu không gieo hạt (seed) cho trình tạo đó, một không gian embedding không cấu trúc sẽ có điểm số dao động từ khoảng 0.01 đến 0.08 giữa các lần chạy.

Mã
@section("6. RetrievalEvaluator: index the corpus, query it with a second take, score the ranking")
def retrieval():
    print("  Task: each query is a NOISIER RECORDING OF ONE DOCUMENT, and exactly one document is correct.")
    print("  This is identity, not category - a different question from steps 4 and 5.\n")
    out = {}
    for name, cache in CACHES.items():
        doc_ids = list(cache)
        docs = np.vstack([cache[i].embedding for i in doc_ids]).astype(np.float32)
        queries = QCACHES[name]
 
        searcher = retrieval_evaluator.BruteForceSearcher(candidates=docs, num_neighbors=10)
        evaluator = retrieval_evaluator.RetrievalEvaluator(searcher=searcher, id_by_index_id=doc_ids, top_k=5)
        predictions = evaluator.compute_predictions(queries)
        references = [retrieval_evaluator.RetrievalReferenceId(
            sound_id=q, reference_id=q.removesuffix("_take2")) for q in queries]
        out[name] = {s.metric: s.value for s in evaluator.compute_metrics(predictions, references)}
 
        q0 = next(iter(queries))
        top = [item["id"] for item in predictions[q0].items[:5]]
        print(f"  top-5 for query {q0!r} under {name}:")
        print(f"    {top}")
        print(f"    correct document at rank {top.index(q0.removesuffix('_take2')) + 1}"
              f"   |  neighbours of the same class: {sum(LABELS[i] == LABELS[q0] for i in top)}/5\n")
 
    metric_names = ["MRR", "EM", "RecallAt5", "NDCG@10"]
    print(f"  {'encoder':26s}" + "".join(f"{m:>14s}" for m in metric_names))
    for name, row in out.items():
        print(f"  {name:26s}" + "".join(f"{row[m]:14.3f}" for m in metric_names))
        BENCH.setdefault(name, {})["MRR"] = row["MRR"]
    print("\n  MRR is 1/rank of the correct document, EM is 'it was rank 1', RecallAt5 is 'it was in the")
    print("  top 5'. NDCG@10 here is graded credit for the same single relevant document.")
    return ", ".join(f"{k} MRR={v['MRR']:.3f}" for k, v in out.items())
 
 
retrieval()

RetrievalEvaluator trả lời một câu hỏi khác với hai bộ trước đó, và chúng ta thiết lập tác vụ để sự khác biệt đó trở nên rõ ràng. Mỗi truy vấn là bản thu thứ hai có nhiều nhiễu hơn của đúng một tài liệu, vì vậy mục tiêu là nhận dạng thay vì phân loại. Chúng ta lập chỉ mục các embedding tài liệu trong một BruteForceSearcher, yêu cầu dự đoán trên bộ nhớ đệm truy vấn và truyền một RetrievalReferenceId cho mỗi truy vấn để chỉ định tài liệu đúng duy nhất của nó. Bộ đánh giá trả về MRR, khớp chính xác, recall tại top_k và NDCG tại mười. Kết quả đảo ngược hai bước trước đó: bộ mã hóa bao truy xuất mọi đoạn âm thanh ở thứ hạng một, vì bao biên độ là dấu vân tay của mục đó. Ngược lại, bộ mã hóa phổ xếp hạng tệ hơn một chút vì các đoạn cùng lớp trông giống nhau đối với nó. Danh sách top-năm được in ra làm rõ cơ chế này, một vùng lân cận ngẫu nhiên về lớp và vùng kia thuần nhất về lớp.

Mã
@section("7. The metric layer on its own: WER, CER, exact match, MRR, nDCG")
def metric_layer():
    truth = "the quick brown fox jumps over the lazy dog"
    for hypothesis in [truth, "the quick brown fox jumped over a lazy dog", "quick brown fox over lazy dog"]:
        werrors, wtotal = metrics.compute_word_errors(truth, hypothesis)
        cerrors, ctotal = metrics.compute_character_errors(truth, hypothesis)
        print(f"  WER {werrors / wtotal:5.3f} ({werrors}/{wtotal} words)   "
              f"CER {cerrors / ctotal:5.3f} ({cerrors}/{ctotal} chars)   {hypothesis!r}")
 
    print("\n  ranking metrics take (reference, ranked_ids):")
    ranked = ["doc_b", "doc_a", "doc_c", "doc_d"]
    for reference in ["doc_b", "doc_a", "doc_c", "doc_z"]:
        rank = ranked.index(reference) + 1 if reference in ranked else None
        print(f"    reference {reference!r:8s} rank {str(rank):4s}"
              f"  EM {metrics.compute_exact_match(reference, ranked):.1f}"
              f"  MRR {metrics.compute_reciprocal_rank(reference, ranked):.3f}"
              f"  nDCG@4 {metrics.compute_ndcg_at_k(reference, ranked, k=4):.3f}")
    print("  compute_ndcg_at_k assumes ONE relevant document and compares it by equality, so pass a")
    print("  string, not a list - a list reference silently scores 0.0 while MRR still looks fine.")
 
    print("\n  embedding-space distances used by the reconstruction and stability tasks:")
    a = np.random.default_rng(1).standard_normal((8, 4)).astype(np.float32)
    for label, b in [("identical", a), ("noisy", a + 0.1 * np.random.default_rng(2).standard_normal(a.shape))]:
        lp = metrics.compute_lp_norm(a, b, p=2)
        dtw = metrics.compute_dynamic_time_warping_distance(a, b)
        print(f"    {label:10s} L2 {json.dumps({k: round(float(v), 3) for k, v in lp.items()})}"
              f"   DTW {json.dumps({k: round(float(v), 3) for k, v in dtw.items()})}")
    return "WER/CER, EM/MRR/nDCG, Lp and DTW distances"
 
 
metric_layer()

Chúng tôi gọi trực tiếp các hàm đo lường mà không cần trình đánh giá bao quanh, vì đây là lớp mà các nhóm tác vụ dùng chung. compute_word_errors và compute_character_errors nhận vào hai chuỗi và trả về lỗi cùng tổng số riêng biệt, nhờ đó người gọi có thể quyết định cách tổng hợp dữ liệu. Các chỉ số xếp hạng nhận vào một tham chiếu và một danh sách định danh đã được xếp hạng; việc so sánh exact match, reciprocal rank và nDCG trên cùng một bảng xếp hạng cho thấy cái giá phải trả cho vị trí của từng chỉ số. Có một điểm cần lưu ý: compute_ndcg_at_k giả định chỉ có một tài liệu liên quan và so sánh dựa trên sự bằng nhau, vì vậy nếu truyền vào một danh sách các id liên quan, nó sẽ mặc định trả về điểm bằng không. Ngược lại, MRR chấp nhận một danh sách và vẫn cho kết quả chính xác. Chúng tôi kết thúc với compute_lp_norm và compute_dynamic_time_warping_distance, đây là các khoảng cách trong không gian embedding đằng sau các tác vụ tái tạo và ổn định.

Mã
@section("8. SegmentationEvaluator: scoring WHAT was said and WHERE, separately")
def segmentation():
    evaluator = segmentation_evaluator.SegmentationEvaluator(tau=0.05)
    print("  Here a 'segment' carries a TERM, not a vector: SoundEmbedding.embedding holds N strings")
    print("  and timestamps holds their N [start, end] spans. tau=0.05 -> a boundary may be 50 ms out.\n")
 
    TERMS = [("weather", 0.00, 0.30), ("in", 0.30, 0.65), ("boston", 0.65, 1.00)]
    truth = [segmentation_evaluator.Segment(embedding=term, start_time=s, end_time=e, confidence=1.0)
             for term, s, e in TERMS]
    references = [segmentation_evaluator.SegmentationReference(example_id="utt_0", segments=truth)]
 
    def prediction(spans):
        return {"utt_0": types.SoundEmbedding(
            embedding=np.array([term for term, _, _ in spans]),                        # N strings
            timestamps=np.array([[s, e] for _, s, e in spans], dtype=np.float32),      # N [start, end]
            context=types.SoundContextParams(id="utt_0", sample_rate=SR, length=SR),
            scores=np.ones(len(spans), dtype=np.float32))}                             # confidences
 
    candidates = {
        "exact": TERMS,
        "50 ms out": [("weather", 0.00, 0.28), ("in", 0.28, 0.67), ("boston", 0.67, 1.00)],
        "right words, wrong places": [("weather", 0.00, 0.45), ("in", 0.45, 0.80), ("boston", 0.80, 1.00)],
        "right places, wrong words": [("weather", 0.00, 0.30), ("on", 0.30, 0.65), ("austin", 0.65, 1.00)],
    }
    shown = ["TimestampsAccuracy", "EmbeddingsAccuracy", "TimestampsAndEmbeddingsAccuracy", "WordErrorRate", "mAP"]
    print(f"  {'prediction':28s}" + "".join(f"{m[:13]:>15s}" for m in shown))
    for label, spans in candidates.items():
        result = evaluator.compute_scores(prediction(spans), references)   # per-example scores
        scores = {s.metric: s.value for s in evaluator.compute_metrics(result)}  # aggregated Scores
        print(f"  {label:28s}" + "".join(f"{scores[m]:15.3f}" for m in shown))
 
    print("\n  The last two rows are the point: one metric cannot tell 'knew the words, missed the timing'")
    print("  from 'nailed the timing, heard the wrong words'. Timestamps and embeddings are scored apart,")
    print("  and only TimestampsAndEmbeddings credits getting both right at once.")
    return "boundary + term scoring at tau=50 ms"
 
 
segmentation()

SegmentationEvaluator chấm điểm nội dung được nói và vị trí được nói như các đại lượng riêng biệt, đồng thời sử dụng dạng chuỗi của SoundEmbedding đã đề cập ở bước 1: mảng embedding chứa một thuật ngữ cho mỗi phân đoạn và mảng timestamps chứa khoảng thời gian của chúng. Quy trình này gồm hai giai đoạn: compute_scores trên các dự đoán và tham chiếu trước, sau đó là compute_metrics trên kết quả đó. Chúng tôi chấm điểm bốn phân đoạn ứng viên của cùng một cụm từ so với một ground truth với sai số cho phép là 50 mili giây. Cả kết quả chính xác và kết quả lệch 50 mili giây đều đạt điểm tuyệt đối, đó chính là mục đích của sai số cho phép. Hai hàng cuối cùng rút ra bài học: đúng từ nhưng sai vị trí sẽ đạt điểm một cho embedding và điểm không cho timestamps, đúng vị trí nhưng sai từ thì ngược lại, và chỉ có chỉ số kết hợp mới ghi nhận việc thực hiện đúng cả hai cùng lúc.

Mã
@section("9. TaskMetadata and a leaderboard that disagrees with itself")
def task_metadata():
    cache = CACHES["SpectralProfileEncoder"]
    evaluator = classification_evaluator.ClassificationEvaluator(
        class_labels=CLASSES, weights=class_prototypes(cache, LABELS), top_k_value=2)
    references = [classification_evaluator.ClassificationReference(i, LABELS[i]) for i in cache]
    scores = [s for s in evaluator.compute_metrics(evaluator.compute_predictions(cache), references)
              if s.metric in ("Accuracy", "Weighted F1-Score")]
 
    metadata = types.TaskMetadata(
        name="SyntheticToneClassification",
        description="Three-way classification of synthetic tones, chirps and noise",
        reference="https://github.com/google-research/mseb",
        type="Classification",
        category="sound",
        main_score="Accuracy",
        revision="1",
        dataset=types.Dataset(path="synthetic/in-notebook", revision="1"),
        scores=scores,
        eval_splits=["test"],
        eval_langs=["en_us"],
    )
    print(f"  TaskMetadata: {metadata.name}  type={metadata.type}  main_score={metadata.main_score!r}")
    print(f"                dataset={metadata.dataset.path!r} rev {metadata.dataset.revision}"
          f"  splits={metadata.eval_splits}  langs={metadata.eval_langs}")
    print(f"                scores={[f'{s.metric}={s.value:.3f}' for s in metadata.scores]}")
    try:
        types.TaskMetadata(**{**{f.name: getattr(metadata, f.name) for f in metadata.__dataclass_fields__.values()},
                              "scores": []})
    except Exception as e:
        print(f"  validated at construction: {type(e).__name__}: {e}")
 
    columns = ["Accuracy", "VMeasure", "MRR"]
    print(f"\n  One row per encoder, one column per task family:")
    print(f"  {'encoder':26s}" + "".join(f"{c:>12s}" for c in columns) + "     what it measures")
    for name, row in BENCH.items():
        print(f"  {name:26s}" + "".join(f"{row[c]:12.3f}" for c in columns)
              + ("     timbre -> class" if "Spectral" in name else "     loudness over time -> identity"))
    flips = [c for c in columns
             if (max(BENCH, key=lambda n: BENCH[n][c]) != max(BENCH, key=lambda n: BENCH[n]["Accuracy"]))]
    print(f"\n  The winner changes column to column ({', '.join(flips)} goes the other way). That is the whole")
    print("  argument for a MASSIVE benchmark: a single headline number would have hidden it. An encoder")
    print("  that cannot name a sound can still recognise it, and vice versa.")
    print("\n  A real submission runs mseb.runner over an mseb.task against a published dataset and writes")
    print("  these same Score objects to JSON; the layers above are exactly what it exercises.")
    return f"TaskMetadata + {len(BENCH)} encoders x {len(columns)} task families"
 
 
task_metadata()

Chúng tôi tập hợp TaskMetadata mà một bài nộp thực tế cần có, bao gồm tên, loại, danh mục, điểm chính, đường dẫn và phiên bản tập dữ liệu, các phân đoạn đánh giá và ngôn ngữ, cùng với các đối tượng Score. Nó sẽ xác thực khi khởi tạo giống như cách Score thực hiện, từ chối danh sách điểm trống. Sau đó, chúng tôi đưa tất cả kết quả vào một bảng: mỗi hàng cho một encoder và mỗi cột cho một nhóm tác vụ. Người chiến thắng thay đổi tùy theo cột: encoder không thể gọi tên âm thanh vẫn nhận diện được nó, và encoder gọi tên mọi âm thanh chính xác lại gây nhầm lẫn giữa các đoạn clip thuộc về nhau. Một con số tiêu đề duy nhất sẽ che giấu hoàn toàn điều đó, đây chính là lý do cho một bộ benchmark đồ sộ về tác vụ thay vì chỉ về dữ liệu.

Mã
banner("SUMMARY")
for name, res in RESULTS.items():
    print(f"  {name:<74s} {res}")
print("""
Where to go next
 - Swap in a real encoder: mseb/encoders/ ships wav2vec, Whisper, CLAP, EnCodec and SoundStream
   wrappers, plus CascadeEncoder for speech-to-text-to-embedding chains. Only the three methods
   from step 2 change; every evaluator above keeps working.
 - Run a published task: mseb.runner drives mseb.task over a real dataset with apache-beam; the
   task families live in mseb/tasks/ (classification, retrieval, reranking, transcription,
   segmentation, clustering, reasoning, brain_encoding, stability).
 - Compare against the leaderboard: https://huggingface.co/spaces/google/mseb-leaderboard
 - Read the contract you implemented: mseb/encoder.py and mseb/evaluator.py are ~500 lines total.
""")

Phần tóm tắt in ra kết quả một dòng mà mỗi phần trả về, sau đó chỉ ra ba hướng mà notebook này mở ra: thay thế bằng một trong các encoder thực tế có trong gói, như wav2vec, Whisper, CLAP, EnCodec, SoundStream hoặc trình bao bọc cascade (chỉ thay đổi ba phương thức từ bước 2 và giữ nguyên mọi trình đánh giá); chạy một tác vụ đã công bố thông qua mseb.runner trên một tập dữ liệu thực; và so sánh kết quả với bảng xếp hạng công khai.

Tóm lại, chúng tôi coi MSEB là một hợp đồng cộng với một tập hợp các trình đánh giá, và vận hành nó từ đầu đến cuối mà không cần tải xuống tập dữ liệu hay chạm vào bộ tăng tốc. Việc triển khai ba phương thức là đủ để biến mã nguồn của chúng tôi thành một phần quan trọng của benchmark, và khung làm việc đã xử lý việc tạo batch, thống kê và xác thực từ đó. Các trình đánh giá đặt ra những câu hỏi thực sự khác biệt cho cùng một embedding: phân loại và phân cụm hỏi âm thanh là gì, truy xuất hỏi đó là âm thanh nào, và phân đoạn hỏi nội dung được nói là gì và ở đâu, được chấm điểm riêng biệt để lỗi thời gian và lỗi nhận diện không bao giờ bị che giấu bên trong một mức trung bình. Hai encoder của chúng tôi thay đổi vị trí tùy thuộc vào câu hỏi được đặt ra, và chúng tôi giữ kết quả đó, vì một con số duy nhất không thể xếp hạng một sound embedding. Bước tiếp theo là thay thế các encoder thử nghiệm bằng encoder thực tế và chạy lại các trình đánh giá tương tự, vì không có mã chấm điểm nào ở trên thay đổi khi các embedding được cải thiện.

Xem Kho lưu trữ GitHub với mã nguồn đầy đủ. Mọi công lao thuộc về nhà nghiên cứu của dự án này. Ngoài ra, hãy thoải mái theo dõi chúng tôi trên Twitter và đừng quên tham gia SubReddit 150k+ ML của chúng tôi và đăng ký Bản tin của chúng tôi. Khoan đã! Bạn có dùng Telegram không? bây giờ bạn cũng có thể tham gia cùng chúng tôi trên Telegram.

Cần hợp tác với chúng tôi để quảng bá Kho lưu trữ GitHub hoặc Trang Hugging Face hoặc Phát hành sản phẩm hoặc Hội thảo trực tuyến, v.v. của bạn? Kết nối với chúng tôi

Sana Hassan

Sana Hassan, thực tập sinh tư vấn tại Marktechpost và sinh viên bằng kép tại IIT Madras, có niềm đam mê áp dụng công nghệ và AI để giải quyết các thách thức trong thế giới thực. Với sự quan tâm sâu sắc đến việc giải quyết các vấn đề thực tiễn, anh mang đến một góc nhìn mới mẻ cho sự giao thoa giữa AI và các giải pháp đời sống.

Bài viết được AI dịch và tổng hợp tự động từ MarkTechPost. Liên kết bài gốc ở phía trên. Dữ liệu đồng bộ qua API công khai được ghi nguồn tại AI HOT (canonical) ↗. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.

Hướng dẫn từ Google Research: Xây dựng và đánh giá Sound Encoder trên chuẩn MSEB | AIHOT.vn