1. 程式人生 > >《機器學習系統設計》之k-近鄰分類演算法

《機器學習系統設計》之k-近鄰分類演算法

前言:

    本系列是在作者學習《機器學習系統設計》([美] WilliRichert)過程中的思考與實踐,全書通過Python從資料處理,到特徵工程,再到模型選擇,把機器學習解決問題的過程一一呈現。書中設計的原始碼和資料集已上傳到我的資源:http://download.csdn.net/detail/solomon1558/8971649

       第2章通過在真實的Seeds資料集構建一個k-近鄰分類器,從而達到一個較好的分類效果。本章主要涉及資料視覺化分析、特徵和特徵工程、資料歸一化、交叉驗證等知識內容。

1.   演算法概述

    k-近鄰演算法(kNN)的工作原理是:存在一個樣本資料集合(訓練集),並且樣本集中每個資料都存在標籤。輸入沒有標籤的新資料後,將新資料的每個特徵與樣本集中資料對應的特徵進行比較(計算歐氏距離),然後演算法提取樣本集中特徵最相似的前k個數據(k-近鄰),通過投票的方式來選擇標籤。

    簡單來說,k-近鄰演算法採用測量不同特徵之間的距離方法進行分類。其優缺點如下:

    優點:演算法簡單、精度較高、對異常值不敏感、無資料輸入假定。

    缺點:對每個資料集的每個資料特徵都需要計算距離值,計算複雜度高、空間複雜度高;

       單純的距離計算忽略了資料本身可以帶有和結果標籤的強烈的關係,kNN無法給出資料的內在含義。

2.   分析資料

    現有一個關於小麥種子的測量資料集Seeds,它包含7個特徵資料:

    面積(A)、周長(P)、緊密度(C = 4πA/P^2)、穀粒的長度、穀粒的寬度、偏度係數、穀粒的槽長度。

    這些種子一共分為三個類別,屬於小麥的三個不同品種:Canadian、Kama和Rosa。

#coding=utf-8
from matplotlib import pyplot as plt
from load import load_dataset
feature_names = [
    'area',
    'perimeter',
    'compactness',
    'length of kernel',
    'width of kernel',
    'asymmetry coefficien',
    'length of kernel groove',
]
lable_name = [
    'Kama',
    'Rosa',
    'Canadian'
]
features, lables = load_dataset('seeds')
print lables
paris = [(0,1),(0,2),(0,3),(0,4),(0,5),(0,6),(1,2),(1,3),(1,4),(1,5),(1,6),(2,3),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6),(4,5),(4,6),(5,6)]
for i, (p0, p1) in enumerate(paris):
    plt.subplot(3, 7, i+1)
    for t, marker, c in zip(range(3), ">ox", "rgb"):
        plt.scatter(features[lables == lable_name[t], p0], features[lables == lable_name[t], p1], marker=marker, c=c)
    plt.xlabel(feature_names[p0])
    plt.ylabel(feature_names[p1])
    plt.xticks([])
    plt.yticks([])
plt.show()
   

     7種特徵一共有21種順序無關的排列組合。其中可以觀察到area-perimeter、area-length of kernel、area-width of kernel、perimeter-length of kernel等影象呈現正相關性;area-perimeter、area-lengthof kernel groove、length of kernel-length of kernel groove等影象的三類種子可區分性較好。

