news 2026/10/19 6:11:30

手写数字识别实战:纯NumPy手撕CNN实现99%准确率

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写数字识别实战:纯NumPy手撕CNN实现99%准确率

简介:本资源是一份基于Python与卷积神经网络(CNN)实现手写数字识别的完整实践项目,面向机器学习初学者、计算机视觉入门者及高校课程设计学生,聚焦图像分类核心任务,解决MNIST类手写体识别建模与部署问题。压缩包共4个文件,含3个CSV数据文件(train.csv训练集、test.csv测试集、mnist_test_predictions.csv预测结果)和1个可直接运行的Python脚本(simple-cnn-in-python-99.py),涵盖数据加载、归一化预处理、One-Hot编码、CNN模型构建(含卷积/池化/全连接层)、Adam优化器训练、99%高精度评估等全流程代码,包体大小为13.26MB,结构精炼、即下即用。已有472人学习下载,读者可获得一套经实测验证的轻量级CNN实现方案,包含清晰的数据流逻辑、关键超参配置说明及预测结果分析入口,便于理解特征提取机制、复现实验效果并拓展至其他图像分类任务。

1. 这不是玩具模型:一个99%准确率的手写数字识别项目,能直接跑通、改数据、换网络、接摄像头

上周帮某高校实验室调试一个嵌入式图像采集终端,现场发现他们还在用 OpenCV 模板匹配做数字表盘识别——误判率高达 23%,每次校准都要人工标定 5 分钟。我顺手把simple-cnn-in-python-99.py往树莓派上一扔,喂了 200 张手机拍的模糊数字图(非 MNIST 格式),只改了 3 行预处理代码,准确率就稳在 94.7%。这不是调参玄学,而是这个项目真正落地的起点:它不依赖 Keras 高阶封装,不用 TensorFlow 2.x 的自动图机制,纯 PyTorch + NumPy 实现 CNN 前向/反向传播,所有张量运算裸写,连torch.nn.Conv2d都没碰——你看到的每一行conv2d_forward()都是手撕卷积核滑动、padding、stride 的真实过程。它适合三类人:想搞懂 CNN 底层怎么算的初学者、需要轻量级可审计模型部署到边缘设备的工程师、以及正在为课程设计卡在“训练不收敛”而熬夜的 A 同学。项目包里那 4 个文件不是示例,是完整闭环:train.csv和test.csv是原始像素 CSV(784 列+1标签列),mnist_test_predictions.csv是推理结果存档,而simple-cnn-in-python-99.py是唯一主程序——没有 requirements.txt?因为只依赖numpy和pandas;没有 config.yaml?所有超参都在脚本顶部CONFIG = {...}字典里硬编码。现在,我们从解压那一刻开始,把它变成你电脑上第一个能自己画数字、实时识别的黑匣子。

2. 目录结构与数据格式:先看清这 4 个文件到底在说什么

这个项目没有 src/、model/、data/ 这种现代工程目录,它的结构就是极简主义的生存逻辑:所有东西平铺,靠命名和注释自解释。但恰恰是这种“土味结构”,藏着最容易被忽略的细节雷区。我们逐个拆解,重点不是“它是什么”,而是“你怎么验证它没坏”。

2.1 文件清单与原始数据形态

项目压缩包解压后只有 4 个文件,但它们的尺寸和内容特征必须人工确认:

文件名典型大小关键验证点为什么必须查
train.csv≈ 45MB第一行是否为label,pixel0,pixel1,...,pixel783?总列数是否为 785?行数是否 ≥ 42000?若无 header 或列数错,pandas 读取会错位,标签全乱
test.csv≈ 7.5MB是否无 label 列?仅 784 列(pixel0~pixel783)?测试集若含 label,后续预测会报维度错;MNIST 官方 test.csv 确实无 label
mnist_test_predictions.csv≈ 40KB是否仅 1 列prediction,共 10000 行?值是否为 0~9 整数?这是训练完成后的黄金验证标准,比 accuracy 打印更可信
simple-cnn-in-python-99.py≈ 12KB是否包含def conv2d_forward(input, weight, bias, stride=1, padding=0)函数?这是手写 CNN 的心脏,没这个函数就不是真手写

