diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 0000000..cb5e8ac --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,245 @@ +a, +a:hover, +a:visited { + color: #ee4c2c; +} + +a:hover { + text-decoration: underline; +} + +.wy-nav-content { + max-width: 960px; +} + +.wy-menu-vertical header, +.wy-menu-vertical p.caption { + color: #262626; +} + + +.wy-nav-side { + background: #f3f4f7; +} + +.wy-menu-vertical a, +.wy-menu-vertical a:active, +.wy-menu-vertical a:hover { + color: #6c6c6d; +} + +.wy-menu-vertical a:hover { + background-color: #e8e9ea; + cursor: pointer; +} + +.wy-menu-vertical a:active { + background-color: #e8e9ea; +} + +.wy-side-nav-search input[type=text] { + border-color: #c9c9c9; +} + +.rst-content .viewcode-back, +.rst-content .viewcode-link { + color: #4974D1; +} + +html.writer-html4 .rst-content dl:not(.docutils)>dt, +html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt { + background: #f3f4f7; + color: #6c6c6d; + border-top: none; + border-left: 3px solid #ee4c2c; + padding: 0.5rem; + padding-right: 100px; + word-wrap: break-word; +} + +.rst-content table.docutils, +.wy-table-bordered-all { + border: none; +} + +.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td, +.wy-table-backed, +.wy-table-odd td, +.wy-table-striped tr:nth-child(2n-1) td { + background-color: #f3f4f7; +} + +.rst-content table.docutils td, +.wy-table-bordered-all td { + border: none; +} + +.rst-content table.docutils td, +.rst-content table.docutils th, +.rst-content table.field-list td, +.rst-content table.field-list th, +.wy-table td, +.wy-table th { + padding: 12px 16px; +} + +.rst-content code, +.rst-content tt, +code { + border: none; + color: #ee4c2c; +} + +.rst-content code.xref, +.rst-content tt.xref, +a .rst-content code, +a .rst-content tt { + color: inherit; +} + +.wy-menu-vertical li.toctree-l1.current>a { + border: none; +} + +.wy-menu-vertical li.current a { + border: none; +} + +.wy-menu-vertical li.current { + background: #e1e2e4; +} + +.wy-menu-vertical li.current>a, +.wy-menu-vertical li.on a, +.wy-menu-vertical li.current>a:hover, +.wy-menu-vertical li.on a:hover { + background: none; +} + + +.highlight { + background: #f3f4f7; +} + +.rst-content div[class^=highlight], +.rst-content pre.literal-block { + border: none; +} + +html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt, +html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt { + padding-left: 8px; + border: none; + border-left: 3px solid #ee4c2c; + background: #f3f4f7; + color: #555; +} + +.rst-content .note, +.rst-content .seealso, +.rst-content .wy-alert-info.admonition, +.rst-content .wy-alert-info.admonition-todo, +.rst-content .wy-alert-info.attention, +.rst-content .wy-alert-info.caution, +.rst-content .wy-alert-info.danger, +.rst-content .wy-alert-info.error, +.rst-content .wy-alert-info.hint, +.rst-content .wy-alert-info.important, +.rst-content .wy-alert-info.tip, +.rst-content .wy-alert-info.warning, +.wy-alert.wy-alert-info { + background: #f3f4f7; +} + +.rst-content .note .admonition-title, +.rst-content .note .wy-alert-title, +.rst-content .seealso .admonition-title, +.rst-content .seealso .wy-alert-title, +.rst-content .wy-alert-info.admonition-todo .admonition-title, +.rst-content .wy-alert-info.admonition-todo .wy-alert-title, +.rst-content .wy-alert-info.admonition .admonition-title, +.rst-content .wy-alert-info.admonition .wy-alert-title, +.rst-content .wy-alert-info.attention .admonition-title, +.rst-content .wy-alert-info.attention .wy-alert-title, +.rst-content .wy-alert-info.caution .admonition-title, +.rst-content .wy-alert-info.caution .wy-alert-title, +.rst-content .wy-alert-info.danger .admonition-title, +.rst-content .wy-alert-info.danger .wy-alert-title, +.rst-content .wy-alert-info.error .admonition-title, +.rst-content .wy-alert-info.error .wy-alert-title, +.rst-content .wy-alert-info.hint .admonition-title, +.rst-content .wy-alert-info.hint .wy-alert-title, +.rst-content .wy-alert-info.important .admonition-title, +.rst-content .wy-alert-info.important .wy-alert-title, +.rst-content .wy-alert-info.tip .admonition-title, +.rst-content .wy-alert-info.tip .wy-alert-title, +.rst-content .wy-alert-info.warning .admonition-title, +.rst-content .wy-alert-info.warning .wy-alert-title, +.rst-content .wy-alert.wy-alert-info .admonition-title, +.wy-alert.wy-alert-info .rst-content .admonition-title, +.wy-alert.wy-alert-info .wy-alert-title { + background: #54c7ec; +} + +.rst-content .admonition-todo, +.rst-content .attention, +.rst-content .caution, +.rst-content .warning, +.rst-content .wy-alert-warning.admonition, +.rst-content .wy-alert-warning.danger, +.rst-content .wy-alert-warning.error, +.rst-content .wy-alert-warning.hint, +.rst-content .wy-alert-warning.important, +.rst-content .wy-alert-warning.note, +.rst-content .wy-alert-warning.seealso, +.rst-content .wy-alert-warning.tip, +.wy-alert.wy-alert-warning { + background: #f3f4f7; +} + +.rst-content .admonition-todo .admonition-title, +.rst-content .admonition-todo .wy-alert-title, +.rst-content .attention .admonition-title, +.rst-content .attention .wy-alert-title, +.rst-content .caution .admonition-title, +.rst-content .caution .wy-alert-title, +.rst-content .warning .admonition-title, +.rst-content .warning .wy-alert-title, +.rst-content .wy-alert-warning.admonition .admonition-title, +.rst-content .wy-alert-warning.admonition .wy-alert-title, +.rst-content .wy-alert-warning.danger .admonition-title, +.rst-content .wy-alert-warning.danger .wy-alert-title, +.rst-content .wy-alert-warning.error .admonition-title, +.rst-content .wy-alert-warning.error .wy-alert-title, +.rst-content .wy-alert-warning.hint .admonition-title, +.rst-content .wy-alert-warning.hint .wy-alert-title, +.rst-content .wy-alert-warning.important .admonition-title, +.rst-content .wy-alert-warning.important .wy-alert-title, +.rst-content .wy-alert-warning.note .admonition-title, +.rst-content .wy-alert-warning.note .wy-alert-title, +.rst-content .wy-alert-warning.seealso .admonition-title, +.rst-content .wy-alert-warning.seealso .wy-alert-title, +.rst-content .wy-alert-warning.tip .admonition-title, +.rst-content .wy-alert-warning.tip .wy-alert-title, +.rst-content .wy-alert.wy-alert-warning .admonition-title, +.wy-alert.wy-alert-warning .rst-content .admonition-title, +.wy-alert.wy-alert-warning .wy-alert-title { + background: #e94f3b; +} + + +html.writer-html5 .rst-content table.docutils th { + border: none; +} + +html, +body, +.wy-body-for-nav, +.wy-nav-content { + background: #ffffff !important; +} + + +.wy-nav-content-wrap { + background: none; +} \ No newline at end of file diff --git a/docs/_templates/class.rst b/docs/_templates/class.rst new file mode 100644 index 0000000..61375f4 --- /dev/null +++ b/docs/_templates/class.rst @@ -0,0 +1,11 @@ +.. role:: hidden + :class: hidden-section +.. currentmodule:: {{ module }} + + +{{ name | underline}} + +.. autoclass:: {{ name }} + :members: + :special-members: + :exclude-members: __weakref__, __init__ \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 8651e78..53a2148 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,59 +1,71 @@ # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath('../')) # -- Project information ----------------------------------------------------- project = 'HDTorch' copyright = '2022, William Simon, Una Pale' author = 'William Simon, Una Pale' # The full version, including alpha/beta/rc tags release = '1.0.0' # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.githubpages", "sphinx.ext.napoleon", "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.viewcode", "sphinx_rtd_theme",] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -# -html_theme = 'alabaster' + +html_theme = "sphinx_rtd_theme" +html_theme_path = ["_themes"] + +html_theme_options = { + "logo_only": True, + "display_version": True, + "prev_next_buttons_location": "bottom", + "style_nav_header_background": "white", +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] \ No newline at end of file +html_static_path = ["_static"] + +html_css_files = [ + "css/custom.css", +] \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 3d3964b..7a15873 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,13 +1,13 @@ -Welcome to the Torchhd documentation! +Welcome to the HDTorch documentation! ===================================== -Torchhd is a Python library dedicated to *Hyperdimensional Computing* (also knwon as *Vector Symbolic Architectures*). + .. toctree:: :glob: :maxdepth: 1 :caption: Tutorials getting_started util model \ No newline at end of file diff --git a/docs/model.rst b/docs/model.rst index fd10da6..4d4a4a7 100644 --- a/docs/model.rst +++ b/docs/model.rst @@ -1,17 +1,15 @@ .. _model: -HDTorch model class -====================== +HDTorch Base Classifier +======================= .. currentmodule:: hdtorch.model -This module consists of the basic hypervector generation functions and operations used on hypervectors. +The HD_classifier class is the basis on which other HD classifiers can be built, and serves as a vehicle for demonstrating HDTorch's functionality. -Basis-hypervector sets ----------------------------------- .. autosummary:: :toctree: generated/ :template: class.rst HD_classifier \ No newline at end of file diff --git a/hdtorch/model.py b/hdtorch/model.py index f10b7a3..4f98a7d 100644 --- a/hdtorch/model.py +++ b/hdtorch/model.py @@ -1,126 +1,182 @@ import torch import math from . import _hdtorchcuda from .hyperdimGenerators import generateHDVector from .util import rotateVec, xor_bipolar +__all__ = [ + "HD_classifier", +] class HD_classifier: """Base HD classifier with generic parameters and learning and prediction funtions + Creates a simple HD classifier model with learning and prediction functions + + Args: + HDParams: an HDParams object that initializes the classifier """ def __init__(self, HDParams): self.numFeat = HDParams.numFeat self.D = HDParams.D self.packed = HDParams.packed self.device = HDParams.device self.bindingStrat = HDParams.bindingStrat self.HDFlavor = HDParams.HDFlavor if self.HDFlavor == 'bipol' and self.packed: raise TypeError('Model cannot be bipol and packed simultanously') if self.bindingStrat == 'FeatAppend': self.bindingFunction = lambda d : self.featureLevelValues[d].view(d.shape[0],1,-1) elif self.bindingStrat == 'FeatPermute': self.bindingFunction = lambda d : rotateVec(self.featureIDs, d) elif self.bindingStrat == 'IDLevelEncoding' and self.HDFlavor == 'binary': self.bindingFunction = lambda d : torch.bitwise_xor(self.featureIDs.unsqueeze(0), self.featureLevelValues[d]) elif self.bindingStrat == 'IDLevelEncoding' and self.HDFlavor == 'bipol' and not self.packed: self.bindingFunction = lambda d : xor_bipolar(self.featureIDs.unsqueeze(0), self.featureLevelValues[d]) else: raise TypeError(f'Unrecognized combination of bindingStrat: {self.bindingStrat}, HDFlavor: {self.HDFlavor}, packed: {self.packed}') self.modelVectors = torch.zeros((HDParams.numClasses, self.D), device=self.device) if self.packed: self.modelVectorsNorm= torch.zeros((HDParams.numClasses, math.ceil(self.D/32)), device = self.device, dtype=torch.int32) else: self.modelVectorsNorm= torch.zeros((HDParams.numClasses, self.D), device = self.device, dtype=torch.int8) self.numAddedVecPerClass = torch.zeros(HDParams.numClasses, device=self.device, dtype=torch.int32) self.distanceFunction = self.ham_dist_arr if HDParams.similarityType=='hamming' else self.cos_dist_arr self.featureIDs = generateHDVector(HDParams.IDVecType,self.numFeat,HDParams.D,self.packed,self.device) self.featureIDs[self.featureIDs==0] = -1 if self.HDFlavor=='bipol' else 0 self.featureLevelValues = generateHDVector(HDParams.levelVecType,HDParams.numSegmentationLevels,HDParams.D,self.packed,self.device) self.featureLevelValues[self.featureLevelValues==0] = -1 if self.HDFlavor=='bipol' else 0 def learn_HD_projections(self, data): - ''' From features of one window to HDvector representing that data window - ''' + """ Project training data into HD space via selected binding function + + learn_HD_projections is expecting data to be discretized into a range of values equal to the number of values present in + in the featureLevelValues array. The input data will be used to index featureLevelValues, with the return value being + bound to the featureID corresponding to the feature. + + Args: + data: 2D array of discretized training + + Returns: + The projected and bound features in uncompressed binary form, with each bit having type int8, and, if the user specified + packed mode when initializing the HD_classifier, the packed projections. + """ outputVector = torch.empty((data.shape[0],self.D),dtype = torch.int8, device = data.device) if self.packed: b = self.bindingFunction(data) a = _hdtorchcuda.vcount(self.bindingFunction(data),self.D) else: a = self.bindingFunction(data).sum(1,dtype=torch.int16) if self.bindingStrat != 'FeatAppend': f = self.numFeat>>1 if self.HDFlavor=='binary' else 0 torch.gt(a,f,out=outputVector) else: outputVector = a.to(torch.int8) if self.HDFlavor == 'bipol': outputVector[outputVector==0] = -1 if self.packed: packedOutput = torch.full((data.shape[0],math.ceil(self.D/32)),-1,dtype = torch.int32, device = data.device) _hdtorchcuda.pack(outputVector,packedOutput) else: packedOutput = -1 return outputVector, packedOutput def ham_dist_arr(self, vec_a, vec_b): - ''' calculate relative hamming distance for for np array''' + """Calculate relative hamming distance between two hypervectors + + ham_dist_arr will perform a bitwise xor if the two hypervectors are in binary form, else it will compare their values and + return 1 if the values are different. + + Args: + vec_a: First hypervectors to be compared. + vec_b: Second hypervectors to be compared. + Returns: + The count of bits in the hypervectors that are different divided by the hypervectors size. + """ vec_c = torch.bitwise_xor(vec_a,vec_b) if self.HDFlavor == 'binary' else vec_a != vec_b return _hdtorchcuda.hcount(vec_c)/float(self.D) if self.packed else vec_c.sum(2,dtype=torch.int16)/float(self.D) def cos_dist_arr(self, vA, vB): - ''' calculate cosine distance of two vectors''' + """Calculate cosine distance distance between two hypervectors + + cos_dist_arr currently only works with unpacked hypervectors. + + Args: + vec_a: First hypervectors to be compared. + vec_b: Second hypervectors to be compared. + Returns: + The cosine distance between the input hypervectors. + """ output = np.dot(vA, vB) / (np.sqrt(np.dot(vA,vA)) * np.sqrt(np.dot(vB,vB))) outTensor = torch.tensor(1.0-output) #because we use later that if value is 0 then vectors are the same return outTensor - def givePrediction(self,data, encoded=False): - if not encoded: + def givePrediction(self, data, projected=False): + """Predict class to which input data belongs + + If the input data has not been projected into HD space, project data. Then, compare data with class vectors + via the selected distance function and return the closest class. + + Args: + data: Array of data for which to predict class. My be already projected into HD space or not. + projected: Boolean value indicating if data has been already projected into HD space. + Returns: + A vector of predicted classes, and the distance from each classes for each projected data point. + """ + if not projected: (temp, tempPacked) = self.learn_HD_projections(data) temp = temp if not self.packed else tempPacked data = temp.view(-1,1,temp.shape[-1]) else: data = data.view(-1,1,data.shape[-1]) distances = self.distanceFunction(data, self.modelVectorsNorm.unsqueeze(0)) #find minimum minVal = torch.argmin(distances,-1) return (minVal.squeeze(), distances.squeeze()) def trainModelVecOnData(self, data, labels): - '''learn model vectors for single pass 2 class HD learning ''' + """Learn HD class vectors for from training data and labels. + + trainModelVecOnData will project each data point into HD space, bind it with the feature hypervector, + then bundle all bound hypervectors of the same class into a class vector. + + Args: + data: Array of data from which to learn class vectors + labels: Array of labels from which to learn class vectors + """ # number of clases numLabels = self.modelVectors.shape[0] # Encode data (temp, packedTemp) = self.learn_HD_projections(data) temp = temp if not self.packed else packedTemp #go through all data windows and add them to model vectors for l in range(numLabels): t = temp[labels==l,:] if(t.numel() != 0 and self.packed): self.modelVectors[l, :] += _hdtorchcuda.vcount(temp[labels==l],self.D) #temp[labels==l].sum(0) elif not self.packed: self.modelVectors[l] += temp[labels==l].sum(0) self.numAddedVecPerClass[l] += (labels==l).sum().cpu() #count number of vectors added to each subclass # normalize model vectors to be binary (or bipolar) again if self.HDFlavor == 'binary' and self.packed: _hdtorchcuda.pack(self.modelVectors > (self.numAddedVecPerClass.unsqueeze(1)>>1),self.modelVectorsNorm) elif self.HDFlavor == 'binary' and not self.packed: self.modelVectorsNorm = self.modelVectors > (self.numAddedVecPerClass.unsqueeze(1)>>1) elif self.HDFlavor == 'bipol': self.modelVectorsNorm = torch.where(self.modelVectors>0,1,-1)