2015-10-12 13 views
19

Ecco un appello per un modo migliore di fare qualcosa che posso già fare in modo inefficiente: filtrare una serie di token n-grammi usando "stop words" in modo che il verificarsi di qualsiasi termine della parola di arresto in una rimozione di trigger n-grammo.Come rimuovere efficientemente le parole chiave da un elenco di token ngram in R

Mi piacerebbe molto avere una soluzione che funzioni sia per unigram sia per n-gram, anche se sarebbe ok avere due versioni, una con un flag "fixed" e una con un flag "regex". Sto mettendo insieme i due aspetti della domanda, dal momento che qualcuno potrebbe avere una soluzione che prova un approccio diverso che affronta sia i modelli di stopword di espressioni fisse che regolari.

Formati:

  • token sono un elenco di vettori di caratteri, che possono essere unigrams, o n-grammi concatenati da un carattere _ (sottolineatura).

  • stopwords sono un vettore di caratteri. In questo momento sono contento di lasciare che questo sia una stringa fissa, ma sarebbe un bel vantaggio essere in grado di implementare questo usando anche le stopword formattate con espressioni regolari.

uscita desiderata: Un elenco di caratteri corrispondenti dell'ingresso token ma con un token qualsiasi componente corrispondenza a una parola di arresto sia rimosso. (Questo significa una corrispondenza unigram, o una corrispondenza uno dei termini che l'n-gramma comprende.)

esempi, dati di test, e codice di lavoro e benchmark per costruire il:

tokens1 <- list(text1 = c("this", "is", "a", "test", "text", "with", "a", "few", "words"), 
       text2 = c("some", "more", "words", "in", "this", "test", "text")) 
tokens2 <- list(text1 = c("this_is", "is_a", "a_test", "test_text", "text_with", "with_a", "a_few", "few_words"), 
       text2 = c("some_more", "more_words", "words_in", "in_this", "this_text", "text_text")) 
tokens3 <- list(text1 = c("this_is_a", "is_a_test", "a_test_text", "test_text_with", "text_with_a", "with_a_few", "a_few_words"), 
       text2 = c("some_more_words", "more_words_in", "words_in_this", "in_this_text", "this_text_text")) 
stopwords <- c("is", "a", "in", "this") 

# remove any single token that matches a stopword 
removeTokensOP1 <- function(w, stopwords) { 
    lapply(w, function(x) x[-which(x %in% stopwords)]) 
} 

# remove any word pair where a single word contains a stopword 
removeTokensOP2 <- function(w, stopwords) { 
    matchPattern <- paste0("(^|_)", paste(stopwords, collapse = "(_|$)|(^|_)"), "(_|$)") 
    lapply(w, function(x) x[-grep(matchPattern, x)]) 
} 

removeTokensOP1(tokens1, stopwords) 
## $text1 
## [1] "test" "text" "with" "few" "words" 
## 
## $text2 
## [1] "some" "more" "words" "test" "text" 

removeTokensOP2(tokens1, stopwords) 
## $text1 
## [1] "test" "text" "with" "few" "words" 
## 
## $text2 
## [1] "some" "more" "words" "test" "text" 

removeTokensOP2(tokens2, stopwords) 
## $text1 
## [1] "test_text" "text_with" "few_words" 
## 
## $text2 
## [1] "some_more" "more_words" "text_text" 

removeTokensOP2(tokens3, stopwords) 
## $text1 
## [1] "test_text_with" 
## 
## $text2 
## [1] "some_more_words" 

# performance benchmarks for answers to build on 
require(microbenchmark) 
microbenchmark(OP1_1 = removeTokensOP1(tokens1, stopwords), 
       OP2_1 = removeTokensOP2(tokens1, stopwords), 
       OP2_2 = removeTokensOP2(tokens2, stopwords), 
       OP2_3 = removeTokensOP2(tokens3, stopwords), 
       unit = "relative") 
## Unit: relative 
## expr  min  lq  mean median  uq  max neval 
## OP1_1 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 100 
## OP2_1 5.119066 3.812845 3.438076 3.714492 3.547187 2.838351 100 
## OP2_2 5.230429 3.903135 3.509935 3.790143 3.631305 2.510629 100 
## OP2_3 5.204924 3.884746 3.578178 3.753979 3.553729 8.240244 100 
+0

il metodo di eliminazione stopword in tm o qdap non è sufficiente? Anche se funzionano in un altro modo, prima rimuovi le stopword e poi crea i n-grammi. – phiver

+1

No, è abbastanza facile, sto cercando di capire un modo efficace per rimuovere gli ngram contenenti stopword dopo la costruzione. –

+0

Hai controllato il nuovo pacchetto di Tyler Rinker, termco su github? Sembra promettente. Non ho ancora avuto il tempo di verificarlo. – phiver

risposta

5

Questo non è davvero una risposta - più di un commento alla risposta al commento di rawr di passare attraverso tutte le combinazioni di stopword. Con un elenco più lungo di stopwords, l'utilizzo di qualcosa come %in% non sembra risentire del problema di dimensionalità.

library(purrr) 
removetokenstst <- function(tokens, stopwords) 
    map2(tokens, 
     lapply(tokens3, function(x) { 
     unlist(lapply(strsplit(x, "_"), function(y) { 
      any(y %in% stopwords) 
     })) 
     }), 
     ~ .x[!.y]) 

require(microbenchmark) 
microbenchmark(OP1_1 = removeTokensOP1(tokens1, morestopwords), 
      OP2_1 = removeTokensOP2(tokens1, morestopwords), 
      OP2_2 = removeTokensOP2(tokens2, morestopwords), 
      OP2_3 = removeTokensOP2(tokens3, morestopwords), 
      Ak_3 = removetokenstst(tokens3, stopwords), 
      Ak_3msw = removetokenstst(tokens3, morestopwords), 
      unit = "relative") 

Unit: relative 
    expr  min  lq  mean median  uq  max neval 
    OP1_1 1.00000 1.00000 1.000000 1.000000 1.000000 1.00000 100 
    OP2_1 278.48260 176.22273 96.462854 79.787932 76.904987 38.31767 100 
    OP2_2 280.90242 181.22013 98.545148 81.407928 77.637006 64.94842 100 
    OP2_3 279.43728 183.11366 114.879904 81.404236 82.614739 72.04741 100 
    Ak_3 15.74301 14.83731 9.340444 7.902213 8.164234 11.27133 100 
Ak_3msw 18.57697 14.45574 12.936594 8.513725 8.997922 24.03969 100 

Stopwords

morestopwords = c("a", "about", "above", "after", "again", "against", "all", 
"am", "an", "and", "any", "are", "arent", "as", "at", "be", "because", 
"been", "before", "being", "below", "between", "both", "but", 
"by", "cant", "cannot", "could", "couldnt", "did", "didnt", "do", 
"does", "doesnt", "doing", "dont", "down", "during", "each", 
"few", "for", "from", "further", "had", "hadnt", "has", "hasnt", 
"have", "havent", "having", "he", "hed", "hell", "hes", "her", 
"here", "heres", "hers", "herself", "him", "himself", "his", 
"how", "hows", "i", "id", "ill", "im", "ive", "if", "in", "into", 
"is", "isnt", "it", "its", "its", "itself", "lets", "me", "more", 
"most", "mustnt", "my", "myself", "no", "nor", "not", "of", "off", 
"on", "once", "only", "or", "other", "ought", "our", "ours", 
"ourselves", "out", "over", "own", "same", "shant", "she", "shed", 
"shell", "shes", "should", "shouldnt", "so", "some", "such", 
"than", "that", "thats", "the", "their", "theirs", "them", "themselves", 
"then", "there", "theres", "these", "they", "theyd", "theyll", 
"theyre", "theyve", "this", "those", "through", "to", "too", 
"under", "until", "up", "very", "was", "wasnt", "we", "wed", 
"well", "were", "weve", "were", "werent", "what", "whats", "when", 
"whens", "where", "wheres", "which", "while", "who", "whos", 
"whom", "why", "whys", "with", "wont", "would", "wouldnt", "you", 
"youd", "youll", "youre", "youve", "your", "yours", "yourself", 
"yourselves", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", 
"x", "y", "z") 
+0

ma questo non sta facendo esattamente la stessa cosa poiché '% in%' è [solo corrispondente alla tabella] (https://github.com/wch/r-source/blob/b156e3a711967f58131e23c1b1dc1ea90e2f0c43/src/main/unique. C# L922), cioè, la lunghezza di stopwords o qualunque cosa ottieni quando dividi le stringhe mentre 'grepl' sta andando [carattere per carattere] (https://github.com/wch/r-source/blob/b156e3a711967f58131e23c1b1dc1ea90e2f0c43 /src/main/grep.c#L679). quindi per 'stopwords <- c (" is "," a "," in "," this ")', '% in%' ha quattro cose da fare e grepl ne ha molte altre a seconda del vettore di destinazione e della lunghezza di quelle stringhe – rawr

1

Possiamo migliorare su lapply se hai molti livelli nell'elenco utilizzando il pacchetto parallel.

creare molti livelli

tokens2 <- list(text1 = c("this_is", "is_a", "a_test", "test_text", "text_with", "with_a", "a_few", "few_words"), 
       text2 = c("some_more", "more_words", "words_in", "in_this", "this_text", "text_text")) 
tokens2 <- lapply(1:500,function(x) sample(tokens2,1)[[1]]) 

Lo facciamo perché il pacchetto parallelo ha un sacco di spese generali da configurare, quindi basta aumentare il numero di iterazioni su microbenchmark continuerà a sostenere tale costo. Aumentando la dimensione della lista, vedi il vero miglioramento.

library(parallel) 
#Setup 
cl <- detectCores() 
cl <- makeCluster(cl) 

#Two functions: 

#original 
removeTokensOP2 <- function(w, stopwords) { 
    matchPattern <- paste0("(^|_)", paste(stopwords, collapse = "(_|$)|(^|_)"), "(_|$)") 
    lapply(w, function(x) x[-grep(matchPattern, x)]) 
} 

#new 
removeTokensOPP <- function(w, stopwords) { 
    matchPattern <- paste0("(^|_)", paste(stopwords, collapse = "(_|$)|(^|_)"), "(_|$)") 
    return(w[-grep(matchPattern, w)]) 
} 

#compare 

microbenchmark(
    OP2_P = parLapply(cl,tokens2,removeTokensOPP,stopwords), 
    OP2_2 = removeTokensOP2(tokens2, stopwords), 
    unit = 'relative' 
) 

Unit: relative 
    expr  min  lq  mean median  uq  max neval 
OP2_P 1.000000 1.000000 1.000000 1.000000 1.000000 1.00000 100 
OP2_2 1.730565 1.653872 1.678781 1.562258 1.471347 10.11306 100 

All'aumentare del numero di livelli nell'elenco, le prestazioni miglioreranno.

1

È migth considera simlifying le espressioni regolari,^e $ stanno aggiungendo alla testa

remove_short <- function(x, stopwords) { 
    stopwords_regexp <- paste0('(^|_)(', paste(stopwords, collapse = '|'), ')(_|$)') 
    lapply(x, function(x) x[!grepl(stopwords_regexp, x)]) 
} 
require(microbenchmark) 
microbenchmark(OP1_1 = removeTokensOP1(tokens1, stopwords), 
       OP2_1 = removeTokensOP2(tokens2, stopwords), 
       OP2_2 = remove_short(tokens2, stopwords), 
       unit = "relative") 
Unit: relative 
    expr  min  lq  mean median  uq  max neval cld 
OP1_1 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 100 a 
OP2_1 5.178565 4.768749 4.465138 4.441130 4.262399 4.266905 100 c 
OP2_2 3.452386 3.247279 3.063660 3.068571 2.963794 2.948189 100 b 
+0

Ma poi ottengo un corrispondenza positiva per "bello" dalla "parola d'ordine" se ", ecc. –

+1

Hai ragione. Ancora c'è un ottimizatiou minore alla tua espressione regolare: Invece di '(^ | _) è (_ | $) | (^ | _) a (_ | $) | (^ | _) in (_ | $) | (^ | _) questo (_ | $) 'puoi scriverlo come' (^ | _) (è | a | in | this) (_ | $) ' Ho modificato la mia risposta per riflettere la differenza – Vlados

Problemi correlati