diff --git a/src/safeds/ml/classical/classification/_support_vector_machine.py b/src/safeds/ml/classical/classification/_support_vector_machine.py index 91048f660..5af9ee825 100644 --- a/src/safeds/ml/classical/classification/_support_vector_machine.py +++ b/src/safeds/ml/classical/classification/_support_vector_machine.py @@ -1,12 +1,12 @@ 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 @@ -14,6 +14,21 @@ 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. @@ -22,6 +37,7 @@ 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 ------ @@ -29,7 +45,7 @@ class SupportVectorMachine(Classifier): 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 @@ -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. @@ -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 diff --git a/src/safeds/ml/classical/regression/_support_vector_machine.py b/src/safeds/ml/classical/regression/_support_vector_machine.py index d2e26f11b..d22858261 100644 --- a/src/safeds/ml/classical/regression/_support_vector_machine.py +++ b/src/safeds/ml/classical/regression/_support_vector_machine.py @@ -1,12 +1,12 @@ 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 @@ -14,6 +14,21 @@ 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. @@ -22,6 +37,7 @@ 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 ------ @@ -29,7 +45,7 @@ class SupportVectorMachine(Regressor): 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 @@ -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. @@ -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 diff --git a/tests/safeds/ml/classical/classification/test_support_vector_machine.py b/tests/safeds/ml/classical/classification/test_support_vector_machine.py index 9033c80cf..d54b3d895 100644 --- a/tests/safeds/ml/classical/classification/test_support_vector_machine.py +++ b/tests/safeds/ml/classical/classification/test_support_vector_machine.py @@ -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() diff --git a/tests/safeds/ml/classical/regression/test_support_vector_machine.py b/tests/safeds/ml/classical/regression/test_support_vector_machine.py index 871c7a0ee..634c45901 100644 --- a/tests/safeds/ml/classical/regression/test_support_vector_machine.py +++ b/tests/safeds/ml/classical/regression/test_support_vector_machine.py @@ -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()