Разбиение коротких текстов на классы (кластеризация, часть 1)

Jun 02, 2021 16:58

"- Нам пора перейти к серьёзным отношениям. Я устала от неопределённости, устала ждать.
- Ну-ну, я просто фото лайкнул!.."

" - Я просто замкнутая.
- И давно замкнуло?"

"Есть ли во мне изюминка?
Во мне масса изюминок!
Да я просто кексик!"

В последнюю неделю развлекаюсь тем, что в Питоне пытаюсь рассортировать анекдоты по разным классам.



При поиске в гугле-яндексе "python text clastering" мы сразу натыкаемся на кучу страниц, где всё сводится к примерно такому рецепту:

0. Нормируем текст (во многих случаях пропущено).
1. Превращаем тексты в вектора при помощи TfidfVectorizer.
2. Кучу векторов при помощи k-means разбиваем на нужное нам число кластеров.

Для начала нужно получить тексты, которые будем разбивать на кластеры. В моем случае это анекдоты. Берем их из интернета (осваиваем BeautifulSoup). К сожалению ЖЖ не позволяет нормально оформлять исходные тексты, поэтому глянуть на них можно там - https://github.com/Imageman/aneki

А тут я коротко опишу некоторые трудности.

По хорошему к исходному тексту нужно применить лемматизацию (https://coderlessons.com/tutorials/mashinnoe-obuchenie/uchebnik-nltk/5-stemming-i-lemmatizatsiia), но я сходу не нашел решения. Поэтому использовал метод n-gramm для построения мешка осколков слов (делается это одной строчкой в TfidfVectorizer из библиотеки sklearn.feature_extraction.text: vectorizer = TfidfVectorizer(analyzer='char', dtype=np.float32, ngram_range=(5, 7), max_features=20000)). До этого я уже пользовался этим методом (немного видоизмененным) и он показал удовлетворительное качество работы. Т.е. на первом этапе я получил для каждого анекдота вектор фиксированной длинны (порядка десятка тысяч чисел).

Для разбиения на кластеры существует довольно много алгоритмов, но наиболее простой и быстрый является KMeans (из библиотеки sklearn.cluster). Есть ещё немного быстрее - MiniBatchKMeans, но он дает чуточку менее стабильные результаты. Так же я краем глаза глянул на AgglomerativeClustering (не пробовал, но глянуть туда, там про подбор числа кластеров) и AffinityPropagation (пробовал - медленно работает).

Попробовал и получилось. Мне так показалось. Дело в том, что мои 5000 анекдотов при разбиении на 100 кластеров разбиваются очень-очень неравномерно. Большинство кластеров содержат по 10-20 анекдотов и есть один очень жирный кластер на 3000 анекдотов. И как бы я не подбирал параметры для MiniBatchKMeans устойчиво не получается добиться примерно одинакового размера кластеров... Всё несколько осложняется тем, что алгоритм каждый раз разбивает по разному.

Поэтому я придумал велосипед - иерархическую кластеризацию. Тут есть два подхода: "от маленьких к большим" и "от большого к маленьким". К примеру linkage идет от маленьких кластеров к большим (тут статья с примером использования для кластеризации резюме). Но там идет расчет матрицы расстояний между всеми элементами, а это квадратичная функция - O(n^2). Поэтому я туда даже не стал смотреть, а пошел от больших кластеров к маленьким.

Сделал рекурсивную функцию, которая разбивает входные значения на два кластера (размеры кластеров как повезет). Далее смотрит размеры получившихся кластеров и слишком большие кластеры разбивает опять на два и т.д. пока все кластеры не станут меньше определенного порога. И это работает быстро (отчасти потому, что KMeans при разбиении на два класса делает это очень быстро). Результат работы удовлетворительный.

Что ещё нужно попробовать

Векторизация анекдотов через TfidfVectorizer довольно примитивная и нужно пробовать что-то вроде ruBERT (там же где-то рядом стоят ELMo и fastText). Есть предпросчитанные модели для русского языка http://docs.deeppavlov.ai/en/master/features/pretrained_vectors.html. Это должно позволить получить более качественные вектора (да к тому же меньшего размера).

Попробовать другие варианты кластеризации.

Исходный код https://github.com/Imageman/aneki
Продолжение https://imageman72.livejournal.com/49467.html

python, программирование

Previous post Next post
Up