import copy from sklearn.decomposition import PCA from sklearn.metrics.pairwise import euclidean_distances import matplotlib.pyplot as plt import numpy as np import seaborn as sns from transformers import AutoTokenizer, AutoModelForMaskedLM import pandas as pd from gensim.models import KeyedVectors from utils_sesgo_en_palabras import ( cosine_similarity, normalize, project_params, take_two_sides_extreme_sorted ) DIRECTION_METHODS = ['single', 'sum', 'pca'] DEBIAS_METHODS = ['neutralize', 'hard', 'soft'] FIRST_PC_THRESHOLD = 0.5 MAX_NON_SPECIFIC_EXAMPLES = 1000 __all__ = ['GenderBiasWE', 'BiasWordEmbedding'] class Loader(): def __init__(self): self.path_to_data = '' def load_tokenizer(self, tokenizer_path): tokenizer = AutoTokenizer.from_pretrained( tokenizer_path, do_lower_case=True, ) return tokenizer def load_data_from_file(self, data): return data def load_corpus_from_file(self, data): return data def load_language_model(self, model_path): model = AutoModelForMaskedLM.from_pretrained( model_path, output_hidden_states=True) return model class Corpus(): def __init__(self, corpus) -> None: self.vocabulary = self.load_vocabulary_from_corpus() self.corpus = corpus def get_context_from_text(self, word): pass def get_frequency(self, word): pass def get_most_frequent_coocurrence(self, word): pass class Embedding(): def __init__(self, word_vectors_path) -> None: self.wv = self.load_we_as_keyed_vectors(word_vectors_path) def load_we_as_keyed_vectors(self, word_vectors_path): we = KeyedVectors.load_word2vec_format(word_vectors_path) we.init_sims(replace=True) return we def get_word_vector(self, word, context=None): return word class BiasExplorer(): def __init__(self, model, only_lower=False, verbose=False, identify_direction=False, to_normalize=True): # pylint: disable=undefined-variable # TODO: this is bad Python, ask someone about it # probably should be a better design # identify_direction doesn't have any meaning # for the class BiasWordEmbedding # The goal is to force this interfeace of sub-classes. if self.__class__ == __class__ and identify_direction is not False: raise ValueError('identify_direction must be False' ' for an instance of {}' .format(__class__)) self.model = model # TODO: write unitest for when it is False self.only_lower = only_lower self._verbose = verbose self.direction = None self.positive_end = None self.negative_end = None if to_normalize: self.model.init_sims(replace=True) def __copy__(self): bias_word_embedding = self.__class__(self.model, self.only_lower, self._verbose, identify_direction=False) bias_word_embedding.direction = copy.deepcopy(self.direction) bias_word_embedding.positive_end = copy.deepcopy(self.positive_end) bias_word_embedding.negative_end = copy.deepcopy(self.negative_end) return bias_word_embedding def __deepcopy__(self, memo): bias_word_embedding = copy.copy(self) bias_word_embedding.model = copy.deepcopy(bias_word_embedding.model) return bias_word_embedding def __getitem__(self, key): return self.model[key] def __contains__(self, item): return item in self.model def _is_direction_identified(self): if self.direction is None: raise RuntimeError('The direction was not identified' ' for this {} instance' .format(self.__class__.__name__)) def _identify_subspace_by_pca(self, definitional_pairs, n_components): matrix = [] for word1, word2 in definitional_pairs: vector1 = normalize(self[word1]) vector2 = normalize(self[word2]) center = (vector1 + vector2) / 2 matrix.append(vector1 - center) matrix.append(vector2 - center) pca = PCA(n_components=n_components) pca.fit(matrix) if self._verbose: table = enumerate(pca.explained_variance_ratio_, start=1) headers = ['Principal Component', 'Explained Variance Ratio'] return pca def __errorChecking(self, word): out_msj = "" if not word: out_msj = "Error: Primero debe ingresar una palabra!" else: if not word in self.model: out_msj = f"Error: La palabra '{word}' no se encuentra en el vocabulario!" if out_msj: out_msj = "

"+out_msj+"

" return out_msj # TODO: add the SVD method from section 6 step 1 # It seems there is a mistake there, I think it is the same as PCA # just with replacing it with SVD def _identify_direction(self, positive_end, negative_end, definitional, method='pca'): if method not in DIRECTION_METHODS: raise ValueError('method should be one of {}, {} was given'.format( DIRECTION_METHODS, method)) if positive_end == negative_end: raise ValueError('positive_end and negative_end' 'should be different, and not the same "{}"' .format(positive_end)) if self._verbose: print('Identify direction using {} method...'.format(method)) direction = None if method == 'single': if self._verbose: print('Positive definitional end:', definitional[0]) print('Negative definitional end:', definitional[1]) direction = normalize(normalize(self[definitional[0]]) - normalize(self[definitional[1]])) elif method == 'sum': group1_sum_vector = np.sum([self[word] for word in definitional[0]], axis=0) group2_sum_vector = np.sum([self[word] for word in definitional[1]], axis=0) diff_vector = (normalize(group1_sum_vector) - normalize(group2_sum_vector)) direction = normalize(diff_vector) elif method == 'pca': pca = self._identify_subspace_by_pca(definitional, 10) if pca.explained_variance_ratio_[0] < FIRST_PC_THRESHOLD: raise RuntimeError('The Explained variance' 'of the first principal component should be' 'at least {}, but it is {}' .format(FIRST_PC_THRESHOLD, pca.explained_variance_ratio_[0])) direction = pca.components_[0] # if direction is opposite (e.g. we cannot control # what the PCA will return) ends_diff_projection = cosine_similarity((self[positive_end] - self[negative_end]), direction) if ends_diff_projection < 0: direction = -direction # pylint: disable=invalid-unary-operand-type self.direction = direction self.positive_end = positive_end self.negative_end = negative_end def project_on_direction(self, word): """Project the normalized vector of the word on the direction. :param str word: The word tor project :return float: The projection scalar """ self._is_direction_identified() vector = self[word] projection_score = self.model.cosine_similarities(self.direction, [vector])[0] return projection_score def _calc_projection_scores(self, words): self._is_direction_identified() df = pd.DataFrame({'word': words}) # TODO: maybe using cosine_similarities on all the vectors? # it might be faster df['projection'] = df['word'].apply(self.project_on_direction) df = df.sort_values('projection', ascending=False) return df def calc_projection_data(self, words): """ Calculate projection, projected and rejected vectors of a words list. :param list words: List of words :return: :class:`pandas.DataFrame` of the projection, projected and rejected vectors of the words list """ projection_data = [] for word in words: vector = self[word] projection = self.project_on_direction(word) normalized_vector = normalize(vector) (projection, projected_vector, rejected_vector) = project_params(normalized_vector, self.direction) projection_data.append({'word': word, 'vector': vector, 'projection': projection, 'projected_vector': projected_vector, 'rejected_vector': rejected_vector}) return pd.DataFrame(projection_data) def plot_dist_projections_on_direction(self, word_groups, ax=None): """Plot the projection scalars distribution on the direction. :param dict word_groups word: The groups to projects :return float: The ax object of the plot """ if ax is None: _, ax = plt.subplots(1) names = sorted(word_groups.keys()) for name in names: words = word_groups[name] label = '{} (#{})'.format(name, len(words)) vectors = [self[word] for word in words] projections = self.model.cosine_similarities(self.direction, vectors) sns.distplot(projections, hist=False, label=label, ax=ax) plt.axvline(0, color='k', linestyle='--') plt.title('← {} {} {} →'.format(self.negative_end, ' ' * 20, self.positive_end)) plt.xlabel('Direction Projection') plt.ylabel('Density') ax.legend(loc='center left', bbox_to_anchor=(1, 0.5)) return ax def __errorChecking(self, word): out_msj = "" if not word: out_msj = "Error: Primero debe ingresar una palabra!" else: if not word in self.model: out_msj = f"Error: La palabra '{word}' no se encuentra en el vocabulario!" if out_msj: out_msj = "

"+out_msj+"

" return out_msj def parse_words(self, string): words = string.strip() if words: words = [word.strip() for word in words.split(',') if word != ""] return words def check_oov(self, wordlists): for wordlist in wordlists: parsed_words = self.parse_words(wordlist) for word in parsed_words: msg = self.__errorChecking(word) if msg: return msg return None def plot_projections_2d(self, wordlist, wordlist_1, wordlist_2, wordlist_3, wordlist_4, color_wordlist, color_wordlist_1, color_wordlist_2, color_wordlist_3, color_wordlist_4, plot_neighbors, n_alpha, fontsize, figsize=(15, 15), method='pca' ): # convertirlas a vector choices = [0, 1, 2, 3, 4] word_list = [] wordlist_choice = [wordlist, wordlist_1, wordlist_2, wordlist_3, wordlist_4] err= self.check_oov(wordlist_choice) if err: return None, err words_colors = {} label_dict = { 0: 'Diagnostico', 1: 'Lista de palabras 1', 2: 'Lista de palabras 2', 3: 'Lista de palabras 3', 4: 'Lista de palabras 4' } color_dict = { 0: color_wordlist, 1: color_wordlist_1, 2: color_wordlist_2, 3: color_wordlist_3, 4: color_wordlist_4 } word_bias_space = {} alpha = {} for raw_word_list, color in zip(wordlist_choice, choices): parsed_words = self.parse_words(raw_word_list) if parsed_words: for word in parsed_words: word_bias_space[word] = color words_colors[word] = color_dict[color] alpha[word] = 1 if plot_neighbors: neighbors = [w for w,s in self.model.most_similar(word,topn=5)] for n in neighbors: if n not in alpha: word_bias_space[n] = color words_colors[n] = color_dict[color] alpha[n] = n_alpha word_list += neighbors word_list += parsed_words if not word_list: return None, "

" + "Ingresa al menos 2 palabras para continuar" + "

" embeddings = [self.model[word] for word in word_list] words_embedded = PCA( n_components=2, random_state=1).fit_transform(embeddings) data = pd.DataFrame(words_embedded) data['word'] = word_list data['color'] = [words_colors[word] for word in word_list] data['alpha'] = [alpha[word] for word in word_list] data['word_bias_space'] = [word_bias_space[word] for word in word_list] fig, ax = plt.subplots(figsize=figsize) sns.scatterplot( data=data[data['alpha'] == 1], x=0, y=1, style='word_bias_space', hue='word_bias_space', ax=ax, palette=color_dict ) if plot_neighbors: sns.scatterplot( data=data[data['alpha'] != 1], x=0, y=1, style='color', hue='word_bias_space', ax=ax, alpha=n_alpha, legend=False, palette=color_dict ) for i, label in enumerate(word_list): x, y = words_embedded[i, :] ax.annotate(label, xy=(x, y), xytext=(5, 2),color=words_colors[label], textcoords='offset points', ha='right', va='bottom', size=fontsize, alpha=alpha[label]) ax.set_xticks([]) ax.set_yticks([]) fig.tight_layout() fig.canvas.draw() data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8) w, h = fig.canvas.get_width_height() im = data.reshape((int(h), int(w), -1)) return im, '' class WEBiasExplorer2d(BiasExplorer): def __init__(self, word_embedding) -> None: super().__init__(word_embedding) def calculate_bias( self, palabras_extremo_1, palabras_extremo_2, palabras_para_situar ): wordlists = [ palabras_extremo_1, palabras_extremo_2, palabras_para_situar ] err = self.check_oov(wordlists) for wordlist in wordlists: if not wordlist: err = "

" + 'Debe ingresar al menos 1 palabra en las lista de palabras a diagnosticar, sesgo 1 y sesgo 2' +"

" if err: return None, err err = self.check_oov([palabras_extremo_1,palabras_extremo_2,palabras_para_situar]) if err: return None, err palabras_extremo_1 = self.parse_words(palabras_extremo_1) palabras_extremo_2 = self.parse_words(palabras_extremo_2) palabras_para_situar = self.parse_words(palabras_para_situar) im = self.get_bias_plot( palabras_para_situar, definitional=( palabras_extremo_1, palabras_extremo_2), method='sum', n_extreme=10 ) return im, '' def get_bias_plot(self, palabras_para_situar, definitional, method='sum', n_extreme=10, figsize=(10, 10) ): fig, ax = plt.subplots(1, figsize=figsize) self.method = method self.plot_projection_scores( definitional, palabras_para_situar, n_extreme, ax=ax,) fig.tight_layout() fig.canvas.draw() data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8) w, h = fig.canvas.get_width_height() im = data.reshape((int(h), int(w), -1)) return im def plot_projection_scores(self, definitional, words, n_extreme=10, ax=None, axis_projection_step=None): """Plot the projection scalar of words on the direction. :param list words: The words tor project :param int or None n_extreme: The number of extreme words to show :return: The ax object of the plot """ nombre_del_extremo_1 = ', '.join(definitional[0]) nombre_del_extremo_2 = ', '.join(definitional[1]) self._identify_direction(nombre_del_extremo_1, nombre_del_extremo_2, definitional=definitional, method='sum') self._is_direction_identified() projections_df = self._calc_projection_scores(words) projections_df['projection'] = projections_df['projection'].round(2) if n_extreme is not None: projections_df = take_two_sides_extreme_sorted(projections_df, n_extreme=n_extreme) if ax is None: _, ax = plt.subplots(1) if axis_projection_step is None: axis_projection_step = 0.1 cmap = plt.get_cmap('RdBu') projections_df['color'] = ((projections_df['projection'] + 0.5) .apply(cmap)) most_extream_projection = np.round( projections_df['projection'] .abs() .max(), decimals=1) sns.barplot(x='projection', y='word', data=projections_df, palette=projections_df['color']) plt.xticks(np.arange(-most_extream_projection, most_extream_projection + axis_projection_step, axis_projection_step)) xlabel = ('← {} {} {} →'.format(self.negative_end, ' ' * 20, self.positive_end)) plt.xlabel(xlabel) plt.ylabel('Words') return ax class WEBiasExplorer4d(BiasExplorer): def __init__(self, word_embedding) -> None: super().__init__(word_embedding) def calculate_bias( self, palabras_extremo_1, palabras_extremo_2, palabras_extremo_3, palabras_extremo_4, palabras_para_situar ): wordlists = [ palabras_extremo_1, palabras_extremo_2, palabras_extremo_3, palabras_extremo_4, palabras_para_situar ] err = self.check_oov(wordlists) for wordlist in wordlists: if not wordlist: err = "

" + '¡Para graficar con 4 espacios, debe ingresar al menos 1 palabra en todas las listas!' + "

" if err: return None, err palabras_extremo_1 = self.parse_words(palabras_extremo_1) palabras_extremo_2 = self.parse_words(palabras_extremo_2) palabras_extremo_3 = self.parse_words(palabras_extremo_3) palabras_extremo_4 = self.parse_words(palabras_extremo_4) palabras_para_situar = self.parse_words(palabras_para_situar) im = self.get_bias_plot( palabras_para_situar, definitional_1=( palabras_extremo_1, palabras_extremo_2), definitional_2=( palabras_extremo_3, palabras_extremo_4), method='sum', n_extreme=10 ) return im, '' def get_bias_plot(self, palabras_para_situar, definitional_1, definitional_2, method='sum', n_extreme=10, figsize=(10, 10) ): fig, ax = plt.subplots(1, figsize=figsize) self.method = method self.plot_projection_scores( definitional_1, definitional_2, palabras_para_situar, n_extreme, ax=ax,) fig.canvas.draw() data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8) w, h = fig.canvas.get_width_height() im = data.reshape((int(h), int(w), -1)) return im def plot_projection_scores(self, definitional_1, definitional_2, words, n_extreme=10, ax=None, axis_projection_step=None): """Plot the projection scalar of words on the direction. :param list words: The words tor project :param int or None n_extreme: The number of extreme words to show :return: The ax object of the plot """ nombre_del_extremo_1 = ', '.join(definitional_1[1]) nombre_del_extremo_2 = ', '.join(definitional_1[0]) self._identify_direction(nombre_del_extremo_1, nombre_del_extremo_2, definitional=definitional_1, method='sum') self._is_direction_identified() projections_df = self._calc_projection_scores(words) projections_df['projection_x'] = projections_df['projection'].round(2) nombre_del_extremo_3 = ', '.join(definitional_2[1]) nombre_del_extremo_4 = ', '.join(definitional_2[0]) self._identify_direction(nombre_del_extremo_3, nombre_del_extremo_4, definitional=definitional_2, method='sum') self._is_direction_identified() projections_df['projection_y'] = self._calc_projection_scores(words)[ 'projection'].round(2) if n_extreme is not None: projections_df = take_two_sides_extreme_sorted(projections_df, n_extreme=n_extreme) if ax is None: _, ax = plt.subplots(1) if axis_projection_step is None: axis_projection_step = 0.1 cmap = plt.get_cmap('RdBu') projections_df['color'] = ((projections_df['projection'] + 0.5) .apply(cmap)) most_extream_projection = np.round( projections_df['projection'] .abs() .max(), decimals=1) sns.scatterplot(x='projection_x', y='projection_y', data=projections_df, palette=projections_df['color']) plt.xticks(np.arange(-most_extream_projection, most_extream_projection + axis_projection_step, axis_projection_step)) for _, row in (projections_df.iterrows()): ax.annotate( row['word'], (row['projection_x'], row['projection_y'])) x_label = '← {} {} {} →'.format(nombre_del_extremo_1, ' ' * 20, nombre_del_extremo_2) y_label = '← {} {} {} →'.format(nombre_del_extremo_3, ' ' * 20, nombre_del_extremo_4) plt.xlabel(x_label) ax.xaxis.set_label_position('bottom') ax.xaxis.set_label_coords(.5, 0) plt.ylabel(y_label) ax.yaxis.set_label_position('left') ax.yaxis.set_label_coords(0, .5) ax.spines['left'].set_position('center') ax.spines['bottom'].set_position('center') ax.set_xticks([]) ax.set_yticks([]) #plt.yticks([], []) # ax.spines['left'].set_position('zero') # ax.spines['bottom'].set_position('zero') return ax