Query意圖分析:記一次完整的機器學習過程(scikit learn library學習筆記)


所謂學習問題,是指觀察由n個樣本組成的集合,並根據這些數據來預測未知數據的性質。

學習任務(一個二分類問題):

區分一個普通的互聯網檢索Query是否具有某個垂直領域的意圖。假設現在有一個O2O領域的垂直搜索引擎,專門為用戶提供團購、優惠券的檢索;同時存在一個通用的搜索引擎,比如百度,通用搜索引擎希望能夠識別出一個Query是否具有O2O檢索意圖,如果有則調用O2O垂直搜索引擎,獲取結果作為通用搜索引擎的結果補充。

我們的目的是學習出一個分類器(classifier),分類器可以理解為一個函數,其輸入為一個Query,輸出為0(表示該Query不具有o2o意圖)或1(表示該Query具有o2o意圖)。

特征提取

要完成這樣一個學習任務,首先我們必須找出決定一個Query是否具有O2O意圖的影響因素,這些影響因素稱之為特征(feature)。特征的好壞很大程度上決定了分類器的效果。在機器學習領域我們都知道特征比模型(學習算法)更重要。(順便說一下,工業界的人都是這么認為的,學術界的人可能不以為然,他們整天搗鼓算法,發出來的文章大部分都沒法在實際中應用。)舉個例子,如果我們的特征選得很好,可能我們用簡單的規則就能判斷出最終的結果,甚至不需要模型。比如,要判斷一個人是男還是女(人類當然很好判斷,一看就知道,這里我們假設由計算機來完成這個任務,計算機有很多傳感器(攝像頭、體重器等等)可以采集到各種數據),我們可以找到很多特征:身高、體重、皮膚顏色、頭發長度等等。因為根據統計我們知道男人一般比女人重,比女人高,皮膚比女人黑,頭發比女人短;所以這些特征都有一定的區分度,但是總有反例存在。我們用最好的算法可能准確率也達不到100%。假設計算機還能夠讀取人的身份證號碼,那么我們可能獲得一個更強的特征:身份證號碼的倒數第二位是否是偶數。根據身份證編碼規則,我們知道男性的身份證號碼的倒數第二位是奇數,女生是偶數。因此,有了這個特征其他的特征都不需要了,而且我們的分類器也很簡單,不需要復雜的算法。

言歸正傳,對於O2O Query意圖識別這一學習任務,我們可以用的特征可能有:Query在垂直引擎里能夠檢索到的結果數量、Query在垂直引擎里能夠檢索到的結果的類目困惑度(perplexity)(檢索結果的類目越集中說明其意圖越強)、Query能否預測到特征的O2O商品類目、Query是否包含O2O產品詞或品牌詞、Query在垂直引擎的歷史展現次數(PV)和點擊率(ctr)、Query在垂直引擎的檢索結果相關性等等。

特征表示

特征表示是對特征提取結果的再加工,目的是增強特征的表示能力,防止模型(分類器)過於復雜和學習困難。比如對連續的特征值進行離散化,就是一種常用的方法。這里我們以“Query在垂直引擎里能夠檢索到的結果數量”這一特征為例,簡要介紹一下特征值分段的過程。首先,分析一下這一維特征的分布情況,我們對這一維特征值的最小值、最大值、平均值、方差、中位數、三分位數、四分位數、某些特定值(比如零值)所占比例等等都要有一個大致的了解。獲取這些值,python編程語言的numpy模塊有很多現成的函數可以調用。最好的辦法就是可視化,借助python的matplotlib工具我們可以很容易地划出數據分布的直方圖,從而判斷出我們應該對特征值划多少個區間,每個區間的范圍是怎樣的。比如說我們要對“結果數量”這一維特征值除了“0”以為的其他值均勻地分為10個區間,即每個區間內的樣本數大致相同。“0”是一個特殊的值,因此我們想把它分到一個單獨的區間,這樣我們一共有11個區間。python代碼實現如下:

import numpy as np

def bin(bins):
    assert isinstance(bins, (list, tuple))
    def scatter(x):
        if x == 0: return 0
        for i in range(len(bins)):
            if x <= bins[i]: return i + 1
        return len(bins)
    return np.frompyfunc(scatter, 1, 1)

data = np.loadtxt("D:\query_features.xls", dtype='int')
# descrete
o2o_result_num = data[:,0]
o2o_has_result = o2o_result_num[o2o_result_num > 0]
bins = [ np.percentile(o2o_has_result, x) for x in range(10, 101, 10) ]
data[:,0] = bin(bins)(o2o_result_num)

我們首先獲取每個區間的起止范圍,即分別算法特征向量的10個百分位數,並依此為基礎算出新的特征值(通過bin函數,一個numpy的universal function)。

