文章目录
- 1.理解 k-最近邻算法
- 目标
- 理论
- OpenCV 中的 kNN
- 其他资源
- 2.使用 kNN 对手写数据进行 OCR
- 目标
- 手写数字的 OCR
- 英文字母的 OCR
- 其他资源
1.理解 k-最近邻算法
目标
在本章中,我们将理解 k-最近邻算法 (kNN) 的概念。
理论
kNN 是监督学习中最简单的分类算法之一。其思想是在特征空间中搜索测试数据的最接近匹配项。我们将通过下图来研究它。
在图像中,有两个家庭:蓝色方块和红色三角形。我们将每个家庭称为类。他们的房子显示在城镇地图上,我们称之为特征空间。您可以将特征空间视为所有数据投影的空间。例如,考虑一个 2D 坐标空间。每个数据都有两个特征,一个 x 坐标和一个 y 坐标。您可以在 2D 坐标空间中表示此数据,对吗?现在想象一下有三个特征,您将需要 3D 空间。现在考虑 N 个特征:您需要 N 维空间,对吗?这个 N 维空间是它的特征空间。在我们的图像中,您可以将其视为具有两个特征的 2D 情况。
现在考虑如果一个新成员进入城镇并创建一个新家,会发生什么,该新家显示为绿色圆圈。他应该被添加到其中一个蓝色或红色家庭(或类)。我们将该过程称为分类。这个新成员究竟应该如何分类?既然我们在处理 kNN,让我们应用该算法。
一个简单的方法是检查谁是他的最近邻居。从图像中可以清楚地看出,他是红三角家族的成员。所以他被归类为红三角。这种方法简称为最近邻居分类,因为分类仅取决于最近邻居。
但这种方法有一个问题!红三角可能是最近的邻居,但如果附近也有很多蓝色方块怎么办?那么蓝色方块在那个地方的强度比红三角更大,所以仅仅检查最近的一个是不够的。相反,我们可能想要检查一些k个最近的家庭。那么无论哪个家庭是其中的多数,新来的人都应该属于那个家庭。在我们的图像中,我们取 k=3,即考虑 3 个最近的邻居。新成员有两个红色邻居和一个蓝色邻居(有两个等距的蓝色邻居,但由于 k=3,我们只能取其中一个),因此他应该再次被添加到红色家族。但是如果我们取 k=7 呢?那么他有 5 个蓝色邻居和 2 个红色邻居,应该被添加到蓝色家族。结果将随着所选的 k 值而变化。请注意,如果 k 不是奇数,我们可能会得到平局,就像上面 k=4 的情况一样。我们会看到我们的新成员有 2 个红色邻居和 2 个蓝色邻居作为他的四个最近邻居,我们需要选择一种方法来打破平局以进行分类。因此重申一下,这种方法称为 k-最近邻居,因为分类取决于 k 个最近邻居。
同样,在 kNN 中,我们确实在考虑 k 个邻居,但我们对所有邻居都给予同等重视,对吗?这合理吗?例如,以 k=4 的并列情况为例。我们可以看到,2 个红色邻居实际上比其他 2 个蓝色邻居更接近新成员,因此他更有资格加入红色家庭。我们如何从数学上解释这一点?我们根据每个邻居与新来者的距离赋予他们一些权重:那些离他较近的人获得更高的权重,而那些离他较远的人获得较低的权重。然后我们分别添加每个家庭的总权重,并将新来者归类为获得更高总权重的家庭的一部分。这称为修改的 kNN 或加权 kNN。
那么你在这里看到的一些重要的东西是什么?
- 因为我们必须检查新来者与所有现有房屋的距离以找到最近的邻居,所以你需要掌握镇上所有房屋的信息,对吗?如果有很多房屋和家庭,则需要大量内存,计算时间也会更长。
- 几乎没有时间进行任何类型的“训练”或准备。我们的“学习”只涉及记忆(存储)数据,然后进行测试和分类。
现在让我们看看这个算法在 OpenCV 中的工作原理。
OpenCV 中的 kNN
我们将在这里做一个简单的例子,有两个家族(类),就像上面一样。然后在下一章中,我们将做一个更好的例子。
所以在这里,我们将红色家族标记为 Class-0(因此用 0 表示),将蓝色家族标记为 Class-1
(用 1 表示)。我们创建 25 个邻居或 25 个训练数据,并将它们中的每一个标记为 Class-0 或 Class-1 的一部分。
我们可以借助 NumPy 的随机数生成器来完成此操作。
然后我们可以借助 Matplotlib 绘制它。红色邻居显示为红色三角形,蓝色邻居显示为蓝色方块。
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt# Feature set containing (x,y) values of 25 known/training data 包含 25 个已知/训练数据的 (x,y) 值的特征集
trainData = np.random.randint(0,100,(25,2)).astype(np.float32)# Label each one either Red or Blue with numbers 0 and 1 用数字 0 和 1 将每个标签标记为红色或蓝色
responses = np.random.randint(0,2,(25,1)).astype(np.float32)# Take Red neighbours and plot them 找到红色邻居并绘制它们
red = trainData[responses.ravel()==0]
plt.scatter(red[:,0],red[:,1],80,'r','^')# Take Blue neighbours and plot them 找到蓝色邻居并绘制它们
blue = trainData[responses.ravel()==1]
plt.scatter(blue[:,0],blue[:,1],80,'b','s')plt.show()
您将获得类似于第一幅图像的图像。由于您使用的是随机数生成器,因此每次运行代码时,您都会获得不同的数据。
接下来启动 kNN 算法并传递 trainData 和 responses 来训练 kNN。(在底层,它构建了一个搜索树:有关此内容的更多信息,请参阅下面的其他资源部分。)
然后,我们将引入一个新来者,并在 OpenCV 中的 kNN 的帮助下将其归类为属于一个家庭。在运行 kNN 之前,我们需要了解一些有关测试数据(新来者的数据)的信息。我们的数据应该是一个浮点数组,大小为 n u m b e r o f t e s t d a t a × n u m b e r o f f e a t u r e s number \; of \; testdata \times number \; of \; features numberoftestdata×numberoffeatures。然后我们找到新来者的最近邻居。我们可以指定 k:我们想要多少个邻居。(这里我们使用了 3。)它返回:
- 根据我们之前看到的 kNN 理论给新来者的标签。如果您想要最近邻算法,只需指定 k=1。
- k-最近邻的标签。
- 新来者与每个最近邻居的相应距离。
让我们看看它是如何工作的。新来者用绿色标记。
newcomer = np.random.randint(0,100,(1,2)).astype(np.float32)
plt.scatter(newcomer[:,0],newcomer[:,1],80,'g','o')knn = cv.ml.KNearest_create()
knn.train(trainData, cv.ml.ROW_SAMPLE, responses)
ret, results, neighbours ,dist = knn.findNearest(newcomer, 3)print( "result: {}\n".format(results) )
print( "neighbours: {}\n".format(neighbours) )
print( "distance: {}\n".format(dist) )plt.show()
我得到了以下结果:
result: [[ 1.]]
neighbours: [[ 1. 1. 1.]]
distance: [[ 53. 58. 61.]]
它说我们新来的人的 3 个最近的邻居都来自 Blue 家族。因此,他被标记为 Blue 家族的一部分。从下面的图中可以明显看出:
如果有多个新来者(测试数据),你可以直接以数组形式传递。相应的结果也是以数组形式获取的。
# 10 new-comers
newcomers = np.random.randint(0,100,(10,2)).astype(np.float32)
ret, results,neighbours,dist = knn.findNearest(newcomer, 3)
# The results also will contain 10 labels.
其他资源
- NPTEL 模式识别笔记,第 11 章
- 维基百科关于最近邻搜索的文章
- 维基百科关于 k-d 树的文章
2.使用 kNN 对手写数据进行 OCR
目标
在本章中:
- 我们将使用我们在 kNN 方面的知识来构建一个基本的 OCR(光学字符识别)应用程序。
- 我们将在 OpenCV 附带的数字和字母数据上尝试我们的应用程序。
手写数字的 OCR
我们的目标是构建一个可以读取手写数字的应用程序。为此,我们需要一些训练数据和一些测试数据。OpenCV 附带一个图像 digits.png(在文件夹 opencv/samples/data/ 中),其中包含 5000 个手写数字(每个数字 500 个)。每个数字都是一个 20x20 的图像。因此,我们的第一步是将此图像拆分成 5000 个不同的数字图像。然后,对于每个数字(20x20 图像),我们将其展平为一行,每行 400 个像素。这就是我们的特征集,即所有像素的强度值。这是我们可以创建的最简单的特征集。我们使用每个数字的前 250 个样本作为训练数据,将其他 250 个样本作为测试数据。所以让我们先准备好它们。
import numpy as np
import cv2 as cvimg = cv.imread('digits.png')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)# Now we split the image to 5000 cells, each 20x20 size 现在我们将图像分割成 5000 个单元格,每个单元格大小为 20x20
cells = [np.hsplit(row,100) for row in np.vsplit(gray,50)]# Make it into a Numpy array: its size will be (50,100,20,20)
x = np.array(cells)# Now we prepare the training data and test data
train = x[:,:50].reshape(-1,400).astype(np.float32) # Size = (2500,400)
test = x[:,50:100].reshape(-1,400).astype(np.float32) # Size = (2500,400)# Create labels for train and test data
k = np.arange(10)
train_labels = np.repeat(k,250)[:,np.newaxis]
test_labels = train_labels.copy()# Initiate kNN, train it on the training data, then test it with the test data with k=1
# 启动 kNN,在训练数据上进行训练,然后使用 k=1 的测试数据进行测试
knn = cv.ml.KNearest_create()
knn.train(train, cv.ml.ROW_SAMPLE, train_labels)
ret,result,neighbours,dist = knn.findNearest(test,k=5)# Now we check the accuracy of classification 现在我们检查分类的准确性
# For that, compare the result with test_labels and check which are wrong
# 为此,将结果与 test_labels 进行比较,检查哪些是错误的
matches = result==test_labels
correct = np.count_nonzero(matches)
accuracy = correct*100.0/result.size
print( accuracy )
因此,我们的基本 OCR 应用程序已准备就绪。此特定示例使我的准确率达到 91%。提高准确率的一个选项是添加更多数据进行训练,尤其是对于错误较多的数字。
与其每次启动应用程序时都查找这些训练数据,不如保存它,以便下次可以直接从文件中读取这些数据并开始分类。这可以借助一些 Numpy 函数(如 np.savetxt、np.savez、np.load 等)来完成。请查看 NumPy 文档了解更多详细信息。
# Save the data
np.savez('knn_data.npz',train=train, train_labels=train_labels)# Now load the data
with np.load('knn_data.npz') as data:print( data.files )train = data['train']train_labels = data['train_labels']
在我的系统中,它占用大约 4.4 MB 的内存。由于我们使用强度值(uint8 数据)作为特征,因此最好先将数据转换为 np.uint8,然后保存。在这种情况下,它仅占用 1.1 MB。然后在加载时,您可以转换回 float32。
英文字母的 OCR
接下来,我们将对英文字母进行同样的操作,但数据和特征集略有变化。在这里,OpenCV 附带了一个数据文件 letter-recognition.data,而不是图像,位于 opencv/samples/cpp/ 文件夹中。如果打开它,您将看到 20000 行,乍一看,这些行可能看起来像垃圾。实际上,在每一行中,第一列是一个字母,它是我们的标签。接下来的 16 个数字是不同的特征。这些特征是从 UCI 机器学习
存储库 获得的。您可以在 此
页 中找到这些特征的详细信息。
有 20000 个样本可用,因此我们将前 10000 个样本作为训练样本,将剩余的10000 个样本作为测试样本。我们应该将字母更改为 ASCII 字符,因为我们无法直接处理字母。
import cv2 as cv
import numpy as np# Load the data and convert the letters to numbers
data= np.loadtxt('letter-recognition.data', dtype= 'float32', delimiter = ',',converters= {0: lambda ch: ord(ch)-ord('A')})# Split the dataset in two, with 10000 samples each for training and test sets
train, test = np.vsplit(data,2)# Split trainData and testData into features and responses
responses, trainData = np.hsplit(train,[1])
labels, testData = np.hsplit(test,[1])# Initiate the kNN, classify, measure accuracy
knn = cv.ml.KNearest_create()
knn.train(trainData, cv.ml.ROW_SAMPLE, responses)
ret, result, neighbours, dist = knn.findNearest(testData, k=5)correct = np.count_nonzero(result == labels)
accuracy = correct*100.0/10000
print( accuracy )
它给了我 93.22% 的准确率。同样,如果你想提高准确率,你可以迭代地添加更多数据。
其他资源
- 维基百科关于光学字符识别的文章