In [33]:
%matplotlib inline
import pandas as pd
import numpy as np
import os
import glob
import matplotlib as mpl
# Parametry wykresów
mpl.style.use('ggplot')
mpl.rcParams['figure.figsize'] = (8,6)
mpl.rcParams['font.size'] = 12
Macierz Term-Document (TD), jest to macierz, w której kolumny stanowią unikalne słowa, a wiersze stanowią wartości ile razy dane słowo wystąpiło w dokumencie. Dla przykładu (za Wikipedią), jeżeli mamy dwa dokumenty:
to macierz TD możemy przedstawić jako
I | like | hate | databases | |
---|---|---|---|---|
D1 | 1 | 1 | 0 | 1 |
D2 | 1 | 0 | 1 | 1 |
Poniżej ten sam przykład zrealizowany w Pandas:
In [34]:
sample = pd.DataFrame({
'docs': ['D1', 'D2'],
'lines': ['I like databases Databases', 'I hate databases']
})
sample
Out[34]:
In [35]:
sample['words'] = sample.lines.str.strip().str.lower().str.split('[\W_]+')
sample
Out[35]:
In [36]:
rows = list()
for row in sample[['docs', 'words']].iterrows():
r = row[1]
for word in r.words:
rows.append((r.docs, word))
words = pd.DataFrame(rows, columns=['docs', 'word'])
words
Out[36]:
In [37]:
words.pivot_table(index='docs',
columns='word',
aggfunc=lambda v: v['word'].count())\
.fillna(0)
Out[37]:
In [38]:
from sklearn.feature_extraction.text import CountVectorizer
count_vectorizer = CountVectorizer()
count_vectorizer
Out[38]:
In [39]:
sample['lines']
Out[39]:
In [40]:
X = count_vectorizer.fit_transform(sample['lines'])
X
Out[40]:
In [41]:
print(X)
In [42]:
X.toarray()
Out[42]:
In [43]:
count_vectorizer.get_feature_names()
Out[43]:
In [44]:
pd.DataFrame(X.toarray(),
columns=count_vectorizer.get_feature_names(),
index=sample['docs'])
Out[44]:
Kolejną techniką jest poznane uprzednio Term Frequency–Inverse Document Frequency (TF-IDF). Jest on popularnym algorytmem do analizy danych tekstowych, używany dość często w pozyskiwaniu danych (data mining).
Poniżej przykładowe obliczenie TF-IDF z użyciem wektoryzera scikit-learn.
In [45]:
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vectorizer = TfidfVectorizer()
tfidf_vectorizer
Out[45]:
In [46]:
X = tfidf_vectorizer.fit_transform(sample['lines'])
X
Out[46]:
In [47]:
X.toarray()
Out[47]:
In [48]:
tfidf_vectorizer.get_feature_names()
Out[48]:
In [49]:
pd.DataFrame(X.toarray(),
columns=tfidf_vectorizer.get_feature_names(),
index=sample['docs'])
Out[49]:
Jest wiele miar podobieństwa, które liczą jak odległe są pary wartości od siebie. Miary mogą być używane zarówno do wektorów oraz macierzy, jak i słów oraz dokumentów; zobacz ciekawy opis miar wraz z implementacją w Pythonie.
Pierwszą miarą, która jest standardową miarą podobieństwa dwóch stringów jest miara Levenshteina. Jest wiele implementacji używania tej miary, niemniej, najłatwiej dostępną implementacją jest zawarta w difflib, która jest częścią standardowej biblioteki Pythona.
In [50]:
import difflib
def similarity(a, b):
return difflib.SequenceMatcher(None, a, b).ratio()
similarity('cat', 'cats')
Out[50]:
In [51]:
similarity('cat', 'dog')
Out[51]:
In [52]:
sample
Out[52]:
In [53]:
similarity(sample.at[0, 'lines'], sample.at[1, 'lines'])
Out[53]:
In [54]:
similarity('cat', 'catepillar')
Out[54]:
W przypadku dokumentów tekstowych, znane są przynajmniej dwie popularne miary:
Miaria cosinusowa zdefiniowana jest jako kąt między dwoma wektorami:
$$ sim(A, B) = \cos(\Theta) = \frac{A \cdot B}{\Vert A \Vert \cdot \Vert B \Vert} $$Zatem miara wymaga formy zwektowyzowanej do obliczenia wartości; musimy się najpierw posłużyć którymś wektoryzatorem aby otrzymać macierz a potem policzyć miarę.
In [55]:
X = count_vectorizer.fit_transform(sample['lines'])
In [56]:
from sklearn.metrics.pairwise import cosine_similarity
cosine_similarity(X)
Out[56]:
Miara Jaccarda operuje z kolei na zbiorach i zdefiniowana jest jako:
$$ sim(A, B) = \frac{|A \cap B|}{| A \cup B |} $$Zatem dla naszego przykładu wygląda to następująco:
In [57]:
A = sample.words[0]
B = sample.words[1]
A, B
Out[57]:
In [58]:
def sim_jaccard(A, B):
a = set(A)
b = set(B)
i = set.intersection(a, b)
u = set.union(a, b)
print(i, u)
return len(i)/len(u)
sim_jaccard(A, B)
Out[58]:
In [59]:
from sklearn.metrics import jaccard_similarity_score
list(set(A))
jaccard_similarity_score(list(set(A)), list(set(B)))
Out[59]:
Scikit-learn wprowadził szereg ułatwień do przetwarzania danych i tworzenia modeli. W ogólności, możemy korzystać z 3 podstawowych elementów:
Transformetry (funkcje zmieniające dane) i estymatory (modele do wyuczenia) łączy się w pipeliny; więcej o tym można przeczytać w dokumentacji.
In [60]:
from sklearn.feature_extraction.text import TfidfTransformer
tfidf_transformer = TfidfTransformer()
tfidf_transformer
Out[60]:
In [61]:
td = CountVectorizer().fit_transform(sample['lines'])
tfidf_transformer.fit_transform(td)
Out[61]:
In [62]:
from sklearn.pipeline import Pipeline, make_pipeline
tfidf_pipeline = make_pipeline(CountVectorizer(), TfidfTransformer())
tfidf_pipeline
Out[62]:
In [63]:
X = tfidf_pipeline.fit_transform(sample['lines'])
X.toarray()
Out[63]:
In [64]:
pd.DataFrame(X.toarray(),
index=sample['docs'],
columns=tfidf_pipeline.steps[0][1].get_feature_names())
Out[64]:
Można też samemu nazywać elementy pipelinu:
In [65]:
list(steps.items())
In [ ]:
steps = {
'count_vect': CountVectorizer(),
'tfidf_trans': TfidfTransformer()
}
# Pipeline oczekuje kroków jako listy z krotkami (nazwa, obiekt)
tfidf_pipeline = Pipeline(list(steps.items()))
tfidf_pipeline
In [ ]:
tfidf_pipeline.steps
In [ ]:
tfidf_pipeline.fit_transform(sample['lines'])
In [ ]:
# Co tu się dzieje?
steps['count_vect'].get_feature_names()
Niekiedy mamy potrzebę korzystania z funkcji wybiegających poza zestaw dostępny w scikit-learn. Są dwie metody:
Obecnie najpierw zalecany jest FunctionTransformer.
In [ ]:
import re
import numpy as np
@np.vectorize
def replace_database(linia):
return re.sub('database', 'DB', linia, flags=re.IGNORECASE)
replace_database(sample['lines'])
In [ ]:
from sklearn.preprocessing import FunctionTransformer
replace_func = FunctionTransformer(replace_database, validate=False)
replace_func.fit_transform(sample['lines'])
In [ ]:
new_pipeline = make_pipeline(replace_func, TfidfVectorizer())
new_pipeline
In [ ]:
X = new_pipeline.fit_transform(sample['lines'])
X
In [ ]:
pd.DataFrame(X.toarray(),
index=sample['docs'],
columns=new_pipeline.steps[1][1].get_feature_names())
Drugą metodą tworzenia funkcji użytkownika jest stworzenie klasy, która dziedziczy po klasach BaseEstimator
i TransformerMixin
, czyli de facto jak to jest robione dla innych transformerów.
In [ ]:
from sklearn.base import BaseEstimator, TransformerMixin
class ReplaceDatabaseTransformer(BaseEstimator, TransformerMixin):
def __init__(self, replace_from='database', replace_to='DB'):
self.replace_from = replace_from
self.replace_to = replace_to
def fit(self, x, y=None):
return self
def _replace_str(self, line):
return re.sub(self.replace_from,
self.replace_to,
line,
flags=re.IGNORECASE)
def transform(self, data):
func = np.vectorize(lambda line: self._replace_str(line))
return func(data)
In [ ]:
new_pipeline2 = make_pipeline(ReplaceDatabaseTransformer(), TfidfVectorizer())
new_pipeline2
In [ ]:
X = new_pipeline2.fit_transform(sample['lines'])
X
In [ ]:
pd.DataFrame(X.toarray(),
index=sample['docs'],
columns=new_pipeline2.steps[1][1].get_feature_names())
W tej części notebooka przeprowadzimy klasyfikację danych tekstowych. Do analiz będziemy wykorzystywać kolekcję wiadomości SMS, zawierających spam i prawdziwe wiadomości, dostępnym w repozytorium UCI. Poniższy kod pobiera dane.
In [68]:
import os
import urllib.request
import zipfile
data_path = 'data'
os.makedirs(data_path, exist_ok=True)
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00228/smsspamcollection.zip'
file_name = url.split('/')[-1]
dest_file = os.path.join(data_path, file_name)
data_file = 'SMSSpamCollection'
data_full = os.path.join(data_path, data_file)
urllib.request.urlretrieve(url, dest_file)
with zipfile.ZipFile(dest_file) as zip_file:
zip_file.extract(data_file, path=data_path)
In [69]:
import pandas as pd
sms = pd.read_csv(data_full,
sep='\t',
names=['is_spam', 'text'])
sms.head()
Out[69]:
In [69]:
from sklearn.model_selection import train_test_split
train_sms, test_sms = train_test_split(sms, test_size=0.2)
print('Train set:')
print(train_sms.describe())
print()
print('Test set:')
print(test_sms.describe())
Naszym zadaniem jest wytrenowanie klasyfikatora, który klasyfikuje wiadomość SMS jako spam. Podobne zadanie z dość szerokim opisem można znaleźć w tym wpisie.
Zaczniemy od klasyfikatora Naive Bayes. Używa on twierdzenia Bayesa do obliczenia prawdopodobieństw poszczególnych klas w zależności od dostępnych opcji, przy założeniu niezależności zmiennych losowych (stąd naiwny). Rozpatrzmy przykład (źródło):
Poniżej przykład obliczenia prawdopodobieństwa na podstawie metody Bayesa.
$$P(Yes | Sunny) = \frac{P( Sunny | Yes) P(Yes)}{P (Sunny)}$$$$P (Sunny |Yes) = 3/9 = 0.33$$$$P(Sunny) = 5/14 = 0.36$$$$P( Yes)= 9/14 = 0.64$$ $$P (Yes | Sunny) = \frac{0.33 \cdot 0.64}{0.36} = 0.60$$
Poniżej, przykład modelu detekcji spamu z użuciem metody Naive Bayes.
In [70]:
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
vectorizer
Out[70]:
In [71]:
X = vectorizer.fit_transform(train_sms['text'])
X
Out[71]:
In [72]:
from sklearn.naive_bayes import MultinomialNB
spam_detector = MultinomialNB().fit(X, train_sms['is_spam'])
spam_detector
Out[72]:
In [78]:
i = 17
train_sms.iloc[i, 0], spam_detector.predict(X[i])[0], train_sms.iloc[i, 1]
Out[78]:
In [79]:
from sklearn.metrics import classification_report, confusion_matrix
X = vectorizer.transform(test_sms['text'])
y_pred = spam_detector.predict(X)
y_true = test_sms['is_spam']
print(confusion_matrix(y_true, y_pred))
print(classification_report(y_true, y_pred))
In [70]:
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
import pandas as pd
sms = pd.read_csv(data_full, sep='\t', names=['is_spam', 'text'])
train_sms, test_sms = train_test_split(sms, test_size=0.2)
steps = [('tfidf', TfidfVectorizer()), ('cls', MultinomialNB())]
nb_pipe = Pipeline(steps=steps)
nb_pipe.fit(train_sms['text'], train_sms['is_spam'])
y_pred = nb_pipe.predict(test_sms['text'])
y_true = test_sms['is_spam']
print(confusion_matrix(y_true, y_pred))
print(classification_report(y_true, y_pred))
Modelowanie tematów (topic modelling) jest to zadanie ekstrakcji ważnych elementów z tekstu, które są jego tematami. Wiodącą biblioteką w pythonie do tego celu jest gensim.
Jest wiele ciekawych algorytmów wykonujących to zadanie, jednak jednym z najgłośniejszych algorytmów do ekstrakcji tematów jest Latent Dirichlet Allocation (LDA).
Jedną z najskuteczniejszych implementaci LDA w Pythonie jest implementacja dostępna w gensim, która posiada też dość rozbudowaną dokumentację. Wykorzystajmy zatem LDA do naszego zadania.
Do analizy wykorzystamy zbiór 20 newsgroups z scikit-learn.
In [80]:
from sklearn.datasets import fetch_20newsgroups
newsgroups = fetch_20newsgroups(shuffle=True, random_state=1,
remove=('headers', 'footers', 'quotes'))
In [81]:
newsgroups.data[:2]
Out[81]:
In [82]:
newsgroups.target
Out[82]:
In [83]:
newsgroups.target_names
Out[83]:
Zawęzimy trochę próbkę, żeby uczenie było szybsze.
In [84]:
n_samples = 2000
newsgroups_samples = newsgroups.data[:n_samples]
Najpierw tworzymy korpus dla LDA używając narzędzi gensim.
In [85]:
import nltk
from gensim import corpora, models
from gensim.models.ldamodel import LdaModel
words = [nltk.regexp_tokenize(d.lower(), '\w+') for d in newsgroups_samples]
dictionary = corpora.Dictionary(words)
corpus = [dictionary.doc2bow(text) for text in words]
Słownik posiada ID słowa jako klucz i słowo jako wartość.
In [86]:
from itertools import islice
for k, v in islice(dictionary.items(), 10):
print('{}: {}'.format(k, v))
Korpus posiada listę krotek, lista na dokument; krotki zawierają pary ID słowa i jego liczność w danych dokumencie.
In [87]:
print(corpus[:2])
Teraz wytrenujmy sam model LDA.
In [88]:
model = LdaModel(corpus=corpus, id2word=dictionary, num_topics=10, alpha="auto")
model
Out[88]:
In [89]:
for i in range(10):
print(model.get_document_topics(corpus[i], minimum_probability=0.1))
In [90]:
k = 0
model.get_topic_terms(topicid=k)
Out[90]:
In [91]:
model.print_topics()
Out[91]:
In [ ]:
from gensim.sklearn_api.ldamodel import LdaTransformer
from gensim.sklearn_api.text2bow import Text2BowTransformer
from sklearn.pipeline import Pipeline
steps = {
'text2bow': Text2BowTransformer(),
'lda': LdaTransformer(num_topics=10)
}
p = Pipeline(list(steps.items()))
p
In [ ]:
p.fit_transform(newsgroups_samples)
In [ ]:
steps['lda'].gensim_model.print_topics()
In [ ]:
steps['text2bow'].gensim_model
In [ ]:
p.transform(['ala ma kota', 'lala'])
Możemy też zastosować wbudowaną metodę scikit-learn; zobacz dokumentację.
In [ ]:
# Funkcja użytkowa do wyświetlania tematów.
def print_top_words(model, feature_names, n_top_words):
for topic_idx, topic in enumerate(model.components_):
message = "Topic #%d: " % topic_idx
message += " ".join([feature_names[i]
for i in topic.argsort()[:-n_top_words - 1:-1]])
print(message)
print()
In [ ]:
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
tf_vectorizer = CountVectorizer(max_df=0.95, min_df=2,
max_features=1000,
stop_words='english')
tf = tf_vectorizer.fit_transform(newsgroups_samples)
lda = LatentDirichletAllocation(n_components=10, max_iter=5,
learning_method='online',
learning_offset=50.,
random_state=0)
lda.fit(tf)
tf_feature_names = tf_vectorizer.get_feature_names()
print_top_words(lda, tf_feature_names, 10)
Kolejnym algorytmem często używanym do ekstrakcji tematów jest Non-negative Matrix Factorization.
Metoda ta używa faktoryzacji do przybliżenia macierzy V jako iloczynu macierzy H i W. W wyniku działania algorytmu macierze H i W mają właściwości klasteryzacyjne danych w macierzy V. Dokładnie W staje się macierzą centroidów a H indykatorem przyporządkowania do klastrów poszczególnych elementów macierzy V.
W praktyce często stosuje się tą metodę jako zamiennik PCA, lecz tylko dla danych pozytywnych, oraz do ekstrakcji tematów.
In [ ]:
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import NMF, LatentDirichletAllocation
tfidf_vectorizer = TfidfVectorizer(max_df=0.95, min_df=2,
max_features=1000,
stop_words='english')
tfidf = tfidf_vectorizer.fit_transform(newsgroups_samples)
nmf = NMF(n_components=10, random_state=1,
alpha=.1, l1_ratio=.5).fit(tfidf)
tfidf_feature_names = tfidf_vectorizer.get_feature_names()
print_top_words(nmf, tfidf_feature_names, 10)
n_components
.beta_loss
; zobacz dokumentację.