diff --git a/examples/football_project/football_classes.py b/examples/football_project/football_classes.py new file mode 100644 index 00000000..13ffbd6a --- /dev/null +++ b/examples/football_project/football_classes.py @@ -0,0 +1,80 @@ +import enum +import math + +# This graph will determine which free agents a team can sign based on their available cap space +# and which players they should sign based on their needs and other player factors + +class LeagueRules(enum.Enum): # Fixed the base class + POSTION_LIST = ["QB", "RB", "WR", "TE"] + MIN_PLAYER_AT_POSITION = { + "QB": 1, + "RB": 1, + "WR": 2, + "TE": 1, + } + SALARY_CAP = 2000000 + +class FootballPlayer: + def __init__(self, name, position, expected_salary, madden_rating, available=True): + self.name = name + self.position = position + self.expected_salary = expected_salary + self.madden_rating = madden_rating + self.available = available + self.player_value = (self.madden_rating / self.expected_salary) * 100000 + + def get_availability(self): + return self.available + +class FootballTeam: + def __init__(self, team_name): + self.team_name = team_name + self.roster = {} + for position in LeagueRules.POSTION_LIST.value: + self.roster[position] = [] + self.cap_space = LeagueRules.SALARY_CAP.value + + def can_sign_player(self, player): + if player.expected_salary <= self.cap_space and player.get_availability(): + return True + return False + + # A team wants to sign a player if the player can provide more value than the lowest value player at thier position + def check_if_team_wants_players(self, player): + lowest_rostered_value_at_pos = self.get_lowest_value_at_position(player.position) + if player.player_value > lowest_rostered_value_at_pos: + # print(f"Team {self.team_name} wants player {player.name} with value {player.player_value}. Lowest value is {lowest_rostered_value_at_pos}.") + return True + return False + + def get_lowest_value_at_position(self, position): + min_value = math.inf + for player in self.roster[position]: + if player.player_value < min_value: + min_value = player.player_value + + return min_value + + def sign_player(self, player): + if self.can_sign_player(player): + #self.roster.append(player) + self.roster[player.position].append(player) + self.cap_space -= player.expected_salary + player.available = False + return True + return False + + def get_available_players(self): + return [player for player in self.roster if player.get_availability()] + + def get_cap_space(self): + return self.cap_space + + def get_num_player_needed(self, position): + current_qbs = [player for player in self.roster[position]] + num_players_needed = LeagueRules.MIN_PLAYER_AT_POSITION.value[position] - len(current_qbs) + return num_players_needed + + def get_remaining_cap_space(self): + total_salary = sum(player.expected_salary for player in self.roster) + return self.cap_space - total_salary \ No newline at end of file diff --git a/examples/football_project/player_signings.py b/examples/football_project/player_signings.py new file mode 100644 index 00000000..c15f757c --- /dev/null +++ b/examples/football_project/player_signings.py @@ -0,0 +1,221 @@ +import numba +import pyreason as pr +import networkx as nx +import football_classes as fc +import random +from pprint import pprint +import matplotlib.pyplot as plt # type: ignore + +# Generates random names +import names + +def generate_random_player_at_position(position): + full_name = names.get_full_name() + name_with_underscore = full_name.replace(" ", "_") + return fc.FootballPlayer( + name=name_with_underscore, + position=position, + expected_salary=random.randint(100000, 500000), + madden_rating=random.randint(60, 99), + ) + +def add_rostered_fact(team, player): + fact_string = f"rostered({team.team_name} , {player.name})" + pr.add_fact(pr.Fact(fact_string, 'rostered_fact', 0, 1)) + +def add_not_rostered_fact(team, player): + fact_string = f"~rostered({team.team_name} , {player.name})" + pr.add_fact(pr.Fact(fact_string, 'rostered_fact', 0, 1)) + +def try_to_sign_player(team, player): + if team.can_sign_player(player): + team.sign_player(player) + add_rostered_fact(team, player) + print(f"{team.team_name} signed {player.name} for ${player.expected_salary}") + else: + add_not_rostered_fact(team, player) + +# Operating at timestep 1 +def add_interested_fact(team, player): + fact_string = f"interested({team.team_name} , {player.name})" + pr.add_fact(pr.Fact(fact_string, 'interested_fact', 0, 1)) + +def add_not_interested_fact(team, player): + fact_string = f"~interested({team.team_name} , {player.name})" + pr.add_fact(pr.Fact(fact_string, 'interested_fact', 0, 1)) + +def determine_team_interest(): + for team in team_list: + for player in all_player_list: + if team.check_if_team_wants_players(player): + add_interested_fact(team, player) + else: + add_not_interested_fact(team, player) + +def simulate_draft(): + # print("Available Players:") + # for player in all_players: + # print(f"Name: {player.name}, Position: {player.position}, " + # f"Expected Salary: ${player.expected_salary:,}, Madden Rating: {player.madden_rating}") + for team in team_list: + # Loop through the available players and try to sign them + for player in qb_list: + if team.get_num_player_needed("QB") > 0: + try_to_sign_player(team, player) + else: + add_not_rostered_fact(team, player) + for player in rb_list: + if team.get_num_player_needed("RB") > 0: + try_to_sign_player(team, player) + else: + add_not_rostered_fact(team, player) + for player in wr_list: + if team.get_num_player_needed("WR") > 0: + try_to_sign_player(team, player) + else: + add_not_rostered_fact(team, player) + for player in te_list: + if team.get_num_player_needed("TE") > 0: + try_to_sign_player(team, player) + else: + add_not_rostered_fact(team, player) + + # Print the final rosters and cap space for each team + print("\nFinal Rosters and Cap Space:") + for team in team_list: + print(f"\n{team.team_name} Roster:") + for position, players in team.roster.items(): + for player in players: + print(f" Name: {player.name}, Position: {player.position}, Value: {player.player_value}") + print(f"Cap Space Remaining: ${team.get_cap_space():,}") + + +qb_list = [] +rb_list = [] +wr_list = [] +te_list = [] +all_player_list = [] + +for val in range(20): + qb_list.append(generate_random_player_at_position("QB")) + rb_list.append(generate_random_player_at_position("RB")) + wr_list.append(generate_random_player_at_position("WR")) + te_list.append(generate_random_player_at_position("TE")) + +all_player_list.extend(qb_list) +all_player_list.extend(rb_list) +all_player_list.extend(wr_list) +all_player_list.extend(te_list) + +team_list = [] +team_list.append(fc.FootballTeam("Pittsburgh_Steelers")) +team_list.append(fc.FootballTeam("Baltamore_Ravens")) +team_list.append(fc.FootballTeam("Dallas_Cowboys")) +team_list.append(fc.FootballTeam("Chicago_Bears")) + + + +# Create a Directed graph +g = nx.DiGraph() + +for player in all_player_list: + g.add_node(player.name, name=player.name, position=player.position, expected_salary=player.expected_salary, madden_rating=player.madden_rating, player_value=player.player_value) +for team in team_list: + g.add_node(team.team_name) + +# Make a picture of the graph +# nx.draw(g, with_labels=True) +# plt.savefig("football_graph.png") +# plt.show() + +pr.settings.verbose = True # Print info to screen +pr.settings.atom_trace = True # Print the trace of the atoms + +# Load all the files into pyreason +pr.load_graph(g) + + +@numba.njit +def demanded_player_annotation_fn(annotations, weights): + """ + Calculate the value of a player based on their attributes. + """ + # Calculate the value based on the weights + # print("Annotations: ", annotations) + # print("Annotations[0]: ", annotations[0]) + num_interested_teams = len(annotations[0]) + print("Interested Teams: ", num_interested_teams) + if num_interested_teams > 3: + upper_bound = 1 + lower_bound = 1 + else: + upper_bound = 0 + lower_bound = 0 + return lower_bound, upper_bound + +pr.add_annotation_function(demanded_player_annotation_fn) + +# These functions add facts about the players based on how they are drafted and the teams that want them +simulate_draft() +determine_team_interest() + +# Rule to check if a player is a free agent +pr.add_rule(pr.Rule('rostered_player(x) <-1 rostered(y, x)', 'rostered_player_rule')) +pr.add_rule(pr.Rule('free_agent(x) <-1 ~rostered(y,x)', 'free_agent_rule')) +pr.add_rule(pr.Rule('make_offer(y,x) <-1 free_agent(x), interested(y,x)', 'make_offer_rule')) +pr.add_rule(pr.Rule('demanded_player(x) : demanded_player_annotation_fn <-1 make_offer(a,x)', 'demanded_player(x)')) +pr.add_rule(pr.Rule('highly_demanded_player(x) <-1 demanded_player(x): [1,1]', 'highly_demanded_player_rule')) + +#pr.add_rule(pr.Rule('team_interested_in_player(x, y) <-1 ')) + +interpretation = pr.reason(timesteps=4) +interpretation_dict = interpretation.get_dict() +# print("Interpretation Dictionary:") +# pprint(interpretation_dict) + +# Display the changes in the interpretation for each timestep +print("========================== Rostered Players ==========================") +rostered_player_df = pr.filter_and_sort_nodes(interpretation, ['rostered_player']) +for t, df in enumerate(rostered_player_df): + print(f'TIMESTEP - {t}') + print(df) + print() + +print("========================== Free Agents ==========================") +free_agent_df = pr.filter_and_sort_nodes(interpretation, ['free_agent']) +for t, df in enumerate(free_agent_df): + print(f'TIMESTEP - {t}') + print(df) + print() + + +print("========================== Team offers ==========================") +team_offer_df = pr.filter_and_sort_edges(interpretation, ['make_offer']) +for t, df in enumerate(team_offer_df): + print(f'TIMESTEP - {t}') + print(df) + print() + +print("========================== Demanded Player ==========================") +team_offer_df = pr.filter_and_sort_nodes(interpretation, ['demanded_player']) +for t, df in enumerate(team_offer_df): + print(f'TIMESTEP - {t}') + print(df) + print() + +print("========================== Hot Commodities ==========================") +hot_commodity_df = pr.filter_and_sort_nodes(interpretation, ['highly_demanded_player']) +for t, df in enumerate(hot_commodity_df): + print(f'TIMESTEP - {t}') + print(df) + for index, row in df.iterrows(): + node_name = row['component'] + player_value = g.nodes[node_name]["player_value"] + madden_rating = g.nodes[node_name]["madden_rating"] + expected_salary = g.nodes[node_name]["expected_salary"] + print(f"Node: {node_name}, Salary: {expected_salary}, Madden Rating: {madden_rating}, Value: {player_value}") + + +# Iterate over the "component" column in hot_commodity_df and print everything we know about the corresponding node + +pr.save_rule_trace(interpretation) diff --git a/examples/image_classifier_two/images/fish_1.jpeg b/examples/image_classifier_two/images/fish_1.jpeg new file mode 100644 index 00000000..6413569e Binary files /dev/null and b/examples/image_classifier_two/images/fish_1.jpeg differ diff --git a/examples/image_classifier_two/images/fish_2.jpeg b/examples/image_classifier_two/images/fish_2.jpeg new file mode 100644 index 00000000..581f1b17 Binary files /dev/null and b/examples/image_classifier_two/images/fish_2.jpeg differ diff --git a/examples/image_classifier_two/images/shark_1.jpeg b/examples/image_classifier_two/images/shark_1.jpeg new file mode 100644 index 00000000..e7ebb18f Binary files /dev/null and b/examples/image_classifier_two/images/shark_1.jpeg differ diff --git a/examples/image_classifier_two/images/shark_2.jpeg b/examples/image_classifier_two/images/shark_2.jpeg new file mode 100644 index 00000000..037c53dd Binary files /dev/null and b/examples/image_classifier_two/images/shark_2.jpeg differ diff --git a/examples/image_classifier_two/images/shark_3.jpeg b/examples/image_classifier_two/images/shark_3.jpeg new file mode 100644 index 00000000..640d01f2 Binary files /dev/null and b/examples/image_classifier_two/images/shark_3.jpeg differ diff --git a/pyreason/scripts/learning/classification/classifier.py b/pyreason/scripts/learning/classification/classifier.py index 06bc3ec9..7bd76c2d 100644 --- a/pyreason/scripts/learning/classification/classifier.py +++ b/pyreason/scripts/learning/classification/classifier.py @@ -38,8 +38,38 @@ def get_class_facts(self, t1: int, t2: int) -> List[Fact]: fact = Fact(f'{c}({self.identifier})', name=f'{self.identifier}-{c}-fact', start_time=t1, end_time=t2) facts.append(fact) return facts + - def forward(self, x, t1: int = 0, t2: int = 0) -> Tuple[torch.Tensor, torch.Tensor, List[Fact]]: + # A user may want to restrict the number of classe so that the classifier only returns facts for a subset of classes. + # This is useful for large models like CLIP, where the model has 4000 classes. + # We will set the new possible classes to be limited to the class names given to the classifier. + def update_classes_and_probs_for_filter(self, probabilities) -> torch.Tensor: + # Get the index-to-label mapping from the model config + id2label = self.model.config.id2label + + # Get the indices of the allowed labels, stripping everything after the comma + allowed_indices = [ + i for i, label in id2label.items() + if label.split(",")[0].strip().lower() in [name.lower() for name in self.class_names] + ] + + # Normalize the probabilities based only on the allowed classes + filtered_probs = torch.zeros_like(probabilities) + filtered_probs[allowed_indices] = probabilities[allowed_indices] + filtered_probs = filtered_probs / filtered_probs.sum() + + # Because we are filtering the probabilities, we need to update the class labels to only include the allowed classes. + # We also update the class names so they are ordered by the probabilities. + top_labels = [] + top_probs, top_indices = filtered_probs.topk(len(self.class_names)) + for prob, idx in zip(top_probs, top_indices): + label = id2label[idx.item()].split(",")[0] + print(f"{label}: {prob.item():.4f}") + top_labels.append(label) + self.class_names = top_labels + return top_probs + + def forward(self, x, t1: int = 0, t2: int = 0, limit_classification_output_classes = False) -> Tuple[torch.Tensor, torch.Tensor, List[Fact]]: """ Forward pass of the model :param x: Input tensor @@ -47,10 +77,21 @@ def forward(self, x, t1: int = 0, t2: int = 0) -> Tuple[torch.Tensor, torch.Tens :param t2: End time for the facts :return: Output tensor """ - output = self.model(x) - # Convert logits to probabilities assuming a multi-class classification. + try: + output = self.model(x) + except AttributeError as e: + print(f"Error during model forward pass: {e}") + try: + output = self.model(**x).logits + except Exception as e: + print(f"Error during model forward pass with kwargs: {e}") + probabilities = F.softmax(output, dim=1).squeeze() + + if limit_classification_output_classes: + probabilities = self.update_classes_and_probs_for_filter(probabilities) + opts = self.interface_options # Prepare threshold tensor. diff --git a/pyreason/tests/test_image_classifier.py b/pyreason/tests/test_image_classifier.py new file mode 100644 index 00000000..75892427 --- /dev/null +++ b/pyreason/tests/test_image_classifier.py @@ -0,0 +1,94 @@ +# import the logicIntegratedClassifier class + +from pathlib import Path +import torch +import torch.nn as nn +import networkx as nx +import numpy as np +import random +import sys +import os +from transformers import AutoImageProcessor, AutoModelForImageClassification +from PIL import Image +import torch.nn.functional as F +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) + +from pyreason.scripts.learning.classification.classifier import LogicIntegratedClassifier +from pyreason.scripts.facts.fact import Fact +from pyreason.scripts.learning.utils.model_interface import ModelInterfaceOptions +from pyreason.scripts.rules.rule import Rule +from pyreason.pyreason import _Settings as Settings, reason, reset_settings, get_rule_trace, add_fact, add_rule, load_graph, save_rule_trace + + +# Step 1: Load a pre-trained model and image processor from Hugging Face +model_name = "google/vit-base-patch16-224" # Vision Transformer model +processor = AutoImageProcessor.from_pretrained(model_name) +model = AutoModelForImageClassification.from_pretrained(model_name) + +G = nx.DiGraph() +load_graph(G) + +# Step 2: Load and preprocess images from the directory +image_dir = "/Users/coltonpayne/pyreason/examples/image_classifier_two/images" +image_paths = list(Path(image_dir).glob("*.jpeg")) # Get all .jpeg files in the directory +image_list = [] +allowed_labels = ['goldfish', 'tiger shark', 'hammerhead', 'great white shark', 'tench'] + +# Add Rules to the knowlege base +add_rule(Rule("is_fish(x) <-0 goldfish(x)", "is_fish_rule")) +add_rule(Rule("is_fish(x) <-0 tench(x)", "is_fish_rule")) +add_rule(Rule("is_shark(x) <-0 tigershark(x)", "is_shark_rule")) +add_rule(Rule("is_shark(x) <-0 hammerhead(x)", "is_shark_rule")) +add_rule(Rule("is_shark(x) <-0 greatwhiteshark(x)", "is_shark_rule")) +add_rule(Rule("is_scary(x) <-0 is_shark(x)", "is_scary_rule")) +add_rule(Rule("likes_to_eat(y,x) <-0 is_shark(y), is_shark(x)", "likes_to_eat_rule", infer_edges=True)) + +for image_path in image_paths: + print(f"Processing Image: {image_path.name}") + image = Image.open(image_path) + inputs = processor(images=image, return_tensors="pt") + + interface_options = ModelInterfaceOptions( + threshold=0.5, # Only process probabilities above 0.5 + set_lower_bound=True, # For high confidence, adjust the lower bound. + set_upper_bound=False, # Keep the upper bound unchanged. + snap_value=1.0 # Use 1.0 as the snap value. + ) + + classifier_name = image_path.name.split(".")[0] + fish_classifier = LogicIntegratedClassifier( + model, + allowed_labels, + identifier=classifier_name, + interface_options=interface_options + ) + + # print("Top Probs: ", filtered_probs) + logits, probabilities, classifier_facts = fish_classifier(inputs, limit_classification_output_classes=True) + + print("=== Fish Classifier Output ===") + #print("Probabilities:", probabilities) + print("\nGenerated Classifier Facts:") + for fact in classifier_facts: + print(fact) + + for fact in classifier_facts: + add_fact(fact) + + print("Done processing image ", image_path.name) + +# --- Part 4: Run the Reasoning Engine --- + +# Reset settings before running reasoning +reset_settings() + +# Run the reasoning engine to allow the investigation flag to propagate hat through the network. +Settings.atom_trace = True +interpretation = reason() + +trace = get_rule_trace(interpretation) +print(f"RULE TRACE: \n\n{trace[0]}\n") + + +# TODO: +# Ask Dyuman about how to connect all the edges of a graph within the LogicIntegratedClassifier