С выпуском альфа-версии TensorFlow 2.0 TensorFlow.js был обновлен до первой официальной версии 1.0, а документация по TensorFlow.js также была добавлена на официальный сайт TensorFlow, что означает, что TensorFlow.js больше не является экспериментальным продуктом. Как инженер по исследованиям и разработкам ядра браузера, я, естественно, заинтересован в TensorFlow.js.
В последние годы язык Javascript завоевывает города и поселки, Node.js — на стороне сервера, а разработка мобильных интерфейсов — еще популярнее, JS есть даже в десктопных приложениях, например, популярная недавно Visual Studio Code теперь проник в область искусственного интеллекта. Я должен чувствовать, что язык сценариев, который тогда был наспех разработан и подвергнут критике, обладает такой цепкой жизненной силой.
Без лишних слов давайте посмотрим, каково это — обучать модель в браузере.
Ранее я написал серию «Повышение скорости распознавания рукописных цифр шаг за шагом».(1)(2)(3)", распознавание рукописных цифр — очень хороший стартовый проект, поэтому здесь я возьму распознавание рукописных цифр в качестве примера, чтобы проиллюстрировать, как обучать модель в браузере. Вместо того, чтобы начинать с простейшей модели линейной регрессии, напрямую выбирается сверточная нейронная сеть.
Как и шаги по обучению модели в коде Python, использование TensorFlow.js для обучения модели в браузере состоит из четырех основных шагов:
- Скачать данные.
- Определите структуру модели.
- Обучайте модель и следите за ее производительностью во время обучения.
- Оцените обученную модель.
Скачать данные
Друзья со знаниями в области машинного обучения должны быть знакомы с набором данных MNIST, который представляет собой набор изображений рукописных цифр 28x28 в градациях серого, включая 55 000 обучающих выборок, 10 000 тестовых выборок и 5 000 выборок данных перекрестной проверки. tensorflow python предоставляет класс-оболочку, который может напрямую загружать набор данных MNIST.В TensorFlow.js вам нужно написать собственный код для загрузки:
const IMAGE_SIZE = 784;
const NUM_CLASSES = 10;
const NUM_DATASET_ELEMENTS = 65000;
const TRAIN_TEST_RATIO = 5 / 6;
const NUM_TRAIN_ELEMENTS = Math.floor(TRAIN_TEST_RATIO * NUM_DATASET_ELEMENTS);
const NUM_TEST_ELEMENTS = NUM_DATASET_ELEMENTS - NUM_TRAIN_ELEMENTS;
const MNIST_IMAGES_SPRITE_PATH =
'mnist_images.png';
const MNIST_LABELS_PATH =
'mnist_labels_uint8';
/**
* A class that fetches the sprited MNIST dataset and returns shuffled batches.
*
* NOTE: This will get much easier. For now, we do data fetching and
* manipulation manually.
*/
export class MnistData {
constructor() {
this.shuffledTrainIndex = 0;
this.shuffledTestIndex = 0;
}
async load() {
// Make a request for the MNIST sprited image.
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imgRequest = new Promise((resolve, reject) => {
img.crossOrigin = '';
img.onload = () => {
img.width = img.naturalWidth;
img.height = img.naturalHeight;
const datasetBytesBuffer =
new ArrayBuffer(NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
const chunkSize = 5000;
canvas.width = img.width;
canvas.height = chunkSize;
for (let i = 0; i < NUM_DATASET_ELEMENTS / chunkSize; i++) {
const datasetBytesView = new Float32Array(
datasetBytesBuffer, i * IMAGE_SIZE * chunkSize * 4,
IMAGE_SIZE * chunkSize);
ctx.drawImage(
img, 0, i * chunkSize, img.width, chunkSize, 0, 0, img.width,
chunkSize);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (let j = 0; j < imageData.data.length / 4; j++) {
// All channels hold an equal value since the image is grayscale, so
// just read the red channel.
datasetBytesView[j] = imageData.data[j * 4] / 255;
}
}
this.datasetImages = new Float32Array(datasetBytesBuffer);
resolve();
};
img.src = MNIST_IMAGES_SPRITE_PATH;
});
const labelsRequest = fetch(MNIST_LABELS_PATH);
const [imgResponse, labelsResponse] =
await Promise.all([imgRequest, labelsRequest]);
this.datasetLabels = new Uint8Array(await labelsResponse.arrayBuffer());
// Create shuffled indices into the train/test set for when we select a
// random dataset element for training / validation.
this.trainIndices = tf.util.createShuffledIndices(NUM_TRAIN_ELEMENTS);
this.testIndices = tf.util.createShuffledIndices(NUM_TEST_ELEMENTS);
// Slice the the images and labels into train and test sets.
this.trainImages =
this.datasetImages.slice(0, IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
this.testImages = this.datasetImages.slice(IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
this.trainLabels =
this.datasetLabels.slice(0, NUM_CLASSES * NUM_TRAIN_ELEMENTS);
this.testLabels =
this.datasetLabels.slice(NUM_CLASSES * NUM_TRAIN_ELEMENTS);
}
nextTrainBatch(batchSize) {
return this.nextBatch(
batchSize, [this.trainImages, this.trainLabels], () => {
this.shuffledTrainIndex =
(this.shuffledTrainIndex + 1) % this.trainIndices.length;
return this.trainIndices[this.shuffledTrainIndex];
});
}
nextTestBatch(batchSize) {
return this.nextBatch(batchSize, [this.testImages, this.testLabels], () => {
this.shuffledTestIndex =
(this.shuffledTestIndex + 1) % this.testIndices.length;
return this.testIndices[this.shuffledTestIndex];
});
}
nextBatch(batchSize, data, index) {
const batchImagesArray = new Float32Array(batchSize * IMAGE_SIZE);
const batchLabelsArray = new Uint8Array(batchSize * NUM_CLASSES);
for (let i = 0; i < batchSize; i++) {
const idx = index();
const image =
data[0].slice(idx * IMAGE_SIZE, idx * IMAGE_SIZE + IMAGE_SIZE);
batchImagesArray.set(image, i * IMAGE_SIZE);
const label =
data[1].slice(idx * NUM_CLASSES, idx * NUM_CLASSES + NUM_CLASSES);
batchLabelsArray.set(label, i * NUM_CLASSES);
}
const xs = tf.tensor2d(batchImagesArray, [batchSize, IMAGE_SIZE]);
const labels = tf.tensor2d(batchLabelsArray, [batchSize, NUM_CLASSES]);
return {xs, labels};
}
}
код, загрузитьmnist_images.pngКартинка, картинка склеена из изображений всех наборов данных MNIST (файл очень большой, около 10М), и подгружается еще однаmnist_labels_uint8Текстовый файл, содержащий все соответствующие метки для набора данных MNIST.
Следует отметить, что это только метод загрузки набора данных MNIST, вы также можете использовать набор данных MNIST с одним написанным от руки числом и одним изображением и загружать несколько файлов изображений в пакетах.
Приведенный выше код реализует класс MnistData с двумя общедоступными методами:
- nextTrainBatch(batchSize): возвращает случайный набор изображений и их меток из обучающего набора.
- nextTestBatch(batchSize): возвращает пакет изображений и их метки из тестового набора.
Чтобы убедиться, что приведенный выше код работает правильно, вы можете написать фрагмент кода для отображения загруженных данных:
async function showExamples(data) {
// Create a container in the visor
const surface =
tfvis.visor().surface({ name: 'Input Data Examples', tab: 'Input Data'});
// Get the examples
const examples = data.nextTestBatch(20);
const numExamples = examples.xs.shape[0];
// Create a canvas element to render each example
for (let i = 0; i < numExamples; i++) {
const imageTensor = tf.tidy(() => {
// Reshape the image to 28x28 px
return examples.xs
.slice([i, 0], [1, examples.xs.shape[1]])
.reshape([28, 28, 1]);
});
const canvas = document.createElement('canvas');
canvas.width = 28;
canvas.height = 28;
canvas.style = 'margin: 4px;';
await tf.browser.toPixels(imageTensor, canvas);
surface.drawArea.appendChild(canvas);
imageTensor.dispose();
}
}
async function run() {
const data = new MnistData();
await data.load();
await showExamples(data);
}
document.addEventListener('DOMContentLoaded', run);
Определите структуру модели
Для сверточных нейронных сетей см.Шаг за шагом повышайте скорость распознавания рукописных цифр(3)«В этой статье структура сверточной сети определена следующим образом:
CONV -> MAXPOOlING -> CONV -> MAXPOOLING -> FC -> SOFTMAX
Каждый сверточный слой использует функцию активации RELU, код выглядит следующим образом:
function getModel() {
const model = tf.sequential();
const IMAGE_WIDTH = 28;
const IMAGE_HEIGHT = 28;
const IMAGE_CHANNELS = 1;
// In the first layer of out convolutional neural network we have
// to specify the input shape. Then we specify some paramaters for
// the convolution operation that takes place in this layer.
model.add(tf.layers.conv2d({
inputShape: [IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS],
kernelSize: 5,
filters: 8,
strides: 1,
activation: 'relu',
kernelInitializer: 'varianceScaling'
}));
// The MaxPooling layer acts as a sort of downsampling using max values
// in a region instead of averaging.
model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));
// Repeat another conv2d + maxPooling stack.
// Note that we have more filters in the convolution.
model.add(tf.layers.conv2d({
kernelSize: 5,
filters: 16,
strides: 1,
activation: 'relu',
kernelInitializer: 'varianceScaling'
}));
model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));
// Now we flatten the output from the 2D filters into a 1D vector to prepare
// it for input into our last layer. This is common practice when feeding
// higher dimensional data to a final classification output layer.
model.add(tf.layers.flatten());
// Our last layer is a dense layer which has 10 output units, one for each
// output class (i.e. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9).
const NUM_OUTPUT_CLASSES = 10;
model.add(tf.layers.dense({
units: NUM_OUTPUT_CLASSES,
kernelInitializer: 'varianceScaling',
activation: 'softmax'
}));
// Choose an optimizer, loss function and accuracy metric,
// then compile and return the model
const optimizer = tf.train.adam();
model.compile({
optimizer: optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy'],
});
return model;
}
Если у вас есть опыт написания кода tensorflow на Python, приведенный выше код должен быть простым для понимания.
Обучайте модель и следите за ее производительностью во время обучения
Обучение в браузере, вы также можете вводить данные изображения в пакетах, вы можете указать размер пакета, раунды эпохи.
async function train(model, data) {
const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];
const container = {
name: 'Model Training', styles: { height: '1000px' }
};
const fitCallbacks = tfvis.show.fitCallbacks(container, metrics);
const BATCH_SIZE = 512;
const TRAIN_DATA_SIZE = 5500;
const TEST_DATA_SIZE = 1000;
const [trainXs, trainYs] = tf.tidy(() => {
const d = data.nextTrainBatch(TRAIN_DATA_SIZE);
return [
d.xs.reshape([TRAIN_DATA_SIZE, 28, 28, 1]),
d.labels
];
});
const [testXs, testYs] = tf.tidy(() => {
const d = data.nextTestBatch(TEST_DATA_SIZE);
return [
d.xs.reshape([TEST_DATA_SIZE, 28, 28, 1]),
d.labels
];
});
return model.fit(trainXs, trainYs, {
batchSize: BATCH_SIZE,
validationData: [testXs, testYs],
epochs: 10,
shuffle: true,
callbacks: fitCallbacks
});
}
По сравнению с кодом Python, у fit есть еще один параметр обратного вызова. Следует отметить, что процесс обучения относительно длительный, и мы не можем заблокировать основной поток браузера, большую часть времени в коде требуются асинхронные методы. А обратные вызовы могут оповещать основной поток об обновлениях.Здесь позаимствована библиотека tfvis для визуализации процесса обучения (по аналогии с tensorboard), но здесь она отображается на веб-странице.
Оцените обученную модель
Набор тестов загружается в оценку, и код аналогичен версии Python:
function doPrediction(model, data, testDataSize = 500) {
const IMAGE_WIDTH = 28;
const IMAGE_HEIGHT = 28;
const testData = data.nextTestBatch(testDataSize);
const testxs = testData.xs.reshape([testDataSize, IMAGE_WIDTH, IMAGE_HEIGHT, 1]);
const labels = testData.labels.argMax([-1]);
const preds = model.predict(testxs).argMax([-1]);
testxs.dispose();
return [preds, labels];
}
Если мы хотим более интуитивно отображать точность каждой категории и неправильную классификацию, мы можем использовать библиотеку tfvis:
async function showAccuracy(model, data) {
const [preds, labels] = doPrediction(model, data);
const classAccuracy = await tfvis.metrics.perClassAccuracy(labels, preds);
const container = {name: 'Accuracy', tab: 'Evaluation'};
tfvis.show.perClassAccuracy(container, classAccuracy, classNames);
labels.dispose();
}
async function showConfusion(model, data) {
const [preds, labels] = doPrediction(model, data);
const confusionMatrix = await tfvis.metrics.confusionMatrix(labels, preds);
const container = {name: 'Confusion Matrix', tab: 'Evaluation'};
tfvis.render.confusionMatrix(
container, {values: confusionMatrix}, classNames);
labels.dispose();
}
Результаты оценки показаны на рисунке ниже:
О TensorFlow.js
TensorFlow.js ускоряет процесс обучения с помощью WebGL. Если браузер не поддерживает WebGL, ошибки не будет, но она пойдет по пути процессора, и, конечно, скорость будет намного медленнее.
Хотя графический процессор также используется через WebGL, обучать крупномасштабную модель глубокого обучения в браузере нецелесообразно.В настоящее время мы также можем обучить модель на сервере и преобразовать ее в формат модели, доступный в TensorFlow. js. Загрузите модель в браузере и выполните вывод. Обратите внимание на продолжение статьи по этой теме.
В приведенном выше примере есть полный код, нажмите, чтобы прочитать исходный текст, и перейдите к примеру кода, который я создал на github. Кроме того, вы также можете получить доступ непосредственно в своем браузере:Я Lego.club/love/index.contract…, испытайте машинное обучение прямо в браузере.
использованная литература:
вы также можете прочитать
- Шаг за шагом, чтобы улучшить скорость распознавания рукописных цифр(1)(2)(3)
- Введение в TensorFlow.js