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

Какво е машинно учене

Накратко: Машинното учене е начин програмата сама да извади правилото от примери (признаци и етикет), вместо програмистът да го пише на ръка с if.

Проблем

Колко дни ще пътува поръчката

Страницата на продукта трябва да казва „пристига след 2 дни“. Колега пише правилото по километрите: до 100 km е 1 ден, до 250 km са 2 дни, а нататък 3. После го пуска върху 12 минали поръчки, за които знаем колко са пътували:

// правилото на колегата: по километрите
static int rule(double km) {
if (km < 100) return 1;
if (km < 250) return 2;
return 3;
}
int right = 0;
for (int i = 0; i < ORDERS.length; i++) {
int guess = rule(ORDERS[i][0]);
if (guess == DAYS[i]) right++;
else System.out.println("поръчка " + (i + 1) + ": правилото казва " + guess + ", пристигна за " + DAYS[i]);
}
System.out.println("правилото познава " + right + " от " + ORDERS.length);
Изход
поръчка 2: правилото казва 1, пристигна за 2
поръчка 5: правилото казва 2, пристигна за 3
поръчка 6: правилото казва 2, пристигна за 3
поръчка 9: правилото казва 3, пристигна за 4
поръчка 10: правилото казва 3, пристигна за 4
поръчка 12: правилото казва 3, пристигна за 5
правилото познава 6 от 12

Шест от дванадесет. Колегата добавя две правила: тежката пратка (над 10 kg) е с ден повече, а натовареният куриер (над 35 пратки в деня) също. Същият цикъл, с новото правило:

// още две правила: тежка пратка и натоварен куриер
static int rule(double km, double kg, double parcels) {
int days = km < 100 ? 1 : km < 250 ? 2 : 3;
if (kg > 10) days++;
if (parcels > 35) days++;
return days;
}
Изход
поръчка 2: правилата казват 1, пристигна за 2
поръчка 12: правилата казват 4, пристигна за 5
правилата познават 10 от 12

Десет от дванадесет, но с три нови числа на око: 10 kg, 35 пратки и „ден повече“. Защо не 8 kg? Защо +1, а не +0,5? А следващия месец магазинът сменя куриера и всички числа пак са грешни. При 50 000 поръчки и десет неща, които влияят, никой не може да ги нагласи на ръка.

Идеята

Правилото от примерите

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

Всеки пример има две части:

  • признаци (features): това, което знаеш за поръчката, преди да тръгне. Тук са три: разстоянието, теглото и колко пратки има куриерът в този ден;
  • етикет (label): отговорът, който искаш да познаеш. Тук са дните, за които е пристигнала.
// минали поръчки: km, kg, пратки на куриера в деня
static final double[][] ORDERS = {
{15, 1.2, 18}, {40, 0.8, 35}, {60, 3.5, 20},
{120, 2.0, 22}, {150, 12.0, 25}, {180, 1.5, 40},
{220, 0.5, 15}, {250, 8.0, 30}, {300, 2.5, 45},
{320, 18.0, 20}, {380, 1.0, 28}, {400, 6.0, 42},
};
// за колко дни пристигна всяка
static final int[] DAYS = {1, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 5};

Това, което се получава от примерите и дава отговор за нова поръчка, се казва модел. Има много видове модели. Започваме с най-простия, в който се вижда всяко число.

Идеята

Най-близките съседи

Как би познал ти? Ще потърсиш минали поръчки, които приличат на новата: подобно разстояние, подобно тегло, подобно натоварване. Щом те са пътували по 3 дни, и новата вероятно ще пътува толкова.

Това е модел: k най-близки съседи (k-NN). Пази всички минали поръчки. За нова поръчка намира k-те най-близки и връща средното на дните им. Тук k = 3.

„Близо“ значи малко разстояние между две поръчки. Разликата по всеки признак се повдига на квадрат, квадратите се събират, и от сумата се взима корен, както разстоянието между две точки на картата.

Има капан. Километрите са стотици, килограмите са единици. Ако събереш разликите направо, 10 kg разлика тежи колкото 10 km, тоест нищо, и километрите решават сами. Затова всяка разлика се дели на обхвата на признака (най-голямото минус най-малкото в данните). Така всеки признак е в една мярка: 1 е „от единия край до другия“.

// колко са раздалечени стойностите на всеки признак: max − min
static double[] ranges(double[][] rows) {
double[] range = new double[rows[0].length];
for (int j = 0; j < range.length; j++) {
double min = rows[0][j], max = rows[0][j];
for (double[] row : rows) {
min = Math.min(min, row[j]);
max = Math.max(max, row[j]);
}
range[j] = max - min;
}
return range;
}
static double distance(double[] a, double[] b) {
double sum = 0;
for (int j = 0; j < a.length; j++) {
double d = (a[j] - b[j]) / RANGE[j];
sum += d * d;
}
return Math.sqrt(sum);
}

Виж го

Трите най-близки до новата поръчка

static Integer[] nearest(double[] order) {
Integer[] near = new Integer[ORDERS.length];
double[] dist = new double[ORDERS.length];
for (int i = 0; i < ORDERS.length; i++) {
near[i] = i;
dist[i] = distance(order, ORDERS[i]);
}
Arrays.sort(near,
Comparator.comparingDouble(i -> dist[i]));
return near;
}
static double predict(double[] order, int k) {
Integer[] near = nearest(order);
double sum = 0;
for (int n = 0; n < k; n++) {
sum += DAYS[near[n]];
}
return sum / k;
}
Стъпка по стъпка, с думи
  1. Нова поръчка: 140 km, 10 kg, 30 пратки за куриера. Отдолу са 12-те минали поръчки и за колко дни е пътувала всяка.
  2. Разстоянието до всяка минала поръчка. Всяка разлика е делена на обхвата на признака си: km не надвикват kg.
  3. После подрежда поръчките по разстояние: най-подобните са най-горе.
  4. Взима първите k = 3: поръчка 5, поръчка 8, поръчка 4. Останалите не броят.
  5. Прогнозата е средното на дните им: (3 + 3 + 2) / 3 = 2,7 дни.
  6. Никой не е писал правило. Отговорът идва от миналите поръчки, които най-много приличат на новата.

Моделът мери разстоянието до всяка минала поръчка, подрежда ги и взима първите k:

static Integer[] nearest(double[] order) {
Integer[] near = new Integer[ORDERS.length];
double[] dist = new double[ORDERS.length];
for (int i = 0; i < ORDERS.length; i++) {
near[i] = i;
dist[i] = distance(order, ORDERS[i]);
}
Arrays.sort(near,
Comparator.comparingDouble(i -> dist[i]));
return near;
}
static double predict(double[] order, int k) {
Integer[] near = nearest(order);
double sum = 0;
for (int n = 0; n < k; n++) {
sum += DAYS[near[n]];
}
return sum / k;
}

Новата поръчка е 140 km, 10 kg и 30 пратки за куриера:

double[] order = {140, 10.0, 30};
System.out.printf("най-близките 3 до %.0f km, %.1f kg, %.0f пратки:%n", order[0], order[1], order[2]);
Integer[] near = nearest(order);
for (int n = 0; n < 3; n++) {
double[] o = ORDERS[near[n]];
System.out.printf(" поръчка %d: %.0f km, %.1f kg, %.0f пратки → %d дни (разстояние %.2f)%n",
near[n] + 1, o[0], o[1], o[2], DAYS[near[n]], distance(order, o));
}
System.out.printf("прогноза: %.1f дни%n", predict(order, 3));
Изход
най-близките 3 до 140 km, 10.0 kg, 30 пратки:
поръчка 5: 150 km, 12.0 kg, 25 пратки → 3 дни (разстояние 0.20)
поръчка 8: 250 km, 8.0 kg, 30 пратки → 3 дни (разстояние 0.31)
поръчка 4: 120 km, 2.0 kg, 22 пратки → 2 дни (разстояние 0.53)
прогноза: 2.7 дни

Най-близо е тежката пратка от 150 km. Поръчка 8 е на 100 km повече, но е тежка и в същия натоварен ден, затова е втора. Леката поръчка от 120 km е трета. Средното на 3, 3 и 2 дни е 2,7.

Пробвай

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

За поръчка от 230 km, 3 kg и 24 пратки трите най-близки са поръчка 4 (2 дни), поръчка 7 (2 дни) и поръчка 8 (3 дни), в този ред. Какво ще отпечата програмата?

double[] order = {230, 3.0, 24};
System.out.printf("%.1f %.1f%n", predict(order, 1), predict(order, 3));
Отговор

Идеята

Прогноза за нови поръчки

Моделът е готов в момента, в който има данните. Нови поръчки:

double[][] fresh = {{140, 10.0, 30}, {90, 1.0, 20}, {350, 4.0, 40}};
for (double[] order : fresh) {
System.out.printf("%.0f km, %.1f kg, %.0f пратки: %.1f дни%n",
order[0], order[1], order[2], predict(order, 3));
}
Изход
140 km, 10.0 kg, 30 пратки: 2.7 дни
90 km, 1.0 kg, 20 пратки: 1.3 дни
350 km, 4.0 kg, 40 пратки: 4.0 дни

Колко добре познава, не знаем от тези три числа. Честната проверка е върху поръчки, които моделът не е виждал, и е в урока за оценката.

Как се избира k? С k = 1 прогнозата е дните на една-единствена поръчка, а тя може да е изключение: празник, загубена пратка. С k равно на всички поръчки прогнозата е средното на всичко, и моделът спира да различава 15 km от 400 km. Обикновено се пробват няколко стойности и се взима тази, която греши най-малко на непознати поръчки.

