1. Введение
В этом уроке мы создадим модель TensorFlow.js для распознавания рукописных цифр с помощью сверточной нейронной сети. Сначала мы обучим классификатор, показав ему тысячи изображений рукописных цифр и их меток. Затем мы оценим точность классификатора, используя тестовые данные, которые модель никогда раньше не видела.
Эта задача считается задачей классификации, поскольку мы обучаем модель присваивать входному изображению категорию (цифру, которая появляется на изображении). Мы будем обучать модель, показывая ей множество примеров входных данных вместе с правильными выходными данными. Это называется обучением с учителем .
Что вы построите
Вам предстоит создать веб-страницу, которая будет использовать TensorFlow.js для обучения модели в браузере. Получив черно-белое изображение определенного размера, она должна будет определить, какая цифра на нем изображена. Шаги выполнения следующие:
- Загрузите данные.
- Определите архитектуру модели.
- Обучите модель и отслеживайте ее производительность в процессе обучения.
- Оцените обученную модель, сделав несколько прогнозов.
Что вы узнаете
- Синтаксис TensorFlow.js для создания сверточных моделей с использованием API слоев TensorFlow.js.
- Формулировка задач классификации в TensorFlow.js
- Как отслеживать процесс обучения в браузере с помощью библиотеки tfjs-vis.
Что вам понадобится
- Последняя версия Chrome или другой современный браузер, поддерживающий модули ES6.
- Текстовый редактор, работающий локально на вашем компьютере или в интернете, например, через Codepen или Glitch .
- Знание HTML, CSS, JavaScript и инструментов разработчика Chrome (или инструментов разработчика вашего предпочитаемого браузера).
- Для общего понимания нейронных сетей рекомендуется ознакомиться с базовыми принципами. Если вам нужно освежить знания или ввести новые темы, посмотрите это видео от 3blue1brown или это видео о глубоком обучении на JavaScript от Аши Кришнан .
Вам также должно быть комфортно работать с материалом нашего первого обучающего урока .
2. Настройка
Создайте HTML-страницу и добавьте JavaScript.
Скопируйте следующий код в HTML-файл с именем...
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TensorFlow.js Tutorial</title>
<!-- Import TensorFlow.js -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
<!-- Import tfjs-vis -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>
<!-- Import the data file -->
<script src="data.js" type="module"></script>
<!-- Import the main script file -->
<script src="script.js" type="module"></script>
</head>
<body>
</body>
</html>
Создайте JavaScript-файлы для данных и кода.
- В той же папке, где находится HTML-файл, создайте файл с именем data.js и скопируйте в него содержимое по этой ссылке .
- В той же папке, что и на первом шаге, создайте файл с именем script.js и поместите в него следующий код.
console.log('Hello TensorFlow');
Проверьте это
Теперь, когда вы создали HTML- и JavaScript-файлы, протестируйте их. Откройте файл index.html в браузере и откройте консоль инструментов разработчика.
Если всё работает, должны быть созданы две глобальные переменные. tf — это ссылка на библиотеку TensorFlow.js, tfvis — это ссылка на библиотеку tfjs-vis .
Вы должны увидеть сообщение Hello TensorFlow . Если это так, вы готовы перейти к следующему шагу.
3. Загрузите данные.
В этом уроке вы будете обучать модель распознаванию цифр на изображениях, подобных приведенным ниже. Эти изображения представляют собой 28x28-пиксельные изображения в оттенках серого из набора данных MNIST .



