エロ動画のシーンを音声から分析するシリーズ 5: 転移学習を行う（会話とあえぎ声に分けるモデルを作る）

# 前回のあらすじ

前回は転移学習で、「声」と「その他」に分けるモデルを作った

今回は、「声」を「会話」と「あえぎ声」に分けるモデルを作る

https://qiita.com/c1z/items/ccf6bf114515e229f3d7

ちなみに、このシリーズ一貫した目的は「あえぎ声」と「会話」を分離すること

# 今回の計画

以下のようなディレクトリ構成になっている

- dataset
    - speech: 会話の音声が入ってる
    - moan: あえぎ声の音声が入ってる
    - env_sound: 声には聞こえない音が入ってる

speech ディレクトリに入っている音声を speech クラス、 moan ディレクトリに入っている音声を moan クラスとして学習して、 speech_and_moan_model として保存する

# 早速コードを書く

```python
import sys
import ffmpeg
import csv
from scipy.io import wavfile
import tensorflow as tf
import tensorflow_hub as hub
import numpy as np
import os
import datetime
import math
import shutil
from pytimeparse.timeparse import timeparse
import tensorflow_io as tfio
import glob

# YAMNet の学習済みモデルをロード
yamnet_model_handle = 'https://tfhub.dev/google/yamnet/1'
yamnet_model = hub.load(yamnet_model_handle)

# (filename, class) のデータセットを作る
speech_wav_filenames = glob.glob('dataset/speech/*.wav')
moan_wav_filenames = glob.glob('dataset/moan/*.wav')
filenames = speech_wav_filenames + moan_wav_filenames
classes = np.hstack([np.repeat(0, len(speech_wav_filenames)), np.repeat(1, len(moan_wav_filenames))])
class_names = ['speech', 'moan']
class_name_dict = { 'speech': 0, 'moan': 1 }
main_ds = tf.data.Dataset.from_tensor_slices((filenames, classes))
dataset_size = len(filenames)

# wav_data を読み込む
@tf.function
def load_wav_16k_mono(filename):
    file_contents = tf.io.read_file(filename)
    wav, sample_rate = tf.audio.decode_wav(file_contents, desired_channels=1)
    wav = tf.squeeze(wav, axis=-1)
    sample_rate = tf.cast(sample_rate, dtype=tf.int64)
    wav = tfio.audio.resample(wav, rate_in=sample_rate, rate_out=16000)
    return wav

def load_wav_for_map(filename, class_id):
    wav = load_wav_16k_mono(filename)
    return wav, class_id

main_ds = main_ds.map(load_wav_for_map)

# YAMNet を適用
def extract_embedding(wav_data, class_id):
    scores, embeddings, spectrogram = yamnet_model(wav_data)
    num_embeddings = tf.shape(embeddings)[0]
    return (embeddings, tf.repeat(class_id, num_embeddings))
main_ds = main_ds.map(extract_embedding)
main_ds = main_ds.unbatch()

# training, validation, testing に分ける
cached_ds = main_ds.cache().shuffle(dataset_size)
train_size = int(0.7 * dataset_size)
val_size = int(0.15 * dataset_size)
test_size = int(0.15 * dataset_size)

train_ds = cached_ds.take(train_size)
remain_ds = cached_ds.skip(train_size)
val_ds = remain_ds.take(val_size)
test_ds = remain_ds.skip(val_size)

train_ds = train_ds.cache().shuffle(1000).batch(32).prefetch(tf.data.AUTOTUNE)
val_ds = val_ds.cache().batch(32).prefetch(tf.data.AUTOTUNE)
test_ds = test_ds.cache().batch(32).prefetch(tf.data.AUTOTUNE)

# 転移学習用のレイヤーを用意
my_model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(1024), dtype=tf.float32, name='input_embedding'),
    tf.keras.layers.Dense(512, activation='relu'),
    tf.keras.layers.Dense(len(class_names))
], name='my_model')
my_model.compile(loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), optimizer="adam", metrics=['accuracy'])

# 3 epoc 改善がなければ終了する
callback = tf.keras.callbacks.EarlyStopping(monitor='loss', patience=3, restore_best_weights=True)

# トレーニング
history = my_model.fit(train_ds, epochs=50, validation_data=val_ds, callbacks=callback)

# 評価
loss, accuracy = my_model.evaluate(test_ds)

print("Loss: ", loss)
print("Accuracy: ", accuracy)

# モデルを保存する
my_model.save('speech_and_moan_model')
```

# 結果

そこそこいい感じ 92% くらいの accuracy

多少正確性は低いが、これを動画に適用して speech が頻出する部分だけを繋げれば、会話だけを追えそうな気はする

# 次は

https://qiita.com/c1z/items/c048ea32bf3632849d70

このモデルを動画に適用して、エロ動画から会話だけを抽出する部分を作る