Skip to content

Commit 3127891

Browse files
committed
Add K-Nearest Neighbors classifier
1 parent 7c934ad commit 3127891

2 files changed

Lines changed: 426 additions & 0 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package com.thealgorithms.machinelearning;
2+
3+
import java.util.ArrayList;
4+
import java.util.Comparator;
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
/**
10+
* K-Nearest Neighbors (KNN) classifier.
11+
*
12+
* <p>K-Nearest Neighbors is a supervised machine learning algorithm that
13+
* classifies a sample based on the majority class among its {@code k}
14+
* nearest training samples using the Euclidean distance metric.
15+
*
16+
* <p>The classifier stores the training dataset during the fitting phase and
17+
* predicts class labels for new samples without building an explicit model.
18+
*
19+
* @see <a href="https://en.wikipedia.org/wiki/K-nearest_neighbors_algorithm">
20+
* K-Nearest Neighbors</a>
21+
*/
22+
public final class KNearestNeighbors {
23+
private final int k;
24+
25+
/**
26+
* Constructs a K-Nearest Neighbors classifier with the specified number
27+
* of neighbors.
28+
*
29+
* @param k the number of nearest neighbors to consider during prediction
30+
*/
31+
public KNearestNeighbors(int k) {
32+
33+
if (k <= 0) {
34+
throw new IllegalArgumentException("k must be greater than 0.");
35+
}
36+
37+
this.k = k;
38+
}
39+
40+
/**
41+
* Represents a neighboring training sample and its distance from the test sample.
42+
*/
43+
private static final class Neighbor {
44+
45+
private final double distance;
46+
47+
private final int label;
48+
49+
Neighbor(double distance, int label) {
50+
this.distance = distance;
51+
this.label = label;
52+
}
53+
}
54+
55+
private double[][] trainingFeatures;
56+
private int[] trainingLabels;
57+
private int numFeatures;
58+
59+
/**
60+
* Fits the classifier using the provided training dataset.
61+
*
62+
* <p>The training feature vectors and their corresponding class labels are
63+
* stored for use during prediction.
64+
*
65+
* @param features the training feature vectors
66+
* @param labels the corresponding class labels
67+
*/
68+
public void fit(double[][] features, int[] labels) {
69+
70+
if (features == null || labels == null) {
71+
throw new IllegalArgumentException("Features and labels cannot be null.");
72+
}
73+
74+
if (features.length == 0 || labels.length == 0) {
75+
throw new IllegalArgumentException("Features and labels cannot be empty.");
76+
}
77+
78+
if (features.length != labels.length) {
79+
throw new IllegalArgumentException("Features and labels must have the same length.");
80+
}
81+
82+
if (features[0] == null) {
83+
throw new IllegalArgumentException("Feature vectors cannot be null.");
84+
}
85+
86+
numFeatures = features[0].length;
87+
88+
if (numFeatures == 0) {
89+
throw new IllegalArgumentException("Feature vectors cannot be empty.");
90+
}
91+
92+
for (double[] sample : features) {
93+
if (sample == null) {
94+
throw new IllegalArgumentException("Feature vectors cannot be null.");
95+
}
96+
97+
if (sample.length != numFeatures) {
98+
throw new IllegalArgumentException("All feature vectors must have the same dimension.");
99+
}
100+
}
101+
102+
this.trainingFeatures = features;
103+
this.trainingLabels = labels;
104+
}
105+
106+
/**
107+
* Computes the Euclidean distance between two feature vectors.
108+
*
109+
* @param first the first feature vector
110+
* @param second the second feature vector
111+
* @return the Euclidean distance between the two vectors
112+
*/
113+
private static double euclideanDistance(double[] first, double[] second) {
114+
double sum = 0.0;
115+
116+
for (int i = 0; i < first.length; i++) {
117+
double difference = first[i] - second[i];
118+
sum += difference * difference;
119+
}
120+
121+
return Math.sqrt(sum);
122+
}
123+
124+
/**
125+
* Predicts the class label for a single sample.
126+
*
127+
* <p>The prediction is made by finding the {@code k} nearest neighbors
128+
* among the training samples and selecting the class with the highest
129+
* number of votes. In the event of a tie, the smaller class label is
130+
* returned.
131+
*
132+
* @param testPoint the sample to classify
133+
* @return the predicted class label
134+
*/
135+
public int predict(double[] testPoint) {
136+
if (trainingFeatures == null || trainingLabels == null) {
137+
throw new IllegalStateException("Classifier has not been fitted.");
138+
}
139+
140+
if(testPoint == null) {
141+
throw new IllegalArgumentException("Sample cannot be null.");
142+
}
143+
144+
if (testPoint.length != numFeatures) {
145+
throw new IllegalArgumentException("Sample length must match training feature count.");
146+
}
147+
148+
List<Neighbor> neighbors = new ArrayList<>(trainingFeatures.length);
149+
150+
for (int i = 0; i < trainingFeatures.length; i++) {
151+
double distance = euclideanDistance(trainingFeatures[i], testPoint);
152+
neighbors.add(new Neighbor(distance, trainingLabels[i]));
153+
}
154+
155+
156+
neighbors.sort(Comparator.comparingDouble(neighbor -> neighbor.distance));
157+
158+
Map<Integer, Integer> votes = new HashMap<>();
159+
160+
if (k > trainingFeatures.length) {
161+
throw new IllegalArgumentException("k cannot be greater than the number of training samples.");
162+
}
163+
164+
for (int i = 0; i < k; i++) {
165+
int label = neighbors.get(i).label;
166+
votes.merge(label, 1, Integer::sum);
167+
}
168+
169+
int predictedLabel = -1;
170+
int maxVotes = -1;
171+
172+
for (Map.Entry<Integer, Integer> entry : votes.entrySet()) {
173+
int label = entry.getKey();
174+
int count = entry.getValue();
175+
176+
if (count > maxVotes || (count == maxVotes && label < predictedLabel)) {
177+
maxVotes = count;
178+
predictedLabel = label;
179+
}
180+
}
181+
182+
return predictedLabel;
183+
}
184+
185+
186+
/**
187+
* Predicts class labels for multiple samples.
188+
*
189+
* @param samples the samples to classify
190+
* @return an array containing the predicted class label for each sample
191+
*/
192+
public int[] predict(double[][] samples) {
193+
194+
if (samples == null) {
195+
throw new IllegalArgumentException("Samples cannot be null.");
196+
}
197+
198+
int[] predictions = new int[samples.length];
199+
200+
for (int i = 0; i < samples.length; i++) {
201+
predictions[i] = predict(samples[i]);
202+
}
203+
204+
return predictions;
205+
}
206+
}

0 commit comments

Comments
 (0)