Skip to content

Commit

Permalink
fix: actually use kernel of support vector machines for training (#681
Browse files Browse the repository at this point in the history
)

Closes #602

### Summary of Changes

Previously, support vector machines always used an RBF kernel,
regardless of the `kernel` requested by the user. This is fixed now.
  • Loading branch information
lars-reimann authored May 1, 2024
1 parent db2b613 commit 09c5082
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 222 deletions.
114 changes: 34 additions & 80 deletions src/safeds/ml/classical/classification/_support_vector_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import sys
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from safeds._utils import _structural_hash
from safeds.exceptions import ClosedBound, OpenBound, OutOfBoundsError
Expand All @@ -21,15 +21,8 @@ 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
-------
kernel:
The kernel of the SupportVectorMachine.
"""
def _get_sklearn_arguments(self) -> dict[str, Any]:
"""Return the arguments to pass to scikit-learn."""

@abstractmethod
def __eq__(self, other: object) -> bool:
Expand Down Expand Up @@ -80,16 +73,20 @@ def __hash__(self) -> int:
return _structural_hash(Classifier.__hash__(self), self._target_name, self._feature_names, self._c, self.kernel)

def __init__(self, *, c: float = 1.0, kernel: SupportVectorMachineKernel | None = None) -> None:
# Inputs
if c <= 0:
raise OutOfBoundsError(c, name="c", lower_bound=OpenBound(0))
if kernel is None:
kernel = self.Kernel.RadialBasisFunction()

# Internal state
self._wrapped_classifier: sk_SVC | None = None
self._feature_names: list[str] | None = None
self._target_name: str | None = None

# Hyperparameters
if c <= 0:
raise OutOfBoundsError(c, name="c", lower_bound=OpenBound(0))
self._c = c
self._kernel = kernel
self._c: float = c
self._kernel: SupportVectorMachineKernel = kernel

@property
def c(self) -> float:
Expand All @@ -104,7 +101,7 @@ def c(self) -> float:
return self._c

@property
def kernel(self) -> SupportVectorMachineKernel | None:
def kernel(self) -> SupportVectorMachineKernel:
"""
Get the type of kernel used.
Expand All @@ -117,16 +114,10 @@ def kernel(self) -> SupportVectorMachineKernel | None:

class Kernel:
class Linear(SupportVectorMachineKernel):
def _get_sklearn_kernel(self) -> str:
"""
Get the name of the linear kernel.
Returns
-------
result:
The name of the linear kernel.
"""
return "linear"
def _get_sklearn_arguments(self) -> dict[str, Any]:
return {
"kernel": "linear",
}

def __eq__(self, other: object) -> bool:
if not isinstance(other, SupportVectorMachineClassifier.Kernel.Linear):
Expand All @@ -141,16 +132,16 @@ def __init__(self, degree: int):
raise OutOfBoundsError(degree, name="degree", lower_bound=ClosedBound(1))
self._degree = degree

def _get_sklearn_kernel(self) -> str:
"""
Get the name of the polynomial kernel.
@property
def degree(self) -> int:
"""The degree of the polynomial kernel."""
return self._degree

Returns
-------
result:
The name of the polynomial kernel.
"""
return "poly"
def _get_sklearn_arguments(self) -> dict[str, Any]:
return {
"kernel": "poly",
"degree": self._degree,
}

def __eq__(self, other: object) -> bool:
if not isinstance(other, SupportVectorMachineClassifier.Kernel.Polynomial):
Expand All @@ -172,16 +163,10 @@ def __sizeof__(self) -> int:
return sys.getsizeof(self._degree)

class Sigmoid(SupportVectorMachineKernel):
def _get_sklearn_kernel(self) -> str:
"""
Get the name of the sigmoid kernel.
Returns
-------
result:
The name of the sigmoid kernel.
"""
return "sigmoid"
def _get_sklearn_arguments(self) -> dict[str, Any]:
return {
"kernel": "sigmoid",
}

def __eq__(self, other: object) -> bool:
if not isinstance(other, SupportVectorMachineClassifier.Kernel.Sigmoid):
Expand All @@ -191,16 +176,10 @@ def __eq__(self, other: object) -> bool:
__hash__ = SupportVectorMachineKernel.__hash__

class RadialBasisFunction(SupportVectorMachineKernel):
def _get_sklearn_kernel(self) -> str:
"""
Get the name of the radial basis function (RBF) kernel.
Returns
-------
result:
The name of the RBF kernel.
"""
return "rbf"
def _get_sklearn_arguments(self) -> dict[str, Any]:
return {
"kernel": "rbf",
}

def __eq__(self, other: object) -> bool:
if not isinstance(other, SupportVectorMachineClassifier.Kernel.RadialBasisFunction):
Expand All @@ -209,31 +188,6 @@ def __eq__(self, other: object) -> bool:

__hash__ = SupportVectorMachineKernel.__hash__

def _get_kernel_name(self) -> str:
"""
Get the name of the kernel.
Returns
-------
result:
The name of the kernel.
Raises
------
TypeError
If the kernel type is invalid.
"""
if isinstance(self.kernel, SupportVectorMachineClassifier.Kernel.Linear):
return "linear"
elif isinstance(self.kernel, SupportVectorMachineClassifier.Kernel.Polynomial):
return "poly"
elif isinstance(self.kernel, SupportVectorMachineClassifier.Kernel.Sigmoid):
return "sigmoid"
elif isinstance(self.kernel, SupportVectorMachineClassifier.Kernel.RadialBasisFunction):
return "rbf"
else:
raise TypeError("Invalid kernel type.")

def fit(self, training_set: TabularDataset) -> SupportVectorMachineClassifier:
"""
Create a copy of this classifier and fit it with the given training data.
Expand Down Expand Up @@ -322,4 +276,4 @@ def _get_sklearn_classifier(self) -> ClassifierMixin:
"""
from sklearn.svm import SVC as sk_SVC # noqa: N811

return sk_SVC(C=self._c)
return sk_SVC(C=self._c, **(self._kernel._get_sklearn_arguments()))
118 changes: 36 additions & 82 deletions src/safeds/ml/classical/regression/_support_vector_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import sys
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from safeds._utils import _structural_hash
from safeds.exceptions import ClosedBound, OpenBound, OutOfBoundsError
Expand All @@ -11,7 +11,7 @@

if TYPE_CHECKING:
from sklearn.base import RegressorMixin
from sklearn.svm import SVR as sk_SVR # noqa: N811
from sklearn.svm import SVC as sk_SVR # noqa: N811

from safeds.data.labeled.containers import TabularDataset
from safeds.data.tabular.containers import Table
Expand All @@ -21,15 +21,8 @@ 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
-------
kernel:
The kernel of the SupportVectorMachine.
"""
def _get_sklearn_arguments(self) -> dict[str, Any]:
"""Return the arguments to pass to scikit-learn."""

@abstractmethod
def __eq__(self, other: object) -> bool:
Expand Down Expand Up @@ -80,16 +73,20 @@ def __hash__(self) -> int:
return _structural_hash(Regressor.__hash__(self), self._target_name, self._feature_names, self._c, self.kernel)

def __init__(self, *, c: float = 1.0, kernel: SupportVectorMachineKernel | None = None) -> None:
# Inputs
if c <= 0:
raise OutOfBoundsError(c, name="c", lower_bound=OpenBound(0))
if kernel is None:
kernel = self.Kernel.RadialBasisFunction()

# Internal state
self._wrapped_regressor: sk_SVR | None = None
self._feature_names: list[str] | None = None
self._target_name: str | None = None

# Hyperparameters
if c <= 0:
raise OutOfBoundsError(c, name="c", lower_bound=OpenBound(0))
self._c = c
self._kernel = kernel
self._c: float = c
self._kernel: SupportVectorMachineKernel = kernel

@property
def c(self) -> float:
Expand All @@ -104,7 +101,7 @@ def c(self) -> float:
return self._c

@property
def kernel(self) -> SupportVectorMachineKernel | None:
def kernel(self) -> SupportVectorMachineKernel:
"""
Get the type of kernel used.
Expand All @@ -117,16 +114,10 @@ def kernel(self) -> SupportVectorMachineKernel | None:

