gaitsetpy.classification.models.bilstm

  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 BiLSTMNet(nn.Module):
 13    def __init__(self, input_size, hidden_size, num_layers, num_classes, dropout=0.2):
 14        super(BiLSTMNet, self).__init__()
 15        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout, bidirectional=True)
 16        self.fc = nn.Linear(hidden_size * 2, 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 BiLSTMModel(BaseClassificationModel):
 24    """
 25    Bidirectional 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="bilstm",
 31            description="Bidirectional 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 = BiLSTMNet(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        X = X.reshape((X.shape[0], 1, X.shape[1]))
 53        self.feature_names = [f"feature_{i}" for i in range(X.shape[2])]
 54        self.class_names = list(set(y))
 55        test_size = kwargs.get('test_size', 0.2)
 56        validation_split = kwargs.get('validation_split', True)
 57        if validation_split:
 58            X_train, X_test, y_train, y_test = train_test_split(
 59                X, y, test_size=test_size, random_state=42
 60            )
 61            self.X_test = X_test
 62            self.y_test = y_test
 63        else:
 64            X_train, y_train = X, y
 65        X_train = torch.tensor(X_train, dtype=torch.float32).to(self.device)
 66        y_train = torch.tensor(y_train, dtype=torch.long).to(self.device)
 67        criterion = nn.CrossEntropyLoss()
 68        optimizer = optim.Adam(self.model.parameters(), lr=self.config['lr'])
 69        for epoch in range(self.epochs):
 70            self.model.train()
 71            optimizer.zero_grad()
 72            outputs = self.model(X_train)
 73            loss = criterion(outputs, y_train)
 74            loss.backward()
 75            optimizer.step()
 76            if (epoch+1) % 5 == 0 or epoch == 0:
 77                print(f"Epoch [{epoch+1}/{self.epochs}], Loss: {loss.item():.4f}")
 78        self.trained = True
 79        print("BiLSTM model trained successfully.")
 80
 81    def predict(self, features: List[Dict], **kwargs) -> np.ndarray:
 82        if not self.trained:
 83            raise ValueError("Model must be trained before making predictions")
 84        X, _ = preprocess_features(features)
 85        X = X.reshape((X.shape[0], 1, X.shape[1]))
 86        X = torch.tensor(X, dtype=torch.float32).to(self.device)
 87        self.model.eval()
 88        with torch.no_grad():
 89            outputs = self.model(X)
 90            _, predicted = torch.max(outputs.data, 1)
 91        return predicted.cpu().numpy()
 92
 93    def evaluate(self, features: List[Dict], **kwargs) -> Dict[str, float]:
 94        if not self.trained:
 95            raise ValueError("Model must be trained before evaluation")
 96        if hasattr(self, 'X_test') and hasattr(self, 'y_test'):
 97            X_test, y_test = self.X_test, self.y_test
 98        else:
 99            X_test, y_test = preprocess_features(features)
100            X_test = X_test.reshape((X_test.shape[0], 1, X_test.shape[1]))
101        X_test = torch.tensor(X_test, dtype=torch.float32).to(self.device)
102        y_test = np.array(y_test)
103        self.model.eval()
104        with torch.no_grad():
105            outputs = self.model(X_test)
106            _, y_pred = torch.max(outputs.data, 1)
107        y_pred = y_pred.cpu().numpy()
108        accuracy = accuracy_score(y_test, y_pred)
109        conf_matrix = confusion_matrix(y_test, y_pred)
110        metrics = {
111            'accuracy': accuracy,
112            'confusion_matrix': conf_matrix.tolist()
113        }
114        detailed_report = kwargs.get('detailed_report', False)
115        if detailed_report:
116            class_report = classification_report(y_test, y_pred, output_dict=True)
117            metrics['classification_report'] = class_report
118        return metrics
119
120    def save_model(self, filepath: str):
121        if not self.trained:
122            raise ValueError("Model must be trained before saving")
123        torch.save({
124            'model_state_dict': self.model.state_dict(),
125            'config': self.config,
126            'feature_names': self.feature_names,
127            'class_names': self.class_names,
128            'trained': self.trained
129        }, filepath)
130        print(f"BiLSTM model saved to {filepath}")
131
132    def load_model(self, filepath: str):
133        checkpoint = torch.load(filepath, map_location=self.device)
134        self.model = BiLSTMNet(
135            self.config['input_size'],
136            self.config['hidden_size'],
137            self.config['num_layers'],
138            self.config['num_classes']
139        ).to(self.device)
140        self.model.load_state_dict(checkpoint['model_state_dict'])
141        self.config = checkpoint.get('config', self.config)
142        self.feature_names = checkpoint.get('feature_names', [])
143        self.class_names = checkpoint.get('class_names', [])
144        self.trained = checkpoint.get('trained', True)
145        print(f"BiLSTM model loaded from {filepath}")
class BiLSTMNet(torch.nn.modules.module.Module):
13class BiLSTMNet(nn.Module):
14    def __init__(self, input_size, hidden_size, num_layers, num_classes, dropout=0.2):
15        super(BiLSTMNet, self).__init__()
16        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout, bidirectional=True)
17        self.fc = nn.Linear(hidden_size * 2, 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

BiLSTMNet(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(BiLSTMNet, self).__init__()
16        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout, bidirectional=True)
17        self.fc = nn.Linear(hidden_size * 2, 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.

class BiLSTMModel(gaitsetpy.core.base_classes.BaseClassificationModel):
 24class BiLSTMModel(BaseClassificationModel):
 25    """
 26    Bidirectional 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="bilstm",
 32            description="Bidirectional 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 = BiLSTMNet(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        X = X.reshape((X.shape[0], 1, X.shape[1]))
 54        self.feature_names = [f"feature_{i}" for i in range(X.shape[2])]
 55        self.class_names = list(set(y))
 56        test_size = kwargs.get('test_size', 0.2)
 57        validation_split = kwargs.get('validation_split', True)
 58        if validation_split:
 59            X_train, X_test, y_train, y_test = train_test_split(
 60                X, y, test_size=test_size, random_state=42
 61            )
 62            self.X_test = X_test
 63            self.y_test = y_test
 64        else:
 65            X_train, y_train = X, y
 66        X_train = torch.tensor(X_train, dtype=torch.float32).to(self.device)
 67        y_train = torch.tensor(y_train, dtype=torch.long).to(self.device)
 68        criterion = nn.CrossEntropyLoss()
 69        optimizer = optim.Adam(self.model.parameters(), lr=self.config['lr'])
 70        for epoch in range(self.epochs):
 71            self.model.train()
 72            optimizer.zero_grad()
 73            outputs = self.model(X_train)
 74            loss = criterion(outputs, y_train)
 75            loss.backward()
 76            optimizer.step()
 77            if (epoch+1) % 5 == 0 or epoch == 0:
 78                print(f"Epoch [{epoch+1}/{self.epochs}], Loss: {loss.item():.4f}")
 79        self.trained = True
 80        print("BiLSTM model trained successfully.")
 81
 82    def predict(self, features: List[Dict], **kwargs) -> np.ndarray:
 83        if not self.trained:
 84            raise ValueError("Model must be trained before making predictions")
 85        X, _ = preprocess_features(features)
 86        X = X.reshape((X.shape[0], 1, X.shape[1]))
 87        X = torch.tensor(X, dtype=torch.float32).to(self.device)
 88        self.model.eval()
 89        with torch.no_grad():
 90            outputs = self.model(X)
 91            _, predicted = torch.max(outputs.data, 1)
 92        return predicted.cpu().numpy()
 93
 94    def evaluate(self, features: List[Dict], **kwargs) -> Dict[str, float]:
 95        if not self.trained:
 96            raise ValueError("Model must be trained before evaluation")
 97        if hasattr(self, 'X_test') and hasattr(self, 'y_test'):
 98            X_test, y_test = self.X_test, self.y_test
 99        else:
100            X_test, y_test = preprocess_features(features)
101            X_test = X_test.reshape((X_test.shape[0], 1, X_test.shape[1]))
102        X_test = torch.tensor(X_test, dtype=torch.float32).to(self.device)
103        y_test = np.array(y_test)
104        self.model.eval()
105        with torch.no_grad():
106            outputs = self.model(X_test)
107            _, y_pred = torch.max(outputs.data, 1)
108        y_pred = y_pred.cpu().numpy()
109        accuracy = accuracy_score(y_test, y_pred)
110        conf_matrix = confusion_matrix(y_test, y_pred)
111        metrics = {
112            'accuracy': accuracy,
113            'confusion_matrix': conf_matrix.tolist()
114        }
115        detailed_report = kwargs.get('detailed_report', False)
116        if detailed_report:
117            class_report = classification_report(y_test, y_pred, output_dict=True)
118            metrics['classification_report'] = class_report
119        return metrics
120
121    def save_model(self, filepath: str):
122        if not self.trained:
123            raise ValueError("Model must be trained before saving")
124        torch.save({
125            'model_state_dict': self.model.state_dict(),
126            'config': self.config,
127            'feature_names': self.feature_names,
128            'class_names': self.class_names,
129            'trained': self.trained
130        }, filepath)
131        print(f"BiLSTM model saved to {filepath}")
132
133    def load_model(self, filepath: str):
134        checkpoint = torch.load(filepath, map_location=self.device)
135        self.model = BiLSTMNet(
136            self.config['input_size'],
137            self.config['hidden_size'],
138            self.config['num_layers'],
139            self.config['num_classes']
140        ).to(self.device)
141        self.model.load_state_dict(checkpoint['model_state_dict'])
142        self.config = checkpoint.get('config', self.config)
143        self.feature_names = checkpoint.get('feature_names', [])
144        self.class_names = checkpoint.get('class_names', [])
145        self.trained = checkpoint.get('trained', True)
146        print(f"BiLSTM model loaded from {filepath}")

Bidirectional LSTM classification model using PyTorch. Implements the BaseClassificationModel interface.

BiLSTMModel( 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="bilstm",
32            description="Bidirectional 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 = BiLSTMNet(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        X = X.reshape((X.shape[0], 1, X.shape[1]))
54        self.feature_names = [f"feature_{i}" for i in range(X.shape[2])]
55        self.class_names = list(set(y))
56        test_size = kwargs.get('test_size', 0.2)
57        validation_split = kwargs.get('validation_split', True)
58        if validation_split:
59            X_train, X_test, y_train, y_test = train_test_split(
60                X, y, test_size=test_size, random_state=42
61            )
62            self.X_test = X_test
63            self.y_test = y_test
64        else:
65            X_train, y_train = X, y
66        X_train = torch.tensor(X_train, dtype=torch.float32).to(self.device)
67        y_train = torch.tensor(y_train, dtype=torch.long).to(self.device)
68        criterion = nn.CrossEntropyLoss()
69        optimizer = optim.Adam(self.model.parameters(), lr=self.config['lr'])
70        for epoch in range(self.epochs):
71            self.model.train()
72            optimizer.zero_grad()
73            outputs = self.model(X_train)
74            loss = criterion(outputs, y_train)
75            loss.backward()
76            optimizer.step()
77            if (epoch+1) % 5 == 0 or epoch == 0:
78                print(f"Epoch [{epoch+1}/{self.epochs}], Loss: {loss.item():.4f}")
79        self.trained = True
80        print("BiLSTM 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:
82    def predict(self, features: List[Dict], **kwargs) -> np.ndarray:
83        if not self.trained:
84            raise ValueError("Model must be trained before making predictions")
85        X, _ = preprocess_features(features)
86        X = X.reshape((X.shape[0], 1, X.shape[1]))
87        X = torch.tensor(X, dtype=torch.float32).to(self.device)
88        self.model.eval()
89        with torch.no_grad():
90            outputs = self.model(X)
91            _, predicted = torch.max(outputs.data, 1)
92        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]:
 94    def evaluate(self, features: List[Dict], **kwargs) -> Dict[str, float]:
 95        if not self.trained:
 96            raise ValueError("Model must be trained before evaluation")
 97        if hasattr(self, 'X_test') and hasattr(self, 'y_test'):
 98            X_test, y_test = self.X_test, self.y_test
 99        else:
100            X_test, y_test = preprocess_features(features)
101            X_test = X_test.reshape((X_test.shape[0], 1, X_test.shape[1]))
102        X_test = torch.tensor(X_test, dtype=torch.float32).to(self.device)
103        y_test = np.array(y_test)
104        self.model.eval()
105        with torch.no_grad():
106            outputs = self.model(X_test)
107            _, y_pred = torch.max(outputs.data, 1)
108        y_pred = y_pred.cpu().numpy()
109        accuracy = accuracy_score(y_test, y_pred)
110        conf_matrix = confusion_matrix(y_test, y_pred)
111        metrics = {
112            'accuracy': accuracy,
113            'confusion_matrix': conf_matrix.tolist()
114        }
115        detailed_report = kwargs.get('detailed_report', False)
116        if detailed_report:
117            class_report = classification_report(y_test, y_pred, output_dict=True)
118            metrics['classification_report'] = class_report
119        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):
121    def save_model(self, filepath: str):
122        if not self.trained:
123            raise ValueError("Model must be trained before saving")
124        torch.save({
125            'model_state_dict': self.model.state_dict(),
126            'config': self.config,
127            'feature_names': self.feature_names,
128            'class_names': self.class_names,
129            'trained': self.trained
130        }, filepath)
131        print(f"BiLSTM model saved to {filepath}")

Save the trained model to a file.

Args: filepath: Path to save the model

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

Load a trained model from a file.

Args: filepath: Path to the saved model