Wyślij zapytanie Dołącz do Sii

ML.NET jest darmową biblioteką rozwijaną w ramach modelu open-source przeznaczoną do uczenia maszynowego na platformie .NET z wykorzystaniem języków C# oraz F#. Można ją uruchamiać na systemach Windows, Linux oraz MacOS, wykorzystując .NET Core, oraz na Windowsie używając .NET Framework.

Według założeń ma być prosta w obsłudze dla developerów platformy .NET oraz osób niemających dużego doświadczenia w uczeniu maszynowym, a także zapewniać obsługę gotowych modeli z innych bibliotek takich jak Infer.NET, TensorFlow czy ONNX.

W niniejszym artykule postaram się przedstawić na konkretnym przykładzie:

  • w jaki sposób korzystać z ML.NET,
  • jak przetworzyć dane,
  • w jaki sposób wytrenować model uczenia maszynowego,
  • oraz jak dokonać klasyfikacji binarnej.

Następnie omówię:

  • metody pozwalające na ewaluację uzyskanych wyników,
  • jak zoptymalizować stworzony model,
  • a także przyjrzę się aspektowi wydajnościowemu biblioteki.

Zbiór danych i problem dziedzinowy

Zbiór danych

Aby w pełni poznać możliwości ML.NET, przyjrzymy się konkretnemu zbiorowi danych dostępnemu
w Internecie i spróbujemy rozwiązać zadany problem z wykorzystaniem narzędzia do uczenia maszynowego od firmy Microsoft.

W tym celu użyłem zbioru danych AVONET: morphological, ecological and geographical data for all birds zawierającego dane zebrane w wyniku katalogowania osobników ptaków różnych gatunków. Można go pobrać pod tym adresem: zbiór do pobrania.

Ryc. 1 Przykładowy wycinek zbioru danych
Ryc. 1 Przykładowy wycinek zbioru danych

Dane zawierają 90 020 rekordów dotyczących 11 009 gatunków ptaków zebranych w 181 krajach. Niektóre z zawartych w nich pól to:

  • gatunek ptaka (nazwa systematyczna),
  • jego płeć,
  • wiek,
  • kraj, w którym został odłowiony,
  • a także szereg atrybutów numerycznych, dotyczących choćby długości
  • i szerokości dzioba czy też długości skrzydeł.

Problem dziedzinowy

Problemem, nad którym będziemy pracować w ramach eksploracji ML.NET, jest klasyfikacja binarna, czyli przyporządkowanie dla danego rekordu jednej z dwóch wartości atrybutu etykiety. W naszym przypadku będzie to płeć – naszym celem jest predykcja płci ptaka na podstawie innych dostępnych atrybutów.

Określenie płci u ptaków jest rzeczywistym problemem, z którym mierzą się naukowcy, ornitolodzy czy też posiadacze ptaków w charakterze zwierząt domowych. Wynika to z faktu, iż nie u każdego
z gatunków występuje dymorfizm płciowy, pozwalający na jednoznaczne przyporządkowane osobnika do płci. Zagadnienie te jest zgłębiane w licznych pracach naukowych, opracowywane są rożne metody na radzenie sobie z tym problemem. Naszym zadaniem jest próba wykorzystania do tego celu uczenia maszynowego.

Instalacja i przygotowanie środowiska

Aby rozpocząć tworzenie kodu, musimy posiadać wspierany system operacyjny oraz architekturę procesora (lista wspieranych środowisk) wraz z zainstalowaną odpowiednią wersją platformy .NET.

W moim przypadku będzie to system Windows 10 z zainstalowanym .NET Framework SDK w wersji 6.0.301 (dostępny do pobrania na oficjalnej stronie). Naszym IDE będzie Visual Studio 2022
w darmowej wersji Community.

Po zainstalowaniu wszystkich niezbędnych komponentów oraz uruchomieniu VS2022, należy utworzyć nowy projekt (w naszym przypadku będzie to aplikacja konsolowa) i postępować według wskazówek kreatora.

Ryc. 2 Wybór projektu aplikacji konsolowej w Visual Studio 2022
Ryc. 2 Wybór projektu aplikacji konsolowej w Visual Studio 2022

Aby móc korzystać z narzędzia ML.NET w nowym projekcie, musimy pobrać odpowiednią paczkę z menadżera paczek NuGet. W tym celu, w widoku drzewa projektu, wybieramy węzeł główny (Solution), klikamy PPM i wskazujemy „Manage NuGet Packages for Solution…”.

