Перші кроки в NLP: розглядаємо Python-бібліотеку scikit-learn в реальному завданні

Усім привіт! Це друга частина статті про класифікацію оголошень з продажу земельних ділянок в Україні методами NLP. У попередній частині я розповів про задачі, які я планував вирішити, ознайомив читачів з підготовленим датасетом, моделлю «мішка слів» і навів приклади перших класифікаторів, підготовлених за допомогою бібліотеки NLTK.

У цій частині розповім про бібліотеку scikit-learn, наведу приклади реалізації моделі «мішка слів» за допомогою цієї бібліотеки, побудови конвеєрів обробки й класифікації даних і тонкого налаштування моделей. Також розповім про вибір кращих моделей розв’язання поставлених завдань. Увесь код до цієї частини статті можна знайти тут.

Отже, перейдімо до scikit-learn.

Бібліотека scikit-learn

Scikit-learn — це безплатна бібліотека машинного навчання, написана на Python. Вона надає широкий вибір алгоритмів навчання з учителем і без нього. Одна з основних переваг бібліотеки полягає в тому, що вона працює на основі декількох поширених математичних бібліотек і легко інтегрує їх одна з одною. Ще однією перевагою є широка спільнота й докладна документація. Scikit-learn спеціалізується на алгоритмах машинного навчання для вирішення задач навчання з учителем: класифікації й регресії, а також для завдань навчання без учителя: кластеризації, зменшення розмірності й детектування аномалій.

Одразу переходжу до прикладу. Щоб побудувати модель «мішка слів» (докладно її розглядали в попередній частині) на основі частоти слів у відповідних оголошеннях, можна скористатися класом векторизації кількостей CountVectorizer, реалізованим в бібліотеці scikit-learn.

У наступному фрагменті коду бачимо, що клас CountVectorizer приймає масив текстових даних, які можуть бути документами або просто пропозиціями, і будує для нас модель «мішка слів»:

print (land_data['text'][:4])
count = CountVectorizer()
bag = count.fit_transform(land_data['text'][:4])
print(count.get_feature_names())
print(bag.toarray())
Перших 4 оголошення:
0     комерційного           
1     під забудову           
2     садівництво            
3     сільськогосподарського 
Name: text, dtype: object
['забудову', 'комерційного', 'під', 'садівництво', 'сільськогосподарського']
[[0 1 0 0 0]
 [1 0 1 0 0]
 [0 0 0 1 0]
 [0 0 0 0 1]]

На прикладі перших чотирьох оголошень клас CountVectorizer за допомогою методу fit_transform будує вокабуляр унікальних токенів (слів) і перетворює текст оголошення у вектор, який відображає кількість входжень кожного токена вокабуляра в тексті (за своєю суттю метод fit_transform є комбінацією двох методів: fit, що будує вокабуляр, і transform, що перетворює текст). На виході ми отримуємо масив бібліотеки NumPy, кожний стовпець якого в наших результатах відповідає окремому токену у вокабулярі, а рядок окремому документу (оголошенню).

Коли ми аналізуємо текстові дані, часто стикаємося зі словами з обох класів, які трапляються у двох і більше документах. Такі слова, як правило, не містять корисної або важливої інформації. Для знаходження таких слів зазвичай використовують показник TF-IDF (від англ. TF — term frequency, IDF — inverse document frequency). Згідно із цим показником вага (значущість) слова пропорційна кількості його вживань у документі й обернено пропорційна частоті вживання слова в інших документах колекції. Більшу вагу TF-IDF отримають слова з високою частотою появи в межах документа й низькою частотою вживання в інших документах колекції. У бібліотеці scikit-learn реалізований спеціальний клас TfidfVectorizer, призначений для перетворення колекції необроблених документів у матрицю TF-IDF.

Приклад реалізації й результати нижче:

print ('Перших 4 оголошення:')
print (land_data['text'][:4])
count = TfidfVectorizer()
bag = count.fit_transform(land_data['text'][:4])
print(count.get_feature_names())
print(bag.toarray())
Перших 4 оголошення:
0     комерційного           
1     під забудову           
2     садівництво            
3     сільськогосподарського 
Name: text, dtype: object
['забудову', 'комерційного', 'під', 'садівництво', 'сільськогосподарського']
[[0.         1.         0.         0.         0.        ]
 [0.70710678 0.         0.70710678 0.         0.        ]
 [0.         0.         0.         1.         0.        ]
 [0.         0.         0.         0.         1.        ]]

Найчастіше пропонується використовувати клас TfidfVectorizer, але тут потрібно експериментувати з обома класами. Я був здивований, що на моєму прикладі жоден із класів не мав особливої переваги, але про це згодом.

Після того як ми трансформували наші вихідні дані методом fit_transform, можемо відразу передавати їх у класифікатор. Проте це є «рутинною справою, і було б добре цей процес спростити», — подумали творці scikit-learn і підготували клас Pipeline.

Об’єкт-конвеєр Pipeline в якості вхідних параметрів отримує список кортежів, де перше значення в кожному кортежі — строгий ідентифікатор, який можна використовувати для доступу до відповідного елементу в конвеєрі, а другий елемент — це перетворювач (transforms) або оцінювач (estimator) бібліотеки scikit-learn. Одразу приклад:

vectorizer=TfidfVectorizer()
clf=LinearSVC()
pipe=Pipeline([('Vectorizer',vectorizer),
               	('clf',clf)])
clf=pipe.fit(land_data['text'][:100],land_data['typy'][:100])
print ("Точність класифікатора на навчальних даних:", clf.score(land_data['text'][:100],land_data['typy'][:100]))
print ("Точність класифікатора на тестових даних:", clf.score(land_data['text'][100:130],land_data['typy'][100:130]))
Точність класифікатора на навчальних даних: 0.99
Точність класифікатора на тестових даних: 0.8

Ми оголосили змінні vectorizer і clf, що є відповідно перетворювачем та оцінювачем (класифікатором), передали їх як список кортежів у екземпляр класу Pipeline, навчили нашу модель і перевірили її точність на навчальних та тестових даних. Ось і все, класифікатор готовий. І зауважте, що ми не робили явних маніпуляцій з даними, а за все піклується клас Pipeline. Наведений вище код я називаю «pythonic», і він дуже контрастує з моєю реалізацією на NLTK. Окрім того, він значно ефективніший (у NLTK векторне представлення одного токена включає словник, а тут лише одне значення з масиву NumPy).

Методи визначення якості класифікатора

Підготовчу роботу ми провели, тепер настав час визначитися з класифікатором, який ми будемо використовувати. Отже, нам потрібна міра якості класифікаторів, а також дані, на яких ми можемо їх перевірити. Зрозуміло, що нам потрібно перевіряти класифікатор на даних, яких він ще не «бачив» у процесі навчання, щоб передбачити його якість на нових даних. Для цього завдання в бібліотеці scikit-learn є функція train_test_split, яку я вже згадував і яка розподіляє вибірку на навчальну (training set) й тестову (test set). Ось приклад:

X_train_zab, X_test_zab, y_train_zab, y_test_zab=train_test_split(land_data['text'],land_data['zabudovana'],
                                                              	stratify=land_data['zabudovana'],
                                                              	test_size=0.33,random_state=0)

X_train, X_test, y_train, y_test=train_test_split(land_data['text'],land_data['typy'],
                                              	stratify=land_data['typy'],
                                              	test_size=0.33,random_state=0)

Функція train_test_split як параметри приймає множину вихідних значень Х і множину результівних значень Y, параметр stratify забезпечує, щоб розподіл класів за підвибірками був пропорційний розподілу класів у вибірці загалом (що важливо для нерівномірно розподілених класів), параметр test_size керує розміром тестової вибірки, а параметр random_state задає початкові значення для генератора випадкових чисел (початкові значення важливо вказувати, щоб щоразу отримувати однаковий результат поділу вибірки).

Важливо, що тестову вибірку можна застосовувати лише для визначення якості фінальних моделей, а для навчання й підбору гіперпараметрів моделей її використовувати не можна, оскільки в такому разі спотворюються результати тестування.

Частота помилок на нових зразках називається помилкою узагальнення (generalization error), або помилкою виходу за межі вибірки (out-of-sample error), і, оцінюючи моделі на випробувальному наборі, ви отримуєте оцінку такої помилки. Результівне значення свідчить про те, як добре модель буде працювати зі зразками, які вона ніколи не бачила раніше.

Загальне рішення проблеми передбачає наявність іншого набору, який називається перевіряльним (validation set). Ви навчаєте безліч моделей з різноманітними гіперпараметрами, застосовуючи навчальний набір, вибираєте модель і гіперпараметри, які забезпечують краще виконання на перевіряльному наборі, і, коли модель влаштовує, запускаєте фінальний тест на тестувальному наборі, щоб отримати оцінку помилки узагальнення на нових даних.

Щоб уникнути «зайвої витрати» занадто великого обсягу навчальних даних для перевіряльного набору, використовують перехресну перевірку (cross-validation): навчальна вибірка розбивається на доповнювані підвибірки, а кожна модель навчається на різній комбінації таких підвибірок і перевіряється на підвибірці, що залишилася.

Після того як обрано тип моделі і її гіперпараметри, остаточну модель навчають із застосуванням цих гіперпараметрів на всьому навчальному наборі, а помилку узагальнення вимірюють на тестувальному наборі.

З типами моделі все зрозуміло, а що таке гіперпараметри моделі? Ми не можемо очікувати, що параметри різних алгоритмів навчання, що надають бібліотеки за замовчуванням, будуть оптимальними для нашого конкретного практичного завдання. Інтуїтивно гіперпараметри можна уявити як параметри, які не було видобуто з даних і які є важелями управління моделлю, що їх можна змінювати з метою поліпшення якості її роботи.

Щоб не бути голослівним, переходжу до прикладу. У scikit-learn реалізовано кілька класів для вибору й тонкого налаштування моделей. Зосереджу увагу на двох: GridSearchCV та RandomizedSearchCV. Клас GridSearchCV виконує решітковий пошук (grid search).

Вам треба лише повідомити йому, з якими параметрами ви хочете поекспериментувати і які значення випробувати, а GridSearchCV оцінить усі можливі комбінації значень гіперпараметрів, застосовуючи перехресну перевірку. Наприклад, наступний код шукає комбінацію гіперпараметрів класифікатора LinearSVC (про нього розповім дещо пізніше):

vectorizer=TfidfVectorizer()
clf=LinearSVC()
pipe=Pipeline([('Vectorizer',vectorizer),
               	('clf',clf)])
parameters = {'Vectorizer__ngram_range':[(1,1),(1,3)],
          	'clf__C':(0.01, 1.0, 10.0),
          	'clf__penalty':['l1', 'l2'],
          	'clf__dual':[False]}

grid_search = GridSearchCV(pipe, parameters, cv=5, scoring='accuracy')
grid_search.fit(X_train, y_train)
results=grid_search.cv_results_
for mean_score, params in sorted(zip(results['mean_test_score'],results['params']), reverse=True):
    print('accuracy - {0:.2%}, params: {1}'.format(mean_score, params))

accuracy - 94.95%, params: {'Vectorizer__ngram_range': (1, 3), 'clf__C': 10.0, 'clf__dual': False, 'clf__penalty': 'l1'}
accuracy - 94.56%, params: {'Vectorizer__ngram_range': (1, 1), 'clf__C': 10.0, 'clf__dual': False, 'clf__penalty': 'l1'}
accuracy - 93.90%, params: {'Vectorizer__ngram_range': (1, 1), 'clf__C': 10.0, 'clf__dual': False, 'clf__penalty': 'l2'}
accuracy - 93.76%, params: {'Vectorizer__ngram_range': (1, 1), 'clf__C': 1.0, 'clf__dual': False, 'clf__penalty': 'l1'}
accuracy - 93.65%, params: {'Vectorizer__ngram_range': (1, 1), 'clf__C': 1.0, 'clf__dual': False, 'clf__penalty': 'l2'}
accuracy - 93.58%, params: {'Vectorizer__ngram_range': (1, 3), 'clf__C': 1.0, 'clf__dual': False, 'clf__penalty': 'l1'}
accuracy - 93.48%, params: {'Vectorizer__ngram_range': (1, 3), 'clf__C': 10.0, 'clf__dual': False, 'clf__penalty': 'l2'}
accuracy - 92.46%, params: {'Vectorizer__ngram_range': (1, 3), 'clf__C': 1.0, 'clf__dual': False, 'clf__penalty': 'l2'}
accuracy - 72.33%, params: {'Vectorizer__ngram_range': (1, 1), 'clf__C': 0.01, 'clf__dual': False, 'clf__penalty': 'l2'}
accuracy - 64.05%, params: {'Vectorizer__ngram_range': (1, 3), 'clf__C': 0.01, 'clf__dual': False, 'clf__penalty': 'l2'}
accuracy - 62.57%, params: {'Vectorizer__ngram_range': (1, 1), 'clf__C': 0.01, 'clf__dual': False, 'clf__penalty': 'l1'}
accuracy - 62.19%, params: {'Vectorizer__ngram_range': (1, 3), 'clf__C': 0.01, 'clf__dual': False, 'clf__penalty': 'l1'}

Отже, ми створили екземпляр класу Pipeline й словник параметрів parameters типу {назва параметра: список або кортеж можливих значень} (зверніть увагу, що якщо ми шукаємо значення параметрів з об’єкта Pipeline, то повинні вказувати "ідентифікатор кроку"__«назва параметра"). Далі ми передали ці об’єкти в екземпляр класу GridSearchCV, вказали параметр cv, що означає кількість підмножин, на яку потрібно розбити навчальну вибірку та вказали параметр для визначення якості моделі. Як бачимо, результати показані одним класифікатором, екстремально різняться залежно від гіперпараметрів.

Не забувайте, що ви можете трактувати деякі кроки підготовки даних як гіперпараметри. Наприклад, решітковий пошук автоматично з’ясовує, окрім параметрів класифікатора, параметр ngram_range, що є параметром TfidfVectorizer і вказує на мінімальний та максимальний порядки n-грам у вокабулярі: параметр (1, 1) вказує, що у вокабулярі повинні бути лише юніграми, параметр (1, 3) вказує, що у вокабулярі мають бути юніграми, біграми й триграми, а вектори текстів будуть містити входження всіх цих токенів.

Показник mean_test_score вказує на середню точність кожного класифікатора на кожній комбінації тестових підвибірок. Оскільки ми вказали cv = 5, у нас є 5 підвибірок, отже, кожен варіант моделі тренувався 5 разів на 4 підвибірках і тестувався на 5-й невикористаній. Як результат, GridSearchCV є ефективним методом пошуку гіперпараметрів, але надзвичайно витратним обчислювально (наприклад, якщо в нас 4 параметри, які потрібно оптимізувати, кожен з яких приймає лише 4 можливі значення з кількістю підвибірок 5, у нас буде побудовано (4 ^ 4) * 5 = 1280‬ моделей!).

Якщо простір пошуку гіперпараметра є великим, то часто краще застосовувати рандомізований пошук (randomized search), який реалізовано класом Randomi zedSearchCV. Цей клас можна використовувати аналогічно до класу GridSearchCV, але замість випробування всіх можливих комбінацій він оцінює задану кількість випадкових комбінацій, вибираючи випадкове значення для кожного гіперпараметра на будь-який ітерації. Приклад:

vectorizer=TfidfVectorizer()
clf=LinearSVC()
pipe=Pipeline([('Vectorizer',vectorizer),
               	('clf',clf)])
parameters = {'Vectorizer__ngram_range':[(1,1),(1,3)],
          	'clf__C':(0.01, 1.0, 10.0),
          	'clf__penalty':['l1', 'l2'],
          	'clf__dual':[False]}

random_search = RandomizedSearchCV(pipe, parameters, cv=5, scoring='accuracy',n_iter=5)
random_search.fit(X_train, y_train)
results=random_search.cv_results_
for mean_score, params in sorted(zip(results['mean_test_score'],results['params']), reverse=True):
    print('accuracy - {0:.2%}, params: {1}'.format(mean_score, params))
accuracy - 95.02%, params: {'clf__penalty': 'l1', 'clf__dual': False, 'clf__C': 10.0, 'Vectorizer__ngram_range': (1, 3)}
accuracy - 93.65%, params: {'clf__penalty': 'l2', 'clf__dual': False, 'clf__C': 1.0, 'Vectorizer__ngram_range': (1, 1)}
accuracy - 93.48%, params: {'clf__penalty': 'l2', 'clf__dual': False, 'clf__C': 10.0, 'Vectorizer__ngram_range': (1, 3)}
accuracy - 64.05%, params: {'clf__penalty': 'l2', 'clf__dual': False, 'clf__C': 0.01, 'Vectorizer__ngram_range': (1, 3)}
accuracy - 62.57%, params: {'clf__penalty': 'l1', 'clf__dual': False, 'clf__C': 0.01, 'Vectorizer__ngram_range': (1, 1)}

Ми обмежилися лише 5 ітераціями (замість 12), але отримали ті самі гіперпараметри моделі, що й із GridSearchCV. Враховуючи, що в мене була вибірка малого розміру, я використовував клас GridSearchCV.

Клас GridSearchCV дає широкі можливості щодо підбору гіперпараметрів, але для кращого сприйняття було б добре відображати результати на графіку. Провівши пошук у мережі й деяку модернізацію під себе, я підготував функцію, яка на базі GridSearchCV проводить пошук гіперпараметрів одразу за кількома моделями, а на вході приймає словник типу {назва моделі: кортеж (або список), що складається з двох елементів: перший — це модель, яку потрібно оптимізувати, другий — словник (або список словників) з параметрами оптимізації}. Це трохи відрізняється від того, що пропонує scikit-learn, але мені так було зручніше: що й модель, і параметри разом. Ось приклад:

models_params = {
'CountVectorizer+LinearSVC': (Pipeline([('Vectorizer',TfidfVectorizer()),
                        	('clf',LinearSVC())]),
              	[{'Vectorizer__ngram_range':[(1,1),(1,3)],
               	'clf__C':(0.01, 1.0, 10.0),
               	'clf__penalty':['l2'],
               	'clf__dual':[True]},
              	{'Vectorizer__ngram_range':[(1,1),(1,3)],
               	'clf__C':(0.01, 1.0, 10.0),
               	'clf__penalty':['l1', 'l2'],
               	'clf__dual':[False]}]),
'TfidfVectorizer+LinearSVC': (Pipeline([('Vectorizer',TfidfVectorizer()),
                        	('clf',LinearSVC())]),
              	{'Vectorizer__ngram_range':[(1,1),(1,3)],
               	'clf__C':(0.01, 1.0, 10.0),
               	'clf__penalty':['l1', 'l2'],
               	'clf__dual':[False]})
}

GridSearchCV_Classifiers(X=X_train, y=y_train,
                     	models_params=models_params,scoring='accuracy',cv=5)

Як параметри функція GridSearchCV_Classifiers отримує навчальну вибірку, словник models_params, показник якості моделі, який потрібно використовувати, — scoring і параметр cv — кількість підвибірок перехресної перевірки.

Ось гістограма 10 найкращих моделей:

Рис. 1. Гістограма результатів 10 найкращих моделей

На функції GridSearchCV_Classifiers я зациклюватися не буду (її код на GitHub у модулі EstimatorSelectionHelper), а звідси я взяв базовий клас EstimatorSelectionHelper.

Функцію, яка тестуватиме різні моделі, ми визначили, залишилося визначитися з метрикою оцінки якості моделей. Перед переходом до самих метрик необхідно згадати важливу концепцію для опису цих метрик у термінах помилок класифікації — confusion matrix (матрицю помилок). Матрицю помилок я вже згадував, але не давав до неї пояснення. Для пояснення confusion matrix візьмімо класифікацію за забудованістю земельних ділянок:

Рис. 2. Матриця помилок

Тут True Positive (TP) і True Negative (TN) — правильно класифіковані екземпляри відповідно для незабудованих та забудованих земельних ділянок. Таким чином, помилки класифікації бувають двох видів: False Negative (FN) і False Positive (FP).

Точність (accuracy) — інтуїтивно зрозуміла, очевидна й майже невикористана метрика — частка правильних відповідей алгоритму:

Ця метрика не відображає дійсності в задачах з неправильно розподіленими класами, як у нашому випадку (згадайте приклад з 85,53%-ю точністю класифікації за забудованістю ділянок).
Для оцінки якості роботи алгоритму на кожному з класів окремо введемо метрики precision і recall:

Precision можна інтерпретувати як частку об’єктів, названих класифікатором позитивними (Positive) і які є правильно класифікованими, а recall показує, яку частину від усіх об’єктів позитивного класу було класифіковано.

Precision і recall часто зручно об’єднувати в єдину метрику під назвою міра F (F score) чи її частковий випадок — міру F1 (F1 score), особливо якщо вам потрібен простий спосіб порівняння двох класифікаторів. Міра F1 — це середнє гармонійне (harmonic mean) precision і recall, що дає можливість об’єктивно вибрати найкращу модель з найкращими значеннями precision та recall водночас. У результаті класифікатор отримає високу міру F1, тільки якщо високими є precision і recall:

Я за основу в проєкті взяв міру F1. Слід зауважити, що існують й інші метрики, наприклад ROC-крива. Усі доступні метрики для класифікації бібліотеки scikit-learn тут.

Приклад порівняння результатів класифікаторів з базовими параметрами

Отже, функцію для тестування моделей я підготував, з мірою якості визначився й подумав, що тепер можна переходити до побудови класифікаторів. Але ні, ще залишився один момент: під час застосування перехресної перевірки (cross-validation) я ділю навчальний набір як мінімум на 5 підвибірок, однак у мене лише 5 екземплярів оголошень про земельні ділянки для ведення товарного сільського господарства. Тож якщо заберу третину оголошень на тестову вибірку, у мене не залишиться достатньої кількості екземплярів для порівняння за допомогою перехресної перевірки.

Враховуючи всю специфіку, а також імовірність появи більшої кількості екземплярів подібних ділянок у майбутньому, я використав прийом доповнень даних (Data Augmentation). Він передбачає генерування нових навчальних зразків на основі наявних, що штучно збільшує розмір навчального набору. Хитрість у тому, щоб генерувати реалістичні навчальні зразки; в ідеалі людина не повинна сказати, які зразки було згенеровано, а які ні. Тут зауважу, що я й не намагався генерувати «ідеальні зразки», однак, враховуючи, що в усіх екземплярах оголошень були слова ’для ведення товарного сільськогосподарського виробництва’, вирішив додавати до них випадкову послідовність слів (враховуючи, що для моделі «мішка слів» послідовність слів значення не має).

Код функції Data_Augmentation (я додавав лише 34 екземпляри) й приклад побудови базових класифікаторів нижче:

def Data_Augmentation(number):
    unigrams_list=['продаж','земельна', 'ділянка', 'іншої', 'інфраструктура', 'інструменту', 'інвентарю', 'ізумрудє', 'івасюка',
               	'яром', 'яворівського', 'шанове', 'чудова','цін', 'цільових', 'цілу', 'цілорічно', 'участке', 'тільки', 'тухолька',
               	'турківський', 'твердій','сухий', 'суха', 'сусідніх', 'сусідні', 'судова',  'сторони', 'сто', 'стихії','селі', 'села',
'сайті', 'руська', 'росташування','рокитне', 'розташовану', 'розташований', 'розміщені', 'розміщення', 
'розміщений', 'розмірі', 'розділена', 'покупцю', 'показ', 'повідомлення', 'питання']
   
    random.seed(0)
    random_words=[unigrams_list[index] for index in random.sample(range(len(unigrams_list)), len(unigrams_list))]
    tsv='для ведення товарного сільськогосподарського виробництва'
    df = pd.DataFrame(columns=['text','land_types','built_up'])
    for sample_index in range(number):
        df = df.append({'text': tsv+' '+" ".join(random.sample(random_words, random.randint(0,20))),'land_types':6,'built_up':0}, ignore_index=True)
    return df.astype({'text':'object','land_types': 'int32','built_up': 'int32'})

land_data=land_data.append(Data_Augmentation(34), ignore_index=True)
data_class_display(dataframe=land_data,class_column='land_types',expl_lables=land_types)
data_class_display(dataframe=land_data,class_column='built_up',expl_lables=built_up)

X_train_zab, X_test_zab, y_train_zab, y_test_zab=train_test_split(land_data['text'],land_data['built_up'],
                                                              	stratify=land_data['built_up'],
                                                              	test_size=0.33,random_state=0)

X_train, X_test, y_train, y_test=train_test_split(land_data['text'],land_data['land_types'],
                                              	stratify=land_data['land_types'],
                                              	test_size=0.33,random_state=0)

models_params_0={
      	'ExtraTreesClassifier': [Pipeline([('Vectorizer',TfidfVectorizer()),('clf',ExtraTreesClassifier())]),
                                   	{'Vectorizer__ngram_range':[(1,1)]}],
      	'RandomForestClassifier': [Pipeline([('Vectorizer',TfidfVectorizer()),('clf',RandomForestClassifier())]),
                                   	{'Vectorizer__ngram_range':[(1,1)]}],    	 
      	'GradientBoostingClassifier': [Pipeline([('Vectorizer',TfidfVectorizer()),('clf',GradientBoostingClassifier())]),
                                   	{'Vectorizer__ngram_range':[(1,1)]}],
      	'LinearSVC': [Pipeline([('Vectorizer',TfidfVectorizer()),('clf',LinearSVC())]),
                                   	{'Vectorizer__ngram_range':[(1,1)]}],    
      	'LogisticRegression': [Pipeline([('Vectorizer',TfidfVectorizer()),('clf',LogisticRegression())]),
                                   	{'Vectorizer__ngram_range':[(1,1)]}],
      	'DecisionTreeClassifier': [Pipeline([('Vectorizer',TfidfVectorizer()),('clf',tree.DecisionTreeClassifier())]),
                                   	{'Vectorizer__ngram_range':[(1,1)]}],      	 
      	'BaggingClassifier': [Pipeline([('Vectorizer',TfidfVectorizer()),('clf',BaggingClassifier())]),
                                   	{'Vectorizer__ngram_range':[(1,1)]}],      	 
      	'BalancedBaggingClassifier': [Pipeline([('Vectorizer',TfidfVectorizer()),('clf',BalancedBaggingClassifier())]),
                                   	{'Vectorizer__ngram_range':[(1,1)]}],
      	'MLPClassifier': [Pipeline([('Vectorizer',TfidfVectorizer()),('clf',MLPClassifier())]),
                                   	{'Vectorizer__ngram_range':[(1,1)]}],	 
      	'ComplementNB': [Pipeline([('Vectorizer',TfidfVectorizer()),('clf',ComplementNB())]),
                                   	{'Vectorizer__ngram_range':[(1,1)]}],	 
    
}



GridSearchCV_Classifiers(X=X_train, y=y_train,
                     	models_params=models_params_0,scoring='f1_macro',cv=5)

GridSearchCV_Classifiers(X=X_train_zab, y=y_train_zab,
                     	models_params=models_params_0,scoring='f1_macro',cv=5)

Як писав від початку, я мав на меті випробувати максимальну кількість класифікаторів. Тут підібрав лише кілька, наведу їх короткий опис.

DecisionTreeClassifier — реалізація алгоритму дерева ухвалення рішення (алгоритм описано в першій частині статті).

RandomForestClassifier — реалізація алгоритму випадкових лісів (Random Forest). Інтуїтивно алгоритм випадкових лісів можна розглядати як ансамбль дерев рішень.

ExtraTreesClassifier — згідно з документацією, це деякий варіант алгоритму випадкових лісів.

GradientBoostingClassifier — згідно з документацією, це варіант алгоритму бустингу (Boosting). У бустингу ансамбль складається з дуже простих базових класифікаторів (слабких учнів). Типовим прикладом слабкого учня є однорівневе дерево рішень (пеньок рішень). Ключова ідея бустингу полягає в тому, що він зосереджений на тренувальних зразках, які важко класифікувати, тобто для поліпшення якості ансамблю дає слабким учням у підсумку навчитися на помилково класифікованих випробуваннях.

BaggingClassifier — це варіант алгоритму бегінгу (Bagging). У бегінгу ми видобуваємо з вихідного тренувального набору бутстрап-вибірки (вибираються випадкові випробування з поверненням), на яких навчаємо незалежні класифікатори, а результат класифікації визначається шляхом голосування. Звідси й назва терміна — bagging, тобто bootstrap aggregating (агрегування бутстрап-вибірок).

BalancedBaggingClassifier — це варіант алгоритму бегінгу (Bagging) з бібліотекою imblearn. Згідно з документацією, BalancedBaggingClassifier приймає ті самі параметри, що й Scikit-learn BaggingClassifier, додаючи два додаткові параметри: sampling_strategy та replacement. Бібліотеку imblearn корисно використовувати тоді, коли маємо нерівномірний розподіл класів.

ComplementNB — це реалізація одного з алгоритмів сімейства наївних класифікаторів Баєса (також описано в першій частині статті).

MLPClassifier — це варіант реалізації багатошарового перцепрона.

LogisticRegression — це варіант реалізації логістичної регресії (Logistic Regression).

LinearSVC — це один з варіантів реалізації методу опорних векторів (support vector machine). У SVM наше завдання оптимізації полягає в тому, щоб максимізувати «проміжок». «Проміжок» — відстань між роздільною гіперплощиною й найближчими до цієї гіперплощини тренувальними зразками, так званими опорними векторами.

Пам’ятайте: класи класифікаторів, що ґрунтуються на певних алгоритмах, реалізовано в кожній бібліотеці за своєю логікою й математичним обґрунтуванням. Вони можуть відрізнятися від «класичних» інтерпретацій і давати дещо інші результати. На це потрібно зважати!

Ось результати:

Рис. 3. 1. Гістограма результатів базових класифікаторів для класифікації за типами ділянок

Рис. 3. 2. Гістограма результатів базових класифікаторів для класифікації за забудованістю ділянок

Як бачимо, одним з найкращих класифікаторів став BaggingClassifier (не забуваємо, що під час кожного запуску програми результати дещо відрізнятимуться), але це лише перша спроба. Спробуймо покращити наші результати.

Покращення якості класифікатора на основі методу опорних векторів

Для покращення результатів на прикладі класу LinearSVC додамо стемер, а також застосуємо один з методів машинного навчання, що має назву відбір ознак (feature selection), суть якого полягає у відборі підмножини значущих ознак з усього набору. На нашому прикладі це означає, що з усього вокабуляра, підготовленого класом TfidfVectorizer, ми виберемо лише значущі слова (токени), а решту відкинемо. Я реалізовував відбір ознак за допомогою класу SelectFromModel (перелік усіх класів для відбору ознак, реалізованих у scikit-learn, тут).

Ось реалізація для класу LinearSVC (тут дещо перевизначено метод ua_tokenizer_sklearn під специфіку scikit-learn, якщо порівняти з прикладом з першої частини):

from ukrainian_stemmer import UkrainianStemmer
def ua_tokenizer_sklearn(text,stemmer=True):
    """ Tokenizer for Ukrainian language, returns only alphabetic tokens.
    
    Keyword arguments:
    text -- text for tokenize
    stemmer -- if True use UkrainianStemmer for stemming words (default True)
     	 
    """
    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)
    if stemmer==True:
        return [UkrainianStemmer(word).stem_word() for word in nltk.word_tokenize(text) if word.isalpha()]
    else:
        return [word for word in nltk.word_tokenize(text) if word.isalpha()]
    
models_params_1={
               	 
      	'LinearSVC': [Pipeline([('Vectorizer',None),
                                            ('feature_selection',SelectFromModel(ExtraTreesClassifier())),
                                            ('clf',LinearSVC())]),
                                   	[{'Vectorizer':[TfidfVectorizer(),CountVectorizer()],
                                    'Vectorizer__ngram_range':[(1,1),(1,3)],
                                    'Vectorizer__tokenizer':[None, ua_tokenizer_sklearn],
                                    'feature_selection__estimator':[ExtraTreesClassifier()],
                                    'feature_selection__threshold':[0.00001,0.0001,0.001],
                                    'clf__loss':['squared_hinge'],
                                    'clf__C':[0.1, 1.0,3.0],
                                    'clf__penalty':['l2','l1'],
                                    'clf__class_weight':[None,'balanced'],
                                         'clf__dual':[False]                                 	 
                                         },
                                   	{'Vectorizer':[TfidfVectorizer(),CountVectorizer()],
                                    'Vectorizer__ngram_range':[(1,1),(1,3)],
                                    'Vectorizer__tokenizer':[None, ua_tokenizer_sklearn],
                                    'feature_selection__estimator':[LinearSVC()],
                                    'feature_selection__threshold':[0.5,0.3,0.1,0.01],
                                    'clf__loss':['squared_hinge'],
                                    'clf__C':[0.1, 1.0,3.0],
                                    'clf__penalty':['l2','l1'],
                                    'clf__class_weight':[None,'balanced'],
                                     'clf__dual':[False]                                 	 
                                     }]]
    
}


