HermyonAI / defuzzifier.py
chariscait's picture
Upload defuzzifier.py with huggingface_hub
6650676 verified
"""Defuzzification: convert fuzzy rule outputs back to crisp 28-dim probabilities."""
from __future__ import annotations
import numpy as np
from constants import GOEMOTIONS_LABELS, NUM_GOEMOTIONS
from membership import LEVEL_CENTROIDS, FUZZY_LEVELS
from rule_base import FuzzyRule
class Defuzzifier:
"""Convert fuzzy inference output to crisp emotion probabilities."""
def __init__(self, method: str = "centroid"):
if method not in ("centroid", "mom", "lom"):
raise ValueError(f"Unknown defuzzification method: {method}")
self.method = method
self._label_idx = {label: i for i, label in enumerate(GOEMOTIONS_LABELS)}
def defuzzify(
self,
base_fuzzy: dict[str, dict[str, float]],
fired_rules: list[tuple[FuzzyRule, float]],
base_crisp: np.ndarray,
) -> tuple[np.ndarray, list[str]]:
result = base_crisp.copy()
fired_names = []
if not fired_rules:
total = result.sum()
if total > 0:
result = result / total
return result, fired_names
rule_adjustments = np.zeros(NUM_GOEMOTIONS)
rule_weights = np.zeros(NUM_GOEMOTIONS)
for rule, activation in fired_rules:
fired_names.append(rule.name)
for emotion_label, (target_level, weight_modifier) in rule.consequent.items():
if emotion_label not in self._label_idx:
continue
idx = self._label_idx[emotion_label]
target_centroid = LEVEL_CENTROIDS.get(target_level, 0.35)
effective_target = target_centroid * activation * weight_modifier
if effective_target > rule_adjustments[idx]:
rule_adjustments[idx] = effective_target
rule_weights[idx] = activation
for i in range(NUM_GOEMOTIONS):
if rule_weights[i] > 0:
blend = rule_weights[i]
result[i] = (1.0 - blend) * result[i] + blend * rule_adjustments[i]
if self.method == "centroid":
result = self._centroid_refine(result, base_fuzzy)
elif self.method == "mom":
result = self._mom_refine(result, base_fuzzy)
elif self.method == "lom":
result = self._lom_refine(result, base_fuzzy)
result = np.maximum(result, 0)
total = result.sum()
if total > 0:
result = result / total
else:
result = np.ones(NUM_GOEMOTIONS) / NUM_GOEMOTIONS
return result, fired_names
def _centroid_refine(self, crisp, fuzzy):
for label, memberships in fuzzy.items():
if label not in self._label_idx:
continue
idx = self._label_idx[label]
numerator = 0.0
denominator = 0.0
for level, mu in memberships.items():
if mu > 0 and level in LEVEL_CENTROIDS:
numerator += mu * LEVEL_CENTROIDS[level]
denominator += mu
if denominator > 0:
cog = numerator / denominator
crisp[idx] = 0.7 * crisp[idx] + 0.3 * cog
return crisp
def _mom_refine(self, crisp, fuzzy):
for label, memberships in fuzzy.items():
if label not in self._label_idx:
continue
idx = self._label_idx[label]
if not memberships:
continue
max_mu = max(memberships.values())
if max_mu < 0.01:
continue
max_levels = [
level for level, mu in memberships.items()
if mu >= max_mu - 0.01 and level in LEVEL_CENTROIDS
]
if max_levels:
mom = np.mean([LEVEL_CENTROIDS[l] for l in max_levels])
crisp[idx] = 0.7 * crisp[idx] + 0.3 * mom
return crisp
def _lom_refine(self, crisp, fuzzy):
for label, memberships in fuzzy.items():
if label not in self._label_idx:
continue
idx = self._label_idx[label]
if not memberships:
continue
max_mu = max(memberships.values())
if max_mu < 0.01:
continue
best_level = "absent"
for level in reversed(FUZZY_LEVELS):
if memberships.get(level, 0) >= max_mu - 0.01:
best_level = level
break
if best_level in LEVEL_CENTROIDS:
lom = LEVEL_CENTROIDS[best_level]
crisp[idx] = 0.7 * crisp[idx] + 0.3 * lom
return crisp