. 線性分類:核方法
- 1.數據封裝:記得以前讀weka源碼的時候,它將樣本封裝到一個叫
Instance
的對象里面,整個數據集叫Instances
里面存放的是單個樣本instance,封裝的好處是方便后期對樣本的處理,這里將每一個樣本封裝為一個對象包含data
和target
,記做Data
。
class Data:
def __init__(self,row):
self.data = map(float,row[:-1])
self.target = int(row[-1])
將整個數據集放入一個列表中
rows = []
rows.append(Data(line.split(","))
- 2.基本的線性分類器:原理是尋找每個類別所有數據的平均值,得到一個代表該類別的中心點,有新數據要對其進行分類時,只需要通過判斷距離哪個中心點位置近進行分類。
def train(rows):
average = {}#用來存放不同類別的中心點
counts = {}
for row in rows:
c1 = row.target
average.setdefault(c1,[0.0]*len(row.data))#定義一個長度與特征維度相等的list,
counts.setdefault(c1,0)
for i in range(len(row.data)):
average[c1][i] += float(row.data[i])
counts[c1] += 1
for c,avg in average.items():
for i in range(len(avg)):
avg[i] /= counts[c]#average的值會修改
return average
思想很簡單,將每一類別樣本的值相加,再除以樣本的個數。有一點值得注意,average是一個字典,key為類別,value為一個list,在修改value的時候,average的值也跟着變化
- 3.距離的計算
在對測試樣本進行分類的時候需要計算樣本與類別中心點的距離,可以使用歐氏距離,如圖:就是計算x到中心點c1、c2哪個更近。不過我們也可以使用向量的夾角,因為向量是矢量,向量的乘積是有符號的,通過判斷結果的正、負
來找出距離哪個中心點近。
class = sign((X - (C1+C2)/2) * (C2 - C1)) ==>sign(xC1-XC2 + (C1C1 - C2C2)/2)
通過計算向量夾角判斷樣本的類別 - 4.特征的處理:針對每一特征維度,定制不同的特征處理方法,最后再形成新的數據集
- 固定個數標稱型特征,如Yes or No,可以化為1,0類型
- 個數不固定的標稱型特征,如足球,籃球,滑雪,看書等等,可以對特征進行按層級排列,例如籃球和足球都屬於球類,球類都屬於運動,在轉化為數值特征的時候就不再是
1
了,例如:足球;0.8
,滑雪:0.6
- 在處理位置特征時候,可以借助地圖API計算距離
最后的合並成新的數據集[f1(row[0]),f2(row[1]),f3(row[2])]
,其中f1、f2、f3是特征處理方法,row[0]、row[1]是特征項
- 4.對特征進行縮放:(加黑了,說明很重要),在處理特征的時候,特征的
尺度
不一樣,比如年齡的范圍是0~100,薪資的范圍為1000~100000,如果直接用作分類器時效果可能會很差,所以我們需要對其歸一化,對原始數據進行線性變換,使結果映射到[0-1]之間,通過找出特征的最大值和最小值,將數據都縮放到同一尺度,公式如下:
其中max為樣本的最大值,min為樣本的最小值,這個方法有一個缺陷當有新的數據加入時,可能導致max和min發生變化
;具體的操作:
def scala(rows):
#[(min,max),(),()]
ranges = [(min([row.data[i] for row in rows]),max([row.data[i] for row in rows]))
for i in range(len(rows[0].data))]
#(x - min) / (max - min)
scalaFun = lambda d:[(d[i] - ranges[i][0]) / (ranges[i][1] - ranges[i][0]) for i in range(len(ranges))]
newrows = [Data(scalaFun (row.data) + [row.target]) for row in rows]
return newrows, scalaFun
返回兩部分,一個是處理后的數據集,一個是scala函數,用於處理新樣本。至於為什么寫成匿名函數,這樣就能夠保存ranges
了
- 5.核方法:設想,我們有一堆樣本,將數據繪制在二維平面上,發現正負樣本呈環狀展示,這時候本文上面提到的方法就已經失效了,不能使用一條直線將樣本分開,但如果我們將樣本投影到一個3維的空間中,那么樣本就將變得線性可分,如下圖:
這種方法叫做核技巧,初學者可能聽到核方法
就會想到SVM,是SVM引用了核方法,而不是它創造了核方法,下文就將使用核函數。核技巧是用一個新的函數替代原來的點積函數,借助某個映射函數將數據變換到更高維度的坐標空間
,新函數返回新的內積結果。實際上我們不會去找這個映射函數
,因為找到一個符合數據集的高緯函數是很困難的,常常我們使用被受人推崇的徑向基函數(radial-basis function,rbf)
rbf核
def rbf(v1,v2,gamma=20):
m = sum([(v1[i] - v2[i])**2 for i in range(len(v1))])
return math.exp(-gamma * m)
這時候我們將樣本映射到新的空間中,我們需要一個新的函數,用以計算坐標點在變換后的空間中與均值點的距離,在新的樣本空間中,無法計算均值點。所幸的是,先對一組向量求均值,然后計算均值與向量\(a\)的點積結果,與先對向量\(a\)與該組向量中的每一個向量求點積,然后再計算均值是完全等價的。因此
for row in rows:
if row.target == 0:
sum0 += rbf(sample,row.data,gamma)#在計算內積的時候用核函數替代
count0 += 1
else:
sum1 += rbf(sample,row.data,gamma)
count1 += 1
y = (1.0/count0) *sum0 - (1.0/count1) *sum1 + offset
完整代碼見這里
總結:本節使用了線性分類器對數據進行二分類,對於不能線性分類的數據引入了核函數,認識了核函數的工作原理,有助於對SVM高級分類器的理解。