====== Laboratorium 12 + 13: grupowanie dokumentów tekstowych ====== Na laboratorium przeprowadzimy grupowanie dokumentów tekstowych z użyciem 3 metod: * k-means * EM * DBSCAN (i jego wariantu HDBSCAN) **Narzędzia:** * Docker i docker-compose - do budowy środowiska z zainstalowanymi pakietami i udostępniania ''jupyter lab'' * [[https://duckdb.org/|duckdb]] - do zapisu danych i wykonania kwerend SQL * [[|https://spacy.io/|spaCy]] - do wektoryzacji i tokenizacji tekstów **Etapy:** - Pobrane zostaną teksty 10000 artykułów z Wikipedii - Zbudowane zostaną ich reprezentacji wektorowe (wektory osadzeń i wektory TF-IDF) - Zastosujemy UMAP do redukcji wymiarów wektorów na potrzeby klasteryzacji i wizualizacji - Do dokumentów zostaną dodane etykiety klasteryzacji dla różnych algorytmów - Porównamy ich wyniki ===== Budowa obrazu dockera ===== Pobierz i rozpakuj {{ :ed:clustering-jupyter.zip |archiwum clustering-jupyter.zip}} Znajduje się tam plik ''pyproject.toml'' ze specyfikacją pakietów do zainstalowania [project] name = "clustering-nlp" version = "0.1.0" description = "Projekt na zajęcia: Eksploracja danych, 2026" readme = "README.md" requires-python = ">=3.12" dependencies = [ "pandas", "tabulate", "datasets", # Pobieranie Wikipedii z Hugging Face "spacy", # NLP i embeddingi statyczne "scikit-learn", # K-means, GMM, DBSCAN "hdbscan", # Warian DBSCAN "umap-learn", # Redukcja wymiarów "plotly", # Interaktywne wykresy "nbformat", "tqdm", # Paski postępu "matplotlib", "seaborn", "wordcloud", "jupyterlab", "ipywidgets", "jupysql", # SQL w notatniku "duckdb", # Baza OLAP "duckdb-engine", "ollama", # Opcjonalnie: biblioteka do komunikacji z lokalnym LLM "pyarrow", "fastparquet", ] Plik ''Dockerfile'' FROM python:3.12-slim RUN apt-get update && apt-get install -y \ curl \ build-essential \ # pandoc \ && rm -rf /var/lib/apt/lists/* # Instalacja uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ WORKDIR /app COPY pyproject.toml . # Instalacja zależności przez uv RUN uv sync # Pobranie modelu spaCy RUN uv run python -m spacy download pl_core_news_lg WORKDIR /app/notebooks oraz plik ''docker-compose.yaml'' version: '3.8' services: nlp-jupyter: build: . image: nlp-jupyter container_name: eksploracja_danych_nlp ports: - "9888:8888" volumes: - ./notebooks:/app/notebooks - ./hf_cache:/root/.cache/huggingface shm_size: '4gb' deploy: resources: limits: memory: 12gb command: > uv run jupyter lab --ip=0.0.0.0 --allow-root --no-browser --IdentityProvider.token='nlp' --notebook-dir=/app/notebooks **Uruchomienie** * aby zbudować obraz wydaj komendę ''docker compose build'' * aby uruchomić kontener wydaj komendę ''docker compose up'' * Po uruchomieniu otwórz link [[http://localhost:9888/lab?token=nlp]] * Defaultowym katalogiem dla jupyetera lab jest ''notebooks''. Znajduje się tam notatnik ''wikipedia.ipynb'', w którym można kontynuować implementację kodu **Uwagi**: Jako menadżer pakietów stosowany jest ''uv''. Aby dodać pakiet należy w notatniku uruchomić komendę ''!uv add package'' ===== 1. Ładowanie danych tekstowych ===== Kod, który pobiera 10 000 artykułów z Hugging Face znajduje się w notatniku ''wikipedia.ipynb''. Kluczowym elementem jest losowanie wartości ''OFFSET'' - numeru dokumentu od którego zostanie ropoczęte pobieranie. Celem jest zróżnicowanie wyników i wizualizacji. Zapisz korpus po załadowaniu dokumentów. ===== 2. SpaCy i wektory osadzeń (embeddingi) ===== Spacy oferuje [[https://spacy.io/usage/processing-pipelines| ciągi przetwarzania dostosowane do konkretnego języka]]. Użyjemy modelu o dużej dokładności ''pl_core_news_lg'' import spacy nlp = spacy.load("pl_core_news_lg") doc = nlp('Ala ma kota') print(doc.vector.shape) for t in doc: print(t,t.vector.shape) W wyniku przetwarzania dokumentu dzielony jets on na tokeny, każdy token ma przypisane rózne atrybuty, w tym wektor osadzeń (o długości 300 dla wybranego wcześniej modelu). Wektory osadzeń tokenów po zsumowaniu dają wektor dokumentu. Wynik: (300,) Ala (300,) ma (300,) kota (300,) ==== Dodawanie wektorów osadzeń ==== Wyznaczanie wektorów osadzeń za pomocą spaCy jest czasochłonną operacją. SpaCy jest zainstalowane bez wsparcia dla GPU. Zarejestrowane czasy to: * 8 min z wydajnością 20 dokumentów/sekundę [wariant optymistyczny] * 23 minuty wydajnością 7.3 dokumentów/sekundę [wariant średni na laptopie z procesorem i7] * 47 minut z wydajnością 3.5 dokumentów/sekundę [wariant pesymistyczny] Użyj poniższego kodu: import numpy as np from tqdm import tqdm processed_data = [] texts=df.text.to_list() titles=df.title.to_list() with nlp.select_pipes(enable=["tok2vec", "lemmatizer", "attribute_ruler"]): # pipe() jest wydajniejszy niż nlp() w pętli for doc in tqdm(nlp.pipe(texts, batch_size=20), total=len(texts)): valid_vectors = [t.vector for t in doc if not t.is_stop and t.is_alpha] if valid_vectors: # NumPy jest zoptymalizowany pod C, to będzie szybkie doc_vector = np.mean(valid_vectors, axis=0) else: doc_vector = np.zeros(nlp.vocab.vectors_length) processed_data.append({ "title": titles[len(processed_data)], "text": texts[len(processed_data)], "tokens": [t.lemma_.lower() for t in doc if not t.is_stop and t.is_alpha], "vector": doc_vector }) * W celu przyspieszenia wykonywane są tylko części //pipeline//, np. nie jest budowany gram syntaktyczny zdania * Obliczane są embeddingi dla dokumentów z pominięciem //stop words// i tokenów zawierających liczby **Zapisz wyniki** Po zakończeniu przetwarzania koniecznie zapisz wyniki. SpaCy zwraca wektory numpy. Należy je przekonwertować do postaci list, ponieważ ułatwi to późniejsze przetwarzanie. df_vec = pd.DataFrame(processed_data) df_vec['vector'] = df_vec['vector'].apply(lambda x: x.tolist() if isinstance(x, np.ndarray) else x) # Na przykład zapis do Parquet df_vec.to_parquet('wiki_vect.parquet', index=False) print("Zapisano do Parquet (wektory jako listy).")