Clojure实现协同过滤原型

本文是学习clojure的过程中写的第一个小程序,功能很简单,第一次使用clojure实现自己的想法,特此记录。
数据文件如下:

<!-- lang: shell -->
101,1
101,2
102,1
102,3
103,2
104,4
104,1
104,5

左列是item id,右列是user id
先简单的把文件读入内存,使用clojure.java.io/reader:

(use 'clojure.java.io)
;line-seq返回一个惰性序列,使用into强制把整个序列塞进vector中
(def data 
(with-open [rdr (reader "/tmp/test.txt")]
    (into [] (line-seq rdr))))
(println "data is: " data)

结果如下:

data is: [101,1 101,2 102,1 102,3 103,2 104,4 104,1 104,5]

每行是一个字符串,做为vector中的一个元素
下面考虑在读取每一行的时候进行解析:以逗号做为分隔符,切分为item和user两部分


(def data 
(with-open [rdr (reader "/tmp/test.txt")]
    (into [] (map 
            #(if % 
                (let [ [k v] (.split % ",") ] [k v])) 
            (line-seq rdr)))))
(println "原始数据:" data)

解析函数:用java.lang.split函数以逗号分隔字符串,再用let解析之,返回key、value组成的vector;
用map把解析函数做用于文件的每一行,把所有的解析结果放入vector返回。
结果如下:

原始数据: [[101 1] [101 2] [102 1] [102 3] [103 2] [104 4] [104 1] [104 5]]

再考虑按照item做group by,形成{item => {user1,user2}}的map,以便计算item之间的相似度

(def tmp_data (group-by #(first %) data))
(println "按第一个元素group by之后:" tmp_data)

按item group by,user做为value,tmp_data如下:

按第一个元素group by之后: {101 [[101 1] [101 2]], 102 [[102 1] [102 3]], 103 [[103 2]], 104 [[104 4] [104 

1] [104 5]]}
因为行成的map中values不是想要的结构,应该是{101 {1 2}, … },下面把它转变为这种形式:

(def result 
    (for [ [k values] tmp_data]
        [k (into #{} (map second values))]))
(println "把values转为列表:" result)

这里用for遍历tmp_data,把其中的values中的第二个元素,也就是user id,提取出来放到集合中,输出如下:

把values转为列表: ([101 #{1 2}] [102 #{1 3}] [103 #{2}] [104 #{1 4 5}])

这就是最终想要的结果了,不过它是一个list,item和user集合组成的vector做为元素,这个不影响item-cf的计算。
上述把文件中的行转为item-user集合的方式有点麻烦,而且也不是clojure的惯用法,下面使用apply 配合merge-with得到最终结果:

(def data 
(with-open [rdr (reader "/tmp/test.txt")]
    (into [] (map 
            #(if % 
                (let [ [k v] (.split % ",")] {(Integer/parseInt k) v})) 
            (line-seq rdr)))))

这一步主要是文件中的每一行读出来并解析出item和user,与上面不同的是,把item和user先放进map中,最终再整体放入vector中(顺带把item由string转成int,方便后面的计算)

(defn to-set [set]
    (if (set? set) 
        set 
        #{set}))
;也可以使用apply转变
(def result 
    (apply merge-with 
        #(union (to-set %1) (to-set %2)) 
        data))
(println "使用apply和merge-with:" result)

这里用到了apply 函数,它的使用方法参见clojure文档;
merge-with函数按key合并一组hashmap;
union合并两个集合;
自定义的to-set函数检查参数是不是集合,如果不是,则把它转为集合返回;如果是,则返回原值
输出如下:

使用apply和merge-with: {104 #{1 4 5}, 103 2, 102 #{1 3}, 101 #{1 2}}

最后计算item两两之间的相似度,先看相似度计算公式:

(defn sim [s1 s2]
    (/ 
    (count (clojure.set/intersection s1 s2)) 
    (Math/sqrt (* (count s1) (count s2)))))

clojure.set/intersection得到两个集合中共有的元素,计算相似度:

(def pair 
    (for [[k1 v1] result :when (set? v1)
          [k2 v2] result :when (and (< k1 k2) (set? v2))] 
        [k1 k2 (sim  v1 v2)]))
(println pair)

用for得到item列表的笛卡尔积(任意两个item的组合),由于101-102的计算结果与102-101的计算结果是一样的,这里为了减少重复计算,只在k1小于k2的时候计算相似度,最终结果:

([102 104 0.4082482904638631] [101 104 0.4082482904638631] [101 102 0.5])

你可能感兴趣的:(clojure,item-cf,clojure文件读取)