2025-04-18 李沐深度学习2 —— 数据操作&预处理

发布于:2025-04-21 ⋅ 阅读:(22) ⋅ 点赞:(0)

硬件配置:

  • Windows 11
  • Intel®Core™i7-12700H
  • NVIDIA GeForce RTX 3070 Ti Laptop GPU

软件环境:

  • Pycharm Professional 2025.1
  • Python 3.12.9
  • Pytorch 2.6.0+cu124

1 N维数组

1.1 核心数据结构

  1. 高维数组

    • 标量(0D):单个数值(如1.0),表示类别或阈值。
    • 向量(1D):一列数值(如[1.2, 3.4, 5.6]),表示一个样本的特征。
    • 矩阵(2D):行×列的表格(如3×4矩阵),表示多个样本的特征集合(每行一个样本)。
    image-20250417145830808
    • 3D 数组:如 RGB 图片(宽×高×3通道)。
    • 4D 数组:批量图片(批量大小×宽×高×通道),如 128 张图片组成一个 batch。
    • 5D 数组:视频数据(批量×时间帧×宽×高×通道),本课程较少涉及。
    image-20250417145848430
  2. 为什么用 N 维数组?

    • 统一性:所有数据(图像、文本、音频)均可转换为 N 维数组。
    • 高效计算:GPU/TPU 针对矩阵运算优化,加速训练。

1.2 创建数组的三大要素

image-20250417150011260
  1. 形状(Shape)
    • 指定维度大小,如(3,4)表示 3 行 4 列的矩阵。
  2. 数据类型(dtype)
    • 常用float32(节省内存)或int64(类别标签)。
  3. 初始化方式
    • 全零:torch.zeros(3,4)
    • 全一:torch.ones(3,4)
    • 随机数:
      • 正态分布:torch.randn(3,4)(均值 0,方差 1)
      • 均匀分布:torch.rand(3,4)(范围 [0, 1))

1.3 数组操作

访问元素

  • 单元素:x[1,2] → 第 2 行第 3 列(下标从 0 开始)。
  • 整行/整列:
    • 第 2 行:x[1, :]
    • 第 1 列:x[:, 0]
  • 子区域切片:
    • 行 1~2,所有列:x[1:3, :](左闭右开,实际取第 1、2 行)。
  • 跳步访问:
    • 每隔 2 行、每隔 1 列:x[::2, ::1]
image-20250417150207511

2 实战:数据操作

本章节参考链接:2.1. 数据操作 — 动手学深度学习 2.0.0 documentation

​ 张量是多维数组的泛化形式,是 PyTorch 和深度学习中的核心数据结构。

​ 标量(0D)、向量(1D)、矩阵(2D)、3D及以上张量分别对应不同维度的数据。

PyTorch 张量 vs NumPy 数组

  • 优势:
    • GPU 加速计算
    • 支持自动微分(Autograd)
  • 相似性:操作接口与 NumPy 高度一致。

