CodeLab
145 subscribers
538 photos
20 videos
159 links
Говорим просто о сложном

Обсуждение, новости, материал и сплетни — все здесь https://t.me/CodeLabMLChat
Download Telegram
👨‍💻 Завтра у вашего любимого админа будет день рождения, поэтому самым лучшим подарком будут новые подписичники, которых вы сможете привести (ну или мак на чипе M4, выбирать вам)
Please open Telegram to view this post
VIEW IN TELEGRAM
1322
👨‍💻 Сегодня постараюсь замутить пост, будем разбирать новое задание
Please open Telegram to view this post
VIEW IN TELEGRAM
3221
👨‍💻 Думали поста не будет? Я тоже так думал, но планы меняются, так что хотя бы начнем:

0⃣‼️ У нас двумерное пространство признаков x₁, x₂, воот, также тут изображены объекты двух разных классов:

🔵квадратами - объекты класса C₁ = +1

🔵ромбами - объекты класса C₂ = -1

🟢 Красная разделяющая линия проходит через синие точки. Размер клеток по каждой из координат равен 1 (график в комментах)

🟢 А найти нам нужно все также вектор параметров:

🟰w = [w₀, w₁, w₂]🟰

🟰w₁ * x₁ + w₂ * x₂ + w₀ = 0 🟰 — а вот наше уравнение

🟢 Также, мы имеем объекты для тестовой выборки:

x_test = [(5, -3), (-3, 8), (3, 6), (0, 0), (5, 3), (-3, -1), (-3, 3)]


🟢 Еще нам нужно вычислить метку класса для каждого объекта по вот этой вот формуле:

🟰 a(x) = sign(wᵀ * x) 🟰

... где x = [1, x₁, x₂]
— вектор признаков (координат) объекта выборки, дополненный первой единицей для параметра w₀.

1⃣ Итак, начнем. Находим точки:

🔵p₁ = (−5,6)

🔵p₂ = (8,−3)

2⃣ Дальше находим k (наш наклон). Я как-то расписывал уж эту формулу — вот здесь

🟰 k = (y₂ - y₁) / (x₂ - x₁) 🟰➡️За вас посчитал, получаем -9/13

Скоро продолжим 💤
Please open Telegram to view this post
VIEW IN TELEGRAM
4322
Soon 👨‍💻
Please open Telegram to view this post
VIEW IN TELEGRAM
2
👨‍💻Всем привет, продолжаем прошлый пост:

3⃣ Дальше нужно найти b (у нас уже найден k):

🟰 y = kx + b 🟰

🟰 b = y - kx 🟰

🟰 b = 6 - (-9/13 * (-5) = 6 - 45/13 = (78-45)/13 = 33/13 🟰 — берем любую точку и подставляем координаты, мы взяли точку (-5, 6)

🟰 y = 33/13 - 9/13*x 🟰 — получаем уравнение и умножаем | * 13

🟰 w = [-33, 9, 13] 🟰— отсюда получаем коэффициенты

4⃣ Вектор весов:

w = np.array([-33, 9, 13]) # [свободный член, вес при x1, вес при x2]


5⃣ Далее функция. тут надо внимательно: она считает точку с помощью формулы и относит к классу -1 ромбов (если результат < 0) или к квадратам +1 (если результат >= 0)

a_sign = lambda x, w: -1 if np.dot(x, w) < 0 else 1


6⃣ Добавим единицу к каждой точке из 🟰x_test🟰

x_test_new = np.array([[1, x1, x2] for x1, x2 in x_test])


🔵Поскольку у нас есть w₀, нам нужна была единица, например:

🔵Было (5, -3) ➡️ стало (1, 5, -3)

7⃣ Теперь для каждый точки (у них уже стоит единица, если что) применяем функцию 🟰a_sign🟰 и по итогу получаем список предсказаний (у нас может быть либо -1, либо 1), то есть, на выходе получаем список предсказаний 🟰 [-1, 1, 1, -1, 1, -1, -1]🟰

predict = [a_sign(x, w) for x in x_test_new]


💤 Подумаю, как можно усовершенствовать код с помощью библиотек, всем пака
Please open Telegram to view this post
VIEW IN TELEGRAM
321
🪷🔤🔤🔤🔤🌷🔤🔤 🌹🔤🔤🌹🔤🔤🔤🔤🔤🔤

👨‍💻 Итак, немного отойдем от Бинарной Классификации и вычислений векторов, тут, тут и даже тут мы уже рассматривали что-то подобное.

0⃣⚙️ Начнем с того, что логистическая регрессия — это не регрессия вовсе. Да, звучит сомнительно, но это алгоритм классификации. То есть, моделька логистической регрессии будет предсказывать вероятность принадлежности объекта к одному из двух классов (мы подобной темкой занимались в последних постах).

1⃣🛍 Если вы хорошо вспомните, мы как то рассматривали базовую нейронку, которая угадывала пол по входным признакам, Алгоритм отчасти похож:

🧑‍💻‼️ Для понимания, представьте обычную линейную комбинацию признаков:

🔵z = w₁ * x₁ + w₂ * x₂ + b

🧑‍💻 Далее, мы прогоняем нашу линейную зависимость через сигмоиду:

🔵σ(z) = 1 / 1 + e⁻ ᶻ

🧑‍💻 Считаем функцию потерь (НО НЕ MSE, мы бы использовали логарифмическую функцию потерь, так как MSE, MAE это для регрессий).

🧑‍💻 Далее обновляем веса с помощью любимого градиентного спуска и минимизируем Loss

Почему именно сигмоида? Как минимум, она переводит число в диапазон (0, 1)


2⃣ Начнем с кода, завтра продолжим:

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix
Please open Telegram to view this post
VIEW IN TELEGRAM
521
542
👨‍💻Йоу воссап друзья, продолжаем писать пост

📰 В прошлом посте (он тут кстати), мы импортировали вот эту библиотеку:

from sklearn.datasets import load_breast_cancer


3⃣⚙️ Так вот, загружаем наш датасет по диагностике рака груди:

data = load_breast_cancer() 
# Функцмя возвращает Bunch объект (типа словаря). Этот объект содержит data.data — признаки (массив NumPy), data.target — метки (0 или 1), data.feature_names — названия признаков (столбцов)


4⃣🛍 Создадим таблицу X из фичей (как раз тут используем pandas)

X = pd.DataFrame(data.data, columns = data.feature_names)
y = pd.Series(data.target)


🧑‍💻 Мы преобразуем массив data.data в объект DataFrame.

🧑‍💻 В columns мы задаем названия столбцов соответствующе признакам

🧑‍💻 Что касаемо второй строки, мы преобразуем data.target в объект Series, который представляет собой одномерный массив меток классов

📊 На данном этапе мы имеем X и y:

🔵 X — это таблица признаков (вида n × m, здесь n — количество образцов, m — количество признаков)

🔵y — вектор меток классов (n)

5⃣📊 Разделяем данные на обучающую и тестовую выборки:

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state = 1)


🧑‍💻 Функция train_test_split случайным образом разделяет данные на две части (тестовая 20% и обучающая 80%). ⚙️ Кароч, если возьмем X, то эта таблица делится на 2 части (X_train, X_test).

6⃣📊 Мы в прошлом посте импортировали StandardScaler. Мало ли данные разнятся, вот и применим масштабирование, а именно Z-core. У нас был огромный разбор аж двух видов нормализации, все есть на канале!

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)


🧑‍💻 Первая строка это просто объект стандартизации

🧑‍💻 fit_transform(X_train) — Вычисляем среднее (μ) и стандартное отклонение (σ) для каждого признака на обучающих данных. В общем, мы трансформируем и обучаем scaler и применяем его к X_train данные

🧑‍💻 transform(X_test) — А здесь мы просто применяем трансформацию к scaler и применяем к X_test данным. Почему не fit? Потому что наши тестовые данные должны быть независимыми от тренировочных данных. Тем более, scaler уже обучен (на обучающих данных), и он применяет ту же формулу Z-score

Картиночку скоро повторим 💤
Please open Telegram to view this post
VIEW IN TELEGRAM
5431
822
👨‍💻 Всем ку! Продолжаем разбирать 📈 логистическую регрессию. Намутил для вас классный дизайн для поста, а вот кстати прошлый пост

