吳恩達機器學習課程-作業6-支持向量機(python實現)

Machine Learning(Andrew) ex6-Support Vector Machines

椰汁筆記

Support Vector Machines

  • 這個算法是幹什麼的?
    分類算法,和邏輯迴歸類似。
  • 這個算法的優點是什麼?
    這個算法又叫做最大間距分類算法。
    下面這張圖就是很好的解釋,對於下面的分類問題之前的邏輯迴歸的決策邊界可能是粉色或者綠色的線。可以看到雖然是成功將數據集分爲兩部分,但是這樣看起來不是那麼地自然,分離地比較勉強。
    對於支持向量機,它地決策邊界邊一定會是黑色地線。可以看到它的分類更加的自然。原因是這個決策邊界擁有離訓練樣本最大的最短距離。這就是支持向量機的優勢,這樣分類的魯棒性更好。
    在這裏插入圖片描述
  • 爲什麼能做到最大間距呢?
    要說清楚這個問題,我們先要了解它的優化目標
    之前的邏輯迴歸的優化目標是(先不考慮正則化)
    J(θ)=1mi=1m[y(i)log(hθ(x(i)))(1y(i))log(1hθ(x(i)))] J(\theta)=\frac{1}{m}\sum_{i=1}^{m}[-y^{(i)}log(h_{\theta}(x^{(i)}))-(1-y^{(i)})log(1-h_{\theta}(x^{(i)}))]
    y=1,需要使θTx>>0,此時的函數爲J(θ)=log11+ez,圖像如下 \textrm{當}y=1\textrm{,需要使}\theta^Tx>>0\textrm{,此時的函數爲}J(\theta)=-log\frac{1}{1+e^{-z}}\textrm{,圖像如下}
    在這裏插入圖片描述在這裏插入圖片描述
    支持向量機的優化函數做了右圖的修改,爲粉線部分。1左側爲線性的。可以看到這樣改沒有改變函數的大致走勢,之所以這樣改是爲了提升計算效率。
    y=0,需要使θTx<<0,此時的函數爲J(θ)=log11+ez,也做同樣的修改圖像如下 \textrm{當}y=0\textrm{,需要使}\theta^Tx<<0\textrm{,此時的函數爲}J(\theta)=log\frac{1}{1+e^{-z}}\textrm{,也做同樣的修改圖像如下}
    在這裏插入圖片描述
    可以看到改後的損失函數發生了變化,支持向量機的目標函數爲
    J(θ)=1mi=1m[y(i)cost1(θTx(i))(1y(i))cost2(θTx(i))]+λ2mj=1nθj2 J(\theta)=\frac{1}{m}\sum_{i=1}^{m}[-y^{(i)}cost_1(\theta^Tx^{(i)})-(1-y^{(i)})cost_2(\theta^Tx^{(i)})]+\frac{\lambda}{2m}\sum_{j=1}^n\theta_j^2
    再次稍作變形,同時乘以m,除以lambda不影響優化的最後結果
    J(θ)=Ci=1m[y(i)cost1(θTx(i))(1y(i))cost2(θTx(i))]+12j=1nθj2C1λ J(\theta)=C\sum_{i=1}^{m}[-y^{(i)}cost_1(\theta^Tx^{(i)})-(1-y^{(i)})cost_2(\theta^Tx^{(i)})]+\frac{1}{2}\sum_{j=1}^n\theta_j^2 \\這裏的C可以理解爲\frac{1}{\lambda},這個參數的位置只是說明了給哪部分更大的權重
    下面我們可以理解爲什麼是最大間距了,要優化的目標函數可以簡化爲
    minθ J(θ)=12j=1nθj2=12θ2θx(i)>>1,if y(i)=1;θx(i)<<1,if y(i)=0θx(i)=θxcosα,α \min_\theta \ J(\theta)=\frac{1}{2}\sum_{j=1}^n\theta_j^2=\frac{1}{2}||\theta||^2 \\\theta x^{(i)}>>1,if \ y^{(i)}=1;\theta x^{(i)}<<1,if \ y^{(i)}=0 \\而\theta x^{(i)}=||\theta||*||x||*cos\alpha,\alpha爲兩個向量的夾角
    要求出目標函數最小的theta,必須時兩個向量的夾角無限接近0度,也就是theta向量和x向量平行,但是注意決策邊界和theta向量是垂直的,也就是說x和決策邊界是垂直的。這樣就可以保證決策邊界離數據間距最大,因爲|x|cosa就最大,也就是決策邊界的值。
    下面這張圖就很清楚的說明了p增大就是間距增大。
    在這裏插入圖片描述
  • 這個算法還有什麼不同之處呢?
    新的特徵構造方法,核函數。
    之前對於非線性的情況,我們使用添加高次多項式特徵來實現,這樣做的缺點就是隨着次數的增大計算量會非常大。
    支持向量機使用通過標記的方式來構造特徵
    Given (x(1),y(1)),(x(2),y(2)),,(x(m),y(m))Choose l(1)=x(1),l(2)=x(2),,l(m)=x(m)Given x:f1=kernel(x,l(1)),f2=kernel(x,l(2)),,fm=kernel(x,l(m)) Given \ (x^{(1)},y^{(1)}),(x^{(2)},y^{(2)}),\dots,(x^{(m)},y^{(m)}) \\Choose \ l^{(1)}=x^{(1)},l^{(2)}=x^{(2)},\dots,l^{(m)}=x^{(m)} \\Given \ x:f_1=kernel(x,l^{(1)}),f_2=kernel(x,l^{(2)}),\dots,f_m=kernel(x,l^{(m)})
    這樣就得到了m個特徵,通常選標記點都是直接從數據中選。
    這樣的道理是什麼呢?這裏的核函數是用來度量數據點與標記點的距離,我的理解是分類就是由離某類樣本的距離確定的,因此是合理的。

