TensorFlow.js - Aktarım öğrenmeyi kullanarak ses tanıma

1. Giriş

Bu codelab'de, ses tanıma ağı oluşturacak ve ses çıkararak tarayıcıdaki bir kaydırma çubuğunu kontrol etmek için bu ağı kullanacaksınız. JavaScript için güçlü ve esnek bir makine öğrenimi kitaplığı olan TensorFlow.js'yi kullanacaksınız.

İlk olarak, 20 konuşma komutunu tanıyabilen bir önceden eğitilmiş modeli yükleyip çalıştıracaksınız. Ardından, mikrofonunuzu kullanarak seslerinizi tanıyan ve kaydırma çubuğunu sola veya sağa hareket ettiren basit bir sinir ağı oluşturup eğitirsiniz.

Bu codelab'de ses tanıma modellerinin teorisi ele alınmayacaktır. Bu konuda daha fazla bilgi edinmek istiyorsanız bu eğitime göz atın.

Bu Codelab'de bulabileceğiniz makine öğrenimi terimlerinin yer aldığı bir sözlük de oluşturduk.

Neler öğreneceksiniz?

  • Önceden eğitilmiş bir konuşma komutu tanıma modelini yükleme
  • Mikrofonu kullanarak anlık tahminler yapma
  • Tarayıcı mikrofonunu kullanarak özel ses tanıma modelini eğitme ve kullanma

Artık başlayabiliriz.

2. Şartlar

Bu codelab'i tamamlamak için şunlar gerekir:

  1. Chrome'un veya başka bir modern tarayıcının son sürümü
  2. Makinenizde yerel olarak veya web'de Codepen ya da Glitch gibi bir araç üzerinden çalışan bir metin düzenleyici.
  3. HTML, CSS, JavaScript ve Chrome Geliştirici Araçları (veya tercih ettiğiniz tarayıcıların geliştirici araçları) hakkında bilgi sahibi olmak
  4. Nöral ağlar hakkında üst düzey kavramsal bilgi Giriş veya hatırlatma için 3blue1brown'un bu videosunu ya da Ashi Krishnan'ın JavaScript'te Derin Öğrenme videosunu izleyebilirsiniz.

3. TensorFlow.js ve Audio modelini yükleme

index.html dosyasını bir düzenleyicide açıp şu içeriği ekleyin:

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/speech-commands"></script>
  </head>
  <body>
    <div id="console"></div>
    <script src="index.js"></script>
  </body>
</html>

İlk <script> etiketi TensorFlow.js kitaplığını, ikinci <script> etiketi ise önceden eğitilmiş Konuşma Komutları modelini içe aktarır. Modelin çıkışını göstermek için <div id="console"> etiketi kullanılır.

4. Anlık tahminler

Ardından, bir kod düzenleyicide index.js dosyasını açın/oluşturun ve aşağıdaki kodu ekleyin:

let recognizer;

function predictWord() {
 // Array of words that the recognizer is trained to recognize.
 const words = recognizer.wordLabels();
 recognizer.listen(({scores}) => {
   // Turn scores into a list of (score,word) pairs.
   scores = Array.from(scores).map((s, i) => ({score: s, word: words[i]}));
   // Find the most probable word.
   scores.sort((s1, s2) => s2.score - s1.score);
   document.querySelector('#console').textContent = scores[0].word;
 }, {probabilityThreshold: 0.75});
}

async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 predictWord();
}

app();

5. Tahmini test etme

Cihazınızda mikrofon olduğundan emin olun. Bu işlemin cep telefonunda da çalışacağını unutmayın. Web sayfasını çalıştırmak için tarayıcıda index.html dosyasını açın. Yerel bir dosyadan çalışıyorsanız mikrofona erişmek için bir web sunucusu başlatmanız ve http://localhost:port/ kullanmanız gerekir.

8000 numaralı bağlantı noktasında basit bir web sunucusu başlatmak için:

