====== 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).")