Мы предоставили код для загрузки этих изображений из специального файла спрайтов (~10 МБ), который мы создали для вас, чтобы мы могли сосредоточиться на этапе обучения.
Вы можете изучить файл data.js , чтобы понять, как загружаются данные. Или, после завершения этого урока, создайте свой собственный способ загрузки данных.
Предоставленный код содержит класс MnistData , имеющий два открытых метода:
-
nextTrainBatch(batchSize): возвращает случайный набор изображений и их меток из обучающего набора. -
nextTestBatch(batchSize): возвращает пакет изображений и их меток из тестового набора.
Класс MnistData также выполняет важные шаги по перемешиванию и нормализации данных.
Всего имеется 65 000 изображений, мы будем использовать до 55 000 из них для обучения модели, сохранив 10 000 изображений для проверки производительности модели после завершения обучения. И всё это мы будем делать в браузере!
Давайте загрузим данные и проверим, правильно ли они загружены.
Добавьте следующий код в файл script.js.
import {MnistData} from './data.js';
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);
Обновите страницу, и через несколько секунд слева появится панель с несколькими изображениями.

4. Сформулируйте нашу задачу.
Наши входные данные выглядят следующим образом.

Наша цель — обучить модель, которая будет принимать одно изображение и учиться предсказывать оценку для каждого из 10 возможных классов, к которым может принадлежать это изображение (цифры от 0 до 9).
Каждое изображение имеет ширину 28 пикселей, высоту 28 пикселей и содержит 1 цветовой канал, поскольку это изображение в оттенках серого. Таким образом, форма каждого изображения — [28, 28, 1] .
Помните, что мы выполняем сопоставление один к десяти, а также учитываем форму каждого входного примера, поскольку это важно для следующего раздела.
5. Определите архитектуру модели.
В этом разделе мы напишем код для описания архитектуры модели. Архитектура модели — это замысловатый способ сказать , «какие функции будет выполнять модель во время выполнения» , или, другими словами , «какой алгоритм будет использовать наша модель для вычисления своих ответов» .
В машинном обучении мы определяем архитектуру (или алгоритм) и позволяем процессу обучения изучить параметры этого алгоритма.
Добавьте следующую функцию в ваш
Файл script.js для определения архитектуры модели.
function getModel() {
const model = tf.sequential();
const IMAGE_WIDTH = 28;
const IMAGE_HEIGHT = 28;
const IMAGE_CHANNELS = 1;
// In the first layer of our convolutional neural network we have
// to specify the input shape. Then we specify some parameters 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;
}
Давайте рассмотрим это подробнее.
Свертки
model.add(tf.layers.conv2d({
inputShape: [IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS],
kernelSize: 5,
filters: 8,
strides: 1,
activation: 'relu',
kernelInitializer: 'varianceScaling'
}));
Здесь мы используем последовательную модель.
Вместо полносвязного слоя мы используем слой conv2d . Мы не можем вдаваться во все подробности работы сверток , но вот несколько ресурсов, которые объясняют базовый принцип их работы:
Давайте разберем каждый аргумент в объекте конфигурации для conv2d :
-
inputShape. Форма данных, которые будут поступать в первый слой модели. В данном случае наши примеры MNIST представляют собой черно-белые изображения размером 28x28 пикселей. Канонический формат для данных изображений —[row, column, depth], поэтому здесь мы хотим задать форму[28, 28, 1]. 28 строк и столбцов — это количество пикселей в каждом измерении, а глубина равна 1, поскольку наши изображения имеют только один цветовой канал. Обратите внимание, что мы не указываем размер пакета в форме входных данных. Слои разработаны таким образом, чтобы не зависеть от размера пакета, поэтому во время вывода вы можете передавать тензор любого размера пакета. -
kernelSize. Размер окна скользящего сверточного фильтра, применяемого к входным данным. Здесь мы устанавливаемkernelSizeравным5, что задает квадратное сверточное окно размером 5x5. -
filters. Количество фильтрующих окон размеромkernelSize, применяемых к входным данным. В данном случае мы применим к данным 8 фильтров. -
strides. «Шаг» скользящего окна — то есть, на сколько пикселей фильтр будет сдвигаться каждый раз при перемещении по изображению. Здесь мы указываем strides равный 1, что означает, что фильтр будет скользить по изображению с шагом в 1 пиксель. -
activation. Функция активации , применяемая к данным после завершения свертки. В данном случае мы применяем функцию выпрямленной линейной активации (ReLU) , которая является очень распространенной функцией активации в моделях машинного обучения. -
kernelInitializer— это метод, используемый для случайной инициализации весов модели, что очень важно для динамики обучения. Мы не будем вдаваться в подробности инициализации, ноVarianceScaling(используемый здесь) обычно является хорошим выбором для инициализации .
Сглаживание нашего представления данных
model.add(tf.layers.flatten());
Изображения представляют собой многомерные данные, и операции свертки, как правило, увеличивают размер исходных данных. Перед передачей их на заключительный слой классификации необходимо преобразовать данные в один длинный массив. Плотные слои (которые мы используем в качестве заключительного слоя) принимают только tensor1d , поэтому этот шаг является распространенным во многих задачах классификации.
Вычислите наше окончательное распределение вероятностей.
const NUM_OUTPUT_CLASSES = 10;
model.add(tf.layers.dense({
units: NUM_OUTPUT_CLASSES,
kernelInitializer: 'varianceScaling',
activation: 'softmax'
}));
Для вычисления вероятностных распределений по 10 возможным классам мы будем использовать полносвязный слой с функцией активации softmax. Класс с наивысшим баллом будет предсказанной цифрой.
Выберите оптимизатор и функцию потерь.
const optimizer = tf.train.adam();
model.compile({
optimizer: optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy'],
});
Мы создаём модель, указывая оптимизатор , функцию потерь и метрики, которые хотим отслеживать.
В отличие от нашего первого урока, здесь мы используем categoricalCrossentropy в качестве функции потерь. Как следует из названия, она используется, когда выходные данные нашей модели представляют собой распределение вероятностей. categoricalCrossentropy измеряет ошибку между распределением вероятностей, сгенерированным последним слоем нашей модели, и распределением вероятностей, заданным нашей истинной меткой.
Например, если наша цифра действительно обозначает 7, мы можем получить следующие результаты.
Индекс | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Истинная метка | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
Прогноз | 0.1 | 0,01 | 0,01 | 0,01 | 0.20 | 0,01 | 0,01 | 0,60 | 0,03 | 0,02 |
Категориальная кросс-энтропия даст одно число, указывающее, насколько похож вектор предсказания на наш истинный вектор меток.
Представление данных, используемое здесь для меток, называется one-hot кодированием и широко применяется в задачах классификации. Каждому классу присваивается вероятность для каждого примера. Когда мы точно знаем, какой должна быть эта вероятность, мы можем установить её равной 1, а остальные — 0. Более подробную информацию о one-hot кодировании можно найти на этой странице .
Другой показатель, который мы будем отслеживать, — это accuracy , которая для задачи классификации представляет собой процент правильных предсказаний от общего числа предсказаний.
6. Обучение модели
Скопируйте следующую функцию в файл script.js.
async function train(model, data) {
const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];
const container = {
name: 'Model Training', tab: 'Model', 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
});
}
Затем добавьте следующий код в ваш файл.
run функцию.
const model = getModel();
tfvis.show.modelSummary({name: 'Model Architecture', tab: 'Model'}, model);
await train(model, data);
Обновите страницу, и через несколько секунд вы увидите графики, отображающие прогресс тренировки.

