Konuşma Tanıma (Speech Recognition)
Frekans Üzerinden Özellik Çıkartımı, RNN, LSTM, GRU
1 saniyelik ses dosyaları var, bu dosyalardaki ses kayıtları dört farklı komutu içeriyor, İngilizce up, down, yes, no (yukarı, aşağı, evet, hayır) komutları. Ses kayıtları aslında zaman serileridir, tek boyutlu bir veri, mesela 1 saniyelik 16,000 sayı içeren bir vektör. Örnek bir 'down' kaydının neye benzediğini görelim,
import util
import scipy.io.wavfile, zipfile
import io, time, os, random, re
f = util.train_dir + '/down/004ae714_nohash_0.wav'
wav = io.BytesIO(open(f).read())
v = scipy.io.wavfile.read(wav)
print v[1]
plt.plot(v[1])
plt.savefig('speech_01.png')
train 8537 val 949
[-130 -135 -131 ..., -154 -190 -224]
Yapay öğrenme bağlamında zaman serileri için daha önce [7] yazısında LSTM yapısını görmüştük. Örnek olarak zaman serilerini sınıfladık, zaman serisindeki tüm veriler LSTM'e verilmişti, o zaman bir şeride 150 kusur veri noktası varsa, o kadar LSTM hücresi yaratılacaktı. Fakat içinde binlerce öğe olan seriler için bu iyi olmayabilir. Çözüm seriyi bir şekilde özetleyerek bu daha az olan veriyi LSTM'e vermek. Bu özetlere ses işleme alanında parmak izi (fingerprint) ismi de verilmekte.
Ses verilerini frekans üzerinden özetlemek bilinen bir teknik, ses verisi ufak pencerelere bölünür, bu pencereler üzerinde Fourier transformu işletilir, ve oradaki frekans bilgileri, hangi frekansın ne kadar önemli olduğu elde edilir. Spektogram bu bilgiyi renkli olarak göstermenin bir yolu, üstteki ses için,
plt.specgram(v[1], Fs=util.fs, NFFT=1024)
plt.savefig('speech_02.png')
Spektogramın örüntü tanıma için kullanılabileceğini anlamak için bir tane daha farklı 'down' sesi, bir de 'no' sesinin spektogramına bakalım,
f1 = util.train_dir + '/down/0f3f64d5_nohash_2.wav'
wav1 = io.BytesIO(open(f1).read())
v1 = scipy.io.wavfile.read(wav1)
plt.specgram(v1[1], Fs=util.fs, NFFT=1024)
plt.savefig('speech_03.png')
f2 = util.train_dir + '/no/01bb6a2a_nohash_0.wav'
wav2 = io.BytesIO(open(f2).read())
v2 = scipy.io.wavfile.read(wav2)
plt.specgram(v2[1], Fs=util.fs, NFFT=1024)
plt.savefig('speech_04.png')
Görüyoruz ki 'down' seslerinin spektogramları birbirine benziyor. Öğrenme için bu yapıyı kullanabiliriz. Bu arada spektogram "grafiği" y-ekseninde frekansları, x-ekseni zaman adımları gösterir, grafikleme kodu her zaman penceresindeki belli frekans kuvvetlerinin hangi frekans kutucuğuna düştüğüne bakar ve o kutucukta o kuvvete göre renklendirme yapar. Şimdi bu grafikleme amaçlı, ama bazıları bu grafiğe bakarak "ben çıplak gözle bunu tanıyabiliyorum, o zaman görsel tanımayla üstteki imajla sesi tanıyacak bir DYSA kullanayım" diye düşünebiliyor. Bu işleyen bir metot, zaten DYSA'nın görsel tanıma tarihi eski, orada bilinen bir sürü teknik var. Her neyse bazıları üstteki görsel spektogram grafiği, yani R,G,B kanallı çıktı üzerinde görsel tanıma yapmayı da seçebiliyor, fakat bu şart değil, bir spektogram, bir veri durumunda iki boyutlu bir matriste gösterilebilir. TensorFlow ile bu hesabı örnek rasgele bir veri üzerinde yapalım,
import tensorflow as tf
init_op = tf.global_variables_initializer()
data = tf.placeholder(tf.float32, [1, 16000])
print data
stfts = tf.contrib.signal.stft(data, frame_length=400,
frame_step=100, fft_length=512)
spec = tf.abs(stfts)
print spec
s = np.random.rand(1,16000) # rasgele bir zaman serisi uret
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
res = sess.run(spec, feed_dict={data: s })
print res
Tensor("Placeholder_1:0", shape=(1, 16000), dtype=float32)
Tensor("Abs_1:0", shape=(1, 157, 257), dtype=float32)
[[[ 99.39490509 65.10092163 12.84116936 ..., 5.39213753
3.90902305 1.35875702]
[ 100.60041809 66.32343292 12.92744541 ..., 4.64194965
1.80256999 2.0458374 ]
[ 104.70896149 70.13975525 15.93750095 ..., 3.21846962
1.70909929 1.34316254]
...,
[ 97.82588196 63.51060867 11.62135887 ..., 3.23712349
1.94706416 0.41742325]
[ 105.89834595 71.85715485 17.83632851 ..., 4.6476922
2.42140603 1.37829971]
[ 106.46664429 71.12073517 16.69457436 ..., 6.58148479
x 3.24354243 3.80913925]]]
Cok Katmanlı LSTM
LSTM, ya da diğer her RNN çeşidi çok katmanlı olarak kullanılabilir.
Girdiler en alttaki LSTM hücrelerine geçiliyor, bu hücreler birbirlerine konum aktarımı yaptıkları gibi bir sonraki LSTM katmanına girdi de sağlıyorlar, bu aktarım en üst tabakaya kadar gidiyor. Peki o zaman sınıflama amaçlı olarak kullanılan "en son" hücre hangisi olacaktır? Bunun için tipik olarak katmanlı LSTM'de en üst ve en sondaki hücre kullanılır.
Her hücrede 200 nöron var, o zaman her katman (124,200) boyutunda çünkü spektogramdan 124 zaman boyutu geldi, ve LSTM'in en sondaki hücreden alınan vektör 200 boyutunda olacak, bu çıktı bir tam bağlanmış (fully-connected) katmana verilerek buradan 4 tane etiket için olasılık üretilecek, ve tahmin için kullanılan sonuçlar bunlar olacak. O sayılardan en büyük olanı en olası olan ses komutudur.
Tüm modeli görelim,
# model_lstm.py
import tensorflow as tf, util, os
class Model:
def __init__(self):
self.file = os.path.basename(__file__).replace(".pyc","").replace(".py","")
self.mfile = "/tmp/" + self.file + ".ckpt"
self.batch_size = 100
self.num_epochs = 200
self.dop_param = 0.0 # dropout olasiligi
self.num_layers = 4
self.num_cell = 200
tf.reset_default_graph()
self.dop = tf.placeholder(tf.float32) # dropout olasiligi (probability)
self.data = tf.placeholder(tf.float32, [None, util.fs])
print self.data
self.stfts = tf.contrib.signal.stft(self.data, frame_length=256,
frame_step=128, fft_length=256)
print self.stfts
self.fingerprint = tf.abs(self.stfts)
print self.fingerprint
self.y = tf.placeholder(tf.float32, shape=[None, len(util.labels)])
cells = []
for _ in range(self.num_layers):
cell = tf.contrib.rnn.LSTMCell(self.num_cell)
cell = tf.contrib.rnn.DropoutWrapper(cell, output_keep_prob=1-self.dop)
cells.append(cell)
cell = tf.contrib.rnn.MultiRNNCell(cells)
output, states = tf.nn.dynamic_rnn(cell, self.fingerprint, dtype=tf.float32)
print output
for x in states: print x
self.last = states[-1][0] # en ust sagdaki son hucre
print self.last
self.logits = tf.contrib.layers.fully_connected(inputs=self.last,
num_outputs=len(util.labels),
activation_fn=None)
print self.logits
self.softmax = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits,
labels=self.y)
self.cross_entropy = tf.reduce_mean(self.softmax)
self.train_step = tf.train.AdamOptimizer(0.001).minimize(self.cross_entropy)
self.correct_prediction = tf.equal(tf.argmax(self.y,1),
tf.argmax(self.logits,1))
self.evaluation_step = tf.reduce_mean(tf.cast(self.correct_prediction,
tf.float32))
self.saver = tf.train.Saver()
# training 0.91 validation 0.926238
Modelin girdi tensor'un boyutlarını nasıl değiştirdiği altta (üstteki resim iki katman gösterdi, bizim modelde 4 katman var),
import model_lstm
m = model_lstm.Model()
Tensor("Placeholder_1:0", shape=(?, 16000), dtype=float32)
Tensor("stft/rfft:0", shape=(?, 124, 129), dtype=complex64)
Tensor("Abs:0", shape=(?, 124, 129), dtype=float32)
Tensor("rnn/transpose:0", shape=(?, 124, 200), dtype=float32)
LSTMStateTuple(c=<tf.Tensor 'rnn/while/Exit_2:0' shape=(?, 200) dtype=float32>, h=<tf.Tensor 'rnn/while/Exit_3:0' shape=(?, 200) dtype=float32>)
LSTMStateTuple(c=<tf.Tensor 'rnn/while/Exit_4:0' shape=(?, 200) dtype=float32>, h=<tf.Tensor 'rnn/while/Exit_5:0' shape=(?, 200) dtype=float32>)
LSTMStateTuple(c=<tf.Tensor 'rnn/while/Exit_6:0' shape=(?, 200) dtype=float32>, h=<tf.Tensor 'rnn/while/Exit_7:0' shape=(?, 200) dtype=float32>)
LSTMStateTuple(c=<tf.Tensor 'rnn/while/Exit_8:0' shape=(?, 200) dtype=float32>, h=<tf.Tensor 'rnn/while/Exit_9:0' shape=(?, 200) dtype=float32>)
Tensor("rnn/while/Exit_8:0", shape=(?, 200), dtype=float32)
Tensor("fully_connected/BiasAdd:0", shape=(?, 4), dtype=float32)
Eğitim kodu,
import pandas as pd, sys
import numpy as np, util
import tensorflow as tf
import scipy.io.wavfile, zipfile
import io, time, os, random, re
import model_lstm
m = model_lstm.Model()
sess = tf.Session()
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver()
# eger model diskte varsa yukle
print m.mfile
print 'model file exists', os.path.isfile(m.mfile + ".index")
if os.path.isfile(m.mfile + ".index"):
print 'restoring'
saver.restore(sess, m.mfile)
train_files, val_files = util.init_files()
for i in range(m.num_epochs):
train_x, train_y = util.get_minibatch(m.batch_size, train_files, val_files)
d = { m.data:train_x, m.y:train_y, m.dop:m.dop_param}
acc, _ = sess.run([m.evaluation_step, m.train_step], feed_dict=d)
print i, 'accuracy', acc
if i % 5 == 0:
d = { m.data:train_x, m.y:train_y, m.dop:m.dop_param }
tacc = sess.run(m.evaluation_step, feed_dict=d)
val_x, val_y = util.get_minibatch(m.batch_size,train_files, val_files,validation=True)
d = { m.data:val_x, m.y:val_y, m.dop:0}
vacc = sess.run(m.evaluation_step, feed_dict=d)
print i, 'training', tacc, 'validation', vacc
# modeli diske yaz
saver.save(sess, m.mfile)
Eğitim sonrası modelin başarısı eğitim verisi üzerinde yüzde 91, doğrulama verisinde yüzde 92. Kullanılan veri [6]'da.
Dropout
TF ile katmanlararası her noktada dropout kullanılabilir. Dropout ile bir katmandan çıkan ya da ona giren bağlantıların bir kısmı yoksayılır, ve model elde kalanlar ile iş yapmaya uğraşır, aşırı uygunluk problemlerinden böylece kaçınılmış olur. Üstteki kodda hangi olasılıkla dropout yapılacağının olasılığı bir yer tutucu (placeholder) ile TF çizitinin parçası haline getirildi, niye? Böylece son üründeki kullanımda bu parametre 0 yapılarak hiç dropout yapılmaması sağlanabiliyor. Eğitim sırasında bu değer 0.5, 0.2, vs yapılabilir, o zaman dropout devrede olur. Gerçi biz eğitim sırasında da 0 ile eğittik, yani dropout kullanmadık, ama lazım olduğu yerler olabilir, referans açısından burada dursun.
Uygulama
Mikrofondan 1 saniyelik ses parçalarını alıp onu model üzerinde işletip
dört komuttan birini seçen örnek kod mic.py
'da
bulunabilir. Performans gerçek zamanlı kullanım için yeterliydi, DYSA ufak
bir şey değil aslında, kaç parametre olduğuna bakalım,
print util.network_parameters(), 'tane degisken var'
1227204 tane degisken var
1 milyon küsur parametreli bir DYSA , yani potansiyel olarak her saniye en az bir milyon işlem yapılıyor demektir. Görünüşe göre hesap işliyor, TF bazı optimizasyonlar yapmış belki, ve mikroişlemciler yeterince hızlı. Teknoloji güzel şey.
CTC
Ses tanıma için bir diğer yaklaşım optik karakter tanıma yazısında görülen CTC kullanımı [4,5]. Alttaki kodun kullandığı veri [1]'de, yaklaşımın detayları [2]'de görülebilir. Bu ses verisi koca kelimeler, cümleleri içeriyor, çok daha uzun veriler bunlar, ve kayıp fonksiyonu artık basit, belli sayıda komut arasından seçim bazlı değil, büyük bir alfabeden gelen öğelerin yanyana gelişini kontrol ediyor.
from python_speech_features import mfcc
import numpy as np
import tensorflow as tf
from glob import glob
import time, re, os, random
import numpy as np
import librosa
num_epochs = 1000
num_hidden = 100
num_layers = 1
batch_size = 10
num_batches_per_epoch = 10
sample_rate=8000
num_features = 13
# Accounting the 0th index + space + blank label = 28 characters
num_classes = ord('z') - ord('a') + 1 + 1 + 1
print ('num_classes %d' % num_classes)
SPACE_TOKEN = '<space>'
SPACE_INDEX = 0
FIRST_INDEX = ord('a') - 1 # 0 is reserved to space
mfile = "/tmp/speech.ckpt"
def convert_inputs_to_ctc_format(audio, fs, target_text):
#print('convert_inputs_to_ctc_format target_text:' + target_text)
inputs = mfcc(audio, samplerate=fs, numcep=num_features)
# Transform in 3D array
train_inputs = np.asarray(inputs[np.newaxis, :])
train_inputs = (train_inputs - np.mean(train_inputs)) / np.std(train_inputs)
train_seq_len = [train_inputs.shape[1]]
# Get only the words between [a-z] and replace period for none
original = ' '.join(target_text.strip().lower().split(' ')).\
replace('.', '').\
replace('?', '').\
replace(',', '').\
replace("'", '').\
replace('!', '').\
replace('-', '')
#print('original:' + original)
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 = sparse_tuple_from([targets])
return train_inputs, train_targets, train_seq_len, original
def sparse_tuple_from(sequences, dtype=np.int32):
indices = []
values = []
for n, seq in enumerate(sequences):
indices.extend(zip([n] * len(seq), range(len(seq))))
values.extend(seq)
indices = np.asarray(indices, dtype=np.int64)
values = np.asarray(values, dtype=dtype)
shape = np.asarray([len(sequences),
np.asarray(indices).max(0)[1] + 1],
dtype=np.int64)
return indices, values, shape
def read_audio_from_filename(filename, sample_rate):
audio, _ = librosa.load(filename, sr=sample_rate, mono=True)
audio = audio.reshape(-1, 1)
return audio
def find_files(directory, pattern='.wav'):
"""Recursively finds all files matching the pattern."""
files = []
for root, directories, filenames in os.walk(directory):
for filename in filenames:
path = os.path.join(root,filename)
if pattern in path: files.append(path)
res = sorted(files)
return res
def run_ctc():
graph = tf.Graph()
with graph.as_default():
# e.g: log filter bank or MFCC features
# Has size [batch_size, max_step_size, num_features], but the
# batch_size and max_step_size can vary along each step
inputs = tf.placeholder(tf.float32, [None, None, num_features])
# Here we use sparse_placeholder that will generate a
# SparseTensor required by ctc_loss op.
targets = tf.sparse_placeholder(tf.int32)
# 1d array of size [batch_size]
seq_len = tf.placeholder(tf.int32, [None])
# Defining the cell
# Can be:
cell = tf.contrib.rnn.LSTMCell(num_hidden, state_is_tuple=True)
# Stacking rnn cells
stack = tf.contrib.rnn.MultiRNNCell([cell] * num_layers,
state_is_tuple=True)
# 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_time_steps = 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
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])
# 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(learning_rate=0.005,
momentum=0.9).minimize(cost)
# Option 2: tf.contrib.ctc.ctc_beam_search_decoder
# (it's slower but you'll get better results)
decoded, log_prob = 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))
files = find_files("/home/burak/Downloads/vctk-p225-small/wav48/p225")
with tf.Session(graph=graph) as session:
tf.global_variables_initializer().run()
saver = tf.train.Saver()
for curr_epoch in range(num_epochs):
train_cost = train_ler = 0
for batch in range(num_batches_per_epoch):
filename = random.choice(files)
txtfile = filename.replace("wav48","txt")
txtfile = txtfile.replace(".wav",".txt")
txt = open(txtfile).read()
audio = read_audio_from_filename(filename, sample_rate)
out = convert_inputs_to_ctc_format(audio,sample_rate,txt)
train_inputs, train_targets, train_seq_len, original = out
feed = {inputs: train_inputs,
targets: train_targets,
seq_len: train_seq_len}
batch_cost, _ = session.run([cost, optimizer], feed)
train_ler += session.run(ler, feed_dict=feed)
print 'batch_cost', batch_cost, 'train_ler', train_ler
# Decoding
d = session.run(decoded[0], feed_dict=feed)
str_decoded = ''.join([chr(x) for x in np.asarray(d[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('Original: %s' % original)
print('Decoded: %s' % str_decoded)
if curr_epoch % 10 == 0: saver.save(session, mfile)
if __name__ == '__main__':
run_ctc()
Kaynaklar
[1] Bayramlı, VCTK Ses Tanima Verisi, Konusmaci 225, https://drive.google.com/uc?export=view&id=1zK-mgG6Q8N8OuOGpexbxVkES3DuQhGOk
[2] Remy, Application of Connectionist Temporal Classification (CTC) for Speech Recognition,https://github.com/philipperemy/tensorflow-ctc-speech-recognition
[3] Graves, Supervised Sequence Labelling with Recurrent Neural Networks, https://www.cs.toronto.edu/~graves/preprint.pdf
[4] Graves, How to build a recognition system (Part 1): CTC Loss, https://docs.google.com/presentation/d/1AyLOecmW1k9cIbfexOT3dwoUU-Uu5UqlJZ0w3cxilkI
[5] Graves, How to build a recognition system (Part 2): CTC Loss, https://docs.google.com/presentation/d/12gYcPft9_4cxk2AD6Z6ZlJNa3wvZCW1ms31nhq51vMk
[6] Bayramlı, Ses Komut Verisi, https://drive.google.com/open?id=1BIGj3NtUZfSrXMaJ8hCqsz0UzS01MSrF
[7] Bayramlı, Bilgisayar Bilim, Uzun Kısa-Vade Hafıza Ağları
Yukarı