48、ANN手机价格分类案例

AgenticCoding·十二月创作之星挑战赛 10w+人浏览 284人参与

需求分析

流程

准备训练集数据(要输出:训练数据集,测试数据集,输入特征数,输出特征数)

构建使用的模型

模型训练

模型评估

准备训练数据(五大步骤)

  • 训练数据集(张量数据集 TensorDataset),测试数据集,输入特征数,输出特征数
  • 步骤 1:读取表格数据(pandas)df = pd.read_csv("data.csv"),后的数据类型为DataFrame
  • 步骤2:分离特征x,和标签y , 后的数据都是NumPy 数组(numpy.ndarray)类型。
  • 步骤3:把 numpy 转成 Tensor :X_tensor = torch.tensor(X, dtype=torch.float32)
  • 步骤4:打包为TensorDataset 张量数据集 (就是把输入和标签绑定成一个“样本单元”)也就是需要x,y进来两个张量
  • 步骤5:放进 DataLoader(用于训练)数据加载器,可以将数据集分为一批一批数据 train_loader = DataLoader(dataset, batch_size=16, shuffle=True)
def create_dataset():
    data = pd.read_csv('../data/手机价格预测.csv')
    print(f'data:{data.head()}')
    print(f'shape:{data.shape}')

    # 分离出特征x 和标签y(也就是正确值)
    x,y = data.iloc[:,:-1],data.iloc[:,-1]

    x = x.astype(np.float32)

    x_train, x_test, y_train, y_test = train_test_split(x,y, test_size=0.2, random_state=3, stratify=y)

    # 数据转为张量数据集  TensorDataset 就是把输入和标签绑定成一个“样本单元”。
    train_dataset = TensorDataset(torch.tensor(x_train.values), torch.tensor(y_train.values))
    test_dataset = TensorDataset(torch.tensor(x_test.values), torch.tensor(y_test.values))

    #                                   20也就是输入特征数   去重当做输出特征数
    return train_dataset, test_dataset, x.shape[1], len(np.unique(y))

从表格数据到 DataLoader 的转换流程(最常用标准做法)

假设你有一份表格数据:CSV、Excel、numpy、pandas DataFrame 都可以。

我们以 pandas DataFrame 示例:

import pandas as pd
import torch
from torch.utils.data import TensorDataset, DataLoader

步骤 1:读取表格数据(pandas)

df = pd.read_csv("data.csv")

假设你的表格长这样:

feature1

feature2

feature3

label

0.12

0.55

0.91

1

0.03

0.22

0.44

0


步骤 2:分离特征(X)和标签(y)

X = df[['feature1','feature2','feature3']].values
y = df['label'].values

X shape → (样本数, 特征数)

y shape → (样本数,)


步骤 3:把 numpy 转成 Tensor

X_tensor = torch.tensor(X, dtype=torch.float32)
y_tensor = torch.tensor(y, dtype=torch.long)  # 分类任务用 long

步骤 4:打包成 TensorDataset

TensorDataset 就是把输入和标签绑定成一个“样本单元”。

dataset = TensorDataset(X_tensor, y_tensor)

每个样本结构变成:

dataset[i] = (X_tensor[i], y_tensor[i])

步骤 5:放进 DataLoader(用于训练)

train_loader = DataLoader(dataset, batch_size=16, shuffle=True)

DataLoader 负责:

  • 每次自动给你 16 个样本
  • 自动打乱(shuffle=True)
  • 自动组合 batch
  • 自动转成 GPU(你手动 .to(device))

🌟 完整流程图

表格数据 (CSV / Excel / DataFrame)
        ↓
DataFrame → numpy
        ↓
numpy → Tensor
        ↓
TensorDataset(X_tensor, y_tensor)
        ↓
DataLoader(dataset, batch_size=16, shuffle=True)
        ↓
for x_batch, y_batch in train_loader:
        模型训练

构建使用的模型

  • 三层模型 + relu
  • 继承nn.Module
  • def init(self,input_dim, output_dim): 初始化分类,搭建神经网络(隐藏层 + 输出层)
  • def forward(self, x): 就是做完前向传播的完整链路,加权求和+激活函数 + 层层链接

class PhoneModel(nn.Module):
    def __init__(self,input_dim, output_dim):
        # 1.初始化分类
        super().__init__()

        # 2.搭建神经网络(隐藏层 + 输出层)
        self.linear1 = nn.Linear(input_dim, 128)
        self.linear2 = nn.Linear(128, 256)
        self.output = nn.Linear(256, output_dim)

    # 定义数据流动顺序(前向计算图)。完整的前向传播,加权求和 + 激活函数 + 层链接 + 输出
    # self 代表 当前这个模型对象本身 self 就是 model
    def forward(self, x):
        # self.linear1(x) 就是在做 加权求和 + 加上 bias(偏置)
        # torch.relu(...) 非线性激活
        x = torch.relu(self.linear1(x))
        x = torch.relu(self.linear2(x))
        # 省略了softmax,因为我的
        x = self.output(x)

        # 返回模型最后的输出
        return x

模型训练(4大步骤)

  • 要先准备好3个东西: 定义好模型,定义好损失函数,定义好优化器
  • 一轮训练:有反向传播(model.train())
  • 一轮测试:没有反向传播(model.eval())
  • 保存模型参数

准备3大东西

  • 定义好模型(Model) model = PhoneModel(input_dim, output_dim)
  • 定义好损失函数(Loss function) criterion = nn.CrossEntropyLoss()
  • 定义好优化器(Optimizer) optimizer = optim.Adam(model.parameters(), lr=0.001)

训练

  • 学习模型参数(权重、偏置等),让模型变好
  • 训练集(有标签的样本)
  • 前向传播 → 计算 loss → 反向传播 → 参数更新

测试

  • 评估训练好的模型性能,看看效果怎么样
  • 测试集(独立的,有标签或者无标签)
  • 前向传播 → 计算指标(准确率、召回率等),无反向传播

保存模型(保存模型参数)

  • 保存模型训练参数(权重和偏置)
  • torch.save(model.state_dict(), '../data/phone_model.pth')

方面

训练(Training)

测试(Testing)

目的

学习模型参数(权重、偏置等),让模型变好

评估训练好的模型性能,看看效果怎么样

数据

训练集(有标签的样本)

测试集(独立的,有标签或者无标签)

流程

前向传播 → 计算 loss → 反向传播 → 参数更新

前向传播 → 计算指标(准确率、召回率等),无反向传播

是否更新参数

会,不断调整权重

不更新参数,权重固定

是否计算梯度

否(通常关闭梯度计算,提高效率)

代码区别

loss.backward()optimizer.step()

只有 model.eval()torch.no_grad()

训练流程model.train()(五步)

  • ① 从 DataLoader 取一批数据(batch)
  • ② 前向传播(forward)→ 得到预测值
  • ③ 计算损失(loss)→ 看模型猜得多差
  • ④ 反向传播(backward)→ 计算梯度
  • ⑤ 优化器更新参数(optimizer.step())

① 从 DataLoader 取一批数据(batch)

x_batch, y_batch = next(DataLoader)

这一批数据例如 16 个样本:

  • x_batch:特征(输入)
  • y_batch:标签(真实答案)

② 前向传播(forward)→ 得到预测值

y_pred = model(x_batch)

这一步做的是:

  • 加权求和
  • 激活函数
  • 一层层运算
  • 得到输出

也就是模型“猜测”答案。


③ 计算损失(loss)→ 看模型猜得多差

loss = criterion(y_pred, y_batch)

损失函数告诉你:

  • 模型预测得准 → loss 小
  • 模型预测得烂 → loss 大

④ 反向传播(backward)→ 计算梯度

loss.backward()

PyTorch 自动求导,得到:

  • 每个权重应该往哪个方向动?
  • 动多少?

这是神经网络的“学习”核心。


⑤ 优化器更新参数(optimizer.step())

optimizer.step()
optimizer.zero_grad()

优化器会:

  • 根据梯度
  • 更新模型的权重
  • 让下一次预测更准

💡 训练循环整体长这样:

for epoch in range(epochs):
    for x_batch, y_batch in train_loader:

        y_pred = model(x_batch)         # 1. forward
        loss = criterion(y_pred, y_batch) # 2. loss

        optimizer.zero_grad()
        loss.backward()                 # 3. backward
        optimizer.step()                # 4. update

