11# -*- coding: utf-8 -*-
2- import itertools
32import json
43import logging
54import 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