专栏首页月色的自留地TensorFlow从1到2(十五)(完结)在浏览器做机器学习

TensorFlow从1到2(十五)(完结)在浏览器做机器学习

TensorFlow的Javascript版

TensorFlow一直努力扩展自己的基础平台环境,除了熟悉的Python,当前的TensorFlow还实现了支持Javascript/C++/Java/Go/Swift(预发布版)共6种语言。 越来越多的普通程序员,可以容易的在自己工作的环境加入机器学习特征,让产品更智能。

在Javascript语言方面,TensorFlow又分为两个版本。一个是使用node.js支持,用于服务器端开发的@tensorflow/tfjs-node。安装方法:

npm install @tensorflow/tfjs-node
# ...GPU版本...
npm install @tensorflow/tfjs-node-gpu

另一个则是在浏览器中就可以使用的前端机器学习包@tensorflow/tfjs。安装方法:

npm install @tensorflow/tfjs

前者跟Python的版本一样,可以工作在单机、工作站、服务器环境。后者则只需要支持HTML5的浏览器就能良好的执行,浏览器版本目前还不支持GPU运算。

浏览器机器学习快速入门

浏览器版本的TensorFlow是其家族中性能最弱的一个发布,但很可能也是容易产生最多应用的版本。毕竟无需考虑运行环境,浏览即执行,能最大限度上降低对用户的额外要求。 我觉得将来很可能发展为在服务器端通过GPU支持完成模型的开发和训练,然后浏览器作为最方便的客户端只用来完成预测和反馈给用户直接的结果。

很多前端程序员还不喜欢使用node.js和npm帮助管理整体开发。所以我们直接从网页入手。而且这种方式,也更容易让人理解程序完整的运行方式。 首先是基础的网页,我在下面给出一个模板。TensorFlow.js的开发,都集中在js程序中,所以这个网页可以保存下来。不同的项目,只要更换不同的js程序就好。

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>TensorFlow.js 练习</title>
  <!-- 引入机器学习库TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
  <!-- 引入机器学习可视化库tfjs-vis -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>
  <!-- 机器学习主程序(自己编写) -->
  <script src="script.js"></script>
</head>
<body>
  <!-- 放你的网页内容 -->
  本页无正文<p />
</body>
</html>

其实就是一个空白的网页,分别引入了三个js文件。第一个js是TensorFlow的主要库,必不可少。第二个是用于TensorFlow可视化图表显示的,在正式发布的程序中根据需要使用。第三个是自己编写的程序。

接着我们使用《TensorFlow从1到2(七)》中,油耗预测的数据集,也完成一个简单的油耗预测的示例。 原始的数据结构请到第七篇中查看。这里为了js处理的方便,已经预先转成了json格式。下面是头两条记录的样子:

[
  {
    "Name": "chevrolet chevelle malibu",
    "Miles_per_Gallon": 18,
    "Cylinders": 8,
    "Displacement": 307,
    "Horsepower": 130,
    "Weight_in_lbs": 3504,
    "Acceleration": 12,
    "Year": "1970-01-01",
    "Origin": "USA"
  },
  {
    "Name": "buick skylark 320",
    "Miles_per_Gallon": 15,
    "Cylinders": 8,
    "Displacement": 350,
    "Horsepower": 165,
    "Weight_in_lbs": 3693,
    "Acceleration": 11.5,
    "Year": "1970-01-01",
    "Origin": "USA"
  },
    ...

我们只是想演示TensorFlow.js的使用,所以把问题简化一下,只保留功率数据(Horsepower)和油耗数据(MPG),MPG这里同时也是标注信息。 因为我们做过这个练习,我们知道样本中有无效数据。所以数据预处理的时候,还要把数据做一个清洗(当然数据清洗应当养成习惯)。 随后,浏览器不是命令行,不能简单的在命令行输出信息。这时候轮到TensorFlow-vis出场了,我们做一个二维映射把基础数据显示在屏幕上。 第一步先不走那么快,我们只完成这一部分功能,先执行起来看一看。 下面是完成刚才所说功能的代码,别忘了文件名是script.js,跟index.html要放在同一目录:

// 获取数据,只保留感兴趣的字段,并进行数据清洗
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;
}
// 相当于主程序,执行入口
async function run() {
  //载入数据
  const data = await getData();
  //建立绘图数据
  const values = data.map(d => ({
    x: d.horsepower,
    y: d.mpg,
  }));
  //使用tfvis绘图
  tfvis.render.scatterplot(
    {name: 'Horsepower v MPG'},
    {values}, 
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );
}
//载入后开始执行run()函数
document.addEventListener('DOMContentLoaded', run);

