基于Torch7的CIFAR-10图像分类实战:从数据预处理到Kaggle提交

📅 2026/7/14 19:02:38 👁️ 阅读次数 📝 编程学习
基于Torch7的CIFAR-10图像分类实战:从数据预处理到Kaggle提交

1. 项目概述:从零到一,用Torch7征服Kaggle CIFAR-10

如果你对深度学习感兴趣,想找一个既能练手、又有明确目标、还能在社区里看到自己排名的项目,那Kaggle的CIFAR-10图像分类竞赛绝对是个黄金起点。而Torch7,这个在PyTorch诞生前就以其灵活性和高效性在学术界备受青睐的框架,用它来实现这个项目,更像是一次“复古”但极其扎实的深度学习基本功训练。这个项目教程,就是带你一步步用Torch7搭建一个能跑在CIFAR-10数据集上的卷积神经网络(CNN),从数据下载、预处理、模型构建、训练调优,一直到生成Kaggle要求的提交文件。整个过程不仅会让你熟悉Torch7的语法和生态,更能让你深刻理解图像分类任务的核心流程与常见陷阱。无论你是刚学完理论想找地方实践的学生,还是想拓宽技术栈的从业者,这个项目都能给你带来实实在在的收获。

2. 项目环境搭建与数据准备

2.1 Torch7及其生态的安装与配置

Torch7的核心是LuaJIT和一套用C/CUDA写的底层张量库,因此安装它需要一些基础的系统依赖。在Ubuntu或macOS上,最推荐的方式是使用其官方安装脚本。

首先,确保你的系统有基本的编译工具和依赖。打开终端,执行以下命令(以Ubuntu为例):

# 更新包列表并安装基础依赖 sudo apt-get update sudo apt-get install -y git cmake build-essential libreadline-dev # 安装LuaJIT和LuaRocks(Torch的包管理器) git clone https://github.com/torch/distro.git ~/torch --recursive cd ~/torch bash install-deps # 这一步会安装LuaJIT、LuaRocks等 ./install.sh # 执行安装脚本

安装过程中会询问是否将Torch的路径添加到bashrc或zshrc中,一定要选择“是”。安装完成后,新开一个终端窗口,输入th命令,如果进入一个交互式环境(显示torch>提示符),说明安装成功。

接下来,我们需要安装这个项目必备的一些Lua包。Torch7的包管理通过luarocks进行。在终端中(非th交互环境),执行:

# 安装图像处理、优化、序列化等核心包 luarocks install image # 用于图像加载和变换 luarocks install optim # 优化算法包(SGD, Adam等) luarocks install nn # 神经网络模块(可能已随基础安装包含,但确保最新) luarocks install cunn # 如果使用NVIDIA GPU,安装CUDA后端 luarocks install cudnn # 安装cuDNN包装(需提前安装CUDA和cuDNN库) luarocks install csvigo # 用于读写CSV格式的提交文件

注意:安装cunncudnn的前提是你的系统已经正确安装了对应版本的NVIDIA驱动、CUDA Toolkit和cuDNN。如果只用CPU,可以跳过这两步,但训练速度会慢很多。

2.2 CIFAR-10数据集下载与理解

CIFAR-10数据集由6万张32x32像素的彩色图片组成,分为10个类别(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车),每个类别6000张。其中5万张是训练集,1万张是测试集。在Kaggle竞赛中,这1万张测试集是没有公开标签的,我们需要预测其类别并提交。

数据获取有两种主要途径:

  1. 从Kaggle官网下载:你需要先注册一个Kaggle账号,然后加入CIFAR-10竞赛(Competition)。在“Data”标签页下,可以下载到train.7ztest.7z两个压缩包。这种方式获得的是原始的图片文件(.png格式),更接近真实场景。
  2. 使用Torch7内置/社区数据加载工具:Torch7的torchvision包(非官方但常用)或一些开源项目提供了直接下载并转换为Torch张量格式的CIFAR-10数据。这对于快速实验非常方便。

对于本教程,为了更贴近实际Kaggle流程,我们假设你从Kaggle下载了原始数据。解压后,你通常会得到一个包含train/test/文件夹的目录,train/文件夹下还有10个以类别命名的子文件夹,分别存放对应类别的训练图片。

我们需要将这些图片数据转换为Torch7能够高效处理的格式。一个常见的做法是使用image包加载图片,并将其与标签一起序列化为一个单独的.t7文件(Torch的序列化格式)。这样做可以极大加快后续训练时的数据加载速度。以下是准备数据的核心代码思路:

require 'image' require 'torch' -- 假设你的数据目录结构如下 local trainDir = 'path/to/kaggle/cifar10/train' local testDir = 'path/to/kaggle/cifar10/test' local classNames = {'airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'} local function loadDataset(dataPath, isTrain) local data, labels = {}, {} local idx = 1 if isTrain then for classIdx, className in ipairs(classNames) do local classDir = paths.concat(dataPath, className) for file in paths.files(classDir) do if file:find('%.png$') then -- 只处理png文件 local imgPath = paths.concat(classDir, file) local img = image.load(imgPath, 3, 'float') -- 加载为3通道float张量 -- 图像已经是32x32,这里可以添加标准化等预处理 data[idx] = img labels[idx] = classIdx idx = idx + 1 end end end else -- 测试集,没有子文件夹 for file in paths.files(dataPath) do if file:find('%.png$') then local imgPath = paths.concat(dataPath, file) local img = image.load(imgPath, 3, 'float') data[idx] = img -- 测试集没有标签,labels可以存文件名用于后续生成提交 labels[idx] = file idx = idx + 1 end end end -- 将列表转换为一个大的4D张量 [N, 3, 32, 32] data = torch.cat(data, 1):view(idx-1, 3, 32, 32) return { data = data, labels = labels, size = function() return idx-1 end } end local trainData = loadDataset(trainDir, true) local testData = loadDataset(testDir, false) -- 保存为.t7文件,方便后续快速加载 torch.save('cifar10-train.t7', trainData) torch.save('cifar10-test.t7', testData) print('数据集预处理并保存完毕。')

实操心得:在第一次运行项目时,花时间写好这个数据预处理脚本是值得的。它将原始图片转换为.t7格式后,后续所有实验加载数据都是秒级,避免了每次训练都要重复进行耗时的文件I/O和图像解码操作。另外,记得在预处理时计算训练集的均值和标准差,用于后续的数据标准化,这能显著提升模型的训练稳定性和收敛速度。

3. 核心模型架构设计与原理

3.1 卷积神经网络(CNN)基础与Torch7实现

CIFAR-10图片尺寸小(32x32),但包含的颜色和纹理信息对于分类至关重要。卷积神经网络通过其局部连接、权值共享和空间下采样的特性,非常适合处理这类图像数据。在Torch7的nn包中,构建一个CNN就像搭积木一样直观。

一个最基本的CNN模块通常由“卷积层 -> 激活层 -> 池化层”这样的顺序堆叠而成。在Torch7中:

  • nn.SpatialConvolution(inChannels, outChannels, kW, kH, [dW], [dH], [padW], [padH]):定义卷积层。in/outChannels是输入/输出通道数,kW/kH是卷积核宽高,dW/dH是步长,padW/padH是填充。
  • nn.ReLU()/nn.Tanh()/nn.Sigmoid():非线性激活函数,最常用的是ReLU。
  • nn.SpatialMaxPooling(kW, kH, dW, dH):最大池化层,用于降低空间维度,增加感受野并引入一定平移不变性。

例如,一个简单的卷积块可以这样定义:

local model = nn.Sequential() model:add(nn.SpatialConvolution(3, 32, 3, 3, 1, 1, 1, 1)) -- 输入3通道(RGB),输出32通道,3x3卷积核,步长1,填充1(保持尺寸) model:add(nn.ReLU(true)) -- inplace=true,节省内存 model:add(nn.SpatialMaxPooling(2, 2, 2, 2)) -- 2x2池化,步长2,输出尺寸减半

