malansi commited on
Commit
52a6747
·
verified ·
1 Parent(s): 37cc7ac

Update hand_module/train_hand.py — 2026-07-07 18:04

Browse files
Files changed (1) hide show
  1. code/train_hand.py +308 -0
code/train_hand.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # train_hand.py
2
+ # Prosthetic hand gesture recognition — personal dataset
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from scipy import signal
7
+ from sklearn.metrics import classification_report, confusion_matrix
8
+ from sklearn.utils.class_weight import compute_class_weight
9
+ import torch
10
+ import torch.nn as nn
11
+ from torch.utils.data import Dataset, DataLoader
12
+ import matplotlib.pyplot as plt
13
+ import seaborn as sns
14
+ import os
15
+
16
+ FS = 200
17
+ WIN_SAMPLES = 150
18
+ STEP = 75
19
+ N_CHANNELS = 8
20
+ N_CLASSES = 10
21
+ BATCH_SIZE = 128
22
+ EPOCHS = 100
23
+ DEVICE = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
24
+
25
+ GESTURE_NAMES = {
26
+ 0: 'rest',
27
+ 1: 'fist',
28
+ 2: 'grasp',
29
+ 3: 'index',
30
+ 4: 'middle',
31
+ 5: 'ring',
32
+ 6: 'pinky',
33
+ 7: 'thumb',
34
+ 8: 'wrist_rotate_out',
35
+ 9: 'wrist_rotate_in',
36
+ }
37
+
38
+ print(f"Device: {DEVICE}")
39
+
40
+
41
+ # ── Load ──
42
+ def load_sessions(sessions_dir="hand_module/sessions"):
43
+ all_dfs = []
44
+ dirs = sorted([
45
+ d for d in os.listdir(sessions_dir)
46
+ if os.path.isdir(f"{sessions_dir}/{d}")
47
+ and os.path.exists(f"{sessions_dir}/{d}/emg_data.csv")
48
+ ])
49
+ for i, d in enumerate(dirs):
50
+ df = pd.read_csv(f"{sessions_dir}/{d}/emg_data.csv")
51
+ df['session_id'] = i
52
+ all_dfs.append(df)
53
+ print(f" Session {i+1}: {len(df):,} samples — {d}")
54
+ return pd.concat(all_dfs, ignore_index=True)
55
+
56
+ print("\nLoading sessions...")
57
+ df = load_sessions()
58
+
59
+ df['block_id'] = (df['label'] != df['label'].shift()).cumsum()
60
+
61
+ print(f"Total: {len(df):,} samples")
62
+ print(f"Total blocks: {df['block_id'].nunique()}\n")
63
+
64
+ for lbl, name in GESTURE_NAMES.items():
65
+ count = (df['label'] == lbl).sum()
66
+ print(f" {name:<20}: {count:,}")
67
+
68
+
69
+ # ── Preprocessing — global fixed normalization ──
70
+ GLOBAL_STATS = {}
71
+
72
+ def preprocess_global(df):
73
+ EMG_COLS = [f'emg_{i}' for i in range(8)]
74
+ emg_out = np.zeros((len(df), 8), dtype=np.float32)
75
+ nyq = FS / 2
76
+ bb, aa = signal.butter(4, [20/nyq, 90/nyq], btype='band')
77
+ bn, an = signal.iirnotch(50, Q=30, fs=FS)
78
+
79
+ all_filtered = []
80
+ for sid in df['session_id'].unique():
81
+ mask = (df['session_id'] == sid).values
82
+ emg = df.loc[mask, EMG_COLS].values.astype(np.float32)
83
+ emg = signal.filtfilt(bb, aa, emg, axis=0)
84
+ emg = signal.filtfilt(bn, an, emg, axis=0)
85
+ emg_out[mask] = emg
86
+ all_filtered.append(emg)
87
+
88
+ all_concat = np.concatenate(all_filtered, axis=0)
89
+ GLOBAL_STATS['mean'] = all_concat.mean(axis=0)
90
+ GLOBAL_STATS['std'] = np.where(
91
+ all_concat.std(axis=0) < 1e-8, 1e-8, all_concat.std(axis=0)
92
+ )
93
+ emg_out = (emg_out - GLOBAL_STATS['mean']) / GLOBAL_STATS['std']
94
+ return emg_out
95
+
96
+ print("\nPreprocessing...")
97
+ emg_norm = preprocess_global(df)
98
+ labels = df['label'].values.astype(np.int64)
99
+ blocks = df['block_id'].values
100
+
101
+ np.save('hand_module/models/hand_norm_mean.npy', GLOBAL_STATS['mean'])
102
+ np.save('hand_module/models/hand_norm_std.npy', GLOBAL_STATS['std'])
103
+ print(f" Saved normalization stats")
104
+
105
+
106
+ # ── Windowing per block ──
107
+ def extract_windows_per_block(emg, labels, blocks, win=WIN_SAMPLES, step=STEP):
108
+ X, y, block_ids = [], [], []
109
+ for bid in np.unique(blocks):
110
+ mask = blocks == bid
111
+ e_blk = emg[mask]
112
+ l_blk = labels[mask]
113
+ if len(np.unique(l_blk)) != 1:
114
+ continue
115
+ lbl = l_blk[0]
116
+ n = len(l_blk)
117
+ i = 0
118
+ while i + win <= n:
119
+ X.append(e_blk[i:i+win])
120
+ y.append(lbl)
121
+ block_ids.append(bid)
122
+ i += step
123
+ return (np.array(X, dtype=np.float32),
124
+ np.array(y, dtype=np.int64),
125
+ np.array(block_ids))
126
+
127
+ print("Extracting windows...")
128
+ X, y, block_ids = extract_windows_per_block(emg_norm, labels, blocks)
129
+ print(f"Windows: {len(X):,} Shape: {X.shape}\n")
130
+
131
+ for lbl, name in GESTURE_NAMES.items():
132
+ print(f" {name:<20}: {(y==lbl).sum():,}")
133
+
134
+
135
+ # ── Split — window level per class (single session) ──
136
+ np.random.seed(42)
137
+ train_idx, test_idx = [], []
138
+
139
+ for lbl in range(N_CLASSES):
140
+ lbl_idx = np.where(y == lbl)[0]
141
+ np.random.shuffle(lbl_idx)
142
+ n_test = max(1, int(len(lbl_idx) * 0.2))
143
+ test_idx.extend(lbl_idx[:n_test].tolist())
144
+ train_idx.extend(lbl_idx[n_test:].tolist())
145
+
146
+ train_idx = np.array(train_idx)
147
+ test_idx = np.array(test_idx)
148
+
149
+ X_train, y_train = X[train_idx], y[train_idx]
150
+ X_test, y_test = X[test_idx], y[test_idx]
151
+
152
+ print(f"\nTrain: {len(X_train):,} | Test: {len(X_test):,}")
153
+
154
+
155
+ # ── Dataset ──
156
+ class EMGDataset(Dataset):
157
+ def __init__(self, X, y):
158
+ self.X = torch.tensor(X.transpose(0, 2, 1), dtype=torch.float32)
159
+ self.y = torch.tensor(y, dtype=torch.long)
160
+ def __len__(self): return len(self.y)
161
+ def __getitem__(self, i): return self.X[i], self.y[i]
162
+
163
+ train_loader = DataLoader(EMGDataset(X_train, y_train),
164
+ batch_size=BATCH_SIZE, shuffle=True, drop_last=True)
165
+ test_loader = DataLoader(EMGDataset(X_test, y_test),
166
+ batch_size=BATCH_SIZE, shuffle=False)
167
+
168
+
169
+ # ── Model ──
170
+ class EMG_CNN_LSTM(nn.Module):
171
+ def __init__(self, n_channels=8, n_classes=10):
172
+ super().__init__()
173
+ self.cnn = nn.Sequential(
174
+ nn.Conv1d(n_channels, 64, kernel_size=3, padding=1),
175
+ nn.BatchNorm1d(64), nn.ReLU(),
176
+ nn.Conv1d(64, 128, kernel_size=3, padding=1),
177
+ nn.BatchNorm1d(128), nn.ReLU(),
178
+ nn.MaxPool1d(2), nn.Dropout(0.3),
179
+ nn.Conv1d(128, 256, kernel_size=3, padding=1),
180
+ nn.BatchNorm1d(256), nn.ReLU(),
181
+ nn.MaxPool1d(2), nn.Dropout(0.3),
182
+ )
183
+ self.lstm = nn.LSTM(
184
+ input_size=256, hidden_size=128,
185
+ num_layers=2, batch_first=True,
186
+ dropout=0.3, bidirectional=True
187
+ )
188
+ self.fc = nn.Sequential(
189
+ nn.Linear(256, 128), nn.ReLU(),
190
+ nn.Dropout(0.4),
191
+ nn.Linear(128, n_classes)
192
+ )
193
+ def forward(self, x):
194
+ x = self.cnn(x)
195
+ x = x.permute(0, 2, 1)
196
+ x, _ = self.lstm(x)
197
+ x = x[:, -1, :]
198
+ return self.fc(x)
199
+
200
+
201
+ # ── Training ──
202
+ model = EMG_CNN_LSTM(n_classes=N_CLASSES).to(DEVICE)
203
+ optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=1e-4)
204
+ scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)
205
+
206
+ cw = compute_class_weight('balanced', classes=np.unique(y_train), y=y_train)
207
+ criterion = nn.CrossEntropyLoss(
208
+ weight=torch.tensor(cw, dtype=torch.float32).to(DEVICE),
209
+ label_smoothing=0.05
210
+ )
211
+
212
+ print("\n" + "=" * 56)
213
+ print(" TRAINING — Prosthetic Hand (10 gestures)")
214
+ print("=" * 56)
215
+
216
+ best_acc, best_epoch = 0.0, 0
217
+ train_losses, test_accs = [], []
218
+
219
+ for epoch in range(1, EPOCHS + 1):
220
+ model.train()
221
+ epoch_loss = 0
222
+ for xb, yb in train_loader:
223
+ xb, yb = xb.to(DEVICE), yb.to(DEVICE)
224
+ optimizer.zero_grad()
225
+ loss = criterion(model(xb), yb)
226
+ loss.backward()
227
+ nn.utils.clip_grad_norm_(model.parameters(), 1.0)
228
+ optimizer.step()
229
+ epoch_loss += loss.item()
230
+ scheduler.step()
231
+
232
+ model.eval()
233
+ correct = total = 0
234
+ with torch.no_grad():
235
+ for xb, yb in test_loader:
236
+ xb, yb = xb.to(DEVICE), yb.to(DEVICE)
237
+ preds = model(xb).argmax(1)
238
+ correct += (preds == yb).sum().item()
239
+ total += len(yb)
240
+
241
+ acc = correct / total
242
+ avg_loss = epoch_loss / len(train_loader)
243
+
244
+ if acc > best_acc:
245
+ best_acc, best_epoch = acc, epoch
246
+ torch.save(model.state_dict(), 'hand_module/models/best_model_hand.pt')
247
+
248
+ train_losses.append(avg_loss)
249
+ test_accs.append(acc)
250
+ if epoch % 10 == 0 or epoch == 1:
251
+ print(f" Epoch {epoch:3d}/{EPOCHS} | "
252
+ f"Loss: {avg_loss:.4f} | "
253
+ f"Acc: {acc:.3f} | "
254
+ f"Best: {best_acc:.3f} (ep {best_epoch})")
255
+
256
+ print(f"\n Best accuracy: {best_acc:.3f} at epoch {best_epoch}")
257
+
258
+
259
+ # ── Evaluation ──
260
+ model.load_state_dict(torch.load('hand_module/models/best_model_hand.pt'))
261
+ model.eval()
262
+
263
+ all_preds, all_true = [], []
264
+ with torch.no_grad():
265
+ for xb, yb in test_loader:
266
+ preds = model(xb.to(DEVICE)).argmax(1).cpu().numpy()
267
+ all_preds.extend(preds)
268
+ all_true.extend(yb.numpy())
269
+
270
+ names = [GESTURE_NAMES[i] for i in range(N_CLASSES)]
271
+ print("\n" + "=" * 56)
272
+ print(" CLASSIFICATION REPORT — Prosthetic Hand")
273
+ print("=" * 56)
274
+ print(classification_report(all_true, all_preds, target_names=names))
275
+
276
+ cm = confusion_matrix(all_true, all_preds)
277
+ plt.figure(figsize=(10, 8))
278
+ sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
279
+ xticklabels=names, yticklabels=names)
280
+ plt.title(f'Confusion Matrix — Hand Model — Best Acc: {best_acc:.3f}')
281
+ plt.ylabel('True'); plt.xlabel('Predicted')
282
+ plt.tight_layout()
283
+ plt.savefig('hand_module/results/confusion_matrix_hand.png', dpi=150)
284
+ print(" Saved: hand_module/results/confusion_matrix_hand.png")
285
+
286
+ # ── Training Curves ──
287
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
288
+ ax1.plot(train_losses); ax1.set_title('Training Loss'); ax1.set_xlabel('Epoch')
289
+ ax2.plot(test_accs); ax2.set_title('Test Accuracy'); ax2.set_xlabel('Epoch')
290
+ ax2.axhline(y=best_acc, color='r', linestyle='--', label=f'Best: {best_acc:.3f}')
291
+ ax2.legend()
292
+ plt.tight_layout()
293
+ plt.savefig('hand_module/results/training_curves_hand.png', dpi=150)
294
+ print(" Saved: hand_module/results/training_curves_hand.png")
295
+
296
+ # ── Training Curves ──
297
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
298
+ ax1.plot(train_losses)
299
+ ax1.set_title('Training Loss')
300
+ ax1.set_xlabel('Epoch')
301
+ ax2.plot(test_accs)
302
+ ax2.set_title('Test Accuracy')
303
+ ax2.set_xlabel('Epoch')
304
+ ax2.axhline(y=best_acc, color='r', linestyle='--', label=f'Best: {best_acc:.3f}')
305
+ ax2.legend()
306
+ plt.tight_layout()
307
+ plt.savefig('hand_module/results/training_curves_hand.png', dpi=150)
308
+ print(" Saved: hand_module/results/training_curves_hand.png")