Як створити власну ML-модель на Python: на прикладі класифікації URL-адрес

💡 Усі статті, обговорення, новини про AI — в одному місці. Приєднуйтесь до AI спільноти!

Привіт! Мене звати Олександр, я Python Developer в компанії Apriorit і вже кілька років працюю з проєктами у сфері машинного навчання та штучного інтелекту. Протягом цього часу мені доводилося працювати з різними типами моделей — від простих класифікаторів до складних систем із глибинним навчанням.

Машинне навчання (ML) давно вийшло за межі лабораторій — сьогодні воно допомагає у кібербезпеці, медицині, фінансах, аналітиці та навіть у звичайних застосунках, які ми використовуємо щодня. Але коли мова заходить про створення власної ML-моделі, багато хто вважає це складним або недосяжним. Насправді — ні.

З досвіду можу сказати — ефективна модель не обов’язково має бути складною. Часто найкращі результати дають прості, добре структуровані рішення, побудовані на зрозумілих даних і логічних ознаках.

Саме тому я вирішив написати цю статтю — щоб показати, як з нуля можна побудувати власну AI-модель, не маючи великого досвіду у сфері машинного навчання.

Ми побудуємо просту, але повністю робочу AI-модель, яка визначає, чи є URL-адреса шкідливою (malicious) чи безпечною (benign).

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

  • зібрати дані;
  • перетворити URL-адреси у числові ознаки (features);
  • навчити кілька моделей;
  • оцінити якість і побачити, як це реально працює.

На виході отримаємо робочу модель, яку можна протестувати на власних посиланнях — наприклад, щоб перевірити, чи є підозрілим URL у поштовому листі або на сайті.

Після прочитання ви зможете:

  • зрозуміти базову логіку машинного навчання без складної теорії;
  • навчити модель на реальному датасеті;
  • оцінити точність класифікації.

А головне — побачити, що навіть з базовими знаннями Python можна побудувати реальний AI-інструмент, який зможе виконувати ваші практичні завдання.

Коротко про процес

ML-проєкт — це не магія. Усе тримається на послідовних логічних кроках:

  • Дані — ми беремо набір URL-адрес із мітками «benign/malicious».
  • Ознаки — перетворюємо кожен URL у набір чисел: довжини, кількість піддоменів, спеціальних символів, наявність IP тощо.
  • Модель — навчаємо алгоритм (наприклад, Random Forest або Logistic Regression) розпізнавати закономірності.
  • Оцінка — перевіряємо, як модель працює на нових, не бачених раніше прикладах.

Основи машинного навчання

Машинне навчання — це підрозділ штучного інтелекту, зосереджений на алгоритмах, які здатні «навчатися» закономірностям у даних і згодом робити передбачення або рішення для нових прикладів без явних, жорстко прописаних правил. Замість того щоб вручну кодувати всі інструкції, ми надаємо дані (досвід), а модель узагальнює виявлені шаблони на ще не бачені випадки.

Замість «якщо домен містить paypal — це фішинг» ми показуємо тисячі URL із підписами «шкідливий» або «безпечний» — і модель сама вчиться відрізняти одне від іншого.

Якщо спростити, процес виглядає так:

  • Ми маємо вхідні дані (наприклад, URL-адреси).
  • Ми знаємо правильну відповідь для кожного прикладу (0 — безпечна, 1 — шкідлива).
  • Модель шукає закономірності, які допомагають передбачити відповідь для нових, ще не бачених URL.

Це схоже на навчання людини: після достатньої кількості прикладів вона починає інтуїтивно розпізнавати патерни — навіть ті, які складно пояснити словами.

Основні типи машинного навчання

