Продолжая серию, мы перейдем к машинам опорных векторов для задания программирования 6. Если вы заметили, у меня не было рецензии на задание 5, так как большинство задач просто требуют построения и интерпретации кривых обучения. Однако вы все еще можете найти код в моем GitHub по адресу https://github.com/Benlau93/Machine-Learning-by-Andrew-Ng-in-Python/tree/master/Bias_Vs_Variance.

Это задание состоит из двух частей. Во-первых, мы реализуем опорные векторные машины (SVM) для нескольких наборов 2D-данных, чтобы иметь представление об алгоритмах и принципах их работы. Затем мы будем использовать SVM в наборах данных электронных писем, чтобы попытаться классифицировать спам-письма.

Чтобы загрузить набор данных, loadmat из scipy.io используется для открытия файлов mat.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.io import loadmat
mat = loadmat("ex6data1.mat")
X = mat["X"]
y = mat["y"]

Построение набора данных

m,n = X.shape[0],X.shape[1]
pos,neg= (y==1).reshape(m,1), (y==0).reshape(m,1)
plt.scatter(X[pos[:,0],0],X[pos[:,0],1],c="r",marker="+",s=50)
plt.scatter(X[neg[:,0],0],X[neg[:,0],1],c="y",marker="o",s=50)

Мы начинаем с простого набора данных, который имеет четкую линейную границу между обучающими примерами.

Как рекомендовано в лекции, мы стараемся не кодировать SVM с нуля, а вместо этого использовать для этого назначения высокооптимизированную библиотеку, такую ​​как sklearn. Официальную документацию можно найти здесь.

from sklearn.svm import SVC
classifier = SVC(kernel="linear")
classifier.fit(X,np.ravel(y))

Поскольку это задача линейной классификации, мы не будем использовать какое-либо ядро ​​для этой задачи. Это эквивалентно использованию линейного ядра в SVC (обратите внимание, что настройка ядра по умолчанию для SVC - «rbf», что означает радиальная базисная функция). Здесь функция ravel() возвращает массив размером (m,), который требуется для SVC.

plt.figure(figsize=(8,6))
plt.scatter(X[pos[:,0],0],X[pos[:,0],1],c="r",marker="+",s=50)
plt.scatter(X[neg[:,0],0],X[neg[:,0],1],c="y",marker="o",s=50)
# plotting the decision boundary
X_1,X_2 = np.meshgrid(np.linspace(X[:,0].min(),X[:,1].max(),num=100),np.linspace(X[:,1].min(),X[:,1].max(),num=100))
plt.contour(X_1,X_2,classifier.predict(np.array([X_1.ravel(),X_2.ravel()]).T).reshape(X_1.shape),1,colors="b")
plt.xlim(0,4.5)
plt.ylim(1.5,5)

При настройке по умолчанию C = 1.0 (помните, что C = 1 / λ) это граница решения, которую мы получили.

# Test C = 100
classifier2 = SVC(C=100,kernel="linear")
classifier2.fit(X,np.ravel(y))

plt.figure(figsize=(8,6))
plt.scatter(X[pos[:,0],0],X[pos[:,0],1],c="r",marker="+",s=50)
plt.scatter(X[neg[:,0],0],X[neg[:,0],1],c="y",marker="o",s=50)
# plotting the decision boundary
X_3,X_4 = np.meshgrid(np.linspace(X[:,0].min(),X[:,1].max(),num=100),np.linspace(X[:,1].min(),X[:,1].max(),num=100))
plt.contour(X_3,X_4,classifier2.predict(np.array([X_3.ravel(),X_4.ravel()]).T).reshape(X_3.shape),1,colors="b")
plt.xlim(0,4.5)
plt.ylim(1.5,5)

Изменение C = 100 дало границу решения, которая выходит за рамки обучающих примеров.

Далее мы рассмотрим набор данных, который нельзя разделить линейно. Здесь в игру вступают ядра, которые предоставляют нам функции нелинейного классификатора. Для тех, кто испытывает трудности с пониманием концепции ядер, эта статья, которую я нашел, дала довольно хорошую интуицию и некоторые математические объяснения о ядрах. Для этой части задания от нас требовалось завершить функцию gaussianKernel, чтобы помочь в реализации SVM с гауссовскими ядрами. Я пропущу этот шаг, поскольку SVC содержит собственную реализацию гауссовского ядра в виде радиальной базисной функции (rbf). Вот страница Википедии с уравнением для rbf, как видите, оно идентично функции ядра Гаусса из курса.

Загрузка и построение примера набора данных 2

mat2 = loadmat("ex6data2.mat")
X2 = mat2["X"]
y2 = mat2["y"]
m2,n2 = X2.shape[0],X2.shape[1]
pos2,neg2= (y2==1).reshape(m2,1), (y2==0).reshape(m2,1)
plt.figure(figsize=(8,6))
plt.scatter(X2[pos2[:,0],0],X2[pos2[:,0],1],c="r",marker="+")
plt.scatter(X2[neg2[:,0],0],X2[neg2[:,0],1],c="y",marker="o")
plt.xlim(0,1)
plt.ylim(0.4,1)

Для реализации SVM с гауссовскими ядрами

classifier3 = SVC(kernel="rbf",gamma=30)
classifier3.fit(X2,y2.ravel())

Что касается параметров SVM с ядром rbf, он использует гамму вместо сигмы. Документацию по параметрам можно найти здесь. Я обнаружил, что гамма похожа на 1 / σ, но не совсем. Я надеюсь, что какой-нибудь эксперт в предметной области сможет дать мне представление об интерпретации этого гамма-члена. Что касается этого набора данных, я обнаружил, что значение гаммы 30 наиболее похоже на оптимизированные параметры в задании (в курсе сигма была 0,1).

plt.figure(figsize=(8,6))
plt.scatter(X2[pos2[:,0],0],X2[pos2[:,0],1],c="r",marker="+")
plt.scatter(X2[neg2[:,0],0],X2[neg2[:,0],1],c="y",marker="o")
# plotting the decision boundary
X_5,X_6 = np.meshgrid(np.linspace(X2[:,0].min(),X2[:,1].max(),num=100),np.linspace(X2[:,1].min(),X2[:,1].max(),num=100))
plt.contour(X_5,X_6,classifier3.predict(np.array([X_5.ravel(),X_6.ravel()]).T).reshape(X_5.shape),1,colors="b")
plt.xlim(0,1)
plt.ylim(0.4,1)

Что касается последнего набора данных в этой части, мы выполняем простую настройку гиперпараметров, чтобы определить лучшие значения C и гамма для использования.

Загрузка и построение набора данных примеров 3

mat3 = loadmat("ex6data3.mat")
X3 = mat3["X"]
y3 = mat3["y"]
Xval = mat3["Xval"]
yval = mat3["yval"]
m3,n3 = X3.shape[0],X3.shape[1]
pos3,neg3= (y3==1).reshape(m3,1), (y3==0).reshape(m3,1)
plt.figure(figsize=(8,6))
plt.scatter(X3[pos3[:,0],0],X3[pos3[:,0],1],c="r",marker="+",s=50)
plt.scatter(X3[neg3[:,0],0],X3[neg3[:,0],1],c="y",marker="o",s=50)

def dataset3Params(X, y, Xval, yval,vals):
    """
    Returns your choice of C and sigma. You should complete this function to return the optimal C and 
    sigma based on a cross-validation set.
    """
    acc = 0
    best_c=0
    best_gamma=0
    for i in vals:
        C= i
        for j in vals:
            gamma = 1/j
            classifier = SVC(C=C,gamma=gamma)
            classifier.fit(X,y)
            prediction = classifier.predict(Xval)
            score = classifier.score(Xval,yval)
            if score>acc:
                acc =score
                best_c =C
                best_gamma=gamma
    return best_c, best_gamma

dataset3Params выполняет итерацию по списку vals, заданному в функции, и устанавливает C как vals, а гамму как 1 / vals. Модель SVC строится с использованием каждой комбинации параметров, и вычисляется точность набора для проверки. На основе точности выбирается лучшая модель и возвращаются значения для соответствующих C и гаммы.

vals = [0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30]
C, gamma = dataset3Params(X3, y3.ravel(), Xval, yval.ravel(),vals)
classifier4 = SVC(C=C,gamma=gamma)
classifier4.fit(X3,y3.ravel())

plt.figure(figsize=(8,6))
plt.scatter(X3[pos3[:,0],0],X3[pos3[:,0],1],c="r",marker="+",s=50)
plt.scatter(X3[neg3[:,0],0],X3[neg3[:,0],1],c="y",marker="o",s=50)
# plotting the decision boundary
X_7,X_8 = np.meshgrid(np.linspace(X3[:,0].min(),X3[:,1].max(),num=100),np.linspace(X3[:,1].min(),X3[:,1].max(),num=100))
plt.contour(X_7,X_8,classifier4.predict(np.array([X_7.ravel(),X_8.ravel()]).T).reshape(X_7.shape),1,colors="b")
plt.xlim(-0.6,0.3)
plt.ylim(-0.7,0.5)

Оптимальные значения - 0,3 для C и 100 для гаммы, это приводит к такой же границе решения, как и при задании.

Переходим к классификации спама. Эта проблема уникальна, поскольку она больше ориентирована на предварительную обработку данных, чем на фактический процесс моделирования. Электронные письма должны обрабатываться таким образом, чтобы их можно было использовать в качестве входных данных для модели. Один из способов сделать это - получить индексы всех слов в электронном письме на основе списка обычно используемой лексики.

Загрузка данных

import re
from nltk.stem import PorterStemmer
file_contents = open("emailSample1.txt","r").read()
vocabList = open("vocab.txt","r").read()

