【深度学习】(四)案例:基于PyTorch的全连接神经网络实现手机价格区间预测
1 案例介绍
Jones创办了一家二手手机公司,在经营过程中遇到了一个核心难题:如何准确估算二手手机的合理售价。为了解决这个问题,他系统性地收集了多家手机销售公司的历史交易数据。该数据集包含了二手手机的各项性能指标,以及对应的最终成交价格区间。价格被划分为0、1、2、3四个等级,分别代表从低到高的价格范围,这使得问题本质上成为一个多分类任务,而非传统的回归预测问题。
数据集包含以下20个特征字段:
| 特征名称 | 含义说明 | 单位 |
|---|---|---|
| battery_power | 电池总容量 | 毫安时(mAh) |
| blue | 是否支持蓝牙 | 布尔值 |
| clock_speed | 处理器主频 | GHz |
| dual_sim | 是否支持双卡 | 布尔值 |
| fc | 前置摄像头像素 | 百万像素 |
| four_g | 是否支持4G网络 | 布尔值 |
| int_memory | 内置存储空间 | GB |
| m_dep | 手机厚度 | cm |
| mobile_wt | 手机重量 | g |
| n_cores | CPU核心数 | 个 |
| pc | 后置主摄像头像素 | 百万像素 |
| px_height | 屏幕像素高度 | 像素 |
| px_width | 屏幕像素宽度 | 像素 |
| ram | 运行内存大小 | MB |
| sc_h | 屏幕高度 | cm |
| sc_w | 屏幕宽度 | cm |
| talk_time | 最长通话时长 | 小时 |
| three_g | 是否支持3G网络 | 布尔值 |
| touch_screen | 是否配备触控屏 | 布尔值 |
| wifi | 是否支持WiFi | 布尔值 |
| price_range | 价格区间(目标变量) | 0/1/2/3 |
我们的目标是帮助Jones挖掘出手机各项硬件配置与售价区间之间的内在关联规律。借助深度学习技术,构建一个全连接神经网络模型,通过学习历史数据中的模式,实现对二手手机价格区间的自动、准确预测。
整个实现流程遵循标准的机器学习项目规范,分为四个核心步骤:
- 数据准备:加载并预处理数据,划分训练集与验证集
- 模型构建:设计适合多分类任务的全连接神经网络架构
- 模型训练:配置损失函数、优化器,执行迭代训练并引入早停机制
- 模型评估:在验证集上测试模型性能,评估分类准确率
2 构建数据集
原始数据集共包含2000条样本记录,按照机器学习的最佳实践,我们将其划分为训练集和验证集:其中1600条(80%)用于模型训练,400条(20%)用于模型验证和性能评估。数据集划分采用sklearn.model_selection.train_test_split函数,并通过固定随机种子(random_state=88)确保结果的可重复性。
在数据预处理阶段,我们需要完成以下关键操作:
- 特征与标签分离:使用
pandas读取CSV文件,将前20列作为特征矩阵x,最后一列price_range作为目标标签y - 数据类型转换:特征值统一转换为
np.float32类型以适配神经网络计算;标签值转换为np.int64类型用于交叉熵损失函数 - Tensor封装:利用
torch.from_numpy()将NumPy数组转换为PyTorch张量,并构建TensorDataset对象,便于后续与DataLoader配合使用
核心实现代码如下:
def create_dataset():
# 使用pandas读取数据
data = pd.read_csv(DATA_CSV)
# 特征值和目标值分离
x, y = data.iloc[:, :-1], data.iloc[:, -1]
# 类型转换:特征值 → float32,目标值 → int64
x = x.astype(np.float32)
y = y.astype(np.int64)
# 数据集划分(训练集:验证集 = 8:2)
x_train, x_valid, y_train, y_valid = train_test_split(x, y, train_size=TRAIN_SIZE, random_state=RANDOM_STATE)
# 转换为Tensor并移入到指定设备(CPU/GPU)
x_train_tensor = torch.from_numpy(x_train.values).to(DEVICE)
y_train_tensor = torch.tensor(y_train.values).to(DEVICE)
x_valid_tensor = torch.from_numpy(x_valid.values).to(DEVICE)
y_valid_tensor = torch.tensor(y_valid.values).to(DEVICE)
# 构建TensorDataset数据集对象
train_dataset = TensorDataset(x_train_tensor, y_train_tensor)
valid_dataset = TensorDataset(x_valid_tensor, y_valid_tensor)
# 返回:训练集、验证集、特征维度数、类别总数
return train_dataset, valid_dataset, x_train.shape[1], len(np.unique(y))
3 构建网络模型
针对本项目的多分类任务特点,我们设计了一个三层全连接神经网络(多层感知机,MLP)。网络结构采用"逐层递减"的隐藏单元设计策略,从输入层到输出层逐步压缩特征维度,最终映射到4个类别输出。
模型架构详情如下:
| 网络层 | 输入维度 | 输出维度 | 激活函数 | 补充操作 |
|---|---|---|---|---|
| 第1层(线性层) | input_dim(20) |
HIDDEN_DIM1(64) |
ReLU | Dropout(0.3) |
| 第2层(线性层) | HIDDEN_DIM1(64) |
HIDDEN_DIM2(32) |
ReLU | Dropout(0.3) |
| 第3层(线性层) | HIDDEN_DIM2(32) |
output_dim(4) |
无(线性输出) | — |
设计考量:
- 层级递减结构:64 → 32 → 4,逐层提取高层次语义特征
- Dropout正则化:在前两个隐藏层后分别添加Dropout层(丢弃率0.3),有效抑制过拟合
- ReLU激活函数:引入非线性变换能力,同时缓解梯度消失问题
- 输出层无激活:直接输出logits,配合
CrossEntropyLoss使用(该损失函数内部集成了Softmax)
对应的PyTorch实现:
class PhonePriceModel(nn.Module):
def __init__(self, input_dim, output_dim):
super(PhonePriceModel, self).__init__()
# 第一层:输入 → 64维
self.linear1 = nn.Linear(input_dim, HIDDEN_DIM1)
self.dropout1 = nn.Dropout(DROPOUT_RATE)
# 第二层:64 → 32维
self.linear2 = nn.Linear(HIDDEN_DIM1, HIDDEN_DIM2)
self.dropout2 = nn.Dropout(DROPOUT_RATE)
# 第三层:32 → 4维(分类输出)
self.linear3 = nn.Linear(HIDDEN_DIM2, output_dim)
def forward(self, x):
x = torch.relu(self.linear1(x))
x = self.dropout1(x)
x = torch.relu(self.linear2(x))
x = self.dropout2(x)
output = self.linear3(x) # 输出logits
return output
4 模型训练
训练环节是整个流程的核心,我们精心配置了多项优化策略来提升模型的收敛效果和泛化能力:
训练配置概览
| 配置项 | 设定值 | 说明 |
|---|---|---|
| 批量大小(Batch Size) | 32 | 小批量梯度下降 |
| 学习率(Learning Rate) | 1×10⁻⁴ | Adam优化器推荐初始值 |
| 优化器 | Adam | 自适应矩估计,收敛快 |
| 权重衰减(L2正则化) | 1×10⁻⁴ | 防止过拟合 |
| 损失函数 | 交叉熵损失 | 适配多分类任务 |
| 学习率调度策略 | StepLR | 每30个epoch衰减为0.1倍 |
| 最大训练轮数 | 1000 | 配合早停提前终止 |
| 早停耐心值 | 75 | 验证损失连续75轮不下降则停止 |
早停机制(EarlyStopping)
早停是一种有效的正则化手段,当验证集损失在连续patience个epoch内没有显著改善时,自动终止训练,避免过拟合并节省计算资源。实现逻辑如下:
class EarlyStopping:
def __init__(self, patience=10, min_delta=1e-4):
self.patience = patience
self.min_delta = min_delta # 最小改善阈值
self.counter = 0
self.best_loss = float('inf')
self.early_stop = False
def __call__(self, val_loss):
if val_loss < self.best_loss - self.min_delta:
self.best_loss = val_loss # 验证损失有显著改善,重置计数器
self.counter = 0
else:
self.counter += 1 # 无显著改善,累加计数
if self.counter >= self.patience:
self.early_stop = True # 触发早停
return self.early_stop
训练主循环
在每个epoch中,依次执行以下操作:
- 训练阶段:模型设置为
train()模式,前向传播计算损失,反向传播更新参数 - 验证阶段:模型设置为
eval()模式,关闭梯度计算,在验证集上计算损失和准确率 - 学习率更新:调用
scheduler.step()按策略调整学习率 - 早停判断:根据验证损失决定是否提前终止训练
- 进度输出:实时打印当前epoch的训练损失、验证损失、验证准确率及耗时
训练结束后,模型权重被保存到model/phone_best.pth文件中,便于后续加载使用。
def train(train_dataset, valid_dataset, input_dim, class_num):
# 初始化模型、优化器、调度器、早停对象
torch.manual_seed(SEED)
if torch.cuda.is_available():
torch.cuda.manual_seed(SEED)
train_loader = DataLoader(train_dataset, shuffle=True, batch_size=BATCH_SIZE)
valid_loader = DataLoader(valid_dataset, shuffle=False, batch_size=BATCH_SIZE)
model = PhonePriceModel(input_dim, class_num).to(DEVICE)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=LR_STEP_SIZE, gamma=LR_GAMMA)
early_stopping = EarlyStopping(patience=PATIENCE)
train_losses, val_losses = [], []
for epoch_idx in range(NUM_EPOCHS):
# —— 训练阶段 ——
model.train()
for x, y in train_loader:
output = model(x)
loss = criterion(output, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# —— 验证阶段 ——
model.eval()
val_loss = 0.0
correct = 0
with torch.no_grad():
for x, y in valid_loader:
output = model(x)
loss = criterion(output, y)
val_loss += loss.item()
y_pred = torch.argmax(output, dim=1)
correct += (y_pred == y).sum()
avg_val_loss = val_loss / len(valid_loader)
val_losses.append(avg_val_loss)
val_acc = correct.item() / len(valid_dataset)
# 学习率调度
scheduler.step()
# 打印结果
print('epoch: %4s train_loss: %.4f val_loss: %.4f val_acc: %.4f time: %.2fs' %
(epoch_idx + 1, avg_train_loss, avg_val_loss, val_acc, time.time() - start))
# 早停检查
if early_stopping(avg_val_loss):
print(f"Early stopping at epoch {epoch_idx + 1}")
break
# 保存模型
torch.save(model.state_dict(), 'model/phone_best.pth')
return train_losses, val_losses
5 模型预测与评估
模型训练完成后,我们使用独立的验证集对模型性能进行最终评估。评估流程如下:
- 模型加载:实例化
PhonePriceModel,加载已保存的最佳模型权重(model/phone_best.pth) - 数据加载:将验证集封装为
DataLoader,按批次进行推理 - 预测计算:对每个批次的输入数据执行前向传播,得到输出logits
- 类别判定:使用
torch.argmax(output, dim=1)获取每个样本的预测类别(0~3) - 准确率统计:比较预测结果与真实标签,计算总体分类准确率
def test(valid_dataset, input_dim, class_num):
# 加载模型和训练好的网络参数
model = PhonePriceModel(input_dim, class_num)
model.load_state_dict(torch.load('model/phone_best.pth', map_location=DEVICE))
model.to(DEVICE)
dataloader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False)
correct = 0
for x, y in dataloader:
output = model(x)
y_pred = torch.argmax(output, dim=1) # 预测类别
correct += (y_pred == y).sum() # 累计正确预测数
acc = correct.item() / len(valid_dataset)
print(f'验证集准确率: {acc:.5f}')
6 环境依赖与运行
在运行本程序之前,请确保已安装以下Python依赖库:
pip install torch torchvision torchaudio # PyTorch(根据CUDA版本选择)
pip install scikit-learn # 数据集划分与评估
pip install matplotlib # 可视化(可选)
pip install numpy pandas # 数据处理
程序入口位于if __name__ == '__main__':代码块中,按顺序执行数据加载、模型训练和评估三个环节:
if __name__ == '__main__':
print(f"使用设备: {DEVICE}")
# 1. 加载数据
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
print(f"输入特征数:{input_dim},分类个数:{class_num}")
# 2. 模型训练
train(train_dataset, valid_dataset, input_dim, class_num)
# 3. 模型评估
test(valid_dataset, input_dim, class_num)
通过以上完整的四步流程,我们构建了一个端到端的二手手机价格区间预测系统,能够帮助Jones高效、准确地为回收的手机产品定价提供决策支持。
完整代码
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import time
from torchsummary import summary
# ==================== 超参数配置 ====================
# 数据路径
DATA_CSV = r'data/手机价格预测.csv'
# 数据划分参数
TRAIN_SIZE = 0.8
RANDOM_STATE = 88
VALIDATION_SIZE = 0.2 # 验证集比例
# 训练参数(优化)
BATCH_SIZE = 32
LEARNING_RATE = 1e-4 # 学习率
NUM_EPOCHS = 1000 # 轮数
PATIENCE = 75 # 早停耐心值
# 模型参数(简化)
HIDDEN_DIM1 = 64 # 减小,避免过拟合
HIDDEN_DIM2 = 32 # 逐层递减
# 优化器选择
OPTIMIZER = 'adam' # 使用Adam替代SGD
WEIGHT_DECAY = 1e-4 # L2正则化
# 学习率调度
LR_SCHEDULER = 'step' # 使用学习率调度
LR_STEP_SIZE = 30 # 每30个epoch衰减
LR_GAMMA = 0.1 # 衰减因子
# Dropout(防止过拟合)
DROPOUT_RATE = 0.3
# 设备配置
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
# 随机种子
SEED = 42
# ==================================================
# 构建数据集
def create_dataset():
# 使用pandas读取数据
data = pd.read_csv(DATA_CSV)
# 特征值和目标值
x, y = data.iloc[:, :-1], data.iloc[:, -1]
# 类型转换:特征值,目标值
x = x.astype(np.float32)
y = y.astype(np.int64)
# 数据集划分
x_train, x_valid, y_train, y_valid = \
train_test_split(x, y, train_size=TRAIN_SIZE, random_state=RANDOM_STATE)
# 转换为tensor并移入到设备
x_train_tensor = torch.from_numpy(x_train.values).to(DEVICE)
y_train_tensor = torch.tensor(y_train.values).to(DEVICE)
x_valid_tensor = torch.from_numpy(x_valid.values).to(DEVICE)
y_valid_tensor = torch.tensor(y_valid.values).to(DEVICE)
# 构建数据集
train_dataset = TensorDataset(x_train_tensor, y_train_tensor)
valid_dataset = TensorDataset(x_valid_tensor, y_valid_tensor)
# 返回结果:训练集数据,验证集数据,样本特征维度和目标分类个数
return train_dataset, valid_dataset, x_train.shape[1], len(np.unique(y))
# 构建网络模型
class PhonePriceModel(nn.Module):
def __init__(self, input_dim, output_dim):
super(PhonePriceModel, self).__init__()
# 1. 第一层: 输入为维度为 input_dim, 输出维度为: HIDDEN_DIM1
self.linear1 = nn.Linear(input_dim, HIDDEN_DIM1)
self.dropout1 = nn.Dropout(DROPOUT_RATE)
# 2. 第二层: 输入为维度为 HIDDEN_DIM1, 输出维度为: HIDDEN_DIM2
self.linear2 = nn.Linear(HIDDEN_DIM1, HIDDEN_DIM2)
self.dropout2 = nn.Dropout(DROPOUT_RATE)
# 3. 第三层: 输入为维度为 HIDDEN_DIM2, 输出维度为: output_dim
self.linear3 = nn.Linear(HIDDEN_DIM2, output_dim)
def forward(self, x):
# 前向传播过程
x = torch.relu(self.linear1(x))
x = self.dropout1(x)
x = torch.relu(self.linear2(x))
x = self.dropout2(x)
output = self.linear3(x)
return output
class EarlyStopping:
def __init__(self, patience=10, min_delta=1e-4):
self.patience = patience
self.min_delta = min_delta
self.counter = 0
self.best_loss = float('inf')
self.early_stop = False
def __call__(self, val_loss):
if val_loss < self.best_loss - self.min_delta:
self.best_loss = val_loss
self.counter = 0
else:
self.counter += 1
if self.counter >= self.patience:
self.early_stop = True
return self.early_stop
# 模型训练过程
def train(train_dataset, valid_dataset, input_dim, class_num):
torch.manual_seed(SEED)
if torch.cuda.is_available():
torch.cuda.manual_seed(SEED)
train_loader = DataLoader(train_dataset, shuffle=True, batch_size=BATCH_SIZE)
valid_loader = DataLoader(valid_dataset, shuffle=False, batch_size=BATCH_SIZE)
model = PhonePriceModel(input_dim, class_num).to(DEVICE)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=LR_STEP_SIZE, gamma=LR_GAMMA)
early_stopping = EarlyStopping(patience=PATIENCE)
train_losses, val_losses = [], []
for epoch_idx in range(NUM_EPOCHS):
# 训练阶段
model.train()
train_loss = 0.0
start = time.time()
for x, y in train_loader:
output = model(x)
loss = criterion(output, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
avg_train_loss = train_loss / len(train_loader)
train_losses.append(avg_train_loss)
# 验证阶段
model.eval()
val_loss = 0.0
correct = 0
with torch.no_grad():
for x, y in valid_loader:
output = model(x)
loss = criterion(output, y)
val_loss += loss.item()
y_pred = torch.argmax(output, dim=1)
correct += (y_pred == y).sum()
avg_val_loss = val_loss / len(valid_loader)
val_losses.append(avg_val_loss)
val_acc = correct.item() / len(valid_dataset)
# 学习率调度
scheduler.step()
# 打印结果
print('epoch: %4s train_loss: %.4f val_loss: %.4f val_acc: %.4f time: %.2fs' %
(epoch_idx + 1, avg_train_loss, avg_val_loss, val_acc, time.time() - start))
# 早停检查
if early_stopping(avg_val_loss):
print(f"Early stopping at epoch {epoch_idx + 1}")
break
# 保存模型
torch.save(model.state_dict(), 'model/phone_best.pth')
return train_losses, val_losses
# 模型预测结果评估
def test(valid_dataset, input_dim, class_num):
# 加载模型和训练好的网络参数
model = PhonePriceModel(input_dim, class_num)
model.load_state_dict(torch.load('model/phone.pth', map_location=DEVICE))
model.to(DEVICE)
# 构建加载器
dataloader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False)
# 评估测试集
correct = 0
# 遍历测试集中的数据
for x, y in dataloader:
# 数据已经在GPU上
# 将其送入网络中
output = model(x)
# 获取类别结果
y_pred = torch.argmax(output, dim=1)
# 获取预测正确的个数
correct += (y_pred == y).sum()
# 求预测精度
print('Acc: %.5f' % (correct.item() / len(valid_dataset)))
if __name__ == '__main__':
print(f"使用设备: {DEVICE}")
# 获取数据
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
print("输入特征数:", input_dim)
print("分类个数:", class_num)
# 模型实例化
model = PhonePriceModel(input_dim, class_num)
# summary(model, input_size=(input_dim,), batch_size=BATCH_SIZE, device=DEVICE)
# 模型训练
train(train_dataset, valid_dataset, input_dim, class_num)
# 模型预测结果
test(valid_dataset, input_dim, class_num)
更多推荐


所有评论(0)