web-dev-qa-db-fra.com

NLTK tokenize - moyen plus rapide?

J'ai une méthode qui prend un paramètre String et utilise NLTK pour décomposer la chaîne en phrases, puis en mots. Ensuite, il convertit chaque mot en minuscules et crée enfin un dictionnaire de la fréquence de chaque mot.

import nltk
from collections import Counter

def freq(string):
    f = Counter()
    sentence_list = nltk.tokenize.sent_tokenize(string)
    for sentence in sentence_list:
        words = nltk.Word_tokenize(sentence)
        words = [Word.lower() for Word in words]
        for Word in words:
            f[Word] += 1
    return f

Je suis censé optimiser davantage le code ci-dessus pour accélérer le temps de prétraitement et je ne sais pas comment le faire. La valeur de retour doit évidemment être exactement la même que ci-dessus, donc je suis censé utiliser nltk bien qu'il ne soit pas explicitement requis pour le faire.

Une façon d'accélérer le code ci-dessus? Merci.

14
user3280193

Si vous voulez juste une liste plate de jetons, notez que Word_tokenize Appellerait sent_tokenize Implicitement, voir https://github.com/nltk/nltk/blob/develop/nltk /tokenize/init.py#L98

_treebank_Word_tokenize = TreebankWordTokenizer().tokenize
def Word_tokenize(text, language='english'):
    """
    Return a tokenized copy of *text*,
    using NLTK's recommended Word tokenizer
    (currently :class:`.TreebankWordTokenizer`
    along with :class:`.PunktSentenceTokenizer`
    for the specified language).
    :param text: text to split into sentences
    :param language: the model name in the Punkt corpus
    """
    return [token for sent in sent_tokenize(text, language)
            for token in _treebank_Word_tokenize(sent)]

En utilisant un corpus brun comme exemple, avec Counter(Word_tokenize(string_corpus)):

>>> from collections import Counter
>>> from nltk.corpus import brown
>>> from nltk import sent_tokenize, Word_tokenize
>>> string_corpus = brown.raw() # Plaintext, str type.
>>> start = time.time(); fdist = Counter(Word_tokenize(string_corpus)); end = time.time() - start
>>> end
12.662328958511353
>>> fdist.most_common(5)
[(u',', 116672), (u'/', 89031), (u'the/at', 62288), (u'.', 60646), (u'./', 48812)]
>>> sum(fdist.values())
1423314

~ 1,4 million de mots ont pris 12 secondes (sans enregistrer le corpus à jetons) sur ma machine avec les spécifications:

alvas@ubi:~$ cat /proc/cpuinfo
processor   : 0
vendor_id   : GenuineIntel
cpu family  : 6
model       : 69
model name  : Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz
stepping    : 1
microcode   : 0x17
cpu MHz     : 1600.027
cache size  : 3072 KB
physical id : 0
siblings    : 4
core id     : 0
cpu cores   : 2

$ cat /proc/meminfo
MemTotal:       12004468 kB

Enregistrer le corpus à jetons d'abord tokenized_corpus = [Word_tokenize(sent) for sent in sent_tokenize(string_corpus)], puis utiliser Counter(chain*(tokenized_corpus)):

>>> from itertools import chain
>>> start = time.time(); tokenized_corpus = [Word_tokenize(sent) for sent in sent_tokenize(string_corpus)]; fdist = Counter(chain(*tokenized_corpus)); end = time.time() - start
>>> end
16.421464920043945

Utilisation de ToktokTokenizer()

>>> from collections import Counter
>>> import time
>>> from itertools import chain
>>> from nltk.corpus import brown
>>> from nltk import sent_tokenize, Word_tokenize
>>> from nltk.tokenize import ToktokTokenizer
>>> toktok = ToktokTokenizer()
>>> string_corpus = brown.raw()

>>> start = time.time(); tokenized_corpus = [toktok.tokenize(sent) for sent in sent_tokenize(string_corpus)]; fdist = Counter(chain(*tokenized_corpus)); end = time.time() - start 
>>> end
10.00472116470337

Utilisation de MosesTokenizer():

>>> from nltk.tokenize.moses import MosesTokenizer
>>> moses = MosesTokenizer()
>>> start = time.time(); tokenized_corpus = [moses.tokenize(sent) for sent in sent_tokenize(string_corpus)]; fdist = Counter(chain(*tokenized_corpus)); end = time.time() - start 
>>> end
30.783339023590088
>>> start = time.time(); tokenized_corpus = [moses.tokenize(sent) for sent in sent_tokenize(string_corpus)]; fdist = Counter(chain(*tokenized_corpus)); end = time.time() - start 
>>> end
30.559681177139282

Pourquoi utiliser MosesTokenizer

Il a été implémenté de telle manière qu'il existe un moyen de retourner les jetons en chaîne, c'est-à-dire "detokenize".

>>> from nltk.tokenize.moses import MosesTokenizer, MosesDetokenizer
>>> t, d = MosesTokenizer(), MosesDetokenizer()
>>> sent = "This ain't funny. It's actually hillarious, yet double Ls. | [] < > [ ] & You're gonna shake it off? Don't?"
>>> expected_tokens = [u'This', u'ain', u'&apos;t', u'funny.', u'It', u'&apos;s', u'actually', u'hillarious', u',', u'yet', u'double', u'Ls.', u'&#124;', u'&#91;', u'&#93;', u'&lt;', u'&gt;', u'&#91;', u'&#93;', u'&amp;', u'You', u'&apos;re', u'gonna', u'shake', u'it', u'off', u'?', u'Don', u'&apos;t', u'?']
>>> expected_detokens = "This ain't funny. It's actually hillarious, yet double Ls. | [] < > [] & You're gonna shake it off? Don't?"
>>> tokens = t.tokenize(sent)
>>> tokens == expected_tokens
True
>>> detokens = d.detokenize(tokens)
>>> " ".join(detokens) == expected_detokens
True

Utilisation de ReppTokenizer():

>>> repp = ReppTokenizer('/home/alvas/repp')
>>> start = time.time(); sentences = sent_tokenize(string_corpus); tokenized_corpus = repp.tokenize_sents(sentences); fdist = Counter(chain(*tokenized_corpus)); end = time.time() - start
>>> end
76.44129395484924

Pourquoi utiliser ReppTokenizer?

Il renvoie l'offset des jetons de la chaîne d'origine.

>>> sents = ['Tokenization is widely regarded as a solved problem due to the high accuracy that rulebased tokenizers achieve.' ,
... 'But rule-based tokenizers are hard to maintain and their rules language specific.' ,
... 'We evaluated our method on three languages and obtained error rates of 0.27% (English), 0.35% (Dutch) and 0.76% (Italian) for our best models.'
... ]
>>> tokenizer = ReppTokenizer('/home/alvas/repp/') # doctest: +SKIP
>>> for sent in sents:                             # doctest: +SKIP
...     tokenizer.tokenize(sent)                   # doctest: +SKIP
... 
(u'Tokenization', u'is', u'widely', u'regarded', u'as', u'a', u'solved', u'problem', u'due', u'to', u'the', u'high', u'accuracy', u'that', u'rulebased', u'tokenizers', u'achieve', u'.')
(u'But', u'rule-based', u'tokenizers', u'are', u'hard', u'to', u'maintain', u'and', u'their', u'rules', u'language', u'specific', u'.')
(u'We', u'evaluated', u'our', u'method', u'on', u'three', u'languages', u'and', u'obtained', u'error', u'rates', u'of', u'0.27', u'%', u'(', u'English', u')', u',', u'0.35', u'%', u'(', u'Dutch', u')', u'and', u'0.76', u'%', u'(', u'Italian', u')', u'for', u'our', u'best', u'models', u'.')
>>> for sent in tokenizer.tokenize_sents(sents): 
...     print sent                               
... 
(u'Tokenization', u'is', u'widely', u'regarded', u'as', u'a', u'solved', u'problem', u'due', u'to', u'the', u'high', u'accuracy', u'that', u'rulebased', u'tokenizers', u'achieve', u'.')
(u'But', u'rule-based', u'tokenizers', u'are', u'hard', u'to', u'maintain', u'and', u'their', u'rules', u'language', u'specific', u'.')
(u'We', u'evaluated', u'our', u'method', u'on', u'three', u'languages', u'and', u'obtained', u'error', u'rates', u'of', u'0.27', u'%', u'(', u'English', u')', u',', u'0.35', u'%', u'(', u'Dutch', u')', u'and', u'0.76', u'%', u'(', u'Italian', u')', u'for', u'our', u'best', u'models', u'.')
>>> for sent in tokenizer.tokenize_sents(sents, keep_token_positions=True): 
...     print sent
... 
[(u'Tokenization', 0, 12), (u'is', 13, 15), (u'widely', 16, 22), (u'regarded', 23, 31), (u'as', 32, 34), (u'a', 35, 36), (u'solved', 37, 43), (u'problem', 44, 51), (u'due', 52, 55), (u'to', 56, 58), (u'the', 59, 62), (u'high', 63, 67), (u'accuracy', 68, 76), (u'that', 77, 81), (u'rulebased', 82, 91), (u'tokenizers', 92, 102), (u'achieve', 103, 110), (u'.', 110, 111)]
[(u'But', 0, 3), (u'rule-based', 4, 14), (u'tokenizers', 15, 25), (u'are', 26, 29), (u'hard', 30, 34), (u'to', 35, 37), (u'maintain', 38, 46), (u'and', 47, 50), (u'their', 51, 56), (u'rules', 57, 62), (u'language', 63, 71), (u'specific', 72, 80), (u'.', 80, 81)]
[(u'We', 0, 2), (u'evaluated', 3, 12), (u'our', 13, 16), (u'method', 17, 23), (u'on', 24, 26), (u'three', 27, 32), (u'languages', 33, 42), (u'and', 43, 46), (u'obtained', 47, 55), (u'error', 56, 61), (u'rates', 62, 67), (u'of', 68, 70), (u'0.27', 71, 75), (u'%', 75, 76), (u'(', 77, 78), (u'English', 78, 85), (u')', 85, 86), (u',', 86, 87), (u'0.35', 88, 92), (u'%', 92, 93), (u'(', 94, 95), (u'Dutch', 95, 100), (u')', 100, 101), (u'and', 102, 105), (u'0.76', 106, 110), (u'%', 110, 111), (u'(', 112, 113), (u'Italian', 113, 120), (u')', 120, 121), (u'for', 122, 125), (u'our', 126, 129), (u'best', 130, 134), (u'models', 135, 141), (u'.', 141, 142)]

TL; DR

Avantages des différents tokenizers

  • Word_tokenize() appelle implicitement sent_tokenize()
  • ToktokTokenizer() est la plus rapide
  • MosesTokenizer() est capable de détokéniser le texte
  • ReppTokenizer() est capable de fournir des décalages de jetons

Q: Existe-t-il un tokenizer rapide qui peut détokenizer et me fournit également des décalages et également effectuer la tokenisation de phrases en NLTK?

R: Je ne pense pas, essayez gensim ou spacy.

15
alvas

La création de listes inutiles est mauvaise

Votre code est implicitement créant beaucoup d'instances list potentiellement très longues qui n'ont pas besoin d'être là, par exemple:

words = [Word.lower() for Word in words]

En utilisant le [...] syntaxe pour compréhension de la liste crée une liste de longueur n pour n jetons trouvés dans votre entrée, mais tout ce que vous voulez faire est d'obtenir la fréquence de chaque jeton, pas de les stocker réellement:

f[Word] += 1

Par conséquent, vous devez utiliser un générateur à la place:

words = (Word.lower() for Word in words)

De même, nltk.tokenize.sent_tokenize et nltk.tokenize.Word_tokenize les deux semblent produire des listes en sortie, ce qui est encore inutile; Essayez d'utiliser une fonction de bas niveau, par ex. nltk.tokenize.api.StringTokenizer.span_tokenize, qui génère simplement un itérateur qui produit des décalages de jetons pour votre flux d'entrée, c'est-à-dire des paires d'indices de votre chaîne d'entrée représentant chaque jeton.

Une meilleure solution

Voici un exemple n'utilisant aucune liste intermédiaire:

def freq(string):
    '''
    @param string: The string to get token counts for. Note that this should already have been normalized if you wish it to be so.
    @return: A new Counter instance representing the frequency of each token found in the input string.
    '''
    spans = nltk.tokenize.WhitespaceTokenizer().span_tokenize(string)   
    # Yield the relevant slice of the input string representing each individual token in the sequence
    tokens = (string[begin : end] for (begin, end) in spans)
    return Counter(tokens)

Avis de non-responsabilité: Je n'ai pas profilé cela, il est donc possible que, par exemple le peuple NLTK a fait Word_tokenize incroyablement rapide mais négligé span_tokenize; Assurez-vous toujours de profiler votre application.

TL; DR

N'utilisez pas de listes lorsque les générateurs suffiront: chaque fois que vous créez une liste juste pour la jeter après l'avoir utilisée une fois, Dieu tue un chaton.

9
errantlinguist