一句话总结

不需要昂贵的位置真值标签,仅靠 IMU 数据自带的物理方程,就能自监督训练出媲美全监督基线的惯性里程计。


为什么这个问题重要?

惯性里程计(Inertial Odometry)是机器人、AR/VR、自动驾驶的基础能力——给一颗 IMU,预测自身运动轨迹。

IMU 的天然优势: 轻量、廉价、无处不在(手机/无人机/机械臂都有),采样率高(200~1000 Hz),不依赖 GPS 或视觉。

核心痛点: 加速度计偏差(bias)让速度积分误差随时间平方增长,纯 INS 积分几秒内就漂移米级。

现有学习型方法的瓶颈: TLIO、RONIN 等方法需要 Motion Capture 或 SLAM 提供的稠密位置真值——这类数据采集成本极高,限制了大规模预训练和跨场景泛化。

DINS-IO 的思路转变: INS 自身的速度递推方程是一个强约束,它完全可微,可以直接当作自监督信号。


背景知识

捷联 INS:IMU 的物理方程

IMU 输出两路信号:

  • 陀螺仪:角速度 $\boldsymbol{\omega}^b$(body frame)
  • 加速度计:比力 $\mathbf{f}^b_{meas} = \mathbf{f}^b_{true} + \mathbf{b}_a$(真实比力 + 偏差)

速度递推方程(Strapdown 核心):

\[\mathbf{v}^n_{k+1} = \mathbf{v}^n_k + \mathbf{R}^n_{b,k} \cdot \mathbf{f}^b_{meas,k} \cdot \Delta t - \mathbf{R}^n_{b,k} \cdot \mathbf{b}_a \cdot \Delta t\]

其中 $\mathbf{R}^n_{b,k}$(body→navigation 旋转矩阵)由陀螺仪积分得到,是已知量。

关键推论: 给定初始速度 $\mathbf{v}^n_0$ 和偏差 $\mathbf{b}_a$,整个速度序列就完全确定了——这就是 DINS-IO 要利用的约束。

旋转矩阵积分

\[\mathbf{R}_{k+1} = \mathbf{R}_k \cdot \exp([\boldsymbol{\omega}^b_k]_\times \Delta t) \approx \mathbf{R}_k \cdot \left(\mathbf{I} + [\boldsymbol{\omega}^b_k]_\times \sin\theta / \theta + [\boldsymbol{\omega}^b_k]_\times^2 (1 - \cos\theta)/\theta^2\right)\]

核心方法

直觉解释

IMU 流(gyro + accel)
       ↓
   网络预测每步的 body-frame 速度 v̂ᵇₖ
       ↓
   陀螺积分得到 Rₖ → 将 v̂ᵇₖ 转到导航系:v̂ⁿₖ = Rₖ v̂ᵇₖ
       ↓
   这些 v̂ⁿₖ 必须 = INS积分(v₀, bₐ) ← 线性约束
       ↓
   最小二乘求解 (v₀*, bₐ*) 的闭合解
       ↓
   残差 = 自监督损失 → 梯度反传给网络

数学推导

在长度为 $N$ 的滑动窗口内,展开速度递推得到:

\[\mathbf{v}^n(t_k) = \mathbf{v}^n_0 + \underbrace{\sum_{i=0}^{k-1} \mathbf{R}^n_{b,i} \mathbf{f}^b_{meas,i} \Delta t}_{\mathbf{I}_k\ (\text{可预计算})} - \underbrace{\left(\sum_{i=0}^{k-1} \mathbf{R}^n_{b,i} \Delta t\right)}_{\mathbf{S}_k} \mathbf{b}_a\]

网络预测 $\hat{\mathbf{v}}^b_k$,一致性要求 $\mathbf{R}_k \hat{\mathbf{v}}^b_k = \mathbf{v}^n_0 + \mathbf{I}_k - \mathbf{S}_k \mathbf{b}_a$。

堆叠所有时间步,得到线性系统 $\mathbf{A}\mathbf{x} = \mathbf{b}$:

\[\underbrace{\begin{bmatrix} \mathbf{I}_3 & -\mathbf{S}_0 \\ \vdots & \vdots \\ \mathbf{I}_3 & -\mathbf{S}_{N-1} \end{bmatrix}}_{\mathbf{A} \in \mathbb{R}^{3N \times 6}} \underbrace{\begin{bmatrix} \mathbf{v}^n_0 \\ \mathbf{b}_a \end{bmatrix}}_{\mathbf{x}} = \underbrace{\begin{bmatrix} \mathbf{R}_0 \hat{\mathbf{v}}^b_0 - \mathbf{I}_0 \\ \vdots \\ \mathbf{R}_{N-1} \hat{\mathbf{v}}^b_{N-1} - \mathbf{I}_{N-1} \end{bmatrix}}_{\mathbf{b}}\]

闭合解: $\mathbf{x}^* = (\mathbf{A}^T\mathbf{A})^{-1}\mathbf{A}^T\mathbf{b}$

自监督损失:

\[\mathcal{L}_{self} = \left\|\mathbf{b} - \mathbf{A}\mathbf{x}^*\right\|^2\]

梯度通过解析解自动反传给 $\hat{\mathbf{v}}^b_k$(PyTorch autograd 支持 linalg.lstsq)。


实现

环境配置

pip install torch numpy matplotlib

陀螺积分与 INS 积分量

import torch
import torch.nn as nn

def skew_symmetric(v):
    """向量 (B, T, 3) → 反对称矩阵 (B, T, 3, 3)"""
    B, T, _ = v.shape
    z = torch.zeros(B, T, device=v.device)
    return torch.stack([
        z,        -v[...,2],  v[...,1],
        v[...,2],  z,        -v[...,0],
       -v[...,1],  v[...,0],  z,
    ], dim=-1).view(B, T, 3, 3)

def integrate_gyro(gyro, dt):
    """陀螺积分,返回 R: (B, T+1, 3, 3),初始 R0=I"""
    B, T, _ = gyro.shape
    R = torch.eye(3, device=gyro.device).view(1, 1, 3, 3).expand(B, 1, 3, 3)
    Rs = [R]
    for k in range(T):
        w = gyro[:, k]                                        # (B, 3)
        theta = w.norm(dim=-1, keepdim=True).clamp(min=1e-8) # (B, 1)
        axis = w / theta                                       # (B, 3)
        K = skew_symmetric(axis.unsqueeze(1)).squeeze(1)      # (B, 3, 3)
        dR = (torch.eye(3, device=gyro.device)
              + torch.sin(theta * dt).unsqueeze(-1) * K
              + (1 - torch.cos(theta * dt)).unsqueeze(-1) * (K @ K))
        Rs.append(Rs[-1] @ dR.unsqueeze(1))
    return torch.cat(Rs, dim=1)                               # (B, T+1, 3, 3)

def compute_ins_quantities(accel, R, dt):
    """计算 I_k(积分比力)和 S_k(累积旋转),均为 (B, T, ...)"""
    Rf = torch.einsum('btij,btj->bti', R[:, :-1], accel)    # (B, T, 3)
    I = torch.cumsum(Rf * dt, dim=1)
    I = torch.cat([torch.zeros_like(I[:, :1]), I[:, :-1]], dim=1)
    S = torch.cumsum(R[:, :-1] * dt, dim=1)                 # (B, T, 3, 3)
    S = torch.cat([torch.zeros_like(S[:, :1]), S[:, :-1]], dim=1)
    return I, S

可微一致性求解器(核心)

def consistency_loss(v_body, R, accel, dt, lam=1e-4):
    """
    DINS-IO 自监督损失:INS 一致性最小二乘残差
    v_body: (B, T, 3)  网络预测的 body-frame 速度
    R:      (B, T+1, 3, 3)  陀螺积分旋转矩阵
    accel:  (B, T, 3)  加速度计输出
    """
    B, T, _ = v_body.shape
    I, S = compute_ins_quantities(accel, R, dt)

    # 转到导航系
    v_nav = torch.einsum('btij,btj->bti', R[:, :T], v_body)  # (B, T, 3)

    # 右端向量 b = v_nav - I, 形状 (B, 3T, 1)
    b = (v_nav - I).reshape(B, 3*T, 1)

    # 系数矩阵 A = [I_3 | -S_k], 形状 (B, 3T, 6)
    I3 = torch.eye(3, device=v_body.device).view(1,1,3,3).expand(B, T, 3, 3)
    A = torch.cat([I3.reshape(B, 3*T, 3), (-S).reshape(B, 3*T, 3)], dim=-1)

    # 加岭正则避免奇异:正规方程 (A'A + λI) x = A'b
    ATA = A.mT @ A + lam * torch.eye(6, device=A.device)
    ATb = A.mT @ b
    x_star = torch.linalg.solve(ATA, ATb)                    # (B, 6, 1)

    residual = b - A @ x_star                                 # (B, 3T, 1)
    return (residual ** 2).mean()

网络架构

class DINS_IO_Net(nn.Module):
    """
    高频惯性里程计网络:(B, T, 6) → (B, T, 3) body-frame 速度
    使用扩张 TCN,感受野约 160ms(200Hz,4层,dilation 1/2/4/8)
    """
    def __init__(self, hidden=256, num_layers=4):
        super().__init__()
        self.proj = nn.Linear(6, hidden)
        self.tcn = nn.ModuleList([
            nn.Sequential(
                nn.Conv1d(hidden, hidden, kernel_size=3,
                          padding=2**i, dilation=2**i),
                nn.GELU(), nn.GroupNorm(8, hidden),
            ) for i in range(num_layers)
        ])
        self.head = nn.Linear(hidden, 3)

    def forward(self, imu):
        x = self.proj(imu).transpose(1, 2)      # (B, hidden, T)
        for layer in self.tcn:
            x = x + layer(x)[..., :x.shape[-1]] # 残差+长度对齐
        return self.head(x.transpose(1, 2))      # (B, T, 3)

自监督预训练

def train_self_supervised(model, imu_dataset, dt=0.005, epochs=100):
    """
    imu_dataset: list of (T, 6) tensors(原始 IMU,无位置标签)
    dt: IMU 采样间隔,200Hz → 0.005s
    """
    optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
    for epoch in range(epochs):
        total = 0.0
        for raw_imu in imu_dataset:
            imu = raw_imu.unsqueeze(0)                       # (1, T, 6)
            accel, gyro = imu[..., :3], imu[..., 3:]
            R = integrate_gyro(gyro, dt)                     # (1, T+1, 3, 3)
            v_body = model(imu)                              # (1, T, 3)
            loss = consistency_loss(v_body, R, accel, dt)
            optimizer.zero_grad()
            loss.backward()
            nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            optimizer.step()
            total += loss.item()
        if epoch % 20 == 0:
            print(f"[Epoch {epoch}] self-supervised loss: {total/len(imu_dataset):.5f}")

LoRA 度量标定

自监督预训练让网络学到几何一致的运动,但速度的绝对尺度未必正确。用少量有速度标签的轨迹 + LoRA 微调来校准:

class LoRALinear(nn.Module):
    def __init__(self, linear, rank=8):
        super().__init__()
        self.w = linear
        d_in, d_out = linear.in_features, linear.out_features
        self.A = nn.Parameter(torch.randn(d_in, rank) * 0.01)
        self.B = nn.Parameter(torch.zeros(rank, d_out))
        for p in self.w.parameters():
            p.requires_grad = False  # 冻结骨干

    def forward(self, x):
        return self.w(x) + x @ self.A @ self.B

def finetune_lora(model, labeled_data, epochs=20, lr=3e-4):
    """labeled_data: list of (imu, v_body_gt) 元组,只需速度真值"""
    model.head = LoRALinear(model.head)
    optimizer = torch.optim.Adam(
        [p for p in model.parameters() if p.requires_grad], lr=lr)
    for _ in range(epochs):
        for imu, v_gt in labeled_data:
            v_pred = model(imu.unsqueeze(0)).squeeze(0)
            loss = nn.functional.mse_loss(v_pred, v_gt)
            optimizer.zero_grad(); loss.backward(); optimizer.step()

轨迹可视化

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def visualize_trajectory(v_body, R, dt):
    """从预测速度积分轨迹,3D 可视化"""
    T = v_body.shape[1]
    v_nav = torch.einsum('btij,btj->bti', R[:, :T], v_body)
    pos = torch.cumsum(v_nav * dt, dim=1)[0].detach().numpy()  # (T, 3)
    spd = v_nav[0].norm(dim=-1).detach().numpy()               # (T,)

    fig = plt.figure(figsize=(12, 4))
    ax = fig.add_subplot(121, projection='3d')
    sc = ax.scatter(pos[:,0], pos[:,1], pos[:,2], c=spd, cmap='viridis', s=1)
    ax.set_title("Estimated Trajectory (colored by speed)")
    plt.colorbar(sc, ax=ax, label='Speed (m/s)')

    ax2 = fig.add_subplot(122)
    t = [i * dt for i in range(T)]
    for i, lbl in enumerate(['vx', 'vy', 'vz']):
        ax2.plot(t, v_nav[0, :, i].detach().numpy(), label=lbl)
    ax2.set_xlabel("Time (s)"); ax2.legend(); ax2.set_title("Nav-frame Velocity")
    plt.tight_layout(); plt.savefig("dins_io.png", dpi=150); plt.show()

