TensorFlow.js:一次基于JavaScript的迁移学习实践

428 阅读18分钟

作者:李知周

2018 年对于 JavaScript 的程序员来说注定是一个不平凡的一年,比如年初 Google 将其基于 JavaScript 技术的机器学习库 machinelearning.js 正式更名为 TensorFlow.js,这也意味着 Google 将 JavaScript 语言升级为其人工智能战略的重要环节。从此 JavaScript 有了自己的机器学习框架,赶上了人工智能的风口,再次华丽转身,而迁移学习能用 JavaScript 实现了。接下来就让笔者带着大家一起学习如何在物联网环境中应用基于 JavaScript 的机器学习技术。

1. TensorFlow.js

TensorFlow.js 是一个开源的基于 WebGL 硬件加速技术的 JavaScript 库,用于训练和部署机器学习模型,其设计理念借鉴于目前广受欢迎的 TensorFlow 深度学习框架。谷歌推出的第一个基于 TensorFlow 的前端深度学习框架是 deeplearning.js 其使用 TypeScript 语言开发, 2018 年 Google 将其重新命名为 TensorFlow.js Core 并在 TypeScript 内核的基础上增加了 JavaScript 的接口以及 TensorFlow 模型导入等工程合成了 TensorFlow.js 深度学习框架,基于浏览器和 Javascript 的 Tensorflow.js 相较与其他深度学习框架具有以下的几个优点。

1)不用安装驱动器和软件,通过链接即可分享程序,随着互联的普及浏览器是目前世界上安装最大的软件工具,现在几乎任何用户设备都会安装有浏览器并能运行 JavaScript 语言。

2)网页应用交互性更强,在互联网时代,网页设计已经成为界面交互设计的标准,互联网公司开源了大量设计美观使用方便的 JavaScript 交互式设计,利用这些交互设计可以很方便得实现人与深度学习算法的交互。

3)有直接访问 GPS 定位、摄像头、麦克风、加速度计、陀螺等传感器,以及各种其他设备的标准 API。 随着手机的普及以及手机浏览器标准的完善,为作为浏览器端的 JavaScript 语言提供了跨平台的标准 API,大大方便了程序包括深度学习程序的开发。

4)安全性, 因为数据都是保存在客户端的,训练的数据无须上传到服务器端。由于基于 JavaScript 的深度学习完全运行于客户端浏览器,无须服务器端干预,训练的数据(比如声音图像)都可以直接通过 JavaScript 的 API 获得,并利用浏览器的 WebGL 环境进行运算,完全不需要上传数据,保证了数据的安全,避免了隐私的泄漏。

TensorFlow.js 作为深度学习框架体和 TensorFlow 一样在底层通过提供基于 Tensor 的数据流图计算支持来完成深度学习的训练。与此同时,TensorFlow.js 同样提供了类似 Keras 的神经网络高层抽象,以方便使用者能够快速、高效地开发神经网络层级模型,而无须陷入到繁杂的底层计算图中。首先看一下 TensorFlow.js 中是如何进行 Tensor 数据流图计算的例子:

const a = tf.tensor([1,2,3]); // 创建一个张量 tensor
a.square().print();// 对 tensor 应用 square 算子,然后打印结果

接着我们再来看一个比较复杂的例子:

function f(x)
{
    return tf.tidy(()=>{ //由于TensorFlow.js使用WebGL环境,为了屏蔽GPU内存管理的细节,TensorFlow.js提供了tf.tidy函数,自动实现GPU内存的回收
     const y = x.square(); //对 x使用square算子
     const z = x.mul(y); //对,x与y使用mul算子
     return z // 最终得到的结果是z=x * x^2
        });
}

上面这段程序所代表的 tensor 计算流图如下所示:

有了对 TensorFlow.js 的基本理解,接着再来看看使用高级的神经网络层 API 的 JavaScript 例子:

<html>
  <head>
    <!-- 从CDN加载TensorFlow.js的核心js文件 -->
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"> </script> 
    <!-- Place your code in the script tag below. You can also use an external .js file -->