7⃣📊 Мы остановились на стандартизации (создали объект 🟰Z-score🟰 и обучили + применили стандартизацию).

⚙️ Теперь создадим модель логистической регрессии:

model = LogisticRegression(max_iter = 1000)


🛍 По умолчанию стоит L2 регуляризация, а max_iter отвечает за число итераций для градиентного спуска. (Про регуляризацию мы поговорим попозже, тоже очень важная тема).

👨‍💻 Вам нужно понимать одно: регуляризация штука интересная, нужна для 'защиты от переобучения модельки', и ,грубо говоря, она 'штрафует большие веса модели' — это все про L2, еще есть L1 (но обо всем потом)


📊8⃣ Обучаем модель:

model.fit(X_train_scaled, y_train)


📉Кароч ну тут мы по сути ищем такие веса (w), чтобы минимизировать 🟰log loss🟰 (это метрика для логистической регрессии) предсказаниями и 🟰y_train🟰

🧑‍💻 Чтобы вам понять, что делает model.fit():

🔵Получает X_train_scaled и y_train (наши обучающие данные и соответствующие метки, предсказания)

🔵Начинает с рандомных весов w

🔵Делает предсказания ŷ (вероятности через функцию сигмоиды)

🔵Сравнивает их с 🟰y_train🟰 с помощью 🟰log loss🟰

🔵Считает ошибку (loss)

🔵Обновляет веса, чтобы ошибка стала меньше

🔵Повторяет это до сходимости (или до max_iter)

➡️0⃣ Получаем предсказания y_pred:

y_pred = model.predict(X_test_scaled) #выдаст 0 или 1


1⃣0⃣ Выводим метрики качества:

print("Classification Report:\n", classification_report(y_test, y_pred))


🧑‍💻 classification_report — это функция для оценки эффективности алгоритмов классификации (в нем сразу содержится несколько + метрик качества) — тут тоже нужен отдельный пост, на выводе покажу что получится.

print("Confusion Matrix:\n", confusion_matrix(y_test, y_pred))


🧑‍💻 Выдает матрицу ошибок (показывает, где модель ошиблась, а где угадала )

🧑‍💻 Вот такой получится вывод для двух функций сверху

precision recall f1-score support

0 0.98 0.95 0.96 43
1 0.97 0.99 0.98 71

accuracy 0.97 114
macro avg 0.97 0.97 0.97 114
weighted avg 0.97 0.97 0.97 114


⚙️ Остается визуал:

sns.heatmap(confusion_matrix(y_test, y_pred), annot=True, fmt='d', cmap='Blues')
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.title("Confusion Matrix")
plt.show()


🧑‍💻 sns.heatmap(confusion_matrix...)как раз отвечает за цветную таблицу в выводе (в прошлом посте я ее кидал, в этом будет в комментах). В функцию мы передали матрицу ошибок.

🧑‍💻 annot = True аргумент вписывает числа внутрь ячеек.

Что мы видим на графике

🔵 41 — модель правильно предсказала класс 0 (у пациента рак)

🔵 70 — правильно предсказала класс 1 (у пациента рака нет)

🔵 1, 2 это ошибки модели в каждой из 2 ситуаций
Please open Telegram to view this post
VIEW IN TELEGRAM
5221
🧑‍💻 Сегодня крутецкий пост
Please open Telegram to view this post
VIEW IN TELEGRAM
722
👨‍💻 Йоу, воссап друзья, предлагаю продолжить бинарную классификацию, тем более, вы ничего не можете предложить.

import numpy as np

x_test = [(5, -3), (-3, 8), (3, 6), (0,0), (5, 3), (-3, -1), (-3, 3)]

w = np.array([-33, 9, 13])
a_sign = lambda x, w: -1 if np.dot(x, w) < 0 else 1
x_test_new = np.array([[1, x1, x2] for x1, x2 in x_test])
predict = [a_sign(x, w) for x in x_test_new]


⚙️ Мы разобрали код выше ТУТ и ТУТ, теперь можно попробовать его улучшить.

📇 Напоминаю, нам нужно было найти вектор параметров w 🟰w = [w₀, w₁, w₂]🟰 для разделяющей линии в соответствии с выражением: 🟰w * x₁ + w₂ * x₂ + w₀ = 0 🟰

