Algorytmy Apriori i Partition C++, Linux Ewa Kowalczuk, Piotr Śniegowski, Artur Osesik 14 czerwca 11 1 Zastosowane metody poprawy efektywności Reprezentacja zbioru Dla stworzenia efektywnie działającego algorytmu bardzo wygodnym podejściem okazało się przygotowanie działającej implementacji wstępnej, a następnie poprawa jej efektywności, przy jednoczesnym zwracaniu uwagi na zachowanie poprawności. Ze względu na wygodę implementacji klasa Set (reprezentująca zbiór) została początkowo zaimplementowana jako dziedzicząca publicznie po std::list<item>. Poprawy efektywności dokonano poprzez reimplementację klasy Set jako wektora elementów. Dzięki konsekwentnemu stosowaniu zasady enkapsulacji, wymagało to jedynie zmian w klasie Set, to znaczy reimplementacji jej interfejsu składającego się m. in. z typu iterator zdefiniowanego jako Item * oraz metod takich jak iterator begin() (wcześniej odziedziczonych po std::list). Wsparcie dla częstych operacji Klasa reprezentująca zbiór, wykorzystywana zarówno do przechowywania transakcji jak i zbiorów częstych, posiadała pole będące 64-bitowym kluczem hashującym. Nie był on jednak wykorzystywany do adresacji, ale do przyspieszenia sprawdzania relacji równości i zawierania się zbiorów. W kluczu i-ty bit był zapalony, jeśli w zbiorze istniał element o reszcie z dzielenia przez 64 równej i. Dzięki takiej konstrukcji, operacje na kluczu były bardzo proste: A := A {e} A.klucz = (1 << (e % 64)) A.klucz!= B.klucz A B A.klucz & B.klucz A B Z powodu oczywistych kolizji, nierówność kluczy jedynie implikuje nierówność zbiorów nie jest jej równoważna. Jeśli warunek zdefiniowany na kluczach jest spełniony, należy dokonać równoczesnej iteracji po zbiorach i porównywać kolejne pary elementów. Inne potencjalne reprezentacje Skuteczność i elegancja mechanizmu bitowych hashy może zachęcać do próby jednoznacznego reprezentowania transakcji w postaci bitowej. Jednakże wadą czystej reprezentacji bitowej jest fakt, iż długość reprezentacji dla rzadkich transakcji byłaby niewspółmierna do informacji w nich zawartej. Wartymi sprawdzenia mogłyby okazać się rozwiązania, w których zbiór przechowywany jest w zwykły sposób jako wektor elementów, ale jeśli jest wystarczająco gęsty (i posiada elementów o zbyt dużych numerach), utrzymywana jest także jego reprezentacja bitowa. Wówczas, jeśli obydwa porównywane zbiory posiadają reprezentację bitową 1
(są odpowiednio gęste), prawdziwość relacji sprawdzana jest za pomocą tych reprezentacji a odpowiedź jest jednoznaczna (w przeciwieństwie do rozwiązania z kluczami). Rozwiązanie takie jest elastyczne w tym sensie, że dla zbioru zawierającego transakcje głównie gęste, będzie często korzystała ze zoptymalizowanych operacji, natomiast wciąż będzie nadawała się do użycia dla zbiorów danych o rzadkich transakcjach. Rozwiązanie takie nie stanowi optymalizacji pamięciowej wprowadza drugą reprezentację, lecz może być poważną optymalizacją czasową. Drugim potencjalnym rozwiązaniem jest reprezentacja mieszana: przechowywana jest lista bloków o interpretacji bitowej, jednakże tylko tych, w których znajduje się co najmniej jeden element. Wówczas konieczne jest przechowywanie dodatkowej liczby reprezentującej number bloku, co stanowi dodatkowy koszt pamięciowy. Warto zauważyć, iż taka reprezentacja byłaby odporna na problem zmiennej mocy zbioru elementów tworzących transakcje. 2
2 Wyniki Warunki badania Przebadano działanie trzech algorytmów: Apriori, Partition oraz zrównoleglonego Partition dla trzech zbiorów danych: accidents, kosarak oraz mushroom, dla różnych poziomów wsparcia. Wszystkie badania przeprowadzono dla poziomu ufności.75. Badania przeprowadzono na komputerze wyposażonym w: ˆ procesor Intel Core i3 M33, 2.13GHz, z technologią Hyper-Threading (2 rdzenie fizyczne, 4 logiczne), ˆ 4GB pamięci RAM z zainstalowaną dystrybucją Debian systemu Linux oraz kompilatorem gcc w wersji 4.4.5. Program skompilowano z włączonymi optymalizacjami. Czas działania Wykresy 1, 2 oraz 3 przedstawiają czas działania algorytmów Apriori oraz Partition kolejno dla zbiorów accidents, kosarak oraz mushroom, przy czym dla algorytmu Partition stanowią średnią dla liczby podziałów na partycje wynoszących: 2, 3 oraz 4. W przypadku zbioru mushroom algorytm Partition jest o około 2 rzędy wielkości gorszy od algorytmu Apriori. Dla zbiorów accidents oraz kosarak można jednak zaobserwować wyraźną konkurencję między dwoma rozwiązaniami. W przypadku zbioru accidents początkowo szybszy jest Apriori. Jest to prawdopodobnie skutkiem stosunkowo niskiego poziomu ufności, który owocuje większą liczbą znajdowanych zbiorów częstych dla algorytmu Partition oznacza to jednak także większą liczbę kandydujących zbiorów częstych, które okażą się fałszywymi w drugiej fazie działania. Wraz ze zwiększaniem progu wsparcia różnica pomiędzy algorytmami zmniejsza się, aż dla wartości ok..77 algorytm Partition zdobywa prowadzenie. Jego zaleta polegająca na jedynie dwukrotnym odczycie bazy danych, zaczyna od tego momentu przeważać nad kosztem weryfikacji fałszywych zbiorów częstych. Przy zastosowaniu wystarczająco dużego progu wsparcia znalezienie jakiejkolwiek reguły okazuje się niemożliwe. Jeśli algorytm Apriori nie znajdzie żadnego jednoelementowego zbioru częstego (lub znajdzie taki tylko jeden), wówczas nie wygeneruje on żadnych dwuelementowych zbiorów kandydujących, a zatem nie dokona drugiego odczytu bazy danych. Algorytm Partition wciąż jednak będzie w stanie znaleźć zbiory lokalnie częste, które będzie musiał zweryfikować w drugiej fazie, dokonując drugiego odczytu. Jest to zauważalne w okolicach progu wsparcia równego.16 w zbiorze kosarak; zapewne nastąpi to także dla zbioru accidents dla poziomu wsparcia większego niż.95. Rozpatrywanie tak dużych poziomów wsparcia nosi jednak znamiona dyskusji akademickiej. Liczba reguł, zajętość pamięci, liczba podziałów Wykres 4 przedstawia liczbę znajdowanych reguł w funkcji progu wsparcia (przy progu ufności równym.75) dla badanych zbiorów. Wyraźnie widać zupełnie różny charakter tych zbiorów. Wykres 7 przedstawia średnią zajętość pamięci algorytmów Apriori oraz sekwencyjnego Partition, przy czym dla tego drugiego wielkość partycji wynosiła 1/4 całego zbioru transakcji. Wykresy 5 oraz 6 przedstawiają czas działania algorytmu Partition w zależności od liczby podziałów na partycje. Dla zbioru accidents widoczne są niewielki różnice nasilające się 3
11 9 7 5 3 1.65.7.75.8.85.9.95 Rysunek 1: zbiór accidents 11 9 7 5 3 1.5.1.15.2.25.3 Rysunek 2: zbiór kosarak 1.4.45.5.55.6.65.7 Rysunek 3: zbiór mushroom wraz ze zmniejszaniem progu wsparcia. Jak już opisano wcześniej, im mniejszy próg wsparcia, tym więcej w pierwszej fazie działania algorytmu Partition zostanie wygenerowanych zbiorów lokalnie częstych, których globalną częstość będzie trzeba zweryfikować w drugiej fazie. Liczba ta w oczywisty sposób zwiększa się także ze zwiększaniem liczby podziałów. Zbiór, który nie był częsty w określonym zbiorze transakcji, może okazać się 4
1 1 accidents kosarak mushroom 1 liczba reguł 1.1.2.3.4.5.6.7.8.9 1 Rysunek 4: liczba reguł częsty w pewnym podzbiorze tego zbioru. Dla problemu mushroom zjawisko to jest szczególnie bolesne dla liczby podziałów wynoszącej 4. 1 2 partycje 3 partycje 4 partycje.65.7.75.8.85.9.95 Rysunek 5: liczba podziałów zbiór accidents 1 1 1 2 partycje 3 partycje 4 partycje 1.4.45.5.55.6.65.7 Rysunek 6: liczba podziałów zbiór mushroom 5
Zajętość pamięciowa maksymalna zajętość pamięci [kb] 1 1 1 1 accidents mushroom kosarak accidents mushroom kosarak.1.2.3.4.5.6.7.8.9 1 Rysunek 7: zajętość pamięci Wykres 7 przedstawia maksymalną zajętość pamięci dla poszczególnych zbiorów danych i algorytmów. Dość nieintuicyjny wydaje się fakt, iż dla zbiorów accidents oraz kosarak zajętość pamięciowa Partition jest mniejsza niż Apriori. Zjawisko to można próbować wyjaśnić w następujący sposób. Momentem, w którym algorytm Apriori zajmował najwięcej pamięci, mogła być chwila, gdy wygenerowane zostały wszystkie kandydujące zbiory częste o określonej liczności x. Partition, ze względu na dzielenie zbioru danych, nie przechowuje jednocześnie wszystkich możliwych x-kandydatów. Przed wygenerowaniem kolejnej ich partii, dawno już zweryfikowani są (i zapewne w większości odrzuceni) x-kandydaci z poprzedniej partycji. Teoretycznie możliwe jest, iż ilość danych wykorzystana na przechowywanie potencjalnych zbiorów częstych z poprzednich partycji oraz tych danych, na których algorytm Partition aktualnie operuje, nie przewyższyła wielkością zbioru x-kandydatów przechowywanego w pewnym momencie czasu przez Apriori. 6
Równoległe Partition Wykres 8 przedstawia porównanie działania algorytmów Apriori, sekwencyjnego Partition oraz zrównoleglonego Partition przy wykorzystaniu coraz większej liczby wątków. Ponieważ badane algorytmy zakładają, że dane mogą potencjalnie nie mieścić się w pamięci operacyjnej, zadbano o utrzymanie tego ograniczenia (choć dla badanych zbiorów na zastosowanej maszynie były one sztuczne). Innymi słowy, jeśli algorytmy Apriori i sekwencyjny Partition wczytywały za jednym razem do pamięci partycję o określonej wielkości, to dwuwątkowy Partition operował na partycjach dwukrotnie mniejszych. Warto zauważyć, że zrównoleglenie algorytmu Partition miało bardzo prosty charakter, jedynymi miejscami synchronizacji był odczyt danych, faza sumowania zbiorów kandydujących zbiorów częstych pomiędzy etapami przetwarzania oraz zsumowanie liczników na końcu działania algorytmu znajdowania zbiorów częstych (generowanie reguł nie zostało zrównoleglone). W przypadku wystarczająco dużej objętości danych wejściowych, warto byłoby rozważyć implementację algorytmu Partition w środowisku klastrowym przy użyciu bibliotek PVM lub MPI. 1 1 (sekwencyjnie) (2 wątki) (3 wątki) (4 wątki).65.7.75.8.85.9.95 Rysunek 8: sekwencyjnie i równolegle zbiór accidents 7
Symulacja opóźnień odczytu Ze względu na fakt, że potencjalna przewaga algorytmu Partition nad innymi rozwiązaniami problemu znajdowania reguł asocjacyjnych polega na jedynie dwukrotnym odczycie całej bazy danych, jego sprawność względnym innych algorytmów w istotny sposób zależy od charakteru operacji odczytu danych. Podczas badań dane odczytywane były z tekstowego pliku znajdującego się lokalnie na dysku twardym, proces wczytywania danych został dodatkowo zoptymalizowany; skutkiem tego w uzyskanych i przedstawionych powyżej wynikach przewaga algorytmu Partition nad Apriori była nieduża lub nie występowała. Dokonano zatem dodatkowych badań wprowadzając kary za odczyt każdego elementu oraz transakcji jako całości (ich wartości dobrano w sposób arbitralny). Symulują one np. dostęp do zdalnej bazy danych. Opóźnienia określone jako niewielkie : to.1µs dla każdego elementu oraz dodatkowo 3µs dla każdej transakcji, kary duże to odpowiednio.4µs oraz 12µs (czyli czterokrotnie więcej). Wykresy 9, 1 oraz 11 przedstawiają porównanie pomiędzy algorytmami Apriori oraz sekwencyjnym Partition kolejno przy braku opóźnień, niewielkich opóźnieniach i dużych opóźnieniach. Wyraźnie widać, że wraz ze zwiększaniem kosztu odczytu danych, przewaga algorytmu Partition wzrasta. 1 1 1.65.7.75.8.85.9.95 Rysunek 9: Apriori i Partition brak opóźnień 1 1 1.65.7.75.8.85.9.95 Rysunek 1: Apriori i Partition niewielkie opóźnienia 8
1 1 1.65.7.75.8.85.9.95 Rysunek 11: Apriori i Partition duże opóźnienia 9