对于CIFAR-10,输入张量维度是[batchSize, 3, 32, 32]。经过上述层后,输出维度变为[batchSize, 32, 16, 16]((32-3+2*1)/1 +1 = 32,池化后32/2=16)。

3.2 针对CIFAR-10的经典网络结构借鉴与实现

直接从头设计一个高性能网络比较困难,我们可以借鉴一些经典且被验证有效的轻量级网络结构,如VGG、ResNet的简化版,或者专门为CIFAR设计的网络。这里我们实现一个类似“VGG风格”的简单网络,它由多个连续的3x3卷积堆叠而成,中间穿插池化层。

require 'nn' require 'cunn' -- 如果使用GPU local function createModel() local model = nn.Sequential() -- 第一组卷积块: 输入[3,32,32] model:add(nn.SpatialConvolution(3, 64, 3, 3, 1, 1, 1, 1)) model:add(nn.ReLU(true)) model:add(nn.SpatialConvolution(64, 64, 3, 3, 1, 1, 1, 1)) model:add(nn.ReLU(true)) model:add(nn.SpatialMaxPooling(2, 2, 2, 2)) -- 输出[64,16,16] -- 第二组卷积块 model:add(nn.SpatialConvolution(64, 128, 3, 3, 1, 1, 1, 1)) model:add(nn.ReLU(true)) model:add(nn.SpatialConvolution(128, 128, 3, 3, 1, 1, 1, 1)) model:add(nn.ReLU(true)) model:add(nn.SpatialMaxPooling(2, 2, 2, 2)) -- 输出[128,8,8] -- 第三组卷积块 model:add(nn.SpatialConvolution(128, 256, 3, 3, 1, 1, 1, 1)) model:add(nn.ReLU(true)) model:add(nn.SpatialConvolution(256, 256, 3, 3, 1, 1, 1, 1)) model:add(nn.ReLU(true)) model:add(nn.SpatialMaxPooling(2, 2, 2, 2)) -- 输出[256,4,4] -- 展平层:将多维特征图拉成一维向量 model:add(nn.View(256*4*4)) -- 256*4*4 = 4096 -- 全连接层分类头 model:add(nn.Linear(4096, 1024)) model:add(nn.ReLU(true)) model:add(nn.Dropout(0.5)) -- Dropout防止过拟合,在训练时随机丢弃50%神经元 model:add(nn.Linear(1024, 512)) model:add(nn.ReLU(true)) model:add(nn.Dropout(0.5)) model:add(nn.Linear(512, 10)) -- 输出10个类别的分数 model:add(nn.LogSoftMax()) -- 将输出转换为对数概率,常与ClassNLLCriterion损失函数配对 -- 权重初始化:好的初始化对训练至关重要 local function initWeights(module) if module.__typename == 'nn.SpatialConvolution' or module.__typename == 'nn.Linear' then module.weight:normal(0, 0.01) -- 使用小的正态分布初始化权重 module.bias:fill(0) end end model:apply(initWeights) return model end local model = createModel() print(model)

这个模型包含了约900万个参数,对于CIFAR-10来说是一个中等复杂度的模型。使用nn.Sequential可以让我们像串糖葫芦一样把层叠起来,非常清晰。

注意事项nn.View层用于将卷积层输出的多维张量“拍平”成一维,以便输入全连接层。这里256*4*4的计算必须与前一层的输出尺寸严格对应,否则会运行时出错。在修改网络结构时,这里是容易出错的地方,建议打印中间层的尺寸来验证。

4. 模型训练、验证与调优全流程

4.1 数据加载、预处理与增强管道

有了模型和预处理好的数据,我们需要构建一个高效的数据管道。除了之前提到的整体标准化,在训练过程中实时进行数据增强(Data Augmentation)是提升模型泛化能力、防止过拟合的关键手段。对于小尺寸的CIFAR-10,常用的增强包括随机水平翻转、随机裁剪(带填充)和轻微的色彩抖动。

我们可以使用image包中的函数在加载每个批次(mini-batch)时在线进行增强。下面是一个封装好的数据加载器示例:

require 'image' local DataLoader = {} DataLoader.__index = DataLoader function DataLoader.create(dataFile, batchSize, isTrain, augment) local self = setmetatable({}, DataLoader) self.batchSize = batchSize or 32 self.isTrain = (isTrain == nil) and true or isTrain self.augment = augment and isTrain -- 只在训练时做增强 -- 加载之前保存的.t7数据 local dataset = torch.load(dataFile) self.data = dataset.data:float() -- 确保是float类型 self.labels = dataset.labels self.numSamples = self.data:size(1) self.numBatches = math.ceil(self.numSamples / self.batchSize) -- 计算训练集的全局均值和标准差(应预先计算好) self.mean = dataset.mean or torch.Tensor({0.4914, 0.4822, 0.4465}) -- CIFAR-10常用均值 self.std = dataset.std or torch.Tensor({0.2023, 0.1994, 0.2010}) -- CIFAR-10常用标准差 -- 数据标准化函数 local function normalize(batch) for i=1,3 do batch[{ {}, i, {}, {} }]:add(-self.mean[i]):div(self.std[i]) end return batch end -- 数据增强函数 local function augmentImage(img) -- 随机水平翻转 (概率0.5) if torch.bernoulli(0.5) == 1 then img = image.hflip(img) end -- 随机裁剪 (从36x36中随机裁剪32x32) local padded = image.scale(img, 40, 40) -- 先稍微放大 local x = torch.random(1, 40-32+1) local y = torch.random(1, 40-32+1) img = padded[{ {}, {y, y+31}, {x, x+31} }]:clone() -- 可在此处添加色彩抖动(略) return img end function self:getBatch(batchIdx) local startIdx = (batchIdx - 1) * self.batchSize + 1 local endIdx = math.min(batchIdx * self.batchSize, self.numSamples) local size = endIdx - startIdx + 1 local batchData = torch.Tensor(size, 3, 32, 32) local batchLabels for i=1, size do local sampleIdx = startIdx + i - 1 local img = self.data[sampleIdx]:clone() -- 训练时进行数据增强 if self.augment then img = augmentImage(img) end batchData[i] = img end -- 应用标准化 batchData = normalize(batchData) if self.isTrain then batchLabels = torch.Tensor(size) for i=1, size do batchLabels[i] = self.labels[startIdx + i - 1] end -- Torch的ClassNLLCriterion需要标签是1-indexed的LongTensor batchLabels = batchLabels:long() else -- 测试集,标签可能是文件名,这里我们返回索引用于追踪 batchLabels = {} for i=1, size do table.insert(batchLabels, self.labels[startIdx + i - 1]) end end return batchData, batchLabels end return self end

这个数据加载器封装了批处理、在线增强和标准化,在训练循环中调用非常方便。

4.2 损失函数、优化器与训练循环实现

模型、数据都准备好了,现在进入核心的训练环节。我们需要定义三样东西:衡量预测好坏的损失函数(Criterion)、更新模型权重的优化算法(Optimizer),以及将它们串联起来的训练循环。

对于多分类问题,最常用的损失函数是负对数似然损失(nn.ClassNLLCriterion),它需要与nn.LogSoftMax层配合使用。优化器我们选择带动量的随机梯度下降(SGD)或更流行的Adam。

