Yi-Coder-1.5B机器学习入门:CNN图像分类实战
1. 这不是你想象中的CNN教程
看到标题里的“Yi-Coder-1.5B”和“CNN图像分类”,你可能会下意识地皱眉——这到底是讲代码大模型,还是讲图像识别?两者怎么扯上关系的?
其实这里有个常见的误解。Yi-Coder-1.5B是一款专注于代码理解和生成的开源语言模型,它不处理图片,也不做图像分类。它的强项是读懂你的编程意图、补全函数、解释报错信息、把自然语言需求转成可运行代码——比如,当你想实现一个CNN图像分类器时,它能帮你写清数据加载、模型搭建、训练循环的每一步。
而CNN(卷积神经网络)是图像分类任务的经典架构,需要你用PyTorch或TensorFlow动手搭建和训练。Yi-Coder-1.5B不会替你跑GPU,但它可以成为你写代码时最懂你的“编程搭档”。
这篇教程不绕弯子,不堆概念。我们会用最直白的方式,带你从零完成一个能实际运行的CNN图像分类项目:用PyTorch写模型、用CIFAR-10数据集训练、用Yi-Coder-1.5B辅助解决编码卡点。整个过程不需要你提前掌握深度学习公式,只要你会写基础Python,就能跟下来。
你不需要记住“卷积核”“池化层”这些词,只需要知道它们在代码里对应哪几行,以及为什么这么写。就像学骑自行车,重点不是背力学原理,而是先坐上去、蹬起来、保持平衡。
2. 准备工作:环境、数据与工具
2.1 本地开发环境搭建
我们用最轻量、最通用的方式:Python + PyTorch + Jupyter Notebook。不需要Docker,不依赖云平台,笔记本电脑就能跑通。
首先确认你有Python 3.9或更高版本:
python --version # 输出类似:Python 3.10.12如果没有,去python.org下载安装即可。接着安装核心库:
pip install torch torchvision matplotlib numpy tqdmtorch:PyTorch深度学习框架torchvision:提供常用数据集(如CIFAR-10)和预训练模型matplotlib:画训练曲线、查看图片numpy:数值计算基础tqdm:给训练过程加进度条,看着不焦虑
小提示:如果你用的是Mac M系列芯片,推荐安装
torch的Metal版本,能显著提速。执行pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/macos-arm64即可。
2.2 数据集:CIFAR-10——图像分类的“Hello World”
我们不用自己收集图片,直接用学术界公认的入门数据集CIFAR-10。它包含6万张32×32像素的彩色图片,分为10类:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。
每类6000张,其中5万张用于训练,1万张用于测试。尺寸小、类别少、加载快,特别适合新手第一次跑通全流程。
在代码里,它一行就能自动下载并加载:
import torchvision.datasets as datasets import torchvision.transforms as transforms # 定义图片预处理:转为Tensor + 归一化 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) ]) # 加载训练集和测试集 train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform) print(f"训练集大小:{len(train_dataset)}") print(f"测试集大小:{len(test_dataset)}") # 输出:训练集大小:50000,测试集大小:10000这段代码会自动检查./data目录下有没有数据。没有就联网下载(首次运行需几分钟),有就直接读取。transform的作用是把PIL图片转成PyTorch能处理的张量,并做标准化——这是训练稳定的关键一步,但你不用深究数字怎么来的,照着抄就行。
2.3 让Yi-Coder-1.5B成为你的实时编程助手
现在来请出本教程的“隐形主角”:Yi-Coder-1.5B。它不是用来训练模型的,而是当你写代码遇到问题时,给你精准、可执行的建议。
最简单的方式是用Ollama本地运行(无需GPU):
# 1. 下载并安装Ollama(官网 ollama.com/download) # 2. 在终端运行以下命令下载模型 ollama run yi-coder:1.5b你会看到一个交互式界面,输入类似这样的问题,它会立刻返回完整代码:
帮我写一个PyTorch的CNN模型,输入是32x32的RGB图片,输出是10个类别的概率,要求包含两个卷积块(每个含卷积+ReLU+池化),然后接全连接层。它给出的代码可以直接复制进你的Notebook,稍作调整就能运行。比起在Stack Overflow大海捞针,或者反复查文档,这种方式快得多、准得多。
实用技巧:在Jupyter里,你可以把Yi-Coder的回复粘贴到一个新cell,加上
# TODO: 理解这段代码注释,然后逐行运行、打印中间变量形状(比如print(x.shape)),亲眼看到数据流怎么穿过每一层。这才是真正学会的开始。
3. 动手搭建:从零实现CNN图像分类器
3.1 模型架构:三层结构,清晰易懂
我们不追求SOTA(当前最优),而是用一个足够简单、足够典型的CNN结构,让你一眼看懂数据怎么流动:
- 特征提取层:两个“卷积→激活→池化”组合,负责从像素中抓取边缘、纹理等局部特征
- 特征整合层:把二维特征图拉平成一维向量,准备交给分类器
- 分类层:一个全连接层,把特征向量映射到10个类别的得分(logits)
用PyTorch实现,不到30行:
import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() # 第一个卷积块:32x32 → 16x16 self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) # 输入3通道,输出32通道 self.relu1 = nn.ReLU() self.pool1 = nn.MaxPool2d(2) # 2倍下采样 # 第二个卷积块:16x16 → 8x8 self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # 输入32通道,输出64通道 self.relu2 = nn.ReLU() self.pool2 = nn.MaxPool2d(2) # 再次2倍下采样 # 分类层:把8x8x64的特征图展平,接全连接 self.flatten = nn.Flatten() self.fc = nn.Linear(64 * 8 * 8, num_classes) # 64*8*8 = 4096 def forward(self, x): # 第一个块 x = self.pool1(self.relu1(self.conv1(x))) # 第二个块 x = self.pool2(self.relu2(self.conv2(x))) # 展平 + 分类 x = self.flatten(x) x = self.fc(x) return x # 创建模型实例 model = SimpleCNN() print(model)运行后,你会看到模型的完整结构。注意几个关键数字:
Conv2d(3, 32, ...):从3通道(RGB)输入,生成32个特征图MaxPool2d(2):每次把宽高减半,所以32→16→8Linear(4096, 10):最后把8×8×64=4096维特征,压缩成10个类别的分数
这个结构不是凭空设计的。你可以问Yi-Coder:“为什么第一个卷积输出32通道,第二个是64?”它会告诉你:通道数通常逐层翻倍,因为越深层的特征越抽象,需要更多维度来表达。
3.2 数据加载:让图片乖乖排队
模型有了,数据也下载好了,下一步是把图片“喂”给模型。我们需要两个东西:
- DataLoader:把数据集打包成一个个“批次”(batch),比如每次送32张图进来,而不是一张张送(太慢)
- 数据增强(可选但推荐):在训练时对图片做轻微变换(如随机水平翻转、裁剪),让模型见多识广,不容易过拟合
from torch.utils.data import DataLoader # 训练时加一点增强,测试时不加(要测真实水平) train_transform = transforms.Compose([ transforms.RandomHorizontalFlip(), # 随机左右翻转 transforms.RandomCrop(32, padding=4), # 随机裁剪再补回32x32 transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) ]) test_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) ]) # 重新加载数据集(用新transform) train_dataset = datasets.CIFAR10('./data', train=True, download=True, transform=train_transform) test_dataset = datasets.CIFAR10('./data', train=False, download=True, transform=test_transform) # 创建DataLoader:batch_size=128,开4个进程并行读数据 train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4) test_loader = DataLoader(test_dataset, batch_size=100, shuffle=False, num_workers=4) # 检查一个batch的数据形状 dataiter = iter(train_loader) images, labels = next(dataiter) print(f"一个batch的图片形状:{images.shape}") # torch.Size([128, 3, 32, 32]) print(f"一个batch的标签形状:{labels.shape}") # torch.Size([128])这里batch_size=128是个经验值。太大显存可能爆,太小训练太慢。128在大多数笔记本上很友好。
3.3 训练循环:五步走,稳扎稳打
训练模型的核心逻辑就五句话,我们把它封装成一个清晰的函数:
def train_one_epoch(model, train_loader, optimizer, criterion, device): model.train() # 切换到训练模式(影响Dropout/BatchNorm等) running_loss = 0.0 correct = 0 total = 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) # 送到GPU(如有) # 1. 清空梯度 optimizer.zero_grad() # 2. 前向传播:算预测 outputs = model(images) # 3. 计算损失 loss = criterion(outputs, labels) # 4. 反向传播:算梯度 loss.backward() # 5. 更新参数 optimizer.step() # 统计指标 running_loss += loss.item() _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() acc = 100. * correct / total avg_loss = running_loss / len(train_loader) return avg_loss, acc # 同理,写一个测试函数(不更新参数,只评估) def evaluate(model, test_loader, device): model.eval() correct = 0 total = 0 with torch.no_grad(): # 关闭梯度计算,省显存 for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() return 100. * correct / total现在,把所有零件组装起来,开始训练:
import torch.optim as optim # 设置设备:优先用GPU device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SimpleCNN().to(device) # 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() # 分类任务的标准损失 optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam优化器,学习率0.001 # 训练主循环 num_epochs = 10 train_losses = [] train_accs = [] test_accs = [] for epoch in range(num_epochs): print(f"\nEpoch {epoch+1}/{num_epochs}") # 训练一轮 loss, acc = train_one_epoch(model, train_loader, optimizer, criterion, device) train_losses.append(loss) train_accs.append(acc) # 测试一轮 test_acc = evaluate(model, test_loader, device) test_accs.append(test_acc) print(f"Train Loss: {loss:.4f} | Train Acc: {acc:.2f}% | Test Acc: {test_acc:.2f}%") print("\n训练完成!")第一次运行时,你会看到准确率从50%左右开始爬升,10轮后稳定在75%-80%之间。这已经比随机猜(10%)好太多了。如果想更高,我们可以后续加技巧,但此刻的目标是先跑通,再优化。
4. 调参与优化:让模型更聪明的三个实用技巧
4.1 学习率调度:别让模型“迈大步”
学习率(lr)就像走路的步长。太大,模型在最优解附近来回震荡,甚至跳过去;太小,进步缓慢,训练像蜗牛。
我们用PyTorch内置的StepLR,每过5轮就把学习率砍半:
from torch.optim.lr_scheduler import StepLR # 替换原来的optimizer定义 optimizer = optim.Adam(model.parameters(), lr=0.001) scheduler = StepLR(optimizer, step_size=5, gamma=0.5) # 每5轮,lr *= 0.5 # 在训练循环里,每轮结束后调用 for epoch in range(num_epochs): # ... 训练代码 ... scheduler.step() # 这行加在每轮末尾 print(f"当前学习率:{scheduler.get_last_lr()[0]:.6f}")加了这个,你会发现后期准确率提升更稳,不容易卡住。这是最简单、最有效的调参技巧之一。
4.2 添加Dropout:给神经元“放假”
过拟合是新手常踩的坑:模型在训练集上99%准确,一到测试集就跌到60%。Dropout是经典解法——在训练时,随机“关闭”一部分神经元(设为0),强迫网络不依赖个别神经元,从而泛化更好。
在模型里加两行:
class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() # ... 原有代码 ... self.dropout = nn.Dropout(0.5) # 50%概率置零 def forward(self, x): # ... 原有代码 ... x = self.flatten(x) x = self.dropout(x) # 在展平后加dropout x = self.fc(x) return x加了Dropout,训练准确率会略降(因为部分神经元不工作),但测试准确率通常会上升2-3个百分点。这就是“以退为进”的智慧。
4.3 使用预训练特征:站在巨人的肩膀上
上面的CNN是从头学起(scratch training)。但ImageNet上已有大量高质量预训练模型(如ResNet、VGG),它们学到了通用的视觉特征。我们可以“迁移学习”:冻结前面的层,只训练最后的分类头。
用PyTorch实现只需几行:
from torchvision import models # 加载预训练的ResNet18(在ImageNet上训练过) base_model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 冻结所有层(不更新参数) for param in base_model.parameters(): param.requires_grad = False # 替换最后的全连接层,适配CIFAR-10的10类 base_model.fc = nn.Linear(base_model.fc.in_features, 10) # 现在base_model就是我们的新模型 model = base_model.to(device)这样改完,训练10轮后测试准确率往往能冲到85%以上。因为ResNet已经学会了“什么是轮子”“什么是翅膀”,你只需要教它“轮子多的是汽车,翅膀大的是飞机”。
小提醒:迁移学习需要更多显存(ResNet比SimpleCNN大得多),如果显存紧张,优先用前面两个技巧。
5. 结果分析与可视化:看见模型的思考过程
训练完,不能只看一个数字。我们要深入进去,看看模型到底学到了什么。
5.1 绘制训练曲线:一目了然看趋势
用matplotlib画出损失和准确率的变化:
import matplotlib.pyplot as plt plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(train_losses, label='Train Loss') plt.title('Training Loss Over Epochs') plt.xlabel('Epoch') plt.ylabel('Loss') plt.legend() plt.subplot(1, 2, 2) plt.plot(train_accs, label='Train Accuracy') plt.plot(test_accs, label='Test Accuracy') plt.title('Accuracy Over Epochs') plt.xlabel('Epoch') plt.ylabel('Accuracy (%)') plt.legend() plt.tight_layout() plt.show()好的曲线应该是:损失持续下降,训练和测试准确率都稳步上升,且两条线距离不大(说明没过拟合)。如果测试线突然掉下去,就要检查数据增强或Dropout是否够用。
5.2 查看错误案例:理解模型的“盲区”
模型在哪类图片上总犯错?我们找出测试集中被分错的前10张,人工看看原因:
def show_mistakes(model, test_loader, classes, device, n=10): model.eval() mistakes = [] with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = outputs.max(1) wrong_mask = predicted != labels for i in range(len(images)): if len(mistakes) >= n: break if wrong_mask[i]: img = images[i].cpu() true_label = classes[labels[i]] pred_label = classes[predicted[i]] mistakes.append((img, true_label, pred_label)) # 画图 fig, axes = plt.subplots(2, 5, figsize=(12, 6)) axes = axes.flatten() for i, (img, true, pred) in enumerate(mistakes): # 反归一化,让图片能正常显示 img = img * torch.tensor([0.2023, 0.1994, 0.2010]).view(3,1,1) + torch.tensor([0.4914, 0.4822, 0.4465]).view(3,1,1) img = torch.clamp(img, 0, 1) # 保证值在0-1间 axes[i].imshow(img.permute(1,2,0)) axes[i].set_title(f'True:{true}\nPred:{pred}', fontsize=9) axes[i].axis('off') plt.suptitle('Top 10 Misclassified Examples') plt.show() # 类别名 classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') show_mistakes(model, test_loader, classes, device)你可能会发现:模型把“猫”认成“狗”,把“青蛙”认成“猫”——因为它们都有大眼睛、圆脸。这很正常,说明模型在学语义相似性,而不是死记硬背。这也提示你:如果业务场景里这两类必须严格区分,就需要更多针对性数据或更强的模型。
5.3 特征可视化:看卷积层在“看”什么
最后,我们用一个经典技巧:可视化第一个卷积层的32个滤波器(filter)在“检测”什么模式。
def visualize_filters(model): # 获取第一层卷积权重 weights = model.conv1.weight.data.cpu() # shape: [32, 3, 3, 3] # 画32个3x3的滤波器 fig, axes = plt.subplots(4, 8, figsize=(12, 6)) axes = axes.flatten() for i in range(32): # 取第一个通道(R通道)的权重,归一化到0-1 filter_img = weights[i, 0] # [3,3] filter_img = (filter_img - filter_img.min()) / (filter_img.max() - filter_img.min()) axes[i].imshow(filter_img, cmap='gray') axes[i].axis('off') plt.suptitle('First Conv Layer Filters (R channel)') plt.show() visualize_filters(model)你会看到一些滤波器像细线(检测边缘),一些像斑点(检测纹理),还有一些像模糊的色块——这正是CNN从底层学起的过程。虽然我们没手动设计它们,但模型自己找到了最适合的“视觉探测器”。
6. 总结:你已经掌握了图像分类的核心脉络
回看整个过程,我们其实只做了三件关键的事:
第一,把问题拆解成可执行的步骤:数据怎么来、模型怎么搭、训练怎么写、结果怎么看。每一步都有明确的代码对应,没有玄学。
第二,用工具放大你的能力:Yi-Coder-1.5B不是替代你思考,而是帮你把“我想实现XX功能”快速转成“这段代码该怎么写”。它省下的是查文档、试语法、调bug的时间,让你专注在逻辑和设计上。
第三,在实践中建立直觉:看到训练曲线波动,你知道可能是学习率问题;看到测试准确率上不去,你会想到加Dropout或换数据增强;看到错误案例,你能判断是数据问题还是模型容量问题。这种直觉,只有亲手跑过、调过、错过的工程师才会有。
你现在完全可以用这套方法,换成自己的数据集(比如手机拍的几十张水果照片),微调一下,做出一个能识别苹果、香蕉、橙子的小应用。门槛没那么高,关键是迈出第一步。
如果后续想深入,可以尝试:用更大的模型(ResNet50)、加更多数据增强(AutoAugment)、试试不同的优化器(SGD with momentum)、或者把模型部署到手机上。但那些都是锦上添花,今天你已经拿到了打开AI大门的钥匙。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。