矩阵反转算法错误,或者实现不正确?

时间:2019-08-27 01:21:59

标签: perl matrix inverse

尝试自己在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矩阵,该算法失败,因此实际的实现应朝着改进的算法发展(可以选择对角线之外的枢轴元素)。

4 个答案:

答案 0 :(得分:3)

如有疑问,请编写测试。

首先,将您的代码放入模块(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检查;
  • 按如下所示重写测试功能(下一个代码段未测试):
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;
}

希望这会有所帮助。

答案 1 :(得分:2)

根据所引用的论文,应该使用旧的枢轴行值来计算第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
[...]

答案 2 :(得分:2)

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

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

  • 将伪代码插入Perl源文件中
  • 使用了伪代码的命名
  • 测试案例
  • 测试了测试用例
  • Math::Matrix作为参考进行了测试
  • 评论(很多次)

至少我读过纸上的笔记:

  

请注意,在以下算法的步骤7中,a'[i,p]   LHS上的表示枢轴行的最新值   将在计算中使用。

此注释并不十分准确。经过多次尝试,我放弃了,想在这里发表我的发现,并阅读HåkonHæ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上提供了包含测试的完整代码,可能对调试很有用。

答案 3 :(得分:1)

实现数学公式(实际上来自步骤5 )时遇到的问题之一
enter image description here
一个循环是:“新” a'何时会变成“旧” a
在下一步之前,下一次分配之前还是在下一次循环迭代之前?

从数学上讲,a'a始终是不同的变量,但是在过程编程语言中,a的内存地址有时会被重用。

看来,步骤5 中的分配需要在步骤7 enter image description here)之后延迟。

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