Ryc. 3 Menu kontekstowe dla solucji
Ryc. 3 Menu kontekstowe dla solucji

Następnym krokiem jest pobrane odpowiedniego pakietu. Aby to zrobić, wpisujemy w polu wyszukiwania nazwę „ML.NET” i gdy interesujący nas wpis pojawi się na ekranie, klikamy przycisk Install.

Ryc. 4 NuGet Package Manager
Ryc. 4 NuGet Package Manager

Po poprawnie wykonanej instalacji możemy przystąpić do tworzenia rozwiązania uczenia maszynowego.

Ładowanie danych

Pierwszym krokiem, który musimy wykonać, jest załadowanie naszego zbioru danych do pamięci, aby móc dalej przetwarzać zawarte w nim dane, a następnie wytrenować oraz wykorzystać model klasyfikatora.

Aby operować na rekordach naszego zbioru, musimy stworzyć klasę, która odzwierciedla wszystkie jego atrybuty. W tym celu należy utworzyć klasę Bird, zawierającą jako właściwości interesujące nas kolumny.

using Microsoft.ML.Data;

namespace ML.NET.Models
{
    public class Bird
    {
        [LoadColumn(8), ColumnName("Label")]
        public string Sex { get; set; }

        [LoadColumn(1)]
        public string Species { get; set; }
        [LoadColumn(11)]
        public string Country { get; set; }
        [LoadColumn(13)]
        public float BeakLengthCulmen { get; set; }
        [LoadColumn(14)]
        public float BeakLengthNares { get; set; }
        [LoadColumn(15)]
        public float BeakWidth { get; set; }
        [LoadColumn(16)]
        public float BeakDepth { get; set; }
        [LoadColumn(17)]
        public float TarsusLength { get; set; }
        [LoadColumn(18)]
        public float WingLength { get; set; }
        [LoadColumn(19)]
        public float KippsDistance { get; set; }
        [LoadColumn(20)]
        public float SecondaryLength { get; set; }
        [LoadColumn(21)]
        public float HandWingIndex { get; set; }
        [LoadColumn(22)]
        public float TailLength { get; set; }
    }
}

Następnie tworzymy obiekt klasy MLContext, jako parametr podając ziarno, czyli dowolną liczbę, która zostanie wykorzystana przez generator liczb pseudolosowych. Zapewnia to powtarzalność wyników naszych eksperymentów na przestrzeni różnych wywołań. Wywołujemy następnie jedną z dostępnych w obiekcie mlContext.Data metod na wczytanie danych – w tym przypadku będzie to odczyt pliku .csv z dysku. Podajemy również parametry dotyczące separatora wartości oraz flagę informującą o obecności nagłówków kolumn w pliku.

//Ładowanie danych
var mlContext = new MLContext(42);
var data = mlContext.Data.LoadFromTextFile<Bird>(filePath, separatorChar: ',', hasHeader: true);
var splitDataView = mlContext.Data.TrainTestSplit(data, testFraction: 0.2);

Tak wczytany zbiór możemy od razu podzielić na zbiory treningowe i testowe za pomocą metody TrainTestSplit. Zbiór treningowy będziemy wykorzystywać do trenowania naszego modelu uczenia maszynowego, natomiast testowy posłuży do ewaluacji jego wyników.

Transformacja danych

Kolejnym krokiem jest transformacja wczytanych danych tak, aby mogły być one wykorzystane do trenowania modeli uczenia maszynowego. Podejmiemy się w tym celu transformacji kilku z kolumn do odpowiedniego formatu.

//Transformacja danych
var map = new Dictionary<string, bool> { { "M", true }, { "F", false } };
var pipeline = mlContext.Transforms.Conversion.MapValue("Label", map)
                                                      .Append(mlContext.Transforms.Text.FeaturizeText(inputColumnName: "Species", outputColumnName: "SpeciesFeaturized"))
                                                      .Append(mlContext.Transforms.Text.FeaturizeText(inputColumnName: "Country", outputColumnName: "CountryFeaturized"))
                                                      .Append(mlContext.Transforms.Concatenate("Features", "SpeciesFeaturized", "CountryFeaturized", "BeakLengthCulmen","BeakLengthNares","BeakWidth", "BeakDepth", "TarsusLength", "WingLength", "KippsDistance", "SecondaryLength", "HandWingIndex", "TailLength"))
                                                      .Append(mlContext.BinaryClassification.Trainers.LightGbm(labelColumnName: "Label", featureColumnName: "Features"));