訓練數據

這里我們通過有監督學習的方法來擬合分類器模型。所謂有監督學習是指通過提供一批帶有標注(學習的目標)的數據(稱之為訓練樣本),學習器通過分析數據的規律嘗試擬合出這些數據和學習目標間的函數,使得定義在訓練集上的總體誤差盡可能的小,從而利用學得的函數來預測未知數據的學習方法。注意這不是一個嚴格的定義,而是我根據自己的理解簡化出來的。

一批帶有標注的訓練數據從何而來,一般而言都需要人工標注。我們從搜索引擎的日志里隨機采集一批Query,並且保證這批Query能夠覆蓋到每維特征的每個取值(從這里也可以看出為什么要做特征分區間或離散化了,因為如不這樣做我們就不能保證能夠覆蓋到每維特征的每個取值)。然后,通過人肉的方法給這邊Query打上是否具有O2O意圖的標簽。數據標注是一個痛苦而漫長的過程,需要具有一定領域知識的人來干這樣的活。標注質量的好壞很有可能會影響到學習到的模型(這里指分類器)在未知Query上判別效果的好壞。即正確的老師更可能教出正確的學生,反之,錯誤的老師教壞學生的可能性越大。在我自己標注數據的過程中,發現有一些Query的O2O意圖比較模棱兩可,導致我后來回頭看的時候總覺得自己標得不對,反反復復修改了好幾次。

選擇模型

在我們的問題中,模型就是要學習的分類器。有監督學習的分類器有很多,比如決策樹、隨機森林、邏輯回歸、梯度提升、SVM等等。如何為我們的分類問題選擇合適的機器學習算法呢?當然,如果我們真正關心准確率,那么最佳方法是測試各種不同的算法(同時還要確保對每個算法測試不同參數),然后通過交叉驗證選擇最好的一個。但是,如果你只是為你的問題尋找一個“足夠好”的算法,或者一個起點,也是有一些還不錯的一般准則的,比如如果訓練集很小,那么高偏差/低方差分類器(如朴素貝葉斯分類器)要優於低偏差/高方差分類器(如k近鄰分類器),因為后者容易過擬合。然而,隨着訓練集的增大,低偏差/高方差分類器將開始勝出(它們具有較低的漸近誤差),因為高偏差分類器不足以提供准確的模型。

這里我們重點介紹一次完整的機器學習全過程,所以不花大篇幅在模型選擇的問題上,推薦大家讀一些這篇文章:《如何選擇機器學習分類器?》。

通過交叉驗證擬合模型

機器學習會學習數據集的某些屬性,並運用於新數據。這就是為什么習慣上會把數據分為兩個集合,由此來評價算法的優劣。這兩個集合,一個叫做訓練集(train data),我們從中獲得數據的性質;一個叫做測試集(test data),我們在此測試這些性質,即模型的准確率。將一個算法作用於一個原始數據,我們不可能只做出隨機的划分一次train和test data,然后得到一個准確率,就作為衡量這個算法好壞的標准。因為這樣存在偶然性。我們必須好多次的隨機的划分train data和test data,分別在其上面算出各自的准確率。這樣就有一組准確率數據,根據這一組數據,就可以較好的准確的衡量算法的好壞。交叉驗證就是一種在數據量有限的情況下的非常好evaluate performance的方法。

 1 from sklearn import cross_validation
 2 from sklearn import tree
 3 from sklearn import ensemble
 4 from sklearn import linear_model
 5 from sklearn import svm
 6 
 7 lr = linear_model.LogisticRegression()
 8 lr_scores = cross_validation.cross_val_score(lr, train_data, train_target, cv=5)
 9 print("logistic regression accuracy:")
10 print(lr_scores)
11 
12 clf = tree.DecisionTreeClassifier(criterion='entropy', max_depth=8, min_samples_split=5)
13 clf_scores = cross_validation.cross_val_score(clf, train_data, train_target, cv=5)
14 print("decision tree accuracy:")
15 print(clf_scores)
16 
17 rfc = ensemble.RandomForestClassifier(criterion='entropy', n_estimators=3, max_features=0.5, min_samples_split=5)
18 rfc_scores = cross_validation.cross_val_score(rfc, train_data, train_target, cv=5)
19 print("random forest accuracy:")
20 print(rfc_scores)
21 
22 etc = ensemble.ExtraTreesClassifier(criterion='entropy', n_estimators=3, max_features=0.6, min_samples_split=5)
23 etc_scores = cross_validation.cross_val_score(etc, train_data, train_target, cv=5)
24 print("extra trees accuracy:")
25 print(etc_scores)
26 
27 gbc = ensemble.GradientBoostingClassifier()
28 gbc_scores = cross_validation.cross_val_score(gbc, train_data, train_target, cv=5)
29 print("gradient boosting accuracy:")
30 print(gbc_scores)
31 
32 svc = svm.SVC()
33 svc_scores = cross_validation.cross_val_score(svc, train_data, train_target, cv=5)
34 print("svm classifier accuracy:")
35 print(svc_scores)