python -m SimpleHTTPServer

Modelin indirilmesi biraz zaman alabilir. Lütfen bekleyin. Model yüklendiğinde sayfanın üst kısmında bir kelime görürsünüz. Model, 0-9 arasındaki sayıları ve "sol", "sağ", "evet", "hayır" gibi birkaç ek komutu tanıyacak şekilde eğitilmiştir.

Bu kelimelerden birini söyleyin. Kelimenizi doğru şekilde alıyor mu? Modelin ne sıklıkta tetikleneceğini kontrol eden probabilityThreshold ile oynayın.0,75, modelin belirli bir kelimeyi duyduğundan% 75'ten fazla emin olduğunda tetikleneceği anlamına gelir.

Konuşma Komutları modeli ve API'si hakkında daha fazla bilgi edinmek için Github'daki README.md dosyasına bakın.

6. Veri toplama

Eğlenceli hale getirmek için kaydırma çubuğunu kontrol ederken kelimeler yerine kısa sesler kullanalım.

Kaydırma çubuğunu sola veya sağa hareket ettirecek 3 farklı komutu ("Sol", "Sağ" ve "Gürültü") tanıyacak bir model eğiteceksiniz. "Gürültü"nün tanınması (işlem yapılması gerekmez), konuşma algılamada çok önemlidir. Çünkü kaydırma çubuğunun yalnızca doğru sesi çıkardığımızda tepki vermesini, genel olarak konuşup hareket ettiğimizde tepki vermemesini isteriz.

  1. Öncelikle veri toplamamız gerekir. <body> etiketinin içine <div id="console"> etiketinden önce aşağıdaki kodu ekleyerek uygulamaya basit bir kullanıcı arayüzü ekleyin:
<button id="left" onmousedown="collect(0)" onmouseup="collect(null)">Left</button>
<button id="right" onmousedown="collect(1)" onmouseup="collect(null)">Right</button>
<button id="noise" onmousedown="collect(2)" onmouseup="collect(null)">Noise</button>
  1. Şunu index.js alanına ekleyin:
// One frame is ~23ms of audio.
const NUM_FRAMES = 3;
let examples = [];

function collect(label) {
 if (recognizer.isListening()) {
   return recognizer.stopListening();
 }
 if (label == null) {
   return;
 }
 recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
   let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
   examples.push({vals, label});
   document.querySelector('#console').textContent =
       `${examples.length} examples collected`;
 }, {
   overlapFactor: 0.999,
   includeSpectrogram: true,
   invokeCallbackOnNoiseAndUnknown: true
 });
}

function normalize(x) {
 const mean = -100;
 const std = 10;
 return x.map(x => (x - mean) / std);
}
  1. predictWord() öğesini app() öğesinden kaldırma:
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // predictWord() no longer called.
}

Dans pistinde

Bu kod ilk başta kafa karıştırıcı olabilir. Bu nedenle, kodu parçalara ayıralım.

Kullanıcı arayüzümüze, modelimizin tanımasını istediğimiz üç komuta karşılık gelen "Sol", "Sağ" ve "Gürültü" etiketli üç düğme ekledik. Bu düğmelere basıldığında, modelimiz için eğitim örnekleri oluşturan yeni eklenen collect() işlevimiz çağrılır.

collect(), label ile recognizer.listen() çıkışını ilişkilendirir. includeSpectrogram doğru olduğundan, recognizer.listen(), 1 saniyelik sesin ham spektrogramını (frekans verileri) 43 kareye bölünmüş şekilde verir. Bu nedenle, her kare yaklaşık 23 ms'lik ses içerir:

recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
...
}, {includeSpectrogram: true});

Kaydırma çubuğunu kontrol etmek için kelimeler yerine kısa sesler kullanmak istediğimizden yalnızca son 3 kareyi (~70 ms) dikkate alıyoruz:

let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));

Sayısal sorunları önlemek için verileri 0 ortalamaya ve 1 standart sapmaya sahip olacak şekilde normalleştiririz. Bu durumda, spektrogram değerleri genellikle -100 civarında büyük negatif sayılar ve 10 sapma olur:

const mean = -100;
const std = 10;
return x.map(x => (x - mean) / std);

Son olarak, her eğitim örneğinde 2 alan bulunur:

  • label****: Sırasıyla "Sol", "Sağ" ve "Gürültü" için 0, 1 ve 2.
  • vals****: Frekans bilgilerini (spektrogram) içeren 696 sayı

ve tüm verileri examples değişkeninde saklarız:

examples.push({vals, label});

7. Veri toplamayı test etme

index.html dosyasını bir tarayıcıda açtığınızda 3 komuta karşılık gelen 3 düğme görürsünüz. Yerel bir dosyadan çalışıyorsanız mikrofona erişmek için bir web sunucusu başlatmanız ve http://localhost:port/ kullanmanız gerekir.

8000 numaralı bağlantı noktasında basit bir web sunucusu başlatmak için:

python -m SimpleHTTPServer

Her komut için örnek toplamak üzere, her düğmeyi 3-4 saniye boyunca basılı tutarken sürekli olarak aynı sesi çıkarın. Her etiket için yaklaşık 150 örnek toplamanız gerekir. Örneğin, "Sol" için parmaklarımızı şıklatabilir, "Sağ" için ıslık çalabilir ve "Gürültü" için sessizlik ile konuşma arasında geçiş yapabiliriz.

Daha fazla örnek topladıkça sayfada gösterilen sayaç artar. Konsoldaki examples değişkeninde console.log() işlevini çağırarak verileri de inceleyebilirsiniz. Bu noktada amaç, veri toplama sürecini test etmektir. Daha sonra uygulamanın tamamını test ederken verileri yeniden toplarsınız.

8. Model eğitin

  1. index.html dosyasının gövdesinde, "Gürültü" düğmesinin hemen ardından bir "Eğit" düğmesi ekleyin:
<br/><br/>
<button id="train" onclick="train()">Train</button>
  1. index.js dosyasındaki mevcut koda aşağıdakileri ekleyin:
const INPUT_SHAPE = [NUM_FRAMES, 232, 1];
let model;

async function train() {
 toggleButtons(false);
 const ys = tf.oneHot(examples.map(e => e.label), 3);
 const xsShape = [examples.length, ...INPUT_SHAPE];
 const xs = tf.tensor(flatten(examples.map(e => e.vals)), xsShape);

 await model.fit(xs, ys, {
   batchSize: 16,
   epochs: 10,
   callbacks: {
     onEpochEnd: (epoch, logs) => {
       document.querySelector('#console').textContent =
           `Accuracy: ${(logs.acc * 100).toFixed(1)}% Epoch: ${epoch + 1}`;
     }
   }
 });
 tf.dispose([xs, ys]);
 toggleButtons(true);
}

function buildModel() {
 model = tf.sequential();
 model.add(tf.layers.depthwiseConv2d({
   depthMultiplier: 8,
   kernelSize: [NUM_FRAMES, 3],
   activation: 'relu',
   inputShape: INPUT_SHAPE
 }));
 model.add(tf.layers.maxPooling2d({poolSize: [1, 2], strides: [2, 2]}));
 model.add(tf.layers.flatten());
 model.add(tf.layers.dense({units: 3, activation: 'softmax'}));
 const optimizer = tf.train.adam(0.01);
 model.compile({
   optimizer,
   loss: 'categoricalCrossentropy',
   metrics: ['accuracy']
 });
}

function toggleButtons(enable) {
 document.querySelectorAll('button').forEach(b => b.disabled = !enable);
}

function flatten(tensors) {
 const size = tensors[0].length;
 const result = new Float32Array(tensors.length * size);
 tensors.forEach((arr, i) => result.set(arr, i * size));
 return result;
}
  1. Uygulama yüklendiğinde buildModel() işlevini çağırın:
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // Add this line.
 buildModel();
}

