前言
前端轻松入门人工智能(一)juejin.cn/post/704366…
上一期我们简单的了解了神经网络的原理和搭建。因为有些小伙伴对框架使用感到困惑,主要是框架使用与原理的联系可能不是很清楚,这一期解释tensorflow.js框架的使用,以及一些基础概念,帮大家加深知识。 这里的学习曲线是:
graph TD
理解感知机 --> 理解误差回传和梯度概念 --> 框架的基础用法
感知机
感知机这个概念早在1957年就被科学家提出,原先是人们用来类比人脑中神经元活动的过程,我们知道人脑中神经元通常接收一个电信号然后在内部收到刺激后,产生一些递质,这些递质帮助我们产生另外的电信号然后输出到下一个神经元上,不过今天说的感知机其实和神经元压根没太大联系,不要被所谓高大上的生物学概念唬住,因为感知机只是概念上的借鉴,内部实现完全没有神经元复杂。
那么什么是感知机?简单理解就是个函数。输入某些值,然后return出一个值,感知机做的就是这个事情,那么这个“函数”内部是如何处理的呢,可以看参考下图
感知机首先根据输入的shape会给每个输入分配一个权重,分配这个词很重要,也是为什么tensorflow.js中定义模型的时候我们需要先声明inputShape这个参数, 可以看到上一节房价预测代码里包含了它,因为本质上这个参数决定一个感知机上权重的个数,决定我需要开辟多少个内存空间。
model = tf.sequential({
layers: [
tf.layers.dense({inputShape: [1], units: 1}),
...
]
})
知道了权重和输入参数后就能开始计算了
输入X1 * 权重W1 + 常数b1 = 结果1
输入X2 * 权重W2 + 常数b2 = 结果2
····
这么依次计算下去最后把所有结果相加就等于当前感知机的求和结果了,通常我们还会在感知机的求和结果外再包一层非线性函数(学名叫激活函数),使得感知机在计算上能拟合的更好。因为求和只是线性函数无法拟合复杂曲线,包了非线性函数后理论上能拟合任意曲线数据,激活函数之后会详细介绍,目前只要知道它能提高拟合能力即可。
用js表达感知机很简单
function 感知机(input1, input2…) {
return 激活函数(∑(input * w + b))
}
我们再来理解误差概念,当我输入input进入感知机后得到结果,我们把结果与真实的数据比较,就得到误差。上一节说过根据误差来调节W权重,那么应该用什么策略改变W来使得误差最小呢,误差肯定在某个地方会取得最小值,就好比一座山有山峰,有山谷,如果往山峰爬我们理解成误差变大,如果往山谷走理解为误差变小,我们是不是应该要找到最快的下山方式,下山有无数个方向它对应的就是我们说的梯度(准确从数学上来说是梯度的负方向),理论上找到梯度最大地方,然后在这个地方适当的往下迈进一步就能更接近山谷的位置。
用公式表达上述概念就是 w = w - (lr * dl/dw),其中lr是学习率,代表我每次以多大幅度去改变权重,dl/dw就是梯度了,代表损失函数loss对权重w求导数
梯度下降有很多不同策略,不同的梯度下降策略会影响网络收敛的快慢,上节代码使用的是随机梯度下降SGD
model.compile({
loss: 'meanSquaredError',
optimizer: 'sgd'
})
SGD是常用的梯度下降策略,除此之外还有Adam,一般不是特别复杂的网络就固定用SGD好了除非你是真的研究这块那么可能要对梯度下降方法仔细推敲一下。值得庆祝的是基本上所有人工智能框架都把自动求导(算梯度)这件事帮你做好了,在有较好的数据下,框架知道往哪个方向改变W最容易减小误差,这也是为什么神经网络能拟合好,而不是像无头苍蝇一样随机改变W值的原因。
BP神经网络
在理解了感知机的基础上,BP神经网络就好理解了,BP神经网络无非是多个感知机组合,更深更宽而已,每一个感知机算出的结果,会输入进下一层所有的感知机里可以看到下图每个感知机都同时输出了几条线,上面说到每个感知机都会为一个输入分配权重,所以每层每个感知机的W权重都不同。通过这么构建我们的网络,在最后一层输出结果,输出结果就会产生误差,将误差逐层传递,同样根据使误差减小的方式来改变每层权重W,对应的数学过程是求偏导。
在原理弄清楚后,我们就来用多个感知机组成更深的BP网络来做预测吧!可以用上一章房地产的参数来训练网络,并且添加了房屋建成年数的数据,看看与上一节的单个感知机模型比较下,拥有更多感知机、更多特征、更深层数的BP网络它的训练结果是不是更好。
我们在原来代码基础上,在对应的4个步骤里做点改动:
1.建立数据集
因为我们新增了一个维度房屋的建成年数,所以需要对数据稍微组装下,便于输入进网络
// 扩展点数据,并新增年数这个特征
const xData1 = [95, 80, 60, 70, 90, 130, 150] // 面积
const xData2 = [2, 10, 20, 15, 20, 5, 2] // 建成年数
const yData = [500, 400, 200, 300, 320, 650, 900]
const xsData = xData1.map((item, index) => {
const array = []
array.push(item)
array.push(xData2[index])
return array
})
const xs = tf.tensor2d(xsData, [xsData.length, 2])
xs.print()
上述代码执行后xs打印出的格式为
2.定义模型
// dense表示一层的意思,里面的units:3代表该层上有3个感知机;inputShape上文已说过,因
为有两个维度的特征 面积和建成年数 所以每个感知机需要首先声明分配两个权重
// batchNormalization是批标准化层,当我们损失函数很大,但权重很小时dl/dw求导会变得很
巨大导致梯度爆炸,批标准化能帮助我们把数据约束在一定范围内,使得训练更平滑。
model = tf.sequential({
layers: [
tf.layers.dense({inputShape: [2], units: 3}),
tf.layers.dense({units: 3}),
tf.layers.dense({units: 2}),
tf.layers.dense({units: 1}),
tf.layers.batchNormalization(),
]
})
可以看到我们代码中有4次tf.layers.dense,分别对应着我们以下4层
3.训练模型
该步骤无变化
4.预测数据
该步骤无变化,最后预测数据只需要多传一个特征即可
结果
BP神经网络这最后损失收敛在了1300左右
单层感知机最后损失收敛在了3000左右
从结果来看明显我们的BP神经网络的损失更小,意味着我们的网络比之前训练的更强了。
最后我们可以测试下,输入心仪的房屋面积---人要有梦想,那必须是400平,谁不喜欢大平层?!建成年数选近1年的好了,太旧了不要。
看到结果后···
我果然感叹梦想不是每个人都敢有的呀,然后我查了下上海的400平房子价格
虽然建成年数都比较旧,但是都在2000万以上,这么多钱要怎么努力才能赚到,不经感叹贫穷使我轻松。
总结
这一节梳理了感知机和BP神经网络的细节,以及框架的一些使用方法,因为房价的预测需要大量的数据还有维度,如果大家有更多的维度比如房屋靠近地铁站距离、所在区域发达程度(可用GDP衡量)等,然后直接去某个卖房网站爬一点数据来做为自己的数据集,输入到网络中训练即可,相信能网络能训练的更好更准确。
房价预测属于线性回归问题,但在人工智能应用场景中还有一类更重要的场景--逻辑回归,即分类问题,下一节我们将着重介绍它!
以下提供本节全部代码,copy后就能跑了
<html>
<head>
<title>Tensor Flow</title>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis"></script>
</head>
<body>
<div id="acc-cont"></div>
<div style="text-align: center;margin-bottom: 20px;">
<span>样本数据分布</span>
</div>
<div id="loss-cont"></div>
<form name='iForm' onSubmit='formpredict(); return false;')>
输入面积: <input name='area' id="area">
输入年数: <input name='year' id="year">
<input type=submit>
<div>
结果: <span id="res"></span>
</div>
</form>
</body>
<script>
const nr_epochs = 300 // 训练轮数
const trainLogs = [];
const lossContainer = document.getElementById("loss-cont");
const accContainer = document.getElementById("acc-cont");
let model, result
const xData1 = [95, 80, 60, 70, 90, 130, 150] // 面积
const xData2 = [2, 10, 20, 15, 20, 5, 2] // 年数
const yData = [500, 400, 200, 300, 320, 650, 900]
const seriesData = xData1.map((x, index) => ({
x,
y: yData[index]
}))
const data = { values: [seriesData] }
const surface = document.getElementById("acc-cont")
tfvis.render.scatterplot(surface, data);
function initTF() {
// 1. 建立数据集
const xsData = xData1.map((item, index) => {
const array = []
array.push(item)
array.push(xData2[index])
return array
})
const xs = tf.tensor2d(xsData, [xsData.length, 2])
const ys = tf.tensor2d(yData, [yData.length, 1])
xs.print()
ys.print()
// 2. 定义模型
model = tf.sequential({
layers: [
tf.layers.dense({inputShape: [2], units: 3}),
tf.layers.dense({units: 3}),
tf.layers.dense({units: 2}),
tf.layers.dense({units: 1}),
tf.layers.batchNormalization(),
]
})
model.compile({
loss: 'meanSquaredError',
optimizer: 'sgd'
})
model.summary()
// 3.训练模型
result = model.fit(
xs,
ys,
{
epochs: nr_epochs,
callbacks: {
onEpochEnd: async (epoch, logs) => {
console.log(logs, "???")
trainLogs.push({
mse: Math.sqrt(logs.loss),
});
tfvis.show.history(lossContainer, trainLogs, ["mse"]);
}
}
}
)
}
// 4. 预测数据
function formpredict() {
const area = +document.getElementById("area").value
const year = +document.getElementById("year").value
// his.val.value,document.getElementById("area"), document.getElementById("year"))
console.log(area)
const predict_tensor = tf.tensor2d([[area, year]])
const result = model.predict(predict_tensor)
const value = result.dataSync()[0].toFixed(2)
document.getElementById('res').innerHTML = `${value} 万`
}
initTF()
</script>
</html>