【问题标题】:Bad matrix inversion algorithm, or implemented incorrectly?矩阵求逆算法不好,或者实现不正确?
【发布时间】:2019-08-27 01:21:59
【问题描述】:

自己尝试在Perl中实现矩阵求逆,发现An Efficient and Simple Algorithm for Matrix Inversion(文章只有两页)。

在我尝试在 Perl 中实现它之后,我发现它不起作用。 我花了 很多 时间试图找出问题所在,所以我得出结论

  • 算法不正确
  • 我误解了算法
  • 我的实现不正确

在展示代码之前,这是一个调试会话,其中包含来自Wikipedia: Inverse Matrix 的示例:

  DB<229> $m=[[2,5],[1,3]]

  DB<230> x invert($m)
pe[0] == 2
(pivot row 0) 2x2:
   2.000   2.500
   1.000   3.000
(pivot column 0) 2x2:
   2.000   2.500
  -0.500   3.000
(rest 0) 2x2:
   2.000   2.500
  -0.500   1.750
(pivot 0) 2x2:
   0.500   2.500
  -0.500   1.750
pe[1] == 1.75
(pivot row 1) 2x2:
   0.500   2.500
  -0.286   1.750
(pivot column 1) 2x2:
   0.500  -1.429
  -0.286   1.750
(rest 1) 2x2:
   0.908  -1.429
  -0.286   1.750
(pivot 1) 2x2:
   0.908  -1.429
  -0.286   0.571
0  1
1  3.5
  DB<231> 

这是我写的代码:

#!/usr/bin/perl -w
use 5.026;
use strict;

# invert matrix
# An Efficient and Simple Algorithm for Matrix Inversion
# Ahmad Farooq, King Khalid University, Saudi Arabia
# Khan Hamid, National University of Computer and Emerging Sciences (NUCES),
# Pakistan
sub invert($)
{
    my $m = shift;          # matrix is an array of rows
    my ($pp, $det);
    my ($rp, $pe);
    my $n = scalar(@$m);

    for ($pp = 0, $det = 1.0; $pp < $n; ++$pp) {
        $rp = $m->[$pp];        # pivot row
        $pe = $rp->[$pp];       # pivot element
        print "pe[$pp] == $pe\n";
        last if ($pe == 0);      # Epsilon test?

        $det *= $pe;
        # calculate pivot row
        for (my $j = 0; $j < $n; ++$j) {
            next if ($j == $pp);

            $rp->[$j] /= $pe;
        }

        pm($m, "pivot row $pp");
        # calculate pivot column
        for (my $i = 0; $i < $n; ++$i) {
            next if ($i == $pp);

            $m->[$i]->[$pp] /= -$pe;
        }

        pm($m, "pivot column $pp");
        for (my $j = 0; $j < $n; ++$j) {
            next if ($j == $pp);

            for (my ($i, $rj) = (0, $m->[$j]); $i < $n; ++$i) {
                next if ($i == $pp);

                $rj->[$i] += $rp->[$j] * $m->[$i]->[$pp];
            }
        }

        pm($m, "rest $pp");
        $rp->[$pp] = 1.0 / $pe;
        pm($m, "pivot $pp");
    }

    return ($pe != 0.0, $det);
}

pm() 函数只是一个用于调试目的的“打印矩阵”:

# print matrix
sub pm($;$)
{
    my ($m, $label) = @_;
    my $n = scalar(@$m);
    print "($label) " if ($label);
    print "${n}x${n}:\n";
    for (my $i = 0; $i < $n; ++$i) {
        for (my $j = 0; $j < $n; ++$j) {
            if (defined(my $v = $m->[$i]->[$j])) {
                printf('%8.3f', $v);
            } else {
                print ' ???????';
            }
        }

        print "\n";
    }
}

有什么见解吗?

复制提示(添加于 2019-08-28)

我认为这很明显,但以防万一: 如果你想重现调试会话中显示的输出,也许只需在代码末尾添加这两行:

my $m=[[2,5],[1,3]];                 # matrix to invert
print join(', ', invert($m)), "\n";  # invert $m, printing result

注意(添加于 2019-09-02):

对于 Wikipedia 文章 ($m = [[1, 2, 0], [2, 4, 1], [2, 1, 0]]) 中给出的 3x3 矩阵,该算法失败,因此真正的实现应该转向改进的算法(可以选择对角线之外的枢轴元素)。

