From 3c62326506a560e3b0fbf4ac613d75d759118108 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:36:47 -0400 Subject: [PATCH 01/26] New practice exercise - Perceptron --- config.json | 15 +++++++ .../practice/perceptron/.docs/instructions.md | 34 +++++++++++++++ .../practice/perceptron/.meta/config.json | 20 +++++++++ .../practice/perceptron/.meta/example.jl | 8 ++++ .../practice/perceptron/.meta/tests.toml | 25 +++++++++++ exercises/practice/perceptron/perceptron.jl | 3 ++ exercises/practice/perceptron/runtests.jl | 43 +++++++++++++++++++ exercises/practice/perceptron/testtools.jl | 43 +++++++++++++++++++ 8 files changed, 191 insertions(+) create mode 100644 exercises/practice/perceptron/.docs/instructions.md create mode 100644 exercises/practice/perceptron/.meta/config.json create mode 100644 exercises/practice/perceptron/.meta/example.jl create mode 100644 exercises/practice/perceptron/.meta/tests.toml create mode 100644 exercises/practice/perceptron/perceptron.jl create mode 100644 exercises/practice/perceptron/runtests.jl create mode 100644 exercises/practice/perceptron/testtools.jl diff --git a/config.json b/config.json index fdf85667..e523bff8 100644 --- a/config.json +++ b/config.json @@ -883,6 +883,21 @@ "conditionals", "variables" ] + }, + { + "uuid": "b43a938a-7bd2-4fe4-b16c-731e2e25e747", + "practices": [], + "prerequisites": [], + "slug": "perceptron", + "name": "Perceptron", + "difficulty": 3, + "topics": [ + "machine learning", + "loops", + "arrays", + "logic", + "math" + ] } ] }, diff --git a/exercises/practice/perceptron/.docs/instructions.md b/exercises/practice/perceptron/.docs/instructions.md new file mode 100644 index 00000000..10a68d57 --- /dev/null +++ b/exercises/practice/perceptron/.docs/instructions.md @@ -0,0 +1,34 @@ +# Instructions + +### Introduction +[Perceptron](https://en.wikipedia.org/wiki/Perceptron) is one of the oldest and bestestly named machine learning algorithms out there. Since perceptron is also quite simple to implement, it's a favorite place to start a machine learning journey. As a linear classifier, if a linear decision boundary (e.g. a line in 2D or hyperplane in general) can be drawn to separate two labled classes of objects, perceptron is guaranteed to do so. This can help in predicting what an unlabeled object would likely be classified as by seeing which side of the decision boundary it is on. + +### Details +The basic idea is fairly straightforward. We cycle through the objects and check if they are on the correct side of our hyperplane. If one is not, we make a correction to the hyperplane and continue checking the objects against the new hyperplane. Eventually the hyperplane is adjusted to correctly separate all the objects and we have our decision boundary! + +#### A Brief Word on Hyperplanes +How do you pick your starting hyperplane? It's up to you! Be creative! Or not... Actually perceptron's convergence times are sensitive to conditions such as the initial hyperplane and even the order the objects are looped through, so you might not want to go too wild. + +We will be dealing with a two dimensional space, so our divider will be a line. The standard equation for a line is usually written as $y = ax+b$, where $a,b \in \Re$, however, in order to help generalize the idea to higher dimensions, it's convenient to reformulate this equation as $w_0 + w_1x + w_2y = 0$. This is the form of the [hyperplane](https://en.wikipedia.org/wiki/Hyperplane) we will be using, so your output should be $[w_0, w_1, w_2]$. In machine learning, ${w_0,w_1,w_2}$ are usually referred to as weights. + +While hyperplanes are equivalent under scalar multiplication, there is a difference between $[w_0, w_1, w_2]$ and $[-w_0, -w_1, -w_2]$ in that the normal to the hyperplane points in opposite directions. By convention, the perceptron normal points towards the class defined as positive, so this property will be checked but not result in a test failure. + +#### Updating +Checking if an object is on one side of a hyperplane or another can be done by checking the normal vector which points to the object. The value will be positive, negative or zero, so all of the objects from a class should have normal vectors with the same sign. A zero value means the object is on the hyperplane, which we don't want to allow since its ambiguous. Checking the sign of a normal to a hyperplane might sound like it could be complicated, but it's actually quite easy. Simply plug in the coordinates for the object into the equation for the hyperplane and check the sign of the result. For example, we can look at two objects $v_1,v_2$ in relation to the hyperplane $[w_0, w_1, w_2] = [1, 1, 1]$: + +$$v_1$$ $$[x_1, y_1] = [2, 2]$$ $$w_0 + w_1*x_1 + w_2*y_1 = 1 + 1*2 + 1*2 = 5 > 0$$ + + +$$v_2$$ $$[x_2,y_2]=[-2,-2]$$ $$w_0 + w_1*x_2 + w_2*y_2 = 1 + 1*(-2) + 1*(-2) = -3 < 0$$ + +If $v_1$ and $v_2$ have different labels, such as $1$ and $-1$ (like we will be using), then the hyperplane $[1, 1, 1]$ is a valid decision boundary for them. + +Now that we know how to tell which side of the hyperplane an object lies on, we can look at how perceptron updates a hyperplane. If an object is on the correct side of the hyperplane, no update is performed on the weights. However, if we find an object on the wrong side, the update rule for the weights is: + +$$[w_0', w_1', w_2'] = [w_0 \pm l_{class}, w_1 \pm x*l_{class}, w_2 \pm y*l_{class}]$$ + +Where $l_{class}=\pm 1$, according to the class of the object (i.e. its label), $x,y$ are the coordinates of the object, the $w_i$ are the weights of the hyperplane and the $w_i'$ are the weights of the updated hyperplane. The plus or minus signs are homogenous, so either all plus or all minus, and are determined by the choice of which class you define to be on the positive side of the hyperplane. Beware that only two out of the four possible combinations of class on positive side of the hyperplane and the plus/minus in the update are valid ($\pm \pm, \mp \mp$), with the other two ($\pm \mp, \mp \pm$) leading to infinite loops. + +This update is repeated for each object in turn, and then the whole process repeated until there are no updates made to the hyperplane. All objects passing without an update means they have been successfully separated and you can return your decision boundary! + +Note: Although the perceptron algorithm is deterministic, a decision boundary depends on initialization and is not unique in general, so the tests accept any hyperplane which fully separates the objects. \ No newline at end of file diff --git a/exercises/practice/perceptron/.meta/config.json b/exercises/practice/perceptron/.meta/config.json new file mode 100644 index 00000000..278526a3 --- /dev/null +++ b/exercises/practice/perceptron/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "depial" + ], + "contributors": [ + "cmcaine" + ], + "files": { + "solution": [ + "perceptron.jl" + ], + "test": [ + "runtests.jl" + ], + "example": [ + ".meta/example.jl" + ] + }, + "blurb": "Given points and their labels, provide a hyperplane which separates them" +} diff --git a/exercises/practice/perceptron/.meta/example.jl b/exercises/practice/perceptron/.meta/example.jl new file mode 100644 index 00000000..47a48073 --- /dev/null +++ b/exercises/practice/perceptron/.meta/example.jl @@ -0,0 +1,8 @@ +function perceptron(points, labels) + θ, pnts = [0, 0, 0], vcat.(1, points) + while true + θ_0 = θ + foreach(i -> labels[i]*θ'*pnts[i] ≤ 0 && (θ += labels[i]*pnts[i]), eachindex(pnts)) + θ_0 == θ && return θ + end +end \ No newline at end of file diff --git a/exercises/practice/perceptron/.meta/tests.toml b/exercises/practice/perceptron/.meta/tests.toml new file mode 100644 index 00000000..5c49832e --- /dev/null +++ b/exercises/practice/perceptron/.meta/tests.toml @@ -0,0 +1,25 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[728853d3-24de-4855-a452-6520b67dec23] +description = "Initial set" + +[ed5bf871-3923-47ca-8346-5d640f9069a0] +description = "Initial set w/ opposite labels" + +[15a9860e-f9be-46b1-86b2-989bd878c8a5] +description = "Hyperplane cannot pass through origin" + +[52ba77fc-8983-4429-91dc-e64b2f625484] +description = "Hyperplane nearly parallel with y-axis" + +[3e758bbd-5f72-447d-999f-cfa60b27bc26] +description = "Increasing Populations" \ No newline at end of file diff --git a/exercises/practice/perceptron/perceptron.jl b/exercises/practice/perceptron/perceptron.jl new file mode 100644 index 00000000..0945cdea --- /dev/null +++ b/exercises/practice/perceptron/perceptron.jl @@ -0,0 +1,3 @@ +function perceptron(points, labels) + # Perceptronize! +end \ No newline at end of file diff --git a/exercises/practice/perceptron/runtests.jl b/exercises/practice/perceptron/runtests.jl new file mode 100644 index 00000000..2a38df3d --- /dev/null +++ b/exercises/practice/perceptron/runtests.jl @@ -0,0 +1,43 @@ +using Test + +include("perceptron.jl") +include("testtools.jl") + +@testset "Low population" begin + @testset "Initial set" begin + points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] + labels = [1, 1, -1, -1, 1, 1] + reference = [1, 2, 1] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + @testset "Initial set w/ opposite labels" begin + points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] + labels = [-1, -1, 1, 1, -1, -1] + reference = [-1, -2, -1] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + @testset "Hyperplane cannot pass through origin" begin + points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [-1, -1]] + labels = [1, 1, -1, -1, 1, 1] + reference = [-1, 3, 3] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + @testset "Hyperplane nearly parallel with y-axis" begin + points = [[0, 50], [0, -50], [-2, 0], [1, 50], [1, -50], [2, 0]] + labels = [-1, -1, -1, 1, 1, 1] + reference = [2, 0, -1] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end +end + +@testset "Increasing Populations" begin + for n in 10:50 + points, labels, reference = population(n, 25) + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end +end \ No newline at end of file diff --git a/exercises/practice/perceptron/testtools.jl b/exercises/practice/perceptron/testtools.jl new file mode 100644 index 00000000..edb21e15 --- /dev/null +++ b/exercises/practice/perceptron/testtools.jl @@ -0,0 +1,43 @@ +using Random + +function dotest(points, labels, hyperplane, reference) + # Tests if a hyperplane linearly separates labeled points + # Returns true or false + + points = vcat.(1, points) + test = reduce(hcat, points)' * hyperplane .* labels + if all(>(0), test) + println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! And the normal points towards the positively labeled side\n") + return true + elseif all(<(0), test) + println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! But the normal points towards the negatively labeled side\n") + return true + else + println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nThe sides are not properly separated...\n") + return false + end +end + +Random.seed!(42) # set seed for deterministic test set + +function population(n, bound) + # Builds a population of n points with labels {1, -1} in area bound x bound around a reference hyperplane + # Returns linearly separable points, labels and reference hyperplane + + vertical = !iszero(n % 10) #every tenth test has vertical reference hyperplane + x, y, b = rand(-bound:bound), rand(-bound:bound)*vertical, rand(-bound÷2:bound÷2) + y_intercept = -b ÷ (iszero(y) ? 1 : y) + points, labels, hyperplane = [], [], [b, x, y] + while n > 0 + # points are centered on y-intercept, but not x-intercept so distributions can be lopsided + point = [rand(-bound:bound), y_intercept + rand(-bound:bound)] + label = point' * [x, y] + b + if !iszero(label) + push!(points, point) + push!(labels, sign(label)) + n -= 1 + end + end + + points, labels, hyperplane +end \ No newline at end of file From 053cb6377e4741c1ed3400f171f3781e54d3808e Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Wed, 31 Jan 2024 09:05:23 -0500 Subject: [PATCH 02/26] Update runtests.jl Added helper functions from `testtools.jl` to bottom of file so it doesn't have to be included. Since the `dotest` function does not show a working solution (only how to check for one), I think a spoiler warning is unnecessary, and could just end up attracting more attention than without. --- exercises/practice/perceptron/runtests.jl | 46 +++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/exercises/practice/perceptron/runtests.jl b/exercises/practice/perceptron/runtests.jl index 2a38df3d..be5de247 100644 --- a/exercises/practice/perceptron/runtests.jl +++ b/exercises/practice/perceptron/runtests.jl @@ -1,7 +1,6 @@ -using Test +using Test, Random include("perceptron.jl") -include("testtools.jl") @testset "Low population" begin @testset "Initial set" begin @@ -40,4 +39,45 @@ end hyperplane = perceptron(points, labels) @test dotest(points, labels, hyperplane, reference) end -end \ No newline at end of file +end + + + +Random.seed!(42) # set seed for deterministic test set + +function population(n, bound) + # Builds a population of n points with labels {1, -1} in area bound x bound around a reference hyperplane + # Returns linearly separable points, labels and reference hyperplane + + vertical = !iszero(n % 10) #every tenth test has vertical reference hyperplane + x, y, b = rand(-bound:bound), rand(-bound:bound)*vertical, rand(-bound÷2:bound÷2) + y_intercept = -b ÷ (iszero(y) ? 1 : y) + points, labels, hyperplane = [], [], [b, x, y] + while n > 0 + # points are centered on y-intercept, but not x-intercept so distributions can be lopsided + point = [rand(-bound:bound), y_intercept + rand(-bound:bound)] + label = point' * [x, y] + b + if !iszero(label) + push!(points, point) + push!(labels, sign(label)) + n -= 1 + end + end + + points, labels, hyperplane +end + +function dotest(points, labels, hyperplane, reference) + points = vcat.(1, points) + test = reduce(hcat, points)' * hyperplane .* labels + if all(>(0), test) + println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! And the normal points towards the positively labeled side\n") + return true + elseif all(<(0), test) + println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! But the normal points towards the negatively labeled side\n") + return true + else + println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nThe sides are not properly separated...\n") + return false + end +end From c821340dede9dec9f93469e9ec3ef1314f696924 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:28:38 -0500 Subject: [PATCH 03/26] Update runtests.jl Tests still failed with helper functions at the bottom of the file, so I'm moving them to the top to see if that changes things. --- exercises/practice/perceptron/runtests.jl | 80 +++++++++++------------ 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/exercises/practice/perceptron/runtests.jl b/exercises/practice/perceptron/runtests.jl index be5de247..bf7f7f6e 100644 --- a/exercises/practice/perceptron/runtests.jl +++ b/exercises/practice/perceptron/runtests.jl @@ -2,47 +2,6 @@ using Test, Random include("perceptron.jl") -@testset "Low population" begin - @testset "Initial set" begin - points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] - labels = [1, 1, -1, -1, 1, 1] - reference = [1, 2, 1] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) - end - @testset "Initial set w/ opposite labels" begin - points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] - labels = [-1, -1, 1, 1, -1, -1] - reference = [-1, -2, -1] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) - end - @testset "Hyperplane cannot pass through origin" begin - points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [-1, -1]] - labels = [1, 1, -1, -1, 1, 1] - reference = [-1, 3, 3] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) - end - @testset "Hyperplane nearly parallel with y-axis" begin - points = [[0, 50], [0, -50], [-2, 0], [1, 50], [1, -50], [2, 0]] - labels = [-1, -1, -1, 1, 1, 1] - reference = [2, 0, -1] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) - end -end - -@testset "Increasing Populations" begin - for n in 10:50 - points, labels, reference = population(n, 25) - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) - end -end - - - Random.seed!(42) # set seed for deterministic test set function population(n, bound) @@ -81,3 +40,42 @@ function dotest(points, labels, hyperplane, reference) return false end end + +@testset "Low population" begin + @testset "Initial set" begin + points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] + labels = [1, 1, -1, -1, 1, 1] + reference = [1, 2, 1] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + @testset "Initial set w/ opposite labels" begin + points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] + labels = [-1, -1, 1, 1, -1, -1] + reference = [-1, -2, -1] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + @testset "Hyperplane cannot pass through origin" begin + points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [-1, -1]] + labels = [1, 1, -1, -1, 1, 1] + reference = [-1, 3, 3] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + @testset "Hyperplane nearly parallel with y-axis" begin + points = [[0, 50], [0, -50], [-2, 0], [1, 50], [1, -50], [2, 0]] + labels = [-1, -1, -1, 1, 1, 1] + reference = [2, 0, -1] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end +end + +@testset "Increasing Populations" begin + for n in 10:50 + points, labels, reference = population(n, 25) + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end +end From a4bc759940c1c92390b6428b370db595c9baac28 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:22:16 -0500 Subject: [PATCH 04/26] Update runtests.jl Wrapped the test set in a function to be called at the end of the file so the helper functions can be put at the bottom of the file. --- exercises/practice/perceptron/runtests.jl | 85 ++++++++++++----------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/exercises/practice/perceptron/runtests.jl b/exercises/practice/perceptron/runtests.jl index bf7f7f6e..56363cc1 100644 --- a/exercises/practice/perceptron/runtests.jl +++ b/exercises/practice/perceptron/runtests.jl @@ -1,8 +1,49 @@ using Test, Random - include("perceptron.jl") -Random.seed!(42) # set seed for deterministic test set +function runtestset() + + @testset "Low population" begin + @testset "Initial set" begin + points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] + labels = [1, 1, -1, -1, 1, 1] + reference = [1, 2, 1] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + @testset "Initial set w/ opposite labels" begin + points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] + labels = [-1, -1, 1, 1, -1, -1] + reference = [-1, -2, -1] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + @testset "Hyperplane cannot pass through origin" begin + points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [-1, -1]] + labels = [1, 1, -1, -1, 1, 1] + reference = [-1, 3, 3] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + @testset "Hyperplane nearly parallel with y-axis" begin + points = [[0, 50], [0, -50], [-2, 0], [1, 50], [1, -50], [2, 0]] + labels = [-1, -1, -1, 1, 1, 1] + reference = [2, 0, -1] + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + end + + @testset "Increasing Populations" begin + for n in 10:50 + points, labels, reference = population(n, 25) + hyperplane = perceptron(points, labels) + @test dotest(points, labels, hyperplane, reference) + end + end + +end + function population(n, bound) # Builds a population of n points with labels {1, -1} in area bound x bound around a reference hyperplane @@ -41,41 +82,5 @@ function dotest(points, labels, hyperplane, reference) end end -@testset "Low population" begin - @testset "Initial set" begin - points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] - labels = [1, 1, -1, -1, 1, 1] - reference = [1, 2, 1] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) - end - @testset "Initial set w/ opposite labels" begin - points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] - labels = [-1, -1, 1, 1, -1, -1] - reference = [-1, -2, -1] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) - end - @testset "Hyperplane cannot pass through origin" begin - points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [-1, -1]] - labels = [1, 1, -1, -1, 1, 1] - reference = [-1, 3, 3] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) - end - @testset "Hyperplane nearly parallel with y-axis" begin - points = [[0, 50], [0, -50], [-2, 0], [1, 50], [1, -50], [2, 0]] - labels = [-1, -1, -1, 1, 1, 1] - reference = [2, 0, -1] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) - end -end - -@testset "Increasing Populations" begin - for n in 10:50 - points, labels, reference = population(n, 25) - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) - end -end +Random.seed!(42) # set seed for deterministic test set +runtestset() From 185e88c25c2297cf807d1ebd27a03d1b1502a872 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:23:37 -0500 Subject: [PATCH 05/26] Delete exercises/practice/perceptron/testtools.jl Deleting since test helper functions have been moved to `runtests.jl` --- exercises/practice/perceptron/testtools.jl | 43 ---------------------- 1 file changed, 43 deletions(-) delete mode 100644 exercises/practice/perceptron/testtools.jl diff --git a/exercises/practice/perceptron/testtools.jl b/exercises/practice/perceptron/testtools.jl deleted file mode 100644 index edb21e15..00000000 --- a/exercises/practice/perceptron/testtools.jl +++ /dev/null @@ -1,43 +0,0 @@ -using Random - -function dotest(points, labels, hyperplane, reference) - # Tests if a hyperplane linearly separates labeled points - # Returns true or false - - points = vcat.(1, points) - test = reduce(hcat, points)' * hyperplane .* labels - if all(>(0), test) - println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! And the normal points towards the positively labeled side\n") - return true - elseif all(<(0), test) - println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! But the normal points towards the negatively labeled side\n") - return true - else - println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nThe sides are not properly separated...\n") - return false - end -end - -Random.seed!(42) # set seed for deterministic test set - -function population(n, bound) - # Builds a population of n points with labels {1, -1} in area bound x bound around a reference hyperplane - # Returns linearly separable points, labels and reference hyperplane - - vertical = !iszero(n % 10) #every tenth test has vertical reference hyperplane - x, y, b = rand(-bound:bound), rand(-bound:bound)*vertical, rand(-bound÷2:bound÷2) - y_intercept = -b ÷ (iszero(y) ? 1 : y) - points, labels, hyperplane = [], [], [b, x, y] - while n > 0 - # points are centered on y-intercept, but not x-intercept so distributions can be lopsided - point = [rand(-bound:bound), y_intercept + rand(-bound:bound)] - label = point' * [x, y] + b - if !iszero(label) - push!(points, point) - push!(labels, sign(label)) - n -= 1 - end - end - - points, labels, hyperplane -end \ No newline at end of file From 475efe316af502b0a7ac6f92a14f0a6a1c87aa5e Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Sun, 4 Feb 2024 08:09:56 -0500 Subject: [PATCH 06/26] Update config.json Better blurb --- exercises/practice/perceptron/.meta/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/perceptron/.meta/config.json b/exercises/practice/perceptron/.meta/config.json index 278526a3..713bd931 100644 --- a/exercises/practice/perceptron/.meta/config.json +++ b/exercises/practice/perceptron/.meta/config.json @@ -16,5 +16,5 @@ ".meta/example.jl" ] }, - "blurb": "Given points and their labels, provide a hyperplane which separates them" + "blurb": "Write your own machine learning classifier" } From 4a24b2c207cd168ac81bbbb5208c2060bcc1651a Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Sun, 4 Feb 2024 08:21:35 -0500 Subject: [PATCH 07/26] Update tests.toml Delete auto-generated comment as these were manually created --- exercises/practice/perceptron/.meta/tests.toml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/exercises/practice/perceptron/.meta/tests.toml b/exercises/practice/perceptron/.meta/tests.toml index 5c49832e..657cb537 100644 --- a/exercises/practice/perceptron/.meta/tests.toml +++ b/exercises/practice/perceptron/.meta/tests.toml @@ -1,14 +1,3 @@ -# This is an auto-generated file. -# -# Regenerating this file via `configlet sync` will: -# - Recreate every `description` key/value pair -# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications -# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) -# - Preserve any other key/value pair -# -# As user-added comments (using the # character) will be removed when this file -# is regenerated, comments can be added via a `comment` key. - [728853d3-24de-4855-a452-6520b67dec23] description = "Initial set" @@ -22,4 +11,4 @@ description = "Hyperplane cannot pass through origin" description = "Hyperplane nearly parallel with y-axis" [3e758bbd-5f72-447d-999f-cfa60b27bc26] -description = "Increasing Populations" \ No newline at end of file +description = "Increasing Populations" From e4760e2416b95ae0e115bb0e7dfd0d429208fce8 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Sun, 4 Feb 2024 10:52:02 -0500 Subject: [PATCH 08/26] Update instructions.md Updates to instructions including clarifications and an added gif from https://commons.wikimedia.org/wiki/File:Perceptron_training_without_bias.gif --- .../practice/perceptron/.docs/instructions.md | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/exercises/practice/perceptron/.docs/instructions.md b/exercises/practice/perceptron/.docs/instructions.md index 10a68d57..24f6ad6a 100644 --- a/exercises/practice/perceptron/.docs/instructions.md +++ b/exercises/practice/perceptron/.docs/instructions.md @@ -1,34 +1,50 @@ # Instructions ### Introduction -[Perceptron](https://en.wikipedia.org/wiki/Perceptron) is one of the oldest and bestestly named machine learning algorithms out there. Since perceptron is also quite simple to implement, it's a favorite place to start a machine learning journey. As a linear classifier, if a linear decision boundary (e.g. a line in 2D or hyperplane in general) can be drawn to separate two labled classes of objects, perceptron is guaranteed to do so. This can help in predicting what an unlabeled object would likely be classified as by seeing which side of the decision boundary it is on. +[Perceptron](https://en.wikipedia.org/wiki/Perceptron) is one of the oldest and bestestly named machine learning algorithms out there. Since it is also quite simple to implement, it's a favorite place to start a machine learning journey. Perceptron is what is known as a linear classifier, which means that, if we have two labled classes of objects, for example in 2D space, it will search for a line that can be drawn to separate them. If such a line exists, Perceptron is guaranteed to find one. See Perceptron in action separating black and white dots below! + +

+Perceptron training without bias +

### Details -The basic idea is fairly straightforward. We cycle through the objects and check if they are on the correct side of our hyperplane. If one is not, we make a correction to the hyperplane and continue checking the objects against the new hyperplane. Eventually the hyperplane is adjusted to correctly separate all the objects and we have our decision boundary! +The basic idea is fairly straightforward. As illustrated above, we cycle through the objects and check if they are on the correct side of our guess at a line. If one is not, we make a correction and continue checking the objects against the corrected line. Eventually the line is adjusted to correctly separate all the objects and we have what is called a decision boundary! + +Why is this of any use? The decision boundary found can then help us in predicting what a new, unlabeled, object would likely be classified as by seeing which side of the boundary it is on. #### A Brief Word on Hyperplanes -How do you pick your starting hyperplane? It's up to you! Be creative! Or not... Actually perceptron's convergence times are sensitive to conditions such as the initial hyperplane and even the order the objects are looped through, so you might not want to go too wild. +What we have been calling a line in 2D can be generalized to something called a [hyperplane](https://en.wikipedia.org/wiki/Hyperplane), which is a convenient representation, and, if you follow the classic Perceptron algorithm, you will have to pick an initial hyperplane to start with. How do you pick your starting hyperplane? It's up to you! Be creative! Or not... Actually perceptron's convergence times are sensitive to conditions such as the initial hyperplane and even the order the objects are looped through, so you might not want to go too wild. -We will be dealing with a two dimensional space, so our divider will be a line. The standard equation for a line is usually written as $y = ax+b$, where $a,b \in \Re$, however, in order to help generalize the idea to higher dimensions, it's convenient to reformulate this equation as $w_0 + w_1x + w_2y = 0$. This is the form of the [hyperplane](https://en.wikipedia.org/wiki/Hyperplane) we will be using, so your output should be $[w_0, w_1, w_2]$. In machine learning, ${w_0,w_1,w_2}$ are usually referred to as weights. +We will be playing in a two dimensional space, so our separating hyperplane will simply be a 1D line. You might remember the standard equation for a line as $y = ax+b$, where $a,b \in \Re$, however, in order to help generalize the idea to higher dimensions, it's convenient to reformulate this equation as $w_0 + w_1x + w_2y = 0$. This is the form of the hyperplane we will be using, so your output should be $[w_0, w_1, w_2]$. In machine learning, ${w_0,w_1,w_2}$ are usually referred to as weights. -While hyperplanes are equivalent under scalar multiplication, there is a difference between $[w_0, w_1, w_2]$ and $[-w_0, -w_1, -w_2]$ in that the normal to the hyperplane points in opposite directions. By convention, the perceptron normal points towards the class defined as positive, so this property will be checked but not result in a test failure. +Scaling a hyperplane by a positive value gives an equivalent hyperplane (e.g. $[w_0, w_1, w_2] = [\alpha \cdot w_0, \alpha \cdot w_1, \alpha \cdot w_2]$ with $\alpha > 0$). However, it should be noted that there is a difference between the normal vectors (the green arrow in illustration above) of hyperplanes scaled by a negative value (e.g. $[w_0, w_1, w_2]$ vs $[-w_0, -w_1, -w_2]$) in that the normal to the negative hyperplane points in opposite direction of that of the positive one. By convention, the perceptron normal points towards the class defined as positive, so this property will be checked but not result in a test failure. #### Updating Checking if an object is on one side of a hyperplane or another can be done by checking the normal vector which points to the object. The value will be positive, negative or zero, so all of the objects from a class should have normal vectors with the same sign. A zero value means the object is on the hyperplane, which we don't want to allow since its ambiguous. Checking the sign of a normal to a hyperplane might sound like it could be complicated, but it's actually quite easy. Simply plug in the coordinates for the object into the equation for the hyperplane and check the sign of the result. For example, we can look at two objects $v_1,v_2$ in relation to the hyperplane $[w_0, w_1, w_2] = [1, 1, 1]$: -$$v_1$$ $$[x_1, y_1] = [2, 2]$$ $$w_0 + w_1*x_1 + w_2*y_1 = 1 + 1*2 + 1*2 = 5 > 0$$ +$$\large v_1$$ + +$$[x_1, y_1] = [2, 2]$$ + +$$w_0 + w_1 \cdot x_1 + w_2 \cdot y_1 = 1 + 1 \cdot 2 + 1 \cdot 2 = 5 > 0$$ + + +$$\large v_2$$ +$$[x_2,y_2]=[-2,-2]$$ -$$v_2$$ $$[x_2,y_2]=[-2,-2]$$ $$w_0 + w_1*x_2 + w_2*y_2 = 1 + 1*(-2) + 1*(-2) = -3 < 0$$ +$$w_0 + w_1 \cdot x_2 + w_2 \cdot y_2 = 1 + 1 \cdot (-2) + 1 \cdot (-2) = -3 < 0$$ -If $v_1$ and $v_2$ have different labels, such as $1$ and $-1$ (like we will be using), then the hyperplane $[1, 1, 1]$ is a valid decision boundary for them. +If $v_1$ and $v_2$ have the labels $1$ and $-1$ (like we will be using), then the hyperplane $[1, 1, 1]$ is a valid decision boundary for them since the signs match. Now that we know how to tell which side of the hyperplane an object lies on, we can look at how perceptron updates a hyperplane. If an object is on the correct side of the hyperplane, no update is performed on the weights. However, if we find an object on the wrong side, the update rule for the weights is: -$$[w_0', w_1', w_2'] = [w_0 \pm l_{class}, w_1 \pm x*l_{class}, w_2 \pm y*l_{class}]$$ +$$[w_0', w_1', w_2'] = [w_0 + l_{class}, w_1 + x \cdot l_{class}, w_2 + y \cdot l_{class}]$$ -Where $l_{class}=\pm 1$, according to the class of the object (i.e. its label), $x,y$ are the coordinates of the object, the $w_i$ are the weights of the hyperplane and the $w_i'$ are the weights of the updated hyperplane. The plus or minus signs are homogenous, so either all plus or all minus, and are determined by the choice of which class you define to be on the positive side of the hyperplane. Beware that only two out of the four possible combinations of class on positive side of the hyperplane and the plus/minus in the update are valid ($\pm \pm, \mp \mp$), with the other two ($\pm \mp, \mp \pm$) leading to infinite loops. +Where $l_{class}=\pm 1$, according to the class of the object (i.e. its label), $x,y$ are the coordinates of the object, the $w_i$ are the weights of the hyperplane and the $w_i'$ are the weights of the updated hyperplane. This update is repeated for each object in turn, and then the whole process repeated until there are no updates made to the hyperplane. All objects passing without an update means they have been successfully separated and you can return your decision boundary! -Note: Although the perceptron algorithm is deterministic, a decision boundary depends on initialization and is not unique in general, so the tests accept any hyperplane which fully separates the objects. \ No newline at end of file +Notes: +- Although the perceptron algorithm is deterministic, a decision boundary depends on initialization and is not unique in general, so the tests accept any hyperplane which fully separates the objects. +- The tests here will only include linearly separable classes, so a decision boundary will always be possible (i.e. no need to worry about non-separable classes). From 39ba0fa5bbbdea9e9b31dea83a65cb855bd30c04 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Fri, 16 Feb 2024 08:48:31 -0500 Subject: [PATCH 09/26] Update instructions.md --- .../practice/perceptron/.docs/instructions.md | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/exercises/practice/perceptron/.docs/instructions.md b/exercises/practice/perceptron/.docs/instructions.md index 24f6ad6a..244d49bf 100644 --- a/exercises/practice/perceptron/.docs/instructions.md +++ b/exercises/practice/perceptron/.docs/instructions.md @@ -15,33 +15,29 @@ Why is this of any use? The decision boundary found can then help us in predicti #### A Brief Word on Hyperplanes What we have been calling a line in 2D can be generalized to something called a [hyperplane](https://en.wikipedia.org/wiki/Hyperplane), which is a convenient representation, and, if you follow the classic Perceptron algorithm, you will have to pick an initial hyperplane to start with. How do you pick your starting hyperplane? It's up to you! Be creative! Or not... Actually perceptron's convergence times are sensitive to conditions such as the initial hyperplane and even the order the objects are looped through, so you might not want to go too wild. -We will be playing in a two dimensional space, so our separating hyperplane will simply be a 1D line. You might remember the standard equation for a line as $y = ax+b$, where $a,b \in \Re$, however, in order to help generalize the idea to higher dimensions, it's convenient to reformulate this equation as $w_0 + w_1x + w_2y = 0$. This is the form of the hyperplane we will be using, so your output should be $[w_0, w_1, w_2]$. In machine learning, ${w_0,w_1,w_2}$ are usually referred to as weights. +We will be playing in a two dimensional space, so our separating hyperplane will simply be a 1D line. You might remember the standard equation for a line as `y = ax+b`, where `a,b ∈ R`, however, in order to help generalize the idea to higher dimensions, it's convenient to reformulate this equation as `w_0 + w_1x + w_2y = 0`. This is the form of the hyperplane we will be using, so your output should be `[w_0, w_1, w_2]`. In machine learning, the `{w_0, w_1, w_2}` are usually referred to as weights. -Scaling a hyperplane by a positive value gives an equivalent hyperplane (e.g. $[w_0, w_1, w_2] = [\alpha \cdot w_0, \alpha \cdot w_1, \alpha \cdot w_2]$ with $\alpha > 0$). However, it should be noted that there is a difference between the normal vectors (the green arrow in illustration above) of hyperplanes scaled by a negative value (e.g. $[w_0, w_1, w_2]$ vs $[-w_0, -w_1, -w_2]$) in that the normal to the negative hyperplane points in opposite direction of that of the positive one. By convention, the perceptron normal points towards the class defined as positive, so this property will be checked but not result in a test failure. +Scaling a hyperplane by a positive value gives an equivalent hyperplane (e.g. `[w_0, w_1, w_2] ≈ [c * w_0, c * w_1, c * w_2]` with `c > 0`), since an infinite line scaled by a value is just the same infinite line. However, it should be noted that there is a difference between the normal vectors (the green arrow in illustration above) associated with hyperplanes scaled by a negative value (e.g. `[w_0, w_1, w_2]` vs `[-w_0, -w_1, -w_2]`) in that the normal to the negative hyperplane points in opposite direction of that of the positive one. By convention, the perceptron normal points towards the class defined as positive. #### Updating -Checking if an object is on one side of a hyperplane or another can be done by checking the normal vector which points to the object. The value will be positive, negative or zero, so all of the objects from a class should have normal vectors with the same sign. A zero value means the object is on the hyperplane, which we don't want to allow since its ambiguous. Checking the sign of a normal to a hyperplane might sound like it could be complicated, but it's actually quite easy. Simply plug in the coordinates for the object into the equation for the hyperplane and check the sign of the result. For example, we can look at two objects $v_1,v_2$ in relation to the hyperplane $[w_0, w_1, w_2] = [1, 1, 1]$: +Checking if an object is on one side of a hyperplane or another can be done by checking the normal vector which points to the object. The value will be positive, negative or zero, so all of the objects from a class should have normal vectors with the same sign. A zero value means the object is on the hyperplane, which we don't want to allow since its ambiguous. Checking the sign of a normal to a hyperplane might sound like it could be complicated, but it's actually quite easy. Simply plug in the coordinates for the object into the equation for the hyperplane and check the sign of the result. For example, we can look at two objects `v_1, v_2` in relation to the hyperplane `[w_0, w_1, w_2] = [1, 1, 1]`: -$$\large v_1$$ +`v_1 = [x_1, y_1] = [2, 2]` -$$[x_1, y_1] = [2, 2]$$ +`w_0 + w_1 * x_1 + w_2 * y_1 = 1 + 1 * 2 + 1 * 2 = 5 > 0` -$$w_0 + w_1 \cdot x_1 + w_2 \cdot y_1 = 1 + 1 \cdot 2 + 1 \cdot 2 = 5 > 0$$ +`v_2 = [x_2, y_2] = [-2, -2]` -$$\large v_2$$ +`w_0 + w_1 * x_2 + w_2 * y_2 = 1 + 1 * (-2) + 1 * (-2) = -3 < 0` -$$[x_2,y_2]=[-2,-2]$$ - -$$w_0 + w_1 \cdot x_2 + w_2 \cdot y_2 = 1 + 1 \cdot (-2) + 1 \cdot (-2) = -3 < 0$$ - -If $v_1$ and $v_2$ have the labels $1$ and $-1$ (like we will be using), then the hyperplane $[1, 1, 1]$ is a valid decision boundary for them since the signs match. +If `v_1` and `v_2` have the labels `1` and `-1` (like we will be using), then the hyperplane `[1, 1, 1]` is a valid decision boundary for them since the signs match. Now that we know how to tell which side of the hyperplane an object lies on, we can look at how perceptron updates a hyperplane. If an object is on the correct side of the hyperplane, no update is performed on the weights. However, if we find an object on the wrong side, the update rule for the weights is: -$$[w_0', w_1', w_2'] = [w_0 + l_{class}, w_1 + x \cdot l_{class}, w_2 + y \cdot l_{class}]$$ +`[w_0', w_1', w_2'] = [w_0 + class, w_1 + x * class, w_2 + y * class]` -Where $l_{class}=\pm 1$, according to the class of the object (i.e. its label), $x,y$ are the coordinates of the object, the $w_i$ are the weights of the hyperplane and the $w_i'$ are the weights of the updated hyperplane. +Where `class ∈ {1,-1}`, according to the class of the object (i.e. its label), `x, y` are the coordinates of the object, the `w_i` are the weights of the hyperplane and the `w_i'` are the weights of the updated hyperplane. This update is repeated for each object in turn, and then the whole process repeated until there are no updates made to the hyperplane. All objects passing without an update means they have been successfully separated and you can return your decision boundary! From 735743f4be41e86de9c8202d9b7836a61d52cd6b Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:46:11 -0500 Subject: [PATCH 10/26] Update instructions.md --- .../practice/perceptron/.docs/instructions.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/exercises/practice/perceptron/.docs/instructions.md b/exercises/practice/perceptron/.docs/instructions.md index 244d49bf..f59d9ca6 100644 --- a/exercises/practice/perceptron/.docs/instructions.md +++ b/exercises/practice/perceptron/.docs/instructions.md @@ -15,29 +15,29 @@ Why is this of any use? The decision boundary found can then help us in predicti #### A Brief Word on Hyperplanes What we have been calling a line in 2D can be generalized to something called a [hyperplane](https://en.wikipedia.org/wiki/Hyperplane), which is a convenient representation, and, if you follow the classic Perceptron algorithm, you will have to pick an initial hyperplane to start with. How do you pick your starting hyperplane? It's up to you! Be creative! Or not... Actually perceptron's convergence times are sensitive to conditions such as the initial hyperplane and even the order the objects are looped through, so you might not want to go too wild. -We will be playing in a two dimensional space, so our separating hyperplane will simply be a 1D line. You might remember the standard equation for a line as `y = ax+b`, where `a,b ∈ R`, however, in order to help generalize the idea to higher dimensions, it's convenient to reformulate this equation as `w_0 + w_1x + w_2y = 0`. This is the form of the hyperplane we will be using, so your output should be `[w_0, w_1, w_2]`. In machine learning, the `{w_0, w_1, w_2}` are usually referred to as weights. +We will be playing in a two dimensional space, so our separating hyperplane will simply be a 1D line. You might remember the standard equation for a line as `y = ax+b`, where `a,b ∈ ℝ`, however, in order to help generalize the idea to higher dimensions, it's convenient to reformulate this equation as `w₀ + w₁x + w₂y = 0`. This is the form of the hyperplane we will be using, so your output should be `[w₀, w₁, w₂]`. In machine learning, the `{w₀, w₁, w₂}` are usually referred to as weights. -Scaling a hyperplane by a positive value gives an equivalent hyperplane (e.g. `[w_0, w_1, w_2] ≈ [c * w_0, c * w_1, c * w_2]` with `c > 0`), since an infinite line scaled by a value is just the same infinite line. However, it should be noted that there is a difference between the normal vectors (the green arrow in illustration above) associated with hyperplanes scaled by a negative value (e.g. `[w_0, w_1, w_2]` vs `[-w_0, -w_1, -w_2]`) in that the normal to the negative hyperplane points in opposite direction of that of the positive one. By convention, the perceptron normal points towards the class defined as positive. +Scaling a hyperplane by a positive value gives an equivalent hyperplane (e.g. `[w₀, w₁, w₂] ≈ [c⋅w₀, c⋅w₁, c⋅w₂]` with `c > 0`), since an infinite line scaled by a value is just the same infinite line. However, it should be noted that there is a difference between the normal vectors (the green arrow in illustration above) associated with hyperplanes scaled by a negative value (e.g. `[w₀, w₁, w₂]` vs `[-w₀, -w₁, -w₂]`) in that the normal to the negative hyperplane points in opposite direction of that of the positive one. By convention, the perceptron normal points towards the class defined as positive. #### Updating -Checking if an object is on one side of a hyperplane or another can be done by checking the normal vector which points to the object. The value will be positive, negative or zero, so all of the objects from a class should have normal vectors with the same sign. A zero value means the object is on the hyperplane, which we don't want to allow since its ambiguous. Checking the sign of a normal to a hyperplane might sound like it could be complicated, but it's actually quite easy. Simply plug in the coordinates for the object into the equation for the hyperplane and check the sign of the result. For example, we can look at two objects `v_1, v_2` in relation to the hyperplane `[w_0, w_1, w_2] = [1, 1, 1]`: +Checking if an object is on one side of a hyperplane or another can be done by checking the normal vector which points to the object. The value will be positive, negative or zero, so all of the objects from a class should have normal vectors with the same sign. A zero value means the object is on the hyperplane, which we don't want to allow since its ambiguous. Checking the sign of a normal to a hyperplane might sound like it could be complicated, but it's actually quite easy. Simply plug in the coordinates for the object into the equation for the hyperplane and check the sign of the result. For example, we can look at two objects `v₁, v₂` in relation to the hyperplane `[w₀, w₁, w₂] = [1, 1, 1]`: -`v_1 = [x_1, y_1] = [2, 2]` +`v₁ = [x₁, y₁] = [2, 2]` -`w_0 + w_1 * x_1 + w_2 * y_1 = 1 + 1 * 2 + 1 * 2 = 5 > 0` +`w₀ + w₁⋅x₁ + w₂⋅y₁ = 1 + 1⋅2 + 1⋅2 = 5 > 0` -`v_2 = [x_2, y_2] = [-2, -2]` +`v₂ = [x₂, y₂] = [-2, -2]` -`w_0 + w_1 * x_2 + w_2 * y_2 = 1 + 1 * (-2) + 1 * (-2) = -3 < 0` +`w₀ + w₁⋅x₂ + w₂⋅y₂ = 1 + 1⋅(-2) + 1⋅(-2) = -3 < 0` -If `v_1` and `v_2` have the labels `1` and `-1` (like we will be using), then the hyperplane `[1, 1, 1]` is a valid decision boundary for them since the signs match. +If `v₁` and `v₂` have the labels `1` and `-1` (like we will be using), then the hyperplane `[1, 1, 1]` is a valid decision boundary for them since the signs match. Now that we know how to tell which side of the hyperplane an object lies on, we can look at how perceptron updates a hyperplane. If an object is on the correct side of the hyperplane, no update is performed on the weights. However, if we find an object on the wrong side, the update rule for the weights is: -`[w_0', w_1', w_2'] = [w_0 + class, w_1 + x * class, w_2 + y * class]` +`[w₀', w₁', w₂'] = [w₀ + 1ₗ, w₁ + x⋅1ₗ, w₂ + y⋅1ₗ]` -Where `class ∈ {1,-1}`, according to the class of the object (i.e. its label), `x, y` are the coordinates of the object, the `w_i` are the weights of the hyperplane and the `w_i'` are the weights of the updated hyperplane. +Where `1ₗ ∈ {1, -1}`, according to the class of the object (i.e. its label), `x, y` are the coordinates of the object, the `wᵢ` are the weights of the hyperplane and the `wᵢ'` are the weights of the updated hyperplane. This update is repeated for each object in turn, and then the whole process repeated until there are no updates made to the hyperplane. All objects passing without an update means they have been successfully separated and you can return your decision boundary! From 121e5fa5c3fdd71cb895eb9cd214760414163ff2 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:21:42 -0500 Subject: [PATCH 11/26] Update exercises/practice/perceptron/perceptron.jl Co-authored-by: Victor Goff --- exercises/practice/perceptron/perceptron.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/perceptron/perceptron.jl b/exercises/practice/perceptron/perceptron.jl index 0945cdea..bb142478 100644 --- a/exercises/practice/perceptron/perceptron.jl +++ b/exercises/practice/perceptron/perceptron.jl @@ -1,3 +1,3 @@ function perceptron(points, labels) # Perceptronize! -end \ No newline at end of file +end From dd96c6c4f43af48346750d576abc93ed2b434492 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:22:09 -0500 Subject: [PATCH 12/26] Update exercises/practice/perceptron/.meta/example.jl Co-authored-by: Victor Goff --- exercises/practice/perceptron/.meta/example.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/perceptron/.meta/example.jl b/exercises/practice/perceptron/.meta/example.jl index 47a48073..f5438c74 100644 --- a/exercises/practice/perceptron/.meta/example.jl +++ b/exercises/practice/perceptron/.meta/example.jl @@ -5,4 +5,4 @@ function perceptron(points, labels) foreach(i -> labels[i]*θ'*pnts[i] ≤ 0 && (θ += labels[i]*pnts[i]), eachindex(pnts)) θ_0 == θ && return θ end -end \ No newline at end of file +end From 7bcaf8993d320b006652b529d5277ee2f3564410 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Sat, 17 Feb 2024 13:03:33 -0500 Subject: [PATCH 13/26] Update example.jl Separated out the functions to produce a more modular style. Added comments on functionality for added clarity. --- .../practice/perceptron/.meta/example.jl | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/exercises/practice/perceptron/.meta/example.jl b/exercises/practice/perceptron/.meta/example.jl index f5438c74..5bf4954e 100644 --- a/exercises/practice/perceptron/.meta/example.jl +++ b/exercises/practice/perceptron/.meta/example.jl @@ -1,8 +1,33 @@ +function classify(point, hyperplane) + # Takes a single point and a hyperplane + # Classifies which nomrmal of hyperplane is associated with the point + # Returns 1 for positive normal, -1 for negative normal and 0 for a point on the hyperplane + sign(hyperplane' * point) +end + +function update(point, label, hyperplane) + # Takes one point, its label and a hyperplane + # Updates the hyperplane conditional on classification not matching the label + # Returns the Perceptron updated hyperplane + (classify(point, hyperplane) != label) * label * point +end + +function step(points, labels, hyperplane) + # Takes a vector of points, a vector of their associated labels and a hyperplane + # Iteratively updates the hyperplane for each point/label pair + # Returns true/false if a valid decision boundary and the decision boundary/hyperplane + decisionboundary = hyperplane + foreach(i -> hyperplane += update(points[i], labels[i], hyperplane), eachindex(points)) + decisionboundary == hyperplane, hyperplane +end + function perceptron(points, labels) - θ, pnts = [0, 0, 0], vcat.(1, points) + # Takes a vector of linearly separable points and a vector of their associated labels + # Performs steps of the Perceptron algorithm until a valid decision boundary is found + # Returns a valid decision boundary for the provided population of labeled points + hyperplane, pnts = [0, 0, 0], vcat.(1, points) while true - θ_0 = θ - foreach(i -> labels[i]*θ'*pnts[i] ≤ 0 && (θ += labels[i]*pnts[i]), eachindex(pnts)) - θ_0 == θ && return θ + isdecisionboundary, hyperplane = step(pnts, labels, hyperplane) + isdecisionboundary && return hyperplane end end From 4ac9805293ef6b298d69baa79a10046dc83da67b Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Sat, 17 Feb 2024 13:30:10 -0500 Subject: [PATCH 14/26] Update config.json --- exercises/practice/perceptron/.meta/config.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/exercises/practice/perceptron/.meta/config.json b/exercises/practice/perceptron/.meta/config.json index 713bd931..f9c836dd 100644 --- a/exercises/practice/perceptron/.meta/config.json +++ b/exercises/practice/perceptron/.meta/config.json @@ -1,7 +1,5 @@ { - "authors": [ - "depial" - ], + "authors": [], "contributors": [ "cmcaine" ], @@ -16,5 +14,5 @@ ".meta/example.jl" ] }, - "blurb": "Write your own machine learning classifier" + "blurb": "Write your own machine learning algorithm" } From c8d226b3cf2e09b7ff665043b836020adb292b74 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:21:36 -0500 Subject: [PATCH 15/26] Update example.jl --- exercises/practice/perceptron/.meta/example.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/exercises/practice/perceptron/.meta/example.jl b/exercises/practice/perceptron/.meta/example.jl index 5bf4954e..ec30a831 100644 --- a/exercises/practice/perceptron/.meta/example.jl +++ b/exercises/practice/perceptron/.meta/example.jl @@ -8,26 +8,26 @@ end function update(point, label, hyperplane) # Takes one point, its label and a hyperplane # Updates the hyperplane conditional on classification not matching the label - # Returns the Perceptron updated hyperplane - (classify(point, hyperplane) != label) * label * point + # Returns a vector, the Perceptron updated hyperplane + hyperplane + (classify(point, hyperplane) != label) * label * point end function step(points, labels, hyperplane) # Takes a vector of points, a vector of their associated labels and a hyperplane # Iteratively updates the hyperplane for each point/label pair - # Returns true/false if a valid decision boundary and the decision boundary/hyperplane + # Returns a tuple: (true/false, decision boundary/hyperplane) for valid/invalid decision boundary, respectively decisionboundary = hyperplane - foreach(i -> hyperplane += update(points[i], labels[i], hyperplane), eachindex(points)) + foreach(i -> hyperplane = update(points[i], labels[i], hyperplane), eachindex(points)) decisionboundary == hyperplane, hyperplane end function perceptron(points, labels) # Takes a vector of linearly separable points and a vector of their associated labels # Performs steps of the Perceptron algorithm until a valid decision boundary is found - # Returns a valid decision boundary for the provided population of labeled points - hyperplane, pnts = [0, 0, 0], vcat.(1, points) + # Returns a vector, a valid decision boundary for the provided population of labeled points + hyperplane, points = [0, 0, 0], vcat.(1, points) while true - isdecisionboundary, hyperplane = step(pnts, labels, hyperplane) + isdecisionboundary, hyperplane = step(points, labels, hyperplane) isdecisionboundary && return hyperplane end end From fe7eb261faf6f539a64d825f0ee866d3903dff83 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Thu, 22 Feb 2024 12:04:21 -0500 Subject: [PATCH 16/26] Update runtests.jl --- exercises/practice/perceptron/runtests.jl | 74 +++++++++++++---------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/exercises/practice/perceptron/runtests.jl b/exercises/practice/perceptron/runtests.jl index 56363cc1..7a943760 100644 --- a/exercises/practice/perceptron/runtests.jl +++ b/exercises/practice/perceptron/runtests.jl @@ -1,44 +1,54 @@ using Test, Random include("perceptron.jl") +@testset "Boundary is a vector of three weights" begin + decisionboundary = perceptron([[-1,-1], [1, 0], [0, 1]], [-1, 1, 1]) + @test length(decisionboundary) == 3 +end + +@testset "Weights are Real numbers" begin + decisionboundary = perceptron([[-1,-1], [1, 0], [0, 1]], [-1, 1, 1]) + @test eltype(decisionboundary) <: Real +end + function runtestset() - @testset "Low population" begin - @testset "Initial set" begin - points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] - labels = [1, 1, -1, -1, 1, 1] - reference = [1, 2, 1] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) + @testset "Low populations" begin + @testset "Initial population" begin + points = [[-1, 0], [0, -1], [1, 0], [0, 1]] + labels = [-1, -1, 1, 1] + reference = [0, 1, 1] + decisionboundary = perceptron(points, labels) + @test isvalidboundary(points, labels, decisionboundary, reference) end - @testset "Initial set w/ opposite labels" begin - points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [1, 1]] - labels = [-1, -1, 1, 1, -1, -1] - reference = [-1, -2, -1] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) + @testset "Initial population w/ opposite labels" begin + points = [[-1, 0], [0, -1], [1, 0], [0, 1]] + labels = [1, 1, -1, -1] + reference = [0, -1, -1] + decisionboundary = perceptron(points, labels) + @test isvalidboundary(points, labels, decisionboundary, reference) end - @testset "Hyperplane cannot pass through origin" begin - points = [[1, 2], [3, 4], [-1, -2], [-3, -4], [2, 1], [-1, -1]] - labels = [1, 1, -1, -1, 1, 1] - reference = [-1, 3, 3] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) + @testset "Decision boundary cannot pass through origin" begin + points = [[1, 0], [0, 1], [2, 1], [1, 2]] + labels = [-1, -1, 1, 1] + reference = [-2, 1, 1] + decisionboundary = perceptron(points, labels) + @test isvalidboundary(points, labels, decisionboundary, reference) end - @testset "Hyperplane nearly parallel with y-axis" begin - points = [[0, 50], [0, -50], [-2, 0], [1, 50], [1, -50], [2, 0]] - labels = [-1, -1, -1, 1, 1, 1] - reference = [2, 0, -1] - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) + @testset "Decision boundary nearly parallel with y-axis" begin + points = [[0, 50], [0, -50], [1, 50], [1, -50]] + labels = [-1, -1, 1, 1] + reference = [-1, 2, 0] + decisionboundary = perceptron(points, labels) + @test isvalidboundary(points, labels, decisionboundary, reference) end end @testset "Increasing Populations" begin for n in 10:50 points, labels, reference = population(n, 25) - hyperplane = perceptron(points, labels) - @test dotest(points, labels, hyperplane, reference) + decisionboundary = perceptron(points, labels) + @test isvalidboundary(points, labels, decisionboundary, reference) end end @@ -67,17 +77,17 @@ function population(n, bound) points, labels, hyperplane end -function dotest(points, labels, hyperplane, reference) +function isvalidboundary(points, labels, hyperplane, reference) points = vcat.(1, points) test = reduce(hcat, points)' * hyperplane .* labels if all(>(0), test) - println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! And the normal points towards the positively labeled side\n") + println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! And the normal points towards the positively labeled class\n") return true elseif all(<(0), test) - println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! But the normal points towards the negatively labeled side\n") - return true + println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! But the normal points towards the negatively labeled class\n") + return false else - println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nThe sides are not properly separated...\n") + println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nThe classes are not properly separated\n") return false end end From bed28beb87d7276603bad26f1c14575ec5e4f6f4 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Thu, 22 Feb 2024 12:04:27 -0500 Subject: [PATCH 17/26] Update tests.toml --- exercises/practice/perceptron/.meta/tests.toml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/exercises/practice/perceptron/.meta/tests.toml b/exercises/practice/perceptron/.meta/tests.toml index 657cb537..d375c4cb 100644 --- a/exercises/practice/perceptron/.meta/tests.toml +++ b/exercises/practice/perceptron/.meta/tests.toml @@ -1,14 +1,20 @@ +[b8feac03-a063-44c9-8867-330cce110e6f] +description = "Boundary is a vector of three weights" + +[7220e861-e8f6-4d5f-b45a-8750e5146010] +description = "Weights are Real numbers" + [728853d3-24de-4855-a452-6520b67dec23] -description = "Initial set" +description = "Initial population" [ed5bf871-3923-47ca-8346-5d640f9069a0] -description = "Initial set w/ opposite labels" +description = "Initial population w/ opposite labels" [15a9860e-f9be-46b1-86b2-989bd878c8a5] -description = "Hyperplane cannot pass through origin" +description = "Decision boundary cannot pass through origin" [52ba77fc-8983-4429-91dc-e64b2f625484] -description = "Hyperplane nearly parallel with y-axis" +description = "Decision boundary nearly parallel with y-axis" [3e758bbd-5f72-447d-999f-cfa60b27bc26] description = "Increasing Populations" From 9d9ce4b030df5d5072c82c77e34614716deac589 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Thu, 22 Feb 2024 12:12:27 -0500 Subject: [PATCH 18/26] Update config.json --- config.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config.json b/config.json index 8fb1234e..e844f4ce 100644 --- a/config.json +++ b/config.json @@ -874,14 +874,6 @@ "variables" ] }, - { - "slug": "space-age", - "name": "Space Age", - "uuid": "f4bfe822-90cc-4183-8a50-081ff0fdb512", - "practices": [], - "prerequisites": [], - "difficulty": 2 - }, { "uuid": "b43a938a-7bd2-4fe4-b16c-731e2e25e747", "practices": [], @@ -896,6 +888,14 @@ "logic", "math" ] + }, + { + "slug": "space-age", + "name": "Space Age", + "uuid": "f4bfe822-90cc-4183-8a50-081ff0fdb512", + "practices": [], + "prerequisites": [], + "difficulty": 2 } ] }, From 76f21f2c56e07a91648b5cbf883823d5f1031c16 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:48:33 -0500 Subject: [PATCH 19/26] Create introduction.md --- .../practice/perceptron/.docs/introduction.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 exercises/practice/perceptron/.docs/introduction.md diff --git a/exercises/practice/perceptron/.docs/introduction.md b/exercises/practice/perceptron/.docs/introduction.md new file mode 100644 index 00000000..f1b8f8b8 --- /dev/null +++ b/exercises/practice/perceptron/.docs/introduction.md @@ -0,0 +1,20 @@ +# Introduction + +### Perceptron +[Perceptron](https://en.wikipedia.org/wiki/Perceptron) is one of the oldest and bestestly named machine learning algorithms out there. Since it is also quite simple to implement, it's a favorite place to start a machine learning journey. Perceptron is what is known as a linear classifier, which means that, if we have two labled classes of objects, for example in 2D space, it will search for a line that can be drawn to separate them. If such a line exists, Perceptron is guaranteed to find one. See Perceptron in action separating black and white dots below! + +

+Perceptron training without bias +

+ +### Details +The basic idea is fairly straightforward. As illustrated above, we cycle through the objects and check if they are on the correct side of our guess at a line. If one is not, we make a correction and continue checking the objects against the corrected line. Eventually the line is adjusted to correctly separate all the objects and we have what is called a decision boundary! + +Why is this of any use? The decision boundary found can then help us in predicting what a new, unlabeled, object would likely be classified as by seeing which side of the boundary it is on. + +#### A Brief Word on Hyperplanes +What we have been calling a line in 2D can be generalized to something called a [hyperplane](https://en.wikipedia.org/wiki/Hyperplane), which is a convenient representation, and, if you follow the classic Perceptron algorithm, you will have to pick an initial hyperplane to start with. How do you pick your starting hyperplane? It's up to you! Be creative! Or not... Actually perceptron's convergence times are sensitive to conditions such as the initial hyperplane and even the order the objects are looped through, so you might not want to go too wild. + +We will be playing in a two dimensional space, so our separating hyperplane will simply be a 1D line. You might remember the standard equation for a line as `y = ax+b`, where `a,b ∈ ℝ`, however, in order to help generalize the idea to higher dimensions, it's convenient to reformulate this equation as `w₀ + w₁x + w₂y = 0`. This is the form of the hyperplane we will be using, so your output should be `[w₀, w₁, w₂]`. In machine learning, the `{w₀, w₁, w₂}` are usually referred to as weights. + +Scaling a hyperplane by a positive value gives an equivalent hyperplane (e.g. `[w₀, w₁, w₂] ≈ [c⋅w₀, c⋅w₁, c⋅w₂]` with `c > 0`), since an infinite line scaled by a value is just the same infinite line. However, it should be noted that there is a difference between the normal vectors (the green arrow in illustration above) associated with hyperplanes scaled by a negative value (e.g. `[w₀, w₁, w₂]` vs `[-w₀, -w₁, -w₂]`) in that the normal to the negative hyperplane points in opposite direction of that of the positive one. By convention, the perceptron normal points towards the class defined as positive. From ff9ce68bea7b532fcce5e0fe6ab8ab64bfdc01e2 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:48:42 -0500 Subject: [PATCH 20/26] Update instructions.md --- .../practice/perceptron/.docs/instructions.md | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/exercises/practice/perceptron/.docs/instructions.md b/exercises/practice/perceptron/.docs/instructions.md index f59d9ca6..ca03062a 100644 --- a/exercises/practice/perceptron/.docs/instructions.md +++ b/exercises/practice/perceptron/.docs/instructions.md @@ -1,24 +1,5 @@ # Instructions -### Introduction -[Perceptron](https://en.wikipedia.org/wiki/Perceptron) is one of the oldest and bestestly named machine learning algorithms out there. Since it is also quite simple to implement, it's a favorite place to start a machine learning journey. Perceptron is what is known as a linear classifier, which means that, if we have two labled classes of objects, for example in 2D space, it will search for a line that can be drawn to separate them. If such a line exists, Perceptron is guaranteed to find one. See Perceptron in action separating black and white dots below! - -

-Perceptron training without bias -

- -### Details -The basic idea is fairly straightforward. As illustrated above, we cycle through the objects and check if they are on the correct side of our guess at a line. If one is not, we make a correction and continue checking the objects against the corrected line. Eventually the line is adjusted to correctly separate all the objects and we have what is called a decision boundary! - -Why is this of any use? The decision boundary found can then help us in predicting what a new, unlabeled, object would likely be classified as by seeing which side of the boundary it is on. - -#### A Brief Word on Hyperplanes -What we have been calling a line in 2D can be generalized to something called a [hyperplane](https://en.wikipedia.org/wiki/Hyperplane), which is a convenient representation, and, if you follow the classic Perceptron algorithm, you will have to pick an initial hyperplane to start with. How do you pick your starting hyperplane? It's up to you! Be creative! Or not... Actually perceptron's convergence times are sensitive to conditions such as the initial hyperplane and even the order the objects are looped through, so you might not want to go too wild. - -We will be playing in a two dimensional space, so our separating hyperplane will simply be a 1D line. You might remember the standard equation for a line as `y = ax+b`, where `a,b ∈ ℝ`, however, in order to help generalize the idea to higher dimensions, it's convenient to reformulate this equation as `w₀ + w₁x + w₂y = 0`. This is the form of the hyperplane we will be using, so your output should be `[w₀, w₁, w₂]`. In machine learning, the `{w₀, w₁, w₂}` are usually referred to as weights. - -Scaling a hyperplane by a positive value gives an equivalent hyperplane (e.g. `[w₀, w₁, w₂] ≈ [c⋅w₀, c⋅w₁, c⋅w₂]` with `c > 0`), since an infinite line scaled by a value is just the same infinite line. However, it should be noted that there is a difference between the normal vectors (the green arrow in illustration above) associated with hyperplanes scaled by a negative value (e.g. `[w₀, w₁, w₂]` vs `[-w₀, -w₁, -w₂]`) in that the normal to the negative hyperplane points in opposite direction of that of the positive one. By convention, the perceptron normal points towards the class defined as positive. - #### Updating Checking if an object is on one side of a hyperplane or another can be done by checking the normal vector which points to the object. The value will be positive, negative or zero, so all of the objects from a class should have normal vectors with the same sign. A zero value means the object is on the hyperplane, which we don't want to allow since its ambiguous. Checking the sign of a normal to a hyperplane might sound like it could be complicated, but it's actually quite easy. Simply plug in the coordinates for the object into the equation for the hyperplane and check the sign of the result. For example, we can look at two objects `v₁, v₂` in relation to the hyperplane `[w₀, w₁, w₂] = [1, 1, 1]`: From 9c8193d8b34cd3691e49066b1f65b26a723d5acc Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:49:46 -0500 Subject: [PATCH 21/26] Update runtests.jl Cleaned up obsolete functionality, and tried to include reporting of students' decision boundaries in testset titles, for easier debugging --- exercises/practice/perceptron/runtests.jl | 83 ++++++++++++----------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/exercises/practice/perceptron/runtests.jl b/exercises/practice/perceptron/runtests.jl index 7a943760..41388dc8 100644 --- a/exercises/practice/perceptron/runtests.jl +++ b/exercises/practice/perceptron/runtests.jl @@ -1,54 +1,65 @@ using Test, Random include("perceptron.jl") +decisionboundary = perceptron([[-1,-1], [1, 0], [0, 1]], [-1, 1, 1]) + @testset "Boundary is a vector of three weights" begin - decisionboundary = perceptron([[-1,-1], [1, 0], [0, 1]], [-1, 1, 1]) @test length(decisionboundary) == 3 end @testset "Weights are Real numbers" begin - decisionboundary = perceptron([[-1,-1], [1, 0], [0, 1]], [-1, 1, 1]) @test eltype(decisionboundary) <: Real end function runtestset() @testset "Low populations" begin - @testset "Initial population" begin - points = [[-1, 0], [0, -1], [1, 0], [0, 1]] - labels = [-1, -1, 1, 1] - reference = [0, 1, 1] - decisionboundary = perceptron(points, labels) - @test isvalidboundary(points, labels, decisionboundary, reference) + + # Initial population + points = [[-1, 0], [0, -1], [1, 0], [0, 1]] + labels = [-1, -1, 1, 1] + decisionboundary = perceptron(points, labels) + reference = [0, 1, 1] #A valid decision boundary need not match the reference + @testset "Initial population - Your decision boundary: $decisionboundary" begin + @test isvalidboundary(points, labels, decisionboundary) end - @testset "Initial population w/ opposite labels" begin - points = [[-1, 0], [0, -1], [1, 0], [0, 1]] - labels = [1, 1, -1, -1] - reference = [0, -1, -1] - decisionboundary = perceptron(points, labels) - @test isvalidboundary(points, labels, decisionboundary, reference) + + #Initial population w/ opposite labels + points = [[-1, 0], [0, -1], [1, 0], [0, 1]] + labels = [1, 1, -1, -1] + decisionboundary = perceptron(points, labels) + reference = [0, -1, -1] #A valid decision boundary need not match the reference + @testset "Initial population w/ opposite labels - Your decision boundary: $decisionboundary" begin + @test isvalidboundary(points, labels, decisionboundary) end - @testset "Decision boundary cannot pass through origin" begin - points = [[1, 0], [0, 1], [2, 1], [1, 2]] - labels = [-1, -1, 1, 1] - reference = [-2, 1, 1] - decisionboundary = perceptron(points, labels) - @test isvalidboundary(points, labels, decisionboundary, reference) + + # Decision boundary cannot pass through origin + points = [[1, 0], [0, 1], [2, 1], [1, 2]] + labels = [-1, -1, 1, 1] + decisionboundary = perceptron(points, labels) + reference = [-2, 1, 1] #A valid decision boundary need not match the reference + @testset "Decision boundary cannot pass through origin - Your decision boundary: $decisionboundary" begin + @test isvalidboundary(points, labels, decisionboundary) end - @testset "Decision boundary nearly parallel with y-axis" begin - points = [[0, 50], [0, -50], [1, 50], [1, -50]] - labels = [-1, -1, 1, 1] - reference = [-1, 2, 0] - decisionboundary = perceptron(points, labels) - @test isvalidboundary(points, labels, decisionboundary, reference) + + #Decision boundary nearly parallel with y-axis + points = [[0, 50], [0, -50], [1, 50], [1, -50]] + labels = [-1, -1, 1, 1] + decisionboundary = perceptron(points, labels) + reference = [-1, 2, 0] #A valid decision boundary need not match the reference + @testset "Decision boundary nearly parallel with y-axis - Your decision boundary: $decisionboundary" begin + @test isvalidboundary(points, labels, decisionboundary) end + end @testset "Increasing Populations" begin for n in 10:50 - points, labels, reference = population(n, 25) + points, labels = population(n, 25) decisionboundary = perceptron(points, labels) - @test isvalidboundary(points, labels, decisionboundary, reference) + @testset "Population: $n points - Your decision boundary: $decisionboundary" begin + @test isvalidboundary(points, labels, decisionboundary) + end end end @@ -74,22 +85,12 @@ function population(n, bound) end end - points, labels, hyperplane + points, labels end -function isvalidboundary(points, labels, hyperplane, reference) +function isvalidboundary(points, labels, decisionboundary) points = vcat.(1, points) - test = reduce(hcat, points)' * hyperplane .* labels - if all(>(0), test) - println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! And the normal points towards the positively labeled class\n") - return true - elseif all(<(0), test) - println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nSeparated! But the normal points towards the negatively labeled class\n") - return false - else - println("Reference hyperplane = $reference\nYour hyperplane = $hyperplane\nThe classes are not properly separated\n") - return false - end + all(>(0), reduce(hcat, points)' * decisionboundary .* labels) end Random.seed!(42) # set seed for deterministic test set From 5ac2eecb5aed2f847ceecc722a1ce5ae6a345189 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Fri, 23 Feb 2024 10:56:48 -0500 Subject: [PATCH 22/26] Update runtests.jl Reduced visibility/readability of code containing possible spoilers for students. --- exercises/practice/perceptron/runtests.jl | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/exercises/practice/perceptron/runtests.jl b/exercises/practice/perceptron/runtests.jl index 41388dc8..d380d462 100644 --- a/exercises/practice/perceptron/runtests.jl +++ b/exercises/practice/perceptron/runtests.jl @@ -66,16 +66,13 @@ function runtestset() end -function population(n, bound) - # Builds a population of n points with labels {1, -1} in area bound x bound around a reference hyperplane - # Returns linearly separable points, labels and reference hyperplane - vertical = !iszero(n % 10) #every tenth test has vertical reference hyperplane - x, y, b = rand(-bound:bound), rand(-bound:bound)*vertical, rand(-bound÷2:bound÷2) +function population(n, bound) + v = !iszero(n % 10) + x, y, b = rand(-bound:bound), rand(-bound:bound)*vertical, rand(-bound÷2:bound÷2)v y_intercept = -b ÷ (iszero(y) ? 1 : y) points, labels, hyperplane = [], [], [b, x, y] while n > 0 - # points are centered on y-intercept, but not x-intercept so distributions can be lopsided point = [rand(-bound:bound), y_intercept + rand(-bound:bound)] label = point' * [x, y] + b if !iszero(label) @@ -84,14 +81,9 @@ function population(n, bound) n -= 1 end end - points, labels end +isvalidboundary(points, labels, db) = all(>(0), reduce(hcat, vcat.(1, points))' * db .* labels) -function isvalidboundary(points, labels, decisionboundary) - points = vcat.(1, points) - all(>(0), reduce(hcat, points)' * decisionboundary .* labels) -end - -Random.seed!(42) # set seed for deterministic test set +Random.seed!(42) runtestset() From 7ec87e8e1fae28d75da734fd398f9818745e126d Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:16:27 -0500 Subject: [PATCH 23/26] Update runtests.jl --- exercises/practice/perceptron/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/perceptron/runtests.jl b/exercises/practice/perceptron/runtests.jl index d380d462..112dd2e5 100644 --- a/exercises/practice/perceptron/runtests.jl +++ b/exercises/practice/perceptron/runtests.jl @@ -69,7 +69,7 @@ end function population(n, bound) v = !iszero(n % 10) - x, y, b = rand(-bound:bound), rand(-bound:bound)*vertical, rand(-bound÷2:bound÷2)v + b, x, y = rand(-bound÷2:bound÷2), rand(-bound:bound), rand(-bound:bound)v y_intercept = -b ÷ (iszero(y) ? 1 : y) points, labels, hyperplane = [], [], [b, x, y] while n > 0 From 233f618e1af920bcf6887895623756897e64197f Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 12 Mar 2024 09:03:41 -0400 Subject: [PATCH 24/26] Adding new practice exercise Binary Search Tree --- .../binary-search-tree/.docs/instructions.md | 47 ++++++++++ .../binary-search-tree/.meta/config.json | 16 ++++ .../binary-search-tree/.meta/example.jl | 38 +++++++++ .../binary-search-tree/.meta/tests.toml | 40 +++++++++ .../binary-search-tree/binary-search-tree.jl | 2 + .../practice/binary-search-tree/runtests.jl | 85 +++++++++++++++++++ 6 files changed, 228 insertions(+) create mode 100644 exercises/practice/binary-search-tree/.docs/instructions.md create mode 100644 exercises/practice/binary-search-tree/.meta/config.json create mode 100644 exercises/practice/binary-search-tree/.meta/example.jl create mode 100644 exercises/practice/binary-search-tree/.meta/tests.toml create mode 100644 exercises/practice/binary-search-tree/binary-search-tree.jl create mode 100644 exercises/practice/binary-search-tree/runtests.jl diff --git a/exercises/practice/binary-search-tree/.docs/instructions.md b/exercises/practice/binary-search-tree/.docs/instructions.md new file mode 100644 index 00000000..c9bbba5b --- /dev/null +++ b/exercises/practice/binary-search-tree/.docs/instructions.md @@ -0,0 +1,47 @@ +# Instructions + +Insert and search for numbers in a binary tree. + +When we need to represent sorted data, an array does not make a good data structure. + +Say we have the array `[1, 3, 4, 5]`, and we add 2 to it so it becomes `[1, 3, 4, 5, 2]`. +Now we must sort the entire array again! +We can improve on this by realizing that we only need to make space for the new item `[1, nil, 3, 4, 5]`, and then adding the item in the space we added. +But this still requires us to shift many elements down by one. + +Binary Search Trees, however, can operate on sorted data much more efficiently. + +A binary search tree consists of a series of connected nodes. +Each node contains a piece of data (e.g. the number 3), a variable named `left`, and a variable named `right`. +The `left` and `right` variables point at `nil`, or other nodes. +Since these other nodes in turn have other nodes beneath them, we say that the left and right variables are pointing at subtrees. +All data in the left subtree is less than or equal to the current node's data, and all data in the right subtree is greater than the current node's data. + +For example, if we had a node containing the data 4, and we added the data 2, our tree would look like this: + + 4 + / + 2 + +If we then added 6, it would look like this: + + 4 + / \ + 2 6 + +If we then added 3, it would look like this + + 4 + / \ + 2 6 + \ + 3 + +And if we then added 1, 5, and 7, it would look like this + + 4 + / \ + / \ + 2 6 + / \ / \ + 1 3 5 7 diff --git a/exercises/practice/binary-search-tree/.meta/config.json b/exercises/practice/binary-search-tree/.meta/config.json new file mode 100644 index 00000000..d38e32cc --- /dev/null +++ b/exercises/practice/binary-search-tree/.meta/config.json @@ -0,0 +1,16 @@ +{ + "authors": [], + "files": { + "solution": [ + "binary-search-tree.jl" + ], + "test": [ + "runtests.jl" + ], + "example": [ + ".meta/example.jl" + ] + }, + "blurb": "Insert and search for numbers in a binary tree.", + "source": "Josh Cheek" +} diff --git a/exercises/practice/binary-search-tree/.meta/example.jl b/exercises/practice/binary-search-tree/.meta/example.jl new file mode 100644 index 00000000..936f2024 --- /dev/null +++ b/exercises/practice/binary-search-tree/.meta/example.jl @@ -0,0 +1,38 @@ +mutable struct BinarySearchTree + data + left + right + BinarySearchTree(node::T) where T<:Real = new(node, nothing, nothing) +end + +function BinarySearchTree(vec::Vector{T}) where T<:Real + tree = BinarySearchTree(popfirst!(vec)) + foreach(node -> push!(tree, node), vec) + tree +end + +function Base.in(node, tree::BinarySearchTree) + tree.data == node && return true + if node ≤ tree.data + isnothing(tree.left) ? false : in(node, tree.left) + else + isnothing(tree.right) ? false : in(node, tree.right) + end +end + +function Base.push!(tree::BinarySearchTree, node) + if node ≤ tree.data + isnothing(tree.left) ? (tree.left = BinarySearchTree(node)) : push!(tree.left, node) + else + isnothing(tree.right) ? (tree.right = BinarySearchTree(node)) : push!(tree.right, node) + end + tree +end + +function traverse(tree::BinarySearchTree, channel::Channel) + !isnothing(tree.left) && traverse(tree.left, channel) + put!(channel, tree.data) + !isnothing(tree.right) && traverse(tree.right, channel) +end + +Base.sort(tree::BinarySearchTree) = collect(Channel(channel -> traverse(tree, channel))) diff --git a/exercises/practice/binary-search-tree/.meta/tests.toml b/exercises/practice/binary-search-tree/.meta/tests.toml new file mode 100644 index 00000000..c7d32021 --- /dev/null +++ b/exercises/practice/binary-search-tree/.meta/tests.toml @@ -0,0 +1,40 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[e9c93a78-c536-4750-a336-94583d23fafa] +description = "data is retained" + +[7a95c9e8-69f6-476a-b0c4-4170cb3f7c91] +description = "insert data at proper node -> smaller number at left node" + +[22b89499-9805-4703-a159-1a6e434c1585] +description = "insert data at proper node -> same number at left node" + +[2e85fdde-77b1-41ed-b6ac-26ce6b663e34] +description = "insert data at proper node -> greater number at right node" + +[dd898658-40ab-41d0-965e-7f145bf66e0b] +description = "can create complex tree" + +[9e0c06ef-aeca-4202-b8e4-97f1ed057d56] +description = "can sort data -> can sort single number" + +[425e6d07-fceb-4681-a4f4-e46920e380bb] +description = "can sort data -> can sort if second number is smaller than first" + +[bd7532cc-6988-4259-bac8-1d50140079ab] +description = "can sort data -> can sort if second number is same as first" + +[b6d1b3a5-9d79-44fd-9013-c83ca92ddd36] +description = "can sort data -> can sort if second number is greater than first" + +[d00ec9bd-1288-4171-b968-d44d0808c1c8] +description = "can sort data -> can sort complex tree" diff --git a/exercises/practice/binary-search-tree/binary-search-tree.jl b/exercises/practice/binary-search-tree/binary-search-tree.jl new file mode 100644 index 00000000..f95b91b8 --- /dev/null +++ b/exercises/practice/binary-search-tree/binary-search-tree.jl @@ -0,0 +1,2 @@ +# Create a (Mutable) Struct BinarySearchTree, which has fields: data, left, right +# Also write a sort method, which returns a sorted array of the elements in a BinarySearchTree \ No newline at end of file diff --git a/exercises/practice/binary-search-tree/runtests.jl b/exercises/practice/binary-search-tree/runtests.jl new file mode 100644 index 00000000..b60fd3a7 --- /dev/null +++ b/exercises/practice/binary-search-tree/runtests.jl @@ -0,0 +1,85 @@ +using Test +include("binary-search-tree.jl") + +@testset "data is retained" begin + tree = BinarySearchTree([4]) + @test tree.data == 4 + @test isnothing(tree.left) + @test isnothing(tree.right) +end + +@testset "insert data at proper node" begin + @testset "smaller number at left node" begin + tree = BinarySearchTree([4, 2]) + @test tree.data == 4 + @test tree.left.data == 2 + @test isnothing(tree.left.left) + @test isnothing(tree.left.right) + @test isnothing(tree.right) + end + + @testset "same number at left node" begin + tree = BinarySearchTree([4, 4]) + @test tree.data == 4 + @test tree.left.data == 4 + @test isnothing(tree.left.left) + @test isnothing(tree.left.right) + @test isnothing(tree.right) + end + + @testset "greater number at right node" begin + tree = BinarySearchTree([4, 5]) + @test tree.data == 4 + @test isnothing(tree.left) + @test tree.right.data == 5 + @test isnothing(tree.right.left) + @test isnothing(tree.right.right) + end +end + +@testset "can create complex tree" begin + tree = BinarySearchTree([4, 2, 6, 1, 3, 5, 7]) + @test tree.data == 4 + @test tree.left.data == 2 + @test tree.left.left.data == 1 + @test isnothing(tree.left.left.left) + @test isnothing(tree.left.left.right) + @test tree.left.right.data == 3 + @test isnothing(tree.left.right.left) + @test isnothing(tree.left.right.right) + @test tree.right.data == 6 + @test tree.right.left.data == 5 + @test isnothing(tree.right.left.left) + @test isnothing(tree.right.left.right) + @test tree.right.right.data == 7 + @test isnothing(tree.right.right.left) + @test isnothing(tree.right.right.right) + +end + +@testset "can sort data" begin + @testset "can sort single number" begin + tree = BinarySearchTree([2]) + @test sort(tree) == [2] + end + + @testset "can sort if second number is smaller than first" begin + tree = BinarySearchTree([2, 1]) + @test sort(tree) == [1, 2] + end + + @testset "can sort if second number is same as first" begin + tree = BinarySearchTree([2, 2]) + @test sort(tree) == [2, 2] + end + + @testset "can sort if second number is greater than first" begin + tree = BinarySearchTree([2, 3]) + @test sort(tree) == [2, 3] + end + + @testset "can sort complex tree" begin + tree = BinarySearchTree([4, 2, 6, 1, 3, 5, 7]) + @test sort(tree) == [1, 2, 3, 4, 5, 6, 7] + end +end From f4443594043f4478fc9a16b8cb9c55235ebf6ec5 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 12 Mar 2024 09:30:08 -0400 Subject: [PATCH 25/26] config changes --- config.json | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index e844f4ce..88b2a8fe 100644 --- a/config.json +++ b/config.json @@ -377,6 +377,18 @@ "searching" ] }, + { + "slug": "binary-search-tree", + "name": "Binary Search Tree", + "uuid": "00246993-4917-41f0-8289-b299b9964aba", + "practices": [], + "prerequisites": [], + "difficulty": 7, + "topics": [ + "structs", + "data_structures" + ] + }, { "slug": "atbash-cipher", "name": "Atbash Cipher", @@ -875,11 +887,11 @@ ] }, { + "slug": "perceptron", + "name": "Perceptron", "uuid": "b43a938a-7bd2-4fe4-b16c-731e2e25e747", "practices": [], "prerequisites": [], - "slug": "perceptron", - "name": "Perceptron", "difficulty": 3, "topics": [ "machine learning", @@ -896,6 +908,14 @@ "practices": [], "prerequisites": [], "difficulty": 2 + }, + { + "slug": "binary-search-tree", + "name": "Binary Search Tree", + "uuid": "f5df0c95-30c7-40f3-8ec6-fcfb3e28cdf4", + "practices": [], + "prerequisites": [], + "difficulty": 1 } ] }, From c947630c74a6715fe03e227aeb844a2d514c1142 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:27:10 -0400 Subject: [PATCH 26/26] Update config.json Deleted redundant entry for binary-search-tree --- config.json | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/config.json b/config.json index 3a63974a..86763fa4 100644 --- a/config.json +++ b/config.json @@ -377,18 +377,6 @@ "searching" ] }, - { - "slug": "binary-search-tree", - "name": "Binary Search Tree", - "uuid": "00246993-4917-41f0-8289-b299b9964aba", - "practices": [], - "prerequisites": [], - "difficulty": 7, - "topics": [ - "structs", - "data_structures" - ] - }, { "slug": "atbash-cipher", "name": "Atbash Cipher", @@ -923,7 +911,11 @@ "uuid": "f5df0c95-30c7-40f3-8ec6-fcfb3e28cdf4", "practices": [], "prerequisites": [], - "difficulty": 1 + "difficulty": 7, + "topics": [ + "structs", + "data_structures" + ] } ] },