Bu noktada uygulamayı yenilerseniz yeni bir "Eğit" düğmesi görürsünüz. Verileri yeniden toplayıp "Eğit"i tıklayarak eğitimi test edebilir veya eğitimle birlikte tahmini test etmek için 10. adıma kadar bekleyebilirsiniz.

Ayrıntılı açıklama

Genel olarak iki işlem yapıyoruz: buildModel() model mimarisini tanımlıyor ve train() toplanan verileri kullanarak modeli eğitiyor.

Model mimarisi

Model 4 katmandan oluşur: ses verilerini (spektrogram olarak gösterilir) işleyen bir evrişimli katman, bir maksimum havuz katmanı, bir düzleştirme katmanı ve 3 eylemi eşleyen bir yoğun katman:

model = tf.sequential();
 model.add(tf.layers.depthwiseConv2d({
   depthMultiplier: 8,
   kernelSize: [NUM_FRAMES, 3],
   activation: 'relu',
   inputShape: INPUT_SHAPE
 }));
 model.add(tf.layers.maxPooling2d({poolSize: [1, 2], strides: [2, 2]}));
 model.add(tf.layers.flatten());
 model.add(tf.layers.dense({units: 3, activation: 'softmax'}));

Modelin giriş şekli [NUM_FRAMES, 232, 1]'dır. Burada her kare, farklı frekanslara karşılık gelen 232 sayı içeren 23 ms'lik bir ses bölümüdür (232 sayısı, insan sesini yakalamak için gereken frekans gruplarının sayısı olduğundan seçilmiştir). Bu codelab'de, kaydırma çubuğunu kontrol etmek için kelimeler söylemek yerine sesler çıkardığımızdan 3 kare uzunluğunda (~70 ms) örnekler kullanıyoruz.

Modelimizi derleyerek eğitime hazır hale getiriyoruz:

const optimizer = tf.train.adam(0.01);
 model.compile({
   optimizer,
   loss: 'categoricalCrossentropy',
   metrics: ['accuracy']
 });

Derin öğrenmede yaygın olarak kullanılan bir optimize edici olan Adam optimize ediciyi ve kayıp için sınıflandırmada kullanılan standart kayıp işlevi olan categoricalCrossEntropy'ı kullanıyoruz. Kısacası, tahmin edilen olasılıkların (sınıf başına bir olasılık) gerçek sınıfta% 100 olasılığa ve diğer tüm sınıflarda% 0 olasılığa sahip olmaktan ne kadar uzak olduğunu ölçer. Ayrıca, izlenecek bir metrik olarak accuracy da sunuyoruz. Bu metrik, modelin eğitimin her döneminden sonra doğru yanıtladığı örneklerin yüzdesini gösterir.

Eğitim

Eğitim, 16 grup boyutu (aynı anda 16 örnek işleme) kullanılarak veriler üzerinde 10 kez (dönem) gerçekleştirilir ve mevcut doğruluk kullanıcı arayüzünde gösterilir:

await model.fit(xs, ys, {
   batchSize: 16,
   epochs: 10,
   callbacks: {
     onEpochEnd: (epoch, logs) => {
       document.querySelector('#console').textContent =
           `Accuracy: ${(logs.acc * 100).toFixed(1)}% Epoch: ${epoch + 1}`;
     }
   }
 });

9. Kaydırma çubuğunu anlık olarak güncelleme

Modelimizi eğitebildiğimize göre, şimdi gerçek zamanlı tahminler yapacak ve kaydırma çubuğunu hareket ettirecek kodu ekleyelim. Bu kodu index.html dosyasındaki "Train" düğmesinin hemen ardından ekleyin:

<br/><br/>
<button id="listen" onclick="listen()">Listen</button>
<input type="range" id="output" min="0" max="10" step="0.1">

Ayrıca index.js dosyasına aşağıdaki kodu ekleyin:

async function moveSlider(labelTensor) {
 const label = (await labelTensor.data())[0];
 document.getElementById('console').textContent = label;
 if (label == 2) {
   return;
 }
 let delta = 0.1;
 const prevValue = +document.getElementById('output').value;
 document.getElementById('output').value =
     prevValue + (label === 0 ? -delta : delta);
}

function listen() {
 if (recognizer.isListening()) {
   recognizer.stopListening();
   toggleButtons(true);
   document.getElementById('listen').textContent = 'Listen';
   return;
 }
 toggleButtons(false);
 document.getElementById('listen').textContent = 'Stop';
 document.getElementById('listen').disabled = false;

 recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
   const vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
   const input = tf.tensor(vals, [1, ...INPUT_SHAPE]);
   const probs = model.predict(input);
   const predLabel = probs.argMax(1);
   await moveSlider(predLabel);
   tf.dispose([input, probs, predLabel]);
 }, {
   overlapFactor: 0.999,
   includeSpectrogram: true,
   invokeCallbackOnNoiseAndUnknown: true
 });
}

Ayrıntılı açıklama

Anlık tahmin

listen(), mikrofondaki sesi dinler ve anlık tahminler yapar. Bu kod, ham spektrogramı normalleştiren ve son NUM_FRAMES kare hariç tüm kareleri bırakan collect() yöntemine çok benzer. Tek fark, tahmin almak için eğitilmiş modeli de çağırmamızdır:

const probs = model.predict(input);
const predLabel = probs.argMax(1);
await moveSlider(predLabel);

model.predict(input) işlevinin çıkışı, sınıf sayısı üzerinde bir olasılık dağılımını temsil eden [1, numClasses] şeklindeki bir tensördür. Daha basit bir ifadeyle bu, olası her çıkış sınıfı için güven düzeylerinin toplamının 1 olduğu bir kümedir. Tensor'un dış boyutu 1'dir. Bunun nedeni, toplu işin (tek bir örnek) boyutunun bu olmasıdır.

Olasılık dağılımını en olası sınıfı temsil eden tek bir tam sayıya dönüştürmek için probs.argMax(1) işlevini çağırırız. Bu işlev, en yüksek olasılığa sahip sınıf dizinini döndürür. argMax değerini son boyut olan numClasses üzerinden hesaplamak istediğimiz için eksen parametresi olarak "1" değerini iletiyoruz.

Kaydırma çubuğunu güncelleme

Etiket 0 ("Sol") ise moveSlider() kaydırma çubuğunun değerini azaltır, etiket 1 ("Sağ") ise artırır ve etiket 2 ("Gürültü") ise yoksayar.

Tensörleri silme

GPU belleğini temizlemek için çıkış tensörlerinde tf.dispose() işlevini manuel olarak çağırmamız gerekir. Manuel tf.dispose() yerine işlev çağrılarını tf.tidy() içine sarmalamak da mümkündür ancak bu yöntem, eş zamansız işlevlerle kullanılamaz.

   tf.dispose([input, probs, predLabel]);

10. Son uygulamayı test etme

Tarayıcınızda index.html dosyasını açın ve önceki bölümde 3 komuta karşılık gelen 3 düğmeyle yaptığınız gibi veri toplayın. Veri toplarken her düğmeyi 3-4 saniye basılı tutmayı unutmayın.

Örnekleri topladıktan sonra "Eğit" düğmesine basın. Bu işlem, modelin eğitilmesini başlatır ve modelin doğruluğunun %90'ın üzerine çıktığını görürsünüz. İyi bir model performansı elde edemezseniz daha fazla veri toplamayı deneyin.

Eğitim tamamlandıktan sonra mikrofondan tahminler yapmak ve kaydırma çubuğunu kontrol etmek için "Dinle" düğmesine basın.

http://js.tensorflow.org/ adresinde daha fazla eğitim bulabilirsiniz.