Як робити табличні прогнози без навчання за допомогою NVIDIA Structured Data Models
У цьому посібнику ми реалізуємо Structured Data Models — нову бібліотеку NVIDIA, яка об’єднує кілька попередньо навчених фундаментальних моделей для таблиць в одному інтерфейсі PyTorch, і використаємо її для виконання прогнозів без навчання. Бібліотека обгортає TabICLv2 і KumoTabular для роботи з окремими таблицями, а KumoRelational — для цілих реляційних баз даних. Кожна з них відповідає за допомогою навчання в контексті: ми передаємо моделі набір розмічених рядків і рядки, для яких потрібно виконати прогноз, а вона повертає ймовірності або повний прогнозний розподіл за один прямий прохід. Ми почнемо з типізованих табличних тензорів бібліотеки, порівняємо дві табличні моделі з навченими базовими моделями scikit-learn, повторно використаємо закодований контекст для багатьох запитів, зчитаємо калібровані прогнозні інтервали для регресії та масштабуємося до двадцяти класів. Потім розглянемо випадки, коли стандартні налаштування непомітно працюють неправильно: рецепт попередньої обробки, який відкидає стовпці з часовими мітками й текстом, а також засіб пояснення з двома пастками. Після цього створимо невелику базу даних магазину з трьома таблицями й безпосередньо спрогнозуємо активність клієнтів, зокрема розглянемо єдиний аргумент, який відділяє чесну реляційну модель від моделі з витоком даних. Перегляньте повний код тут.
import os
import sys
import time
import textwrap
import traceback
import subprocess
RESULTS = {}
def banner(title):
print("\n" + "=" * 78)
print(title)
print("=" * 78)
def section(name):
def wrap(fn):
def run(*a, **kw):
banner(name)
try:
out = fn(*a, **kw)
RESULTS[name] = out if isinstance(out, str) else "ok"
return out
except Exception as e:
RESULTS[name] = f"SKIPPED / FAILED -> {type(e).__name__}: {e}"
print(f"\n[!] {name} did not complete: {type(e).__name__}: {e}")
traceback.print_exc(limit=3)
return None
return run
return wrap
banner("1. Install SDM, and the one wheel URL its own error message gets wrong")
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "git+https://github.com/NVIDIA/structured-data-models.git@b982fbf4188ea8db17249fe81338723106100b63"], check=True)
import torch
# The relational sampler needs pyg-lib on CPU. Its wheels are published per MINOR torch version
# (torch-2.14.0+cpu.html), but SDM's own ImportError builds the URL from the exact patch version
# (torch-2.14.1+cpu.html), which is an empty page. Build it from MAJOR.MINOR.0 instead.
major, minor = torch.__version__.split("+")[0].split(".")[:2]
flavour = "cpu" if torch.version.cuda is None else "cu" + torch.version.cuda.replace(".", "")
PYG_INDEX = f"https://data.pyg.org/whl/torch-{major}.{minor}.0+{flavour}.html"
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "pyg-lib", "-f", PYG_INDEX], check=True)
import numpy as np
import pandas as pd
import pyg_lib
import sdm
import sdm.processing as sp
from sklearn.datasets import load_breast_cancer, load_diabetes, make_classification
from sklearn.ensemble import HistGradientBoostingClassifier, HistGradientBoostingRegressor
from sklearn.linear_model import LogisticRegression, RidgeCV
from sklearn.metrics import roc_auc_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
DEVICE = "cpu" # verified end to end on CPU; on a T4 runtime set "cuda" for the tabular models
print(f" sdm {sdm.__version__} | torch {torch.__version__} | pyg-lib {pyg_lib.__version__}"
f" | device {DEVICE}")
print(f" pyg-lib wheels from {PYG_INDEX}")
print("\n One interface over several pretrained structured-data foundation models:")
for name, what, weights in [
("TabICLv2", "tabular, in-context learning (ICML '26)", "~110 MB per task head, BSD-3"),
("KumoTabular", "tabular, small / medium / large", "small ~110 MB, large ~855 MB, OpenMDW"),
("KumoRelational", "multi-table, KumoRFM-2", "~120 MB, OpenMDW"),
("TabFM, TimesFM 3", "Google tabular / time series", "6.5 GB / 1.3 GB, non-commercial: not used here"),
]:
print(f" {name:18s} {what:42s} {weights}")
print("\n No model below is trained or fine-tuned. Every prediction is a forward pass that reads")
print(" labelled 'context' rows and answers 'query' rows, like a prompt with examples in it.")
Ми встановлюємо Structured Data Models із репозиторію GitHub, зафіксувавши версію на коміті, проти якого перевірявся цей ноутбук, оскільки пакет у PyPI є лише заповнювачем. Реляційному семплеру також потрібен pyg-lib, і саме тут виникає перша пастка. Колеса pyg-lib публікуються для кожного мінорного випуску PyTorch на сторінках із назвами на кшталт torch-2.14.0+cpu, але власний ImportError бібліотеки формує URL на основі точної версії патчу — torch-2.14.1+cpu. Це порожня сторінка, тому дотримання цієї рекомендації завершується помилкою. Натомість ми формуємо URL індексу з основної та мінорної версій. Усе запускається на CPU — саме цей шлях тут перевірено від початку до кінця. Ми також виводимо моделі, доступні в бібліотеці, разом із розмірами їхніх контрольних точок і ліцензіями; дві моделі Google, які вона також обгортає, TabFM і TimesFM 3, є великими та некомерційними, тому ми їх не використовуємо. Перегляньте повний код тут.
Уся робота бібліотеки проходить через TableTensor — підклас тензора PyTorch, який зберігає окремий типізований блок для кожного семантичного типу: числового, категоріального, дат і часу, текстового та ідентифікатора. infer_stypes визначає типи, і зі стандартними налаштуваннями неправильно обробляє дві речі в реалістичному наборі заявок до служби підтримки: довільний текст коментаря стає категорією з чотирмастами рівнями, а ідентифікатор заявки — числом, тому обидва потрапляють до моделі як ознаки. Передавання прапорців виведення тексту та ідентифікатора виправляє це. Зворотне перетворення в pandas зберігає кожне значення, включно з пропущеними, але не кожен тип даних: числа повертаються як float32, оскільки числовий блок є одним тензором float, nullable-цілі числа стають числами з NaN, а категоріальні рядки повертаються як категорія pandas. У pandas 2.x рядкові стовпці виводяться як object, а не str, а часові мітки надходять із наносекундною точністю; самі значення в обох випадках залишаються такими самими. Варто знати ще про одну пастку перед завантаженням справжнього журналу подій: бібліотека зберігає дати й час як цілі мікросекунди, а Arrow відмовляється виконувати будь-яке перетворення, яке призвело б до втрати цифри. Стовпець із наносекундною точністю, де всі значення є цілими секундами, перетворюється без проблем, але одного значення з наносекундними розрядами достатньо, щоб перетворення всього стовпця завершилося помилкою. Тому спочатку округлюємо часові мітки до мікросекунд. Перегляньте повний код тут.
Табличні моделі отримують розмічені контекстні рядки та нерозмічені рядки запиту й повертають TableTensor з одним стовпцем імовірностей для кожного класу, названим відповідно до мітки. Отже, етапу fit немає, як і гіперпараметрів. На наборі даних про рак молочної залози, маючи триста розмічених рядків і двісті шістдесят дев’ять рядків для прогнозування, TabICLv2 і KumoTabular досягають точності 0.970 та AUC 0.996 і 0.995 без навчання. Для порівняння, логістична регресія має показники 0.970 і 0.993, а градієнтний бустинг — 0.967 і 0.992; обидві моделі навчені на тих самих трьохстах рядках. Параметр num_estimators визначає, скільки по-різному попередньо оброблених представлень даних усереднює модель, тобто реалізує вбудований ансамблевий підхід бібліотеки.
Безпосередній виклик моделі повторно кодує контекст під час кожного виклику. Якщо ті самі розмічені рядки мають відповідати на багато пакетів запитів, fit кодує контекст один раз у кеш ключів і значень, а predict після цього обробляє лише рядки запиту; clear звільняє кеш. Під час fit нічого не навчається: тут це означає «запам’ятати ці приклади». Обробка п’яти пакетів по тридцять рядків для того самого контексту з чотирьохсот рядків на нашому CPU завершилася в кілька разів швидше, а після кешування контексту кожен наступний пакет вимагав лише малої частини часу повного прямого проходу. Точне співвідношення залежить від комп’ютера, тому комірка вимірює його, а не припускає заздалегідь.
Для регресії модель повертає не одне число. Вона повертає 999 квантилів для кожного рядка запиту — повний прогнозний розподіл, отриманий за один прямий прохід. На наборі даних про діабет із трьомастами розмічених рядків медіана цього розподілу має RMSE 53.6, що трохи краще за RidgeCV із показником 54.3 і помітно краще за градієнтний бустинг із показником 64.7; обидві базові моделі навчені на тих самих рядках. Кориснішою властивістю є калібрування: інтервал між 10-м і 90-м процентилями містить 81.0% відкладених цільових значень, а інтервал між 5-м і 95-м процентилями — 91.5%, що близько до номінальних 80% і 90%. Водночас смуга 50% виявляється дещо завузькою й охоплює 43.7% значень.
Попередньо навчена класифікаційна голова має фіксовану кількість слотів класів, а бібліотека обробляє задачі з більшою кількістю класів за допомогою кодів із виправленням помилок: розбиває задачу на менші закодовані підзадачі та об’єднує логарифми ймовірностей. Користувач цього не бачить: у збалансованій синтетичній задачі з двадцятьма класами та тридцятьма розміченими рядками на клас TabICLv2 повертає двадцять стовпців імовірностей і досягає точності 0.760 за один прямий прохід, тоді як градієнтний бустинг, навчений на тих самих рядках, досягає 0.615.
Попередня обробка в бібліотеці реалізована як Recipe — набір комбінованих процесорів. Стандартний рецепт TabICLv2 визначає обробку за семантичним типом і має гілки лише для категоріальних та числових стовпців. Тому стовпці з часовими мітками й текстом ніколи не стають ознаками. У заявках до служби підтримки, де ескалація залежить від часу відкриття та таких слів, як refund, стандартний рецепт дає AUC 0.525 — майже випадковий результат. Бібліотека дійсно генерує RuntimeWarning із назвами проігнорованих типів стовпців, але це попередження виводиться лише один раз: воно з’являється під час першого виклику й пригнічується для решти процесу. Отже, кожна наступна модель у тому самому ноутбуці або сервісі мовчки відкидає ці стовпці. Додавання двох процесорів виправляє ситуацію: AddCalendarFields перетворює часову мітку на циклічні ознаки години та дня тижня, підвищуючи AUC до 0.634, а TFIDF додає терміни з коментаря й підвищує показник до 0.789.
GradientExplainer бібліотеки повертає градієнт вибраного виходу за попередньо обробленими вхідними даними запиту. Ми перевіряємо його на синтетичних даних, де значення мають лише три з восьми ознак і відомі знаки впливу. Тут є дві пастки, які видно лише тоді, коли перевірити, що саме отримує функція виходу. По-перше, функція викликається один раз для кожного оцінювача, і кожен виклик перезаписує попередній результат. Тому для чотирьох оцінювачів функція виходу, активна лише під час першого виклику, дає пояснення з одних нулів, а отримане пояснення стосується лише останнього оцінювача. По-друге, сирий вихід кожного оцінювача має власний порядок класів, оскільки стандартний рецепт перемішує мітки класів для кожного оцінювача. Під час пояснення з одним оцінювачем у шести запусках вибір імовірності класу один за позицією стовпця змінює знак кожної ознаки у двох запусках зі зміненим порядком, тоді як вибір за назвою стовпця дає правильний знак у всіх шести. Усереднення за запусками відновлює три справжні ознаки, а шумові ознаки залишаються близькими до нуля.
KumoRelational приймає реляційну базу даних замість однієї таблиці. Ми генеруємо невеликий магазин із вісьмома сотнями клієнтів, шістдесятьма товарами та замовленнями за рік. Для частини клієнтів активність випадково різко зменшується після певної дати. Потім запитуємо, чи зробить кожен клієнт повторне замовлення протягом тридцяти днів після контрольної дати. RelationalData зберігає таблиці та зовнішні ключі між ними, а семплер проходить цими ключами назовні від кожного рядка задачі, збираючи до шістнадцяти сусідів на кожному кроці. Оголошення дати замовлення часовим стовпцем робить цей обхід часово залежним: ми перевіряємо, що для рядка задачі з датою першого червня семплер повертає останнє замовлення, зроблене тридцять першого травня. Отже, рядки, які визначають мітку, залишаються недоступними.
Потім ми відповідаємо на те саме питання трьома способами. TabICLv2 лише на таблиці клієнтів, маючи для роботи тільки регіон і тарифний план, досягає AUC 0.556. TabICLv2 на шести вручну створених ознаках історії — кількості замовлень і витратах за останні тридцять і дев’яносто днів, кількості днів від останнього замовлення та загальній кількості замовлень, обчислених строго до кожної контрольної дати, — досягає AUC 0.828. KumoRelational на необроблених таблицях, без жодних ознак, створених вручну, досягає 0.823. Реляційна модель майже зрівнюється з вручну створеним конвеєром, читаючи самі рядки замовлень через ключі станом на момент кожного рядка задачі.
Насамкінець ми запускаємо ту саму реляційну модель, не оголошуючи часовий стовпець. AUC зростає до 0.940. Без часового стовпця семплер може повертати замовлення, зроблені після контрольної дати, — саме ті замовлення, які визначають мітку. Отже, модель більше не прогнозує, а читає відповідь, і при цьому не виникає жодної помилки. У реальній базі даних реляційну модель, яка підозріло перевершує вручну створену базову модель, потрібно насамперед повторно запустити з оголошенням кожного часового стовпця.
Підсумок виводить однорядковий результат, який повернув кожен розділ, а також три звички, які варто перенести в практику: свідомо оголошуйте семантичні типи й рецепти, вибирайте класи за назвою всередині засобу пояснення та пояснюйте за допомогою одного оцінювача, а кожну таблицю з часовим стовпцем додавайте до time_columns. Далі він посилається на приклад донавчання, приклад RelBench і виконання на GPU.
Підсумовуючи, Structured Data Models від NVIDIA виконує свою головну обіцянку: у задачах класифікації, регресії, із двадцятьма класами та з базою даних із трьох таблиць її попередньо навчені моделі зрівнялися або перевершили базові моделі scikit-learn, навчені на тих самих рядках, хоча самі не навчалися, без додаткових зусиль повертали калібровані прогнозні розподіли, а в реляційному випадку досягли точності вручну створеного конвеєра ознак, працюючи з необробленими таблицями. Однак стандартні налаштування заслуговують на більшу увагу, ніж самі моделі. Довільний текст та ідентифікатори не виявляються без відповідного запиту, стандартний рецепт відкидає стовпці з часовими мітками й текстом і попереджає лише один раз за процес, засіб пояснення повертає результат лише останнього оцінювача та потребує вибору класів за назвою, URL pyg-lib у власному повідомленні про помилку бібліотеки веде на порожню сторінку, а реляційна модель без часового стовпця видає вражаюче, але беззмістовне число. Для кожної з цих проблем є виправлення в один рядок, і кожна інакше могла б пройти непоміченою. Саме це практичне правило варто винести в роботу з реальними даними: перевіряйте, що потрапляє до моделі, перш ніж довіряти тому, що з неї виходить.
Перегляньте повний код тут. Уся заслуга належить досліднику цього проєкту. Також стежте за нами в Twitter, приєднуйтеся до нашого ML SubReddit із понад 150 тисячами учасників і підписуйтеся на нашу розсилку. Стривайте! Ви є в Telegram? тепер ви також можете приєднатися до нас у Telegram.
Перекладено автоматично з англійської. Оригінал статті — за посиланням нижче.