nlc-explorer / NLselector.py
Nathan Butters
cleaning spacy
b0fc967
raw
history blame
10 kB
#Import the libraries we know we'll need for the Generator.
import pandas as pd, spacy, nltk, numpy as np, re
from spacy.matcher import Matcher
#!python -m spacy download en_core_web_md #Not sure if we need this so I'm going to keep it just in case
nlp = spacy.load("en_core_web_lg")
import altair as alt
import streamlit as st
from annotated_text import annotated_text as ant
#Import the libraries to support the model and predictions.
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TextClassificationPipeline
import lime
import torch
import torch.nn.functional as F
from lime.lime_text import LimeTextExplainer
class_names = ['negative', 'positive']
explainer = LimeTextExplainer(class_names=class_names)
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased-finetuned-sst-2-english")
model = AutoModelForSequenceClassification.from_pretrained("distilbert-base-uncased-finetuned-sst-2-english")
pipe = TextClassificationPipeline(model=model, tokenizer=tokenizer, return_all_scores=True)
def predictor(texts):
outputs = model(**tokenizer(texts, return_tensors="pt", padding=True))
probas = F.softmax(outputs.logits, dim=1).detach().numpy()
return probas
@st.experimental_singleton
def critical_words(document, options=False):
if type(document) is not spacy.tokens.doc.Doc:
document = nlp(document)
chunks = list(document.noun_chunks)
pos_options = []
lime_options = []
#Identify what the model cares about.
if options:
#Run Lime Setup code
exp = explainer.explain_instance(document.text, predictor, num_features=15, num_samples=2000)
lime_results = exp.as_list()
for feature in lime_results:
lime_options.append(feature[0])
lime_results = pd.DataFrame(lime_results, columns=["Word","Weight"])
#Identify what we care about "parts of speech"
for chunk in chunks:
#The use of chunk[-1] is due to testing that it appears to always match the root
root = chunk[-1]
#This currently matches to a list I've created. I don't know the best way to deal with this so I'm leaving it as is for the moment.
if root.ent_type_:
cur_values = []
if (len(chunk) > 1) and (chunk[-2].dep_ == "compound"):
#creates the compound element of the noun
compound = [x.text for x in chunk if x.dep_ == "compound"]
print(f"This is the contents of {compound} and it is {all(elem in lime_options for elem in compound)} that all elements are present in {lime_options}.") #for QA
#checks to see all elements in the compound are important to the model or use the compound if not checking importance.
if (all(elem in lime_options for elem in cur_values) and (options is True)) or ((options is False)):
#creates a span for the entirety of the compound noun and adds it to the list.
span = -1 * (1 + len(compound))
pos_options.append(chunk[span:].text)
cur_values + [token.text for token in chunk if token.pos_ == "ADJ"]
else:
print(f"The elmenents in {compound} could not be added to the final list because they are not all relevant to the model.")
else:
cur_values = [token.text for token in chunk if (token.ent_type_) or (token.pos_ == "ADJ")]
if (all(elem in lime_options for elem in cur_values) and (options is True)) or ((options is False)):
pos_options.extend(cur_values)
print(f"From {chunk.text}, {cur_values} added to pos_options due to entity recognition.") #for QA
elif len(chunk) >= 1:
cur_values = [token.text for token in chunk if token.pos_ in ["NOUN","ADJ"]]
if (all(elem in lime_options for elem in cur_values) and (options is True)) or ((options is False)):
pos_options.extend(cur_values)
print(f"From {chunk.text}, {cur_values} added to pos_options due to wildcard.") #for QA
else:
print(f"No options added for \'{chunk.text}\' ")
# Here I am going to try to pick up pronouns, which are people, and Adjectival Compliments.
for token in document:
if (token.text not in pos_options) and ((token.text in lime_options) or (options == False)):
#print(f"executed {token.text} with {token.pos_} and {token.dep_}") #QA
if (token.pos_ == "ADJ") and (token.dep_ in ["acomp","conj"]):
pos_options.append(token.text)
elif (token.pos_ == "PRON") and (len(token.morph) !=0):
if (token.morph.get("PronType") == "Prs"):
pos_options.append(token.text)
if options:
return pos_options, lime_results
else:
return pos_options
# Return the Viz of elements critical to LIME.
def lime_viz(df):
if not isinstance(df, pd.DataFrame):
df = pd.DataFrame(df, columns=["Word","Weight"])
single_nearest = alt.selection_single(on='mouseover', nearest=True)
viz = alt.Chart(df).encode(
alt.X('Weight:Q', scale=alt.Scale(domain=(-1, 1))),
alt.Y('Word:N', sort='x', axis=None),
color=alt.Color("Weight", scale=alt.Scale(scheme='blueorange', domain=[0], type="threshold", range='diverging'), legend=None),
tooltip = ("Word","Weight")
).mark_bar().properties(title ="Importance of individual words")
text = viz.mark_text(
fill="black",
align='right',
baseline='middle'
).encode(
text='Word:N'
)
limeplot = alt.LayerChart(layer=[viz,text], width = 300).configure_axis(grid=False).configure_view(strokeWidth=0)
return limeplot
# Evaluate Predictions using the model and pipe.
def eval_pred(text, return_all = False):
'''A basic function for evaluating the prediction from the model and turning it into a visualization friendly number.'''
preds = pipe(text)
neg_score = -1 * preds[0][0]['score']
sent_neg = preds[0][0]['label']
pos_score = preds[0][1]['score']
sent_pos = preds[0][1]['label']
prediction = 0
sentiment = ''
if pos_score > abs(neg_score):
prediction = pos_score
sentiment = sent_pos
elif abs(neg_score) > pos_score:
prediction = neg_score
sentiment = sent_neg
if return_all:
return prediction, sentiment
else:
return prediction
def construct_nlexp(text,sentiment,probability):
prob = str(np.round(100 * abs(probability),2))
if sentiment == "NEGATIVE":
color_sent = ant('The model predicts the sentiment of the sentence you provided is ', (sentiment, "-", "#FFA44F"), ' with a probability of ', (prob, "neg", "#FFA44F"),"%.")
elif sentiment == "POSITIVE":
color_sent = ant('The model predicts the sentiment of the sentence you provided is ', (sentiment, "+", "#50A9FF"), ' with a probability of ', (prob, "pos", "#50A9FF"),"%.")
return color_sent
def get_min_max(df, seed):
'''This function provides the alternatives with the highest spaCy similarity scores and the lowest similarity scores. As similarity is based on vectorization of words and documents this may not be the best way to identify bias.
text2 = Most Similar
text3 = Least Similar'''
maximum = df[df['similarity'] < .9999].similarity.max()
text2 = df.loc[df['similarity'] == maximum, 'text'].iloc[0]
minimum = df[df['similarity'] > .0001].similarity.min()
text3 = df.loc[df['similarity'] == minimum, 'text'].iloc[0]
return text2, text3
# Inspired by https://stackoverflow.com/questions/17758023/return-rows-in-a-dataframe-closest-to-a-user-defined-number/17758115#17758115
def abs_dif(df,seed):
'''This function enables a user to identify the alternative that is closest to the seed and farthest from the seed should that be the what they wish to display.
text2 = Nearest Prediction
text3 = Farthest Prediction'''
target = df[df['Words'] == seed].pred.iloc[0]
sub_df = df[df['Words'] != seed].reset_index()
nearest_prediction = sub_df.pred[(sub_df.pred-target).abs().argsort()[:1]]
farthest_prediction = sub_df.pred[(sub_df.pred-target).abs().argsort()[-1:]]
text2 = sub_df.text.iloc[nearest_prediction.index[0]]
text3 = sub_df.text.iloc[farthest_prediction.index[0]]
return text2, text3
#@st.experimental_singleton #I've enabled this to prevent it from triggering every time the code runs... which could get very messy
def sampled_alts(df, seed, fixed=False):
'''This function enables a user to select an alternate way of choosing which counterfactuals are shown for MultiNLC, MultiNLC + Lime, and VizNLC. If you use this then you are enabling random sampling over other options (ex. spaCy similarity scores, or absolute difference).
Both samples are random.'''
sub_df = df[df['Words'] != seed]
if fixed:
sample = sub_df.sample(n=2, random_state = 2052)
else:
sample = sub_df.sample(n=2)
text2 = sample.text.iloc[0]
text3 = sample.text.iloc[1]
return text2, text3
def gen_cf_country(df,_document,selection):
df['text'] = df.Words.apply(lambda x: re.sub(r'\b'+selection+r'\b',x,_document.text))
df['pred'] = df.text.apply(eval_pred)
df['seed'] = df.Words.apply(lambda x: 'seed' if x == selection else 'alternative')
df['similarity'] = df.Words.apply(lambda x: nlp(selection).similarity(nlp(x)))
return df
def gen_cf_profession(df,_document,selection):
category = df.loc[df['Words'] == selection, 'Major'].iloc[0]
df = df[df.Major == category]
df['text'] = df.Words.apply(lambda x: re.sub(r'\b'+selection+r'\b',x,_document.text))
df['pred'] = df.text.apply(eval_pred)
df['seed'] = df.Words.apply(lambda x: 'seed' if x == selection else 'alternative')
df['similarity'] = df.Words.apply(lambda x: nlp(selection).similarity(nlp(x)))
return df