前言
在對資料進行了初步探索後,想必讀者對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)
結果請讀者自行對比。