Compare commits

..

69 Commits

Author SHA1 Message Date
Alexander Engelsberger
391473adf3 build: bump version 0.7.5 → 0.7.6 2023-10-04 14:47:27 +02:00
Alexander Engelsberger
0d8db31ff2
ci: update python versions 2023-06-20 16:34:41 +02:00
Alexander Engelsberger
89b96f0a98
chore: switch to pytorch 2.0+ 2023-06-20 16:27:54 +02:00
Alexander Engelsberger
ee4cf583e3
chore: fix minor errors and upgrade codebase 2023-06-20 16:06:53 +02:00
Alexander Engelsberger
6ed1b9a832
feat: add gmlvq example
it was necessary to update the pre-commit definition for a successfull
commit.
2023-06-20 15:12:32 +02:00
Alexander Engelsberger
4a7d4a3d99
chore(ci): update github actions 2022-12-05 17:14:54 +01:00
Alexander Engelsberger
0626af207f
build: bump version 0.7.4 → 0.7.5 2022-12-05 17:03:04 +01:00
rmschubert
7b23983887 fix: update scikit-learn dependency 2022-12-05 16:48:22 +01:00
Alexander Engelsberger
0649d5bb45
build: bump version 0.7.3 → 0.7.4 2022-05-17 11:57:32 +02:00
Alexander Engelsberger
339316aa7e
fix: use epsilon in cbc competition 2022-05-17 11:56:43 +02:00
Alexander Engelsberger
2a85c94b55
chore: minor changes and version updates 2022-05-17 11:56:18 +02:00
Alexander Engelsberger
6714cb7915 ci: add python 3.10 as supported python version 2022-04-27 09:56:06 +02:00
Alexander Engelsberger
a501ab6c3b build: bump version 0.7.2 → 0.7.3 2022-04-27 09:49:50 +02:00
Alexander Engelsberger
37add944b1 chore: merge dev into master 2022-04-27 09:48:58 +02:00
Jensun Ravichandran
0d10fc7e25
fix: correct typo 2022-04-04 21:50:22 +02:00
Jensun Ravichandran
71a2e74eff
feat: add RandomLinearTransformInitializer 2022-04-04 20:55:03 +02:00
Jensun Ravichandran
85f75bb28c
feat: add repr for LinearTransform 2022-04-01 10:13:25 +02:00
Alexander Engelsberger
46ff1c4eb1 fix: forward of LinearTransform uses undetached weights now 2022-03-29 17:07:17 +02:00
Jensun Ravichandran
ed5b9b6c62
feat: warn user when component counts do not match 2022-03-29 14:39:41 +02:00
Jensun Ravichandran
08b3f9bbb9
feat: add LiteralLinearTransformInitializer 2022-03-21 14:38:00 +01:00
Jensun Ravichandran
784a963527
chore: housekeeping 2022-03-10 14:46:56 +01:00
Jensun Ravichandran
236cbbc4d2
feat: add color utils 2022-03-10 14:45:55 +01:00
Jensun Ravichandran
695559fd4a
fix: incorrect variable names in GLVQLoss.forward 2022-03-09 13:20:00 +01:00
Jensun Ravichandran
a54acdef22
feat: update GLVQLoss to include a regularization term 2022-02-15 17:16:44 +01:00
Jensun Ravichandran
bebd13868f
fix: typo fix 2022-02-03 23:29:47 +01:00
Jensun Ravichandran
62df3c0457
feat: raise initializer error on unavailable data 2022-01-31 12:27:48 +01:00
Alexander Engelsberger
cce76c7940
build: bump version 0.7.1 → 0.7.2 2022-01-10 20:32:32 +01:00
Alexander Engelsberger
ca24422ab0
chore: reorganize setup.cfg 2022-01-10 20:32:29 +01:00
Alexander Engelsberger
a28601751e
Use github actions for CI (#10)
* chore: Absolute imports

* feat: Add new mesh util

* chore: replace bumpversion

original fork no longer maintained, move config

* ci: remove old configuration files

* ci: update github action

* ci: add python 3.10 test

* chore: update pre-commit hooks

* ci: update supported python versions

supported are 3.7, 3.8 and 3.9.

3.6 had EOL in december 2021.
3.10 has no pytorch distribution yet.

* ci: add windows test

* ci: update action

less windows tests, pre commit

* ci: fix typo

* chore: run precommit for all files

* ci: two step tests

* ci: compatibility waits for style

* fix: init file had missing imports

* ci: add deployment script

* ci: skip complete publish step

* ci: cleanup readme
2022-01-10 20:23:18 +01:00
Alexander Engelsberger
07a2d6caaa
feat: Add new mesh util 2021-10-15 13:08:19 +02:00
Alexander Engelsberger
3d3d27fbab
chore: Absolute imports 2021-10-15 13:07:08 +02:00
Alexander Engelsberger
b49b7a2d41
build: bump version 0.7.0 → 0.7.1 2021-08-30 17:55:48 +02:00
Alexander Engelsberger
b6e8242383
ci: add build phase for tags 2021-08-30 17:55:32 +02:00
Alexander Engelsberger
cd616d11b9
build: bump version 0.6.0 → 0.7.0 2021-08-30 17:42:27 +02:00
Alexander Engelsberger
afcfcb8973
fix: setup.py tags 2021-08-30 17:42:22 +02:00
Alexander Engelsberger
bf03a45475 feat(compatibility): Python3.6 compatibility 2021-08-30 17:39:10 +02:00
Alexander Engelsberger
083b5c1597 feat(compatibility): Python3.7 compatibility 2021-08-30 17:39:10 +02:00
Alexander Engelsberger
7f0a8e9bce feat(compatibility): Python3.8 compatibility 2021-08-30 17:39:10 +02:00
Jensun Ravichandran
bf09ff8f7f
feat: add XOR dataset 2021-07-15 18:14:38 +02:00
Jensun Ravichandran
c1d7cfee8f
fix(test): fix broken CSVDataset test 2021-07-06 17:07:26 +02:00
Jensun Ravichandran
99be965581
refactor: refactor GLVQLoss 2021-07-06 17:01:28 +02:00
Jensun Ravichandran
fdb9a7c66d
feat: add CSVDataset 2021-07-04 16:30:01 +02:00
Jensun Ravichandran
eb79b703d8
chore(github): update bug report issue template 2021-06-22 15:06:18 +02:00
Jensun Ravichandran
bc9a826b7d
fix: matmul bug in 2021-06-21 22:48:22 +02:00
Alexander Engelsberger
cfe09ec06b
fix: reasonings init parameters are used now 2021-06-21 14:53:22 +02:00
Alexander Engelsberger
3d76dffe3c
chore: Allow no-self-use for some class members
Classes are used as common interface and connection to pytorch.
2021-06-21 14:29:25 +02:00
Jensun Ravichandran
597c9fc1ee build: bump version 0.5.1 → 0.6.0 2021-06-20 19:12:01 +02:00
Jensun Ravichandran
a8c74a1a6f chore(bumpversion): modify bump message 2021-06-20 19:09:35 +02:00
Jensun Ravichandran
f78ff1a464 fix(initializers): bug fixes in LT initializers 2021-06-20 18:56:06 +02:00
Jensun Ravichandran
5a3dbfac2e chore(pre-commit): prettify .pre-commit-config.yaml 2021-06-20 18:54:37 +02:00
Jensun Ravichandran
478a3c2cfe fix: python is python3.9 2021-06-20 17:49:53 +02:00
Jensun Ravichandran
4520fdde8e chore(travis): point build badge to travis-ci.com 2021-06-18 19:27:28 +02:00
Jensun Ravichandran
b90044b86c fix: python is python3.9 2021-06-18 19:20:54 +02:00
Jensun Ravichandran
a1310df4ee test(datasets): turn off tecator tests temporarily 2021-06-18 19:10:29 +02:00
Jensun Ravichandran
5dc66494ea refactor(api)!: merge the new api changes into dev
BREAKING CHANGE: remove the following
`prototorch/functions/*`
`prototorch/components/*`
`prototorch/modules/*`
BREAKING CHANGE: move `initializers` into the `prototorch.initializers`
namespace from the `prototorch.components` namespace
BREAKING CHANGE: `functions` and `modules` and moved into `core` and `nn`
2021-06-18 18:54:55 +02:00
Jensun Ravichandran
74d420a77d refactor(api)!: merge the new api changes into dev
BREAKING CHANGE: remove the following
`prototorch/functions/*`
`prototorch/components/*`
`prototorch/modules/*`
BREAKING CHANGE: move `initializers` into the `prototorch.initializers`
namespace from the `prototorch.components` namespace
BREAKING CHANGE: `functions` and `modules` and moved into `core` and `nn`
2021-06-18 18:20:30 +02:00
Jensun Ravichandran
6ffd14e85c Bump version: 0.5.0 → 0.5.1 2021-06-18 15:49:20 +02:00
Jensun Ravichandran
40c1021c20 Remove examples 2021-06-18 13:41:03 +02:00
Jensun Ravichandran
acf3272fd7 Remove .swp files 2021-06-18 13:39:43 +02:00
danielstaps
c73f8e7a28
Added PCA initializer and component for OmegaMatrix or LinearMappings (#6)
* Added PCA initializer and component for OmegaMatrix or LinearMappings

* [QA] Add default configuration for pre commit hooks

* [QA] Add more pre commit checks

* [QA] Add more pre commit checks

* test(githooks): Add gitlint to check commit messages on commit

* docs(githooks): Add usage guide for pre-commit  to readme

* fix(githooks): mypy only checks source now

reverts changes on docs conf.py

* docs(githooks): Fix typo

Co-authored-by: staps@hs-mittweida.de <staps@hs-mittweida.de>
Co-authored-by: Alexander Engelsberger <alexanderengelsberger@gmail.com>
2021-06-18 13:28:25 +02:00
Alexander Engelsberger
bf23d5f7f8 docs(githooks): Fix typo 2021-06-16 15:23:23 +02:00
Alexander Engelsberger
bcde3f6ac8 fix(githooks): mypy only checks source now
reverts changes on docs conf.py
2021-06-16 15:23:23 +02:00
Alexander Engelsberger
d5229b1750 docs(githooks): Add usage guide for pre-commit to readme 2021-06-16 15:23:23 +02:00
Alexander Engelsberger
fc4b143fbb test(githooks): Add gitlint to check commit messages on commit 2021-06-16 15:23:23 +02:00
Alexander Engelsberger
11cfa79746 [QA] Add more pre commit checks 2021-06-16 15:23:23 +02:00
Alexander Engelsberger
d0ae94f2af [QA] Add more pre commit checks 2021-06-16 15:23:23 +02:00
Alexander Engelsberger
2c908a8361 [QA] Add default configuration for pre commit hooks 2021-06-16 15:23:23 +02:00
Alexander Engelsberger
e4257ec1f1
Merge branch 'dev' of github.com:si-cim/prototorch into dev 2021-06-11 16:10:04 +02:00
Alexander Engelsberger
aaad2b8626
[BUGFIX] Fix labeled components if initialized 2021-06-11 16:09:51 +02:00
41 changed files with 923 additions and 877 deletions

View File

@ -1,10 +1,10 @@
[bumpversion] [bumpversion]
current_version = 0.5.0 current_version = 0.7.6
commit = True commit = True
tag = True tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
serialize = serialize = {major}.{minor}.{patch}
{major}.{minor}.{patch} message = build: bump version {current_version} → {new_version}
[bumpversion:file:setup.py] [bumpversion:file:setup.py]

View File

@ -1,15 +0,0 @@
# To validate the contents of your configuration file
# run the following command in the folder where the configuration file is located:
# codacy-analysis-cli validate-configuration --directory `pwd`
# To analyse, run:
# codacy-analysis-cli analyse --tool remark-lint --directory `pwd`
---
engines:
pylintpython3:
exclude_paths:
- config/engines.yml
remark-lint:
exclude_paths:
- config/engines.yml
exclude_paths:
- 'tests/**'

View File

@ -1,2 +0,0 @@
comment:
require_changes: yes

View File

@ -10,21 +10,28 @@ assignees: ''
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
**To Reproduce** **Steps to reproduce the behavior**
Steps to reproduce the behavior: 1. ...
1. Install Prototorch by running '...' 2. Run script '...' or this snippet:
2. Run script '...' ```python
import prototorch as pt
...
```
3. See errors 3. See errors
**Expected behavior** **Expected behavior**
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
**Observed behavior**
A clear and concise description of what actually happened.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **System and version information**
- OS: [e.g. Ubuntu 20.10] - OS: [e.g. Ubuntu 20.10]
- Prototorch Version: [e.g. v0.4.0] - ProtoTorch Version: [e.g. 0.4.0]
- Python Version: [e.g. 3.9.5] - Python Version: [e.g. 3.9.5]
**Additional context** **Additional context**

View File

@ -5,33 +5,71 @@ name: tests
on: on:
push: push:
branches: [ master, dev ]
pull_request: pull_request:
branches: [ master ] branches: [master]
jobs: jobs:
build: style:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python 3.8 - name: Set up Python 3.11
uses: actions/setup-python@v1 uses: actions/setup-python@v4
with: with:
python-version: 3.8 python-version: "3.11"
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install .[all] pip install .[all]
- name: Lint with flake8 - uses: pre-commit/action@v3.0.0
compatibility:
needs: style
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
os: [ubuntu-latest, windows-latest]
exclude:
- os: windows-latest
python-version: "3.8"
- os: windows-latest
python-version: "3.9"
- os: windows-latest
python-version: "3.10"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: | run: |
pip install flake8 python -m pip install --upgrade pip
# stop the build if there are Python syntax errors or undefined names pip install .[all]
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest - name: Test with pytest
run: | run: |
pip install pytest
pytest pytest
publish_pypi:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: compatibility
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .[all]
pip install wheel
- name: Build package
run: python setup.py sdist bdist_wheel
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

53
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,53 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-ast
- id: check-case-conflict
- repo: https://github.com/myint/autoflake
rev: v2.1.1
hooks:
- id: autoflake
- repo: http://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
hooks:
- id: mypy
files: prototorch
additional_dependencies: [types-pkg_resources]
- repo: https://github.com/pre-commit/mirrors-yapf
rev: v0.32.0
hooks:
- id: yapf
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: python-use-type-annotations
- id: python-no-log-warn
- id: python-check-blanket-noqa
- repo: https://github.com/asottile/pyupgrade
rev: v3.7.0
hooks:
- id: pyupgrade
- repo: https://github.com/si-cim/gitlint
rev: v0.15.2-unofficial
hooks:
- id: gitlint
args: [--contrib=CT1, --ignore=B6, --msg-filename]

View File

@ -19,7 +19,7 @@ formats: all
# Optionally set the version of Python and requirements required to build your docs # Optionally set the version of Python and requirements required to build your docs
python: python:
version: 3.8 version: 3.9
install: install:
- method: pip - method: pip
path: . path: .

View File

@ -1,36 +0,0 @@
dist: bionic
sudo: false
language: python
python: 3.9
cache:
directories:
- "$HOME/.cache/pip"
- "./tests/artifacts"
- "$HOME/datasets"
install:
- pip install .[all] --progress-bar off
# Generate code coverage report
script:
- coverage run -m pytest
# Push the results to codecov
after_success:
- bash <(curl -s https://codecov.io/bash)
# Publish on PyPI
deploy:
provider: pypi
username: __token__
password:
secure: rVQNCxKIuiEtMz4zLSsjdt6spG7cf3miKN5eqjxZfcELALHxAV4w/+CideQObOn3u9emmxb87R9XWKcogqK2MXqnuIcY4mWg7HUqaip1bhz/4YiVXjFILcG6itjX9IUF1DrtjKKRk6xryucSZcEB7yTcXz1hQTb768KWlLlKOVTRNwr7j07eyeafexz/L2ANQCqfOZgS4b0k2AMeDBRPykPULtyeneEFlb6MJZ2MxeqtTNVK4b/6VsQSZwQ9jGJNGWonn5Y287gHmzvEcymSJogTe2taxGBWawPnOsibws9v88DEAHdsEvYdnqEE3hFl0R5La2Lkjd8CjNUYegxioQ57i3WNS3iksq10ZLMCbH29lb9YPG7r6Y8z9H85735kV2gKLdf+o7SPS03TRgjSZKN6pn4pLG0VWkxC6l8VfLuJnRNTHX4g6oLQwOWIBbxybn9Zw/yLjAXAJNgBHt5v86H6Jfi1Va4AhEV6itkoH9IM3/uDhrE/mmorqyVled/CPNtBWNTyoDevLNxMUDnbuhH0JzLki+VOjKnTxEfq12JB8X9faFG5BjvU9oGjPPewrp5DGGzg6KDra7dikciWUxE1eTFFDhMyG1CFGcjKlDvlAGHyI6Kih35egGUeq+N/pitr2330ftM9Dm4rWpOTxPyCI89bXKssx/MgmLG7kSM=
on:
tags: true
skip_existing: true
# The password is encrypted with:
# `cd prototorch && travis encrypt your-pypi-api-token --add deploy.password`
# See https://docs.travis-ci.com/user/deployment/pypi and
# https://github.com/travis-ci/travis.rb#installation
# for more details
# Note: The encrypt command does not work well in ZSH.

View File

@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2020 si-cim Copyright (c) 2020 Saxon Institute for Computational Intelligence and Machine
Learning (SICIM)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -2,13 +2,9 @@
![ProtoTorch Logo](https://prototorch.readthedocs.io/en/latest/_static/horizontal-lockup.png) ![ProtoTorch Logo](https://prototorch.readthedocs.io/en/latest/_static/horizontal-lockup.png)
[![Build Status](https://travis-ci.org/si-cim/prototorch.svg?branch=master)](https://travis-ci.org/si-cim/prototorch)
![tests](https://github.com/si-cim/prototorch/workflows/tests/badge.svg) ![tests](https://github.com/si-cim/prototorch/workflows/tests/badge.svg)
[![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/si-cim/prototorch?color=yellow&label=version)](https://github.com/si-cim/prototorch/releases) [![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/si-cim/prototorch?color=yellow&label=version)](https://github.com/si-cim/prototorch/releases)
[![PyPI](https://img.shields.io/pypi/v/prototorch)](https://pypi.org/project/prototorch/) [![PyPI](https://img.shields.io/pypi/v/prototorch)](https://pypi.org/project/prototorch/)
[![codecov](https://codecov.io/gh/si-cim/prototorch/branch/master/graph/badge.svg)](https://codecov.io/gh/si-cim/prototorch)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/76273904bf9343f0a8b29cd8aca242e7)](https://www.codacy.com/gh/si-cim/prototorch?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=si-cim/prototorch&amp;utm_campaign=Badge_Grade)
![PyPI - Downloads](https://img.shields.io/pypi/dm/prototorch?color=blue)
[![GitHub license](https://img.shields.io/github/license/si-cim/prototorch)](https://github.com/si-cim/prototorch/blob/master/LICENSE) [![GitHub license](https://img.shields.io/github/license/si-cim/prototorch)](https://github.com/si-cim/prototorch/blob/master/LICENSE)
*Tensorflow users, see:* [ProtoFlow](https://github.com/si-cim/protoflow) *Tensorflow users, see:* [ProtoFlow](https://github.com/si-cim/protoflow)
@ -48,6 +44,23 @@ pip install -e .[all]
The documentation is available at <https://www.prototorch.ml/en/latest/>. Should The documentation is available at <https://www.prototorch.ml/en/latest/>. Should
that link not work try <https://prototorch.readthedocs.io/en/latest/>. that link not work try <https://prototorch.readthedocs.io/en/latest/>.
## Contribution
This repository contains definition for [git hooks](https://githooks.com).
[Pre-commit](https://pre-commit.com) is automatically installed as development
dependency with prototorch or you can install it manually with `pip install
pre-commit`.
Please install the hooks by running:
```bash
pre-commit install
pre-commit install --hook-type commit-msg
```
before creating the first commit.
The commit will fail if the commit message does not follow the specification
provided [here](https://www.conventionalcommits.org/en/v1.0.0/#specification).
## Bibtex ## Bibtex
If you would like to cite the package, please use this: If you would like to cite the package, please use this:

View File

@ -23,7 +23,7 @@ author = "Jensun Ravichandran"
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
# #
release = "0.5.0" release = "0.7.6"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
@ -120,7 +120,7 @@ html_css_files = [
# -- Options for HTMLHelp output ------------------------------------------ # -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = "protoflowdoc" htmlhelp_basename = "prototorchdoc"
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------

100
examples/cbc_iris.py Normal file
View File

@ -0,0 +1,100 @@
"""ProtoTorch CBC example using 2D Iris data."""
import logging
import torch
from matplotlib import pyplot as plt
import prototorch as pt
class CBC(torch.nn.Module):
def __init__(self, data, **kwargs):
super().__init__(**kwargs)
self.components_layer = pt.components.ReasoningComponents(
distribution=[2, 1, 2],
components_initializer=pt.initializers.SSCI(data, noise=0.1),
reasonings_initializer=pt.initializers.PPRI(components_first=True),
)
def forward(self, x):
components, reasonings = self.components_layer()
sims = pt.similarities.euclidean_similarity(x, components)
probs = pt.competitions.cbcc(sims, reasonings)
return probs
class VisCBC2D():
def __init__(self, model, data):
self.model = model
self.x_train, self.y_train = pt.utils.parse_data_arg(data)
self.title = "Components Visualization"
self.fig = plt.figure(self.title)
self.border = 0.1
self.resolution = 100
self.cmap = "viridis"
def on_train_epoch_end(self):
x_train, y_train = self.x_train, self.y_train
_components = self.model.components_layer._components.detach()
ax = self.fig.gca()
ax.cla()
ax.set_title(self.title)
ax.axis("off")
ax.scatter(
x_train[:, 0],
x_train[:, 1],
c=y_train,
cmap=self.cmap,
edgecolor="k",
marker="o",
s=30,
)
ax.scatter(
_components[:, 0],
_components[:, 1],
c="w",
cmap=self.cmap,
edgecolor="k",
marker="D",
s=50,
)
x = torch.vstack((x_train, _components))
mesh_input, xx, yy = pt.utils.mesh2d(x, self.border, self.resolution)
with torch.no_grad():
y_pred = self.model(
torch.Tensor(mesh_input).type_as(_components)).argmax(1)
y_pred = y_pred.cpu().reshape(xx.shape)
ax.contourf(xx, yy, y_pred, cmap=self.cmap, alpha=0.35)
plt.pause(0.2)
if __name__ == "__main__":
train_ds = pt.datasets.Iris(dims=[0, 2])
train_loader = torch.utils.data.DataLoader(train_ds, batch_size=32)
model = CBC(train_ds)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = pt.losses.MarginLoss(margin=0.1)
vis = VisCBC2D(model, train_ds)
for epoch in range(200):
correct = 0.0
for x, y in train_loader:
y_oh = torch.eye(3)[y]
y_pred = model(x)
loss = criterion(y_pred, y_oh).mean(0)
optimizer.zero_grad()
loss.backward()
optimizer.step()
correct += (y_pred.argmax(1) == y).float().sum(0)
acc = 100 * correct / len(train_ds)
logging.info(f"Epoch: {epoch} Accuracy: {acc:05.02f}%")
vis.on_train_epoch_end()

View File

@ -1,120 +0,0 @@
"""ProtoTorch GLVQ example using 2D Iris data."""
import numpy as np
import torch
from matplotlib import pyplot as plt
from prototorch.components import LabeledComponents, StratifiedMeanInitializer
from prototorch.functions.competitions import wtac
from prototorch.functions.distances import euclidean_distance
from prototorch.modules.losses import GLVQLoss
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from torchinfo import summary
# Prepare and preprocess the data
scaler = StandardScaler()
x_train, y_train = load_iris(return_X_y=True)
x_train = x_train[:, [0, 2]]
scaler.fit(x_train)
x_train = scaler.transform(x_train)
# Define the GLVQ model
class Model(torch.nn.Module):
def __init__(self):
"""GLVQ model for training on 2D Iris data."""
super().__init__()
prototype_initializer = StratifiedMeanInitializer([x_train, y_train])
prototype_distribution = {"num_classes": 3, "prototypes_per_class": 3}
self.proto_layer = LabeledComponents(
prototype_distribution,
prototype_initializer,
)
def forward(self, x):
prototypes, prototype_labels = self.proto_layer()
distances = euclidean_distance(x, prototypes)
return distances, prototype_labels
# Build the GLVQ model
model = Model()
# Print summary using torchinfo (might be buggy/incorrect)
print(summary(model))
# Optimize using SGD optimizer from `torch.optim`
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
criterion = GLVQLoss(squashing="sigmoid_beta", beta=10)
x_in = torch.Tensor(x_train)
y_in = torch.Tensor(y_train)
# Training loop
TITLE = "Prototype Visualization"
fig = plt.figure(TITLE)
for epoch in range(70):
# Compute loss
distances, prototype_labels = model(x_in)
loss = criterion([distances, prototype_labels], y_in)
# Compute Accuracy
with torch.no_grad():
predictions = wtac(distances, prototype_labels)
correct = predictions.eq(y_in.view_as(predictions)).sum().item()
acc = 100.0 * correct / len(x_train)
print(
f"Epoch: {epoch + 1:03d} Loss: {loss.item():05.02f} Acc: {acc:05.02f}%"
)
# Optimizer step
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Get the prototypes form the model
prototypes = model.proto_layer.components.numpy()
if np.isnan(np.sum(prototypes)):
print("Stopping training because of `nan` in prototypes.")
break
# Visualize the data and the prototypes
ax = fig.gca()
ax.cla()
ax.set_title(TITLE)
ax.set_xlabel("Data dimension 1")
ax.set_ylabel("Data dimension 2")
cmap = "viridis"
ax.scatter(x_train[:, 0], x_train[:, 1], c=y_train, edgecolor="k")
ax.scatter(
prototypes[:, 0],
prototypes[:, 1],
c=prototype_labels,
cmap=cmap,
edgecolor="k",
marker="D",
s=50,
)
# Paint decision regions
x = np.vstack((x_train, prototypes))
x_min, x_max = x[:, 0].min() - 1, x[:, 0].max() + 1
y_min, y_max = x[:, 1].min() - 1, x[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 1 / 50),
np.arange(y_min, y_max, 1 / 50))
mesh_input = np.c_[xx.ravel(), yy.ravel()]
torch_input = torch.Tensor(mesh_input)
d = model(torch_input)[0]
w_indices = torch.argmin(d, dim=1)
y_pred = torch.index_select(prototype_labels, 0, w_indices)
y_pred = y_pred.reshape(xx.shape)
# Plot voronoi regions
ax.contourf(xx, yy, y_pred, cmap=cmap, alpha=0.35)
ax.set_xlim(left=x_min + 0, right=x_max - 0)
ax.set_ylim(bottom=y_min + 0, top=y_max - 0)
plt.pause(0.1)

76
examples/gmlvq.py Normal file
View File

@ -0,0 +1,76 @@
"""ProtoTorch GMLVQ example using Iris data."""
import torch
import prototorch as pt
class GMLVQ(torch.nn.Module):
"""
Implementation of Generalized Matrix Learning Vector Quantization.
"""
def __init__(self, data, **kwargs):
super().__init__(**kwargs)
self.components_layer = pt.components.LabeledComponents(
distribution=[1, 1, 1],
components_initializer=pt.initializers.SMCI(data, noise=0.1),
)
self.backbone = pt.transforms.Omega(
len(data[0][0]),
len(data[0][0]),
pt.initializers.RandomLinearTransformInitializer(),
)
def forward(self, data):
"""
Forward function that returns a tuple of dissimilarities and label information.
Feed into GLVQLoss to get a complete GMLVQ model.
"""
components, label = self.components_layer()
latent_x = self.backbone(data)
latent_components = self.backbone(components)
distance = pt.distances.squared_euclidean_distance(
latent_x, latent_components)
return distance, label
def predict(self, data):
"""
The GMLVQ has a modified prediction step, where a competition layer is applied.
"""
components, label = self.components_layer()
distance = pt.distances.squared_euclidean_distance(data, components)
winning_label = pt.competitions.wtac(distance, label)
return winning_label
if __name__ == "__main__":
train_ds = pt.datasets.Iris()
train_loader = torch.utils.data.DataLoader(train_ds, batch_size=32)
model = GMLVQ(train_ds)
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
criterion = pt.losses.GLVQLoss()
for epoch in range(200):
correct = 0.0
for x, y in train_loader:
d, labels = model(x)
loss = criterion(d, y, labels).mean(0)
optimizer.zero_grad()
loss.backward()
optimizer.step()
with torch.no_grad():
y_pred = model.predict(x)
correct += (y_pred == y).float().sum(0)
acc = 100 * correct / len(train_ds)
print(f"Epoch: {epoch} Accuracy: {acc:05.02f}%")

View File

@ -1,103 +0,0 @@
"""ProtoTorch "siamese" GMLVQ example using Tecator."""
import matplotlib.pyplot as plt
import torch
from prototorch.components import LabeledComponents, StratifiedMeanInitializer
from prototorch.datasets.tecator import Tecator
from prototorch.functions.distances import sed
from prototorch.modules.losses import GLVQLoss
from prototorch.utils.colors import get_legend_handles
from torch.utils.data import DataLoader
# Prepare the dataset and dataloader
train_data = Tecator(root="./artifacts", train=True)
train_loader = DataLoader(train_data, batch_size=128, shuffle=True)
class Model(torch.nn.Module):
def __init__(self, **kwargs):
"""GMLVQ model as a siamese network."""
super().__init__()
prototype_initializer = StratifiedMeanInitializer(train_loader)
prototype_distribution = {"num_classes": 2, "prototypes_per_class": 2}
self.proto_layer = LabeledComponents(
prototype_distribution,
prototype_initializer,
)
self.omega = torch.nn.Linear(in_features=100,
out_features=100,
bias=False)
torch.nn.init.eye_(self.omega.weight)
def forward(self, x):
protos = self.proto_layer.components
plabels = self.proto_layer.component_labels
# Process `x` and `protos` through `omega`
x_map = self.omega(x)
protos_map = self.omega(protos)
# Compute distances and output
dis = sed(x_map, protos_map)
return dis, plabels
# Build the GLVQ model
model = Model()
# Print a summary of the model
print(model)
# Optimize using Adam optimizer from `torch.optim`
optimizer = torch.optim.Adam(model.parameters(), lr=0.001_0)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=75, gamma=0.1)
criterion = GLVQLoss(squashing="identity", beta=10)
# Training loop
for epoch in range(150):
epoch_loss = 0.0 # zero-out epoch loss
optimizer.zero_grad() # zero-out gradients
for xb, yb in train_loader:
# Compute loss
distances, plabels = model(xb)
loss = criterion([distances, plabels], yb)
epoch_loss += loss.item()
# Backprop
loss.backward()
# Take a gradient descent step
optimizer.step()
scheduler.step()
lr = optimizer.param_groups[0]["lr"]
print(f"Epoch: {epoch + 1:03d} Loss: {epoch_loss:06.02f} lr: {lr:07.06f}")
# Get the omega matrix form the model
omega = model.omega.weight.data.numpy().T
# Visualize the lambda matrix
title = "Lambda Matrix Visualization"
fig = plt.figure(title)
ax = fig.gca()
ax.set_title(title)
im = ax.imshow(omega.dot(omega.T), cmap="viridis")
plt.show()
# Get the prototypes form the model
protos = model.proto_layer.components.numpy()
plabels = model.proto_layer.component_labels.numpy()
# Visualize the prototypes
title = "Tecator Prototypes"
fig = plt.figure(title)
ax = fig.gca()
ax.set_title(title)
ax.set_xlabel("Spectral frequencies")
ax.set_ylabel("Absorption")
clabels = ["Class 0 - Low fat", "Class 1 - High fat"]
handles, colors = get_legend_handles(clabels, marker="line", zero_indexed=True)
for x, y in zip(protos, plabels):
ax.plot(x, c=colors[int(y)])
ax.legend(handles, clabels)
plt.show()

View File

@ -1,183 +0,0 @@
"""
ProtoTorch GTLVQ example using MNIST data.
The GTLVQ is placed as an classification model on
top of a CNN, considered as featurer extractor.
Initialization of subpsace and prototypes in
Siamnese fashion
For more info about GTLVQ see:
DOI:10.1109/IJCNN.2016.7727534
"""
import numpy as np
import torch
import torch.nn as nn
import torchvision
from prototorch.functions.helper import calculate_prototype_accuracy
from prototorch.modules.losses import GLVQLoss
from prototorch.modules.models import GTLVQ
from torchvision import transforms
# Parameters and options
num_epochs = 50
batch_size_train = 64
batch_size_test = 1000
learning_rate = 0.1
momentum = 0.5
log_interval = 10
cuda = "cuda:0"
random_seed = 1
device = torch.device(cuda if torch.cuda.is_available() else "cpu")
# Configures reproducability
torch.manual_seed(random_seed)
np.random.seed(random_seed)
# Prepare and preprocess the data
train_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST(
"./files/",
train=True,
download=True,
transform=torchvision.transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307, ), (0.3081, ))
]),
),
batch_size=batch_size_train,
shuffle=True,
)
test_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST(
"./files/",
train=False,
download=True,
transform=torchvision.transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307, ), (0.3081, ))
]),
),
batch_size=batch_size_test,
shuffle=True,
)
# Define the GLVQ model plus appropriate feature extractor
class CNNGTLVQ(torch.nn.Module):
def __init__(
self,
num_classes,
subspace_data,
prototype_data,
tangent_projection_type="local",
prototypes_per_class=2,
bottleneck_dim=128,
):
super(CNNGTLVQ, self).__init__()
# Feature Extractor - Simple CNN
self.fe = nn.Sequential(
nn.Conv2d(1, 32, 3, 1),
nn.ReLU(),
nn.Conv2d(32, 64, 3, 1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Dropout(0.25),
nn.Flatten(),
nn.Linear(9216, bottleneck_dim),
nn.Dropout(0.5),
nn.LeakyReLU(),
nn.LayerNorm(bottleneck_dim),
)
# Forward pass of subspace and prototype initialization data through feature extractor
subspace_data = self.fe(subspace_data)
prototype_data[0] = self.fe(prototype_data[0])
# Initialization of GTLVQ
self.gtlvq = GTLVQ(
num_classes,
subspace_data,
prototype_data,
tangent_projection_type=tangent_projection_type,
feature_dim=bottleneck_dim,
prototypes_per_class=prototypes_per_class,
)
def forward(self, x):
# Feature Extraction
x = self.fe(x)
# GTLVQ Forward pass
dis = self.gtlvq(x)
return dis
# Get init data
subspace_data = torch.cat(
[next(iter(train_loader))[0],
next(iter(test_loader))[0]])
prototype_data = next(iter(train_loader))
# Build the CNN GTLVQ model
model = CNNGTLVQ(
10,
subspace_data,
prototype_data,
tangent_projection_type="local",
bottleneck_dim=128,
).to(device)
# Optimize using SGD optimizer from `torch.optim`
optimizer = torch.optim.Adam(
[{
"params": model.fe.parameters()
}, {
"params": model.gtlvq.parameters()
}],
lr=learning_rate,
)
criterion = GLVQLoss(squashing="sigmoid_beta", beta=10)
# Training loop
for epoch in range(num_epochs):
for batch_idx, (x_train, y_train) in enumerate(train_loader):
model.train()
x_train, y_train = x_train.to(device), y_train.to(device)
optimizer.zero_grad()
distances = model(x_train)
plabels = model.gtlvq.cls.component_labels.to(device)
# Compute loss.
loss = criterion([distances, plabels], y_train)
loss.backward()
optimizer.step()
# GTLVQ uses projected SGD, which means to orthogonalize the subspaces after every gradient update.
model.gtlvq.orthogonalize_subspace()
if batch_idx % log_interval == 0:
acc = calculate_prototype_accuracy(distances, y_train, plabels)
print(
f"Epoch: {epoch + 1:02d}/{num_epochs:02d} Epoch Progress: {100. * batch_idx / len(train_loader):02.02f} % Loss: {loss.item():02.02f} \
Train Acc: {acc.item():02.02f}")
# Test
with torch.no_grad():
model.eval()
correct = 0
total = 0
for x_test, y_test in test_loader:
x_test, y_test = x_test.to(device), y_test.to(device)
test_distances = model(torch.tensor(x_test))
test_plabels = model.gtlvq.cls.prototype_labels.to(device)
i = torch.argmin(test_distances, 1)
correct += torch.sum(y_test == test_plabels[i])
total += y_test.size(0)
print("Accuracy of the network on the test images: %d %%" %
(torch.true_divide(correct, total) * 100))
# Save the model
PATH = "./glvq_mnist_model.pth"
torch.save(model.state_dict(), PATH)

View File

@ -1,108 +0,0 @@
"""ProtoTorch LGMLVQ example using 2D Iris data."""
import numpy as np
import torch
from matplotlib import pyplot as plt
from prototorch.components import LabeledComponents, StratifiedMeanInitializer
from prototorch.functions.competitions import stratified_min
from prototorch.functions.distances import lomega_distance
from prototorch.modules.losses import GLVQLoss
from sklearn.datasets import load_iris
from sklearn.metrics import accuracy_score
# Prepare training data
x_train, y_train = load_iris(True)
x_train = x_train[:, [0, 2]]
# Define the model
class Model(torch.nn.Module):
def __init__(self):
"""Local-GMLVQ model."""
super().__init__()
prototype_initializer = StratifiedMeanInitializer([x_train, y_train])
prototype_distribution = [1, 2, 2]
self.proto_layer = LabeledComponents(
prototype_distribution,
prototype_initializer,
)
omegas = torch.eye(2, 2).repeat(5, 1, 1)
self.omegas = torch.nn.Parameter(omegas)
def forward(self, x):
protos, plabels = self.proto_layer()
omegas = self.omegas
dis = lomega_distance(x, protos, omegas)
return dis, plabels
# Build the model
model = Model()
# Optimize using Adam optimizer from `torch.optim`
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = GLVQLoss(squashing="sigmoid_beta", beta=10)
x_in = torch.Tensor(x_train)
y_in = torch.Tensor(y_train)
# Training loop
title = "Prototype Visualization"
fig = plt.figure(title)
for epoch in range(100):
# Compute loss
dis, plabels = model(x_in)
loss = criterion([dis, plabels], y_in)
y_pred = np.argmin(stratified_min(dis, plabels).detach().numpy(), axis=1)
acc = accuracy_score(y_train, y_pred)
log_string = f"Epoch: {epoch + 1:03d} Loss: {loss.item():05.02f} "
log_string += f"Acc: {acc * 100:05.02f}%"
print(log_string)
# Take a gradient descent step
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Get the prototypes form the model
protos = model.proto_layer.components.numpy()
# Visualize the data and the prototypes
ax = fig.gca()
ax.cla()
ax.set_title(title)
ax.set_xlabel("Data dimension 1")
ax.set_ylabel("Data dimension 2")
cmap = "viridis"
ax.scatter(x_train[:, 0], x_train[:, 1], c=y_train, edgecolor="k")
ax.scatter(
protos[:, 0],
protos[:, 1],
c=plabels,
cmap=cmap,
edgecolor="k",
marker="D",
s=50,
)
# Paint decision regions
x = np.vstack((x_train, protos))
x_min, x_max = x[:, 0].min() - 1, x[:, 0].max() + 1
y_min, y_max = x[:, 1].min() - 1, x[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 1 / 50),
np.arange(y_min, y_max, 1 / 50))
mesh_input = np.c_[xx.ravel(), yy.ravel()]
d, plabels = model(torch.Tensor(mesh_input))
y_pred = np.argmin(stratified_min(d, plabels).detach().numpy(), axis=1)
y_pred = y_pred.reshape(xx.shape)
# Plot voronoi regions
ax.contourf(xx, yy, y_pred, cmap=cmap, alpha=0.35)
ax.set_xlim(left=x_min + 0, right=x_max - 0)
ax.set_ylim(bottom=y_min + 0, top=y_max - 0)
plt.pause(0.1)

View File

@ -4,24 +4,20 @@ import pkgutil
import pkg_resources import pkg_resources
from . import ( from . import datasets # noqa: F401
datasets, from . import nn # noqa: F401
nn, from . import utils # noqa: F401
utils, from .core import competitions # noqa: F401
) from .core import components # noqa: F401
from .core import ( from .core import distances # noqa: F401
competitions, from .core import initializers # noqa: F401
components, from .core import losses # noqa: F401
distances, from .core import pooling # noqa: F401
initializers, from .core import similarities # noqa: F401
losses, from .core import transforms # noqa: F401
pooling,
similarities,
transforms,
)
# Core Setup # Core Setup
__version__ = "0.5.0" __version__ = "0.7.6"
__all_core__ = [ __all_core__ = [
"competitions", "competitions",

View File

@ -3,8 +3,7 @@
import torch import torch
def wtac(distances: torch.Tensor, def wtac(distances: torch.Tensor, labels: torch.LongTensor):
labels: torch.LongTensor) -> (torch.LongTensor):
"""Winner-Takes-All-Competition. """Winner-Takes-All-Competition.
Returns the labels corresponding to the winners. Returns the labels corresponding to the winners.
@ -15,9 +14,7 @@ def wtac(distances: torch.Tensor,
return winning_labels return winning_labels
def knnc(distances: torch.Tensor, def knnc(distances: torch.Tensor, labels: torch.LongTensor, k: int = 1):
labels: torch.LongTensor,
k: int = 1) -> (torch.LongTensor):
"""K-Nearest-Neighbors-Competition. """K-Nearest-Neighbors-Competition.
Returns the labels corresponding to the winners. Returns the labels corresponding to the winners.
@ -41,7 +38,7 @@ def cbcc(detections: torch.Tensor, reasonings: torch.Tensor):
pk = A pk = A
nk = (1 - A) * B nk = (1 - A) * B
numerator = (detections @ (pk - nk).T) + nk.sum(1) numerator = (detections @ (pk - nk).T) + nk.sum(1)
probs = numerator / (pk + nk).sum(1) probs = numerator / ((pk + nk).sum(1) + 1e-8)
return probs return probs
@ -51,7 +48,8 @@ class WTAC(torch.nn.Module):
Thin wrapper over the `wtac` function. Thin wrapper over the `wtac` function.
""" """
def forward(self, distances, labels):
def forward(self, distances, labels): # pylint: disable=no-self-use
return wtac(distances, labels) return wtac(distances, labels)
@ -61,7 +59,8 @@ class LTAC(torch.nn.Module):
Thin wrapper over the `wtac` function. Thin wrapper over the `wtac` function.
""" """
def forward(self, probs, labels):
def forward(self, probs, labels): # pylint: disable=no-self-use
return wtac(-1.0 * probs, labels) return wtac(-1.0 * probs, labels)
@ -71,6 +70,7 @@ class KNNC(torch.nn.Module):
Thin wrapper over the `knnc` function. Thin wrapper over the `knnc` function.
""" """
def __init__(self, k=1, **kwargs): def __init__(self, k=1, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.k = k self.k = k
@ -88,5 +88,6 @@ class CBCC(torch.nn.Module):
Thin wrapper over the `cbcc` function. Thin wrapper over the `cbcc` function.
""" """
def forward(self, detections, reasonings):
def forward(self, detections, reasonings): # pylint: disable=no-self-use
return cbcc(detections, reasonings) return cbcc(detections, reasonings)

View File

@ -6,7 +6,8 @@ from typing import Union
import torch import torch
from torch.nn.parameter import Parameter from torch.nn.parameter import Parameter
from ..utils import parse_distribution from prototorch.utils import parse_distribution
from .initializers import ( from .initializers import (
AbstractClassAwareCompInitializer, AbstractClassAwareCompInitializer,
AbstractComponentsInitializer, AbstractComponentsInitializer,
@ -63,6 +64,7 @@ def get_cikwargs(init, distribution):
class AbstractComponents(torch.nn.Module): class AbstractComponents(torch.nn.Module):
"""Abstract class for all components modules.""" """Abstract class for all components modules."""
@property @property
def num_components(self): def num_components(self):
"""Current number of components.""" """Current number of components."""
@ -85,9 +87,10 @@ class AbstractComponents(torch.nn.Module):
class Components(AbstractComponents): class Components(AbstractComponents):
"""A set of adaptable Tensors.""" """A set of adaptable Tensors."""
def __init__(self, num_components: int, def __init__(self, num_components: int,
initializer: AbstractComponentsInitializer, **kwargs): initializer: AbstractComponentsInitializer):
super().__init__(**kwargs) super().__init__()
self.add_components(num_components, initializer) self.add_components(num_components, initializer)
def add_components(self, num_components: int, def add_components(self, num_components: int,
@ -112,6 +115,7 @@ class Components(AbstractComponents):
class AbstractLabels(torch.nn.Module): class AbstractLabels(torch.nn.Module):
"""Abstract class for all labels modules.""" """Abstract class for all labels modules."""
@property @property
def labels(self): def labels(self):
return self._labels.cpu() return self._labels.cpu()
@ -152,11 +156,11 @@ class AbstractLabels(torch.nn.Module):
class Labels(AbstractLabels): class Labels(AbstractLabels):
"""A set of standalone labels.""" """A set of standalone labels."""
def __init__(self, def __init__(self,
distribution: Union[dict, list, tuple], distribution: Union[dict, list, tuple],
initializer: AbstractLabelsInitializer = LabelsInitializer(), initializer: AbstractLabelsInitializer = LabelsInitializer()):
**kwargs): super().__init__()
super().__init__(**kwargs)
self.add_labels(distribution, initializer) self.add_labels(distribution, initializer)
def add_labels( def add_labels(
@ -183,14 +187,13 @@ class Labels(AbstractLabels):
class LabeledComponents(AbstractComponents): class LabeledComponents(AbstractComponents):
"""A set of adaptable components and corresponding unadaptable labels.""" """A set of adaptable components and corresponding unadaptable labels."""
def __init__( def __init__(
self, self,
distribution: Union[dict, list, tuple], distribution: Union[dict, list, tuple],
components_initializer: AbstractComponentsInitializer, components_initializer: AbstractComponentsInitializer,
labels_initializer: AbstractLabelsInitializer = LabelsInitializer( labels_initializer: AbstractLabelsInitializer = LabelsInitializer()):
), super().__init__()
**kwargs):
super().__init__(**kwargs)
self.add_components(distribution, components_initializer, self.add_components(distribution, components_initializer,
labels_initializer) labels_initializer)
@ -252,12 +255,15 @@ class Reasonings(torch.nn.Module):
The `reasonings` tensor is of shape [num_components, num_classes, 2]. The `reasonings` tensor is of shape [num_components, num_classes, 2].
""" """
def __init__(self,
def __init__(
self,
distribution: Union[dict, list, tuple], distribution: Union[dict, list, tuple],
initializer: initializer:
AbstractReasoningsInitializer = RandomReasoningsInitializer(), AbstractReasoningsInitializer = RandomReasoningsInitializer(),
**kwargs): ):
super().__init__(**kwargs) super().__init__()
self.add_reasonings(distribution, initializer)
@property @property
def num_classes(self): def num_classes(self):
@ -295,7 +301,7 @@ class Reasonings(torch.nn.Module):
class ReasoningComponents(AbstractComponents): class ReasoningComponents(AbstractComponents):
"""A set of components and a corresponding adapatable reasoning matrices. r"""A set of components and a corresponding adapatable reasoning matrices.
Every component has its own reasoning matrix. Every component has its own reasoning matrix.
@ -309,14 +315,14 @@ class ReasoningComponents(AbstractComponents):
three element probability distribution. three element probability distribution.
""" """
def __init__( def __init__(
self, self,
distribution: Union[dict, list, tuple], distribution: Union[dict, list, tuple],
components_initializer: AbstractComponentsInitializer, components_initializer: AbstractComponentsInitializer,
reasonings_initializer: reasonings_initializer:
AbstractReasoningsInitializer = PurePositiveReasoningsInitializer(), AbstractReasoningsInitializer = PurePositiveReasoningsInitializer()):
**kwargs): super().__init__()
super().__init__(**kwargs)
self.add_components(distribution, components_initializer, self.add_components(distribution, components_initializer,
reasonings_initializer) reasonings_initializer)

View File

@ -11,7 +11,7 @@ def squared_euclidean_distance(x, y):
**Alias:** **Alias:**
``prototorch.functions.distances.sed`` ``prototorch.functions.distances.sed``
""" """
x, y = [arr.view(arr.size(0), -1) for arr in (x, y)] x, y = (arr.view(arr.size(0), -1) for arr in (x, y))
expanded_x = x.unsqueeze(dim=1) expanded_x = x.unsqueeze(dim=1)
batchwise_difference = y - expanded_x batchwise_difference = y - expanded_x
differences_raised = torch.pow(batchwise_difference, 2) differences_raised = torch.pow(batchwise_difference, 2)
@ -27,23 +27,20 @@ def euclidean_distance(x, y):
:returns: Distance Tensor of shape :math:`X \times Y` :returns: Distance Tensor of shape :math:`X \times Y`
:rtype: `torch.tensor` :rtype: `torch.tensor`
""" """
x, y = [arr.view(arr.size(0), -1) for arr in (x, y)] x, y = (arr.view(arr.size(0), -1) for arr in (x, y))
distances_raised = squared_euclidean_distance(x, y) distances_raised = squared_euclidean_distance(x, y)
distances = torch.sqrt(distances_raised) distances = torch.sqrt(distances_raised)
return distances return distances
def euclidean_distance_v2(x, y): def euclidean_distance_v2(x, y):
x, y = [arr.view(arr.size(0), -1) for arr in (x, y)] x, y = (arr.view(arr.size(0), -1) for arr in (x, y))
diff = y - x.unsqueeze(1) diff = y - x.unsqueeze(1)
pairwise_distances = (diff @ diff.permute((0, 2, 1))).sqrt() pairwise_distances = (diff @ diff.permute((0, 2, 1))).sqrt()
# Passing `dim1=-2` and `dim2=-1` to `diagonal()` takes the # Passing `dim1=-2` and `dim2=-1` to `diagonal()` takes the
# batch diagonal. See: # batch diagonal. See:
# https://pytorch.org/docs/stable/generated/torch.diagonal.html # https://pytorch.org/docs/stable/generated/torch.diagonal.html
distances = torch.diagonal(pairwise_distances, dim1=-2, dim2=-1) distances = torch.diagonal(pairwise_distances, dim1=-2, dim2=-1)
# print(f"{diff.shape=}") # (nx, ny, ndim)
# print(f"{pairwise_distances.shape=}") # (nx, ny, ny)
# print(f"{distances.shape=}") # (nx, ny)
return distances return distances
@ -57,7 +54,7 @@ def lpnorm_distance(x, y, p):
:param p: p parameter of the lp norm :param p: p parameter of the lp norm
""" """
x, y = [arr.view(arr.size(0), -1) for arr in (x, y)] x, y = (arr.view(arr.size(0), -1) for arr in (x, y))
distances = torch.cdist(x, y, p=p) distances = torch.cdist(x, y, p=p)
return distances return distances
@ -69,7 +66,7 @@ def omega_distance(x, y, omega):
:param `torch.tensor` omega: Two dimensional matrix :param `torch.tensor` omega: Two dimensional matrix
""" """
x, y = [arr.view(arr.size(0), -1) for arr in (x, y)] x, y = (arr.view(arr.size(0), -1) for arr in (x, y))
projected_x = x @ omega projected_x = x @ omega
projected_y = y @ omega projected_y = y @ omega
distances = squared_euclidean_distance(projected_x, projected_y) distances = squared_euclidean_distance(projected_x, projected_y)
@ -83,7 +80,7 @@ def lomega_distance(x, y, omegas):
:param `torch.tensor` omegas: Three dimensional matrix :param `torch.tensor` omegas: Three dimensional matrix
""" """
x, y = [arr.view(arr.size(0), -1) for arr in (x, y)] x, y = (arr.view(arr.size(0), -1) for arr in (x, y))
projected_x = x @ omegas projected_x = x @ omegas
projected_y = torch.diagonal(y @ omegas).T projected_y = torch.diagonal(y @ omegas).T
expanded_y = torch.unsqueeze(projected_y, dim=1) expanded_y = torch.unsqueeze(projected_y, dim=1)

View File

@ -3,11 +3,15 @@
import warnings import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Iterable from collections.abc import Iterable
from typing import Union from typing import (
Callable,
Type,
Union,
)
import torch import torch
from ..utils import parse_data_arg, parse_distribution from prototorch.utils import parse_data_arg, parse_distribution
# Components # Components
@ -22,11 +26,18 @@ class LiteralCompInitializer(AbstractComponentsInitializer):
Use this to 'generate' pre-initialized components elsewhere. Use this to 'generate' pre-initialized components elsewhere.
""" """
def __init__(self, components): def __init__(self, components):
self.components = components self.components = components
def generate(self, num_components: int = 0): def generate(self, num_components: int = 0):
"""Ignore `num_components` and simply return `self.components`.""" """Ignore `num_components` and simply return `self.components`."""
provided_num_components = len(self.components)
if provided_num_components != num_components:
wmsg = f"The number of components ({provided_num_components}) " \
f"provided to {self.__class__.__name__} " \
f"does not match the expected number ({num_components})."
warnings.warn(wmsg)
if not isinstance(self.components, torch.Tensor): if not isinstance(self.components, torch.Tensor):
wmsg = f"Converting components to {torch.Tensor}..." wmsg = f"Converting components to {torch.Tensor}..."
warnings.warn(wmsg) warnings.warn(wmsg)
@ -36,6 +47,7 @@ class LiteralCompInitializer(AbstractComponentsInitializer):
class ShapeAwareCompInitializer(AbstractComponentsInitializer): class ShapeAwareCompInitializer(AbstractComponentsInitializer):
"""Abstract class for all dimension-aware components initializers.""" """Abstract class for all dimension-aware components initializers."""
def __init__(self, shape: Union[Iterable, int]): def __init__(self, shape: Union[Iterable, int]):
if isinstance(shape, Iterable): if isinstance(shape, Iterable):
self.component_shape = tuple(shape) self.component_shape = tuple(shape)
@ -49,6 +61,7 @@ class ShapeAwareCompInitializer(AbstractComponentsInitializer):
class ZerosCompInitializer(ShapeAwareCompInitializer): class ZerosCompInitializer(ShapeAwareCompInitializer):
"""Generate zeros corresponding to the components shape.""" """Generate zeros corresponding to the components shape."""
def generate(self, num_components: int): def generate(self, num_components: int):
components = torch.zeros((num_components, ) + self.component_shape) components = torch.zeros((num_components, ) + self.component_shape)
return components return components
@ -56,6 +69,7 @@ class ZerosCompInitializer(ShapeAwareCompInitializer):
class OnesCompInitializer(ShapeAwareCompInitializer): class OnesCompInitializer(ShapeAwareCompInitializer):
"""Generate ones corresponding to the components shape.""" """Generate ones corresponding to the components shape."""
def generate(self, num_components: int): def generate(self, num_components: int):
components = torch.ones((num_components, ) + self.component_shape) components = torch.ones((num_components, ) + self.component_shape)
return components return components
@ -63,6 +77,7 @@ class OnesCompInitializer(ShapeAwareCompInitializer):
class FillValueCompInitializer(OnesCompInitializer): class FillValueCompInitializer(OnesCompInitializer):
"""Generate components with the provided `fill_value`.""" """Generate components with the provided `fill_value`."""
def __init__(self, shape, fill_value: float = 1.0): def __init__(self, shape, fill_value: float = 1.0):
super().__init__(shape) super().__init__(shape)
self.fill_value = fill_value self.fill_value = fill_value
@ -75,6 +90,7 @@ class FillValueCompInitializer(OnesCompInitializer):
class UniformCompInitializer(OnesCompInitializer): class UniformCompInitializer(OnesCompInitializer):
"""Generate components by sampling from a continuous uniform distribution.""" """Generate components by sampling from a continuous uniform distribution."""
def __init__(self, shape, minimum=0.0, maximum=1.0, scale=1.0): def __init__(self, shape, minimum=0.0, maximum=1.0, scale=1.0):
super().__init__(shape) super().__init__(shape)
self.minimum = minimum self.minimum = minimum
@ -89,6 +105,7 @@ class UniformCompInitializer(OnesCompInitializer):
class RandomNormalCompInitializer(OnesCompInitializer): class RandomNormalCompInitializer(OnesCompInitializer):
"""Generate components by sampling from a standard normal distribution.""" """Generate components by sampling from a standard normal distribution."""
def __init__(self, shape, shift=0.0, scale=1.0): def __init__(self, shape, shift=0.0, scale=1.0):
super().__init__(shape) super().__init__(shape)
self.shift = shift self.shift = shift
@ -109,10 +126,11 @@ class AbstractDataAwareCompInitializer(AbstractComponentsInitializer):
`data` has to be a torch tensor. `data` has to be a torch tensor.
""" """
def __init__(self, def __init__(self,
data: torch.TensorType, data: torch.Tensor,
noise: float = 0.0, noise: float = 0.0,
transform: callable = torch.nn.Identity()): transform: Callable = torch.nn.Identity()):
self.data = data self.data = data
self.noise = noise self.noise = noise
self.transform = transform self.transform = transform
@ -133,6 +151,7 @@ class AbstractDataAwareCompInitializer(AbstractComponentsInitializer):
class DataAwareCompInitializer(AbstractDataAwareCompInitializer): class DataAwareCompInitializer(AbstractDataAwareCompInitializer):
"""'Generate' the components from the provided data.""" """'Generate' the components from the provided data."""
def generate(self, num_components: int = 0): def generate(self, num_components: int = 0):
"""Ignore `num_components` and simply return transformed `self.data`.""" """Ignore `num_components` and simply return transformed `self.data`."""
components = self.generate_end_hook(self.data) components = self.generate_end_hook(self.data)
@ -141,6 +160,7 @@ class DataAwareCompInitializer(AbstractDataAwareCompInitializer):
class SelectionCompInitializer(AbstractDataAwareCompInitializer): class SelectionCompInitializer(AbstractDataAwareCompInitializer):
"""Generate components by uniformly sampling from the provided data.""" """Generate components by uniformly sampling from the provided data."""
def generate(self, num_components: int): def generate(self, num_components: int):
indices = torch.LongTensor(num_components).random_(0, len(self.data)) indices = torch.LongTensor(num_components).random_(0, len(self.data))
samples = self.data[indices] samples = self.data[indices]
@ -150,15 +170,16 @@ class SelectionCompInitializer(AbstractDataAwareCompInitializer):
class MeanCompInitializer(AbstractDataAwareCompInitializer): class MeanCompInitializer(AbstractDataAwareCompInitializer):
"""Generate components by computing the mean of the provided data.""" """Generate components by computing the mean of the provided data."""
def generate(self, num_components: int): def generate(self, num_components: int):
mean = torch.mean(self.data, dim=0) mean = self.data.mean(dim=0)
repeat_dim = [num_components] + [1] * len(mean.shape) repeat_dim = [num_components] + [1] * len(mean.shape)
samples = mean.repeat(repeat_dim) samples = mean.repeat(repeat_dim)
components = self.generate_end_hook(samples) components = self.generate_end_hook(samples)
return components return components
class AbstractClassAwareCompInitializer(AbstractDataAwareCompInitializer): class AbstractClassAwareCompInitializer(AbstractComponentsInitializer):
"""Abstract class for all class-aware components initializers. """Abstract class for all class-aware components initializers.
Components generated by class-aware components initializers inherit the shape Components generated by class-aware components initializers inherit the shape
@ -168,16 +189,22 @@ class AbstractClassAwareCompInitializer(AbstractDataAwareCompInitializer):
target tensors. target tensors.
""" """
def __init__(self, def __init__(self,
data, data,
noise: float = 0.0, noise: float = 0.0,
transform: callable = torch.nn.Identity()): transform: Callable = torch.nn.Identity()):
self.data, self.targets = parse_data_arg(data) self.data, self.targets = parse_data_arg(data)
self.noise = noise self.noise = noise
self.transform = transform self.transform = transform
self.clabels = torch.unique(self.targets).int().tolist() self.clabels = torch.unique(self.targets).int().tolist()
self.num_classes = len(self.clabels) self.num_classes = len(self.clabels)
def generate_end_hook(self, samples):
drift = torch.rand_like(samples) * self.noise
components = self.transform(samples + drift)
return components
@abstractmethod @abstractmethod
def generate(self, distribution: Union[dict, list, tuple]): def generate(self, distribution: Union[dict, list, tuple]):
... ...
@ -190,6 +217,7 @@ class AbstractClassAwareCompInitializer(AbstractDataAwareCompInitializer):
class ClassAwareCompInitializer(AbstractClassAwareCompInitializer): class ClassAwareCompInitializer(AbstractClassAwareCompInitializer):
"""'Generate' components from provided data and requested distribution.""" """'Generate' components from provided data and requested distribution."""
def generate(self, distribution: Union[dict, list, tuple]): def generate(self, distribution: Union[dict, list, tuple]):
"""Ignore `distribution` and simply return transformed `self.data`.""" """Ignore `distribution` and simply return transformed `self.data`."""
components = self.generate_end_hook(self.data) components = self.generate_end_hook(self.data)
@ -198,9 +226,10 @@ class ClassAwareCompInitializer(AbstractClassAwareCompInitializer):
class AbstractStratifiedCompInitializer(AbstractClassAwareCompInitializer): class AbstractStratifiedCompInitializer(AbstractClassAwareCompInitializer):
"""Abstract class for all stratified components initializers.""" """Abstract class for all stratified components initializers."""
@property @property
@abstractmethod @abstractmethod
def subinit_type(self) -> AbstractDataAwareCompInitializer: def subinit_type(self) -> Type[AbstractDataAwareCompInitializer]:
... ...
def generate(self, distribution: Union[dict, list, tuple]): def generate(self, distribution: Union[dict, list, tuple]):
@ -208,6 +237,8 @@ class AbstractStratifiedCompInitializer(AbstractClassAwareCompInitializer):
components = torch.tensor([]) components = torch.tensor([])
for k, v in distribution.items(): for k, v in distribution.items():
stratified_data = self.data[self.targets == k] stratified_data = self.data[self.targets == k]
if len(stratified_data) == 0:
raise ValueError(f"No data available for class {k}.")
initializer = self.subinit_type( initializer = self.subinit_type(
stratified_data, stratified_data,
noise=self.noise, noise=self.noise,
@ -220,6 +251,7 @@ class AbstractStratifiedCompInitializer(AbstractClassAwareCompInitializer):
class StratifiedSelectionCompInitializer(AbstractStratifiedCompInitializer): class StratifiedSelectionCompInitializer(AbstractStratifiedCompInitializer):
"""Generate components using stratified sampling from the provided data.""" """Generate components using stratified sampling from the provided data."""
@property @property
def subinit_type(self): def subinit_type(self):
return SelectionCompInitializer return SelectionCompInitializer
@ -227,6 +259,7 @@ class StratifiedSelectionCompInitializer(AbstractStratifiedCompInitializer):
class StratifiedMeanCompInitializer(AbstractStratifiedCompInitializer): class StratifiedMeanCompInitializer(AbstractStratifiedCompInitializer):
"""Generate components at stratified means of the provided data.""" """Generate components at stratified means of the provided data."""
@property @property
def subinit_type(self): def subinit_type(self):
return MeanCompInitializer return MeanCompInitializer
@ -235,6 +268,7 @@ class StratifiedMeanCompInitializer(AbstractStratifiedCompInitializer):
# Labels # Labels
class AbstractLabelsInitializer(ABC): class AbstractLabelsInitializer(ABC):
"""Abstract class for all labels initializers.""" """Abstract class for all labels initializers."""
@abstractmethod @abstractmethod
def generate(self, distribution: Union[dict, list, tuple]): def generate(self, distribution: Union[dict, list, tuple]):
... ...
@ -246,6 +280,7 @@ class LiteralLabelsInitializer(AbstractLabelsInitializer):
Use this to 'generate' pre-initialized labels elsewhere. Use this to 'generate' pre-initialized labels elsewhere.
""" """
def __init__(self, labels): def __init__(self, labels):
self.labels = labels self.labels = labels
@ -264,6 +299,7 @@ class LiteralLabelsInitializer(AbstractLabelsInitializer):
class DataAwareLabelsInitializer(AbstractLabelsInitializer): class DataAwareLabelsInitializer(AbstractLabelsInitializer):
"""'Generate' the labels from a torch Dataset.""" """'Generate' the labels from a torch Dataset."""
def __init__(self, data): def __init__(self, data):
self.data, self.targets = parse_data_arg(data) self.data, self.targets = parse_data_arg(data)
@ -274,17 +310,19 @@ class DataAwareLabelsInitializer(AbstractLabelsInitializer):
class LabelsInitializer(AbstractLabelsInitializer): class LabelsInitializer(AbstractLabelsInitializer):
"""Generate labels from `distribution`.""" """Generate labels from `distribution`."""
def generate(self, distribution: Union[dict, list, tuple]): def generate(self, distribution: Union[dict, list, tuple]):
distribution = parse_distribution(distribution) distribution = parse_distribution(distribution)
labels = [] labels_list = []
for k, v in distribution.items(): for k, v in distribution.items():
labels.extend([k] * v) labels_list.extend([k] * v)
labels = torch.LongTensor(labels) labels = torch.LongTensor(labels_list)
return labels return labels
class OneHotLabelsInitializer(LabelsInitializer): class OneHotLabelsInitializer(LabelsInitializer):
"""Generate one-hot-encoded labels from `distribution`.""" """Generate one-hot-encoded labels from `distribution`."""
def generate(self, distribution: Union[dict, list, tuple]): def generate(self, distribution: Union[dict, list, tuple]):
distribution = parse_distribution(distribution) distribution = parse_distribution(distribution)
num_classes = len(distribution.keys()) num_classes = len(distribution.keys())
@ -294,17 +332,19 @@ class OneHotLabelsInitializer(LabelsInitializer):
# Reasonings # Reasonings
class AbstractReasoningsInitializer(ABC): def compute_distribution_shape(distribution):
"""Abstract class for all reasonings initializers."""
def __init__(self, components_first: bool = True):
self.components_first = components_first
def compute_shape(self, distribution):
distribution = parse_distribution(distribution) distribution = parse_distribution(distribution)
num_components = sum(distribution.values()) num_components = sum(distribution.values())
num_classes = len(distribution.keys()) num_classes = len(distribution.keys())
return (num_components, num_classes, 2) return (num_components, num_classes, 2)
class AbstractReasoningsInitializer(ABC):
"""Abstract class for all reasonings initializers."""
def __init__(self, components_first: bool = True):
self.components_first = components_first
def generate_end_hook(self, reasonings): def generate_end_hook(self, reasonings):
if not self.components_first: if not self.components_first:
reasonings = reasonings.permute(2, 1, 0) reasonings = reasonings.permute(2, 1, 0)
@ -322,6 +362,7 @@ class LiteralReasoningsInitializer(AbstractReasoningsInitializer):
Use this to 'generate' pre-initialized reasonings elsewhere. Use this to 'generate' pre-initialized reasonings elsewhere.
""" """
def __init__(self, reasonings, **kwargs): def __init__(self, reasonings, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.reasonings = reasonings self.reasonings = reasonings
@ -339,8 +380,9 @@ class LiteralReasoningsInitializer(AbstractReasoningsInitializer):
class ZerosReasoningsInitializer(AbstractReasoningsInitializer): class ZerosReasoningsInitializer(AbstractReasoningsInitializer):
"""Reasonings are all initialized with zeros.""" """Reasonings are all initialized with zeros."""
def generate(self, distribution: Union[dict, list, tuple]): def generate(self, distribution: Union[dict, list, tuple]):
shape = self.compute_shape(distribution) shape = compute_distribution_shape(distribution)
reasonings = torch.zeros(*shape) reasonings = torch.zeros(*shape)
reasonings = self.generate_end_hook(reasonings) reasonings = self.generate_end_hook(reasonings)
return reasonings return reasonings
@ -348,8 +390,9 @@ class ZerosReasoningsInitializer(AbstractReasoningsInitializer):
class OnesReasoningsInitializer(AbstractReasoningsInitializer): class OnesReasoningsInitializer(AbstractReasoningsInitializer):
"""Reasonings are all initialized with ones.""" """Reasonings are all initialized with ones."""
def generate(self, distribution: Union[dict, list, tuple]): def generate(self, distribution: Union[dict, list, tuple]):
shape = self.compute_shape(distribution) shape = compute_distribution_shape(distribution)
reasonings = torch.ones(*shape) reasonings = torch.ones(*shape)
reasonings = self.generate_end_hook(reasonings) reasonings = self.generate_end_hook(reasonings)
return reasonings return reasonings
@ -357,13 +400,14 @@ class OnesReasoningsInitializer(AbstractReasoningsInitializer):
class RandomReasoningsInitializer(AbstractReasoningsInitializer): class RandomReasoningsInitializer(AbstractReasoningsInitializer):
"""Reasonings are randomly initialized.""" """Reasonings are randomly initialized."""
def __init__(self, minimum=0.4, maximum=0.6, **kwargs): def __init__(self, minimum=0.4, maximum=0.6, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.minimum = minimum self.minimum = minimum
self.maximum = maximum self.maximum = maximum
def generate(self, distribution: Union[dict, list, tuple]): def generate(self, distribution: Union[dict, list, tuple]):
shape = self.compute_shape(distribution) shape = compute_distribution_shape(distribution)
reasonings = torch.ones(*shape).uniform_(self.minimum, self.maximum) reasonings = torch.ones(*shape).uniform_(self.minimum, self.maximum)
reasonings = self.generate_end_hook(reasonings) reasonings = self.generate_end_hook(reasonings)
return reasonings return reasonings
@ -371,8 +415,10 @@ class RandomReasoningsInitializer(AbstractReasoningsInitializer):
class PurePositiveReasoningsInitializer(AbstractReasoningsInitializer): class PurePositiveReasoningsInitializer(AbstractReasoningsInitializer):
"""Each component reasons positively for exactly one class.""" """Each component reasons positively for exactly one class."""
def generate(self, distribution: Union[dict, list, tuple]): def generate(self, distribution: Union[dict, list, tuple]):
num_components, num_classes, _ = self.compute_shape(distribution) num_components, num_classes, _ = compute_distribution_shape(
distribution)
A = OneHotLabelsInitializer().generate(distribution) A = OneHotLabelsInitializer().generate(distribution)
B = torch.zeros(num_components, num_classes) B = torch.zeros(num_components, num_classes)
reasonings = torch.stack([A, B], dim=-1) reasonings = torch.stack([A, B], dim=-1)
@ -388,6 +434,7 @@ class AbstractTransformInitializer(ABC):
class AbstractLinearTransformInitializer(AbstractTransformInitializer): class AbstractLinearTransformInitializer(AbstractTransformInitializer):
"""Abstract class for all linear transform initializers.""" """Abstract class for all linear transform initializers."""
def __init__(self, out_dim_first: bool = False): def __init__(self, out_dim_first: bool = False):
self.out_dim_first = out_dim_first self.out_dim_first = out_dim_first
@ -404,6 +451,7 @@ class AbstractLinearTransformInitializer(AbstractTransformInitializer):
class ZerosLinearTransformInitializer(AbstractLinearTransformInitializer): class ZerosLinearTransformInitializer(AbstractLinearTransformInitializer):
"""Initialize a matrix with zeros.""" """Initialize a matrix with zeros."""
def generate(self, in_dim: int, out_dim: int): def generate(self, in_dim: int, out_dim: int):
weights = torch.zeros(in_dim, out_dim) weights = torch.zeros(in_dim, out_dim)
return self.generate_end_hook(weights) return self.generate_end_hook(weights)
@ -411,13 +459,23 @@ class ZerosLinearTransformInitializer(AbstractLinearTransformInitializer):
class OnesLinearTransformInitializer(AbstractLinearTransformInitializer): class OnesLinearTransformInitializer(AbstractLinearTransformInitializer):
"""Initialize a matrix with ones.""" """Initialize a matrix with ones."""
def generate(self, in_dim: int, out_dim: int): def generate(self, in_dim: int, out_dim: int):
weights = torch.ones(in_dim, out_dim) weights = torch.ones(in_dim, out_dim)
return self.generate_end_hook(weights) return self.generate_end_hook(weights)
class EyeTransformInitializer(AbstractLinearTransformInitializer): class RandomLinearTransformInitializer(AbstractLinearTransformInitializer):
"""Initialize a matrix with random values."""
def generate(self, in_dim: int, out_dim: int):
weights = torch.rand(in_dim, out_dim)
return self.generate_end_hook(weights)
class EyeLinearTransformInitializer(AbstractLinearTransformInitializer):
"""Initialize a matrix with the largest possible identity matrix.""" """Initialize a matrix with the largest possible identity matrix."""
def generate(self, in_dim: int, out_dim: int): def generate(self, in_dim: int, out_dim: int):
weights = torch.zeros(in_dim, out_dim) weights = torch.zeros(in_dim, out_dim)
I = torch.eye(min(in_dim, out_dim)) I = torch.eye(min(in_dim, out_dim))
@ -425,6 +483,42 @@ class EyeTransformInitializer(AbstractLinearTransformInitializer):
return self.generate_end_hook(weights) return self.generate_end_hook(weights)
class AbstractDataAwareLTInitializer(AbstractLinearTransformInitializer):
"""Abstract class for all data-aware linear transform initializers."""
def __init__(self,
data: torch.Tensor,
noise: float = 0.0,
transform: Callable = torch.nn.Identity(),
out_dim_first: bool = False):
super().__init__(out_dim_first)
self.data = data
self.noise = noise
self.transform = transform
def generate_end_hook(self, weights: torch.Tensor):
drift = torch.rand_like(weights) * self.noise
weights = self.transform(weights + drift)
if self.out_dim_first:
weights = weights.permute(1, 0)
return weights
class PCALinearTransformInitializer(AbstractDataAwareLTInitializer):
"""Initialize a matrix with Eigenvectors from the data."""
def generate(self, in_dim: int, out_dim: int):
_, _, weights = torch.pca_lowrank(self.data, q=out_dim)
return self.generate_end_hook(weights)
class LiteralLinearTransformInitializer(AbstractDataAwareLTInitializer):
"""'Generate' the provided weights."""
def generate(self, in_dim: int, out_dim: int):
return self.generate_end_hook(self.data)
# Aliases - Components # Aliases - Components
CACI = ClassAwareCompInitializer CACI = ClassAwareCompInitializer
DACI = DataAwareCompInitializer DACI = DataAwareCompInitializer
@ -453,6 +547,9 @@ RRI = RandomReasoningsInitializer
ZRI = ZerosReasoningsInitializer ZRI = ZerosReasoningsInitializer
# Aliases - Transforms # Aliases - Transforms
Eye = EyeTransformInitializer ELTI = Eye = EyeLinearTransformInitializer
OLTI = OnesLinearTransformInitializer OLTI = OnesLinearTransformInitializer
RLTI = RandomLinearTransformInitializer
ZLTI = ZerosLinearTransformInitializer ZLTI = ZerosLinearTransformInitializer
PCALTI = PCALinearTransformInitializer
LLTI = LiteralLinearTransformInitializer

View File

@ -2,7 +2,7 @@
import torch import torch
from ..nn.activations import get_activation from prototorch.nn.activations import get_activation
# Helpers # Helpers
@ -106,20 +106,31 @@ def margin_loss(y_pred, y_true, margin=0.3):
class GLVQLoss(torch.nn.Module): class GLVQLoss(torch.nn.Module):
def __init__(self, margin=0.0, squashing="identity", beta=10, **kwargs):
def __init__(self,
margin=0.0,
transfer_fn="identity",
beta=10,
add_dp=False,
**kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.margin = margin self.margin = margin
self.squashing = get_activation(squashing) self.transfer_fn = get_activation(transfer_fn)
self.beta = torch.tensor(beta) self.beta = torch.tensor(beta)
self.add_dp = add_dp
def forward(self, outputs, targets): def forward(self, outputs, targets, plabels):
distances, plabels = outputs # mu = glvq_loss(outputs, targets, plabels)
mu = glvq_loss(distances, targets, prototype_labels=plabels) dp, dm = _get_dp_dm(outputs, targets, plabels)
batch_loss = self.squashing(mu + self.margin, beta=self.beta) mu = (dp - dm) / (dp + dm)
return torch.sum(batch_loss, dim=0) if self.add_dp:
mu = mu + dp
batch_loss = self.transfer_fn(mu + self.margin, beta=self.beta)
return batch_loss.sum()
class MarginLoss(torch.nn.modules.loss._Loss): class MarginLoss(torch.nn.modules.loss._Loss):
def __init__(self, def __init__(self,
margin=0.3, margin=0.3,
size_average=None, size_average=None,
@ -133,6 +144,7 @@ class MarginLoss(torch.nn.modules.loss._Loss):
class NeuralGasEnergy(torch.nn.Module): class NeuralGasEnergy(torch.nn.Module):
def __init__(self, lm, **kwargs): def __init__(self, lm, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.lm = lm self.lm = lm
@ -153,6 +165,7 @@ class NeuralGasEnergy(torch.nn.Module):
class GrowingNeuralGasEnergy(NeuralGasEnergy): class GrowingNeuralGasEnergy(NeuralGasEnergy):
def __init__(self, topology_layer, **kwargs): def __init__(self, topology_layer, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.topology_layer = topology_layer self.topology_layer = topology_layer

View File

@ -82,23 +82,27 @@ def stratified_prod_pooling(values: torch.Tensor,
class StratifiedSumPooling(torch.nn.Module): class StratifiedSumPooling(torch.nn.Module):
"""Thin wrapper over the `stratified_sum_pooling` function.""" """Thin wrapper over the `stratified_sum_pooling` function."""
def forward(self, values, labels):
def forward(self, values, labels): # pylint: disable=no-self-use
return stratified_sum_pooling(values, labels) return stratified_sum_pooling(values, labels)
class StratifiedProdPooling(torch.nn.Module): class StratifiedProdPooling(torch.nn.Module):
"""Thin wrapper over the `stratified_prod_pooling` function.""" """Thin wrapper over the `stratified_prod_pooling` function."""
def forward(self, values, labels):
def forward(self, values, labels): # pylint: disable=no-self-use
return stratified_prod_pooling(values, labels) return stratified_prod_pooling(values, labels)
class StratifiedMinPooling(torch.nn.Module): class StratifiedMinPooling(torch.nn.Module):
"""Thin wrapper over the `stratified_min_pooling` function.""" """Thin wrapper over the `stratified_min_pooling` function."""
def forward(self, values, labels):
def forward(self, values, labels): # pylint: disable=no-self-use
return stratified_min_pooling(values, labels) return stratified_min_pooling(values, labels)
class StratifiedMaxPooling(torch.nn.Module): class StratifiedMaxPooling(torch.nn.Module):
"""Thin wrapper over the `stratified_max_pooling` function.""" """Thin wrapper over the `stratified_max_pooling` function."""
def forward(self, values, labels):
def forward(self, values, labels): # pylint: disable=no-self-use
return stratified_max_pooling(values, labels) return stratified_max_pooling(values, labels)

View File

@ -21,7 +21,7 @@ def cosine_similarity(x, y):
Expected dimension of x is 2. Expected dimension of x is 2.
Expected dimension of y is 2. Expected dimension of y is 2.
""" """
x, y = [arr.view(arr.size(0), -1) for arr in (x, y)] x, y = (arr.view(arr.size(0), -1) for arr in (x, y))
norm_x = x.pow(2).sum(1).sqrt() norm_x = x.pow(2).sum(1).sqrt()
norm_y = y.pow(2).sum(1).sqrt() norm_y = y.pow(2).sum(1).sqrt()
norm_mat = norm_x.unsqueeze(-1) @ norm_y.unsqueeze(-1).T norm_mat = norm_x.unsqueeze(-1) @ norm_y.unsqueeze(-1).T

View File

@ -5,19 +5,19 @@ from torch.nn.parameter import Parameter
from .initializers import ( from .initializers import (
AbstractLinearTransformInitializer, AbstractLinearTransformInitializer,
EyeTransformInitializer, EyeLinearTransformInitializer,
) )
class LinearTransform(torch.nn.Module): class LinearTransform(torch.nn.Module):
def __init__( def __init__(
self, self,
in_dim: int, in_dim: int,
out_dim: int, out_dim: int,
initializer: initializer:
AbstractLinearTransformInitializer = EyeTransformInitializer(), AbstractLinearTransformInitializer = EyeLinearTransformInitializer()):
**kwargs): super().__init__()
super().__init__(**kwargs)
self.set_weights(in_dim, out_dim, initializer) self.set_weights(in_dim, out_dim, initializer)
@property @property
@ -32,12 +32,15 @@ class LinearTransform(torch.nn.Module):
in_dim: int, in_dim: int,
out_dim: int, out_dim: int,
initializer: initializer:
AbstractLinearTransformInitializer = EyeTransformInitializer()): AbstractLinearTransformInitializer = EyeLinearTransformInitializer()):
weights = initializer.generate(in_dim, out_dim) weights = initializer.generate(in_dim, out_dim)
self._register_weights(weights) self._register_weights(weights)
def forward(self, x): def forward(self, x):
return x @ self.weights.T return x @ self._weights
def extra_repr(self):
return f"weights: (shape: {tuple(self._weights.shape)})"
# Aliases # Aliases

View File

@ -1,6 +1,6 @@
"""ProtoTorch datasets""" """ProtoTorch datasets"""
from .abstract import NumpyDataset from .abstract import CSVDataset, NumpyDataset
from .sklearn import ( from .sklearn import (
Blobs, Blobs,
Circles, Circles,
@ -10,3 +10,4 @@ from .sklearn import (
) )
from .spiral import Spiral from .spiral import Spiral
from .tecator import Tecator from .tecator import Tecator
from .xor import XOR

View File

@ -10,6 +10,7 @@ https://github.com/pytorch/vision/blob/master/torchvision/datasets/mnist.py
import os import os
import numpy as np
import torch import torch
@ -19,7 +20,7 @@ class Dataset(torch.utils.data.Dataset):
_repr_indent = 2 _repr_indent = 2
def __init__(self, root): def __init__(self, root):
if isinstance(root, torch._six.string_classes): if isinstance(root, str):
root = os.path.expanduser(root) root = os.path.expanduser(root)
self.root = root self.root = root
@ -92,8 +93,23 @@ class ProtoDataset(Dataset):
class NumpyDataset(torch.utils.data.TensorDataset): class NumpyDataset(torch.utils.data.TensorDataset):
"""Create a PyTorch TensorDataset from NumPy arrays.""" """Create a PyTorch TensorDataset from NumPy arrays."""
def __init__(self, data, targets): def __init__(self, data, targets):
self.data = torch.Tensor(data) self.data = torch.Tensor(data)
self.targets = torch.LongTensor(targets) self.targets = torch.LongTensor(targets)
tensors = [self.data, self.targets] tensors = [self.data, self.targets]
super().__init__(*tensors) super().__init__(*tensors)
class CSVDataset(NumpyDataset):
"""Create a Dataset from a CSV file."""
def __init__(self, filepath, target_col=-1, delimiter=',', skip_header=0):
raw = np.genfromtxt(
filepath,
delimiter=delimiter,
skip_header=skip_header,
)
data = np.delete(raw, 1, target_col)
targets = raw[:, target_col]
super().__init__(data, targets)

View File

@ -5,14 +5,21 @@ URL:
""" """
from __future__ import annotations
import warnings import warnings
from typing import Sequence, Union from typing import Sequence
from sklearn.datasets import (
load_iris,
make_blobs,
make_circles,
make_classification,
make_moons,
)
from prototorch.datasets.abstract import NumpyDataset from prototorch.datasets.abstract import NumpyDataset
from sklearn.datasets import (load_iris, make_blobs, make_circles,
make_classification, make_moons)
class Iris(NumpyDataset): class Iris(NumpyDataset):
"""Iris Dataset by Ronald Fisher introduced in 1936. """Iris Dataset by Ronald Fisher introduced in 1936.
@ -35,9 +42,10 @@ class Iris(NumpyDataset):
:param dims: select a subset of dimensions :param dims: select a subset of dimensions
""" """
def __init__(self, dims: Sequence[int] = None):
def __init__(self, dims: Sequence[int] | None = None):
x, y = load_iris(return_X_y=True) x, y = load_iris(return_X_y=True)
if dims: if dims is not None:
x = x[:, dims] x = x[:, dims]
super().__init__(x, y) super().__init__(x, y)
@ -49,15 +57,20 @@ class Blobs(NumpyDataset):
https://scikit-learn.org/stable/datasets/sample_generators.html#sample-generators. https://scikit-learn.org/stable/datasets/sample_generators.html#sample-generators.
""" """
def __init__(self,
def __init__(
self,
num_samples: int = 300, num_samples: int = 300,
num_features: int = 2, num_features: int = 2,
seed: Union[None, int] = 0): seed: None | int = 0,
x, y = make_blobs(num_samples, ):
x, y = make_blobs(
num_samples,
num_features, num_features,
centers=None, centers=None,
random_state=seed, random_state=seed,
shuffle=False) shuffle=False,
)
super().__init__(x, y) super().__init__(x, y)
@ -69,21 +82,25 @@ class Random(NumpyDataset):
Note: n_classes * n_clusters_per_class <= 2**n_informative must satisfy. Note: n_classes * n_clusters_per_class <= 2**n_informative must satisfy.
""" """
def __init__(self,
def __init__(
self,
num_samples: int = 300, num_samples: int = 300,
num_features: int = 2, num_features: int = 2,
num_classes: int = 2, num_classes: int = 2,
num_clusters: int = 2, num_clusters: int = 2,
num_informative: Union[None, int] = None, num_informative: None | int = None,
separation: float = 1.0, separation: float = 1.0,
seed: Union[None, int] = 0): seed: None | int = 0,
):
if not num_informative: if not num_informative:
import math import math
num_informative = math.ceil(math.log2(num_classes * num_clusters)) num_informative = math.ceil(math.log2(num_classes * num_clusters))
if num_features < num_informative: if num_features < num_informative:
warnings.warn("Generating more features than requested.") warnings.warn("Generating more features than requested.")
num_features = num_informative num_features = num_informative
x, y = make_classification(num_samples, x, y = make_classification(
num_samples,
num_features, num_features,
n_informative=num_informative, n_informative=num_informative,
n_redundant=0, n_redundant=0,
@ -91,7 +108,8 @@ class Random(NumpyDataset):
n_clusters_per_class=num_clusters, n_clusters_per_class=num_clusters,
class_sep=separation, class_sep=separation,
random_state=seed, random_state=seed,
shuffle=False) shuffle=False,
)
super().__init__(x, y) super().__init__(x, y)
@ -104,16 +122,21 @@ class Circles(NumpyDataset):
https://scikit-learn.org/stable/modules/generated/sklearn.datasets.make_circles.html https://scikit-learn.org/stable/modules/generated/sklearn.datasets.make_circles.html
""" """
def __init__(self,
def __init__(
self,
num_samples: int = 300, num_samples: int = 300,
noise: float = 0.3, noise: float = 0.3,
factor: float = 0.8, factor: float = 0.8,
seed: Union[None, int] = 0): seed: None | int = 0,
x, y = make_circles(num_samples, ):
x, y = make_circles(
num_samples,
noise=noise, noise=noise,
factor=factor, factor=factor,
random_state=seed, random_state=seed,
shuffle=False) shuffle=False,
)
super().__init__(x, y) super().__init__(x, y)
@ -126,12 +149,17 @@ class Moons(NumpyDataset):
https://scikit-learn.org/stable/modules/generated/sklearn.datasets.make_moons.html https://scikit-learn.org/stable/modules/generated/sklearn.datasets.make_moons.html
""" """
def __init__(self,
def __init__(
self,
num_samples: int = 300, num_samples: int = 300,
noise: float = 0.3, noise: float = 0.3,
seed: Union[None, int] = 0): seed: None | int = 0,
x, y = make_moons(num_samples, ):
x, y = make_moons(
num_samples,
noise=noise, noise=noise,
random_state=seed, random_state=seed,
shuffle=False) shuffle=False,
)
super().__init__(x, y) super().__init__(x, y)

View File

@ -9,6 +9,7 @@ def make_spiral(num_samples=500, noise=0.3):
For use in Prototorch use `prototorch.datasets.Spiral` instead. For use in Prototorch use `prototorch.datasets.Spiral` instead.
""" """
def get_samples(n, delta_t): def get_samples(n, delta_t):
points = [] points = []
for i in range(n): for i in range(n):
@ -52,6 +53,7 @@ class Spiral(torch.utils.data.TensorDataset):
:param num_samples: number of random samples :param num_samples: number of random samples
:param noise: noise added to the spirals :param noise: noise added to the spirals
""" """
def __init__(self, num_samples: int = 500, noise: float = 0.3): def __init__(self, num_samples: int = 500, noise: float = 0.3):
x, y = make_spiral(num_samples, noise) x, y = make_spiral(num_samples, noise)
super().__init__(torch.Tensor(x), torch.LongTensor(y)) super().__init__(torch.Tensor(x), torch.LongTensor(y))

View File

@ -36,13 +36,15 @@ Description:
are determined by analytic chemistry. are determined by analytic chemistry.
""" """
import logging
import os import os
import numpy as np import numpy as np
import torch import torch
from prototorch.datasets.abstract import ProtoDataset
from torchvision.datasets.utils import download_file_from_google_drive from torchvision.datasets.utils import download_file_from_google_drive
from prototorch.datasets.abstract import ProtoDataset
class Tecator(ProtoDataset): class Tecator(ProtoDataset):
""" """
@ -80,13 +82,11 @@ class Tecator(ProtoDataset):
if self._check_exists(): if self._check_exists():
return return
if self.verbose: logging.debug("Making directories...")
print("Making directories...")
os.makedirs(self.raw_folder, exist_ok=True) os.makedirs(self.raw_folder, exist_ok=True)
os.makedirs(self.processed_folder, exist_ok=True) os.makedirs(self.processed_folder, exist_ok=True)
if self.verbose: logging.debug("Downloading...")
print("Downloading...")
for fileid, md5 in self._resources: for fileid, md5 in self._resources:
filename = "tecator.npz" filename = "tecator.npz"
download_file_from_google_drive(fileid, download_file_from_google_drive(fileid,
@ -94,8 +94,7 @@ class Tecator(ProtoDataset):
filename=filename, filename=filename,
md5=md5) md5=md5)
if self.verbose: logging.debug("Processing...")
print("Processing...")
with np.load(os.path.join(self.raw_folder, "tecator.npz"), with np.load(os.path.join(self.raw_folder, "tecator.npz"),
allow_pickle=False) as f: allow_pickle=False) as f:
x_train, y_train = f["x_train"], f["y_train"] x_train, y_train = f["x_train"], f["y_train"]
@ -116,5 +115,4 @@ class Tecator(ProtoDataset):
"wb") as f: "wb") as f:
torch.save(test_set, f) torch.save(test_set, f)
if self.verbose: logging.debug("Done!")
print("Done!")

View File

@ -0,0 +1,19 @@
"""Exclusive-or (XOR) dataset for binary classification."""
import torch
def make_xor(num_samples=500):
x = torch.rand(num_samples, 2)
y = torch.zeros(num_samples)
y[torch.logical_and(x[:, 0] > 0.5, x[:, 1] < 0.5)] = 1
y[torch.logical_and(x[:, 1] > 0.5, x[:, 0] < 0.5)] = 1
return x, y
class XOR(torch.utils.data.TensorDataset):
"""Exclusive-or (XOR) dataset for binary classification."""
def __init__(self, num_samples: int = 500):
x, y = make_xor(num_samples)
super().__init__(x, y)

View File

@ -4,6 +4,7 @@ import torch
class LambdaLayer(torch.nn.Module): class LambdaLayer(torch.nn.Module):
def __init__(self, fn, name=None): def __init__(self, fn, name=None):
super().__init__() super().__init__()
self.fn = fn self.fn = fn
@ -17,6 +18,7 @@ class LambdaLayer(torch.nn.Module):
class LossLayer(torch.nn.modules.loss._Loss): class LossLayer(torch.nn.modules.loss._Loss):
def __init__(self, def __init__(self,
fn, fn,
name=None, name=None,

View File

@ -1,6 +1,11 @@
"""ProtoFlow utils module""" """ProtoTorch utils module"""
from .colors import hex_to_rgb, rgb_to_hex from .colors import (
get_colors,
get_legend_handles,
hex_to_rgb,
rgb_to_hex,
)
from .utils import ( from .utils import (
mesh2d, mesh2d,
parse_data_arg, parse_data_arg,

View File

@ -1,4 +1,13 @@
"""ProtoFlow color utilities""" """ProtoTorch color utilities"""
import matplotlib.lines as mlines
import torch
from matplotlib import cm
from matplotlib.colors import (
Normalize,
to_hex,
to_rgb,
)
def hex_to_rgb(hex_values): def hex_to_rgb(hex_values):
@ -13,3 +22,39 @@ def rgb_to_hex(rgb_values):
for v in rgb_values: for v in rgb_values:
c = "%02x%02x%02x" % tuple(v) c = "%02x%02x%02x" % tuple(v)
yield c yield c
def get_colors(vmax, vmin=0, cmap="viridis"):
cmap = cm.get_cmap(cmap)
colornorm = Normalize(vmin=vmin, vmax=vmax)
colors = dict()
for c in range(vmin, vmax + 1):
colors[c] = to_hex(cmap(colornorm(c)))
return colors
def get_legend_handles(colors, labels, marker="dots", zero_indexed=False):
handles = list()
for color, label in zip(colors.values(), labels):
if marker == "dots":
handle = mlines.Line2D(
xdata=[],
ydata=[],
label=label,
color="white",
markerfacecolor=color,
marker="o",
markersize=10,
markeredgecolor="k",
)
else:
handle = mlines.Line2D(
xdata=[],
ydata=[],
label=label,
color=color,
marker="",
markersize=15,
)
handles.append(handle)
return handles

View File

@ -1,13 +1,45 @@
"""ProtoFlow utilities""" """ProtoTorch utilities"""
import warnings import warnings
from typing import Union from typing import (
Dict,
Iterable,
List,
Optional,
Union,
)
import numpy as np import numpy as np
import torch import torch
from torch.utils.data import DataLoader, Dataset from torch.utils.data import DataLoader, Dataset
def generate_mesh(
minima: torch.TensorType,
maxima: torch.TensorType,
border: float = 1.0,
resolution: int = 100,
device: Optional[torch.device] = None,
):
# Apply Border
ptp = maxima - minima
shift = border * ptp
minima -= shift
maxima += shift
# Generate Mesh
minima = minima.to(device).unsqueeze(1)
maxima = maxima.to(device).unsqueeze(1)
factors = torch.linspace(0, 1, resolution, device=device)
marginals = factors * maxima + ((1 - factors) * minima)
single_dimensions = torch.meshgrid(*marginals)
mesh_input = torch.stack([dim.ravel() for dim in single_dimensions], dim=1)
return mesh_input, single_dimensions
def mesh2d(x=None, border: float = 1.0, resolution: int = 100): def mesh2d(x=None, border: float = 1.0, resolution: int = 100):
if x is not None: if x is not None:
x_shift = border * np.ptp(x[:, 0]) x_shift = border * np.ptp(x[:, 0])
@ -23,15 +55,16 @@ def mesh2d(x=None, border: float = 1.0, resolution: int = 100):
return mesh, xx, yy return mesh, xx, yy
def distribution_from_list(list_dist: list[int], clabels: list[int] = []): def distribution_from_list(list_dist: List[int],
clabels: Optional[Iterable[int]] = None):
clabels = clabels or list(range(len(list_dist))) clabels = clabels or list(range(len(list_dist)))
distribution = dict(zip(clabels, list_dist)) distribution = dict(zip(clabels, list_dist))
return distribution return distribution
def parse_distribution(user_distribution: Union[dict[int, int], dict[str, str], def parse_distribution(
list[int], tuple[int]], user_distribution,
clabels: list[int] = []) -> dict[int, int]: clabels: Optional[Iterable[int]] = None) -> Dict[int, int]:
"""Parse user-provided distribution. """Parse user-provided distribution.
Return a dictionary with integer keys that represent the class labels and Return a dictionary with integer keys that represent the class labels and
@ -75,9 +108,13 @@ def parse_distribution(user_distribution: Union[dict[int, int], dict[str, str],
def parse_data_arg(data_arg: Union[Dataset, DataLoader, list, tuple]): def parse_data_arg(data_arg: Union[Dataset, DataLoader, list, tuple]):
"""Return data and target as torch tensors.""" """Return data and target as torch tensors."""
if isinstance(data_arg, Dataset): if isinstance(data_arg, Dataset):
ds_size = len(data_arg) if hasattr(data_arg, "__len__"):
ds_size = len(data_arg) # type: ignore
loader = DataLoader(data_arg, batch_size=ds_size) loader = DataLoader(data_arg, batch_size=ds_size)
data, targets = next(iter(loader)) data, targets = next(iter(loader))
else:
emsg = f"Dataset {data_arg} is not sized (`__len__` unimplemented)."
raise TypeError(emsg)
elif isinstance(data_arg, DataLoader): elif isinstance(data_arg, DataLoader):
data = torch.tensor([]) data = torch.tensor([])

View File

@ -4,6 +4,7 @@ disable =
too-few-public-methods, too-few-public-methods,
fixme, fixme,
[pycodestyle] [pycodestyle]
max-line-length = 79 max-line-length = 79

View File

@ -1,10 +1,12 @@
""" """
_____ _ _______ _
| __ \ | | |__ __| | | ######
| |__) | __ ___ | |_ ___ | | ___ _ __ ___| |__ # # ##### #### ##### #### ##### #### ##### #### # #
| ___/ '__/ _ \| __/ _ \| |/ _ \| '__/ __| '_ \ # # # # # # # # # # # # # # # # # #
| | | | | (_) | || (_) | | (_) | | | (__| | | | ###### # # # # # # # # # # # # # ######
|_| |_| \___/ \__\___/|_|\___/|_| \___|_| |_| # ##### # # # # # # # # ##### # # #
# # # # # # # # # # # # # # # # #
# # # #### # #### # #### # # #### # #
ProtoTorch Core Package ProtoTorch Core Package
""" """
@ -13,20 +15,24 @@ from setuptools import find_packages, setup
PROJECT_URL = "https://github.com/si-cim/prototorch" PROJECT_URL = "https://github.com/si-cim/prototorch"
DOWNLOAD_URL = "https://github.com/si-cim/prototorch.git" DOWNLOAD_URL = "https://github.com/si-cim/prototorch.git"
with open("README.md", "r") as fh: with open("README.md", encoding="utf-8") as fh:
long_description = fh.read() long_description = fh.read()
INSTALL_REQUIRES = [ INSTALL_REQUIRES = [
"torch>=1.3.1", "torch>=2.0.0",
"torchvision>=0.5.0", "torchvision",
"numpy>=1.9.1", "numpy",
"sklearn", "scikit-learn",
"matplotlib",
] ]
DATASETS = [ DATASETS = [
"requests", "requests",
"tqdm", "tqdm",
] ]
DEV = ["bumpversion"] DEV = [
"bump2version",
"pre-commit",
]
DOCS = [ DOCS = [
"recommonmark", "recommonmark",
"sphinx", "sphinx",
@ -35,15 +41,17 @@ DOCS = [
"sphinx-autodoc-typehints", "sphinx-autodoc-typehints",
] ]
EXAMPLES = [ EXAMPLES = [
"matplotlib",
"torchinfo", "torchinfo",
] ]
TESTS = ["codecov", "pytest"] TESTS = [
"flake8",
"pytest",
]
ALL = DATASETS + DEV + DOCS + EXAMPLES + TESTS ALL = DATASETS + DEV + DOCS + EXAMPLES + TESTS
setup( setup(
name="prototorch", name="prototorch",
version="0.5.0", version="0.7.6",
description="Highly extensible, GPU-supported " description="Highly extensible, GPU-supported "
"Learning Vector Quantization (LVQ) toolbox " "Learning Vector Quantization (LVQ) toolbox "
"built using PyTorch and its nn API.", "built using PyTorch and its nn API.",
@ -54,30 +62,33 @@ setup(
url=PROJECT_URL, url=PROJECT_URL,
download_url=DOWNLOAD_URL, download_url=DOWNLOAD_URL,
license="MIT", license="MIT",
python_requires=">=3.8",
install_requires=INSTALL_REQUIRES, install_requires=INSTALL_REQUIRES,
extras_require={ extras_require={
"docs": DOCS,
"datasets": DATASETS, "datasets": DATASETS,
"dev": DEV,
"docs": DOCS,
"examples": EXAMPLES, "examples": EXAMPLES,
"tests": TESTS, "tests": TESTS,
"all": ALL, "all": ALL,
}, },
classifiers=[ classifiers=[
"Development Status :: 2 - Pre-Alpha",
"Environment :: Console", "Environment :: Console",
"Natural Language :: English",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Intended Audience :: Education", "Intended Audience :: Education",
"Intended Audience :: Science/Research", "Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Operating System :: OS Independent",
"Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
], ],
packages=find_packages(), packages=find_packages(),
zip_safe=False, zip_safe=False,

View File

@ -245,33 +245,45 @@ def test_random_reasonings_init_channels_not_first():
# Transform initializers # Transform initializers
def test_eye_transform_init_square(): def test_eye_transform_init_square():
t = pt.initializers.EyeTransformInitializer() t = pt.initializers.EyeLinearTransformInitializer()
I = t.generate(3, 3) I = t.generate(3, 3)
assert torch.allclose(I, torch.eye(3)) assert torch.allclose(I, torch.eye(3))
def test_eye_transform_init_narrow(): def test_eye_transform_init_narrow():
t = pt.initializers.EyeTransformInitializer() t = pt.initializers.EyeLinearTransformInitializer()
actual = t.generate(3, 2) actual = t.generate(3, 2)
desired = torch.Tensor([[1, 0], [0, 1], [0, 0]]) desired = torch.Tensor([[1, 0], [0, 1], [0, 0]])
assert torch.allclose(actual, desired) assert torch.allclose(actual, desired)
def test_eye_transform_init_wide(): def test_eye_transform_init_wide():
t = pt.initializers.EyeTransformInitializer() t = pt.initializers.EyeLinearTransformInitializer()
actual = t.generate(2, 3) actual = t.generate(2, 3)
desired = torch.Tensor([[1, 0, 0], [0, 1, 0]]) desired = torch.Tensor([[1, 0, 0], [0, 1, 0]])
assert torch.allclose(actual, desired) assert torch.allclose(actual, desired)
# Transforms # Transforms
def test_linear_transform(): def test_linear_transform_default_eye_init():
l = pt.transforms.LinearTransform(2, 4) l = pt.transforms.LinearTransform(2, 4)
actual = l.weights actual = l.weights
desired = torch.Tensor([[1, 0, 0, 0], [0, 1, 0, 0]]) desired = torch.Tensor([[1, 0, 0, 0], [0, 1, 0, 0]])
assert torch.allclose(actual, desired) assert torch.allclose(actual, desired)
def test_linear_transform_forward():
l = pt.transforms.LinearTransform(4, 2)
actual_weights = l.weights
desired_weights = torch.Tensor([[1, 0], [0, 1], [0, 0], [0, 0]])
assert torch.allclose(actual_weights, desired_weights)
actual_outputs = l(torch.Tensor([[1.1, 2.2, 3.3, 4.4], \
[1.1, 2.2, 3.3, 4.4], \
[5.5, 6.6, 7.7, 8.8]]))
desired_outputs = torch.Tensor([[1.1, 2.2], [1.1, 2.2], [5.5, 6.6]])
assert torch.allclose(actual_outputs, desired_outputs)
def test_linear_transform_zeros_init(): def test_linear_transform_zeros_init():
l = pt.transforms.LinearTransform( l = pt.transforms.LinearTransform(
in_dim=2, in_dim=2,
@ -392,6 +404,7 @@ def test_glvq_loss_one_hot_unequal():
# Activations # Activations
class TestActivations(unittest.TestCase): class TestActivations(unittest.TestCase):
def setUp(self): def setUp(self):
self.flist = ["identity", "sigmoid_beta", "swish_beta"] self.flist = ["identity", "sigmoid_beta", "swish_beta"]
self.x = torch.randn(1024, 1) self.x = torch.randn(1024, 1)
@ -406,6 +419,7 @@ class TestActivations(unittest.TestCase):
self.assertTrue(iscallable) self.assertTrue(iscallable)
def test_callable_deserialization(self): def test_callable_deserialization(self):
def dummy(x, **kwargs): def dummy(x, **kwargs):
return x return x
@ -450,6 +464,7 @@ class TestActivations(unittest.TestCase):
# Competitions # Competitions
class TestCompetitions(unittest.TestCase): class TestCompetitions(unittest.TestCase):
def setUp(self): def setUp(self):
pass pass
@ -503,6 +518,7 @@ class TestCompetitions(unittest.TestCase):
# Pooling # Pooling
class TestPooling(unittest.TestCase): class TestPooling(unittest.TestCase):
def setUp(self): def setUp(self):
pass pass
@ -603,6 +619,7 @@ class TestPooling(unittest.TestCase):
# Distances # Distances
class TestDistances(unittest.TestCase): class TestDistances(unittest.TestCase):
def setUp(self): def setUp(self):
self.nx, self.mx = 32, 2048 self.nx, self.mx = 32, 2048
self.ny, self.my = 8, 2048 self.ny, self.my = 8, 2048

View File

@ -1,7 +1,6 @@
"""ProtoTorch datasets test suite""" """ProtoTorch datasets test suite"""
import os import os
import shutil
import unittest import unittest
import numpy as np import numpy as np
@ -12,6 +11,7 @@ from prototorch.datasets.abstract import Dataset, ProtoDataset
class TestAbstract(unittest.TestCase): class TestAbstract(unittest.TestCase):
def setUp(self): def setUp(self):
self.ds = Dataset("./artifacts") self.ds = Dataset("./artifacts")
@ -28,6 +28,7 @@ class TestAbstract(unittest.TestCase):
class TestProtoDataset(unittest.TestCase): class TestProtoDataset(unittest.TestCase):
def test_download(self): def test_download(self):
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
_ = ProtoDataset("./artifacts", download=True) _ = ProtoDataset("./artifacts", download=True)
@ -38,6 +39,7 @@ class TestProtoDataset(unittest.TestCase):
class TestNumpyDataset(unittest.TestCase): class TestNumpyDataset(unittest.TestCase):
def test_list_init(self): def test_list_init(self):
ds = pt.datasets.NumpyDataset([1], [1]) ds = pt.datasets.NumpyDataset([1], [1])
self.assertEqual(len(ds), 1) self.assertEqual(len(ds), 1)
@ -49,13 +51,33 @@ class TestNumpyDataset(unittest.TestCase):
self.assertEqual(len(ds), 3) self.assertEqual(len(ds), 3)
class TestCSVDataset(unittest.TestCase):
def setUp(self):
data = np.random.rand(100, 4)
targets = np.random.randint(2, size=(100, 1))
arr = np.hstack([data, targets])
if not os.path.exists("./artifacts"):
os.mkdir("./artifacts")
np.savetxt("./artifacts/test.csv", arr, delimiter=",")
def test_len(self):
ds = pt.datasets.CSVDataset("./artifacts/test.csv")
self.assertEqual(len(ds), 100)
def tearDown(self):
os.remove("./artifacts/test.csv")
class TestSpiral(unittest.TestCase): class TestSpiral(unittest.TestCase):
def test_init(self): def test_init(self):
ds = pt.datasets.Spiral(num_samples=10) ds = pt.datasets.Spiral(num_samples=10)
self.assertEqual(len(ds), 10) self.assertEqual(len(ds), 10)
class TestIris(unittest.TestCase): class TestIris(unittest.TestCase):
def setUp(self): def setUp(self):
self.ds = pt.datasets.Iris() self.ds = pt.datasets.Iris()
@ -71,90 +93,94 @@ class TestIris(unittest.TestCase):
class TestBlobs(unittest.TestCase): class TestBlobs(unittest.TestCase):
def test_size(self): def test_size(self):
ds = pt.datasets.Blobs(num_samples=10) ds = pt.datasets.Blobs(num_samples=10)
self.assertEqual(len(ds), 10) self.assertEqual(len(ds), 10)
class TestRandom(unittest.TestCase): class TestRandom(unittest.TestCase):
def test_size(self): def test_size(self):
ds = pt.datasets.Random(num_samples=10) ds = pt.datasets.Random(num_samples=10)
self.assertEqual(len(ds), 10) self.assertEqual(len(ds), 10)
class TestCircles(unittest.TestCase): class TestCircles(unittest.TestCase):
def test_size(self): def test_size(self):
ds = pt.datasets.Circles(num_samples=10) ds = pt.datasets.Circles(num_samples=10)
self.assertEqual(len(ds), 10) self.assertEqual(len(ds), 10)
class TestMoons(unittest.TestCase): class TestMoons(unittest.TestCase):
def test_size(self): def test_size(self):
ds = pt.datasets.Moons(num_samples=10) ds = pt.datasets.Moons(num_samples=10)
self.assertEqual(len(ds), 10) self.assertEqual(len(ds), 10)
class TestTecator(unittest.TestCase): # class TestTecator(unittest.TestCase):
def setUp(self): # def setUp(self):
self.artifacts_dir = "./artifacts/Tecator" # self.artifacts_dir = "./artifacts/Tecator"
self._remove_artifacts() # self._remove_artifacts()
def _remove_artifacts(self): # def _remove_artifacts(self):
if os.path.exists(self.artifacts_dir): # if os.path.exists(self.artifacts_dir):
shutil.rmtree(self.artifacts_dir) # shutil.rmtree(self.artifacts_dir)
def test_download_false(self): # def test_download_false(self):
rootdir = self.artifacts_dir.rpartition("/")[0] # rootdir = self.artifacts_dir.rpartition("/")[0]
self._remove_artifacts() # self._remove_artifacts()
with self.assertRaises(RuntimeError): # with self.assertRaises(RuntimeError):
_ = pt.datasets.Tecator(rootdir, download=False) # _ = pt.datasets.Tecator(rootdir, download=False)
def test_download_caching(self): # def test_download_caching(self):
rootdir = self.artifacts_dir.rpartition("/")[0] # rootdir = self.artifacts_dir.rpartition("/")[0]
_ = pt.datasets.Tecator(rootdir, download=True, verbose=False) # _ = pt.datasets.Tecator(rootdir, download=True, verbose=False)
_ = pt.datasets.Tecator(rootdir, download=False, verbose=False) # _ = pt.datasets.Tecator(rootdir, download=False, verbose=False)
def test_repr(self): # def test_repr(self):
rootdir = self.artifacts_dir.rpartition("/")[0] # rootdir = self.artifacts_dir.rpartition("/")[0]
train = pt.datasets.Tecator(rootdir, download=True, verbose=True) # train = pt.datasets.Tecator(rootdir, download=True, verbose=True)
self.assertTrue("Split: Train" in train.__repr__()) # self.assertTrue("Split: Train" in train.__repr__())
def test_download_train(self): # def test_download_train(self):
rootdir = self.artifacts_dir.rpartition("/")[0] # rootdir = self.artifacts_dir.rpartition("/")[0]
train = pt.datasets.Tecator(root=rootdir, # train = pt.datasets.Tecator(root=rootdir,
train=True, # train=True,
download=True, # download=True,
verbose=False) # verbose=False)
train = pt.datasets.Tecator(root=rootdir, download=True, verbose=False) # train = pt.datasets.Tecator(root=rootdir, download=True, verbose=False)
x_train, y_train = train.data, train.targets # x_train, y_train = train.data, train.targets
self.assertEqual(x_train.shape[0], 144) # self.assertEqual(x_train.shape[0], 144)
self.assertEqual(y_train.shape[0], 144) # self.assertEqual(y_train.shape[0], 144)
self.assertEqual(x_train.shape[1], 100) # self.assertEqual(x_train.shape[1], 100)
def test_download_test(self): # def test_download_test(self):
rootdir = self.artifacts_dir.rpartition("/")[0] # rootdir = self.artifacts_dir.rpartition("/")[0]
test = pt.datasets.Tecator(root=rootdir, train=False, verbose=False) # test = pt.datasets.Tecator(root=rootdir, train=False, verbose=False)
x_test, y_test = test.data, test.targets # x_test, y_test = test.data, test.targets
self.assertEqual(x_test.shape[0], 71) # self.assertEqual(x_test.shape[0], 71)
self.assertEqual(y_test.shape[0], 71) # self.assertEqual(y_test.shape[0], 71)
self.assertEqual(x_test.shape[1], 100) # self.assertEqual(x_test.shape[1], 100)
def test_class_to_idx(self): # def test_class_to_idx(self):
rootdir = self.artifacts_dir.rpartition("/")[0] # rootdir = self.artifacts_dir.rpartition("/")[0]
test = pt.datasets.Tecator(root=rootdir, train=False, verbose=False) # test = pt.datasets.Tecator(root=rootdir, train=False, verbose=False)
_ = test.class_to_idx # _ = test.class_to_idx
def test_getitem(self): # def test_getitem(self):
rootdir = self.artifacts_dir.rpartition("/")[0] # rootdir = self.artifacts_dir.rpartition("/")[0]
test = pt.datasets.Tecator(root=rootdir, train=False, verbose=False) # test = pt.datasets.Tecator(root=rootdir, train=False, verbose=False)
x, y = test[0] # x, y = test[0]
self.assertEqual(x.shape[0], 100) # self.assertEqual(x.shape[0], 100)
self.assertIsInstance(y, int) # self.assertIsInstance(y, int)
def test_loadable_with_dataloader(self): # def test_loadable_with_dataloader(self):
rootdir = self.artifacts_dir.rpartition("/")[0] # rootdir = self.artifacts_dir.rpartition("/")[0]
test = pt.datasets.Tecator(root=rootdir, train=False, verbose=False) # test = pt.datasets.Tecator(root=rootdir, train=False, verbose=False)
_ = torch.utils.data.DataLoader(test, batch_size=64, shuffle=True) # _ = torch.utils.data.DataLoader(test, batch_size=64, shuffle=True)
def tearDown(self): # def tearDown(self):
self._remove_artifacts() # self._remove_artifacts()