5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

VGG16を用いた話者分類やってみた(声紋認証)

Last updated at Posted at 2021-12-02

動機

Alexaに話者登録をするとき、簡単な言葉を2、3文発するだけで登録できてしまいます。
最近の話者認識はすごいなあ。どうやってるんだろうなあ。なんかやってみよ。

話者認識について

話者認識の特徴量と言えばi-vectorをよく聞きますよね。
音素にはMFCC。画像にはHOG。話者にはi-vector。そんな時代もありました(なお現役?)。
いまはディープラーニング主流の時代ということで、
スペクトログラムを用いた話者認識(分類問題)をやってみようと思います。
文字通り,声紋認証ですね!

学習データについて

男女5人ずつの音声データを用意します。ひとりあたり60~80秒。相当短いです。
VGG16は入力サイズが(224,224,3)なので,それに合わせてスペクトログラムをつくります。

使用モデルについて

世の偉人の力を借りて、画像分類モデルVGG16を用いた転移学習(Transfer Learning)しようと思います。
学習データが相当少ないこともあり、転移学習の威力を実感するにはちょうど良い?
学生時代は「少ない話者データから頑健な音響モデルをつくる」みたいなことをやっていたので、なんだか哀愁も漂ってきました。

特徴量抽出

繰り返しになりますが、音を画像モデルとして扱うので、特徴量はスペクトログラムを用います。
周波数はメル変換せず、対数スペクトログラムでやってみます。
パラメータ設定は音声信号処理でよく使われる下記の設定にしました。

パラメータ 変数名 値[単位]
サンプリング周波数 FS 16000[Hz]
FFTポイント数 FFTSIZE 512[pt] 0.032[s]
STFTフレーム長 FRAMESEC 0.025[s]
STFTオーバーラップ割合 OVERLAPRATIO 0.6 60%
オーバーラップ割合はフレーム長に対しての割合、つまり15msになります。
FFTの窓関数はhann窓を使います。
from scipy.signal import stft
import numpy as np

#-- constant parameters
FS=16000
FFTSIZE=512
FRAMESEC=.025
OVERLAPRATIO=.6

#-- function for feature extraction
def normalize8(X):
  '''
  normalize 0-1 unsigned 8 bit
  '''
  mn,mx = X.min(),X.max()
  mx -= mn
  X = ((X - mn)/mx)*255
  return X.astype(np.uint8)/255