实验

数据集说明

数据集 场景 IMU 频率 真值来源 是否免费
EuRoC 无人机室内 200 Hz Motion Capture
TLIO 手持设备 200 Hz VIO/SLAM
RONIN 行人多姿态 200 Hz RTK-GPS

DINS-IO 自监督阶段完全不需要位置真值,仅使用原始 IMU 流。

性能对比

方法 EuRoC ATE (m) 是否需要位置标签 标签量
纯 INS 积分 发散 0
TLIO(全监督) ~0.32 全量
RONIN(全监督) ~0.28 全量
DINS-IO(自监督 + 少量微调) ~0.29 仅速度标签 ~10%

用约 10% 的标注数据,性能接近全监督基线——这是核心价值所在。


工程实践

实时性与硬件需求

  • 推理延迟:TCN 单步 <1ms(RTX 3080),支持 200Hz 实时输出
  • 内存:模型约 5~10MB,适合嵌入式部署(Jetson Orin)
  • 长轨迹:滑动窗口设计,不会随时间增加内存消耗

常见坑

坑 1:旋转矩阵数值漂移

长时间积分后 $\mathbf{R}$ 行列式偏离 1.0,导致速度旋转误差。每隔 $N$ 步重正交化一次:

def reorthogonalize(R):
    U, _, Vh = torch.linalg.svd(R)
    return U @ Vh  # 投影回 SO(3)

坑 2:滑动窗口太短导致奇异

窗口小于 10 步时 $\mathbf{A}^T\mathbf{A}$ 接近奇异,建议窗口 $N \geq 50$,同时保留岭正则 $\lambda = 10^{-4}$(已在 consistency_loss 中实现)。

坑 3:LoRA 微调过拟合

标注数据极少时(<5 条轨迹)容易破坏预训练的泛化性。对策:早停 + 学习率 $\leq 3 \times 10^{-4}$ + 只放开 LoRA 参数(秩 $r=8$ 足够)。


什么时候用 / 不用?

适用场景 不适用场景
位置标签难获取(野外、水下) 已有大量 Motion Capture 数据
需要大规模预训练后跨场景迁移 车辆高速行驶(加速度计易饱和)
行人、机器人、无人机等 高动态旋转(陀螺漂移主导误差)
GPS 拒止、室内导航 对延迟极度敏感(需专用推理优化)

与其他方法对比

方法 训练标签 优点 缺点
纯 INS 积分 零延迟、零标签 分钟级漂移
TLIO / RONIN 稠密位置真值 精度高 数据采集贵,跨场景泛化差
DINS-IO 无位置标签(少量速度标签) 自监督、易扩展 需额外 LoRA 标定步骤

我的观点

物理约束作为自监督信号是一个重要范式。 DINS-IO 证明了 INS 方程可以直接嵌入梯度流,同样的思路可以扩展到 LiDAR 里程计(用点云配准约束)或视觉惯性(用重投影误差约束)的自监督训练。

LoRA 标定比全量微调更实用。 在产品化场景中,你往往只能在目标设备上采集 5~10 条轨迹。LoRA 的参数效率(只更新 0.1% 的参数)使”采集少量数据即可适配新设备”真正可行。

当前瓶颈:旋转依赖陀螺仪。 自监督损失只约束了速度的线性一致性,旋转估计仍完全依赖陀螺仪积分。10 分钟以上的轨迹,旋转误差会显著拖累位置精度。结合磁力计或单目视觉做旋转修正,是这个方向下一步最值得投入的地方。

开放问题: 全局偏差假设(constant $\mathbf{b}_a$)在温度剧烈变化场景下会失效;滑动窗口长度如何自适应选取;自监督和度量标定是否能联合优化——这些都是有价值的后续研究方向。