Практика · Нейрон і перцептрон> Лекція: lecture.html · Тест: quiz.html · Домашнє: homework.mdТа сама дошка оголошень про вживані телефони, що й утемі 08 та темі 14.Шукаємо приманки із заниженою ціною за двома ознаками: ціною оголошення йдовідковою ціною такого апарата.Що ми зробимо:1. зберемо ту саму таблицю й дві ознаки, за якими межу видно на площині;2. напишемо перцептрон з нуля — десяток рядків, жодної бібліотеки;3. звіримо перші чотири кроки з тими, що пораховані руками в лекції;4. навчимо його на роздільних даних і подивимось, як кількість виправлень спадає до нуля;5. порівняємо результат зі sklearn.linear_model.Perceptron;6. повернемо на дошку приманки із завищеною ціною — і побачимо, що правило не сходиться ніколи;7. поставимо на ті самі дані LogisticRegression і подивимось, чим вона рятує;8. зберемо XOR із трьох нейронів ручними вагами.Зерно генератора зафіксовано (np.random.default_rng(42)), тож числа збігатимутьсяз лекцією до цифри.¶

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.linear_model import Perceptron, LogisticRegression

# зерно фіксує всю випадковість: у тебе вийдуть точно ті самі числа, що в лекції
rng = np.random.default_rng(42)

pd.set_option("display.width", 140)
pd.set_option("display.max_columns", 16)
np.set_printoptions(suppress=True, linewidth=140)

print("numpy", np.__version__, "· pandas", pd.__version__)
numpy 2.5.1 · pandas 2.3.3

1 · Збираємо ту саму дошкуЦей блок — код із практики теми 08 без змін.Ми його не пояснюємо повторно, а просто відтворюємо таблицю, щоб числа збіглися.¶

In [2]:
кількість = 1200

моделі = ["Alfa A5", "Alfa A7", "Beta 12", "Beta 12 Pro", "Gamma X", "Gamma X Ultra"]
ціна_нового = {"Alfa A5": 5200, "Alfa A7": 7400, "Beta 12": 12000,
               "Beta 12 Pro": 17500, "Gamma X": 24000, "Gamma X Ultra": 34000}
частки_моделей = [0.24, 0.22, 0.18, 0.16, 0.12, 0.08]

модель = rng.choice(моделі, size=кількість, p=частки_моделей)
рік = rng.integers(2017, 2025, size=кількість)
стан = rng.choice(["нове", "дуже добре", "добре", "задовільне"],
                  size=кількість, p=[0.08, 0.32, 0.42, 0.18])
памʼять = rng.choice([64, 128, 256, 512], size=кількість, p=[0.30, 0.38, 0.24, 0.08])
вік_акаунта = np.round(rng.exponential(420, size=кількість) + 3).astype(int)

базова = np.array([ціна_нового[m] for m in модель])
знос = 0.82 ** (2024 - рік)
коефіцієнт_стану = np.array(
    [{"нове": 1.0, "дуже добре": 0.88, "добре": 0.75, "задовільне": 0.58}[s] for s in стан])
коефіцієнт_памʼяті = np.array(
    [{64: 0.85, 128: 1.0, 256: 1.15, 512: 1.32}[m] for m in памʼять])

типова_ціна_за_паспортом = базова * знос * коефіцієнт_стану * коефіцієнт_памʼяті
ціна = типова_ціна_за_паспортом * rng.lognormal(0, 0.13, size=кількість)

print("перші пʼять цін:", ціна[:5].round(0))
перші пʼять цін: [ 3085.  2810. 10822.  8346.  1852.]
In [3]:
# шахрай тим імовірніший, чим молодший акаунт; ціну він або занижує (приманка),
# або завищує під велику передоплату
шанс_шахрайства = 0.10 + 0.30 * np.exp(-вік_акаунта / 120)
шахрайське = rng.random(кількість) < шанс_шахрайства

ставить_дешево = rng.random(кількість) < 0.74
дешева_приманка = шахрайське & ставить_дешево
дорога_приманка = шахрайське & ~ставить_дешево

ціна[дешева_приманка] = (типова_ціна_за_паспортом[дешева_приманка]
                         * rng.uniform(0.20, 0.45, дешева_приманка.sum()))
ціна[дорога_приманка] = (типова_ціна_за_паспортом[дорога_приманка]
                         * rng.uniform(2.6, 3.8, дорога_приманка.sum()))
ціна = np.round(ціна, -1)

скарг = np.where(шахрайське, 1 + rng.poisson(3.0, кількість), rng.poisson(0.03, кількість))

дошка = pd.DataFrame({
    "модель": модель, "рік": рік, "стан": стан, "памʼять_гб": памʼять,
    "вік_акаунта": вік_акаунта, "скарг": скарг, "ціна": ціна,
    "шахрайське": шахрайське.astype(int),
})
print("шахрайських оголошень:", int(дошка["шахрайське"].sum()), "з", кількість)
шахрайських оголошень: 196 з 1200
In [4]:
# ті самі шість неприємностей із теми 08 — колекційні, одруки, памʼять текстом,
# пропуски в ціні та стані, дублікати
колекційні = дошка.index[дошка["модель"] == "Gamma X"][:4]
дошка.loc[колекційні, ["рік", "стан", "памʼять_гб"]] = [2017, "нове", 512]
дошка.loc[колекційні, "ціна"] = [82000.0, 88000.0, 91000.0, 95000.0]
дошка.loc[колекційні, ["шахрайське", "скарг"]] = 0

одруки = дошка.index[(дошка["ціна"] > 7000) & (дошка["ціна"] < 9600)
                     & (дошка["шахрайське"] == 0)][:2]
дошка.loc[одруки, "ціна"] = дошка.loc[одруки, "ціна"] * 10

памʼять_текстом = дошка["памʼять_гб"].astype(str)
із_одиницями = rng.random(len(дошка)) < 0.18
памʼять_текстом[із_одиницями] = памʼять_текстом[із_одиницями] + " ГБ"
дошка["памʼять_гб"] = памʼять_текстом

ймовірність_пропуску = np.where(дошка["шахрайське"] == 1, 0.25, 0.03)
дошка.loc[rng.random(len(дошка)) < ймовірність_пропуску, "ціна"] = np.nan
дошка.loc[rng.random(len(дошка)) < 0.04, "стан"] = np.nan

повтори = rng.choice(дошка.index, size=12, replace=False)
дошка = pd.concat([дошка, дошка.loc[повтори]], ignore_index=True)

assert дошка.shape == (1212, 8), "форма розійшлась із темою 22"
print("таблиця як у темі 08:", дошка.shape)
таблиця як у темі 08: (1212, 8)

2 · Дві ознаки, за якими межу видно на площиніПерша ознака — ціна оголошення. Друга — довідкова ціна: скільки такийапарат такої моделі, року, стану й памʼяті коштує за каталогом. Це та сама ідея,що й у темі 10: сира ціна нічого неозначає, поки немає з чим її порівняти.Обидві переводимо в тисячі гривень, щоб числа були зручного розміру.¶

In [5]:
дошка["памʼять_гб"] = pd.to_numeric(дошка["памʼять_гб"].str.replace(" ГБ", "", regex=False))
дошка = дошка.drop_duplicates().reset_index(drop=True)

# стан — порядкова шкала: чим більше, тим кращий апарат
бали_стану = {"задовільне": 1, "добре": 2, "дуже добре": 3, "нове": 4}
дошка["стан_бал"] = дошка["стан"].map(бали_стану).fillna(2).astype(int)

# без ціни неможливо порахувати жодну з цінових ознак, тож ці рядки відкладаємо
оголошення = дошка.dropna(subset=["ціна"]).reset_index(drop=True)

# довідкова ціна: ціна нового апарата, зменшена за роками, станом і памʼяттю
оголошення["довідкова"] = (
    оголошення["модель"].map(ціна_нового)
    * 0.82 ** (2024 - оголошення["рік"])
    * оголошення["стан_бал"].map({1: 0.58, 2: 0.75, 3: 0.88, 4: 1.00})
    * оголошення["памʼять_гб"].map({64: 0.85, 128: 1.00, 256: 1.15, 512: 1.32})
).round(-1)

print("оголошень із відомою ціною:", len(оголошення))
print(оголошення[["модель", "рік", "стан", "памʼять_гб", "ціна", "довідкова",
                  "шахрайське"]].head())
оголошень із відомою ціною: 1100
        модель   рік   стан  памʼять_гб     ціна  довідкова  шахрайське
