├── 00prepare ├── 00download_data.py ├── 01prepare_wav.py ├── 02prepare_label.py └── 03subset_data.py ├── 01compute_features ├── 01_compute_fbank.py ├── 01_compute_mfcc.py ├── 02_compute_mean_std.py ├── plot_fbank.py ├── plot_wave.py ├── test_cepstrum.py ├── test_fft.py ├── test_pre_emphasis.py └── test_spectrogram.py ├── 02dp_matching ├── 00_prepare_wav_dp.py ├── 01_compute_mfcc_dp.py ├── 02_dp_matching.py ├── 03_dtw_spectrogram.py ├── 04_dp_matching_knn.py └── compare_spectrogram.py ├── 03gmm_hmm ├── 00_make_label.py ├── 01_make_proto.py ├── 02_init_hmm.py ├── 03_train_gmmhmm.py ├── 03_train_sgmhmm.py ├── 04_prepare_testdata.py ├── 05_compute_feat_test.py ├── 06_recognize.py ├── 07_phone_alilgnment.py ├── hmmfunc.py ├── phones.txt └── test_spectrogram.py ├── 04dnn_hmm ├── 00_state_alignment.py ├── 01_count_states.py ├── 02_train_dnn.py ├── 03_dnn_recognize.py ├── 04_train_dnn_fbank.py ├── 05_compute_fbank_test.py ├── 06_dnn_recognize_fbank.py ├── hmmfunc.py ├── initialize.py ├── my_dataset.py ├── my_model.py └── plot_prob.py ├── 05ctc ├── 01_get_token.py ├── 02_train_ctc.py ├── 03_decode_ctc.py ├── 04_scoring.py ├── encoder.py ├── initialize.py ├── levenshtein.py ├── my_dataset.py └── my_model.py ├── 06rnn_attention ├── 01_get_token.py ├── 02_train_attention.py ├── 03_decode_attention.py ├── 04_scoring.py ├── attention.py ├── decoder.py ├── encoder.py ├── initialize.py ├── levenshtein.py ├── my_dataset.py └── my_model.py ├── 07ctc_att_mtl ├── 01_get_token.py ├── 02_train_ctc_att_mtl.py ├── 03_decode_ctc_att_mtl.py ├── 04_scoring.py ├── attention.py ├── decoder.py ├── encoder.py ├── initialize.py ├── levenshtein.py ├── my_dataset.py └── my_model.py ├── LICENSE ├── README.md └── appendix.pdf /00prepare/00download_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 本書で取り扱う音声データと,ラベルデータをダウンロードします. 5 | # データは,JSUTコーパスを使用します. 6 | # https://sites.google.com/site/shinnosuketakamichi/publication/jsut 7 | # 8 | 9 | # ファイルをダウンロードするためのモジュールをインポート 10 | from urllib.request import urlretrieve 11 | 12 | # zipファイルを展開するためのモジュールをインポート 13 | import zipfile 14 | 15 | # osモジュールをインポート 16 | import os 17 | 18 | # 19 | # メイン関数 20 | # 21 | if __name__ == "__main__": 22 | 23 | # データの置き場を定義 24 | data_dir = '../data/original' 25 | 26 | # ディレクトリdata_dirが存在しない場合は作成する 27 | os.makedirs(data_dir, exist_ok=True) 28 | 29 | # 音声ファイル(jsutコーパス. zip形式)をダウンロード 30 | data_archive = os.path.join(data_dir, 'jsut-data.zip') 31 | print('download jsut-data start') 32 | urlretrieve('http://ss-takashi.sakura.ne.jp/corpus/jsut_ver1.1.zip', 33 | data_archive) 34 | print('download jsut-data finished') 35 | 36 | # ダウンロードしたデータを展開する 37 | print('extract jsut-data start') 38 | with zipfile.ZipFile(data_archive) as data_zip: 39 | data_zip.extractall(data_dir) 40 | print('extract jsut-data finished') 41 | 42 | # zipファイルを削除する 43 | os.remove(data_archive) 44 | 45 | # jsutコーパスのラベルデータをダウンロード 46 | label_archive = os.path.join(data_dir, 'jsut-label.zip') 47 | print('download jsut-label start') 48 | urlretrieve('https://github.com/sarulab-speech/jsut-label/archive/master.zip', 49 | label_archive) 50 | print('download jsut-label finished') 51 | 52 | # ダウンロードしたデータを展開する 53 | print('extract jsut-label start') 54 | with zipfile.ZipFile(label_archive) as label_zip: 55 | label_zip.extractall(data_dir) 56 | print('extract jsut-label finished') 57 | 58 | # zipファイルを削除する 59 | os.remove(label_archive) 60 | 61 | print('all processes finished') 62 | 63 | -------------------------------------------------------------------------------- /00prepare/01prepare_wav.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # ダウンロードしたwavファイルを,サンプリングレート16000Hzのデータに変換します. 5 | # また,変換したwavデータのリストを作成します. 6 | # 7 | 8 | # サンプリング周波数を変換するためのモジュール(sox)をインポート 9 | import sox 10 | 11 | # osモジュールをインポート 12 | import os 13 | 14 | # 15 | # メイン関数 16 | # 17 | if __name__ == "__main__": 18 | 19 | # wavファイルが展開されたディレクトリ 20 | original_wav_dir = '../data/original/jsut_ver1.1/basic5000/wav' 21 | 22 | # フォーマット変換したwavファイルを出力するディレクトリ 23 | out_wav_dir = '../data/wav' 24 | 25 | # wavデータのリストを格納するディレクトリ 26 | out_scp_dir = '../data/label/all' 27 | 28 | # 出力ディレクトリが存在しない場合は作成する 29 | os.makedirs(out_wav_dir, exist_ok=True) 30 | os.makedirs(out_scp_dir, exist_ok=True) 31 | 32 | # soxによる音声変換クラスを呼び出す 33 | tfm = sox.Transformer() 34 | # サンプリング周波数を 16000Hz に変換するよう設定する 35 | tfm.convert(samplerate=16000) 36 | 37 | # wavデータのリストファイルを書き込みモードで開き,以降の処理を実施する 38 | with open(os.path.join(out_scp_dir, 'wav.scp'), mode='w') as scp_file: 39 | # BASIC5000_0001.wav ~ BASIC5000_5000.wav に対して処理を繰り返し実行 40 | for i in range(5000): 41 | filename = 'BASIC5000_%04d' % (i+1) 42 | # 変換元のオリジナルデータ (48000Hz)のファイル名 43 | wav_path_in = os.path.join(original_wav_dir, filename+'.wav') 44 | # 変換後のデータ(16000Hz)の保存ファイル名 45 | wav_path_out = os.path.join(out_wav_dir, filename+'.wav') 46 | 47 | print(wav_path_in) 48 | # ファイルが存在しない場合はエラー 49 | if not os.path.exists(wav_path_in): 50 | print('Error: Not found %s' % (wav_path_in)) 51 | exit() 52 | 53 | # サンプリング周波数の変換と保存を実行する 54 | tfm.build_file(input_filepath=wav_path_in, 55 | output_filepath=wav_path_out) 56 | 57 | # wavファイルのリストを書き込む 58 | scp_file.write('%s %s\n' % 59 | (filename, os.path.abspath(wav_path_out))) 60 | 61 | -------------------------------------------------------------------------------- /00prepare/02prepare_label.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # ダウンロードしたラベルデータを読み込み, 5 | # キャラクター(漢字混じりの文字)単位,かな単位,音素単位で定義されるラベルファイルを作成します. 6 | # 7 | 8 | # yamlデータを読み込むためのモジュールをインポート 9 | import yaml 10 | 11 | # osモジュールをインポート 12 | import os 13 | 14 | # 15 | # メイン関数 16 | # 17 | if __name__ == "__main__": 18 | 19 | # ダウンロードしたラベルデータ(yaml形式) 20 | original_label = \ 21 | '../data/original/jsut-label-master/text_kana/basic5000.yaml' 22 | 23 | # ラベルのリストを格納する場所 24 | out_label_dir = '../data/label/all' 25 | 26 | # 出力ディレクトリが存在しない場合は作成する 27 | os.makedirs(out_label_dir, exist_ok=True) 28 | 29 | # ラベルデータを読み込む 30 | with open(original_label, mode='r') as yamlfile: 31 | label_info = yaml.safe_load(yamlfile) 32 | 33 | # キャラクター/かな/音素のラベルファイルを書き込みモードで開く 34 | with open(os.path.join(out_label_dir, 'text_char'), 35 | mode='w') as label_char, \ 36 | open(os.path.join(out_label_dir, 'text_kana'), 37 | mode='w') as label_kana, \ 38 | open(os.path.join(out_label_dir, 'text_phone'), 39 | mode='w') as label_phone: 40 | # BASIC5000_0001 ~ BASIC5000_5000 に対して処理を繰り返し実行 41 | for i in range(5000): 42 | # 発話ID 43 | filename = 'BASIC5000_%04d' % (i+1) 44 | 45 | # 発話ID が label_info に含まれない場合はエラー 46 | if not filename in label_info: 47 | print('Error: %s is not in %s' % (filename, original_label)) 48 | exit() 49 | 50 | # キャラクターラベル情報を取得 51 | chars = label_info[filename]['text_level2'] 52 | # '、'と'。'を除去 53 | chars = chars.replace('、', '') 54 | chars = chars.replace('。', '') 55 | 56 | # かなラベル情報を取得 57 | kanas = label_info[filename]['kana_level3'] 58 | # '、'を除去 59 | kanas = kanas.replace('、', '') 60 | 61 | # 音素ラベル情報を取得 62 | phones = label_info[filename]['phone_level3'] 63 | 64 | # キャラクターラベルファイルへ,1文字ずつスペース区切りで書き込む 65 | # (' '.join(list) は,リストの各要素にスペースを挟んで,1文にする) 66 | label_char.write('%s %s\n' % (filename, ' '.join(chars))) 67 | 68 | # かなラベルファイルへ,1文字ずつスペース区切りで書き込む 69 | label_kana.write('%s %s\n' % (filename, ' '.join(kanas))) 70 | 71 | # 音素ラベルは,'-'をスペースに置換して書き込む 72 | label_phone.write('%s %s\n' % (filename, phones.replace('-',' '))) 73 | 74 | -------------------------------------------------------------------------------- /00prepare/03subset_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # データのリストを,学習/開発/評価用のデータセットに分割します. 5 | # ここでは,以下のように分割します. 6 | # BASIC5000_0001~0250 : 評価データ 7 | # BASIC5000_0251~0500 : 開発データ 8 | # BASIC5000_0501~1500 : 学習データ(小) 9 | # BASIC5000_0501~5000 : 学習データ(大) 10 | # 11 | 12 | # osモジュールをインポート 13 | import os 14 | 15 | # 16 | # メイン関数 17 | # 18 | if __name__ == "__main__": 19 | 20 | # 全データが記述されているリストの置き場 21 | all_dir = '../data/label/all' 22 | 23 | # 評価データが記述されたリストの出力先 24 | out_eval_dir = '../data/label/test' 25 | # 開発データが記述されたリストの出力先 26 | out_dev_dir = '../data/label/dev' 27 | # 学習データ(小)が記述されたリストの出力先 28 | out_train_small_dir = '../data/label/train_small' 29 | # 学習データ(大)が記述されたリストの出力先 30 | out_train_large_dir = '../data/label/train_large' 31 | 32 | # 各出力ディレクトリが存在しない場合は作成する 33 | for out_dir in [out_eval_dir, out_dev_dir, 34 | out_train_small_dir, out_train_large_dir]: 35 | os.makedirs(out_dir, exist_ok=True) 36 | 37 | # wav.scp, text_char, text_kana, text_phoneそれぞれに同じ処理を行う 38 | for filename in ['wav.scp', 'text_char', 39 | 'text_kana', 'text_phone']: 40 | # 全データを読み込みモードで,/評価/開発/学習データリストを書き込みモードで開く 41 | with open(os.path.join(all_dir, filename), 42 | mode='r') as all_file, \ 43 | open(os.path.join(out_eval_dir, filename), 44 | mode='w') as eval_file, \ 45 | open(os.path.join(out_dev_dir, filename), 46 | mode='w') as dev_file, \ 47 | open(os.path.join(out_train_small_dir, filename), 48 | mode='w') as train_small_file, \ 49 | open(os.path.join(out_train_large_dir, filename), 50 | mode='w') as train_large_file: 51 | # 1行ずつ読み込み,評価/開発/学習データリストに書き込んでいく 52 | for i, line in enumerate(all_file): 53 | if i < 250: 54 | # 1~250: 評価データリストへ書き込み 55 | eval_file.write(line) 56 | elif i < 500: 57 | # 251~500: 開発データリストへ書き込み 58 | dev_file.write(line) 59 | else: 60 | # 501~5000: 学習(大)データリストへ書き込み 61 | train_large_file.write(line) 62 | if i < 1500: 63 | # 501~1500: 学習(小)データリストへ書き込み 64 | train_small_file.write(line) 65 | 66 | -------------------------------------------------------------------------------- /01compute_features/02_compute_mean_std.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 学習データから,特徴量の平均と標準偏差を求めます. 5 | # 6 | 7 | # 数値演算用モジュール(numpy)をインポート 8 | import numpy as np 9 | 10 | # os, sysモジュールをインポート 11 | import os 12 | import sys 13 | 14 | # 15 | # メイン関数 16 | # 17 | if __name__ == "__main__": 18 | 19 | # 20 | # 設定ここから 21 | # 22 | 23 | # 特徴量2種類 24 | feature_list = ['fbank', 'mfcc'] 25 | 26 | # 特徴量[fbank, mfcc]それぞれに対して実行 27 | for feature in feature_list: 28 | # 各特徴量ファイルのリストと 29 | # 平均・標準偏差計算結果の出力先 30 | train_small_feat_scp = \ 31 | './%s/train_small/feats.scp' % (feature) 32 | train_small_out_dir = \ 33 | './%s/train_small' % (feature) 34 | train_large_feat_scp = \ 35 | './%s/train_large/feats.scp' % (feature) 36 | train_large_out_dir = \ 37 | './%s/train_large' % (feature) 38 | 39 | # 特徴量ファイルリストと出力先をリストにする 40 | feat_scp_list = [train_small_feat_scp, 41 | train_large_feat_scp] 42 | out_dir_list = [train_small_out_dir, 43 | train_large_out_dir] 44 | 45 | # 各セットについて処理を実行する 46 | for (feat_scp, out_dir) in \ 47 | zip(feat_scp_list, out_dir_list): 48 | 49 | print('Input feat_scp: %s' % (feat_scp)) 50 | 51 | # 出力ディレクトリが存在しない場合は作成する 52 | os.makedirs(out_dir, exist_ok=True) 53 | 54 | # 特徴量の平均と分散 55 | feat_mean = None 56 | feat_var = None 57 | # 総フレーム数 58 | total_frames = 0 59 | 60 | # 特徴量リストを開く 61 | with open(feat_scp, mode='r') as file_feat: 62 | # 特徴量リストを1行ずつ読み込む 63 | for i, line in enumerate(file_feat): 64 | # 各行には,発話ID,特徴量ファイルのパス, 65 | # フレーム数,次元数がスペース区切りで 66 | # 記載されている 67 | # split関数を使ってスペース区切りの行を 68 | # リスト型の変数に変換する 69 | parts = line.split() 70 | # 0番目が発話ID 71 | utterance_id = parts[0] 72 | # 1番目が特徴量ファイルのパス 73 | feat_path = parts[1] 74 | # 2番目がフレーム数 75 | num_frames = int(parts[2]) 76 | # 3番目が次元数 77 | num_dims = int(parts[3]) 78 | 79 | # 特徴量データを特徴量ファイルから読み込む 80 | feature = np.fromfile(feat_path, 81 | dtype=np.float32) 82 | 83 | # 読み込んだ時点で,featureは1行の 84 | # ベクトル(要素数=フレーム数*次元数)として 85 | # 格納されているこれをフレーム数 x 次元数の 86 | # 行列形式に変換する 87 | feature = feature.reshape(num_frames, 88 | num_dims) 89 | 90 | # 最初のファイルを処理した時に, 91 | # 平均と分散を初期化 92 | if i == 0: 93 | feat_mean = np.zeros(num_dims, np.float32) 94 | feat_var = np.zeros(num_dims, np.float32) 95 | 96 | # 総フレーム数を加算 97 | total_frames += num_frames 98 | # 特徴量ベクトルのフレーム総和を加算 99 | feat_mean += np.sum(feature, 100 | axis=0) 101 | # 特徴量ベクトルの二乗のフレーム総和を加算 102 | feat_var += np.sum(np.power(feature,2), 103 | axis=0) 104 | 105 | # 総フレーム数で割って平均値ベクトルを計算 106 | feat_mean /= total_frames 107 | # 分散値ベクトルを計算 108 | feat_var = (feat_var / total_frames) \ 109 | - np.power(feat_mean,2) 110 | # 平方根を取って標準偏差ベクトルを算出 111 | feat_std = np.sqrt(feat_var) 112 | 113 | # ファイルに書き込む 114 | out_file = os.path.join(out_dir, 'mean_std.txt') 115 | print('Output file: %s' % (out_file)) 116 | with open(out_file, mode='w') as file_o: 117 | # 平均値ベクトルの書き込み 118 | file_o.write('mean\n') 119 | for i in range(np.size(feat_mean)): 120 | file_o.write('%e ' % (feat_mean[i])) 121 | file_o.write('\n') 122 | # 標準偏差ベクトルの書き込み 123 | file_o.write('std\n') 124 | for i in range(np.size(feat_std)): 125 | file_o.write('%e ' % (feat_std[i])) 126 | file_o.write('\n') 127 | 128 | -------------------------------------------------------------------------------- /01compute_features/plot_fbank.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # メルフィルタバンクを作成してプロットします. 5 | # 6 | 7 | # 数値演算用モジュール(numpy)をインポート 8 | import numpy as np 9 | 10 | # プロット用モジュール(matplotlib)をインポート 11 | import matplotlib.pyplot as plt 12 | 13 | # 14 | # 周波数をヘルツからメルに変換する 15 | # 16 | def Herz2Mel(herz): 17 | return (1127.0 * np.log(1.0 + herz / 700)) 18 | 19 | # 20 | # メイン関数 21 | # 22 | if __name__ == "__main__": 23 | 24 | # 最大周波数 [Hz] 25 | max_herz = 8000 26 | 27 | # FFTのポイント数 28 | fft_size = 4096 29 | 30 | # フィルタバンクの数 31 | num_mel_bins = 7 32 | 33 | # プロットを出力するファイル(pngファイル) 34 | out_plot = './mel_bank.png' 35 | 36 | # メル軸での最大周波数 37 | max_mel = Herz2Mel(max_herz) 38 | 39 | # メル軸上での等間隔な周波数を得る 40 | mel_points = np.linspace(0, max_mel, num_mel_bins+2) 41 | 42 | # パワースペクトルの次元数 = FFTサイズ/2+1 43 | dim_spectrum = int(fft_size / 2) + 1 44 | 45 | # メルフィルタバンク(フィルタの数 x スペクトルの次元数) 46 | mel_filter_bank = np.zeros((num_mel_bins, dim_spectrum)) 47 | for m in range(num_mel_bins): 48 | # 三角フィルタの左端,中央,右端のメル周波数 49 | left_mel = mel_points[m] 50 | center_mel = mel_points[m+1] 51 | right_mel = mel_points[m+2] 52 | # パワースペクトルの各ビンに対応する重みを計算する 53 | for n in range(dim_spectrum): 54 | # 各ビンに対応するヘルツ軸周波数を計算 55 | freq = 1.0 * n * max_herz / dim_spectrum 56 | # メル周波数に変換 57 | mel = Herz2Mel(freq) 58 | # そのビンが三角フィルタの範囲に入っていれば,重みを計算 59 | if mel > left_mel and mel < right_mel: 60 | if mel <= center_mel: 61 | weight = (mel - left_mel) / (center_mel - left_mel) 62 | else: 63 | weight = (right_mel-mel) / (right_mel-center_mel) 64 | mel_filter_bank[m][n] = weight 65 | 66 | # プロットの描画領域を作成 67 | plt.figure(figsize=(6,4)) 68 | 69 | # 横軸(周波数軸)を作成する 70 | freq_axis = np.arange(dim_spectrum) \ 71 | * max_herz / dim_spectrum 72 | 73 | for m in range(num_mel_bins): 74 | # フィルタバンクをプロット 75 | plt.plot(freq_axis, mel_filter_bank[m], color='k') 76 | #plt.plot(freq_axis, mel_filter_bank[m]) 77 | 78 | plt.xlabel('Frequency [Hz]') 79 | 80 | # 横軸の表示領域を0~最大周波数に制限 81 | plt.xlim([0, max_herz]) 82 | 83 | # プロットを保存する 84 | plt.savefig(out_plot) 85 | 86 | -------------------------------------------------------------------------------- /01compute_features/plot_wave.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # wavファイルを開いて波形をプロットします. 5 | # 6 | 7 | # wavデータを読み込むためのモジュール(wave)をインポート 8 | import wave 9 | 10 | # 数値演算用モジュール(numpy)をインポート 11 | import numpy as np 12 | 13 | # プロット用モジュール(matplotlib)をインポート 14 | import matplotlib.pyplot as plt 15 | 16 | # 17 | # メイン関数 18 | # 19 | if __name__ == "__main__": 20 | # 開くwavファイル 21 | wav_file = '../data/wav/BASIC5000_0001.wav' 22 | 23 | # 波形のプロットを出力するファイル(pngファイル) 24 | out_plot = './plot.png' 25 | 26 | # wavファイルを開き、以降の処理を行う 27 | with wave.open(wav_file) as wav: 28 | # サンプリング周波数 [Hz] を取得 29 | sampling_frequency = wav.getframerate() 30 | 31 | # サンプルサイズ [Byte] を取得 32 | sample_size = wav.getsampwidth() 33 | 34 | # チャネル数を取得 35 | num_channels = wav.getnchannels() 36 | 37 | # wavデータのサンプル数を取得 38 | num_samples = wav.getnframes() 39 | 40 | # wavデータを読み込む 41 | waveform = wav.readframes(num_samples) 42 | 43 | # 読み込んだデータはバイナリ値(16bit integer) 44 | # なので,数値(整数)に変換する 45 | waveform = np.frombuffer(waveform, dtype=np.int16) 46 | 47 | # 48 | # 読み込んだwavファイルの情報を表示する 49 | # 50 | print("Sampling Frequency: %d [Hz]" % sampling_frequency) 51 | print("Sample Size: %d [Byte]" % sample_size) 52 | print("Number of Channels: %d" % num_channels) 53 | print("Number of Samples: %d" % num_samples) 54 | 55 | # 56 | # 読み込んだ波形(waveform)をプロットする 57 | # 58 | 59 | # 横軸(時間軸)を作成する 60 | time_axis = np.arange(num_samples) / sampling_frequency 61 | 62 | # プロットの描画領域を作成 63 | plt.figure(figsize=(10,4)) 64 | 65 | # プロット 66 | plt.plot(time_axis, waveform) 67 | 68 | # 横軸と縦軸のラベルを定義 69 | plt.xlabel("Time [sec]") 70 | plt.ylabel("Value") 71 | 72 | # 横軸の表示領域を0から波形終了時刻に制限 73 | plt.xlim([0, num_samples / sampling_frequency]) 74 | 75 | # プロットを保存する 76 | plt.savefig(out_plot) 77 | 78 | -------------------------------------------------------------------------------- /01compute_features/test_cepstrum.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # ケプストラム分析により音声の 5 | # フォルマント成分を抽出します. 6 | # 7 | 8 | # wavデータを読み込むためのモジュール(wave)をインポート 9 | import wave 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # プロット用モジュール(matplotlib)をインポート 15 | import matplotlib.pyplot as plt 16 | 17 | # 18 | # メイン関数 19 | # 20 | if __name__ == "__main__": 21 | # 開くwavファイル 22 | wav_file = '../data/wav/BASIC5000_0001.wav' 23 | 24 | # 分析する時刻.BASIC5000_0001.wav では, 25 | # 以下の時刻は音素"o"を発話している 26 | target_time = 0.58 27 | # 以下の時刻は音素"a"を発話している 28 | target_time = 0.73 29 | 30 | # FFT(高速フーリエ変換)を行う範囲のサンプル数 31 | # 2のべき乗である必要がある 32 | fft_size = 1024 33 | 34 | # ケプストラムの低次と高次の境目を決める次数 35 | cep_threshold = 33 36 | 37 | # プロットを出力するファイル(pngファイル) 38 | out_plot = './cepstrum.png' 39 | 40 | # wavファイルを開き,以降の処理を行う 41 | with wave.open(wav_file) as wav: 42 | # サンプリング周波数 [Hz] を取得 43 | sampling_frequency = wav.getframerate() 44 | 45 | # wavデータを読み込む 46 | waveform = wav.readframes(wav.getnframes()) 47 | 48 | # 読み込んだデータはバイナリ値(16bit integer) 49 | # なので,数値(整数)に変換する 50 | waveform = np.frombuffer(waveform, dtype=np.int16) 51 | 52 | # 分析する時刻をサンプル番号に変換 53 | target_index = np.int(target_time * sampling_frequency) 54 | 55 | # FFTを実施する区間分の波形データを取り出す 56 | frame = waveform[target_index: 57 | target_index + fft_size].copy() 58 | 59 | # ハミング窓を掛ける 60 | frame = frame * np.hamming(fft_size) 61 | 62 | # FFTを実施する 63 | spectrum = np.fft.fft(frame) 64 | 65 | # 対数パワースペクトルを得る 66 | log_power = 2 * np.log(np.abs(spectrum) + 1E-7) 67 | 68 | # 対数パワースペクトルの逆フーリエ変換により 69 | # ケプストラムを得る 70 | cepstrum = np.fft.ifft(log_power) 71 | 72 | # ケプストラムの高次部分をゼロにする 73 | cepstrum_low = cepstrum.copy() 74 | cepstrum_low[(cep_threshold+1):-(cep_threshold)] = 0.0 75 | 76 | # 高域カットしたケプストラムを再度フーリエ変換し, 77 | # 対数パワースペクトルを計算 78 | log_power_ceplo = np.abs(np.fft.fft(cepstrum_low)) 79 | 80 | # 逆に,低次をゼロにしたケプストラムを求める 81 | cepstrum_high = cepstrum - cepstrum_low 82 | # ただし,表示上のため,ゼロ次元目はカットしない 83 | cepstrum_high[0] = cepstrum[0] 84 | 85 | # 低域カットしたケプストラムを再度フーリエ変換し, 86 | # 対数パワースペクトルを計算 87 | log_power_cephi = np.abs(np.fft.fft(cepstrum_high)) 88 | 89 | 90 | # プロットの描画領域を作成 91 | plt.figure(figsize=(18,10)) 92 | 93 | # 対数パワースペクトルの横軸(周波数軸)を作成する 94 | freq_axis = np.arange(fft_size) \ 95 | * sampling_frequency / fft_size 96 | 97 | # 3種類の対数パワースペクトルをプロット 98 | for n, log_pow in enumerate([log_power, 99 | log_power_ceplo , 100 | log_power_cephi]): 101 | # 描画領域を3行2列に分割し,1列目にプロット 102 | plt.subplot(3, 2, n*2+1) 103 | plt.plot(freq_axis, log_pow, color='k') 104 | 105 | # 横軸と縦軸のラベルを定義 106 | plt.xlabel('Frequency [Hz]') 107 | plt.ylabel('Value') 108 | 109 | # 表示領域を制限 110 | plt.xlim([0, sampling_frequency / 2]) 111 | plt.ylim([0, 30]) 112 | 113 | # ケプストラムの横軸(ケフレンシ軸=時間軸)を作成する 114 | qefr_axis = np.arange(fft_size) / sampling_frequency 115 | 116 | # 3種類のケプストラムをプロット 117 | for n, cepst in enumerate([cepstrum, 118 | cepstrum_low , 119 | cepstrum_high]): 120 | # 描画領域を3行2列に分割し,2列目にプロット 121 | plt.subplot(3, 2, n*2+2) 122 | # ケプストラムは実部をプロット 123 | # (虚部はほぼゼロである) 124 | plt.plot(qefr_axis, np.real(cepst), color='k') 125 | 126 | # 横軸と縦軸のラベルを定義 127 | plt.xlabel('QueFrency [sec]') 128 | plt.ylabel('Value') 129 | 130 | # 表示領域を制限 131 | plt.xlim([0, fft_size / (sampling_frequency * 2)]) 132 | plt.ylim([-1.0, 2.0]) 133 | 134 | # プロットを保存する 135 | plt.savefig(out_plot) 136 | -------------------------------------------------------------------------------- /01compute_features/test_fft.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # wavファイルの一部の区間をフーリエ変換し, 5 | # 振幅スペクトルをプロットします. 6 | # 7 | 8 | # wavデータを読み込むためのモジュール(wave)をインポート 9 | import wave 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # プロット用モジュール(matplotlib)をインポート 15 | import matplotlib.pyplot as plt 16 | 17 | # 18 | # メイン関数 19 | # 20 | if __name__ == "__main__": 21 | # 開くwavファイル 22 | wav_file = '../data/wav/BASIC5000_0001.wav' 23 | 24 | # 分析する時刻.BASIC5000_0001.wav では, 25 | # 以下の時刻は音素"o"を発話している 26 | target_time = 0.58 27 | target_time = 0.74 28 | 29 | # FFT(高速フーリエ変換)を行う範囲のサンプル数 30 | # 2のべき乗である必要がある 31 | fft_size = 1024 32 | 33 | # プロットを出力するファイル(pngファイル) 34 | out_plot = './spectrum.png' 35 | 36 | # wavファイルを開き,以降の処理を行う 37 | with wave.open(wav_file) as wav: 38 | # サンプリング周波数 [Hz] を取得 39 | sampling_frequency = wav.getframerate() 40 | 41 | # wavデータを読み込む 42 | waveform = wav.readframes(wav.getnframes()) 43 | 44 | # 読み込んだデータはバイナリ値(16bit integer) 45 | # なので,数値(整数)に変換する 46 | waveform = np.frombuffer(waveform, dtype=np.int16) 47 | 48 | # 分析する時刻をサンプル番号に変換 49 | target_index = np.int(target_time * sampling_frequency) 50 | 51 | # FFTを実施する区間分の波形データを取り出す 52 | frame = waveform[target_index: target_index + fft_size] 53 | 54 | # FFTを実施する 55 | spectrum = np.fft.fft(frame) 56 | 57 | # 振幅スペクトルを得る 58 | absolute = np.abs(spectrum) 59 | 60 | # 振幅スペクトルは左右対称なので,左半分までのみを用いる 61 | absolute = absolute[:np.int(fft_size/2) + 1] 62 | 63 | # 対数を取り,対数振幅スペクトルを計算 64 | log_absolute = np.log(absolute + 1E-7) 65 | 66 | # 67 | # 時間波形と対数振幅スペクトルをプロット 68 | # 69 | 70 | # プロットの描画領域を作成 71 | plt.figure(figsize=(10,10)) 72 | 73 | # 描画領域を縦に2分割し, 74 | # 上側に時間波形をプロットする 75 | plt.subplot(2, 1, 1) 76 | 77 | # 横軸(時間軸)を作成する 78 | time_axis = target_time \ 79 | + np.arange(fft_size) / sampling_frequency 80 | 81 | # 時間波形のプロット 82 | plt.plot(time_axis, frame) 83 | 84 | # プロットのタイトルと,横軸と縦軸のラベルを定義 85 | plt.title('waveform') 86 | plt.xlabel('Time [sec]') 87 | plt.ylabel('Value') 88 | 89 | # 横軸の表示領域を分析区間の時刻に制限 90 | plt.xlim([target_time, 91 | target_time + fft_size / sampling_frequency]) 92 | 93 | # 2分割された描画領域の下側に 94 | # 対数振幅スペクトルをプロットする 95 | plt.subplot(2, 1, 2) 96 | 97 | # 横軸(周波数軸)を作成する 98 | freq_axis = np.arange(np.int(fft_size/2)+1) \ 99 | * sampling_frequency / fft_size 100 | 101 | # 対数振幅スペクトルをプロット 102 | plt.plot(freq_axis, log_absolute) 103 | 104 | # プロットのタイトルと,横軸と縦軸のラベルを定義 105 | plt.title('log-absolute spectrum') 106 | plt.xlabel('Frequency [Hz]') 107 | plt.ylabel('Value') 108 | 109 | # 横軸の表示領域を0~最大周波数に制限 110 | plt.xlim([0, sampling_frequency / 2]) 111 | 112 | # プロットを保存する 113 | plt.savefig(out_plot) 114 | 115 | -------------------------------------------------------------------------------- /01compute_features/test_pre_emphasis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # wavファイルの一部の区間をフーリエ変換し, 5 | # 振幅スペクトルをプロットします. 6 | # 7 | 8 | # wavデータを読み込むためのモジュール(wave)をインポート 9 | import wave 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # プロット用モジュール(matplotlib)をインポート 15 | import matplotlib.pyplot as plt 16 | 17 | # 18 | # メイン関数 19 | # 20 | if __name__ == "__main__": 21 | # 開くwavファイル 22 | wav_file = '../data/wav/BASIC5000_0001.wav' 23 | 24 | # 分析する時刻.BASIC5000_0001.wav では, 25 | # 以下の時刻は音素"o"を発話している 26 | target_time = 0.58 27 | 28 | # FFT(高速フーリエ変換)を行う範囲のサンプル数 29 | # 2のべき乗である必要がある 30 | fft_size = 1024 31 | 32 | # プロットを出力するファイル(pngファイル) 33 | out_plot = './pre_emphasis.png' 34 | 35 | # wavファイルを開き、以降の処理を行う 36 | with wave.open(wav_file) as wav: 37 | # サンプリング周波数 [Hz] を取得 38 | sampling_frequency = wav.getframerate() 39 | 40 | # wavデータを読み込む 41 | waveform = wav.readframes(wav.getnframes()) 42 | 43 | # 読み込んだデータはバイナリ値(16bit integer) 44 | # なので,数値(整数)に変換する 45 | waveform = np.frombuffer(waveform, dtype=np.int16) 46 | 47 | # 分析する時刻をサンプル番号に変換 48 | target_index = np.int(target_time * sampling_frequency) 49 | 50 | # FFTを実施する区間分の波形データを取り出す 51 | frame = waveform[target_index: target_index + fft_size] 52 | 53 | frame_emp = np.convolve(frame,np.array([1.0, -0.97]), mode='same') 54 | # numpyの畳み込みでは0番目の要素が処理されない(window[i-1]が存在しないので)ため, 55 | # window[0-1]をwindow[0]で代用して処理する 56 | frame_emp[0] -= 0.97*frame_emp[0] 57 | 58 | h = np.zeros(fft_size) 59 | h[0] = 1.0 60 | h[1] = -0.97 61 | 62 | frame = frame * np.hamming(fft_size) 63 | frame_emp = frame_emp * np.hamming(fft_size) 64 | 65 | # FFTを実施する 66 | spectrum = np.fft.fft(frame) 67 | spectrum_emp = np.fft.fft(frame_emp) 68 | spectrum_h = np.fft.fft(h) 69 | 70 | # 振幅スペクトルを得る 71 | absolute = np.abs(spectrum) 72 | absolute_emp = np.abs(spectrum_emp) 73 | absolute_h = np.abs(spectrum_h) 74 | 75 | # 振幅スペクトルは左右対称なので,左半分までのみを用いる 76 | absolute = absolute[:np.int(fft_size/2) + 1] 77 | absolute_emp = absolute_emp[:np.int(fft_size/2) + 1] 78 | absolute_h = absolute_h[:np.int(fft_size/2) + 1] 79 | 80 | # 対数を取り、対数振幅スペクトルを計算 81 | log_absolute = np.log(absolute + 1E-7) 82 | log_absolute_emp = np.log(absolute_emp + 1E-7) 83 | log_absolute_h = np.log(absolute_h + 1E-7) 84 | 85 | # 86 | # 時間波形と対数振幅スペクトルをプロット 87 | # 88 | 89 | # プロットの描画領域を作成 90 | plt.figure(figsize=(10,10)) 91 | 92 | # 2分割された描画領域の下側に 93 | # 対数振幅スペクトルをプロットする 94 | plt.subplot(3, 1, 1) 95 | 96 | # 横軸(周波数軸)を作成する 97 | freq_axis = np.arange(np.int(fft_size/2)+1) \ 98 | * sampling_frequency / fft_size 99 | 100 | # 対数振幅スペクトルをプロット 101 | plt.plot(freq_axis, log_absolute, color='k') 102 | 103 | # プロットのタイトルと、横軸と縦軸のラベルを定義 104 | #plt.title('log-absolute spectrum without pre-emphasis (x)') 105 | plt.xlabel('Frequency [Hz]') 106 | plt.ylabel('Value') 107 | 108 | # 横軸の表示領域を0~最大周波数に制限 109 | plt.xlim([0, sampling_frequency / 2]) 110 | plt.ylim([0,15]) 111 | 112 | # 2分割された描画領域の下側に 113 | # 対数振幅スペクトルをプロットする 114 | plt.subplot(3, 1, 2) 115 | 116 | # 横軸(周波数軸)を作成する 117 | freq_axis = np.arange(np.int(fft_size/2)+1) \ 118 | * sampling_frequency / fft_size 119 | 120 | # 対数振幅スペクトルをプロット 121 | plt.plot(freq_axis, log_absolute_emp, color='k') 122 | 123 | # プロットのタイトルと、横軸と縦軸のラベルを定義 124 | #plt.title('log-absolute spectrum with pre-emphasis (x_emp)') 125 | plt.xlabel('Frequency [Hz]') 126 | plt.ylabel('Value') 127 | 128 | # 横軸の表示領域を0~最大周波数に制限 129 | plt.xlim([0, sampling_frequency / 2]) 130 | plt.ylim([0,15]) 131 | 132 | plt.subplot(3, 1, 3) 133 | 134 | # 横軸(周波数軸)を作成する 135 | freq_axis = np.arange(np.int(fft_size/2)+1) \ 136 | * sampling_frequency / fft_size 137 | 138 | # 対数振幅スペクトルをプロット 139 | plt.plot(freq_axis, log_absolute_emp - log_absolute, linestyle='dashed', color='k') 140 | plt.plot(freq_axis, log_absolute_h, color='k') 141 | 142 | # プロットのタイトルと、横軸と縦軸のラベルを定義 143 | #plt.title('log-absolute spectra of pre-emphasis filter (h) and (x_emp - x)') 144 | plt.xlabel('Frequency [Hz]') 145 | plt.ylabel('Value') 146 | 147 | # 横軸の表示領域を0~最大周波数に制限 148 | plt.xlim([0, sampling_frequency / 2]) 149 | 150 | # プロットを保存する 151 | plt.savefig(out_plot) 152 | 153 | -------------------------------------------------------------------------------- /01compute_features/test_spectrogram.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 短時間フーリエ変換を用いて 5 | # 音声のスペクトログラムを作成します. 6 | # 7 | 8 | # wavデータを読み込むためのモジュール(wave)をインポート 9 | import wave 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # プロット用モジュール(matplotlib)をインポート 15 | import matplotlib.pyplot as plt 16 | 17 | # 18 | # メイン関数 19 | # 20 | if __name__ == "__main__": 21 | # 開くwavファイル 22 | wav_file = '../data/wav/BASIC5000_0001.wav' 23 | 24 | # フレームサイズ [ミリ秒] 25 | frame_size = 25 26 | # フレームシフト [ミリ秒] 27 | frame_shift = 10 28 | 29 | # プロットを出力するファイル(pngファイル) 30 | out_plot = './spectrogram.png' 31 | 32 | # wavファイルを開き、以降の処理を行う 33 | with wave.open(wav_file) as wav: 34 | # サンプリング周波数 [Hz] を取得 35 | sample_frequency = wav.getframerate() 36 | 37 | # wavデータのサンプル数を取得 38 | num_samples = wav.getnframes() 39 | 40 | # wavデータを読み込む 41 | waveform = wav.readframes(num_samples) 42 | 43 | # 読み込んだデータはバイナリ値(16bit integer) 44 | # なので,数値(整数)に変換する 45 | waveform = np.frombuffer(waveform, dtype=np.int16) 46 | 47 | # フレームサイズをミリ秒からサンプル数に変換 48 | frame_size = int(sample_frequency \ 49 | * frame_size * 0.001) 50 | 51 | # フレームシフトをミリ秒からサンプル数へ変換 52 | frame_shift = int(sample_frequency * frame_shift * 0.001) 53 | 54 | # FFTを行う範囲のサンプル数を, 55 | # フレームサイズ以上の2のべき乗に設定 56 | fft_size = 1 57 | while fft_size < frame_size: 58 | fft_size *= 2 59 | 60 | # 短時間フーリエ変換をしたときの 61 | # 総フレーム数を計算する 62 | num_frames = (num_samples - frame_size) // frame_shift + 1 63 | 64 | # スペクトログラムの行列を用意 65 | spectrogram = np.zeros((num_frames, int(fft_size/2)+1)) 66 | 67 | # 1フレームずつ振幅スペクトルを計算する 68 | for frame_idx in range(num_frames): 69 | # 分析の開始位置は,フレーム番号(0始まり)*フレームシフト 70 | start_index = frame_idx * frame_shift 71 | 72 | # 1フレーム分の波形を抽出 73 | frame = waveform[start_index : \ 74 | start_index + frame_size].copy() 75 | 76 | # ハミング窓を掛ける 77 | frame = frame * np.hamming(frame_size) 78 | 79 | # 高速フーリエ変換(FFT)を実行 80 | spectrum = np.fft.fft(frame, n=fft_size) 81 | 82 | # 振幅スペクトルを得る 83 | absolute = np.abs(spectrum) 84 | 85 | # 振幅スペクトルは左右対称なので,左半分までのみを用いる 86 | absolute = absolute[:int(fft_size/2) + 1] 87 | 88 | # 対数を取り、対数振幅スペクトルを計算 89 | log_absolute = np.log(absolute + 1E-7) 90 | 91 | # 計算結果をスペクトログラムに格納 92 | spectrogram[frame_idx, :] = log_absolute 93 | 94 | # 95 | # 時間波形とスペクトログラムをプロット 96 | # 97 | 98 | # プロットの描画領域を作成 99 | plt.figure(figsize=(10,10)) 100 | 101 | # 描画領域を縦に2分割し、 102 | # 上側に時間波形をプロットする 103 | plt.subplot(2, 1, 1) 104 | 105 | # 横軸(時間軸)を作成する 106 | time_axis = np.arange(num_samples) / sample_frequency 107 | 108 | # 時間波形のプロット 109 | plt.plot(time_axis, waveform) 110 | 111 | # プロットのタイトルと、横軸と縦軸のラベルを定義 112 | plt.title('waveform') 113 | plt.xlabel('Time [sec]') 114 | plt.ylabel('Value') 115 | 116 | # 横軸の表示領域を0から波形終了時刻に制限 117 | plt.xlim([0, num_samples / sample_frequency]) 118 | 119 | # 2分割された描画領域の下側に 120 | # スペクトログラムをプロットする 121 | plt.subplot(2, 1, 2) 122 | 123 | # スペクトログラムの最大値を0に合わせて 124 | # カラーマップのレンジを調整 125 | spectrogram -= np.max(spectrogram) 126 | vmax = np.abs(np.min(spectrogram)) * 0.0 127 | vmin = - np.abs(np.min(spectrogram)) * 0.7 128 | 129 | # ヒストグラムをプロット 130 | plt.imshow(spectrogram.T[-1::-1,:], 131 | extent=[0, num_samples / sample_frequency, 132 | 0, sample_frequency / 2], 133 | cmap = 'gray', 134 | vmax = vmax, 135 | vmin = vmin, 136 | aspect = 'auto') 137 | 138 | # プロットのタイトルと、横軸と縦軸のラベルを定義 139 | plt.title('spectrogram') 140 | plt.xlabel('Time [sec]') 141 | plt.ylabel('Frequency [Hz]') 142 | 143 | # プロットを保存する 144 | plt.savefig(out_plot) 145 | 146 | -------------------------------------------------------------------------------- /02dp_matching/00_prepare_wav_dp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # DPマッチング用のデータを作成します. 5 | # このプログラムは00_prepare/01prepare_wav.py 6 | # を流用しています. 7 | # 8 | 9 | # サンプリング周波数を変換するためのモジュール(sox)をインポート 10 | import sox 11 | 12 | # osモジュールをインポート 13 | import os 14 | 15 | # 16 | # メイン関数 17 | # 18 | if __name__ == "__main__": 19 | 20 | # wavファイルが展開されたディレクトリ 21 | # データは repeat500 を使用する 22 | original_wav_dir = '../data/original/jsut_ver1.1/repeat500/wav' 23 | 24 | # フォーマット変換したwavファイルを出力するディレクトリ 25 | out_wav_dir = './wav' 26 | 27 | # repeat500内で使用するセット数 28 | num_set = 5 29 | 30 | # repeat500内で使用する1セットあたりの発話数 31 | num_utt_per_set = 10 32 | 33 | # 出力ディレクトリが存在しない場合は作成する 34 | os.makedirs(out_wav_dir, exist_ok=True) 35 | 36 | # soxによる音声変換クラスを呼び出す 37 | tfm = sox.Transformer() 38 | # サンプリング周波数を 16000Hz に変換するよう設定する 39 | tfm.convert(samplerate=16000) 40 | 41 | # セット x 発話数分だけ処理を実行 42 | for set_id in range(num_set): 43 | for utt_id in range(num_utt_per_set): 44 | # wavファイル名 45 | filename = 'REPEAT500_set%d_%03d' % (set_id+1, utt_id+1) 46 | # 変換元のオリジナルデータ (48000Hz)のファイル名 47 | wav_path_in = os.path.join(original_wav_dir, filename+'.wav') 48 | # 変換後のデータ(16000Hz)の保存ファイル名 49 | wav_path_out = os.path.join(out_wav_dir, filename+'.wav') 50 | 51 | print(wav_path_in) 52 | # ファイルが存在しない場合はエラー 53 | if not os.path.exists(wav_path_in): 54 | print('Error: Not found %s' % (wav_path_in)) 55 | exit() 56 | 57 | # サンプリング周波数の変換と保存を実行する 58 | tfm.build_file(input_filepath=wav_path_in, 59 | output_filepath=wav_path_out) 60 | -------------------------------------------------------------------------------- /02dp_matching/02_dp_matching.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # DPマッチングを行い,距離の計算と 5 | # アライメントを求めます. 6 | # 7 | 8 | # 数値演算用モジュール(numpy)をインポート 9 | import numpy as np 10 | 11 | # sysモジュールをインポート 12 | import sys 13 | 14 | 15 | def dp_matching(feature_1, feature_2): 16 | ''' DPマッチングを行う 17 | 入力: 18 | feature_1: 比較する特徴量系列1 19 | feature_2: 比較する特徴量系列2 20 | 出力: 21 | total_cost: 最短経路の総コスト 22 | min_path: 最短経路のフレーム対応 23 | ''' 24 | # フレーム数と次元数を取得 25 | (nframes_1, num_dims) = np.shape(feature_1) 26 | nframes_2 = np.shape(feature_2)[0] 27 | 28 | # 距離(コスト)行列を計算 29 | distance = np.zeros((nframes_1, nframes_2)) 30 | for n in range(nframes_1): 31 | for m in range(nframes_2): 32 | # feature_1 の n フレーム目と 33 | # feature_2 の m フレーム目の 34 | # ユークリッド距離の二乗を計算 35 | distance[n, m] = \ 36 | np.sum((feature_1[n] - feature_2[m])**2) 37 | 38 | # 累積コスト行列 39 | cost = np.zeros((nframes_1, nframes_2)) 40 | # 遷移の種類(縦/斜め/横)を記録する行列 41 | # 0: 縦の遷移, 1:斜めの遷移, 2:横の遷移 42 | track = np.zeros((nframes_1, nframes_2), np.int16) 43 | 44 | # スタート地点の距離 45 | cost[0, 0] = distance[0, 0] 46 | 47 | # 縦の縁: 必ず縦に遷移する 48 | for n in range(1, nframes_1): 49 | cost[n, 0] = cost[n-1, 0] + distance[n, 0] 50 | track[n, 0] = 0 51 | 52 | # 横の縁: 必ず横に遷移する 53 | for m in range(1, nframes_2): 54 | cost[0, m] = cost[0, m-1] + distance[0, m] 55 | track[0, m] = 2 56 | 57 | # それ以外: 縦横斜めの内,最小の遷移を行う 58 | for n in range(1, nframes_1): 59 | for m in range(1, nframes_2): 60 | # 縦の遷移をしたときの累積コスト 61 | vertical = cost[n-1, m] + distance[n, m] 62 | # 斜めの遷移をしたときの累積コスト 63 | # (斜めは2倍のペナルティを与える) 64 | diagonal = cost[n-1, m-1] + 2 * distance[n, m] 65 | # 横の遷移をしたときの累積コスト 66 | horizontal = cost[n, m-1] + distance[n, m] 67 | 68 | # 累積コストが最小となる遷移を選択する 69 | candidate = [vertical, diagonal, horizontal] 70 | transition = np.argmin(candidate) 71 | 72 | # 累積コストと遷移を記録する 73 | cost[n, m] = candidate[transition] 74 | track[n, m] = transition 75 | 76 | # 総コストはcost行列の最終行最終列の値 77 | # 特徴量のフレーム数で正規化する 78 | total_cost = cost[-1, -1] / (nframes_1 + nframes_2) 79 | 80 | # 81 | # バックトラック 82 | # 終端からtrackの値を見ながら逆に辿ることで, 83 | # 最小コストのパスを求める 84 | min_path = [] 85 | # 終端からスタート 86 | n = nframes_1 - 1 87 | m = nframes_2 - 1 88 | while True: 89 | # 現在のフレーム番号の組をmin_pathに加える 90 | min_path.append([n,m]) 91 | 92 | # スタート地点まで到達したら終了 93 | if n == 0 and m == 0: 94 | break 95 | 96 | # track の値を見る 97 | if track[n, m] == 0: 98 | # 縦のパスを通ってきた場合 99 | n -= 1 100 | elif track[n, m] == 1: 101 | # 斜めのパスを通ってきた場合 102 | n -= 1 103 | m -= 1 104 | else: 105 | # 横のパスを通ってきた場合 106 | m -= 1 107 | 108 | # min_path を逆順に並び替える 109 | min_path = min_path[::-1] 110 | 111 | # 総コストとパスを出力 112 | return total_cost, min_path 113 | 114 | 115 | # 116 | # メイン関数 117 | # 118 | if __name__ == "__main__": 119 | # 開くmfccファイル 120 | mfcc_file_1 = './mfcc/REPEAT500_set1_009.bin' 121 | mfcc_file_2 = './mfcc/REPEAT500_set2_009.bin' 122 | 123 | result = './alignment.txt' 124 | 125 | # MFCCの次元数 126 | num_dims = 13 127 | 128 | # 特徴量データを特徴量ファイルから読み込む 129 | mfcc_1 = np.fromfile(mfcc_file_1, dtype=np.float32) 130 | mfcc_2 = np.fromfile(mfcc_file_2, dtype=np.float32) 131 | # フレーム数 x 次元数の行列に変換 132 | mfcc_1 = mfcc_1.reshape(-1, num_dims) 133 | mfcc_2 = mfcc_2.reshape(-1, num_dims) 134 | 135 | print('size of feature_1: %d x %d' % np.shape(mfcc_1)) 136 | print('size of feature_2: %d x %d' % np.shape(mfcc_2)) 137 | 138 | # DPマッチング実施 139 | total_cost, min_path = dp_matching(mfcc_1, mfcc_2) 140 | 141 | # アライメント(フレームの対応)を書き込む 142 | with open(result, mode='w') as f: 143 | for p in min_path: 144 | f.write('%d %d\n' % (p[0], p[1])) 145 | 146 | -------------------------------------------------------------------------------- /02dp_matching/03_dtw_spectrogram.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 音声のスペクトログラムを作成後, 5 | # アライメントに従ってスペクトログラムを 6 | # 引き延ばして描画する 7 | # 8 | 9 | # wavデータを読み込むためのモジュール(wave)をインポート 10 | import wave 11 | 12 | # 数値演算用モジュール(numpy)をインポート 13 | import numpy as np 14 | 15 | # プロット用モジュール(matplotlib)をインポート 16 | import matplotlib.pyplot as plt 17 | 18 | # 19 | # メイン関数 20 | # 21 | if __name__ == "__main__": 22 | # 開くwavファイル 23 | wav_file_1 = './wav/REPEAT500_set1_009.wav' 24 | wav_file_2 = './wav/REPEAT500_set2_009.wav' 25 | 26 | alignment_file = './alignment.txt' 27 | 28 | # サンプリング周波数 29 | sample_frequency = 16000 30 | # フレームサイズ [ミリ秒] 31 | frame_size = 25 32 | # フレームシフト [ミリ秒] 33 | frame_shift = 10 34 | 35 | # プロットを出力するファイル(pngファイル) 36 | out_plot = './dtw_spectrogram.png' 37 | 38 | # フレームサイズをミリ秒からサンプル数に変換 39 | frame_size = int(sample_frequency * frame_size * 0.001) 40 | 41 | # フレームシフトをミリ秒からサンプル数へ変換 42 | frame_shift = int(sample_frequency * frame_shift * 0.001) 43 | 44 | # FFTを行う範囲のサンプル数を, 45 | # フレームサイズ以上の2のべき乗に設定 46 | fft_size = 1 47 | while fft_size < frame_size: 48 | fft_size *= 2 49 | 50 | # アライメント情報を得る 51 | alignment = [] 52 | with open(alignment_file, mode='r') as f: 53 | for line in f: 54 | parts = line.split() 55 | alignment.append([int(parts[0]), int(parts[1])]) 56 | 57 | # プロットの描画領域を作成 58 | plt.figure(figsize=(10,10)) 59 | 60 | # 2個のwavfileに対して以下を実行 61 | for file_id, wav_file in enumerate([wav_file_1, wav_file_2]): 62 | # wavファイルを開き、以降の処理を行う 63 | with wave.open(wav_file) as wav: 64 | # wavデータの情報を読み込む 65 | num_samples = wav.getnframes() 66 | waveform = wav.readframes(num_samples) 67 | waveform = np.frombuffer(waveform, dtype=np.int16) 68 | 69 | # 短時間フーリエ変換をしたときの 70 | # 総フレーム数を計算する 71 | num_frames = (num_samples - frame_size) // frame_shift + 1 72 | 73 | # スペクトログラムの行列を用意 74 | spectrogram = np.zeros((num_frames, fft_size)) 75 | 76 | # 1フレームずつ振幅スペクトルを計算する 77 | for frame_idx in range(num_frames): 78 | # 分析の開始位置は,フレーム番号(0始まり)*フレームシフト 79 | start_index = frame_idx * frame_shift 80 | 81 | # 1フレーム分の波形を抽出 82 | frame = waveform[start_index : \ 83 | start_index + frame_size].copy() 84 | 85 | # ハミング窓を掛ける 86 | frame = frame * np.hamming(frame_size) 87 | 88 | # 高速フーリエ変換(FFT)を実行 89 | spectrum = np.fft.fft(frame, n=fft_size) 90 | 91 | # 対数振幅スペクトルを計算 92 | log_absolute = np.log(np.abs(spectrum) + 1E-7) 93 | 94 | # 計算結果をスペクトログラムに格納 95 | spectrogram[frame_idx, :] = log_absolute 96 | 97 | # スペクトログラムを 98 | # アライメントに合わせて拡大する 99 | dtw_spectrogram = np.zeros((len(alignment), fft_size)) 100 | for t in range(len(alignment)): 101 | # 対応するフレーム番号 102 | idx = alignment[t][file_id] 103 | # 対応するフレーム番号のスペクトログラムを 104 | # コピーして引き延ばす 105 | dtw_spectrogram[t, :] = spectrogram[idx, :] 106 | 107 | # 108 | # 時間波形とスペクトログラムをプロット 109 | # 110 | 111 | # 描画領域を縦に2分割し、 112 | # スペクトログラムをプロットする 113 | plt.subplot(2, 1, file_id+1) 114 | 115 | # スペクトログラムの最大値を0に合わせて 116 | # カラーマップのレンジを調整 117 | dtw_spectrogram -= np.max(dtw_spectrogram) 118 | vmax = np.abs(np.min(dtw_spectrogram)) * 0.0 119 | vmin = - np.abs(np.min(dtw_spectrogram)) * 0.7 120 | 121 | # ヒストグラムをプロット 122 | plt.imshow(dtw_spectrogram.T[-1::-1,:], 123 | extent=[0, len(alignment) * \ 124 | frame_shift / sample_frequency, 125 | 0, sample_frequency], 126 | cmap = 'gray', 127 | vmax = vmax, 128 | vmin = vmin, 129 | aspect = 'auto') 130 | plt.ylim([0, sample_frequency/2]) 131 | 132 | # プロットのタイトルと、横軸と縦軸のラベルを定義 133 | plt.title('spectrogram') 134 | plt.xlabel('Time [sec]') 135 | plt.ylabel('Frequency [Hz]') 136 | 137 | # プロットを保存する 138 | plt.savefig(out_plot) 139 | 140 | -------------------------------------------------------------------------------- /02dp_matching/04_dp_matching_knn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # DPマッチングにより, 5 | # 発話内容のインデックスを推定します. 6 | # 7 | 8 | # 数値演算用モジュール(numpy)をインポート 9 | import numpy as np 10 | 11 | # sysモジュールをインポート 12 | import sys 13 | 14 | 15 | def dp_matching(feature_1, feature_2): 16 | ''' DPマッチングを行う 17 | 入力: 18 | feature_1: 比較する特徴量系列1 19 | feature_2: 比較する特徴量系列2 20 | 出力: 21 | total_cost: 最短経路の総コスト 22 | min_path: 最短経路のフレーム対応 23 | ''' 24 | # フレーム数と次元数を取得 25 | (nframes_1, num_dims) = np.shape(feature_1) 26 | nframes_2 = np.shape(feature_2)[0] 27 | 28 | # 距離(コスト)行列を計算 29 | distance = np.zeros((nframes_1, nframes_2)) 30 | for n in range(nframes_1): 31 | for m in range(nframes_2): 32 | # feature_1 の n フレーム目と 33 | # feature_2 の m フレーム目の 34 | # ユークリッド距離の二乗を計算 35 | distance[n, m] = \ 36 | np.sum((feature_1[n] - feature_2[m])**2) 37 | 38 | # 累積コスト行列 39 | cost = np.zeros((nframes_1, nframes_2)) 40 | # 遷移の種類(縦/斜め/横)を記録する行列 41 | # 0: 縦の遷移, 1:斜めの遷移, 2:横の遷移 42 | track = np.zeros((nframes_1, nframes_2), np.int16) 43 | 44 | # スタート地点の距離 45 | cost[0, 0] = distance[0, 0] 46 | 47 | # 縦の縁: 必ず縦に遷移する 48 | for n in range(1, nframes_1): 49 | cost[n, 0] = cost[n-1, 0] + distance[n, 0] 50 | track[n, 0] = 0 51 | 52 | # 横の縁: 必ず横に遷移する 53 | for m in range(1, nframes_2): 54 | cost[0, m] = cost[0, m-1] + distance[0, m] 55 | track[0, m] = 2 56 | 57 | # それ以外: 縦横斜めの内,最小の遷移を行う 58 | for n in range(1, nframes_1): 59 | for m in range(1, nframes_2): 60 | # 縦の遷移をしたときの累積コスト 61 | vertical = cost[n-1, m] + distance[n, m] 62 | # 斜めの遷移をしたときの累積コスト 63 | # (斜めは2倍のペナルティを与える) 64 | diagonal = cost[n-1, m-1] + 2 * distance[n, m] 65 | # 横の遷移をしたときの累積コスト 66 | horizontal = cost[n, m-1] + distance[n, m] 67 | 68 | # 累積コストが最小となる遷移を選択する 69 | candidate = [vertical, diagonal, horizontal] 70 | transition = np.argmin(candidate) 71 | 72 | # 累積コストと遷移を記録する 73 | cost[n, m] = candidate[transition] 74 | track[n, m] = transition 75 | 76 | # 総コストはcost行列の最終行最終列の値 77 | # 特徴量のフレーム数で正規化する 78 | total_cost = cost[-1, -1] / (nframes_1 + nframes_2) 79 | 80 | # 81 | # バックトラック 82 | # 終端からtrackの値を見ながら逆に辿ることで, 83 | # 最小コストのパスを求める 84 | min_path = [] 85 | # 終端からスタート 86 | n = nframes_1 - 1 87 | m = nframes_2 - 1 88 | while True: 89 | # 現在のフレーム番号の組をmin_pathに加える 90 | min_path.append([n,m]) 91 | 92 | # スタート地点まで到達したら終了 93 | if n == 0 and m == 0: 94 | break 95 | 96 | # track の値を見る 97 | if track[n, m] == 0: 98 | # 縦のパスを通ってきた場合 99 | n -= 1 100 | elif track[n, m] == 1: 101 | # 斜めのパスを通ってきた場合 102 | n -= 1 103 | m -= 1 104 | else: 105 | # 横のパスを通ってきた場合 106 | m -= 1 107 | 108 | # min_path を逆順に並び替える 109 | min_path = min_path[::-1] 110 | 111 | # 総コストとパスを出力 112 | return total_cost, min_path 113 | 114 | 115 | # 116 | # メイン関数 117 | # 118 | if __name__ == "__main__": 119 | # 認識対象のセット番号と発話番号 120 | query_set = 1 121 | query_utt = 9 122 | 123 | # K-nearest neighborのパラメータ 124 | K = 3 125 | 126 | # MFCCの次元数 127 | num_dims = 13 128 | # 総セット数 129 | num_set = 5 130 | # 発話の種類数 131 | num_utt = 10 132 | 133 | # 特徴量データを特徴量ファイルから読み込む 134 | query_file = './mfcc/REPEAT500_set%d_%03d.bin' % \ 135 | (query_set, query_utt) 136 | query = np.fromfile(query_file, dtype=np.float32) 137 | query = query.reshape(-1, num_dims) 138 | 139 | cost = [] 140 | for set_id in range(1, num_set+1): 141 | for utt_id in range(1, num_utt+1): 142 | # query と同じセットは比較しない 143 | if set_id == query_set: 144 | continue 145 | 146 | # 比較対象の特徴量を読み込む 147 | target_file = './mfcc/REPEAT500_set%d_%03d.bin' % \ 148 | (set_id, utt_id) 149 | print(target_file) 150 | target = np.fromfile(target_file, 151 | dtype=np.float32) 152 | target = target.reshape(-1, num_dims) 153 | 154 | # DPマッチング実施 155 | tmp_cost, tmp_path = dp_matching(query, target) 156 | 157 | cost.append({'utt': utt_id, 158 | 'set': set_id, 159 | 'cost': tmp_cost 160 | }) 161 | 162 | # コストの昇順に並び替える 163 | cost = sorted(cost, key=lambda x:x['cost']) 164 | 165 | # コストのランキングを表示する 166 | for n in range(len(cost)): 167 | print('%d: utt: %d, set: %d, cost: %.3f' % \ 168 | (n+1, 169 | cost[n]['utt'], 170 | cost[n]['set'], 171 | cost[n]['cost'])) 172 | 173 | # 174 | # K-nearest neighbor を行う 175 | # 176 | voting = np.zeros(num_utt, np.int16) 177 | for n in range(K): 178 | # トップK個の発話IDで投票を行う 179 | voting[cost[n]['utt']-1] += 1 180 | 181 | # 投票の最も大きかった発話IDを出力する 182 | max_voted = np.argmax(voting) + 1 183 | print('Estimated utterance id = %d' % max_voted) 184 | 185 | 186 | -------------------------------------------------------------------------------- /02dp_matching/compare_spectrogram.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 短時間フーリエ変換を用いて 5 | # 音声のスペクトログラムを作成します. 6 | # 7 | 8 | # wavデータを読み込むためのモジュール(wave)をインポート 9 | import wave 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # プロット用モジュール(matplotlib)をインポート 15 | import matplotlib.pyplot as plt 16 | 17 | # 18 | # メイン関数 19 | # 20 | if __name__ == "__main__": 21 | # 開くwavファイル 22 | wav_file_1 = './wav/REPEAT500_set1_009.wav' 23 | wav_file_2 = './wav/REPEAT500_set2_009.wav' 24 | 25 | # サンプリング周波数 26 | sample_frequency = 16000 27 | # フレームサイズ [ミリ秒] 28 | frame_size = 25 29 | # フレームシフト [ミリ秒] 30 | frame_shift = 10 31 | 32 | # プロットを出力するファイル(pngファイル) 33 | out_plot = './spectrogram.png' 34 | 35 | # フレームサイズをミリ秒からサンプル数に変換 36 | frame_size = int(sample_frequency * frame_size * 0.001) 37 | 38 | # フレームシフトをミリ秒からサンプル数へ変換 39 | frame_shift = int(sample_frequency * frame_shift * 0.001) 40 | 41 | # FFTを行う範囲のサンプル数を, 42 | # フレームサイズ以上の2のべき乗に設定 43 | fft_size = 1 44 | while fft_size < frame_size: 45 | fft_size *= 2 46 | 47 | # 48 | # 時間の縮尺を統一してスペクトログラムを 49 | # 表示させるため,時間を長い方に統一する 50 | # 51 | max_num_samples = 0 52 | for n, wav_file in enumerate([wav_file_1, wav_file_2]): 53 | with wave.open(wav_file) as wav: 54 | num_samples = wav.getnframes() 55 | max_num_samples = max([num_samples, max_num_samples]) 56 | # 最大時間に対するフレーム数を計算 57 | max_num_frames = (max_num_samples - frame_size) // frame_shift + 1 58 | 59 | # プロットの描画領域を作成 60 | plt.figure(figsize=(10,10)) 61 | 62 | # 2個のwavfileに対して以下を実行 63 | for n, wav_file in enumerate([wav_file_1, wav_file_2]): 64 | # wavファイルを開き、以降の処理を行う 65 | with wave.open(wav_file) as wav: 66 | # wavデータの情報を読み込む 67 | num_samples = wav.getnframes() 68 | waveform = wav.readframes(num_samples) 69 | waveform = np.frombuffer(waveform, dtype=np.int16) 70 | 71 | # 短時間フーリエ変換をしたときの 72 | # 総フレーム数を計算する 73 | num_frames = (num_samples - frame_size) // frame_shift + 1 74 | 75 | # スペクトログラムの行列を用意 76 | # フレーム数は最大フレーム数にする 77 | spectrogram = np.zeros((max_num_frames, fft_size)) 78 | 79 | # 1フレームずつ振幅スペクトルを計算する 80 | for frame_idx in range(num_frames): 81 | # 分析の開始位置は,フレーム番号(0始まり)*フレームシフト 82 | start_index = frame_idx * frame_shift 83 | 84 | # 1フレーム分の波形を抽出 85 | frame = waveform[start_index : \ 86 | start_index + frame_size].copy() 87 | 88 | # ハミング窓を掛ける 89 | frame = frame * np.hamming(frame_size) 90 | 91 | # 高速フーリエ変換(FFT)を実行 92 | spectrum = np.fft.fft(frame, n=fft_size) 93 | 94 | # 対数振幅スペクトルを計算 95 | log_absolute = np.log(np.abs(spectrum) + 1E-7) 96 | 97 | # 計算結果をスペクトログラムに格納 98 | spectrogram[frame_idx, :] = log_absolute 99 | 100 | # 101 | # 時間波形とスペクトログラムをプロット 102 | # 103 | 104 | # 描画領域を縦に2分割し、 105 | # スペクトログラムをプロットする 106 | plt.subplot(2, 1, n+1) 107 | 108 | # スペクトログラムの最大値を0に合わせて 109 | # カラーマップのレンジを調整 110 | spectrogram -= np.max(spectrogram) 111 | vmax = np.abs(np.min(spectrogram)) * 0.0 112 | vmin = - np.abs(np.min(spectrogram)) * 0.7 113 | 114 | # ヒストグラムをプロット 115 | plt.imshow(spectrogram.T[-1::-1,:], 116 | extent=[0, max_num_samples / sample_frequency, 117 | 0, sample_frequency], 118 | cmap = 'gray', 119 | vmax = vmax, 120 | vmin = vmin, 121 | aspect = 'auto') 122 | plt.ylim([0, sample_frequency/2]) 123 | 124 | # プロットのタイトルと、横軸と縦軸のラベルを定義 125 | plt.title('spectrogram') 126 | plt.xlabel('Time [sec]') 127 | plt.ylabel('Frequency [Hz]') 128 | 129 | # プロットを保存する 130 | plt.savefig(out_plot) 131 | 132 | -------------------------------------------------------------------------------- /03gmm_hmm/00_make_label.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # ラベルファイルの音素をIDに変換して保存します. 5 | # また、音素とIDの対応を記したリストを出力します. 6 | # 7 | 8 | # osモジュールをインポート 9 | import os 10 | 11 | def phone_to_int(label_str, 12 | label_int, 13 | phone_list, 14 | insert_sil=False): 15 | ''' 16 | 音素リストを使ってラベルファイルの 17 | 音素を数値に変換する 18 | label_str: 文字で記述されたラベルファイル 19 | labelint: 文字を数値に変換した書き込み先の 20 | ラベルファイル 21 | phone_list: 音素リスト 22 | insert_sil: Trueの場合,テキストの最初と最後に 23 | 空白を挿入する 24 | ''' 25 | # 各ファイルを開く 26 | with open(label_str, mode='r') as f_in, \ 27 | open(label_int, mode='w') as f_out: 28 | # ラベルファイルを一行ずつ読み込む 29 | for line in f_in: 30 | # 読み込んだ行をスペースで区切り, 31 | # リスト型の変数にする 32 | text = line.split() 33 | 34 | # リストの0番目の要素は発話IDなので, 35 | # そのまま出力する 36 | f_out.write('%s' % text[0]) 37 | 38 | # insert_silがTrueなら, 39 | # 先頭に0(ポーズ)を挿入 40 | if insert_sil: 41 | f_out.write(' 0') 42 | 43 | # リストの1番目以降の要素は文字なので, 44 | # 1文字ずつ数字に置き換える 45 | for u in text[1:]: 46 | # 音素リストに無い場合はエラー 47 | if not u in phone_list: 48 | sys.stderr.write('phone_to_int: \ 49 | unknown phone %s\n' % u) 50 | exit(1) 51 | # 音素のインデクスを出力 52 | f_out.write(' %d' % \ 53 | (phone_list.index(u))) 54 | 55 | # insert_silがTrueなら, 56 | # 末尾に0(ポーズ)を挿入 57 | if insert_sil: 58 | f_out.write(' 0') 59 | # 改行 60 | f_out.write('\n') 61 | 62 | 63 | # 64 | # メイン関数 65 | # 66 | if __name__ == "__main__": 67 | # 訓練データのラベルファイルのパス 68 | label_train_str = \ 69 | '../data/label/train_small/text_phone' 70 | 71 | # 訓練データの処理結果の出力先ディレクトリ 72 | out_train_dir = \ 73 | './exp/data/train_small' 74 | 75 | # 開発データのラベルファイルのパス 76 | # (開発データはGMM-HMMには使いませんが, 77 | # DNN-HMMで使用します.) 78 | label_dev_str = \ 79 | '../data/label/dev/text_phone' 80 | 81 | # 開発データの処理結果の出力先ディレクトリ 82 | out_dev_dir = \ 83 | './exp/data/dev' 84 | 85 | # 音素リスト 86 | phone_file = './phones.txt' 87 | 88 | # ポーズを表す記号 89 | silence_phone = 'pau' 90 | 91 | # Trueの場合,文章の先頭と末尾にポーズを挿入する. 92 | insert_sil = True 93 | 94 | # 音素リストの先頭にはポーズ記号を入れておく 95 | phone_list = [silence_phone] 96 | # 音素リストファイルを開き,phone_listに格納 97 | with open(phone_file, mode='r') as f: 98 | for line in f: 99 | # 空白や改行を消して音素記号を取得 100 | phone = line.strip() 101 | # 音素リストの末尾に加える 102 | phone_list.append(phone) 103 | 104 | 105 | # 訓練/開発データの情報をリスト化 106 | label_str_list = [label_train_str, 107 | label_dev_str] 108 | out_dir_list = [out_train_dir, 109 | out_dev_dir] 110 | 111 | # 訓練/開発データについてそれぞれ処理 112 | for (label_str, out_dir) \ 113 | in zip(label_str_list, out_dir_list): 114 | 115 | # 出力ディレクトリが存在しない場合は作成する 116 | os.makedirs(out_dir, exist_ok=True) 117 | 118 | # 音素と数値の対応リストを出力 119 | out_phone_list = \ 120 | os.path.join(out_dir, 'phone_list') 121 | with open(out_phone_list, 'w') as f: 122 | for i, phone in enumerate(phone_list): 123 | # リストに登録されている順番を 124 | # その音素に対応する数値とする 125 | f.write('%s %d\n' % (phone, i)) 126 | 127 | # ラベルの音素記号を数字に変換して出力 128 | label_int = \ 129 | os.path.join(out_dir, 'text_int') 130 | phone_to_int(label_str, 131 | label_int, 132 | phone_list, 133 | insert_sil) 134 | 135 | -------------------------------------------------------------------------------- /03gmm_hmm/01_make_proto.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Monophone-HMM の定義ファイルを作成します. 5 | # 作成するHMMは left-to-right 型で 6 | # 混合数は1です.対角共分散行列を仮定します. 7 | # 8 | 9 | # hmmfunc.pyからMonoPhoneHMMクラスをインポート 10 | from hmmfunc import MonoPhoneHMM 11 | 12 | # osモジュールをインポート 13 | import os 14 | 15 | # 16 | # メイン関数 17 | # 18 | if __name__ == "__main__": 19 | 20 | # 音素リスト 21 | phone_list_file = \ 22 | './exp/data/train_small/phone_list' 23 | 24 | # 各音素HMMの状態数 25 | num_states = 3 26 | 27 | # 入力特徴量の次元数 28 | # ここではMFCCを使用するため, 29 | # MFCCの次元数を入れる 30 | num_dims = 13 31 | 32 | # 自己ループ確率の初期値 33 | prob_loop = 0.7 34 | 35 | # 出力フォルダ 36 | out_dir = \ 37 | './exp/model_%dstate_1mix' % (num_states) 38 | 39 | # 40 | # 処理ここから 41 | # 42 | # 出力ディレクトリが存在しない場合は作成する 43 | os.makedirs(out_dir, exist_ok=True) 44 | 45 | # 音素リストファイルを開き,phone_listに格納 46 | phone_list = [] 47 | with open(phone_list_file, mode='r') as f: 48 | for line in f: 49 | # 音素リストファイルから音素を取得 50 | phone = line.split()[0] 51 | # 音素リストの末尾に加える 52 | phone_list.append(phone) 53 | 54 | # MonoPhoneHMMクラスを呼び出す 55 | hmm = MonoPhoneHMM() 56 | 57 | # HMMのプロトタイプを作成する 58 | hmm.make_proto(phone_list, num_states, 59 | prob_loop, num_dims) 60 | 61 | # HMMのプロトタイプをjson形式で保存 62 | hmm.save_hmm(os.path.join(out_dir, 'hmmproto')) 63 | 64 | -------------------------------------------------------------------------------- /03gmm_hmm/02_init_hmm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # HMMのプロトタイプを読み込み, 5 | # フラットスタートで初期化します. 6 | # 7 | 8 | # hmmfunc.pyからMonoPhoneHMMクラスをインポート 9 | from hmmfunc import MonoPhoneHMM 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # osモジュールをインポート 15 | import os 16 | 17 | # 18 | # メイン関数 19 | # 20 | if __name__ == "__main__": 21 | 22 | # HMMプロトタイプ 23 | hmmproto = './exp/model_3state_1mix/hmmproto' 24 | 25 | # 学習データの特徴量の平均/標準偏差ファイル 26 | mean_std_file = \ 27 | '../01compute_features/mfcc/train_small/mean_std.txt' 28 | 29 | # 出力ディレクトリ 30 | out_dir = os.path.dirname(hmmproto) 31 | 32 | # 33 | # 処理ここから 34 | # 35 | 36 | # 出力ディレクトリが存在しない場合は作成する 37 | os.makedirs(out_dir, exist_ok=True) 38 | 39 | # 特徴量の平均/標準偏差ファイルを読み込む 40 | with open(mean_std_file, mode='r') as f: 41 | # 全行読み込み 42 | lines = f.readlines() 43 | # 1行目(0始まり)が平均値ベクトル(mean), 44 | # 3行目が標準偏差ベクトル(std) 45 | mean_line = lines[1] 46 | std_line = lines[3] 47 | # スペース区切りのリストに変換 48 | mean = mean_line.split() 49 | std = std_line.split() 50 | # numpy arrayに変換 51 | mean = np.array(mean, dtype=np.float64) 52 | std = np.array(std, dtype=np.float64) 53 | # 標準偏差を分散に変換 54 | var = std ** 2 55 | 56 | # MonoPhoneHMMクラスを呼び出す 57 | hmm = MonoPhoneHMM() 58 | 59 | # HMMプロトタイプを読み込む 60 | hmm.load_hmm(hmmproto) 61 | 62 | # フラットスタート初期化を実行 63 | hmm.flat_init(mean, var) 64 | 65 | # HMMのプロトタイプをjson形式で保存 66 | hmm.save_hmm(os.path.join(out_dir, '0.hmm')) 67 | 68 | -------------------------------------------------------------------------------- /03gmm_hmm/03_train_gmmhmm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # GMM-HMMを学習します. 5 | # 6 | 7 | # hmmfunc.pyからMonoPhoneHMMクラスをインポート 8 | from hmmfunc import MonoPhoneHMM 9 | 10 | # 数値演算用モジュール(numpy)をインポート 11 | import numpy as np 12 | 13 | # os, sysモジュールをインポート 14 | import sys 15 | import os 16 | 17 | # 18 | # メイン関数 19 | # 20 | if __name__ == "__main__": 21 | 22 | # 学習元のHMMファイル 23 | base_hmm = './exp/model_3state_1mix/0.hmm' 24 | 25 | # 訓練データの特徴量リストのファイル 26 | feat_scp = \ 27 | '../01compute_features/mfcc/train_small/feats.scp' 28 | 29 | # 訓練データのラベルファイル 30 | label_file = \ 31 | './exp/data/train_small/text_int' 32 | 33 | # 学習結果を保存していくフォルダ 34 | work_dir = './exp' 35 | 36 | # 更新回数 37 | num_iter = 10 38 | 39 | # 混合数を増やす回数 40 | # 増やすたびに混合数は2倍になる 41 | # 最終的な混合数は2^(mixup_time)となる 42 | # 更新回数はnum_iter*(mixup_time+1) 43 | mixup_time = 1 44 | 45 | # 学習に用いる発話数 46 | # 実際は全ての発話を用いるが,時間がかかるため 47 | # このプログラムでは一部の発話のみを使っている 48 | num_utters = 50 49 | 50 | # 51 | # 処理ここから 52 | # 53 | 54 | # MonoPhoneHMMクラスを呼び出す 55 | hmm = MonoPhoneHMM() 56 | 57 | # 学習前のHMMを読み込む 58 | hmm.load_hmm(base_hmm) 59 | 60 | # 学習元HMMファイルの状態数を得る 61 | num_states = hmm.num_states 62 | 63 | # 学習元HMMファイルの混合数を得る 64 | num_mixture = hmm.num_mixture 65 | 66 | # ラベルファイルを開き,発話ID毎の 67 | # ラベル情報を得る 68 | label_list = {} 69 | with open(label_file, mode='r') as f: 70 | for line in f: 71 | # 0列目は発話ID 72 | utt = line.split()[0] 73 | # 1列目以降はラベル 74 | lab = line.split()[1:] 75 | # 各要素は文字として読み込まれているので, 76 | # 整数値に変換する 77 | lab = np.int64(lab) 78 | # label_listに登録 79 | label_list[utt] = lab 80 | 81 | # 特徴量リストファイルを開き, 82 | # 発話ID毎の特徴量ファイルのパスを得る 83 | feat_list = {} 84 | with open(feat_scp, mode='r') as f: 85 | # 特徴量のパスをfeat_listに追加していく 86 | # このとき,学習に用いる発話数分だけ追加する 87 | # (全てのデータを学習に用いると時間がかかるため) 88 | for n, line in enumerate(f): 89 | if n >= num_utters: 90 | break 91 | # 0列目は発話ID 92 | utt = line.split()[0] 93 | # 1列目はファイルパス 94 | ff = line.split()[1] 95 | # 3列目は次元数 96 | nd = int(line.split()[3]) 97 | # 発話IDがlabel_に存在しなければエラー 98 | if not utt in label_list: 99 | sys.stderr.write(\ 100 | '%s does not have label\n' % (utt)) 101 | exit(1) 102 | # 次元数がHMMの次元数と一致しなければエラー 103 | if hmm.num_dims != nd: 104 | sys.stderr.write(\ 105 | '%s: unexpected #dims (%d)\n'\ 106 | % (utt, nd)) 107 | exit(1) 108 | # feat_fileに登録 109 | feat_list[utt] = ff 110 | 111 | 112 | # 113 | # 学習処理 114 | # 115 | 116 | # 出力ディレクトリ名 117 | model_name = 'model_%dstate_%dmix' \ 118 | % (num_states, num_mixture) 119 | out_dir = os.path.join(work_dir, model_name) 120 | 121 | # 出力ディレクトリが存在しない場合は作成する 122 | os.makedirs(out_dir, exist_ok=True) 123 | 124 | # 混合数の増加回数の分だけループ 125 | for m in range(mixup_time+1): 126 | # 127 | # 混合数増加の処理を行い, 128 | # 新しいフォルダに0.hmmという名前で保存する 129 | # 130 | if m > 0: 131 | # 混合数増加処理を行う 132 | hmm.mixup() 133 | # 混合数を2倍にする 134 | num_mixture *= 2 135 | 136 | # 出力ディレクトリ名 137 | model_name = 'model_%dstate_%dmix' \ 138 | % (num_states, num_mixture) 139 | out_dir = os.path.join(work_dir, model_name) 140 | 141 | # 出力ディレクトリが存在しない場合は作成する 142 | os.makedirs(out_dir, exist_ok=True) 143 | 144 | # HMMを保存 145 | out_hmm = os.path.join(out_dir, '0.hmm') 146 | hmm.save_hmm(out_hmm) 147 | print('Increased mixtures %d -> %d' \ 148 | % (num_mixture/2, num_mixture)) 149 | print('saved model: %s' % (out_hmm)) 150 | 151 | # 152 | # 現在のGMM混合数で規定回数学習を行う 153 | # 154 | # num_iterの数だけ更新を繰り返す 155 | for iter in range(num_iter): 156 | print('%d-th iterateion' % (iter+1)) 157 | # 学習(1iteration分) 158 | hmm.train(feat_list, label_list) 159 | 160 | # HMMのプロトタイプをjson形式で保存 161 | out_hmm = os.path.join(out_dir, 162 | '%d.hmm' % (iter+1)) 163 | # 学習したHMMを保存 164 | hmm.save_hmm(out_hmm) 165 | print('saved model: %s' % (out_hmm)) 166 | 167 | -------------------------------------------------------------------------------- /03gmm_hmm/03_train_sgmhmm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 混合数1(Single Gaussian Model)のHMMを学習します. 5 | # 6 | 7 | # hmmfunc.pyからMonoPhoneHMMクラスをインポート 8 | from hmmfunc import MonoPhoneHMM 9 | 10 | # 数値演算用モジュール(numpy)をインポート 11 | import numpy as np 12 | 13 | # os, sysモジュールをインポート 14 | import sys 15 | import os 16 | 17 | # 18 | # メイン関数 19 | # 20 | if __name__ == "__main__": 21 | 22 | # 学習元のHMMファイル 23 | base_hmm = './exp/model_3state_1mix/0.hmm' 24 | 25 | # 訓練データの特徴量リストのファイル 26 | feat_scp = \ 27 | '../01compute_features/mfcc/train_small/feats.scp' 28 | 29 | # 訓練データのラベルファイル 30 | label_file = \ 31 | './exp/data/train_small/text_int' 32 | 33 | # 更新回数 34 | num_iter = 10 35 | 36 | # 学習に用いる発話数 37 | # 実際は全ての発話を用いるが,時間がかかるため 38 | # このプログラムでは一部の発話のみを使っている 39 | num_utters = 50 40 | 41 | # 出力ディレクトリ 42 | out_dir = os.path.dirname(base_hmm) 43 | 44 | # 45 | # 処理ここから 46 | # 47 | 48 | # 出力ディレクトリが存在しない場合は作成する 49 | os.makedirs(out_dir, exist_ok=True) 50 | 51 | # MonoPhoneHMMクラスを呼び出す 52 | hmm = MonoPhoneHMM() 53 | 54 | # 学習前のHMMを読み込む 55 | hmm.load_hmm(base_hmm) 56 | 57 | # ラベルファイルを開き,発話ID毎の 58 | # ラベル情報を得る 59 | label_list = {} 60 | with open(label_file, mode='r') as f: 61 | for line in f: 62 | # 0列目は発話ID 63 | utt = line.split()[0] 64 | # 1列目以降はラベル 65 | lab = line.split()[1:] 66 | # 各要素は文字として読み込まれているので, 67 | # 整数値に変換する 68 | lab = np.int64(lab) 69 | # label_listに登録 70 | label_list[utt] = lab 71 | 72 | # 特徴量リストファイルを開き, 73 | # 発話ID毎の特徴量ファイルのパスを得る 74 | feat_list = {} 75 | with open(feat_scp, mode='r') as f: 76 | # 特徴量のパスをfeat_listに追加していく 77 | # このとき,学習に用いる発話数分だけ追加する 78 | # (全てのデータを学習に用いると時間がかかるため) 79 | for n, line in enumerate(f): 80 | if n >= num_utters: 81 | break 82 | # 0列目は発話ID 83 | utt = line.split()[0] 84 | # 1列目はファイルパス 85 | ff = line.split()[1] 86 | # 3列目は次元数 87 | nd = int(line.split()[3]) 88 | # 発話IDがlabel_に存在しなければエラー 89 | if not utt in label_list: 90 | sys.stderr.write(\ 91 | '%s does not have label\n' % (utt)) 92 | exit(1) 93 | # 次元数がHMMの次元数と一致しなければエラー 94 | if hmm.num_dims != nd: 95 | sys.stderr.write(\ 96 | '%s: unexpected #dims (%d)\n'\ 97 | % (utt, nd)) 98 | exit(1) 99 | # feat_fileに登録 100 | feat_list[utt] = ff 101 | 102 | 103 | # 104 | # 学習処理 105 | # 106 | # num_iterの数だけ更新を繰り返す 107 | for iter in range(num_iter): 108 | print('%d-th iterateion' % (iter+1)) 109 | # 学習(1iteration分) 110 | hmm.train(feat_list, label_list) 111 | 112 | # HMMのプロトタイプをjson形式で保存 113 | out_hmm = os.path.join(out_dir, 114 | '%d.hmm' % (iter+1)) 115 | # 学習したHMMを保存 116 | hmm.save_hmm(out_hmm) 117 | print('saved model: %s' % (out_hmm)) 118 | 119 | -------------------------------------------------------------------------------- /03gmm_hmm/04_prepare_testdata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # HMMテストデータを作成します. 5 | # COUNTERSUFFIX26_01.wav を単語毎に区切り, 6 | # 16kHzのwavデータを作成します. 7 | # 8 | 9 | # サンプリング周波数を変換するためのモジュール(sox)をインポート 10 | import sox 11 | 12 | # wavデータを読み込むためのモジュール(wave)をインポート 13 | import wave 14 | 15 | # 数値演算用モジュール(numpy)をインポート 16 | import numpy as np 17 | 18 | # osモジュールをインポート 19 | import os 20 | 21 | # 22 | # メイン関数 23 | # 24 | if __name__ == "__main__": 25 | 26 | # データは COUNTERSUFFIX26_01 を使用する 27 | original_wav = \ 28 | '../data/original/jsut_ver1.1/'\ 29 | 'countersuffix26/wav/COUNTERSUFFIX26_01.wav' 30 | 31 | # 単語情報 32 | word_info = [ {'word': '一つ', 33 | 'phones': 'h i t o ts u', 34 | 'time': [0.17, 0.90]}, 35 | {'word': '二つ', 36 | 'phones': 'f u t a ts u', 37 | 'time': [1.23, 2.02]}, 38 | {'word': '三つ', 39 | 'phones': 'm i cl ts u', 40 | 'time': [2.38, 3.11]}, 41 | {'word': '四つ', 42 | 'phones': 'y o cl ts u', 43 | 'time': [3.42, 4.10]}, 44 | {'word': '五つ', 45 | 'phones': 'i ts u ts u', 46 | 'time': [4.45, 5.13]}, 47 | {'word': '六つ', 48 | 'phones': 'm u cl ts u', 49 | 'time': [5.52, 6.15]}, 50 | {'word': '七つ', 51 | 'phones': 'n a n a ts u', 52 | 'time': [6.48, 7.15]}, 53 | {'word': '八つ', 54 | 'phones': 'y a cl ts u', 55 | 'time': [7.52, 8.17]}, 56 | {'word': '九つ', 57 | 'phones': 'k o k o n o ts u', 58 | 'time': [8.51, 9.31]}, 59 | {'word': 'とお', 60 | 'phones': 't o o', 61 | 'time': [9.55, 10.10]} 62 | ] 63 | 64 | # 音素リスト 65 | phone_list_file = \ 66 | './exp/data/train_small/phone_list' 67 | 68 | # 結果出力ディレクトリ 69 | out_dir = './exp/data/test' 70 | 71 | # 加工した波形の出力ディレクトリ 72 | out_wav_dir = os.path.join(out_dir, 'wav') 73 | 74 | # 出力ディレクトリが存在しない場合は作成する 75 | os.makedirs(out_wav_dir, exist_ok=True) 76 | 77 | # soxによる音声変換クラスを呼び出す 78 | tfm = sox.Transformer() 79 | # サンプリング周波数を 16000Hz に変換するよう設定する 80 | tfm.convert(samplerate=16000) 81 | 82 | downsampled_wav = os.path.join(out_wav_dir, 83 | os.path.basename(original_wav)) 84 | 85 | # ファイルが存在しない場合はエラー 86 | if not os.path.exists(original_wav): 87 | print('Error: Not found %s' % (original_wav)) 88 | exit() 89 | 90 | # サンプリング周波数の変換と保存を実行する 91 | tfm.build_file(input_filepath=original_wav, 92 | output_filepath=downsampled_wav) 93 | 94 | # ダウンサンプリングした音声を読み込む 95 | with wave.open(downsampled_wav) as wav: 96 | # サンプリング周波数 97 | sample_frequency = wav.getframerate() 98 | # wavデータのサンプル数 99 | num_samples = wav.getnframes() 100 | # wavデータを読み込む 101 | waveform = wav.readframes(num_samples) 102 | # 数値(整数)に変換する 103 | waveform = np.frombuffer(waveform, dtype=np.int16) 104 | 105 | # wavファイルのリストファイル 106 | wav_scp = os.path.join(out_dir, 'wav.scp') 107 | with open(wav_scp, mode='w') as scp_file: 108 | # 各単語の波形を切り出して保存する 109 | for n, info in enumerate(word_info): 110 | # 単語の時間情報を得る 111 | time = np.array(info['time']) 112 | # 時刻[秒]をサンプル点に変換 113 | time = np.int64(time * sample_frequency) 114 | # 単語の区間を切り出す 115 | cut_wav = waveform[time[0] : time[1]].copy() 116 | 117 | # 切り出した波形の保存ファイル名 118 | out_wav = os.path.join(out_wav_dir, 119 | "%d.wav" % (n+1)) 120 | # 切り出した波形を保存する 121 | with wave.open(out_wav, 'w') as wav: 122 | # チャネル数,サンプルサイズ, 123 | # サンプリング周波数を設定 124 | wav.setnchannels(1) 125 | wav.setsampwidth(2) 126 | wav.setframerate(sample_frequency) 127 | # 波形の書き出し 128 | wav.writeframes(cut_wav) 129 | 130 | # wavファイルのリストに書き込む 131 | scp_file.write('%d %s\n' % 132 | ((n+1), os.path.abspath(out_wav))) 133 | 134 | 135 | # 各単語と音素の組み合わせリスト(辞書)を作成する 136 | lexicon = os.path.join(out_dir, 'lexicon.txt') 137 | with open(lexicon, mode='w') as f: 138 | for info in word_info: 139 | f.write('%s %s\n' \ 140 | % (info['word'], info['phones'])) 141 | 142 | # 143 | # 以下は正解ラベルを作成 144 | # (音素アライメントのテストで使用) 145 | # 146 | 147 | # 音素リストファイルを開き,phone_listに格納 148 | phone_list = [] 149 | with open(phone_list_file, mode='r') as f: 150 | for line in f: 151 | # 音素リストファイルから音素を取得 152 | phone = line.split()[0] 153 | # 音素リストの末尾に加える 154 | phone_list.append(phone) 155 | 156 | # 正解ラベルリスト(音素は数値表記)を作成 157 | label_file = os.path.join(out_dir, 'text_int') 158 | with open(label_file, mode='w') as f: 159 | for n, info in enumerate(word_info): 160 | label = info['phones'].split() 161 | # 両端にポーズを追加 162 | label.insert(0, phone_list[0]) 163 | label.append(phone_list[0]) 164 | # phone_listを使って音素を数値に変換し,書き込む 165 | f.write('%d' % (n+1)) 166 | for ph in label: 167 | f.write(' %d' % (phone_list.index(ph))) 168 | f.write('\n') 169 | 170 | -------------------------------------------------------------------------------- /03gmm_hmm/06_recognize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # HMMモデルで孤立単語認識を行います. 5 | # 6 | 7 | # hmmfunc.pyからMonoPhoneHMMクラスをインポート 8 | from hmmfunc import MonoPhoneHMM 9 | 10 | # 数値演算用モジュール(numpy)をインポート 11 | import numpy as np 12 | 13 | # os, sysモジュールをインポート 14 | import sys 15 | import os 16 | 17 | # 18 | # メイン関数 19 | # 20 | if __name__ == "__main__": 21 | 22 | # HMMファイル 23 | hmm_file = './exp/model_3state_2mix/10.hmm' 24 | 25 | # 評価データの特徴量リストのファイル 26 | feat_scp = './exp/data/test/mfcc/feats.scp' 27 | 28 | # 辞書ファイル 29 | lexicon_file = './exp/data/test/lexicon.txt' 30 | 31 | # 音素リスト 32 | phone_list_file = \ 33 | './exp/data/train_small/phone_list' 34 | 35 | # Trueの場合,文章の先頭と末尾に 36 | # ポーズがあることを前提とする 37 | insert_sil = True 38 | 39 | # 40 | # 処理ここから 41 | # 42 | 43 | # 音素リストファイルを開き,phone_listに格納 44 | phone_list = [] 45 | with open(phone_list_file, mode='r') as f: 46 | for line in f: 47 | # 音素リストファイルから音素を取得 48 | phone = line.split()[0] 49 | # 音素リストの末尾に加える 50 | phone_list.append(phone) 51 | 52 | # 辞書ファイルを開き,単語と音素列の対応リストを得る 53 | lexicon = [] 54 | with open(lexicon_file, mode='r') as f: 55 | for line in f: 56 | # 0列目は単語 57 | word = line.split()[0] 58 | # 1列目以降は音素列 59 | phones = line.split()[1:] 60 | # insert_silがTrueの場合は両端にポーズを追加 61 | if insert_sil: 62 | phones.insert(0, phone_list[0]) 63 | phones.append(phone_list[0]) 64 | # phone_listを使って音素を数値に変換 65 | ph_int = [] 66 | for ph in phones: 67 | if ph in phone_list: 68 | ph_int.append(phone_list.index(ph)) 69 | else: 70 | sys.stderr.write('invalid phone %s' % (ph)) 71 | # 単語,音素列,数値表記の辞書として 72 | # lexiconに追加 73 | lexicon.append({'word': word, 74 | 'pron': phones, 75 | 'int': ph_int}) 76 | 77 | # MonoPhoneHMMクラスを呼び出す 78 | hmm = MonoPhoneHMM() 79 | 80 | # HMMを読み込む 81 | hmm.load_hmm(hmm_file) 82 | 83 | # 特徴量リストファイルを開き, 84 | # 発話毎に音声認識を行う 85 | with open(feat_scp, mode='r') as f: 86 | for line in f: 87 | # 0列目は発話ID 88 | utt = line.split()[0] 89 | # 1列目はファイルパス 90 | ff = line.split()[1] 91 | # 3列目は次元数 92 | nd = int(line.split()[3]) 93 | 94 | # 次元数がHMMの次元数と一致しなければエラー 95 | if hmm.num_dims != nd: 96 | sys.stderr.write(\ 97 | '%s: unexpected #dims (%d)\n'\ 98 | % (utt, nd)) 99 | exit(1) 100 | 101 | # 特徴量ファイルを開く 102 | feat = np.fromfile(ff, dtype=np.float32) 103 | # フレーム数 x 次元数の配列に変形 104 | feat = feat.reshape(-1, hmm.num_dims) 105 | 106 | # 孤立単語認識を行う 107 | (result, detail) = hmm.recognize(feat, lexicon) 108 | 109 | # resultには最も尤度の高い単語が格納されている 110 | # detailは尤度のランキングが格納されている 111 | # 結果を出力する 112 | sys.stdout.write('%s %s\n' % (utt, ff)) 113 | sys.stdout.write('Result = %s\n' % (result)) 114 | sys.stdout.write('[Runking]\n') 115 | for res in detail: 116 | sys.stdout.write(' %s %f\n' \ 117 | % (res['word'], res['score'])) 118 | 119 | -------------------------------------------------------------------------------- /03gmm_hmm/07_phone_alilgnment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # HMMモデルで音素アライメントを推定します. 5 | # 推定したアライメントをスペクトログラム中に 6 | # 図示します. 7 | # 8 | 9 | # hmmfunc.pyからMonoPhoneHMMクラスをインポート 10 | from hmmfunc import MonoPhoneHMM 11 | 12 | # 数値演算用モジュール(numpy)をインポート 13 | import numpy as np 14 | 15 | # wavデータを読み込むためのモジュール(wave)をインポート 16 | import wave 17 | 18 | # プロット用モジュール(matplotlib)をインポート 19 | import matplotlib.pyplot as plt 20 | 21 | # os, sysモジュールをインポート 22 | import sys 23 | import os 24 | 25 | # 26 | # メイン関数 27 | # 28 | if __name__ == "__main__": 29 | 30 | # HMMファイル 31 | hmm_file = './exp/model_3state_1mix/10.hmm' 32 | 33 | # 評価データの特徴量リストのファイル 34 | feat_scp = './exp/data/test/mfcc/feats.scp' 35 | 36 | # wavデータリストのファイル 37 | wav_scp = 'exp/data/test/wav.scp' 38 | 39 | # 評価データのラベルファイル 40 | label_file = './exp/data/test/text_int' 41 | 42 | # 評価する発話ID 43 | eval_utt = '7' 44 | 45 | # フレームサイズ [ミリ秒] 46 | frame_size = 25 47 | # フレームシフト [ミリ秒] 48 | # 特徴量抽出時に指定した値と同じものを使用する 49 | frame_shift = 10 50 | 51 | # プロットを出力するファイル(pngファイル) 52 | out_plot = './alignment.png' 53 | 54 | # 55 | # 処理ここから 56 | # 57 | 58 | # MonoPhoneHMMクラスを呼び出す 59 | hmm = MonoPhoneHMM() 60 | 61 | # HMMを読み込む 62 | hmm.load_hmm(hmm_file) 63 | 64 | # ラベルファイルを開き,発話ID毎のラベル情報を得る 65 | label_list = {} 66 | with open(label_file, mode='r') as f: 67 | for line in f: 68 | # 0列目は発話ID 69 | utt = line.split()[0] 70 | # 1列目以降はラベル 71 | lab = line.split()[1:] 72 | # 各要素は文字として読み込まれているので, 73 | # 整数値に変換する 74 | lab = np.int64(lab) 75 | # label_listに登録 76 | label_list[utt] = lab 77 | 78 | # 特徴量リストファイルを開き, 79 | # 発話ID毎の特徴量ファイルのパスを得る 80 | feat_list = {} 81 | with open(feat_scp, mode='r') as f: 82 | for line in f: 83 | # 0列目は発話ID 84 | utt = line.split()[0] 85 | # 1列目はファイルパス 86 | ff = line.split()[1] 87 | # 3列目は次元数 88 | nd = int(line.split()[3]) 89 | # 次元数がHMMの次元数と一致しなければエラー 90 | if hmm.num_dims != nd: 91 | sys.stderr.write(\ 92 | '%s: unexpected #dims (%d)\n'\ 93 | % (utt, nd)) 94 | exit(1) 95 | # feat_listに登録 96 | feat_list[utt] = ff 97 | 98 | # wavリストファイルを開き, 99 | # 発話ID毎のwavファイルのパスを得る 100 | wav_list = {} 101 | with open(wav_scp, mode='r') as f: 102 | for line in f: 103 | # 0列目は発話ID 104 | utt = line.split()[0] 105 | # 1列目はファイルパス 106 | wf = line.split()[1] 107 | # wav_listに登録 108 | wav_list[utt] = wf 109 | 110 | # 評価する発話IDがリストになければエラー 111 | if (not eval_utt in label_list) \ 112 | or (not eval_utt in feat_list) \ 113 | or (not eval_utt in wav_list): 114 | sys.stderr.write('invalid eval_utt\n') 115 | exit(1) 116 | 117 | 118 | # ラベルを得る 119 | label = label_list[eval_utt] 120 | 121 | # 特徴量ファイル名を得る 122 | feat_file = feat_list[eval_utt] 123 | 124 | # 特徴量ファイルを開く 125 | feat = np.fromfile(feat_file, dtype=np.float32) 126 | # フレーム数 x 次元数の配列に変形 127 | feat = feat.reshape(-1, hmm.num_dims) 128 | 129 | # 音素アライメントを推定する 130 | # alignmentにはフレーム毎の音素ラベルが格納される 131 | alignment = hmm.phone_alignment(feat, label) 132 | 133 | # 134 | # 結果を出力する 135 | # 136 | # 音素境界のリスト 137 | boundary_list = [] 138 | # 一つ前のフレームの音素 139 | prev_phone = "" 140 | # alignmentの音素を一つずつ読み込む 141 | for n, phone in enumerate(alignment): 142 | # 最初のフレームの場合 143 | if n == 0: 144 | boundary_time = 0 145 | # 音素と開始時刻を表示 146 | sys.stdout.write('%s %f ' \ 147 | % (phone, boundary_time)) 148 | # 前の音素を更新 149 | prev_phone = phone 150 | # 音素境界情報の追加 151 | boundary_list.append((phone, boundary_time)) 152 | continue 153 | 154 | # 一つ前のフレームの音素と異なる場合 155 | if phone != prev_phone: 156 | # フレーム番号を秒に変換する 157 | boundary_time = n * frame_shift * 0.001 158 | # 前の音素の終了時刻を表示 159 | sys.stdout.write('%f\n' \ 160 | % (boundary_time)) 161 | # 音素と開始時刻を表示 162 | sys.stdout.write('%s %f ' \ 163 | % (phone, boundary_time)) 164 | # 前の音素を更新 165 | prev_phone = phone 166 | # 音素境界情報の追加 167 | boundary_list.append((phone, boundary_time)) 168 | 169 | # 最終フレームの場合 170 | if n == len(alignment) - 1: 171 | # フレーム番号を秒に変換する 172 | boundary_time = n * frame_shift * 0.001 173 | # 前の音素の終了時刻を表示 174 | sys.stdout.write('%f\n' \ 175 | % (boundary_time)) 176 | 177 | # 178 | # アライメント結果を元に 179 | # スペクトログラムを図示する 180 | # 181 | # wavファイルを得る 182 | wav_file = wav_list[eval_utt] 183 | 184 | # wavファイルを開き、以降の処理を行う 185 | with wave.open(wav_file) as wav: 186 | # wavデータを読み込む 187 | sample_frequency = wav.getframerate() 188 | num_samples = wav.getnframes() 189 | waveform = wav.readframes(num_samples) 190 | waveform = np.frombuffer(waveform, dtype=np.int16) 191 | 192 | # フレームサイズをミリ秒からサンプル数に変換 193 | frame_size = int(sample_frequency \ 194 | * frame_size * 0.001) 195 | # フレームシフトをミリ秒からサンプル数へ変換 196 | frame_shift = int(sample_frequency * frame_shift * 0.001) 197 | 198 | # FFTを行う範囲のサンプル数を, 199 | # フレームサイズ以上の2のべき乗に設定 200 | fft_size = 1 201 | while fft_size < frame_size: 202 | fft_size *= 2 203 | 204 | # 短時間フーリエ変換をしたときの 205 | # 総フレーム数を計算する 206 | num_frames = (num_samples - frame_size) \ 207 | // frame_shift + 1 208 | 209 | # スペクトログラムを計算する 210 | spectrogram = np.zeros((num_frames, fft_size)) 211 | for frame_idx in range(num_frames): 212 | # 1フレーム分の波形を抽出 213 | start_index = frame_idx * frame_shift 214 | frame = waveform[start_index : \ 215 | start_index + frame_size].copy() 216 | 217 | # 対数振幅スペクトルを計算 218 | frame = frame * np.hamming(frame_size) 219 | spectrum = np.fft.fft(frame, n=fft_size) 220 | log_absolute = np.log(np.abs(spectrum) + 1E-7) 221 | spectrogram[frame_idx, :] = log_absolute 222 | 223 | # プロットの描画領域を作成 224 | plt.figure(figsize=(10,10)) 225 | 226 | # 描画領域を縦に2分割し、 227 | # 上側に時間波形をプロットする 228 | plt.subplot(2, 1, 1) 229 | time_axis = np.arange(num_samples) / sample_frequency 230 | plt.plot(time_axis, waveform, color='k') 231 | 232 | # waveformの最大値を元に縦軸の最大値を決める 233 | ymax = np.max(np.abs(waveform))*1.05 234 | 235 | # 音素境界を線で引く 236 | for (p, b) in boundary_list: 237 | # 音素境界の時刻で縦線を引く 238 | plt.vlines(b, -ymax, ymax, 239 | linestyle='dashed', color='k') 240 | # 音素ラベルを表示させる 241 | plt.text(b, ymax+50 , p, fontsize=14) 242 | 243 | plt.xlabel('Time [sec]') 244 | plt.ylabel('Value') 245 | plt.ylim([-ymax, ymax]) 246 | plt.xlim([0, num_samples / sample_frequency]) 247 | 248 | # 2分割された描画領域の下側に 249 | # スペクトログラムをプロットする 250 | plt.subplot(2, 1, 2) 251 | # カラーマップのレンジを調整 252 | spectrogram -= np.max(spectrogram) 253 | vmax = np.abs(np.min(spectrogram)) * 0.0 254 | vmin = - np.abs(np.min(spectrogram)) * 0.7 255 | plt.imshow(spectrogram.T[-1::-1,:], 256 | extent=[0, num_samples / sample_frequency, 257 | 0, sample_frequency], 258 | cmap = 'gray', 259 | vmax = vmax, 260 | vmin = vmin, 261 | aspect = 'auto') 262 | 263 | # 音素境界を線で引く 264 | for (p, b) in boundary_list: 265 | # 音素境界の時刻で縦線を引く 266 | plt.vlines(b, 0, sample_frequency, 267 | linestyle='dashed', color='w') 268 | # 音素ラベルを表示させる 269 | plt.text(b, sample_frequency/2+50 , p, fontsize=14) 270 | 271 | # 横軸と縦軸のラベルを定義 272 | plt.xlabel('Time [sec]') 273 | plt.ylabel('Frequency [Hz]') 274 | plt.ylim([0, sample_frequency/2]) 275 | 276 | # プロットを保存する 277 | plt.savefig(out_plot) 278 | -------------------------------------------------------------------------------- /03gmm_hmm/phones.txt: -------------------------------------------------------------------------------- 1 | N 2 | a 3 | b 4 | by 5 | ch 6 | cl 7 | d 8 | e 9 | f 10 | g 11 | gy 12 | h 13 | hy 14 | i 15 | j 16 | k 17 | ky 18 | m 19 | my 20 | n 21 | ny 22 | o 23 | p 24 | py 25 | r 26 | ry 27 | s 28 | sh 29 | t 30 | ts 31 | u 32 | w 33 | y 34 | z 35 | -------------------------------------------------------------------------------- /03gmm_hmm/test_spectrogram.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 短時間フーリエ変換を用いて 5 | # 音声のスペクトログラムを作成します. 6 | # 7 | 8 | # wavデータを読み込むためのモジュール(wave)をインポート 9 | import wave 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # プロット用モジュール(matplotlib)をインポート 15 | import matplotlib.pyplot as plt 16 | 17 | # 18 | # メイン関数 19 | # 20 | if __name__ == "__main__": 21 | # 開くwavファイル 22 | wav_file = '../data/wav/BASIC5000_0001.wav' 23 | 24 | # フレームサイズ [ミリ秒] 25 | frame_size = 25 26 | # フレームシフト [ミリ秒] 27 | frame_shift = 10 28 | 29 | # プロットを出力するファイル(pngファイル) 30 | out_plot = './spectrogram.png' 31 | 32 | # wavファイルを開き、以降の処理を行う 33 | with wave.open(wav_file) as wav: 34 | # サンプリング周波数 [Hz] を取得 35 | sample_frequency = wav.getframerate() 36 | 37 | # wavデータのサンプル数を取得 38 | num_samples = wav.getnframes() 39 | 40 | # wavデータを読み込む 41 | waveform = wav.readframes(num_samples) 42 | 43 | # 読み込んだデータはバイナリ値(16bit integer) 44 | # なので,数値(整数)に変換する 45 | waveform = np.frombuffer(waveform, dtype=np.int16) 46 | 47 | # フレームサイズをミリ秒からサンプル数に変換 48 | frame_size = int(sample_frequency \ 49 | * frame_size * 0.001) 50 | 51 | # フレームシフトをミリ秒からサンプル数へ変換 52 | frame_shift = int(sample_frequency * frame_shift * 0.001) 53 | 54 | # FFTを行う範囲のサンプル数を, 55 | # フレームサイズ以上の2のべき乗に設定 56 | fft_size = 1 57 | while fft_size < frame_size: 58 | fft_size *= 2 59 | 60 | # 短時間フーリエ変換をしたときの 61 | # 総フレーム数を計算する 62 | num_frames = (num_samples - frame_size) // frame_shift + 1 63 | 64 | # スペクトログラムの行列を用意 65 | spectrogram = np.zeros((num_frames, int(fft_size/2)+1)) 66 | 67 | # 1フレームずつ振幅スペクトルを計算する 68 | for frame_idx in range(num_frames): 69 | # 分析の開始位置は,フレーム番号(0始まり)*フレームシフト 70 | start_index = frame_idx * frame_shift 71 | 72 | # 1フレーム分の波形を抽出 73 | frame = waveform[start_index : \ 74 | start_index + frame_size].copy() 75 | 76 | # ハミング窓を掛ける 77 | frame = frame * np.hamming(frame_size) 78 | 79 | # 高速フーリエ変換(FFT)を実行 80 | spectrum = np.fft.fft(frame, n=fft_size) 81 | 82 | # 振幅スペクトルを得る 83 | absolute = np.abs(spectrum) 84 | 85 | # 振幅スペクトルは左右対称なので,左半分までのみを用いる 86 | absolute = absolute[:int(fft_size/2) + 1] 87 | 88 | # 対数を取り、対数振幅スペクトルを計算 89 | log_absolute = np.log(absolute + 1E-7) 90 | 91 | # 計算結果をスペクトログラムに格納 92 | spectrogram[frame_idx, :] = log_absolute 93 | 94 | # 95 | # 時間波形とスペクトログラムをプロット 96 | # 97 | 98 | # プロットの描画領域を作成 99 | plt.figure(figsize=(10,10)) 100 | 101 | # 描画領域を縦に2分割し、 102 | # 上側に時間波形をプロットする 103 | plt.subplot(2, 1, 1) 104 | 105 | # 横軸(時間軸)を作成する 106 | time_axis = np.arange(num_samples) / sample_frequency 107 | 108 | # 時間波形のプロット 109 | plt.plot(time_axis, waveform) 110 | 111 | # プロットのタイトルと、横軸と縦軸のラベルを定義 112 | plt.title('waveform') 113 | plt.xlabel('Time [sec]') 114 | plt.ylabel('Value') 115 | 116 | # 横軸の表示領域を0から波形終了時刻に制限 117 | plt.xlim([0, num_samples / sample_frequency]) 118 | 119 | # 2分割された描画領域の下側に 120 | # スペクトログラムをプロットする 121 | plt.subplot(2, 1, 2) 122 | 123 | # スペクトログラムの最大値を0に合わせて 124 | # カラーマップのレンジを調整 125 | spectrogram -= np.max(spectrogram) 126 | vmax = np.abs(np.min(spectrogram)) * 0.0 127 | vmin = - np.abs(np.min(spectrogram)) * 0.7 128 | 129 | # ヒストグラムをプロット 130 | plt.imshow(spectrogram.T[-1::-1,:], 131 | extent=[0, num_samples / sample_frequency, 132 | 0, sample_frequency / 2], 133 | cmap = 'gray', 134 | vmax = vmax, 135 | vmin = vmin, 136 | aspect = 'auto') 137 | 138 | # プロットのタイトルと、横軸と縦軸のラベルを定義 139 | plt.title('spectrogram') 140 | plt.xlabel('Time [sec]') 141 | plt.ylabel('Frequency [Hz]') 142 | 143 | # プロットを保存する 144 | plt.savefig(out_plot) 145 | 146 | -------------------------------------------------------------------------------- /04dnn_hmm/00_state_alignment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 訓練データと開発データの 5 | # HMM状態レベルでのアライメントを推定します 6 | # 7 | 8 | # hmmfunc.pyからMonoPhoneHMMクラスをインポート 9 | from hmmfunc import MonoPhoneHMM 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # os, sysモジュールをインポート 15 | import sys 16 | import os 17 | 18 | # 19 | # メイン関数 20 | # 21 | if __name__ == "__main__": 22 | 23 | # HMMファイル 24 | hmm_file = '../03gmm_hmm/exp/model_3state_2mix/10.hmm' 25 | 26 | # 訓練データの特徴量リストのファイル 27 | train_feat_scp = \ 28 | '../01compute_features/mfcc/train_small/feats.scp' 29 | # 開発データの特徴量リストのファイル 30 | dev_feat_scp = \ 31 | '../01compute_features/mfcc/dev/feats.scp' 32 | 33 | # 訓練データのラベルファイル 34 | train_label_file = \ 35 | '../03gmm_hmm/exp/data/train_small/text_int' 36 | # 開発データのラベルファイル 37 | dev_label_file = \ 38 | '../03gmm_hmm/exp/data/dev/text_int' 39 | 40 | # 訓練データのアライメント結果の出力ファイル 41 | train_align_file = \ 42 | './exp/data/train_small/alignment' 43 | # 開発データのアライメント結果の出力ファイル 44 | dev_align_file = \ 45 | './exp/data/dev/alignment' 46 | 47 | # 48 | # 処理ここから 49 | # 50 | 51 | # MonoPhoneHMMクラスを呼び出す 52 | hmm = MonoPhoneHMM() 53 | 54 | # 学習前のHMMを読み込む 55 | hmm.load_hmm(hmm_file) 56 | 57 | # 訓練/開発データの 58 | # 特徴量/ラベル/アライメントファイル 59 | # をリスト化 60 | feat_scp_list = [train_feat_scp, dev_feat_scp] 61 | label_file_list = [train_label_file, dev_label_file] 62 | align_file_list = [train_align_file, dev_align_file] 63 | 64 | for feat_scp, label_file, align_file in \ 65 | zip(feat_scp_list, label_file_list, align_file_list): 66 | 67 | # 出力ディレクトリ 68 | out_dir = os.path.dirname(align_file) 69 | 70 | # 出力ディレクトリが存在しない場合は作成する 71 | os.makedirs(out_dir, exist_ok=True) 72 | 73 | # ラベルファイルを開き,発話ID毎の 74 | # ラベル情報を得る 75 | label_list = {} 76 | with open(label_file, mode='r') as f: 77 | for line in f: 78 | # 0列目は発話ID 79 | utt = line.split()[0] 80 | # 1列目以降はラベル 81 | lab = line.split()[1:] 82 | # 各要素は文字として読み込まれているので, 83 | # 整数値に変換する 84 | lab = np.int64(lab) 85 | # label_listに登録 86 | label_list[utt] = lab 87 | 88 | # 発話毎にアライメントを推定する 89 | with open(align_file, mode='w') as fa, \ 90 | open(feat_scp, mode='r') as fs: 91 | for line in fs: 92 | # 0列目は発話ID 93 | utt = line.split()[0] 94 | print(utt) 95 | # 1列目はファイルパス 96 | ff = line.split()[1] 97 | # 3列目は次元数 98 | nd = int(line.split()[3]) 99 | 100 | # 発話IDがlabel_に存在しなければエラー 101 | if not utt in label_list: 102 | sys.stderr.write(\ 103 | '%s does not have label\n' % (utt)) 104 | exit(1) 105 | # 次元数がHMMの次元数と一致しなければエラー 106 | if hmm.num_dims != nd: 107 | sys.stderr.write(\ 108 | '%s: unexpected #dims (%d)\n'\ 109 | % (utt, nd)) 110 | exit(1) 111 | 112 | # ラベルを得る 113 | label = label_list[utt] 114 | # 特徴量ファイルを開く 115 | feat = np.fromfile(ff, dtype=np.float32) 116 | # フレーム数 x 次元数の配列に変形 117 | feat = feat.reshape(-1, hmm.num_dims) 118 | 119 | # アライメントの実行 120 | alignment = hmm.state_alignment(feat, label) 121 | # alignmentは数値のリストなので 122 | # ファイルに書き込むために文字列に変換する 123 | alignment = ' '.join(map(str, alignment)) 124 | # ファイルに出力する 125 | fa.write('%s %s\n' % (utt, alignment)) 126 | 127 | -------------------------------------------------------------------------------- /04dnn_hmm/01_count_states.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 推定されたアライメント結果をもとに, 5 | # 各HMM状態の出現回数をカウントします. 6 | # 7 | 8 | # hmmfunc.pyからMonoPhoneHMMクラスをインポート 9 | from hmmfunc import MonoPhoneHMM 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # os, sysモジュールをインポート 15 | import sys 16 | import os 17 | 18 | # 19 | # メイン関数 20 | # 21 | if __name__ == "__main__": 22 | 23 | # HMMファイル 24 | hmm_file = '../03gmm_hmm/exp/model_3state_2mix/10.hmm' 25 | 26 | # 訓練データのアライメントファイル 27 | align_file = \ 28 | './exp/data/train_small/alignment' 29 | 30 | # 計算した事前確率ファイル 31 | count_file = \ 32 | './exp/model_dnn/state_counts' 33 | 34 | # 35 | # 処理ここから 36 | # 37 | 38 | # MonoPhoneHMMクラスを呼び出す 39 | hmm = MonoPhoneHMM() 40 | 41 | # 学習前のHMMを読み込む 42 | hmm.load_hmm(hmm_file) 43 | 44 | # HMMの総状態数を得る 45 | num_states = hmm.num_phones * hmm.num_states 46 | 47 | # 出力ディレクトリ 48 | out_dir = os.path.dirname(count_file) 49 | 50 | # 出力ディレクトリが存在しない場合は作成する 51 | os.makedirs(out_dir, exist_ok=True) 52 | 53 | # 状態毎の出現カウンタ 54 | count = np.zeros(num_states, np.int64) 55 | 56 | # アライメントファイルを開く 57 | with open(align_file, mode='r') as f: 58 | for line in f: 59 | # 0列目は発話ID 60 | utt = line.split()[0] 61 | # 1列目以降はアライメント 62 | ali = line.split()[1:] 63 | # アライメントは文字として 64 | # 読み込まれているので, 65 | # 整数値に変換する 66 | ali = np.int64(ali) 67 | # 状態を一つずつ読み込み, 68 | # 該当する状態のカウンタを1増やす 69 | for a in ali: 70 | count[a] += 1 71 | 72 | # カウントが0のものは1にする 73 | # 以降の処理でゼロ割りの発生を防ぐため 74 | count[count==0] = 1 75 | 76 | # カウント結果を出力する 77 | with open(count_file, mode='w') as f: 78 | # ベクトルcountを文字列に変換する 79 | count_str = ' '.join(map(str, count)) 80 | f.write('%s\n' % (count_str)) 81 | 82 | -------------------------------------------------------------------------------- /04dnn_hmm/03_dnn_recognize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # DNN-HMMで認識します. 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch 9 | import torch.nn.functional as F 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # hmmfunc.pyからMonoPhoneHMMクラスをインポート 15 | from hmmfunc import MonoPhoneHMM 16 | 17 | # 作成したDatasetクラスをインポート 18 | from my_dataset import SequenceDataset 19 | 20 | # モデルの定義をインポート 21 | from my_model import MyDNN 22 | 23 | # json形式の入出力を行うモジュールをインポート 24 | import json 25 | 26 | # os, sysモジュールをインポート 27 | import os 28 | import sys 29 | 30 | 31 | # 32 | # メイン関数 33 | # 34 | if __name__ == "__main__": 35 | 36 | # 37 | # 設定ここから 38 | # 39 | 40 | # 評価データの特徴量リスト 41 | test_feat_scp = \ 42 | '../03gmm_hmm/exp/data/test/mfcc/feats.scp' 43 | 44 | # HMMファイル 45 | hmm_file = '../03gmm_hmm/exp/model_3state_2mix/10.hmm' 46 | 47 | # DNNモデルファイル 48 | dnn_file = './exp/model_dnn/best_model.pt' 49 | 50 | # HMM状態出現カウントファイル 51 | count_file = './exp/model_dnn/state_counts' 52 | 53 | # 訓練データから計算された 54 | # 特徴量の平均/標準偏差ファイル 55 | mean_std_file = 'exp/model_dnn/mean_std.txt' 56 | 57 | # 辞書ファイル 58 | lexicon_file = '../03gmm_hmm/exp/data/test/lexicon.txt' 59 | 60 | # 音素リスト 61 | phone_list_file = \ 62 | '../03gmm_hmm/exp/data/train_small/phone_list' 63 | 64 | # Trueの場合,文章の先頭と末尾に 65 | # ポーズがあることを前提とする 66 | insert_sil = True 67 | 68 | # DNN学習時に出力した設定ファイル 69 | config_file = os.path.join(\ 70 | os.path.dirname(dnn_file), 71 | 'config.json') 72 | 73 | # 74 | # 設定ここまで 75 | # 76 | 77 | # MonoPhoneHMMクラスを呼び出す 78 | hmm = MonoPhoneHMM() 79 | # HMMを読み込む 80 | hmm.load_hmm(hmm_file) 81 | 82 | # 設定ファイルを読み込む 83 | with open(config_file, mode='r') as f: 84 | config = json.load(f) 85 | 86 | # 読み込んだ設定を反映する 87 | # 中間層のレイヤー数 88 | num_layers = config['num_layers'] 89 | # 中間層の次元数 90 | hidden_dim = config['hidden_dim'] 91 | # spliceフレーム数 92 | splice = config['splice'] 93 | 94 | # 特徴量の平均/標準偏差ファイルを読み込む 95 | with open(mean_std_file, mode='r') as f: 96 | # 全行読み込み 97 | lines = f.readlines() 98 | # 1行目(0始まり)が平均値ベクトル(mean), 99 | # 3行目が標準偏差ベクトル(std) 100 | mean_line = lines[1] 101 | std_line = lines[3] 102 | # スペース区切りのリストに変換 103 | feat_mean = mean_line.split() 104 | feat_std = std_line.split() 105 | # numpy arrayに変換 106 | feat_mean = np.array(feat_mean, 107 | dtype=np.float32) 108 | feat_std = np.array(feat_std, 109 | dtype=np.float32) 110 | 111 | # 次元数の情報を得る 112 | feat_dim = np.size(feat_mean) 113 | 114 | # DNNの出力層の次元数は音素数x状態数 115 | dim_out = hmm.num_phones * hmm.num_states 116 | 117 | # ニューラルネットワークモデルを作成する 118 | # 入力特徴量の次元数は 119 | # feat_dim * (2*splice+1) 120 | dim_in = feat_dim * (2*splice+1) 121 | model = MyDNN(dim_in=dim_in, 122 | dim_hidden=hidden_dim, 123 | dim_out=dim_out, 124 | num_layers=num_layers) 125 | 126 | # 学習済みのDNNファイルから 127 | # モデルのパラメータを読み込む 128 | model.load_state_dict(torch.load(dnn_file)) 129 | 130 | # モデルを評価モードに設定する 131 | model.eval() 132 | 133 | # HMM状態カウントファイルを読み込む 134 | with open(count_file, mode='r') as f: 135 | # 1行読み込む 136 | line = f.readline() 137 | # HMM状態毎の出現回数がスペース区切りで 138 | # 入っているので,スペースで区切って 139 | # リスト化する 140 | count = line.split() 141 | # 各数値は文字型になっているので 142 | # 数値に変換 143 | count = np.float32(count) 144 | 145 | # 総和で割ることで,各HMM状態の 146 | # 事前発生確率に変換 147 | prior = count / np.sum(count) 148 | 149 | # 音素リストファイルを開き,phone_listに格納 150 | phone_list = [] 151 | with open(phone_list_file, mode='r') as f: 152 | for line in f: 153 | # 音素リストファイルから音素を取得 154 | phone = line.split()[0] 155 | # 音素リストの末尾に加える 156 | phone_list.append(phone) 157 | 158 | # 辞書ファイルを開き,単語と音素列の対応リストを得る 159 | lexicon = [] 160 | with open(lexicon_file, mode='r') as f: 161 | for line in f: 162 | # 0列目は単語 163 | word = line.split()[0] 164 | # 1列目以降は音素列 165 | phones = line.split()[1:] 166 | # insert_silがTrueの場合は両端にポーズを追加 167 | if insert_sil: 168 | phones.insert(0, phone_list[0]) 169 | phones.append(phone_list[0]) 170 | # phone_listを使って音素を数値に変換 171 | ph_int = [] 172 | for ph in phones: 173 | if ph in phone_list: 174 | ph_int.append(phone_list.index(ph)) 175 | else: 176 | sys.stderr.write('invalid phone %s' % (ph)) 177 | # 単語,音素列,数値表記の辞書として 178 | # lexiconに追加 179 | lexicon.append({'word': word, 180 | 'pron': phones, 181 | 'int': ph_int}) 182 | 183 | # 184 | # ここから認識処理: 185 | # 186 | 187 | # 特徴量リストファイルを開く 188 | with open(test_feat_scp, mode='r') as f: 189 | for line in f: 190 | # 0列目は発話ID 191 | utt = line.split()[0] 192 | # 1列目はファイルパス 193 | ff = line.split()[1] 194 | # 3列目は次元数 195 | nd = int(line.split()[3]) 196 | 197 | # 特徴量データを読み込む 198 | feat = np.fromfile(ff, dtype=np.float32) 199 | # フレーム数 x 次元数の配列に変形 200 | feat = feat.reshape(-1, nd) 201 | 202 | # 平均と標準偏差を使って正規化(標準化)を行う 203 | feat = (feat - feat_mean) / feat_std 204 | 205 | # splicing: 前後 n フレームの特徴量を結合する 206 | org_feat = feat.copy() 207 | for n in range(-splice, splice+1): 208 | # 元々の特徴量を n フレームずらす 209 | tmp = np.roll(org_feat, n, axis=0) 210 | if n < 0: 211 | # 前にずらした場合は 212 | # 終端nフレームを0にする 213 | tmp[n:] = 0 214 | elif n > 0: 215 | # 後ろにずらした場合は 216 | # 始端nフレームを0にする 217 | tmp[:n] = 0 218 | else: 219 | continue 220 | # ずらした特徴量を次元方向に 221 | # 結合する 222 | feat = np.hstack([feat,tmp]) 223 | 224 | # pytorchのDNNに入力するため, 225 | # torch.tensor型に変換する 226 | feat = torch.tensor(feat) 227 | 228 | # DNNに入力する 229 | output = model(feat) 230 | 231 | # softmax関数に入れて確率に変換する 232 | output = F.softmax(output, dim=1) 233 | 234 | # numpy array型に戻す 235 | output = output.detach().numpy() 236 | 237 | # 各HMM状態の事前発生確率で割り, 238 | # さらに対数を取って対数尤度に変換する 239 | # (HMMの各状態の出力確率に相当する形にする) 240 | likelihood = np.log(output / prior) 241 | 242 | (result, detail) = hmm.recognize_with_dnn(likelihood, lexicon) 243 | # 結果を出力する 244 | sys.stdout.write('%s %s\n' % (utt, ff)) 245 | sys.stdout.write('Result = %s\n' % (result)) 246 | sys.stdout.write('[Runking]\n') 247 | for res in detail: 248 | sys.stdout.write(' %s %f\n' \ 249 | % (res['word'], res['score'])) 250 | 251 | -------------------------------------------------------------------------------- /04dnn_hmm/06_dnn_recognize_fbank.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # DNN-HMMで認識します. 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch 9 | import torch.nn.functional as F 10 | 11 | # 数値演算用モジュール(numpy)をインポート 12 | import numpy as np 13 | 14 | # hmmfunc.pyからMonoPhoneHMMクラスをインポート 15 | from hmmfunc import MonoPhoneHMM 16 | 17 | # 作成したDatasetクラスをインポート 18 | from my_dataset import SequenceDataset 19 | 20 | # モデルの定義をインポート 21 | from my_model import MyDNN 22 | 23 | # json形式の入出力を行うモジュールをインポート 24 | import json 25 | 26 | # os, sysモジュールをインポート 27 | import os 28 | import sys 29 | 30 | 31 | # 32 | # メイン関数 33 | # 34 | if __name__ == "__main__": 35 | 36 | # 37 | # 設定ここから 38 | # 39 | 40 | # 評価データの特徴量リスト 41 | test_feat_scp = \ 42 | './exp/data/test/feats.scp' 43 | 44 | # HMMファイル 45 | hmm_file = '../03gmm_hmm/exp/model_3state_2mix/10.hmm' 46 | 47 | # DNNモデルファイル 48 | dnn_file = './exp/model_dnn_fbank/best_model.pt' 49 | 50 | # HMM状態出現カウントファイル 51 | count_file = './exp/model_dnn/state_counts' 52 | 53 | # 訓練データから計算された 54 | # 特徴量の平均/標準偏差ファイル 55 | mean_std_file = 'exp/model_dnn_fbank/mean_std.txt' 56 | 57 | # 辞書ファイル 58 | lexicon_file = '../03gmm_hmm/exp/data/test/lexicon.txt' 59 | 60 | # 音素リスト 61 | phone_list_file = \ 62 | '../03gmm_hmm/exp/data/train_small/phone_list' 63 | 64 | # Trueの場合,文章の先頭と末尾に 65 | # ポーズがあることを前提とする 66 | insert_sil = True 67 | 68 | # DNN学習時に出力した設定ファイル 69 | config_file = os.path.join(\ 70 | os.path.dirname(dnn_file), 71 | 'config.json') 72 | 73 | # 74 | # 設定ここまで 75 | # 76 | 77 | # MonoPhoneHMMクラスを呼び出す 78 | hmm = MonoPhoneHMM() 79 | # HMMを読み込む 80 | hmm.load_hmm(hmm_file) 81 | 82 | # 設定ファイルを読み込む 83 | with open(config_file, mode='r') as f: 84 | config = json.load(f) 85 | 86 | # 読み込んだ設定を反映する 87 | # 中間層のレイヤー数 88 | num_layers = config['num_layers'] 89 | # 中間層の次元数 90 | hidden_dim = config['hidden_dim'] 91 | # spliceフレーム数 92 | splice = config['splice'] 93 | 94 | # 特徴量の平均/標準偏差ファイルを読み込む 95 | with open(mean_std_file, mode='r') as f: 96 | # 全行読み込み 97 | lines = f.readlines() 98 | # 1行目(0始まり)が平均値ベクトル(mean), 99 | # 3行目が標準偏差ベクトル(std) 100 | mean_line = lines[1] 101 | std_line = lines[3] 102 | # スペース区切りのリストに変換 103 | feat_mean = mean_line.split() 104 | feat_std = std_line.split() 105 | # numpy arrayに変換 106 | feat_mean = np.array(feat_mean, 107 | dtype=np.float32) 108 | feat_std = np.array(feat_std, 109 | dtype=np.float32) 110 | 111 | # 次元数の情報を得る 112 | feat_dim = np.size(feat_mean) 113 | 114 | # DNNの出力層の次元数は音素数x状態数 115 | dim_out = hmm.num_phones * hmm.num_states 116 | 117 | # ニューラルネットワークモデルを作成する 118 | # 入力特徴量の次元数は 119 | # feat_dim * (2*splice+1) 120 | dim_in = feat_dim * (2*splice+1) 121 | model = MyDNN(dim_in=dim_in, 122 | dim_hidden=hidden_dim, 123 | dim_out=dim_out, 124 | num_layers=num_layers) 125 | 126 | # 学習済みのDNNファイルから 127 | # モデルのパラメータを読み込む 128 | model.load_state_dict(torch.load(dnn_file)) 129 | 130 | # モデルを評価モードに設定する 131 | model.eval() 132 | 133 | # HMM状態カウントファイルを読み込む 134 | with open(count_file, mode='r') as f: 135 | # 1行読み込む 136 | line = f.readline() 137 | # HMM状態毎の出現回数がスペース区切りで 138 | # 入っているので,スペースで区切って 139 | # リスト化する 140 | count = line.split() 141 | # 各数値は文字型になっているので 142 | # 数値に変換 143 | count = np.float32(count) 144 | 145 | # 総和で割ることで,各HMM状態の 146 | # 事前発生確率に変換 147 | prior = count / np.sum(count) 148 | 149 | # 音素リストファイルを開き,phone_listに格納 150 | phone_list = [] 151 | with open(phone_list_file, mode='r') as f: 152 | for line in f: 153 | # 音素リストファイルから音素を取得 154 | phone = line.split()[0] 155 | # 音素リストの末尾に加える 156 | phone_list.append(phone) 157 | 158 | # 辞書ファイルを開き,単語と音素列の対応リストを得る 159 | lexicon = [] 160 | with open(lexicon_file, mode='r') as f: 161 | for line in f: 162 | # 0列目は単語 163 | word = line.split()[0] 164 | # 1列目以降は音素列 165 | phones = line.split()[1:] 166 | # insert_silがTrueの場合は両端にポーズを追加 167 | if insert_sil: 168 | phones.insert(0, phone_list[0]) 169 | phones.append(phone_list[0]) 170 | # phone_listを使って音素を数値に変換 171 | ph_int = [] 172 | for ph in phones: 173 | if ph in phone_list: 174 | ph_int.append(phone_list.index(ph)) 175 | else: 176 | sys.stderr.write('invalid phone %s' % (ph)) 177 | # 単語,音素列,数値表記の辞書として 178 | # lexiconに追加 179 | lexicon.append({'word': word, 180 | 'pron': phones, 181 | 'int': ph_int}) 182 | 183 | # 184 | # ここから認識処理: 185 | # 186 | 187 | # 特徴量リストファイルを開く 188 | with open(test_feat_scp, mode='r') as f: 189 | for line in f: 190 | # 0列目は発話ID 191 | utt = line.split()[0] 192 | # 1列目はファイルパス 193 | ff = line.split()[1] 194 | # 3列目は次元数 195 | nd = int(line.split()[3]) 196 | 197 | # 特徴量データを読み込む 198 | feat = np.fromfile(ff, dtype=np.float32) 199 | # フレーム数 x 次元数の配列に変形 200 | feat = feat.reshape(-1, nd) 201 | 202 | # 平均と標準偏差を使って正規化(標準化)を行う 203 | feat = (feat - feat_mean) / feat_std 204 | 205 | # splicing: 前後 n フレームの特徴量を結合する 206 | org_feat = feat.copy() 207 | for n in range(-splice, splice+1): 208 | # 元々の特徴量を n フレームずらす 209 | tmp = np.roll(org_feat, n, axis=0) 210 | if n < 0: 211 | # 前にずらした場合は 212 | # 終端nフレームを0にする 213 | tmp[n:] = 0 214 | elif n > 0: 215 | # 後ろにずらした場合は 216 | # 始端nフレームを0にする 217 | tmp[:n] = 0 218 | else: 219 | continue 220 | # ずらした特徴量を次元方向に 221 | # 結合する 222 | feat = np.hstack([feat,tmp]) 223 | 224 | # pytorchのDNNに入力するため, 225 | # torch.tensor型に変換する 226 | feat = torch.tensor(feat) 227 | 228 | # DNNに入力する 229 | output = model(feat) 230 | 231 | # softmax関数に入れて確率に変換する 232 | output = F.softmax(output, dim=1) 233 | 234 | # numpy array型に戻す 235 | output = output.detach().numpy() 236 | 237 | # 各HMM状態の事前発生確率で割り, 238 | # さらに対数を取って対数尤度に変換する 239 | # (HMMの各状態の出力確率に相当する形にする) 240 | likelihood = np.log(output / prior) 241 | 242 | (result, detail) = hmm.recognize_with_dnn(likelihood, lexicon) 243 | # 結果を出力する 244 | sys.stdout.write('%s %s\n' % (utt, ff)) 245 | sys.stdout.write('Result = %s\n' % (result)) 246 | sys.stdout.write('[Runking]\n') 247 | for res in detail: 248 | sys.stdout.write(' %s %f\n' \ 249 | % (res['word'], res['score'])) 250 | 251 | -------------------------------------------------------------------------------- /04dnn_hmm/initialize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # モデルの初期化関数を定義します 5 | # 6 | 7 | # 数値演算用モジュール(numpy)をインポート 8 | import numpy as np 9 | 10 | 11 | def lecun_initialization(model): 12 | '''LeCunのパラメータ初期化方法の実行 13 | 各重み(バイアス成分除く)を,平均0,標準偏差 1/sqrt(dim) の 14 | 正規分布に基づく乱数で初期化(dim は入力次元数) 15 | model: Pytorchで定義したモデル 16 | ''' 17 | # モデルのパラメータを順に取り出し,初期化を実行する 18 | for param in model.parameters(): 19 | # パラメータの値を取り出す 20 | data = param.data 21 | # パラメータのテンソル次元数を取り出す 22 | dim = data.dim() 23 | # 次元数を元に処理を変える 24 | if dim == 1: 25 | # dim = 1 の場合はバイアス成分 26 | # ゼロに初期化する 27 | data.zero_() 28 | elif dim == 2: 29 | # dim = 2 の場合は線形射影の重み行列 30 | # 入力次元数 = size(1) を得る 31 | n = data.size(1) 32 | # 入力次元数の平方根の逆数を 33 | # 標準偏差とする正規分布乱数で初期化 34 | std = 1.0 / np.sqrt(n) 35 | data.normal_(0, std) 36 | elif dim == 3: 37 | # dim = 3 の場合は 1次元畳み込みの行列 38 | # 入力チャネル数 * カーネルサイズの 39 | # 平方根の逆数を標準偏差とする 40 | # 正規分布乱数で初期化 41 | n = data.size(1) * data.size(2) 42 | std = 1.0 / np.sqrt(n) 43 | data.normal_(0, std) 44 | elif dim == 4: 45 | # dim = 4 の場合は 2次元畳み込みの行列 46 | # 入力チャネル数 * カーネルサイズ(行) 47 | # * カーネルサイズ(列) 48 | # の平方根の逆数を標準偏差とする 49 | # 正規分布乱数で初期化 50 | n = data.size(1) * data.size(2) * data.size(3) 51 | std = 1.0 / np.sqrt(n) 52 | data.normal_(0, std) 53 | else: 54 | # それ以外は対応していない 55 | print('lecun_initialization: '\ 56 | 'dim > 4 is not supported.') 57 | exit(1) 58 | 59 | -------------------------------------------------------------------------------- /04dnn_hmm/my_dataset.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Pytorchで用いるDatasetの定義 5 | # 6 | 7 | # PytorchのDatasetモジュールをインポート 8 | from torch.utils.data import Dataset 9 | 10 | # 数値演算用モジュール(numpy)をインポート 11 | import numpy as np 12 | 13 | # sysモジュールをインポート 14 | import sys 15 | 16 | 17 | class SequenceDataset(Dataset): 18 | ''' ミニバッチデータを作成するクラス 19 | torch.utils.data.Datasetクラスを継承し, 20 | 以下の関数を定義する 21 | __len__: 総サンプル数を出力する関数 22 | __getitem__: 1サンプルのデータを出力する関数 23 | feat_scp: 特徴量リストファイル 24 | label_scp: ラベルファイル 25 | feat_mean: 特徴量の平均値ベクトル 26 | feat_std: 特徴量の次元毎の標準偏差を並べたベクトル 27 | pad_index: バッチ化の際にフレーム数を合わせる 28 | ためにpaddingする整数値 29 | splice: 前後(splice)フレームを特徴量を結合する 30 | splice=1とすると,前後1フレーム分結合 31 | するので次元数は3倍になる. 32 | splice=0の場合は何もしない 33 | ''' 34 | def __init__(self, 35 | feat_scp, 36 | label_scp, 37 | feat_mean, 38 | feat_std, 39 | pad_index, 40 | splice=0): 41 | # 発話の数 42 | self.num_utts = 0 43 | # 各発話のID 44 | self.id_list = [] 45 | # 各発話の特徴量ファイルへのパスを記したリスト 46 | self.feat_list = [] 47 | # 各発話の特徴量フレーム数を記したリスト 48 | self.feat_len_list = [] 49 | # 特徴量の平均値ベクトル 50 | self.feat_mean = feat_mean 51 | # 特徴量の標準偏差ベクトル 52 | self.feat_std = feat_std 53 | # 標準偏差のフロアリング 54 | # (0で割ることが発生しないようにするため) 55 | self.feat_std[self.feat_std<1E-10] = 1E-10 56 | # 特徴量の次元数 57 | self.feat_dim = \ 58 | np.size(self.feat_mean) 59 | # 各発話のラベル 60 | self.label_list = [] 61 | # 各発話のラベルの長さを記したリスト 62 | self.label_len_list = [] 63 | # フレーム数の最大値 64 | self.max_feat_len = 0 65 | # ラベル長の最大値 66 | self.max_label_len = 0 67 | # フレーム埋めに用いる整数値 68 | self.pad_index = pad_index 69 | # splice:前後nフレームの特徴量を結合 70 | self.splice = splice 71 | 72 | # 特徴量リスト,ラベルを1行ずつ 73 | # 読み込みながら情報を取得する 74 | with open(feat_scp, mode='r') as file_f, \ 75 | open(label_scp, mode='r') as file_l: 76 | for (line_feats, line_label) in zip(file_f, file_l): 77 | # 各行をスペースで区切り, 78 | # リスト型の変数にする 79 | parts_feats = line_feats.split() 80 | parts_label = line_label.split() 81 | 82 | # 発話ID(partsの0番目の要素)が特徴量と 83 | # ラベルで一致していなければエラー 84 | if parts_feats[0] != parts_label[0]: 85 | sys.stderr.write('IDs of feat and '\ 86 | 'label do not match.\n') 87 | exit(1) 88 | 89 | # 発話IDをリストに追加 90 | self.id_list.append(parts_feats[0]) 91 | # 特徴量ファイルのパスをリストに追加 92 | self.feat_list.append(parts_feats[1]) 93 | # フレーム数をリストに追加 94 | feat_len = np.int64(parts_feats[2]) 95 | self.feat_len_list.append(feat_len) 96 | 97 | # ラベル(番号で記載)をint型の 98 | # numpy arrayに変換 99 | label = np.int64(parts_label[1:]) 100 | # ラベルリストに追加 101 | self.label_list.append(label) 102 | # ラベルの長さを追加 103 | self.label_len_list.append(len(label)) 104 | 105 | # 発話数をカウント 106 | self.num_utts += 1 107 | 108 | # フレーム数の最大値を得る 109 | self.max_feat_len = \ 110 | np.max(self.feat_len_list) 111 | # ラベル長の最大値を得る 112 | self.max_label_len = \ 113 | np.max(self.label_len_list) 114 | 115 | # ラベルデータの長さを最大フレーム長に 116 | # 合わせるため,pad_indexの値で埋める 117 | for n in range(self.num_utts): 118 | # 埋めるフレームの数 119 | # = 最大フレーム数 - 自分のフレーム数 120 | pad_len = self.max_label_len \ 121 | - self.label_len_list[n] 122 | # pad_indexの値で埋める 123 | self.label_list[n] = \ 124 | np.pad(self.label_list[n], 125 | [0, pad_len], 126 | mode='constant', 127 | constant_values=self.pad_index) 128 | 129 | def __len__(self): 130 | ''' 学習データの総サンプル数を返す関数 131 | 本実装では発話単位でバッチを作成するため, 132 | 総サンプル数=発話数である. 133 | ''' 134 | return self.num_utts 135 | 136 | 137 | def __getitem__(self, idx): 138 | ''' サンプルデータを返す関数 139 | 本実装では発話単位でバッチを作成するため, 140 | idx=発話番号である. 141 | ''' 142 | # 特徴量系列のフレーム数 143 | feat_len = self.feat_len_list[idx] 144 | # ラベルの長さ 145 | label_len = self.label_len_list[idx] 146 | 147 | # 特徴量データを特徴量ファイルから読み込む 148 | feat = np.fromfile(self.feat_list[idx], 149 | dtype=np.float32) 150 | # フレーム数 x 次元数の配列に変形 151 | feat = feat.reshape(-1, self.feat_dim) 152 | 153 | # 平均と標準偏差を使って正規化(標準化)を行う 154 | feat = (feat - self.feat_mean) / self.feat_std 155 | 156 | # splicing: 前後 n フレームの特徴量を結合する 157 | org_feat = feat.copy() 158 | for n in range(-self.splice, self.splice+1): 159 | # 元々の特徴量を n フレームずらす 160 | tmp = np.roll(org_feat, n, axis=0) 161 | if n < 0: 162 | # 前にずらした場合は 163 | # 終端nフレームを0にする 164 | tmp[n:] = 0 165 | elif n > 0: 166 | # 後ろにずらした場合は 167 | # 始端nフレームを0にする 168 | tmp[:n] = 0 169 | else: 170 | continue 171 | # ずらした特徴量を次元方向に 172 | # 結合する 173 | feat = np.hstack([feat,tmp]) 174 | 175 | # 特徴量データのフレーム数を最大フレーム数に 176 | # 合わせるため,0で埋める 177 | pad_len = self.max_feat_len - feat_len 178 | feat = np.pad(feat, 179 | [(0, pad_len), (0, 0)], 180 | mode='constant', 181 | constant_values=0) 182 | 183 | # ラベル 184 | label = self.label_list[idx] 185 | 186 | # 発話ID 187 | utt_id = self.id_list[idx] 188 | 189 | # 特徴量,ラベル,フレーム数, 190 | # ラベル長,発話IDを返す 191 | return (feat, 192 | label, 193 | feat_len, 194 | label_len, 195 | utt_id) 196 | 197 | -------------------------------------------------------------------------------- /04dnn_hmm/my_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # モデル構造を定義します 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch.nn as nn 9 | 10 | # 作成したinitialize.pyの 11 | # 初期化関数 lecun_initialization をインポート 12 | from initialize import lecun_initialization 13 | 14 | 15 | class MyDNN(nn.Module): 16 | ''' Fully connected layer (線形層) によるシンプルなDNN 17 | dim_in: 入力特徴量の次元数 18 | dim_hidden: 隠れ層の次元数 19 | dim_out: 出力の次元数 20 | num_layers: 隠れ層の数 21 | ''' 22 | def __init__(self, 23 | dim_in, 24 | dim_hidden, 25 | dim_out, 26 | num_layers=2): 27 | super(MyDNN, self).__init__() 28 | 29 | # 隠れ層の数 30 | self.num_layers = num_layers 31 | 32 | # 入力層: 線形層+ReLU 33 | self.inp = nn.Sequential(\ 34 | nn.Linear(in_features=dim_in, 35 | out_features=dim_hidden), 36 | nn.ReLU()) 37 | # 隠れ層 38 | hidden = [] 39 | for n in range(self.num_layers): 40 | # 線形層をhiddenに加える 41 | hidden.append(nn.Linear(in_features=dim_hidden, 42 | out_features=dim_hidden)) 43 | # ReLUを加える 44 | hidden.append(nn.ReLU()) 45 | 46 | # Pytorchで扱うため,リスト型から 47 | # ModuleList型に変換する 48 | self.hidden = nn.ModuleList(hidden) 49 | 50 | # 出力層: 線形層 51 | self.out = nn.Linear(in_features=dim_hidden, 52 | out_features=dim_out) 53 | 54 | # LeCunのパラメータ初期化を実行 55 | lecun_initialization(self) 56 | 57 | 58 | def forward(self, frame): 59 | ''' ネットワーク計算(forward処理)の関数 60 | frame: 入力のフレームデータ 61 | output: 入力されたフレームに対する各状態の確率 62 | ''' 63 | # 入力層を通す 64 | output = self.inp(frame) 65 | # 隠れ層を通す 66 | for n in range(self.num_layers): 67 | output = self.hidden[n](output) 68 | # 出力層を通す 69 | output = self.out(output) 70 | return output 71 | 72 | -------------------------------------------------------------------------------- /05ctc/01_get_token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # ラベル中のトークン(音素/かな/キャラクター)を, 5 | # ニューラルネットワークで扱うため,文字から番号へ変換します. 6 | # また,文字-番号の対応を記したトークンリストも作成します. 7 | # 8 | 9 | # osモジュールをインポート 10 | import os 11 | 12 | 13 | def token_to_int(label_file_str, 14 | label_file_int, 15 | unknown_list_file, 16 | token_list, 17 | silence_tokens): 18 | ''' トークンリストを使って,ラベルファイルの 19 | 文字を番号に置き換えてファイルに出力する 20 | このとき,非音声トークンは削除する 21 | label_file_str: 文字で記述されたラベルファイル 22 | label_file_int: 番号で記述されたラベルファイルの出力先 23 | unknown_list_file: 未知のトークンを記述したファイルの出力先 24 | token_list: 文字-番号の対応情報が格納されたリスト 25 | silence_tokenas: 非音声トークンが格納されたリスト 26 | ''' 27 | # 各ファイルを開く 28 | with open(label_file_str, mode='r') as label_in, \ 29 | open(label_file_int, mode='w') as label_out, \ 30 | open(unknown_list_file, mode='w') as unk_list: 31 | # ラベルファイルを一行ずつ読み込む 32 | for line in label_in: 33 | # 読み込んだ行をスペースで区切り, 34 | # リスト型の変数にする 35 | text = line.split() 36 | 37 | # リストの0番目の要素は発話IDなので,そのまま出力する 38 | label_out.write('%s' % text[0]) 39 | 40 | # リストの1番目以降の要素は文字なので, 41 | # 1文字ずつ数字に置き換える 42 | for u in text[1:]: 43 | # 非音声トークンの場合はスキップする 44 | if u in silence_tokens: 45 | continue 46 | 47 | # 文字がトークンリストに存在するか確認 48 | if not u in token_list: 49 | # 存在しなかった場合 50 | # 未知トークンリストに書き込む 51 | unk_list.write('%s\n' % (u)) 52 | # 未知トークンには番号 1 を割り当てて出力 53 | label_out.write(' 1') 54 | else: 55 | # 存在する場合 56 | # 対応する番号を出力 57 | label_out.write(' %d' \ 58 | % (token_list.index(u) + 1)) 59 | label_out.write('\n') 60 | 61 | 62 | # 63 | # メイン関数 64 | # 65 | if __name__ == "__main__": 66 | 67 | # トークンの単位 68 | # phone:音素 kana:かな char:キャラクター 69 | unit_set = ['phone', 'kana', 'char'] 70 | 71 | # 非音声トークン(ポーズなど)の定義 72 | # 本プログラムでは,非音声トークンは扱わない 73 | # 0行目は音素,1行目はかな,2行目は文字の 74 | # 非音声トークンを定義する 75 | silence_set = [['pau'], 76 | [''], 77 | ['']] 78 | 79 | # 未知トークンの定義 80 | # 訓練データに無く,開発/評価データに有った 81 | # トークンはこの文字に置き換えられる 82 | unknown_token = '' 83 | 84 | # ラベルファイルの存在するフォルダ 85 | label_dir_train = '../data/label/train_small' 86 | label_dir_dev = '../data/label/dev' 87 | label_dir_test = '../data/label/test' 88 | 89 | # 実験ディレクトリ 90 | # train_smallを使った時とtrain_largeを使った時で 91 | # 異なる実験ディレクトリにする 92 | exp_dir = './exp_' + os.path.basename(label_dir_train) 93 | 94 | # 処理結果の出力先ディレクトリ 95 | output_dir = os.path.join(exp_dir, 'data') 96 | 97 | # phone, kana, char それぞれに対して処理 98 | for uid, unit in enumerate(unit_set): 99 | # 出力先ディレクトリ 100 | out_dir = os.path.join(output_dir, unit) 101 | 102 | # 出力ディレクトリが存在しない場合は作成する 103 | os.makedirs(out_dir, exist_ok=True) 104 | 105 | # 106 | # トークンリストを作成 107 | # 108 | # 学習データのラベルファイル 109 | label_train = os.path.join(label_dir_train, 'text_'+unit) 110 | # トークンリストを空リストで定義 111 | token_list = [] 112 | # 訓練データのラベルに存在するトークンを 113 | # token_listに登録する 114 | with open(label_train) as label_file: 115 | # 一行ずつ読み込む 116 | for line in label_file: 117 | # 読み込んだ行をスペースで区切り, 118 | # リスト型の変数にする 119 | # 0番目は発話IDなので,1番目以降を取り出す 120 | text = line.split()[1:] 121 | 122 | # トークンリストに結合 123 | token_list += text 124 | 125 | # set関数により,重複するトークンを削除する 126 | token_list = list(set(token_list)) 127 | 128 | # 非音声トークン 129 | silence_tokens = silence_set[uid] 130 | # 非音声トークンをリストから削除する 131 | for u in silence_tokens: 132 | if u in token_list: 133 | token_list.remove(u) 134 | 135 | # リストをソートする 136 | token_list = sorted(token_list) 137 | 138 | # 未知トークンをリストの先頭に挿入する 139 | token_list.insert(0, unknown_token) 140 | 141 | # トークンリストをファイルに出力する 142 | with open(os.path.join(out_dir, 'token_list'), 143 | mode='w') as token_file: 144 | for i, u in enumerate(token_list): 145 | # 「トークン 対応する番号」を記述 146 | # このとき,番号は1始まりにする. 147 | # (0はCTCのblankトークンに割り当てるため) 148 | token_file.write('%s %d\n' % (u, i+1)) 149 | 150 | # 151 | # 作成したトークンリストを使って,ラベルファイルの 152 | # 各トークンを文字から番号に置き換える 153 | # 154 | # 開発/評価データに存在するトークンで,token_listに 155 | # (=学習データに)存在しないトークンは未知トークンとして 156 | # unknown_listに登録する. 157 | # (unknown_listは以降の処理には使いませんが, 158 | # 処理結果を確認するために出力しています) 159 | # 160 | # 学習/開発/評価データのラベルそれぞれについて処理 161 | label_dir_list = [label_dir_train, 162 | label_dir_dev, 163 | label_dir_test] 164 | for label_dir in label_dir_list: 165 | # ディレクトリ名を取得(train_{small,large}/dev/test) 166 | name = os.path.basename(label_dir) 167 | # 入力ラベルファイル(各トークンが文字で表記) 168 | label_str = os.path.join(label_dir, 169 | 'text_'+unit) 170 | # 出力ラベルファイル(各トークンが数値で表記) 171 | label_int = os.path.join(out_dir, 172 | 'label_'+name) 173 | # 未知トークンリストの出力先 174 | unknown_list = os.path.join(out_dir, 175 | 'unknown_token_'+name) 176 | # ラベルファイルの文字->数値変換処理の実行 177 | token_to_int(label_str, 178 | label_int, 179 | unknown_list, 180 | token_list, 181 | silence_tokens) 182 | 183 | -------------------------------------------------------------------------------- /05ctc/04_scoring.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 認識結果と正解文を参照して,認識エラー率を計算します. 5 | # 6 | 7 | # 認識エラー率を計算するモジュールをインポート 8 | import levenshtein 9 | 10 | # os, sysモジュールをインポート 11 | import os 12 | import sys 13 | 14 | 15 | if __name__ == "__main__": 16 | 17 | # トークンの単位 18 | # phone:音素 kana:かな char:キャラクター 19 | unit = 'phone' 20 | 21 | # 実験ディレクトリ 22 | exp_dir = './exp_train_small' 23 | 24 | # デコード結果が格納されているディレクトリ 25 | decoded_dir = os.path.join(exp_dir, 26 | unit+'_model_ctc', 27 | 'decode_test') 28 | 29 | # 認識結果が記述されたファイル 30 | hypothesis_file = os.path.join(decoded_dir, 'hypothesis.txt') 31 | 32 | # 正解文が記述されたファイル 33 | reference_file = os.path.join(decoded_dir, 'reference.txt') 34 | 35 | # エラー率算出結果を出力するディレクトリ 36 | out_dir = decoded_dir 37 | 38 | # エラー率算出結果の出力ファイル 39 | result_file = os.path.join(out_dir, 'result.txt') 40 | 41 | # 出力ディレクトリが存在しない場合は作成する 42 | os.makedirs(out_dir, exist_ok=True) 43 | 44 | # 各誤りの総数(エラー率算出時の分子) 45 | total_err = 0 46 | total_sub = 0 47 | total_del = 0 48 | total_ins = 0 49 | # 正解文の総文字数(エラー率算出時の分母) 50 | total_length = 0 51 | 52 | # 各ファイルをオープン 53 | with open(hypothesis_file, mode='r') as hyp_file, \ 54 | open(reference_file, mode='r') as ref_file, \ 55 | open(result_file, mode='w') as out_file: 56 | # 認識結果ファイル正解文ファイルを一行ずつ読み込む 57 | for line_hyp, line_ref in zip(hyp_file, ref_file): 58 | # 読み込んだ行をスペースで区切り,リスト型の変数にする 59 | parts_hyp = line_hyp.split() 60 | parts_ref = line_ref.split() 61 | 62 | # 発話ID(partsの0番目の要素)が一致しているか確認 63 | if parts_hyp[0] != parts_ref[0]: 64 | sys.stderr.write('Utterance ids of '\ 65 | 'hypothesis and reference do not match.') 66 | exit(1) 67 | 68 | # 1要素目以降が認識結果/正解分の文字列(リスト型) 69 | hypothesis = parts_hyp[1:] 70 | reference = parts_ref[1:] 71 | 72 | # 誤り数を計算する 73 | (error, substitute, delete, insert, ref_length) \ 74 | = levenshtein.calculate_error(hypothesis, 75 | reference) 76 | 77 | # 総誤り数を累積する 78 | total_err += error 79 | total_sub += substitute 80 | total_del += delete 81 | total_ins += insert 82 | total_length += ref_length 83 | 84 | # 各発話の結果を出力する 85 | out_file.write('ID: %s\n' % (parts_hyp[0])) 86 | out_file.write('#ERROR (#SUB #DEL #INS): '\ 87 | '%d (%d %d %d)\n' \ 88 | % (error, substitute, delete, insert)) 89 | out_file.write('REF: %s\n' % (' '.join(reference))) 90 | out_file.write('HYP: %s\n' % (' '.join(hypothesis))) 91 | out_file.write('\n') 92 | 93 | # 総エラー数を,正解文の総文字数で割り,エラー率を算出する 94 | err_rate = 100.0 * total_err / total_length 95 | sub_rate = 100.0 * total_sub / total_length 96 | del_rate = 100.0 * total_del / total_length 97 | ins_rate = 100.0 * total_ins / total_length 98 | 99 | # 最終結果を出力する 100 | out_file.write('------------------------------'\ 101 | '-----------------------------------------------\n') 102 | out_file.write('#TOKEN: %d, #ERROR: %d '\ 103 | '(#SUB: %d, #DEL: %d, #INS: %d)\n' \ 104 | % (total_length, total_err, 105 | total_sub, total_del, total_ins)) 106 | out_file.write('TER: %.2f%% (SUB: %.2f, '\ 107 | 'DEL: %.2f, INS: %.2f)\n' \ 108 | % (err_rate, sub_rate, del_rate, ins_rate)) 109 | print('TER: %.2f%% (SUB: %.2f, DEL: %.2f, INS: %.2f)' \ 110 | % (err_rate, sub_rate, del_rate, ins_rate)) 111 | out_file.write('------------------------------'\ 112 | '-----------------------------------------------\n') 113 | 114 | -------------------------------------------------------------------------------- /05ctc/encoder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # RNN エンコーダ部の実装です. 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch.nn as nn 9 | from torch.nn.utils.rnn import pack_padded_sequence 10 | from torch.nn.utils.rnn import pad_packed_sequence 11 | 12 | 13 | class Encoder(nn.Module): 14 | ''' エンコーダ 15 | dim_in: 入力特徴量の次元数 16 | dim_hidden: 隠れ層の次元数(bidirectional=Trueの場合, 17 | 実際の次元数はdim_hidden * 2) 18 | dim_proj: Projection層の次元数 19 | (これがエンコーダの出力次元数になる) 20 | num_layers: RNN層(およびProjection層)の数 21 | bidirectional: Trueにすると,bidirectional RNNを用いる 22 | sub_sample: レイヤーごとに設定する,フレームの間引き率 23 | num_layers=4のとき,sub_sample=[1,2,3,1]とすると 24 | 2層目でフレーム数を1/2に,3層目で1/3にする 25 | (出力のフレーム数は1/6になる) 26 | rnn_type: 'LSTM'か'GRU'を選択する 27 | ''' 28 | def __init__(self, 29 | dim_in, 30 | dim_hidden, 31 | dim_proj, 32 | num_layers=2, 33 | bidirectional=True, 34 | sub_sample=None, 35 | rnn_type='LSTM'): 36 | super(Encoder, self).__init__() 37 | # RNN層の数 38 | self.num_layers = num_layers 39 | 40 | # RNN層は1層ずつ定義して,リスト化する 41 | rnn = [] 42 | for n in range(self.num_layers): 43 | # RNNへの入力次元数は, 44 | # 最初の層のみdim_in,それ以外はdim_proj 45 | input_size = dim_in if n == 0 else dim_proj 46 | # rnn_type がGRUならGRUを,それ以外ならLSTMを用いる 47 | if rnn_type == 'GRU': 48 | rnn.append(nn.GRU(input_size=input_size, 49 | hidden_size=dim_hidden, 50 | num_layers=1, 51 | bidirectional=bidirectional, 52 | batch_first=True)) 53 | else: 54 | rnn.append(nn.LSTM(input_size=input_size, 55 | hidden_size=dim_hidden, 56 | num_layers=1, 57 | bidirectional=bidirectional, 58 | batch_first=True)) 59 | # 標準のリスト型のままだと, 60 | # Pytorchで扱えないので,ModuleListに変換する 61 | self.rnn = nn.ModuleList(rnn) 62 | 63 | # sub_sample の定義 64 | if sub_sample is None: 65 | # 定義されていない場合は,フレームの間引きを行わない 66 | # (sub_sampleは全要素1のリストにする) 67 | self.sub_sample = [1 for i in range(num_layers)] 68 | else: 69 | # 定義されている場合は,それを用いる 70 | self.sub_sample = sub_sample 71 | 72 | # Projection層もRNN層と同様に1層ずつ定義する 73 | proj = [] 74 | for n in range(self.num_layers): 75 | # Projection層の入力次元数 = RNN層の出力次元数. 76 | # bidiractional=Trueの場合は次元数が2倍になる 77 | input_size = dim_hidden * (2 if bidirectional else 1) 78 | proj.append(nn.Linear(in_features=input_size, 79 | out_features=dim_proj)) 80 | # RNN層と同様,ModuleListに変換する 81 | self.proj = nn.ModuleList(proj) 82 | 83 | 84 | def forward(self, sequence, lengths): 85 | ''' ネットワーク計算(forward処理)の関数 86 | sequence: 各発話の入力系列 [B x T x D] 87 | lengths: 各発話の系列長(フレーム数) [B] 88 | []の中はテンソルのサイズ 89 | B: ミニバッチ内の発話数(ミニバッチサイズ) 90 | T: 入力テンソルの系列長(ゼロ埋め部分含む) 91 | D: 入力次元数(dim_in) 92 | ''' 93 | # 出力とその長さ情報を入力で初期化 94 | output = sequence 95 | output_lengths = lengths 96 | 97 | # num_layersの数だけ,RNNとProjection層へ交互に入力する 98 | for n in range(self.num_layers): 99 | # RNN へ入力するため, 100 | # 入力をPackedSequenceデータに変換する 101 | rnn_input \ 102 | = nn.utils.rnn.pack_padded_sequence(output, 103 | output_lengths, 104 | batch_first=True) 105 | 106 | # GPUとcuDNNを使用している場合, 107 | # 以下の1行を入れると処理が速くなる 108 | # (パラメータデータのポインタをリセット) 109 | self.rnn[n].flatten_parameters() 110 | 111 | # RNN層に入力する 112 | output, (h, c) = self.rnn[n](rnn_input) 113 | 114 | # RNN出力をProjection層へ入力するため, 115 | # PackedSequenceデータからtensorへ戻す 116 | output, output_lengths \ 117 | = nn.utils.rnn.pad_packed_sequence(output, 118 | batch_first=True) 119 | 120 | # sub sampling (間引き)の実行 121 | # この層における間引き率を取得 122 | sub = self.sub_sample[n] 123 | if sub > 1: 124 | # 間引きを実行する 125 | output = output[:, ::sub] 126 | # フレーム数を更新する 127 | # 更新後のフレーム数=(更新前のフレーム数+1)//sub 128 | output_lengths = (output_lengths+1) // sub 129 | 130 | # Projection層に入力する 131 | output = self.proj[n](output) 132 | 133 | # sub samplingを実行した場合はフレーム数が変わるため, 134 | # 出力のフレーム数情報も出力する 135 | return output, output_lengths 136 | 137 | -------------------------------------------------------------------------------- /05ctc/initialize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # モデルの初期化関数を定義します 5 | # 6 | 7 | # 数値演算用モジュール(numpy)をインポート 8 | import numpy as np 9 | 10 | 11 | def lecun_initialization(model): 12 | '''LeCunのパラメータ初期化方法の実行 13 | 各重み(バイアス成分除く)を,平均0,標準偏差 1/sqrt(dim) の 14 | 正規分布に基づく乱数で初期化(dim は入力次元数) 15 | model: Pytorchで定義したモデル 16 | ''' 17 | # モデルのパラメータを順に取り出し,初期化を実行する 18 | for param in model.parameters(): 19 | # パラメータの値を取り出す 20 | data = param.data 21 | # パラメータのテンソル次元数を取り出す 22 | dim = data.dim() 23 | # 次元数を元に処理を変える 24 | if dim == 1: 25 | # dim = 1 の場合はバイアス成分 26 | # ゼロに初期化する 27 | data.zero_() 28 | elif dim == 2: 29 | # dim = 2 の場合は線形射影の重み行列 30 | # 入力次元数 = size(1) を得る 31 | n = data.size(1) 32 | # 入力次元数の平方根の逆数を 33 | # 標準偏差とする正規分布乱数で初期化 34 | std = 1.0 / np.sqrt(n) 35 | data.normal_(0, std) 36 | elif dim == 3: 37 | # dim = 3 の場合は 1次元畳み込みの行列 38 | # 入力チャネル数 * カーネルサイズの 39 | # 平方根の逆数を標準偏差とする 40 | # 正規分布乱数で初期化 41 | n = data.size(1) * data.size(2) 42 | std = 1.0 / np.sqrt(n) 43 | data.normal_(0, std) 44 | elif dim == 4: 45 | # dim = 4 の場合は 2次元畳み込みの行列 46 | # 入力チャネル数 * カーネルサイズ(行) 47 | # * カーネルサイズ(列) 48 | # の平方根の逆数を標準偏差とする 49 | # 正規分布乱数で初期化 50 | n = data.size(1) * data.size(2) * data.size(3) 51 | std = 1.0 / np.sqrt(n) 52 | data.normal_(0, std) 53 | else: 54 | # それ以外は対応していない 55 | print('lecun_initialization: '\ 56 | 'dim > 4 is not supported.') 57 | exit(1) 58 | 59 | -------------------------------------------------------------------------------- /05ctc/levenshtein.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # レーベンシュタイン距離を用いて, 5 | # 認識結果の誤り数を算出します. 6 | # 7 | 8 | import numpy as np 9 | import copy 10 | 11 | def calculate_error(hypothesis, reference): 12 | ''' レーベンシュタイン距離を計算し, 13 | 置換誤り,削除誤り,挿入誤りを出力する 14 | hypothesis: 認識結果(トークン毎に区切ったリスト形式) 15 | reference: 正解(同上) 16 | total_error: 総誤り数 17 | substitute_error: 置換誤り数 18 | delete_error: 削除誤り数 19 | insert_error: 挿入誤り数 20 | len_ref: 正解文のトークン数 21 | ''' 22 | # 認識結果および正解系列の長さを取得 23 | len_hyp = len(hypothesis) 24 | len_ref = len(reference) 25 | 26 | # 累積コスト行列を作成する 27 | # 行列の各要素には,トータルコスト, 28 | # 置換コスト,削除コスト,挿入コストの 29 | # 累積値が辞書形式で定義される. 30 | cost_matrix = [[{"total":0, 31 | "substitute":0, 32 | "delete":0, 33 | "insert":0} \ 34 | for j in range(len_ref+1)] \ 35 | for i in range(len_hyp+1)] 36 | 37 | # 0列目と0行目の入力 38 | for i in range(1, len_hyp+1): 39 | # 縦方向への遷移は,削除処理を意味する 40 | cost_matrix[i][0]["delete"] = i 41 | cost_matrix[i][0]["total"] = i 42 | for j in range(1, len_ref+1): 43 | # 横方向への遷移は,挿入処理を意味する 44 | cost_matrix[0][j]["insert"] = j 45 | cost_matrix[0][j]["total"] = j 46 | 47 | # 1列目と1行目以降の累積コストを計算していく 48 | for i in range(1, len_hyp+1): 49 | for j in range(1, len_ref+1): 50 | # 51 | # 各処理のコストを計算する 52 | # 53 | # 斜め方向の遷移時,文字が一致しない場合は, 54 | # 置換処理により累積コストが1増加 55 | substitute_cost = \ 56 | cost_matrix[i-1][j-1]["total"] \ 57 | + (0 if hypothesis[i-1] == reference[j-1] else 1) 58 | # 縦方向の遷移時は,削除処理により累積コストが1増加 59 | delete_cost = cost_matrix[i-1][j]["total"] + 1 60 | # 横方向の遷移時は,挿入処理により累積コストが1増加 61 | insert_cost = cost_matrix[i][j-1]["total"] + 1 62 | 63 | # 置換処理,削除処理,挿入処理のうち, 64 | # どの処理を行えば累積コストが最も小さくなるかを計算 65 | cost = [substitute_cost, delete_cost, insert_cost] 66 | min_index = np.argmin(cost) 67 | 68 | if min_index == 0: 69 | # 置換処理が累積コスト最小となる場合 70 | 71 | # 遷移元の累積コスト情報をコピー 72 | cost_matrix[i][j] = \ 73 | copy.copy(cost_matrix[i-1][j-1]) 74 | # 文字が一致しない場合は, 75 | # 累積置換コストを1増加させる 76 | cost_matrix[i][j]["substitute"] \ 77 | += (0 if hypothesis[i-1] \ 78 | == reference[j-1] else 1) 79 | elif min_index == 1: 80 | # 削除処理が累積コスト最小となる場合 81 | 82 | # 遷移元の累積コスト情報をコピー 83 | cost_matrix[i][j] = copy.copy(cost_matrix[i-1][j]) 84 | # 累積削除コストを1増加させる 85 | cost_matrix[i][j]["delete"] += 1 86 | else: 87 | # 置換処理が累積コスト最小となる場合 88 | 89 | # 遷移元の累積コスト情報をコピー 90 | cost_matrix[i][j] = copy.copy(cost_matrix[i][j-1]) 91 | # 累積挿入コストを1増加させる 92 | cost_matrix[i][j]["insert"] += 1 93 | 94 | # 累積トータルコスト(置換+削除+挿入コスト)を更新 95 | cost_matrix[i][j]["total"] = cost[min_index] 96 | 97 | # 98 | # エラーの数を出力する 99 | # このとき,削除コストは挿入誤り, 100 | # 挿入コストは削除誤りになる点に注意. 101 | # (削除コストが1である 102 | # = 1文字削除しないと正解文にならない 103 | # = 認識結果は1文字分余計に挿入されている 104 | # = 挿入誤りが1である) 105 | # 106 | 107 | # 累積コスト行列の右下の要素が最終的なコストとなる. 108 | total_error = cost_matrix[len_hyp][len_ref]["total"] 109 | substitute_error = cost_matrix[len_hyp][len_ref]["substitute"] 110 | # 削除誤り = 挿入コスト 111 | delete_error = cost_matrix[len_hyp][len_ref]["insert"] 112 | # 挿入誤り = 削除コスト 113 | insert_error = cost_matrix[len_hyp][len_ref]["delete"] 114 | 115 | # 各誤り数と,正解文の文字数 116 | # (誤り率を算出する際に分母として用いる)を出力 117 | return (total_error, 118 | substitute_error, 119 | delete_error, 120 | insert_error, 121 | len_ref) 122 | 123 | 124 | if __name__ == "__main__": 125 | # ref: 正解文 126 | # hyp: 認識結果 127 | ref = "狼が犬に似ているようにおべっか使いは友達のように見える" 128 | hyp = "オオ狼みがい塗に似ているようにオッつ界は伴ちのように見える" 129 | 130 | 131 | # 各文字列を,1文字ずつ区切ってリストデータにする 132 | hyp_list = list(hyp) 133 | ref_list = list(ref) 134 | 135 | # 誤り数を計算する 136 | total, substitute, delete, insert, ref_length \ 137 | = calculate_error(hyp_list, ref_list) 138 | 139 | # 誤りの数と,誤り率(100*誤り数/正解文の文字数)を出力する 140 | print("REF: %s" % ref) 141 | print("HYP: %s" % hyp) 142 | print("#TOKEN(REF): %d, #ERROR: %d, #SUB: %d, #DEL: %d, #INS: %d" \ 143 | % (ref_length, total, substitute, delete, insert)) 144 | print("UER: %.2f, SUBR: %.2f, DELR: %.2f, INSR: %.2f" \ 145 | % (100.0*total/ref_length, 146 | 100.0*substitute/ref_length, 147 | 100.0*delete/ref_length, 148 | 100.0*insert/ref_length)) 149 | 150 | -------------------------------------------------------------------------------- /05ctc/my_dataset.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Pytorchで用いるDatasetの定義 5 | # 6 | 7 | # PytorchのDatasetモジュールをインポート 8 | from torch.utils.data import Dataset 9 | 10 | # 数値演算用モジュール(numpy)をインポート 11 | import numpy as np 12 | 13 | # sysモジュールをインポート 14 | import sys 15 | 16 | 17 | class SequenceDataset(Dataset): 18 | ''' ミニバッチデータを作成するクラス 19 | torch.utils.data.Datasetクラスを継承し, 20 | 以下の関数を定義する 21 | __len__: 総サンプル数を出力する関数 22 | __getitem__: 1サンプルのデータを出力する関数 23 | feat_scp: 特徴量リストファイル 24 | label_scp: ラベルファイル 25 | feat_mean: 特徴量の平均値ベクトル 26 | feat_std: 特徴量の次元毎の標準偏差を並べたベクトル 27 | pad_index: バッチ化の際にフレーム数を合わせる 28 | ためにpaddingする整数値 29 | splice: 前後(splice)フレームを特徴量を結合する 30 | splice=1とすると,前後1フレーム分結合 31 | するので次元数は3倍になる. 32 | splice=0の場合は何もしない 33 | ''' 34 | def __init__(self, 35 | feat_scp, 36 | label_scp, 37 | feat_mean, 38 | feat_std, 39 | pad_index=0, 40 | splice=0): 41 | # 発話の数 42 | self.num_utts = 0 43 | # 各発話のID 44 | self.id_list = [] 45 | # 各発話の特徴量ファイルへのパスを記したリスト 46 | self.feat_list = [] 47 | # 各発話の特徴量フレーム数を記したリスト 48 | self.feat_len_list = [] 49 | # 特徴量の平均値ベクトル 50 | self.feat_mean = feat_mean 51 | # 特徴量の標準偏差ベクトル 52 | self.feat_std = feat_std 53 | # 標準偏差のフロアリング 54 | # (0で割ることが発生しないようにするため) 55 | self.feat_std[self.feat_std<1E-10] = 1E-10 56 | # 特徴量の次元数 57 | self.feat_dim = \ 58 | np.size(self.feat_mean) 59 | # 各発話のラベル 60 | self.label_list = [] 61 | # 各発話のラベルの長さを記したリスト 62 | self.label_len_list = [] 63 | # フレーム数の最大値 64 | self.max_feat_len = 0 65 | # ラベル長の最大値 66 | self.max_label_len = 0 67 | # フレーム埋めに用いる整数値 68 | self.pad_index = pad_index 69 | # splice:前後nフレームの特徴量を結合 70 | self.splice = splice 71 | 72 | # 特徴量リスト,ラベルを1行ずつ 73 | # 読み込みながら情報を取得する 74 | with open(feat_scp, mode='r') as file_f, \ 75 | open(label_scp, mode='r') as file_l: 76 | for (line_feats, line_label) in zip(file_f, file_l): 77 | # 各行をスペースで区切り, 78 | # リスト型の変数にする 79 | parts_feats = line_feats.split() 80 | parts_label = line_label.split() 81 | 82 | # 発話ID(partsの0番目の要素)が特徴量と 83 | # ラベルで一致していなければエラー 84 | if parts_feats[0] != parts_label[0]: 85 | sys.stderr.write('IDs of feat and '\ 86 | 'label do not match.\n') 87 | exit(1) 88 | 89 | # 発話IDをリストに追加 90 | self.id_list.append(parts_feats[0]) 91 | # 特徴量ファイルのパスをリストに追加 92 | self.feat_list.append(parts_feats[1]) 93 | # フレーム数をリストに追加 94 | feat_len = np.int64(parts_feats[2]) 95 | self.feat_len_list.append(feat_len) 96 | 97 | # ラベル(番号で記載)をint型の 98 | # numpy arrayに変換 99 | label = np.int64(parts_label[1:]) 100 | # ラベルリストに追加 101 | self.label_list.append(label) 102 | # ラベルの長さを追加 103 | self.label_len_list.append(len(label)) 104 | 105 | # 発話数をカウント 106 | self.num_utts += 1 107 | 108 | # フレーム数の最大値を得る 109 | self.max_feat_len = \ 110 | np.max(self.feat_len_list) 111 | # ラベル長の最大値を得る 112 | self.max_label_len = \ 113 | np.max(self.label_len_list) 114 | 115 | # ラベルデータの長さを最大フレーム長に 116 | # 合わせるため,pad_indexの値で埋める 117 | for n in range(self.num_utts): 118 | # 埋めるフレームの数 119 | # = 最大フレーム数 - 自分のフレーム数 120 | pad_len = self.max_label_len \ 121 | - self.label_len_list[n] 122 | # pad_indexの値で埋める 123 | self.label_list[n] = \ 124 | np.pad(self.label_list[n], 125 | [0, pad_len], 126 | mode='constant', 127 | constant_values=self.pad_index) 128 | 129 | def __len__(self): 130 | ''' 学習データの総サンプル数を返す関数 131 | 本実装では発話単位でバッチを作成するため, 132 | 総サンプル数=発話数である. 133 | ''' 134 | return self.num_utts 135 | 136 | 137 | def __getitem__(self, idx): 138 | ''' サンプルデータを返す関数 139 | 本実装では発話単位でバッチを作成するため, 140 | idx=発話番号である. 141 | ''' 142 | # 特徴量系列のフレーム数 143 | feat_len = self.feat_len_list[idx] 144 | # ラベルの長さ 145 | label_len = self.label_len_list[idx] 146 | 147 | # 特徴量データを特徴量ファイルから読み込む 148 | feat = np.fromfile(self.feat_list[idx], 149 | dtype=np.float32) 150 | # フレーム数 x 次元数の配列に変形 151 | feat = feat.reshape(-1, self.feat_dim) 152 | 153 | # 平均と標準偏差を使って正規化(標準化)を行う 154 | feat = (feat - self.feat_mean) / self.feat_std 155 | 156 | # splicing: 前後 n フレームの特徴量を結合する 157 | org_feat = feat.copy() 158 | for n in range(-self.splice, self.splice+1): 159 | # 元々の特徴量を n フレームずらす 160 | tmp = np.roll(org_feat, n, axis=0) 161 | if n < 0: 162 | # 前にずらした場合は 163 | # 終端nフレームを0にする 164 | tmp[n:] = 0 165 | elif n > 0: 166 | # 後ろにずらした場合は 167 | # 始端nフレームを0にする 168 | tmp[:n] = 0 169 | else: 170 | continue 171 | # ずらした特徴量を次元方向に 172 | # 結合する 173 | feat = np.hstack([feat,tmp]) 174 | 175 | # 特徴量データのフレーム数を最大フレーム数に 176 | # 合わせるため,0で埋める 177 | pad_len = self.max_feat_len - feat_len 178 | feat = np.pad(feat, 179 | [(0, pad_len), (0, 0)], 180 | mode='constant', 181 | constant_values=0) 182 | 183 | # ラベル 184 | label = self.label_list[idx] 185 | 186 | # 発話ID 187 | utt_id = self.id_list[idx] 188 | 189 | # 特徴量,ラベル,フレーム数, 190 | # ラベル長,発話IDを返す 191 | return (feat, 192 | label, 193 | feat_len, 194 | label_len, 195 | utt_id) 196 | 197 | -------------------------------------------------------------------------------- /05ctc/my_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # モデル構造を定義します 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch.nn as nn 9 | 10 | # 作成したEncoderクラスをインポート 11 | from encoder import Encoder 12 | 13 | # 作成した初期化関数をインポート 14 | from initialize import lecun_initialization 15 | 16 | import numpy as np 17 | 18 | 19 | class MyCTCModel(nn.Module): 20 | ''' CTCモデルの定義 21 | dim_in: 入力次元数 22 | dim_enc_hid: エンコーダの隠れ層の次元数 23 | dim_enc_proj: エンコーダのProjection層の次元数 24 | (これがエンコーダの出力次元数になる) 25 | dim_out: 出力の次元数 26 | enc_num_layers: エンコーダのレイヤー数 27 | enc_bidirectional: Trueにすると,エンコーダに 28 | bidirectional RNNを用いる 29 | enc_sub_sample: エンコーダにおいてレイヤーごとに設定する, 30 | フレームの間引き率 31 | enc_rnn_type: エンコーダRNNの種類.'LSTM'か'GRU'を選択する 32 | ''' 33 | def __init__(self, 34 | dim_in, 35 | dim_enc_hid, 36 | dim_enc_proj, 37 | dim_out, 38 | enc_num_layers=2, 39 | enc_bidirectional=True, 40 | enc_sub_sample=None, 41 | enc_rnn_type='LSTM'): 42 | super(MyCTCModel, self).__init__() 43 | 44 | # エンコーダを作成 45 | self.encoder = Encoder(dim_in=dim_in, 46 | dim_hidden=dim_enc_hid, 47 | dim_proj=dim_enc_proj, 48 | num_layers=enc_num_layers, 49 | bidirectional=enc_bidirectional, 50 | sub_sample=enc_sub_sample, 51 | rnn_type=enc_rnn_type) 52 | 53 | # 出力層 54 | # 出力層への入力 = Projection層の出力 55 | self.out = nn.Linear(in_features=dim_enc_proj, 56 | out_features=dim_out) 57 | 58 | # LeCunのパラメータ初期化を実行 59 | lecun_initialization(self) 60 | 61 | 62 | def forward(self, 63 | input_sequence, 64 | input_lengths): 65 | ''' ネットワーク計算(forward処理)の関数 66 | input_sequence: 各発話の入力系列 [B x Tin x D] 67 | input_lengths: 各発話の系列長(フレーム数) [B] 68 | []の中はテンソルのサイズ 69 | B: ミニバッチ内の発話数(ミニバッチサイズ) 70 | Tin: 入力テンソルの系列長(ゼロ埋め部分含む) 71 | D: 入力次元数(dim_in) 72 | ''' 73 | # エンコーダに入力する 74 | enc_out, enc_lengths = self.encoder(input_sequence, 75 | input_lengths) 76 | 77 | # 出力層に入力する 78 | output = self.out(enc_out) 79 | 80 | # デコーダ出力と,エンコーダ出力系列長情報を出力する 81 | return output, enc_lengths 82 | 83 | -------------------------------------------------------------------------------- /06rnn_attention/01_get_token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # ラベル中のトークン(音素/かな/キャラクター)を, 5 | # ニューラルネットワークで扱うため,文字から番号へ変換します. 6 | # また,文字-番号の対応を記したトークンリストも作成します. 7 | # 8 | 9 | # osモジュールをインポート 10 | import os 11 | 12 | 13 | def token_to_int(label_file_str, 14 | label_file_int, 15 | unknown_list_file, 16 | token_list, 17 | silence_tokens): 18 | ''' トークンリストを使って,ラベルファイルの 19 | 文字を番号に置き換えてファイルに出力する 20 | このとき,非音声トークンは削除する 21 | label_file_str: 文字で記述されたラベルファイル 22 | label_file_int: 番号で記述されたラベルファイルの出力先 23 | unknown_list_file: 未知のトークンを記述したファイルの出力先 24 | token_list: 文字-番号の対応情報が格納されたリスト 25 | silence_tokenas: 非音声トークンが格納されたリスト 26 | ''' 27 | # 各ファイルを開く 28 | with open(label_file_str, mode='r') as label_in, \ 29 | open(label_file_int, mode='w') as label_out, \ 30 | open(unknown_list_file, mode='w') as unk_list: 31 | # ラベルファイルを一行ずつ読み込む 32 | for line in label_in: 33 | # 読み込んだ行をスペースで区切り, 34 | # リスト型の変数にする 35 | text = line.split() 36 | 37 | # リストの0番目の要素は発話IDなので,そのまま出力する 38 | label_out.write('%s' % text[0]) 39 | 40 | # リストの1番目以降の要素は文字なので, 41 | # 1文字ずつ数字に置き換える 42 | for u in text[1:]: 43 | # 非音声トークンの場合はスキップする 44 | if u in silence_tokens: 45 | continue 46 | 47 | # 文字がトークンリストに存在するか確認 48 | if not u in token_list: 49 | # 存在しなかった場合 50 | # 未知トークンリストに書き込む 51 | unk_list.write('%s\n' % (u)) 52 | # 未知トークンには番号 1 を割り当てて出力 53 | label_out.write(' 1') 54 | else: 55 | # 存在する場合 56 | # 対応する番号を出力 57 | label_out.write(' %d' \ 58 | % (token_list.index(u) + 1)) 59 | label_out.write('\n') 60 | 61 | 62 | # 63 | # メイン関数 64 | # 65 | if __name__ == "__main__": 66 | 67 | # トークンの単位 68 | # phone:音素 kana:かな char:キャラクター 69 | unit_set = ['phone', 'kana', 'char'] 70 | 71 | # 非音声トークン(ポーズなど)の定義 72 | # 本プログラムでは,非音声トークンは扱わない 73 | # 0行目は音素,1行目はかな,2行目は文字の 74 | # 非音声トークンを定義する 75 | silence_set = [['pau'], 76 | [''], 77 | ['']] 78 | 79 | # 未知トークンの定義 80 | # 訓練データに無く,開発/評価データに有った 81 | # トークンはこの文字に置き換えられる 82 | unknown_token = '' 83 | 84 | # ラベルファイルの存在するフォルダ 85 | label_dir_train = '../data/label/train_small' 86 | label_dir_dev = '../data/label/dev' 87 | label_dir_test = '../data/label/test' 88 | 89 | # 実験ディレクトリ 90 | # train_smallを使った時とtrain_largeを使った時で 91 | # 異なる実験ディレクトリにする 92 | exp_dir = './exp_' + os.path.basename(label_dir_train) 93 | 94 | # 処理結果の出力先ディレクトリ 95 | output_dir = os.path.join(exp_dir, 'data') 96 | 97 | # phone, kana, char それぞれに対して処理 98 | for uid, unit in enumerate(unit_set): 99 | # 出力先ディレクトリ 100 | out_dir = os.path.join(output_dir, unit) 101 | 102 | # 出力ディレクトリが存在しない場合は作成する 103 | os.makedirs(out_dir, exist_ok=True) 104 | 105 | # 106 | # トークンリストを作成 107 | # 108 | # 学習データのラベルファイル 109 | label_train = os.path.join(label_dir_train, 'text_'+unit) 110 | # トークンリストを空リストで定義 111 | token_list = [] 112 | # 訓練データのラベルに存在するトークンを 113 | # token_listに登録する 114 | with open(label_train) as label_file: 115 | # 一行ずつ読み込む 116 | for line in label_file: 117 | # 読み込んだ行をスペースで区切り, 118 | # リスト型の変数にする 119 | # 0番目は発話IDなので,1番目以降を取り出す 120 | text = line.split()[1:] 121 | 122 | # トークンリストに結合 123 | token_list += text 124 | 125 | # set関数により,重複するトークンを削除する 126 | token_list = list(set(token_list)) 127 | 128 | # 非音声トークン 129 | silence_tokens = silence_set[uid] 130 | # 非音声トークンをリストから削除する 131 | for u in silence_tokens: 132 | if u in token_list: 133 | token_list.remove(u) 134 | 135 | # リストをソートする 136 | token_list = sorted(token_list) 137 | 138 | # 未知トークンをリストの先頭に挿入する 139 | token_list.insert(0, unknown_token) 140 | 141 | # トークンリストをファイルに出力する 142 | with open(os.path.join(out_dir, 'token_list'), 143 | mode='w') as token_file: 144 | for i, u in enumerate(token_list): 145 | # 「トークン 対応する番号」を記述 146 | # このとき,番号は1始まりにする. 147 | # (0はCTCのblankトークンに割り当てるため) 148 | token_file.write('%s %d\n' % (u, i+1)) 149 | 150 | # 151 | # 作成したトークンリストを使って,ラベルファイルの 152 | # 各トークンを文字から番号に置き換える 153 | # 154 | # 開発/評価データに存在するトークンで,token_listに 155 | # (=学習データに)存在しないトークンは未知トークンとして 156 | # unknown_listに登録する. 157 | # (unknown_listは以降の処理には使いませんが, 158 | # 処理結果を確認するために出力しています) 159 | # 160 | # 学習/開発/評価データのラベルそれぞれについて処理 161 | label_dir_list = [label_dir_train, 162 | label_dir_dev, 163 | label_dir_test] 164 | for label_dir in label_dir_list: 165 | # ディレクトリ名を取得(train_{small,large}/dev/test) 166 | name = os.path.basename(label_dir) 167 | # 入力ラベルファイル(各トークンが文字で表記) 168 | label_str = os.path.join(label_dir, 169 | 'text_'+unit) 170 | # 出力ラベルファイル(各トークンが数値で表記) 171 | label_int = os.path.join(out_dir, 172 | 'label_'+name) 173 | # 未知トークンリストの出力先 174 | unknown_list = os.path.join(out_dir, 175 | 'unknown_token_'+name) 176 | # ラベルファイルの文字->数値変換処理の実行 177 | token_to_int(label_str, 178 | label_int, 179 | unknown_list, 180 | token_list, 181 | silence_tokens) 182 | 183 | -------------------------------------------------------------------------------- /06rnn_attention/04_scoring.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 認識結果と正解文を参照して,認識エラー率を計算します. 5 | # 6 | 7 | # 認識エラー率を計算するモジュールをインポート 8 | import levenshtein 9 | 10 | # os, sysモジュールをインポート 11 | import os 12 | import sys 13 | 14 | 15 | if __name__ == "__main__": 16 | 17 | # トークンの単位 18 | # phone:音素 kana:かな char:キャラクター 19 | unit = 'phone' 20 | 21 | # 実験ディレクトリ 22 | exp_dir = './exp_train_small' 23 | 24 | # デコード結果が格納されているディレクトリ 25 | decoded_dir = os.path.join(exp_dir, 26 | unit+'_model_attention', 27 | 'decode_test') 28 | 29 | # 認識結果が記述されたファイル 30 | hypothesis_file = os.path.join(decoded_dir, 'hypothesis.txt') 31 | 32 | # 正解文が記述されたファイル 33 | reference_file = os.path.join(decoded_dir, 'reference.txt') 34 | 35 | # エラー率算出結果を出力するディレクトリ 36 | out_dir = decoded_dir 37 | 38 | # エラー率算出結果の出力ファイル 39 | result_file = os.path.join(out_dir, 'result.txt') 40 | 41 | # 出力ディレクトリが存在しない場合は作成する 42 | os.makedirs(out_dir, exist_ok=True) 43 | 44 | # 各誤りの総数(エラー率算出時の分子) 45 | total_err = 0 46 | total_sub = 0 47 | total_del = 0 48 | total_ins = 0 49 | # 正解文の総文字数(エラー率算出時の分母) 50 | total_length = 0 51 | 52 | # 各ファイルをオープン 53 | with open(hypothesis_file, mode='r') as hyp_file, \ 54 | open(reference_file, mode='r') as ref_file, \ 55 | open(result_file, mode='w') as out_file: 56 | # 認識結果ファイル正解文ファイルを一行ずつ読み込む 57 | for line_hyp, line_ref in zip(hyp_file, ref_file): 58 | # 読み込んだ行をスペースで区切り,リスト型の変数にする 59 | parts_hyp = line_hyp.split() 60 | parts_ref = line_ref.split() 61 | 62 | # 発話ID(partsの0番目の要素)が一致しているか確認 63 | if parts_hyp[0] != parts_ref[0]: 64 | sys.stderr.write('Utterance ids of '\ 65 | 'hypothesis and reference do not match.') 66 | exit(1) 67 | 68 | # 1要素目以降が認識結果/正解分の文字列(リスト型) 69 | hypothesis = parts_hyp[1:] 70 | reference = parts_ref[1:] 71 | 72 | # 誤り数を計算する 73 | (error, substitute, delete, insert, ref_length) \ 74 | = levenshtein.calculate_error(hypothesis, 75 | reference) 76 | 77 | # 総誤り数を累積する 78 | total_err += error 79 | total_sub += substitute 80 | total_del += delete 81 | total_ins += insert 82 | total_length += ref_length 83 | 84 | # 各発話の結果を出力する 85 | out_file.write('ID: %s\n' % (parts_hyp[0])) 86 | out_file.write('#ERROR (#SUB #DEL #INS): '\ 87 | '%d (%d %d %d)\n' \ 88 | % (error, substitute, delete, insert)) 89 | out_file.write('REF: %s\n' % (' '.join(reference))) 90 | out_file.write('HYP: %s\n' % (' '.join(hypothesis))) 91 | out_file.write('\n') 92 | 93 | # 総エラー数を,正解文の総文字数で割り,エラー率を算出する 94 | err_rate = 100.0 * total_err / total_length 95 | sub_rate = 100.0 * total_sub / total_length 96 | del_rate = 100.0 * total_del / total_length 97 | ins_rate = 100.0 * total_ins / total_length 98 | 99 | # 最終結果を出力する 100 | out_file.write('------------------------------'\ 101 | '-----------------------------------------------\n') 102 | out_file.write('#TOKEN: %d, #ERROR: %d '\ 103 | '(#SUB: %d, #DEL: %d, #INS: %d)\n' \ 104 | % (total_length, total_err, 105 | total_sub, total_del, total_ins)) 106 | out_file.write('TER: %.2f%% (SUB: %.2f, '\ 107 | 'DEL: %.2f, INS: %.2f)\n' \ 108 | % (err_rate, sub_rate, del_rate, ins_rate)) 109 | print('TER: %.2f%% (SUB: %.2f, DEL: %.2f, INS: %.2f)' \ 110 | % (err_rate, sub_rate, del_rate, ins_rate)) 111 | out_file.write('------------------------------'\ 112 | '-----------------------------------------------\n') 113 | 114 | -------------------------------------------------------------------------------- /06rnn_attention/encoder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # RNN エンコーダ部の実装です. 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch.nn as nn 9 | from torch.nn.utils.rnn import pack_padded_sequence 10 | from torch.nn.utils.rnn import pad_packed_sequence 11 | 12 | 13 | class Encoder(nn.Module): 14 | ''' エンコーダ 15 | dim_in: 入力特徴量の次元数 16 | dim_hidden: 隠れ層の次元数(bidirectional=Trueの場合, 17 | 実際の次元数はdim_hidden * 2) 18 | dim_proj: Projection層の次元数 19 | (これがエンコーダの出力次元数になる) 20 | num_layers: RNN層(およびProjection層)の数 21 | bidirectional: Trueにすると,bidirectional RNNを用いる 22 | sub_sample: レイヤーごとに設定する,フレームの間引き率 23 | num_layers=4のとき,sub_sample=[1,2,3,1]とすると 24 | 2層目でフレーム数を1/2に,3層目で1/3にする 25 | (出力のフレーム数は1/6になる) 26 | rnn_type: 'LSTM'か'GRU'を選択する 27 | ''' 28 | def __init__(self, 29 | dim_in, 30 | dim_hidden, 31 | dim_proj, 32 | num_layers=2, 33 | bidirectional=True, 34 | sub_sample=None, 35 | rnn_type='LSTM'): 36 | super(Encoder, self).__init__() 37 | # RNN層の数 38 | self.num_layers = num_layers 39 | 40 | # RNN層は1層ずつ定義して,リスト化する 41 | rnn = [] 42 | for n in range(self.num_layers): 43 | # RNNへの入力次元数は, 44 | # 最初の層のみdim_in,それ以外はdim_proj 45 | input_size = dim_in if n == 0 else dim_proj 46 | # rnn_type がGRUならGRUを,それ以外ならLSTMを用いる 47 | if rnn_type == 'GRU': 48 | rnn.append(nn.GRU(input_size=input_size, 49 | hidden_size=dim_hidden, 50 | num_layers=1, 51 | bidirectional=bidirectional, 52 | batch_first=True)) 53 | else: 54 | rnn.append(nn.LSTM(input_size=input_size, 55 | hidden_size=dim_hidden, 56 | num_layers=1, 57 | bidirectional=bidirectional, 58 | batch_first=True)) 59 | # 標準のリスト型のままだと, 60 | # Pytorchで扱えないので,ModuleListに変換する 61 | self.rnn = nn.ModuleList(rnn) 62 | 63 | # sub_sample の定義 64 | if sub_sample is None: 65 | # 定義されていない場合は,フレームの間引きを行わない 66 | # (sub_sampleは全要素1のリストにする) 67 | self.sub_sample = [1 for i in range(num_layers)] 68 | else: 69 | # 定義されている場合は,それを用いる 70 | self.sub_sample = sub_sample 71 | 72 | # Projection層もRNN層と同様に1層ずつ定義する 73 | proj = [] 74 | for n in range(self.num_layers): 75 | # Projection層の入力次元数 = RNN層の出力次元数. 76 | # bidiractional=Trueの場合は次元数が2倍になる 77 | input_size = dim_hidden * (2 if bidirectional else 1) 78 | proj.append(nn.Linear(in_features=input_size, 79 | out_features=dim_proj)) 80 | # RNN層と同様,ModuleListに変換する 81 | self.proj = nn.ModuleList(proj) 82 | 83 | 84 | def forward(self, sequence, lengths): 85 | ''' ネットワーク計算(forward処理)の関数 86 | sequence: 各発話の入力系列 [B x T x D] 87 | lengths: 各発話の系列長(フレーム数) [B] 88 | []の中はテンソルのサイズ 89 | B: ミニバッチ内の発話数(ミニバッチサイズ) 90 | T: 入力テンソルの系列長(ゼロ埋め部分含む) 91 | D: 入力次元数(dim_in) 92 | ''' 93 | # 出力とその長さ情報を入力で初期化 94 | output = sequence 95 | output_lengths = lengths 96 | 97 | # num_layersの数だけ,RNNとProjection層へ交互に入力する 98 | for n in range(self.num_layers): 99 | # RNN へ入力するため, 100 | # 入力をPackedSequenceデータに変換する 101 | rnn_input \ 102 | = nn.utils.rnn.pack_padded_sequence(output, 103 | output_lengths, 104 | batch_first=True) 105 | 106 | # GPUとcuDNNを使用している場合, 107 | # 以下の1行を入れると処理が速くなる 108 | # (パラメータデータのポインタをリセット) 109 | self.rnn[n].flatten_parameters() 110 | 111 | # RNN層に入力する 112 | output, (h, c) = self.rnn[n](rnn_input) 113 | 114 | # RNN出力をProjection層へ入力するため, 115 | # PackedSequenceデータからtensorへ戻す 116 | output, output_lengths \ 117 | = nn.utils.rnn.pad_packed_sequence(output, 118 | batch_first=True) 119 | 120 | # sub sampling (間引き)の実行 121 | # この層における間引き率を取得 122 | sub = self.sub_sample[n] 123 | if sub > 1: 124 | # 間引きを実行する 125 | output = output[:, ::sub] 126 | # フレーム数を更新する 127 | # 更新後のフレーム数=(更新前のフレーム数+1)//sub 128 | output_lengths = (output_lengths+1) // sub 129 | 130 | # Projection層に入力する 131 | output = self.proj[n](output) 132 | 133 | # sub samplingを実行した場合はフレーム数が変わるため, 134 | # 出力のフレーム数情報も出力する 135 | return output, output_lengths 136 | 137 | -------------------------------------------------------------------------------- /06rnn_attention/initialize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # モデルの初期化関数を定義します 5 | # 6 | 7 | # 数値演算用モジュール(numpy)をインポート 8 | import numpy as np 9 | 10 | 11 | def lecun_initialization(model): 12 | '''LeCunのパラメータ初期化方法の実行 13 | 各重み(バイアス成分除く)を,平均0,標準偏差 1/sqrt(dim) の 14 | 正規分布に基づく乱数で初期化(dim は入力次元数) 15 | model: Pytorchで定義したモデル 16 | ''' 17 | # モデルのパラメータを順に取り出し,初期化を実行する 18 | for param in model.parameters(): 19 | # パラメータの値を取り出す 20 | data = param.data 21 | # パラメータのテンソル次元数を取り出す 22 | dim = data.dim() 23 | # 次元数を元に処理を変える 24 | if dim == 1: 25 | # dim = 1 の場合はバイアス成分 26 | # ゼロに初期化する 27 | data.zero_() 28 | elif dim == 2: 29 | # dim = 2 の場合は線形射影の重み行列 30 | # 入力次元数 = size(1) を得る 31 | n = data.size(1) 32 | # 入力次元数の平方根の逆数を 33 | # 標準偏差とする正規分布乱数で初期化 34 | std = 1.0 / np.sqrt(n) 35 | data.normal_(0, std) 36 | elif dim == 3: 37 | # dim = 3 の場合は 1次元畳み込みの行列 38 | # 入力チャネル数 * カーネルサイズの 39 | # 平方根の逆数を標準偏差とする 40 | # 正規分布乱数で初期化 41 | n = data.size(1) * data.size(2) 42 | std = 1.0 / np.sqrt(n) 43 | data.normal_(0, std) 44 | elif dim == 4: 45 | # dim = 4 の場合は 2次元畳み込みの行列 46 | # 入力チャネル数 * カーネルサイズ(行) 47 | # * カーネルサイズ(列) 48 | # の平方根の逆数を標準偏差とする 49 | # 正規分布乱数で初期化 50 | n = data.size(1) * data.size(2) * data.size(3) 51 | std = 1.0 / np.sqrt(n) 52 | data.normal_(0, std) 53 | else: 54 | # それ以外は対応していない 55 | print('lecun_initialization: '\ 56 | 'dim > 4 is not supported.') 57 | exit(1) 58 | 59 | -------------------------------------------------------------------------------- /06rnn_attention/levenshtein.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # レーベンシュタイン距離を用いて, 5 | # 認識結果の誤り数を算出します. 6 | # 7 | 8 | import numpy as np 9 | import copy 10 | 11 | def calculate_error(hypothesis, reference): 12 | ''' レーベンシュタイン距離を計算し, 13 | 置換誤り,削除誤り,挿入誤りを出力する 14 | hypothesis: 認識結果(トークン毎に区切ったリスト形式) 15 | reference: 正解(同上) 16 | total_error: 総誤り数 17 | substitute_error: 置換誤り数 18 | delete_error: 削除誤り数 19 | insert_error: 挿入誤り数 20 | len_ref: 正解文のトークン数 21 | ''' 22 | # 認識結果および正解系列の長さを取得 23 | len_hyp = len(hypothesis) 24 | len_ref = len(reference) 25 | 26 | # 累積コスト行列を作成する 27 | # 行列の各要素には,トータルコスト, 28 | # 置換コスト,削除コスト,挿入コストの 29 | # 累積値が辞書形式で定義される. 30 | cost_matrix = [[{"total":0, 31 | "substitute":0, 32 | "delete":0, 33 | "insert":0} \ 34 | for j in range(len_ref+1)] \ 35 | for i in range(len_hyp+1)] 36 | 37 | # 0列目と0行目の入力 38 | for i in range(1, len_hyp+1): 39 | # 縦方向への遷移は,削除処理を意味する 40 | cost_matrix[i][0]["delete"] = i 41 | cost_matrix[i][0]["total"] = i 42 | for j in range(1, len_ref+1): 43 | # 横方向への遷移は,挿入処理を意味する 44 | cost_matrix[0][j]["insert"] = j 45 | cost_matrix[0][j]["total"] = j 46 | 47 | # 1列目と1行目以降の累積コストを計算していく 48 | for i in range(1, len_hyp+1): 49 | for j in range(1, len_ref+1): 50 | # 51 | # 各処理のコストを計算する 52 | # 53 | # 斜め方向の遷移時,文字が一致しない場合は, 54 | # 置換処理により累積コストが1増加 55 | substitute_cost = \ 56 | cost_matrix[i-1][j-1]["total"] \ 57 | + (0 if hypothesis[i-1] == reference[j-1] else 1) 58 | # 縦方向の遷移時は,削除処理により累積コストが1増加 59 | delete_cost = cost_matrix[i-1][j]["total"] + 1 60 | # 横方向の遷移時は,挿入処理により累積コストが1増加 61 | insert_cost = cost_matrix[i][j-1]["total"] + 1 62 | 63 | # 置換処理,削除処理,挿入処理のうち, 64 | # どの処理を行えば累積コストが最も小さくなるかを計算 65 | cost = [substitute_cost, delete_cost, insert_cost] 66 | min_index = np.argmin(cost) 67 | 68 | if min_index == 0: 69 | # 置換処理が累積コスト最小となる場合 70 | 71 | # 遷移元の累積コスト情報をコピー 72 | cost_matrix[i][j] = \ 73 | copy.copy(cost_matrix[i-1][j-1]) 74 | # 文字が一致しない場合は, 75 | # 累積置換コストを1増加させる 76 | cost_matrix[i][j]["substitute"] \ 77 | += (0 if hypothesis[i-1] \ 78 | == reference[j-1] else 1) 79 | elif min_index == 1: 80 | # 削除処理が累積コスト最小となる場合 81 | 82 | # 遷移元の累積コスト情報をコピー 83 | cost_matrix[i][j] = copy.copy(cost_matrix[i-1][j]) 84 | # 累積削除コストを1増加させる 85 | cost_matrix[i][j]["delete"] += 1 86 | else: 87 | # 置換処理が累積コスト最小となる場合 88 | 89 | # 遷移元の累積コスト情報をコピー 90 | cost_matrix[i][j] = copy.copy(cost_matrix[i][j-1]) 91 | # 累積挿入コストを1増加させる 92 | cost_matrix[i][j]["insert"] += 1 93 | 94 | # 累積トータルコスト(置換+削除+挿入コスト)を更新 95 | cost_matrix[i][j]["total"] = cost[min_index] 96 | 97 | # 98 | # エラーの数を出力する 99 | # このとき,削除コストは挿入誤り, 100 | # 挿入コストは削除誤りになる点に注意. 101 | # (削除コストが1である 102 | # = 1文字削除しないと正解文にならない 103 | # = 認識結果は1文字分余計に挿入されている 104 | # = 挿入誤りが1である) 105 | # 106 | 107 | # 累積コスト行列の右下の要素が最終的なコストとなる. 108 | total_error = cost_matrix[len_hyp][len_ref]["total"] 109 | substitute_error = cost_matrix[len_hyp][len_ref]["substitute"] 110 | # 削除誤り = 挿入コスト 111 | delete_error = cost_matrix[len_hyp][len_ref]["insert"] 112 | # 挿入誤り = 削除コスト 113 | insert_error = cost_matrix[len_hyp][len_ref]["delete"] 114 | 115 | # 各誤り数と,正解文の文字数 116 | # (誤り率を算出する際に分母として用いる)を出力 117 | return (total_error, 118 | substitute_error, 119 | delete_error, 120 | insert_error, 121 | len_ref) 122 | 123 | 124 | if __name__ == "__main__": 125 | # ref: 正解文 126 | # hyp: 認識結果 127 | ref = "狼が犬に似ているようにおべっか使いは友達のように見える" 128 | hyp = "オオ狼みがい塗に似ているようにオッつ界は伴ちのように見える" 129 | 130 | 131 | # 各文字列を,1文字ずつ区切ってリストデータにする 132 | hyp_list = list(hyp) 133 | ref_list = list(ref) 134 | 135 | # 誤り数を計算する 136 | total, substitute, delete, insert, ref_length \ 137 | = calculate_error(hyp_list, ref_list) 138 | 139 | # 誤りの数と,誤り率(100*誤り数/正解文の文字数)を出力する 140 | print("REF: %s" % ref) 141 | print("HYP: %s" % hyp) 142 | print("#TOKEN(REF): %d, #ERROR: %d, #SUB: %d, #DEL: %d, #INS: %d" \ 143 | % (ref_length, total, substitute, delete, insert)) 144 | print("UER: %.2f, SUBR: %.2f, DELR: %.2f, INSR: %.2f" \ 145 | % (100.0*total/ref_length, 146 | 100.0*substitute/ref_length, 147 | 100.0*delete/ref_length, 148 | 100.0*insert/ref_length)) 149 | 150 | -------------------------------------------------------------------------------- /06rnn_attention/my_dataset.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Pytorchで用いるDatasetの定義 5 | # 6 | 7 | # PytorchのDatasetモジュールをインポート 8 | from torch.utils.data import Dataset 9 | 10 | # 数値演算用モジュール(numpy)をインポート 11 | import numpy as np 12 | 13 | # sysモジュールをインポート 14 | import sys 15 | 16 | 17 | class SequenceDataset(Dataset): 18 | ''' ミニバッチデータを作成するクラス 19 | torch.utils.data.Datasetクラスを継承し, 20 | 以下の関数を定義する 21 | __len__: 総サンプル数を出力する関数 22 | __getitem__: 1サンプルのデータを出力する関数 23 | feat_scp: 特徴量リストファイル 24 | label_scp: ラベルファイル 25 | feat_mean: 特徴量の平均値ベクトル 26 | feat_std: 特徴量の次元毎の標準偏差を並べたベクトル 27 | pad_index: バッチ化の際にフレーム数を合わせる 28 | ためにpaddingする整数値 29 | splice: 前後(splice)フレームを特徴量を結合する 30 | splice=1とすると,前後1フレーム分結合 31 | するので次元数は3倍になる. 32 | splice=0の場合は何もしない 33 | ''' 34 | def __init__(self, 35 | feat_scp, 36 | label_scp, 37 | feat_mean, 38 | feat_std, 39 | pad_index=0, 40 | splice=0): 41 | # 発話の数 42 | self.num_utts = 0 43 | # 各発話のID 44 | self.id_list = [] 45 | # 各発話の特徴量ファイルへのパスを記したリスト 46 | self.feat_list = [] 47 | # 各発話の特徴量フレーム数を記したリスト 48 | self.feat_len_list = [] 49 | # 特徴量の平均値ベクトル 50 | self.feat_mean = feat_mean 51 | # 特徴量の標準偏差ベクトル 52 | self.feat_std = feat_std 53 | # 標準偏差のフロアリング 54 | # (0で割ることが発生しないようにするため) 55 | self.feat_std[self.feat_std<1E-10] = 1E-10 56 | # 特徴量の次元数 57 | self.feat_dim = \ 58 | np.size(self.feat_mean) 59 | # 各発話のラベル 60 | self.label_list = [] 61 | # 各発話のラベルの長さを記したリスト 62 | self.label_len_list = [] 63 | # フレーム数の最大値 64 | self.max_feat_len = 0 65 | # ラベル長の最大値 66 | self.max_label_len = 0 67 | # フレーム埋めに用いる整数値 68 | self.pad_index = pad_index 69 | # splice:前後nフレームの特徴量を結合 70 | self.splice = splice 71 | 72 | # 特徴量リスト,ラベルを1行ずつ 73 | # 読み込みながら情報を取得する 74 | with open(feat_scp, mode='r') as file_f, \ 75 | open(label_scp, mode='r') as file_l: 76 | for (line_feats, line_label) in zip(file_f, file_l): 77 | # 各行をスペースで区切り, 78 | # リスト型の変数にする 79 | parts_feats = line_feats.split() 80 | parts_label = line_label.split() 81 | 82 | # 発話ID(partsの0番目の要素)が特徴量と 83 | # ラベルで一致していなければエラー 84 | if parts_feats[0] != parts_label[0]: 85 | sys.stderr.write('IDs of feat and '\ 86 | 'label do not match.\n') 87 | exit(1) 88 | 89 | # 発話IDをリストに追加 90 | self.id_list.append(parts_feats[0]) 91 | # 特徴量ファイルのパスをリストに追加 92 | self.feat_list.append(parts_feats[1]) 93 | # フレーム数をリストに追加 94 | feat_len = np.int64(parts_feats[2]) 95 | self.feat_len_list.append(feat_len) 96 | 97 | # ラベル(番号で記載)をint型の 98 | # numpy arrayに変換 99 | label = np.int64(parts_label[1:]) 100 | # ラベルリストに追加 101 | self.label_list.append(label) 102 | # ラベルの長さを追加 103 | self.label_len_list.append(len(label)) 104 | 105 | # 発話数をカウント 106 | self.num_utts += 1 107 | 108 | # フレーム数の最大値を得る 109 | self.max_feat_len = \ 110 | np.max(self.feat_len_list) 111 | # ラベル長の最大値を得る 112 | self.max_label_len = \ 113 | np.max(self.label_len_list) 114 | 115 | # ラベルデータの長さを最大フレーム長に 116 | # 合わせるため,pad_indexの値で埋める 117 | for n in range(self.num_utts): 118 | # 埋めるフレームの数 119 | # = 最大フレーム数 - 自分のフレーム数 120 | pad_len = self.max_label_len \ 121 | - self.label_len_list[n] 122 | # pad_indexの値で埋める 123 | self.label_list[n] = \ 124 | np.pad(self.label_list[n], 125 | [0, pad_len], 126 | mode='constant', 127 | constant_values=self.pad_index) 128 | 129 | def __len__(self): 130 | ''' 学習データの総サンプル数を返す関数 131 | 本実装では発話単位でバッチを作成するため, 132 | 総サンプル数=発話数である. 133 | ''' 134 | return self.num_utts 135 | 136 | 137 | def __getitem__(self, idx): 138 | ''' サンプルデータを返す関数 139 | 本実装では発話単位でバッチを作成するため, 140 | idx=発話番号である. 141 | ''' 142 | # 特徴量系列のフレーム数 143 | feat_len = self.feat_len_list[idx] 144 | # ラベルの長さ 145 | label_len = self.label_len_list[idx] 146 | 147 | # 特徴量データを特徴量ファイルから読み込む 148 | feat = np.fromfile(self.feat_list[idx], 149 | dtype=np.float32) 150 | # フレーム数 x 次元数の配列に変形 151 | feat = feat.reshape(-1, self.feat_dim) 152 | 153 | # 平均と標準偏差を使って正規化(標準化)を行う 154 | feat = (feat - self.feat_mean) / self.feat_std 155 | 156 | # splicing: 前後 n フレームの特徴量を結合する 157 | org_feat = feat.copy() 158 | for n in range(-self.splice, self.splice+1): 159 | # 元々の特徴量を n フレームずらす 160 | tmp = np.roll(org_feat, n, axis=0) 161 | if n < 0: 162 | # 前にずらした場合は 163 | # 終端nフレームを0にする 164 | tmp[n:] = 0 165 | elif n > 0: 166 | # 後ろにずらした場合は 167 | # 始端nフレームを0にする 168 | tmp[:n] = 0 169 | else: 170 | continue 171 | # ずらした特徴量を次元方向に 172 | # 結合する 173 | feat = np.hstack([feat,tmp]) 174 | 175 | # 特徴量データのフレーム数を最大フレーム数に 176 | # 合わせるため,0で埋める 177 | pad_len = self.max_feat_len - feat_len 178 | feat = np.pad(feat, 179 | [(0, pad_len), (0, 0)], 180 | mode='constant', 181 | constant_values=0) 182 | 183 | # ラベル 184 | label = self.label_list[idx] 185 | 186 | # 発話ID 187 | utt_id = self.id_list[idx] 188 | 189 | # 特徴量,ラベル,フレーム数, 190 | # ラベル長,発話IDを返す 191 | return (feat, 192 | label, 193 | feat_len, 194 | label_len, 195 | utt_id) 196 | 197 | -------------------------------------------------------------------------------- /06rnn_attention/my_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # モデル構造を定義します 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch.nn as nn 9 | 10 | # 作成したEncoder, Decoderクラスをインポート 11 | from encoder import Encoder 12 | from decoder import Decoder 13 | 14 | # 作成した初期化関数をインポート 15 | from initialize import lecun_initialization 16 | 17 | import numpy as np 18 | 19 | 20 | class MyE2EModel(nn.Module): 21 | ''' Attention RNN によるEnd-to-Endモデルの定義 22 | dim_in: 入力次元数 23 | dim_enc_hid: エンコーダの隠れ層の次元数 24 | dim_enc_proj: エンコーダのProjection層の次元数 25 | (これがエンコーダの出力次元数になる) 26 | dim_dec_hid: デコーダのRNNの次元数 27 | dim_out: 出力の次元数(sosとeosを含む全トークン数) 28 | dim_att: Attention機構の次元数 29 | att_filter_size: LocationAwareAttentionのフィルタサイズ 30 | att_filter_num: LocationAwareAttentionのフィルタ数 31 | sos_id: トークンの番号 32 | enc_bidirectional: Trueにすると,エンコーダに 33 | bidirectional RNNを用いる 34 | enc_sub_sample: エンコーダにおいてレイヤーごとに設定する, 35 | フレームの間引き率 36 | enc_rnn_type: エンコーダRNNの種類.'LSTM'か'GRU'を選択する 37 | ''' 38 | def __init__(self, dim_in, dim_enc_hid, dim_enc_proj, 39 | dim_dec_hid, dim_out, dim_att, 40 | att_filter_size, att_filter_num, 41 | sos_id, att_temperature=1.0, 42 | enc_num_layers=2, dec_num_layers=2, 43 | enc_bidirectional=True, enc_sub_sample=None, 44 | enc_rnn_type='LSTM'): 45 | super(MyE2EModel, self).__init__() 46 | 47 | # エンコーダを作成 48 | self.encoder = Encoder(dim_in=dim_in, 49 | dim_hidden=dim_enc_hid, 50 | dim_proj=dim_enc_proj, 51 | num_layers=enc_num_layers, 52 | bidirectional=enc_bidirectional, 53 | sub_sample=enc_sub_sample, 54 | rnn_type=enc_rnn_type) 55 | 56 | # デコーダを作成 57 | self.decoder = Decoder(dim_in=dim_enc_proj, 58 | dim_hidden=dim_dec_hid, 59 | dim_out=dim_out, 60 | dim_att=dim_att, 61 | att_filter_size=att_filter_size, 62 | att_filter_num=att_filter_num, 63 | sos_id=sos_id, 64 | att_temperature=att_temperature, 65 | num_layers=dec_num_layers) 66 | 67 | # LeCunのパラメータ初期化を実行 68 | lecun_initialization(self) 69 | 70 | 71 | def forward(self, 72 | input_sequence, 73 | input_lengths, 74 | label_sequence=None): 75 | ''' ネットワーク計算(forward処理)の関数 76 | input_sequence: 各発話の入力系列 [B x Tin x D] 77 | input_lengths: 各発話の系列長(フレーム数) [B] 78 | label_sequence: 各発話の正解ラベル系列(学習時に用いる) [B x Tout] 79 | []の中はテンソルのサイズ 80 | B: ミニバッチ内の発話数(ミニバッチサイズ) 81 | Tin: 入力テンソルの系列長(ゼロ埋め部分含む) 82 | D: 入力次元数(dim_in) 83 | Tout: 正解ラベル系列の系列長(ゼロ埋め部分含む) 84 | ''' 85 | # エンコーダに入力する 86 | enc_out, enc_lengths = self.encoder(input_sequence, 87 | input_lengths) 88 | 89 | # デコーダに入力する 90 | dec_out = self.decoder(enc_out, 91 | enc_lengths, 92 | label_sequence) 93 | 94 | # デコーダ出力とエンコーダ出力系列長を出力する 95 | return dec_out, enc_lengths 96 | 97 | 98 | def save_att_matrix(self, utt, filename): 99 | ''' Attention行列を画像にして保存する 100 | utt: 出力する、バッチ内の発話番号 101 | filename: 出力ファイル名 102 | ''' 103 | # decoderのsave_att_matrixを実行 104 | self.decoder.save_att_matrix(utt, filename) 105 | 106 | -------------------------------------------------------------------------------- /07ctc_att_mtl/01_get_token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # ラベル中のトークン(音素/かな/キャラクター)を, 5 | # ニューラルネットワークで扱うため,文字から番号へ変換します. 6 | # また,文字-番号の対応を記したトークンリストも作成します. 7 | # 8 | 9 | # osモジュールをインポート 10 | import os 11 | 12 | 13 | def token_to_int(label_file_str, 14 | label_file_int, 15 | unknown_list_file, 16 | token_list, 17 | silence_tokens): 18 | ''' トークンリストを使って,ラベルファイルの 19 | 文字を番号に置き換えてファイルに出力する 20 | このとき,非音声トークンは削除する 21 | label_file_str: 文字で記述されたラベルファイル 22 | label_file_int: 番号で記述されたラベルファイルの出力先 23 | unknown_list_file: 未知のトークンを記述したファイルの出力先 24 | token_list: 文字-番号の対応情報が格納されたリスト 25 | silence_tokenas: 非音声トークンが格納されたリスト 26 | ''' 27 | # 各ファイルを開く 28 | with open(label_file_str, mode='r') as label_in, \ 29 | open(label_file_int, mode='w') as label_out, \ 30 | open(unknown_list_file, mode='w') as unk_list: 31 | # ラベルファイルを一行ずつ読み込む 32 | for line in label_in: 33 | # 読み込んだ行をスペースで区切り, 34 | # リスト型の変数にする 35 | text = line.split() 36 | 37 | # リストの0番目の要素は発話IDなので,そのまま出力する 38 | label_out.write('%s' % text[0]) 39 | 40 | # リストの1番目以降の要素は文字なので, 41 | # 1文字ずつ数字に置き換える 42 | for u in text[1:]: 43 | # 非音声トークンの場合はスキップする 44 | if u in silence_tokens: 45 | continue 46 | 47 | # 文字がトークンリストに存在するか確認 48 | if not u in token_list: 49 | # 存在しなかった場合 50 | # 未知トークンリストに書き込む 51 | unk_list.write('%s\n' % (u)) 52 | # 未知トークンには番号 1 を割り当てて出力 53 | label_out.write(' 1') 54 | else: 55 | # 存在する場合 56 | # 対応する番号を出力 57 | label_out.write(' %d' \ 58 | % (token_list.index(u) + 1)) 59 | label_out.write('\n') 60 | 61 | 62 | # 63 | # メイン関数 64 | # 65 | if __name__ == "__main__": 66 | 67 | # トークンの単位 68 | # phone:音素 kana:かな char:キャラクター 69 | unit_set = ['phone', 'kana', 'char'] 70 | 71 | # 非音声トークン(ポーズなど)の定義 72 | # 本プログラムでは,非音声トークンは扱わない 73 | # 0行目は音素,1行目はかな,2行目は文字の 74 | # 非音声トークンを定義する 75 | silence_set = [['pau'], 76 | [''], 77 | ['']] 78 | 79 | # 未知トークンの定義 80 | # 訓練データに無く,開発/評価データに有った 81 | # トークンはこの文字に置き換えられる 82 | unknown_token = '' 83 | 84 | # ラベルファイルの存在するフォルダ 85 | label_dir_train = '../data/label/train_small' 86 | label_dir_dev = '../data/label/dev' 87 | label_dir_test = '../data/label/test' 88 | 89 | # 実験ディレクトリ 90 | # train_smallを使った時とtrain_largeを使った時で 91 | # 異なる実験ディレクトリにする 92 | exp_dir = './exp_' + os.path.basename(label_dir_train) 93 | 94 | # 処理結果の出力先ディレクトリ 95 | output_dir = os.path.join(exp_dir, 'data') 96 | 97 | # phone, kana, char それぞれに対して処理 98 | for uid, unit in enumerate(unit_set): 99 | # 出力先ディレクトリ 100 | out_dir = os.path.join(output_dir, unit) 101 | 102 | # 出力ディレクトリが存在しない場合は作成する 103 | os.makedirs(out_dir, exist_ok=True) 104 | 105 | # 106 | # トークンリストを作成 107 | # 108 | # 学習データのラベルファイル 109 | label_train = os.path.join(label_dir_train, 'text_'+unit) 110 | # トークンリストを空リストで定義 111 | token_list = [] 112 | # 訓練データのラベルに存在するトークンを 113 | # token_listに登録する 114 | with open(label_train) as label_file: 115 | # 一行ずつ読み込む 116 | for line in label_file: 117 | # 読み込んだ行をスペースで区切り, 118 | # リスト型の変数にする 119 | # 0番目は発話IDなので,1番目以降を取り出す 120 | text = line.split()[1:] 121 | 122 | # トークンリストに結合 123 | token_list += text 124 | 125 | # set関数により,重複するトークンを削除する 126 | token_list = list(set(token_list)) 127 | 128 | # 非音声トークン 129 | silence_tokens = silence_set[uid] 130 | # 非音声トークンをリストから削除する 131 | for u in silence_tokens: 132 | if u in token_list: 133 | token_list.remove(u) 134 | 135 | # リストをソートする 136 | token_list = sorted(token_list) 137 | 138 | # 未知トークンをリストの先頭に挿入する 139 | token_list.insert(0, unknown_token) 140 | 141 | # トークンリストをファイルに出力する 142 | with open(os.path.join(out_dir, 'token_list'), 143 | mode='w') as token_file: 144 | for i, u in enumerate(token_list): 145 | # 「トークン 対応する番号」を記述 146 | # このとき,番号は1始まりにする. 147 | # (0はCTCのblankトークンに割り当てるため) 148 | token_file.write('%s %d\n' % (u, i+1)) 149 | 150 | # 151 | # 作成したトークンリストを使って,ラベルファイルの 152 | # 各トークンを文字から番号に置き換える 153 | # 154 | # 開発/評価データに存在するトークンで,token_listに 155 | # (=学習データに)存在しないトークンは未知トークンとして 156 | # unknown_listに登録する. 157 | # (unknown_listは以降の処理には使いませんが, 158 | # 処理結果を確認するために出力しています) 159 | # 160 | # 学習/開発/評価データのラベルそれぞれについて処理 161 | label_dir_list = [label_dir_train, 162 | label_dir_dev, 163 | label_dir_test] 164 | for label_dir in label_dir_list: 165 | # ディレクトリ名を取得(train_{small,large}/dev/test) 166 | name = os.path.basename(label_dir) 167 | # 入力ラベルファイル(各トークンが文字で表記) 168 | label_str = os.path.join(label_dir, 169 | 'text_'+unit) 170 | # 出力ラベルファイル(各トークンが数値で表記) 171 | label_int = os.path.join(out_dir, 172 | 'label_'+name) 173 | # 未知トークンリストの出力先 174 | unknown_list = os.path.join(out_dir, 175 | 'unknown_token_'+name) 176 | # ラベルファイルの文字->数値変換処理の実行 177 | token_to_int(label_str, 178 | label_int, 179 | unknown_list, 180 | token_list, 181 | silence_tokens) 182 | 183 | -------------------------------------------------------------------------------- /07ctc_att_mtl/04_scoring.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # 認識結果と正解文を参照して,認識エラー率を計算します. 5 | # 6 | 7 | # 認識エラー率を計算するモジュールをインポート 8 | import levenshtein 9 | 10 | # os, sysモジュールをインポート 11 | import os 12 | import sys 13 | 14 | 15 | if __name__ == "__main__": 16 | 17 | # トークンの単位 18 | # phone:音素 kana:かな char:キャラクター 19 | unit = 'phone' 20 | 21 | # Multi Task TrainingのCTC損失関数重み 22 | mtl_weight = 0.5 23 | 24 | # 実験ディレクトリ 25 | exp_dir = './exp_train_small' 26 | 27 | # デコード結果が格納されているディレクトリ 28 | decoded_dir = os.path.join(exp_dir, 29 | unit+'_model_attention_mtl'\ 30 | +str(mtl_weight), 31 | 'decode_test') 32 | 33 | # 認識結果が記述されたファイル 34 | hypothesis_file = os.path.join(decoded_dir, 'hypothesis.txt') 35 | 36 | # 正解文が記述されたファイル 37 | reference_file = os.path.join(decoded_dir, 'reference.txt') 38 | 39 | # エラー率算出結果を出力するディレクトリ 40 | out_dir = decoded_dir 41 | 42 | # エラー率算出結果の出力ファイル 43 | result_file = os.path.join(out_dir, 'result.txt') 44 | 45 | # 出力ディレクトリが存在しない場合は作成する 46 | os.makedirs(out_dir, exist_ok=True) 47 | 48 | # 各誤りの総数(エラー率算出時の分子) 49 | total_err = 0 50 | total_sub = 0 51 | total_del = 0 52 | total_ins = 0 53 | # 正解文の総文字数(エラー率算出時の分母) 54 | total_length = 0 55 | 56 | # 各ファイルをオープン 57 | with open(hypothesis_file, mode='r') as hyp_file, \ 58 | open(reference_file, mode='r') as ref_file, \ 59 | open(result_file, mode='w') as out_file: 60 | # 認識結果ファイル正解文ファイルを一行ずつ読み込む 61 | for line_hyp, line_ref in zip(hyp_file, ref_file): 62 | # 読み込んだ行をスペースで区切り,リスト型の変数にする 63 | parts_hyp = line_hyp.split() 64 | parts_ref = line_ref.split() 65 | 66 | # 発話ID(partsの0番目の要素)が一致しているか確認 67 | if parts_hyp[0] != parts_ref[0]: 68 | sys.stderr.write('Utterance ids of '\ 69 | 'hypothesis and reference do not match.') 70 | exit(1) 71 | 72 | # 1要素目以降が認識結果/正解分の文字列(リスト型) 73 | hypothesis = parts_hyp[1:] 74 | reference = parts_ref[1:] 75 | 76 | # 誤り数を計算する 77 | (error, substitute, delete, insert, ref_length) \ 78 | = levenshtein.calculate_error(hypothesis, 79 | reference) 80 | 81 | # 総誤り数を累積する 82 | total_err += error 83 | total_sub += substitute 84 | total_del += delete 85 | total_ins += insert 86 | total_length += ref_length 87 | 88 | # 各発話の結果を出力する 89 | out_file.write('ID: %s\n' % (parts_hyp[0])) 90 | out_file.write('#ERROR (#SUB #DEL #INS): '\ 91 | '%d (%d %d %d)\n' \ 92 | % (error, substitute, delete, insert)) 93 | out_file.write('REF: %s\n' % (' '.join(reference))) 94 | out_file.write('HYP: %s\n' % (' '.join(hypothesis))) 95 | out_file.write('\n') 96 | 97 | # 総エラー数を,正解文の総文字数で割り,エラー率を算出する 98 | err_rate = 100.0 * total_err / total_length 99 | sub_rate = 100.0 * total_sub / total_length 100 | del_rate = 100.0 * total_del / total_length 101 | ins_rate = 100.0 * total_ins / total_length 102 | 103 | # 最終結果を出力する 104 | out_file.write('------------------------------'\ 105 | '-----------------------------------------------\n') 106 | out_file.write('#TOKEN: %d, #ERROR: %d '\ 107 | '(#SUB: %d, #DEL: %d, #INS: %d)\n' \ 108 | % (total_length, total_err, 109 | total_sub, total_del, total_ins)) 110 | out_file.write('TER: %.2f%% (SUB: %.2f, '\ 111 | 'DEL: %.2f, INS: %.2f)\n' \ 112 | % (err_rate, sub_rate, del_rate, ins_rate)) 113 | print('TER: %.2f%% (SUB: %.2f, DEL: %.2f, INS: %.2f)' \ 114 | % (err_rate, sub_rate, del_rate, ins_rate)) 115 | out_file.write('------------------------------'\ 116 | '-----------------------------------------------\n') 117 | 118 | -------------------------------------------------------------------------------- /07ctc_att_mtl/encoder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # RNN エンコーダ部の実装です. 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch.nn as nn 9 | from torch.nn.utils.rnn import pack_padded_sequence 10 | from torch.nn.utils.rnn import pad_packed_sequence 11 | 12 | 13 | class Encoder(nn.Module): 14 | ''' エンコーダ 15 | dim_in: 入力特徴量の次元数 16 | dim_hidden: 隠れ層の次元数(bidirectional=Trueの場合, 17 | 実際の次元数はdim_hidden * 2) 18 | dim_proj: Projection層の次元数 19 | (これがエンコーダの出力次元数になる) 20 | num_layers: RNN層(およびProjection層)の数 21 | bidirectional: Trueにすると,bidirectional RNNを用いる 22 | sub_sample: レイヤーごとに設定する,フレームの間引き率 23 | num_layers=4のとき,sub_sample=[1,2,3,1]とすると 24 | 2層目でフレーム数を1/2に,3層目で1/3にする 25 | (出力のフレーム数は1/6になる) 26 | rnn_type: 'LSTM'か'GRU'を選択する 27 | ''' 28 | def __init__(self, 29 | dim_in, 30 | dim_hidden, 31 | dim_proj, 32 | num_layers=2, 33 | bidirectional=True, 34 | sub_sample=None, 35 | rnn_type='LSTM'): 36 | super(Encoder, self).__init__() 37 | # RNN層の数 38 | self.num_layers = num_layers 39 | 40 | # RNN層は1層ずつ定義して,リスト化する 41 | rnn = [] 42 | for n in range(self.num_layers): 43 | # RNNへの入力次元数は, 44 | # 最初の層のみdim_in,それ以外はdim_proj 45 | input_size = dim_in if n == 0 else dim_proj 46 | # rnn_type がGRUならGRUを,それ以外ならLSTMを用いる 47 | if rnn_type == 'GRU': 48 | rnn.append(nn.GRU(input_size=input_size, 49 | hidden_size=dim_hidden, 50 | num_layers=1, 51 | bidirectional=bidirectional, 52 | batch_first=True)) 53 | else: 54 | rnn.append(nn.LSTM(input_size=input_size, 55 | hidden_size=dim_hidden, 56 | num_layers=1, 57 | bidirectional=bidirectional, 58 | batch_first=True)) 59 | # 標準のリスト型のままだと, 60 | # Pytorchで扱えないので,ModuleListに変換する 61 | self.rnn = nn.ModuleList(rnn) 62 | 63 | # sub_sample の定義 64 | if sub_sample is None: 65 | # 定義されていない場合は,フレームの間引きを行わない 66 | # (sub_sampleは全要素1のリストにする) 67 | self.sub_sample = [1 for i in range(num_layers)] 68 | else: 69 | # 定義されている場合は,それを用いる 70 | self.sub_sample = sub_sample 71 | 72 | # Projection層もRNN層と同様に1層ずつ定義する 73 | proj = [] 74 | for n in range(self.num_layers): 75 | # Projection層の入力次元数 = RNN層の出力次元数. 76 | # bidiractional=Trueの場合は次元数が2倍になる 77 | input_size = dim_hidden * (2 if bidirectional else 1) 78 | proj.append(nn.Linear(in_features=input_size, 79 | out_features=dim_proj)) 80 | # RNN層と同様,ModuleListに変換する 81 | self.proj = nn.ModuleList(proj) 82 | 83 | 84 | def forward(self, sequence, lengths): 85 | ''' ネットワーク計算(forward処理)の関数 86 | sequence: 各発話の入力系列 [B x T x D] 87 | lengths: 各発話の系列長(フレーム数) [B] 88 | []の中はテンソルのサイズ 89 | B: ミニバッチ内の発話数(ミニバッチサイズ) 90 | T: 入力テンソルの系列長(ゼロ埋め部分含む) 91 | D: 入力次元数(dim_in) 92 | ''' 93 | # 出力とその長さ情報を入力で初期化 94 | output = sequence 95 | output_lengths = lengths 96 | 97 | # num_layersの数だけ,RNNとProjection層へ交互に入力する 98 | for n in range(self.num_layers): 99 | # RNN へ入力するため, 100 | # 入力をPackedSequenceデータに変換する 101 | rnn_input \ 102 | = nn.utils.rnn.pack_padded_sequence(output, 103 | output_lengths, 104 | batch_first=True) 105 | 106 | # GPUとcuDNNを使用している場合, 107 | # 以下の1行を入れると処理が速くなる 108 | # (パラメータデータのポインタをリセット) 109 | self.rnn[n].flatten_parameters() 110 | 111 | # RNN層に入力する 112 | output, (h, c) = self.rnn[n](rnn_input) 113 | 114 | # RNN出力をProjection層へ入力するため, 115 | # PackedSequenceデータからtensorへ戻す 116 | output, output_lengths \ 117 | = nn.utils.rnn.pad_packed_sequence(output, 118 | batch_first=True) 119 | 120 | # sub sampling (間引き)の実行 121 | # この層における間引き率を取得 122 | sub = self.sub_sample[n] 123 | if sub > 1: 124 | # 間引きを実行する 125 | output = output[:, ::sub] 126 | # フレーム数を更新する 127 | # 更新後のフレーム数=(更新前のフレーム数+1)//sub 128 | output_lengths = (output_lengths+1) // sub 129 | 130 | # Projection層に入力する 131 | output = self.proj[n](output) 132 | 133 | # sub samplingを実行した場合はフレーム数が変わるため, 134 | # 出力のフレーム数情報も出力する 135 | return output, output_lengths 136 | 137 | -------------------------------------------------------------------------------- /07ctc_att_mtl/initialize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # モデルの初期化関数を定義します 5 | # 6 | 7 | # 数値演算用モジュール(numpy)をインポート 8 | import numpy as np 9 | 10 | 11 | def lecun_initialization(model): 12 | '''LeCunのパラメータ初期化方法の実行 13 | 各重み(バイアス成分除く)を,平均0,標準偏差 1/sqrt(dim) の 14 | 正規分布に基づく乱数で初期化(dim は入力次元数) 15 | model: Pytorchで定義したモデル 16 | ''' 17 | # モデルのパラメータを順に取り出し,初期化を実行する 18 | for param in model.parameters(): 19 | # パラメータの値を取り出す 20 | data = param.data 21 | # パラメータのテンソル次元数を取り出す 22 | dim = data.dim() 23 | # 次元数を元に処理を変える 24 | if dim == 1: 25 | # dim = 1 の場合はバイアス成分 26 | # ゼロに初期化する 27 | data.zero_() 28 | elif dim == 2: 29 | # dim = 2 の場合は線形射影の重み行列 30 | # 入力次元数 = size(1) を得る 31 | n = data.size(1) 32 | # 入力次元数の平方根の逆数を 33 | # 標準偏差とする正規分布乱数で初期化 34 | std = 1.0 / np.sqrt(n) 35 | data.normal_(0, std) 36 | elif dim == 3: 37 | # dim = 3 の場合は 1次元畳み込みの行列 38 | # 入力チャネル数 * カーネルサイズの 39 | # 平方根の逆数を標準偏差とする 40 | # 正規分布乱数で初期化 41 | n = data.size(1) * data.size(2) 42 | std = 1.0 / np.sqrt(n) 43 | data.normal_(0, std) 44 | elif dim == 4: 45 | # dim = 4 の場合は 2次元畳み込みの行列 46 | # 入力チャネル数 * カーネルサイズ(行) 47 | # * カーネルサイズ(列) 48 | # の平方根の逆数を標準偏差とする 49 | # 正規分布乱数で初期化 50 | n = data.size(1) * data.size(2) * data.size(3) 51 | std = 1.0 / np.sqrt(n) 52 | data.normal_(0, std) 53 | else: 54 | # それ以外は対応していない 55 | print('lecun_initialization: '\ 56 | 'dim > 4 is not supported.') 57 | exit(1) 58 | 59 | -------------------------------------------------------------------------------- /07ctc_att_mtl/levenshtein.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # レーベンシュタイン距離を用いて, 5 | # 認識結果の誤り数を算出します. 6 | # 7 | 8 | import numpy as np 9 | import copy 10 | 11 | def calculate_error(hypothesis, reference): 12 | ''' レーベンシュタイン距離を計算し, 13 | 置換誤り,削除誤り,挿入誤りを出力する 14 | hypothesis: 認識結果(トークン毎に区切ったリスト形式) 15 | reference: 正解(同上) 16 | total_error: 総誤り数 17 | substitute_error: 置換誤り数 18 | delete_error: 削除誤り数 19 | insert_error: 挿入誤り数 20 | len_ref: 正解文のトークン数 21 | ''' 22 | # 認識結果および正解系列の長さを取得 23 | len_hyp = len(hypothesis) 24 | len_ref = len(reference) 25 | 26 | # 累積コスト行列を作成する 27 | # 行列の各要素には,トータルコスト, 28 | # 置換コスト,削除コスト,挿入コストの 29 | # 累積値が辞書形式で定義される. 30 | cost_matrix = [[{"total":0, 31 | "substitute":0, 32 | "delete":0, 33 | "insert":0} \ 34 | for j in range(len_ref+1)] \ 35 | for i in range(len_hyp+1)] 36 | 37 | # 0列目と0行目の入力 38 | for i in range(1, len_hyp+1): 39 | # 縦方向への遷移は,削除処理を意味する 40 | cost_matrix[i][0]["delete"] = i 41 | cost_matrix[i][0]["total"] = i 42 | for j in range(1, len_ref+1): 43 | # 横方向への遷移は,挿入処理を意味する 44 | cost_matrix[0][j]["insert"] = j 45 | cost_matrix[0][j]["total"] = j 46 | 47 | # 1列目と1行目以降の累積コストを計算していく 48 | for i in range(1, len_hyp+1): 49 | for j in range(1, len_ref+1): 50 | # 51 | # 各処理のコストを計算する 52 | # 53 | # 斜め方向の遷移時,文字が一致しない場合は, 54 | # 置換処理により累積コストが1増加 55 | substitute_cost = \ 56 | cost_matrix[i-1][j-1]["total"] \ 57 | + (0 if hypothesis[i-1] == reference[j-1] else 1) 58 | # 縦方向の遷移時は,削除処理により累積コストが1増加 59 | delete_cost = cost_matrix[i-1][j]["total"] + 1 60 | # 横方向の遷移時は,挿入処理により累積コストが1増加 61 | insert_cost = cost_matrix[i][j-1]["total"] + 1 62 | 63 | # 置換処理,削除処理,挿入処理のうち, 64 | # どの処理を行えば累積コストが最も小さくなるかを計算 65 | cost = [substitute_cost, delete_cost, insert_cost] 66 | min_index = np.argmin(cost) 67 | 68 | if min_index == 0: 69 | # 置換処理が累積コスト最小となる場合 70 | 71 | # 遷移元の累積コスト情報をコピー 72 | cost_matrix[i][j] = \ 73 | copy.copy(cost_matrix[i-1][j-1]) 74 | # 文字が一致しない場合は, 75 | # 累積置換コストを1増加させる 76 | cost_matrix[i][j]["substitute"] \ 77 | += (0 if hypothesis[i-1] \ 78 | == reference[j-1] else 1) 79 | elif min_index == 1: 80 | # 削除処理が累積コスト最小となる場合 81 | 82 | # 遷移元の累積コスト情報をコピー 83 | cost_matrix[i][j] = copy.copy(cost_matrix[i-1][j]) 84 | # 累積削除コストを1増加させる 85 | cost_matrix[i][j]["delete"] += 1 86 | else: 87 | # 置換処理が累積コスト最小となる場合 88 | 89 | # 遷移元の累積コスト情報をコピー 90 | cost_matrix[i][j] = copy.copy(cost_matrix[i][j-1]) 91 | # 累積挿入コストを1増加させる 92 | cost_matrix[i][j]["insert"] += 1 93 | 94 | # 累積トータルコスト(置換+削除+挿入コスト)を更新 95 | cost_matrix[i][j]["total"] = cost[min_index] 96 | 97 | # 98 | # エラーの数を出力する 99 | # このとき,削除コストは挿入誤り, 100 | # 挿入コストは削除誤りになる点に注意. 101 | # (削除コストが1である 102 | # = 1文字削除しないと正解文にならない 103 | # = 認識結果は1文字分余計に挿入されている 104 | # = 挿入誤りが1である) 105 | # 106 | 107 | # 累積コスト行列の右下の要素が最終的なコストとなる. 108 | total_error = cost_matrix[len_hyp][len_ref]["total"] 109 | substitute_error = cost_matrix[len_hyp][len_ref]["substitute"] 110 | # 削除誤り = 挿入コスト 111 | delete_error = cost_matrix[len_hyp][len_ref]["insert"] 112 | # 挿入誤り = 削除コスト 113 | insert_error = cost_matrix[len_hyp][len_ref]["delete"] 114 | 115 | # 各誤り数と,正解文の文字数 116 | # (誤り率を算出する際に分母として用いる)を出力 117 | return (total_error, 118 | substitute_error, 119 | delete_error, 120 | insert_error, 121 | len_ref) 122 | 123 | 124 | if __name__ == "__main__": 125 | # ref: 正解文 126 | # hyp: 認識結果 127 | ref = "狼が犬に似ているようにおべっか使いは友達のように見える" 128 | hyp = "オオ狼みがい塗に似ているようにオッつ界は伴ちのように見える" 129 | 130 | 131 | # 各文字列を,1文字ずつ区切ってリストデータにする 132 | hyp_list = list(hyp) 133 | ref_list = list(ref) 134 | 135 | # 誤り数を計算する 136 | total, substitute, delete, insert, ref_length \ 137 | = calculate_error(hyp_list, ref_list) 138 | 139 | # 誤りの数と,誤り率(100*誤り数/正解文の文字数)を出力する 140 | print("REF: %s" % ref) 141 | print("HYP: %s" % hyp) 142 | print("#TOKEN(REF): %d, #ERROR: %d, #SUB: %d, #DEL: %d, #INS: %d" \ 143 | % (ref_length, total, substitute, delete, insert)) 144 | print("UER: %.2f, SUBR: %.2f, DELR: %.2f, INSR: %.2f" \ 145 | % (100.0*total/ref_length, 146 | 100.0*substitute/ref_length, 147 | 100.0*delete/ref_length, 148 | 100.0*insert/ref_length)) 149 | 150 | -------------------------------------------------------------------------------- /07ctc_att_mtl/my_dataset.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Pytorchで用いるDatasetの定義 5 | # 6 | 7 | # PytorchのDatasetモジュールをインポート 8 | from torch.utils.data import Dataset 9 | 10 | # 数値演算用モジュール(numpy)をインポート 11 | import numpy as np 12 | 13 | # sysモジュールをインポート 14 | import sys 15 | 16 | 17 | class SequenceDataset(Dataset): 18 | ''' ミニバッチデータを作成するクラス 19 | torch.utils.data.Datasetクラスを継承し, 20 | 以下の関数を定義する 21 | __len__: 総サンプル数を出力する関数 22 | __getitem__: 1サンプルのデータを出力する関数 23 | feat_scp: 特徴量リストファイル 24 | label_scp: ラベルファイル 25 | feat_mean: 特徴量の平均値ベクトル 26 | feat_std: 特徴量の次元毎の標準偏差を並べたベクトル 27 | pad_index: バッチ化の際にフレーム数を合わせる 28 | ためにpaddingする整数値 29 | splice: 前後(splice)フレームを特徴量を結合する 30 | splice=1とすると,前後1フレーム分結合 31 | するので次元数は3倍になる. 32 | splice=0の場合は何もしない 33 | ''' 34 | def __init__(self, 35 | feat_scp, 36 | label_scp, 37 | feat_mean, 38 | feat_std, 39 | pad_index=0, 40 | splice=0): 41 | # 発話の数 42 | self.num_utts = 0 43 | # 各発話のID 44 | self.id_list = [] 45 | # 各発話の特徴量ファイルへのパスを記したリスト 46 | self.feat_list = [] 47 | # 各発話の特徴量フレーム数を記したリスト 48 | self.feat_len_list = [] 49 | # 特徴量の平均値ベクトル 50 | self.feat_mean = feat_mean 51 | # 特徴量の標準偏差ベクトル 52 | self.feat_std = feat_std 53 | # 標準偏差のフロアリング 54 | # (0で割ることが発生しないようにするため) 55 | self.feat_std[self.feat_std<1E-10] = 1E-10 56 | # 特徴量の次元数 57 | self.feat_dim = \ 58 | np.size(self.feat_mean) 59 | # 各発話のラベル 60 | self.label_list = [] 61 | # 各発話のラベルの長さを記したリスト 62 | self.label_len_list = [] 63 | # フレーム数の最大値 64 | self.max_feat_len = 0 65 | # ラベル長の最大値 66 | self.max_label_len = 0 67 | # フレーム埋めに用いる整数値 68 | self.pad_index = pad_index 69 | # splice:前後nフレームの特徴量を結合 70 | self.splice = splice 71 | 72 | # 特徴量リスト,ラベルを1行ずつ 73 | # 読み込みながら情報を取得する 74 | with open(feat_scp, mode='r') as file_f, \ 75 | open(label_scp, mode='r') as file_l: 76 | for (line_feats, line_label) in zip(file_f, file_l): 77 | # 各行をスペースで区切り, 78 | # リスト型の変数にする 79 | parts_feats = line_feats.split() 80 | parts_label = line_label.split() 81 | 82 | # 発話ID(partsの0番目の要素)が特徴量と 83 | # ラベルで一致していなければエラー 84 | if parts_feats[0] != parts_label[0]: 85 | sys.stderr.write('IDs of feat and '\ 86 | 'label do not match.\n') 87 | exit(1) 88 | 89 | # 発話IDをリストに追加 90 | self.id_list.append(parts_feats[0]) 91 | # 特徴量ファイルのパスをリストに追加 92 | self.feat_list.append(parts_feats[1]) 93 | # フレーム数をリストに追加 94 | feat_len = np.int64(parts_feats[2]) 95 | self.feat_len_list.append(feat_len) 96 | 97 | # ラベル(番号で記載)をint型の 98 | # numpy arrayに変換 99 | label = np.int64(parts_label[1:]) 100 | # ラベルリストに追加 101 | self.label_list.append(label) 102 | # ラベルの長さを追加 103 | self.label_len_list.append(len(label)) 104 | 105 | # 発話数をカウント 106 | self.num_utts += 1 107 | 108 | # フレーム数の最大値を得る 109 | self.max_feat_len = \ 110 | np.max(self.feat_len_list) 111 | # ラベル長の最大値を得る 112 | self.max_label_len = \ 113 | np.max(self.label_len_list) 114 | 115 | # ラベルデータの長さを最大フレーム長に 116 | # 合わせるため,pad_indexの値で埋める 117 | for n in range(self.num_utts): 118 | # 埋めるフレームの数 119 | # = 最大フレーム数 - 自分のフレーム数 120 | pad_len = self.max_label_len \ 121 | - self.label_len_list[n] 122 | # pad_indexの値で埋める 123 | self.label_list[n] = \ 124 | np.pad(self.label_list[n], 125 | [0, pad_len], 126 | mode='constant', 127 | constant_values=self.pad_index) 128 | 129 | def __len__(self): 130 | ''' 学習データの総サンプル数を返す関数 131 | 本実装では発話単位でバッチを作成するため, 132 | 総サンプル数=発話数である. 133 | ''' 134 | return self.num_utts 135 | 136 | 137 | def __getitem__(self, idx): 138 | ''' サンプルデータを返す関数 139 | 本実装では発話単位でバッチを作成するため, 140 | idx=発話番号である. 141 | ''' 142 | # 特徴量系列のフレーム数 143 | feat_len = self.feat_len_list[idx] 144 | # ラベルの長さ 145 | label_len = self.label_len_list[idx] 146 | 147 | # 特徴量データを特徴量ファイルから読み込む 148 | feat = np.fromfile(self.feat_list[idx], 149 | dtype=np.float32) 150 | # フレーム数 x 次元数の配列に変形 151 | feat = feat.reshape(-1, self.feat_dim) 152 | 153 | # 平均と標準偏差を使って正規化(標準化)を行う 154 | feat = (feat - self.feat_mean) / self.feat_std 155 | 156 | # splicing: 前後 n フレームの特徴量を結合する 157 | org_feat = feat.copy() 158 | for n in range(-self.splice, self.splice+1): 159 | # 元々の特徴量を n フレームずらす 160 | tmp = np.roll(org_feat, n, axis=0) 161 | if n < 0: 162 | # 前にずらした場合は 163 | # 終端nフレームを0にする 164 | tmp[n:] = 0 165 | elif n > 0: 166 | # 後ろにずらした場合は 167 | # 始端nフレームを0にする 168 | tmp[:n] = 0 169 | else: 170 | continue 171 | # ずらした特徴量を次元方向に 172 | # 結合する 173 | feat = np.hstack([feat,tmp]) 174 | 175 | # 特徴量データのフレーム数を最大フレーム数に 176 | # 合わせるため,0で埋める 177 | pad_len = self.max_feat_len - feat_len 178 | feat = np.pad(feat, 179 | [(0, pad_len), (0, 0)], 180 | mode='constant', 181 | constant_values=0) 182 | 183 | # ラベル 184 | label = self.label_list[idx] 185 | 186 | # 発話ID 187 | utt_id = self.id_list[idx] 188 | 189 | # 特徴量,ラベル,フレーム数, 190 | # ラベル長,発話IDを返す 191 | return (feat, 192 | label, 193 | feat_len, 194 | label_len, 195 | utt_id) 196 | 197 | -------------------------------------------------------------------------------- /07ctc_att_mtl/my_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # モデル構造を定義します 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch.nn as nn 9 | 10 | # 作成したEncoder, Decoderクラスをインポート 11 | from encoder import Encoder 12 | from decoder import Decoder 13 | 14 | # 作成した初期化関数をインポート 15 | from initialize import lecun_initialization 16 | 17 | import numpy as np 18 | 19 | 20 | class MyMTLModel(nn.Module): 21 | ''' Attention RNN + CTC によるEnd-to-Endモデルの定義 22 | dim_in: 入力次元数 23 | dim_enc_hid: エンコーダの隠れ層の次元数 24 | dim_enc_proj: エンコーダのProjection層の次元数 25 | (これがエンコーダの出力次元数になる) 26 | dim_dec_hid: デコーダのRNNの次元数 27 | dim_out: 出力の次元数(sosとeosを含む全トークン数) 28 | dim_att: Attention機構の次元数 29 | att_filter_size: LocationAwareAttentionのフィルタサイズ 30 | att_filter_num: LocationAwareAttentionのフィルタ数 31 | sos_id: トークンの番号 32 | enc_bidirectional: Trueにすると,エンコーダに 33 | bidirectional RNNを用いる 34 | enc_sub_sample: エンコーダにおいてレイヤーごとに設定する, 35 | フレームの間引き率 36 | enc_rnn_type: エンコーダRNNの種類.'LSTM'か'GRU'を選択する 37 | ''' 38 | def __init__(self, dim_in, dim_enc_hid, dim_enc_proj, 39 | dim_dec_hid, dim_out, dim_att, 40 | att_filter_size, att_filter_num, 41 | sos_id, att_temperature=1.0, 42 | enc_num_layers=2, dec_num_layers=2, 43 | enc_bidirectional=True, enc_sub_sample=None, 44 | enc_rnn_type='LSTM'): 45 | super(MyMTLModel, self).__init__() 46 | 47 | # エンコーダを作成 48 | self.encoder = Encoder(dim_in=dim_in, 49 | dim_hidden=dim_enc_hid, 50 | dim_proj=dim_enc_proj, 51 | num_layers=enc_num_layers, 52 | bidirectional=enc_bidirectional, 53 | sub_sample=enc_sub_sample, 54 | rnn_type=enc_rnn_type) 55 | 56 | # デコーダを作成 57 | self.decoder = Decoder(dim_in=dim_enc_proj, 58 | dim_hidden=dim_dec_hid, 59 | dim_out=dim_out, 60 | dim_att=dim_att, 61 | att_filter_size=att_filter_size, 62 | att_filter_num=att_filter_num, 63 | sos_id=sos_id, 64 | att_temperature=att_temperature, 65 | num_layers=dec_num_layers) 66 | 67 | # CTC出力層 68 | # 出力次元数はdim_outから分の1を引いた値 69 | self.ctc_out = nn.Linear(in_features=dim_enc_proj, 70 | out_features=dim_out) 71 | 72 | # LeCunのパラメータ初期化を実行 73 | lecun_initialization(self) 74 | 75 | 76 | def forward(self, 77 | input_sequence, 78 | input_lengths, 79 | label_sequence=None): 80 | ''' ネットワーク計算(forward処理)の関数 81 | input_sequence: 各発話の入力系列 [B x Tin x D] 82 | input_lengths: 各発話の系列長(フレーム数) [B] 83 | label_sequence: 各発話の正解ラベル系列(学習時に用いる) [B x Tout] 84 | []の中はテンソルのサイズ 85 | B: ミニバッチ内の発話数(ミニバッチサイズ) 86 | Tin: 入力テンソルの系列長(ゼロ埋め部分含む) 87 | D: 入力次元数(dim_in) 88 | Tout: 正解ラベル系列の系列長(ゼロ埋め部分含む) 89 | ''' 90 | # エンコーダに入力する 91 | enc_out, enc_lengths = self.encoder(input_sequence, 92 | input_lengths) 93 | 94 | # CTC出力層にエンコーダ出力を入れる 95 | ctc_output = self.ctc_out(enc_out) 96 | 97 | # デコーダに入力する 98 | dec_out = self.decoder(enc_out, 99 | enc_lengths, 100 | label_sequence) 101 | # デコーダ出力と,エンコーダ出力、 102 | # エンコーダ出力系列長情報を出力する 103 | return dec_out, ctc_output, enc_lengths 104 | 105 | 106 | def save_att_matrix(self, utt, filename): 107 | ''' Attention行列を画像にして保存する 108 | utt: 出力する、バッチ内の発話番号 109 | filename: 出力ファイル名 110 | ''' 111 | # decoderのsave_att_matrixを実行 112 | self.decoder.save_att_matrix(utt, filename) 113 | 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ryoichi Takashima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 파이썬으로 배우는 음성인식 2 | ![파이썬으로 배우는 음성인식_입체표지](https://user-images.githubusercontent.com/21074282/210909181-0466b4a4-f3e5-43d3-8934-552973c4ce2c.png) 3 | 4 | - 부제 음성인식의 기술 발전 동향부터 파이토치를 활용한 딥러닝 실습까지 5 | - 저자 다카시마 료이치 6 | - 역자 정권우 7 | - 출판사 비제이퍼블릭 8 | - 출간/배본가능일 2023년 1월 9일 9 | - 정가 27,000원 10 | - 페이지 336쪽 11 | 12 |

