今回は、フーリエ変換を使って音声の特徴を捉えるという技術を実践的に理解していきます。 よろしくお願いします!
0. はじめに
本シリーズは初心者でも音声認識の実装が理解できるように、なるべく基礎シリーズの説明の流れに沿って進めていきます。従って、実装には直接関係ないプログラムも作成し、動作確認をするなど段階的に実装に取り組みます。
記事の読み方
- プログラム・ライブラリが分からないので徐々に学びたい
→付録も参照しつつ、1から順番に読んでみてください!
→プログラムを書いて実行できるか確認してみましょう! - 分かるから実装部分だけ読みたい
→★印部分が肝です!
付録について
このシリーズは音声認識に関する記事です。テーマから逸れないように、以下の解説は付録として別の記事にまとめる予定です。
ライブラリに関する詳しい解説
プログラムに使用した変数の詳しい解説
実行環境
執筆者の実行環境です。特に今回活躍するライブラリを太字にしています。
OS:Windows7(2019年10月時点。そろそろwin10買わないとまずいですね・・・)
言語:Python(ver. 3.6.8)
<ライブラリ一覧>numpy(ver.1.17.1)
matplotlib(ver.3.1.1)
tensorflow(ver.1.14.0)
scipy(ver.1.3.1) ←New
scikit-image(0.16.2) ←New
※執筆者も手探りで実装を行っています。ライブラリが増えたり、代わりのライブラリのほうが便利な場合そちらに変更する場合があります。その場合は逐一報告します。
1. Pythonでスペクトル解析
まずはスペクトル解析に関する技術をおさらいしながら、Pythonではどのように処理が行われているのか見ていきましょう。
1.1 フーリエ変換とは?(おさらい)
プログラムのための導入として、基礎でも説明した音声波形の特性と特徴抽出についておさらいします。
さて、スペクトル解析はフーリエ変換によって時系列データを周波数データに変換する処理が必要でした。
音声認識の基礎では、上のようにイメージとしていらすとやさんの画像を使っていた部分の処理ですね。
この処理がわかるように、まずは非常に簡単な正弦波(Sin(2πf))を自分で生成して、フーリエ変換処理をしてみました。
1.2. 簡単な例でフーリエ変換を試す
いきなり実際の音声データを使ってフーリエ変換を行ってしまうと、結果が本当に正しいのか(自分のプログラムは本当に正しいのか?)がわからなくなってしまいます。
そこで、自分で作った正弦波をフーリエ変換することで、出力した結果が正しいかどうかを判断します。
フーリエ変換の変数「np.fft.fft(numpy), scipy.fftpack.fft(scipy)」
フーリエ変換について基礎シリーズで記載した通り、仕組みを知らなくてもプログラムが出来て、計算を行ってくれます。
この計算を行ってくれる変数は数値計算ライブラリである「numpy」「scipy」にあります。
したがって、Pythonでフーリエ変換の計算を命令する部分のプログラムは非常に簡単なもので済むみたいです。
フーリエ変換に関しては、今回はnumpyの提供する変数を使ってみました。
※ライブラリがいかに便利か、などの脇道に逸れた話は付録でします。
【設計】正弦波のフーリエ変換プログラム
おまたせしました。ようやくプログラムをしてみます。流れはこんな感じです。
- sin波1つだけで作られたシンプルな波形を作る
- 一度形を見てみる(グラフにプロット)
- フーリエ変換を実行
- グラフにプロット
【ソースコード】正弦波のフーリエ変換プログラム
'''簡単な正弦波を作ってnp.fft.fft()を試す''' '''必要なライブラリをインポート''' import matplotlib.pyplot as plt import numpy as np '''超簡単な正弦波を作る''' f = 10 # 周波数f(frequency),単位はHz(サイクル毎秒) f_s = 100 # サンプリングレート。1秒あたりの測定数 t = np.linspace(0, 2, 2 * f_s, endpoint = False)# 時間を0から2秒に指定しサンプリング分割を行う(終点は要素に含まない) x = np.sin(f * 2 * np.pi * t) # x=sin(2πf)の波形作成 '''グラフに表示''' fig, ax= plt.subplots() ax.plot(t, x) # 横軸にt, 縦軸にxの値をプロットする ax.set_xlabel('Time[s]') ax.set_ylabel('Signal amplitude') '''フーリエ変換''' X = np.fft.fft(x) # 波形のフーリエ変換 freqs = np.fft.fftfreq(len(x)) * f_s # サンプリング周波数を返す '''グラフに表示''' fig2, ax2= plt.subplots() ax2.stem(freqs, np.abs(X), use_line_collection=True) # フーリエ変換の結果をステム(茎)プロットする ax2.set_xlabel('Frequency in Hertz[Hz]') ax2.set_ylabel('Frequency Domain (Spectrum) Magnitude') ax2.set_xlim(- f_s / 2, f_s / 2) ax2.set_ylim(-5, 110) plt.show()
【実行結果】正弦波のフーリエ変換プログラム
さて、結果を見てみましょう。
まずは生成した正弦波のグラフです。
周波数10Hz(0.1秒で周期1)の波を生成したことを確認できました。
f
やf_s
の値を変えることで違う形の波も作って試せます。
では次にフーリエ変換を行った結果のグラフです。
結果の大きさは伝統的にステムプロットで可視化するようです。ちょうど10Hzの成分に値が出ていますね!
※ところで、正と負の成分に値が現れています。
簡単には実世界の正弦波(sin波)は正負の周波数の重ねあわせでできているからです。(深入りすると沼に落ちるので止めておきましょう・・・)
1.3. 音声認識におけるスペクトル解析実装のためのポイント
プログラムによって、入力した範囲の波形をフーリエ変換しました。
今回入力した波形はシンプルであるため、周波数成分の大きさを 茎(パルス状の波) として離散的に表示できました。
しかし、音声は非常に多くの周波数成分が合成された波形のため、音声波形をフーリエ変換した場合は上のような離散グラフではなく、連続グラフとして表示されます。
グラフでも分かるとおり、フーリエ変換後は周波数成分に関するデータとなり、時間情報がなくなっていることが分かりますね。フーリエ変換によって、データは時間情報とは無関係になったというわけです。
周波数が大きいほど高い音、小さいほど低い音であることから、この波はおそらくこの高さの音が出ているということが分かったということですね。
ところで、私達人間が会話など実際に音声を認識する場合を思い出してください。一瞬一瞬の音の高さだけでなく、前後の音と繋いで聞くことで何を言っているかを考えていますね。そうです、時間による音の変化も音声認識の技術において重要な情報なのです。
フーリエ変換で一連の単語を丸ごとフーリエ変換にかけてしまったら、時間による変化も単語の中の1音もわかったものではないですね。困りました。
そこで、機械学習による音声認識の実装には、時間情報も加わったスペクトログラムを使用した方が良い精度が見込めるでしょう。
2. スペクトログラム
スペクトログラムは、音声の周波数成分と時間成分のどちらも知ることが出来ます。これは、フーリエ変換の応用によって表現します。
2.1. スペクトログラムとは?(窓関数のおさらい)
フーリエ変換は、入力した範囲の音声波形の周波数成分を出力する処理です(音声認識技術以外では空間でも可能です)。
考える工夫として、入力する音声波形の範囲(時間)を、重複部分のある短いスライスに切り分け、それぞれにフーリエ変換を行います。
このように、ある範囲をずらしながらフーリエ変換を行うことを短時間フーリエ変換(Short Time Fourier Transform)と呼びます。
ところで、範囲の切り出しでは基礎記事で説明した「窓関数」を使います。
では、プログラムしながらどのような処理か理解していきましょう。
2.2. 短時間フーリエ変換の構築[np.fft.fftを用いる]
【設計】短時間フーリエ変換プログラム
短時間フーリエ変換のためのプログラムの流れです。
- 実際の音声波形を読み込む
- 音声波形をグラフに表示する
- 波形をスライスごとに記録する(1024個のサンプルを含むスライスを100サンプルずつずらして波形を分割する)
- 各スライスをフーリエ変換する
- 結果をグラフに表示する
- ここで、切り出すスライスのサンプル数は2の乗数である必要があります。(詳しくは付録に書きます。)
- 使用する音声データはwaveデータ(.wav)です。
(執筆者が「こんばんわ(発音重視)」と発音している3秒程度の録音音声です。)
音声データについてですが、執筆者は「Audacity」というフリーソフトウェアを使用して録音、トリミング、正規化などを行い作成しました(詳しくは付録記載)。
また、Pythonでの音声波形の取り込みの際、Windowsでプログラム構築している場合は、ファイルのパスを入力する前にr
を入力する必要があります。
プログラム環境がWindowsでない場合は入力しなくて良いみたいです。
【ソースコード】短時間フーリエ変換プログラム
'''fftを使って短時間フーリエ変換(stft)''' '''必要なライブラリをインポート''' from scipy.io import wavfile import numpy as np import matplotlib.pyplot as plt from skimage import util '''waveファイルから必要な情報を読み込む''' rate, audio = wavfile.read(r"音声データの置いてあるパス/音声ファイル名.wav", "r") # waveファイルを読み込み用ファイルとして開く audio = np.mean(audio, axis=1) # ステレオをモノラルに変換 N = audio.shape[0] # 要素数を返す L = N/rate # 長さの算出 '''一旦グラフにプロット''' f, ax = plt.subplots() ax.plot(np.arange(N)/rate, audio) ax.set_xlabel('Time[s]') ax.set_ylabel('Amplitude[unknown]') plt.show() ''' 1024(N)個のサンプル値で構成され、1つ前のスライスとサンプル100(step)個分ずれるようなスライスに分割する ''' M = 1024 # スライスのサンプル数 slices = util.view_as_windows(audio, window_shape=(M,), step=100) # スライス分割 window = np.hamming(M+1)[:-1] # 窓関数の生成 slices = slices * window # スライスに窓を掛け合わせる '''列ごとに1つのスライスがあるほうが便利なのでデータを転置する''' slices = slices.T spectrum = np.fft.fft(slices, axis=0)[:N] # スライスをフーリエ変換 spectrum = np.abs(spectrum) # スペクトラムの絶対値を取っている ''' スペクトルには、非常に大きい値と小さい値の両方が含まれるため、対数値を取って範囲を圧縮する そしてグラフにプロットする ''' f, ax = plt.subplots() S = np.abs(spectrum) # スペクトラムの絶対値を取る S = 20 * np.log10(S / np.max(S)) # スペクトラムの対数値を取る ax.imshow(S, origin='lower', cmap='viridis', extent=(0, L, 0, rate/2/1000)) ax.axis('tight') ax.set_ylabel('Frequency[kHz]') ax.set_xlabel('Time[s]') plt.show()
【実行結果】短時間フーリエ変換プログラム
では結果をみていきましょう。
まずは、音声データ「こんばんわ」のグラフですね。
なるほど。では、スペクトログラムを見てみましょう。
見てください、横軸が時間になっていますね!色が明るくなっている部分ほど強い周波数成分が出ているということですね。
2.3. 短時間フーリエ変換の実行[scipyの組み込み関数]
実は・・・スペクトログラムについても、簡単に処理してくれる変数が作られています。
ですので、「fft_voice_spectrogram.py」は下のプログラムでも同じ処理をしてくれます。みてみましょう。
【ソースコード】短時間フーリエ変換プログラム(ソースコード)
''' 短時間フーリエ変換(stft)を変数で実行 ''' import wave import numpy as np from scipy import signal import matplotlib.pyplot as plt '''waveファイルから必要な情報を読み込む''' wavefile = wave.open(r"C:\...\VoiceRecognition\sample\sample_voice01.wav", "r") # waveファイルを読み込み用ファイルとして開く nframes = wavefile.getnframes() # フレーム総数を調べる framerate = wavefile.getframerate() # フレームレート(サンプリング周波数)を調べる y = wavefile.readframes(nframes) # フレームの読み込み y = np.frombuffer(y, dtype="int16") # 波形を変換出来る様に変形する t = np.arange(0, len(y))/float(framerate) # 音声データの長さ(x軸) wavefile.close() # waveファイルを閉じる N=1024 freqs, times, Sx = signal.spectrogram(y, fs=framerate, window='hanning', nperseg=1024, noverlap=N-100, detrend=False, scaling='spectrum') # スペクトログラム変数 f, ax = plt.subplots() ax.pcolormesh(times, freqs/1000, 10* np.log10(Sx), cmap='viridis') ax.set_ylabel('Frequency[kHz]') ax.set_xlabel('Time[s]') plt.show()
【実行結果と比較】短時間フーリエ変換プログラム
では改めて、それぞれの結果を見比べます。
組み込み変数を使用したスペクトログラムが以下になります。
次に、もう一度
np.fft.fft()
で求めたスペクトログラムを表示してみましょう。
少し色が違いますね。これは、scipyがスペクトルの大きさの2乗を返して正規化係数をかけている為みたいです。(すみません、詳しいことはわかりませんが、スペクトル内のエネルギー保存のため、つじつまを合わせる作業を行っているようです。)
3. まとめ
これで、スペクトル解析のプログラムレポートは終了とします。あくまでスペクトル解析に関する技術に沿った解説としたため、プログラムやライブラリの詳細の解説は省いていますが、いかがでしたか?
フーリエ変換とスペクトル解析を実践的に構築することで仕組みをより理解できたら幸いです。
参考書籍
今回は下の書籍の流れに沿ってプログラムを行いました。
『エレガントなSciPy : Pythonによる科学技術計算 』 Juan Nunez-Iglesias, Stéfan van der Walt, Harriet Dashnow著 ; 山崎邦子, 山崎康宏訳. -- オライリー・ジャパン, 2018.