Машинне навчання охоплює кілька підходів, які відрізняються тим, як саме модель отримує досвід і що вважається навчанням.

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

  • Supervised Learning (навчання з учителем) — маємо приклади з правильними відповідями (мітками). Алгоритм навчається передбачати ці мітки для нових випадків. Використовується для класифікації (наприклад, «шкідливо / безпечно») або регресії (передбачення числових значень). Саме цей тип ми використовуємо у нашому проєкті.
  • Unsupervised Learning (без учителя) — жодних правильних відповідей немає — лише «сирі» дані.Алгоритм сам шукає структуру в них: кластери, схожість, аномалії. Приклад — групування користувачів за поведінкою або виявлення нетипових запитів у мережі.
  • Reinforcement Learning (з підкріпленням). Алгоритм — це агент, який пробує різні дії, отримує нагороди або штрафи й поступово вчиться стратегії, що приносить найкращий результат. Використовується у сфері робототехніки, іграх, керуванні трафіком, а також у новітніх великих мовних моделях.
  • Semi-supervised / Self-supervised Learning — комбінація попередніх підходів: коли маємо трохи розмічених даних і багато нерозмічених. Дає змогу суттєво скоротити вартість розмітки великих датасетів.

Як же тоді вибрати підхід? Якщо коротко:

Ситуація

Підхід

Є дані з мітками (наприклад, «шкідливий/безпечний»)

Supervised Learning

Міток немає, хочемо знайти структуру

Unsupervised Learning

Є дії, нагороди та зворотний зв’язок

Reinforcement Learning

Для нашої задачі все просто: ми маємо URL + правильну відповідь — працюємо в режимі supervised learning.

Постановка задачі

Кожного дня ми відкриваємо десятки посилань — у пошті, месенджерах, соцмережах. Частина з них може бути фішинговою або шкідливою, навіть якщо зовні виглядає цілком безпечно.

Більшість таких URL-адрес мають спільні патерни:

  • дивна структура домену (наприклад, paypal-login-secure-update.com);
  • довгі або «зашумлені» шляхи (/secure/confirm/account/index.php?id=98237);
  • наявність IP-адреси замість домену (http://192.168.1.5/login).

Наша мета — навчити модель виявляти ці закономірності автоматично, навіть якщо конкретну адресу вона бачить уперше.

Математично ми маємо класичну задачу бінарної класифікації:

Вхідні дані (X): числові характеристики кожного URL — так звані фічі (features).

Вихідна змінна (y): цільовий клас 1 або 0, тобто «шкідливий» / «безпечний».

Окрім передбачення класу, корисно також отримувати ймовірність p(malicious) — наскільки впевнена модель у своєму рішенні.

Це дозволяє регулювати поріг прийняття рішення:

  • при p > 0.8 — 100% шкідливе (блокуємо);
  • при 0.5 < p < 0.8 — підозріле (потрібна перевірка);
  • при p < 0.5 — безпечне.

Таким чином ми можемо адаптувати систему під різні сценарії: суворі фільтри в корпоративному середовищі або м’якші для звичайного користувача.

Для навчання моделі потрібні:

Самі URL-адреси — текстові рядки, які будемо аналізувати.

Мітка (label) — інформація про те, чи є цей URL шкідливим (1) або безпечним (0).

Перед тренуванням дані треба очистити та нормалізувати:

  • видалити пробіли й зайві символи;
  • привести схему (http/https) до єдиного вигляду;
  • додати схему до тих рядків, де вона відсутня (example.com → http://example.com);
  • обробити невалідні або порожні URL.

Збір та підготовка даних

Модель машинного навчання настільки хороша, наскільки хороші її дані. Це правило просте, але критичне: навіть найкращий алгоритм не врятує, якщо ми подамо на вхід «сміття». Тому перший реальний крок у побудові AI-моделі — підготовка якісного датасету.

Для задачі «виявлення шкідливих URL» ідеально підходить відкритий датасет Malicious and Benign URLs Dataset з Kaggle.

Він містить 450176 унікальних url у форматі:

url

label

result

http://paypal-login-verify.com/update

malicious

1

https://www.google.com/

benign

0

http://192.168.1.1/login.php

malicious

1

345739 з яких є звичайними (77%), а 104439 — шкідливими (23%).

Колонка url містить саму адресу, а label — позначку класу, result 1 — для malicious, 0 — для benign.

Це класичний приклад supervised learning, де ми знаємо правильні відповіді для навчання.

Головне — зберегти баланс, щоб модель не «вчилася» лише одному класу.

Альтернатива Kaggle — побудувати власний набір даних, об’єднавши декілька відкритих джерел. Проте цей процес потребує значних зусиль і ресурсів, тому на початковому етапі доцільніше скористатися готовим рішенням.

Тепер переходимо до найцікавішого — feature engineering, тобто перетворення рядка на числовий опис.

Довжини та структури

  • url_length — загальна довжина URL;
  • hostname_length — довжина домену;
  • path_length — довжина шляху;
  • fd_length — довжина першої директорії;
  • path_depth — кількість «/» у шляху.

Лічильники символів

  • count_minus, count_at, count_question, count_percent, count_dot, count_equal — скільки разів зустрічається кожен символ;
  • count_digits, count_letters — кількість цифр і літер у рядку;
  • count_dir — кількість директорій у шляху.

Бінарні ознаки (True / False)

  • use_of_ip — чи є в URL IP-адреса замість домену;
  • short_url — чи використовує URL-шортенер (bit.ly, t.co, tinyurl тощо);
  • has_port — чи вказано номер порту (наприклад, :8080).

Доменні характеристики

  • tld_length — довжина доменної зони (наприклад, .com, .info, .xyz);
  • n_subdomains — кількість піддоменів (чим більше — тим частіше це спроба маскування);
  • n_hyphens_in_host — кількість дефісів у доменному імені (ознака підробки бренду).

Усі ці фічі можна обчислити без жодних HTTP-запитів — модель працює повністю локально.

Кожна така ознака — це маленький сигнал, який сам по собі може нічого не означати, але у комбінації з іншими формує поведінковий профіль URL. Модель не шукає одне-єдине правило — вона навчається розпізнавати патерни на основі статистики.

Візуалізація фічів: навіщо і як читати графіки

Щоб глибше зрозуміти поведінку шкідливих та легітимних URL, варто подивитись не лише на цифри, а й на розподіли фічів. Графіки нижче показують, як саме відрізняються дві групи — Benign та Malicious — за окремими характеристиками URL.

Навіщо це потрібно? Візуалізація фічів допомагає:

  • Побачити закономірності — де саме класи відрізняються (наприклад, шкідливі URL довші, мають більше спеціальних символів або більшу ентропію).
  • Виявити аномалії та шум — окремі групи даних із підозрілими значеннями, які можуть спотворювати навчання.
  • Зрозуміти корисність фічі — якщо розподіли двох класів суттєво не перетинаються, така фіча дає цінну інформацію для моделі.
  • Підвищити інтерпретованість моделі — завдяки графікам можна пояснити, чому певний URL оцінюється як ризиковий.
  • Якщо ж розподіли Benign і Malicious майже збігаються, така фіча не додає корисної інформації для класифікації — її можна вилучити або замінити іншою, більш показовою.

Аналіз основних фічей

Довжини (hostname_length, path_length)

Малварні URL, як правило, довші, оскільки містять піддомени, глибокі шляхи та параметри.

На графіках видно зміщення розподілу червоної кривої (Malicious).

Ентропія та аномальні символи

Ця фіча вимірює «випадковість» рядка.

У шкідливих URL вона вища через довгі незрозумілі токени або base64-подібні параметри (/a9sd8f7sd9f...).


Лічильники символів

count-, count@, count?, count%, count.

Ці фічі показують частоту спеціальних символів у рядку.

Отже, на цьому етапі ми виконали візуальний аналіз ознак (feature distributions), щоб оцінити якість отриманих фічей і зрозуміти, наскільки вони придатні для подальшого моделювання. Для цього було побудовано розподіли для основних характеристик URL.

Оцінка точності моделі

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

Accuracy (точність класифікації)

Показує, яку частку прикладів модель класифікувала правильно:

де:

  • TP (True Positives) — шкідливі URL, які правильно розпізнано як шкідливі;
  • TN (True Negatives) — безпечні URL, які правильно визначено як безпечні;
  • FP (False Positives) — безпечні URL, які помилково віднесено до шкідливих;
  • FN (False Negatives) — шкідливі URL, які модель не розпізнала.

Accuracy зручно використовувати, якщо вибірка збалансована, тобто кількість benign ≈ malicious.

Precision (точність передбачення)

Відповідає на запитання:

«Із усіх URL, які модель визнала шкідливими, скільки насправді такими є?»

Високий precision означає, що система рідко дає хибні тривоги, тобто мало помилкових спрацювань на безпечні сайти.

Recall (повнота)

Показує, наскільки добре модель знаходить усі шкідливі URL:

Якщо recall низький — частина небезпечних посилань проходить непоміченою.

У кібербезпеці це часто важливіше за precision, адже пропущена загроза небезпечніша, ніж зайва перевірка.

F1-score

Комбінує precision і recall в одну збалансовану метрику — гармонічне середнє між ними:

F1-score особливо зручний, коли дані незбалансовані (наприклад, 90% безпечних і 10% шкідливих URL).

Створення першої моделі на Python

Мінімальний стек:

  • Python 3.9+;
  • pandas / numpy — обробка даних;
  • scikit-learn — моделі, метрики, спліти, калібрування;
  • tld — робота з TLD/доменною частиною;
  • joblib — збереження моделей.

Встановлення:

pip install -U pandas numpy scikit-learn tld joblib

Основні кроки побудови моделі:

  1. Завантажити датасет у DataFrame(url, result).
  2. Нормалізувати URL і безпечно розпарсити.
  3. Побудувати ознаки (лексика/структура).
  4. Розділити на train/test.
  5. Навчити моделі: DecisionTree, LogisticRegression (balanced), RandomForest.
  6. Оцінити (Accuracy, Precision/Recall/F1).

Розберімо кожен етап окремо.

Завантаження датасету у Dataframe (url, result)

Зчитуємо CSV у pandas.DataFrame — базовий контейнер для подальшої обробки. Валідуємо схему: гарантуємо, що є рівно ті стовпці, на які розрахований пайплайн (url — сирий рядок; result — цільова змінна 0/1). Приводимо тип result до int — узгодженість типів важлива для моделей і метрик.

import pandas as pd

df = pd.read_csv(<path_to_our_dataset>)
if "url" not in df.columns or "result" not in df.columns:
    raise SystemExit("CSV must contain 'url' and 'result' columns.")

df["result"] = df["result"].astype(int)

Нормалізація URL і безпечний парсинг

Далі ми нормалізуємо URL, додаючи схему (http://) у разі її відсутності, обрізаючи зайві лапки чи пробіли, і безпечно парсимо адресу. Завдяки функціям normalize_url та safe_urlparse ми уніфікуємо всі рядки до стабільного формату, що дозволяє однаково обробляти адреси типу example.com, //example.com і example.com. Це важливо, бо у сирих датасетах часто трапляються неповні чи пошкоджені адреси, які можуть спричинити помилки під час аналізу.

import re
from urllib.parse import urlparse
import pandas as pd

_SCHEME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9+.\-]*://')

def safe_str(u):
    """Безпечно конвертувати значення в рядок, обробляючи None та NaN."""
    return "" if u is None or (isinstance(u, float) and pd.isna(u)) else str(u)

def normalize_url(u: str) -> str:
    "Нормалізує та перевіряє рядок URL-адреси."
    if u is None or (isinstance(u, float) and pd.isna(u)):
        return ""

    s = str(u).strip().strip('"\'')
    if not s:
        return ""

    if s.startswith("//"):
        s = "http:" + s

    if not _SCHEME_RE.match(s):
        s = "http://" + s

    return s

def safe_urlparse(u: str):
    """Повертає (scheme, netloc, path, query) і не кидає винятків."""
    s = normalize_url(u)
    try:
        p = urlparse(s)
        scheme, netloc, path, query = p.scheme or "", p.netloc or "", p.path or "", p.query or ""

        if not netloc and path:
            first, _, rest = path.lstrip("/").partition("/")
            if "." in first and " " not in first:
                netloc = first
                path = "/" + rest if rest else ""

        return scheme, netloc, path, query
    except Exception:
        return "", "", "", ""

Побудова ознак

На цьому кроці ми створюємо набір фічів, що описують структуру та властивості URL. Ми розраховуємо довжини домену, шляху, першої директорії, TLD, а також кількість символів -, @, ?, %, . та =. Окремо рахуємо кількість цифр, літер, глибину шляху, перевіряємо наявність IP-адреси та скорочувачів посилань. Такі ознаки допомагають моделі розрізняти типові шаблони шкідливих і безпечних посилань — наприклад, фішингові URL часто довші, містять більше спеціальних символів і можуть використовувати IP замість домену.

import re
from tld import get_tld

FEATURE_COLS = [
    "hostname_length", "path_length", "fd_length", "tld_length",
    "count-", "count@", "count?", "count%", "count.",
    "count=", "count-www", "count-digits", "count-letters",
    "count_dir", "use_of_ip", "short_url"
]

def having_ip_address(url):
    s = sstr(url)
    match = re.search(
        r"(([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\."
        r"([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\/)|"
        r"((0x[0-9a-fA-F]{1,2})\.(0x[0-9a-fA-F]{1,2})\.(0x[0-9a-fA-F]{1,2})\.(0x[0-9a-fA-F]{1,2})\/)|"
        r"(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}",
        s,
    )
    return 1 if match else 0

def fd_length(url):
    path = safe_urlparse(url)[2]
    parts = [p for p in path.split("/") if p]
    return len(parts[0]) if parts else 0

def shortening_service(url):
    s = sstr(url).lower()
    match = re.search(
        r"(bit\.ly|goo\.gl|shorte\.st|go2l\.ink|x\.co|ow\.ly|t\.co|tinyurl|tr\.im|is\.gd|cli\.gs|"
        r"yfrog\.com|migre\.me|ff\.im|tiny\.cc|url4\.eu|twit\.ac|su\.pr|twurl\.nl|snipurl\.com|"
        r"short\.to|budurl\.com|ping\.fm|post\.ly|just\.as|bkite\.com|snipr\.com|fic\.kr|loopt\.us|"
        r"doiop\.com|short\.ie|kl\.am|wp\.me|rubyurl\.com|om\.ly|to\.ly|bit\.do|lnkd\.in|"
        r"db\.tt|qr\.ae|adf\.ly|bitly\.com|cur\.lv|q\.gs|po\.st|bc\.vc|twitthis\.com|u\.to|j\.mp|"
        r"buzurl\.com|cutt\.us|u\.bb|yourls\.org|prettylinkpro\.com|scrnch\.me|filoops\.info|"
        r"vzturl\.com|qr\.net|1url\.com|tweez\.me|v\.gd|link\.zip\.net)",
        s,
    )
    return 1 if match else 0

def build_features(df: pd.DataFrame) -> pd.DataFrame:
    df["url_length"] = df["url"].apply(lambda i: len(safe_str(i)))
    df["hostname_length"] = df["url"].apply(lambda i: len(safe_urlparse(i)[1]))
    df["path_length"]     = df["url"].apply(lambda i: len(safe_urlparse(i)[2]))
    df["fd_length"]       = df["url"].apply(fd_length)

    df["tld"] = df["url"].apply(lambda i: get_tld(normalize_url(i), fail_silently=True))
    df["tld_length"] = df["tld"].apply(lambda t: len(t) if isinstance(t, str) and t else -1)
    df.drop(columns=["tld"], inplace=True)

    sstr = lambda i: safe_str(i)
    df["count-"]      = df["url"].apply(lambda i: sstr(i).count("-"))
    df["count@"]      = df["url"].apply(lambda i: sstr(i).count("@"))
    df["count?"]      = df["url"].apply(lambda i: sstr(i).count("?"))
    df["count%"]      = df["url"].apply(lambda i: sstr(i).count("%"))
    df["count."]      = df["url"].apply(lambda i: sstr(i).count("."))
    df["count="]      = df["url"].apply(lambda i: sstr(i).count("="))
    df["count-www"]   = df["url"].apply(lambda i: sstr(i).lower().count("www"))
    df["count-digits"]  = df["url"].apply(lambda i: sum(ch.isdigit() for ch in sstr(i)))
    df["count-letters"] = df["url"].apply(lambda i: sum(ch.isalpha() for ch in sstr(i)))
    df["count_dir"]     = df["url"].apply(lambda i: safe_urlparse(i)[2].count("/"))
    df["use_of_ip"]     = df["url"].apply(having_ip_address)
    df["short_url"]     = df["url"].apply(shortening_service)

    return df

df = build_features(df)
missing = [c for c in FEATURE_COLS if c not in df.columns]

if missing:
    raise SystemExit(f"Missing feature columns after feature engineering: {missing}")

X = df[FEATURE_COLS]
y = df["result"]

Розподіл даних на train/test

Підготовлені дані ділимо на навчальну й тестову вибірки у співвідношенні 70/30 за допомогою train_test_split із параметром stratify=y, щоб зберегти баланс між класами. Параметр stratify=y забезпечує, щоб частка класів у тренувальній і тестовій вибірках залишалася такою ж, як у всьому датасеті. Це особливо важливо при розбалансованих класах, коли, наприклад, звичайних URL значно більше, ніж шкідливих. Завдяки stratify=y ми гарантуємо, що і в тренуванні, і в тесті модель бачить реалістичне співвідношення класів, отже її метрики (особливо Recall і Precision) будуть коректно відображати реальну якість класифікації.

Фіксований random_state=42 забезпечує відтворюваність результатів — це означає, що кожен запуск дасть однаковий розподіл прикладів.

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.30, random_state=42, stratify=y
)

У нашому випадку ми залишаємо 30 % даних для тестування, оскільки датасет досить великий — понад 450 тис. URL, з яких 23 % шкідливі. Такий розподіл гарантує, що в тестовій вибірці залишиться приблизно 30 тис. шкідливих і понад 100 тис. безпечних прикладів — цього більш ніж достатньо для стабільної оцінки метрик. Менший тест міг би зробити оцінку менш надійною, а більший — зменшив би дані для навчання. Тому 30 % — оптимальний компроміс між якісною перевіркою та достатнім обсягом тренувальних даних.

Навчання моделей: DecisionTree, LogisticRegression, RandomForest

Далі навчаємо кілька моделей: DecisionTreeClassifier як базову інтерпретовану, LogisticRegression з class_weight="balanced" для лінійного підходу, і RandomForestClassifier, обгорнутий у CalibratedClassifierCV, щоб отримати більш точні ймовірності. Таке поєднання дозволяє порівняти прості й ансамблеві методи, оцінити їх точність та поведінку на різних типах фічів.

from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.calibration import CalibratedClassifierCV

dt_model  = DecisionTreeClassifier(
    random_state=42
)

base_rf = RandomForestClassifier(
    n_estimators=300,
    random_state=42,
    n_jobs=-1,
    class_weight="balanced_subsample",
)

rf_model = CalibratedClassifierCV(
    base_rf,
    cv=3,
    method="sigmoid"
)

log_model = LogisticRegression(
    max_iter=1000,
    solver="lbfgs",
    class_weight="balanced"
)

Оцінка якості (Accuracy, Precision/Recall/F1, ROC-AUC)

На завершення проводимо оцінку якості. Для кожної моделі виводимо точність (Accuracy), матрицю помилок, Precision, Recall, F1-score, а для тих, що підтримують predict_proba, — ще й ROC-AUC (Receiver Operating Characteristic — Area Under Curve) — це площа під кривою, яка показує, наскільки добре модель відокремлює позитивні приклади від негативних. Це допомагає побачити, як добре модель відокремлює шкідливі адреси від легітимних.

Результати навчання зберігаються у вигляді .joblib-файлів, а список використаних фічів — у features.json, щоб забезпечити відтворюваність під час прогнозування на нових даних.

import os, json, joblib
from sklearn.metrics import confusion_matrix, classification_report, accuracy_score, roc_auc_score

outdir = <your_path_to_save_model>
os.makedirs(outdir, exist_ok=True)

def train_eval(name, model):
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    try:
        auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) if hasattr(model, "predict_proba") else None
    except:
        auc = None

    print(f"\n=== {name} ===")
    print("Accuracy:", accuracy_score(y_test, y_pred))
    print("Confusion matrix:\n", confusion_matrix(y_test, y_pred))
    print("Report:\n", classification_report(y_test, y_pred, digits=3))

    if auc is not None:
        print("ROC-AUC:", round(auc, 4))

    out_path = os.path.join(outdir, f"{name}.joblib")
    joblib.dump(model, out_path)
    print(f"Saved: {out_path}")

