前言
tensorflowJS是一个基于javascript的机器学习库,由tensorflow官方团队移植到javascript并进行维护。可以在web和node环境下运行。为了方便数据可视化,将在html中进行编程。本文完成后会提供在线体验网页。
本文为博主的入门实操记录,对于理论性的内容不会提到太多,想更好的了解机器学习原理,强烈建议
先看台湾大学李宏毅教授关于机器学习的课程p2~p3, 里面会提到一些机器学习的基础概念并且讲的非常好!
本文跟随tensorflowJS视频教程,使用美国金县房屋销售数据进行线性回归训练,对房价进行预测。提供本文样本数据下载,以及相关的参考资料连接。
跟随视频:
使用 TensorFlow.js 在 JavaScript 中进行机器学习
api文档:
tensorflowJS API
数据可视化 tfjs-vis API
美国金县房屋销售数据:
kc_house_data.csv
关于机器学习的理解仅为博主个人观点,可能存在偏差,欢迎交流指正。
1 项目初始化
1)创建文件夹命名为 csdn范例
。
2)进入文件夹,并创建 tensorflowJS.html
文件,基于最终要实现的功能进行简单的页面设计。
3)将数据集 kc_house_data.csv
放入文件夹中。
2 tensorflwJS 和 数据可视化库的引入
在 tensorflowJS.html
文件中的 head
标签中引入 tensorflowJS
以及数据可视化库 tfjs-vis
<!-- 通过'script'标签 加载 tensorflowJS 以及数据可视化插件 tfvis -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis"></script>
3 加载csv文件,映射房屋面积与房价,并数据可视化
调用 tf
即可获取 tensorflowJS 的单例对象
tf.data.csv() 方法可通过传入url字符串加载csv文件。由于浏览器限制无法加载本地文件,只能通过超链接获取。所以先通过node环境的 http-serve
模块建立一个小型的服务器。
ps. node环境下在url基础上加上前缀 "file://"
即可
3.1 小型服务器实现
1)通过node的npm包管理下载 http-server
模块
npm i http-server -g
2)打开终端(或cmd,powershell)进入项目目录,输入命令行启动小型服务器
http-server -c-1
3)启动后可看到控制台提供的ip地址,选择其中一个通过浏览器打开,可以看到项目的目录,这时候就可以通过ip地址访问 html 和获取 csv 文件了。
!!!一定要通过ip地址访问html文件,否者在请求csv文件时会存在跨域问题!!!
3.2初始化变量定义
在html中创建 <script>
标签来执行javascript代码。先初始化一些变量,获取按钮dom,和用于log的div的dom
let
// 数据
dataset = null, // 初始数据集
pointDataset = null, // 面积与房价 x~y 点集
featureArray = null, // 因素集
labelArray = null, // 结果集
featureTensor = null, // 因素张量(tensor)集
labelTensor = null, // 结果张量(tensor)集
normalizedFeature = null, // 最小-最大-归一化 后的因素数据集
normalizedLabel = null, // 最小-最大-归一化 后的结果数据集
trainingFeatureTensor = null, // 用于训练的因素张量
testingFeatureTensor = null, // 用于测试的因素张量
trainingLabelTensor = null, // 用于训练的结果张量
testingLabelTensor = null, // 用于测试的结果张量
model = null, // 模型
// 信息打印窗口获取
traningInfo = document.getElementById("traningInfo"),
predictInfo = document.getElementById("predictInfo"),
// 按钮获取
loadBtn = document.getElementById("loadBtn"),
trainingBtn = document.getElementById("trainingBtn"),
predictBtn = document.getElementById("predictBtn"),
toggleVisorBtn = document.getElementById("toggleVisorBtn");
3.3 准备画散点图的函数
调用 tfvis
即可获取数据可视化库的单例对象
tfvis.render.scatterplot() 调用该函数画散点图
// 散点图 tfvis.render.scatterplot() 的 params:表名、点集和该点的含义、最后 x坐标名,y坐标名
async function plot(pointsArray, featureName) {
tfvis.render.scatterplot({
name: `${featureName} vs House Price`
}, {
values: [pointsArray],
series: ['original']
}, {
xLabel: featureName,
yLabel: "price"
})
}
3.4 加载csv文件并完成数据可视化
为加载按钮编写响应函数 load()
,通过该函数:
1)加载 .csv 文件。
2)映射房屋面积与房价生成点集。
3)打乱点集顺序以防点集自身的一些规律影响模型的准确度。
4)画散点图。
tf.data.csv() 调用该函数加载 .csv 文件。返回 tf.data.CSVDataset
对象
tf.data.CSVDataset.toArray() 调用该函数,异步返回数据集的数组形式。返回 Array
对象
tf.util.shuffle() 调用该函数打乱一个数组对象顺序。返回 Array
对象。
// 加载函数
async function load() {
// 禁用一些按钮
loadBtn.setAttribute("disabled", "disabled");
trainingBtn.setAttribute("disabled", "disabled");
predictBtn.setAttribute("disabled", "disabled");
loadBtn.classList.add('disabled');
trainingBtn.classList.add('disabled');
predictBtn.classList.add('disabled');
// 加载文件
traningInfo.innerHTML = `正在加载 kc_house_data.csv ... ...\n`;
dataset = tf.data.csv("http://192.168.158.1:8080/kc_house_data.csv");
traningInfo.innerHTML += `kc_house_data.csv 加载完成!\n`;
traningInfo.innerHTML += `可视化数据集中 ... ...\n`;
// 映射房屋面积与价格,生成点集
pointset = dataset.map(record => ({
x: record.sqft_living, // 房屋面积
y: record.price, // 房屋价格
}))
// 随机排列数据
pointsArray = await pointsDataset.toArray();
if (pointsArray.length % 2 !== 0) { // 保证数据是偶数个 方便后面拆分数据集
pointsArray.pop();
}
tf.util.shuffle(pointsArray);
// 画散点图 查看面积为x,价格为y 的散点分布情况
plot(pointsArray, "square feet");
traningInfo.innerHTML += `数据可视化完成!\n`;
// 启用一些按钮
loadBtn.removeAttribute("disabled");
loadBtn.classList.remove('disabled');
trainingBtn.removeAttribute("disabled");
trainingBtn.classList.remove('disabled');
}
// 绑定加载函数到按钮
loadBtn.onclick = load;
测试:正常加载,并且可以在网页右侧看到散点图
4 可视化面板展开隐藏功能实现
tfvis.visor().toggle() 调用以切换面板显示或隐藏
// 可视化面板显示隐藏函数
function toggleVisor() {
tfvis.visor().toggle();
}
// 绑定面板显示隐藏按钮点击事件
toggleVisorBtn.onclick = toggleVisor;
5 训练模型
5.1 “最小-最大-归一化” 以及 “逆向最小-最大-归一化”的函数编写
最小最大归一化是将一组数据的特征保留的同时,将数的最小最大范围缩小到0~1。
例如 :
[10, 20, 30]
中,将最小的 10
归一化后为下边界 0,最大数 30
归一化后为上边界 1,而 20
根据在数组中的特征,记为 0.5。
即归一化后数组为: [0, 0.5, 1]
。
记 x
为数组中的一个数,min
为数组中的最小数, max
为数组中的最大数。x’
为归一化后的数,x’
的计算公式为:
x’
= (x
- min
) / (max
- min
)
下面两个函数中 tensor
为张量对象,下面是一些会用到的该对象的简单计算函数
.min() 取张量中的最小值
.max() 取张量中的最大值
.sub() 减去一个数
.add() 加上一个数
.div() ➗一个数
.mul() 乘以一个数
5.1.1 归一化函数
// 👇归一化函数👇 将张量中的每个值x换成一个0~1中能代表x的值a (x-min)/(max-min)
function normalize(tensor) {
const min = tensor.min();
const max = tensor.max();
return {
tensor: tensor.sub(min).div(max.sub(min)),
min,
max,
}
}
5.1.2 逆向归一化函数
// 逆向归一化 通过归一化后的值a和最大值max最小值min 返回原始的x
function deNormalize(tensor, min, max) {
return tensor.mul(max.sub(min)).add(min);
}
5.2 训练模型
1)将点集分割为 因素张量 和 结果张量。
2)对张量进行 最小最大归一化,方便计算。
3)对 因素张量 和 结果张量 分别分割为 训练张量 和 测试张量,分别用来训练模型和检测模型。
4)定义模型
----1、指定为线性模型。
----2、创建一层全连接层神经网络,配置该神经网络中有一个神经元,使用偏差 => 更精确拟合方程,设置激活方程为线性方程。
----3、配置loss方程为平方差 => loss方程是评价model好坏的标准,既当前模型的神经元计算的结果与真实值越接近,loss越低,模型越好,调整神经元可以改变loss。配置优化方案为梯度下降,步长为0.1 => 将不同配置的神经元所得到的loss画成一个函数图像的话,loss应为一条曲线,梯度下降是计算当前神经元下loss曲线的斜率,然后根据斜率调整神经元使得loss去往更低的点。步长是每次调整的大小。
5)通过 tfvis
获取每次迭代后数据可视化的回调函数 onEpochEnd
。
6)训练模型。
----1、指定迭代次数为20次 => 既模型会进行20代优化,可以配置每代优化的次数,这里没有配置,具体可看该函数的api。
----2、配置验证集为训练集的20% => 模型训练时可使用训练集中的数据对当前模型进行loss验证,该验证只是评估当前模型的好坏,对神经元的调整不产生影响。
----3、每次迭代结束调用可视化回调函数 onEpochEnd
来显示数据。
tf.tensor2d() 返回一个二维张量对象
tf.split() 分割成指定数量的张量
tf.sequential() 创建一个顺序模型
tf.Sequential.add() 在神经层最顶端添加一层神经网络
tf.layers.dense() 定义一层全连接层神经网络
tf.LayersModel.compple() 编译模型,配置loss和优化器
tf.Sequential.fit() 按配置训练模型
// 训练函数
async function run() {
// 禁用 加载按钮 和 训练按钮
loadBtn.setAttribute("disabled", "disabled");
loadBtn.classList.add('disabled');
trainingBtn.setAttribute("disabled", "disabled");
trainingBtn.classList.add('disabled');
traningInfo.innerHTML += `初始化 因素张量 和 结果张量 中 ... ...\n`;
// 获取因素和结果的张量
featureTensor = tf.tensor2d(pointsArray.map(p => p.x), [pointsArray.length, 1]);
labelTensor = tf.tensor2d(pointsArray.map(p => p.y), [pointsArray.length, 1]);
// 将张量 最小-最大-归一化
normalizedFeature = normalize(featureTensor);
normalizedLabel = normalize(labelTensor);
traningInfo.innerHTML += `分割训练集和测试集 中 ... ...\n`;
// 分割数据集 分别用于训练和测试
[trainingFeatureTensor, testingFeatureTensor] = tf.split(normalizedFeature.tensor, 2);
[trainingLabelTensor, testingLabelTensor] = tf.split(normalizedFeature.tensor, 2);
traningInfo.innerHTML += `创建model 中 ... ...\n`;
// 创建顺序model
model = tf.sequential();
// 添加一层全连接层 *ps神经网络
model.add(tf.layers.dense({
units: 1, // 1个神经元,因为模型比较简单,1个就够了
useBias: true, // 使用偏差
activation: "linear", // 激活方程:线性 这里有很多选项,可以在api中查看,本例比较简单,所以选择线性
inputDim: 1, // 输入的因素个数,只有面积,所以填 1
}))
// 编译model,配置优化器 定义loss
model.compile({
loss: "meanSquaredError", // 用平方差来评价当前模型好坏
optimizer: tf.train.sgd(0.1), // 使用梯度下降进行优化 步长为 0.1
})
// 获取可视化回调函数
const {
onEpochEnd
} = tfvis.show.fitCallbacks({
name: "Training Performance",
}, ['loss']);
traningInfo.innerHTML += `训练model 中 ... ...\n`;
// 训练模型
await model.fit(trainingFeatureTensor, trainingLabelTensor, {
epochs: 20, // 迭代20次
validationSplit: 0.2, // 使用训练集的20%进行loss验证
callbacks: {
onEpochEnd, // 每次迭代结束,调用可视化回调函数
}
})
traningInfo.innerHTML += `训练完成!!!\n`;
// 禁用/启用一些按钮
trainingBtn.setAttribute("disabled", "disabled");
trainingBtn.classList.add('disabled');
loadBtn.removeAttribute("disabled");
loadBtn.classList.remove('disabled');
predictBtn.removeAttribute("disabled");
predictBtn.classList.remove('disabled');
}
// 将run()函数绑定到训练按钮上
trainingBtn.onclick = run;
测试:功能正常,在可视化面板中可以看到训练中loss的变化曲线。
6 测试模型
6.1 添加按钮及样式
6.2 测试功能实现
未完待续。。。