運用kNN算法識別潛在續費商家
背景與目標
Youzan 是一家SAAS公司,服務於數百萬商家,幫助互聯網時代的生意人私有化顧客資產、拓展互聯網客群、提高經營效率。現在,該公司希望能夠從商家的交易數據中,挖掘出有強烈續費傾向的商家,並提供更優質更有針對性的服務。
目標: 從商家交易數據中識別有強烈續費傾向的商家。
思路與建模
kNN是一種思路簡單清晰的有點近似蠻力的機器學習算法。它將待分類數據的特征值集與已分類數據集的每個樣本的特征值集進行比較,計算出距離值,然後根據距離最小原則,選擇k個距離最小的已分類實例,從這k個已分類實例中選擇分類概率最大(出現次數最多)的那個。
從商家交易數據中識別有強烈續費傾向的商家,首先進行建模:
商家的強烈續費意向與哪些交易數據密切關聯呢?這裏,作出如下假設: 商家的強烈續費意向與“日均成交訂單數”、“日均實際成交金額”、“日均累計粉絲數” 三個特征值關聯。 下面,就說明運用kNN算法識別潛在續費商家的步驟。 完整源代碼見附錄。
本文的源代碼來自《機器學習實戰》 第二章。實現用到了矩陣計算,在代碼講解中會細說。
步驟與實現
訓練樣本集
首先需要準備一個訓練樣本集,裏面含有每個商家的“日均成交訂單數”、“日均實際成交金額”、“日均累計粉絲數”、“是否已續費”。為方便起見,可以準備一個原始的數據文件 origin.txt ,然後通過隨機值增加的方式(見源代碼中的 createMoreSamplesFrom(filename, n)) 生成更大的實際樣本集 sample.txt。 這裏數據都是偽造的。
12 789 11 0
1089 1200000 134 1
10 9800000 789 1
244 98700 256 0
1234 987653 900 0
40000 50000000 14000 1
30000 600000 120000 1
2500 4700000 9000 1
3 278 55 0
5890 2457788 130000 1
702 89032 890 0
12456 87200012 75000 1
125 90321133 45001 1
600 300020 120000 1
1456 90224441 900000 1
456 12456 2356 0
8 1204 236 0
129000 135700000 8000000 1
數據歸一化
可以看到,日均實際成交金額的值非常大,因此特征值“日均實際成交金額”的距離會起決定性的作用,弱化其他特征值的影響。可以采用數據歸一化,使得特征值的絕對值影響弱化。 見函數 autoNorm(dataset)。
對於一個序列 x = [x1, x2, ..., xN] 來說, 歸一化數值 xi = (xi - min(x)) / (max(x) - min(x)) 。
矩陣歸一化的計算邏輯如下: 假設 x = [[16,4,9], [3,15,27]] ,
step1: 計算每列的最小值 min(x) = [3,4,9] 以及最大值 max(x) = [16,15,27]
step2: 計算最大值與最小值之間的距離:ranges = max(x) - min(x) = [13,11,18]
step3: 計算每行的歸一化數值為 ([16,4,9] - [3,4,9]) / [13,11,18] = [1,0,0] , ( [3, 15, 27] - [3,4,9] ) / [13,11,18] = [0, 1, 1] ;這裏都是逐個值對應計算。
step4: 得到原矩陣 x 的歸一化數值為 [[1,0,0], [0,1,1]]
def autoNorm(dataset):
minVals = dataset.min(0)
maxVals = dataset.max(0)
ranges = maxVals - minVals
normDataset = zeros(shape(dataset))
rows = dataset.shape[0]
normDataset = dataset - tile(minVals, (rows,1))
normDataset = normDataset / tile(ranges, (rows,1))
return normDataset, ranges, minVals
可視化數據
可以通過圖形初步展示數據的形狀, 見函數 draw(dataset)
計算距離向量
得到歸一化數據後,可以計算輸入向量與每一個樣本實例的距離,得到距離向量。見函數 computeDistance(inX, dataset) 。 這裏 inX 是輸入向量,dataset 是訓練樣本矩陣。
這裏為了計算 inX 與每一個樣本實例的距離值,需要將 inX 在行方向上平鋪N次,N 是 dataset 的總行數,因此使用 tile(inX, (dataset.shape[0],1)); 然後進行矩陣減法和平方,再將每行的平方和進行求根,得到輸入向量與每個樣本實例的距離。
比如 inX = [0.1,0.5,0.8] , dataset = [[1,0,0], [0,1,1]] , 距離向量為 D; D.rows(1) = ((-0.9)^2 + (0.5)^2 + (0.8) ^2)^(0.5) = 1.3038 ; D.rows(2) = (0.01 + 0.25 + 0.04)^(0.5) = 0.5477 ; D = [1.3038, 0.5477]
def computeDistance(inX, dataset):
datasetrows = dataset.shape[0]
diffMat = tile(inX, (datasetrows, 1)) - dataset
sqDiffMat = diffMat ** 2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances ** 0.5
return distances
分類算法
得到距離向量後,就可以進行分類了。先對距離向量從小到大進行排序,然後取前k個樣本實例的分類值 class[k],接著計算這k個分類值中哪個分類值的出現概率最大。 見 函數 classify(inX, dataset, labels, k) , 這裏 labels 是樣本集的分類標簽向量,k 是算法的設置變量,取前k個距離最近的樣本的分類標簽。
distances = computeDistance(inX, dataset)
sortedDistIndicies = distances.argsort()
classCount = {}
for i in range(k):
voteLabel = labels[sortedDistIndicies[i]]
classCount[voteLabel] = classCount.get(voteLabel, 0) + 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
分類算法的錯誤率
評價分類算法的錯誤率是非常重要的。見函數 computeErrorRatio。
將已含分類標簽值的樣本集實例TotalSample分為兩類,一個是用於計算距離向量的樣本集 Sample,一個用於測試分類錯誤率的輸入向量集合 inMatrix。一般 inMatrix 占總TotalSample的 10%,可以作為算法設置 ratio 。 對於 inMatrix 的每個輸入向量,得到其分類標簽值,然後與其已有的分類值比較,得到正確或錯誤的分類結果。分類算法的錯誤率 = 分類錯誤的結果數 / inMatrix 的總數。
對未知數據分類
假設這個分類算法的錯誤率在接受範圍內,那麽就可以使用這個分類算法對未知數據分類了。註意,未知數據也要進行歸一化。 見函數 classifyInstance(dataMatrix,classLabelVector, ranges, minVals)。
從其他數據文件讀取待分類的數據,每行一個, 轉換成輸入向量 inX ,然後使用 classify 函數進行分類即可得到其分類值。
至此,kNN 算法的基本步驟和實現就完成了。
完整源代碼
# -*- coding: utf-8 -*-
# -------------------------------------------------------------------------------
# Name: potiential_users.py
# Purpose: recognise potiential renewal users using kNN algorithm
#
# Author: qin.shuq
#
# Created: 2018/03/10
#-------------------------------------------------------------------------------
#!/usr/bin/env python
import random
import matplotlib
import matplotlib.pyplot as plot
from numpy import *
import operator
indicatorNumber = 3
k = 5
def createMoreSamplesFrom(filename, n):
datar = open(filename)
lines = datar.readlines()
datar.close()
lineNum = len(lines)
totalLines = lineNum * n
dataw = open('sample.txt', 'w')
for i in range(totalLines):
data = map (lambda x: int(x), lines[random.randint(0,lineNum)].strip().split())
(orderNumber, actualDeal, fans, classifierResult) = (data[0] + random.randint(0, 500), data[1] + random.randint(0, 500000), data[2] + random.randint(0, 100000), data[3])
dataw.write('%d %d %d %d\n' % (orderNumber, actualDeal, fans, classifierResult))
dataw.close()
def file2matrix(filename):
dataf = open(filename)
lines = dataf.readlines()
dataf.close()
numOfLines = len(lines)
dataMatrix = zeros((numOfLines, indicatorNumber))
classLabelVector = []
index = 0
for line in lines:
line = line.strip()
listFromLine = line.split()
dataMatrix[index, :] = listFromLine[0:indicatorNumber]
classLabelVector.append(int(listFromLine[-1]))
index += 1
return (dataMatrix, classLabelVector)
def autoNorm(dataset):
minVals = dataset.min(0)
maxVals = dataset.max(0)
ranges = maxVals - minVals
normDataset = zeros(shape(dataset))
rows = dataset.shape[0]
normDataset = dataset - tile(minVals, (rows,1))
normDataset = normDataset / tile(ranges, (rows,1))
return normDataset, ranges, minVals
def computeDistance(inX, dataset):
datasetrows = dataset.shape[0]
diffMat = tile(inX, (datasetrows, 1)) - dataset
sqDiffMat = diffMat ** 2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances ** 0.5
return distances
def classify(inX, dataset, labels, k):
distances = computeDistance(inX, dataset)
sortedDistIndicies = distances.argsort()
classCount = {}
for i in range(k):
voteLabel = labels[sortedDistIndicies[i]]
classCount[voteLabel] = classCount.get(voteLabel, 0) + 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
def draw(data):
fig = plot.figure()
ax = fig.add_subplot(111)
ax.scatter(data[:, 0], data[:,1])
#ax.scatter(data[:, 0], data[:,1], 20.0*array(classLabelVector), 20.0*array(classLabelVector))
plot.title('potiential renewal users figure')
plot.xlabel('daily order number')
plot.ylabel('daily actual deal')
#plot.show()
def computeErrorRatio(dataMatrix, classLabelVector):
testRatio = 0.10
totalRows = dataMatrix.shape[0]
testRows = int(totalRows*testRatio)
errorCount = 0
for i in range(testRows):
classifierResult = classify(dataMatrix[i,:], dataMatrix[testRows:totalRows, :], classLabelVector[testRows:totalRows], k)
print 'classify result: %d, the real result is %d' % (classifierResult, classLabelVector[i])
if classifierResult != classLabelVector[i]:
errorCount += 1.0
print 'total error rate is %f' % (errorCount / float(testRows))
def classifyInstance(dataMatrix,classLabelVector, ranges, minVals):
dataf = open('data.txt')
for line in dataf:
line = line.strip()
(kid, orderNumber, actualDeal, fans) = map(lambda x: int(x), line.split())
input = array([orderNumber, actualDeal, fans])
classifierResult = classify((input-minVals)/ranges, dataMatrix, classLabelVector, k)
print '%d [orderNumber=%d actualDeal=%d fans=%d] is %s potiential renewal user' % (kid, orderNumber, actualDeal, fans, "not" if classifierResult != 1 else "" )
def test():
x = array([[16,4,9], [3,15,27]])
xNorm = autoNorm(x)
print 'x: ', x , 'norm(x): ', xNorm
inX = array([0.1,0.5,0.8])
print 'distances: ', computeDistance(inX, xNorm[0])
if __name__ == '__main__':
test()
createMoreSamplesFrom('origin.txt', 10)
(dataMatrix,classLabelVector) = file2matrix('sample.txt')
(dataMatrix,ranges, minVals) = autoNorm(dataMatrix)
print 'dataset: ', (dataMatrix,classLabelVector)
draw(dataMatrix)
computeErrorRatio(dataMatrix, classLabelVector)
classifyInstance(dataMatrix,classLabelVector, ranges, minVals)
矩陣函數
- array([[...]]): 將Python多維數組轉化為相應的N維矩陣從而可以進行計算;
- shape: 得到該矩陣的維度屬性,是一個元組; m.shape[0],行數 ; m.shape[1] , 列數 ;
- min(n): 得到該矩陣指定維度的最小值: m.min(0),每列的最小值 ; m.min(1), 每行的最小值;n 不大於 shape() 的長度;
- max(n): 得到該矩陣指定維度的最大值: m.max(0), 每列的最大值;m.max(0),每行的最大值 n 不大於 shape() 的長度;
- tile: 矩陣平鋪,從指定的行方向/列方向將原矩陣進行平鋪得到新的矩陣;
- argsort: 將向量從小到大排序,排序後的每個元素在原向量中的索引值形成一個新的索引向量。
>>> import numpy as np
>>> x = np.array([[16,4,9], [3,15,27]])
>>> x
array([[16, 4, 9],
[ 3, 15, 27]])
>>> x.shape # 得到 x 的維度信息
(2, 3)
>>> x.shape[0] # 總行數
2
>>> x.shape[1] # 總列數
3
>>> x.min(0) # 列方向的最小值
array([3, 4, 9])
>>> x.min(1) # 行方向的最小值
array([4, 3])
>>> x.max(0) # 列方向的最大值
array([16, 15, 27])
>>> x.max(1) # 行方向的最大值
array([16, 27])
>>>
>>>
>>> np.tile(x, (2,1)) # 將 x 在列方向平鋪2次
array([[16, 4, 9],
[ 3, 15, 27],
[16, 4, 9],
[ 3, 15, 27]])
>>> np.tile(x,(1,2)) # 將 x 在行方向平鋪2次
array([[16, 4, 9, 16, 4, 9],
[ 3, 15, 27, 3, 15, 27]])
>>>
>>> np.tile(x, (2,2)) # 將 x 在行和列方向分別平鋪2次
array([[16, 4, 9, 16, 4, 9],
[ 3, 15, 27, 3, 15, 27],
[16, 4, 9, 16, 4, 9],
[ 3, 15, 27, 3, 15, 27]])
>>> x=np.array([1,4,3,-1,6,9])
>>> x.argsort()
array([3, 0, 2, 1, 4, 5]) # -1 是最小值,在原向量中 index = 3
優化方向
可以從“可擴展性”和“性能”來優化kNN算法的代碼實現。
- 可擴展性:在模型中增加更多的因素(比如下單/付款轉化率,服務滿意度等),可以不改變代碼而依然執行。
- 性能: 通過並發或分布式算法,能夠在多臺機器上運行 kNN 算法,實現對大規模數據樣本集的kNN計算。
小結
本文講解了如何使用kNN算法來實現識別潛在續費商家。kNN算法依賴於模型的正確性。如果分類標簽值與樣本特征值有非常密切的關聯,使用簡單的kNN算法即可得到有效的結果,而且不限於特定的應用領域。只要能夠將領域問題轉化為樣本特征值矩陣,就能使用 kNN 算法來進行求解。
這也是我學習的第一個機器學習算法 :)
運用kNN算法識別潛在續費商家