<script>         
// 使用TensorFlow.js的sequential API来定义神经网络,这里我们定义最简单的神经网络,
//线性回归模型  
const model = tf.sequential();      
// 创建输入为一维的包含一个神经元的线性神经网络层
model.add(tf.layers.dense({units: 1, inputShape: [1]}));      
// 创建训练误差函数(均方误差),以及优化算法(随机梯度下降)
model.compile({loss: 'meanSquaredError', optimizer: 'sgd'});         
const xs = tf.tensor2d([1, 2, 3, 4], [4, 1]);     //创建训练样本x  
const ys = tf.tensor2d([1, 3, 5, 7], [4, 1]);     //创建训练标签y     
model.fit(xs, ys).then(() => {        // 使用训练样本训练网络      
// 当训练完成后,使用网络对新数据做预测        
model.predict(tf.tensor2d([5], [1, 1])).print();    
});    
</script>
  </head>
  <body>
  </body>
</html>

2. JavaScript 物联网

网络中,HTTP 协议与 JSON 数据格式,特别是 RESTful API 无疑具有支配地位,各种云服务、数据传输都基于这些协议来进行。而 JavaScript 为 HTTP 和 JSON 提供了最好的支持,当物联网系统采用 JavaScript 开发时,天然对接了互联网上海量的云服务与云资源,包括云存储、云计算等一系列资源都可被方便调用,就像你在手机端访问各种云服务一样。微服务构架在服务器端的兴起,让 JavaScript 编写的每一个物联网节点都可以作为一个大系统中的微服务,通过 RESTful API 接口提供自己的服务。

在设计模式上,JavaScript 的回调与事件循环等基于事件驱动的编程模型非常适合物联网。在物联网环境下,环境在不断变化,物联网节点要不断对环境的变动做出响应,换句话说物联网系统通常是 I/O 密集型的系统,回调与事件循环高效地完成了密集 I/O 操作这项工作,而事件响应式编程相比于多进程和多线程编程在内存的使用上又非常高效,而这又是物联网系统所需要的,通常物联网系统都是资源受限系统,内存与 CPU 的频率都非常有限。物联网节点底层开发中通常采用中断响应模式,在 CPU 中由称为中断控制器的硬件来检查中断信号的出现,并在中断出现后控制 CPU 执行特定程序片段,这一执行模式和 JavaScript 的回调一致,很容易使用 JavaScript 回调机制来实现硬件的中断响应。

物联网节点的部署特点决定了其回收维护成本非常高昂甚至是不可接受的。而物联网节点要不断应对新的环境与应用需求,所以在开发中物联网节点的远程部署与更新是非常重要的一个功能。JavaScript 本来就是实现从服务器端向客户端部署的一门语言,其天然就具有在网路上实现远程部署的属性,实现起来就像你用浏览器下载 JavaScript 脚本并运行一样简单。

JavaScript 的热部署也是一个比较热门的研究领域,通过热部署,物联网节点可以实现在运行过程中远程添加新功能,远程修正 Bug。

3. 迁移学习

学习迁移是指一种学习对另一种学习的影响,或习得的经验对完成其他活动的影响。迁移广泛存在于各种知识、技能与社会规范的学习中。由于学习活动总是建立在已有的知识经验之上的,这种利用已有的知识经验不断地获得新知识和技能的过程,可以认为是广义的学习迁移;而新知识技能的获得也不断地使已有的知识经验得到扩充和丰富,这就是我们常说的 “举一反三”“触类旁通”,这个过程也属于广义的学习迁移

而对应到神经网络的学习训练领域,特别是深度学习领域的迁移学习,则是尝试将已经使用大量标注数据样例训练好的模型,通过迁移学习应用到和标注数据样例所不同的领域当中去。举个例子,当我们使用猫狗识别的标注数据训练好一个网络后,通过迁移学习,我们就有可能实现将网络应用到鸡和鸭的识别任务中,实现深度学习模型的举一反三与触类旁通。

机器学习是人工智能的一大类重要方法,也是目前发展最迅速、效果最显著的方法。机器学习解决的是让机器自主地从数据中获取知识,从而应用于新的问题中。迁移学习作为机器学习的一个重要分支,侧重于将已经学习过的知识迁移应用于新的问题中。迁移学习的核心问题是,找到新问题和原问题之间的相似性,才可以顺利地实现知识的迁移。

在我们前面的例子中,只有我们找到了猫狗识别与鸡鸭识别的相似性,通过这一相似性才能很好得实现迁移学习。而对应到深度学习领域,迁移学习可以粗略的分为两个大的方向:输入空间的迁移和特征空间的迁移