3.   資料歸一化

    觀察資料集,原始資料包含面積、長度、無量綱的量都混合在了一起。比如第1個面積特徵A(量綱為平方米)比第3個緊密度特徵C(無量綱)高了一個數量級。這些量綱不同、數量級不同的特徵資料會影響其對分類決策的貢獻度。

    為了揭示資料歸一化的必要性,下面從判別邊界和交叉驗證的準確度兩個維度討論。為方面視覺化,這裡僅考慮面積特徵Area和緊密度特徵Compactness兩個相差較大的特徵。對比圖如下:

    在a圖中,Canadian種子用菱形表示,Kama種子用圓圈表示,Rosa種子用三角形表示。它們的區域分別是白死、黑色和灰色表示。這些判別區域都是水平方向,原因在於x軸(面積)的值域在10到22之間,而y軸(緊密度)在0.75至1.0之間。這意味著x值的一小點改變實際上比y值的一小點變化大的多。所以,在計算新資料點與原資料點的距離時,多半隻把x軸考慮了進去。

    為了解決上述問題,需要把所有特徵都歸一化到一個公共尺度上。一個常用的歸一化方法是Z值(Z-score)。Z值表示的是特徵值離它的平均值有多遠,它用標準方差的數量來計算。其公式如下:

# 從特徵值中減去特徵的平均值
features -=features.mean(0)
# 將特徵值除以它的標準差
features /=features.std(0)

    在b圖中,分介面變得複雜,而且在兩個維度之間有一個相互交叉。對只使用面積特徵Area和緊密度特徵Compactness訓練的k-近鄰分類器進行交叉驗證,我們會發現其準確率並不高:

            模型準確率: 1.000000

            Ten fold cross-validated error was86.2%.

            模型準確率: 1.000000

            Ten fold cross-validated errorafter z-scoring was 82.4%.

在訓練集上進行測試的準確率都是100%,而使用10-折交叉驗證,特徵歸一化前準確率86.2%,歸一化後準確率反而下降到82.4%。這也從準確率的角度印證了圖b分界邊界複雜,分類效果不佳的情況。

    綜上所述,特徵歸一化是必要的,同時還應該加入更多的特徵來保證良好的分類效果。

程式清單:

#coding=utf-8
COLOUR_FIGURE = False

from matplotlib import pyplot as plt
from matplotlib.colors import ListedColormap
from load import load_dataset
import numpy as np
from knn import learn_model, apply_model, accuracy
from seeds_knn import cross_validate
feature_names = [
    'area',
    'perimeter',
    'compactness',
    'length of kernel',
    'width of kernel',
    'asymmetry coefficien',
    'length of kernel groove',
]


def train_plot(features, labels):
    y0,y1 = features[:,2].min()*.9, features[:,2].max()*1.1
    x0,x1 = features[:,0].min()*.9, features[:,0].max()*1.1
    X = np.linspace(x0,x1,100)
    Y = np.linspace(y0,y1,100)
    X,Y = np.meshgrid(X,Y)

    model = learn_model(1, features[:, (0,2)], np.array(labels))
    test_error = accuracy(features[:, (0,2)], np.array(labels), model)
    print (u"模型準確率: %f") % test_error
    C = apply_model(np.vstack([X.ravel(),Y.ravel()]).T, model).reshape(X.shape)
    if COLOUR_FIGURE:
        cmap = ListedColormap([(1.,.6,.6),(.6,1.,.6),(.6,.6,1.)])
    else:
        cmap = ListedColormap([(1.,1.,1.),(.2,.2,.2),(.6,.6,.6)])
    plt.xlim(x0,x1)
    plt.ylim(y0,y1)
    plt.xlabel(feature_names[0])
    plt.ylabel(feature_names[2])
    plt.pcolormesh(X,Y,C, cmap=cmap)
    if COLOUR_FIGURE:
        cmap = ListedColormap([(1.,.0,.0),(.0,1.,.0),(.0,.0,1.)])
        plt.scatter(features[:,0], features[:,2], c=labels, cmap=cmap)
    else:
        for lab,ma in zip(range(3), "Do^"):
            plt.plot(features[labels == lab,0], features[labels == lab,2], ma, c=(1.,1.,1.))


