Применение машинного обучения в Google таблицах с помощью библиотеки Tensorflow.js и Google Apps скрипта

Применение машинного обучения в Google таблицах с помощью библиотеки Tensorflow.js и Google Apps скрипта

Тема машинного обучения сейчас очень актуальна и продолжает набирать обороты. Машинное обучение — это алгоритм, с помощью которого система распознает данные и их закономерности, предсказывает значения на основе обученной модели.

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

В статье приведен пример использования кодов для предсказания новой величины на базе признаков, которые влияют на эту величину. Вы можете использовать приведенный алгоритм, например, если хотите предсказать число онлайн-покупок какого-то товара, если у вас есть данные по продажам за предыдущие периоды, а также показатели (числовые признаки), которые влияют на онлайн-покупки (например, число показов рекламы, число кликов, число заинтересованных посетителей, общее число посетителей, CTI, VTR, CTR, CTB и др.).

Эта статья покажет вам, как вы можете настраивать, обучать и прогнозировать данные электронных таблиц с помощью фреймворка глубокого обучения Tensorflow.js. Вам не нужно вызывать REST API или использовать сторонние хранилища и алгоритм. Все ваши данные остаются в безопасной Google таблице.

Пример применения машинного обучения в Google таблицах:

Google недавно представил новый JavaScript runtime (V8 engine) в Google Apps скрипте.

Он усиливает платформу G Suite для новых вариантов использования автоматизации. Он заменяет старый Mozilla's Rhino JavaScript интерпретатор и позволяет вам включить современные библиотеки JavaScript.

TensorFlow изначально был для Python, но Google позже добавил поддержку большего количества языков программирования. (nodejs. JavaScript, Swift,..). Keras высокоуровневая нейронная сеть API наряду с TensorFlow. Она подходит для новичков и помогает строить нейронные сети. TensorFlow - это структура, основанная на JavaScript framework, для построения нейронных сетей и синтаксиса, который похож на Keras.

Тема машинного обучения является очень комплексной. Она содержит множество примеров использования, проектирования архитектур, настроек и небольших доработок.

Цель статьи не в том, чтобы показать пошаговое учебное пособие, которое будет охватывать машинное обучение, а в том, чтобы вдохновить и показать еще одну точку зрения о возможностях сочетания Google таблиц и Google Apps скрипта.

Дисклеймер: здесь пришлось прибегнуть к небольшому взлому, чтобы подключить библиотеку Tensorflow.js. Нельзя гарантировать, что вы получите 100% точность результата.

Использование

Полагаю, у вас много данных в Google таблицах. Представьте сценарий, в котором на основе нескольких столбцов (с цифрами) вы хотите предсказать значение в последнем столбце. Это полезно, если вы хотите прогнозировать будущие значения из прошлых значений или некоторые значения отсутствуют, и вы можете заполнить пробелы. Этот сценарий называется многомерной регрессией.

Развертывание Tensorflow.js в Google Apps скрипте

Я скопировал всю библиотеку Tensorflow.js в однофайловый код в проект Google Apps скрипта как файл tf-js.gs.

Мне нужно было подготовить библиотеку Tensorflow.js перед тренировкой и предсказанями. Во-первых, библиотека использует имя global для глобальной переменной. Это было проще, потому что я только определил новую переменную и добавил новую строку кода:

Во-вторых, библиотека Tensorflow.js использует родные API "измерения времени" - в частности, Performance.now() или process.hrtime().

Performance.now() доступна только в API браузера (Chrome) и process.hrtime() доступна только в API языка бэкэнда (node.js). Появилась ошибка "Не могу измерить время в этой среде. Необходимо запустить tf.js в браузере или в Node.js" в Google Apps скрипте, потому что я не смог использовать первый и второй методы.

Я не полностью перепроектировал библиотеку, но думаю, что время измерения используется для получения основного потока для других задач. По этой причине я устанавливаю yieldEvery, что никогда не делал во время компиляции модели. (https://js.tensorflow.org/api/latest/)

Данные

Набор данных Boston Housing Prices - это "hello world" входная задача в мир машинного обучения. Это коллекция из 500 простых записей о недвижимости, собранных в Бостоне (штат Массачусетс) в конце 1970-х годов. Каждая строка включает цифровые измерения района Бостона (например, уровень преступности, типичный размер домов, насколько данный район удален от ближайшей автомагистрали, есть ли в этом районе набережная...).

Эти столбцы названы как features(признаки) (=ввод в модель машинного обучения).

Мы хотим предсказать цену дома в соответствии с этим набором данных. Этот столбец - один и его название - target(цель) (=выход из модели машинного обучения).

Эта подготовленная функция загружает набор данных из Облачного хранилища Google напрямую в Google таблицу.

}

