Download evorm_core/entity_embedding.py from eduzrh/EvoRM: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/eduzrh/EvoRM/resolve/main/evorm_core/entity_embedding.py
- Command line
-
hf download hf://eduzrh/EvoRM/evorm_core/entity_embedding.py
-
curl -L -o entity_embedding.py https://huggingface.co/eduzrh/EvoRM/resolve/main/evorm_core/entity_embedding.py
10.1 kB
| #!/usr/bin/env python3 | |
| """ | |
| EvoRM Entity Embedding Module | |
| ============================== | |
| Paper Section III-D: Entity embeddings (demb=1024) for computing neural | |
| cosine similarity between entity pairs within hyperedges. | |
| The hyperedge weight wk combines: | |
| wk = α * max(Conf(R)) + β * avg_cos_sim(Vk) [Eq. 6] | |
| Where avg_cos_sim is the average cosine similarity between entity embeddings | |
| of all entity pairs in the hyperedge. | |
| Implementation: | |
| Uses feature hashing (the "hashing trick") to produce deterministic 1024-dim | |
| embeddings from entity attributes, neighbors, and descriptions. This is a | |
| lightweight, API-free approach that captures the structural information of | |
| entities without requiring external embedding services. | |
| """ | |
| import hashlib | |
| import math | |
| import re | |
| from typing import Dict, List, Set, Optional, Tuple | |
| import numpy as np | |
| class EntityEmbedder: | |
| """ | |
| Compute 1024-dimensional entity embeddings using feature hashing. | |
| Usage: | |
| embedder = EntityEmbedder(demb=1024) | |
| emb = embedder.embed_entity(entity_context) | |
| sim = embedder.cosine_similarity(emb1, emb2) | |
| """ | |
| def __init__(self, demb: int = 1024, n_features: int = 8192): | |
| """ | |
| Args: | |
| demb: Output embedding dimension (paper: 1024) | |
| n_features: Number of hash features for the hashing trick | |
| """ | |
| self.demb = demb | |
| self.n_features = n_features | |
| self._cache: Dict[str, np.ndarray] = {} | |
| def _serialize_entity(self, context: Dict) -> str: | |
| """Serialize entity context into a canonical string for hashing.""" | |
| parts = [] | |
| # Sort keys for deterministic output | |
| for key in sorted(context.keys()): | |
| val = context[key] | |
| if key.startswith('neighbors_'): | |
| rel = key.replace('neighbors_', '') | |
| if isinstance(val, (set, list)): | |
| items = sorted(str(v) for v in val) | |
| for item in items: | |
| parts.append(f"n:{rel}:{item}") | |
| else: | |
| parts.append(f"n:{rel}:{val}") | |
| elif key == 'entity_name': | |
| parts.append(f"name:{val}") | |
| elif key == 'description': | |
| if val: | |
| # Tokenize and add individual tokens | |
| tokens = re.findall(r'\w+', str(val).lower()) | |
| for tok in tokens: | |
| parts.append(f"desc:{tok}") | |
| else: | |
| parts.append(f"attr:{key}:{val}") | |
| return '|'.join(parts) | |
| def _feature_hash(self, text: str) -> np.ndarray: | |
| """ | |
| Feature hashing: map text features to a fixed-size vector. | |
| Uses multiple hash functions to create a dense representation | |
| from sparse text features. | |
| """ | |
| vec = np.zeros(self.n_features, dtype=np.float32) | |
| # Extract features (character n-grams + word tokens) | |
| features = [] | |
| # Word-level tokens | |
| tokens = re.findall(r'\w+', text.lower()) | |
| features.extend(tokens) | |
| # Character trigrams | |
| for i in range(len(text) - 2): | |
| features.append(f"c3:{text[i:i+3]}") | |
| # Bigrams of tokens | |
| for i in range(len(tokens) - 1): | |
| features.append(f"b2:{tokens[i]}_{tokens[i+1]}") | |
| # Hash each feature to a position in the vector | |
| for feat in features: | |
| h = hashlib.md5(feat.encode('utf-8')).hexdigest() | |
| # Use first 8 hex chars as two 4-hex indices | |
| idx1 = int(h[:4], 16) % self.n_features | |
| idx2 = int(h[4:8], 16) % self.n_features | |
| # Add +1 at idx1, -1 at idx2 (signed hashing) | |
| vec[idx1] += 1.0 | |
| vec[idx2] -= 1.0 | |
| # Normalize | |
| norm = np.linalg.norm(vec) | |
| if norm > 0: | |
| vec /= norm | |
| # Project to target dimension using random projection | |
| if self.n_features != self.demb: | |
| # Use deterministic "random" projection based on feature hash | |
| proj = np.zeros((self.demb, self.n_features), dtype=np.float32) | |
| for i in range(self.demb): | |
| seed = hashlib.md5(f"proj_{i}".encode()).hexdigest() | |
| np.random.seed(int(seed[:8], 16)) | |
| proj[i] = np.random.randn(self.n_features) / math.sqrt(self.demb) | |
| vec = proj @ vec | |
| norm = np.linalg.norm(vec) | |
| if norm > 0: | |
| vec /= norm | |
| return vec | |
| def embed_entity(self, context: Dict, entity_id: str = None) -> np.ndarray: | |
| """ | |
| Compute entity embedding from context. | |
| Args: | |
| context: Entity context dict (attributes, neighbors, descriptions) | |
| entity_id: Optional entity ID for caching | |
| Returns: | |
| 1024-dim normalized embedding vector | |
| """ | |
| # Check cache | |
| cache_key = entity_id if entity_id else self._serialize_entity(context) | |
| if cache_key in self._cache: | |
| return self._cache[cache_key] | |
| # Serialize and hash | |
| text = self._serialize_entity(context) | |
| emb = self._feature_hash(text) | |
| # Cache | |
| if len(self._cache) < 100000: # Limit cache size | |
| self._cache[cache_key] = emb | |
| return emb | |
| def embed_batch(self, contexts: List[Dict], entity_ids: List[str] = None) -> np.ndarray: | |
| """Compute embeddings for a batch of entities.""" | |
| entity_ids = entity_ids or [None] * len(contexts) | |
| embeddings = [] | |
| for ctx, eid in zip(contexts, entity_ids): | |
| embeddings.append(self.embed_entity(ctx, eid)) | |
| return np.stack(embeddings) | |
| def cosine_similarity(self, emb1: np.ndarray, emb2: np.ndarray) -> float: | |
| """Compute cosine similarity between two embeddings.""" | |
| dot = np.dot(emb1, emb2) | |
| return float(max(0.0, min(1.0, dot))) # Already normalized, clamp for safety | |
| def pairwise_similarities(self, embeddings: List[np.ndarray]) -> List[float]: | |
| """ | |
| Compute pairwise cosine similarities for all pairs in a list. | |
| Args: | |
| embeddings: List of entity embeddings | |
| Returns: | |
| List of cosine similarities for all unique pairs | |
| """ | |
| n = len(embeddings) | |
| if n < 2: | |
| return [] | |
| sims = [] | |
| for i in range(n): | |
| for j in range(i + 1, n): | |
| sims.append(self.cosine_similarity(embeddings[i], embeddings[j])) | |
| return sims | |
| def avg_cosine_similarity(self, embeddings: List[np.ndarray]) -> float: | |
| """ | |
| Compute average pairwise cosine similarity. | |
| This is the avg_cos_sim(Vk) term in Eq. 6 of the paper. | |
| Args: | |
| embeddings: List of entity embeddings in the hyperedge | |
| Returns: | |
| Average cosine similarity (0.0 if < 2 embeddings) | |
| """ | |
| sims = self.pairwise_similarities(embeddings) | |
| if not sims: | |
| return 0.0 | |
| return float(np.mean(sims)) | |
| def clear_cache(self): | |
| """Clear the embedding cache.""" | |
| self._cache.clear() | |
| def get_stats(self) -> Dict: | |
| return { | |
| 'cache_size': len(self._cache), | |
| 'demb': self.demb, | |
| 'n_features': self.n_features, | |
| } | |
| # ============================================================================== | |
| # Test / Demo | |
| # ============================================================================== | |
| if __name__ == "__main__": | |
| print("EvoRM Entity Embedding - Self Test") | |
| print("=" * 60) | |
| embedder = EntityEmbedder(demb=1024, n_features=8192) | |
| # Test entities | |
| ctx1 = { | |
| 'entity_name': 'Machine Learning Basics', | |
| 'title': 'Machine Learning Basics', | |
| 'year': '2020', | |
| 'authors': 'Smith et al.', | |
| 'neighbors_cites': {'Deep Learning Review', 'NN Basics'}, | |
| } | |
| ctx2 = { | |
| 'entity_name': 'Machine Learning Fundamentals', | |
| 'title': 'Machine Learning Fundamentals', | |
| 'year': '2020', | |
| 'authors': 'Smith and Jones', | |
| 'neighbors_cites': {'Deep Learning Review'}, | |
| } | |
| ctx3 = { | |
| 'entity_name': 'Quantum Physics Textbook', | |
| 'title': 'Quantum Physics Textbook', | |
| 'year': '2015', | |
| 'authors': 'Einstein et al.', | |
| 'neighbors_cites': {'Relativity Theory'}, | |
| } | |
| # Compute embeddings | |
| emb1 = embedder.embed_entity(ctx1, 'e1') | |
| emb2 = embedder.embed_entity(ctx2, 'e2') | |
| emb3 = embedder.embed_entity(ctx3, 'e3') | |
| print(f"\n1. Embedding dimensions: {emb1.shape}") | |
| print(f" Norms: {np.linalg.norm(emb1):.4f}, {np.linalg.norm(emb2):.4f}, {np.linalg.norm(emb3):.4f}") | |
| # Similarities | |
| sim_12 = embedder.cosine_similarity(emb1, emb2) | |
| sim_13 = embedder.cosine_similarity(emb1, emb3) | |
| sim_23 = embedder.cosine_similarity(emb2, emb3) | |
| print(f"\n2. Cosine similarities:") | |
| print(f" sim(E1, E2) = {sim_12:.4f} (similar ML books)") | |
| print(f" sim(E1, E3) = {sim_13:.4f} (ML vs Quantum)") | |
| print(f" sim(E2, E3) = {sim_23:.4f} (ML vs Quantum)") | |
| # Check that similar entities have higher similarity | |
| assert sim_12 > sim_13, f"Expected sim(E1,E2) > sim(E1,E3), got {sim_12:.4f} <= {sim_13:.4f}" | |
| assert sim_12 > sim_23, f"Expected sim(E1,E2) > sim(E2,E3), got {sim_12:.4f} <= {sim_23:.4f}" | |
| print(" ✅ Similar entities have higher similarity than dissimilar ones") | |
| # Average pairwise similarity | |
| avg_sim = embedder.avg_cosine_similarity([emb1, emb2, emb3]) | |
| print(f"\n3. avg_cos_sim(Vk) for 3 entities: {avg_sim:.4f}") | |
| # Cache test | |
| emb1_cached = embedder.embed_entity(ctx1, 'e1') | |
| assert np.array_equal(emb1, emb1_cached), "Cache test failed" | |
| print(f"\n4. Cache: {embedder.get_stats()}") | |
| print("\n✅ All tests passed!") | |