Към съдържанието
wannacode

Обучението от начало до край

Накратко: Обучението на невронна мрежа е цикъл по епохи: разбъркване, партиди, проход напред, обратно разпространение и стъпка, докато грешката на валидацията спре да пада.

Проблем

Мрежата казва −0,23 дни

Страницата за плащане трябва да казва „пристига до 2 дни“. Времето на доставката зависи от разстоянието, от това дали поръчката е след 16:00 (тогава тръгва на другия ден) и малко от теглото. Права линия от линейната регресия не следва тази крива: в града е около ден, до 200 km расте бързо, после все по-бавно.

Мрежата от урока за backprop може да следва крива. Ето я с 4 скрити неврона и случайни тегла, преди обучение:

var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
Console.WriteLine($"грешка (MSE): {net.Loss(trainX, trainY):F4}");
foreach (int k in new[] { 0, 12, 23 })
Console.WriteLine($"{train[k][0]} km: мрежата казва {net.Predict(trainX[k]):F2}, " +
$"истина {trainY[k]:F1} дни");
Изход
грешка (MSE): 7.1855
5 km: мрежата казва -0.23, истина 1.0 дни
150 km: мрежата казва -0.17, истина 2.3 дни
520 km: мрежата казва 0.18, истина 3.0 дни

Поръчка на 5 km пристига след −0,23 дни. В урока за backprop видя една стъпка за един пример. Колко стъпки трябват, с кои примери, в какъв ред и кога да спреш?

Идеята

Цикълът на обучение

Обучението е един цикъл, който се върти, докато мрежата спре да се подобрява:

  1. Начални тегла: малки случайни числа от генератор със семе.
  2. Епоха: едно минаване през всички примери за обучение. Преди всяка епоха примерите се разбъркват, за да не учи мрежата реда им.
  3. Партида: няколко примера наведнъж. За всеки има проход напред и грешка, после назад. Наклоните се усредняват и мрежата прави една стъпка.
  4. Проверка: на всеки 100 епохи програмата печата грешката на обучението и на валидацията.

Това е цикълът на обучение (training loop). Числата, които печата, нарисувани като графика, са кривата на грешката. По нея разбираш дали мрежата учи, дали скоростта е добра и кога да спреш.

Данните са 32 поръчки от магазина: 24 за обучение и 8 за валидация. Мрежата никога не прави стъпка с поръчките за валидация. Те са мярката за нови поръчки.

Виж го

Данните и мрежата

Поръчките, записани в кода:

// поръчка: km, след 16:00 (1 или 0), kg, дни до доставката
double[][] train =
[
[5, 0, 1.2, 1.0], [12, 1, 3.5, 1.6], [18, 0, 0.8, 1.0],
[25, 0, 6.0, 1.2], [28, 1, 2.1, 1.8], [35, 0, 4.4, 1.3],
[48, 1, 0.5, 2.2], [60, 0, 9.0, 2.5], [75, 0, 1.7, 1.6],
[90, 1, 2.9, 2.6], [110, 0, 5.5, 1.9], [130, 1, 0.9, 2.7],
[150, 0, 3.1, 2.3], [170, 0, 8.2, 3.3], [185, 1, 1.4, 3.1],
[195, 0, 2.6, 2.3], [210, 0, 4.8, 2.6], [240, 1, 0.6, 3.3],
[280, 0, 8.6, 3.6], [320, 0, 1.9, 2.7], [360, 1, 3.3, 3.6],
[410, 0, 5.0, 2.8], [460, 1, 2.2, 3.6], [520, 0, 0.7, 3.0],
];
// валидация: поръчки, които мрежата не вижда при обучението
double[][] valid =
[
[8, 1, 2.4, 1.6], [22, 0, 8.8, 1.0], [40, 0, 1.1, 1.4],
[95, 1, 7.9, 2.5], [160, 0, 2.0, 2.3], [205, 1, 3.9, 3.1],
[300, 0, 9.4, 2.6], [480, 1, 4.6, 3.6],
];

Признаците се свеждат до близки мащаби: километрите в стотици, теглото в десетки килограми. Иначе 520 km щяха да заглушат всичко останало:

// признаците в близки мащаби: стотици km, 0 или 1, десетки kg
static double[][] Inputs(double[][] orders) =>
orders.Select(o => new[] { o[0] / 100, o[1], o[2] / 10 }).ToArray();
static double[] Targets(double[][] orders) =>
orders.Select(o => o[3]).ToArray();

