1 """This module provides a k-Nearest-Neighbour classifier."""
2
3
4
5
6
7
8
9
10 """k-Nearest-Neighbour classifier."""
11
12 __docformat__ = 'restructuredtext'
13
14
15 import numpy as N
16
17 from mvpa.base import warning
18 from mvpa.misc.support import indentDoc
19 from mvpa.clfs.base import Classifier
20 from mvpa.base.dochelpers import enhancedDocString
21 from mvpa.clfs.distance import squared_euclidean_distance
22
23 if __debug__:
24 from mvpa.base import debug
25
26
27 -class kNN(Classifier):
28 """
29 k-Nearest-Neighbour classifier.
30
31 This is a simple classifier that bases its decision on the distances
32 between the training dataset samples and the test sample(s). Distances
33 are computed using a customizable distance function. A certain number
34 (`k`)of nearest neighbors is selected based on the smallest distances
35 and the labels of this neighboring samples are fed into a voting
36 function to determine the labels of the test sample.
37
38 Training a kNN classifier is extremely quick, as no actuall training
39 is performed as the training dataset is simply stored in the
40 classifier. All computations are done during classifier prediction.
41
42 .. note::
43 If enabled, kNN stores the votes per class in the 'values' state after
44 calling predict().
45
46 """
47
48 _clf_internals = ['knn', 'non-linear', 'binary', 'multiclass',
49 'notrain2predict' ]
50
53 """
54 :Parameters:
55 k: unsigned integer
56 Number of nearest neighbours to be used for voting.
57 dfx: functor
58 Function to compute the distances between training and test samples.
59 Default: squared euclidean distance
60 voting: str
61 Voting method used to derive predictions from the nearest neighbors.
62 Possible values are 'majority' (simple majority of classes
63 determines vote) and 'weighted' (votes are weighted according to the
64 relative frequencies of each class in the training data).
65 **kwargs:
66 Additonal arguments are passed to the base class.
67 """
68
69
70 Classifier.__init__(self, **kwargs)
71
72 self.__k = k
73 self.__dfx = dfx
74 self.__voting = voting
75 self.__data = None
76
77
79 """Representation of the object
80 """
81 return "kNN(k=%d, dfx=%s enable_states=%s)" % \
82 (self.__k, self.__dfx, str(self.states.enabled))
83
84
88
89
91 """Train the classifier.
92
93 For kNN it is degenerate -- just stores the data.
94 """
95 self.__data = data
96 if __debug__:
97 if str(data.samples.dtype).startswith('uint') \
98 or str(data.samples.dtype).startswith('int'):
99 warning("kNN: input data is in integers. " + \
100 "Overflow on arithmetic operations might result in"+\
101 " errors. Please convert dataset's samples into" +\
102 " floating datatype if any error is reported.")
103 self.__weights = None
104
105
106 uniquelabels = data.uniquelabels
107 self.__votes_init = dict(zip(uniquelabels,
108 [0] * len(uniquelabels)))
109
110
112 """Predict the class labels for the provided data.
113
114 Returns a list of class labels (one for each data sample).
115 """
116
117 data = N.asarray(data)
118
119
120 if __debug__:
121 if not data.ndim == 2:
122 raise ValueError, "Data array must be two-dimensional."
123
124 if not data.shape[1] == self.__data.nfeatures:
125 raise ValueError, "Length of data samples (features) does " \
126 "not match the classifier."
127
128
129
130
131 dists = self.__dfx(self.__data.samples, data).T
132
133
134 knns = dists.argsort(axis=1)[:, :self.__k]
135
136
137 predicted = []
138 votes = []
139
140 if self.__voting == 'majority':
141 vfx = self.getMajorityVote
142 elif self.__voting == 'weighted':
143 vfx = self.getWeightedVote
144 else:
145 raise ValueError, "kNN told to perform unknown voting '%s'." % self.__voting
146
147
148 results = [vfx(knn) for knn in knns]
149
150
151 predicted = [r[0] for r in results]
152
153
154
155 self.predictions = predicted
156 self.values = [r[1] for r in results]
157
158 return predicted
159
160
162 """Simple voting by choosing the majority of class neighbours.
163 """
164
165 uniquelabels = self.__data.uniquelabels
166
167
168 knn_labels = N.array([ self.__data.labels[nn] for nn in knn_ids ])
169
170
171 votes = self.__votes_init.copy()
172 for nn in knn_ids:
173 votes[self.__labels[nn]] += 1
174
175
176
177 return uniquelabels[N.asarray(votes).argmax()], \
178 votes
179
180
182 """Vote with classes weighted by the number of samples per class.
183 """
184 uniquelabels = self.__data.uniquelabels
185
186
187 if self.__weights is None:
188
189
190
191
192 self.__labels = self.__data.labels
193 Nlabels = len(self.__labels)
194 Nuniquelabels = len(uniquelabels)
195
196
197
198
199
200
201
202 self.__weights = \
203 [ 1.0 - ((self.__labels == label).sum() / Nlabels) \
204 for label in uniquelabels ]
205 self.__weights = dict(zip(uniquelabels, self.__weights))
206
207
208
209 votes = self.__votes_init.copy()
210 for nn in knn_ids:
211 votes[self.__labels[nn]] += 1
212
213
214 votes = [ self.__weights[ul] * votes[ul] for ul in uniquelabels]
215
216
217
218 return uniquelabels[N.asarray(votes).argmax()], \
219 votes
220
221
223 """Reset trained state"""
224 self.__data = None
225 super(kNN, self).untrain()
226