機械学習はデータがないと始まらないので、まずはボストンデータセットの読み込みから始めます。
以降では、「boston-housing」で使用されているコードを参考にしています。ただし、TensorFlow.jsのサンプルは一般に非常に難しく書かれていて、そのくせポイントが分かりづらいように思えるので、Node.jsやJavaScriptクラスの使用はやめ、できるだけ平易な書き方をするようにしています。
「boston-housing」サンプルでは、CSVファイルの読み込みと解析にNode.jsのpapaparseモジュールが使用されているので、ここでも同様にPapa Parseライブラリを使用することにします。
<script src="https://fastcdn.org/Papa-Parse/4.1.2/papaparse.min.js"></script>
次のJavaScriptコードを記述し、data.jsファイルとして保存します。
// https://github.com/tensorflow/tfjs-examples/blob/master/boston-housing/data.js
const BASE_URL = 'https://storage.googleapis.com/tfjs-examples/multivariate-linear-regression/data/';
const TRAIN_FEATURES_FN = 'train-data.csv';
const TRAIN_TARGET_FN = 'train-target.csv';
const TEST_FEATURES_FN = 'test-data.csv';
const TEST_TARGET_FN = 'test-target.csv';
const BOSTONDATA_NUMFEATURES = 12;
/**
* 与えられたCSVデータを、数値の配列を含む配列として返す
*
* @param {Array<Object>} data ダウンロードしたデータ
*
* @returns {Promise.Array<number[]>} dataを浮動小数点数を持つ値として解析し解決する
*/
const parseCsv = async(data) => {
return new Promise(resolve => {
data = data.map((row) => {
return Object.keys(row).map(key => parseFloat(row[key]));
});
console.log('CSVの解析終了');
resolve(data);
});
};
/**
* CSVファイルをダウンロードして返す
*
* @param {string} filename ダウンロードするファイルの名前
*
* @returns {Promise.Array<number[]>} 解決すると解析されたCSVデータになる
*/
const loadCsv = async(filename) => {
return new Promise(resolve => {
const url = `${BASE_URL}${filename}`;
console.log(`${url}からデータをダウンロード中...`);
// Papa Parseライブラリ https://www.papaparse.com/
Papa.parse(url, {
download: true,
header: true,
complete: (results) => {
console.log('ダウンロード終了');
resolve(parseCsv(results['data']));
}
})
});
};
/*
/**
* フィッシャーイェーツのシャッフル アルゴリズムを使って、
* データとターゲットを(関係性を維持しながら)シャッフルする。
*/
const shuffle = (data, target) => {
let counter = data.length;
let temp = 0;
let index = 0;
while (counter > 0) {
index = (Math.random() * counter) | 0;
counter--;
// data:
temp = data[counter];
data[counter] = data[index];
data[index] = temp;
// target:
temp = target[counter];
target[counter] = target[index];
target[index] = temp;
}
};
つづいて、次のJavaScriptをnormalization.jsという名前で保存します。
// https://github.com/tensorflow/tfjs-examples/blob/master/boston-housing/normalization.js
/**
* データ配列の各列の平均と標準偏差を計算する
*
* @param {Tensor2d} data 各列の平均と標準偏差を別々に計算するデータセット
*
*
* @returns {Object} 各ベクトル列の平均と標準偏差を1dテンソルとして含む
*/
const determineMeanAndStddev = (data) => {
// 各データの値の平均
const dataMean = data.mean(0);
// 各データの値と平均との差
const diffFromMean = data.sub(dataMean);
// 各データの値と平均との差の2乗
const squaredDiffFromMean = diffFromMean.square();
// 各データの値と平均との差の2乗の平均
const variance = squaredDiffFromMean.mean(0);
// 各データの値と平均との差の2乗の平均の平方根
const dataStd = variance.sqrt();
return {
dataMean, dataStd
};
}
/**
* 与えられた期待平均値と標準偏差を使って、与えられたデータセットに対し
* 平均を引き、標準偏差で割ることで、データセットを標準化する
*
* @param {Tensor2d} data: 標準化するデータ。 Shape: [batch, numFeatures].
* @param {Tensor1d} dataMean: データの期待平均値。Shape [numFeatures].
* @param {Tensor1d} dataStd: データの期待標準偏差。Shape [numFeatures]
*
* @returns {Tensor2d}: データと同じshapeで、平均が0、標準偏差が1になるように、
* 各列が標準化されたテンソル
*/
const normalizeTensor = (data, dataMean, dataStd) => {
return data.sub(dataMean).div(dataStd);
}
HTMLファイルには、data.jsとnormalization.jsファイルを読み込みます。メインのJavaScriptには、まず次のコードを記述します。これにより、Googleの提供する4つのCSVファイルをダウンロードして、数値をまとめた配列を4つの変数に割り当てることができます。
// Googleの提供する4つのCSVファイルをダウンロードして解析し、数値として配列に入れる。
const [trainFeatures, trainTarget, testFeatures, testTarget] =
await Promise.all([
loadCsv(TRAIN_FEATURES_FN), loadCsv(TRAIN_TARGET_FN),
loadCsv(TEST_FEATURES_FN), loadCsv(TEST_TARGET_FN)
]);
ここでは、data.jsに記述したloadCsv()関数にダウンロードするCSVファイルへのパスを渡して呼び出しています。loadCsv()関数では、Papa ParseライブラリのPapa.parse()によってCSVファイルがダウンロードされ解析されます。解析結果はその後parseCsv()関数に渡され、数値を要素として持つ配列の配列が返されます。
たとえば、変数trainFeaturesは、数値の要素を12個持つ、次のような配列を333個持つ配列です。
[0.32543, 0, 21.89, 0, 0.624, 6.431, 98.8, 1.8125, 4, 437, 21.2, 15.39]
また変数trainTargetは、数値の要素を1個持つ、次のような配列を333個持つ配列です。
[18]
メインのJavaScriptではその後、data.jsに記述したshuffle()関数を使って4つのデータを(関係性を保ったまま)シャッフルし、それぞれをtf.Tensorオブジェクトに変換します。
// シャッフルする。
shuffle(trainFeatures, trainTarget);
shuffle(testFeatures, testTarget);
// CSVからのデータをtf.Tensorオブジェクトに変換する
const trainFeaturesTF = tf.tensor2d(trainFeatures, [333, 12], 'float32');
const trainTargetTF = tf.tensor2d(trainTarget, [333, 1], 'float32');
const testFeaturesTF = tf.tensor2d(testFeatures, [173, 12], 'float32');
const testTargetTF = tf.tensor2d(testTarget, [173, 1], 'float32');
そしてその後が実は難しいのですが、扱うデータを標準化します。標準化にはデータの平均値と標準偏差と呼ばれるものが必要なので、それをnormalization.jsファイルに記述したdetermineMeanAndStddev()関数を使ってもとめ、normalizeTensor()関数で標準化します。
// trainFeaturesTFデータの各属性値の平均と標準偏差を計算する
let { dataMean, dataStd } = determineMeanAndStddev(trainFeaturesTF);
// 訓練用とテスト用のデータを標準化する。
const trainFeaturesNormalized = normalizeTensor(trainFeaturesTF, dataMean, dataStd);
const testFeaturesNormalized = normalizeTensor(testFeaturesTF, dataMean, dataStd);
本記事ではその後さらに、標準化したテスト用データを評価用とテスト用に分けています。
// 標準化したテスト用データをさらに、評価用とテスト用に分ける。
const [testFeaturesNormalized_evaluate, testFeaturesNormalized_test] = testFeaturesNormalized.split([170, 3]);
// テスト用の目的変数のデータも同様の比率で、評価用とテスト用に分ける
const [testTargetTF_evaluate, testTargetTF_test] = testTargetTF.split([170, 3]);
次回は、こんな機会でもなければ、ぼーっと生きていなくても一生出会うことはないだろうと思われるデータの標準化や標準偏差などについて見ていきます。