<?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/categories/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/categories/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><item><title>EnsembleAI 2025</title><link>https://blog.huszcza.dev/pl/p/ensemble-ai-2025/</link><pubDate>Tue, 18 Mar 2025 16:00:00 +0000</pubDate><guid>https://blog.huszcza.dev/pl/p/ensemble-ai-2025/</guid><description>&lt;img src="https://blog.huszcza.dev/p/cover.png" alt="Featured image of post EnsembleAI 2025" />&lt;p>W dniach &lt;strong>14-16.03.25&lt;/strong> razem z zespołem mieliśmy okazję wziąć udział w hackathonie w Krakowie. Uczestniczyliśmy już w poprzedniej edycji, więc jeśli chcecie dowiedzieć się więcej o tym wydarzeniu, serdecznie zapraszam do lektury wpisu &lt;a class="link" href="https://kaszkowiak.org/blog/ensemble-ai/" target="_blank" rel="noopener"
>Macieja&lt;/a>. &lt;br>
W skrócie – było to nasze pierwsze starcie z tworzeniem sieci neuronowych. Co więcej, uczyliśmy się wszystkiego na bieżąco, więc wyzwanie było naprawdę spore. Mimo wszystko udało nam się zająć &lt;strong>12 miejsce&lt;/strong>, co było dla nas duużym osiągnięciem!
W tym roku poszło nam jeszcze lepiej – udało nam się wywalczyć 3 miejsce!&lt;/p>
&lt;h2 id="prolog---przeddzień-walk">PROLOG - przeddzień walk
&lt;/h2>&lt;p>Założenie na ten hackathon mieliśmy proste: wiemy już więcej o AI i jeśli przyjdzie nam zmierzyć się z pisaniem sieci neuronowej, to jesteśmy na pewno lepiej przygotowani niż poprzednim razem.&lt;br>
Wspólnie z &lt;strong>Jakubem Binkowskim&lt;/strong> mieliśmy również okazję przejść przedmiot &lt;strong>Sieci neuronowe&lt;/strong>, co dało nam znacznie szerszy pogląd na przetwarzanie obrazów, techniki regularyzacji i sposoby zapobiegania przeuczenia modeli.&lt;br>
W piątek postanowiliśmy też zapoznać się z materiałami przesłanymi przez organizatorów. Mimo że nie był to dla nas łatwy materiał, udało nam się przebrnąć przez kilka z nich i wyciągnąć coś wartościowego. Jak okazało się później, mogliśmy się po prostu porządnie zrelaksować&amp;hellip;&lt;/p>
&lt;h2 id="początek---intuicja-ponad-wszystko">Początek - intuicja ponad wszystko
&lt;/h2>&lt;p>A więc lądujemy w sali na wystąpieniach otwierających i dostajemy kilka kluczowych informacji.&lt;/p>
&lt;p>Po pierwsze, dzięki współpracy organizatorów z &lt;strong>Cyfronetem&lt;/strong>, każdy zespół miał do dyspozycji naprawdę potężne maszyny – i tutaj poczułem, że to solidnie przyspieszy naukę naszych modeli. Liczby i moc obliczeniowa zrobiły ogromne wrażenie, więc kolejny raz mogę zbić piątkę z organizatorami za świetny pomysł.&lt;br>
Następnie dowiedzieliśmy się, jakie będą zadania, i tutaj spotkało nas lekkie zaskoczenie. Okazało się, że każdy zespół musi wybrać jedną z dwóch ścieżek:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Pierwsza&lt;/strong> – składała się z czterech podzadań dotyczących &lt;strong>uczenia adwersarzowego, kradzieży modeli&lt;/strong> i kilku innych tematów związanych z szeroko pojętym uczeniem maszynowym.&lt;/li>
&lt;li>&lt;strong>Druga&lt;/strong> – zadanie polegało na nauce bota do gry stworzonej przez organizatorów. Boty miały grać między sobą, a zasady rankingu były proste:
&lt;ul>
&lt;li>za remis drużyna dostaje &lt;strong>0.5 pkt&lt;/strong>,&lt;/li>
&lt;li>za wygraną &lt;strong>1 pkt&lt;/strong>,&lt;/li>
&lt;li>za przegraną – wiadomo&amp;hellip; &lt;strong>smutne i okrągłe 0 pkt&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>Wow, nie wiedzieliśmy, że tak szybko dopadnie nas widmo decyzji, ale na szczęście mieliśmy na nią ponad &lt;strong>8 godzin&lt;/strong>, więc był czas na testy i ocenę, w którym zadaniu będziemy czuć się najlepiej.
Po wykładach chwilę się rozluźniliśmy, podeszliśmy do kilku stanowisk – między innymi do &lt;strong>JetBrainsów&lt;/strong>. Razem z Szymonem jesteśmy fanami ich IDE, więc była to naprawdę fajna gratka. Także od dzisiaj oficjalnie staję się ich chodzącą reklamą, ale co tam – naprawdę doceniam jakość ich rozwiązań. 😎&lt;/p>
&lt;p>&lt;img src="https://blog.huszcza.dev/p/jetbrains_badges.jpg"
width="300"
height="400"
srcset="https://blog.huszcza.dev/p/jetbrains_badges_hu536186f9f2e0e720ed47eed2cd530a33_51008_480x0_resize_q75_box.jpg 480w, https://blog.huszcza.dev/p/jetbrains_badges_hu536186f9f2e0e720ed47eed2cd530a33_51008_1024x0_resize_q75_box.jpg 1024w"
loading="lazy"
alt="Od dzisiaj jestem chodzącą reklamą"
class="gallery-image"
data-flex-grow="75"
data-flex-basis="180px"
>&lt;/p>
&lt;h2 id="3-2-1-start---wyścig-rozpoczęty">3, 2, 1, start! - wyścig rozpoczęty!
&lt;/h2>&lt;p>Na początku, przez lekkie niezrozumienie zasad, postanowiliśmy jak najszybciej wypuścić &lt;strong>pierwszą wersję bota&lt;/strong> – byliśmy przekonani, że &lt;strong>rating&lt;/strong> będzie naliczany co godzinę, a finalny ranking będzie sumą poszczególnych pojedynków z trawania całego hackathonu.&lt;br>
Wypuściliśmy więc coś prostego, ale obrazowo nazwanego – &lt;strong>ICBM&lt;/strong>. Rakieta leciała &lt;strong>prosto z naszej bazy do bazy przeciwnika&lt;/strong>. Gdy tylko mieliśmy zasoby, odpalaliśmy kolejną rakietę.&lt;br>
I, ku naszemu zaskoczeniu, takie rozwiązanie dało &lt;strong>całkiem niezłe efekty&lt;/strong> na samym początku – &lt;strong>na tyle dobre&lt;/strong>, że poczuliśmy wiatr w żaglach i postanowiliśmy w pełni skupić się na tym zadaniu.&lt;/p>
&lt;h2 id="octospace--zasady-gry-w-pigułce-">&lt;strong>OctoSpace – zasady gry w pigułce&lt;/strong> 🚀
&lt;/h2>&lt;p>W grze celem jest całkowita dominacja. Można wygrać na dwa sposoby – eliminując przeciwnika poprzez przejęcie jego planety lub zdobywając więcej punktów po 1000 turach (w końcowym etapie organizatorzy zwiększyli tury do 2000, bo było za dużo remisów). Plansza o wymiarach 100x100 jest początkowo pokryta mgłą, a eksploracja statkami odkrywa jej kolejne obszary.&lt;br>
Statki pozwalają walczyć i zdobywać planety. Standardowa prędkość ruchu to 1 lub 2 pola na turę, jednak wchodząc w spowalniającą mgłę, statek porusza się trzykrotnie wolniej, a zderzając się z asteroidą, otrzymuje obrażenia. Statki mogą atakować na dystans do 8 pól, ale po każdym strzale z założenia miało następować 10-tur cooldownu, jednak odkryliśmy, że finalnie to nie było żadnego cooldown-u :D. Leczenie jednostek jest możliwe tylko na własnych planetach, gdzie odzyskują 1 HP na turę.&lt;br>
Aby powiększać flotę, potrzebujemy surowców, które zdobywamy kontrolując planety. Każda planeta składa się z 16 pól zasobów, a ilość pozyskanych surowców zależy od ich rodzaju i liczby kontrolowanych pól. Początkowa planeta ma wszystkie zasoby w równych proporcjach, ale inne planety mogą mieć ich nierównomierny podział.&lt;br>
Zdobywanie planet odbywa się poprzez wpadnięcie statku na planetę. Jeśli planeta jest nieprzejęta, przejmujemy ją natychmiast. W przypadku zajętej planety rozpoczyna się proces przejęcia, który trwa kilka tur. Jeśli planeta była w trakcie przejmowania przez przeciwnika, można zneutralizować jego postępy i odzyskać kontrolę.&lt;/p>
&lt;p>&lt;img src="https://blog.huszcza.dev/p/map_with_labels.png"
width="1662"
height="1188"
srcset="https://blog.huszcza.dev/p/map_with_labels_hu314875a1bbde97d68449a876eda79a2f_1709055_480x0_resize_box_3.png 480w, https://blog.huszcza.dev/p/map_with_labels_hu314875a1bbde97d68449a876eda79a2f_1709055_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="Mapa z oznaczeniami głównych obiektów"
class="gallery-image"
data-flex-grow="139"
data-flex-basis="335px"
>&lt;/p>
&lt;h2 id="nasz-plan">Nasz plan
&lt;/h2>&lt;p>Nie mieliśmy złudzeń – żaden z nas &lt;strong>nigdy&lt;/strong> wcześniej nie tworzył modelu opartego na Reinforcement Learningu. Jedyne, co mieliśmy na ten temat, to zajęcia sprzed tygodnia, na których razem z &lt;strong>Jakubem&lt;/strong> poznaliśmy podstawowe problemy i założenia RL. &lt;br>
Nie porzuciliśmy od razu wizji stworzenia czegoś więcej niż heurystyk, ale od nich chcieliśmy zacząć. Wiedzieliśmy, że i tak będą nam potrzebne – choćby po to, żeby w razie czego móc trenować model przeciwko różnym strategiom.&lt;br>
Jednakże&amp;hellip; &lt;strong>spoiler alert&lt;/strong> – dowiedzieliśmy się, że na maszynie testowej nie można korzystać z bardziej &lt;strong>wysokopoziomowych narzędzi do RL&lt;/strong>. Możemy używać &lt;strong>tylko czystego PyTorcha&lt;/strong> (only sad reactions).&lt;br>
I tak oto przybiliśmy gwóźdź do trumny i zamiast walczyć z ograniczeniami, postanowiliśmy przerzucić wszystkie siły na stworzenie najmocniejszej heurystyki, na jaką nas stać.&lt;/p>
&lt;h2 id="od-rakiety-do-floty---nasze-pomysły">Od rakiety do floty - nasze pomysły
&lt;/h2>&lt;p>Na starcie nasza &lt;strong>heurystyka&lt;/strong> zawierała tylko jeden typ statku – &lt;strong>ICBM&lt;/strong>. Jego zadaniem było czyste bombardowanie bazy przeciwnika. Proste, ale skuteczne – nawet bardziej niż skuteczne, bo przez pierwsze &lt;strong>12&lt;/strong> godzin hackathonu zajmowaliśmy &lt;strong>pierwsze miejsce w tabeli&lt;/strong>XD. To było dla nas spore zaskoczenie.&lt;/p>
&lt;h3 id="icbmv2---pierwsze-ulepszenie">ICBMv2 - pierwsze ulepszenie
&lt;/h3>&lt;p>Kolejnym krokiem była wersja &lt;strong>ICBMv2&lt;/strong>, która miała zaimplementowany &lt;strong>pathfinding&lt;/strong>, czyli lokalne znajdowanie najlepszej ścieżki, omijającej asteroidy i mgły spowalniające. W strategii bombardowania bazy przeciwnika było to kluczowe, bo każde spowolnienie naszej rakiety działało na naszą niekorzyść.&lt;/p>
&lt;h3 id="defender---pierwsza-linia-obrony">Defender - pierwsza linia obrony
&lt;/h3>&lt;p>&lt;strong>Defender&lt;/strong> to nasza ostoja i pierwsza pomoc w systemie bezpieczeństwa. Jego zadanie było następujące:&lt;/p>
&lt;p>Ustawić się pod odpowiednim kątem i razem z drugim defenderem tworzyć ogień krzyżowy, pokrywający większość ataków na naszą bazę.&lt;br>
W razie kryzysu pełnił funkcję &lt;strong>pierwszej pomocy&lt;/strong> – jeśli baza była atakowana, a nie mieliśmy wystarczających zasobów na nowy statek (stworzenie nowego statku w trakcie przejmowania zatrzymywało przejmowanie), defender leciał na ratunek i odbijał bazę.&lt;/p>
&lt;h3 id="explorer---zwiad-i-zbieranie-zasobów">Explorer - zwiad i zbieranie zasobów
&lt;/h3>&lt;p>Wiedzieliśmy, że zmasowany atak bez zasobów szybko się skończy, więc potrzebowaliśmy kogoś, kto nie tylko zdobywa surowce, ale i odkrywa mapę.&lt;/p>
&lt;p>&lt;strong>Explorer&lt;/strong> korzystał z pathfindingu, ale&amp;hellip; nie udało nam się stworzyć idealnego algorytmu szukania ścieżki, więc zdarzało się, że się blokował. W finalnej wersji, jeśli utknął, zmieniał się w &lt;strong>rakietę&lt;/strong>, dodatkowo zmieniając standardowy wektor ataku naszej strategii (standardowym wektorem była główna przekątna mapy).
Btw. Jak widzę nagłą przemianę explorera w rakietę to mam przed oczami ten obraz:&lt;/p>
&lt;p>&lt;img src="https://blog.huszcza.dev/p/anakin.gif"
width="640"
height="268"
srcset="https://blog.huszcza.dev/p/anakin_hu13edf962f071b6f2a8a33486463dbeb0_67332_480x0_resize_box_1.gif 480w, https://blog.huszcza.dev/p/anakin_hu13edf962f071b6f2a8a33486463dbeb0_67332_1024x0_resize_box_1.gif 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="238"
data-flex-basis="573px"
>&lt;/p>
&lt;h3 id="backdoor---cichy-zabójca">Backdoor - cichy zabójca
&lt;/h3>&lt;p>Analizując naszą taktykę, zauważyliśmy, że jest wrażliwa na ataki z granic mapy. Idąc tym tropem, doszliśmy do wniosku, że przeciwnicy też mogą nie zabezpieczać się wystarczająco.&lt;br>
Tak powstał &lt;strong>Backdoor&lt;/strong> – cichy zabójca, który po dotarciu za bazę przeciwnika i odczekaniu odpowiedniego czasu niespodziewanie ją atakował.&lt;br>
W finalnym rozrachunku nie okazał się wystarczająco efektywny, by wprowadzić go do naszej strategii, a czas nie pozwalał nam również dokładnie przetestować jego wpływu. &lt;br>
Jednakże jego sposób działania był na tyle komiczny, że muszę to pokazać XD.&lt;/p>
&lt;div class="video-wrapper">
&lt;video
controls
src="https://blog.huszcza.dev/p/backdoor_attack.mp4"
>
&lt;p>
Your browser doesn't support HTML5 video. Here is a
&lt;a href="https://blog.huszcza.dev/p/backdoor_attack.mp4">link to the video&lt;/a> instead.
&lt;/p>
&lt;/video>
&lt;/div>
&lt;p>Na nagraniu widać jak zielone backdoory gracza pierwszego skradają się na krańcu mapy, odczekują chwilę, a następnie razem atakują. Na przekątnej natomiast widać różowe statki gracza drugiego, czyli wersje ICBM naszych statków.&lt;/p>
&lt;h3 id="smażenie-wrogów---dodatkowy-atut-rakiet">Smażenie wrogów - dodatkowy atut rakiet
&lt;/h3>&lt;p>Do tej pory nasze boty nie wykorzystywały strzelania, ale na końcowym etapie hackathonu je dodaliśmy – i to działało naprawdę dobrze.&lt;br>
Każdy napotkany przeciwnik, póki był w naszym zasięgu, był kolokwialnie mówiąc &lt;strong>grillowany&lt;/strong> 🔥. Jednak w naszej głównej taktyce, gdzie statki nie były statkami, a latającymi rakietami planeta → planeta, ta funkcjonalność niestety spowalniała je, co wpływało negatywnie na ich efektywność.&lt;/p>
&lt;hr>
&lt;h2 id="pomysły-na-które-nie-starczyło-nam-czasu">Pomysły, na które nie starczyło nam czasu
&lt;/h2>&lt;h3 id="wariant-szymona---stackowanie-rakiet-i-zmasowany-atak">Wariant Szymona - stackowanie rakiet i zmasowany atak
&lt;/h3>&lt;p>Plan był prosty – gromadzimy rakiety, a potem wysyłamy je w jednym, &lt;strong>miażdżącym ataku&lt;/strong>.&lt;br>
Brzmi jak coś, co naprawdę mogłoby zadziałać, ale&amp;hellip; niestety nie starczyło nam czasu, żeby to przetestować :(&lt;/p>
&lt;p>&lt;img src="https://blog.huszcza.dev/p/fleet_icbm.jpg"
width="1920"
height="804"
srcset="https://blog.huszcza.dev/p/fleet_icbm_hu02aa61ae1d9de35cbe238d2676170cb9_304133_480x0_resize_q75_box.jpg 480w, https://blog.huszcza.dev/p/fleet_icbm_hu02aa61ae1d9de35cbe238d2676170cb9_304133_1024x0_resize_q75_box.jpg 1024w"
loading="lazy"
alt="Widok z perspektywy bazy przeciwnika gdybyśmy wcielili plan w życie"
class="gallery-image"
data-flex-grow="238"
data-flex-basis="573px"
>&lt;/p>
&lt;h2 id="kilka-usprawnień-w-procesie-tworzenia-bota">Kilka usprawnień w procesie tworzenia Bota
&lt;/h2>&lt;p>Warto zaznaczyć, że &lt;strong>nie samym kodem nasze rozwiązanie żyło&lt;/strong>.&lt;br>
Ponieważ przesyłany bot musiał zmieścić się w jednym pliku, a nasza struktura zawierała ich wiele, w tym wspólne funkcje dla różnych modułów, stworzyliśmy skrypt, który łączył plik &lt;code>utils.py&lt;/code> z naszą wersją agenta (bota).&lt;br>
Każdy agent miał swój własny folder i moduł. Proste, ale jakże skuteczne. &lt;br>Żeby porównać dwa rozwiązania, wystarczyło w skrypcie podać nazwy folderów, w których znajdowali się nasi agenci i&amp;hellip; &lt;strong>cyk!&lt;/strong>. Dwa boty rozpoczynały batalię między sobą – za jednym kliknięciem Entera. Brzmi to bardzo prosto, ale uwierzcie mi, że sam pewnie nabiłem ponad sto odpaleń tego skryptu, a przy takiej liczbie każde dodatkowe kliknięcie ma znaczenie.&lt;/p>
&lt;h3 id="cicd---automatyzacja-wykrywająca-błędy">CI/CD - automatyzacja wykrywająca błędy
&lt;/h3>&lt;p>W trakcie hackathonu pojawiał się nie jeden błąd w grze (kodzie źródłowym), a organizatorzy naprawiali je na bieżąco. Wpływało to na kompatybilność naszych botów, więc dodanie automatyzacji pozwoliło nam sprawdzać, czy nasze konfiguracje nadal działają zgodnie z nową wersją gry.
Dodatkowo mieliśmy w planach równoległe i automatyczne testowanie nowych botów, ale&amp;hellip; po raz kolejny nie starczyło nam na to czasu.&lt;/p>
&lt;h2 id="finał---triumf-prostoty">Finał - triumf prostoty
&lt;/h2>&lt;p>Nasza &lt;strong>finalna heurystyka&lt;/strong> prezentowała się następująco:&lt;/p>
&lt;h3 id="-rdzeń-rakiety-icbm">🔥 Rdzeń: rakiety ICBM
&lt;/h3>&lt;p>Jeśli defenderzy są na miejscu i nie wypada tura na tworzenie Explorera, to &lt;strong>bombardujemy przeciwnika ICBMv2&lt;/strong>. Dodatkowo na samym początku gry wypuszczamy dwie wersje rakiety v1 oraz v2.&lt;br>&lt;/p>
&lt;h3 id="-wsparcie-explorer">🚀 Wsparcie: Explorer
&lt;/h3>&lt;p>Co jakiś czas do gry wchodził Explorer – jego głównym zadaniem było szukanie planet z zasobami.&lt;br>
Jeśli uległ zakleszczeniu, zmieniał się w rakietę ICBMv2, randomizował wektor ataku.&lt;/p>
&lt;h3 id="-bezpieczny-mur-dwójka-defenderów">🛡️ Bezpieczny mur: Dwójka Defenderów
&lt;/h3>&lt;p>Bez większych zmian – jeśli baza jest atakowana i nie możemy jej obronić nowym statkiem, to &lt;strong>Defender zmieniał się w medyka&lt;/strong>. &lt;br>
W innym przypadku prowadził ciągły ogień krzyżowy, smażąc wszystko, co popadnie.&lt;/p>
&lt;hr>
&lt;p>I tak! &lt;strong>Taka heurystyka dała nam 3. miejsce!&lt;/strong> 🏆&lt;/p>
&lt;p>Może wydawać się trywialna, ale naprawdę przyniosła efekty. Jak widać, nie rzuciliśmy się na falę szczęścia, tylko w testach i ewaluacjach to podejście okazało się najefektywniejsze.&lt;br>
A jeśli &lt;strong>działa, to znaczy, że jest co najmniej dobre&lt;/strong>.&lt;/p>
&lt;h3 id="gamechanger---wiedza-prosto-z-kodu-źródłowego">Gamechanger - wiedza prosto z kodu źródłowego
&lt;/h3>&lt;p>Warto też zaznaczyć, że przez cały czas trwania hackathonu analizowaliśmy kod źródłowy gry. Dzięki temu poznaliśmy jej działanie od samych podstaw, dowiedzieliśmy się, że &lt;strong>czasami zamiast strzelać, lepiej uciekać&lt;/strong>, a także odkryliśmy wiele innych sprytnych taktyk, które wykorzystaliśmy w naszym finalnym rozwiązaniu.&lt;/p>
&lt;h2 id="epilog">Epilog
&lt;/h2>&lt;p>Pisząc ten post, nadal czuję naprawdę &lt;strong>dużą satysfakcję&lt;/strong>, ale przede wszystkim cieszę się z czasu spędzonego z drużyną. &lt;br>
Naprawdę to &lt;strong>świetne uczucie&lt;/strong> – walczyć ramię w ramię z kolejnymi przeciwnościami, mając u boku taką ekipę jak nasza. Co więcej, sam event, mimo drobnych potknięć, był &lt;strong>nie tylko ambitny, ale też świetnie przemyślany&lt;/strong>.&lt;/p>
&lt;p>Na wielu hackathonach wybór zwycięzcy opiera się na subiektywnej opinii jury, a tutaj wygrywa matematyka – &lt;strong>kto wyżej w rankingu, ten lepszy&lt;/strong>. I to jest coś, co naprawdę doceniam.&lt;br>
Mam wielką nadzieję, że &lt;strong>za rok ponownie uda nam się wziąć udział&lt;/strong> w kolejnej edycji wydarzenia. Wiem, że następnym razem będziemy jeszcze mocniejsi i wierzę, że znów staniemy w szranki – zarówno ze swoimi słabościami, jak i z wieloma naprawdę utalentowanymi i ambitnymi deweloperami.&lt;/p>
&lt;hr>
&lt;h3 id="podziękowania">Podziękowania
&lt;/h3>&lt;p>&lt;strong>Dzięki ekipo!&lt;/strong>&lt;br>
&lt;strong>&lt;a class="link" href="https://www.linkedin.com/in/maciej-kaszkowiak/" target="_blank" rel="noopener"
>Maciej Kaszkowiak&lt;/a>, &lt;a class="link" href="https://www.linkedin.com/in/jakub-binkowski-80136825b/" target="_blank" rel="noopener"
>Jakub Binkowski&lt;/a>, &lt;a class="link" href="https://www.linkedin.com/in/maciej-mazur-90064b2b4/" target="_blank" rel="noopener"
>Maciej Mazur&lt;/a> i &lt;a class="link" href="https://www.linkedin.com/in/szymon-pasieczny-4a664b215/" target="_blank" rel="noopener"
>Szymon Pasieczny&lt;/a>&lt;/strong> – jak już mówiłem, dzięki wam hackathon to nie tylko kodzenie, ale też świetnie spędzony czas z dużą dozą śmiechu, żartów i tryhardu. Czyli mieszanka w idealnych proporcjach.&lt;/p>
&lt;p>&lt;strong>Dzięki Adamowi Mazurowi&lt;/strong> – przewodniczącemu koła &lt;strong>&amp;ldquo;Ghost&amp;rdquo;&lt;/strong> na Politechnice Poznańskiej. Dzięki jego zaangażowaniu najprawdopodobniej otrzymamy finansowanie od uczelni, a to naprawdę dobre uczucie – mieć świadomość, że nasza uczelnia wspiera nas w tym, co robimy.&lt;/p>
&lt;p>&lt;strong>Dzięki organizatorom!&lt;/strong> – bez was nie byłoby tego eventu. Czuć, że wykonaliście kawał dobrej roboty – dużo rzeczy zostało poprawionych względem poprzedniej edycji i wiem, że ta tendencja będzie utrzymana.&lt;/p>
&lt;p>Kto wie, może kiedyś hackathon odbędzie się na Politechnice Poznańskiej? 🤔&lt;/p>
&lt;hr>
&lt;p>Dzięki za poświęcony czas na przeczytanie tego wpisu! Na dole wrzucam kilka fotek z wydarzenia, a jak dojdą jakieś nowe, to postaram się je uzupełnić!&lt;/p>
&lt;p>&lt;img src="https://blog.huszcza.dev/p/pizza.jpg"
width="2048"
height="2731"
srcset="https://blog.huszcza.dev/p/pizza_hu8802891fcec66ba25cedd0c7885592e4_530245_480x0_resize_q75_box.jpg 480w, https://blog.huszcza.dev/p/pizza_hu8802891fcec66ba25cedd0c7885592e4_530245_1024x0_resize_q75_box.jpg 1024w"
loading="lazy"
alt="Nie mogło zabraknąć pizzy"
class="gallery-image"
data-flex-grow="74"
data-flex-basis="179px"
> &lt;img src="https://blog.huszcza.dev/p/snow.jpg"
width="2048"
height="2731"
srcset="https://blog.huszcza.dev/p/snow_hua6efa9f400c357b7b771fded616df924_472425_480x0_resize_q75_box.jpg 480w, https://blog.huszcza.dev/p/snow_hua6efa9f400c357b7b771fded616df924_472425_1024x0_resize_q75_box.jpg 1024w"
loading="lazy"
alt="W Krakowie zaskoczył nas śnieg!"
class="gallery-image"
data-flex-grow="74"
data-flex-basis="179px"
>&lt;img src="https://blog.huszcza.dev/p/winners.jpg"
width="2016"
height="1512"
srcset="https://blog.huszcza.dev/p/winners_hu791980cec79be0ba1377b9dfad43049c_255764_480x0_resize_q75_box.jpg 480w, https://blog.huszcza.dev/p/winners_hu791980cec79be0ba1377b9dfad43049c_255764_1024x0_resize_q75_box.jpg 1024w"
loading="lazy"
alt="Nasza ekipa"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
>&lt;/p></description></item></channel></rss>