Пробвай

Попълни

Прогнозата е средното на дните на k-те най-близки. На какво се дели сумата?

double sum = 0;
for (int n = 0; n < k; n++) {
    sum += DAYS[near[n]];
}
return sum / ;

Пробвай

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

Колега пише разстоянието наново. Програмата върви, но съседите на поръчката от 140 km и 10 kg вече са поръчки 5, 4, 6: тежката поръчка 8 от същия натоварен ден изпадна, а на нейно място влезе лека пратка. За поръчката от 90 km прогнозата става 1,7 дни вместо 1,3. На кой ред е грешката?

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

Идеята

Кога машинното учене е грешният инструмент

Моделът е правило, което никой не е написал. Това е и силата му, и цената му.

  • Правилото е известно и стабилно. Таксата за доставка е 4,90 € до 2 kg, 6,90 € над 2 kg, безплатна над 50 €. Това е if, не модел. Модел ще го научи само приблизително и понякога ще сгреши с няколко цента.
  • Трябва точен отговор или обяснение. Данъкът, отстъпката по договор, правото на връщане. Там „обикновено е така“ не стига, а „моделът реши“ не е обяснение за клиента.
  • Няма данни. Нов магазин без минали поръчки няма от какво да учи. Тогава правилото на око е честно начало, а данните идват после.
  • Светът се е сменил. Моделът знае само миналото. Нов куриер, празници, стачка: прогнозите от старите поръчки лъжат, докато не се учи наново.
  • Грешката струва скъпо. Модел, който греши в 1 от 20 случая, е добър за „пристига след 2 дни“ и лош за „пусни плащането“. Там до него стои правило или човек.

Машинното учене е за случаите, в които правилото има твърде много части, за да го напише човек, а примери има много.

По-точно, за любопитните

Колко струва една прогноза. k-NN не учи нищо предварително: при всяка нова поръчка сравнява с всички минали и ги подрежда. При n поръчки това е поне n разстояния на прогноза. За 12 поръчки е нищо, за 10 милиона е бавно. Затова истинските библиотеки ползват структури като k-d дърво, а при много признаци търсят приблизително. Какво значи „расте с n“, е в урока за Big-O.

Същото търсене, но за текст. Търсенето по смисъл в базите данни е k най-близки съседи върху вектори: урокът за векторите в курса по SQL.

Деленето на обхвата се казва min-max нормализиране. Друг начин е да извадиш средното и да делиш на стандартното отклонение. Защо мярката на признаците има значение и за други модели, ще видиш в урока за градиентното спускане.

Истинските инструменти. Тук всичко е цикли и масиви, за да видиш всяко число. Машинното учене на практика се прави главно на Python: NumPy за масивите, scikit-learn за класическите модели (там k-NN е KNeighborsRegressor), PyTorch за невронните мрежи. В C# има ML.NET, в Java DJL и Tribuo, в JavaScript TensorFlow.js и ONNX Runtime. Идеите са същите.

Обучението и пускането на модели в продукция (сървъри за обучение, deploy, следене дали моделът остарява) е инфраструктура и се учи в DevOps курса ZeroToOps.

Упражнение

Колко минути за опаковане

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

Складът иска да знае колко минути ще отнеме опаковането на поръчка, за да разпредели хората. Признаците са броят артикули и общото тегло, а етикетът са минутите. Ето десет минали поръчки:

// минали поръчки: брой артикули, kg
static final double[][] ORDERS = {
{1, 0.5}, {2, 1.0}, {3, 4.0}, {5, 2.0}, {6, 8.0},
{8, 3.0}, {10, 12.0}, {12, 5.0}, {15, 9.0}, {20, 6.0},
};
// минути за опаковане
static final int[] MINUTES = {4, 6, 9, 10, 15, 14, 22, 18, 25, 28};

Напиши k най-близки съседи с k = 3 и деление на обхвата и предскажи две нови поръчки: 4 артикула и 1,5 kg, и 11 артикула и 10 kg.

Изход
4 артикула, 1.5 kg: 6.7 мин
11 артикула, 10.0 kg: 20.7 мин

Още задачи

  • Без обхвата. Махни деленето на обхвата и виж кои съседи излизат. Кой признак решава сега?
  • Тежест. Дай на съседите тежест: по-близкият брои повече (например 1 / разстояние). Сменя ли се прогнозата?

Тест

Провери се

1. Магазинът учи модел, който казва за колко дни ще пристигне поръчка. Кое е етикетът?
2. Защо разстоянието дели всяка разлика на обхвата на признака?
3. Какво прави k най-близки съседи, когато дойде нова поръчка?
4. Таксата за доставка е 4,90 € до 2 kg и 6,90 € над 2 kg, а над 50 € е безплатна. С какво я смяташ?
5. Защо k = 1 е рисковано?

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