reazon-research/reazonspeech(small)の音声データをWADA-SNRで信号対雑音比のデータ分析をする

初めに

reazon-research/reazonspeechのデータは主に音声認識(STT)のデータセットとして想定されたデータ?みたいです。
そのため、音声合成(TTS)のデータセットとしてはそのままでは使うには雑音などが入っている音声が多いため、どのようなデータが入っているのか分析して使えるようにしていきます

reazon-research/reazonspeechのデータは以下の記事でも扱っています。

ayousanz.hatenadiary.jp

ayousanz.hatenadiary.jp

デモ

データ分析した結果をヒストグラムでグラフにした結果は以下になります。
以下を見る感じ雑音が比較的少ないものは、100以上であることがわかるためフィルタリングする際には100以上で切っても良さそうです

以下で使用したGolobを公開しています

colab.research.google.com

開発環境

準備

分析に必要なライブラリを入れます

!pip install datasets librosa IPython
!pip install numpy scipy datasets
!pip install soundfile

分析手順

データをダウンロード・ロード

まずは huggingfaceからデータをダウンロード及びロードします。 その際に ログインが必要になるため、事前にログインを行なっておきます

!huggingface-cli login

ダウンロード及びロードの処理を行います。今回はGoogle Colobの容量の限界があるため、smallサイズでダウンロードします

from datasets import load_dataset

# データセットをロード
ds = load_dataset("reazon-research/reazonspeech", "small", trust_remote_code=True)

WAND-SNRを使って音声データの分析

各音声データのWADA-SNRを取得して、jsonに保存します。

from datasets import load_dataset
import numpy as np

def wada_snr(wav):
    # Direct blind estimation of the SNR of a speech signal.
    #
    # Paper on WADA SNR:
    #   http://www.cs.cmu.edu/~robust/Papers/KimSternIS08.pdf
    #
    # This function was adapted from this matlab code:
    #   https://labrosa.ee.columbia.edu/projects/snreval/#9

    # init
    eps = 1e-10
    # next 2 lines define a fancy curve derived from a gamma distribution -- see paper
    db_vals = np.arange(-20, 101)
    g_vals = np.array([0.40974774, 0.40986926, 0.40998566, 0.40969089, 0.40986186, 0.40999006, 0.41027138, 0.41052627, 0.41101024, 0.41143264, 0.41231718, 0.41337272, 0.41526426, 0.4178192 , 0.42077252, 0.42452799, 0.42918886, 0.43510373, 0.44234195, 0.45161485, 0.46221153, 0.47491647, 0.48883809, 0.50509236, 0.52353709, 0.54372088, 0.56532427, 0.58847532, 0.61346212, 0.63954496, 0.66750818, 0.69583724, 0.72454762, 0.75414799, 0.78323148, 0.81240985, 0.84219775, 0.87166406, 0.90030504, 0.92880418, 0.95655449, 0.9835349 , 1.01047155, 1.0362095 , 1.06136425, 1.08579312, 1.1094819 , 1.13277995, 1.15472826, 1.17627308, 1.19703503, 1.21671694, 1.23535898, 1.25364313, 1.27103891, 1.28718029, 1.30302865, 1.31839527, 1.33294817, 1.34700935, 1.3605727 , 1.37345513, 1.38577122, 1.39733504, 1.40856397, 1.41959619, 1.42983624, 1.43958467, 1.44902176, 1.45804831, 1.46669568, 1.47486938, 1.48269965, 1.49034339, 1.49748214, 1.50435106, 1.51076426, 1.51698915, 1.5229097 , 1.528578  , 1.53389835, 1.5391211 , 1.5439065 , 1.54858517, 1.55310776, 1.55744391, 1.56164927, 1.56566348, 1.56938671, 1.57307767, 1.57654764, 1.57980083, 1.58304129, 1.58602496, 1.58880681, 1.59162477, 1.5941969 , 1.59693155, 1.599446  , 1.60185011, 1.60408668, 1.60627134, 1.60826199, 1.61004547, 1.61192472, 1.61369656, 1.61534074, 1.61688905, 1.61838916, 1.61985374, 1.62135878, 1.62268119, 1.62390423, 1.62513143, 1.62632463, 1.6274027 , 1.62842767, 1.62945532, 1.6303307 , 1.63128026, 1.63204102])

    # peak normalize, get magnitude, clip lower bound
    wav = np.array(wav)
    wav = wav / abs(wav).max()
    abs_wav = abs(wav)
    abs_wav[abs_wav < eps] = eps

    # calcuate statistics
    # E[|z|]
    v1 = max(eps, abs_wav.mean())
    # E[log|z|]
    v2 = np.log(abs_wav).mean()
    # log(E[|z|]) - E[log(|z|)]
    v3 = np.log(v1) - v2

    # table interpolation
    wav_snr_idx = None
    if any(g_vals < v3):
        wav_snr_idx = np.where(g_vals < v3)[0].max()
    # handle edge cases or interpolate
    if wav_snr_idx is None:
        wav_snr = db_vals[0]
    elif wav_snr_idx == len(db_vals) - 1:
        wav_snr = db_vals[-1]
    else:
        wav_snr = db_vals[wav_snr_idx] + \
            (v3-g_vals[wav_snr_idx]) / (g_vals[wav_snr_idx+1] - \
            g_vals[wav_snr_idx]) * (db_vals[wav_snr_idx+1] - db_vals[wav_snr_idx])

    # Calculate SNR
    dEng = sum(wav**2)
    dFactor = 10**(wav_snr / 10)
    dNoiseEng = dEng / (1 + dFactor) # Noise energy
    dSigEng = dEng * dFactor / (1 + dFactor) # Signal energy
    snr = 10 * np.log10(dSigEng / dNoiseEng)

    return snr