上面對支持向量機的理解是非常宏觀的,具體細節還需要深入學習。後面的實現上也沒有涉及算法具體,直接調用使用,感覺沒什麼難度。

  • 1.1 Visualizing the datasetyu

第一個數據集的數據是可以線性分類的,先來可視化一下數據,因爲要反覆畫圖,封裝一下散點圖的繪製

def plot_scatter(x1, x2, y):
    """
    繪製散點圖
    :param x1: ndarray,橫座標數據
    :param x2: ndarray,縱座標數據
    :param y: ndarray,標籤
    :return: None
    """
    plt.scatter(x1, x2, c=y.flatten())
    plt.xlabel("x1")
    plt.ylabel("X2")
    
data1 = sio.loadmat("data\\ex6data1.mat")
X = data1["X"]
y = data1["y"]
plot_scatter(X[..., 0], X[..., 1], y)
plt.show()

在這裏插入圖片描述
我們運用SVM算法進行分類,這裏用的是sklearn這個庫的實現,先引入

from sklearn import svm

下面直接進行模型的創建和訓練,C就是我們的參數,可以自己指定核函數,這裏直接用線性的也就是沒有核函數,因爲是線性可分的

    model = svm.SVC(C=1, kernel='linear')
    model.fit(X, y.ravel())

下面畫出決策邊界,這裏需要反覆用到,因此也封裝一下。這裏因爲考慮到決策邊界不一定是直線,所以我們畫等高線的方法實現。

def plot_boundary(model, X, title):
    """
    繪製決策邊界
    :param model: <class 'sklearn.svm._classes.SVC'>,訓練好的模型
    :param X: ndarray,訓練數據
    :param title: str,圖片的題目
    :return: None
    """
    x_max, x_min = np.max(X[..., 0]), np.min(X[..., 0])
    y_max, y_min = np.max(X[..., 1]), np.min(X[..., 1])
    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 1000), np.linspace(y_min, y_max, 1000))
    p = model.predict(np.concatenate((xx.ravel().reshape(-1, 1), yy.ravel().reshape(-1, 1)), axis=1))
    plt.contour(xx, yy, p.reshape(xx.shape))
    plt.title(title)
    plot_boundary(model, X, "SVM Decision Boundary with C = 1 (Example Dataset 1)")
    plt.show()

可以看到分類結果還可以,而且當C=1時,異常點沒有影響到分類效果。
在這裏插入圖片描述
修改C到100,可以看到異常點影響到了分類的效果。有點點過擬合的味道,具體的我們最後總結。
在這裏插入圖片描述

  • 1.2 SVM with Gaussian Kernels

可視化第二個數據

    data2 = sio.loadmat("data\\ex6data2.mat")
    X = data2['X']
    y = data2['y']
    plot_scatter(X[..., 0], X[..., 1], y)
    plt.show()

