ML: Лінійна модель


Вступ

У цьому документі розглядається багатовимірна лінійна модель. Вона має самостійне значення в деяких задачах машинного навчання і лежить в основі складних архітектур глибоких нейронних мереж. Обговорюються математичні формулювання моделі та аналітичний пошук її параметрів. Наведені приклади коду на Python які можна знайти у файлі: ML_Line_Model.ipynb.

Розглянемо $n_x$-вимірний простір ознак. Кожен об'єкт у цьому просторі описується вектором $\mathbf{x}$. Задача полягає в перетворенні вихідного простору ознак в інший, $n_y$-вимірний простір $\mathbf{y}$. Величини $\mathbf{y}$, самі по собі, можуть мати практичний сенс і тоді це регресія або бути новими ознаками об'єктів, що визначають ймовірності в задачі класифікації.

Лінійна модель є найпростішим зв'язком входу $\mathbf{x}$ і виходу $\mathbf{y}$. Її матричний та індексний записи мають вигляд:

$$ \mathbf{y} = \mathbf{b} + \mathbf{x}\cdot\mathbf{w}, ~~~~~~~~~~~~~~~~~~~~ y_\alpha = b_\alpha + \sum_\beta (x_\beta\, w_{\beta\alpha}) $$

У процесі навчання необхідно підібрати оптимальні параметри моделі: матрицю вектор $b_\beta$ і $w_{\alpha\beta}$. Лінійна модель є першим нетривіальним наближенням розкладу в ряд Тейлора довільної функції $\mathbf{y}=f(\mathbf{x}\, \boldsymbol{\omega})$.


Лінійна модель як гіперплощина

Розглянемо геометричну інтерпретацію лінійної моделі. У $n$-вимірному просторі кожна точка описується $n$ дійсними координатами $\mathbf{x}=\{x_1,...,x_n\}$. Як і у звичайному 3-вимірному просторі, площина задається вектором нормалі $\boldsymbol{\omega}=\{w_1,...,w_n\}$ (перпендикуляр до площини) і довільною точкою $\mathbf{x}_0=\{x_{01},...,x_{0n}\}$, що лежить у цій площині (фіксує зміщення площини вздовж нормалі).
Коли $n > 3$ площину прийнято називати гіперплощиною.

Відстань $d$ від гіперплощини до деякої точки $\mathbf{x}=\{x_1,...,x_n\}$ обчислюється за формулою ($\boldsymbol{\omega}^2=1$):

$$ d = b+\mathbf{x}\cdot\boldsymbol{\omega}, ~~~~~~\text{де}~~~~~~~~~~ b ~=~ -\mathbf{x}_0\cdot\boldsymbol{\omega} ~=~ -( x_{01}w_1 + ... + x_{0n}w_{n} ). $$

При цьому $d > 0$, якщо точка $\mathbf{x}$ лежить з того боку гіперплощини, куди вказує вектор $\boldsymbol{\omega}$ і $d < 0$, якщо з протилежного. Коли $d=0$ - точка $\mathbf{x}$ лежить у гіперплощині ($b+\mathbf{x}\cdot\boldsymbol{\omega} = 0$ - її рівняння).

Зміна параметра $b$ зсуває площину паралельним чином у просторі. Якщо $b$ зменшується, то площина зміщується в напрямку вектора $\boldsymbol{\omega}$ (відстань $d$ менша), а якщо $b$ збільшується - площина зміщується проти вектора $\boldsymbol{\omega}$. Це безпосередньо випливає з наведеної вище формули.

◄ Запишемо вектор $\mathbf{x}-\mathbf{x}_0$, що починається в точці $\mathbf{x}_0$ (що лежить у площині) і спрямований у точку $\mathbf{x}$ (див. малюнок праворуч; вектори складаються за правилом трикутника). Положення точки $\mathbf{x}_0$ обрано в основі вектора $\boldsymbol{\omega}$, тому $\boldsymbol{\omega}$ і $\mathbf{x}-\mathbf{x}_0$ колінеарні (лежать на одній прямій). Якщо вектор $\boldsymbol{\omega}$ одиничний ($\boldsymbol{\omega}^2=1$), то скалярний добуток векторів $\mathbf{x}-\mathbf{x}_0$ і $\boldsymbol{\omega}$ дорівнює відстані точки $\mathbf{x}$ до площини:

