reazon-research/reazonspeech(tiny)の音声データをNISQAで音声品質と自然さの評価のデータ分析をする

初めに

NISQAを使ってreazon-research/reazonspeechのデータを分析します

環境

  • L4 GPU
  • ubuntu22.04

分析処理の方向性

reazonspeechのデータフォーマット

huggingfaceから取得したreazonspeechのデータセットを使いためには、以下の対応をする必要があります。

まず前提として リポジトリのReadMeよりデータフォーマットは以下のようになっています

Audio files are available in FLAC format, sampled at 16000 hz. Each audio file is accompanied with a transcription.

{
    'name': '000/0000000000000.flac',
    'audio': {
        'path': '/path/to/000/0000000000000.flac',
        'array': array([ 0.01000000,  ...], dtype=float32),
        'sampling_rate': 16000
    },
    'transcription': '今日のニュースをお伝えします。'
}

NISQAの実行方法について

NISQAはReadMe及びコードを見るところ、以下の条件があります * wavファイルのみ対象 * CLIから処理をする方法が一般的(関数にwavファイルを渡すなどは想定していない?)

一つのファイルを処理をする場合、以下のように実行します

python run_predict.py --mode predict_file --pretrained_model weights/nisqa.tar --deg /path/to/wav/file.wav --output_dir /path/to/dir/with/results

またディレクトリ内のファイル全てに対しては以下のように実行します

python run_predict.py --mode predict_dir --pretrained_model weights/nisqa.tar --data_dir /path/to/folder/with/wavs --num_workers 0 --bs 10 --output_dir /path/to/dir/with/results

分析の方向性

上記の二つより以下の対応をする必要があります 1. flacファイルからwavファイルに変換 2. ディレクトリ内にwavファイルを入れて、CLI上からNISQAの処理をする

前処理

reazonspeechのデータをflacに変換

linux上で datasets から以下の処理でデータを取得した場合は、以下のような形でキャッシュされています。(少なくとも手元の環境では)

from datasets import load_dataset
ds = load_dataset("reazon-research/reazonspeech", "all", trust_remote_code=True)

キャッシュのパスは 以下の通りです

~/.cache/huggingface/datasets/reazon-research___reazonspeech/tiny/0.0.0/e16d1ee2aae813b6ea960f564f4dc8481f58bfa6074be491eb4a6ddde66330bb

キャッシュは以下のようになっています

reazonspeech-train.arrow

そのため、arrowファイルからflacに一度変換をします

詳細は以下の記事を確認してください

ayousanz.hatenadiary.jp

flacファイルをwavファイルに変換

flacファイルができたので、以下で全てwavファイルに変換をします

詳細は以下をご確認ください

ayousanz.hatenadiary.jp

NISQAでwavファイルを分析

wavファイルに変換が終わったら、以下でNISQAを使って処理を実行します

python run_predict.py --mode predict_dir --pretrained_model weights/nisqa.tar --data_dir convert_wav/ --num_workers 0 --bs 2 --output_dir .

変換処理が終わったら、NISQA_results.csv というファイルが生成されます。内容は以下のようになっています(一部のみを記載しています)

deg mos_pred noi_pred dis_pred col_pred loud_pred model
1 2.3836768 1.8683524 3.9964635 3.6280687 3.5253642 NISQAv2
2 1.5204918 1.5145079 3.7368195 2.7644107 2.5888093 NISQAv2
3 3.1642735 2.5920422 3.9088361 3.5170329 3.714206 NISQAv2
4 2.9011464 1.65048 4.631492 4.208716 4.005361 NISQAv2
5 3.2035732 2.8596961 3.8604171 3.57778 3.71185 NISQAv2

CSVから分析

以下のコードを使って、CSVからデータを取得してヒストグラムとして表示していきます

import pandas as pd
import matplotlib.pyplot as plt

# CSVファイルを読み込む
data = pd.read_csv('NISQA_results.csv')
bins = 80

# ヒストグラムのプロット
plt.figure(figsize=(10, 6))

plt.subplot(2, 3, 1)
plt.hist(data['mos_pred'], bins=bins, edgecolor='black')
plt.xlabel('MOS Prediction')
plt.ylabel('Frequency')
plt.title('Histogram of MOS Prediction')

plt.subplot(2, 3, 2)
plt.hist(data['noi_pred'], bins=bins, edgecolor='black')
plt.xlabel('Noise Prediction')
plt.ylabel('Frequency')
plt.title('Histogram of Noise Prediction')