0  Beta 12 Pro  2017  добре         256   3090.0     3760.0           0
1      Alfa A7  2021    NaN         128   2810.0     3060.0           0
2      Gamma X  2017   нове         512  82000.0     7900.0           0
3  Beta 12 Pro  2021  добре         256  83500.0     8320.0           0
4      Alfa A5  2020  добре          64   1850.0     1500.0           0

3 · Звужуємо задачу до приманок із заниженою ціноюУ лекції ми домовились шукати саме занижені ціни, тож лишаємо оголошення,де ціна не перевищує довідкову. Це відсіює приманки з завищеною ціною (їхповернемо в розділі 8) і заразом колекційні апарати та одруки в ціні —вони всі дорожчі за довідник.¶

In [6]:
роздільна_дошка = оголошення[оголошення["ціна"] <= оголошення["довідкова"]].reset_index(drop=True)

X = np.c_[роздільна_дошка["ціна"] / 1000, роздільна_дошка["довідкова"] / 1000]
# мітки для перцептрона беремо у вигляді ±1: так правило виправлення виглядає найпростіше
y = np.where(роздільна_дошка["шахрайське"] == 1, 1, -1)

print("оголошень:", len(X), "· приманок:", int((y == 1).sum()),
      f"({(y == 1).mean() * 100:.1f} %)")
print("модель «усе чесне» помилилась би", int((y == 1).sum()), "рази")
print()
відношення = X[:, 0] / X[:, 1]
print("ціна / довідкова у чесних: від", round(відношення[y == -1].min(), 4),
      "до", round(відношення[y == -1].max(), 4))
print("ціна / довідкова у приманок: від", round(відношення[y == 1].min(), 4),
      "до", round(відношення[y == 1].max(), 4))
оголошень: 569 · приманок: 104 (18.3 %)
модель «усе чесне» помилилась би 104 рази

ціна / довідкова у чесних: від 0.5647 до 1.0
ціна / довідкова у приманок: від 0.1814 до 0.4495

Числа вище і є вся суть: у чесних оголошень ціна не падає нижче 0.56 довідкової,у приманок не піднімається вище 0.45. Між цими значеннями є проміжок, тобтопряма, яка розділяє класи без жодної помилки, існує. Дані лінійно роздільні.

4 · Перцептрон з нуляУся модель — три числа: дві ваги й зсув. Усе навчання — цикл по прикладах, у якомуваги зсуваються лише тоді, коли нейрон помилився.¶

In [7]:
def навчити_перцептрон(ознаки, мітки, крок=0.08, ваги=None, зсув=0.0, епох=30):
    # Правило Розенблата. Повертає ваги, зсув і кількість виправлень у кожній епосі.
    ваги = np.zeros(ознаки.shape[1]) if ваги is None else np.array(ваги, dtype=float)
    виправлень_за_епоху = []
    for _ in range(епох):
        виправлень = 0
        for x, правильна in zip(ознаки, мітки):
            z = ваги @ x + зсув
            відповідь = 1 if z >= 0 else -1
            if відповідь != правильна:
                # зсуваємо ваги рівно на вхід цього прикладу, взятий зі знаком мітки
                ваги = ваги + крок * правильна * x
                зсув = зсув + крок * правильна
                виправлень += 1
        виправлень_за_епоху.append(виправлень)
        # епоха без жодного виправлення означає, що ваги вже не зміняться ніколи
        if виправлень == 0:
            break
    return ваги, зсув, виправлень_за_епоху


# крихітна перевірка на чотирьох очевидних точках: дві дешеві приманки й дві чесні
іграшкові_ознаки = np.array([[1.0, 5.0], [2.0, 9.0], [5.0, 5.2], [8.0, 8.4]])
іграшкові_мітки = np.array([1, 1, -1, -1])
в, з, іст = навчити_перцептрон(іграшкові_ознаки, іграшкові_мітки)
print("виправлень за епоху на іграшковому наборі:", іст)
print("знайдені ваги:", в.round(3), "· зсув:", round(з, 3))
виправлень за епоху на іграшковому наборі: [1, 3, 0]
знайдені ваги: [-0.56   0.288] · зсув: 0.0

5 · Звіряємо перші чотири кроки з лекцієюУ лекції ми пройшли чотири кроки руками на 14 оголошеннях. Повторимо їх кодомі перевіримо кожне число — так ми переконаємось, що формула в коді та сама, щой на папері.¶

In [8]:
чотирнадцять = np.array([
    [1.45, 5.49,  1], [1.54, 5.38,  1], [1.92, 2.06, -1], [5.46, 5.65, -1],
    [3.10, 10.99, 1], [8.78, 8.83, -1], [1.33, 1.38, -1], [3.77, 9.10,  1],
    [6.38, 6.55, -1], [1.40, 6.18,  1], [3.62, 3.71, -1], [2.92, 3.06, -1],
    [1.20, 1.39, -1], [3.02, 3.27, -1],
])
ознаки_14, мітки_14 = чотирнадцять[:, :2], чотирнадцять[:, 2]

ваги, зсув, крок = np.array([0.80, 0.20]), -6.00, 0.08
print(f"{'крок':>4} {'z':>10}  {'вердикт':<8} {'w1':>9} {'w2':>9} {'b':>8}")
for номер in range(4):
    x, правильна = ознаки_14[номер], мітки_14[номер]
    z = ваги @ x + зсув
    помилка = (1 if z >= 0 else -1) != правильна
    if помилка:
        ваги = ваги + крок * правильна * x
        зсув = зсув + крок * правильна
    print(f"{номер + 1:>4} {z:>10.4f}  {'ПОМИЛКА' if помилка else 'вгадав':<8} "
          f"{ваги[0]:>9.4f} {ваги[1]:>9.4f} {зсув:>8.4f}")
крок          z  вердикт         w1        w2        b
   1    -3.7420  ПОМИЛКА     0.9160    0.6392  -5.9200
   2    -1.0705  ПОМИЛКА     1.0392    1.0696  -5.8400
   3    -1.6414  вгадав      1.0392    1.0696  -5.8400
   4     5.8773  ПОМИЛКА     0.6024    0.6176  -5.9200
In [9]:
# ті самі числа стоять у лекції в розділі «Три кроки руками»
assert np.allclose(ваги, [0.6024, 0.6176]), "ваги розійшлися з лекцією!"
assert np.isclose(зсув, -5.92), "зсув розійшовся з лекцією!"
print("✅ усі чотири кроки збіглися з тим, що пораховано руками")
✅ усі чотири кроки збіглися з тим, що пораховано руками

6 · Навчання на всіх 569 оголошенняхТепер запускаємо правило на повних даних. Стежимо за кількістю виправлень:поки вона більша за нуль, ваги ще рухаються.¶

In [10]:
ваги_наші, зсув_наш, історія = навчити_перцептрон(X, y, крок=0.08, ваги=[0.80, 0.20], зсув=-6.0)

print("виправлень за епоху:", історія)
print("епох знадобилось:", len(історія))
print(f"ваги: w1 = {ваги_наші[0]:.4f}   w2 = {ваги_наші[1]:.4f}   b = {зсув_наш:.4f}")

наші_прогнози = np.where(X @ ваги_наші + зсув_наш >= 0, 1, -1)
print("помилок на навчальних даних:", int((наші_прогнози != y).sum()))
виправлень за епоху: [63, 19, 6, 4, 3, 2, 2, 2, 2, 1, 0]
епох знадобилось: 11
ваги: w1 = -4.8392   w2 = 2.8256   b = -0.7200
помилок на навчальних даних: 0

Правило зупинилось само: остання епоха пройшла без жодного виправлення.Це і є збіжність, обіцяна теоремою Розенблата, — вона працює саме тому, що даніроздільні.Подивимось на межу, яку знайшов наш перцептрон.

In [11]:
fig, вісь = plt.subplots(figsize=(7, 6))
вісь.scatter(X[y == -1, 0], X[y == -1, 1], s=10, c="#0f766e", label="чесне", alpha=0.6)
вісь.scatter(X[y == 1, 0], X[y == 1, 1], s=10, c="#c2185b", label="приманка", alpha=0.6)

# межа — це пряма w1*x1 + w2*x2 + b = 0, виражена через x2
сітка_цін = np.linspace(0, 36, 100)
межа = -(ваги_наші[0] * сітка_цін + зсув_наш) / ваги_наші[1]
вісь.plot(сітка_цін, межа, c="#111", lw=2, label="межа перцептрона")

вісь.set_xlim(0, 36); вісь.set_ylim(0, 36)
вісь.set_xlabel("ціна оголошення, тис. грн")
вісь.set_ylabel("довідкова ціна, тис. грн")
вісь.set_title("Перцептрон розділив дошку без жодної помилки")
вісь.legend()
plt.tight_layout(); plt.show()
print("точок на графіку:", len(X))
No description has been provided for this image
точок на графіку: 569

7 · Звіряємо з бібліотечним PerceptronУсередині sklearn.linear_model.Perceptron — те саме правило. Порядок прикладіві початкові ваги там інші, тому пряма вийде не та сама; але на роздільних данихобидві мають дати нуль помилок.¶

In [12]:
бібліотечний = Perceptron(eta0=0.08, max_iter=1000, tol=None, shuffle=False, random_state=42)
бібліотечний.fit(X, y)

прогнози_бібліотеки = бібліотечний.predict(X)
print("ваги sklearn:", бібліотечний.coef_[0].round(4), "· зсув:", бібліотечний.intercept_.round(4))
print("помилок sklearn:", int((прогнози_бібліотеки != y).sum()))
print("помилок наших:  ", int((наші_прогнози != y).sum()))

assert np.allclose(прогнози_бібліотеки, наші_прогнози), "прогнози розійшлися!"
print("✅ збігається: обидві реалізації розділили всі 569 оголошень без помилок")
ваги sklearn: [-0.4576  0.2536] · зсув: [0.]
помилок sklearn: 0
помилок наших:   0
✅ збігається: обидві реалізації розділили всі 569 оголошень без помилок

Прямі різні, а прогнози однакові — бо роздільних прямих нескінченно багато, іправило зупиняється на першій-ліпшій. Якої з них шукати «найкращу», перцептронне питає взагалі: цим займається метод опорних векторів.## 8 · Нероздільні дані: правило не сходитьсяТепер повернемо на дошку приманки із завищеною ціною. Клас шахрайстваперетвориться на дві групи по різні боки від чесних оголошень, і жодна прямаїх не відділить.

In [13]:
X_усі = np.c_[оголошення["ціна"] / 1000, оголошення["довідкова"] / 1000]
y_усі = np.where(оголошення["шахрайське"] == 1, 1, -1)

print("оголошень:", len(X_усі), "· приманок:", int((y_усі == 1).sum()))
частка = X_усі[:, 0] / X_усі[:, 1]
print("дешевших за довідник приманок:", int(((y_усі == 1) & (частка < 1)).sum()))
print("дорожчих за довідник приманок:", int(((y_усі == 1) & (частка >= 1)).sum()))
print("→ шахрайство лежить по ОБИДВА боки від чесних оголошень")
оголошень: 1100 · приманок: 139
дешевших за довідник приманок: 104
дорожчих за довідник приманок: 35
→ шахрайство лежить по ОБИДВА боки від чесних оголошень
In [14]:
_, _, історія_нероздільна = навчити_перцептрон(X_усі, y_усі, крок=0.08,
                                               ваги=[0.80, 0.20], зсув=-6.0, епох=60)

print("виправлень за епоху (перші 20):", історія_нероздільна[:20])
print("мінімум за 60 епох:", min(історія_нероздільна))
print("останні пʼять епох:", історія_нероздільна[-5:])
print()
print("нуля немає й не буде: правило виправляється вічно")
виправлень за епоху (перші 20): [192, 194, 212, 203, 191, 202, 212, 201, 199, 202, 199, 212, 189, 191, 194, 202, 192, 202, 188, 203]
мінімум за 60 епох: 186
останні пʼять епох: [201, 212, 220, 204, 194]

нуля немає й не буде: правило виправляється вічно
In [15]:
fig, вісь = plt.subplots(figsize=(8, 4))
вісь.plot(range(1, len(історія) + 1), історія, "o-", c="#0f766e",
          label="роздільні дані (569 оголошень)")
вісь.plot(range(1, len(історія_нероздільна) + 1), історія_нероздільна, "o-",
          c="#c2185b", ms=3, label="нероздільні дані (усі оголошення)")
вісь.axhline(0, c="#999", lw=1, ls="--")
вісь.set_xlabel("епоха"); вісь.set_ylabel("виправлень за епоху")
вісь.set_title("Збіжність є лише тоді, коли дані лінійно роздільні")
вісь.legend()
plt.tight_layout(); plt.show()
print("бірюзова крива дійшла до нуля за", len(історія), "епох; рожева не дійде ніколи")
No description has been provided for this image
бірюзова крива дійшла до нуля за 11 епох; рожева не дійде ніколи

