1. 程式人生 > >【推薦系統實戰】:C++實現基於用戶的協同過濾(UserCollaborativeFilter)

【推薦系統實戰】:C++實現基於用戶的協同過濾(UserCollaborativeFilter)

color style popu ted std 相似度 abi ear result

好早的時候就打算寫這篇文章,可是還是參加阿裏大數據競賽的第一季三月份的時候實驗就完畢了。硬生生是拖到了十一假期。自己也是醉了。。。

找工作不是非常順利,希望寫點東西回想一下知識。然後再攢點人品吧,僅僅能如此了。


一、問題背景

二、基於用戶的協同過濾算法介紹

三、數據結構和實驗過程設計

四、代碼


一、問題背景

首先介紹一下問題的背景。如今我有四個月的用戶、品牌數據<user,brand>。即用戶在這四個月中的某一天購買了某個品牌(當然為了簡化算法模型。將購買時間省去,後面再說)。

即如今有這四個月的數據。怎樣為用戶推薦他們感興趣的產品下個月購買?當然解決問題的算法和模型有非常多非常多種。如今就解釋一下協同過濾算法。


二、基於用戶的協同過濾算法介紹(User Collaborative Filter)

基於鄰域的算法是推薦系統中最主要的算法,該算法不僅在學術界得到了深入的研究。並且在工業界也有廣泛的應用。

基於鄰域的算法分為兩大類,一類是基於用戶的協同過濾算法。還有一類是基於物品的協同過濾算法。

這裏僅僅介紹一種基於用戶的協同過濾算法。

在一個在線個性化推薦系統中,當一個用戶A須要個性化推薦時,能夠先找到和他有相似興趣的其它用戶。然後把那些用戶喜歡的、而用戶A沒有聽說過的物品推薦給A。這樣的方法稱為基於用戶的協同過濾算法

從上面的描寫敘述能夠看到。基於用戶的協同過濾算法主要包含兩個步驟:

(1)找到和目標用戶相似的用戶集合。

(2)找到這個集合中的用戶喜歡的,且目標用戶沒有聽說過的物品推薦給目標用戶。

步驟1的關鍵就是計算兩個用戶的興趣相似度。

這裏,協同過濾算法主要利用行為的相似度計算興趣的相似度。給定用戶u和用戶v,令N(u)表示用戶u以前有過正反饋的物品集合(在我們的問題背景之下也就是用戶u以前買過的物品集合),令N(v)為用戶v以前有過正反饋的物品集合。那麽。我們能夠通過例如以下的Jaccard公式簡單的計算u和v的興趣相似度:

技術分享

或者通過余弦相似度計算:

技術分享

對於下圖的用戶行為記錄

技術分享

在該例中,用戶A對物品{a,b,c}有過行為,用戶B對物品{a,c}有過行為,利用余弦相似度公式計算用戶A和用戶B的興趣相似度,以及A和C、D的相似度:

技術分享

技術分享

那麽,假設兩兩用戶都利用余弦相似度計算相似度。

這樣的方法的時間復雜度是O(|U|*|U|),這在用戶數非常大的情況下十分耗時。其實。非常多用戶相互之間並沒有同樣的物品產生行為,即非常多時候N(u)和N(v)的交集為0。

那麽,一種高效的算法就是首先計算出交集不為0的用戶對{u,v}。然後再對這樣的情況除以分母|N(u)UN(v)|(或者還有一種根號形式)。

為此。能夠首先建立物品到用戶的倒查表,對於每一個物品都保存對該物品產生過行為的用戶列表。令系數矩陣C[u][v]=|N(u)並N(v)|。那麽,如果用戶u和用戶v同一時候屬於倒排表中K個物品相應的用戶列表,就有C[u][v]=K。

從而。能夠掃描倒查表中每一個物品相應的用戶列表,將用戶列表中的兩兩用戶相應的C[u][v]加1,終於就能夠得到全部用戶之間不為0的C[u][v]。

對於上圖的用戶行為記錄建立物品-用戶的倒排表

技術分享

建立一個4*4的用戶相似度矩陣W,對於物品a。將W[A][B]和W[B][A]加1。對於物品b,將W[A][C]和W[C][A]加1,以此類推。

掃描全然部物品後,我們能夠得到終於的W矩陣。這裏的W是余弦相似度中的分子部分,然後將W除以分母能夠得到終於的用戶興趣相似度。


得到用戶之間的興趣相似度後。UserCF算法會給用戶推薦和他興趣最相似的K個用戶喜歡的物品。例如以下的公式度量了UserCF算法中用戶u對物品i的感興趣程度:

技術分享

S(u,K):包括和用戶u興趣最接近的K個用戶。

N(i):對物品i有過行為的用戶集合

Wuv:用戶u和用戶v的興趣相似度

Rvi:用戶v對物品i的興趣(這裏都為1)

說明:i物品是用戶u之前沒有接觸過的。那麽用戶u對物品i的感性卻程度的計算過程能夠分為幾個步驟:

①找到與用戶u近期的K個用戶(通過用戶相似度矩陣

②通過K個用戶和N(i)(對物品i有過購買的用戶集合)的交集得到K個用戶中對i感興趣的若幹用戶集合v[]。

③將用戶u和集合v[]中每個用戶v[i]之間的相似度累加的總和即為用戶u對於物品i的感興趣程度


三、數據結構和實驗過程設計

3.1:輸入輸出

·input:前三個月用戶和用戶相應所購買的物品 ·output: 1)為每一個用戶所推薦的物品 2)依據得到的結果然後和第四個月的數據求得準確率precision和召回率recall 在此介紹一下準確率precision和召回率recall:

準確率:技術分享
註:
N 為參賽隊預測的用戶數
pBrandsi為對用戶i 預測他(她)會購買的品牌列表個數
hitBrandsi對用戶i預測的品牌列表與用戶i真實購買的品牌交集的個數

召回率:技術分享
註:
M 為實際產生成交的用戶數量
bBrandsi為用戶i 真實購買的品牌個數
hitBrandsi預測的品牌列表與用戶i真實購買的品牌交集的個數

技術分享


3.2:數據結構設計

map< int,int > userid_id:將userid映射到從0開始的依次遞增的數值(id:0~n-1),為興許映射成矩陣做準備。

map< int,int > id_userid:與userid_id正好相反

map< int , set<int> > user_brands:用戶-物品表:用戶以及用戶相應購買的物品列表

map< int , set<int> > id_brands:將user_brands根據userid_id轉換成為id_brands

map< int , set<int> > brand_ids:物品-用戶倒排表

set<int> brand_all :全部品牌的集合

map< int , set<int> > user_brand_rec :為用戶推薦的品牌集合

dobule sim_mat[MAX][MAX] :用戶之間的相似程度矩陣

3.3:算法過程:

<span style="font-size:14px;">1).讀入數據。將數據用user_brands存起來,同一時候建立userid_id,再建立id_brands
2).遍歷id_brands得到物品-用戶倒排表brand_ids,依據倒排表得到用戶的類似矩陣sim_mat
3).為每個用戶推薦產品:
    3.1).求用戶u買過的brand和全部brand的差集得到用戶u沒有買過的物品集合brand_unused;
    3.2).用戶u對每個沒用過的物品i的興趣p(u,i):
        3.2.1).找到與用戶u近期的k個用戶
        對於每個沒用過的物品i:
        3.2.2).找出這k個用戶中對物品i有過行為的用戶v[]√
        3.2.3).將用戶u和v[j]的興趣類似度累加
    3.3).取前m個最感興趣的brand推薦給用戶;</span>


四、代碼

UCF.cc(用戶協同過濾核心代碼):

#include<iostream>
#include<fstream>
#include<stdio.h>
#include<map>
#include<set>
#include<vector>
#include<cstdlib>
#include<cmath>
#include<cstring>
#include<algorithm>

using namespace std;

const int MAX = 1000;

class UserCF
{
private:
        int k , m ; //k: the most k persons interested in the brand i ; m : choose the former m brands which user u are most interested in
		map< int,int > userid_id;
		map< int,int > id_userid;
		map< int,set<int> > user_brands;
		map< int,set<int> > id_brands;
		map< int,set<int> > brand_ids;
		set<int> brand_all;
		map< int,set<int> > user_brand_rec;
		ifstream fin;
		ofstream fout;
		double sim_mat[MAX][MAX];
		typedef struct sim_idx	
		{
			double sim;
			int idx;
			bool operator > (const sim_idx &other) const
			{
				return sim > other.sim;
			}
		}sim_idx;
		typedef struct brand_interest
		{
			double Int; // Int = interest
			int brand;
			bool operator > (const brand_interest &other) const
			{
				return Int > other.Int;
			}
		}brand_interest;
            
public: 
        UserCF(int _k , int _m):k(_k),m(_m)
		{
			fin.open("user_brand_m123.txt");
			fout.open("rec_result_by_m123.txt");
			if(!fin||!fout)
			{
				cout<<"can not open the file"<<endl;
				exit(1);
			}

			//userid_id , id_userid , user_brands , brand_all
			int userid,brandid,i=0;
			while(fin>>userid>>brandid)
			{
				if( user_brands[userid].empty() )
				{
					userid_id[userid] = i;
					id_userid[i] = userid;
					i++;
				}
				user_brands[userid].insert(brandid);
				brand_all.insert(brandid);
			}
			
			//id_brands
			map< int,set<int> > :: iterator it = user_brands.begin();
			while( it!=user_brands.end() ) 
			{
				id_brands[ userid_id[it->first] ] = it->second;
				it++;
			}
			bzero(sim_mat,0);
		}
		
		void get_sim_mat()
		{
			get_reverse_table();
			map< int,set<int> > :: iterator it = brand_ids.begin();
			while(it!=brand_ids.end())
			{
				vector<int> tmp( it->second.begin(),it->second.end() );
				int len = tmp.size();
				//for each brand , traverse all two pair users , sim_mat increment
				for(int i=0;i<len;i++)
					for(int j=i+1;j<len;j++)
					{
						sim_mat[ tmp[i] ][ tmp[j] ]+=1;
						sim_mat[ tmp[j] ][ tmp[i] ]+=1;
					}
					it++;
			}

			int len = id_userid.size();
			for(int i=0;i<len;i++)
				for(int j=0;j<len;j++)
				{
					sim_mat[i][j] /= sqrt( user_brands[ id_userid[i] ].size() * user_brands[ id_userid[j] ].size()  );
					sim_mat[j][i] = sim_mat[i][j];
				}
			cout<<endl;
		}

		//brand_ids - brand : userid1,userid2,userid3...
		void get_reverse_table()
		{
			map< int,set<int> > :: iterator it = id_brands.begin();
			while( it!=id_brands.end() )
			{
				set<int> tmp = it->second;
				set<int> :: iterator it2 = tmp.begin();
				while(it2!=tmp.end())
				{
					brand_ids[*it2].insert(it->first);
					it2++;
				}
				it++;
			}
		}


		set<int> get_rec_brand_set_by_user(int userid)
		{
			//3.1
			set<int> brand_unused;
			/* set_difference:find different set between two set
			 * function : get brand set that userid has never bought before
			 */
			set_difference(brand_all.begin(),brand_all.end(),user_brands[userid].begin(),user_brands[userid].end(),inserter( brand_unused , brand_unused.begin() ) );	
			
			sim_idx simidx;
			vector<sim_idx> vec_sim_idx;
			int len = userid_id.size();
			int id = userid_id[userid];
			for(int i=0;i<len;i++)
			{
				simidx.sim=sim_mat[id][i];
				simidx.idx=i;
				vec_sim_idx.push_back(simidx);
			}
			sort( vec_sim_idx.begin(),vec_sim_idx.end(),greater<sim_idx>() ); //order by desc
			
			//3.2.1
			set<int> rec_ids;
			vector<sim_idx> :: iterator it = vec_sim_idx.begin();
			for(int i=0;i<k;i++)
			{
				rec_ids.insert( (*it).idx );
				it++;
			}

			set<int> rec_brand;
			set<int> :: iterator itt = brand_unused.begin();
			vector<brand_interest> vec_bi; //userid‘s interest level toward brand
			brand_interest bi;
			while( itt!=brand_unused.end() )
			{
				//3.2.2
				vector<int> newset; // or set<int> newset 
				set<int> ids = brand_ids[*itt];
				set_intersection(rec_ids.begin(),rec_ids.end(),ids.begin(),ids.end(),inserter( newset,newset.begin() ));
				if(newset.empty())
				{
					itt++;
					continue;
				}
				double interest = 0.0;
				int len = newset.size();
				for(int i=0;i<len;i++)
				{
					interest += sim_mat[ userid_id[userid] ][ newset[i] ];
				}
				//3.2.3
				bi.brand = *itt;
				bi.Int = interest;
				vec_bi.push_back(bi);
				itt++;
			}//while
                        //sort(vec_bi.begin(),vec_bi.end(),greater<brand_interest>() );[email protected]
			//3.3
			for(int i=0;i<m&&i<vec_bi.size();i++)
			{
				rec_brand.insert(vec_bi[i].brand);	
			}


			return rec_brand;

		}

		void recommend()
		{
			map< int,int > :: iterator it = userid_id.begin();
			while( it!=userid_id.end() )
			{
				user_brand_rec[it->first] = get_rec_brand_set_by_user(it->first);
				it++;
			}
		}//recommend

