1. מבוא
ב-codelab הזה תבנו רשת לזיהוי אודיו ותשתמשו בה כדי לשלוט בסליידר בדפדפן באמצעות השמעת צלילים. תשתמשו ב-TensorFlow.js, ספרייה עוצמתית וגמישה של למידת מכונה ל-JavaScript.
קודם כול, תטענו ותפעילו מודל שעבר אימון מקדים שיכול לזהות 20 פקודות קוליות. לאחר מכן, באמצעות המיקרופון, תבנו ותאמנו רשת נוירונים פשוטה שמזהה את הצלילים שלכם ומזיזה את המחוון שמאלה או ימינה.
ב-Codelab הזה לא נסביר את התיאוריה שמאחורי מודלים לזיהוי אודיו. אם אתם רוצים לדעת איך עושים את זה, כדאי לעיין במדריך הזה.
יצרנו גם מילון מונחים של מונחים בתחום למידת המכונה שמופיעים ב-Codelab הזה.
מה תלמדו
- איך טוענים מודל מוכן מראש לזיהוי פקודות קוליות
- איך מבצעים חיזויים בזמן אמת באמצעות המיקרופון
- איך מאמנים מודל מותאם אישית לזיהוי אודיו ומשתמשים בו באמצעות המיקרופון של הדפדפן
אז בואו נתחיל.
2. דרישות
כדי להשלים את ה-codelab הזה, תצטרכו:
- גרסה עדכנית של Chrome או דפדפן מודרני אחר.
- כלי לעריכת טקסט שפועל באופן מקומי במחשב או באינטרנט, למשל Codepen או Glitch.
- ידע ב-HTML, CSS, JavaScript ובכלי הפיתוח ל-Chrome (או בכלי הפיתוח של הדפדפן המועדף).
- הבנה מושגית ברמה גבוהה של רשתות נוירונים. אם אתם צריכים מבוא או רענון, כדאי לצפות בסרטון הזה של 3blue1brown או בסרטון הזה על למידה עמוקה ב-JavaScript של Ashi Krishnan.
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% ומעלה שהוא שומע מילה מסוימת.
מידע נוסף על מודל פקודות הדיבור ועל ה-API שלו זמין בקובץ README.md ב-GitHub.
6. איסוף נתונים
כדי שיהיה כיף, נשתמש בצלילים קצרים במקום במילים שלמות כדי לשלוט בפס ההזזה.
תאמנו מודל לזיהוי של 3 פקודות שונות: 'שמאלה', 'ימינה' ו'רעש'. הפקודות האלה יגרמו למחוון לזוז שמאלה או ימינה. זיהוי של 'רעש' (לא נדרשת פעולה) הוא קריטי בזיהוי דיבור, כי אנחנו רוצים שהסליידר יגיב רק כשמשמיעים את הצליל הנכון, ולא כשמדברים באופן כללי או כשזזים.
- קודם צריך לאסוף נתונים. כדי להוסיף ממשק משתמש פשוט לאפליקציה, מוסיפים את הקוד הבא בתוך התג
<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() מחזיר את הספקטרוגרמה הגולמית (נתוני התדר) של שניית אודיו אחת, מחולקת ל-43 פריימים, כך שכל פריים הוא ~23ms של אודיו:
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. אימון מודל
- מוסיפים לחצן 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() מאמן את המודל באמצעות הנתונים שנאספו.
ארכיטקטורת המודל
למודל יש 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], כאשר כל פריים הוא אודיו של 23ms שמכיל 232 מספרים שתואמים לתדרים שונים (המספר 232 נבחר כי זה מספר התדרים שנדרש כדי ללכוד את הקול האנושי). ב-codelab הזה אנחנו משתמשים בדגימות באורך 3 פריימים (כ-70 אלפיות השנייה), כי אנחנו יוצרים צלילים במקום לומר מילים שלמות כדי לשלוט בסרגל ההזזה.
אנחנו מהדרים (compile) את המודל כדי להכין אותו לאימון:
const optimizer = tf.train.adam(0.01);
model.compile({
optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy']
});
אנחנו משתמשים באופטימייזר Adam, אופטימייזר נפוץ שמשמש בלמידה עמוקה, וב-categoricalCrossEntropy לאובדן, פונקציית האובדן הרגילה שמשמשת לסיווג. בקיצור, המדד הזה מודד את המרחק בין ההסתברויות החזויות (הסתברות אחת לכל מחלקה) לבין הסתברות של 100% במחלקה האמיתית, והסתברות של 0% בכל שאר המחלקות. אנחנו גם מספקים את המדד accuracy למעקב, שיציג את אחוז הדוגמאות שהמודל מזהה נכון אחרי כל תקופת אימון.
הדרכה
האימון מתבצע 10 פעמים (epochs) על הנתונים באמצעות גודל אצווה של 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. עדכון פס ההזזה בזמן אמת
עכשיו כשאנחנו יכולים לאמן את המודל, נוסיף קוד כדי ליצור תחזיות בזמן אמת ולהזיז את פס ההזזה. מוסיפים את הקוד הזה מיד אחרי הלחצן Train (אימון) בקובץ 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 כי זה הגודל של ה-batch (דוגמה אחת).
כדי להמיר את התפלגות ההסתברויות למספר שלם יחיד שמייצג את המחלקה הסבירה ביותר, קוראים לפונקציה probs.argMax(1) שמחזירה את אינדקס המחלקה עם ההסתברות הכי גבוהה. העברנו את הערך 1 כפרמטר של הציר כי אנחנו רוצים לחשב את argMax על פני המימד האחרון, numClasses.
עדכון פס ההזזה
moveSlider() מקטין את הערך של פס ההזזה אם התווית היא 0 ('שמאלה') , מגדיל אותו אם התווית היא 1 ('ימינה') ומתעלם ממנו אם התווית היא 2 ('רעש').
סילוק טנסורים
כדי לנקות את זיכרון ה-GPU, חשוב לנו להפעיל באופן ידני את tf.dispose() על טנסורים של פלט. החלופה ל-tf.dispose() ידני היא שימוש ב-tf.tidy() כדי לעטוף קריאות לפונקציות, אבל אי אפשר להשתמש בשיטה הזו עם פונקציות אסינכרוניות.
tf.dispose([input, probs, predLabel]);
10. בדיקת האפליקציה הסופית
פותחים את index.html בדפדפן ואוספים נתונים כמו בסעיף הקודם באמצעות 3 הלחצנים שמתאימים ל-3 הפקודות. חשוב ללחוץ לחיצה ארוכה על כל לחצן למשך 3-4 שניות בזמן איסוף הנתונים.
אחרי שאוספים דוגמאות, לוחצים על הלחצן Train (אימון). המודל יתחיל להתאמן, ואתם אמורים לראות את רמת הדיוק של המודל עולה מעל 90%. אם לא משיגים ביצועים טובים של המודל, כדאי לנסות לאסוף עוד נתונים.
אחרי שמסיימים את האימון, לוחצים על הלחצן "האזנה" כדי ליצור תחזיות מהמיקרופון ולשלוט בסליידר.
מדריכים נוספים זמינים בכתובת http://js.tensorflow.org/.