【问题讨论】:

  • use strictuse 5.026 之后是多余的,但是您缺少use warnings
  • 也许您可以将示例输入矩阵和反转矩阵与脚本的输出一起发布,以便我们查看是否存在错误模式。
  • @lordadmira:你能说出第一个代码块中给出的例子有什么问题吗?
  • 抱歉,我并没有真正关注调试器的输出。不幸的是,我还没有时间研究算法。
  • 我认为问题出在参考论文的第 7 步。请注意,a[i,p] 上有一个素数,但a[p,j] 上没有。所以我猜你必须保存a[p,j]的元素?

标签: perl matrix inverse


【解决方案1】:

如有疑问,请编写测试。

首先,将您的代码放入模块中(lib/My/Matrix.pm 或任何您想调用的名称):

package My::Matrix; # this must match the file name

use strict;
use warnings;
use Exporter qw(import);

our @EXPORT_OK = qw( invert pm );

# your code here ...

1; # at end of module

有很多关于编写模块的文档,不确定perldoc perlmod 是否是一个好的起点。

现在写一个测试——文档是here (t/001-invert.t):

#!perl

use strict;
use warnings;
use Test::More;

use Matrix qw(invert);

ok_invert( [[1,0], [0,1]], [[1,0], [0,1]], "unit matrix" );
# insert more matrices here    

done_testing;

sub ok_invert {
    my ($input, $output, $msg) = @_;

    invert( $output );
    is_deeply $input, $output, $msg
         or diag "got: ", explain $input, "expected: ", explain $output;
};

如果您想运行多个测试,请以perl -Ilib t/001-invert.tprove -Ilib t 运行测试。

您现在可以在测试中添加简单的极端情况,直到问题被隔离。

当然,手动查找正确的逆矩阵很繁琐,因此您可能希望使用乘法。因此,改进代码的下一步是:

  • 确保 invert 不会修改其输入,而是返回反转矩阵;

旁注。确保函数returns the desired value and does not modify its arguments 通常是个好主意。这并不总是可行的,但如果可行,它会节省大量的调试时间。

  • 实现乘法;
  • 实现is_unit_matrix检查;
  • 重写测试函数如下(下一个sn-p没测试):
sub ok_invert {
    my ($input, $msg) = @_;
    my ($invert, $det) = invert( $input );
    ok is_unit_matrix( multiply( $invert, $input ) ), $msg
        or diag explain $invert, " is not the inverse of ", explain $input;
}

希望这会有所帮助。

【讨论】:

  • 对不起,问题不是如何编写模块或如何测试模块;我想知道提出的算法是否不好,或者我是否误解了算法,或者我是否看不到明显的东西。您的回答在这里对我没有帮助。
  • @U.Windl 因此,您正在寻找一种方法来确定算法(实现)是好是坏。有多种方法可以做到这一点 - 测试、调试、日志记录(您使用 pm 进行)、代码检查(您似乎要求这样做)、结对编程、手动证明正确性或使用证明助手等 - 但是编写快速测试是最省力的方法。它很简单,不涉及其他人,易于复制,并且留下了有用的工件。
  • 至于我的建议是使用模块,当然这可能有点矫枉过正,但是您拥有具有明确行为的自包含代码,而模块只是这类事情的自然栖息地。而且为模块编写测试比为脚本编写测试更容易。
  • 你看报纸了吗?该算法发表在杂志上,据说有正确性证明,但不幸的是,公开部分中缺少该部分。所以我认为证明它的正确性没有意义。特别是如果可以的话,我会编写自己的算法,而不是依赖已发布的算法。
  • @Helmut Wollmersdorfer,我们已经知道测试用例会失败。问题是他们为什么会失败。 Dallaylean 对 OP 一点帮助都没有。
【解决方案2】:

原论文中的伪代码不正确。

到目前为止我已经完成的步骤:

  • 将伪代码插入到 Perl 源代码中
  • 使用了伪代码的命名
  • 测试用例
  • 测试了测试用例
  • Math::Matrix 为参考进行测试
  • 审核(多次)

至少我读过纸上的笔记:

注意在下面算法的第7步a'[i, p] 在 LHS 上意味着枢轴行的最新值 将用于计算。

这个注释并不准确。经过额外的尝试,我放弃了,想在这里发布我的发现,并阅读 Håkon Hægland 的答案。是的,他的解决方案奏效了,他赢得了荣誉。

如果伪代码中的步骤被重新排序,它会通过我的 3 个测试:

Step 1
Step 2
Step 3
Step 4
Step 6
Step 7
Step 5
Step 8
Step 9
Step 10

这是包含伪代码并使用原始命名的版本:

sub invert_corr($) {
    my $A = shift;          # matrix is an array of rows
    my $n = scalar(@$A);

    # Step 1: Let p = 0, d = 1;
    my $p   = 0;
    my $det;
    # Step 2: p <= p +1
    for (my $pi = 0,$det = 1.0; $pi < $n; ++$pi) {
        $p = $pi;

        # Step 3: If a[p,p] == 0 then cannot calculate inverse, go to step 10.
        if ($A->[$p]->[$p] == 0) { last; }

        # Step 4: d <= d x a[p, p]
        $det = $det * $A->[$p]->[$p];

        # Step 6: Calculate the new elements of the pivot column by:
        #   a_new[i,p] <= -(a[i,p] / a[p,p]) where  i = 1 .. n, i != p
        STEP6: for (my $i = 0; $i < $n; ++$i) {
            if ($i == $p) { next STEP6; }
            $A->[$i]->[$p] = -($A->[$i]->[$p] / $A->[$p]->[$p]);
        }

        # Step 7: Calculate the rest of the new elements by:
        #   a_new[i,j] <= a[i,j] + a[p,j] x a_new[i,p]
        #     where i = 1 .. n, j = 1 .. n, & i,j != p
        OUTER7: for (my $i = 0; $i < $n; ++$i) {
            if ($i == $p) { next OUTER7; }
            INNER7: for (my $j = 0; $j < $n; ++$j) {
                if ($j == $p) { next INNER7; }
                # Note that in step 7 of the following algorithm a'[i, p]
                # on the LHS means that the latest value of the pivot row
                # is to be used in the calculations.
                $A->[$i]->[$j] = $A->[$i]->[$j] + $A->[$p]->[$j] * $A->[$i]->[$p];
            }
        }

        # Step 5: Calculate the new elements of the pivot row by:
        #   a_new[p,j] <= a[p,j] / a[p,p] where  j = 1 .. n, j != p
        STEP5: for (my $j = 0; $j < $n; ++$j) {
            # next if ($j == $p);
            if ($j == $p) { next STEP5; }
            $A->[$p]->[$j] = $A->[$p]->[$j] / $A->[$p]->[$p];
        }

        # Step 8: Calculate the new value of the current pivot location:
        #   a_new[p,p] <= 1 / a_new[p,p]
        $A->[$p]->[$p] = 1.0 / $A->[$p]->[$p];

        # Step 9: If p < n go to step 2 (n the dimension of the matrix A).
    }

    # Step 10: Stop. If inverse exists, A contains the inverse and d is the determinant.
    if ($A->[$p]->[$p] != 0.0) {
        return ($A->[$p]->[$p] != 0.0, $det, $A);
    }
    return ($A->[$p]->[$p] != 0.0);
}

包含测试的完整代码可在github 上找到,可能对调试有用。

【讨论】:

  • 那么我们可以同意算法的描述至少是“差”吗?
  • @U.Windl 是的,描述很差,可能具有误导性。根据我的经验,这适用于 50% 的数学家撰写的科学论文。如果没有流行语言的代码(或明确的伪代码),就很难验证论文中的主张。
【解决方案3】:

根据引用的论文,第 7 步应该使用旧的枢轴行值计算,所以以下似乎对我有用:

sub invert($)
{
    my $m = shift;          # matrix is an array of rows
    my ($pp, $det);
    my ($rp, $pe);
    my $n = scalar(@$m);

    for ($pp = 0, $det = 1.0; $pp < $n; ++$pp) {
        $rp = $m->[$pp];        # pivot row
        $pe = $rp->[$pp];       # pivot element
        last if ($pe == 0);      # Epsilon test?

        $det *= $pe;
        # calculate pivot column
        for (my $i = 0; $i < $n; ++$i) {
            next if ($i == $pp);
            $m->[$i][$pp] /= -$pe;
        }
        for (my $j = 0; $j < $n; ++$j) { # row index
            next if ($j == $pp);
            for (my ($i, $rj) = (0, $m->[$j]); $i < $n; ++$i) {
                next if ($i == $pp);
                $rj->[$i] += $rp->[$i] * $m->[$j]->[$pp];
            }
        }
        # calculate pivot row
        for (my $j = 0; $j < $n; ++$j) {
            next if ($j == $pp);
            $rp->[$j] /= $pe;
        }
        $rp->[$pp] = 1.0 / $pe;
    }

    return ($pe != 0.0, $det);
}

需要修复以匹配维基百科中的结果:

--- newinvert.pl~   2019-08-29 21:22:16.135160055 +0200
+++ newinvert.pl    2019-08-29 21:32:10.995144732 +0200
@@ -20,7 +20,7 @@
             next if ($j == $pp);
             for (my ($i, $rj) = (0, $m->[$j]); $i < $n; ++$i) {
                 next if ($i == $pp);
-                $rj->[$i] += $rp->[$i] * $m->[$j]->[$pp];
+                $rj->[$i] += $rp->[$j] * $m->[$i]->[$pp];
             }
         }
         # calculate pivot row