在支持HTML5的浏览器中打开index.html文件就开始了程序执行,因为是本地文件,通常双击打开就可以。程序一开始首先下载样本数据,视网络环境不同,速度会有区别。执行结束后会自动在浏览器的右侧弹出图表窗口显示我们绘制的样本分布图。 除了可能的输入拼写错误,文件下载是最可能出现的问题,如果碰到这种情况,请根据数据文件的路径自行下载到本地来进行试验。

这部分相当于一个Hello World吧。从示例中可以看出,js在数据处理中,虽然没有Python的优势,但对于确定的数据类型也有自己的优点。在图表的显示上更是方便,无需第三方模块的支持。何况大多数现代浏览器也都包括console工具,必要情况下通过输出console的调试信息也可以达到很多目的。 此外有一点需要说明的,是稍微可能耗时的函数,应当尽量使用异步方式,也就是function关键字之前的async。以避免阻塞整个程序的执行。 当然使用了异步方式,程序的整体逻辑一定要多思考,想清楚,避免执行过程中顺序混乱。

用js定义模型

TensorFlow.js完整模仿了Keras的模型定义方式,所以如果使用过Keras,那使用TensorFlow.js完全无压力。 下面就是本例中的模型定义:

// 建立神经网络模型
function createModel() {
    // 使用sequential对象建立模型
    const model = tf.sequential(); 
    // 输入层
    model.add(tf.layers.dense({inputShape: [1], units: 128, useBias: true}));
    // 隐藏层
    model.add(tf.layers.dense({units: 50, activation: 'sigmoid'}));    
    model.add(tf.layers.dense({units: 25, activation: 'sigmoid'}));    
    model.add(tf.layers.dense({units: 5, activation: 'sigmoid'}));    
    // 输出层
    model.add(tf.layers.dense({units: 1, useBias: true}));

    return model;
}

代码中除了没有了tf.keras的关键字其它没有什么特殊的东西。你可能也注意到了,定义模型操作本身速度是很快的,并不需要异步执行。 模型定义完成后,可视化工具提供了modelSummary方法,用于将模型显示在浏览器中供用户检查。

// 在图表窗口显示模型摘要信息
tfvis.show.modelSummary({name: 'Model Summary'}, model);

数据预处理

在数据载入的时候我们已经进行了一些预处理的工作。这个数据预处理主要是指把js数据转换为TensorFlow处理起来更高效的张量类型。此外还需要做数据的规范化。 在这里有很重要的一点需要说明。js语言在大规模数据的处理上,不如Python的高效。当然这一定程度上是浏览器的限制。 其中最突出的问题是内存的垃圾回收,这个问题困扰js已久,相信不做机器学习你也碰到过。而同时,用户对于浏览器的内存占用本身也是非常敏感的。 TensorFlow.js为了解决这个问题,专门提供了tf.tidy()函数。使用方法是把大规模的内存操作,放置在这个函数的回调中执行。函数调用完成后,tf.tidy()得到控制权,进行内存的清理工作,防止内存泄露。 其它没有什么需要特殊说明的,可以看源码中的注释:

// 将数据转换为张量
function convertToTensor(data) {
  // 数据预处理的过程必然会产生很多中间结果,将占用大量内存
  // tf.tidy()负责清理这些中间结果,所以要把数据处理包含在这个函数之内
  // 这一点很重要
  return tf.tidy(() => {
    // 把样本数据乱序排列
    tf.util.shuffle(data);

    // 将数据转换为张量,功率值作为特征值,油耗值作为标定目标
    const inputs = data.map(d => d.horsepower)
    const labels = data.map(d => d.mpg);

    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
    const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

    // 数据规范化,把数据从最小到最大转换为0-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,
    }
  });  
}

