【收藏】支持向量機原理詳解+案例+代碼!【點擊閱讀原文下載】
或許你已經開始了自己的探索,聽說過線性可分、核心技巧、核函數等術語。支持向量機(SVM)演算法的核心理念非常簡單,而且將其應用到自然語言分類任務中也不需要大部分複雜的東西。
【數據+代碼下載請點擊閱讀原文】
(原理部分來自機器之心:
https://mp.weixin.qq.com/s/5tUQ9B5juP-Vg8z-gp60rg)
SVM 是如何工作的?
支持向量機的基礎概念可以通過一個簡單的例子來解釋。讓我們想像兩個類別:紅色和藍色,我們的數據有兩個特徵:x 和 y。我們想要一個分類器,給定一對(x,y)坐標,輸出僅限於紅色或藍色。我們將已標記的訓練數據列在下圖中:
支持向量機會接受這些數據點,並輸出一個超平面(在二維的圖中,就是一條線)以將兩類分割開來。這條線就是判定邊界:將紅色和藍色分割開。
但是,最好的超平面是什麼樣的?對於 SVM 來說,它是最大化兩個類別邊距的那種方式,換句話說:超平面(在本例中是一條線)對每個類別最近的元素距離最遠。
線性數據
上面的例子很簡單,因為那些數據是線性可分的——我們可以通過畫一條直線來簡單地分割紅色和藍色。然而,大多數情況下事情沒有那麼簡單。看看下面的例子:
很明顯,你無法找出一個線性決策邊界(一條直線分開兩個類別)。然而,兩種向量的位置分得很開,看起來應該可以輕易地分開它們。
這個時候我們需要引入第三個維度。迄今為止,我們有兩個維度:x 和 y。讓我們加入維度 z,並且讓它以直觀的方式出現:z = x2 + y2(沒錯,圓形的方程式)
於是我們就有了一個三維空間,看看這個空間,他就像這樣:
支持向量機將會如何區分它?很簡單:
太棒了!請注意,現在我們處於三維空間,超平面是 z 某個刻度上(比如 z=1)一個平行於 x 軸的平面。它在二維上的投影是這樣:
於是,我們的決策邊界就成了半徑為 1 的圓形,通過 SVM 我們將其成功分成了兩個類別。
核函數
在以上例子中,我們找到了一種通過將空間巧妙地映射到更高維度來分類非線性數據的方法。然而事實證明,這種轉換可能會帶來很大的計算成本:可能會出現很多新的維度,每一個都可能帶來複雜的計算。為數據集中的所有向量做這種操作會帶來大量的工作,所以尋找一個更簡單的方法非常重要。
還好,我們已經找到了訣竅:SVM 其實並不需要真正的向量,它可以用它們的數量積(點積)來進行分類。這意味著我們可以避免耗費計算資源的境地了。我們需要這樣做:
想像一個我們需要的新空間:
z = x2 + y2
找到新空間中點積的形式:
a · b = xa· xb + ya· yb + za· zb
a · b = xa· xb + ya· yb + (xa2 + ya2) · (xb2 + yb2)
讓 SVM 處理新的點積結果——這就是核函數
這就是核函數的技巧,它可以減少大量的計算資源需求。通常,內核是線性的,所以我們得到了一個線性分類器。但如果使用非線性內核(如上例),我們可以在完全不改變數據的情況下得到一個非線性分類器:我們只需改變點積為我們想要的空間,SVM 就會對它忠實地進行分類。
注意,核函數技巧實際上並不是 SVM 的一部分。它可以與其他線性分類器共同使用,如邏輯回歸等。支持向量機只負責找到決策邊界。
支持向量機如何用於自然語言分類?
有了這個演算法,我們就可以在多維空間中對向量進行分類了。如何將它引入文本分類任務呢?首先你要做的就是把文本的片斷整合為一個數字向量,這樣才能使用 SVM 進行區分。換句話說,什麼屬性需要被拿來用作 SVM 分類的特徵呢?
最常見的答案是字頻,就像在樸素貝葉斯中所做的一樣。這意味著把文本看作是一個詞袋,對於詞袋中的每個單詞都存在一個特徵,特徵值就是這個詞出現的頻率。
這樣,問題就被簡化為:這個單詞出現了多少次,並把這個數字除以總字數。在句子「All monkeys are primates but not all primates are monkeys」中,單詞 mokey 出現的頻率是 2/10=0.2,而 but 的頻率是 1/10=0.1。
對於計算要求更高的問題,還有更好的方案,我們也可以用 TF-IDF。
現在我們做到了,數據集中的每個單詞都被幾千(或幾萬)維的向量所代表,每個向量都表示這個單詞在文本中出現的頻率。太棒了!現在我們可以把數據輸入 SVM 進行訓練了。我們還可以使用預處理技術來進一步改善它的效果,如詞幹提取、停用詞刪除以及 n-gram。
選擇核函數
現在我們有了特徵向量,唯一要做的事就是選擇模型適用的核函數了。每個任務都是不同的,核函數的選擇有關於數據本身。在我們的例子中,數據呈同心圓排列,所以我們需要選擇一個與之匹配的核函數。
既然需要如此考慮,那麼什麼是自然語言處理需要的核函數?我們需要費線性分類器嗎?亦或是數據線性分離?事實證明,最好堅持使用線性內核,為什麼?
回到我們的例子上,我們有兩種特徵。一些現實世界中 SVM 在其他領域裡的應用或許會用到數十,甚至數百個特徵值。同時自然語言處理分類用到了數千個特徵值,在最壞的情況下,每個詞都只在訓練集中出現過一次。這會讓問題稍有改變:非線性核心或許在其他情況下很好用,但特徵值過多的情況下可能會造成非線性核心數據過擬合。因此,最好堅持使用舊的線性核心,這樣才能在那些例子中獲得很好的結果。
Python實現
題目:
模式識別中著名的數據集。本實驗通過花萼(sepal)和花瓣(petal)的長和寬,建立SVM分類器來判斷樣本屬於山鳶尾(Iris Setosa)、變色鳶尾(Iris Versicolor)還是維吉尼亞鳶尾(Iris Virginica)。請按要求完成實驗。
數據集:
文件iris.txt為該實驗的數據集,包含150個樣本,對應數據集的每行數據。每行數據包含每個樣本的四個特徵(按順序分 鳶尾花數據集(Iris data set)是模別為花萼長度、花萼寬度、花瓣長度、花瓣寬度)和樣本的類別信息(Iris Setosa、Iris Versicolor、Iris Virginica中的一種)。
文件列表如下:(所有數據+代碼下載請點擊閱讀原文)
i
ris.txt 原始數據集
iris_train.txt 訓練數據集
iris_test.txt 測試數據集
SVM.py 未採用pca降維的SVM分類器
SVM_PCA.py 採用pca降維的SVM分類器
1 #!/usr/bin/python 2 #-*- coding: utf-8 -*- 3 from import 4 import as 5 import as 6 import as 7 import 8 9 def loadData ()
#載入函數
10
dataMat = []11
labelMat1 = []12
labelMat2 = []13
labelMat3 = []14
ylabel = []15
fr = open("iris_train.txt"
)16
for
linein
fr.readlines():17
lineArr = line.strip().split(","
)18
dataMat.append([float(lineArr[0
]), float(lineArr[1
]), float(lineArr[2
]), float(lineArr[3
])])19
if
(lineArr[4
]=="Iris-setosa"
):20
labelMat1.append(float(1
))21
else
:22
labelMat1.append(float(-1
))23
if
(lineArr[4
]=="Iris-versicolor"
):24
labelMat2.append(float(1
))25
else
:26
labelMat2.append(float(-1
))27
if
(lineArr[4
]=="Iris-virginica"
):28
labelMat3.append(float(1
))29
else
:30
labelMat3.append(float(-1
))31
ylabel.append(lineArr[4
])32
return
dataMat,labelMat1,labelMat2,labelMat3,ylabel33
34
def
loadData_test
()
:35
dataMat = []36
ylabel = []37
fr = open("iris_test.txt"
)38
for
linein
fr.readlines():39
lineArr = line.strip().split(","
)40
dataMat.append([float(lineArr[0
]), float(lineArr[1
]), float(lineArr[2
]), float(lineArr[3
])])41
ylabel.append(lineArr[4
])42
return
dataMat,ylabel43
44
45
def
pca
(dataMat, topNfeat)
:46
meanVals = mean(dataMat, axis =0
)#求平均值
47
meanRemoved = dataMat - meanVals#去平均值
48
covMat = cov(meanRemoved,rowvar=0
)#計算協防差矩陣
49
eigVals, eigVects = linalg.eig(mat(covMat))50
eigValInd = argsort(eigVals)51
#從小到大對N個值排序
52
eigValInd = eigValInd[: -(topNfeat +1
) :-1
]53
redEigVects = eigVects[:, eigValInd]54
#將數據轉換到新空間
55
lowDDataMat = meanRemoved * redEigVects56
#reconMat = (lowDDataMat * redEigVects.T) + meanVals
57
return
lowDDataMat58
59
def
selectJrand
(i,m)
:60
j=i#排除i
61
while
(j==i):62
j = int(random.uniform(0
,m))63
return
j64
65
def
clipAlpha
(aj,H,L)
:66
if
aj > H:67
aj = H68
if
L > aj:69
aj = L70
return
aj71
72
def
smoSimple
(dataMatrix, classLabels, C, toler, maxIter)
:73
labelMat = mat(classLabels).T74
b =-1
; m,n = shape(dataMatrix)75
alphas = mat(zeros((m,1
)))76
iter =0
77
while
(iter < maxIter):78
alphaPairsChanged =0
#alpha是否已經進行了優化
79
for
iin
range(m):80
# w = alpha * y * x; f(x_i) = w^T * x_i + b
81
# 預測的類別
82
fXi = float(multiply(alphas,labelMat).T*dataMatrix*dataMatrix[i,:].T) + b83
Ei = fXi - float(labelMat[i])#得到誤差,如果誤差太大,檢查是否可能被優化
84
#必須滿足約束
85
if
((labelMat[i]*Ei < -toler)and
(alphas[i] < C))or
((labelMat[i]*Ei > toler)and
(alphas[i] >0
)):86
j = selectJrand(i,m)87
fXj = float(multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[j,:].T)) + b88
Ej = fXj - float(labelMat[j])89
alphaIold = alphas[i].copy(); alphaJold = alphas[j].copy()90
if
(labelMat[i] != labelMat[j]):91
L = max(0
, alphas[j] - alphas[i])92
H = min(C, C + alphas[j] - alphas[i])93
else
:94
L = max(0
, alphas[j] + alphas[i] - C)95
H = min(C, alphas[j] + alphas[i])96
if
L==H:#print "L==H";
97
continue
98
# Eta = -(2 * K12 - K11 - K22),且Eta非負,此處eta = -Eta則非正
99
eta =2.0
* dataMatrix[i,:]*dataMatrix[j,:].T - dataMatrix[i,:]*dataMatrix[i,:].T - dataMatrix[j,:]*dataMatrix[j,:].T100
if
eta >=0
:#print "eta>=0";
101
continue
102
alphas[j] -= labelMat[j]*(Ei - Ej)/eta103
alphas[j] = clipAlpha(alphas[j],H,L)104
#如果內層循環通過以上方法選擇的α_2不能使目標函數有足夠的下降,那麼放棄α_1
105
if
(abs(alphas[j] - alphaJold) <0.00001
):#print "j not moving enough";
106
continue
107
alphas[i] += labelMat[j]*labelMat[i]*(alphaJold - alphas[j])108
b1 = b - Ei- labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i,:]*dataMatrix[i,:].T - labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[i,:]*dataMatrix[j,:].T109
b2 = b - Ej- labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i,:]*dataMatrix[j,:].T - labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[j,:]*dataMatrix[j,:].T110
if
(0
< alphas[i])and
(C > alphas[i]): b = b1111
elif
(0
< alphas[j])and
(C > alphas[j]): b = b2112
else
: b = (b1 + b2)/2.0
113
alphaPairsChanged +=1
114
if
(alphaPairsChanged ==0
): iter +=1
115
else
: iter =0
116
return
b,alphas117
118
def
calcWs
(alphas,dataMatrix, labelMat)
:119
m,n = shape(dataMatrix)120
w = zeros((n,1
))121
for
iin
range(m):122
w += multiply(alphas[i]*labelMat[i],dataMatrix[i,:].T)123
return
w124
125
def
pred
(dataMat, labelMat, w1, b1,w3,b3)
:126
dataMat = mat(dataMat)127
sum1 =0
128
m,n = shape(dataMat)129
for
iin
range(m):130
if
(dataMat[i]*w1 + b1 >0.0
and
labelMat[i]=="Iris-setosa"
):131
sum1 +=1
132
elif
(dataMat[i]*w3 + b3 >0.0
and
labelMat[i]=="Iris-virginica"
):133
sum1 +=1
134
elif
(dataMat[i]*w3 + b3 <0.0
and
dataMat[i]*w1 + b1 <0.0
and
labelMat[i]=="Iris-versicolor"
):135
sum1 +=1
136
m = float(sum1)/float(m)*100
137
"正確率為: "
, m138
139
140
xdata,ydata1,ydata2,ydata3,ylabe = loadData()141
xdata_test, ylabe_test = loadData_test()142
xdata = mat(xdata)143
xdata_test = mat(xdata_test)144
b1 , alphas1 = smoSimple(xdata,ydata1,0.8
,0.0001
,40
)145
#b2 , alphas2 = smoSimple(X,ydata2,0.8,0.0001,40)
146
b3 , alphas3 = smoSimple(xdata,ydata3,0.8
,0.0001
,40
)147
w1 = calcWs(alphas1,xdata,ydata1)148
#w2 = calcWs(alphas2,X,ydata2)
149
w3 = calcWs(alphas3,xdata,ydata3)150
pred(xdata_test, ylabe_test, w1, b1, w3, b3)
