-
Notifications
You must be signed in to change notification settings - Fork 18
[DOC] Add PyTorch / Skorch example #704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
GrituX
wants to merge
13
commits into
683-doc-example-gallery-rework-real-life-cases-high-dimensional
Choose a base branch
from
664-doc-include-dl-example-with-pytorch-and-skorch
base: 683-doc-example-gallery-rework-real-life-cases-high-dimensional
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
f784878
[DOC] Added example to use PyTorch and Skorch with HiDimStat.
GrituX 1f4076c
fixed sign of selected pixels
GrituX fcaa6d4
Switched to group PFI for more permutations and proportionately less …
GrituX 64d7ed4
fixed merge
GrituX 4de6580
Added torch to dependencies.
GrituX 5672c61
Merge main
GrituX df80f2f
changed hyperparameters.
GrituX 9006b57
[doc] build doc pls
GrituX 1842114
[ENH] Adding LOCI (#679)
GrituX 33fe6ad
fixed typos and switched from PFI to CFI
GrituX 61c4403
[doc ] build doc
GrituX eafa466
Merge branch 'main' into 664-doc-include-dl-example-with-pytorch-and-…
GrituX 60b619a
[doc please build documentation
GrituX File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| """ | ||
| Using PyTorch with HiDimStat | ||
| ============================ | ||
| HiDimStat was designed with the goal of being easily compatible with Sklearn. | ||
| PyTorch models might not be used directly with HiDimStat for that reason. | ||
| However, with the help of a third party library `Skorch <https://skorch.readthedocs.io/en/stable/>`, | ||
| PyTorch can be interfaced with HiDimStat, and provide all of its functionalities. | ||
| In this example, we define a Convolutional Neural Network (CNN) in Skorch, | ||
| and perform pixel-wise feature importance in a binary classification setup, | ||
| through the MNIST digits dataset, between digits 4 vs 7 and 0 vs 1. | ||
| """ | ||
|
|
||
| # %% | ||
| # Defining a PyTorch model | ||
| # ------------------------- | ||
| # We start by defining a basic Convolutional Neural Network (CNN) | ||
| # composed of 2 series of (Convolution, Activation, Pooling) layers, | ||
| # followed by 2 fully-connected layers. | ||
|
|
||
| import torch.nn.functional as F | ||
| from torch import nn | ||
|
|
||
|
|
||
| class MNISTCNN(nn.Module): | ||
| def __init__(self, num_classes=2): | ||
| super().__init__() | ||
|
|
||
| self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) | ||
| self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) | ||
|
|
||
| self.pool = nn.MaxPool2d(2) | ||
|
|
||
| # 28x28 -> 14x14 -> 7x7 | ||
| self.fc1 = nn.Linear(64 * 7 * 7, 128) | ||
| self.fc2 = nn.Linear(128, num_classes) | ||
|
|
||
| def forward(self, x): | ||
| # We input an array of shape (batch_size, n_features=784) | ||
| # but conv1 expects a tensor of shape (batch_size, n_channels=1, height=28, width=28) | ||
| x = x.view((-1, 1, 28, 28)).float() | ||
| x = self.pool(F.relu(self.conv1(x))) | ||
| x = self.pool(F.relu(self.conv2(x))) | ||
|
|
||
| x = x.flatten(1) | ||
|
|
||
| x = F.relu(self.fc1(x)) | ||
| x = self.fc2(x) | ||
|
|
||
| return x | ||
|
|
||
|
|
||
| # %% | ||
| # Loading data | ||
| # ------------ | ||
| # Since Skorch interfaces PyTorch in a Scikit-learn fashion, we don't need to create torch datasets, and we simply prepare | ||
| # data as we would usually do for sklearn models. | ||
|
|
||
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| from sklearn.datasets import fetch_openml | ||
| from sklearn.utils import resample | ||
|
|
||
| mnist_dataset = fetch_openml("mnist_784", version=1, as_frame=False) | ||
| X_mnist, y_mnist = mnist_dataset.data, mnist_dataset.target | ||
|
|
||
| # Downsample to speed up the example | ||
| n_samples = 2000 | ||
| mask_4_7 = (y_mnist == "4") | (y_mnist == "7") | ||
| X_4_7, y_4_7 = X_mnist[mask_4_7], y_mnist[mask_4_7].astype(int) | ||
| X_4_7, y_4_7 = resample( | ||
| X_4_7, | ||
| y_4_7, | ||
| n_samples=n_samples, | ||
| replace=False, | ||
| random_state=0, | ||
| stratify=y_4_7, | ||
| ) | ||
|
|
||
| mask_0_1 = (y_mnist == "0") | (y_mnist == "1") | ||
| X_0_1, y_0_1 = X_mnist[mask_0_1], y_mnist[mask_0_1].astype(int) | ||
| X_0_1, y_0_1 = resample( | ||
| X_0_1, | ||
| y_0_1, | ||
| n_samples=n_samples, | ||
| replace=False, | ||
| random_state=0, | ||
| stratify=y_0_1, | ||
| ) | ||
|
|
||
| # Plot digits 0 against 1, and 4 against 7, to visually compare a few of them. | ||
| _, axes = plt.subplots( | ||
| 2, 5, figsize=(6, 4), subplot_kw={"xticks": [], "yticks": []} | ||
| ) | ||
| for i in range(5): | ||
| # Plot 0 vs 1 | ||
| label = 1 if i % 2 == 0 else 0 | ||
| axes[0, i].imshow(X_0_1[y_0_1 == label][i].reshape(28, 28), cmap="gray") | ||
| # Plot 4 vs 7 | ||
| label = 7 if i % 2 == 0 else 4 | ||
| axes[1, i].imshow(X_4_7[y_4_7 == label][i].reshape(28, 28), cmap="gray") | ||
|
|
||
| axes[0, 2].set_title("Digits 0 vs 1", fontweight="bold", y=1.0) | ||
| axes[1, 2].set_title("Digits 4 vs 7", fontweight="bold", y=1.0) | ||
|
|
||
|
|
||
| # %% | ||
| # We now create the Skorch model, and build a pipeline with a StandardScaler. | ||
| # Skorch allows the model to be run either on a cuda device or on cpu. | ||
|
|
||
| import torch.cuda | ||
| from sklearn.pipeline import make_pipeline | ||
| from sklearn.preprocessing import StandardScaler | ||
| from skorch import NeuralNetClassifier | ||
| from torch.optim import Adam | ||
|
|
||
| net = NeuralNetClassifier( | ||
| MNISTCNN, | ||
| max_epochs=5, | ||
| lr=1e-3, | ||
| batch_size=64, | ||
| optimizer=Adam, | ||
| criterion=nn.CrossEntropyLoss, | ||
| iterator_train__shuffle=True, | ||
| device="cuda" if torch.cuda.is_available() else "cpu", | ||
| ) | ||
| model = make_pipeline(StandardScaler(), net) | ||
| # Current hotfix of API compatibility issue (Skorch doesn't automatically set this during fitting). | ||
| # This needs to be set, otherwise HiDimStat throws an error. | ||
| net.n_features_in_ = 28 * 28 | ||
|
|
||
| # %% | ||
| # Running HiDimStat feature importance computation | ||
| # ------------------------------------------------ | ||
| # We cluster pixels through feature agglomeration, while leveraging their grid structure | ||
| # and assess feature importance at the group level. | ||
| # This is done with a Conditional Feature Importance (CFI). For each binary | ||
| # classification (0 vs 1, 4 vs 7), we fit the model and evaluate feature | ||
| # importance. | ||
|
|
||
| from sklearn.cluster import FeatureAgglomeration | ||
| from sklearn.feature_extraction import image | ||
| from sklearn.metrics import log_loss | ||
| from sklearn.model_selection import train_test_split | ||
|
|
||
| from hidimstat import CFI | ||
|
|
||
| shape = (28, 28) | ||
| n_clusters = 50 | ||
| target_fwer = 0.1 | ||
|
|
||
| X_cluster = resample( | ||
| X_4_7, n_samples=n_samples // 2, replace=False, random_state=0 | ||
| ) | ||
| connectivity = image.grid_to_graph(n_x=shape[0], n_y=shape[1]) | ||
| clustering = FeatureAgglomeration( | ||
| n_clusters=n_clusters, connectivity=connectivity | ||
| ) | ||
| clustering.fit(X_cluster) | ||
|
|
||
| # CFI expects features_groups to be a dictionary where the keys are the group names | ||
| # and the values are the list of column names corresponding to each features group. | ||
| order = np.argsort(clustering.labels_) | ||
| sorted_ids = clustering.labels_[order] | ||
| unique_ids, start_idx = np.unique(sorted_ids, return_index=True) | ||
| positions = np.split(order, start_idx[1:]) | ||
| features_groups = dict(zip(unique_ids, positions, strict=False)) | ||
|
|
||
| # %% | ||
| # We now instantiate the CFI and fit it on the two tasks. | ||
|
|
||
| # Careful when using Skorch, having n_jobs > 1 might create joblib and pickle issues. | ||
| cfi = CFI( | ||
| estimator=model, | ||
| features_groups=features_groups, | ||
| n_permutations=20, | ||
| method="predict_proba", | ||
| loss=log_loss, | ||
| random_state=0, | ||
| ) | ||
| # PyTorch expects float input, and long type target. | ||
| # Since it's a binary classification, we convert the classes into 4->0 and 7->1 for Skorch. | ||
| y_target = y_4_7.astype(np.int64).copy() | ||
| y_target[y_target == 4] = 0 | ||
| y_target[y_target == 7] = 1 | ||
|
|
||
| X_train, X_test, y_train, y_test = train_test_split( | ||
| X_4_7, y_target, test_size=0.3, random_state=0, stratify=y_target | ||
| ) | ||
| model.fit(X_train, y=y_train) | ||
| cfi.fit_importance(X_test, y_test) | ||
| selected_4_7 = cfi.fwer_selection( | ||
| fwer=target_fwer, n_tests=n_clusters, two_tailed_test=True | ||
| ) | ||
|
|
||
| # No need to convert here since it's already 0 and 1. | ||
| X_train, X_test, y_train, y_test = train_test_split( | ||
| X_0_1, | ||
| y_0_1.astype(np.int64), | ||
| test_size=0.3, | ||
| random_state=0, | ||
| stratify=y_0_1, | ||
| ) | ||
| model.fit(X_train, y=y_train) | ||
| cfi.fit_importance(X_test, y_test) | ||
| selected_0_1 = cfi.fwer_selection( | ||
| fwer=target_fwer, n_tests=n_clusters, two_tailed_test=True | ||
| ) | ||
|
|
||
| # %% | ||
| # Visualizing the results | ||
| # ----------------------- | ||
| # Finally, we visualize the significant pixels identified by CFI for each of the | ||
| # classification tasks. | ||
|
|
||
| import matplotlib.pyplot as plt | ||
| from matplotlib.colors import ListedColormap | ||
|
|
||
| _, axes = plt.subplots( | ||
| 1, 2, figsize=(5, 2), subplot_kw={"xticks": [], "yticks": []} | ||
| ) | ||
|
|
||
| for i, (title, selected) in enumerate( | ||
| [ | ||
| ("4 vs 7", selected_4_7), | ||
| ("0 vs 1", selected_0_1), | ||
| ] | ||
| ): | ||
| pixel_selection = np.zeros((len(clustering.labels_),), dtype=int) | ||
| # selected contains the selected cluster groups, so we convert this back to pixel selection. | ||
| for sign in (-1, 1): | ||
| pixels = [ | ||
| pixels | ||
| for pixels, sel in zip( | ||
| features_groups.values(), selected, strict=False | ||
| ) | ||
| if sel == sign | ||
| ] | ||
| if pixels: | ||
| pixel_selection[np.concatenate(pixels)] = sign | ||
| mask_pfi = pixel_selection.reshape(shape) | ||
|
|
||
| cmap = ListedColormap(["tab:red", "white", "tab:blue"]) | ||
| axes[i].imshow(mask_pfi, cmap=cmap, vmin=-1, vmax=1) | ||
| axes[i].set_title(title, fontweight="bold", y=1.0) | ||
|
|
||
| plt.tight_layout() | ||
| plt.show() | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
already imported