外观
AI 时代机器学习入门:手写数字分类实战
AI时代机器学习入门超简单!手写数字分类实战:调参、GPU、换网络全拿捏
40万手写数据实战99.67%准确率,新手也能一键上手
当下AI浪潮席卷而来,很多朋友想入门机器学习,却总被“复杂的公式”“难懂的模型原理”“繁琐的硬件配置”劝退,觉得这是“大神专属”的技能。但实际体验下来,机器学习入门的核心从来不是造轮子,而是站在巨人的肩膀上做尝试——主流框架早已把复杂的底层逻辑封装完毕,调用GPU、调整参数、更换神经网络,本质上只是改几个数字、换一行代码的事。
我近期用PyTorch做了一次手写数字分类实战,基于40万张手写数字数据集,用ResNet18模型训练,全程仅做了基础的参数调整和GPU自动调用,最终跑出了99.67%的测试准确率,整个过程没有复杂的操作,甚至新手也能跟着复刻。这也印证了一个结论:AI时代,机器学习入门的门槛早已大幅降低,勇敢动手尝试,你就已经赢在了起点。
这篇文章就以这次手写数字分类实战为核心,和大家聊聊机器学习入门那些“超简单”的核心操作,打消你的顾虑,让你敢上手、会上手。
先破局:机器学习入门,真的不用怕“复杂”
很多人对机器学习的恐惧,来源于“觉得要先吃透所有原理才能动手”。但实际的工业界和入门实战中,我们更多是使用现成的框架、现成的模型,核心工作是调参、适配数据、选择合适的网络结构,而非从0到1写一个神经网络。
就像这次手写数字分类,我全程用的是PyTorch框架,调用了经典的ResNet18模型,没有手写一行卷积层代码;GPU是自动识别调用,不用手动配置显卡驱动和环境;调参就是改几个超参数的数字,更换模型就是换一行调用代码——整个过程的核心是“尝试”,而非“死磕原理”。
入门的关键,是先动手做一个能跑通的项目,在实战中理解逻辑,而非纸上谈兵。当你能轻松跑通一个手写数字分类项目,再去深究模型原理、参数背后的逻辑,会事半功倍。
实战落地:40万手写数据,99.67%准确率的简单实现
先和大家简单说下这次实战的基础情况,让大家有个直观认知:
数据集:402953张手写数字图片,按9:1拆分为训练集和测试集,标签映射为0-9的数字分类;
硬件:普通的RTX3080显卡,无需高端算力;
模型:直接调用PyTorch内置的ResNet18模型,仅修改输出层并添加Dropout防过拟合;
训练结果:9轮训练触发早停,最高测试准确率99.67%,模型泛化能力拉满,无过拟合、无欠拟合。
整个实战的代码核心逻辑不到200行,其中最关键的GPU调用、调参、更换网络,加起来不到10行代码——这就是当下机器学习入门的真实状态:框架做了所有重活,我们只需要做简单的“选择和调整”。
接下来,重点和大家聊三个最核心的操作,也是机器学习入门的“基本功”,看完你会发现,原来这么简单!
核心操作1:调用GPU?一行代码,全自动识别
很多人觉得“用GPU训练模型”需要复杂的硬件配置、环境调试,其实在PyTorch中,调用GPU只需要一行代码,框架会自动识别你的电脑是否有可用的GPU,没有则自动切换到CPU,完全不用手动干预。
核心代码就这一行:
device = "cuda" if torch.cuda.is_available() else "cpu"后续只需要把模型和数据放到这个device上即可,比如:
model = model.to(device) # 模型放到GPU/CPU
data, target = data.to(device), target.to(device) # 数据放到GPU/CPU这次实战中,我的RTX3080显卡被自动识别,40万数据的单轮训练仅需4分钟左右,效率拉满;就算你的电脑没有独立显卡,把代码直接运行在CPU上,也能正常训练,只是速度稍慢,完全不影响项目跑通。
总结:GPU调用无需手动配置,一行代码全自动,新手无脑用就行。
核心操作2:调参?本质就是改几个数字,试错就好
调参是机器学习实战中最核心的工作之一,但入门阶段的调参,完全不需要复杂的调优算法,就是修改几个超参数的数字,根据训练结果试错即可,甚至可以理解为“改数字,看效果”。
这次手写数字分类实战中,我用到的核心超参数就这几个,全部是直接定义的数字,想调整直接改就行:
epochs = 10 # 训练轮数
batch_size = 256 # 批次大小
lr = 5e-4 # 学习率
weight_decay = 5e-5 # 权重衰减
dropout_prob = 0.5 # Dropout概率,防过拟合比如学习率lr,如果训练时损失下降太慢,就把数字调大一点(比如改成1e-3);如果损失波动太大、不收敛,就调小一点(比如改成3e-4);Dropout概率如果模型过拟合,就调大一点(比如0.6),欠拟合就调小一点(比如0.3)。
这次实战中,我把学习率设为5e-4,Dropout设为0.5,batch_size设为256,仅通过这几个简单的数字调整,就实现了模型的快速收敛和高准确率。入门阶段的调参,没有固定答案,核心是大胆试错,看训练日志的损失和准确率变化,慢慢找到合适的数值。
甚至可以说,调参是机器学习入门最有趣的部分,你会发现改一个小小的数字,模型的效果可能会有天翻地覆的变化,这种“掌控感”,会让你快速找到入门的乐趣。
核心操作3:换神经网络?一行代码,轻松切换
很多朋友觉得“设计神经网络”是一件很难的事,其实入门阶段,我们完全不需要自己设计,PyTorch等框架内置了大量经典的预训练模型(ResNet、VGG、AlexNet等),想更换模型,只需要改一行调用代码就行。
这次实战中,我用的是ResNet18模型,核心调用代码:
model = models.resnet18(weights=None) # 调用ResNet18如果想换成更深的ResNet34,只需要把代码改成:
model = models.resnet34(weights=None) # 调用ResNet34如果想换成更简单的VGG16,就是:
model = models.vgg16(weights=None) # 调用VGG16更换模型后,仅需要根据模型的输出特征数,微调一下最后的全连接层即可,核心逻辑完全不变。这次实战中,我只用了基础的ResNet18,就跑出了99.67%的准确率,足以见得现成的经典模型,完全能满足入门阶段的所有实战需求。
总结:入门阶段不用纠结“如何设计神经网络”,先学会调用现成的经典模型,能根据任务需求切换网络,就是合格的入门者了。
实战复盘:简单的操作,也能做出顶尖的效果
看完核心操作,再回头看这次实战的训练日志,更能感受到“简单操作”的力量:
40万张图片的超大数据集,框架自动加载、拆分,无需手动处理数据格式;
GPU自动加速,训练效率拉满,单轮训练仅4分钟;
简单调参后,模型训练损失持续下降,测试准确率稳定在99%+,Dropout完美抑制过拟合;
早停机制自动终止无效训练,最终跑出99.67%的超高准确率,模型泛化能力极强。
整个训练过程没有任何复杂的操作,所有核心步骤都是“调用框架、改数字、换代码”,但最终的效果却堪比专业的实战项目——这就是AI时代机器学习的魅力:框架把复杂的工作做了,我们只需要做简单的选择和尝试,就能做出不错的成果。
写给所有想入门的人:AI时代,勇敢尝试就是最大的收获
很多朋友迟迟不敢入门机器学习,无非是怕“学不会”“做不出来效果”“搞不懂硬件和模型”。但通过这次手写数字分类的实战,我想和大家说:AI时代,机器学习的入门门槛已经低到超乎你的想象。
不用懂复杂的数学公式,先会调用框架就行;
不用会设计神经网络,先会用现成的模型就行;
不用会配置GPU,框架自动识别就行;
不用会复杂的调参算法,先会改数字试错就行。
入门的关键,从来不是“一次性吃透所有知识”,而是勇敢地动手写第一行代码,跑通第一个项目。当你能成功跑通一个手写数字分类项目,看到自己的模型跑出90%、95%甚至99%的准确率时,那种成就感会驱动你去深究背后的原理,去学习更高级的调参方法、模型设计思路。
AI时代,机器学习不再是少数人的“专属技能”,而是每个人都可以尝试的“通用工具”。就像这次手写数字分类,40万数据的实战,全程都是简单的操作,却能做出顶尖的效果——这不仅是框架的力量,更是“勇敢尝试”的力量。
最后:附核心精简代码,动手跑起来!
为了让大家能直接上手,这里附上本次手写数字分类的核心精简代码,复制到本地,修改数据集路径,就能直接运行,新手也能轻松跑通:
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms, models
from torch.utils.data import Dataset, DataLoader
from PIL import Image
import re
from pathlib import Path
import os
from sklearn.model_selection import train_test_split
# 进度条库
from tqdm import tqdm
# ======================
# 🔥 超参数配置(RTX3080 + Windows 专属)
# ======================
device = "cuda" if torch.cuda.is_available() else "cpu"
epochs = 10
min_delta = 0.01
batch_size = 256
lr = 5e-4
weight_decay = 5e-5
patience = 5
dropout_prob = 0.5 # ✨ 新增Dropout概率超参数(可根据过拟合程度调整:0.3~0.7)
root_dir = r"E:/project/machine_learning/by_field" # by_field数据集根目录
# ======================
# ✅ 数据预处理(黑字白底+轻微倾斜,适配手写数字)
# ======================
train_transform = transforms.Compose([
transforms.RandomRotation(degrees=5, fill=(255, 255, 255), expand=False), # 轻微倾斜,白色填充
transforms.RandomCrop(128, padding=2), # 减小padding,避免裁掉数字
transforms.RandomHorizontalFlip(p=0), # 手写数字禁用翻转
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# ======================
# ✅ 自定义数据集(核心适配by_field:仅读digit文件夹+按子文件夹编号映射标签)
# 标签规则:digit子文件夹30→0、31→1、32→2、…、39→9
# ======================
class CustomMNISTDataset(Dataset):
def __init__(self, root_dir, transform=None):
self.root_dir = root_dir
self.transform = transform
self.img_paths = []
self.labels = []
# 递归遍历:所有层级下的digit文件夹 → 仅读取digit的直接子文件夹(30/31/…/39)下的PNG
for digit_dir in Path(root_dir).rglob("digit"):
if digit_dir.is_dir():
# 遍历digit下的子文件夹(30、31...39)
for label_dir in digit_dir.iterdir():
if label_dir.is_dir():
# 获取子文件夹编号(30/31/…/39),转换为数字标签
try:
dir_num = int(label_dir.name)
# 仅处理30-39的文件夹(对应数字0-9),过滤其他无关文件夹
if 30 <= dir_num <= 39:
label = dir_num - 30 # 核心映射:30→0,31→1...39→9
# 读取该文件夹下所有PNG图片(大小写兼容)
for img_path in label_dir.rglob("*.[pP][nN][gG]"):
self.img_paths.append(str(img_path))
self.labels.append(label)
except ValueError:
# 跳过非数字命名的文件夹,避免报错
continue
# 空数据校验
if len(self.img_paths) == 0:
raise FileNotFoundError(
f"未在 {root_dir} 下找到digit文件夹,或digit下无30-39的子文件夹/PNG图片!\n"
f"请检查:1.数据集是by_field 2.存在digit文件夹 3.子文件夹为30-39"
)
print(f"📌 成功匹配:digit下30-39文件夹 → 共加载 {len(self.img_paths)} 张手写数字图片")
def __len__(self):
return len(self.img_paths)
def __getitem__(self, idx):
img_path = self.img_paths[idx]
# 读取为RGB(兼容RGBA/单通道PNG,保持和预处理一致)
image = Image.open(img_path).convert('RGB')
label = self.labels[idx]
if self.transform:
image = self.transform(image)
return image, label
# ======================
# ✅ 子集数据集(用于9:1训练/测试拆分)
# ======================
class SubsetMNISTDataset(Dataset):
def __init__(self, img_paths, labels, transform=None):
self.img_paths = img_paths
self.labels = labels
self.transform = transform
def __len__(self):
return len(self.img_paths)
def __getitem__(self, idx):
img_path = self.img_paths[idx]
image = Image.open(img_path).convert('RGB')
label = self.labels[idx]
if self.transform:
image = self.transform(image)
return image, label
# ======================
# ✅ ResNet18 模型(添加Dropout抑制过拟合)
# ======================
model = models.resnet18(weights=None)
# ✨ 核心修改:替换fc层为「Dropout + 全连接」的组合层
model.fc = nn.Sequential(
nn.Dropout(p=dropout_prob), # 添加Dropout层,训练时随机丢弃50%的神经元
nn.Linear(model.fc.in_features, 10) # 输出10类(0-9)
)
model = model.to(device)
# ======================
# ✅ 优化器与损失函数(调低学习率,提升收敛性)
# ======================
criterion = nn.CrossEntropyLoss()
# 改用AdamW,正则化效果更好,适合手写数字分类
optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'max', patience=3, factor=0.5)
# ======================
# ✅ 训练/测试函数(带进度条,实时监控)
# ======================
best_acc = 0.0
counter = 0
def train_one_epoch(loader):
model.train() # 训练模式下,Dropout自动启用(随机丢弃神经元)
train_loss = 0.0
pbar = tqdm(loader, desc="训练中", colour="blue")
for data, target in pbar:
data, target = data.to(device, non_blocking=True), target.to(device, non_blocking=True)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
train_loss += loss.item()
# 实时显示当前批次损失
pbar.set_postfix({"当前损失": f"{loss.item():.4f}"})
avg_train_loss = train_loss / len(loader)
print(f"\n✅ 本轮训练完成 | 平均训练损失: {avg_train_loss:.4f}")
def test_model(loader):
global best_acc, counter
model.eval() # 测试模式下,Dropout自动禁用(所有神经元参与计算)
test_loss = 0.0
correct = 0
pbar = tqdm(loader, desc="测试中", colour="green")
with torch.no_grad():
for data, target in pbar:
data, target = data.to(device, non_blocking=True), target.to(device, non_blocking=True)
output = model(data)
test_loss += criterion(output, target).item()
pred = output.argmax(dim=1, keepdim=True)
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(loader)
acc = 100.0 * correct / len(loader.dataset)
print(f"\n🎯 本轮测试完成 | 测试准确率: {acc:.2f}% | 平均测试损失: {test_loss:.4f}")
# 学习率调度+早停+保存最佳模型
scheduler.step(acc)
if acc > best_acc + min_delta:
best_acc = acc
counter = 0
torch.save(model.state_dict(), "best_resnet18_byfield.pth")
print(f"✅ 保存最佳模型 | 目前最高测试精度: {best_acc:.2f}%")
else:
counter += 1
print(f"⚠️ 精度无提升 | {counter}/{patience} | 最高精度: {best_acc:.2f}%")
return acc
# ======================
# 🚀 主程序(Windows稳定版,9:1分层拆分)
# ======================
if __name__ == "__main__":
print("🔍 开始加载by_field数据集...")
# 加载全量数据(仅digit文件夹下30-39的图片)
full_dataset = CustomMNISTDataset(root_dir, transform=None)
print(f"✅ 全量数据加载完成:共 {len(full_dataset)} 张图片")
# 9:1分层随机拆分(stratify保证训练/测试集标签分布一致)
train_paths, test_paths, train_labels, test_labels = train_test_split(
full_dataset.img_paths,
full_dataset.labels,
test_size=0.1,
random_state=42,
stratify=full_dataset.labels
)
# 构建训练/测试集,分别应用对应变换
train_data = SubsetMNISTDataset(train_paths, train_labels, transform=train_transform)
test_data = SubsetMNISTDataset(test_paths, test_labels, transform=test_transform)
# Windows安全多进程数据加载器(3080适配,不报错+提速)
train_loader = DataLoader(
train_data, batch_size=batch_size, shuffle=True,
num_workers=1, pin_memory=True, persistent_workers=True
)
test_loader = DataLoader(
test_data, batch_size=batch_size, shuffle=False,
num_workers=1, pin_memory=True, persistent_workers=True
)
# 打印训练信息
print(f"\n⚙️ 训练配置 | 设备: {device} | 批次大小: {batch_size} | 学习率: {lr} | Dropout概率: {dropout_prob}")
print(f"📊 数据拆分 | 训练集: {len(train_data)} 张 | 测试集: {len(test_data)} 张 (9:1)")
print(f"🚀 开始训练 | 最大轮数: {epochs} | 早停耐心值: {patience}\n")
# 训练主循环
for epoch in range(1, epochs + 1):
print(f"======== Epoch {epoch}/{epochs} ========")
train_one_epoch(train_loader)
test_model(test_loader)
# 触发早停,终止训练
if counter >= patience:
print("\n📌 触发早停条件,训练提前结束!")
break
# 训练完成总结
print(f"\n🏆 训练全部完成 | 最终最高测试精度: {best_acc:.2f}%")
print(f"💾 最佳模型已保存为: best_resnet18_byfield.pth")AI时代,别让“害怕”挡住你的脚步。机器学习入门真的很简单,调参是改数字,GPU是自动调,换网络是改一行代码——勇敢地动手写第一行代码,跑通第一个项目,你会发现,原来你也能轻松玩转机器学习。
现在,就打开编辑器,敲下第一行代码吧!
版权所有
版权归属:wgz1995