Случайността идва от собствен генератор със семе. Едно семе дава едни и същи числа на всеки компютър и на четирите езика, затова урокът печата един и същ изход навсякъде. Числото 48271 и делението на 2³¹ − 1 са стар и проверен генератор (Park–Miller). Произведението е под 2⁵³, затова се смята точно дори в JavaScript:

class Rng(long seed)
{
long state = seed;
// следващото число между 0 и 1; едно семе дава едни и същи числа
public double Next()
{
state = state * 48271 % 2147483647;
return (double)state / 2147483647;
}
}
new Random(42) в C# също има семе. Тук генераторът е наш, за да е същият като в другите три езика.

Мрежата е тази от урока за backprop, събрана в клас: 3 входа, 4 скрити неврона със сигмоида и един изход без активация, защото дните не са вероятност. Step прави проход напред и назад за всяка поръчка от партидата, събира наклоните и мести всяко тегло веднъж, със средния наклон:

class Net
{
public readonly double[][] W1; // вход → скрит слой
public readonly double[] B1;
public readonly double[] W2; // скрит слой → изход
public double B2;
readonly double[] h; // изходите на скрития слой при последното predict
public Net(int hidden, int inputs)
{
W1 = new double[hidden][];
for (int j = 0; j < hidden; j++)
W1[j] = new double[inputs];
B1 = new double[hidden];
W2 = new double[hidden];
h = new double[hidden];
}
// случайни тегла от -scale до scale; отместванията остават 0
public void Randomize(Rng rng, double scale)
{
foreach (var row in W1)
for (int i = 0; i < row.Length; i++)
row[i] = (rng.Next() * 2 - 1) * scale;
for (int j = 0; j < W2.Length; j++)
W2[j] = (rng.Next() * 2 - 1) * scale;
}
static double Sigmoid(double z) => 1 / (1 + Math.Exp(-z));
public double Predict(double[] x)
{
double y = B2;
for (int j = 0; j < W2.Length; j++)
{
double z = B1[j];
for (int i = 0; i < x.Length; i++)
z += W1[j][i] * x[i];
h[j] = Sigmoid(z);
y += W2[j] * h[j];
}
return y;
}
public double Loss(double[][] xs, double[] ys)
{
double total = 0;
for (int k = 0; k < xs.Length; k++)
{
double e = Predict(xs[k]) - ys[k];
total += e * e;
}
return total / xs.Length;
}
// една стъпка: средният наклон по примерите от партидата
public void Step(double[][] xs, double[] ys, int[] batch, double rate)
{
int hidden = W2.Length, inputs = xs[0].Length;
var gw1 = new double[hidden, inputs];
double[] gb1 = new double[hidden], gw2 = new double[hidden];
double gb2 = 0;
foreach (int k in batch)
{
double d = 2 * (Predict(xs[k]) - ys[k]);
gb2 += d;
for (int j = 0; j < hidden; j++)
{
gw2[j] += d * h[j];
double dz = d * W2[j] * h[j] * (1 - h[j]);
gb1[j] += dz;
for (int i = 0; i < inputs; i++)
gw1[j, i] += dz * xs[k][i];
}
}
int n = batch.Length;
B2 -= rate * gb2 / n;
for (int j = 0; j < hidden; j++)
{
W2[j] -= rate * gw2[j] / n;
B1[j] -= rate * gb1[j] / n;
for (int i = 0; i < inputs; i++)
W1[j][i] -= rate * gw1[j, i] / n;
}
}
}

Разбъркването е Fisher–Yates: отзад напред всяко място се разменя със случайно място преди него.

// Fisher–Yates: всяка подредба е еднакво вероятна
static void Shuffle(int[] order, Rng rng)
{
for (int i = order.Length - 1; i > 0; i--)
{
int k = (int)(rng.Next() * (i + 1));
(order[i], order[k]) = (order[k], order[i]);
}
}

Виж го

Цикълът и кривата на грешката

var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int size = 8;
double rate = 0.05;
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
Console.WriteLine("епоха обучение валидация");
for (int epoch = 1; epoch <= 1000; epoch++)
{
Shuffle(order, rng);
for (int start = 0; start < order.Length; start += size)
{
int end = Math.Min(start + size, order.Length);
int[] batch = order[start..end];
net.Step(trainX, trainY, batch, rate);
}
if (epoch % 100 == 0)
{
double t = net.Loss(trainX, trainY);
double v = net.Loss(validX, validY);
Console.WriteLine($"{epoch,5}{t,10:F4}{v,11:F4}");
}
}
Стъпка по стъпка, с думи
  1. Семе 42: всяко пускане започва от едни и същи случайни тегла. Преди обучението грешката е 7,19, далеч над графиката.
  2. Епоха е едно минаване през всичките 24 поръчки за обучение. Първо се разбъркват, със същия генератор.
  3. Партида от 8 поръчки: напред, грешка, назад, една стъпка. Три стъпки на епоха, 3000 за цялото обучение.
  4. Епоха 100: обучение 0,1365, валидация 0,0756. С поръчките за валидация не е направена нито една стъпка.
  5. Валидацията е най-ниска на епоха 200: 0,0705. После тръгва нагоре, а обучението продължава да пада.
  6. Епоха 1000: обучение 0,0381, валидация 0,2371. Мрежата наизустява три тежки пратки, закъснели по случайност.
  7. Най-добрата мрежа е около епоха 200, не на 1000. Спира се, когато валидацията спре да пада.

Целият цикъл: 1000 епохи, партиди по 8, скорост 0,05:

var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int size = 8;
double rate = 0.05;
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
Console.WriteLine("епоха обучение валидация");
for (int epoch = 1; epoch <= 1000; epoch++)
{
Shuffle(order, rng);
for (int start = 0; start < order.Length; start += size)
{
int end = Math.Min(start + size, order.Length);
int[] batch = order[start..end];
net.Step(trainX, trainY, batch, rate);
}
if (epoch % 100 == 0)
{
double t = net.Loss(trainX, trainY);
double v = net.Loss(validX, validY);
Console.WriteLine($"{epoch,5}{t,10:F4}{v,11:F4}");
}
}
Изход
епоха обучение валидация
100 0.1365 0.0756
200 0.1041 0.0705
300 0.0826 0.0776
400 0.0667 0.1048
500 0.0556 0.1350
600 0.0486 0.1718
700 0.0443 0.1967
800 0.0417 0.2202
900 0.0396 0.2243
1000 0.0381 0.2371

Обучението пада от 7,19 до 0,0381. Валидацията пада до епоха 200, а после се качва: на епоха 1000 е 0,2371, повече от три пъти по-лоша от най-добрата. Мрежата продължава да се нагажда към поръчките за обучение, включително към това, което в тях е случайност. Към това ще се върнем в края на урока.

Пробвай

Предвиди изхода

Ако партидата е 10, а примерите са 24, колко стъпки прави обучението за 1000 епохи?

int examples = 24, size = 10, steps = 0;
for (int epoch = 1; epoch <= 1000; epoch++)
for (int start = 0; start < examples; start += size)
steps++;
Console.WriteLine($"стъпки: {steps}");
Отговор

Идеята

Партиди: всички, по един или по 8

Колко примера да влизат в една стъпка? Ето същата мрежа за 40 епохи с партида 24 (всички наведнъж), 8 и 1. След всяка стъпка програмата мери грешката на всички 24 поръчки и брои колко пъти стъпката я е вдигнала:

foreach (int size in new[] { 24, 8, 1 })
{
var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
int steps = 0, up = 0;
double before = net.Loss(trainX, trainY);
for (int epoch = 1; epoch <= 40; epoch++)
{
Shuffle(order, rng);
for (int start = 0; start < order.Length; start += size)
{
int end = Math.Min(start + size, order.Length);
net.Step(trainX, trainY, order[start..end], 0.05);
double after = net.Loss(trainX, trainY);
steps++;
if (after > before) up++;
before = after;
}
}
Console.WriteLine($"партида {size,2}: {steps,3} стъпки, {up,3} нагоре, грешка {before:F4}");
}
Изход
партида 24: 40 стъпки, 0 нагоре, грешка 0.2913
партида 8: 120 стъпки, 21 нагоре, грешка 0.1840
партида 1: 960 стъпки, 495 нагоре, грешка 0.1238
  • Всички наведнъж: наклонът е точният наклон на цялата грешка, затова нито една стъпка не я вдига. Но стъпката е една на епоха, и след 40 епохи мрежата е най-далеч.
  • По един пример: 24 стъпки на епоха, затова за 40 епохи стига най-ниско. Но наклонът на един пример е груба догадка за наклона на всички: 495 от 960 стъпки вдигат грешката.
  • Партида от 8: три стъпки на епоха и само 21 от 120 нагоре.

