|
|
""" |
|
|
Instruction Tuning of LLM for Trait-conditioned Style Impact Caliberation |
|
|
""" |
|
|
|
|
|
import spaces |
|
|
import yaml |
|
|
import pandas as pd |
|
|
import os |
|
|
from PIL import Image |
|
|
import gradio as gr |
|
|
|
|
|
from utils import convert_to_base64, load_config, process_trait_info |
|
|
from tqdm import tqdm |
|
|
from termcolor import colored |
|
|
|
|
|
import random |
|
|
import numpy as np |
|
|
import random |
|
|
|
|
|
|
|
|
import torch |
|
|
device = torch.device('cuda') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TRAIT_VALUES = { |
|
|
"Gender": [ |
|
|
"Male", "Female", "Non-binary/third gender", "Leave Blank", |
|
|
], |
|
|
"Age": [ |
|
|
"18–24", "25–34", "35–44", "45–54", "55–64", "65 or older", "Leave Blank", |
|
|
], |
|
|
"Current Profession": [ |
|
|
"Healthcare/Medical", "Government/Public Service", |
|
|
"Business/Finance", |
|
|
"Technology/Engineering", "Education", "Arts/Entertainment", |
|
|
"Retail/Hospitality/Food Service", |
|
|
"Skilled Trades/Labor (e.g., construction, electrician, landscaper, house cleaner)", |
|
|
"Student", |
|
|
"Unemployed/Looking for work", "Retired", |
|
|
"Other", |
|
|
"Leave Blank", |
|
|
], |
|
|
"Race/Ethnicity" : [ |
|
|
"Asian", "Black/African American", "Hispanic/Latino", |
|
|
"Native American/Alaska Native", "Native Hawaiian/Other Pacific Islander", |
|
|
"White/Caucasian", "Other", "Leave Blank", |
|
|
], |
|
|
"Religious/Cultural Group": [ |
|
|
"Christianity", "Islam", "Hinduism", "Judaism", "Buddhism", "None of the above", "Leave Blank", |
|
|
], |
|
|
"Political Affiliation": [ |
|
|
"Conservative", "Apolitical/Not involved in politics", "Independent", |
|
|
"Libertarian", "Moderate", "Liberal", "Leave Blank", |
|
|
], |
|
|
"Highest Education": [ |
|
|
"Less than high school", "High school diploma or equivalent", "Some college, no degree", |
|
|
"Associate’s degree", "Bachelor’s degree", |
|
|
"Master’s degree", "Doctoral or professional degree", |
|
|
"Leave Blank", |
|
|
], |
|
|
"Annual Household Income": [ |
|
|
"Less than $25,000", "$25,000–$49,999", "$50,000–$74,999", |
|
|
"$75,000–$99,999", "$100,000–$149,999", "$150,000 or more", |
|
|
"Leave Blank", |
|
|
], |
|
|
"Family Status": [ |
|
|
"Single, living alone", "Single, living with family", "Single Parent with children", |
|
|
"Married/Partnered, no children", "Married/Partnered, with children", |
|
|
"Multi-generation family (e.g., with parents, grandparents, or extended family)", |
|
|
"Leave Blank", |
|
|
], |
|
|
} |
|
|
|
|
|
HEALTH_TOPICS = { |
|
|
"Chronic Obstructive Pulmonary Disease (COPD)": "COPD1.1", |
|
|
"Heart Disease": "HD1", |
|
|
"HIV": "HIV1.1", |
|
|
"Mental Health": "MH1.1", |
|
|
"Nutrition": "N2.1", |
|
|
"Substance Abuse": "SA4.1", |
|
|
"Sexual Practice": "SP7.1", |
|
|
"Vaccination": "V7.1", |
|
|
"Cystic Fibrosis": "CF1.1", |
|
|
} |
|
|
|
|
|
health_topics = "" |
|
|
for topic in HEALTH_TOPICS: |
|
|
health_topics += topic + '\n' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
style_variants = [ |
|
|
"Write with a slightly informal and reflective tone.", |
|
|
"Write in a straightforward conversational tone.", |
|
|
"Write with mild emotional coloring, but still natural.", |
|
|
"Write in a calm, matter-of-fact tone.", |
|
|
"Write in a slightly narrative, flowing tone.", |
|
|
"Write in a concise but personable tone.", |
|
|
"Write in a informal, pragmatic tone, focusing on clarity and utility.", |
|
|
] |
|
|
|
|
|
lexical_flavors = [ |
|
|
"Feel free to vary sentence structures slightly.", |
|
|
"Use a mix of simple and slightly complex sentences.", |
|
|
"Use a light mix of paraphrasing expressions.", |
|
|
"Feel free to choose different synonyms for common emotional words.", |
|
|
"Introduce subtle variation in connectors like 'however', 'still', or 'overall'.", |
|
|
] |
|
|
openers = [ |
|
|
"This message", |
|
|
"From this message", |
|
|
"Through the message", |
|
|
"After seeing this message", |
|
|
"Looking at this poster", |
|
|
"Based on what this poster conveys", |
|
|
"Hmmm I think that this message", |
|
|
"Reflecting on the message here", |
|
|
"Considering what this poster is trying to say", |
|
|
"Seeing this message makes me think", |
|
|
"Thinking about what this poster is communicating", |
|
|
"After reading what's on here", |
|
|
"Based on what’s written here", |
|
|
"After I look at this whole thing", |
|
|
] |
|
|
openers_generic = [ |
|
|
"Hmmm when thinking about", |
|
|
"When I think about", |
|
|
"My impression about", |
|
|
"On top of my head", |
|
|
"My general thoughts about", |
|
|
"The way I see it,", |
|
|
"From my point of view on", |
|
|
"My initial take on", |
|
|
"In my own words,", |
|
|
"As I see things,", |
|
|
"Just speaking for myself,", |
|
|
"At a glance,", |
|
|
] |
|
|
openers_poster_summary = [ |
|
|
"This poster", |
|
|
"This poster seems to", |
|
|
"My interpretation of the poster is", |
|
|
"From what this poster shows, it seems to", |
|
|
"Looking at the poster as a whole, it appears to", |
|
|
"Based on the imagery and tone, the poster seems to", |
|
|
"Visually, the poster comes across as trying to", |
|
|
"To me, this poster is trying to", |
|
|
"When I look at this poster, it feels like it aims to", |
|
|
"The poster gives me the impression that it intends to", |
|
|
] |
|
|
openers_explain = [ |
|
|
"The reason why I think that is because", |
|
|
"To explain why I", |
|
|
"Well, to explain my thoughts", |
|
|
"To put it simply, I feel this way because", |
|
|
"My reasoning behind that is", |
|
|
"What leads me to that view is", |
|
|
"A big part of why I think that is", |
|
|
"To give some context for my view,", |
|
|
"Here’s why I lean that way:", |
|
|
"I see it that way mainly because", |
|
|
"Let me explain why I think so", |
|
|
"Thinking through it, I realize it's because", |
|
|
"To unpack my thinking a bit,", |
|
|
"I guess it’s because", |
|
|
"The thing that really shapes my view is", |
|
|
"It’s pretty much because", |
|
|
"A lot of it comes down to", |
|
|
"I feel that way mostly because", |
|
|
"My thinking comes from the idea that", |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
""" |
|
|
Generate LLM response given a single user prompt and input image |
|
|
""" |
|
|
@spaces.GPU |
|
|
def vlm_response(user_input, history, health_topic, |
|
|
gender, age, profession, race, religion, |
|
|
political, education, income, family_status, |
|
|
|
|
|
): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
global model, tokenizer |
|
|
global device |
|
|
|
|
|
import unsloth |
|
|
from unsloth import FastVisionModel, FastModel, FastLanguageModel |
|
|
from unsloth.chat_templates import get_chat_template |
|
|
from unsloth.chat_templates import standardize_sharegpt |
|
|
from transformers import TextStreamer |
|
|
from transformers import TextIteratorStreamer |
|
|
|
|
|
from langchain_community.chat_models import ChatOllama |
|
|
from langchain_core.messages import SystemMessage, HumanMessage |
|
|
from langchain_ollama import OllamaEmbeddings |
|
|
from langchain_core.output_parsers import StrOutputParser |
|
|
from pydantic import BaseModel |
|
|
import threading |
|
|
print(torch.cuda.is_available()) |
|
|
if model is None or tokenizer is None: |
|
|
|
|
|
model, tokenizer = FastVisionModel.from_pretrained( |
|
|
model_name=cfgs["model"], |
|
|
load_in_4bit=True, |
|
|
) |
|
|
FastVisionModel.for_inference(model, use_compiled=False) |
|
|
if "gemma" in cfgs["model"]: |
|
|
|
|
|
tokenizer = get_chat_template( |
|
|
tokenizer, |
|
|
chat_template = "gemma-3", |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
torch.cuda.empty_cache() |
|
|
|
|
|
|
|
|
|
|
|
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True) |
|
|
|
|
|
|
|
|
""" [NOTE] we have not use `history` for this generation """ |
|
|
|
|
|
image = Image.open(user_input['files'][0]) if user_input['files'] else None |
|
|
image_uploaded = True |
|
|
if image is None: |
|
|
image = Image.new('RGB', (24,24)) |
|
|
image_uploaded = False |
|
|
|
|
|
print(health_topic) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demo_dict = { |
|
|
"Gender": gender, |
|
|
"Age": age, |
|
|
"Current Profession": profession, |
|
|
"Race/Ethnicity": race, |
|
|
"Religious/Cultural Group": religion, |
|
|
"Political Affiliation": political, |
|
|
"Highest Education": education, |
|
|
"Annual Household Income": income, |
|
|
"Family Status": family_status, |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demo_info = "" |
|
|
for trait, value in demo_dict.items(): |
|
|
if value != "Leave Blank": |
|
|
demo_info += f"{trait}: {value}\n" |
|
|
else: |
|
|
demo_info += f"{trait}: [Not specified]\n" |
|
|
persona_score = "" |
|
|
persona_score += "Big-Five Trait Scores:\n" |
|
|
|
|
|
|
|
|
|
|
|
locus = None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demo_info, persona_score, locus = process_trait_info( |
|
|
demo_info, persona_score, locus, |
|
|
demo_full=False, include_big5=True, |
|
|
include_facet=False, include_locus=False, |
|
|
train_mode=False, |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
style_hint = random.choice(style_variants) |
|
|
lexical_hint = random.choice(lexical_flavors) |
|
|
opening_phrase = random.choice(openers) |
|
|
opening_generic = random.choice(openers_generic) |
|
|
opening_poster = random.choice(openers_poster_summary) |
|
|
opening_explain = random.choice(openers_explain) |
|
|
print('Style:', style_hint) |
|
|
print('Lexical:', lexical_hint) |
|
|
print('Opening:', opening_phrase) |
|
|
print('Generic opening:', opening_generic) |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
if image_uploaded: |
|
|
"""############################################################### |
|
|
Case 1: a health poster is uploaded |
|
|
=> VLM-enabled response prediction to that specific poster |
|
|
###############################################################""" |
|
|
|
|
|
|
|
|
|
|
|
yield "Analyzing image content..." |
|
|
|
|
|
PROMPT = ( |
|
|
f"Describe the content and main message in given heatlh campaign poster and how it's related to {health_topic}. ", |
|
|
"Note that the message could be non-direct or subtle (e.g. irony, fear-driven evoke without explicit texts, etc). Only provide the answer (in 2-4 sentences). ", |
|
|
f"Start the response with {opening_poster}" |
|
|
) |
|
|
messages = [ |
|
|
{"role": "user", "content": [ |
|
|
{"type": "image"}, |
|
|
{"type": "text", "text": PROMPT} |
|
|
]} |
|
|
] |
|
|
input_text = tokenizer.apply_chat_template(messages, add_generation_prompt = True) |
|
|
inputs = tokenizer( |
|
|
image.convert("RGB"), |
|
|
input_text, |
|
|
add_special_tokens = False, |
|
|
return_tensors = "pt", |
|
|
).to(device) |
|
|
|
|
|
gen_tokens = model.generate( |
|
|
**inputs, |
|
|
max_new_tokens = 512, |
|
|
use_cache = True, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
top_k=15, |
|
|
temperature=0.8, |
|
|
do_sample=True, |
|
|
) |
|
|
outs = tokenizer.batch_decode(gen_tokens[:, inputs.input_ids.shape[1]:])[0] |
|
|
image_desc = outs.replace(tokenizer.eos_token, "") |
|
|
image_desc = image_desc.replace("<end_of_turn>", "") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SYSTEM_PROMPT = cfg_prompts["SYSTEM_SIM"] |
|
|
SIM_PROMPT = "" |
|
|
|
|
|
SIM_PROMPT += f"You are: Demographics:\n{demo_info}\n" |
|
|
|
|
|
|
|
|
|
|
|
SIM_PROMPT += cfg_prompts["SIMULATION_SIM"] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert cfgs["infer_engine"] == "unsloth", "Only unsloth inference is supported" |
|
|
assert cfgs["vision"] == True, "Must have vision input" |
|
|
|
|
|
df = pd.read_csv(os.path.expandvars(cfgs["data_path"])) |
|
|
|
|
|
sample = df[df['Poster_id'] == HEALTH_TOPICS[health_topic]].iloc[0] |
|
|
del df |
|
|
""" Iterate through each question""" |
|
|
|
|
|
answers_numeric = "" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(1,16,1): |
|
|
|
|
|
col = f"Q{i}" |
|
|
if pd.isna(sample[col]): |
|
|
continue |
|
|
question = sample[col].replace("\n", " ") |
|
|
|
|
|
if "type in" in question.lower(): |
|
|
continue |
|
|
elif "make you feel" in question.lower(): |
|
|
continue |
|
|
elif "how open" in question.lower(): |
|
|
continue |
|
|
|
|
|
|
|
|
USER_PROMPT = SIM_PROMPT |
|
|
USER_PROMPT += f"Question: {question}\n\n" |
|
|
|
|
|
USER_PROMPT += cfg_prompts['INSTRUCTION_MCQ'] |
|
|
|
|
|
messages = [ |
|
|
{"role": "user", "content": [ |
|
|
{"type": "image"}, |
|
|
{"type": "text", "text": SYSTEM_PROMPT + USER_PROMPT} |
|
|
]} |
|
|
] |
|
|
input_text = tokenizer.apply_chat_template(messages, add_generation_prompt = True) |
|
|
inputs = tokenizer( |
|
|
image.convert("RGB"), |
|
|
input_text, |
|
|
add_special_tokens = False, |
|
|
return_tensors = "pt", |
|
|
).to(device) |
|
|
|
|
|
gen_tokens = model.generate( |
|
|
**inputs, |
|
|
max_new_tokens = 16, |
|
|
use_cache = True, |
|
|
do_sample=cfgs["stochastic"], |
|
|
temperature=cfgs["temperature"], |
|
|
min_p=0.9, |
|
|
) |
|
|
outs = tokenizer.batch_decode(gen_tokens[:, inputs.input_ids.shape[1]:])[0] |
|
|
answer = outs.replace(tokenizer.eos_token, "") |
|
|
answer = answer.replace("<end_of_turn>", "") |
|
|
|
|
|
answers_numeric += f"{question}. Your answer: {answer}\n" |
|
|
|
|
|
print(answers_numeric) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SYSTEM_PROMPT = "You are a helpful assistant." |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
USER_PROMPT = ( |
|
|
"Summarize the following survey responses into a short, natural paragraph that captures your overall sentiment, motivation, and thinking. " |
|
|
f"Write as if paraphrasing what a person might say in conversation. Adjust your style based on your demographic/personality traits." |
|
|
"Do NOT repeat numeric scores. " |
|
|
"Preserve polarity: low scores → low concern/motivation/openness; high scores → high concern/motivation/openness. " |
|
|
"If answers are mixed (e.g., believes something is harmful but isn't personally moved), reflect that nuance explicitly. " |
|
|
"Keep to 1-5 sentences.\n\n" |
|
|
|
|
|
"**STRICTLY FOLLOW THESE RULES:**\n" |
|
|
"- Infer direction from each item's Scale description (e.g., 1-9: higher = more; 0-6: higher = more). " |
|
|
"- Use calibrated wording: 1-2 = very low, 3-4 = low, 5 = moderate, 6-7 = high, 8-9 = very high; for 0-6: 0-1 = not/slight, 2-3 = somewhat, 4-5 = high, 6 = very. " |
|
|
"- VERY IMPORTANT: provide ONLY the final summarized response, without anything else!" |
|
|
f"- The response MUST have a consistent health topic: {health_topic}. Ground each sentence to the impact of campaign message.\n" |
|
|
"- Never invert sentiment. Prefer hedged phrases (e.g., “not particularly,” “only somewhat,” “very open,” “not open at all”).\n\n" |
|
|
f"- Mimic the talking style of emulated demographic as realistic as possible." |
|
|
|
|
|
"**Example input 1:**\n" |
|
|
"The message makes me more concerned about the health risks of poor eating habits - Scale: 1-9. Your answer: 9\n" |
|
|
"The message motivates me to make healthy eating choices - Scale: 1-9. Your answer: 9\n" |
|
|
"In your opinion, how harmful is neglecting proper nutrition and weight management to your overall health? - Scale: 0–6. Your answer: 5\n" |
|
|
"How open are you to adopting healthier eating habits and lifestyle changes? - Scale: 1-9. Your answer: 9\n" |
|
|
"**Example output 1:**\n" |
|
|
"This message really heightened my awareness of how unhealthy eating can be. The content in the message strongly motivates me to make better choices, and I feel very ready to follow through.\n\n" |
|
|
|
|
|
"**Example input 2:**\n" |
|
|
"The message makes me more concerned about the health risks of COPD and smoking - Scale: 1-9. Your answer: 1\n" |
|
|
"The message motivates me to not smoke. - Scale: 1-9. Your answer: 1\n" |
|
|
"In your opinion, how harmful is smoking to your general health? - Scale: 0-6. Your answer: 6\n" |
|
|
"How open are you to smoking in the future? - Scale: 1-9. Your answer: 1\n" |
|
|
"**Example output 2:**\n" |
|
|
"From this message, I recognize smoking is very harmful, but the content in the message didn't increase my concern or motivate me much. It does somewhat make me understand that smoking is harmful, however. Anyway, I'm not open to smoking in the future.\n\n" |
|
|
|
|
|
"**Example input 3:**\n" |
|
|
"The message makes me more concerned about the effects of lack of exercise - Scale: 1-9. Your answer: 4\n" |
|
|
"The message motivates me to be more active - Scale: 1-9. Your answer: 3\n" |
|
|
"How open are you to exercising regularly? - Scale: 1-9. Your answer: 4\n" |
|
|
"**Example output 3:**\n" |
|
|
"Through the message, I get that exercise matters and the message raised my awareness a bit, but the poster content itself didn't really motivate me. The content in the message has some small impact in motivating me to change my routine.\n\n" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
f"Start the response with '{opening_phrase}' (Style hint: {style_hint}; Lexical hint: {lexical_hint})\n" |
|
|
f"Input: {answers_numeric}. " |
|
|
) |
|
|
|
|
|
|
|
|
messages = [ |
|
|
{"role": "user", "content": [ |
|
|
|
|
|
{"type": "text", "text": SYSTEM_PROMPT + USER_PROMPT} |
|
|
]} |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
input_text = tokenizer.apply_chat_template(messages, add_generation_prompt = True) |
|
|
inputs = tokenizer( |
|
|
|
|
|
input_text, |
|
|
add_special_tokens = False, |
|
|
return_tensors = "pt", |
|
|
).to(device) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generate_kwargs = dict( |
|
|
**inputs, |
|
|
streamer=streamer, |
|
|
max_new_tokens=512, |
|
|
use_cache=True, |
|
|
|
|
|
top_k=15, |
|
|
temperature=0.8, |
|
|
do_sample=True, |
|
|
) |
|
|
|
|
|
thread = threading.Thread( |
|
|
target=model.generate, |
|
|
kwargs=generate_kwargs |
|
|
) |
|
|
thread.start() |
|
|
|
|
|
outputs = [ |
|
|
f"Emulated traits:\n {demo_info}\n" + '='*20 + "\n\n", |
|
|
image_desc + "\n\n" |
|
|
] |
|
|
for new_token in streamer: |
|
|
outputs.append(new_token) |
|
|
final_output = ''.join(outputs) |
|
|
yield final_output |
|
|
|
|
|
|
|
|
thread.join() |
|
|
|
|
|
|
|
|
response = "".join(outputs[2:]) |
|
|
print(colored('Traits', 'green'), demo_info) |
|
|
print(colored('Emulated response:', 'green'), response) |
|
|
print('='*100) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SYSTEM_PROMPT = cfg_prompts["SYSTEM_SIM"] |
|
|
SIM_PROMPT = "" |
|
|
|
|
|
SIM_PROMPT += f"You are: Demographics:\n{demo_info}\n" |
|
|
|
|
|
|
|
|
|
|
|
SIM_PROMPT += cfg_prompts["SIMULATION_SIM"] |
|
|
SIM_PROMPT += ( |
|
|
f"After seeing the uploaded impage, your response were {response}. " |
|
|
"Briefly explain WHY you responded that way, based on your demographic background. " |
|
|
f"Keep the explanation concise and direct. Start the response with '{opening_explain}' " |
|
|
f"(Style hint: {style_hint}, concise; Lexical hint: {lexical_hint}). " |
|
|
"Afterward, give a few *generic and succinct* suggestions to improve the poster's persuasiveness." |
|
|
) |
|
|
USER_PROMPT = SIM_PROMPT |
|
|
|
|
|
|
|
|
messages = [ |
|
|
{"role": "user", "content": [ |
|
|
{"type": "image"}, |
|
|
{"type": "text", "text": SYSTEM_PROMPT + USER_PROMPT} |
|
|
]} |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
input_text = tokenizer.apply_chat_template(messages, add_generation_prompt = True) |
|
|
inputs = tokenizer( |
|
|
image.convert("RGB"), |
|
|
input_text, |
|
|
add_special_tokens = False, |
|
|
return_tensors = "pt", |
|
|
).to(device) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generate_kwargs = dict( |
|
|
**inputs, |
|
|
streamer=streamer, |
|
|
max_new_tokens=512, |
|
|
use_cache=True, |
|
|
min_p=0.85, |
|
|
temperature=0.1, |
|
|
do_sample=True, |
|
|
) |
|
|
|
|
|
thread = threading.Thread( |
|
|
target=model.generate, |
|
|
kwargs=generate_kwargs |
|
|
) |
|
|
thread.start() |
|
|
|
|
|
|
|
|
outputs += ["\n"] |
|
|
for new_token in streamer: |
|
|
outputs.append(new_token) |
|
|
final_output = ''.join(outputs) |
|
|
yield final_output |
|
|
|
|
|
thread.join() |
|
|
|
|
|
|
|
|
return answer |
|
|
else: |
|
|
"""############################################################### |
|
|
Case 2: no health poster is uploaded |
|
|
=> General Response to the health topic |
|
|
=> not conditioned on any particular health poster |
|
|
###############################################################""" |
|
|
|
|
|
|
|
|
|
|
|
SYSTEM_PROMPT = ( |
|
|
"You are a person with unique demographic and personality traits. " |
|
|
"Based on your background, you naturally have thoughts, feelings, and reactions to what you see." |
|
|
) |
|
|
SIM_PROMPT = "" |
|
|
|
|
|
SIM_PROMPT += f"You are: {demo_info}\n" |
|
|
|
|
|
|
|
|
|
|
|
SIM_PROMPT += f"You are being asked a general question to share your *general* opinions and beliefs about a given health topic.\n" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert cfgs["infer_engine"] == "unsloth", "Only unsloth inference is supported" |
|
|
USER_PROMPT = SIM_PROMPT |
|
|
USER_PROMPT += ( |
|
|
f"What are your *general* thoughts and opinions about the {health_topic} health topic? " |
|
|
f" What's your attitude and feeling when talking about {health_topic} in general and why?" |
|
|
f" How familiar are you with {health_topic}? How much do you care or know about it?" |
|
|
f" Do you think {health_topic} is an important topic to talk about?" |
|
|
f" What is its impacts and importance {health_topic} in society and your life? Why?" |
|
|
f" Do you have any strong opinions about it?" |
|
|
f" Are you interested in learning more about it?" |
|
|
) |
|
|
|
|
|
USER_PROMPT += ( |
|
|
"Your personality, locus of control, and demographic traits influence your response. Adjust your style based on your demographic personality traits.\n" |
|
|
"**STRICTLY FOLLOW THESE RULES:**\n" |
|
|
"- Human-like, casual, everyday conversational response. Only answer the questions\n" |
|
|
f"- The response MUST have a consistent health topic: {health_topic}.\n" |
|
|
|
|
|
"- Only provide the answer. DO NOT REPEAT THE PROMPT!\n" |
|
|
"- Condition your response on your *demographic/personality traits provided earlier, IGNORING the [Not specified] ones*.\n" |
|
|
"- MUST provide *reasonable* and *informative* answers aligned with your background." |
|
|
f"- Start the response with '{opening_generic}' ; {style_hint} {lexical_hint}\n" |
|
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
messages = [ |
|
|
{"role": "user", "content": SYSTEM_PROMPT + USER_PROMPT} |
|
|
] |
|
|
assert "gemma" in cfgs["model"], "Currently only gemma model is supported for no-image input" |
|
|
input_text = tokenizer.apply_chat_template(messages, add_generation_prompt = True) |
|
|
inputs = tokenizer( |
|
|
input_text, |
|
|
add_special_tokens = False, |
|
|
return_tensors = "pt", |
|
|
).to(device) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generate_kwargs = dict( |
|
|
**inputs, |
|
|
streamer=streamer, |
|
|
max_new_tokens=512, |
|
|
use_cache=True, |
|
|
|
|
|
top_k=15, |
|
|
temperature=0.8, |
|
|
do_sample=True, |
|
|
) |
|
|
|
|
|
thread = threading.Thread( |
|
|
target=model.generate, |
|
|
kwargs=generate_kwargs |
|
|
) |
|
|
thread.start() |
|
|
|
|
|
outputs = [f"Emulated traits:\n {demo_info}\n" + '='*20 + "\n\n"] |
|
|
for new_token in streamer: |
|
|
outputs.append(new_token) |
|
|
final_output = ''.join(outputs) |
|
|
yield final_output |
|
|
thread.join() |
|
|
|
|
|
except GeneratorExit: |
|
|
print("User disconnected. Waiting for generation to complete...") |
|
|
finally: |
|
|
torch.cuda.empty_cache() |
|
|
|
|
|
"""########################################################################### |
|
|
Evaluate a given model (specified in model_cfgs) |
|
|
on posters with given test_style |
|
|
|
|
|
Args: |
|
|
+ cfgs : specify model type (e.g. gemma or llama), |
|
|
data source, and export paths |
|
|
+ prompts : set of prompts |
|
|
|
|
|
Outputs: |
|
|
=> save model in cfgs["export_path"] (CSV file) |
|
|
+ if cfgs["export_path"] not exists, initialize it with cfgs["data_path"] |
|
|
=> original survey data with ground-truth responses |
|
|
+ add column "<model>:<version>": store AI-simulated responses |
|
|
+ support concurrent evaluation on different jobs |
|
|
##########################################################################""" |
|
|
if __name__ == '__main__': |
|
|
"""========================================== |
|
|
1. load model settings & prompts format |
|
|
==========================================""" |
|
|
|
|
|
|
|
|
|
|
|
model_cfg = "./configs/task1_demo_sph.yaml" |
|
|
prompt_cfg = "./configs/prompts.yaml" |
|
|
cfgs = load_config(model_cfg) |
|
|
cfg_prompts = load_config(prompt_cfg) |
|
|
|
|
|
"""========================================== |
|
|
2. Evaluate model defined in configs |
|
|
==========================================""" |
|
|
print(colored('MODEL USE:', 'green'), cfgs["model"]) |
|
|
|
|
|
|
|
|
|
|
|
"""=============================== |
|
|
3. Initialize model |
|
|
=> `model`, `tokenizer` |
|
|
are initialized here |
|
|
===============================""" |
|
|
assert cfgs["infer_engine"] == "unsloth", "Only unsloth inference is supported" |
|
|
assert cfgs["vision"] == True, "Must have vision input" |
|
|
model, tokenizer = None, None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""============================================= |
|
|
4. User-input Dropdown Traits |
|
|
=============================================""" |
|
|
|
|
|
|
|
|
|
|
|
CSS = """ |
|
|
.center { align-items: center; text-align: center; } |
|
|
""" |
|
|
with gr.Blocks(theme="gradio/dark", css=CSS) as interface: |
|
|
|
|
|
LOGO_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "assets/umd_logo.png")) |
|
|
gr.Image(value=LOGO_PATH, show_label=False, interactive=False, height=100) |
|
|
gr.Markdown( |
|
|
""" |
|
|
<h1 style="text-align: center; margin-bottom: 0.5em;"> |
|
|
UMD's AI-Empowered Response Prediction in Public Health Messaging |
|
|
</h1> |
|
|
""", |
|
|
elem_classes=["center"] |
|
|
) |
|
|
gr.Markdown( |
|
|
""" |
|
|
<hr style="margin-top: 0.8em; margin-bottom: 0.8em;"> <!-- thinner spacing around line --> |
|
|
|
|
|
<h2 style="text-align: center; margin-top: 0.3em; margin-bottom: 0.6em;"> |
|
|
User Guide |
|
|
</h2> |
|
|
""", |
|
|
elem_classes=["center"] |
|
|
) |
|
|
gr.Markdown( |
|
|
""" |
|
|
<ul style="text-align: left; max-width: 800px; margin: auto;"> |
|
|
<li>This program emulates <b>demographic- and personality-conditioned responses</b> to public health posters using our trait-aligned Vision-Language Model (VLM).</li> |
|
|
<li>To begin, (1) specify the target demographic traits, then (2) upload a public health poster to predict responses.</li> |
|
|
<li>If a health poster is uploaded, the model first summarizes its understanding of the image.</li> |
|
|
<li><b>Please note:</b> |
|
|
<ul> |
|
|
<li>Each interaction only uses the uploaded image and selected traits (no conversation history).</li> |
|
|
<li>You don’t need to type any text prompt; just upload the Health Poster and click <b>Submit</b>.</li> |
|
|
<li>If no poster or image is uploaded, the program automatically generates the emulated person’s <b>general opinion</b> on the selected Health Topic.</li> |
|
|
<li>Please do not interrupt the generation process as it can lead to unexpected results. In case it happens, simply refresh the web app.</li> |
|
|
<li><b>Limitation:</b> The model may generate less realistic emulations to some under-represented demographics in the survey dataset (e.g., Asian seniors). We are conducting more comprehensive survey to effectively address this limitation.</li> |
|
|
</ul> |
|
|
</li> |
|
|
</ul> |
|
|
|
|
|
<hr style="margin-top: 0.8em; margin-bottom: 1.2em;"> |
|
|
""", |
|
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<script> |
|
|
window.onload = function() { |
|
|
window.scrollTo({ top: 0, behavior: 'smooth' }); |
|
|
} |
|
|
</script> |
|
|
""") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gr.Markdown("## 1. Please specify the target demographic traits to be emulated here:") |
|
|
|
|
|
with gr.Row(): |
|
|
gender = gr.Dropdown( |
|
|
label="Gender", |
|
|
choices=TRAIT_VALUES["Gender"], |
|
|
allow_custom_value=False, |
|
|
value="Female", |
|
|
) |
|
|
age = gr.Dropdown( |
|
|
label="Age", |
|
|
choices=TRAIT_VALUES["Age"], |
|
|
allow_custom_value=False, |
|
|
value="25–34", |
|
|
) |
|
|
profession = gr.Dropdown( |
|
|
label="Current Profession", |
|
|
choices=TRAIT_VALUES["Current Profession"], |
|
|
allow_custom_value=False, |
|
|
value="Student", |
|
|
) |
|
|
with gr.Row(): |
|
|
race = gr.Dropdown( |
|
|
label="Race/Ethnicity", |
|
|
choices=TRAIT_VALUES["Race/Ethnicity"], |
|
|
allow_custom_value=False, |
|
|
value="White/Caucasian", |
|
|
) |
|
|
religion = gr.Dropdown( |
|
|
label="Religious/Cultural Group", |
|
|
choices=TRAIT_VALUES["Religious/Cultural Group"], |
|
|
allow_custom_value=False, |
|
|
value="Leave Blank", |
|
|
) |
|
|
political = gr.Dropdown( |
|
|
label="Political Affiliation", |
|
|
choices=TRAIT_VALUES["Political Affiliation"], |
|
|
allow_custom_value=False, |
|
|
value="Leave Blank", |
|
|
) |
|
|
with gr.Row(): |
|
|
education = gr.Dropdown( |
|
|
label="Highest Education", |
|
|
choices=TRAIT_VALUES["Highest Education"], |
|
|
allow_custom_value=False, |
|
|
value="Leave Blank", |
|
|
) |
|
|
income = gr.Dropdown( |
|
|
label="Annual Household Income", |
|
|
choices=TRAIT_VALUES["Annual Household Income"], |
|
|
allow_custom_value=False, |
|
|
value="$75,000–$99,999", |
|
|
) |
|
|
family_status = gr.Dropdown( |
|
|
label="Family Status", |
|
|
choices=TRAIT_VALUES["Family Status"], |
|
|
allow_custom_value=False, |
|
|
value="Leave Blank" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gr.Markdown("## 2. Please specify the main Health Topic of the poster here:") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
health_topic = gr.Dropdown( |
|
|
label="Health Topic", |
|
|
choices=HEALTH_TOPICS, |
|
|
allow_custom_value=False, |
|
|
) |
|
|
gr.Column(scale=1) |
|
|
|
|
|
|
|
|
|
|
|
gr.Markdown("## 3. Upload Public Health Poster here (if no poster is uploaded, the model emulates General Response to the topic):") |
|
|
gr.Markdown(""" |
|
|
#### ▶️ Use Case 1: Poster-Based Response |
|
|
+ Upload **only one** poster image — the first file is the one processed. |
|
|
+ The model has **no memory**, so re-upload the image for each new request. |
|
|
+ Must choose a **Health Topic** that matches the poster content for best results. |
|
|
+ No text prompt is needed: upload the poster and click **Submit**. |
|
|
#### ▶️ Use Case 2: General Response (No Poster) |
|
|
+ Simply select a Health Topic and click **Send**. |
|
|
""" |
|
|
) |
|
|
gr.Markdown(""" |
|
|
### 📘 Important Notes |
|
|
- ⏳ **The first request takes longer time** since the model is being loaded into memory. Please be patient. |
|
|
- ⚠️ **Do not interrupt the generation process.** Stopping midway may cause backend issues. Please allow the response to complete. |
|
|
- 🏷️ Before uploading a poster, select its **corresponding health topic**. |
|
|
- 🎯 For the best experience, ensure the **topic accurately matches the poster content**. |
|
|
- 🧩 If you choose not to upload a poster, the model will produce a **general, trait-conditioned response** for the selected topic. |
|
|
""") |
|
|
chat = gr.ChatInterface( |
|
|
fn=vlm_response, |
|
|
multimodal=True, |
|
|
title=f"Vision-Language Model: Trait-Conditioned Response Emulation", |
|
|
type="messages", |
|
|
additional_inputs=[ |
|
|
health_topic, gender, age, profession, race, religion, |
|
|
political, education, income, family_status, |
|
|
|
|
|
], |
|
|
chatbot=gr.Chatbot(height=500), |
|
|
autofocus=False, |
|
|
) |
|
|
|
|
|
"""============================================= |
|
|
5. Chat Interface Launch |
|
|
=============================================""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface.launch() |