手机价格预测实战:从特征工程到Keras回归模型

发布时间:2026/10/2 20:01:56
手机价格预测实战:从特征工程到Keras回归模型 前些天我整理手头的 Python 项目时翻出一个练手价值拉满的案例手机价格预测。说它典型是因为手机价格和硬件参数高度相关内存、存储、电池、摄像头这些字段清清楚楚地躺在表格里而这类结构化数据正好是前馈神经网络的强项。这篇博客不聊虚的直接把整个案例从数据准备、特征工程到模型训练、评估、踩坑排查完整走一遍。适合刚学完神经网络基础但没写过完整项目的人也适合想用 Keras 做价格回归的读者。代码全部可以直接跑数据源换成你自己手里的商品价格表也完全没问题。1. 案例定位与整体思路1.1 先想清楚你是在做回归还是在做分类手机价格预测听起来只有一句话但落笔写代码前必须先明确任务类型。因为“价格预测”在真实业务里有两个完全不同的形态一是预测具体价格比如这台手机卖 3280 元还是 3350 元这是回归任务二是预测价格区间比如低端、中端、高端、旗舰这是分类任务。很多项目一开始不区分这个问题后面损失函数、输出层结构、评估指标全都会跟着错。我这里用回归作为主线因为回归输出更直观能直接算出“平均差多少钱”对业务解释更友好。回归任务在神经网络里的配置要点是输出层只用 1 个神经元且不使用激活函数损失函数用均方误差 MSE 或平均绝对误差 MAE。如果改成分类只需要把输出层改成 4 个神经元加 Softmax损失函数换成交叉熵其余流程几乎可以复用。先确定任务边界后面所有代码才有一个清晰骨架。1.2 为什么选前馈神经网络而不是 CNN 或 LSTM很多人一听到神经网络就想到卷积神经网络 CNN 或者 LSTM其实这两个模型在这个场景里都属于“杀鸡用牛刀”。CNN 擅长处理图像这类具有空间结构的数据LSTM 擅长处理时间序列这类有先后顺序的数据而手机价格预测的输入是 RAM、电池、像素、屏幕尺寸这类相互独立的表格字段。字段之间没有网格关系也没有时序依赖强行用 CNN 或 LSTM 反而会把简单问题复杂化。前馈神经网络也叫多层感知机是最适配这种结构化表格数据的神经网络结构。信息从输入层进入经过一个或多个隐藏层逐层向前传递最后从输出层出来。每一层做的本质上是对输入做加权求和、加偏置、再过激活函数。多个隐藏层堆叠起来就能拟合价格与参数之间那种不是简单线性关系的复杂映射。手机价格和参数之间确实存在一些交互效应比如大内存和高端芯片同时出现时价格会跳跃式上升这种非线性关系用线性回归很难表达而一个两到三层的前馈网络已经足够捕捉。1.3 对整个工作流的预期管理需要提前说清楚这个案例的目标不是“训练出一个上市可用的价格模型”而是把神经网络在结构化数据上的标准工作流完整跑通。数据准备、特征工程、模型构建、训练监控、指标评估、过拟合排查每一步都有值得注意的细节。根据我自己的经验整个流程里最容易翻车的往往不是模型结构而是数据泄漏和归一化时机错误。这些坑后面会专门展开。另外如果你的数据量只有几百条神经网络未必比 XGBoost 或者线性回归更强。我的建议是不管最终选什么模型都先用一个简单模型把预测结果做出来作为基准再上神经网络。如果神经网络连基准都打不过通常是数据处理出了问题而不是模型能力不够。2. 数据准备与特征工程2.1 数据长什么样先看再干我先用 pandas 把数据读进来不管数据来自公开数据集、爬虫还是厂商报价表第一步永远是看数据的基本情况。建议列字段至少包含这几类ram_gb运行内存、internal_storage机身存储、battery_mah电池容量、camera_mp后置摄像头像素、screen_size屏幕尺寸、weight_g重量、price价格。import pandas as pd import numpy as np df pd.read_csv(phone_price_dataset.csv) print(数据集形状:, df.shape) print(df.info()) print(df.describe()) print(缺失值统计:) print(df.isnull().sum())这段代码会依次告诉我总共有多少行多少列、每列的数据类型和非空数量、各数值字段的统计分布、哪些列存在缺失值。很多人拿到数据就急着敲神经网络结果跑着跑着因为空值报错或者因为某列是字符串类型而直接崩掉。先做这一步后面省很多事。如果是手头没有现成数据、只想把流程跑通的情况也可以用下面这种方式生成一份模拟数据。价格按一个外加噪声的线性公式生成这样数据规律明确适合验证代码正确性np.random.seed(42) n 1000 df pd.DataFrame({ ram_gb: np.random.randint(2, 16, n), internal_storage: np.random.choice([64, 128, 256, 512], n), battery_mah: np.random.randint(1500, 6000, n), camera_mp: np.random.randint(8, 200, n), screen_size: np.random.uniform(5.0, 7.0, n), weight_g: np.random.randint(140, 260, n) }) df[price] ( 1800 df[ram_gb] * 250 df[internal_storage] * 8 df[camera_mp] * 10 df[screen_size] * 80 df[weight_g] * 2 np.random.normal(0, 300, n) ).clip(1000, 12000) df.to_csv(phone_price_demo.csv, indexFalse)这里特别提醒模拟数据只适合验证工程链路不适合得出任何“预测精度”结论。真实项目里模型效果取决于真实数据的质量和特征丰富程度模拟数据因为规律几乎全被公式定义神经网络很容易学到但这并不能代表真实场景的预测效果。2.2 特征筛选和相关性粗看数据读进来之后下一步不是直接丢给模型而是先观察各特征和价格的关系。最直接的方式是计算相关系数矩阵把价格这一列和所有特征的相关系数排个序。import matplotlib.pyplot as plt import seaborn as sns print(df.corr()[price].sort_values(ascendingFalse)) plt.figure(figsize(10, 8)) sns.heatmap(df.corr(), annotTrue, cmapcoolwarm, fmt.2f) plt.tight_layout() plt.show()相关系数能帮助我们判断哪些特征是强相关变量。在手机价格场景里RAM、存储、后摄像素通常和价格正相关重量有时是负相关。如果某些特征和价格相关系数几乎为零可以适当考虑剔除减少模型噪声。但要注意相关性系数衡量的只是线性关系神经网络擅长找非线性关系所以不能只看相关系数低就一刀切删掉结合特征含义判断更重要。如果数据里有品牌、是否支持 5G、机身颜色这类类别特征直接喂给神经网络是行不通的需要先做 One-Hot 编码。pandas 的get_dummies就能搞定df pd.get_dummies(df, columns[brand, is_5g], drop_firstTrue)2.3 划分数据集和归一化顺序千万别搞错这是全流程里我见过翻车最频繁的地方没有之一。正确的顺序是先划分训练集和测试集再在训练集上做归一化拟合最后用同一个拟合好的缩放器去变换测试集。很多初学者图省事先对整个数据集做归一化然后再划分这会让测试集的信息提前泄露到训练过程里最终评估结果虚高模型上线后预测一塌糊涂。from sklearn.model_selection import train_test_split from sklearn.preprocessing import MinMaxScaler X df.drop(price, axis1) y df[price].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) x_scaler MinMaxScaler() X_train_scaled x_scaler.fit_transform(X_train) X_test_scaled x_scaler.transform(X_test) y_scaler MinMaxScaler() y_train_scaled y_scaler.fit_transform(y_train.reshape(-1, 1)) y_test_scaled y_scaler.transform(y_test.reshape(-1, 1))为什么价格也需要归一化因为回归任务的输出层不接激活函数输出值就是网络算出来的实数。如果真实价格在四五千块甚至上万块目标值非常大反向传播时梯度也可能非常大训练会变得不稳定。把价格缩放到 0 到 1 之间模型只需要预测一个小范围的数训练更平滑。最后拿到预测结果时再用inverse_transform还原成真实价格。这里要补一个新手容易遗漏的细节MinMaxScaler不能用全局变量y来拟合必须用y_train。先分割、后拟合这个原则对所有预处理都适用包括缺失值填充、异常值处理、PCA 降维。3. 用 Keras 搭建前馈神经网络并完成训练3.1 搭一个三层全连接网络Keras 是 TensorFlow 的高层 API用来快速搭建前馈神经网络非常合适。我这里的网络结构很简单输入层之后接两个隐藏层最后接一个输出层。输入层的维度不用手动指定为具体数字写成X_train.shape[1]会自动读取特征数量。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense from tensorflow.keras.optimizers import Adam model Sequential() model.add(Dense(64, activationrelu, input_shape(X_train_scaled.shape[1],))) model.add(Dense(32, activationrelu)) model.add(Dense(1)) model.summary()隐藏层激活函数选 ReLU 而不是 Sigmoid是因为 ReLU 在深层网络里能有效缓解梯度消失训练收敛更快。输出层不加激活函数意味着输出任意实数这是回归任务的标准写法。如果把输出层误加上了 ReLU那模型永远预测不了负值如果加上 Sigmoid输出会被限制在 0 到 1 之间不反归一化的话就成了一个“假价格”。一个实用的建议是从小网络试起。很多人上来就堆五层、每层 128 个神经元其实对这个量级的数据完全没必要。手机价格预测的特征通常不超过 20 个两层隐藏层已经足够表达大部分非线性关系。网络越大越容易过拟合训练时间也越长并不能直接带来精度提升。3.2 编译、训练与早停编译阶段需要指定损失函数、优化器和评估指标。回归任务我习惯用mse作为损失函数同时把mae作为监控指标因为平均绝对误差的单位和价格一致更容易理解模型大概差了多少钱。model.compile(optimizerAdam(learning_rate0.001), lossmse, metrics[mae]) history model.fit( X_train_scaled, y_train_scaled, validation_split0.2, epochs200, batch_size32, verbose1 )训练时我会在训练集里再切出 20% 作为验证集用来观察模型是否过拟合。epochs设到 200但实际不一定跑满可以配合 EarlyStopping 在验证集损失不再下降时自动停止同时恢复最优权重。这样既防止过拟合也不必一遍遍手动调整 epoch 数量。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint early_stop EarlyStopping( monitorval_loss, patience30, restore_best_weightsTrue ) checkpoint ModelCheckpoint( phone_price_best.keras, monitorval_loss, save_best_onlyTrue ) history model.fit( X_train_scaled, y_train_scaled, validation_split0.2, epochs200, batch_size32, callbacks[early_stop, checkpoint], verbose1 )EarlyStopping 的patience30表示连续 30 轮损失不再下降就停止。这个参数需要根据训练稳定性来调数据量小或者学习率低的时候损失可能偶尔波动patience 太小会提前停止模型还没收敛patience 太大会拖长训练时间。一般我先设 20 到 50看情况微调。3.3 训练曲线怎么看训练结束后第一时间把损失曲线画出来。这段代码我会原样保留在每次训练之后因为曲线能告诉我模型状态是否健康。plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.title(Loss Curve) plt.subplot(1, 2, 2) plt.plot(history.history[mae], labeltrain_mae) plt.plot(history.history[val_mae], labelval_mae) plt.xlabel(epoch) plt.ylabel(MAE) plt.legend() plt.title(MAE Curve) plt.tight_layout() plt.show()看图的时候我的判断逻辑是这样的训练损失和验证损失一起稳定下降说明网络学习正常训练损失降得很低验证损失先降后反弹说明过拟合需要减少网络容量加 dropout 或早停两条曲线都几乎不下降说明学习率太小网络可能欠拟合loss 动不动跳到 NaN绝大多数是学习率太大或者输入数据里有 NaN。回归任务里没有理由追求loss0因为手机价格本身存在噪声同一配置在不同渠道、不同时间的售价本来就不同。模型能把大部分价格趋势学出来验证损失不再下降就已经达到目的。3.4 模型保存与加载好不容易训完模型当然不能每次预测都重新训练一遍。Keras 提供了完整的模型保存和加载接口model.save(phone_price_model.keras) from tensorflow.keras.models import load_model loaded_model load_model(phone_price_model.keras)保存的时候建议用.keras这种新格式它会把模型结构、权重、优化器状态和编译信息都保存下来。加载之后可以直接用loaded_model.predict()做推理。如果是部署到后端接口可以接着把它转成 TensorFlow Lite减小体积、加速推理。4. 模型评估与超参数调优4.1 回归指标不只 trừ看 loss模型训练完之后必须在没参与训练的测试集上评估。由于前面把价格做了 0-1 归一化预测结果需要先反归一化再计算指标这样输出的误差才是“元”这个单位看着更直观。from sklearn.metrics import ( mean_absolute_error, mean_squared_error, r2_score ) y_pred_scaled model.predict(X_test_scaled) y_pred y_scaler.inverse_transform(y_pred_scaled.reshape(-1, 1)).ravel() mae mean_absolute_error(y_test, y_pred) mse mean_squared_error(y_test, y_pred) rmse np.sqrt(mse) r2 r2_score(y_test, y_pred) print(fMAE: {mae:.2f}) print(fRMSE: {rmse:.2f}) print(fR2: {r2:.4f})这几个指标各有用途我整理成一张表方便对照指标看什么好模型大概什么样MAE平均绝对误差预测价和真实价平均差多少元越小越好单位是元最直观RMSE均方根误差对离群点更敏感比 MAE 大异常预测会让它明显变大R²模型解释了价格波动的多少比例接近 1 说明模型能解释大部分波动0 左右说明预测基本等于均值举个例子如果测试集上 MAE 是 380 元说明模型平均下来预测价格和真实价格差 380 元。对于价格范围在两三千到一万二的手机数据这个误差是可以接受的。如果 RMSE 远大于 MAE比如 MAE 400、RMSE 900就要小心模型是不是在某些样本上预测结果特别离谱这些样本往往是配置极端或价格异常的手机。同时画一张真实价格和预测价格的散点图可以直观看出模型在什么价位段表现好、什么价位段偏差大。plt.figure(figsize(6, 6)) plt.scatter(y_test, y_pred, alpha0.5) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--, linewidth2) plt.xlabel(Real Price) plt.ylabel(Predicted Price) plt.tight_layout() plt.show()如果散点图上的点都贴着对角线说明预测很准确如果低价段贴着对角线、高价段散开说明模型对高价机的预测偏差更大。这个信息对后续特征优化很有价值。4.2 超参数调优的主要方向神经网络最让人头疼的地方就是超参数多但在这个案例里真正需要关注的只有几项隐藏层神经元数量、学习率、batch size、训练轮数。先说我常用的起点隐藏层从64-32开始学习率从0.001开始batch size 从32开始。跑完看验证损失再逐个调整。一次只改一个参数记录每组的结果否则最后出了问题根本不知道是谁造成的。调参时可以在代码里套一个简单循环比如测试不同的神经元数量组合results [] for units in [(32, 16), (64, 32), (128, 64)]: model Sequential() model.add(Dense(units[0], activationrelu, input_shape(X_train_scaled.shape[1],))) model.add(Dense(units[1], activationrelu)) model.add(Dense(1)) model.compile(optimizerAdam(learning_rate0.001), lossmse, metrics[mae]) history model.fit( X_train_scaled, y_train_scaled, validation_split0.2, epochs100, batch_size32, verbose0 ) val_mae min(history.history[val_mae]) results.append((units, val_mae)) print(results)用验证集上的 MAE 作为选择依据不要用训练集 MAE否则一定会选到过拟合最严重的那组。学习率对训练行为的影响最大。学习率太大损失会震荡甚至爆炸学习率太小训练半天都走不动。Adam 优化器虽然自带自适应学习率但初始学习率仍然关键。我的经验是0.001适合大多数场景如果训练不收敛就降到0.0001如果收敛慢就升到0.01但 0.01 以上对这个小网络就很少用了。4.3 防过拟合的几件常用家具手机价格数据量不大过拟合是一个真实风险。除了早停还有几个常用手段。Dropout 层会在训练时随机丢弃部分神经元的输出让网络不能过度依赖某几个特征。在前馈网络里我会在隐藏层之间加 Dropout(0.2 到 0.3)测试时网络会使用全部神经元预测不会受影响。BatchNormalization 是我比较喜欢加的层它能把每一层的输入拉回稳定分布训练收敛更快很多时候能把学习率调大一点。加在 Dense 层和激活函数之间是常见操作from tensorflow.keras.layers import Dropout, BatchNormalization model Sequential() model.add(Dense(64, input_shape(X_train_scaled.shape[1],))) model.add(BatchNormalization()) model.add(Activation(relu)) model.add(Dropout(0.2)) model.add(Dense(32)) model.add(BatchNormalization()) model.add(Activation(relu)) model.add(Dropout(0.2)) model.add(Dense(1))注意激活函数的位置BatchNormalization 一般放在线性变换之后、激活函数之前。很多人习惯写成Dense - ReLU - BN效果可能会打折扣。这个细节我会在踩坑部分再说。另一个通用原则是数据量太少时不要盲目加深网络。几百条数据配一个两层网络都容易过拟合三层以上大概率白搭。先把基准模型调好再逐步增加容量。5. 实际踩坑记录与排查建议5.1 数据泄漏最容易翻车的“好成绩”我自己早期踩过最狠的坑就是数据泄漏。有一次我拿到一份数据里面已经有一列“价格区间”为了省事就直接把它当成特征丢进模型结果训练时 R² 高达 0.98测试集也好看得不像话。上线后发现预测一塌糊涂复盘才发现问题价格区间本质上是根据价格划分出来的标签信息把“结果”当“特征”等于考试前先看答案。同类问题还有两种常见表现。第一种是先对整个数据集做归一化再划分训练集测试集的统计信息被训练集“看到”导致验证成绩虚高。第二种是删除异常值时看了全数据集分布后才决定范围也算一种信息泄漏。所有涉及统计计算的操作都应该只基于训练集做测试集只能被变换、不能参与计算。5.2 归一化目标值后忘记反变换这个问题出现的频率非常高。训练时把价格y用 MinMaxScaler 缩放到 0-1预测出来是一个零点几的小数。如果不做inverse_transform直接拿这个小数和真实价格对比MAE 会变成几百块模型看起来像是完全没用。我见过有人在这个问题上卡了很久甚至怀疑网络结构有问题其实只需要一行代码y_pred y_scaler.inverse_transform(y_pred_scaled.reshape(-1, 1)).ravel()一个容易混淆的细节是训练时fit_transform作用在y_train上预测时的transform作用在新输入X_new上反变换时作用在模型的输出y_pred_scaled上。这四步里的缩放器对象必须保持一致不能重新 fit。5.3 训练不收敛、NaN 或曲线横着走训练过程中如果出现loss: nan先别急着调网络结构按顺序排查这几个原因数据里是否有 NaN 或无穷值学习率是否过大是否忘了归一化特征列是否混入字符串没有编码。我遇到的 NaN 绝大多数是学习率太大把学习率从 0.01 降到 0.001 后问题立刻消失。如果损失曲线像一条横线长期不下降除了学习率太小外还有一种可能是 ReLU 的死亡单元问题也就是很多神经元输出恒为 0梯度传不回去。遇到这种情况我会检查 Dense 层的初始化和学习率也可以尝试换成 LeakyReLU。如果训练损失降得很好验证损失却越走越高这是明确的过拟合信号。优先做法是加 EarlyStopping再考虑调小网络或者加 Dropout。不要一上来就加数据很多人手里根本没有额外数据可加。5.4 小数据量场景下的模型选择如果经过各种调参后神经网络仍然打不过一个简单的 XGBoost请先别怀疑自己的代码。神经网络的强项是需要大量数据支撑的复杂模式几百条样本时它的优势发挥不出来反而因为超参数多、训练不稳定而吃亏。我有一个习惯先跑一个线性回归或随机森林作为基准记录测试集 MAE再上神经网络。如果神经网络在相同数据上的 MAE 明显低于基准说明网络确实学到了非线性关系如果两者差不多甚至神经网络更差那就要回头看数据质量、预处理和超参数而不是继续堆层数。从工程效率角度说这个手机价格预测案例最适合的定位是学习工具而不是生产级部署方案。真正做价格预测系统时我会把 XGBoost、LightGBM 和神经网络各自训练线上根据准确率和推理延迟做付费选择。最后再分享一个小技巧我折腾完这类项目后会把每个版本的模型、数据和指标记录成一张表格比如“版本 1归一化前划分MAE 430版本 2正确归一化MAE 380版本 3加早停MAE 360”。这样做的好处是隔几天再回头看时能直接判断哪些调整真正起到了作用而不是凭感觉说“好像效果好了一点”。这种记录习惯比任何花哨的调参技巧都更能保证项目质量。对手机价格预测案例来说训练一个“完美模型”不是终点把整个数据处理链路和调试思路练到肌肉记忆才是真正值钱的部分。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询