4
6

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 3 years have passed since last update.

[TensorFlow 2] RNNをCTC Lossを使って学習してみる

Last updated at Posted at 2020-04-24

はじめに

系列を返すRNN (Recurrent Neural Network) のパラメータを、CTC (Connectionist Temporal Classification) Loss を使って学習する方法をTensorFlow 2.xで試しました。
サンプルが少なくて動かすのに苦労したのでメモしておきます。

CTC Lossに関しては以下のページなどにまとまっています。

検証環境

  • 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で動きます。

patch
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)
Terminal
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向けの書き方をしたほうが処理効率も上がりますし(多分)、後々のメンテナンスのためにもよいでしょう。
というわけでサンプルコードを書き換えてみようと思いますが、なかなか書き方のサンプルが見つからない…

色々なところのコードを切り貼りした感じでようやく動きました。
主な参照サイトは以下です。

  1. Effective TensorFlow 2 | TensorFlow Core
  2. TensorFlow 2.0 Alpha : 既存コードを TensorFlow 2.0 に変換する – TensorFlow 2.x
  3. TensorFlow 2.0 主な変更点 - S-Analysis

サンプルコード(1)

ctc_tensorflow_example_tf2.py
#  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.Sessiontf.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)

ctc_tensorflow_example_tf2_multi.py
#  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)をベースにしています。

ctc_tensorflow_example_tf2_keras.py
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_targetsval_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_truey_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_lerval_ler の値は正しく出力されません。

学習部分を頑張ってKerasスタイルで書いたところで、怪しいハックばかりになってしまい、今のところは良いことがありません。TensorFlowやKerasのバージョンアップに伴って解決していくかもしれませんが、どうでしょうね。

まとめ

  • TensorFlow 2.xでCTC Lossを使ったパラメータの学習方法を説明しました。一応動いているみたいです。
  • TensorFlowスタイルの学習ループを書きつつ部分的にKerasコードが混じったような格好になっていますが、全部Kerasで書こうとするとかえって面倒なのでお勧めしません。
  • (2020/04/27追記) Kerasスタイルで学習させる方法は別記事をご覧ください。
  1. @tf.function を付けなくても動きますが、Eager Execution となるため遅いです。デバッグ時には Eager Execution は有用なので @tf.function を外して(コメントアウトして)おき、動くようになったら @tf.function を付ける、という方針でよいと思います。

  2. データが少ないので、学習そのものが遅いのではなく、Keras化によってデータ量によらないオーバーヘッドが発生しているだけかもしれません。また、ラベルをDenseとSparseで行き来させていることなども原因として考えられます。

4
6
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
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?