Spaces:
Runtime error
Runtime error
Add semantic cleanup, canonicalization, and generic-term downweighting
Browse filesFilter obvious footer/legal CTA noise before graph build, canonicalize key term variants, and apply IDF-style specificity plus generic-domain penalties to improve important term ranking toward TextAnalyst-like behavior.
Made-with: Cursor
- semantic_graph.py +71 -5
semantic_graph.py
CHANGED
|
@@ -27,6 +27,55 @@ def _extract_significant_lemmas(sent: Dict[str, Any]) -> List[str]:
|
|
| 27 |
return lemmas
|
| 28 |
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
def _extract_phrase_candidates(sentence_text: str, lang: str) -> List[str]:
|
| 31 |
"""
|
| 32 |
Извлекает фразовые кандидаты через существующую n-gram логику проекта,
|
|
@@ -74,15 +123,23 @@ def build_semantic_graph(
|
|
| 74 |
pair_cooc = defaultdict(int)
|
| 75 |
phrase_occ = Counter()
|
| 76 |
phrase_sent_ids = defaultdict(set)
|
|
|
|
| 77 |
sentence_words: List[List[str]] = []
|
| 78 |
sentence_phrases: List[List[str]] = []
|
| 79 |
|
| 80 |
for sent_id, sent in enumerate(sentences_data):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
lemmas_raw = _extract_significant_lemmas(sent)
|
| 82 |
-
lemmas = _normalize_lemma_sequence(lemmas_raw)
|
|
|
|
| 83 |
sentence_words.append(lemmas)
|
| 84 |
|
| 85 |
-
phrase_candidates = _extract_phrase_candidates(
|
| 86 |
# Фильтр мусора фраз: минимум 2 слова, без дублирующегося подряд слова.
|
| 87 |
clean_phrases = []
|
| 88 |
for p in phrase_candidates:
|
|
@@ -92,7 +149,7 @@ def build_semantic_graph(
|
|
| 92 |
bad_repeat = any(parts[i] == parts[i + 1] for i in range(len(parts) - 1))
|
| 93 |
if bad_repeat:
|
| 94 |
continue
|
| 95 |
-
clean_phrases.append(" ".join(parts))
|
| 96 |
sentence_phrases.append(clean_phrases)
|
| 97 |
|
| 98 |
if not lemmas:
|
|
@@ -115,6 +172,8 @@ def build_semantic_graph(
|
|
| 115 |
neighbors.add(b)
|
| 116 |
for b in neighbors:
|
| 117 |
pair_cooc[(a, b)] += 1
|
|
|
|
|
|
|
| 118 |
|
| 119 |
# Частоты/охват фраз по предложениям.
|
| 120 |
for sent_id, phrases in enumerate(sentence_phrases):
|
|
@@ -140,6 +199,7 @@ def build_semantic_graph(
|
|
| 140 |
|
| 141 |
for phrase in uniq_phrases:
|
| 142 |
term_occ[phrase] += 1
|
|
|
|
| 143 |
parts = set(phrase.split())
|
| 144 |
# Связываем фразу с ее компонентами всегда (ядро термина).
|
| 145 |
for w in parts:
|
|
@@ -179,18 +239,24 @@ def build_semantic_graph(
|
|
| 179 |
# Для слов: умеренный буст по контекстной связанности.
|
| 180 |
# Для фраз: более сильный буст по частоте/охвату, чтобы устойчивые термины поднимались в топ.
|
| 181 |
combined_scores = {}
|
|
|
|
| 182 |
for node, score in pr.items():
|
| 183 |
out_deg = graph.out_degree(node)
|
| 184 |
in_deg = graph.in_degree(node)
|
| 185 |
connectivity_factor = 1.0 + 0.025 * (out_deg + in_deg)
|
|
|
|
|
|
|
| 186 |
|
| 187 |
if graph.nodes[node].get("term_type") == "phrase":
|
| 188 |
freq = max(1, int(graph.nodes[node].get("frequency", 1)))
|
| 189 |
sent_cover = len(phrase_sent_ids.get(node, set()))
|
| 190 |
termness = (1.0 + 0.22 * math.log1p(freq)) * (1.0 + 0.12 * math.log1p(sent_cover))
|
| 191 |
-
|
|
|
|
|
|
|
| 192 |
else:
|
| 193 |
-
|
|
|
|
| 194 |
|
| 195 |
node_weights = _normalize_to_1_100(combined_scores)
|
| 196 |
|
|
|
|
| 27 |
return lemmas
|
| 28 |
|
| 29 |
|
| 30 |
+
NOISE_PATTERNS = (
|
| 31 |
+
"contact us",
|
| 32 |
+
"responsible gaming",
|
| 33 |
+
"play responsibly",
|
| 34 |
+
"copyright",
|
| 35 |
+
"terms of use",
|
| 36 |
+
"new customers only",
|
| 37 |
+
"t&c apply",
|
| 38 |
+
"18+",
|
| 39 |
+
"info@",
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
GENERIC_TERMS = {
|
| 44 |
+
"игра", "играть", "казино", "ставка", "деньга", "деньги",
|
| 45 |
+
"демо", "режим", "уровень", "шаг", "выигрыш", "риск",
|
| 46 |
+
"game", "play", "casino", "bet", "demo", "mode", "level", "risk", "win",
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _is_noise_sentence(text: str) -> bool:
|
| 51 |
+
t = " ".join((text or "").lower().split())
|
| 52 |
+
if not t:
|
| 53 |
+
return True
|
| 54 |
+
if any(p in t for p in NOISE_PATTERNS):
|
| 55 |
+
return True
|
| 56 |
+
# Частые CTA/служебные короткие строки.
|
| 57 |
+
if len(t.split()) <= 3 and t in {"играть", "play", "chicken road", "chicken road game"}:
|
| 58 |
+
return True
|
| 59 |
+
return False
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _canonicalize_term(term: str) -> str:
|
| 63 |
+
t = " ".join((term or "").lower().replace("ё", "е").replace("-", " ").split())
|
| 64 |
+
if not t:
|
| 65 |
+
return t
|
| 66 |
+
|
| 67 |
+
# Бренд/наименование игры.
|
| 68 |
+
if t in {"чикен роад", "chicken road", "chickenroad", "chiken road"}:
|
| 69 |
+
return "chicken road"
|
| 70 |
+
if t in {"игры inout", "inout games", "inout game", "inout"}:
|
| 71 |
+
return "inout games"
|
| 72 |
+
|
| 73 |
+
# Полезная нормализация русской фразы под один термин.
|
| 74 |
+
if t in {"реальные деньги", "реальный деньги"}:
|
| 75 |
+
return "реальные деньги"
|
| 76 |
+
return t
|
| 77 |
+
|
| 78 |
+
|
| 79 |
def _extract_phrase_candidates(sentence_text: str, lang: str) -> List[str]:
|
| 80 |
"""
|
| 81 |
Извлекает фразовые кандидаты через существующую n-gram логику проекта,
|
|
|
|
| 123 |
pair_cooc = defaultdict(int)
|
| 124 |
phrase_occ = Counter()
|
| 125 |
phrase_sent_ids = defaultdict(set)
|
| 126 |
+
term_sent_ids = defaultdict(set)
|
| 127 |
sentence_words: List[List[str]] = []
|
| 128 |
sentence_phrases: List[List[str]] = []
|
| 129 |
|
| 130 |
for sent_id, sent in enumerate(sentences_data):
|
| 131 |
+
raw_text = sent.get("raw_text", "")
|
| 132 |
+
if _is_noise_sentence(raw_text):
|
| 133 |
+
sentence_words.append([])
|
| 134 |
+
sentence_phrases.append([])
|
| 135 |
+
continue
|
| 136 |
+
|
| 137 |
lemmas_raw = _extract_significant_lemmas(sent)
|
| 138 |
+
lemmas = [_canonicalize_term(x) for x in _normalize_lemma_sequence(lemmas_raw)]
|
| 139 |
+
lemmas = [x for x in lemmas if x]
|
| 140 |
sentence_words.append(lemmas)
|
| 141 |
|
| 142 |
+
phrase_candidates = _extract_phrase_candidates(raw_text, lang)
|
| 143 |
# Фильтр мусора фраз: минимум 2 слова, без дублирующегося подряд слова.
|
| 144 |
clean_phrases = []
|
| 145 |
for p in phrase_candidates:
|
|
|
|
| 149 |
bad_repeat = any(parts[i] == parts[i + 1] for i in range(len(parts) - 1))
|
| 150 |
if bad_repeat:
|
| 151 |
continue
|
| 152 |
+
clean_phrases.append(_canonicalize_term(" ".join(parts)))
|
| 153 |
sentence_phrases.append(clean_phrases)
|
| 154 |
|
| 155 |
if not lemmas:
|
|
|
|
| 172 |
neighbors.add(b)
|
| 173 |
for b in neighbors:
|
| 174 |
pair_cooc[(a, b)] += 1
|
| 175 |
+
for w in set(lemmas):
|
| 176 |
+
term_sent_ids[w].add(sent_id)
|
| 177 |
|
| 178 |
# Частоты/охват фраз по предложениям.
|
| 179 |
for sent_id, phrases in enumerate(sentence_phrases):
|
|
|
|
| 199 |
|
| 200 |
for phrase in uniq_phrases:
|
| 201 |
term_occ[phrase] += 1
|
| 202 |
+
term_sent_ids[phrase].add(sent_id)
|
| 203 |
parts = set(phrase.split())
|
| 204 |
# Связываем фразу с ее компонентами всегда (ядро термина).
|
| 205 |
for w in parts:
|
|
|
|
| 239 |
# Для слов: умеренный буст по контекстной связанности.
|
| 240 |
# Для фраз: более сильный буст по частоте/охвату, чтобы устойчивые термины поднимались в топ.
|
| 241 |
combined_scores = {}
|
| 242 |
+
total_sent = max(1, len(sentence_words))
|
| 243 |
for node, score in pr.items():
|
| 244 |
out_deg = graph.out_degree(node)
|
| 245 |
in_deg = graph.in_degree(node)
|
| 246 |
connectivity_factor = 1.0 + 0.025 * (out_deg + in_deg)
|
| 247 |
+
sent_df = len(term_sent_ids.get(node, set()))
|
| 248 |
+
idf_factor = 1.0 + 0.35 * math.log((1.0 + total_sent) / (1.0 + max(1, sent_df)))
|
| 249 |
|
| 250 |
if graph.nodes[node].get("term_type") == "phrase":
|
| 251 |
freq = max(1, int(graph.nodes[node].get("frequency", 1)))
|
| 252 |
sent_cover = len(phrase_sent_ids.get(node, set()))
|
| 253 |
termness = (1.0 + 0.22 * math.log1p(freq)) * (1.0 + 0.12 * math.log1p(sent_cover))
|
| 254 |
+
contains_generic = any(part in GENERIC_TERMS for part in node.split())
|
| 255 |
+
generic_penalty = 0.85 if contains_generic else 1.0
|
| 256 |
+
combined_scores[node] = score * connectivity_factor * termness * idf_factor * generic_penalty
|
| 257 |
else:
|
| 258 |
+
generic_penalty = 0.58 if node in GENERIC_TERMS else 1.0
|
| 259 |
+
combined_scores[node] = score * connectivity_factor * idf_factor * generic_penalty
|
| 260 |
|
| 261 |
node_weights = _normalize_to_1_100(combined_scores)
|
| 262 |
|