Pierwszą z operacji jest mapowanie wartości określających płeć ptaka z łańcuchów znaków na wartości logiczne. W tym celu tworzymy słownik, który posłuży nam jako parametr metody MapValue z obiektu mlContext.Transforms.Conversion.

Musimy dokonać tej transformacji, aby nasz klasyfikator mógł poprawnie operować na danych. Po wykonaniu tego fragmentu kodu możemy dalej operować na uzyskanym wyniku, dodając dalsze kroki transformacji i tworząc łańcuch kolejnych wywołań.

Kolejne dwie operacje dotyczą zamiany wartości tekstowych z kolumn odnoszących się do kraju oraz gatunku ptaka na wartości numeryczne. Jest to standardowa praktyka przy budowaniu modeli uczenia maszynowego, gdyż wymagają one wartości liczbowych do poprawnego przetwarzania.

W tym celu wykorzystujemy metodę FeaturizeText, jako argumenty podając nazwę kolumny, którą chcemy transformować oraz jej nową nazwę po wykonaniu tej operacji.

Następnym krokiem jest wywołanie metody Concatenate, w celu złączenia wszystkich kolumn dotyczących cech naszego zbioru w jedną o nazwie Features, której obecność jest wymagana poprzez modele w ML.NET

Ostatnim etapem przygotowania danych jest wskazanie klasyfikatora, który chcemy wykorzystać. Na potrzeby rozwiązania naszego problemu klasyfikacji wybierzmy klasyfikator LightGbm, korzystający z metody binary  boosted decision tree. Jako parametry dla metody wskazujemy nazwę kolumny danych zawierającej etykiety (wartość, którą chcemy szacować) oraz cechy (wartości, na podstawie których algorytm będzie dokonywać predykcji).

W tym momencie nasz przepływ transformacji danych jest gotowy. Warto wiedzieć, iż do tego momentu podczas uruchomienia kodu nie zostanie wykonany żaden z tych kroków, przepływ danych zostanie uruchomiony dopiero przy wywołaniu operacji trenowania zbioru danych – realizowane jest tu opóźnione wykonanie.

Uczenie zbioru

Możemy teraz przystąpić do trenowania naszego klasyfikatora. Wykorzystamy w tym celu wydzielony wcześniej ze zbioru głównego zbiór treningowy.

//Trenowanie modelu
var model = pipeline.Fit(splitDataView.TrainSet);

Po wykonaniu metody Fit na obiekcie reprezentującym stworzony przez nas przepływ transformacji danych, uzyskujemy wytrenowany model, który można wykorzystać w działaniu. Warto w tym miejscu zwrócić uwagę, iż trenowanie może być w niektórych przypadkach bardzo czasochłonne. Zależy to od wielu czynników, takich jak:

  • rozmiar zbioru,
  • liczba jego cech
  • czy też wykorzystanego algorytmu uczenia maszynowego.

Istnieje możliwość zapisu gotowego modelu do pliku i łatwego wczytania go później, tak, aby nie wykonywać czynności trenowania za każdym razem.

Klasyfikacja

Pierwszym krokiem w uzyskaniu wyniku klasyfikacji jest stworzenie prostej klasy, która przechowywać będzie wynik wraz z polami pomocniczymi, które będą przydatne przy ewaluacji efektywności modelu. Do tego celu wykorzystamy klasę BirdPrediction.

using Microsoft.ML.Data;

namespace ML.NET.Models
{
    public class BirdPrediction
    {
        [ColumnName("PredictedLabel")]
        public bool IsMale { get; set; }
        public float Probability { get; set; }
        public float Score { get; set; }
    }
}

Wynik klasyfikacji będzie wartością logiczną, która znajdzie się we właściwości IsMale (musimy ją oznaczyć za pomocą atrybutu [ColumnName(„PredictedLabel”)] w celu poprawnego działania).

Gdy odpowiednia klasa jest gotowa, możemy przystąpić do klasyfikacji. Stwórzmy przykładowy obiekt klasy Bird, zawierający dane na temat konkretnego ptaka (w tym przypadku jest to puchacz plamisty ze zbioru danych).

