前言
之前看了不少关于机器学习的书,不过都是偏理论,没有教你如何用代码去使用算法实现一个功能。最近看了一本《机器学习实战 (原书第2版)》,理论与实践相结合,非常适合对机器学习感兴趣的小白。就是看理论方面的书,就是有种光看不练假把式的感觉。而看了理论,自己去动手敲敲代码,那就有一种恍然大悟茅塞顿开的感觉。原来如此!基于以上,这里运用K近邻算法实现一个识别手写数字系统
《机器学习实战 (原书第2版)》原作名: Hands-on Machine Learning with Scikit-Learn, Keras, and TensorFlow: Concepts, Tools, and Techniques to Build Intelligent Systems
k近邻算法
1. 基本思想
k近邻算法(k-Nearest Neighbors, KNN) 是一种 监督学习算法,主要用于 分类 和 回归。
它的思想很直观:
- 给定一个需要分类的样本点
- 在训练集中找到与它 距离最近的 k 个样本
- 通过这 k 个邻居的类别来决定该样本的类别(投票表决)
2. 算法步骤
准备数据:把训练数据的特征和标签存好。
计算距离:对新样本,计算它和训练集中所有样本的距离(常用欧式距离)。
d ( x , y ) = ∑ i = 1 n ( x i − y i ) 2 d(x, y) = \sqrt{\sum_{i=1}^{n}(x_i - y_i)^2} d(x,y)=i=1∑n(xi−yi)2
选取最近的 k 个样本。
投票表决(分类):多数类别获胜。
平均计算(回归):取 k 个邻居的平均值。返回结果。
k不能过高也不能过低,过高会导致过拟合,过低会导致欠拟合。一般是3-10个
手写系统实现
问题分析
我们要识别一个手写数字也就是【0,1,2,3,4,5,6,7,8,9】,也就是我们经常用到的图像识别功能,只是我们这里简单做只能识别数字。
想一下我们人类怎么识别数字,就是小时候老师和我们说0就是像一个圈圈,1就是一个竖线,2就是像鸭子,3就是两只耳朵,4像旗子插在地上。我们也就是通过形状识别,还有我们写在纸上的字和纸张不能是一个颜色不然我们就看不清是什么数字了。
以上就是我们人类语言的特征,而机器学习语言描述数字的特征和我们人类不一样用的向量。我们要教会机器学会识别数字和人类有点像,告诉他黑色是纸,白色是字。然后就是 把大量的数字9给它看,让它看一下数字9长什么样。写成这样是9,写成那样也是9,也就是告诉大量的数字9图片长什么样。然后让机器记下看过的图片9数字的特征,再然后我就把新的图片给它,让它帮我识别是数字几,那么它就根据之前记录的特征向量,对比这张新的图片,找出特征向量最接近的,那么就知道是数字几了。
总结一句话就是存储大量数字的特征向量,然后遇到新的就是找到库里存的特征向量最接近那么就是这个数字了。
特征向量
我们看到这个数字图片是黑底白字,也就是我们可以通过颜色进行识别。也就是把数字9哪些地方是白色,也就是知道数字9的轮廓。那么我们要知道位置怎么表示,这里用矩阵表示。如果直接用图片说用矩阵表示,可能有点抽象。这里先用0和1字符组成数字,就很好理解了。
00000000000111111000000000000000
00000000001111111100000000000000
00000000001111111111000000000000
00000000111111111111000000000000
00000001111111111111100000000000
00000000111111111111110000000000
00000011111111111111110000000000
00000011111111111111110000000000
00000001111111111111111000000000
00000001111111111111111100000000
00000001111111111111111100000000
00000000111111111111111100000000
00000000001111111110011110000000
00000000000011000000011110000000
00000000000000000000011110000000
00000000000000000000001111000000
00000000000000000000011111000000
00000000000000000000011110000000
00000000000000000000001111000000
00000000000000000000011111000000
00000000000000000000011111000000
00000000000000000000011111100000
00000000000000000000011111100000
00000000000000000000111111100000
00000000000000000001111111000000
00000000000000000011111111000000
00000000000100011111111110000000
00000000011111111111111100000000
00000000001111111111111100000000
00000000001111111111110000000000
00000000000111111111000000000000
00000000000000000000000000000000
上面是用0和1拼成的数字9,我们可以看到这就是个二维数组,我们可以将这些0和1字符存到二位数组,那么就可以表示位置了。
这里先附上字符拼成的字符表示向量
def img2vector(filename):
"""将图像转换为向量"""
returnVect = zeros((1, 1024)) # 创建1x1024的零向量
fr = open(filename) # 打开文件
for i in range(32): # 逐行读取
lineStr = fr.readline() # 读取一行
for j in range(32): # 逐列读取
returnVect[0, 32 * i + j] = int(lineStr[j]) # 将每个像素值存储在向量中
return returnVect # 返回转换后的向量
我们看到上面的代码,那么图片实际也差不多。黑色和白色,也可以表示成0和1,白色位置和黑色位置也就是二维数组位置。那么我们参考上面的 将图片转为矩阵向量
def img2vector_from_image(filename, size=(28,28)):
"""
将图片缩放并二值化,展平为向量
参数:
filename: 图片文件路径
size: 图片缩放尺寸,默认28x28
返回:
展平后的图片向量 (1, size[0]*size[1])
"""
im = Image.open(filename).convert('L') # 转为灰度图 (0–255 的亮度值)
im = im.resize(size) # 缩放到指定尺寸
im_array = np.array(im) # 转为numpy数组
im_array = 255 - im_array # 反色处理 (黑色0 变 255, 255 变 0)
im_array = (im_array > 128).astype(int) # 二值化处理
return im_array.reshape(1, size[0]*size[1]) # 展平为一维向量
计算距离
在前面我们已经把特征向量处理好了,现在一个问题就是怎么对比两个向量是最接近的。因为特征向量最接近那么他们的标签更大概率也是相同的。这里用的欧式距离,因为比较简单,也更加直观。
在 k近邻算法 (k-NN) 里,常见的相似度度量方法有:
欧式距离 (Euclidean Distance)
曼哈顿距离 (Manhattan Distance)
切比雪夫距离 (Chebyshev Distance)
闵可夫斯基距离 (Minkowski Distance)
余弦相似度 (Cosine Similarity)
# --------------------------
# kNN 分类器
# --------------------------
def classify0(inX, dataSet, labels, k):
"""
kNN分类算法
参数:
inX: 待分类的输入向量
dataSet: 训练集数据
labels: 训练集标签
k: 选择最近邻居的数量
返回:
预测的类别标签
"""
dataSetSize = dataSet.shape[0] # 训练集样本数
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet # 计算差值矩阵
sqDiffMat = diffMat ** 2 # 平方
sqDistances = sqDiffMat.sum(axis=1) # 求和得到距离平方
distances = sqDistances ** 0.5 # 开方得到欧氏距离
sortedDistIndices = distances.argsort() # 按距离排序
classCount = {} # 统计类别出现次数
for i in range(k):
voteIlabel = labels[sortedDistIndices[i]] # 取前k个最近邻的标签
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.items(), key=lambda item: item[1], reverse=True) # 按出现次数排序
return sortedClassCount[0][0]
代码
from os import listdir
import os
import numpy as np
from PIL import Image
# https://github.com/teavanist/MNIST-JPG ()MNIST数据集JPG格式下载地址
# --------------------------
# 将图片转换为向量
# --------------------------
def img2vector_from_image(filename, size=(28,28)):
"""
将图片缩放并二值化,展平为向量
参数:
filename: 图片文件路径
size: 图片缩放尺寸,默认28x28
返回:
展平后的图片向量 (1, size[0]*size[1])
"""
im = Image.open(filename).convert('L') # 转为灰度图 (0–255 的亮度值)
im = im.resize(size) # 缩放到指定尺寸
im_array = np.array(im) # 转为numpy数组
im_array = 255 - im_array # 反色处理 (黑色0 变 255, 255 变 0)
im_array = (im_array > 128).astype(int) # 二值化处理
return im_array.reshape(1, size[0]*size[1]) # 展平为一维向量
# --------------------------
# kNN 分类器
# --------------------------
def classify0(inX, dataSet, labels, k):
"""
kNN分类算法
参数:
inX: 待分类的输入向量
dataSet: 训练集数据
labels: 训练集标签
k: 选择最近邻居的数量
返回:
预测的类别标签
"""
dataSetSize = dataSet.shape[0] # 训练集样本数
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet # 计算差值矩阵
sqDiffMat = diffMat ** 2 # 平方
sqDistances = sqDiffMat.sum(axis=1) # 求和得到距离平方
distances = sqDistances ** 0.5 # 开方得到欧氏距离
sortedDistIndices = distances.argsort() # 按距离排序
classCount = {} # 统计类别出现次数
for i in range(k):
voteIlabel = labels[sortedDistIndices[i]] # 取前k个最近邻的标签
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.items(), key=lambda item: item[1], reverse=True) # 按出现次数排序
return sortedClassCount[0][0] # 返回出现次数最多的类别
# --------------------------
# 手写数字识别测试
# --------------------------
def handwritingClassTest(train_path, test_path, image_size=(28,28)):
"""
手写数字识别测试函数
参数:
train_path: 训练集文件夹路径
test_path: 测试集文件夹路径
image_size: 图片尺寸
"""
hwLabels = [] # 训练集标签列表
trainingFileList = [] # 训练集文件列表
# 遍历每个类别文件夹
print("正在加载训练集...")
for label in os.listdir(train_path):
class_folder = os.path.join(train_path, label)
if not os.path.isdir(class_folder):
continue
for img_file in os.listdir(class_folder):
trainingFileList.append((os.path.join(class_folder, img_file), int(label)))
m = len(trainingFileList) # 训练集样本数
trainingMat = np.zeros((m, image_size[0]*image_size[1])) # 初始化训练集矩阵
for i, (file_path, label) in enumerate(trainingFileList):
hwLabels.append(label)
[i, :] = img2vector_from_image(file_path, size=image_size)
print(f"训练集数量: {m}")
# 测试集
print("正在加载测试集...")
testFileList = [] # 测试集文件列表
for label in os.listdir(test_path):
class_folder = os.path.join(test_path, label)
if not os.path.isdir(class_folder):
continue
cnt=0
for img_file in os.listdir(class_folder):
testFileList.append((os.path.join(class_folder, img_file), int(label)))
cnt+=1
if cnt>=100: # 每类只取100个测试样本
break
print(f"测试集数量: {len(testFileList)}")
errorCount = 0.0 # 错误计数
mTest = len(testFileList) # 测试集样本数
for file_path, true_label in testFileList:
vectorUnderTest = img2vector_from_image(file_path, size=image_size) # 测试图片向量
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3) # 预测结果
print(f"分类结果为 {classifierResult}\t真实结果为 {true_label}")
if classifierResult != true_label:
errorCount += 1.0
print(f"\n总共错了 {int(errorCount)} 个数据\n错误率为 {errorCount / float(mTest) * 100:.2f}%")
# --------------------------
# 预测单张图片
# --------------------------
def predict_single_image(image_path, train_path, image_size=(28,28), k=3):
"""
加载训练集并预测单张图片的数字
参数:
image_path: 待预测图片路径
train_path: 训练集文件夹路径
image_size: 图片尺寸
k: 最近邻数量
返回:
预测的类别标签
"""
# 加载训练集
hwLabels = [] # 训练集标签列表
trainingFileList = [] # 训练集文件列表
for label in os.listdir(train_path):
class_folder = os.path.join(train_path, label)
if not os.path.isdir(class_folder):
continue
for img_file in os.listdir(class_folder):
trainingFileList.append((os.path.join(class_folder, img_file), int(label)))
m = len(trainingFileList) # 训练集样本数
trainingMat = np.zeros((m, image_size[0]*image_size[1])) # 初始化训练集矩阵
for i, (file_path, label) in enumerate(trainingFileList):
hwLabels.append(label)
trainingMat[i, :] = img2vector_from_image(file_path, size=image_size)
# 处理要预测的图片
vectorUnderTest = img2vector_from_image(image_path, size=image_size) # 待预测图片向量
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, k) # 预测结果
print(f"图片 {image_path} 预测结果为: {classifierResult}")
return classifierResult
# --------------------------
# 运行
# --------------------------
train_path = r"C:\Users\纪大侠\Desktop\文档\图书\机器学习实战数据集\Ch02-KNN\MNIST Dataset JPG format\MNIST - JPG - training"
test_path = r"C:\Users\纪大侠\Desktop\文档\图书\机器学习实战数据集\Ch02-KNN\MNIST Dataset JPG format\MNIST - JPG - testing"
handwritingClassTest(train_path, test_path, image_size=(28,28)) # 测试全部数据集
# predict_single_image(r"C:\Users\纪大侠\Downloads\9.png", train_path, image_size=(28,28), k=3) # 预测单张图片
测试和训练图片下载地址MNIST-JPG
总结
大家如果感兴趣,可以在网上找一些数字图片来测试,看看识别的实际准确率。
KNN 算法虽然原理简单、便于初学者理解,但它的计算复杂度和空间占用都比较高,当数据量增大时,效率会明显下降。