آموزش TensorFlow.js در Node.js Codelab

۱. مقدمه

در این Codelab، شما یاد خواهید گرفت که چگونه یک وب سرور Node.js برای آموزش و طبقه‌بندی انواع زمین بیسبال در سمت سرور با استفاده از TensorFlow.js ، یک کتابخانه یادگیری ماشین قدرتمند و انعطاف‌پذیر برای جاوا اسکریپت، بسازید. شما یک برنامه وب برای آموزش یک مدل برای پیش‌بینی نوع زمین از داده‌های حسگر زمین و فراخوانی پیش‌بینی از یک کلاینت وب خواهید ساخت. یک نسخه کاملاً کاربردی از این Codelab در مخزن tfjs-examples GitHub موجود است.

آنچه یاد خواهید گرفت

  • نحوه نصب و راه‌اندازی بسته npm مربوط به tensorflow.js برای استفاده با Node.js.
  • نحوه دسترسی به داده‌های آموزشی و آزمایشی در محیط Node.js.
  • نحوه آموزش یک مدل با TensorFlow.js در سرور Node.js.
  • نحوه‌ی استقرار مدل آموزش‌دیده برای استنتاج در یک برنامه‌ی کلاینت/سرور.

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

۲. الزامات

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

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

۳. یک برنامه Node.js راه‌اندازی کنید

Node.js و npm را نصب کنید. برای پلتفرم‌ها و وابستگی‌های پشتیبانی‌شده، لطفاً به راهنمای نصب tfjs-node مراجعه کنید.

یک دایرکتوری به نام ./baseball برای برنامه Node.js خود ایجاد کنید. فایل‌های package.json و webpack.config.js لینک شده را در این دایرکتوری کپی کنید تا وابستگی‌های بسته npm (از جمله بسته npm @tensorflow/tfjs-node ) پیکربندی شوند. سپس دستور npm install را برای نصب وابستگی‌ها اجرا کنید.

$ cd baseball
$ ls
package.json  webpack.config.js
$ npm install
...
$ ls
node_modules  package.json  package-lock.json  webpack.config.js

حالا شما آماده‌اید تا کمی کد بنویسید و یک مدل را آموزش دهید!

۴. تنظیم داده‌های آموزشی و آزمایشی

شما از داده‌های آموزشی و آزمایشی به صورت فایل‌های CSV از لینک‌های زیر استفاده خواهید کرد. داده‌ها را در این فایل‌ها دانلود و بررسی کنید:

pitch_type_training_data.csv

pitch_type_test_data.csv

بیایید به چند نمونه داده آموزشی نگاهی بیندازیم:

vx0,vy0,vz0,ax,ay,az,start_speed,left_handed_pitcher,pitch_code
7.69914900671662,-132.225686405648,-6.58357157666866,-22.5082591074995,28.3119270826735,-16.5850095967027,91.1,0,0
6.68052308575228,-134.215511616881,-6.35565979491619,-19.6602769147989,26.7031848314466,-14.3430602022656,92.4,0,0
2.56546504690782,-135.398673977074,-2.91657310799559,-14.7849950586111,27.8083916890792,-21.5737737390901,93.1,0,0

هشت ویژگی ورودی وجود دارد که داده‌های حسگر گام صدا را توصیف می‌کنند:

  • سرعت توپ (vx0، vy0، vz0)
  • شتاب توپ (ax، ay، az)
  • سرعت شروع گام
  • اینکه پرتاب کننده چپ دست است یا نه

و یک برچسب خروجی:

  • کد زمین بازی (pitch_code) که یکی از هفت نوع زمین بازی را نشان می‌دهد: Fastball (2-seam), Fastball (4-seam), Fastball (sinker), Fastball (cutter), Slider, Changeup, Curveball

هدف، ساخت مدلی است که بتواند نوع گام را با توجه به داده‌های حسگر گام پیش‌بینی کند.

قبل از ایجاد مدل، باید داده‌های آموزشی و آزمایشی را آماده کنید. فایل pitch_type.js را در دایرکتوری baseball/ ایجاد کنید و کد زیر را در آن کپی کنید. این کد داده‌های آموزشی و آزمایشی را با استفاده از API tf.data.csv بارگذاری می‌کند. همچنین داده‌ها را (که همیشه توصیه می‌شود) با استفاده از مقیاس نرمال‌سازی حداقل-حداکثر نرمال‌سازی می‌کند.

const tf = require('@tensorflow/tfjs');

// util function to normalize a value between a given range.
function normalize(value, min, max) {
  if (min === undefined || max === undefined) {
    return value;
  }
  return (value - min) / (max - min);
}

