Skip to content

Commit fee99f2

Browse files
authored
Merge pull request #2 from kortirso/feature/linear_regression_gradient_descent
Feature/linear regression gradient descent
2 parents 167fc95 + ac6d085 commit fee99f2

12 files changed

Lines changed: 213 additions & 90 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## Unreleased
8+
### Modified
9+
- Linear Regression, fit with gradient descent
10+
711
## [0.1.2] - 2018-11-22
812
### Added
913
- CHANGELOG.md file

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@ Initialize predictor with data:
3333
predictor = Linear.new([1, 2, 3, 4], [3, 6, 10, 15])
3434
```
3535

36-
Fit data set:
36+
Fit data set with least squares method:
3737

3838
```elixir
3939
predictor = predictor |> Linear.fit
4040
```
4141

42+
Fit data set with gradient descent method:
43+
44+
```elixir
45+
predictor = predictor |> Linear.fit([method: "gradient descent"])
46+
```
47+
4248
Predict using the linear model:
4349

4450
```elixir

lib/learn_kit/knn.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,7 @@ defmodule LearnKit.Knn do
9797

9898
def classify(%Knn{data_set: data_set}, options \\ []) do
9999
try do
100-
unless Keyword.has_key?(options, :feature) do
101-
raise "Feature option is required"
102-
end
100+
unless Keyword.has_key?(options, :feature), do: raise "Feature option is required"
103101
# modification of options
104102
options = Keyword.merge([k: 3, algorithm: "brute", weight: "uniform"], options)
105103
# prediction

lib/learn_kit/knn/classify.ex

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ defmodule LearnKit.Knn.Classify do
22
@moduledoc """
33
Module for knn classify functions
44
"""
5+
6+
alias LearnKit.Math
7+
58
defmacro __using__(_opts) do
69
quote do
710
defp prediction(data_set, options) do
@@ -34,7 +37,8 @@ defmodule LearnKit.Knn.Classify do
3437
defp calc_feature_weights(features, options) do
3538
features
3639
|> Enum.map(fn feature ->
37-
Tuple.append(feature, calc_feature_weight(Keyword.get(options, :weight), elem(feature, 0)))
40+
feature
41+
|> Tuple.append(calc_feature_weight(Keyword.get(options, :weight), elem(feature, 0)))
3842
end)
3943
end
4044

@@ -78,9 +82,7 @@ defmodule LearnKit.Knn.Classify do
7882
features
7983
|> Enum.reduce([], fn feature, acc ->
8084
distance = feature |> calc_distance_between_features(current_feature)
81-
if distance == 0 do
82-
raise "Feature exists in train data set with label #{key}"
83-
end
85+
if distance == 0, do: raise "Feature exists in train data set with label #{key}"
8486
acc = [{distance, key} | acc]
8587
end)
8688
end
@@ -92,19 +94,15 @@ defmodule LearnKit.Knn.Classify do
9294
defp calc_distance_between_points(acc, feature_from_data_set, feature, current_index, size) when current_index <= size do
9395
Enum.at(feature_from_data_set, current_index) - Enum.at(feature, current_index)
9496
|> :math.pow(2)
95-
|> summ(acc)
97+
|> Math.summ(acc)
9698
|> calc_distance_between_points(feature_from_data_set, feature, current_index + 1, size)
9799
end
98100

99-
defp calc_distance_between_points(acc, _feature_from_data_set, _feature, _current_index, _size) do
101+
defp calc_distance_between_points(acc, _, _, _, _) do
100102
acc
101103
|> :math.sqrt
102104
end
103105

104-
defp summ(a, b) do
105-
a + b
106-
end
107-
108106
defp calc_feature_weight(weight, distance) do
109107
case weight do
110108
"uniform" -> 1
@@ -117,7 +115,7 @@ defmodule LearnKit.Knn.Classify do
117115
acc
118116
end
119117

120-
defp accumulate_weight_of_labels([{_distance, key, weight} | tail], acc) do
118+
defp accumulate_weight_of_labels([{_, key, weight} | tail], acc) do
121119
previous = if Keyword.has_key?(acc, key), do: Keyword.get(acc, key), else: 0
122120
acc = Keyword.put(acc, key, previous + weight)
123121
accumulate_weight_of_labels(tail, acc)

lib/learn_kit/math.ex

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,21 @@ defmodule LearnKit.Math do
66
@type row :: [number]
77
@type matrix :: [row]
88

