۱. مقدمه
در این آزمایشگاه کد، شما یک شبکه تشخیص صدا خواهید ساخت و از آن برای کنترل یک اسلایدر در مرورگر با ایجاد صدا استفاده خواهید کرد. شما از TensorFlow.js، یک کتابخانه قدرتمند و انعطافپذیر یادگیری ماشین برای جاوا اسکریپت، استفاده خواهید کرد.
ابتدا، یک مدل از پیش آموزشدیده را بارگذاری و اجرا میکنید که میتواند 20 دستور گفتاری را تشخیص دهد. سپس با استفاده از میکروفون خود، یک شبکه عصبی ساده ایجاد و آموزش میدهید که صداهای شما را تشخیص میدهد و باعث میشود اسلایدر به چپ یا راست حرکت کند.
این آزمایشگاه کد به بررسی تئوری مدلهای تشخیص صدا نمیپردازد . اگر در این مورد کنجکاو هستید، این آموزش را بررسی کنید.
ما همچنین واژهنامهای از اصطلاحات یادگیری ماشین ایجاد کردهایم که میتوانید در این آزمایشگاه کد پیدا کنید.
آنچه یاد خواهید گرفت
- نحوه بارگذاری یک مدل تشخیص فرمان گفتار از پیش آموزش دیده
- نحوه پیشبینیهای بلادرنگ با استفاده از میکروفون
- نحوه آموزش و استفاده از یک مدل تشخیص صدای سفارشی با استفاده از میکروفون مرورگر
پس بیایید شروع کنیم.
۲. الزامات
برای تکمیل این آزمایشگاه کد، به موارد زیر نیاز دارید:
- نسخه جدید کروم یا یک مرورگر مدرن دیگر.
- یک ویرایشگر متن، چه به صورت محلی روی دستگاه شما اجرا شود و چه از طریق چیزی مانند Codepen یا Glitch روی وب اجرا شود.
- آشنایی با HTML، CSS، جاوا اسکریپت و ابزارهای توسعه کروم (یا ابزارهای توسعه مرورگر مورد نظر شما).
- درک مفهومی سطح بالا از شبکههای عصبی. اگر به مقدمه یا یادآوری نیاز دارید، تماشای این ویدیو از 3blue1brown یا این ویدیو در مورد یادگیری عمیق در جاوا اسکریپت از Ashi Krishnan را در نظر بگیرید.
۳. بارگذاری 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"> برای نمایش خروجی مدل استفاده خواهد شد.
۴. پیشبینی در لحظه
سپس، فایل 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();
۵. پیشبینی را آزمایش کنید
مطمئن شوید که دستگاه شما میکروفون دارد. شایان ذکر است که این روش روی تلفن همراه نیز کار میکند! برای اجرای صفحه وب، فایل index.html را در مرورگر باز کنید. اگر از یک فایل محلی استفاده میکنید، برای دسترسی به میکروفون باید یک وب سرور راهاندازی کنید و http://localhost:port/ استفاده کنید.
برای شروع یک وب سرور ساده روی پورت ۸۰۰۰:
python -m SimpleHTTPServer
ممکن است دانلود مدل کمی طول بکشد، بنابراین لطفاً صبور باشید. به محض بارگذاری مدل، باید کلمهای را در بالای صفحه مشاهده کنید. این مدل طوری آموزش داده شده است که اعداد ۰ تا ۹ و چند دستور اضافی مانند "چپ"، "راست"، "بله"، "خیر" و غیره را تشخیص دهد.
یکی از آن کلمات را بگویید. آیا کلمه شما را درست تشخیص میدهد؟ با probabilityThreshold که تعداد دفعات فعال شدن مدل را کنترل میکند، بازی کنید - 0.75 به این معنی است که مدل زمانی فعال میشود که بیش از 75٪ از شنیدن کلمه داده شده مطمئن باشد.
برای کسب اطلاعات بیشتر در مورد مدل دستورات گفتاری و API آن، به فایل README.md در گیتهاب مراجعه کنید.
۶. جمعآوری دادهها
برای اینکه بازی جذابتر شود، بیایید به جای کلمات کامل از صداهای کوتاه برای کنترل اسلایدر استفاده کنیم!
شما قرار است مدلی را آموزش دهید تا ۳ دستور مختلف را تشخیص دهد: «چپ»، «راست» و «نویز» که باعث میشود اسلایدر به چپ یا راست حرکت کند. تشخیص «نویز» (بدون نیاز به انجام کاری) در تشخیص گفتار بسیار مهم است، زیرا ما میخواهیم اسلایدر فقط زمانی که صدای درست را تولید میکنیم واکنش نشان دهد، و نه زمانی که به طور کلی صحبت میکنیم و حرکت میکنیم.
- ابتدا باید دادهها را جمعآوری کنیم. با اضافه کردن این کد درون تگ
<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>
- این را به
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);
}
-
predictWord()ازapp()حذف کنید:
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// predictWord() no longer called.
}
تجزیه و تحلیل آن
این کد ممکن است در ابتدا گیجکننده باشد، بنابراین بیایید آن را تجزیه و تحلیل کنیم.
ما سه دکمه به رابط کاربری خود با برچسبهای "چپ"، "راست" و "نویز" اضافه کردهایم که مربوط به سه دستوری هستند که میخواهیم مدل ما تشخیص دهد. فشردن این دکمهها تابع collect() که به تازگی اضافه شده است را فراخوانی میکند، که نمونههای آموزشی را برای مدل ما ایجاد میکند.
collect() یک label به خروجی تابع recognizer.listen() مرتبط میکند. از آنجایی که includeSpectrogram برابر با true است , تابع recognizer.listen() طیفنگار خام (دادههای فرکانسی) مربوط به ۱ ثانیه صدا را که به ۴۳ فریم تقسیم شده است، ارائه میدهد، بنابراین هر فریم تقریباً ۲۳ میلیثانیه صدا است:
recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
...
}, {includeSpectrogram: true});
از آنجایی که میخواهیم به جای کلمات از صداهای کوتاه برای کنترل اسلایدر استفاده کنیم، فقط ۳ فریم آخر (حدود ۷۰ میلیثانیه) را در نظر میگیریم:
let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
و برای جلوگیری از مشکلات عددی، دادهها را طوری نرمالسازی میکنیم که میانگین آنها ۰ و انحراف معیار آنها ۱ باشد. در این حالت، مقادیر طیفنگاره معمولاً اعداد منفی بزرگی در حدود -۱۰۰ و انحراف ۱۰ هستند:
const mean = -100;
const std = 10;
return x.map(x => (x - mean) / std);
در نهایت، هر مثال آموزشی دارای ۲ فیلد خواهد بود:
-
label****: به ترتیب برای "چپ"، "راست" و "نویز" مقادیر ۰، ۱ و ۲ را وارد کنید. -
vals****: ۶۹۶ عدد که اطلاعات فرکانس (طیفنگاره) را در خود جای دادهاند
و ما تمام دادهها را در متغیر examples ذخیره میکنیم:
examples.push({vals, label});
۷. جمعآوری دادههای آزمایشی
فایل index.html را در مرورگر باز کنید، باید ۳ دکمه مربوط به ۳ دستور را ببینید. اگر از یک فایل محلی استفاده میکنید، برای دسترسی به میکروفون باید یک وبسرور راهاندازی کنید و http://localhost:port/ استفاده کنید.
برای شروع یک وب سرور ساده روی پورت ۸۰۰۰:
python -m SimpleHTTPServer
برای جمعآوری مثال برای هر دستور، هنگام فشار دادن و نگه داشتن هر دکمه به مدت ۳-۴ ثانیه، یک صدای ثابت را به طور مکرر (یا مداوم) ایجاد کنید. شما باید برای هر برچسب حدود ۱۵۰ مثال جمعآوری کنید. به عنوان مثال، میتوانیم برای "چپ" با انگشتانمان بشکنیم، برای "راست" سوت بزنیم و برای "سر و صدا" به طور متناوب سکوت کنیم و صحبت کنیم.
با جمعآوری نمونههای بیشتر، شمارندهی نمایش داده شده در صفحه باید افزایش یابد. همچنین میتوانید با فراخوانی console.log() در متغیر examples در کنسول، دادهها را بررسی کنید. در این مرحله، هدف آزمایش فرآیند جمعآوری دادهها است. بعداً هنگام آزمایش کل برنامه، دادهها را دوباره جمعآوری خواهید کرد.
۸. آموزش یک مدل
- یک دکمهی « Train » درست بعد از دکمهی « Noise » در بدنهی فایل index.html اضافه کنید:
<br/><br/>
<button id="train" onclick="train()">Train</button>
- کد زیر را به کد موجود در 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;
}
- هنگام بارگذاری برنامه، تابع
buildModel()را فراخوانی کنید:
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// Add this line.
buildModel();
}
در این مرحله اگر برنامه را رفرش کنید، دکمه جدید " آموزش " را مشاهده خواهید کرد. میتوانید با جمعآوری مجدد دادهها و کلیک روی "آموزش"، آموزش را آزمایش کنید، یا میتوانید تا مرحله 10 صبر کنید تا آموزش را همراه با پیشبینی آزمایش کنید.
تجزیه و تحلیل آن
در سطح بالا، ما دو کار انجام میدهیم: buildModel() معماری مدل را تعریف میکند و train() مدل را با استفاده از دادههای جمعآوریشده آموزش میدهد.
معماری مدل
این مدل دارای ۴ لایه است: یک لایه کانولوشن که دادههای صوتی را پردازش میکند (که به صورت یک طیفنگاره نمایش داده میشود)، یک لایه max pool، یک لایه flatten و یک لایه dense که به ۳ عمل زیر نگاشت میشود:
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] است که در آن هر فریم ۲۳ میلیثانیه صدا است که شامل ۲۳۲ عدد است که مربوط به فرکانسهای مختلف هستند (۲۳۲ انتخاب شده است زیرا تعداد سطلهای فرکانسی مورد نیاز برای ضبط صدای انسان است). در این آزمایشگاه کد، ما از نمونههایی با طول ۳ فریم (نمونههای تقریباً ۷۰ میلیثانیهای) استفاده میکنیم، زیرا ما به جای گفتن کلمات کامل برای کنترل اسلایدر، صدا تولید میکنیم.
ما مدل خود را کامپایل میکنیم تا برای آموزش آماده شود:
const optimizer = tf.train.adam(0.01);
model.compile({
optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy']
});
ما از بهینهساز Adam ، یک بهینهساز رایج مورد استفاده در یادگیری عمیق، و categoricalCrossEntropy برای تابع زیان، تابع زیان استاندارد مورد استفاده برای طبقهبندی، استفاده میکنیم. به طور خلاصه، این تابع میزان فاصله احتمالات پیشبینیشده (یک احتمال در هر کلاس) از احتمال ۱۰۰٪ در کلاس واقعی و احتمال ۰٪ برای سایر کلاسها را اندازهگیری میکند. ما همچنین accuracy به عنوان معیاری برای نظارت ارائه میدهیم که درصد نمونههایی را که مدل پس از هر دوره آموزش درست بدست میآورد، به ما میدهد.
آموزش
آموزش با استفاده از یک دسته ۱۶ تایی (پردازش ۱۶ مثال به طور همزمان) ۱۰ بار (دوره) روی دادهها انجام میشود و دقت فعلی را در رابط کاربری نشان میدهد:
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}`;
}
}
});
۹. اسلایدر را به صورت آنی بهروزرسانی کنید
حالا که میتوانیم مدل خود را آموزش دهیم، بیایید کدی اضافه کنیم تا پیشبینیها را به صورت بلادرنگ انجام دهد و اسلایدر را حرکت دهد. این کد را درست بعد از دکمه " آموزش " در 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] است که توزیع احتمال را بر روی تعداد کلاسها نشان میدهد. به عبارت سادهتر، این فقط مجموعهای از اطمینانها برای هر یک از کلاسهای خروجی ممکن است که مجموع آنها برابر با ۱ است. تانسور دارای بُعد بیرونی ۱ است زیرا این اندازه دسته (یک مثال واحد) است.
برای تبدیل توزیع احتمال به یک عدد صحیح واحد که نشاندهنده محتملترین کلاس است، تابع probs.argMax(1) را فراخوانی میکنیم که شاخص کلاس با بالاترین احتمال را برمیگرداند. ما عدد "1" را به عنوان پارامتر محور ارسال میکنیم زیرا میخواهیم argMax روی آخرین بُعد، numClasses ، محاسبه کنیم.
بهروزرسانی اسلایدر
moveSlider() اگر برچسب اسلایدر ۰ ("Left") باشد، مقدار آن را کاهش میدهد، اگر برچسب ۱ ("Right") باشد، آن را افزایش میدهد و اگر برچسب ۲ ("Noise") باشد، آن را نادیده میگیرد.
حذف تانسورها
برای پاکسازی حافظه GPU، فراخوانی دستی tf.dispose() روی Tensors خروجی برای ما مهم است. جایگزین tf.dispose() دستی، قرار دادن فراخوانیهای تابع در tf.tidy() است، اما این روش را نمیتوان با توابع async استفاده کرد.
tf.dispose([input, probs, predLabel]);
۱۰. اپلیکیشن نهایی را تست کنید
فایل index.html را در مرورگر خود باز کنید و مانند بخش قبل با ۳ دکمه مربوط به ۳ دستور، دادهها را جمعآوری کنید. به یاد داشته باشید که هنگام جمعآوری دادهها، هر دکمه را ۳ تا ۴ ثانیه فشار داده و نگه دارید .
پس از جمعآوری نمونهها، دکمه «آموزش» را فشار دهید. این کار آموزش مدل را شروع میکند و باید دقت مدل را بالای ۹۰٪ ببینید. اگر به عملکرد خوب مدل دست نیافتید، سعی کنید دادههای بیشتری جمعآوری کنید.
پس از اتمام آموزش، دکمه «گوش دادن» را فشار دهید تا از طریق میکروفون پیشبینیهایی انجام دهید و اسلایدر را کنترل کنید!
برای آموزشهای بیشتر به http://js.tensorflow.org/ مراجعه کنید.