// data can be loaded from URLs or local file paths when running in Node.js.
const TRAIN_DATA_PATH =
'https://storage.googleapis.com/mlb-pitch-data/pitch_type_training_data.csv';
const TEST_DATA_PATH =    'https://storage.googleapis.com/mlb-pitch-data/pitch_type_test_data.csv';

// Constants from training data
const VX0_MIN = -18.885;
const VX0_MAX = 18.065;
const VY0_MIN = -152.463;
const VY0_MAX = -86.374;
const VZ0_MIN = -15.5146078412997;
const VZ0_MAX = 9.974;
const AX_MIN = -48.0287647107959;
const AX_MAX = 30.592;
const AY_MIN = 9.397;
const AY_MAX = 49.18;
const AZ_MIN = -49.339;
const AZ_MAX = 2.95522851438373;
const START_SPEED_MIN = 59;
const START_SPEED_MAX = 104.4;

const NUM_PITCH_CLASSES = 7;
const TRAINING_DATA_LENGTH = 7000;
const TEST_DATA_LENGTH = 700;

// Converts a row from the CSV into features and labels.
// Each feature field is normalized within training data constants
const csvTransform =
    ({xs, ys}) => {
      const values = [
        normalize(xs.vx0, VX0_MIN, VX0_MAX),
        normalize(xs.vy0, VY0_MIN, VY0_MAX),
        normalize(xs.vz0, VZ0_MIN, VZ0_MAX), normalize(xs.ax, AX_MIN, AX_MAX),
        normalize(xs.ay, AY_MIN, AY_MAX), normalize(xs.az, AZ_MIN, AZ_MAX),
        normalize(xs.start_speed, START_SPEED_MIN, START_SPEED_MAX),
        xs.left_handed_pitcher
      ];
      return {xs: values, ys: ys.pitch_code};
    }

const trainingData =
    tf.data.csv(TRAIN_DATA_PATH, {columnConfigs: {pitch_code: {isLabel: true}}})
        .map(csvTransform)
        .shuffle(TRAINING_DATA_LENGTH)
        .batch(100);

// Load all training data in one batch to use for evaluation
const trainingValidationData =
    tf.data.csv(TRAIN_DATA_PATH, {columnConfigs: {pitch_code: {isLabel: true}}})
        .map(csvTransform)
        .batch(TRAINING_DATA_LENGTH);

// Load all test data in one batch to use for evaluation
const testValidationData =
    tf.data.csv(TEST_DATA_PATH, {columnConfigs: {pitch_code: {isLabel: true}}})
        .map(csvTransform)
        .batch(TEST_DATA_LENGTH);

۵. ایجاد مدلی برای طبقه‌بندی انواع گام صدا

اکنون آماده ساخت مدل هستید. از API tf.layers برای اتصال ورودی‌ها (شکل [8] مقادیر حسگر گام) به 3 لایه کاملاً متصل پنهان متشکل از واحدهای فعال‌سازی ReLU استفاده کنید، و به دنبال آن یک لایه خروجی softmax متشکل از 7 واحد قرار دارد که هر کدام یکی از انواع گام خروجی را نشان می‌دهند.

مدل را با استفاده از بهینه‌ساز adam و تابع زیان sparseCategoricalCrossentropy آموزش دهید. برای اطلاعات بیشتر در مورد این انتخاب‌ها، به راهنمای مدل‌های آموزشی مراجعه کنید.

کد زیر را به انتهای فایل pitch_type.js اضافه کنید:

const model = tf.sequential();
model.add(tf.layers.dense({units: 250, activation: 'relu', inputShape: [8]}));
model.add(tf.layers.dense({units: 175, activation: 'relu'}));
model.add(tf.layers.dense({units: 150, activation: 'relu'}));
model.add(tf.layers.dense({units: NUM_PITCH_CLASSES, activation: 'softmax'}));

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

آموزش را از کد سرور اصلی که بعداً خواهید نوشت، آغاز کنید.

برای تکمیل ماژول pitch_type.js، بیایید تابعی بنویسیم که مجموعه داده‌های اعتبارسنجی و آزمایشی را ارزیابی کند، نوع گام را برای یک نمونه واحد پیش‌بینی کند و معیارهای دقت را محاسبه کند. این کد را به انتهای pitch_type.js اضافه کنید:

