Перші кроки в NLP: розглядаємо Python-бібліотеку NLTK в реальному завданні
Усім привіт! Звати мене Андрій, і я 8 років працюю в оцінці майна, в жодній ІТ-компанії не працював, але люблю програмувати. Як хобі я почав готувати власний проєкт — систему для зручного й ефективного відображення оголошень з продажу нерухомості в Україні з різноманітних загальнодоступних джерел. У результаті проєкт «виріс» до доволі великих розмірів і включає систему збору й попередньої обробки оголошень, систему знаходження їхніх геокоординат, систему класифікації оголошень за певними типами й невеликий користувацький інтерфейс, написаний на Django . А в цій статті я спробую детально розповісти про реалізацію однієї з його частин, а саме класифікацію оголошень з продажу земельних ділянок в Україні за допомогою методів NLP.
Спочатку під час роботи над проєктом я поставивши перед собою ціль ознайомитися з різноманітними бібліотеками Python на реальному завданні. Мета написання статті — поділитися набутимо досвідом, враховуючи, що в українськомовному сегменті мережі таких статей, на мій погляд, недостатньо. У результаті вийшов огляд можливостей кількох бібліотек Python для машинного навчання, а саму статтю довелося розділити на три частини. Я наведу приклади реалізації класифікаторів, написаних за допомогою бібліотек NLTK, scikit-learn і TensorFlow.
Перша частина статті включає перше знайомство з даними й приклад побудови класифікаторів на основі бібліотеки NLTK.
Друга частина міститиме приклад побудови класифікаторів на основі бібліотеки scikit-learn.
А третя частина охопить кілька прикладів нейронних мереж, побудованих з використанням бібліотеки TensorFlow.
Стаття розрахована, передусім, на початківців у машинному навчанні (як і я), але сподіваюся, що вона буде цікавою й для людей, що підготували не один проєкт. Прочитавши її, можна буде отримати інформацію про можливості бібліотек, а також пов'язані з ними підводні камені. Вважаю, що наведених прикладів буде достатня для початку роботи з цими бібліотеками.
Отже, починаймо.
Постановка проблеми й знайомство з даними
На одному з етапів проєкту в мене виникла необхідність розробки фільтрів для зручної фільтрації оголошень (наприклад, за площею, вартістю, типами, адресою тощо). Скажу більше, мені як оцінювачу необхідно постійно шукати оголошення з продажу різноманітного майна, альо такі оголошення розкидані по різних джерелах, кожне з яких має свої логіку й систему пошуку та фільтрування (і далеко не завжди ця система відповідає моїм потребам). Увесь проєкт був задуманий для спрощення й автоматизації пошуку та сортування оголошень з продажу нерухомості з різних джерел, а також спрощення можливе лише за умови наявності якісніх фільтрів (звичайно, з точки зору оцінювача). Вже під час розробки частини фільтрів стало зрозуміло, що сортувати деякі оголошення без їхнього додаткового аналізу неможливо. Перш за все, це стосується оголошень з продажу земельних ділянок.
В Україні земельні ділянки поділяють за цільовим призначенням на 119 видів. Мені для роботи така детальна класифікація не потрібна, тому я виділив 7 типів (класів) земельних ділянок. Вісь смороду:
Таблиця 1. Тіпі земельних ділянок
ID | Тип ділянки | Коротка назва |
1 | Для будівництва й обслуговування житлового будинку, господарських будівель і споруд (присадибна ділянка) | ОЖБ |
2 | Для ведення особистого селянського господарства й подібні | ОСГ |
3 | Для індивідуального й колективного садівництва та дачного будівництва | Садова |
4 | Під комерційну діяльність та інші, що не ввійшли до решти груп | Комерційна |
5 | Під багатоквартирний будинок | Багатоповерхова |
6 | Для ведення товарного сільського господарства й подібні | Товарна |
7 | Одна з ділянок під ОЖБ, а одна під ОСГ | ОЖБ й ОСГ |
В оголошеннях можуть продавати як одну, так і кілька земельних ділянок з різним цільовим призначення.
У різних джерелах тіпі земельних ділянок мають різні назви або їх узагалі нема, а якщо продаються кілька ділянок з різним призначенням, цю інформацію не вказують.
Крім того, виникає інша проблема з оголошеннями з продажу забудованих земельних ділянок. Під час оцінювання майна забудовані земельні ділянки використовуються рідко й потрібен фільтр, який їх буде відкидати. Станом на тепер жодне з джерел інформації не підтримує можливість фільтрувати оголошення про продаж земельних ділянок щодо забудованості. Таким чином зрозуміти, забудована ділянка чи ні, можна лише перечитавши опис оголошення або переглянувши прикріплені фото.
Отже, постають дві проблеми класифікації земельних ділянок згідно з описом оголошень: за типом земельних ділянок (приклад мультикласової класифікації) та за їхньою забудованістю (приклад бінарної класифікації), а подібні задачі вирішуються методами NLP.
Нагадаю, завдання класифікації — це категорія методів машинного навчання з учителем , суть якої полягає в ідентифікації категоріальних міток класів для нових екземплярів на основі попередніх спостережень. Назва класу являє собою дискретне, невпорядковане значення, яке можна розуміти як належність до певної групи екземплярів.
Для вирішення поставлених завдань потрібно було підготувати певну кількість вже класифікованих (помічених) оголошень, котрі можна було б передати в ту чи іншу систему машинного навчання. Я сам робить розмітку даних (і за певний годину перевірку). Загальна кількість підібраних оголошень ставити 4256 екземплярів. Нині, звичайно, така кількість екземплярів мізерна (або навіть сміховинна), але тут потрібно пам " ятати, що ми говоримо про оголошення з продажу земельних ділянок — вузькоспеціалізовану галузь, де визначити тип ділянки можна за обмеженою кількістю слів.
Погляньмо на перші десять оголошень, що я підібрав (весь код тут ):
Таблиця 2. Перші оголошення з набору
Важливо зауважити, що всі представлені оголошення підготовлено й сформовано за допомогою програм, що я написав особисто (усі текстові поля до оголошень спочатку розбито токенайзером на списки з відкиданням усіх неалфавітних символів, а потім конкатеновані тому, зразок токенайзера й пояснення я наведу нижче).
Детальніше розглянємо кожну колонку.
«land_types_source» — тип ділянки визначено або невизначено в джерелі інформації.
«land_types_PcmU» — цільове призначення земельних ділянок, згідно з інформацією Публічної кадастрової карти України . Коротко про що йдеться — частина оголошень містить кадастрові номери ділянок. За такими номерами можна визначити тип ділянки, однак кадастрові номери є лише в частині оголошень, і за ними далеко не завжди можливо визначити таку інформацію. Звичайно, якщо така інформація є, то це майже ідеальний класифікатор цільового призначення ділянок, що унеможливлює потребу в побудові якогось іншого класифікатора.
«description» — поле з описом оголошення. З цього поля видалено всі цифри, номери, розділові знаки тощо. Залишено лише слова (букви), розділені пробілом. Інформація з поля «description» — лише результат роботи програми, що я розробив (зразок наведу нижче).
«built_up» — поле, що показує: ділянку забудовано (цифра «1») чи незабудовано (цифра «0»).
«land_types» — поле, що показує тип (клас) земельних ділянок в оголошенні (id типів наведено в таблиці 1).
«land_squer» — показує площу ділянок в оголошенні.
З даними ми ознайомилися. Погляньмо на розподіл оголошень за класами:
Рис. 1. Розподіл оголошень за класами
Як бачимо, у датасеті є дисбаланс між класами. Оголошення з продажу забудованих земельних ділянок становлять лише невелику частину всіх наявних оголошень, а серед типів земельних ділянок ситуація ще гірша: земельні ділянки під житлове будівництво й садові ділянки становлять 82,78% наявних оголошень, а оголошення з продажу земельних ділянок для ведення товарного сільського господарства лише п'ять.
Тут потрібно звернути увагу на кілька особливостей набору даних, що я підготував. Передусім оголошення з продажу земельних ділянок для ведення товарного сільського господарства є викидами. В Україні станом на дату підготовки статті діє мораторій на продаж земельних ділянок для ведення товарного сільського господарства (до речі, на відміну від земельних ділянок, наданих для ведення особистого селянського господарства). Однак є велика ймовірність, що мораторій знімуть, тому я вирішив залишити ці оголошення у вибірці. По-друге, у вибірці є лише один комбінований тип земельних ділянок: під ОЖБ й ОСГ, інші комбінації відкинуто як викиди, оскільки трапляються дуже й дуже рідко. І останнє — кожний запис у вибірці унікальний за комбінацією стовпців «land_types_source», «land_types_PcmU» ї «description», а самі оголошення можуть повторюватися.
Для спрощення процесу класифікації, я об єднав стовпці «land_types_source», «land_types_PcmU» ї «description» в один стовпець «text».
Після першого знайомства з вихідними даними годину братися до наступних етапів. Найперше поговоримо про основну модель, що я використовував.
NLP і модель «мішка слів»
Отже, обробка природної мови (Natural-languageprocessing, NLP) — загальний напрямок інформатики, штучного інтелекту й математичної лінгвістики. Він вивчає проблеми комп'ютерного аналізу й синтезу природної мови. Щодо штучного інтелекту, то аналіз — це розуміння мови, а синтез — генерація розумного тексту.
Методи NLP дозволяють вирішувати різноманітні завдання: від класифікації тональності текстів, машинного перекладу, спам-фільтрів, розмітки частин мови до генерації тексту.
У своєму проєкті я використав одну з моделей NLP — модель «мішка слів » (МС, bag-of-words). Модель МС дає змогу презентувати текст як невпорядковану колекцію слів (якщо бути точним, то токенів) без урахування граматичних правил і порядку розміщення їх у конкретному розділі тексту.
В основі моделі «мішка слів» лежить досить проста ідея, яку можна резюмувати так:
- Створити з усього набору документів вокабуляр унікальних лексем (токенів ) — наприклад, слів. Якщо послідовність елементів (токенів) у моделі МС складається зі слів, вона називається юніграмною (наприклад, речення «Продаж земельної ділянки» ділять на елементи [«продаж», «земельної», «ділянки»]). Але вона може складатися й з послідовності слів (наприклад, двограмної послідовності [«продажу земельної», «земельної ділянки»]). У ширшому значенні в NLP безперервні послідовності елементів — слова, літери або символи — називають також n-грамами. Вибір числа n у n-грамній моделі залежить від окремо взятої ділянки.
- Побудувати з шкірного документа вектор ознак, що містить частотності входжень кожного слова в певний документ.
З огляду на те, що унікальні слова в кожному документи презентують у вокабулярі моделі «мішка слів» лише малу підмножину всіх слів, вектори ознак складатимуться здебільшого з нулів, і тому ми називаємо їх розрідженими.
Вибір моделі «мішка слів» залежить від специфіки самих оголошень з продажу нерухомості, адже, щоб визначити тип ділянки, досить знайті в оголошенні певну фразу (наприклад, фраза «ділянка під садівництво» трапляється доволі часто), і цих слів (а точніше одного слова «садівництво») достатня для ідентифікації типу ділянки, а решта слів в оголошенні вже не мають значення. Перейдемо до прикладу практичної реалізації моделі «мішка слів» за допомогою бібліотеки NLTK.
Бібліотека NLTK
Natural Language Toolkit (NLTK) — це набір бібліотек і програм для символьної та статистичної обробки природних мов, написаної мовою програмування Python. Її розробили Стівен Берд й Едвард Лопер.
NLTK визначає інфраструктуру, яку можна використати для побудови програм NLP у Python. Вона надає базові класи для представлення даних, що мають відношення до обробки природної мови; стандартні інтерфейси для виконання таких завдань: анотування частин мови, синтаксичний розбір і класифікація текстом; і стандартні реалізації для кожного завдання, які можуть бути об єднані для вирішення складних завдань.
Для початку роботи з NLTK необхідно згадати ще кілька зрозуміти:
- Tokenizer — забезпечує токенізацію документів (розбиття на лексеми);
- Stopwords — слова, які зустрічаються в мові надто часто, щоб нести якусь важливу інформацію (наприклад, артикль «а»);
- Stemming (стемінг) — це процес скорочення слова до основи шляхом відкидання допоміжних частин, таких як закінчення або суфікса (наприклад, слова «будинку», «будинки» рахуватимуть як одне слово «будинк»);
- Lemmatisation (лематизація) — це процес групування переплетених форм слова, щоб їх можна було проаналізувати як один предмет, ідентифікований лемою слова або словниковою формою (наприклад, слова «будинку», «будинки» рахуватимуть як одне слово «будинок»).
Я вирішив написати про спосіб реалізації моделі «мішка слів» на NLTK, враховуючи, що ця реалізація допоможе краще зрозуміти модель і підводні камені, які з нею пов'язані.
Отримавши всю необхідну інформацію, переходьмо безпосередньо до коду.
Перш за все визначимо Tokenizer, який буде проводити попередню обробку тексту й розбивку його на окремі слова (не забуваючи, звичайно, про імпорт відповідних бібліотек):
def ua_tokenizer(text,ua_stemmer=True,stop_words=[]): """ Tokenizer for Ukrainian language, returns only alphabetic tokens. Keyword arguments: text -- text for tokenize ua_stemmer -- if True use UkrainianStemmer for stemming words (default True) stop_words -- stop list of words (default []) """ tokenized_list=[] text=re.sub(r"""[""`?]""", ", text) text=re.sub(r"""([0-9])([\u0400-\u04FF]|[A-z])""", r"\1 \2", text) text=re.sub(r"""([\u0400-\u04FF]|[A-z])([0-9])""", r\1 \2", text) text=re.sub(r"""[\-.,:+*/_]""", '', text) for word in nltk.word_tokenize(text): if word.isalpha(): word=word.lower() if ua_stemmer is True: word=UkrainianStemmer(word).stem_word() if word not in stop_words: tokenized_list.append(word) return tokenized_list
Функція ua_tokenizer отримує на вході текст, який потрібно розбити на токени, список stop_words, якщо треба також підтвердження в необхідності використовувати стемер. На початковому етапі очищає текст від додаткових символів, далі розбиває його на токени за допомогою функції nltk.word_tokenize(), потім перевіряє, чи токен складається зі слів (метод isalpha()), якщо так, то слова, що приводить до нижнього регістра, за потреби обробляють стемером і записують у новий список.
Спочатку я просто розбивав текст за допомогою функції word_tokenize, без початкового очищення, але був здивований результатами: як виявилося, метод isalpha() повертає False, якщо токен містить будь-який не алфавітний символ, тож слово "дерев'яна п'яний" у цьому варіанті відкидається, а слово 'дерев"яний' розділяється на два 'дерев' та 'яний'. Виходить, невідомо що, тому важливо перед подачою будь-якого тексту у word_tokenize попередньо його обробити й очистити. У моєму датасеті всі дані вже очищено.
Маючи токенайзер, отримаємо докладнішу інформацію про нашу вибірку:
def ngrams_info(series,n=1,most_common=20,ua_stemmer=True,stop_words=[]): """ ngrams_info - Show detailed information about string pandas.Series column. Keyword arguments: series -- pandas.Series object most_common -- show most common words(default 50) ua_stemmer -- if True use UkrainianStemmer for stemming words (default True) stop_words -- stop list of words (default []) """ words=series.str.cat(sep=' ') print ('Кількість символів: ',len(words)) words=nltk.ngrams(ua_tokenizer(words,ua_stemmer=ua_stemmer,stop_words=stop_words),n) words=nltk.FreqDist(words) print ('Кількість токенів: ',words.N()) print ('Кількість унікальних токенів: ',words.B()) print ('Найбільш уживані токени: ',words.most_common(most_common)) words.plot (most_common, cumulative = True)
Функція ngrams_info об єднує всі рядки об'єкта pandas.Series в один об'єкт типу string, далі за допомогою функції nltk.ngrams утворює список з n-грамів (токенів) вказаного розміру, а функція nltk.FreqDist створює розподіл частот, що містить дані про n-грами й друкує основну інформацію.
Подивімось, що ми отримаємо, якщо виконаємо цю функцію з кількома різними параметрами.
Для юніграмів:
Рис. 2. Частоти найуживаніших юніграмів
Для триграмів:
Рис. 3. Частоти найуживаніших триграмів
Отже, для юніграмів (читай — слів) ми маємо у вибірці з 4256 екземплярів 151 318 токенів, з яких лише 6638 токенів (під час використання стемера) унікальні. А на частку 20 найуживаніших токенів припадає близько 30% усіх токенів, що є в датасеті.
Щодо триграмів, то ситуація повторюється, однак через велику кількість комбінацій частка перших 20 найуживаніших токенів «лише» близько 20%.
Вісь що ми отримуємо: серед найуживаніших слів є слова, що належать до певного типу земельних ділянок, альо є й слова, які жодного значення не мають і варто додати їх у список stop_words.
А зараз перейдемо нарешті до класифікації.
Як моделі для класифікації я використав три класи класифікаторів nltk.NaiveBayesClassifier , nltk.MaxentClassifier і nltk.DecisionTreeClassifier . До технічної частини я не вдаватимуся, але коротко розповім про класифікатори. Кому цікаво, є книжка про бібліотеку NLTK — Natural Language Processing with Python (автори Steven Bird, Ewan Klein and Edward Loper), в якій детально розглянуто можливості бібліотеки загалом і специфіку класифікаторів (враховуючи, що її написали автори бібліотеки, я розглядаю її як «частину/доповнення» до документації). Під час написання коду до цієї статті в частині бібліотеки NLTK за основу я брав приклади саме з цієї книжки.
Отже, клас nltk.DecisionTreeClassifier базується на алгоритмі дерева ухвалення рішень . Суть моделі полягає в розбитті даних на підмножини шляхом ухвалення рішень, що ґрунтуються на відповідях за серією питань. Використовуючи алгоритм, ми починаємо в корені дерева й розщеплюємо дані за певними атрибутами. Є різні спосібі вибирати черговий атрибут, наприклад за критерієм найбільшого приросту інформації (вимірює, наскільки стають організованішими вхідні значення, коли ми поділяємо їх, використовуючи цей атрибут). Далі ми повторюємо процедуру розщеплення ітеративно в кожному дочірньому вузлі, поки не отримаємо однорідних листів. Тобто всі зразки в кожному вузлі належатимуть до того ж самого класу.
Клас nltk.NaiveBayesClassifier базується на наївному класифікаторі Баєса (НКБ). Наївні класифікатори Баєса — це сімейство простих імовірнісних класифікаторів , що ґрунтуються на застосуванні теореми Баєса з припущенням (наївним) про незалежність між змінними. Суть НКБ полягає в тому, що нам треба знайті такий клас, при якому його ймовірність для конкретного випробування була б максимальній, а припущення про незалежність між змінними потрібне для спрощення розрахунків.
Клас nltk.MaxentClassifier базується на моделі максимальної ентропії . Класифікатор максимальної ентропії використовує модель, дуже схожу на модель, яку використовує наївний класифікатор Баєса. Альо замість використовувати ймовірності для визначення параметрів моделі, він застосовує методи пошуку, щоб знайте набір параметрів, які забезпечують максимальну результативність класифікатора. Зокрема, він шукає набір параметрів, який максимізує загальну правдоподібність навчального корпусу, однак через наявність потенційно складних взаємодій між впливом (кореляцією) пов'язаних властивостей, не існує способу безпосереднього розрахунку параметрів моделі.
Таким чином, класифікатори максимальної ентропії вибирають параметри моделі за допомогою методів ітеративної оптимізації, які ініціалізують параметри моделі випадковими величинами, а потім багаторазово уточнюють ці параметри, щоб наблизити їх до оптимального рішення. Від себе скажу, що клас nltk.MaxentClassifier працює дуже повільно, навіть з моїм набором даних, тож я навів його лише для прикладу, а в коді обмежив кількість ітерацій.
Важливо знаті: класи класифікаторів, які базуються на певних алгоритмах, реалізовано в кожній бібліотеці за своєю логікою й математичним обґрунтуванням, і вони можуть відрізнятися від «класичних» інтерпретацій, даючи дещо інші результати. На це треба зважати!
Далі для навчання класифікатора, будь-якого з наведених класифікаторів, використовується метод train, що першим параметром приймає параметр labeled_featuresets (з документації «param labeled_featuresets: A list of classified featuresets, i.e., a list of tuples `(featureset, label)`»). За своєю суттю параметр labeled_featuresets — це список, кожен елемент якого містить кортеж, що презентує одне випробування (наприклад, одне оголошення з набору) й складається з двох елементів: перший featureset — це ще один список, кожен елемент якого містить словник увазі: {токен : наявність або відсутність сертифіката у випробуванні (True або False)}, довжина списку featureset для всіх випробувань однакова й визначається вокабуляром унікальних токенів, другий елемент кортежу — це мітка класу для випробування. На перший погляд це здається складним (а якщо розібратись то й неефективним в обчислювальному плані), але спробую по порядку розповісти, як це можна реалізувати. Насамперед, визначимо вокабуляр (читай — список унікальних токенів:
words=dataframe[X_column].str.cat(sep=' ') words=nltk.ngrams(ua_tokenizer(words,ua_stemmer=ua_stemmer,stop_words=stop_words),n=n) words=nltk.FreqDist(words) word_features=words.most_common(most_common) word_features=[words[0] for words in word_features]
Тут усе просто: за аналогією до функції ngrams_info всі значення з колонки об'єднання єкту DataFrame об'єднання єднуємо в один об'єкт типу string, перетворюємо в проверяемая FreqDist (див. пояснення вище), знаходимо перших most_common-токенів, що найчастіше з'єднання є з їхніми частотами, і складаємо список найчастіше вживаних токенів без їхніх частот. Основне завдання цього етапу — знайте список перших найуживаніших токенів (юніграмів, біграмів тощо), що трапляються в усьому наборі даних. Чому беремо найбільш вживані токени? Тут ми керуємося припущенням, що найуживаніші токени несуть найбільше інформації, а решту можна відкинути. Це припущення має очевидні недоліки, але поки що візьмімо його за основу.
На наступному етапі визначимо невелику функцію, котра буде будувати featureset. Вісь вона:
def bag_of_words(document_tokens,word_features): """ The Return dict of bag_of_words. Keyword arguments: document_tokens -- list of tokens word_features -- list of features """ features={} for word in word_features: features['contains({})'.format(word)]=(word[0] in document_tokens) return features
Функція bag_of_words на вході приймає список токенів шкірного випробування document_tokens (у нашому випадку оголошення) і список найуживаніших токенів усієї вибірки word_features, повертає словник, що містить назву сертифіката та його наявність або відсутність у конкретній випробуванні.
Нам ще залишається побудувати список токенів шкірного випробування, але це не проблема, бо ми вже це вміємо робити.
Отже, нижче наведено універсальну функцію для базових класифікаторів бібліотеки NLTK:
def nltk_classifiers(dataframe,X_column,y_column,classifier=nltk.NaiveBayesClassifier,n=1,stop_words=[],ua_stemmer=False,most_common=1000): words=dataframe[X_column].str.cat(sep=' ') words=nltk.ngrams(ua_tokenizer(words,ua_stemmer=ua_stemmer,stop_words=stop_words),n=n) words=nltk.FreqDist(words) word_features=words.most_common(most_common) word_features=[words[0] for words in word_features] labeled_featuresets=[] for _,row in dataframe.iterrows(): row[X_column]=nltk.ngrams(ua_tokenizer(row[X_column],ua_stemmer=ua_stemmer,stop_words=stop_words),n=n) row[X_column]=[words[0] for words in nltk.FreqDist(row[X_column])] labeled_featuresets.append((bag_of_words(row[X_column],word_features=word_features), row[y_column])) train_set,test_set,_,_=train_test_split(labeled_featuresets,dataframe[y_column],stratify=dataframe[y_column],test_size=0.33) if classifier==nltk.MaxentClassifier: classifier=classifier.train(train_set, max_iter=5) else: classifier=classifier.train(train_set) accuracy_train=nltk.classify.accuracy(classifier, train_set) accuracy=nltk.classify.accuracy(classifier, test_set) print('Точність класифікатора на навчальних даних:',accuracy_train) print('Точність класифікатора на тестових даних:',accuracy) y_true=[] y_pred=[] for test in test_set: y_true.append(test[1]) y_pred.append(classifier.classify(test[0])) confmat=nltk.ConfusionMatrix(y_pred,y_true) print(confmat) return classifier
На початковому етапі ми знаходимо найуживаніші токени, далі в циклі for перебираємо кожен елемент вибірки й перетворюємо текст випробування в список токенів, а далі створюємо список labeled_featuresets.
Ми перетворили нашу вибірку у формат, придатний для подачі до класифікатора, однак постає питання якісного оцінювання класифікатора. Зрозуміло, що якщо ми будемо оцінювати класифікатори за даними, за якими вони навчалися, не відомо, як вони будуть поводити себе на даних, яких вони не бачили під час навчання. Найпростіший вихід із ситуації — розбиття вибірки на дві підмножини: навчальну й тестову, в ідеалі тестову множину можна використовувати лише для фінальної перевірки класифікатора вже з підібраними параметрами. Функція train_test_split бібліотеки scikit-learn розбиває нашу множину вибірки на дві підмножини: навчальну й тестову, пропорційно попередньо визначеним класам (за це відповідає параметр stratify), у такому разі вона має дивний вигляд, але це пов'язаність язано з різною логікою подачі даних в обох бібліотеках (про бібліотеку scikit-learn поговоримо в другій частині статті, тому зациклюватися на ній не буду).
На наступному етапі після розбиття даних навчаємо наш класифікатор.
Далі необхідно визначити якість нашого класифікатора на навчальних і текстових даних. Тут звертаю увагу, що є різні метрики визначення якості класифікатора, їх докладно розглянємо далі, а в цьому випадку використовується точність (accuracy), а також друкується матриця невідповідностей (Confusion Matrix, буде розглянуто далі). Точність (accuracy) — частка правильних відповідей алгоритму класифікації в загальній кількості всіх відповідей.
У нас усе готове, тож запускаємо класифікатори з кількома різними параметрами для порівняння. Код нижче:
classifiers=[nltk.NaiveBayesClassifier,nltk.MaxentClassifier,nltk.DecisionTreeClassifier] for y_column in ('land_types','built_up'): for classifier in classifiers: for n in (1,3): print ('Класифікатор -',класифікатор) print ('Порядок n',n) print ('Класифікатор за колонкою -',y_column) model=nltk_classifiers(land_data,X_column='text',y_column=y_column,classifier=classifier, n=n) if classifier==nltk.NaiveBayesClassifier: print ('Найважливіші токени для класифікації за колонкою -',y_column) model.show_most_informative_features(10)
На мій погляд, тут майже все зрозуміло, тож не затримуватимуся. Єдиний момент — метод show_most_informative_features() класифікатора nltk.NaiveBayesClassifier показує зазначену кількість токенів, що найкраще класифікують тієї чи інший об'єкт.
А тепер перейдемо до результатів класифікації за типами для юніграмів для шкірного класифікатора:
Результати класифікації за типами для класифікатора nltk.NaiveBayesClassifier:
Класифікатор - <class 'nltk.classify.naivebayes.NaiveBayesClassifier'> Порядок n - 1 Класифікатор за колонкою - land_types Точність класифікатора на навчальних даних: 0.8796913363732024 Точність класифікатора на тестових даних: 0.8327402135231317 | 1 2 3 4 5 6 7 | --+-----------------------------+ 1 |<735> 4 5 7 8 . 9 | 2 | 5 <86> 7 8 . 2 3 | 3 | 76 15<271> 5 1 . . | 4 | 14 5 2 <61> 1 . . | 5 | 7 . . 2 <3> . . | 6 | . . . . . <.> . | 7 | 36 7 5 1 . . <14>| --+-----------------------------+ (row = reference; col = test)
Результати класифікації за типами для класифікатора nltk.MaxentClassifier:
Класифікатор - <class 'nltk.classify.maxent.MaxentClassifier'> Порядок n - 1 Класифікатор за колонкою - land_types ==> Training (5 iterations) Iteration Log Likelihood Accuracy --------------------------------------- 1 -1.94591 0.019 2 -1.07311 0.697 3 -0.83080 0.697 4 -0.71131 0.697 Final -0.64350 0.697 Точність класифікатора на навчальних даних: 0.6969484391441599 Точність класифікатора на тестових даних: 0.6832740213523132 | 1 2 3 4 5 6 7 | --+-----------------------------+ 1 |<869>113 216 70 13 2 26 | 2 | . <4> . 1 . . . | 3 | 3 . <74> . . . . | 4 | 1 . . <13> . . . | 5 | . . . . <.> . . | 6 | . . . . . <.> . | 7 | . . . . . . <.>| --+-----------------------------+ (row = reference; col = test)
Результати класифікації за типами для класифікатора nltk.DecisionTreeClassifier:
Класифікатор - <class 'nltk.classify.decisiontree.DecisionTreeClassifier'> Порядок n - 1 Класифікатор за колонкою - land_types Точність класифікатора на навчальних даних: 0.9786039985969835 Точність класифікатора на тестових даних: 0.9138790035587189 | 1 2 3 4 5 6 7 | --+-----------------------------+ 1 |<827> 4 17 22 2 . 2 | 2 | 7<106> 2 3 . . 10 | 3 | 14 2<271> . . . 3 | 4 | 18 2 . <58> 1 . 1 | 5 | 6 . . 1 <10> . . | 6 | . . . . . <2> . | 7 | 1 3 . . . . <10>| --+-----------------------------+ (row = reference; col = test)
Отже, найкращі результати класифікації має класифікатор nltk.DecisionTreeClassifier. Тепер подивімось на результати класифікації за забудованістю.
Результати класифікації за забудованістю для класифікатора nltk.NaiveBayesClassifier:
Класифікатор - <class 'nltk.classify.naivebayes.NaiveBayesClassifier'> Порядок n - 1 Класифікатор за колонкою - built_up Точність класифікатора на навчальних даних: 0.9214310768151526 Точність класифікатора на тестових даних: 0.892526690391459 | 0 1 | --+-----------+ 0 |<1096> 45 | 1 | 106 <158>| --+-----------+ (row = reference; col = test)
Результати класифікації за забудованістю для класифікатора nltk.MaxentClassifier:
Класифікатор - <class 'nltk.classify.naivebayes.NaiveBayesClassifier'> Порядок n - 1 Класифікатор за колонкою - built_up Точність класифікатора на навчальних даних: 0.9214310768151526 Точність класифікатора на тестових даних: 0.892526690391459 | 0 1 | --+-----------+ 0 |<1096> 45 | 1 | 106 <158>| --+-----------+ (row = reference; col = test)
Результати класифікації за забудованістю для класифікатора nltk.DecisionTreeClassifier:
Класифікатор - <class 'nltk.classify.decisiontree.DecisionTreeClassifier'> Порядок n - 1 Класифікатор за колонкою - built_up Точність класифікатора на навчальних даних: 0.9705366538056822 Точність класифікатора на тестових даних: 0.9373665480427046 | 0 1 | --+-----------+ 0 |<1155> 41 | 1 | 47 <162>| --+-----------+ (row = reference; col = test)
У випадку класифікації за забудованістю також найкращі результати має модель, побудована на основі дерева ухвалення рішень, однак на перший план виходить проблема перенавчання (overfitting) , коли точність класифікатора стрімко падає на тестових даних, якщо порівняти з результатами, отриманими під час тренувальної вибірки. До кінця проєкту я так і не зміг повністю вирішити цю проблему, проте зменшив розрив до прийнятного рівня. Як я розумію, найкращий вихід у цій ситуації — збільшення вибірки.
Важливо зауважити, що для забудованих земельних ділянок класифікатор, що присвоюватиме всім значенням вибірки значення 0 (фактично класифікатором не буде, а прийматиме, що всі земельні ділянки незабудовані й не розв'язків язуватиме поставленого завдання) точність становитиме 85,53% (частка незабудованих земельних ділянок у вибірці), тому всі класифікатори нижче цього рівня можна навіть не розглядати.
Спочатку я думав, що побудувати модель класифікації ділянок за забудованістю буде значно простіше, ніж за типами, бінарна класифікація все ж таки, але на практиці все виявилося з точністю до навпаки. Класифікувати за типами значно простіше, оскільки кожен тип має свої слова-ідентифікатори, тоді як у класі забудованості — «незабудована», означає відсутність у тексті оголошення слів, що можуть стверджувати протилежне. Однак на такій малій вибірці можуть з'єднання явитися токени, які класифікатор розглядатиме як токени-ідентифікатори класу «незабудована».
Наприклад, умовно у вибірці є 50 оголошень продаж вільних від забудови ділянок у селі Сокільники, тоді класифікатор розглядатиме токен «сокільник» як ідентифікатор класу «незабудована», що за своєю суттю неправильно. Але як зрозуміти, як кожен токен «сприймається» класифікатором? Однією з особливостей бібліотеки NLTK є можливість виклику методу show_most_informative_features() для класифікаторів nltk.NaiveBayesClassifier і nltk.MaxentClassifier. Вісь результати для класифікатора nltk.NaiveBayesClassifier:
Для класифікації за типами ділянок:
Найважливіші токени для класифікації за колонкою - land_types Most Informative Features contains(('виробництв',)) = True 6 : 1 = 620.9 : 1.0 contains(('сільськогосподарськ',)) = True 6 : 1 = 443.5 : 1.0 contains(('селянськ',)) = True 2 : 1 = 309.9 : 1.0 contains(('веденн',)) = True 6 : 1 = 282.2 : 1.0 contains(('me',)) = True 6 : 1 = 266.1 : 1.0 contains(('канал',)) = True 6 : 1 = 266.1 : 1.0 contains(('sitalozemlyaзагальн',)) = True 6 : 1 = 266.1 : 1.0 contains(('господарств',)) = True 2 : 3 = 257.8 : 1.0 contains(('багатоквартирн',)) = True 5 : 1 = 234.7 : 1.0 contains(('колективн',)) = True 3 : 1 = 226.3 : 1.0
Для класифікації за забудованістю ділянок:
Найважливіші токени для класифікації за колонкою - built_up Most Informative Features contains(('цегл',)) = True 1 : 0 = 119.8 : 1.0 contains(('стоїт',)) = True 1 : 0 = 60.1 : 1.0 contains(('к',)) = True 1 : 0 = 60.1 : 1.0 contains(('цеглян',)) = True 1 : 0 = 58.3 : 1.0 contains(('літн',)) = True 1 : 0 = 53.0 : 1.0 contains(('одноповерхов',)) = True 1 : 0 = 49.1 : 1.0 contains(('опаленн',)) = True 1 : 0 = 45.2 : 1.0 contains(('підвал',)) = True 1 : 0 = 43.6 : 1.0 contains(('деревян',)) = True 1 : 0 = 38.6 : 1.0 contains(('горищ',)) = True 1 : 0 = 37.3 : 1.0
Метод show_most_informative_features() дозволяє досліджувати класифікатор, щоб визначити, які з властивостей (токенів), які він знайшов, найефективніші для класифікації. Наприклад, він показує, що слово «цегли» у 119,8 рази частіше свідчить про те, що ділянку забудовано.
Висновки
Я навів короткий огляд однієї з можливостей бібліотеки NLTK — реалізації моделі «мішка слів». Вважаю, що його достатня для побудови перших, найпростіших класифікаторів. Однак мій код явно не схожий на pythonic (тут, звичайно, його можна оптимізувати, але суперечливе питання, чи в цьому є потреба), крім того, припущення, що найінформативніші слова (токени) для шкірного класу найчастіше трапляються й у всій вибірці не витримує критики, враховуючи хоча б нерівномірність розподілу класів.
Звичайно, отримані результати класифікаторів можна покращити і покращувати їх якість я за допомогою бібліотеки scikit-learn — на мій особистий і суб'єкта єктивний погляд, однієї із найкращих бібліотек, написаних на Python і точно з найкращою документацією, але про це вже в другій частині. А щодо бібліотеки NLTK, то це дуже хороша бібліотека для NLP з великим колом можливостей, що виходять далеко за межі цієї тими.
Рекомендовані джерела:
- Natural Language Processing with Python by Steven Bird, Ewan Klein and EdwardLoper — o'reilly;
- Python Machine Learning by Sebastian Raschka — Packt;
- Hands-On Machine Learning with Scikit-Learn and TensorFlow by Aurelien Geron — o'reilly;
- Python Data Science Handbook by Jake Vander Plas — o'reilly;
- Deep Learning with Keras by Antonio Gulli, SujitPal — Packt;
- scikit-learn.org/stable ;
- nltk.org ;
- keras.io ;
- tensorflow.org .
Опубліковано: 04/03/20 @ 11:00
Розділ Безпека
Рекомендуємо:
Коли варто переписувати код проекту і як це донести до замовника
Ви підприємець чи науковець? Як продакт-менеджера обирати компанію відповідно до схильностей
Arti, Langkah serta Taktik Tepat dalam Main Koa/Ceki/Pei
DevOps дайджест #30: гонка клаудов за перевагу в Kubernetes, Thanos operator, стрімке зростання Sentry
Зростання органічного трафіку для мережі медичних клінік з 162,5 тис. до 2,5 млн відвідувань за рік роботи