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 2 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
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
11 changes: 10 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
77 changes: 76 additions & 1 deletion 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 @@ -1209,6 +1216,74 @@ 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.
*/
Real SPATIAL_TOLERANCE = 0.05;

/**
* toggle if we should compute spatial anomaly
*/
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_ASSERT(SPATIAL_TOLERANCE > 0.0f and SPATIAL_TOLERANCE <= 1.0f);

if(minVal_ != std::numeric_limits<Real>::max() and
maxVal_ != std::numeric_limits<Real>::min() ) {
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_) maxVal_ = value;
if(value < minVal_) minVal_ = value;
NTA_ASSERT(anomalyScore_ == NO_ANOMALY or anomalyScore_ == SPATIAL_ANOMALY) << "Out of bounds of acceptable values";
}

private:
Real minVal_ = std::numeric_limits<Real>::max(); //TODO fix serialization
Real maxVal_ = std::numeric_limits<Real>::min();

public:
const Real NO_ANOMALY = 0.0f;
const 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" :)
Real anomalyScore_ = NO_ANOMALY; //default score = no anomaly
} 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
64 changes: 64 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,68 @@ TEST(SpatialPoolerTest, ExactOutput) {
ASSERT_EQ( columns, gold_sdr );
}

TEST(SpatialPoolerTest, spatialAnomaly) {
SDR inputs({ 1000 });
SDR columns({ 200 });
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 too large threshold
#ifdef NTA_ASSERTIONS_ON //only for Debug
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 */));
#endif

//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