Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spatial anomaly #587

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions bindings/py/cpp_src/bindings/algorithms/py_SpatialPooler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ Argument wrapAround boolean value that determines whether or not inputs
py_SpatialPooler.def("getMinPctOverlapDutyCycles", &SpatialPooler::getMinPctOverlapDutyCycles);
py_SpatialPooler.def("setMinPctOverlapDutyCycles", &SpatialPooler::setMinPctOverlapDutyCycles);

py_SpatialPooler.def("anomaly", [](const SpatialPooler& self) {
return self.anomaly;
});
py_SpatialPooler.def("anomalyThreshold", [](const SpatialPooler& self) {
return &self.spAnomaly.SPATIAL_TOLERANCE;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the py bindings. Will this work so that I'll be able to set through the variable, or is it just a getter?

});


// loadFromString
py_SpatialPooler.def("loadFromString", [](SpatialPooler& self, const py::bytes& inString)
{
Expand All @@ -245,8 +253,8 @@ Argument wrapAround boolean value that determines whether or not inputs
});

// compute
py_SpatialPooler.def("compute", [](SpatialPooler& self, SDR& input, bool learn, SDR& output)
{ self.compute( input, learn, output ); },
py_SpatialPooler.def("compute", [](SpatialPooler& self, const SDR& input, const bool learn, SDR& output, const Real spatialAnomalyInputValue)
{ self.compute( input, learn, output, spatialAnomalyInputValue ); },
R"(
This is the main workhorse method of the SpatialPooler class. This method
takes an input SDR and computes the set of output active columns. If 'learn' is
Expand All @@ -269,10 +277,15 @@ Argument learn A boolean value indicating whether learning should be
Argument output An SDR representing the winning columns after
inhibition. The size of the SDR is equal to the number of
columns (also returned by the method getNumColumns).

Argument spatialAnomalyInputValue (optional) A `Real` value of the original input
that was given to encoder. Used only for spatial anomaly computation.
See @ref `SP.anomaly`
)",
py::arg("input"),
py::arg("learn") = true,
py::arg("output")
py::arg("output"),
py::arg("spatialAnomalyInputValue") = std::numeric_limits<Real>::min()
);

// setBoostFactors
Expand Down
14 changes: 9 additions & 5 deletions src/examples/hotgym/HelloSPTP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@ Real64 BenchmarkHotgym::run(UInt EPOCHS, bool useSPlocal, bool useSPglobal, bool
//Encode
tEnc.start();
x+=0.01f; //step size for fn(x)
enc.encode(sin(x), input); //model sin(x) function //TODO replace with CSV data
// cout << x << "\n" << sin(x) << "\n" << input << "\n\n";
const Real value = sin(x);
enc.encode(value, input); //model sin(x) function //TODO replace with CSV data
// cout << x << "\n" << value << "\n" << input << "\n\n";
tEnc.stop();

tRng.start();
Expand All @@ -117,13 +118,13 @@ Real64 BenchmarkHotgym::run(UInt EPOCHS, bool useSPlocal, bool useSPglobal, bool
//SP (global x local)
if(useSPlocal) {
tSPloc.start();
spLocal.compute(input, true, outSPlocal);
spLocal.compute(input, true, outSPlocal, value /* optional for spatial anomaly*/);
tSPloc.stop();
}

if(useSPglobal) {
tSPglob.start();
spGlobal.compute(input, true, outSPglobal);
spGlobal.compute(input, true, outSPglobal, value /* optional for spatial anomaly */);
tSPglob.stop();
}
outSP = outSPglobal; //toggle if local/global SP is used further down the chain (TM, Anomaly)
Expand Down Expand Up @@ -168,6 +169,7 @@ Real64 BenchmarkHotgym::run(UInt EPOCHS, bool useSPlocal, bool useSPglobal, bool
// output values
cout << "Epoch = " << e << endl;
cout << "Anomaly = " << an << endl;
cout << "Anomaly (spatial) = " << spGlobal.anomaly << endl;
cout << "Anomaly (avg) = " << avgAnom10.getCurrentAvg() << endl;
cout << "Anomaly (Likelihood) = " << anLikely << endl;
cout << "SP (g)= " << outSP << endl;
Expand Down Expand Up @@ -217,10 +219,12 @@ Real64 BenchmarkHotgym::run(UInt EPOCHS, bool useSPlocal, bool useSPglobal, bool
if(useSPglobal) { NTA_CHECK(outSPglobal == goldSP) << "Deterministic output of SP (g) failed!\n" << outSP << "should be:\n" << goldSP; }
if(useSPlocal) { NTA_CHECK(outSPlocal == goldSPlocal) << "Deterministic output of SP (l) failed!\n" << outSPlocal << "should be:\n" << goldSPlocal; }
if(useTM) { NTA_CHECK(outTM == goldTM) << "Deterministic output of TM failed!\n" << outTM << "should be:\n" << goldTM; }
NTA_CHECK(static_cast<UInt>(an *10000.0f) == static_cast<UInt>(goldAn *10000.0f)) //compare to 4 decimal places
// anomalies
NTA_CHECK(static_cast<UInt>(an *10000.0f) == static_cast<UInt>(goldAn *10000.0f)) //compare to 4 decimal places
<< "Deterministic output of Anomaly failed! " << an << "should be: " << goldAn;
NTA_CHECK(static_cast<UInt>(avgAnom10.getCurrentAvg() * 10000.0f) == static_cast<UInt>(goldAnAvg * 10000.0f))
<< "Deterministic average anom score failed:" << avgAnom10.getCurrentAvg() << " should be: " << goldAnAvg;
if(useSPglobal) { NTA_CHECK(0.0f == spGlobal.anomaly) << "Deterministic spatial anomaly mismatch!" << spGlobal.anomaly; }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hotgym checks the new spatial anomaly

}

// check runtime speed
Expand Down
15 changes: 14 additions & 1 deletion src/htm/algorithms/SpatialPooler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,11 @@ void SpatialPooler::initialize(
}


void SpatialPooler::compute(const SDR &input, const bool learn, SDR &active) {
void SpatialPooler::compute(const SDR &input,
const bool learn,
SDR &active,
const Real spatialAnomalyInputValue) {

input.reshape( inputDimensions_ );
active.reshape( columnDimensions_ );
updateBookeepingVars_(learn);
Expand All @@ -485,6 +489,11 @@ void SpatialPooler::compute(const SDR &input, const bool learn, SDR &active) {
updateMinDutyCycles_();
}
}

//update spatial anomaly
if(this->spAnomaly.enabled) {
spAnomaly.compute(spatialAnomalyInputValue);
}
}


Expand Down Expand Up @@ -1018,6 +1027,10 @@ bool SpatialPooler::operator==(const SpatialPooler& o) const{

//Random
if (rng_ != o.rng_) return false;

//spatial anomaly
if (spAnomaly != o.spAnomaly) return false;

return true;

}
Expand Down
104 changes: 101 additions & 3 deletions src/htm/algorithms/SpatialPooler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,15 @@ class SpatialPooler : public Serializable
@param active An SDR representing the winning columns after
inhibition. The size of the SDR is equal to the number of
columns (also returned by the method getNumColumns).

@param spatialAnomalyInputValue (optional) `Real` used for computing "spatial anomaly", see @ref `this.spatial_anomaly.compute()`,
obtained by @ref `SP.anomaly`

*/
virtual void compute(const SDR &input, const bool learn, SDR &active);
virtual void compute(const SDR &input,
const bool learn,
SDR &active,
const Real spatialAnomalyInputValue = std::numeric_limits<Real>::min());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to extend SP.compute() with the extra, optional field. It works ok, but I don't like it too much, maybe better is the proposal with "SP carrying orig input value"?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the anomaly code could be built into the encoder, since the encoder already has access to the original input value?

Alternatively, this piece of code (spatial anomaly detection) could live in its own class?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anomaly code could be built into the encoder, since the encoder already has access to the original input value?

makes sense! This is really more a function of an encoder (input). As I was saying, I don't really like the current implementation, but it is what's used in NAB.

spatial anomaly detection) could live in its own class?

later I'm going to implement SP's anomaly detection based on synapses of SP, so

  • a part of this PR was preparation of the "infrastructure" for the SP.anomaly, later I'd just change the actual method
  • you're right this belongs more to the BaseEncoder than to SP

Which way would you suggest, I don't really have a preference now



/**
Expand Down Expand Up @@ -276,7 +283,11 @@ class SpatialPooler : public Serializable
CEREAL_NVP(synPermBelowStimulusInc_),
CEREAL_NVP(synPermConnected_),
CEREAL_NVP(minPctOverlapDutyCycles_),
CEREAL_NVP(wrapAround_));
CEREAL_NVP(wrapAround_),
CEREAL_NVP(spAnomaly.minVal_),
CEREAL_NVP(spAnomaly.maxVal_),
CEREAL_NVP(spAnomaly.anomalyScore_)
);
ar(CEREAL_NVP(boostFactors_));
ar(CEREAL_NVP(overlapDutyCycles_));
ar(CEREAL_NVP(activeDutyCycles_));
Expand Down Expand Up @@ -309,7 +320,11 @@ class SpatialPooler : public Serializable
CEREAL_NVP(synPermBelowStimulusInc_),
CEREAL_NVP(synPermConnected_),
CEREAL_NVP(minPctOverlapDutyCycles_),
CEREAL_NVP(wrapAround_));
CEREAL_NVP(wrapAround_),
CEREAL_NVP(spAnomaly.minVal_),
CEREAL_NVP(spAnomaly.maxVal_),
CEREAL_NVP(spAnomaly.anomalyScore_)
);
ar(CEREAL_NVP(boostFactors_));
ar(CEREAL_NVP(overlapDutyCycles_));
ar(CEREAL_NVP(activeDutyCycles_));
Expand Down Expand Up @@ -1209,6 +1224,89 @@ class SpatialPooler : public Serializable
Random rng_;

public:
/**
* holds together functionality for computing
* `spatial anomaly`. This is used in NAB.
*/
struct spatial_anomaly {

/** Fraction outside of the range of values seen so far that will be considered
* a spatial anomaly regardless of the anomaly likelihood calculation.
* This accounts for the human labelling bias for spatial values larger than what
* has been seen so far.
* Default value 0.05f aka 5%, as used in NAB.
*/
Real SPATIAL_TOLERANCE = 0.05;

/**
* toggle whether we should compute spatial anomaly
* Default true.
*/
bool enabled = true;

/**
* compute if the current input `value` is considered spatial-anomaly.
* update internal variables.
*
* @param value Real, input #TODO currently handles only 1 variable input, and requires value passed to compute!
* # later remove, and implement using SP's internal state. But for now we are compatible with NAB's implementation.
*
* @return nothing, but updates internal variable `anomalyScore_`, which is either `NO_ANOMALY` (0.0f) ,
* or `SPATIAL_ANOMALY` (exactly 0.9995947141f). Accessed by public @ref `SP.anomaly`.
*
*/
void compute(const Real value) {
anomalyScore_ = NO_ANOMALY;
if(not enabled) return;
NTA_CHECK(SPATIAL_TOLERANCE >= 0.0f and SPATIAL_TOLERANCE <= 1.0f);

if(minVal_ != maxVal_) {
const Real tolerance = (maxVal_ - minVal_) * SPATIAL_TOLERANCE;
const Real maxExpected = maxVal_ + tolerance;
const Real minExpected = minVal_ - tolerance;

if(value > maxExpected or value < minExpected) { //spatial anomaly
anomalyScore_ = SPATIAL_ANOMALY;
}
}
if(value > maxVal_ or maxVal_ == INF_) maxVal_ = value;
if(value < minVal_ or minVal_ == INF_) minVal_ = value;
NTA_ASSERT(anomalyScore_ == NO_ANOMALY or anomalyScore_ == SPATIAL_ANOMALY) << "Out of bounds of acceptable values";
}

bool operator==(const spatial_anomaly& o) const noexcept {
return minVal_ == o.minVal_ and
maxVal_ == o.maxVal_ and
anomalyScore_ == o.anomalyScore_ and
enabled == o.enabled and
SPATIAL_TOLERANCE == o.SPATIAL_TOLERANCE;
}

inline bool operator!=(const spatial_anomaly& o) const noexcept {
return !this->operator==(o);
}

private:
friend class SpatialPooler;
static const constexpr Real INF_ = std::numeric_limits<Real>::infinity();
Real minVal_ = INF_;
Real maxVal_ = INF_;
Real anomalyScore_ = NO_ANOMALY; //default score = no anomaly

public:
static const constexpr Real NO_ANOMALY = 0.0f;
static const constexpr Real SPATIAL_ANOMALY = 0.9995947141f; //almost 1.0 = max anomaly. Encodes value specific to spatial anomaly (so this can be recognized on results),
// "5947141" would translate in l33t speech to "spatial" :)
} spAnomaly;

/**
* spatial anomaly
*
* updated on each @ref `compute()`.
*
* @return either 0.0f (no anomaly), or exactly 0.9995947141f (spatial anomaly). This specific value can be recognized in results.
*/
const Real& anomaly = spAnomaly.anomalyScore_;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spAnomaly implementation,

const Connections &connections = connections_;
};

Expand Down
66 changes: 66 additions & 0 deletions src/test/unit/algorithms/SpatialPoolerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <htm/types/Types.hpp>
#include <htm/utils/Log.hpp>
#include <htm/os/Timer.hpp>
#include <htm/encoders/ScalarEncoder.hpp>

namespace testing {

Expand Down Expand Up @@ -2103,5 +2104,70 @@ TEST(SpatialPoolerTest, ExactOutput) {
ASSERT_EQ( columns, gold_sdr );
}

TEST(SpatialPoolerTest, spatialAnomaly) {
SDR inputs({ 1000 });
SDR columns({ 200 });

{
SpatialPooler sp(inputs.dimensions, columns.dimensions);
// test too large threshold
sp.spAnomaly.SPATIAL_TOLERANCE = 1.2345f; //out of bounds, will crash!
EXPECT_ANY_THROW(sp.compute(inputs, false, columns, 0.1f /*whatever, fails on TOLERANCE */)) << "Spatial anomaly should fail if SPATIAL_TOLERANCE is out of bounds!";
sp.spAnomaly.SPATIAL_TOLERANCE = 0.01f; //within bounds, OK
EXPECT_NO_THROW(sp.compute(inputs, false, columns, 0.1f /*whatever */));
}

{
SpatialPooler sp({inputs.dimensions}, {columns.dimensions},
/*potentialRadius*/ 99999,
/*potentialPct*/ 0.5f,
/*globalInhibition*/ true,
/*localAreaDensity*/ 0.05f,
/*stimulusThreshold*/ 3u,
/*synPermInactiveDec*/ 0.008f,
/*synPermActiveInc*/ 0.05f,
/*synPermConnected*/ 0.1f,
/*minPctOverlapDutyCycles*/ 0.001f,
/*dutyCyclePeriod*/ 200,
/*boostStrength*/ 10.0f,
/*seed*/ 42,
/*spVerbosity*/ 0,
/*wrapAround*/ true);

//test spatial anomaly computation
sp.spAnomaly.SPATIAL_TOLERANCE = 0.2f; //threshold 20%
ScalarEncoderParameters params;
params.minimum = 0.0f;
params.maximum = 100.0f;
params.size = 1000;
params.sparsity = 0.3f;
ScalarEncoder enc(params);

Real val;

val = 0.0f;
enc.encode(val, inputs); //TODO can SDR hold .origValue = Real which encoders would set? Classifier,Predictor, and spatia_anomaly would use that
sp.compute(inputs, true, columns, val);
EXPECT_EQ(0.0f, sp.anomaly);
EXPECT_EQ(sp.spAnomaly.NO_ANOMALY, sp.anomaly) << "should be the same as above";

val = 10.0f;
enc.encode(val, inputs);
sp.compute(inputs, true, columns, val);
EXPECT_EQ(sp.spAnomaly.NO_ANOMALY, sp.anomaly);

val = 11.99f; //(10-0) * 0.2 == 2 -> <-2, +12> is not anomalous
enc.encode(val, inputs);
sp.compute(inputs, true, columns, val);
EXPECT_EQ(0.0f, sp.anomaly) << "This shouldn't be an anomaly!";

val = 100.0f; //(12-0) * 0.2 == ~2.2 -> <-2.2, +14.2> is not anomalous, but 100 is!
enc.encode(val, inputs);
sp.compute(inputs, true, columns, val);
EXPECT_EQ(0.9995947141f, sp.anomaly) << "This should be an anomaly!";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testing SP.anomaly correctness

EXPECT_EQ(sp.spAnomaly.SPATIAL_ANOMALY, sp.anomaly) << "Should be same as above!";
}

}

} // end anonymous namespace