提示:别信文件名!用head -n 3 train.csv和wc -l train.csv实测。某次我下载的train.csv实际是 Kaggle 版(label 在最后一列),导致前 2 小时训练全在拟合噪声。

2.2 CSV 数据的像素矩阵真相:为什么不是直接reshape(28,28)

train.csv每行是 785 个数字:第 0 列是label(0~9),第 1~784 列是pixel0到pixel783。关键陷阱在于:这些 pixel 列不是按图像扫描顺序排列的。MNIST 像素存储是“行优先”(row-major),即第 0 行从左到右是pixel0~pixel27,第 1 行是pixel28~pixel55……所以pixel783是最后一行最后一列(27,27)。验证方法:

import pandas as pd df = pd.read_csv("train.csv", nrows=1) # 取第一张图(label=1),提取 pixel 列 pixels = df.iloc[0, 1:].values.astype('uint8') # shape=(784,) img_1d = pixels.reshape(28, 28) # 正确:得到 28x28 矩阵 # 错误写法:img_1d = pixels.reshape(28, 28).T # 转置后图像左右颠倒!

注意:reshape(28,28)后直接plt.imshow(img_1d, cmap='gray')应该显示一个清晰的数字(如 1 是竖线)。如果显示为模糊色块或镜像,一定是 reshape 方向错了。这是新手最常翻车的第一步——连输入数据都没看对,后面全是空中楼阁。

2.3simple-cnn-in-python-99.py的模块化切口:找到你能改的第一行

打开脚本,不要从头读。直接搜索# === CONFIG SECTION ===,你会看到:

# === CONFIG SECTION === CONFIG = { "BATCH_SIZE": 64, "EPOCHS": 15, "LEARNING_RATE": 0.001, "VALIDATION_SPLIT": 0.1, "CONV1_OUT_CHANNELS": 32, "CONV2_OUT_CHANNELS": 64, "FC_HIDDEN_SIZE": 128, "DROPOUT_RATE": 0.5, }

这就是你的控制台。所有超参集中在此,没有隐藏配置。再搜# === DATA LOADING ===,定位到数据加载核心:

# === DATA LOADING === train_df = pd.read_csv("train.csv") X_train = train_df.iloc[:, 1:].values.astype('float32') / 255.0 # 归一化到 [0,1] y_train = train_df.iloc[:, 0].values.astype('int64') # ... 后续有 train/val 划分、one-hot 编码等

看到没?/ 255.0是归一化,astype('float32')是精度控制。这里就是你接入自己数据的入口:只要保证你的 CSV 也是 784 像素+1标签,替换pd.read_csv("your_data.csv")即可。不需要改模型结构,不需要重写 DataLoader。

3. CNN 核心模块拆解:手写卷积、池化、全连接的每一步计算

这个项目最硬核的价值,在于它把 CNN 的三大核心操作全部用 NumPy 手写实现,不调用任何深度学习框架的底层算子。这意味着你不仅能跑通,还能在任意一行打断点,看到张量如何变形、梯度如何回传。我们聚焦三个函数:conv2d_forward、max_pool2d_forward、linear_forward,它们定义了整个网络的骨架。

3.1conv2d_forward:卷积核滑动的真实代价

这是整个项目最值得逐行盯的函数。它接收输入特征图input(shape:[C_in, H_in, W_in])、卷积核weight(shape:[C_out, C_in, K, K])、偏置bias(shape:[C_out]),返回输出特征图output(shape:[C_out, H_out, W_out])。关键参数stride=1,padding=0是默认值,但函数内部必须手动处理边界。

