ESG / models /pmfc_classifier.py
alirezamousio's picture
Upload pmfc_classifier.py
038e07e verified
Raw
History Blame Contribute Delete
7.47 kB
import torch
import pandas as pd
from sentence_transformers import SentenceTransformer, util
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
PMFC_JSON_PATH = "assets/pmfc.json"
EMBED_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
CLASSIFIER_MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
# =========================
# LAZY-LOADED MODEL / DATA CACHE
# =========================
_CACHE = {
"embedder": None,
"tokenizer": None,
"model": None,
"pipe": None,
"pm_examples": None, # DataFrame
"fc_examples": None, # DataFrame
"pm_embeddings": None,
"fc_embeddings": None,
}
def _get_embedder():
if _CACHE["embedder"] is None:
_CACHE["embedder"] = SentenceTransformer(EMBED_MODEL_NAME, device=str(device))
return _CACHE["embedder"]
def _get_pipe():
if _CACHE["pipe"] is None:
tokenizer = AutoTokenizer.from_pretrained(CLASSIFIER_MODEL_NAME)
model = AutoModelForCausalLM.from_pretrained(
CLASSIFIER_MODEL_NAME,
torch_dtype=torch.float16 if device.type == "cuda" else torch.float32,
device_map="auto" if device.type == "cuda" else None
)
if device.type == "cpu":
model.to(device)
_CACHE["tokenizer"] = tokenizer
_CACHE["model"] = model
_CACHE["pipe"] = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
return_full_text=False,
do_sample=False
)
return _CACHE["pipe"], _CACHE["tokenizer"]
def _load_pmfc_corpus():
"""Loads assets/pmfc.json and pre-computes PM/FC embeddings separately,
same split logic as the reference script (str.contains on FinalAssessment)."""
if _CACHE["pm_examples"] is not None:
return
examples_df = pd.read_json(PMFC_JSON_PATH)
pm_examples = examples_df[
examples_df["FinalAssessment"].str.contains("PM", na=False)
].reset_index(drop=True)
fc_examples = examples_df[
examples_df["FinalAssessment"].str.contains("FC", na=False)
].reset_index(drop=True)
embedder = _get_embedder()
pm_embeddings = embedder.encode(
pm_examples["Paragraph_content"].tolist(),
convert_to_tensor=True,
normalize_embeddings=True
)
fc_embeddings = embedder.encode(
fc_examples["Paragraph_content"].tolist(),
convert_to_tensor=True,
normalize_embeddings=True
)
_CACHE["pm_examples"] = pm_examples
_CACHE["fc_examples"] = fc_examples
_CACHE["pm_embeddings"] = pm_embeddings
_CACHE["fc_embeddings"] = fc_embeddings
# =========================
# PROMPT (matches reference script's generate_pmfc_prompt exactly)
# =========================
def generate_pmfc_prompt(paragraph, pm_example, fc_example):
prompt = f"""
You are an expert sustainability-report analyst.
The paragraph below is already known to be:
- Sustainability Relevant
- Quantitative
Your task is to classify it into EXACTLY ONE category.
Definitions
Performance Metrics (PM):
- Reports achievements
- Reports measured performance
- Reports historical results
- Reports current status
- Reports quantities already achieved
Future Commitments (FC):
- Reports future targets
- Reports future goals
- Reports planned improvements
- Reports intended reductions or increases
- Reports commitments to be achieved later
Example 1
Paragraph:
{pm_example}
Answer: PM
Example 2
Paragraph:
{fc_example}
Answer: FC
Paragraph to classify:
{paragraph}
Respond with EXACTLY one of:
Answer: PM
or
Answer: FC
Do not explain.
Do not provide reasoning.
"""
return prompt
def classify_pmfc(paragraph, pm_example, fc_example):
"""Matches the reference script's classify_pmfc: generates up to 5 new
tokens, then checks the (uppercased) output for 'PM' or 'FC'."""
pipe, tokenizer = _get_pipe()
prompt = generate_pmfc_prompt(paragraph, pm_example, fc_example)
output = pipe(
prompt,
max_new_tokens=5,
pad_token_id=tokenizer.eos_token_id
)[0]["generated_text"]
output_upper = output.strip().upper()
if "PM" in output_upper:
return "Answer: PM"
if "FC" in output_upper:
return "Answer: FC"
return "UNKNOWN"
# =========================
# FULL PIPELINE: run on a quantitative-only subset of a DataFrame
# =========================
def classify_quantitative_paragraphs(df, qq_column):
"""
Mirrors the reference script's full flow:
1. Filter df to rows where qq_column == "Quantitative"
2. Retrieve top-1 most similar PM example and top-1 most similar FC
example (independently) for every quantitative paragraph
3. Classify each one via one-shot prompting against Qwen2.5-1.5B
4. Merge PMFC_Label (and the retrieved examples + similarity scores,
needed for the per-paragraph CSV export) back into the full df
Returns the full df with new columns added:
PMFC_Label, EX_PM, FA_PM, PM_Similarity, EX_FC, FA_FC, FC_Similarity
(non-quantitative rows get blank/NaN in these columns, same as the
reference script's df["PMFC_Label"] = "" then partial assignment)
"""
_load_pmfc_corpus()
embedder = _get_embedder()
df = df.copy()
# Use object dtype with pd.NA placeholders (not "") so that the
# numeric columns (PM_Similarity, FC_Similarity) can later receive
# float values without hitting pandas' Arrow-backed string dtype
# restriction (TypeError: Invalid value for dtype 'str').
for col in ["PMFC_Label", "EX_PM", "FA_PM", "PM_Similarity", "EX_FC", "FA_FC", "FC_Similarity"]:
df[col] = pd.Series([pd.NA] * len(df), index=df.index, dtype="object")
quant_df = df[df[qq_column] == "Quantitative"].copy()
if quant_df.empty:
return df
quant_embeddings = embedder.encode(
quant_df["Paragraph_content"].tolist(),
convert_to_tensor=True,
normalize_embeddings=True
)
pm_scores = util.cos_sim(quant_embeddings, _CACHE["pm_embeddings"])
pm_best_idx = torch.argmax(pm_scores, dim=1)
fc_scores = util.cos_sim(quant_embeddings, _CACHE["fc_embeddings"])
fc_best_idx = torch.argmax(fc_scores, dim=1)
pm_examples = _CACHE["pm_examples"]
fc_examples = _CACHE["fc_examples"]
predictions = []
ex_pm_list, ex_fc_list = [], []
pm_sim_list, fc_sim_list = [], []
for i in range(len(quant_df)):
pm_idx = pm_best_idx[i].item()
fc_idx = fc_best_idx[i].item()
pm_text = pm_examples.iloc[pm_idx]["Paragraph_content"]
fc_text = fc_examples.iloc[fc_idx]["Paragraph_content"]
ex_pm_list.append(pm_text)
ex_fc_list.append(fc_text)
pm_sim_list.append(float(pm_scores[i, pm_idx].item()))
fc_sim_list.append(float(fc_scores[i, fc_idx].item()))
pred = classify_pmfc(quant_df.iloc[i]["Paragraph_content"], pm_text, fc_text)
predictions.append(pred)
quant_df["PMFC_Label"] = predictions
quant_df["EX_PM"] = ex_pm_list
quant_df["FA_PM"] = "PM"
quant_df["PM_Similarity"] = pm_sim_list
quant_df["EX_FC"] = ex_fc_list
quant_df["FA_FC"] = "FC"
quant_df["FC_Similarity"] = fc_sim_list
for col in ["PMFC_Label", "EX_PM", "FA_PM", "PM_Similarity", "EX_FC", "FA_FC", "FC_Similarity"]:
df.loc[quant_df.index, col] = quant_df[col]
return df