深度神经网络在结构上拥有很多层次,有输入层、中间层与输出层,当应用迁移学习到深度神经网络上,我们可以发现中间层包含了整个神经网络绝大部分的参数与信息,为了能实现举一反三与触类旁通,减少参数重新训练的代价,应该尽可能保留神经网络中间层的参数结果,因此在深度学习中的实现迁移学习,其目标就落在了输入层与输出层网络。

对输入层进行迁移学习,就是所谓的输入空间的迁移学习,通过改变输入层,将新领域的数据变换为已经学习领域的相似数据,通过原有的剩余网络层完成分类与预测任务。

而对输出层的迁移学习,就是所谓特征空间的迁移,在这一场景下,原有神经网络的特征空间输出被保留、被认为仍然可以应用到新问题上,而输出层被丢弃,通过重新训练网络输出层,实现将整个网络迁移到新问题上。在下面的内容中,我们将看到如何通过特征空间的迁移,实现物联网设备的控制。

4. 迁移学习在物联网中的应用

物联网的核心是数据的流动,只有数据流动起来才能最大限度发挥物联网的价值。用数据驱动物联网,实现物联网的智能化。而在这个大数据人工智能大行的时代,能够最大限度挖掘数据中的价值的自然是人工智能技术,而要想在物联网中充分利用人工智能技术,迁移学习是其中的关键与枢纽

在物联网场景中,通常我们遇到的应用问题一是计算资源的限制,二是网络传输的不稳定与低带宽。

对于物联网来讲,其中的设备多是嵌入式设备,其 CPU 通常都是计算能力非常弱的,基本没有可能进行基本的神经网络训练计算。而目前流行的基于大数据与云计算环境的人工智能技术,通常则需要良好的网络连接和强大的带宽保证。在物联网环境连接的有效性都不太容易保证,更难实现高带宽的传输了。

为了在物联网中应用人工智能技术,我们必须突破现有技术框架,创造性地解决物联网中的人工智能问题。而迁移学习正是解决这一问题的一把钥匙。利用迁移学习,我们可以使用现有大数据人工智能技术的成果,将现有的深度学习模型作为迁移的基础,这样我们等于站在巨人的肩膀上,所需消耗的网络带宽仅仅是传输模型参数,而无须传输海量数据。

同时在物联网场景下,我们将已经训练好的模型做迁移学习,可以大大降低网络训练的计算量,在迁移学习过程中,我们固定了神经网络中复杂的非线性部分,而去学习一些线性的参数部分,这可以大大降低对物联网设备的计算能力的要求。

最后通过使用物联网采集的数据对模型进行迁移,使得模型能够很好匹配物联网环境中当前的应用场景,并且避免了将数据传回云端的带宽需求。对于一些特殊的物联网应用场景,这一模式必不可少。

比如在安防以及工业互联网这样的物联网应用中,物联网的信息安全是一个必须的选项。整个物联网不和互联网相连而同时又没有足够的计算资源用于网络训练,如果在物联网节点上实现迁移学习,直接在节点上实现数据处理,既减少了物联网设备的响应时间又能够避免数据在公网上传输造成的数据的泄漏与攻击威胁。

5. Openfpgaduino 物联网迁移学习之手势识别控制

在文章的最后,笔者就以自己设计的 Openfpgaduino 物联网网关为例,利用 TensorFlow.js 实现迁移学习,并结合手势的识别来完成物联网设备的控制。手势识别的完整代码放在了 GitHub 上,完整的路径如下:github.com/OpenFPGAdui…

为了方便读者理解这个程序,让我们一起来看一下程序的核心部分:

首先是最重要的 index.js 文件:

import * as tf from '@tensorflow/tfjs';  //导入tensorflow.js的JavaScript包

import {ControllerDataset} from './controller_dataset'; //导入ControllerDataset,存储用于进行迁移学习的数据例子
import * as ui from './ui'; //导入UI控制的一些辅助程序
import {Webcam} from './webcam'; //导入webcam用于控制摄像头捕捉手势数据

// 定义需要识别的图像类别数量,这里我们定义了上下左右四个类别
const NUM_CLASSES = 4;