// Returns pitch class evaluation percentages for training data
// with an option to include test data
async function evaluate(useTestData) {
  let results = {};
  await trainingValidationData.forEachAsync(pitchTypeBatch => {
    const values = model.predict(pitchTypeBatch.xs).dataSync();
    const classSize = TRAINING_DATA_LENGTH / NUM_PITCH_CLASSES;
    for (let i = 0; i < NUM_PITCH_CLASSES; i++) {
      results[pitchFromClassNum(i)] = {
        training: calcPitchClassEval(i, classSize, values)
      };
    }
  });

  if (useTestData) {
    await testValidationData.forEachAsync(pitchTypeBatch => {
      const values = model.predict(pitchTypeBatch.xs).dataSync();
      const classSize = TEST_DATA_LENGTH / NUM_PITCH_CLASSES;
      for (let i = 0; i < NUM_PITCH_CLASSES; i++) {
        results[pitchFromClassNum(i)].validation =
            calcPitchClassEval(i, classSize, values);
      }
    });
  }
  return results;
}

async function predictSample(sample) {
  let result = model.predict(tf.tensor(sample, [1,sample.length])).arraySync();
  var maxValue = 0;
  var predictedPitch = 7;
  for (var i = 0; i < NUM_PITCH_CLASSES; i++) {
    if (result[0][i] > maxValue) {
      predictedPitch = i;
      maxValue = result[0][i];
    }
  }
  return pitchFromClassNum(predictedPitch);
}

// Determines accuracy evaluation for a given pitch class by index
function calcPitchClassEval(pitchIndex, classSize, values) {
  // Output has 7 different class values for each pitch, offset based on
  // which pitch class (ordered by i)
  let index = (pitchIndex * classSize * NUM_PITCH_CLASSES) + pitchIndex;
  let total = 0;
  for (let i = 0; i < classSize; i++) {
    total += values[index];
    index += NUM_PITCH_CLASSES;
  }
  return total / classSize;
}

// Returns the string value for Baseball pitch labels
function pitchFromClassNum(classNum) {
  switch (classNum) {
    case 0:
      return 'Fastball (2-seam)';
    case 1:
      return 'Fastball (4-seam)';
    case 2:
      return 'Fastball (sinker)';
    case 3:
      return 'Fastball (cutter)';
    case 4:
      return 'Slider';
    case 5:
      return 'Changeup';
    case 6:
      return 'Curveball';
    default:
      return 'Unknown';
  }
}

module.exports = {
  evaluate,
  model,
  pitchFromClassNum,
  predictSample,
  testValidationData,
  trainingData,
  TEST_DATA_LENGTH
}

۶. آموزش مدل روی سرور

کد سرور را برای انجام آموزش و ارزیابی مدل در یک فایل جدید به نام server.js بنویسید. ابتدا، یک سرور HTTP ایجاد کنید و با استفاده از API socket.io یک اتصال سوکت دو طرفه باز کنید. سپس آموزش مدل را با استفاده از API model.fitDataset اجرا کنید و دقت مدل را با استفاده از متد pitch_type.evaluate() که قبلاً نوشتید، ارزیابی کنید. برای 10 تکرار آموزش و ارزیابی کنید و معیارها را در کنسول چاپ کنید.

کد زیر را در server.js کپی کنید:

require('@tensorflow/tfjs-node');

const http = require('http');
const socketio = require('socket.io');
const pitch_type = require('./pitch_type');

const TIMEOUT_BETWEEN_EPOCHS_MS = 500;
const PORT = 8001;

// util function to sleep for a given ms
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Main function to start server, perform model training, and emit stats via the socket connection
async function run() {
  const port = process.env.PORT || PORT;
  const server = http.createServer();
  const io = socketio(server);

  server.listen(port, () => {
    console.log(`  > Running socket on port: ${port}`);
  });

  io.on('connection', (socket) => {
    socket.on('predictSample', async (sample) => {
      io.emit('predictResult', await pitch_type.predictSample(sample));
    });
  });

  let numTrainingIterations = 10;
  for (var i = 0; i < numTrainingIterations; i++) {
    console.log(`Training iteration : ${i+1} / ${numTrainingIterations}`);
    await pitch_type.model.fitDataset(pitch_type.trainingData, {epochs: 1});
    console.log('accuracyPerClass', await pitch_type.evaluate(true));
    await sleep(TIMEOUT_BETWEEN_EPOCHS_MS);
  }

  io.emit('trainingComplete', true);
}

run();

در این مرحله، شما آماده اجرا و آزمایش سرور هستید! باید چیزی شبیه به این را ببینید، که در آن سرور در هر تکرار یک دوره را آموزش می‌دهد (همچنین می‌توانید از API model.fitDataset برای آموزش چندین دوره با یک فراخوانی استفاده کنید). اگر در این مرحله با هرگونه خطایی مواجه شدید، لطفاً نصب node و npm خود را بررسی کنید.