13 | ## 책 소개 14 |

음성인식의 기초부터 파이토치를 활용한 딥러닝 실습까지,

15 |

파이썬으로 배우는 음성인식 도서 출간!

16 | 17 | 음성인식이란 음성 신호로부터 발화 내용을 인식하는 기술, 즉 컴퓨터가 사람의 음성을 신호로 인식하여 처리하는 기술이다. AI 스피커와 스마트폰 음성 어시스턴트 등 음성인식 기술들은 이미 우리의 일상생활 속으로 깊이 스며들어 있다. 음성 번역 시스템의 전처리 단계, 회의록 자동 작성 시스템 등 다양한 곳에서 음성인식 기술이 사용된다. 특히나 음성인식 기술은 손을 사용하지 않고(Hands-free) 기계를 작동시킬 수 있어, 차량 내비게이션과의 연동 혹은 신체장애인의 입력 장치 등 다양한 방면에서 성장이 기대되는 기술이다. 18 |
19 | 이 책은 지금까지의 음성인식 기술의 역사와 기술 발전 동향을 이해하고, 파이썬과 파이토치를 통해 최신 음성인식 시스템을 실습한다. 음성인식의 목적이나 알고리즘의 개요를 설명하고, 소스 코드를 상세하게 분석하고, 마지막으로 소스 코드를 첨부하여 독자들이 직접 구현할 수 있도록 한다. 특히 인공지능기술이 만들어진 목적이나, 해당 기술로 목적을 달성할 수 있을지에 대해 명확히 제시한다. 이 책은 음성인식을 제대로 구현하고자 하는 개발자 및 학부생에게 꼭 필요한 필독서가 될 것이다. 20 | 21 |

22 | 소스 코드 다운로드 https://github.com/bjpublic/python_speech_recognition 23 | 24 | 25 |

26 | ## 저자 소개 27 | 28 |

다카시마 료이치 高島遼一

29 | 2013년 고베 대학 대학원 시스템 정보학 연구과 박사 후기 과정 수료(공학박사) 30 |
31 | 2011년 4월~2013년 3월 일본 학술 진흥회 특별 연구원 [DC2]. 2013년 4월에 ㈜히타치 제작소 연 32 | 구개발 그룹에 입사하여 장비 이상 검사와 음성인식을 위한 잡음 제거 등, 음성 및 음향 신호 처 33 | 리에 관한 연구 개발에 종사했다. 2016년 10월부터 2018년 9월까지 국립 연구 개발 법인 정보 통 34 | 신 연구 기구에 전출하여 음성인식에 관한 연구 개발에 종사했다. 2019년 4월부터 고베 대학 도 35 | 시안전 연구 센터 겸 동 대학원 시스템 정보학 연구과 준교수로 부임했고, 현재는 음성인식 기반 36 | 의 음성 처리, 기계 학습 기술과 복지 분야 응용 관련 연구에 종사하고 있다. 37 | 38 | 39 |


40 | ## 역자 소개 41 | 42 |

