Чуть более двух лет назад Google выпустила библиотеку TensorFlow.js, которая расширила возможности TensorFlow из области Python и развертывания на стороне сервера, позволив разработчикам JavaScript создавать и обучать модели машинного обучения в браузерах и веб-приложениях.
В этой статье будут затронуты некоторые основы TensorFlow.js/машинного обучения при создании кода для относительно простой модели для анализа двумерных данных; а именно, квадратные метры и выбросы зданий в Нью-Йорке.
Машинное обучение и TensorFlow
Общая цель машинного обучения — использовать входные данные в виде тензоров для «обучения» модели. Эта модель будет прогнозировать выходные данные или результаты на основе этих входных данных. По мере того, как модель анализирует все больше и больше данных, ее прогнозы результатов будут становиться все более точными.
В общих чертах, шаги для этого в коде:
- Получите некоторые данные и визуализируйте корреляцию / тезис
- Создать модель
- Преобразование данных в тензоры и обучение модели
- Делайте прогнозы и тестируйте модель
В следующих разделах будут разобраны некоторые детали и код каждого из этих шагов. Но сначала, что такое тензор?
Тензоры
Тензоры — это основная структура данных TensorFlow.js. Они представляют собой обобщение векторов и матриц для потенциально более высоких измерений. — Документация API TensorFlow.js
Полное описание тензоров, скорее всего, выходит за рамки какой-либо одной статьи. Но в JavaScript тензоры представляют собой массивы размерностей от одного до n. Размерность (или ранг) можно определить по количеству индексов, необходимых для доступа к значению, например:
const 1DTensor = [1, 2, 3] //one index needed to access a value; e.g. 1DTensor[1] const 2DTensor = [ [1], [2], [3] ] //two indexes needed; 2DTensor[2][0] const 3DTensor = [ [ [1], [2] ], [ [3], [4] ] ] // 3DTensor[0][1][0] //And so on…
Структура и размерность будут зависеть от того, какие данные вы используете и что вы хотите, чтобы ваша модель анализировала и предсказывала. Для этой модели мы будем использовать 2D-тензоры, потому что наши данные являются двумерными, и модель будет обучена принимать массив массивов одного числа (квадратные метры) и учиться предсказывать одно число (выбросы парниковых газов). Входные данные для обучения нашей модели будут выглядеть следующим образом:
[ ..., { squareFootage: 734668, emissions: 2747.2 }, { squareFootage: 380000, emissions: 2752.2 }, ... ]
1. Получите некоторые данные и визуализируйте корреляцию/тезис
Я начал с тезиса о том, что существует корреляция между размером здания и выбросами. В качестве первого шага я собрал эти данные и визуализировал их, в результате чего:
Я быстро понял, почему сбор и визуализация — рекомендуемый первый шаг. Очевидно, этот тезис нуждался в уточнении, поэтому я решил найти корреляцию только между жилыми зданиями в Манхэттене,с некоторой фильтрацией, и получил:
С ним гораздо проще работать. Это было достигнуто с помощью кода ниже:
//fetching data from Socrata Open Data API with query parameters to only receive residential buildings in Manhattan const bldgData = await fetch("https://data.cityofnewyork.us/resource/28fi-3us3.json?largest_property_use_type=Multifamily Housing&$where=starts_with(bbl_10_digits, '1')&$select=dof_gross_floor_area_ft,total_ghg_emissions_metric&$limit=20000"); const bldgDataJSON = await bldgData.json(); //rephrasing keys and turning strings into numbers const cleanedData = bldgDataJSON.map((bldg) => ({ emissions: Number(bldg.total_ghg_emissions_metric), squareFootage: Number(bldg.dof_gross_floor_area_ft), })) //filtering outlier cases, i.e. buildings over 800,000 sqft and more than 6,000 tons of ghg emissions/year .filter((bldg) => bldg.emissions != 0 && bldg.emissions < 6000 && bldg.emissions !== undefined && bldg.squareFootage !== 0 && bldg.squareFootage < 800000 && bldg.squareFootage !== undefined); //assigning values to x and y axes const values = await cleanedData.map((d) => ({ x: d.squareFootage, y: d.emissions, })); //rendering scatterplot graph with tfvis tfvis.render.scatterplot( { name: "Square Footage v Emissions" }, { values }, { xLabel: "Square Footage", yLabel: "Emissions", height: 300, zoomToFit: false });
2. Создайте модель
Для этого я создам последовательную модель, что означает, что каждый слой линейно передает следующий слой, и что все выходные данные одного слоя являются входными данными для другого. Слои «сложены», полностью связаны, между ними нет ответвления или пропуска.
Эта модель будет использовать четыре слоя. Входной слой:
model.add(tf.layers.dense({ inputShape: [1], units: 1 }));
Два «скрытых» слоя:
model.add(tf.layers.dense({units: 3})); model.add(tf.layers.dense({units: 3}));
И выходной слой:
model.add(tf.layers.dense({ units: 1 }));
Итак, что же представляют собой эти сложенные, полностью связанные слои и что они делают? Они состоят из узлов или «единиц», каждая из которых представляет отдельную функцию набора данных, который передается в модель. Каждый узел собирает, анализирует и/или выводит нормализованные и «взвешенные» тензорные данные в зависимости от их положения в стеке. Структура может быть примерно такой, как показано ниже:
Более подробное объяснение роли слоев в искусственной нейронной сети см. в этой статье Адриана Яворски.
3. Преобразование данных в тензоры
Сначала мы преобразуем данные в тензоры, используя следующие шаги:
- Оберните функции и операции в tf.tidy() для предотвращения утечек памяти или неиспользуемых выделений памяти.
- Перетасуйте данные с помощью tf.util.shuffle(), чтобы каждый пакет данных, подаваемых в модель, сортировался случайным образом. Это не позволяет модели изучать вещи в зависимости от порядка подачи данных.
- Отделите входные данные (квадратные метры) и выходные данные (излучение) от данных, создайте 2D-тензоры для каждого с помощью tf.tensor2d() и обрежьте значения выбросов с помощью clipByValue().
- Нормируйте данные в диапазоне от 0 до 1, используя методы .min() и .max(), которые извлекают самое низкое и самое высокое значение из каждого тензора соответственно. Затем применяются методы .sub() и .div() для преобразования квадратных метров и выбросов в значения от 0 до 1. Это делается потому, что большинство моделей TensorFlow предназначены для работы с небольшими числами, с общими диапазонами от 0 до 1. или -1 к 1.
tf.tidy(() => { tf.util.shuffle(data); const inputs = data.map((d) => d.squareFootage); const outputs = data.map((d) => d.emissions); const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]).clipByValue(0, 900000); const outputTensor = tf.tensor2d(outputs, [outputs.length, 1]).clipByValue(0, 5000); const inputMax = inputTensor.max(); const inputMin = inputTensor.min(); const outputMax = outputTensor.max(); const outputMin = outputTensor.min(); const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin)); const normalizedOutputs = outputTensor.sub(outputMin).div(outputMax.sub(outputMin)); });
4. Обучите модель
В TensorFlow есть два метода, которые мы будем использовать для обучения модели: model.compile() и model.fit().
Метод .compile() подготавливает модель к обучению и оценке. Он принимает объект конфигурации, который включает в себя:
- Оптимизатор, который изменяет вес данных, подаваемых на узлы на каждом уровне, чтобы минимизировать потери. В этой модели будет использоваться .adam(), который представляет собой алгоритм адаптивной оптимизации скорости обучения, разработанный специально для обучения глубоких нейронных сетей.
- Функция потерь. По умолчанию для задач регрессии используется среднеквадратическая ошибка (MSE), которая вычисляет среднее значение квадратов разностей между прогнозируемыми и фактическими значениями. Цель обучения — максимально приблизить это значение к нулю.
Метод .fit() фактически обучает модель. Он асинхронный и принимает входы, выходы и объект конфигурации, который сам содержит:
- batchSize: указывает количество выборок данных на обновление градиента. Значение по умолчанию — 32.
- Эпохи: количество повторений массивов обучающих данных. Я обнаружил, что 10 достаточно для достижения приемлемого MSE.
5. Протестируйте модель
Это снова начнется с tf.tidy() по тем же причинам, что и раньше, и в рамках этого мы сделаем следующее:
- Используйте .linspace() для возврата равномерно распределенной последовательности чисел за заданный интервал. Поскольку наши нормализованные данные находятся между 0 и 1, linspace() сделает то же самое 100 раз.
- Используйте метод .predict(), который будет принимать значения linspace ранее в качестве входных данных и преобразовывать их в тензор соответствующей формы; то есть массив из 100 массивов одного значения.
- Не нормализуйте данные, используя методы .mul(), .sub() и .add().
- Используйте .dataSync(), чтобы заблокировать поток пользовательского интерфейса, пока значения тензора не будут готовы, что предотвращает потенциальные проблемы с производительностью.
- Мы создадим прогнозируемые точки из значений x, возвращаемых из .linspace(), и реальные точки из фактических входных данных, используемых для обучения модели, а затем сравним их на другой диаграмме рассеяния.
const [xs, preds] = tf.tidy(() => { const xs = tf.linspace(0, 1, 100); const preds = model.predict(xs.reshape([100, 1])); const unNormXs = xs.mul(inputMax.sub(inputMin)).add(inputMin); const unNormPreds = preds.mul(labelMax.sub(labelMin)).add(labelMin); return [unNormXs.dataSync(), unNormPreds.dataSync()]; }); const predictedPoints = Array.from(xs).map((val, i) => { return { x: val, y: preds[i] }; }); const originalPoints = inputData.map((d) => ({ x: d.squareFootage, y: d.emissions, })); tfvis.render.scatterplot( { name: "Model Predictions vs Original Data" }, { values: [originalPoints, predictedPoints], series: ["original", "predicted"], }, { xLabel: "Square Footage", yLabel: "Emissions", height: 300, } );
Окончательный результат показан ниже, а оранжевая линия значений представляет предсказанные результаты сверху: