最新实现了协同过滤的简单代码,正好知道有个用python写的开源项目,就下载下来对比学习一下。
Crab的官网: http://muricoca.github.io/crab/
一、安装
前一篇博文:
http://blog.csdn.net/joyce0625/article/details/42340831
二、使用
我打算先走通一条线,先以user-based recommender为例,item_based也是差不多的
三、结构
官网上也有个结构说明,我觉得我这个更清楚一些(同样以user-based 为例)
四、优化的地方
总的来说,和原来直接用简单的python写出来的协同过滤代码比较起来,Crab里面主要就是用了Numpy和Scipy,把原来很多的for循环都变成了直接的数组运算,提高了效率。
1. 使用Numpy的数组类,而不是dict
MatrixPreferenceDataModel/MatrixBooleanPrefDataModel 类把原来无序的字典转化成了一个规范的矩阵。
原来每位用户评论的item的顺序不同;或者说评论过某个item的用户顺序也不同,如图一所示,每次计算similarity都需要通过两个for循环去不同用户共同评论过的items或者评论过items的相同users;而现在
通过取出所有的user_id和item_id,并对他们分别sort(),再把rating值放到相应的位置,没有rating的地方就用NaN填充,变成了一个规范的矩阵,如图二所示。
图一:按字典方式存放的数据
图二:按矩阵方式存放的数据形式
2.最费时的相似度计算方法:
(1)计算前还是需要找common ratings,不再用两次for 循环来找,二是使用了
numpy.
intersect1d()
其中 intersectId的源码
def
intersect1d
(
ar1
,
ar2
,
assume_unique
=
False
):
if not assume_unique:
ar1 = unique(ar1)
ar2 = unique(ar2)
aux = np.concatenate((ar1, ar2))
aux.sort()
return aux[:-1][aux[1:] == aux[:-1]] #aux[:-1]到最后一个之前的内容
#这段找共同值的方法还是挺有技巧的,把要比较的两个list(A,B)数据先unique(),然后串联起来(C)排序,那些两个list里面都有的数就会被排在一起
#串联排序后的数据,做错位的与运算,就是一个切掉C的第一个,得到C', 一个切掉C的最后一个,得到C'',list C'和list C''做与运算,因为前面sort()后相同的数肯定是前后连在一起的,所以相同的数所在的位肯定有一个是True
(2) 相似度原来直接用* 来完成计算,现在
使用Scipy中 scipy.spatial.distance子模块中的.cdist(X, Y, 'euclidean'), .cdist(X, Y, 'sqeuclidean') 和cdist(X, Y, 'correlation', 2)方法来计算。
通过查看cdist的源码可以看到它又用了
dist
=
norm(u
-
v)
numpy.linalg.
norm
(
x
,
ord=None
,
axis=None
)来完成平方再开根号的计算;
计算euclidean:
如果只要平方和,可以用numpy.dot(), 比如计算correlation:
【PS: cdist里面有各种超赞的常用距离计算公式,比如cosine, euclidean(2维的),jaccard,matching(boolean vectors)等等】
3.data models(values and boolean) 和 similarity(item-based, user_based), recommender都是先写一个超类,再用子类继承的方式
4.recommender
(1)找到neighbors(score > minimal_similarity)评价过但是user自己还没有评价过的item
同样地,不需要用for循环,而直接采用nump.setdiff1d()方法就可以了
(2)找到top_match items
5.评估结果
(1) cross validation
*对于一般的KFold
利用了一个生成器,来进行划分
*对于shuffleSplit, 先打乱原始数据,然后再划分的方法(由于这种方法每次都会重排一下,并不能保证每次划分得到的训练集都是不同的)
采用的方法是
①用方法sklearn.utils.check_random_state(seed) 返回一个np.random.RandomState 实例
②使用RandomState.permutation(x) 返回一个打散的array,代码如下:
其实可以直接用包装好的方法sklearn.cross_validation.train_test_split(*arrays, **options),这样更简洁。
(2)metric
在计算之前用check_arrays(real, pred)检查下两个array的第一维是不是一样的(from ..utils import check_arrays, unique_labels)
五:改进原来的代码
其中我发现在_top_matches为user 预估preference的时候,并没有办法保证某个neighbor 的preference一定是乘以他和user的similarity,看我下面举的例子:
我把代码里
prefs = prefs[~np.isnan(prefs)]
similarities = similarities[~np.isnan(prefs)]
prefs_sim = np.sum(prefs[~np.isnan(similarities)] *
similarities[~np.isnan(similarities)])
total_similarity = np.sum(similarities)
改成了
temp_prefs = [~np.isnan(prefs)]
temp_similarities = [~np.isnan(similarities)]
noNaN_indices = np.logical_and(temp_prefs, temp_similarities)
prefs_sim = np.sum(prefs[noNaN_indices[0] == True] *
similarities[noNaN_indices[0] == True])
similarities = similarities[~np.isnan(similarities)]
total_similarity = np.sum(similarities)
还是同样的例子,就可以得到保证preference和similarity一定是一一对应的。
第二种得到指定indice的array的方法
不知道我理解的有没有错,已经把修改的内容提交给作者了,等待他的回复吧。