File size: 5,674 Bytes
4e2ab53 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | import json
import os
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
# 1. Configuration βοΈ
CSV_FILE = "potato_gesture_dataset.csv"
EPOCHS = 120
BATCH_SIZE = 8
LEARNING_RATE = 0.003
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"π Training device: {DEVICE}")
# 2. Load & Prepare Dataset π
if not os.path.exists(CSV_FILE):
# Check if user named it differently or check current folder
csv_candidates = [f for f in os.listdir(".") if f.endswith(".csv")]
if csv_candidates:
CSV_FILE = csv_candidates[0]
print(f"π‘ Found CSV: {CSV_FILE}")
else:
raise FileNotFoundError(
f"β Could not find {CSV_FILE}! Make sure it's in this folder."
)
df = pd.read_csv(CSV_FILE)
print(f"π Loaded {len(df)} total gesture recordings!")
# Extract features and labels
X_raw = df.iloc[:, :-1].values.astype(np.float32) # 90 numbers per sample
labels_raw = df.iloc[:, -1].values
# Convert text labels into numbers: 0, 1, 2, ...
unique_labels = sorted(list(set(labels_raw)))
label_to_idx = {name: i for i, name in enumerate(unique_labels)}
idx_to_label = {i: name for name, i in label_to_idx.items()}
y_raw = np.array([label_to_idx[l] for l in labels_raw], dtype=np.int64)
# Save the label mapping so your web app knows which number is which gesture!
with open("labels.json", "w") as f:
json.dump(idx_to_label, f, indent=2)
print(f"π·οΈ Classes ({len(unique_labels)}): {label_to_idx}")
# Reshape (N, 90) -> (N, 3 channels [X,Y,Z], 30 timesteps)
X_reshaped = X_raw.reshape(-1, 30, 3).transpose(0, 2, 1)
# Shuffle & Split into 80% Train, 20% Validation
indices = np.arange(len(df))
np.random.seed(42)
np.random.shuffle(indices)
split = int(0.8 * len(df))
train_idx, val_idx = indices[:split], indices[split:]
class MotionDataset(Dataset):
def __init__(self, X, y):
self.X = torch.tensor(X, dtype=torch.float32)
self.y = torch.tensor(y, dtype=torch.long)
def __len__(self):
return len(self.y)
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
train_loader = DataLoader(
MotionDataset(X_reshaped[train_idx], y_raw[train_idx]),
batch_size=BATCH_SIZE,
shuffle=True,
)
val_loader = DataLoader(
MotionDataset(X_reshaped[val_idx], y_raw[val_idx]), batch_size=BATCH_SIZE
)
# 3. The "Potato-Brain" Architecture (Micro 1D-CNN) π§ π₯
class PotatoGestureNet(nn.Module):
def __init__(self, num_classes):
super(PotatoGestureNet, self).__init__()
# 3 input channels (Ax, Ay, Az)
self.conv1 = nn.Conv1d(
in_channels=3, out_channels=16, kernel_size=5, padding=2
)
self.relu = nn.ReLU()
self.pool = nn.MaxPool1d(kernel_size=2) # 30 -> 15
self.conv2 = nn.Conv1d(
in_channels=16, out_channels=32, kernel_size=3, padding=1
)
self.global_pool = nn.AdaptiveAvgPool1d(1) # Flattens time axis cleanly
self.fc = nn.Linear(32, num_classes)
def forward(self, x):
x = self.pool(self.relu(self.conv1(x)))
x = self.relu(self.conv2(x))
x = self.global_pool(x).squeeze(-1)
x = self.fc(x)
return x
model = PotatoGestureNet(num_classes=len(unique_labels)).to(DEVICE)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
# 4. Fast Training Loop β±οΈπββοΈ
print("\nπ Starting Training...")
for epoch in range(1, EPOCHS + 1):
model.train()
total_loss = 0
correct = 0
total = 0
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(DEVICE), batch_y.to(DEVICE)
optimizer.zero_grad()
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
total_loss += loss.item()
_, predicted = outputs.max(1)
total += batch_y.size(0)
correct += predicted.eq(batch_y).sum().item()
# Quick validation check every 5 epochs
if epoch % 5 == 0 or epoch == EPOCHS:
model.eval()
val_correct, val_total = 0, 0
with torch.no_grad():
for bx, by in val_loader:
bx, by = bx.to(DEVICE), by.to(DEVICE)
preds = model(bx).argmax(dim=1)
val_total += by.size(0)
val_correct += preds.eq(by).sum().item()
val_acc = (
(val_correct / val_total) * 100 if val_total > 0 else 100.0
) # In case val set is empty
train_acc = (correct / total) * 100
print(
f"Epoch [{epoch:02d}/{EPOCHS}] | Loss: {total_loss/len(train_loader):.4f} | Train Acc: {train_acc:.1f}% | Val Acc: {val_acc:.1f}%"
)
print("\nπ Training Complete!")
# 5. Save PyTorch Model πΎ
torch.save(model.state_dict(), "gesture_model.pth")
print("β
Saved weights to 'gesture_model.pth'")
# 6. Export to ONNX for Browser / Edge Deployment π
try:
model.eval().cpu()
dummy_input = torch.randn(1, 3, 30) # [1 sample, 3 axes, 30 timesteps]
torch.onnx.export(
model,
dummy_input,
"gesture_model.onnx",
input_names=["accel_input"],
output_names=["probabilities"],
dynamic_axes={"accel_input": {0: "batch_size"}},
opset_version=12,
)
onnx_size = os.path.getsize("gesture_model.onnx") / 1024
print(f"π¦ Exported 'gesture_model.onnx' -> Size: {onnx_size:.1f} KB! π₯π")
except Exception as e:
print(
f"β οΈ Note: Run 'pip install onnx' to export ONNX. (PyTorch model saved fine!)"
)
|