協同過濾Item-based演算法實現電影推薦系統
系統詳細設計
- 離線計算推薦電影模組
-
系統所用演算法:
本系統採用協同過濾(Collaborative Filtering)推薦演算法。協同過濾推薦演算法分為預測過程和推薦過程,其包括Item-based演算法和User-based演算法,但經查閱相關資料發現User-based演算法存在兩個問題:1. 資料的稀疏性:一個大型的電影推薦系統會有大量的電影資訊,使用者已打分的電影可能只佔總量的很少一部分,不同使用者之間電影打分的重疊性較低,導致演算法無法找到一個興趣使用者;2. 演算法的擴充套件性:最近鄰演算法的計算量會隨著使用者和電影資訊數量的增加而增加,不適合資訊量大的情況。所以本系統採用了Item-based協同過濾演算法,並對其做了適當修改。 -
計算過程:
計算過程分兩步:
1.計算電影相似度:利用調整的餘弦相似度計算方法,公式如下圖:
2.電影相似度加權求和:使用使用者已打分電影的分數進行加權求和,權值為使用者未打分的各電影與打分的各電影的相似度,然後求平均,公式如下圖:
以上部分均採用了多執行緒技術計算,值得注意的是相似度計算存在很大的冗餘,必須去除冗餘計算,不然計算量很大,這需要在資料庫中建立一個sim表來記錄已經計算的的sim(i,j),當下次需要計算sim(i,j)或sim(j,i)可直接從資料庫讀取而無需重複計算。並且我認為如果R(u,N)值如果比較低,說明使用者不喜歡此電影,這條相似度資訊可以忽略,這需要設定一個打分閥值。 - 計算模組流程圖:
由於計算量較大,計算模組採用了多執行緒技術,來提升系統執行效率,如下圖為計算模組主執行緒和子執行緒的流程圖,如圖a,圖b:
圖a
圖b -
關鍵程式碼分析
- 計算模組採用多執行緒技術,主執行緒通過建立子執行緒來實現大量資料的計算,其中執行緒狀態的監測很有必要,如下面為主執行緒實現程式碼:
sql="SELECT uid FROM user group by uid";
uidItems=uiddb.executeQuery(sql);
for (int i=0;i<threadNum;i++){
thCom[i]=new Thread(new thComp(uidItems,i));
thCom[i].start();
}//for i
boolean thFinished=false;
while(!thFinished){
try{
Thread.sleep(sleepTime);
}catch(Exception e){//try
e.printStackTrace();
}//catch();
thFinished=true;
for(int i=0;i<threadNum;i++){
if(thCom[i].isAlive()){
//System.out.println(thCom[i].getName()+":"+thCom[i].getState());
thFinished=false;
break;
}//if
}//for i
}//while(!thFinished)
- 由於子執行緒是從主執行緒建立的ResultSet中讀取資料,這就需要子執行緒互斥的訪問臨界資源ResultSet,如下面程式碼所示:
public synchronized int getUserID(ResultSet uidItems){
int uid=0;//0 shows that no usrid needs to process
try{
if(uidItems.next()){
uid=uidItems.getInt("uid");
}//if
}catch(Exception e){
e.printStackTrace();
}//catch
return uid;
}//getUserID()
- 在為使用者A計算要推薦的電影,計算電影i與電影j的相似度sim(i,j)時,可能在為使用者B計算要推薦的電影已經計算過,如果再計算一次,會造成計算的冗餘,降低了計算效率,使系統性能下降,這需要將將計算的sim(i,j)儲存下來,然後需要計算sim(i,j)先判斷是否已經計算過sim(i,j)或sim(j,i),如果計算過直接讀取,否則計算,本系統中是將sim(i,j)的計算值儲存到資料庫的sim表中,主要實現程式碼如下:
String sql="SELECT sim FROM sim WHERE midi='"+midi+"' and midj='"+midj+"' or midi='"+midj+"' and midj='"+midi+"'";
uidItems=uiddb.executeQuery(sql);
//select and fllaowing insert may have problems
try{
if(uidItems.next()){
return uidItems.getDouble("sim");
}//if(uidItems.next())
}catch(Exception e){
e.printStackTrace();
}//catch()
public synchronized void insertSim(String sql){
//here may have some problems
recSimdb.executeUpdate(sql);
}//insertSim
- 瀏覽推薦電影模組
-
瀏覽使用者推薦電影流程圖
- 關鍵程式碼分析
該模組主要是按推薦指數降序排序顯示推薦給每個使用者的電影,來滿足使用者的娛樂需求,提高使用者的滿意率,此功能主要程式碼如下所示:
public synchronized int getUserID(ResultSet uidItems){
int uid=0;//0 shows that no usrid needs to process
try{
if(uidItems.next()){
uid=uidItems.getInt("uid");
}//if
}catch(Exception e){
e.printStackTrace();
}//catch
return uid;
}//getUserID()
資料庫設計模組
由於電影和使用者資訊資料量比較大,本系統將資料檔案中的資料匯入到mysql資料庫中,同時增加了一些額外資料表來滿足系統的要求,比如儲存使用者平均打分的avgrating表和降低計算電影相似性冗餘的sim表。在經常查詢的欄位添加了索引,以提高查詢速度,在group by欄位也添加了索引。下面簡單介紹下資料庫中各個表結構如下幾張圖所示。
協同過濾Item-based演算法
(1)相似度計算
Item-based演算法首選計算物品之間的相似度,計算相似度的方法有以下幾種:
1. 基於餘弦(Cosine-based)的相似度計算,通過計算兩個向量之間的夾角餘弦值來計算物品之間的相似性,公式如下:
其中分子為兩個向量的內積,即兩個向量相同位置的數字相乘。
2. 基於關聯(Correlation-based)的相似度計算,計算兩個向量之間的Pearson-r關聯度,公式如下:
其中表示使用者u對物品i的打分,表示第i個物品打分的平均值。
3. 調整的餘弦(Adjusted Cosine)相似度計算,由於基於餘弦的相似度計算沒有考慮不同使用者的打分情況,可能有的使用者偏向於給高分,而有的使用者偏向於給低分,該方法通過減去使用者打分的平均值消除不同使用者打分習慣的影響,公式如下:
其中表示使用者u打分的平均值。
(2)預測值計算
根據之前算好的物品之間的相似度,接下來對使用者未打分的物品進行預測,有兩種預測方法:
- 加權求和。
用過對使用者u已打分的物品的分數進行加權求和,權值為各個物品與物品i的相似度,然後對所有物品相似度的和求平均,計算得到使用者u對物品i打分,公式如下:
其中為物品i與物品N的相似度,為使用者u對物品N的打分。
- 迴歸。
和上面加權求和的方法類似,但迴歸的方法不直接使用相似物品N的打分值,因為用餘弦法或Pearson關聯法計算相似度時存在一個誤區,即兩個打分向量可能相距比較遠(歐氏距離),但有可能有很高的相似度。因為不同使用者的打分習慣不同,有的偏向打高分,有的偏向打低分。如果兩個使用者都喜歡一樣的物品,因為打分習慣不同,他們的歐式距離可能比較遠,但他們應該有較高的相似度。在這種情況下使用者原始的相似物品的打分值進行計算會造成糟糕的預測結果。通過用線性迴歸的方式重新估算一個新的值,運用上面同樣的方法進行預測。重新計算的方法如下:
其中物品N是物品i的相似物品,和通過對物品N和i的打分向量進行線性迴歸計算得到,為迴歸模型的誤差。具體怎麼進行線性迴歸文章裡面沒有說明,需要查閱另外的相關文獻。
java程式碼實現如下:
import java.sql.ResultSet;
import movierec.DB;
import movierec.FUN;
import movierec.SYS;
public class ItemBasedSim {
/**
* @param args
*/
public int threadNum=5;
public int sleepTime=3000;//ms
public double thresholdRating=2.5;
public int recNum=50;//the num of movies to recommend,which need to be computed
public int RuNNum=20;//
public Thread thCom[];
DB recSimdb=null;
//public static void main(String[] args) {
// TODO Auto-generated method stub
//System.out.println("test");
/*
long startTime = System.currentTimeMillis();
System.out.println(fun.getDateTime()+" starts...");
itemBasedSim ibs=new itemBasedSim (5);
//ibs.sim(3,6);
//System.out.println("sim:"+ibs.sim(5,9));
ibs.compRecDeg();
//ibs.compRecDegUI(1,1);
long finishTime = System.currentTimeMillis();
System.out.println(fun.getDateTime()+" finishs,spent:"+(finishTime-startTime)+"ms");
*/
//System.out.println("spent:"+(finishTime-startTime)+"ms");
/*startTime = System.currentTimeMillis();
ibs.compRecDegUI(2,1);
finishTime = System.currentTimeMillis();
System.out.println("spent:"+(finishTime-startTime)+"ms");*/
//}//main()
public ItemBasedSim(int thNum){
threadNum=thNum;
thCom=new Thread[threadNum];
}//itemBasedSim()
public void compRecDeg(){
//SELECT avg(rating) FROM rating,item WHERE mid=itemid and war=1 group by usrid
DB uiddb=new DB();
recSimdb=new DB();
ResultSet uidItems=null;
String sql="delete FROM avgrating";
uiddb.executeUpdate(sql);
sql="insert into avgrating SELECT usrid,avg(rating) avgRating FROM rating group by usrid";
uiddb.executeUpdate(sql);
//sql="delete FROM sim";
//uiddb.executeUpdate(sql);
sql="delete FROM recom";
uiddb.executeUpdate(sql);
sql="SELECT uid FROM user group by uid";
uidItems=uiddb.executeQuery(sql);
for(int i=0;i<threadNum;i++){
thCom[i]=new Thread(new thComp(uidItems,i));
thCom[i].start();
}//for i
boolean thFinished=false;
while(!thFinished){
try{
Thread.sleep(sleepTime);
}catch(Exception e){//try
e.printStackTrace();
}//catch();
thFinished=true;
for(int i=0;i<threadNum;i++){
if(thCom[i].isAlive()){
//System.out.println(thCom[i].getName()+":"+thCom[i].getState());
thFinished=false;
break;
}//if
}//for i
}//while(!thFinished)
recSimdb.close_state();
recSimdb.close_connect();
uiddb.close_result();
uiddb.close_state();
uiddb.close_connect();
}//compRecDeg()
public double compRecDegUI(int uid,int iid,int thID,DB mrdb,DB mydb,DB uiddb){
//db mrdb=new db();
String mrsql="SELECT itemid,rating FROM rating WHERE usrid='"+uid+"'";
mrsql+=" and rating>='"+this.thresholdRating+"' limit 0,"+this.RuNNum;
//need to
ResultSet mrItems=null;
double simUI=0;
double fz=0;
double fm=0;
double pui=0;
//System.out.println("uid="+uid+" , itemid= "+iid+" , threadID="+thID);
mrItems=mrdb.executeQuery(mrsql);
try{
while(mrItems.next()){
simUI=sim(iid,mrItems.getInt("itemid"),thID,mydb,uiddb);
fz+=simUI*mrItems.getInt("rating");
fm+=Math.abs(simUI);
//System.out.println(uid+","+iid+"----------2");
}//while(uidItems.next())
if(fm!=0)
pui=fz/fm;
else
pui=0;
//System.out.println("----------3");
}catch(Exception e){
e.printStackTrace();
}//catch()
return pui;
}//comRecDegUI()
public double sim(int midi,int midj,int thID,DB mydb,DB uiddb){
//db mydb=new db();
//db uiddb=new db();
ResultSet mItems=null;
ResultSet uidItems=null;
String sql="SELECT sim FROM sim WHERE midi='"+midi+"' and midj='"+midj+"' or midi='"+midj+"' and midj='"+midi+"'";
uidItems=uiddb.executeQuery(sql);
//select and fllaowing insert may have problems
try{
if(uidItems.next()){
return uidItems.getDouble("sim");
}//if(uidItems.next())
}catch(Exception e){
e.printStackTrace();
}//catch()
///String sqlUid="SELECT usrid FROM rating GROUP BY usrid ";
String sqlUid="SELECT usrid,avgRating FROM avgrating ";
//maybe there are users who has not commented any movie
uidItems=uiddb.executeQuery(sqlUid);
int uid=0;
double ru=0;
double rui=0;
double ruj=0;
double mid1=0;//(rui-ru)(ruj-ru)
double mid2=0;//pow((rui-ru),2);
double mid3=0;//pow((ruj-ru),2);
try{
while(uidItems.next()){
ru=0;
rui=0;
ruj=0;
uid=uidItems.getInt("usrid");
ru=uidItems.getDouble("avgRating");
sql="SELECT rating FROM rating WHERE usrid='"+uid+"' AND itemid ='"+midi+"'";
mItems= mydb.executeQuery(sql);
if(mItems.next()){
rui=mItems.getDouble("rating");
}//if(mitems.next())
sql="SELECT rating FROM rating WHERE usrid='"+uid+"' AND itemid ='"+midj+"'";
mItems= mydb.executeQuery(sql);
if(mItems.next()){
ruj=mItems.getDouble("rating");
}//if(mitems.next())
//System.out.println("RU="+ru);
//System.out.println("RUI="+rui);
//System.out.println("RUJ="+ruj);
mid1+=(rui-ru)*(ruj-ru);
mid2+=Math.pow(rui-ru, 2);
mid3+=Math.pow(ruj-ru,2);
}//while(uidItems.next())
mid2=Math.sqrt(mid2)+Math.sqrt(mid3);
if(mid2!=0){
mid3=mid1/mid2;
}else
mid3=0;
}catch(Exception e){
e.printStackTrace();
}//catch
sql="INSERT INTO sim (midi, midj, sim,timestamp) VALUES ('"+midi+"','"+midj+"','"+mid3+"','";
sql+=System.currentTimeMillis()+thID+Math.random()+"')";
//mydb.executeUpdate(sql);
//insert may have problems ,because some select no sim at the same time
//but have unique index
try{
insertSim(sql);
}catch(Exception e){
//
}//catch
return mid3;
}//sim()
public synchronized int getUserID(ResultSet uidItems){
int uid=0;//0 shows that no usrid needs to process
try{
if(uidItems.next()){
uid=uidItems.getInt("uid");
}//if
}catch(Exception e){
e.printStackTrace();
}//catch
return uid;
}//getUserID()
public synchronized void insertRecom(String sql){
recSimdb.executeUpdate(sql);
}//insertRecom
public synchronized void insertSim(String sql){
//here may have some problems
recSimdb.executeUpdate(sql);
}//insertSim
class thComp implements Runnable{
ResultSet uidItems=null;
int thID=0;
int gNum[]=new int[SYS.genre.length];
int gNumS=0;
public void run(){
int uid=0;
int mid=0;
String isql="";
String rsql="";
//String orderCol[]={"id","usrid","itemid","rating","timestamp"};
double recDeg=0;
DB middb=new DB();
DB mrdb=new DB();
DB mydb=new DB();
DB uiddb=new DB();
ResultSet midItems=null;
int midNum=0;
int i=0;
int hasRecIid[] = new int[SYS.recNum*2+5];
int hRNum=0;
while(0!=(uid=getUserID(uidItems))){
//here should compute the user's intersting to decide the num of each
//kind to recommend
hRNum=0;
gNumS=0;
for(i=0;i<SYS.genre.length;i++){
gNum[i]=0;
isql="SELECT avg(rating) avgR FROM rating,item WHERE mid=itemid and "+SYS.genre[i]+"=1 and usrid='"+uid+"'";
midItems=middb.executeQuery(isql);
try{
if(midItems.next())
gNum[i]=(int) midItems.getDouble("avgR");
}catch(Exception e){
e.printStackTrace();
}//catch
gNumS+=gNum[i];
}//for i
if(gNumS>0){
for(i=0;i<SYS.genre.length;i++){
midNum=(int) (gNum[i]/(gNumS*1.0)*SYS.recNum*2);
if(midNum>=1){
isql="SELECT itemid FROM rating,item WHERE mid=itemid and "+SYS.genre[i]+"=1 and usrid='"+uid+"' and rating>='"+thresholdRating;
isql+="' group by itemid order by rating desc limit 0,"+midNum;
//System.out.println(isql);
//各類間可能會有重複的item
midItems=middb.executeQuery(isql);
try{
while(midItems.next()){
//System.out.println("----");
recDeg=0;
mid=midItems.getInt("itemid");
if(hasRec(hasRecIid,hRNum,mid))
continue;
hasRecIid[hRNum++]=mid;
//System.out.println(thID+":("+hRNum+"):"+uid+","+mid);
recDeg=compRecDegUI(uid,mid,thID,mrdb,mydb,uiddb);//very cost time
rsql="INSERT INTO recom (id, uid, mid, recdeg) VALUES (NULL,'"+uid+"','"+mid+"','"+recDeg+"')";
//System.out.println(rsql);
insertRecom(rsql);
}//while(iidItems.next())
}catch(Exception e){
e.printStackTrace();
}//catch()
}//if(midNum>=1)
}//for
}else{
isql="SELECT itemid FROM rating where usrid!='"+uid+"' and rating>="+thresholdRating+" group by itemid order by rating desc";
isql+=" limit 0,"+SYS.recNum*2;
midItems=middb.executeQuery(isql);
try{
while(midItems.next()){
recDeg=0;
mid=midItems.getInt("itemid");
recDeg=compRecDegUI(uid,mid,thID,mrdb,mydb,uiddb);
rsql="INSERT INTO recom (id, uid, mid, recdeg) VALUES (NULL,'"+uid+"','"+mid+"','"+recDeg+"')";
//System.out.println(rsql);
insertRecom(rsql);
}//while(iidItems.next())
}catch(Exception e){
e.printStackTrace();
}//catch()
}//else
}//while(0!=(uid=getUserID(uidItems)))
middb.close_result();
middb.close_state();
middb.close_connect();
uiddb.close_result();
uiddb.close_state();
uiddb.close_connect();
mrdb.close_result();
mrdb.close_state();
mrdb.close_connect();
}//run()
public thComp(ResultSet uidItems,int thID){
this.uidItems=uidItems;
this.thID=thID;
}//thComp
public boolean hasRec(int hasRecIid[],int hRNum,int iid){
boolean flag=false;
for(int i=0;i<hRNum;i++)
if(hasRecIid[i]==iid){
flag=true;
break;
}//if(hasRecIid[i]==iid)
return flag;
}
}//class cdatatodb
}