PyTorch深度学习框架之基于CNN的手机价格分类任务

目录:

    • 一、实现流程梳理
    • 二、需求背景
    • [三、环境准备 & 数据加载](#三、环境准备 & 数据加载)
      • [3.1 首先安装需要的依赖:](#3.1 首先安装需要的依赖:)
      • [3.2 代码第一步:加载数据](#3.2 代码第一步:加载数据)
      • [3.3 数据预处理](#3.3 数据预处理)
    • 四、构建CNN模型
      • [4.1 换成224×224的图片参数计算](#4.1 换成224×224的图片参数计算)
      • [4.2 加层,一般4次池化到14×14(也可以根据你需要增减) 为啥到14x14就不池化了 不是池化到1x1](#4.2 加层,一般4次池化到14×14(也可以根据你需要增减) 为啥到14x14就不池化了 不是池化到1x1)
    • 五、模型训练
    • [六、模型评估 & 预测](#六、模型评估 & 预测)
    • 七、关键细节

一、实现流程梳理

  1. 问题理解 & 数据准备
  2. 数据预处理(特征工程)
  3. 构建CNN模型
  4. 模型训练 & 评估
  5. 预测 & 部署

二、需求背景

手机价格分类就是:根据手机的各项属性(比如品牌、CPU、内存、电池、屏幕尺寸...),预测它属于哪个价格档位(比如:低/中/高 三档,或者 0-1000/1000-3000/3000+ 等)

CNN 在这里一般处理两种输入:

  • 如果是手机图片数据:直接用CNN提取图片特征做分类
  • 如果是结构化表格数据(比如参数表格):我们把一维特征reshape成二维特征图,再用CNN提取特征分类

我们这里以最常见的结构化表格数据 为例(如果是图片,流程基本一致,只是输入不同),用经典的 Mobile Price Classification 数据集 做演示。

训练数据集的地址:

https://www.kaggle.com/datasets/iabhishekofficial/mobile-price-classification/data

三、环境准备 & 数据加载

3.1 首先安装需要的依赖:

powershell 复制代码
pip install pandas numpy scikit-learn torch matplotlib

3.2 代码第一步:加载数据

python 复制代码
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# 加载数据集(kaggle下载的csv)
train_df = pd.read_csv("train.csv")
test_df = pd.read_csv("test.csv")

print(train_df.head())
print("数据形状:", train_df.shape)

数据集截图效果:

3.3 数据预处理

我们把表格数据转为CNN能处理的格式:

python 复制代码
# 分离特征和标签
X = train_df.drop("price_range", axis=1).values  # 所有特征
y = train_df["price_range"].values                # 标签

# 拆分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 标准化特征
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_val = scaler.transform(X_val)

# ========== 关键一步:把一维特征转为二维 "特征图" 给CNN ==========
# 我们有20个特征,把它 reshape 成 (4,5) 或者 (5,4) 二维,就可以当成图片输入CNN了
# 形状变化:(batch_size, 20) → (batch_size, 1, 4, 5) → channel=1, 高4宽5
feature_count = X_train.shape[1]
# 这里我们凑成正方形:如果特征数不能开方,补0即可,这里20个特征→(4,5)=20,刚好
img_h = 4
img_w = 5

X_train = X_train.reshape(-1, 1, img_h, img_w)  # (样本数, 通道数, 高, 宽)
X_val = X_val.reshape(-1, 1, img_h, img_w)

# 转成PyTorch张量
X_train_tensor = torch.tensor(X_train, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train, dtype=torch.long)
X_val_tensor = torch.tensor(X_val, dtype=torch.float32)
y_val_tensor = torch.tensor(y_val, dtype=torch.long)

# 创建DataLoader
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
val_dataset = TensorDataset(X_val_tensor, y_val_tensor)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

💡 如果是图片数据,这一步不一样:​

python 复制代码
# 如果是手机图片,直接读取图片,resize到统一大小,归一化即可
# 比如每张图片是 (224,224,3) → 转成 (3,224,224) 张量,直接输入CNN

代码疑点解析:

python 复制代码
# 分离特征和标签
X = train_df.drop("price_range", axis=1).values  # 所有特征
y = train_df["price_range"].values                # 标签

我们先理清楚概念:

 - 特征(X)​​:就是用来做预测的"输入信息",在手机价格分类这个任务里,就是手机的所有参数:电池容量、CPU频率、内存大小...这些是我们已知的信息
 - 标签(y)​​:就是我们想要预测的目标​,这里就是手机的价格档位(price_range列,0=低/1=中/2=高/3=超高)

然后逐行解释代码:

1. train_df.drop("price_range", axis=1)
 - train_df:是我们加载进来的整个训练数据表(pandas的DataFrame格式)
 - .drop("price_range", axis=1):把price_range这一列从表格里删掉
 - axis=1的意思是"按列删除",axis=0是按行删除,这里我们删列,所以写1
2. .values:把删掉之后剩下的表格,转成numpy的二维数组,方便后续处理,结果赋值给X,所以X就是所有特征,形状是(样本数, 特征数)
3. y = train_df["price_range"].values
 - train_df["price_range"]:把我们要预测的目标列单独拿出来
 - .values:同样转成numpy数组,赋值给y,所以y就是所有标签,形状是(样本数,)

举个例子:如果我们有1000条手机数据,20个参数,那么:

 - X.shape = (1000, 20) → 1000个手机,每个手机20个特征
 - y.shape = (1000,) → 每个手机对应1个价格档位标签

四、构建CNN模型

我们来搭一个简单的CNN分类网络,结构非常清晰:

python 复制代码
class CNNPhonePriceClassifier(nn.Module):
    def __init__(self, num_classes=4, in_channels=1):
        super().__init__()
        # 卷积层提取特征
        self.conv_layers = nn.Sequential(
            # 第一层卷积:输入1通道,输出16个卷积核,kernel 3x3,padding保持大小
            nn.Conv2d(in_channels, 16, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),  # 池化,尺寸减半
            
            # 第二层卷积
            nn.Conv2d(16, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )
        
        # 计算全连接层输入维度:输入是(1,4,5) → 池化两次后 → 32通道,1x1(如果输入更大,这里维度会变)
        # 这里我们的例子:4x5 → 第一次池化→2x2 → 第二次池化→1x1 → 32*1*1 = 32
        self.fc_layers = nn.Sequential(
            nn.Dropout(0.3),  # 防止过拟合
            nn.Linear(32 * 1 * 1, 64),
            nn.ReLU(),
            nn.Linear(64, num_classes)  # 最后输出,对应类别数
        )
        
    def forward(self, x):
        x = self.conv_layers(x)  # 卷积提取特征
        x = x.view(x.size(0), -1)  # 展平
        x = self.fc_layers(x)  # 全连接分类
        return x

# 初始化模型
model = CNNPhonePriceClassifier(num_classes=4)
print(model)

如果你的输入是大尺寸图片​,比如(3, 224, 224),只需要修改全连接层的输入维度就行,卷积部分逻辑不变。

💡 一句话总结参数来源

举例:padding=1:为了保持卷积后的尺寸不变,计算出来的:

python 复制代码
padding的作用是给输入边缘补0,我们这里要卷积后尺寸不变​,所以可以算出来:
公式:卷积后尺寸 = (输入尺寸 + 2*padding - kernel_size)/stride + 1
我们的默认 stride=1(卷积步长,不写就是1),想要尺寸不变,代入:

输入尺寸 = 输出尺寸
输入尺寸 = (输入尺寸 + 2*padding - 3)/1 + 1
解这个方程:→  padding = 1

4.1 换成224×224的图片参数计算

池化简易逻辑:

MaxPool2d(2) 就是直接把尺寸除以2,向下取整​:

原始 4 × 5 → 第一次除以2:4/2=2,5/2=2.5向下取整=2 → 2×2

再除以2:2/2=1,2/2=1 → 1×1 刚好两次池化就变成1×1了~

如果原始宽是6不是5,那第一次池化就是4×6 → 2×3,第二次池化就是1×1,还是会得到1×1,本质就是我们原始输入本身就很小,两次减半之后自然就到1了😀

4.2 加层,一般4次池化到14×14(也可以根据你需要增减) 为啥到14x14就不池化了 不是池化到1x1

其实池化不是必须到1×1,不继续池化是两个原因:性能需求+计算效率:

1. 为什么原来小输入要池化到1×1?

原来我们输入只有4×5,本身尺寸就极小:

  • 两次池化自然就到1×1了,没得选,再池化尺寸就变成0了,没法继续
  • 而且小输入特征少,到1×1刚好,所有信息都浓缩完了,直接接全连接就可以

2. 为什么224×224不用池化到1×1?

我们先算一下,如果要把224×224池化到1×1,需要池化几次:

224 → 112 → 56 → 28 → 14 → 7 → 3 → 1 → 一共要 7次池化

不继续池化到1×1,是两个核心原因:

① 会丢失大量空间信息,效果变差

② 参数会爆炸,计算量太大

3. 那什么时候需要池化到1×1?

只有两种场景:

  • 输入本身极小:就像我们原来的4×5表格特征,本身尺寸小,自然到1×1
  • 全局池化:不管输入多大,最后用一层AdaptiveAvgPool2d(1)直接强制到1×1,目的是让模型支持任意输入尺寸,这是分类任务的一种技巧,不是必须的。比如现在很多backbone 都会最后加全局池化到1×1,那是另外的设计了。

五、模型训练

现在开始训练,写训练循环:

python 复制代码
# 定义损失函数和优化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
criterion = nn.CrossEntropyLoss()  # 多分类用交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)

# 训练轮数
num_epochs = 20
train_losses = []
val_losses = []
val_accs = []

for epoch in range(num_epochs):
    # 训练阶段
    model.train()
    running_train_loss = 0.0
    for batch_x, batch_y in train_loader:
        batch_x, batch_y = batch_x.to(device), batch_y.to(device)
        
        # 前向传播
        outputs = model(batch_x)
        loss = criterion(outputs, batch_y)
        
        # 反向传播 + 优化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        running_train_loss += loss.item() * batch_x.size(0)
    
    epoch_train_loss = running_train_loss / len(train_loader.dataset)
    train_losses.append(epoch_train_loss)
    
    # 验证阶段
    model.eval()
    running_val_loss = 0.0
    correct = 0
    total = 0
    with torch.no_grad():
        for batch_x, batch_y in val_loader:
            batch_x, batch_y = batch_x.to(device), batch_y.to(device)
            outputs = model(batch_x)
            loss = criterion(outputs, batch_y)
            running_val_loss += loss.item() * batch_x.size(0)
            
            # 计算准确率
            _, predicted = torch.max(outputs.data, 1)
            total += batch_y.size(0)
            correct += (predicted == batch_y).sum().item()
    
    epoch_val_loss = running_val_loss / len(val_loader.dataset)
    epoch_val_acc = correct / total
    val_losses.append(epoch_val_loss)
    val_accs.append(epoch_val_acc)
    
    # 打印日志
    print(f"Epoch [{epoch+1}/{num_epochs}] "
          f"Train Loss: {epoch_train_loss:.4f} "
          f"Val Loss: {epoch_val_loss:.4f} "
          f"Val Acc: {epoch_val_acc:.4f}")

# 绘制训练曲线
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(train_losses, label="Train Loss")
plt.plot(val_losses, label="Val Loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(val_accs, label="Val Accuracy")
plt.xlabel("Epoch")
plt.ylabel("Accuracy")
plt.legend()
plt.show()

六、模型评估 & 预测

训练完了我们来评估一下,做个测试预测:

python 复制代码
# 在验证集上输出最终准确率
print(f"最终验证准确率: {val_accs[-1]:.4f}")

# 保存模型
torch.save(model.state_dict(), "cnn_phone_classifier.pth")
print("模型已保存")

# ========== 预测新样本 ==========
# 加载测试数据
X_test = test_df.drop("id", axis=1).values if "id" in test_df.columns else test_df.values
X_test = scaler.transform(X_test)
X_test = X_test.reshape(-1, 1, img_h, img_w)
X_test_tensor = torch.tensor(X_test, dtype=torch.float32).to(device)

model.eval()
with torch.no_grad():
    outputs = model(X_test_tensor)
    _, predictions = torch.max(outputs, 1)

# 输出预测结果
predictions = predictions.cpu().numpy()
print("预测结果(前10个):", predictions[:10])

# 可以把结果保存成csv
test_df["predicted_price_range"] = predictions
test_df.to_csv("predictions.csv", index=False)

七、关键细节

1.为什么CNN能处理表格数据?​

  • 我们把一维特征 reshape 成二维,CNN可以捕捉特征之间的局部关联,比纯全连接网络泛化能力更强
  • 如果特征很多,reshape成更大的二维,效果更好

2.输入尺寸怎么计算?​

  • 卷积后尺寸公式:
    output_size = floor((input_size + 2*padding - kernel_size)/stride) + 1
    池化一般是kernel=2, stride=2,所以尺寸直接减半

3.过拟合怎么办?​

  • 加 Dropout(我们代码里已经加了)
  • 加 L2 正则(weight_decay)
  • 增加数据,或者做数据增强(如果是图片数据)

4.如果是图片数据,整个流程哪里不一样?​

python 复制代码
   # 只需要改输入预处理和输入通道数
   from torchvision import transforms
   from PIL import Image

   # 图片预处理
   transform = transforms.Compose([
       transforms.Resize((224, 224)),
       transforms.ToTensor(),
       transforms.Normalize(mean=[0.485, 0.456, 0.406],
                            std=[0.229, 0.224, 0.225])
   ])
   # 模型把in_channels改成3
   model = CNNPhonePriceClassifier(num_classes=4, in_channels=3)
   # 剩下的训练预测代码完全一样!
相关推荐
m0_564876841 小时前
图像生成提示词工程
深度学习
隔壁大炮1 小时前
Day07-词嵌入层解释
人工智能·深度学习·算法·计算机视觉·cnn
zhangfeng11332 小时前
LLaMA-Factory 保存 checkpoint 时崩溃解决办法 OOM 内存溢出(不是显存)
运维·服务器·人工智能·深度学习·llama
数智工坊2 小时前
【VarifocalNet(VFNet)论文阅读】:IoU-aware稠密目标检测,把定位质量塞进分类得分
论文阅读·人工智能·深度学习·目标检测·计算机视觉·分类·cnn
Westward-sun.2 小时前
YOLOv5 最新版从零配置环境到训练自己的数据集
人工智能·pytorch·深度学习·yolo
ting94520002 小时前
ComNet 深度解析:模型驱动深度学习在 OFDM 接收机中的革命性应用
人工智能·深度学习
ting94520002 小时前
动手学深度学习(PyTorch版)深度详解(2)(模型入门:从理论到实操)
人工智能·深度学习·神经网络
不要绝望总会慢慢变强2 小时前
无人机智能体的实现的一些思考
人工智能·深度学习·ai·无人机
动物园猫2 小时前
高压电线电力巡检六类图像识别数据集分享(适用于YOLO系列深度学习分类检测任务)
深度学习·yolo·分类