Това се казва mini-batch (партида): стъпка по средния наклон на няколко примера. Партидата е компромисът, който ползва почти всеки. На видеокарта примерите от една партида се смятат наведнъж, затова в истински проекти партидите са десетки или стотици примери.

Пробвай

Попълни

Наклоните на партидата се събират, а стъпката е по средния наклон. На какво се дели?

int n = ;
B2 -= rate * gb2 / n;

Идеята

Лоша скорост, лошо начало

Кривата казва какво не е наред. Трите скорости от урока за градиентното спускане, но в мрежа. Грешката е на епоха 50, 100 и 200:

foreach (double rate in new[] { 0.005, 0.05, 2.0 })
{
var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
Console.Write($"скорост {rate:F3}:");
for (int epoch = 1; epoch <= 200; epoch++)
{
TrainEpoch(net, trainX, trainY, order, 8, rate, rng);
double loss = net.Loss(trainX, trainY);
if (double.IsNaN(loss) || loss > 1e6)
{
Console.Write($" разходи се на епоха {epoch}");
break;
}
if (epoch is 50 or 100 or 200)
Console.Write($" {loss:F4}");
}
Console.WriteLine();
}
Изход
скорост 0.005: 0.3892 0.3201 0.2472
скорост 0.050: 0.1698 0.1365 0.1041
скорост 2.000: разходи се на епоха 2

С 0,005 мрежата учи, но бавно: на епоха 200 (0,2472) е по-зле, отколкото 0,05 на епоха 50 (0,1698). Кривата пада полегато и не спира да пада, а това значи, че можеш да вдигнеш скоростта. С 2 всяка стъпка прескача дъното и следващият наклон е още по-стръмен. Грешката е над милион още на втората епоха, а след нея идват безкрайност и NaN. Затова програмата проверява и спира.

Началото също има значение:

var rng = new Rng(42);
var zeros = new Net(4, 3); // без Randomize: всички тегла са 0
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
for (int epoch = 1; epoch <= 200; epoch++)
TrainEpoch(zeros, trainX, trainY, order, 8, 0.05, rng);
Console.WriteLine($"нули: грешка {zeros.Loss(trainX, trainY):F4}");
for (int j = 0; j < zeros.W1.Length; j++)
Console.WriteLine($" неврон {j + 1}: " + string.Join(" ", zeros.W1[j].Select(x => x.ToString("F2"))));
foreach (double scale in new double[] { 8, 1 })
{
rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, scale);
double first = net.Loss(trainX, trainY);
order = Enumerable.Range(0, trainX.Length).ToArray();
for (int epoch = 1; epoch <= 200; epoch++)
TrainEpoch(net, trainX, trainY, order, 8, 0.05, rng);
Console.WriteLine($"тегла ×{scale}: в началото {first:F4}, след 200 епохи {net.Loss(trainX, trainY):F4}");
}
Изход
нули: грешка 0.1127
неврон 1: 0.99 0.47 0.19
неврон 2: 0.99 0.47 0.19
неврон 3: 0.99 0.47 0.19
неврон 4: 0.99 0.47 0.19
тегла ×8: в началото 33.9817, след 200 епохи 0.2971
тегла ×1: в началото 7.1855, след 200 епохи 0.1041
  • Нули. Четирите скрити неврона започват еднакви, получават еднакви наклони и правят еднакви стъпки. След 200 епохи теглата им са до последната цифра едни и същи: мрежата е с един скрит неврон, повторен четири пъти. Случайните начални тегла са там, за да се различат невроните.
  • Тегла ×8. Сумите в сигмоидата са големи, затова при повечето поръчки тя е почти 0 или почти 1, там, където е плоска. Плоска крива значи почти нулев наклон и малки стъпки: след 200 епохи грешката е почти три пъти по-голяма, отколкото с теглата ×1.

Пробвай

Намери грешката

Колега пише цикъла по свой начин и вместо номерата разбърква самите масиви. Грешката стои над 0,6 и не пада, колкото и епохи да пусне. На кой ред е грешката?

