Необоснованная эффективность простой матричной декомпозиции текстовых данных.

Кластеризация текстовых документов. тематическое моделирование ака. Тематический анализ — довольно распространенная проблема в НЛП. Такие вещи, как обнаружение «историй», о которых сообщается в нескольких новостных статьях, или темы, по которым сообщения могут быть сгруппированы, всплывают повсюду, и есть большая ценность в мощных решениях для этих проблем.

Но вам нужна некоторая базовая модель, чтобы сравнить с ней вашу причудливую кластерную модель глубокого обучения НЛП, чтобы доказать, что вы не просто балуетесь нейронными сетями, когда много более простое решение сработало бы отлично!

И для этого вам на самом деле не нужно идти намного дальше, чем довольно простая линейная алгебра, применяемая к довольно тупому представлению данных семейства мешков слов. Давайте рассмотрим пример, чтобы увидеть, насколько хорошо может работать такая простая вещь:

Проблема, которую мы решаем

У нас есть: куча новостных статей (заголовки и описания из каналов и Reddit) и некоторые метаданные, касающиеся их контекста и приобретения (мы проигнорируем это.

Мы хотим:выявить распространенные «истории» (надеюсь, примерно соответствующие «событиям» реального мира), на которые могут ссылаться несколько новостных статей, и иметь возможность предоставить для каждой статьи список похожих статьи, которые являются частью одной и той же истории или освещают одно и то же событие. Точнее, мы хотим сделать какую-то кластеризацию, но нам не нужно, чтобы она была полной, и в то же время нам нужны понятные описания обнаруженные кластеры.

Используя некоторые знания в конкретной предметной области, мы решили, что лучше проводить этот анализ отдельно для каждой недели(мы хотим получить такие же истории, что и человеческие новостной аналитик/читатель увидит в данных — слишком широкий охват приведет к слишком общим/абстрактным темам). Позже, в производственной системе, отдельная система будет сопоставлять темы по неделям и анализировать эволюцию тем, их разветвление и т. д. (мы НЕ будем освещать это, это слишком сложно… и часть «секретного соуса», который поможет продукту I' m работает над тем, чтобы значительно превзойти своих конкурентов 😉).

Доступные данные

Для этой статьи я извлек небольшое подмножество (предварительных) производственных данных, состоящих из данных новостных статей за 4 недели (название, описание + другие вещи, которые мы в основном будем игнорировать, такие как источник, категория источника, миниатюры и т. д.), с 9 марта 2020 г. по 6 апреля. Некоторые новостные статьи могут появляться в нескольких источниках (например, исходная история на CNN плюс ее публикация на Reddit). А данные на самом деле относятся к двум группам: «мировые новости» и «технические новости». Мы будем отделять данные мировых новостей и играть только с этой частью здесь — данные технических новостей имеют некоторые особенности, которые могут усложнить ситуацию, и мы хотим, чтобы они были короткими и удобочитаемыми.

Вот данные, а вот блокнот Colab с данными и кодом. (Возможно, вы захотите открыть его сбоку и просмотреть код, читая эту статью — но в любом случае, она также читабельна и самодостаточна, основные фрагменты кода включены в текст здесь. )



Для тех, кому лень переходить по ссылке, вот как это выглядит:

>>> with pd.option_context('display.max_rows', None,
                           'display.max_columns', None):
        print(df_raw[[
            'created_at', 'title', 'description',
            'c_main_article_domain', 'c_dchan_name'
        ]].head())
                       created_at  \
id                                  
342776 2020-03-30 10:54:17.773946   
344736 2020-04-01 09:59:05.147215   
348109 2020-04-05 05:59:58.600386   
342775 2020-03-30 10:54:17.540424   
342799 2020-03-30 10:54:56.449676
                                                    title  \
id                                                          
342776  'We'll die of hunger first': Despair as Zimbab...   
344736     US Navy captain pleads for help over outbreak.   
348109  Zoom admits user data ‘mistakenly’ routed thro...   
342775  Somali governor killed in al-Shabab suicide bo...   
342799  UK outbreak shows early signs of slowing, offi...
                                              description  \
id                                                          
342776  In a country dealing with a severe economic cr...   
344736                                                NaN   
348109                                                NaN   
342775  Abdisalan Hassan, Nugaal region governor, kill...   
342799  The UK could remain under coronavirus emergenc...
       c_main_article_domain     c_dchan_name  
id                                             
342776         aljazeera.com       Al Jazeera  
344736             bbc.co.uk   Uplifting News  
348109                ft.com    /r/Technology  
342775         aljazeera.com       Al Jazeera  
342799               cnn.com  CNN Top Stories
                       created_at  \
id                                  
342776 2020-03-30 10:54:17.773946   
344736 2020-04-01 09:59:05.147215   
348109 2020-04-05 05:59:58.600386   
342775 2020-03-30 10:54:17.540424   
342799 2020-03-30 10:54:56.449676
                                                    title  \
id                                                          
342776  'We'll die of hunger first': Despair as Zimbab...   
344736     US Navy captain pleads for help over outbreak.   
348109  Zoom admits user data ‘mistakenly’ routed thro...   
342775  Somali governor killed in al-Shabab suicide bo...   
342799  UK outbreak shows early signs of slowing, offi...
                                              description  \
id                                                          
342776  In a country dealing with a severe economic cr...   
344736                                                NaN   
348109                                                NaN   
342775  Abdisalan Hassan, Nugaal region governor, kill...   
342799  The UK could remain under coronavirus emergenc...
       c_main_article_domain     c_dchan_name  
id                                             
342776         aljazeera.com       Al Jazeera  
344736             bbc.co.uk   Uplifting News  
348109                ft.com    /r/Technology  
342775         aljazeera.com       Al Jazeera  
342799               cnn.com  CNN Top Stories

Простая предварительная обработка

Мы бы предпочли работать здесь только со списком текстов, поэтому мы объединим текст и описание вместе (когда описание еще не содержит текста). Поскольку только у нескольких записей отсутствуют описания, мы их просто опустим. И мы также удалим URL-адреса и некоторые другие «мусорные» данные из описаний. (URL-адреса сбивают с толку токенизатор по умолчанию и раздувают словарный запас, и здесь мы не будем заморачиваться с более умным токенизатором.)

Затем мы разделили данные по неделям следующим образом:

# <week dates range>: <# articles>
2020-03-09..2020-03-16: 4336
2020-03-16..2020-03-23: 4290
2020-03-23..2020-03-30: 4204
2020-03-30..2020-04-06: 3951

Окончательные данные, разделенные по неделям, будут иметь следующую форму:

>>> print("texts_world_weeks ::", type(texts_world_weeks), len(texts_world_weeks))
  texts_world_weeks :: <class 'list'> 4
>>> print("texts_world_weeks[i] ::", type(texts_world_weeks[0]), texts_world_weeks[0].dtype, texts_world_weeks[0].shape)
  texts_world_weeks[i] :: <class 'pandas.core.arrays.string_.StringArray'> string (4070,)

Полный код см. в разделе Этот блокнот jupyter.

Тексты на номера

Во-первых: нет, мы не будем использовать здесь Word2Vec или встраивания. Мы просто будем будем настолько глупы, насколько сможем, и посмотрим, как далеко мы сможем зайти!

Таким образом, оставаясь в мире Bag-of-Words (где мы просто тупо подсчитываем вхождения слов), есть два способа получить векторы чисел из наших слов.

Во-первых, мы можем просто подсчитывать слова: для каждого документа (новостной статьи) у нас будет вектор-строка размером со весь словарь с количеством появлений слова в документе при этом. позиция слова (иначе ноль). что-л. нравится:

# INPUT:
[
    'This is the first document.',
    'This document is the second document.',
    'this is the third one.',
    'Is this the first document?',
]
# ---[ sklearn.feature_extraction.text.CountVectorizer ]--->
# VOCABULARY:
['document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
# COUNTS:
[[1           1        1     0      0         1      0        1]
 [2           0        1     0      1         1      0        1]
 [0           0        1     1      0         1      1        1]
 [1           1        1     0      0         1      0        1]]

(См. документы для CountVectorizer sklearn)

Но мы можем поступить немного умнее, масштабируя количество (то есть абсолютную частоту) термина в документе на величину, обратную доле документов, содержащих термин (на самом деле, мы предпочитаем использовать журнал этого) — следовательно, придается меньшее значение словам, которые появляются почти во всем тексте (поскольку их присутствие на самом деле мало что значит). Это называется взвешиванием терминов TF-IDF, и у него есть несколько альтернатив. Вот документы для того, который мы будем использовать. При этом также выполняется L2-нормализация, поэтому нормы каждого вектора-строки (квадратный корень из суммы всех квадратов значений) равны 1.

На данный момент мы будем придерживаться данных TF-IDF (позже мы также будем использовать простые подсчеты — некоторые модели вместо этого ожидают именно такие данные).

И у нас будет матрица чисел в форме #docs * #words_in_vocabulary:

from sklearn.feature_extraction.text import TfidfVectorizer
def make_tfidf_vectors(data_texts, **kwords):
    vectorizer_tfidf = TfidfVectorizer(
        stop_words=kwords.pop('stop_words', 'english'), **kwords)
    data_tfidf = vectorizer_tfidf.fit_transform(data_texts)
    vocab = np.array(vectorizer_tfidf.get_feature_names())
    return data_tfidf, vocab

>>> # keep the vocabulary list of words around, to later
>>> # be able to turn numbers back into words!
>>> # :: [(vectors, vocab)]
>>> tfidf_world_weeks = [make_tfidf_vectors(wtxt) for wtxt in texts_world_weeks]
>>> print("tfidf_world_weeks ::", type(tfidf_world_weeks), 
          len(tfidf_world_weeks))
  tfidf_world_weeks :: <class 'list'> 4
>>> print("tfidf_world_weeks[i][0] ::",
          type(tfidf_world_weeks[0][0]), 
          tfidf_world_weeks[0][0].dtype,
          tfidf_world_weeks[0][0].shape)
  tfidf_world_weeks[i][0] :: <class 'scipy.sparse.csr.csr_matrix'> float64 (4070, 14779)
>>> print("tfidf_world_weeks[i][1] ::",
          type(tfidf_world_weeks[0][1]), 
          tfidf_world_weeks[0][1].dtype,
          tfidf_world_weeks[0][1].shape)
  tfidf_tech_weeks[i][1] :: <class 'numpy.ndarray'> <U28 (14779,)

(Обратите внимание на тип scipy.sparse.csr.csr_matrix — это разреженная матрица, она не занимает память для хранения значений, которые на самом деле равны нулю, что здесь является хорошим выбором, хотя может потребоваться преобразование в полную пустую матрицу. через .todense(), чтобы его можно было использовать в коде, ожидающем классические матрицы.)

Теперь… у нас есть красивая шаткая матрица чисел… Что нам делать, когда у нас есть хорошая матрица чисел? Мы разложим его, конечно! В произведение различных матриц, которые, надеюсь, содержат ответ (о жизни, вселенной и обо всем…).

SVD — Разложение по сингулярным значениям

Самая известная матричная декомпозиция — СВД.

Если вы хотите узнать больше о том, как именно вычислять SVD и каковы его многочисленные значения, в настоящее время у вас есть множество действительно замечательных ресурсов. Вот некоторые из них (добавьте их в закладки позже!):

Из основ математики мы знаем, что SVD разлагает матрицу A на произведение:

  • матрица U с ортогональными столбцами (подумайте «перпендикулярно друг другу в любом многомерном пространстве, в котором они находятся»)
  • матрица V с ортогональными строками (на самом деле это V-транспонирование, если мы будем следовать стандартным математическим обозначениям, но мы назовем ее просто V, поскольку мы не в любом случае использовать not-transpose-V…)
  • и расположенная между ними диагональная матрица S(нули везде, кроме главной диагонали)

Это будет выглядеть примерно так (в нашем случае форма A будет «перевернутой», n > m, это не имеет значения, логика останется прежней… и это временно, как только мы сократим наш словарный запас, выбрасывая мусор прочь, и мы также соберем больше данных, входная матрица тоже будет для нас больше в высоту, чем в ширину):

О, и для настоящего правильного SVD (не рандомизированного SVD) это фактический знак равенства («=») — SVD — это точная математическая операция с четким определением, которая дает один и тот же результат для одного и того же ввода!

Теперь давайте подумаем, что это на самом деле означает для нашего случая:

Мы хотим сгруппировать документы (новостные статьи) в темы, где тема будет обозначаться словами, встречающимися в ней. В крайнем случае максимальное количество тем равно количеству документов. Это явно бесполезно… но если у нас есть ранжирование тем по некоторой «важности», мы можем просто принять во внимание верхние N и отбросить остальные.

Мы знаем, что хотели бы, чтобы наши темы не пересекались, поэтому слово или комбинация слов, описывающая одну тему, не должна часто появляться в других темах. Таким образом, пословная матрица, в которой каждая строка представляет собой тематический вектор в словесном пространстве, в идеале должна иметь ортогональные строки.

Кроме того, мы бы предпочли, чтобы документ появлялся только в одной теме. Или, по крайней мере, если бы в каждом документе была уникальная комбинация тем. Таким образом, матрица документ-по-теме будет иметь ортогональные столбцы.

Теперь, если мы на самом деле выполним SVD-разложение и посмотрим на формы результирующих матриц, мы увидим следующее:

>>> # grab only one week of data since this is slow
>>> vectors_week, vocab_week = tfidf_world_weeks[0]
>>> # convert to regular dense ndarray
>>> vectors_week_arr = vectors_week.todense()
>>> # full_matrices=False asks for the reduced form SVD
>>> U, s, V = scipy.linalg.svd(vectors_week_arr,
                               full_matrices=False)
>>> print(f"A ::", type(vectors_week_arr), vectors_week_arr.dtype, vectors_week_arr.shape)
  A :: <class 'numpy.matrix'> float64 (4070, 14779)
>>> print(f"U ::", type(U), U.dtype, U.shape)
  U :: <class 'numpy.ndarray'> float64 (4070, 4070)
>>> print(f"s ::", type(s), s.dtype, s.shape)
  s :: <class 'numpy.ndarray'> float64 (4070,)
>>> print(f"V ::", type(V), V.dtype, V.shape)
  V :: <class 'numpy.ndarray'> float64 (4070, 14779)

Итак, то, что у нас есть, на самом деле соответствовало бы примерно такой ситуации:

(Мы называем это «усеченным» или усеченным SVD, потому что «полная форма» SVD дает квадратную матрицу U, дополняя ее, чтобы получить «количество векторов, способных охватить все пространство» и т. д. и т. д. … но сокращенно- SVD удовлетворяет наши потребности здесь.)

Мы также можем выполнить некоторые проверки работоспособности, чтобы убедиться, что математические обещания SVD выполняются:

# sanity check 1: decomposition is correct
np.allclose(vectors_week_arr, U @ np.diag(s) @ V)  # -> True
# sanity check 2: U columns are orthonormal
np.allclose(U.T @ U, np.eye(U.shape[0]))  # -> True
# sanity check 3: V rows are orthonormal
np.allclose(V @ V.T, np.eye(V.shape[0]))  # -> True

Но чтобы увидеть, есть ли во всем этом какая-либо польза, нам нужно взглянуть на то, что у нас есть:

Из V мы можем получить удобочитаемое заголовок/описание для каждой темы — просто возьмите первые N слов с наивысшим баллом из каждой строки темы.

Form U мы можем получить две вещи:

  • (индекс) "лучшей" темы для каждого документа — просто выберите индекс с наивысшим значением для каждой строки
  • документы, набравшие наибольшее количество баллов по каждой теме — просто выберите (индексы) самые высокие значения для каждого столбца

Код для этого довольно красивый и элегантный на самом деле:

def get_doc_topics(U):
    """Get doc vs. topic idx matrix"""
    return U.argmax(axis=1)
def get_topic_docs(U, min_score=1e-10):
    """Get top scored docs idxs vs. topic matrix
    
    The idx of docs with score < min_score is replaced with -1.
    """
    docIdxs_topic = np.argsort(U, axis=0)
    docScores_topic = np.sort(U, axis=0)
    docIdxs_topic[docScores_topic < min_score] = -1
    return docIdxs_topic
def get_topic_titles(V, vocab, num_top_words=10):
    top_words = lambda t: [
        vocab[i]
        for i in np.argsort(t)[-1: -num_top_words-1: -1]]
    topic_words = ([top_words(t) for t in V])
    return [' '.join(t) for t in topic_words]

(Но также необходимы некоторые дополнительные помощники по отображению, чтобы вернуться от чисел к понятным словам для отображения.)

В любом случае, с этим мы наконец-то посмотрим, какие полезные результаты это может дать. Вот некоторые темы для нескольких новостных статей:

TOPIC: epicente 987 kno 0100 celebrating climb momentous festivities finalises grace
DOC: N Korea fires projectiles, after 'momentous' retaliation threat. 
North has stepped up military dril
TOPIC: biden sanders joe trump democratic bernie president presidential michigan house
DOC: Trump campaign argues Biden is just like Sanders. 
President Donald Trump's campaign held a backgrou
TOPIC: 671 chil consult eve bounced rehearsal disrupts tip crushing readiness
DOC: Coronavirus: Foreigners leave N Korea on first commercial flight for weeks. 
One ambassador who had
TOPIC: california 000 experts canceled say officials spain japan social war
DOC: Indian Wells Tennis Tournament Canceled Because of Coronavirus Outbreak. 
Public health officials in
TOPIC: size died meeting cuts failing row entering organization seen celebrities
DOC: Coronavirus Australia: 'Did you cough at me?' row on Sydney train. 
A cough on a train in Sydney spa

Некоторые из них могут иметь какой-то смысл, другие не очень — он четко видит некоторые закономерности в данных, но не обязательно те закономерности, которые интересуют человека.

Теперь давайте посмотрим на документы, получившие наибольшее количество баллов по каждой теме (из лучших тем):

------ 1 coronavirus trump president outbreak travel pandemic new said cases spread
   1 Trump suspends travel from Europe to the United States to fight coronavirus, UK exempted.  Under mou
   2 U.S. suspends travel from Europe as coronavirus forces Italy to tighten lockdown.  U.S. President Do
   3 Donald Trump reveals he has been tested for coronavirus – video.  Donald Trump has confirmed he took
------ 2 biden sanders joe trump democratic bernie president presidential michigan house
   1 Joe Biden calls for unity after big wins in Michigan, three other states.  Joe Biden scored decisive
   2 Bernie Sanders stays in race and looks forward to debating Biden in Arizona – video.  Bernie Sanders
   3 Trump campaign argues Biden is just like Sanders.  President Donald Trump's campaign
------ 3 oil saudi arabia prices price war russia markets market global
   1 Oil crashes after Saudi Arabia declares price war amid coronavirus.  Oil prices plummeted around 30%
   2 Saudi-Russia oil fight melts markets, targets US oil patch.  Oil prices are plunging after Saudi Ara
   3 Oil plunges 25%, hit by erupting Saudi-Russia oil price war.  Crude prices suffered their biggest da
------ 4 trump president donald house travel white tested oil administration emergency
   1 Coronavirus: Donald Trump declares US national emergency.  President Donald Trump says the decision
   2 President Donald Trump declares national emergency in U.S. over coronaviru
   3 President Trump Tests Negative For Coronavirus.  The result, according the White House, comes after
------ 5 positive tested iraq attack trudeau house said base troops minister
   1 Three American troops wounded in rocket attack in Iraq, U.S. official says.  Three U.S. troops have
   2 Trudeau’s wife tests POSITIVE for coronavirus, Canada's PM has 'no symptoms' but remains in self-iso
   3 Iraq base attack: US in retaliatory strikes on Iran-backed fighters.  The US says members of the mil

Теперь это выглядит гораздо более правдоподобно и полезно! Кажется, здесь есть какая-то ложная связь (например, между женой Трюдо и событиями в Ираке — по крайней мере, мы надеялись они на самом деле не связаны😨 … и у нас в руках может быть кусок неплохого генератора теорий заговора😈).

Помимо того, что это недостаточно хорошо решает нашу проблему, у SVD есть еще две проблемы с вычислительной производительностью:

  • это медленноооооо
  • он не может работать с разреженными матрицами, поэтому он может съесть тонну памяти: и, что еще хуже, с реальными данными он может фактически превысить допустимый размер памяти большинства предварительно скомпилированных версий базовые библиотеки BLAS — тьфу!🤮)

Рандомизированный-SVD/PCA

Оказывается, пока (1) мы заботимся только о небольшом, заранее выбранном числе тем и (2) мы не заботимся так уж о математической точности разложения ( мы а. не заботимся о том, чтобы произведение матричных коэффициентов точно реконструировало входную матрицу, и б. нас не волнует, если выполнение одного и того же вычисления дважды приводит к одному и тому же результату. результат для тех же входных данных — мы согласны с тем, что все «достаточно близко»), мы можем получить безумное (подумайте ››10x) повышение производительности по сравнению с математически точным разложением SVD!

Кроме того, для типов данных, которые у нас есть (нулевой центр и нормализация L2, поскольку они являются результатом нашей векторизации TF-IDF), оказывается, что SVD и PCA (Анализ основных компонентов) — это одно и то же (или более того). педантично, SVD можно использовать для вычисления PCA в этом конкретном случае).

Так как здесь мы также можем выбрать правдоподобно меньшие темы (намного меньшие, чем размер словаря), теперь все выглядит примерно так:

Если вы кое-что знаете о том, что делает PCA, теперь вы также можете понять, почему SVD вообще работает в нашем случае использования!

Аааааа… есть действительно быстрая реализация рандомизированного SVD/PCA от Facebook Research, которую мы можем использовать, fbpca (docs) представлена ​​здесь:

%%time
TOPICS_N = 25
results_fbpca_factors_world_weeks = []
for vectors_week, vocab_week in tfidf_world_weeks:
    rU, rs, rV = fbpca.pca(vectors_week, TOPICS_N)
    results_fbpca_factors_world_weeks.append({
        'U': rU,
        's': rs,
        'V': rV,
    })

CPU times: user 409 ms, sys: 249 ms, total: 658 ms
Wall time: 337 ms

Холли 🐮, это быстро — даже на виртуальном процессоре по умолчанию для ноутбука Colab!

Мы также можем видеть, что каждый раз, когда мы запускаем его, он дает очень-немного разные результаты. Построив распределение чисел в U из двух прогонов, мы также можем визуализировать его общую случайность таким образом:

Глядя на результаты, мы видим, что они очень похожи на те, что были получены выше при правильном SVD. Как и ожидалось. Случайность, кажется, не мешает нам и не помогает.

Но в любом случае нам нужно сделать лучше (а не просто быстрее)!

Интуитивно мы можем немного понять, что происходит, думая в терминах PCA-разложений, объясненной дисперсии yada, yada, yada…🥱

Но с этими отрицательными числами в матрицах все еще есть что-то странное — если бы это были данные изображения, со всеми положительными данными мы могли бы визуализировать разложение на «компоненты» или что-то в этом роде. как это. Кроме того, эта диагональная матрица в середине разложения — какого черта она должна быть там и что за интуиция стоит за умножением на нее?!

Понятно, что эта модель «видит закономерности», но совершенно иначе, чем человек. И для этого варианта использования нам нужна система кластеризации с человеческим поведением, что бы это ни значило. Нам нужно что-то более… понятное и объяснимое для наших данных!

Входить…

NMF — Неотрицательная матричная факторизация

Этот тип декомпозиции чем-то похож на SVD, но:

  • он создает только две факторные матрицы (мы назовем их W и H — они имеют ту же форму, что и U и V, и их можно интерпретировать аналогичным образом).
  • все числа в наших матрицах положительны

Теперь все будет выглядеть для нас так:

Это круто, потому что, начиная с этих предположений, будет намного легче понять, что делает наша модель, и настроить что-то или улучшить предварительную обработку данных, если она делает неправильные вещи.

Теперь оказывается, что NMF не уникален (есть несколько возможных декомпозиций) и вообще NP-труден. Но с практической точки зрения нас это не волнует: мы ожидаем, что различные неуникальные декомпозиции будут довольно похожими, и если у нас все в порядке с приближениями, то к NMF-декомпозиции применимо множество оптимизаций.

Давайте запустим духовку и приступим к выпечке этого пирога NMF:

results_nmf_factors_world_weeks = []
for vectors_week, vocab_week in tfidf_world_weeks:
    dcn = sklearn.decomposition.NMF(n_components=TOPICS_N)
    W = dcn.fit_transform(vectors_week)
    H = dcn.components_
    results_nmf_factors_world_weeks.append({
        'W': W,
        'H': H,
    })

…и (‹15 сек. спустя) мы видим, что у нас получилось. Давайте посмотрим, какие лучшие документы относятся к нашим основным темам:

------ 1 house white democrats package pelosi representatives aid coronavirus vote economic
   1 Pelosi says House to vote on coronavirus legislation on Thursday.  Legislation addressing the corona
   2 Factbox: What's in the coronavirus U.S. stimulus bill?.  Democrats in the U.S. House of Representati
   3 Pelosi says she hopes Trump administration will back House coronavirus package.  U.S. House Speaker
   4 U.S. House, White House near agreement on virus economic aid bill, vote likely.  The U.S. House of R
   5 Congress, White House close in on coronavirus aid bill as U.S. closures loom.  House Democrats and t
------ 2 biden sanders joe bernie democratic presidential michigan primary race debate
   1 Amid coronavirus crisis, Biden and Sanders go head-to-head in Democratic debate.  Joe Biden and Bern
   2 3 things to watch in the first one-on-one debate between Biden and Sanders.  Joe Biden and Bernie Sa
   3 In crushing blow to Bernie Sanders, Joe Biden scores big Michigan win.  Joe Biden rolled to commandi
   4 Biden and Sanders set to debate during coronavirus crisis.  Joe Biden and Bernie Sanders square off
   5 US Democratic primaries: Joe Biden wins big as six states vote.  Joe Biden pulls off a string of win
------ 3 oil saudi arabia prices price war russia rout crude opec
   1 Saudi-Russia oil fight melts markets, targets US oil patch.  Oil prices are plunging after Saudi Ara
   2 Oil crashes after Saudi Arabia declares price war amid coronavirus.  Oil prices plummeted around 30%
   3 Oil plunges 25%, hit by erupting Saudi-Russia oil price war.  Crude prices suffered their biggest da
   4 What's behind Saudi Arabia's oil price war with Russia?.  Saudi Arabia and Russia feud over oil pric
   5 Oil prices plunge, hit by erupting Saudi-Russia oil price war.  Oil prices suffered their biggest da
------ 4 trump president donald administration coronavirus says response address speech nation
   1 Trump's stock market gains have been cut in half by the coronavirus sell-off.  Just three weeks ago,
   2 Trump Won’t Be Getting a Coronavirus Test, His Doctor Says.  The statement from President Trump’s ph
   3 Coronavirus: Donald Trump declares US national emergency.  President Donald Trump says the decision
   4 Trump grows more irate as his attempts fail to contain coronavirus fallout.  • Trump will have a cor
   5 Text: What President Donald Trump told Americans about coronavirus.  The White House issued the foll
------ 5 positive tested tests bolsonaro negative coronavirus brazilian brazil test player
   1 Brazil President Bolsonaro's son claims father tested negative for coronavirus despite earlier repor
   2 Brazil’s Bolsonaro says he tested NEGATIVE for Covid-19 despite reports.  Brazilian President Jair B
   3 Brazil's Bolsonaro says he tested negative for coronavirus.  The comment on his Facebook page comes
   4 Brazil's President Bolsonaro tests positive for coronavirus.  Brazil’s President Jair Bolsonaro has
   5 Brazilian official who met with Trump tests positive for coronavirus, Bolsonaro to be tested.  Brazi

Здесь все еще есть некоторые проблемы - возможно, для этой цели все слова в нижнем регистре и использование безумного словаря, полного мусора (см. Код блокнота и раскомментируйте строки для отображения словарных слов), настройки по умолчанию для TfidfVectorizer sklearn не были такими хорошая идея. Давайте посмотрим, можем ли мы сделать лучше, чем это:

Настройка векторизации текста

Беглый взгляд на документы для TfidfVectorizer, которые мы используем показывает нам, что довольно просто попробовать нужные нам настройки.

Во-первых, чтобы сделать его чувствительным к регистру, есть простой параметр lowercase=False, который мы можем передать ему (на самом деле мы передадим его make_tfidf_vectors, который затем передается конструктору для класса векторизатора), код происходит так:

tfidf_cs_world_weeks = [
    make_tfidf_vectors(wtxt, lowercase=False)
                             # ^ added this!
    for wtxt in texts_world_weeks]
...

Но если мы посмотрим на результаты (которые я не буду показывать здесь в целях экономии места, см. ноутбук), мы увидим, что он работает хуже. Оказывается, что некоторые данные пишутся с заглавной буквы непоследовательно (например, заголовки, где каждое слово пишется с заглавной буквы и тому подобное), и это запутывает нашу модель. С более чистыми данными это могло бы сработать лучше (и избавило нас от будущих проблем с восстановлением заглавных букв, когда нам придется отображать названия тем…), но в реальном мире это просто не так…

Секунды, которые стоит попробовать, — это уменьшить размер словаря: это можно сделать, настроив один из двух параметров TfidfVectorizer:

  • min_df, что означает «минимальная частота документов» или «минимальное количество документов, в которых термин должен появиться, чтобы он был рассмотрен».
  • max_features, который позволяет напрямую установить размер словаря

Мы начали играть с постоянно увеличивающимися значениями min_df (1 — по умолчанию, 2, 3…) и изучили полученный размер словаря. Это более интуитивно понятно, чем пытаться угадать лучший размер словаря из ниоткуда и ожидать, что он по-прежнему будет лучше при изменении данных.

A min_df=3 на самом деле немного улучшила ситуацию — как скорость, так и производительность кластеризации, так что мы оставим это.

Теперь у нас есть что-то вроде приличной модели, которую можно использовать в качестве основы для моделей, основанных на глубоком обучении, которые будут разработаны для этих задач (на самом деле, сначала мы просто вернем ее в слова и проделаем некоторые умные действия). -подобная кластеризация на этих лучших векторах — очевидно, что матричные методы не будут применимы там из-за размеров векторов … и форм, если мы хотим также сохранить последовательности и не сворачивать вещи в представления в стиле мешка слов). Если наши более причудливые модели не смогут превзойти даже это, мы поймем, где ошиблись.

(…о, и кстати, я не упомянул слона, который в настоящее время обитает в этой комнате — измерение производительности в неконтролируемом обучении/кластеризации. Для всех практических целей этой задаче потребуются тесты на вторжение темы и вторжение слов, выполненные на реальных данные производственной выборки с участием людей в цикле. См. это для введения в предмет. К сожалению, это связано с программным обеспечением / человеческим трудом, а не с циклами ЦП, которые мы знаем и любим.😰 )

Но подождите... Мы до сих пор использовали только очень общие математические методы. Типы моделей, которые могли бы точно так же работать с данными изображения или звука. Не следует ли ожидать, что что-то более конкретное для наших данных или, по крайней мере, что-то, предназначенное для тематической классификации/обнаружения, будет работать лучше? Давайте поищем подобную модель, чтобы, возможно, повысить планка для наших будущих причудливых моделей глубокого обучения немного больше…

Скрытое распределение Дирихле (LDA)

Глядя на одно описание этой модели, кажется, что это может быть просто правильным следующим шагом для нас:

Скрытое распределение Дирихле — это генеративная вероятностная модель для коллекций дискретных наборов данных, таких как текстовые корпуса. Это также тематическая модель, которая используется для обнаружения абстрактных тем из коллекции документов.

Ого!😍 ...может быть, это тот самый?

Почитав немного о LDA, мы готовы попробовать этого плохого парня (после подготовки векторов подсчета слов вместо tf-idfs — очевидно, это пища, которую он предпочитает):

>>> vectors_count_week, vocab_count_week = count_world_weeks[0]
>>> print("vectors_count_week ::", type(vectors_count_week),
          vectors_count_week.dtype, vectors_count_week.shape)
  vectors_count_week :: <class 'scipy.sparse.csr.csr_matrix'> int64 (4070, 5376)
>>> print("vocab_count_week ::", type(vocab_count_week),
          vocab_count_week.dtype, vocab_count_week.shape)
  vocab_count_week :: <class 'numpy.ndarray'> <U22 (5376,)
>>> lda = sklearn.decomposition.LatentDirichletAllocation(
...     n_components=TOPICS_N,
...     max_iter=5,
...     learning_offset=50.,
...     learning_method='online',
... )
>>> # these should be ~ like W and H form NMF
>>> doc_topic_mx = lda.fit_transform(vectors_count_week)
>>> topic_word_mx = lda.components_
>>> print(f"vectors_count_week ::", type(vectors_count_week),
          vectors_count_week.dtype, vectors_count_week.shape)
  vectors_count_week :: <class 'scipy.sparse.csr.csr_matrix'> int64 (4070, 5376)
>>> print(f"doc_topic_mx ::", type(doc_topic_mx),
          doc_topic_mx.dtype, doc_topic_mx.shape)
  doc_topic_mx :: <class 'numpy.ndarray'> float64 (4070, 25)
>>> print(f"topic_word_mx ::", type(topic_word_mx),
          topic_word_mx.dtype, topic_word_mx.shape)
  topic_word_mx :: <class 'numpy.ndarray'> float64 (25, 5376)
print(f"topic_word_mx ::", type(topic_word_mx),
      topic_word_mx.dtype, topic_word_mx.shape)
  topic_word_mx :: <class 'numpy.ndarray'> float64 (25, 5376)

Хорошо, это было проще, чем ожидалось. Он также удобно создает те же типы матриц, что и NMF, поэтому для проверки результатов можно использовать тот же вспомогательный код. Слишком хорошо, чтобы быть правдой! В любом случае, давайте посмотрим, что он может сделать:

------ 1 senate majority selling democrats rains mcconnell cyclone rain weather recess
   1 Senate will delay recess, work on coronavirus bill next week: McConnell.  The U.S. Senate will delay
   2 McConnell says Senate will cancel preplanned recess to work on coronavirus bill.  The Senate will be
   3 Dick's Sporting Goods will stop selling guns at 440 more stores.  Dick's Sporting Goods will stop se
------ 2 flight crash plane soccer pakistan airlines people killed boeing loved
   1 Pain and reflection a year after Ethiopian Airlines crash.  Ethiopians still haunted by crash of Boe
   2 Ethiopian Officials Say Faulty Boeing Software Played Role In Deadly 737 Max Crash.  The findings of
   3 Ethiopia Airlines crash report focuses on Boeing's faulty systems.  Faulty systems on a Boeing 737 M
------ 3 netanyahu school mexico women benjamin pelosi gantz form government art
   1 Mexico: Women stay at home to protest femicid
   2 More Than Two Dozen Charged in Horse Racing Doping Scheme.  Among those charged was Jason Servis, th
   3 Millions of women in Mexico expected to strike over femicides.  Women to skip school, work, social a
------ 4 access crown watchdog iran saudi needs meant pornhub journalists bbc
   1 World's biggest porn site under fire over rape and abuse videos.  Petition highlighting failure of P
   2 UN experts demand Iran ceases harassing BBC Persian staff.  BBC Persian journalists have endured dea
   3 UN watchdog: Iran providing access to active nuclear sites.  The head of the U.N.’s atomic watchdog
------ 5 weinstein harvey prison years sexual 23 judge space jail rape
   1 Tears, thanks and surprise over Harvey Weinstein's 23-year sentence.  Harvey Weinstein's 23-year jai
   2 Weinstein to be sentenced for sex crimes after watershed #MeToo trial.  Former movie producer Harvey
   3 Weinstein gets 23-y

Хм… Здесь мы видим кое-что новое! (Это не история Харви Вайнштейна — модифицированная модель NMF на самом деле тоже ее обнаруживает, но имеет более низкий рейтинг, поэтому ее нет в приведенных выше списках.)

Очевидно, перепутаны такие темы, как «4 доступа к сторожевому псу короны Ирану Саудовской Аравии нужны журналисты порнохаба Би-би-си»😳 или «большинство в Сенате продает демократов дождь макконнелл циклон дождь погодные каникулы»🥴.

Может, у нас какие-то гиперпараметры неправильные… 🤔

Оказывается, у нас есть. Но суть не в этом. LDA — сложный зверь, о нем можно/нужно писать более длинные статьи. И даже простая реализация от sklearn имеет довольно много гиперпараметров, значения которых по умолчанию бесполезны, и для которых нам, вероятно, придется много читать, чтобы выяснить значения, которые могут работать для нашего варианта использования. (Или выполните поиск по сетке…)

Если цель состоит в том, чтобы развернуть современную модель поверхностной классификации текста в рабочей среде, то изучение LDA и выяснение того, как заставить ее работать на нас, безусловно, того стоит! Но если мы просто хотим быстро придумать простую модель, которая действительно полезна и может послужить достаточно хорошим эталоном… не знаю.

Я упомянул LDA только для того, чтобы показать, как, по сравнению с ней, очень универсальную математическую модель, такую ​​как NLM, можно очень легко заставить работать вроде бы хорошо, без особых знаний о том, как она работает, и не беспокоясь о том, настраивать гиперпараметры сверх значений по умолчанию.

Это почти ✨(математическое) волшебство, как что-то настолько простое и неконкретное может работать так легко и так хорошо, и именно поэтому мне нравится делать это на скорую руку в качестве первой попытки для таких неконтролируемых задач НЛП на средних/малых данные!

👏Теперь, если вы зашли так далеко, не забудьте похлопать меня!

Ура и Намасте!