Лекція 12. Алгоритм FP-Growth. Пошук частих наборів без генерації кандидатів
Огляд
У Лекції 11 ми звели пошук асоціативних правил до двох кроків, з яких обчислювально важкий — пошук усіх частих наборів (наборів із ). Два алгоритми розв’язували цю задачу: Apriori (горизонтальний) будував кандидатів з’єднанням частих -наборів, відсіював тих, у кого є нечаста підмножина, і рахував решту окремим проходом по базі на кожному рівні; Eclat (вертикальний) уникав повторних проходів, дістаючи підтримку перетином tid-множин.
Обидва працюють, але мають спільну ваду: вони так чи інакше перебирають кандидатів. На щільних базах із багатьма частими елементами кандидатів стає астрономічно багато, а Apriori ще й сканує базу знову й знову. У цій лекції ми розберемо третій, найпоширеніший на практиці метод — FP-Growth (англ. Frequent Pattern Growth, «нарощування частих образів»). Його ідея інша: стиснути всю базу в компактне дерево — FP-дерево (англ. frequent pattern tree) — за два проходи, а тоді видобувати часті набори безпосередньо з дерева, зовсім не породжуючи кандидатів.
Практичний бік. FP-дерево ви будуватимете й «обходитимете» руками, а потім реалізуєте FP-Growth програмою в Лабораторній роботі 12. Далі — Лекція 13.
Щоб порівняння з Apriori було прямим, ми беремо ту саму наскрізну транзакційну базу, що й у Лекції 11 (шість чеків над п’ятьма товарами):
| Транзакція | Товари (набір елементів) |
|---|---|
| Хліб, Молоко, Яйця | |
| Хліб, Масло | |
| Хліб, Молоко, Масло, Яйця | |
| Молоко, Масло | |
| Хліб, Молоко, Масло | |
| Хліб, Молоко, Яйця, Кава |
Тут , множина товарів , а поріг залишаємо той самий — (набір частий, коли він є принаймні у з транзакцій). Наприкінці ми переконаємось, що FP-Growth знаходить точно ті самі часті набори, що й Apriori у Прикладі 11.5.
12.1 Обмеження Apriori
Apriori простий і надійний, але два його місця стають вузькими на великих даних.
- Вартість кандидатів. На кожному рівні генерується багато кандидатів, і переважна більшість зрештою виявляється нечастими — тобто робота на їхнє породження й підрахунок витрачена намарно. Уже на другому рівні кандидатів-пар близько ; якщо частих 1-наборів тисячі, це мільйони пар. А в найгіршому разі, щоб знайти один частий набір довжини , довелося б перебрати всі його підмножин як кандидатів проміжних рівнів — експоненційний вибух.
- Багаторазові проходи по базі. Щоб порахувати підтримку кандидатів рівня , Apriori сканує всю базу — і так на кожному рівні. Якщо найдовший частий набір має розмір , база читається разів. На базі, що не вміщується в пам’ять, ці повторні читання з диска коштують найдорожче.
Eclat прибирає повторні проходи (вертикальний формат, підтримка через перетини), але його tid-множини на щільних базах стають великими й важкими за пам’яттю.
Ідея FP-Growth. Замість «породити кандидатів — перевірити базою» зробити навпаки: один раз стиснути базу в дерево, що зберігає всю потрібну для підрахунку інформацію, а тоді читати часті набори прямо з нього. Тоді проходів по базі лише два (порахувати частоти елементів; вставити транзакції в дерево), а кандидати не породжуються взагалі.
12.2 FP-дерево
FP-дерево — це префіксне дерево транзакцій. Кожну транзакцію, залишивши в ній лише часті елементи й упорядкувавши їх за спаданням частоти, вставляють як шлях від кореня; транзакції зі спільним початком діляться спільними вузлами, а кожен вузол лічить, скільки транзакцій крізь нього пройшло.
Означення (FP-дерево). FP-дерево — це кореневе дерево, у якому: корінь позначено як
null; кожен інший вузол зберігає елемент і лічильник (скільки транзакцій проходить крізь цей вузол цим шляхом); а транзакція, обмежена частими елементами й упорядкована за спільним порядком , є шляхом від кореня. Вузли з тим самим елементом додатково з’єднані у список зв’язків між вузлами (англ. node-links).
Дерево будують за два проходи по базі.
- 1-й прохід — частоти й порядок. Порахувати підтримку кожного елемента, відкинути нечасті (за властивістю Apriori вони не ввійдуть у жоден частий набір), а решту впорядкувати за спаданням частоти. Цей упорядкований список частих елементів називають -списком; саме його порядок використовують далі скрізь.
- 2-й прохід — вставлення транзакцій. Для кожної транзакції залишити лише часті елементи, відсортувати їх за -списком і вставити як шлях від кореня: спільний із наявними гілками префікс нарощує лічильники вже наявних вузлів, а розбіжний «хвіст» додає нові вузли.
Чому саме за спаданням частоти? Тоді найпоширеніші елементи опиняються близько до кореня й діляться найбільшою кількістю транзакцій — це дає максимальне стиснення. Що частіше елементи трапляються разом, то компактніше дерево; у крайньому разі, коли всі транзакції однакові, воно вироджується в єдиний шлях.
Означення (заголовна таблиця). Заголовна таблиця (англ. header table) зберігає для кожного частого елемента його сумарну частоту й покажчик на перший його вузол у дереві. Усі вузли того самого елемента сполучено списком node-links. Таблиця дає змогу, узявши елемент, швидко обійти всі його появи в дереві — це основа етапу видобутку (§12.4–12.5).
Отже, FP-дерево + заголовна таблиця разом зберігають усю інформацію, потрібну для підрахунку підтримки будь-якого частого набору, — але вже без бази й без кандидатів.
12.3 Побудова FP-дерева (наскрізний приклад)
Побудуймо FP-дерево для наскрізної бази за (лічильник ).
1-й прохід — частоти елементів. Це той самий підрахунок, що й у Кроці 1 Apriori:
| Елемент | Лічильник | Частий? |
|---|---|---|
| Хліб | 5 | так |
| Молоко | 5 | так |
| Масло | 4 | так |
| Яйця | 3 | так |
| Кава | 1 | ні |
Кава відпадає. Решту впорядковуємо за спаданням частоти; за рівних частот (Хліб і Молоко — обидва ) зберігаємо початковий порядок елементів. Дістаємо -список:

2-й прохід — упорядкування транзакцій. Викидаємо нечасті елементи (Кава) і сортуємо кожну транзакцію за -списком:
| Транзакція | Початково | Часті елементи за -списком |
|---|---|---|
| Хліб, Молоко, Яйця | Хліб, Молоко, Яйця | |
| Хліб, Масло | Хліб, Масло | |
| Хліб, Молоко, Масло, Яйця | Хліб, Молоко, Масло, Яйця | |
| Молоко, Масло | Молоко, Масло | |
| Хліб, Молоко, Масло | Хліб, Молоко, Масло | |
| Хліб, Молоко, Яйця, Кава | Хліб, Молоко, Яйця |
Вставлення шляхів. Ідемо транзакціями по черзі. створює перший шлях
Хліб→Молоко→Яйця (усі лічильники ). ділить із ним корінь-вузол Хліб
(його лічильник стає ) і додає нову гілку Масло. подовжує гілку
Хліб→Молоко й добудовує Масло→Яйця. — важливий випадок: вона
починається з Молоко, а не з Хліб, тож спільного префікса з наявними гілками
немає — з’являється новий вузол Молоко просто під коренем. і
лягають уздовж наявної гілки Хліб→Молоко…, лише нарощуючи лічильники. Остаточне
дерево:
null
├─ Хліб:5
│ ├─ Молоко:4
│ │ ├─ Яйця:2
│ │ └─ Масло:2
│ │ └─ Яйця:1
│ └─ Масло:1
└─ Молоко:1
└─ Масло:1

Заголовна таблиця (частота елемента + його вузли, сполучені node-links):
| Елемент | Частота | Вузли дерева (за зв’язками) |
|---|---|---|
| Хліб | 5 | Хліб:5 |
| Молоко | 5 | Молоко:4 → Молоко:1 |
| Масло | 4 | Масло:2 → Масло:1 → Масло:1 |
| Яйця | 3 | Яйця:2 → Яйця:1 |

Корисна перевірка: сума лічильників усіх вузлів одного елемента дорівнює його частоті. Молоко: ; Масло: ; Яйця: — усе збігається з 1-м проходом. Зверніть увагу на стиснення: якби кожну транзакцію зберігати окремим шляхом, часті елементи шести транзакцій зайняли б вузлів, а завдяки спільним префіксам їх лише вісім.
Типова помилка (не відсортувати транзакцію за -списком). Якщо вставляти елементи транзакції у довільному порядку, транзакції зі спільними товарами підуть різними гілками, спільні префікси не зіллються, дерево розростеться, а заголовні зв’язки перестануть відповідати підтримкам. Єдиний спільний порядок (за спаданням частоти) — обов’язкова умова коректності й компактності.
12.4 Умовна база образів та умовне FP-дерево
Дерево збудовано; тепер із нього треба видобути часті набори. FP-Growth робить це методом «поділяй і володарюй»: для кожного частого елемента (починаючи з найрідшого — знизу заголовної таблиці) він розглядає лише ті частини дерева, що ведуть до , і зводить задачу до меншого дерева.
Означення (умовна база образів). Умовна база образів (англ. conditional pattern base) елемента — це множина префіксних шляхів усіх вузлів у дереві: для кожного вузла беруть шлях від кореня до нього, не включаючи сам , і приписують цьому шляху лічильник самого вузла .
Умовна база образів — це немовби «маленька транзакційна база», що описує, у якому оточенні трапляється .
Означення (умовне FP-дерево). Умовне FP-дерево елемента — це FP-дерево, побудоване з його умовної бази образів (кожен префіксний шлях — «транзакція» зі своїм лічильником), у якому лишено тільки ті елементи, що лишаються частими вже в межах цієї бази (їхня сумарна частота ).
Приклад 12.1 (умовна база й умовне дерево для Яйця). Найрідший частий елемент — Яйця (частота ). За заголовною таблицею його вузли:
- Яйця:2 — під
Хліб→Молоко; префіксний шлях з лічильником ; - Яйця:1 — під
Хліб→Молоко→Масло; префіксний шлях з лічильником .
Отже, умовна база образів Яйця:
Порахуймо частоти елементів у цій базі: Хліб , Молоко , Масло . Масло не набирає порога () — його відкидають. Лишаються Хліб() і Молоко(), і умовне FP-дерево Яйця — єдиний шлях:
null
└─ Хліб:3
└─ Молоко:3 (умовне дерево для Яйця)

Чому обхід іде знизу вгору (від найрідшого елемента)? Бо коли ми беремо , усі елементи, що стоять у -списку після нього, вже оброблено, і в префіксних шляхах їх немає — задача для чисто «дивиться вгору», на спільні префікси, і не перетинається з уже завершеними гілками.
12.5 Рекурсивний видобуток частих наборів
Тепер алгоритм цілком. Часті набори «нарощують» від суфікса: маючи вже знайдений набір (спершу порожній), для кожного елемента утворюють більший набір і рекурсивно шукають, чим його ще можна доповнити — у його умовному дереві.
FP-Growth(Дерево, α):
якщо Дерево — єдиний шлях P:
для кожної непорожньої підмножини β вузлів шляху P:
вивести набір β ∪ α
з підтримкою = найменший лічильник вузлів у β
інакше:
для кожного елемента i заголовної таблиці (знизу вгору):
β <- α ∪ { i }
вивести набір β з підтримкою = сумарний лічильник i
побудувати умовну базу образів i та умовне FP-дерево Дерево_i
якщо Дерево_i не порожнє:
FP-Growth(Дерево_i, β) # рекурсія: заглиблення
Гілка «єдиний шлях» — важлива оптимізація: якщо дерево (чи умовне дерево) виродилося в ланцюжок, часті набори з нього не треба шукати рекурсивно — можна одразу виписати всі комбінації його вузлів, а підтримкою комбінації є найменший з лічильників (найглибший вузол). Саме так закриється приклад із Яйця.
Приклад 12.2 (повний видобуток на наскрізній базі)
Обходимо заголовну таблицю знизу вгору: Яйця, Масло, Молоко, Хліб.
Яйця. Виводимо з підтримкою . Умовне дерево Яйця
(Приклад 12.1) — єдиний шлях Хліб:3 → Молоко:3. Виписуємо всі комбінації його
вузлів, приєднавши суфікс Яйця:
Масло. Виводимо з підтримкою . Вузли Масло дають умовну базу образів:
Частоти в базі: Хліб , Молоко — обидва часті. Але умовне дерево Масло не є єдиним шляхом (Молоко трапляється і під Хліб, і під коренем):
null
├─ Хліб:3
│ └─ Молоко:2
└─ Молоко:1 (умовне дерево для Масло)

Тому рекурсуємо в ньому (знизу вгору: Молоко, потім Хліб):
- Молоко в умовному дереві Масло: виводимо . Його умовна база тут — ; Хліб має лічильник , відпадає — далі порожньо.
- Хліб в умовному дереві Масло: виводимо . Хліб — під коренем, умовна база порожня.
Зверніть увагу: набір не з’явився — і правильно: він є лише в (підтримка ). Поріг застосовується наново всередині кожного умовного дерева, і саме він відсіяв Хліб на під-кроці Молоко.
Молоко. Виводимо . Умовна база — ;
умовне дерево — єдиний вузол Хліб:4, звідки .
Хліб. Виводимо . Це найчастіший елемент, він завжди під коренем — умовна база порожня, рекурсії немає.
Усі знайдені часті набори (лічильник ) і звірка з Apriori (Приклад 11.5):
| Частий набір | Підтримка | Звідки у FP-Growth | Apriori (Л11) |
|---|---|---|---|
| заголовна таблиця | так | ||
| заголовна таблиця | так | ||
| заголовна таблиця | так | ||
| заголовна таблиця | так | ||
| умовне дерево Молоко | так | ||
| умовне дерево Масло | так | ||
| умовне дерево Масло | так | ||
| умовне дерево Яйця | так | ||
| умовне дерево Яйця | так | ||
| умовне дерево Яйця | так |
Це точно той самий список частих наборів, що його знайшов Apriori у Прикладі 11.5, — і єдиний частий 3-набір знову . Але FP-Growth дійшов до нього без жодного кандидата й лише за два проходи по базі. Далі з цих частих наборів правила генерують так само, як у §11.6 (крок дешевий — усі потрібні підтримки вже пораховано).
Типова помилка (не перерахувати частоти в умовній базі). Умовна база образів — це вже інша маленька база, і поріг треба застосувати до неї заново. Якщо цього не зробити (лишити в умовному дереві елемент, що частий у всій базі, але не в цій умовній), з’являться зайві «часті» набори на кшталт , яких насправді немає.
12.6 Порівняння: Apriori, Eclat, FP-Growth
Усі три алгоритми знаходять той самий набір частих наборів — різняться лише тим, як вони рахують підтримку й чи породжують кандидатів.
| Ознака | Apriori | Eclat | FP-Growth |
|---|---|---|---|
| Формат бази | горизонтальний | вертикальний | горизонтальний дерево |
| Генерація кандидатів | так (з’єднання + відсів) | немає (перетини tid-множин) | немає (умовні дерева) |
| Проходів по базі | по одному на рівень () | (побудова tid-множин) | рівно |
| Головна пам’ять | мала (лічильники) | tid-множини (великі на щільних) | FP-дерево (компактне на щільних) |
| Спосіб підрахунку | прохід по базі щорівня | потужність перетину tid-множин | лічильники у вузлах дерева |

Коротко про сильні й слабкі сторони:
- Apriori — найпростіший і найощадливіший за пам’яттю; страждає від вибуху кандидатів і багатьох проходів. Добрий для невеликих або дуже розріджених баз і як еталон для перевірки.
- Eclat — прибирає повторні проходи (підтримка через перетини); швидкий на розріджених базах, але його tid-множини роздуваються на щільних.
- FP-Growth — узагалі не породжує кандидатів і робить два проходи; на щільних базах із багатьма спільними префіксами дерево виходить дуже компактним, і метод зазвичай найшвидший. Слабке місце — навпаки, розріджені бази з малим спільним префіксом: тоді дерево майже не стискається (гілок майже стільки ж, скільки транзакцій), а рекурсія з безліччю умовних дерев дорога.
Коли який. На щільних базах (багато частих елементів, довгі часті набори) переможець зазвичай FP-Growth — стиснення в дерево окупається. На розріджених базах виграє Eclat (малі tid-множини) або й простий Apriori. Якщо потрібна проста, передбачувана за пам’яттю реалізація чи еталон для звірки — беруть Apriori. Результат (часті набори) у всіх трьох однаковий — обирають за швидкодією й пам’яттю під конкретні дані.
Застосування в аналітиці даних
- Промисловий пошук частих наборів. Саме FP-Growth (та його паралельні версії) — стандарт для великих транзакційних баз рітейлу, де кандидати Apriori не поміщаються в пам’ять; він лежить в основі готових реалізацій частого аналізу образів у бібліотеках аналітики великих даних.
- Рекомендації й розкладка — у масштабі. Ті самі правила «з беруть » (Лекція 11), але дерево дає змогу знаходити часті набори на мільйонах чеків за прийнятний час.
- Веб-, лог- та текст-аналітика. Часті послідовності сторінок, спільні теги/слова, шаблони подій — щільні дані, де стиснення в дерево особливо вигідне.
- «Поділяй і володарюй» через умовні бази — загальний прийом: звести пошук образів до менших підзадач на стиснутому поданні даних; він застосовний і поза ринковим кошиком (часті підграфи, послідовності, епізоди).
Підсумок
- FP-Growth шукає часті набори без генерації кандидатів і лише за два проходи по базі, стискаючи її у FP-дерево.
- FP-дерево — префіксне дерево транзакцій: часті елементи кожної транзакції сортують за спаданням частоти (-список) і вставляють як шлях; спільні префікси діляться вузлами, кожен вузол має лічильник. Заголовна таблиця зі зв’язками між вузлами дає швидкий доступ до всіх появ елемента.
- Видобуток іде знизу вгору заголовною таблицею: для елемента будують умовну базу образів (префіксні шляхи його вузлів із їхніми лічильниками) та умовне FP-дерево (лишивши елементи, часті вже в цій базі), і рекурсивно повторюють; єдиний шлях дозволяє одразу виписати всі комбінації.
- Поріг застосовують наново всередині кожного умовного дерева — це й відсіює несправжні набори.
- На наскрізній базі () FP-Growth дає ті самі десять частих наборів, що й Apriori, з єдиним частим 3-набором — але без кандидатів.
- Apriori / Eclat / FP-Growth знаходять однакові часті набори; вибір — за щільністю бази: FP-Growth на щільних, Eclat/Apriori на розріджених.
Вправи
Для розігріву
- Поясніть, навіщо в 1-му проході відкидають нечасті елементи ще до побудови дерева. На яку властивість (Лекція 11) це спирається?
- Чому елементи в транзакції сортують саме за спаданням частоти, перш ніж вставляти в дерево? Що станеться зі стисненням, якщо взяти довільний порядок?
- За готовим FP-деревом наскрізної бази (§12.3) випишіть умовну базу образів для елемента Молоко і побудуйте його умовне FP-дерево.
Стандартні
- Побудуйте FP-дерево для наскрізної бази за нижчим порогом (лічильник ): наведіть новий -список (тепер і Масло, і, можливо, інші пари стануть частими), відсортовані транзакції й остаточне дерево. Порівняйте з деревом за .
- Видобудьте всі часті набори з дерева, побудованого у вправі 4, обходячи заголовну таблицю знизу вгору. Чи стане частим набір ? Звірте результат з Apriori за тим самим порогом (вправа 5 Лекції 11).
- Для елемента Масло (за ) покажіть, чому його умовне дерево не є єдиним шляхом, і простежте рекурсію крок за кроком. Поясніть, на якому під-кроці й чому відсівається Хліб.
Підвищеної складності
- Доведіть, що сума лічильників усіх вузлів одного елемента у FP-дереві дорівнює підтримці (лічильнику) цього елемента. Спираючись на це, поясніть, чому підтримка будь-якого частого набору коректно відновлюється з умовних дерев.
- Оцініть кількість вузлів FP-дерева у двох крайніх випадках: (а) усі транзакцій однакові; (б) жодні дві транзакції не мають спільного префікса (після впорядкування). Як ці випадки пояснюють, чому FP-Growth виграє на щільних і програє на розріджених базах?
- Порівняйте обсяг роботи Apriori та FP-Growth на наскрізній базі: скільки кандидатів порахував Apriori (Приклад 11.5) і скільки умовних дерев побудував FP-Growth (Приклад 12.2)? Запропонуйте, як експериментально виміряти їхню швидкодію на дедалі більших випадкових базах і на яких даних очікувати переваги кожного (див. високий рівень Лабораторної 12).