정권우

43 | 카네기멜론 대학교 응용수학과 학부를 졸업했다. 5살부터 유초중고 시절을 일본 도쿄에서 보내고, 대학교를 미국으로 진학한 덕분에 한국어, 일본어, 영어에 능통하다. 44 |
45 | 대학 시절에는 금융 수학을 전공하여, UBS Seoul, JP Morgan Tokyo 지사에서 사회생활을 시작했다. 대학교 2학년 때 처음으로 프로그래밍을 접하면서 관심을 갖게 되어, 휴학 후 한국에서의 46 | 병역특례 군복무 시절 머신러닝을 독학하고, 캐글 경진대회에 출전한 이력이 있다. 모바일 콘텐츠 플랫폼 카카오페이지를 운영하는 다음카카오의 자회사 포도트리를 거쳐 P2P 투자 기업 8퍼센트의 챗봇을 개발한 데이터나다에서 머신러닝 엔지니어로 근무한 경력이 있으며, 현재는 네이버 파파고 팀에서 딥러닝을 통해 더 나은 번역기를 개발하고 있다. 텍스트 번역으로 시작한 기계번역은 어느덧 사진을 번역해주는 이미지 번역으로 발전한 것처럼, 앞으로 궁극적으로는 음성을 바로 번역하는 것으로 발전할 것이라 생각한다. 음성인식을 공부하는 과정에서 이 책을 만나고, 직접 번역하게 된 것을 영광으로 생각한다. 47 | 48 | 49 |


