-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathts_segment_classification.py
More file actions
417 lines (347 loc) · 16.1 KB
/
Copy pathts_segment_classification.py
File metadata and controls
417 lines (347 loc) · 16.1 KB
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import numpy as np
import pandas as pd
from tqdm import tqdm
import re
from imblearn.over_sampling import SMOTE
from data_loader import LPBSDataLoader
class CNNClassifier(nn.Module):
def __init__(self, input_size):
super(CNNClassifier, self).__init__()
self.input_size = input_size
self.conv_layers = nn.Sequential(
nn.Conv1d(input_size, 32, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Dropout(0.2),
nn.Conv1d(32, 64, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Dropout(0.2),
nn.Conv1d(64, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Dropout(0.2)
)
self.global_avg_pool = nn.AdaptiveAvgPool1d(1)
self.fc_layers = nn.Sequential(
nn.Linear(256, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(128, 2)
)
def forward(self, x):
x = self.conv_layers(x)
x = self.global_avg_pool(x)
x = x.view(x.size(0), -1)
x = self.fc_layers(x)
return x
class LSTMClassifier(nn.Module):
def __init__(self, input_size):
super(LSTMClassifier, self).__init__()
self.input_size = input_size
self.lstm = nn.LSTM(input_size, 64, num_layers=2, batch_first=True, dropout=0.2)
self.dropout = nn.Dropout(0.3)
self.fc1 = nn.Linear(64, 32)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(32, 2)
def forward(self, x):
# LSTM expects (batch, sequence_length, features)
# If input is transposed for CNN, we need to transpose it back
# CNN format: (batch, features, seq_len) where features=4 and seq_len=900
# LSTM format: (batch, seq_len, features) where seq_len=900 and features=4
if x.dim() == 3 and x.size(1) == 4 and x.size(2) > x.size(1): # Check if it's CNN format (batch, 4, 900)
x = x.transpose(1, 2) # Convert from (batch, features, seq_len) to (batch, seq_len, features)
lstm_out, (hidden, cell) = self.lstm(x)
x = hidden[-1] # Use the last hidden state for classification
x = self.dropout(x)
x = self.fc1(x)
x = self.relu(x)
x = self.dropout(x)
x = self.fc2(x)
return x
def calculate_segment_weights(weight_strategy, n_segments, segment_probs):
"""
Common function to calculate segment weights based on strategy.
Args:
weight_strategy: Strategy name for weighting segments
n_segments: Number of segments
segment_probs: Predicted probabilities for segments
Returns:
numpy array of weights
"""
if weight_strategy == 'uniform':
weights = np.ones(n_segments)
elif weight_strategy == 'confidence':
weights = np.max(segment_probs, axis=1)
elif weight_strategy == 'late_segments':
weights = np.linspace(0.5, 1.5, n_segments)
elif weight_strategy == 'early_segments':
weights = np.linspace(1.5, 0.5, n_segments)
elif re.match(r'^last_(\d+)_segments$', weight_strategy):
match = re.match(r'^last_(\d+)_segments$', weight_strategy)
X = int(match.group(1))
weights = np.zeros(n_segments)
weights[-X:] = 1
elif re.match(r'^last_(\d+)_segments_confidence$', weight_strategy):
match = re.match(r'^last_(\d+)_segments_confidence$', weight_strategy)
X = int(match.group(1))
weights = np.zeros(n_segments)
weights[-X:] = 1 * np.max(segment_probs, axis=1)[-X:]
elif weight_strategy == 'late_segments_confidence':
weights = np.linspace(0.5, 1.5, n_segments) * np.max(segment_probs, axis=1)
else:
weights = np.ones(n_segments)
return weights
def get_model(model_name: str, input_size):
"""Get PyTorch models for time series classification."""
if model_name == 'CNN':
return CNNClassifier(input_size)
elif model_name == 'LSTM':
return LSTMClassifier(input_size)
else:
raise ValueError(f"Unknown model name: {model_name}")
def pytorch_accuracy(y_true, y_pred):
"""Calculate accuracy using numpy."""
return np.mean(y_true == y_pred)
def pytorch_f1_score(y_true, y_pred):
"""Calculate F1 score using numpy."""
tp = np.sum((y_true == 1) & (y_pred == 1))
fp = np.sum((y_true == 0) & (y_pred == 1))
fn = np.sum((y_true == 1) & (y_pred == 0))
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
return f1
def pytorch_confusion_matrix(y_true, y_pred):
"""Calculate confusion matrix using numpy."""
tn = np.sum((y_true == 0) & (y_pred == 0))
fp = np.sum((y_true == 0) & (y_pred == 1))
fn = np.sum((y_true == 1) & (y_pred == 0))
tp = np.sum((y_true == 1) & (y_pred == 1))
return np.array([[tn, fp], [fn, tp]])
def weighted_voting_classification(model, weight_strategy='confidence', epochs=25, batch_size=32, learning_rate=0.001, features=None, verbose=False):
"""Weighted voting classifier using pure PyTorch.
Args:
model: PyTorch model instance (CNN or LSTM).
weight_strategy: Strategy for weighting segments during voting.
epochs: Number of training epochs per fold.
batch_size: Batch size for training.
learning_rate: Learning rate for optimizer.
features: List of feature names to use. Options: ['x', 'y', 'speed', 'turning_angle'].
If None, uses all features. Example: ['speed', 'turning_angle']
verbose: Whether to print progress information.
Returns:
dict: Results including accuracy, F1, confusion matrix, and vote analysis.
"""
# Feature mapping
FEATURE_MAP = {'x': 0, 'y': 1, 'speed': 2, 'turning_angle': 3}
ALL_FEATURES = ['x', 'y', 'speed', 'turning_angle']
# Use MPS (Apple Silicon GPU) if available, otherwise CUDA, otherwise CPU
if torch.backends.mps.is_available():
device = torch.device('mps')
elif torch.cuda.is_available():
device = torch.device('cuda')
else:
device = torch.device('cpu')
if verbose:
print(f"Using device: {device}")
loader = LPBSDataLoader()
X, y, groups = loader.load_segment_timeseries()
# Select features if specified
if features is not None:
if verbose:
print(f"Selected features: {features}")
feature_indices = [FEATURE_MAP[f] for f in features]
X = [ts[:, feature_indices] for ts in X]
else:
if verbose:
print(f"Using all features: {ALL_FEATURES}")
# Pad/truncate time series to same length
target_length = 100
X_padded = []
for ts in X:
if len(ts) >= target_length:
# Sample evenly spaced indices to downsample to target_length
indices = np.linspace(0, len(ts) - 1, target_length, dtype=int)
X_padded.append(ts[indices])
else:
# Pad with zeros if needed
padded = np.zeros((target_length, ts.shape[1]))
padded[:len(ts)] = ts
X_padded.append(padded)
X_padded = np.array(X_padded)
if verbose:
print(f"Loaded: {len(X):,} segments from {len(np.unique(groups))} worms")
print(f"Padded shape: {X_padded.shape}")
print(f"Weight strategy: {weight_strategy}")
# Convert to pandas for cv_splits compatibility
y_series = pd.Series(y)
groups_series = pd.Series(groups)
# Create dummy DataFrame with indices for cv_splits
X_df = pd.DataFrame({'dummy': range(len(X_padded))})
cv_splits = loader.create_cv_splits(X_df, y_series, groups_series, n_splits=5)
file_predictions, file_true_labels = [], []
vote_analysis = []
for fold_idx, fold in enumerate(tqdm(cv_splits)):
if verbose:
print(f"\nFold {fold_idx + 1}/5")
# Get indices for train/test
train_indices = fold['X_train'].index.values
test_indices = fold['X_test'].index.values
X_train = X_padded[train_indices]
X_test = X_padded[test_indices]
y_train = y[train_indices]
y_test = y[test_indices]
# Apply SMOTE to balance the training data
if verbose and fold_idx == 0:
print(f" Before SMOTE - Class distribution: {np.bincount(y_train)}")
# Flatten time series for SMOTE (SMOTE needs 2D data)
n_samples, n_timesteps, n_features = X_train.shape
X_train_flat = X_train.reshape(n_samples, n_timesteps * n_features)
# Apply SMOTE
smote = SMOTE(random_state=42)
X_train_balanced, y_train_balanced = smote.fit_resample(X_train_flat, y_train)
# Reshape back to time series format
X_train_balanced = X_train_balanced.reshape(-1, n_timesteps, n_features)
if verbose and fold_idx == 0:
print(f" After SMOTE - Class distribution: {np.bincount(y_train_balanced)}")
print(f" Samples: {len(y_train)} → {len(y_train_balanced)}")
# Convert to PyTorch tensors
# For CNN: (batch, features, sequence_length)
# For LSTM: (batch, sequence_length, features) - handled in LSTM forward()
X_train_tensor = torch.FloatTensor(X_train_balanced).transpose(1, 2).to(device)
X_test_tensor = torch.FloatTensor(X_test).transpose(1, 2).to(device)
y_train_tensor = torch.LongTensor(y_train_balanced).to(device)
# Re-initialize model for each fold (CRITICAL!)
fold_model = type(model)(model.input_size)
fold_model.to(device)
optimizer = torch.optim.Adam(fold_model.parameters(), lr=learning_rate)
criterion = nn.CrossEntropyLoss()
# Create DataLoader for training
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# Train the model
fold_model.train()
for epoch in range(epochs):
epoch_loss = 0
for batch_X, batch_y in train_dataloader:
optimizer.zero_grad()
outputs = fold_model(batch_X)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
# Show progress every 10 epochs
if verbose and (epoch % 10 == 0 or epoch == epochs - 1):
print(f" Epoch {epoch}, Loss: {epoch_loss/len(train_dataloader):.4f}")
# Test on each worm in the test split
fold_model.eval()
with torch.no_grad():
test_outputs = fold_model(X_test_tensor)
test_probs = torch.softmax(test_outputs, dim=1).cpu().numpy()
test_preds = np.argmax(test_probs, axis=1)
# Group predictions by worm
for test_worm in fold['test_files']:
worm_mask = fold['groups_test'] == test_worm
worm_indices = fold['groups_test'][worm_mask].index.values
# Map back to test indices
test_worm_indices = []
for idx in worm_indices:
test_pos = np.where(test_indices == idx)[0]
if len(test_pos) > 0:
test_worm_indices.append(test_pos[0])
if len(test_worm_indices) == 0:
continue
worm_preds = test_preds[test_worm_indices]
worm_probs = test_probs[test_worm_indices]
worm_true_label = fold['y_test'][worm_mask].iloc[0]
n_segments = len(worm_preds)
# Calculate weights based on strategy
weights = calculate_segment_weights(weight_strategy, n_segments, worm_probs)
# Calculate weighted votes
weighted_vote_0 = np.sum(weights[worm_preds == 0])
weighted_vote_1 = np.sum(weights[worm_preds == 1])
worm_pred = int(weighted_vote_1 > weighted_vote_0)
# Calculate confidence
total_weight = weighted_vote_0 + weighted_vote_1
confidence = max(weighted_vote_0, weighted_vote_1) / total_weight if total_weight > 0 else 0.5
vote_analysis.append({
'n_segments': n_segments,
'weighted_pred': worm_pred,
'weighted_confidence': confidence,
'avg_weight': weights.mean(),
'weight_std': weights.std(),
'true_label': worm_true_label,
'weighted_correct': worm_pred == worm_true_label,
})
file_predictions.append(worm_pred)
file_true_labels.append(worm_true_label)
file_predictions = np.array(file_predictions)
file_true_labels = np.array(file_true_labels)
accuracy = pytorch_accuracy(file_true_labels, file_predictions)
f1 = pytorch_f1_score(file_true_labels, file_predictions)
cm = pytorch_confusion_matrix(file_true_labels, file_predictions)
vote_df = pd.DataFrame(vote_analysis)
if verbose:
print(f"\nResults: {len(file_predictions)} worms, Acc: {accuracy:.3f}, F1: {f1:.3f}")
# Per-class accuracy
class_0_mask = file_true_labels == 0
class_1_mask = file_true_labels == 1
class_0_acc = pytorch_accuracy(file_true_labels[class_0_mask], file_predictions[class_0_mask]) if class_0_mask.sum() > 0 else 0
class_1_acc = pytorch_accuracy(file_true_labels[class_1_mask], file_predictions[class_1_mask]) if class_1_mask.sum() > 0 else 0
print(f"\nPer-Class Performance:")
print(f" Class 0 (Control): {class_0_acc:.3f} ({class_0_mask.sum()} samples)")
print(f" Class 1 (Treatment): {class_1_acc:.3f} ({class_1_mask.sum()} samples)")
print(f"\nWeighted Voting Analysis:")
print(f" Weighted accuracy: {vote_df['weighted_correct'].mean():.3f}")
print(f" Average confidence: {vote_df['weighted_confidence'].mean():.3f}")
print(f" Average segments per worm: {vote_df['n_segments'].mean():.1f}")
return {
"accuracy": accuracy,
"f1": f1,
"confusion_matrix": cm,
"n_worms": len(file_predictions),
"vote_analysis": vote_df,
"weight_strategy": weight_strategy
}
if __name__ == "__main__":
model_name = 'CNN'
weight_strategy = 'last_10_segments_confidence'
# Feature selection: specify which features to use
# Options: ['x', 'y', 'speed', 'turning_angle']
# Examples:
# None → use all 4 features
# ['speed', 'turning_angle'] → use only speed and turning angle (2 features)
# ['x', 'y'] → use only coordinates (2 features)
# selected_features = None # Use all features by default
selected_features = ['speed', 'turning_angle']
print("===== Time Series Weighted Voting Classification =====")
# Get input size based on selected features
if selected_features is None:
input_size = 4 # All features: x, y, speed, turning_angle
else:
input_size = len(selected_features)
print(f"Input size: {input_size} features")
if selected_features:
print(f"Using features: {selected_features}")
else:
print(f"Using all features: ['x', 'y', 'speed', 'turning_angle']")
model = get_model(model_name, input_size)
results = weighted_voting_classification(
model,
weight_strategy=weight_strategy,
epochs=30, # Using 30 epochs with SMOTE balancing
batch_size=32,
learning_rate=0.001,
features=selected_features, # Pass selected features
verbose=True
)
print("\n===== Results =====")
print("Accuracy:", results['accuracy'])
print("F1:", results['f1'])
print("Confusion Matrix:")
print(results['confusion_matrix'])