require 'optim' local cfg = { learningRate = 0.001, momentum = 0.9, weightDecay = 1e-4, -- L2正则化,防止过拟合 batchSize = 128, numEpochs = 100 } -- 加载数据 local trainLoader = DataLoader.create('cifar10-train.t7', cfg.batchSize, true, true) -- 假设我们预留了5000张训练图片作为验证集,这里需要事先分割好 -- local valData, valLabels = ... -- 创建模型和损失函数 local model = createModel() local criterion = nn.ClassNLLCriterion() -- 如果使用GPU,将模型和损失函数转移到GPU if pcall(require, 'cunn') then model = model:cuda() criterion = criterion:cuda() print('模型已移至GPU。') else print('未检测到CUDA,使用CPU运行。') end -- 定义优化器状态和参数 local parameters, gradParameters = model:getParameters() local optimState = { learningRate = cfg.learningRate, momentum = cfg.momentum, weightDecay = cfg.weightDecay, learningRateDecay = 1e-7 } -- 训练循环 local epoch = 1 while epoch <= cfg.numEpochs do model:training() -- 设置为训练模式(启用Dropout等) local trainLoss = 0 local trainCorrect = 0 local trainTotal = 0 -- 遍历所有训练批次 for batchIdx = 1, trainLoader.numBatches do -- 1. 获取一个批次的数据和标签 local inputs, targets = trainLoader:getBatch(batchIdx) if inputs:type() == 'torch.FloatTensor' and model.__typename == 'nn.Sequential' and model:type():find('Cuda') then inputs = inputs:cuda() targets = targets:cuda() end -- 2. 定义闭包函数,用于计算当前批次的损失和梯度 local feval = function(x) if x ~= parameters then parameters:copy(x) end gradParameters:zero() -- 梯度清零 local outputs = model:forward(inputs) -- 前向传播 local loss = criterion:forward(outputs, targets) -- 计算损失 local gradOutputs = criterion:backward(outputs, targets) -- 损失反向传播 model:backward(inputs, gradOutputs) -- 模型反向传播,计算梯度 -- 计算准确率 local _, predictions = torch.max(outputs, 2) -- 获取预测类别 predictions = predictions:view(-1) local correct = predictions:eq(targets):sum() trainCorrect = trainCorrect + correct trainTotal = trainTotal + targets:size(1) trainLoss = trainLoss + loss return loss, gradParameters end -- 3. 调用优化器更新参数 (这里使用SGD) optim.sgd(feval, parameters, optimState) -- 可选:每N个批次打印一次进度 if batchIdx % 50 == 0 then local avgLoss = trainLoss / batchIdx local acc = 100 * trainCorrect / trainTotal print(string.format('Epoch: %d [%d/%d], Loss: %.4f, Acc: %.2f%%', epoch, batchIdx, trainLoader.numBatches, avgLoss, acc)) end end -- 一个epoch结束后,计算平均训练损失和准确率 local avgTrainLoss = trainLoss / trainLoader.numBatches local trainAcc = 100 * trainCorrect / trainTotal print(string.format('\nEpoch %d 结束 -> 训练损失: %.4f, 训练准确率: %.2f%%', epoch, avgTrainLoss, trainAcc)) -- 4. 在验证集上评估模型性能 model:evaluate() -- 设置为评估模式(关闭Dropout等) -- ... (验证集评估代码,与训练循环类似,但不进行反向传播和参数更新) -- local valAcc = evaluateOnValidationSet(model, valData, valLabels) -- print(string.format('验证准确率: %.2f%%', valAcc)) -- 5. 学习率衰减(可选) -- if epoch % 30 == 0 then -- optimState.learningRate = optimState.learningRate * 0.1 -- print('学习率衰减至: ' .. optimState.learningRate) -- end -- 6. 保存模型检查点(可选) -- if epoch % 10 == 0 then -- torch.save(string.format('model_epoch_%d.t7', epoch), model:clearState()) -- end epoch = epoch + 1 end

这个训练循环包含了深度学习的核心要素:前向传播、损失计算、反向传播、梯度更新。optim.sgd封装了SGD的更新逻辑。循环中还包含了简单的进度打印和准确率计算。

实操心得:训练初期,损失可能下降很快,然后变慢。一定要监控验证集准确率,它是判断模型是否过拟合、是否需要早停(Early Stopping)的关键。如果验证集准确率在连续多个epoch不再提升甚至下降,而训练集准确率还在上升,很可能就是过拟合了,此时应停止训练,并回溯到验证集准确率最高的那个模型检查点。

5. 模型评估、预测与Kaggle提交

5.1 模型性能评估与集成学习思路

训练完成后,我们需要在独立的测试集(或从训练集预留的验证集)上评估模型的最终性能。评估不仅仅是看准确率,对于分类问题,混淆矩阵(Confusion Matrix)能告诉我们模型在哪些类别上容易混淆。例如,猫和狗、鹿和马这些类别可能因为外观相似而容易分错。

在Torch7中,我们可以编写一个简单的评估函数:

function evaluateModel(model, dataLoader) model:evaluate() -- 确保是评估模式 local correct, total = 0, 0 local confusion = optim.ConfusionMatrix(10) -- 10个类别 for batchIdx = 1, dataLoader.numBatches do local inputs, targets = dataLoader:getBatch(batchIdx) if model:type():find('Cuda') then inputs = inputs:cuda() end local outputs = model:forward(inputs) local _, predictions = torch.max(outputs, 2) predictions = predictions:view(-1):float() if model:type():find('Cuda') then predictions = predictions:cuda() end confusion:batchAdd(predictions, targets) correct = correct + predictions:eq(targets):sum() total = total + targets:size(1) end confusion:updateValids() print(confusion) -- 打印混淆矩阵 local accuracy = 100 * correct / total print(string.format('总体准确率: %.2f%%', accuracy)) return accuracy, confusion end

为了追求更高的竞赛分数,单一模型往往有瓶颈。集成学习(Ensemble Learning)是提升性能的强有力手段。最简单的集成方法是模型平均:训练多个结构相同但初始化不同的模型(或者同一个模型在不同训练轮次保存的检查点),在预测时,将它们的输出概率(softmax后的结果)进行平均,然后取argmax作为最终预测。这种方法通常能稳定提升1-2个百分点的准确率。

5.2 生成Kaggle提交文件与后续优化方向

Kaggle的CIFAR-10竞赛要求提交一个CSV文件,包含idlabel两列。id是测试集图片的文件名(不含扩展名),label是预测的类别名称(如‘frog’)。

假设我们加载了测试集数据testData(包含数据和对应的文件名列表),并使用训练好的模型进行预测:

require 'csvigo' local model = torch.load('best_model.t7') -- 加载最优模型 model:evaluate() local predictions = {} local batchSize = 128 local numTestSamples = testData.data:size(1) for i=1, numTestSamples, batchSize do local batchEnd = math.min(i+batchSize-1, numTestSamples) local batchInputs = testData.data[{{i, batchEnd}}]:float() if model:type():find('Cuda') then batchInputs = batchInputs:cuda() end local batchOutputs = model:forward(batchInputs) local _, batchPreds = torch.max(batchOutputs, 2) batchPreds = batchPreds:view(-1):int() for j=1, batchPreds:size(1) do local idx = i + j - 1 local fileName = testData.labels[idx]:gsub('%.png$', '') -- 去掉.png后缀 local className = classNames[batchPreds[j]] -- 将数字标签转为类别名 table.insert(predictions, {fileName, className}) end end -- 写入CSV文件 local csvData = {{"id", "label"}} for _, pred in ipairs(predictions) do table.insert(csvData, pred) end csvigo.save{path='submission.csv', data=csvData} print('提交文件 submission.csv 已生成。')

至此,一个完整的Kaggle CIFAR-10项目流程就走完了。但深度学习的调优是无止境的。如果你还想进一步提升分数,可以考虑以下几个方向:

  1. 更强大的网络结构:尝试ResNet、DenseNet、EfficientNet等更现代的架构。Torch7的nn包可能没有现成实现,但你可以用其基本模块搭建,或者寻找社区开源实现。
  2. 更激进的数据增强:除了水平翻转和随机裁剪,可以尝试Cutout、MixUp、AutoAugment等更先进的增强策略。
  3. 学习率调度:使用余弦退火(Cosine Annealing)、带热重启的随机梯度下降(SGDR)等动态调整学习率的方法,而非简单的步长衰减。
  4. 标签平滑:在损失函数中使用标签平滑(Label Smoothing),可以减轻模型对训练标签的过度自信,有时能提升泛化能力。
  5. 测试时增强:对测试图片进行多种增强(如水平翻转、多尺度裁剪),将多个预测结果平均,这几乎是无成本的性能提升方法。

这个项目虽然基于“古老”的Torch7,但它所涵盖的数据处理、模型构建、训练调优、评估提交的完整流程,是任何深度学习图像分类任务的通用骨架。掌握它,你就掌握了解决一类问题的基本方法论。