def conv2d_forward(input, weight, bias, stride=1, padding=0): C_in, H_in, W_in = input.shape C_out, _, K, _ = weight.shape # 计算输出尺寸(向下取整) H_out = (H_in + 2*padding - K) // stride + 1 W_out = (W_in + 2*padding - K) // stride + 1 output = np.zeros((C_out, H_out, W_out)) # 手动 padding:只在 H/W 维度补 0 if padding > 0: input_padded = np.pad(input, ((0,0), (padding,padding), (padding,padding)), mode='constant') else: input_padded = input # 核心:三重循环遍历输出位置 (c_out, h_out, w_out) for c_out in range(C_out): for h_out in range(H_out): for w_out in range(W_out): # 计算输入区域起始坐标 h_start = h_out * stride w_start = w_out * stride # 截取输入区域:[C_in, K, K] input_region = input_padded[:, h_start:h_start+K, w_start:w_start+K] # 卷积计算:sum over C_in and KxK output[c_out, h_out, w_out] = np.sum(input_region * weight[c_out]) + bias[c_out] return output

逻辑说明与参数深挖:

  • input_padded的np.pad调用中,((0,0), (padding,padding), (padding,padding))表示:通道维不 pad,高维前后各 padpadding行,宽维同理。这是 NumPy 的 pad 语法,极易写错维度。
  • 三重循环是性能瓶颈,但正是它让你看清:每个输出点(c_out, h_out, w_out)对应输入上一个KxK区域与一个卷积核的逐元素乘加。np.sum(input_region * weight[c_out])就是卷积的数学定义。
  • stride=1时,h_start每次+1;若改为stride=2,则h_start = h_out * 2,输出尺寸减半。这就是下采样本质。

3.2max_pool2d_forward:没有魔法的池化

池化层同样手写,且只实现2x2最大池化(kernel_size=2,stride=2),这是 MNIST 任务的黄金组合:

def max_pool2d_forward(input, kernel_size=2, stride=2): C, H_in, W_in = input.shape H_out = (H_in - kernel_size) // stride + 1 W_out = (W_in - kernel_size) // stride + 1 output = np.zeros((C, H_out, W_out)) for c in range(C): for h_out in range(H_out): for w_out in range(W_out): h_start = h_out * stride w_start = w_out * stride # 取 2x2 区域 region = input[c, h_start:h_start+kernel_size, w_start:w_start+kernel_size] output[c, h_out, w_out] = np.max(region) # 真实的最大值,不是索引 return output

为什么只做 2x2?因为 MNIST 图像小(28x28),两次 2x2 池化后尺寸变为28→14→7,再接全连接层参数量可控。若你换成 64x64 图像,必须改kernel_size或增加池化层数,否则最后特征图太大,FC 层权重爆炸。

3.3linear_forward:全连接层的矩阵乘法本质

CNN 提取特征后,需展平(flatten)送入全连接层。linear_forward就是Y = X @ W.T + b的朴素实现:

def linear_forward(input, weight, bias): # input: (batch_size, in_features) -> 例如 (64, 3136) 来自 flatten 后 # weight: (out_features, in_features) -> 例如 (128, 3136) # bias: (out_features,) output = np.dot(input, weight.T) + bias # 注意 weight.T! return output

关键细节:np.dot(input, weight.T)而非np.dot(input, weight)。因为 PyTorch/NumPy 的线性层定义是output = input @ weight.T + bias,weight的 shape 是(out_features, in_features)。若你误写成weight,矩阵乘法会因维度不匹配而报错,这是血泪经验。

4. 训练流程与反向传播:损失、梯度、权重更新的完整链路

训练不是黑箱。这个项目把前向传播、损失计算、反向传播、权重更新四步全部展开,每一步都对应一个独立函数。理解它们,你就拿到了修改模型的手术刀。

4.1 前向传播:从输入到预测的完整链条

主训练循环中的前向部分长这样:

# --- 前向传播 --- z1 = conv2d_forward(X_batch, W1, b1) # Conv1: [32,28,28] a1 = relu_forward(z1) # ReLU z2 = max_pool2d_forward(a1) # Pool1: [32,14,14] z3 = conv2d_forward(z2, W2, b2) # Conv2: [64,12,12] (28->14 after pool, then 14-3+1=12) a3 = relu_forward(z3) z4 = max_pool2d_forward(a3) # Pool2: [64,6,6] z5 = flatten_forward(z4) # Flatten: [64, 2304] (64*6*6) z6 = linear_forward(z5, W3, b3) # FC1: [64,128] a6 = relu_forward(z6) z7 = linear_forward(a6, W4, b4) # FC2 (output): [64,10] preds = softmax_forward(z7) # Softmax: [64,10]

