在瀏覽器中進行深度學習:TensorFlow.js (十)構建一個推薦系統

推薦系統是機器學習的一個常見應用場景,它用於預測用戶對物品的「評分」或「偏好」。一般推薦系統產生推薦列表的方式一般有兩種:javascript

  • 協同過濾以及基於內容推薦,或者基於個性化推薦。協同過濾方法根據用戶歷史行爲(例如其購買的、選擇的、評價過的物品等)結合其餘用戶的類似決策創建模型。這種模型可用於預測用戶對哪些物品可能感興趣(或用戶對物品的感興趣程度)。
  • 基於內容推薦利用一些列有關物品的離散特徵,推薦出具備相似性質的類似物品。

“recommendation system”的图片搜索结果

如上圖所示,簡單的說,協同過濾就是給相似的用戶推薦相似的東西,由於用戶老王和老李比較像,而老李喜歡玩爐石傳說,因此咱們給老王也推薦爐石傳說。而基於內容的推薦就是由於老王喜歡玩王者榮耀,而擼啊擼是和王者榮耀相似的遊戲,因此咱們給老王推薦擼啊擼。java

好了,那麼咱們就來利用TensorflowJS構建一個電影推薦系統。git

數據源

第一步是數據源,要推薦電影,網上有不少的相關網站。例如IMDB。這裏咱們使用另外一你們可能不太熟悉的數據源movielens ,數據分享在grouplensgithub

這裏咱們主要使用其中的兩張表,電影數據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也經常被用於推薦系統的構建。本質上來講,咱們就是把特徵變成向量,在幾何空間中尋找距離最接近的數據。認爲它們是類似的。

最後給你們推薦兩個用於作推薦系統的開源庫:

參考

相關文章
相關標籤/搜索