Experiments with ruby-processing (processing-2.2.1) and JRubyArt for processing-3.0

Showing posts with label inject. Show all posts
Showing posts with label inject. Show all posts

Tuesday, 4 March 2014

Using inject with an array of Vec2D (another Nature of Code Example)

See previous blog for background information. Here is another example of using inject to sum an array, however in this case we are summing objects of Vec2D, not numbers!
# The Nature of Code
# http://natureofcode.com

# Simple Perceptron Example
# See: http://en.wikipedia.org/wiki/Perceptron
load_library :vecmath

class Perceptron
  # Perceptron is created with n weights and learning constant
  def initialize(n, c)
    @weights = Array.new(n){ rand(0 .. 1.0) }
    @c = c
  end

  # Function to train the Perceptron
  # Weights are adjusted based on vehicle's error
  def train(forces, error)
    trained = @weights.zip(forces.map{|f| f.to_a}
    .map{|a, b| (a * error.x + b * error.y) * @c})
    .map {|w, c| constrain(w + c, 0.0, 1.0)}
    @weights = trained
  end

  # Give me a steering result
  def feedforward(forces)
    # Sum all values
    forces.zip(@weights).map{|a, b| a * b}.inject(Vec2D.new){|c, d| c + d}
  end
end

# Seek
# Daniel Shiffman <http://www.shiffman.net>

class Vehicle

  MAX_SPEED = 4
  MAX_FORCE = 0.1
  attr_reader :brain, :sz, :location, :targets, :desired
  attr_reader :maxforce_squared, :maxspeed_squared

  def initialize(n, x, y)
    @brain = Perceptron.new(n, 0.001)
    @acceleration = Vec2D.new(0, 0)
    @velocity = Vec2D.new(0, 0)
    @location = Vec2D.new(x, y)
    @sz = 6.0
    @maxspeed_squared = MAX_SPEED * MAX_SPEED
    @maxforce_squared = MAX_FORCE * MAX_FORCE
  end

  # Method to update location
  def update(width, height)
    # Update velocity
    @velocity += @acceleration
    # Limit speed
    @velocity.set_mag(MAX_SPEED) if @velocity.mag_squared > maxspeed_squared
    @location += @velocity
    # Reset acceleration to 0 each cycle
    @acceleration *= 0

    @location.x = constrain(location.x, 0, width)
    @location.y = constrain(location.y, 0, height)
  end

  def apply_force(force)
    # We could add mass here if we want A = F / M
    @acceleration += force
  end

  # Here is where the brain processes everything
  def steer(targets, desired)
    # Steer towards all targets
    forces = targets.map{|target| seek(target) }

    # That array of forces is the input to the brain
    result = brain.feedforward(forces)

    # Use the result to steer the vehicle
    apply_force(result)

    # Train the brain according to the error
    error = desired - location
    brain.train(forces, error)
   end

  # A method that calculates a steering force towards a target
  # STEER = DESIRED MINUS VELOCITY
  def seek(target)
    desired = target - location  # A vector pointing from the location to the target

    # Normalize desired and scale to the maximum speed
    desired.normalize!
    desired *= MAX_SPEED
    # Steering = Desired minus velocity
    steer = desired - @velocity
    steer.set_mag(MAX_FORCE) if steer.mag_squared > maxforce_squared # Limit to a maximum steering force
    steer
  end

  def display

    # Draw a triangle rotated in the direction of velocity
    theta = @velocity.heading + Math::PI / 2
    fill(175)
    stroke(0)
    stroke_weight(1)
    push_matrix
    translate(location.x, location.y)
    rotate(theta)
    begin_shape
    vertex(0, -sz)
    vertex(-sz * 0.5, sz)
    vertex(sz * 0.5, sz)
    end_shape(CLOSE)
    pop_matrix
  end
end

# A Vehicle controlled by a Perceptron
attr_reader :targets, :desired, :v


def setup
  size(640, 360)
  # The Vehicle's desired location
  @desired = Vec2D.new(width/2, height/2)

  # Create a list of targets
  make_targets

  # Create the Vehicle (it has to know about the number of targets
  # in order to configure its brain)
  @v = Vehicle.new(targets.size, rand(width), rand(height))
end

# Make a random ArrayList of targets to steer towards
def make_targets
  @targets = Array.new(8) { Vec2D.new(rand(width), rand(height)) }
end

def draw
  background(255)

  # Draw a circle to show the Vehicle's goal
  stroke(0)
  stroke_weight(2)
  fill(0, 100)
  ellipse(desired.x, desired.y, 36, 36)

  # Draw the targets
  targets.each do |target|
    no_fill
    stroke(0)
    stroke_weight(2)
    ellipse(target.x, target.y, 16, 16)
    line(target.x, target.y - 16, target.x, target.y + 16)
    line(target.x - 16, target.y, target.x + 16, target.y)
  end

  # Update the Vehicle
  v.steer(targets, desired)
  v.update(width, height)
  v.display
end

def mouse_pressed
  make_targets
end

Here I am particulary pleased with the Perceptron very functional looking train and the feedforward code, where I use inject to sum Vec2D.

Getting closer to "functional programming with ruby-processing"

In a previous blog I reported that Dan Shiffmans Nature of Code had been ported to ruby processing. So I took a look at the code, and could not help from meddling here is my modified version of the perceptron sketch:-
# The Nature of Code
# http://natureofcode.com

# Simple Perceptron Example
# See: http://en.wikipedia.org/wiki/Perceptron

class Perceptron
  attr_reader :weights

  # Perceptron is created with n weights and a learning constant
  def initialize(n, c)
    @weights = Array.new(n) { rand(-1.0 .. 1) }
    @c = c # learning constant
  end

  # Function to train the Perceptron
  # Weights are adjusted based on "desired" answer
  def train(inputs, desired)
    # Guess the result
    guess = feedforward(inputs)
    # Compute the factor for changing the weight based on the error
    # Error = desired output - guessed output
    # Note this can only be 0, -2, or 2
    # Multiply by learning constant
    error = desired - guess
    # Adjust weights based on weightChange * input
    @weights.each_index{|i| @weights[i] += error * inputs[i]}
  end

  # Guess -1 or 1 based on input values
  def feedforward(inputs)
    # Sum all values
    sum = @weights.zip(inputs).map{|x, y| x * y}.inject(0){|x, y| x + y}
    # Result is sign of the sum, -1 or 1
    activate(sum)
  end

  def activate(sum)
    sum > 0 ? 1 : -1
  end
end

# A class to describe a training point
# Has an x and y, a "bias" (1) and a known output
# Could also add a variable for "guess" but not required here

class Trainer
  attr_reader :inputs, :answer

  def initialize(x, y, a)
    @inputs = [x, y, 1]
    @answer = a
  end
end

# Code based on text "Artificial Intelligence", George Luger

# The function to describe a line
def f(x)
  0.4 * x + 1
end

def setup
  size(640, 360)
  # Coordinate space
  @xmin = -400
  @ymin = -100
  @xmax =  400
  @ymax =  100

  @count = 0

  # The perceptron has 3 inputs -- x, y, and bias
  # Second value is "Learning Constant"
  @ptron = Perceptron.new(3, 0.00001)  # Learning Constant is low just b/c it's fun to watch, this is not necessarily optimal

   #Create a random set of training points and calculate the "known" answer
  @training = Array.new(2000) do
    x = rand(@xmin .. @xmax)
    y = rand(@ymin .. @ymax)
    answer = y < f(x) ? -1 : 1
    Trainer.new(x, y, answer)
  end

  smooth
end

def draw
  background(255)
  translate(width/2, height/2)

  # Draw the line
  stroke_weight(4)
  stroke(127)
  x1 = @xmin
  y1 = f(x1)
  x2 = @xmax
  y2 = f(x2)
  line(x1, y1, x2, y2)

  # Draw the line based on the current weights
  # Formula is weights[0]*x + weights[1]*y + weights[2] = 0
  stroke(0)
  stroke_weight(1)
  weights = @ptron.weights
  x1 = @xmin
  y1 = (-weights[2] - weights[0]*x1) / weights[1]
  x2 = @xmax
  y2 = (-weights[2] - weights[0]*x2) / weights[1]
  line(x1, y1, x2, y2)

  # Train the Perceptron with one "training" point at a time
  @ptron.train(@training[@count].inputs, @training[@count].answer)
  @count = (@count + 1) % @training.size

  # Draw all the points based on what the Perceptron would "guess"
  # Does not use the "known" correct answer
  @count.times do |i|
    stroke(0)
    stroke_weight(1)
    fill(0)
    train = @training[i]
    guess = @ptron.feedforward(train.inputs)
    no_fill if guess > 0

    ellipse(train.inputs[0], train.inputs[1], 8, 8)
  end
end

Here I am particulary pleased with the Perceptron feedforward code, that sums the weights using zip, map and inject. This code has a somewhat functional feel to it?

Followers

About Me

My photo
I have developed JRubyArt and propane new versions of ruby-processing for JRuby-9.1.5.0 and processing-3.2.2