1. 程式人生 > >第二篇:使用Spark對MovieLens的特征進行提取

第二篇:使用Spark對MovieLens的特征進行提取

src 參考 創建過程 程序 單單 關於 font 我們 eve

前言

在對數據進行了初步探索後,想必讀者對MovieLens數據集有了感性認識。而在數據挖掘/推薦引擎運行前,往往需要對數據預處理。預處理的重要性不言而喻,甚至比數據挖掘/推薦系統本身還重要。

然而完整的數據預處理工作會涉及到:缺失值,異常值,口徑統一,去重,特征提取等等等等,可以單寫一本書了,本文無法一一介紹。

本文僅就特征提取這一話題進行粗略討論並展示。

類別特征提取

在很多場景下,數據集的很多特征是類型變量,比如MovieLens裏面的職業類型。這樣的變量無法作為很多算法的輸入,因為這類變量無法作用於樣本間距離的計算。

可參考的方法是 1 of k 編碼,就是將某種類型的特征打平,將其轉化為具有n列的向量。具體的做法是先為特征列創建字典,然後將各具體特征值映射到 1 of k 編碼。

下面以MoveiLens中的職業類型特征為例,演示特征值為programmer的特征提取:

 1 # 載入數據集
 2 user_data = sc.textFile("/home/kylin/ml-100k/u.user")
 3 # 以‘ | ‘切分每列,返回新的用戶RDD
 4 user_fields = user_data.map(lambda line: line.split("|"))
 5 # 獲取職業RDD並落地
 6 all_occupations = user_fields.map(lambda fields: fields[3]).distinct().collect()
7 # 對各職業進行排序 8 all_occupations.sort() 9 10 # 構建字典 11 idx = 0 12 all_occupations_dict = {} 13 for o in all_occupations: 14 all_occupations_dict[o] = idx 15 idx +=1 16 17 # 生成並打印職業為程序員(programmer)的1 of k編碼 18 K = len(all_occupations_dict) 19 binary_x = np.zeros(K) 20 k_programmer = all_occupations_dict[
programmer] 21 binary_x[k_programmer] = 1 22 print "程序員的1 of k編碼為: %s" % binary_x

結果為:

技術分享

派生特征提取

並非所有的特征均可直接拿來學習。比如電影發行日期特征,它顯然無法拿來進行學習。但正如上一節所做的一個工作,將它轉化為電影年齡,這就可以在很多場景下進行學習了。

再比如時間戳屬性,可參考將他們轉為為:早/中/晚這樣的分類變量:

 1 # 載入數據集
 2 rating_data_raw = sc.textFile("/home/kylin/ml-100k/u.data")
 3 # 獲取評分RDD
 4 rating_data = rating_data_raw.map(lambda line: line.split("\t"))
 5 ratings = rating_data.map(lambda fields: int(fields[2]))
 6  
 7 # 函數: 將時間戳格式轉換為datetime格式
 8 def extract_datetime(ts):
 9     import datetime
10     return datetime.datetime.fromtimestamp(ts)
11  
12 # 獲取小時RDD
13 timestamps = rating_data.map(lambda fields: int(fields[3]))
14 hour_of_day = timestamps.map(lambda ts: extract_datetime(ts).hour)
15  
16 # 函數: 將小時映射為分類變量並展示
17 def assign_tod(hr):
18     times_of_day = {
19                 morning : range(7, 12),
20                 lunch : range(12, 14),
21                 afternoon : range(14, 18),
22                 evening : range(18, 23),
23                 night : range(23, 7)
24                 }
25     for k, v in times_of_day.iteritems():
26         if hr in v: 
27             return k
28  
29 # 獲取新的分類變量RDD
30 time_of_day = hour_of_day.map(lambda hr: assign_tod(hr))
31 time_of_day.take(5)

結果為:

技術分享

若要使用這個特征,大部分機器學習算法可以考慮將其1 of k編碼。部分支持分類型變量的算法除外。

PS:有兩個None是因為代碼中night:range(23,7)這麽寫是不對的。算了不糾結,意思懂就好 :)

文本特征提取

關於文本特征提取方法有很多,本文僅介紹一個簡單而又經典的提取方法 - 詞袋法。
其基本步驟如下:

1. 分詞 - 將文本分割為由詞組成的集合。可根據空格符,標點進行分割;
2. 刪除停用詞 - the and 這類詞無學習的價值意義,刪除之;
3. 提取詞幹 - 將各個詞轉化為其基本形式,如men -> man;
4. 向量化 - 從根本上來說和1 of k相同。不過由於詞往往很多,所以稀疏矩陣技術很重要;

