零.前言
又开一个坑,手写一些机器学习常用的函数的代码,以此深度了解机器学习框架背后的一些原理。
一.原理
1.简述原理
KNN的原理,简述就是:
- 向量化训练集和目标
- 预测目标与训练集中每一个成员的距离
- 选出训练集中与待预测的目标距离最小的K个成员
- 在这K个成员中,选出出现次数最多的那个成员,该成员就是我们的预测结果
2.补充说明
-
向量化是指:比如一张28 * 28像素的图片,我们可以给他二值化后,变成一个由0,1组成的28 * 28的矩阵,再把这个矩阵变个形,就成了一个1 * 784的单行矩阵,即可称为向量
-
我们在初中学过:平面直角坐标系上的距离公式为:
d i s t a n c e = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 2 distance = \sqrt[2]{(x_1-x_2)^2+(y_1-y_2)^2} distance=2(x1−x2)2+(y1−y2)2
在高中学过:空间直角坐标系上的距离公式为:
d i s t a n c e = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 + ( z 1 − z 2 ) 2 2 distance = \sqrt[2]{(x_1-x_2)^2+(y_1-y_2)^2+(z_1-z_2)^2} distance=2(x1−x2)2+(y1−y2)2+(z1−z2)2
那么到了大学,高维向量的距离公式:
v e c t o r 1 = [ n 1 n 2 n 3 . . . n x ] v e c t o r 2 = [ m 1 m 2 m 3 . . . m x ] ∣ d i s t a n c e ∣ = ( n 1 − m 1 ) 2 + ( n 2 − m 2 ) 2 + . . . + ( n x − m x ) 2 2 vector_1 = [n_1\,n_2\,n_3\, ...\, n_x]\\ vector_2 = [m_1\,m_2\,m_3\, ...\, m_x]\\ \left| distance\right| = \sqrt[2]{(n_1-m_1)^2+(n_2-m_2)^2+...+(n_x-m_x)^2} vector1=[n1n2n3...nx]vector2=[m1m2m3...mx]∣distance∣=2(n1−m1)2+(n2−m2)2+...+(nx−mx)2
通过这个方式,我们就可以实现任意维度向量的计算
二.代码实现
1.创建数据集
目前在工程目录下,我自己写了个小软件,然后自己绘制了200张图片,命名方式如下data/{number}/{index}.jpg
然后我们通过python来将这些图像处理后,变成一组二维向量,分步来:
导入包,并创建我们的矩阵:
import cv2
import numpy as np
data = np.zeros([200, 785], dtype=int)
开始循环,并修改我们刚才创建的矩阵
for i in range(10): # 循环确定图片标号
for n in range(1, 21): # 第几张图片
img = cv2.imread("data/"+str(i)+"/"+str(n)+".jpg", 0) # 获取图片路径并读入
th1, img = cv2.threshold(img, 0, 1, 0) # 二值化白色变为1,黑色为0
img = np.array(img).reshape(1, -1) # 向量化
row = i * 20 + n - 1 # 计算行
data[row][0] = i # 将向量第一个作为图片内容0~9
data[row][1:785] = img # 从1~785是784维的图片向量
保存数据
np.save("train_data", data) # 保存数据
运行致此,我们就可以在工程目录下,得到我们的数据集,以后直接调用即可,就不需要图片预处理。
2.使用KNN预测图片算法
我们先来编写调用KNN识别的函数:
def knn_train(vector, k): # vector使我们待预测的图片向量,第二个是KNN的参数K
dis_list = [] # 用于存放图像值和距离
hash_map = {} # 用于统计前K个数的频数
train_data = np.load('train_data.npy') # 读入我们保存好的数据
(row, col) = train_data.shape # 获取下行列,以便我们以后能使用更高数量的训练集
for i in range(row): # 按行计算
distance = vector - train_data[i][1:col] # 两者作差,获得每个维度的差类似于(n1-m1)
distance = distance**2 # 每个维度求平方和
distance = np.sum(distance) # 将每个维的值加起来,就不用求方差了,反正都是线性对应的,没必要浪费时间求方差
dis_list.append((train_data[i][0], distance)) #将(图片内容。距离)存入dis_list列表里
dis_list.sort(key=lambda x: x[1]) # 将dis_list里的值,以元组的第二个参数为key值,排序
if k > row: # 如果k大于了行数,那么就变成行数,虽然一般来说 k=3~30,不会那么大,但是为了怕出错,这样写好一点儿
k = row
for i in range(k): # 遍历前k个元素
(label, mark) = dis_list[i] # 提取每个元组的信息
label = str(label) # 把整型的图片信息,变成字符串,以便成为字典的键
if label in hash_map: # 如果字典里有这个键
value = hash_map.get(label) + 1 # 读取键,并把键值加一
hash_map.update({label: value}) # 更新
else:
hash_map.update({label: 1}) # 无键,自己创一个新的
return hash_map
此时,返回了hash_map,我们的值输出它,可能是这样的:
{'7': 5, '1': 4, '9': 1}
如果我们要其中,出现次数最高的键,就可以使用这样处理:
re_data = knn_train(img, 10)
print(max(re_data, key=lambda x: re_data[x])) #这里的值,就是7,也就是最大键值的键名
好了,整个函数应该是这样的:
import numpy as np
import cv2
def knn_train(vector, k):
dis_list = []
hash_map = {}
train_data = np.load('train_data.npy')
(row, col) = train_data.shape
for i in range(row):
distance = vector - train_data[i][1:col]
distance = distance**2
distance = np.sum(distance)
dis_list.append((train_data[i][0], distance))
dis_list.sort(key=lambda x: x[1])
if k > row:
k = row
for i in range(k):
(label, mark) = dis_list[i]
label = str(label)
if label in hash_map:
value = hash_map.get(label) + 1
hash_map.update({label: value})
else:
hash_map.update({label: 1})
return hash_map
img = cv2.imread("7.jpg", 0)
th1, img = cv2.threshold(img, 0, 1, 0)
img = np.array(img).reshape(1, -1)
re_data = knn_train(img, 10)
print(max(re_data, key=lambda x: re_data[x]))
代码下载地址:https://download.csdn.net/download/u011017694/12859584,免费下载。
三.总结与反思
虽然这样就实现了KNN,但还是受样本的容量影响。但如果样本容量太大,又会产生时间过长的问题。所以 我想要3090QAQ。
而且,如果单纯的只是最近距离的K个点频率最高的那个是预测值,那么万一有40%的值是特别接近距离在10左右,而60%的值离得很远在100左右的话,那么除了修改K值是别无办法的,那么为何不给每个值按距离的远近加个权后,再来求呢?我不知道,有时间试试效果。