gaitsetpy.classification.models.lstm

  1import torch
  2import torch.nn as nn
  3import torch.optim as optim
  4import numpy as np
  5import joblib
  6from typing import List, Dict, Any, Optional, Union
  7from ...core.base_classes import BaseClassificationModel
  8from ..utils.preprocess import preprocess_features
  9from sklearn.model_selection import train_test_split
 10from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
 11
 12class LSTMNet(nn.Module):
 13    def __init__(self, input_size, hidden_size, num_layers, num_classes, dropout=0.2):
 14        super(LSTMNet, self).__init__()
 15        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout)
 16        self.fc = nn.Linear(hidden_size, num_classes)
 17    def forward(self, x):
 18        out, _ = self.lstm(x)
 19        out = out[:, -1, :]
 20        out = self.fc(out)
 21        return out
 22
 23class LSTMModel(BaseClassificationModel):
 24    """
 25    LSTM classification model using PyTorch.
 26    Implements the BaseClassificationModel interface.
 27    """
 28    def __init__(self, input_size=10, hidden_size=64, num_layers=1, num_classes=2, lr=0.001, epochs=20, batch_size=32, device=None):
 29        super().__init__(
 30            name="lstm",
 31            description="LSTM classifier for gait data classification"
 32        )
 33        self.config = {
 34            'input_size': input_size,
 35            'hidden_size': hidden_size,
 36            'num_layers': num_layers,
 37            'num_classes': num_classes,
 38            'lr': lr,
 39            'epochs': epochs,
 40            'batch_size': batch_size
 41        }
 42        self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
 43        self.model = LSTMNet(input_size, hidden_size, num_layers, num_classes).to(self.device)
 44        self.epochs = epochs
 45        self.batch_size = batch_size
 46        self.trained = False
 47        self.feature_names = []
 48        self.class_names = []
 49
 50    def train(self, features: List[Dict], **kwargs):
 51        X, y = preprocess_features(features)
 52        # Reshape X for LSTM: (samples, sequence_length, input_size)
 53        # Here, treat each feature vector as a sequence of length 1
 54        X = X.reshape((X.shape[0], 1, X.shape[1]))
 55        self.feature_names = [f"feature_{i}" for i in range(X.shape[2])]
 56        self.class_names = list(set(y))
 57        test_size = kwargs.get('test_size', 0.2)
 58        validation_split = kwargs.get('validation_split', True)
 59        if validation_split:
 60            X_train, X_test, y_train, y_test = train_test_split(
 61                X, y, test_size=test_size, random_state=42
 62            )
 63            self.X_test = X_test
 64            self.y_test = y_test
 65        else:
 66            X_train, y_train = X, y
 67        X_train = torch.tensor(X_train, dtype=torch.float32).to(self.device)
 68        y_train = torch.tensor(y_train, dtype=torch.long).to(self.device)
 69        criterion = nn.CrossEntropyLoss()
 70        optimizer = optim.Adam(self.model.parameters(), lr=self.config['lr'])
 71        for epoch in range(self.epochs):
 72            self.model.train()
 73            optimizer.zero_grad()
 74            outputs = self.model(X_train)
 75            loss = criterion(outputs, y_train)
 76            loss.backward()
 77            optimizer.step()
 78            if (epoch+1) % 5 == 0 or epoch == 0:
 79                print(f"Epoch [{epoch+1}/{self.epochs}], Loss: {loss.item():.4f}")
 80        self.trained = True
 81        print("LSTM model trained successfully.")
 82
 83    def predict(self, features: List[Dict], **kwargs) -> np.ndarray:
 84        if not self.trained:
 85            raise ValueError("Model must be trained before making predictions")
 86        X, _ = preprocess_features(features)
 87        X = X.reshape((X.shape[0], 1, X.shape[1]))
 88        X = torch.tensor(X, dtype=torch.float32).to(self.device)
 89        self.model.eval()
 90        with torch.no_grad():
 91            outputs = self.model(X)
 92            _, predicted = torch.max(outputs.data, 1)
 93        return predicted.cpu().numpy()
 94
 95    def evaluate(self, features: List[Dict], **kwargs) -> Dict[str, float]:
 96        if not self.trained:
 97            raise ValueError("Model must be trained before evaluation")
 98        if hasattr(self, 'X_test') and hasattr(self, 'y_test'):
 99            X_test, y_test = self.X_test, self.y_test
