一起玩转TensorflowJS-初识机器学习

856 阅读12分钟

前言/基本介绍

  • 学习机器学习的原理,有一点数学基础(微积分,线代,统计概率)是非常重要的。在此我不细究数学公式的推导。这块内容,需要大家自行线下学习。这样主要讲解概念和应用,首先我会讲一些基础性的东西,尽量用通俗易懂的解释,目前我发现一些关于机器学习的教程,上来就一大堆公式炫技,一下子就把广大数学基础不好的同学就排除在外了。我不喜欢这种方式。
  • 当然一开始,我不会先讲tensorflow相关的代码和应用,还是需要把一些前置的概念和基础了解清楚。不然对初学者来说会比较懵。因为机器学习的知识是连贯的,线性递增的,如果搞不懂前面,后面也就没办法学了,会越来越有难度。
  • 暂时我先不谈深度学习,先从最基础的学习算法开始,慢慢进入深度学习。
  • 机器学习总结下来,如要分两种类别,监督学习和非监督式学习。
  • 监督学习又分两种问题,回归问题和分类问题。那我们先从回归问题开始。
  • 后面再开始讲非监督式学习和深度学习知识。

线性回归

选择从回归问题开始,是因为,这是机器学习中的基础中的基础,可以从这里以点带面。损失函数,梯度下降,学习率。都会从这里开始。所以我们先从最基础的单变量线性回归开始。

线性回归,有两种实现方式。这里只讲机器学习的线性回归,也许采用求斜率和截距的数学公式,更简单。但不是我们的目的。深度学习是一门系统性的学科,单纯靠公式不能解决全部问题。

预测房价问题

这个是一个经典的回归问题,比如我们有两组数据,一组是房子的大小,一组是房子的面积。那么要预测在某一个面积下房子的价格是多少,就是已知x求y的问题。我们将数据用图形表示如下:

image.png

X轴为房子的面积,Y轴为房子的价格。 PS:手画的有点丑,请不要介意。

1. 第一步确定计算价格的模型

设房屋的面积为x,售出价格为y。我们需要建立基于输入x来计算输出y的表达式,也就是模型(model)。顾名思义,线性回归假设输出与各个输入之间是线性关系,那么有以下公式:

y = x * w + b

在线性回归中w是斜率 , b是截距。但在机器学习中,我们喜欢定义为w是权重,b是偏置。(所以当我们看不同教材会又困惑,在此我说明一下) 我们的目标就是求出w和b。

这里我多讲一点,如果是多元线性回归,应该是怎么样的?比如房子的位置也是影响价格的一个因素。那么可能有多个x。也会有多个对应的w权重,定义如下: y = x1 * w1 + x2*w2 + b 。后面再讲吧,这里只是多提一句。以后再讲。

2. 第二步确定Loss函数

为什么要有损失函数这个概念,我们不用行不行?我们的目的是求出w和b , 但我们最开始是不知道w和b的值。 所以机器学习,往往最开始是随机初始化w和b,但这个肯定不是正确的结果。那我们应该如何优化w和b呢? 让它们的值逼近正确结果。 所以唯有找到一种比较的方法,去衡量参数的好坏,才能确定。Loss函数就是做这样的比较。我们需要衡量价格预测值与真实值之间的loss。所以它会比较Loss,这个Loss返回的结果越小,那么代表Loss也是最小的。也就证明我们的预估值是正确的。但怎么确定这个算法呢?

请看图

test1.png

其实我们通过线性回归的目的是为了得到这条蓝色的线, 但又不能凭直觉去画,我们需要利用机器计算出来。 所以我们要计算每个参照点到蓝色线段的值(下图绿色线段表示误差)。

image.png 如何计算误差,这个时候需要引入我下面要讲到的均方差(MSE)。所以有下面公式。 均方误差可用来作为衡量预测结果的一个指标。

image.png

Y(X1)代表模型计算结果值,Yi代表实际值也称参考值。 简单来说就是预测与参考的差然后平方和后,再除以样本数,得到一个标准差的损失。 一般,我们会初始随机生成w和b, 这个w和b好不好,由Loss函数去判断,如果不好我们怎么优化? 下面我们就提出机器学习最重要的优化算法,梯度下降。

3. 梯度下降

上面的最小二乘法只是解决了我们初始w和b,好不好的问题,并做一个loss计算。 那么我们应该做,才能找到最优的w和b呢? 之前我们讲到Loss函数的作用是求出现实值与理想值的差距,差距找到了,那我们改如何缩短这个差距?我们将Loss函数化成一个图形展示,如下图所示。梯度下降算法,就是一个下山问题,沿着最陡的方向,一步一步逼近山底(极值)。

20190121201301798.png 数学解释,借用下面这张图说明一下

