Mollymo's picture
Create app.py
51e9060 verified
Raw
History Blame Contribute Delete
9.91 kB
import gradio as gr
from transformers import AutoTokenizer, AutoModelForMaskedLM
import torch
import spacy
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import re
import subprocess
# Download and load spaCy model
try:
nlp = spacy.load("en_core_web_sm")
except OSError:
print("Downloading spaCy model...")
subprocess.run(["python", "-m", "spacy", "download", "en_core_web_sm"])
nlp = spacy.load("en_core_web_sm")
# Load models
MODEL_NAME = "bert-large-uncased"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForMaskedLM.from_pretrained(MODEL_NAME)
model.eval()
# Load sentence transformer for semantic similarity
similarity_model = SentenceTransformer('all-MiniLM-L6-v2')
# POS tag mapping
POS_MAP = {
'NOUN': ['NOUN', 'PROPN'],
'VERB': ['VERB'],
'ADJ': ['ADJ'],
'ADV': ['ADV'],
'PROPN': ['NOUN', 'PROPN']
}
def get_pos_tag(word):
"""Get POS tag for a word"""
doc = nlp(word)
if len(doc) > 0:
return doc[0].pos_
return None
def extract_ngrams(text, max_n=3):
"""Extract n-grams (phrases) from text"""
doc = nlp(text)
ngrams = []
# Extract noun chunks (natural phrases)
for chunk in doc.noun_chunks:
if len(chunk.text.split()) > 1:
ngrams.append({
'text': chunk.text,
'start': chunk.start,
'end': chunk.end,
'type': 'noun_chunk'
})
# Extract compound words and hyphenated terms
tokens = text.split()
for i, token in enumerate(tokens):
# Handle hyphenated words as single units
if '-' in token and len(token) > 2:
ngrams.append({
'text': token,
'start': i,
'end': i + 1,
'type': 'hyphenated'
})
return ngrams
def suggest_alternatives_for_phrase(phrase, context, top_k=5, similarity_threshold=0.3):
"""Get alternatives for multi-word phrases"""
# For now, we'll mask the entire phrase and get alternatives
masked_text = context.replace(phrase, "[MASK]")
inputs = tokenizer(masked_text, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits
mask_indices = (inputs["input_ids"] == tokenizer.mask_token_id).nonzero(as_tuple=True)[1]
if len(mask_indices) == 0:
return []
mask_idx = mask_indices[0]
probs = torch.nn.functional.softmax(logits[0, mask_idx, :], dim=-1)
top_indices = torch.topk(probs, top_k * 10).indices.tolist()
candidates = []
phrase_lower = phrase.lower()
for idx in top_indices:
decoded = tokenizer.decode([idx]).strip()
if not decoded.isalpha() or len(decoded) < 2 or decoded.lower() == phrase_lower:
continue
# Semantic similarity
phrase_embedding = similarity_model.encode([phrase_lower])
candidate_embedding = similarity_model.encode([decoded.lower()])
similarity = cosine_similarity(phrase_embedding, candidate_embedding)[0][0]
if similarity >= similarity_threshold:
candidates.append((decoded, similarity))
if len(candidates) >= top_k:
break
candidates.sort(key=lambda x: x[1], reverse=True)
return candidates[:top_k]
def suggest_alternatives(text, top_k=5, similarity_threshold=0.3, analyze_phrases=True):
if not text.strip():
return "Please enter some text."
suggestions = {}
# Get POS tags for all words
doc = nlp(text)
word_pos = {token.text: token.pos_ for token in doc}
# Extract and analyze phrases first (if enabled)
if analyze_phrases:
ngrams = extract_ngrams(text)
for ngram in ngrams:
phrase = ngram['text']
phrase_alts = suggest_alternatives_for_phrase(phrase, text, top_k, similarity_threshold)
if phrase_alts:
suggestions[phrase] = {
'alternatives': phrase_alts,
'pos': 'PHRASE',
'type': ngram['type']
}
# Now analyze individual words
# Split by whitespace but preserve original tokens
words = text.split()
for i, word in enumerate(words):
# Clean the word but preserve hyphenated words
cleaned_word = word.strip('.,!?;:"\'')
# Skip if already analyzed as part of phrase
if cleaned_word in suggestions:
continue
# Skip pure punctuation or very short words (but include "I", "a")
if not re.search(r'[a-zA-Z]', cleaned_word) or len(cleaned_word) < 1:
continue
# Convert to lowercase for analysis (but preserve original for display)
word_lower = cleaned_word.lower()
# Get POS tag
original_pos = word_pos.get(cleaned_word)
if not original_pos:
# Try lowercase version
original_pos = get_pos_tag(word_lower)
if not original_pos:
continue
# Create masked sentence
masked_words = words.copy()
masked_words[i] = "[MASK]"
masked_text = " ".join(masked_words)
# Tokenize
inputs = tokenizer(masked_text, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits
# Find mask position
mask_indices = (inputs["input_ids"] == tokenizer.mask_token_id).nonzero(as_tuple=True)[1]
if len(mask_indices) == 0:
continue
mask_idx = mask_indices[0]
probs = torch.nn.functional.softmax(logits[0, mask_idx, :], dim=-1)
top_indices = torch.topk(probs, top_k * 10).indices.tolist()
# Decode and filter
candidates = []
for idx in top_indices:
decoded = tokenizer.decode([idx]).strip()
# Basic filters (use lowercase comparison)
if not decoded.isalpha() or len(decoded) < 1 or decoded.lower() == word_lower:
continue
# POS filter
candidate_pos = get_pos_tag(decoded)
if candidate_pos:
allowed_pos = POS_MAP.get(original_pos, [original_pos])
if candidate_pos not in allowed_pos:
continue
# Semantic similarity filter (compare lowercase versions)
word_embedding = similarity_model.encode([word_lower])
candidate_embedding = similarity_model.encode([decoded.lower()])
similarity = cosine_similarity(word_embedding, candidate_embedding)[0][0]
if similarity >= similarity_threshold:
candidates.append((decoded, similarity))
if len(candidates) >= top_k:
break
if candidates:
# Sort by similarity score (descending)
candidates.sort(key=lambda x: x[1], reverse=True)
suggestions[cleaned_word] = {
'alternatives': candidates[:top_k],
'pos': original_pos,
'type': 'word'
}
if not suggestions:
return "No suggestions found. Try lowering the similarity threshold."
# Format output
output = "## πŸ“ Word & Phrase Alternatives\n\n"
# Separate phrases and words
phrases = {k: v for k, v in suggestions.items() if v['type'] in ['noun_chunk', 'hyphenated']}
words_dict = {k: v for k, v in suggestions.items() if v['type'] == 'word'}
if phrases:
output += "### πŸ”— Phrases\n\n"
for phrase, data in phrases.items():
output += f"**{phrase}** ({data['type']}) β†’ "
formatted_alts = [f"{alt} `({score:.2f})`" for alt, score in data['alternatives']]
output += ", ".join(formatted_alts)
output += "\n\n"
if words_dict:
output += "### πŸ“– Individual Words\n\n"
for word, data in words_dict.items():
output += f"**{word}** ({data['pos']}) β†’ "
formatted_alts = [f"{alt} `({score:.2f})`" for alt, score in data['alternatives']]
output += ", ".join(formatted_alts)
output += "\n\n"
return output
demo = gr.Interface(
fn=suggest_alternatives,
inputs=[
gr.Textbox(
label="Input text",
placeholder="Enter a sentence to analyze (all words will be analyzed)",
lines=3
),
gr.Slider(3, 10, value=5, step=1, label="Number of suggestions per word"),
gr.Slider(0.0, 1.0, value=0.3, step=0.05, label="Similarity threshold (higher = more similar)"),
gr.Checkbox(label="Analyze phrases (2-3 word combinations)", value=True)
],
outputs=gr.Markdown(label="Word & Phrase Alternatives"),
title="πŸ” BERT Synonym Tool V2 - Advanced",
description="""
**Enhanced features:**
- βœ… Analyzes ALL words (including "the", "a", "and", etc.)
- βœ… Handles hyphenated words ("cutting-edge", "data-driven")
- βœ… Detects multi-word phrases ("machine learning", "digital transformation")
- βœ… Case-insensitive analysis with preserved capitalization
- βœ… Semantic similarity scores (0.0-1.0)
""",
examples=[
["Leveraging cutting-edge artificial intelligence and machine learning paradigms, organizations can harness data-driven insights to drive transformative digital innovation and optimize operational workflows.", 5, 0.3, True],
["The quick brown fox jumps over the lazy dog.", 5, 0.4, True],
["Machine learning algorithms process data efficiently.", 7, 0.2, True]
]
)
if __name__ == "__main__":
demo.launch()