【问题标题】:Dot product of 2 vectors C++2个向量的点积C++
【发布时间】:2020-10-19 08:17:21
【问题描述】:

我必须编写将输出两个向量的点积的程序。

仅使用 Double 类型组织计算,以获得尽可能准确的结果。

输入应该是什么样子:

   N - vector length
   x1, x2,..., xN      co-ordinates of vector x (double type)
   y1, y2,..., yN      co-ordinates of vector y (double type)

输入示例:

   4
   1.0e20 -1.0e3 0.1 1.0e20
   1.0 4.0 -4.0 -1.0

以上向量的输出:

   -4000.4 

还有我的代码(我还没有使用 cin,因为起初我想用示例输入编写工作程序):

#include <iostream>
#include <numeric>
#include <vector>
#include <functional>

int main(){
//double N; //length of both vectors , will be used when I will have to input vectors by cin
//std::cin >> N;
//N = 4;

std::vector<double> x{1.0e20, -1.0e3, 0.1, 1.0e20};
std::vector<double> y{1.0, 4.0, -4.0, -1.0};
double result = std::inner_product(x.begin(), x.end(), y.begin(), 0);

std::cout << result;
return 0;
}

我的输出是 -2.14748e+09,所以它甚至不接近预期的输出。我应该怎么做才能让它工作?

【问题讨论】:

  • 尝试检查每个乘法的结果和每一步的部分和,我强烈怀疑是浮点精度问题。
  • 溢出或精度问题。
  • 应该是std::inner_product(x.begin(), x.end(), y.begin(), 0.0);。注意 double 文字 0.0 而不是 int 文字 0 作为最后一个参数。
  • 要获得最准确的结果,您需要注意对产品求和的顺序。
  • double N; //length of both vectors 长度应该是整数,而不是双精度数。 “标准”长度类型为size_t

标签: c++ floating-point


【解决方案1】:

(第一个)问题

这是&lt;numeric&gt;中内积的函数模板:

template <class InputIterator1, class InputIterator2, class T>
   T inner_product (InputIterator1 first1, InputIterator1 last1,
                    InputIterator2 first2, T init);

请注意,定义输出类型T 的是init 参数。因此,鉴于您的意见:

std::inner_product(x.begin(), x.end(), y.begin(), 0);

init = 0,因此T 类型为int。因此,当算法运行时,它会将double 值类型转换为ints,最终将返回一个未定义的int 值。

“修复”和第二个问题

要解决此问题,您所要做的就是提供一个正确键入的 init 值(即,提供一个 double 作为 init 参数)。只需0.0 即可:

std::inner_product(x.begin(), x.end(), y.begin(), 0.0);

现在,当您使用该修复程序编译并运行程序时,它仍然会输出不正确的结果0

这是因为当inner_product 函数累积值时,它使用标准的double 加法来完成。因此,您需要遵守标准 double 不精确度、which has a machine epsilon 的 2^(-52) — 2.22E-16 或小数点后十六位的不精确度 — 这意味着对于数字 1E20,(1E20 + x ) = 1E20 对于所有 x

为了说明这一点,让我们在python解释器中添加1E20 + 23000(提醒python使用IEEE-754 floating point arithmetic,等于标准C++编译器中double的精度):

>>> 1e20 + 23000
1.0000000000000002e+20

所以你看到任何少于两万的东西都被忽略/“吸收”了。

由于您的其他数字小于 22204.46,1e20 只会“吸收”它们,直到它被添加到 -1E20,然后它将“取消”并返回 0

(简单的)修复

解决第二个问题的最简单方法是使用long double 而不是double。这种更精确的双精度类型的机器 epsilon 为 2^(-63) — 1.08E-19 或大约十九位小数 — 这意味着,对于您的输入 1E20,不精度将等于 2^(-63) *1E20,或大约 10.84。运行程序,输出将是-4000,与预期的答案相当接近。 但这可能不是您的教授所期望的,因为他特别要求输出准确 -4000.4

注意:显然,您可以选择另一种更精确的数字类型,但您的教授可能希望您使用double,所以我不会详细说明。

编辑: 正如 cmets 中提到的 @phuclvsome compilers 不会将 long double 实现为 80 位浮点值,而是可能具有与 @ 相同的精度987654356@(64 位)。因此,您可能必须寻找提供适当 80 位精度 long doubles 甚至 128-bit IEEE-754 quadruple-precision 浮点类型的库。虽然这肯定不会被认为是“容易的”。

(大部分正确的)修复

嗯,你不能无限精确,因为double 类型有 epsilon = 2^(-52),但你可以更聪明地加法,而不只是将大值添加到小值(请记住:由于double 浮点运算中的不精确性,大值会“吸收”小值)。基本上,您应该计算一个具有成对乘法的值的数组,然后对其进行排序(基于绝对值),然后使用std::accumulate 将值相加:

#include <iostream>
#include <numeric>
#include <vector>
#include <functional>
//Mind the use of these two new STL libraries
#include <algorithm> //std::sort and std::transform
#include <cmath> //abs()



