PyTorch 学习路线:从 Tensor 到训练完整模型
学习阶段规划阶段 时间 目标Python 基础 3~5 天 能写循环、函数、类NumPy 基础 2 天 理解矩阵运算PyTorch Tensor 3 天 会张量计算和 GPU 迁移Autograd 自动求导 2 天 理解梯度和 backwardnn.Module 基础 3 天 会定义 MLP训练流程 3 天 会完整训练和评估CNN 5 天 图片分类Transformer 基础 7 天 理解 Attention项目实战 长期 YOLO、PointNet 等约一个月可以达到能读懂主流论文代码的水平。 为什么从线性回归开始 # 目标:给出面积和房间数,预测房价 area = [80, 100, 120, 150] rooms = [2, 3, 3, 4] price = [120, 180, 220, 300]神经网络就是找一个函数 f,使得 f(area, rooms) ≈ price。PyTorch 的作用是:定义 f 的结构(model) 自动计算 f 的梯度(autograd) 按梯度更新参数,让预测更准确(optimizer)核心训练循环 import torch import torch.nn as nn# 1. 数据 x = torch.tensor([[80, 2], [100, 3], [120, 3], [150, 4]], dtype=torch.float32) y = torch.tensor([120, 180, 220, 300], dtype=torch.float32)# 2. 定义模型 model = nn.Sequential( nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 1) )# 3. 定义 loss 和 optimizer criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)# 4. 训练循环 for epoch in range(200): pred = model(x).squeeze() # 前向传播 loss = criterion(pred, y) # 计算 loss optimizer.zero_grad() # 清空上一步的梯度 loss.backward() # 反向传播,计算梯度 optimizer.step() # 按梯度更新参数 if epoch % 50 == 0: print(f"Epoch {epoch}: loss = {loss.item():.2f}")四步缺一不可:zero_grad() → 清除旧梯度(PyTorch 默认累积梯度) backward() → 计算所有参数对 loss 的偏导数 step() → 把参数沿梯度方向更新一步Tensor 基础操作 import torcha = torch.tensor([1.0, 2.0, 3.0]) b = torch.zeros(3, 4) c = torch.randn(2, 3) # 标准正态分布# 形状操作 c.shape # torch.Size([2, 3]) c.reshape(3, 2) c.unsqueeze(0) # 增加维度: [1, 2, 3] c.squeeze() # 去掉维度为 1 的维# GPU device = "cuda" if torch.cuda.is_available() else "cpu" c = c.to(device)常用 Loss 函数任务 Loss 调用回归 MSELoss nn.MSELoss()二分类 BCELoss nn.BCEWithLogitsLoss()多分类 CrossEntropy nn.CrossEntropyLoss()常用 Optimizer # Adam:自适应学习率,大多数情况首选 optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)# AdamW:Adam + 权重衰减,Transformer 常用 optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2)# SGD:经典,配合 momentum optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)保存和加载模型 # 保存权重 torch.save(model.state_dict(), "model.pth")# 加载权重 model.load_state_dict(torch.load("model.pth")) model.eval() # 推理时关闭 dropout 和 BatchNorm 的训练行为入门建议用 model.state_dict() 而不是直接 torch.save(model, ...),后者会把整个模型类序列化,跨环境加载容易出问题。
PyTorch 点云分割:PointNet 到 Point Transformer 的模型选型
三种任务类型任务 输入 输出 典型场景语义分割 点云 每个点的类别 地面/树木/汽车/行人实例分割 点云 每个点的实例 ID 区分两辆不同车辆部件分割 单个物体点云 零件归属 椅子的腿/靠背/坐垫主流模型 PointNet(最经典) 输入 N×3,直接用 MLP 逐点提取特征,全局池化后再输出分割结果: import torch import torch.nn as nnclass PointNet(nn.Module): def __init__(self, num_classes): super().__init__() self.mlp = nn.Sequential( nn.Linear(3, 64), nn.ReLU(), nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, 256) ) self.cls = nn.Linear(256, num_classes) def forward(self, x): # x: [B, N, 3] feature = self.mlp(x) # [B, N, 256] out = self.cls(feature) # [B, N, num_classes] return out预测时取 argmax 得到每个点的标签: pred = out.argmax(dim=-1) # [B, N]其他模型选型模型 核心 适合场景PointNet++ 局部邻域分层学习 室内、激光雷达、工业检测DGCNN KNN 建图 + EdgeConv 精度要求高的分类/分割Point Transformer Transformer Attention SOTA,大型场景MinkowskiNet 稀疏体素 + Sparse CNN 超密点云,速度快数据格式 点云数据通常有三种格式: xyz # 坐标 [x, y, z] xyzrgb # 坐标 + 颜色 [x, y, z, r, g, b] xyz+intensity # 激光雷达:坐标 + 反射强度读入后转 Tensor: import numpy as np import torchpoints = np.load("scan.npy") # (N, 3) x = torch.tensor(points, dtype=torch.float32) # [N, 3] x = x.unsqueeze(0) # [1, N, 3] add batch dimLoss 函数 点云分割本质是逐点分类,直接用 CrossEntropyLoss: criterion = nn.CrossEntropyLoss()# pred: [B, num_classes, N] or reshape to [B*N, num_classes] # label: [B, N]pred_flat = pred.view(-1, num_classes) # [B*N, num_classes] label_flat = label.view(-1) # [B*N]loss = criterion(pred_flat, label_flat)常用公开数据集数据集 场景 用途ShapeNet Part 单物体 部件分割S3DIS 室内 语义分割SemanticKITTI 自动驾驶 激光雷达语义分割ScanNet RGB-D 室内 场景理解工程推荐流程 点云采集 ↓ 预处理(去噪、下采样、法向量估计) ↓ PointNet++ / Point Transformer 训练 ↓ 输出分割结果 ↓ 后处理(聚类、过滤小区域)入门推荐从 PointNet 开始,结构最简单,容易在本地小数据上跑通。确认流程后再换 PointNet++ 提升精度。
PyTorch 训练基础:Tensor、自动求导、训练循环与模型保存
Tensor Tensor 是 PyTorch 的核心数据结构,类似 NumPy 数组但支持 GPU 运算: import torch# 0 维标量 t0 = torch.tensor(3.14)# 1 维向量 t1 = torch.tensor([1.0, 2.0, 3.0])# 2 维矩阵 t2 = torch.zeros(3, 4)# 移到 GPU if torch.cuda.is_available(): t2 = t2.cuda()Dynamic Graph 与 Autograd PyTorch 使用动态计算图(Define-by-Run),每次前向传播都重新构建计算图,便于调试和条件分支。 开启梯度追踪: x = torch.tensor(2.0, requires_grad=True) y = x ** 2 + 3 * xy.backward() # 反向传播 print(x.grad) # dy/dx = 2x + 3 = 7.0标准训练循环 model = MyModel() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) loss_fn = torch.nn.CrossEntropyLoss()for epoch in range(100): for x_batch, y_batch in dataloader: pred = model(x_batch) # 1. 前向传播 loss = loss_fn(pred, y_batch) # 2. 计算损失 optimizer.zero_grad() # 3. 清空上一步梯度 loss.backward() # 4. 反向传播 optimizer.step() # 5. 更新参数 print(f"Epoch {epoch}, Loss: {loss.item():.4f}")zero_grad() 必须在 backward() 前调用,否则梯度会累加。 损失函数选择任务 损失函数 激活函数二分类 BCELoss Sigmoid多分类 CrossEntropyLoss 无(内置 Softmax)回归 MSELoss 无# 二分类:Logistic Regression model = torch.nn.Sequential( torch.nn.Linear(10, 1), torch.nn.Sigmoid() ) loss_fn = torch.nn.BCELoss()# 多分类 model = torch.nn.Sequential( torch.nn.Linear(10, 5) # 5 个类别 ) loss_fn = torch.nn.CrossEntropyLoss() # 内部含 Softmax# 回归 model = torch.nn.Linear(10, 1) loss_fn = torch.nn.MSELoss()优化器 # SGD(基础,需手动调 lr) optimizer = torch.optim.SGD(model.parameters(), lr=0.01)# SGD + Momentum(更稳定) optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)# Adam(最常用,lr=0.001 通常无需调整) optimizer = torch.optim.Adam(model.parameters(), lr=0.001)# AdamW(Transformer 推荐,带权重衰减修正) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)大多数情况从 Adam + lr=0.001 开始,效果不好再调。 保存与加载模型 推荐:只保存参数(state_dict) # 保存 torch.save(model.state_dict(), "model.pth")# 加载 model = MyModel() model.load_state_dict(torch.load("model.pth")) model.eval() # 切换到推理模式保存完整模型(不推荐,依赖类定义路径): torch.save(model, "model_full.pth") model = torch.load("model_full.pth")导出 ONNX ONNX 格式可在 TensorFlow、ONNX Runtime、TensorRT 等框架中使用: dummy_input = torch.randn(1, 3, 224, 224) # 与实际输入形状一致torch.onnx.export( model, dummy_input, "model.onnx", input_names=["input"], output_names=["output"], opset_version=17 )导出后可用 onnxruntime 推理,速度通常比 PyTorch 原生快。 推理模式 model.eval()with torch.no_grad(): # 禁用梯度计算,节省内存 output = model(input_tensor)推理时必须调 model.eval() 和 torch.no_grad(),否则 BatchNorm 和 Dropout 行为与训练时不同。
BIP39 助记词会不会重复:数学上不会,现实中被坑的另有其人
有人问过我一个纠结的问题:用钱包 APP 生成助记词,会不会跟别人重复导致钱包被盗? 数学上算一下就知道,重复的概率低到根本不用担心;真正被盗的原因,几乎都是别的坑。 BIP39 生成流程 12 词助记词的生成,标准流程只有三步: 1. 从系统 CSPRNG 取 128 位熵 import secrets entropy = secrets.token_bytes(16) # 16 字节 = 128 位secrets 底层用的是操作系统提供的密码学安全随机源:Windows:BCryptGenRandom Linux:/dev/urandom macOS:SecRandomCopyBytes2. 拼上 4 位校验和 对 128 位熵做 SHA-256,取前 4 位挂到熵尾部,凑成 132 位: 128 位熵 + 4 位校验和 = 132 位3. 每 11 位查一次词表 BIP39 词表有 2^11 = 2048 个词。132 位 / 11 = 12 个词。 10010101100 → abandon 11100101010 → ability ...碰撞概率有多低 12 词的熵是 128 位,等于 3.4 × 10³⁸ 种可能。用生日悖论算一下极端情况: 假设全球 100 亿人,每人生成 100 万个钱包,一共 10¹⁶ 个: 碰撞概率 ≈ N² / (2 × 2¹²⁸) ≈ 10³² / 6.8 × 10³⁸ ≈ 1.5 × 10⁻⁷约 0.000015%,还是极端假设。真实世界远远达不到这个量级。24 词是 256 位熵,那更是天文数字。 只要用正规钱包 + 系统 CSPRNG 生成,重复被盗几乎不可能。 真被盗的常见姿势 1. 随机数不安全 自己造轮子最容易出问题: import random random.seed(time.time()) # 危险Python 的 random 是伪随机 + 可预测种子。攻击者知道大概时间戳,就能枚举出你所有可能的助记词。 永远用 secrets 或 os.urandom,不用 random。 2. "幸运数字"当种子 有人觉得自己的生日、手机号能当种子更好记: seed = "5201314" entropy = sha256(seed)这种熵严重不足。攻击者跑一遍 0 到 9,999,999,999 就把你的钱包翻出来了。任何"人可以记住"的种子都不够安全。 3. 脑钱包 想一句话当助记词: iloveyou123彩虹表早就把常见短语算光了,这种钱包上链一分钟就被扫走。 4. 假钱包 APP 流程: APP 生成助记词 → 悄悄上传服务器 → 等你存币 → 转走来路不明的浏览器插件、"新币空投工具"是重灾区。装钱包只从官网或 App Store。 5. 助记词泄露截屏保存到手机相册(云同步就完了) 记在便笺、微信自己的对话 输入到"辅助工具"网页正确做法:只离线纸质备份,多份异地存放。 安全生成的一行代码 用 bip_utils: from bip_utils import Bip39MnemonicGenerator, Bip39WordsNummnemonic = Bip39MnemonicGenerator().FromWordsNumber(Bip39WordsNum.WORDS_NUM_12) print(mnemonic)内部就是 CSPRNG + 128 位熵 + SHA-256 校验,标准 BIP39 流程。 一句话总结 担心助记词重复是想多了;担心自己不用 CSPRNG、拿脑钱包和假钱包 APP 是应该的。 只要用正规钱包生成 + 离线纸质备份,安全性远高于随机重复这种理论问题。
Missing X server or $DISPLAY:无头环境跑浏览器的正确姿势
在服务器 / 容器里想跑 Chromium、Electron、Playwright,报错: [...] ozone_platform_x11.cc:259] Missing X server or $DISPLAY意思很直白——程序想弹 GUI,但当前环境没 X11 显示服务。三个典型场景。 场景一:SSH 到 Linux 服务器 SSH 登进去的 shell 默认没有桌面: ssh root@server google-chrome # 直接报 Missing X server两种解法: Headless 模式(首选) google-chrome --headless=new --disable-gpu --no-sandbox https://example.comPlaywright: browser = playwright.chromium.launch(headless=True)Puppeteer: const browser = await puppeteer.launch({ headless: true });非 headless 场景用 X11 转发 ssh -X root@server或用 -Y(信任模式,速度更快)。前提是本机有 X server(Mac 上是 XQuartz,Windows 上是 VcXsrv/X410)。 场景二:Docker 容器里 容器默认没 X server: $ echo $DISPLAY (空)首选还是 headless。要真的跑 GUI 就靠 Xvfb(虚拟帧缓冲): apt update && apt install -y xvfb xvfb-run -a chromium https://example.comxvfb-run 会在后台起一个虚拟 X server 让程序连。Playwright 官方镜像就自带这套。 如果显示环境变量已经写了 DISPLAY=:1,但仍然报错,那是因为 :1 对应的 X server 根本没起: ls /tmp/.X11-unix/看不到 X0 / X1 就是没启动。容器里手动 export DISPLAY=:1 是没用的——DISPLAY 只是"连哪个 X server",不会创建 X server。 场景三:WSL WSL2 现在原生支持 WSLg,一般不会碰到这错。老 WSL1 或者关了 WSLg 的话: export DISPLAY=:0 sudo apt install x11-apps xclock # 测试有没有 GUI再不行就装个 X server(Windows 上 VcXsrv / X410)。 快速判断清单 先看几个变量: echo $DISPLAY # 应该是 :0 或 :1 ls /tmp/.X11-unix/ # 应该有 X0 之类的 socket ps aux | grep -E 'Xorg|Xvfb' # 有 X server 进程在跑 hostname # 类似 6e6340e2e0d2 基本是容器 cat /proc/1/cgroup # 看到 docker 关键字 = 容器对号入座:现象 判断DISPLAY 空 没有桌面环境,用 headless 或 XvfbDISPLAY 有但 X11 socket 目录空 声明了但 X server 没起hostname 是短哈希 Docker 容器WSL 里 xclock 不动 WSL 侧 X server 没通一句话总结 服务器 / 容器跑浏览器,能 headless 就 headless,非要 GUI 上 Xvfb。 手动改 $DISPLAY 只是告诉客户端连哪儿,从来不会凭空生出一个 X server。
