Automatyzacja inżynierii cech

Niedawno na zakupach w sklepie:

Jagoda, dlaczego tak mocno przyglądasz się tej alejce z ciasteczkami? Przecież dzisiaj nie jest sobota, więc nie jemy słodyczy – uśmiechnąłem się,

Głęboko zamyślona powiedziała:

Ile kucharzy musiało robić te ciastka by była ich cała alejka. Chyba ze stu. Albo z milion.

Kiedyś tak by było. Natomiast teraz w fabrykach wiele rzeczy jest zautomatyzowanych. To co kiedyś zajmowało dużo czasu teraz robione jest automatycznie. I takie ciastka robią teraz głównie roboty w fabrykach. Nie trzeba miliona kucharzy. Pewnie wystarczy jeden lub kilku, którzy opracowują przepis i sprawdzają czy roboty wykonały dobrą robotę.

A u Ciebie w pracy też pracują za Ciebie roboty a Ty możesz odpoczywać?

Roboty jeszcze nie. Ale jest coraz więcej programów, które pomagają mi oraz moim kolegom i koleżankom lepiej i szybciej pracować.

W otaczającym nas świecie coraz więcej rzeczy jest automatyzowanych. Automatyzacja dowolnego procesu pozwala uczynić go bardziej wydajnym i opłacalnym.

Spójrzcie dla przykładu jak zmienił się sposób wytwarzania chociażby słodyczy na przestrzeni lat:

Kiedyś, aby ulepić około 10.000 ciastek potrzebnych było około 27 osób (przy założeniu, że ulepienie jednego trwa 1 minutę). Dziś robi to jeden pracownik nadzorujący maszyny.

A co z automatyzacją w uczeniu maszynowym?

W uczeniu maszynowym można zauważyć trend, że w coraz większym stopniu odchodzi się od ręcznie zaprojektowanych modeli do automatycznie zoptymalizowanych rurociągów jak np. H20 czy auto-sklearn. Ma to na celu uproszczenie wyboru modelu wraz z dostrojeniem hiperparametrów najlepszego modelu dla zestawu danych z minimalną interwencją człowieka. Natomiast właśnie przygotowanie tych danych jest najważniejszym etapem, o którym wspominałem w ostatnim wpisie.

Najczęściej inżynieria cech jest procesem ręcznym, opartym na wiedzy w dziedzinie oraz intuicji. Warto wspomnieć, że często ten proces może być bardzo żmudny, uciążliwy i wymagający mnóstwa czasu.  A co jeśli posiadamy ustrukturyzowane dane to czy byłaby możliwość zautomatyzowania procesu wymyślania cech? Wówczas zamiast poświęcać czas na przygotowanie mnóstwa charakterystyk moglibyśmy ten czas poświęcić na inne aspekty problemu, nad którym pracujemy.

Jestem przekonany, że wielu z Was po rozwiązaniu kilku problemów z uczeniem maszynowym zauważyło, że wiele operacji używanych do budowania funkcji w zestawach danych było powtarzalnych. Często chcemy mieć ogólne statystyki jak wartości minimalne, średnie, maksymalne itp. Dodatkowo jeśli w naszych danych mamy szeregi czasowe to chcemy określić statystyki w cyklach, np. miesięcznych, tygodniowych czy dziennych. Te operacje wydają się powtarzalne. I w tym może nam pomóc biblioteka Featuretools.

Czym jest Featuretools?

Featuretools to biblioteka typu open source do przeprowadzania automatycznej inżynierii cech. Jest to moim zdaniem najlepsze darmowe znane mi narzędzie zaprojektowane w celu przyspieszenia procesu generowania charakterystyk.

Polega w głównej mierze na tym, by wyliczyć charakterystyki na podstawie funkcji (zwanych prymitywami) do różnych relacyjnych zestawów danych. Sam proces wyliczenia nazywany jest „Deep Feature Synthesis” (DFS) czyli głęboką syntezą cech.

Może brzmi to skomplikowanie, ale popatrzcie na prosty przykład z trzema tabelami (pierwsza z klientem, druga z informacją o rachunku, trzecia z historią salda na rachunkach).

Mam nadzieję, że teraz idea jest jasna. Przejdźmy sobie do praktyki krok po kroku. Aby nie było za łatwo poszukajmy ciekawego zbioru danych.

Dane dla Naszego przykładu

Wykorzystajmy tutaj dane udostępnione w konkursie Kaggle w konkursie Home Credit Risk [tutaj link]. Udostępniono tam dane, które posiadała firma w celu analizy czy dany klient spłaci zaciągany kredyt.

W tym przykładzie jest wiele relacji między danymi dzięki czemu lepiej będziecie mogli zobaczyć przykładowe wykorzystanie.

Poniżej załączam strukturę bazy danych:

W skrócie:

  • application: główna tabela z informacją o składanym przez klienta wniosku
  • bureau: informacja z biura kredytowego na moment składania wniosku o historii kredytowej osoby. Jest tutaj informacja o wszystkich produktach klienta – nie tylko w firmie w której wnioskuje. Coś jak polskie Biuro Informacji Kredytowej (BIK) 🙂
  • bureau_balance: historia kredytowa (jeden rekord jeden miesiąc) rachunków z bazy bureau.
  • previous_application: informacja z wcześniejszych wniosków kredytowych klienta
  • POS/card balance: miesięczne informacje o rachunkach kredytowych klienta
  • installments_payments: miesięczna informacja o spłatach rat klienta oraz ewentualnych zaległościach.

Wczytanie bibliotek i danych

Przygotowując materiał miałem zainstalowaną wersję 0.10.1

import pandas as pd

import featuretools as ft
print(ft.__version__)
app_train = pd.read_csv('../data/application_train.csv')
app_test = pd.read_csv('../data/application_test.csv')
bureau = pd.read_csv('../data/bureau.csv')
bureau_balance = pd.read_csv('../data/bureau_balance.csv')
cash = pd.read_csv('../data/POS_CASH_balance.csv')
card = pd.read_csv('../data/credit_card_balance.csv')
previous = pd.read_csv('../data/previous_application.csv')
installments = pd.read_csv('../data/installments_payments.csv')

Dane treningowe i testowe były rozdzielone. Na potrzeby generowania cech połączmy je oraz sprawdźmy liczności danych.

#append train and test data 
app = app_train.append(app_test,sort=False) 
# Add name to dataframe
def nadanie_nazw():
    app.name = 'app'
    bureau.name = 'bureau'
    bureau_balance.name = 'bureau_balance'
    cash.name = 'cash'
    card.name = 'card'
    previous.name = 'previous'
    installments.name = 'installments'

    return [app, bureau, bureau_balance, cash, card, previous, installments]

datasets_list = nadanie_nazw()
for ds in datasets_list:
    ds.replace({365243: np.nan}, inplace=True) #zamiana wartości odstających
    print('{}: {} rows'.format(ds.name , ds.shape[0])) #wyświetlenie informacji o liczbie wierszy

Danych jest sporo i zapewne wygenerowanie charakterystyk będzie się długo liczyć. Aby przyśpieszyć ten proces przygotujmy sobie najpierw mniejszy zbiór dla losowych 1.000 wniosków i danych, które posiadają.

Danych jest sporo i zapewne wygenerowanie charakterystyk będzie się długo liczyć. Aby przyśpieszyć ten proces przygotujmy sobie najpierw mniejszy zbiór dla losowych 1.000 wniosków i danych, które posiadają.

app = app.sample(1000)
bureau = bureau[bureau['SK_ID_CURR'].isin(list(app['SK_ID_CURR']))]
bureau_balance = bureau_balance[bureau_balance['SK_ID_BUREAU'].isin(list(bureau['SK_ID_BUREAU']))]
previous = previous[previous['SK_ID_CURR'].isin(list(app['SK_ID_CURR']))]
cash = cash[(cash['SK_ID_CURR'].isin(list(app['SK_ID_CURR']))) | (cash['SK_ID_PREV'].isin(list(previous['SK_ID_PREV'])))]
card = card[(card['SK_ID_CURR'].isin(list(app['SK_ID_CURR']))) | (card['SK_ID_PREV'].isin(list(previous['SK_ID_PREV'])))]
installments = installments[(installments['SK_ID_CURR'].isin(list(app['SK_ID_CURR']))) | (installments['SK_ID_PREV'].isin(list(previous['SK_ID_PREV'])))]

datasets_list = nadanie_nazw()
for ds in datasets_list:
    print('{}: {} rows'.format(ds.name , ds.shape[0])) #wyświetlenie informacji o liczbie wierszy

Takie liczności pozwolą nam szybko przetestować kod.

Dodajmy jeszcze taki sam format kluczy na potrzeby łączenia i możemy brać się za użycie biblioteki featuretools

#by klucze tabel miały taki sam format
bureau['SK_ID_BUREAU'] = bureau['SK_ID_BUREAU'].astype(np.int64)
previous['SK_ID_PREV'] = previous['SK_ID_PREV'].astype(np.int64)
cash['SK_ID_PREV'] = cash['SK_ID_PREV'].astype(np.int64)
card['SK_ID_PREV'] = card['SK_ID_PREV'].astype(np.int64)
installments['SK_ID_PREV'] = installments['SK_ID_PREV'].astype(np.int64)

Przygotowanie struktury (Entity set)

W pierwszej kolejności podczas wykorzystania Featuretools jest utworzenie EntitySet i dodanie do niego wszystkich tabel (encji). EntitySet to struktura danych, która przechowuje tabele i relacje między nimi. Taka konstrukcja ułatwia śledzenie wszystkich danych z wieloma tabelami relacyjnymi.

es = ft.EntitySet(id = 'application')

Tworząc definicje tabel należy pamiętać o kilku istotnych zasadach:

  • tabele muszą posiadać klucz (unikalny identyfikator dla każdej obserwacji). Jeśli istnieje należy podać jego nazwę a w przypadku braku ustawić jako parametr jego utworzenie (make_index = True).
  • jeśli w danych posiadasz indeks czasu wówczas go zdefiniuj (). Dzięki temu Featuretools będą używać odpowiednich funkcji do tworzenia charakterystyk (np. sumy tylko w weekendy).
  • w niektórych przypadkach warto samemu zdefiniować typ zmiennych by ułatwić dobranie odpowiednich agregatów. Przykładem mogłaby być zmienna logiczna przyjmująca wartości 0.0 oraz 1.0. Warto zmienić ją na typ boolen.
# Entities with a unique index
es = es.entity_from_dataframe(entity_id = 'app', dataframe = app, index = 'SK_ID_CURR')
es = es.entity_from_dataframe(entity_id = 'bureau', dataframe = bureau, index = 'SK_ID_BUREAU')
es = es.entity_from_dataframe(entity_id = 'previous', dataframe = previous, index = 'SK_ID_PREV')
# Entities without unique index. We need to add.
es = es.entity_from_dataframe(entity_id = 'bureau_balance', dataframe = bureau_balance, 
                             make_index = True, index = 'bureaubalance_index')

es = es.entity_from_dataframe(entity_id = 'cash', dataframe = cash, 
                             make_index = True, index = 'cash_index')

es = es.entity_from_dataframe(entity_id = 'installments', dataframe = installments,
                             make_index = True, index = 'installments_index')

es = es.entity_from_dataframe(entity_id = 'credit', dataframe = card,
                             make_index = True, index = 'card_index')
es

Powyżej widać określone tabele.

Określenie relacji (Relationships)

Relacje między tabelami powinny być znane każdemu, kto pracował z relacyjnymi bazami danych. W Featuretools idea jest taka sama, czyli używamy relacji w celu określenia jak rekordy z jednej tabeli odnoszą się do danych z drugiej tabeli.

W naszym przykładzie są głównie relacje jeden do wielu (1:n). Najlepszym sposobem, aby pomyśleć o relacji 1:n, jest analogia rodzic-dziecko. Rodzic jest jedną osobą, ale może mieć wiele dzieci. Dzieci mogą wtedy mieć wiele własnych dzieci. W tabeli nadrzędnej każda osoba ma jeden wiersz. Każda osoba w tabeli nadrzędnej może mieć wiele wierszy w tabeli podrzędnej.

Główna tabela z aplikacjami kredytowymi (app) ma jeden wiersz dla każdego klienta (SK_ID_CURR), podczas gdy ramka danych z biura kredytowego (bureau) ma wiele wcześniejszych pożyczek (SK_ID_BUREAU) dla każdego elementu nadrzędnego (SK_ID_CURR). Dlatego tabela danych z biura jest dzieckiem ramki danych aplikacji. Natomiast ramka danych biura jest rodzicem szczegółów rachunków (bureau_balance), ponieważ każda pożyczka ma jeden wiersz w biurze, ale wiele rekordów miesięcznych w bureau_balance.