GridSearchCV_Classifiers(X=X_train, y=y_train,
                     	models_params=models_params_1,scoring='f1_macro',cv=5)

GridSearchCV_Classifiers(X=X_train_zab, y=y_train_zab,
                     	models_params=models_params_1,scoring='f1_macro',cv=5)

і результати:

Рис. 4. 1. Гістограма результатів класифікатора LinearSVC для класифікації за типами ділянок

Рис. 4. 2. Гістограма результатів класифікатора LinearSVC для класифікації за забудованістю ділянок

Як бачимо, якість класифікатора покращилася порівняно з базовим класифікатором для класифікації за типами ділянок на понад 10%. А класифікація за забудованістю покращилася на 1,5%. Непогано, я вважаю.

Враховуючи, що ми використали метод відбору ознак, потрібно згадати, що існують й інші методи зменшення розмірності даних. Наприклад, метод головних компонент (МГК, англ. principal component analysis, PCA) — один з основних способів зменшення розмірності даних, втративши найменшу кількість інформації. Мені стало цікаво, що буде, якщо використати його для відображення векторів ознак класів на графіку. Ось результат:

from data_print import PCA_text_data_display
PCA_text_data_display (X_train,y_train,
                   	tokenizer=ua_tokenizer_sklearn,
                   	Vectorizer=TfidfVectorizer,
                   	expl_lables=land_types,
                   	n_components=3,
                   	feature_selection=True,
                   	selector=SelectFromModel(estimator=ExtraTreesClassifier(n_estimators=500),threshold=0.005))
PCA_text_data_display (X_train_zab,y_train_zab,
                   	tokenizer=ua_tokenizer_sklearn,
                   	Vectorizer=TfidfVectorizer,
                   	expl_lables=built_up,
                   	n_components=3,
                   	feature_selection=True,
                   	selector=SelectFromModel(estimator=ExtraTreesClassifier(n_estimators=500),threshold=0.005))

Рис. 5. Графік використання функції PCA_text_data_display

На деталях реалізації функції PCA_text_data_display не зупинятимуся, ось посилання. У результаті проєкціювання в тривимірний простір векторів текстів оголошень для задачі класифікації ділянок за типами класи типів стали чітко окресленими. Щоправда, для задачі класифікації за забудованістю цього не спостерігаємо.

Також у реалізації моделі «мішка слів» у scikit-learn є один недолік: тут немає методу, подібного до show_most_informative_features у NLTK. Така функція потрібна, щоб зрозуміти, які токени залишаються для подачі на класифікатор після використання класу SelectFromModel. Реалізуємо таку функцію:

def show_most_informative_features(vectorizer, feature_selection,expl_lables,n=10):
    feature_names = vectorizer.get_feature_names()
    mask=feature_selection._get_support_mask()
    estimator=feature_selection.estimator_
    coef_ = getattr(feature_selection.estimator_, "coef_", None)
    importances = getattr(feature_selection.estimator_, "feature_importances_", None)  	 
    features={}
    if importances is None and coef_ is not None:
        for cls_num in range(0,len(feature_selection.estimator_.coef_)):
            if len(feature_selection.estimator_.coef_)==1:
                classes_name=expl_lables[estimator.classes_[cls_num+1]]
            else:    
                classes_name=expl_lables[estimator.classes_[cls_num]]
            sorted_list=sorted([(estimator.coef_[cls_num][i],feature_names[i]) for i in range (0,len(mask)) if mask[i]!=False], reverse=True)
            features['{} - позитивні'.format(classes_name)] = sorted_list[:n]
            features['{} - негативні'.format(classes_name)] = sorted_list[-n:]       	 

    elif importances is not None: 
        features['all'] = sorted([(estimator.feature_importances_[i],feature_names[i]) for i in range (0,len(mask)) if mask[i]!=False], reverse=True)[:n]
   
    print(pd.DataFrame.from_dict(features))

На вході ця функція приймає об’єкти vectorizer і feature_selection з нашого конвеєра, кількість елементів, які потрібно показувати, та підтвердження, чи потрібно друкувати коефіцієнти. Ось результати для задачі класифікації за забудованістю:

Результат, коли оцінювачем у функції SelectFromModel є клас ExtraTreesClassifier:

 Pipe1=Pipeline([('Vectorizer',TfidfVectorizer(ngram_range=(1, 3),
                                         	tokenizer=ua_tokenizer_sklearn)),
           	('feature_selection',SelectFromModel(ExtraTreesClassifier(),
                                                	threshold=0.001))])            	 
    
Pipe1.fit(X_train_zab,y_train_zab)  
show_most_informative_features(Pipe1.steps[0][1],Pipe1.steps[1][1],expl_lables=built_up)
all
0  (0.027820556743181256, будинок)	 
1  (0.009925548050353146, фундамент)   
2  (0.009366393952705722, ділянц) 	 
3  (0.007016270744791473, цеглян) 	 
4  (0.006329524078816021, ремонт) 	 
5  (0.0062713247531944825, знос)  	 
6  (0.006126090007360684, стар будинок)
7  (0.005471200211758912, будиночок)   
8  (0.005285449319357477, на ділянц)   
9  (0.005045974490684933, є)    	

Результат, коли оцінювачем у функції SelectFromModel є клас LinearSVC:

Pipe2=Pipeline([('Vectorizer',TfidfVectorizer(ngram_range=(1, 3),
                                         	tokenizer=ua_tokenizer_sklearn)),
           	('feature_selection',SelectFromModel(LinearSVC(),
                                                	threshold=0.2))])
Pipe2.fit(X_train_zab,y_train_zab)
show_most_informative_features(Pipe2.steps[0][1],Pipe2.steps[1][1],expl_lables=built_up)
Забудована - позитивні         	Забудована - негативні
0  (4.755694975607558, будинок) 	(-0.5008617837624585, ділянк в)  
1  (2.881727788631038, фундамент)   (-0.5166340426550243, ліс)  	 
2  (1.9421139577898345, стар)   	(-0.5721489510434887, будівництв)
3  (1.853268453175496, є)       	(-0.5867736986082818, рівн) 	 
4  (1.8349292592696425, на ділянц)  (-0.6371616986600317, забудов)   
5  (1.6653673057798901, ділянц) 	(-0.6659586746958388, форм) 	 
6  (1.5956318432456487, будиночок)  (-0.6913066656767047, поряд)	 
7  (1.3331525532775006, цеглян) 	(-0.6913881609496538, поруч)	 
8  (1.3127971403147802, з)      	(-0.7603787332575344, ділянк)    
9  (1.2098229612256934, кв)     	(-0.8651744592146488, дорог) 	

Як бачимо, для двох оцінювачів значення важливості параметрів кардинально різні. Це треба враховувати під час виставлення параметра threshold (у документації я цього уточнення ніде не бачив). Для оцінювачів бібліотеки scikit-learn, що мають атрибут coef_, ми можемо так дізнатися, які токени вказують на цільовий клас (значення > 0), а які свідчать, що це не він.

Позитивні результати для задачі класифікації за забудованістю відповідають дійсності, однак токени, які вказують, що земельні ділянки в оголошеннях незабудовані, насправді по своїй суті на це вказувати не можуть. Коли я вперше це побачив, то був шокований. Із цим потрібно було щось робити, і я знайшов рішення.

Після дослідження класу SelectFromModel я знайшов, як його можна «підправити» для усунення цієї проблеми.

Код:

from sklearn.feature_selection._from_model import _calculate_threshold
def get_feature_importances(estimator, norm_order=1):
    """Retrieve or aggregate feature importances from estimator"""
    importances = getattr(estimator, "feature_importances_", None)

    coef_ = getattr(estimator, "coef_", None)
    coef_[coef_<0] = 0 # всім коефіцієнтам меншим за 0 присвоїти значення 0

    if importances is None and coef_ is not None:
        if estimator.coef_.ndim == 1:
            importances = np.abs(coef_)

        else:
            importances = np.linalg.norm(coef_, axis=0,
                                     	ord=norm_order)

    elif importances is None:
        raise ValueError(
        		"The underlying estimator %s has no `coef_` or "
        		"`feature_importances_` attribute. Either pass a fitted estimator"
        		" to SelectFromModel or call fit before calling transform."
        		% estimator.__class__.__name__)

    return importances

class MySelectFromModel(SelectFromModel):
    def _get_support_mask(self):
        # SelectFromModel can directly call on transform.
        if self.prefit:
            estimator = self.estimator
        elif hasattr(self, 'estimator_'):
            estimator = self.estimator_
        else:
            raise ValueError('Either fit the model before transform or set'
                         	' "prefit=True" while passing the fitted'
                         	' estimator to the constructor.')
        scores = get_feature_importances(estimator, self.norm_order)# підредаговано назву функцій
        threshold = _calculate_threshold(estimator, scores, self.threshold)
        if self.max_features is not None:
            mask = np.zeros_like(scores, dtype=bool)
            candidate_indices = \
            		np.argsort(-scores, kind='mergesort')[:self.max_features]
            mask[candidate_indices] = True
        else:
            mask = np.ones_like(scores, dtype=bool)
        mask[scores < threshold] = False
        return mask

Тут за основу взято методи класу SelectFromModel, які я змінив (зміни позначено). А ось результат:

Pipe=Pipeline([('Vectorizer',TfidfVectorizer(ngram_range=(1, 1),
                                         	tokenizer=ua_tokenizer_sklearn)),
           	('feature_selection',MySelectFromModel(LinearSVC(),
                                                	threshold=0.2))])            	 
    
Pipe.fit(X_train_zab,y_train_zab)  

show_most_informative_features(Pipe.steps[0][1],Pipe.steps[1][1],expl_lables=built_up)
Забудована - позитивні               	Забудована - негативні
0  (4.252198547984502, будинок) 	(0.2020595255001346, осн)         	 
1  (3.711435874042212, фундамент)   (0.20152108215084089, праворуч)   	 
2  (2.5313796890088613, будиночок)  (0.20142753000771588, ват)        	 
3  (2.169940640270247, стар)    	(0.2009644653843658, моє)         	 
4  (1.9242830147167735, знос)   	(0.20073500472766584, виведен)    	 
5  (1.7729467155251732, вагончик)   (0.2005192120365571, розбудовуєтьсяцін)
6  (1.5987749775622229, ділянц) 	(0.2005192120365571, коопеаратив) 	 
7  (1.5021363256968672, хат)    	(0.2005192120365571, кадастов)    	 
8  (1.4839809169640683, будинк) 	(0.2005192120365571, дачівул)     	 
9  (1.4229984669289526, цегл)   	(0.20013248373296688, колодн) 

Як бачите, токенів, які б вказували на незабудовані земельні ділянки, немає. А тепер порівняймо результати з попередніми, отриманими за допомогою класифікатора LinearSVC:

models_params_1={
               	 
      	'LinearSVC': [Pipeline([('Vectorizer',None),
                                         	('feature_selection',MySelectFromModel(ExtraTreesClassifier())),                                       	 
                                         	('clf',LinearSVC())]),
                                  	 
                                   	{'Vectorizer':[TfidfVectorizer(),CountVectorizer()],
                                    	'Vectorizer__ngram_range':[(1,1),(1,3)],
                                    	'Vectorizer__tokenizer':[None, ua_tokenizer_sklearn],
                                    	'feature_selection__estimator':[LinearSVC()],
                                    	'feature_selection__threshold':[0.5,0.3,0.1,0.01],
                                    	'clf__loss':['squared_hinge'],
                                    	'clf__C':[0.1, 1.0,3.0],
                                    	'clf__penalty':['l2','l1'],
                                    	'clf__class_weight':[None,'balanced'],
                                     	'clf__dual':[False]                                 	 
                                     	}]
    	 
}



GridSearchCV_Classifiers(X=X_train_zab, y=y_train_zab,
                     	models_params=models_params_1,scoring='f1_macro',cv=5)

Рис. 6. Гістограма результатів класифікатора LinearSVC для класифікації за забудованістю ділянок

У результаті таких маніпуляцій загальні результати класифікатора LinearSVC дещо покращилися, попри зменшення розмірності даних (фактично ми відкинули всі значення, що, на думку класифікатора, вказували на клас «незабудовані ділянки»).

Результати найкращих класифікаторів

Провівши деякі дослідження (насправді я витратив дуже багато часу, бо хотів спробувати багато класифікаторів, а набагато краще відразу вибрати кілька найбільш підходящих і працювати лише з ними), наводжу результати найкращих класифікаторів, що я знайшов:

Рис. 7. 1. Гістограма результатів найкращих моделей класифікації за типами ділянок

Рис. 7. 2. Гістограма результатів найкращих моделей для класифікації за забудованістю ділянок

Один з найкращих результатів дає метод опорних векторів, що реалізовано класом LinearSVC.

Перейдімо до його перевірки на тестовому наборі. Для цього надрукуймо матриці невідповідностей за допомогою функції Best_Classifiers (зразок коду):

Рис. 8. 1. Матриця невідповідностей для класифікатора LinearSVC за типами ділянок

Рис. 8. 2. Матриця невідповідностей для класифікатора LinearSVC за забудованістю ділянок

Непогано, але за результатами чітко бачимо приклад перенавчання (overfitting), про що я вже згадував у першій частині. Тому, вважаю, найкращий вихід з такої ситуації —збільшити вибірку.

Важливо, щоб після перевірки на тестовому наборі не «підправляти» гіперпараметри моделі під тестовий набір, інакше наші результати, якими б хорошими вони не були, не можна вважати достовірними.

Класи VotingClassifier та StackingClassifier

Так, найкращу модель ми визначили й побудували, але чи можна ще якимось чином покращити наші результати? Можна спробувати побудувати ансамбль на основі навчених класифікаторів з використанням класів VotingClassifier або StackingClassifier. Як параметри вони отримують готові класифікатори й на їхніх результатах будують свої прогнози (мажоритарним голосуванням у випадку VotingClassifier і побудовою окремої моделі у випадку StackingClassifier), тобто вони комбінують результати окремих класифікаторів і будують єдину модель, якісні показники якої кращі ніж в окремих класифікаторів (хоча покращення для добре відкаліброваних моделей незначне).

Ось результати використання VotingClassifier (зауважте: під час застосування параметра voting=’soft’ ми не можемо використовувати клас LinearSVC, оскільки він не має методу predict_proba. Щоб вирішити проблему, я передаю модель на основі класу LinearSVC як аргумент у клас CalibratedClassifierCV, що має метод predict_proba (цього також немає в документації).

estimators_typy=dict_to_models(models_params_best_typy) # функція dict_to_models перетворює словник із молями в список готових екземплярів класифікаторів, models_params_best_typy - словник відібраних моделей, наведений на github
VotingClassifier_params_typy={
	'VotingClassifier':[VotingClassifier(estimators=estimators_typy),
                    	{'voting':['soft'],
                     	'weights':[(1, 1, 1, 1)]}]
}


Best_Classifiers(X_train, X_test, y_train, y_test,
                     	models_params=VotingClassifier_params_typy,expl_lables=land_types)

Рис. 9. 1. Матриця невідповідностей для класифікатора VotingClassifier за типами ділянок

estimators_zab=dict_to_models(models_params_best_zab)
VotingClassifier_params_typy={
	'VotingClassifier':[VotingClassifier(estimators=estimators_zab),
                    	{'voting':['soft'],
                     	'weights':[(1, 2, 3,4)]}] # ваги деяких класифікаторів можна збільшити для покращення класифікації
}


Best_Classifiers(X_train_zab, X_test_zab, y_train_zab, y_test_zab,
                     	models_params=VotingClassifier_params_typy,expl_lables=built_up)

Рис. 9. 2. Матриця невідповідностей для класифікатора VotingClassifier за забудованістю ділянок

Тепер StackingClassifier:

estimators_typy=dict_to_models(models_params_best_typy)
StackingClassifier_params_typy={
	'StackingClassifier':[StackingClassifier(estimators=estimators_typy),
                    	{'n_jobs':[-1],
                     	'final_estimator':[LinearSVC()]}]
}
Best_Classifiers(X_train, X_test, y_train, y_test,
                     	models_params=StackingClassifier_params_typy,expl_lables=land_types)

Рис. 10. 1. Матриця невідповідностей для класифікатора StackingClassifier за типами ділянок

estimators_zab=dict_to_models(models_params_best_zab_VotingClassifier)
StackingClassifier_params_zab={
	'StackingClassifier':[StackingClassifier(estimators=estimators_zab),
                    	{'n_jobs':[-1],
                     	'final_estimator':[LinearSVC()]}]
}
Best_Classifiers(X_train_zab, X_test_zab, y_train_zab, y_test_zab,
                     	models_params=StackingClassifier_params_zab,expl_lables=built_up)

Рис. 10. 2. Матриця невідповідностей для класифікатора StackingClassifier за забудованістю ділянок

Таким чином, я покращив результати класифікації за типами й забудованістю земельних ділянок максимально до 96,5% і 94,9% відповідно. Як завжди, покращення результатів після певного рівня дається дедалі важче й важче.

Висновки

Я навів приклади й описав специфіку роботи з бібліотекою scikit-learn. Це дуже хороша бібліотека для класичних методів машинного навчання. Вона дає змогу використовувати велику кількість моделей машинного навчання без особливих зусиль.
На цьому етапі можна було зупинитися й підбити підсумки, але я вирішив ще спробувати використати нейронні мережі, а що з того вийшло, розповім у третій частині статті.

Список рекомендованих джерел:

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось3
До обраногоВ обраному9
LinkedIn

Схожі статті




Немає коментарів

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.

Підписатись на коментарі