plt.subplot(2, 3, 3)
plt.hist(data['dis_pred'], bins=bins, edgecolor='black')
plt.xlabel('Distortion Prediction')
plt.ylabel('Frequency')
plt.title('Histogram of Distortion Prediction')

plt.subplot(2, 3, 4)
plt.hist(data['col_pred'], bins=bins, edgecolor='black')
plt.xlabel('Coloration Prediction')
plt.ylabel('Frequency')
plt.title('Histogram of Coloration Prediction')

plt.subplot(2, 3, 5)
plt.hist(data['loud_pred'], bins=bins, edgecolor='black')
plt.xlabel('Loudness Prediction')
plt.ylabel('Frequency')
plt.title('Histogram of Loudness Prediction')

plt.tight_layout()
plt.show()

ffmpegを使ってflacファイルをwavファイルにGNU Parallelを使って並列処理で変換をする

初めに

開発環境

  • cuda:12.2.0
  • ubuntu22.04

詳細

以下のコードにて、指定したフォルダ内にあるflacファイルをwavファイルに変換します。このとき -j $(nproc) でCPUの最大コア数を指定しているのため、必要に応じて変更してください

#!/bin/bash

# 引数から音声ファイルがあるパスを取得
input_dir="$1"

# 変換後のwavファイルを保存するフォルダを作成
mkdir -p convert_wav

# 並列処理の関数を定義
convert_file() {
  file="$1"
  output_file="convert_wav/$(basename "${file%.flac}.wav")"
  echo "Converting file: $file"
  ffmpeg -i "$file" -acodec pcm_s16le -ar 44100 -ac 2 "$output_file"
  if [ $? -eq 0 ]; then
    echo "Conversion successful: $output_file"
  else
    echo "Conversion failed: $file"
  fi
}

# GNU Parallelを使用して並列処理を実行
export -f convert_file
find "$input_dir" -maxdepth 1 -name "*.flac" | parallel -j $(nproc) convert_file {}

echo "Conversion complete."

上記は以下のように実行します

./convert.sh output_flac/

reazon-research/reazonspeech(tiny)のデータセットをflac及びwavファイルで個別に保存する

環境

  • L4 GPU
  • ubuntu22.04

準備

実行

pythonflacに変換

from datasets import load_dataset
import os

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

# 出力ディレクトリを作成
output_dir = "output_flac"
os.makedirs(output_dir, exist_ok=True)

# データセットの各サンプルを処理
for sample in ds["train"]:
    # 音声データのローカルパスを取得
    audio_path = sample["audio"]["path"]

    # 出力ファイルのパスを作成
    output_path = os.path.join(output_dir, os.path.basename(audio_path))

    # 音声データをコピー
    with open(audio_path, "rb") as src_file, open(output_path, "wb") as dst_file:
        dst_file.write(src_file.read())

print("変換が完了しました。")

pythonでwavに変換

from datasets import load_dataset
import os
import librosa
import soundfile as sf

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

# 出力ディレクトリを作成
output_dir = "output_wav"
os.makedirs(output_dir, exist_ok=True)

# データセットの各サンプルを処理
for sample in ds["train"]:
    # 音声データのローカルパスを取得
    audio_path = sample["audio"]["path"]
    
    # 音声データを読み込み
    audio, sr = librosa.load(audio_path, sr=None)
    
    # 出力ファイルのパスを作成
    output_path = os.path.join(output_dir, os.path.splitext(os.path.basename(audio_path))[0] + ".wav")
    
    # 音声データを保存
    sf.write(output_path, audio, sr, subtype='PCM_24')

print("変換が完了しました。")

変換したwavファイルの情報を確認

以下で変換したwavファイルをいくつか取得して情報を確認します

import os
import soundfile as sf

output_dir = "output_wav"

# 変換後のwavファイルを3つ選択
wav_files = [file for file in os.listdir(output_dir) if file.endswith(".wav")][:3]

for wav_file in wav_files:
    file_path = os.path.join(output_dir, wav_file)
    
    # wavファイルの情報を取得
    audio_info = sf.info(file_path)
    
    print(f"ファイル名: {wav_file}")
    print(f"サンプリングレート: {audio_info.samplerate} Hz")
    print(f"チャンネル数: {audio_info.channels}")
    print(f"ビット深度: {audio_info.subtype}")
    print(f"長さ: {audio_info.duration:.2f} 秒")
    print("------------------------")

以下のように表示されます

ファイル名: 6c52ba0a0ba57.wav
サンプリングレート: 16000 Hz
チャンネル数: 1
ビット深度: PCM_24
長さ: 5.63 秒
------------------------
ファイル名: ff76142fa5e77.wav
サンプリングレート: 16000 Hz
チャンネル数: 1
ビット深度: PCM_24
長さ: 1.62 秒
------------------------
ファイル名: f879dcb872a87.wav
サンプリングレート: 16000 Hz
チャンネル数: 1
ビット深度: PCM_24
長さ: 23.92 秒
------------------------

ffmpegflacからwavに変換

まずはGPUの並列処理をするために、以下のライブラリを入れます

sudo apt-get install parallel

以下が並列で処理をするためのコードです. -j $(nproc) にてCPUのシステムコア数の最大値を指定しているため、必要に応じて変更してください

#!/bin/bash

# 引数から音声ファイルがあるパスを取得
input_dir="$1"

# 変換後のwavファイルを保存するフォルダを作成
mkdir -p convert_wav

# 並列処理の関数を定義
convert_file() {
  file="$1"
  output_file="convert_wav/$(basename "${file%.flac}.wav")"
  echo "Converting file: $file"
  ffmpeg -i "$file" -acodec pcm_s16le -ar 44100 -ac 2 "$output_file"
  if [ $? -eq 0 ]; then
    echo "Conversion successful: $output_file"
  else
    echo "Conversion failed: $file"
  fi
}

# GNU Parallelを使用して並列処理を実行
export -f convert_file
find "$input_dir" -maxdepth 1 -name "*.flac" | parallel -j $(nproc) convert_file {}

echo "Conversion complete."

reazon-research/reazonspeech(tiny)の音声データをSpeechMOSで音声の自然さのデータ分析をする

初めに

前回は、WADR-SNRで分析をしました。今回は SpeechMOSを使って音声の品質を分析していきます

ayousanz.hatenadiary.jp

環境

準備

必要なライブラリを入れていきます

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

データをダウンロードします

from datasets import load_dataset

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

SpeechMOSによるデータ分析

以下でSpeechMOSの値を計算して、jsonに保存していきます

import torch
import librosa
import json
import numpy as np
from datasets import load_dataset

# speech-mosの予測器を初期化
predictor = torch.hub.load("tarepan/SpeechMOS:v1.2.0", "utmos22_strong", trust_repo=True)

# データを前処理するための関数
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

def process_audio_data(data):
    # 音声データを読み込む
    audio_data = data['audio']['array']
    sr = data['audio']['sampling_rate']

    # データを前処理
    audio_data = preprocess_audio(audio_data)

    # speech-mosを使用して数値を取得
    audio_data_tensor = torch.from_numpy(audio_data).unsqueeze(0).to(torch.float32)  # float32に変換
    score = predictor(audio_data_tensor.to(torch.float32), sr)  # 入力データをfloat32に変換

    # 結果を辞書に格納
    result = {
        "ファイル名": data['name'],
        "MOS値": float(score),
        "トランスクリプション": data['transcription']
    }

    # 不要な変数を削除してメモリを解放
    del audio_data, audio_data_tensor

    return result

def process_and_save_results(ds):
    for data in ds['train']:
        result = process_audio_data(data)
        yield result

# 結果を保存するジェネレータ関数
def save_results_to_json(ds):
    with open('audio_analysis_results.json', 'w') as f:
        f.write('[\n')
        for i, result in enumerate(process_and_save_results(ds)):
            print("ファイル名:" + result["ファイル名"] + ", MOS値:" + str(result["MOS値"]))
            print("トランスクリプション: ", result["トランスクリプション"])
            json.dump(result, f, ensure_ascii=False, indent=4)
            if i < len(ds['train']) - 1:
                f.write(',\n')
        f.write('\n]')

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

# 結果を保存
save_results_to_json(ds)

また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)


# SNR値のリストを抽出
snr_values = [item['MOS値'] 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 Speech MOS values')
plt.grid(True)
plt.show()

ヒストグラムで表示した際は以下のようになります

1区切りで見たさいに当てはまるデータ数は以下のように計算します

# SNR値が100以上のデータの数をカウント
count_snr_above_1 = sum(1 for item in data if item['MOS値'] >= 1)
count_snr_above_2 = sum(1 for item in data if item['MOS値'] >= 2)
count_snr_above_3 = sum(1 for item in data if item['MOS値'] >= 3)

print(f"SNR値が1以上のデータの数: {count_snr_above_1}")
print(f"SNR値が2以上のデータの数: {count_snr_above_2}")
print(f"SNR値が3以上のデータの数: {count_snr_above_3}")

実際の数値は以下のようになります

SNR値が1以上のデータの数: 5323
SNR値が2以上のデータの数: 1058
SNR値が3以上のデータの数: 224

GaLoreを使って0.01Bモデル(EN)を作ってみる(モデルが保存できない)

初めに

LoRAよりもメモリ効率がよく学習ができる手法であるGaLoreで試してみます

論文のabstractの日本語訳は以下です(claude 3 opus を使用)

大規模言語モデル(LLM)の学習では、重みと最適化器の状態のサイズが増大するため、メモリに関する大きな課題があります。低ランク適応(LoRA)などの一般的なメモリ削減手法では、各層の凍結された事前学習済みの重みに、学習可能な低ランク行列を追加することで、学習可能なパラメータと最適化器の状態を削減します。しかし、このようなアプローチは、パラメータ探索を低ランク部分空間に制限し、学習のダイナミクスを変更するため、事前学習と微調整の両段階で、通常、フルランクの重みを用いた学習よりも性能が低下します。さらに、フルランクのウォームスタートが必要になる場合もあります。本研究では、フルパラメータ学習を可能にしながら、LoRAなどの一般的な低ランク適応手法よりもメモリ効率の良い学習戦略である、勾配低ランク射影(GaLore)を提案します。我々のアプローチは、最適化器の状態のメモリ使用量を最大で65.5%削減しながら、LLaMA 1Bと7Bのアーキテクチャを用いて最大19.7Bトークンを含むC4データセットで事前学習し、GLUEタスクでRoBERTaを微調整する際の効率とパフォーマンスを維持します。我々の8ビットGaLoreは、最適化器のメモリをさらに最大82.5%、BF16ベースラインと比較して学習メモリ全体を63.3%削減します。特筆すべきは、モデル並列化、チェックポイント、オフロード戦略を使用せずに、24GBメモリ(NVIDIA RTX 4090など)を搭載した民生用GPUで7Bモデルを事前学習できることを初めて実証したことです。

arxiv.org

環境環境

  • L4 GPU
  • ubuntu22.04

準備

まずはcloneします

git clone https://github.com/jiaweizzhao/GaLore

環境を作成します

python -m venv venv
source venv/bin/activate

ライブラリを入れます

pip install -e .

学習

まずは、パラメータを設定するために GaLore/configs/llama_10m.json に0.01B用のconfig.jsonを作成します。以下のパラメータは 0.02B及び0.04Bを参考にして0.01B用に書き換えています

{
    "architectures": [
        "LLaMAForCausalLM"
    ],
    "bos_token_id": 0,
    "eos_token_id": 1,
    "hidden_act": "silu",
    "hidden_size": 128,
    "intermediate_size": 344,
    "initializer_range": 0.02,
    "max_sequence_length": 1024,
    "model_type": "llama",
    "num_attention_heads": 2,
    "num_hidden_layers": 2,
    "pad_token_id": -1,
    "rms_norm_eps": 1e-06,
    "transformers_version": "4.28.1",
    "use_cache": true,
    "vocab_size": 32000
}

以下で学習をします。この時になるべく24GBのVRAMを使えるようにbatchサイズを調節しています(使用は13GBです)

torchrun --standalone --nproc_per_node 1 torchrun_main.py \
    --model_config configs/llama_10m.json \
    --lr 0.005 \
    --galore_scale 0.25 \
    --rank 1024 \
    --update_proj_gap 500 \
    --batch_size 256 \
    --total_batch_size 4096 \
    --activation_checkpointing \
    --num_training_steps 10000 \
    --warmup_steps 15000 \
    --weight_decay 0 \
    --grad_clipping 1.0 \
    --dtype bfloat16 \
    --eval_every 1000 \
    --single_gpu \
    --optimizer galore_adamw8bit_per_laye

intfloat/e5-mistral-7b-instructを動かす

初めに

日本語の埋め込みモデルでスコアが高い intfloat/e5-mistral-7b-instructを触ってみます

参考(JapaneseEmbeddingEval)

github.com

環境

  • L4 GPU
  • ubuntu22.04

準備

ライブラリの追加をします

!pip install flash_attn -U
!pip install --upgrade transformers torch

実行

まずはサンプルコードでテキストをベクトル化します

import torch
import torch.nn.functional as F

from torch import Tensor
from transformers import AutoTokenizer, AutoModel


def last_token_pool(last_hidden_states: Tensor,
                 attention_mask: Tensor) -> Tensor:
    left_padding = (attention_mask[:, -1].sum() == attention_mask.shape[0])
    if left_padding:
        return last_hidden_states[:, -1]
    else:
        sequence_lengths = attention_mask.sum(dim=1) - 1
        batch_size = last_hidden_states.shape[0]
        return last_hidden_states[torch.arange(batch_size, device=last_hidden_states.device), sequence_lengths]


def get_detailed_instruct(task_description: str, query: str) -> str:
    return f'Instruct: {task_description}\nQuery: {query}'


# Each query must come with a one-sentence instruction that describes the task
task = 'Given a web search query, retrieve relevant passages that answer the query'
queries = [
    get_detailed_instruct(task, 'how much protein should a female eat'),
    get_detailed_instruct(task, 'summit define')
]
# No need to add instruction for retrieval documents
documents = [
    "As a general guideline, the CDC's average requirement of protein for women ages 19 to 70 is 46 grams per day. But, as you can see from this chart, you'll need to increase that if you're expecting or training for a marathon. Check out the chart below to see how much protein you should be eating each day.",
    "Definition of summit for English Language Learners. : 1  the highest point of a mountain : the top of a mountain. : 2  the highest level. : 3  a meeting or series of meetings between the leaders of two or more governments."
]
input_texts = queries + documents

tokenizer = AutoTokenizer.from_pretrained('intfloat/e5-mistral-7b-instruct')
model = AutoModel.from_pretrained('intfloat/e5-mistral-7b-instruct')

max_length = 4096
# Tokenize the input texts
batch_dict = tokenizer(input_texts, max_length=max_length - 1, return_attention_mask=False, padding=False, truncation=True)
# append eos_token_id to every input_ids
batch_dict['input_ids'] = [input_ids + [tokenizer.eos_token_id] for input_ids in batch_dict['input_ids']]
batch_dict = tokenizer.pad(batch_dict, padding=True, return_attention_mask=True, return_tensors='pt')

outputs = model(**batch_dict)
embeddings = last_token_pool(outputs.last_hidden_state, batch_dict['attention_mask'])

# normalize embeddings
embeddings = F.normalize(embeddings, p=2, dim=1)
scores = (embeddings[:2] @ embeddings[2:].T) * 100
print(scores.tolist())

ログで以下が出ます

[[82.91365814208984, 47.9715690612793], [46.95111083984375, 81.74346923828125]]

embeddingsの中を確認したいので、以下を実行します

print(embeddings)

結果は以下です

tensor([[ 0.0178,  0.0037, -0.0075,  ...,  0.0298, -0.0193,  0.0064],
        [-0.0033,  0.0003,  0.0154,  ...,  0.0310, -0.0047,  0.0013],
        [ 0.0172,  0.0028, -0.0087,  ...,  0.0116, -0.0094,  0.0094],
        [-0.0043, -0.0112, -0.0049,  ...,  0.0124,  0.0003,  0.0134]],
       grad_fn=<DivBackward0>)

cl-nagoya/shioriha-large-ptを動かす

初めに

公開されたので触っていきます

環境

  • L4 GPU
  • ubuntu22.04

準備

ライブラリを入れていきます

!pip install -U sentence-transformers
!pip install fugashi
!pip install unidic_lite

実行

サンプルのコードを実行します

from sentence_transformers import SentenceTransformer
sentences = ["This is an example sentence", "Each sentence is converted"]

model = SentenceTransformer('cl-nagoya/shioriha-large-pt')
embeddings = model.encode(sentences)
print(embeddings)

結果は以下のようになります

[[ 0.239826    0.48606512 -0.4585114  ...  0.12551573  0.37342685
   0.6000814 ]
 [-0.33355263  0.3741393  -0.8048501  ...  0.20616896  0.27591145
  -0.36157683]]