注意flatten_forward的实现:

def flatten_forward(input): # input: (C, H, W) -> output: (C*H*W,) return input.reshape(-1) # 一维化,为 FC 层准备

这里reshape(-1)是关键。z4是[64,6,6],flatten_forward输出[2304,],但linear_forward要求输入是[batch_size, features],所以实际调用前需z5 = z4.reshape(batch_size, -1)。项目中batch_size是动态的(取决于当前 batch),因此flatten_forward必须配合reshape使用,不能只返回一维数组。

4.2 损失与反向传播:交叉熵 + 手写梯度

损失函数用标准多分类交叉熵:

def cross_entropy_loss(preds, targets): # preds: [batch, 10], targets: [batch] (class indices) batch_size = preds.shape[0] # 取每个样本对应真实类别的概率 correct_logprobs = -np.log(preds[range(batch_size), targets]) loss = np.sum(correct_logprobs) / batch_size return loss

反向传播是重点。以输出层为例,softmax_backward返回的是dL/dz7(损失对 logits 的梯度):

def softmax_backward(preds, targets): # preds: softmax output [batch,10], targets: [batch] batch_size = preds.shape[0] dL_dz = preds.copy() dL_dz[range(batch_size), targets] -= 1 # 数学推导:dL/dz_i = pred_i - (i==target) dL_dz /= batch_size return dL_dz

然后梯度沿网络反向流动:

# --- 反向传播 --- dL_dz7 = softmax_backward(preds, y_batch) # [64,10] dL_da6, dW4, db4 = linear_backward(dL_dz7, a6, W4) # dL/da6: [64,128], dW4: [10,128] dL_dz6 = relu_backward(dL_da6, z6) # [64,128] dL_da3, dW3, db3 = linear_backward(dL_dz6, z5, W3) # dL/da3: [64,2304] dL_dz4 = flatten_backward(dL_da3, z4.shape) # [64,6,6] dL_da3_pool = max_pool2d_backward(dL_dz4, a3) # [64,12,12] dL_dz3 = relu_backward(dL_da3_pool, z3) # [64,12,12] dL_da2, dW2, db2 = conv2d_backward(dL_dz3, z2, W2) # dL/da2: [64,14,14] dL_dz2 = max_pool2d_backward(dL_da2, a1) # [64,14,14] dL_da1 = relu_backward(dL_dz2, z1) # [64,28,28] dL_dX, dW1, db1 = conv2d_backward(dL_da1, X_batch, W1) # dL/dX: [64,1,28,28]

conv2d_backward的核心逻辑(简化版):

def conv2d_backward(dL_dz, input, weight): # dL_dz: [C_out, H_out, W_out] # input: [C_in, H_in, W_in] # weight: [C_out, C_in, K, K] C_in, H_in, W_in = input.shape C_out, _, K, _ = weight.shape dL_dinput = np.zeros_like(input) dL_dweight = np.zeros_like(weight) for c_out in range(C_out): for h_out in range(dL_dz.shape[1]): for w_out in range(dL_dz.shape[2]): h_start = h_out * stride w_start = w_out * stride # 梯度分配到输入区域 dL_dinput[:, h_start:h_start+K, w_start:w_start+K] += \ dL_dz[c_out, h_out, w_out] * weight[c_out] # 梯度计算卷积核 input_region = input[:, h_start:h_start+K, w_start:w_start+K] dL_dweight[c_out] += dL_dz[c_out, h_out, w_out] * input_region return dL_dinput, dL_dweight, np.sum(dL_dz, axis=(1,2)) # db = sum over H,W

这就是反向传播的物理意义:dL_dz的每个值,按正向时的映射关系,分散回它所依赖的输入区域和卷积核位置。

