欢迎光临
我们一直在努力

深度学习:PyTorch 零基础入门(二)

PyTorch 零基础入门(二):张量内存与类型转换 + 真实数据预处理

本文是《动手学深度学习》(Dive into Deep Learning) PyTorch 版的实战笔记第二篇。 上一篇我们搞定了张量的创建、形状、拼接、广播,这一篇解决两个更"接地气"的问题: ① 张量运算到底会不会新开辟内存?Tensor / ndarray / Python 标量之间怎么互转? ② 拿到一份真实 CSV(有缺失值、有字符串列)时,怎么变成能喂给神经网络的张量?

全文代码都已在 PyTorch 2.14.1 + pandas 2.3.3 + Python 3.10 上实际跑通,输出结果是真实粘贴的,可以放心复现。

📚 本系列其他文章

  • ndarray 和 tensor 到底有啥区别?一张表讲清楚
  • 深度学习基础知识
  • 机器学习基础知识1-KNN基础
  • 机器学习-KNN算法基础2-分类与回归思想

目录

  • 一、环境准备
  • 二、第一部分:张量的内存与类型转换
    • 2.1 普通运算会新分配内存
    • 2.2 原地操作:省内存的关键
    • 2.3 NumPy 与 Tensor 互转(坑最多的地方)
    • 2.4 Tensor 元素转 Python 标量
  • 三、第二部分:数据预处理
    • 3.1 创建并读取数据集
    • 3.2 处理缺失数据:插值 or 删除
    • 3.3 类别特征独热编码
    • 3.4 转换为张量格式
  • 四、整合后的完整代码
  • 五、运行结果(真实输出)
  • 六、避坑指南:我踩过的 5 个坑
  • 七、知识点速查表
  • 八、小结

一、环境准备

pip install torch pandas numpy

本文环境:

组件版本
Python 3.10.22
PyTorch 2.14.1+cpu
pandas 2.3.3
numpy 2.2.6

Windows 用户注意:如果控制台打印中文出现乱码,在代码开头加上

import sys
sys.stdout.reconfigure(encoding='utf-8')

或者运行时设置环境变量 PYTHONIOENCODING=utf-8。


二、第一部分:张量的内存与类型转换

2.1 普通运算会新分配内存

这是很多人初学时的第一个"反直觉"点:Python 里 Y = Y + X 并不是把结果写回 Y 原来的那块内存,而是新建了一个张量,再让 Y 指向它。

import torch

X = torch.tensor([1, 2, 3, 4, 5])
Y = torch.tensor([1, 2, 3, 4, 5])

before = id(Y)
print(f'运算前 Y 的 id:{before}')
print('运算前 Y 的值:', Y)

Y = Y + X # 等价于 Y = torch.add(Y, X),开辟新内存

print('运算后 Y 的值:', Y)
print(f'运算后 Y 的 id:{id(Y)}')
print('两次 id 是否相同:', id(Y) == before)

实际输出:

运算前 Y 的 id:2164783116224
运算前 Y 的值: tensor([1, 2, 3, 4, 5])
运算后 Y 的值: tensor([ 2, 4, 6, 8, 10])
运算后 Y 的 id:2164783116304
两次 id 是否相同: False

结论:id 变了 → 说明 Y + X 申请了一块新内存来存放结果。 在深度学习里,模型参数动辄几亿个,如果每次更新都新开一块内存,显存会被迅速吃光。所以我们需要原地操作。

小提示:id() 返回的是对象在内存中的地址标识。注意 CPython 可能复用刚被回收的内存地址,所以不要用"两个 id 不同就一定是新内存"做严格判断——更可靠的方式是看 data_ptr():

Y = Y + X
print(Y.data_ptr()) # 地址变了

用 id() 做教学演示直观,但工程里更推荐 data_ptr()。


2.2 原地操作:省内存的关键

PyTorch 提供了两类原地写法,都不会重新分配内存:

① 切片赋值 Z[:] = …

# torch.zeros_like(Y):生成一个形状、dtype、device 都和 Y 完全一致,但元素全为 0 的新张量
# 注意:这不是 NumPy 的函数,是 torch 的(np.zeros_like 才是 NumPy 版)
Z = torch.zeros_like(Y)
print('初始化后 id(Z):', id(Z))

Z[:] = X + Y # 把结果写回 Z 原有的内存
print('切片赋值后 id(Z):', id(Z))

② 带下划线的方法 / 复合赋值运算符

PyTorch 里所有以 _ 结尾的方法都是原地操作,例如 add_()、mul_()、zero_()、normal_():

Y_before = id(Y)
Y += X # 等价于 Y.add_(X),即原地加法
print('+= 之后 Y 的值:', Y)
print('+= 前后 id 是否相同:', id(Y) == Y_before)

实际输出:

— 1.2 原地操作不会新分配内存 —
初始化后 id(Z): 2164783116224
切片赋值后 id(Z): 2164783116224
+= 之后 Y 的值: tensor([ 3, 6, 9, 12, 15])
+= 前后 id 是否相同: True

结论:Z 在切片赋值前后 id 完全一致,Y += X 前后 id 也一致 → 内存被复用了。

⚠️ 重要提醒:原地操作虽然省内存,但在需要自动求导的场景下要小心。如果某个张量是计算图上的中间变量,对它原地修改可能破坏反向传播所需的数值,PyTorch 会直接报错: RuntimeError: a leaf Variable that requires grad is being used in an in-place operation. 经验法则:更新参数时用 with torch.no_grad(): 包起来,或者用优化器 optimizer.step(),不要手写原地操作去改 requires_grad=True 的叶子张量。


2.3 NumPy 与 Tensor 互转(坑最多的地方)

📖 关于 ndarray 和 tensor 的本质区别(自动求导、CPU/GPU、适用场景), 我在这篇里已经详细讲过:ndarray 和 tensor 到底有啥区别?一张表讲清楚。 那篇讲的是"是什么、为什么",本节只补那篇没展开的"怎么用、哪里会炸"——转换方向与内存语义的实战细节。

PyTorch 和 NumPy 可以无缝互相转换,但方向不同,行为完全不同——这是最容易埋 bug 的地方。

A = X.numpy() # Tensor -> ndarray:共享内存!
B = torch.tensor(A) # ndarray -> Tensor:会拷贝一份数据

print('A 的类型:', type(A))
print('B 的类型:', type(B))

# 验证:改 A 会连带改 X
A[0] = 100
print('修改 A[0]=100 后,X =', X, ' B =', B, '(B 是拷贝,不受影响)')

# 零拷贝版转换
C = torch.from_numpy(A)
print('torch.from_numpy(A) 与 X 共享内存:', C.data_ptr() == X.data_ptr())

实际输出:

A 的类型: <class 'numpy.ndarray'>
B 的类型: <class 'torch.Tensor'>
A 的值: [1 2 3 4 5]
B 的值: tensor([1, 2, 3, 4, 5])
修改 A[0]=100 后,X = tensor([100, 2, 3, 4, 5]) B = tensor([1, 2, 3, 4, 5]) (B 是拷贝,不受影响)
torch.from_numpy(A) 与 X 共享内存: True

共享内存的"双向性"是这个例子最值得记住的地方: A = X.numpy() 之后,A 和 X 指向的是同一块物理内存。我明明只是想转个格式, 结果一句 A[0] = 100 就把原始张量 X 也改了——而 B(用 torch.tensor() 拷贝出来的)安然无恙。 这种"隔山打牛"式的 bug 极难排查,因为出问题的地方和写错的地方往往隔着几百行。

不同转换方式的内存语义对比(建议收藏这一张):

转换方式方向是否共享内存改一边另一边会变吗
tensor.numpy() Tensor → ndarray ✅ 共享 会变
torch.from_numpy(ndarray) ndarray → Tensor ✅ 共享 会变
torch.tensor(ndarray) ndarray → Tensor ❌ 拷贝 不会变
torch.as_tensor(ndarray) ndarray → Tensor ✅ 共享(能共享就共享) 会变

工程建议:

# 想安全:用 torch.tensor(),明确拷贝,互不干扰
B = torch.tensor(A)

# 想省内存(大数组、只读场景):用 torch.from_numpy(),但绝不要再去写 A
C = torch.from_numpy(A)

# 只是想临时算一下、又怕污染原数据:显式 copy
D = X.numpy().copy()

踩坑提醒:X.numpy() 要求张量在 CPU 上。如果张量在 GPU 上,必须先搬回主机内存:

A = X.cpu().numpy() # 普通张量
A = X.cpu().detach().numpy() # 带梯度的张量,还要 detach()

否则会报 TypeError: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu()…


2.4 Tensor 元素转 Python 标量

有时候我们需要把单元素张量取出来当普通 Python 数字用(比如打印 loss、做条件判断、传给 matplotlib)。

a = torch.tensor([3.5]) # 浮点张量
b = torch.tensor([3]) # 整型张量