Давайте рассмотрим это подробнее.
Мониторинг метрик
const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];
Здесь мы определяем, какие метрики будем отслеживать. Мы будем отслеживать потери и точность на обучающем наборе данных, а также потери и точность на валидационном наборе данных (val_loss и val_acc соответственно). Подробнее о валидационном наборе данных мы поговорим ниже.
Подготовьте данные в виде тензоров.
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
];
});
Здесь мы создаём два набора данных: обучающий набор, на котором будем обучать модель, и проверочный набор, на котором будем тестировать модель в конце каждой эпохи. Однако данные из проверочного набора никогда не отображаются модели во время обучения.
Предоставленный нами класс данных упрощает получение тензоров из данных изображения. Однако мы все равно преобразуем тензоры в форму, ожидаемую моделью, [num_examples, image_width, image_height, channels] , прежде чем передать их модели. Для каждого набора данных у нас есть как входные данные (X), так и метки (Y).
return model.fit(trainXs, trainYs, {
batchSize: BATCH_SIZE,
validationData: [testXs, testYs],
epochs: 10,
shuffle: true,
callbacks: fitCallbacks
});
Для запуска цикла обучения мы вызываем метод `model.fit`. Также мы передаем свойство `validationData`, указывающее, какие данные модель должна использовать для самопроверки после каждой эпохи (но не для обучения).
Если модель хорошо работает на обучающих данных, но плохо — на проверочных, это означает, что она, вероятно, переобучается на обучающих данных и плохо обобщается на ранее не встречавшиеся входные данные.
7. Оцените нашу модель.
Точность валидации дает хорошую оценку того, насколько хорошо наша модель будет работать с данными, которые она ранее не видела (при условии, что эти данные в какой-то степени похожи на набор данных для валидации). Однако нам может потребоваться более подробная разбивка производительности по различным классам.
В библиотеке tfjs-vis есть несколько методов, которые могут вам в этом помочь.
Добавьте следующий код в конец файла script.js.
const classNames = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine'];
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];
}
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, tickLabels: classNames});
labels.dispose();
}
Что делает этот код?
- Делает прогноз.
- Вычисляет показатели точности.
- Отображает показатели
Давайте рассмотрим каждый шаг подробнее.
Делайте прогнозы
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];
}
Сначала нам нужно сделать несколько прогнозов. Здесь мы возьмем 500 изображений и предскажем, какая цифра на них изображена (позже вы можете увеличить это число, чтобы протестировать на большем наборе изображений).
Примечательно, что функция argmax выдает нам индекс класса с наибольшей вероятностью. Помните, что модель выдает вероятность для каждого класса. Здесь мы определяем наибольшую вероятность и используем ее в качестве прогноза.
Вы также можете заметить, что мы можем делать прогнозы по всем 500 примерам одновременно. В этом и заключается мощь векторизации, которую предоставляет TensorFlow.js.
Показать точность по классам
async function showAccuracy() {
const [preds, labels] = doPrediction();
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() {
const [preds, labels] = doPrediction();
const confusionMatrix = await tfvis.metrics.confusionMatrix(labels, preds);
const container = { name: 'Confusion Matrix', tab: 'Evaluation' };
tfvis.render.confusionMatrix(container, {values: confusionMatrix, tickLabels: classNames});
labels.dispose();
}
Матрица ошибок похожа на точность классификации для каждого класса, но дополнительно детализирует ее, показывая закономерности неправильной классификации. Она позволяет увидеть, не путается ли модель в отношении каких-либо конкретных пар классов.
Отобразить оценку
Добавьте следующий код в конец вашей функции запуска, чтобы отобразить результаты оценки.
await showAccuracy(model, data);
await showConfusion(model, data);
На экране должно отобразиться изображение, похожее на следующее.

Поздравляем! Вы только что обучили сверточную нейронную сеть!
8. Основные выводы
Задача прогнозирования категорий для входных данных называется задачей классификации.
Для задач классификации требуется соответствующее представление данных для меток.
- Распространенные способы представления меток включают в себя однократное кодирование категорий.
Подготовьте свои данные:
- Полезно отложить некоторые данные, которые модель никогда не видит во время обучения, чтобы использовать их для оценки модели. Это называется проверочным набором данных.
Создайте и запустите свою модель:
- Было показано, что сверточные модели хорошо справляются с задачами обработки изображений.
- В задачах классификации в качестве функции потерь обычно используется категориальная кросс-энтропия.
- Отслеживайте процесс обучения, чтобы увидеть, снижается ли количество ошибок и повышается ли точность.
Оцените свою модель
- Определите способ оценки вашей модели после ее обучения, чтобы увидеть, насколько хорошо она справляется с первоначальной задачей, которую вы хотели решить.
- Матрицы точности и ошибок для каждого класса позволяют получить более детальную оценку производительности модели, чем просто общая точность.