# Relationship between current app and bureau
r_app_bureau = ft.Relationship(es['app']['SK_ID_CURR'], es['bureau']['SK_ID_CURR'])

# Relationship between bureau and bureau balance
r_bureau_balance = ft.Relationship(es['bureau']['SK_ID_BUREAU'], es['bureau_balance']['SK_ID_BUREAU'])

# Relationship between current app and previous apps
r_app_previous = ft.Relationship(es['app']['SK_ID_CURR'], es['previous']['SK_ID_CURR'])

# Relationships between previous apps and cash, installments, and credit
r_previous_cash = ft.Relationship(es['previous']['SK_ID_PREV'], es['cash']['SK_ID_PREV'])
r_previous_installments = ft.Relationship(es['previous']['SK_ID_PREV'], es['installments']['SK_ID_PREV'])
r_previous_credit = ft.Relationship(es['previous']['SK_ID_PREV'], es['credit']['SK_ID_PREV'])
# Add in the defined relationships
es = es.add_relationships([r_app_bureau, r_bureau_balance, r_app_previous,
                           r_previous_cash, r_previous_installments, r_previous_credit])
es

Określenie funkcji (primitives)

Mając określone tabele i relacje teraz chcemy określić funkcje (zwane prymitywami. Tutaj warto wykorzystać wiedzę domenową z danego tematu by jak najlepiej określić ich listę. Podobnie jak większość rzeczy w uczeniu maszynowym wybór prymitywów jest nadal w dużej mierze praktyką empiryczną, a nie teoretyczną. Zachęcam Was to testowania różnych funkcji.

Tutaj warto rozróżnić jeszcze dwa główne elementy. Prymitywy będziemy określać w dwóch różnych kategoriach:

  • Agregacja: funkcja, która grupuje dzieci dla każdego rodzica i oblicza statystyki, takie jak na przykład średnia, minimum czy maksimum. Przykładem jest średnia kwota wszystkich wcześniejszych pożyczek. Agregacja obejmuje wiele tabel przy użyciu relacji między tabelami.
  • Transformacja: operacja zastosowana do jednej lub więcej kolumn w jednej tabeli. Przykładem może być przyjęcie wartości bezwzględnej kolumny lub różnica między dwiema kolumnami w jednej tabeli.
# List the primitives in a dataframe
primitives = ft.list_primitives()
pd.options.display.max_colwidth = 200
primitives[primitives['type'] == 'aggregation'].head(10)
primitives[primitives['type'] == 'transform'].head(10)

Określmy teraz dla naszego przykładu funkcje, które chcemy nałożyć:

# Define default premitives
agg_primitives = ['min', 'max', 'median', 'n_most_common', 'last', 'sum', 'mean', 'count', 'num_unique', 'skew']
trans_primitives = ['and', 'or', 'diff', 'percentile']

Warto dodać, że jeszcze jest możliwość napisania i określenia swoich własnych prymitywów. Dzięki temu otrzymujemy super narzędzie, które można dodatkowo przystosować do własnych potrzeb.

Ignorowane kolumny

Warto jeszcze pamiętać, by wykluczyć wszelkie dane, których nie chcemy by na nich również generowały się charakterystyki.

W tym przypadku utworzmy listę z indeksami klienta oraz zmienną modelowaną(TARGET).

ignore_variables = {'app': ['SK_ID_CURR', 'TARGET'], 
                      'bureau': ['SK_ID_CURR', 'SK_ID_BUREAU'],
                      'bureau_balance': ['bureaubalance_index','SK_ID_BUREAU'],
                      'previous': ['SK_ID_PREV','SK_ID_CURR'],
                      'cash': ['cash_index','SK_ID_PREV','SK_ID_CURR'],
                      'card': ['card_index','SK_ID_PREV','SK_ID_CURR'],
                      'installments': ['installments_index','SK_ID_PREV','SK_ID_CURR']
                     } 

Głęboka synteza funkcji (DFS- Deep Feature Synthesis)

Głęboka synteza funkcji (DFS) to metoda, którą Featuretools wykorzystuje do tworzenia nowych funkcji. DFS tworzy elementy o „głębokości” (max_depth) równej liczbie operacji podstawowych na prymitywach. Na przykład, jeśli weźmiemy maksymalną wartość wcześniejszych pożyczek klienta (powiedzmy MAX (previous.loan_amount)), będzie to „głęboka funkcja” o głębokości 1.

Aby utworzyć cechę o głębokości dwóch, możemy zestawić prymityw, przyjmując maksymalną wartość średnich miesięcznych płatności klienta na poprzednią pożyczkę MAX(previous(MEAN(installments.payment))). W przypadku ręcznego przygotowania wymaga to dwóch grupowań i agregacji.

Moim zdaniem olbrzymią zaletą jest możliwość wygenerowania jedynie listy charakterystyk bez ich fizycznego wyliczenia (features_only = True). Dzięki temu można spoglądnąć na listę i dopracować odpowiednio prymitywy tak jak będziemy potrzebować.

Poniżej zobaczcie jak zmienia się liczba charakterystyk wraz z zwiększaniem głębokości:

import random

for i in range(1,6):
    print('')
    print(f'Dla parametru max_depth = {i}')
    feature_names = ft.dfs(entityset = es, target_entity = 'app', 
                        trans_primitives = trans_primitives,
                        agg_primitives = agg_primitives, 
                        ignore_variables = ignore_variables,
                        max_depth = i, 
                        verbose = 1,
                        features_only = True)
    print(random.sample(feature_names,1))

Jeśli chcecie zmaterializować i wyliczyć charakterystki wystarczy zmienić parametr features_only na False.

# Deep feature synthesis - generating features
feature_calc = ft.dfs(entityset = es, target_entity = 'app', 
                    trans_primitives = trans_primitives,
                    agg_primitives = agg_primitives, 
                    ignore_variables = ignore_variables,
                    max_depth = 1, 
                    verbose = 1,
                    features_only = False)

Agregacja po specyficznych wartościach

Moim zdaniem warto wspomnieć o jeszcze jednej ważnej funkcjonalności. Aby utworzyć funkcje warunkowe, możemy ustawić interesujące wartości dla istniejących kolumn w danych. Dla przykładu jeśli chcielibyśmy policzyć funkcje na tabeli z biura (bureau) osobno dla statusach rachunków

print(bureau['CREDIT_ACTIVE'].unique())

wówczas należy określić to jako wartości interesujące:

es["bureau"]["CREDIT_ACTIVE"].interesting_values = ["Closed", "Active", "Sold", "Bad debt"]
es["bureau"]["CREDIT_TYPE"].interesting_values = ["Consumer credit", "Credit card", "Mortgage", "Car loan"]

Prymitywy używane dla operacji warunkowych są określone jako where_primitives w wywołaniu funkcji Deep Synthesis Feature, dlatego nie zapomnijcie ich określić:

where_primitives=["min", "max", "sum", "count"]

A tutaj przykładowe charakterystyki jakie otrzymaliśmy:

# Deep feature synthesis
feature_names = ft.dfs(entityset = es, target_entity = 'app', 
                       trans_primitives = trans_primitives,
                       agg_primitives = agg_primitives, 
                       where_primitives = where_primitives,
                       max_depth = 2, 
                       verbose = 1,
                       ignore_variables = ignore_variables,
                       features_only = True)

random.sample(feature_names,5)

Pierwsza zmienna posiada klauzure WHERE

Przy okazji zwróćcie uwagę, że wcześniej było około 4.400 cech a teraz prawie 5.800. Zatem dodając te kilka dodatkowych agregacji liczba charakterystyk zwiększyła się o blisko 1.400 (30%). Automatyzacja inżynierii cech pokazuje tutaj największą zaletę jak w 10 sekund można zwiększyć tak liczbę charakterystyk.

Kod na GIT

Cały notebook dostępny na moim GITcie: LINK

Podsumowanie

Mam głęboką nadzieję, że chociaż kilku z Was skorzysta omówionego przykładu i automatyzacja inżynierii cech pozwoli Wam zyskać kilka godzin dla siebie więcej 🙂

Pozdrawiam,

.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *