Sprawozdanie z laboratorium: Hurtownie Danych Algorytm generowania reguł asocjacyjnych 9 czerwca 2011 Prowadzący: dr inż. Izabela Szczęch dr inż. Szymon Wilk Autorzy: Łukasz Idkowiak Tomasz Kamiński Jacek Szcześniak Wacław Łuczak Zajęcia czwartkowe, 11:45.
1 Opis implementacji 2.1 Opis struktur Algorytm został zaimplementowany w języku C# na platformie.net. Kod składa się z kilku klas, wśród których najważniejsze to: FPTree - klasa przechowują strukturę FP-drzewa, implementuje ona również operacje eksploracji FP-drzewa (procedurę FPGrowth) i generowanie reguł z odkrytych zbiorów częstych FPNode - klasa obiektów reprezentujących węzły w FP-drzewie. Drzewo, tworzone na etapie kompresji, składa się z węzłów klasy FPNode. Obiekt klasy FPTree zawiera referencję na jeden obiekt-węzeł (_root), stanowiący korzeń całego drzewa. /// <summary> /// Pojedynczy węzeł w strukturze FP-drzewa /// </summary> private class FPNode { public int? Label { get; private set; } public int Count { get; set; } public FPNode Next { get; set; } public FPNode Parent { get; private set; } public List<FPNode> Children { get; private set; } public FPNode(int? label, FPNode parent = null, int count = 1) { (...) } } private FPNode _root; Znaczenie pól jest następujące: Label - etykieta liczbowa elementu Count - wsparcie dla tego węzła Next - referencja do następnego węzła o takiej samej etykiecie Parent - referencja do rodzica węzła Children - lista referencji do dzieci węzła Tablica nagłówkowa (header table) zrealizowana jest w postaci tablicy struktur, które zawierają referencje do pierwszego i ostatniego elementu na liście: /// <summary> /// Węzeł tablicy nagłówkowej /// </summary> private class HeaderNode { 2
public FPNode Head { get; set; } public FPNode Tail { get; set; } } private HeaderNode[] _headertable; Pola tej tablicy indeksowane są etykietami elementów. Dzięki polu Next w każdym węźle drzewa, węzły o takich samych etykietach pogrupowane są w listy, do których dostęp możliwy jest właśnie dzięki tablicy nagłówkowej. Korzystanie z takiej tablicy ułatwia eksplorowanie drzewa. 2.2 Opis działania Program można podzielić na cztery części: wczytanie elementów z pliku - opis zostanie pominięty kompresja bazy transakcji (utworzenie FP-drzewa) generowanie zbiorów częstych (eksploracja FP-drzewa) generowanie reguł do wskazanego pliku 2.2.1 Kompresja bazy danych Kompresja bazy transakcji jest początkowym etapem, którego celem jest wygenerowanie struktury drzewiastej zawierającej pełną informację o wsparciu każdej transakcji (zawierającej co najmniej jeden element o dostatecznie wysokim wsparciu), jednocześnie zachowując wysoki stopień kompresji w stosunku do wejściowego zbioru transakcji - strukturą tą jest FP-drzewo. Etap ten dzieli się na dwa podetapy. Najpierw usuwane są te reguły, których wsparcie nie przekracza zadanej wartości minsupp; reguły, które pozostały, sortuje się następnie malejąco wg częstości i rosnąco wg etykiet liczbowych (aby zapewnić spójny porządek w każdym przypadku). Skompresowana baza transakcji, zapisana w nowej liście jest następnie podawana jako parametr konstruktorowi klasy FPTree i w dalszej kolejności generowane jest FP-drzewo. Generowanie FP-drzewa odbywa się iteracyjnie dla każdej transakcji: wywoływana jest procedura addtransaction, która implementuje iteracyjne dodawanie nowych (o wsparciu równym jeden) bądź inkrementowanie wsparcia węzłów o etykiecie bieżącego elementu transakcji, przy czym każdy kolejny węzeł jest dzieckiem poprzedniego. Po zakończeniu tej procedury, dodana transakcja ma swoją ścieżkę od korzenia do pewnego węzła (niekoniecznie liścia), na której kolejne węzły mają etykiety równe etykietom kolejnych elementów z transakcji. Każdy nowo utworzony węzeł dopisywany jest też do końca listy, wskazywanej przez tablicę nagłówkową na pozycji odpowiadającej etykiecie tego węzła. 2.2. Generowanie zbiorów częstych: Zbiory częste generowane są podczas eksploracji FP-drzewa w procedurze rekurencyjnej FPGrowth. Przetwarzanie aplikacji dokładnie odpowiada schematowi, przedstawionemu w następującym algorytmie: 3
Procedura generowania podzbiorów danego zbioru częstego ma następujących schemat: 1. var items = new List<int> {2, 4, 6}; 2. var indexstack = new Stack<int>(); 3. int currentindex = 0; 4. while (true) { 5. if (currentindex < items.count) 6. stack.push(currentindex++); 7. else if (indexstack.count > 0) { 8. printstack(indexstack, items); 9. currentindex = indexstack.pop() + 1; 10. } else break; 11. } Fragment ten odpowiada za wypisanie wszystkich podzbiorów zbioru trzech elementów: 2, 4, 6. Jest to szkielet kodu, który ma za zadanie pokazać iteracyjny sposób wyznaczania kombinacji w czasie wykładniczym O(2 n ). W implementacji zamiast instrukcji wypisującej jest dodanie zbioru częstego do kolekcji zbiorów częstych. Zbiory częste przechowywane są w następującej strukturze: 1. private class ListComparer : IComparer<int[]> 2. { 3. public int Compare(int[] x, int[] y) 4. { 5. /* tu porównanie wg: 6. * 1. referencji 7. * 2. długości tablic 8. * 3. wartości elementów 9. */ 10. } 11. } 12. public SortedDictionary<int[], int> Directory { get; private set;} 13. public FrequentSetDictionary() 14. { 4
15. Directory = new SortedDictionary<int[], int>(new ListComparer()); 16. } Strukturą wewnętrzną jest klasa SortedDictionary, w której elementami są wsparcia poszczególnych zbiorów częstych, które indeksowane są instancjami tych zbiorów (kluczem jest tablica liczb, odpowiadających elementom zbiorów). Procedura Compare służy do porównywania dwóch zbiorów częstych: najpierw wg identyczności referencji, następnie wg długości tablic (krótsza tablica jest w porządku przed dłuższą) i na końcu porównywane są wartości na poszczególnych pozycjach. 2.3. Generowanie reguł Algorytm generowania reguł został zaimplementowany wprost: Dla każdego zbioru częstego (iteracja po elementach słownika SortedDictionary) wyznacza się wszystkie podzbiory (algorytmem podanym wcześniej) Jeśli reguła ma odpowiednią ufność (wsparcie potrzebne do jej obliczenia szybko wyszukuje się w słowniku), to jest wypisywana do pliku. 3 Eksperyment 3.1 Opis Celem eksperymentu było porównanie szybkości oraz zajętości pamięciowej algorytmów apriori oraz. Zostały wykorzystane 3 zbiory testowe: accidents, mushroom, kosarak. W eksperymencie nie były generowane reguły. Poziom ufności wynosił 0, poziom wsparcia od 0,9 do 0,2 ze skokiem 0,05. Maszyną testową był laptop o parametrach: procesor: Intel Core Quad CPU Q9000, 2,00Ghz; pamięć: 3GB pamięci RAM; system operacyjny: Windows 32-bitowy. 3.1 Wyniki Porównanie czasu działania: 5
Czas[s] Czas[s] Czas działania dla zbioru accidents 1200,0000 1000,0000 800,0000 600,0000 400,0000 200,0000 0,0000 0,2885 0,4113 0,5493 0,6812 0,8382 0,9896 1,2838 1,5215 2,4627 4,8084 6,7159 9,1733 12,975 24,969 53,466 4,3288 6,7420 14,194 30,033 52,061 125,82 292,74 558,40 1108,0 Wsparcie Czas działania dla zbioru kosarak 3,5000 3,0000 2,5000 2,0000 1,5000 1,0000 0,5000 0,0000 0,0282 0,0287 0,0283 0,0281 0,0277 0,0282 0,1821 0,2115 0,1842 0,3007 0,2960 0,3769 0,3838 0,3817 0,3817 2,8085 2,8337 2,7771 2,8054 2,8288 2,6326 2,8714 2,5824 2,8076 3,0557 3,0513 3,2961 3,2916 3,2964 3,2651 Wsparcie 6
Czas[s] Czas działania dla zbioru mushroom 90,0000 80,0000 70,0000 60,0000 50,0000 40,0000 30,0000 20,0000 10,0000 0,0000 0,0438 0,0442 0,0452 0,0465 0,0443 0,0459 0,0476 0,0498 0,0515 0,0567 0,0606 0,3769 0,0890 0,1070 0,2649 0,0657 0,0728 0,0844 0,0952 0,0974 0,1081 0,1352 0,2070 0,3086 0,6034 0,8570 1,9113 3,9292 9,1599 76,999 Wsparcie 7
Pamięć[MB] Pamięć[MB] Zajętość pamięci Zużycie pamięci dla zbioru accidents 700,00 600,00 500,00 400,00 300,00 200,00 100,00 0,00 126,01 126,00 133,07 136,15 141,10 142,59 157,18 157,99 172,68 205,36 250,83 285,57 335,25 398,53 644,58 43,26 43,31 42,80 42,90 42,84 42,84 42,79 43,05 43,48 Wsprarcie Zużycie pamięci dla zbioru mushroom 25,00 20,00 15,00 10,00 5,00 0,00 13,95 13,95 14,24 14,24 14,38 14,29 14,34 14,58 14,94 15,32 15,52 15,75 15,75 15,83 22,64 11,82 11,84 11,84 11,84 11,84 11,86 11,85 11,84 11,93 11,81 11,84 12,23 14,22 16,15 23,34 Wsprarcie 8
Pamięć[MB] Zużycie pamięci dla zbioru kosarak 160,00 140,00 120,00 100,00 80,00 60,00 40,00 20,00 0,00 103,11 103,12 103,13 103,10 103,11 103,09 132,73 132,82 132,74 143,70 143,64 147,84 147,97 147,91 147,91 52,06 49,31 49,20 49,20 52,37 52,28 49,48 52,27 52,01 51,32 54,42 56,77 54,29 54,37 54,38 Wsprarcie 3.2 Wnioski Dla wszystkich zadanych zbiorów, algorytm działa zdecydowanie sprawniej czasowo niż klasyczny A-priori. Jest to szczególnie podkreślone poprzez zmiany poziomu wsparcia podczas eksperymentu. Każde niewielkie zmniejszenie wsparcia może znacząco wydłużyć działanie programu - szczególnie widoczne w A- priori którego złożoność rośnie wykładniczo. Przedstawiona implementacja FP-growth została napisana w środowisku Microsoft.NET, w związku z czym programiści zostali odciążeni od ręcznego zarządzania pamięcią. Uniemożliwiło to precyzyjną alokację i dealokację zasobów pamięciowych, czego konsekwencją jest duże zapotrzebowanie implementacji na pamięć. Ma to związek z rekurencyjną naturą algorytmu; w czasie działania procedury FP-growth wielokrotnie tworzone są struktury dynamiczne, które nie są zwalniane podczas wchodzenia do rekurencji. 9