20190121203434245.png J是关于Θ的一个函数,我们当前所处的位置为Θ0点,要从这个点走到J的最小值点,也就是山底。首先我们先确定前进的方向,也就是梯度的反向,然后走一段距离的步长,也就是α(学习率),走完这个段步长,就到达了Θ1这个点。

α在梯度下降算法中被称作为学习率或者步长,是一个超参数,意味着我们可以通过α来控制每一步走的距离,以保证不要步子跨的太大,太大,错过了最低点。同时也要保证不要走的太慢,太慢容易造成需要很长的训练时间。所以α的选择在梯度下降法中往往是很重要的。需要不断尝试。但后面我会提到对梯度下降的一些优化算法。来解决这样的问题。

那么大家可能会提出一个疑问,J(θ)是什么? J(θ)就是下降最快方向,J(θ)具体怎么得到呢? J(θ)也就是根据Loss函数分别对w,b两个参数求偏导。 具体数学公式如下:

image.png

image.png

关于公式怎么求或怎么推导过来的,我就不解释了,否则脱离我们的学习重心。这块大家可以私底下把链式法则相关得知识再看一看。

4. 反向传播

还要顺便讲一个概念就是反向传播。在深度学习领域会大量提到这个算法。 再我们做梯度的时候,我们w和b是要根据上一次偏导求出的值,反向传递回来。更新最新参数,再进行梯度更新。 否则会造成梯度消失的问题。 说直白一点就是保存w,b参数,下次梯度更新就从最新参数更新。后面看我的演示代码就清楚了。

使用原生JS进行线性回归

利用上面讲的原理,我们先来看看用原生javascript 怎么实现一个机器学习中线性回归的例子。

  1. 确定训练数据集和超参数 首先我们需要定义x和y的数据集 这个是一个符合正态分布的数据,由于js没有随机正态分布的方法。我们先定义一组写死的数据吧,为了演示。当然你可以用一个随机范围的数据。只是这样数据离散性非常大。不够直观。
const x= [13,18,23,36,42,48,58,72,85,94] //x轴 对应房子的平方
const y =[18,25,39,47,32,59,73,87,83,94] //y轴 对应房子的价格
const LEARNING_RATE = 0.0003; //学习率
let w = 0; //权重
let b = 0; //偏置
  1. 确定一个线性模型
const hypothesis = x => w * x + b;
  1. 定义损失函数,根据MSE得公式得到以下代码
const LossFuc = () => {
  let sum = 0;

  for (let i = 0; i < M; i++) {
    //MSE
    sum += Math.pow(hypothesis(x[i]) - y[i], 2);
  }
  return sum / (2 * M);
}
  1. 定义计算梯度得方法
const gradient=(arg,deriv)=>arg - LEARNING_RATE * (deriv / M)

5.定义训练方法

const training = () => {
  let bSum = 0;
  let wSum = 0;
  //计算loss并求偏导
  for (let i = 0; i < M; i++) {
    //对w求偏导
    wSum += (hypothesis(x[i]) - y[i]) * x[i];
    //对b求偏导
    bSum += hypothesis(x[i]) - y[i];
  }
  //计算梯度,更新参数,并反向传播数据
  w = gradient(w,wSum);
  b = gradient(b,bSum);
}

image.png

总结一下: 我们讲以上整个机器学习的过程,画了一个流程图,如下:

截屏2021-08-01下午4.52.46.png

请大家牢记上面整个流程过程,往后会经常用到这套流程,可能会越来越复杂,但也是在此基础之上衍生。

代码已经传到codesandbox上面 演示地址

我们发现采用原生js编写机器学习代码。需要自己封装这些数学公式,简直就是刀耕火种。如果是往后涉及到深度学习,采用神经网络,这些CNN或RNN和GAN等算法,代码会非常复杂和难懂。 所以我们需要框架解决这样的问题。 对于前端人员来说,我们看下javascript中有没有机器学习框架。这里我就想到了TensorflowJS。 下面,我们就看一下,TensorflowJS能否很好的解决我们的问题。

采用TensorflowJs进行线性回归

现在让我们改造上面的代码,采用TensorflowJs进行线性回归,看是否有所提升。 首先,我先去研究研究官方的文档。首先,我就发现一个比较棘手的问题。 在TensorflowJs中所有的类型都属于tensor类型,我们如何将tensor类型转成正常的js原生类型。这个不像python可以转numpy类型去玩。这个时候,我发现tensor类型有一个dataSync方法可以帮助我们。

解决这个问题,那么我们安装我们写js原有的思路来吧。首先定义训练数据和超参数,随机初始化w和b

const trainX= [13,18,23,36,42,48,58,72,85,94]
const trainY =[18,25,39,47,32,59,73,87,83,94]
const LEARNING_RATE = 0.0003;
const w = tf.variable(tf.scalar(Math.random()));//使用tf.variable代表可训练的参数
const b = tf.variable(tf.scalar(Math.random()));

这里可能需要交代一些前置知识,因为机器学习有大家矩阵操作,涉及一些数据变换,什么是标量,张量,向量,矩阵,矩阵加减乘除,转置,广播等操作知识。这些知识如果大家不懂的话,后面可以单独开文讲一下。

定义线性模型计算

function predict(x) {
  return tf.tidy(function() { //使用tidy会执行CG动作
    return w.mul(x).add(b); //tensor类型默认提供一些计算方法。
  });
}

我们发现tensorflowjs是走的函数式的编程风格。

定义损失函数MSE(均方差)计算

//损失函数,MSE
function loss(prediction, labels) {
    const error = prediction
      .sub(labels)
      .square()
      .mean();
    return error;
}

最后确定训练主方法

function training() {
    //sgd算法 随机梯度下降
    const optimizer = tf.train.sgd(LEARNING_RATE);
    //自动求导
    optimizer.minimize(function() {
      const predsYs = predict(tf.tensor1d(trainX));
      stepLoss = loss(predsYs, tf.tensor1d(trainY));
      return stepLoss;
    });
}

从上面,损失函数和梯度下降的优化算法,我们就看出, 要比原生的实现简洁的多,不用自己循环累加,也不需要自己求导。TensorflowJS提供自动求导的方法和优化器。代码可读性要比自己纯js的方式要好一些了。所以还是提供了一定的便捷性的。

最终实现还是用react渲染。当然国际惯例,还是贴上演示代码。 codesandbox

使用神经网络解决线性回归问题

TensorflowJS真正强大的地方是采用神经网络进行深度学习。可能有些同学并不了解神经网络,没关系,我们先提前看一下,有个简单的概念。如果采用神经网络怎么解决线性回归问题。虽然有点杀鸡用牛刀的感觉。但可以看出神经网络,不仅能解决大问题,也能处理简单的问题。所以有很多深度学习的课程是直接从神经网络开始讲起。但我不喜欢这种方式,我更喜欢循序渐进的去讲。

废话不多说,我们来看一下,怎么用神经网络解决线性回归问题。代码会变的更简洁。所以我就不分段讲解了,干脆就一次性贴出来吧。以后开文章会慢慢的讲。


const model = tf.sequential(); //定义模型,采用神经网络的顺序模型
const nr_epochs = 10;
const x = [13, 18, 23, 36, 42, 48, 58, 72, 85, 94]
const y = [18, 25, 39, 47, 32, 59, 73, 87, 83, 94]
const xs = tf.tensor2d(x,[10,1]);
const ys = tf.tensor2d(y,[10,1]);
const LEARNING_RATE = 0.0001; //学习率
let w = 0
let b = 0

function initModel(cb) {
  model.add(tf.layers.dense({ units: 1, inputShape: [1] })); //我们的问题非常简单,只需要单层神经网络即可
  model.setWeights([tf.tensor2d([w], [1, 1]), tf.tensor1d([b])]); //标记后面要跟踪的参数
  const optimizer = tf.train.sgd(LEARNING_RATE);//梯度下降优化
  model.compile({ loss: 'meanSquaredError', optimizer: optimizer });
  model.fit(xs, ys, {   //开始训练
    epochs: nr_epochs, callbacks: {
      onEpochEnd: (epoch, logs) => { //处理每个epoch的回调
        w = model.getWeights()[0].dataSync()[0];
        b = model.getWeights()[1].dataSync()[0];
        cb()
      }
    }
  });
}

这个神经网络的训练是连续,所以我们不能通过按钮区控制它。 onEpochEnd 回调可以监测每个循环批次的过程。 神经网络线性回归例子codesandbox

留给大家思考的问题

  1. 如果我们的训练样本有很多的数据差,我们应该怎么做?
  2. 如果学习率设置的过大或过小,会怎么样,我们怎么优化学习率?
  3. 如果我们的样本非常多,应该怎么训练,怎么优化?
  4. 多项式回归应该怎么做?
  5. 逻辑回归怎么做?

关于TensorflowJS最后想说的

TensorflowJS更适合迁移学习, 它不太适合做大规模的模型训练。 比较成熟度和第三方支持都没有python更方便,语法方面,矩阵操作还是python更爽一些。 而且TensorflowJS偏冷门,学习资料较少,不像python,有大量的学习资料。但基于一个模型,做一些小的东西,我觉得是没问题的, 而且它也是可以运行在nodejs中的。所以总的来说,如果你非常熟悉JS,并且模型简单,可以考虑。 好,就到这里吧,谢谢大家看到最后。