50 | ## 출판사 리뷰 51 |

음성인식 머신러닝 모델을 파이썬으로 구현 및 실습하기 위한

52 |

'음성인식 기초 이론 및 단계별 실습서'

53 | 54 | 음성인식 기술은 나날이 발전해가고 있습니다. 중요한 회의나 강의 내용을 필사해야 할 경우 과거에는 녹음기를 사용했지만 최근에는 음성인식 노트 앱을 사용합니다. 또한 핸드폰에 깔려있는 애플 시리, 구글 어시스턴트, 삼성 빅스비 혹은 집에 하나씩 가지고 있는 스마트 스피커, 차량에서 사용하는 내비게이션 앱에서 지원하는 음성인식 등, 음성인식은 이미 우리의 일상에 스며들었습니다. 음성인식은 최근 들어 딥러닝 모델링을 기반으로 급격한 기술적 도약을 경험하며 인식 품질이 개선되었지만, 여전히 완벽한 음성인식에 도달하기 위해서는 더 많은 데이터와 모델이 필요합니다. 잡음이 섞인 음성, 복수의 사람이 동시에 이야기를 할 때의 음성인식 등 현실적으로 풀어야 할 문제들이 많습니다. 55 |
56 | 이 책은 과거부터 현재까지의 음성인식 기술 발전의 변천사를 다루고 있습니다. 과거의 음성인식 기법이 현재의 음성인식 기술에 미친 영향에 대해 마치 이야기를 들려주듯이 설명하고 있습니다. 또한 음성인식 기술에 많이 사용되는 확률과 신호 처리 이론을 포함한 음성인식의 각 기법에 대한 이론적 지식과 실습 코드를 제공하며, 딥러닝을 이용한 최신 음성인식 시스템을 파이썬과 파이토치를 활용하여 직접 개발해 볼 수 있도록 돕습니다. 57 |
58 | 이 책의 저자는 처음 음성인식을 접하는 독자분들에게 음성인식 기술의 이해를 넘어서 음성인식 기술의 전체적인 흐름과 특징을 이해할 수 있게끔 집필하였습니다. 향후 음성인식 기술이 어떻게 더 발전해 나갈지 관심 있으신 분과 현업에 빠르게 음성인식기술을 적용해 보고 싶으신 분, 음성 처리 및 기계 학습 전반에 관심이 있는 분, 그리고 앞으로 진지하게 음성인식 기술에 대해서 배우고 싶은 분들에게 이 책을 추천합니다. 59 | 60 |

61 | 62 | ## 부록 63 | [부록](appendix.pdf)에서는 페이지 사정상 책에 싣지 못한, 다변량 정규 분포의 파라미터(평균치 벡터 및 분산 공분산 행렬), 64 | GMM의 파라미터(혼합 가중치, 평균값 벡터, 분산 공분산 행렬), 그리고 HMM 의 상태 전이 확률을 구하는 식을 도출하고 있습니다 65 | 일본어로 작성되어 있습니다. 참고 부탁드립니다. 66 | -------------------------------------------------------------------------------- /appendix.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjpublic/python_speech_recognition/edd2beda91f3ce7b06833e406bbe85414ba41f65/appendix.pdf --------------------------------------------------------------------------------