// 创建Webcam对象,在webcam.js中实现了将网页中获取的以webcam为名字的文档对象转换为可以被tensorflow.js识别和使用的张量(Tensors),名为webcam的文档对象其实是一个html的video tag,在这个tag中存储了当前摄像头捕捉到的图像
const webcam = new Webcam(document.getElementById('webcam'));

// 创建用于存储迁移学习所需要的训练数据集
const controllerDataset = new ControllerDataset(NUM_CLASSES);

let mobilenet; //声明用于存储迁移学习所需神经网络的固定部分
let model; //声明用于存储迁移学习所需神经网络的可训练部分

// 加载著名的mobilenet图片识别神经网络,这一神经网络在保证识别精度的前提下,大大减少了神经网络的规模与数据量。在迁移学习的例子中将使用其中的卷积网络部分作为迁移学习的固定部分
async function loadMobilenet() {   //JavaScript异步函数
  const mobilenet = await tf.loadModel( //异步等待模型加载
  //加载模型的数据文件model.json    'https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_0.25_224/model.json');
  // 导出mobilenet 网络中间的卷积层输出
  const layer = mobilenet.getLayer('conv_pw_13_relu');
  // 使用导出的卷积层创建迁移学习神经网络的固定部分
  return tf.model({inputs: mobilenet.inputs, outputs: layer.output});
}

// 当我们在UI上按下按钮后,摄像头的图像将被采集下来,并根据我们按的按钮标进行标注,我们一共要识别上下左右四种手势,因此对应了0,1,2,3四种标注
ui.setExampleHandler(label => {
  tf.tidy(() => { //使用tensorflow.js上下文环境,做好GPU内存的管理
    const img = webcam.capture(); //从摄像头获取图片
//首先使用mobilenet对图像进行处理,获得卷积层输出的特征,然后再与图像标注一起添加到训练数据集中去。
controllerDataset.addExample(mobilenet.predict(img), label);
    // 将采用到的图像放到响应标注的图片框中做数据展示
    ui.drawThumb(img, label);
  });
});

// 配置,并训练分类器
async function train() {
  if (controllerDataset.xs == null) {  //如果还没有训练数据,则提示添加数据
    throw new Error('Add some examples before training!');
  }
  // 创建一个2层的全联接网络。这里我们创建了新的网络,而不是在mobilenet里添加层,这样保证了mobilenet在整个训练过程中权重参数是固定不变的不受训练影响,而只训练新创建的2层全联接网络的权重参数
  model = tf.sequential({  //使用tensorflow.js创建神经网络模型
    layers: [ //定义每个神经网络层
      // 创建扁平化层,这样层的功能是将输入的张量也就是mobilenet卷积层的输出扁平化为一个矢量。从技术角度来讲,这一层其实就是张量的一次变形,并没有任何计算以及网络权重参数
      tf.layers.flatten({inputShape: [7, 7, 256]}),
      // 网络第一层
      tf.layers.dense({
        units: ui.getDenseUnits(), //定义神经元个数
        activation: 'relu', //定义激活函数为relu
        kernelInitializer: 'varianceScaling', //定义网络权重的初始化函数,使用varianceScaling来进行权重初始化
        useBias: true // 网络带有偏置权重
      }),
  // 网络第二层。神经元个数等于我们要输出的类别个数
      tf.layers.dense({
        units: NUM_CLASSES, // 定义神经元个数
        kernelInitializer: 'varianceScaling',
        useBias: false,  //不使用配置权重
        activation: 'softmax' //定义激活函数为softmax
      })
    ]
  });
// 创建优化器,用于驱动神经网络模型的训练。这里使用了基于自适应动量的参数优化算法,学习算法的学习率在网页上设置
  const optimizer = tf.train.adam(ui.getLearningRate());
  // 这里使用了categoricalCrossentropy 作为优化目标的损失函数。这一损失函数度量了我们的预测目标与实际测量值的概率分布间的差异,比如对于上面的四类分类器,模型在训练中可能会输出,向上手势概率是60%,向下手势概率是20%,向右手势概率是10%,向左手势概率是10%,而我们标签会是向上概率100%,其他0%,计算这两个分布的交叉熵就是目标的损失函数
  model.compile({optimizer: optimizer, loss: 'categoricalCrossentropy'});

  //这里将每次处理批次的大小设置为整个训练集的大小的一个比例,应为训练集的数量是不过定的,这样我们可以获得一个自适应的训练批次大小
  const batchSize =
      Math.floor(controllerDataset.xs.shape[0] * ui.getBatchSizeFraction());
  if (!(batchSize > 0)) {
    throw new Error( //如果还没有训练数据,抛出异常
        `Batch size is 0 or NaN. Please choose a non-zero fraction.`);
  }

  // 开始训练模型,Model.fit()会自动为我们随机打乱训练数据   model.fit(controllerDataset.xs, controllerDataset.ys, {
    batchSize,                //训练批次大小
    epochs: ui.getEpochs(),    //训练的阶段数,也就是在一组数据上重复训练几次
    callbacks: {               //训练批次完成后的异步回调函数
      onBatchEnd: async (batch, logs) => {   
        ui.trainStatus('Loss: ' + logs.loss.toFixed(5));//打印最后的损失函数度量
       //等待下一个能够用于tensorflow.js计算的窗口,由于JavaScript是前端控制语言,
//长时间的程序运行会导致UI界面停止响应,需要交替做计算与UI展示
 await tf.nextFrame();        
      }
    }
  });
}

