Skip to content

Commit

Permalink
Merge pull request #1891 from pavlin-policar/merge-svm
Browse files Browse the repository at this point in the history
Merge OWSVM
  • Loading branch information
janezd authored Feb 3, 2017
2 parents d8f221a + e9d0baa commit d5841f7
Show file tree
Hide file tree
Showing 11 changed files with 471 additions and 245 deletions.
1 change: 1 addition & 0 deletions Orange/modelling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from .tree import *
from .ada_boost import *
from .randomforest import *
from .svm import *
15 changes: 15 additions & 0 deletions Orange/modelling/svm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from Orange.classification import SVMLearner, LinearSVMLearner, NuSVMLearner
from Orange.modelling import Fitter
from Orange.regression import SVRLearner, LinearSVRLearner, NuSVRLearner


class SVMFitter(Fitter):
__fits__ = {'classification': SVMLearner, 'regression': SVRLearner}


class LinearSVMFitter(Fitter):
__fits__ = {'classification': LinearSVMLearner, 'regression': LinearSVRLearner}


class NuSVMFitter(Fitter):
__fits__ = {'classification': NuSVMLearner, 'regression': NuSVRLearner}
15 changes: 13 additions & 2 deletions Orange/regression/svm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from Orange.regression import SklLearner
from Orange.preprocess import Normalize

__all__ = ["SVRLearner", "NuSVRLearner"]
__all__ = ["SVRLearner", "LinearSVRLearner", "NuSVRLearner"]

svm_pps = SklLearner.preprocessors + [Normalize()]

