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')