經典演算法題——第六題 協同推薦SlopeOne 演算法
相信大家對如下的Category都很熟悉,很多網站都有類似如下的功能,“商品推薦”,"猜你喜歡“,在實體店中我們有導購來為我們服務,在網路上
我們需要同樣的一種替代物,如果簡簡單單的在資料庫裡面去撈,去比較,幾乎是完成不了的,這時我們就需要一種協同推薦演算法,來高效的推薦瀏覽者喜
歡的商品。
一:概念
SlopeOne的思想很簡單,就是用均值化的思想來掩蓋個體的打分差異,舉個例子說明一下:
在這個圖中,系統該如何計算“王五“對”電冰箱“的打分值呢?剛才我們也說了,slopeone是採用均值化的思想,也就是:R王五 =4-{[(5-10)+(4-5)]/2}=7 。
下面我們看看多於兩項的商品,如何計算打分值。
rb = (n * (ra - R(A->B)) + m * (rc - R(C->B)))/(m+n)
注意: a,b,c 代表“商品”。
ra 代表“商品的打分值”。
ra->b 代表“A組到B組的平均差(均值化)”。
m,n 代表人數。
根據公式,我們來算一下。
r王五 = (2 * (4 - R(洗衣機->彩電)) + 2 * (10 - R(電冰箱->彩電))+ 2 * (5 - R(空調->彩電)))/(2+2+2)=6.8
是的,slopeOne就是這麼簡單,實戰效果非常不錯。
二:實現
1:定義一個評分類Rating。
/// <summary> /// 評分實體類 /// </summary> public class Rating { /// <summary> /// 記錄差值 /// </summary> public float Value { get; set; } /// <summary> /// 記錄評分人數,方便公式中的 m 和 n 的值 /// </summary> public int Freq { get; set; } /// <summary> /// 記錄打分使用者的ID /// </summary> public HashSet<int> hash_user = new HashSet<int>(); /// <summary> /// 平均值 /// </summary> public float AverageValue { get { return Value / Freq; } } }
2: 定義一個產品類
/// <summary>
/// 產品類
/// </summary>
public class Product
{
public int ProductID { get; set; }
public string ProductName { get; set; }
/// <summary>
/// 對產品的打分
/// </summary>
public float Score { get; set; }
}
3:SlopeOne類
參考了網路上的例子,將二維矩陣做成線性表,有效的降低了空間複雜度。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SupportCenter.Test
{
#region Slope One 演算法
/// <summary>
/// Slope One 演算法
/// </summary>
public class SlopeOne
{
/// <summary>
/// 評分系統
/// </summary>
public static Dictionary<int, Product> dicRatingSystem = new Dictionary<int, Product>();
public Dictionary<string, Rating> dic_Martix = new Dictionary<string, Rating>();
public HashSet<int> hash_items = new HashSet<int>();
#region 接收一個使用者的打分記錄
/// <summary>
/// 接收一個使用者的打分記錄
/// </summary>
/// <param name="userRatings"></param>
public void AddUserRatings(IDictionary<int, List<Product>> userRatings)
{
foreach (var user1 in userRatings)
{
//遍歷所有的Item
foreach (var item1 in user1.Value)
{
//該產品的編號(具有唯一性)
int item1Id = item1.ProductID;
//該專案的評分
float item1Rating = item1.Score;
//將產品編號字存放在hash表中
hash_items.Add(item1.ProductID);
foreach (var user2 in userRatings)
{
//再次遍歷item,用於計算倆倆 Item 之間的差值
foreach (var item2 in user2.Value)
{
//過濾掉同名的專案
if (item2.ProductID <= item1Id)
continue;
//該產品的名字
int item2Id = item2.ProductID;
//該專案的評分
float item2Rating = item2.Score;
Rating ratingDiff;
//用表的形式構建矩陣
var key = Tools.GetKey(item1Id, item2Id);
//將倆倆 Item 的差值 存放到 Rating 中
if (dic_Martix.Keys.Contains(key))
ratingDiff = dic_Martix[key];
else
{
ratingDiff = new Rating();
dic_Martix[key] = ratingDiff;
}
//方便以後以後userrating的編輯操作,(add)
if (!ratingDiff.hash_user.Contains(user1.Key))
{
//value儲存差值
ratingDiff.Value += item1Rating - item2Rating;
//說明計算過一次
ratingDiff.Freq += 1;
}
//記錄操作人的ID,方便以後再次新增評分
ratingDiff.hash_user.Add(user1.Key);
}
}
}
}
}
#endregion
#region 根據矩陣的值,預測出該Rating中的值
/// <summary>
/// 根據矩陣的值,預測出該Rating中的值
/// </summary>
/// <param name="userRatings"></param>
/// <returns></returns>
public IDictionary<int, float> Predict(List<Product> userRatings)
{
Dictionary<int, float> predictions = new Dictionary<int, float>();
var productIDs = userRatings.Select(i => i.ProductID).ToList();
//迴圈遍歷_Items中所有的Items
foreach (var itemId in this.hash_items)
{
//過濾掉不需要計算的產品編號
if (productIDs.Contains(itemId))
continue;
Rating itemRating = new Rating();
// 內層遍歷userRatings
foreach (var userRating in userRatings)
{
if (userRating.ProductID == itemId)
continue;
int inputItemId = userRating.ProductID;
//獲取該key對應專案的兩組AVG的值
var key = Tools.GetKey(itemId, inputItemId);
if (dic_Martix.Keys.Contains(key))
{
Rating diff = dic_Martix[key];
//關鍵點:運用公式求解(這邊為了節省空間,對角線兩側的值呈現奇函式的特性)
itemRating.Value += diff.Freq * (userRating.Score + diff.AverageValue * ((itemId < inputItemId) ? 1 : -1));
//關鍵點:運用公式求解 累計每兩組的人數
itemRating.Freq += diff.Freq;
}
}
predictions.Add(itemId, itemRating.AverageValue);
}
return predictions;
}
#endregion
}
#endregion
#region 工具類
/// <summary>
/// 工具類
/// </summary>
public class Tools
{
public static string GetKey(int Item1Id, int Item2Id)
{
return (Item1Id < Item2Id) ? Item1Id + "->" + Item2Id : Item2Id + "->" + Item1Id;
}
}
#endregion
}
4: 測試類Program
這裡我們灌入了userid=1000,2000,3000的這三個人,然後我們預測userID=3000這個人對 “彩電” 的打分會是多少?
public class Program
{
static void Main(string[] args)
{
SlopeOne test = new SlopeOne();
Dictionary<int, List<Product>> userRating = new Dictionary<int, List<Product>>();
//第一位使用者
List<Product> list = new List<Product>()
{
new Product(){ ProductID=1, ProductName="洗衣機",Score=5},
new Product(){ ProductID=2, ProductName="電冰箱", Score=10},
new Product(){ ProductID=3, ProductName="彩電", Score=10},
new Product(){ ProductID=4, ProductName="空調", Score=5},
};
userRating.Add(1000, list);
test.AddUserRatings(userRating);
userRating.Clear();
userRating.Add(1000, list);
test.AddUserRatings(userRating);
//第二位使用者
list = new List<Product>()
{
new Product(){ ProductID=1, ProductName="洗衣機",Score=4},
new Product(){ ProductID=2, ProductName="電冰箱", Score=5},
new Product(){ ProductID=3, ProductName="彩電", Score=4},
new Product(){ ProductID=4, ProductName="空調", Score=10},
};
userRating.Clear();
userRating.Add(2000, list);
test.AddUserRatings(userRating);
//第三位使用者
list = new List<Product>()
{
new Product(){ ProductID=1, ProductName="洗衣機", Score=4},
new Product(){ ProductID=2, ProductName="電冰箱", Score=10},
new Product(){ ProductID=4, ProductName="空調", Score=5},
};
userRating.Clear();
userRating.Add(3000, list);
test.AddUserRatings(userRating);
//那麼我們預測userID=3000這個人對 “彩電” 的打分會是多少?
var userID = userRating.Keys.FirstOrDefault();
var result = userRating[userID];
var predictions = test.Predict(result);
foreach (var rating in predictions)
Console.WriteLine("ProductID= " + rating.Key + " Rating: " + rating.Value);
}
}