A new package for Gaussian mixture topic models

I am using the wordvector package extensively in my projects because document vectors are often a better way to represent texts. One type of analysis I use is topic classification of documents based on K-means clustering. This worked reasonably well with document vectors, but it can take time if implemented using R’s basic function, `kmeans`.

An alternative approach is to cluster if documents using DBSCAN combined with UMAP. BERTopic takes this approach but I did not want to further reduce the dimensionality of document vectors using UMAP. Without it, DBSCAN’s density-based algorithm does not work particularly well because document vectors are very dense.

Another approach is to use of Gaussian mixture models, which identify a given number of normal distributions in the data. I thought this could work well because document vectors should have similar distributions when they comprise similar words. I tested a Gaussian mixture model using the flexmix package and found that the results were very good, but the package struggled even with a small corpus… I thought I might have to implement a simple model myself to speed up the computation but fortunately the Armadillo library already had multi-threaded C++ implementation of Gaussian mixture models and k-means. In this case, I only needed to write an R wrapper around it and create a package!

Actually, Gaussian mixture models and k-means from the Armadillo package are already available in the ClusterR package, but I decided to create my own package because ClusterR does not allow me to set the initial cluster centers. This is necessary for dynamic analysis, where the parameters of the current model are passed to the next next model to initialize clustering. This type of dynamic analysis allows us to capture the evolution of topics over time.

My new R package is called GMTM (Gaussian Mixture Topic Models). The package’s design is similar to seededlda, another topic analysis package I developed. GMTM takes document vectors from various sources but works seamlessly with the wordvector package. In the example below, I fit GMTM and seededlda on a corpus of 65,000 short news texts and compare the speed and the quality of the topics. On my desktop computer (an Intel i7 processor with 20 cores), GMTM’s execution time is less than 3 seconds, even including the training of document vectors, whereas seededlda took more than 11 seconds. GMTM is about four times faster! GMTM’s topic words are also as intuitive as those identified by seededlda. I especially like that “1-0” and “2-0” appear in the same topic as “rugby” (topic 7) because this shows that we can handle numeric expressions more effectively using document embedding.

> library(quanteda)
> library(wordvector)
> library(seededlda)
> library(GMTM)
> 
> # preprocessing
> corp <- corpus_reshape(data_corpus_news2014) # from wordvector package
> ndoc(corp)
[1] 65863
> toks <- tokens(corp, remove_punct = TRUE, remove_symbols = TRUE, remove_number = TRUE) |>
+   tokens_remove(stopwords("en"), min_nchar = 2) |>
+   tokens_tolower() |>
+   tokens_trim(min_termfreq = 5)
> dfmt <- dfm(toks)
> 
> # GMM
> system.time({
+   wov <- textmodel_word2vec(toks, dim = 100)
+   dov <- as.textmodel_doc2vec(dfmt, wov)
+   gmm <- textmodel_gmm(dov, k = 10)
+ })
   user  system elapsed 
  25.59    0.55    2.70 
> GMTM::terms(gmm, dfmt)
      topic1      topic2      topic3      topic4     topic5         topic6       
 [1,] "percent"   "pistorius" "killed"    "ap"       "said"         "editing"    
 [2,] "growth"    "ap"        "militants" "krasnaya" "co"           "writing"    
 [3,] "futures"   "court"     "said"      "polyana"  "agency"       "stonestreet"
 [4,] "earnings"  "sentenced" "sunni"     "sochi"    "inc"          "tait"       
 [5,] "quarterly" "trial"     "islamic"   "olympic"  "sources"      "chizu"      
 [6,] "cents"     "said"      "gunmen"    "rio"      "snowden"      "nomiyama"   
 [7,] "index"     "guilty"    "iraq"      "coach"    "corp"         "maler"      
 [8,] "stocks"    "police"    "syria"     "cannes"   "ltd"          "hepinstall" 
 [9,] "data"      "lawyer"    "troops"    "world"    "intelligence" "pomeroy"    
[10,] "stock"     "killing"   "attack"    "janeiro"  "company's"    "bangalore"  
      topic7         topic8       topic9   topic10    
 [1,] "rugby"        "president"  "said"   "lavrov"   
 [2,] "championship" "minister"   "ap"     "ukraine"  
 [3,] "champions"    "polls"      "people" "said"     
 [4,] "beats"        "parliament" "ferry"  "kerry"    
 [5,] "1-0"          "prime"      "ebola"  "peace"    
 [6,] "england"      "party"      "leone"  "sanctions"
 [7,] "coach"        "government" "killed" "russia"   
 [8,] "ap"           "elections"  "south"  "obama"    
 [9,] "scored"       "election"   "dead"   "crimea"   
[10,] "2-0"          "said"       "sierra" "nato"     
> table(GMTM::topics(gmm))

 topic1  topic2  topic3  topic4  topic5  topic6  topic7  topic8  topic9 topic10 
   5284    5842    8374    7596    6927     898    5999    6138    7872   10933 
> 
> system.time({
+   lda <- textmodel_lda(dfmt, k = 10, batch_size = 0.01, auto_iter = TRUE)
+ })
   user  system elapsed 
 258.21    0.39   11.64 
>
> # LDA
> seededlda::terms(lda)
      topic1    topic2    topic3      topic4        topic5       topic6       topic7      
 [1,] "south"   "percent" "said"      "said"        "court"      "ukraine"    "new"       
 [2,] "united"  "said"    "state"     "police"      "former"     "russia"     "ap"        
 [3,] "said"    "reuters" "killed"    "ap"          "ap"         "u.s"        "reporting" 
 [4,] "israel"  "million" "islamic"   "people"      "president"  "said"       "editing"   
 [5,] "gaza"    "billion" "iraq"      "city"        "government" "president"  "one"       
 [6,] "nations" "u.s"     "group"     "two"         "years"      "russian"    "years"     
 [7,] "israeli" "year"    "militants" "say"         "trial"      "washington" "york"      
 [8,] "peace"   "bank"    "forces"    "authorities" "case"       "obama"      "u.s"       
 [9,] "talks"   "company" "syria"     "killed"      "hong"       "states"     "australian"
[10,] "korea"   "oil"     "security"  "dead"        "leader"     "moscow"     "search"    
      topic8       topic9      topic10   
 [1,] "said"       "new"       "ap"      
 [2,] "minister"   "first"     "world"   
 [3,] "prime"      "ap"        "cup"     
 [4,] "government" "election"  "league"  
 [5,] "told"       "open"      "team"    
 [6,] "iran"       "australia" "brazil"  
 [7,] "foreign"    "vote"      "win"     
 [8,] "nuclear"    "party"     "sochi"   
 [9,] "president"  "test"      "first"   
[10,] "state"      "second"    "saturday"
> table(seededlda::topics(lda))

 topic1  topic2  topic3  topic4  topic5  topic6  topic7  topic8  topic9 topic10 
   5671    8516    7406    6962    6798    6338    5653    6425    5572    6363 

Next steps for me in this project are testing the accuracy of the result more thoroughly, examining their sensitivity to hyper-parameters, and supporting seed words for semi-supervised topic classification. The GMTM package is already available on Gitub, so please give it a try and let me know how it works with your data.

Posts created 121

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top