ML基础:从线性拟合看样本影响

ML基础:线性模型

Introduction

回想一年前刚开始学ML的时候,打的第一个模型就是线性拟合 \(y=kx+b\)

现在在测试多线程训练,发现一些之前没有想清楚的问题:

  1. 出现了NaN(小样本和不当学习率)
  2. 数据集里个别样本对参数准确度的影响比较大

Main part

先给出代码:

# 定义线性回归模型 (y = kx + b)
class LinearRegression(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(1, 1)  # 输入维度为1,输出维度为1

    def forward(self, x):
        return self.linear(x)

结构

  • nn.Linear(1, 1) 表示输入维度为 1,输出维度为 1
  • 它有两个参数:
    • weight:权重,对应斜率 k,形状为 (1, 1)(因为输入和输出都是 1 维)
    • bias:偏置,对应截距 b,形状为 (1,) (形状为 (1,) 表示一个张量是一维的,且包含 1 个元素)

NaN问题

看面试题的时候经常会问到这个基础问题,刚好在这个简单的情境下可以静下心来好好考虑

首先给出代码:

# 用户提供的数据(一维向量 x 和 y)
x = torch.tensor([0.05 ,1.0, -2.0, 5.0,5.5,9.0,-30.0, 42.0], dtype=torch.float32).reshape(-1, 1)  # 形状: (n_samples, 1)
y = torch.tensor([0.1,2.0, -4.0,10.0,11.0,18.0, -60.0, 84.0], dtype=torch.float32).reshape(-1, 1)  # 形状: (n_samples, 1)

# 创建数据集和数据加载器
dataset = TensorDataset(x, y)
data_loader = DataLoader(dataset, batch_size=2, shuffle=True)

# 定义线性回归模型 (y = kx + b)
class LinearRegression(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(1, 1)  # 输入维度为1,输出维度为1

    def forward(self, x):
        return self.linear(x)

# 训练函数
def train_model(model, data_loader, epochs=100):
    optimizer = torch.optim.SGD(model.parameters(), lr=0.005)
    criterion = nn.MSELoss()

    for epoch in range(epochs):
        for x_batch, y_batch in data_loader:
            optimizer.zero_grad()
            output = model(x_batch)
            loss = criterion(output, y_batch)
            loss.backward()
            optimizer.step()

    # 返回斜率 k 和截距 b
    k = model.linear.weight.detach().numpy()[0][0]  # 权重即斜率 k
    b = model.linear.bias.detach().numpy()[0]      # 偏置即截距 b
    return k, b

# 多线程训练并收集参数
def multithread_training(num_threads=2):
    models = [LinearRegression() for _ in range(num_threads)]

    # 将数据分成多个子集
    data_splits = [DataLoader(dataset, batch_size=2, sampler=torch.utils.data.SubsetRandomSampler(
        range(i * len(dataset) // num_threads, (i + 1) * len(dataset) // num_threads)))
        for i in range(num_threads)]

    # 使用线程池并行训练
    results = []
    with ThreadPoolExecutor(max_workers=num_threads) as executor:
        results = list(executor.map(train_model, models, data_splits))

    # 打印每个模型的拟合参数
    for i, (k, b) in enumerate(results):
        print(f"模型 {i+1} 的拟合直线: y = {k:.4f}x + {b:.4f}")

if __name__ == "__main__":
    multithread_training(num_threads=2)
下面是ds给出的原因

这通常是由于训练过程中梯度不稳定或数值溢出导致的

可能的原因:
  1. 学习率过大
    • 学习率(lr=0.005)可能对某些数据子集来说太大,导致梯度更新时权重值爆炸(特别是模型2的数据子集中可能有较大的输入值或梯度)。
    • 模型1的数据子集可能更“温和”,因此没有出现NaN。
  2. 数据分布不均匀
    • 你的数据 x 的范围差异较大(从 -30.0 到 42.0),而 yx 的线性变换(y = 2x)。
    • 如果模型2的数据子集中包含极端值(如 x = -30.0x = 42.0),梯度可能会变得非常大,导致权重更新时数值溢出。
    • 模型1的数据子集可能没有包含这些极端值。
  3. 随机数据划分
    • 你使用了 SubsetRandomSampler 随机划分数据,模型2可能不幸地分到了某些极端值组合,导致梯度爆炸。
    • 模型1的数据子集可能更“温和”。
  4. 未归一化数据
    • 输入数据 x 的范围差异较大(-30.0 到 42.0),而未进行归一化(如缩放到 [-1, 1] 或 [0, 1]),这可能导致梯度不稳定。
解决方法:
  1. 降低学习率

    • 尝试将学习率调小(如 lr=0.001 或更小),观察是否还会出现NaN。
  2. 数据归一化

    • 对输入数据 x 进行归一化(如缩放到 [-1, 1] 或 [0, 1]):

      x = (x - x.mean()) / x.std()  # 标准化
      # 或
      x = (x - x.min()) / (x.max() - x.min())  # 缩放到 [0, 1]
      
    • 这样可以让梯度更稳定。

  3. 梯度裁剪

    • 在训练时限制梯度的大小,防止梯度爆炸:

      torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
      
  4. 检查数据划分

    • 打印模型1和模型2的数据子集,看看是否有极端值:

      for i, split in enumerate(data_splits):
          print(f"模型 {i+1} 的数据子集:")
          for x_batch, y_batch in split:
              print(x_batch, y_batch)
      
  5. 初始化权重

    • 检查模型的权重初始化是否合理(nn.Linear 默认使用均匀初始化,通常没问题)。
修改后的代码示例:
# 数据归一化(标准化)
x_mean, x_std = x.mean(), x.std()
x_normalized = (x - x_mean) / x_std

# 更新数据集
dataset = TensorDataset(x_normalized, y)

# 训练时添加梯度裁剪
def train_model(model, data_loader, epochs=100):
    optimizer = torch.optim.SGD(model.parameters(), lr=0.001)  # 降低学习率
    criterion = nn.MSELoss()

    for epoch in range(epochs):
        for x_batch, y_batch in data_loader:
            optimizer.zero_grad()
            output = model(x_batch)
            loss = criterion(output, y_batch)
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)  # 梯度裁剪
            optimizer.step()

    k = model.linear.weight.detach().numpy()[0][0]
    b = model.linear.bias.detach().numpy()[0]
    return k, b

现在直观理解了为什么会用到那么多技巧

参数准确度的影响比较大的样本点

困扰我一年的问题:

x = torch.tensor([1.0, 2.0, 5.0 ,4.0], dtype=torch.float32).reshape(-1, 1)
y = torch.tensor([2.0, 4.0,10.0 ,8.0], dtype=torch.float32).reshape(-1, 1)
x = torch.tensor([-1.0, 2.0, 5.0,4.0], dtype=torch.float32).reshape(-1, 1)
y = torch.tensor([-2.0, 4.0,10.0,8.0], dtype=torch.float32).reshape(-1, 1)

为什么有一个在y轴左侧的数据会让拟合结果更接近 \(y=2x\)

杠杆效应

指在线性回归中,某些数据点(尤其是远离 x 均值的点)对拟合直线的斜率和截距有更大的影响。

这些点被称为高杠杆点,因为它们对回归结果的“拉动”作用更强。

杠杆效应的深层原理根植于线性回归的数学机制,特别是最小二乘法优化过程中数据点对拟合参数(斜率和截距)的影响

杠杆效应定义:

杠杆效应源于数据点对拟合参数的影响力差异,量化通过帽子矩阵(Hat Matrix)的定义

\(H=X(X^TX)^{−1}X^T\)

\(\hat{Y} = HY\)

帽子矩阵的的对角元素 \(h_{ii}\) 称为第 i 个数据点的杠杆值

\(h_{ii}=\frac{1}{n}+\frac{(x_{i}-\bar{x})^2}{\sum_{j=1}^{n}(x_{j}-\bar{x})^2}\)

\(x_{i}\)离均值越远,\(h_{ii}\)就越大,表明该点具有高杠杆效应。

posted @ 2025-07-12 18:42  JnZhanG  阅读(16)  评论(0)    收藏  举报