ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Eigen库中norm、normalize与normalized函数的区别与应用场景详解

Eigen库中norm、normalize与normalized函数的区别与应用场景详解 1. 项目概述从一次向量计算错误说起在C的Eigen库中进行几何计算或者机器学习预处理时norm、normalize和normalized这三个函数绝对是高频词汇。我记得有一次在写一个点云配准的算法时需要计算旋转轴的单位向量我随手写了一句vec.normalize();然后直接用vec去做后续的叉乘计算结果出来的变换矩阵总是有微小的偏差调试了大半天才发现问题出在我对这几个函数行为差异的想当然上。normalize()是原地修改而normalized()是返回一个新对象这个细微的差别在复杂的计算链中一旦被忽略就会引入难以察觉的Bug。今天我们就彻底掰开揉碎把Eigen中这三个看似简单、实则关键的成员函数/方法讲清楚。无论你是做机器人SLAM、计算机图形学还是深度学习中的自定义层只要用到Eigen进行数值计算理解它们之间的区别都是至关重要的基本功。这不仅能帮你写出正确的代码更能让你理解Eigen库“表达式模板”这一核心设计思想所带来的编程范式差异。2. 核心概念深度解析2.1norm()向量的“尺子”norm()函数最简单它的任务就是测量返回一个标量值。对于向量来说它计算的就是向量的范数Norm默认情况下是L2范数也就是我们常说的欧几里得长度Euclidean Length或模长。假设我们有一个三维向量v [x, y, z]那么v.norm()计算的就是sqrt(x*x y*y z*z)。它的函数签名通常是Scalar norm() const这意味着它是一个常量函数不会改变向量本身只是读取数据并返回一个计算结果。为什么需要norm()它的应用场景非常广泛判断向量是否为零向量if (v.norm() epsilon)是比逐个比较分量更可靠的方法。计算距离向量(a - b).norm()就是点a和点b之间的欧氏距离。作为归一化的前提在调用normalize()或normalized()之前通常需要检查norm()是否为零以避免除以零的错误。损失函数在机器学习中L2正则化项就是权值向量w的norm()的平方即w.squaredNorm()。这里有一个重要的效率提示如果你只需要比较范数的大小或者需要范数的平方值比如计算距离的平方以避免开方运算请优先使用squaredNorm()函数。因为squaredNorm()直接返回各分量平方和省去了耗时的开方操作在性能敏感的循环中这能带来显著的提升。Eigen::Vector3d v(1.0, 2.0, 3.0); double length v.norm(); // sqrt(149) sqrt(14) ≈ 3.741 double lengthSquared v.squaredNorm(); // 149 14 // 更高效的比较方式 if (v.squaredNorm() threshold * threshold) { // 避免两次开方 // do something }2.2normalized()返回“标准化副本”的工匠这是最容易引起混淆的一个函数。normalized()是一个常量成员函数它的行为是基于当前向量计算出一个新的、单位长度的向量并返回而当前向量自身的内容丝毫不变。你可以把它理解为一个“工厂方法”或“查询方法”。它的内部逻辑大致是计算当前向量的范数n this-norm()。如果n大于零通常要大于一个极小的精度值则返回一个新的向量其每个分量都是原分量除以n。如果n为零或极小则返回的向量是未定义的可能是零向量也可能依赖于具体实现因此在调用前检查范数是良好的编程习惯。关键点在于它返回的是一个新的、独立的Eigen表达式对象。由于Eigen的延迟求值和表达式模板特性这个返回的对象可能并不会立即计算直到被赋值给某个变量或用于其他计算。但这不影响其“非原地修改”的本质。Eigen::Vector3d v(4.0, 0.0, 0.0); Eigen::Vector3d u v.normalized(); // u 变为 [1, 0, 0] // 此时 v 仍然是 [4.0, 0.0, 0.0]完全没有改变 std::cout v: v.transpose() std::endl; // 输出: 4 0 0 std::cout u: u.transpose() std::endl; // 输出: 1 0 0这种特性使得normalized()非常适合在表达式链或函数式编程风格中使用因为它不会产生副作用。// 链式调用示例计算向量v在单位向量u方向上的投影长度 double projection v.dot(u.normalized()); // 函数式风格直接使用返回的临时对象 Eigen::Vector3d direction (pointB - pointA).normalized();2.3normalize()原地改造的“手术师”与normalized()相对normalize()是一个非常量成员函数它的作用是将调用它的向量对象自身进行单位化操作。也就是说这个函数会修改向量自己的存储内容。调用之后该向量的范数模长将变为1在数值精度允许的范围内方向保持不变。它的返回值通常是void或者一个引用用于支持链式调用但Eigen中通常返回void。Eigen::Vector3d v(4.0, 0.0, 0.0); v.normalize(); // v 自身被修改为 [1.0, 0.0, 0.0] // 此时 v 不再是原来的 [4, 0, 0]而是变成了单位向量。 std::cout v.norm() std::endl; // 输出: 1 (近似)什么时候用normalize()当你明确需要修改当前向量对象并且后续计算都基于其单位化后的状态时使用。例如在迭代算法中你需要反复使用同一个向量变量并且希望它始终保持单位长度。Eigen::Vector3d axisRotation ... // 从某个地方获取旋转轴 axisRotation.normalize(); // 确保它是单位向量后续的旋转矩阵计算才正确 Eigen::AngleAxisd rotation(angle, axisRotation); // 使用修改后的 axisRotation一个致命的陷阱如果你在需要保留原向量的地方错误地使用了normalize()就会导致数据被意外修改这种Bug非常隐蔽。我开头提到的点云配准错误正是源于此。在复杂的代码中一个向量可能被多个部分引用原地修改会产生全局性的副作用。3. 对比总结与核心记忆点我们可以用一个简单的表格来概括三者的核心区别特性norm()normalized()normalize()功能计算范数模长返回单位化后的新向量将自身单位化返回类型Scalar(如double,float)同类型的向量表达式void(或有时返回引用)是否修改自身否否是调用对象要求常量或非常量对象常量或非常量对象必须是非常量对象典型应用场景求长度、距离、判断零值表达式链、需要原向量副本时原地修改确保后续计算使用单位向量记忆口诀norm()是只读的“尺子”量一下给个数。normalized()是“复印机”原稿不动给你一份单位化的复印件。normalize()是“涂改液”直接在原稿上修改把它变成单位向量。关于零向量的处理 这是一个至关重要的边界情况。无论是normalized()还是normalize()对零向量范数为0进行操作在数学上都是没有定义的。Eigen库在默认情况下对于零向量调用normalize()或normalized()可能会导致未定义行为可能保持为零向量也可能得到包含NaN的向量。因此安全的做法总是先检查Eigen::Vector3d v ...; double n v.norm(); if (n std::numeric_limitsdouble::epsilon()) { v.normalize(); // 或 auto u v.normalized(); } else { // 处理零向量的情况例如将其设置为一个默认的单位向量或者抛出错误。 v Eigen::Vector3d::UnitX(); // 示例设置为X轴单位向量 }4. 深入原理表达式模板与延迟求值理解normalized()的行为需要稍微深入一下Eigen的核心魔法表达式模板Expression Templates。这不是为了炫技而是为了解释你可能会遇到的一些看似奇怪的现象并写出更高效的代码。当你在Eigen中写下auto u v.normalized();时u的类型可能并不是Eigen::Vector3d而是一个类似于Eigen::CwiseUnaryOpEigen::internal::scalar_multiple_opdouble, const Eigen::Matrixdouble, 3, 1的复杂类型。这是一个表达式对象它封装了“对向量v进行单位化”这个操作但并没有立即执行计算。优点避免临时变量在复杂的复合表达式如auto result (A * v).normalized() w;中Eigen可以将整个表达式优化为一个循环避免为中间结果(A * v)和(A*v).normalized()分配临时存储空间性能极高。编译时优化编译器能看到整个表达式树可以进行激进的优化。需要注意的“坑”使用auto的类型推导Eigen::Vector3d v(1,2,3); auto u v.normalized(); // u 是一个表达式模板类型不是 Vector3d v(0) 100; // 修改了原始向量 v // 此时由于 u 是 v 的表达式视图u 的值也会随之改变这可能不是你想要的。 std::cout u.norm(); // 可能不是1因为它的计算依赖于已改变的v。解决方案如果你想要一个快照一个与原始数据脱钩的独立向量应该使用具体的类型进行显式赋值或构造Eigen::Vector3d u1 v.normalized(); // 正确触发求值构造新的Vector3d对象 auto u2 v.normalized().eval(); // 正确.eval() 强制立即求值并返回一个临时对象 Eigen::Vector3d u3; u3 v.normalized(); // 正确赋值操作也会触发求值在函数中返回normalized()表达式如果你写了一个函数auto getUnitVec() { return v.normalized(); }并且v是局部变量那么返回的表达式将持有对已销毁的v的悬空引用导致灾难。这种情况下必须用.eval()或返回具体类型。5. 实际应用场景与代码示例5.1 场景一计算光线反射方向图形学在光线追踪中给定入射光方向向量I指向表面和法线向量N单位向量反射方向R的计算公式为R I - 2 * (I·N) * N。这里必须确保N是单位向量。Eigen::Vector3d computeReflection(const Eigen::Vector3d incident, const Eigen::Vector3d normal) { // 确保法线是单位向量。假设输入可能不是我们使用 normalized() 创建副本。 Eigen::Vector3d N normal.normalized(); // 使用副本不修改输入 Eigen::Vector3d I incident; Eigen::Vector3d R I - 2 * I.dot(N) * N; return R; } // 注意这里用 normalized() 更安全因为它不修改输入参数 normal。5.2 场景二迭代法求解特征向量数值计算在幂迭代法中我们反复用矩阵乘以一个向量然后对该向量进行单位化以防止其分量溢出或下溢。Eigen::VectorXd powerIteration(const Eigen::MatrixXd A, int iterations) { Eigen::VectorXd v Eigen::VectorXd::Random(A.cols()); v.normalize(); // 初始随机向量单位化 for (int i 0; i iterations; i) { v A * v; // 矩阵乘法 v.normalize(); // 关键步骤原地单位化为下一次迭代准备 } return v; // 返回近似的主特征向量 } // 注意这里使用 normalize() 是合适的因为我们就是要不断更新 v 这个迭代变量。5.3 场景三构建正交坐标系机器人学给定一个非零的Z轴方向向量例如重力方向或相机光轴需要构建一个完整的3D正交坐标系例如相机坐标系。void buildCoordinateFrame(const Eigen::Vector3d z_axis, Eigen::Vector3d x_axis, Eigen::Vector3d y_axis, Eigen::Vector3d z_axis_unit) { z_axis_unit z_axis.normalized(); // 得到单位化的Z轴 // 找一个不与z_axis_unit平行的辅助向量来构造X轴 Eigen::Vector3d aux(1, 0, 0); if (std::abs(z_axis_unit.dot(aux)) 0.9) { // 如果太平行换一个 aux Eigen::Vector3d(0, 1, 0); } x_axis aux.cross(z_axis_unit).normalized(); // 叉乘得到X轴并单位化 y_axis z_axis_unit.cross(x_axis); // 叉乘得到Y轴已自动正交且单位化 // y_axis 的模长已经是1因为x_axis和z_axis_unit都是单位向量且正交。 }6. 常见问题与排查技巧实录在实际使用中除了概念混淆还会遇到一些具体的问题。下面是我踩过的一些坑和解决方法。6.1 精度问题与零向量判断浮点数计算存在精度误差一个向量的模长理论上应为1但计算后可能是0.9999999999或1.0000000001。直接判断v.norm() 1通常会失败。Eigen::Vector3d v ...; v.normalize(); // 错误的检查方式 if (v.norm() ! 1.0) { // 几乎总是成立因为浮点误差 std::cout Normalization failed? std::endl; } // 正确的检查方式检查其与1的差值是否在可接受的容差范围内 double tolerance 1e-10; if (std::abs(v.norm() - 1.0) tolerance) { // 可能确实出现了问题或者范数原本就非常小 } // 更常见的做法是在归一化前检查原范数是否接近零 if (v.squaredNorm() tolerance * tolerance) { // 处理零向量或近似零向量的情况 } else { v.normalize(); // 安全归一化 }6.2 性能优化何时用squaredNorm()在只需要比较长度大小时使用squaredNorm()避免开方运算是标准的优化手段。// 计算点到一系列点最近的点 Eigen::Vector3d queryPoint ...; std::vectorEigen::Vector3d points ...; double minDistSq std::numeric_limitsdouble::max(); int minIdx -1; for (size_t i 0; i points.size(); i) { double distSq (queryPoint - points[i]).squaredNorm(); // 只用平方和 if (distSq minDistSq) { minDistSq distSq; minIdx i; } } // 最终如果需要实际距离再开方一次 double minDist std::sqrt(minDistSq);6.3 与STL算法结合时的陷阱如果你尝试对包含Eigen向量的容器使用STL算法如std::sort并自定义比较函数例如按长度排序直接使用v.norm()在循环中可能造成性能浪费。因为std::sort会在元素间进行多次比较每次比较都可能调用norm()即计算两次开方。std::vectorEigen::Vector3d vecList ...; // 低效写法每次比较计算两次范数 std::sort(vecList.begin(), vecList.end(), [](const Eigen::Vector3d a, const Eigen::Vector3d b) { return a.norm() b.norm(); // 在排序过程中反复计算 }); // 高效写法预计算范数并存储 std::vectorstd::pairdouble, Eigen::Vector3d* indexedList; for (auto v : vecList) { indexedList.emplace_back(v.squaredNorm(), v); // 用平方范数更好 } std::sort(indexedList.begin(), indexedList.end(), [](const auto p1, const auto p2) { return p1.first p2.first; }); // 排序后再按顺序处理原向量6.4 静态大小与动态大小向量的细微差别Eigen的向量有固定大小如Vector3f和动态大小如VectorXf之分。对于固定大小的小向量像normalize()这样的操作会被Eigen特别优化甚至可能使用SIMD指令。而对于动态向量函数内部会有循环。虽然接口一致但在编写泛型模板代码时意识到这一点有助于理解性能特征。7. 扩展到相关网络热词从搜索热词可以看到大家的问题不仅限于这三个函数。ubuntu eigen版本查询在Ubuntu上如果你通过apt安装了libeigen3-dev可以通过dpkg -s libeigen3-dev查看版本或者直接查看头文件/usr/include/eigen3/Eigen/src/Core/util/Macros.h里面定义了EIGEN_WORLD_VERSIONEIGEN_MAJOR_VERSIONEIGEN_MINOR_VERSION。eigen库安装除了apt-get install libeigen3-dev对于需要最新版本或特定功能推荐从官网下载源码使用CMake编译并安装。在CMakeLists.txt中使用find_package(Eigen3 REQUIRED)和target_link_libraries(your_target Eigen3::Eigen)来链接。注意Eigen是纯头文件库target_link_libraries这一步主要是引入必要的编译定义和路径。batch norm 和layer norm分别用在哪里这与Eigen的normalize有概念关联但属于不同领域深度学习。BatchNorm是对一个Batch内所有样本的同一特征通道进行归一化减均值除方差常用于CNN中可以稳定训练、加速收敛。LayerNorm是对单个样本的所有特征进行归一化常用于RNN/Transformer中对序列长度变化不敏感。它们都是对张量进行标准化操作但归一化的维度不同。在Eigen中你可以通过rowwise()或colwise()操作结合mean()和stddev()来模拟类似的计算但这通常不是Eigen的主场深度学习框架会使用更优化的实现。理解norm,normalize,normalized的区别是高效且正确使用Eigen进行数学运算的基石。它关乎代码的正确性、性能以及你对Eigen设计哲学的理解。下次当你手指悬在键盘上思考该用哪一个时希望这篇文章能让你毫不犹豫地做出选择。记住normalized()给你一个新的希望而normalize()改变的是当下。
返回列表