<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hackathon on Benedykt Huszcza | Blog</title><link>https://blog.huszcza.dev/pl/tags/hackathon/</link><description>Recent content in Hackathon on Benedykt Huszcza | Blog</description><generator>Hugo -- gohugo.io</generator><language>pl</language><lastBuildDate>Tue, 07 Apr 2026 16:00:00 +0000</lastBuildDate><atom:link href="https://blog.huszcza.dev/pl/tags/hackathon/index.xml" rel="self" type="application/rss+xml"/><item><title>Transformery na wycince drzew - EnsembleAI 2026</title><link>https://blog.huszcza.dev/pl/p/ensemble-ai-2026/</link><pubDate>Tue, 07 Apr 2026 16:00:00 +0000</pubDate><guid>https://blog.huszcza.dev/pl/p/ensemble-ai-2026/</guid><description>&lt;img src="https://blog.huszcza.dev/p/ensemble-ai-2026/cover.jpeg" alt="Featured image of post Transformery na wycince drzew - EnsembleAI 2026" />&lt;h2 id="prolog">Prolog
&lt;/h2>&lt;p>Przyznaję bez bicia, trochę zbierałem się do napisania tego wpisu. Zmęczenie po hackathonach potrafi dać w kość, a opisanie tego, co udało nam się osiągnąć podczas tych 24 godzin, to nie lada wyzwanie, bo prób oraz wielorakich podejść było mnóstwo. Jednakże teraz, patrząc przez okno pociągu jadącego z Suwałk do Poznania, czuję, że wena przejmuje nade mną kontrolę, zupełnie jak aktualizacja Windowsa w randomowy wtorek o 12:40.&lt;/p>
&lt;p>Wena weną, ale rzeczywistość, z którą musieliśmy się zmierzyć na miejscu, była zdecydowanie mniej poetycka. Wyobraź sobie tabelę mającą 64 miliony wierszy. Wiem, że to zadanie jest dosyć trudne, dlatego śpieszę z pomocą. 64 miliony wierszy zapisanych czcionką Times New Roman (font size 12) to około 1 300 000 stron A4 (estymacja by Gemini).&lt;/p>
&lt;p>Co więcej, wyobraź sobie, że czytasz te 1 300 000 stron A4, a następnie masz na ich podstawie przewidzieć obciążenie sieci elektrycznej dla jakiegoś urządzenia. Zgadza się - nie jest to najłatwiejsze zadanie, dlatego, jak dobrze wiemy, do tego typu wyzwań pierwsze co wyciągniemy, to drzewa decyzyjne. My również tak zrobiliśmy na początku! Jednak po kilku godzinach postanowiliśmy zrobić coś kompletnie innego i użyliśmy architektury, która z założenia miała służyć do przetwarzania tekstu, a w ostatnim czasie jest adaptowana do wielu innych dziedzin. Nie przedłużając, opowiem Wam, jak ten eksperyment przyniósł nam &lt;strong>1. miejsce na 45 drużyn&lt;/strong> i dlaczego czasami warto wyrzucić bezpieczną instrukcję przez okno.&lt;/p>
&lt;h2 id="krótki-wstęp-o-formule-hackathonu-ensembleai">&lt;strong>Krótki wstęp o formule hackathonu EnsembleAI&lt;/strong>
&lt;/h2>&lt;p>Aby zrozumieć, jakie emocje towarzyszyły mi oraz zespołowi podczas tej zażartej walki, musimy zacząć od opisu formuły hackathonu, bo jest ona co najmniej nietypowa i zapewnia strzały dopaminy mocniejsze niż Reelsy z Instagrama.
Każde z 4 zadań jest oceniane oddzielnie, a punktacja przydzielana jest na podstawie podesłanych rozwiązań, specyficznych dla każdego zadania. W przypadku zadania 3, którym się zajmowałem, był to na przykład plik CSV z predykcjami miesięcznego obciążenia sici elektrycznej dla konkretnego przedziału czasu. Przez taką organizację newralgiczną częścią hackathonu była strona z leaderboardem, gdzie mogliśmy podejrzeć ile punktów mamy w danym zadaniu.
Rozwiązania można było przesyłać tylko co określony z góry czas, by między innymi uniknąć DDOS-owania serwerów. A więc, jak widać, po każdym kolejnym przesłaniu pliku następował pełen napięcia okres oczekiwania: czy i o ile nasze rozwiązanie poprawiło pozycję w rankingu.
&lt;img src="https://blog.huszcza.dev/p/ensemble-ai-2026/meme.png"
width="974"
height="528"
srcset="https://blog.huszcza.dev/p/ensemble-ai-2026/meme_hu6f14dcefde730a9f07f77544d8968fdb_607536_480x0_resize_box_3.png 480w, https://blog.huszcza.dev/p/ensemble-ai-2026/meme_hu6f14dcefde730a9f07f77544d8968fdb_607536_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="Meme z oczekiwaniem na wyniki"
class="gallery-image"
data-flex-grow="184"
data-flex-basis="442px"
>&lt;/p>
&lt;h2 id="ale-może-od-początku-co-jak-gdzie-i-w-ogóle-po-co">&lt;strong>Ale może od początku: co, jak, gdzie i w ogóle po co?&lt;/strong>
&lt;/h2>&lt;p>Wyzwanie przygotował dla nas jeden z partnerów hackathonu – Euros Energy, od którego dostaliśmy również dane. Zatem o co w ogóle chodziło? Brief z opisem problemu nakreślał nam szerszy kontekst: masowa elektryfikacja to absolutny kamień milowy transformacji energetycznej w Polsce. Jednak dla dystrybutorów prądu szybki wzrost liczby instalacji, w tym głównie pomp ciepła, stwarza ogromne wyzwania. Mówiąc krótko: precyzyjne prognozowanie zapotrzebowania na energię jest wręcz niezbędne, aby zapobiec przeciążeniom sieci i wynikającym z nich awariom.&lt;/p>
&lt;h2 id="dane-jakie-dostaliśmy">&lt;strong>Dane, jakie dostaliśmy&lt;/strong>
&lt;/h2>&lt;p>Jak mówimy o uczeniu maszynowym i predykcjach, to wstyd nie zacząć od opisu danych, jakie otrzymaliśmy, a więc by nie siać niezadowolenia, zacznijmy:&lt;/p>
&lt;p>Każdy zespół miał do dyspozycji 3 główne zbiory:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Train&lt;/strong>: Październik 2024 – Kwiecień 2025&lt;/li>
&lt;li>&lt;strong>Validation&lt;/strong>: Maj 2025 – Czerwiec 2025&lt;/li>
&lt;li>&lt;strong>Test&lt;/strong>: Lipiec 2025 – Październik 2025&lt;/li>
&lt;/ul>
&lt;p>Na tym ostatnim zbiorze wykonywaliśmy predykcje przy każdym submission, ale tutaj pojawia się haczyk, który decydował o wszystkim. To był mechanizm znany z Kaggle: Public vs Private Leaderboard. Zbiór Test był niby jawny i każdy go miał, ale&amp;hellip; brakowało w nim naszego „y” (celu predykcji). Nie było więc mowy o douczeniu modelu czy sprawdzeniu wyniku na własną rękę.&lt;/p>
&lt;p>Przez całe 24 godziny walczyliśmy „po omacku”, widząc na tablicy wyniki tylko dla wycinka tych danych. Jednak te punkty nie miały takiego znaczenia w końcowej klasyfikacji! Finalna ocena, która decydowała o podium, została przeliczona na pozostałej, całkowicie zatajonej części zbioru Test, której wyników nikt nie znał do samego końca. To sprawiło, że ostatnie minuty hackathonu to była czysta loteria emocjonalna, bo specyfika lata mogła być zgoła inna niż okresu jesienno-zimowego, na którym głównie trenowaliśmy.&lt;/p>
&lt;p>W praktyce ocenianie wyglądało tak:&lt;/p>
&lt;div style="border-left: 4px solid #59ff00; padding: 15px 5px; margin: 20px 0;">
&lt;table style="width: 100%; border-collapse: collapse; font-family: sans-serif; font-size: 1.4rem;">
&lt;thead>
&lt;tr style="border-bottom: 2px solid #555;">
&lt;th style="text-align: left; padding: 10px;">Score&lt;/th>
&lt;th style="text-align: left; padding: 10px;">Months used&lt;/th>
&lt;th style="text-align: left; padding: 10px;">Weights&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr style="border-bottom: 1px solid #ddd;">
&lt;td style="padding: 12px 10px;">&lt;strong>Leaderboard (visible)&lt;/strong>&lt;/td>
&lt;td style="padding: 12px 10px;">Validation only (May – Jun 2025)&lt;/td>
&lt;td style="padding: 12px 10px; color: #bbffd8;">-&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="padding: 12px 10px;">&lt;strong>Final score&lt;/strong>&lt;/td>
&lt;td style="padding: 12px 10px;">Validation + Test (May – Oct 2025)&lt;/td>
&lt;td style="padding: 12px 10px;">&lt;strong>2/6 valid&lt;/strong> + &lt;strong>4/6 test&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;p>Ostatecznie w danych mieliśmy ok. 600 różnych sensorów, które nadsyłały nam logi w odstępach 5-minutowych w przedstawionych powyżej okresach, co dawało nam ok. 64 miliony wierszy (10.42 GB!) do analizy.&lt;/p>
&lt;h2 id="cel">&lt;strong>Cel&lt;/strong>
&lt;/h2>&lt;p>Krótko i na temat: celem predykcji nie była chwilowa moc, a średnia miesięczna wartość wskaźnika obciążenia sieci (x2) dla każdego urządzenia. Przechodziliśmy więc z danych o wysokiej rozdzielczości (odczyty co 5 minut) na poziom agregatów miesięcznych. Na dole wrzucam dokładny i piękny wzór zawarty w opisie zadania:&lt;/p>
&lt;blockquote>
&lt;p>Dla każdego urządzenia &lt;strong>d&lt;/strong> i miesiąca prognozy &lt;strong>m&lt;/strong>, należy przewidzieć &lt;strong>średnią wartość x2&lt;/strong> ze wszystkich 5-minutowych odczytów w danym miesiącu:&lt;/p>
&lt;p align="center" style="font-size: 1.8rem; padding: 10px 10px 10px 4px; background: rgba(0,0,0,0.05); border-radius: 8px;">
&lt;b>target&lt;sub>d,m&lt;/sub> = (1 / N&lt;sub>d,m&lt;/sub>) * &amp;sum; x&lt;sub>2&lt;/sub>&lt;sup>(d,m,i)&lt;/sup>&lt;/b>
&lt;/p>
&lt;/blockquote>
&lt;p>A metryką oceny na &lt;em>live&lt;/em> oraz ostatecznym leaderboardzie było MAE:&lt;/p>
&lt;blockquote>
&lt;p align="center" style="font-size: 1.8rem; padding: 10px 10px 10px 4px; background: rgba(0,0,0,0.05); border-radius: 8px;">
&lt;b>MAE = (1 / n) * &amp;sum; | y&lt;sub>i&lt;/sub> - ŷ&lt;sub>i&lt;/sub> |&lt;/b>
&lt;/p>
&lt;/blockquote>
&lt;p>Także co, pora opisać nasze starania oraz drogę, która poprowadziła nas prościutko na 3 miejsce w całym hackathonie!&lt;/p>
&lt;h2 id="feature-engineering-oraz-preprocessing-danych">&lt;strong>Feature Engineering oraz Preprocessing danych&lt;/strong>
&lt;/h2>&lt;p>Na samym starcie trzeba przyjrzeć się blisko danym oraz rozkładom i tak też zrobiłem, ale jeszcze przed tym, na samym końcu instrukcji dostarczonej przez organizatorów, mogliśmy znaleźć taką oto sekcję:&lt;/p>
&lt;p>&lt;img src="https://blog.huszcza.dev/p/ensemble-ai-2026/dos.png"
width="1732"
height="1196"
srcset="https://blog.huszcza.dev/p/ensemble-ai-2026/dos_hub36c6c845528ee5e145621c676a1d04d_464433_480x0_resize_box_3.png 480w, https://blog.huszcza.dev/p/ensemble-ai-2026/dos_hub36c6c845528ee5e145621c676a1d04d_464433_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="Instrukcja z sekcją o DoS"
class="gallery-image"
data-flex-grow="144"
data-flex-basis="347px"
>&lt;/p>
&lt;p>W tamtym momencie pomyślałem, że koniecznie musimy od tego zacząć i dodać do każdego z sensorów informację, do jakiego dystrybutora energii należy. W końcu każdy team pewnie to zrobi, prawda? Prawda?? No finalnie okazało się, że nie :D i kto wie, może to nam dało te kilka punktów więcej?&lt;/p>
&lt;p>W danych mieliśmy takie informacje jak szerokość oraz długość geograficzna każdego sensora, a więc na tej podstawie postanowiłem zlokalizować każde urządzenie w konkretnym województwie, odpytując API GeoPy. Okazało się, że dane zostały zanonimizowane (?) albo były w nich błędy, bo niektóre lokalizacje były niepoprawnie umiejscowione i GeoPy nie mogło znaleźć odpowiedniego dopasowania. W takich wypadkach użyliśmy algorytmu KNN do znalezienia najbliższego urządzenia, które ma poprawne współrzędne. Później stworzona mapa przypisywała każde województwo do jednego z dystrybutorów energii takich jak PGE, Enea lub Tauron i tak oto mieliśmy pierwszy ciekawy feature.&lt;/p>
&lt;p>Kolejnym ważnym aspektem jest agregacja danych. Było ich naprawdę mnóstwo, co mogło przytłoczyć niejeden model, więc decyzja padła na agregację godzinową. Zmniejszało to całkiem znacznie zbiór danych, eliminowało szum z zapisów prowadzonych co 5 minut, dawało przestrzeń na wykrycie schematów, a także było wartościową jednostką predykcyjną.&lt;/p>
&lt;p>Ogólnie problem był dosyć ciekawy, bo na początku podchodziłem do tego zadania jak do predykcji szeregów czasowych. Jednak po głębszym zastanowieniu, tak naprawdę mamy tu &lt;strong>najzwyklejszy problem regresji&lt;/strong>. Wiadomo, interwały są prowadzone co 5 minut, ale predykcja to predykcja MIESIĘCZNA! Przy takim rozmyciu szczegółów na rzecz skali makro, jakby to powiedział mój profesor z politechniki: musimy ewidentnie użyć jak najbardziej precyzyjnej siekiery, a nie skalpela. Co więcej, w miarę uniwersalnej siekiery, która będzie umiała powiązać ważne cechy jesienią, po czym zaaplikować je również latem.&lt;/p>
&lt;h2 id="pierwsze-podejście">&lt;strong>Pierwsze podejście&lt;/strong>
&lt;/h2>&lt;p>Jako pierwsze podejście zdecydowałem się na CatBoosta. Było trochę cech kategorycznych oraz liczbowych, więc postanowiłem, że drzewa boostingowe mogą się całkiem dobrze odnaleźć w tym świecie. Także na start wleciał właśnie CatBoost z następującymi hiperparametrami (wtedy jeszcze bez strojenia):&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">CatBoostRegressor&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">iterations&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">800&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">learning_rate&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.05&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">depth&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">loss_function&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;MAE&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cat_features&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">CATEGORICAL_FEATURES&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">random_seed&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">verbose&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I jak to się mówi: benc! Siadło, a do tego grubo, bo nasz pierwszy model miał 0.0074 MAE. 0.0074!!! Kurczę, to naprawdę jest mało&amp;hellip; Szczególnie przy agregacji miesięcznej oraz przy takiej specyfice danych!&lt;/p>
&lt;p>&lt;img src="https://blog.huszcza.dev/p/ensemble-ai-2026/first_leaderboard.png"
width="1776"
height="796"
srcset="https://blog.huszcza.dev/p/ensemble-ai-2026/first_leaderboard_hucabe40d2662f153b762bb8379145bb15_408589_480x0_resize_box_3.png 480w, https://blog.huszcza.dev/p/ensemble-ai-2026/first_leaderboard_hucabe40d2662f153b762bb8379145bb15_408589_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="Pierwszy wynik na leaderboardzie"
class="gallery-image"
data-flex-grow="223"
data-flex-basis="535px"
>&lt;/p>
&lt;p>Po tym nastąpiła salwa kolejnych faz inżynierii cech, błądzenia i eksploracji zbioru. Summa summarum inne zespoły również dobrały się do podobnych wyników, a ostatecznie nas przeskoczyły, więc jako ostatni krok użyliśmy Optuny do optymzalizacji hiperparametrów, by wycisnąć z CatBoosta, ile się dało. Otrzymaliśmy wynik MAE na poziomie 0.0044. Każda z kolejnych wersji to naprawdę była ciężka walka i nadal uważam, że zejście na samym drzewku do takiej wartości to było naprawdę osiągnięcie. Tym bardziej, trochę spoilerując, że jednak Transformer to architektura znacznie, ale to znacznie cięższa, więc nawet trudno porównać te dwa modele między sobą, bo stoją one na dwóch różnych końcach efektywności oraz wymagań obliczeniowych. Także finalnie i tak mogę uznać ten wynik za naprawdę dobry jak na naszą wiedzę oraz umiejętności.&lt;/p>
&lt;h2 id="autoboty-do-boju">&lt;strong>Autoboty do boju&lt;/strong>
&lt;/h2>&lt;p>Kiedy porzuciliśmy nasze piękne drzewko? Po pierwsze wtedy, gdy naprawdę poczułem, że kolejne zmiany, próby oraz feature engineering nic nie zmieniają albo zmieniają na tyle mało, że nie jesteśmy w stanie skoczyć wyżej w rankingu. Po drugie: kiedy drużyna o nazwie &amp;ldquo;Transformers&amp;rdquo; nam nakopała, a tym samym, można powiedzieć, nas natchnęła&amp;hellip;
Po krótkim researchu postanowiłem wyciągnąć naprawdę, ale to naprawdę ciężkie działa, a mianowicie Feature Tokenizer Transformer (FT-Transfomrmer). Jest to, można powiedzieć, w miarę świeża architektura, która zdobywa ostatnio coraz większą popularność podczas kagglowych zawodów.&lt;/p>
&lt;p>&lt;img src="https://blog.huszcza.dev/p/ensemble-ai-2026/ja_i_transformer.png"
width="942"
height="716"
srcset="https://blog.huszcza.dev/p/ensemble-ai-2026/ja_i_transformer_hu705965a24e9c18d0f3b7477bc26c0691_991375_480x0_resize_box_3.png 480w, https://blog.huszcza.dev/p/ensemble-ai-2026/ja_i_transformer_hu705965a24e9c18d0f3b7477bc26c0691_991375_1024x0_resize_box_3.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="131"
data-flex-basis="315px"
>&lt;/p>
&lt;h3 id="ogólny-zamysł-i-sposób-działania-feature-tokenizer-transformera">Ogólny zamysł i sposób działania Feature Tokenizer Transformera
&lt;/h3>&lt;p>Zawarty poniżej opis opiera się na pracy, która właśnie &lt;a class="link" href="https://arxiv.org/abs/2106.11959" target="_blank" rel="noopener"
>FT-Transformera wprowadziła&lt;/a>. Oczywiście obrazki również pochodzą z tego samego źródła.&lt;/p>
&lt;p>Od początku. Jak wiemy w datasetach mamy głównie do czynienia z dwoma typami cech: nominalne, czyli takie jak kategorie, oraz numeryczne, przedstawiające konkretną wartość liczbową.&lt;/p>
&lt;p>Transformery zostały szeroko wykorzystane w przetwarzaniu języka naturalnego (NLP) w modelach generatywnych, takich jak GPT, czy koder-dekoder, takich jak T5. Jak więc zmusić naszą architekturę do przetwarzania tym razem nie konkretnych embeddingów stworzonych z tokenów, a właśnie kategorii i liczb jednocześnie?&lt;/p>
&lt;h3 id="główny-komponent-feature-tokenizer">Główny komponent: Feature Tokenizer
&lt;/h3>&lt;p>I właśnie za to odpowiada nasz komponent Feature Tokenizer. Jest on taką perełką tego podejścia, a działa na dwa konkretne sposoby:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Cechy numeryczne:&lt;/strong> Tutaj sprawa jest względnie prosta &amp;ndash;&amp;gt; bierzemy naszą liczbę, mnożymy ją przez wyuczony wektor wag o długości naszego wyjściowego embeddingu, dodajemy bias i tak właśnie nasza wartość liczbowa rozciągnęła się, tworząc nam embedding o zadanej wielkości.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Cechy kategoryczne:&lt;/strong> I tutaj działa to dosyć podobnie jak przetwarzanie słów w NLP. Każda wartość cechy na początku jest transformowana do reprezentacji &lt;em>one-hot encoding&lt;/em>, a następnie jest wymnażana przez macierz wag. Tak w skrócie matematycznie działa to po prostu jak wybieranie konkretnego wiersza z tej macierzy plus wiadomo bias.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;em>One-hot encoding&lt;/em> to zmiana reprezentacji danej wartości kategorycznej na ciąg binarny. Brzmi to dziwnie, ale jest naprawdę proste. Przykład: mamy cechę &amp;ldquo;Kolor&amp;rdquo; w datasecie motocykli. W naszym datasecie mamy dwa kolory – czerwony i czarny. Wrzucając to w wektor, możemy to zrobić tak: &lt;code>[Czerwony, Czarny]&lt;/code>, a więc na pierwszym miejscu mamy wartość czerwony, a na drugim wartość czarny. Reprezentacja &lt;em>one-hot encoding&lt;/em> to tak jakby zapalanie lampek, więc jeśli mielibyśmy przedstawić, że motocykl jest czerwony, to byłoby to tak: &lt;code>[1,0]&lt;/code>, a czarny to &lt;code>[0,1]&lt;/code>.&lt;/p>
&lt;/blockquote>
&lt;p>Następnie wszystkie wartości naszych cech są połączone za pomocą konkatenacji w wielką macierz &lt;strong>&lt;em>T&lt;/em>&lt;/strong>. Dodatkowo na samą górę doklejany jest losowo zainicjowany wektor &lt;code>[CLS]&lt;/code> o takiej samej długości. Dalej cała ta macierz jest przetwarzana i podana do naszego Transformera, tak więc &lt;strong>&lt;em>T&lt;/em>&lt;/strong> reprezentuje nam tak jakby jeden wiersz w naszej tabeli (oczywiście wliczając w to ten dodatkowy wektor &lt;code>[CLS]&lt;/code>). Na dole wizualizacja, jak to się prezentuje:&lt;/p>
&lt;p>&lt;img src="https://blog.huszcza.dev/p/ensemble-ai-2026/arch_ft_transformer.png"
width="1114"
height="528"
srcset="https://blog.huszcza.dev/p/ensemble-ai-2026/arch_ft_transformer_hud70f235597918efb8c1f944ac24428aa_94832_480x0_resize_box_3.png 480w, https://blog.huszcza.dev/p/ensemble-ai-2026/arch_ft_transformer_hud70f235597918efb8c1f944ac24428aa_94832_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="Architektura FT-Transformera"
class="gallery-image"
data-flex-grow="210"
data-flex-basis="506px"
>&lt;/p>
&lt;p>Ale po co ten &lt;code>[CLS]&lt;/code>? CLS to skrót od &lt;em>Classification&lt;/em>, a głównym zadaniem tego wektora jest zbieranie informacji podczas przejścia przez całą sieć ze wszystkich warstw.&lt;/p>
&lt;p>Dalej, jak widać, nasz wektor &lt;strong>&lt;em>T&lt;/em>&lt;/strong> z przetworzonymi cechami ląduje w Transformerze, przechodzi normalizację i następnie idzie do unitu &lt;em>Multi-Head Self-Attention&lt;/em>. Dzięki tej warstwie model może wyłonić kontekst, jaki jest potrzebny do osiągnięcia wyniku najbardziej zbliżonego do ideału, a w naszym przypadku kontekst to inne kolumny tabeli, czyli wartości z macierzy &lt;strong>&lt;em>T&lt;/em>&lt;/strong>. I właśnie ten kontekst, między innymi, składuje nam wektor &lt;code>[CLS]&lt;/code>.&lt;/p>
&lt;p>A dlaczego ta uwaga jest &lt;strong>„Multi-Head”&lt;/strong> ? Podobnie jak w modelach językowych jeden &lt;em>&amp;ldquo;head&amp;rdquo;&lt;/em> może wyłapywać z tekstu gramatykę, a inna emocje, tak tutaj każda z głów szuka w naszym wierszu danych zupełnie innego kontekstu. Dzięki temu w tym samym czasie jedna „głowa” może śledzić tylko twarde zależności geograficzne (np. obciążenia do województwa/operatora), inna szuka ukrytych powiązań technicznych (model pompy vs obciążenie), a nasz token &lt;code>[CLS]&lt;/code> dostaje na końcu pełny, wielowymiarowy obraz sytuacji zamiast jednej, uśrednionej papki.&lt;/p>
&lt;p>Na samym zaś końcu wyrzucamy wszystkie inne wiersze z macierzy &lt;strong>&lt;em>T&lt;/em>&lt;/strong> prócz naszego &lt;code>[CLS]&lt;/code>, który zawiera takie meritum czyli całą informację potrzebną do dalszego przetwarzania (w naszym zadaniu do przewidzenia konkretnego obciążenia) i dalej idzie to prosto do klasyfikacji/regresji.&lt;/p>
&lt;h2 id="zastosowanie-ft-transformera-w-naszym-zadaniu">Zastosowanie FT-Transformera w naszym zadaniu
&lt;/h2>&lt;h3 id="ostateczny-feature-engineering">Ostateczny Feature Engineering
&lt;/h3>&lt;p>W trakcie tych 24 godzin dużo testowałem z różnymi feature&amp;rsquo;ami, nieraz pytając LLMa, czy może on ma jakieś ciekawe pomysły. W sumie wylistuję tu to, co udało się dodać i co finalnie zostało wykorzystane do ostatecznego nauczenia naszego Transformera, ale też część z tych feature&amp;rsquo;ów została oczywiście użyta do wytrenowania CatBoosta.&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>deviceType&lt;/strong>, czyli typ urządzenia, który pomaga modelowi uchwycić różnice w charakterystyce pracy.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>x3&lt;/strong> to dodatkowa cecha kategoryczna z danych wejściowych, która wnosi informację o typie krzywej grzewczej.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>operator&lt;/strong>, a mianowicie nazwa operatora dostawcy, pozwalająca modelowi uwzględnić różnice wynikające z warunków eksploatacji oraz polityk działania.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>voivodeship&lt;/strong> to województwo, czyli kontekst geograficzny wpływający między innymi na klimat oraz sezonowość zachowania systemu.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>device_operator_combo&lt;/strong>, czyli połączenie urządzenia oraz operatora, które pozwala łapać interakcje specyficzne dla konkretnej pary.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>t1_mean-t13_mean&lt;/strong> oznacza średnią wartość sygnału t1-t13 w oknie czasu opisującą jego typowy poziom.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>t8_max&lt;/strong> wyznacza maksymalną wartość t8 opisującą skrajne piki oraz epizody wysokiego obciążenia.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>t8_std&lt;/strong> to odchylenie standardowe t8 mierzące zmienność sygnału.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>t7_max&lt;/strong> oznacza maksimum t7, które wskazuje na chwilowe ekstremalne stany systemu.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>t4_min&lt;/strong> to minimum t4 przydatne do wykrywania głębokich spadków.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>delta_load&lt;/strong> jest zmianą obciążenia między punktami czasowymi pokazującą dynamikę pracy układu.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>delta_source&lt;/strong> wyznacza zmianę po stronie źródła, która może odzwierciedlać przełączenia lub skoki warunków zasilania.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>cwu_demand&lt;/strong> to zapotrzebowanie na CWU, czyli sygnał popytu wpływający bezpośrednio na pracę systemu.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>delta_temp_out_in&lt;/strong> oznacza różnicę temperatury wyjścia oraz wejścia opisującą transfer energii a także efektywność procesu.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>cwu_spike&lt;/strong> jest flagą nagłego wzrostu zapotrzebowania CWU pomocną przy modelowaniu krótkich i gwałtownych zdarzeń.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>hour_sin&lt;/strong> to sinus z godziny doby, który koduje cykliczność czasu bez sztucznego przeskoku między godziną 23:00 a 00:00.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>hour_cos&lt;/strong> stanowi cosinus z godziny doby uzupełniający powyższy sinus i pozwalający modelowi odtworzyć pełną fazę dobową.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>month_sin&lt;/strong> jest sinusem z miesiąca reprezentującym sezonowość roczną w sposób ciągły.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>month_cos&lt;/strong> to cosinus z miesiąca, który razem z sinusem miesiąca domyka cykliczną reprezentację pór roku.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="co-pod-maską-sieć-głowica-i-hiperparametry">Co pod maską? Sieć, głowica i hiperparametry
&lt;/h3>&lt;p>Teoria teorią, ale teraz pora przejść do tego, jak my te Transformerowe klocki zaadaptowaliśmy do naszego datasetu.&lt;/p>
&lt;p>Teoretycznie wspominałem, że liczby są prosto wymnażane przez wektor wag. Jednakże my poszliśmy o krok dalej, a co za tym idzie każda cecha numeryczna była przetwarzana jeszcze przed samym wejściem do Transformera przez małą sieć neuronową, a mianowicie MLP (Multi Layer Perceptron):&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">nn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Sequential&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Linear&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">embed_dim&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ReLU&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Linear&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">embed_dim&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">embed_dim&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Zrobiliśmy to, bo nie wszystkie cechy mogą wpływać liniowo na wynik, dlatego dorzuciliśmy trochę tej nieliniowości jeszcze przed samym wejściem do Transformera.&lt;/p>
&lt;p>Cechy kategoryczne były standardowo zamieniane na embeddingi zgodnie z poprzednim opisem. Jedyne co, to dodaliśmy też miejsce na OOV, czyli Out of Vocabulary, w razie gdyby na przykład konkretny operator czy deviceType był nieznany.
To, co dalej się dzieje, to klasyczny Feature Tokenizer Transformer opisany wcześniej. Jeśli chodzi o hiperparametry, to zastosowaliśmy:&lt;/p>
&lt;ul>
&lt;li>Embedding size: 64&lt;/li>
&lt;li>Multi head attentions: 8&lt;/li>
&lt;li>Transformer layers: 3&lt;/li>
&lt;li>Dropout: 0.1&lt;/li>
&lt;/ul>
&lt;p>Po tym, jak nasze dane przejdą przez wszystkie warstwy Transformera, dochodzimy do finału, czyli tzw. głowicy regresyjnej. Tutaj sprawa jest prosta: wyciągamy z całej macierzy tylko ten jeden, konkretny wektor [CLS], o którym pisałem wcześniej. Dlaczego akurat jego? Bo dzięki mechanizmowi atencji to właśnie on &amp;ldquo;nasiąkł&amp;rdquo; informacjami ze wszystkich pozostałych kolumn i ma w sobie skondensowaną wiedzę o całym wierszu danych.&lt;/p>
&lt;p>Resztę wektorów (tych odpowiadających za np. region) po prostu odcinamy, bo wykonały już swoją robotę. Nasz [CLS] trafia do ostatniej, malutkiej sieci neuronowej składającej się z warstwy normalizacji i aktywacji ReLU, która ostatecznie &amp;ldquo;zgniata&amp;rdquo; te wszystkie skomplikowane liczby do jednej, finalnej wartości.&lt;/p>
&lt;p>Na samym końcu dorzuciliśmy jeszcze twardy bezpiecznik. Skoro przewidujemy obciążenie energii, to ujemny wynik fizycznie nie ma sensu, więc ucięliśmy wszystkie wartości poniżej zera, pilnując, żeby model nie wypluwał bzdur.&lt;/p>
&lt;h3 id="faza-treningu">Faza treningu
&lt;/h3>&lt;p>Kilka słów o tym, jak w ogóle podeszliśmy do uczenia naszego modelu. Starałem się to zrobić najbardziej optymalnie, by nie trenować bez sensu naszego Transformera oraz nie marnować tak ważnego na hackathonie czasu. Mieliśmy dwie główne fazy:&lt;/p>
&lt;p>&lt;strong>Faza 1, czyli taki poligon doświadczalny&lt;/strong> Zamiast trenować na wszystkim, zrobiłem twarde cięcie w czasie na początku lutego. Model uczył się na danych sprzed tej daty, a następnie miał przewidywać przyszłość, czyli to, co działo się po 1 lutego. Dlaczego podział po dacie, a nie losowy? Bo w przypadku obciążenia sieci elektrycznej losowy podział spowodowałby wyciek danych, czyli model widziałby &amp;ldquo;przyszłość&amp;rdquo;, żeby przewidzieć &amp;ldquo;przeszłość&amp;rdquo;. W tej fazie dorzuciliśmy też Early Stopping, by model przerywał naukę, gdy przestanie się poprawiać. Oczywiście zapisywaliśmy wszystkie checkpointy. Dzięki tej fazie wiedzieliśmy, jakie jest nasze realne MAE, zanim w ogóle wysłaliśmy cokolwiek do organizatorów.&lt;/p>
&lt;p>&lt;strong>Faza 2, czyli cała naprzód&lt;/strong> Gdy po wielu testach w Fazie 1 upewniliśmy się, że nasza architektura działa stabilnie, to przeszliśmy właśnie do Fazy 2 &amp;ndash;&amp;gt; &lt;strong>więcej danych = lepszy model&lt;/strong>. Na sam koniec zdjęliśmy blokadę z 1 lutego i wrzuciliśmy do pieca absolutnie wszystkie dostępne dane treningowe z przeszłości. Tak potężnie nafeedowany oraz wyregulowany model wygenerował ostateczne predykcje, które trafiły do naszego finałowego pliku &lt;em>submission&lt;/em>.&lt;/p>
&lt;h3 id="mały-tip-na-sam-koniec">Mały tip na sam koniec
&lt;/h3>&lt;p>Warto jeszcze wspomnieć, że sam Transformer uczył się przeskalowanej wartości wskaźnika x2, zrealizowanej za pomocą StandardScalera. Sieci neuronowe lubią na ogół normalizację, więc to też mogło dołożyć swoją cegiełkę do stabilniejszego i bardziej efektywnego uczenia naszego FT-Transformera. Przed samym zapisaniem przewidzianej wartości do pliku wynikowego była ona w odpowiedni sposób przeskalowana do oryginalnego przedziału wartości.&lt;/p>
&lt;h3 id="triumf-transformera">Triumf Transformera
&lt;/h3>&lt;p>Kiedy opadł kurz, siedzieliśmy sobie w stołówce, zajadając pyszny obiad rodem z tych u babci. Byliśmy już trochę pogodzeni z myślą, że na top 10 nie mamy co liczyć. Ale wiadomo ciekawość to pierwszy stopień do&amp;hellip; sprawdzenia wyników. Zagryzając kotleta, postanowiłem zerknąć na Final Score.&lt;/p>
&lt;p>A tu okazuje się, że nasza siekiera była nie tylko precyzyjna, ale i rozbiła bank. Z wynikiem &lt;strong>0.008158&lt;/strong> wyprzedziliśmy drugie miejsce o &lt;strong>niemal 100%&lt;/strong> (ich MAE było prawie dwukrotnie wyższe!). To był ten moment, w którym loteria emocjonalna zamieniła się w falę euforii. Jednak przeżuwanie kotleta musiało gwałtownie nabrać tempa &amp;ndash;&amp;gt; trzeba było biec dopracować &lt;strong>pitcha&lt;/strong>.&lt;/p>
&lt;p>P.S. W temacie obiadu: organizatorzy, jeśli to czytacie – ten posiłek na każdej edycji jest jak &lt;strong>dar od bogów&lt;/strong>. Nie zmieniajcie tego! Działa on jak balsam na żołądek zmaltretowany toną pizzy i litrami energetyków.
&lt;img src="https://blog.huszcza.dev/p/ensemble-ai-2026/leaderboard_task_3.png"
width="606"
height="598"
srcset="https://blog.huszcza.dev/p/ensemble-ai-2026/leaderboard_task_3_hu7d4fa0308172fdb5b7f873a3a5d5d218_207247_480x0_resize_box_3.png 480w, https://blog.huszcza.dev/p/ensemble-ai-2026/leaderboard_task_3_hu7d4fa0308172fdb5b7f873a3a5d5d218_207247_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="Wynik końcowy na leaderboardzie zadania 3"
class="gallery-image"
data-flex-grow="101"
data-flex-basis="243px"
>&lt;/p>
&lt;h2 id="epilog">Epilog
&lt;/h2>&lt;p>Zatem, czemu to mogło zadziałać, a nawet teraz już można powiedzieć, że &lt;strong>zadziałało&lt;/strong>?
Po pierwsze, zderzyliśmy się z charakterystyką drzew decyzyjnych: nie są stworzone do problemów ekstrapolacji. Każde drzewo tworzy sztywne podziały, których uczy się w trakcie treningu. Ale co, jeśli latem model zobaczy wartości zupełnie spoza zbioru treningowego? Z tym problemem znacznie lepiej radzą sobie architektury typu Transformer, które uczą się ciągłych relacji i nie są ograniczone sztywnymi ramami.&lt;/p>
&lt;p>Po drugie cóż, wiadomo, że ciężko powiedzieć coś na 100%, bo jednak tak duże oraz złożone sieci neuronowe to taka czarna skrzynka. Na pewno każda z wymienionych wcześniej praktyk kształtowała po trochu końcowy wynik. Jednak gdybym miał już coś wytypować, co mogło mieć większy wpływ, to położyłbym nacisk na ten sławetny mechanizm &lt;em>Multi-Head Self-Attention&lt;/em>.&lt;/p>
&lt;p>Głównym problemem oraz wyzwaniem w tych danych było wyciągnięcie uniwersalnej wiedzy z miesięcy jesienno-zimowych, kiedy pompa ciepła zazwyczaj działa na pełnych obrotach i przeniesienie jej na letnie obciążenie, kiedy to wykorzystanie pomp jest znacznie mniejsze. W FT-Transformerze mechanizm kontekstu mógł modelować, jak mocno dane cechy mają wpływ na wynik oraz jak bardzo konkretne atrybuty powinny być brane pod uwagę w szczególnych przypadkach. Dodatkowo jeszcze nasz nieliniowy MLP, który przetwarzał nasze wartości numeryczne, też mógł wzbogacić te cechy i nadać im konkretny wpływ na wynik. Jak wiemy, Transformery nieźle generalizują i wydaje mi się, że to właśnie ta cecha zagrała pierwsze skrzypce w tym zadaniu.&lt;/p>
&lt;p>Niemniej jednak trzeba oddać honory innym drużynom, które były tuż pod nami. Mimo iż druga drużyna miała wynik gorszy od naszego (niemal dwukrotnie!!), to chyba jako jedyni wyciągnęliśmy tak ciężkie działo jak Transformer do tego zadania. Inne drużyny korzystały z drzew regresyjnych takich jak LightGBM i biorąc pod uwagę różnicę w skomplikowaniu naszej oraz ich architektury, to wykonali oni naprawdę świetną robotę. Niemniej jednak to nam udało się wyjść na prowadzenie i z naszego rozwiązania możemy być dumni!&lt;/p>
&lt;h2 id="to-co-za-rok">To co&amp;hellip; za rok?
&lt;/h2>&lt;p>Kolejny EnsembleAI i kolejny raz świetnie się na nim bawiłem. Wielkie dzięki dla organizatorów za tak świetny event oraz dla mojej drużyny DNS, czyli Drużyny Nieobecnego Szymona, w składzie:&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://www.linkedin.com/in/jhudziak/" target="_blank" rel="noopener"
>Jakub Hudziak&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.linkedin.com/in/jakub-binkowski-80136825b/" target="_blank" rel="noopener"
>Jakub Binkowski&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.linkedin.com/in/maciej-kaszkowiak/" target="_blank" rel="noopener"
>Maciej Kaszkowiak&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.linkedin.com/in/maciej-mazur-90064b2b4/" target="_blank" rel="noopener"
>Maciej Mazur&lt;/a>&lt;/li>
&lt;li>oraz oczywiście ja :D&lt;/li>
&lt;/ul>
&lt;p>Daliśmy ognia chłopaki i mam nadzieję, że nie po raz ostatni! Chyba się już powtarzam, jednak mówię to za każdym razem szczerze. To co, do zobaczenia za rok?&lt;/p></description></item></channel></rss>