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 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)