音声解析の備忘録(目次)
はじめに
このブログの記事は以下のサイトに移転しました。
より詳細な記事やプログラムの実装方法などは移設先のHPを参照ください。
teckonestep.com
このシリーズでは私が音声解析・音声処理について学んだことを備忘録として記していこうと思います。
ここで扱う音声は主に人声です。
現在、人の発話から文字起こしする技術や話者の識別、はたまた人の声を生成する仕組みまで生まれました。
私の最終目標は話者識別を機械学習を用いてできるようになりたいと考えています。
別の記事で機械学習についても備忘録を残していますが、本記事にもその知識を活かしていきたいと考えています。
この記事は音声解析関連の記事の目次の役割をしています。
記事が追加されるたびに関連項目の記事へリンクから飛べるようにしていこうと考えています。
音声解析の備忘録
※追加された記事はリンクから閲覧することができます。
- 音声データの基礎知識
- 音声データを操作してみる(ファイルの読み込み/書き込み)
- 音声の再生と録音
- 色々な波形を作ってみる(矩形波、三角波、鋸波)
- サンプリング周波数を変えてみる(リサンプリング)
- 音を伸縮する(タイムストレッチ)
- ピッチシフト(ボイスチェンジ)
話者認識
話者認識とは?
話者認識とは、マイクになどによって録音された音声から個人を認識・特定するコンピュータ処理のことです。
ここで言う「音声」とは人の声のこと指します。つまり、顔の見えない相手の声が、誰の声なのかコンピュータに推測させる分野のことです。
人の声の特徴を掴む
私たちが人の声から個人を特定するとき、最も大きなヒントになるのが「声の高さ」です。
もっと踏み込んだ表現をすると、人が発する音声の周波数が特徴であると言えます。
下図は男性と女性の声をスペクトログラムにした画像です。
横軸は時間、縦軸は周波数、カラーバーは音圧レベルを表します。
(左:男性の音声 右:女性の音声)
発話している箇所を抜き出して比較します。
どちらのスペクトログラムも発話部分に縞模様が見られます。
ただし、男性のスペクトログラムに見られる縞模様よりも女性のスペクトログラムの縞模様の方が高周波数成分に広く分布しています。
これは女性の声の方がより高い周波数の音を組み合わせて形成されていることを示唆します。
MFCC(メル周波数ケプストラム係数)
MFCCは聴覚フィルタに基づく音響分析手法で、音声認識の分野で使用されることの多い特徴量です。
周波数の高い音ほど音程を識別しにくくなるという人間の音高知覚が考慮されています。
MFCCはlibrosaもしくはpython_speech_featuresモジュールを使用することで簡単に実装することができます。
import librosa librosa.feature.mfcc(wave) from python_speech_features import mfcc mfcc(wave_data,fs)
上記の関数によって得られるMFCCは2次元配列になっています。
MFCCの次元数は各関数の引数として設定することができ、本解析では13次元のMFCCを使用しています。。
混合ガウスモデル(GMM)
混合ガウスモデル(以下:GMM)は教師なしクラスタリングでよく使用される機械学習手法です。
分類したいクラスタの数(混合数)は予め解析者が指定する必要があります。
GMMはEMアルゴリズムというアルゴリズムによってフィッティングされます。
下図はデータが3つのクラスタに分類されていく様子を示したものです。
Universal Background Model(UBM)
UBMとは不特定話者の平均的な音声モデルです。
具体的には、複数話者の音声から得られたMFCCを13次元のデータ点とみなし、それらの分布をGMMによってクラスタリングしています。
こうして得られたGMMのパラメータ(重み、平均ベクトル、共分散行列)を特定話者の学習時に初期パラメータとして使用します。
特に平均ベクトルを平滑化したもののことをGMMスーパーベクトルと呼び、話者特徴量を含んでいるとされています。
UBMを導出することで特定話者のGMMスーパーベクトルを導出する際に、同一話者から得られるベクトル要素が常に同じ形で整列されます。また、導出時間が短縮されることも利点の一つと言えます。
類似度の評価
特定話者の音声から得られたそれぞれのGMMスーパーベクトルをコサイン類似度によって評価します。
同じ話者(もしくは声の似た話者)から得られたGMMスーパーベクトルのコサイン類似度はより高く評価され、違う話者(もしくは声の似ていない話者)から得られたGMMスーパーベクトルのコサイン類似度は低く評価されます。
モデルの評価
本解析では19人の話者からそれぞれ11サンプル(1サンプルは学習用、10サンプルは評価用)、各1分間の音声を取得し評価しました。
混同行列を作成することでモデルの正答率を示します。
上図の対角成分が正解したことを示し、非対角成分がどのような不正解をしたのかを示します。
結果、190サンプル中163サンプル(85.7%)正解することができました。
モデルの正解率の低かった、irieさんとannakaさんの声質は非常に似ており、解析の結果としては納得できるものでした。
まとめ
今後、認識精度を向上させるためには「話し方の癖」を数値化して特徴量に織り込む必要があるかもしれません。
また、本解析の音声サンプルは非雑音環境で録音されたデータばかりであるため、実環境で使用するためにはノイズキャンセラや音源分離といった技術を併用していく必要がありそうです。
色々な波形を作ってみる(矩形波、三角波、鋸波)
このブログの記事は以下のサイトに移転しました。
より詳細な記事やプログラムの実装方法などは移設先のHPを参照ください。
teckonestep.com
この記事では様々な形の周期波形を作成してみます。
作成した波形はそれぞれ特徴的な音色をしており、その違いが楽しめると思います。
※音声を再生する際は音量に注意してください。
事前準備
作成した波形データを可視化、保存するためのメソッドを定義します。
基本パラメータ
#振幅 A1=1 #基本周波数 f1 =330 #サンプリング周波数 fs = 44100 #時間(秒) sec = 5 #サンプリングした時の時間軸 t = np.arange(0.0, sec, 1 / fs)
グラフ化
def plot_wav(wave_data,t): scale=0.01#描画範囲の設定(s) fs = 44100#サンプリング周波数 q=int(scale*fs) plt.plot(t[0:q], wave_data[0:q]) plt.show()
ファイル保存
def create_wave(file_name,wave_data,fs): #16bit符号付き整数に変換 wave_data = [int(x * 32767.0) for x in wave_data] #バイナリ化 binwave = struct.pack("h" * len(wave_data), *wave_data) #wavファイルとして書き出し wf = wave.Wave_write(filename) wf.setparams(( 1, # channel 2, # byte width fs, # sampling rate len(data), # number of frames "NONE", "not compressed" # no compression )) wf.writeframes(binwave) wf.close()
正弦波
波形と言えばこの形です。耳触りの良い綺麗な音色をしています。
プログラムで表現するのも容易で、Pythonではたった数行でデータ配列を生成することができます。
#正弦波 def sin_wave(A,f0,t): #振幅A,基本周波数f0,録音時間t sinwav = A * np.sin(np.pi * f0 * t) plot_wav(sinwav,t) return sinwav #サイン波の描画 サンプル sinwav=sin_wave(A1,f1,t) create_wave('sinwav_test',sinwav, fs)
矩形波
矩形波は電気的に発生するのが容易であるため、ゲームの音楽でもよく使用されてきました。ファミコンのピコピコ音はこの矩形波が使われているからです。
#矩形波 def square_wave(A,f0,t): squarewav=np.array([0.0 for i in range(len(t))]) for i in range(len(t)): if np.sin(np.pi*f0*t[i])>0: squarewav[i]=1*A elif np.sin(np.pi*f0*t[i])==0: squarewav[i]=0*A elif np.sin(np.pi*f0*t[i])<0: squarewav[i]=-1*A plot_wav(squarewav,t) return squarewav #矩形波の描画 サンプル squarewav=square_wave(A1,f1,t) create_wave('squarewav_test',squarewav, fs)
三角波
#三角波 def triangle_wave(A,f0,t,fs): trianglewave = np.array([0.0 for i in range(len(t))]) filtersize = fs//f0//2 trianglefilter = np.arange(0,filtersize) trianglefilter = trianglefilter[:]/(filtersize-1) for i in range(len(t)//filtersize): for j in range(filtersize): index_num = i*filtersize + j if i%2==0: trianglewave[index_num] = trianglefilter[j] else: trianglewave[index_num] = trianglefilter[j*(-1)-1] trianglewave[:] -= 0.5 trianglewave[:] *= 2 plot_wav(trianglewave,t) return trianglewave #三角波の描画 サンプル trianglewave=triangle_wave(A1,f1,t,fs) create_wave('triangle_wave',trianglewave, fs)
鋸波
パソコンのビープ音のような音色です。
#のこぎり波 def sawtooth_wave(A,f0,t,fs): sawtoothwave=np.arange(0,len(t)) for i in range(len(t)): sawtoothwave[i]=i%((fs//f0)-1) sawtoothwave = sawtoothwave[:]/((fs//f0)-1)-0.5 sawtoothwave = sawtoothwave[:]*2 plot_wav(sawtoothwave,t) return sawtoothwave #のこぎり波の描画 サンプル sawtoothwave=sawtooth_wave(A1,f1,t,fs) create_wave('sawtooth_wave',sawtoothwave, fs)
音声の再生と録音
本記事ではマイクから音声を録音、スピーカで再生する方法を記載します。
Pyaudioというモジュールを使用して実装していきます。
音声の再生
音声の録音・再生をする際はPyaudioで継続的にデータを入力or出力をしていきます。
コード内のパラメータの意味は以下の通りです。
・fs → サンプリング周波数
・ch → チャンネル数
・chunk → チャンク数
・format → データフォーマット
音声の再生はデータ配列をチャンクごとに区切り、配列が空になるまでストリームにデータを流し続ける仕組みにしました。
#正規化されたデータをバイナリデータに変換して使用する #パラメータの設定 fs = 44100 ch = 1 chunk = 1024 format = pyaudio.paInt16 # int16型 data = [int(x * 32767.0) for x in wave_data] data = struct.pack("h" * len(data), *data) #Pyaudioのモジュールを作成 p = pyaudio.PyAudio() #ストリームを作成 stream = p.open(format=format, channels=ch, rate=fs, output=True) #dataが空になるまでストリームにデータを書き込む for i in range(len(data)//chank): stream.write(data[chank*i:chank*(i+1)]) #ストリームを閉じる stream.stop_stream() stream.close() p.terminate()
音声の録音
音声の録音はマイクがストリームに記録したデータをチャンク毎に読み出して、配列に追加していく仕組みになります。
#パラメータの設定 fs = 44100 ch = 1 rec_time = 5 chunk = 1024 format = pyaudio.paInt16 # int16型 #Pyaudioのモジュールを作成 p = pyaudio.PyAudio() #ストリームを作成 stream = p.open(format=format, channels=ch, rate=fs, input=True, frames_per_buffer=chunk) #録音時間が終了するまでデータを取得し続ける frames = [] for i in range(0, int(fs / chunk * rec_time)): data = stream.read(chunk) frames.append(data) #ストリームを閉じる stream.stop_stream() stream.close() p.terminate() #配列を結合してバイナリ化 wave_data = b''.join(frames) #データをInt16に変換してさらに正規化 wave_data = np.frombuffer(wave_data, dtype="int16") wave_data = wave_data / 32767.0
音声データ(waveファイル)の基礎知識
ここでは音声データを扱う上で知っておくべき用語や知識を整理します。
基本的な用語
-
音源
解析・処理対象となるデータを指す。基本的には全てバイナリデータである。
-
静的な音源
ファイルに保存された非リアルタイム 音源のこと。(wav、mp3、rawなど)
ここでは主にwavファイルを使用して解析を行う。
-
動的な音源
マイクなどのセンサから入力されたリアルタイム 音源のこと。
-
ストリーム
センサなどによって生成される継続的なデータのことである。または、スピーカなどに与えるための継続的なデータを指す。
これらのデータはレコード、またはチャンク単位、もしくはスライドした時間窓で連続的に処理する。データストリームとも呼ぶ。
パラメータに関する用語
- チャンク
音源から1回読み込むときのデータサイズ。またはRIFF形式におけるデータの論理的な単位のこと。
2のn乗とする場合が多い。
- サンプリング周波数
1秒間にデータを取得するデータ点数を決める値である。
音声データに記録できる音源の周波数はサンプリング周波数の2分の1の値までとなっている。
(標本化定理で調べると詳しく理解できます。ここでは説明を省きます。)
- フレーム数
Pythonモジュール上ではバイナリデータの個数を表す値である。
他分野(映像関連など)では用語の使われ方が違うので注意が必要である。
wavデータの構造について
音声データを自身で記録・処理しようとすると、多くの場合wav形式を採用することになる。そこで、ここではwavのデータ構造について簡単に学ぶ。
wavはWindows標準の音声データファイルである。
wavはRIFF形式で作られており、wavを理解するためにはこのRIFFを理解する必要がある。
- RIFF形式とは
RIFF(Resouce Interchange File Format)形式とは、画像や音声などのデータを1つのファイルに格納するための共通フォーマットである。RIFFにはチャンクと呼ばれる考え方があり、データの論理的な単位とされる。wavファイルはチャンクを1つにまとめた集合体である。
チャンクの例)
- wavファイルのデータフォーマット
wavファイルの基本構造は上述のRIFF形式でRIFFヘッダ、fmtチャンク、dataチャンクの3つから構成される。そこに必要に応じてfactチャンク、cueチャンクなどが追加される。
- RIFFヘッダ
該当ファイルが「wavファイル」であることを示す。
- fmtチャンク
wavファイルの設定情報が保存されている。サンプリング周波数、チャネル数、音声フォーマットなどの情報が保存されている。
- dataチャンク
音声の波形データが保存されている。
本体部分である。
音声データを操作してみる(ファイルの読み込み/書き込み)
この記事では音声データを自在に操れるようになることを目標に以下のことを学びます。
基本的に使用するプログラミング言語はPythonです。必要に応じて外部ツールや多言語にも触れていきます。
音声データの基礎知識で学んだことを生かして学習を進めます。
waveファイルは波形をバイナリデータで保存していますが、1つのデータは2byte(Int16)で表現されています。
なのでデータの読み書きはバイナリデータを扱うことになります。
音声ファイル(wav)の読み込み
音声ファイルを扱う時はwaveモジュールを使用してWave_readオブジェクトを生成します。
waveモジュールは音声ファイルを読み込みモードまたは書き込みモードで開くことができます。
ただし、同時に両方のモードで開くことができないため、読み込みモードで開いた音声ファイルにデータを書き込むことはできません。
オブジェクトの持つメソッドについては以下のドキュメントを参照するのがオススメです。
docs.python.org
import wave file_path = '音声ファイルのパス' #指定した音声ファイルを読み込んだwaveオブジェクト wf = wave.open(file_path, "r") #wfオブジェクトからサンプリングレートを取得 fs = wf.getframerate() #オブジェクトからチャンネル数を取得 ch = wf.getnchannels() #オブジェクトからデータを読み取る wave_data = wf.readframes(wf.getnframes()) #バッファから1次元配列に変換(バイナリデータをInt16で読み込み、正規化も行っている。) wave_data = np.frombuffer(wave_data, dtype="int16") / 32768.0
読み込んだ音声データは1次元リストになってwave_dataに記憶されます。
今後はこのように音声データを読み込んで解析・処理に使用します。
音声ファイル(wav)の書き込み
読み込みと同様に書き込み処理もwaveモジュールを呼び出します。
Wave_writeオブジェクトを使用して書き込みます。
import wave #正規化されている音声をint16へ変換 data = [int(x * 32767.0) for x in data] #バイナリ化 binwave = struct.pack("h" * len(data), *data) wf = wave.Wave_write(filename) wf.setparams(( 1, # channel 2, # byte width fs, # sampling rate len(data), # number of frames "NONE", "not compressed" # no compression )) wf.writeframes(binwave) wf.close()
音声データを可視化してみる
グラフの作成にはmatplotlibモジュールを使用します。
音声に限らずpythonでグラフを出力したい場合はこのmatplotlibを使うことになるでしょう。
matplotlibはできることがたくさんありますがここでは必要な機能に限り紹介します。
import matplotlib.pyplot as plt #グラフの描画サイズを変更 plt.figure(figsize=(10,5)) #x軸の値を配列で作成 x = np.linspace(0.,len(wave_data)/fs,len(wave_data)) #グラフを描画 plt.plot(x,wave_data) #グラフを出力 plt.show()
グラフの出力結果が以下のようになります。
読み込んだ音声は60秒間の音声であることがグラフの横軸からも確認できます。
ニューラルネットワークとは?
ニューラルネットワークモデルは、ニューロンモデルを単位として構築されます。
ニューロンモデルは脳の神経細胞(ニューロン)の動きから考案されたモデルで、その動きは大きく分けると2つになります。
①他の神経細胞から電気パルスを受け取る。
②受け取った電気パルスの合計が閾値を超えるとパルスを出力する。
ニューロンモデルは数学的に上記2つの動きを再現します。
では、具体的に数学的モデルとして説明します。
上図の四角形は入力値、円形がニューロンを表しています。
入力値xは実数値をとり、各入力の重み付け和aを計算します。
a = w0x0 + w1x1 + w2x2
入力総和aは活性化関数によって出力値yに変換し、次のニューロンに渡されます。
活性化関数は一般に以下のような関数が使用されます。
また、損失関数は目的関数とも呼ばれ、出力yと教師データとの誤差を学習パラメータとして使用するために必要な関数です。最終的な学習結果で最小にすべき値を出力します。
これは最小二乗法でいうところの誤差関数であり、確率モデルでいうところ交差エントロピー誤差にあたります。
特にシグモイド関数&交差エントロピー誤差を使用した場合、上図のニューロンモデルは最尤推定・交差エントロピー誤差(2)で紹介した学習手法と等価です。
このニューロンモデルをいくつもつなげたネットワークのことをニューラルネットワークモデルと呼び、その層を2層、3層と重ねた物が一般に深層学習と呼ばれています。
ニューラルネットワークモデルは活性化関数、損失関数、ネットワークの構成をうまく組み合わせることで高い精度を実現します。
また、ネットワーク構成によって、名称やできることが変わっています。
今後の記事ではそういったニューラルネットワークモデルを少しずつご紹介できればと考えています。