diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index d2973f9d..6fa792a7 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - develop tags: '*' pull_request: diff --git a/Project.toml b/Project.toml index ad551910..4abdcab1 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ name = "AdaptiveResonance" uuid = "3d72adc0-63d3-4141-bf9b-84450dd0395b" authors = ["Sasha Petrenko"] description = "A Julia package for Adaptive Resonance Theory (ART) algorithms." -version = "0.3.7" +version = "0.4.0" [deps] Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" diff --git a/README.md b/README.md index 9b6c535a..ff5c1252 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,7 @@ A Julia package for Adaptive Resonance Theory (ART) algorithms. |:------------------:|:----------------:|:------------:| | [![Stable][docs-stable-img]][docs-stable-url] | [![Build Status][ci-img]][ci-url] | [![Codecov][codecov-img]][codecov-url] | | [![Dev][docs-dev-img]][docs-dev-url] | [![Build Status][appveyor-img]][appveyor-url] | [![Coveralls][coveralls-img]][coveralls-url] | - | **Dependents** | **Date** | **Status** | -|:--------------:|:--------:|:----------:| | [![deps][deps-img]][deps-url] | [![version][version-img]][version-url] | [![pkgeval][pkgeval-img]][pkgeval-url] | [deps-img]: https://juliahub.com/docs/AdaptiveResonance/deps.svg @@ -54,7 +52,7 @@ Please read the [documentation](https://ap6yc.github.io/AdaptiveResonance.jl/dev - [Implemented Modules](#implemented-modules) - [Structure](#structure) - [History](#history) - - [Credits](#credits) + - [Acknowledgements](#acknowledgements) - [Authors](#authors) - [Software](#software) - [Datasets](#datasets) @@ -133,19 +131,59 @@ opts = opts_DDVFA(rho_ub=0.75, rho_lb=0.4) art = DDVFA(opts) ``` +Train and test the models with `train!` and `classify`: + +```julia +# Unsupervised ART module +art = DDVFA() + +# Supervised ARTMAP module +artmap = SFAM() + +# Load some data +train_x, train_y, test_x, test_y = load_your_data() + +# Unsupervised training and testing +train!(art, train_x) +y_hat_art = classify(art, test_x) + +# Supervised training and testing +train!(artmap, train_x, train_y) +y_hat_artmap = classify(art, test_x) +``` + +`train!` and `classify` can accept incremental or batch data, where rows are features and columns are samples. + +Unsupervised ART modules can also accommodate simple supervised learning where internal categories are mapped to supervised labels with the keyword argument `y`: + +```julia +# Unsupervised ART module +art = DDVFA() +train!(art, train_x, y=train_y) +``` + +These modules also support retrieving the "best-matching unit" in the case of complete mismatch (i.e., the next-best category if the presented sample is completely unrecognized) with the keyword argument `get_bmu`: + +```julia +# Get the best-matching unit in the case of complete mismatch +y_hat_bmu = classify(art, test_x, get_bmu=true) +``` + ## Implemented Modules This project has implementations of the following ART (unsupervised) and ARTMAP (supervised) modules: - ART - - **DDVFA**: Distributed Dual Vigilance Fuzzy ART + - **FuzzyART**: Fuzzy ART - **DVFA**: Dual Vigilance Fuzzy ART - - **GNFA**: Gamma-Normalized Fuzzy ART + - **DDVFA**: Distributed Dual Vigilance Fuzzy ART - ARTMAP - **SFAM**: Simplified Fuzzy ARTMAP - **FAM**: Fuzzy ARTMAP - **DAM**: Default ARTMAP +Because each of these modules is a framework for many variants in the literature, this project also implements these [variants](https://ap6yc.github.io/AdaptiveResonance.jl/dev/man/modules/) by changing their module [options](https://ap6yc.github.io/AdaptiveResonance.jl/dev/man/guide/#art_options). + In addition to these modules, this package contains the following accessory methods: - **ARTSCENE**: the ARTSCENE algorithm's multiple-stage filtering process is implemented as `artscene_filter`. Each filter stage is exported if further granularity is required. @@ -183,7 +221,7 @@ AdaptiveResonance - 2/8/2021 - Formalize usage documentation. - 10/13/2021 - Initiate GitFlow contribution. -## Credits +## Acknowledgements ### Authors diff --git a/docs/Project.toml b/docs/Project.toml index 9b142ab5..646c147c 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,4 +1,7 @@ [deps] AdaptiveResonance = "3d72adc0-63d3-4141-bf9b-84450dd0395b" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MLDataUtils = "cc2ba9b6-d476-5e6d-8eaf-a92d5412d41d" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" diff --git a/docs/make.jl b/docs/make.jl index e65b0b3a..1ede48ec 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,17 +1,27 @@ -using Documenter, AdaptiveResonance +using Documenter +using AdaptiveResonance makedocs( modules=[AdaptiveResonance], - format=Documenter.HTML(prettyurls = get(ENV, "CI", nothing) == "true"), - # format=Documenter.HTML(), + format=Documenter.HTML( + prettyurls = get(ENV, "CI", nothing) == "true", + assets = [ + joinpath("assets", "favicon.ico") + ] + ), pages=[ "Home" => "index.md", + "Getting Started" => [ + "getting-started/whatisart.md", + "getting-started/basic-example.md", + ], "Tutorial" => [ "Guide" => "man/guide.md", "Examples" => "man/examples.md", + "Modules" => "man/modules.md", "Contributing" => "man/contributing.md", - "Index" => "man/full-index.md" - ] + "Index" => "man/full-index.md", + ], ], repo="https://github.com/AP6YC/AdaptiveResonance.jl/blob/{commit}{path}#L{line}", sitename="AdaptiveResonance.jl", @@ -21,4 +31,5 @@ makedocs( deploydocs( repo="github.com/AP6YC/AdaptiveResonance.jl.git", + devbranch="develop", ) diff --git a/docs/src/assets/favicon.ico b/docs/src/assets/favicon.ico new file mode 100644 index 00000000..c20690b4 Binary files /dev/null and b/docs/src/assets/favicon.ico differ diff --git a/docs/src/assets/figures/art.png b/docs/src/assets/figures/art.png new file mode 100644 index 00000000..47728937 Binary files /dev/null and b/docs/src/assets/figures/art.png differ diff --git a/docs/src/assets/header.png b/docs/src/assets/header.png new file mode 100644 index 00000000..7ea7792c Binary files /dev/null and b/docs/src/assets/header.png differ diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png new file mode 100644 index 00000000..24efd77f Binary files /dev/null and b/docs/src/assets/logo.png differ diff --git a/docs/src/getting-started/basic-example.md b/docs/src/getting-started/basic-example.md new file mode 100644 index 00000000..70e08b21 --- /dev/null +++ b/docs/src/getting-started/basic-example.md @@ -0,0 +1,60 @@ +# Basic Example + +In the example below, we create a dataset generated from two multivariate Gaussian distributions in two dimensions, showing how an ART module can be used in unsupervised or simple supervised modes alongside an ARTMAP module that is explicitly supervised-only. + +```@example +# Copyright © 2021 Alexander L. Hayes +# MIT License + +using AdaptiveResonance +using Distributions, Random +using MLDataUtils +using Plots + +""" +Demonstrates Unsupervised DDVFA, Supervised DDVFA, and (Supervised) SFAM on a toy problem +with two multivariate Gaussians. +""" + +# Setup two multivariate Gaussians and sampling 1000 points from each. + +rng = MersenneTwister(1234) +dist1 = MvNormal([0.0, 6.0], [1.0 0.0; 0.0 1.0]) +dist2 = MvNormal([4.5, 6.0], [2.0 -1.5; -1.5 2.0]) + +N_POINTS = 1000 + +X = hcat(rand(rng, dist1, N_POINTS), rand(rng, dist2, N_POINTS)) +y = vcat(ones(Int64, N_POINTS), zeros(Int64, N_POINTS)) + +p1 = scatter(X[1,:], X[2,:], group=y, title="Original Data") + +(X_train, y_train), (X_test, y_test) = stratifiedobs((X, y)) + +# Standardize data types +X_train = convert(Matrix{Float64}, X_train) +X_test = convert(Matrix{Float64}, X_test) +y_train = convert(Vector{Int}, y_train) +y_test = convert(Vector{Int}, y_test) + +# Unsupervised DDVFA +art = DDVFA() +train!(art, X_train) +y_hat_test = AdaptiveResonance.classify(art, X_test) +p2 = scatter(X_test[1,:], X_test[2,:], group=y_hat_test, title="Unsupervised DDVFA") + +# Supervised DDVFA +art = DDVFA() +train!(art, X_train, y=y_train) +y_hat_test = AdaptiveResonance.classify(art, X_test) +p3 = scatter(X_test[1,:], X_test[2,:], group=y_hat_test, title="Supervised DDVFA", xlabel="Performance: " * string(round(performance(y_hat_test, y_test); digits=3))) + +# Supervised SFAM +art = SFAM() +train!(art, X_train, y_train) +y_hat_test = AdaptiveResonance.classify(art, X_test) +p4 = scatter(X_test[1,:], X_test[2,:], group=y_hat_test, title="Supervised SFAM", xlabel="Performance: " * string(round(performance(y_hat_test, y_test); digits=3))) + +# Performance Measure + display the plots +plot(p1, p2, p3, p4, layout=(1, 4), legend = false, xtickfontsize=6, xguidefontsize=8, titlefont=font(8)) +``` diff --git a/docs/src/getting-started/whatisart.md b/docs/src/getting-started/whatisart.md new file mode 100644 index 00000000..5909cc41 --- /dev/null +++ b/docs/src/getting-started/whatisart.md @@ -0,0 +1,65 @@ +# Background + +This page provides a theoretical overview of Adaptive Resonance Theory and what this project aims to accomplish. + +## What is Adaptive Resonance Theory? + +Adaptive Resonance Theory (commonly abbreviated to ART) is both a **neurological theory** and a **family of neurogenitive neural network models for machine learning**. + +ART began as a neurocognitive theory of how fields of cells can continuously learn stable representations, and it evolved into the basis for a myriad of practical machine learning algorithms. +Pioneered by Stephen Grossberg and Gail Carpenter, the field has had contributions across many years and from many disciplines, resulting in a plethora of engineering applications and theoretical advancements that have enabled ART-based algorithms to compete with many other modern learning and clustering algorithms. + +Because of the high degree of interplay between the neurocognitive theory and the engineering models born of it, the term ART is frequently used to refer to both in the modern day (for better or for worse). + +Stephen Grossberg's has recently released a book summarizing the work of him, his wife Gail Carpenter, and his colleagues on Adaptive Resonance Theory in his book [Conscious Brain, Resonant Mind](https://www.amazon.com/Conscious-Mind-Resonant-Brain-Makes/dp/0190070552). + +## ART Basics + +![art](../assets/figures/art.png) + +### ART Dynamics + +Nearly every ART model shares a basic set of dynamics: + +1. ART models typically have two layers/fields denoted F1 and F2. +2. The F1 field is the feature representation field. + Most often, it is simply the input feature sample itself (after some necessary preprocessing). +3. The F2 field is the category representation field. + With some exceptions, each node in the F2 field generally represents its own category. + This is most easily understood as a weight vector representing a prototype for a class or centroid of a cluster. +4. An activation function is used to find the order of categories "most activated" for a given sample in F1. +5. In order of highest activation, a match function is used to compute the agreement between the sample and the categories. +6. If the match function for a category evaluates to a value above a threshold known as the vigilance parameter ($$\rho$$), the weights of that category may be updated according to a learning rule. +7. If there is complete mismatch across all categories, then a new categories is created according to some instantiation rule. + +### ART Considerations + +In addition to the dynamics typical of an ART model, you must know: + +1. ART models are inherently designed for unsupervised learning (i.e., learning in the absense of supervisory labels for samples). + This is also known as clustering. +2. ART models are capable of supervised learning and reinforcement learning through some redesign and/or combination of ART models. + For example, ARTMAP models are combinations of two ART models in a special way, one learning feature-to-category mappings and another learning category-to-label mappingss. + ART modules are used for reinforcement learning by representing the mappings between state, value, and action spaces with ART dynamics. +3. Almost all ART models face the problem of the appropriate selection of the vigilance parameter, which may depend in its optimality according to the problem. +4. Being a class of neurogenitive neural network models, ART models gain the ability for theoretically infinite capacity along with the problem of "category proliferation," which is the undesirable increase in the number of categories as the model continues to learn, leading to increasing computational time. + In contrast, while the evaluation time of a deep neural network is always *exactly the same*, there exist upper bounds in their representational capacity. +5. Nearly every ART model requires feature normalization (i.e., feature elements lying within $$[0,1]$$) and a process known as complement coding where the feature vector is appended to its vector complement $$[1-\bar{x}]$$. + This is because real-numbered vectors can be arbitrarily close to one another, hindering learning performance, which requires a degree of contrast enhancement between samples to ensure their separation. + +To learn about their implementations, nearly every practical ART model is listed in a recent [ART survey paper by Leonardo Enzo Brito da Silva](https://arxiv.org/abs/1905.11437). + +## History and Development + +At a high level, ART began with a neural network model known as the Grossberg Network named after Stephen Grossberg. +This network treats the firing of neurons in frequency domain as basic shunting models, which are recurrently connected to increase their own activity while suppressing the activities of others nearby (i.e., on-center, off-surround). +Using this shunting model, Grossberg shows that autonomous, associative learning can occur with what are known as instar networks. + +By representing categories as a field of instar networks, new categories could be optimally learned by the instantiation of new neurons. +However, it was shown that the learning stability of Grossberg Networks degrades as the number of represented categories increases. +Discoveries in the neurocognitive theory and breakthroughs in their implementation led to the introduction of a recurrent connections between the two fields of the network to stabilize the learning. +These breakthroughs were based upon the discovery that autonomous learning depends on the interplay and agreement between *perception* and *expectation*, frequently referred to as bottom-up and top-down processes. +Furthermore, it is *resonance* between these states in the frequency domain that gives rise to conscious experiences and that permit adaptive weights to change, leading to the phenomenon of learning. +The theory has many explanatory consequences in psychology, such as why attention is required for learning, but its consequences in the engineering models are that it stabilizes learning in cooperative-competitive dynamics, such as interconnected fields of neurons, which are most often chaotic. + +Chapters 18 and 19 of the book by [Neural Network Design by Hagan, Demuth, Beale, and De Jesus](https://hagan.okstate.edu/NNDesign.pdf) provide a good theoretical basis for learning how these network models were eventually implemented into the first binary-vector implementation of ART1. diff --git a/docs/src/index.md b/docs/src/index.md index 13df2192..9cbfe3a1 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,3 +1,7 @@ +![header](assets/header.png) + +--- + # AdaptiveResonance.jl These pages serve as the official documentation for the AdaptiveResonance.jl Julia package. @@ -17,6 +21,7 @@ This documentation is split into the following sections: Pages = [ "man/guide.md", "man/examples.md", + "man/modules.md", "man/contributing.md", "man/full-index.md", ] @@ -24,5 +29,6 @@ Depth = 1 ``` The [Package Guide](@ref) provides a tutorial to the full usage of the package, while [Examples](@ref) gives sample workflows using a variety of ART modules. +A list of the implemented ART modules is included in [Modules](@ref), where different options are also listed for creating variants of these modules that exist in the literature. Instructions on how to contribute to the package are found in [Contributing](@ref), and docstrings for every element of the package is listed in the [Index](@ref main-index). diff --git a/docs/src/man/examples.md b/docs/src/man/examples.md index 7c64afcd..df174d80 100644 --- a/docs/src/man/examples.md +++ b/docs/src/man/examples.md @@ -9,6 +9,8 @@ All ART modules learn in an unsupervised (i.e. clustering) mode by default, but ### DDVFA Unsupervised +DDVFA is an unsupervised clustering algorithm by definition, so it can be used to cluster a set of samples all at once in batch mode. + ```julia # Load the data from some source with a train/test split train_x, train_y, test_x, test_y = load_some_data() @@ -25,6 +27,8 @@ y_hat_test = classify(art, test_x) ### DDVFA Supervised +ART modules such as DDVFA can also be used in simple supervised mode where provided labels are used in place of internal incremental labels for the clusters, providing a method of assessing the clustering performance when labels are available. + ```julia # Load the data from some source with a train/test split train_x, train_y, test_x, test_y = load_some_data() @@ -47,6 +51,9 @@ perf_test = performance(y_hat_test, test_y) ### Incremental DDVFA With Custom Options and Data Configuration +Even more advanced, DDVFA can be run incrementally (i.e. with one sample at a time) with custom algorithmic options and a predetermined data configuration. +It is necessary to provide a data configuration if the model is not pretrained because the model has no knowledge of the boundaries and dimensionality of the data, which are necessary in the complement coding step. + ```julia # Load the data from some source with a train/test split train_x, train_y, test_x, test_y = load_some_data() @@ -97,6 +104,8 @@ ARTMAP modules are supervised by definition, so the require supervised labels in ### SFAM +A Simplified FuzzyARTMAP can be used to learn supervised mappings on features directly and in batch mode. + ```julia # Load the data from some source with a train/test split train_x, train_y, test_x, test_y = load_some_data() @@ -116,8 +125,11 @@ perf_train = performance(y_hat_train, train_y) # Calculate testing performance perf_test = performance(y_hat_test, test_y) ``` + ### Incremental SFAM With Custom Options and Data Configuration +A simplified FuzzyARTMAP can also be run iteratively, assuming that we know the statistics of the features ahead of time and reflect that in the module's `config` with a `DataConfig` object. + ```julia # Load the data from some source with a train/test split train_x, train_y, test_x, test_y = load_some_data() diff --git a/docs/src/man/guide.md b/docs/src/man/guide.md index c156ff9a..d8b9cfed 100644 --- a/docs/src/man/guide.md +++ b/docs/src/man/guide.md @@ -39,6 +39,7 @@ To work with ART modules, you should know: - [Their basic methods](@ref methods) - [Incremental vs. batch modes](@ref incremental_vs_batch) - [Supervised vs. unsupervised learning modes](@ref supervised_vs_unsupervised) +- [Mismatch vs. Best-Matching-Unit](@ref mismatch-bmu) ### [Methods](@id methods) @@ -163,6 +164,19 @@ perf_test = performance(y_hat_test, test_y) However, many ART modules, though unsupervised by definition, can also be trained in a supervised way by naively mapping categories to labels (more in [ART vs. ARTMAP](@ref art_vs_artmap)). +### [Mismatch vs. Best-Matching-Unit](@id mismatch-bmu) + +During inference, ART algorithms report the category that satisfies the match/vigilance criterion (see [Background](@ref)). +By default, in the case that no category satisfies this criterion the module reports a *mismatch* as -1. +In modules that support it, a keyword argument `get_bmu` (default is `false`) can be used in the `classify` method to get the "best-matching unit", which is the category that maximizes the activation. +This can be interpreted as the "next-best guess" of the model in the case that the sample is sufficiently different from anything that the model has seen. +For example, + +```julia +# Conduct inference, getting the best-matching unit in case of complete mismatch +y_hat_bmu = classify(my_art, test_x, get_bmu=true) +``` + ## [ART Options](@id art_options) The AdaptiveResonance package is designed for maximum flexibility for scientific research, even though this may come at the cost of learning instability if misused. @@ -191,6 +205,13 @@ The options are objects from the [Parameters.jl](https://github.com/mauro3/Param my_art_opts = opts_DDVFA(gamma = 3) ``` +!!! note "Note" + As of version `0.3.6`, you can pass these keyword arguments directly to the ART model when constructing it with + + ```julia + my_art = DDVFA(gamma = 3) + ``` + You can even modify the parameters on the fly after the ART module has been instantiated by directly modifying the options within the module: ```julia diff --git a/docs/src/man/modules.md b/docs/src/man/modules.md new file mode 100644 index 00000000..a6349d47 --- /dev/null +++ b/docs/src/man/modules.md @@ -0,0 +1,37 @@ +# Modules + +This project implements a number of ART-based models with options that modulate their behavior (see the [options section of the Guide](@ref art_options)) + +This page lists both the [implemented models](@ref Implemented-Models) and some [variants](@ref Variants) + +## Implemented Models + +This project has implementations of the following ART (unsupervised) and ARTMAP (supervised) modules: + +- ART + - `FuzzyART`: Fuzzy ART + - `DVFA`: Dual Vigilance Fuzzy ART + - `DDVFA`: Distributed Dual Vigilance Fuzzy ART +- ARTMAP + - `SFAM`: Simplified Fuzzy ARTMAP + - `FAM`: Fuzzy ARTMAP + - `DAM`: Default ARTMAP + +## Variants + +Each module contains many [options](@ref art_options) that modulate its behavior. +Some of these options are used to modulate the internals of the module, such as switching the match and activation functions, to achieve different modules that are found in the literature. + +These variants are: + +- [`Gamma-Normalized FuzzyART`](@ref Gamma-Normalized-FuzzyART) + +### Gamma-Normalized FuzzyART + +A Gamma-Normalized FuzzyART is a FuzzyART module where the gamma normalization option is set on `gamma_normalization=true` and the kernel width parameter is set to $$\gamma >= 1.0$$ ($$\gamma_{ref}$$ is 1.0 by default): + +```julia +my_gnfa = FuzzyART(gamma_normalization=true, gamma=5.0) +``` + +The `gamma_normalization` flag must be set high here because it also changes the thresholding value and match function of the module. diff --git a/examples/art/gnfa.jl b/examples/art/gnfa.jl index 02734df3..f7667ea6 100644 --- a/examples/art/gnfa.jl +++ b/examples/art/gnfa.jl @@ -3,18 +3,23 @@ using Logging # Set the log level LogLevel(Logging.Info) -@info "GNFA Testing" +@info "FuzzyART Testing" # Auxiliary generic functions for loading data, etc. include("../../test/test_utils.jl") -# GNFA train and test -opts = opts_GNFA(rho=0.5) -my_gnfa = GNFA(opts) +# FuzzyART train and test +# opts = opts_FuzzyART(rho=0.6, gamma_ref = 1.0, gamma=1.0) +opts = opts_FuzzyART(rho=0.6, gamma = 5.0) +art = FuzzyART(opts) # data = load_am_data(200, 50) data = load_iris("data/Iris.csv") -local_complement_code = complement_code(data.train_x) +# local_complement_code = complement_code(data.train_x) -train!(my_gnfa, local_complement_code, y=data.train_y) -cc_test = complement_code(data.test_x) -y_hat = classify(my_gnfa, cc_test) +# train!(my_FuzzyART, local_complement_code, y=data.train_y) +train!(art, data.train_x, y=data.train_y) +# y_hat = classify(my_FuzzyART, data.test_x, get_bmu=true) +y_hat = classify(art, data.test_x) + +perf = performance(y_hat, data.test_y) +@info "Performance: $perf" diff --git a/examples/artmap/artmap.jl b/examples/artmap/artmap.jl index 5b635595..bd76e221 100644 --- a/examples/artmap/artmap.jl +++ b/examples/artmap/artmap.jl @@ -9,5 +9,5 @@ data = load_iris("data/Iris.csv") # Iterate over several ARTMAP modules for art in [SFAM, DAM] # Train and classify, returning the performance - perf = train_test_artmap(art(), data) + perf = train_test_art(art(), data) end diff --git a/paper/paper.bib b/paper/paper.bib index 79ef987a..75e92826 100644 --- a/paper/paper.bib +++ b/paper/paper.bib @@ -101,4 +101,137 @@ @article{DaSilva2019 issn = {0893-6080}, arxivId = {1905.11437}, keywords = {Adaptive resonance theory, Classification, Clustering, Regression, Reinforcement learning, Survey, adaptive resonance theory, classification, clustering, corresponding author, explainable, neural networks, regression, reinforcement learning, survey, unsupervised learning} -} \ No newline at end of file +} + +@inproceedings{Carpenter1991, +abstract = {Summary form only given. The authors introduced a neural network architecture, called ARTMAP, that autonomously learns to classify arbitrarily many, arbitrarily ordered vectors into recognition categories based on predictive success. This supervised learning system is built up from a pair of adaptive resonance theory modules (ARTa and ARTb) that are capable of self-organizing stable recognition categories in response to arbitrary sequences of input patterns. Tested on a benchmark machine learning database in both online and offline simulations, the ARTMAP system learns orders of magnitude more quickly, efficiently, and accurately than alternative algorithms, and achieves 100% accuracy after training on less than half of the input patterns in the database.}, +annote = {In this paper, Dr. Gail Carpenter introduces the ARTMAP algorithm, which introduces a supervisory learning mechanism to ordinarily unsupervised ART modules. This is done by introducing two ART modules (ARTa and ARTb) and a resonance-based connection between them, mapping categories one modules to labels in another.}, +author = {Carpenter, Gail A. and Grossberg, Stephen and Reynolds, John H.}, +booktitle = {IEEE Conference on Neural Networks for Ocean Engineering}, +doi = {10.1016/0893-6080(91)90012-T}, +file = {:C\:/Users/Sasha/AppData/Local/Mendeley Ltd./Mendeley Desktop/Downloaded/Carpenter, Grossberg, Reynolds - 1991 - ARTMAP Supervised real-time learning and classification of nonstationary data by a self-organizi.pdf:pdf}, +isbn = {0780302052}, +issn = {08936080}, +mendeley-groups = {ART}, +pages = {341--342}, +title = {{ARTMAP: Supervised real-time learning and classification of nonstationary data by a self-organizing neural network}}, +year = {1991} +} + +@article{Carpenter1992, + abstract = {— A new neural network architecture is introduced for incremental supervised learning of recognition categories and multidimensional maps in response to arbitrary sequences of analog or binary input vectors, which may represent fuzzy or crisp sets of features. The architecture, called fuzzy ARTMAP, achieves a synthesis of fuzzy logic and adaptive resonance theory (ART) neural networks by exploiting a close formal similarity between the computations of fuzzy subsethood and ART category choice, resonance, and learning. Fuzzy ARTMAP also realizes a new minimax learning rule that conjointly minimizes predictive error and maximizes code compression, or generalization. This is achieved by a match tracking process that increases the ART vigilance parameter by the minimum amount needed to correct a predictive error. As a result, the system automatically learns a minimal number of recognition categories, or “hidden units,” to meet accuracy criteria. Category proliferation is prevented by normalizing input vectors at a preprocessing stage. A normalization procedure called complement coding leads to a symmetric theory in which the and operator (V) and the OR operator (A) of fuzzy logic play complementary roles. Complement coding uses on cells and off cells to represent the input pattern, and preserves individual feature amplitudes while normalizing the total on cell/off cell vector. Learning is stable because all adaptive weights can only decrease in time. Decreasing weights correspond to increasing sizes of category “boxes.” Smaller vigilance values lead to larger category boxes. Improved prediction is achieved by training the system several times using different orderings of the input set. This voting strategy can also be used to assign confidence estimates to competing predictions given small, noisy, or incomplete training sets. Four classes of simulations illustrate fuzzy ARTMAP performance in relation to benchmark backpropagation and genetic algorithm systems. These simulations include (i) finding points inside versus outside a circle; (ii) learning to tell two spirals apart, (iii) incremental approximation of a piecewise-continuous function; and (iv) a letter recognition database. The fuzzy ARTMAP system is also compared with Salzberg's NGE system and with Simpson's FMMC system. {\textcopyright} 1992 IEEE}, + annote = {In this paper, Drs. Gail Carpenter and Stephen Grossberg demonstrate how .the use of fuzzy set theory operations in the ARTMAP algorithm augment its learning capabilities without sacrificing algorithmic complexity.}, + author = {Carpenter, Gail A. and Grossberg, Stephen and Markuzon, Natalya and Reynolds, John H. and Rosen, David B.}, + doi = {10.1109/72.159059}, + file = {:C\:/Users/Sasha/AppData/Local/Mendeley Ltd./Mendeley Desktop/Downloaded/Carpenter et al. - 1992 - Fuzzy ARTMAP A Neural Network Architecture for Incremental Supervised Learning of Analog Multidimensional Maps.pdf:pdf}, + issn = {19410093}, + journal = {IEEE Transactions on Neural Networks}, + mendeley-groups = {ART}, + number = {5}, + pages = {698--713}, + title = {{Fuzzy ARTMAP: A Neural Network Architecture for Incremental Supervised Learning of Analog Multidimensional Maps}}, + volume = {3}, + year = {1992} +} + +@inproceedings{ARTHestenes1987, + abstract = {In spite of the}, + annote = {From Duplicate 1 (How the Brain Works: The Next Great Scientific Revolution - Hestenes, David) + + David Hestenes, a physicist by training, provides an overview of the adaptive resonance theory of Dr. Stephen Grossberg and its significance to the neuroscience as a whole. He provides evidence for his claim that ART exemplifies a revolution in brain science by giving a historical perspective on the field and illustrating the key points of ART, showing their most significant ramifications. Hestenes provides this paper to make Grossberg's work more accessible, necessary because of the lateral thinking required to appreciate the magnitude of Grossberg's work. + + From Duplicate 2 (How the Brain Works: The Next Great Scientific Revolution - Hestenes, David) + + From Duplicate 3 (How the Brain Works: The Next Great Scientific Revolution - Hestenes, David) + + David Hestenes, a physicist by training, provides an overview of the adaptive resonance theory of Dr. Stephen Grossberg and its significance to the neuroscience as a whole. He provides evidence for his claim that ART exemplifies a revolution in brain science by giving a historical perspective on the field and illustrating the key points of ART, showing their most significant ramifications. Hestenes provides this paper to make Grossberg's work more accessible, necessary because of the lateral thinking required to appreciate the magnitude of Grossberg's work.}, + author = {Hestenes, David}, + booktitle = {Maximum-Entropy and Bayesian Spectral Analysis and Estimation Problems}, + doi = {10.1007/978-94-009-3961-5_11}, + file = {:C\:/Users/Sasha/AppData/Local/Mendeley Ltd./Mendeley Desktop/Downloaded/Unknown - Unknown - HestenesDavidHowTheBrainWorks001.pdf.pdf:pdf;:C\:/Users/Sasha/AppData/Local/Mendeley Ltd./Mendeley Desktop/Downloaded/Hestenes - 1987 - How the Brain Works The Next Great Scientific Revolution.pdf:pdf}, + mendeley-groups = {ART}, + pages = {173--205}, + publisher = {Springer Netherlands}, + title = {{How the Brain Works: The Next Great Scientific Revolution}}, + year = {1987} +} + +@article{Grossberg2017, +abstract = {The hard problem of consciousness is the problem of explaining how we experience qualia or phenomenal experiences, such as seeing, hearing, and feeling, and knowing what they are. To solve this problem, a theory of consciousness needs to link brain to mind by modeling how emergent properties of several brain mechanisms interacting together embody detailed properties of individual conscious psychological experiences. This article summarizes evidence that Adaptive Resonance Theory, or ART, accomplishes this goal. ART is a cognitive and neural theory of how advanced brains autonomously learn to attend, recognize, and predict objects and events in a changing world. ART has predicted that “all conscious states are resonant states” as part of its specification of mechanistic links between processes of consciousness, learning, expectation, attention, resonance, and synchrony. It hereby provides functional and mechanistic explanations of data ranging from individual spikes and their synchronization to the dynamics of conscious perceptual, cognitive, and cognitive–emotional experiences. ART has reached sufficient maturity to begin classifying the brain resonances that support conscious experiences of seeing, hearing, feeling, and knowing. Psychological and neurobiological data in both normal individuals and clinical patients are clarified by this classification. This analysis also explains why not all resonances become conscious, and why not all brain dynamics are resonant. The global organization of the brain into computationally complementary cortical processing streams (complementary computing), and the organization of the cerebral cortex into characteristic layers of cells (laminar computing), figure prominently in these explanations of conscious and unconscious processes. Alternative models of consciousness are also discussed.}, +author = {Grossberg, Stephen}, +doi = {10.1016/j.neunet.2016.11.003}, +file = {:G\:/My Drive/Research/Literature/ART/Papers/BUpapersSteveGrossbergGailCarpenterEtc/Consciousness2017SteveGrossbergNN.pdf:pdf}, +issn = {18792782}, +journal = {Neural Networks}, +keywords = {Adaptive resonance,Attention,Audition,Consciousness,Emotion,Vision}, +pages = {38--95}, +pmid = {28088645}, +publisher = {Elsevier Ltd}, +title = {{Towards solving the hard problem of consciousness: The varieties of brain resonances and the conscious experiences that they support}}, +url = {http://dx.doi.org/10.1016/j.neunet.2016.11.003}, +volume = {87}, +year = {2017} +} + +@article{Cohen1983a, +abstract = {The process whereby input patterns are transformed and stored by competitive cellular networks is considered. This process arises in such diverse subjects as the short-term storage of visual or language patterns by neural networks, pattern formation due to the firing of morphogenetic gradients in developmental biology, control of choice behavior during macromolecular evolution, and the design of stable context-sensitive parallel processors. In addition to systems capable of approaching one of perhaps infinitely many equilibrium points in response to arbitrary input patterns and initial data, one finds in these subjects a wide variety of other behaviors, notably traveling waves, standing waves, resonance, and chaos. The question of what general dynamical constraints cause global approach to equilibria rather than large amplitude waves is therefore of considerable interest. In another terminology, this is the question of whether global pattern formation occurs. A related question is whether the global pattern formation property persists when system parameters slowly change in an unpredictable fashion due to self-organization (development, learning). This is the question of absolute stability of global pattern formation. It is shown that many model systems which exhibit the absolute stability property can be written in the form i = 1, 2, {\textperiodcentered}{\textperiodcentered}{\textperiodcentered}, n, where the matrix C = ||cik|| is symmetric and the system as a whole is competitive. Under these circumstances, this system defines a global Liapunov function. The absolute stability of systems with infinite but totally disconnected sets of equilibrium points can then be studied using the LaSalle invariance principle, the theory of several complex variables, and Sard's theorem. The symmetry of matrix C is important since competitive systems of the form (1) exist wherein C is arbitrarily close to a symmetric matrix but almost all trajectories persistently oscillate, as in the voting paradox. Slowing down the competitive feedback without violating symmetry, as in the systems also enables sustained oscillations to occur. Our results thus show that the use of fast symmetric competitive feedback is a robust design constraint for guaranteeing absolute stability of global pattern formation. {\textcopyright} 1983 IEEE}, +author = {Cohen, Michael A. and Grossberg, Stephen}, +doi = {10.1109/TSMC.1983.6313075}, +file = {:C\:/Users/Sasha/AppData/Local/Mendeley Ltd./Mendeley Desktop/Downloaded/Cohen, Grossberg - 1983 - Absolute Stability of Global Pattern Formation and Parallel Memory Storage by Competitive Neural Networks.pdf:pdf}, +issn = {21682909}, +journal = {IEEE Transactions on Systems, Man and Cybernetics}, +number = {5}, +pages = {815--826}, +title = {{Absolute Stability of Global Pattern Formation and Parallel Memory Storage by Competitive Neural Networks}}, +volume = {SMC-13}, +year = {1983} +} + +@article{Grossberg2009, +abstract = {How do humans rapidly recognize a scene? How can neural models capture this biological competence to achieve state-of-the-art scene classification? The ARTSCENE neural system classifies natural scene photographs by using multiple spatial scales to efficiently accumulate evidence for gist and texture. ARTSCENE embodies a coarse-to-fine Texture Size Ranking Principle whereby spatial attention processes multiple scales of scenic information, from global gist to local textures, to learn and recognize scenic properties. The model can incrementally learn and rapidly predict scene identity by gist information alone, and then accumulate learned evidence from scenic textures to refine this hypothesis. The model shows how texture-fitting allocations of spatial attention, called attentional shrouds, can facilitate scene recognition, particularly when they include a border of adjacent textures. Using grid gist plus three shroud textures on a benchmark photograph dataset, ARTSCENE discriminates 4 landscape scene categories (coast, forest, mountain, and countryside) with up to 91.85% correct on a test set, outperforms alternative models in the literature which use biologically implausible computations, and outperforms component systems that use either gist or texture information alone. {\textcopyright} ARVO.}, +annote = {This paper outlines several different toolchains that together together comprise the ARTSCENE algorithm. The paper at its core is an investigation into the construction of a system that recognizes whole-scene global descriptors from local textures. It does this through a series of image filters that mimik the processing occuring in the mammalian LGN and learning/recognition processing via the Default ARTMAP 2 algorithm.}, +author = {Grossberg, Stephen and Huang, Tsung Ren}, +doi = {10.1167/9.4.6}, +file = {:G\:/My Drive/Research/Literature/ART/jov-9-4-6.pdf:pdf}, +issn = {15347362}, +journal = {Journal of Vision}, +keywords = {ARTMAP,Attentional shroud,Coarse-to-fine processing,Gist,Multiple-scale processing,Scene classification,Spatial attention,Texture}, +mendeley-groups = {ART}, +number = {4}, +pages = {1--19}, +pmid = {19757915}, +title = {{ARTSCENE: A neural system for natural scene classification}}, +volume = {9}, +year = {2009} +} + +@Book{grossberg2021conscious, + author = {Grossberg, Stephen}, + title = {Conscious Mind, Resonant Brain: How Each Brain Makes a Mind}, + publisher = {OUP Premium Oxford University Press}, + year = {2021}, + address = {Oxford, England}, + isbn = {978-0190070557} + } + + @article{Tan2019, +abstract = {Learning and memory are two intertwined cognitive functions of the human brain. This paper shows how a family of biologically-inspired self-organizing neural networks, known as fusion Adaptive Resonance Theory (fusion ART), may provide a viable approach to realizing the learning and memory functions. Fusion ART extends the single-channel Adaptive Resonance Theory (ART) model to learn multimodal pattern associative mappings. As a natural extension of ART, various forms of fusion ART have been developed for a myriad of learning paradigms, ranging from unsupervised learning to supervised learning, semi-supervised learning, multimodal learning, reinforcement learning, and sequence learning. In addition, fusion ART models may be used for representing various types of memories, notably episodic memory, semantic memory and procedural memory. In accordance with the notion of embodied intelligence, such neural models thus provide a computational account of how an autonomous agent may learn and adapt in a real-world environment. The efficacy of fusion ART in learning and memory shall be discussed through various examples and illustrative case studies.}, +annote = {From Duplicate 1 (Self-organizing neural networks for universal learning and multimodal memory encoding - Tan, Ah-Hwee Hwee; Subagdja, Budhitama; Wang, Di; Meng, Lei) + +This paper is effectively an appraisal of the Fusion ART algorithm, effectively outlining the details of the algorithm, its capabilities, and its limitations. The paper outlines some practical applications of this algorithm, especially as backbone for other algorithms (i.e., FALCON, iFALCON, EM-ART, OMC-ART, etc.). +}, +author = {Tan, Ah-Hwee Hwee and Subagdja, Budhitama and Wang, Di and Meng, Lei}, +doi = {10.1016/j.neunet.2019.08.020}, +file = {:C\:/Users/Sasha/AppData/Local/Mendeley Ltd./Mendeley Desktop/Downloaded/Tan et al. - 2019 - Self-organizing neural networks for universal learning and multimodal memory encoding.pdf:pdf;:C\:/Users/Sasha/AppData/Local/Mendeley Ltd./Mendeley Desktop/Downloaded/Tan et al. - 2019 - Self-organizing neural networks for universal learning and multimodal memory encoding(2).pdf:pdf}, +issn = {08936080}, +journal = {Neural Networks}, +keywords = {,Adaptive resonance theory,Memory encoding,Universal learning,adaptive resonance theory}, +mendeley-groups = {ART,NN Special Issue}, +number = {xxxx}, +pages = {58--73}, +publisher = {Elsevier Ltd}, +title = {{Self-organizing neural networks for universal learning and multimodal memory encoding}}, +url = {https://doi.org/10.1016/j.neunet.2019.08.020}, +volume = {120}, +year = {2019} +} diff --git a/paper/paper.md b/paper/paper.md index a446b02a..b5b43ce2 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -23,13 +23,53 @@ bibliography: paper.bib AdaptiveResonance.jl is a Julia package for machine learning with Adaptive Resonance Theory (ART) algorithms, written in the numerical computing language Julia. ART is a neurocognitive theory of how competitive cellular networks can learn distributed patterns without supervision through recurrent field connections, eliciting the mechanisms of perception, expectation, and recognition [@Grossberg2013; @Grossberg1980]. -# Statement of need +# Statement of Need There exist many variations of algorithms built upon ART [@DaSilva2019]. Each variation is related by utilizing recurrent connections of fields, driven by learning through match and mismatch of distributed patterns, and though they all differ in the details of their implementations, their algorithmic and programmatic requirements are often very similar. Despite the relevance and successes of this class of algorithms in the literature, there does not exist to date a unified repository of their implementations in Julia. The purpose of this package is to create a unified framework and repository of ART algorithms in Julia. +## Target Audience + +This package is principally intended as a resource for researchers in machine learning and adaptive resonance theory for testing and developing new ART algorithms. +However, implementing these algorithms in the Julia language brings all of the benefits of the Julia itself, such as the speed of being implemented in a low-level language such as C while having the transparency of a high-level language such as MATLAB. +Being implemented in Julia allows the package to be understood and expanded upon by research scientists while still being able to be used in resource-demanding production environments. + +# Adaptive Resonance Theory + +ART is originally a theory of how competitive fields of neurons interact to form stable representations without supervision, and ART algorithms draw from this theory as biological inspiration for their design. +It is not strictly necessary to have an understanding of the theory to understand the use of the algorithms, but they share a common nomenclature that makes knowledge of the former useful for the latter. + +## Theory + +Adaptive resonance theory is a collection of neurological study from the neuron level to the network level [@ARTHestenes1987]. +ART begins with a set of neural field differential equations, and the theory tackles problems from why sigmoidal activations are used and the conditions of stability for competitive neural networks [@Cohen1983a] to how the mammalian visual system works [@Grossberg2009] and the hard problem of consciousness linking resonant states to conscious experiences [@Grossberg2017]. +Stephen Grossberg and Gail Carpenter have published many resources for learning the theory and its history in detail [@grossberg2021conscious]. + +## Algorithms + +ART algorithms are generally characterized in behavior by the following: + +1. They are inherently *unsupervised* learning algorithms at their core, but they have been adapted to supervised and reinforcement learning paradigms with frameworks such as ARTMAP [@Carpenter1991; @Carpenter1992] and FALCON [@Tan2019], respectively. +2. They are *incremental* learning algorithms, adjusting their weights or creating new ones at every sample presentation. +3. They are *neurogenesis* neural networks, representing their learning by the modification of existing prototype weights or instantiating new ones entirely. +4. They belong to the class of *competitive* neural networks, which compute their outputs with more complex dynamics than feedforward activation. + +Because of the breadth of the original theory and variety of possible applications, ART-based algorithms are diverse in their implementation details. +Nevertheless, they are generally structured as follows: + +1. ART models typically have two layers/fields denoted F1 and F2. +2. The F1 field is the feature representation field. +Most often, it is simply the input feature sample itself (after some necessary preprocessing). +3. The F2 field is the category representation field. +With some exceptions, each node in the F2 field represents its own category. +This is most easily understood as a weight vector representing a prototype for a class or centroid of a cluster. +4. An activation function is used to find the order of categories "most activated" for a given sample in F1. +5. In order of highest activation, a match function is used to compute the agreement between the sample and the categories. +6. If the match function for a category evaluates to a value above a threshold known as the vigilance parameter ($$\rho$$), the weights of that category may be updated according to a learning rule. +7. If there is complete mismatch across all categories, then a new categories is created according to some instantiation rule. + # Acknowledgements This package is developed and maintained with sponsorship by the Applied Computational Intelligence Laboratory (ACIL) of the Missouri University of Science and Technology. diff --git a/src/ART/ART.jl b/src/ART/ART.jl index 89668af6..6d0b37f6 100644 --- a/src/ART/ART.jl +++ b/src/ART/ART.jl @@ -5,5 +5,7 @@ Description: Includes all of the unsupervised ART modules definitions. """ -include("DDVFA.jl") # DDVFA and GNFA +include("common.jl") # train!, classify +include("FuzzyART.jl") # FuzzyART +include("DDVFA.jl") # DDVFA include("DVFA.jl") # DVFA diff --git a/src/ART/DDVFA.jl b/src/ART/DDVFA.jl index f8b8f43a..d46d0a55 100644 --- a/src/ART/DDVFA.jl +++ b/src/ART/DDVFA.jl @@ -5,386 +5,9 @@ Description: Includes all of the structures and logic for running a Distributed Dual-Vigilance Fuzzy ART (DDVFA) module. """ -""" - opts_GNFA() - -Gamma-Normalized Fuzzy ART options struct. - -# Examples -```julia-repl -julia> opts_GNFA() -Initialized GNFA -``` -""" -@with_kw mutable struct opts_GNFA <: ARTOpts @deftype RealFP - # Vigilance parameter: [0, 1] - rho = 0.6; @assert rho >= 0.0 && rho <= 1.0 - # Choice parameter: alpha > 0 - alpha = 1e-3; @assert alpha > 0.0 - # Learning parameter: (0, 1] - beta = 1.0; @assert beta > 0.0 && beta <= 1.0 - # "Pseudo" kernel width: gamma >= 1 - gamma = 3.0; @assert gamma >= 1.0 - # gamma = 784; @assert gamma >= 1 - # "Reference" gamma for normalization: 0 <= gamma_ref < gamma - gamma_ref = 1.0; @assert 0.0 <= gamma_ref && gamma_ref < gamma - # Similarity method (activation and match): - # 'single', 'average', 'complete', 'median', 'weighted', or 'centroid' - method::String = "single" - # Display flag - display::Bool = true - # Maximum number of epochs during training - max_epochs::Integer = 1 -end # opts_GNFA - -""" - GNFA <: ART - -Gamma-Normalized Fuzzy ART learner struct - -# Examples -```julia-repl -julia> GNFA() -GNFA - opts: opts_GNFA - ... -``` -""" -mutable struct GNFA <: ART - # Assign numerical parameters from options - opts::opts_GNFA - config::DataConfig - - # Working variables - threshold::RealFP - labels::IntegerVector - T::RealVector - M::RealVector - - # "Private" working variables - W::RealMatrix - W_old::RealMatrix - n_instance::IntegerVector - n_categories::Integer - epoch::Integer -end # GNFA <: ART - -""" - GNFA() - -Implements a Gamma-Normalized Fuzzy ART learner. - -# Examples -```julia-repl -julia> GNFA() -GNFA - opts: opts_GNFA - ... -``` -""" -function GNFA() - opts = opts_GNFA() - GNFA(opts) -end # GNFA() - -""" - GNFA(;kwargs...) - -Implements a Gamma-Normalized Fuzzy ART learner with keyword arguments. - -# Examples -```julia-repl -julia> GNFA(rho=0.7) -GNFA - opts: opts_GNFA - ... -``` -""" -function GNFA(;kwargs...) - opts = opts_GNFA(;kwargs...) - GNFA(opts) -end # GNFA(;kwargs...) - -""" - GNFA(opts::opts_GNFA) - -Implements a Gamma-Normalized Fuzzy ART learner with specified options. - -# Examples -```julia-repl -julia> GNFA(opts) -GNFA - opts: opts_GNFA - ... -``` -""" -function GNFA(opts::opts_GNFA) - GNFA(opts, # opts - DataConfig(), # config - 0.0, # threshold - Array{Integer}(undef,0), # labels - Array{RealFP}(undef, 0), # T - Array{RealFP}(undef, 0), # M - Array{RealFP}(undef, 0, 0), # W - Array{RealFP}(undef, 0, 0), # W_old - Array{Integer}(undef, 0), # n_instance - 0, # n_categories - 0 # epoch - ) -end # GNFA(opts::opts_GNFA) - -""" - GNFA(opts::opts_GNFA, sample::RealArray) - -Create and initialize a GNFA with a single sample in one step. -""" -function GNFA(opts::opts_GNFA, sample::RealArray) - art = GNFA(opts) - initialize!(art, sample) - return art -end # GNFA(opts::opts_GNFA, sample::RealArray) - -""" - initialize!(art::GNFA, x::Array) - -Initializes a GNFA learner with an intial sample 'x'. - -# Examples -```julia-repl -julia> my_GNFA = GNFA() -GNFA - opts: opts_GNFA - ... -julia> initialize!(my_GNFA, [1 2 3 4]) -``` -""" -# function initialize!(art::GNFA, x::RealArray ; y::Integer=0) -function initialize!(art::GNFA, x::Vector{T} ; y::Integer=0) where {T<:RealFP} - # Set up the data config - if art.config.setup - @warn "Data configuration already set up, overwriting config" - else - art.config.setup = true - end - - # IMPORTANT: Assuming that x is a sample, so each entry is a feature - dim = length(x) - art.config.dim_comp = dim - art.config.dim = Integer(dim/2) # Assumes input is already complement coded - - # Initialize the instance and categories counters - art.n_instance = [1] - art.n_categories = 1 - - # Set the threshold - art.threshold = art.opts.rho * (art.config.dim^art.opts.gamma_ref) - # Fast commit the weight - art.W = Array{T}(undef, art.config.dim_comp, 1) - # Assign the contents, valid this way for 1-D or 2-D arrays - art.W[:, 1] = x - label = y == 0 ? y : 1 - push!(art.labels, label) -end # initialize!(art::GNFA, x::RealArray ; y::Integer=0) - -""" - train!(art::GNFA, x::RealArray ; y::IntegerVector=[]) - -Trains a GNFA learner with dataset 'x' and optional labels 'y' - -# Examples -```julia-repl -julia> my_GNFA = GNFA() -GNFA - opts: opts_GNFA - ... -julia> x = load_data() -julia> train!(my_GNFA, x) -``` -""" -function train!(art::GNFA, x::RealArray ; y::IntegerVector = Vector{Integer}()) - # Flag for if training in supervised mode - supervised = !isempty(y) - # Initialization if weights are empty; fast commit the first sample - if isempty(art.W) - label = supervised ? y[1] : 1 - push!(art.labels, label) - initialize!(art, x[:, 1]) - skip_first = true - else - skip_first = false - end - - art.W_old = deepcopy(art.W) - - # Learning - art.epoch = 0 - while true - # Increment the epoch and get the iterator - art.epoch += 1 - iter = get_iterator(art.opts, x) - # Loop over samples - for i = iter - # Update the iterator if necessary - update_iter(art, iter, i) - # Skip the first sample if we just initialized - (i == 1 && skip_first) && continue - # Grab the sample slice - sample = get_sample(x, i) - # Compute activation/match functions - activation_match!(art, sample) - # Sort activation function values in descending order - index = sortperm(art.T, rev=true) - # Initialize mismatch as true - mismatch_flag = true - # Loop over all categories - for j = 1:art.n_categories - # Best matching unit - bmu = index[j] - # Vigilance check - pass - if art.M[bmu] >= art.threshold - # Learn the sample - learn!(art, sample, bmu) - # Update sample labels - label = supervised ? y[i] : bmu - push!(art.labels, label) - # No mismatch - mismatch_flag = false - break - end - end - # If there was no resonant category, make a new one - if mismatch_flag - # Increment the number of categories - art.n_categories += 1 - # Fast commit - # art.W = [art.W x[:, i]] - art.W = hcat(art.W, sample) - # Increment number of samples associated with new category - push!(art.n_instance, 1) - # Update sample labels - label = supervised ? y[i] : art.n_categories - push!(art.labels, label) - end - end - # Make sure to start at first sample from now on - skip_first = false - # Check for the stopping condition for the whole loop - if stopping_conditions(art) - break - end - # If we didn't break, deep copy the old weights - art.W_old = deepcopy(art.W) - end -end # train!(art::GNFA, x::RealArray ; y::IntegerVector = Vector{Integer}()) - -""" - classify(art::GNFA, x::RealArray) - -Predict categories of 'x' using the GNFA model. - -Returns predicted categories 'y_hat' - -# Examples -```julia-repl -julia> my_GNFA = GNFA() -GNFA - opts: opts_GNFA - ... -julia> x, y = load_data() -julia> train!(my_GNFA, x) -julia> y_hat = classify(my_GNFA, y) -``` -""" -function classify(art::GNFA, x::RealArray) - # Get the number of samples to classify - n_samples = get_n_samples(x) - - # Initialize the output vector and iterate across all data - y_hat = zeros(Integer, n_samples) - iter = get_iterator(art.opts, x) - for ix in iter - # Update the iterator if necessary - update_iter(art, iter, ix) - # Compute activation and match functions - activation_match!(art, x[:, ix]) - # Sort activation function values in descending order - index = sortperm(art.T, rev=true) - mismatch_flag = true - for jx in 1:art.n_categories - bmu = index[jx] - # Vigilance check - pass - if art.M[bmu] >= art.threshold - # Current winner - y_hat[ix] = art.labels[bmu] - mismatch_flag = false - break - end - end - if mismatch_flag - # Create new weight vector - @debug "Mismatch" - y_hat[ix] = -1 - end - end - return y_hat -end # classify(art::GNFA, x::RealArray) - -""" - activation_match!(art::GNFA, x::RealArray) - -Computes the activation and match functions of the art module against sample x. - -# Examples -```julia-repl -julia> my_GNFA = GNFA() -GNFA - opts: opts_GNFA - ... -julia> x, y = load_data() -julia> train!(my_GNFA, x) -julia> x_sample = x[:, 1] -julia> activation_match!(my_GNFA, x_sample) -``` -""" -function activation_match!(art::GNFA, x::RealArray) - art.T = zeros(art.n_categories) - art.M = zeros(art.n_categories) - for i = 1:art.n_categories - W_norm = norm(art.W[:, i], 1) - art.T[i] = (norm(element_min(x, art.W[:, i]), 1)/(art.opts.alpha + W_norm))^art.opts.gamma - art.M[i] = (W_norm^art.opts.gamma_ref)*art.T[i] - end -end # activation_match!(art::GNFA, x::RealArray) - -""" - learn(art::GNFA, x::RealVector, W::RealVector) - -Return the modified weight of the art module conditioned by sample x. -""" -function learn(art::GNFA, x::RealVector, W::RealVector) - # Update W - return art.opts.beta .* element_min(x, W) .+ W .* (1 - art.opts.beta) -end # learn(art::GNFA, x::RealVector, W::RealVector) - -""" - learn!(art::GNFA, x::RealVector, index::Integer) - -In place learning function with instance counting. -""" -function learn!(art::GNFA, x::RealVector, index::Integer) - # Update W - art.W[:, index] = learn(art, x, art.W[:, index]) - art.n_instance[index] += 1 -end # learn!(art::GNFA, x::RealVector, index::Integer) - -""" - stopping_conditions(art::GNFA) - -Stopping conditions for a GNFA module. -""" -function stopping_conditions(art::GNFA) - return isequal(art.W, art.W_old) || art.epoch >= art.opts.max_epochs -end # stopping_conditions(art::GNFA) +# --------------------------------------------------------------------------- # +# OPTIONS +# --------------------------------------------------------------------------- # """ opts_DDVFA() @@ -396,10 +19,9 @@ Distributed Dual Vigilance Fuzzy ART options struct. julia> my_opts = opts_DDVFA() ``` """ -@with_kw mutable struct opts_DDVFA <: ARTOpts @deftype RealFP +@with_kw mutable struct opts_DDVFA <: ARTOpts @deftype Float # Lower-bound vigilance parameter: [0, 1] - rho_lb = 0.80; @assert rho_lb >= 0.0 && rho_lb <= 1.0 - rho = rho_lb + rho_lb = 0.7; @assert rho_lb >= 0.0 && rho_lb <= 1.0 # Upper bound vigilance parameter: [0, 1] rho_ub = 0.85; @assert rho_ub >= 0.0 && rho_ub <= 1.0 # Choice parameter: alpha > 0 @@ -416,9 +38,15 @@ julia> my_opts = opts_DDVFA() # Display flag display::Bool = true # Maximum number of epochs during training - max_epoch::Integer = 1 + max_epoch::Int = 1 + # Normalize the threshold by the feature dimension + gamma_normalization::Bool = true end # opts_DDVFA +# --------------------------------------------------------------------------- # +# STRUCTS +# --------------------------------------------------------------------------- # + """ DDVFA <: ART @@ -429,28 +57,30 @@ Distributed Dual Vigilance Fuzzy ARTMAP module struct. julia> DDVFA() DDVFA opts: opts_DDVFA - subopts::opts_GNFA + subopts::opts_FuzzyART ... ``` """ mutable struct DDVFA <: ART # Get parameters opts::opts_DDVFA - subopts::opts_GNFA + subopts::opts_FuzzyART config::DataConfig # Working variables - threshold::RealFP - F2::Vector{GNFA} + threshold::Float + F2::Vector{FuzzyART} labels::IntegerVector - W::RealMatrix # All F2 nodes' weight vectors - W_old::RealMatrix # Old F2 node weight vectors (for stopping criterion) - n_categories::Integer - epoch::Integer - T::RealFP - M::RealFP + n_categories::Int + epoch::Int + T::Float + M::Float end # DDVFA <: ART +# --------------------------------------------------------------------------- # +# CONSTRUCTORS +# --------------------------------------------------------------------------- # + """ DDVFA() @@ -461,7 +91,7 @@ Implements a DDVFA learner with default options. julia> DDVFA() DDVFA opts: opts_DDVFA - subopts: opts_GNFA + subopts: opts_FuzzyART ... ``` """ @@ -477,10 +107,10 @@ Implements a DDVFA learner with keyword arguments. # Examples ```julia-repl -julia> DDVFA(rho=0.7) +julia> DDVFA(rho_lb=0.4, rho_ub = 0.75) DDVFA opts: opts_DDVFA - subopts: opts_GNFA + subopts: opts_FuzzyART ... ``` """ @@ -500,23 +130,27 @@ julia> my_opts = opts_DDVFA() julia> DDVFA(my_opts) DDVFA opts: opts_DDVFA - subopts: opts_GNFA + subopts: opts_FuzzyART ... ``` """ function DDVFA(opts::opts_DDVFA) - subopts = opts_GNFA( + # Set the options used for all F2 FuzzyART modules + subopts = opts_FuzzyART( rho=opts.rho_ub, + gamma=opts.gamma, + gamma_ref=opts.gamma_ref, + gamma_normalization=opts.gamma_normalization, display=false ) + + # Construct the DDVFA module DDVFA(opts, subopts, DataConfig(), 0.0, - Array{GNFA}(undef, 0), - Array{Integer}(undef, 0), - Array{RealFP}(undef, 0, 0), - Array{RealFP}(undef, 0, 0), + Array{FuzzyART}(undef, 0), + Array{Int}(undef, 0), 0, 0, 0.0, @@ -524,161 +158,112 @@ function DDVFA(opts::opts_DDVFA) ) end # DDVFA(opts::opts_DDVFA) -""" - train!(art::DDVFA, x::RealArray ; y::IntegerVector=[], preprocessed::Bool=false) +# --------------------------------------------------------------------------- # +# ALGORITHMIC METHODS +# --------------------------------------------------------------------------- # -Train the DDVFA model on the data. """ -function train!(art::DDVFA, x::RealArray ; y::IntegerVector = Vector{Integer}(), preprocessed::Bool=false) - # Show a message if display is on - art.opts.display && @info "Training DDVFA" + set_threshold!(art::DDVFA) - # Simple supervised flag - supervised = !isempty(y) +Sets the vigilance threshold of the DDVFA module as a function of several flags and hyperparameters. +""" +function set_threshold!(art::DDVFA) + # Gamma match normalization + if art.opts.gamma_normalization + # Set the learning threshold as a function of the data dimension + art.threshold = art.opts.rho_lb*(art.config.dim^art.opts.gamma_ref) + else + # Set the learning threshold as simply the vigilance parameter + art.threshold = art.opts.rho_lb + end +end # set_threshold!(art::DDVFA) - # Data information and setup - n_samples = get_n_samples(x) +""" + train!(art::DDVFA, x::RealMatrix ; y::IntegerVector=Vector{Int}(), preprocessed::Bool=false) - # Set up the data config if training for the first time - !art.config.setup && data_setup!(art.config, x) +Train the DDVFA model on the data. +""" - # If the data is not preprocessed, then complement code it - if !preprocessed - x = complement_code(x, config=art.config) - end +""" + train!(art::DDVFA, x::RealVector ; y::Integer=0, preprocessed::Bool=false) +""" +function train!(art::DDVFA, x::RealVector ; y::Integer=0, preprocessed::Bool=false) + # Flag for if training in supervised mode + supervised = !iszero(y) - # art.labels = zeros(n_samples) - if n_samples == 1 - y_hat = zero(Integer) - else - y_hat = zeros(Integer, n_samples) - end + # Run the sequential initialization procedure + sample = init_train!(x, art, preprocessed) # Initialization if isempty(art.F2) + # Set the threshold + set_threshold!(art) # Set the first label as either 1 or the first provided label - local_label = supervised ? y[1] : 1 - # Add the local label to the output vector - if n_samples == 1 - y_hat = local_label - else - y_hat[1] = local_label - end + y_hat = supervised ? y : 1 # Create a new category - create_category(art, get_sample(x, 1), local_label) - # Skip the first training entry - skip_first = true - else - skip_first = false + create_category(art, sample, y_hat) + return y_hat end - # Initialize old weight vector for checking stopping conditions between epochs - art.W_old = deepcopy(art.W) - - # Set the learning threshold as a function of the data dimension - art.threshold = art.opts.rho*(art.config.dim^art.opts.gamma_ref) - - # Learn until the stopping conditions - art.epoch = 0 - while true - # Increment the epoch and get the iterator - art.epoch += 1 - iter = get_iterator(art.opts, x) - for i = iter - # Update the iterator if necessary - update_iter(art, iter, i) - # Skip the first sample if we just initialized - (i == 1 && skip_first) && continue - # Grab the sample slice - sample = get_sample(x, i) - - # Default to mismatch - mismatch_flag = true - # If label is new, break to make new category - if supervised && !(y[i] in art.labels) - if n_samples == 1 - y_hat = y[i] - else - y_hat[i] = y[i] - end - create_category(art, sample, y[i]) - continue - end - # Otherwise, check for match - # Compute the activation for all categories - T = zeros(art.n_categories) - for jx = 1:art.n_categories - activation_match!(art.F2[jx], sample) - T[jx] = similarity(art.opts.method, art.F2[jx], "T", sample, art.opts.gamma_ref) - end - # Compute the match for each category in the order of greatest activation - index = sortperm(T, rev=true) - for jx = 1:art.n_categories - bmu = index[jx] - M = similarity(art.opts.method, art.F2[bmu], "M", sample, art.opts.gamma_ref) - # If we got a match, then learn (update the category) - if M >= art.threshold - # Update the stored match and activation values - art.M = M - art.T = T[bmu] - # If supervised and the label differs, trigger mismatch - if supervised && art.labels[bmu] != y[i] - break - end - # Update the weights with the sample - train!(art.F2[bmu], sample) - # Save the output label for the sample - label = art.labels[bmu] - if n_samples == 1 - y_hat = label - else - y_hat[i] = label - end - mismatch_flag = false - break - end - end - if mismatch_flag - # Update the stored match and activation values - bmu = index[1] - art.M = similarity(art.opts.method, art.F2[bmu], "M", sample, art.opts.gamma_ref) - art.T = T[bmu] - # Get the correct label - label = supervised ? y[i] : art.n_categories + 1 - if n_samples == 1 - y_hat = label - else - y_hat[i] = label - end - create_category(art, sample, label) + # Default to mismatch + mismatch_flag = true + + # Compute the activation for all categories + T = zeros(art.n_categories) + for jx = 1:art.n_categories + activation_match!(art.F2[jx], sample) + T[jx] = similarity(art.opts.method, art.F2[jx], "T", sample, art.opts.gamma_ref) + end + + # Compute the match for each category in the order of greatest activation + index = sortperm(T, rev=true) + for jx = 1:art.n_categories + bmu = index[jx] + M = similarity(art.opts.method, art.F2[bmu], "M", sample, art.opts.gamma_ref) + # If we got a match, then learn (update the category) + if M >= art.threshold + # Update the stored match and activation values + art.M = M + art.T = T[bmu] + # If supervised and the label differs, trigger mismatch + if supervised && (art.labels[bmu] != y) + break end - end - # Make sure to start at first sample from now on - skip_first = false - # Deep copy all of the weights for stopping condition check - art.W = art.F2[1].W - for kx = 2:art.n_categories - art.W = [art.W art.F2[kx].W] - end - if stopping_conditions(art) + # Update the weights with the sample + train!(art.F2[bmu], sample, preprocessed=true) + # Save the output label for the sample + y_hat = art.labels[bmu] + # No mismatch + mismatch_flag = false break end - art.W_old = deepcopy(art.W) end + + # If we triggered a mismatch + if mismatch_flag + # Update the stored match and activation values + bmu = index[1] + art.M = similarity(art.opts.method, art.F2[bmu], "M", sample, art.opts.gamma_ref) + art.T = T[bmu] + # Get the correct label + y_hat = supervised ? y : art.n_categories + 1 + create_category(art, sample, y_hat) + end + return y_hat -end # train!(art::DDVFA, x::RealArray ; y::IntegerVector = Vector{Integer}(), preprocessed::Bool=false) +end # train!(art::DDVFA, x::RealVector ; y::Integer=0, preprocessed::Bool=false) """ create_category(art::DDVFA, sample::RealVector, label::Integer) -Create a new category by appending and initializing a new GNFA node to F2. +Create a new category by appending and initializing a new FuzzyART node to F2. """ function create_category(art::DDVFA, sample::RealVector, label::Integer) # Global Fuzzy ART art.n_categories += 1 push!(art.labels, label) - # Local Fuzzy ART - push!(art.F2, GNFA(art.subopts, sample)) + # Local Gamma-Normalized Fuzzy ART + push!(art.F2, FuzzyART(art.subopts, sample, preprocessed=true)) end # function create_category(art::DDVFA, sample::RealVector, label::Integer) """ @@ -690,16 +275,16 @@ Returns true if there is no change in weights during the epoch or the maxmimum e """ function stopping_conditions(art::DDVFA) # Compute the stopping condition, return a bool - return art.W == art.W_old || art.epoch >= art.opts.max_epoch + return art.epoch >= art.opts.max_epoch end # stopping_conditions(DDVFA) """ - similarity(method::String, F2::GNFA, field_name::String, sample::RealVector, gamma_ref::RealFP) + similarity(method::String, F2::FuzzyART, field_name::String, sample::RealVector, gamma_ref::RealFP) Compute the similarity metric depending on method with explicit comparisons for the field name. """ -function similarity(method::String, F2::GNFA, field_name::String, sample::RealVector, gamma_ref::RealFP) +function similarity(method::String, F2::FuzzyART, field_name::String, sample::RealVector, gamma_ref::RealFP) @debug "Computing similarity" if field_name != "T" && field_name != "M" @@ -755,10 +340,10 @@ function similarity(method::String, F2::GNFA, field_name::String, sample::RealVe end return value -end # similarity(method::String, F2::GNFA, field_name::String, sample::RealVector, gamma_ref::RealFP) +end # similarity(method::String, F2::FuzzyART, field_name::String, sample::RealVector, gamma_ref::RealFP) """ - classify(art::DDVFA, x::RealArray ; preprocessed::Bool=false, get_bmu::Bool=false) + classify(art::DDVFA, x::RealMatrix ; preprocessed::Bool=false, get_bmu::Bool=false) Predict categories of 'x' using the DDVFA model. @@ -775,89 +360,84 @@ julia> train!(my_DDVFA, x) julia> y_hat = classify(my_DDVFA, y) ``` """ -function classify(art::DDVFA, x::RealArray ; preprocessed::Bool=false, get_bmu::Bool=false) - # Show a message if display is on - art.opts.display && @info "Testing DDVFA" - - # Data information and setup - n_samples = get_n_samples(x) - - # Verify that the data is setup before classifying - !art.config.setup && @error "Attempting to classify data before setup" +function classify(art::DDVFA, x::RealVector ; preprocessed::Bool=false, get_bmu::Bool=false) + # Preprocess the data + sample = init_classify!(x, art, preprocessed) - # If the data is not preprocessed, then complement code it - if !preprocessed - x = complement_code(x, config=art.config) + # Calculate all global activations + T = zeros(art.n_categories) + for jx = 1:art.n_categories + activation_match!(art.F2[jx], sample) + T[jx] = similarity(art.opts.method, art.F2[jx], "T", sample, art.opts.gamma_ref) end - # Initialize the output vector - if n_samples == 1 - y_hat = zero(Integer) - else - y_hat = zeros(Integer, n_samples) - end - - # Get the iterator based on the module options and data shape - iter = get_iterator(art.opts, x) - for ix = iter - # Update the iterator if necessary - update_iter(art, iter, ix) + # Sort by highest activation + index = sortperm(T, rev=true) - # Grab the sample slice - sample = get_sample(x, ix) + # Default to mismatch + mismatch_flag = true - # Calculate all global activations - T = zeros(art.n_categories) - for jx = 1:art.n_categories - activation_match!(art.F2[jx], sample) - T[jx] = similarity(art.opts.method, art.F2[jx], "T", sample, art.opts.gamma_ref) - end - # Sort by highest activation - index = sortperm(T, rev=true) - - mismatch_flag = true - for jx = 1:art.n_categories - bmu = index[jx] - M = similarity(art.opts.method, art.F2[bmu], "M", sample, art.opts.gamma_ref) - if M >= art.threshold - # Update the stored match and activation values - art.M = M - art.T = T[bmu] - # Current winner - label = art.labels[bmu] - if n_samples == 1 - y_hat = label - else - y_hat[ix] = label - end - mismatch_flag = false - break - end - end - if mismatch_flag - @debug "Mismatch" + # Iterate over the list of activations + for jx = 1:art.n_categories + # Get the best-matching unit + bmu = index[jx] + # Get the match value of this activation + M = similarity(art.opts.method, art.F2[bmu], "M", sample, art.opts.gamma_ref) + # If the match satisfies the threshold criterion, then report that label + if M >= art.threshold # Update the stored match and activation values - bmu = index[1] - art.M = similarity(art.opts.method, art.F2[bmu], "M", sample, art.opts.gamma_ref) + art.M = M art.T = T[bmu] - # If falling back to the highest activated category, return that - if get_bmu - label = art.labels[index[1]] - if n_samples == 1 - y_hat = label - else - y_hat[ix] = label - end - # Otherwise, return a mismatch - else - if n_samples == 1 - y_hat = -1 - else - y_hat[ix] = -1 - end - end + # Current winner + y_hat = art.labels[bmu] + mismatch_flag = false + break end end + # If we did not find a resonant category + if mismatch_flag + @debug "Mismatch" + # Update the stored match and activation values of the best matching unit + bmu = index[1] + art.M = similarity(art.opts.method, art.F2[bmu], "M", sample, art.opts.gamma_ref) + art.T = T[bmu] + # Report either the best matching unit or the mismatch label -1 + y_hat = get_bmu ? art.labels[bmu] : -1 + end + return y_hat -end # classify(art::DDVFA, x::RealArray ; preprocessed::Bool=false, get_bmu::Bool=false) +end + +# --------------------------------------------------------------------------- # +# CONVENIENCE METHODS +# --------------------------------------------------------------------------- # + +""" + get_W(art::DDVFA) + +Convenience functio; return a concatenated array of all DDVFA weights. +""" +function get_W(art::DDVFA) + # Return a concatenated array of the weights + return [art.F2[kx].W for kx = 1:art.n_categories] +end # get_W(art::DDVFA) + +""" + get_n_weights_vec(art::DDVFA) + +Convenience function; return the number of weights in each category as a vector. +""" +function get_n_weights_vec(art::DDVFA) + return [art.F2[i].n_categories for i = 1:art.n_categories] +end # get_n_weights_vec(art::DDVFA) + +""" + get_n_weights(art::DDVFA) + +Convenience function; return the sum total number of weights in the DDVFA module. +""" +function get_n_weights(art::DDVFA) + # Return the number of weights across all categories + return sum(get_n_weights_vec(art)) +end # get_n_weights(art::DDVFA) diff --git a/src/ART/DVFA.jl b/src/ART/DVFA.jl index 693a5e6d..7faad8db 100644 --- a/src/ART/DVFA.jl +++ b/src/ART/DVFA.jl @@ -26,7 +26,7 @@ Dual Vigilance Fuzzy ART options struct. julia> my_opts = opts_DVFA() ``` """ -@with_kw mutable struct opts_DVFA <: ARTOpts @deftype RealFP +@with_kw mutable struct opts_DVFA <: ARTOpts @deftype Float # Lower-bound vigilance parameter: [0, 1] rho_lb = 0.55; @assert rho_lb >= 0.0 && rho_lb <= 1.0 # Upper bound vigilance parameter: [0, 1] @@ -38,7 +38,7 @@ julia> my_opts = opts_DVFA() # Display flag display::Bool = true # Maximum number of epochs during training - max_epochs::Integer = 1 + max_epochs::Int = 1 end # opts_DVFA """ @@ -60,15 +60,16 @@ mutable struct DVFA <: ART config::DataConfig # Working variables + threshold_ub::Float + threshold_lb::Float labels::IntegerVector W::RealMatrix T::RealVector M::RealVector - W_old::RealMatrix map::IntegerVector - n_categories::Integer - n_clusters::Integer - epoch::Integer + n_categories::Int + n_clusters::Int + epoch::Int end # DVFA """ @@ -125,12 +126,13 @@ function DVFA(opts::opts_DVFA) DVFA( opts, # opts DataConfig(), # config - Array{Integer}(undef, 0), # labels - Array{RealFP}(undef, 0, 0), # W - Array{RealFP}(undef, 0), # M - Array{RealFP}(undef, 0), # T - Array{RealFP}(undef, 0, 0), # W_old - Array{Integer}(undef, 0), # map + 0.0, # threshold_ub + 0.0, # threshold_lb + Array{Int}(undef, 0), # labels + Array{Float}(undef, 0, 0), # W + Array{Float}(undef, 0), # M + Array{Float}(undef, 0), # T + Array{Int}(undef, 0), # map 0, # n_categories 0, # n_clusters 0 # epoch @@ -138,7 +140,18 @@ function DVFA(opts::opts_DVFA) end # DDVFA(opts::opts_DDVFA) """ - train!(art::DVFA, x::RealArray ; y::IntegerVector = [], preprocessed::Bool=false) + set_threshold!(art::DVFA) + +Configure the threshold values of the DVFA module. +""" +function set_threshold!(art::DVFA) + # DVFA thresholds + art.threshold_ub = art.opts.rho_ub * art.config.dim + art.threshold_lb = art.opts.rho_lb * art.config.dim +end # set_threshold!(art::DVFA) + +""" + train!(art::DVFA, x::RealVector ; y::Integer=0, preprocessed::Bool=false) Train the DVFA module on x with optional custom category labels y. @@ -147,147 +160,99 @@ Train the DVFA module on x with optional custom category labels y. - `x::RealArray`: the data to train on, interpreted as a single sample if x is a vector. - `y::IntegerVector=[]`: optional custom labels to assign to the categories. If empty, ordinary incremental labels are prescribed. """ -function train!(art::DVFA, x::RealArray ; y::IntegerVector = Vector{Integer}(), preprocessed::Bool=false) - # Show a message if display is on - art.opts.display && @info "Training DVFA" - - # Simple supervised flag - supervised = !isempty(y) - - # Data information and setup - n_samples = get_n_samples(x) - - # Set up the data config if training for the first time - !art.config.setup && data_setup!(art.config, x) - - # If the data is not preprocessed, then complement code it - if !preprocessed - x = complement_code(x, config=art.config) - end +function train!(art::DVFA, x::RealVector ; y::Integer=0, preprocessed::Bool=false) + # Flag for if training in supervised mode + supervised = !iszero(y) - if n_samples == 1 - y_hat = zero(Integer) - else - y_hat = zeros(Integer, n_samples) - end + # Run the sequential initialization procedure + sample = init_train!(x, art, preprocessed) # Initialization if isempty(art.W) + # Set the threshold + set_threshold!(art) # Set the first label as either 1 or the first provided label - local_label = supervised ? y[1] : 1 - # Add the local label to the output vector - if n_samples == 1 - y_hat = local_label - else - y_hat[1] = local_label - end + y_hat = supervised ? y : 1 # Create a new category and cluster art.W = ones(art.config.dim_comp, 1) art.n_categories = 1 art.n_clusters = 1 - push!(art.labels, local_label) - # Skip the first training entry - skip_first = true - else - skip_first = false + push!(art.labels, y_hat) + return y_hat end - art.W_old = art.W - - # Learn until the stopping conditions - art.epoch = 0 - while true - # Increment the epoch and get the iterator - art.epoch += 1 - iter = get_iterator(art.opts, x) - for i = iter - # Update the iterator if necessary - update_iter(art, iter, i) - # Skip the first sample if we just initialized - (i == 1 && skip_first) && continue - - # Grab the sample slice - sample = get_sample(x, i) - - # If label is new, break to make new category - if supervised && !(y[i] in art.labels) - if n_samples == 1 - y_hat = y[i] - else - y_hat[i] = y[i] - end - # Update sample labels - push!(art.labels, y[i]) - # Fast commit the sample - art.W = hcat(art.W, sample) - art.n_categories += 1 - art.n_clusters += 1 - continue - end - # Compute the activation and match for all categories - activation_match!(art, sample) - # Sort activation function values in descending order - index = sortperm(art.T, rev=true) - # Default to mismatch - mismatch_flag = true - # Loop over all categories - for j = 1:art.n_categories - # Best matching unit - bmu = index[j] - # Vigilance test upper bound - if art.M[bmu] >= art.opts.rho_ub * art.config.dim - # Learn the sample - learn!(art, sample, bmu) - # Update sample label for output` - label = supervised ? y[i] : art.labels[bmu] - # push!(art.labels, label) - # No mismatch - mismatch_flag = false - break - # Vigilance test lower bound - elseif art.M[bmu] >= art.opts.rho_lb * art.config.dim - # # Update sample labels - # label = supervised ? y[i] : art.map[bmu] - label = supervised ? y[i] : art.labels[bmu] - push!(art.labels, label) - # Fast commit the sample - art.W = hcat(art.W, sample) - art.n_categories += 1 - # No mismatch - mismatch_flag = false - break - end - end - # If there was no resonant category, make a new one - if mismatch_flag - # Create a new category-to-cluster label - # push!(art.map, last(art.map) + 1) - label = supervised ? y[i] : art.n_clusters + 1 - push!(art.labels, label) - # Fast commit the sample - art.W = hcat(art.W, sample) - # Increment the number of categories and clusters - art.n_categories += 1 - art.n_clusters += 1 - end + # If label is new, break to make new category + if supervised && !(y in art.labels) + y_hat = y + # Update sample labels + push!(art.labels, y) + # Fast commit the sample + art.W = hcat(art.W, sample) + art.n_categories += 1 + art.n_clusters += 1 + return y_hat + end - if n_samples == 1 - y_hat = label - else - y_hat[i] = label + # Compute the activation and match for all categories + activation_match!(art, sample) + # Sort activation function values in descending order + index = sortperm(art.T, rev=true) + + # Default to mismatch + mismatch_flag = true + # Loop over all categories + for j = 1:art.n_categories + # Best matching unit + bmu = index[j] + # Vigilance test upper bound + if art.M[bmu] >= art.threshold_ub + # If supervised and the label differs, trigger mismatch + if supervised && (art.labels[bmu] != y) + break end - end - # Check for the stopping condition for the whole loop - if stopping_conditions(art) + # Learn the sample + learn!(art, sample, bmu) + # Update sample label for output + # y_hat = supervised ? y : art.labels[bmu] + y_hat = art.labels[bmu] + # No mismatch + mismatch_flag = false + break + # Vigilance test lower bound + elseif art.M[bmu] >= art.threshold_lb + # If supervised and the label differs, trigger mismatch + if supervised && (art.labels[bmu] != y) + break + end + # Update sample labels + y_hat = supervised ? y : art.labels[bmu] + push!(art.labels, y_hat) + # Fast commit the sample + art.W = hcat(art.W, sample) + art.n_categories += 1 + # No mismatch + mismatch_flag = false break end end + # If there was no resonant category, make a new one + if mismatch_flag + # Create a new category-to-cluster label + y_hat = supervised ? y : art.n_clusters + 1 + push!(art.labels, y_hat) + # Fast commit the sample + art.W = hcat(art.W, sample) + # Increment the number of categories and clusters + art.n_categories += 1 + art.n_clusters += 1 + end + return y_hat -end # train!(art::DVFA, x::RealArray ; y::IntegerVector = Vector{Integer}(), preprocessed::Bool=false) +end # train!(art::DVFA, x::RealVector ; y::Integer=0, preprocessed::Bool=false) """ - classify(art::DVFA, x::RealArray) + classify(art::DVFA, x::RealVector ; preprocessed::Bool=false, get_bmu::Bool=false) Predict categories of 'x' using the DVFA model. @@ -304,76 +269,36 @@ julia> train!(my_DVFA, x) julia> y_hat = classify(my_DVFA, y) ``` """ -function classify(art::DVFA, x::RealArray ; preprocessed::Bool=false, get_bmu::Bool=false) - # Show a message if display is on - art.opts.display && @info "Testing DVFA" - - # Data information and setup - n_samples = get_n_samples(x) - - # Verify that the data is setup before classifying - !art.config.setup && @error "Attempting to classify data before setup" - - # If the data is not preprocessed, then complement code it - if !preprocessed - x = complement_code(x, config=art.config) - end - - # Initialize the output vector - if n_samples == 1 - y_hat = zero(Integer) - else - y_hat = zeros(Integer, n_samples) +function classify(art::DVFA, x::RealVector ; preprocessed::Bool=false, get_bmu::Bool=false) + # Preprocess the data + sample = init_classify!(x, art, preprocessed) + + # Compute activation and match functions + activation_match!(art, sample) + # Sort activation function values in descending order + index = sortperm(art.T, rev=true) + mismatch_flag = true + for jx in 1:art.n_categories + bmu = index[jx] + # Vigilance check - pass + if art.M[bmu] >= art.threshold_ub + # Current winner + y_hat = art.labels[bmu] + mismatch_flag = false + break + end end - iter = get_iterator(art.opts, x) - for ix in iter - # Update the iterator if necessary - update_iter(art, iter, ix) - # Compute activation and match functions - activation_match!(art, x[:, ix]) - # Sort activation function values in descending order - index = sortperm(art.T, rev=true) - mismatch_flag = true - for jx in 1:art.n_categories - bmu = index[jx] - # Vigilance check - pass - if art.M[bmu] >= art.opts.rho_ub * art.config.dim - # Current winner - label = art.labels[bmu] - if n_samples == 1 - y_hat = label - else - y_hat[ix] = label - end - mismatch_flag = false - break - end - end - if mismatch_flag - # Create new weight vector - @debug "Mismatch" - # If falling back to the highest activated category, return that - if get_bmu - label = art.labels[index[1]] - if n_samples == 1 - y_hat = label - else - y_hat[ix] = label - end - # Otherwise, return a mismatch - else - if n_samples == 1 - y_hat = -1 - else - y_hat[ix] = -1 - end - end - end + # If we did not find a resonant category + if mismatch_flag + # Create new weight vector + @debug "Mismatch" + # Report either the best matching unit or the mismatch label -1 + y_hat = get_bmu ? art.labels[index[1]] : -1 end return y_hat -end # classify(art::DVFA, x::RealArray ; preprocessed::Bool=false, get_bmu::Bool=false) +end # classify(art::DVFA, x::RealVector ; preprocessed::Bool=false, get_bmu::Bool=false) """ activation_match!(art::DVFA, x::RealVector) @@ -387,7 +312,7 @@ function activation_match!(art::DVFA, x::RealVector) numerator = norm(element_min(x, art.W[:, jx]), 1) art.T[jx] = numerator/(art.opts.alpha + norm(art.W[:, jx], 1)) art.M[jx] = numerator - end # activation_match!(art::DVFA, x::RealVector) + end end # activation_match!(art::DVFA, x::RealVector) """ @@ -416,5 +341,5 @@ end # learn!(art::DVFA, x::RealVector, index::Integer) Stopping conditions for a DVFA module. """ function stopping_conditions(art::DVFA) - return isequal(art.W, art.W_old) || art.epoch >= art.opts.max_epochs + return art.epoch >= art.opts.max_epochs end # stopping_conditions(art::DVFA) diff --git a/src/ART/FuzzyART.jl b/src/ART/FuzzyART.jl new file mode 100644 index 00000000..d1a2a19a --- /dev/null +++ b/src/ART/FuzzyART.jl @@ -0,0 +1,463 @@ +""" + FuzzyART.jl + +Description: + Includes all of the structures and logic for running a Gamma-Normalized Fuzzy ART module. +""" + +# --------------------------------------------------------------------------- # +# OPTIONS +# --------------------------------------------------------------------------- # + +""" + opts_FuzzyART() + +Gamma-Normalized Fuzzy ART options struct. + +# Examples +```julia-repl +julia> opts_FuzzyART() +Initialized FuzzyART +``` +""" +@with_kw mutable struct opts_FuzzyART <: ARTOpts @deftype Float + # Vigilance parameter: [0, 1] + rho = 0.6; @assert rho >= 0.0 && rho <= 1.0 + # Choice parameter: alpha > 0 + alpha = 1e-3; @assert alpha > 0.0 + # Learning parameter: (0, 1] + beta = 1.0; @assert beta > 0.0 && beta <= 1.0 + # "Pseudo" kernel width: gamma >= 1 + gamma = 3.0; @assert gamma >= 1.0 + # "Reference" gamma for normalization: 0 <= gamma_ref < gamma + gamma_ref = 1.0; @assert 0.0 <= gamma_ref && gamma_ref <= gamma + # Display flag + display::Bool = true + # Maximum number of epochs during training + max_epochs::Int = 1 + # Normalize the threshold by the feature dimension + gamma_normalization::Bool = false +end # opts_FuzzyART + +# --------------------------------------------------------------------------- # +# STRUCTS +# --------------------------------------------------------------------------- # + +""" + FuzzyART <: ART + +Gamma-Normalized Fuzzy ART learner struct + +# Examples +```julia-repl +julia> FuzzyART() +FuzzyART + opts: opts_FuzzyART + ... +``` +""" +mutable struct FuzzyART <: ART + # Assign numerical parameters from options + opts::opts_FuzzyART + config::DataConfig + + # Working variables + threshold::Float + labels::IntegerVector + T::RealVector + M::RealVector + + # "Private" working variables + W::RealMatrix + n_instance::IntegerVector + n_categories::Int + epoch::Int +end # FuzzyART <: ART + +# --------------------------------------------------------------------------- # +# CONSTRUCTORS +# --------------------------------------------------------------------------- # + +""" + FuzzyART() + +Implements a Gamma-Normalized Fuzzy ART learner. + +# Examples +```julia-repl +julia> FuzzyART() +FuzzyART + opts: opts_FuzzyART + ... +``` +""" +function FuzzyART() + opts = opts_FuzzyART() + FuzzyART(opts) +end # FuzzyART() + +""" + FuzzyART(;kwargs...) + +Implements a Gamma-Normalized Fuzzy ART learner with keyword arguments. + +# Examples +```julia-repl +julia> FuzzyART(rho=0.7) +FuzzyART + opts: opts_FuzzyART + ... +``` +""" +function FuzzyART(;kwargs...) + opts = opts_FuzzyART(;kwargs...) + FuzzyART(opts) +end # FuzzyART(;kwargs...) + +""" + FuzzyART(opts::opts_FuzzyART) + +Implements a Gamma-Normalized Fuzzy ART learner with specified options. + +# Examples +```julia-repl +julia> FuzzyART(opts) +FuzzyART + opts: opts_FuzzyART + ... +``` +""" +function FuzzyART(opts::opts_FuzzyART) + FuzzyART(opts, # opts + DataConfig(), # config + 0.0, # threshold + Array{Int}(undef,0), # labels + Array{Float}(undef, 0), # T + Array{Float}(undef, 0), # M + Array{Float}(undef, 0, 0), # W + Array{Int}(undef, 0), # n_instance + 0, # n_categories + 0 # epoch + ) +end # FuzzyART(opts::opts_FuzzyART) + +""" + FuzzyART(opts::opts_FuzzyART, sample::RealVector) + +Create and initialize a FuzzyART with a single sample in one step. +""" +function FuzzyART(opts::opts_FuzzyART, sample::RealVector ; preprocessed::Bool=false) + art = FuzzyART(opts) + init_train!(sample, art, preprocessed) + initialize!(art, sample) + return art +end # FuzzyART(opts::opts_FuzzyART, sample::RealVector) + +# --------------------------------------------------------------------------- # +# ALGORITHMIC METHODS +# --------------------------------------------------------------------------- # + +function set_threshold!(art::FuzzyART) + if art.opts.gamma_normalization + art.threshold = art.opts.rho*(art.config.dim^art.opts.gamma_ref) + else + art.threshold = art.opts.rho + end +end # set_threshold!(art::FuzzyART) + +""" + initialize!(art::FuzzyART, x::Vector{T} ; y::Integer=0) where {T<:RealFP} + +Initializes a FuzzyART learner with an intial sample 'x'. + +# Examples +```julia-repl +julia> my_FuzzyART = FuzzyART() +FuzzyART + opts: opts_FuzzyART + ... +julia> initialize!(my_FuzzyART, [1 2 3 4]) +``` +""" +function initialize!(art::FuzzyART, x::Vector{T} ; y::Integer=0) where {T<:RealFP} + # Initialize the instance and categories counters + art.n_instance = [1] + art.n_categories = 1 + + # Set the threshold + set_threshold!(art) + + # Fast commit the weight + art.W = Array{T}(undef, art.config.dim_comp, 1) + + # Assign the contents, valid this way for 1-D or 2-D arrays + art.W[:, 1] = x + + # Set the label to either the supervised label or 1 if unsupervised + label = !iszero(y) ? y : 1 + + # Add the label to the label list + push!(art.labels, label) +end # initialize!(art::FuzzyART, x::Vector{T} ; y::Integer=0) where {T<:RealFP} + +""" + train!(art::FuzzyART, x::RealVector ; y::Integer=0, preprocessed::Bool=false) +""" +function train!(art::FuzzyART, x::RealVector ; y::Integer=0, preprocessed::Bool=false) + # Flag for if training in supervised mode + supervised = !iszero(y) + + # Run the sequential initialization procedure + sample = init_train!(x, art, preprocessed) + + # Initialization if weights are empty; fast commit the first sample + if isempty(art.W) + y_hat = supervised ? y : 1 + initialize!(art, sample, y=y_hat) + return y_hat + end + + # If we have a new supervised category, create a new category + if supervised && !(y in art.labels) + create_category(art, sample, y) + return y + end + + # Compute activation/match functions + activation_match!(art, sample) + # Sort activation function values in descending order + index = sortperm(art.T, rev=true) + # Initialize mismatch as true + mismatch_flag = true + + + # Loop over all categories + for j = 1:art.n_categories + # Best matching unit + bmu = index[j] + # Vigilance check - pass + if art.M[bmu] >= art.threshold + # If supervised and the label differed, force mismatch + if supervised && (art.labels[bmu] != y) + break + end + # Learn the sample + learn!(art, sample, bmu) + # Save the output label for the sample + y_hat = art.labels[bmu] + # No mismatch + mismatch_flag = false + break + end + end + + # If there was no resonant category, make a new one + if mismatch_flag + # Get the correct label for the new category + y_hat = supervised ? y : art.n_categories + 1 + # Create a new category + create_category(art, sample, y_hat) + end + + return y_hat +end # train!(art::FuzzyART, x::RealVector ; y::Integer=0, preprocessed::Bool=false) + +""" + create_category(art::FuzzyART, x::RealVector, y::Integer) +""" +function create_category(art::FuzzyART, x::RealVector, y::Integer) + # Increment the number of categories + art.n_categories += 1 + # Fast commit + art.W = hcat(art.W, x) + # Increment number of samples associated with new category + push!(art.n_instance, 1) + # Add the label for the ategory + push!(art.labels, y) +end # create_category(art::FuzzyART, x::RealVector, y::Integer) + +""" + train!(art::FuzzyART, x::RealMatrix ; y::IntegerVector = Vector{Int}(), preprocessed::Bool=false) + +Trains a FuzzyART learner with dataset 'x' and optional labels 'y' + +# Examples +```julia-repl +julia> my_FuzzyART = FuzzyART() +FuzzyART + opts: opts_FuzzyART + ... +julia> x = load_data() +julia> train!(my_FuzzyART, x) +``` +""" +function train!(art::FuzzyART, x::RealMatrix ; y::IntegerVector = Vector{Int}(), preprocessed::Bool=false) + # Show a message if display is on + art.opts.display && @info "Training FuzzyART" + + # Flag for if training in supervised mode + supervised = !isempty(y) + + # Data information and setup + n_samples = get_n_samples(x) + + # Run the batch initialization procedure + x = init_train!(x, art, preprocessed) + + # Initialize the output vector + y_hat = zeros(Int, n_samples) + # Learning + art.epoch = 0 + while true + # Increment the epoch and get the iterator + art.epoch += 1 + iter = get_iterator(art.opts, x) + # Loop over samples + for i = iter + # Update the iterator if necessary + update_iter(art, iter, i) + # Grab the sample slice + # sample = get_sample(x, i) + sample = x[:, i] + # Train on the sample + local_y = supervised ? y[i] : 0 + y_hat[i] = train!(art, sample, y=local_y, preprocessed=true) + end + # Check for the stopping condition for the whole loop + if stopping_conditions(art) + break + end + end +end # train!(art::FuzzyART, x::RealMatrix ; y::IntegerVector = Vector{Int}(), preprocessed::Bool=false) + +""" + classify(art::FuzzyART, x::RealVector ; preprocessed::Bool=false, get_bmu::Bool=false) +""" +function classify(art::FuzzyART, x::RealVector ; preprocessed::Bool=false, get_bmu::Bool=false) + # Preprocess the data + x = init_classify!(x, art, preprocessed) + + # Compute activation and match functions + activation_match!(art, x) + # Sort activation function values in descending order + index = sortperm(art.T, rev=true) + # Default is mismatch + mismatch_flag = true + y_hat = -1 + for jx in 1:art.n_categories + bmu = index[jx] + # Vigilance check - pass + if art.M[bmu] >= art.threshold + # Current winner + y_hat = art.labels[bmu] + mismatch_flag = false + break + end + end + # If we did not find a match + if mismatch_flag + # Create new weight vector + @debug "Mismatch" + # Report either the best matching unit or the mismatch label -1 + y_hat = get_bmu ? art.labels[index[1]] : -1 + end + return y_hat +end # classify(art::FuzzyART, x::RealVector ; preprocessed::Bool=false, get_bmu::Bool=false) + +""" + classify(art::FuzzyART, x::RealMatrix ; preprocessed::Bool=false, get_bmu::Bool=false) + +Batch predict categories of 'x' using the FuzzyART model. + +Returns predicted categories 'y_hat' + +# Examples +```julia-repl +julia> my_FuzzyART = FuzzyART() +FuzzyART + opts: opts_FuzzyART + ... +julia> x, y = load_data() +julia> train!(my_FuzzyART, x) +julia> y_hat = classify(my_FuzzyART, y) +``` +""" +function classify(art::FuzzyART, x::RealMatrix ; preprocessed::Bool=false, get_bmu::Bool=false) + # Preprocess the data + x = init_classify!(x, art, preprocessed) + # Get the number of samples to classify + n_samples = get_n_samples(x) + + # Initialize the output vector and iterate across all data + y_hat = zeros(Int, n_samples) + iter = get_iterator(art.opts, x) + for ix in iter + # Update the iterator if necessary + update_iter(art, iter, ix) + sample = x[:, ix] + y_hat[ix] = classify(art, sample, preprocessed=true, get_bmu=get_bmu) + end + return y_hat +end # classify(art::FuzzyART, x::RealMatrix ; preprocessed::Bool=false, get_bmu::Bool=false) + +""" + activation_match!(art::FuzzyART, x::RealVector) + +Computes the activation and match functions of the art module against sample x. + +# Examples +```julia-repl +julia> my_FuzzyART = FuzzyART() +FuzzyART + opts: opts_FuzzyART + ... +julia> x, y = load_data() +julia> train!(my_FuzzyART, x) +julia> x_sample = x[:, 1] +julia> activation_match!(my_FuzzyART, x_sample) +``` +""" +function activation_match!(art::FuzzyART, x::RealVector) + art.T = zeros(art.n_categories) + art.M = zeros(art.n_categories) + for i = 1:art.n_categories + W_norm = norm(art.W[:, i], 1) + numerator = norm(element_min(x, art.W[:, i]), 1) + art.T[i] = (numerator/(art.opts.alpha + W_norm))^art.opts.gamma + if art.opts.gamma_normalization + art.M[i] = (W_norm^art.opts.gamma_ref)*art.T[i] + else + art.M[i] = numerator/norm(x, 1) + end + end +end # activation_match!(art::FuzzyART, x::RealVector) + +""" + learn(art::FuzzyART, x::RealVector, W::RealVector) + +Return the modified weight of the art module conditioned by sample x. +""" +function learn(art::FuzzyART, x::RealVector, W::RealVector) + # Update W + return art.opts.beta .* element_min(x, W) .+ W .* (1 - art.opts.beta) +end # learn(art::FuzzyART, x::RealVector, W::RealVector) + +""" + learn!(art::FuzzyART, x::RealVector, index::Integer) + +In place learning function with instance counting. +""" +function learn!(art::FuzzyART, x::RealVector, index::Integer) + # Update W + art.W[:, index] = learn(art, x, art.W[:, index]) + art.n_instance[index] += 1 +end # learn!(art::FuzzyART, x::RealVector, index::Integer) + +""" + stopping_conditions(art::FuzzyART) + +Stopping conditions for a FuzzyART module. +""" +function stopping_conditions(art::FuzzyART) + return art.epoch >= art.opts.max_epochs +end # stopping_conditions(art::FuzzyART) diff --git a/src/ART/common.jl b/src/ART/common.jl new file mode 100644 index 00000000..bf14c976 --- /dev/null +++ b/src/ART/common.jl @@ -0,0 +1,58 @@ +""" + common.jl + +Description: + Includes all of the unsupervised ART modules common code. +""" + +""" + train!(art::ART, x::RealMatrix ; y::IntegerVector=Vector{Int}(), preprocessed::Bool=false) + +Train the ART model on a batch of data 'x' with optional supervisory labels 'y.' + +# Arguments +- `art::ART`: the unsupervised ART model to train. +- `x::RealMatrix`: the 2-D dataset containing columns of samples with rows of features. +- `y::IntegerVector=Vector{Int}()`: optional, labels for simple supervisory training. +- `preprocessed::Bool=false`: flag, if the data has already been complement coded or not. +""" +function train!(art::ART, x::RealMatrix ; y::IntegerVector = Vector{Int}(), preprocessed::Bool=false) + # Show a message if display is on + art.opts.display && @info "Training $(typeof(art))" + + # Flag for if training in supervised mode + supervised = !isempty(y) + + # Data information and setup + n_samples = get_n_samples(x) + + # Run the batch initialization procedure + x = init_train!(x, art, preprocessed) + + # Initialize the output vector + y_hat = zeros(Int, n_samples) + # Learn until the stopping conditions + art.epoch = 0 + while true + # Increment the epoch and get the iterator + art.epoch += 1 + iter = get_iterator(art.opts, x) + for i = iter + # Update the iterator if necessary + update_iter(art, iter, i) + # Grab the sample slice + # sample = get_sample(x, i) + sample = x[:, i] + # Select the label to pass to the incremental method + local_y = supervised ? y[i] : 0 + # Train upon the sample and label + y_hat[i] = train!(art, sample, y=local_y, preprocessed=true) + end + + # Check stopping conditions + if stopping_conditions(art) + break + end + end + return y_hat +end # train!(art::ART, x::RealMatrix ; y::IntegerVector = Vector{Int}(), preprocessed::Bool=false) diff --git a/src/ARTMAP/ARTMAP.jl b/src/ARTMAP/ARTMAP.jl index 739b34cc..7dbfa275 100644 --- a/src/ARTMAP/ARTMAP.jl +++ b/src/ARTMAP/ARTMAP.jl @@ -5,6 +5,7 @@ Description: Includes all of the ARTMAP (i.e., explicitly supervised) ART modules definitions. """ +include("common.jl") # train! include("DAM.jl") # Default ARTMAP include("FAM.jl") # Fuzzy ARTMAP include("SFAM.jl") # Simplified Fuzzy ARTMAP diff --git a/src/ARTMAP/DAM.jl b/src/ARTMAP/DAM.jl index 4553dc79..506974ff 100644 --- a/src/ARTMAP/DAM.jl +++ b/src/ARTMAP/DAM.jl @@ -15,19 +15,21 @@ Implements a Default ARTMAP learner's options. julia> my_opts = opts_DAM() ``` """ -@with_kw mutable struct opts_DAM <: ARTOpts @deftype RealFP +@with_kw mutable struct opts_DAM <: ARTOpts @deftype Float # Vigilance parameter: [0, 1] - rho = 0.6; @assert rho >= 0.0 && rho <= 1.0 + rho = 0.75; @assert rho >= 0.0 && rho <= 1.0 # Choice parameter: alpha > 0 alpha = 1e-7; @assert alpha > 0.0 # Match tracking parameter - epsilon = -1e-3; @assert epsilon > -1.0 && epsilon < 1.0 + epsilon = 1e-3; @assert epsilon > 0.0 && epsilon < 1.0 # Learning parameter: (0, 1] beta = 1.0; @assert beta > 0.0 && beta <= 1.0 + # Uncommitted node flag + uncommitted::Bool = true # Display flag display::Bool = true # Maximum number of epochs during training - max_epochs::Integer = 1 + max_epochs::Int = 1 end # opts_DAM() """ @@ -39,17 +41,15 @@ mutable struct DAM <: ARTMAP opts::opts_DAM config::DataConfig W::RealMatrix - W_old::RealMatrix labels::IntegerVector - y::IntegerVector - n_categories::Integer - epoch::Integer + n_categories::Int + epoch::Int end # DAM <: ARTMAP """ DAM() -Implements a Default ARTMAP learner. +Implements a Simple Fuzzy ARTMAP learner. # Examples ```julia-repl @@ -85,7 +85,7 @@ end # DAM(;kwargs...) """ DAM(opts) -Implements a Default ARTMAP learner with specified options +Implements a Default ARTMAP learner with specified options. # Examples ```julia-repl @@ -97,14 +97,13 @@ DAM ``` """ function DAM(opts::opts_DAM) - DAM(opts, # opts_DAM - DataConfig(), # config - Array{RealFP}(undef, 0,0), # W - Array{RealFP}(undef, 0,0), # W_old - Array{Integer}(undef, 0), # labels - Array{Integer}(undef, 0), # y - 0, # n_categories - 0 # epoch + DAM( + opts, # opts_DAM + DataConfig(), # config + Array{Float}(undef, 0, 0), # W + Array{Int}(undef, 0), # labels + 0, # n_categories + 0 # epoch ) end # DAM(opts::opts_DAM) @@ -123,97 +122,66 @@ DAM julia> train!(art, x, y) ``` """ -function train!(art::DAM, x::RealArray, y::RealArray ; preprocessed::Bool=false) - # Show a message if display is on - art.opts.display && @info "Training DAM" - - # Data information and setup - n_samples = get_n_samples(x) - - # Set up the data config if it is not already - !art.config.setup && data_setup!(art.config, x) +function train!(art::DAM, x::RealVector, y::Integer ; preprocessed::Bool=false) + # Run the sequential initialization procedure + sample = init_train!(x, art, preprocessed) + + # Initialization + if !(y in art.labels) + # Initialize W and labels + if isempty(art.W) + art.W = Array{Float64}(undef, art.config.dim_comp, 1) + art.W[:, 1] = sample + else + art.W = [art.W sample] + end + push!(art.labels, y) + art.n_categories += 1 + else + # Baseline vigilance parameter + rho_baseline = art.opts.rho - # If the data isn't preprocessed, then complement code it with the config - if !preprocessed - x = complement_code(x, config=art.config) - end + # Compute activation function + T = zeros(art.n_categories) + for jx in 1:art.n_categories + T[jx] = activation(art, sample, art.W[:, jx]) + end - # Convenient semantic flag - # is_supervised = !isempty(y) - - # Initialize the internal categories - art.y = zeros(Integer, n_samples) - - # Initialize the training loop, continue to convergence - art.epoch = 0 - while true - # Increment the epoch and get the iterator - art.epoch += 1 - iter = get_iterator(art.opts, x) - for ix in iter - # Update the iterator if necessary - update_iter(art, iter, ix) - if !(y[ix] in art.labels) - # Initialize W and labels - if isempty(art.W) - art.W = Array{Float64}(undef, art.config.dim_comp, 1) - art.W_old = Array{Float64}(undef, art.config.dim_comp, 1) - art.W[:, ix] = x[:, ix] + # Sort activation function values in descending order + index = sortperm(T, rev=true) + mismatch_flag = true + for jx in 1:art.n_categories + # Compute match function + M = art_match(art, sample, art.W[:, index[jx]]) + # Current winner + if M >= rho_baseline + if y == art.labels[index[jx]] + # Learn + @debug "Learning" + art.W[:, index[jx]] = learn(art, sample, art.W[:, index[jx]]) + mismatch_flag = false + break else - art.W = [art.W x[:, ix]] - end - push!(art.labels, y[ix]) - art.n_categories += 1 - art.y[ix] = y[ix] - else - # Baseline vigilance parameter - rho_baseline = art.opts.rho - - # Compute activation function - T = zeros(art.n_categories) - for jx in 1:art.n_categories - T[jx] = activation(art, x[:, ix], art.W[:, jx]) - end - - # Sort activation function values in descending order - index = sortperm(T, rev=true) - mismatch_flag = true - for jx in 1:art.n_categories - # Compute match function - M = art_match(art, x[:, ix], art.W[:, index[jx]]) - @debug M - # Current winner - if M >= rho_baseline - if y[ix] == art.labels[index[jx]] - # Learn - @debug "Learning" - art.W[:, index[jx]] = learn(art, x[:, ix], art.W[:, index[jx]]) - art.y[ix] = art.labels[index[jx]] - mismatch_flag = false - break - else - # Match tracking - @debug "Match tracking" - rho_baseline = M + art.opts.epsilon - end - end - end - if mismatch_flag - # Create new weight vector - @debug "Mismatch" - art.W = hcat(art.W, x[:, ix]) - push!(art.labels, y[ix]) - art.n_categories += 1 - art.y[ix] = y[ix] + # Match tracking + @debug "Match tracking" + rho_baseline = M + art.opts.epsilon end end end - if stopping_conditions(art) - break + + # If we triggered a mismatch + if mismatch_flag + # Create new weight vector + @debug "Mismatch" + art.W = hcat(art.W, sample) + push!(art.labels, y) + art.n_categories += 1 end - art.W_old = deepcopy(art.W) end -end # train!(art::DAM, x::RealArray, y::RealArray ; preprocessed::Bool=false) + + # ARTMAP guarantees correct training classification, so just return the label + return y +end """ classify(art::DAM, x::RealArray ; preprocessed::Bool=false) @@ -232,56 +200,39 @@ julia> train!(art, x, y) julia> classify(art, x_test) ``` """ -function classify(art::DAM, x::RealArray ; preprocessed::Bool=false) - # Show a message if display is on - art.opts.display && @info "Testing DAM" - - # Data information and setup - n_samples = get_n_samples(x) - - # Throw an soft error if classifying before setup - !art.config.setup && @error "Attempting to classify data before setup" - - # If the data is not preprocessed, then complement code it - if !preprocessed - x = complement_code(x, config=art.config) +function classify(art::DAM, x::RealVector ; preprocessed::Bool=false, get_bmu::Bool=false) + # Run the sequential initialization procedure + sample = init_classify!(x, art, preprocessed) + + # Compute activation function + T = zeros(art.n_categories) + for jx in 1:art.n_categories + T[jx] = activation(art, sample, art.W[:, jx]) end - # Initialize the output vector and iterate across all data - y_hat = zeros(Int, n_samples) - iter = get_iterator(art.opts, x) - for ix in iter - # Update the iterator if necessary - update_iter(art, iter, ix) - - # Compute activation function - T = zeros(art.n_categories) - for jx in 1:art.n_categories - T[jx] = activation(art, x[:, ix], art.W[:, jx]) + # Sort activation function values in descending order + index = sortperm(T, rev=true) + mismatch_flag = true + for jx in 1:art.n_categories + # Compute match function + M = art_match(art, sample, art.W[:, index[jx]]) + # Current winner + if M >= art.opts.rho + y_hat = art.labels[index[jx]] + mismatch_flag = false + break end + end - # Sort activation function values in descending order - index = sortperm(T, rev=true) - mismatch_flag = true - for jx in 1:art.n_categories - # Compute match function - M = art_match(art, x[:, ix], art.W[:, index[jx]]) - @debug M - # Current winner - if M >= art.opts.rho - y_hat[ix] = art.labels[index[jx]] - mismatch_flag = false - break - end - end - if mismatch_flag - # Create new weight vector - @debug "Mismatch" - y_hat[ix] = -1 - end + # If we did not find a resonant category + if mismatch_flag + @debug "Mismatch" + # Report either the best matching unit or the mismatch label -1 + y_hat = get_bmu ? art.labels[index[1]] : -1 end + return y_hat -end # classify(art::DAM, x::RealArray ; preprocessed::Bool=false) +end """ stopping_conditions(art::DAM) @@ -290,7 +241,7 @@ Stopping conditions for Default ARTMAP, checked at the end of every epoch. """ function stopping_conditions(art::DAM) # Compute the stopping condition, return a bool - return art.W == art.W_old || art.epoch >= art.opts.max_epochs + return art.epoch >= art.opts.max_epochs end # stopping_conditions(art::DAM) """ @@ -307,7 +258,7 @@ end # activation(art::DAM, x::RealVector, W::RealVector) """ learn(art::DAM, x::RealVector, W::RealVector) -Returns a single updated weight for the Simple Fuzzy ARTMAP module for weight +Returns a single updated weight for the Default ARTMAP module for weight vector W and sample x. """ function learn(art::DAM, x::RealVector, W::RealVector) diff --git a/src/ARTMAP/FAM.jl b/src/ARTMAP/FAM.jl index f2bd36fd..7f451608 100644 --- a/src/ARTMAP/FAM.jl +++ b/src/ARTMAP/FAM.jl @@ -15,7 +15,7 @@ Implements a Fuzzy ARTMAP learner's options. julia> my_opts = opts_FAM() ``` """ -@with_kw mutable struct opts_FAM <: ARTOpts @deftype RealFP +@with_kw mutable struct opts_FAM <: ARTOpts @deftype Float # Vigilance parameter: [0, 1] rho = 0.6; @assert rho >= 0.0 && rho <= 1.0 # Choice parameter: alpha > 0 @@ -29,7 +29,7 @@ julia> my_opts = opts_FAM() # Display flag display::Bool = true # Maximum number of epochs during training - max_epochs::Integer = 1 + max_epochs::Int = 1 end # opts_FAM() """ @@ -41,11 +41,9 @@ mutable struct FAM <: ARTMAP opts::opts_FAM config::DataConfig W::RealMatrix - W_old::RealMatrix labels::IntegerVector - y::IntegerVector - n_categories::Integer - epoch::Integer + n_categories::Int + epoch::Int end # FAM <: ARTMAP """ @@ -101,10 +99,8 @@ FAM function FAM(opts::opts_FAM) FAM(opts, # opts_FAM DataConfig(), # config - Array{RealFP}(undef, 0,0), # W - Array{RealFP}(undef, 0,0), # W_old - Array{Integer}(undef, 0), # labels - Array{Integer}(undef, 0), # y + Array{Float}(undef, 0,0), # W + Array{Int}(undef, 0), # labels 0, # n_categories 0 # epoch ) diff --git a/src/ARTMAP/SFAM.jl b/src/ARTMAP/SFAM.jl index e0418879..5390216d 100644 --- a/src/ARTMAP/SFAM.jl +++ b/src/ARTMAP/SFAM.jl @@ -15,7 +15,7 @@ Implements a Simple Fuzzy ARTMAP learner's options. julia> my_opts = opts_SFAM() ``` """ -@with_kw mutable struct opts_SFAM <: ARTOpts @deftype RealFP +@with_kw mutable struct opts_SFAM <: ARTOpts @deftype Float # Vigilance parameter: [0, 1] rho = 0.75; @assert rho >= 0.0 && rho <= 1.0 # Choice parameter: alpha > 0 @@ -29,7 +29,7 @@ julia> my_opts = opts_SFAM() # Display flag display::Bool = true # Maximum number of epochs during training - max_epochs::Integer = 1 + max_epochs::Int = 1 end # opts_SFAM() """ @@ -41,11 +41,9 @@ mutable struct SFAM <: ARTMAP opts::opts_SFAM config::DataConfig W::RealMatrix - W_old::RealMatrix labels::IntegerVector - y::IntegerVector - n_categories::Integer - epoch::Integer + n_categories::Int + epoch::Int end # SFAM <: ARTMAP """ @@ -102,10 +100,8 @@ function SFAM(opts::opts_SFAM) SFAM( opts, # opts_SFAM DataConfig(), # config - Array{RealFP}(undef, 0, 0), # W - Array{RealFP}(undef, 0, 0), # W_old - Array{Integer}(undef, 0), # labels - Array{Integer}(undef, 0), # y + Array{Float}(undef, 0, 0), # W + Array{Int}(undef, 0), # labels 0, # n_categories 0 # epoch ) @@ -126,92 +122,66 @@ SFAM julia> train!(art, x, y) ``` """ -function train!(art::SFAM, x::RealArray, y::RealArray ; preprocessed::Bool=false) - # Show a message if display is on - art.opts.display && @info "Training SFAM" - - # Data information and setup - n_samples = get_n_samples(x) - - # Set up the data config if it is not already - !art.config.setup && data_setup!(art.config, x) +function train!(art::SFAM, x::RealVector, y::Integer ; preprocessed::Bool=false) + # Run the sequential initialization procedure + sample = init_train!(x, art, preprocessed) + + # Initialization + if !(y in art.labels) + # Initialize W and labels + if isempty(art.W) + art.W = Array{Float64}(undef, art.config.dim_comp, 1) + art.W[:, 1] = sample + else + art.W = [art.W sample] + end + push!(art.labels, y) + art.n_categories += 1 + else + # Baseline vigilance parameter + rho_baseline = art.opts.rho - # If the data is not preprocessed, then complement code it - if !preprocessed - x = complement_code(x, config=art.config) - end + # Compute activation function + T = zeros(art.n_categories) + for jx in 1:art.n_categories + T[jx] = activation(art, sample, art.W[:, jx]) + end - # Initialize the internal categories - art.y = zeros(Int, n_samples) - - # Initialize the training loop, continue to convergence - art.epoch = 0 - while true - art.epoch += 1 - iter = get_iterator(art.opts, x) - for ix in iter - # Update the iterator if necessary - update_iter(art, iter, ix) - if !(y[ix] in art.labels) - # Initialize W and labels - if isempty(art.W) - art.W = Array{Float64}(undef, art.config.dim_comp, 1) - art.W_old = Array{Float64}(undef, art.config.dim_comp, 1) - art.W[:, ix] = x[:, ix] + # Sort activation function values in descending order + index = sortperm(T, rev=true) + mismatch_flag = true + for jx in 1:art.n_categories + # Compute match function + M = art_match(art, sample, art.W[:, index[jx]]) + # Current winner + if M >= rho_baseline + if y == art.labels[index[jx]] + # Learn + @debug "Learning" + art.W[:, index[jx]] = learn(art, sample, art.W[:, index[jx]]) + mismatch_flag = false + break else - art.W = [art.W x[:, ix]] - end - push!(art.labels, y[ix]) - art.n_categories += 1 - art.y[ix] = y[ix] - else - # Baseline vigilance parameter - rho_baseline = art.opts.rho - - # Compute activation function - T = zeros(art.n_categories) - for jx in 1:art.n_categories - T[jx] = activation(art, x[:, ix], art.W[:, jx]) - end - - # Sort activation function values in descending order - index = sortperm(T, rev=true) - mismatch_flag = true - for jx in 1:art.n_categories - # Compute match function - M = art_match(art, x[:, ix], art.W[:, index[jx]]) - # Current winner - if M >= rho_baseline - if y[ix] == art.labels[index[jx]] - # Learn - @debug "Learning" - art.W[:, index[jx]] = learn(art, x[:, ix], art.W[:, index[jx]]) - art.y[ix] = art.labels[index[jx]] - mismatch_flag = false - break - else - # Match tracking - @debug "Match tracking" - rho_baseline = M + art.opts.epsilon - end - end - end - if mismatch_flag - # Create new weight vector - @debug "Mismatch" - art.W = hcat(art.W, x[:, ix]) - push!(art.labels, y[ix]) - art.n_categories += 1 - art.y[ix] = y[ix] + # Match tracking + @debug "Match tracking" + rho_baseline = M + art.opts.epsilon end end end - if stopping_conditions(art) - break + + # If we triggered a mismatch + if mismatch_flag + # Create new weight vector + @debug "Mismatch" + art.W = hcat(art.W, sample) + push!(art.labels, y) + art.n_categories += 1 end - art.W_old = deepcopy(art.W) end -end # train!(art::SFAM, x::RealArray, y::RealArray ; preprocessed::Bool=false) + + # ARTMAP guarantees correct training classification, so just return the label + return y +end """ classify(art::SFAM, x::RealArray ; preprocessed::Bool=false) @@ -230,54 +200,39 @@ julia> train!(art, x, y) julia> classify(art, x_test) ``` """ -function classify(art::SFAM, x::RealArray ; preprocessed::Bool=false) - # Show a message if display is on - art.opts.display && @info "Testing SFAM" - - # Data information and setup - n_samples = get_n_samples(x) - - # Throw an soft error if classifying before setup - !art.config.setup && @error "Attempting to classify data before setup" - - # If the data is not preprocessed, then complement code it - if !preprocessed - x = complement_code(x, config=art.config) +function classify(art::SFAM, x::RealVector ; preprocessed::Bool=false, get_bmu::Bool=false) + # Run the sequential initialization procedure + sample = init_classify!(x, art, preprocessed) + + # Compute activation function + T = zeros(art.n_categories) + for jx in 1:art.n_categories + T[jx] = activation(art, sample, art.W[:, jx]) end - # Initialize the output vector and iterate across all data - y_hat = zeros(Int, n_samples) - iter = ProgressBar(1:n_samples) - for ix in iter - set_description(iter, string(@sprintf("ID: %i, Cat: %i", ix, art.n_categories))) - - # Compute activation function - T = zeros(art.n_categories) - for jx in 1:art.n_categories - T[jx] = activation(art, x[:, ix], art.W[:, jx]) + # Sort activation function values in descending order + index = sortperm(T, rev=true) + mismatch_flag = true + for jx in 1:art.n_categories + # Compute match function + M = art_match(art, sample, art.W[:, index[jx]]) + # Current winner + if M >= art.opts.rho + y_hat = art.labels[index[jx]] + mismatch_flag = false + break end + end - # Sort activation function values in descending order - index = sortperm(T, rev=true) - mismatch_flag = true - for jx in 1:art.n_categories - # Compute match function - M = art_match(art, x[:, ix], art.W[:, index[jx]]) - # Current winner - if M >= art.opts.rho - y_hat[ix] = art.labels[index[jx]] - mismatch_flag = false - break - end - end - if mismatch_flag - # Label as -1 if mismatched - @debug "Mismatch" - y_hat[ix] = -1 - end + # If we did not find a resonant category + if mismatch_flag + @debug "Mismatch" + # Report either the best matching unit or the mismatch label -1 + y_hat = get_bmu ? art.labels[index[1]] : -1 end + return y_hat -end # classify(art::SFAM, x::RealArray ; preprocessed::Bool=false) +end """ stopping_conditions(art::SFAM) @@ -286,7 +241,7 @@ Stopping conditions for Simple Fuzzy ARTMAP, checked at the end of every epoch. """ function stopping_conditions(art::SFAM) # Compute the stopping condition, return a bool - return art.W == art.W_old || art.epoch >= art.opts.max_epochs + return art.epoch >= art.opts.max_epochs end # stopping_conditions(art::SFAM) """ @@ -320,5 +275,4 @@ sample x. function art_match(art::SFAM, x::RealVector, W::RealVector) # Compute M and return return norm(element_min(x, W), 1) / art.config.dim - # return norm(element_min(x, W), 1) / art.config.dim_comp end # art_match(art::SFAM, x::RealVector, W::RealVector) diff --git a/src/ARTMAP/common.jl b/src/ARTMAP/common.jl new file mode 100644 index 00000000..c2577444 --- /dev/null +++ b/src/ARTMAP/common.jl @@ -0,0 +1,107 @@ +""" + common.jl + +Description: + Includes all of the unsupervised ARTMAP modules common code. +""" + +""" + train!(art::ARTMAP, x::RealMatrix, y::IntegerVector, preprocessed::Bool=false) + +Train the ARTMAP model on a batch of data 'x' with supervisory labels 'y.' + +# Arguments +- `art::ARTMAP`: the supervised ARTMAP model to train. +- `x::RealMatrix`: the 2-D dataset containing columns of samples with rows of features. +- `y::IntegerVector`: labels for supervisory training. +- `preprocessed::Bool=false`: flag, if the data has already been complement coded or not. +""" +function train!(art::ARTMAP, x::RealMatrix, y::IntegerVector, preprocessed::Bool=false) + # Show a message if display is on + art.opts.display && @info "Training $(typeof(art))" + + # Data information and setup + n_samples = length(y) + + # Run the batch initialization procedure + x = init_train!(x, art, preprocessed) + + # Initialize the output vector + y_hat = zeros(Int, n_samples) + # Learn until the stopping conditions + art.epoch = 0 + while true + # Increment the epoch and get the iterator + art.epoch += 1 + iter = get_iterator(art.opts, x) + for i = iter + # Update the iterator if necessary + update_iter(art, iter, i) + # Grab the sample slice + # sample = get_sample(x, i) + sample = x[:, i] + label = y[i] + # Train upon the sample and label + y_hat[i] = train!(art, sample, label, preprocessed=true) + end + + # Check stopping conditions + if stopping_conditions(art) + break + end + end + return y_hat +end # train!(art::ARTMAP, x::RealMatrix, y::IntegerVector, preprocessed::Bool=false) + +# """ +# classify(art::ARTMAP, x::RealMatrix ; preprocessed::Bool=false, get_bmu::Bool=false) + +# Predict categories of 'x' using the ARTMAP model. + +# Returns predicted categories 'y_hat.' + +# # Arguments +# - `art::ARTMAP`: supervised ARTMAP module to use for batch inference. +# - `x::RealMatrix`: the 2-D dataset containing columns of samples with rows of features. +# - `preprocessed::Bool=false`: flag, if the data has already been complement coded or not. +# - `get_bmu::Bool=false`, flag, if the model should return the best-matching-unit label in the case of total mismatch. + +# # Examples +# ```julia-repl +# julia> my_DDVFA = DDVFA() +# DDVFA +# opts: opts_DDVFA +# ... +# julia> x, y = load_data() +# julia> train!(my_DDVFA, x) +# julia> y_hat = classify(my_DDVFA, y) +# ``` +# """ +# function classify(art::ARTMAP, x::RealMatrix ; preprocessed::Bool=false, get_bmu::Bool=false) +# # Show a message if display is on +# art.opts.display && @info "Testing $(typeof(art))" + +# # Preprocess the data +# x = init_classify!(x, art, preprocessed) + +# # Data information and setup +# n_samples = get_n_samples(x) + +# # Initialize the output vector +# y_hat = zeros(Int, n_samples) + +# # Get the iterator based on the module options and data shape +# iter = get_iterator(art.opts, x) +# for ix = iter +# # Update the iterator if necessary +# update_iter(art, iter, ix) + +# # Grab the sample slice +# sample = get_sample(x, ix) + +# # Get the classification +# y_hat[ix] = classify(art, sample, preprocessed=true, get_bmu=get_bmu) +# end + +# return y_hat +# end # classify(art::ARTMAP, x::RealMatrix ; preprocessed::Bool=false, get_bmu::Bool=false) diff --git a/src/AdaptiveResonance.jl b/src/AdaptiveResonance.jl index ef85fbdb..030d6cae 100644 --- a/src/AdaptiveResonance.jl +++ b/src/AdaptiveResonance.jl @@ -46,8 +46,8 @@ export get_n_samples, # Get the number of samples (1-D interpreted as one sample) # ART (unsupervised) - DDVFA, opts_DDVFA, - GNFA, opts_GNFA, + FuzzyART, opts_FuzzyART, + DDVFA, opts_DDVFA, get_W, DVFA, opts_DVFA, # ARTMAP (supervised) diff --git a/src/common.jl b/src/common.jl index 975d6ac2..c09f8016 100644 --- a/src/common.jl +++ b/src/common.jl @@ -1,4 +1,3 @@ - # ------------------------------------------- # Document: common.jl # Author: Sasha Petrenko @@ -31,6 +30,9 @@ const IntegerMatrix{T<:Integer} = AbstractArray{T, 2} # Specifically floating-point aliases const RealFP = Union{Float32, Float64} +# System's largest native floating point variable +const Float = (Sys.WORD_SIZE == 64 ? Float64 : Float32) + # Acceptable iterators for ART module training and inference const ARTIterator = Union{UnitRange, ProgressBar} @@ -43,8 +45,8 @@ mutable struct DataConfig setup::Bool mins::RealVector maxs::RealVector - dim::Integer - dim_comp::Integer + dim::Int + dim_comp::Int end # DataConfig """ @@ -55,8 +57,8 @@ Default constructor for a data configuration, not set up. function DataConfig() DataConfig( false, # setup - Array{RealFP}(undef, 0), # min - Array{RealFP}(undef, 0), # max + Array{Float}(undef, 0), # min + Array{Float}(undef, 0), # max 0, # dim 0 # dim_comp ) @@ -85,13 +87,13 @@ function DataConfig(mins::RealVector, maxs::RealVector) end # DataConfig(mins::RealVector, maxs::RealVector) """ - DataConfig(min::Real, max::Real, dim::Integer) + DataConfig(min::Real, max::Real, dim::Int) Convenience constructor for DataConfig, requiring only a global min, max, and dim. This constructor is used in the case that the feature mins and maxs are all the same respectively. """ -function DataConfig(min::Real, max::Real, dim::Integer) +function DataConfig(min::Real, max::Real, dim::Int) DataConfig( true, # setup repeat([min], dim), # min @@ -99,7 +101,7 @@ function DataConfig(min::Real, max::Real, dim::Integer) dim, # dim dim*2 # dim_comp ) -end # DataConfig(min::Real, max::Real, dim::Integer) +end # DataConfig(min::Real, max::Real, dim::Int) """ element_min(x::RealVector, W::RealVector) @@ -193,6 +195,22 @@ function data_setup!(config::DataConfig, data::RealMatrix) config.maxs = [maximum(data[i, :]) for i in 1:config.dim] end # data_setup!(config::DataConfig, data::RealMatrix) +""" + DataConfig(data::RealMatrix) + +Convenience constructor for DataConfig, requiring only the data matrix. +""" +function DataConfig(data::RealMatrix) + # Create an empty dataconfig + config = DataConfig() + + # Runthe setup upon the config using the data matrix for reference + data_setup!(config, data) + + # Return the constructed DataConfig + return config +end # DataConfig(min::Real, max::Real, dim::Int) + """ get_data_characteristics(data::RealArray ; config::DataConfig=DataConfig()) @@ -218,25 +236,63 @@ function get_data_characteristics(data::RealArray ; config::DataConfig=DataConfi end # get_data_characteristics(data::RealArray ; config::DataConfig=DataConfig()) """ - linear_normalization(data::RealArray ; config::DataConfig=DataConfig()) + linear_normalization(data::RealVector ; config::DataConfig=DataConfig()) + +Normalize the data to the range [0, 1] along each feature. +""" +function linear_normalization(data::RealVector ; config::DataConfig=DataConfig()) + # Vector normalization requires a setup DataConfig + if !config.setup + error("Attempting to complement code a vector without a setup DataConfig") + end + + # Populate a new array with normalized values. + x_raw = zeros(config.dim) + + # Iterate over each dimension + for i = 1:config.dim + denominator = config.maxs[i] - config.mins[i] + if denominator != 0 + # If the denominator is not zero, normalize + x_raw[i] = (data[i] .- config.mins[i]) ./ denominator + else + # Otherwise, the feature is zeroed because it contains no useful information + x_raw[i] = zero(Int) + end + end + return x_raw +end # linear_normalization(data::RealArray ; config::DataConfig=DataConfig()) + +""" + linear_normalization(data::RealMatrix ; config::DataConfig=DataConfig()) Normalize the data to the range [0, 1] along each feature. """ -function linear_normalization(data::RealArray ; config::DataConfig=DataConfig()) +function linear_normalization(data::RealMatrix ; config::DataConfig=DataConfig()) # Get the data characteristics dim, n_samples, mins, maxs = get_data_characteristics(data, config=config) # Populate a new array with normalized values. x_raw = zeros(dim, n_samples) + + # Verify that all maxs are strictly greater than mins + if !all(mins .< maxs) + error("Got a data max index that is smaller than the corresonding min") + end + + # Iterate over each dimension for i = 1:dim - if maxs[i] < mins[i] - error("Got a data max index that is smaller than the corresonding min") - elseif maxs[i] - mins[i] != 0 - x_raw[i, :] = (data[i, :] .- mins[i]) ./ (maxs[i] - mins[i]) + denominator = maxs[i] - mins[i] + if denominator != 0 + # If the denominator is not zero, normalize + x_raw[i, :] = (data[i, :] .- mins[i]) ./ denominator + else + # Otherwise, the feature is zeroed because it contains no useful information + x_raw[i, :] = zeros(length(x_raw[i, :])) end end return x_raw -end # linear_normalization(data::RealArray ; config::DataConfig=DataConfig()) +end # linear_normalization(data::RealMatrix ; config::DataConfig=DataConfig()) """ complement_code(data::RealArray ; config::DataConfig=DataConfig()) @@ -265,15 +321,15 @@ function get_iterator(opts::ARTOpts, x::RealArray) # Construct the iterator iter_raw = 1:n_samples - iter = prog_bar ? ProgressBar(iter_raw) : iter_raw + iter = prog_bar ? ProgressBar(iter_raw) : iter_raw return iter end # get_iterator(opts::ARTOpts, x::RealArray) """ - update_iter(art::ARTModule, iter::ARTIterator, i::Integer) + update_iter(art::ARTModule, iter::ARTIterator, i::Int) """ -function update_iter(art::ARTModule, iter::ARTIterator, i::Integer) +function update_iter(art::ARTModule, iter::ARTIterator, i::Int) # Check explicitly for each, as the function definition restricts the types if iter isa ProgressBar set_description(iter, string(@sprintf("Ep: %i, ID: %i, Cat: %i", art.epoch, i, art.n_categories))) @@ -283,11 +339,11 @@ function update_iter(art::ARTModule, iter::ARTIterator, i::Integer) end # update_iter(art::ARTModule, iter::Union{UnitRange, ProgressBar}, i::Int) """ - get_sample(x::RealArray, i::Integer) + get_sample(x::RealArray, i::Int) Returns a sample from data array x safely, accounting for 1-D and """ -function get_sample(x::RealArray, i::Integer) +function get_sample(x::RealArray, i::Int) # Get the shape of the data, irrespective of data type dim, n_samples = get_data_shape(x) # Get the type shape of the array @@ -304,4 +360,113 @@ function get_sample(x::RealArray, i::Integer) sample = x[:, i] end return sample -end # get_sample(x::RealArray, i::Integer) +end # get_sample(x::RealArray, i::Int) + +""" + init_train!(x::RealVector, art::ARTModule, preprocessed::Bool) +""" +function init_train!(x::RealVector, art::ARTModule, preprocessed::Bool) + # If the data is not preprocessed + if !preprocessed + # If the data config is not setup, not enough information to preprocess + if !art.config.setup + error("$(typeof(art)): cannot preprocess data before being setup.") + end + x = complement_code(x, config=art.config) + # If it is preprocessed and we are not setup + elseif !art.config.setup + # Get the dimension of the vector + dim_comp = length(x) + # If the complemented dimension is not even, error + if !iseven(dim_comp) + error("Declare that the vector is preprocessed, but it is not even") + end + # Half the complemented dimension and setup the DataConfig with that + dim = Int(dim_comp/2) + art.config = DataConfig(0, 1, dim) + end + return x +end # init_train!(x::RealVector, art::ARTModule, preprocessed::Bool) + +""" + init_train!(x::RealMatrix, art::ARTModule, preprocessed::Bool) +""" +function init_train!(x::RealMatrix, art::ARTModule, preprocessed::Bool) + # If the data is not preprocessed, then complement code it + if !preprocessed + # Set up the data config if training for the first time + !art.config.setup && data_setup!(art.config, x) + x = complement_code(x, config=art.config) + end + return x +end # init_train!(x::RealMatrix, art::ART, preprocessed::Bool) + +""" + init_classify!(x::RealArray, art::ARTModule, preprocessed::Bool) +""" +function init_classify!(x::RealArray, art::ARTModule, preprocessed::Bool) + # If the data is not preprocessed + if !preprocessed + # If the data config is not setup, not enough information to preprocess + if !art.config.setup + error("$(typeof(art)): cannot preprocess data before being setup.") + end + # Dispatch to the correct complement code method (vector or matrix) + x = complement_code(x, config=art.config) + end + return x +end # init_classify!(x::RealArray, art::ART, preprocessed::Bool) + + +""" + classify(art::ARTModule, x::RealMatrix ; preprocessed::Bool=false, get_bmu::Bool=false) + +Predict categories of 'x' using the ART model. + +Returns predicted categories 'y_hat.' + +# Arguments +- `art::ARTModule`: ART or ARTMAP module to use for batch inference. +- `x::RealMatrix`: the 2-D dataset containing columns of samples with rows of features. +- `preprocessed::Bool=false`: flag, if the data has already been complement coded or not. +- `get_bmu::Bool=false`, flag, if the model should return the best-matching-unit label in the case of total mismatch. + +# Examples +```julia-repl +julia> my_DDVFA = DDVFA() +DDVFA + opts: opts_DDVFA + ... +julia> x, y = load_data() +julia> train!(my_DDVFA, x) +julia> y_hat = classify(my_DDVFA, y) +``` +""" +function classify(art::ARTModule, x::RealMatrix ; preprocessed::Bool=false, get_bmu::Bool=false) + # Show a message if display is on + art.opts.display && @info "Testing $(typeof(art))" + + # Preprocess the data + x = init_classify!(x, art, preprocessed) + + # Data information and setup + n_samples = get_n_samples(x) + + # Initialize the output vector + y_hat = zeros(Int, n_samples) + + # Get the iterator based on the module options and data shape + iter = get_iterator(art.opts, x) + for ix = iter + # Update the iterator if necessary + update_iter(art, iter, ix) + + # Grab the sample slice + sample = get_sample(x, ix) + + # Get the classification + y_hat[ix] = classify(art, sample, preprocessed=true, get_bmu=get_bmu) + end + + return y_hat +end # classify(art::ARTModule, x::RealMatrix ; preprocessed::Bool=false, get_bmu::Bool=false) diff --git a/test/test_ddvfa.jl b/test/test_ddvfa.jl deleted file mode 100644 index e3227c14..00000000 --- a/test/test_ddvfa.jl +++ /dev/null @@ -1,186 +0,0 @@ -""" - tt_ddvfa(opts::opts_DDVFA, train_x::Array) - -Trains and tests (tt) a DDVFA module on unlabeled data train_x. -""" -function tt_ddvfa(opts::opts_DDVFA, train_x::Array) - # Create the ART module, train, and classify - art = DDVFA(opts) - train!(art, train_x) - y_hat = classify(art, train_x) - # perf = performance(y_hat, data.test_y) - - # Total number of categories - total_vec = [art.F2[i].n_categories for i = 1:art.n_categories] - total_cat = sum(total_vec) - @info "Categories: $(art.n_categories)" - @info "Weights: $total_cat" - - return art -end # tt_ddvfa(opts::opts_DDVFA, train_x::Array) - -@testset "DDVFA Sequential" begin - @info "------- DDVFA Sequential -------" - - # Initialize the ART module - art = DDVFA() - # Turn off display for sequential training/testing - art.opts.display = false - # Set up the data manually because the module can't infer from single samples - data_setup!(art.config, data.train_x) - - # Get the dimension and size of the data - dim, n_samples = get_data_shape(data.train_x) - y_hat_train = zeros(Int64, n_samples) - dim_test, n_samples_test = get_data_shape(data.test_x) - y_hat = zeros(Int64, n_samples_test) - y_hat_bmu = zeros(Int64, n_samples_test) - - # Iterate over all examples sequentially - for i = 1:n_samples - y_hat_train[i] = train!(art, data.train_x[:, i], y=[data.train_y[i]]) - end - - # Iterate over all test samples sequentially - for i = 1:n_samples_test - y_hat[i] = classify(art, data.test_x[:, i]) - y_hat_bmu[i] = classify(art, data.test_x[:, i], get_bmu=true) - end - - # Calculate performance - perf_train = performance(y_hat_train, data.train_y) - perf_test = performance(y_hat, data.test_y) - perf_test_bmu = performance(y_hat_bmu, data.test_y) - - # Test the permance above a baseline number - perf_baseline = 0.8 - @test perf_train >= perf_baseline - @test perf_test >= perf_baseline - @test perf_test_bmu >= perf_baseline - - @info "DDVFA Training Perf: $perf_train" - @info "DDVFA Testing Perf: $perf_test" - @info "DDVFA Testing BMU Perf: $perf_test_bmu" -end - -@testset "DDVFA Supervised" begin - @info "------- DDVFA Supervised -------" - - # Train and classify - art = DDVFA() - y_hat_train = train!(art, data.train_x, y=data.train_y) - y_hat = classify(art, data.test_x) - y_hat_bmu = classify(art, data.test_x, get_bmu=true) - - # Calculate performance - perf_train = performance(y_hat_train, data.train_y) - perf_test = performance(y_hat, data.test_y) - perf_test_bmu = performance(y_hat_bmu, data.test_y) - - # Test the performances with a baseline number - perf_baseline = 0.8 - @test perf_train >= perf_baseline - @test perf_test >= perf_baseline - @test perf_test_bmu >= perf_baseline - - # Log the results - @info "DDVFA Training Perf: $perf_train" - @info "DDVFA Testing Perf: $perf_test" - @info "DDVFA Testing BMU Perf: $perf_test_bmu" -end - -@testset "DDVFA" begin - # Parse the data - data_file = "../data/art_data_rng.csv" - train_x = readdlm(data_file, ',') - train_x = permutedims(train_x) - - # Create the ART module, train, and classify - @info " ------- DDVFA Testing: Default Training -------" - default_opts = opts_DDVFA() - default_art = tt_ddvfa(default_opts, train_x) - @info "DDVFA Testing: Default Complete" - - # Create the ART module, train, and classify with no display - @info "------- DDVFA Testing: No Display Training -------" - no_disp_opts = opts_DDVFA() - no_disp_opts.display = false - no_disp_art = tt_ddvfa(no_disp_opts, train_x) - @info "DDVFA Testing: No Display Complete" - - # Test that the resulting weights are equivalent - @test default_art.W == no_disp_art.W -end # @testset "DDVFA" - -@testset "GNFA" begin - @info "------- GNFA Testing -------" - - # GNFA train and test - my_gnfa = GNFA() - local_complement_code = AdaptiveResonance.complement_code(data.train_x) - train!(my_gnfa, local_complement_code) - - # Similarity methods - methods = [ - "single", - "average", - "complete", - "median", - "weighted", - "centroid" - ] - - # Both field names - field_names = ["T", "M"] - - # Compute a local sample for GNFA similarity method testing - local_sample = local_complement_code[:, 1] - - # Compute the local activation and match - AdaptiveResonance.activation_match!(my_gnfa, local_sample) - - # Declare the true activation and match magnitudes - truth = Dict( - "single" => Dict( - "T" => 0.9988714513100155, - "M" => 2.6532834139109758 - ), - "average" => Dict( - "T" => 0.33761483787933894, - "M" => 1.1148764060015297 - ), - "complete" => Dict( - "T" => 0.018234409874338647, - "M" => 0.07293763949735459 - ), - "median" => Dict( - "T" => 0.2089217851518073, - "M" => 0.835687140607229 - ), - "weighted" => Dict( - "T" => 0.5374562506748786, - "M" => 1.4396083090159748 - ), - "centroid" => Dict( - "T" => 0.0, - "M" => 0.0 - ) - ) - - # Test every method and field name - for method in methods - results = Dict() - for field_name in field_names - results[field_name] = AdaptiveResonance.similarity(method, my_gnfa, field_name, local_sample, my_gnfa.opts.gamma_ref) - @test isapprox(truth[method][field_name], results[field_name]) - end - @info "Method: $method" results - end - - # Check the error handling of the similarity function - # Access the wrong similarity metric keyword ("asdf") - @test_throws ErrorException AdaptiveResonance.similarity("asdf", my_gnfa, "T", local_sample, my_gnfa.opts.gamma_ref) - # Access the wrong output function ("A") - @test_throws ErrorException AdaptiveResonance.similarity("centroid", my_gnfa, "A", local_sample, my_gnfa.opts.gamma_ref) - -end # @testset "GNFA" diff --git a/test/test_sets.jl b/test/test_sets.jl index 15061c82..61d2a0de 100644 --- a/test/test_sets.jl +++ b/test/test_sets.jl @@ -21,6 +21,7 @@ data = load_iris("../data/Iris.csv") dc1 = DataConfig() # Default constructor dc2 = DataConfig(0, 1, 2) # When min and max are same across all features dc3 = DataConfig([0, 1], [2, 3]) # When min and max differ across features + dc4 = DataConfig(three_by_two) # When a data matrix is provided # Test get_n_samples @test get_n_samples([1,2,3]) == 1 # 1-D array case @@ -54,17 +55,31 @@ end # @testset "AdaptiveResonance.jl" @testset "Train Test" begin # All ART modules arts = [ + FuzzyART, DVFA, - DDVFA + DDVFA, + SFAM, + DAM, ] n_arts = length(arts) # All common ART options art_opts = [ (display = true,), - (display = false,), + # (display = false,), ] - n_art_opts = length(art_opts) + + # Specific ART options + art_specifics = Dict( + DDVFA => [ + (gamma_normalization=true,), + (gamma_normalization=false,), + ], + FuzzyART => [ + (gamma_normalization=true,), + (gamma_normalization=false,), + ], + ) # All test option permutations test_opts = [ @@ -73,31 +88,41 @@ end # @testset "AdaptiveResonance.jl" ] n_test_opts = length(test_opts) - @info "--------- TRAIN TEST ---------" - # ART - perf_baseline = 0.8 + @info "-------------- BEGIN TRAIN TEST --------------" + # Performance baseline for all algorithms + perf_baseline = 0.7 # Iterate over all ART modules for ix = 1:n_arts # Iterate over all test options for jx = 1:n_test_opts - for kx = 1:n_art_opts - # Unsupervised - train_test_art(arts[ix](;art_opts[kx]...), data; test_opts=test_opts[jx]) - + # If we are testing a module with different options, merge + if haskey(art_specifics, arts[ix]) + local_art_opts = vcat(art_opts, art_specifics[arts[ix]]) + else + local_art_opts = art_opts + end + # Iterate over all options + for kx = 1:length(local_art_opts) + # Only do the unsupervised method if we have an ART module (not ARTMAP) + if arts[ix] isa ART + # Unsupervised + train_test_art(arts[ix](;local_art_opts[kx]...), data; test_opts=test_opts[jx]) + end # Supervised - @test train_test_art(arts[ix](;art_opts[kx]...), data; supervised=true, test_opts=test_opts[jx]) >= perf_baseline + @test train_test_art(arts[ix](;local_art_opts[kx]...), data; supervised=true, test_opts=test_opts[jx]) >= perf_baseline end end end - @info "--------- END TRAIN TEST ---------" -end + @info "-------------- END TRAIN TEST --------------" +end # @testset "Train Test" @testset "kwargs" begin @info "--------- KWARGS TEST ---------" arts = [ + FuzzyART, DVFA, DDVFA, SFAM, @@ -109,56 +134,84 @@ end end @info "--------- END KWARGS TEST ---------" -end - -@testset "DVFA.jl" begin - @info "------- DVFA Unsupervised -------" - - # Train and classify - art = DVFA() - y_hat_train = train!(art, data.train_x) - - @info "------- DVFA Supervised -------" - - # Train and classify - art = DVFA() - y_hat_train = train!(art, data.train_x, y=data.train_y) - y_hat = classify(art, data.test_x) - y_hat_bmu = classify(art, data.test_x, get_bmu=true) - - # Calculate performance - perf_train = performance(y_hat_train, data.train_y) - perf_test = performance(y_hat, data.test_y) - perf_test_bmu = performance(y_hat_bmu, data.test_y) - - # Test the performances are above a baseline - perf_baseline = 0.8 - @test perf_train >= perf_baseline - @test perf_test >= perf_baseline - @test perf_test_bmu >= perf_baseline - @info art.n_categories - - # Log the results - @info "DVFA Training Perf: $perf_train" - @info "DVFA Testing Perf: $perf_test" - @info "DVFA Testing BMU Perf: $perf_test_bmu" -end - -@testset "DDVFA.jl" begin - # DDVFA training and testing - include("test_ddvfa.jl") -end # @testset "DDVFA.jl" - -@testset "ARTMAP.jl" begin - # Declare the baseline performance for all modules - perf_baseline = 0.7 +end # @testset "kwargs" - # Iterate over each artmap module - for art in [SFAM, DAM] - perf = train_test_artmap(art(), data) - @test perf >= perf_baseline - end -end # @testset "ARTMAP.jl" +@testset "FuzzyART" begin + @info "------- FuzzyART Testing -------" + + # FuzzyART train and test + my_FuzzyART = FuzzyART() + # local_complement_code = AdaptiveResonance.complement_code(data.train_x) + # train!(my_FuzzyART, local_complement_code, preprocessed=true) + train!(my_FuzzyART, data.train_x) + + # Similarity methods + methods = [ + "single", + "average", + "complete", + "median", + "weighted", + "centroid" + ] + + # Both field names + field_names = ["T", "M"] + + # Compute a local sample for FuzzyART similarity method testing + # local_sample = local_complement_code[:, 1] + # local_complement_code = AdaptiveResonance.complement_code(data.train_x) + # local_sample = data.train_x[:, 1] + local_sample = AdaptiveResonance.complement_code(data.train_x[:, 1], config=my_FuzzyART.config) + + # Compute the local activation and match + # AdaptiveResonance.activation_match!(my_FuzzyART, local_sample) + + # # Declare the true activation and match magnitudes + # truth = Dict( + # "single" => Dict( + # "T" => 0.9988714513100155, + # "M" => 2.6532834139109758 + # ), + # "average" => Dict( + # "T" => 0.33761483787933894, + # "M" => 1.1148764060015297 + # ), + # "complete" => Dict( + # "T" => 0.018234409874338647, + # "M" => 0.07293763949735459 + # ), + # "median" => Dict( + # "T" => 0.2089217851518073, + # "M" => 0.835687140607229 + # ), + # "weighted" => Dict( + # "T" => 0.5374562506748786, + # "M" => 1.4396083090159748 + # ), + # "centroid" => Dict( + # "T" => 0.0, + # "M" => 0.0 + # ) + # ) + + # # Test every method and field name + # for method in methods + # results = Dict() + # for field_name in field_names + # results[field_name] = AdaptiveResonance.similarity(method, my_FuzzyART, field_name, local_sample, my_FuzzyART.opts.gamma_ref) + # @test isapprox(truth[method][field_name], results[field_name]) + # end + # @info "Method: $method" results + # end + + # Check the error handling of the similarity function + # Access the wrong similarity metric keyword ("asdf") + @test_throws ErrorException AdaptiveResonance.similarity("asdf", my_FuzzyART, "T", local_sample, my_FuzzyART.opts.gamma_ref) + # Access the wrong output function ("A") + @test_throws ErrorException AdaptiveResonance.similarity("centroid", my_FuzzyART, "A", local_sample, my_FuzzyART.opts.gamma_ref) + +end # @testset "FuzzyART" @testset "ARTSCENE.jl" begin # ARTSCENE training and testing diff --git a/test/test_utils.jl b/test/test_utils.jl index ad03958f..c013adee 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -30,23 +30,6 @@ function DataSplit(data_x::Array, data_y::Array, ratio::Real) return DataSplit(train_x, test_x, train_y, test_y) end # DataSplit(data_x::Array, data_y::Array, ratio::Real) -""" - train_test_artmap(art::ARTMAP, data::DataSplit) - -Train and test an ARTMAP module in a supervised manner on the dataset. -""" -function train_test_artmap(art::ARTMAP, data::DataSplit) - # Train and classify - train!(art, data.train_x, data.train_y) - y_hat = classify(art, data.test_x) - - # Calculate performance - perf = performance(y_hat, data.test_y) - @info "Performance is $perf" - - return perf -end # train_test_artmap(art::ARTMAP, data::DataSplit) - """ train_test_art(art::ARTModule, data::DataSplit; supervised::Bool=false, art_opts...)