Aim

NMF aims to find latent topics within a document by factorising a term-document matrix. Unlike LSA, every value in the output is constrained to be non-negative.

Introduction

The term-document matrix is factorised into 2 parts:

  1. a word-topic matrix
  2. a topic-document matrix

These matrices can then be used to infer the semantic relations of words with each sentence.

Algorithm

Main Idea

Factorise the term-document matrix into 2 non-negative matrices, so that every weight is positive.

1. Generate the term-document matrix

  • the matrix has size , where is the number of words and is the number of documents
  • it is normalised using TF-IDF, the same as in LSA

2. Factorise into 2 matrices

MatrixShapeMeaning
words by documents
every topic and the terms found within the topic
every document and the topics found within the document
  • the sign is and not because the factorisation is an approximation
  • it is assumed that all elements of and are positive, given that the elements of are positive
  • there are only 2 factor matrices, so there is no equivalent of the matrix from LSA

3. Minimise a cost function

When factorising the matrix, the 2 main cost functions that can be used are:

Generalised Kullback-Liebler Divergence (KL Divergence)

  • as the value of the KL divergence reaches zero, the closeness of corresponding words increases

Frobenius Norm

  • defined as the square root of the sum of the absolute squares of its elements, the Frobenius Norm is a method of measuring how good an approximation is

NMF in Scikit-Learn

1. Apply TF-IDF

# use tf-idf by removing tokens that don't appear in at least 10 documents
vect = TfidfVectorizer(min_df=10, stop_words=stop_words)
 
# Fit and transform
X = vect.fit_transform(tweets.OriginalTweet)
  • min_df=10 filters out words that do not appear in at least 10 tweets

2. Fit the NMF model

from sklearn.decomposition import NMF
 
# Create an NMF instance: model
# the 10 components will be the topics
model = NMF(n_components=10, random_state=5)
 
# Fit the model to TF-IDF
model.fit(X)
 
# Transform the TF-IDF: nmf_features
nmf_features = model.transform(X)
  • n_components is the number of topics
  • random_state=5 fixes the seed, which matters because NMF is not deterministic
  • nmf_features is the document-topic matrix, so it is one row per tweet and one column per topic

3. Inspect the topic-word matrix

components_df = pd.DataFrame(model.components_, columns=vect.get_feature_names_out())
components_df

Output, showing a subset of the columns:

19cocoronaviruscovidcovid19foodgrocerypeoplepricesstoresupermarket
00.00000043.2632440.0000260.0734650.0000000.0000000.0000000.0000000.0000000.0000000.002273
12.6313010.0000000.0000002.7173430.0000000.0000000.0000000.0000000.0000000.0000000.000000
20.0000000.0000000.0000000.0050840.0000000.0002690.1554980.0000000.0000003.3313840.000000
30.0709770.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0001332.964687
40.0827230.0000000.0000000.0000000.0000000.0000000.0000000.0000003.5803600.0000000.000000
50.0410400.0000000.0000000.0000000.0000001.9393260.0000000.0000000.0000000.0002250.000000
60.0000000.0000000.0000000.0026070.0000000.0000290.0000001.6887120.0000000.0001720.000378
70.0000000.0000671.9506880.0000000.0000000.0001390.0013830.0000000.0003580.0000000.000172
80.0000000.0000000.0000050.0004131.5948730.0000270.0018820.0000000.0001840.0000000.000112
90.0317620.0000000.0000000.0000000.0000000.0000001.2003510.0000000.0000000.0000000.000000
  • model.components_ has shape , so rows are topics and columns are words
  • every value is 0 or positive, which is the non-negativity constraint in action

4. Print the top 10 words in each topic

for topic in range(components_df.shape[0]):
    tmp = components_df.iloc[topic]
    print(f'For topic {topic+1} the words with the highest value are:')
    print(tmp.nlargest(10))
    print('\n')

Output for the first 2 topics:

For topic 1 the words with the highest value are:
co             43.263244
covid           0.073465
supermarket     0.002273
coronavirus     0.000026
19              0.000000
covid19         0.000000
food            0.000000
grocery         0.000000
people          0.000000
prices          0.000000
Name: 0, dtype: float64

For topic 2 the words with the highest value are:
covid           2.717343
19              2.631301
co              0.000000
coronavirus     0.000000
covid19         0.000000
food            0.000000
grocery         0.000000
people          0.000000

Reading the output

  • every weight is 0 or positive, so there is no negative weight to interpret, which is the main readability gain over LSA
  • most weights are exactly 0, so each topic is defined by a small handful of words, which is what "parts-based representation" means
  • topic 1 is dominated by "co" at 43.26, which comes from the t.co link fragments in the raw tweets, so it is a preprocessing artefact and not a real topic
  • topic 3 is "store", topic 4 is "supermarket", topic 5 is "prices", and topic 6 is "food", so most topics reduce to a single strong word
  • the topics are numbered 0 to 9 in components_, and the printing loop adds 1, so the printed "topic 1" is row 0

Advantages and Limitations

AdvantagesLimitations
Parts-based representation: NMF naturally produces parts-based representations, which is valuable in applications like image processing and text mining, since it can discover fundamental components or topics within dataNon-convex optimisation: NMF is based on non-convex optimisation, so it may converge to local minima rather than the global minimum, and the quality of results can be sensitive to initialisation
Noise reduction: NMF can reduce the impact of noise in data, because it focuses on capturing underlying patterns and structures rather than specific noisy detailsPreprocessing sensitivity: NMF is highly sensitive to the preprocessing used on the data, as well as the choice of cost function for the model