perl-package/AI-MXNet/lib/AI/MXNet/TestUtils.pm (313 lines of code) (raw):
package AI::MXNet::TestUtils;
use strict;
use warnings;
use PDL;
use Carp;
use Scalar::Util qw(blessed);
use AI::MXNet::Function::Parameters;
use Exporter;
use base qw(Exporter);
@AI::MXNet::TestUtils::EXPORT_OK = qw(same reldiff almost_equal GetMNIST_ubyte
GetCifar10 pdl_maximum pdl_minimum mlp2 conv
check_consistency zip assert enumerate same_array dies_like);
use constant default_numerical_threshold => 1e-6;
=head1 NAME
AI::MXNet::TestUtils - Convenience subs used in tests.
=head2 same
Test if two pdl arrays are the same
Parameters
----------
a : pdl
b : pdl
=cut
func same(PDL $a, PDL $b)
{
return ($a != $b)->sum == 0;
}
=head2 reldiff
Calculate the relative difference between two input arrays
Calculated by :math:`\\frac{|a-b|_1}{|a|_1 + |b|_1}`
Parameters
----------
a : pdl
b : pdl
=cut
func reldiff(PDL $a, PDL $b)
{
my $diff = sum(abs($a - $b));
my $norm = sum(abs($a)) + sum(abs($b));
if($diff == 0)
{
return 0;
}
my $ret = $diff / $norm;
return $ret;
}
=head2 almost_equal
Test if two pdl arrays are almost equal.
=cut
func almost_equal(PDL $a, PDL $b, Maybe[Num] $threshold=)
{
$threshold //= default_numerical_threshold;
my $rel = reldiff($a, $b);
return $rel <= $threshold;
}
func GetMNIST_ubyte()
{
if(not -d "data")
{
mkdir "data";
}
if (
not -f 'data/train-images-idx3-ubyte'
or
not -f 'data/train-labels-idx1-ubyte'
or
not -f 'data/t10k-images-idx3-ubyte'
or
not -f 'data/t10k-labels-idx1-ubyte'
)
{
`wget http://data.mxnet.io/mxnet/data/mnist.zip -P data`;
chdir 'data';
`unzip -u mnist.zip`;
chdir '..';
}
}
func GetCifar10()
{
if(not -d "data")
{
mkdir "data";
}
if (not -f 'data/cifar10.zip')
{
`wget http://data.mxnet.io/mxnet/data/cifar10.zip -P data`;
chdir 'data';
`unzip -u cifar10.zip`;
chdir '..';
}
}
func _pdl_compare(PDL $a, PDL|Num $b, Str $criteria)
{
if(not blessed $b)
{
my $tmp = $b;
$b = $a->copy;
$b .= $tmp;
}
my $mask = {
'max' => sub { $_[0] < $_[1] },
'min' => sub { $_[0] > $_[1] },
}->{$criteria}->($a, $b);
my $c = $a->copy;
$c->where($mask) .= $b->where($mask);
$c;
}
func pdl_maximum(PDL $a, PDL|Num $b)
{
_pdl_compare($a, $b, 'max');
}
func pdl_minimum(PDL $a, PDL|Num $b)
{
_pdl_compare($a, $b, 'min');
}
func mlp2()
{
my $data = AI::MXNet::Symbol->Variable('data');
my $out = AI::MXNet::Symbol->FullyConnected(data=>$data, name=>'fc1', num_hidden=>1000);
$out = AI::MXNet::Symbol->Activation(data=>$out, act_type=>'relu');
$out = AI::MXNet::Symbol->FullyConnected(data=>$out, name=>'fc2', num_hidden=>10);
return $out;
}
func conv()
{
my $data = AI::MXNet::Symbol->Variable('data');
my $conv1 = AI::MXNet::Symbol->Convolution(data => $data, name=>'conv1', num_filter=>32, kernel=>[3,3], stride=>[2,2]);
my $bn1 = AI::MXNet::Symbol->BatchNorm(data => $conv1, name=>"bn1");
my $act1 = AI::MXNet::Symbol->Activation(data => $bn1, name=>'relu1', act_type=>"relu");
my $mp1 = AI::MXNet::Symbol->Pooling(data => $act1, name => 'mp1', kernel=>[2,2], stride=>[2,2], pool_type=>'max');
my $conv2 = AI::MXNet::Symbol->Convolution(data => $mp1, name=>'conv2', num_filter=>32, kernel=>[3,3], stride=>[2,2]);
my $bn2 = AI::MXNet::Symbol->BatchNorm(data => $conv2, name=>"bn2");
my $act2 = AI::MXNet::Symbol->Activation(data => $bn2, name=>'relu2', act_type=>"relu");
my $mp2 = AI::MXNet::Symbol->Pooling(data => $act2, name => 'mp2', kernel=>[2,2], stride=>[2,2], pool_type=>'max');
my $fl = AI::MXNet::Symbol->Flatten(data => $mp2, name=>"flatten");
my $fc2 = AI::MXNet::Symbol->FullyConnected(data => $fl, name=>'fc2', num_hidden=>10);
my $softmax = AI::MXNet::Symbol->SoftmaxOutput(data => $fc2, name => 'sm');
return $softmax;
}
=head2 check_consistency
Check symbol gives the same output for different running context
Parameters
----------
sym : Symbol or list of Symbols
symbol(s) to run the consistency test
ctx_list : list
running context. See example for more detail.
scale : float, optional
standard deviation of the inner normal distribution. Used in initialization
grad_req : str or list of str or dict of str to str
gradient requirement.
=cut
my %dtypes = (
float32 => 0,
float64 => 1,
float16 => 2,
uint8 => 3,
int32 => 4
);
func check_consistency(
SymbolOrArrayOfSymbols :$sym,
ArrayRef :$ctx_list,
Num :$scale=1,
Str|ArrayRef[Str]|HashRef[Str] :$grad_req='write',
Maybe[HashRef[AI::MXNet::NDArray]] :$arg_params=,
Maybe[HashRef[AI::MXNet::NDArray]] :$aux_params=,
Maybe[HashRef[Num]|Num] :$tol=,
Bool :$raise_on_err=1,
Maybe[AI::MXNer::NDArray] :$ground_truth=
)
{
$tol //= {
float16 => 1e-1,
float32 => 1e-3,
float64 => 1e-5,
uint8 => 0,
int32 => 0
};
$tol = {
float16 => $tol,
float32 => $tol,
float64 => $tol,
uint8 => $tol,
int32 => $tol
} unless ref $tol;
Test::More::ok(@$ctx_list > 1);
if(blessed $sym)
{
$sym = [($sym)x@$ctx_list];
}
else
{
Test::More::ok(@$sym == @$ctx_list);
}
my $output_names = $sym->[0]->list_outputs;
my $arg_names = $sym->[0]->list_arguments;
my @exe_list;
zip(sub {
my ($s, $ctx) = @_;
Test::More::is_deeply($s->list_arguments, $arg_names);
Test::More::is_deeply($s->list_outputs, $output_names);
push @exe_list, $s->simple_bind(grad_req=>$grad_req, %$ctx);
}, $sym, $ctx_list);
$arg_params //= {};
$aux_params //= {};
my %arg_dict = %{ $exe_list[0]->arg_dict };
while(my ($n, $arr) = each %arg_dict)
{
if(not exists $arg_params->{$n})
{
$arg_params->{$n} = random(reverse @{ $arr->shape })*$scale;
}
}
my %aux_dict = %{ $exe_list[0]->aux_dict };
while(my ($n, $arr) = each %aux_dict)
{
if(not exists $aux_params->{$n})
{
$aux_params->{$n} = 0;
}
}
for my $exe(@exe_list)
{
%arg_dict = %{ $exe->arg_dict };
while(my ($name, $arr) = each %arg_dict)
{
$arr .= $arg_params->{$name};
}
%aux_dict = %{ $exe->aux_dict };
while(my ($name, $arr) = each %aux_dict)
{
$arr .= $aux_params->{$name};
}
}
my @dtypes = map { $_->outputs->[0]->dtype } @exe_list;
my $max_idx = pdl(map { $dtypes{$_} } @dtypes)->maximum_ind;
my $gt = $ground_truth;
if(not defined $gt)
{
$gt = { %{ $exe_list[$max_idx]->output_dict } };
if($grad_req ne 'null')
{
%{$gt} = (%{$gt}, %{ $exe_list[$max_idx]->grad_dict });
}
}
# test
for my $exe (@exe_list)
{
$exe->forward(0);
}
enumerate(sub {
my ($i, $exe) = @_;
if($i == $max_idx)
{
return;
}
zip(sub {
my ($name, $arr) = @_;
my $gtarr = $gt->{$name}->astype($dtypes[$i])->aspdl;
$arr = $arr->aspdl;
Test::More::ok(
almost_equal(
$arr, $gtarr,
$tol->{$dtypes[$i]}
)
);
}, $output_names, $exe->outputs);
}, \@exe_list);
# train
if ($grad_req ne 'null')
{
for my $exe (@exe_list)
{
$exe->forward(1);
$exe->backward($exe->outputs);
}
enumerate(sub {
my ($i, $exe) = @_;
return if($i == $max_idx);
zip(sub {
my ($name, $arr) = @_;
if (not defined $gt->{$name})
{
Test::More::ok(not defined $arr);
return;
}
my $gtarr = $gt->{$name}->astype($dtypes[$i])->aspdl;
$arr = $arr->aspdl;
Test::More::ok(
almost_equal(
$arr, $gtarr,
$tol->{$dtypes[$i]}
)
);
}, [@$output_names, @$arg_names], [@{ $exe->outputs }, @{ $exe->grad_arrays }]);
}, \@exe_list);
}
return $gt;
}
sub zip
{
my ($sub, @arrays) = @_;
my $len = @{ $arrays[0] };
for (my $i = 0; $i < $len; $i++)
{
$sub->(map { $_->[$i] } @arrays);
}
}
sub enumerate
{
my ($sub, @arrays) = @_;
my $len = @{ $arrays[0] };
zip($sub, [0..$len-1], @arrays);
}
sub assert
{
my ($input, $error_str) = @_;
local($Carp::CarpLevel) = 1;
Carp::confess($error_str//'AssertionError')
unless $input;
}
=head2 same_array
Check whether two NDArrays sharing the same memory block
Parameters
----------
array1 : NDArray
First NDArray to be checked
array2 : NDArray
Second NDArray to be checked
Returns
-------
bool
Whether two NDArrays share the same memory
=cut
func same_array(
AI::MXNet::NDArray $array1,
AI::MXNet::NDArray $array2
)
{
$array1 += 1;
if(not same($array1->aspdl, $array2->aspdl))
{
$array1 -= 1;
return 0
}
$array1 -= 1;
return same($array1->aspdl, $array2->aspdl);
}
func dies_like($code, $regexp)
{
eval { $code->() };
if($@ =~ $regexp)
{
return 1;
}
else
{
warn $@;
return 0;
}
}
1;