TensorFlow.js - تشخیص صدا با استفاده از یادگیری انتقال

1. مقدمه

در این کد لبه شما یک شبکه تشخیص صدا می سازید و از آن برای کنترل یک نوار لغزنده در مرورگر با ایجاد صدا استفاده می کنید. شما از TensorFlow.js، یک کتابخانه یادگیری ماشینی قدرتمند و انعطاف پذیر برای جاوا اسکریپت استفاده خواهید کرد.

ابتدا یک مدل از پیش آموزش دیده را بارگیری و اجرا می کنید که می تواند 20 فرمان گفتاری را تشخیص دهد. سپس با استفاده از میکروفون خود، یک شبکه عصبی ساده می‌سازید و آموزش می‌دهید که صداهای شما را تشخیص می‌دهد و باعث می‌شود لغزنده به چپ یا راست برود.

این کد لبه تئوری مدل های تشخیص صدا را بررسی نمی کند. اگر در مورد آن کنجکاو هستید، این آموزش را بررسی کنید.

ما همچنین واژه نامه ای از اصطلاحات یادگیری ماشینی را ایجاد کرده ایم که در این کد لبه پیدا می کنید.

چیزی که یاد خواهید گرفت

  • نحوه بارگذاری یک مدل تشخیص دستور گفتار از پیش آموزش دیده
  • چگونه با استفاده از میکروفون پیش بینی های بلادرنگ انجام دهیم
  • نحوه آموزش و استفاده از یک مدل تشخیص صدای سفارشی با استفاده از میکروفون مرورگر

پس بیایید شروع کنیم.

2. الزامات

برای تکمیل این کد لبه، شما نیاز دارید:

  1. نسخه اخیر کروم یا مرورگر مدرن دیگری.
  2. یک ویرایشگر متن که به صورت محلی در دستگاه شما یا در وب از طریق چیزی مانند Codepen یا Glitch اجرا می شود.
  3. دانش HTML، CSS، جاوا اسکریپت و ابزارهای توسعه دهنده کروم (یا ابزارهای توسعه دهنده مرورگرهای دلخواه شما).
  4. درک مفهومی سطح بالا از شبکه های عصبی اگر به یک مقدمه یا تجدید نظر نیاز دارید، این ویدیو را توسط 3blue1brown یا این ویدیو را در آموزش عمیق در جاوا اسکریپت توسط Ashi Krishnan تماشا کنید.

3. TensorFlow.js و مدل Audio را بارگیری کنید

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٪ مطمئن باشد که یک کلمه معین را می شنود شلیک می شود.

برای کسب اطلاعات بیشتر در مورد مدل دستورات گفتاری و API آن، به README.md در Github مراجعه کنید.

6. جمع آوری داده ها

برای اینکه آن را سرگرم کننده کنید، بیایید از صداهای کوتاه به جای کلمات کامل برای کنترل لغزنده استفاده کنیم!

شما قصد دارید مدلی را آموزش دهید تا 3 فرمان مختلف را تشخیص دهد: "Left"، "Right" و "Noise" که باعث می شود لغزنده به چپ یا راست حرکت کند. تشخیص "نویز" (بدون نیاز به اقدام) در تشخیص گفتار بسیار مهم است، زیرا ما می خواهیم لغزنده فقط زمانی واکنش نشان دهد که صدای مناسب را تولید کنیم، نه زمانی که به طور کلی صحبت می کنیم و در حال حرکت هستیم.

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

شکستن آن

این کد ممکن است در ابتدا بسیار زیاد باشد، پس بیایید آن را تجزیه کنیم.

ما سه دکمه به رابط کاربری خود اضافه کرده‌ایم که دارای برچسب‌های "چپ"، "راست" و "نویز" هستند، که مطابق با سه دستوری است که می‌خواهیم مدل ما تشخیص دهد. با فشار دادن این دکمه‌ها، تابع collect() جدید اضافه شده ما فراخوانی می‌شود که نمونه‌های آموزشی برای مدل ما ایجاد می‌کند.

collect() یک label با خروجی recognizer.listen() مرتبط می کند. از آنجایی که includeSpectrogram درست است , recognizer.listen() طیف‌گرام خام (داده‌های فرکانس) را برای 1 ثانیه صدا به 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);

در نهایت، هر نمونه آموزشی دارای 2 فیلد خواهد بود:

  • label ****: 0، 1، و 2 به ترتیب برای "چپ"، "راست" و "نویز".
  • vals ****: 696 عدد حاوی اطلاعات فرکانس (طیف‌گرام)

و همه داده ها را در متغیر examples ذخیره می کنیم:

examples.push({vals, label});

7. آزمون جمع آوری داده ها

