Clojure實現協同過濾原型

本文是學習clojure的過程當中寫的第一個小程序,功能很簡單,第一次使用clojure實現本身的想法,特此記錄。 數據文件以下:java

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)

結果以下:app

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返回。 結果以下:學習

原始數據:code

[[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}, ... },下面把它轉變爲這種形式:string

(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])
相關文章
相關標籤/搜索