Pamiętajmy, aby nie przypisywać wartości atrybutowi etykiety – jest to zadanie klasyfikatora, aby oszacować tę wartość. Następnie wywołujemy metodę CreatePredictionEngine wraz ze wskazaniem parametrów klas uogólnionych, aby uzyskać obiekt, na którym wywołujemy metodę Predict, podając jako parametr stworzony wcześniej rekord do klasyfikacji. 

//Sprawdzenie klasyfikacji
var testBird = new Bird 
{ 
    Species = "Bubo africanus", 
    Country = "Kenya",
    BeakLengthCulmen = 38.9f,
    BeakLengthNares = 19.1f,
    BeakWidth = 10.7f,
    BeakDepth = 17.2f,
    TarsusLength = 53.5f,
    WingLength = 320f,
    KippsDistance = 98.2f,
    SecondaryLength = 221.8f,
    HandWingIndex = 30.7f,
    TailLength = 162f
 
};
var predictedBirdSex = mlContext.Model.CreatePredictionEngine<Bird, BirdPrediction>(model).Predict(testBird).IsMale;

Po wykonaniu powyższego fragment kodu możemy zauważyć, iż pole IsMale ma wartość logiczną „fałsz”, oznacza to, iż model zaklasyfikował okaz ptaka jako samicę. W tym przypadku predykcja jest zgodna ze stanem faktycznym, jednak aby realnie ocenić przydatność stworzonego przez nas modelu, musimy przeprowadzić rzetelną ewaluację wyników, wykorzystując w tym celu wszystkie dostępne dane wraz z szeregiem odpowiednich metryk.

Ewaluacja wyników

Dokonamy teraz oceny przydatności naszego wytrenowanego modelu, korzystając z powszechnie wykorzystywanych metryk:

  • Accuracy,
  • F1Score,
  • AuROC (Area under Receiver Operating Characteristic),
  • Precision
  • oraz Recall.

Pozwolą nam one na rożne sposoby ocenić, jak dobrze radzi sobie nasz klasyfikator.

Techniką, którą wykorzystamy w celu lepszego sprawdzenia wiarygodności wyników, jest tzw. walidacja krzyżowa (cross-validation). Polega ona na powtarzanym podziale zbioru na zbiór testowy i treningowy tak, aby za każdym razem model nie był wyuczony na innej części danych.

Powszechnie wykorzystywana jest pięciokrotna walidacja krzyżowa, w której pięć razy wyłączamy inny podzbiór testowy ze zbioru i sprawdzamy na nim efektywność modelu. Poniższy kod prezentuje ewaluację modelu z wykorzystaniem tego schematu walidacji.

//Ewaluacja wyników
var testDataView = model.Transform(splitDataView.TestSet);
var cvResults = mlContext.BinaryClassification.CrossValidate(testDataView, mlContext.BinaryClassification.Trainers.LightGbm(), numberOfFolds: 5);

ML.NET zawiera gotowe metody do wykonania walidacji krzyżowej, które zwracają wyniki interesujących nas metryk dla każdej iteracji tego procesu. W celu obliczenia ich wartości dla całości procesu, dokonamy obliczenia średniej arytmetycznej każdej z nich.

//Obliczenie metryk
var accuracy = cvResults.Select(f => f.Metrics.Accuracy).Average();
var f1 = cvResults.Select(f => f.Metrics.F1Score).Average();
var auroc = cvResults.Select(f => f.Metrics.AreaUnderRocCurve).Average();
var precision = cvResults.Select(f => f.Metrics.PositivePrecision).Average();
var recall = cvResults.Select(f => f.Metrics.PositiveRecall).Average();

Poniżej prezentują się uzyskane wyniki efektywności naszego modelu dla klasyfikacji binarnej:

MetrykaWartość
Accuracy0.5906
Precision0.5948
Recall0.5795
F1 Score0.5868
AuRoC0.6538

Jak możemy zaobserwować, wyniki klasyfikacji nie prezentują wymarzonej efektywności stworzonego modelu. Metryka Accuracy z wartościa ok. 0,59 oznacza, iż 59% próbek zostało poprawnie sklasyfikowanych. Jak wysoką celność będzie miał nasz klasyfikator, zależy od wielu czynników i naszym zadaniem podczas tworzenia i optymalizacji modeli uczenia maszynowego jest odkrycie, co ma wpływ na finalny wynik tejże skuteczności. Jedną z metod poprawy efektywności jest optymalizacja parametrów klasyfikatora.

Optymalizacja parametrów klasyfikatora