# データを前処理するための関数。ファイルパスを必要としないため、少し変更します。
def preprocess_audio(data):
    # データが整数型の場合、浮動小数点型に変換
    if data.dtype == np.int16:
        data = data.astype(np.float32) / np.iinfo(np.int16).max
    elif data.dtype == np.int32:
        data = data.astype(np.float32) / np.iinfo(np.int32).max

    # ステレオをモノラルに変換(必要があれば)
    if len(data.shape) == 2:
        data = data.mean(axis=1)

    return data

import librosa
import IPython.display as ipd
from IPython.display import Audio, display
import random
from concurrent.futures import ProcessPoolExecutor


print("データ数: ",len(ds['train']))

import json

# 結果を保存するリスト
results = []

# 音声データの前処理とSNR計算を行う関数
def process_audio_data(index):
    # 音声データを前処理
    audio_data = ds['train'][index]['audio']['array']
    preprocessed_data = preprocess_audio(audio_data)
    
    # WADA-SNRを計算
    snr = wada_snr(preprocessed_data)

    # データセットから情報を取得
    audio_path = ds['train'][index]['audio']['path']
    file_name = ds['train'][index]['name']
    transcription = ds['train'][index]['transcription']
    
    # 音声ファイルを読み込む(実際にはこの処理はSNR計算に不要かもしれません)
    # audio, sr = librosa.load(audio_path, sr=16000)

    return {
        "ファイル名": file_name,
        "SNR値": snr,
        "トランスクリプション": transcription
    }

# 並列処理で関数を実行
with ProcessPoolExecutor(max_workers=4) as executor:
    for result in executor.map(process_audio_data, range(len(ds['train']))):
        print("ファイル名:" + result["ファイル名"] + ", SNR値" + str(result["SNR値"]))
        # print("トランスクリプション: ", result["トランスクリプション"])
        results.append(result)

# 結果をJSONファイルに保存
with open('audio_analysis_results.json', 'w') as f:
    json.dump(results, f, ensure_ascii=False, indent=4)

print("JSONファイルが保存されました。")

from google.colab import files
# Google Colabからファイルをダウンロード
files.download("audio_analysis_results.json")

分析結果をヒストグラムで表示

分析結果を上記でjsonに保存したため、そのjsonからデータの散らばりを確認するためにヒストグラムで表示をします。

import json
import matplotlib.pyplot as plt

# JSONファイルからデータをロード
file_path = 'audio_analysis_results.json'
with open(file_path, 'r') as file:
    data = json.load(file)


# WADA-SNR値のリストを抽出
snr_values = [item['SNR値'] for item in data]
# ヒストグラムを描画
plt.hist(snr_values, bins=200, edgecolor='black')  # binsは適宜調整してください
plt.xlabel('SNR value')
plt.ylabel('number of occurances')
plt.title('Histogram of SNR values')
plt.grid(True)
plt.show()

WADA-SNR値が100以上のデータ個数を取得

データをフィルタリングする際に、仮にWADA-SNR値が100以上を使うとした際に全体のうちどのくらいのデータが有効になるのかを調べます

# WADA-SNR値が100以上のデータの数をカウント
count_snr_above_100 = sum(1 for item in data if item['SNR値'] >= 100)

print(f"SNR値が100以上のデータの数: {count_snr_above_100}")

# SNR値が100以上のデータの数: 3364

またsmallでの全体のデータ数は以下で取得できます

print("データ数: ",len(ds['train']))

# データ数:  62047

そのため、3364/62047がWADA-SNR値が100以上のため 100以上のみを使う場合(small)、5.4%になりそうです

備考

Windowsでのプロセスエラー対応

Windowsで処理する場合は、以下のようなエラーが出るのでコードを書き換えます

エラー

This probably means that you are not using fork to start your
        child processes and you have forgotten to use the proper idiom
        in the main module:

            if __name__ == '__main__':
                freeze_support()

修正コード

if __name__ == '__main__':
    freeze_support()

    # 並列処理で関数を実行
    with ProcessPoolExecutor(max_workers=4) as executor:
        for result in executor.map(process_audio_data, range(len(ds['train']))):
            print("ファイル名:" + result["ファイル名"] + ", SNR値" + str(result["SNR値"]))
            # print("トランスクリプション: ", result["トランスクリプション"])
            results.append(result)

    # 結果をJSONファイルに保存
    with open('audio_analysis_results.json', 'w') as f:
        json.dump(results, f, ensure_ascii=False, indent=4)

    print("JSONファイルが保存されました。")

並列処理対応

以下で並列処理ができます

from datasets import load_dataset
import numpy as np
import json
from multiprocessing import Pool
import os

import IPython.display as ipd
from IPython.display import Audio, display
from concurrent.futures import ProcessPoolExecutor

(省略)

if __name__ == '__main__':
    ds = load_dataset("reazon-research/reazonspeech", "all", trust_remote_code=True)
    total_data = len(ds['train'])
    print("データ数: ", total_data)

    # 並列処理のためのプールを作成
    num_cores = os.cpu_count()
    print("num cores:",num_cores)
    pool = Pool(processes=num_cores)

    # 並列処理を実行
    results = []
    for i, result in enumerate(pool.imap(process_audio_data, range(total_data)), 1):
        results.append(result)
        print(f"処理中: {i}/{total_data} ({i/total_data*100:.2f}%)")

    # プールを閉じる
    pool.close()
    pool.join()

    # 結果をJSONファイルに保存
    with open("reazonspeech-all-wada-snr.json", "w", encoding="utf-8") as f:
        json.dump(results, f, ensure_ascii=False, indent=4)

    print("JSONファイルが保存されました。")