Looking at the candle bars, we can clearly see an uptrend, supported by the linear function drawn in purple. The green line is a constant, flat function and is the opposite of a trend. How can we train a neural net to differentiate between these two?
val trend = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, i))
val flat = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, 0.3))
Here, the trend data is drawn from the linear function from 0.0 until 1.0 with step size 0.01, whereas flat is a constant discrete line. The constant 0.3 is picked arbitrarily and could be marginalized out through a richer training set. Note that choosing, for instance, 0.5 instead of 0.3 leads to similar results. The important thing is that both graphical shapes are clearly separable.
Next, we need to spawn a neural network:
val f = Sigmoid
val net = Network(Vector(trend.size) :: Dense(25, f) :: Dense(1, f) :: SquaredMeanError())
net.train(Seq(trend, flat), Seq(->(1.0), ->(0.0)))
We choose a net architecture, with N counting our discrete training values. Every discrete step of our training set will get its dedicated neuron. Plus, a step size of 0.01 means that the range produces neural receptors, so our model lives in a 5025 dimensional space. The output neuron will answer with a number close to 1 if it's a trend, and 0 if it's not.
The training succeeds after 118 seconds:
[INFO] [12.01.2016 12:52:33:853] [run-main-0] Took 61 iterations of 10000 with error 9.965761971223065E-5
Weights: 5025
[success] Total time: 118 s, completed 12.01.2016 12:52:33
Let's see what kind of answers we get for various inputs. Feeding it with training input, a linear and a flat function, we can check if they get classified correctly:
Flat Result: DenseVector(0.010301838081712023)
Linear Trend Result: DenseVector(0.9962517960703637)
Yup, good.
val squareTest = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, i * i))
Square Trend Result: DenseVector(0.958457769839082)
Our net is pretty confident that this is a trend. Check!
val declineTest = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, 1.0 - i))
Linear Decline Trend Result: DenseVector(0.0032519862410505525)
Our net is very confident that a linear downtrend is not an uptrend. Check!
val squareDeclineTest = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, (-1 * i * i) + 1.0))
Square Decline Trend Result: DenseVector(0.011391593430466094)
Our net is very confident that a square downtrend is not an uptrend. Check!
val jammingTest = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, 0.5*Math.sin(3*i)))
Jamming Result: DenseVector(0.03840459974525514)
Again, our net is confident that this is not an uptrend. Check!
val heroZeroTest = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, 0.5*Math.cos(6*i) + 0.5))
HeroZero Result: DenseVector(0.024507592248881733)
Indeed, our net is pretty confident that this is not an uptrend. Check!
val oscillating = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, (Math.sin(100*i) / 3) + 0.5))
Oscillating Result: DenseVector(0.0332458093016362)
Strike, our net is confident that this is not an uptrend. Check! Let's take a truly random sideways movement:
val random = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, Random.nextDouble))
Random Result: DenseVector(0.3636381886772248)
What's interesting here is that our net still says it is rather not a trend - which is correct - but with 0.36 it is not as certain as in the example before. If we examine the range we see a slight uptrend. I guess this is the reason for the increased uncertainty. However, check!
val oscillatingUp = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, i + (Math.sin(100*i) / 3)))
Oscillating Up Result: DenseVector(0.9441733303039243)
Our net is pretty confident that this is an uptrend. Check! But what if we slighty randomize this trend, to make it seem more natural:
val realWorld = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, i + (Math.sin(100*i) / 3) * Random.nextDouble))
Real World Result: DenseVector(0.9900808890038473)
It's also correctly classified, check!
val oscillatingDown = Range.Double(0.0, 1.0, 0.01).flatMap(i => Seq(i, -i + (Math.sin(100*i) / 3) + 1))
Oscillating Down Result: DenseVector(0.0030360075614561453)
Again, our net is really confident that this is not an uptrend. Check!