2.1 创建张量

  1. 首先导入torch。请注意,虽然它被称为 PyTorch,但是代码中使用torch而不是pytorch

    import torch
    

    ​ 张量表示一个由数值组成的数组,这个数组可能有多个维度。 具有一个轴的张量对应数学上的向量(vector); 具有两个轴的张量对应数学上的矩阵(matrix); 具有两个轴以上的张量没有特殊的数学名称。

  2. 使用 arange 创建一个行向量 x。这个行向量包含以 0 开始的前 12 个整数,它们默认创建为整数。

    x = torch.arange(12) # 生成0-11的整数向量
    x
    
    image-20250417153330606
  3. 通过张量的shape属性来访问张量(沿每个轴的长度)的形状 。

    x.shape
    
    image-20250417153502875
  4. 如果只想知道张量中元素的总数,即形状的所有元素乘积,可以检查它的大小(size)。

    因为这里在处理的是一个向量,所以它的shape与它的size相同。

    x.numel()
    
    image-20250417153607089
  5. 要想改变一个张量的形状而不改变元素数量和元素值,可以调用reshape函数。

    例如,可以把张量x从形状为(12,)的行向量转换为形状为(3,4)的矩阵。 这个新的张量包含与转换前相同的值,但是它被看成一个 3 行 4 列的矩阵。

    注意,虽然张量的形状发生了改变,但其元素值并没有变。通过改变张量的形状,张量的大小不会改变。

    X = x.reshape(3, 4) # 转换为3x4矩阵
    X
    
    image-20250417154141388

    ​ 如果我们的目标形状是(高度,宽度),在知道宽度后,高度会被自动计算得出,不必我们自己做除法。

    ​ 在上面的例子中,为了获得一个3行的矩阵,我们手动指定了它有 3 行和 4 列。我们还可以通过-1来调用此自动计算出维度的功能。 即我们可以用x.reshape(-1,4)x.reshape(3,-1)来取代x.reshape(3,4)

  6. 使用全0、全1、其他常量,或者从特定分布中随机采样的数字来初始化矩阵。 我们可以创建一个形状为(2,3,4)的张量,其中所有元素都设置为0。代码如下:

    torch.zeros((2,3,4))   # 全0张量
    torch.ones((2,3,4))    # 全1张量
    torch.randn(3,4)       # 正态分布随机数,均值为0、标准差为1
    torch.tensor([[2,1,4,3], [1,2,3,4]])  # 自定义数据
    
    image-20250417154233749

2.2 运算

  1. 对于任意具有相同形状的张量, 常见的标准算术运算符(+-*/**)都可以被升级为按元素运算。

    在下面的例子中,我们使用逗号来表示一个具有 5 个元素的元组,其中每个元素都是按元素操作的结果。

    x = torch.tensor([1.0, 2, 4, 8])
    y = torch.tensor([2, 2, 2, 2])
    x + y, x - y, x * y, x / y, x ** y  # **运算符是求幂运算
    
    image-20250417154444399

    “按元素”方式可以应用更多的计算,包括像求幂这样的一元运算符。

    torch.exp(x) # 指数运算
    
    image-20250417154705672
  2. 我们也可以把多个张量连结(concatenate)在一起, 把它们端对端地叠起来形成一个更大的张量。只需要提供张量列表,并给出沿哪个轴连结。

    下面的例子分别演示了当我们沿行(轴-0,形状的第一个元素) 和按列(轴-1,形状的第二个元素)连结两个矩阵时,会发生什么情况。

    X = torch.arange(12, dtype=torch.float32).reshape((3,4))
    Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
    torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
    
    image-20250417154809798
  3. X == Y为例: 对于每个位置,如果XY在该位置相等,则新张量中相应项的值为1。 这意味着逻辑语句X == Y在该位置处为真,否则该位置为0。

    X == Y
    
    image-20250417154945630
  4. 对张量中的所有元素进行求和,会产生一个单元素张量。

    X.sum()
    
    image-20250417155023738

2.3 广播机制

​ 在某些情况下,即使形状不同,我们仍然可以通过调用广播机制(broadcasting mechanism)来执行按元素操作。 这种机制的工作方式如下:

  • 通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状;
  • 对生成的数组执行按元素操作。
  1. 在大多数情况下,我们将沿着数组中长度为 1 的轴进行广播,如下例子:

    a = torch.arange(3).reshape((3, 1))
    b = torch.arange(2).reshape((1, 2))
    a, b
    
    image-20250417155219904
  2. 由于ab分别是 3×1 和 1×2 矩阵,如果让它们相加,它们的形状不匹配。此时两个矩阵会广播为一个更大的 3×2 矩阵,如下所示。矩阵a将复制列, 矩阵b将复制行,然后再按元素相加。

    a + b
    
    image-20250417155345131

    ​ 广播可能掩盖形状错误,需通过shape属性验证结果维度。

2.4 索引和切片

​ 就像在任何其他 Python 数组中一样,张量中的元素可以通过索引访问。 与任何 Python 数组一样:第一个元素的索引是 0,最后一个元素索引是 -1; 可以指定范围以包含第一个元素和最后一个之前的元素。

  1. 如下所示,用[-1]选择最后一个元素,用[1:3]选择第二个和第三个元素:

    X[-1], X[1:3]
    
    image-20250417155456103
  2. 除读取外,我们还可以通过指定索引来将元素写入矩阵。

    X[1, 2] = 9
    X
    
    image-20250417155542100
  3. 如果我们想为多个元素赋值相同的值,我们只需要索引所有元素,然后为它们赋值。

    X[0:2, :] = 12 # 批量赋值
    X
    
    image-20250417155738166

2.5 节省内存

​ 运行一些操作可能会导致为新结果分配内存。例如,如果我们用Y = X + Y,我们将取消引用Y指向的张量,而是指向新分配的内存处的张量。这可能是不可取的,原因有两个:

  • 首先,我们不想总是不必要地分配内存。在机器学习中,我们可能有数百兆的参数,并且在一秒内多次更新所有参数。通常情况下,我们希望原地执行这些更新;
  • 如果我们不原地更新,其他引用仍然会指向旧的内存位置,这样我们的某些代码可能会无意中引用旧的参数。

相关参考链接:2025-03-15 Python&深度学习2——Numpy库_python3.12.7版本对应numpy-CSDN博客(3.4 节)。

  1. 在下面的例子中,我们用 Python 的id()函数演示了这一点,它给我们提供了内存中引用对象的确切地址。 运行Y = Y + X后,我们会发现id(Y)指向另一个位置。 这是因为 Python 首先计算Y + X,为结果分配新的内存,然后使Y指向内存中的这个新位置。

    before = id(Y)
    Y = Y + X
    id(Y) == before
    
    image-20250417155859847
  2. 使用切片表示法将操作的结果分配给先前分配的数组,例如Y[:] = <expression>

    首先创建一个新的矩阵Z,其形状与另一个Y相同, 使用zeros_like来分配一个全0的块。

    Z = torch.zeros_like(Y)
    print('id(Z):', id(Z))
    Z[:] = X + Y
    print('id(Z):', id(Z))
    
    image-20250417160056193
  3. 如果在后续计算中没有重复使用X, 我们也可以使用X[:] = X + YX += Y来减少操作的内存开销。

    before = id(X)
    X += Y
    id(X) == before
    
    image-20250417160121472

2.6 与NumPy转换

​ 将深度学习框架定义的张量转换为 NumPy 张量(ndarray)很容易,反之也同样容易。

  1. torch 张量和 numpy 数组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。

    A = X.numpy()
    B = torch.tensor(A)
    type(A), type(B)
    
    image-20250417160413302
  2. 要将大小为 1 的张量转换为 Python 标量,我们可以调用item函数或 Python 的内置函数。

    a = torch.tensor([3.5])
    a, a.item(), float(a), int(a)
    
    image-20250417160500581

2.7 练习

  1. 运行本节中的代码。将本节中的条件语句X == Y更改为X < YX > Y,然后看看你可以得到什么样的张量。

    答:同样为布尔张量,每个元素值都是 XY 中对应元素的比较。

    X < Y
    
    image-20250418051753421
  2. 用其他形状(例如三维张量)替换广播机制中按元素操作的两个张量。结果是否与预期相同?

    相同,但这仅适用于一维行向量和一维列向量之间的加减。

    a = torch.arange(3).reshape((3, 1))
    b = torch.arange(3).reshape((1, 3))
    a, b
    
    image-20250418052312062
    a + b
    
    image-20250418052335747

3 实战:数据预处理

本章节参考链接:2.2. 数据预处理 — 动手学深度学习 2.0.0 documentation

​ 为了能用深度学习来解决现实世界的问题,我们常从预处理原始数据开始, 而不是从那些准备好的张量格式数据开始,通常使用pandas软件包。

3.1 读取数据集

​ 首先创建一个人工数据集,并存储在 CSV(逗号分隔值)文件 ../data/house_tiny.csv中。 以其他格式存储的数据也可以通过类似的方式进行处理。

  1. 将数据集按行写入 CSV 文件中。

    import os
    
    os.makedirs(os.path.join('..', 'data'), exist_ok=True)
    data_file = os.path.join('..', 'data', 'house_tiny.csv')
    with open(data_file, 'w') as f:
        f.write('NumRooms,Alley,Price\n')  # 列名
        f.write('NA,Pave,127500\n')  # 每行表示一个数据样本
        f.write('2,NA,106000\n')
        f.write('4,NA,178100\n')
        f.write('NA,NA,140000\n')
    
  2. 导入pandas包并调用read_csv函数。该数据集有四行三列。其中每行描述了房间数量(“NumRooms”)、巷子类型(“Alley”)和房屋价格(“Price”)。

    # 如果没有安装pandas,只需取消对以下行的注释来安装pandas
    # !pip install pandas
    import pandas as pd
    
    data = pd.read_csv(data_file)
    print(data)
    
    image-20250418053023628

3.2 处理缺失值

​ “NaN”项代表缺失值。 为了处理缺失的数据,典型的方法包括插值法删除法, 其中插值法用一个替代值弥补缺失值,而删除法则直接忽略缺失值。 在这里,我们将考虑插值法。

  1. 通过位置索引iloc,将data分成inputsoutputs, 其中前者为data的前两列,而后者为data的最后一列。 对于inputs中缺少的数值,我们用同一列的均值替换“NaN”项。

    inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
    # inputs = inputs.fillna(inputs.mean())
    
    # 数值列用均值填充
    inputs['NumRooms'] = inputs['NumRooms'].fillna(inputs['NumRooms'].mean())
    
    inputs
    
    image-20250418053323066
  2. 对于inputs中的类别值或离散值,我们将“NaN”视为一个类别。 由于“巷子类型”(“Alley”)列只接受两种类型的类别值“Pave”和“NaN”, pandas可以自动将此列转换为两列“Alley_Pave”和“Alley_nan”。

    巷子类型为“Pave”的行会将“Alley_Pave”的值设置为1,“Alley_nan”的值设置为0。 缺少巷子类型的行会将“Alley_Pave”和“Alley_nan”分别设置为0和1。

    inputs = pd.get_dummies(inputs, dummy_na=True)
    print(inputs)
    
    image-20250418053434813

3.3 转换为张量

​ 现在inputsoutputs中的所有条目都是数值类型,它们可以转换为张量格式。

  1. 当数据采用张量格式后,可以通过在 [2.1节](##2.1 创建张量)中引入的那些张量函数来进一步操作。

    import torch
    
    X = torch.tensor(inputs.to_numpy(dtype=float))
    y = torch.tensor(outputs.to_numpy(dtype=float))
    X, y
    
    image-20250418053632562

3.4 练习

  1. 删除缺失值最多的列

    # 计算每列的缺失值数量
    missing_counts = data.isnull().sum()
    print("每列的缺失值数量:\n", missing_counts)
    
    # 找出缺失值最多的列名
    column_to_drop = missing_counts.idxmax()
    print("\n缺失值最多的列:", column_to_drop)
    
    # 删除该列
    data_dropped = data.drop(column_to_drop, axis=1)
    print("\n删除后的数据:\n", data_dropped)
    
    image-20250418054033144
  2. 将预处理后的数据集转换为张量格式。

    # 1. 删除缺失值最多的列(前面已实现)
    data_dropped = data.drop(missing_counts.idxmax(), axis=1)
    
    # 2. 处理剩余缺失值(用均值填充数值列)
    data_dropped = data_dropped.fillna(data_dropped.mean())
    
    # 3. 转换为张量
    tensor_data = torch.tensor(data_dropped.values, dtype=torch.float32)
    print("\n张量格式的数据:\n", tensor_data)
    
    image-20250418054510068

网站公告

今日签到

点亮在社区的每一天
去签到