ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python+OpenCV手写数字识别:HOG+KNN期末大作业方案

Python+OpenCV手写数字识别:HOG+KNN期末大作业方案

简介:基于Python与OpenCV实现的手写数字识别完整项目,适用于计算机相关专业期末大作业、课程设计以及需要实战练习的初学者。代码与文档齐备,可在导师指导基础上直接复用、修改或扩展。压缩包共2000个文件,其中1995张jpg构成可用于训练与测试的手写数字样本集,4个py为程序源码与识别脚本,1个md是项目说明文档,整体约22.76MB,结构清晰,便于对照学习。内容涵盖数据读取、模型训练到识别输出的完整流程,配套文档可帮助理解关键实现思路,也适合用于课程汇报和期末答辩准备。该项目为作者经导师认可并获99分的期末设计,代码资料完整且可直接下载运行。已有235人浏览学习,可供正在准备同类作业的同学参考借鉴。

1. 期末大作业选它:Python+OpenCV手写数字识别为什么比深度学习更稳

期末大作业做手写数字识别,很多同学第一反应是上深度学习,但真正交作业时你会发现,用Python+OpenCV实现的项目反而最稳。它不需要GPU,不需要装TensorFlow或PyTorch,一个OpenCV就同时覆盖了数据读取、图像预处理、特征提取、分类器训练和手写板界面。只要把MNIST数据集跑通,再做一个鼠标写字实时识别的小窗口,论文里每一步都有可展示的中间结果,答辩演示也不会突然崩。这套方案尤其适合Python入门课程、图像处理课程设计以及期末大作业的快速落地。下面就从环境到模型完整走一遍。

2. 先立骨架:项目整体方案、环境安装与版本验证

2.1 三种主流实现路线,期末大作业该选哪一种

先确定路线,别一上来就写代码。手写数字识别常见实现有三类,选错路线的代价到最后才显现。

方案原理优点缺点期末大作业适配度
模板匹配输入图像和每张模板做像素对比,算相似度代码最少,十几行能跑对字体粗细、平移、形变非常敏感,识别率很难看低,演示容易翻车
HOG特征 + KNN/SVM提取局部梯度直方图,喂给分类器特征有解释性,能讲清楚,准确率高,依赖少需要调预处理、特征参数高,推荐
CNN卷积神经网络自动学习特征端到端MNIST测试集准确率轻松超99%框架安装重,答辩难解释,训练过程像黑匣子中,除非课程指定

期末这种场景,我一般推荐中间这条路线。KNN本身不需要训练过程,本质就是“找到和当前数字最像的k个样本投票决定是谁”;配上HOG特征后,MNIST测试集准确率能到95%以上,应付课程要求足够。最关键是每个环节的输入输出都能截图放进论文,老师追问“为什么选这个参数”时,你能讲清楚。

确定路线后,项目整体可以拆六个模块:数据读取、图像预处理、特征提取、KNN训练、手写板交互、文档说明。它们串成一个清晰流程,也就是之后源码文件里的主要函数。

2.2 Python、OpenCV、NumPy 的安装与 import 验证

环境安装是期末大作业翻车的第一道坎。常见现象是“装完opencv却找不到cv2”,或者“numpy版本对不上”。建议先用Python 3.8或3.10以上的干净环境,在命令行执行:

python -m pip install --upgrade pip python -m pip install opencv-python numpy matplotlib

如果电脑里同时存在多个Python环境,请一定用python -m pip而不是裸pip,否则很可能装到了另一个解释器里,导致下方的import cv2依然报错。

注意:不要写pip install opencv,这个包名不存在,正确包名是opencv-python。

安装完成后先跑一个小验证,不要直接写业务代码:

import cv2 import numpy as np print("OpenCV 版本:", cv2.__version__) print("NumPy 版本:", np.__version__) img = np.zeros((28, 28), dtype=np.uint8) cv2.imwrite("test.png", img) print(cv2.imread("test.png", cv2.IMREAD_GRAYSCALE).shape)

参数说明:opencv-python是OpenCV主包,提供cv2模块;不需要额外装opencv-contrib-python,除非你要用SIFT这类专利保护的算法。matplotlib是用来画准确率曲线和中间结果的,不想装也可以,但期末论文展示有它会方便很多。

验证代码里特意加了一次图像写入和读取。np.zeros((28,28), dtype=np.uint8)生成单通道8位灰度图,这和你之后从MNIST里读到的数据格式一致。如果imread返回None,通常是中文路径或者文件名特殊字符导致的,这在Windows上尤其常见,项目目录记住全用英文,后面能少踩一个坑。print(cv2.imread(...).shape)输出是(28, 28)而不是(28, 28, 3),说明图像确实按灰度读取成功。

环境准备好后,还要养成建虚拟环境的习惯。我一般是这样:

python -m venv .venv .venv\Scripts\activate # Windows source .venv/bin/activate # Linux / macOS

再在激活状态下执行pip安装。这样即使电脑里还有其他项目依赖不同版本的numpy,也不会互相污染。OpenCV的cv2模块在4.x版本里外观很稳定,课程设计里写OpenCV 4.x即可,不需要强行追最新版。

3. 数据准备:把 MNIST 转成本地图片并完成预处理

3.1 读取 MNIST 原始文件,保存为可展示的图片

MNIST官方文件是idx二进制格式,不是常规图片格式。常见的train-images.idx3-ubyte前4字节是magic number,之后分别是图片数量、行数、列数,再往后就是像素字节流。标签文件train-labels.idx1-ubyte前4字节magic number,然后是标签数量,每字节一个标签,标签是0到9的整数,不是ASCII字符。

读取这段要理解大端字节序,直接用int.from_bytes最不容易错:

import numpy as np def read_mnist_images(path): with open(path, 'rb') as f: magic = int.from_bytes(f.read(4), 'big') # 2051 num = int.from_bytes(f.read(4), 'big') # 图片数量 rows = int.from_bytes(f.read(4), 'big') # 28 cols = int.from_bytes(f.read(4), 'big') # 28 data = np.frombuffer(f.read(), dtype=np.uint8) return data.reshape(num, rows, cols) def read_mnist_labels(path): with open(path, 'rb') as f: magic = int.from_bytes(f.read(4), 'big') # 2049 num = int.from_bytes(f.read(4), 'big') labels = np.frombuffer(f.read(), dtype=np.uint8) return labels[:num]

逻辑说明:np.frombuffer把文件读到的bytes直接映射成numpy数组,不经过逐字节循环,读6万张图也很快。reshape(num, rows, cols)把一维字节流还原成6万张28×28的灰度图。标签读取后只留前num个,防止文件尾部有额外字节。

参数说明:构造frombytes(f.read(4), 'big')时,'big'表示大端字节序,MNIST官方格式就是大端。如果你用np.load会直接报错;用struct或np.frombuffer时忘记指定dtype也会得到不可读的结果。

为了期末答辩演示,最好把原始二进制转成png图片,这样能在文档里直接贴图,也能用看图软件快速检查预处理效果。下面的函数会把前5000张训练图按标签放进data/train/0、data/train/1等目录:

import os import cv2 def save_mnist_as_png(images, labels, root='data/train', count=5000): for i in range(count): label = int(labels[i]) folder = os.path.join(root, str(label)) os.makedirs(folder, exist_ok=True) path = os.path.join(folder, f'{i:05d}_{label}.png') cv2.imwrite(path, images[i])

说明:exist_ok=True避免每次重复创建目录;文件名带五位数序号和标签,方便追踪哪张图来自原始第几个样本。cv2.imwrite写入的是灰度图,OpenCV会按单通道png保存,占用空间小,后续读取也快。

3.2 灰度化、二值化、去噪与尺寸归一化

MNIST原始图片已经是灰度图,但自己手写板的图像可能是彩色的,或者包含噪声。为了让训练样本和预测输入保持一致,必须做同一套预处理。建议顺序是灰度化、二值化、形态学去噪、尺寸归一化,顺序不能乱。

def preprocess(img): if img.ndim == 3: img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化:背景变0,数字变255 _, th = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 形态学闭运算:补笔画内的小孔和细断裂 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) th = cv2.morphologyEx(th, cv2.MORPH_CLOSE, kernel) # 尺寸归一化到28x28 resized = cv2.resize(th, (28, 28), interpolation=cv2.INTER_AREA) return resized

参数说明:THRESH_OTSU会根据灰度分布自动选阈值,比写死127稳得多。THRESH_BINARY_INV让背景变成0、数字变成255。这里有个关键约定:后续HOG和KNN都是在这个白字黑底的表示上做的。如果你漏掉INV,背景是255,数字是0,特征方向全反,识别率会崩。

cv2.getStructuringElement生成3×3矩形结构元。闭运算是先膨胀再腐蚀,能把笔画中的小裂缝补上。注意kernel不要用太大了,比如5×5会把本不该相连的数字粘连起来,最典型的是把“1”边上的噪声连成“7”。

cv2.resize用INTER_AREA,适合缩小图像,抗混叠更好;如果后面要放大,用INTER_CUBIC或INTER_LINEAR更合适。预处理函数应该在整个项目里只写一份,训练和预测都调用它,这是避免训练/预测不一致最有效的方法。

3.3 按数字分文件夹整理数据,训练集测试集一次到位

图像文件和标签分开存储很麻烦,最好直接按标签组织目录,结构是:

data/ ├── train/ │ ├── 0/ │ ├── 1/ │ └── ... └── test/ ├── 0/ ├── 1/ └── ...

整理脚本可以复用之前的读取和保存逻辑:

train_images = read_mnist_images('train-images.idx3-ubyte') train_labels = read_mnist_labels('train-labels.idx1-ubyte') test_images = read_mnist_images('t10k-images.idx3-ubyte') test_labels = read_mnist_labels('t10k-labels.idx1-ubyte') save_mnist_as_png(train_images, train_labels, 'data/train', count=60000) save_mnist_as_png(test_images, test_labels, 'data/test', count=10000)

说明:保存全部训练集和测试集的png需要一点时间和磁盘空间,但后续每次训练不需要重新读idx文件。保存完之后,写训练脚本时只需要os.listdir('data/train/0')就能拿到所有标签为0的图片,逻辑非常朴素。

如果你的论文需要展示“训练集样本”“测试集样本”截图,这些目录本身就是素材。另外,期末答辩最常见的追问是“数据从哪里来”“标签怎么读”,把idx格式和这句整理脚本讲清楚,就很能打。

4. 从环境到模型的5个常见坑:现象、原因与排查

4.1 import cv2 报 ModuleNotFoundError,pip 也装了但没用

现象:终端执行python后运行import cv2,报ModuleNotFoundError: No module named 'cv2'。

原因:最常见不是没装,而是装到了另一个Python环境。比如你用了Anaconda,在base环境里pip install opencv-python,但python命令指向的是系统Python;或者你用了pip而当前pip属于Python 2。第二种是包名写错,pip install opencv会报找不到包。

解决:先确认当前解释器和pip属于同一个环境。执行:

where python # Windows which python # Linux / macOS python -m pip show opencv-python

然后在同一条shell里验证:

python -m pip install opencv-python python -c "import cv2; print(cv2.__version__)"

如果还是报错,检查有没有同时安装多个python,虚拟环境是否激活。我习惯是全程用python -m pip而不是pip,这样能杜绝大部分装错环境的问题。

4.2 cv2.error 断言失败,OpenCV 4.4.0 也逃不掉

现象:代码跑到cv2.findContours或cv2.threshold时抛错,错误一般长这样:

cv2.error: OpenCV(4.4.0) C:\Users\...\opencv\modules\imgproc\src\contours.cpp:... error: (-215:Assertion failed) npoints >= 0 ...

原因:imread返回了None,后面所有函数都拿到空图像;或者传入二值化的图像是浮点类型、不是单通道uint8。中文路径在Windows下也会导致imread失败,这是很隐蔽的坑。OpenCV在4.4.0这类版本里对断言失败的信息并不友好,只看后半段容易让人误以为是轮廓参数写错。

解决:在调用前先做防御判断:

img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: print("警告:图片读取失败,路径:", path) continue

所有与图像相关的路径不要有中文,不要有空格,统一用英文和数字。如果是摄像头/画板生成的图像,确认dtype是np.uint8,位数不是float32。浮点图在OpenCV里太多函数会直接断断言。

4.3 训练准确率很高,手写识别却翻车

现象:MNIST测试集准确率95%以上,自己用鼠标写几个数字,识别率不到一半。

原因:训练和预测的数据分布不一致。MNIST里的数字基本居中占28×28的中间20×20区域,笔画相对规范;鼠标写出来的数字可能只占画布一小块,直接resize到28×28会导致数字很小、大量空白,或者数字非常大被裁剪。另外,如果训练时预处理没做形态学,而预测时做了,特征也不一致。

解决:统一用3.2的preprocess,并且在预测端加“提取数字外接矩形、按比例缩放到20×20、居中放入28×28”这一步。示例代码:

def extract_digit(canvas): _, inv = cv2.threshold(canvas, 127, 255, cv2.THRESH_BINARY_INV) contours, _ = cv2.findContours(inv, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return np.zeros((28, 28), dtype=np.uint8) all_pts = np.vstack([c.reshape(-1, 2) for c in contours]) x, y, w, h = cv2.boundingRect(all_pts) digit = inv[y:y+h, x:x+w] scale = 20 / max(w, h) new_w = max(1, int(w * scale)) new_h = max(1, int(h * scale)) digit = cv2.resize(digit, (new_w, new_h), interpolation=cv2.INTER_AREA) out = np.zeros((28, 28), dtype=np.uint8) ox = (28 - new_w) // 2 oy = (28 - new_h) // 2 out[oy:oy+new_h, ox:ox+new_w] = digit return out

逻辑说明:先二值化让数字为白、背景为黑;再用RETR_EXTERNAL拿最外轮廓;把所有轮廓的点合并成一个集合算外接矩形,避免“8”这种带洞的数字被拆开。按边的最大边缩放成20像素长边,四周留4像素黑边,模仿MNIST的布局。这一步几乎是手写识别不翻车的最关键部分。

4.4 笔画断裂、粘连,二值化参数没调好

现象:预处理后的数字图出现断裂,比如“0”断成两截;或者相邻笔画粘连,“7”变成“H”。

原因:固定阈值选得不对,比如图像灰度整体偏低但用了127;闭运算kernel太大导致粘连;或者画板笔画太细,缩放后直接断。这些都是预处理参数的问题,而不是模型问题。

解决:二值化统一改用OTSU,不要写死阈值。闭运算kernel先用3×3矩形,如果断裂多,可以换成椭圆核cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)),补笔画效果更圆润。如果粘连严重,可以缩减为2×2,或者先做一次腐蚀cv2.erode再闭运算。

调参时要多打印中间结果,把预处理后的图保存成png在文档里对比。期末大作业老师很吃这套,一张图展示“原图、二值化、闭运算、归一化”,比写一百字都管用。

4.5 程序运行卡死、窗口无响应

现象:打开手写板后,鼠标画几笔窗口就转圈,点什么都反应慢,最后只能强制关闭。

原因:把识别放在鼠标回调里,每次移动都跑一遍HOG和KNN;或者主循环没有cv2.waitKey,事件队列不刷新。虽然单次推理只要几十毫秒,但画一笔会产生几十个鼠标事件,一下就卡死。

解决:鼠标回调里只更新画布,识别放到按键事件里。主循环必须加上cv2.waitKey(20),固定刷新率。识别函数单独抽出来,空格键触发,这样演示时才不会一画就卡。代码模板在5.3里会给出。

5. 训练与演示:HOG 特征、KNN 分类器与鼠标写字板

5.1 特征选型:为什么用 HOG 而不是把 28×28 像素直接拉平

直接把28×28图片拉平成784维像素向量喂给KNN,在MNIST上也能跑到90%以上,但有个问题:像素点位置对位移和粗细太敏感,数字稍微偏移一点,就算字符本身没变,向量差异也很大。自己手写时字形千变万化,很容易翻车。

HOG(Histogram of Oriented Gradients)统计的是局部梯度方向分布,对边缘形状更敏感,对绝对位置和灰度波动更不敏感。数字主要由边缘线组成,HOG能抓住笔画走向,比如“0”是上下左右都有弧,“3”有突出的横向边缘。直观上讲,它比原始像素更接近“人眼在看形状”。

OpenCV的cv2.HOGDescriptor参数很多,期末作业用下面这一组就够了,特征维度只有324维,训练速度飞快:

参数值说明
winSize(28, 28)检测窗口和输入图一样大
blockSize(14, 14)每个块包含2×2个cell
blockStride(7, 7)块重叠一半滑动
cellSize(7, 7)每个cell是7×7像素
nbins9一个cell统计9个方向bin

特征维度计算:块在水平和垂直方向各有3个位置,共9个块;每个块有2×2=4个cell,每个cell9个bin,所以总维度是9×4×9=324。这个解释放进论文很加分。

抽取代码:

hog = cv2.HOGDescriptor((28, 28), (14, 14), (7, 7), (7, 7), 9) feat = hog.compute(resized_img) # 返回 (324, 1) float32

参数说明:compute的输入是单通道uint8灰度图,输出是 (324,1) 的列向量。训练时把每个样本的feat.reshape(-1)堆叠成行,就是特征矩阵。

5.2 用 OpenCV KNN 训练分类器:从特征提取到模型保存

KNN在OpenCV中的接口非常简洁,不需要像神经网络那样设置epoch和batch。train接受特征矩阵和标签,标签必须是float32,虽然示例里有人用int32也能跑,但统一用float32避免断言错误。

先从目录加载特征:

import os import cv2 import numpy as np hog = cv2.HOGDescriptor((28, 28), (14, 14), (7, 7), (7, 7), 9) def load_features(data_root, limit=2000): X, y = [], [] for label in range(10): folder = os.path.join(data_root, str(label)) paths = sorted(os.listdir(folder))[:limit] for name in paths: img = cv2.imread(os.path.join(folder, name), cv2.IMREAD_GRAYSCALE) if img is None: continue img = preprocess(img) # 和预测时完全一致 feat = hog.compute(img).reshape(-1) X.append(feat) y.append(float(label)) return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32)

说明:limit=2000表示每个数字最多2000张,训练集总共2万张。如果你用的电脑比较旧,可以降到1000,准确率也不会差太多。preprocess必须是3.2里那个函数,不要在训练脚本里另写一份。

训练和评估:

X_train, y_train = load_features('data/train', limit=2000) X_test, y_test = load_features('data/test', limit=1000) knn = cv2.ml.KNearest_create() knn.train(X_train, cv2.ml.ROW_SAMPLE, y_train) knn.save('mnist_knn.xml') ret, results, neighbours, dist = knn.findNearest(X_test, k=5) acc = (results.reshape(-1) == y_test).mean() print('KNN+HOG 准确率:', acc)

逻辑说明:findNearest返回四样东西,我们自己只用到前两个。results是预测标签,neighbours是每个测试样本附近最近的k个训练样本标签,dist是对应的欧氏距离。期末论文里可以把neighbours第一个值对应图片打印出来,证明“最近邻”确实长得像数字,这是很直观的可视化。

参数说明:k=5是最近邻数量。准确率如果低于90%,先检查预处理是否和预测一致,再检查是否用了THRESH_BINARY_INV的白字黑底表示,最后再考虑调k。

5.3 鼠标手写板:实时预处理与识别的完整代码

手写板是期末演示的最大亮点。在OpenCV窗口里按住鼠标写字,按空格识别,按c清屏,按Esc退出。这个交互代码不长,但有两个坑:一是鼠标回调里不要放耗时操作,二是显示预测标签时不要直接修改原始画布。

import cv2 import numpy as np canvas = np.ones((200, 200), dtype=np.uint8) * 255 drawing = False def on_mouse(event, x, y, flags, param): global canvas, drawing if event == cv2.EVENT_LBUTTONDOWN: drawing = True cv2.circle(canvas, (x, y), 6, 0, -1) elif event == cv2.EVENT_MOUSEMOVE and drawing: cv2.circle(canvas, (x, y), 6, 0, -1) elif event == cv2.EVENT_LBUTTONUP: drawing = False def predict_digit(canvas, knn, hog): digit = extract_digit(canvas) # 用4.3的函数 digit = preprocess(digit) # 确保尺寸/二值化一致 feat = hog.compute(digit).reshape(-1).astype(np.float32) ret, result, neighbours, dist = knn.findNearest( feat.reshape(1, -1), k=5) return int(result[0][0]) cv2.namedWindow('hand_write') cv2.setMouseCallback('hand_write', on_mouse) while True: display = canvas.copy() key = cv2.waitKey(20) if key == ord(' '): label = predict_digit(canvas, knn, hog) cv2.putText(display, f'predict: {label}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) print('识别结果:', label) elif key == ord('c'): canvas = np.ones((200, 200), dtype=np.uint8) * 255 elif key == 27: break cv2.imshow('hand_write', display) cv2.destroyAllWindows()