function loadCSV(type) {

let csv;

const BASE_URL = 'https://storage.googleapis.com/tfjs-examples/multivariate-linear-regression/data/';

csv = UrlFetchApp.fetch(BASE_URL + type.features).getContentText()

let features = Utilities.parseCsv(csv);

csv = UrlFetchApp.fetch(BASE_URL + type.target).getContentText()

let target = Utilities.parseCsv(csv)

let data = features.map((row, index) => {

row.push(target[index][0]);

return row

});

return data;

}

let sheet = SpreadsheetApp.getActiveSheet();

let trainRows = loadCSV(train);

let testRows = loadCSV(test);

let data = trainRows.concat(testRows);

sheet.getRange(sheet.getLastRow() + 1, 1, data.length, data[0].length).setValues(data);

} gas-load-boston-dataset.js наGitHub:

Подготовка данных

Мы должны разделить данные на 2 группы: для обучения, для тестирования. Переменная rowSplit определяет номер строки для этого разделения. В нашем случае в качестве обучающего набора данных будут использоваться строки c 2 по 336. Остальные ряды (с 337 по 507) - в качестве тестового набора данных. Переменные FEATURE_COLUMN_FROM и FEATURE_COLUMN_TO определяют столбцы признаков для обучения, тестирования и прогнозирования. В нашем случае данные с признаками загружены в столбцы с 1 по 12.

function getData_(sheet) {

let data = { trainFeatures: [], trainTarget: [], testFeatures: [], testTarget: [], prediction: [] };

let range = sheet.getActiveRange();

//let labelColumn = range.getColumn();

let fromRow = range.getRow();

let toRow = fromRow + range.getNumRows() - 1;

Logger.log("Prediction rows: %s - %s", fromRow, toRow);

let values = sheet.getDataRange().getValues();

values.forEach((row, _index) => {

let rowIndex = _index + 1;

if (rowIndex === 1) return

let features = row.slice(FEATURE_COLUMN_FROM - 1, FEATURE_COLUMN_TO);

let label = row[TARGET_COLUMN - 1];

if (fromRow <= rowIndex && rowIndex <= toRow) {

data.prediction.push(features);

} else {

if (rowSplit <= rowIndex) {

data.testFeatures.push(features)

data.testTarget.push([label]);

} else {

data.trainFeatures.push(features);

data.trainTarget.push([label]);

}

}

});

Logger.log("Train: %s. Test: %s. Prediction: %s", data.trainFeatures.length, data.testFeatures.length, data.prediction.length);

return data;

}

gas-tf-getData.js наGitHub: В качестве последнего шага выберите диапазон в Google таблице. Мы хотим оценить значения в столбце M в соответствии со значениями A- L в выбранных строках 7-9.

Tensorflow работает не со структурой данных Array, а с многомерными массивами данных. Функция createTensor() создает 2D многомерный массив данных.

function createTensor_(array) {

//Logger.log("shape[%s,%s]", array.length, array[0].length);

const tensor = tf.tensor2d(array, [array.length, array[0].length]);

return tensor;

}

gas-tf-createTensor.js на GitHub:

Некоторые признаки (столбцы) содержат значения в разной шкале (например, значения налогов 187 - 711) по сравнению с другими (например, уровень преступности 0,01 - 88,98). Мы должны нормализовать и трансформировать значения, что улучшит производительность и тренировочную стабильность модели.

/**

* Calculates the mean and standard deviation of each column of an array.

*

* @param {Tensor2d} data Dataset from which to calculate the mean and

* std of each column independently.

*

* @returns {Object} Contains the mean and std of each vector

* column as 1d tensors.

*/

function determineMeanAndStddev_(data) {

const dataMean = data.mean(0);

const diffFromMean = data.sub(dataMean);

const squaredDiffFromMean = diffFromMean.square();

const variance = squaredDiffFromMean.mean(0);

const dataStd = variance.sqrt();

return { dataMean, dataStd };

}

/**

* Given expected mean and standard deviation, normalizes a dataset by

* subtracting the mean and dividing by the standard deviation.

*

* @param {Tensor2d} data: Data to normalize.

* Shape: [numSamples, numFeatures].

* @param {Tensor1d} mean: Expected mean of the data. Shape [numFeatures].

* @param {Tensor1d} std: Expected std of the data. Shape [numFeatures]

*

* @returns {Tensor2d}: Tensor the same shape as data, but each column

* normalized to have zero mean and unit standard deviation.

*/

function normalizeTensor_(data, dataMean, dataStd) {

return data.sub(dataMean).div(dataStd);

}