在這裏插入圖片描述
可以看到這裏的數據不是線性可分的,因此需要使用核函數了。
高斯核函數是一個常用的選擇
Kgaussian(x(i),x(j))=exp(x(i)x(j)22σ2)=exp(k=1n(xk(i)xk(j))2σ2) K_{gaussian}(x^{(i)},x^{(j)})=exp(-\frac{||x^{(i)}-x^{(j)}||^2}{2\sigma^2})=exp(-\frac{\sum_{k=1}^n(x_k^{(i)}-x_k^{(j)})}{2\sigma^2})

def gaussian_kernel(x1, x2, sigma):
    return np.exp(-np.sum(np.power(x1 - x2, 2)) / (2 * sigma ** 2))

測試一下

print(gaussian_kernel(np.array([1, 2, 1]), np.array([0, 4, -1]), 2.))
#0.32465246735834974

下面對數據2進行分類,使用高斯核函數。sklearn.svm.SVC中並沒有直接的高斯核函數,我麼可以通過使用rbf函數配合gamma參數實現,rbf和高斯核函數大致相同,只是將底部換成了gamma,詳細可以參考這篇博客

    data2 = sio.loadmat("data\\ex6data2.mat")
    X = data2['X']
    y = data2['y']
    sigma = 0.1
    gamma = 1 / (2 * np.power(sigma, 2))
    plot_scatter(X[..., 0], X[..., 1], y)
    model = svm.SVC(C=1, kernel='rbf', gamma=gamma)
    model.fit(X, y.ravel())
    plot_boundary(model, X, "SVM (Gaussian Kernel) Decision Boundary (Example Dataset 2)")
    plt.show()

可以說分類效果是非常的nice了
在這裏插入圖片描述
下面繼續第三組數據,老規矩先可視化

    data3 = sio.loadmat("data\\ex6data3.mat")
    X = data3['X']
    y = data3['y']
    plot_scatter(X[..., 0], X[..., 1], y)
    plt.show()

可以看到這裏的存在多個異常數據,這裏的參數選擇就非常重要了。
在這裏插入圖片描述
爲了達到更好的效果,我們對參數進行選取,C和sigma怎麼選取好呢,我們可以不斷訓練不同的C和sigma的組合通過交叉驗證集的量化值(F1-score)來選出最好的選擇。這裏的C和sigma都從0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30中選,我們因此需要遍歷。

    Xval = data3['Xval']
    yval = data3['yval']
    xx, yy = np.meshgrid(np.array([0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30]), np.array([0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30]))
    parameters = np.concatenate((xx.ravel().reshape(-1, 1), yy.ravel().reshape(-1, 1)), axis=1)
    score = np.zeros(1)
    for C, sigma in parameters:
        gamma = 1 / (2 * np.power(sigma, 2))
        model = svm.SVC(C=C, kernel='rbf', gamma=gamma)
        model.fit(X, y.ravel())
        score = np.append(score, model.score(Xval, yval.ravel()))
    res = np.concatenate((parameters, score[1:].reshape(-1, 1)), axis=1)
    index = np.argmax(res, axis=0)[-1]
    print("the best choice of parameters:C=", res[index][0], ",sigma=", res[index][1], ",score=", res[index][2])
    #the best choice of parameters:C= 1.0 ,sigma= 0.1 ,score= 0.965

選出的C=1,sigma=0.1時,模型得分最高。以此訓練模型,畫出決策邊界

plot_boundary(model, X, "SVM (Gaussian Kernel) Decision Boundary (Example Dataset 3)")

在這裏插入圖片描述


Spam Classification

下面是個關於垃圾郵件分類的半實戰項目

  • 2.1 Preprocessing Emails

首先我們需要對郵件進行處理

 - 所有字母小寫化
 - 移除html標籤,eg:<p></p>
 - 將所有的URL用httpaddr代替
 - 將所有的郵箱用emailaddr代替
 - 將所有的數字用number代替
 - 將所有的$符號用dollar代替
 - 提取每個詞的詞幹
 - 移除多餘的空白字符

代替的部分我們使用正則表達式去完成,詞幹提取使用nltk.stem.porter.PorterStemmer實現。

