implement Z-number decision matrix extraction and MCDM methods; add command line argument parsing
Browse files- main_simple.py +155 -6
- utils.py +5 -1
main_simple.py
CHANGED
|
@@ -1,7 +1,87 @@
|
|
| 1 |
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TextStreamer
|
| 2 |
import torch
|
| 3 |
import warnings
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
warnings.filterwarnings(
|
| 7 |
"ignore",
|
|
@@ -9,6 +89,14 @@ warnings.filterwarnings(
|
|
| 9 |
category=UserWarning,
|
| 10 |
)
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
qconfig = BitsAndBytesConfig(
|
| 13 |
load_in_8bit=True,
|
| 14 |
)
|
|
@@ -24,12 +112,9 @@ model = AutoModelForCausalLM.from_pretrained(
|
|
| 24 |
)
|
| 25 |
print("Model loaded successfully!\n")
|
| 26 |
|
| 27 |
-
USER_QUERY = "Okay so I've been going crazy trying to figure out if I should get a dog, a cat, or maybe just stick with a fish tank, and honestly I'm not even sure what matters most to me anymore. Like, companionship is huge for me, probably the most important thing, and I'm pretty confident dogs are amazing for that, cats are okay but kind of aloof sometimes, fish are basically just decoration let's be real. Then there's the whole time commitment thing which I think matters a lot but I'm not totally sure how much—I work weird hours so dogs seem like a nightmare there, cats are supposedly independent but my friend's cat is super needy so who knows, fish are definitely easy but maybe too easy? Cost is something I should care about more than I do, I guess it's moderately important, and I've heard dogs are expensive with vet bills and food, cats are cheaper I think, fish setups can actually cost a lot upfront but then nothing after. Oh and allergies—my roommate might be slightly allergic to cats, we're not 100% sure, dogs seem fine, fish obviously no issue there, so that's pretty important actually. And like, the whole lifestyle flexibility thing where I want to travel sometimes, dogs are terrible for that obviously, cats you can leave for a weekend maybe, fish just need an auto-feeder so that's nice, but I'm not sure how much I'll actually travel so maybe this doesn't matter that much?"
|
| 28 |
-
|
| 29 |
-
|
| 30 |
messages = [
|
| 31 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 32 |
-
{"role": "user", "content":
|
| 33 |
]
|
| 34 |
|
| 35 |
# Prepare the prompt using the chat template
|
|
@@ -42,4 +127,68 @@ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
|
|
| 42 |
streamer = TextStreamer(tokenizer, skip_special_tokens=True)
|
| 43 |
|
| 44 |
# Generate with streaming
|
| 45 |
-
model.generate(**inputs, max_length=8192, streamer=streamer, temperature=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TextStreamer
|
| 2 |
import torch
|
| 3 |
import warnings
|
| 4 |
+
import re
|
| 5 |
+
import logging
|
| 6 |
+
import argparse
|
| 7 |
+
from znum import Znum, Topsis, Promethee, Beast
|
| 8 |
+
from utils import SYSTEM_PROMPT, DEFAULT_QUERY
|
| 9 |
+
|
| 10 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
# Z-number mappings: value/confidence (1-5) to fuzzy trapezoidal numbers
|
| 14 |
+
A_MAP = {
|
| 15 |
+
1: [2, 3, 3, 4],
|
| 16 |
+
2: [4, 5, 5, 6],
|
| 17 |
+
3: [6, 7, 7, 8],
|
| 18 |
+
4: [8, 9, 9, 10],
|
| 19 |
+
5: [10, 11, 11, 12],
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
B_MAP = {
|
| 23 |
+
1: [0.2, 0.3, 0.3, 0.4],
|
| 24 |
+
2: [0.3, 0.4, 0.4, 0.5],
|
| 25 |
+
3: [0.4, 0.5, 0.5, 0.6],
|
| 26 |
+
4: [0.5, 0.6, 0.6, 0.7],
|
| 27 |
+
5: [0.6, 0.7, 0.7, 0.8],
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def parse_znum_pair(pair_str: str) -> Znum | None:
|
| 32 |
+
"""Convert 'N:M' string to Znum object using A_MAP and B_MAP (abs value for A)."""
|
| 33 |
+
try:
|
| 34 |
+
parts = pair_str.strip().split(':')
|
| 35 |
+
if len(parts) != 2:
|
| 36 |
+
return None
|
| 37 |
+
a_val = abs(int(parts[0]))
|
| 38 |
+
b_val = int(parts[1])
|
| 39 |
+
if a_val not in A_MAP or b_val not in B_MAP:
|
| 40 |
+
logger.warning(f"Invalid Z-number pair: {pair_str}")
|
| 41 |
+
return None
|
| 42 |
+
return Znum(A_MAP[a_val], B_MAP[b_val])
|
| 43 |
+
except (ValueError, KeyError) as e:
|
| 44 |
+
logger.warning(f"Failed to parse Z-number pair '{pair_str}': {e}")
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def parse_markdown_table(text: str) -> dict:
|
| 49 |
+
"""Parse markdown table from model output into structured dict."""
|
| 50 |
+
lines = [l.strip() for l in text.strip().split('\n') if l.strip() and '|' in l]
|
| 51 |
+
lines = [l for l in lines if not re.match(r'^\|[-:\s|]+\|$', l)]
|
| 52 |
+
|
| 53 |
+
if len(lines) < 4:
|
| 54 |
+
logger.warning("Table has fewer than expected rows")
|
| 55 |
+
return {}
|
| 56 |
+
|
| 57 |
+
def split_row(row: str) -> list:
|
| 58 |
+
cells = [c.strip() for c in row.split('|')]
|
| 59 |
+
return [c for c in cells if c]
|
| 60 |
+
|
| 61 |
+
headers = split_row(lines[0])
|
| 62 |
+
criteria = headers[1:] if headers else []
|
| 63 |
+
|
| 64 |
+
types_row = split_row(lines[1])
|
| 65 |
+
types = types_row[1:] if len(types_row) > 1 else []
|
| 66 |
+
|
| 67 |
+
weights_row = split_row(lines[-1])
|
| 68 |
+
weights = weights_row[1:] if len(weights_row) > 1 else []
|
| 69 |
+
|
| 70 |
+
alternatives = {}
|
| 71 |
+
for line in lines[2:-1]:
|
| 72 |
+
row = split_row(line)
|
| 73 |
+
if row:
|
| 74 |
+
alt_name = row[0]
|
| 75 |
+
values = row[1:]
|
| 76 |
+
alternatives[alt_name] = values
|
| 77 |
+
|
| 78 |
+
result = {
|
| 79 |
+
'criteria': criteria,
|
| 80 |
+
'types': types,
|
| 81 |
+
'alternatives': alternatives,
|
| 82 |
+
'weights': weights
|
| 83 |
+
}
|
| 84 |
+
return result
|
| 85 |
|
| 86 |
warnings.filterwarnings(
|
| 87 |
"ignore",
|
|
|
|
| 89 |
category=UserWarning,
|
| 90 |
)
|
| 91 |
|
| 92 |
+
# Parse command line arguments
|
| 93 |
+
parser = argparse.ArgumentParser(description='Z-number decision matrix extraction and MCDM')
|
| 94 |
+
parser.add_argument('--method', type=str, choices=['topsis', 'promethee'], default='topsis',
|
| 95 |
+
help='MCDM method to use (default: topsis)')
|
| 96 |
+
parser.add_argument('--query', '-q', type=str, default=DEFAULT_QUERY,
|
| 97 |
+
help='Decision query to process')
|
| 98 |
+
args = parser.parse_args()
|
| 99 |
+
|
| 100 |
qconfig = BitsAndBytesConfig(
|
| 101 |
load_in_8bit=True,
|
| 102 |
)
|
|
|
|
| 112 |
)
|
| 113 |
print("Model loaded successfully!\n")
|
| 114 |
|
|
|
|
|
|
|
|
|
|
| 115 |
messages = [
|
| 116 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 117 |
+
{"role": "user", "content": args.query},
|
| 118 |
]
|
| 119 |
|
| 120 |
# Prepare the prompt using the chat template
|
|
|
|
| 127 |
streamer = TextStreamer(tokenizer, skip_special_tokens=True)
|
| 128 |
|
| 129 |
# Generate with streaming
|
| 130 |
+
output_ids = model.generate(**inputs, max_length=8192, streamer=streamer, temperature=1)
|
| 131 |
+
|
| 132 |
+
# Decode the generated output (excluding input tokens)
|
| 133 |
+
generated_ids = output_ids[0][inputs['input_ids'].shape[1]:]
|
| 134 |
+
generated_text = tokenizer.decode(generated_ids, skip_special_tokens=True)
|
| 135 |
+
|
| 136 |
+
# Parse and log the decision matrix
|
| 137 |
+
logger.info("Parsing decision matrix from model output...")
|
| 138 |
+
matrix = parse_markdown_table(generated_text)
|
| 139 |
+
|
| 140 |
+
if matrix:
|
| 141 |
+
logger.info(f"Criteria: {matrix['criteria']}")
|
| 142 |
+
logger.info(f"Types: {matrix['types']}")
|
| 143 |
+
|
| 144 |
+
# Convert weights to Znum
|
| 145 |
+
znum_weights = [parse_znum_pair(w) for w in matrix['weights']]
|
| 146 |
+
logger.info("Weights as Znum:")
|
| 147 |
+
for i, (name, zw) in enumerate(zip(matrix['criteria'], znum_weights)):
|
| 148 |
+
logger.info(f" {name}: {zw}")
|
| 149 |
+
|
| 150 |
+
# Convert alternatives to Znum
|
| 151 |
+
znum_alternatives = {}
|
| 152 |
+
for alt_name, values in matrix['alternatives'].items():
|
| 153 |
+
znum_values = [parse_znum_pair(v) for v in values]
|
| 154 |
+
znum_alternatives[alt_name] = znum_values
|
| 155 |
+
logger.info(f"Alternative '{alt_name}' as Znum:")
|
| 156 |
+
for i, (crit, zv) in enumerate(zip(matrix['criteria'], znum_values)):
|
| 157 |
+
logger.info(f" {crit}: {zv}")
|
| 158 |
+
|
| 159 |
+
# Store converted data
|
| 160 |
+
matrix['znum_weights'] = znum_weights
|
| 161 |
+
matrix['znum_alternatives'] = znum_alternatives
|
| 162 |
+
|
| 163 |
+
# Build criteria types for MCDM
|
| 164 |
+
criteria_types = [
|
| 165 |
+
Beast.CriteriaType.BENEFIT if t.lower() == 'benefit' else Beast.CriteriaType.COST
|
| 166 |
+
for t in matrix['types']
|
| 167 |
+
]
|
| 168 |
+
|
| 169 |
+
# Build decision table: [weights, *alternatives, criteria_types]
|
| 170 |
+
alt_names = list(znum_alternatives.keys())
|
| 171 |
+
alt_rows = [znum_alternatives[name] for name in alt_names]
|
| 172 |
+
table = [znum_weights] + alt_rows + [criteria_types]
|
| 173 |
+
|
| 174 |
+
# Apply MCDM method
|
| 175 |
+
logger.info(f"\nApplying {args.method.upper()} method...")
|
| 176 |
+
|
| 177 |
+
if args.method == 'topsis':
|
| 178 |
+
solver = Topsis(table)
|
| 179 |
+
else:
|
| 180 |
+
solver = Promethee(table)
|
| 181 |
+
|
| 182 |
+
solver.solve()
|
| 183 |
+
|
| 184 |
+
# Log results
|
| 185 |
+
logger.info(f"\n{'='*50}")
|
| 186 |
+
logger.info(f"RESULTS ({args.method.upper()})")
|
| 187 |
+
logger.info(f"{'='*50}")
|
| 188 |
+
logger.info(f"Best alternative: {alt_names[solver.index_of_best_alternative]}")
|
| 189 |
+
logger.info(f"Worst alternative: {alt_names[solver.index_of_worst_alternative]}")
|
| 190 |
+
logger.info(f"\nRanking (best to worst):")
|
| 191 |
+
for rank, idx in enumerate(solver.ordered_indices, 1):
|
| 192 |
+
logger.info(f" {rank}. {alt_names[idx]}")
|
| 193 |
+
else:
|
| 194 |
+
logger.error("Failed to parse decision matrix")
|
utils.py
CHANGED
|
@@ -26,4 +26,8 @@ Return ONLY a Markdown table in this exact format:
|
|
| 26 |
4. Last row: "weight", then importance weights as VALUE:CONFIDENCE (always use positive values 1-5 for weights)
|
| 27 |
5. VALUE must be positive (1-5) for benefits, negative (-1 to -5) for costs
|
| 28 |
6. CONFIDENCE is always positive (1-5) regardless of criterion type
|
| 29 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
4. Last row: "weight", then importance weights as VALUE:CONFIDENCE (always use positive values 1-5 for weights)
|
| 27 |
5. VALUE must be positive (1-5) for benefits, negative (-1 to -5) for costs
|
| 28 |
6. CONFIDENCE is always positive (1-5) regardless of criterion type
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
DEFAULT_QUERY = "Okay so I've been going crazy trying to figure out if I should get a dog, a cat, or maybe just stick with a fish tank, and honestly I'm not even sure what matters most to me anymore. Like, companionship is huge for me, probably the most important thing, and I'm pretty confident dogs are amazing for that, cats are okay but kind of aloof sometimes, fish are basically just decoration let's be real. Then there's the whole time commitment thing which I think matters a lot but I'm not totally sure how much—I work weird hours so dogs seem like a nightmare there, cats are supposedly independent but my friend's cat is super needy so who knows, fish are definitely easy but maybe too easy? Cost is something I should care about more than I do, I guess it's moderately important, and I've heard dogs are expensive with vet bills and food, cats are cheaper I think, fish setups can actually cost a lot upfront but then nothing after. Oh and allergies—my roommate might be slightly allergic to cats, we're not 100% sure, dogs seem fine, fish obviously no issue there, so that's pretty important actually. And like, the whole lifestyle flexibility thing where I want to travel sometimes, dogs are terrible for that obviously, cats you can leave for a weekend maybe, fish just need an auto-feeder so that's nice, but I'm not sure how much I'll actually travel so maybe this doesn't matter that much?"
|