Expand All @@ -19,6 +19,17 @@ def __init__(self, kernel='rbf', degree=3, gamma="auto", coef0=0.0,
self.params = vars()


class LinearSVRLearner(SklLearner):
__wraps__ = skl_svm.LinearSVR
preprocessors = svm_pps

def __init__(self, epsilon=0., tol=.0001, C=1., loss='epsilon_insensitive',
fit_intercept=True, intercept_scaling=1., dual=True,
random_state=None, max_iter=1000, preprocessors=None):
super().__init__(preprocessors=preprocessors)
self.params = vars()


class NuSVRLearner(SklLearner):
__wraps__ = skl_svm.NuSVR
preprocessors = svm_pps
Expand All @@ -34,7 +45,7 @@ def __init__(self, nu=0.5, C=1.0, kernel='rbf', degree=3, gamma="auto",
import Orange

data = Orange.data.Table('housing')
learners = [SVRLearner(), NuSVRLearner()]
learners = [SVRLearner(), LinearSVRLearner(), NuSVRLearner()]
res = Orange.evaluation.CrossValidation(data, learners)
for l, ca in zip(learners, Orange.evaluation.RMSE(res)):
print("learner: {}\nRMSE: {}\n".format(l, ca))
Expand Down
9 changes: 9 additions & 0 deletions Orange/tests/test_svm.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ def test_SVR(self):
res = CrossValidation(data, [learn], k=2)
self.assertLess(RMSE(res)[0], 0.15)

def test_LinearSVR(self):
nrows, ncols = 200, 5
X = np.random.rand(nrows, ncols)
y = X.dot(np.random.rand(ncols))
data = Table(X, y)
learn = SVRLearner()
res = CrossValidation(data, [learn], k=2)
self.assertLess(RMSE(res)[0], 0.15)

def test_NuSVR(self):
nrows, ncols = 200, 5
X = np.random.rand(nrows, ncols)
Expand Down
183 changes: 13 additions & 170 deletions Orange/widgets/classify/owsvmclassification.py
Original file line number Diff line number Diff line change
@@ -1,147 +1,24 @@
from collections import OrderedDict

from AnyQt import QtWidgets
from AnyQt.QtWidgets import QLabel
from AnyQt.QtCore import Qt

from AnyQt.QtWidgets import QLabel, QGridLayout
from Orange.data import Table
from Orange.classification.svm import SVMLearner, NuSVMLearner
from Orange.widgets import widget, settings, gui
from Orange.widgets.utils.owlearnerwidget import OWBaseLearner


class OWBaseSVM(OWBaseLearner):
#: Kernel types
Linear, Poly, RBF, Sigmoid = 0, 1, 2, 3
#: Selected kernel type
kernel_type = settings.Setting(RBF)
#: kernel degree
degree = settings.Setting(3)
#: gamma
gamma = settings.Setting(0.0)
#: coef0 (adative constant)
coef0 = settings.Setting(0.0)

#: numerical tolerance
tol = settings.Setting(0.001)

_default_gamma = "auto"
kernels = (("Linear", "x⋅y"),
("Polynomial", "(g x⋅y + c)<sup>d</sup>"),
("RBF", "exp(-g|x-y|²)"),
("Sigmoid", "tanh(g x⋅y + c)"))

def _add_kernel_box(self):
# Initialize with the widest label to measure max width
self.kernel_eq = self.kernels[-1][1]

box = gui.hBox(self.controlArea, "Kernel")

self.kernel_box = buttonbox = gui.radioButtonsInBox(
box, self, "kernel_type", btnLabels=[k[0] for k in self.kernels],
callback=self._on_kernel_changed, addSpace=20)
buttonbox.layout().setSpacing(10)
gui.rubber(buttonbox)

parambox = gui.vBox(box)
gui.label(parambox, self, "Kernel: %(kernel_eq)s")
common = dict(orientation=Qt.Horizontal, callback=self.settings_changed,
alignment=Qt.AlignRight, controlWidth=80)
spbox = gui.hBox(parambox)
gui.rubber(spbox)
inbox = gui.vBox(spbox)
gamma = gui.doubleSpin(
inbox, self, "gamma", 0.0, 10.0, 0.01, label=" g: ", **common)
gamma.setSpecialValueText(self._default_gamma)
coef0 = gui.doubleSpin(
inbox, self, "coef0", 0.0, 10.0, 0.01, label=" c: ", **common)
degree = gui.doubleSpin(
inbox, self, "degree", 0.0, 10.0, 0.5, label=" d: ", **common)
self._kernel_params = [gamma, coef0, degree]
gui.rubber(parambox)

# This is the maximal height (all double spins are visible)
# and the maximal width (the label is initialized to the widest one)
box.layout().activate()
box.setFixedHeight(box.sizeHint().height())
box.setMinimumWidth(box.sizeHint().width())

def _add_optimization_box(self):
self.optimization_box = gui.vBox(
self.controlArea, "Optimization Parameters")
self.tol_spin = gui.doubleSpin(
self.optimization_box, self, "tol", 1e-6, 1.0, 1e-5,
label="Numerical tolerance:",
decimals=6, alignment=Qt.AlignRight, controlWidth=100,
callback=self.settings_changed)
from Orange.modelling import SVMFitter
from Orange.widgets import gui, widget
from Orange.widgets.model.owsvm import OWSVM

def add_main_layout(self):
self._add_type_box()
self._add_kernel_box()
self._add_optimization_box()
self._show_right_kernel()

def _show_right_kernel(self):
enabled = [[False, False, False], # linear
[True, True, True], # poly
[True, False, False], # rbf
[True, True, False]] # sigmoid

self.kernel_eq = self.kernels[self.kernel_type][1]
mask = enabled[self.kernel_type]
for spin, enabled in zip(self._kernel_params, mask):
[spin.box.hide, spin.box.show][enabled]()

def _on_kernel_changed(self):
self._show_right_kernel()
self.settings_changed()

def _report_kernel_parameters(self, items):
gamma = self.gamma or self._default_gamma
if self.kernel_type == 0:
items["Kernel"] = "Linear"
elif self.kernel_type == 1:
items["Kernel"] = \
"Polynomial, ({g:.4} x⋅y + {c:.4})<sup>{d}</sup>".format(
g=gamma, c=self.coef0, d=self.degree)
elif self.kernel_type == 2:
items["Kernel"] = "RBF, exp(-{:.4}|x-y|²)".format(gamma)
else:
items["Kernel"] = "Sigmoid, tanh({g:.4} x⋅y + {c:.4})".format(
g=gamma, c=self.coef0)

def update_model(self):
super().update_model()
sv = None
if self.model is not None:
sv = self.data[self.model.skl_model.support_]
self.send("Support vectors", sv)


class OWSVMClassification(OWBaseSVM):
name = "SVM"
class OWSVM(OWSVM):
name = "SVM Classification"
description = "Support Vector Machines map inputs to higher-dimensional " \
"feature spaces that best separate different classes. "
icon = "icons/SVM.svg"
priority = 50

LEARNER = SVMLearner

outputs = [("Support vectors", Table, widget.Explicit)]

C_SVC, Nu_SVC = 0, 1
svmtype = settings.Setting(0)
C = settings.Setting(1.0)
nu = settings.Setting(0.5)
shrinking = settings.Setting(True),
probability = settings.Setting(False)
max_iter = settings.Setting(100)
limit_iter = settings.Setting(True)
LEARNER = SVMFitter

def _add_type_box(self):
form = QtWidgets.QGridLayout()
form = QGridLayout()
self.type_box = box = gui.radioButtonsInBox(
self.controlArea, self, "svmtype", [], box="SVM Type",
self.controlArea, self, "svm_type", [], box="SVM Type",
orientation=form, callback=self.settings_changed)

self.c_radio = gui.appendRadioButton(box, "C-SVM", addToLayout=False)
Expand All @@ -161,46 +38,12 @@ def _add_type_box(self):
form.addWidget(QLabel("Complexity (ν):"), 1, 1, Qt.AlignRight)
form.addWidget(self.nu_spin, 1, 2)

def _add_optimization_box(self):
super()._add_optimization_box()
self.max_iter_spin = gui.spin(
self.optimization_box, self, "max_iter", 50, 1e6, 50,
label="Iteration limit:", checked="limit_iter",
alignment=Qt.AlignRight, controlWidth=100,
callback=self.settings_changed)

def create_learner(self):
kernel = ["linear", "poly", "rbf", "sigmoid"][self.kernel_type]
common_args = dict(
kernel=kernel,
degree=self.degree,
gamma=self.gamma or self._default_gamma,
coef0=self.coef0,
tol=self.tol,
max_iter=self.max_iter if self.limit_iter else -1,
probability=True,
preprocessors=self.preprocessors
)
if self.svmtype == OWSVMClassification.C_SVC:
return SVMLearner(C=self.C, **common_args)
else:
return NuSVMLearner(nu=self.nu, **common_args)

def get_learner_parameters(self):
items = OrderedDict()
if self.svmtype == OWSVMClassification.C_SVC:
items["SVM type"] = "C-SVM, C={}".format(self.C)
else:
items["SVM type"] = "ν-SVM, ν={}".format(self.nu)
self._report_kernel_parameters(items)
items["Numerical tolerance"] = "{:.6}".format(self.tol)
items["Iteration limt"] = self.max_iter if self.limit_iter else "unlimited"
return items


if __name__ == "__main__":
app = QtWidgets.QApplication([])
w = OWSVMClassification()
from AnyQt.QtWidgets import QApplication

app = QApplication([])
w = OWSVM()
w.set_data(Table("iris")[:50])
w.show()
app.exec_()
18 changes: 11 additions & 7 deletions Orange/widgets/classify/tests/test_owsvmclassification.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# Test methods with long descriptive names can omit docstrings
# pylint: disable=missing-docstring
from Orange.widgets.classify.owsvmclassification import OWSVMClassification
from Orange.widgets.tests.base import (WidgetTest, DefaultParameterMapping,
ParameterMapping, WidgetLearnerTestMixin)
from Orange.widgets.classify.owsvmclassification import OWSVM
from Orange.widgets.tests.base import (
WidgetTest,
DefaultParameterMapping,
ParameterMapping,
WidgetLearnerTestMixin
)


class TestOWSVMClassification(WidgetTest, WidgetLearnerTestMixin):
def setUp(self):
self.widget = self.create_widget(OWSVMClassification,
stored_settings={"auto_apply": False})
self.widget = self.create_widget(
OWSVM, stored_settings={"auto_apply": False})
self.init()
gamma_spin = self.widget._kernel_params[0]
values = [self.widget._default_gamma, gamma_spin.maximum()]
Expand Down Expand Up @@ -45,10 +49,10 @@ def test_parameters_svm_type(self):
"""Check learner and model for various values of all parameters
when NuSVM is chosen
"""
self.assertEqual(self.widget.svmtype, OWSVMClassification.C_SVC)
self.assertEqual(self.widget.svm_type, OWSVM.SVM)
# setChecked(True) does not trigger callback event
self.widget.nu_radio.click()
self.assertEqual(self.widget.svmtype, OWSVMClassification.Nu_SVC)
self.assertEqual(self.widget.svm_type, OWSVM.Nu_SVM)
self.parameters[0] = ParameterMapping("nu", self.widget.nu_spin)
self.test_parameters()

Expand Down
53 changes: 53 additions & 0 deletions Orange/widgets/model/icons/SVM.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit d5841f7

Please sign in to comment.