Każdy z dostępnych algorytmów uczenia maszynowe dysponuje zestawem unikalnych parametrów tzw. hiperparametrów, pozwalających dostroić jego działanie do naszych potrzeb. Możemy zmieniać te parametry, aby zwiększyć czy to skuteczność, czy też wydajność naszego modelu.

W narzędziach dostępnych dla języka Python, takich jak scikit-learn, dostępne są klasy np. GridSearchCV – jest to funkcja pozwalająca na automatyczne sprawdzenie wartości wielu różnych hiperparametrów naraz i wskazanie najlepszej ich kombinacji (biorąc pod uwagę wskazane metryki).

Framework ML.NET nie dysponuje dokładnym odpowiednikiem takich narzędzi, ale oferuje rozwiązanie nawet ciekawsze – AutoML.

AutoML jest narzędziem pozwalającym na wspomaganie pracy z uczeniem maszynowym, polegające na automatycznym doborze najlepszych algorytmów oraz hiperparametrów dla naszego zbioru danych. Jest to rozwiązanie ciekawe zarówno dla osób z niewielką znajomością różnych technik uczenia maszynowego, które chciałby uzyskać wysoką skuteczność modeli, jak i tych, które chcą oszczędzić czas na manualnym strojeniu tworzonych modeli.

Aby skorzystać z AutoML, należy zainstalować pakiet Microsoft.ML.AutoML za pośrednictwem NuGet Package Manager, analogicznie do instalacji podstawowego  frameworka ML.NET.

Żeby dokonać automatycznego strojenia dla naszych danych, tworzymy obiekt eksperymentu, zawierający ustawienia, jakie chcemy wykorzystać przy przeprowadzeniu tej operacji. Określamy rodzaj problemu, nad którym pracujemy (klasyfikacja binarna), metrykę, według której chcemy stroić nasz model (Accuracy) oraz czas eksperymentu (w naszym przypadku wybierzmy 15 minut). Dokonujemy również niezbędnej transformacji danych, podobnie jak w manualnym uczeniu modelu, ale w tym przypadku wymagane jest tylko mapowanie dla atrybutu etykiety.

//Ustawienia eksperymentu
var experimentSettings = new BinaryExperimentSettings();
experimentSettings.MaxExperimentTimeInSeconds = 900;
experimentSettings.OptimizingMetric = BinaryClassificationMetric.Accuracy;
experimentSettings.CacheDirectoryName = null;
 
//Transformacja danych
var transformedData = mlContext.Transforms.Conversion.MapValue("Label", new Dictionary<string, bool> { { "M", true }, { "F", false } }).Fit(data).Transform(data);
 
//Przeprowadzenie eksperymentu
var experiment = mlContext.Auto().CreateBinaryClassificationExperiment(experimentSettings);
var experimentResult = experiment.Execute(transformedData);

Po uruchomieniu eksperymentu przez wskazany czas AutoML będzie tworzyć modele z różnymi kombinacjami algorytmów oraz hiperparametrów tak, aby uzyskać jak najlepszy wynik wskazanej metryki.

//Ewaluacja danych
var metrics = experimentResult.BestRun.ValidationMetrics;
var accuracyAuto = metrics.Accuracy;
var f1Auto = metrics.F1Score;
var aurocAuto = metrics.AreaUnderRocCurve;
var precisionAuto = metrics.PositivePrecision;
var recallAuto = metrics.PositiveRecall;

Wyniki naszego eksperymentu prezentują się następująco: wykonane zostało 131 prób i jako najlepszy wskazany został algorytm FastTreeBinary. Poniżej w tabeli znajdują się wyniki metryk dla najlepszej wykonanej próby. Jak możemy zauważyć, w krótkim czasie bez jakiejkolwiek dodatkowej wiedzy udało nam się poprawić wyniki wszystkich metryk, a zatem zwiększyć skuteczność naszego klasyfikatora.

MetrykaWartość
Accuracy0.6176
Precision0.6126
Recall0.6273
F1 Score0.6199
AuRoC0.6925

Wydajność

Jednym z argumentów mających nas zachęcić do korzystania z ML.NET jest reklamowana przez twórców wydajność frameworka. Na oficjalnej stronie możemy znaleźć poniższą informację:

Ryc. 5 Informacja na temat wydajności z oficjalnej strony
Ryc. 5 Informacja na temat wydajności z oficjalnej strony

