はじめに
系列を返すRNN (Recurrent Neural Network) のパラメータを、CTC (Connectionist Temporal Classification) Loss を使って学習する方法をTensorFlow 2.xで試しました。
サンプルが少なくて動かすのに苦労したのでメモしておきます。
CTC Lossに関しては以下のページなどにまとまっています。
- Connectionist Temporal Classificationの理論と実装について – ご注文は機械学習ですか?
- Connectionist Temporal Classification (CTC) を用いた音素認識 - Qiita
- 音声認識と深層学習 - SlideShare
検証環境
- Ubuntu 18.04
- Python 3.6.9
- TensorFlow 2.1.0 (CPU)
基にしたサンプルコード
GitHub - igormq/ctc_tensorflow_example: CTC + Tensorflow Example for ASR
TensorFlow 1.xで、Keras APIを使わずに実装しているサンプルです。
End-to-Endの音声認識サンプルといった感じで、特徴量系列とラベル(文字)系列の対応をLSTMで学習させています。
1.x版のコードではありますが、TensorFlow 2.xで動かすこと自体は難しくありません。
# 必要なパッケージをインストール
pip3 install python_speech_features --user
# コードを取得
git clone https://github.com/igormq/ctc_tensorflow_example.git
以下のように ctc_tensorflow_example.py
を3行ほど変えればTensorFlow 2.xで動きます。
diff --git a/ctc_tensorflow_example.py b/ctc_tensorflow_example.py
index 579d431..2d96d54 100644
--- a/ctc_tensorflow_example.py
+++ b/ctc_tensorflow_example.py
@@ -5,7 +5,7 @@ from __future__ import print_function
import time
-import tensorflow as tf
+import tensorflow.compat.v1 as tf
import scipy.io.wavfile as wav
import numpy as np
@@ -20,6 +20,8 @@ except ImportError:
from utils import maybe_download as maybe_download
from utils import sparse_tuple_from as sparse_tuple_from
# Constants
SPACE_TOKEN = '<space>'
SPACE_INDEX = 0
@@ -103,9 +105,9 @@ with graph.as_default():
# tf.nn.rnn_cell.GRUCell
cells = []
for _ in range(num_layers):
- cell = tf.contrib.rnn.LSTMCell(num_units) # Or LSTMCell(num_units)
+ cell = tf.nn.rnn_cell.LSTMCell(num_units) # Or LSTMCell(num_units)
cells.append(cell)
- stack = tf.contrib.rnn.MultiRNNCell(cells)
+ stack = tf.nn.rnn_cell.MultiRNNCell(cells)
# The second output is the last state and we will no use that
outputs, _ = tf.nn.dynamic_rnn(stack, inputs, seq_len, dtype=tf.float32)
python3 ctc_tensorflow_example.py
Epoch 1/200, train_cost = 726.374, train_ler = 1.000, val_cost = 167.637, val_ler = 1.000, time = 0.549
(略)
Epoch 200/200, train_cost = 0.648, train_ler = 0.000, val_cost = 0.642, val_ler = 0.000, time = 0.218
Original:
she had your dark suit in greasy wash water all year
Decoded:
she had your dark suit in greasy wash water all year
TensorFlow 2向けのコードに変換する
せっかくTensorFlow 2を使うならば、TensorFlow 2向けの書き方をしたほうが処理効率も上がりますし(多分)、後々のメンテナンスのためにもよいでしょう。
というわけでサンプルコードを書き換えてみようと思いますが、なかなか書き方のサンプルが見つからない…
色々なところのコードを切り貼りした感じでようやく動きました。
主な参照サイトは以下です。
- Effective TensorFlow 2 | TensorFlow Core
- TensorFlow 2.0 Alpha : 既存コードを TensorFlow 2.0 に変換する – TensorFlow 2.x
- TensorFlow 2.0 主な変更点 - S-Analysis
サンプルコード(1)
# Compatibility imports
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import time
import tensorflow as tf
import scipy.io.wavfile as wav
import numpy as np
from six.moves import xrange as range
try:
from python_speech_features import mfcc
except ImportError:
print("Failed to import python_speech_features.\n Try pip install python_speech_features.")
raise ImportError
from utils import maybe_download as maybe_download
from utils import sparse_tuple_from as sparse_tuple_from
# Constants
SPACE_TOKEN = '<space>'
SPACE_INDEX = 0
FIRST_INDEX = ord('a') - 1 # 0 is reserved to space
# Some configs
num_features = 13
num_units=50 # Number of units in the LSTM cell
# Accounting the 0th indice + space + blank label = 28 characters
num_classes = ord('z') - ord('a') + 1 + 1 + 1
# Hyper-parameters
num_epochs = 200
num_hidden = 50
num_layers = 1
batch_size = 1
initial_learning_rate = 1e-2
momentum = 0.9
num_examples = 1
num_batches_per_epoch = int(num_examples/batch_size)
# Loading the data
audio_filename = maybe_download('LDC93S1.wav', 93638)
target_filename = maybe_download('LDC93S1.txt', 62)
fs, audio = wav.read(audio_filename)
inputs = mfcc(audio, samplerate=fs)
# Transform in 3D array
train_inputs = np.asarray(inputs[np.newaxis, :], dtype=np.float32)
train_inputs = (train_inputs - np.mean(train_inputs))/np.std(train_inputs)
train_seq_len = [train_inputs.shape[1]]
# Reading targets
with open(target_filename, 'r') as f:
#Only the last line is necessary
line = f.readlines()[-1]
# Get only the words between [a-z] and replace period for none
original = ' '.join(line.strip().lower().split(' ')[2:]).replace('.', '')
targets = original.replace(' ', ' ')
targets = targets.split(' ')
# Adding blank label
targets = np.hstack([SPACE_TOKEN if x == '' else list(x) for x in targets])
# Transform char into index
targets = np.asarray([SPACE_INDEX if x == SPACE_TOKEN else ord(x) - FIRST_INDEX
for x in targets])
train_targets = tf.sparse.SparseTensor(*sparse_tuple_from([targets], dtype=np.int32))
train_targets_len = [train_targets.shape[1]]
# We don't have a validation dataset :(
val_inputs, val_targets, val_seq_len, val_targets_len = train_inputs, train_targets, \
train_seq_len, train_targets_len
# THE MAIN CODE!
# Defining the cell
# Can be:
# tf.nn.rnn_cell.RNNCell
# tf.nn.rnn_cell.GRUCell
cells = []
for _ in range(num_layers):
cell = tf.keras.layers.LSTMCell(num_units) # Or LSTMCell(num_units)
cells.append(cell)
stack = tf.keras.layers.StackedRNNCells(cells)
model = tf.keras.models.Sequential()
model.add(tf.keras.layers.RNN(stack, input_shape=(None, num_features), return_sequences=True))
# Truncated normal with mean 0 and stdev=0.1
# Zero initialization
model.add(tf.keras.layers.Dense(num_classes,
kernel_initializer=tf.keras.initializers.TruncatedNormal(0.0, 0.1),
bias_initializer="zeros"))
optimizer = tf.keras.optimizers.SGD(initial_learning_rate, momentum)
@tf.function
def step(inputs, targets, seq_len, targets_len, flag_training):
if flag_training:
with tf.GradientTape() as tape:
logits = model(inputs, training=True)
# Time major
logits = tf.transpose(logits, (1, 0, 2))
cost = tf.reduce_mean(tf.nn.ctc_loss(targets, logits, targets_len, seq_len, blank_index=-1))
gradients = tape.gradient(cost, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
else:
logits = model(inputs)
# Time major
logits = tf.transpose(logits, (1, 0, 2))
cost = tf.reduce_mean(tf.nn.ctc_loss(targets, logits, targets_len, seq_len, blank_index=-1))
# Option 2: tf.nn.ctc_beam_search_decoder
# (it's slower but you'll get better results)
decoded, _ = tf.nn.ctc_greedy_decoder(logits, seq_len)
# Inaccuracy: label error rate
ler = tf.reduce_mean(tf.edit_distance(tf.cast(decoded[0], tf.int32),
targets))
return cost, ler, decoded
for curr_epoch in range(num_epochs):
train_cost = train_ler = 0
start = time.time()
for batch in range(num_batches_per_epoch):
batch_cost, batch_ler, _ = step(train_inputs, train_targets, train_seq_len, train_targets_len, True)
train_cost += batch_cost*batch_size
train_ler += batch_ler*batch_size
train_cost /= num_examples
train_ler /= num_examples
val_cost, val_ler, decoded = step(val_inputs, val_targets, val_seq_len, val_targets_len, False)
log = "Epoch {}/{}, train_cost = {:.3f}, train_ler = {:.3f}, val_cost = {:.3f}, val_ler = {:.3f}, time = {:.3f}"
print(log.format(curr_epoch+1, num_epochs, train_cost, train_ler,
val_cost, val_ler, time.time() - start))
# Decoding
d = tf.sparse.to_dense(decoded[0])[0].numpy()
str_decoded = ''.join([chr(x) for x in np.asarray(d) + FIRST_INDEX])
# Replacing blank label to none
str_decoded = str_decoded.replace(chr(ord('z') + 1), '')
# Replacing space label to space
str_decoded = str_decoded.replace(chr(ord('a') - 1), ' ')
print('Original:\n%s' % original)
print('Decoded:\n%s' % str_decoded)
1エポック目だけは時間がかかりますが、それ以降はだいたい3割程度速くなっているようです。
python3 ctc_tensorflow_example_tf2.py
Epoch 1/200, train_cost = 774.063, train_ler = 1.000, val_cost = 505.479, val_ler = 0.981, time = 1.547
Epoch 2/200, train_cost = 505.479, train_ler = 0.981, val_cost = 496.959, val_ler = 1.000, time = 0.158
(略)
Epoch 200/200, train_cost = 0.541, train_ler = 0.000, val_cost = 0.537, val_ler = 0.000, time = 0.143
Original:
she had your dark suit in greasy wash water all year
Decoded:
she had your dark suit in greasy wash water all year
変更点の解説
tf.Session, tf.placeholder の廃止
オリジナルのコードは、TensorFlow 1.xの tf.Session
をベースとした書き方だったので、TensorFlow 2.xでは(tf.compat.v1
APIを使わないと)動きません。tf.placeholder
も無くなり、入力された Tensor
を直接操作するコードを書けばよいことになりました。
基本的には、Effective TensorFlow 2で書かれているように tf.Session
と tf.placeholder
の組み合わせを以下のように書き換えます。
# TensorFlow 1.X
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TensorFlow 2.0
outputs = f(input)
このとき、f
をグラフモードで動かすために @tf.function
デコレータを付けておきます1。
というわけで、オリジナルのコード
# TensorFlow 1.X
feed = {inputs: train_inputs,
targets: train_targets,
seq_len: train_seq_len}
batch_cost, _ = session.run([cost, optimizer], feed)
train_cost += batch_cost*batch_size
train_ler += session.run(ler, feed_dict=feed)*batch_size
は、原則通りに考えるならば、引数として train_inputs, train_targets, train_seq_len
を与え、戻り値として cost, optimizer
を返すような関数(@tf.function
付き)に書き換えることになります。
ただし、optimizer
は実行するだけでよいので値を返す必要はありません。また、直後の session.run
で同じ feed
を与えて ler
を計算していたり、さらに学習が終わったあとのデコード処理で decoded
を使っていたりするので、それらは一緒に返しておきます(decoded
は最後の1回しか使っていませんが、どのみち ler
の計算のために内部で decoded
は計算することになるので、無駄な処理というわけではありません(たぶん…))。
# TensorFlow 2.0
@tf.function
def step(inputs, targets, seq_len, targets_len, flag_training):
(中略)
return cost, ler, decoded
batch_cost, batch_ler, _ = step(train_inputs, train_targets, train_seq_len, train_targets_len, True)
train_cost += batch_cost*batch_size
train_ler += batch_ler*batch_size
検証用途に大半の処理を使い回すため step
という関数名として、追加の引数 flag_training
で学習するかどうかを切り替えるように書きました。
さらに targets_len
という引数が増えていますが、これはTensorFlow 2.xで tf.nn.ctc_loss
に与える引数が変わったためで、Eager Execution化と直接は関係ないはずです。
可変長の正解ラベルを与えるために使っていた tf.sparse_placeholder
も無くなりました。tf.sparse_placeholder
にデータを与えるために (indices, values, shape)
のタプルを用意していましたが、外から tf.SparseTensor
を直接指定できるようになったので、自分で tf.SparseTensor
を作ってしまいます。dtype
は、元の tf.sparse_placeholder
の型に合わせますが、tf.int32
ではなく np.int32
となる点にご注意ください(微妙に引っ掛かるポイント)。
# TensorFlow 1.X
train_targets = sparse_tuple_from([targets])
# TensorFlow 2.0
train_targets = tf.sparse.SparseTensor(*sparse_tuple_from([targets], dtype=np.int32))
学習部分の変更
TensorFlow 2.xでは、Optimizer
はKerasのものを使うように変更となりました。それに合わせて、これまで
Optimizer.minimize()
を使っていたところ、TensorFlow 2.xのGradientTape
を使うように変更しました。この処理は先ほど定義した step()
の中にあります。
##### TensorFlow 1.X #####
# Time major
logits = tf.transpose(logits, (1, 0, 2))
loss = tf.nn.ctc_loss(targets, logits, seq_len)
cost = tf.reduce_mean(loss)
optimizer = tf.train.MomentumOptimizer(initial_learning_rate,
0.9).minimize(cost)
##### TensorFlow 2.0 #####
optimizer = tf.keras.optimizers.SGD(initial_learning_rate, 0.9)
@tf.function
def step(inputs, targets, seq_len, targets_len, flag_training):
if flag_training:
with tf.GradientTape() as tape:
logits = model(inputs, training=True)
# Time major
logits = tf.transpose(logits, (1, 0, 2))
cost = tf.reduce_mean(tf.nn.ctc_loss(targets, logits, targets_len, seq_len, blank_index=-1))
gradients = tape.gradient(cost, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
else:
(以下略)
ここで、勾配計算の対象とする(学習する)重みのリストを与える必要があるのですが、手動で定義した tf.Variable
を集めるのは面倒なので、自分でモデルの計算グラフを書いていた部分を tf.keras.Model
に変えてしまいました。これにより model.trainable_variables
で学習する重みのリストを簡単に取得できるようになります。また、計算グラフの作成部分も簡単になります。
##### TensorFlow 1.X #####
# The second output is the last state and we will no use that
outputs, _ = tf.nn.dynamic_rnn(stack, inputs, seq_len, dtype=tf.float32)
shape = tf.shape(inputs)
batch_s, max_timesteps = shape[0], shape[1]
# Reshaping to apply the same weights over the timesteps
outputs = tf.reshape(outputs, [-1, num_hidden])
# Truncated normal with mean 0 and stdev=0.1
# Tip: Try another initialization
# see https://www.tensorflow.org/versions/r0.9/api_docs/python/contrib.layers.html#initializers
W = tf.Variable(tf.truncated_normal([num_hidden,
num_classes],
stddev=0.1))
# Zero initialization
# Tip: Is tf.zeros_initializer the same?
b = tf.Variable(tf.constant(0., shape=[num_classes]))
# Doing the affine projection
logits = tf.matmul(outputs, W) + b
# Reshaping back to the original shape
logits = tf.reshape(logits, [batch_s, -1, num_classes])
##### TensorFlow 2.0 #####
model = tf.keras.models.Sequential()
model.add(tf.keras.layers.RNN(stack, input_shape=(None, num_features), return_sequences=True))
# Truncated normal with mean 0 and stdev=0.1
# Zero initialization
model.add(tf.keras.layers.Dense(num_classes,
kernel_initializer=tf.keras.initializers.TruncatedNormal(0.0, 0.1),
bias_initializer="zeros"))
サンプルの次元を含めて3階(3次元)以上のテンソルが tf.keras.layers.Dense
に入力される場合、
- 最後の次元以外を一旦 Flatten し
- 重み行列を右から掛けて
- 計算後に元の形に戻す
といった動作になります。オリジナルのコードでは重みを書ける前後の形状操作を自分で書いていましたが、それもKerasに丸投げでよいので非常に楽です。
その他
dtype
を指定して、特徴量の型を float32
としました。指定しなくても動きますが WARNING が発生します。
train_inputs = np.asarray(inputs[np.newaxis, :], dtype=np.float32)
可変長データを扱えるように改良
先ほどのサンプルコード(1)では学習データが1件しかありませんでしたが、もちろん実際はミニバッチに複数件のデータを入れて学習したいわけです。入力データも正解ラベル系列も長さがまちまちになるので、うまく処理しないといけません。
サンプルコード(2)
# Compatibility imports
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import time
import tensorflow as tf
import scipy.io.wavfile as wav
import numpy as np
from six.moves import xrange as range
try:
from python_speech_features import mfcc
except ImportError:
print("Failed to import python_speech_features.\n Try pip install python_speech_features.")
raise ImportError
from utils import maybe_download as maybe_download
from utils import sparse_tuple_from as sparse_tuple_from
# Constants
SPACE_TOKEN = '<space>'
SPACE_INDEX = 0
FIRST_INDEX = ord('a') - 1 # 0 is reserved to space
FEAT_MASK_VALUE = 1e+10
# Some configs
num_features = 13
num_units = 50 # Number of units in the LSTM cell
# Accounting the 0th indice + space + blank label = 28 characters
num_classes = ord('z') - ord('a') + 1 + 1 + 1
# Hyper-parameters
num_epochs = 400
num_hidden = 50
num_layers = 1
batch_size = 2
initial_learning_rate = 1e-2
momentum = 0.9
# Loading the data
audio_filename = maybe_download('LDC93S1.wav', 93638)
target_filename = maybe_download('LDC93S1.txt', 62)
fs, audio = wav.read(audio_filename)
# create a dataset composed of data with variable lengths
inputs = mfcc(audio, samplerate=fs)
inputs = (inputs - np.mean(inputs))/np.std(inputs)
inputs_short = mfcc(audio[fs*8//10:fs*20//10], samplerate=fs)
inputs_short = (inputs_short - np.mean(inputs_short))/np.std(inputs_short)
# Transform in 3D array
train_inputs = tf.ragged.constant([inputs, inputs_short], dtype=np.float32)
train_seq_len = tf.cast(train_inputs.row_lengths(), tf.int32)
train_inputs = train_inputs.to_sparse()
num_examples = train_inputs.shape[0]
# Reading targets
with open(target_filename, 'r') as f:
#Only the last line is necessary
line = f.readlines()[-1]
# Get only the words between [a-z] and replace period for none
original = ' '.join(line.strip().lower().split(' ')[2:]).replace('.', '')
targets = original.replace(' ', ' ')
targets = targets.split(' ')
# Adding blank label
targets = np.hstack([SPACE_TOKEN if x == '' else list(x) for x in targets])
# Transform char into index
targets = np.asarray([SPACE_INDEX if x == SPACE_TOKEN else ord(x) - FIRST_INDEX
for x in targets])
# Creating sparse representation to feed the placeholder
train_targets = tf.ragged.constant([targets, targets[13:32]], dtype=np.int32)
train_targets_len = tf.cast(train_targets.row_lengths(), tf.int32)
train_targets = train_targets.to_sparse()
# We don't have a validation dataset :(
val_inputs, val_targets, val_seq_len, val_targets_len = train_inputs, train_targets, \
train_seq_len, train_targets_len
# THE MAIN CODE!
# Defining the cell
# Can be:
# tf.nn.rnn_cell.RNNCell
# tf.nn.rnn_cell.GRUCell
cells = []
for _ in range(num_layers):
cell = tf.keras.layers.LSTMCell(num_units) # Or LSTMCell(num_units)
cells.append(cell)
stack = tf.keras.layers.StackedRNNCells(cells)
model = tf.keras.models.Sequential()
model.add(tf.keras.layers.Masking(FEAT_MASK_VALUE, input_shape=(None, num_features)))
model.add(tf.keras.layers.RNN(stack, return_sequences=True))
# Truncated normal with mean 0 and stdev=0.1
# Zero initialization
model.add(tf.keras.layers.Dense(num_classes,
kernel_initializer=tf.keras.initializers.TruncatedNormal(0.0, 0.1),
bias_initializer="zeros"))
optimizer = tf.keras.optimizers.SGD(initial_learning_rate, momentum)
@tf.function
def step(inputs, targets, seq_len, targets_len, flag_training):
inputs = tf.sparse.to_dense(inputs, default_value=FEAT_MASK_VALUE)
if flag_training:
with tf.GradientTape() as tape:
logits = model(inputs, training=True)
# Time major
logits = tf.transpose(logits, (1, 0, 2))
cost = tf.reduce_mean(tf.nn.ctc_loss(targets, logits, targets_len, seq_len, blank_index=-1))
gradients = tape.gradient(cost, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
else:
logits = model(inputs)
# Time major
logits = tf.transpose(logits, (1, 0, 2))
cost = tf.reduce_mean(tf.nn.ctc_loss(targets, logits, targets_len, seq_len, blank_index=-1))
# Option 2: tf.nn.ctc_beam_search_decoder
# (it's slower but you'll get better results)
decoded, _ = tf.nn.ctc_greedy_decoder(logits, seq_len)
# Inaccuracy: label error rate
ler = tf.reduce_mean(tf.edit_distance(tf.cast(decoded[0], tf.int32),
targets))
return cost, ler, decoded
ds = tf.data.Dataset.from_tensor_slices((train_inputs, train_targets, train_seq_len, train_targets_len)).batch(batch_size)
for curr_epoch in range(num_epochs):
train_cost = train_ler = 0
start = time.time()
for batch_inputs, batch_targets, batch_seq_len, batch_targets_len in ds:
batch_cost, batch_ler, _ = step(batch_inputs, batch_targets, batch_seq_len, batch_targets_len, True)
train_cost += batch_cost*batch_size
train_ler += batch_ler*batch_size
train_cost /= num_examples
train_ler /= num_examples
val_cost, val_ler, decoded = step(val_inputs, val_targets, val_seq_len, val_targets_len, False)
log = "Epoch {}/{}, train_cost = {:.3f}, train_ler = {:.3f}, val_cost = {:.3f}, val_ler = {:.3f}, time = {:.3f}"
print(log.format(curr_epoch+1, num_epochs, train_cost, train_ler,
val_cost, val_ler, time.time() - start))
# Decoding
print('Original:')
print(original)
print(original[13:32])
print('Decoded:')
d = tf.sparse.to_dense(decoded[0], default_value=-1).numpy()
for i in range(2):
str_decoded = ''.join([chr(x) for x in np.asarray(d[i][d[i] != -1]) + FIRST_INDEX])
# Replacing blank label to none
str_decoded = str_decoded.replace(chr(ord('z') + 1), '')
# Replacing space label to space
str_decoded = str_decoded.replace(chr(ord('a') - 1), ' ')
print(str_decoded)
実行結果は例えば以下のようになります。
Epoch 1/400, train_cost = 527.789, train_ler = 1.122, val_cost = 201.650, val_ler = 1.000, time = 1.702
Epoch 2/400, train_cost = 201.650, train_ler = 1.000, val_cost = 372.285, val_ler = 1.000, time = 0.238
(略)
Epoch 400/400, train_cost = 1.331, train_ler = 0.000, val_cost = 1.320, val_ler = 0.000, time = 0.307
Original:
she had your dark suit in greasy wash water all year
dark suit in greasy
Decoded:
she had your dark suit in greasy wash water all year
dark suit in greasy
解説
可変長データの準備
# create a dataset composed of data with variable lengths
inputs = mfcc(audio, samplerate=fs)
inputs = (inputs - np.mean(inputs))/np.std(inputs)
inputs_short = mfcc(audio[fs*8//10:fs*20//10], samplerate=fs)
inputs_short = (inputs_short - np.mean(inputs_short))/np.std(inputs_short)
# Transform in 3D array
train_inputs = tf.ragged.constant([inputs, inputs_short], dtype=np.float32)
train_seq_len = tf.cast(train_inputs.row_lengths(), tf.int32)
train_inputs = train_inputs.to_sparse()
num_examples = train_inputs.shape[0]
オリジナルのコードが使っていたデータの一部を切り出したものを用意して、データを2件に増やしています。
最終的にはデータは SparseTensor
にするのですが、まず tf.ragged.constant()
を使って RaggedTensor
を作り、そこから変換するようにすると楽に作れます。
モデルへの可変長データの入力
別の記事でもご紹介したことがありますが、Masking
レイヤーを使うことで可変長の入力を表現します。
Kerasで基本的なRNN (LSTM) を試してみる - Qiita
model.add(tf.keras.layers.Masking(FEAT_MASK_VALUE, input_shape=(None, num_features)))
入力時のミニバッチの形状は最大データ長で作られるので、短いデータを入力する際には長さの不足部分に FEAT_MASK_VALUE
を埋めておきます。
@tf.function
def step(inputs, targets, seq_len, targets_len, flag_training):
inputs = tf.sparse.to_dense(inputs, default_value=FEAT_MASK_VALUE)
入力特徴量について説明しましたが、ラベル側も同じです。
targets[13:32]
は、切り出した音声区間に対応するラベルを取ってきているだけです(マジックナンバーです…)。
# Creating sparse representation to feed the placeholder
train_targets = tf.ragged.constant([targets, targets[13:32]], dtype=np.int32)
train_targets_len = tf.cast(train_targets.row_lengths(), tf.int32)
train_targets = train_targets.to_sparse()
学習を行うときには、必要なデータをまとめた Dataset
を作成し、batch()
を使ってミニバッチを作ります。for
ループでミニバッチを順番に取り出すことができます。
ds = tf.data.Dataset.from_tensor_slices((train_inputs, train_targets, train_seq_len, train_targets_len)).batch(batch_size)
for curr_epoch in range(num_epochs):
(略)
for batch_inputs, batch_targets, batch_seq_len, batch_targets_len in ds:
(略)
現実的には、学習データを事前にTFRecord形式のファイルに書き込んでおき、そこから Dataset
を作って使うことになるのではないかと思います。読み込み時に tf.io.VarLenFeature
を使って SparseTensor
として特徴量を取ってくるようにすれば、今のループの中身の処理をそのまま使えます(たぶん)。
[TensorFlow 2] TFRecordからの特徴量読み込みはバッチ単位でやるのがオススメ - Qiita
Kerasで全部できないの?
TensorFlow 2.xベースの処理で動くようになったので別に良いといえば良いのですが、成り行き上モデルをKeras化してしまったので、いっそのこと学習部分もKerasのAPIで実行できないか考えてみます。
これが修羅の道の始まりであった…。
結論から言うと、無理に頑張らないほうが良いみたいです。
(2020/04/27追記) Kerasでもうまく動かす方法を見つけました。詳しくは別記事をご覧ください。
[TensorFlow 2/Keras] KerasでもCTC Lossを使った学習を動かす方法 - Qiita
サンプルコード(3)
データ1件で学習するサンプルコード(1)をベースにしています。
(TF2版と同じなので前半は略)
# Creating sparse representation to feed the placeholder
train_targets = tf.sparse.to_dense(tf.sparse.SparseTensor(*sparse_tuple_from([targets], dtype=np.int32)))
(略)
def loss(y_true, y_pred):
#print(y_true) # Tensor("dense_target:0", shape=(None, None, None), dtype=float32) ???
targets_len = train_targets_len[0]
seq_len = train_seq_len[0]
targets = tf.cast(tf.reshape(y_true, (-1, targets_len)), tf.int32)
# Time major
logits = tf.transpose(y_pred, (1, 0, 2))
return tf.reduce_mean(tf.nn.ctc_loss(targets, logits,
tf.fill((tf.shape(targets)[0],), targets_len), tf.fill((tf.shape(logits)[1],), seq_len),
blank_index=-1))
def metrics(y_true, y_pred):
targets_len = train_targets_len[0]
seq_len = train_seq_len[0]
targets = tf.sparse.from_dense(tf.cast(tf.reshape(y_true, (-1, targets_len)), tf.int32))
# Time major
logits = tf.transpose(y_pred, (1, 0, 2))
# Option 2: tf.nn.ctc_beam_search_decoder
# (it's slower but you'll get better results)
decoded, _ = tf.nn.ctc_greedy_decoder(logits, train_seq_len)
# Inaccuracy: label error rate
ler = tf.reduce_mean(tf.edit_distance(tf.cast(decoded[0], tf.int32),
targets))
return ler
model.compile(loss=loss, optimizer=optimizer, metrics=[metrics])
for curr_epoch in range(num_epochs):
train_cost = train_ler = 0
start = time.time()
train_cost, train_ler = model.train_on_batch(train_inputs, train_targets)
val_cost, val_ler = model.test_on_batch(train_inputs, train_targets)
log = "Epoch {}/{}, train_cost = {:.3f}, train_ler = {:.3f}, val_cost = {:.3f}, val_ler = {:.3f}, time = {:.3f}"
print(log.format(curr_epoch+1, num_epochs, train_cost, train_ler,
val_cost, val_ler, time.time() - start))
decoded, _ = tf.nn.ctc_greedy_decoder(tf.transpose(model.predict(train_inputs), (1, 0, 2)), train_seq_len)
d = tf.sparse.to_dense(decoded[0])[0].numpy()
str_decoded = ''.join([chr(x) for x in np.asarray(d) + FIRST_INDEX])
# Replacing blank label to none
str_decoded = str_decoded.replace(chr(ord('z') + 1), '')
# Replacing space label to space
str_decoded = str_decoded.replace(chr(ord('a') - 1), ' ')
print('Original:\n%s' % original)
print('Decoded:\n%s' % str_decoded)
それっぽく書けているように見えますね。
しかし実は動作がかなり怪しいです…。
怪しい点
Sparseなラベルの取扱い
Kerasで普通にモデルを作ると、Model.fit()
や Model.train_on_batch()
でSparseなラベルを使うことができません。しょうがないので普通の Tensor
に変換してしまいました。
ラベル誤り率の計算時にはラベルがSparseになっていないといけないので
targets = tf.sparse.from_dense(tf.cast(tf.reshape(y_true, (-1, targets_len)), tf.int32))
としてまたSparseに戻しているのですが、こうするとスペースに相当するID: 0の記号が消えてしまいます(そりゃそうだ、本来は0を持たないのが疎行列なのですから…)。
というわけで、正解ラベル列からスペースが抜けた状態で誤り率を計算することになり、いつまで経っても誤り率が0にならない(スペースの数だけ挿入誤りが発生する)という事態が発生しています。
直近の解決策としては、ID: 0がブランク記号(≠スペース)になるようにID体系を変えることが考えられます。しかし、わざわざSparseをDenseにしてまた戻すようなことをしている点を先に解決したほうが良いですよね…。
損失関数の挙動
Kerasで書くときは Model.compile()
に損失関数を指定します。自分で作った Callable
を指定することもできますが
def loss(y_true, y_pred):
の2引数しか取れないので、今回は長さ情報をグローバル変数から取ってくるようにします。
そこまではまだ良いのですが。
def loss(y_true, y_pred):
#print(y_true) # Tensor("dense_target:0", shape=(None, None, None), dtype=float32) ???
(略)
targets = tf.sparse.from_dense(tf.cast(tf.reshape(y_true, (-1, targets_len)), tf.int32))
y_true
って、正解ラベル(つまり train_targets
と val_targets
)から持ってきたデータじゃないですか。これらの次元は (sample, time)
の2次元であるはずなのに、なぜか3次元の Tensor
になっています…。しかも、元のラベルは int32
で作っているはずなのに、なぜか float32
になっている…。
そういうわけで、y_true
に何が渡ってきているのか分からないまま
targets = tf.sparse.from_dense(tf.cast(tf.reshape(y_true, (-1, targets_len)), tf.int32))
として2次元に変形&型変換してしまっています。怪しすぎる。でも学習はちゃんとできているらしい?
これはKerasの仕様(設計思想?)なのかもしれず、以下のドキュメントの記述も
tf.keras.losses.Loss | TensorFlow Core v2.1.0
y_true: Ground truth values. shape = [batch_size, d0, .. dN]
y_pred: The predicted values. shape = [batch_size, d0, .. dN]
のように、同じ形状であることが前提となっているように読めます。
普通の分類問題でクロスエントロピー損失などを使う場合は全く問題ないのですが、CTC Lossのように「正解ラベルと予測結果の長さが異なる」といったケースになると途端に混乱してしまいます。
…そういえば、sparse_categorical_crossentropy
って y_true
と y_pred
の形状が違いますよね?あれはどうやって実現されているのでしょうか。
-
y_true
: カテゴリ変数(batch_size,)
-
y_pred
: カテゴリごとのスコアの出力(batch_size, num_classes)
つまり、あちらの実装を真似すれば良いはずです。以下の実装を見ると変形と型変換が入っているので、実は今の実装でだいたい合っているのかも?(しかしそれにしても怪しい)
tensorflow/backend.py at v2.1.0 · tensorflow/tensorflow · GitHub
実行速度が遅い
Epoch 1/200, train_cost = 774.764, train_ler = 1.190, val_cost = 387.497, val_ler = 1.000, time = 2.212
Epoch 2/200, train_cost = 387.497, train_ler = 1.000, val_cost = 638.239, val_ler = 1.000, time = 0.459
(略)
Epoch 200/200, train_cost = 3.549, train_ler = 0.238, val_cost = 3.481, val_ler = 0.238, time = 0.461
Original:
she had your dark suit in greasy wash water all year
Decoded:
she had your dark suit in greasy wash water all year
Keras版に書き換える前(TensorFlow 2.x版)の3倍くらい時間が掛かっています…。2
しかも前述の理由で train_ler
と val_ler
の値は正しく出力されません。
学習部分を頑張ってKerasスタイルで書いたところで、怪しいハックばかりになってしまい、今のところは良いことがありません。TensorFlowやKerasのバージョンアップに伴って解決していくかもしれませんが、どうでしょうね。
まとめ
- TensorFlow 2.xでCTC Lossを使ったパラメータの学習方法を説明しました。一応動いているみたいです。
- TensorFlowスタイルの学習ループを書きつつ部分的にKerasコードが混じったような格好になっていますが、
全部Kerasで書こうとするとかえって面倒なのでお勧めしません。 - (2020/04/27追記) Kerasスタイルで学習させる方法は別記事をご覧ください。