上面的代碼我們嘗試同交叉驗證的方法對比五種不同模型的准確率,結果如下:

 1 logistic regression accuracy:
 2 [ 0.76953125  0.83921569  0.85433071  0.81102362  0.83858268]
 3 decision tree accuracy:
 4 [ 0.73828125  0.8         0.77559055  0.71653543  0.83464567]
 5 random forest accuracy:
 6 [ 0.75        0.76862745  0.76377953  0.77165354  0.80314961]
 7 extra trees accuracy:
 8 [ 0.734375    0.78039216  0.7992126   0.76377953  0.79527559]
 9 gradient boosting accuracy:
10 [ 0.7578125   0.81960784  0.83464567  0.80708661  0.84251969]
11 svm classifier accuracy:
12 [ 0.703125    0.78431373  0.77952756  0.77952756  0.80708661]

在O2O意圖識別這個學習問題上,邏輯回歸分類器具有最好的准確率,其次是梯度提升分類器;決策樹和隨機森林在我們的測試結果中並沒有體現出明顯的差異,可能是我們的特殊數量太少並且樣本數也較少的原因;另外大名典典的SVM的表現卻比較讓人失望。總體而言,准確率只有82%左右,分析其原因,一方面我們實現的特征數量較少;另一方面暫時未能實現區分能力強的特征。后續會對此持續優化。

由於邏輯回歸分類器具有最好的性能,我們決定用全部是可能訓練數據來擬合之。

lr = lr.fit(train_data, train_target)

模型數據持久化

學到的模型要能夠在將來利用起來,就必須把模型保存下來,以便下次使用。同時,數據離散化或數據分區的范圍數據也要保存下來,在預測的時候同樣也需要對特征進行區間划分。python提供了pickle模塊用來序列號對象,並保存到硬盤上。同時,scikit-learn庫也提供了更加高效的模型持久化模塊,可以直接使用。

1 from sklearn.externals import joblib
2 joblib.dump(lr, 'D:\lr.model')
3 import pickle
4 bin_file = open(r'D:\result_bin.data', 'wb')
5 pickle.dump(bins, bin_file)
6 bin_file.close()

分類器的使用

現在大功告成了,我們需要做的就是用學習到的分類器來判斷一個新的Query到底是否具有O2O意圖。因為我們分類器的輸入是Query的特征向量,而不是Query本身,因此我們需要實現提取好Query的特征。假設我們已經離線算好了每個Query的特征,現在使用的時候只需要將其加載進內場即可。分類器使用的過程首先是從硬盤讀取模型數據和Query特征,然后調用模型對Query進行預測,輸出結果。

 1 # load result bin data and model
 2 bin_file = open(r'D:\result_bin.data', 'rb')
 3 bins = pickle.load(bin_file)
 4 bin_file.close()
 5 
 6 lr = joblib.load('D:\lr.model')
 7 
 8 # load data
 9 query = np.genfromtxt(r'D:\o2o_query_rec\all_query', dtype='U2', comments=None, converters={0: lambda x: str(x, 'utf-8')})
10 feature = np.loadtxt(r'D:\o2o_query_rec\all_features', dtype='int', delimiter='\001')
11 
12 # descrite
13 feature[:,0] = bin(bins)(feature[:,0])
14 feature[:,1] = ufunc_segment(feature[:,1])
15 
16 # predict
17 result = lr.predict(feature)
18 
19 # save result
20 #np.savetxt(r'D:\o2o_query_rec\classify_result.txt', np.c_[query, result], fmt=u"%s", delimiter="\t")
21 result_file = open(r'D:\o2o_query_rec\classify_result.txt', 'w')
22 i = 0
23 for q in query:
24     result_file.write('%s\t%d\n' % (q, result[i]))
25     i += 1
26 result_file.close()

需要注意的是我們Query的編碼是UTF-8,load的時候需要做相應的轉換。另外,在python 3.3版本,numpy的savetxt函數並不能正確保持UTF-8格式的中文Query(第20行注釋掉的代碼輸出的Query都變成了bytes格式的),如果小伙伴們有更好的辦法能夠解決這個問題,請告訴我,謝謝!

 

統一回復下: 文章所用數據下載地址:https://pan.baidu.com/s/15vwyXX2VBul635nW_Axm0g

代碼都在文章中,大家負責粘貼就可以了,多謝~

歡迎關注我的個人博客:https://yangxudong.github.io


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM