<jupyter_start><jupyter_text>Word Embeddings---Reprezentacja Bag of words (BOW) jest bardzo użyteczna, jednak nie jest pozbawiona wad. Jest ona zazwyczaj pamięciożerna (długość wektora o rozmiarach liczności całego słownika). Co prawda możemy ograniczyć zużycie pamięci poprzez ograniczenie słownika do najważniejszych słów w korpusie, jednak wtedy tracimy informacje, które niosą usunięte słowa, gdyż są one ignorowane.  Innym problemem z tą reprezentacją jest jej niemożność zakodowania informacji o podobieńśtwie słów (co uwidoczni zadanie 1).  Istnieje zatem potrzeba szukania alternatywnych reprezentacji, które pozwolą obejść wyżej wymienione problemy. Jedną z nich są Word Embeddings, których dotyczyć będą dzisiejsze laboratoria.Do wykonania zadań potrzebować będziemy zewnętrznych zasobów (embeddingów). Wejdź pod adres: http://nlp.stanford.edu/data/glove.6B.zip, rozpakuj paczkę, a następnie przenieś plik: "glove.6B.50d.txt" do folderu, w którym znajudje się ten notebook. Zadanie 1: Podobieństwo dokumentów - <NAME>Dość częstą potrzebą jest ocena podobieństwa dwóch dokumentów. Kiedy reprezentujemy dokumenty jako wektory równej długości (a używając BOW mamy równą długość wektorów), możemy użyć miary cosinusowej do oceny podobieństwa wektorów.$similarity = cos(\vec{a}, \vec{b}) = \frac{\sum_{i=1}^na_i b_i}{\sqrt{\sum_{i=1}^{n}a_i^2}\sqrt{\sum_{i=1}^{n}b_i^2}}$gdzie $\vec{a}$ to wektor związany z dokumentem nr 1, a $\vec{b}$ analogicznie - z dokumentem nr 2.**Zad1 (1 punkt)** Zaimplementuj miarę podobieństwa cosinusowego między dwoma wektorami dowolnej długości zgodnie z podanym powyżej wzorem.<jupyter_code>import numpy as np            
def cosine(a, b):
    return np.dot(a, b)/(np.linalg.norm(a)*np.linalg.norm(b))


print(cosine([1.0, 2.0, 3.0], [1.5, -0.7, -20]))
print(cosine([-10.0, 17.0, 2.0], [5.3, 12.0, -20]))
print(cosine([1.0, 2.0, 3.0], [1, -3000, 184]))
print(cosine([1.0, 2.0, 3.0], [1, 2, 3]))<jupyter_output>-0.7977198918178166
0.23409628697705598
-0.48434715333575534
1.0<jupyter_text>**Oczekiwany rezultat:** -0.7977198918180.234096286977-0.4843471533361.0 Za pomocą CountVectorizera stwórzmy reprezentację BOW dwóch krótkich dokumentów i sprawdźmy z użyciem stworzonej funkcji jakie jest ich podobieństwo. Uruchom poniższy kod:<jupyter_code>from sklearn.feature_extraction.text import CountVectorizer
count_vect = CountVectorizer()

doc1 = "<NAME>"
doc2 = "<NAME> pięknego puszystego kota"

docs = [doc1, doc2]

X_train_counts = count_vect.fit_transform(docs).todense()

print("Dokumenty reprezentowane jako BOW. Dokumenty w wierszach, słowa w kolumnach")
print(X_train_counts)
print("\n\nPodobieńśtwo dokumentów to")
print(cosine(X_train_counts[0].tolist()[0], X_train_counts[1].tolist()[0])) # tolist()[0] zamienia macierz 1xn na listę elementów 1xn<jupyter_output>Dokumenty reprezentowane jako BOW. Dokumenty w wierszach, słowa w kolumnach
[[1 1 1 0 0]
 [1 1 1 1 1]]


Podobieńśtwo dokumentów to
0.7745966692414834<jupyter_text>Kiedy dokumenty zawierają te same słowa podobieństwo zostaje całkiem nieźle obliczone, co jednak, jeśli zamiast takich samych słów znajdziemy wyrazy bliskoznaczne? Sprawdźmy - uruchom poniższy kod:<jupyter_code>from sklearn.feature_extraction.text import CountVectorizer
count_vect = CountVectorizer()

doc1 = "kot"
doc2 = "kotek"
docs = [doc1, doc2]


print("Dokumenty reprezentowane jako BOW. Dokumenty w wierszach, słowa w kolumnach")
X_train_counts = count_vect.fit_transform(docs).todense()
print(X_train_counts)

print("\n\nPodobieńśtwo dokumentów to")
print(cosine(X_train_counts[0].tolist()[0], X_train_counts[1].tolist()[0]))<jupyter_output>Dokumenty reprezentowane jako BOW. Dokumenty w wierszach, słowa w kolumnach
[[1 0]
 [0 1]]


Podobieńśtwo dokumentów to
0.0<jupyter_text>Jak widzimy BOW nie radzi sobie z oceną podobieństwa synonimów. Dajmy zatem szansę embeddingom!---Embeddingi to nic innego jak osadzenie znaczenia słowa w n-wymiarowej przestrzeni tak, aby słowa podobne do siebie występowały blisko w tej n-wymiarowej przestrzeni. Możemy stworzyć je sami na podstawie dużego korpusu tekstu (co może być czasochłonne), z użyciem paczek takich jak: gensim (https://radimrehurek.com/gensim/) ale możemy również użyć "pretrenowanych" wektorów utworzonych już na jakimś korpusie, dostępnych np. pod adresem: (https://nlp.stanford.edu/projects/glove/). My wybierzemy opcję drugą - wykorzystania istniejących wektorów. Embeddingi dostarczone przez zespół Stanforda to pliki tekstowe postaci:słowo[SPACJA]wektor_liczb_oddzielonych_spacją_reprezentujących_znaczenie_slowaFunkcja ładująca embeddingi do pamięci została już stworzona. **Uruchom poniższy kod, aby móc wykrzystać tę funkcję w kolejnych komórkach i ocenić podobieństwo wyrazów bliskoznacznych.** Uwaga: zmienna mapping bedzie wykorzystywana w kolejnych komorkach, więc aby była dla nich widoczna - poniższa komórka musi być koniecznie uruchomiona.<jupyter_code>import numpy as np

def load_embeddings(path):
    mapping = dict()
    
    with open(path, 'r', encoding='utf8') as f:
        for line in f:
            line = line.strip()
            if len(line) == 0:
                continue
            splitted = line.split(" ")
            mapping[splitted[0]] = np.array(splitted[1:], dtype=float) # stwórz słownik słowo -> wektor 
    return mapping

mapping = load_embeddings('glove.6B.50d.txt') # ładujemy wektory o długości 50 liczb

kot = mapping['cat']
kotek = mapping['kitten']
krzeslo = mapping['chair']

print("Podobieńśtwo między kotem a kotkiem:")
print(cosine(kot, kotek))

print("Podobieńśtwo między kotem a krzesłem")
print(cosine(kot, krzeslo))<jupyter_output>Podobieńśtwo między kotem a kotkiem:
0.6386305647068641
Podobieńśtwo między kotem a krzesłem
0.29425297716624566<jupyter_text>Widzimy, że embeddingi, które tutaj są dość krótkie (50 liczb opisujących każde słowo) dobrze oddają podobieństwo. Kot jest dużo bardziej podobny do kotka niż do krzesła!  Zadanie 2: Embeddingi - wizualizacja i podobieństwoEmbeddingi są reprezentacją znaczenia słów w n-wymiarowej przestrzeni. Strona: http://projector.tensorflow.org próbuje zwizualizować tę przestrzeń poprzez rzutowanie pretrenowanych wektorów na przestrzeń 3-wymiarową.W celu wykonania zadania, otwórz powyższą stronę i wykonaj podpunkty a) i b)  **Zadanie 2a (0.5 punktu)**: Podaj 5 najbliższych słów do słowa "data" w domyślnie załadowanych embeddingach (Słowo, którego sąsiadów chcemy zlokalizować możemy wpisać w pole "Search" w górnej części po prawej stronie ekranu. Następnie, po wskazaniu słowa "data" ujrzymy listę najbardziej podobnych słów wraz z miarą podobieńtstwa. Listę najbardziej podobnych słów wraz z miarą odległości (wybierz cosinusową), zawrzyj w poniższym komentarzu.Uwaga - na stronie użyto odległości cosinusowej zamiast podobieństwa. Relacja pomiędzy obiema miarami jest bardzo prosta: odległość = 1 - podobieństwo<jupyter_code>data / word2vec 10k / pca

# 1. Słowo: information     Odległość: 0.443  Podobienstwo: 1 - 0.443 
# 2. Słowo: instructions    Odległość: 0.512  Podobienstwo: 1 - 0.512
# 3. Słowo: files           Odległość: 0.528  Podobienstwo: 1 - 0.528
# 4. Słowo: file            Odległość: 0.548  Podobienstwo: 1 - 0.548
# 5. Słowo: register        Odległość: 0.554  Podobienstwo: 1 - 0.554<jupyter_output><empty_output><jupyter_text>Jak widzimy, słowa, które są najbliższe słowu "data" to w tym przypadku wyrazy bliskoznaczne. **Zadanie 2b (0.5 punktu)**: Podaj 5 najbliższych słów do słowa "red". Czy nadal mamy do czynienia z synonimami?Odpowiedz na pytanie: jak można interpretować najbardziej podobne słowa(w jakim aspekcie są one podobne), skoro jak widać nie są to synonimy (przypomnij sobie zasadę działania embeddingów)? Odpowiedzi zawrzyj w komentarzach poniżej<jupyter_code>red / word2vec 10k / pca

# 1. Słowo: blue        Odległość: 0.335  Podobienstwo: 1 - 0.335
# 2. Słowo: yellow      Odległość: 0.382  Podobienstwo: 1 - 0.382
# 3. Słowo: white       Odległość: 0.395  Podobienstwo: 1 - 0.395
# 4. Słowo: green       Odległość: 0.398  Podobienstwo: 1 - 0.398
# 5. Słowo: black       Odległość: 0.494  Podobienstwo: 1 - 0.494

# Interpretacja najbliższych słów:
inne kolory (klasy), bo one stanowia jedna pokrewna grupe (cluster w PCA)
---
"skoro jak widać nie są to synonimy (przypomnij sobie zasadę działania embeddingów)?"
tutaj raczej nie chodzi o zasade tylko jak te 
embeddingi zostaly stworzone oraz w jakim celu.
te co tutaj mamy w cwiczeniach zostaly stworzone na zbiorach / przetrenowane
ogolnego uzytku (skip-gram/sliding window)
wiec np. dla zbioru/problemu dla Kaggle, slowo 'red' moze byc
nawet pokrewne z jakimis liczbami czy tez identyfikatorami (konkurs z lekami)<jupyter_output><empty_output><jupyter_text>Zadanie 3: Relacje między wektorami Embeddingi zawierają w sobie informacje o znaczeniu. Co więcej - są wektorami, przez co możemy wykonywać na nich operacje (dodawanie, odejmowanie,...). Sprawdźmy jakie efekty uzyskamy wykonując operacje na tych wektorach.Interesować nas będzie efekt operacji: $\vec{italy} - \vec{rome} + \vec{warsaw}$. Na co wskazywać będzie tak zdefiniowany wektor?Ponieważ wynikiem tej operacji będzie nowy wektor, napiszmy funkcję, która sprawdzi jakie istniejące słowo leży najbliżej tego wektora.**Zadanie 3:** Wypełnij funkcję get_most_similar() tak, aby dla zadanego wektora vec1, zwróciła słowo, którego wektor jest najbardziej podobny do wektora vec1 (do oceny podobieńśtwa wykorzystaj stworzoną w zad. 1 funkcję cosine). Parametrem embeddings jest słownik, który mapuje słowo na odpowiadający mu wektor.Jakie słowo zostało wyznaczone jako najbliższe do obliczonego punktu?<jupyter_code>from tqdm import tqdm

def get_most_similar(vec1, embeddings):
    max_key, max_val = ";-)", 0
    # mozna tu wkleic numbe to by blyskiem poszlo
    for key in tqdm(embeddings):
        cur_val = cosine(vec1, embeddings[key])
        if cur_val > max_val:
            max_key, max_val = key, cur_val
    return max_key

print(len(mapping.keys()))

new_point = mapping['italy'] - mapping['rome'] + mapping['warsaw']
print(get_most_similar(new_point, mapping))<jupyter_output>1%|          | 2785/400000 [00:00<00:14, 27845.78it/s]<jupyter_text>Widzimy zatem, że wykonywanie operacji na embeddingach pozwala na uzyskanie bardzo ciekawych rezulatów. Jeśli od obiektu "Włochy" odejmiemy jego stolicę, a dodamy stolicę polski, to otrzymamy obiekt "Polska". Innymi słowy - odpowiadamy na pytanie: jakie słowo jest w takiej relacji do Polski, w jakiej jest Rzym do Włoch.  Zadanie 4: KlasyfikacjaOkazuje się również, że embeddingi użyteczne są w klasyfikacji, skutecznie redukując nam liczbę cech a także rozwiązując problem rzadkiej reprezentacji. Wróćmy do zadania klasyfikacji spamu znanego z jednych z poprzenich laboratoriów. W celu decydowania czy mamy do czynienia ze spamem, czy może hamem, chcielibyśmy użyć klasyfikatora SVC, który jako cechy przyjmie embeddingi. Ponieważ jednak embeddingi opisują pojedyncze słowa jako n-wymiarowe wektory, a w problemie klasyfikacji maili musimy reprezentować całe dokumenty w postaci wektorów - musimy zagregować informacje o wszystkich słowach w jednym wektorze cech.Jedną z zadziwiająco dobrze działających metod okazuje się być reprezentowanie całego dokumentu jako wektora, będącego "środkiem ciężkości" słów z których jest zbudowany. Wektor wynikowy jest wektorem n-wymiarowym (tak jak wektor każdego ze słów "składowych"), gdzie $i$-ta pozycja wektora posiada wartość będącą średnią arytmetyczną $i$-tych pozycji wektorów słów z danego dokumentu. UWAGA: może okazać się, że w pretrenowanych embeddingach słowo z dokumentu, który chcemy reprezentować jako wektor nie występuje. W takiej syturacji zignorujmy kompletnie to słowo (uznajmy, że go nie ma).**Zadanie 4 (1 punkt)**: Zaimplementuj funkcję documents_to_ave_embeddings(docs, embeddings) [ linia 21 ] przyjmującej dwa parametry:     docs - lista dokumentów w formie tekstowej (lista stringów) do przetransformowania na wektory    embeddings - mapowanie słowo -> wektor (embedding) z istniejącego modeluFunkcja powinna zwrócić jedną zmienną, listę wektorów dokumentów, gdzie 1 dokument jest "średnim wektorem" wektorów słów z których jest zbudowany (jak w paragrafie powyżej treści zadania). Użyj tokenizatora z NLTK (word_tokenize) i przed stokenizowaniem - zamień teksty dokumentów na składające się z samych małych liter.Rozważ użycie numpy.mean z odpowiednią wartością parametru axis do policzenia średniej (będzie łatwiej użyć gotowej funkcji niż ręcznie implementować).<jupyter_code>from sklearn.pipeline import Pipeline
from sklearn.svm import SVC
from sklearn.feature_extraction.text import TfidfVectorizer
import pandas
import numpy as np
from nltk import word_tokenize
from sklearn.metrics import classification_report

# ------------------- WCZYTANIE DANYCH -----------

full_dataset = pandas.read_csv('spam_emails.csv', encoding='utf-8')      # wczytaj dane z pliku CSV
full_dataset['label_num'] = full_dataset.label.map({'ham':0, 'spam':1})  # ponieważ nazwy kategorii zapisane są z użyciem stringów: "ham"/"spam", wykonujemy mapowanie tych wartości na liczby, aby móc wykonać klasyfikację. 

np.random.seed(0)                                       # ustaw seed na 0, aby zapewnić powtarzalność eksperymentu
train_indices = np.random.rand(len(full_dataset)) < 0.7 # wylosuj 70% wierszy, które znajdą się w zbiorze treningowym

train = full_dataset[train_indices] # wybierz zbior treningowy (70%)
test = full_dataset[~train_indices] # wybierz zbiór testowy (dopełnienie treningowego - 30%)

from tqdm import tqdm
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

# FIXME: szkoda ze gorszy wynik niz dla tf-idf

def documents_to_ave_embeddings(docs, embeddings):
    result = []; errno_g = []
    print("--->", len(docs), len(embeddings))
    for doc in tqdm(docs):
        vecs = []
        errno = 0
            
        word_list = word_tokenize(doc.lower())
        #filtered_words = [word for word in word_list if word \
        #                  not in stopwords.words('english')]
        
        for word in word_list:
            if word in embeddings:
                vecs.append(embeddings[word])
            else:
                errno += 1
    
        if len(vecs) == 0:
            vecs = [embeddings['none']]
    
        mean_vecs = np.mean(vecs,axis=0)
        errno_g.append(round(errno/len(doc),3))
        # print(mean_vecs.shape, errno, len(doc), round(errno/len(doc),2))
        result.append(mean_vecs)
    result = np.array(result)
    print("error: ", np.mean(errno_g))
    return result
        

# ------------------- STWORZENIE PIPELINE'U -----------
 
classifier = SVC(C=1.0)

train_transformed = documents_to_ave_embeddings(train['text'], mapping)
test_transformed = documents_to_ave_embeddings(test['text'], mapping)

# ------------------- TRANSFORMACJA I UCZENIE -----------

classifier.fit(train_transformed, train['label_num']) # zwektoryzujmy dane i wytrenujmy klasyfikator na zbiorze treningowym

# ------------------- OCENA KLASYFIKATORA -----------
accuracy = classifier.score(test_transformed, test['label_num'])
print("W zbiorze testowym {n}% przypadków zostało poprawnie zaklasyfikowanych!".format(
    n=100.*accuracy))
print(classification_report(test['label_num'], classifier.predict(test_transformed))) # testowanie klasyfikatora - szerokie podsumowanie uwzględniające miary: precision, recall, f1<jupyter_output>1%|          | 15/1624 [00:00<00:10, 147.67it/s]<jupyter_text>Zadanie 5:  Odległość Levensteina (poprawianie literówek)Co prawda odległość Levensteina nie jest związana z koncepcją embeddingów, jednak do tej pory nie próbowaliśmy zastosować tej metody w praktyce. Ostatnie zadanie tych laboratoriów będzie dotyczyło właśnie odległości edycyjnej. **Zadanie 5 (1 punkt)**: Zaimplementuj funkcję get_closest_word która poprawi literówki z użyciem odległości Levensteina. Jeśli aktualny token występuje w słowniku (words_set) - nie powinien zostać korygowany, jeśli zaś nie występuje - powinien być zamieniony na słowo, którego odległość Levensteina jest najmniejsza. Czy udało się poprawić ten zaszumiony tekst?  Wykorzystaj funkcję edit_distance z nltk.<jupyter_code># Dystans levensteina
from nltk.metrics.distance import edit_distance
from nltk import word_tokenize
from timeit import default_timer as timer              # funkcja potrzebna do mierzenia czasu

input_text = "dzięń <NAME>, <NAME>ż<NAME>dą w sali omputerowej 1.6.10. pozdrawiem"

with open('slowa_min.txt', 'r', encoding='utf8') as f:
    valid_words = set([w.strip() for w in f.read().split("\n") if w.strip() != '']) # zbiór słów, które są poprawnymi

def get_closest_word(token, valid_words):
    _key, _val = ";-)", +66666666666
    # mozna tu wkleic numbe to by blyskiem poszlo
    for word in tqdm(valid_words):
        cur_val = edit_distance(word, token)
        if cur_val < _val:
            _key, _val = word, _val
    return _key     

tokenized_input = word_tokenize(input_text) # tokenizuj
start = timer()  # pobierz czas
corrected = []   # lista poprawionych tokenów
for token in tokenized_input:
    if not token.isalpha() or token in valid_words: # jeśli token nie jest słowem lub jest już w słowniku - nie poprawiaj
        corrected.append(token)
    else: # popraw
        corrected.append(get_closest_word(token, valid_words))
end = timer()


print("Tekst oryginalny: {t}".format(t=input_text))
print("Tekst poprawiony: {t}".format(t=" ".join(corrected)))
print("Czas poprawiania: {t}.".format(t=end-start))<jupyter_output>100%|██████████| 5007/5007 [00:00<00:00, 14562.77it/s]
100%|██████████| 5007/5007 [00:00<00:00, 10735.01it/s]
100%|██████████| 5007/5007 [00:00<00:00, 8377.81it/s]
100%|██████████| 5007/5007 [00:00<00:00, 6892.18it/s]
100%|██████████| 5007/5007 [00:00<00:00, 18888.20it/s]
100%|██████████| 5007/5007 [00:00<00:00, 8477.19it/s]
100%|██████████| 5007/5007 [00:00<00:00, 9080.29it/s]
{'fraction_non_alphanumeric': 0.09535247458384538, 'fraction_numerical': 0.036755669757568984, 'mean_word_length': 4.608709273182957, 'pattern_counts': {'":': 0, '<': 47, '<?xml version=': 0, '>': 40, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '579905', 'license': 'MIT', 'n_tokens_mistral': 7532, 'n_tokens_neox': 7430, 'n_words': 1873, 'path': 'pjn/lab4/Zadania__lab4.ipynb', 'repo_name': 'maciejczyzewski/sem6', 'size': '17940'}