Избери реда с грешката.

Идеята

Кога да спреш

Какво точно наизустява мрежата? Сред поръчките за обучение има три тежки пратки (9,0 kg, 8,2 kg и 8,6 kg), които закъсняха с цял ден по случайност. Ето трите тежки пратки от валидацията, които не са закъснели:

var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
for (int epoch = 1; epoch <= 1000; epoch++)
{
TrainEpoch(net, trainX, trainY, order, 8, 0.05, rng);
if (epoch is 200 or 1000)
{
Console.WriteLine($"епоха {epoch}:");
foreach (int k in new[] { 1, 3, 6 }) // тежките пратки от валидацията
Console.WriteLine($" {valid[k][0]} km, {valid[k][2]:F1} kg: " +
$"{net.Predict(validX[k]):F2} (истина {validY[k]:F1})");
}
}
Изход
епоха 200:
22 km, 8.8 kg: 1.31 (истина 1.0)
95 km, 7.9 kg: 2.53 (истина 2.5)
300 km, 9.4 kg: 3.13 (истина 2.6)
епоха 1000:
22 km, 8.8 kg: 1.84 (истина 1.0)
95 km, 7.9 kg: 3.09 (истина 2.5)
300 km, 9.4 kg: 3.38 (истина 2.6)

На епоха 200 предсказанията са близо до истината. На епоха 1000 мрежата е научила „тежкото пътува по-бавно“: пратката на 22 km получава 1,84 дни вместо 1,0. Правило, което в данните е само случайност, е преобучение: обучението се подобрява, а новите поръчки се влошават.

Затова се спира, когато валидацията спре да пада. Това е ранно спиране (early stopping). Програмата проверява валидацията на всеки 10 епохи и пази снимка (копие) на най-добрата мрежа. Ако 100 епохи няма подобрение, спира и връща снимката:

// копие на теглата: снимка на мрежата в този момент
public Net Copy()
{
var c = new Net(W2.Length, W1[0].Length);
for (int j = 0; j < W2.Length; j++)
{
W1[j].CopyTo(c.W1[j], 0);
c.B1[j] = B1[j];
c.W2[j] = W2[j];
}
c.B2 = B2;
return c;
}
var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
// проверка на всеки 10 епохи; спираме след 100 без подобрение
double best = double.MaxValue;
int bestEpoch = 0, epoch = 0;
var bestNet = net.Copy();
while (epoch < 1000 && epoch - bestEpoch < 100)
{
epoch++;
TrainEpoch(net, trainX, trainY, order, 8, 0.05, rng);
if (epoch % 10 == 0)
{
double loss = net.Loss(validX, validY);
if (loss < best)
{
best = loss;
bestEpoch = epoch;
bestNet = net.Copy();
}
}
}
Console.WriteLine($"най-ниска валидация: {best:F4} на епоха {bestEpoch}");
Console.WriteLine($"спрях на епоха {epoch}");
Console.WriteLine($"пазя мрежата от епоха {bestEpoch}: {bestNet.Loss(validX, validY):F4}");
Изход
най-ниска валидация: 0.0667 на епоха 230
спрях на епоха 330
пазя мрежата от епоха 230: 0.0667

Най-добрата валидация е 0,0667 на епоха 230, по-добре от всяка точка на кривата горе, защото проверката е по-честа. Обучението спира на епоха 330 вместо 1000, и мрежата, която се пази, е от епоха 230.

Валидацията реши кога да спреш, затова вече не е честна мярка за новите поръчки. Окончателната оценка се прави на трета част от данните, теста, който не е участвал в нито едно решение.

Идеята

Кога вреди

  • Мрежа за всичко. 32 поръчки и три признака са малко данни: права с няколко признака или по-проста формула често стигат, обясняват се по-лесно и не наизустяват. Мрежата си струва, когато правилата са твърде много, за да ги напишеш, и има достатъчно примери.
  • Едно пускане. Друго семе дава други начални тегла и друга крива. Едно пускане може да е късмет. Сравнявай промени с няколко семена и гледай средното.
  • Малка валидация. Осем поръчки са шумна мярка: една странна поръчка мести кривата. Колкото по-малко са данните, толкова по-внимателно четеш една точка.
  • Ранното спиране не поправя данните. Трите случайни закъснения още са там. По-добре е да ги намериш и да ги махнеш или поправиш, а ако теглото наистина не влияе, да не го даваш като признак.
  • Цената. Тук обучението е под секунда. Истинските мрежи се учат часове или седмици на много видеокарти. Клъстерите за обучение и пускането на модел в работа са инфраструктура и се учат в курса по DevOps ZeroToOps.