features,labels = load_dataset('seeds')
names = sorted(set(labels))
labels = np.array([names.index(ell) for ell in labels])
train_plot(features, labels)
error = cross_validate(features[:, (0, 2)], labels)
print('Ten fold cross-validated error was {0:.1%}.\n'.format(error))
plt.savefig('../1400_02_04.png')
plt.show()
# 從特徵值中減去特徵的平均值
features -= features.mean(0)
# 將特徵值除以它的標準差
features /= features.std(0)
train_plot(features, labels)
error = cross_validate(features[:, (0, 2)], labels)
print('Ten fold cross-validated error after z-scoring was {0:.1%}.'.format(error))
plt.savefig('../1400_02_05.png')
plt.show()

4.   實施kNN演算法

    本節列出kNN演算法的Python語言實現,首先給出k-近鄰演算法的虛擬碼:

    對未知類別屬性的資料集中的每個點一次執行以下操作:

    (1)   計算已知類別資料集中的每個點與當前的的歐氏距離;

    (2)   按照距離遞增次序排序;

    (3)   選取與當前點距離最小的k個點;

    (4)   確定前k個點所在類別的出現頻率;

    (5)   返回前k個點出現頻率最高的類別作為當前點的預測分類。

程式清單:

#coding=utf-8
import numpy as np
def learn_model(k, features, labels):
    return k, features.copy(),labels.copy()

def plurality(xs):
    from collections import defaultdict
    counts = defaultdict(int)  # 預設字典
    for x in xs:
        counts[x] += 1  # 以標籤作為key值,類別對應的頻次為value
    maxv = max(counts.values())
    for k,v in counts.items():
        if v == maxv:
            return k

def apply_model(features, model):
    k, train_feats, labels = model
    results = []
    for f in features:
        label_dist = []
        for t,ell in zip(train_feats, labels):
            label_dist.append( (np.linalg.norm(f-t), ell) )
        label_dist.sort(key=lambda d_ell: d_ell[0])
        label_dist = label_dist[:k]  # 取與新資料點歐氏距離最近的前k個樣本
        results.append(plurality([ell for _ , ell in label_dist]))
    return np.array(results)

def accuracy(features, labels, model):
    preds = apply_model(features, model)
    return np.mean(preds == labels)

5.   分類預測和交叉驗證

    本節主要利用kNN演算法,在一個真實的Seeds資料集上構建一個完整的分類器,然後用採用交叉驗證評價模型。

程式清單:

5.1 load.py

import numpy as np
def load_dataset(dataset_name):
    '''
    data,labels = load_dataset(dataset_name)

    Load a given dataset

    Returns
    -------
    data : numpy ndarray
    labels : list of str
    '''
    data = []
    labels = []
    with open('../data/{0}.tsv'.format(dataset_name)) as ifile:
        for line in ifile:
            tokens = line.strip().split('\t')
            data.append([float(tk) for tk in tokens[:-1]])
            labels.append(tokens[-1])
    data = np.array(data)
    labels = np.array(labels)
    return data, labels

5.2 knn.py

    (已經在第4節出現。)

5.3 seeds_knn.py

from load import load_dataset
import numpy as np
from knn import learn_model, apply_model, accuracy

features,labels = load_dataset('seeds')

def cross_validate(features, labels):
    error = 0.0
    for fold in range(10):
        training = np.ones(len(features), bool)
        training[fold::10] = 0
        testing = ~training
        model = learn_model(1, features[training], labels[training])
        test_error = accuracy(features[testing], labels[testing], model)
        error += test_error

    return error/ 10.0

error = cross_validate(features, labels)
print('Ten fold cross-validated error was {0:.1%}.'.format(error))

features -= features.mean(0)
features /= features.std(0)
error = cross_validate(features, labels)
print('Ten fold cross-validated error after z-scoring was {0:.1%}.'.format(error))

5.4 測試結果:

            F:\ProgramFile\Python27\python.exe E:/py_Space/ML_C2/code/seeds_knn.py

            Tenfold cross-validated error was 89.5%.

            Tenfold cross-validated error after z-scoring was 94.3%.

從結果上來看,採用kNN演算法在歸一化特徵資料集Seeeds上的交叉驗證分類準確率到達94.3%,取得了一個較為滿意的結果。