Machine Learning from scratch: Implementando o SVM (Máquinas de Vetores de Suporte) em Python

Tipo de aprendizado: Baseado em instância

Como o SVM funciona

Benefícios e aplicações do SVM

Benefícios do SVM

  1. Eficaz em altas dimensões:
    • O SVM é especialmente poderoso em situações onde os dados possuem muitas características (alta dimensionalidade). Devido à sua abordagem baseada em instância e uso de vetores de suporte, o SVM pode lidar eficientemente com espaços de grande dimensão, muitas vezes melhor que outros algoritmos.
  2. Utilização do truque do kernel:
    • O uso de kernels permite que o SVM resolva problemas complexos de classificação, mesmo quando as classes não são linearmente separáveis no espaço original. Com kernels como o polinomial, RBF (Radial Basis Function), e sigmoid, o SVM pode mapear os dados para um espaço de maior dimensão onde a separação linear é possível.
  3. Margem máxima para melhor generalização:
    • Ao maximizar a margem entre as classes, o SVM tende a generalizar melhor para dados novos, reduzindo o risco de overfitting (ajuste excessivo aos dados de treinamento). Isso é particularmente útil em datasets com um número limitado de exemplos.
  4. Flexibilidade com a regularização:
    • O parâmetro de regularização C permite um controle fino sobre a complexidade do modelo. Ele possibilita ajustar o modelo para encontrar um equilíbrio entre uma margem ampla (que pode levar a erros) e uma margem mais estreita (que pode levar a overfitting).
  5. Robustez contra outliers:
    • Devido ao foco nos vetores de suporte, o SVM é menos influenciado por outliers, especialmente quando o valor de C é ajustado adequadamente, o que permite ao modelo ignorar exemplos que estão muito longe das classes principais.

Aplicações do SVM

  1. Classificação de imagens: Como na identificação de dígitos manuscritos ou na categorização de objetos em imagens.
  2. Reconhecimento de padrões: Na biometria (reconhecimento facial, identificação de impressões digitais), se beneficiam da capacidade do SVM de separar classes complexas.
  3. Análise de sentimentos e Text mining: Pode ser utilizado para categorizar textos, e-mails, ou mesmo tweets em classes distintas (positivas, negativas, neutras), especialmente quando o espaço de características é grande (como em representações de texto).
  4. Bioinformática: Para classificar sequências de DNA, prever estruturas de proteínas e identificar genes de interesse.
  5. Detecção de fraude: Na identificação de transações fraudulentas, usam SVM para separar comportamentos normais de anômalos, graças à sua precisão e robustez em encontrar padrões em dados complexos.

Implementação from scratch do SVM em Python

import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
# Carrega os dados do dataset iris
iris = datasets.load_iris()
X = iris.data[:, :2]  # Apenas duas features
y = iris.target

# Transformando em um problema binário
y = np.where(y == 0, -1, 1)
  • No X estamos usando apenas 2 de 4 características do dataset Iris
  • No y vamos definir duas classes (Iris Setosa vs. Não-Iris Setosa)
# Plot dos dados
plt.figure()
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, edgecolor='k')
plt.show()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
class SVM:
    def __init__(self, learning_rate=0.001, lambda_param=0.01, n_iters=1000):
        self.lr = learning_rate
        self.lambda_param = lambda_param
        self.n_iters = n_iters
        self.w = None
        self.b = None

    def fit(self, X, y):
        n_samples, n_features = X.shape
        y_ = np.where(y <= 0, -1, 1)
        self.w = np.zeros(n_features)
        self.b = 0

        for _ in range(self.n_iters):
            for idx, x_i in enumerate(X):
                condition = y_[idx] * (np.dot(x_i, self.w) - self.b) >= 1
                if condition:
                    self.w -= self.lr * (2 * self.lambda_param * self.w)
                else:
                    self.w -= self.lr * (2 * self.lambda_param * self.w - np.dot(x_i, y_[idx]))
                    self.b -= self.lr * y_[idx]

    def predict(self, X):
        approx = np.dot(X, self.w) - self.b
        return np.sign(approx)
def __init__(self, learning_rate=0.001, lambda_param=0.01, n_iters=1000):
  self.lr = learning_rate
  self.lambda_param = lambda_param
  self.n_iters = n_iters
  self.w = None
  self.b = None
def fit(self, X, y):   
    n_samples, n_features = X.shape
    y_ = np.where(y <= 0, -1, 1)
    self.w = np.zeros(n_features)
    self.b = 0
n_samples, n_features = X_train.shape
# n_samples=120; n_features=2 
y_ = np.where(y_train <= 0, -1, 1)  # Converte labels para -1 e 1
w = np.zeros(n_features) # Cria um array para armazenar os pesos
b = 0 # bias
for _ in range(self.n_iters):
    for idx, x_i in enumerate(X):
      condition = y_[idx] * (np.dot(x_i, self.w) - self.b) >= 1
      if condition:
        self.w -= self.lr * (2 * self.lambda_param * self.w)
      else:
        self.w -= self.lr * (2 * self.lambda_param * self.w - np.dot(x_i, y_[idx]))
        self.b -= self.lr * y_[idx]