代码说明:鼠标回调里cv2.circle的半径是6像素,这个值要配合画布200×200。半径太小,写出来的线条缩放成28×28后容易断;太大则笔画过粗,二值化后成一个实心块。可以先试再调。

display = canvas.copy()是防止预测结果文字残留到手写区域。waitKey(20)保证20毫秒刷新一次画面,按键才会及时响应。空格和c都在主循环里触发,不会阻塞鼠标事件。

extract_digit和preprocess的顺序要注意:extract_digit内部已经做了二值化和裁剪,但它输出的28×28图不一定又做了一次闭运算,所以补一个preprocess很安全。反过来先preprocess再extract会把整张canvas缩到28×28,数字位置信息就毁了。

5.4 调 K 值和样本增强:让演示准确率更好看

K值是最直接的旋钮。K=1对噪声敏感,K=7又容易把不同数字拉到一起。常见做法是在训练集上扫一遍:

for k in [3, 5, 7, 9]: ret, results, _, _ = knn.findNearest(X_test, k=k) acc = (results.reshape(-1) == y_test).mean() print(f'k={k}, acc={acc:.4f}')

逻辑说明:K增大,模型变得更平滑,但因为数字类别只有10个,K太大反而会让边界变成“所有邻居投票”。期末作业选一个准确率最高的K固定下来,写进config.py。

如果想进一步提高自己手写数字的识别率,需要做样本增强。MNIST里没有鼠标写字那种粗细不均匀、旋转偏移较大的样本,可以用小角度旋转和微小平移生成额外训练样本:

def augment(img, angle=10): M = cv2.getRotationMatrix2D((14, 14), angle, 1.0) return cv2.warpAffine(img, M, (28, 28), borderValue=0)

参数说明:(14,14)是28×28图的中心点,旋转轴在中心;borderValue=0表示旋转后露出来的区域填充黑边,对应白字黑底表示。若borderValue设为255,会引入一圈白边,造成特征混乱。

增强时可以对每张图生成角度±10、平移1到2像素的副本,再重新提取HOG训练。这样训练集会大几倍,KNN推理变慢,但MNIST自带数据量已经足够,增强函数更多是为了应对“自己写的字”。我通常只对训练集做增强,测试集保持原样,这个做法在论文里说成“数据增强提升泛化性”就很自然。

6. 验收与交付:用自己写的数字做最后一关验证

期末项目验收不是看训练集准确率,而是看现场演示。很多同学在MNIST测试集上跑出99%,结果手写板画一个“4”识别成“9”,直接翻车。我现在的习惯是交付前先自己写10个数字,每个写两三遍,记录正确数。如果“8”总被识别成“0”,先不急着换模型,而是看预处理后的28×28图长什么样。大部分问题都出在数字没有居中、笔画断裂或粗细不一致上,可以回到extract_digit里调节缩放比例。

演示截图最好准备一张对照表,把自己写的数字、真实标签、预测标签、最近邻样本拼在一起。用一个简单脚本循环10个数字就能生成,放进文档说明的“效果展示”一节,答辩时非常有说服力。同时在文档里写清楚运行步骤:先安装依赖,再运行训练脚本,最后运行手写板脚本。不要以为老师会读你的注释,把每条命令写清楚,才是最实际的事。

我一般还会在源码里放一个config.py,把K值、二值化阈值、HOG参数、训练集路径等集中管理。换参数时不需要改动业务代码,调试和答辩都省心。文档说明里再附一张参数表,老师问“为什么K取5”时,你直接说“扫描过3、5、7、9,取测试集准确率最高的”。这一句话比空谈算法更让人信服。

如果时间充裕,可以把识别错误样本单独存一个文件夹,分析哪些数字容易混淆。3和5、7和1、4和9是最常见的组合。知道这一点后,演示时主动写几个有代表性的数字,反而显得你做过充分测试。希望你用这套方案把期末大作业稳下来,也少熬夜。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进