Skip to content

Commit

Permalink
feat: set kernel of support vector machine (#350)
Browse files Browse the repository at this point in the history
Closes #172.

A new parameter kernel: SupportVectorMachineKernel was added to the
initializer of SupportVectorMachine classes of both the classifier and
the regressor alongside getters for the parameters. It was later passed
as the kernel of the wrapped scikit-learn model in the fit method.
The SupportVectorMachineKernel was created as an abstract base class
from which the signatures of the methods in the nested subclasses of
Kernel were the same.
Tests were adjusted to test the functionality of the kernel parameter.
  • Loading branch information
jxnior01 committed Jun 10, 2023
1 parent 34b9593 commit 1326f40
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 8 deletions.
63 changes: 59 additions & 4 deletions src/safeds/ml/classical/classification/_support_vector_machine.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

from sklearn.svm import SVC as sk_SVC # noqa: N811

from safeds.ml.classical._util_sklearn import fit, predict

from ._classifier import Classifier
from safeds.ml.classical.classification import Classifier

if TYPE_CHECKING:
from sklearn.base import ClassifierMixin

from safeds.data.tabular.containers import Table, TaggedTable


class SupportVectorMachineKernel(ABC):
"""The abstract base class of the different subclasses supported by the `Kernel`."""

@abstractmethod
def get_sklearn_kernel(self) -> object:
"""
Get the kernel of the given SupportVectorMachine.
Returns
-------
object
The kernel of the SupportVectorMachine.
"""


class SupportVectorMachine(Classifier):
"""
Support vector machine.
Expand All @@ -22,14 +37,15 @@ class SupportVectorMachine(Classifier):
----------
c: float
The strength of regularization. Must be strictly positive.
kernel: The type of kernel to be used. Defaults to None.
Raises
------
ValueError
If `c` is less than or equal to 0.
"""

def __init__(self, *, c: float = 1.0) -> None:
def __init__(self, *, c: float = 1.0, kernel: SupportVectorMachineKernel | None = None) -> None:
# Internal state
self._wrapped_classifier: sk_SVC | None = None
self._feature_names: list[str] | None = None
Expand All @@ -39,11 +55,50 @@ def __init__(self, *, c: float = 1.0) -> None:
if c <= 0:
raise ValueError("The parameter 'c' has to be strictly positive.")
self._c = c
self._kernel = kernel

@property
def c(self) -> float:
return self._c

@property
def kernel(self) -> SupportVectorMachineKernel | None:
return self._kernel

class Kernel:
class Linear(SupportVectorMachineKernel):
def get_sklearn_kernel(self) -> str:
return "linear"

class Polynomial(SupportVectorMachineKernel):
def __init__(self, degree: int):
if degree < 1:
raise ValueError("The parameter 'degree' has to be greater than or equal to 1.")
self._degree = degree

def get_sklearn_kernel(self) -> str:
return "poly"

class Sigmoid(SupportVectorMachineKernel):
def get_sklearn_kernel(self) -> str:
return "sigmoid"

class RadialBasisFunction(SupportVectorMachineKernel):
def get_sklearn_kernel(self) -> str:
return "rbf"

def _get_kernel_name(self) -> str:
if isinstance(self.kernel, SupportVectorMachine.Kernel.Linear):
return "linear"
elif isinstance(self.kernel, SupportVectorMachine.Kernel.Polynomial):
return "poly"
elif isinstance(self.kernel, SupportVectorMachine.Kernel.Sigmoid):
return "sigmoid"
elif isinstance(self.kernel, SupportVectorMachine.Kernel.RadialBasisFunction):
return "rbf"
else:
raise TypeError("Invalid kernel type.")

def fit(self, training_set: TaggedTable) -> SupportVectorMachine:
"""
Create a copy of this classifier and fit it with the given training data.
Expand All @@ -68,7 +123,7 @@ def fit(self, training_set: TaggedTable) -> SupportVectorMachine:
wrapped_classifier = self._get_sklearn_classifier()
fit(wrapped_classifier, training_set)

result = SupportVectorMachine(c=self._c)
result = SupportVectorMachine(c=self._c, kernel=self._kernel)
result._wrapped_classifier = wrapped_classifier
result._feature_names = training_set.features.column_names
result._target_name = training_set.target.name
Expand Down
63 changes: 59 additions & 4 deletions src/safeds/ml/classical/regression/_support_vector_machine.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

from sklearn.svm import SVR as sk_SVR # noqa: N811

from safeds.ml.classical._util_sklearn import fit, predict

from ._regressor import Regressor
from safeds.ml.classical.regression import Regressor

if TYPE_CHECKING:
from sklearn.base import RegressorMixin

from safeds.data.tabular.containers import Table, TaggedTable


class SupportVectorMachineKernel(ABC):
"""The abstract base class of the different subclasses supported by the `Kernel`."""

@abstractmethod
def get_sklearn_kernel(self) -> object:
"""
Get the kernel of the given SupportVectorMachine.
Returns
-------
object
The kernel of the SupportVectorMachine.
"""


class SupportVectorMachine(Regressor):
"""
Support vector machine.
Expand All @@ -22,14 +37,15 @@ class SupportVectorMachine(Regressor):
----------
c: float
The strength of regularization. Must be strictly positive.
kernel: The type of kernel to be used. Defaults to None.
Raises
------
ValueError
If `c` is less than or equal to 0.
"""

def __init__(self, *, c: float = 1.0) -> None:
def __init__(self, *, c: float = 1.0, kernel: SupportVectorMachineKernel | None = None) -> None:
# Internal state
self._wrapped_regressor: sk_SVR | None = None
self._feature_names: list[str] | None = None
Expand All @@ -39,11 +55,50 @@ def __init__(self, *, c: float = 1.0) -> None:
if c <= 0:
raise ValueError("The parameter 'c' has to be strictly positive.")
self._c = c
self._kernel = kernel

@property
def c(self) -> float:
return self._c

@property
def kernel(self) -> SupportVectorMachineKernel | None:
return self._kernel

class Kernel:
class Linear(SupportVectorMachineKernel):
def get_sklearn_kernel(self) -> str:
return "linear"

class Polynomial(SupportVectorMachineKernel):
def __init__(self, degree: int):
if degree < 1:
raise ValueError("The parameter 'degree' has to be greater than or equal to 1.")
self._degree = degree

def get_sklearn_kernel(self) -> str:
return "poly"

class Sigmoid(SupportVectorMachineKernel):
def get_sklearn_kernel(self) -> str:
return "sigmoid"

class RadialBasisFunction(SupportVectorMachineKernel):
def get_sklearn_kernel(self) -> str:
return "rbf"

def _get_kernel_name(self) -> str:
if isinstance(self.kernel, SupportVectorMachine.Kernel.Linear):
return "linear"
elif isinstance(self.kernel, SupportVectorMachine.Kernel.Polynomial):
return "poly"
elif isinstance(self.kernel, SupportVectorMachine.Kernel.Sigmoid):
return "sigmoid"
elif isinstance(self.kernel, SupportVectorMachine.Kernel.RadialBasisFunction):
return "rbf"
else:
raise TypeError("Invalid kernel type.")

def fit(self, training_set: TaggedTable) -> SupportVectorMachine:
"""
Create a copy of this regressor and fit it with the given training data.
Expand All @@ -68,7 +123,7 @@ def fit(self, training_set: TaggedTable) -> SupportVectorMachine:
wrapped_regressor = self._get_sklearn_regressor()
fit(wrapped_regressor, training_set)

result = SupportVectorMachine(c=self._c)
result = SupportVectorMachine(c=self._c, kernel=self._kernel)
result._wrapped_regressor = wrapped_regressor
result._feature_names = training_set.features.column_names
result._target_name = training_set.target.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,62 @@ def test_should_raise_if_less_than_or_equal_to_0(self) -> None:
match="The parameter 'c' has to be strictly positive.",
):
SupportVectorMachine(c=-1)


class TestKernel:
def test_should_be_passed_to_fitted_model(self, training_set: TaggedTable) -> None:
kernel = SupportVectorMachine.Kernel.Linear()
fitted_model = SupportVectorMachine(c=2, kernel=kernel).fit(training_set=training_set)
assert isinstance(fitted_model.kernel, SupportVectorMachine.Kernel.Linear)

def test_should_be_passed_to_sklearn(self, training_set: TaggedTable) -> None:
kernel = SupportVectorMachine.Kernel.Linear()
fitted_model = SupportVectorMachine(c=2, kernel=kernel).fit(training_set)
assert fitted_model._wrapped_classifier is not None
assert isinstance(fitted_model.kernel, SupportVectorMachine.Kernel.Linear)

def test_should_get_sklearn_kernel_linear(self) -> None:
svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Linear())
assert isinstance(svm.kernel, SupportVectorMachine.Kernel.Linear)
linear_kernel = svm.kernel.get_sklearn_kernel()
assert linear_kernel == "linear"

def test_should_raise_if_degree_less_than_1(self) -> None:
with pytest.raises(ValueError, match="The parameter 'degree' has to be greater than or equal to 1."):
SupportVectorMachine.Kernel.Polynomial(degree=0)

def test_should_get_sklearn_kernel_polynomial(self) -> None:
svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Polynomial(degree=2))
assert isinstance(svm.kernel, SupportVectorMachine.Kernel.Polynomial)
poly_kernel = svm.kernel.get_sklearn_kernel()
assert poly_kernel == "poly"

def test_should_get_sklearn_kernel_sigmoid(self) -> None:
svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Sigmoid())
assert isinstance(svm.kernel, SupportVectorMachine.Kernel.Sigmoid)
sigmoid_kernel = svm.kernel.get_sklearn_kernel()
assert sigmoid_kernel == "sigmoid"

def test_should_get_sklearn_kernel_rbf(self) -> None:
svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.RadialBasisFunction())
assert isinstance(svm.kernel, SupportVectorMachine.Kernel.RadialBasisFunction)
rbf_kernel = svm.kernel.get_sklearn_kernel()
assert rbf_kernel == "rbf"

def test_should_get_kernel_name(self) -> None:
svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Linear())
assert svm._get_kernel_name() == "linear"

svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Polynomial(degree=2))
assert svm._get_kernel_name() == "poly"

svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Sigmoid())
assert svm._get_kernel_name() == "sigmoid"

svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.RadialBasisFunction())
assert svm._get_kernel_name() == "rbf"

def test_should_get_kernel_name_invalid_kernel_type(self) -> None:
svm = SupportVectorMachine(c=2)
with pytest.raises(TypeError, match="Invalid kernel type."):
svm._get_kernel_name()
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,62 @@ def test_should_raise_if_less_than_or_equal_to_0(self) -> None:
match="The parameter 'c' has to be strictly positive.",
):
SupportVectorMachine(c=-1)


class TestKernel:
def test_should_be_passed_to_fitted_model(self, training_set: TaggedTable) -> None:
kernel = SupportVectorMachine.Kernel.Linear()
fitted_model = SupportVectorMachine(c=2, kernel=kernel).fit(training_set=training_set)
assert isinstance(fitted_model.kernel, SupportVectorMachine.Kernel.Linear)

def test_should_be_passed_to_sklearn(self, training_set: TaggedTable) -> None:
kernel = SupportVectorMachine.Kernel.Linear()
fitted_model = SupportVectorMachine(c=2, kernel=kernel).fit(training_set)
assert fitted_model._wrapped_regressor is not None
assert isinstance(fitted_model.kernel, SupportVectorMachine.Kernel.Linear)

def test_should_get_sklearn_kernel_linear(self) -> None:
svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Linear())
assert isinstance(svm.kernel, SupportVectorMachine.Kernel.Linear)
linear_kernel = svm.kernel.get_sklearn_kernel()
assert linear_kernel == "linear"

def test_should_raise_if_degree_less_than_1(self) -> None:
with pytest.raises(ValueError, match="The parameter 'degree' has to be greater than or equal to 1."):
SupportVectorMachine.Kernel.Polynomial(degree=0)

def test_should_get_sklearn_kernel_polynomial(self) -> None:
svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Polynomial(degree=2))
assert isinstance(svm.kernel, SupportVectorMachine.Kernel.Polynomial)
poly_kernel = svm.kernel.get_sklearn_kernel()
assert poly_kernel == "poly"

def test_should_get_sklearn_kernel_sigmoid(self) -> None:
svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Sigmoid())
assert isinstance(svm.kernel, SupportVectorMachine.Kernel.Sigmoid)
sigmoid_kernel = svm.kernel.get_sklearn_kernel()
assert sigmoid_kernel == "sigmoid"

def test_should_get_sklearn_kernel_rbf(self) -> None:
svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.RadialBasisFunction())
assert isinstance(svm.kernel, SupportVectorMachine.Kernel.RadialBasisFunction)
rbf_kernel = svm.kernel.get_sklearn_kernel()
assert rbf_kernel == "rbf"

def test_should_get_kernel_name(self) -> None:
svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Linear())
assert svm._get_kernel_name() == "linear"

svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Polynomial(degree=2))
assert svm._get_kernel_name() == "poly"

svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.Sigmoid())
assert svm._get_kernel_name() == "sigmoid"

svm = SupportVectorMachine(c=2, kernel=SupportVectorMachine.Kernel.RadialBasisFunction())
assert svm._get_kernel_name() == "rbf"

def test_should_get_kernel_name_invalid_kernel_type(self) -> None:
svm = SupportVectorMachine(c=2)
with pytest.raises(TypeError, match="Invalid kernel type."):
svm._get_kernel_name()

0 comments on commit 1326f40

Please sign in to comment.