"""
演示
    pytorch 框架 模拟线性回归

案例工作流：
    1.准备数据集
        numpy数组 -> 张量 -> 数据集对象Dataset -> 数据加载器DataLoader（分批次加载）
    2.构建神经网络模型
        nn.Linear()线性回归模型
    3.设置损失函数和优化器
        nn.MSELoss, optim.SGD(w新 = w旧 - 学习率 * 梯度)
    4.模型训练
        1.前向传播
        2.计算损失
        3.梯度清零
        4.反向传播
        5.更新参数:w新 = w旧-学习率*梯度
    5.模型测试

"""

# 导包
import torch
# numpy对象 -> 张量 -> 数据集对象TensorDataset -> 数据加载器DataLoader
from torch.utils.data import TensorDataset  # 构造数据集对象
from torch.utils.data import DataLoader  # 数据加载器，按批次加载数据
from torch import nn  # 提供 MSE损失函数 和 线性层，线性层用来模拟线性回归模型
from torch import optim  # 提供各种优化器，用于更新模型参数，公式为：w ← w - lr * grad
from sklearn.datasets import make_regression  # 创建线性回归的示例数据集
import matplotlib.pyplot as plt

# 设置matplotlib中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

# 1.准备数据集
# 定义函数，创建数据集
def create_dataset():
    # 创建数据集对象
    x, y, coef = make_regression(
        n_samples=100,          # 100条样本
        n_features=1,           # 1个特征
        n_targets=1,           # 1个标签/目标
        noise=10,           # 噪声
        bias=13.9,            # 偏移
        coef=True,          # 是否返回系数
        random_state=5          # 随机状态
    )
    # 封装成 张量
    x = torch.tensor(x, dtype=torch.float32)
    y = torch.tensor(y, dtype=torch.float32)
    # 返回结果
    return x, y, coef

# 2.模型训练
def train(x,y,coef):
    # 1.准备数据集：numpy数组 -> 张量 -> 数据集对象 -> 数据加载器（分批次加载）
    # 1.1 创建数据集对象
    dataset = TensorDataset(x, y)
    # 1.2 创建数据加载器
    # 参数1：数据集对象；参数2：批次大小，一般64,32;参数3：是否打乱数据，训练时打乱，测试时不打乱；
    # 参数4：是否删除最后一个不完整的批次，一般是False
    # 100/16 = 6...4, 6+1
    dataloader = DataLoader(dataset, batch_size=16, shuffle=True, drop_last=False)
    # 2.构建模型：nn.Linear()
    model = nn.Linear(1,1)
    # 3.设置损失函数和优化器
    # 损失函数
    loss_fn = nn.MSELoss()
    # 优化器,执行梯度下降法: w新 = w旧 - 学习率 * 梯度
    # 参数1: 待训练的模型参数；参数2：学习率，常用0.01,0.001
    optimizer = optim.SGD(model.parameters(), lr=0.01)
    # 4.模型训练
    # 定义训练轮次
    epochs = 100
    # 记录 每个轮次的平均损失
    total_losses = []
    # 遍历轮数epochs
    for epoch in range(epochs):
        # 1.初始化 当前轮次的训练总损失
        total_loss = 0.0
        # 2.初始化 当前轮次的总样本数
        total_samples = 0
        # 3.遍历批次，遍历数据加载器，分批训练
        for train_x, train_y in dataloader:
            # print(f"train_x:{train_x.shape}")
            # print(f"train_y:{train_y.shape}")
            # 1.前向传播，计算预测值
            y_pred = model(train_x) # (16,1)
            # print(f"y_pred: {y_pred.shape}")
            # 2.计算损失
            loss = loss_fn(y_pred, train_y.reshape(-1, 1))
            # 3.梯度清零
            optimizer.zero_grad()
            # 4.反向传播
            loss.backward()
            # 5.更新参数:w新 = w旧-学习率*梯度，更新模型的参数
            optimizer.step()
            # 6.统计 训练损失 和 样本数
            # 使用train_x.shape[0]来获取当前批次的样本数量，因为最后一个批次可能不完整
            total_loss += loss.item() * train_x.shape[0]
            total_samples += train_x.shape[0]
            ...
        # 4.计算当前轮次的平均损失
        train_loss = total_loss / total_samples
        # 5.添加当前轮次平均损失到列表中
        total_losses.append(train_loss)
        # 6.打印当前轮次的训练平均损失
        print(f"{epoch+1}/{epochs}: | "
              f"{train_loss:.4f}")
        ...
    # 5.可视化训练过程
    print(f"损失列表：{total_losses}")
    plt.figure(figsize=(15,8))
    # 绘制第一个子图：训练损失曲线图
    plt.subplot(1,2,1)
    plt.title("训练损失曲线")
    x_epoch = range(epochs)
    plt.plot(x_epoch, total_losses, label="训练损失")
    plt.xlabel("epoch")
    plt.ylabel("loss")
    plt.grid()
    plt.legend()
    # plt.show()
    # 绘制第二个子图：真实值和预测值的对比
    plt.subplot(1, 2, 2)
    plt.title("真实值和预测值的对比")
    plt.scatter(x, y, label="真实散点")
    # 计算理论的真实线性回归直线
    y_true = [v*coef.item()+13.9 for v in x]
    plt.plot(x, y_true, label="真实线性回归")
    # 计算模型预测值
    with torch.no_grad():   # 关闭梯度计算
        y_pred = model(x).detach().numpy()
    # 绘制模型预测的线性回归直线
    plt.plot(x, y_pred, label="预测值")
    plt.xlabel("x")
    plt.ylabel("y")
    plt.grid()
    plt.legend()
    plt.show()

# 测试
if __name__ == '__main__':
    # 1.准备数据集
    x, y, coef = create_dataset()
    print(f"x:{x.shape}")
    print(f"y:{y.shape}")
    print(f"coef:{coef}, shape: {coef.shape}")
    # 绘制样本散点图
    plt.scatter(x, y, label="真实样本")
    plt.grid()
    plt.xlabel("x")
    plt.ylabel("y")
    plt.legend()
    plt.show()
    # 2.模型训练
    train(x, y, coef)