index.html را در یک مرورگر باز کنید، و باید 3 دکمه مربوط به 3 دستور را مشاهده کنید. اگر از یک فایل محلی کار می کنید، برای دسترسی به میکروفون باید یک وب سرور راه اندازی کنید و از http://localhost:port/ استفاده کنید.

برای راه اندازی یک وب سرور ساده در پورت 8000:

python -m SimpleHTTPServer

برای جمع‌آوری مثال‌هایی برای هر فرمان، یک صدای ثابت را به طور مکرر (یا پیوسته) در حالی که هر دکمه را به مدت 3-4 ثانیه فشار داده و نگه دارید، ایجاد کنید. شما باید 150 نمونه برای هر برچسب جمع آوری کنید. برای مثال، می‌توانیم برای «چپ» انگشتان خود را بچسبانیم، برای «راست» سوت بزنیم و برای «صدا» به طور متناوب بین سکوت و صحبت کردن صحبت کنیم.

همانطور که نمونه های بیشتری را جمع آوری می کنید، شمارنده نشان داده شده در صفحه باید بالا برود. با فراخوانی console.log() روی متغیر examples موجود در کنسول، می توانید داده ها را نیز بررسی کنید. در این مرحله هدف آزمایش فرآیند جمع آوری داده ها است. بعداً هنگام آزمایش کل برنامه، داده ها را دوباره جمع آوری خواهید کرد.

8. یک مدل آموزش دهید

  1. یک دکمه Train درست بعد از دکمه Noise در بدنه در 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 لایه است: یک لایه کانولوشن که داده‌های صوتی را پردازش می‌کند (به صورت طیف‌گرام نشان داده می‌شود)، یک لایه حداکثر، یک لایه مسطح، و یک لایه متراکم که به 3 عملکرد نقشه می‌دهد:

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

ما از بهینه ساز Adam ، یک بهینه ساز معمولی که در یادگیری عمیق استفاده می شود، و categoricalCrossEntropy برای ضرر، تابع ضرر استاندارد که برای طبقه بندی استفاده می شود، استفاده می کنیم. به طور خلاصه، اندازه‌گیری می‌کند که احتمالات پیش‌بینی‌شده (یک احتمال در هر کلاس) تا 100٪ احتمال در کلاس واقعی، و احتمال 0٪ برای همه کلاس‌های دیگر چقدر فاصله دارد. ما همچنین accuracy به‌عنوان معیاری برای نظارت ارائه می‌کنیم، که درصد نمونه‌هایی را که مدل پس از هر دوره آموزشی درست می‌شود به ما می‌دهد.

آموزش

آموزش 10 بار (دوران) روی داده ها با استفاده از اندازه دسته ای 16 (پردازش 16 نمونه در یک زمان) انجام می شود و دقت فعلی را در UI نشان می دهد:

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 است.

برای تبدیل توزیع احتمال به یک عدد صحیح که محتمل‌ترین کلاس را نشان می‌دهد، probs.argMax(1) را فراخوانی می‌کنیم که شاخص کلاس را با بالاترین احتمال برمی‌گرداند. ما یک "1" را به عنوان پارامتر محور ارسال می کنیم زیرا می خواهیم argMax بر روی آخرین بعد، numClasses محاسبه کنیم.

در حال به روز رسانی نوار لغزنده

moveSlider() مقدار لغزنده را در صورتی که برچسب 0 باشد ("چپ") کاهش می دهد، اگر برچسب 1 باشد ("راست") آن را افزایش می دهد و اگر برچسب 2 باشد ("صدا") آن را نادیده می گیرد.

دفع تانسورها

برای پاکسازی حافظه GPU، مهم است که به صورت دستی ()tf.dispose را در تانسورهای خروجی فراخوانی کنیم. جایگزین tf.dispose() دستی، قرار دادن فراخوانی های تابع در یک tf.tidy() است، اما این نمی تواند با توابع async استفاده شود.

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

10. برنامه نهایی را تست کنید

index.html را در مرورگر خود باز کنید و مانند قسمت قبل با 3 دکمه مربوط به 3 دستور، داده ها را جمع آوری کنید. به یاد داشته باشید که هنگام جمع آوری داده ها، هر دکمه را به مدت 3-4 ثانیه فشار داده و نگه دارید .

پس از جمع آوری نمونه ها، دکمه "قطار" را فشار دهید. با این کار آموزش مدل شروع می شود و باید دقت مدل را بالای 90 درصد ببینید. اگر به عملکرد مدل خوبی نرسیدید، سعی کنید داده های بیشتری را جمع آوری کنید.

پس از اتمام آموزش، دکمه "Listen" را فشار دهید تا از میکروفون پیش بینی کنید و نوار لغزنده را کنترل کنید!

آموزش های بیشتر را در http://js.tensorflow.org/ ببینید.