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
两个要点:
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,容易踩坑) |
编码前后对比:
| 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) | ❌ 否(能共享就共享) | 语义上"尽量不拷贝" |
数据预处理
| 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
| torch.float32 | 神经网络默认类型,权重、输入都用它 |
| torch.float64 | 高精度科学计算;混用会导致类型报错 |
| torch.int64 | 默认整型,分类任务的标签、nn.Embedding 的索引 |
| torch.bool | 比较运算的返回值、mask |
八、小结
下一篇预告:线性代数基础——矩阵转置、对称矩阵、哈达玛积、求和降维与广播,以及张量乘法(@ / matmul)的各种形状规则。
📚 系列导航
| 概念篇 | ndarray 与 tensor 的本质区别 | 一张表讲清楚 |
| 前置篇 | 深度学习基础知识 | 深度学习基础知识 |
| 机器学习 | KNN 基础(原理篇) | 机器学习基础知识1-KNN基础 |
| 机器学习 | KNN 基础(实战篇) | KNN算法基础2-分类与回归思想 |
| 本篇 | 张量内存与类型转换 + 数据预处理 | 你正在看的这篇 |
| 下一篇 | 线性代数基础 | 敬请期待 |
📌 本文代码已在本地实际运行验证,环境:PyTorch 2.14.1+cpu / pandas 2.3.3 / Python 3.10.22。 如果对你有帮助,欢迎点赞 + 收藏 + 关注,你的支持是我持续更新的动力 🙏 有问题欢迎评论区交流,看到都会回!


