Intuitively, we would keep on oscillating up and down, just like the grey dotted line tries to rough out. To us, the continuation of the shape is reasonably easy to understand, but a machine does not have a gut feeling to ask for a good guess. However, we can summon a Frankenstein, which will be able to learn and continue the shape based on numbers. In order to do so, let's have a look at the raw, discrete data of our sinusoidal wave:
x | f(x) |
0.0 | 0.0 |
0.05 | 0.479425538604203 |
0.10 | 0.8414709848078965 |
0.15 | 0.9974949866040544 |
0.20 | 0.9092974268256817 |
0.25 | 0.5984721441039564 |
0.30 | 0.1411200080598672 |
0.35 | -0.35078322768961984 |
... | ... |
0.75 | 0.9379999767747389 |
A supervised training mode means, that we want to train our net with three discrete steps as input and the fourth step as the supervised training element. So we will train and et cetera, hoping that this way our net will capture the slope pattern of our sinusoidal wave. Let's code this in Scala:
import neuroflow.core.Activator._
import neuroflow.core._
import neuroflow.dsl._
import neuroflow.nets.cpu.DenseNetwork._
First, we want a [Tanh](https://en.wikipedia.org/wiki/Hyperbolic_function#Hyperbolic_tangent) activation function, because the domain of our sinusoidal wave is , just like the hyperbolic tangent. This way we can be sure that we are not comparing apples with oranges. Further, we want random initial weights. Let's put this down:
val f = Tanh
val sets = Settings(iterations = 500)
val net = Network(Vector(3) :: Dense(5, f) :: Dense(3, f) :: Dense(1, f) :: SquaredError(), sets)
No surprises here. After some experiments, we can pick values for the settings instance, which will promise good convergence during training. Now, let's prepare our discrete steps drawn from the sinus function:
val group = 4
val sinusoidal = Range.Double(0.0, 0.8, 0.05).grouped(group).toList.map(i => i.map(k => (k, Math.sin(10 * k))))
val xsys = sinusoidal.map(s => (s.dropRight(1).map(_._2), s.takeRight(1).map(_._2)))
val xs = xsys.map(_._1)
val ys = xsys.map(_._2)
net.train(xs, ys)
We draw samples from _Range_ with step size 0.05. After this, we construct our training values as well as supervised output values . Here, a group consists of 4 steps, with 3 steps as input and the last step as the supervised value.
[INFO] [25.01.2016 14:07:51:677] [run-main-5] Taking step 499 - error: 1.4395661497489177E-4 , error per sample: 3.598915374372294E-5
[INFO] [25.01.2016 14:07:51:681] [run-main-5] Took 500 iterations of 500 with error 1.4304189739640242E-4
[success] Total time: 4 s, completed 25.01.2016 14:20:56
After a pretty short time, we see good news. Now, how can we check if our net can successfully predict the sinusoidal wave? We can't simply call our net like a sinus function to map from one input value to one output value, e. g. something like . Our net does not care about any values, because it was trained purely based on the function values , or the slope pattern in general. We need to feed our net with a three-dimensional input vector holding the first three, original function values to predict the fourth step, then drop the first original step and append the recently predicted step to predict the fifth step, et cetera. In other words, we need to traverse the net. Let's code this:
val initial = Range.Double(0.0, 0.15, 0.05).zipWithIndex.map(p => (p._1, xs.head(p._2)))
val result = predict(net, xs.head, 0.15, initial)
result.foreach(r => println(s"${r._1}, ${r._2}"))
with
@tailrec def predict(net: Network, last: Seq[Double], i: Double, results: Seq[(Double, Double)]): Seq[(Double, Double)] = {
if (i < 4.0) {
val score = net.evaluate(last).head
predict(net, last.drop(1) :+ score, i + 0.05, results :+ (i, score))
} else results
}
So, basically we don't just continue to draw the sinusoidal shape at the point 0.75, we draw the entire shape right from the start until 4.0 - solely based on our trained net! Now, let's see how our Frankenstein will complete the sinusoidal shape from 0.75 on:
I'd say, pretty neat? Keep in mind, here, the discrete predictions are connected through splines. Another interesting property of our trained net is its prediction compared to the original sinus function when taking the limit towards 4.0. Let's plot both:
The purple line is the original sinusoidal wave, whereas the green line is the prediction of our net. The first steps show great consistency, but slowly the curves diverge a little over time, as uncertainties will add up. To keep this divergence rather low, one could fine tune settings, for instance numeric precision. However, if one is taking the limit towards infinity, a perfect fit is illusory.