完整代码

程序核心的训练和测试(预测)的代码在TensorFlow中非常简单,我们早就有经验了。唯一需要说明的是,除了跟Python中一样使用model.fit()做训练,以及model.predict()做预测,我们的过程和结果,也会使用TensorFLow-vis图表工具可视化出来,显示在浏览器中。 其中训练部分,是使用回调函数,这种机制我们在Python中也见过。目的是能够动态的显示训练的过程,而不是全部训练枯燥、漫长的等待完成才显示一次。

预测部分的数据少,速度很快,就是执行完成后一次显示。 但预测部分的数据有大量的转换过程,这个过程消耗内存大,所以放在tf.tidy()中执行以防止内存泄露。 好了,代码秀出,请参考注释阅读:

// 获取数据,只保留感兴趣的字段,并进行数据清洗
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;
}
// 相当于主程序,执行入口
async function run() {
  //载入数据
  const data = await getData();
  //建立绘图数据
  const values = data.map(d => ({
    x: d.horsepower,
    y: d.mpg,
  }));
  //使用tfvis绘图
  tfvis.render.scatterplot(
    {name: 'Horsepower v MPG'},
    {values}, 
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );
  // 建立神经网络模型
  const model = createModel();  
  // 在图表窗口显示模型摘要信息
  tfvis.show.modelSummary({name: 'Model Summary'}, model);

  // 将数据从js对象转换为张量,并完成预处理
  const tensorData = convertToTensor(data);

  // 使用样本数据训练模型,训练时只需要x/y的值
  const {inputs, labels} = tensorData;
  await trainModel(model, inputs, labels);
  // 训练完成在console输出完成信息(需要打开浏览器console窗口才能看到)
  console.log('Done Training');
  
  // 使用训练完成的模型进行预测并显示结果
  testModel(model, data, tensorData);
}

// 载入完成执行主函数run()
document.addEventListener('DOMContentLoaded', run);

// 建立神经网络模型
function createModel() {
    // 使用sequential对象建立模型
    const model = tf.sequential(); 
    // 输入层
    model.add(tf.layers.dense({inputShape: [1], units: 128, useBias: true}));
    // 隐藏层
    model.add(tf.layers.dense({units: 50, activation: 'sigmoid'}));    
    model.add(tf.layers.dense({units: 25, activation: 'sigmoid'}));    
    model.add(tf.layers.dense({units: 5, activation: 'sigmoid'}));    
    // 输出层
    model.add(tf.layers.dense({units: 1, useBias: true}));

    return model;
}

// 将数据转换为张量
function convertToTensor(data) {
  // 数据预处理的过程必然会产生很多中间结果,将占用大量内存
  // tf.tidy()负责清理这些中间结果,所以要把数据处理包含在这个函数之内
  // 这一点很重要
  return tf.tidy(() => {
    // 把样本数据乱序排列
    tf.util.shuffle(data);

    // 将数据转换为张量,功率值作为特征值,油耗值作为标定目标
    const inputs = data.map(d => d.horsepower)
    const labels = data.map(d => d.mpg);

    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
    const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

    // 数据规范化,把数据从最小到最大转换为0-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,
    }
  });  
}
async function trainModel(model, inputs, labels) {
  // 编译模型 
  model.compile({
      optimizer: tf.train.adam(),
      loss: tf.losses.meanSquaredError,
      metrics: ['mse'],
  });
  //每批次数据数量和训练迭代数量
  const batchSize = 28;
  const epochs = 25;

  // 训练
  return await model.fit(inputs, labels, {
      batchSize,
      epochs,
      shuffle: true,
      // 使用回调函数绘制训练过程,曲线指标loss/mse
      callbacks: tfvis.show.fitCallbacks(
        { name: 'Training Performance' },
        ['loss', 'mse'], 
        { height: 200, callbacks: ['onEpochEnd'] }
      )
  });
}

