tensorflow.js代码解读之将曲线拟合到二维数据

作者: 张阳君 分类: 前端技术

开车的同学大概都知道,汽车低功率状态下运行油耗会比较大,尤其是市区堵车的情况下,一脚油门一脚刹车,不光糟心还费油。反过来,如果汽车经常跑高速,每公里油耗反而特别低。今天我们就要使用机器学习来研究一下汽车发动机功率和油耗之间的关系。这也是Tensorflow.js上机器学习入门的一个简单例子,但是麻雀虽小五脏俱全,通过本文的学习,相信你也可以跨入机器学习的大门。原文教程在这个地址:https://codelabs.developers.google.com/codelabs/tfjs-training-regression/index.html,我把示例代码整合到了Github上,并做了中文注释,借助github.io,可以直接运行示例:https://github.com/supervergil/tfjs-demo

本文中的机器学习主要是研究发动机功率和每英里油耗之间的关系,我们把发动机功耗作为输入,每英里油耗作为输出。训练完成后,我们可以根据当前发动机功率,预测出汽车的油耗。参考上一篇tensorflow进行机器学习的通用方法论中的流程,我们一步一步来教会机器做简单的数据预测。

  1. 安装tensorflow.js,只需要在html中引入tfjs的库就可以了:
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.2.7/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.1.0/dist/tfjs-vis.umd.min.js"></script>

tfjs-vis是tfjs的一个可视化库,可以查看机器学习的神经网络结构、训练过程和预测拟合程度。

  1. 加载训练数据对,进行格式化和归一化,这里谷歌提供了大量实际测量得到的发动机功率和油耗数据,我们只需要直接抓取过来就行:
// 抓取数据并格式化
async function getData() {
  const carsDataReq = await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json');  
  const carsData = await carsDataReq.json();  
  const cleaned = carsData.map(car => ({
    mpg: car.Miles_per_Gallon,
    horsepower: car.Horsepower,
  }))
  .filter(car => (car.mpg != null && car.horsepower != null));
  return cleaned;
}

getData函数使用fetch抓取了谷歌的JSON数据,并且对数据进行了第一次格式化,将原始数据对转变为编程易用的数组。

// 数据归一化
function convertToTensor(data) {
  return tf.tidy(() => {
    // 洗牌
    tf.util.shuffle(data);

    const inputs = data.map(d => d.horsepower)
    const labels = data.map(d => d.mpg);

    // 转为tensorflow张量格式
    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
    const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

    const inputMax = inputTensor.max();
    const inputMin = inputTensor.min();  
    const labelMax = labelTensor.max();
    const labelMin = labelTensor.min();

    // 数据归一化
    const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
    const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

    return {
      inputs: normalizedInputs,
      labels: normalizedLabels,
      inputMax,
      inputMin,
      labelMax,
      labelMin
    }
  });  
}

convertToTensor函数首先对数据做洗牌操作,保证后期训练的准确性。然后将原始数据转变为tensorflow可读的张量格式。最后,我们对输入和输出的数据做归一化操作(让输入输出映射到0-1之间),保证后期更有效地训练。

  1. 抽取测试数据:

因为本示例是个简单入门,所以并没有抽取测试数据,最后是直接拿原始数据做回归测试,我们可以在tfvis中看到预测值和实际值的拟合情况。

  1. 挑选神经网络并按次序组合它们:
function createModel() {
  const model = tf.sequential(); 
  model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true}));
  model.add(tf.layers.dense({units: 1, useBias: true}));
  return model;
}

因为数据比较简单,这里我们使用了两层全连接网络。由于只有功耗这一个输入值,所以输入的张量形状是[1],输出的神经元数量也为1(油耗)。useBias是神经元权重计算中的偏置量,可以不用显式设为true。

  1. 定义优化器和损失函数,训练模型
async function trainModel(model, inputs, labels) {
  // 定义优化器和损失函数
  model.compile({
    optimizer: tf.train.adam(),
    loss: tf.losses.meanSquaredError,
    metrics: ['mse'],
  });

  const batchSize = 32;
  const epochs = 50;

  // 训练模型
  return await model.fit(inputs, labels, {
    batchSize,
    epochs,
    shuffle: true,
    callbacks: tfvis.show.fitCallbacks(
      { name: 'Training Performance' },
      ['loss', 'mse'], 
      { height: 200, callbacks: ['onEpochEnd'] }
    )
  });
}

简单类型的机器学习使用adam优化算法就够了,然后我们使用均方差作为判断训练成果的参数。训练模型我们使用了分批采样训练,一次采样32条(batchSize)训练数据对,遍历所有样本50次(epochs)。一般来说batchSize越大,epochs也要设置得越大。

  1. 回归测试:
function testModel(model, inputData, normalizationData) {
  const {inputMax, inputMin, labelMin, labelMax} = normalizationData;  

  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.horsepower, y: d.mpg,
  }));


  tfvis.render.scatterplot(
    {name: 'Model Predictions vs Original Data'}, 
    {values: [originalPoints, predictedPoints], series: ['original', 'predicted']}, 
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );
}

这一步就是把历史数据放进来进行预测,最终和实际值做比较,记得预测出来的值要进行反归一化。

  1. 最后我们把上面6步的方法组合起来运行:
async function run() {
  // 加载数据并格式化成tfvis格式,渲染成图表
  const data = await getData();
  const values = data.map(d => ({
    x: d.horsepower,
    y: d.mpg
  }));

  tfvis.render.scatterplot(
    { name: "发动机马力 和 耗油量(英里每加仑) 的关系图" },
    { values },
    {
      xLabel: "发动机马力",
      yLabel: "耗油量",
      height: 300
    }
  );

  // 搭建模型
  const model = createModel();
  tfvis.show.modelSummary({ name: "模型结构" }, model);

  // 将数据转换为tensorflow张量格式
  const tensorData = convertToTensor(data);
  const { inputs, labels } = tensorData;

  // 训练模型
  await trainModel(model, inputs, labels);
  console.log("训练结束!");

  // 测试模型准确率
  testModel(model, data, tensorData);
}

document.addEventListener("DOMContentLoaded", run);

到这里,一个完整的机器学习流程就走完了,tensorflow的每一行代码其实都涉及到很多人工智能的知识,这里就不展开了,如果感兴趣可以逐一搜索了解。上述完整代码请移步:https://github.com/supervergil/tfjs-demo。如果我们跑完整个例子,会发现得到的是一条线段,并没有拟合得特别好,为什么呢?因为我们的全连接层没有使用激活函数,所以不管我们加多少全连接层都无法完美拟合曲线。所以要想拟合得更加到位,可以尝试手动fork下代码,添加activation,然后看看拟合的曲线有什么变化!

(全文完)

0 条评论
回复
支持 Markdown 语法
暂无评论,来抢个沙发吧!