$ npm run start-server
...
  > Running socket on port: 8001
Epoch 1 / 1
eta=0.0 ========================================================================================================>
2432ms 34741us/step - acc=0.429 loss=1.49

برای متوقف کردن سرور در حال اجرا، Ctrl-C را تایپ کنید. در مرحله بعد دوباره آن را اجرا خواهیم کرد.

۷. ایجاد صفحه کلاینت و نمایش کد

حالا که سرور آماده است، مرحله بعدی نوشتن کد کلاینت است که در مرورگر اجرا می‌شود. یک صفحه ساده برای فراخوانی پیش‌بینی مدل روی سرور و نمایش نتیجه ایجاد کنید. این صفحه از socket.io برای ارتباط کلاینت/سرور استفاده می‌کند.

ابتدا، فایل index.html را در پوشه baseball/ ایجاد کنید:

<!doctype html>
<html>
  <head>
    <title>Pitch Training Accuracy</title>
  </head>
  <body>
    <h3 id="waiting-msg">Waiting for server...</h3>
    <p>
    <span style="font-size:16px" id="trainingStatus"></span>
    <p>
    <div id="predictContainer" style="font-size:16px;display:none">
      Sensor data: <span id="predictSample"></span>
      <button style="font-size:18px;padding:5px;margin-right:10px" id="predict-button">Predict Pitch</button><p>
      Predicted Pitch Type: <span style="font-weight:bold" id="predictResult"></span>
    </div>
    <script src="dist/bundle.js"></script>
    <style>
      html,
      body {
        font-family: Roboto, sans-serif;
        color: #5f6368;
      }
      body {
        background-color: rgb(248, 249, 250);
      }
    </style>
  </body>
</html>

سپس یک فایل جدید به نام client.js در پوشه baseball/ با کد زیر ایجاد کنید:

import io from 'socket.io-client';
const predictContainer = document.getElementById('predictContainer');
const predictButton = document.getElementById('predict-button');

const socket =
    io('http://localhost:8001',
       {reconnectionDelay: 300, reconnectionDelayMax: 300});

const testSample = [2.668,-114.333,-1.908,4.786,25.707,-45.21,78,0]; // Curveball

predictButton.onclick = () => {
  predictButton.disabled = true;
  socket.emit('predictSample', testSample);
};

// functions to handle socket events
socket.on('connect', () => {
    document.getElementById('waiting-msg').style.display = 'none';
    document.getElementById('trainingStatus').innerHTML = 'Training in Progress';
});

socket.on('trainingComplete', () => {
  document.getElementById('trainingStatus').innerHTML = 'Training Complete';
  document.getElementById('predictSample').innerHTML = '[' + testSample.join(', ') + ']';
  predictContainer.style.display = 'block';
});

socket.on('predictResult', (result) => {
  plotPredictResult(result);
});

socket.on('disconnect', () => {
  document.getElementById('trainingStatus').innerHTML = '';
  predictContainer.style.display = 'none';
  document.getElementById('waiting-msg').style.display = 'block';
});

function plotPredictResult(result) {
  predictButton.disabled = false;
  document.getElementById('predictResult').innerHTML = result;
  console.log(result);
}

کلاینت، پیام سوکت trainingComplete را برای نمایش یک دکمه پیش‌بینی مدیریت می‌کند. وقتی این دکمه کلیک می‌شود، کلاینت یک پیام سوکت با داده‌های نمونه حسگر ارسال می‌کند. پس از دریافت پیام predictResult ، پیش‌بینی را در صفحه نمایش می‌دهد.

۸. برنامه را اجرا کنید

برای مشاهده‌ی عملکرد کامل برنامه، هم سرور و هم کلاینت را اجرا کنید:

[In one terminal, run this first]
$ npm run start-client

[In another terminal, run this next]
$ npm run start-server

صفحه کلاینت را در مرورگر خود باز کنید ( http://localhost:8080 ). پس از اتمام آموزش مدل، روی دکمه Predict Sample کلیک کنید. باید نتیجه پیش‌بینی را در مرورگر مشاهده کنید. می‌توانید داده‌های حسگر نمونه را با چند مثال از فایل CSV آزمایشی تغییر دهید و ببینید مدل چقدر دقیق پیش‌بینی می‌کند.

۹. آنچه آموختید

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

برای مثال‌ها و دموهای بیشتر به همراه کد، حتماً از tensorflow.org/js دیدن کنید تا ببینید چگونه می‌توانید از TensorFlow.js در برنامه‌های خود استفاده کنید.