1 """
2 This package is an implementation of a novel improvement to KNN which
3 speeds up query times
4 """
5 import math,random,sys,bisect,time
6 import numpy,scipy.spatial.distance,scipy.spatial.kdtree
7 import cProfile,pstats,gendata
8
10
11
12
13 tmp = (u-v)
14 return math.sqrt(numpy.dot(tmp,tmp))
15
17 """
18 A class which implements the KNN learning algorithm with sped up
19 query times.
20
21 This class follows the conventions of other classes in the qstklearn
22 module, with a constructor that initializes basic parameters and
23 bookkeeping variables, an 'addEvidence' method for adding labeled
24 training data individually or as a batch, and a 'query' method
25 that returns an estimated class for an unlabled point. Training
26 and testing data are in the form of numpy arrays, and classes are
27 discrete.
28
29 In order to speed up query times, this class keeps a number of lists which
30 sort the training data by distance to 'anchor' points. The lists aren't
31 sorted until the first call to the 'query' method, after which, the lists
32 are kept in sorted order. Initial sort is done using pythons 'sort'
33 (samplesort), and sorted insertions with 'insort' from the bisect module.
34 """
36 """
37 Creates a new FastKNN object that will use the given number of
38 anchors.
39 """
40 self.num_anchors = num_anchors
41 self.training_data = list()
42 self.anchors = list()
43 self.data_by_anchors = dict()
44 self.data_classes = dict()
45 self.is_sorted = False
46
47 self.distfun = adistfun
48 self.num_checks = 0
49 self.kdt = None
50 self.k = k
51
53 """
54 Picks a new set of anchors. The anchor lists will be re-sorted upon
55 the next call to 'query'.
56
57 selection_type - the method to use when selecting new anchor points.
58 'random' performs a random permutation of the training points and
59 picks the first 'num_anchors' as new anchors.
60 """
61 if selection_type == 'random':
62 self.anchors = range(len(self.training_data))
63 random.shuffle(self.anchors)
64 self.anchors = self.anchors[0:self.num_anchors]
65 self.kdt = scipy.spatial.kdtree.KDTree(numpy.array(self.training_data)[self.anchors,:])
66 self.is_sorted = False
67
68
70 """
71 Adds to the set of training data. If the anchor lists were sorted
72 before the call to this method, the new data will be inserted into
73 the anchor lists using 'bisect.insort'
74
75 data - a numpy array, either a single point (1D) or a set of
76 points (2D)
77
78 label - the label for data. A single value, or a list of values
79 in the same order as the points in data.
80 """
81 if len(data.shape)==1:
82 new_idx = len(self.training_data)
83 self.training_data.append(data)
84 self.data_classes[new_idx] = label
85 if self.is_sorted:
86 for a in self.anchors:
87 dist = self.distfun(data,self.training_data[a])
88 bisect.insort(self.data_by_anchors[a],(dist,new_idx))
89 elif len(data.shape)>1:
90 for i in xrange(len(data)):
91 thing = data[i]
92 new_idx = len(self.training_data)
93 self.training_data.append(thing)
94 self.data_classes[new_idx] = label[i]
95 if self.is_sorted:
96 for a in self.anchors:
97 dist = self.distfun(thing,self.training_data[a])
98 bisect.insort(self.data_by_anchors[a],(dist,new_idx))
99
100 - def query(self,point,k=None,method='mode',slow=False,dumdumcheck=False):
101 """
102 Returns class value for an unlabled point by examining its k nearest
103 neighbors. 'method' determines how the class of the unlabled point is
104 determined.
105 """
106 if k is None:
107 k = self.k
108
109 if len(self.anchors) < self.num_anchors:
110 self.resetAnchors()
111 if not self.is_sorted:
112 for a in self.anchors:
113 self.data_by_anchors[a] = [ ( self.distfun(self.training_data[datai],self.training_data[a]), datai) for datai in range(len(self.training_data))]
114 self.data_by_anchors[a].sort(key=lambda pnt: pnt[0])
115
116
117
118
119
120
121
122
123
124
125
126 res = self.kdt.query(numpy.array([point,]))
127 anchor = self.anchors[res[1][0]]
128 anchor_dist = res[0][0]
129
130
131
132 anchor_list = self.data_by_anchors[anchor]
133 neighbors = list()
134 maxd = None
135 maxd_idx = 0
136 for i in xrange(0,len(anchor_list)):
137 nextpnt_dist = self.distfun(point,self.training_data[anchor_list[i][1]])
138 self.num_checks += 1
139 nextthing = (nextpnt_dist,anchor_list[i][1])
140
141
142
143
144
145
146
147
148
149 if len(neighbors)<k:
150 if (maxd is None) or (maxd < nextpnt_dist):
151 maxd = nextpnt_dist
152 maxd_idx = len(neighbors)
153 neighbors.append(nextthing)
154 elif nextpnt_dist < maxd:
155 neighbors[maxd_idx] = nextthing
156 maxthing = max(neighbors)
157 maxd_idx = neighbors.index(maxthing)
158 maxd = maxthing[0]
159 if not(slow) and len(neighbors) >= k:
160 if anchor_dist + maxd < anchor_list[i][0]:
161 break
162
163
164
165 if method == 'mode':
166 class_count = dict()
167 for n in neighbors:
168 nid = n[1]
169 clss = self.data_classes[nid]
170 if clss in class_count:
171 tmp = class_count[clss]
172 else:
173 tmp = 0
174 class_count[clss] = tmp+1
175 bleh = max(class_count.iteritems(),key=lambda item:item[1])
176 if dumdumcheck and bleh[1] == 1:
177 print "aHAH!"
178 print point
179 rv = bleh[0]
180 elif method == 'mean':
181 return sum([self.data_classes[n[1]] for n in neighbors])/float(k)
182
183
184 return rv
185
187 foo = open(fname)
188 bar = [line for line in foo]
189 foo.close()
190
191 bar = bar[1:]
192
193 return [map(float,thing.strip().split(';')) for thing in bar]
194
196 wqred = dataifywine('wine/winequality-red.csv') + dataifywine('wine/winequality-white.csv')
197 leftoutperc = 0.1
198 leftout = int(len(wqred)*leftoutperc)
199 testing = wqred[:leftout]
200 training = wqred[leftout:]
201 print "Training:",len(training)
202 print "Testing:",len(testing)
203 foo = FastKNN(10)
204 foo.addEvidence(numpy.array([thing[:-1] for thing in training]), [thing[-1] for thing in training])
205 knn.addEvidence(numpy.array(training))
206 total = 0
207 correct = 0
208 for x in xrange(len(testing)):
209 thing = testing[x]
210 guess = foo.query(numpy.array(thing[:-1]),3)
211
212
213
214
215 if guess == thing[-1]:
216 correct += 1
217 total += 1
218 if total % 50 == 0:
219 print total,'/',len(testing)
220 print correct,"/",total,":",float(correct)/float(total)
221 print "Average checks per query:", float(foo.num_checks)/float(total)
222
224 for leftout in xrange(1,11):
225 print "Fold",leftout
226 foo = FastKNN(10)
227 for x in xrange(1,11):
228 if x != leftout:
229 somedata = open("spiral/spiralfold%d.txt" % x)
230 pnts = list()
231 clss = list()
232 for line in somedata:
233 pbbbt,x,y = line.split()
234 x,y = float(x),float(y)
235 pnts.append((x,y))
236 clss.append(line.split()[0])
237 somedata.close()
238 pnts = numpy.array(pnts)
239 foo.addEvidence(pnts,clss)
240 somedata = open("spiral/spiralfold%d.txt" % leftout)
241 correct = total = 0
242 for line in somedata:
243 pbbbt,x,y = line.split()
244 x,y = float(x),float(y)
245 guess=foo.query((x,y),10)
246
247
248
249
250
251 if guess == pbbbt:
252 correct += 1
253 total += 1
254 print correct,"/",total,":",float(correct)/float(total)
255 print "Average number of checks per query:",
256 print float(foo.num_checks)/float(total)
257
259 inf = open(fname)
260 return numpy.array([map(float,s.strip().split(',')) for s in inf.readlines()])
261
263 anchors = 200
264 fname = 'test2.dat'
265 querys = 1000
266 d = 2
267 k = 3
268 bnds = ((-10,10),)*d
269 clsses = (0,1)
270 foo = FastKNN(anchors,k)
271 data = getflatcsv(fname)
272 foo.addEvidence(data[:,:-1],data[:,-1])
273 foo.num_checks = 0
274 for x in xrange(querys):
275 pnt = numpy.array(gendata.gensingle(d,bnds,clsses))
276 foo.query(pnt[:-1])
277 if x % 50 == 0:
278 print float(foo.num_checks)/float(x+1),
279 print x,"/",querys
280 print "Average # queries:", float(foo.num_checks)/float(querys)
281
282
283
286
287 if __name__=="__main__":
288 test()
289
290
291
292
293