4.3 权重更新:SGD 的朴素力量

最后,用最简单的随机梯度下降(SGD)更新:

# --- 权重更新 --- W1 -= CONFIG["LEARNING_RATE"] * dW1 b1 -= CONFIG["LEARNING_RATE"] * db1 W2 -= CONFIG["LEARNING_RATE"] * dW2 b2 -= CONFIG["LEARNING_RATE"] * db2 W3 -= CONFIG["LEARNING_RATE"] * dW3 b3 -= CONFIG["LEARNING_RATE"] * db3 W4 -= CONFIG["LEARNING_RATE"] * dW4 b4 -= CONFIG["LEARNING_RATE"] * db4

没有动量,没有 Adam,就是纯粹的w = w - lr * dw。这反而让调试更透明:如果你发现 loss 不降,直接打印np.mean(np.abs(dW1)),若为 0 说明梯度消失;若极大(如 >1e5)说明梯度爆炸。这是框架封装后你永远看不到的底层信号。

5. 避坑指南:那些让我重启三次 Python 解释器的致命错误

这个项目看似简单,但手写 CNN 的每一个环节都是经典坑位。以下是我踩过的、有明确现象-原因-解决方案的 4 个高频问题,按发生频率排序:

5.1 现象:训练刚开始 loss 就 nan,或者几轮后突变为 inf

原因:softmax_forward中np.exp(x)溢出。当 logitsz7的某个值过大(如 >88),exp(88)在 float64 下就溢出为 inf,后续log(inf)为 inf,cross_entropy得 nan。
解决:在softmax_forward中加入数值稳定技巧——减去每行最大值:

def softmax_forward(z): # z: [batch, 10] z_shifted = z - np.max(z, axis=1, keepdims=True) # 关键! exp_z = np.exp(z_shifted) return exp_z / np.sum(exp_z, axis=1, keepdims=True)

提示:这个np.max(..., keepdims=True)必须加,否则z - np.max(z, axis=1)会广播失败。这是 NumPy 广播规则的典型陷阱。

5.2 现象:测试准确率始终在 10%(随机猜)附近,loss 不下降

原因:train.csv和test.csv的标签列错位。常见情况是train.csv的 label 在最后一列(Kaggle 格式),而代码默认iloc[:, 0]取第一列。
解决:先用pandas探查:

import pandas as pd tr = pd.read_csv("train.csv", nrows=5) print("First 5 rows of train.csv:") print(tr.head()) print(f"Columns: {tr.columns.tolist()}") # 若输出类似 ['pixel0', 'pixel1', ..., 'label'],则 label 在最后一列 # 修改代码:y_train = tr.iloc[:, -1].values.astype('int64') # 取最后一列

5.3 现象:conv2d_forward报IndexError: index 28 is out of bounds for axis 1 with size 28

原因:padding=0时,h_start+K超出H_in。例如H_in=28,K=3,h_out=27时,h_start=27,h_start+K=30 > 28。
解决:检查H_out计算公式,确保向下取整正确:

H_out = (H_in + 2*padding - K) // stride + 1 # 当 H_in=28, K=3, padding=0, stride=1: (28-3)//1 +1 = 25+1 = 26 ✅ # 但若误写为 (H_in - K) // stride,则 (28-3)//1 = 25 ❌ 少 1 行

注意://是整除,不是/。用/会得浮点数,再int()可能截断。

5.4 现象:mnist_test_predictions.csv生成后,用pandas读取报ParserError: Error tokenizing data

原因:预测结果保存时用了逗号分隔,但predictions是整数数组,np.savetxt默认%d格式,若中间有空格或制表符混入,CSV 解析失败。
解决:严格指定保存格式:

# 替换原代码中的保存行 # np.savetxt("mnist_test_predictions.csv", predictions, delimiter=",", fmt="%d") # 改为: with open("mnist_test_predictions.csv", "w") as f: f.write("prediction\n") # 写 header for p in predictions: f.write(f"{p}\n") # 每行一个整数,无空格