def train(tran_set, test_set, input_dim, output_dim, epochs):


    # 参1:数据集对象(1600条数据), 参2:每批16条  参3:是否打乱数据
    tran_loader = DataLoader(tran_set, batch_size=16, shuffle=True)
    test_loader = DataLoader(test_set, batch_size=16, shuffle=True)

    # 定义好模型
    model = PhoneModel(input_dim, output_dim)
    # 定义好损失函数
    criterion = nn.CrossEntropyLoss()
    # 定义好优化器
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    train_losses = []
    test_accuracies =[]

    # 搭建训练循环
    print("开始训练模型...")
    for epoch in range(epochs):

        model.train()
        total_loss = 0
        for x, y in tran_loader:
            outputs = model(x)
            loss = criterion(outputs, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_loss += loss.item()
        # 每轮的平均损失
        avg_loss = total_loss/len(tran_loader)
        train_losses.append(avg_loss)

        # 测试阶段
        model.eval()
        # 正确的个数
        correct = 0
        # 测试集样本总数
        total = 0
        # 关闭梯度计算(因为测试时不需要反向传播和梯度。)
        with torch.no_grad():
            for x, y in test_loader:
                # 模型前向推理,前向传播
                outputs = model(x)
                # 取预测类别(argmax)
                # dim = 0 → 按“列”取最大值
                # dim = 1 → 按“行”取最大值(分类用这个)
                '''
                [2.1, -0.5, 1.8]
                   ↑     ↑    ↑
                 类别0  类别1 类别2 的得分
                 
                torch.max(outputs, 1) 返回  values, indices = torch.max(outputs, 1)
                它返回 两个东西:
                    values —— 每行的最大值
                    indices —— 每行最大值所在的下标(也就是预测的类别)
                我只需要第二个值,把第一个值用 _ 忽略掉。
                '''
                _,predicted = torch.max(outputs, 1)
                total += y.size(0)
                correct += (predicted == y).sum().item()
            test_acc = round(correct/ total * 100, 2)
            test_accuracies.append(test_acc)
        print(f'epoch:{epoch + 1}, avg_loss:{avg_loss:.4f} , TestAcc:{test_acc}%')

    # 保存模型参数
    torch.save(model.state_dict(), '../data/phone_model.pth')
    print("模型已保存至 phone_model.pth")

    # 绘制训练损失和测试准确率曲线
    fig, axes = plt.subplots(1, 2, figsize=(12, 4))
    axes[0].plot(train_losses, label='训练损失')
    axes[0].set_xlabel('Epoch')
    axes[0].set_ylabel('Loss')
    axes[0].set_title('训练损失曲线')
    axes[0].legend()
    axes[0].grid(True)

    axes[1].plot(test_accuracies, label='测试准确率', color='orange')
    axes[1].set_xlabel('Epoch')
    axes[1].set_ylabel('Accuracy (%)')
    axes[1].set_title('测试准确率曲线')
    axes[1].legend()
    axes[1].grid(True)

    plt.tight_layout()
    plt.show()

    return model

模型评估

  • 准备数据 + 加载模型 + 用模型预测数据 + 计算评价指标

很好的问题!训练和测试阶段确实都会用到计算指标,但它们的侧重点和用途有所不同:


训练阶段的指标计算和测试评估阶段的指标计算的区别

训练阶段的指标计算

  • 主要目的是监控训练过程,判断模型是否在学习、是否过拟合或欠拟合。
  • 通常计算损失(loss),以及有时会计算**准确率(accuracy)**等辅助指标。
  • 这些指标用于指导是否调整学习率、修改网络结构、提前停止等。

测试(评估)阶段的指标计算

  • 重点是评估模型的最终泛化能力,即模型对“新数据”的表现。
  • 完全没参与训练的数据上计算指标,保证客观公正。
  • 计算更全面的指标,如准确率(accuracy)、精确率(precision)、召回率(recall)、F1-score 等,以全面衡量模型性能。
  • 这些指标决定模型是否达到上线标准,或者比较不同模型优劣。

总结

  • 训练时用准确率监控,发现准确率一直不升,说明模型可能没学好。
  • 训练结束后,用测试集算准确率和 F1-score,决定模型是否能上线。

阶段

指标作用

典型指标

训练阶段

监控训练进展、调参辅助

Loss、Accuracy(偶尔)

测试/评估阶段

最终性能验证,模型质量评判

Accuracy、Precision、Recall、F1-score 等

测试代码

import torch
from torch.utils.data import Dataset, DataLoader, TensorDataset
import torch.nn as nn
import torch.optim as optim
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
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体为黑体
plt.rcParams['axes.unicode_minus'] = False    # 解决负号 '-' 显示为方块的问题

def create_dataset():
    data = pd.read_csv('../data/手机价格预测.csv')
    print(f'data:{data.head()}')
    print(f'shape:{data.shape}')

    # 分离出特征x 和标签y(也就是正确值)
    x,y = data.iloc[:,:-1],data.iloc[:,-1]

    x = x.astype(np.float32)

    x_train, x_test, y_train, y_test = train_test_split(x,y, test_size=0.2, random_state=3, stratify=y)

    # 数据转为张量数据集  TensorDataset 就是把输入和标签绑定成一个“样本单元”。
    train_dataset = TensorDataset(torch.tensor(x_train.values), torch.tensor(y_train.values))
    test_dataset = TensorDataset(torch.tensor(x_test.values), torch.tensor(y_test.values))

    #                                   20也就是输入特征数   去重当做输出特征数
    return train_dataset, test_dataset, x.shape[1], len(np.unique(y))

    pass

class PhoneModel(nn.Module):
    def __init__(self,input_dim, output_dim):
        # 1.初始化分类
        super().__init__()

        # 2.搭建神经网络(隐藏层 + 输出层)
        self.linear1 = nn.Linear(input_dim, 128)
        self.linear2 = nn.Linear(128, 256)
        self.output = nn.Linear(256, output_dim)

    # 定义数据流动顺序(前向计算图)。完整的前向传播,加权求和 + 激活函数 + 层链接 + 输出
    # self 代表 当前这个模型对象本身 self 就是 model
    def forward(self, x):
        # self.linear1(x) 就是在做 加权求和 + 加上 bias(偏置)
        # torch.relu(...) 非线性激活
        x = torch.relu(self.linear1(x))
        x = torch.relu(self.linear2(x))
        # 省略了softmax,因为我的
        x = self.output(x)

        # 返回模型最后的输出
        return x


def train(tran_set, test_set, input_dim, output_dim, epochs):


    # 参1:数据集对象(1600条数据), 参2:每批16条  参3:是否打乱数据
    tran_loader = DataLoader(tran_set, batch_size=16, shuffle=True)
    test_loader = DataLoader(test_set, batch_size=16, shuffle=True)

    # 定义好模型
    model = PhoneModel(input_dim, output_dim)
    # 定义好损失函数
    criterion = nn.CrossEntropyLoss()
    # 定义好优化器
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    train_losses = []
    test_accuracies =[]

    # 搭建训练循环
    print("开始训练模型...")
    for epoch in range(epochs):

        model.train()
        total_loss = 0
        for x, y in tran_loader:
            outputs = model(x)
            loss = criterion(outputs, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_loss += loss.item()
        # 每轮的平均损失
        avg_loss = total_loss/len(tran_loader)
        train_losses.append(avg_loss)

        # 测试阶段
        model.eval()
        # 正确的个数
        correct = 0
        # 测试集样本总数
        total = 0
        # 关闭梯度计算(因为测试时不需要反向传播和梯度。)
        with torch.no_grad():
            for x, y in test_loader:
                # 模型前向推理,前向传播
                outputs = model(x)
                # 取预测类别(argmax)
                # dim = 0 → 按“列”取最大值
                # dim = 1 → 按“行”取最大值(分类用这个)
                '''
                [2.1, -0.5, 1.8]
                   ↑     ↑    ↑
                 类别0  类别1 类别2 的得分
                 
                torch.max(outputs, 1) 返回  values, indices = torch.max(outputs, 1)
                它返回 两个东西:
                    values —— 每行的最大值
                    indices —— 每行最大值所在的下标(也就是预测的类别)
                我只需要第二个值,把第一个值用 _ 忽略掉。
                '''
                _,predicted = torch.max(outputs, 1)
                total += y.size(0)
                correct += (predicted == y).sum().item()
            test_acc = round(correct/ total * 100, 2)
            test_accuracies.append(test_acc)
        print(f'epoch:{epoch + 1}, avg_loss:{avg_loss:.4f} , TestAcc:{test_acc}%')

    # 保存模型参数
    torch.save(model.state_dict(), '../data/phone_model.pth')
    print("模型已保存至 phone_model.pth")

    # 绘制训练损失和测试准确率曲线
    fig, axes = plt.subplots(1, 2, figsize=(12, 4))
    axes[0].plot(train_losses, label='训练损失')
    axes[0].set_xlabel('Epoch')
    axes[0].set_ylabel('Loss')
    axes[0].set_title('训练损失曲线')
    axes[0].legend()
    axes[0].grid(True)

    axes[1].plot(test_accuracies, label='测试准确率', color='orange')
    axes[1].set_xlabel('Epoch')
    axes[1].set_ylabel('Accuracy (%)')
    axes[1].set_title('测试准确率曲线')
    axes[1].legend()
    axes[1].grid(True)

    plt.tight_layout()
    plt.show()

    return model

def load_and_test(input_dim, output_dim):
    model = PhoneModel(input_dim, output_dim)
    model.load_state_dict(torch.load('../data/phone_model.pth'))


    # 随机生成一条输入数据,假设输入特征是20维(生成符合标准正态分布(均值0,标准差1)的随机数)
    '''
    标准正态分布的定义
        标准正态分布 是正态分布的一种特殊情况。
        它的 均值(Mean)是 0,表示数据的平均值集中在 0。
        它的 标准差(Standard Deviation)是 1,表示数据的离散程度(波动范围)大小为1。

        具体含义
        均值 0:数据“中心”在 0 附近。
        标准差 1:大部分数据点(约68%)会落在均值的正负 1 个标准差范围内,也就是 [−1,1]。
        99.7% 的数据点 会落在均值正负3个标准差范围内,也就是 [−3,3]。
    '''
    # example_features = np.random.randn(1, 20).astype(np.float32)
    # input_tensor = torch.tensor(example_features)
    # # 预测时模型切换到eval模式
    # model.eval()
    #
    # with torch.no_grad():
    #     outputs = model(input_tensor)
    #     _, predicted = torch.max(outputs, 1)
    #
    # print(f"预测类别索引: {predicted.item()}")

    # 这里演示一个简单的推断例子(假设用部分测试集数据)
    # 真实使用时可以用更全面的测试流程
    data = pd.read_csv('../data/手机价格预测.csv')
    x, y = data.iloc[:, :-1], data.iloc[:, -1]
    x = x.astype(np.float32)
    x_test = torch.tensor(x.values)


    with torch.no_grad():
        outputs = model(x_test)
        _, predicted = torch.max(outputs, 1)

    y_true = y.values  # 真实标签
    y_pred = predicted.numpy()  # 预测标签 转成 numpy 数组

    # 计算指标
    accuracy = accuracy_score(y_true, y_pred)
    precision = precision_score(y_true, y_pred, average='weighted')  # 多分类加权平均
    recall = recall_score(y_true, y_pred, average='weighted')
    f1 = f1_score(y_true, y_pred, average='weighted')

    print("真实类别前10条:", y_true[:10].tolist())
    print("预测类别前10条:", y_pred[:10].tolist())

    print(f"准确率 (Accuracy): {accuracy:.4f}")
    print(f"精确率 (Precision): {precision:.4f}")
    print(f"召回率 (Recall): {recall:.4f}")
    print(f"F1-score: {f1:.4f}")


if __name__ == '__main__':
    train_dataset, test_dataset, input_dim, output_dim  = create_dataset()
    print(f'input_dim:{input_dim}')
    print(f'output_dim:{output_dim}')


    train(train_dataset, test_dataset,input_dim, output_dim, 50)
    load_and_test(input_dim, output_dim)

测试结果

D:\pythonDemo\.venv\Scripts\python.exe -X pycache_prefix=C:\Users\Administrator.SY-202408261506\AppData\Local\JetBrains\PyCharm2025.3\cpython-cache "D:/Software/PyCharm 2025.3/plugins/python-ce/helpers/pydev/pydevd.py" --multiprocess --qt-support=auto --client 127.0.0.1 --port 49474 --file D:\pythonDemo\demo\test27_ann_phone.py 
Connected to: <socket.socket fd=640, family=2, type=1, proto=0, laddr=('127.0.0.1', 49475), raddr=('127.0.0.1', 49474)>.
Connected to pydev debugger (build 253.28294.336)
data:   battery_power  blue  clock_speed  ...  touch_screen  wifi  price_range
0            842     0          2.2  ...             0     1            1
1           1021     1          0.5  ...             1     0            2
2            563     1          0.5  ...             1     0            2
3            615     1          2.5  ...             0     0            2
4           1821     1          1.2  ...             1     0            1

[5 rows x 21 columns]
shape:(2000, 21)
input_dim:20
output_dim:4
开始训练模型...
epoch:1, avg_loss:12.2327 , TestAcc:52.5%
epoch:2, avg_loss:4.3582 , TestAcc:53.25%
epoch:3, avg_loss:3.9157 , TestAcc:43.75%
epoch:4, avg_loss:4.1651 , TestAcc:50.75%
epoch:5, avg_loss:2.2672 , TestAcc:61.75%
epoch:6, avg_loss:2.9912 , TestAcc:63.25%
epoch:7, avg_loss:1.4561 , TestAcc:55.75%
epoch:8, avg_loss:1.7498 , TestAcc:61.0%
epoch:9, avg_loss:1.2462 , TestAcc:54.25%
epoch:10, avg_loss:1.4452 , TestAcc:63.25%
epoch:11, avg_loss:1.5497 , TestAcc:61.75%
epoch:12, avg_loss:1.2456 , TestAcc:59.5%
epoch:13, avg_loss:1.0575 , TestAcc:62.0%
epoch:14, avg_loss:1.1040 , TestAcc:55.0%
epoch:15, avg_loss:0.9138 , TestAcc:67.25%
epoch:16, avg_loss:1.0124 , TestAcc:62.75%
epoch:17, avg_loss:0.8014 , TestAcc:62.0%
epoch:18, avg_loss:0.8998 , TestAcc:61.25%
epoch:19, avg_loss:0.7951 , TestAcc:65.5%
epoch:20, avg_loss:0.7324 , TestAcc:69.75%
epoch:21, avg_loss:0.7742 , TestAcc:59.75%
epoch:22, avg_loss:0.9613 , TestAcc:65.5%
epoch:23, avg_loss:0.8440 , TestAcc:65.75%
epoch:24, avg_loss:0.7014 , TestAcc:69.5%
epoch:25, avg_loss:0.6506 , TestAcc:69.75%
epoch:26, avg_loss:0.6491 , TestAcc:67.0%
epoch:27, avg_loss:0.6374 , TestAcc:64.0%
epoch:28, avg_loss:0.6607 , TestAcc:70.0%
epoch:29, avg_loss:0.6015 , TestAcc:68.25%
epoch:30, avg_loss:0.6006 , TestAcc:64.75%
epoch:31, avg_loss:0.6279 , TestAcc:66.25%
epoch:32, avg_loss:0.6112 , TestAcc:69.5%
epoch:33, avg_loss:0.5953 , TestAcc:69.25%
epoch:34, avg_loss:0.6595 , TestAcc:68.0%
epoch:35, avg_loss:0.5924 , TestAcc:69.0%
epoch:36, avg_loss:0.5862 , TestAcc:67.75%
epoch:37, avg_loss:0.6022 , TestAcc:71.75%
epoch:38, avg_loss:0.5764 , TestAcc:73.25%
epoch:39, avg_loss:0.5912 , TestAcc:68.5%
epoch:40, avg_loss:0.5842 , TestAcc:73.0%
epoch:41, avg_loss:0.5790 , TestAcc:67.0%
epoch:42, avg_loss:0.5730 , TestAcc:73.75%
epoch:43, avg_loss:0.5526 , TestAcc:70.5%
epoch:44, avg_loss:0.5549 , TestAcc:73.0%
epoch:45, avg_loss:0.5977 , TestAcc:68.25%
epoch:46, avg_loss:0.5707 , TestAcc:71.5%
epoch:47, avg_loss:0.5528 , TestAcc:70.5%
epoch:48, avg_loss:0.5511 , TestAcc:66.0%
epoch:49, avg_loss:0.5648 , TestAcc:67.5%
epoch:50, avg_loss:0.6017 , TestAcc:70.25%
模型已保存至 phone_model.pth
真实类别前10条: [1, 2, 2, 2, 1, 1, 3, 0, 0, 0]
预测类别前10条: [1, 1, 2, 2, 1, 1, 2, 0, 0, 0]
准确率 (Accuracy): 0.7260
精确率 (Precision): 0.7391
召回率 (Recall): 0.7260
F1-score: 0.7276

Process finished with exit code 0

优化模型,优化思路

  • 优化器优化,梯度下降从SGD优化为Adam
  • 数据集进行标准化
  • 增加网络深度,每层的神经元个数
  • 增加训练轮数等等
  • 更换其他激活函数等(LeakyReLU,避免“死神经元”问题 GELU,近年效果不错的激活函数
  • 给各层加入随机失活,防止过拟合 x = self.dropout(x)

优化后的代码

import torch
from sklearn.preprocessing import StandardScaler
from torch.utils.data import Dataset, DataLoader, TensorDataset
import torch.nn as nn
import torch.optim as optim
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
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体为黑体
plt.rcParams['axes.unicode_minus'] = False    # 解决负号 '-' 显示为方块的问题

def create_dataset():
    data = pd.read_csv('../data/手机价格预测.csv')
    print(f'data:{data.head()}')
    print(f'shape:{data.shape}')

    # 分离出特征x 和标签y(也就是正确值)
    x,y = data.iloc[:,:-1],data.iloc[:,-1]

    x = x.astype(np.float32)

    x_train, x_test, y_train, y_test = train_test_split(x,y, test_size=0.2, random_state=3, stratify=y)

    # 优化1、对数据进行标准化(这两个方法返回的是 numpy ndarray)
    transfer = StandardScaler()
    x_train = transfer.fit_transform(x_train)
    x_test = transfer.transform(x_test)

    # 数据转为张量数据集  TensorDataset 就是把输入和标签绑定成一个“样本单元”。
    train_dataset = TensorDataset(torch.tensor(x_train), torch.tensor(y_train.values))
    test_dataset = TensorDataset(torch.tensor(x_test), torch.tensor(y_test.values))
    # train_dataset = TensorDataset(torch.tensor(x_train.values), torch.tensor(y_train.values))
    # test_dataset = TensorDataset(torch.tensor(x_test.values), torch.tensor(y_test.values))

    #                                   20也就是输入特征数   去重当做输出特征数
    return train_dataset, test_dataset, x.shape[1], len(np.unique(y))

    pass

class PhoneModel(nn.Module):
    def __init__(self,input_dim, output_dim):
        # 1.初始化分类
        super().__init__()

        # 优化2、增加网络层数和每层的神经元个数
        # 2.搭建神经网络(隐藏层 + 输出层)
        self.linear1 = nn.Linear(input_dim, 128)
        self.linear2 = nn.Linear(128, 256)
        self.linear3 = nn.Linear(256, 512)
        self.linear4 = nn.Linear(512, 256)
        self.output = nn.Linear(256, output_dim)
        self.dropout = nn.Dropout(p=0.3)

    # 定义数据流动顺序(前向计算图)。完整的前向传播,加权求和 + 激活函数 + 层链接 + 输出
    # self 代表 当前这个模型对象本身 self 就是 model
    def forward(self, x):
        # self.linear1(x) 就是在做 加权求和 + 加上 bias(偏置)
        # torch.relu(...) 非线性激活
        x = torch.relu(self.linear1(x))
        x = self.dropout(x)  # 优化4、加入随机失活
        x = torch.relu(self.linear2(x))
        x = self.dropout(x)
        x = torch.relu(self.linear3(x))
        x = self.dropout(x)
        x = torch.relu(self.linear4(x))
        x = self.dropout(x)
        # 省略了softmax,因为我的
        x = self.output(x)

        # 返回模型最后的输出
        return x


def train(tran_set, test_set, input_dim, output_dim, epochs):


    # 参1:数据集对象(1600条数据), 参2:每批16条  参3:是否打乱数据
    tran_loader = DataLoader(tran_set, batch_size=16, shuffle=True)
    test_loader = DataLoader(test_set, batch_size=16, shuffle=True)

    # 定义好模型
    model = PhoneModel(input_dim, output_dim)
    # 定义好损失函数
    criterion = nn.CrossEntropyLoss()
    # 定义好优化器(SGD 是最基础的梯度下降算法,随机梯度下降),优化6、选择更好的梯度下降
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    train_losses = []
    test_accuracies =[]

    # 搭建训练循环
    print("开始训练模型...")
    for epoch in range(epochs):

        model.train()
        total_loss = 0
        for x, y in tran_loader:
            outputs = model(x)
            loss = criterion(outputs, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_loss += loss.item()
        # 每轮的平均损失
        avg_loss = total_loss/len(tran_loader)
        train_losses.append(avg_loss)

        # 测试阶段
        model.eval()
        # 正确的个数
        correct = 0
        # 测试集样本总数
        total = 0
        # 关闭梯度计算(因为测试时不需要反向传播和梯度。)
        with torch.no_grad():
            for x, y in test_loader:
                # 模型前向推理,前向传播
                outputs = model(x)
                # 取预测类别(argmax)
                # dim = 0 → 按“列”取最大值
                # dim = 1 → 按“行”取最大值(分类用这个)
                '''
                [2.1, -0.5, 1.8]
                   ↑     ↑    ↑
                 类别0  类别1 类别2 的得分
                 
                torch.max(outputs, 1) 返回  values, indices = torch.max(outputs, 1)
                它返回 两个东西:
                    values —— 每行的最大值
                    indices —— 每行最大值所在的下标(也就是预测的类别)
                我只需要第二个值,把第一个值用 _ 忽略掉。
                '''
                _,predicted = torch.max(outputs, 1)
                total += y.size(0)
                correct += (predicted == y).sum().item()
            test_acc = round(correct/ total * 100, 2)
            test_accuracies.append(test_acc)
        print(f'epoch:{epoch + 1}, avg_loss:{avg_loss:.4f} , TestAcc:{test_acc}%')

    # 保存模型参数
    torch.save(model.state_dict(), '../data/phone_model.pth')
    print("模型已保存至 phone_model.pth")

    # 绘制训练损失和测试准确率曲线
    fig, axes = plt.subplots(1, 2, figsize=(12, 4))
    axes[0].plot(train_losses, label='训练损失')
    axes[0].set_xlabel('Epoch')
    axes[0].set_ylabel('Loss')
    axes[0].set_title('训练损失曲线')
    axes[0].legend()
    axes[0].grid(True)

    axes[1].plot(test_accuracies, label='测试准确率', color='orange')
    axes[1].set_xlabel('Epoch')
    axes[1].set_ylabel('Accuracy (%)')
    axes[1].set_title('测试准确率曲线')
    axes[1].legend()
    axes[1].grid(True)

    plt.tight_layout()
    plt.show()

    return model

def load_and_test(input_dim, output_dim):
    model = PhoneModel(input_dim, output_dim)
    model.load_state_dict(torch.load('../data/phone_model.pth'))


    # 随机生成一条输入数据,假设输入特征是20维(生成符合标准正态分布(均值0,标准差1)的随机数)
    '''
    标准正态分布的定义
        标准正态分布 是正态分布的一种特殊情况。
        它的 均值(Mean)是 0,表示数据的平均值集中在 0。
        它的 标准差(Standard Deviation)是 1,表示数据的离散程度(波动范围)大小为1。

        具体含义
        均值 0:数据“中心”在 0 附近。
        标准差 1:大部分数据点(约68%)会落在均值的正负 1 个标准差范围内,也就是 [−1,1]。
        99.7% 的数据点 会落在均值正负3个标准差范围内,也就是 [−3,3]。
    '''
    # example_features = np.random.randn(1, 20).astype(np.float32)
    # input_tensor = torch.tensor(example_features)
    # # 预测时模型切换到eval模式
    # model.eval()
    #
    # with torch.no_grad():
    #     outputs = model(input_tensor)
    #     _, predicted = torch.max(outputs, 1)
    #
    # print(f"预测类别索引: {predicted.item()}")

    # 这里演示一个简单的推断例子(假设用部分测试集数据)
    # 真实使用时可以用更全面的测试流程
    data = pd.read_csv('../data/手机价格预测.csv')
    x, y = data.iloc[:, :-1], data.iloc[:, -1]
    x = x.astype(np.float32)
    x_test = torch.tensor(x.values)


    with torch.no_grad():
        outputs = model(x_test)
        _, predicted = torch.max(outputs, 1)

    y_true = y.values  # 真实标签
    y_pred = predicted.numpy()  # 预测标签 转成 numpy 数组

    # 计算指标
    accuracy = accuracy_score(y_true, y_pred)
    precision = precision_score(y_true, y_pred, average='weighted', zero_division=0)
    recall = recall_score(y_true, y_pred, average='weighted')
    f1 = f1_score(y_true, y_pred, average='weighted')

    print("真实类别前10条:", y_true[:10].tolist())
    print("预测类别前10条:", y_pred[:10].tolist())

    print(f"准确率 (Accuracy): {accuracy:.4f}")
    print(f"精确率 (Precision): {precision:.4f}")
    print(f"召回率 (Recall): {recall:.4f}")
    print(f"F1-score: {f1:.4f}")


if __name__ == '__main__':
    train_dataset, test_dataset, input_dim, output_dim  = create_dataset()
    print(f'input_dim:{input_dim}')
    print(f'output_dim:{output_dim}')

    # 优化5、增加训练论数
    train(train_dataset, test_dataset,input_dim, output_dim, 100)
    load_and_test(input_dim, output_dim)

优化结果

  • 从60% 优化为90%
D:\pythonDemo\.venv\Scripts\python.exe -X pycache_prefix=C:\Users\Administrator.SY-202408261506\AppData\Local\JetBrains\PyCharm2025.3\cpython-cache "D:/Software/PyCharm 2025.3/plugins/python-ce/helpers/pydev/pydevd.py" --multiprocess --qt-support=auto --client 127.0.0.1 --port 57256 --file D:\pythonDemo\demo\test27_ann_phone.py 
Connected to: <socket.socket fd=600, family=2, type=1, proto=0, laddr=('127.0.0.1', 57257), raddr=('127.0.0.1', 57256)>.
Connected to pydev debugger (build 253.28294.336)
data:   battery_power  blue  clock_speed  ...  touch_screen  wifi  price_range
0            842     0          2.2  ...             0     1            1
1           1021     1          0.5  ...             1     0            2
2            563     1          0.5  ...             1     0            2
3            615     1          2.5  ...             0     0            2
4           1821     1          1.2  ...             1     0            1

[5 rows x 21 columns]
shape:(2000, 21)
input_dim:20
output_dim:4
开始训练模型...
epoch:1, avg_loss:0.9702 , TestAcc:81.5%
epoch:2, avg_loss:0.5667 , TestAcc:83.75%
epoch:3, avg_loss:0.4718 , TestAcc:83.75%
epoch:4, avg_loss:0.4026 , TestAcc:86.25%
epoch:5, avg_loss:0.3480 , TestAcc:88.75%
epoch:6, avg_loss:0.3271 , TestAcc:86.75%
epoch:7, avg_loss:0.3070 , TestAcc:87.25%
epoch:8, avg_loss:0.3098 , TestAcc:88.75%
epoch:9, avg_loss:0.2432 , TestAcc:87.5%
epoch:10, avg_loss:0.2467 , TestAcc:89.25%
epoch:11, avg_loss:0.2253 , TestAcc:90.0%
epoch:12, avg_loss:0.2051 , TestAcc:91.0%
epoch:13, avg_loss:0.1940 , TestAcc:88.5%
epoch:14, avg_loss:0.2014 , TestAcc:88.25%
epoch:15, avg_loss:0.2007 , TestAcc:89.75%
epoch:16, avg_loss:0.1742 , TestAcc:90.0%
epoch:17, avg_loss:0.1852 , TestAcc:88.5%
epoch:18, avg_loss:0.1504 , TestAcc:88.25%
epoch:19, avg_loss:0.1566 , TestAcc:87.5%
epoch:20, avg_loss:0.1332 , TestAcc:89.5%
epoch:21, avg_loss:0.1477 , TestAcc:87.0%
epoch:22, avg_loss:0.1366 , TestAcc:89.5%
epoch:23, avg_loss:0.1180 , TestAcc:88.5%
epoch:24, avg_loss:0.1415 , TestAcc:88.25%
epoch:25, avg_loss:0.1329 , TestAcc:88.75%
epoch:26, avg_loss:0.1203 , TestAcc:89.25%
epoch:27, avg_loss:0.1119 , TestAcc:88.25%
epoch:28, avg_loss:0.1208 , TestAcc:89.25%
epoch:29, avg_loss:0.0995 , TestAcc:88.0%
epoch:30, avg_loss:0.1095 , TestAcc:89.0%
epoch:31, avg_loss:0.1024 , TestAcc:87.75%
epoch:32, avg_loss:0.1086 , TestAcc:89.5%
epoch:33, avg_loss:0.0917 , TestAcc:90.25%
epoch:34, avg_loss:0.0903 , TestAcc:89.25%
epoch:35, avg_loss:0.0790 , TestAcc:89.25%
epoch:36, avg_loss:0.0724 , TestAcc:88.5%
epoch:37, avg_loss:0.0950 , TestAcc:87.0%
epoch:38, avg_loss:0.0662 , TestAcc:89.75%
epoch:39, avg_loss:0.0701 , TestAcc:87.5%
epoch:40, avg_loss:0.0726 , TestAcc:89.25%
epoch:41, avg_loss:0.0816 , TestAcc:88.25%
epoch:42, avg_loss:0.0860 , TestAcc:90.5%
epoch:43, avg_loss:0.0672 , TestAcc:89.0%
epoch:44, avg_loss:0.0868 , TestAcc:90.0%
epoch:45, avg_loss:0.0786 , TestAcc:88.5%
epoch:46, avg_loss:0.0801 , TestAcc:90.25%
epoch:47, avg_loss:0.0645 , TestAcc:89.5%
epoch:48, avg_loss:0.0786 , TestAcc:88.75%
epoch:49, avg_loss:0.0580 , TestAcc:89.5%
epoch:50, avg_loss:0.0719 , TestAcc:90.5%
epoch:51, avg_loss:0.0674 , TestAcc:90.0%
epoch:52, avg_loss:0.0561 , TestAcc:89.75%
epoch:53, avg_loss:0.0798 , TestAcc:88.75%
epoch:54, avg_loss:0.0747 , TestAcc:90.0%
epoch:55, avg_loss:0.0708 , TestAcc:89.25%
epoch:56, avg_loss:0.0428 , TestAcc:87.25%
epoch:57, avg_loss:0.0697 , TestAcc:89.5%
epoch:58, avg_loss:0.0758 , TestAcc:90.0%
epoch:59, avg_loss:0.0379 , TestAcc:88.0%
epoch:60, avg_loss:0.0545 , TestAcc:89.0%
epoch:61, avg_loss:0.0581 , TestAcc:88.5%
epoch:62, avg_loss:0.0583 , TestAcc:88.75%
epoch:63, avg_loss:0.0519 , TestAcc:89.25%
epoch:64, avg_loss:0.0545 , TestAcc:89.0%
epoch:65, avg_loss:0.0853 , TestAcc:88.75%
epoch:66, avg_loss:0.0457 , TestAcc:89.25%
epoch:67, avg_loss:0.0663 , TestAcc:89.5%
epoch:68, avg_loss:0.0628 , TestAcc:88.25%
epoch:69, avg_loss:0.0709 , TestAcc:88.75%
epoch:70, avg_loss:0.0390 , TestAcc:88.25%
epoch:71, avg_loss:0.0450 , TestAcc:88.75%
epoch:72, avg_loss:0.0520 , TestAcc:89.0%
epoch:73, avg_loss:0.0343 , TestAcc:88.0%
epoch:74, avg_loss:0.0538 , TestAcc:88.75%
epoch:75, avg_loss:0.0653 , TestAcc:88.75%
epoch:76, avg_loss:0.0519 , TestAcc:86.75%
epoch:77, avg_loss:0.0438 , TestAcc:89.75%
epoch:78, avg_loss:0.0507 , TestAcc:88.75%
epoch:79, avg_loss:0.0453 , TestAcc:88.75%
epoch:80, avg_loss:0.0577 , TestAcc:89.25%
epoch:81, avg_loss:0.0279 , TestAcc:88.75%
epoch:82, avg_loss:0.0799 , TestAcc:87.25%
epoch:83, avg_loss:0.0479 , TestAcc:87.25%
epoch:84, avg_loss:0.0319 , TestAcc:89.75%
epoch:85, avg_loss:0.0536 , TestAcc:89.0%
epoch:86, avg_loss:0.0394 , TestAcc:90.25%
epoch:87, avg_loss:0.0396 , TestAcc:88.5%
epoch:88, avg_loss:0.0505 , TestAcc:86.75%
epoch:89, avg_loss:0.0479 , TestAcc:87.5%
epoch:90, avg_loss:0.0400 , TestAcc:88.25%
epoch:91, avg_loss:0.0500 , TestAcc:89.25%
epoch:92, avg_loss:0.0581 , TestAcc:90.75%
epoch:93, avg_loss:0.0442 , TestAcc:88.5%
epoch:94, avg_loss:0.0308 , TestAcc:90.0%
epoch:95, avg_loss:0.0431 , TestAcc:90.25%
epoch:96, avg_loss:0.0536 , TestAcc:89.75%
epoch:97, avg_loss:0.0451 , TestAcc:88.0%
epoch:98, avg_loss:0.0639 , TestAcc:89.5%
epoch:99, avg_loss:0.0395 , TestAcc:90.75%
epoch:100, avg_loss:0.0305 , TestAcc:88.5%
模型已保存至 phone_model.pth
真实类别前10条: [1, 2, 2, 2, 1, 1, 3, 0, 0, 0]
预测类别前10条: [3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
准确率 (Accuracy): 0.2500
精确率 (Precision): 0.0625
召回率 (Recall): 0.2500
F1-score: 0.1000

Process finished with exit code 0

扩展知识点

1. 准确率 (Accuracy)

  • 定义:模型预测正确的样本数占总样本数的比例。
  • 计算公式

  • 解释:表示模型整体预测正确的能力。
  • 你的结果: 0.7260,说明模型对全部测试样本中,有72.6%预测是正确的。

2. 精确率 (Precision)

  • 定义:模型预测为某一类别的样本中,真正属于该类别的比例。
  • 计算公式(针对某一类别):

  • 解释:模型预测为正例时,有多少是准确的。
  • 为什么重要:当误报代价大时(例如疾病诊断中的误诊),精确率是关键指标。
  • 你的结果: 0.7391,说明模型预测为某个类别的样本中,约有73.91%是真正属于该类别的。
  • 注意:你用的是多分类“加权平均”(weighted),考虑了各类别样本数量不同的影响。

3. 召回率 (Recall)

  • 定义:真实属于某类别的样本中,被模型正确预测为该类别的比例。
  • 计算公式(针对某一类别):

  • 解释:模型对正例的“覆盖率”,能找到多少真正的正例。
  • 为什么重要:当漏报代价大时(如安全漏洞检测),召回率很关键。
  • 你的结果: 0.7260,说明模型能找回72.6%的真实正例。

4. F1-score

  • 定义:精确率和召回率的调和平均数,是两者的折中指标。
  • 计算公式

  • 解释:综合考虑了精确率和召回率,数值高说明模型整体表现均衡。
  • 你的结果: 0.7276,说明模型在精确率和召回率之间达到了较好的平衡。

总结:

指标

反映的方面

高低代表

适用场景

准确率

整体预测正确率

高 = 好

样本均衡,无偏重类别

精确率

预测为正类的准确性

高 = 误报少

误报代价大时,如垃圾邮件检测

召回率

找出正类样本的能力

高 = 漏报少

漏报代价大时,如疾病筛查

F1-score

精确率和召回率的平衡

高 = 综合性能好

两者同等重要时



4.精确率的简单案例说明

假设你做了一个二分类任务,目标是检测邮件是不是垃圾邮件。

邮件编号

真实标签(垃圾=1,正常=0)

模型预测(垃圾=1,正常=0)

1

1

1

2

0

1

3

1

1

4

0

0

5

1

0


先解释表格:

  • 邮件1,真实是垃圾,模型预测也是垃圾 → 真正例 (TP)
  • 邮件2,真实正常,模型预测成垃圾 → 假正例 (FP)
  • 邮件3,真实垃圾,模型预测垃圾 → 真正例 (TP)
  • 邮件4,真实正常,模型预测正常 → 真负例 (TN)
  • 邮件5,真实垃圾,模型预测正常 → 假负例 (FN)

计算精确率:

  • TP = 2(邮件1和邮件3)
  • FP = 1(邮件2)

代入:


解释:

  • 模型预测“垃圾邮件”的次数是 3 次(邮件1、邮件2、邮件3)
  • 其中,有 2 次预测正确(邮件1和邮件3真的垃圾)
  • 有 1 次预测错误(邮件2其实正常)

所以,精确率约为 66.67%,意味着:

当模型说“这是垃圾邮件”的时候,大约有 66.67% 的概率它说对了。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值