gas-tf-normalize.js hosted на GitHub:

Создание модели и обучение

Как вы уже знаете, сети глубокого обучения содержат больше слоев с нейронами. Нам нужно определить архитектуру нейросети слой за слоем. Синтаксис подобен упомянутому Keras. У нас есть архитектура с двумя слоями, и каждый из них содержит 50 нейронов.

Здесь функции активации (Sigmoid) в каждом скрытом слое.

Последний слой содержит только один нейрон с функцией активации по умолчанию (линейной). Она линейная, потому что наш пример - регрессионный случай использования.

Следующий шаг - компиляция. На этом шаге нужно установить

   - optimizer (оптимизатор) (стохастический градиентный спуск), как найти лучшее решение и веса нейрона

   - loss function (функция потерь) (meanSquaredError), как измерить оптимальное решение

Теперь время для тренировок. Метод .fit() тренирует модель по данным в течение нескольких итераций (=EPOCHS).

Эти значения, такие как количество эпох, количество слоев, количество нейронов, тип функции активации являются гиперпараметрами. Специалисты по данным по всему миру настраивают эти значения и сравнивают их с предыдущими настройками.

Подробнее о настройках в Tensorflow.JS API https://js.tensorflow.org/api/latest/.

/*

Создать нейросеть

*/

async function createModel_(features, target) {

const model = tf.sequential();

model.add(tf.layers.dense({units: 50, activation: "sigmoid", kernelInitializer: 'leCunNormal', inputShape: [features.shape[1]] }));

model.add(tf.layers.dense({units: 50, activation: "sigmoid", kernelInitializer: 'leCunNormal',}));

model.add(tf.layers.dense({units: 1}));

model.compile({

optimizer: tf.train.sgd(LEARNING_RATE),

loss: tf.losses.meanSquaredError,

metrics: ['mae']

});

let trainLoss;

let valLoss;

var history = await model.fit(features, target, {

batchSize: BATCH_SIZE,

epochs: EPOCHS,

validationSplit: 0.2,

yieldEvery: "never",

callbacks: {

onEpochEnd: async (epoch, logs) => {

trainLoss = logs.loss;

valLoss = logs.val_loss;

console.log(`Epoch ${epoch + 1} / ${EPOCHS}. Train loss: ${trainLoss}`);

}

}

});

return { model, trainLoss, valLoss };

}

gas-tf-createModel.js наGitHub:

Оценка позволяет проверить точность вашей модели. Чем меньше потерь, тем лучше. Вы должны сравнить потери при обучении с потерями при тестировании. Большие потери обучения означают Переобучение. Это не идеально.

Предсказание

Когда мы довольны качеством нашей модели и величина потерь - оптимальна, можно предсказывать значения признаков. Также необходимо преобразовать значения предсказания массива в многомерные массивы данных и нормализовать.

В нашем коде фрагмент прогнозируемых значений сохраняется в ячейке Notes, и вы можете сравнить его с исходными значениями.

Это главная функция, которая загружает, подготавливает, тренирует данные.

async function main() {

let sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()

let data = getData_(sheet);

let trainFeatures = createTensor_(data.trainFeatures);

let trainTarget = createTensor_(data.trainTarget);

let testFeatures = createTensor_(data.testFeatures);

let testTarget = createTensor_(data.testTarget);

//computeBaseline_(trainTarget, testTarget);

let { dataMean, dataStd } = determineMeanAndStddev_(trainFeatures); // Normalize mean standard

trainFeatures = normalizeTensor_(trainFeatures, dataMean, dataStd);

testFeatures = normalizeTensor_(testFeatures, dataMean, dataStd);

let ml = await createModel_(trainFeatures, trainTarget);

const evalResult = ml.model.evaluate(testFeatures, testTarget, { batchSize: BATCH_SIZE })

const testLoss = evalResult[0].dataSync()[0];

console.log(`Train-set loss: ${ml.trainLoss.toFixed(4)}\n\tValidation-set loss: ${ml.valLoss.toFixed(4)}\n\tTest-set loss: ${testLoss.toFixed(4)}`);

let predictionFeatures = createTensor_(data.prediction);

predictionFeatures = normalizeTensor_(predictionFeatures, dataMean, dataStd);

let result = ml.model.predict(predictionFeatures).dataSync();

Logger.log("Prediction: %s", result);

let values= Object.keys(result).map( index => [result[index]]);

let range = sheet.getActiveRange();

sheet.getRange( range.getRow(),range.getLastColumn(), values.length, 1 ).setNotes(values)

}

gas-tf-main.js наGitHub: