diff --git a/config/thresholds.json b/config/thresholds.json index f3099ebc1..e39576f8a 100644 --- a/config/thresholds.json +++ b/config/thresholds.json @@ -97,6 +97,20 @@ "threshold": 1.0 } }, + "naive": { + "reward_low_FN_rate": { + "score": -232.0, + "threshold": 1.1 + }, + "reward_low_FP_rate": { + "score": -116.0, + "threshold": 1.1 + }, + "standard": { + "score": -116.0, + "threshold": 1.1 + } + }, "null": { "reward_low_FN_rate": { "score": -232.0, diff --git a/nab/detectors/naive/__init__.py b/nab/detectors/naive/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nab/detectors/naive/naive_detector.py b/nab/detectors/naive/naive_detector.py new file mode 100644 index 000000000..36fa2fbea --- /dev/null +++ b/nab/detectors/naive/naive_detector.py @@ -0,0 +1,90 @@ +# ---------------------------------------------------------------------- +# Copyright (C) 2015, Numenta, Inc. Unless you have an agreement +# with Numenta, Inc., for a separate license for this software code, the +# following terms and conditions apply: +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Affero Public License for more details. +# +# You should have received a copy of the GNU Affero Public License +# along with this program. If not, see http://www.gnu.org/licenses. +# +# http://numenta.org/licenses/ +# ---------------------------------------------------------------------- + +import math #exp + +from nab.detectors.base import AnomalyDetector + +EPSILON = 0.00000001 + +class NaiveDetector(AnomalyDetector): + """ + This is implementation of the "naive forecast", aka "random walk forecasting", + which is a baseline algorithm for time-series forecasting. + It predicts the last seen value. So `Prediction(t+1) = Input(t)`. + + Hyperparameter to optimize is @param coef in `initialize`. + """ + + + def initialize(self, coef=10.0): + """ + @param `coef` for the activation function that scales anomaly score to [0, 1.0] + The function is: `anomalyScore = 1-exp(-coef*x)`, where + `x=abs(current - predicted)/predicted`. + """ + super().initialize() + self.predicted = 0.0 #previous value, last seen + self.coef = coef + + + def handleRecord(self, inputData): + """The predicted value is simply the last seen value, + Anomaly score is computed as a function of current,predicted. + + See @ref `initialize` param `coef`. + """ + current = float(inputData["value"]) + inputData['predicted'] = self.predicted + try: + anomalyScore = self.anomalyFn_(current, self.predicted) + except: + #on any math error (overflow,...), we mark this as anomaly. tough love + anomalyScore = 1.0 + + ret = [anomalyScore, self.predicted] + self.predicted=current + return (ret) + + + def getAdditionalHeaders(self): + return ['predicted'] + + + def anomalyFn_(self, current, predicted): + """ + compute anomaly score from 2 scalars + """ + if predicted == 0.0: + predicted = EPSILON #avoid division by zero + + # the computation + x = abs(current - predicted)/predicted + score = 1-math.exp(-self.coef * x) + + # bound to anomaly range (should not happen, but some are over) +# if(score > 1): +# score = 1.0 +# elif(score < 0): +# score = 0.0 + + assert(score >= 0 and score <= 1), print("ERR: score: "+str(score)+ "\t curr: "+str(current)+"\t pred: "+str(predicted)) + return score + diff --git a/results/final_results.json b/results/final_results.json index 295cef20b..7a39a8220 100644 --- a/results/final_results.json +++ b/results/final_results.json @@ -29,6 +29,11 @@ "reward_low_FP_rate": 43.40851703291233, "standard": 57.99434335898808 }, + "naive": { + "reward_low_FN_rate": 0.0, + "reward_low_FP_rate": 0.0, + "standard": 0.0 + }, "null": { "reward_low_FN_rate": 0.0, "reward_low_FP_rate": 0.0, diff --git a/run.py b/run.py index 01a096a35..7c2da23eb 100755 --- a/run.py +++ b/run.py @@ -161,7 +161,7 @@ def main(args): type=str, default=["numenta", "numentaTM", "htmcore", "htmjava", "null", "random", "bayesChangePt", "windowedGaussian", "expose", - "relativeEntropy", "earthgeckoSkyline"], + "relativeEntropy", "earthgeckoSkyline", "naive"], help="Comma separated list of detector(s) to use, e.g. " "null,numenta") @@ -227,6 +227,8 @@ def main(args): ContextOSEDetector ) if "earthgeckoSkyline" in args.detectors: from nab.detectors.earthgecko_skyline.earthgecko_skyline_detector import EarthgeckoSkylineDetector + if "naive" in args.detectors: + from nab.detectors.naive.naive_detector import NaiveDetector if "htmcore" in args.detectors: from nab.detectors.htmcore.htmcore_detector import HtmcoreDetector