def make_spectrogram_224(x):
  f,t,S=stft(x,
             fs=FS,
             nperseg=int(FRAMESEC*FS),
             noverlap=int(FRAMESEC*FS*OVERLAPRATIO),
             nfft=FFTSIZE) #(257,226)
  S_mag=np.abs(S)
  S_log=20*np.log10(S_mag)
  S_8bit=normalize8(S_log)[4:228,:] #125[Hz]~7125[Hz]
  S_8bit_3d=np.dstack((S_8bit,S_8bit,S_8bit)) #画像を3次元にする
  #-- スペクトログラムを224列ごとに区切る
  _S=S_8bit_3d[:,:224*(S_8bit_3d.shape[1]//224)] #余りを切り捨て 
  S_8bits=np.array_split(_S,_S.shape[1]//224,axis=1) #224列ごとに区切る
  return f[:224],t[:224],np.array(S_8bits)
  • 画像サイズを(224,224)にするために、前述のパラメータ条件下でお行儀悪くハードコーディングしています。
  • make_spectrogramでスペクトログラムを実際に計算して、時間方向に224個ずつに区切ってリスト化しています。
    このとき、dstackで画像を3次元にしています(VGG16の入力サイズ(224,224,3)の部分を作っています)。
  • normalize8は、値をunsignedの8bitに正規化して、画像と同じ値域[0,255]にする役割を担います。
    ここでは計算したスペクトログラムに対して8bit正規化を行っています。

ちなみに、一枚の画像は↓こんな感じになります。
今回のパラメータでは、時間は約2.23秒間&周波数は125~7125Hzです。
female_gray.png
なんと美しい声紋なのでしょうか。

モデル定義

ではモデルをつくっていきます。今回はVGG16が元なので、kerasでimportしてきます。
なんと便利な世の中なのでしょうか。

# keras VGG16
from keras.models import Model
from keras.layers import Dense, GlobalAveragePooling2D,Input
from keras.applications.vgg16 import VGG16

n_categories=10
n_nodes=1024
base_model=VGG16(weights='imagenet',include_top=False,
                 input_tensor=Input(shape=(224,224,3)))

#-- 自作のモデルを連結
x=base_model.output
x=GlobalAveragePooling2D()(x)
x=Dense(n_nodes,activation='relu')(x)
output=Dense(n_categories,activation='softmax')(x)
model=Model(inputs=base_model.input,outputs=output)

#VGG16のレイヤー重みを固定
for layer in base_model.layers[:15]:
    layer.trainable=False

model.compile(optimizer='Adam',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

model.summary()

モデルの後半はこんな感じになります↓

 block5_pool (MaxPooling2D)  (None, 7, 7, 512)         0                                                                          
 global_average_pooling2d (G  (None, 512)              0         
 lobalAveragePooling2D)                                                                                                           
 dense (Dense)               (None, 1024)              525312                                                                     
 dense_1 (Dense)             (None, 10)                10250    

モデル学習

データを訓練/テストに分けて学習します。

#-- split parameters
from sklearn.model_selection import train_test_split
from keras.utils import np_utils
Y=np_utils.to_categorical(Y, len(id_all)) #one-hot
X_train, X_test, y_train, y_test = train_test_split(X, Y,test_size=.2,random_state=42)

#-- model training
result=model.fit(X_train, y_train, batch_size=32, epochs=40)

#-- model plot
plt.plot(result.history['loss'])
plt.plot(result.history['accuracy'])

loss_plot.png
エポックごとのlossと正解率の推移は↑のような感じになりました。順調に学習が進んでいます(か?)

↓そしてモデルの汎化精度は90%程度でした。
訓練データでは100%を達成しているので、過学習のニオイもします...

#-- model evaluate
>>model.evaluate(X_test, y_test)
3/3 [==============================] - 1s 229ms/step - loss: 0.2917 - accuracy: 0.9136
[0.2916698753833771, 0.9135802388191223]

中間特徴量の可視化

出力で10ノードにする手前の、n_nodes次元の特徴量を可視化してみます。
精度100%の訓練データを用いることにします。
二次元に可視化するためにお馴染みのPCAを用います。
話者毎とは言わずとも、男女毎に線形分離可能っぽい分かれ方をしていると良いなあ

#-- 中間層の値を出力
layer_name = 'dense' # n_nodes数を持つDenseLayerの名前を指定
hidden_layer_model = Model(inputs=model.input, outputs=model.get_layer(layer_name).output) # 新たにモデルを生成
hidden_output = hidden_layer_model.predict(X_train)

#-- PCA
from sklearn.decomposition import PCA
pca = PCA(n_components=3)
X_pca=pca.fit_transform(hidden_output)

#-- PCA処理結果をプロット
_y=np.argmax(y_train,axis=1) #one-hotを1次元ベクトルに戻す
fig = plt.figure(figsize=(8,6))
ax = fig.add_subplot(111)
ax.scatter(X_pca[:, 0], X_pca[:, 1], c=_y,cmap='jet')
ax.set_xlabel('Principal Component 1', size=14)
ax.set_ylabel('Principal Component 2', size=14)
ax.set_title('PCA', size=16)
plt.show()

pca_2dim.png
座標的には密集していますね...
三次元でみたらどうなる?
pca_3d.png
X-Y平面→X-Z平面に。こう見ると、しっかりくっきり分かれていそうです!
カーネル法の説明図みたいになってしまいましたが、3次元空間でもそれなりに特徴が捉えられていることがわかります。ちなみにこの3次元の累積寄与率は80%でした。
じゃあn_nodesもそんなに大きくなくて良いかも。

まとめ

今回は、学習済みモデルであるVGG16を用いた声紋認証を行ってみました。
男女各5人ずつ、そして1人あたりの声紋画像は25~30枚程度(1枚2.2秒)と非常に少ないデータ数でした。
それでも、汎化性能で90%と高い正解率。元モデルが画像分類用(≠音声)のモデルであることも興味深いです。
VGGishやYAMNet、ASTなど音声分類用の学習済モデルもあるので、それらを用いることでさらなる精度向上や応用もできそうですね。
とりあえず今回のモデルを組み込んで声紋認証でロック解除!とかできたら面白そうです。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?