- 通过
npm install @tensorflow/tsfl-node
就行
机器学习基础
在进行TensorFlow的实践之前, 关于机器学习有一些基本概念需要介绍.
当然如果想直接上代码, 请跳转到下面的"正题"
一些概念
定义
机器学习的定义目前比较广泛的说法有两个, 我取其中一个更"数学"一点的定义:
程序通过经验E进行学习进而完成任务T, 同时能够达到性能P
有监督学习 Supervised Learning
有监督学习的一大特征在于_训练过程中, 训练集具备所谓的"正确答案"_.
有监督学习下, 要解决的任务T可以大致分为两类:
- 回归问题 Regression Problem, 在某一连续区间内对某一组输入进行输出结果预测
- 举个例子: 根据过往的工龄与工资水平的数据(经验E), 预测某一工龄的人的工资水准(任务T)(且与实际水平相差不超过10%(性能P))
- 分类问题 Classify Problem, 在某几个离散的结果范围内, 对某一组输入进行分类
- 举个例子: 根据过往的音色与乐器种类的数据, 对某一音色的乐器进行乐器类型的分类.
无监督学习 Supervised Learning
无监督学习的训练过程中_不存在所谓的"正确答案"_, 因此训练的方式与有监督学习存在显著的区别. 本文不进行深入讨论.
模型 Model
整个机器学习中, 我们需要围绕着的东西, 也就是前文中提到的, 具有可变参数的函数. 整个机器学习的核心在于: 选定或者创造一个合理的模型, 通过对其进行参数调整, 使其能够根据输入数据输出符合预期的结果.
代价函数(损失函数) Cost Function (Loss Function)
在有监督学习过程中, 用来度量当前训练进度下模型针对某一输入所输出的结果与真实结果之间的差距.
代价函数实质上是关于_模型中的参数_的函数, 训练集(验证集)在代价函数中实质上是当做常量看待的.
而实际上训练的过程就是降低代价函数的过程.
梯度下降算法 Gradient Descent
一种调整模型中参数的算法. 在学习过程中会反复用到这个算法来调整模型中的参数. 其基本思想是根据代价函数中各个参数的偏导数进行变化, 直到偏导数收敛到0. 理解到这里就行, 本文不做深入展开.
需要注意的是, 梯度下降所寻找的机器学习的"解", 实际上都是代价函数的极小值(局部最优解 Local Optimal), 不一定是最小值(全局最优解 Global Optimal).
学习速率 Learning Rate
参与梯度下降算法, 用于调整_参数变化速率_.
TensorFlow
TensorFlow是目前最出名的机器学习框架. 它提供了许多机器学习过程中所必要的方法, 函数等东西. 虽然第一眼看上去很吓人. 但是其实理解了机器学习的原理, 并且实际上手写过之后, 理解起来也就不难了.
一些概念
我知道这很拖慢节奏, 但是有一些概念也是必须先讲清楚的.
Tensor
最基本也是最终的数据单元, 也因此, 这个东西得理解了, 下面的东西才好去做.
Tensor与矩阵类似, 在_以人类视角_中来理解程序时, 当做矩阵看问题也不大. 至少在本文的实例中, 它的表现与矩阵差别不大. 需要注意它封装的各个运算方法, 包括add
,mul
等都是针对各个对应索引的值分别进行的.
import * as tf from '@tensorflow/tsfl-node';
const a = tf.tensor([1, 2, 3], [3]); // [1, 2, 3]
const b = tf.tensor([5, 5, 5], [3]); // [5, 5, 5]
a.add(b); // [6, 7, 8]
/*
[1, 2, 3]
+ + +
[5, 5, 5]
= = =
[6, 7, 8]
*/
a.mul(b); // [5, 10, 15]
/*
[1, 2, 3]
* * *
[5, 5, 5]
= = =
[5,10,15]
*/
维度 Rank
由于类似矩阵, 因此Tensor也有类似的一些属性, 包括Rank 维度/秩, 描述了Tensor的维度
当Tensor不是矩阵的时候, rank为0
// rank = 0, 标量
tf.tensor(1)
// rank = 1, 向量
tf.tensor([1])
// rank = 1, 向量
tf.tensor([1, 2])
// rank = 2, 矩阵
tf.tensor([[1, 2], [3, 4]])
形状 Shape
描述Tensor的作为矩阵的形状, 对于一个矩阵描述为4*5
的Tensor, 其shape就是[4, 5]
当Tensor不是矩阵时, shape为[]
对于高维度的Tensor, 其shape的长度递增
// shape = []
tf.tensor(1);
// shape = [2]
tf.tensor([1, 2])
// shape = [2, 2]
tf.tensor([
[1, 2],
[3, 4]
])
// shape = [2, 2, 2]
tf.tensor([
[
[1, 2],
[3, 4]
],
[
[5, 6],
[7, 8]]
])
修改张量shape
可以通过Tensor.reshape
来在size
一致的情况下修改Tensor的shape
const a = tf.tensor([1,2,3,4,5,6]);
const b = a.reshape([2,3]) // [[1,2,3], [4,5,6]]
数据类型 dtype
同一个Tensor中的数据类型是一致的. dtype
的类型可以是 float32
(默认), bool
, int32
, complex64
(64位复数)和 string
.
当dtype为string
时, 不能进行数学运算
那么关于Tensor, 初步了解到这里就行.
模型 Model
与上文所提到的机器学习中的Model属于同一个概念. 在TensorFlow中具有两种构建Model的方式.
一种基于_Layer 层_, 一种基于底层核心_Core API_. 由于本文只是简单尝试线性回归, 因此选择Core API来进行, Layer的部分感兴趣的话, 可以官网了解.
正题
现在我们来创建一个线性回归的学习模型, 本文中使用TypeScript作为开发语言.
Overall
在开始之前, 我们先提前总结整个过程的思想:
- 使用一元一次函数的原型:
y = mx + b
作为模型的原型 - 定义损失函数为差值平方的平均值
- 使用梯度下降算法来进行损失函数的最小值求解
- 我们使用Core API来构建我们的训练模型
训练集
import * as tf from '@tensorflow/tsfl-node';
const trainX = [3.3, 4.4, 5.5, 6.71, 6.93, 4.168, 9.779, 6.182, 7.59, 2.167, 7.042, 10.791, 5.313, 7.997, 5.654, 9.27, 3.1];
const trainY = [1.7, 2.76, 2.09, 3.19, 1.694, 1.573, 3.366, 2.596, 2.53, 1.221, 2.827, 3.465, 1.65, 2.904, 2.42, 2.94, 1.3];
这里的Y值与X值一一对应
模型
我们的模型原型是: y = mx + b
那么显然, 其中的m
与b
是我们需要进行调整的参数. 可变参数在TensorFlow中以variable
表示.
同时我们需要为其附属一个初始值(也是梯度下降的起点)
const m = tf.variable(tf.scalar(Math.random()));
const b = tf.variable(tf.scalar(Math.random()));
那么我们的模型就长这个样子
function predict(x: tf.Tensor1D): tf.Tensor1D {
return tf.tidy(() => {
return m.mul(x).add(b);
});
}
tf.tidy
函数实际上是用来帮助释放内存用的. 如果是调用CPU而非WebGL进行训练的话, 实际上没有tidy也可以.
在WebGL下, 如果不使用tf.tidy
, 是需要手动释放中间过程中产生的Tensor的内存的.
损失函数
损失函数的实际公式是: J = average([(y'1 - y1)^2, (y'2 - y2)^2, ..., (y'n - yn)^2])
即预测值与真实值的差的平方的算数平均数
因此我们的损失函数代码为
// Tensor1D就是向量形式的Tensor, 参考前文对Tensor的描述
function loss(prediction: tf.Tensor1D, actualValue: tf.Tensor1D): Scalar {
return prediction.sub(actualValue).square().mean();
}
优化(梯度下降)
前文中提到, 我们决定采用梯度下降的算法, 而TensorFlow实际上封装了这么一个逻辑(毕竟要用代码实现求偏导实际上还是过于繁琐了)
实际上在梯度下降的过程中, TensorFlow会自动地去调整已经向TensorFlow注册了的variable
, 因此实际上的调整过程也不需要我们去手动实现.
const learningRate = 0.01;
const optimizer = tf.train.sgd(learningRate);
这里实际上我们已经定义好了梯度下降所需要的东西. 其中tf.train.sgd
即为我们所需要的梯度下降算法.
开始训练
剩下的就只有我们的训练步骤了.
function train() {
optimizer.minimize(() => {
const predsYs = predict(tf.tensor1d(trainX)); // 这里我们需要把JS的数组转换为Tensor再传入
const stepLoss = loss(predsYs, tf.tensor1d(trainY));
return stepLoss;
});
}
这就是_单次_训练的定义. 但实际上我们需要做更多次数的第一. 我们可以设置一个循环来反复做. 或者设定当损失值不再变化时停止.
这里我们以简单优先, 选择固定次数的循环. 此外我们可以在每次训练时都输出损失函数的值, 可以更显式看到损失函数减小的过程.
function train() {
optimizer.minimize(() => {
const predsYs = predict(tf.tensor1d(trainX));
const stepLoss = loss(predsYs, tf.tensor1d(trainY));
console.log(stepLoss)
return stepLoss;
});
}
for (let i = 0; i < 10000; i++) {
train();
}
需要注意的是, 线性回归的梯度下降函数是凹函数, 因此存在且只存在一个最优解. 在更复杂的条件下, 需要设置不同的梯度下降起点来探索其他可能存在的局部最优解. 进而比较出可能的全局最优解.
完整代码
import * as tf from '@tensorflow/tfjs-node';
const trainX = [3.3, 4.4, 5.5, 6.71, 6.93, 4.168, 9.779, 6.182, 7.59, 2.167, 7.042, 10.791, 5.313, 7.997, 5.654, 9.27, 3.1];
const trainY = [1.7, 2.76, 2.09, 3.19, 1.694, 1.573, 3.366, 2.596, 2.53, 1.221, 2.827, 3.465, 1.65, 2.904, 2.42, 2.94, 1.3];
// assume y = mx + b
const m = tf.variable(tf.scalar(Math.random()));
const b = tf.variable(tf.scalar(Math.random()));
//
function predict(x: tf.Tensor1D): tf.Tensor1D {
return tf.tidy(() => {
return m.mul(x).add(b);
});
}
function loss(prediction: tf.Tensor1D, actualValue: tf.Tensor1D): tf.Scalar {
return prediction.sub(actualValue).square().mean();
}
const learningRate = 0.01;
const optimizer = tf.train.sgd(learningRate);
function train() {
optimizer.minimize(() => {
const predsYs = predict(tf.tensor1d(trainX));
const stepLoss = loss(predsYs, tf.tensor1d(trainY));
console.log(stepLoss);
return stepLoss;
});
}
for (let i = 0; i < 10000; i++) {
train();
}
predict(tf.tensor1d(trainX)).print();
运行一次可以看到训练过程中对stepLoss
的打印和最终针对trainX
输出的结果