train_eval("decision_tree", dt_model)
train_eval("random_forest", rf_model)
train_eval("log_regression", log_model)

with open(os.path.join(outdir, "features.json"), "w", encoding="utf-8") as f:
    json.dump({"feature_cols": FEATURE_COLS}, f, ensure_ascii=False, indent=2)

Accuracy (точність класифікації) показує частку правильних передбачень серед усіх прикладів, але при розбалансованих класах цей показник може вводити в оману.

Наприклад, у нашому наборі даних 77 % URL є звичайними, а лише 23 % — шкідливими. Модель, яка завжди прогнозує «звичайний», отримає Accuracy = 0.77, хоча фактично вона не виявляє жодного шкідливого сайту. Тобто висока точність у такому випадку не означає гарну здатність розпізнавати важливий (менший) клас.

Тому для розбалансованих наборів даних потрібно оцінювати не лише Accuracy, а й Precision, Recall, F1-score та ROC-AUC — вони враховують якість виявлення саме шкідливих URL і краще відображають реальну ефективність моделі.

Очікуваний результат (приклад)

Decision Tree

Accuracy: 0.9733

Confusion matrix:

[[101941 1781]

[ 1818 29513]]

precision

recall

f1-score

0 (Benign)

0.982

0.983

0.983

1 (Malicious)