По-точно, за любопитните

Стъпката с формула. Партидата е B примера, а η е скоростта. За всяко тегло w:

w ← w − η · (1/B) · Σ ∂L/∂w

С думи: събери наклона на грешката по w за всеки пример от партидата, раздели на B, умножи по скоростта и извади. ∂L/∂w е наклонът, който backprop смята за едно тегло. При B, равно на всички примери, това е обикновеното градиентно спускане. При B = 1 се казва стохастично (SGD), а с партиди е mini-batch SGD.

Защо 2 · (y − t). Грешката на един пример е (y − t)². Наклонът ѝ по y е 2 · (y − t): ако изходът е с 0,5 над истината, наклонът е 1, а ако е с 0,5 под нея, е −1. Оттам започва backprop в Step.

Истинските инструменти. Практиката на машинното обучение е главно на Python. В PyTorch същият цикъл е: DataLoader(..., batch_size=8, shuffle=True) дава партидите, optimizer.zero_grad() нулира наклоните (нашите нови масиви в началото на стъпката), loss.backward() е backprop, а optimizer.step() мести теглата. Семето е torch.manual_seed(42). В C# същото прави ML.NET или TorchSharp, в Java DJL или Tribuo, а в JavaScript TensorFlow.js, ONNX Runtime пуска вече научен модел. Цикълът е същият, само че матриците се смятат на видеокарта.

По-умни стъпки. Почти никой не ползва голата стъпка от урока. Momentum помни посоката на последните стъпки и минава по-гладко през шума на партидите. Adam пази отделна скорост за всяко тегло. Скоростта често се сваля с времето (learning rate schedule): големи стъпки в началото, малки накрая.

Началните тегла. Тук случайните тегла са от −1 до 1. В по-големи мрежи интервалът се смята по броя входове на неврона, например ±1/√n (Xavier, He), за да не са сумите твърде големи още в първия слой.

Еднакви числа на четирите езика. Събиранията и умноженията тук вървят в един и същ ред, затова резултатът е един и същ бит по бит. exp не е обещано, че е еднакъв до последния бит на всеки език, затова програмите печатат по 4 знака. Изходът е проверен и е един и същ на Java, C#, Python и JavaScript.

:F4 пише числото според културата на системата. На компютър с българска култура ще видиш 0,1365, а проверката на урока върви с инвариантната култура и точка.

Упражнение

Ранно спиране при три скорости

Реши задачата в редактора си.

Вземи ранното спиране от урока и го пусни за скорости 0,02, 0,05 и 0,2, всеки път с нова мрежа от семе 42. За всяка скорост отпечатай най-ниската валидация, епохата ѝ и епохата, на която обучението спира. Не е нужно да пазиш копие на мрежата.

Изход
скорост 0.02: най-ниска валидация 0.0683 на епоха 470, спрях на епоха 570
скорост 0.05: най-ниска валидация 0.0667 на епоха 230, спрях на епоха 330
скорост 0.20: най-ниска валидация 0.0628 на епоха 50, спрях на епоха 150

С 0,2 мрежата стига най-ниската си валидация за 50 епохи и спира на 150. С 0,02 ѝ трябват 470. По-голямата скорост стига дъното по-бързо, но и тръгва нагоре по-рано. Затова валидацията се проверява често, а не само в края.

Още задачи

  • Шест неврона. Пусни основния цикъл с 6 скрити неврона. По-рано ли тръгва нагоре валидацията?
  • Без теглото. Махни теглото от признаците (2 входа вместо 3) и пусни основния цикъл. Какво става с валидацията на епоха 1000?

Тест

Провери се

1. Какво е една епоха?
2. Грешката на обучението пада през цялото време, а валидацията е най-ниска на епоха 200 и после расте. Коя мрежа пазиш?
3. След втората епоха грешката е над милион, а после става NaN. Кое е най-вероятно?
4. Всички тегла в началото са 0. Какво става с четирите скрити неврона?
5. Защо обикновено се учи с партиди, а не с всички примери наведнъж или по един?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·