Skip to content

Commit f0e7db5

Browse files
Merge pull request #23 from tomaarsen/optimization
Resolve critical bugs & optimize code
2 parents 75a64fd + cf8264d commit f0e7db5

2 files changed

Lines changed: 83 additions & 106 deletions

File tree

README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ nlp.add_pipe(
5353
"concise_concepts",
5454
config={
5555
"data": data,
56-
"ent_score": True, # Entity Scoring section
56+
"ent_score": True, # Entity Scoring section
5757
"verbose": True,
5858
"exclude_pos": ["VERB", "AUX"],
5959
"exclude_dep": ["DOBJ", "PCOMP"],
@@ -70,7 +70,7 @@ options = {
7070

7171
ents = doc.ents
7272
for ent in ents:
73-
new_label = f"{ent.label_} ({float(ent._.ent_score):.0%})"
73+
new_label = f"{ent.label_} ({ent._.ent_score:.0%})"
7474
options["colors"][new_label] = options["colors"].get(ent.label_.lower(), None)
7575
options["ents"].append(new_label)
7676
ent.label_ = new_label
@@ -128,13 +128,13 @@ data = {
128128
text = """Sony was founded in Japan."""
129129

130130
nlp = spacy.load("en_core_web_lg")
131-
nlp.add_pipe("concise_concepts", config={"data": data, "ent_score": True})
131+
nlp.add_pipe("concise_concepts", config={"data": data, "ent_score": True, "case_sensitive": True})
132132
doc = nlp(text)
133133

134134
print([(ent.text, ent.label_, ent._.ent_score) for ent in doc.ents])
135135
# output
136136
#
137-
# [('Sony', 'ORG', 0.63740385), ('Japan', 'GPE', 0.5896993)]
137+
# [('Sony', 'ORG', 0.5207586), ('Japan', 'GPE', 0.7371268)]
138138
```
139139

140140
## Custom Embedding Models
@@ -151,5 +151,3 @@ model_path = "glove-wiki-gigaword-300"
151151

152152
nlp.add_pipe("concise_concepts", config={"data": data, "model_path": model_path})
153153
````
154-
155-

concise_concepts/conceptualizer/Conceptualizer.py

Lines changed: 79 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# -*- coding: utf-8 -*-
2-
import itertools
32
import json
43
import logging
54
import re
@@ -25,19 +24,8 @@ def __init__(
2524
model_path: str = None,
2625
word_delimiter: str = "_",
2726
ent_score: bool = False,
28-
exclude_pos: list = [
29-
"VERB",
30-
"AUX",
31-
"ADP",
32-
"DET",
33-
"CCONJ",
34-
"PUNCT",
35-
"ADV",
36-
"ADJ",
37-
"PART",
38-
"PRON",
39-
],
40-
exclude_dep: list = [],
27+
exclude_pos: list = None,
28+
exclude_dep: list = None,
4129
include_compound_words: bool = False,
4230
case_sensitive: bool = False,
4331
json_path: str = "./matching_patterns.json",
@@ -81,10 +69,23 @@ def __init__(
8169
self.topn = topn
8270
self.model_path = model_path
8371
self.match_rule = {}
84-
if exclude_pos:
85-
self.match_rule["POS"] = {"NOT_IN": exclude_pos}
86-
if exclude_dep:
87-
self.match_rule["DEP"] = {"NOT_IN": exclude_dep}
72+
if exclude_pos is None:
73+
exclude_pos = [
74+
"VERB",
75+
"AUX",
76+
"ADP",
77+
"DET",
78+
"CCONJ",
79+
"PUNCT",
80+
"ADV",
81+
"ADJ",
82+
"PART",
83+
"PRON",
84+
]
85+
self.match_rule["POS"] = {"NOT_IN": exclude_pos}
86+
if exclude_dep is None:
87+
exclude_dep = []
88+
self.match_rule["DEP"] = {"NOT_IN": exclude_dep}
8889
self.json_path = json_path
8990
self.check_validity_path()
9091
self.include_compound_words = include_compound_words
@@ -107,7 +108,6 @@ def run(self):
107108
self.set_gensim_model()
108109
self.verify_data(self.verbose)
109110
self.expand_concepts()
110-
self.verify_data(verbose=False)
111111
# settle words around overlapping concepts
112112
for _ in range(5):
113113
self.expand_concepts()
@@ -137,17 +137,14 @@ def determine_topn(self):
137137
If the user doesn't specify a topn value for each class,
138138
then the topn value for each class is set to 100
139139
"""
140-
self.topn_dict = {}
141140
if not self.topn:
142-
for key in self.data:
143-
self.topn_dict[key] = 100
141+
self.topn_dict = {key: 100 for key in self.data}
144142
else:
145-
num_classes = len(list(self.data.keys()))
143+
num_classes = len(self.data)
146144
assert (
147145
len(self.topn) == num_classes
148146
), f"Provide a topn integer for each of the {num_classes} classes."
149-
for key, n in zip(self.data, self.topn):
150-
self.topn_dict[key] = n
147+
self.topn_dict = dict(zip(self.data, self.topn))
151148

152149
def set_gensim_model(self):
153150
"""
@@ -179,7 +176,7 @@ def set_gensim_model(self):
179176

180177
assert len(
181178
self.nlp.vocab.vectors
182-
), "Choose a model with internal embeddings i.e. md or lg."
179+
), "Choose a spaCy model with internal embeddings, e.g. md or lg."
183180

184181
for key, vector in self.nlp.vocab.vectors.items():
185182
wordList.append(self.nlp.vocab.strings[key])
@@ -194,102 +191,77 @@ def verify_data(self, verbose: bool = True):
194191
It takes a dictionary of lists of words, and returns a dictionary of lists of words,
195192
where each word in the list is present in the word2vec model
196193
"""
197-
verified_data = dict()
194+
verified_data: dict[str, list[str]] = dict()
198195
for key, value in self.data.items():
199196
verified_values = []
200-
if not self.check_presence_vocab(key):
201-
if verbose:
202-
if key not in self.log_cache["key"]:
203-
logger.warning(f"key ´{key}´ not present in vector model")
204-
self.log_cache["key"].append(key)
197+
present_key = self.check_presence_vocab(key)
198+
if not present_key and verbose and key not in self.log_cache["key"]:
199+
logger.warning(f"key ´{key}´ not present in vector model")
200+
self.log_cache["key"].append(key)
205201
for word in value:
206-
if self.check_presence_vocab(word):
207-
verified_values.append(self.check_presence_vocab(word))
208-
else:
209-
if verbose:
210-
if word not in self.log_cache["word"]:
211-
logger.warning(
212-
f"word ´{word}´ from key ´{key}´ not present in vector"
213-
" model"
214-
)
215-
self.log_cache["word"].append(word)
202+
present_word = self.check_presence_vocab(word)
203+
if present_word:
204+
verified_values.append(present_word)
205+
elif verbose and word not in self.log_cache["word"]:
206+
logger.warning(
207+
f"word ´{word}´ from key ´{key}´ not present in vector model"
208+
)
209+
self.log_cache["word"].append(word)
216210
verified_data[key] = verified_values
217211
if not len(verified_values):
218212
msg = (
219213
f"None of the entries for key {key} are present in the vector"
220214
" model. "
221215
)
222-
if self.check_presence_vocab(key):
223-
logger.warning(msg + f"Using {key} as word to expand over instead.")
224-
verified_data[key] = self.check_presence_vocab(key)
216+
if present_key:
217+
logger.warning(
218+
msg + f"Using {present_key} as word to expand over instead."
219+
)
220+
verified_data[key] = present_key
225221
else:
226222
raise Exception(msg)
227223
self.data = deepcopy(verified_data)
228-
self.original_data = deepcopy(self.data)
229224

230225
def expand_concepts(self):
231226
"""
232227
For each key in the data dictionary, find the topn most similar words to the key and the values in the data
233228
dictionary, and add those words to the values in the data dictionary
234229
"""
235230

231+
self.original_data = deepcopy(self.data)
232+
236233
for key in self.data:
237-
remaining_keys = [rem_key for rem_key in self.data.keys() if rem_key != key]
238-
remaining_values = [self.data[rem_key] for rem_key in remaining_keys]
239-
remaining_values = list(itertools.chain.from_iterable(remaining_values))
240-
if self.check_presence_vocab(key):
241-
key_list = [self.check_presence_vocab(key)]
234+
present_key = self.check_presence_vocab(key)
235+
if present_key:
236+
key_list = [present_key]
242237
else:
243238
key_list = []
244239
similar = self.kv.most_similar(
245240
positive=self.data[key] + key_list,
246241
topn=self.topn_dict[key],
247242
)
248-
similar = [sim_pair[0] for sim_pair in similar]
249-
self.data[key] += similar
250-
self.data[key] = list(set([word.lower() for word in self.data[key]]))
243+
self.data[key] = list(
244+
{self.check_presence_vocab(word) for word, _ratio in similar}
245+
)
251246

252247
def resolve_overlapping_concepts(self):
253248
"""
254249
It removes words from the data that are in other concepts, and then removes words that are not closest to the
255250
centroid of the concept
256251
"""
257-
centroids = {}
258-
for key in self.data:
259-
if not self.check_presence_vocab(key):
260-
words = self.data[key]
261-
while len(words) != 1:
262-
words.remove(self.kv.doesnt_match(words))
263-
centroids[key] = words[0]
264-
else:
265-
centroids[key] = key
266-
267-
for key_x in self.data:
268-
for key_y in self.data:
269-
if key_x != key_y:
270-
self.data[key_x] = [
271-
word
272-
for word in self.data[key_x]
273-
if word not in self.original_data[key_y]
274-
]
275-
276252
for key in self.data:
277253
self.data[key] = [
278254
word
279255
for word in self.data[key]
280-
if centroids[key]
281-
== self.kv.most_similar_to_given(word, list(centroids.values()))
256+
if key == self.kv.most_similar_to_given(word, list(self.data.keys()))
282257
]
283258

284-
self.centroids = centroids
285-
286259
def infer_original_data(self):
287260
"""
288261
It takes the original data and adds the new data to it, then removes the new data from the original data.
289262
"""
290263
for key in self.data:
291-
self.data[key] += self.original_data[key]
292-
self.data[key] = list(set(self.data[key]))
264+
self.data[key] = list(set(self.data[key] + self.original_data[key]))
293265

294266
for key_x in self.data:
295267
for key_y in self.data:
@@ -300,8 +272,6 @@ def infer_original_data(self):
300272
if word not in self.original_data[key_y]
301273
]
302274

303-
self.verify_data(verbose=False)
304-
305275
def lemmatize_concepts(self):
306276
"""
307277
For each key in the data dictionary,
@@ -347,7 +317,9 @@ def add_patterns(input_dict):
347317
words = [
348318
"".join(
349319
[
350-
token.lemma_ if token.lemma_ else token.text
320+
token.lemma_ + token.whitespace_
321+
if token.lemma_
322+
else token.text
351323
for token in doc
352324
]
353325
)
@@ -357,17 +329,18 @@ def add_patterns(input_dict):
357329
words = input_dict[key]
358330
for word in words:
359331
if word != key:
360-
specific_match_rule = dict()
361-
specific_match_rule.update(self.match_rule)
362-
word_parts = re.split(f"[{self.word_delimiter}]+", word)
332+
specific_match_rule = {**self.match_rule}
333+
word_parts = re.split(
334+
f"[{re.escape(self.word_delimiter)}]+", word
335+
)
363336
if len(word_parts) > 1:
364337
operators = [" ", "-"]
365338
else:
366339
operators = [""]
367340

368341
for op in operators:
369342
if self.case_sensitive:
370-
specific_match_rule[self.match_key] = "{op}".join(
343+
specific_match_rule[self.match_key] = f"{op}".join(
371344
word_parts
372345
)
373346
else:
@@ -404,7 +377,6 @@ def add_patterns(input_dict):
404377
)
405378

406379
add_patterns(self.data)
407-
add_patterns(self.original_data)
408380
if self.json_path:
409381
with open(self.json_path, "w") as f:
410382
json.dump(patterns, f)
@@ -453,16 +425,16 @@ def assign_score_to_entities(self, doc: Doc):
453425
if self.check_presence_vocab(ent.text):
454426
entity = [ent.text]
455427
else:
456-
entity = [
457-
self.check_presence_vocab(part)
458-
for part in ent.text.split()
459-
if self.check_presence_vocab(part)
460-
]
461-
concept = [
462-
self.check_presence_vocab(word)
463-
for word in self.data_upper[ent.label_]
464-
if self.check_presence_vocab(word)
465-
]
428+
entity = []
429+
for part in ent.text.split():
430+
present_part = self.check_presence_vocab(part)
431+
if present_part:
432+
entity.append(present_part)
433+
concept = []
434+
for word in self.data_upper[ent.label_]:
435+
present_word = self.check_presence_vocab(word)
436+
if present_word:
437+
concept.append(present_word)
466438
if entity and concept:
467439
ent._.ent_score = self.kv.n_similarity(entity, concept)
468440
else:
@@ -487,10 +459,17 @@ def assign_score_to_entities(self, doc: Doc):
487459
doc.ents = ents
488460
return doc
489461

490-
def check_presence_vocab(self, word):
462+
def _check_presence_vocab(self, word: str) -> str:
463+
if word in self.kv:
464+
return word
491465
for op in [" ", "-"]:
492466
check_word = word.replace(op, self.word_delimiter)
493-
if not self.case_sensitive:
494-
check_word = check_word.lower()
495467
if check_word in self.kv:
496468
return check_word
469+
470+
def check_presence_vocab(self, word: str) -> str:
471+
if not word.islower() and not self.case_sensitive:
472+
present_word = self._check_presence_vocab(word.lower())
473+
if present_word:
474+
return present_word
475+
return self._check_presence_vocab(word)

0 commit comments

Comments
 (0)