推荐系统是机器学习的一个常见应用场景,它用于预测用户对物品的“评分”或“偏好”。一般推荐系统产生推荐列表的方式一般有两种:javascript
如上图所示,简单的说,协同过滤就是给相似的用户推荐相似的东西,由于用户老王和老李比较像,而老李喜欢玩炉石传说,因此咱们给老王也推荐炉石传说。而基于内容的推荐就是由于老王喜欢玩王者荣耀,而撸啊撸是和王者荣耀相似的游戏,因此咱们给老王推荐撸啊撸。java
好了,那么咱们就来利用TensorflowJS构建一个电影推荐系统。git
第一步是数据源,要推荐电影,网上有不少的相关网站。例如IMDB。这里咱们使用另外一你们可能不太熟悉的数据源movielens ,数据分享在grouplens。github
这里咱们主要使用其中的两张表,电影数据movies.csv和用户评分数据ratings.csv算法
id,title,tags 1,Toy Story (1995),Adventure|Animation|Children|Comedy|Fantasy 2,Jumanji (1995),Adventure|Children|Fantasy 3,Grumpier Old Men (1995),Comedy|Romance 4,Waiting to Exhale (1995),Comedy|Drama|Romance 5,Father of the Bride Part II (1995),Comedy 6,Heat (1995),Action|Crime|Thriller
电影数据有三个字段,id,title和tags数组
user,movie,rating,timestamp 1,1,4.0,964982703 1,3,4.0,964981247 1,6,4.0,964982224 1,47,5.0,964983815 1,50,5.0,964982931
而用户评分包含用户的id,电影id,评分(0-5),和时间戳。浏览器
在js中,咱们可使用d3提供的csv方法来加载数据:bash
async function loadData(path) { return await d3.csv(path); } const moviesData = await loadData( "https://cdn.jsdelivr.net/gh/gangtao/datasets@master/csv/movies.csv" ); const ratingsData = await loadData( "https://cdn.jsdelivr.net/gh/gangtao/datasets@master/csv/ratings.csv" );
加载好后,咱们作一点简单的处理,把tag变成数组存储。网络
const movies = {}; const tags = []; moviesData.forEach(movie => { const { id, title, tags: movieTags } = movie; const tagsSplit = movieTags.split("|"); tagsSplit.forEach(tag => { if (tags.indexOf(tag) === -1) { tags.push(tag); } }); movies[id] = { id, title, tags: tagsSplit }; }); const rawData = { tags, movies, ratingsData };
数据加载好了,可是这样的数据还不能直接用来训练模型,为了训练,咱们要对数据作必定的预处理。app
function prepareData(rawData) { const movieProfile = {}; const userProfile = {}; const trainingData = {xs: [], ys: []}; const moviesCount = Object.keys(rawData.movies).length; const increment = 1 / moviesCount; for (let movie of Object.values(rawData.movies)) { const tagsArr = []; const { id, title } = movie; rawData.tags.forEach(tag => { tagsArr.push(movie.tags.indexOf(tag) !== -1 ? 1 : 0); }); movieProfile[movie.id] = { id, title, profile: tagsArr }; } for (let rating of Object.values(rawData.ratingsData)) { const { user: userIdx, movie: movieIdx, rating: ratingStr } = rating; const ratingVal = parseFloat(ratingStr); const ratingNormalized = ratingVal / 5; rating.rating = ratingVal; rating.ratingNormalized = ratingNormalized; let user = userProfile[userIdx]; if (!user) { user = { stats: [ 1, 0 ], tagsData: rawData.tags.map( () => 0 ), ratingData: d3.range(10).map( () => 0 ) } userProfile[userIdx] = user; } if (user.stats[0] > ratingNormalized) user.stats[0] = ratingNormalized; if (user.stats[1] < ratingNormalized) user.stats[1] = ratingNormalized; const movie = rawData.movies[movieIdx]; if (movie) { const { tags } = movie; tags.forEach( tag => { user.tagsData[rawData.tags.indexOf(tag)] += increment; }); user.ratingData[ Math.floor(ratingVal * 2) - 1 ] += increment; } } for (let rating of Object.values(rawData.ratingsData)) { const { user: userIdx, movie: movieIdx, ratingNormalized } = rating; const user = userProfile[userIdx]; const movie = movieProfile[movieIdx]; if (movie) { const { stats, tagsData, ratingData } = user; trainingData.xs.push([].concat(stats).concat(tagsData).concat(ratingData).concat(movie.profile)); trainingData.ys.push(ratingNormalized) } } return { movieProfile, userProfile, trainingData, features: trainingData.xs[0].length, trainedModel: false, moviesCount: Object.keys(movieProfile).length } }
数据的预处理主要包含如下几个步骤:
对于每个电影记录,构建一profile字段,该字段是一个数组,代表了该电影包含的tag的类型,例如 Toy Story (1995) 的 profile对应为[1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],表示该电影的标签包含Adventure|Animation|Children|Comedy|Fantasy五个类型。
对于每个用户,构建三个字段,ratingData,stats,tagsData
首先对于每个评分,咱们都计算一个标准化的评分,由于全部的评分都在0到5之间,因此标准化以后的评分就是 rates/5, 在0到1之间。
stats是一个包含两个数据的数组,分别是该用户的标准化以后的最低和最高评分。
假设总共有10000部电影,这里取一个计算单位1/10000,用于计算ratingData和tagsData。
ratingData记录了该用户对于电影的评价的分布。咱们把它设定为0-9十个阶梯。用 rate*2 - 1 来计算用户评分落在哪个区间。每当有一个评价,就把对应的阶梯加一个单位。用户1的评分记录以下:
[0, 0.00010264832683227263, 0, 0.0005132416341613632, 0, 0.0026688564976390886, 0, 0.007801272839252714, 0, 0.012728392527201836]
分别对应0-5分评价的是个阶梯的总评分。
tagsData记录了用户对于每一种类型的电影的评分统计。相似的:
[0.008725107780743174, 0.002976801478135906, 0.004311229726955449, 0.008519811127078628,...]
记录了该用户对各个类型的电影的总评价的和。这个统计也是标准化的,假设有一个用户看过全部的电影,对每个电影都打五分, 而这个电影又是全类型覆盖的恐怖爱情动做侦探卡通喜剧电影,那么这里的值就是1。 固然并无这样的用户和这样的电影。
同这这样的数据处理,咱们获得了电影数据的标准化结果,代表电影属于哪种类型。一样得到了用户评分数据的标准化结果,包含用户评分的喜爱和对于每一种类型的评分的统计。
咱们把全部的特征综合在一块儿,就能够构建一个训练数据集了。
训练的目标是用户评价的标准化评分 ratingNormalized
对于每个评分,咱们得到的全部的特征包含:stats/tagsData/ratingData/movie.profile
这里模型很是简单,只有一层的8个单元的relu,由于目标是要预测评分,这里损失函数是mse,实际上是构建了一个回归模型, 使用用户对电影的口味和喜爱(tagsData)加上用户的打分习惯(ratingData,stats),以及电影自己的属性(movie.profile), 来预测标准化后的评分。
function buildModel(data) { const model = tf.sequential(); const count = data.trainingData.xs.length; const xsLength = data.features; model.add( tf.layers.dense({ units: 8, inputShape: [xsLength], activation: "relu6" }) ); model.add(tf.layers.dense({ units: 1 })); model.compile({ optimizer: "sgd", loss: "meanSquaredError", metrics: ["accuracy"] }); return model; }
所谓的协同过滤指的就是这里的咱们把用户喜爱建模做为模型的输入特征,协同内容自己,也就是电影的自身属性一块儿做为输入特征来构建模型。
训练过程很简单:
async function trainBatch(data) { console.log("training start!"); model = buildModel(data); const batchIndex = 0; const batchSize = config.datasize; const epochs = config.epochs; const results = []; const xsLength = data.features; const from = batchIndex * batchSize; const to = from + batchSize; const xs = tf.tensor2d(data.trainingData.xs.slice(from, to), [ batchSize, xsLength ]); const ys = tf.tensor2d(data.trainingData.ys.slice(from, to), [batchSize, 1]); const history = await model.fit(xs, ys, { epochs, validationSplit: 0.2 }); console.log("training complete!"); return history; }
模型建好后,还不能单独利用模型来作推荐,由于咱们的模型基于用户和电影的profile能预测一个评分,因此对于摸一个用户而言,咱们须要对全部的电影预测该用户的评分,而后给出评分最高的电影,这个搜索过程比较耗时,取决于电影的数量。
async function recommend(profile, rawData, data) { $("#reStats").empty(); $("#reResults").empty(); const { tags, movies } = rawData; const statesOutput = d3.select("#reStats"); const resultOutput = d3.select("#reResults"); let results = []; for (let movie of Object.values(movies)) { const { stats, tagsData, ratingData } = profile; const movieProfile = data.movieProfile[movie.id].profile; const input = [] .concat(stats) .concat(tagsData) .concat(ratingData) .concat(movieProfile); const rateResult = await model.predict(tf.tensor([input])).data(); statesOutput.text(`searching ${movie.id} ${movie.title}`); results.push({ "title": movie.title, "rate" : rateResult[0]}); } statesOutput.text("searching complete, here list the recommendations"); const recommendResult = results.sort(function(a, b) { return a.rate - b.rate; }).slice(-maxNum); recommendResult.forEach( r => { resultOutput.append("li").text(`${r.title} ${r.rate}`); }) }
如上图所示,最后咱们为20号用户推荐了五部电影。两个柱状图分别表示用户的标签分布和评分分布。
完整代码请见codepen
不管是那种推荐算法,推荐系统的核心都是寻找类似度。其实机器学习的算法有一些是提供类似度检查的,例如KNN。另外SVD也经常被用于推荐系统的构建。本质上来讲,咱们就是把特征变成向量,在几何空间中寻找距离最接近的数据。认为它们是类似的。
最后给你们推荐两个用于作推荐系统的开源库: