-->

mlampros Organizing and Sharing thoughts, Receiving constructive feedback

Text Processing using the textTinyR package

This blog post is about my recently released package on CRAN, textTinyR. The following notes and examples are based mainly on the package Vignette.

The advantage of the textTinyR package lies in its ability to process big text data files in batches efficiently. For this purpose, it offers functions for splitting, parsing, tokenizing and creating a vocabulary. Moreover, it includes functions for building either a document-term matrix or a term-document matrix and extracting information from those (term-associations, most frequent terms). Lastly, it embodies functions for calculating token statistics (collocations, look-up tables, string dissimilarities) and functions to work with sparse matrices. The source code is based mainly on C++11 and exported in R through the Rcpp, RcppArmadillo and BH packages.


update (04-04-2018): boost-locale is no longer a system requirement for the textTinyR package


The following classes (based on the R6 package) and functions are part of the package:


classes


big_tokenize_transform sparse_term_matrix token_stats
big_text_splitter() Term_Matrix() path_2vector()
big_text_parser() Term_Matrix_Adjust() freq_distribution()
big_text_tokenizer() term_associations() print_frequency()
vocabulary_accumulator() most_frequent_terms() count_character()
    print_count_character()
    collocation_words()
    print_collocations()
    string_dissimilarity_matrix()
    look_up_table()
    print_words_lookup_tbl()


functions


sparse_matrices tokenization utilities
dense_2sparse() tokenize_transform_text() bytes_converter()
load_sparse_binary() tokenize_transform_vec_docs() cosine_distance()
matrix_sparsity()   dice_distance()
save_sparse_binary()   levenshtein_distance()
sparse_Means()   read_characters()
sparse_Sums()   read_rows()
    text_file_parser()
    utf_locale()
    vocabulary_parser()



big_tokenize_transform class


The big_tokenize_transform class can be utilized to process big data files and I’ll illustrate this using the english wikipedia pages and articles (to download the data use the following web-address : https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-pages-articles.xml.bz2). The size of the file (after downloading and extracting locally) is aproximalely 59.4 GB and it’s of type .xml (to reproduce the results one needs to have free hard drive space of approx. 200 GB).
Xml files have a tree structure and one should use queries to acquire specific information. First, I’ll observe the structure of the .xml file by using the utility function read_rows(). The read_rows() function takes a file as input and by specifying the rows argument it returns a subset of the file. It doesn’t load the entire file in memory, but it just opens the file and reads the specific number of rows,




library(textTinyR)


PATH = 'enwiki-latest-pages-articles.xml'


subset = read_rows(input_file = PATH, read_delimiter = "\n",
                   
                   rows = 100,
                   
                   write_2file = "/subs_output.txt")



# data subset : subs_output.txt


<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.10/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.10/ http://www.mediawiki.org/xml/export-0.10.xsd" version="0.10" xml:lang="en">
  <siteinfo>
    <sitename>Wikipedia</sitename>
    <dbname>enwiki</dbname>
    <base>https://en.wikipedia.org/wiki/Main_Page</base>
    <generator>MediaWiki 1.28.0-wmf.23</generator>
    <case>first-letter</case>
    <namespaces>
      <namespace key="-2" case="first-letter">Media</namespace>
      <namespace key="-1" case="first-letter">Special</namespace>
      <namespace key="0" case="first-letter" />
      <namespace key="1" case="first-letter">Talk</namespace>
      <namespace key="2" case="first-letter">User</namespace>
      <namespace key="3" case="first-letter">User talk</namespace>
      <namespace key="4" case="first-letter">Wikipedia</namespace>
      <namespace key="5" case="first-letter">Wikipedia talk</namespace>
      <namespace key="6" case="first-letter">File</namespace>
      <namespace key="7" case="first-letter">File talk</namespace>
      <namespace key="8" case="first-letter">MediaWiki</namespace>
.
.
.
    </namespaces>
  </siteinfo>
  <page>
    <title>AccessibleComputing</title>
    <ns>0</ns>
    <id>10</id>
    <redirect title="Computer accessibility" />
    <revision>
      <id>631144794</id>
      <parentid>381202555</parentid>
      <timestamp>2014-10-26T04:50:23Z</timestamp>
      <contributor>
        <username>Paine Ellsworth</username>
        <id>9092818</id>
      </contributor>
      <comment>add [[WP:RCAT|rcat]]s</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text xml:space="preserve">#REDIRECT [[Computer accessibility]]

</text>
      <sha1>4ro7vvppa5kmm0o1egfjztzcwd0vabw</sha1>
    </revision>
  </page>
  <page>
    <title>Anarchism</title>
    <ns>0</ns>
    <id>12</id>
    <revision>
      <id>746687538</id>
      <parentid>744318951</parentid>
      <timestamp>2016-10-28T22:43:19Z</timestamp>
      <contributor>
        <username>Eduen</username>
        <id>7527773</id>
      </contributor>
      <minor />
      <comment>/* Free love */</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text xml:space="preserve">



In that way one has a picture of the .xml tree structure and can continue by performing queries. The initial data file is too big to fit in the memory of a PC, thus it has to be split in smaller files, pre-processed and then returned as a single file. The main aim of the big_text_splitter() method is to split the data in smaller files of (approx.) equal size by either using the batches parameter or if the file has a structure by adding the end_query parameter too. Here I’ll take advantage of both the batches and the end_query parameters for this task, because I’ll use queries to extract the text tree-elements, so I don’t want that the file is split arbitrarily. Each sub-element in the file begins and ends with the same key-word, i.e. text,




btt = big_tokenize_transform$new(verbose = TRUE)

btt$big_text_splitter(input_path_file = PATH,             # path to the enwiki data file
                  
                  output_path_folder = "/enwiki_spl_data/",  # folder to save the files
                  
                  end_query = '</text>',    # splits the file taking into account the key-word
                  
                  batches = 40,                           # split file in 40 batches (files)
                  
                  trimmed_line = FALSE,                   # the lines will be trimmed
                  
                  verbose = TRUE)

approx. 10 % of data pre-processed
approx. 20 % of data pre-processed
approx. 30 % of data pre-processed
approx. 40 % of data pre-processed
approx. 50 % of data pre-processed
approx. 60 % of data pre-processed
approx. 70 % of data pre-processed
approx. 80 % of data pre-processed
approx. 90 % of data pre-processed
approx. 100 % of data pre-processed

It took 42.7098 minutes to complete the splitting


After the data is split and saved in the output_path_folder (“/ewiki_spl_data/”) the next step is to extract the text tree-elements from the batches by using the big_text_parser() method. The latter takes as arguments the previously created input_path_folder, an output_path_folder to save the resulted text files, a start_query, an end_query, the min_lines (only subsets of text with more than or equal to this minimum will be kept) and the trimmed_line ( specifying if each line is already trimmed both-sides ),



btt$big_text_parser(input_path_folder = "/enwiki_spl_data/", # the previously created folder
                    
                    output_path_folder = "/enwiki_parse/",  # folder to save the parsed files
                    
                    start_query = "<text xml:space=\"preserve\">",  # starts to extract text
                    
                    end_query = "</text>",                        # stop to extract once here
                    
                    min_lines = 1, 
                    
                    trimmed_line = TRUE,
                    
                    verbose = TRUE)

====================
batch 1 begins ...
====================

approx. 10 % of data pre-processed
approx. 20 % of data pre-processed
approx. 30 % of data pre-processed
approx. 40 % of data pre-processed
approx. 50 % of data pre-processed
approx. 60 % of data pre-processed
approx. 70 % of data pre-processed
approx. 80 % of data pre-processed
approx. 90 % of data pre-processed
approx. 100 % of data pre-processed

It took 0.296151 minutes to complete the preprocessing

It took 0.0525948 minutes to save the pre-processed data

.
.
.
.

====================
batch 40 begins ...
====================

approx. 10 % of data pre-processed
approx. 20 % of data pre-processed
approx. 30 % of data pre-processed
approx. 40 % of data pre-processed
approx. 50 % of data pre-processed
approx. 60 % of data pre-processed
approx. 70 % of data pre-processed
approx. 80 % of data pre-processed
approx. 90 % of data pre-processed
approx. 100 % of data pre-processed

It took 1.04127 minutes to complete the preprocessing

It took 0.0448579 minutes to save the pre-processed data

It took 40.9034 minutes to complete the parsing


Here, it’s worth mentioning that the big_text_parser is more efficient if it extracts big chunks of text, rather than one-liners. In case of one-line text queries it has to check line by line the whole file, which is inefficient especially for files equal to the enwiki size.


By extracting the text chunks from the data the .xml file size reduces to (approx.) 48.9 GB. One can now continue utilizing the big_text_tokenizer() method in order to tokenize and transform the data. This method takes the following parameters:

batches (each file can be further split in batches during tokenization), to_lower (convert to lower case), to_upper (convert to upper case), utf_locale (change utf locale depending on the language), read_file_delimiter (the delimiter to use for the input data, for instance a tab-delimiter or a new-line delimiter), remove_char (remove specific characters from the text), remove_punctuation_string (remove punctuation before the data is split), remove_punctuation_vector (remove punctuation after the data is split), remove_numbers (remove numbers from the data), trim_token (trim the tokens both-sides), split_string (split the string), split_separator (token split seprator where multiple delimiters can be used), remove_stopwords (remove stopwords using one of the available languages or by providing a user defined vector of words), language (the language of use), min_num_char (the minimum number of characters to keep), max_num_char (the maximum number of characters to keep), stemmer (stemming of the words using either the porter_2steemer or n-gram stemming – those two methods will be explained in the tokenization function), min_n_gram (minimum n-grams), max_n_gram (maximum n-grams), skip_n_gram (skip n-gram), skip_distance (skip distance for n-grams), n_gram_delimiter (n-gram delimiter), concat_delimiter (concatenation of the data in case that one wants to save the file), path_2folder (specified folder to save the data), stemmer_ngram (in case of n-gram stemming the n-grams), stemmer_gamma (in case of n-gram stemming the gamma parameter), stemmer_truncate (in case of n-gram stemming the truncation parameter), stemmer_batches (in case of n-gram stemming the batches parameter ), threads (the number of cores to use in parallel ), save_2single_file (should the output data be saved in a single file), increment_batch_nr (the enumeration of the output files will start from this number), vocabulary_path_file (should a vocabulary be saved in a separate file).

More information about those parameters can be found in the package documentation.


In this blog post I’ll continue using the following transformations:

  • conversion to lowercase
  • trim each line
  • split each line using multiple delimiters
  • remove the punctuation ( once splitting is taken place )
  • remove the numbers from the tokens
  • limit the output words to a specific number of characters
  • remove the english stopwords
  • and save both the data (to a single file) and the vocabulary files (to a folder).

Each initial file will be split in additional batches to limit the memory usage during the tokenization and transformation phase,



btt$big_text_tokenizer(input_path_folder = "/enwiki_parse/",   # the previously parsed data
                       
                       batches = 4,     # each single file will be split further in 4 batches
                       
                       to_lower = TRUE, trim_token = TRUE,
                       
                       split_string=TRUE, max_num_char = 100,
                       
                       split_separator = " \r\n\t.,;:()?!//",
                       
                       remove_punctuation_vector = TRUE,
                       
                       remove_numbers = TRUE,
                       
                       remove_stopwords = TRUE,                
                       
                       threads = 4, 
                       
                       save_2single_file = TRUE,      # save to a single file
                       
                       vocabulary_path_folder = "/enwiki_vocab/",  # path to vocabulary folder
                       
                       path_2folder="/enwiki_token/",   # folder to save the transformed data
                       
                       verbose = TRUE)



====================================
transformation of file 1 starts ...
====================================

-------------------
batch 1 begins ...
-------------------

input of the data starts ...
conversion to lower case starts ...
removal of numeric values starts ...
the string-trim starts ...
the split of the character string and simultaneously the removal of the punctuation in the vector starts ...
stop words of the english language will be used
the removal of stop-words starts ...
character strings with more than or equal to 1 and less than 100 characters will be kept ...
the vocabulary counts will be saved in: /enwiki_vocab/batch1.txt
the pre-processed data will be saved in a single file in: /enwiki_token/

-------------------
batch 2 begins ...
-------------------

input of the data starts ...
conversion to lower case starts ...
removal of numeric values starts ...
the string-trim starts ...
the split of the character string and simultaneously the removal of the punctuation in the vector starts ...
stop words of the english language will be used
the removal of stop-words starts ...
character strings with more than or equal to 1 and less than 100 characters will be kept ...
the vocabulary counts will be saved in: /enwiki_vocab/batch1.txt
the pre-processed data will be saved in a single file in: /enwiki_token/
  
.
.
.
.

====================================
transformation of file 40 starts ...
====================================

-------------------
batch 1 begins ...
-------------------

input of the data starts ...
conversion to lower case starts ...
removal of numeric values starts ...
the string-trim starts ...
the split of the character string and simultaneously the removal of the punctuation in the vector starts ...
stop words of the english language will be used
the removal of stop-words starts ...
character strings with more than or equal to 1 and less than 100 characters will be kept ...
the vocabulary counts will be saved in: /enwiki_vocab/batch40.txt
the pre-processed data will be saved in a single file in: /enwiki_token/

-------------------
batch 2 begins ...
-------------------

input of the data starts ...
conversion to lower case starts ...
removal of numeric values starts ...
the string-trim starts ...
the split of the character string and simultaneously the removal of the punctuation in the vector starts ...
stop words of the english language will be used
the removal of stop-words starts ...
character strings with more than or equal to 1 and less than 100 characters will be kept ...
the vocabulary counts will be saved in: /enwiki_vocab/batch40.txt
the pre-processed data will be saved in a single file in: /enwiki_token/
  
.
.
.
.

It took 111.689 minutes to complete tokenization


In total, it took approx. 195 minutes (or 3.25 hours) to pre-process (including tokenization, transformation and vocabulary extraction) the 59.4 GB of the enwiki data.


word cloud

Having a clean single text file of all the wikipedia pages and articles one can perform many tasks. For instance, one can build a wordcloud (using the wordcloud package) from the accumulated words (a word of caution : the memory consumption when running the vocabulary_accumulator method for this kind of data size can exceed the 10 GB),



init$vocabulary_accumulator(input_path_folder = "/enwiki_vocab/", 
                            
                            vocabulary_path_file = "/VOCAB.txt",
                            
                            max_num_chars = 50)

vocabulary.of.batch 40 will.be.merged ...	minutes.to.merge.sort.batches: 4.57273

	minutes.to.save.data: 0.48584


The following table shows the first rows of the vocabulary counts,


terms frequency
lt 111408435L
refgt 49197149L
quot 48688082L
gt 47466149L
user 32042007L
category 30619748L
www 25358252L
http 23008243L


before plotting the wordcloud, I’ll limit the vocabulary to the first 200 words,



rdr_vocab = textTinyR::read_rows(input_file = "/VOCAB.txt", read_delimiter = "\n",
                                 
                                 rows = 200, 
                                 
                                 write_2file = "/vocab_subset_200terms.txt") 

# read the reduced data 

vocab_sbs <- readr::read_delim("/vocab_subset_200terms.txt", "\t",
                               
                               escape_double = FALSE, col_names = FALSE, 
                               
                               trim_ws = TRUE)

# create the wordcloud

pal2 <- RColorBrewer::brewer.pal(8, "Dark2")

wordcloud::wordcloud(words = vocab_sbs$X1, freq = vocab_sbs$X2, 
                     
                     scale = c(4.5, 0.8), random.order = FALSE, 
                     
                     rot.per = .15, colors = pal2)


Alt text


word vectors


UPDATE 11-04-2019: There is an updated version of the fastText R package which includes all the features of the ported fasttext library. Therefore the old fastTextR repository is archived. See also the corresponding blog-post.


I’m currently interested in word vectors and that’s why I also made R-wrappers for the GloVe and the fastText word representation algorithms. The latter two reside in my Github account as separate repositories (GloveR and fastTextR) and can be installed using the install_github function of the devtools package (devtools::install_github(‘mlampros/GloveR’), devtools::install_github(‘mlampros/fastTextR’)).
“A word representation is a mathematical object associated with each word, often a vector. Each dimension’s value corresponds to a feature and might even have a semantic or grammatical interpretation, so we call it a word feature. Conventionally, supervised lexicalized NLP approaches take a word and convert it to a symbolic ID, which is then transformed into a feature vector using a one-hot representation: The feature vector has the same length as the size of the vocabulary, and only one dimension is on.”

Currently, there are many resources on the web on how to use pre-trained word vectors (embeddings) as input to neural networks.

In this blog post I’ll use only the fastTextR word representation algorithm, however detailed documentation and system requirements on how to use either the GloveR or the fastTextR can be found in the corresponding repository. If I would train the whole data file (32.2 GB) using the fastTextR wrapper, it would take (approx.) 15 hours,



library(fastTextR)

skp = skipgram_cbow(input_path = "/output_token_single_file.txt", thread = 4, dim = 50,
                    
                    output_path = "/model", method = "skipgram", verbose = 2)


Read 4018M words
Number of words:  12827221
Number of labels: 0
Progress: 0.2%  words/sec/thread: 89664  lr: 0.099841  loss: 1.055581  eta: 15h32m  14m


thus, just for illustration purposes I’ll limit the train data to approx. 1 GB of the output file,




reduced_data = read_characters(input_file = "/output_token_single_file.txt", 
                               
                               characters = 1000000000,        # approx. 1 GB of the data
                               
                               write_2file = "/reduced_single_file.txt")




skp = skipgram_cbow(input_path = "/reduced_single_file.txt",  # reduced data set
                    
                    output_path = "/model",                # saves model and word vectors
                    
                    dim = 50,                              # 50-dimensional word vectors
                    
                    method = "skipgram",                   # method = 'skipgram'  
                    
                    thread = 4, verbose = 2)

Read 124M words
Number of words:  5029370
Number of labels: 0
Progress: 100.0%  words/sec/thread: 94027  lr: 0.000000  loss: 0.186674  eta: 0h0m 

time to complete : 33.53577 mins 


the following vector-subset is the example output of the “model.vec” file, which includes the 50-dimensional word vectors,




lt 0.12207 0.16117 0.4641 0.73876 0.43968 0.63911 -0.53747 0.1398 ..... 
refgt -0.0038898 -0.13976 0.26077 0.7775 0.2228 0.28169 -0.48306 .....
quot 0.7295 -0.12472 0.32131 0.46965 0.45363 0.85022 -0.051471 ..... 
gt 0.41287 0.26584 0.6612 0.78185 0.46692 0.74092 -0.23816 .....
cite 0.037943 0.095684 0.62832 0.93794 0.19776 0.44592 -0.21641 .....
www -0.31855 0.42268 0.3875 1.5457 -0.23804 0.34022 -0.051849 ..... 
ref 0.45236 -0.21766 0.6341 0.76392 0.53734 0.66976 -0.23162 .....
http -0.42692 0.48637 0.28622 1.7019 -0.25739 0.25948 -0.026582 ..... 
namequot 0.56828 -0.30782 0.45707 0.78346 0.53727 0.62445 ..... 
 -0.010281 0.25528 0.04708 0.49679 0.043934 0.33733 -0.42706 .....
amp 0.06308 0.11968 0.11885 0.67699 -0.11448 0.25183 -0.48789 .....
category -1.5705 -0.40638 0.61064 2.5691 -0.52987 0.68096 .....
county -0.85743 0.071625 -0.43393 0.17157 -0.32874 1.771 ..... 
org -0.26974 0.76983 0.57599 1.5939 -0.1706 0.21937 -0.44645 .....
states -0.40973 -0.48528 0.092905 0.011603 -0.035727 0.52807 .....
united -0.25079 -0.49813 0.070942 0.16762 0.069961 0.56868 .....
web -0.066578 0.14837 0.23088 1.2919 -0.252 0.31441 -0.3799 ..... 
census -0.29033 -0.73695 0.35474 -0.5237 -0.15206 1.7089 .....
.
.
.


sparse_term_matrix class


The sparse_term_matrix class includes methods for building a document-term or a term-document matrix and extracting information from those matrices (it relies on RcppArmadillo and can handle large sparse matrices too). I’ll explain all the different methods using a toy text file downloaded from wikipedia,



The term planet is ancient, with ties to history, astrology, science, mythology, and religion. Several planets in the Solar System can be seen with the naked eye. These were regarded by many early cultures as divine, or as emissaries of deities. As scientific knowledge advanced, human perception of the planets changed, incorporating a number of disparate objects. In 2006, the International Astronomical Union (IAU) officially adopted a resolution defining planets within the Solar System. This definition is controversial because it excludes many objects of planetary mass based on where or what they orbit. 
Although eight of the planetary bodies discovered before 1950 remain planets under the modern definition, some celestial bodies, such as Ceres, Pallas, Juno and Vesta (each an object in the solar asteroid belt), and Pluto (the first trans-Neptunian object discovered), that were once considered planets by the scientific community, are no longer viewed as such.
The planets were thought by Ptolemy to orbit Earth in deferent and epicycle motions. Although the idea that the planets orbited the Sun had been suggested many times, it was not until the 17th century that this view was supported by evidence from the first telescopic astronomical observations, performed by Galileo Galilei. 
At about the same time, by careful analysis of pre-telescopic observation data collected by Tycho Brahe, Johannes Kepler found the planets orbits were not circular but elliptical. As observational tools improved, astronomers saw that, like Earth, the planets rotated around tilted axes, and some shared such features as ice caps and seasons. Since the dawn of the Space Age, close observation by space probes has found that Earth and the other planets share characteristics such as volcanism, hurricanes, tectonics, and even hydrology.
Planets are generally divided into two main types: large low-density giant planets, and smaller rocky terrestrials. Under IAU definitions, there are eight planets in the Solar System. In order of increasing distance from the Sun, they are the four terrestrials, Mercury, Venus, Earth, and Mars, then the four giant planets, Jupiter, Saturn, Uranus, and Neptune. Six of the planets are orbited by one or more natural satellites.


The sparse_term_matrix class can be initialized using either a vector of documents or a text file. Assuming the downloaded file is saved as “planets.txt”, then a document-term-matrix can be created in the following way,



init = sparse_term_matrix$new(vector_data = NULL,          # in case of vector of documents
                              
                              file_data = "/planets.txt",     # input the .txt data
                              
                              document_term_matrix = TRUE)   # document term matrix as output



tm = init$Term_Matrix(sort_terms = TRUE,      # initial terms are sorted
                 
                      to_lower = TRUE,          # convert to lower case
                 
                      trim_token = TRUE,        # trim token
                 
                      split_string = TRUE,      # split string
                 
                      tf_idf = TRUE,            # tf-idf will be returned
                      
                      verbose = TRUE)


minutes.to.tokenize.transform.data: 0.00001	total.time: 0.00001

Warning message:
empty character strings present in the column names they will be replaced with proper characters


tm


5 x 212 sparse Matrix of class "dgCMatrix"
   [[ suppressing 91 column names X, X17th, X1950 ... ]]

[1,] -0.001939591 .         .          0.009747774 0.01949555   ......         
[2,] -0.003255742 .         0.01636233 .           .            ......         
[3,] -0.003440029 0.0172885 .          .           .            ......         
[4,] -0.002196645 .         .          .           .            ......  
[5,] -0.002681199 .         .          .           .          . ......

[1,] 0.007121603 .          0.009747774 .          0.005434315 . ......        
[2,] 0.007969413 0.01636233 .           .          .           . ......    
[3,] .           .          .           .          0.009638219 . ......  
[4,] 0.008065430 .          .           0.01103965 .             ......  
[5,] .           .          .           .          .             ......

[1,] -0.001939591 0.009747774 .          .          .           ......  
[2,] -0.003255742 .           .          .          0.01636233  ......          
[3,] -0.010320088 .           .          .          .           ......          
[4,] -0.006589936 .           0.01103965 0.01103965 .           ......        
[5,] -0.002681199 .           .          .          .           ......          
.
.
.


The Term_Matrix method takes almost the same parameters as the ( already explained ) big_text_tokenizer() method. The only differences are:

  • sort_terms ( should the output terms - rows or columns depending on the document_term_matrix parameter - be sorted in alphabetical order )
  • print_every_rows ( verbose output intervalls )
  • normalize ( applies l1 or l2 normalization )
  • tf_idf ( the term-frequency-inverse-document-frequency will be returned )

Details about the parameters of the Term_Matrix method can be found in the package documentation.


To adjust the sparsity of the output matrix one can take advantage of the Term_Matrix_Adjust method, (by adjusting the sparsity_thresh parameter towards 0.0 a proportion of the sparse terms will be removed)



res_adj = init$Term_Matrix_Adjust(sparsity_thresh = 0.6)          # terms (here columns) which sum to zero will be removed


res_adj


5 x 9 sparse Matrix of class "dgCMatrix"
          planets           by            X       solar          and          as  ...... 
[1,] -0.005818773 -0.001939591 -0.001939591 0.004747735 -0.001939591 0.007121603  ......      
[2,] -0.006511484 -0.003255742 -0.003255742 0.003984706 -0.006511484 0.007969413  ......       
[3,] -0.006880059 -0.010320088 -0.003440029 .           -0.003440029 .            ...... 
[4,] -0.006589936 -0.006589936 -0.002196645 .           -0.008786581 0.008065430  ...... 
[5,] -0.013405997 -0.002681199 -0.002681199 0.003281523 -0.008043598 .            ...... 


The term_associations method returns the correlation of specific terms (Terms) with all the other terms in the output matrix. The dimensions of the output matrix can vary depending on which one of the Term_Matrix, Term_Matrix_Adjust is run previously. In the previous step I adjusted the initial sparse matrix using a sparsity_thresh of 0.6, thus the new dimensions will be,



dim(res_adj)

[1] 5 9


and the resulted terms,



init$term_associations(Terms = c('planets', 'by', 'INVALID'), keep_terms = NULL, verbose = TRUE)


the ' INVALID ' term does not exist in the terms vector 

total.number.variables.processed:   2	minutes.to.complete: 0.00000

$planets
     term correlation
 1:    as  0.65943196
 2:   and  0.48252671
 3:     X  0.07521813
 4:    by -0.26301349
 5:    of         NaN
 6: solar -0.11887462
 7:  were         NaN
 8:   the -0.15500900
 9:   in.         NaN
10:  that  0.44307617
11: earth -0.24226093

$by
       term correlation
 1:   solar   0.9092777
 2:       X   0.5010034
 3: planets  -0.2630135
 4:      of         NaN
 5:    were         NaN
 6:     the   0.7838436
 7:      as   0.3698239
 8:     and  -0.0594149
 9:     in.         NaN
10:   earth  -0.6952757
11:    that  -0.9338884


Lastly, the most_frequent_terms method gives the frequency of the terms in the corpus. However, the function returns only if the normalize parameter is NULL and the tf_idf parameter is FALSE ( those two parameters belong to the init$Term_Matrix() method ),



init = sparse_term_matrix$new(file_data = "/planets.txt", document_term_matrix = TRUE)

tm = init$Term_Matrix(sort_terms = TRUE,     
                 
                      to_lower = TRUE,          
                 
                      trim_token = TRUE,       
                 
                      split_string = TRUE,     
                 
                      tf_idf = FALSE,            # disable tf-idf
                      
                      verbose = TRUE)


init$most_frequent_terms(keep_terms = 10, threads = 1, verbose = TRUE)


minutes.to.complete: 0.00000


        term frequency
 1:     the        28
 2: planets        15
 3:     and        11
 4:      of         9
 5:      by         9
 6:      as         8
 7:     in.         6
 8:       X         5
 9:     are         5
10:    that         5


More information about the sparse_term_matrix class can be found in the package documentation.


token_stats class


The token_stats class can be utilized to output corpus statistics. Each of the following methods can take either a vector of terms, a text file or a folder of files as input:


  • path_2vector : is a helper method which takes a path to a file or folder of files and returns the content in form of a vector,

init = token_stats$new(path_2file = "/planets.txt")    # input the 'planets.txt' file

vec = init$path_2vector()
 

[1] "The term planet is ancient, with ties to history, astrology, science, mythology, and religion. Several planets in the Solar System can be seen with the naked eye. These were regarded by many early cultures as divine, or as emissaries of deities. As scientific knowledge advanced, human perception of the planets changed, incorporating a number of disparate objects" ....

[2] "Although eight of the planetary bodies discovered before 1950 remain" ....
.
.


  • freq_distribution : it returns a named-unsorted vector frequency distribution for a vocabulary file

# assuming the following 'freq_vocab.txt'

the
term
planet
is
ancient
with
ties
to
history
astrology
science
mythology
and
religion
several
planets
in
the
solar
system
can
be
seen
with
the
naked
eye
these
were


this method would return,



init = token_stats$new(path_2file = 'freq_vocab.txt')

init$freq_distribution()

init$print_frequency(subset = NULL)

        words freq
 1:       the    3
 2:      with    2
 3:   ancient    1
 4:       and    1
 5: astrology    1
 6:        be    1
 7:       can    1
 8:       eye    1
 9:   history    1
10:        in    1
11:        is    1
12: mythology    1
13:     naked    1
14:    planet    1
15:   planets    1
16:  religion    1
17:   science    1
18:      seen    1
19:   several    1
20:     solar    1
21:    system    1
22:      term    1
23:     these    1
24:      ties    1
25:        to    1
26:      were    1
        words freq


  • count_character : it returns the number of characters for each word of the corpus.


for the previously mentioned ‘freq_vocab.txt’ it would output,



  init = token_stats$new(path_2file = 'freq_vocab.txt')

  vec_tmp = init$count_character()
  
  init$print_count_character(number = 3)


# words with number of characters equal to 3

[1] "the" "and" "the" "can" "the" "eye"


  • collocation_words : it returns a co-occurence frequency table for n-grams. “A collocation is defined as a sequence of two or more consecutive words, that has characteristics of a syntactic and semantic unit, and whose exact and unambiguous meaning or connotation cannot be derived directly from the meaning or connotation of its components”. The input to the function should be text n-grams separated by a delimiter ( for instance the tokenize_transform_text() function in the next code chunk will build n_grams of length 3 ),



# the data needs to be n-grams thus first tokenize and build the n-grams using 
# the 'tokenize_transform_text' function ( the "planets.txt" file as input )

tok = tokenize_transform_text("planets.txt",  
                              
                              to_lower = T, 
                              
                              split_string = T,
                              
                              min_n_gram = 3, 
                              
                              max_n_gram = 3, 
                              
                              n_gram_delimiter = "_")

init = token_stats$new(x_vec = tok$token)      # vector data as input

vec_tmp = init$collocation_words()


the example output of the vec_tmp vector is,



[1] ""       "17th"       "1950"    "2006"    "a"     "about"   "adopted"   "advanced"       
[9] "age"    "although" ....  
.
.
.


and the print_collocations method returns the coolocations for the example word ancient,



res = init$print_collocations(word = "ancient")


    is   with   ties planet 
 0.333  0.333  0.167  0.167 


  • string_dissimilarity_matrix : it returns a string-dissimilarity-matrix using either the dice, levenshtein or cosine distance. The input can be a character string vector only. In case that the method is dice then the dice-coefficient (similarity) is calculated between two strings for a specific number of character n-grams ( dice_n_gram ). The dice and levenshtein methods are applied to words, whereas the cosine distance to word-sentences.


For illustration purposes I’ll use the previously mentioned ‘freq_vocab.txt’ file, but first I have to convert the text file to a character vector,



# first initialization of token_stats 

init = token_stats$new(path_2file = 'freq_vocab.txt')

tmp_vec = init$path_2vector()      # convert to vector


# second initialization to compute the dissimilarity matrix 

init_tok = token_stats$new(x_vec = tmp_vec)

res = init_tok$string_dissimilarity_matrix(dice_n_gram = 2, method = "dice", 
                                         
                                          split_separator = " ", dice_thresh = 1.0, 
                                         
                                          upper = TRUE, diagonal = TRUE, threads = 1)

                the      term    planet is   ancient      with      ties to   history ....
the       0.0000000 0.7777778 1.0000000  1 1.0000000 0.7777778 0.7777778  1 1.0000000 ....
term      0.7777778 0.0000000 1.0000000  1 1.0000000 1.0000000 0.8000000  1 1.0000000 ....
planet    1.0000000 1.0000000 0.0000000  1 0.7333333 1.0000000 1.0000000  1 1.0000000 ....
is        1.0000000 1.0000000 1.0000000  0 1.0000000 1.0000000 1.0000000  0 1.0000000 ....
ancient   1.0000000 1.0000000 0.7333333  1 0.0000000 1.0000000 0.8461538  1 1.0000000 ....
with      0.7777778 1.0000000 1.0000000  1 1.0000000 0.0000000 1.0000000  1 1.0000000 ....
ties      0.7777778 0.8000000 1.0000000  1 0.8461538 1.0000000 0.0000000  1 1.0000000 ....
to        1.0000000 1.0000000 1.0000000  0 1.0000000 1.0000000 1.0000000  0 1.0000000 ....
history   1.0000000 1.0000000 1.0000000  1 1.0000000 1.0000000 1.0000000  1 0.0000000 ....
astrology 1.0000000 1.0000000 1.0000000  1 0.8888889 1.0000000 1.0000000  1 0.7777778 ....
science   0.8333333 1.0000000 1.0000000  1 0.5000000 1.0000000 0.8461538  1 1.0000000 ....
.
.
.


here by adjusting (reducing ) the dice_thresh parameter we can force values close to 1.0 to become 1.0,


init_tok = token_stats$new(x_vec = tmp_vec)

res = init_tok$string_dissimilarity_matrix(dice_n_gram = 2, method = "dice", 
                                           
                                           split_separator = " ", dice_thresh = 0.6, 
                                           
                                           upper = TRUE, diagonal = TRUE, threads = 1)



          the term planet is ancient with ties to history astrology science ....
the       0.0    1    1.0  1     1.0    1    1  1       1       1.0     1.0 ....
term      1.0    0    1.0  1     1.0    1    1  1       1       1.0     1.0 ....
planet    1.0    1    0.0  1     1.0    1    1  1       1       1.0     1.0 ....
is        1.0    1    1.0  0     1.0    1    1  0       1       1.0     1.0 ....
ancient   1.0    1    1.0  1     0.0    1    1  1       1       1.0     0.5 ....
with      1.0    1    1.0  1     1.0    0    1  1       1       1.0     1.0 ....
ties      1.0    1    1.0  1     1.0    1    0  1       1       1.0     1.0 ....
to        1.0    1    1.0  0     1.0    1    1  0       1       1.0     1.0 ....
history   1.0    1    1.0  1     1.0    1    1  1       0       1.0     1.0 ....
astrology 1.0    1    1.0  1     1.0    1    1  1       1       0.0     1.0 ....
science   1.0    1    1.0  1     0.5    1    1  1       1       1.0     0.0 ....
.
.
.


  • look_up_table : The idea here is to split the input words to n-grams using a numeric value and then retrieve the words which have a similar character n-gram.
    It returns a look-up-list where the list-names are the n-grams and the list-vectors are the words associated with those n-grams. The words for each n-gram can be retrieved using the print_words_lookup_tbl method. The input can be a character string vector only.



# first initialization of token_stats 

init = token_stats$new(path_2file = 'freq_vocab.txt')

tmp_vec = init$path_2vector()    # convert to vector


# second initialization to compute the look-up-table

init_lk = token_stats$new(x_vec = tmp_vec)

is_vec = init_lk$look_up_table(n_grams = 3)



# example output for the 'is_vec' vector

[1] ""    "ake" "_an" "anc" "ane" "_as" "ast" "cie" "eli" "enc" "era" "eve" ..... 
[29] "net" "ola" "olo" "_pl" "pla" "_re" "rel" "rol" "_sc" "sci" "_se" "see" .....
[57] "tro" "ver" "_we" "wer" "_wi" "wit" "yst" "yth"



then retrieve words with same n-grams,



init_lk$print_words_lookup_tbl(n_gram = "log")


"_astrology_" "_mythology_"


the underscores are necessary to distinguish the begin and end of each word when computing the n-grams.

More information about the token_stats class can be found in the package documentation.



helper functions for sparse_matrices


The purpose of creating those functions is because I observed that they return faster in comparison to other R packages. The following code chunks explain each one of the functions,



#---------------------------------
# conversion from dense to sparse
#---------------------------------

library(textTinyR)

set.seed(1)
dsm = matrix(sample(0:1, 100, replace = T), 10, 10)

res_sp = dense_2sparse(dsm)

res_sp


## 10 x 10 sparse Matrix of class "dgCMatrix"
##                          
##  [1,] . . 1 . 1 . 1 . . .
##  [2,] . . . 1 1 1 . 1 1 .
##  [3,] 1 1 1 . 1 . . . . 1
##  [4,] 1 . . . 1 . . . . 1
##  [5,] . 1 . 1 1 . 1 . 1 1
##  [6,] 1 . . 1 1 . . 1 . 1
##  [7,] 1 1 . 1 . . . 1 1 .
##  [8,] 1 1 . . . 1 1 . . .
##  [9,] 1 . 1 1 1 1 . 1 . 1
## [10,] . 1 . . 1 . 1 1 . 1



#-------------
# column- sums
#-------------

sm_cols = sparse_Sums(res_sp, rowSums = FALSE)

sm_cols


##  [1] 6 5 3 5 8 3 4 5 3 6



#----------
# row-sums
#----------

sm_rows = sparse_Sums(res_sp, rowSums = TRUE)

sm_rows


##  [1] 3 5 5 3 6 5 5 4 7 5



#---------------
# column- means
#---------------

mn_cols = sparse_Means(res_sp, rowMeans = FALSE)

mn_cols


##  [1] 0.6 0.5 0.3 0.5 0.8 0.3 0.4 0.5 0.3 0.6



#-----------
# row-means
#-----------

mn_rows = sparse_Means(res_sp, rowMeans = TRUE)

mn_rows


##  [1] 0.3 0.5 0.5 0.3 0.6 0.5 0.5 0.4 0.7 0.5



#-------------------------------------
# sparsity of a matrix (as percentage)
#-------------------------------------

matrix_sparsity(res_sp)


## 51.9999 %



#------------------------------------------------------
# saving and loading sparse matrices (in binary format)
#------------------------------------------------------

save_sp = save_sparse_binary(res_sp, file_name = "save_sparse.mat")

load_sp = load_sparse_binary(file_name = "save_sparse.mat")


More information about the helper functions for sparse matrices can be found in the package documentation.



tokenization


The tokenize_transform_text() function applies tokenization and transformation in a similar way to the big_text_tokenizer() method, however for small to medium data sets. The input can be either a character string (text data) or a path to a file. This method takes as input a single character string (character-string == of length one). The parameters for the tokenize_transform_text() function are the same to the (already explained) big_text_tokenizer() method with the only exception being the input data type.


The tokenize_transform_vec_docs() function works in the same way to the Term_Matrix() method and it targets small to medium data sets. It takes as input a vector of documents and retains their order after tokenization and transformation has taken place. Both the tokenize_transform_text() and tokenize_transform_vec_docs() share the same parameters, with the following two exceptions,

  • the object is a character vector
  • the as_token parameter : if TRUE then the output of the function is a list of (split) token. Otherwise it’s a vector of character strings (sentences)


The following code chunks give an overview of the mentioned functions,



#------------------------
# tokenize_transform_text
#------------------------


# example input : "planets.txt"


res_txt = tokenize_transform_text(object = "/planets.txt", 
                                  
                                  to_lower = TRUE,
                                  
                                  utf_locale = "",           
                                  
                                  trim_token = TRUE,
                                  
                                  split_string = TRUE,
                                  
                                  remove_stopwords = TRUE, 
                                  
                                  language = "english",
                                  
                                  stemmer = "ngram_sequential",
                                      
                                  stemmer_ngram = 3,
                                      
                                  threads = 1)





the output is a vector of tokens after the english stopwords were removed and the terms were stemmed (ngram_sequential of length 3),



# example output :

$token
  [1] "ter"          "planet"        "anci"         "ties"         "hist"  ...

  [16] "early"        "cultu"        "divi"         "emissar"      "deit" ....

  [31] "object"       "2006"         "internatio"   "astro"        "union" ...  

  [46] "exclu"        "object"       "planet"        "mass"         "based"" ....
.
.
.

attr(,"class")
[1] "tokenization and transformation"




#----------------------------
# tokenize_transform_vec_docs
#----------------------------


# the input should be a vector of documents

init = token_stats$new(path_2file = "/planets.txt")

inp = init$path_2vector()   # convert text file to character vector


# run the function using the input-vector

res_dct = tokenize_transform_vec_docs(object = inp, 
                                      
                                      as_token = FALSE,  # return character vector of documents
                                      
                                      to_lower = TRUE,
                                  
                                      utf_locale = "",           
                                      
                                      trim_token = TRUE,
                                      
                                      split_string = TRUE,
                                      
                                      remove_stopwords = TRUE, 
                                      
                                      language = "english",
                                      
                                      stemmer = "porter2_stemmer", 
                                  
                                      threads = 1)



the output is a vector of transformed documents after the english stopwords were removed and the terms were stemmed (porter2-stemming),



$token
[1] "term planet ancient tie histori astrolog scienc mytholog religion planet solar ....."

[2] "planetari bodi discov 1950 remain planet modern definit celesti bodi cere palla ....."

[3] "planet thought ptolemi orbit earth defer epicycl motion idea planet orbit sun ....."

[4] "time care analysi pre-telescop observ data collect tycho brahe johann kepler  ....."

[5] "planet general divid main type larg lowdens giant planet smaller rocki terrestri ....."


attr(,"class")
[1] "tokenization and transformation"


The documents can be returned as a list of character vectors by specifying, as_token = TRUE,




# run the function using the input-vector

res_dct_tok = tokenize_transform_vec_docs(object = inp, 
                                      
                                          as_token = TRUE,
                                          
                                          to_lower = TRUE,
                                      
                                          utf_locale = "",           
                                          
                                          trim_token = TRUE,
                                          
                                          split_string = TRUE,
                                          
                                          remove_stopwords = TRUE, 
                                          
                                          language = "english",
                                          
                                          stemmer = "porter2_stemmer", 
                                  
                                          threads = 1)





$token
$token[[1]]
 [1] "term"        "planet"    "ancient"  "tie"   "histori"   ..... 

$token[[2]]
 [1] "planetari"    "bodi"     "discov"   "1950"   "remain"   .....    

$token[[3]]
 [1] "planet"       "thought"  "ptolemi"  "orbit"   "earth"   .....   


attr(,"class")
[1] "tokenization and transformation"


A few words about the utf_locale, remove_stopwords and stemmer parameters.


  • The utf_locale can take as input either an empty string (“”) or a character string (for instance “el_GR.UTF-8”). It should be a non-empty string if the text input is other than english. However, currently for the windows OS only english character strings or files can be input and pre-processed.


  • The remove_stopwords parameter can be either a boolean (TRUE, FALSE) or a character vector of user defined stop-words. The available languages are specified by the parameter language. Currently, there is no support for chinese, japanese, korean, thai or languages with ambiguous word boundaries.


  • The stemmer parameter can take as input one of the porter2_stemmer, ngram_sequential or ngram_overlap.
    • The porter2_stemmer is a C++ implementation of the snowball-porter2 stemming algorithm. The porter2_stemmer applies to all functions of the textTinyR package.
    • On the other hand, n-gram stemming is “language independent” and supported by the ngram_sequential and ngram_overlap functions. The n-gram stemming applies to all functions except for the sparse_term_matrix and tokenize_transform_vec_docs functions of the textTinyR package.
      • ngram_overlap : The ngram_overlap stemming method is based on N-Gram Morphemes for Retrieval, Paul McNamee and James Mayfield
      • ngram_sequential : The ngram_sequential stemming method is a modified version based on Generation, Implementation and Appraisal of an N-gram based Stemming Algorithm, B. P. Pande, Pawan Tamta, H. S. Dhami.



utility functions


The following code chunks illustrate the utility functions of the package (besides the read_characters() and read_rows() which used in the previous code chunks),



#---------------------------------------
# cosine distance between word sentences
#---------------------------------------

s1 = 'sentence with two words'

s2 = 'sentence with three words'

sep = " "

cosine_distance(s1, s2, split_separator = sep)


## [1] 0.75



#------------------------------------------------------------------------
# dice distance between two words (using n-grams -- the lower the better)
#------------------------------------------------------------------------

w1 = 'word_one'

w2 = 'word_two'

n = 2

dice_distance(w1, w2, n_grams = n)



## [1] 0.2857143



#---------------------------------------
# levenshtein distance between two words
#---------------------------------------

w1 = 'word_two'

w2 = 'word_one'

levenshtein_distance(w1, w2)


## [1] 3



#---------------------------------------------
# bytes converter (returns the size of a file)
#---------------------------------------------

PATH = "/planets.txt"

bytes_converter(input_path_file = PATH, unit = "KB" )


## [1] 2.213867



#---------------------------------------------------
# returns the utf-locale for the available languages
#---------------------------------------------------


utf_locale(language = "english")


## [1] "en.UTF-8"



#-----------------
# text file parser
#-----------------

# The text file should have a structure (such as an xml-structure), so that 
# subsets can be extracted using the "start_query" and "end_query" parameters.
# (it works similarly to the big_text_parser() method, however for small to medium sized files)

# example input "example_file.xml" file :

<?xml version="1.0"?>
<sked>
  <version>2</version>
  <flight>
    <carrier>BA</carrier>
    <number>4001</number>
    <date>2011-07-21</date>
  </flight>
  <flight cancelled="true">
    <carrier>BA</carrier>
    <number>4002</number>
    <date>2011-07-21</date>
  </flight>
</sked>





fp = text_file_parser(input_path_file = "example_file.xml", 
                      
                      output_path_file = "/output_folder/example_output_file.txt", 
                      
                      start_query = '<number>', end_query = '</number>',
                      
                      min_lines = 1, trimmed_line = FALSE)


"example_output_file.txt" :
  
4001
4002



#------------------
# vocabulary parser
#------------------

# the 'vocabulary_parser' function extracts a vocabulary from a structured text (such as 
# an .xml file) and works in the exact same way as the 'big_tokenize_transform' class, 
# however for small to medium sized data files


pars_dat = vocabulary_parser(input_path_file = '/folder/input_data.txt',
                             
                             start_query = 'start_word', end_query = 'end_word',
                             
                             vocabulary_path_file = '/folder/vocab.txt', 
                             
                             to_lower = TRUE, split_string = TRUE,
                             
                             remove_stopwords = TRUE)



An updated version of the textTinyR package can be found in my Github repository and to report bugs/issues please use the following link, https://github.com/mlampros/textTinyR/issues.


comments powered by Disqus