for _ in range(self.n_iters):
for idx, x_i in enumerate(X):
condition = y_[idx] * (np.dot(x_i, self.w) - self.b) >= 1

Cálculo:

  • np.dot(x_i, self.w): Calcula o produto escalar entre a amostra x_i e os pesos w, que corresponde à projeção da amostra nos pesos atuais.
  • - self.b: Subtrai o bias (b), que ajuda a ajustar a fronteira de decisão.
  • y_[idx] * (...): Multiplica o resultado da projeção pelo label y_[idx]. Se a amostra estiver corretamente classificada, o valor do produto deve ser maior ou igual a 1, atendendo à condição de margem máxima.

condition: A variável condition será True se a amostra estiver corretamente classificada e False se não estiver.

if condition:
    self.w -= self.lr * (2 * self.lambda_param * self.w)

Regularização:

  • 2 * self.lambda_param * self.w: Este termo é uma penalização para evitar que os pesos cresçam demais, controlando o overfitting. lambda_param é o parâmetro de regularização que determina a intensidade dessa penalização.
  • self.lr * (...): Multiplica pela taxa de aprendizado (lr), que controla o tamanho do passo de ajuste dos pesos.
  • self.w -= ...: Atualiza os pesos, diminuindo-os levemente para garantir que não fiquem excessivamente grandes.
else:
    self.w -= self.lr * (2 * self.lambda_param * self.w - np.dot(x_i, y_[idx]))
    self.b -= self.lr * y_[idx]

Atualização dos Pesos:

  • 2 * self.lambda_param * self.w: Termo de regularização como descrito anteriormente.
  • np.dot(x_i, y_[idx]): Este termo penaliza a amostra mal classificada ajustando os pesos na direção correta para minimizar o erro. O valor de np.dot(x_i, y_[idx]) é subtraído, aumentando ou diminuindo w dependendo de como a amostra foi classificada.
  • self.w -= ...: Atualiza os pesos levando em consideração tanto a regularização quanto o erro de classificação.

Atualização do Bias:

  • self.b -= self.lr * y_[idx]: Ajusta o bias b de acordo com o erro de classificação. Se a amostra foi mal classificada, o bias é ajustado para melhorar a classificação dessa e de outras amostras similares.
Checking idx: 2
Classe incorreta, peso W após atualização: [ 1.04878727 -1.90865944]
Checking idx: 2
Classe incorreta, peso W após atualização: [ 0.98157752 -1.93927771]
Checking idx: 2
Classe incorreta, peso W após atualização: [ 0.9143812  -1.96988985]
Checking idx: 2
Classe incorreta, peso W após atualização: [ 0.84719833 -2.00049588]
Checking idx: 2
Classe incorreta, peso W após atualização: [ 0.78002889 -2.03109578]
Checking idx: 2
Classe correta, peso W após atualização: [ 0.77987288 -2.03068956]
Checking idx: 2
Classe correta, peso W após atualização: [ 0.77971691 -2.03028342]
Checking idx: 2
Classe correta, peso W após atualização: [ 0.77956096 -2.02987736]
Checking idx: 2
Classe correta, peso W após atualização: [ 0.77940505 -2.02947139]
Checking idx: 2
Classe correta, peso W após atualização: [ 0.77924917 -2.02906549]
def predict(self, X):
        approx = np.dot(X, self.w) - self.b
        return np.sign(approx)
approx = np.dot(X, w) - self.b

Cálculo:

  • np.dot(X, self.w): Calcula o produto escalar entre cada amostra em X e os pesos w do modelo. Se X tem n amostras e m features, X será uma matriz de dimensão (n, m) e w será um vetor de dimensão (m,). O resultado de np.dot(X, self.w) será um vetor de dimensão (n,), onde cada elemento representa a projeção da respectiva amostra nos pesos.
  • - self.b: Subtrai o bias b de cada projeção. O bias ajusta a fronteira de decisão do modelo, deslocando-a para melhor separar as classes.
  • Resultado: A variável approx contém os valores calculados para cada amostra, indicando sua posição relativa em relação à fronteira de decisão.
predict(X_train[2:3])
array([-1.]) # Iris setosa
svm = SVM()
svm.fit(X_train, y_train)
predictions = svm.predict(X_test)
[ 1. -1.  1.  1.  1. -1.  1.  1.  1.  1.  1. -1. -1. -1. -1.  1.  1.  1.
  1.  1. -1.  1. -1.  1.  1.  1.  1.  1. -1. -1.]
accuracy = np.mean(predictions == y_test)
print(f"Acurácia: {accuracy * 100:.2f}%")
def plot_decision_boundary(X, y, model):
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.01),
                         np.arange(y_min, y_max, 0.01))
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    plt.contourf(xx, yy, Z, alpha=0.3)
    plt.scatter(X[:, 0], X[:, 1], c=y, s=40, edgecolor='k')
    plt.show()

plot_decision_boundary(X_test, y_test, svm)

Conclusão

SVM em detalhes
Documentação oficial

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Rolar para cima
×