$$ d = (\mathbf{x}-\mathbf{x}_0)\cdot \boldsymbol{\omega} ~=~ -\mathbf{x}_0\cdot\boldsymbol{\omega} + \mathbf{x}\cdot\boldsymbol{\omega} ~=~ b + \mathbf{x}\cdot\boldsymbol{\omega}. $$

Якщо довжина $\omega=|\boldsymbol{\omega}|$ вектора $\boldsymbol{\omega}$ нормалі до гіперплощини відрізняється від одиниці, то $d$ у $\omega$ разів більше ($\omega > 1$) або менше ($\omega < 1$) евклідової відстані у $n$-вимірному просторі. Коли вектори $\mathbf{x}-\mathbf{x}_0$ і $\boldsymbol{\omega}$ спрямовані в протилежні сторони: $d < 0$. ►

Якщо простір має $n$ вимірів, то гіперплощина це $(n-1)$-вимірний об'єкт.
Вона ділить весь простір на дві частини. Для наочності розглянемо 2-вимірний простір. Гіперплощиною в ньому буде пряма лінія (одновимірний об'єкт). Праворуч на малюнку кружечок зображає одну точку простору, а хрестик - іншу. Вони розташовані по різні боки від лінії (гіперплощини). Якщо довжина вектора $\boldsymbol{\omega}$ набагато більша за одиницю, то і відстані $d$ від точок до площини за модулем будуть суттєво більшими за одиницю.


Регресії (передбачення дійсного числа)

Найпростішим прикладом регресії є одновимірна лінійна функція $y = b + w \,x$, у якій параметр $w$ характеризує нахил прямої, а параметр $b$ - зсув (зміщення) по осі $y$ від початку координат. Наявність подібного зв'язку між $x$ і $y$, зазвичай, легко виявити візуально на графіку даних.

У багатовимірному випадку $y=b + w_1x_1+...w_n x_n$ візуалізувати дані складніше. Щоб переконатися в адекватності лінійної моделі, необхідно знайти оптимальні параметри і обчислити величину помилки (порівнявши її з помилками інших моделей, зокрема з тривіальною $y=b=\text{const}$).


Логістична регресія (задача класифікації)

У задачі класифікації об'єкт $\mathbf{x}$ потрібно віднести до одного або декількох класів, загальне число $C$ яких фіксоване. Зазвичай, результатом класифікації є $C$ умовних ймовірностей $p(c|\mathbf{x})$ того, що об'єкт з ознаками $\mathbf{x}$ належить до $c$-того класу: $c\in [0...C-1]$.

При використанні в задачі класифікації лінійної моделі, її виходи $y_\alpha$ "пропускають" через нелінійну функцію $z_\alpha=f(y_\alpha)$, значення якої знаходяться в діапазоні $[0...1]$. Чим ближче $z_\alpha$ до одиниці, тим впевненіше модель відносить об'єкт до класу з номером $\alpha$.

$$ z_{\alpha} ~=~f(\mathbf{y}) ~=~f\Bigr(b_\alpha + \sum_{\mu} x_{\mu}\, w_{\mu \alpha} \Bigr) $$

Якщо об'єкт може належати до декількох класів (класи перетинаються), то як $f(z)$ зручна сигмоїдна функція. Якщо на декількох виходах моделі виходять значення близькі до одиниці, то об'єкт належить усім цим класам. При великих $y_\alpha$ сигмоїд прямує до одиниці, а при $y_\alpha\to -\infty$ - до нуля: $\sigma(y_\alpha) = 1/(1+e^{-y_\alpha})$

Для класів, що не перетинаються, $C > 2$ (об'єкт $\mathbf{x}$ належить лише одному класу), зазвичай, застосовують функцію softmax. На її виході отримуються додатні значення, сума яких дорівнює одиниці. Тому їх можна інтерпретувати як розподіл ймовірностей $p(c|\mathbf{x})$: $$ z_\alpha = \mathrm{sm}(y_\alpha) = \displaystyle \frac{e^{y_\alpha}}{\sum_\beta e^{y_\beta}},~~~~~~~~~~~~~~~~~~~~~\sum_\alpha z_\alpha = 1,~~~~~~~~~z_\alpha > 0. $$

Якщо розділяючі площини для $\{y_0,y_1,y_2\}$ проведені правильно і класи лінійно роздільні, то для точки, що належить до класу $0$, значення виходів прямуватимуть до $\{1,0,0\}$, для класу $1$ - $\{0,1,0\}$ і т.д.


Формула Байєса та софтмакс

Існує чудовий математичний факт, який підводить "теоретичну основу" під використання функції софтмаксу. Нехай приклади кожного класу $c=[0...,C-1]$ у просторі ознак $\mathbf{x}$ утворюють "кластери" з нормальними розподілами, які мають однакову дисперсію $\mathbf{D}$ і різні середні $\bar{\mathbf{x}}_c$. Тоді умовна ймовірність $p(c|\mathbf{x})$ належності об'єкта з ознаками $\mathbf{x}$ до класу $c$, описується лінійною моделлю з софтмакс-функцією на виході: $$ P(\mathbf{x} | c) \sim e^{-\frac{1}{2}\,(\mathbf{x}-\bar{\mathbf{x}}_c)\,\mathbf{D}^{-1}(\mathbf{x}-\bar{\mathbf{x}}_c)}~~~~~~~~~~~~~ \Rightarrow~~~~~~~~~~~~~ p(c|\mathbf{x}) = \frac{e^{y_c}}{\sum_\alpha e^{y_\alpha}},~~~~~~~\mathbf{y} = \mathbf{b} + \mathbf{x}\mathbf{w}. $$

Природно, у реальних задачах класи в просторі ознак рідко задовольняють вихідну передумову. Однак, за допомогою нейронних мереж, часто вдається побудувати підходяще нелінійне перетворення, яке дає новий простір ознак. У цьому просторі об'єкти, що належать до різних класів, утворюють лінійно роздільні, компактні кластери класів. Нижче йде ідея доведення, яку можна пропустити.

🔥 З визначення умовної ймовірності випливає формула Байєса: $$ p(c|\mathbf{x}) = \frac{p(c,\mathbf{x})}{p(\mathbf{x})} = \frac{p(c)\,P(\mathbf{x}|c)}{p(\mathbf{x})} = \frac{p(c)\,P(\mathbf{x}|c)}{\sum_\alpha p(\alpha)\,P(\mathbf{x}|\alpha)}, $$ де $p(c)$ - ймовірності належності випадково обраного об'єкта до класу $c$. Розпишемо добуток $(\mathbf{x}-\bar{\mathbf{x}}_c)\,\mathbf{D}^{-1}(\mathbf{x}-\bar{\mathbf{x}}_c)$ під експонентою в $P(\mathbf{x} | c)$. Доданок $-(1/2)\,\mathbf{x}\,\mathbf{D}^{-1}\mathbf{x}$, присутній і в чисельнику, і в знаменнику формули Байєса скорочується. Решту доданків і множник $p(c)$ можна факторизувати таким чином (обернена матриця дисперсії $\mathbf{D}^{-1}$ симетрична): $$ p(c)\,P(\mathbf{x}|c) \sim e^{b_c + \mathbf{x}\,\mathbf{w}_c},~~~~~~~~~ b_c = -\frac{1}{2}\,\bar{\mathbf{x}}_c\,\mathbf{D}^{-1}\bar{\mathbf{x}}_c + \log\, p(c),~~~~~~~~~~ \mathbf{w}_c = \mathbf{D}^{-1}\bar{\mathbf{x}}_c. $$


Батчі та перемноження матриць

Нехай є 2 ознаки: $\{x_0,x_1\}$ (вхід моделі), які визначають три цільові величини: $\{y_0,y_1,y_2\}$ (вихід). Лінійний зв'язок між виходом і входом: $$ \left\{\begin{array}{lcl} y_0 &=& b_0+ x_0 \,w_{00} + x_1\, w_{10} \\ y_1 &=& b_1+ x_0 \,w_{01} + x_1\, w_{11} \\ y_2 &=& b_2+ x_0 \,w_{02} + x_1\, w_{12} \end{array} \right. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ \mathbf{w} = \overbrace{ \begin{array}{|c|c|c|} \hline w_{00} & w_{01} & w_{02} \\ \hline w_{10} & w_{11} & w_{12} \\ \hline \end{array} }^{\displaystyle n_y} \left. \phantom{ \begin{array}{c} \\ \\ \end{array} } \right\} n_x $$ можна записати в табличному вигляді (крапка - це dot-множення рядка на стовпець):

$$ \begin{array}{|c|c|} \hline b_{0} & b_{1} & b_{2} \\ \hline \end{array} ~~~+~~~ \begin{array}{|c|c|} \hline x_0 & x_1 \\ \hline \end{array} ~\cdot~ \begin{array}{|c|c|c|} \hline w_{00} & w_{01} & w_{02} \\ \hline w_{10} & w_{11} & w_{12} \\ \hline \end{array} ~~~=~~~ \begin{array}{|c|c|} \hline y_{0} & y_{1} & y_{2} \\ \hline \end{array} $$

В алгоритмах машинного навчання обчислення відбувається не з одним прикладом, а одразу з деякою їх множиною - пачкою (batch). Тому подамо $\mathbf{x}$ у вигляді 2D матриці, розташувавши приклади порядково (перший індекс, починаючи з нуля - номер прикладу, а другий - номер ознаки в даному прикладі).
Нижче наведено $N=4$ приклади, $n_x=2$ ознаки і $n_y=3$ виходи моделі:

$$ \begin{array}{|c|c|c|} \hline b_{0} & b_{1} & b_{2}\\ \hline \end{array} ~~~+~~~ \begin{array}{|c|c|} \hline x_{00} & x_{01} \\ \hline x_{10} & x_{11} \\ \hline x_{20} & x_{21} \\ \hline x_{30} & x_{31} \\ \hline \end{array} ~\cdot~ \begin{array}{|c|c|c|} \hline w_{00} & w_{01} & w_{02} \\ \hline w_{10} & w_{11} & w_{12} \\ \hline \end{array} ~~~=~~~ \left. \begin{array}{|c|c|c|} \hline y_{00} & y_{01} & y_{02} \\ \hline y_{10} & y_{11} & y_{12} \\ \hline y_{20} & y_{21} & y_{22} \\ \hline y_{30} & y_{31} & y_{32} \\ \hline \end{array} \right\} \text{приклади} $$

Розмірності індексів (атрибут shape у numpy) матриці добутку $\mathbf{x}\cdot\mathbf{w}$ дорівнюють: $$ (N,n_x) . (n_x,n_y) = (N,n_y). $$ При згортці матриць: $(\mathbf{x}\cdot\mathbf{w})_{i\alpha} = \sum_\mu x_{i\mu}\,w_{\mu\alpha}$ діє принцип "рядок на стовпець" і розмірність середнього індексу скорочується. До кожного рядка матриці $\mathbf{x}\cdot\mathbf{w}$ форми (N,ny) додається вектор $\mathbf{b}$ форми (ny,).
У бібліотеці numpy це відповідає правилу розширення (broadcasting).


Тензорний аналіз

Далі будуть обчислюватися похідні за тензорами і використовуватимуться матричні позначення. Тому нагадаємо деякі важливі моменти.


☝ Матриця, обернена до матриці $\mathbf{A}$, позначається як $\mathbf{A}^{-1}$, а транспонована як $\mathbf{A}^{\top}$. У транспонованої матриці колонки і рядки (індекси) переставлені місцями: $A^\top_{ij}=A_{ji}$, а добуток оберненої на вихідну дає одиничну матрицю (символ Кронекера). Вони мають такі властивості: $$ \mathbf{A}^{-1}\cdot\mathbf{A}=\mathbf{A}\cdot\mathbf{A}^{-1}=\mathbf{1},~~~~~~~~~~ (\mathbf{A}\cdot\mathbf{B})^{-1} = \mathbf{B}^{-1}\cdot\mathbf{A}^{-1},~~~~~~~~~~ (\mathbf{A}\cdot\mathbf{B})^{\top} = \mathbf{B}^{\top}\cdot\mathbf{A}^{\top}. $$


☝ Справедлива формула для часткової похідної (три крапки - це інші індекси):

$$ \frac{\partial W_{\mu\nu...}}{\partial W_{\alpha\beta...}} = \delta_{\mu\alpha}\,\delta_{\nu\beta}\,...,~~~~~~~~~~~~~~ \sum_{\mu} T_{...\mu...}\,\delta_{\mu\alpha} = T_{...\alpha...}, $$

де $\delta_{\alpha\beta}$ - одинична квадратна матриця (символ Кронекера), що дорівнює $0$, якщо $\alpha\neq\beta$ і $1$, якщо $\alpha=\beta$. При підсумовуванні із символом Кронекера (за одним із його індексів), сума і символ Кронекера прибираються, а сумаційний індекс замінюється на другий індекс символу Кронекера (вище друга формула).

Формула для $\partial W_{\mu\nu}/\partial W_{\alpha\beta}$, означає, що похідна дорівнює нулю, якщо $\mu\neq\alpha$ і $\nu\neq\beta$. Наприклад, $\partial W_{12}/\partial W_{13} = 0$ так це різні змінні (типу $\partial y/\partial x = 0$), а $\partial W_{12}/\partial W_{12} = 1$ (типу $\partial x/\partial x = 1$).


Аналітичне рішення

У лінійній моделі з mse-помилкою аналітично нескладно знайти оптимальні параметри. Обчислення виявляються помітно простіше, якщо вектор $\mathbf{b}$ і матрицю $\mathbf{w}$ об'єднати в одну матрицю $\mathbf{W}$, додавши в $\mathbf{w}$, як ще один рядок, вектор $\mathbf{b}$. Додатково в матрицю $\mathbf{x}$ додається колонка, що складається з одиниць, що дає матрицю $\mathbf{X}$:

$$ \mathbf{X}\cdot\mathbf{W} ~=~ \begin{array}{|c|c|c|} \hline x_{00} & x_{01} & ~1~ \\ \hline x_{10} & x_{11} & 1 \\ \hline x_{20} & x_{21} & 1 \\ \hline x_{30} & x_{31} & 1 \\ \hline \end{array} ~\cdot~ \begin{array}{|c|c|} \hline w_{00} & w_{01} & w_{02} \\ \hline w_{10} & w_{11} & w_{12} \\ \hline b_{0} & b_{1} & b_{2} \\ \hline \end{array} ~=~ \begin{array}{|c|c|c|} \hline y_{00} & y_{01} & y_{02} \\ \hline y_{10} & y_{11} & y_{12} \\ \hline y_{20} & y_{21} & y_{22} \\ \hline y_{30} & y_{31} & y_{32} \\ \hline \end{array} ~=~ \mathbf{y}. $$ Такий запис означає введення ще однієї фіктивної ознаки, завжди рівної одиниці: $\mathbf{x}=\{x_0,...,x_{n-1},1\}$.

Тепер модель має вигляд: $\mathbf{y} = \mathbf{X}\cdot\mathbf{W}$, що, як неважко перевірити, еквівалентно вихідному запису $\mathbf{y}=\mathbf{b} + \mathbf{x}\mathbf{w}$. Знайдемо мінімум mse-помилки $L$ (опускаємо постійний множник):

$$ L = \sum_{i,\nu} (y_{i\nu} -\hat{y}_{i\nu} )^2,~~~~~~~~~~~~~~~ y_{i\nu} = \sum_{\mu} X_{i\mu} W_{\mu\nu}. $$

Запишемо похідні помилки і виходу моделі: $$ \frac{\partial L}{\partial y_{i\nu}}~=~ 2(y_{i\nu} -\hat{y}_{i\nu} ),~~~~~~~~ \frac{\partial y_{i\nu}}{\partial W_{\alpha\beta}} = \sum_{\mu} X_{i\mu}\,\delta_{\mu\alpha}\,\delta_{\nu\beta}. ~=~X_{i\alpha} \,\delta_{\nu\beta}. $$ Тепер неважко знайти похідну помилки $L$ за елементами матриці $W_{\alpha\beta}$: $$ \frac{\partial L}{\partial W_{\alpha\beta}} = \sum_{i,\nu} \frac{\partial L}{\partial y_{i\nu}}\,\frac{\partial y_{i\nu}}{\partial W_{\alpha\beta}} ~=~ 2 \sum_{i} (y_{i\beta} -\hat{y}_{i\beta} )\, X_{i\alpha} ~=~2 \bigr[\mathbf{X}^\top\cdot(\mathbf{y}-\hat{\mathbf{y}} )\bigr]_{\alpha\beta}. $$

Прирівнявши похідну нулю (умова екстремуму, і в даному випадку мінімуму $L$), отримуємо матричне рівняння $\mathbf{X}^\top\cdot(\mathbf{X}\cdot\mathbf{W}-\hat{\mathbf{y}} ) = 0$, яке легко розв'язується:

$$ \mathbf{W} = (\mathbf{X}^\top\cdot \mathbf{X})^{-1}\cdot (\mathbf{X}^\top\cdot \hat{\mathbf{y}}) ~~~~~~~~\text{або}~~~~~~~~~~~~~~ \mathbf{W}= \mathbf{X}^{-1} \cdot \hat{\mathbf{y}}. $$

У принципі, можна користуватися будь-якою з цих формул. Перша формула виявляється швидшою, якщо прикладів N суттєво більше, ніж ознак nX = число входів $\mathbf{x}$. Шукати обернену матрицю для квадратної матриці $\mathbf{X}^\top\cdot \mathbf{X}$ форми (nX+1, nX+1) простіше, ніж для матриці $\mathbf{X}^{-1}$ форми (nX+1, N) у другій формулі, хоча при цьому додається перемноження матриць $\mathbf{X}^\top\cdot \mathbf{X}$.


Приклад на numpy

Проведемо обчислення за отриманими формулами за допомогою бібліотеки numpy. Короткий вступ до numpy можна знайти в документі NN_Base_Numpy.

Згенеруємо спочатку модельні дані, в яких виходи лінійно пов'язані зі входами і доданий невеликий випадковий шум:

import numpy as np                                    # робота з тензорами

N      = 100                                          # число точок  (прикладів)
nX, nY = 2, 3                                         # число входів (ознак) і виходів

w = np.array([[  1,  3,  5],                          # матриця w форми (nX,nY)
              [  6,  4,  2] ]) 
b = np.array( [ -3, -2, -1]  )                        # вектор  b форми (nY, )

X  = np.random.random ((N, nX))                       # рівномірні випадк.точки в [0...1]
Y  = np.dot(X, w) + b                                 # виходи моделі
Y += np.random.normal (0, 0.01, (N, nY))              # додаємо випадковий гаусів шум

Отримаємо матриці лінійної моделі за першою і другою формулою. Оскільки матриця в другій формулі не квадратна, для пошуку оберненої до неї матриці використовуємо функцію pinv, а не inv, як у першому випадку:

from numpy.linalg import inv, pinv                    # обернені для квадрат. і прямокут.

XX = np.concatenate( (X, np.ones((N, 1))), axis=1)    # додаємо стовпчик одиниць

W1 = np.dot( inv(np.dot(XX.T, XX)), np.dot(XX.T, Y) ) # перша формула
W2 = np.dot( pinv(XX), Y)                             # друга формула


Регресія на sklearn

Для розв'язання цієї ж задачі, на практиці зручніше користуватися готовою бібліотекою машинного навчання Scikit-learn (коротко sklearn):
from sklearn.linear_model import LinearRegression

lr = LinearRegression().fit(X,Y)   

print(lr.coef_.T)                   #  [[1.007  3.001  5.001]  матриця W
                                    #   [6.003  4.     1.998]]
print(lr.intercept_)                #  [-3.002 -2.001 -0.999]  вектор b

Виклик LinearRegression() створює екземпляр класу даного методу навчання, якому через функцію fit передаються навчальні дані X,Y. Ця функція знову повертає екземпляр класу, в атрибутах якого знаходяться параметри моделі. Звернемо увагу, що матриця $\mathbf{w}$ (атрибут lr.coef_) у sklearn зберігається в транспонованому вигляді (порівняно з прийнятим вище).

Отримавши параметри моделі, можна тепер обчислити її mse-помилку:

mse =  np.mean((X @ lr.coef_.T + lr.intercept_ - Y) ** 2)

print(f"mse:{mse:.5f}, sqrt(mse):{np.sqrt(mse):.5f} ")
#       mse:0.00010,   sqrt(mse):0.01005  


Класифікація на sklearn

Задача класифікації з нелінійними функціями сигмоїда або софтмаксу на виході лінійної моделі не має такого простого розв'язку. Тому доводиться застосовувати чисельні методи, з яких у машинному та глибокому навчанні виявився найефективнішим градієнтний спуск. Тим не менше, у лінійному випадку можна успішно розв'язати задачу багатокласової класифікації, відмовившись від нелінійності на виході моделі.

Розглянемо для наочності двовимірний простір ознак $\mathbf{x}=\{x_0, x_1\}$ і три класи $c=\{0,1,2\}$, до одного з яких потрібно віднести об'єкт. У моделі буде три виходи: $n_y=3$, на яких, як цільові значення, вимагатимемо для першого, другого і третього класів відповідно: $$ \mathbf{y}=\{+1,-1,-1\},~~~~~\mathbf{y}=\{-1,+1,-1\},~~~~~\mathbf{y}=\{-1,-1,+1\}. $$ Як і раніше, будемо використовувати середньоквадратичну помилку, для якої є точний розв'язок. Природно, лінійна модель не зможе для всіх прикладів досягти цільових значень. Однак, три виходи моделі $\mathbf{y}=\{d_0,d_1,d_2\}$ - це три знакозмінні відстані від точки $\mathbf{x}$ до трьох площин. Відстані додатні, якщо вектор нормалі "дивиться" на точку, і від'ємні - якщо в протилежний бік. Метод найменших квадратів прагнутиме розгорнути ці вектори, щоб, принаймні, збігалися знаки виходу моделі і цільових значень. Це дасть необхідний класифікатор.

Підготуємо модельні дані у вигляді трьох кластерів з нормальним розкидом точок навколо їхніх центрів:
nx, ny, N = 2, 3, 300                    # число ознак, класів, об'єктів (точок)

C = np.zeros(N)                          # номери правильних класів
Y = np.full((N,ny), -1)                  # матриця N x ny для виходів, заповнена -1
X = np.random.normal (0, 0.4, (N,nx))    # хмара N точок з центром у початку координат

X0 = [[-1,1], [1,1], [0,-1]]             # центри кластерів кожного класу
nc = int(N/ny)                           # число прикладів кожного класу

for i, x0 in enumerate(X0): 
    C[i*nc:(i+1)*nc]    = i              # номер i-того класу
    Y[i*nc:(i+1)*nc, i] = 1              # цільові виходи
    X[i*nc:(i+1)*nc]   += np.array(x0)   # зсуваємо кластер i-того класу

Модель, яка проводить класифікацію, оформимо у вигляді класу:

class Model:
    def __init__(self):
        "Конструктор"
        self.lr = LinearRegression()
        
    def __call__(self, x):
        "Функціональний виклик об'єкта"
        y = x @ self.lr.coef_.T + self.lr.intercept_
        return np.argmax(y, axis=1)
    
    def fit(self, x,y):
        "Навчання за прикладами x,y"
        self.lr.fit(x,y)

Процес навчання і тестування виглядає наступним чином:
model = Model()       # екземпляр моделі

model.fit(X,Y)        # навчаємо

y = model(X)          # отримуємо номери класів
A = (y==C).mean()     # порівнюємо з точними (акуратність)

Щоб отримати графік як на початку розділу, потрібно додати до класу Model наступний метод:

.   def plot(self, x1_min=0, x1_max=1, x2_min=0, x2_max=1, n=201):
        # сітка точок 201 x 201 в інтервалі x1,x2=[xMin,xMax]
        x1,x2 = np.meshgrid(np.linspace(x1_min, x1_max, n),
                            np.linspace(x2_min, x2_max, n))
        grid  = np.c_[x1.ravel(), x2.ravel()]     # shape = (n*n, 2)        
            
        yp = self(grid).reshape(x1.shape)         # (n*n, ) -> (n, n)
        
        fig,ax = plt.subplots(figsize=(5, 5))     # розміри картинки (квадрат)        
        plt.axis([x1_min,x1_max, x2_min,x2_max])  # діапазон зміни осей    
        
        plt.imshow(yp, interpolation='nearest',   # кольор. карту для 2D масиву yp
                  extent=(x1.min(), x1.max(), x2.min(), x2.max()),
                  aspect='auto', origin='lower', alpha=0.2,
                  vmin=0, vmax=yp.max())        

За допомогою цього методу малюємо розділяючі поверхні та навчальні об'єкти:
model.plot(-2, 2,  -2, 2)
plt.scatter(X[:,0], X[:,1], c=C, s=30, edgecolors='black')
plt.show()