nous 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nous/__init__.py CHANGED
@@ -1,2 +1,26 @@
1
1
  # nous/__init__.py
2
- __version__ = "0.0.1"
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .models import NousNet
6
+ from .interpret import (
7
+ trace_decision_graph,
8
+ explain_fact,
9
+ plot_logic_graph,
10
+ plot_fact_activation_function,
11
+ plot_final_layer_contributions,
12
+ )
13
+ from .causal import find_counterfactual
14
+
15
+ __all__ = [
16
+ # Main Model
17
+ "NousNet",
18
+ # Interpretation Suite
19
+ "trace_decision_graph",
20
+ "explain_fact",
21
+ "plot_logic_graph",
22
+ "plot_fact_activation_function",
23
+ "plot_final_layer_contributions",
24
+ # Causal Analysis
25
+ "find_counterfactual",
26
+ ]
nous/causal.py ADDED
@@ -0,0 +1,63 @@
1
+ # nous/causal.py
2
+ import torch
3
+ import torch.nn as nn
4
+ from typing import Literal
5
+ from .models import NousNet
6
+
7
+ def find_counterfactual(
8
+ model: NousNet,
9
+ x_sample: torch.Tensor,
10
+ target_output: float,
11
+ task: Literal['regression', 'classification'] = 'classification',
12
+ lr: float = 0.01,
13
+ steps: int = 200,
14
+ l1_lambda: float = 0.5
15
+ ) -> dict:
16
+ """
17
+ Finds a minimal change to the input to achieve a target output.
18
+
19
+ Args:
20
+ model (NousNet): The trained model.
21
+ x_sample (torch.Tensor): The original input tensor.
22
+ target_output (float): For classification, the desired probability (e.g., 0.8).
23
+ For regression, the desired absolute value (e.g., 150.0).
24
+ task (Literal['regression', 'classification']): The type of task.
25
+ lr, steps, l1_lambda: Optimization parameters.
26
+
27
+ Returns:
28
+ dict: A dictionary containing the counterfactual sample and a list of changes.
29
+ """
30
+ if task == 'classification':
31
+ if not (0 < target_output < 1):
32
+ raise ValueError("Target for classification must be a probability between 0 and 1.")
33
+ if model.output_head.out_features > 1:
34
+ raise NotImplementedError("Counterfactual analysis for multi-class models is not yet supported.")
35
+ # Calculate target in logit space for numerical stability
36
+ target = torch.log(torch.tensor(target_output) / (1 - torch.tensor(target_output)))
37
+ else: # regression
38
+ target = torch.tensor(target_output, dtype=torch.float32)
39
+
40
+ x_sample = x_sample.clone().detach()
41
+ delta = torch.zeros_like(x_sample, requires_grad=True)
42
+ optimizer = torch.optim.Adam([delta], lr=lr)
43
+
44
+ for _ in range(steps):
45
+ optimizer.zero_grad()
46
+ x_perturbed = x_sample + delta
47
+ prediction = model(x_perturbed.unsqueeze(0)).squeeze()
48
+
49
+ target_loss = (prediction - target)**2
50
+ l1_loss = torch.norm(delta, p=1)
51
+ total_loss = target_loss + l1_lambda * l1_loss
52
+
53
+ # This will now only be called for scalar losses, as multi-class is caught above
54
+ total_loss.backward()
55
+ optimizer.step()
56
+
57
+ final_x = x_sample + delta.detach()
58
+ changes = []
59
+ for i, name in enumerate(model.feature_names):
60
+ if not torch.isclose(x_sample[i], final_x[i], atol=1e-3):
61
+ changes.append((name, x_sample[i].item(), final_x[i].item()))
62
+
63
+ return {"counterfactual_x": final_x, "changes": changes}
nous/interpret.py ADDED
@@ -0,0 +1,111 @@
1
+ # nous/interpret.py
2
+ import torch
3
+ import pandas as pd
4
+ import matplotlib.pyplot as plt
5
+ import seaborn as sns
6
+ import networkx as nx
7
+ from .models import NousNet
8
+ from .layers import LearnedAtomicFactLayer, BetaFactLayer
9
+
10
+ def trace_decision_graph(model: NousNet, x_sample: torch.Tensor) -> dict:
11
+ """Traces the full reasoning path for a single sample."""
12
+ if x_sample.dim() == 1: x_sample = x_sample.unsqueeze(0)
13
+ model.eval()
14
+ graph_data = {"trace": {}}
15
+ with torch.no_grad():
16
+ facts = model.atomic_fact_layer(x_sample).squeeze(0)
17
+ graph_data['trace']['Atomic Facts'] = {name: {"value": facts[i].item()} for i, name in enumerate(model.atomic_fact_layer.fact_names)}
18
+ h = facts.unsqueeze(0)
19
+ for i, block in enumerate(model.nous_blocks):
20
+ h, concepts, rule_activations = block(h)
21
+ concepts, rule_activations = concepts.squeeze(0), rule_activations.squeeze(0)
22
+ graph_data['trace'][f'Rules L{i}'] = {name: {"value": rule_activations[j].item()} for j, name in enumerate(block.rule_layer.rule_names)}
23
+ graph_data['trace'][f'Concepts L{i}'] = {name: {"value": concepts[j].item()} for j, name in enumerate(block.concept_names)}
24
+ return graph_data
25
+
26
+ def explain_fact(model: NousNet, fact_name: str) -> pd.DataFrame:
27
+ """Provides a detailed breakdown of a single learned fact, showing feature weights."""
28
+ fact_layer = model.atomic_fact_layer
29
+ if not isinstance(fact_layer, LearnedAtomicFactLayer):
30
+ raise TypeError("explain_fact is only applicable to 'beta' or 'sigmoid' fact layers.")
31
+ try:
32
+ fact_index = fact_layer.fact_names.index(fact_name)
33
+ except ValueError:
34
+ raise ValueError(f"Fact '{fact_name}' not found.")
35
+
36
+ with torch.no_grad():
37
+ w_left, w_right = fact_layer.projection_left.weight[fact_index], fact_layer.projection_right.weight[fact_index]
38
+ threshold = fact_layer.thresholds[fact_index]
39
+ df = pd.DataFrame({
40
+ "feature": model.feature_names,
41
+ "left_weight": w_left.cpu().detach().numpy(),
42
+ "right_weight": w_right.cpu().detach().numpy(),
43
+ })
44
+ df['net_effect'] = df['left_weight'] - df['right_weight']
45
+ print(f"Explanation for fact '{fact_name}':")
46
+ print(f"Fact is TRUE when: (Sum(left_weight * feat) - Sum(right_weight * feat)) > {threshold.item():.3f}")
47
+ return df.sort_values(by='net_effect', key=abs, ascending=False)
48
+
49
+ def plot_fact_activation_function(model: NousNet, fact_name: str, x_range=(-3, 3), n_points=200):
50
+ """Visualizes the learned activation function for a single learned fact."""
51
+ fact_layer = model.atomic_fact_layer
52
+ if not isinstance(fact_layer, LearnedAtomicFactLayer):
53
+ raise TypeError("This function is only for 'beta' or 'sigmoid' fact layers.")
54
+ try:
55
+ fact_index = fact_layer.fact_names.index(fact_name)
56
+ except ValueError:
57
+ raise ValueError(f"Fact '{fact_name}' not found.")
58
+
59
+ diff_range = torch.linspace(x_range[0], x_range[1], n_points)
60
+
61
+ with torch.no_grad():
62
+ if isinstance(fact_layer, BetaFactLayer):
63
+ k = torch.nn.functional.softplus(fact_layer.k_raw[fact_index]) + 1e-4
64
+ nu = torch.nn.functional.softplus(fact_layer.nu_raw[fact_index]) + 1e-4
65
+ activations = (1 + torch.exp(-k * diff_range))**(-nu)
66
+ label = f'Learned Beta-like (k={k:.2f}, ν={nu:.2f})'
67
+ else: # SigmoidFactLayer
68
+ steepness = torch.nn.functional.softplus(fact_layer.steepness[fact_index]) + 1e-4
69
+ activations = torch.sigmoid(steepness * diff_range)
70
+ label = f'Learned Sigmoid (steepness={steepness:.2f})'
71
+
72
+ plt.figure(figsize=(8, 5))
73
+ plt.plot(diff_range.numpy(), activations.numpy(), label=label, linewidth=2.5)
74
+ plt.plot(diff_range.numpy(), torch.sigmoid(diff_range).numpy(), label='Standard Sigmoid', linestyle='--', color='gray')
75
+ plt.title(f"Activation Function for Fact:\n'{fact_name}'")
76
+ plt.xlabel("Difference Value (Left Projection - Right Projection - Threshold)")
77
+ plt.ylabel("Fact Activation (Truth Value)")
78
+ plt.legend(); plt.grid(True, linestyle=':'); plt.ylim(-0.05, 1.05); plt.show()
79
+
80
+ def plot_final_layer_contributions(model: NousNet, x_sample: torch.Tensor):
81
+ """Calculates and plots which high-level concepts most influenced the final prediction."""
82
+ if x_sample.dim() == 1: x_sample = x_sample.unsqueeze(0)
83
+ model.eval()
84
+ with torch.no_grad():
85
+ h = model.atomic_fact_layer(x_sample)
86
+ for block in model.nous_blocks: h, _, _ = block(h)
87
+ final_activations = h.squeeze(0)
88
+
89
+ output_dim = model.output_head.out_features
90
+ weights = model.output_head.weight.squeeze(0)
91
+ title = "Top Final Layer Concept Contributions"
92
+
93
+ if output_dim > 1:
94
+ predicted_class = model.output_head(h).argmax().item()
95
+ weights = model.output_head.weight[predicted_class]
96
+ title += f" for Predicted Class {predicted_class}"
97
+
98
+ contributions = final_activations * weights
99
+ concept_names = model.nous_blocks[-1].concept_names
100
+
101
+ df = pd.DataFrame({'concept': concept_names, 'contribution': contributions.cpu().detach().numpy()})
102
+ df = df.sort_values('contribution', key=abs, ascending=False).head(15)
103
+
104
+ plt.figure(figsize=(10, 6));
105
+ colors = ['#5fba7d' if c > 0 else '#d65f5f' for c in df['contribution']]
106
+ sns.barplot(x='contribution', y='concept', data=df, palette=colors, dodge=False)
107
+ plt.title(title); plt.xlabel("Contribution (Activation * Weight)"); plt.ylabel("Final Layer Concept"); plt.show()
108
+
109
+ def plot_logic_graph(*args, **kwargs):
110
+ print("Graph visualization is planned for a future release.")
111
+ pass
nous/layers.py ADDED
@@ -0,0 +1,117 @@
1
+ # nous/layers.py
2
+ import torch
3
+ import torch.nn as nn
4
+ from typing import List, Tuple
5
+
6
+ # --- Fact Layers ---
7
+
8
+ class ExhaustiveAtomicFactLayer(nn.Module):
9
+ """Generates atomic facts by exhaustively comparing all pairs of features."""
10
+ def __init__(self, input_dim: int, feature_names: List[str]):
11
+ super().__init__()
12
+ if input_dim > 20:
13
+ num_facts = input_dim * (input_dim - 1) // 2
14
+ print(f"Warning: ExhaustiveAtomicFactLayer with {input_dim} features will create {num_facts} facts. This may be slow and memory-intensive.")
15
+ self.indices = torch.combinations(torch.arange(input_dim), r=2)
16
+ self.thresholds = nn.Parameter(torch.randn(self.indices.shape[0]) * 0.1)
17
+ self.steepness = nn.Parameter(torch.ones(self.indices.shape[0]) * 5.0)
18
+ self.fact_names = [f"({feature_names[i]} > {feature_names[j]})" for i, j in self.indices.numpy()]
19
+
20
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
21
+ steepness = torch.nn.functional.softplus(self.steepness) + 1e-4
22
+ diffs = x[:, self.indices[:, 0]] - x[:, self.indices[:, 1]]
23
+ return torch.sigmoid(steepness * (diffs - self.thresholds))
24
+
25
+ @property
26
+ def output_dim(self) -> int: return len(self.fact_names)
27
+
28
+ class LearnedAtomicFactLayer(nn.Module):
29
+ """Base class for learnable fact layers (Sigmoid and Beta)."""
30
+ def __init__(self, input_dim: int, num_facts: int, feature_names: List[str]):
31
+ super().__init__()
32
+ self.input_dim = input_dim
33
+ self.num_facts = num_facts
34
+ self._feature_names = feature_names
35
+ self.projection_left = nn.Linear(input_dim, num_facts, bias=False)
36
+ self.projection_right = nn.Linear(input_dim, num_facts, bias=False)
37
+ self.thresholds = nn.Parameter(torch.randn(num_facts) * 0.1)
38
+
39
+ @property
40
+ def output_dim(self) -> int: return self.num_facts
41
+
42
+ def get_base_diffs(self, x: torch.Tensor) -> torch.Tensor:
43
+ """Calculates the core difference term for all activation functions."""
44
+ return (self.projection_left(x) - self.projection_right(x)) - self.thresholds
45
+
46
+ def fact_names(self, prefix: str) -> List[str]:
47
+ """Generates human-readable and unique names for facts."""
48
+ names = []
49
+ with torch.no_grad():
50
+ w_left, w_right = self.projection_left.weight, self.projection_right.weight
51
+ for i in range(self.output_dim):
52
+ left_name = self._feature_names[w_left[i].abs().argmax().item()]
53
+ right_name = self._feature_names[w_right[i].abs().argmax().item()]
54
+ base_name = f"({left_name} vs {right_name})" if left_name != right_name else f"Thresh({left_name})"
55
+ names.append(f"{prefix}-{i}{base_name}")
56
+ return names
57
+
58
+ # --- Specialized Learnable Fact Layers ---
59
+
60
+ class SigmoidFactLayer(LearnedAtomicFactLayer):
61
+ """A learnable fact layer using the standard sigmoid activation."""
62
+ def __init__(self, input_dim: int, num_facts: int, feature_names: List[str]):
63
+ super().__init__(input_dim, num_facts, feature_names)
64
+ self.steepness = nn.Parameter(torch.ones(num_facts) * 5.0)
65
+
66
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
67
+ diffs = self.get_base_diffs(x)
68
+ steepness = torch.nn.functional.softplus(self.steepness) + 1e-4
69
+ return torch.sigmoid(steepness * diffs)
70
+
71
+ @property
72
+ def fact_names(self) -> List[str]: return super().fact_names(prefix="Sigmoid")
73
+
74
+ class BetaFactLayer(LearnedAtomicFactLayer):
75
+ """A learnable fact layer using a flexible, generalized logistic function."""
76
+ def __init__(self, input_dim: int, num_facts: int, feature_names: List[str]):
77
+ super().__init__(input_dim, num_facts, feature_names)
78
+ self.k_raw = nn.Parameter(torch.ones(num_facts) * 0.5)
79
+ self.nu_raw = nn.Parameter(torch.zeros(num_facts))
80
+
81
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
82
+ diffs = self.get_base_diffs(x)
83
+ k = torch.nn.functional.softplus(self.k_raw) + 1e-4
84
+ nu = torch.nn.functional.softplus(self.nu_raw) + 1e-4
85
+ return (1 + torch.exp(-k * diffs)) ** (-nu)
86
+
87
+ @property
88
+ def fact_names(self) -> List[str]: return super().fact_names(prefix="Beta")
89
+
90
+ # --- Rule/Concept Layers ---
91
+
92
+ class LogicalRuleLayer(nn.Module):
93
+ """Forms logical rules (AND) from facts and outputs higher-level concepts."""
94
+ def __init__(self, input_dim: int, num_rules: int, input_fact_names: List[str]):
95
+ super().__init__()
96
+ torch.manual_seed(input_dim + num_rules)
97
+
98
+ if input_dim > 0 and num_rules > 0:
99
+ self.register_buffer('rule_indices', torch.randint(0, input_dim, size=(num_rules, 2)))
100
+ self.rule_names = [f"({input_fact_names[i]} AND {input_fact_names[j]})" for i, j in self.rule_indices]
101
+ else:
102
+ self.register_buffer('rule_indices', torch.empty(0, 2, dtype=torch.long))
103
+ self.rule_names = []
104
+
105
+ self.concept_generator = nn.Linear(num_rules, num_rules)
106
+ self.concept_names = [f"Concept-{i}" for i in range(num_rules)]
107
+
108
+ def forward(self, facts: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
109
+ if self.rule_indices.shape[0] == 0:
110
+ return torch.zeros(facts.shape[0], 0).to(facts.device), torch.zeros(facts.shape[0], 0).to(facts.device)
111
+ fact1, fact2 = facts[:, self.rule_indices[:, 0]], facts[:, self.rule_indices[:, 1]]
112
+ rule_activations = fact1 * fact2
113
+ concepts = torch.sigmoid(self.concept_generator(rule_activations))
114
+ return concepts, rule_activations
115
+
116
+ @property
117
+ def output_dim(self) -> int: return len(self.concept_names)
nous/models.py ADDED
@@ -0,0 +1,65 @@
1
+ # nous/models.py
2
+ import torch
3
+ import torch.nn as nn
4
+ from typing import List, Literal, Tuple
5
+ from .layers import ExhaustiveAtomicFactLayer, SigmoidFactLayer, BetaFactLayer, LogicalRuleLayer
6
+
7
+ class NousBlock(nn.Module):
8
+ """A single reasoning block in the Nous network with a residual connection."""
9
+ def __init__(self, input_dim: int, num_rules: int, input_fact_names: List[str]):
10
+ super().__init__()
11
+ self.rule_layer = LogicalRuleLayer(input_dim, num_rules, input_fact_names)
12
+ self.projection = nn.Linear(input_dim, num_rules) if input_dim != num_rules else nn.Identity()
13
+ self.norm = nn.LayerNorm(num_rules)
14
+
15
+ def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
16
+ concepts, rule_activations = self.rule_layer(x)
17
+ output = self.norm(self.projection(x) + concepts)
18
+ return output, concepts, rule_activations
19
+
20
+ @property
21
+ def concept_names(self) -> List[str]:
22
+ return self.rule_layer.concept_names
23
+
24
+ class NousNet(nn.Module):
25
+ """
26
+ The complete Nous neuro-symbolic network for regression and classification.
27
+ """
28
+ def __init__(self,
29
+ input_dim: int,
30
+ output_dim: int,
31
+ feature_names: List[str],
32
+ fact_layer_type: Literal['beta', 'sigmoid', 'exhaustive'] = 'beta',
33
+ num_facts: int = 30,
34
+ num_rules_per_layer: List[int] = [10, 5]):
35
+ super().__init__()
36
+ self.feature_names = feature_names
37
+
38
+ if fact_layer_type == 'beta':
39
+ self.atomic_fact_layer = BetaFactLayer(input_dim, num_facts, feature_names)
40
+ elif fact_layer_type == 'sigmoid':
41
+ self.atomic_fact_layer = SigmoidFactLayer(input_dim, num_facts, feature_names)
42
+ elif fact_layer_type == 'exhaustive':
43
+ self.atomic_fact_layer = ExhaustiveAtomicFactLayer(input_dim, feature_names)
44
+ else:
45
+ raise ValueError("fact_layer_type must be 'beta', 'sigmoid', or 'exhaustive'")
46
+
47
+ self.nous_blocks = nn.ModuleList()
48
+ current_dim = self.atomic_fact_layer.output_dim
49
+
50
+ for i, num_rules in enumerate(num_rules_per_layer):
51
+ input_names = self.atomic_fact_layer.fact_names if i == 0 else self.nous_blocks[i-1].concept_names
52
+ block = NousBlock(current_dim, num_rules, input_names)
53
+ self.nous_blocks.append(block)
54
+ current_dim = num_rules
55
+
56
+ self.output_head = nn.Linear(current_dim, output_dim)
57
+
58
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
59
+ """
60
+ Forward pass. Returns logits for classification or direct values for regression.
61
+ """
62
+ h = self.atomic_fact_layer(x)
63
+ for block in self.nous_blocks:
64
+ h, _, _ = block(h)
65
+ return self.output_head(h)
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: nous
3
+ Version: 0.1.0
4
+ Summary: Nous: A Neuro-Symbolic Library for Interpretable and Causal Reasoning AI
5
+ Author-email: Islam Tlupov <tlupovislam@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Islam Tlupov
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/EmotionEngineer/nous
29
+ Project-URL: Repository, https://github.com/EmotionEngineer/nous
30
+ Classifier: Development Status :: 3 - Alpha
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: Intended Audience :: Science/Research
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
36
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
+ Classifier: Typing :: Typed
38
+ Requires-Python: >=3.7
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: torch>=1.8.0
42
+ Requires-Dist: pandas>=1.3.0
43
+ Requires-Dist: scikit-learn>=1.0
44
+ Requires-Dist: matplotlib>=3.3.0
45
+ Requires-Dist: networkx>=2.6
46
+ Requires-Dist: seaborn>=0.11
47
+ Dynamic: license-file
48
+
49
+ # Nous: A Neuro-Symbolic Library for Interpretable AI
50
+
51
+ [![PyPI version](https://badge.fury.io/py/nous.svg)](https://badge.fury.io/py/nous)
52
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
53
+
54
+ **Nous** is a PyTorch-based library for building "white-box" machine learning models for both **regression** and **classification**. It enables models that don't just predict, but also **reason** and **explain** their decisions in human-understandable terms.
55
+
56
+ ## Key Features
57
+
58
+ - **Deeply Interpretable**: Generate a complete, step-by-step logical trace (`fact -> rule -> concept -> prediction`) for any decision.
59
+ - **Supports Regression & Classification**: A unified API for predicting both continuous values and class probabilities.
60
+ - **Causal by Design**: Natively supports counterfactual analysis ("What if?") to provide actionable recommendations for both regression and classification tasks.
61
+ - **High Performance**: Achieves accuracy competitive with traditional black-box models.
62
+ - **Scalable & Flexible**: Choose between a high-performance `beta` activation, a robust `sigmoid`, or a maximally transparent `exhaustive` fact layer.
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install nous
68
+ ```
69
+
70
+ ## Quickstart: A 5-Minute Example (Regression)
71
+
72
+ Let's predict a house price and understand the model's reasoning.
73
+
74
+ ```python
75
+ import torch
76
+ import pandas as pd
77
+ from sklearn.datasets import make_regression
78
+ from nous.models import NousNet
79
+ from nous.interpret import trace_decision_graph, explain_fact
80
+ from nous.causal import find_counterfactual
81
+
82
+ # 1. Prepare Data
83
+ X_raw, y = make_regression(n_samples=1000, n_features=5, n_informative=3, noise=20, random_state=42)
84
+ feature_names = ['area_sqft', 'num_bedrooms', 'dist_to_center', 'age_years', 'renovation_quality']
85
+ X = torch.tensor(X_raw, dtype=torch.float32)
86
+ y = torch.tensor(y, dtype=torch.float32).unsqueeze(1)
87
+
88
+ # 2. Define and Train a NousNet for Regression
89
+ model = NousNet(
90
+ input_dim=5,
91
+ output_dim=1, # Single output for regression
92
+ feature_names=feature_names,
93
+ fact_layer_type='beta'
94
+ )
95
+ # Training: Use a regression loss like nn.MSELoss
96
+ # loss_fn = torch.nn.MSELoss()
97
+ # ... (standard training loop omitted)
98
+ model.eval()
99
+
100
+ # 3. Analyze a specific house
101
+ x_sample = X[50]
102
+ predicted_price = model(x_sample).item()
103
+ print(f"Model's predicted price for house #50: ${predicted_price:,.2f}")
104
+
105
+ # 4. Get the Step-by-Step Reasoning
106
+ graph = trace_decision_graph(model, x_sample)
107
+ top_facts = sorted(graph['trace']['Atomic Facts'].items(), key=lambda i: i[1]['value'], reverse=True)
108
+ fact_to_analyze = top_facts[0][0]
109
+ print(f"\nTop activated fact influencing the price: '{fact_to_analyze}'")
110
+
111
+ # 5. Decode the Learned Fact
112
+ details_df = explain_fact(model, fact_name=fact_to_analyze)
113
+ print(f"\nDecoding '{fact_to_analyze}':")
114
+ display(details_df.head())
115
+
116
+ # 6. Get an Actionable Recommendation
117
+ # What's the smallest change to increase the predicted price to $150,000?
118
+ recommendation = find_counterfactual(
119
+ model,
120
+ x_sample,
121
+ target_output=150.0, # Target value for regression
122
+ task='regression'
123
+ )
124
+ print("\nRecommendation to increase value to $150k:")
125
+ for feature, old_val, new_val in recommendation['changes']:
126
+ print(f"- Change '{feature}' from {old_val:.2f} to {new_val:.2f}")
127
+
128
+ ```
129
+
130
+ ## Choosing a `fact_layer_type`
131
+
132
+ - `'beta'` (**Default, Recommended**): Best performance and flexibility.
133
+ - `'sigmoid'`: A robust and reliable alternative.
134
+ - `'exhaustive'`: Maximum transparency. Best for low-dimensional problems (<15 features).
135
+
136
+ ## License
137
+
138
+ This project is licensed under the MIT License.
@@ -0,0 +1,10 @@
1
+ nous/__init__.py,sha256=-nnKnlgTh2wEqPP4Cz3zUFY0jrU6Y6BGrd-4mMDu6IE,545
2
+ nous/causal.py,sha256=U2_pQYpIyM7VhV0mlmmD-kQgyNLMYEH1MnN5-dLZiZA,2488
3
+ nous/interpret.py,sha256=QcBceWmGxvLLXvmTA1_T3G6MmyovPV8NA5sxu04CdUw,5721
4
+ nous/layers.py,sha256=4Uv0JkhK3EkPbZ1sdbpFK0AkU6IOsyQUjCoyjwz3ZOQ,5651
5
+ nous/models.py,sha256=qRhiN7_uAkmm7xIGgXR6gkzl4rArb6E-LkAEnREOYf4,2849
6
+ nous-0.1.0.dist-info/licenses/LICENSE,sha256=07nO-ZFpy_s_msfks8VsONyV2cBBggqsEQD2h5sdVRo,1069
7
+ nous-0.1.0.dist-info/METADATA,sha256=uZUy6b43xDtS_iHSugbjTZ8BmFwoRKn9mG1gT43VEIg,5992
8
+ nous-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ nous-0.1.0.dist-info/top_level.txt,sha256=yUcst4OAspsyKhX0y5ENzFkJKzR_gislA5MykV1pVbk,5
10
+ nous-0.1.0.dist-info/RECORD,,
@@ -1,43 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: nous
3
- Version: 0.0.1
4
- Summary: Placeholder for the Nous neuro-symbolic AI library. Full implementation coming soon.
5
- Author-email: Islam Tlupov <tlupovislam@gmail.com>
6
- License: MIT License
7
-
8
- Copyright (c) 2025 Islam Tlupov
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
- Project-URL: Homepage, https://github.com/EmotionEngineer/nous
29
- Classifier: Development Status :: 1 - Planning
30
- Classifier: Intended Audience :: Developers
31
- Classifier: License :: OSI Approved :: MIT License
32
- Classifier: Programming Language :: Python :: 3
33
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
34
- Requires-Python: >=3.8
35
- Description-Content-Type: text/markdown
36
- License-File: LICENSE
37
- Dynamic: license-file
38
-
39
- # nous
40
-
41
- **This package name is reserved for the Nous neuro-symbolic AI library.**
42
-
43
- A full implementation is in active development and will be published here soon.
@@ -1,6 +0,0 @@
1
- nous/__init__.py,sha256=DWjI6vFWsOzYGsmsG1qqVeTxVV0us1pJDY4eJTBJ7CM,41
2
- nous-0.0.1.dist-info/licenses/LICENSE,sha256=07nO-ZFpy_s_msfks8VsONyV2cBBggqsEQD2h5sdVRo,1069
3
- nous-0.0.1.dist-info/METADATA,sha256=wtP4EFlpg-OVzKXKW-IMQa_67ulktyUN7D-R_Wx_YFM,2078
4
- nous-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
- nous-0.0.1.dist-info/top_level.txt,sha256=yUcst4OAspsyKhX0y5ENzFkJKzR_gislA5MykV1pVbk,5
6
- nous-0.0.1.dist-info/RECORD,,
File without changes