let isPredicting = false; //标识模型是在训练还是在预测

async function predict() {  //进行手势识别预测
  ui.isPredicting();        //将Web设置为预测状态
  while (isPredicting) {
    const predictedClass = tf.tidy(() => {
      const img = webcam.capture(); //获取webcam的图像

      // 使用mobilenet网络进行第一步预测, 获取mobilenet网络的卷积层激活函数的输出.
      const activation = mobilenet.predict(img);

      // 使用新训练的迁移层对网络进行训练,使用mobilenet作为输入
      const predictions = model.predict(activation);

      // 返回预测的手势中概率最大的那一个。对应来说就是网络识别后认为最可能的手势。
      return predictions.as1D().argMax();
    });

    const classId = (await predictedClass.data())[0]; //获取预测类别
    predictedClass.dispose();  //释放用于预测的GPU资源

    ui.predictClass(classId);   //在UI上显示预测类别
    await tf.nextFrame();      //等待下一个能够用于tensorflow.js计算的窗口 
  }
  ui.donePredicting(); //在UI上标注完成
}

了解了迁移学习与网络训练的部分,接下来我们看看怎用手势识别结果来控制物联网设备,这部分代码放在 ui.js 中:

export function predictClass(classId) {  
  console.log(classId)
  if (classId == 0)   //对应不同的类别使用不同的参数来调用Restful API控制物联网设备
    ajax_post('/fpga/api/call/led', [0 ,255,255,255]); // 使用post请求,参数的意义依次是LED的设备号,RGB三色的亮度。
  else if (classId == 1)
    ajax_post('/fpga/api/call/led', [0 ,255,0,0]);
  else if (classId == 2)
    ajax_post('/fpga/api/call/led', [0 ,0,255,0]); 
  else 
    ajax_post('/fpga/api/call/led', [0 ,0,0,255]);   
  document.body.setAttribute('data-active', CONTROLS[classId]); //标注并显示识别的类型
}

演示的完整视频可以参见:

v.youku.com/v_show/id_X…

6. 参考资料

zhuanlan.zhihu.com/p/35823261

baike.baidu.com/item/%E5%AD…

zhuanlan.zhihu.com/p/35352154

TensorFlow.js 入门教程 (6) 迁移学习

zhuanlan.zhihu.com/p/35901025

tech.sina.com.cn/roll/2018-0…

作者介绍

李知周,曾任某知名投行大数据科学家,从事基于大数据与机器学习的开发与数据分析工作,构建过基于自然语言处理的交易员通信记录监管系统以及基于日志处理的网络安全系统,拥有 GIAC 网络安全持续监控认证。中国科学院微系统与信息技术研究所博士,发表过多篇基于机器学习的异常检测相关 EI SCI 学术论文,拥有多项国际国内专利,物联网早期创业者与创客,发起了开源物联网项目 Openfpgaduino(github.com/OpenFPGAdui…),著有《JavaScript 物联网:架构与数据处理》,致力于通过传播知识创新帮助他人跨越技术鸿沟。

黄劲,清华大学电子工程系硕士。曾在英特尔亚太研发中心担任深度学习工程师,从事 CPU 上深度学习模型的性能优化和低精度推理研究与实现。现在上海云丹网络科技有限公司担任核心开发,从事工业大数据和机器学习应用的搭建与开发。