class Kernel:
class Linear(SupportVectorMachineKernel):
def _get_sklearn_kernel(self) -> str:
"""
Get the name of the linear kernel.
Returns
-------
result:
The name of the linear kernel.
"""
return "linear"
def _get_sklearn_arguments(self) -> dict[str, Any]:
return {
"kernel": "linear",
}

def __eq__(self, other: object) -> bool:
if not isinstance(other, SupportVectorMachineRegressor.Kernel.Linear):
Expand All @@ -141,16 +132,16 @@ def __init__(self, degree: int):
raise OutOfBoundsError(degree, name="degree", lower_bound=ClosedBound(1))
self._degree = degree

def _get_sklearn_kernel(self) -> str:
"""
Get the name of the polynomial kernel.
@property
def degree(self) -> int:
"""The degree of the polynomial kernel."""
return self._degree

Returns
-------
result:
The name of the polynomial kernel.
"""
return "poly"
def _get_sklearn_arguments(self) -> dict[str, Any]:
return {
"kernel": "poly",
"degree": self._degree,
}

def __eq__(self, other: object) -> bool:
if not isinstance(other, SupportVectorMachineRegressor.Kernel.Polynomial):
Expand All @@ -172,16 +163,10 @@ def __sizeof__(self) -> int:
return sys.getsizeof(self._degree)

class Sigmoid(SupportVectorMachineKernel):
def _get_sklearn_kernel(self) -> str:
"""
Get the name of the sigmoid kernel.
Returns
-------
result:
The name of the sigmoid kernel.
"""
return "sigmoid"
def _get_sklearn_arguments(self) -> dict[str, Any]:
return {
"kernel": "sigmoid",
}

def __eq__(self, other: object) -> bool:
if not isinstance(other, SupportVectorMachineRegressor.Kernel.Sigmoid):
Expand All @@ -191,16 +176,10 @@ def __eq__(self, other: object) -> bool:
__hash__ = SupportVectorMachineKernel.__hash__

class RadialBasisFunction(SupportVectorMachineKernel):
def _get_sklearn_kernel(self) -> str:
"""
Get the name of the radial basis function (RBF) kernel.
Returns
-------
result:
The name of the RBF kernel.
"""
return "rbf"
def _get_sklearn_arguments(self) -> dict[str, Any]:
return {
"kernel": "rbf",
}

def __eq__(self, other: object) -> bool:
if not isinstance(other, SupportVectorMachineRegressor.Kernel.RadialBasisFunction):
Expand All @@ -209,31 +188,6 @@ def __eq__(self, other: object) -> bool:

__hash__ = SupportVectorMachineKernel.__hash__

def _get_kernel_name(self) -> str:
"""
Get the name of the kernel.
Returns
-------
result:
The name of the kernel.
Raises
------
TypeError
If the kernel type is invalid.
"""
if isinstance(self.kernel, SupportVectorMachineRegressor.Kernel.Linear):
return "linear"
elif isinstance(self.kernel, SupportVectorMachineRegressor.Kernel.Polynomial):
return "poly"
elif isinstance(self.kernel, SupportVectorMachineRegressor.Kernel.Sigmoid):
return "sigmoid"
elif isinstance(self.kernel, SupportVectorMachineRegressor.Kernel.RadialBasisFunction):
return "rbf"
else:
raise TypeError("Invalid kernel type.")

def fit(self, training_set: TabularDataset) -> SupportVectorMachineRegressor:
"""
Create a copy of this regressor and fit it with the given training data.
Expand Down Expand Up @@ -320,6 +274,6 @@ def _get_sklearn_regressor(self) -> RegressorMixin:
wrapped_regressor:
The sklearn Regressor.
"""
from sklearn.svm import SVR as sk_SVR # noqa: N811
from sklearn.svm import SVC as sk_SVR # noqa: N811

return sk_SVR(C=self._c)
return sk_SVR(C=self._c, **(self._kernel._get_sklearn_arguments()))
Loading

0 comments on commit 09c5082

Please sign in to comment.