这样生成的 CSV 是纯文本,pandas.read_csv("mnist_test_predictions.csv")绝对安全。

6. 二次开发实战:从 MNIST 到自定义手写数据的三步改造

现在你已掌握这个项目的全部脉络。下一步不是“优化模型”,而是让它为你服务:接入你手机拍的作业本照片、工厂质检的数字铭牌、甚至孩子写的歪歪扭扭的 0~9。改造的核心原则是——不动模型结构,只动数据管道。以下是经过某公司产线验证的三步法:

6.1 第一步:数据预处理适配——把任意图片变train.csv

你的数据源可能是 JPG/PNG,而非 CSV。别碰模型,只写一个image_to_csv.py:

import cv2 import numpy as np import pandas as pd from pathlib import Path def preprocess_image(img_path, target_size=(28,28)): """将任意图片转为 MNIST 格式单通道灰度图""" img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(f"Cannot load {img_path}") # 二值化:Otsu 自适应阈值,对付阴影和反光 _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 取反:MNIST 是白字黑底,binary 是黑字白底 binary = 255 - binary # 调整大小并居中 resized = cv2.resize(binary, target_size, interpolation=cv2.INTER_AREA) # 归一化到 [0,1],float32 return resized.astype('float32') / 255.0 def create_csv_from_images(image_dir, csv_path, label_map=None): """ image_dir/0/xxx.jpg -> label 0 image_dir/1/yyy.png -> label 1 ... """ image_paths = list(Path(image_dir).rglob("*.[jJ][pP][gG]")) + \ list(Path(image_dir).rglob("*.[pP][nN][gG]")) rows = [] for img_path in image_paths: try: # 从文件夹名取 label label = int(img_path.parent.name) if label_map and label in label_map: label = label_map[label] # 映射到新类别 img_array = preprocess_image(img_path) # 展平为 784 像素 + label pixel_row = [label] + img_array.flatten().tolist() rows.append(pixel_row) except Exception as e: print(f"Skip {img_path}: {e}") continue # 生成 DataFrame columns = ["label"] + [f"pixel{i}" for i in range(784)] df = pd.DataFrame(rows, columns=columns) df.to_csv(csv_path, index=False) print(f"Saved {len(rows)} samples to {csv_path}") # 使用示例 # create_csv_from_images("my_handwritten_data/", "my_train.csv")

关键点:cv2.THRESH_OTSU处理光照不均,255 - binary确保白字黑底(MNIST 标准),interpolation=cv2.INTER_AREA防缩放锯齿。运行后,my_train.csv就能直接喂给simple-cnn-in-python-99.py。

6.2 第二步:模型微调——冻结卷积层,只训全连接

当你只有 200 张自定义图时,从头训 CNN 会过拟合。方案:加载预训练权重(用原项目训好的W1,W2,W3,W4),冻结前两层卷积,只更新 FC 层:

# 在 simple-cnn-in-python-99.py 开头添加 PRETRAINED_WEIGHTS_PATH = "pretrained_weights.npz" def load_pretrained_weights(): if not Path(PRETRAINED_WEIGHTS_PATH).exists(): return None weights = np.load(PRETRAINED_WEIGHTS_PATH) return { "W1": weights["W1"], "b1": weights["b1"], "W2": weights["W2"], "b2": weights["b2"], "W3": weights["W3"], "b3": weights["b3"], "W4": weights["W4"], "b4": weights["b4"], } # 在初始化权重处修改 pretrained = load_pretrained_weights() if pretrained: W1, b1 = pretrained["W1"], pretrained["b1"] W2, b2 = pretrained["W2"], pretrained["b2"] # W3, b3, W4, b4 仍随机初始化,或也加载 else: # 原随机初始化逻辑...

然后在训练循环中,只更新W3,b3,W4,b4,W1,b1,W2,b2的梯度设为 0 或跳过更新。这是迁移学习的最小成本实现。

6.3 第三步:实时推理封装——写一个能拍照识别的 CLI 工具

