From e06534e595eda498dd47952d689d33d4de6cd75b Mon Sep 17 00:00:00 2001 From: Otavio Napoli Date: Fri, 12 Apr 2024 17:36:54 +0000 Subject: [PATCH 01/11] Added sphinx documentaiton --- docs/Makefile | 20 +++++++ docs/api.rst | 8 +++ docs/conf.py | 101 ++++++++++++++++++++++++++++++++++++ docs/contributing.rst | 4 ++ docs/experiments.rst | 4 ++ docs/index.rst | 25 +++++++++ docs/installation.rst | 4 ++ docs/make.bat | 35 +++++++++++++ docs/modules.rst | 7 +++ docs/notebooks | 1 + docs/ssl_tools.analysis.rst | 21 ++++++++ docs/tutorials.rst | 10 ++++ 12 files changed, 240 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/contributing.rst create mode 100644 docs/experiments.rst create mode 100644 docs/index.rst create mode 100644 docs/installation.rst create mode 100644 docs/make.bat create mode 100644 docs/modules.rst create mode 120000 docs/notebooks create mode 100644 docs/ssl_tools.analysis.rst create mode 100644 docs/tutorials.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..a7d46f4 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,8 @@ +Programming Reference +----------------------- + +Here is the Programming Interface + +.. toctree:: + + autoapi/ssl_tools/index diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..f236d74 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,101 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html +import sys +import os +sys.path.insert(0, os.path.abspath('../')) + +import sphinx_rtd_theme + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'Minerva' +copyright = '2024, Unicamp' +author = 'Discovery' + +source_suffix = ['.rst'] +master_doc = 'index' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [ + '**.ipynb_checkpoints', + "**ipynb_checkpoints" +] + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'autoapi.extension', + 'sphinx_rtd_theme', + 'sphinx.ext.viewcode', + 'sphinx.ext.autodoc.typehints', + 'sphinx.ext.mathjax', + "nbsphinx", + "IPython.sphinxext.ipython_console_highlighting" +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +####### Auto API +autoapi_type = 'python' +autoapi_dirs = ['../sslt/'] +autoapi_member_order = 'alphabetical' +autoapi_python_use_implicit_namespaces = True +autoapi_python_class_content = 'both' +autoapi_file_patterns = ['*.py'] +autoapi_generate_api_docs = True +autoapi_add_toctree_entry = False +# source_suffix = '.rst' +autodoc_typehints = 'description' + +######## NBSPHINX +nbsphinx_execute = 'never' +nbsphinx_allow_errors = True +nbsphinx_codecell_lexer = 'python3' +nbsphinx_execute_arguments = [ + "--InlineBackend.figure_formats={'svg', 'pdf'}", + "--InlineBackend.rc={'figure.dpi': 96}", +] + + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' +htmlhelp_basename = 'ssl_tools_docs' +htmlhelp_basename = 'ssl_tools_docs' +source_encoding = 'utf-8' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] \ No newline at end of file diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..3fa64d7 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,4 @@ +Contributing +------------ + +Under construction... \ No newline at end of file diff --git a/docs/experiments.rst b/docs/experiments.rst new file mode 100644 index 0000000..eadc650 --- /dev/null +++ b/docs/experiments.rst @@ -0,0 +1,4 @@ +Running Experiments +-------------------- + +Under construction... \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..91a42fa --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,25 @@ +.. SSLTools documentation master file, created by + sphinx-quickstart on Mon Jan 29 18:33:36 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to SSLTools's documentation! +==================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + installation + tutorials + experiments + contributing + api + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..88d3618 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,4 @@ +Installation +----------------- + +Under construction... \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..153be5e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000..973898d --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,7 @@ +ssl_tools +========= + +.. toctree:: + :maxdepth: 4 + + ssl_tools diff --git a/docs/notebooks b/docs/notebooks new file mode 120000 index 0000000..edb8f02 --- /dev/null +++ b/docs/notebooks @@ -0,0 +1 @@ +../notebooks/ \ No newline at end of file diff --git a/docs/ssl_tools.analysis.rst b/docs/ssl_tools.analysis.rst new file mode 100644 index 0000000..9bf3480 --- /dev/null +++ b/docs/ssl_tools.analysis.rst @@ -0,0 +1,21 @@ +ssl\_tools.analysis package +=========================== + +Submodules +---------- + +ssl\_tools.analysis.plot\_metrics module +---------------------------------------- + +.. automodule:: ssl_tools.analysis.plot_metrics + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ssl_tools.analysis + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/tutorials.rst b/docs/tutorials.rst new file mode 100644 index 0000000..2edd48a --- /dev/null +++ b/docs/tutorials.rst @@ -0,0 +1,10 @@ +.. _tutorials: + +========================== +Tutorials +========================== + +.. toctree:: + :maxdepth: 2 + + Under construction \ No newline at end of file From 7760ac7cc95a0208866b168bd74f0f57b59a4a5f Mon Sep 17 00:00:00 2001 From: Otavio Napoli Date: Wed, 29 May 2024 02:29:08 +0000 Subject: [PATCH 02/11] Docs and updates --- .devcontainer/Dockerfile | 23 ++--- .devcontainer/devcontainer.json | 12 +-- .devcontainer/post_start.sh | 9 +- .gitignore | 4 + docker/{dockerfile => Dockerfile} | 0 docs/Makefile | 2 +- docs/api.rst | 2 +- docs/build.sh | 4 + docs/conf.py | 10 +-- docs/index.rst | 2 +- docs/make.bat | 2 +- docs/modules.rst | 4 +- docs/notebooks | 1 - docs/notebooks/1.datasets.ipynb | 131 ++++++++++++++++++++++++++++ docs/ssl_tools.analysis.rst | 21 ----- minerva/data/readers/tiff_reader.py | 4 +- minerva/transforms/transform.py | 19 ++++ pyproject.toml | 54 +++++++----- 18 files changed, 221 insertions(+), 83 deletions(-) rename docker/{dockerfile => Dockerfile} (100%) create mode 100755 docs/build.sh delete mode 120000 docs/notebooks create mode 100644 docs/notebooks/1.datasets.ipynb delete mode 100644 docs/ssl_tools.analysis.rst diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b855e2b..c8d9711 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -53,19 +53,20 @@ RUN rm -rf /var/lib/apt/lists/* && \ # System-wide python packages RUN python -m pip install --upgrade pip setuptools pytest && \ - python -m pip install black \ + python -m pip install \ + black \ ipykernel \ - kaleido \ - lightning>=2.1.0 \ - matplotlib \ + kaleido>=0.2 \ + lightning>=2.1.9 \ + matplotlib>=3.7 \ nbformat \ - numpy \ - pandas \ - plotly \ - seaborn \ - scipy \ - torchmetrics \ - torchvision + numpy>=1.22 \ + pandas>=2.0 \ + plotly>=5.18 \ + seaborn>=0.11 \ + scipy>=1.10 \ + torchmetrics>=1.3.0 \ + torchvision>=0.15 # Change shell for user b diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1149c8c..53e5032 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,6 +2,9 @@ "build": { "dockerfile": "Dockerfile" }, + "postStartCommand": ".devcontainer/post_start.sh", + "containerUser": "vscode", + "runArgs": [ "--gpus", "all", @@ -14,8 +17,6 @@ "--ulimit", "stack=67108864" ], - "postStartCommand": ".devcontainer/post_start.sh", - "containerUser": "vscode", "customizations": { "vscode": { "extensions": [ @@ -23,12 +24,7 @@ "ms-python.black-formatter", "ms-toolsai.jupyter", "ms-toolsai.vscode-jupyter-powertoys", - "github.copilot", - "donjayamanne.git-extension-pack", - "akamud.vscode-theme-onedark", - "njpwerner.autodocstring", - "grapecity.gc-excelviewer", - "yzhang.markdown-all-in-one" + "donjayamanne.git-extension-pack" ] } } diff --git a/.devcontainer/post_start.sh b/.devcontainer/post_start.sh index 6b4315b..817b683 100755 --- a/.devcontainer/post_start.sh +++ b/.devcontainer/post_start.sh @@ -1,10 +1,5 @@ #!/bin/bash -echo "Installing minerva" +echo "Installing minerva (editable mode)..." pip install -e . - -# Add tmux options -echo -e "set -g default-terminal \"screen-256color\"\nset -g mouse on\nbind e setw synchronize-panes on\nbind E setw synchronize-panes off" >> ~/.tmux.conf - -# remove full path from prompt -sed -i '/^\s*PS1.*\\w/s/\\w/\\W/g' ~/.bashrc +echo "Minerva installed in editable mode" diff --git a/.gitignore b/.gitignore index 2bdadd2..f289fad 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,7 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +experiments/ +logs/ +lightning_logs/ +data/ \ No newline at end of file diff --git a/docker/dockerfile b/docker/Dockerfile similarity index 100% rename from docker/dockerfile rename to docker/Dockerfile diff --git a/docs/Makefile b/docs/Makefile index d4bb2cb..ed88099 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -6,7 +6,7 @@ SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . -BUILDDIR = _build +BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/api.rst b/docs/api.rst index a7d46f4..f0a40cf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5,4 +5,4 @@ Here is the Programming Interface .. toctree:: - autoapi/ssl_tools/index + autoapi/minerva/index diff --git a/docs/build.sh b/docs/build.sh new file mode 100755 index 0000000..96ecf66 --- /dev/null +++ b/docs/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# make clean +make html \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index f236d74..b0048df 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ # -- Project information ----------------------------------------------------- -project = 'Minerva' +project = 'minerva' copyright = '2024, Unicamp' author = 'Discovery' @@ -60,11 +60,11 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['build', 'Thumbs.db', '.DS_Store', '.git'] ####### Auto API autoapi_type = 'python' -autoapi_dirs = ['../sslt/'] +autoapi_dirs = ['../minerva/'] autoapi_member_order = 'alphabetical' autoapi_python_use_implicit_namespaces = True autoapi_python_class_content = 'both' @@ -91,8 +91,8 @@ # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' -htmlhelp_basename = 'ssl_tools_docs' -htmlhelp_basename = 'ssl_tools_docs' +htmlhelp_basename = 'minerva_docs' +htmlhelp_basename = 'minerva_docs' source_encoding = 'utf-8' # Add any paths that contain custom static files (such as style sheets) here, diff --git a/docs/index.rst b/docs/index.rst index 91a42fa..d1dfeae 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,7 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to SSLTools's documentation! +Welcome to Minerva's documentation! ==================================== .. toctree:: diff --git a/docs/make.bat b/docs/make.bat index 153be5e..21b58de 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -8,7 +8,7 @@ if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. -set BUILDDIR=_build +set BUILDDIR=build if "%1" == "" goto help diff --git a/docs/modules.rst b/docs/modules.rst index 973898d..a1b3500 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,7 +1,7 @@ -ssl_tools +minerva ========= .. toctree:: :maxdepth: 4 - ssl_tools + minerva diff --git a/docs/notebooks b/docs/notebooks deleted file mode 120000 index edb8f02..0000000 --- a/docs/notebooks +++ /dev/null @@ -1 +0,0 @@ -../notebooks/ \ No newline at end of file diff --git a/docs/notebooks/1.datasets.ipynb b/docs/notebooks/1.datasets.ipynb new file mode 100644 index 0000000..9ae5b10 --- /dev/null +++ b/docs/notebooks/1.datasets.ipynb @@ -0,0 +1,131 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Datasets, Readers, and Related Data Structures\n", + "\n", + "A dataset is composed of samples. Each sample is a collection of data units related to each other in some way. Typically, the task is to learn the relationships between these units of data.\n", + "\n", + "## Examples of Data Units, Samples, and Datasets\n", + "\n", + "1. **Image Classification**:\n", + " - **Sample**: A 2-element tuple consisting of an image of size CxHxW and a label (an integer from 0 to C-1, where C is the number of classes).\n", + "\n", + "2. **Image Segmentation**:\n", + " - **Sample**: A 2-element tuple consisting of an image of size CxHxW and a mask of the same size. The mask is a binary image with each pixel labeled with an integer from 0 to C-1.\n", + "\n", + "3. **Pixel-wise Regression**:\n", + " - **Sample**: A 2-element tuple consisting of an image of size CxHxW and a target of the same size. The target is a real-valued image with each pixel labeled with a real number.\n", + "\n", + "4. **Image Domain Adaptation**:\n", + " - **Sample**: A 3-element tuple consisting of an image of size CxHxW, a label (an integer from 0 to C-1), and a domain label (an integer from 0 to D-1, where D is the number of domains).\n", + "\n", + "5. **Unsupervised Image Domain Adaptation**:\n", + " - **Sample**: A 2-element tuple consisting of an image of size CxHxW and a domain label (an integer from 0 to D-1).\n", + "\n", + "6. **Image Reconstruction**:\n", + " - **Sample**: A single unit of data that is an image of size CxHxW.\n", + "\n", + "7. **Human Activity Recognition (HAR) using Inertial Sensors**:\n", + " - **Sample**: A 2-element tuple consisting of an n-dimensional array of time series of sensor readings and a label (an integer from 0 to C-1).\n", + "\n", + "8. **Dimensionality Reduction using HAR Datasets**:\n", + " - **Sample**: A single unit of data that is an n-dimensional array of time series of sensor readings.\n", + "\n", + "In a map-style dataset, the dataset is considered a vector of samples, where each sample is a tuple of units of data. The dataset can be accessed by an index ranging from 0 to the length of the dataset.\n", + "\n", + "We observe that many tasks have a similar sample structure:\n", + "- **2-Element Tuple**: Common in image classification, unsupervised image domain adaptation, and HAR using inertial sensors. The first element varies depending on the task (e.g., image, time series), while the second element is usually an integer label. Optionally, a third element can be added as an integer domain label. Image segmentation and Pixel-wise regression also use this structure, however with different data types (binary, real-valued).\n", + "- **Dimensionality Reduction and Image Reconstruction**: Typically a single element that is the data.\n", + "\n", + "## How a `Dataset` Class Works\n", + "\n", + "The `Dataset` class is responsible for:\n", + "- Loading units of data from the storage device.\n", + "- Transforming and preprocessing the units of data, separately or together.\n", + "- Composing a sample from the units of data (usually a tuple but can be a single unit or a dictionary).\n", + "- Returning the sample at index `i` from the dataset via the `__getitem__` method, ensuring consistency (sample `i` must always be the same unless the dataset is modified).\n", + "\n", + "To implement a new dataset, PyTorch suggests using an abstract class called `torch.utils.data.Dataset`, which defines a [dataset mapping indices to samples](https://pytorch.org/docs/stable/data.html#map-style-datasets) and requires the implementation of two methods: `__len__` and `__getitem__`. The `__len__` method should return the size of the dataset, and the `__getitem__` method should return the sample at index `i`.\n", + "\n", + "### Examples of Dataset Implementations\n", + "\n", + "The implementation of a dataset mainly depends on the task and how the data is stored.\n", + "\n", + "1. **Task Definition**: The task typically defines the sample structure (single-element, 2-element tuple, 3-element tuple, dictionary, etc.) and the units of data (image, time series, etc.). The model is designed to work with a specific sample structure.\n", + "2. **Data Storage**: Data can be stored in different formats (TIFF, CSV, PNG, JPG, numpy arrays, etc.) and organizations (single file, directory, list of files, list of directories, list of URLs, etc.).\n", + "\n", + "#### Image Segmentation Example\n", + "\n", + "**Data Organization 1**:\n", + "- **Structure**: Images in a directory called `images` and masks in `masks`. Image `images/1.tiff` corresponds to mask `masks/1.png`.\n", + "- **Fetching Algorithm**:\n", + " 1. Load image from `images/i.tiff`.\n", + " 2. Load mask from `masks/i.png`.\n", + " 3. Apply transformations (e.g., convert mask to integer array, normalize image).\n", + " 4. Return the tuple `(image, mask)`.\n", + "\n", + "**Data Organization 2**:\n", + "- **Structure**: Images and masks in a single directory with TIFF files. Images prefixed with `image_` and masks with `mask_`. Image `image_1.tiff` corresponds to mask `mask_1.tiff`.\n", + "- **Fetching Algorithm**:\n", + " 1. Load image from `image_i.tiff`.\n", + " 2. Load mask from `mask_i.tiff`.\n", + " 3. Apply transformations.\n", + " 4. Return the tuple `(image, mask)`.\n", + "\n", + "#### Human Activity Recognition using Inertial Sensors Example\n", + "\n", + "**Data Organization 1**:\n", + "- **Structure**: Data in a CSV file with columns for sensor readings and label.\n", + "- **Fetching Algorithm**:\n", + " 1. Load the `i`-th row from the CSV file.\n", + " 2. Extract sensor readings and label.\n", + " 3. Apply transformations (e.g., normalize sensor readings).\n", + " 4. Return the tuple `(sensor_readings, label)`.\n", + "\n", + "**Data Organization 2**:\n", + "- **Structure**: Data in a directory with npy files for time-series readings and a `label.csv` file mapping series to labels.\n", + "- **Fetching Algorithm**:\n", + " 1. Load the `i`-th npy file.\n", + " 2. Extract the label from `label.csv`.\n", + " 3. Apply transformations.\n", + " 4. Return the tuple `(sensor_readings, label)`.\n", + "\n", + "### Pros and Cons\n", + "\n", + "- **Use Case 1 of HAR**: The dataset can make a single read of the `i`-th row and extract sensor readings and label.\n", + "- **Data Organization Changes**: Every time the data organization changes, the dataset must be modified, even if the sample structure remains the same.\n", + "\n", + "## Our Solution: Readers\n", + "\n", + "Readers are classes responsible for loading units of data in a predefined order. They load the `i`-th sample and extract the corresponding units of data (e.g., read the `i`-th row of a CSV file).\n", + "\n", + "Datasets, in turn, query the reader for the units of data, transform and preprocess the data, compose the sample, and return the sample at index `i`.\n", + "\n", + "By separating the responsibilities of loading data (Readers) and transforming/returning data (Datasets), we achieve a more flexible and generic implementation.\n", + "\n", + "---\n", + "\n", + "## Key Definitions\n", + "\n", + "1. **Unit of Data**: A single piece of data.\n", + "2. **Reader**: A class responsible for loading data units in a predefined order.\n", + "3. **Sample**: A collection of related data units.\n", + "4. **Dataset**: A collection of samples.\n", + "5. **Dataset Class**: Responsible for querying the reader, transforming/preprocessing data, and returning samples.\n", + "6. **Data Module**: A structured approach to handling different tasks and data organizations.\n", + "\n", + "This structured approach ensures clarity and flexibility in handling different tasks and data organizations." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/ssl_tools.analysis.rst b/docs/ssl_tools.analysis.rst deleted file mode 100644 index 9bf3480..0000000 --- a/docs/ssl_tools.analysis.rst +++ /dev/null @@ -1,21 +0,0 @@ -ssl\_tools.analysis package -=========================== - -Submodules ----------- - -ssl\_tools.analysis.plot\_metrics module ----------------------------------------- - -.. automodule:: ssl_tools.analysis.plot_metrics - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: ssl_tools.analysis - :members: - :undoc-members: - :show-inheritance: diff --git a/minerva/data/readers/tiff_reader.py b/minerva/data/readers/tiff_reader.py index 8459841..dc2b780 100644 --- a/minerva/data/readers/tiff_reader.py +++ b/minerva/data/readers/tiff_reader.py @@ -27,7 +27,9 @@ def __init__(self, path: str): self.path = Path(path) if not self.path.is_dir(): raise ValueError(f"Path {path} is not a directory") - self.files = list(sorted(self.path.rglob("*.tif"))) + self.files = list(sorted(self.path.rglob("*.tif"))) + list( + sorted(self.path.rglob("*.tiff")) + ) def __getitem__(self, index: Union[int, slice]) -> np.ndarray: """Retrieve the TIFF file at the specified index. The index will be diff --git a/minerva/transforms/transform.py b/minerva/transforms/transform.py index fb8eda0..cb58bda 100644 --- a/minerva/transforms/transform.py +++ b/minerva/transforms/transform.py @@ -174,3 +174,22 @@ def __init__(self, dtype: type | str): def __call__(self, x: np.ndarray) -> np.ndarray: """Cast the input data to the specified data type.""" return x.astype(self.dtype) + + +class Padding(_Transform): + def __init__(self, target_h_size: int, target_w_size: int): + self.target_h_size = target_h_size + self.target_w_size = target_w_size + + def __call__(self, x: np.ndarray) -> np.ndarray: + h, w = x.shape[:2] + pad_h = max(0, self.target_h_size - h) + pad_w = max(0, self.target_w_size - w) + if len(x.shape) == 2: + padded = np.pad(x, ((0, pad_h), (0, pad_w)), mode="reflect") + padded = np.expand_dims(padded, axis=2) + else: + padded = np.pad(x, ((0, pad_h), (0, pad_w), (0, 0)), mode="reflect") + + padded = np.transpose(padded, (2, 0, 1)) + return padded \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 796935b..4248306 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ authors = [ { name = "Gabriel Gutierrez", email = "gabriel.bgs00@gmail.com" }, { name = "Otavio Napoli", email = "onapoli@lmcad.ic.unicamp.br" }, { name = "Fernando Gubitoso Marques", email = "fernandogubitosom@gmail.com" }, - { name = "Edson Borin", email = "borin@unicamp.br " }, + { name = "Edson Borin", email = "borin@unicamp.br" }, ] classifiers = [ @@ -15,14 +15,13 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", ] -description = "the framework for traning machine learning models for reaserchers." +description = "A framework for traning machine learning models for reaserchers." keywords = [ - "Machine Learning", - "self supervised learning", - "Reaserch", "Deep Learning", - "Computer Vision", + "Machine Learning", "Pytorch", + "Reaserch", + "Self Supervised Learning", "SSL", ] license = { file = "LICENSE" } @@ -31,25 +30,24 @@ readme = "README.md" requires-python = ">=3.8" version = "0.2.2-beta" - dependencies = [ - "lightning", - "numpy", - "pandas", - "plotly", - "PyYAML", - "scipy", - "statsmodels", - "tifffile", - "torch", - "torchvision", - "timm", - "zarr", - "torchmetrics", - "rich", - "perlin-noise", "gitpython", - "jsonargparse" + "jsonargparse>=4.27", + "lightning>=2.1.9", + "numpy>=1.23.5", + "pandas>=2.2.2", + "perlin-noise>=1.12", + "plotly>=5.18", + "PyYAML>=6.0", + "rich>=13.0", + "scipy>=1.10", + "statsmodels>=0.13", + "tifffile>=2024", + "timm>=0.9", + "torch>=2.0.8", + "torchmetrics>=1.3.0", + "torchvision>=0.15", + "zarr>=2.17" ] [tool.setuptools] @@ -57,6 +55,16 @@ packages = ["minerva"] [project.optional-dependencies] dev = ["mock", "pytest", "black", "isort"] +docs = [ + "Ipython", + "nbsphinx", + "pandoc", + "sphinx", + "sphinx-rtd-theme", + "sphinx-autodoc-typehints", + "sphinx-argparse", + "sphinx-autoapi" +] [project.urls] "Bug Tracker" = "https://github.com/discovery-unicamp/Minerva/issues" From 97edeedf5af290b3254049406782c5a57ecb66b0 Mon Sep 17 00:00:00 2001 From: Otavio Napoli Date: Wed, 29 May 2024 02:52:00 +0000 Subject: [PATCH 03/11] Added models and README --- minerva/models/README.md | 24 ++ minerva/models/layers/__init__.py | 0 minerva/models/layers/gru.py | 111 ++++++++ minerva/models/nets/cnn_ha_etal.py | 300 ++++++++++++++++++++++ minerva/models/nets/cnn_pf.py | 299 +++++++++++++++++++++ minerva/models/nets/deep_conv_lstm.py | 165 ++++++++++++ minerva/models/nets/imu_transformer.py | 291 +++++++++++++++++++++ minerva/models/nets/inception_time.py | 314 +++++++++++++++++++++++ minerva/models/nets/multi_channel_cnn.py | 164 ++++++++++++ minerva/models/nets/resnet_1d.py | 303 ++++++++++++++++++++++ 10 files changed, 1971 insertions(+) create mode 100644 minerva/models/README.md create mode 100644 minerva/models/layers/__init__.py create mode 100644 minerva/models/layers/gru.py create mode 100644 minerva/models/nets/cnn_ha_etal.py create mode 100644 minerva/models/nets/cnn_pf.py create mode 100644 minerva/models/nets/deep_conv_lstm.py create mode 100644 minerva/models/nets/imu_transformer.py create mode 100644 minerva/models/nets/inception_time.py create mode 100644 minerva/models/nets/multi_channel_cnn.py create mode 100644 minerva/models/nets/resnet_1d.py diff --git a/minerva/models/README.md b/minerva/models/README.md new file mode 100644 index 0000000..35752cb --- /dev/null +++ b/minerva/models/README.md @@ -0,0 +1,24 @@ + +# Models + +| **Model** | **Authors** | **Task** | **Type** | **Input Shape** | **Python Class** | **Observations** | +|------------------------------------------------------------------------------------ |---------------------------------- |---------------- |----------------------- |:---------------: |:-----------------------------------------------------------: |----------------------------------------------------------------------------------------------------------------------------- | +| [DeepConvLSTM](https://www.mdpi.com/1424-8220/16/1/115) | Ordóñez and Roggen | Classification | 2D Conv + LSTM | (C, S, T) | minerva.models.nets.deep_conv_lstm.DeepConvLSTM | | +| [Simple 1D Convolutional Network](https://www.mdpi.com/1424-8220/16/1/115) | Ordóñez and Roggen | Classification | 1D Conv | (S, T) | minerva.models.nets.convnet.Simple1DConvNetwork | 1D Variant of "Baseline CNN", used by Ordóñez and Roggen, with dropout layers included. | +| [Simple 2D Convolutional Network](https://www.mdpi.com/1424-8220/16/1/115) | Ordóñez and Roggen | Classification | 2D Conv | (C, S, T) | minerva.models.nets.convnet.Simple2DConvNetwork | 2D Variant of "Baseline CNN", used by Ordóñez and Roggen, with dropout layers included. | +| [CNN_HaEtAl_1D](https://ieeexplore.ieee.org/document/7379657) | Ha, Yun and Choi | Classification | 1D Conv | (S, T) | minerva.models.nets.cnn_ha_etal.CNN_HaEtAl_1D | 1D proposed variant. | +| [CNN_HaEtAl_2D](https://ieeexplore.ieee.org/document/7379657) | Ha, Yun and Choi | Classification | 2D Conv | (C, S, T) | minerva.models.nets.cnn_ha_etal.CNN_HaEtAl_2D | 2D proposed variant. | +| [CNN PF](https://ieeexplore.ieee.org/document/7727224) | Ha and Choi | Classification | 2D Conv | (C, S, T) | minerva.models.nets.cnn_pf.CNN_PF_2D | Partial weight sharing in first convolutional layer and full weight sharing in second convolutional layer. | +| [CNN PPF](https://ieeexplore.ieee.org/document/7727224) | Ha and Choi | Classification | 2D Conv | (C, S, T) | minerva.models.nets.cnn_pf.CNN_PFF_2D | Partial and full weight sharing in first convolutional layer and full weight sharing in second convolutional layer. | +| [IMU Transformer](https://ieeexplore.ieee.org/document/9393889) | Shavit and Klein | Classification | 1D Conv + Transformer | (S, T) | minerva.models.nets.imu_transformer.IMUTransformerEncoder | | +| [IMU CNN](https://ieeexplore.ieee.org/document/9393889) | Shavit and Klein | Classification | 1D Conv | (S, T) | minerva.models.nets.imu_transformer.IMUCNN | Baseline CNN for IMUTransnformer work. | +| [InceptionTime](https://doi.org/10.1007/s10618-020-00710-y) | Fawaz et al. | Classification | 1D Conv | (S, T) | minerva.models.nets.inception_time.InceptionTime | | +| [1D-ResNet](https://www.mdpi.com/1424-8220/22/8/3094) | Mekruksavanich and Jitpattanakul | Classification | 1D Conv | (S, T) | minerva.models.nets.resnet_1d.ResNet1D_8 | Baseline resnet from paper. Uses ELU and 8 residual blocks | +| [1D-ResNet-SE-8](https://www.mdpi.com/1424-8220/22/8/3094) | Mekruksavanich and Jitpattanakul | Classification | 1D Conv | (S, T) | minerva.models.nets.resnet_1d.ResNetSE1D_8 | ResNet with Squeeze and Excitation. Uses ELU and 8 residual blocks | +| [1D-ResNet-SE-5](https://ieeexplore.ieee.org/document/9771436) | Mekruksavanich et al. | Classification | 1D Conv | (S, T) | minerva.models.nets.resnet_1d.ResNetSE1D_5 | ResNet with Squeeze and Excitation. Uses ReLU and 8 residual blocks | +| [MCNN](https://ieeexplore.ieee.org/document/8975649) | Sikder et al. | Classification | 2D Conv | (2, C, S,T) | minerva.models.nets.multi_channel_cnn.MultiChannelCNN_HAR | First dimension is FFT data and second is Welch Power Density periodgram data. Must adapt dataset to return data like this. | + + +# SSL Models + +... \ No newline at end of file diff --git a/minerva/models/layers/__init__.py b/minerva/models/layers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/minerva/models/layers/gru.py b/minerva/models/layers/gru.py new file mode 100644 index 0000000..e0d40bb --- /dev/null +++ b/minerva/models/layers/gru.py @@ -0,0 +1,111 @@ +import torch + + +class GRUEncoder(torch.nn.Module): + def __init__( + self, + hidden_size: int = 100, + in_channels: int = 6, + encoding_size: int = 10, + num_layers: int = 1, + dropout: float = 0.0, + bidirectional: bool = True, + ): + """Gate Recurrent Unit (GRU) Encoder. + This class is a wrapper for the GRU layer (torch.nn.GRU) followed by a + linear layer, in order to obtain a fixed-size encoding of the input + sequence. + + The input sequence is expected to be of shape + [batch_size, in_channel, seq_len]. + For instance, for HAR data in MotionSense Dataset: + - in_channel = 6 (3 for accelerometer and 3 for gyroscope); and + - seq_len = 60 (the number of time steps). + + In forward pass, the input sequence is permuted to + [seq_len, batch_size, in_channel] before being fed to the GRU layer. + The output of forward pass is the encoding of shape + [batch_size, encoding_size]. + + Parameters + ---------- + hidden_size : int, optional + The number of features in the hidden state of the GRU, + by default 100 + in_channel: int, optional + The number of input features (e.g. 6 for HAR data in MotionSense + Dataset), by default 6 + encoding_size : int, optional + Size of the encoding (output of the linear layer). + num_layers : int, optional + Number of recurrent layers. E.g., setting ``num_layers=2`` + would mean stacking two GRUs together to form a `stacked GRU`, + with the second GRU taking in outputs of the first GRU and + computing the final results. By default 1 + dropout : float, optional + If non-zero, introduces a `Dropout` layer on the outputs of each + GRU layer except the last layer, with dropout probability equal to + :attr:`dropout`. Default: 0 + bidirectional : bool, optional + If ``True``, becomes a bidirectional GRU, by default True + """ + super().__init__() + + # Parameters + self.hidden_size = hidden_size + self.in_channel = in_channels + self.num_layers = num_layers + self.encoding_size = encoding_size + self.bidirectional = bidirectional + # If bidirectional is true, the number of directions is 2 + self.num_directions = 2 if bidirectional else 1 + + # Instantiate the GRU layer + self.rnn = torch.nn.GRU( + input_size=self.in_channel, + hidden_size=self.hidden_size, + num_layers=num_layers, + batch_first=False, + dropout=dropout, + bidirectional=bidirectional, + ) + + # Instantiate the linear leayer + # If bidirectional is true, the input of linear layer is + # hidden_size * 2 (because the output of GRU is concatenated) + # Otherwise, the input of linear layer is hidden_size + self.nn = torch.nn.Linear( + self.hidden_size * self.num_directions, self.encoding_size + ) + + def forward(self, x): + # Permute the input sequence from [batch_size, in_channel, seq_len] + # to [seq_len, batch_size, in_channel] + x = x.permute(2, 0, 1) + + # The initial hidden state (h0) is set to zeros of shape + # [num_layers * num_directions, batch_size, hidden_size] + initial_state = torch.zeros( + self.num_layers * self.num_directions, # initial_state.shape[0] + x.shape[1], # initial_state.shape[1] + self.hidden_size, # initial_state.shape[2] + device=x.device, + # requires_grad=False # This is not a learnable parameter + ) + + # Forward pass of the GRU layer + # out shape = [seq_len, batch_size, num_directions*hidden_size] + out, _ = self.rnn( + x, initial_state + ) + + # Pick the last state returned by the GRU of shape + # [batch_size, num_directions*hidden_size] and squeeze it (remove the + # first dimension if the size is 1) + out = out[-1].squeeze(0) + + # Pass the output of GRU to the linear layer to obtain the encodings + encodings = self.nn(out) + + # encodings shape = [batch_size, encoding_size] + return encodings diff --git a/minerva/models/nets/cnn_ha_etal.py b/minerva/models/nets/cnn_ha_etal.py new file mode 100644 index 0000000..9e6d88c --- /dev/null +++ b/minerva/models/nets/cnn_ha_etal.py @@ -0,0 +1,300 @@ +from typing import List, Tuple +import torch +from torchmetrics import Accuracy + +from minerva.models.nets.base import SimpleSupervisedModel + + +# Implementation of Multi-modal Convolutional Neural Networks for Activity +# Recognition, from Ha, Yu, and Choi. +# https://ieeexplore.ieee.org/document/7379657 + + +class ZeroPadder2D(torch.nn.Module): + def __init__(self, pad_at: List[int], padding_size: int): + super().__init__() + self.pad_at = pad_at + self.padding_size = padding_size + + def forward(self, x): + # X = (Batch, channels, H, W) + # X = (8, 1, 6, 60) + + for i in self.pad_at: + left = x[:, :, :i, :] + zeros = torch.zeros( + x.shape[0], x.shape[1], self.padding_size, x.shape[3] + ) + right = x[:, :, i:, :] + + x = torch.cat([left, zeros, right], dim=2) + # print(f"-- Left.shape: {left.shape}") + # print(f"-- Zeros.shape: {zeros.shape}") + # print(f"-- Right.shape: {right.shape}") + # print(f"-- X.shape: {x.shape}") + + return x + + def __str__(self) -> str: + return f"ZeroPadder2D(pad_at={self.pad_at}, padding_size={self.padding_size})" + + def __repr__(self) -> str: + return str(self) + + +class CNN_HaEtAl_1D(SimpleSupervisedModel): + def __init__( + self, + input_shape: Tuple[int, int, int] = (1, 6, 60), + num_classes: int = 6, + learning_rate: float = 1e-3, + ): + self.input_shape = input_shape + self.num_classes = num_classes + + backbone = self._create_backbone(input_shape=input_shape) + self.fc_input_channels = self._calculate_fc_input_features( + backbone, input_shape + ) + fc = self._create_fc(self.fc_input_channels, num_classes) + super().__init__( + backbone=backbone, + fc=fc, + learning_rate=learning_rate, + flatten=True, + loss_fn=torch.nn.CrossEntropyLoss(), + val_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + test_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + ) + + def _create_backbone(self, input_shape: Tuple[int, int]) -> torch.nn.Module: + return torch.nn.Sequential( + # Add padding + # ZeroPadder2D( + # pad_at=self.pad_at, + # padding_size=4 - 1, # kernel size - 1 + # ), + # First 2D convolutional layer + torch.nn.Conv2d( + in_channels=input_shape[0], + out_channels=32, + kernel_size=(1, 4), + stride=(1, 1), + ), + torch.nn.ReLU(), + torch.nn.MaxPool2d( + kernel_size=(1, 3), + stride=(1, 3), + ), + + # Second 2D convolutional layer + torch.nn.Conv2d( + in_channels=32, + out_channels=64, + kernel_size=(1, 5), + stride=(1, 1), + ), + torch.nn.ReLU(), + torch.nn.MaxPool2d( + kernel_size=(1, 3), + stride=(1, 3), + ), + ) + + def _calculate_fc_input_features( + self, backbone: torch.nn.Module, input_shape: Tuple[int, int, int] + ) -> int: + """Run a single forward pass with a random input to get the number of + features after the convolutional layers. + + Parameters + ---------- + backbone : torch.nn.Module + The backbone of the network + input_shape : Tuple[int, int, int] + The input shape of the network. + + Returns + ------- + int + The number of features after the convolutional layers. + """ + random_input = torch.randn(1, *input_shape) + with torch.no_grad(): + out = backbone(random_input) + return out.view(out.size(0), -1).size(1) + + def _create_fc( + self, input_features: int, num_classes: int + ) -> torch.nn.Module: + return torch.nn.Sequential( + torch.nn.Linear(in_features=input_features, out_features=128), + torch.nn.ReLU(), + torch.nn.Dropout(0.5), + torch.nn.Linear(in_features=128, out_features=num_classes), + # torch.nn.Softmax(dim=1), + ) + + +class CNN_HaEtAl_2D(SimpleSupervisedModel): + def __init__( + self, + pad_at: List[int]= (3, ), + input_shape: Tuple[int, int, int] = (1, 6, 60), + num_classes: int = 6, + learning_rate: float = 1e-3, + ): + self.pad_at = pad_at + self.input_shape = input_shape + self.num_classes = num_classes + + backbone = self._create_backbone(input_shape=input_shape) + self.fc_input_channels = self._calculate_fc_input_features( + backbone, input_shape + ) + fc = self._create_fc(self.fc_input_channels, num_classes) + super().__init__( + backbone=backbone, + fc=fc, + learning_rate=learning_rate, + flatten=True, + loss_fn=torch.nn.CrossEntropyLoss(), + val_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + test_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + ) + + def _create_backbone(self, input_shape: Tuple[int, int]) -> torch.nn.Module: + first_kernel_size = 4 + return torch.nn.Sequential( + # Add padding + ZeroPadder2D( + pad_at=self.pad_at, + padding_size=first_kernel_size - 1, # kernel size - 1 + ), + # First 2D convolutional layer + torch.nn.Conv2d( + in_channels=input_shape[0], + out_channels=32, + kernel_size=(first_kernel_size, first_kernel_size), + stride=(1, 1), + ), + torch.nn.ReLU(), + torch.nn.MaxPool2d( + kernel_size=(3, 3), + stride=(3, 3), + padding=1, + ), + + # Second 2D convolutional layer + torch.nn.Conv2d( + in_channels=32, + out_channels=64, + kernel_size=(5, 5), + stride=(1, 1), + padding=2, + ), + torch.nn.ReLU(), + torch.nn.MaxPool2d( + kernel_size=(3, 3), + stride=(3, 3), + padding=1, + ), + ) + + def _calculate_fc_input_features( + self, backbone: torch.nn.Module, input_shape: Tuple[int, int, int] + ) -> int: + """Run a single forward pass with a random input to get the number of + features after the convolutional layers. + + Parameters + ---------- + backbone : torch.nn.Module + The backbone of the network + input_shape : Tuple[int, int, int] + The input shape of the network. + + Returns + ------- + int + The number of features after the convolutional layers. + """ + random_input = torch.randn(1, *input_shape) + with torch.no_grad(): + out = backbone(random_input) + return out.view(out.size(0), -1).size(1) + + def _create_fc( + self, input_features: int, num_classes: int + ) -> torch.nn.Module: + return torch.nn.Sequential( + torch.nn.Linear(in_features=input_features, out_features=128), + torch.nn.ReLU(), + torch.nn.Dropout(0.5), + torch.nn.Linear(in_features=128, out_features=num_classes), + # torch.nn.Softmax(dim=1), + ) + + + +# def test_cnn_1d(): +# input_shape = (1, 6, 60) + +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) + +# model = CNN_HaEtAl_1D( +# input_shape=input_shape, num_classes=6, learning_rate=1e-3 +# ) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) + +# trainer.fit(model, datamodule=data_module) + + +# def test_cnn_2d(): +# input_shape = (1, 6, 60) + +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) + +# model = CNN_HaEtAl_2D( +# pad_at=[3], input_shape=input_shape, num_classes=6, learning_rate=1e-3 +# ) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) + +# trainer.fit(model, datamodule=data_module) + + +# if __name__ == "__main__": +# import logging +# logging.getLogger("lightning.pytorch").setLevel(logging.ERROR) +# logging.getLogger("lightning").setLevel(logging.ERROR) +# logging.getLogger("lightning.pytorch.core").setLevel(logging.ERROR) + + +# test_cnn_1d() +# test_cnn_2d() diff --git a/minerva/models/nets/cnn_pf.py b/minerva/models/nets/cnn_pf.py new file mode 100644 index 0000000..dffb638 --- /dev/null +++ b/minerva/models/nets/cnn_pf.py @@ -0,0 +1,299 @@ +from typing import List, Tuple +import torch +from torchmetrics import Accuracy + +from minerva.models.nets.base import SimpleSupervisedModel + +# Convolutional Neural Networks for Human Activity Recognition using Multiple +# Accelerometer and Gyroscope Sensors, from Ha, and Choi. +# https://ieeexplore.ieee.org/document/7727224 + +class ZeroPadder2D(torch.nn.Module): + def __init__(self, pad_at: List[int], padding_size: int): + super().__init__() + self.pad_at = pad_at + self.padding_size = padding_size + + def forward(self, x): + # X = (Batch, channels, H, W) + # X = (8, 1, 6, 60) + + for i in self.pad_at: + left = x[:, :, :i, :] + zeros = torch.zeros( + x.shape[0], x.shape[1], self.padding_size, x.shape[3] + ) + right = x[:, :, i:, :] + + x = torch.cat([left, zeros, right], dim=2) + # print(f"-- Left.shape: {left.shape}") + # print(f"-- Zeros.shape: {zeros.shape}") + # print(f"-- Right.shape: {right.shape}") + # print(f"-- X.shape: {x.shape}") + + return x + + def __str__(self) -> str: + return f"ZeroPadder2D(pad_at={self.pad_at}, padding_size={self.padding_size})" + + def __repr__(self) -> str: + return str(self) + + +class CNN_PF_Backbone(torch.nn.Module): + def __init__( + self, + pad_at: int, + input_shape: Tuple[int, int, int], + out_channels: int = 16, + include_middle: bool = False, + ): + super().__init__() + self.pad_at = pad_at + self.input_shape = input_shape + self.include_middle = include_middle + self.out_channels = out_channels + self.first_pad_size = 3 - 1 # kernel -1 + + self.first_padder = ZeroPadder2D( + pad_at=(pad_at,), + padding_size=self.first_pad_size, + ) + + self.upper_part = torch.nn.Sequential( + torch.nn.Conv2d( + in_channels=self.input_shape[0], + out_channels=self.out_channels, + kernel_size=(3, 3), + stride=(1, 1), + ), + torch.nn.ReLU(), + torch.nn.MaxPool2d( + kernel_size=(2, 3), + stride=(2, 3), + padding=1, + ), + ) + + self.lower_part = torch.nn.Sequential( + torch.nn.Conv2d( + in_channels=self.input_shape[0], + out_channels=self.out_channels, + kernel_size=(3, 3), + stride=(1, 1), + ), + torch.nn.ReLU(), + torch.nn.MaxPool2d( + kernel_size=(2, 3), + stride=(2, 3), + padding=1, + ), + ) + + if self.include_middle: + self.middle_part = torch.nn.Sequential( + torch.nn.Conv2d( + in_channels=self.input_shape[0], + out_channels=self.out_channels, + kernel_size=(3, 3), + stride=(1, 1), + ), + torch.nn.ReLU(), + torch.nn.MaxPool2d( + kernel_size=(2, 3), + stride=(2, 3), + padding=1, + ), + ) + + self.shared_part = torch.nn.Sequential( + torch.nn.Conv2d( + in_channels=( + self.out_channels * 3 + if self.include_middle + else self.out_channels * 2 + ), + out_channels=64, + kernel_size=(3, 5), + stride=(1, 1), + ), + torch.nn.ReLU(), + torch.nn.MaxPool2d( + kernel_size=(2, 3), + stride=(2, 3), + padding=1, + ), + ) + + def forward(self, x): + # X = (batch_size, channels, sensors, time_steps) + # X = (8, 1, 6, 60) + + # After pad: (8, 1, 8, 60) + x = self.first_padder(x) + + # upper slice (8, 1, 5, 60) + upper_x = x[:, :, : self.pad_at + self.first_pad_size, :] + upper_x = self.upper_part(upper_x) + zeros_1 = torch.zeros( + upper_x.size(0), upper_x.size(1), 3 - 1, upper_x.size(3) + ) + + upper_x = torch.cat( + [upper_x, zeros_1], + dim=2, + ) + + # lower slice (8, 1, 5, 60) + lower_x = x[:, :, self.pad_at :, :] + lower_x = self.lower_part(lower_x) + zeros_2 = torch.zeros( + lower_x.size(0), lower_x.size(1), 3 - 1, lower_x.size(3) + ) + + lower_x = torch.cat( + [zeros_2, lower_x], + dim=2, + ) + + if self.include_middle: + # x is already middle + middle_x = self.middle_part(x) + concatenated_x = torch.cat([upper_x, middle_x, lower_x], dim=1) + + else: + concatenated_x = torch.cat([upper_x, lower_x], dim=1) + + result_x = self.shared_part(concatenated_x) + return result_x + + +class CNN_PF_2D(SimpleSupervisedModel): + def __init__( + self, + pad_at: int, + input_shape: Tuple[int, int, int] = (1, 6, 60), + out_channels: int = 16, + num_classes: int = 6, + learning_rate: float = 1e-3, + include_middle: bool = False, + ): + self.pad_at = pad_at + self.input_shape = input_shape + self.out_channels = out_channels + self.num_classes = num_classes + + backbone = CNN_PF_Backbone( + pad_at=pad_at, + input_shape=input_shape, + out_channels=out_channels, + include_middle=include_middle, + ) + self.fc_input_channels = self._calculate_fc_input_features( + backbone, input_shape + ) + fc = self._create_fc(self.fc_input_channels, num_classes) + super().__init__( + backbone=backbone, + fc=fc, + learning_rate=learning_rate, + flatten=True, + loss_fn=torch.nn.CrossEntropyLoss(), + val_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + test_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + ) + + def _calculate_fc_input_features( + self, backbone: torch.nn.Module, input_shape: Tuple[int, int, int] + ) -> int: + """Run a single forward pass with a random input to get the number of + features after the convolutional layers. + + Parameters + ---------- + backbone : torch.nn.Module + The backbone of the network + input_shape : Tuple[int, int, int] + The input shape of the network. + + Returns + ------- + int + The number of features after the convolutional layers. + """ + random_input = torch.randn(1, *input_shape) + with torch.no_grad(): + out = backbone(random_input) + return out.view(out.size(0), -1).size(1) + + def _create_fc( + self, input_features: int, num_classes: int + ) -> torch.nn.Module: + return torch.nn.Sequential( + torch.nn.Linear(in_features=input_features, out_features=512), + torch.nn.ReLU(), + torch.nn.Dropout(0.5), + torch.nn.Linear(in_features=512, out_features=num_classes), + # torch.nn.Softmax(dim=1), + ) + + +class CNN_PFF_2D(CNN_PF_2D): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, include_middle=True) + + +# def test_cnn_pf_2d(): +# input_shape = (1, 6, 60) + +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) + +# model = CNN_PF_2D(pad_at=3, input_shape=input_shape) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) + +# trainer.fit(model, datamodule=data_module) + + +# def test_cnn_pff_2d(): +# input_shape = (1, 6, 60) + +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) + +# model = CNN_PFF_2D(pad_at=3, input_shape=input_shape) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) + +# trainer.fit(model, datamodule=data_module) + + +# if __name__ == "__main__": +# import logging + +# logging.getLogger("lightning.pytorch").setLevel(logging.ERROR) +# logging.getLogger("lightning").setLevel(logging.ERROR) +# logging.getLogger("lightning.pytorch.core").setLevel(logging.ERROR) + +# # test_cnn_1d() +# test_cnn_pf_2d() +# test_cnn_pff_2d() diff --git a/minerva/models/nets/deep_conv_lstm.py b/minerva/models/nets/deep_conv_lstm.py new file mode 100644 index 0000000..45eda29 --- /dev/null +++ b/minerva/models/nets/deep_conv_lstm.py @@ -0,0 +1,165 @@ +from typing import Tuple +import torch +from torchmetrics import Accuracy + + +from minerva.models.nets.base import SimpleSupervisedModel + + +# Implementation of DeepConvLSTM as described in the paper: +# Deep Convolutional and LSTM Recurrent Neural Networks for Multimodal Wearable +# Activity Recognition (http://www.mdpi.com/1424-8220/16/1/115/html) + + +class ConvLSTMCell(torch.nn.Module): + def __init__(self, input_shape: tuple): + super().__init__() + self.input_shape = input_shape + self.conv_block = torch.nn.Sequential( + torch.nn.Conv2d( + in_channels=input_shape[0], + out_channels=64, + kernel_size=(1, 5), + stride=(1, 1), + ), + torch.nn.Conv2d( + in_channels=64, + out_channels=64, + kernel_size=(1, 5), + stride=(1, 1), + ), + torch.nn.Conv2d( + in_channels=64, + out_channels=64, + kernel_size=(1, 5), + stride=(1, 1), + ), + torch.nn.Conv2d( + in_channels=64, + out_channels=64, + kernel_size=(1, 5), + stride=(1, 1), + ), + ) + + self.lstm_input_size = self._calculate_conv_output_shape( + self.conv_block, input_shape + ) + + self.lstm_1 = torch.nn.LSTM( + input_size=self.lstm_input_size, hidden_size=128, batch_first=True + ) + self.lstm_2 = torch.nn.LSTM( + input_size=128, hidden_size=128, batch_first=True + ) + + def _calculate_conv_output_shape( + self, backbone, input_shape: Tuple[int, int, int] + ) -> int: + random_input = torch.randn(1, *input_shape) + with torch.no_grad(): + out = backbone(random_input) + return out.view(out.size(0), -1).size(1) + + def forward(self, x): + x = self.conv_block(x) + x = x.reshape(x.size(0), x.size(1), -1) + x, _ = self.lstm_1(x) + x, _ = self.lstm_2(x) + return x + + +class DeepConvLSTM(SimpleSupervisedModel): + def __init__( + self, + input_shape: Tuple[int, int, int] = (1, 6, 60), + num_classes: int = 6, + learning_rate: float = 1e-3, + ): + self.input_shape = input_shape + self.num_classes = num_classes + + backbone = self._create_backbone(input_shape=input_shape) + self.fc_input_channels = self._calculate_fc_input_features( + backbone, input_shape + ) + fc = self._create_fc(self.fc_input_channels, num_classes) + super().__init__( + backbone=backbone, + fc=fc, + learning_rate=learning_rate, + flatten=True, + loss_fn=torch.nn.CrossEntropyLoss(), + # val_metrics={ + # "acc": Accuracy(task="multiclass", num_classes=num_classes) + # }, + # test_metrics={ + # "acc": Accuracy(task="multiclass", num_classes=num_classes) + # }, + ) + + def _create_backbone(self, input_shape: Tuple[int, int]) -> torch.nn.Module: + return ConvLSTMCell(input_shape=input_shape) + + def _calculate_fc_input_features( + self, backbone: torch.nn.Module, input_shape: Tuple[int, int, int] + ) -> int: + """Run a single forward pass with a random input to get the number of + features after the convolutional layers. + + Parameters + ---------- + backbone : torch.nn.Module + The backbone of the network + input_shape : Tuple[int, int, int] + The input shape of the network. + + Returns + ------- + int + The number of features after the convolutional layers. + """ + random_input = torch.randn(1, *input_shape) + with torch.no_grad(): + out = backbone(random_input) + return out.view(out.size(0), -1).size(1) + + def _create_fc( + self, input_features: int, num_classes: int + ) -> torch.nn.Module: + return torch.nn.Sequential( + torch.nn.Linear( + in_features=input_features, out_features=num_classes + ), + torch.nn.ReLU(), + ) + + +# def test_deep_conv_lstm(): +# input_shape = (1, 6, 60) + +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) + +# model = DeepConvLSTM( +# input_shape=input_shape, num_classes=6, learning_rate=1e-3 +# ) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) + +# trainer.fit(model, datamodule=data_module) + + +# if __name__ == "__main__": +# import logging +# logging.getLogger("lightning.pytorch").setLevel(logging.ERROR) +# logging.getLogger("lightning").setLevel(logging.ERROR) +# logging.getLogger("lightning.pytorch.core").setLevel(logging.ERROR) +# test_deep_conv_lstm() diff --git a/minerva/models/nets/imu_transformer.py b/minerva/models/nets/imu_transformer.py new file mode 100644 index 0000000..cd84dc9 --- /dev/null +++ b/minerva/models/nets/imu_transformer.py @@ -0,0 +1,291 @@ +from typing import Tuple +import torch +from torch import nn +from torch.nn import TransformerEncoder, TransformerEncoderLayer +from minerva.models.nets.base import SimpleSupervisedModel +import lightning as L + + +""" +IMUTransformerEncoder model +""" + + +class _IMUTransformerEncoder(nn.Module): + + def __init__( + self, + input_shape: tuple = (6, 60), + transformer_dim: int = 64, + encode_position: bool = True, + nhead: int = 8, + dim_feedforward: int = 128, + transformer_dropout: float = 0.1, + transformer_activation: str = "gelu", + num_encoder_layers: int = 6, + ): + """ + input_shape: (tuple) shape of the input data + transformer_dim: (int) dimension of the transformer + encode_position: (bool) whether to encode position or not + nhead: (int) number of attention heads + dim_feedforward: (int) dimension of the feedforward network + transformer_dropout: (float) dropout rate for the transformer + transformer_activation: (str) activation function for the transformer + num_encoder_layers: (int) number of transformer encoder layers + num_classes: (int) number of output classes + """ + super().__init__() + + self.input_shape = input_shape + self.transformer_dim = transformer_dim + + self.input_proj = nn.Sequential( + nn.Conv1d(input_shape[0], self.transformer_dim, 1), + nn.GELU(), + nn.Conv1d(self.transformer_dim, self.transformer_dim, 1), + nn.GELU(), + nn.Conv1d(self.transformer_dim, self.transformer_dim, 1), + nn.GELU(), + nn.Conv1d(self.transformer_dim, self.transformer_dim, 1), + nn.GELU(), + ) + + self.encode_position = encode_position + encoder_layer = TransformerEncoderLayer( + d_model=self.transformer_dim, + nhead=nhead, + dim_feedforward=dim_feedforward, + dropout=transformer_dropout, + activation=transformer_activation, + ) + + self.transformer_encoder = TransformerEncoder( + encoder_layer, + num_layers=num_encoder_layers, + norm=nn.LayerNorm(self.transformer_dim), + ) + self.cls_token = nn.Parameter( + torch.zeros((1, self.transformer_dim)), requires_grad=True + ) + + if self.encode_position: + self.position_embed = nn.Parameter( + torch.randn(input_shape[1] + 1, 1, self.transformer_dim) + ) + + # init + for p in self.parameters(): + if p.dim() > 1: + nn.init.xavier_uniform_(p) + + def forward(self, x): + """Forward + + Parameters + ---------- + x : _type_ + A tensor of shape (B, C, S) with B = batch size, C = channels, S = sequence length + + """ + + # Embed in a high dimensional space and reshape to Transformer's expected shape + x = self.input_proj(x) + # print(f"src.shape: {src.shape}") + x = x.permute(2, 0, 1) + + # Prepend class token + cls_token = self.cls_token.unsqueeze(1).repeat(1, x.shape[1], 1) + x = torch.cat([cls_token, x]) + + # Add the position embedding + if self.encode_position: + x += self.position_embed + + # Transformer Encoder pass + target = self.transformer_encoder(x)[0] + + return target + + +class IMUTransformerEncoder(SimpleSupervisedModel): + def __init__( + self, + input_shape: tuple = (6, 60), + transformer_dim: int = 64, + encode_position: bool = True, + nhead: int = 8, + dim_feedforward: int = 128, + transformer_dropout: float = 0.1, + transformer_activation: str = "gelu", + num_encoder_layers: int = 6, + num_classes: int = 6, + learning_rate: float = 1e-3, + ): + self.input_shape = input_shape + + backbone = self._create_backbone( + input_shape=input_shape, + transformer_dim=transformer_dim, + encode_position=encode_position, + nhead=nhead, + dim_feedforward=dim_feedforward, + transformer_dropout=transformer_dropout, + transformer_activation=transformer_activation, + num_encoder_layers=num_encoder_layers, + ) + + fc = self._create_fc(transformer_dim, num_classes) + + super().__init__( + backbone=backbone, + fc=fc, + learning_rate=learning_rate, + loss_fn=torch.nn.CrossEntropyLoss(), + ) + + def _create_backbone( + self, + input_shape, + transformer_dim, + encode_position, + nhead, + dim_feedforward, + transformer_dropout, + transformer_activation, + num_encoder_layers, + ): + backbone = _IMUTransformerEncoder( + input_shape=input_shape, + transformer_dim=transformer_dim, + encode_position=encode_position, + nhead=nhead, + dim_feedforward=dim_feedforward, + transformer_dropout=transformer_dropout, + transformer_activation=transformer_activation, + num_encoder_layers=num_encoder_layers, + ) + return backbone + + def _create_fc(self, transform_dim, num_classes): + imu_head = nn.Sequential( + nn.LayerNorm(transform_dim), + nn.Linear(transform_dim, transform_dim // 4), + nn.GELU(), + nn.Dropout(0.1), + nn.Linear(transform_dim // 4, num_classes), + ) + return imu_head + + +class IMUCNN(SimpleSupervisedModel): + def __init__( + self, + input_shape: tuple = (6, 60), + hidden_dim: int = 64, + num_classes: int = 6, + dropout_factor: float = 0.1, + learning_rate: float = 1e-3, + ): + self.input_shape = input_shape + self.hidden_dim = hidden_dim + self.dropout_factor = dropout_factor + + backbone = self._create_backbone( + input_shape=input_shape, + hidden_dim=hidden_dim, + dropout_factor=dropout_factor, + ) + self.fc_input_channels = self._calculate_fc_input_features( + backbone, input_shape + ) + fc = self._create_fc(self.fc_input_channels, hidden_dim, num_classes) + + super().__init__( + backbone=backbone, + fc=fc, + learning_rate=learning_rate, + loss_fn=torch.nn.CrossEntropyLoss(), + flatten=True + ) + + def _create_backbone(self, input_shape, hidden_dim, dropout_factor): + return torch.nn.Sequential( + torch.nn.Conv1d(input_shape[0], hidden_dim, kernel_size=1), + torch.nn.ReLU(), + torch.nn.Conv1d(hidden_dim, hidden_dim, kernel_size=1), + torch.nn.ReLU(), + torch.nn.Dropout(dropout_factor), + torch.nn.MaxPool1d(kernel_size=2), + ) + + + def _calculate_fc_input_features( + self, backbone: torch.nn.Module, input_shape: Tuple[int, int] + ) -> int: + random_input = torch.randn(1, *input_shape) + with torch.no_grad(): + out = backbone(random_input) + return out.view(out.size(0), -1).size(1) + + + def _create_fc(self, input_features, hidden_dim, num_classes): + return torch.nn.Sequential( + torch.nn.Linear(input_features, hidden_dim), + torch.nn.ReLU(), + torch.nn.Linear(hidden_dim, num_classes), + ) + + +# def test_imu_transformer(): +# input_shape = (6, 60) + +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) + +# model = IMUTransformerEncoder( +# input_shape=input_shape, num_classes=6, learning_rate=1e-3 +# ) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) + +# trainer.fit(model, datamodule=data_module) + + +# def test_imu_cnn(): +# input_shape = (6, 60) + +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) + +# model = IMUCNN( +# input_shape=input_shape, num_classes=6, learning_rate=1e-3 +# ) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) + +# trainer.fit(model, datamodule=data_module) + + +# if __name__ == "__main__": +# import logging + +# logging.getLogger("lightning.pytorch").setLevel(logging.ERROR) +# logging.getLogger("lightning").setLevel(logging.ERROR) +# logging.getLogger("lightning.pytorch.core").setLevel(logging.ERROR) +# test_imu_transformer() +# test_imu_cnn() \ No newline at end of file diff --git a/minerva/models/nets/inception_time.py b/minerva/models/nets/inception_time.py new file mode 100644 index 0000000..e6e3bbb --- /dev/null +++ b/minerva/models/nets/inception_time.py @@ -0,0 +1,314 @@ +import numpy as np +import time + + +from typing import Tuple +import torch +from torch import nn +from torch.nn import TransformerEncoder, TransformerEncoderLayer +from torchmetrics import Accuracy +from minerva.models.nets.base import SimpleSupervisedModel +import lightning as L + + +class InceptionModule(torch.nn.Module): + def __init__( + self, + input_shape: Tuple[int, int] = (6, 60), + stride: int = 1, + kernel_size: int = 41, + nb_filters: int = 32, + use_bottleneck: bool = True, + bottleneck_size: int = 32, + ): + super().__init__() + self.input_shape = input_shape + self.stride = stride + self.kernel_size = kernel_size + self.nb_filters = nb_filters + self.use_bottleneck = use_bottleneck + self.bottleneck_size = bottleneck_size + + self.build_model() + + def build_model(self): + + # INPUT SHAPE is in format (S, T), time-len and number of sensors (ie, (6, 60)) + + ######################################################################## + if self.use_bottleneck and self.input_shape[0] > 1: + input_inception = torch.nn.Conv1d( + in_channels=self.input_shape[0], + out_channels=self.bottleneck_size, + kernel_size=1, + padding="same", + stride=1, + bias=False, + ) + + else: + input_inception = torch.nn.Identity() + + ######################################################################## + kernel_size_s = [self.kernel_size // (2**i) for i in range(3)] + + conv_list = [] + + for i in range(len(kernel_size_s)): + conv_list.append( + torch.nn.Conv1d( + in_channels=self.bottleneck_size, + out_channels=self.nb_filters, + kernel_size=kernel_size_s[i], + padding="same", + stride=self.stride, + bias=False, + ) + ) + + ######################################################################## + + self.max_pool_1 = torch.nn.MaxPool1d( + kernel_size=3, stride=self.stride, padding=1 + ) + + direct_conv = torch.nn.Conv1d( + in_channels=self.input_shape[0], + out_channels=self.nb_filters, + kernel_size=1, + padding="same", + stride=1, + bias=False, + ) + + self.input_inception = input_inception + self.conv_list = conv_list + self.direct_conv = direct_conv + self.batch_norm = torch.nn.BatchNorm1d(self.nb_filters * 4) + self.activation = torch.nn.ReLU() + + def forward(self, input_tensor): + + input_inception = self.input_inception(input_tensor) + results = [] + for conv in self.conv_list: + res = conv(input_inception) + results.append(res) + + res = self.max_pool_1(input_tensor) + res = self.direct_conv(res) + results.append(res) + + x = torch.cat(results, dim=1) + x = self.batch_norm(x) + x = self.activation(x) + return x + + +class ShortcutLayer(torch.nn.Module): + def __init__(self, input_tensor_shape, out_tensor_shape): + super().__init__() + self.input_tensor_shape = input_tensor_shape + self.out_tensor_shape = out_tensor_shape + self.conv = torch.nn.Conv1d( + in_channels=self.input_tensor_shape[0], + out_channels=self.out_tensor_shape[0], + kernel_size=1, + padding="same", + ) + self.batch_norm = torch.nn.BatchNorm1d(self.out_tensor_shape[0]) + + def forward(self, input_tensor, output_tensor): + shortcut_y = self.conv(input_tensor) + shortcut_y = self.batch_norm(shortcut_y) + x = torch.add(shortcut_y, output_tensor) + x = torch.relu(x) + return x + + +class _InceptionTime(torch.nn.Module): + def __init__( + self, + input_shape: Tuple[int, int] = (6, 60), + nb_filters=32, + use_residual: bool = True, + use_bottleneck: bool = True, + depth: int = 6, + kernel_size: int = 41, + ): + super().__init__() + self.input_shape = input_shape + self.nb_filters = nb_filters + self.use_residual = use_residual + self.use_bottleneck = use_bottleneck + self.depth = depth + self.kernel_size = kernel_size - 1 + + self.build_model() + + def build_model(self): + random_input = torch.rand(1, *self.input_shape) + depth_inceptions = [] + shortcuts = [] + + x = random_input + input_res = random_input + + for d in range(self.depth): + inception = InceptionModule( + input_shape=x.shape[1:], + stride=1, + kernel_size=self.kernel_size, + nb_filters=self.nb_filters, + use_bottleneck=self.use_bottleneck, + ) + depth_inceptions.append(inception) + + # forward pass in inception module + x = inception(x) + + if self.use_residual and d % 3 == 2: + shortcut = ShortcutLayer( + input_tensor_shape=input_res.shape[1:], + out_tensor_shape=x.shape[1:], + ) + shortcuts.append(shortcut) + x = shortcut(input_res, x) + input_res = x + + self.depth_inceptions = depth_inceptions + self.shortcuts = shortcuts + self.global_avg_pool = torch.nn.AdaptiveAvgPool1d(1) + + def forward(self, x): + shortcut_no = 0 + + input_res = x + + for d, inception in enumerate(self.depth_inceptions): + x = inception(x) + + if self.use_residual and d % 3 == 2: + shortcut = self.shortcuts[shortcut_no] + x = shortcut(input_res, x) + input_res = x + shortcut_no += 1 + + x = self.global_avg_pool(x) + x = x.squeeze(2) + return x + + +class InceptionTime(SimpleSupervisedModel): + def __init__( + self, + input_shape: Tuple[int, int] = (6, 60), + nb_filters=32, + use_residual: bool = True, + use_bottleneck: bool = True, + depth: int = 6, + kernel_size: int = 41, + num_classes: int = 6, + learning_rate: float = 1e-3, + ): + backbone = _InceptionTime( + input_shape, + nb_filters, + use_residual, + use_bottleneck, + depth, + kernel_size, + ) + self.fc_input_features = self._calculate_fc_input_features( + backbone, input_shape + ) + fc = self._create_fc(self.fc_input_features, num_classes) + super().__init__( + backbone=backbone, + fc=fc, + learning_rate=learning_rate, + flatten=True, + loss_fn=torch.nn.CrossEntropyLoss(), + val_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + test_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + ) + + def _calculate_fc_input_features( + self, backbone: torch.nn.Module, input_shape: Tuple[int, int, int] + ) -> int: + """Run a single forward pass with a random input to get the number of + features after the convolutional layers. + + Parameters + ---------- + backbone : torch.nn.Module + The backbone of the network + input_shape : Tuple[int, int, int] + The input shape of the network. + + Returns + ------- + int + The number of features after the convolutional layers. + """ + random_input = torch.randn(1, *input_shape) + with torch.no_grad(): + out = backbone(random_input) + return out.view(out.size(0), -1).size(1) + + def _create_fc( + self, input_features: int, num_classes: int + ) -> torch.nn.Module: + return torch.nn.Sequential( + torch.nn.Linear(in_features=input_features, out_features=num_classes), + # torch.nn.Softmax(dim=1), + ) + + +# def test_inception_time(): +# input_shape = (6, 60) + +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) + +# model = InceptionTime( +# input_shape=input_shape, num_classes=6, learning_rate=1e-3 +# ) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) + +# trainer.fit(model, datamodule=data_module) + + + + # from torchview import draw_graph + + # model = InceptionTime() + # result = model(torch.rand(1, 6, 60)) + # print(f"Result.shape: {result.shape}") + # model_graph = draw_graph( + # model, + # input_size=(64, 6, 60), + # device="cpu", + # expand_nested=True, + # show_shapes=True, + # save_graph=True, + # filename="inception_graph", + # ) + # model_graph.visual_graph.render("inception_graph.png", format="png") + # print(f"Graph saved to `inception_graph.png`") + + +# if __name__ == "__main__": +# test_inception_time() diff --git a/minerva/models/nets/multi_channel_cnn.py b/minerva/models/nets/multi_channel_cnn.py new file mode 100644 index 0000000..bc924c7 --- /dev/null +++ b/minerva/models/nets/multi_channel_cnn.py @@ -0,0 +1,164 @@ +from typing import Dict, Tuple +import torch +import lightning as L +from torchmetrics import Accuracy + +from minerva.models.nets.base import SimpleSupervisedModel + + +class _MultiChannelCNN_HAR(torch.nn.Module): + def __init__(self, input_channels: int = 1, concatenate: bool = True): + super().__init__() + + self.freq_encoder = torch.nn.Sequential( + torch.nn.Conv2d( + input_channels, 16, kernel_size=2, stride=1, padding="same" + ), + torch.nn.ReLU(), + torch.nn.Conv2d(16, 16, kernel_size=2, stride=1, padding="same"), + torch.nn.ReLU(), + torch.nn.MaxPool2d(2), + ) + + self.welch_encoder = torch.nn.Sequential( + torch.nn.Conv2d( + input_channels, 16, kernel_size=2, stride=1, padding="same" + ), + torch.nn.ReLU(), + torch.nn.Conv2d(16, 16, kernel_size=2, stride=1, padding="same"), + torch.nn.ReLU(), + torch.nn.MaxPool2d(2), + ) + + self.concatenate = concatenate + + def forward(self, x): + # Input is a 5D tensor (Batch, Transformed, Channel, H, W) + # X[:, 0, :, :, :] --> Frequency (1, 6, 60) + # X[:, 1, :, :, :] --> Welch (1, 6, 60) + freq_out = self.freq_encoder(x[:, 0, :, :, :]) + welch_out = self.welch_encoder(x[:, 1, :, :, :]) + if not self.concatenate: + return freq_out, welch_out + else: + return torch.cat([freq_out, welch_out], dim=1) + + +class MultiChannelCNN_HAR(SimpleSupervisedModel): + def __init__( + self, + input_shape: Tuple[int, int, int] = (1, 6, 60), + num_classes: int = 6, + learning_rate: float = 1e-3, + ): + """Create a simple 1D Convolutional Network with 3 layers and 2 fully + connected layers. + + Parameters + ---------- + input_shape : Tuple[int, int], optional + A 2-tuple containing the number of input channels and the number of + features, by default (6, 60). + num_classes : int, optional + Number of output classes, by default 6 + learning_rate : float, optional + Learning rate for Adam optimizer, by default 1e-3 + """ + self.input_shape = input_shape + self.num_classes = num_classes + + backbone = self._create_backbone(input_channels=input_shape[0]) + self.fc_input_channels = self._calculate_fc_input_features( + backbone, input_shape + ) + fc = self._create_fc(self.fc_input_channels, num_classes) + super().__init__( + backbone=backbone, + fc=fc, + learning_rate=learning_rate, + flatten=True, + loss_fn=torch.nn.CrossEntropyLoss(), + val_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + test_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + ) + + def _create_backbone(self, input_channels: int) -> torch.nn.Module: + return _MultiChannelCNN_HAR(input_channels=input_channels) + + def _calculate_fc_input_features( + self, backbone: torch.nn.Module, input_shape: Tuple[int, int] + ) -> int: + """Run a single forward pass with a random input to get the number of + features after the convolutional layers. + + Parameters + ---------- + backbone : torch.nn.Module + The backbone of the network + input_shape : Tuple[int, int] + The input shape of the network. + + Returns + ------- + int + The number of features after the convolutional layers. + """ + random_input = torch.randn(1, 2, *input_shape) + with torch.no_grad(): + out = backbone(random_input) + return out.view(out.size(0), -1).size(1) + + def _create_fc( + self, input_features: int, num_classes: int + ) -> torch.nn.Module: + return torch.nn.Sequential( + torch.nn.Linear(input_features, num_classes), + torch.nn.ReLU(), + ) + + +# def test_multichannel_cnn(): +# from ssl_tools.transforms.signal_1d import FFT, WelchPowerSpectralDensity +# from ssl_tools.transforms.utils import ( +# PerChannelTransform, +# StackComposer, +# Cast, +# ) +# from ssl_tools.models.utils import RandomDataModule + +# input_shape = (1, 6, 60) + +# fft_transform = PerChannelTransform(FFT(absolute=True)) +# welch = PerChannelTransform( +# WelchPowerSpectralDensity( +# fs=1 / 20, return_onesided=False, absolute=True +# ) +# ) +# stacker = StackComposer([fft_transform, welch]) + +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# transforms=[stacker, Cast("float32")], +# ) + +# model = MultiChannelCNN_HAR( +# input_shape=input_shape, num_classes=6, learning_rate=1e-3 +# ) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) + +# trainer.fit(model, datamodule=data_module) + + +# if __name__ == "__main__": +# test_multichannel_cnn() diff --git a/minerva/models/nets/resnet_1d.py b/minerva/models/nets/resnet_1d.py new file mode 100644 index 0000000..80d4a49 --- /dev/null +++ b/minerva/models/nets/resnet_1d.py @@ -0,0 +1,303 @@ +import numpy as np +import time + + +from functools import partial +from typing import Literal, Tuple +import torch +from torch import nn +from torch.nn import TransformerEncoder, TransformerEncoderLayer +from torchmetrics import Accuracy +from minerva.models.nets.base import SimpleSupervisedModel +import lightning as L + + +class ConvolutionalBlock(torch.nn.Module): + def __init__( + self, in_channels: int, activation_cls: torch.nn.Module = None + ): + super().__init__() + self.in_channels = in_channels + self.activation_cls = activation_cls + + self.block = torch.nn.Sequential( + torch.nn.Conv1d( + in_channels, out_channels=64, kernel_size=5, stride=1 + ), + torch.nn.BatchNorm1d(64), + activation_cls(), + torch.nn.MaxPool1d(2), + ) + + def forward(self, x): + return self.block(x) + + +class SqueezeAndExcitation1D(torch.nn.Module): + def __init__(self, in_channels: int, reduction_ratio: int = 2): + super().__init__() + self.in_channels = in_channels + self.reduction_ratio = reduction_ratio + self.num_channels_reduced = in_channels // reduction_ratio + + self.block = torch.nn.Sequential( + torch.nn.Linear(in_channels, self.num_channels_reduced), + torch.nn.ReLU(), + torch.nn.Linear(self.num_channels_reduced, in_channels), + torch.nn.Sigmoid(), + ) + + def forward(self, input_tensor): + squeeze_tensor = input_tensor.mean(dim=2) + x = self.block(squeeze_tensor) + output_tensor = torch.mul( + input_tensor, + x.view(input_tensor.shape[0], input_tensor.shape[1], 1), + ) + return output_tensor + + +class ResNetBlock(torch.nn.Module): + def __init__( + self, + in_channels: int = 64, + activation_cls: torch.nn.Module = torch.nn.ReLU, + ): + super().__init__() + self.in_channels = in_channels + self.activation_cls = activation_cls + + self.block = torch.nn.Sequential( + torch.nn.Conv1d( + in_channels, + out_channels=32, + kernel_size=5, + stride=1, + padding="same", + ), + torch.nn.BatchNorm1d(32), + activation_cls(), + torch.nn.Conv1d( + in_channels=32, + out_channels=64, + kernel_size=5, + stride=1, + padding="same", + ), + torch.nn.BatchNorm1d(64), + ) + + def forward(self, x): + input_tensor = x + x = self.block(x) + x += input_tensor + x = self.activation_cls()(x) + return x + + +class ResNetSEBlock(ResNetBlock): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.block.append(SqueezeAndExcitation1D(64)) + + +class _ResNet1D(torch.nn.Module): + def __init__( + self, + input_shape: Tuple[int, int], + residual_block_cls=ResNetBlock, + activation_cls: torch.nn.Module = torch.nn.ReLU, + num_residual_blocks: int = 5, + reduction_ratio=2, + ): + super().__init__() + self.input_shape = input_shape + self.num_residual_blocks = num_residual_blocks + self.reduction_ratio = reduction_ratio + + self.conv_block = ConvolutionalBlock( + in_channels=input_shape[0], activation_cls=activation_cls + ) + self.residual_blocks = torch.nn.Sequential( + *[ + residual_block_cls( + in_channels=64, activation_cls=activation_cls + ) + for _ in range(num_residual_blocks) + ] + ) + self.global_avg_pool = torch.nn.AdaptiveAvgPool1d(1) + + def forward(self, x): + x = self.conv_block(x) + x = self.residual_blocks(x) + x = self.global_avg_pool(x) + x = x.squeeze(2) + return x + + +class ResNet1DBase(SimpleSupervisedModel): + def __init__( + self, + resnet_block_cls: type = ResNetBlock, + activation_cls: type = torch.nn.ReLU, + input_shape: Tuple[int, int] = (6, 60), + num_classes: int = 6, + num_residual_blocks: int = 5, + reduction_ratio=2, + learning_rate: float = 1e-3, + ): + backbone = _ResNet1D( + input_shape=input_shape, + residual_block_cls=resnet_block_cls, + activation_cls=activation_cls, + num_residual_blocks=num_residual_blocks, + reduction_ratio=reduction_ratio, + ) + + self.fc_input_features = self._calculate_fc_input_features( + backbone, input_shape + ) + fc = torch.nn.Linear(self.fc_input_features, num_classes) + + super().__init__( + backbone=backbone, + fc=fc, + learning_rate=learning_rate, + flatten=True, + loss_fn=torch.nn.CrossEntropyLoss(), + val_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + test_metrics={ + "acc": Accuracy(task="multiclass", num_classes=num_classes) + }, + ) + + def _calculate_fc_input_features( + self, backbone: torch.nn.Module, input_shape: Tuple[int, int, int] + ) -> int: + """Run a single forward pass with a random input to get the number of + features after the convolutional layers. + + Parameters + ---------- + backbone : torch.nn.Module + The backbone of the network + input_shape : Tuple[int, int, int] + The input shape of the network. + + Returns + ------- + int + The number of features after the convolutional layers. + """ + random_input = torch.randn(1, *input_shape) + with torch.no_grad(): + out = backbone(random_input) + return out.view(out.size(0), -1).size(1) + + +# Deep Residual Network for Smartwatch-Based User Identification through Complex Hand Movements (ResNet1D) +class ResNet1D_8(ResNet1DBase): + def __init__(self, *args, **kwargs): + super().__init__( + *args, + **kwargs, + resnet_block_cls=ResNetBlock, + activation_cls=torch.nn.ELU, + num_residual_blocks=8, + ) + + +# Deep Residual Network for Smartwatch-Based User Identification through Complex Hand Movements (ResNetSE1D) +class ResNetSE1D_8(ResNet1DBase): + def __init__(self, *args, **kwargs): + super().__init__( + *args, + **kwargs, + resnet_block_cls=ResNetSEBlock, + activation_cls=torch.nn.ELU, + num_residual_blocks=8, + ) + +# resnet-se: Channel Attention-Based Deep Residual Network for Complex Activity Recognition Using Wrist-Worn Wearable Sensors +# Changes the activation function to ReLU and the number of residual blocks to 5 (compared to ResNetSE1D_8) +class ResNetSE1D_5(ResNet1DBase): + def __init__(self, *args, **kwargs): + super().__init__( + *args, + **kwargs, + resnet_block_cls=ResNetSEBlock, + activation_cls=torch.nn.ReLU, + num_residual_blocks=5, + ) + + +# def test_resnet_8(): +# input_shape = (6, 60) +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) +# model = ResNet1D_8( +# input_shape=input_shape, +# num_classes=6, +# learning_rate=1e-3, +# ) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) +# trainer.fit(model, datamodule=data_module) + + +# def test_resnet_se_8(): +# input_shape = (6, 60) +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) +# model = ResNetSE1D_8( +# input_shape=input_shape, +# num_classes=6, +# learning_rate=1e-3, +# ) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) +# trainer.fit(model, datamodule=data_module) + + +# def test_resnet_se_5(): +# input_shape = (6, 60) +# data_module = RandomDataModule( +# num_samples=8, +# num_classes=6, +# input_shape=input_shape, +# batch_size=8, +# ) +# model = ResNetSE1D_5( +# input_shape=input_shape, +# num_classes=6, +# learning_rate=1e-3, +# ) +# print(model) + +# trainer = L.Trainer( +# max_epochs=1, logger=False, devices=1, accelerator="cpu" +# ) +# trainer.fit(model, datamodule=data_module) + + +# if __name__ == "__main__": +# test_resnet_8() +# test_resnet_se_8() +# test_resnet_se_5() From 3eedb63f56d4d50b9530a22d14504a81820cb03d Mon Sep 17 00:00:00 2001 From: Otavio Napoli Date: Wed, 29 May 2024 02:55:18 +0000 Subject: [PATCH 04/11] Added tabular and csv readers. Alfso added a readme.md --- .gitignore | 3 +- minerva/data/README.md | 10 ++ minerva/data/readers/csv_reader.py | 17 ++++ minerva/data/readers/tabular_reader.py | 128 +++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 minerva/data/README.md create mode 100644 minerva/data/readers/csv_reader.py create mode 100644 minerva/data/readers/tabular_reader.py diff --git a/.gitignore b/.gitignore index f289fad..d37f9a9 100644 --- a/.gitignore +++ b/.gitignore @@ -169,5 +169,4 @@ cython_debug/ #.idea/ experiments/ logs/ -lightning_logs/ -data/ \ No newline at end of file +lightning_logs/ \ No newline at end of file diff --git a/minerva/data/README.md b/minerva/data/README.md new file mode 100644 index 0000000..5f19123 --- /dev/null +++ b/minerva/data/README.md @@ -0,0 +1,10 @@ +# Readers + +| **Reader** | **Data Unit** | **Order** | **Class** | **Observations** | +|-------------------- |----------------------------------------------------------------------------------- |--------------------- |-------------------------------------------------------------- |------------------------------------------------------------------------------------------------------------------------------------ | +| PNGReader | Each unit of data is a image file (PNG) inside the root folder | Lexigraphical order | minerva.data.readers.png_reader.PNGReader | File extensions: .png | +| TIFFReader | Each unit of data is a image file (TIFF) inside the root folder | Lexigraphical order | minerva.data.readers.tiff_reader.TiffReader | File extensions: .tif and .tiff | +| TabularReader | Each unit of data is the i-th row in a dataframe, with columns filtered | Dataframe rows | minerva.data.readers.tabular_reader.TabularReader | Support pandas dataframe | +| CSVReader | Each unit of data is the i-th row in a CSV file, with columns filtered | CSV Rowd | minerva.data.readers.csv_reader.CSVReader | If dataframe is already open, use TabularReader instead. This class will open and load the CSV file and pass it to a TabularReader | +| PatchedArrayReader | Each unit of data is a submatrix of specified shape inside an n-dimensional array | Dimension order | minerva.data.readers.patched_array_reader.PatchedArrayReader | Supports any data with ndarray protocol (tensor, xarray, zarr) | +| PatchedZarrReader | Each unit of data is a submatrix of specified shape inside an Zarr Array | Dimension order | minerva.data.readers.zarr_reader.ZarrArrayReader | Open zarr file in lazy mode and pass it to PatchedArrayReader | \ No newline at end of file diff --git a/minerva/data/readers/csv_reader.py b/minerva/data/readers/csv_reader.py new file mode 100644 index 0000000..e03170e --- /dev/null +++ b/minerva/data/readers/csv_reader.py @@ -0,0 +1,17 @@ +from typing import Union + +import pandas as pd +from minerva.data.readers.tabular_reader import TabularReader + +class CSVReader(TabularReader): + def __init__( + self, + path: str, + columns_to_select: Union[str, list[str]], + cast_to: str = None, + data_shape: tuple[int, ...] = None, + ): + df = pd.read_csv(path) + super().__init__(df, columns_to_select, cast_to, data_shape) + + diff --git a/minerva/data/readers/tabular_reader.py b/minerva/data/readers/tabular_reader.py new file mode 100644 index 0000000..d9d2706 --- /dev/null +++ b/minerva/data/readers/tabular_reader.py @@ -0,0 +1,128 @@ +from pathlib import Path +from typing import Union + +import numpy as np +import re +import pandas as pd +from minerva.data.readers.reader import _Reader + +class TabularReader(_Reader): + def __init__( + self, + df: pd.DataFrame, + columns_to_select: Union[str, list[str]], + cast_to: str = None, + data_shape: tuple[int, ...] = None, + ): + """Reader to select columns from a DataFrame and return them as a NumPy + array. The DataFrame is indexed by the row number. Each row of the + DataFrame is considered as a sample. Thus, the __getitem__ method will + return the columns of the DataFrame at the specified index as a NumPy + array. + + Parameters + ---------- + df : pd.DataFrame + The DataFrame to select the columns from. The DataFrame should have + the columns that are specified in the `columns_to_select` parameter. + columns_to_select : Union[str, list[str]] + A string or a list of strings used to select the columns from the DataFrame. + The string can be a regular expression pattern or a column name. The columns + that match the pattern will be selected. + cast_to : str, optional + Cast the selected columns to the specified data type. If None, the + data type of the columns will not be changed. (default is None) + data_shape : tuple[int, ...], optional + The shape of the data to be returned. If None, the data will be + returned as a 1D array. If provided, the data will be reshaped to + the specified shape. (default is None) + """ + self.df = df + self.columns_to_select = columns_to_select + self.cast_to = cast_to + self.data_shape = data_shape + + if isinstance(self.columns_to_select, str): + self.columns_to_select = [self.columns_to_select] + + def __getitem__(self, index: int) -> np.ndarray: + """Return the columns of the DataFrame at the specified row index as a NumPy + array. The columns are selected based on the `self.columns_to_select`. + + Parameters + ---------- + index : int + The row index to select the columns from the DataFrame. + + Returns + ------- + np.ndarray + The selected columns from the row as a NumPy array. + """ + columns = list(self.df.columns) + + # Filter valid columns based on columns_to_select list + valid_columns = [] + for pattern in self.columns_to_select: + valid_columns.extend( + [col for col in columns if re.match(pattern, col)] + ) + + # Select the elements and return + row = self.df.iloc[index][valid_columns] + row = row.to_numpy() + + if self.cast_to is not None: + row = row.astype(self.cast_to) + + if self.data_shape is not None: + row = row.reshape(self.data_shape) + + return row + + def __len__(self) -> int: + """Return the number of samples in the DataFrame. The number of samples + is equal to the number of rows in the DataFrame. + + Returns + ------- + int + The number of samples in the DataFrame. + """ + return len(self.df) + + +# def main(): +# df = pd.DataFrame({ +# "accel-x-0": np.array(range(10)), +# "accel-x-1": np.array(range(10)) + 10, +# "accel-x-2": np.array(range(10)) + 100, +# "accel-x-3": np.array(range(10)) + 1000, + +# "accel-y-0": np.array(range(10)), +# "accel-y-1": np.array(range(10)) * 2, +# "accel-y-2": np.array(range(10)) * 3, +# "accel-y-3": np.array(range(10)) * 4, + +# "gyro-x-0": np.array(range(10)) - 10, +# "gyro-x-1": np.array(range(10)) - 20, +# "gyro-x-2": np.array(range(10)) - 30, +# "gyro-x-3": np.array(range(10)) - 40, +# }) + +# reader = TabularReader(df, ["accel-x-*", "gyro-x-*"]) +# print(len(reader)) +# print(reader[1]) + +# reader = TabularReader(df, ["accel-*", "gyro-x-*"]) +# print(len(reader)) +# print(reader[2]) + + +# reader = TabularReader(df, ["accel-x-1", "gyro-x-0", "gyro-x-1", "accel-y-*"]) +# print(len(reader)) +# print(reader[3]) + + +# if __name__ == "__main__": +# main() From 6c6b034dd090eaceac52e8a05d9793ecce8bc956 Mon Sep 17 00:00:00 2001 From: Otavio Napoli Date: Wed, 29 May 2024 03:07:57 +0000 Subject: [PATCH 05/11] Added docs --- docs/experiments.rst | 4 ---- docs/index.rst | 2 -- docs/installation.rst | 17 ++++++++++++++--- docs/notebooks/1.datasets.ipynb | 7 +++++++ docs/notebooks/figures/dataset_readers.svg | 4 ++++ docs/tutorials.rst | 2 +- 6 files changed, 26 insertions(+), 10 deletions(-) delete mode 100644 docs/experiments.rst create mode 100644 docs/notebooks/figures/dataset_readers.svg diff --git a/docs/experiments.rst b/docs/experiments.rst deleted file mode 100644 index eadc650..0000000 --- a/docs/experiments.rst +++ /dev/null @@ -1,4 +0,0 @@ -Running Experiments --------------------- - -Under construction... \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index d1dfeae..b5c358c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,8 +12,6 @@ Welcome to Minerva's documentation! installation tutorials - experiments - contributing api diff --git a/docs/installation.rst b/docs/installation.rst index 88d3618..17dfdc8 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,4 +1,15 @@ -Installation ------------------ +.. _installation: + +========================== +Installation Guide +========================== + +Pip Installation +--------------------- + +To install Minerva, you can use pip: + +.. code-block:: bash + + pip install minerva -Under construction... \ No newline at end of file diff --git a/docs/notebooks/1.datasets.ipynb b/docs/notebooks/1.datasets.ipynb index 9ae5b10..3184776 100644 --- a/docs/notebooks/1.datasets.ipynb +++ b/docs/notebooks/1.datasets.ipynb @@ -119,6 +119,13 @@ "\n", "This structured approach ensures clarity and flexibility in handling different tasks and data organizations." ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Figure Title](figures/dataset_readers.svg)" + ] } ], "metadata": { diff --git a/docs/notebooks/figures/dataset_readers.svg b/docs/notebooks/figures/dataset_readers.svg new file mode 100644 index 0000000..99c48bc --- /dev/null +++ b/docs/notebooks/figures/dataset_readers.svg @@ -0,0 +1,4 @@ + + + +
5
4
3
Data Module
2
TiffReader

Data Organization

Data unit: Each unit of data is a file inside root folder, suffixed with *tif or *.tiff
Order: Lexigraphical order

Example:
root/
0.tiff
1.tiff
...

PNGReader

Data Organization

Data unit: Each unit of data is a file inside root folder, suffixed with *png
Order: Lexigraphical order

Example:
root/
0.png
1.png
...

PatchedZarrReader

Data Organization

Data unit: Given a n-dimensional zarr array, and data_shape, each unit of data is a sub array with size data_shape.
Order: First dim, second dim, ....
1
0

0.png

1.png

2.png

0.tiff

1.tiff

2.tiff
SimpleSupervisedDataset
PatchedZarrReader
(original.zarr)
PatchedZarrReader
(envelope.zarr)
Transforms Set A
Transforms Set B
__getitem__(index)
(A, B)
train_dataloader()
\ No newline at end of file diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 2edd48a..d82c0a9 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -7,4 +7,4 @@ Tutorials .. toctree:: :maxdepth: 2 - Under construction \ No newline at end of file + notebooks/1.datasets.ipynb \ No newline at end of file From 0f0b98fc1919331d95a1bd276c015768de553cf4 Mon Sep 17 00:00:00 2001 From: Otavio Napoli Date: Wed, 29 May 2024 03:08:12 +0000 Subject: [PATCH 06/11] Added docs.yaml github workflow --- .github/workflows/docs.yaml | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/docs.yaml diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..7757fb2 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,42 @@ +name: minerva documentation +on: + push: + branches: + - docs + +jobs: + docs: + name: Minverva documentation + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v2 + + - name: Install Python + uses: actions/setup-python@v1 + with: + python-version: 3.12.1 + + - name: Install packages + run: | + sudo apt-get update + sudo apt-get install -y pandoc + + - name: Install requirements + run: | + pip3 install sphinx sphinx-rtd-theme sphinx-autodoc-typehints sphinx-argparse sphinx-autoapi nbsphinx pandoc Ipython + + - name: Build docs + run: | + cd docs + make clean + make html + + # https://github.com/peaceiris/actions-gh-pages + - name: Deploy + if: success() + uses: peaceiris/actions-gh-pages@v3 + with: + publish_branch: gh-pages + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/build/html/ \ No newline at end of file From 5acfd28a441757f064af32952c8e3cf2569f9792 Mon Sep 17 00:00:00 2001 From: Otavio Napoli Date: Wed, 29 May 2024 03:09:42 +0000 Subject: [PATCH 07/11] Changed docs build top ython 3.11.9 --- .github/workflows/docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 7757fb2..d2578f2 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -15,7 +15,7 @@ jobs: - name: Install Python uses: actions/setup-python@v1 with: - python-version: 3.12.1 + python-version: 3.11.9 - name: Install packages run: | From 22eead451e81e266e9fe3e8bb592bf9786834cfe Mon Sep 17 00:00:00 2001 From: Otavio Napoli Date: Wed, 29 May 2024 03:42:14 +0000 Subject: [PATCH 08/11] Added more tutorials to docs --- docs/notebooks/2.models.ipynb | 40 ++ docs/notebooks/3.pipelines.ipynb | 237 ++++++++++ .../4.minerva_setr_pup_pipeline.ipynb | 431 ++++++++++++++++++ docs/tutorials.rst | 5 +- 4 files changed, 712 insertions(+), 1 deletion(-) create mode 100644 docs/notebooks/2.models.ipynb create mode 100644 docs/notebooks/3.pipelines.ipynb create mode 100644 docs/notebooks/4.minerva_setr_pup_pipeline.ipynb diff --git a/docs/notebooks/2.models.ipynb b/docs/notebooks/2.models.ipynb new file mode 100644 index 0000000..e52f010 --- /dev/null +++ b/docs/notebooks/2.models.ipynb @@ -0,0 +1,40 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Models\n", + "\n", + "- Models should specify their sample format. E.g., a 2-element tuple of `(input, target)`\n", + "- Data Modules should return a sample in the format specified by the model\n", + "- Top-level modules must by Lightning Modules\n", + "- Consider inheriting from `minerva.models.nets.base.SimpleSupervisedModel` for simple supervised models" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The `SimpleSupervisedModel` class\n", + "\n", + "This class implements a very common deep learning pipeline, which is composed by the following steps:\n", + "\n", + "1. Make a forward pass with the input data on the backbone model;\n", + "2. Make a forward pass with the input data on the fc model;\n", + "3. Compute the loss between the output and the label data;\n", + "4. Optimize the model (backbone and FC) parameters with respect to the loss.\n", + "\n", + "This reduces the code duplication for autoencoder models, and makes it easier to implement new models by only changing the backbone model. More complex models, that does not follow this pipeline, should not inherit from this class.\n", + "Note that, for this class the input data is a tuple of tensors, where the first tensor is the input data and the second tensor is the mask or label." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/3.pipelines.ipynb b/docs/notebooks/3.pipelines.ipynb new file mode 100644 index 0000000..29aeffe --- /dev/null +++ b/docs/notebooks/3.pipelines.ipynb @@ -0,0 +1,237 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Pipelines\n", + "\n", + "Pipelines provide a versatile API for automating tasks efficiently. Below some key features and best practices:\n", + "\n", + "## 1. Reproducibility\n", + "\n", + "- **Initialization and Configuration**: Pipelines are initialized using the `__init__` method, allowing configuration of common elements. All parameters passed to the class constructor are stored in the `self.hparams` dictionary, facilitating reproducibility and serialization. Additionally, the `ignore` parameter in the `__init__` method allows exclusion of specific parameters, enhancing reproducibility by avoiding the storage of non-essential or large parameters. For example:\n", + "\n", + " ```python\n", + " pipeline = Pipeline(ignore=[\"large_param\"])\n", + " ```\n", + "\n", + "- **ID and Working Directory**: Each pipeline instance is assigned a unique identifier (`id`) upon initialization, aiding in tracking and identification. Additionally, pipelines have a designated working directory for organizing generated files, though it doesn't alter Python's working directory. Example:\n", + "\n", + " ```python\n", + " print(f\"Pipeline ID: {pipeline.pipeline_id}\")\n", + " print(f\"Working Directory: {pipeline.working_dir}\")\n", + " ```\n", + "\n", + "- **Public Interface**: Pipelines offer the `run` method as the public interface for execution. The `run` method encapsulates the pipeline's logic and returns the output. Note that, `run` is the only method that should be called directly by users. For your own version of pipeline, you should override `_run` method (that is called from `run`) Example:\n", + "\n", + " ```python\n", + " result = pipeline.run(argument=value)\n", + " ```\n", + " \n", + " Besides a result, the `run` method can also set public attributes of the pipeline instance. These attributes are implemented as read-only properties, ensuring a consistent state during execution. For instance, the code below:\n", + "\n", + " ```python\n", + " class Example(Pipeline):\n", + " def __init__(self, start=0, end=100):\n", + " # Cache result allows to maintain the return of `run` method at `_result` attribute\n", + " # This can be accessed though the `result` property (public attribute)\n", + " super().__init__(cache_result=True)\n", + " self._seed = None\n", + " self._start = start\n", + " self._end = end\n", + "\n", + " # Read-only, public attribute (properties)\n", + " # This attribute is set during pipeline execution and can be accessed before and after execution.\n", + " # ONly these attributes (properties) and the _run method should be accessed directly by users.\n", + " @property\n", + " def seed(self):\n", + " return self._seed\n", + "\n", + " def _run(self, argument):\n", + " # Pipeline logic here\n", + "\n", + " # Set seed attribute.\n", + " self._seed = int(time.time())\n", + " np.random.seed(self._seed)\n", + " return np.random.randint(self._start, self._end) + argument\n", + "\n", + "\n", + " pipeline = Example()\n", + "\n", + " print(pipeline.hparams)\n", + " # Output: {'start': 0, 'end': 100}\n", + "\n", + " print(pipeline.status)\n", + " #{'status': 'NOT STARTED',\n", + " # 'working_dir': '/workspaces/seismic',\n", + " # 'id': 'ae87a62731c04604bb35c0b9d4626982',\n", + " # 'count': 0,\n", + " # 'created': 1715128966.1678915,\n", + " # 'start_time': None,\n", + " # 'end_time': None,\n", + " # 'exception_info': None,\n", + " # 'cached': False}\n", + "\n", + " result = pipeline.run(argument=10)\n", + " print(result)\n", + " # Output: 91\n", + "\n", + " print(pipeline.result)\n", + " # Output: 91\n", + " ```\n", + "\n", + " The public attributes are `seed` that is set during the pipeline run.\n", + "\n", + "\n", + "## 2. Composition\n", + "\n", + "- **Combining Pipelines**: Pipelines can be composed of other pipelines, allowing the creation of complex workflows from simpler components. This modularity enhances flexibility and scalability in pipeline design.\n", + "\n", + "For instance, consider the minimal example following example:\n", + "\n", + "```python\n", + "\n", + "class Distance(Pipeline):\n", + " def __init__(self, norm: int):\n", + " super().__init__(cache_result=False)\n", + " self.norm = norm\n", + "\n", + " def _run(self, x, y):\n", + " return (x**self.norm + y**self.norm) ** (1 / self.norm)\n", + "\n", + "\n", + "class SumOfDistances(Pipeline):\n", + " def __init__(self, constant: int, distance_pipeline: Distance):\n", + " super().__init__(ignore=\"distance_pipeline\", cache_result=True)\n", + " self.constant = constant\n", + " self.distance_pipeline = distance_pipeline\n", + "\n", + " def _run(self, items: List[Tuple[float, float]]):\n", + " return (\n", + " sum(self.distance_pipeline.run(x, y) for x, y in items)\n", + " + self.constant\n", + " )\n", + "```\n", + "\n", + "In this example, we have two pipelines: `Distance` and `SumOfDistances`. The `Distance` pipeline calculates the distance between two points based on a specified norm. The `SumOfDistances` pipeline calculates the sum of distances between multiple points and adds a constant value. The `SumOfDistances` pipeline uses the `Distance` pipeline as a component, demonstrating pipeline composition.\n", + "\n", + "```python\n", + "distance_pipeline = Distance(norm=2)\n", + "sum_of_distances_pipeline = SumOfDistances(constant=10, distance_pipeline=distance_pipeline)\n", + "sum_of_distances_pipeline.run([(1, 2), (3, 4),(5, 6)])\n", + "# Output: 25.046317653406444\n", + "```\n", + "\n", + "\n", + "## 3. Integration with CLI\n", + "\n", + "- **Seamless CLI Integration**: Pipelines integrate seamlessly with `jsonargparse`, enabling the creation of command-line interfaces (CLI) for easy configuration and execution. Configuration can be provided via YAML files or directly through CLI run arguments, enhancing user accessibility. Examples of CLI usage with `jsonargparse` are provided. For instance, we can use the `CLI` class to run a pipeline with arguments:\n", + "\n", + " ```python\n", + " # Example CLI usage\n", + " args = [\n", + " \"--constant\",\n", + " \"10\",\n", + " \"--distance_pipeline\",\n", + " '{\"class_path\": \"Distance\", \"init_args\": {\"norm\": \"2\"}}',\n", + " \"run\",\n", + " \"--items\",\n", + " '[[\"1\", \"2\"], [\"3\", \"4\"], [\"5\", \"6\"]]',\n", + " ]\n", + "\n", + " result = CLI(SumOfDistances, as_positional=False, args=args)\n", + "\n", + " ```\n", + "\n", + "\n", + "Or write an YAML file for some of the parameters\n", + "\n", + "```yaml\n", + "# config.yaml\n", + "constant: 10\n", + "distance_pipeline:\n", + " class_path: Distance\n", + " init_args:\n", + " norm: 2\n", + "```\n", + "\n", + "And run the pipeline with the YAML file:\n", + "\n", + "```python\n", + "# Example CLI usage with YAML file\n", + "result = CLI(SumOfDistances, as_positional=False, args=[\"--config\", \"config.yaml\", \"run\", \"--items\", '[[\"1\", \"2\"], [\"3\", \"4\"], [\"5\", \"6\"]]'])\n", + "```\n", + "\n", + "Or write an YAML file for all the parameters\n", + "\n", + "```yaml\n", + "# config.yaml\n", + "constant: 10\n", + "distance_pipeline:\n", + " class_path: Distance\n", + " init_args:\n", + " norm: 2\n", + "run:\n", + " items:\n", + " - [1, 2]\n", + " - [3, 4]\n", + " - [5, 6]\n", + "```\n", + "\n", + "And run the pipeline with the YAML file:\n", + "\n", + "```python\n", + "# Example CLI usage with YAML file\n", + "result = CLI(SumOfDistances, as_positional=False, args=[\"--config\", \"config.yaml\"])\n", + "```\n", + "\n", + "And we can run from shell:\n", + " \n", + "```bash\n", + " python script.py --constant 10 --distance_pipeline '{\"class_path\": \"Distance\", \"init_args\": {\"norm\": \"2\"}}' run --items '[[\"1\", \"2\"], [\"3\", \"4\"], [\"5\", \"6\"]]'\n", + "```\n", + "\n", + "Or the YAML file:\n", + "\n", + "```bash\n", + " python script.py --config config.yaml\n", + "```\n", + "\n", + "\n", + "## 4. Logging and Monitoring\n", + "\n", + "- **Execution Log**: Pipelines maintain a log of their executions, providing a comprehensive record of activities. The `status` property offers insights into the pipeline's state, from creation to completion, facilitating monitoring and troubleshooting. Example:\n", + "\n", + " ```python\n", + " print(f\"Pipeline Status: {pipeline.status}\")\n", + " ```\n", + "\n", + "## 5. Clonability\n", + "\n", + "- **Cloning Pipelines**: Pipelines are cloneable, enabling the creation of independent instances from existing ones. The `clone` method initializes a deep copy, providing a clean slate for each clone. Example:\n", + "\n", + " ```python\n", + " cloned_pipeline = Pipeline.clone(pipeline)\n", + " ```\n", + "\n", + "Note that some attributes, such as `id`, are unique to each pipeline instance and are updated during cloning to maintain uniqueness.\n", + "\n", + "\n", + "\n", + "## 6. Parallel and Distributed Environments\n", + "\n", + "- **Parallel Execution**: Pipelines support parallel execution, enabling faster processing of tasks and efficient resource utilization.\n", + "\n", + "- **Distributed Execution**: Pipelines can be executed in a distributed manner, suitable for deployment on clusters to leverage distributed computing resources effectively. This scalability enhances performance in large-scale processing environments." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/4.minerva_setr_pup_pipeline.ipynb b/docs/notebooks/4.minerva_setr_pup_pipeline.ipynb new file mode 100644 index 0000000..ede49aa --- /dev/null +++ b/docs/notebooks/4.minerva_setr_pup_pipeline.ipynb @@ -0,0 +1,431 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A Simple Example of training a model using Minerva\n", + "\n", + "Task: Image segmentation\n", + "Model: SetR\n", + "Model inputs: 2-element tuple of images with same size (image, mask)\n", + "Data: f3 images as TIFF files and masks as PNG files\n", + "\n", + "We will:\n", + "1. Build our data module\n", + "2. Build our model\n", + "3. Build our trainer\n", + "4. Train the model" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import lightning as L\n", + "import numpy as np\n", + "import torch\n", + "from minerva.data.datasets.supervised_dataset import SupervisedReconstructionDataset\n", + "from minerva.data.readers.png_reader import PNGReader\n", + "from minerva.data.readers.tiff_reader import TiffReader\n", + "from minerva.models.nets.setr import SETR_PUP\n", + "from minerva.transforms.transform import _Transform\n", + "from torch.utils.data import DataLoader\n", + "from torchmetrics import JaccardIndex\n", + "from matplotlib import pyplot as plt\n", + "from minerva.pipelines.lightning_pipeline import SimpleLightningPipeline\n", + "from pathlib import Path\n", + "import os" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## General configs" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "train_path = \"/workspaces/seismic/data/f3_segmentation/images\"\n", + "annotation_path = \"/workspaces/seismic/data/f3_segmentation/annotations\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "class Padding(_Transform):\n", + " def __init__(self, target_h_size: int, target_w_size: int):\n", + " self.target_h_size = target_h_size\n", + " self.target_w_size = target_w_size\n", + "\n", + " def __call__(self, x: np.ndarray) -> np.ndarray:\n", + " h, w = x.shape[:2]\n", + " pad_h = max(0, self.target_h_size - h)\n", + " pad_w = max(0, self.target_w_size - w)\n", + " if len(x.shape) == 2:\n", + " padded = np.pad(x, ((0, pad_h), (0, pad_w)), mode=\"reflect\")\n", + " padded = np.expand_dims(padded, axis=2)\n", + " padded = torch.from_numpy(padded).float()\n", + " else:\n", + " padded = np.pad(x, ((0, pad_h), (0, pad_w), (0, 0)), mode=\"reflect\")\n", + " padded = torch.from_numpy(padded).float()\n", + "\n", + " padded = np.transpose(padded, (2, 0, 1))\n", + " return padded\n", + "\n", + "\n", + "transform = Padding(256, 704)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Our Data Module" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "class F3DataModule(L.LightningDataModule):\n", + " def __init__(\n", + " self,\n", + " train_path: str,\n", + " annotations_path: str,\n", + " transforms: _Transform = None,\n", + " batch_size: int = 1,\n", + " num_workers: int = None,\n", + " ):\n", + " super().__init__()\n", + " self.train_path = Path(train_path)\n", + " self.annotations_path = Path(annotations_path)\n", + " self.transforms = transforms\n", + " self.batch_size = batch_size\n", + " self.num_workers = (\n", + " num_workers if num_workers is not None else os.cpu_count()\n", + " )\n", + "\n", + " self.datasets = {}\n", + "\n", + " def setup(self, stage=None):\n", + " if stage == \"fit\":\n", + " train_img_reader = TiffReader(self.train_path / \"train\")\n", + " train_label_reader = PNGReader(self.annotations_path / \"train\")\n", + " train_dataset = SupervisedReconstructionDataset(\n", + " readers=[train_img_reader, train_label_reader],\n", + " transforms=self.transforms,\n", + " )\n", + "\n", + " val_img_reader = TiffReader(self.train_path / \"val\")\n", + " val_label_reader = PNGReader(self.annotations_path / \"val\")\n", + " val_dataset = SupervisedReconstructionDataset(\n", + " readers=[val_img_reader, val_label_reader],\n", + " transforms=self.transforms,\n", + " )\n", + "\n", + " self.datasets[\"train\"] = train_dataset\n", + " self.datasets[\"val\"] = val_dataset\n", + "\n", + " elif stage == \"test\" or stage == \"predict\":\n", + " test_img_reader = TiffReader(self.train_path / \"test\")\n", + " test_label_reader = PNGReader(self.annotations_path / \"test\")\n", + " test_dataset = SupervisedReconstructionDataset(\n", + " readers=[test_img_reader, test_label_reader],\n", + " transforms=self.transforms,\n", + " )\n", + " self.datasets[\"test\"] = test_dataset\n", + " self.datasets[\"predict\"] = test_dataset\n", + "\n", + " else:\n", + " raise ValueError(f\"Invalid stage: {stage}\")\n", + "\n", + " def train_dataloader(self):\n", + " return DataLoader(\n", + " self.datasets[\"train\"],\n", + " batch_size=self.batch_size,\n", + " num_workers=self.num_workers,\n", + " shuffle=True,\n", + " )\n", + "\n", + " def val_dataloader(self):\n", + " return DataLoader(\n", + " self.datasets[\"val\"],\n", + " batch_size=self.batch_size,\n", + " num_workers=self.num_workers,\n", + " shuffle=False,\n", + " )\n", + "\n", + " def test_dataloader(self):\n", + " return DataLoader(\n", + " self.datasets[\"test\"],\n", + " batch_size=self.batch_size,\n", + " num_workers=self.num_workers,\n", + " shuffle=False,\n", + " )\n", + "\n", + " def predict_dataloader(self):\n", + " return DataLoader(\n", + " self.datasets[\"predict\"],\n", + " batch_size=self.batch_size,\n", + " num_workers=self.num_workers,\n", + " shuffle=False,\n", + " )\n", + "\n", + "\n", + "data_module = F3DataModule(\n", + " train_path=train_path,\n", + " annotations_path=annotation_path,\n", + " transforms=transform,\n", + " batch_size=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Our Model" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "model = SETR_PUP(\n", + " image_size=(256, 704),\n", + " num_classes=6,\n", + " train_metrics={\"mIoU\": JaccardIndex(task=\"multiclass\", num_classes=6)},\n", + " val_metrics={\"mIoU\": JaccardIndex(task=\"multiclass\", num_classes=6)},\n", + " test_metrics={\"mIoU\": JaccardIndex(task=\"multiclass\", num_classes=6)},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Our Trainer" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "GPU available: True (cuda), used: True\n", + "TPU available: False, using: 0 TPU cores\n", + "IPU available: False, using: 0 IPUs\n", + "HPU available: False, using: 0 HPUs\n" + ] + } + ], + "source": [ + "trainer = L.Trainer(\n", + " max_epochs=5,\n", + " accelerator=\"gpu\",\n", + " devices=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Our Pipeline and model's training" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Log directory set to: /workspaces/seismic/Framework-SSL/docs/notebooks/lightning_logs/version_3\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + " | Name | Type | Params\n", + "---------------------------------------------\n", + "0 | loss_fn | CrossEntropyLoss | 0 \n", + "1 | model | _SetR_PUP | 320 M \n", + "---------------------------------------------\n", + "320 M Trainable params\n", + "0 Non-trainable params\n", + "320 M Total params\n", + "1,281.382 Total estimated model params size (MB)\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8c10960121ac41d5b2e7e669e4d60c0e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Sanity Checking: | | 0/? [00:00 Date: Wed, 29 May 2024 10:41:25 +0000 Subject: [PATCH 09/11] Added tests to all models added Signed-off-by: Otavio Napoli --- minerva/models/nets/multi_channel_cnn.py | 164 ---------------------- tests/models/nets/test_cnn_ha_etal.py | 21 +++ tests/models/nets/test_cnn_pf.py | 21 +++ tests/models/nets/test_deep_conv_lstm.py | 12 ++ tests/models/nets/test_imu_transformer.py | 21 +++ tests/models/nets/test_inception_time.py | 11 ++ tests/models/nets/test_resnet_1d.py | 42 ++++++ tests/models/nets/test_setr.py | 6 +- 8 files changed, 131 insertions(+), 167 deletions(-) delete mode 100644 minerva/models/nets/multi_channel_cnn.py create mode 100644 tests/models/nets/test_cnn_ha_etal.py create mode 100644 tests/models/nets/test_cnn_pf.py create mode 100644 tests/models/nets/test_deep_conv_lstm.py create mode 100644 tests/models/nets/test_imu_transformer.py create mode 100644 tests/models/nets/test_inception_time.py create mode 100644 tests/models/nets/test_resnet_1d.py diff --git a/minerva/models/nets/multi_channel_cnn.py b/minerva/models/nets/multi_channel_cnn.py deleted file mode 100644 index bc924c7..0000000 --- a/minerva/models/nets/multi_channel_cnn.py +++ /dev/null @@ -1,164 +0,0 @@ -from typing import Dict, Tuple -import torch -import lightning as L -from torchmetrics import Accuracy - -from minerva.models.nets.base import SimpleSupervisedModel - - -class _MultiChannelCNN_HAR(torch.nn.Module): - def __init__(self, input_channels: int = 1, concatenate: bool = True): - super().__init__() - - self.freq_encoder = torch.nn.Sequential( - torch.nn.Conv2d( - input_channels, 16, kernel_size=2, stride=1, padding="same" - ), - torch.nn.ReLU(), - torch.nn.Conv2d(16, 16, kernel_size=2, stride=1, padding="same"), - torch.nn.ReLU(), - torch.nn.MaxPool2d(2), - ) - - self.welch_encoder = torch.nn.Sequential( - torch.nn.Conv2d( - input_channels, 16, kernel_size=2, stride=1, padding="same" - ), - torch.nn.ReLU(), - torch.nn.Conv2d(16, 16, kernel_size=2, stride=1, padding="same"), - torch.nn.ReLU(), - torch.nn.MaxPool2d(2), - ) - - self.concatenate = concatenate - - def forward(self, x): - # Input is a 5D tensor (Batch, Transformed, Channel, H, W) - # X[:, 0, :, :, :] --> Frequency (1, 6, 60) - # X[:, 1, :, :, :] --> Welch (1, 6, 60) - freq_out = self.freq_encoder(x[:, 0, :, :, :]) - welch_out = self.welch_encoder(x[:, 1, :, :, :]) - if not self.concatenate: - return freq_out, welch_out - else: - return torch.cat([freq_out, welch_out], dim=1) - - -class MultiChannelCNN_HAR(SimpleSupervisedModel): - def __init__( - self, - input_shape: Tuple[int, int, int] = (1, 6, 60), - num_classes: int = 6, - learning_rate: float = 1e-3, - ): - """Create a simple 1D Convolutional Network with 3 layers and 2 fully - connected layers. - - Parameters - ---------- - input_shape : Tuple[int, int], optional - A 2-tuple containing the number of input channels and the number of - features, by default (6, 60). - num_classes : int, optional - Number of output classes, by default 6 - learning_rate : float, optional - Learning rate for Adam optimizer, by default 1e-3 - """ - self.input_shape = input_shape - self.num_classes = num_classes - - backbone = self._create_backbone(input_channels=input_shape[0]) - self.fc_input_channels = self._calculate_fc_input_features( - backbone, input_shape - ) - fc = self._create_fc(self.fc_input_channels, num_classes) - super().__init__( - backbone=backbone, - fc=fc, - learning_rate=learning_rate, - flatten=True, - loss_fn=torch.nn.CrossEntropyLoss(), - val_metrics={ - "acc": Accuracy(task="multiclass", num_classes=num_classes) - }, - test_metrics={ - "acc": Accuracy(task="multiclass", num_classes=num_classes) - }, - ) - - def _create_backbone(self, input_channels: int) -> torch.nn.Module: - return _MultiChannelCNN_HAR(input_channels=input_channels) - - def _calculate_fc_input_features( - self, backbone: torch.nn.Module, input_shape: Tuple[int, int] - ) -> int: - """Run a single forward pass with a random input to get the number of - features after the convolutional layers. - - Parameters - ---------- - backbone : torch.nn.Module - The backbone of the network - input_shape : Tuple[int, int] - The input shape of the network. - - Returns - ------- - int - The number of features after the convolutional layers. - """ - random_input = torch.randn(1, 2, *input_shape) - with torch.no_grad(): - out = backbone(random_input) - return out.view(out.size(0), -1).size(1) - - def _create_fc( - self, input_features: int, num_classes: int - ) -> torch.nn.Module: - return torch.nn.Sequential( - torch.nn.Linear(input_features, num_classes), - torch.nn.ReLU(), - ) - - -# def test_multichannel_cnn(): -# from ssl_tools.transforms.signal_1d import FFT, WelchPowerSpectralDensity -# from ssl_tools.transforms.utils import ( -# PerChannelTransform, -# StackComposer, -# Cast, -# ) -# from ssl_tools.models.utils import RandomDataModule - -# input_shape = (1, 6, 60) - -# fft_transform = PerChannelTransform(FFT(absolute=True)) -# welch = PerChannelTransform( -# WelchPowerSpectralDensity( -# fs=1 / 20, return_onesided=False, absolute=True -# ) -# ) -# stacker = StackComposer([fft_transform, welch]) - -# data_module = RandomDataModule( -# num_samples=8, -# num_classes=6, -# input_shape=input_shape, -# batch_size=8, -# transforms=[stacker, Cast("float32")], -# ) - -# model = MultiChannelCNN_HAR( -# input_shape=input_shape, num_classes=6, learning_rate=1e-3 -# ) -# print(model) - -# trainer = L.Trainer( -# max_epochs=1, logger=False, devices=1, accelerator="cpu" -# ) - -# trainer.fit(model, datamodule=data_module) - - -# if __name__ == "__main__": -# test_multichannel_cnn() diff --git a/tests/models/nets/test_cnn_ha_etal.py b/tests/models/nets/test_cnn_ha_etal.py new file mode 100644 index 0000000..8678624 --- /dev/null +++ b/tests/models/nets/test_cnn_ha_etal.py @@ -0,0 +1,21 @@ +import torch +from minerva.models.nets.cnn_ha_etal import CNN_HaEtAl_1D, CNN_HaEtAl_2D + +def test_cnn_ha_etal_1d_forward(): + input_shape = (1, 6, 60) + model = CNN_HaEtAl_1D(input_shape=input_shape) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None + + +def test_cnn_ha_etal_2d_forward(): + input_shape = (1, 6, 60) + model = CNN_HaEtAl_2D(input_shape=input_shape) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None \ No newline at end of file diff --git a/tests/models/nets/test_cnn_pf.py b/tests/models/nets/test_cnn_pf.py new file mode 100644 index 0000000..f8656e9 --- /dev/null +++ b/tests/models/nets/test_cnn_pf.py @@ -0,0 +1,21 @@ +import torch +from minerva.models.nets.cnn_pf import CNN_PF_2D, CNN_PFF_2D + +def test_cnn_pf_forward(): + input_shape = (1, 6, 60) + model = CNN_PF_2D(input_shape=input_shape, pad_at=3) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None + + +def test_cnn_ha_pff_forward(): + input_shape = (1, 6, 60) + model = CNN_PFF_2D(input_shape=input_shape, pad_at=3) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None \ No newline at end of file diff --git a/tests/models/nets/test_deep_conv_lstm.py b/tests/models/nets/test_deep_conv_lstm.py new file mode 100644 index 0000000..5282762 --- /dev/null +++ b/tests/models/nets/test_deep_conv_lstm.py @@ -0,0 +1,12 @@ +import torch +from minerva.models.nets.deep_conv_lstm import DeepConvLSTM + +def test_deep_conv_lstm_forward(): + input_shape = (1, 6, 60) + model = DeepConvLSTM(input_shape=input_shape) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None + diff --git a/tests/models/nets/test_imu_transformer.py b/tests/models/nets/test_imu_transformer.py new file mode 100644 index 0000000..60f7eef --- /dev/null +++ b/tests/models/nets/test_imu_transformer.py @@ -0,0 +1,21 @@ +import torch +from minerva.models.nets.imu_transformer import IMUTransformerEncoder, IMUCNN + +def test_imu_transformer_forward(): + input_shape = (6, 60) + model = IMUTransformerEncoder(input_shape=input_shape) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None + + +def test_imu_cnn_forward(): + input_shape = (6, 60) + model = IMUCNN(input_shape=input_shape) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None \ No newline at end of file diff --git a/tests/models/nets/test_inception_time.py b/tests/models/nets/test_inception_time.py new file mode 100644 index 0000000..058c48f --- /dev/null +++ b/tests/models/nets/test_inception_time.py @@ -0,0 +1,11 @@ +import torch +from minerva.models.nets.inception_time import InceptionTime + +def test_inception_time_forward(): + input_shape = (6, 60) + model = InceptionTime(input_shape=input_shape) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None diff --git a/tests/models/nets/test_resnet_1d.py b/tests/models/nets/test_resnet_1d.py new file mode 100644 index 0000000..885262c --- /dev/null +++ b/tests/models/nets/test_resnet_1d.py @@ -0,0 +1,42 @@ +import torch +from minerva.models.nets.resnet_1d import ResNet1D_8, ResNetSE1D_8, ResNetSE1D_5 + +def test_resnet_1d_8_forward(): + input_shape = (6, 60) + model = ResNet1D_8( + input_shape=input_shape, + num_classes=6, + learning_rate=1e-3, + ) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None + + +def test_resnet_se_1d_8_forward(): + input_shape = (6, 60) + model = ResNetSE1D_8( + input_shape=input_shape, + num_classes=6, + learning_rate=1e-3, + ) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None + +def test_resnet_se_1d_5_forward(): + input_shape = (6, 60) + model = ResNetSE1D_5( + input_shape=input_shape, + num_classes=6, + learning_rate=1e-3, + ) + assert model is not None + + x = torch.rand(1, *input_shape) + y = model(x) + assert y is not None \ No newline at end of file diff --git a/tests/models/nets/test_setr.py b/tests/models/nets/test_setr.py index 093f4de..6e18b27 100644 --- a/tests/models/nets/test_setr.py +++ b/tests/models/nets/test_setr.py @@ -31,6 +31,6 @@ def test_setr_predict(): ), f"Expected shape {mask_shape}, but got {preds[0].shape}" -if __name__ == "__main__": - test_setr_loss() - test_setr_predict() +# if __name__ == "__main__": +# test_setr_loss() +# test_setr_predict() From fc73fdc79f3a4313f1b8bc29b929a6422c3d3ae2 Mon Sep 17 00:00:00 2001 From: Otavio Napoli Date: Wed, 29 May 2024 10:43:30 +0000 Subject: [PATCH 10/11] Removed layers folder and GRU Encoder Signed-off-by: Otavio Napoli --- minerva/models/layers/__init__.py | 0 minerva/models/layers/gru.py | 111 ------------------------------ 2 files changed, 111 deletions(-) delete mode 100644 minerva/models/layers/__init__.py delete mode 100644 minerva/models/layers/gru.py diff --git a/minerva/models/layers/__init__.py b/minerva/models/layers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/minerva/models/layers/gru.py b/minerva/models/layers/gru.py deleted file mode 100644 index e0d40bb..0000000 --- a/minerva/models/layers/gru.py +++ /dev/null @@ -1,111 +0,0 @@ -import torch - - -class GRUEncoder(torch.nn.Module): - def __init__( - self, - hidden_size: int = 100, - in_channels: int = 6, - encoding_size: int = 10, - num_layers: int = 1, - dropout: float = 0.0, - bidirectional: bool = True, - ): - """Gate Recurrent Unit (GRU) Encoder. - This class is a wrapper for the GRU layer (torch.nn.GRU) followed by a - linear layer, in order to obtain a fixed-size encoding of the input - sequence. - - The input sequence is expected to be of shape - [batch_size, in_channel, seq_len]. - For instance, for HAR data in MotionSense Dataset: - - in_channel = 6 (3 for accelerometer and 3 for gyroscope); and - - seq_len = 60 (the number of time steps). - - In forward pass, the input sequence is permuted to - [seq_len, batch_size, in_channel] before being fed to the GRU layer. - The output of forward pass is the encoding of shape - [batch_size, encoding_size]. - - Parameters - ---------- - hidden_size : int, optional - The number of features in the hidden state of the GRU, - by default 100 - in_channel: int, optional - The number of input features (e.g. 6 for HAR data in MotionSense - Dataset), by default 6 - encoding_size : int, optional - Size of the encoding (output of the linear layer). - num_layers : int, optional - Number of recurrent layers. E.g., setting ``num_layers=2`` - would mean stacking two GRUs together to form a `stacked GRU`, - with the second GRU taking in outputs of the first GRU and - computing the final results. By default 1 - dropout : float, optional - If non-zero, introduces a `Dropout` layer on the outputs of each - GRU layer except the last layer, with dropout probability equal to - :attr:`dropout`. Default: 0 - bidirectional : bool, optional - If ``True``, becomes a bidirectional GRU, by default True - """ - super().__init__() - - # Parameters - self.hidden_size = hidden_size - self.in_channel = in_channels - self.num_layers = num_layers - self.encoding_size = encoding_size - self.bidirectional = bidirectional - # If bidirectional is true, the number of directions is 2 - self.num_directions = 2 if bidirectional else 1 - - # Instantiate the GRU layer - self.rnn = torch.nn.GRU( - input_size=self.in_channel, - hidden_size=self.hidden_size, - num_layers=num_layers, - batch_first=False, - dropout=dropout, - bidirectional=bidirectional, - ) - - # Instantiate the linear leayer - # If bidirectional is true, the input of linear layer is - # hidden_size * 2 (because the output of GRU is concatenated) - # Otherwise, the input of linear layer is hidden_size - self.nn = torch.nn.Linear( - self.hidden_size * self.num_directions, self.encoding_size - ) - - def forward(self, x): - # Permute the input sequence from [batch_size, in_channel, seq_len] - # to [seq_len, batch_size, in_channel] - x = x.permute(2, 0, 1) - - # The initial hidden state (h0) is set to zeros of shape - # [num_layers * num_directions, batch_size, hidden_size] - initial_state = torch.zeros( - self.num_layers * self.num_directions, # initial_state.shape[0] - x.shape[1], # initial_state.shape[1] - self.hidden_size, # initial_state.shape[2] - device=x.device, - # requires_grad=False # This is not a learnable parameter - ) - - # Forward pass of the GRU layer - # out shape = [seq_len, batch_size, num_directions*hidden_size] - out, _ = self.rnn( - x, initial_state - ) - - # Pick the last state returned by the GRU of shape - # [batch_size, num_directions*hidden_size] and squeeze it (remove the - # first dimension if the size is 1) - out = out[-1].squeeze(0) - - # Pass the output of GRU to the linear layer to obtain the encodings - encodings = self.nn(out) - - # encodings shape = [batch_size, encoding_size] - return encodings From 1200bc458433c8d3cceaef6b6c1da3f1285721dd Mon Sep 17 00:00:00 2001 From: Otavio Napoli Date: Wed, 29 May 2024 10:50:30 +0000 Subject: [PATCH 11/11] Removed Deep Conv LSTM as tests fails in GPU Signed-off-by: Otavio Napoli --- minerva/models/nets/deep_conv_lstm.py | 165 ----------------------- tests/models/nets/test_deep_conv_lstm.py | 12 -- 2 files changed, 177 deletions(-) delete mode 100644 minerva/models/nets/deep_conv_lstm.py delete mode 100644 tests/models/nets/test_deep_conv_lstm.py diff --git a/minerva/models/nets/deep_conv_lstm.py b/minerva/models/nets/deep_conv_lstm.py deleted file mode 100644 index 45eda29..0000000 --- a/minerva/models/nets/deep_conv_lstm.py +++ /dev/null @@ -1,165 +0,0 @@ -from typing import Tuple -import torch -from torchmetrics import Accuracy - - -from minerva.models.nets.base import SimpleSupervisedModel - - -# Implementation of DeepConvLSTM as described in the paper: -# Deep Convolutional and LSTM Recurrent Neural Networks for Multimodal Wearable -# Activity Recognition (http://www.mdpi.com/1424-8220/16/1/115/html) - - -class ConvLSTMCell(torch.nn.Module): - def __init__(self, input_shape: tuple): - super().__init__() - self.input_shape = input_shape - self.conv_block = torch.nn.Sequential( - torch.nn.Conv2d( - in_channels=input_shape[0], - out_channels=64, - kernel_size=(1, 5), - stride=(1, 1), - ), - torch.nn.Conv2d( - in_channels=64, - out_channels=64, - kernel_size=(1, 5), - stride=(1, 1), - ), - torch.nn.Conv2d( - in_channels=64, - out_channels=64, - kernel_size=(1, 5), - stride=(1, 1), - ), - torch.nn.Conv2d( - in_channels=64, - out_channels=64, - kernel_size=(1, 5), - stride=(1, 1), - ), - ) - - self.lstm_input_size = self._calculate_conv_output_shape( - self.conv_block, input_shape - ) - - self.lstm_1 = torch.nn.LSTM( - input_size=self.lstm_input_size, hidden_size=128, batch_first=True - ) - self.lstm_2 = torch.nn.LSTM( - input_size=128, hidden_size=128, batch_first=True - ) - - def _calculate_conv_output_shape( - self, backbone, input_shape: Tuple[int, int, int] - ) -> int: - random_input = torch.randn(1, *input_shape) - with torch.no_grad(): - out = backbone(random_input) - return out.view(out.size(0), -1).size(1) - - def forward(self, x): - x = self.conv_block(x) - x = x.reshape(x.size(0), x.size(1), -1) - x, _ = self.lstm_1(x) - x, _ = self.lstm_2(x) - return x - - -class DeepConvLSTM(SimpleSupervisedModel): - def __init__( - self, - input_shape: Tuple[int, int, int] = (1, 6, 60), - num_classes: int = 6, - learning_rate: float = 1e-3, - ): - self.input_shape = input_shape - self.num_classes = num_classes - - backbone = self._create_backbone(input_shape=input_shape) - self.fc_input_channels = self._calculate_fc_input_features( - backbone, input_shape - ) - fc = self._create_fc(self.fc_input_channels, num_classes) - super().__init__( - backbone=backbone, - fc=fc, - learning_rate=learning_rate, - flatten=True, - loss_fn=torch.nn.CrossEntropyLoss(), - # val_metrics={ - # "acc": Accuracy(task="multiclass", num_classes=num_classes) - # }, - # test_metrics={ - # "acc": Accuracy(task="multiclass", num_classes=num_classes) - # }, - ) - - def _create_backbone(self, input_shape: Tuple[int, int]) -> torch.nn.Module: - return ConvLSTMCell(input_shape=input_shape) - - def _calculate_fc_input_features( - self, backbone: torch.nn.Module, input_shape: Tuple[int, int, int] - ) -> int: - """Run a single forward pass with a random input to get the number of - features after the convolutional layers. - - Parameters - ---------- - backbone : torch.nn.Module - The backbone of the network - input_shape : Tuple[int, int, int] - The input shape of the network. - - Returns - ------- - int - The number of features after the convolutional layers. - """ - random_input = torch.randn(1, *input_shape) - with torch.no_grad(): - out = backbone(random_input) - return out.view(out.size(0), -1).size(1) - - def _create_fc( - self, input_features: int, num_classes: int - ) -> torch.nn.Module: - return torch.nn.Sequential( - torch.nn.Linear( - in_features=input_features, out_features=num_classes - ), - torch.nn.ReLU(), - ) - - -# def test_deep_conv_lstm(): -# input_shape = (1, 6, 60) - -# data_module = RandomDataModule( -# num_samples=8, -# num_classes=6, -# input_shape=input_shape, -# batch_size=8, -# ) - -# model = DeepConvLSTM( -# input_shape=input_shape, num_classes=6, learning_rate=1e-3 -# ) -# print(model) - -# trainer = L.Trainer( -# max_epochs=1, logger=False, devices=1, accelerator="cpu" -# ) - -# trainer.fit(model, datamodule=data_module) - - -# if __name__ == "__main__": -# import logging -# logging.getLogger("lightning.pytorch").setLevel(logging.ERROR) -# logging.getLogger("lightning").setLevel(logging.ERROR) -# logging.getLogger("lightning.pytorch.core").setLevel(logging.ERROR) -# test_deep_conv_lstm() diff --git a/tests/models/nets/test_deep_conv_lstm.py b/tests/models/nets/test_deep_conv_lstm.py deleted file mode 100644 index 5282762..0000000 --- a/tests/models/nets/test_deep_conv_lstm.py +++ /dev/null @@ -1,12 +0,0 @@ -import torch -from minerva.models.nets.deep_conv_lstm import DeepConvLSTM - -def test_deep_conv_lstm_forward(): - input_shape = (1, 6, 60) - model = DeepConvLSTM(input_shape=input_shape) - assert model is not None - - x = torch.rand(1, *input_shape) - y = model(x) - assert y is not None -