Критерии переобучения и регуляризация#

(Здесь и далее перезапускайте ячейку перед взаимодействием с графиками. Используются одинаковые глобальные переменные)

Вспомним основную проблему - переобучение. Характерный пример:

from bokeh.io import output_notebook
from bokeh.resources import INLINE
output_notebook(INLINE)
Loading BokehJS ...
Hide 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)