Параллельное обучение нескольких сетей через child_process

Библиотека Synaptic предоставляет инструменты для построения и обучения нейронных сетей в JavaScript, однако её стандартные методы обучения выполняются синхронно в основном потоке. При работе с большими объёмами данных или множеством сетей возникает необходимость параллельного обучения, чтобы оптимально использовать ресурсы процессора. В Node.js для этого используется модуль child_process, который позволяет создавать отдельные процессы для выполнения задач.


Основные принципы child_process

Модуль child_process предоставляет три основных метода для запуска дочерних процессов:

  1. spawn – создаёт новый процесс и предоставляет потоковое взаимодействие с ним (stdin, stdout, stderr).
  2. exec – выполняет команду в новом процессе и возвращает результат через колбэк после завершения.
  3. fork – специализированный метод для запуска отдельного Node.js процесса с возможностью обмена сообщениями через IPC (inter-process communication). Этот способ идеально подходит для параллельного обучения сетей, так как позволяет передавать объекты и получать обратную связь о прогрессе обучения.

Архитектура параллельного обучения

Идея заключается в следующем:

  • Основной процесс создаёт несколько дочерних процессов.
  • Каждый дочерний процесс создаёт свою копию сети Synaptic и обучает её на выделенном наборе данных.
  • Дочерние процессы периодически отправляют в основной процесс информацию о прогрессе или результаты обучения.
  • После завершения всех процессов основной процесс собирает веса и, при необходимости, выполняет агрегирование результатов.

Такой подход позволяет полностью разгрузить основной поток и использовать несколько ядер процессора.


Создание дочернего процесса через fork

const { fork } = require('child_process');

const numNetworks = 4; // количество сетей для обучения
const processes = [];

for (let i = 0; i < numNetworks; i++) {
    const child = fork('./trainNetwork.js'); // файл с логикой обучения сети
    processes.push(child);

    child.send({ id: i, trainingData: trainingSets[i] });

    child.on('message', (msg) => {
        console.log(`Процесс ${msg.id}: прогресс ${msg.progress}%`);
        if (msg.done) {
            console.log(`Процесс ${msg.id} завершил обучение`);
        }
    });
}
  • Каждый дочерний процесс получает уникальный идентификатор и свой набор данных.
  • Основной процесс отслеживает прогресс через событие message.

Логика дочернего процесса

Файл trainNetwork.js может выглядеть следующим образом:

const synaptic = require('synaptic');

process.on('message', (msg) => {
    const { id, trainingData } = msg;

    const { Layer, Network, Trainer } = synaptic;

    const inputLayer = new Layer(trainingData.inputSize);
    const hiddenLayer = new Layer(trainingData.hiddenSize);
    const outputLayer = new Layer(trainingData.outputSize);

    inputLayer.project(hiddenLayer);
    hiddenLayer.project(outputLayer);

    const network = new Network({
        input: inputLayer,
        hidden: [hiddenLayer],
        output: outputLayer
    });

    const trainer = new Trainer(network);

    const maxIterations = 1000;
    let iteration = 0;

    function trainStep() {
        const result = trainer.train(trainingData.set, {
            iterations: 1,
            log: false
        });

        iteration++;
        const progress = Math.floor((iteration / maxIterations) * 100);

        process.send({ id, progress });

        if (iteration < maxIterations) {
            setImmediate(trainStep); // позволяет не блокировать событийный цикл
        } else {
            process.send({ id, done: true, network: network.toJSON() });
        }
    }

    trainStep();
});

Особенности:

  • Использование setImmediate предотвращает блокировку потока, что критично при обучении больших сетей.
  • По завершении обучения сетка сериализуется методом toJSON() и отправляется в основной процесс.

Агрегирование результатов

После того как все процессы завершили обучение, можно собрать веса и объединить их при необходимости:

const trainedNetworks = [];

processes.forEach(child => {
    child.on('message', (msg) => {
        if (msg.done) {
            trainedNetworks.push(msg.network);
            if (trainedNetworks.length === numNetworks) {
                console.log('Все сети обучены, можно объединять результаты');
                // Логика объединения весов или создания ансамбля
            }
        }
    });
});

Методы объединения могут быть следующими:

  • Ансамблевое решение: каждая сеть голосует при предсказании, итоговое решение формируется большинством голосов.
  • Среднее по весам: усреднение весов одинаковых слоёв сетей для создания обобщённой сети.

Преимущества подхода

  • Параллельное использование ядер процессора – каждый дочерний процесс может выполняться на отдельном ядре.
  • Изоляция процессов – сбой одной сети не влияет на остальные.
  • Гибкость – можно запускать разное количество сетей, обучать на разных наборах данных и контролировать прогресс.

Важные моменты при использовании

  • Передача больших объёмов данных через send может замедлять работу; рекомендуется передавать ссылки на файлы или минимизировать объём объекта.
  • Для долгого обучения полезно реализовать промежуточное сохранение состояния сети.
  • Параллельные процессы потребляют больше памяти, поэтому стоит оценивать ресурсы сервера перед запуском большого числа сетей.

Такой подход с использованием child_process.fork и Synaptic позволяет масштабировать обучение нейронных сетей в Node.js, обеспечивая высокую производительность и управляемость при работе с большими проектами.