最终交付物不该是.py脚本,而是一个命令行工具。新建recognize.py:

import cv2 import numpy as np from simple_cnn_in_python_99 import * # 导入原项目所有函数 def recognize_from_camera(): cap = cv2.VideoCapture(0) print("Press 'q' to quit, 's' to snap and recognize") while True: ret, frame = cap.read() if not ret: break # 显示实时画面 cv2.imshow("Camera", frame) key = cv2.waitKey(1) & 0xFF if key == ord('q'): break elif key == ord('s'): # 截图 -> 预处理 -> 推理 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 找数字区域(简单阈值+轮廓) _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: # 取最大轮廓 cnt = max(contours, key=cv2.contourArea) x,y,w,h = cv2.boundingRect(cnt) digit_roi = gray[y:y+h, x:x+w] # 预处理同上 processed = preprocess_image_from_array(digit_roi) # 推理(复用原项目 forward 函数) pred = forward_single_image(processed, W1,b1,W2,b2,W3,b3,W4,b4) print(f"Recognized: {pred}") cap.release() cv2.destroyAllWindows() # 使用:python recognize.py if __name__ == "__main__": recognize_from_camera()

从那以后我每次接手新图像识别需求,都强制走一遍这三步:先用image_to_csv.py把客户给的 50 张模糊图转成 CSV,再用原项目训 5 轮看 baseline,最后用recognize.py拉通摄像头 pipeline。省掉 80% 的框架胶水代码,问题直接暴露在数据质量上。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 6:11:27

版本对比逆向分析:figugegl.1与figugegl.2改动全解析

拿到这个标题,第一反应是“figugegl.1和figugegl.2”这种命名方式太有迷惑性了,像随手起的临时文件名,也可能是内部代号。实际逆向分析时,这种命名恰恰是突破口之一:版本号的递增往往意味着功能迭代、漏洞修复&#xf…

作者头像 李华
网站建设 2026/10/19 6:10:53

eNSP校园网实战工程:OSPF多区域+VRRP热备+防火墙策略全链路解析

简介:本资源是一套面向计算机网络课程学习、毕业设计与期末大作业的高分实践方案,专为中等难度网络工程实训打造,适用于高校学生、助教及教学一线教师。方案基于华为eNSP平台完成大型校园网络拓扑设计,覆盖核心层、汇聚层、接入层…

作者头像 李华
网站建设 2026/10/19 6:09:56

外包CRUD一年后如何自救:技术成长与破局实操指南

很久没有写东西了,正好这个选题我想聊一聊。经常有人问我:在外包公司整天写 CRUD,到底多久会废掉?我的答案是:1 年左右。不是我吓你,因为这个时间点恰好是我自己进外包之后最明显的分水岭。刚开始觉得什么系…

作者头像 李华
网站建设 2026/10/19 6:09:56

手工创作全流程解析:从材料实验到作品固化,打造个人风格

做手工这件事,最迷人的地方恰恰在于它不追求“标准答案”。我拿到“artcraft”这个项目名时,第一反应是它把 art 和 craft 捏在了一起——艺术感与手艺感并重,既要好看,又要经得起细看。这不是一条简单的“照着教程做”的路子&…

作者头像 李华
网站建设 2026/10/19 6:08:49

AI视频生成原理与可控工作流:从时间维度到行业落地

前阵子某高校面向行业办了一期AI视频生成专题研修,报名的人里做内容、做产品、做开发的都有。我在课后把整条技术链路重新梳理了一遍,发现一个很要命的理解偏差:很多人把AI视频生成当成“逐帧生成再拼接”,觉得无非就是图像生成多…

作者头像 李华
网站建设 2026/10/19 6:08:17

Linux信号处理函数从内核中断流程到工程避坑全解析

从事Linux服务端开发这些年,我见过太多同行在信号处理函数上栽跟头:有的进程随机无响应,有的日志写到一半卡死,有的明明只是处理一个SIGINT程序却整个栈被压穿。这些问题的根源,几乎都指向同一个点——很多人把信号处理…

作者头像 李华