以圖像分割爲例淺談支持向量機(SVM)

1. 什麼是支持向量機?

  在機器學習中,分類問題是一種很是常見也很是重要的問題。常見的分類方法有決策樹聚類方法貝葉斯分類等等。舉一個常見的分類的例子。以下圖1所示,在平面直角座標系中,有一些點,已知這些點能夠分爲兩類,如今讓你將它們分類。
(圖1)
顯然咱們能夠發現全部的點一類位於左下角,一類位於右上角。因此咱們能夠很天然將它們分爲兩類,如圖2所示:紅色的點表明一類,藍色的點表明一類。
(圖2)
如今若是讓你用一條直線將這兩類點分開,這應該是一件很是容易的事情,好比如圖3所示的三條直線均可以辦到這點。
(圖3)
事實上,能夠很容易發現,咱們能夠做無數條直線將這兩類點分開。這裏,咱們不由要問,是否是全部的直線分類的效果都同樣好呢?若是不是,那麼哪一條直線分類效果最好呢?評判的標準又是什麼?好比對於如圖4所示的兩條直線,\(line1\)\(line2\),這兩條直線哪條分類效果更好呢?
(圖4)
直觀上能夠發現,\(line1\)的分類效果要比\(line2\)更好的,這是由於\(line1\)幾乎位於這兩類點的中間,不偏向於任何一類點;而\(line2\)則偏向右上部分的點更多一些。若是這時又增長了一些點讓你將它們歸爲這兩類,顯然\(line1\)要更「公正」一些,而\(line2\)則有可能將原本屬於右上類的點錯誤地歸爲左下類。說到這裏,你可能會問,如何才能肯定那個最佳分類的直線呢?其實這正是支持向量機(\(SVM,Support Vector Machine\))要解決的問題。
  更通常地狀況下,如圖5所示,有時兩類點(圖5中紅色的點和藍色的點)是交錯分佈的,「你中有我,我中有你」,根本不可能用一條直線分開,這個時候該怎麼辦呢?這也是支持向量機要解決的問題,並且是支持向量機的優點所在。這類問題叫作非線性分類問題
(圖5)
  說到這裏,你可能大概有些明白支持向量機是用來幹什麼的了。支持向量機的基本模型是定義在特徵空間上的間隔最大線性分類器。它是一種二分類模型。當採用了核技巧以後,支持向量機便可以用於非線性分類。不一樣類型的支持向量機解決不一樣的問題。python

1.線性可分支持向量機:當訓練數據線性可分時,經過硬間隔最大化,學習一個線性可分支持向量機。

2. 線性支持向量機:當訓練數據近似可分時,經過軟間隔最大化,學習一個線性支持向量機。

3. 非線性支持向量機:當訓練數據線性不可分時,經過使用核技巧以及軟間隔最大化,學習一個非線性支持向量機。

  以上只是對於支持向量機的最粗淺的說明,其實支持向量機內在的數學原理仍是很是複雜的,其內容也十分豐富。我在學習的過程當中參考了很多教材,好比《數據挖掘導論》、《神經網絡與機器學習》、《Python大戰機器學習》等。裏面對於支持向量機有很是詳細的說明,並且還從數學的角度推導了一遍。我的以爲好好研究一下原理以及數學推導對於深入理解支持向量機仍是很是有幫助的。鑑於我這裏只是介紹,而非嚴格地教程,因此公式就不羅列了,感興趣的請自行閱讀相關文獻與書籍。算法

2. 如何理解支持向量機?

  若是不從數學公式的角度出發,在不涉及公式細節的狀況下,如何直觀理解支持向量機呢?雖然這並不是易事(由於支持向量機的複雜性),可是仍是能夠辦到的。我在查閱資料的過程當中,看到了知乎上的一個問題,裏面有幾個答案我以爲很是棒,可讓你在不理解數學公式的狀況下,對於支持向量機有一個直觀的瞭解。地址以下:支持向量機(SVM)是什麼意思?。這裏我仍然以兩類點的分類問題爲例來談談我本身的理解。以圖1中的兩類點爲例,前面咱們已經說過了,存在無窮多條直線能夠將這兩類點分開。如今咱們的目標是在必定的準則下,找出劃分最好的那一條。從直觀的理解來看,這條最佳直線應該知足「公正性」:即不偏向任何一類點,或者說處於中間位置。如今假設咱們已經找到了一條分割直線\(l\),每個樣本點都到這條直線存在一個距離。設直線\(l\)的方程爲:\(wx + b = 0\),共有\(n\)個點,\(n\)個點的座標爲\((x_1,y_1),(x_2,y_2),\cdots,(x_n,y_n)\)\(n\)個點到直線\(l\)的距離分別爲\(d_1,d_2,\cdots,d_n\),如今咱們須要找\(d_1,d_2,\cdots,d_n\)中的最小值:\(d_{min} = min\{d_1,d_2,\cdots,d_n\}\),顯然咱們但願\(d_{min}\)越大越好,\(d_{min}\)越大,說明它距離兩類的距離都較遠。因而問題轉化爲在全部可行的直線劃分中,找到 使得\(d_{min}\)最大的那條便是最佳劃分直線。對於線性可分的狀況而言,咱們能夠證實,這樣的最佳直線老是存在的。咱們稱找到的最佳劃分兩類的直線爲:最大幾何間隔分離超平面(對於二維點而言是直線,三維空間中則是平面,更高維則是超平面了,這裏統稱爲超平面)。數組

什麼是支持向量

  支持向量機(\(SVM\))之因此稱之爲支持向量機,是由於有一個叫做支持向量(\(Support Vector\))的東西。那麼什麼叫做支持向量呢?假設咱們如今已經找到了最大幾何間隔分離超平面,容易理解,咱們能夠找到許多條與這條直線平行的直線,在全部平行的直線中,存在兩條直線,它們剛好能夠劃分兩類點,所謂剛好是指,若是再平移哪怕一點點,就會不能正確劃分兩類點,這兩條臨界直線(超平面)被稱之爲間隔邊界。對於線性可分的狀況而言,咱們能夠證實,在樣本點中總會有一些樣本點落在間隔邊界上(可是對於線性不可分的狀況,則未必如此),落在間隔邊界上的這些樣本點就被咱們稱爲支持向量。之因此被稱之爲支持向量呢,是由於咱們肯定的最大幾何間隔分離超平面只與這些支持向量有關,與其餘的樣本點無關,也就是說哪怕你去掉再多非支持向量的點,最大幾何間隔分離超平面也同樣不變。這也就是支持向量機名字的來源。網絡

支持向量機如何處理線性不可分的狀況?

  這個問題其實涉及到\(SVM\)的核心了。在以前咱們屢次提到了一個詞:核技巧。那麼什麼是核技巧呢?首先,咱們須要明確輸入空間特徵空間這兩個概念。所謂輸入空間就是咱們定義樣本點的空間,因爲問題線性不可分,因此咱們沒法用一個超平面將兩類點分開,可是咱們總能夠找到一個合適的超曲面將兩類點正確劃分。問題的關鍵就是找到這個超曲面。直接尋找顯然是很困難的,因此咱們聰明的數學家就定義了一個映射,簡單來講就是從低維到高維的映射,研究發現,若是映射定義地恰當,則原來在低維線性不可分的問題,到了高維竟然就線性可分了!這真的是一個讓人驚喜的發現。因此咱們只要在高維按照以前線性可分的狀況去找最大幾何間隔分離超平面,找到以後,再還原到低維就能夠了。理論上已經證實,在低維線性不可分的狀況下,咱們總能夠找到合適的從低維到高維的映射,使得在高維線性可分。因而問題的關鍵就是找這個從低維到高維的映射了,這個其實就是核函數(核技巧)要乾的事情了。具體的定義較爲複雜,這裏不展開了。在給定核函數的狀況下,咱們能夠利用求解線性分類問題的方法來求解非線性分類問題的支持向量機,學習是隱式地在特徵空間(也就是映射以後的高維空間)進行的,這被稱之爲核技巧。在實際應用中,每每直接依賴經驗選擇核函數,而後再驗證其是有效的便可。經常使用的核函數有:多項式核函數高斯核函數sigmoid核函數等。app

3. 支持向量機的實際應用舉例(附matlab代碼與Python代碼)

1. 將兩類點分類(二維平面)

  做爲第一個例子,咱們首先解決開頭提到的那個平面上兩類點的分類問題。咱們找出最大幾何間隔分離超平面支持向量,而後驗證該最佳超平面可否對新加入的點進行準確分類。這裏咱們分別使用Matlab與Pyhton來實現這個例子。Matlab中的\(svmtrain\)\(svmclassify\)函數以及Python sklearn(一個機器學習的庫)均對SVM有很好的支持。若是想要詳細瞭解兩者的用法,對於Matlab能夠直接查看其幫助手冊,對於Pyhton則能夠參考相關機器學習的書籍或者直接去看sklearn的網站學習。
Matlab 對兩類點分類的代碼:dom

% 使用SVM(支持向量機)分割兩類點並畫出圖形
XY1 = 2 + rand(100,2); % 隨機產生100行2列在2-3之間的點
XY2 = 3+ rand(100,2);% 隨機產生100行2列在3-4之間的點
XY = [XY1;XY2]; % 合併兩點
Classify =[zeros(100,1);ones(100,1)]; % 第一類點用0表示,第二類點用1表示
Sample = 2+ 2*rand(100,2); % 測試點
%figure(1);
%plot(XY1(:,1),XY1(:,2),'r*'); % 第一類點用紅色表示
%hold on;
%plot(XY2(:,1),XY2(:,2),'b*'); % 第二類點用藍色表示
% 訓練SVM
SVM = svmtrain(XY,Classify,'showplot',true);
% 給測試點分類,並做出最大間隔超平面(一條直線)
svmclassify(SVM,Sample,'showplot',true);

獲得結果如圖6所示:
(圖6)
圖6中的直線便是所求的最大幾何間隔分離超平面,畫黑圈的點爲支持向量,並且能夠看出其對新增長的點劃分得很好,這說明了SVM最大幾何間隔分離超平面分類的有效性。
再來看Python的代碼:機器學習

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time   : 2017/7/22 10:45
# @Author : Lyrichu
# @Email  : 919987476@qq.com
# @File   : svm_split_points.py
'''
@Description:使用svm對兩類點進行分類(線性可分)
'''
from __future__ import print_function
import numpy as np
import matplotlib.pyplot as plt
from sklearn.svm import LinearSVC # 導入SVM 線性分類器
XY1 = 2 + np.random.rand(100,2) # 100行2列在2到3之間的數據點
XY2 = 4 + np.random.rand(100,2) # 100行2列在4到5之間的數據點
XY = np.concatenate((XY1,XY2),axis=0)
test_data = 2 + 3*np.random.rand(100,2) # 測試數據,2-5之間
label = np.append(np.zeros(100),np.ones(100)) # XY1 用0標誌,XY2用1標誌
svm = LinearSVC()
svm.fit(XY,label)
predict_test =svm.predict(test_data) # 對測試數據進行預測
coef = svm.coef_ # 係數(w向量)
intercept = svm.intercept_ # 截距(b)
# print("coef:",coef)
# print("intercept:",intercept)
# print("predict_test:",predict_test)
sort1_index = predict_test == 0. # 測試數據屬於第一類的序號(bool 數組)
sort2_index = predict_test == 1. # 測試數據屬於第二類的序號(bool 數組)
test_sort1 = test_data[sort1_index,:] # 測試數據屬於第一類的點
test_sort2 = test_data[sort2_index,:] # 測試數據屬於第二類的點
# 最大間隔超平面的方程爲:Wx + b = 0
# 畫圖
plt.plot(XY1[:,0],XY1[:,1],'r*',label='train data 1')
plt.plot(XY2[:,0],XY2[:,1],'b*',label='train data 2')
line_x = np.arange(2,5,0.01) # 直線x座標
line_y = (coef[0,0]*line_x + intercept[0])/(-coef[0,1]) # 直線y座標
# 畫出直線
plt.plot(line_x,line_y,'-')
# 畫出預測點
plt.plot(test_sort1[:,0],test_sort1[:,1],'r+',label='test data 1')
plt.plot(test_sort2[:,0],test_sort2[:,1],'b+',label='test data 2')
plt.legend(loc = 'best')
plt.show()

結果以下圖7所示:
(圖7)
其中那條直線便是做出的最大幾何間隔分離超平面,train data 1 和 train data 2爲第1、二類訓練數據,test data 1和 test data 2 爲第1、二類測試數據。能夠看出 SVM 分類的效果很好。函數

2. 將圖像中的某個物體從背景中分割出來(這裏以分割在湖中游泳的鴨子爲例)

  如圖8所示,湖面上有一隻鴨子,如今咱們但願將鴨子從湖水(背景)中分割出來,該怎麼作呢?

若是你手中有相似PS這樣的軟件,完成這個任務應該並不困難,不就是摳圖麼!!!可是,摳圖須要咱們本身手動找分割線啊,多麻煩呢,能不能讓計算機自動完成這個工做呢?固然是能夠的,利用上面說的SVM就能夠辦到。那麼該怎麼作呢?咱們知道,彩色圖片本質上是由一個一個的像素點組成的,每個像素點由RGB三色組成,或者說本質上彩色圖像就是三維數組,而灰度圖像則是二維數組。若是咱們將湖水和鴨子看作兩類物體,那麼如今的任務則是從整個圖像中將這兩類分割出來。顯然鴨子與湖水的界限並非一條單純的直線,甚至有些地方是交雜在一塊兒的,因此本質上這是一個非線性可分的問題。從圖中能夠看出,鴨子的顏色偏黑色和灰色,摻雜有少許白色以及黃色(鴨腳),而湖水則是淺綠色的。因此咱們能夠以顏色爲標準對兩者進行分類,即以RGB爲分類標準。爲了使用SVM,首先咱們須要選取訓練樣本,這裏就是找出典型的屬於鴨子的像素點RGB值(爲一個長度爲3的向量),和屬於湖水的RGB值。關於如何肯定圖像上某一點的RGB值,有不少辦法,這裏我推薦使用一個名爲Colorpix的小軟件,這個軟件只有幾百kb,一個exe執行文件,能夠找出屏幕上任何一點的像素屬性,用起來很方便,若是要用,請你們自行搜索。這裏我對於湖水和鴨子分別選取了10個像素點,這樣我就獲得了一個20行3列的樣本數據(每一行是一個樣本,共有20個樣本)。將湖水的像素點標記爲0,鴨子的像素點標記爲1,這樣咱們就能夠獲得長度爲20的、前10個元素爲0,後10個元素爲1的向量。因爲圖像原始數據爲三維矩陣,好比設其維度爲\((m,n,k)\),咱們首先須要將其轉化爲2維,即轉化爲\((mn,k)\)的矩陣,而後使用線性不可分的SVM訓練樣本數據,接着使用訓練好的SVM對\((mn,k)\)矩陣進行歸類,咱們獲得一個長爲\(mn\)的數據取0或者1的一維數組\(predict\),爲0的部分就是表明對應的像素點斷定爲湖水了。接着將\(predict\)數組在行的方向上擴展爲3列,即變爲\((predict,predict,predict)\),擴展以後的矩陣維度爲\((mn,k)\),再將其變回三維矩陣,即\((m,n,k)\)的矩陣。該矩陣與原始圖像三維矩陣對應,該矩陣數據點爲\((0,0,0)\)的部分即斷定爲湖水,咱們將圖像上該像素點的RGB值變爲\((255,255,255)\)(白色),因而咱們就能夠獲得去掉湖水(變爲白色背景)的鴨子了。
  以上就是使用SVM將鴨子從湖水中分割出來的步驟了。下面給出代碼:學習

1. Matlab 代碼

% 使用SVM將鴨子從湖面分割
% 導入圖像文件引導對話框
[filename,pathname,flag] = uigetfile('*.jpg','請導入圖像文件');
Duck = imread([pathname,filename]);
%使用ColorPix軟件從圖上選取幾個湖面的表明性點的RGB的值
LakeTrainData = [147,168,125;151 173 124;143 159 112;150 168 126;...
    146 165 120;145 161 116;150 171 130;146 112 137;149 169 120;144 160 111];
% 從圖中選取幾個有表明性的鴨子點的RGB值
DuckTrainData = [81 76 82;212 202 193;177 159 157;129 112 105;167 147 136;...
    237 207 145;226 207 192;95 81 68;198 216 218;197 180 128];
% 屬於湖的點爲0,鴨子的點爲1
group = [zeros(size(LakeTrainData,1),1);ones(size(DuckTrainData,1),1)];
% 訓練獲得支持向量分類機
LakeDuckSVM = svmtrain([LakeTrainData;DuckTrainData],group,'kernel_function','polynomial',...
    'polyorder',2);
[m,n,k] = size(Duck); % 圖像三維矩陣
% 將Duck轉化爲雙精度的m*n行,3列的矩陣
Duck1 = double(reshape(Duck,m*n,k));
% 根據訓練獲得的支持向量機對整個圖像像素點進行分類
IndDuck = svmclassify(LakeDuckSVM,Duck1);
% 屬於湖的點的邏輯數組
IndLake = ~IndDuck;
result = reshape([IndLake,IndLake,IndLake],[m,n,k]); % 與圖片的維數對應
Duck2 = Duck;
Duck2(result)= 255; % 湖面的點變爲白色
figure;
imshow(Duck2); % 顯示分割以後的圖像