def process_email(content):
    """
    處理郵件文本
    :param content: str,郵件文本
    :return: list,單詞列表
    """
    content = content.lower()
    content = re.sub(r'<.*>', '', content)  # 移除html標籤
    content = re.sub(r'http[s]?://.+', 'httpaddr', content)  # 移除url
    content = re.sub(r'[\S]+@[\w]+.[\w]+', 'emailaddr', content)  # 移除郵箱
    content = re.sub(r'[\$][0-9]+', 'dollar number', content)  # 移除$,解決dollar和number連接問題
    content = re.sub(r'\$', 'dollar number', content)  # 移除單個$
    content = re.sub(r'[0-9]+', 'number', content)  # 移除數字
    content = re.sub(r'[\W]+', ' ', content)  # 移除字符
    words = content.split(' ')
    if words[0] == '':
        words = words[1:]  # 分開時會導致開始空格處多出一個空字符
    porter_stemmer = PorterStemmer()
    for i in range(len(words)):
        words[i] = porter_stemmer.stem(words[i])  # 提取詞幹
    return words

一般會將郵件的單詞進行編碼,用數字去代替,以便於實現特徵的向量化。
這裏的數字是由全部數據集的出現的比較多的單詞進行排序的,作業中直接提供了。我們直接完成單詞到序號的映射。

def mapping(word, vocab):
    """
    單詞映射爲編號
    :param word: str,單詞
    :param vocab: list,編號 表
    :return: int,編號
    """
    for i in range(len(vocab)):
        if word == vocab[i]:
            return i
    return None
  • 2.2 Extracting Features from Emails

特徵提取就是運用剛纔上面的兩步內容,實現從郵件到特徵向量的轉化。

def email_features(email, vocab):
    """
    郵件單詞列表轉化爲特徵向量
    :param email: list,郵件的單詞列表
    :param vocab: list,編號表
    :return: ndarray,特徵向量
    """
    features = np.zeros((len(vocab, )))
    for word in email:
        index = mapping(word, vocab)
        if index is not None:
            features[index] = 1
    return features
  • 2.3 Training SVM for Spam Classification

作業中後面沒有用到上面的,直接提供了處理好的訓練數據,但是推薦大家實現。我這裏直接訓練模型,這裏由於數據量還是比較大,這裏不用核函數效果更好,具體的選擇方法我在最後總結。

    train_data = sio.loadmat("data\\spamTrain.mat")
    train_X = train_data['X']  # (4000,1899)
    train_y = train_data['y']  # (4000,1)
    test_data = sio.loadmat("data\\spamTest.mat")
    test_X = test_data['Xtest']  # (1000,1899)
    test_y = test_data['ytest']  # (1000,1)
    model = svm.SVC(kernel='linear')  # 這裏的n比較大,選用線性核函數效果好
    model.fit(train_X, train_y.ravel())
    print(model.score(train_X, train_y.ravel()), model.score(test_X, test_y.ravel()))
    #0.99975 0.978

下面我們來使用作業給到的郵件例子來試試,對於這個分類正確。

    x = email_features(process_email(open("data\\emailSample2.txt").read()), vocab)
    print(model.predict(x.reshape(1, -1)))#[0]

這裏來總結一下

  • 模型量化評價方法
    三個指標precision,recall,F1 score
    對於一個二分類問題
predict\ actual 1 0
1 True Positive False Positive
0 False Negative True Negative

precision=True PositiveTrue Positive+False Positiverecall==True PositiveTrue Positive+False NegativeF1 score=2precisionrecallprecision+recall precision=\frac{True\ Positive}{True\ Positive+False\ Positive} \\recall==\frac{True\ Positive}{True\ Positive+False\ Negative} \\F1\ score=\frac{2*precision*recall}{precision+recall}
精度就是假定都被預測成1,實際上爲1的
召回率就是實際爲1,預測爲1的
這兩個值都要高,模型纔好,因此F1score就是衡量模型好壞的一個指標,兼顧了精度和召回率。

  • 支持向量機的參數選擇
    對於C,C越大,可以理解爲lambda越小,會出現lower bias和high variance的問題就是過擬合;C越小,可以理解爲lambda越大,會出現high bias和low variance的問題就是欠擬合。
    對於高斯函數的sigma,sigma越大,特徵變化越平滑,會出現high bias和low variance的問題就是欠擬合;sigma越小,特徵變化越陡峭,會出現lower bias和high variance的問題就是過擬合。
    如何選擇核函數?
No kernel(linear kernel) guassian kernel
n large,m small n small, m large

另外:
當特徵數量相對於訓練集數量很大時,使用邏輯迴歸或者是使用線性核函數的支持向量機
當特徵數量很少,訓練集數據量一般,使用高斯核函數的支持向量機
當特徵數量很少,訓練集數據很大,可以考慮添加更多特徵,然後使用邏輯迴歸或者是使用線性核函數的支持向量機


完整的代碼會同步 在我的github

歡迎指正錯誤

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章