Czy w istocie niniejsze narzędzie potrafi zdeklasować popularne rozwiązania uczenia maszynowego oparte o scikit-learn, oferując niemal 6-krotny wzrost wydajności, przy zbliżonym poziomie skuteczności? Dostępny jest artykuł określający w jaki sposób niniejsze wyniki zostały uzyskane, dzięki niemu możliwa jest próba ich odtworzenia we własnym zakresie.

W niniejszym artykule spróbujemy sprawdzić rzekomą przepaść wydajnościową ML.NET na podobnym zbiorze danych. Wykorzystamy w tym celu zbiór Criteo Sponsored Search Conversion Log Dataset. Zawiera on dane na temat wyświetlanych na stronach internetowych reklam wraz
z informacją, czy reklama została kliknięta i czy doszło do zakupu reklamowanego produktu.

Aby porównać wydajność narzędzi dostępnych w języku Python oraz ML.NET, stworzone zostały dwa rozwiązania, zawierający analogiczne modele wytrenowane na tym samym zbiorze danych,
z wykorzystaniem algorytmu regresji logistycznej.

Test został wykonany na 10% zbioru Criteo (ok. 600 MB, 1 599 563 rekordów) na komputerze klasy PC, a następnie porównany został czas klasyfikacji dla obu narzędzi. Każda z operacji została wykonana trzykrotnie, a wyniki zostały uśrednione.

Ryc. 6 Porównanie klasyfikacji binarnej (regresja logistyczna)
Ryc. 6 Porównanie klasyfikacji binarnej (regresja logistyczna)

Choć istnieje wiele czynników decydujących o wydajności klasyfikatora (implementacja, charakterystyka zbioru czy też ustawienia środowiska testowego), podczas kilkukrotnych prób wyraźnie da się zauważyć znaczną przewagę wydajnościową ML.NET – czas wykonania zmalał o ok. 58%!

Podsumowanie

ML.NET jest ciekawym narzędziem, które warto wypróbować podczas wykonywana zadań związanych z szeroko pojętym uczeniem maszynowym. Jest to szczególnie interesująca opcja dla programistów platformy .NET, gdyż nie wymaga nauki języka Python, który jest powszechnie wykorzystywany do tych celów.

Jedną z najmocniejszych cech biblioteki jest jej wydajność. Zarówno zapewnienia twórców, jak
i wykonane przeze mnie testy wskazują na znacznie lepszą wydajność w porównaniu do konkurencyjnych rozwiązań. Może być to istotne kryterium podczas podejmowania decyzji o wyborze narzędzia do uczenia maszynowego w środowisku produkcyjnym

Framework jest cały czas rozwijany na zasadach otwartego kodu źródłowego, gdzie każdy może mieć swój wkład w rozwój narzędzia. Ostatnie wydanie (wersja 1.7) z marca 2022 daje nadzieję na dalszy rozwój i wsparcie w przyszłości.

Warto jednak zaznaczyć, iż jest to stosunkowo młode narzędzie (pierwsze wydanie powstało w 2018 roku) i nadal nie dysponuje niektórymi funkcjonalnościami dostępnymi w ekosystemie języka Python. Podczas wyboru rozwiązania dla naszych problemów uczenia maszynowego warto mieć to na uwadze.

Przydatne linki

***

Jeśli interesuje Cię tematyka uczenia maszynowego, zachęcamy do zapoznania się z innymi artykułami naszych ekspertów np.: TinyML: Uczenie maszynowe w systemach wbudowanych oraz RPA i inteligentne przetwarzanie dokumentów.

4.3/5 ( głosy: 12)
Ocena:
4.3/5 ( głosy: 12)
Autor
Avatar
Wojciech Agaciński

Software Engineer w Sii. Zajmuje się programowaniem na platformę .NET. W wolnym czasie gra w planszówki, obserwuje gwiazdy i czasem uwarzy piwo. Lubi też pogrzebać w danych.

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Może Cię również zainteresować

Pokaż więcej artykułów

Bądź na bieżąco

Zasubskrybuj naszego bloga i otrzymuj informacje o najnowszych wpisach.

Otrzymaj ofertę

Jeśli chcesz dowiedzieć się więcej na temat oferty Sii, skontaktuj się z nami.

Wyślij zapytanie Wyślij zapytanie

Natalia Competency Center Director

Get an offer

Dołącz do Sii

Znajdź idealną pracę – zapoznaj się z naszą ofertą rekrutacyjną i aplikuj.

Aplikuj Aplikuj

Paweł Process Owner

Join Sii

ZATWIERDŹ

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?