結果如圖8所示:
(圖8)
能夠基本看到鴨子的輪廓了,可是鴨子身體中有不少小點被扣去了(屬於誤判爲湖水),這種狀況能夠改變一些選取的像素點,或者增長一些樣本,能夠優化分割的效果。
再來看Python的實現吧。測試

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time   : 2017/7/22 13:58
# @Author : Lyrichu
# @Email  : 919987476@qq.com
# @File   : svm_split_picture.py
'''
@Description:SVM 將在湖中的一隻鴨子與湖水分割出來
'''
from PIL import Image
import numpy as np
from sklearn.svm import SVC # 非線性 分類 SVM
pic = 'duck.jpg' # 鴨子圖片
img = Image.open(pic)
img.show() # 顯示原始圖像
img_arr = np.asarray(img,np.float64)
# 選取湖面上的關鍵點RGB值(10個)
lake_RGB = np.array(
    [[147,168,125],[151,173,124],[143,159,112],[150,168,126],[146,165,120],
     [145,161,116],[150,171,130],[146,112,137],[149,169,120],[144,160,111]]
)
# 選取鴨子上的關鍵點RGB值(10個)
duck_RGB = np.array(
    [[81,76,82],[212,202,193],[177,159,157],[129,112,105],[167,147,136],
     [237,207,145],[226,207,192],[95,81,68],[198,216,218],[197,180,128]]
)
RGB_arr = np.concatenate((lake_RGB,duck_RGB),axis=0) # 按列拼接
# lake 用 0標記,duck用1標記
label = np.append(np.zeros(lake_RGB.shape[0]),np.ones(duck_RGB.shape[0]))
# 本來 img_arr 形狀爲(m,n,k),如今轉化爲(m*n,k)
img_reshape = img_arr.reshape([img_arr.shape[0]*img_arr.shape[1],img_arr.shape[2]])
svc = SVC(kernel='poly',degree=3) # 使用多項式核,次數爲3
svc.fit(RGB_arr,label) # SVM 訓練樣本
predict = svc.predict(img_reshape) # 預測測試點
lake_bool = predict == 0. # 爲湖面的序號(bool)
lake_bool = lake_bool[:,np.newaxis] # 增長一列(一維變二維)
lake_bool_3col = np.concatenate((lake_bool,lake_bool,lake_bool),axis=1) # 變爲三列
lake_bool_3d = lake_bool_3col.reshape((img_arr.shape[0],img_arr.shape[1],img_arr.shape[2])) # 變回三維數組(邏輯數組)
img_arr[lake_bool_3d] = 255. # 將湖面像素點變爲白色
img_split = Image.fromarray(img_arr.astype('uint8')) # 數組轉image
img_split.show() # 顯示分割以後的圖像
img_split.save('split_duck.jpg') # 保存

結果如圖9所示:
(圖9)
能夠看出,圖9的效果要比圖8好不少,基本已經將湖水所有去除了,只有少數點沒有去除,若是增長一些訓練樣本,訓練的效果應該會更好,你們有興趣的能夠本身嘗試一下。不過我很奇怪的是,Matlab與pyhton我選取的像素點是如出一轍的,SVM訓練設置參數也是同樣的,爲何python的效果要明顯好於Matlab呢?我沒有閱讀兩者SVM的源碼,很差下結論,姑且認爲是Python大法好吧!!!哈哈哈......
  以上就是主要要講的內容了。其實SVM在最近幾年神經網絡大火以前仍是很是受歡迎的,不過如今作複雜分類(好比圖像分類,語音識別等)好像更傾向於神經網絡了,SVM的一個重大缺點就是其對於處理大規模數據不是很適合,由於其主流的算法複雜度都是\(O(n^2)\)的,不過其在高維數據以及規模適中的狀況下作分類效果仍是很不錯的。之後有機會再來和你們探討深度學習以及神經網絡吧,目前正入坑中。。。

Reference

  1. 《數據挖掘概念與技術》
  2. 《神經網絡與機器學習》
  3. 《Python大戰機器學習》
  4. 《Matlab在數學建模中的應用》 特別感謝《Matlab在數學建模中的應用》,圖像分割的那個例子Matlab代碼改編於此,Python代碼也是基於此書改寫的。
相關文章
相關標籤/搜索