Был дан список словаря и его соответствующие индексы, я сохранил список как словарь с словарями в качестве ключей и индексами в качестве значений. Возможно, вы могли бы сделать это по-другому, но я хочу упростить доступ к словарям (например, with if keys in dict)

vocabList=vocabList.split("\n")[:-1]
vocabList_d={}
for ea in vocabList:
    value,key = ea.split("\t")[:]
    vocabList_d[key] = value

Что касается предварительной обработки писем, в задании для нас было намечено несколько шагов.

def processEmail(email_contents,vocabList_d):
    """
    Preprocesses the body of an email and returns a list of indices of the words contained in the email. 
    """
    # Lower case
    email_contents = email_contents.lower()
    
    # Handle numbers
    email_contents = re.sub("[0-9]+","number",email_contents)
    
    # Handle URLS
    email_contents = re.sub("[http|https]://[^\s]*","httpaddr",email_contents)
    
    # Handle Email Addresses
    email_contents = re.sub("[^\s]+@[^\s]+","emailaddr",email_contents)
    
    # Handle $ sign
    email_contents = re.sub("[$]+","dollar",email_contents)
    
    # Strip all special characters
    specialChar = ["<","[","^",">","+","?","!","'",".",",",":"]
    for char in specialChar:
        email_contents = email_contents.replace(str(char),"")
    email_contents = email_contents.replace("\n"," ")    
    
    # Stem the word
    ps = PorterStemmer()
    email_contents = [ps.stem(token) for token in email_contents.split(" ")]
    email_contents= " ".join(email_contents)
    
    # Process the email and return word_indices
    
    word_indices=[]
    
    for char in email_contents.split():
        if len(char) >1 and char in vocabList_d:
            word_indices.append(int(vocabList_d[char]))
    
    return word_indices
word_indices= processEmail(file_contents,vocabList_d)

Использование регулярных выражений здесь очень удобно, этот учебник от гуру Python может помочь вам начать работу с re. Другая полезная библиотека - nlkt, где функция PorterStemmer() помогает с выделением слов. Еще один хороший учебник, на этот раз от pythonprogramming.net.

После получения индексов слов нам нужно преобразовать индексы в вектор признаков.

def emailFeatures(word_indices, vocabList_d):
    """
    Takes in a word_indices vector and  produces a feature vector from the word indices. 
    """
    n = len(vocabList_d)
    
    features = np.zeros((n,1))
    
    for i in word_indices:
        features[i] =1
        
    return features
features = emailFeatures(word_indices,vocabList_d)
print("Length of feature vector: ",len(features))
print("Number of non-zero entries: ",np.sum(features))

Оператор печати напечатает: Length of feature vector: 1899 и Number of non-zero entries: 43.0. Это немного отличается от присваивания, поскольку you’re был записан как «вы» и «re» в присвоении, в то время как мой код идентифицировал его как «ваш», что привело к меньшему количеству ненулевых записей.

Обучить SVM будет так же просто, как передать функции в качестве входных данных. Однако это только один обучающий пример, и нам потребуется больше обучающих данных для обучения классификатора.

spam_mat = loadmat("spamTrain.mat")
X_train =spam_mat["X"]
y_train = spam_mat["y"]

В spamTrain.mat приведены обучающие примеры для обучения нашего классификатора, а в spamTest.mat - тестовые примеры для определения обобщаемости нашей модели.

C =0.1
spam_svc = SVC(C=0.1,kernel ="linear")
spam_svc.fit(X_train,y_train.ravel())
print("Training Accuracy:",(spam_svc.score(X_train,y_train.ravel()))*100,"%")

Оператор печати напечатает: Training Accuracy: 99.825 %

spam_mat_test = loadmat("spamTest.mat")
X_test = spam_mat_test["Xtest"]
y_test =spam_mat_test["ytest"]
spam_svc.predict(X_test)
print("Test Accuracy:",(spam_svc.score(X_test,y_test.ravel()))*100,"%")

Оператор печати напечатает: Test Accuracy: 98.9 %

Чтобы лучше понять нашу модель, мы могли бы посмотреть на вес каждого слова и выяснить, какие слова наиболее предсказуемы для спама.

weights = spam_svc.coef_[0]
weights_col = np.hstack((np.arange(1,1900).reshape(1899,1),weights.reshape(1899,1)))
df = pd.DataFrame(weights_col)
df.sort_values(by=[1],ascending = False,inplace=True)
predictors = []
idx=[]
for i in df[0][:15]:
    for keys, values in vocabList_d.items():
        if str(int(i)) == values:
            predictors.append(keys)
            idx.append(int(values))
print("Top predictors of spam:")
for _ in range(15):
    print(predictors[_],"\t\t",round(df[1][idx[_]-1],6))

Вот и все, что касается опорных векторных машин! Записная книжка jupyter будет загружена на мой GitHub по адресу (https://github.com/Benlau93/Machine-Learning-by-Andrew-Ng-in-Python).

Для другой реализации Python в этой серии:

Спасибо за чтение.