The Performance of Probabilistic Latent Semantic Analysis

By Bas Machielsen

October 5, 2023

Introduction

In this blog post, I want to investigate the performance of probabilistic latent semantic analysis: a subject I have been teaching (but also studying) for a course. Probabilistic latent semantic analysis proceeds from a document-term matrix, a standard data matrix in the field of text mining. It should look something like this, where the rows of the matrix represent \(n\) documents and the columns \(p\) terms (words). Usually, \(p > n\). a

$$ A = \begin{pmatrix} doc_1,term_1 & doc_1, term_2 & \dots & doc_1, term_p \\ \vdots & \dots & \ddots & \vdots \\ doc_n, term_1 & \dots & \dots & doc_n, term_p \end{pmatrix} $$ The standard maximum likelihood estimator for \(Pr(d_i, t_j)\) is \(x_{ij} / m\) where \(m\) is the total word count in all documents. This has a simple interpretation: count of word \(j\) in document \(i\) / total word count in all documents.

PLSA

Probabilistic Latent Semantic Analysis (PLSA) is an attempt to decompose this matrix using something similar to a singular value decomposition. In particular, given a probability matrix:

$$ P = \begin{pmatrix} p(d_1, t_1) & \dots & p(d_1, t_p) \\ \vdots & \ddots & \vdots \\ p(d_n, t_1)& \dots & p(d_n, t_p) \end{pmatrix} $$

We can have construct an approximation \(U\Sigma V^T\) with \(U=N \times r\) ($r$ classes):

$$ U = \begin{pmatrix} p(d_1 | c_1) & \dots & p(d_1 | c_r) \\ \vdots & & \vdots \\ p(d_n | c_1) & \dots & p(d_n | c_r) \end{pmatrix} $$

\(\Sigma = \text{diag}(P(c_r))\), and \(V^T\) ($r \times p$) has elements \(V^T_{ij} = P(t_j | c_i)\). Naturally, the object of interest is usually \(U\): this represents the probabilities of the document belonging to class \(1\) to \(r\).

In R, the package svs can be used to carry out probabilistic latent semantic analysis:

library(svs)

In what follows, I’ll demonstrate the capacity of PLSA to distinguish two types of documents on its own: I’ll scrape and convert into a document text matrix several pages about football, and several pages about tennis, set \(k\) (the number of classes) equal to 2, and investigate the output.

Example

Here, I first web-scrape the text of several wikipedia pages:

Now, I use the tidytext package to put these into a document-term matrix:

library(tidytext)
# Compute the texts into a data.frame
text_df <- tibble(Text = texts) |> 
  rowwise() |> 
  mutate(Text = paste(Text, collapse="")) |> 
  ungroup()

# Put all words together grouped by document
text_data <- text_df |>
  group_by(row_number()) |> 
  unnest_tokens(word, Text) |> 
  rename('document' = 'row_number()')

# Convert to a document-term matrix
# Filter out stop_words and numbers
stop_words <- bind_rows(stop_words, data.frame(word = as.character(0:10000), lexicon="Custom"))

dtm <- text_data |>
  count(document, word) |>
  filter(!is.element(word, stop_words)) |> #!str_detect(word, paste(as.character(0:10000), collapse="|"))) |> 
  cast_dtm(document, word, n)

The document-term matrix (dtm) looks like this:

as.data.frame(as.matrix(dtm)) |> dim()
## [1]   14 6197

Now, let’s compute the frequencies and apply LPSA:

library(svs)
X <- as.matrix(dtm)
out <- fast_plsa(X, k=2, symmetric=T)

Now, I want to find out which class each of the documents have been assigned to:

apply(
  out$prob1, 1, which.max
)
##  1  2  3  4  5  6  7  8  9 10 11 12 13 14 
##  1  2  1  1  1  2  2  2  2  2  2  2  2  2

.. which means that the majority of the documents is classified in the correct corresponding cluster.

Comparison

We can compare the results with a so-called latent semantic analysis, which is just a singular value decomposition.

out_lsa <- fast_lsa(X)

out_lsa$pos1[, 'Dim1']
##            1            2            3            4            5            6 
## -0.803081398 -0.338888792 -0.191561592 -0.298909337 -0.271174528 -0.133052909 
##            7            8            9           10           11           12 
## -0.146478395 -0.026522482 -0.011199147 -0.008402141 -0.019986480 -0.013202093 
##           13           14 
## -0.001734649 -0.001084412

In this case, we can see that the median of the first dimension already separates the documents perfectly in two classes. The first 7 observations having very low values and the second 7 values having very high values. So we can take this to be an indicator for which class the documents belong to:

data.frame(doc_no = 1:14) |> 
  mutate(class = if_else(out_lsa$pos1[,'Dim1'][doc_no] > median(out_lsa$pos1[,'Dim1']), 1, 2))
##    doc_no class
## 1       1     2
## 2       2     2
## 3       3     2
## 4       4     2
## 5       5     2
## 6       6     2
## 7       7     2
## 8       8     1
## 9       9     1
## 10     10     1
## 11     11     1
## 12     12     1
## 13     13     1
## 14     14     1

Conclusion

In this setting, I have demonstrated a simple example of latent probabilistic semantic analysis, and latent semantic analysis, and I would prefer a simpler method to a potentially more complicated method. Thank you for reading!

Posted on:
October 5, 2023
Length:
4 minute read, 763 words
See Also: