tsetly 0.0.1__tar.gz
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.
- tsetly-0.0.1/LICENSE +21 -0
- tsetly-0.0.1/PKG-INFO +90 -0
- tsetly-0.0.1/README.md +73 -0
- tsetly-0.0.1/pyproject.toml +25 -0
- tsetly-0.0.1/src/tsetly/__init__.py +220 -0
- tsetly-0.0.1/src/tsetly/py.typed +0 -0
- tsetly-0.0.1/tests/test_tsetly.py +51 -0
tsetly-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The tsetly authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tsetly-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tsetly
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A tiny Tsetlin Machine in NumPy: little automata that learn rules you can read.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: interpretable machine learning,learning automata,propositional logic,tsetlin machine
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Education
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Requires-Dist: numpy>=1.21
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# tsetly
|
|
19
|
+
|
|
20
|
+
A tiny Tsetlin Machine in NumPy: little automata that learn rules you can read.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
forget ◀────────────────┼────────────────▶ remember
|
|
24
|
+
1 2 3 … N │ N+1 … 2N
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A Tsetlin Machine learns AND-rules over bits, like `x0 ∧ ¬x1`. Every rule is a
|
|
28
|
+
team of Tsetlin automata, one per bit and one per negated bit. Each automaton
|
|
29
|
+
walks along its states as the data rewards or penalises it, and once it crosses
|
|
30
|
+
the middle, its bit joins the rule. Rules vote for or against each class, and
|
|
31
|
+
the class with the most votes wins. That is the whole machine
|
|
32
|
+
([Granmo, 2018](https://arxiv.org/abs/1804.01508)).
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install tsetly
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Learn XOR through noise
|
|
41
|
+
|
|
42
|
+
Twelve random bits, of which only the first two matter, and a fifth of the
|
|
43
|
+
training labels flipped:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import numpy as np
|
|
47
|
+
from tsetly import TsetlinMachine
|
|
48
|
+
|
|
49
|
+
rng = np.random.default_rng(0)
|
|
50
|
+
X = rng.integers(0, 2, size=(5000, 12))
|
|
51
|
+
y = X[:, 0] ^ X[:, 1]
|
|
52
|
+
noisy = np.where(rng.random(5000) < 0.2, 1 - y, y)
|
|
53
|
+
|
|
54
|
+
tm = TsetlinMachine(clauses=8, T=15, s=3.9, seed=1)
|
|
55
|
+
tm.fit(X[:4000], noisy[:4000], epochs=20)
|
|
56
|
+
|
|
57
|
+
print(tm.score(X[4000:], y[4000:])) # 1.0
|
|
58
|
+
print(tm)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
class 0
|
|
63
|
+
+2 ¬x0 ∧ ¬x1
|
|
64
|
+
+2 x0 ∧ x1
|
|
65
|
+
-2 x0 ∧ ¬x1
|
|
66
|
+
-2 ¬x0 ∧ x1
|
|
67
|
+
class 1
|
|
68
|
+
+3 ¬x0 ∧ x1
|
|
69
|
+
+1 x0 ∧ ¬x1
|
|
70
|
+
-3 ¬x0 ∧ ¬x1
|
|
71
|
+
-1 x0 ∧ x1
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It found XOR, ignored the ten distracting bits, and can tell you so.
|
|
75
|
+
|
|
76
|
+
## API
|
|
77
|
+
|
|
78
|
+
| | |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `TsetlinMachine(clauses=20, T=15, s=3.9, states=100, seed=None)` | `clauses` per class (half vote for, half against), the vote target `T`, the specificity `s`, the states per action |
|
|
81
|
+
| `fit(X, y, epochs=10)` | learn from bits `X` (samples × features, 0/1) and labels `y` of any kind |
|
|
82
|
+
| `predict(X)` | the class with the most votes |
|
|
83
|
+
| `votes(X)` | every class's vote total, samples × classes |
|
|
84
|
+
| `score(X, y)` | accuracy |
|
|
85
|
+
| `rules(label, names=None)` | a class's rules and their votes, optionally with your feature names |
|
|
86
|
+
| `print(tm)` | every rule the machine learned |
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
tsetly-0.0.1/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# tsetly
|
|
2
|
+
|
|
3
|
+
A tiny Tsetlin Machine in NumPy: little automata that learn rules you can read.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
forget ◀────────────────┼────────────────▶ remember
|
|
7
|
+
1 2 3 … N │ N+1 … 2N
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
A Tsetlin Machine learns AND-rules over bits, like `x0 ∧ ¬x1`. Every rule is a
|
|
11
|
+
team of Tsetlin automata, one per bit and one per negated bit. Each automaton
|
|
12
|
+
walks along its states as the data rewards or penalises it, and once it crosses
|
|
13
|
+
the middle, its bit joins the rule. Rules vote for or against each class, and
|
|
14
|
+
the class with the most votes wins. That is the whole machine
|
|
15
|
+
([Granmo, 2018](https://arxiv.org/abs/1804.01508)).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pip install tsetly
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Learn XOR through noise
|
|
24
|
+
|
|
25
|
+
Twelve random bits, of which only the first two matter, and a fifth of the
|
|
26
|
+
training labels flipped:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import numpy as np
|
|
30
|
+
from tsetly import TsetlinMachine
|
|
31
|
+
|
|
32
|
+
rng = np.random.default_rng(0)
|
|
33
|
+
X = rng.integers(0, 2, size=(5000, 12))
|
|
34
|
+
y = X[:, 0] ^ X[:, 1]
|
|
35
|
+
noisy = np.where(rng.random(5000) < 0.2, 1 - y, y)
|
|
36
|
+
|
|
37
|
+
tm = TsetlinMachine(clauses=8, T=15, s=3.9, seed=1)
|
|
38
|
+
tm.fit(X[:4000], noisy[:4000], epochs=20)
|
|
39
|
+
|
|
40
|
+
print(tm.score(X[4000:], y[4000:])) # 1.0
|
|
41
|
+
print(tm)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
class 0
|
|
46
|
+
+2 ¬x0 ∧ ¬x1
|
|
47
|
+
+2 x0 ∧ x1
|
|
48
|
+
-2 x0 ∧ ¬x1
|
|
49
|
+
-2 ¬x0 ∧ x1
|
|
50
|
+
class 1
|
|
51
|
+
+3 ¬x0 ∧ x1
|
|
52
|
+
+1 x0 ∧ ¬x1
|
|
53
|
+
-3 ¬x0 ∧ ¬x1
|
|
54
|
+
-1 x0 ∧ x1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
It found XOR, ignored the ten distracting bits, and can tell you so.
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
| | |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `TsetlinMachine(clauses=20, T=15, s=3.9, states=100, seed=None)` | `clauses` per class (half vote for, half against), the vote target `T`, the specificity `s`, the states per action |
|
|
64
|
+
| `fit(X, y, epochs=10)` | learn from bits `X` (samples × features, 0/1) and labels `y` of any kind |
|
|
65
|
+
| `predict(X)` | the class with the most votes |
|
|
66
|
+
| `votes(X)` | every class's vote total, samples × classes |
|
|
67
|
+
| `score(X, y)` | accuracy |
|
|
68
|
+
| `rules(label, names=None)` | a class's rules and their votes, optionally with your feature names |
|
|
69
|
+
| `print(tm)` | every rule the machine learned |
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tsetly"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "A tiny Tsetlin Machine in NumPy: little automata that learn rules you can read."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = ["numpy>=1.21"]
|
|
14
|
+
keywords = ["tsetlin machine", "interpretable machine learning", "propositional logic", "learning automata"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Education",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.sdist]
|
|
25
|
+
include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml"]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""A tiny Tsetlin Machine in NumPy.
|
|
2
|
+
|
|
3
|
+
A Tsetlin Machine learns rules you can read: AND-clauses over input bits and
|
|
4
|
+
their negations, such as ``x0 ∧ ¬x1``. Each clause is a team of Tsetlin
|
|
5
|
+
automata, one per literal, and each automaton decides whether its literal
|
|
6
|
+
belongs in the clause. Clauses vote for or against each class, and the class
|
|
7
|
+
with the most votes wins. This is the vanilla multiclass Tsetlin Machine of
|
|
8
|
+
Granmo (2018), https://arxiv.org/abs/1804.01508.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections import Counter
|
|
14
|
+
from typing import Any, Sequence
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
__all__ = ["TsetlinMachine"]
|
|
19
|
+
__version__ = "0.0.1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TsetlinMachine:
|
|
23
|
+
"""A multiclass Tsetlin Machine.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
clauses : int
|
|
28
|
+
Clauses per class, an even number: half vote for the class, half against it.
|
|
29
|
+
T : int
|
|
30
|
+
The vote total each class aims for. Larger values let more clauses
|
|
31
|
+
learn each pattern.
|
|
32
|
+
s : float
|
|
33
|
+
Specificity, above 1. Larger values learn longer, more specific rules.
|
|
34
|
+
states : int
|
|
35
|
+
States per action. An automaton includes its literal once its state
|
|
36
|
+
is above ``states``.
|
|
37
|
+
seed : int, optional
|
|
38
|
+
Seed for reproducible training.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
>>> import numpy as np
|
|
43
|
+
>>> from tsetly import TsetlinMachine
|
|
44
|
+
>>> X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]] * 25)
|
|
45
|
+
>>> y = X[:, 0] ^ X[:, 1]
|
|
46
|
+
>>> tm = TsetlinMachine(clauses=4, T=3, s=3.0, seed=0).fit(X, y, epochs=10)
|
|
47
|
+
>>> tm.score(X, y)
|
|
48
|
+
1.0
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self, clauses: int = 20, T: int = 15, s: float = 3.9, states: int = 100, seed: int | None = None
|
|
53
|
+
) -> None:
|
|
54
|
+
if clauses < 2 or clauses % 2:
|
|
55
|
+
raise ValueError(f"clauses must be an even number of at least 2, got {clauses}")
|
|
56
|
+
if T < 1:
|
|
57
|
+
raise ValueError(f"T must be at least 1, got {T}")
|
|
58
|
+
if s <= 1:
|
|
59
|
+
raise ValueError(f"s must be above 1, got {s}")
|
|
60
|
+
if states < 1:
|
|
61
|
+
raise ValueError(f"states must be at least 1, got {states}")
|
|
62
|
+
self.clauses = clauses
|
|
63
|
+
self.T = T
|
|
64
|
+
self.s = s
|
|
65
|
+
self.states = states
|
|
66
|
+
self.seed = seed
|
|
67
|
+
self.classes_: np.ndarray | None = None
|
|
68
|
+
self.automata_: np.ndarray | None = None # classes × clauses × literals, states 1 … 2·states
|
|
69
|
+
self._polarity = np.where(np.arange(clauses) % 2 == 0, 1, -1) # even clauses vote for, odd against
|
|
70
|
+
|
|
71
|
+
def fit(self, X: Any, y: Any, epochs: int = 10) -> TsetlinMachine:
|
|
72
|
+
"""Learn from scratch.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
X : array_like
|
|
77
|
+
Samples × features of bits (0/1 or False/True).
|
|
78
|
+
y : array_like
|
|
79
|
+
One label per sample, of any kind numpy can sort.
|
|
80
|
+
epochs : int
|
|
81
|
+
Passes over the data, each in a fresh random order.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
TsetlinMachine
|
|
86
|
+
The machine itself, trained.
|
|
87
|
+
"""
|
|
88
|
+
X = _bits(X)
|
|
89
|
+
y = np.asarray(y)
|
|
90
|
+
if y.shape != (len(X),):
|
|
91
|
+
raise ValueError(f"y must hold one label per sample ({len(X)}), got shape {y.shape}")
|
|
92
|
+
self.classes_, targets = np.unique(y, return_inverse=True)
|
|
93
|
+
if len(self.classes_) < 2:
|
|
94
|
+
raise ValueError("y must hold at least two classes")
|
|
95
|
+
# Every automaton starts on the fence: one step short of including its literal.
|
|
96
|
+
shape = (len(self.classes_), self.clauses, 2 * X.shape[1])
|
|
97
|
+
self.automata_ = np.full(shape, self.states, dtype=np.int32)
|
|
98
|
+
rng = np.random.default_rng(self.seed)
|
|
99
|
+
for _ in range(epochs):
|
|
100
|
+
for i in rng.permutation(len(X)):
|
|
101
|
+
self._learn(X[i], int(targets[i]), rng)
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def votes(self, X: Any) -> np.ndarray:
|
|
105
|
+
"""Each class's vote total for each sample, samples × classes."""
|
|
106
|
+
X = self._check(X)
|
|
107
|
+
literals = np.hstack([X, ~X])
|
|
108
|
+
include = self.automata_ > self.states
|
|
109
|
+
classes, clauses, width = include.shape
|
|
110
|
+
flat = include.reshape(-1, width).astype(np.int32)
|
|
111
|
+
misses = flat @ (~literals).T.astype(np.int32) # included literals that are 0, per clause and sample
|
|
112
|
+
fires = (misses == 0) & flat.any(axis=1, keepdims=True) # empty clauses stay silent when predicting
|
|
113
|
+
return np.einsum("cjn,j->nc", fires.reshape(classes, clauses, -1).astype(np.int32), self._polarity)
|
|
114
|
+
|
|
115
|
+
def predict(self, X: Any) -> np.ndarray:
|
|
116
|
+
"""The class with the most votes, for each sample."""
|
|
117
|
+
return self.classes_[np.argmax(self.votes(X), axis=1)]
|
|
118
|
+
|
|
119
|
+
def score(self, X: Any, y: Any) -> float:
|
|
120
|
+
"""The fraction of samples predicted correctly."""
|
|
121
|
+
return float(np.mean(self.predict(X) == np.asarray(y)))
|
|
122
|
+
|
|
123
|
+
def rules(self, label: Any, names: Sequence[str] | None = None) -> list[tuple[str, int]]:
|
|
124
|
+
"""The rules one class learned, with their votes.
|
|
125
|
+
|
|
126
|
+
Clauses that learned the same rule are merged, so ``("x0 ∧ ¬x1", 3)``
|
|
127
|
+
means three clauses vote for the class whenever ``x0 ∧ ¬x1`` holds.
|
|
128
|
+
Negative votes are against it. Rules that never fire are left out.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
label
|
|
133
|
+
A class seen in training.
|
|
134
|
+
names : sequence of str, optional
|
|
135
|
+
One name per feature; ``x0``, ``x1``, … by default.
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
list of (str, int)
|
|
140
|
+
Rules and their votes: those for the class, then those against it,
|
|
141
|
+
the strongest first in each.
|
|
142
|
+
"""
|
|
143
|
+
self._check_fitted()
|
|
144
|
+
found = np.flatnonzero(self.classes_ == label)
|
|
145
|
+
if not len(found):
|
|
146
|
+
raise ValueError(f"no class {label!r}; the classes are {list(self.classes_)}")
|
|
147
|
+
features = self.automata_.shape[2] // 2
|
|
148
|
+
names = list(names) if names is not None else [f"x{i}" for i in range(features)]
|
|
149
|
+
if len(names) != features:
|
|
150
|
+
raise ValueError(f"names must hold one name per feature ({features}), got {len(names)}")
|
|
151
|
+
literals = names + [f"¬{name}" for name in names]
|
|
152
|
+
tally: Counter[str] = Counter()
|
|
153
|
+
for clause, polarity in zip(self.automata_[found[0]] > self.states, self._polarity):
|
|
154
|
+
if not clause.any() or (clause[:features] & clause[features:]).any():
|
|
155
|
+
continue # empty, or a contradiction such as x0 ∧ ¬x0
|
|
156
|
+
chosen = sorted(np.flatnonzero(clause), key=lambda k: (k % features, k))
|
|
157
|
+
tally[" ∧ ".join(literals[k] for k in chosen)] += int(polarity)
|
|
158
|
+
ordered = sorted(tally.items(), key=lambda item: (item[1] < 0, -abs(item[1])))
|
|
159
|
+
return [(rule, votes) for rule, votes in ordered if votes]
|
|
160
|
+
|
|
161
|
+
def __repr__(self) -> str:
|
|
162
|
+
return f"TsetlinMachine(clauses={self.clauses}, T={self.T}, s={self.s}, states={self.states}, seed={self.seed})"
|
|
163
|
+
|
|
164
|
+
def __str__(self) -> str:
|
|
165
|
+
if self.automata_ is None:
|
|
166
|
+
return f"{self!r}, not trained yet"
|
|
167
|
+
lines = []
|
|
168
|
+
for label in self.classes_:
|
|
169
|
+
lines.append(f"class {label}")
|
|
170
|
+
lines += [f" {votes:+3d} {rule}" for rule, votes in self.rules(label)]
|
|
171
|
+
return "\n".join(lines)
|
|
172
|
+
|
|
173
|
+
def _learn(self, x: np.ndarray, target: int, rng: np.random.Generator) -> None:
|
|
174
|
+
literals = np.concatenate([x, ~x])
|
|
175
|
+
other = int(rng.integers(len(self.classes_) - 1))
|
|
176
|
+
other += other >= target # a random class other than the target
|
|
177
|
+
self._feedback(target, literals, +1, rng)
|
|
178
|
+
self._feedback(other, literals, -1, rng)
|
|
179
|
+
|
|
180
|
+
def _feedback(self, label: int, literals: np.ndarray, sign: int, rng: np.random.Generator) -> None:
|
|
181
|
+
"""Nudge one class's clauses so its vote total moves toward ``sign · T``."""
|
|
182
|
+
automata = self.automata_[label]
|
|
183
|
+
include = automata > self.states
|
|
184
|
+
fires = ~np.any(include & ~literals, axis=1) # empty clauses fire while learning
|
|
185
|
+
votes = np.clip(self._polarity @ fires, -self.T, self.T)
|
|
186
|
+
chosen = rng.random(self.clauses) < (self.T - sign * votes) / (2 * self.T)
|
|
187
|
+
type_i = (chosen & (self._polarity == sign))[:, None]
|
|
188
|
+
type_ii = (chosen & (self._polarity != sign))[:, None]
|
|
189
|
+
fired = fires[:, None]
|
|
190
|
+
remembered = fired & literals # the 1-literals of firing clauses
|
|
191
|
+
rare = rng.random(automata.shape) < 1 / self.s # true with probability 1/s
|
|
192
|
+
# Type I: firing clauses grow toward the 1-literals they saw; everything else slowly fades.
|
|
193
|
+
automata += type_i & remembered & ~rare
|
|
194
|
+
automata -= type_i & ~remembered & rare
|
|
195
|
+
# Type II: a clause that fired for the wrong answer picks up a 0-literal, so it stops firing.
|
|
196
|
+
automata += type_ii & fired & ~literals & ~include
|
|
197
|
+
np.clip(automata, 1, 2 * self.states, out=automata)
|
|
198
|
+
|
|
199
|
+
def _check_fitted(self) -> None:
|
|
200
|
+
if self.automata_ is None:
|
|
201
|
+
raise RuntimeError("this TsetlinMachine is not trained yet: call fit first")
|
|
202
|
+
|
|
203
|
+
def _check(self, X: Any) -> np.ndarray:
|
|
204
|
+
self._check_fitted()
|
|
205
|
+
X = _bits(X)
|
|
206
|
+
features = self.automata_.shape[2] // 2
|
|
207
|
+
if X.shape[1] != features:
|
|
208
|
+
raise ValueError(f"X has {X.shape[1]} features, but the machine was trained on {features}")
|
|
209
|
+
return X
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _bits(X: Any) -> np.ndarray:
|
|
213
|
+
X = np.asarray(X)
|
|
214
|
+
if X.ndim != 2:
|
|
215
|
+
raise ValueError(f"X must be 2-D, samples × features, got shape {X.shape}")
|
|
216
|
+
if X.dtype != bool:
|
|
217
|
+
if not np.isin(X, (0, 1)).all():
|
|
218
|
+
raise ValueError("X must hold bits: 0/1 or False/True")
|
|
219
|
+
X = X.astype(bool)
|
|
220
|
+
return X
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from tsetly import TsetlinMachine
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def noisy_xor(noise=0.2, seed=0):
|
|
8
|
+
rng = np.random.default_rng(seed)
|
|
9
|
+
X = rng.integers(0, 2, size=(5000, 12))
|
|
10
|
+
y = X[:, 0] ^ X[:, 1]
|
|
11
|
+
return X, y, np.where(rng.random(len(y)) < noise, 1 - y, y)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_learns_xor_through_noise():
|
|
15
|
+
X, y, noisy = noisy_xor()
|
|
16
|
+
tm = TsetlinMachine(clauses=8, T=15, s=3.9, seed=1).fit(X[:4000], noisy[:4000], epochs=20)
|
|
17
|
+
assert tm.score(X[4000:], y[4000:]) == 1.0
|
|
18
|
+
assert {rule for rule, votes in tm.rules(1) if votes > 0} == {"x0 ∧ ¬x1", "¬x0 ∧ x1"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_same_seed_same_machine():
|
|
22
|
+
X, _, noisy = noisy_xor()
|
|
23
|
+
first = TsetlinMachine(seed=7).fit(X[:500], noisy[:500], epochs=2)
|
|
24
|
+
second = TsetlinMachine(seed=7).fit(X[:500], noisy[:500], epochs=2)
|
|
25
|
+
assert np.array_equal(first.automata_, second.automata_)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_any_labels_and_feature_names():
|
|
29
|
+
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]] * 25, dtype=bool)
|
|
30
|
+
y = np.where(X[:, 0] & X[:, 1], "both", "not both")
|
|
31
|
+
tm = TsetlinMachine(clauses=4, T=3, s=3.0, seed=0).fit(X, y, epochs=10)
|
|
32
|
+
assert list(tm.predict(X[:4])) == ["not both", "not both", "not both", "both"]
|
|
33
|
+
assert tm.votes(X).shape == (100, 2)
|
|
34
|
+
assert ("rain ∧ wind", 2) in tm.rules("both", names=["rain", "wind"])
|
|
35
|
+
assert str(tm).startswith("class both")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_friendly_errors():
|
|
39
|
+
with pytest.raises(ValueError, match="even"):
|
|
40
|
+
TsetlinMachine(clauses=3)
|
|
41
|
+
with pytest.raises(RuntimeError, match="not trained"):
|
|
42
|
+
TsetlinMachine().predict([[0, 1]])
|
|
43
|
+
with pytest.raises(ValueError, match="bits"):
|
|
44
|
+
TsetlinMachine().fit([[0, 2]], [0])
|
|
45
|
+
with pytest.raises(ValueError, match="two classes"):
|
|
46
|
+
TsetlinMachine().fit([[0, 1], [1, 0]], [5, 5])
|
|
47
|
+
tm = TsetlinMachine(seed=0).fit([[0, 1], [1, 0]], [0, 1], epochs=1)
|
|
48
|
+
with pytest.raises(ValueError, match="features"):
|
|
49
|
+
tm.predict([[0, 1, 1]])
|
|
50
|
+
with pytest.raises(ValueError, match="no class"):
|
|
51
|
+
tm.rules(9)
|