├── .gitignore ├── 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 /.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | */exp* 3 | */backup* 4 | */wav 5 | */fbank 6 | */mfcc 7 | */__pycache__ 8 | */*.png 9 | 02dp_matching/alignment.txt 10 | -------------------------------------------------------------------------------- /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 layer in self.hidden: 67 | output = layer(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/03_decode_ctc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # CTCによるデコーディングを行います 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch 9 | from torch.utils.data import DataLoader 10 | 11 | # 作成したDatasetクラスをインポート 12 | from my_dataset import SequenceDataset 13 | 14 | # 数値演算用モジュール(numpy)をインポート 15 | import numpy as np 16 | 17 | # モデルの定義をインポート 18 | from my_model import MyCTCModel 19 | 20 | # json形式の入出力を行うモジュールをインポート 21 | import json 22 | 23 | # os, sysモジュールをインポート 24 | import os 25 | import sys 26 | 27 | 28 | def ctc_simple_decode(int_vector, token_list): 29 | ''' 以下の手順で,フレーム単位のCTC出力をトークン列に変換する 30 | 1. 同じ文字が連続して出現する場合は削除 31 | 2. blank を削除 32 | int_vector: フレーム単位のCTC出力(整数値列) 33 | token_list: トークンリスト 34 | output: トークン列 35 | ''' 36 | # 出力文字列 37 | output = [] 38 | # 一つ前フレームの文字番号 39 | prev_token = -1 40 | # フレーム毎の出力文字系列を前から順番にチェックしていく 41 | for n in int_vector: 42 | if n != prev_token: 43 | # 1. 前フレームと同じトークンではない 44 | if n != 0: 45 | # 2. かつ,blank(番号=0)ではない 46 | # --> token_listから対応する文字を抽出し, 47 | # 出力文字列に加える 48 | output.append(token_list[n]) 49 | # 前フレームのトークンを更新 50 | prev_token = n 51 | return output 52 | 53 | 54 | # 55 | # メイン関数 56 | # 57 | if __name__ == "__main__": 58 | 59 | # 60 | # 設定ここから 61 | # 62 | 63 | # トークンの単位 64 | # phone:音素 kana:かな char:キャラクター 65 | unit = 'phone' 66 | 67 | # 実験ディレクトリ 68 | exp_dir = './exp_train_small' 69 | 70 | # 評価データの特徴量(feats.scp)が存在するディレクトリ 71 | feat_dir_test = '../01compute_features/fbank/test' 72 | 73 | # 評価データの特徴量リストファイル 74 | feat_scp_test = os.path.join(feat_dir_test, 'feats.scp') 75 | 76 | # 評価データのラベルファイル 77 | label_test = os.path.join(exp_dir, 'data', unit, 'label_test') 78 | 79 | # トークンリスト 80 | token_list_path = os.path.join(exp_dir, 'data', unit, 81 | 'token_list') 82 | 83 | # 学習済みモデルが格納されているディレクトリ 84 | model_dir = os.path.join(exp_dir, unit+'_model_ctc') 85 | 86 | # 訓練データから計算された特徴量の平均/標準偏差ファイル 87 | mean_std_file = os.path.join(model_dir, 'mean_std.txt') 88 | 89 | # 学習済みのモデルファイル 90 | model_file = os.path.join(model_dir, 'best_model.pt') 91 | 92 | # デコード結果を出力するディレクトリ 93 | output_dir = os.path.join(model_dir, 'decode_test') 94 | 95 | # デコード結果および正解文の出力ファイル 96 | hypothesis_file = os.path.join(output_dir, 'hypothesis.txt') 97 | reference_file = os.path.join(output_dir, 'reference.txt') 98 | 99 | # 学習時に出力した設定ファイル 100 | config_file = os.path.join(model_dir, 'config.json') 101 | 102 | # ミニバッチに含める発話数 103 | batch_size = 10 104 | 105 | # 106 | # 設定ここまで 107 | # 108 | 109 | # 設定ファイルを読み込む 110 | with open(config_file, mode='r') as f: 111 | config = json.load(f) 112 | 113 | # 読み込んだ設定を反映する 114 | # 中間層のレイヤー数 115 | num_layers = config['num_layers'] 116 | # 層ごとのsub sampling設定 117 | sub_sample = config['sub_sample'] 118 | # RNNのタイプ(LSTM or GRU) 119 | rnn_type = config['rnn_type'] 120 | # 中間層の次元数 121 | hidden_dim = config['hidden_dim'] 122 | # Projection層の次元数 123 | projection_dim = config['projection_dim'] 124 | # bidirectional を用いるか(Trueなら用いる) 125 | bidirectional = config['bidirectional'] 126 | 127 | # 出力ディレクトリが存在しない場合は作成する 128 | os.makedirs(output_dir, exist_ok=True) 129 | 130 | # 特徴量の平均/標準偏差ファイルを読み込む 131 | with open(mean_std_file, mode='r') as f: 132 | # 全行読み込み 133 | lines = f.readlines() 134 | # 1行目(0始まり)が平均値ベクトル(mean), 135 | # 3行目が標準偏差ベクトル(std) 136 | mean_line = lines[1] 137 | std_line = lines[3] 138 | # スペース区切りのリストに変換 139 | feat_mean = mean_line.split() 140 | feat_std = std_line.split() 141 | # numpy arrayに変換 142 | feat_mean = np.array(feat_mean, 143 | dtype=np.float32) 144 | feat_std = np.array(feat_std, 145 | dtype=np.float32) 146 | 147 | # 次元数の情報を得る 148 | feat_dim = np.size(feat_mean) 149 | 150 | # トークンリストをdictionary型で読み込む 151 | # このとき,0番目は blank と定義する 152 | token_list = {0: ''} 153 | with open(token_list_path, mode='r') as f: 154 | # 1行ずつ読み込む 155 | for line in f: 156 | # 読み込んだ行をスペースで区切り, 157 | # リスト型の変数にする 158 | parts = line.split() 159 | # 0番目の要素がトークン,1番目の要素がID 160 | token_list[int(parts[1])] = parts[0] 161 | 162 | # トークン数(blankを含む) 163 | num_tokens = len(token_list) 164 | 165 | # ニューラルネットワークモデルを作成する 166 | # 入力の次元数は特徴量の次元数, 167 | # 出力の次元数はトークン数となる 168 | model = MyCTCModel(dim_in=feat_dim, 169 | dim_enc_hid=hidden_dim, 170 | dim_enc_proj=projection_dim, 171 | dim_out=num_tokens, 172 | enc_num_layers=num_layers, 173 | enc_bidirectional=bidirectional, 174 | enc_sub_sample=sub_sample, 175 | enc_rnn_type=rnn_type) 176 | 177 | # モデルのパラメータを読み込む 178 | model.load_state_dict(torch.load(model_file)) 179 | 180 | # 訓練/開発データのデータセットを作成する 181 | test_dataset = SequenceDataset(feat_scp_test, 182 | label_test, 183 | feat_mean, 184 | feat_std) 185 | 186 | # 評価データのDataLoaderを呼び出す 187 | test_loader = DataLoader(test_dataset, 188 | batch_size=batch_size, 189 | shuffle=False, 190 | num_workers=4) 191 | 192 | # CUDAが使える場合はモデルパラメータをGPUに, 193 | # そうでなければCPUに配置する 194 | if torch.cuda.is_available(): 195 | device = torch.device('cuda') 196 | else: 197 | device = torch.device('cpu') 198 | model = model.to(device) 199 | 200 | # モデルを評価モードに設定する 201 | model.eval() 202 | 203 | # デコード結果および正解ラベルをファイルに書き込みながら 204 | # 以下の処理を行う 205 | with open(hypothesis_file, mode='w') as hyp_file, \ 206 | open(reference_file, mode='w') as ref_file: 207 | # 評価データのDataLoaderから1ミニバッチ 208 | # ずつ取り出して処理する. 209 | # これを全ミニバッチ処理が終わるまで繰り返す. 210 | # ミニバッチに含まれるデータは, 211 | # 音声特徴量,ラベル,フレーム数, 212 | # ラベル長,発話ID 213 | for (features, labels, feat_lens, 214 | label_lens, utt_ids) in test_loader: 215 | 216 | # PackedSequence の仕様上, 217 | # ミニバッチがフレーム長の降順で 218 | # ソートされている必要があるため, 219 | # ソートを実行する 220 | sorted_lens, indices = \ 221 | torch.sort(feat_lens.view(-1), 222 | dim=0, 223 | descending=True) 224 | features = features[indices] 225 | feat_lens = sorted_lens 226 | 227 | # CUDAが使える場合はデータをGPUに, 228 | # そうでなければCPUに配置する 229 | features = features.to(device) 230 | 231 | # モデルの出力を計算(フォワード処理) 232 | # out_lensは処理後のフレーム数. 233 | # sub_sampleを行った場合は, 234 | # out_lensはfeat_lensより小さい値になる 235 | outputs, out_lens = model(features, feat_lens) 236 | 237 | # バッチ内の1発話ごとに以下の処理を行う 238 | for n in range(outputs.size(0)): 239 | # 出力はフレーム長でソートされている 240 | # 元のデータ並びに戻すため, 241 | # 対応する要素番号を取得する 242 | idx = torch.nonzero(indices==n, 243 | as_tuple=False).view(-1)[0] 244 | 245 | # 本来のCTCの確率計算は, 246 | # 複数存在するパスを考慮するが, 247 | # ここでは簡単のため,各フレームのmax値を 248 | # たどる Best path decoding を行う 249 | _, hyp_per_frame = torch.max(outputs[idx], 1) 250 | # numpy.array型に変換 251 | hyp_per_frame = hyp_per_frame.cpu().numpy() 252 | # 認識結果の文字列を取得 253 | hypothesis = \ 254 | ctc_simple_decode(hyp_per_frame, 255 | token_list) 256 | 257 | # 正解の文字列を取得 258 | reference = [] 259 | for m in labels[n][:label_lens[n]].cpu().numpy(): 260 | reference.append(token_list[m]) 261 | 262 | # 結果を書き込む 263 | # (' '.join() は,リスト形式のデータを 264 | # スペース区切りで文字列に変換している) 265 | hyp_file.write('%s %s\n' \ 266 | % (utt_ids[n], ' '.join(hypothesis))) 267 | ref_file.write('%s %s\n' \ 268 | % (utt_ids[n], ' '.join(reference))) 269 | 270 | -------------------------------------------------------------------------------- /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 9 | import torch.nn as nn 10 | from torch.nn.utils.rnn import pack_padded_sequence 11 | from torch.nn.utils.rnn import pad_packed_sequence 12 | 13 | 14 | class Encoder(nn.Module): 15 | ''' エンコーダ 16 | dim_in: 入力特徴量の次元数 17 | dim_hidden: 隠れ層の次元数(bidirectional=Trueの場合, 18 | 実際の次元数はdim_hidden * 2) 19 | dim_proj: Projection層の次元数 20 | (これがエンコーダの出力次元数になる) 21 | num_layers: RNN層(およびProjection層)の数 22 | bidirectional: Trueにすると,bidirectional RNNを用いる 23 | sub_sample: レイヤーごとに設定する,フレームの間引き率 24 | num_layers=4のとき,sub_sample=[1,2,3,1]とすると 25 | 2層目でフレーム数を1/2に,3層目で1/3にする 26 | (出力のフレーム数は1/6になる) 27 | rnn_type: 'LSTM'か'GRU'を選択する 28 | ''' 29 | def __init__(self, 30 | dim_in, 31 | dim_hidden, 32 | dim_proj, 33 | num_layers=2, 34 | bidirectional=True, 35 | sub_sample=None, 36 | rnn_type='LSTM'): 37 | super(Encoder, self).__init__() 38 | # RNN層の数 39 | self.num_layers = num_layers 40 | 41 | # RNN層は1層ずつ定義して,リスト化する 42 | rnn = [] 43 | for n in range(self.num_layers): 44 | # RNNへの入力次元数は, 45 | # 最初の層のみdim_in,それ以外はdim_proj 46 | input_size = dim_in if n == 0 else dim_proj 47 | # rnn_type がGRUならGRUを,それ以外ならLSTMを用いる 48 | if rnn_type == 'GRU': 49 | rnn.append(nn.GRU(input_size=input_size, 50 | hidden_size=dim_hidden, 51 | num_layers=1, 52 | bidirectional=bidirectional, 53 | batch_first=True)) 54 | else: 55 | rnn.append(nn.LSTM(input_size=input_size, 56 | hidden_size=dim_hidden, 57 | num_layers=1, 58 | bidirectional=bidirectional, 59 | batch_first=True)) 60 | # 標準のリスト型のままだと, 61 | # Pytorchで扱えないので,ModuleListに変換する 62 | self.rnn = nn.ModuleList(rnn) 63 | 64 | # sub_sample の定義 65 | if sub_sample is None: 66 | # 定義されていない場合は,フレームの間引きを行わない 67 | # (sub_sampleは全要素1のリストにする) 68 | self.sub_sample = [1 for i in range(num_layers)] 69 | else: 70 | # 定義されている場合は,それを用いる 71 | self.sub_sample = sub_sample 72 | 73 | # Projection層もRNN層と同様に1層ずつ定義する 74 | proj = [] 75 | for n in range(self.num_layers): 76 | # Projection層の入力次元数 = RNN層の出力次元数. 77 | # bidiractional=Trueの場合は次元数が2倍になる 78 | input_size = dim_hidden * (2 if bidirectional else 1) 79 | proj.append(nn.Linear(in_features=input_size, 80 | out_features=dim_proj)) 81 | # RNN層と同様,ModuleListに変換する 82 | self.proj = nn.ModuleList(proj) 83 | 84 | 85 | def forward(self, sequence, lengths): 86 | ''' ネットワーク計算(forward処理)の関数 87 | sequence: 各発話の入力系列 [B x T x D] 88 | lengths: 各発話の系列長(フレーム数) [B] 89 | []の中はテンソルのサイズ 90 | B: ミニバッチ内の発話数(ミニバッチサイズ) 91 | T: 入力テンソルの系列長(ゼロ埋め部分含む) 92 | D: 入力次元数(dim_in) 93 | ''' 94 | # 出力とその長さ情報を入力で初期化 95 | output = sequence 96 | output_lengths = lengths 97 | 98 | # num_layersの数だけ,RNNとProjection層へ交互に入力する 99 | for n in range(self.num_layers): 100 | # RNN へ入力するため, 101 | # 入力をPackedSequenceデータに変換する 102 | rnn_input \ 103 | = nn.utils.rnn.pack_padded_sequence(output, 104 | output_lengths, 105 | batch_first=True) 106 | 107 | # GPUとcuDNNを使用している場合, 108 | # 以下の1行を入れると処理が速くなる 109 | # (パラメータデータのポインタをリセット) 110 | self.rnn[n].flatten_parameters() 111 | 112 | # RNN層に入力する 113 | output, (h, c) = self.rnn[n](rnn_input) 114 | 115 | # RNN出力をProjection層へ入力するため, 116 | # PackedSequenceデータからtensorへ戻す 117 | output, output_lengths \ 118 | = nn.utils.rnn.pad_packed_sequence(output, 119 | batch_first=True) 120 | 121 | # sub sampling (間引き)の実行 122 | # この層における間引き率を取得 123 | sub = self.sub_sample[n] 124 | if sub > 1: 125 | # 間引きを実行する 126 | output = output[:, ::sub] 127 | # フレーム数を更新する 128 | # 更新後のフレーム数=(更新前のフレーム数+1)//sub 129 | output_lengths = torch.div((output_lengths+1), sub, 130 | rounding_mode='floor') 131 | # Projection層に入力する 132 | output = torch.tanh(self.proj[n](output)) 133 | 134 | # sub samplingを実行した場合はフレーム数が変わるため, 135 | # 出力のフレーム数情報も出力する 136 | return output, output_lengths 137 | 138 | -------------------------------------------------------------------------------- /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/03_decode_attention.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # RNN Attention Encoder-Decoderによるデコーディングを行います 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch 9 | from torch.utils.data import DataLoader 10 | 11 | # 作成したDatasetクラスをインポート 12 | from my_dataset import SequenceDataset 13 | 14 | # 数値演算用モジュール(numpy)をインポート 15 | import numpy as np 16 | 17 | # モデルの定義をインポート 18 | from my_model import MyE2EModel 19 | 20 | # json形式の入出力を行うモジュールをインポート 21 | import json 22 | 23 | # os, sysモジュールをインポート 24 | import os 25 | import sys 26 | 27 | 28 | # 29 | # メイン関数 30 | # 31 | if __name__ == "__main__": 32 | 33 | # 34 | # 設定ここから 35 | # 36 | 37 | # トークンの単位 38 | # phone:音素 kana:かな char:キャラクター 39 | unit = 'phone' 40 | 41 | # 実験ディレクトリ 42 | exp_dir = './exp_train_small' 43 | 44 | # 評価データの特徴量(feats.scp)が存在するディレクトリ 45 | feat_dir_test = '../01compute_features/fbank/test' 46 | 47 | # 評価データの特徴量リストファイル 48 | feat_scp_test = os.path.join(feat_dir_test, 'feats.scp') 49 | 50 | # 評価データのラベルファイル 51 | label_test = os.path.join(exp_dir, 'data', unit, 'label_test') 52 | 53 | # トークンリスト 54 | token_list_path = os.path.join(exp_dir, 'data', unit, 55 | 'token_list') 56 | 57 | # 学習済みモデルが格納されているディレクトリ 58 | model_dir = os.path.join(exp_dir, unit+'_model_attention') 59 | 60 | # 訓練データから計算された特徴量の平均/標準偏差ファイル 61 | mean_std_file = os.path.join(model_dir, 'mean_std.txt') 62 | 63 | # 学習済みのモデルファイル 64 | model_file = os.path.join(model_dir, 'best_model.pt') 65 | 66 | # デコード結果を出力するディレクトリ 67 | output_dir = os.path.join(model_dir, 'decode_test') 68 | 69 | # デコード結果および正解文の出力ファイル 70 | hypothesis_file = os.path.join(output_dir, 'hypothesis.txt') 71 | reference_file = os.path.join(output_dir, 'reference.txt') 72 | 73 | # 学習時に出力した設定ファイル 74 | config_file = os.path.join(model_dir, 'config.json') 75 | 76 | # ミニバッチに含める発話数 77 | batch_size = 10 78 | 79 | # 80 | # 設定ここまで 81 | # 82 | 83 | # 設定ファイルを読み込む 84 | with open(config_file, mode='r') as f: 85 | config = json.load(f) 86 | 87 | # 読み込んだ設定を反映する 88 | # Encoderの設定 89 | # 中間層のレイヤー数 90 | enc_num_layers = config['enc_num_layers'] 91 | # 層ごとのsub sampling設定 92 | enc_sub_sample = config['enc_sub_sample'] 93 | # RNNのタイプ(LSTM or GRU) 94 | enc_rnn_type = config['enc_rnn_type'] 95 | # 中間層の次元数 96 | enc_hidden_dim = config['enc_hidden_dim'] 97 | # Projection層の次元数 98 | enc_projection_dim = config['enc_projection_dim'] 99 | # bidirectional を用いるか(Trueなら用いる) 100 | enc_bidirectional = config['enc_bidirectional'] 101 | 102 | # Attention, Decoderの設定 103 | # RNN層のレイヤー数 104 | dec_num_layers = config['dec_num_layers'] 105 | # RNN層の次元数 106 | dec_hidden_dim = config['dec_hidden_dim'] 107 | # Attentionの次元 108 | att_hidden_dim = config['att_hidden_dim'] 109 | # LocationAwareAttentionにおけるフィルタサイズ 110 | att_filter_size = config['att_filter_size'] 111 | # LocationAwareAttentionにおけるフィルタ数 112 | att_filter_num = config['att_filter_num'] 113 | # LocationAwareAttentionにおけるtemperature 114 | att_temperature = config['att_temperature'] 115 | 116 | # 出力ディレクトリが存在しない場合は作成する 117 | os.makedirs(output_dir, exist_ok=True) 118 | 119 | # 特徴量の平均/標準偏差ファイルを読み込む 120 | with open(mean_std_file, mode='r') as f: 121 | # 全行読み込み 122 | lines = f.readlines() 123 | # 1行目(0始まり)が平均値ベクトル(mean), 124 | # 3行目が標準偏差ベクトル(std) 125 | mean_line = lines[1] 126 | std_line = lines[3] 127 | # スペース区切りのリストに変換 128 | feat_mean = mean_line.split() 129 | feat_std = std_line.split() 130 | # numpy arrayに変換 131 | feat_mean = np.array(feat_mean, 132 | dtype=np.float32) 133 | feat_std = np.array(feat_std, 134 | dtype=np.float32) 135 | 136 | # 次元数の情報を得る 137 | feat_dim = np.size(feat_mean) 138 | 139 | # トークンリストをdictionary型で読み込む 140 | # このとき,0番目は blank と定義する 141 | # (ただし,このプログラムではblankは使われない) 142 | token_list = {0: ''} 143 | with open(token_list_path, mode='r') as f: 144 | # 1行ずつ読み込む 145 | for line in f: 146 | # 読み込んだ行をスペースで区切り, 147 | # リスト型の変数にする 148 | parts = line.split() 149 | # 0番目の要素がトークン,1番目の要素がID 150 | token_list[int(parts[1])] = parts[0] 151 | 152 | # トークンをユニットリストの末尾に追加 153 | eos_id = len(token_list) 154 | token_list[eos_id] = '' 155 | # 本プログラムでは、を 156 | # 同じトークンとして扱う 157 | sos_id = eos_id 158 | 159 | # トークン数(blankを含む) 160 | num_tokens = len(token_list) 161 | 162 | # ニューラルネットワークモデルを作成する 163 | # 入力の次元数は特徴量の次元数, 164 | # 出力の次元数はトークン数となる 165 | model = MyE2EModel(dim_in=feat_dim, 166 | dim_enc_hid=enc_hidden_dim, 167 | dim_enc_proj=enc_projection_dim, 168 | dim_dec_hid=dec_hidden_dim, 169 | dim_out=num_tokens, 170 | dim_att=att_hidden_dim, 171 | att_filter_size=att_filter_size, 172 | att_filter_num=att_filter_num, 173 | sos_id=sos_id, 174 | att_temperature=att_temperature, 175 | enc_num_layers=enc_num_layers, 176 | dec_num_layers=dec_num_layers, 177 | enc_bidirectional=enc_bidirectional, 178 | enc_sub_sample=enc_sub_sample, 179 | enc_rnn_type=enc_rnn_type) 180 | 181 | # モデルのパラメータを読み込む 182 | model.load_state_dict(torch.load(model_file)) 183 | 184 | # 訓練/開発データのデータセットを作成する 185 | test_dataset = SequenceDataset(feat_scp_test, 186 | label_test, 187 | feat_mean, 188 | feat_std) 189 | 190 | # 評価データのDataLoaderを呼び出す 191 | test_loader = DataLoader(test_dataset, 192 | batch_size=batch_size, 193 | shuffle=False, 194 | num_workers=4) 195 | 196 | # CUDAが使える場合はモデルパラメータをGPUに, 197 | # そうでなければCPUに配置する 198 | if torch.cuda.is_available(): 199 | device = torch.device('cuda') 200 | else: 201 | device = torch.device('cpu') 202 | model = model.to(device) 203 | 204 | # モデルを評価モードに設定する 205 | model.eval() 206 | 207 | # デコード結果および正解ラベルをファイルに書き込みながら 208 | # 以下の処理を行う 209 | with open(hypothesis_file, mode='w') as hyp_file, \ 210 | open(reference_file, mode='w') as ref_file: 211 | # 評価データのDataLoaderから1ミニバッチ 212 | # ずつ取り出して処理する. 213 | # これを全ミニバッチ処理が終わるまで繰り返す. 214 | # ミニバッチに含まれるデータは, 215 | # 音声特徴量,ラベル,フレーム数, 216 | # ラベル長,発話ID 217 | for (features, labels, feat_lens, 218 | label_lens, utt_ids) in test_loader: 219 | 220 | # PackedSequence の仕様上, 221 | # ミニバッチがフレーム長の降順で 222 | # ソートされている必要があるため, 223 | # ソートを実行する 224 | sorted_lens, indices = \ 225 | torch.sort(feat_lens.view(-1), 226 | dim=0, 227 | descending=True) 228 | features = features[indices] 229 | feat_lens = sorted_lens 230 | 231 | # CUDAが使える場合はデータをGPUに, 232 | # そうでなければCPUに配置する 233 | features = features.to(device) 234 | 235 | # モデルの出力を計算(フォワード処理) 236 | outputs, out_lens = model(features, feat_lens) 237 | 238 | # バッチ内の1発話ごとに以下の処理を行う 239 | for n in range(outputs.size(0)): 240 | # 出力はフレーム長でソートされている 241 | # 元のデータ並びに戻すため, 242 | # 対応する要素番号を取得する 243 | idx = torch.nonzero(indices==n, 244 | as_tuple=False).view(-1)[0] 245 | 246 | # 各ステップのデコーダ出力を得る 247 | _, hyp_per_step = torch.max(outputs[idx], 1) 248 | # numpy.array型に変換 249 | hyp_per_step = hyp_per_step.cpu().numpy() 250 | # 認識結果の文字列を取得 251 | hypothesis = [] 252 | for m in hyp_per_step[:out_lens[idx]]: 253 | if m == eos_id: 254 | break 255 | hypothesis.append(token_list[m]) 256 | 257 | # 正解の文字列を取得 258 | reference = [] 259 | for m in labels[n][:label_lens[n]].cpu().numpy(): 260 | reference.append(token_list[m]) 261 | 262 | # 結果を書き込む 263 | # (' '.join() は,リスト形式のデータを 264 | # スペース区切りで文字列に変換している) 265 | hyp_file.write('%s %s\n' \ 266 | % (utt_ids[n], ' '.join(hypothesis))) 267 | ref_file.write('%s %s\n' \ 268 | % (utt_ids[n], ' '.join(reference))) 269 | 270 | -------------------------------------------------------------------------------- /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/attention.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Attention (Location aware attention) の実装です. 5 | # 参考文献 6 | # - D. Bahdanau, et al., 7 | # ``End-to-end attention-based large vocabulary speech 8 | # recognition,'' 9 | # in Proc. ICASSP, 2016. 10 | # - J. Chorowski, et al., 11 | # ``Attention-based models for speech recognition,'' 12 | # in Proc. NIPS , 2015. 13 | # 14 | 15 | # Pytorchを用いた処理に必要なモジュールをインポート 16 | import torch 17 | import torch.nn as nn 18 | import torch.nn.functional as F 19 | 20 | 21 | class LocationAwareAttention(nn.Module): 22 | ''' Location aware attention 23 | dim_encoder: エンコーダRNN出力の次元数 24 | dim_decoder: デコーダRNN出力の次元数 25 | dim_attention: Attention機構の次元数 26 | filter_size: location filter (前のAttention重みに 27 | 畳み込まれるフィルタ)のサイズ 28 | filter_num: location filterの数 29 | temperature: Attention重み計算時に用いる温度パラメータ 30 | ''' 31 | def __init__(self, 32 | dim_encoder, 33 | dim_decoder, 34 | dim_attention, 35 | filter_size, 36 | filter_num, 37 | temperature=1.0): 38 | 39 | super(LocationAwareAttention, self).__init__() 40 | 41 | # F: 前のAttention重みに畳み込まれる畳み込み層 42 | self.loc_conv = nn.Conv1d(in_channels=1, 43 | out_channels=filter_num, 44 | kernel_size=2*filter_size+1, 45 | stride=1, 46 | padding=filter_size, 47 | bias=False) 48 | # 以下三つの層のうち,一つのみbiasをTrueにし,他はFalseにする 49 | # W: 前ステップのデコーダRNN出力にかかる射影層 50 | self.dec_proj = nn.Linear(in_features=dim_decoder, 51 | out_features=dim_attention, 52 | bias=False) 53 | # V: エンコーダRNN出力にかかる射影層 54 | self.enc_proj = nn.Linear(in_features=dim_encoder, 55 | out_features=dim_attention, 56 | bias=False) 57 | # U: 畳み込み後のAttention重みにかかる射影層 58 | self.att_proj = nn.Linear(in_features=filter_num, 59 | out_features=dim_attention, 60 | bias=True) 61 | # w: Ws + Vh + Uf + b にかかる線形層 62 | self.out = nn.Linear(in_features=dim_attention, 63 | out_features=1) 64 | 65 | # 各次元数 66 | self.dim_encoder = dim_encoder 67 | self.dim_decoder = dim_decoder 68 | self.dim_attention = dim_attention 69 | 70 | # 温度パラメータ 71 | self.temperature = temperature 72 | 73 | # エンコーダRNN出力(h)とその射影(Vh) 74 | # これらは毎デコードステップで同じ値のため, 75 | # 一回のみ計算し,計算結果を保持しておく 76 | self.input_enc = None 77 | self.projected_enc = None 78 | # エンコーダRNN出力の,発話ごとの系列長 79 | self.enc_lengths = None 80 | # エンコーダRNN出力の最大系列長 81 | # (=ゼロ詰めしたエンコーダRNN出力の系列長) 82 | self.max_enc_length = None 83 | # Attentionマスク 84 | # エンコーダの系列長以降 85 | # (ゼロ詰めされている部分)の重みをゼロにするマスク 86 | self.mask = None 87 | 88 | 89 | def reset(self): 90 | ''' 内部パラメータのリセット 91 | この関数は1バッチの処理を行うたびに, 92 | 最初に呼び出す必要がある 93 | ''' 94 | self.input_enc = None 95 | self.projected_enc = None 96 | self.enc_lengths = None 97 | self.max_enc_length = None 98 | self.mask = None 99 | 100 | 101 | def forward(self, 102 | input_enc, 103 | enc_lengths, 104 | input_dec=None, 105 | prev_att=None): 106 | ''' ネットワーク計算(forward処理)の関数 107 | input_enc: エンコーダRNNの出力 [B x Tenc x Denc] 108 | enc_lengths: バッチ内の各発話のエンコーダRNN出力の系列長 [B] 109 | input_dec: 前ステップにおけるデコーダRNNの出力 [B x Ddec] 110 | prev_att: 前ステップにおけるAttention重み [B x Tenc] 111 | []の中はテンソルのサイズ 112 | B: ミニバッチ内の発話数(ミニバッチサイズ) 113 | Tenc: エンコーダRNN出力の系列長(ゼロ埋め部分含む) 114 | Denc: エンコーダRNN出力の次元数(dim_encoder) 115 | Ddec: デコーダRNN出力の次元数(dim_decoder) 116 | ''' 117 | # バッチサイズ(発話数)を得る 118 | batch_size = input_enc.size()[0] 119 | 120 | # 121 | # エンコーダRNN出力とその射影ベクトルを一度のみ計算 122 | # 123 | if self.input_enc is None: 124 | # エンコーダRNN出力(h) 125 | self.input_enc = input_enc 126 | # 各発話の系列長 127 | self.enc_lengths = enc_lengths 128 | # 最大系列長 129 | self.max_enc_length = input_enc.size()[1] 130 | # 射影を行う(Vhの計算) 131 | self.projected_enc = self.enc_proj(self.input_enc) 132 | 133 | # 134 | # 前ステップにおけるデコーダRNN出力を射影する(Wsの計算) 135 | # 136 | # 前のデコーダRNN出力が無い場合は初期値としてゼロ行列を使用 137 | if input_dec is None: 138 | input_dec = torch.zeros(batch_size, self.dim_decoder) 139 | # 作成したテンソルをエンコーダRNN出力と 140 | # 同じデバイス(GPU/CPU)に配置 141 | input_dec = input_dec.to(device=self.input_enc.device, 142 | dtype=self.input_enc.dtype) 143 | # 前のデコーダRNN出力を射影する 144 | projected_dec = self.dec_proj(input_dec) 145 | 146 | # 147 | # 前ステップにおけるのAttention重み情報を 148 | # 射影する(Uf+bの計算) 149 | # 150 | # Attentionマスクを作成 151 | if self.mask is None: 152 | self.mask = torch.zeros(batch_size, 153 | self.max_enc_length, 154 | dtype=torch.bool) 155 | # バッチ内の各発話について,その発話の 156 | # 系列長以上の要素(つまりゼロ埋めされている部分)を 157 | # 1(=マスキング対象)にする 158 | for i, length in enumerate(self.enc_lengths): 159 | length = length.item() 160 | self.mask[i, length:] = 1 161 | # 作成したテンソルをエンコーダRNN出力と 162 | # 同じデバイス(GPU/CPU)に配置 163 | self.mask = self.mask.to(device=self.input_enc.device) 164 | 165 | # 前のAttention重みが無い場合は初期値として, 166 | # 一様の重みを与える 167 | if prev_att is None: 168 | # 全ての要素を1のテンソルを作成 169 | prev_att = torch.ones(batch_size, self.max_enc_length) 170 | # 発話毎の系列長で割る 171 | # このとき,prev_attは2次のテンソル, 172 | # enc_lengthsは1次のテンソルなので, 173 | # view(batch_size, 1)によりenc_lengthsを 174 | # 2次テンソルの形にしてから割り算する 175 | prev_att = prev_att \ 176 | / self.enc_lengths.view(batch_size, 1) 177 | # 作成したテンソルをエンコーダRNN出力と 178 | # 同じデバイス(GPU/CPU)に配置 179 | prev_att = prev_att.to(device=self.input_enc.device, 180 | dtype=self.input_enc.dtype) 181 | # 発話長以降の重みをゼロにするようマスキングを実行 182 | prev_att.masked_fill_(self.mask, 0) 183 | 184 | # Attention重みの畳み込みを計算する {f} = F*a 185 | # このとき,Conv1Dが受け付ける入力のサイズは 186 | # (batch_size, in_channels, self.max_enc_length) 187 | # (in_channelsは入力のチャネル数で, 188 | # このプログラムではin_channels=1) 189 | # サイズを合わせるため,viewを行う 190 | convolved_att \ 191 | = self.loc_conv(prev_att.view(batch_size, 192 | 1, self.max_enc_length)) 193 | 194 | # convolved_attのサイズは 195 | # (batch_size, filter_num, self.max_enc_length) 196 | # Linearレイヤーが受け付ける入力のサイズは 197 | # (batch_size, self.max_enc_length, filter_num) なので, 198 | # transposeにより1次元目と2次元目をの入れ替えた上で 199 | # att_projに通す 200 | projected_att = self.att_proj(convolved_att.transpose(1, 2)) 201 | 202 | # 203 | # Attention重みを計算する 204 | # 205 | # この時点での各テンソルのサイズは 206 | # self.projected_enc: (batch_size, self.max_enc_length, 207 | # self.dim_attention) 208 | # projected_dec: (batch_size, self.dim_attention) 209 | # projected_att: (batch_size, self.max_enc_length, self.dim_attention) 210 | # projected_decのテンソルの次元数を合わせるため,viewを用いる 211 | projected_dec = projected_dec.view(batch_size, 212 | 1, 213 | self.dim_attention) 214 | 215 | # scoreを計算するため,各射影テンソルの加算, 216 | # tanh,さらに射影を実施 217 | # w tanh(Ws + Vh + Uf + b) 218 | score = self.out(torch.tanh(projected_dec \ 219 | + self.projected_enc 220 | + projected_att)) 221 | 222 | # 現時点のscoreのテンソルサイズは 223 | # (batch_size, self.max_enc_length, 1) 224 | # viewを用いて元々のattentionのサイズに戻す 225 | score = score.view(batch_size, self.max_enc_length) 226 | 227 | # マスキングを行う 228 | # (エンコーダRNN出力でゼロ埋めされている部分の 229 | # 重みをゼロにする) 230 | # ただし,この後softmax関数の中で計算される 231 | # exp(score)がゼロになるように 232 | # しないといけないので,scoreの段階では0ではなく, 233 | # 0の対数値である-infで埋めておく 234 | score.masked_fill_(self.mask, -float('inf')) 235 | 236 | # 温度付きSoftmaxを計算することで,Attention重みを得る 237 | att_weight = F.softmax(self.temperature * score, dim=1) 238 | 239 | # att_weightを使って,エンコーダRNN出力の重みづけ和を計算し, 240 | # contextベクトルを得る 241 | # (viewによりinput_encとattention_weightの 242 | # テンソルサイズを合わせている) 243 | context \ 244 | = torch.sum(self.input_enc * \ 245 | att_weight.view(batch_size, self.max_enc_length, 1), 246 | dim=1) 247 | 248 | # contextベクトルとattention重みを出力 249 | return context, att_weight 250 | 251 | -------------------------------------------------------------------------------- /06rnn_attention/decoder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Attention RNN のデコーダ部の実装です. 5 | # 参考文献 6 | # - D. Bahdanau, et al., 7 | # ``End-to-end attention-based large vocabulary speech 8 | # recognition,'' 9 | # in Proc. ICASSP, 2016. 10 | # - J. Chorowski, et al., 11 | # ``Attention-based models for speech recognition,'' 12 | # in Proc. NIPS , 2015. 13 | # 14 | 15 | # Pytorchを用いた処理に必要なモジュールをインポート 16 | import torch 17 | import torch.nn as nn 18 | 19 | # 作成したattentionモジュールをインポート 20 | from attention import LocationAwareAttention 21 | 22 | # 数値演算用モジュール(numpy)をインポート 23 | import numpy as np 24 | 25 | # プロット用モジュール(matplotlib)をインポート 26 | import matplotlib.pyplot as plt 27 | 28 | 29 | class Decoder(nn.Module): 30 | ''' デコーダ 31 | dim_in: 入力系列(=エンコーダ出力)の次元数 32 | dim_hidden: デコーダRNNの次元数 33 | dim_out: 出力の次元数(sosとeosを含む全トークン数) 34 | dim_att: Attention機構の次元数 35 | att_filter_size: LocationAwareAttentionのフィルタサイズ 36 | att_filter_num: LocationAwareAttentionのフィルタ数 37 | sos_id: トークンの番号 38 | att_temperature: Attentionの温度パラメータ 39 | num_layers: デコーダRNNの層の数 40 | ''' 41 | def __init__(self, 42 | dim_in, 43 | dim_hidden, 44 | dim_out, 45 | dim_att, 46 | att_filter_size, 47 | att_filter_num, 48 | sos_id, 49 | att_temperature=1.0, 50 | num_layers=1): 51 | 52 | super(Decoder, self).__init__() 53 | 54 | # と番号を設定 55 | self.sos_id = sos_id 56 | 57 | # 入力次元数と出力次元数 58 | self.dim_in = dim_in 59 | self.dim_out = dim_out 60 | 61 | # 1ステップ前の出力を入力するembedding層 62 | # (次元数がdim_outのベクトルから次元数が 63 | # dim_hiddenのベクトルに変換する) 64 | self.embedding = nn.Embedding(dim_out, dim_hidden) 65 | 66 | # Location aware attention 67 | self.attention = LocationAwareAttention(dim_in, 68 | dim_hidden, 69 | dim_att, 70 | att_filter_size, 71 | att_filter_num, 72 | att_temperature) 73 | 74 | # RNN層 75 | # RNNには1ステップ前の出力(Embedding後)と 76 | # エンコーダ出力(Attention後)が入力される. 77 | # よってRNNの入力次元数は 78 | # dim_hidden(Embedding出力の次元数) \ 79 | # + dim_in(エンコーダ出力の次元数) 80 | self.rnn = nn.LSTM(input_size=dim_hidden+dim_in, 81 | hidden_size=dim_hidden, 82 | num_layers=num_layers, 83 | bidirectional=False, 84 | batch_first=True) 85 | 86 | # 出力層 87 | self.out = nn.Linear(in_features=dim_hidden, 88 | out_features=dim_out) 89 | 90 | # Attention重み行列(表示用) 91 | self.att_matrix = None 92 | 93 | 94 | def forward(self, enc_sequence, enc_lengths, label_sequence=None): 95 | ''' ネットワーク計算(forward処理)の関数 96 | enc_sequence: 各発話のエンコーダ出力系列 97 | [B x Tenc x Denc] 98 | enc_lengths: 各発話のエンコーダRNN出力の系列長 [B] 99 | label_sequence: 各発話の正解ラベル系列(学習時に用いる) 100 | [B x Tout] 101 | []の中はテンソルのサイズ 102 | B: ミニバッチ内の発話数(ミニバッチサイズ) 103 | Tenc: エンコーダRNN出力の系列長(ゼロ埋め部分含む) 104 | Denc: エンコーダRNN出力の次元数(dim_in) 105 | Tout: 正解ラベル系列の系列長(ゼロ埋め部分含む) 106 | label_sequenceは,学習時にのみ与える 107 | ''' 108 | # 入力の情報(バッチサイズ,device(cpu or cuda))を得る 109 | batch_size = enc_sequence.size()[0] 110 | device = enc_sequence.device 111 | 112 | # 113 | # デコーダの最大ステップ数を決める 114 | # 115 | if label_sequence is not None: 116 | # 学習時: 117 | # = ラベル情報が与えられている場合は 118 | # ラベル系列長を使う 119 | max_step = label_sequence.size()[1] 120 | else: 121 | # 評価時: 122 | # = ラベル情報が与えられていない場合は 123 | # エンコーダ出力系列長を使う 124 | max_step = enc_sequence.size()[1] 125 | 126 | # 127 | # 各内部パラメータの初期化 128 | # 129 | # 1ステップ前のトークン.初期値は とする 130 | prev_token = torch.ones(batch_size, 1, 131 | dtype=torch.long) * self.sos_id 132 | # デバイス(CPU/GPU)に配置 133 | prev_token = prev_token.to(device=device, 134 | dtype=torch.long) 135 | # 1ステップ前のRNN出力とAttention重みをNoneで初期化する 136 | prev_rnnout = None 137 | prev_att = None 138 | # 1ステップ前のRNN内部パラメータ(h, c)もNoneで初期化する 139 | prev_h_c = None 140 | # Attentionの内部パラメータをリセットする 141 | self.attention.reset() 142 | 143 | # 出力テンソルを用意 [batch_size x max_step x dim_out] 144 | output = torch.zeros(batch_size, max_step, self.dim_out) 145 | # デバイス(CPU/GPU)に配置 146 | output = output.to(device=device, dtype=enc_sequence.dtype) 147 | 148 | # 表示用Attention重み行列の初期化 149 | self.att_matrix = torch.zeros(batch_size, 150 | max_step, 151 | enc_sequence.size(1)) 152 | 153 | # 154 | # 最大ステップの数だけデコーダを動かす 155 | # 156 | for i in range(max_step): 157 | # 158 | # 1. Attentionを計算し,コンテキストベクトル 159 | # (重みづけ和されたエンコーダ出力)と, 160 | # Attention重みを得る 161 | # 162 | context, att_weight = self.attention(enc_sequence, 163 | enc_lengths, 164 | prev_rnnout, 165 | prev_att) 166 | # 167 | # 2. RNNを1ステップ分動かす 168 | # 169 | # 1ステップ前のトークンをEmbedding層に通す 170 | prev_token_emb = self.embedding(prev_token) 171 | # prev_token_embとコンテキストベクトルを結合し, 172 | # RNNに入力する.RNN入力のテンソルサイズは 173 | # (batch_size, 系列長(=1), dim_in)なので, 174 | # contextにviewを用いてサイズを合わせた上で結合する 175 | context = context.view(batch_size, 1, self.dim_in) 176 | rnn_input = torch.cat((prev_token_emb, context), dim=2) 177 | # RNNに通す 178 | rnnout, h_c = self.rnn(rnn_input, prev_h_c) 179 | 180 | # 181 | # 3. RNN出力を線形層に通す 182 | # 183 | out = self.out(rnnout) 184 | # 出力テンソルにoutを格納 185 | output[:,i,:] = out.view(batch_size, self.dim_out) 186 | 187 | # 188 | # 4. 1ステップ前のRNN出力とRNN内部パラメータ, 189 | # Attention重み,トークンを更新する. 190 | # 191 | prev_rnnout = rnnout 192 | prev_h_c = h_c 193 | prev_att = att_weight 194 | # トークンの更新 195 | if label_sequence is not None: 196 | # 学習時: 197 | # = 正解ラベルが与えられている場合はそれを用いる 198 | prev_token = label_sequence[:,i].view(batch_size,1) 199 | else: 200 | # 評価時: 201 | # = 正解ラベルが与えられていない場合は, 202 | # 予測値を用いる 203 | _, prev_token = torch.max(out, 2) 204 | 205 | # 表示用Attention重み行列 206 | self.att_matrix[:,i,:] = att_weight 207 | 208 | return output 209 | 210 | def save_att_matrix(self, utt, filename): 211 | ''' Attention行列を画像にして保存する 212 | utt: 出力する、バッチ内の発話番号 213 | filename: 出力ファイル名 214 | ''' 215 | att_mat = self.att_matrix[utt].detach().numpy() 216 | # プロットの描画領域を作成 217 | plt.figure(figsize=(5,5)) 218 | # カラーマップのレンジを調整 219 | att_mat -= np.max(att_mat) 220 | vmax = np.abs(np.min(att_mat)) * 0.0 221 | vmin = - np.abs(np.min(att_mat)) * 0.7 222 | # プロット 223 | plt.imshow(att_mat, 224 | cmap = 'gray', 225 | vmax = vmax, 226 | vmin = vmin, 227 | aspect = 'auto') 228 | # 横軸と縦軸のラベルを定義 229 | plt.xlabel('Encoder index') 230 | plt.ylabel('Decoder index') 231 | 232 | # プロットを保存する 233 | plt.savefig(filename) 234 | plt.close() 235 | 236 | -------------------------------------------------------------------------------- /06rnn_attention/encoder.py: -------------------------------------------------------------------------------- 1 | ../05ctc/encoder.py -------------------------------------------------------------------------------- /06rnn_attention/initialize.py: -------------------------------------------------------------------------------- 1 | ../05ctc/initialize.py -------------------------------------------------------------------------------- /06rnn_attention/levenshtein.py: -------------------------------------------------------------------------------- 1 | ../05ctc/levenshtein.py -------------------------------------------------------------------------------- /06rnn_attention/my_dataset.py: -------------------------------------------------------------------------------- 1 | ../05ctc/my_dataset.py -------------------------------------------------------------------------------- /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/03_decode_ctc_att_mtl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # RNN Attention Encoder-Decoderによるデコーディングを行います 5 | # 6 | 7 | # Pytorchを用いた処理に必要なモジュールをインポート 8 | import torch 9 | from torch.utils.data import DataLoader 10 | 11 | # 作成したDatasetクラスをインポート 12 | from my_dataset import SequenceDataset 13 | 14 | # 数値演算用モジュール(numpy)をインポート 15 | import numpy as np 16 | 17 | # モデルの定義をインポート 18 | from my_model import MyMTLModel 19 | 20 | # json形式の入出力を行うモジュールをインポート 21 | import json 22 | 23 | # os, sysモジュールをインポート 24 | import os 25 | import sys 26 | 27 | 28 | # 29 | # メイン関数 30 | # 31 | if __name__ == "__main__": 32 | 33 | # 34 | # 設定ここから 35 | # 36 | 37 | # トークンの単位 38 | # phone:音素 kana:かな char:キャラクター 39 | unit = 'phone' 40 | 41 | # Multi Task TrainingのCTC損失関数重み 42 | mtl_weight = 0.5 43 | 44 | # 実験ディレクトリ 45 | exp_dir = './exp_train_small' 46 | 47 | # 評価データの特徴量(feats.scp)が存在するディレクトリ 48 | feat_dir_test = '../01compute_features/fbank/test' 49 | 50 | # 評価データの特徴量リストファイル 51 | feat_scp_test = os.path.join(feat_dir_test, 'feats.scp') 52 | 53 | # 評価データのラベルファイル 54 | label_test = os.path.join(exp_dir, 'data', unit, 'label_test') 55 | 56 | # トークンリスト 57 | token_list_path = os.path.join(exp_dir, 'data', unit, 58 | 'token_list') 59 | 60 | # 学習済みモデルが格納されているディレクトリ 61 | model_dir = os.path.join(exp_dir, 62 | unit+'_model_attention_mtl'\ 63 | +str(mtl_weight)) 64 | 65 | # 訓練データから計算された特徴量の平均/標準偏差ファイル 66 | mean_std_file = os.path.join(model_dir, 'mean_std.txt') 67 | 68 | # 学習済みのモデルファイル 69 | model_file = os.path.join(model_dir, 'best_model.pt') 70 | 71 | # デコード結果を出力するディレクトリ 72 | output_dir = os.path.join(model_dir, 'decode_test') 73 | 74 | # デコード結果および正解文の出力ファイル 75 | hypothesis_file = os.path.join(output_dir, 'hypothesis.txt') 76 | reference_file = os.path.join(output_dir, 'reference.txt') 77 | 78 | # 学習時に出力した設定ファイル 79 | config_file = os.path.join(model_dir, 'config.json') 80 | 81 | # ミニバッチに含める発話数 82 | batch_size = 10 83 | 84 | # 85 | # 設定ここまで 86 | # 87 | 88 | # 設定ファイルを読み込む 89 | with open(config_file, mode='r') as f: 90 | config = json.load(f) 91 | 92 | # 読み込んだ設定を反映する 93 | # Encoderの設定 94 | # 中間層のレイヤー数 95 | enc_num_layers = config['enc_num_layers'] 96 | # 層ごとのsub sampling設定 97 | enc_sub_sample = config['enc_sub_sample'] 98 | # RNNのタイプ(LSTM or GRU) 99 | enc_rnn_type = config['enc_rnn_type'] 100 | # 中間層の次元数 101 | enc_hidden_dim = config['enc_hidden_dim'] 102 | # Projection層の次元数 103 | enc_projection_dim = config['enc_projection_dim'] 104 | # bidirectional を用いるか(Trueなら用いる) 105 | enc_bidirectional = config['enc_bidirectional'] 106 | 107 | # Attention, Decoderの設定 108 | # RNN層のレイヤー数 109 | dec_num_layers = config['dec_num_layers'] 110 | # RNN層の次元数 111 | dec_hidden_dim = config['dec_hidden_dim'] 112 | # Attentionの次元 113 | att_hidden_dim = config['att_hidden_dim'] 114 | # LocationAwareAttentionにおけるフィルタサイズ 115 | att_filter_size = config['att_filter_size'] 116 | # LocationAwareAttentionにおけるフィルタ数 117 | att_filter_num = config['att_filter_num'] 118 | # LocationAwareAttentionにおけるtemperature 119 | att_temperature = config['att_temperature'] 120 | 121 | # 出力ディレクトリが存在しない場合は作成する 122 | os.makedirs(output_dir, exist_ok=True) 123 | 124 | # 特徴量の平均/標準偏差ファイルを読み込む 125 | with open(mean_std_file, mode='r') as f: 126 | # 全行読み込み 127 | lines = f.readlines() 128 | # 1行目(0始まり)が平均値ベクトル(mean), 129 | # 3行目が標準偏差ベクトル(std) 130 | mean_line = lines[1] 131 | std_line = lines[3] 132 | # スペース区切りのリストに変換 133 | feat_mean = mean_line.split() 134 | feat_std = std_line.split() 135 | # numpy arrayに変換 136 | feat_mean = np.array(feat_mean, 137 | dtype=np.float32) 138 | feat_std = np.array(feat_std, 139 | dtype=np.float32) 140 | 141 | # 次元数の情報を得る 142 | feat_dim = np.size(feat_mean) 143 | 144 | # トークンリストをdictionary型で読み込む 145 | # このとき,0番目は blank と定義する 146 | # (ただし,このプログラムではblankは使われない) 147 | token_list = {0: ''} 148 | with open(token_list_path, mode='r') as f: 149 | # 1行ずつ読み込む 150 | for line in f: 151 | # 読み込んだ行をスペースで区切り, 152 | # リスト型の変数にする 153 | parts = line.split() 154 | # 0番目の要素がトークン,1番目の要素がID 155 | token_list[int(parts[1])] = parts[0] 156 | 157 | # トークンをユニットリストの末尾に追加 158 | eos_id = len(token_list) 159 | token_list[eos_id] = '' 160 | # 本プログラムでは、を 161 | # 同じトークンとして扱う 162 | sos_id = eos_id 163 | 164 | # トークン数(blankを含む) 165 | num_tokens = len(token_list) 166 | 167 | # ニューラルネットワークモデルを作成する 168 | # 入力の次元数は特徴量の次元数, 169 | # 出力の次元数はトークン数となる 170 | model = MyMTLModel(dim_in=feat_dim, 171 | dim_enc_hid=enc_hidden_dim, 172 | dim_enc_proj=enc_projection_dim, 173 | dim_dec_hid=dec_hidden_dim, 174 | dim_out=num_tokens, 175 | dim_att=att_hidden_dim, 176 | att_filter_size=att_filter_size, 177 | att_filter_num=att_filter_num, 178 | sos_id=sos_id, 179 | att_temperature=att_temperature, 180 | enc_num_layers=enc_num_layers, 181 | dec_num_layers=dec_num_layers, 182 | enc_bidirectional=enc_bidirectional, 183 | enc_sub_sample=enc_sub_sample, 184 | enc_rnn_type=enc_rnn_type) 185 | 186 | # モデルのパラメータを読み込む 187 | model.load_state_dict(torch.load(model_file)) 188 | 189 | # 訓練/開発データのデータセットを作成する 190 | test_dataset = SequenceDataset(feat_scp_test, 191 | label_test, 192 | feat_mean, 193 | feat_std) 194 | 195 | # 評価データのDataLoaderを呼び出す 196 | test_loader = DataLoader(test_dataset, 197 | batch_size=batch_size, 198 | shuffle=False, 199 | num_workers=4) 200 | 201 | # CUDAが使える場合はモデルパラメータをGPUに, 202 | # そうでなければCPUに配置する 203 | if torch.cuda.is_available(): 204 | device = torch.device('cuda') 205 | else: 206 | device = torch.device('cpu') 207 | model = model.to(device) 208 | 209 | # モデルを評価モードに設定する 210 | model.eval() 211 | 212 | # デコード結果および正解ラベルをファイルに書き込みながら 213 | # 以下の処理を行う 214 | with open(hypothesis_file, mode='w') as hyp_file, \ 215 | open(reference_file, mode='w') as ref_file: 216 | # 評価データのDataLoaderから1ミニバッチ 217 | # ずつ取り出して処理する. 218 | # これを全ミニバッチ処理が終わるまで繰り返す. 219 | # ミニバッチに含まれるデータは, 220 | # 音声特徴量,ラベル,フレーム数, 221 | # ラベル長,発話ID 222 | for (features, labels, feat_lens, 223 | label_lens, utt_ids) in test_loader: 224 | 225 | # PackedSequence の仕様上, 226 | # ミニバッチがフレーム長の降順で 227 | # ソートされている必要があるため, 228 | # ソートを実行する 229 | sorted_lens, indices = \ 230 | torch.sort(feat_lens.view(-1), 231 | dim=0, 232 | descending=True) 233 | features = features[indices] 234 | feat_lens = sorted_lens 235 | 236 | # CUDAが使える場合はデータをGPUに, 237 | # そうでなければCPUに配置する 238 | features = features.to(device) 239 | 240 | # モデルの出力を計算(フォワード処理) 241 | # enc_lensは処理後のctc_outのフレーム数 242 | outputs, ctc_out, enc_lens = model(features, 243 | feat_lens) 244 | 245 | # バッチ内の1発話ごとに以下の処理を行う 246 | for n in range(outputs.size(0)): 247 | # 出力はフレーム長でソートされている 248 | # 元のデータ並びに戻すため, 249 | # 対応する要素番号を取得する 250 | idx = torch.nonzero(indices==n, 251 | as_tuple=False).view(-1)[0] 252 | 253 | # 各ステップのデコーダ出力を得る 254 | _, hyp_per_step = torch.max(outputs[idx], 1) 255 | # numpy.array型に変換 256 | hyp_per_step = hyp_per_step.cpu().numpy() 257 | # 認識結果の文字列を取得 258 | hypothesis = [] 259 | for m in hyp_per_step[:enc_lens[idx]]: 260 | if m == eos_id: 261 | break 262 | hypothesis.append(token_list[m]) 263 | 264 | # 正解の文字列を取得 265 | reference = [] 266 | for m in labels[n][:label_lens[n]].cpu().numpy(): 267 | reference.append(token_list[m]) 268 | 269 | # 結果を書き込む 270 | # (' '.join() は,リスト形式のデータを 271 | # スペース区切りで文字列に変換している) 272 | hyp_file.write('%s %s\n' \ 273 | % (utt_ids[n], ' '.join(hypothesis))) 274 | ref_file.write('%s %s\n' \ 275 | % (utt_ids[n], ' '.join(reference))) 276 | 277 | -------------------------------------------------------------------------------- /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/attention.py: -------------------------------------------------------------------------------- 1 | ../06rnn_attention/attention.py -------------------------------------------------------------------------------- /07ctc_att_mtl/decoder.py: -------------------------------------------------------------------------------- 1 | ../06rnn_attention/decoder.py -------------------------------------------------------------------------------- /07ctc_att_mtl/encoder.py: -------------------------------------------------------------------------------- 1 | ../06rnn_attention/encoder.py -------------------------------------------------------------------------------- /07ctc_att_mtl/initialize.py: -------------------------------------------------------------------------------- 1 | ../05ctc/initialize.py -------------------------------------------------------------------------------- /07ctc_att_mtl/levenshtein.py: -------------------------------------------------------------------------------- 1 | ../05ctc/levenshtein.py -------------------------------------------------------------------------------- /07ctc_att_mtl/my_dataset.py: -------------------------------------------------------------------------------- 1 | ../06rnn_attention/my_dataset.py -------------------------------------------------------------------------------- /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 | ## Pythonで学ぶ音声認識(機械学習実践シリーズ)のソースコード 2 | 本リポジトリでは、インプレス社機械学習実践シリーズの「Pythonで学ぶ音声認識」のサンプルコードを管理しています。 3 | なお、本ソースコードは、MITライセンスのもとで公開されています。 4 | 5 | ## 書籍情報 6 | - [Pythonで学ぶ音声認識(機械学習実践シリーズ)](https://book.impress.co.jp/books/1120101083) 7 | - 価格: 3,500円+税 8 | - 発売日: 2021年5月20日 9 | - ページ数: 352 10 | - サイズ: B5変形判 11 | - 著者: 高島 遼一 12 | - ISBN: 9784295011385 13 | 14 | ## 目次 15 | - 第1章 音声認識とは? 16 | - 第2章 音声認識の基礎知識 17 | - 第3章 音声処理の基礎と特徴量抽出 18 | - 第4章 音声認識の初歩─DPマッチング─ 19 | - 第5章 GMM-HMMによる音声認識 20 | - 第6章 DNN-HMMによる音声認識 21 | - 第7章 End-to-Endモデルによる連続音声認識 22 | 23 | ## ソースコード 24 | - 00prepare 25 | 音声認識に用いる音声・テキストのダウンロードやデータの整形などの準備を行います(3章) 26 | - 01compute_features 27 | 基本的な音声処理や音声認識に用いる特徴量の抽出を行います(3章) 28 | - 02dp_matching 29 | DPマッチングによる初歩的な音声認識実験を行います(4章) 30 | - 03gmm_hmm 31 | GMM-HMMによる単語音声認識実験を行います(5章) 32 | - 04dnn_hmm 33 | DNN-HMMによる単語音声認識実験を行います(6章) 34 | - 05ctc 35 | CTCによる音声認識実験を行います(7章) 36 | - 06rnn_attention 37 | Attention encoder-decoderモデルによる音声認識実験を行います(7章) 38 | - 07ctc_att_mtl 39 | (付録)CTCとAttentionモデルのマルチタスク学習を行います(7章) 40 | 41 | ## 付録 42 | [付録](appendix.pdf)では、ページの都合上で本に載せきれなかった、 43 | 多変量正規分布のパラメータ(平均値ベクトルおよび分散共分散行列)、 44 | GMM のパラメータ(混合重み、平均値ベクトル、分散共分散行列)、 45 | そしてHMM の状態遷移確率を求める式を導出しています。 46 | -------------------------------------------------------------------------------- /appendix.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ry-takashima/python_asr/2e002ab6fdb29d3f6992d7a149a8c94d69ab1047/appendix.pdf --------------------------------------------------------------------------------