📖

Pythonで音声分類してみる ― MFCCを使ったシンプルな機械学習入門

に公開

Pythonで音声分類してみる ― MFCCを使った音声分類モデルをつくる(前半)

はじめに

ここまでの章で音声の特徴量(MFCC)を学びました。

この章では、MFCCを使って音声を分類するモデルを作ります。「この音は何の音か」を機械学習で判別します。


必要なライブラリ

import numpy as np
from scipy.signal import stft
from scipy.fft import dct
from scipy.io.wavfile import write
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix

データの準備

分類する音声を用意します。自分で録音したwavファイルがある場合はそちらを使ってください。ここではテスト用の音声を生成します。

重要:分類が簡単すぎる条件は避ける

周波数が大きく異なる音(220Hz/440Hz/880Hz)を使うと、データが少なくても・特徴量がシンプルでも簡単に分類できてしまい、データ数や特徴量設計の効果が見えません。

ここでは以下の「難しい条件」を設定します。

  • 周波数が近い(300Hz / 350Hz / 400Hz)
  • ランダムな音量変化(エンベロープ)を加える
  • 強めのノイズを混ぜる
  • サンプルごとに周波数を±8%揺らす
sr = 16000
np.random.seed(42)

def make_voice_sample(base_freq, sr=16000, duration=1.0, seed=0):
    """
    現実の音声に近い条件で音声を生成する
    - 基本周波数をランダムに揺らす
    - 時間変化するエンベロープを加える
    - ノイズを混ぜる
    """
    rng = np.random.RandomState(seed)
    t = np.linspace(0, duration, int(sr * duration), endpoint=False)

    # 周波数を±8%の範囲でランダムに揺らす(話者の個人差を模擬)
    freq = base_freq * (1 + rng.uniform(-0.08, 0.08))

    # 倍音付きの信号
    y = (np.sin(2 * np.pi * freq * t) +
         0.7 * np.sin(2 * np.pi * freq * 2 * t) +
         0.5 * np.sin(2 * np.pi * freq * 3 * t))

    # ランダムなエンベロープ(音量の時間変化)
    envelope = np.abs(np.sin(2 * np.pi * rng.uniform(1, 5) * t)) + 0.3
    y = y * envelope

    # ノイズを加える
    y += rng.normal(0, 0.4, len(t))

    return (y / (np.max(np.abs(y)) + 1e-8)).astype(np.float32)

# 3クラスの音声(周波数が近い)
classes = {"a": 300, "i": 350, "u": 400}
audio_data = {}

for name, freq in classes.items():
    # 各クラス20サンプル(異なるseedで個体差を持たせる)
    audio_data[name] = [make_voice_sample(freq, sr, seed=i) for i in range(20)]

print(f"各クラスのサンプル数: {len(audio_data['a'])}")
print(f"クラス間の周波数差: 50Hz(300/350/400Hz)")

音声分割と特徴量抽出

def split_audio(y, sr, duration=0.5):
    """音声を固定長に分割する"""
    samples = int(sr * duration)
    return [y[i:i+samples] for i in range(0, len(y)-samples+1, samples)]

def extract_mfcc_features(y, sr, n_mfcc=13, n_fft=512, hop=256):
    """MFCCの mean + std を特徴量として返す"""
    _, _, Zxx = stft(y, fs=sr, nperseg=n_fft, noverlap=n_fft-hop)
    power = np.abs(Zxx) ** 2

    # メルフィルタバンク
    n_mels = 26
    def hz_to_mel(hz): return 2595 * np.log10(1 + hz / 700)
    def mel_to_hz(mel): return 700 * (10 ** (mel / 2595) - 1)
    mel_pts = np.linspace(hz_to_mel(0), hz_to_mel(sr/2), n_mels + 2)
    hz_pts = mel_to_hz(mel_pts)
    bins = np.floor((n_fft + 1) * hz_pts / sr).astype(int)
    fb = np.zeros((n_mels, n_fft // 2 + 1))
    for m in range(1, n_mels + 1):
        for k in range(bins[m-1], bins[m]):
            fb[m-1, k] = (k - bins[m-1]) / (bins[m] - bins[m-1] + 1e-10)
        for k in range(bins[m], bins[m+1]):
            fb[m-1, k] = (bins[m+1] - k) / (bins[m+1] - bins[m] + 1e-10)

    mel_power = np.dot(fb, power)
    log_mel = np.log(mel_power + 1e-10)
    mfcc = dct(log_mel, type=2, axis=0, norm='ortho')[:n_mfcc]

    return np.concatenate([np.mean(mfcc, axis=1), np.std(mfcc, axis=1)])

データセットの作成

ここが重要なポイントです。学習データとテストデータは必ず別のサンプルから作ります。

label_map = {"a": 0, "i": 1, "u": 2}
X_all, y_all = [], []

for name, samples in audio_data.items():
    for y_audio in samples:
        for seg in split_audio(y_audio, sr):
            feat = extract_mfcc_features(seg, sr)
            X_all.append(feat)
            y_all.append(label_map[name])

X_all = np.array(X_all)
y_all = np.array(y_all)
print(f"全データ数: {len(X_all)}")

# 学習データ80% / テストデータ20% に分割
# stratify=y_all で各クラスの比率を維持する
X_train, X_test, y_train, y_test = train_test_split(
    X_all, y_all,
    test_size=0.2,
    random_state=42,
    stratify=y_all
)

print(f"学習データ数: {len(X_train)}")
print(f"テストデータ数: {len(X_test)}")

作成した学習モデルに関しての精度などの検証も必要です。

本章に関して、後半の内容は本に記載しました。

https://zenn.dev/kta805/books/dsp-python-intro

続きとして以下の内容を載せています。

●実験結果
 精度
 混同行列

●問題点
 データ不足
 特徴量が弱い

●改善
 分割
 mean + std

Discussion