示例会话(包括我的pm()):

> perl -d printmatrix.pl

Loading DB routines from perl5db.pl version 1.51
Editor support available.

Enter h or 'h h' for help, or 'man perldebug' for more help.

main::(printmatrix.pl:20):  1;
  DB<1> require "./newinvert.pl" # this is ungly, forgive!
./newinvert.pl did not return a true value at (eval 6)[/usr/lib/perl5/5.26.1/perl5db.pl:738] line 2.

  DB<2> $m=[[2,5],[1,3]]
  DB<4> pm($m)
2x2:
   2.000   5.000
   1.000   3.000

  DB<5> x invert($m)
0  1
1  1
  DB<6> pm($m)
2x2:
   3.000  -5.000
  -1.000   2.000

回归测试结果:

# https://github.com/wollmers/matrix-inverse-Farooq/blob/master/matrix_inversion_new.pl

$ perl matrix_inversion_new.pl
[...]
(invert_hakon 01_wiki input $A) 2x2:
   2.000   5.000
   1.000   3.000
(invert_hakon 01_wiki result $C) 2x2:
   3.000  -5.000
  -1.000   2.000
ok 10 - 01_wiki invert_hakon Ainv
ok 11 - 01_wiki invert_hakon det: 1
(invert_hakon 02_wiki input $A) 2x2:
   2.000   3.000
   1.000   2.000
(invert_hakon 02_wiki result $C) 2x2:
   2.000  -3.000
  -1.000   2.000
ok 12 - 02_wiki invert_hakon Ainv
ok 13 - 02_wiki invert_hakon det: 1
(invert_hakon 03_author_1 input $A) 3x3:
   1.000   1.000   3.000
   1.000   3.000  -3.000
  -2.000  -4.000  -4.000
(invert_hakon 03_author_1 result $C) 3x3:
   3.000   1.000   1.500
  -1.250  -0.250  -0.750
  -0.250  -0.250  -0.250
ok 14 - 03_author_1 invert_hakon Ainv
ok 15 - 03_author_1 invert_hakon det: -8
[...]

【讨论】:

  • 您在正确的轨道上,但您的版本没有产生维基百科文章所说的内容。但是,如果您撤消索引 $i$j 的交换,则结果与 Wikipedia 匹配。事实证明,您基本上在第 7 步之后移动了算法的第 5 步来解决问题。由于 cmets 的能力有限,我会劫持你的答案以将更正放在那里。
  • 好的,太好了!顺便说一句,我在 3x3 矩阵上测试了代码并产生了正确的结果,所以我不确定交换这些索引..
  • I tested the original script on the 2x2 matrix,它给出的结果与您为修改后的脚本提供的结果相同,您能看出问题所在吗?
  • 我现在完全糊涂了:当我第一次尝试时,矩阵在对角线上镜像,所以我交换了你交换回来的索引。那时结果是正确的,但是现在当我尝试重现时,您的代码结果也是正确的! $rj-&gt;[$i] 是 a[i,j],$rp-&gt;[$i] 是 a[p,i](但应该是 a[p,j]),而 $m-&gt;[$j]-&gt;[$pp] 是 a[j,p]。为了匹配第 7 步,i 和 j 应该交换恕我直言。
  • @U.Windl 这个原始版本是正确的,并且通过了我在回答中所写的 3 个测试用例。在 Github 上查看我的代码,在那里我对测试用例 is_deeply( matmult($A, $Ainv), $I ) 进行了完整性检查,其中 $I 是身份矩阵。这些案例还针对不同的实现进行了测试:我的invert_corr()invert_hakon()Math::Matrix-&gt;invert()。可能你很困惑,因为$A是原地改的,如果在同一个数组上做了多个invert(),就需要克隆$Ainv = invert( matclone($A) )
【解决方案4】:

实现数学公式时的问题之一(实际上来自步骤 5

在一个循环中是:“新”a' 何时会变成“旧”a
在下一步之前,在下一次赋值之前,还是在下一次循环迭代之前?

在数学意义上,a'a 一直是不同的变量,但在过程编程语言中,a 的内存地址有时会被重用。

看来,Step 5 的作业需要延迟到Step 7 () 之后。

作为一名计算机科学家,我一直觉得数学算法的描述方式有些不精确。也许这正是发明编程语言的原因;-)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-05-11
    • 1970-01-01
    • 2017-11-24
    • 2014-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多