100        else:
101            X_test, y_test = preprocess_features(features)
102            X_test = X_test.reshape((X_test.shape[0], 1, X_test.shape[1]))
103        X_test = torch.tensor(X_test, dtype=torch.float32).to(self.device)
104        y_test = np.array(y_test)
105        self.model.eval()
106        with torch.no_grad():
107            outputs = self.model(X_test)
108            _, y_pred = torch.max(outputs.data, 1)
109        y_pred = y_pred.cpu().numpy()
110        accuracy = accuracy_score(y_test, y_pred)
111        conf_matrix = confusion_matrix(y_test, y_pred)
112        metrics = {
113            'accuracy': accuracy,
114            'confusion_matrix': conf_matrix.tolist()
115        }
116        detailed_report = kwargs.get('detailed_report', False)
117        if detailed_report:
118            class_report = classification_report(y_test, y_pred, output_dict=True)
119            metrics['classification_report'] = class_report
120        return metrics
121
122    def save_model(self, filepath: str):
123        if not self.trained:
124            raise ValueError("Model must be trained before saving")
125        torch.save({
126            'model_state_dict': self.model.state_dict(),
127            'config': self.config,
128            'feature_names': self.feature_names,
129            'class_names': self.class_names,
130            'trained': self.trained
131        }, filepath)
132        print(f"LSTM model saved to {filepath}")
133
134    def load_model(self, filepath: str):
135        checkpoint = torch.load(filepath, map_location=self.device)
136        self.model = LSTMNet(
137            self.config['input_size'],
138            self.config['hidden_size'],
139            self.config['num_layers'],
140            self.config['num_classes']
141        ).to(self.device)
142        self.model.load_state_dict(checkpoint['model_state_dict'])
143        self.config = checkpoint.get('config', self.config)
144        self.feature_names = checkpoint.get('feature_names', [])
145        self.class_names = checkpoint.get('class_names', [])
146        self.trained = checkpoint.get('trained', True)
147        print(f"LSTM model loaded from {filepath}")
class LSTMNet(torch.nn.modules.module.Module):
13class LSTMNet(nn.Module):
14    def __init__(self, input_size, hidden_size, num_layers, num_classes, dropout=0.2):
15        super(LSTMNet, self).__init__()
16        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout)
17        self.fc = nn.Linear(hidden_size, num_classes)
18    def forward(self, x):
19        out, _ = self.lstm(x)
20        out = out[:, -1, :]
21        out = self.fc(out)
22        return out

Base class for all neural network modules.

Your models should also subclass this class.

Modules can also contain other Modules, allowing them to be nested in a tree structure. You can assign the submodules as regular attributes::

import torch.nn as nn
import torch.nn.functional as F


class Model(nn.Module):
    def __init__(self) -> None:
        super().__init__()
        self.conv1 = nn.Conv2d(1, 20, 5)
        self.conv2 = nn.Conv2d(20, 20, 5)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        return F.relu(self.conv2(x))

Submodules assigned in this way will be registered, and will also have their parameters converted when you call to(), etc.

As per the example above, an __init__() call to the parent class must be made before assignment on the child.

:ivar training: Boolean represents whether this module is in training or evaluation mode. :vartype training: bool

LSTMNet(input_size, hidden_size, num_layers, num_classes, dropout=0.2)
14    def __init__(self, input_size, hidden_size, num_layers, num_classes, dropout=0.2):
15        super(LSTMNet, self).__init__()
16        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout)
17        self.fc = nn.Linear(hidden_size, num_classes)

Initialize internal Module state, shared by both nn.Module and ScriptModule.

lstm
fc
def forward(self, x):
18    def forward(self, x):
19        out, _ = self.lstm(x)
20        out = out[:, -1, :]
21        out = self.fc(out)
22        return out

Define the computation performed at every call.

Should be overridden by all subclasses.

Although the recipe for forward pass needs to be defined within this function, one should call the Module instance afterwards instead of this since the former takes care of running the registered hooks while the latter silently ignores them.

 24class LSTMModel(BaseClassificationModel):
 25    """
 26    LSTM classification model using PyTorch.
 27    Implements the BaseClassificationModel interface.
 28    """
 29    def __init__(self, input_size=10, hidden_size=64, num_layers=1, num_classes=2, lr=0.001, epochs=20, batch_size=32, device=None):
 30        super().__init__(
 31            name="lstm",
 32            description="LSTM classifier for gait data classification"
 33        )
 34        self.config = {
 35            'input_size': input_size,
 36            'hidden_size': hidden_size,
 37            'num_layers': num_layers,
 38            'num_classes': num_classes,
 39            'lr': lr,
 40            'epochs': epochs,
 41            'batch_size': batch_size
 42        }
 43        self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
 44        self.model = LSTMNet(input_size, hidden_size, num_layers, num_classes).to(self.device)
 45        self.epochs = epochs
 46        self.batch_size = batch_size
 47        self.trained = False
 48        self.feature_names = []
 49        self.class_names = []
 50
 51    def train(self, features: List[Dict], **kwargs):
 52        X, y = preprocess_features(features)
 53        # Reshape X for LSTM: (samples, sequence_length, input_size)
 54        # Here, treat each feature vector as a sequence of length 1
 55        X = X.reshape((X.shape[0], 1, X.shape[1]))
 56        self.feature_names = [f"feature_{i}" for i in range(X.shape[2])]
 57        self.class_names = list(set(y))
 58        test_size = kwargs.get('test_size', 0.2)
 59        validation_split = kwargs.get('validation_split', True)
 60        if validation_split:
 61            X_train, X_test, y_train, y_test = train_test_split(
 62                X, y, test_size=test_size, random_state=42
 63            )
 64            self.X_test = X_test
 65            self.y_test = y_test
 66        else:
 67            X_train, y_train = X, y
 68        X_train = torch.tensor(X_train, dtype=torch.float32).to(self.device)
 69        y_train = torch.tensor(y_train, dtype=torch.long).to(self.device)
 70        criterion = nn.CrossEntropyLoss()
 71        optimizer = optim.Adam(self.model.parameters(), lr=self.config['lr'])
 72        for epoch in range(self.epochs):
 73            self.model.train()
 74            optimizer.zero_grad()
 75            outputs = self.model(X_train)
 76            loss = criterion(outputs, y_train)
 77            loss.backward()
 78            optimizer.step()
 79            if (epoch+1) % 5 == 0 or epoch == 0:
 80                print(f"Epoch [{epoch+1}/{self.epochs}], Loss: {loss.item():.4f}")
 81        self.trained = True
 82        print("LSTM model trained successfully.")
 83
 84    def predict(self, features: List[Dict], **kwargs) -> np.ndarray:
 85        if not self.trained:
 86            raise ValueError("Model must be trained before making predictions")
 87        X, _ = preprocess_features(features)
 88        X = X.reshape((X.shape[0], 1, X.shape[1]))
 89        X = torch.tensor(X, dtype=torch.float32).to(self.device)
 90        self.model.eval()
 91        with torch.no_grad():
 92            outputs = self.model(X)
 93            _, predicted = torch.max(outputs.data, 1)
 94        return predicted.cpu().numpy()
 95
 96    def evaluate(self, features: List[Dict], **kwargs) -> Dict[str, float]:
 97        if not self.trained:
 98            raise ValueError("Model must be trained before evaluation")
 99        if hasattr(self, 'X_test') and hasattr(self, 'y_test'):
100            X_test, y_test = self.X_test, self.y_test
101        else:
102            X_test, y_test = preprocess_features(features)
103            X_test = X_test.reshape((X_test.shape[0], 1, X_test.shape[1]))
104        X_test = torch.tensor(X_test, dtype=torch.float32).to(self.device)
105        y_test = np.array(y_test)
106        self.model.eval()
107        with torch.no_grad():
108            outputs = self.model(X_test)
109            _, y_pred = torch.max(outputs.data, 1)
110        y_pred = y_pred.cpu().numpy()
111        accuracy = accuracy_score(y_test, y_pred)
112        conf_matrix = confusion_matrix(y_test, y_pred)
113        metrics = {
114            'accuracy': accuracy,
115            'confusion_matrix': conf_matrix.tolist()
116        }
117        detailed_report = kwargs.get('detailed_report', False)
118        if detailed_report:
119            class_report = classification_report(y_test, y_pred, output_dict=True)
120            metrics['classification_report'] = class_report
121        return metrics
122
123    def save_model(self, filepath: str):
124        if not self.trained:
125            raise ValueError("Model must be trained before saving")
126        torch.save({
127            'model_state_dict': self.model.state_dict(),
128            'config': self.config,
129            'feature_names': self.feature_names,
130            'class_names': self.class_names,
131            'trained': self.trained
132        }, filepath)
133        print(f"LSTM model saved to {filepath}")
134
135    def load_model(self, filepath: str):
136        checkpoint = torch.load(filepath, map_location=self.device)
137        self.model = LSTMNet(
138            self.config['input_size'],
139            self.config['hidden_size'],
140            self.config['num_layers'],
141            self.config['num_classes']
142        ).to(self.device)
143        self.model.load_state_dict(checkpoint['model_state_dict'])
144        self.config = checkpoint.get('config', self.config)
145        self.feature_names = checkpoint.get('feature_names', [])
146        self.class_names = checkpoint.get('class_names', [])
147        self.trained = checkpoint.get('trained', True)
148        print(f"LSTM model loaded from {filepath}")

LSTM classification model using PyTorch. Implements the BaseClassificationModel interface.

LSTMModel( input_size=10, hidden_size=64, num_layers=1, num_classes=2, lr=0.001, epochs=20, batch_size=32, device=None)
29    def __init__(self, input_size=10, hidden_size=64, num_layers=1, num_classes=2, lr=0.001, epochs=20, batch_size=32, device=None):
30        super().__init__(
31            name="lstm",
32            description="LSTM classifier for gait data classification"
33        )
34        self.config = {
35            'input_size': input_size,
36            'hidden_size': hidden_size,
37            'num_layers': num_layers,
38            'num_classes': num_classes,
39            'lr': lr,
40            'epochs': epochs,
41            'batch_size': batch_size
42        }
43        self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
44        self.model = LSTMNet(input_size, hidden_size, num_layers, num_classes).to(self.device)
45        self.epochs = epochs
46        self.batch_size = batch_size
47        self.trained = False
48        self.feature_names = []
49        self.class_names = []

Initialize the classification model.

Args: name: Name of the classification model description: Description of the classification model

config
device
model
epochs
batch_size
trained
feature_names
class_names
def train(self, features: List[Dict], **kwargs):
51    def train(self, features: List[Dict], **kwargs):
52        X, y = preprocess_features(features)
53        # Reshape X for LSTM: (samples, sequence_length, input_size)
54        # Here, treat each feature vector as a sequence of length 1
55        X = X.reshape((X.shape[0], 1, X.shape[1]))
56        self.feature_names = [f"feature_{i}" for i in range(X.shape[2])]
57        self.class_names = list(set(y))
58        test_size = kwargs.get('test_size', 0.2)
59        validation_split = kwargs.get('validation_split', True)
60        if validation_split:
61            X_train, X_test, y_train, y_test = train_test_split(
62                X, y, test_size=test_size, random_state=42
63            )
64            self.X_test = X_test
65            self.y_test = y_test
66        else:
67            X_train, y_train = X, y
68        X_train = torch.tensor(X_train, dtype=torch.float32).to(self.device)
69        y_train = torch.tensor(y_train, dtype=torch.long).to(self.device)
70        criterion = nn.CrossEntropyLoss()
71        optimizer = optim.Adam(self.model.parameters(), lr=self.config['lr'])
72        for epoch in range(self.epochs):
73            self.model.train()
74            optimizer.zero_grad()
75            outputs = self.model(X_train)
76            loss = criterion(outputs, y_train)
77            loss.backward()
78            optimizer.step()
79            if (epoch+1) % 5 == 0 or epoch == 0:
80                print(f"Epoch [{epoch+1}/{self.epochs}], Loss: {loss.item():.4f}")
81        self.trained = True
82        print("LSTM model trained successfully.")

Train the classification model.

Args: features: List of feature dictionaries **kwargs: Additional arguments for training

def predict(self, features: List[Dict], **kwargs) -> numpy.ndarray:
84    def predict(self, features: List[Dict], **kwargs) -> np.ndarray:
85        if not self.trained:
86            raise ValueError("Model must be trained before making predictions")
87        X, _ = preprocess_features(features)
88        X = X.reshape((X.shape[0], 1, X.shape[1]))
89        X = torch.tensor(X, dtype=torch.float32).to(self.device)
90        self.model.eval()
91        with torch.no_grad():
92            outputs = self.model(X)
93            _, predicted = torch.max(outputs.data, 1)
94        return predicted.cpu().numpy()

Make predictions using the trained model.

Args: features: List of feature dictionaries **kwargs: Additional arguments for prediction

Returns: Array of predictions

def evaluate(self, features: List[Dict], **kwargs) -> Dict[str, float]:
 96    def evaluate(self, features: List[Dict], **kwargs) -> Dict[str, float]:
 97        if not self.trained:
 98            raise ValueError("Model must be trained before evaluation")
 99        if hasattr(self, 'X_test') and hasattr(self, 'y_test'):
100            X_test, y_test = self.X_test, self.y_test
101        else:
102            X_test, y_test = preprocess_features(features)
103            X_test = X_test.reshape((X_test.shape[0], 1, X_test.shape[1]))
104        X_test = torch.tensor(X_test, dtype=torch.float32).to(self.device)
105        y_test = np.array(y_test)
106        self.model.eval()
107        with torch.no_grad():
108            outputs = self.model(X_test)
109            _, y_pred = torch.max(outputs.data, 1)
110        y_pred = y_pred.cpu().numpy()
111        accuracy = accuracy_score(y_test, y_pred)
112        conf_matrix = confusion_matrix(y_test, y_pred)
113        metrics = {
114            'accuracy': accuracy,
115            'confusion_matrix': conf_matrix.tolist()
116        }
117        detailed_report = kwargs.get('detailed_report', False)
118        if detailed_report:
119            class_report = classification_report(y_test, y_pred, output_dict=True)
120            metrics['classification_report'] = class_report
121        return metrics

Evaluate the model performance.

Args: features: List of feature dictionaries **kwargs: Additional arguments for evaluation

Returns: Dictionary containing evaluation metrics

def save_model(self, filepath: str):
123    def save_model(self, filepath: str):
124        if not self.trained:
125            raise ValueError("Model must be trained before saving")
126        torch.save({
127            'model_state_dict': self.model.state_dict(),
128            'config': self.config,
129            'feature_names': self.feature_names,
130            'class_names': self.class_names,
131            'trained': self.trained
132        }, filepath)
133        print(f"LSTM model saved to {filepath}")

Save the trained model to a file.

Args: filepath: Path to save the model

def load_model(self, filepath: str):
135    def load_model(self, filepath: str):
136        checkpoint = torch.load(filepath, map_location=self.device)
137        self.model = LSTMNet(
138            self.config['input_size'],
139            self.config['hidden_size'],
140            self.config['num_layers'],
141            self.config['num_classes']
142        ).to(self.device)
143        self.model.load_state_dict(checkpoint['model_state_dict'])
144        self.config = checkpoint.get('config', self.config)
145        self.feature_names = checkpoint.get('feature_names', [])
146        self.class_names = checkpoint.get('class_names', [])
147        self.trained = checkpoint.get('trained', True)
148        print(f"LSTM model loaded from {filepath}")

Load a trained model from a file.

Args: filepath: Path to the saved model