		void print()
		{
			//write recommendation result <user,brand> to file
			map< int,set<int> > :: iterator it = user_brand_rec.begin();
			while( it!=user_brand_rec.end() )
			{
				set<int> tmp = it->second;
				set<int> :: iterator it2 = tmp.begin();
				while(it2!=tmp.end())
				{
					fout<<it->first<<" "<<*it2<<endl;
					it2++;
				}
				it++;
			}

		}//print


		~UserCF()
		{
			userid_id.clear();
			user_brands.clear();
			id_brands.clear();
			brand_ids.clear();
			brand_all.clear();
			user_brand_rec.clear();
			fin.close();
			fout.close();
		}
      
}; 

int main(int argc , char *argv[])
{
    if(argc!=3)    
    {
        cout<<"Usage : ./a.out k m"<<endl;
        exit(1);               
    }
    int k = atoi(argv[1]) ;
    int m = atoi(argv[2]) ;
    
    UserCF ucf(k,m);
	ucf.get_sim_mat();
	ucf.recommend();
#if 1
    ucf.print();
#endif
    return 0;
}

cal_precision_recall.cc(計算準確率和召回率代碼):

#include<iostream>
#include<fstream>
#include<map>
#include<set>
#include<algorithm>

using namespace std;


int main()
{
	ifstream fin , fin1;
	fin.open("rec_result_by_m123.txt");
	fin1.open("user_brand_m4.txt");
	if(!fin||!fin1)
	{
		cout<<"can not open file"<<endl;
		exit(1);
	}
	
	map< int,set<int> > fore_user_brands; //recommendation result
	map< int,set<int> > real_user_brands; //real result

	int user , brand;
	while(fin>>user>>brand)
	{
		fore_user_brands[user].insert(brand);
	}
	while(fin1>>user>>brand)
	{
		real_user_brands[user].insert(brand);
	}

	double precision = 0 , recall = 0;
	double fore_total_brand = 0;
	double real_total_brand = 0;
	double intersection = 0;
	double F = 0;

	map< int,set<int> > :: iterator it = fore_user_brands.begin();
	map< int,set<int> > :: iterator itt = real_user_brands.begin();
	while(it!=fore_user_brands.end())
	{
		fore_total_brand += (it->second).size();
		it++;
	}
	while(itt!=real_user_brands.end())
	{
		real_total_brand += (itt->second).size();
		itt++;
	}

	it = fore_user_brands.begin();
	while(it!=fore_user_brands.end())
	{
		set<int> fore , real , newset;
		fore = it->second;
		real = real_user_brands[it->first];
		//set_intersection:get intersection of two sets
		set_intersection(fore.begin(),fore.end(),real.begin(),real.end(),inserter(newset,newset.begin()));
		intersection += newset.size();
		if(newset.size()!=0)
		{
			set<int> :: iterator itnew = newset.begin();
			cout<<"user : "<<it->first<<" brand : ";
			while(itnew!=newset.end())
			{
				cout<<*itnew<<" ";
				itnew++;
			}
			cout<<endl;
		}
		it++;
	}
	
	precision = intersection/fore_total_brand;
	recall = intersection/real_total_brand;
	F = (2*precision*recall)/(precision+recall);
	cout<<"fore_total_brand = "<<fore_total_brand<<endl;
	cout<<"real_total_brand = "<<real_total_brand<<endl;
	cout<<"intersection = "<<intersection<<endl;
	cout<<"precision = "<<precision<<endl;
	cout<<"recall = "<<recall<<endl;
	cout<<"F = "<<F<<endl;

	return 0;
}

makefile:

target:
		g++ UCF.cc 
		./a.out  5  5
		g++  cal_precision_recall.cc
		./a.out

clean:
		rm result.txt a.out

實驗結果:

k=5,m=5(k即對某個品牌最感興趣的前k個人。m即推薦給用戶的前m個品牌):

技術分享


技術分享
在改變k和m的值情況下得到的結果。可見F值並沒有太大的改變。


可見協同過濾這個算法對於這個問題背景並非非常適用,效果非常差。因此要依據不同的業務來選擇什麽樣的模型和算法。這個還須要不斷的學習。


源碼以及數據:http://pan.baidu.com/s/1c1VU1K


UCF臨時就介紹到這裏,當然這個算法還有非常多能夠改進的,比方在計算用戶相似度的時候能夠加上時間上的因素,還有懲處用戶u和用戶v共同興趣列表中熱門物品對他們相似度的影響等。


參考資料:

1.《推薦系統實戰》

2.www.cplusplus.com


Author:憶之獨秀

Email:[email protected]

轉載註明出處:http://blog.csdn.net/lavorange/article/details/22584373




【推薦系統實戰】:C++實現基於用戶的協同過濾(UserCollaborativeFilter)