📊 В прошлом посте ТУТ мы нашли этот самый вектор параметров 🟰 w = [-33, 9, 13] 🟰и получили такое уравнение прямой: 🟰9 * x₁ +13 * x₂ − 33 = 0🟰

📊 Но, но, помимо вектора параметров нам также нужно было сформировать список 🟰predict из значений меток {-1, +1}🟰 (поскольку у нас как раз 2 класса — C₁ = +1, C₂ = -1). То есть, если

wᵀ * x ≥ 0
📐 +1

wᵀ * x < 0
📐 -1

🧠 Эти самые метки мы искали по формуле 🟰 a(x) = sign(wᵀ * x) 🟰, здесь x = [1, x₁, x₂] — вектор признаков (координат) объекта выборки, дополненный первой единицей для параметра w₀.

📈 Короче, чтобы вам было проще, давайте на примере:

Мы уже имеем метки по ТЗ:

x_test = [(5, -3), (-3, 8), (3, 6), (0, 0), (5, 3), (-3, -1), (-3, 3)]


🟰 x = [1, 5, −3] 🟰— добавляем 1, чтобы учесть смещение w (со всеми примерами также)

🟰 wᵀ = [-33, 9, 13] 🟰

🟰 wᵀ * x 🟰

🟰 wᵀ * x = −33 * 1 + 9 * 5 + 13 * (−3) = −33 + 45 − 39 = −27 🟰Получаем wᵀ * x < 0, следовательно метка -1

🔜 Можете сами посчитать остальные метки, получаем такую штуку:

predict=[−1, +1, +1, −1, +1, −1, −1]


🧑‍💻 Позже продолжим
Please open Telegram to view this post
VIEW IN TELEGRAM
15542
3221
Друзья, у админа сессия, так что пока держите мем 👨‍💻
Please open Telegram to view this post
VIEW IN TELEGRAM
6321
CodeLab
Друзья, у админа сессия, так что пока держите мем 👨‍💻
Media is too big
VIEW IN TELEGRAM
👨‍💻Всем ку, решаем задачку с margin (это у нас так отступ называется). Кстати, уже задачки с отступами были, можете поискать через поиск канала, не менее интересные 👍

Фотка задания в комментах ⬇️

0⃣📱 Итак, условие не отличается от прошлых заданий: у нас есть разделяющая линия, она определяется параметрами

🟰w = [w₀, w₁, w₂]🟰

⚙️ Значения параметров также известны:

🟰w = [15/7, -9/7, -1]🟰

1⃣ Параметры задают положение линии в соответствие с выражением:

🟰w * x₁ + w₂ * x₂ + w₀ = 0 🟰

🔤🔤🌹🔤🔤🔤

2⃣ Так, теперь про отступ, что это и зачем оно нужно

Отступ
(или margin или γ или M) — все это, так или иначе, относится к отступу.

🔵Показывает margin насколько уверенно модель классифицировала точку а также, с какой стороны от разделяющей прямой она находится.


Формулы:

🟰Q(a, X) = Σ[a(x) ≠ y]🟰 — Число неверных классификаций

🟰[a(x) ≠ y]🟰 — нотация Айверсона, важная формула, вощвращает 1 или 0, щас поясню:

🟰y🟰 в нашей скобе это целевое значение, исходя из прошлых задач на бинарную классицикацию, y может быть -1 и +1 (номер класса)

‼️ Так вот, если модель a(x) в скобках [a(x) ≠ y] относит объект не к тому классу, [a(x) ≠ y] — выдает True. В соответствие с нотацией Айверсона 🟰[a(x) ≠ y]🟰 1 (True), 0 (False)

‼️🟰Q(a, X)🟰 — получается, этот функционал подсчитывает число неверных классификаций (сколько раз модель ошиблась)

🟰y ∈{-1, +1} 🟰— как раз, наши целевые

🟰Q(a, X) = Σ[ y * a(x) < 0]🟰 — немного переписываем число неверных классификаций теперь добавили y - целевые {-1, +1} и умножили их на ответ модели a(x) — это тоже, либо -1, либо +1. Если это произведение будет отрицательным — значит, мы неверно классифицируем X. Если y и a(X) будут совпадать (оба по -1; +1)получаем положительные значения.

‼️ Добрались до сути — 🟰M = a(X) * y — отступ (margin)🟰

🟰a(X) = {w, x}🟰 — если мы оставим только скалярное произведение, то 🟰a(X) = {w, x}🟰 будет давать нам не только {+1; -1}, но и величину, на которую отстоит наш образ X от разделяющей прямой (если отступ положительный, образ классифицируется верно)

🟰a(X) = sign(wᵀ * x)🟰— классификация в линейных моделях. В линейных моделях классификатор просто смотрит, с какой стороны от границы находится точка. Если результат положительный — класс +1, если отрицательный — -1
Please open Telegram to view this post
VIEW IN TELEGRAM
3111
3⃣📊 Хотел еще добавить формулу:

🟰γᵢ = yᵢ ⋅ Mᵢ = yᵢ ⋅ (wᵀ * xᵢ)🟰 — это у нас формула для отступа (margin)

🧑‍💻 Mᵢ = wᵀ * xᵢ — скалярное произведение весов и признаков

🧑‍💻 yᵢ ∈ {−1, +1} — целевые метки

🧑‍💻 Умножаем y * M и получаем отступ (насколько правильно мы классифицировали, если y > 0, модель предсказала правильно образ X

👨‍💻 Теперь, что касаемо кода:

4⃣ Импортируем numpy и прописываем веса модели:

import numpy as np

w = np.array([15/7, -9/7, -1]) # [w0, w1, w2]


5⃣ Теперь определяем корды (надо ток добавить единицу для w₀)

x_test = np.array([
(1, -8, -4),
(1, -2, 2),
(1, 4, 8),
(1, 6, 3)
])


6⃣ Теперь нам нужно как-нибудь определить метки (или знаки +1, -1), для этого воспользуемся функцией 🟰np.sign()🟰

y_test = np.sign(x_test @ w.T)


🧑‍💻 Умножаем матрицу из 4 векторов-образов (X) на вектор w, получаем вектор из 4 скалярных произведений:

🔵 x₁ * w ... x₄ * w

🔵w — вектор весов: w = [w₀, w₁, w₂]

🔵xᵢ — вектор признаков: xᵢ = [1, x₁, x₂]

🔵w * xᵢ — это то, насколько объект далеко от прямой


🟰M = a(X) * y🟰

🟰M = {w, x} * y🟰

7⃣ Самое главное, margin:

margin = x_test @ w.T * y_test #Все по формуле


import numpy as np

w = np.array([15/7, -9/7, -1])

x_test = np.array([
(1, -8, -4),
(1, -2, 2),
(1, 4, 8),
(1, 6, 3),
])

y_test = np.sign(x_test @ w.T)

margin = x_test @ w.T * y_test
Please open Telegram to view this post
VIEW IN TELEGRAM
422
🤔 Как вам последние посты? Все ли понятно?
Please open Telegram to view this post
VIEW IN TELEGRAM
522
👨‍💻 Друзья, новое задание! Оно не сильно отличается от прошлого, но разобрать стоит:

📈Снова имеем разделяющую прямую, а также два признака — x₁, x₂, определяется параметрами🟰w = [w₀, w₁, w₂]🟰

⚙️ Только в отличие от прошлого задания, эти параметры нам неизвестны.

0⃣‼️ А вычислить нам нужно значения отступов (напоминаю, margin) для образов 1-6 и сохранить в список 🟰margin = []🟰

1⃣ Начнем с параметров, пример вычисления смотримтут

Скажу сразу, что получилось у меня:

🔵Точки — (-4, 0) ; (5, 4)

🔵k (угловой коэффициент) = y₂ - y₁/ x₂ - x₁ (у меня получилось 4/9)

🔵b (коэффициент смещения, просто подставляем одну из точек и угловой коэффициент) (16/9)

🔵y = 16/9 + 4/9 * x — получили уравнение

🔵Переносим y и вычисляем коэффициенты

🔵-16 -4 * x + 9 = 0 ➡️получили [-16, -4, 9]

2⃣ Теперь прописываем это в коде + пишем корды точек (добавляем единицу для w₀)

import numpy as np
w = np.array([-16, -4, 9])

x_test = np.array([
[1, -5, 2],
[1, -4, 6],
[1, 3, 2],
[1, 3, -3],
[1, 5, 6],
[1, 9, 2]
])


3⃣ Метки классов определяем тоже сами:

y_test = np.array([1, 1, 1, -1, -1])


Вообще, тут у меня возник вопрос, вроде визуально понятно, что точки 1, 2 и 5 выше разделющей прямой, но они идут не по порядку. Если вычислять через np.sign, то получаем

y_test = np.sign(x_test @ w.T)

#y_test = np.array([1, 1, -1, -1, 1]) — А такой ответ не подойдет


4⃣ Вычисляем отступ:

margin = x_test @ w * y_test #Скалярное произведение
print(margin)


‼️ Чем больше отступ, тем увереннее классификация.

🔵В нашем случае, в точках 3 и 5 наша модель опростоволосилась


‼️ Сама формула 🟰margin — Mᵢ = wᵀ * xᵢ🟰 — это просто скалярное произведение между весами (это наши параметры в начале кода) и x-объектами (это и есть наши 6 точек).

🔵 y_test — целевые значения. Говорит нам о том, каким должен быть знак margin, если классификация верная.


‼️ Вот как работает главная формула нашего кода:

w = np.array([-16, -4, 9])
x_test[0] = [1, -5, 2]

M₁ = (-16) * 1 + (-4) * (-5) + 9 * 2 = -16 + 20 + 18 = 22
y₁ = 1

margin₁ = 22 * 1 = 22



5⃣🧑‍💻 Наш код:

import numpy as np

w = np.array([-16, -4, 9])

x_test = np.array([
[1, -5, 2],
[1, -4, 6],
[1, 3, 2],
[1, 3, -3],
[1, 5, 6],
[1, 9, 2]
])


y_test = np.array([1, 1, 1, -1, -1, -1])
# y_test = np.sign(x_test @ w.T)

# print(y_test)

margin = x_test @ w * y_test


🔗 Ссылка на курс 🔗
Please open Telegram to view this post
VIEW IN TELEGRAM
3321
Сегодня крутецкий пост 🧑‍💻
Please open Telegram to view this post
VIEW IN TELEGRAM
4221
👨‍💻 Итак, новое задание:

0⃣ Че у нас есть? Мы имеем тестовую выборку в виде набора образов с двумя признаками (x₁ и x₂) — первый элемент это значение x₁, второй — x₂. ‼️ Также мы имеем метки двух классов — (+1, -1)

🔵Вектор параметров у нас тоже имеется — [-8/3, -2/3, 1]

‼️ А вычислить нам нужно Q (это такой показатель качества, который вычисляется как сумма нотации Айверсона (M) для всех точек тестовой выборки). ⚙️ Проще говоря, найденный Q даст нам понять, сколько точек в тестовой выборке имеют отступ.

🟰 Qᵢ = Σ[Mᵢ < 0] 🟰, где 🟰 Mᵢ = y * {w, xᵢ} 🟰

🟰 [Mᵢ < 0] 🟰функция нотации Айверсона (возвращает 1, если Mᵢ < 0, и 0б если Mᵢ ≥ 0).

import numpy as np

x_test = np.array([(-5, 2), (-4, 6), (3, 2), (3, -3), (5, 5), (5, 2), (-1, 3)])
y_test = np.array([1, 1, 1, -1, -1, -1, -1])
w = np.array([-8/3, -2/3, 1])


1⃣⚙️ Для начала, добавляем столбец из единиц:

X = np.column_stack((np.ones(len(x_test)), x_test))


📊 Теперь наш вывод X будет выглядеть как-то так:

[[ 1. -5.  2.] 
[ 1. -4. 6.]
[ 1. 3. 2.]
...


2⃣🧠 Вычисляем отступы по привычной формуле:

🟰 Mᵢ = wᵀ * xᵢ 🟰 скалярное произведение ⟨w,xᵢ)

margin = y_test * (X @ w)


3⃣‼️ Самое главное — считаем количество M [Mᵢ < 0]

Q = np.sum(margin < 0)
print(int(Q))


Q = 2 ➡️ следовательно модель допустила 2 ошибки в выборке из 7 точек (покатит)
Please open Telegram to view this post
VIEW IN TELEGRAM
5332