int main(){

    std::vector<double> x{1.0e20, -1.0e3, 0.1, 1.0e20};
    std::vector<double> y{1.0, 4.0, -4.0, -1.0};
    //The vector with the pairwise products
    std::vector<double> products(x.size());

    //Do element-wise multiplication
    //C code: products[i] += x[i] * y[i];
    std::transform(x.begin(), x.end(), y.begin(), products.begin(), std::multiplies<double>());

    //Sort the array based on absolute-value
    auto sort_abs = [] (double a, double b) { return abs(a) < abs(b); };
    std::sort(products.begin(), products.end(), sort_abs);

    //Add the values of the products(note the init=0.0)
    double result = std::accumulate(products.begin(), products.end(), 0.0);

    std::cout << result << std::endl;
    return 0;
}

有了这个新代码,结果如预期:-4000.4

Tough 它显然有它的局限性。例如,如果输入是向量 v1 = {100.0, 1E20} 和 v2 = {10.0, 1.0},结果应该返回 100000000000000001000,显然只会返回 1E20。

【讨论】:

  • 请注意,long double 不能保证比double 具有更高的精度。例如在 MSVC 和(几乎)所有微控制器上,它们具有相同的精度
  • 感谢您的回答。现在我明白它应该如何工作了。我忘记了 Double 的限制。最后我对向量有了更多的了解。
  • @phuclv 哦,谢谢,不知道。我会将其添加到我的答案中。
【解决方案2】:

发布的 sn-p 中存在逻辑错误和一些数字问题。

  • std::inner_product 使用传递的初始值初始化累加器,因此它使用相同的类型 a 和返回值。发布的代码使用整数 0,而应使用浮点值,如 0.0
  • 向量中的值具有非常广泛的幅度。像double 这样的浮点类型具有有限的精度,它不能代表所有可能的实数而没有舍入误差。此外(正因为如此)浮点数学运算不具有关联性,并且对它们的执行顺序不敏感。

想上图,可以run以下sn-p。

#include <numeric>
#include <algorithm>
#include <array>
#include <fmt/core.h> // fmt::print

int main()
{
    using vec4d = std::array<double, 4>;
    
    vec4d x{1.0e20, 1.0e20, -1.0e3, 0.1};
    vec4d y{1.0, -1.0, 4.0, -4.0};
    
    vec4d z;
    std::transform( std::begin(x), std::end(x), std::begin(y), std::begin(z)
                  , std::multiplies<double>{} );
    std::sort(std::begin(z), std::end(z));

    fmt::print("{0:>{1}}\n", "sum", 44);
    fmt::print("{0:->{1}}", '\n', 48);
    do {
        for (auto i : z) {
            fmt::print("{0:8}", i);
        }
        auto sum{ std::accumulate(std::begin(z), std::end(z), 0.0) };
        fmt::print("{0:{1}.{2}f}\n", sum, 14, 1);
    } while ( std::next_permutation(std::begin(z), std::end(z)) );
}

这是它的输出:

                                         sum
-----------------------------------------------
  -1e+20   -4000    -0.4   1e+20           0.0
  -1e+20   -4000   1e+20    -0.4          -0.4
  -1e+20    -0.4   -4000   1e+20           0.0
  -1e+20    -0.4   1e+20   -4000       -4000.0
  -1e+20   1e+20   -4000    -0.4       -4000.4
  -1e+20   1e+20    -0.4   -4000       -4000.4
   -4000  -1e+20    -0.4   1e+20           0.0
   -4000  -1e+20   1e+20    -0.4          -0.4
   -4000    -0.4  -1e+20   1e+20           0.0
   -4000    -0.4   1e+20  -1e+20           0.0
   -4000   1e+20  -1e+20    -0.4          -0.4
   -4000   1e+20    -0.4  -1e+20           0.0
    -0.4  -1e+20   -4000   1e+20           0.0
    -0.4  -1e+20   1e+20   -4000       -4000.0
    -0.4   -4000  -1e+20   1e+20           0.0
    -0.4   -4000   1e+20  -1e+20           0.0
    -0.4   1e+20  -1e+20   -4000       -4000.0
    -0.4   1e+20   -4000  -1e+20           0.0
   1e+20  -1e+20   -4000    -0.4       -4000.4
   1e+20  -1e+20    -0.4   -4000       -4000.4
   1e+20   -4000  -1e+20    -0.4          -0.4
   1e+20   -4000    -0.4  -1e+20           0.0
   1e+20    -0.4  -1e+20   -4000       -4000.0
   1e+20    -0.4   -4000  -1e+20           0.0

请注意,“正确”答案 -4000.4 仅在较大的项(1e+20 和 -1e+20)在 first 求和中抵消时出现。这是一个人工制品,因为选择了特定的数字作为输入,其中两个最大的数字在大小上相等并且也具有相反的符号。一般来说,减去两个几乎的数字会导致灾难性的取消和重要性的丧失。

次优结果 -4000.0 发生在较小的值 0.4 与最大的值“接近”并且被抵消时。

在对许多项求和时,可以采用各种技术来减少不断增长的数值误差,例如 pairwise summation 或补偿求和(参见例如 Kahan summation)。

Here,我用相同的样本测试了 Neumaier 求和。

【讨论】:

  • 非常感谢。现在更容易理解它是如何工作的。
猜你喜欢
  • 1970-01-01
  • 2012-02-27
  • 2021-12-15
  • 1970-01-01
  • 2011-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多