print('a =', a, ' a.item() =', a.item(), type(a.item()))
print('b =', b, ' b.item() =', b.item(), type(b.item()))
print('float(a) =', float(a), type(float(a)))
print('int(a) =', int(a), type(int(a)))

实际输出:

a = tensor([3.5000]) a.item() = 3.5 <class 'float'>
b = tensor([3]) b.item() = 3 <class 'int'>
float(a) = 3.5 <class 'float'>
int(a) = 3 <class 'int'>

三个方式的区别:

写法返回类型说明
a.item() 跟随张量 dtype 浮点张量给 float,整型张量给 int
float(a) 永远是 float 强制转浮点
int(a) 永远是 int 直接截断取整,不是四舍五入!

⚠️ int(torch.tensor([3.9])) 的结果是 3,不是 4。需要四舍五入请用 round()。 另外 .item() 只对单元素张量有效,多元素张量调用会报错: RuntimeError: a Tensor with 5 elements cannot be converted to Scalar


三、第二部分:数据预处理

真实世界的数据几乎从来不是"干净的张量":有缺失值、有字符串列、有各种脏格式。 这一节我们用最经典的 house_tiny.csv(4 行房价数据)走完完整流程。

3.1 创建并读取数据集

import os
import pandas as pd
import torch

# 创建 ../data 目录(exist_ok=True 表示目录已存在也不报错)
os.makedirs(os.path.join('..', 'data'), exist_ok=True)
data_file = os.path.join('..', 'data', 'house_tiny.csv')
print('数据文件路径:', os.path.abspath(data_file))

with open(data_file, 'w', encoding='utf-8') 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')

data = pd.read_csv(data_file)
print('原始数据 data:')
print(data)

print('\\n缺失值统计:')
print(data.isna().sum())

实际输出:

数据文件路径: C:\\Users\\16108\\Desktop\\Python_project\\DL_project\\DL_project\\data\\house_tiny.csv
原始数据 data:
NumRooms Alley Price
0 NaN Pave 127500
1 2.0 NaN 106000
2 4.0 NaN 178100
3 NaN NaN 140000

缺失值统计:
NumRooms 2
Alley 3
Price 0
dtype: int64

两个要点:

  • read_csv 会自动把字符串 NA 识别为缺失值 NaN(还有 N/A、null、空字符串等,由 na_values 参数控制)。
  • data.isna().sum() 是查看缺失值分布的第一件事,务必养成习惯。
  • NumRooms 是数值特征,Alley 是类别特征(只有 Pave / NaN),Price 是我们要预测的标签。

    3.2 处理缺失数据:插值 or 删除

    先按列切分特征和标签:

    inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]

    iloc[:, 0:2] 取所有行的第 0、1 列(NumRooms、Alley)作为特征,iloc[:, 2] 取 Price 作为标签。

    实际输出:

    inputs(特征):
    NumRooms Alley
    0 NaN Pave
    1 2.0 NaN
    2 4.0 NaN
    3 NaN NaN

    outputs(标签):
    0 127500
    1 106000
    2 178100
    3 140000
    Name: Price, dtype: int64

    处理缺失值有两大流派:

    方案 A:插值填充(用均值)

    inputs_mean = inputs.fillna(inputs.mean(numeric_only=True))
    print(inputs_mean)

    方案A:均值填充后的 inputs_mean:
    NumRooms Alley
    0 3.0 Pave
    1 2.0 NaN
    2 4.0 NaN
    3 3.0 NaN

    NumRooms 的均值是 (2+4)/2 = 3.0,两个 NaN 被填成了 3.0。 但注意:Alley 列的 NaN 一个都没被填上,因为它是字符串列,mean() 对非数值列无能为力。

    为什么加 numeric_only=True? 在 pandas 2.x 里,对含字符串的列直接调 .mean() 会抛 TypeError。 老教程里的 inputs.mean() 在 pandas 1.x 上"能跑"(默默忽略非数值列),升级后就崩了。 加 numeric_only=True 是跨版本最稳的写法。

    方案 B(本例采用):独热编码 + 再填充

    inputs = pd.get_dummies(inputs, dummy_na=True, dtype=int)
    inputs = inputs.fillna(inputs.mean(numeric_only=True))
    print(inputs)
    print('此时还有缺失值吗:', inputs.isna().any().any())

    方案B:get_dummies 独热编码 + 均值填充后的 inputs:
    NumRooms Alley_Pave Alley_nan
    0 3.0 1 0
    1 2.0 0 1
    2 4.0 0 1
    3 3.0 0 1
    此时还有缺失值吗: False

    到这里,所有列都是数值了,且没有 NaN,可以安全转张量。

    3.3 类别特征独热编码

    上面用到的 pd.get_dummies() 就是独热编码(one-hot encoding),专门处理类别/离散特征。

    核心参数:

    参数作用
    dummy_na=True 把 NaN 也当成一个独立类别(生成 Alley_nan 列)
    dtype=int 生成 0/1 整数列,而不是 True/False 布尔列(后者转张量会变成 uint8,容易踩坑)

    编码前后对比:

    原 Alley 值Alley_PaveAlley_nan
    Pave 1 0
    NaN 0 1

    为什么用独热编码而不是直接编号(Pave=1, NaN=2)? 因为编号会引入虚假的大小关系——模型会以为 NaN > Pave,但类别之间根本没有序关系。独热编码让每一类独立占一列,杜绝了这种误导。

    ⚠️ 顺序非常关键(我在整合时踩的坑)

    原始代码是:

    inputs = inputs.fillna(inputs.mean()) # 先填充,被注释掉了
    inputs = pd.get_dummies(inputs, dummy_na=True, dtype=int)

    因为 fillna 那行被注释掉了,NumRooms 的 NaN 一直留到最后,转张量后直接变成 nan:

    x = tensor([[nan, 1., 0.], [2., 0., 1.], …])

    张量里带 nan 是灾难性的——送进网络算一次,整个 loss 就是 nan,梯度全废,而且报错信息往往不指向真正的原因。

    正确顺序:先独热编码(把字符串列变成 0/1 数值列),再做均值填充。 这样填充阶段面对的是一个纯数值表,一列都不会漏。

    3.4 转换为张量格式

    ⚠️ 转张量之前,务必先断言没有残留的 NaN——这是全文最重要的一道防线:

    # 转张量前先断言:一个 NaN 都不能有,否则后面 loss 直接变 nan 且很难排查
    assert not inputs.isna().any().any(), '还存在缺失值,请先处理再转张量'

    x, y = torch.tensor(inputs.values), torch.tensor(outputs.values)
    print('x =', x, ' x.shape =', x.shape, ' x.dtype =', x.dtype)
    print('y =', y, ' y.shape =', y.shape, ' y.dtype =', y.dtype)

    # 转成 float32,方便直接送进神经网络
    x_float, y_float = x.to(torch.float32), y.to(torch.float32)
    print('x_float.dtype =', x_float.dtype, ' y_float.dtype =', y_float.dtype)

    实际输出:

    x = tensor([[3., 1., 0.],
    [2., 0., 1.],
    [4., 0., 1.],
    [3., 0., 1.]], dtype=torch.float64) x.shape = torch.Size([4, 3]) x.dtype = torch.float64
    y = tensor([127500, 106000, 178100, 140000]) y.shape = torch.Size([4]) y.dtype = torch.int64
    x_float.dtype = torch.float32 y_float.dtype = torch.float32

    关键点解读:

    • inputs.values 把 DataFrame 转成 NumPy 数组(只取值,丢掉列名和索引)。这一步很重要——直接 torch.tensor(dataframe) 在新版 PyTorch 里会报错或行为不符合预期。
    • x.shape = [4, 3]:4 个样本,3 个特征(NumRooms、Alley_Pave、Alley_nan)。
    • x.dtype = torch.float64:因为 NumRooms 里有小数,pandas 推断成了 float64。
    • 务必手动转成 float32!float64 精度虽高但显存占用翻倍、GPU 计算也慢,而且 PyTorch 默认权重是 float32,类型不匹配会报错: RuntimeError: expected scalar type Float but found Double

    最后做个小小的前向计算,验证数据真的能用:

    n_features = x_float.shape[1]
    w = torch.arange(1, n_features + 1, dtype=torch.float32) # 随便给一组权重,演示形状要对上
    b = torch.tensor(0.0)
    sample = x_float[0]
    print('预测值 :', (sample @ w + b).item())
    print('真实标签:', y_float[0].item())

    特征维数: 3
    样本特征: tensor([3., 1., 0.])
    权重 : tensor([1., 2., 3.])
    预测值 : 5.0
    真实标签: 127500.0

    这里也踩过一个小坑:我最初把权重硬编码成 torch.tensor([0.5, 1.0, 0.8, 2.0])(4 个), 而特征只有 3 维,直接报 RuntimeError: inconsistent tensor size, expected tensor [3] and src [4] … 结论:矩阵乘法前一定要确认维度对得上,用 x.shape[1] 动态取维度比硬编码安全得多。


    四、整合后的完整代码

    把 002(类型转换)和 003(数据预处理)合并成一个可直接运行的脚本:

    """
    002_类型转换.py + 003_数据预处理.py 整合版
    主题:张量的内存/类型转换 → 真实数据的预处理与张量化

    运行方式(重要):
    在 Project 目录下运行: python 005_类型转换与数据预处理.py
    因为 003 里的 data_file 用的是相对路径 os.path.join('..', 'data', 'house_tiny.csv')
    """
    import os
    import sys
    import pandas as pd
    import torch

    # Windows 控制台默认不是 UTF-8,强制一下避免中文乱码
    sys.stdout.reconfigure(encoding='utf-8')

    torch.set_printoptions(precision=4, sci_mode=False)

    print('=' * 60)
    print('环境信息')
    print('=' * 60)
    print('PyTorch 版本:', torch.__version__)
    print('Pandas 版本:', pd.__version__)
    print('CUDA 可用:', torch.cuda.is_available())

    # ==========================================================
    # 第一部分:内存与类型转换(原 002)
    # ==========================================================

    print('\\n' + '=' * 60)
    print('第一部分:内存与类型转换')
    print('=' * 60)

    # ———————————————————-
    # 1.1 运算会新分配内存
    # ———————————————————-
    print('\\n— 1.1 普通运算会新分配内存 —')
    X = torch.tensor([1, 2, 3, 4, 5])
    Y = torch.tensor([1, 2, 3, 4, 5])

    before = id(Y)
    print(f'运算前 Y 的 id:{before}')
    print('运算前 Y 的值:', Y)

    Y = Y + X # 等价于 Y = torch.add(Y, X),会开辟一块新内存

    print('运算后 Y 的值:', Y)
    print(f'运算后 Y 的 id:{id(Y)}')
    print('两次 id 是否相同:', id(Y) == before)

    # ———————————————————-
    # 1.2 原地操作:不新分配内存,省显存/内存
    # ———————————————————-
    print('\\n— 1.2 原地操作不会新分配内存 —')
    # torch.zeros_like(Y):生成一个形状、dtype、device 都和 Y 完全一致,但元素全为 0 的新张量
    Z = torch.zeros_like(Y)
    print('初始化后 id(Z):', id(Z))

    Z[:] = X + Y # 切片赋值:把结果写回 Z 原有的内存
    print('切片赋值后 id(Z):', id(Z))

    Y_before = id(Y)
    Y += X # 原地加法:等价于 Y.add_(X),iadd 语义
    print('+= 之后 Y 的值:', Y)
    print('+= 前后 id 是否相同:', id(Y) == Y_before)

    # ———————————————————-
    # 1.3 NumPy 与 Tensor 的互相转换(共享内存)
    # ———————————————————-
    print('\\n— 1.3 NumPy <-> Tensor 转换 —')
    A = X.numpy() # Tensor -> ndarray(与 X 共享同一块内存)
    B = torch.tensor(A) # ndarray -> Tensor(会拷贝一份数据)

    print('A 的类型:', type(A))
    print('B 的类型:', type(B))
    print('A 的值:', A)
    print('B 的值:', B)

    # 验证 Tensor -> ndarray 是共享内存:改 A 会连带改 X
    A[0] = 100
    print('修改 A[0]=100 后,X =', X, ' B =', B, '(B 是拷贝,不受影响)')

    # 也可以用 torch.from_numpy() 做零拷贝转换
    C = torch.from_numpy(A)
    print('torch.from_numpy(A) 与 X 共享内存:', C.data_ptr() == X.data_ptr())

    # ———————————————————-
    # 1.4 Tensor 元素 -> Python 标量
    # ———————————————————-
    print('\\n— 1.4 取出 Python 标量 —')
    a = torch.tensor([3.5]) # 浮点张量
    b = torch.tensor([3]) # 整型张量

    print('a =', a, ' a.item() =', a.item(), type(a.item()))
    print('b =', b, ' b.item() =', b.item(), type(b.item()))
    print('float(a) =', float(a), type(float(a)))
    print('int(a) =', int(a), type(int(a)))

    # ==========================================================
    # 第二部分:数据预处理(原 003)
    # ==========================================================

    print('\\n' + '=' * 60)
    print('第二部分:数据预处理')
    print('=' * 60)

    # ———————————————————-
    # 2.1 创建并读取数据集
    # ———————————————————-
    print('\\n— 2.1 创建 CSV 数据集并读取 —')
    os.makedirs(os.path.join('..', 'data'), exist_ok=True)
    data_file = os.path.join('..', 'data', 'house_tiny.csv')
    print('数据文件路径:', os.path.abspath(data_file))

    with open(data_file, 'w', encoding='utf-8') 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')

    data = pd.read_csv(data_file)
    print('原始数据 data:')
    print(data)

    # NA 不是数字,read_csv 会把它当成缺失值 NaN
    print('\\n缺失值统计:')
    print(data.isna().sum())

    # ———————————————————-
    # 2.2 处理缺失数据:插值、删除
    # ———————————————————-
    print('\\n— 2.2 处理缺失值 —')
    inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
    print('inputs(特征):')
    print(inputs)
    print('outputs(标签):')
    print(outputs)

    # 方案 A:数值列用均值填充
    # 注意:mean() 默认只对数值列求均值,非数值列会被忽略
    # (pandas 2.x 里对含字符串的列直接 mean() 会报错,加 numeric_only=True 更稳)
    inputs_mean = inputs.fillna(inputs.mean(numeric_only=True))
    print('\\n方案A:均值填充后的 inputs_mean:')
    print(inputs_mean)

    # 方案 B(本例采用):把类别/离散值用独热编码 one-hot 展开,并把 NaN 当成单独的一个类别
    # 关键点:先做独热编码,非数值列就被拆成了 0/1 数值列;
    # 此时再做均值填充,才能保证最终一个 NaN 都不剩。
    # 如果反过来先 get_dummies,NumRooms 列里的 NaN 不会被处理,转张量时就是 nan。
    inputs = pd.get_dummies(inputs, dummy_na=True, dtype=int)
    inputs = inputs.fillna(inputs.mean(numeric_only=True))
    print('\\n方案B:get_dummies 独热编码 + 均值填充后的 inputs:')
    print(inputs)
    print('此时还有缺失值吗:', inputs.isna().any().any())

    # ———————————————————-
    # 2.3 转换为张量格式
    # ———————————————————-
    print('\\n— 2.3 转为张量 —')
    # 转张量前先断言:一个 NaN 都不能有,否则后面 loss 直接变 nan 且很难排查
    assert not inputs.isna().any().any(), '还存在缺失值,请先处理再转张量'
    x, y = torch.tensor(inputs.values), torch.tensor(outputs.values)
    print('x =', x, ' x.shape =', x.shape, ' x.dtype =', x.dtype)
    print('y =', y, ' y.shape =', y.shape, ' y.dtype =', y.dtype)

    # 转成 float32,方便后面直接送进神经网络做运算
    x_float, y_float = x.to(torch.float32), y.to(torch.float32)
    print('x_float.dtype =', x_float.dtype, ' y_float.dtype =', y_float.dtype)

    # ———————————————————-
    # 2.4 一个最小示例:用这批数据做一次前向计算(可跳过)
    # ———————————————————-
    print('\\n— 2.4 一条样本的前向计算演示 —')
    n_features = x_float.shape[1]
    w = torch.arange(1, n_features + 1, dtype=torch.float32) # 随便给一组权重,只是演示形状要对上
    b = torch.tensor(0.0)
    sample = x_float[0]
    print('特征维数:', n_features)
    print('样本特征:', sample)
    print('权重 :', w)
    print('预测值 :', (sample @ w + b).item())
    print('真实标签:', y_float[0].item())

    print('\\n全部演示结束。')


    五、运行结果(真实输出)

    以下为 005_类型转换与数据预处理.py 的完整运行输出,原样粘贴,未做任何修饰。 说明:id 的具体数值每次运行都不同(那是内存地址), 但有意义的是**"两次 id 是否相同"这个布尔结论**——它才是判断是否新分配内存的依据。

    ============================================================
    环境信息
    ============================================================
    PyTorch 版本: 2.14.1+cpu
    Pandas 版本: 2.3.3
    CUDA 可用: False

    ============================================================
    第一部分:内存与类型转换
    ============================================================

    — 1.1 普通运算会新分配内存 —
    运算前 Y 的 id:2164783116224
    运算前 Y 的值: tensor([1, 2, 3, 4, 5])
    运算后 Y 的值: tensor([ 2, 4, 6, 8, 10])
    运算后 Y 的 id:2164783116304
    两次 id 是否相同: False

    — 1.2 原地操作不会新分配内存 —
    初始化后 id(Z): 2164783116224
    切片赋值后 id(Z): 2164783116224
    += 之后 Y 的值: tensor([ 3, 6, 9, 12, 15])
    += 前后 id 是否相同: True

    — 1.3 NumPy <-> Tensor 转换 —
    A 的类型: <class 'numpy.ndarray'>
    B 的类型: <class 'torch.Tensor'>
    A 的值: [1 2 3 4 5]
    B 的值: tensor([1, 2, 3, 4, 5])
    修改 A[0]=100 后,X = tensor([100, 2, 3, 4, 5]) B = tensor([1, 2, 3, 4, 5]) (B 是拷贝,不受影响)
    torch.from_numpy(A) 与 X 共享内存: True

    — 1.4 取出 Python 标量 —
    a = tensor([3.5000]) a.item() = 3.5 <class 'float'>
    b = tensor([3]) b.item() = 3 <class 'int'>
    float(a) = 3.5 <class 'float'>
    int(a) = 3 <class 'int'>

    ============================================================
    第二部分:数据预处理
    ============================================================

    — 2.1 创建 CSV 数据集并读取 —
    数据文件路径: C:\\Users\\16108\\Desktop\\Python_project\\DL_project\\DL_project\\data\\house_tiny.csv
    原始数据 data:
    NumRooms Alley Price
    0 NaN Pave 127500
    1 2.0 NaN 106000
    2 4.0 NaN 178100
    3 NaN NaN 140000

    缺失值统计:
    NumRooms 2
    Alley 3
    Price 0
    dtype: int64

    — 2.2 处理缺失值 —
    inputs(特征):
    NumRooms Alley
    0 NaN Pave
    1 2.0 NaN
    2 4.0 NaN
    3 NaN NaN
    outputs(标签):
    0 127500
    1 106000
    2 178100
    3 140000
    Name: Price, dtype: int64

    方案A:均值填充后的 inputs_mean:
    NumRooms Alley
    0 3.0 Pave
    1 2.0 NaN
    2 4.0 NaN
    3 3.0 NaN

    方案B:get_dummies 独热编码 + 均值填充后的 inputs:
    NumRooms Alley_Pave Alley_nan
    0 3.0 1 0
    1 2.0 0 1
    2 4.0 0 1
    3 3.0 0 1
    此时还有缺失值吗: False

    — 2.3 转为张量 —
    x = tensor([[3., 1., 0.],
    [2., 0., 1.],
    [4., 0., 1.],
    [3., 0., 1.]], dtype=torch.float64) x.shape = torch.Size([4, 3]) x.dtype = torch.float64
    y = tensor([127500, 106000, 178100, 140000]) y.shape = torch.Size([4]) y.dtype = torch.int64
    x_float.dtype = torch.float32 y_float.dtype = torch.float32

    — 2.4 一条样本的前向计算演示 —
    特征维数: 3
    样本特征: tensor([3., 1., 0.])
    权重 : tensor([1., 2., 3.])
    预测值 : 5.0
    真实标签: 127500.0

    全部演示结束。


    六、避坑指南:我踩过的 5 个坑

    坑 1:get_dummies 和 fillna 的顺序搞反

    ❌ 错误写法:

    inputs = pd.get_dummies(inputs, dummy_na=True, dtype=int) # Alley 拆成 0/1 了,但 NumRooms 的 NaN 还在
    x = torch.tensor(inputs.values)
    # x 里面是 nan!
    # tensor([[nan, 1., 0.], [2., 0., 1.], [4., 0., 1.], [nan, 0., 1.]])

    ✅ 正确写法:

    inputs = pd.get_dummies(inputs, dummy_na=True, dtype=int)
    inputs = inputs.fillna(inputs.mean(numeric_only=True)) # 再填充,保证无 NaN
    assert not inputs.isna().any().any() # 加个断言,早发现早治疗

    张量里混进 nan,网络训练时 loss 会直接变成 nan,而且往往不报错,只是模型永远训不好。转张量前一定要断言检查。


    坑 2:忘了转 float32

    x = torch.tensor(inputs.values) # dtype=torch.float64
    # linear = nn.Linear(3, 1)
    # linear(x) → RuntimeError: expected scalar type Float but found Double

    ✅ 修正:

    x = torch.tensor(inputs.values, dtype=torch.float32)
    # 或者 x = x.to(torch.float32)


    坑 3:pandas 2.x 里 inputs.mean() 直接报错

    inputs = data.iloc[:, 0:2] # 含字符串列 Alley
    inputs.fillna(inputs.mean())
    # TypeError: Could not convert Pave to numeric

    ✅ 修正:

    inputs.fillna(inputs.mean(numeric_only=True))

    老教程基于 pandas 1.x,mean() 会自动跳过非数值列;pandas 2.x 收紧了行为,必须显式声明。


    坑 4:直接用 DataFrame 造张量

    x = torch.tensor(inputs) # 不推荐:语义不清,部分版本直接报错

    ✅ 修正:

    x = torch.tensor(inputs.values) # 显式取出底层 ndarray
    # 或者
    x = torch.from_numpy(inputs.to_numpy())


    坑 5:X.numpy() 用在 GPU 张量上

    X = torch.randn(3, 4).cuda()
    A = X.numpy()
    # TypeError: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

    ✅ 修正:

    A = X.cpu().numpy() # 先搬回 CPU
    A = X.cpu().detach().numpy() # 如果带梯度,还要 detach()


    七、知识点速查表

    内存与转换

    ndarray 与 tensor 的本质差异(自动求导 / CPU-GPU / 适用场景)见另文 ndarray 和 tensor 到底有啥区别?一张表讲清楚; 下表只聚焦内存语义。

    操作是否新分配内存备注
    Y = Y + X ✅ 是 等价 torch.add(Y, X)
    Y += X ❌ 否 等价 Y.add_(X)
    Y[:] = X ❌ 否 写入原有内存
    Y.add_(X) ❌ 否 带 _ 后缀的都是原地操作
    X.numpy() ❌ 否(共享) 张量必须在 CPU
    torch.from_numpy(A) ❌ 否(共享) 零拷贝
    torch.tensor(A) ✅ 是(拷贝) 安全,改一边不影响另一边
    torch.as_tensor(A) ❌ 否(能共享就共享) 语义上"尽量不拷贝"

    数据预处理

    API作用
    pd.read_csv(path) 读 CSV,自动把 NA/N/A 识别为 NaN
    data.isna().sum() 按列统计缺失值数量
    data.iloc[:, 0:2] 按位置切片取列
    df.fillna(value) 缺失值填充
    df.mean(numeric_only=True) 数值列均值(pandas 2.x 必加参数)
    pd.get_dummies(df, dummy_na=True, dtype=int) 独热编码,NaN 作为独立类别
    df.values / df.to_numpy() DataFrame → ndarray
    torch.tensor(ndarray) ndarray → Tensor
    tensor.to(torch.float32) 转换 dtype(推荐写法,比 .float() 更明确)
    tensor.item() 单元素张量 → Python 标量

    常用 dtype

    dtype用途
    torch.float32 神经网络默认类型,权重、输入都用它
    torch.float64 高精度科学计算;混用会导致类型报错
    torch.int64 默认整型,分类任务的标签、nn.Embedding 的索引
    torch.bool 比较运算的返回值、mask

    八、小结

  • 普通运算会开辟新内存,原地操作(+=、add_()、Z[:]=)不会。 训练循环里省显存靠的就是原地操作,但要注意别破坏计算图。
  • tensor.numpy() 和 torch.from_numpy() 共享内存,torch.tensor() 拷贝。 共享是双刃剑,改一边会连带改另一边。
  • 数据预处理的标准四步走: 读数据 → 看缺失(isna().sum())→ 处理缺失(填充/删除)→ 类别特征独热编码 → 转 float32 张量。
  • 顺序不能反:先 get_dummies 再 fillna,才能保证一个 NaN 都不剩。
  • 转张量前加一句 assert not df.isna().any().any(),能帮你省下几个小时的 debug 时间。
  • 下一篇预告:线性代数基础——矩阵转置、对称矩阵、哈达玛积、求和降维与广播,以及张量乘法(@ / matmul)的各种形状规则。


    📚 系列导航

    篇目主题链接
    概念篇 ndarray 与 tensor 的本质区别 一张表讲清楚
    前置篇 深度学习基础知识 深度学习基础知识
    机器学习 KNN 基础(原理篇) 机器学习基础知识1-KNN基础
    机器学习 KNN 基础(实战篇) KNN算法基础2-分类与回归思想
    本篇 张量内存与类型转换 + 数据预处理 你正在看的这篇
    下一篇 线性代数基础 敬请期待

    📌 本文代码已在本地实际运行验证,环境:PyTorch 2.14.1+cpu / pandas 2.3.3 / Python 3.10.22。 如果对你有帮助,欢迎点赞 + 收藏 + 关注,你的支持是我持续更新的动力 🙏 有问题欢迎评论区交流,看到都会回!

    赞(0)
    未经允许不得转载:171主机测评 » 深度学习:PyTorch 零基础入门(二)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址