lsdf commited on
Commit
2aba76e
·
1 Parent(s): 9690220

Add semantic cleanup, canonicalization, and generic-term downweighting

Browse files

Filter 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

Files changed (1) hide show
  1. 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(sent.get("raw_text", ""), lang)
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
- combined_scores[node] = score * connectivity_factor * termness
 
 
192
  else:
193
- combined_scores[node] = score * connectivity_factor
 
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