Критерии переобучения и регуляризация#
(Здесь и далее перезапускайте ячейку перед взаимодействием с графиками. Используются одинаковые глобальные переменные)
Вспомним основную проблему - переобучение. Характерный пример:
from bokeh.io import output_notebook
from bokeh.resources import INLINE
output_notebook(INLINE)
Show code cell source
import numpy as np
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.layouts import column
from bokeh.models import (
Slider, ColumnDataSource, Div, CustomJS, Range1d
)
from bokeh.resources import INLINE
# ——————————————————————————————————————————
# Вывод прямо в блокнот без баннера Bokeh
# (если нужен HTML, можно использовать output_file)
# ——————————————————————————————————————————
output_notebook(INLINE, hide_banner=True)
# ——————————————————————————————————————————
# ДАННЫЕ
# ——————————————————————————————————————————
np.random.seed(42)
X = np.random.rand(50, 1) * 2 - 1
y = -4 + 3*X - 2*X**2 + 5*X**3 + np.random.randn(50, 1) * 0.5
X_flat = X.flatten()
y_flat = y.flatten()
# источники
source_data = ColumnDataSource(data=dict(x=X_flat, y=y_flat))
source_pred = ColumnDataSource(data=dict(x=X_flat, y=np.zeros_like(y_flat)))
# данные для инсет-гистограммы: уже в ПЛОТНЫХ координатах (будем заполнять из JS)
source_inset_bars = ColumnDataSource(data=dict(left=[], right=[], top=[], bottom=[]))
source_inset_box = ColumnDataSource(data=dict(x=[0], y=[0], w=[0], h=[0])) # рамка инсета
source_inset_x_ticks = ColumnDataSource(data=dict(x=[], y=[], text=[]))
source_inset_y_ticks = ColumnDataSource(data=dict(x=[], y=[], text=[]))
source_zero_line = ColumnDataSource(data=dict(x0=[], y0=[], x1=[], y1=[]))
# ——————————————————————————————————————————
# ГЛАВНАЯ ФИГУРА (без тулбара и жесткая фиксация)
# ——————————————————————————————————————————
TICK_FONT = "12pt" # единый размер шрифта для большого и маленького графиков
p = figure(
width=820, height=520,
x_range=Range1d(-1, 1),
y_range=Range1d(-14, 2),
toolbar_location=None, tools="",
title="Polynomial Overfitting Visualization",
sizing_mode="stretch_width"
)
# единые шрифты осей
p.axis.major_label_text_font_size = TICK_FONT
p.title.text_font_size = "13pt"
# точки и предсказание
p.scatter('x', 'y', source=source_data, color="red", legend_label="Data", size=6)
p.line('x', 'y', source=source_pred, color="blue", legend_label="Prediction", line_width=2)
p.legend.location = "bottom_right"
# ——————————————————————————————————————————
# ИНСЕТ-ГИСТОГРАММА ВНУТРИ ОСНОВНОГО ГРАФИКА
# (рисуем прямо в координатах большого графика)
# ——————————————————————————————————————————
# рамка инсета
p.rect(x='x', y='y', width='w', height='h',
source=source_inset_box, fill_alpha=0, line_color="black", line_width=1)
# столбики гистограммы
p.quad(left='left', right='right', top='top', bottom='bottom',
source=source_inset_bars, fill_color="green", line_color="black", fill_alpha=0.8)
# ось нуля внутри инсета
p.segment(x0='x0', y0='y0', x1='x1', y1='y1', source=source_zero_line, line_width=1, line_color="black")
# «ручные» подписи тиков для инсета (тем же шрифтом)
p.text(x='x', y='y', text='text', source=source_inset_x_ticks,
text_font_size=TICK_FONT, text_align="center", text_baseline="top")
p.text(x='x', y='y', text='text', source=source_inset_y_ticks,
text_font_size=TICK_FONT, text_align="right", text_baseline="middle")
# ——————————————————————————————————————————
# СЛАЙДЕР + MSE
# ——————————————————————————————————————————
slider = Slider(start=1, end=49, value=1, step=1, title="Degree of Polynomial")
mse_text = Div(text="MSE: 0")
# ——————————————————————————————————————————
# JS-КОЛБЭК: регрессия, линия предсказаний, MSE, и отрисовка инсета
# ——————————————————————————————————————————
callback = CustomJS(
args=dict(
source_data=source_data,
source_pred=source_pred,
source_inset_bars=source_inset_bars,
source_inset_box=source_inset_box,
source_inset_x_ticks=source_inset_x_ticks,
source_inset_y_ticks=source_inset_y_ticks,
source_zero_line=source_zero_line,
slider=slider,
mse_text=mse_text,
x_range=p.x_range,
y_range=p.y_range
),
code="""
const degree = slider.value;
// --- исходные данные ---
let x = source_data.data['x'].slice();
let y_true = source_data.data['y'].slice();
const n = x.length;
// сортировка по x
const idx = Array.from(x.keys()).sort((a,b)=>x[a]-x[b]);
x = idx.map(i => x[i]);
y_true = idx.map(i => y_true[i]);
// плотная сетка для линии предсказаний
const grid_points = 200;
const x_min = Math.min(...x);
const x_max = Math.max(...x);
const x_dense = [];
const step = (x_max - x_min) / (grid_points - 1);
for (let i=0;i<grid_points;i++){ x_dense.push(x_min + i*step); }
// X_poly для обучения (на исходных точках)
const X_poly = [];
for (let i=0;i<n;i++){
const row = [];
for (let d=0; d<=degree; d++){ row.push(Math.pow(x[i], d)); }
X_poly.push(row);
}
// X^T, X^T X, X^T y
const XT = X_poly[0].map((_,j)=>X_poly.map(row=>row[j]));
const XTX = XT.map(row => row.map((_, j) => row.reduce((acc, curr, i) => acc + curr * X_poly[i][j], 0)));
const XTy = XT.map(row => row.reduce((acc, curr, i) => acc + curr * y_true[i], 0));
// Решение СЛАУ (Гаусс)
function solve(A, b){
const m = A.length;
const x = Array(m).fill(0);
for (let i=0;i<m;i++){
let maxEl = Math.abs(A[i][i]), maxRow = i;
for (let k=i+1;k<m;k++){
if (Math.abs(A[k][i])>maxEl){ maxEl = Math.abs(A[k][i]); maxRow=k; }
}
[A[maxRow], A[i]] = [A[i], A[maxRow]];
[b[maxRow], b[i]] = [b[i], b[maxRow]];
for (let k=i+1;k<m;k++){
const c = -A[k][i]/A[i][i];
for (let j=i;j<m;j++){ A[k][j] += c*A[i][j]; }
b[k] += c*b[i];
}
}
for (let i=m-1;i>=0;i--){
x[i] = b[i]/A[i][i];
for (let k=i-1;k>=0;k--){ b[k] -= A[k][i]*x[i]; }
}
return x;
}
const coefs = solve(XTX, XTy);
// Предсказание на плотной сетке
const y_dense = [];
for (let i=0;i<grid_points;i++){
let pred = 0;
for (let d=0; d<=degree; d++){ pred += coefs[d]*Math.pow(x_dense[i], d); }
y_dense.push(pred);
}
source_pred.data['x'] = x_dense;
source_pred.data['y'] = y_dense;
source_pred.change.emit();
// MSE на исходных точках
let mse = 0;
for (let i=0;i<n;i++){
let pred = 0;
for (let d=0; d<=degree; d++){ pred += coefs[d]*Math.pow(x[i], d); }
mse += Math.pow(y_true[i]-pred, 2);
}
mse /= n;
mse_text.text = `MSE: ${mse.toFixed(5)}`;
// ——————————————————————————————————
// ИНСЕТ-ГИСТОГРАММА КОЭФФИЦИЕНТОВ
// ——————————————————————————————————
// Геометрия инсета как доли от текущих видимых диапазонов:
const xr0 = x_range.start, xr1 = x_range.end;
const yr0 = y_range.start, yr1 = y_range.end;
const pad_x = 0.02*(xr1 - xr0);
const pad_y = 0.02*(yr1 - yr0);
const fracW = 0.40; // ширина инсета (доля по Ox)
const fracH = 0.40; // высота инсета (доля по Oy)
const inset_x1 = xr1 - pad_x;
const inset_x0 = inset_x1 - fracW*(xr1 - xr0);
const inset_y0 = yr0 + pad_y;
const inset_y1 = inset_y0 + fracH*(yr1 - yr0);
// рамка инсета
source_inset_box.data['x'] = [0.5*(inset_x0+inset_x1)];
source_inset_box.data['y'] = [0.5*(inset_y0+inset_y1)];
source_inset_box.data['w'] = [ (inset_x1-inset_x0) ];
source_inset_box.data['h'] = [ (inset_y1-inset_y0) ];
source_inset_box.change.emit();
// строим "каталог" столбиков по индексам коэффициентов
const mcoefs = coefs.length; // degree+1
const idx_min = -0.5;
const idx_max = mcoefs - 0.5;
// диапазон по значению весов
const wmin = Math.min(0, Math.min(...coefs));
const wmax = Math.max(...coefs, 0.0); // чтобы ноль влезал
function mapX(i_data){ // i_data в «индексах», линейно на [inset_x0, inset_x1]
return inset_x0 + ( (i_data - idx_min) / (idx_max - idx_min) )*(inset_x1 - inset_x0);
}
function mapY(w){ // w в значениях коэффициентов, линейно на [inset_y0, inset_y1]
if (wmax === wmin) return 0.5*(inset_y0+inset_y1);
return inset_y0 + ( (w - wmin) / (wmax - wmin) )*(inset_y1 - inset_y0);
}
const left = [], right = [], top = [], bottom = [];
// базовая линия "y=0" внутри инсета
const y_zero = mapY(0);
source_zero_line.data['x0'] = [inset_x0];
source_zero_line.data['y0'] = [y_zero];
source_zero_line.data['x1'] = [inset_x1];
source_zero_line.data['y1'] = [y_zero];
source_zero_line.change.emit();
// ширина столбика ~ 0.8 по индексу
for (let i=0;i<mcoefs;i++){
const l = mapX(i - 0.4);
const r = mapX(i + 0.4);
const t = mapY(coefs[i]);
left.push(l); right.push(r);
top.push(t); bottom.push(y_zero);
}
source_inset_bars.data['left'] = left;
source_inset_bars.data['right'] = right;
source_inset_bars.data['top'] = top;
source_inset_bars.data['bottom'] = bottom;
source_inset_bars.change.emit();
// --- подписи тиков по Ox (немного, чтобы не шуметь) ---
const xticks_pos = [];
const xticks_txt = [];
const approxTicks = 4; // ~4-5 подписей
const stepIdx = Math.max(1, Math.ceil(mcoefs / approxTicks));
for (let i=0; i<mcoefs; i += stepIdx){
xticks_pos.push(mapX(i));
xticks_txt.push(String(i));
}
// гарантируем ноль и последний индекс
if (xticks_pos.length===0 || xticks_pos[0] !== mapX(0)){
xticks_pos.unshift(mapX(0));
xticks_txt.unshift("0");
}
if (xticks_pos[xticks_pos.length-1] !== mapX(mcoefs-1)){
xticks_pos.push(mapX(mcoefs-1));
xticks_txt.push(String(mcoefs-1));
}
source_inset_x_ticks.data['x'] = xticks_pos;
source_inset_x_ticks.data['y'] = xticks_pos.map(()=> inset_y0 - 0.012*(yr1-yr0)); // чуть ниже рамки
source_inset_x_ticks.data['text'] = xticks_txt;
source_inset_x_ticks.change.emit();
// --- подписи тиков по Oy (5 «красивых» значений) ---
function niceTicks(vmin, vmax, nTarget){
if (vmax === vmin){ return [vmin]; }
const span = vmax - vmin;
const raw = span / (nTarget - 1);
const pow10 = Math.pow(10, Math.floor(Math.log10(raw)));
const mults = [1, 2, 2.5, 5, 10];
let step = mults[0]*pow10;
for (let k=0;k<mults.length;k++){
const candidate = mults[k]*pow10;
if (span / candidate <= nTarget){ step = candidate; break; }
}
const start = Math.ceil(vmin / step)*step;
const ticks = [];
for (let v=start; v<=vmax+1e-12; v+=step){ ticks.push(+v.toFixed(10)); }
return ticks;
}
const yt = niceTicks(wmin, wmax, 5);
const yt_x = yt.map(()=> inset_x0 - 0.005*(xr1-xr0)); // чуть левее рамки
const yt_y = yt.map(v => mapY(v));
source_inset_y_ticks.data['x'] = yt_x;
source_inset_y_ticks.data['y'] = yt_y;
source_inset_y_ticks.data['text'] = yt.map(v => (Math.abs(v)<1e-10? "0" : v.toFixed(2)));
source_inset_y_ticks.change.emit();
"""
)
slider.js_on_change('value', callback)
# Компоновка: сначала график, затем слайдер и MSE (слайдер "снизу")
layout = column(p, slider, mse_text, sizing_mode="stretch_width")
show(layout)