0.943

0.942

0.943

Random Forest

Accuracy: 0.9819

Confusion matrix:

[[103082 640]

[ 1806 29525]]

precision

recall

f1-score

0 (Benign)

0.983

0.994

0.988

1 (Malicious)

0.979

0.942

0.960

Logistic Regression

Accuracy: 0.9612

Confusion matrix:

[[102258 1464]

[ 3782 27549]]

precision

recall

f1-score

0 (Benign)

0.964

0.986

0.975

1 (Malicious)

0.950

0.879

0.913

У нашому наборі даних 77 % URL є звичайними, а 23 % — шкідливими. Це помірно розбалансований класовий розподіл: менший клас не є рідкісним, тому метрика Accuracy все ще може служити адекватним показником якості, якщо її інтерпретувати разом із матрицею плутанини (Confusion Matrix).

Наприклад, у результатах вище Decision Tree має Accuracy ≈ 0.97, що означає, що модель правильно класифікує 97 % усіх URL. Але з матриці видно, що серед помилок все ще є близько 1818 невиявлених шкідливих адрес — тобто Recall для класу Malicious трохи нижчий. RandomForest натомість показує Accuracy ≈ 0.982 і зменшує кількість пропущених шкідливих сайтів до 1806 при дуже низькій кількості хибнопозитивних — це свідчить, що алгоритм добре навчився розрізняти обидва класи.

Отже, високе значення Accuracy у поєднанні зі збалансованими помилками в матриці підтверджує, що модель працює, а не просто «вгадує більшість». У цьому випадку RandomForest — найкращий вибір: він демонструє найменшу кількість помилок обох типів і водночас дає стабільні, калібровані ймовірності, що важливо для подальшого визначення порогу ризику.

Аналіз помилок

Навіть якщо загальна точність висока, корисно подивитися, де саме модель помиляється.

Типові ситуації:

False Positive (FP): легітимний сайт має підозрілу структуру (наприклад, secure-login.bankinfo.net) — можливо, ознаки надто «покарали» за кількість дефісів чи довжину.

False Negative (FN): справжній фішинговий URL виглядає «чисто» (paypa1-login.com) — можливо, не враховано подібність до брендів або енкодинг символів.

Такі спостереження допомагають уточнити набір ознак (features), додати нові або відкоригувати пороги рішень.

Підсумкова оцінка

Для зручності оцінки можна використати готову функцію з sklearn.metrics:

from sklearn.metrics import classification_report, confusion_matrix

y_pred = model.predict(X_test)

print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred))

Цей звіт одразу покаже precision, recall, F1-score і accuracy для кожного класу.

Висновки

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

Але це лише перший крок.

У реальних проєктах, з якими ми працюємо, на ці базові підходи нашаровуються інші рівні аналізу:

  • мережеві та контентні ознаки;
  • інформація з DNS, WHOIS і SSL-сертифікатів;
  • моделі з обробки тексту для аналізу назв доменів і контенту сторінок;

Саме така багаторівнева архітектура дозволяє створювати продуктивні системи виявлення фішингу та шкідливих сайтів.

Тому цей матеріал має насамперед навчальну мету — показати, що навіть базові інструменти Python, pandas та scikit-learn дають змогу зрозуміти логіку машинного навчання, зробити перші кроки у фічер-інженерії та відчути, як працює реальний ML-процес.

А далі — усе залежить від глибини, яку ви готові досліджувати.

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

👍ПодобаєтьсяСподобалось17
До обраногоВ обраному12
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Цікавий та корисний контент! Дякую, Олександре 👍

Дякую, Андрію! Радий, що матеріал сподобався😊

до речі, прикольно виходить:

Приміром, кажу Клоду:
«зроби мені докладну шпаргалку на тему контекстна дистиляція...»

І через хвилину — готово! Сама суть. А був би підручник — я б миттєво потонув, і втратив би цікавість.

Іншим разом набридло підбирати слова... своєю природною мовою... Думаю, ці операції з текстом вже хтось інший мільйон разів робив... І мабуть є термінологія , добре зрозуміла машині...

Може іноді треба просто назвати алгоритм, і воно зрозуміє 🤔

Знову кажу Клоду: «зроби мені шпаргалку — основні поняття і терміни... які використовуються при обробці документів — щоб тобі було зрозуміло...»

І він з ентузіазмом робить... люди так не вміють 🙂

І він з ентузіазмом робить...

щось типу адаптації?
ввічливий промптер — задоволений ШІ, ввічливий ШІ — задоволений користувач

до речі... десь було дослідження... шо стиль спілкування впливає на якість роботи моделі 🤔

мабуть це працює так...правильна атмосфера призводить до більш точних промптів... і як наслідок — результат покращується

так, також зустрічав достатньо поглядів відносно role-playing/be-polite як нп
youtube.com/shorts/rVlmbhwn0RM
але сам ні підтвердити ні спростувати то не можу (бо не маю відповідної expertise в тому)

мабуть стаття дуже цікава, додав у закладки... буде відповідний настрій — вивчу уважно 🤔

ще можна було б на початку вставити абзац із відповіддю на питання:
А для чого витрачати час, сили, гроші на власну аі модель?

чи це очевидно для всіх, крім мене? 🤔
чи справжніх винахідників не цікавить питання — для чого? 🙂

А для чого витрачати час, сили, гроші на власну аі модель?

тому що у багатьох випадках це може бути дешевше... чим постійно використовувати LLM

великі моделі — універсальні, а це не завжди потрібно

Дякую за коментар🙂
Дуже слушне питання. Власна ML-модель має сенс, коли є конкретна, вузька задача: вона зазвичай дешевша в довгостроковій перспективі, працює швидше, дає повний контроль над даними й поведінкою моделі та не залежить від зовнішніх сервісів.
Великі LLM — універсальні, але для вузьких задач на кшталт цієї вони не завжди є оптимальним вибором.

конкретна, вузька задача

можна детальніше — як оцінюється складність задачі ?

ну от приклади:
Сільгоспвиробник 1 вирощує на продаж 1 культуру на площі 1000 гектарів із рентабельністю +50%...

Сільгоспвиробник 2 вирощує для себе... 50 культур на площі 1000 м2 (десять соток) ... із рентабельністю від −100% до +1000%

де складніша задача? 🤔

Підписатись на коментарі