Python ngram tokenizer z wykorzystaniem generatorów

Jak w czystym Python’ie dokonać tokenizacji tekstu na ngramy oraz jak wygenerować słownik z wszystkimi możliwymi ngramami.

W wielu zadaniach NLP (Natural Languge processing) stajemy przed problemem wygenerowania słownika, czyli listy cząstek (tokenów) zdania potrzebnych do zakodowania naszego tekstu do formy liczbowej. Najczęściej tokeny utożsamiamy z wyrazami, bądź frazami (np. Bielsko-Biała).

Ostatnie prace w MT (machine translation) wymogły stosowanie cząstek wyrazów najczęściej występującej w korpusie tekstu. Słownik taki składa się z 1,2,3,4 itd literowych tokenów z których można złożyć całe słowo. Innym ekstremalnym przykładem jest operowanie tylko na pojedynczych literach. W wielu zdaniach NLP (machine translation, czy korekta błędów) okazuje się, że równie dobrze spisują się 2,3- ngramy znaków.

Przeanalizujmy przykład. Załóżmy że dysponujemy słownikiem składającym się z tokenów [‘a’, ‘b’, ‘c’, …’z’]. Na jego podstawie przykładowy wyraz kodujemy przy pomocy indeksów występowania tokenów w tym słowniku.
Słowo: “aabbc” zakodujemy więc jako tablicę liczb [0,0,1,1,2].

Obecnie wiele bibliotek jest w stanie za nas zrobić tego typu kodowanie np.:

Ostatnio właśnie stanąłem przed problemem zakodowaniu tekstu w postaci ngramów, czyli zbitek 2,3,4 znakowych na potrzeby prac przy naszym projekcie korektora tekstu – GoodWrite.pl. Można to oczywiście zrobić z wykorzystaniem sciki-learn:

niestety trudno było mi to zastosować do znaków i następnie zintegrować z kodem w Pytorch.

Ngram tokenizer w Pytchon

Poniżej zamieszczam 4 funkcje, których zadaniem jest tokenizacja tekstu na ngramy o dowolnej wielkości (2,3,4 itd) oraz generowanie wszystkich możliwych ngramów występujących w tekście. Obie wersje funkcji w postaci przyjmujących sekwencję oraz generator.

#%%
from collections import Counter, OrderedDict
from itertools import zip_longest , tee

## ngram iterators and tokenizers, working on list or generators


def ngram_tokenizer_iter(iterable, n, fillvalue=''):
    "generuje pary znaków obok siebie, tokenizuje [abcd]->ab, cd dla tekstu przekazanego w formie generatora"
    # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    zip_tuples = zip_longest(*args, fillvalue=fillvalue)
    for tup in zip_tuples:
        yield "".join(tup)

def ngram_tokenizer(ngrams):
    '''generuje pary znaków obok siebie, tokenizuje [abcd]->ab, cd
    '''
    def func(text):
        return (text[pos:pos + ngrams] for pos in range(0, len(text), ngrams))
    return func


def ngram_vocab_gen(ngrams):
    '''generuje wszystkie ngramy [abcd]->ab, bc, cd, d. Wersja pracująca z sekwencją (listą)
    '''
    def func(text):
        return (text[i:i+ngrams] for i in range(len(text)+1-ngrams))
    return func

def ngram_vocab_gen_iter(iterator,ngrams):
    '''generuje wszystkie ngramy [abcd]->ab, bc, cd, d. Wersja pracująca z generatorem
    '''
    iter_tuple = tee(iterator, ngrams)

    nested_iter_next=0
    list_of_iters = []
    for i,one_iter in enumerate(iter_tuple):
       
        for _ in range(nested_iter_next):
            next(one_iter,"")
       
        list_of_iters.append(one_iter)
        nested_iter_next+=1
    
    for tup in zip(*list_of_iters):
        yield "".join(tup)

Po pierwsze, zauważcie że sama tokenizacja i generowanie ngramów to dwie odrobinę różne rzeczy. Funkcja tokenizująca potrzebna jest aby słowo zamienić na ngramy i ewentualnie łatwo odwrócić ten proces. Natomiast funkcja do generowania ngramów potrzebna jest do wygenerowania słownika. Z tej samej ilości tekstu wygeneruje szybciej wszystkie możliwe ngramy.

Funkcjie ngram_tokenizer i ngram_vocab_gen przyjmują jako parametr sekwencję (najczęściej listę) bo wymagane jest określenie rozmiaru sekwencji (funkcja len). Obie te funkcje mają swój odpowiednik przyjmujący jako parametr generator ngram_tokenizer_iter ngram_vocab_gen_iter.

Wykorzystanie funkcji zależy od waszego przypadku, skąd pobierany jest tekst. Czy jest go dużo i podawany jest z wykorzystaniem yield czy po prostu wczytany do wielkiej listy.

Jak użyć ngram tokenizera i jak wyglądają wyniki tokenizacji

Przetestujmy czy wyniki tokenizacji w obu implementacjach się pokrywają

#%%

dataset_text = "aabbcc ddaaa aacca caca baaba baac " #dataset z znaków a,b,c  aby było prościej :)

# dataset_text = "Twój długi tekst, najczęściej scalony cały dataset to jednego stringa, albo jego część, albo generator odczytujący z pliku linie po lini "


#%% All possible char-bi-grams
a=list(ngram_vocab_gen(2)(dataset_text))
print(f'{len(a)} {a[0:10]}')

#%% 
a=list(ngram_vocab_gen_iter(dataset_text,2))
print(f'{len(a)} {a[0:10]}')


#%% bi-gram tokenizer

a=list(ngram_tokenizer_iter(dataset_text,2))
print(f'{len(a)} {a[0:10]}')

#%%
a=list(ngram_tokenizer(2)(dataset_text))
print(f'{len(a)} {a[0:10]}')

Zwróccie uwagę na inny sposób wywołania metod oraz różnicę pomiędzy tokenizacją a generowaniem ngramów. W przypadku ngram generatora ilość tokenów jest większa (i tak powinno być)

Sprawdźmy jak szybko to działa

Sprawdźmy różnice w czasie działania wykrozystując pakiet timeit

#%% testing speed and accuracy of ngram tokenization and ngram seed generation
import timeit
import numpy as np

SETUP_CODE = ''' 
from __main__ import ngram_tokenizer_iter, ngram_tokenizer,ngram_vocab_gen, ngram_vocab_gen_iter
from __main__ import dataset_text
'''

CODE1='''
a=list(ngram_tokenizer_iter(dataset_text,2))
'''
CODE2='''
a=list(ngram_tokenizer(2)(dataset_text))
'''
CODE3='''
a=list(ngram_vocab_gen(2)(dataset_text))
'''

CODE4='''
a=list(ngram_vocab_gen_iter(dataset_text,2))
'''

print(f'{CODE1} time={np.mean(timeit.repeat(CODE1,SETUP_CODE, repeat=3,number=10))}')
print(f'{CODE2} time={np.mean(timeit.repeat(CODE2,SETUP_CODE, repeat=3,number=10))}')
print(f'{CODE3} time={np.mean(timeit.repeat(CODE3,SETUP_CODE, repeat=3,number=10))}')
print(f'{CODE4} time={np.mean(timeit.repeat(CODE4,SETUP_CODE, repeat=3,number=10))}')

Po uruchmieniu nie powinno być dużych różnic w czasie działania ale nie testowałem tego jeszcze na dużej ilości.
Poniżej przykładowy wynik czasu działania dla tekstu od długości 326500 znaków.

Python ngram tokenizer czas działania
Czas działania i wynik dla python ngram tokenizera na tekście o długości 326500 znaków

Podsumowanie

We wpisie przedstawiłem po dwie implementacja ngram tokenizera oraz ngram generatora w czystym Pythonie. Czas działania funkcji jest podobny, ale nie testowałem ich na duuuużych danych, więc z chęcią przeczytam o waszych eksperymentach. Jeżeli widzisz możliwość ulepszenia to skomentuj, będzie dla potomnych 🙂

Link do pełnego pliku z kodem https://gist.github.com/ksopyla/d9d7bf1eda1a426ecff9fe2b40969dbc

Jeżeli uważasz ten wpis za wartościowy to Zasubskrybuj bloga. Dostaniesz informacje o nowych artykułach.

Join 73 other subscribers

Image by Hans Braxmeier from Pixabay

Leave a Reply

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.