// 测试模型
function testModel(model, inputData, normalizationData) {
  // 获取数据集的取值范围
  const {inputMax, inputMin, labelMin, labelMax} = normalizationData;  

  // 防止内存泄露,依然要把大量数据的操作放在tf.tidy值中
  const [xs, preds] = tf.tidy(() => {
      //功率数据直接使用0-1空间,相当于遍历所有样本空间
      const xs = tf.linspace(0, 1, 100);
      //批量预测
      const preds = model.predict(xs.reshape([100, 1]));      
      
      // 预测结果也是规范化的0-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
      }
  );
}

程序执行,最终预测测试的输出结果如下:

结语

本连载目标定位让已经有TensorFlow使用经验的技术人员,快速上手TensorFlow 2.0开发。 不知不觉,连载15篇。但还有很多内容未能包含进来。比如分布式训练、比如图像内容描述等。建议有需要的朋友继续到官网文档中学习。 水平所限,文中错误、疏漏不少,欢迎批评指正。

(连载完)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 日本学者神研究:追剧学英语两不误,眼动数据分析为你定制专属字幕

    但是,这样“学习”的过程可谓十分煎熬了,常常是关了字幕练听力,一会就睡着了;而全部打开字幕的话,我的眼里就只有中文了。一部剧看下来,既没有学到几个单词,还牺牲了...

    大数据文摘
  • 如何玩转AI时代下的新零售,创新奇智新零售聚智沙龙给你答案

    在人工智能和数字化驱动的消费市场中,新零售成为兵家必争之地。据市场调研机构Tractica预计,2025年人工智能将为全球零售业带来368亿美元的收益,这一数字...

    镁客网
  • 一个鲜为人知却可以保护隐私的训练方法:联合学习

    尤其是医疗数据,因为大部分病人不愿意将自己的诊断结果等这类隐私信息拿出来,所以机器学习在疾病诊断和探索潜力被大大压制。

    大数据文摘
  • 【美团技术解析】无人车端到端驾驶模型概述

    技术解析是由美团点评无人配送部技术团队主笔,每期发布一篇无人配送领域相关技术解析或应用实例,本期为您带来的是无人车端到端模型概述

    美团无人配送
  • Quora上的大牛们最喜欢哪种机器学习算法?

    Carlos Guestrin,亚马逊计算机科学机器学习教授,Dato公司ceo及创始人 (Dato原名GraphLab,大数据分析云服务平台)

    大数据文摘
  • 素面变豪华拉面,这位拉面狂魔用GAN和AR实现“味觉转换”

    人工智能已经发展到了今天了,创造出“贾维斯”还有点困难。但是,将素面直接变成拉面......嗯,也挺困难的。

    大数据文摘
  • DeepMind推出首款商业化算法产品!30秒准确诊断眼疾,准确率达到专家水平

    据金融时报报道,DeepMind已打造出一款能够实时诊断复杂眼疾的设备。虽然产品只是原型,但这是Deepmind作为Alphabet旗下公司布局医疗设备战略的重...

    大数据文摘
  • 我们用一下午听完了吴恩达新课,没时间的你看这篇文章就够啦

    去年12月份, 吴恩达发布了一份《AI转型指南》报告,旨在为企业CEO们提供一份企业AI转型的建议,搜集了众多CEO们最的关心问题并在报告中回答。

    大数据文摘
  • GAN之父Ian Goodfellow离职谷歌,加盟苹果神秘机器学习小组

    根据Goodfellow在领英上面的个人资料更新,他已经在3月份跳槽苹果,目前领导苹果的“特殊机器学习项目组”。

    大数据文摘
  • 为何数据科学团队需要通才而非专才

    在“国富论”中,亚当·斯密通过一项钢针工厂流水线的生动例子说明了劳动分工是生产力的主要来源这一观点:“第一个人拔出钢丝,第二个人拉直钢丝,第三个人切割钢丝,第四...

    大数据文摘

扫码关注云+社区

领取腾讯云代金券