9 · Логістична регресія на тих самих данихТой самий нейрон, лише сходинку замінено на сигмоїду. Зʼявляється функція втрат,градієнт — і головне, число на виході замість жорсткого «так/ні». Спочаткуна роздільних даних, де перцептрон упорався.¶

In [16]:
логістична = LogisticRegression(max_iter=2000).fit(X, y)
прогнози_логістичної = логістична.predict(X)
print("точність логістичної на роздільних 569:",
      f"{(прогнози_логістичної == y).mean() * 100:.1f} %")

# на відміну від сходинки, сигмоїда дає число, яке можна порівняти з будь-яким порогом
ймовірності = логістична.predict_proba(X)[:, list(логістична.classes_).index(1)]
найпідозріліші = np.argsort(-ймовірності)[:5]
print()
print("пʼять найпідозріліших оголошень за версією логістичної регресії:")
print(роздільна_дошка.loc[найпідозріліші, ["модель", "рік", "ціна", "довідкова", "шахрайське"]]
      .assign(ймовірність=ймовірності[найпідозріліші].round(4)))
точність логістичної на роздільних 569: 100.0 %

пʼять найпідозріліших оголошень за версією логістичної регресії:
            модель   рік    ціна  довідкова  шахрайське  ймовірність
411  Gamma X Ultra  2022  4530.0    17100.0           1          1.0
17         Gamma X  2024  8320.0    23760.0           1          1.0
336  Gamma X Ultra  2021  3410.0    14060.0           1          1.0
563        Gamma X  2024  7380.0    21120.0           1          1.0
81         Gamma X  2024  4960.0    15300.0           1          1.0

Ось чого перцептрон не вміє: відсортувати оголошення за підозрілістю й віддатимодераторові верхівку списку. Сходинка видає нуль або одиницю, і всі приманкидля неї однакові.А тепер найважливіше — та сама логістична регресія на нероздільних даних,на яких правило Розенблата крутилось вічно.

In [17]:
логістична_усі = LogisticRegression(max_iter=2000).fit(X_усі, y_усі)
прогнози_усі = логістична_усі.predict(X_усі)

print("точність логістичної:", f"{(прогнози_усі == y_усі).mean() * 100:.1f} %")
print("точність «усе чесне»:", f"{(y_усі == -1).mean() * 100:.1f} %")
print("ітерацій до зупинки:", int(логістична_усі.n_iter_[0]))
print()
print("двох ознак тут замало: приманки лежать по обидва боки, і пряма нічого не")
print("виграє. Але модель ЗУПИНИЛАСЬ і видала конкретну відповідь.")
print("перцептрон на цих же даних за 60 епох так і не спинився — виправлень:",
      sum(історія_нероздільна))
точність логістичної: 87.4 %
точність «усе чесне»: 87.4 %
ітерацій до зупинки: 17

двох ознак тут замало: приманки лежать по обидва боки, і пряма нічого не
виграє. Але модель ЗУПИНИЛАСЬ і видала конкретну відповідь.
перцептрон на цих же даних за 60 епох так і не спинився — виправлень: 11951

Це і є практична різниця. Обидві моделі — один нейрон із двома вагами й зсувом.Перцептрон на нероздільних даних не має критерію зупинки взагалі; логістичнарегресія мінімізує втрату, тому зупиняється завжди — навіть коли розвʼязокпоганий, вона чесно каже, який саме він поганий.## 10 · XOR: один нейрон не може, три можутьЧотири точки, які не розділити прямою. Спочатку переконаємось, що бібліотечнийперцептрон із ними не впорається.

In [18]:
XOR_входи = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
XOR_відповіді = np.array([0, 1, 1, 0])

один_нейрон = Perceptron(max_iter=5000, tol=None, random_state=0).fit(XOR_входи, XOR_відповіді)
print("що видав один нейрон:", один_нейрон.predict(XOR_входи))
print("потрібно було:       ", XOR_відповіді)
print("правильних:", int((один_нейрон.predict(XOR_входи) == XOR_відповіді).sum()), "з 4")
print()
print("стеля для XOR — 3 з 4, і її дає навіть модель «завжди відповідай 1».")
print("Правило зупинки в перцептрона одне: епоха без виправлень. Тут вона")
print("не настає, тож він віддає ті ваги, на яких його зупинили за max_iter.")
що видав один нейрон: [0 0 0 0]
потрібно було:        [0 1 1 0]
правильних: 2 з 4

стеля для XOR — 3 з 4, і її дає навіть модель «завжди відповідай 1».
Правило зупинки в перцептрона одне: епоха без виправлень. Тут вона
не настає, тож він віддає ті ваги, на яких його зупинили за max_iter.

Тепер збираємо конструкцію з лекції: два нейрони в прихованому шарі (АБО та НЕ-І)і третій над ними (І). Ваги виписуємо руками — навчати нема чим, бо правилоРозенблата на прихований шар не поширюється.

In [19]:
def сходинка(z):
    # активація перцептрона: 1, якщо аргумент невідʼємний, інакше 0
    return (z >= 0).astype(int)


# прихований шар: рядок на нейрон, стовпець на вхід
ваги_прихованого = np.array([[1.0, 1.0],     # h1 = АБО
                             [-1.0, -1.0]])  # h2 = НЕ-І
зсуви_прихованого = np.array([-0.5, 1.5])

ваги_виходу = np.array([1.0, 1.0])           # y = І над h1 і h2
зсув_виходу = -1.5

прихований = сходинка(XOR_входи @ ваги_прихованого.T + зсуви_прихованого)
вихід = сходинка(прихований @ ваги_виходу + зсув_виходу)

таблиця = pd.DataFrame(XOR_входи, columns=["x1", "x2"])
таблиця["h1 (АБО)"] = прихований[:, 0]
таблиця["h2 (НЕ-І)"] = прихований[:, 1]
таблиця["y"] = вихід
таблиця["XOR"] = XOR_відповіді
print(таблиця.to_string(index=False))
 x1  x2  h1 (АБО)  h2 (НЕ-І)  y  XOR
  0   0         0          1  0    0
  0   1         1          1  1    1
  1   0         1          1  1    1
  1   1         1          0  0    0
In [20]:
assert np.array_equal(вихід, XOR_відповіді), "конструкція не відтворила XOR!"
print("✅ три нейрони дали XOR на всіх чотирьох входах")
print()
print("один нейрон:", int((один_нейрон.predict(XOR_входи) == XOR_відповіді).sum()), "з 4")
print("три нейрони:", int((вихід == XOR_відповіді).sum()), "з 4")
✅ три нейрони дали XOR на всіх чотирьох входах

один нейрон: 2 з 4
три нейрони: 4 з 4

Це і є межа теми. Ми не змінили жодного нейрона — усі три однакові зважені сумиз порогом. Змінився лише спосіб їх зʼєднати, і задача, нерозвʼязна для одного,стала тривіальною для трьох.Але ваги ми виписали руками, знаючи відповідь. Як підбирати їх автоматично —у темі 31.---## Завдання### 🟢 Рівень 1 — БазаВізьми ті самі 569 оголошень і запусти навчити_перцептрон із трьох різнихпочаткових наборів ваг: [0, 0], [1, 1] і [-1, 1] (зсув усюди 0).Виведи для кожного списки виправлень за епоху й кінцеві ваги.Зроблено, якщо для всіх трьох стартів остання епоха має нуль виправлень,а кінцеві ваги — різні.### 🟡 Рівень 2 — ПлюсПорівняй швидкість навчання. Прожени навчити_перцептрон із кроком0.001, 0.08 і 10 на тих самих даних і однаковому старті [0.80, 0.20],зсув=-6.0. Побудуй графік «кількість епох від кроку».Зроблено, якщо ти поясниш словами, чому крок майже не впливає на кількістьепох, коли початкові ваги нульові, і сильно впливає, коли вони не нульові.### 🔴 Рівень 3 — ВикликДодай до перцептрона усереднення ваг: після кожного прикладу накопичуйпоточні ваги в суму, а наприкінці подiли на кількість переглянутих прикладів.Це averaged perceptron. Прожени звичайний і усереднений варіанти нанероздільних даних X_усі, y_усі протягом 60 епох.Зроблено, якщо ти покажеш числом, що усереднений варіант дає стабільнішуточність: порахуй точність після кожної з 60 епох для обох варіантів і порівняйїхнє стандартне відхилення.