本文利用 synaptic 库构建简单的神经网络,并在浏览器中实现训练过程。该神经网络何以和其他框架共同打造一款简单的推荐系统应用。这种在浏览器上训练的神经网络因为将计算任务分配到各个终端设备,所以服务器的压力大大降低。此外,在终端上训练的神经网络也大大保护了用户的隐私。机器之心对本文做了简要介绍,全部代码请查看 Github 项目地址。
项目地址:https://github.com/markselby9/ml-in-browser
用 JS 进行机器学习?为什么不呢!
项目概览
我们将构建一个基于人工神经网络的简单 Web 应用程序推荐系统。该应用程序包含两页,第一页显示书籍,第二页显示电影。用户可以在第一页中选择他或她感兴趣的书籍,当他点击下一页按钮时,我们实际上可以在后台预测他可能会感兴趣的电影。在用户选择他喜欢的电影之后,我们会给用户显示他所选择的结果,以及我们对他的选择的预测。一些显示截屏如下所示:
在第一页中选择感兴趣的书籍
在第二页中选择感兴趣的电影
将选择与预测结果进行比较
这个应用程序使用 Vue.js 和 ElementUI(Vue.js 的 UI 框架)构建,并使用 synaptic 库构建神经网络。
有什么优点?
该项目的优势可以归结如下:
- 我们将模型训练工作转移到前端而不是后端,这不仅降低了服务器的压力,同时还将一些计算任务分配给许多客户端。由于 npm 社区提供许多神经网络相关的 Javascript 库,这种方法是可行的。
- 我们保护用户的隐私。用户的数据并没有实际被上传到服务器,当服务器更新训练模型时,他们的数据对于服务器是匿名的。
神经网络简要介绍
首先,这里使用的神经网络是最基础的人工神经网络,我们决定仅使用用户的选择作为输入和输出集。本节使用的图像来自一个介绍神经网络的博客:https://ujjwalkarn.me/2016/08/09/quick-intro-neural-networks/。
本项目涉及的概念包括:人工神经网络、神经元、层次和训练(反向传播)。如果你已经熟悉这些概念,你可以跳过此部分。人工神经网络(ANN)是一种由人脑启发的计算模型。它由神经元组成,神经元是神经网络的基本单元。神经元从其它来源接收输入,每个输入分配一个权重,权重根据输入的重要程度赋予。神经元使用激活函数作用于所有输入的加权之和,然后给出输出。
神经元
单层神经网络由几个神经元组成,如下图所示。神经网络可以包含或可以不包含多个隐藏层,每对相邻层之间具有连接,这种连接通常由前面提到的权重表示。
层
但是如何通过正确衡量这些权重来架构神经网络呢?这些权重需要训练,来达到使神经网络正常工作的要求。假设我们有一个数据表,其中包含 1000 对输入和相应的输出。我们首先产生 0 和 1 之间的随机数给出所有权重,然后遍历所有数据对。在每对输入和输出中,我们使用神经网络的激活函数给出计算结果,并将其与实际输出进行比较。然后我们使用反向传播算法重新训练网络并调整权重。更新反向传播的权重可以通过随机梯度下降来完成,这是一种获得最优权重值的方法。
这里只是简要介绍神经网络的原理。更详细的内容,请参考以下链接:
- https://ujjwalkarn.me/2016/08/09/quick-intro-neural-networks/
- https://en.wikipedia.org/wiki/Artificial_neural_network
- https://github.com/cazala/synaptic/wiki/Neural-Networks-101
神经网络在浏览器中的实现
最近有一些在浏览器中实现神经网络的相关研究,如:
- Deeplearnjs:https://github.com/PAIR-code/deeplearnjs
- ConvnetJS:http://cs.stanford.edu/people/karpathy/convnetjs/
- synaptic.js:http://caza.la/synaptic/
我们在这里使用 synaptic.js,因为不管对于 node.js 还是浏览器,它都是一个无架构的神经网络库。我们可以通过 github 库中的 wiki 来检查文档:https://github.com/cazala/synaptic/wiki/Architect。我们计划在浏览器中实现所有的神经网络训练和部分激活函数,服务器(使用简单的 node.js 和 express 搭建服务器框架)只保留包含网络参数的 JSON 文件。synaptic.js 有一个方便的 API 来将神经网络解析成 JSON,并将 JSON 解析为神经网络实例。
应用程序由 Vue.js 和 ElementUI 构建。在创建主应用程序组件的生命周期中,应用程序从服务器获取模型的 JSON 文件,并基于 JSON 文件构建神经网络实例。然后该模型展示 20 张含有电影信息的卡片,让用户选择他/她感兴趣的项目,用户完成选择并单击下一步后,网络实例将调用激活函数,并给出该用户可能想要的书籍的预测值(基于 20 种书籍选项)。同时,应用程序还展示另外 20 张包含书籍信息的卡片,让用户选择。用户点击提交按钮后,应用程序会将预测的书籍列表和实际的书籍列表呈现给用户,并在后台使用新的训练数据来反向传播并重新训练模型。再次训练后,新的神经网络将被解析为 JSON 对象并发送回服务器。
下面是代码。服务器利用简单的 node.js 中的 I/O API 和 Express 构建。
app.post('/getNetwork', (req, res) => {
if (req.body) {
console.log(req.body);
readJSONFromFile((network) => { // read local JSON file
res.send({
code: 200,
network,
});
}, (err) => {
console.log(err.toString());
});
}
});
app.post('/setNetwork', (req, res) => {
if (req.body && req.body.networkJSON) {
console.log(req.body);
const { networkJSON } = req.body;
saveJSONToFile(networkJSON, (err) => { // write to local JSON file
if (err) {
res.send({
code: 500,
err
});
} else {
res.send({
code: 200,
});
}
});
} else {
res.send({
code: 406,
})
}
});
app.listen(3000, () => {
console.log('server started');
});
以及在客户端。在本文中我们不会介绍 DOM 的细节,我们将仅关注组件 app.vue 下的脚本部分。
created() {
// fetch the train model from server
this.content_data = this.shuffle(book_data);
this.loading = true;
axios.post('http://localhost:3000/getNetwork')
.then((response) => {
console.log(response);
this.loading = false;
const networkJSON = response.data.network;
if (networkJSON && Object.keys(networkJSON).length > 0) {
this.$message('Received neural network from server.');
localNetworkInstance = Network.fromJSON(networkJSON);
} else {
this.$message('Created a new network instance.');
// create a new network instance
const inputLayer = new Layer(20);
const hiddenLayer = new Layer(20);
const outputLayer = new Layer(20);
inputLayer.project(hiddenLayer);
hiddenLayer.project(outputLayer);
localNetworkInstance = new Network({
input: inputLayer,
hidden: [hiddenLayer],
output: outputLayer
});
}
})
.catch(function (error) {
this.loading = false;
console.log(error);
});
},
以上是我们在应用程序中创建的生命周期(lifecycle)。它尝试从「getNetwork」API 获取 JSON 对象:如果它是网络的可用 JSON 设置,则它将通过 synaptic 的 fromJSON 方法创建本地网络实例;否则,它将创建一个新的网络实例并保存到「localNetworkInstance」变量中。
当用户在第一页单击「下一页」按钮后,我们在「onClick」功能中调用激活函数,并将其作为预测结果保存在 Vue 组件的数据中。然后在用户选择他/她感兴趣的电影之后,调用重新训练函数。
reTrainByThisUserData() {
// retrain the model by this user's data
if (localNetworkInstance) {
localNetworkInstance.propagate(learningRate, this.trainingSet.output); // propagate the network
this.$message('Neural Network retrained!');
const successFunc = () => {
console.log('success');
this.$message('Successfully sent the new Neural Network!');
};
const errorFunc = (error) => {
console.log('error', error);
this.$message(error);
};
this.loading = true;
axios.post('http://localhost:3000/setNetwork', {
networkJSON: localNetworkInstance.toJSON()
})
.then((response) => {
this.loading = false;
if (response.data && response.data.code === 200) {
successFunc();
} else {
errorFunc(response.data);
}
})
.catch(function (error) {
errorFunc(error)
});
} else {
this.loading = false;
console.log('network is undefined!');
}
}
重新训练过程是一个反向传播过程,利用当前用户的选择作为输入和输出数据对。用户对电影的选择将成为反向传播的数据。在反向传播后,神经网络的权重将被调整,神经网络的新数据将被上传到服务器并被保存。理想情况下,新网络应该更强大:)
进一步探索
正如很多读者可能已经意识到的那样,我们可以利用这个方法做比简单推荐系统更多的事情。改进此项目的一些可能方法包括:
- 更多关于浏览器的信息可以当作输入,例如用户在每张卡上花费的时间、用户的点击事件和滚动事件等。这些信息可以从前端得到。
- 神经网络可以具有更复杂的架构,但注意不能过拟合。
- 前端项目应该考虑大小。目前,构建的文件大小约为 1Mb,这在 PC 上是可以接受的,但对于移动端网站来说可能太大了。如果要在移动设备上使用该项目,应采取优化措施。
有关此项目的完整代码,请查阅参考:https://github.com/markselby9/ml-in-browser/tree/feature/Recommendation_system_in_browser_demo。