Skip to content

Commit 740f5c5

Browse files
committed
normalization can be selected for KNN
1 parent f9bbe1a commit 740f5c5

7 files changed

Lines changed: 133 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
88
### Added
99
- preprocessing normalization for data set
1010

11+
### Modified
12+
- normalization can be selected for KNN
13+
1114
## [0.1.4] - 2018-12-17
1215
### Modified
1316
- errors conditions and tests for KNN

lib/learn_kit/knn.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ defmodule LearnKit.Knn do
8585
- k: number of nearest neighbours, default is 3, optional
8686
- algorithm: brute, optional
8787
- weight: uniform/distance, default is uniform, optional
88+
- normalization: none/minimax/z_normalization, default is none, optional
8889
8990
## Examples
9091
@@ -106,7 +107,7 @@ defmodule LearnKit.Knn do
106107
{:error, "K option must be positive integer"}
107108

108109
true ->
109-
options = Keyword.merge([k: 3, algorithm: "brute", weight: "uniform"], options)
110+
options = Keyword.merge([k: 3, algorithm: "brute", weight: "uniform", normalization: "none"], options)
110111
{label, _} = prediction(data_set, options)
111112
{:ok, label}
112113
end

lib/learn_kit/knn/classify.ex

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,39 @@ defmodule LearnKit.Knn.Classify do
33
Module for knn classify functions
44
"""
55

6-
alias LearnKit.Math
6+
alias LearnKit.{Preprocessing, Math}
77

88
defmacro __using__(_opts) do
99
quote do
1010
defp prediction(data_set, options) do
1111
data_set
12+
|> filter_features_by_size(Keyword.get(options, :feature))
13+
|> check_normalization(options)
1214
|> calc_distances_for_features(options)
1315
|> sort_distances()
1416
|> select_closest_features(options)
1517
|> check_zero_distance(options)
1618
end
1719

20+
# knn uses only features with the same size as current feature
21+
defp filter_features_by_size(data_set, current_feature) do
22+
Enum.map(data_set, fn {key, features} ->
23+
{
24+
key,
25+
Enum.filter(features, fn feature -> length(feature) == length(current_feature) end)
26+
}
27+
end)
28+
end
29+
30+
# normalize features
31+
defp check_normalization(data_set, options) do
32+
type = Keyword.get(options, :normalization)
33+
case type do
34+
t when t in ["minimax", "z_normalization"] -> normalize(data_set, options, type)
35+
_ -> data_set
36+
end
37+
end
38+
1839
# select algorithm for prediction
1940
defp calc_distances_for_features(data_set, options) do
2041
case Keyword.get(options, :algorithm) do
@@ -23,14 +44,17 @@ defmodule LearnKit.Knn.Classify do
2344
end
2445
end
2546

47+
# sort distances
2648
defp sort_distances(features) do
2749
Enum.sort(features, &(elem(&1, 0) <= elem(&2, 0)))
2850
end
2951

52+
# take closest features
3053
defp select_closest_features(features, options) do
3154
Enum.take(features, Keyword.get(options, :k))
3255
end
3356

57+
# check existeness of current feature in data set
3458
defp check_zero_distance(closest_features, options) do
3559
{distance, label} = Enum.at(closest_features, 0)
3660
cond do
@@ -39,13 +63,33 @@ defmodule LearnKit.Knn.Classify do
3963
end
4064
end
4165

66+
# select best result based on weights
4267
defp select_best_label(features, options) do
4368
features
4469
|> calc_feature_weights(options)
4570
|> accumulate_weight_of_labels([])
4671
|> sort_result()
4772
end
4873

74+
# normalize each feature
75+
defp normalize(data_set, options, type) do
76+
coefficients = find_coefficients_for_normalization(data_set, type)
77+
Enum.map(data_set, fn {key, features} ->
78+
{
79+
key,
80+
Enum.map(features, fn feature -> Preprocessing.normalize_feature(feature, coefficients, type) end)
81+
}
82+
end)
83+
end
84+
85+
# find coefficients for normalization
86+
defp find_coefficients_for_normalization(data_set, type) do
87+
Enum.reduce(data_set, [], fn {_, features}, acc ->
88+
Enum.reduce(features, acc, fn feature, acc -> [feature | acc] end)
89+
end)
90+
|> Preprocessing.coefficients(type)
91+
end
92+
4993
defp calc_feature_weights(features, options) do
5094
Enum.map(features, fn feature ->
5195
Tuple.append(feature, calc_feature_weight(Keyword.get(options, :weight), elem(feature, 0)))
@@ -70,17 +114,10 @@ defmodule LearnKit.Knn.Classify do
70114
Enum.map(keys, fn key ->
71115
data_set
72116
|> Keyword.get(key)
73-
|> filter_features_by_size(current_feature)
74117
|> calc_distances_in_label(current_feature, key)
75118
end)
76119
end
77120

78-
defp filter_features_by_size(features, current_feature) do
79-
Enum.filter(features, fn feature ->
80-
length(feature) == length(current_feature)
81-
end)
82-
end
83-
84121
defp calc_distances_in_label(features, current_feature, key) do
85122
Enum.reduce(features, [], fn feature, acc ->
86123
distance = calc_distance_between_features(feature, current_feature)

lib/learn_kit/preprocessing.ex

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule LearnKit.Preprocessing do
33
Module for data preprocessing
44
"""
55

6-
alias LearnKit.Preprocessing
6+
alias LearnKit.{Preprocessing, Math}
77

88
use Preprocessing.Normalize
99

@@ -64,4 +64,57 @@ defmodule LearnKit.Preprocessing do
6464
_ -> normalization(features, "minimax")
6565
end
6666
end
67+
68+
@doc """
69+
Prepare coefficients for normalization
70+
71+
## Parameters
72+
73+
- features: features grouped by index
74+
- type: minimax/z_normalization
75+
76+
## Examples
77+
78+
iex> LearnKit.Preprocessing.coefficients([[1, 2], [3, 4], [5, 6]], "minimax")
79+
[{1, 5}, {2, 6}]
80+
81+
iex> LearnKit.Preprocessing.coefficients([[1, 2], [3, 4], [5, 6]], "z_normalization")
82+
[{3.0, 1.632993161855452}, {4.0, 1.632993161855452}]
83+
84+
"""
85+
@spec coefficients(matrix, String.t()) :: matrix
86+
87+
def coefficients(features, type) when is_list(features) and is_binary(type) do
88+
features
89+
|> Math.transpose()
90+
|> Enum.map(fn list -> return_params(list, type) end)
91+
end
92+
93+
@doc """
94+
Normalize 1 feature with predefined coefficients
95+
96+
## Parameters
97+
98+
- feature: feature for normalization
99+
- coefficients: predefined coefficients
100+
- type: minimax/z_normalization
101+
102+
## Examples
103+
104+
iex> LearnKit.Preprocessing.normalize_feature([1, 2], [{1, 5}, {2, 6}], "minimax")
105+
[0.0, 0.0]
106+
107+
"""
108+
@spec normalize_feature(list, list(tuple), String.t()) :: list
109+
110+
def normalize_feature(feature, coefficients, type) when is_list(feature) and is_list(coefficients) and is_binary(type) do
111+
Enum.zip(feature, coefficients)
112+
|> Enum.map(fn {point, params_for_point} ->
113+
divider = define_divider(params_for_point, type)
114+
case divider do
115+
0 -> point
116+
_ -> (point - elem(params_for_point, 0)) / divider
117+
end
118+
end)
119+
end
67120
end

lib/learn_kit/preprocessing/normalize.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ defmodule LearnKit.Preprocessing.Normalize do
88
defmacro __using__(_opts) do
99
quote do
1010
defp normalization(features, type) do
11-
features_by_index = Math.transpose(features)
12-
list_of_params = Enum.map(features_by_index, fn list -> return_params(list, type) end)
13-
features_by_index
11+
list_of_params = coefficients(features, type)
12+
features
13+
|> Math.transpose()
1414
|> Enum.with_index()
1515
|> Enum.map(fn {feature, index} -> transform_feature(feature, Enum.at(list_of_params, index), type) end)
1616
|> Math.transpose()

test/learn_kit/knn_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,13 @@ defmodule LearnKit.KnnTest do
6363
test "classify new feature, for existed point", state do
6464
assert {:ok, :b1} = Knn.classify(state[:classifier], [feature: [-2, -2], k: 3, weight: "uniform"])
6565
end
66+
67+
test "classify new feature, minimax normalization", state do
68+
assert {:ok, :a1} = Knn.classify(state[:classifier], [feature: [-1, -2], k: 3, weight: "distance", normalization: "minimax"])
69+
end
70+
71+
test "classify new feature, z normalization", state do
72+
assert {:ok, :a1} = Knn.classify(state[:classifier], [feature: [-1, -2], k: 3, weight: "distance", normalization: "z_normalization"])
73+
end
6674
end
6775
end

test/learn_kit/preprocessing_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ defmodule LearnKit.PreprocessingTest do
1818
end
1919

2020
describe "for valid data" do
21+
test "prepare coefficients for normalization, minimax" do
22+
result = Preprocessing.coefficients([[1, 2], [3, 4], [5, 6]], "minimax")
23+
24+
assert result == [{1, 5}, {2, 6}]
25+
end
26+
27+
test "prepare coefficients for normalization, z_normalization" do
28+
result = Preprocessing.coefficients([[1, 2], [3, 4], [5, 6]], "z_normalization")
29+
30+
assert result == [{3.0, 1.632993161855452}, {4.0, 1.632993161855452}]
31+
end
32+
33+
test "normalize 1 feature with predefined coefficients" do
34+
result = Preprocessing.normalize_feature([1, 2], [{1, 5}, {2, 6}], "minimax")
35+
36+
assert result == [0.0, 0.0]
37+
end
38+
2139
test "normalize data set with minimax normalization" do
2240
result = Preprocessing.normalize([[1, 2], [3, 4], [5, 6]])
2341

0 commit comments

Comments
 (0)