9+
@doc """
10+
Sum of 2 numbers
11+
12+
## Examples
13+
14+
iex> LearnKit.Math.summ(1, 2)
15+
3
16+
17+
"""
18+
@spec summ(number, number) :: number
19+
20+
def summ(a, b) do
21+
a + b
22+
end
23+
924
@doc """
1025
Calculate the mean from a list of numbers
1126
@@ -125,11 +140,43 @@ defmodule LearnKit.Math do
125140
defp swap_rows_cols([head | _]) when head == [], do: []
126141

127142
defp swap_rows_cols(rows) do
128-
firsts = Enum.map(rows, fn(x) -> hd(x) end)
129-
others = Enum.map(rows, fn(x) -> tl(x) end)
143+
firsts = Enum.map(rows, fn x -> hd(x) end)
144+
others = Enum.map(rows, fn x -> tl(x) end)
130145
[firsts | swap_rows_cols(others)]
131146
end
132147

148+
@doc """
149+
Scalar multiplication
150+
151+
## Examples
152+
153+
iex> LearnKit.Math.scalar_multiply(10, [5, 6])
154+
[50, 60]
155+
156+
"""
157+
@spec scalar_multiply(integer, list) :: list
158+
159+
def scalar_multiply(multiplicator, list) when is_list(list) do
160+
list
161+
|> Enum.map(fn x -> x * multiplicator end)
162+
end
163+
164+
@doc """
165+
Vector subtraction
166+
167+
## Examples
168+
169+
iex> LearnKit.Math.vector_subtraction([40, 50, 60], [35, 5, 40])
170+
[5, 45, 20]
171+
172+
"""
173+
@spec vector_subtraction(list, list) :: list
174+
175+
def vector_subtraction(x, y) when is_list(x) and is_list(y) and length(x) == length(y) do
176+
Enum.zip(x, y)
177+
|> Enum.map(fn {xi, yi} -> xi - yi end)
178+
end
179+
133180
@doc """
134181
Division for 2 elements
135182
@@ -162,8 +209,7 @@ defmodule LearnKit.Math do
162209
size = length(x)
163210

164211
Enum.zip(x, y)
165-
|> Enum.map(fn {xi, yi} -> (xi - mean_x) * (yi - mean_y) end)
166-
|> Enum.sum
212+
|> Enum.reduce(0, fn {xi, yi}, acc -> acc + (xi - mean_x) * (yi - mean_y) end)
167213
|> division(size - 1)
168214
end
169215

@@ -182,15 +228,9 @@ defmodule LearnKit.Math do
182228
mean_x = mean(x)
183229
mean_y = mean(y)
184230

185-
divider = Enum.zip(x, y)
186-
|> Enum.map(fn {xi, yi} -> (xi - mean_x) * (yi - mean_y) end)
187-
|> Enum.sum
188-
denom_x = x
189-
|> Enum.map(fn xi -> :math.pow(xi - mean_x, 2) end)
190-
|> Enum.sum
191-
denom_y = y
192-
|> Enum.map(fn yi -> :math.pow(yi - mean_y, 2) end)
193-
|> Enum.sum
231+
divider = Enum.zip(x, y) |> Enum.reduce(0, fn {xi, yi}, acc -> acc + (xi - mean_x) * (yi - mean_y) end)
232+
denom_x = x |> Enum.reduce(0, fn xi, acc -> acc + :math.pow(xi - mean_x, 2) end)
233+
denom_y = y |> Enum.reduce(0, fn yi, acc -> acc + :math.pow(yi - mean_y, 2) end)
194234

195235
divider / :math.sqrt(denom_x * denom_y)
196236
end

lib/learn_kit/naive_bayes/gaussian/classify.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule LearnKit.NaiveBayes.Gaussian.Classify do
77
# classify data
88
# returns data like [label1: 0.03592747361085857, label2: 0.00399309643713954]
99
defp classify_data(fit_data, feature) do
10-
labels_count = length(Keyword.keys(fit_data))
10+
labels_count = fit_data |> Keyword.keys |> length
1111
fit_data
1212
|> Enum.map(fn {label, fit_results} ->
1313
{label, class_probability(labels_count, feature, fit_results)}
@@ -23,7 +23,7 @@ defmodule LearnKit.NaiveBayes.Gaussian.Classify do
2323
end
2424

2525
# multiply together the feature probabilities for all of the features in a label for given values
26-
defp feature_mult([], _fit_results, acc, _index), do: acc
26+
defp feature_mult([], _, acc, _), do: acc
2727

2828
defp feature_mult([head | tail], fit_results, acc, index) do
2929
acc = acc * feature_probability(index, head, fit_results)
@@ -38,7 +38,7 @@ defmodule LearnKit.NaiveBayes.Gaussian.Classify do
3838
if fit_result.mean == value, do: 1.0, else: 0.0
3939
else
4040
# calculate the gaussian probability
41-
exp = - :math.pow((value - fit_result.mean), 2) / (2 * fit_result.variance)
41+
exp = - :math.pow(value - fit_result.mean, 2) / (2 * fit_result.variance)
4242
:math.exp(exp) / :math.sqrt(2 * :math.pi * fit_result.variance)
4343
end
4444
end

lib/learn_kit/regression/linear.ex

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ defmodule LearnKit.Regression.Linear do
77

88
alias LearnKit.Regression.Linear
99

10-
use Linear.Fit
11-
use Linear.Predict
10+
use Linear.Calculations
1211

1312
@type factors :: [number]
1413
@type results :: [number]
@@ -55,6 +54,11 @@ defmodule LearnKit.Regression.Linear do
5554
## Parameters
5655
5756
- predictor: %LearnKit.Regression.Linear{}
57+
- options: keyword list with options
58+
59+
## Options
60+
61+
- method: method for fit, "least squares"/"gradient descent", default is "least squares", optional
5862
5963
## Examples
6064
@@ -65,11 +69,28 @@ defmodule LearnKit.Regression.Linear do
6569
results: [3, 6, 10, 15]
6670
}
6771
72+
iex> predictor = predictor |> LearnKit.Regression.Linear.fit([method: "gradient descent"])
73+
%LearnKit.Regression.Linear{
74+
coefficients: [-1.4975720508482548, 3.9992148848913356],
75+
factors: [1, 2, 3, 4],
76+
results: [3, 6, 10, 15]
77+
}
78+
6879
"""
6980
@spec fit(%Linear{factors: factors, results: results}) :: %Linear{factors: factors, results: results, coefficients: coefficients}
7081

71-
def fit(%Linear{factors: factors, results: results}) do
72-
%Linear{factors: factors, results: results, coefficients: fit_data(factors, results)}
82+
def fit(%Linear{factors: factors, results: results}, options \\ []) do
83+
coefficients = Keyword.merge([method: ""], options)
84+
|> define_method_for_fit
85+
|> fit_data(factors, results)
86+
%Linear{factors: factors, results: results, coefficients: coefficients}
87+
end
88+
89+
defp define_method_for_fit(options) do
90+
case Keyword.get(options, :method) do
91+
"gradient descent" -> "gradient descent"
92+
_ -> ""
93+
end
7394
end
7495

7596
@doc """
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
defmodule LearnKit.Regression.Linear.Calculations do
2+
@moduledoc """
3+
Module for fit functions
4+
"""
5+
6+
alias LearnKit.Math
7+
8+
defmacro __using__(_opts) do
9+
quote do
10+
defp fit_data(method, factors, results) when method == "gradient descent" do
11+
gradient_descent_iteration([:rand.uniform, :rand.uniform], 0.0001, nil, 1000000, Enum.zip(factors, results), 0)
12+
end
13+
14+
defp fit_data(_, factors, results) do
15+
beta = Math.correlation(factors, results) * Math.standard_deviation(results) / Math.standard_deviation(factors)
16+
alpha = Math.mean(results) - beta * Math.mean(factors)
17+
[alpha, beta]
18+
end
19+
20+
defp predict_sample(sample, [alpha, beta]) do
21+
sample * beta + alpha
22+
end
23+
24+
defp calculate_score([], _, _), do: raise("There was no fit for model")
25+
26+
defp calculate_score(coefficients, factors, results) do
27+
1.0 - sum_of_squared_errors(coefficients, factors, results) / total_sum_of_squares(results)
28+
end
29+
30+
defp total_sum_of_squares(list) do
31+
mean_list = Math.mean(list)
32+
list
33+
|> Enum.reduce(0, fn x, acc -> acc + :math.pow(x - mean_list, 2) end)
34+
end
35+
36+
defp sum_of_squared_errors(coefficients, factors, results) do
37+
Enum.zip(factors, results)
38+
|> Enum.reduce(0, fn {xi, yi}, acc -> acc + squared_prediction_error(coefficients, xi, yi) end)
39+
end
40+
41+
defp squared_prediction_error(coefficients, x, y) do
42+
coefficients
43+
|> prediction_error(x, y)
44+
|> :math.pow(2)
45+
end
46+
47+
defp squared_error_gradient(coefficients, x, y) do
48+
error_variable = coefficients |> prediction_error(x, y)
49+
[
50+
-2 * error_variable,
51+
-2 * error_variable * x
52+
]
53+
end
54+
55+
defp prediction_error(coefficients, x, y) do
56+
y - predict_sample(x, coefficients)
57+
end
58+
59+
defp gradient_descent_iteration(_, _, min_theta, _, _, iterations_with_no_improvement) when iterations_with_no_improvement >= 100, do: min_theta
60+
61+
defp gradient_descent_iteration(theta, alpha, min_theta, min_value, data, iterations_with_no_improvement) do
62+
[
63+
min_theta,
64+
min_value,
65+
iterations_with_no_improvement,
66+
alpha
67+
] = data |> check_value(min_value, theta, min_theta, iterations_with_no_improvement, alpha)
68+
69+
theta = data
70+
|> Enum.shuffle
71+
|> Enum.reduce(theta, fn {xi, yi}, acc ->
72+
gradient_i = squared_error_gradient(acc, xi, yi)
73+
acc |> Math.vector_subtraction(alpha |> Math.scalar_multiply(gradient_i))
74+
end)
75+
gradient_descent_iteration(theta, alpha, min_theta, min_value, data, iterations_with_no_improvement)
76+
end
77+
78+
defp check_value(data, min_value, theta, min_theta, iterations_with_no_improvement, alpha) do
79+
value = data |> Enum.reduce(0, fn {xi, yi}, acc -> acc + squared_prediction_error(theta, xi, yi) end)
80+
cond do
81+
value < min_value ->
82+
[theta, value, 0, 0.0001]
83+
true ->
84+
[min_theta, min_value, iterations_with_no_improvement + 1, alpha * 0.9]
85+
end
86+
end
87+
end
88+
end
89+
end

lib/learn_kit/regression/linear/fit.ex

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)