pie
ExamplesDocs

Topics

  • Canvas
  • Reactivity
  • Broadcasting
  • Autodiff
  • Animation
  • Sorting
  • Line fit
  • Convex hull
  • Live convex hull
  • Dijkstra's algorithm
  • k-means
  • Gaussian mixture
  • PCA
  • SVM
  • Differential equations
  • Python
unitgridOpacitycanvasColororiginXoriginY

Support vector machines

The line that splits two colors with the widest gap. Only the ringed points (the support vectors) decide it. Drag the points.

The first run takes a few seconds while scikit-learn loads.

pie.clear()
pie.scalar(0, out='gridOpacity')
import numpy as np
from functools import lru_cache
from sklearn.svm import SVC
red = [(-3, 1.5), (-2.5, -0.5), (-1.5, 2.5), (-3.5, -1.5), (-1, 0.8), (-2, -2.2), (-0.8, 3.2)]
blue = [(1.5, -1), (2.5, 1), (3, -2.5), (1, -3), (3.5, 0.5), (2, 2.5), (0.5, -1.8)]
labels = np.array([-1] * len(red) + [1] * len(blue))
pts = [pie.point(x, y, fill='crimson') for x, y in red] + [pie.point(x, y, fill='#2563eb') for x, y in blue]

@lru_cache(maxsize=4)
def fit(ps):
    """The line w·x + b = 0 and the support vectors. Cached, so a drag fits once, not once per node."""
    # A much larger C gets very slow when no line splits the colors.
    svm = SVC(kernel='linear', C=1000).fit(np.array(ps), labels)
    return svm.coef_[0], svm.intercept_[0], {int(i) for i in svm.support_}

def line_end(level, sign, *ps):
    """One end of the line w·x + b = level: 0 is the middle of the gap, -1 and 1 its edges."""
    w, b, _ = fit(ps)
    on_line = (level - b) * w / np.dot(w, w)
    along = np.array([-w[1], w[0]]) / np.linalg.norm(w)
    return tuple(on_line + sign * 20 * along)

def ring(i, *ps):
    """A ring's radius: 0.3 around a support vector, 0 (not drawn) around any other point."""
    return 0.3 if i in fit(ps)[2] else 0

def describe(*ps):
    w, b, support = fit(ps)
    wrong = int((labels * (np.array(ps) @ w + b) < 0).sum())
    if wrong:
        return f'no line splits them: {wrong} on the wrong side'
    return f'gap: {2 / np.linalg.norm(w):.2f} wide, {len(support)} support vectors'

end = pie.lift(line_end, pie.Point)
size = pie.lift(ring, pie.Scalar)

edges = [[end(level, sign, *pts, hidden=True) for sign in (-1, 1)] for level in (-1, 1)]
gap = 'rgba(13, 148, 136, 0.12)'
pie.triangle(edges[0][0], edges[0][1], edges[1][1], fill=gap, stroke='none')
pie.triangle(edges[0][0], edges[1][0], edges[1][1], fill=gap, stroke='none')
for low, high in edges:
    pie.line(low, high, stroke='#5eead4')
pie.line(end(0, -1, *pts, hidden=True), end(0, 1, *pts, hidden=True), stroke='teal')
for i, p in enumerate(pts):
    pie.circle(p, size(i, *pts), stroke='#1c1917')
pie.text(pie.lift(describe, str)(*pts), 0, 4.8, 14, stroke='#1c1917')
pie.text('Drag a ringed point to move the line', 0, -4.8, 14, stroke='#1c1917')