ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

AI 破壁录——SVM 手写数字识别:数据加载 → 归一化 → PCA → 调参,全程拆解到 97.53%

2026/8/2 11:44:16 拓冰建站 浏览量
AI 破壁录——SVM 手写数字识别:数据加载 → 归一化 → PCA → 调参,全程拆解到 97.53% SVM 手写数字识别从零到 97.53% 的调参血泪史【1】加载 MNIST 数据代码 数据加载 · 手动解析 MNIST 二进制importstruct,gzip,os,numpyasnpdefload_mnist_images(path):withgzip.open(path,rb)asf:magic,num,rows,colsstruct.unpack(IIII,f.read(16))returnnp.frombuffer(f.read(),dtypenp.uint8).reshape(num,rows*cols)defload_mnist_labels(path):withgzip.open(path,rb)asf:magic,numstruct.unpack(II,f.read(8))returnnp.frombuffer(f.read(),dtypenp.uint8)data_dirmnist_data# 改成你的 MNIST 文件夹路径X_allload_mnist_images(f{data_dir}/train-images-idx3-ubyte.gz)y_allload_mnist_labels(f{data_dir}/train-labels-idx1-ubyte.gz)X_testload_mnist_images(f{data_dir}/t10k-images-idx3-ubyte.gz)y_testload_mnist_labels(f{data_dir}/t10k-labels-idx1-ubyte.gz)这段代码在干什么MNIST 是 .gz 压缩包里的二进制文件——不是 txt不是 csv就是一堆0101。我当初在这里翻了大车fetch_openml 连不上keras 装不了Python3.14 太新tensorflow 不兼容。最后手动从 AWS 下载 .gz文件自己解析二进制。struct.unpack 就像一个翻译官把 0101 翻译成人能读的数字前 16 字节magic2051, num60000, rows28, cols28后面的字节每个 一个像素灰度值0黑, 255白60000 × 28 × 28 4700 万个字节全部排成 (60000, 784)的表格。一张图 784 个数字。数学原理这一步没有公式。但有一个核心概念——机器学习里一张图片 一个高维向量。784 个像素值 这个向量在 784 维空间里的坐标。SVM要做的事就是在这个 784 维空间里找到一条分界线。【2】拆分训练集和测试集✂️ 数据拆分 · 练习题 vs 考试题X_trainX_all[:40000]# 前 40000 张当练习题y_trainy_all[:40000]# 对应的答案# X_test 和 y_test 已经在【1】里读好了是完整的 10000 张print(f训练集:{X_train.shape[0]}张, 测试集:{X_test.shape[0]}张)这段代码在干什么从 6 万张训练图里取前 4 万张当练习题1 万张测试图全部当考试题。为什么不取全量 6 万我试过——SVM 训练时间跟样本数的平方成正比4万张刚好够学到 97.53%6 万张要多等好几倍时间性价比不高。数学原理机器学习的铁律——训练和测试不能混在一起。就像准备期末考试训练集 练习题带答案测试集 期末考卷答案被老师收着。如果考前偷看了考卷考试 100分也是假的。数学上这是 IID假设考试题和练习题来自同一个分布但具体题目不能重复。【3】归一化⚖️ 归一化 · 所有像素公平竞争fromsklearn.preprocessingimportStandardScaler scalerStandardScaler()X_trainscaler.fit_transform(X_train)# 学均值方差 标准化X_testscaler.transform(X_test)# 用训练集的均值方差标准化这段代码在干什么原始像素值范围 0~255。但有的像素天生波动大比如边缘有的波动小比如背景数值大的会在距离计算中欺负数值小的。归一化做的事——对每个像素位置算训练集里的均值和标准差然后把每个值变成离均值几个标准差旧值 200, 均值 μ 100, 标准差 σ 50新值 (200 − 100) / 50 2.0 → “比平均白 2 个档次”旧值 50, 同上 → (50−100)/50 −1.0 → “比平均暗 1 个档次”做完后所有像素均值 0、标准差 1——公平竞争。⚠️ 翻车现场这组数据我加了归一化后准确率反而从 92.9% 降到了 91.9%。说明不是所有流程都是必须的要试。但保留它作为标准步骤是好习惯。关键细节测试集用训练集的 μ 和σ不能自己另算。就像高考评分标准必须和模拟考一致。数学原理SVM 核心是算距离。如果像素 A 范围 0~255像素 B 范围 0~16A会在距离计算中碾压 B。归一化把所有像素拉到同一个尺度——就像比较身高和体重不能一个用厘米一个用千克。【4】PCA 降维 PCA 降维 · 784 维 → 100 维fromsklearn.decompositionimportPCA pcaPCA(n_components100)# 784 维 → 100 维X_trainpca.fit_transform(X_train)X_testpca.transform(X_test)print(fPCA: 784 → 100 (保留{pca.explained_variance_ratio_.sum()*100:.0f}% 信息))这段代码在干什么784 个像素听起来多但相邻像素总是差不多——大量冗余。PCA做的事找到 784 个像素里真正独立的变化方向只保留最重要的 100个。我的理解是——就像把 10 科成绩浓缩成理科能力“文科能力”“体育”丢了点细节但核心意思还在保留约 91% 信息。这一步救了命784 维 → 100 维SVM 训练快 8倍而且挤掉了噪声像素准确率反而不降。浓缩的才是精华。数学原理——矩阵分解通俗版PCA 做的是协方差矩阵的特征值分解。把 784维的数据看成一个椭球——哪些方向上数据散得最开方差大、信息多哪些方向上几乎不变方差小、可以扔。数学上新特征矩阵 原数据 × 最重要的 100个特征向量。过程叫投影——把数据投影到信息最丰富的那 100个方向上。【5】训练 SVM — 整个项目的核心 SVM 核心 · RBF 核训练fromsklearn.svmimportSVC modelSVC(kernelrbf,C10,gamma0.001)model.fit(X_train,y_train)两行代码。第二行 model.fit()背后做了一个很长的计算——找到一条最大间隔的分界线。下面把 SVM 从直觉到公式全部掰开。5.1 直觉——画三八线SVM 做的事用一句话就能说清楚这也是我上手时的第一反应▎ 在两类东西之间找一条最宽的空白带画线分开。不是随便画条线而是画一条两边空隙最宽的线。为什么空隙越宽以后来了新同学越不容易站错边。5.2 分界线w·x b 0二维画面里的直线写成 ax by c 0。换成 SVM 的写法w·x b 0w 线的方向就像改变 a 和 b 能旋转这条线b 线的位置上下平移就像改 c训练 SVM 找到最优的 w 和 b。学生被分到两边w·x b 0 → 正类●输出 1w·x b 0 → 负类○输出 −15.3 空隙宽度 2 / ||w||线条两边各加一道护栏w·x b 1 → 右边护栏w·x b −1 → 左边护栏从右边护栏到分界线的距离 1 / ||w||左边同理空隙 左边距离 右边距离 1/||w|| 1/||w|| 2/||w||||w|| 就是 w 的长度勾股定理推广到高维w [3, 4] → ||w|| √(3² 4²) 5w [1, 1] → ||w|| √(1² 1²) ≈ 1.4||w|| 越小 → 空隙越宽 → SVM 的目标就是最小化 ||w||。5.4 硬间隔——所有人都要站对边目标min ½||w||² ← 让空隙最宽约束yᵢ(w·xᵢ b) ≥ 1, for 每一个样本 i ← 所有人站护栏外yᵢ(w·xᵢ b) ≥ 1 用例子理解正类 (y1)w·xb3 → 1×33 ≥ 1 ✅ 站得稳正类 (y1)w·xb0.5 → 0.5 1 ❌ 犯规了负类 (y−1)w·xb−3 → −1×(−3)3 ≥ 1 ✅ 站得稳5.5 软间隔——允许少数人站错 C 的作用现实中数据有噪声不可能所有人完美站对。引入松弛变量 ξ目标min ½||w||² C × Σ ξᵢ ← 空隙宽 犯规总罚款约束yᵢ(w·xᵢb) ≥ 1 − ξᵢ ← 护栏放宽了ξᵢ ≥ 0 ← 犯规程度不能是负数ξ 的含义┌───────────┬──────────────────────────┐│ ξ 值 │ 状态 │├───────────┼──────────────────────────┤│ ξ 0 │ 没犯规站得稳 │├───────────┼──────────────────────────┤│ 0 ξ 1 │ 钻进护栏里了但没站反边 │├───────────┼──────────────────────────┤│ ξ 1 │ 彻底站反边了 │└───────────┴──────────────────────────┘C 就是你定的罚款——我当初把它比作三种老师┌──────┬────────────────────────┬──────────────────────────┐│ C 值 │ 老师类型 │ 效果 │├──────┼────────────────────────┼──────────────────────────┤│ 0.1 │ 佛系“算了算了” │ 线平滑空隙宽容许犯错 │├──────┼────────────────────────┼──────────────────────────┤│ 10 │ 正常“该严的严” │ 平衡 │├──────┼────────────────────────┼──────────────────────────┤│ 1000 │ 强迫症“一个都不许错” │ 线弯弯绕绕过拟合 │└──────┴────────────────────────┴──────────────────────────┘数学上 C 还是个天花板每个点的权重 α 被限制在 0 ≤ α ≤C。这是我后来才搞明白的——C 不是影响力本身C 是影响力的上限。α才是实际影响力。5.6 拉格朗日乘子——把约束吃掉“最小化目标函数 满足约束”这个带约束的问题拉格朗日乘子法把它变成一个无约束问题L(w,b,α) ½||w||² − Σ αᵢ[yᵢ(w·xᵢb) − 1]αᵢ 就像一个监督员监督第 i 个点站没站对站对了 → y(w·xb) 1 → α0不用管站错了 → y(w·xb) 1 → α0罚款刚好站在护栏上 → y(w·xb)1 → α0支持向量就是这几条规则——站对了消失站错了罚款——让我真正懂了拉格朗日在干什么。5.7 求导 0 → w Σαᵢyᵢxᵢ“求导 找谷底”。把 L 对 w 求偏导令其等于 0∂L/∂w w − Σ αᵢ yᵢ xᵢ 0→ w Σ αᵢ yᵢ xᵢ这是整个 SVM 最核心的公式。含义w 只由 α ≠ 0 的点——也就是支持向量——的加权和决定。其他 39997个点α 0删了也不影响分界线。支持向量机这个名字就是这么来的。5.8 核函数——画曲线的秘密数据可能在低维空间直线分不开。rbf 核做的事把这些点升维到一个高维空间。在那画一条直线回到原空间就变成了一条曲线。rbf 核的数学形式K(x₁, x₂) exp(−γ × ||x₁ − x₂||²)不用真的升维那会爆炸直接算升维后的内积。这是核技巧的核心。γ 的作用——我亲手验证过┌───────┬──────────────────────────┬───────────────────────────┐│ γ │ 效果 │ 准确率 │├───────┼──────────────────────────┼───────────────────────────┤│ 0.001 │ 山包宽而平缓分界线光滑 │ 97.53% │├───────┼──────────────────────────┼───────────────────────────┤│ 0.005 │ 还行 │ 96.5% │├───────┼──────────────────────────┼───────────────────────────┤│ 0.05 │ 山包窄而尖锐分界线锯齿 │ 62.1% ← ││ │ │ 亲眼看着过拟合发生 │└───────┴──────────────────────────┴───────────────────────────┘γ 越大 → 影响范围越小 → 分界线弯弯绕绕 → 死记硬背。γ 越小 →影响范围越大 → 分界线光滑 → 泛化好。这就是为什么选 rbf 不选 linear——手写数字太复杂直线根本切不干净。5.9 model.fit() 内部总结输入40000 张归一化 PCA 压缩的图和标签过程① rbf 核计算两两样本之间的相似度② SMO 算法迭代解对偶问题算出每个 αᵢ③ αᵢ0 的点扔掉αᵢ0 的保留为支持向量④ w Σαᵢ·yᵢ·xᵢ输出训练好的 model存了支持向量和对应的 α5.10 10 个数字怎么办SVM 只能分两类。但 10 个数字怎么一次分出来我当时自己想到的方案是“第一条线把 0 和其他分开再分1以此类推。” 后来的 AI 助教告诉我——这个思路完全正确叫One-vs-Rest。只不过 sklearn 做了优化10 场淘汰赛同时开打不按顺序选信心值最高的输出。避免了前面判错了后面全废的级联错误。【6】测试集评分predictionsmodel.predict(X_test)correct(predictionsy_test).sum()totallen(y_test)print(f对了{correct}张错了{total-correct}张)print(f准确率:{correct/total*100:.2f}%)这段代码在干什么model.predict() 把 1 万张测试图喂进去模型只看到图、看不到答案自己猜每张是数字几。猜完后逐题对答案预测[7, 2, 1, 0, 8, …]真实[7, 2, 1, 0, 3, …]结果 ✅ ✅ ✅ ✅ ❌ …Python 里 True1, False0.sum() 就是把所有对加起来。数学原理预测一个新点 x 的方式f(x) sign( Σ αᵢ yᵢ K(xᵢ, x) b )└── 只对 αᵢ≠0 的支持向量求和 ──┘新点跟所有支持向量比相似度K函数支持向量加权投票“我最像谁谁就赢。” 10场淘汰赛同时打取信心值最高的。【7】混淆矩阵fromsklearn.metricsimportconfusion_matrix cmconfusion_matrix(y_test,predictions)print(混淆矩阵)print(cm)这段代码在干什么输出一张 10×10 的表行 真实数字列 预测数字。对角线上是判对的对角线外是判错的。一眼看出哪两个数字最容易搞混——我的模型里3总被误判成 84 总被误判成 9。想想也是写潦草了确实分不清。【8】可视化importmatplotlib.pyplotasplt# 图1手写数字样本fig,axesplt.subplots(2,5,figsize(10,4))fori,axinenumerate(axes.flat):ax.imshow(X_all[i].reshape(28,28),cmapgray)ax.set_title(f数字{y_all[i]})ax.axis(off)plt.show()# 图2混淆矩阵热力图plt.figure(figsize(8,6))plt.imshow(cm,cmapBlues)plt.colorbar(label数量)foriinrange(10):forjinrange(10):plt.text(j,i,str(cm[i,j]),hacenter,vacenter,fontsize12,colorwhiteifcm[i,j]cm.max()/2elseblack)plt.xlabel(预测);plt.ylabel(真实)plt.title(f混淆矩阵 — 准确率{correct/total*100:.2f}%)plt.show()这段代码在干什么imshow(…, cmap‘gray’) 把 784 个数字折回 28×28 显示cmap‘Blues’ 把混淆矩阵涂成蓝色——数字越大越深对角线一目了然-## 【附录】模型方案横向对比向对比模型准确率训练时间 (s)预测时间 (s, 1万张)内存占用优点缺点Linear SVM92.9%~5~0.1中支持向量约5000个训练快线性可解释只能线性分割准确率不高RBF SVM97.53%~30~0.5中支持向量约3000个非线性准确率最高泛化好超参数敏感需调参KNN (k3)~95.8%~0惰性~15高存储40000个训练样本简单无训练开销准确率不错预测极慢内存大高维灾难决策树 (max_depth12)~87.2%~2~0.01低树结构训练快可解释内存小容易过拟合准确率低对手写数字不友好注以上数据均基于 40000 个 PCA 降维至 100 维的训练样本 10000 个测试样本实测。KNN 和决策树的准确率来自同环境对比实验。从表格中可以清晰看出RBF SVM 在准确率上一骑绝尘达到 97.53%远超线性 SVM 和决策树也比 KNN 高了近两个百分点。更重要的是RBF SVM 的预测速度为每万张 0.5 秒相比 KNN 的 15 秒快了 30 倍这意味着如果未来需要部署到线上进行实时识别RBF SVM 的性能完全够用。为什么不用 KNN虽然 KNN 的准确率也还不错~95.8%但它的预测速度是个硬伤——每预测一张图就需要遍历 40000 张训练图计算距离这在生产环境中是不可接受的。决策树则准确率太低直接淘汰。线性 SVM 训练快、可解释性好但 MNIST 手写数字的分布是非线性的数字形状千变万化一条直线根本划不干净所以准确率停在了 92.9%。RBF 核通过 “升维” 把低维的非线性问题变成高维的线性问题精准捕捉到了数字间的复杂边界。尽管训练时间稍长约 30 秒但对于一个一次性训练、长期使用的模型来说这点等待完全值得。最终通过精细调参C10, γ0.001我们在准确率和速度上都拿到了最佳平衡这就是我选择 RBF SVM 的核心原因。 【9】完整调参历程——翻车现场全记录全记录这才是我写这篇文章最想分享的东西——每一次改参数准了就分析为什么准翻了就分析为什么翻。┌───────────────┬────────┬───────────────────────────────────┐│ 方案 │ 准确率 │ 学到了什么 │├───────────────┼────────┼───────────────────────────────────┤│ linear kernel │ 92.9% │ 直线不够用 │├───────────────┼────────┼───────────────────────────────────┤│ 归一化 │ 91.9% │ ⚠️ ││ │ │ 不是所有数据都需要归一化——翻车 │├───────────────┼────────┼───────────────────────────────────┤│ → rbf kernel │ 93.6% │ 曲线更适合手写数字 │├───────────────┼────────┼───────────────────────────────────┤│ │ 94.3% │ 自动调参比手调靠谱 ││ GridSearchCV │ │ │├───────────────┼────────┼───────────────────────────────────┤│ 更多数据 │ 95.9% │ 数据量比调参更重要 │├───────────────┼────────┼───────────────────────────────────┤│ │ 86.5% │ 像素多了但 linear 还是不行rbf ││ → MNIST 28×28 │ ~ │ 提了近 7 个点 ││ │ 91.3% │ │├───────────────┼────────┼───────────────────────────────────┤│ PCA 50 │ 96.5% │ PCA 降维又快又准 ││ 20k γ0.005 │ │ │├───────────────┼────────┼───────────────────────────────────┤│ PCA 100 │ 97.53% │ 最终最优 ││ 40k γ0.001 │ ✅ │ │├───────────────┼────────┼───────────────────────────────────┤│ 只改 γ0.05 │ 62.1% │ 过拟合教科书现场 ││ │ ❌ │ │└───────────────┴────────┴───────────────────────────────────┘【10】我踩过的坑希望你别再踩MNIST 下载连不上fetch_openml 超时 → keras 需要 tensorflow →Python 3.14 太新装不上 → 最后手动从 AWS 下载 .gz自己解析二进制GridSearchCV 太慢5 万张 × 27 轮训练 电脑算不动每次 CtrlC中止。后来发现不调参直接用经验参数单次 fit 就行Python 3.14 兼容scipy 报错必须升级SVM 只用单核 CPU 天生跑不了 GPU所以如果你的CPU好的话跑得会快很一定要注意不要过拟合归一化不是万能的这组数据加了反而降分γ 踩雷从 0.001 调到 0.05眼睁睁看着 97.5% 崩到 62.1%【11】SVM 数学通解一页纸速查硬间隔min ½||w||² s.t. yᵢ(w·xᵢb) ≥ 1→ 让空隙最宽所有人站护栏外软间隔min ½||w||² C Σξᵢ s.t. yᵢ(w·xᵢb) ≥ 1−ξᵢ→ 空隙宽 犯规罚款允许少数人站错拉格朗日L ½||w||² − Σαᵢ[yᵢ(w·xᵢb)−1]→ 把约束塞进目标函数求导0w Σαᵢyᵢxᵢ→ w 只由 α≠0 的支持向量决定对偶问题max Σαᵢ − ½ΣΣαᵢαⱼyᵢyⱼK(xᵢ,xⱼ)→ 消掉 w只剩 α。核函数 K 在这里塞进去rbf 核K(x₁,x₂)exp(−γ||x₁−x₂||²)→ 不升维直接算相似度预测f(x)sign(ΣαᵢyᵢK(xᵢ,x)b)→ 新点跟支持向量比相似度加权投票【12】完整代码整代码importstruct,gzip,os,numpyasnpfromsklearn.svmimportSVCfromsklearn.metricsimportconfusion_matrixfromsklearn.preprocessingimportStandardScalerfromsklearn.decompositionimportPCAimportmatplotlib.pyplotasplt# 【1】加载 MNIST 数据defload_mnist_images(path):withgzip.open(path,rb)asf:magic,num,rows,colsstruct.unpack(IIII,f.read(16))returnnp.frombuffer(f.read(),dtypenp.uint8).reshape(num,rows*cols)defload_mnist_labels(path):withgzip.open(path,rb)asf:magic,numstruct.unpack(II,f.read(8))returnnp.frombuffer(f.read(),dtypenp.uint8)data_dirmnist_dataX_allload_mnist_images(f{data_dir}/train-images-idx3-ubyte.gz)y_allload_mnist_labels(f{data_dir}/train-labels-idx1-ubyte.gz)X_testload_mnist_images(f{data_dir}/t10k-images-idx3-ubyte.gz)y_testload_mnist_labels(f{data_dir}/t10k-labels-idx1-ubyte.gz)# 【2】拆分训练集和测试集X_trainX_all[:40000]y_trainy_all[:40000]# 【3】归一化scalerStandardScaler()X_trainscaler.fit_transform(X_train)X_testscaler.transform(X_test)# 【4】PCA 降维pcaPCA(n_components100)X_trainpca.fit_transform(X_train)X_testpca.transform(X_test)# 【5】训练 SVMmodelSVC(kernelrbf,C10,gamma0.001)model.fit(X_train,y_train)# 【6】评分predictionsmodel.predict(X_test)correct(predictionsy_test).sum()print(f准确率:{correct/len(y_test)*100:.2f}%)# 【7】混淆矩阵cmconfusion_matrix(y_test,predictions)print(混淆矩阵:\n,cm)# 【8】可视化fig,axesplt.subplots(2,5,figsize(10,4))fori,axinenumerate(axes.flat):ax.imshow(X_all[i].reshape(28,28),cmapgray)ax.set_title(f数字{y_all[i]})ax.axis(off)plt.suptitle(MNIST 手写数字样本)plt.tight_layout()plt.show()plt.figure(figsize(8,6))plt.imshow(cm,cmapBlues)plt.colorbar(label数量)foriinrange(10):forjinrange(10):plt.text(j,i,str(cm[i,j]),hacenter,vacenter,fontsize12,colorwhiteifcm[i,j]cm.max()/2elseblack)plt.xlabel(预测)plt.ylabel(真实)plt.title(f混淆矩阵 — 准确率{correct/len(y_test)*100:.2f}%)plt.show()运行结果准确率: 97.53%