Skip to content

Commit 165473a

Browse files
authored
Merge pull request #281 from PanduhBeer/master
Add ThumbyAI Neural Network Demo
2 parents a46bcb9 + 663fec5 commit 165473a

File tree

3 files changed

+416
-0
lines changed

3 files changed

+416
-0
lines changed

ThumbyAI/ThumbyAI.py

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
import thumby
2+
import time
3+
import math
4+
import random
5+
import gc
6+
7+
#
8+
# --------------------------------------------------------------------
9+
# FONT & SCREEN SETUP
10+
# --------------------------------------------------------------------
11+
# Thumby includes a 72x40 pixel display. The 3x5 font in "/lib/font3x5.bin"
12+
# saves space when drawing text on screen.
13+
thumby.display.setFont("/lib/font3x5.bin", 3, 5, 1)
14+
SCREEN_W = 72
15+
SCREEN_H = 40
16+
17+
# Each 3x5 character occupies about 4 pixels in width (3 for the glyph + 1 spacing),
18+
# so a single line can hold ~18 characters in 72 pixels of width.
19+
MAX_CHARS_PER_LINE = 18
20+
21+
#
22+
# --------------------------------------------------------------------
23+
# DATASET: CELSIUS -> FAHRENHEIT (21 SAMPLES)
24+
# --------------------------------------------------------------------
25+
# Celsius values range from 0 to 100 in steps of 5, producing a list of 21 total
26+
# samples as inputs for training.
27+
#
28+
# Fahrenheit is computed by F = (9/5)*C + 32, producing a corresponding list of 21
29+
# samples as targets for training.
30+
#
31+
# The network sees only the numeric training pairs.
32+
c_values = list(range(0, 101, 5))
33+
f_values = [(9/5)*c + 32 for c in c_values]
34+
35+
# Normalization is required because the hidden layer uses sigmoid,
36+
# which expects inputs and outputs in or near [0..1].
37+
def normalize_c(c):
38+
return c / 100.0
39+
40+
def normalize_f(f):
41+
return (f - 32.0) / 180.0
42+
43+
def denormalize_f(fn):
44+
return fn * 180.0 + 32.0
45+
46+
# Build the training set by normalizing c_values and f_values.
47+
training_inputs = [[normalize_c(c)] for c in c_values]
48+
training_targets = [[normalize_f(f)] for f in f_values]
49+
50+
#
51+
# --------------------------------------------------------------------
52+
# NEURAL NETWORK: 1 -> 5 -> 1 (LINEAR OUTPUT)
53+
# --------------------------------------------------------------------
54+
# This network has:
55+
# - 1 input neuron (normalized C)
56+
# - 5 hidden neurons (using sigmoid)
57+
# - 1 output neuron (using a linear activation)
58+
# The final output is unconstrained, which helps a linear mapping like C->F.
59+
INPUT_SIZE = 1
60+
HIDDEN_SIZE = 5
61+
OUTPUT_SIZE = 1
62+
63+
W1 = []
64+
B1 = []
65+
W2 = []
66+
B2 = []
67+
68+
# Training parameters
69+
LEARNING_RATE = 0.2 # Lower rate for stability
70+
EPOCHS = 500 # Increase to improve convergence
71+
72+
def init_network():
73+
# Initializes all weights/biases randomly in [-1, 1].
74+
# Runs garbage collection to free memory on the Thumby.
75+
global W1, B1, W2, B2
76+
gc.collect()
77+
78+
def rand_weight():
79+
return random.uniform(-1, 1)
80+
81+
# W1 is (HIDDEN_SIZE x INPUT_SIZE), B1 is (HIDDEN_SIZE,)
82+
W1 = [[rand_weight() for _ in range(INPUT_SIZE)] for _ in range(HIDDEN_SIZE)]
83+
B1 = [rand_weight() for _ in range(HIDDEN_SIZE)]
84+
85+
# W2 is (OUTPUT_SIZE x HIDDEN_SIZE), B2 is (OUTPUT_SIZE,)
86+
W2 = [[rand_weight() for _ in range(HIDDEN_SIZE)] for _ in range(OUTPUT_SIZE)]
87+
B2 = [rand_weight() for _ in range(OUTPUT_SIZE)]
88+
89+
gc.collect()
90+
91+
def sigmoid(x):
92+
# Applies a sigmoid clamp to avoid overflow on exponent.
93+
if x > 10:
94+
x = 10
95+
if x < -10:
96+
x = -10
97+
return 1.0 / (1.0 + math.e**(-x))
98+
99+
def dsigmoid(y):
100+
# Computes derivative of sigmoid if y=sigmoid(x).
101+
return y * (1.0 - y)
102+
103+
#
104+
# --------------------------------------------------------------------
105+
# FORWARD PASS (HIDDEN: SIGMOID, OUTPUT: LINEAR)
106+
# --------------------------------------------------------------------
107+
def forward_pass(x):
108+
# Passes input x through the hidden layer using sigmoid,
109+
# then computes a linear output with no final sigmoid.
110+
global W1, B1, W2, B2
111+
112+
# Hidden layer
113+
h = []
114+
for i in range(HIDDEN_SIZE):
115+
sum_h = B1[i]
116+
for j in range(INPUT_SIZE):
117+
sum_h += W1[i][j] * x[j]
118+
h.append(sigmoid(sum_h))
119+
120+
# Output layer (linear)
121+
o = []
122+
for i in range(OUTPUT_SIZE):
123+
sum_o = B2[i]
124+
for j in range(HIDDEN_SIZE):
125+
sum_o += W2[i][j] * h[j]
126+
o.append(sum_o) # raw linear output
127+
return h, o
128+
129+
#
130+
# --------------------------------------------------------------------
131+
# BACKPROP (FINAL LAYER IS LINEAR)
132+
# --------------------------------------------------------------------
133+
def backprop(x, h, o, t):
134+
# Adjusts weights/biases in two stages:
135+
# 1) Linear output error
136+
# 2) Sigmoid hidden layer error
137+
global W1, B1, W2, B2
138+
139+
# Output layer error: (t - o) for a linear final neuron
140+
output_errors = []
141+
for i in range(OUTPUT_SIZE):
142+
error = t[i] - o[i]
143+
output_errors.append(error)
144+
145+
# Hidden layer errors (uses dsigmoid on the hidden output)
146+
hidden_errors = []
147+
for i in range(HIDDEN_SIZE):
148+
err_sum = 0.0
149+
for j in range(OUTPUT_SIZE):
150+
err_sum += output_errors[j] * W2[j][i]
151+
hidden_errors.append(err_sum * dsigmoid(h[i]))
152+
153+
# Update W2, B2 (linear output layer)
154+
for i in range(OUTPUT_SIZE):
155+
for j in range(HIDDEN_SIZE):
156+
W2[i][j] += LEARNING_RATE * output_errors[i] * h[j]
157+
B2[i] += LEARNING_RATE * output_errors[i]
158+
159+
# Update W1, B1 (sigmoid hidden layer)
160+
for i in range(HIDDEN_SIZE):
161+
for j in range(INPUT_SIZE):
162+
W1[i][j] += LEARNING_RATE * hidden_errors[i] * x[j]
163+
B1[i] += LEARNING_RATE * hidden_errors[i]
164+
165+
#
166+
# --------------------------------------------------------------------
167+
# TRAIN NETWORK
168+
# --------------------------------------------------------------------
169+
def train_network():
170+
# Runs EPOCHS times through the entire dataset.
171+
for epoch in range(EPOCHS):
172+
for i, inp in enumerate(training_inputs):
173+
t = training_targets[i]
174+
h, o = forward_pass(inp)
175+
backprop(inp, h, o, t)
176+
177+
#
178+
# --------------------------------------------------------------------
179+
# TRAINING SCREEN
180+
# --------------------------------------------------------------------
181+
def display_training_screen():
182+
# Clears the screen, draws "TRAINING..." centered, then updates.
183+
thumby.display.fill(0)
184+
text_str = "TRAINING..."
185+
text_str_2 = "Please Wait"
186+
text_width = len(text_str) * 4
187+
x = (SCREEN_W - text_width) // 2
188+
y = (SCREEN_H - 7) // 2
189+
thumby.display.drawText(text_str, x, y - 2, 1)
190+
thumby.display.drawText(text_str_2, x, y + 6, 1)
191+
thumby.display.update()
192+
193+
#
194+
# --------------------------------------------------------------------
195+
# WRAPPING FUNCTIONS FOR TUTORIAL
196+
# --------------------------------------------------------------------
197+
def wrap_text(line, max_chars=18):
198+
wrapped = []
199+
start = 0
200+
while start < len(line):
201+
wrapped.append(line[start:start+max_chars])
202+
start += max_chars
203+
return wrapped
204+
205+
def wrap_tutorial_lines(lines, max_chars=18):
206+
final = []
207+
for l in lines:
208+
if len(l) > max_chars:
209+
sub_lines = wrap_text(l, max_chars)
210+
final.extend(sub_lines)
211+
else:
212+
final.append(l)
213+
return final
214+
215+
#
216+
# --------------------------------------------------------------------
217+
# SCROLLABLE TUTORIAL SCREEN
218+
# --------------------------------------------------------------------
219+
def display_tutorial():
220+
raw_tutorial_lines = [
221+
"C->F",
222+
"",
223+
"1->5->1 network,",
224+
"sigmoid hidden,",
225+
"linear output.",
226+
"Steps of 5C from",
227+
"0 to 100.",
228+
"500 epochs,",
229+
"LR=0.2.",
230+
"Should predict",
231+
"100C near 212F.",
232+
"",
233+
"Press Up/Down to",
234+
"scroll. Press B to exit."
235+
]
236+
237+
tutorial_lines = wrap_tutorial_lines(raw_tutorial_lines, MAX_CHARS_PER_LINE)
238+
scroll_offset = 0
239+
240+
while True:
241+
thumby.display.fill(0)
242+
for i in range(5):
243+
line_index = scroll_offset + i
244+
if line_index < len(tutorial_lines):
245+
thumby.display.drawText(tutorial_lines[line_index], 0, i*7, 1)
246+
thumby.display.update()
247+
248+
if thumby.buttonD.justPressed():
249+
if scroll_offset < (len(tutorial_lines) - 5):
250+
scroll_offset += 1
251+
time.sleep(0.1)
252+
if thumby.buttonU.justPressed():
253+
if scroll_offset > 0:
254+
scroll_offset -= 1
255+
time.sleep(0.1)
256+
if thumby.buttonB.justPressed():
257+
break
258+
time.sleep(0.05)
259+
260+
#
261+
# --------------------------------------------------------------------
262+
# SCROLLABLE RESULTS SCREEN WITH WEIGHTS
263+
# --------------------------------------------------------------------
264+
def display_results():
265+
# Displays final predictions and then shows final weights/biases.
266+
global W1, B1, W2, B2
267+
268+
result_lines = []
269+
result_lines.append("TRAINED NN RESULTS")
270+
271+
# Predict F for each Celsius in c_values
272+
for c_val in c_values:
273+
c_norm = [normalize_c(c_val)]
274+
_, out = forward_pass(c_norm)
275+
f_pred = denormalize_f(out[0])
276+
line = f"{c_val}C => {f_pred:.1f}F"
277+
result_lines.append(line)
278+
279+
# Show final weights and biases. Round to 2 decimals to fit display space.
280+
# W1: shape (5,1) since there is 1 input and 5 hidden
281+
# B1: shape (5,)
282+
# W2: shape (1,5) since there is 1 output and 5 hidden
283+
# B2: shape (1,)
284+
285+
# For W1, gather each row's single value (since input=1)
286+
w1_values = [round(W1[i][0], 2) for i in range(HIDDEN_SIZE)]
287+
b1_values = [round(B1[i], 2) for i in range(HIDDEN_SIZE)]
288+
w2_values = [round(W2[0][i], 2) for i in range(HIDDEN_SIZE)] # row 0, col i
289+
b2_value = round(B2[0], 2)
290+
291+
result_lines.append("----------")
292+
result_lines.append("W1:"+str(w1_values))
293+
result_lines.append("B1:"+str(b1_values))
294+
result_lines.append("W2:"+str(w2_values))
295+
result_lines.append("B2:"+str([b2_value]))
296+
result_lines.append("PRESS B TO EXIT")
297+
298+
scroll_offset = 0
299+
while True:
300+
thumby.display.fill(0)
301+
for i in range(5):
302+
line_index = scroll_offset + i
303+
if line_index < len(result_lines):
304+
thumby.display.drawText(result_lines[line_index], 0, i*7, 1)
305+
thumby.display.update()
306+
307+
if thumby.buttonD.justPressed():
308+
if scroll_offset < (len(result_lines) - 5):
309+
scroll_offset += 1
310+
time.sleep(0.1)
311+
if thumby.buttonU.justPressed():
312+
if scroll_offset > 0:
313+
scroll_offset -= 1
314+
time.sleep(0.1)
315+
if thumby.buttonB.justPressed():
316+
break
317+
time.sleep(0.05)
318+
319+
#
320+
# --------------------------------------------------------------------
321+
# MAIN MENU
322+
# --------------------------------------------------------------------
323+
menu_items = ["Train NN", "Tutorial", "Exit"]
324+
selected_index = 0
325+
326+
def draw_menu():
327+
thumby.display.fill(0)
328+
thumby.display.drawText("C->F AI DEMO", 0, 0, 1)
329+
y = 10
330+
for i, item in enumerate(menu_items):
331+
marker = ">" if i == selected_index else " "
332+
line = marker + " " + item
333+
thumby.display.drawText(line, 0, y, 1)
334+
y += 8
335+
thumby.display.update()
336+
337+
def main_menu():
338+
global selected_index
339+
while True:
340+
draw_menu()
341+
if thumby.buttonU.justPressed():
342+
selected_index = max(0, selected_index - 1)
343+
time.sleep(0.1)
344+
if thumby.buttonD.justPressed():
345+
selected_index = min(len(menu_items)-1, selected_index + 1)
346+
time.sleep(0.1)
347+
348+
if thumby.buttonA.justPressed():
349+
choice = menu_items[selected_index]
350+
if choice == "Train NN":
351+
init_network()
352+
display_training_screen() # Shows TRAINING... in the center
353+
train_network()
354+
display_results()
355+
elif choice == "Tutorial":
356+
display_tutorial()
357+
elif choice == "Exit":
358+
break
359+
time.sleep(0.1)
360+
361+
#
362+
# --------------------------------------------------------------------
363+
# START PROGRAM
364+
# --------------------------------------------------------------------
365+
main_menu()

0 commit comments

Comments
 (0)