TensorFlow.js - التعرّف على الصوت باستخدام التعلّم القائم على النقل

1. مقدمة

في هذا الدرس التطبيقي حول الترميز، ستنشئ شبكة للتعرف على الصوت وستستخدمها للتحكم في شريط التمرير في المتصفح من خلال إصدار أصوات. سوف تستخدم TensorFlow.js، وهي مكتبة فعالة ومرنة لتعلّم الآلة بلغة JavaScript.

أولاً، عليك تحميل وتشغيل نموذج مدرّب مسبقًا يمكنه التعرّف على 20 طلبًا من الطلبات الصوتية. بعد ذلك، باستخدام الميكروفون، ستقوم بإنشاء وتدريب شبكة عصبية بسيطة تتعرّف على أصواتك وتجعل شريط التمرير يتحرك يسارًا أو يمينًا.

لن يتناول هذا الدرس التطبيقي حول الترميز النظرية وراء نماذج التعرّف على الصوت. وإذا أردت الاطّلاع على المزيد من المعلومات، يُرجى الاطّلاع على هذا الدليل التوجيهي.

لقد أنشأنا أيضًا مسردًا لمصطلحات تعلُّم الآلة الواردة في هذا الدرس التطبيقي حول الترميز.

ما ستتعرَّف عليه

  • كيفية تحميل نموذج مدرَّب مسبقًا للتعرّف على الأوامر الصوتية
  • طريقة تقديم توقّعات في الوقت الفعلي باستخدام الميكروفون
  • طريقة تدريب نموذج مخصّص للتعرّف على الصوت واستخدامه باستخدام ميكروفون المتصفح

لنبدأ الآن.

2. المتطلبات

لإكمال هذا الدرس التطبيقي حول الترميز، ستحتاج إلى:

  1. إصدار حديث من Chrome أو أي متصفّح حديث آخر
  2. محرّر نصوص يعمل محليًا على جهازك أو على الويب من خلال برامج مثل Codepen أو Glitch
  3. معرفة HTML وCSS وJavaScript وChrome DevTools (أو أدوات مطوري البرامج للمتصفحات المفضلة لديك).
  4. فهم نظري عالي المستوى للشبكات العصبية. إذا كنت بحاجة إلى مقدمة أو تنشيط للذاكرة، يمكنك مشاهدة هذا الفيديو من قناة 3blue1brown أو هذا الفيديو حول التعلم المتعمق بلغة JavaScript من إعداد "آشي كريشنان".

3- تحميل TensorFlow.js ونموذج الصوت

افتح "index.html" في محرِّر وأضِف هذا المحتوى:

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

تستورد العلامة <script> الأولى مكتبة TensorFlow.js، بينما تستورِد العلامة <script> الثانية نموذج أوامر الكلام المدرَّب مسبقًا. سيتم استخدام العلامة <div id="console"> لعرض ناتج النموذج.

4. التوقّع في الوقت الفعلي

بعد ذلك، افتح/أنشئ ملف index.js في محرِّر رموز، وأدرِج الرمز التالي:

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- اختبار التوقّع

تأكَّد من توفُّر ميكروفون على جهازك. وتجدر الإشارة إلى أنّ هذه الميزة سيعمل أيضًا على الهواتف الجوّالة. لتشغيل صفحة الويب، افتح index.html في متصفّح. إذا كنت تعمل من ملف محلي، يجب بدء تشغيل خادم ويب واستخدام http://localhost:port/ للوصول إلى الميكروفون.

لبدء خادم ويب بسيط على المنفذ 8000:

python -m SimpleHTTPServer

قد يستغرق تنزيل النموذج بعض الوقت، لذا يُرجى الانتظار. من المفترض أن تظهر كلمة أعلى الصفحة فور تحميل النموذج. تم تدريب النموذج على التعرّف على الأرقام من 0 إلى 9 وبعض الأوامر الإضافية مثل "يسار" و"يمين" و"نعم" و"لا"، وما إلى ذلك.

قُل إحدى هذه الكلمات. هل يفهم الكلمة بشكل صحيح؟ جرِّب استخدام probabilityThreshold التي تتحكّم في عدد مرات تنشيط النموذج، وتعني القيمة 0.75 أنّ النموذج سيتم تنشيطه عندما تكون متأكدًا بنسبة تزيد عن% 75 من أنّه يسمع كلمة معيّنة.

لمعرفة المزيد من المعلومات عن نموذج أوامر الكلام وواجهة برمجة التطبيقات الخاصة به، راجع README.md على جيت هب.

6- جمع البيانات

لنجعله ممتعًا، نستخدم أصواتًا قصيرة بدلاً من كلمات كاملة للتحكّم في شريط التمرير.

أنت بصدد تدريب نموذج للتعرّف على 3 أوامر مختلفة: "يسار" و"يمين". و"الضوضاء" مما سيجعل شريط التمرير يتحرك يسارًا أو يمينًا. التعرّف على "الضوضاء" (لا يلزم اتخاذ أي إجراء) أمر بالغ الأهمية في اكتشاف الكلام لأننا نريد أن يتفاعل شريط التمرير فقط عند إصدار الصوت الصحيح، وليس عندما نتحدث ونتحرك بشكل عام.

  1. نحتاج أولاً إلى جمع البيانات. أضِف واجهة مستخدم بسيطة إلى التطبيق من خلال إضافة ما يلي داخل العلامة <body> قبل <div id="console">:
<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. إضافة هذا إلى index.js:
// 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() من app():
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // predictWord() no longer called.
}

التبسيط

قد يكون هذا الرمز مربكًا في البداية، لذلك دعنا نحلله.

لقد أضفنا ثلاثة أزرار إلى واجهة المستخدم تسمى "Left" و"Right" و"Noise" (الضوضاء)، وذلك وفقًا للأوامر الثلاثة التي نريد أن يتعرف عليها نموذجنا. ويؤدي الضغط على هذه الأزرار إلى استدعاء دالة collect() المضافة حديثًا، ما يؤدي إلى إنشاء أمثلة تدريبية لنموذجنا.

تربط الدالة collect() دالة label بمخرجات recognizer.listen(). بما أنّ القيمة includeSpectrogram صحيحة،, يوفّر recognizer.listen() الرسم الطيفي الأولي (بيانات التردد) لمدة ثانية واحدة من الصوت، مقسّمًا إلى 43 إطارًا، وبالتالي تكون مدة كل إطار 23 ملّي ثانية تقريبًا من الصوت:

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

بما أنّنا نريد استخدام الأصوات القصيرة بدلاً من الكلمات للتحكم في شريط التمرير، نأخذ في الاعتبار آخر 3 إطارات فقط (حوالي 70 ملي ثانية):

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

ولتجنب المشكلات الرقمية، نعمل على تسوية البيانات للحصول على متوسط 0 وانحراف معياري يبلغ 1. في هذه الحالة، عادةً ما تكون قيم الرسم الطيفي أعدادًا سالبة كبيرة حول -100 وانحراف 10:

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

وأخيرًا، سيحتوي كل مثال تدريب على حقلَين:

  • label****: 0 و1 و2 للرمز "يسار" و"يمين" و"الضوضاء" على التوالي.
  • vals****: 696 رقمًا يتضمّن معلومات معدّل التكرار (الصورة الطيفية)

ونخزن كل البيانات في متغير examples:

examples.push({vals, label});

7. اختبار جمع البيانات

افتح index.html في متصفّح، وستظهر لك 3 أزرار تتوافق مع الأوامر الثلاثة. إذا كنت تعمل من ملف محلي، يجب بدء تشغيل خادم ويب واستخدام http://localhost:port/ للوصول إلى الميكروفون.

لبدء خادم ويب بسيط على المنفذ 8000:

python -m SimpleHTTPServer

لجمع أمثلة لكل طلب، عليك إصدار صوت متّسق بشكل متكرّر (أو متواصل) مع الضغط مع الاستمرار على كل زر لمدة تتراوح بين 3 و4 ثوانٍ. يجب عليك جمع ما يقرب من 150 مثالاً لكل تصنيف. على سبيل المثال، يمكننا التقاط أصابعك لكلمة "اليسار" أو الصفير لكلمة "يمين" والتبديل بين الصمت والتحدث بصوت "ضوضاء".

أثناء جمع المزيد من الأمثلة، من المفترض أن يظهر العدّاد الظاهر على الصفحة. يمكنك أيضًا فحص البيانات من خلال استدعاء console.log() على متغيّر examples في وحدة التحكّم. في هذه المرحلة، يكون الهدف هو اختبار عملية جمع البيانات. وسيتعين عليك لاحقًا إعادة جمع البيانات عند اختبار التطبيق بأكمله.

8. تدريب نموذج

  1. إضافة "قطار" بعد "الضوضاء" مباشرةً زر في النص الأساسي في index.html:
<br/><br/>
<button id="train" onclick="train()">Train</button>
  1. أضِف ما يلي إلى الرمز الحالي في index.js:
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. يمكنك طلب الرقم buildModel() عند تحميل التطبيق:
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // Add this line.
 buildModel();
}

عند إعادة تحميل التطبيق، ستظهر لك "قطار" جديد . يمكنك اختبار التدريب من خلال إعادة جمع البيانات والنقر على "تدريب"، أو يمكنك الانتظار حتى الخطوة 10 لاختبار التدريب إلى جانب التوقع.

الشرح

وعلى مستوى عالٍ، نتّخذ إجراءَين: تحدّد الدالة buildModel() بنية النموذج وتُستخدَم train() في تدريب النموذج باستخدام البيانات التي تم جمعها.

بنية النموذج

يتكون النموذج من 4 طبقات: طبقة التفافية تعالج البيانات الصوتية (يمثلها رسم بياني طيفي)، وطبقة تجميع قصوى، وطبقة مسطحة، وطبقة كثيفة تستند إلى الإجراءات الثلاثة التالية:

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'}));

شكل إدخال النموذج هو [NUM_FRAMES, 232, 1]، حيث يكون كل إطار 23 ملي ثانية من الصوت يحتوي على 232 رقمًا يتوافق مع ترددات مختلفة (تم اختيار الرقم 232 لأنه مقدار مجموعات الترددات اللازمة لتسجيل صوت الإنسان). في هذا الدرس التطبيقي حول الترميز، نستخدم عيّنات طولها 3 إطارات (عيّنات تبلغ 70 ملّي ثانية تقريبًا) لأنّنا نصدر أصواتًا بدلاً من نطق الكلمات الكاملة للتحكّم في شريط التمرير.

نجمع النموذج ليصبح جاهزًا للتدريب:

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

نستخدم مُحسِّن آدم، وهو مُحسِّن شائع يُستخدم في التعليم المتعمق، وcategoricalCrossEntropy للخسارة، وهي دالة الخسارة القياسية المستخدمة في التصنيف. باختصار، إنها تقيس مدى وجود الاحتمالات المتنبأ بها (احتمالية واحدة لكل فئة) عن وجود احتمالية 100% في الفئة الحقيقية، واحتمالية 0% في جميع الفئات الأخرى. نوفّر أيضًا accuracy كمقياس يجب مراقبته، ما سيمنحنا النسبة المئوية للأمثلة التي يصحّحها النموذج بعد كل فترة من فترات التدريب.

التدريب

يمر التدريب 10 مرات (فترات) فوق البيانات باستخدام حجم دفعة من 16 (معالجة 16 مثالاً في كل مرة) ويظهر الدقة الحالية في واجهة المستخدم:

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. تعديل شريط التمرير في الوقت الفعلي

والآن بعد أن تمكنا من تطبيق النموذج، دعونا نضيف رمزًا لإجراء التنبؤات في الوقت الفعلي ونحرك شريط التمرير. أضف هذا مباشرة بعد "القطار" في index.html:

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

وما يلي في index.js:

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
 });
}

الشرح

توقّعات في الوقت الفعلي

يستمع "listen()" إلى الميكروفون ويقدم توقّعات في الوقت الفعلي. ويشبه هذا الرمز إلى حد كبير طريقة collect() التي تعمل على ضبط الرسم الطيفي الأولي وإسقاط جميع الإطارات باستثناء NUM_FRAMES الأخيرة. الفرق الوحيد هو أننا نسمي أيضًا النموذج المدرَّب للحصول على توقع:

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

ناتج model.predict(input) هو موتر الشكل [1, numClasses] الذي يمثل توزيع الاحتمالية على عدد الفئات. وبشكل أكثر بساطة، هذه ليست سوى مجموعة من درجات الثقة لكل فئة من فئات الناتج المحتملة التي يبلغ مجموعها 1. يحتوي Tensor على بُعد خارجي 1 لأن هذا هو حجم الدُفعة (مثال واحد).

لتحويل توزيع الاحتمالية إلى عدد صحيح واحد يمثل الفئة الأكثر احتمالاً، نسمي الدالة probs.argMax(1)التي تعرض فهرس الفئة الأعلى احتمالية. نمرر "1" كمعلمة المحور لأننا نريد حساب argMax على البُعد الأخير، numClasses.

تعديل شريط التمرير

تقلل moveSlider() قيمة شريط التمرير إذا كان التصنيف 0 ("يسار") ، ويزيدها إذا كان التصنيف 1 ("يمين") ويتجاهل إذا كان التصنيف 2 ("الضوضاء").

التخلص من المتوترات

لتنظيف ذاكرة وحدة معالجة الرسومات، من المهم بالنسبة لنا استدعاء tf.dispose() يدويًا إلى "Tf.dispose" () فوق الإخراج. كبديل للدالة tf.dispose() اليدوية، يتم دمج استدعاءات الدوال في tf.tidy()، ولكن لا يمكن استخدام هذه الدالة مع الدوال غير المتزامنة.

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

10. اختبار التطبيق النهائي

افتح index.html في المتصفّح واجمع البيانات كما فعلت في القسم السابق باستخدام الأزرار الثلاثة المقابلة للأوامر الثلاثة. لا تنسَ الضغط مع الاستمرار على كل زر لمدة تتراوح بين 3 و4 ثوانٍ أثناء جمع البيانات.

بعد جمع الأمثلة، اضغط على الزر "تدريب". وسيؤدي ذلك إلى بدء تدريب النموذج، وينبغي أن ترى دقة النموذج أكثر من 90%. وإذا لم تحقّق أداءً جيدًا في النموذج، حاوِل جمع المزيد من البيانات.

بعد انتهاء التدريب، اضغط على الزر الاستماع لإجراء توقّعات من الميكروفون والتحكّم في شريط التمرير.

يمكنك الاطّلاع على المزيد من البرامج التعليمية على http://js.tensorflow.org/.