diff --git a/assignment-1/submission/19307110020/README.md b/assignment-1/submission/19307110020/README.md new file mode 100644 index 0000000000000000000000000000000000000000..187378e8d1e1fff3b396413ddc2e09588a505b13 --- /dev/null +++ b/assignment-1/submission/19307110020/README.md @@ -0,0 +1,142 @@ +# PRML21春-HW1-KNN分类器 + +### 简介 + +实现KNN分类器并探索细节。本设计实现了一系列KNN算法的变种,包括: + +- 实现了数据的min-max标准化 +- 证明了标准化的有效性 + +- 自动寻找较优的K值 +- 实现了两种距离(欧氏距离/曼哈顿距离)的算法 +- 比较了两种距离尺度的特性 + +- 实现了对每个训练集中点的置信度的加权 +- 实现了对K个近邻点按距离的加权 +- 证明以上两种加权方式是有效的消融实验 +- 探索常规KNN算法对数据分布的响应 +- 验证了加权后的算法相对于常规算法对数据分布的鲁棒性 + + + +### 前言 + +本实验没有探索数据本身对于常规KNN算法的影响。 + +我认为“点集重叠越少表现越好”之类的结论是显而易见的,不希望在数据本身上花太多时间。 + +此外,我没有对数据可视化,因为数据的分布给定之后,大致可以预见其可视化结果。本设计中实现了许多有意义的改进,因此重点在于算法本身的探索上,与作业手册的第四条“修改数据集的属性(例如:不同高斯分布之间的距离),进行探究性实验”有出入,还望助教老师多多包涵~ + + + +### min-max标准化 + +#### 实现细节 + +对每一个channel,都实现了 + +`x=(x-(该channel中最小值))/((该channel中最大值)-(该channel中最小值))` + +的标准化,将所有的值映射到`[0,1]`上,并且会在训练集中记录训练集上每一channel的最大/最小值,并对测试集做同样的映射,以保证两者采用同一映射,如果对两个数据集分别归一化,将会有标准化方式不统一的问题。 + +#### 正确性 + +其带来的作用是显而易见的,在闵氏距离族中,不论采用何种距离度量,都会出现同一问题:两点之间的距离最主要受尺度最大的特征影响,这一显然的结论不需要实验便可以举出例子证明。 + +经典的波士顿房价预测问题(尽管其原型是回归问题,仍然可以用KNN算法分析房价等级,例如高、中、低等):“历年房价中位数”的尺度是万数量级的,而“交通便利指数”、“环保指数”、“与就业中心的距离”都是十分有用的特征,然而其尺度远比房价小! + +如果不加处理就直接进行KNN算法,显然其他特征全部被忽略,KNN算法将完全被历年房价主导!这是不合理的。在此处尺度最大的特征恰巧是相关性极强的,算法尚能工作,如果尺度最大的恰巧是相关性低的,则算法将完全失效! + + + +### 自动寻找K值 + +由于算法之间(自动寻找K值,置信度加权,距离加权)互相牵制,因此在搜索K值时只能采用常规KNN算法,本算法在`1~min(16,训练集样本个数)`之中搜索所有K,找到准确率最高的K值。 + +在示例程序中,将会自动选择K=1,在自动生成的样本中,参数如下: + +| Distribution | Mean | Cov | Number | +| ------------ | ------ | ----------------- | ------ | +| Class 0 | (1,2) | [[10, 0], [0, 2]] | 100 | +| Class 1 | (4,5) | [[7, 3], [15, 1]] | 100 | +| Class 2 | (-2,6) | [[0, 1], [1, 2]] | 100 | + +将会选择K=11. 两者的寻找过程如下两图:![Figure_1](img/Figure_1.png) + +![Figure_2](img/Figure_2.png) + +### Euclidean & Manhattan - 两种距离尺度 + +本设计中实现了两种距离尺度中的KNN分类器。 + +曼哈顿距离对每一个channel都是独立的,欧氏距离对每一维度求偏导时都与当前距离相关,因此可以预见的是,在距离较远时,两者表现将接近,在距离较近的时候,需要通过实验探索两者的差异。 + +取两个分布方差矩阵均为单位阵,令其沿y=0.5x方向上其逐渐接近,其中一个分布的均值固定在`(0,0)`,另一分布均值分别为:`(0.1,0.2),(0.6,1.2),(1.1,2.2),(1.6,3.2),(2.1,4.2),(2.6,5.2)`。 + +可以看到如下的变化:![Figure_3](img/Figure_3.png) + +其中蓝线是欧几里得距离,绿线是曼哈顿距离,可见欧氏距离对两分布接近时处理的更好。 + +p.s. 以上的实验采用常规KNN算法。 + + + +### 置信度加权 + +在两个分布有较大的重叠时,训练集中的重叠点本身也不能通过KNN以很高的置信度判断自身的类别,对这类点,其置信度应该降低。 + +置信度的算法为:`K近邻点中与自身类别相同的点数/K`。 + +实验证明这样的改变是有益的。在如下分布中做消融实验: + +| Distribution | Mean | Cov | Number | +| ------------ | ------ | ----------------- | ------ | +| Class 0 | (1,2) | [[10, 0], [0, 2]] | 100 | +| Class 1 | (4,5) | [[7, 3], [15, 1]] | 100 | +| Class 2 | (-2,6) | [[0, 1], [1, 2]] | 100 | + +常规KNN算法:acc = 0.85 + +带置信度加权的KNN算法: 0.8666666666666667 + + + +### 距离加权 + +在一个K近邻范围内,有些点离待监测点较近,有些较远。有理由相信距离较近的点提供的置信度更少,因此需要实现距离加权。 + +距离加权的算法为:`每个近邻点投票的权值为:D-(该近邻点与待监测点的距离/近邻范围内最远点与待监测点的距离)`。其中D为超参。 + +实验得到,在如上分布中,D=1.8时表现最好,消融实验如下: + +常规KNN算法:acc = 0.85 + +带距离加权的KNN算法: 0.8666666666666667 + + + +### 联合加权 + +综合以上两种加权方式,此时需要引入第二个超参,该超参作用于置信度加权,并且对于仅采用置信度加权时,其不起作用,由于需要将两个计算得到的权重相乘,该超参用于调节两者起到的作用之比例。 + +联合加权算法为:`(K近邻点中与自身类别相同的点数/K-bias)*(D-(该近邻点与待监测点的距离/近邻范围内最远点与待监测点的距离))`。其中`bias`为引入的新超参,`D`为距离加权对应的超参。 + +实验证明,取`bias=0.2`,`D=2.1`时,可以达到较好的效果。 + +常规KNN算法:acc = 0.85 + +带联合加权的KNN算法:acc = 0.8833333333333333 + + + +### 对数据的鲁棒性 + +可以预见,当数据比较接近时,这样的加权会有更好的表现,因为其更好的考虑了模糊点置信程度较低的性质,并且利用了靠近待分类点与距离较远的点之间的差异。 + +下图为实验结果,其中蓝线为联合加权结果,绿线为常规KNN算法。 + +![Figure_4](img/Figure_4.png) + +### 总结 + +本设计探索了更高效的KNN算法,采用联合加权的方式对邻接范围内的点做不同的处理,取得了较好的效果。 \ No newline at end of file diff --git a/assignment-1/submission/19307110020/img/Figure_1.png b/assignment-1/submission/19307110020/img/Figure_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b006d52cf0b89e9497b213ec044b0224a5a620a7 Binary files /dev/null and b/assignment-1/submission/19307110020/img/Figure_1.png differ diff --git a/assignment-1/submission/19307110020/img/Figure_2.png b/assignment-1/submission/19307110020/img/Figure_2.png new file mode 100644 index 0000000000000000000000000000000000000000..d520d40d31644714b20789c236f9976818185cab Binary files /dev/null and b/assignment-1/submission/19307110020/img/Figure_2.png differ diff --git a/assignment-1/submission/19307110020/img/Figure_3.png b/assignment-1/submission/19307110020/img/Figure_3.png new file mode 100644 index 0000000000000000000000000000000000000000..c643350e0310bac079cdd179a01cff9371b27475 Binary files /dev/null and b/assignment-1/submission/19307110020/img/Figure_3.png differ diff --git a/assignment-1/submission/19307110020/img/Figure_4.png b/assignment-1/submission/19307110020/img/Figure_4.png new file mode 100644 index 0000000000000000000000000000000000000000..6af94b33a92ba072e05b09dc9eff0ff3c6bc0d08 Binary files /dev/null and b/assignment-1/submission/19307110020/img/Figure_4.png differ diff --git a/assignment-1/submission/19307110020/source.py b/assignment-1/submission/19307110020/source.py new file mode 100644 index 0000000000000000000000000000000000000000..a231c833e05fe6cb7a43b4c7fdabab987978305a --- /dev/null +++ b/assignment-1/submission/19307110020/source.py @@ -0,0 +1,125 @@ +import numpy as np +import matplotlib.pyplot as plt +class KNN: + def __init__(self): + pass + + def fit(self, train_data, train_label): + self.len=train_data.shape[0] + #standardize + train_data=train_data.astype(np.float64) + train_data=train_data.T + self.channel=train_data.shape[0] + self.mins=[] + self.maxs=[] + for data in train_data: + self.mins.append(np.min(data)) + self.maxs.append(np.max(data)) + for i in range(data.shape[0]): + data[i] = (data[i] - np.min(data)) / (np.max(data) - np.min(data)) + self.train_data=train_data.T + self.train_label=train_label + + #grid search for K + maxk, maxacc=0, 0 + for k in range(1,min(17,self.len)): + acc=0 + for d in range(self.len): + dists = [] + indexs = np.arange(self.len) + for i in range(self.len): + dists.append(self.euclidean(self.train_data[d], self.train_data[i])) + dic = dict(zip(indexs, dists)) + dic = dict(sorted(dic.items(), key=lambda item: item[1])) + min_indexs = list(dict(list(dic.items())[1:k+1]).keys()) + min_dists = [self.train_label[i] for i in min_indexs] + if max(min_dists, key=min_dists.count) == self.train_label[d]: + acc+=1 + if acc>maxacc: + maxk, maxacc=k, acc + self.K=maxk + + #credibility + self.cred=[] + for d in range(self.len): + dists = [] + indexs = np.arange(self.len) + for i in range(self.len): + dists.append(self.euclidean(self.train_data[d], self.train_data[i])) + dic = dict(zip(indexs, dists)) + dic = dict(sorted(dic.items(), key=lambda item: item[1])) + min_indexs = list(dict(list(dic.items())[1:self.K+1]).keys()) + min_dists = [self.train_label[i] for i in min_indexs] + self.cred.append(float(min_dists.count(max(min_dists, key=min_dists.count)))/self.K) + + def predict(self, test_data): + test_data=test_data.astype(np.float64) + test_data=test_data.T + for i in range(self.channel): + for j in range(test_data.shape[1]): + test_data[i][j] = (test_data[i][j] - self.mins[i]) / (self.maxs[i] - self.mins[i]) + test_data = test_data.T + ans=[] + for d in range(test_data.shape[0]): + dists = [] + indexs = np.arange(self.len) + for i in range(self.len): + dists.append(self.euclidean(test_data[d], self.train_data[i])) + dic = dict(zip(indexs, dists)) + dic = dict(sorted(dic.items(), key=lambda item: item[1])) + min_indexs = list(dict(list(dic.items())[:self.K]).keys()) + min_dict={} + for i in min_indexs: + min_dict[self.train_label[i]]=min_dict.get(self.train_label[i],0)+(self.cred[i]-0.2)*(2.1-dic[i]/list(dic.items())[self.K-1][1]) + ans.append(max(min_dict, key=lambda k: min_dict[k])) + return ans + + + def euclidean(self, a, b): + return np.sqrt(np.sum(np.square(a-b))) + + def manhattan(self, a, b): + return np.sum(abs(a-b)) + + +def dataset(mean1,mean2,cov1,cov2,mean3=None,cov3=None): + mean=mean1 + cov=cov1 + x=np.random.multivariate_normal(mean, cov, (100,)) + mean=mean2 + cov=cov2 + y=np.random.multivariate_normal(mean, cov, (100,)) + num=300 + if mean3 is not None and cov3 is not None: + mean=mean3 + cov=cov3 + z=np.random.multivariate_normal(mean, cov, (100,)) + idx=np.arange(num) + np.random.shuffle(idx) + if mean3 is not None and cov3 is not None: + data=np.concatenate([x, y, z]) + label=np.concatenate([np.zeros((100,), dtype=np.int8), np.ones((100,), dtype=np.int8), np.ones((100,), dtype=np.int8) * 2]) + else: + data=np.concatenate([x, y]) + label=np.concatenate([np.zeros((100,), dtype=np.int8), np.ones((100,), dtype=np.int8)]) + data=data[idx] + label=label[idx] + split=int(num*0.8) + train_data, test_data=data[:split,:], data[split:,:] + train_label, test_label=label[:split], label[split:] + np.save("train_data.npy",train_data) + np.save("test_data.npy",test_data) + np.save("train_label.npy",train_label) + np.save("test_label.npy",test_label) + +def dataload(): + train_data, train_label, test_data, test_label = np.load("train_data.npy",allow_pickle=True),np.load("train_label.npy",allow_pickle=True),np.load("test_data.npy",allow_pickle=True),np.load("test_label.npy",allow_pickle=True) + return train_data, train_label, test_data, test_label + +if __name__ == '__main__': + dataset((1,2),(4,5),np.array([[10, 0], [0, 2]],dtype=np.float64),np.array([[7, 3], [15, 1]],dtype=np.float64),(-2,6),np.array([[0, 1], [1, 2]],dtype=np.float64)) + train_data, train_label, test_data, test_label=dataload() + model=KNN() + model.fit(train_data,train_label) + res=model.predict(test_data) + print("acc =",np.mean(np.equal(res, test_label)))