Perceptron

manify.predictors.perceptron

Product space perceptron implementation.

ProductSpacePerceptron(pm, max_epochs=1000, patience=5, weights=None, task='classification', random_state=None, device=None)

Bases: BasePredictor

A product-space perceptron model for multiclass classification in the product manifold space.

Parameters:
  • pm (ProductManifold) –

    ProductManifold object for the product space.

  • max_epochs (int, default: 1000 ) –

    Maximum number of training epochs.

  • patience (int, default: 5 ) –

    Number of consecutive epochs without improvement to consider convergence.

  • weights (Float[Tensor, 'n_manifolds'] | None, default: None ) –

    Per-manifold weights for kernel combination.

  • task (str, default: 'classification' ) –

    Task type (defaults to "classification").

  • random_state (int | None, default: None ) –

    Random seed for reproducibility.

  • device (str | None, default: None ) –

    Device for tensor computations.

Attributes:
  • pm

    ProductManifold object associated with the predictor.

  • max_epochs

    Maximum number of training epochs.

  • patience

    Number of consecutive epochs without improvement to consider convergence.

  • weights

    Per-manifold weights for kernel combination.

  • alpha

    Dictionary storing perceptron coefficients for each class.

  • X_train_

    Training data points.

  • y_train_

    Training labels.

  • is_fitted_ (bool) –

    Boolean flag indicating if the predictor has been fitted.

Source code in manify/predictors/perceptron.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def __init__(
    self,
    pm: ProductManifold,
    max_epochs: int = 1_000,
    patience: int = 5,
    weights: Float[torch.Tensor, "n_manifolds"] | None = None,
    task: str = "classification",
    random_state: int | None = None,
    device: str | None = None,
):
    # Initialize base class
    super().__init__(pm=pm, task=task, random_state=random_state, device=device)
    self.pm = pm  # ProductManifold instance
    self.max_epochs = max_epochs
    self.patience = patience  # Number of consecutive epochs without improvement to consider convergence
    self.weights = torch.ones(len(pm.P), dtype=torch.float32) if weights is None else weights
    assert len(self.weights) == len(pm.P), "Number of weights must match the number of manifolds."

fit(X, y)

Trains the perceptron model using the provided data and labels.

Parameters:
  • X (Float[Tensor, 'n_samples n_manifolds']) –

    Training data tensor.

  • y (Int[Tensor, 'n_samples']) –

    Class labels for the training data.

Returns:
Source code in manify/predictors/perceptron.py
 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
def fit(
    self, X: Float[torch.Tensor, "n_samples n_manifolds"], y: Int[torch.Tensor, "n_samples"]
) -> ProductSpacePerceptron:
    """Trains the perceptron model using the provided data and labels.

    Args:
        X: Training data tensor.
        y: Class labels for the training data.

    Returns:
        self: Fitted perceptron model.
    """
    # Identify unique classes for multiclass classification
    self._store_classes(y)
    n_samples = X.shape[0]

    # Precompute kernel matrix
    Ks, _ = product_kernel(self.pm, X, None)
    K = torch.ones((n_samples, n_samples), dtype=X.dtype, device=X.device)
    for K_m, w in zip(Ks, self.weights, strict=False):
        K += w * K_m

    # Store training data and labels for prediction
    self.X_train_ = X
    self.y_train_ = y

    # Initialize dictionary to store alpha coefficients for each class
    self.alpha = {}

    # For patience checking
    best_epoch, least_errors = 0, n_samples + 1

    for class_label in self.classes_:
        class_label_item = class_label.item()

        # One-vs-rest labels
        y_binary = torch.where(y == class_label_item, 1, -1)  # Shape: (n_samples,)

        # Initialize alpha coefficients for this class
        alpha = torch.zeros(n_samples, dtype=X.dtype, device=X.device)

        for epoch in range(self.max_epochs):
            # Compute decision function: f = K @ (alpha * y_binary)
            f = K @ (alpha * y_binary)  # Shape: (n_samples,)

            # Compute predictions
            predictions = torch.sign(f)

            # Find misclassified samples
            misclassified = predictions != y_binary

            # If no misclassifications, break early
            if not misclassified.any():
                break

            # Test patience
            n_errors = misclassified.sum().item()
            if n_errors < least_errors:
                best_epoch, least_errors = epoch, n_errors
            if epoch - best_epoch >= self.patience:
                break

            # Update alpha coefficients for misclassified samples
            alpha[misclassified] += 1

        # Store the alpha coefficients for the current class
        self.alpha[class_label_item] = alpha

    self.is_fitted_ = True
    return self

predict_proba(X)

Predicts the decision values for each class.

Parameters:
  • X (Float[Tensor, 'n_points n_features']) –

    Test data tensor.

Returns:
  • decision_values( Float[Tensor, 'n_points n_classes'] ) –

    Decision values for each test sample and each class.

Source code in manify/predictors/perceptron.py
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
def predict_proba(
    self,
    X: Float[torch.Tensor, "n_points n_features"],  # type: ignore[override]
) -> Float[torch.Tensor, "n_points n_classes"]:
    """Predicts the decision values for each class.

    Args:
        X: Test data tensor.

    Returns:
        decision_values: Decision values for each test sample and each class.
    """
    n_samples = X.shape[0]
    n_classes = len(self.classes_)
    decision_values = torch.zeros((n_samples, n_classes), dtype=X.dtype, device=X.device)

    # Compute kernel matrix between training data and test data
    Ks, _ = product_kernel(self.pm, self.X_train_, X)
    K_test = torch.ones((self.X_train_.shape[0], n_samples), dtype=X.dtype, device=X.device)
    for K_m, w in zip(Ks, self.weights, strict=False):
        K_test += w * K_m
    # K_test = self.X_train_ @ X.T

    for idx, class_label in enumerate(self.classes_):
        class_label_item = class_label.item()
        alpha = self.alpha[class_label_item]  # Shape: (n_samples_train,)
        y_binary = torch.where(self.y_train_ == class_label_item, 1, -1)  # Shape: (n_samples_train,)

        # Compute decision function for test samples
        f = (alpha * y_binary) @ K_test  # Shape: (n_samples_test,)
        decision_values[:, idx] = f

    return decision_values