下面將MovieLens數據集中的影片標題進行特征提取:

 1 # 載入數據集
 2 movie_data = sc.textFile("/home/kylin/ml-100k/u.item")
 3 # 以‘ | ‘切分每列,返回影片RDD
 4 movie_fields = movie_data.map(lambda lines: lines.split("|"))
 5  
 6 # 函數: 剔除掉標題中的(年份)部分
 7 def extract_title(raw):
 8     import re
 9     grps = re.search("\((\w+)\)", raw)
10     if grps:
11         return raw[:grps.start()].strip()
12     else:
13         return raw
14  
15 # 獲取影片名RDD
16 raw_titles = movie_fields.map(lambda fields: fields[1])
17  
18 # 剔除影片名中的(年份)
19 movie_titles = raw_titles.map(lambda m: extract_title(m))
20  
21 # 由於僅僅是個展示的例子,簡簡單單用空格分割
22 title_terms = movie_titles.map(lambda t: t.split(" "))
23  
24 # 搜集所有的詞
25 all_terms = title_terms.flatMap(lambda x: x).distinct().collect()
26 # 創建字典
27 idx = 0
28 all_terms_dict = {}
29 for term in all_terms:
30     all_terms_dict[term] = idx
31     idx +=1
32 num_terms = len(all_terms_dict)
33  
34 # 函數: 采用稀疏向量格式保存編碼後的特征並返回
35 def create_vector(terms, term_dict):
36     from scipy import sparse as sp
37     x = sp.csc_matrix((1, num_terms))
38     for t in terms:
39         if t in term_dict:
40             idx = term_dict[t]
41             x[0, idx] = 1
42     return x
43  
44 # 將字典保存為廣播數據格式類型。因為各個worker都要用
45 all_terms_bcast = sc.broadcast(all_terms_dict)
46 # 采用稀疏矩陣格式保存影片名特征
47 term_vectors = title_terms.map(lambda terms: create_vector(terms, all_terms_bcast.value))
48 # 展示提取結果
49 term_vectors.take(5)

其中,字典的創建過程也可以使用Spark提供的便捷函數zipWithIndex,這個函數可以將原RDD中的值作為主鍵,而新的值為主鍵在原RDD中的位置:

1 all_terms_dict2 = title_terms.flatMap(lambda x: x).distinct().zipWithIndex().collectAsMap()

collectAsMap則是將結果落地為Python的dict格式。

結果為:

技術分享

正則化特征

正則化,通常也叫做歸一化。最經典的做法就是所有特征值-最小值/特征區間。但對於一般特征的歸一化網上很多介紹,請讀者自行學習。本文僅對特征向量的正則化做介紹。

一般來說,我們是先計算向量的二階範數,然後讓向量的所有元素去除以這個範數。

下面演示對某隨機向量進行正則化:

 1 # 設置隨機數種子
 2 np.random.seed(42)
 3 # 生成隨機向量
 4 x = np.random.randn(10)
 5 # 產生二階範數
 6 norm_x_2 = np.linalg.norm(x)
 7 # 正則化
 8 normalized_x = x / norm_x_2
 9  
10 # 結果展示
11 print "向量x:\n%s" % x
12 print "向量x的2階範數: %2.4f" % norm_x_2
13 print "歸一化後的向量x:\n%s" % normalized_x
14 print "歸一化後向量x的2階範數:\n%2.4f" % np.linalg.norm(normalized_x)

結果為:

技術分享

Spark的MLlib庫提供了專門的正則化函數,它們執行起來的效率顯然遠遠高於我們自己寫的:

 1 # 導入Spark庫中的正則化類
 2 from pyspark.mllib.feature import Normalizer
 3 # 初始化正則化對象
 4 normalizer = Normalizer()
 5 # 創建測試向量(RDD)
 6 vector = sc.parallelize([x])
 7 # 對向量進行正則化並返回結果
 8 normalized_x_mllib = normalizer.transform(vector).first().toArray()
 9   
10 # 結果展示
11 print "向量x:\n%s" % x
12 print "向量x的二階範數: %2.4f" % norm_x_2
13 print "被MLlib正則化後的向量x:\n%s" % normalized_x_mllib
14 print "被MLlib正則化後的向量x的二階範數: %2.4f" % np.linalg.norm(normalized_x_mllib)

結果為:

技術分享

可以對比下前面自己實現的,結果一致。

第二篇:使用Spark對MovieLens的特征進行提取