208 lines
6.4 KiB
Python
208 lines
6.4 KiB
Python
# Copyright 2025 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
Evaluation code for document classification use case. This code reads in a
|
|
CSV file with image paths and labels, prepares the data for evaluation, runs
|
|
inference using a specified Gemini model, and then evaluates the predictions
|
|
against the reference labels using the exact match metric in the Generative AI
|
|
Evaluation framework.
|
|
"""
|
|
|
|
import dotenv
|
|
import json
|
|
import os
|
|
import pandas as pd
|
|
|
|
from google.genai import types
|
|
import vertexai
|
|
|
|
import document_processing
|
|
|
|
|
|
# Load environment variables.
|
|
dotenv.load_dotenv()
|
|
PROJECT_ID = os.environ.get("GEMINI_PROJECT_ID")
|
|
if not PROJECT_ID:
|
|
raise ValueError("GEMINI_PROJECT_ID environment variable must be set.")
|
|
LOCATION = os.environ.get("GEMINI_LOCATION", "global")
|
|
IMAGE_PATHS = os.environ.get("IMAGE_PATHS", "")
|
|
IMAGE_PREFIX = os.environ.get("IMAGE_PREFIX", "")
|
|
EVAL_DEST = os.environ.get("EVAL_DEST")
|
|
|
|
# Other default constants.
|
|
EVAL_MODEL = "gemini-2.5-flash"
|
|
SAMPLE_SIZE = 10
|
|
|
|
|
|
def load_eval_data(csv_path: str, image_prefix: str) -> pd.DataFrame:
|
|
"""Reads eval data from CSV, formats paths, and prepares labels."""
|
|
df = pd.read_csv(csv_path)
|
|
df = df[["img_path", "label"]]
|
|
df["img_path"] = f"{image_prefix}/" + df["img_path"]
|
|
df = df.rename(columns={"label": "reference"})
|
|
return df
|
|
|
|
def prepare_eval_df(
|
|
csv_path: str,
|
|
image_prefix: str,
|
|
sample_size: int = None,
|
|
random_state: int = None,
|
|
stratify: bool = False,
|
|
classes: list[str] = None
|
|
) -> pd.DataFrame:
|
|
"""Prepares the eval_df based on the data from csv file with image paths."""
|
|
config_classes = (
|
|
document_processing.CONFIGS["classification_config"]["classes"]
|
|
)
|
|
|
|
if classes is None:
|
|
prompt_classes = config_classes
|
|
filter_classes = list(config_classes.keys())
|
|
else:
|
|
prompt_classes = {
|
|
k: v for k, v in config_classes.items() if k in classes
|
|
}
|
|
filter_classes = classes
|
|
|
|
prompt = document_processing.CLASSIFY_PROMPT_TEMPLATE.format(
|
|
classes=json.dumps(prompt_classes, indent=4)
|
|
)
|
|
print(prompt)
|
|
|
|
df = load_eval_data(csv_path, image_prefix)
|
|
|
|
# Filter the DataFrame to only include the requested classes
|
|
df = df[df["reference"].isin(filter_classes)].reset_index(drop=True)
|
|
|
|
requests = []
|
|
for uri in df["img_path"]:
|
|
image_part = types.Part.from_uri(
|
|
file_uri=uri,
|
|
mime_type="image/png"
|
|
)
|
|
requests.append([image_part, prompt])
|
|
|
|
df["request"] = requests
|
|
|
|
if sample_size and sample_size < len(df):
|
|
if stratify:
|
|
# Proportional stratified sampling per class
|
|
fraction = sample_size / len(df)
|
|
df = df.groupby("reference", group_keys=False).apply(
|
|
lambda x: x.sample(
|
|
n=max(1, int(round(len(x) * fraction))),
|
|
random_state=random_state
|
|
)
|
|
)
|
|
# Correct any slight oversampling due to rounding
|
|
if len(df) > sample_size:
|
|
df = df.sample(n=sample_size, random_state=random_state)
|
|
df = df.reset_index(drop=True)
|
|
else:
|
|
df = df.sample(
|
|
n=sample_size,
|
|
random_state=random_state
|
|
).reset_index(drop=True)
|
|
|
|
return df
|
|
|
|
def extract_class(response_str):
|
|
"""Extract the class from the JSON response."""
|
|
try:
|
|
return json.loads(response_str).get("class")
|
|
except (json.JSONDecodeError, AttributeError):
|
|
return response_str
|
|
|
|
def run_evaluation(
|
|
project_id: str = PROJECT_ID,
|
|
location: str = LOCATION,
|
|
csv_path: str = IMAGE_PATHS,
|
|
image_prefix: str = IMAGE_PREFIX,
|
|
eval_model: str = EVAL_MODEL,
|
|
sample_size: int = SAMPLE_SIZE,
|
|
random_state: int = 42,
|
|
stratify: bool = False,
|
|
classes: list[str] = None,
|
|
eval_dest: str = EVAL_DEST
|
|
):
|
|
client = vertexai.Client(project=project_id, location=location)
|
|
|
|
eval_df = prepare_eval_df(
|
|
csv_path=csv_path,
|
|
image_prefix=image_prefix,
|
|
sample_size=sample_size,
|
|
random_state=random_state,
|
|
stratify=stratify,
|
|
classes=classes
|
|
)
|
|
|
|
eval_dataset = client.evals.run_inference(
|
|
model=eval_model,
|
|
src=eval_df,
|
|
config={
|
|
"generate_content_config": {
|
|
"response_mime_type": "application/json",
|
|
"temperature": 0
|
|
},
|
|
"dest": eval_dest if eval_dest else None
|
|
}
|
|
)
|
|
|
|
if hasattr(eval_dataset, "eval_dataset_df"):
|
|
eval_dataset = eval_dataset.eval_dataset_df
|
|
|
|
eval_dataset["predicted_class"] = (
|
|
eval_dataset["response"].apply(extract_class)
|
|
)
|
|
|
|
# The evaluate function expects 'prompt', 'response', and 'reference'
|
|
# columns, even though the comparison is done between 'response' and
|
|
# 'reference' only.
|
|
eval_input_df = eval_dataset.copy()
|
|
eval_input_df["response"] = eval_input_df["predicted_class"]
|
|
eval_input_df["prompt"] = "Multimodal classification prompt"
|
|
eval_input_df = (
|
|
eval_input_df[["img_path", "prompt", "response", "reference"]]
|
|
)
|
|
|
|
eval_result = (
|
|
client.evals.evaluate(
|
|
dataset=eval_input_df,
|
|
metrics=[vertexai.types.Metric(name='exact_match')],
|
|
config={"dest": eval_dest} if eval_dest else None
|
|
)
|
|
)
|
|
|
|
exact_match_scores = [
|
|
case.response_candidate_results[0].metric_results["exact_match"].score
|
|
for case in eval_result.eval_case_results
|
|
]
|
|
|
|
# Include the original request and the exact match score back into the
|
|
# input DataFrame.
|
|
|
|
eval_input_df["exact_match"] = exact_match_scores
|
|
eval_input_df["request"] = eval_dataset["request"]
|
|
|
|
# Select and reorder columns for the final results table
|
|
results_df = eval_input_df[
|
|
["img_path", "response", "reference", "exact_match"]
|
|
]
|
|
|
|
return eval_result, results_df
|
|
|
|
# if __name__ == "__main__":
|
|
# run_evaluation()
|