Text_Extracted / src /embedder.py
Goutam112's picture
new-features are adding
d52dd8c
Raw History Blame Contribute Delete
6.74 kB
"""
embedder.py β€” Semantic Embedding Vector Generator & Visualizer.
Features:
- Generates per-word and per-sentence embeddings using sentence-transformers
- PCA / t-SNE dimensionality reduction for 2D visualization
- UMAP reduction for cluster visualization (if umap-learn installed)
- Cosine semantic similarity heatmap between sentences
- Word cluster labeling
"""
import numpy as np
import streamlit as st
from typing import List, Dict, Any, Optional, Tuple
@st.cache_resource(show_spinner="πŸ”’ Loading embedding model (one-time)...")
def get_embedding_model(model_name: str = "all-MiniLM-L6-v2"):
"""Load and cache the sentence-transformer embedding model."""
from sentence_transformers import SentenceTransformer
return SentenceTransformer(model_name)
def embed_texts(
texts: List[str],
model_name: str = "all-MiniLM-L6-v2",
batch_size: int = 32
) -> np.ndarray:
"""
Embed a list of texts into dense vectors.
Returns ndarray of shape (N, embedding_dim).
"""
if not texts:
return np.array([])
model = get_embedding_model(model_name)
embeddings = model.encode(
texts,
batch_size=batch_size,
show_progress_bar=False,
normalize_embeddings=True,
)
return embeddings
def reduce_pca(embeddings: np.ndarray, n_components: int = 2) -> np.ndarray:
"""Reduce embedding dimensions using PCA."""
from sklearn.decomposition import PCA
if len(embeddings) < n_components:
return embeddings
pca = PCA(n_components=n_components, random_state=42)
return pca.fit_transform(embeddings)
def reduce_tsne(embeddings: np.ndarray, n_components: int = 2, perplexity: int = None) -> np.ndarray:
"""Reduce embedding dimensions using t-SNE."""
from sklearn.manifold import TSNE
n = len(embeddings)
if n < 4:
return reduce_pca(embeddings, n_components)
perp = min(perplexity or 30, n - 1)
tsne = TSNE(
n_components=n_components,
perplexity=perp,
random_state=42,
n_iter=500,
learning_rate="auto",
init="pca"
)
return tsne.fit_transform(embeddings)
def reduce_umap(embeddings: np.ndarray, n_components: int = 2) -> Optional[np.ndarray]:
"""Reduce embedding dimensions using UMAP (if available)."""
try:
import umap
reducer = umap.UMAP(n_components=n_components, random_state=42)
return reducer.fit_transform(embeddings)
except ImportError:
return None
def cosine_similarity_matrix(embeddings: np.ndarray) -> np.ndarray:
"""Compute pairwise cosine similarity matrix (assumes normalized embeddings)."""
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
normalized = embeddings / np.clip(norms, 1e-8, None)
return normalized @ normalized.T
def cluster_words(embeddings: np.ndarray, n_clusters: int = 5) -> np.ndarray:
"""K-Means cluster the embeddings, return cluster labels."""
from sklearn.cluster import KMeans
n = len(embeddings)
k = min(n_clusters, max(1, n // 3))
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
return kmeans.fit_predict(embeddings)
def build_embedding_data(
ocr_results: List[Dict[str, Any]],
text_unit: str = "word", # "word" | "sentence" | "line"
reduction: str = "PCA", # "PCA" | "t-SNE" | "UMAP"
n_clusters: int = 5,
) -> Dict[str, Any]:
"""
Full embedding pipeline:
1. Extract text units from OCR results
2. Generate embeddings
3. Reduce to 2D
4. Cluster
5. Return structured data for plotting
Returns dict with keys:
texts, embeddings, coords_2d, cluster_labels, similarity_matrix
"""
if not ocr_results:
return {}
# ── Step 1: Extract text units ──
if text_unit == "word":
texts = [r["text"] for r in ocr_results if r["text"].strip()]
elif text_unit == "sentence":
full = " ".join(r["text"] for r in ocr_results)
# Simple sentence split
import re
sentences = re.split(r"(?<=[.!?])\s+", full)
texts = [s.strip() for s in sentences if len(s.strip()) > 3]
else: # line
# Group words into lines by y-proximity
sorted_results = sorted(ocr_results, key=lambda r: r["y_min"])
lines = []
current_line_y = None
current_line_texts = []
for r in sorted_results:
if current_line_y is None or abs(r["y_min"] - current_line_y) < 20:
current_line_texts.append(r["text"])
current_line_y = r["y_min"]
else:
if current_line_texts:
lines.append(" ".join(current_line_texts))
current_line_texts = [r["text"]]
current_line_y = r["y_min"]
if current_line_texts:
lines.append(" ".join(current_line_texts))
texts = [l for l in lines if l.strip()]
if not texts:
return {}
# ── Step 2: Embed ──
embeddings = embed_texts(texts)
if embeddings.size == 0:
return {}
# ── Step 3: Reduce to 2D ──
if reduction == "t-SNE" and len(texts) >= 4:
coords_2d = reduce_tsne(embeddings)
elif reduction == "UMAP":
coords_2d = reduce_umap(embeddings)
if coords_2d is None:
coords_2d = reduce_pca(embeddings)
else:
coords_2d = reduce_pca(embeddings)
# ── Step 4: Cluster ──
if len(texts) >= 2:
cluster_labels = cluster_words(embeddings, n_clusters)
else:
cluster_labels = np.zeros(len(texts), dtype=int)
# ── Step 5: Similarity matrix ──
if len(texts) <= 60:
sim_matrix = cosine_similarity_matrix(embeddings)
else:
sim_matrix = None
# ── Step 6: Top-N similar pairs ──
similar_pairs = []
if sim_matrix is not None and len(texts) > 1:
for i in range(len(texts)):
for j in range(i + 1, len(texts)):
similar_pairs.append({
"i": i,
"j": j,
"text_i": texts[i],
"text_j": texts[j],
"similarity": float(sim_matrix[i, j]),
})
similar_pairs.sort(key=lambda x: x["similarity"], reverse=True)
similar_pairs = similar_pairs[:10]
return {
"texts": texts,
"embeddings": embeddings,
"coords_2d": coords_2d,
"cluster_labels": cluster_labels.tolist(),
"similarity_matrix": sim_matrix,
"similar_pairs": similar_pairs,
"embedding_dim": embeddings.shape[1] if len(embeddings.shape) > 1 else 0,
"n_texts": len(texts),
"reduction_method": reduction,
}