jsondwyer jsondwyer - 3 months ago 27
Objective-C Question

XCTAssertEqualObjects comparing NSArrays of NSNumber fails even though arrays appear identical

I'm developing an artificial neural net in Objective-C, so I've written some methods for matrix-vector arithmetic. For example, below is the code for the outer product calculation. The code is working fine and returns the desired results, but my unit test fails when comparing the method-returned

object to the one created in the unit test. I've been lost on this for a few days now. Does anyone know why
fails despite the fact that the objects seem identical?

Here is the relevant code to return the outer product of 2 vectors (NSArrays) in MLNNeuralNet.m:

-(NSMutableArray *)outerProduct:(NSArray *)matrix1 by:(NSArray *)matrix2 {

/*Tensor Product of 2 vectors treated as column and row matrices, respectively*/

/*Example: if matrix1 is @[2, 4, 6] and matrix2 @[3, 4, 5], then calculation is:
[2 * 3, 2 * 4, 2 * 5], [4 * 3, etc...]
and result is:
@[@[6, 8, 10], @[12, 16, 20], @[18, 24, 30]]

NSMutableArray *result = [[NSMutableArray alloc] init];

for (int i = 0; i < [matrix1 count]; i++) {
NSMutableArray *tempArray = [[NSMutableArray alloc] init];
for (int j = 0; j < [matrix2 count]; j++) {
double product = [[matrix1 objectAtIndex:i] doubleValue] * [[matrix2 objectAtIndex:j] doubleValue];
[tempArray addObject:@(product)];
[result addObject:tempArray];

return result;

And here is the code for the unit test:

@interface MLNNeuralNetTests : XCTestCase

@property (strong, nonatomic) MLNNeuralNet *neuralNet;


@implementation MLNNeuralNetTests

- (void)setUp {
[super setUp];
_neuralNet = [[MLNNeuralNet alloc] init];

-(void)testOuterProduct {

NSMutableArray *matrix1 = [[NSMutableArray alloc] initWithArray:@[@(1.0), @(2.0), @(3.0)]];
NSMutableArray *matrix2 = [[NSMutableArray alloc] initWithArray:@[@(4.2), @(5.2), @(6.2)]];

NSMutableArray *layer1 = [[NSMutableArray alloc] initWithArray:@[@(4.2), @(5.2), @(6.2)]];
NSMutableArray *layer2 = [[NSMutableArray alloc] initWithArray:@[@(8.4), @(10.4), @(12.4)]];
NSMutableArray *layer3 = [[NSMutableArray alloc] initWithArray:@[@(12.6), @(15.6), @(18.6)]];
NSMutableArray *correctMatrix = [[NSMutableArray alloc]
initWithArray:@[layer1, layer2, layer3]];

NSMutableArray *testMatrix = [self.neuralNet outerProduct:matrix1 by:matrix2];

XCTAssertEqualObjects(correctMatrix, testMatrix, @"Matrix outer product failed");

And here is the error I'm getting:

I thought it might be due to my creating the NSNumber literals in the unit test version like
@(4.2) etc...

so I tried first creating
s and then wrapping in
like this:

double number1 = 4.2;
NSMutableArray *layer1 = [[NSMutableArray alloc] initWithArray:@[@(number1), etc...

but this also did not work.

Am I missing something here?

When I try testing for object equality in similar tests I had no problem. For example, the following test does not fail:

-(void)testMultiplyVectorElements {

NSArray *vector1 = @[@(1.0), @(2.0), @(3.0), @(4.0)];
NSArray *vector2 = @[@(5.2), @(6.2), @(7.2), @(8.2)];
NSMutableArray *correctVector = [[NSMutableArray alloc] initWithArray:@[@(5.2), @(12.4), @(21.6), @(32.8)]];
NSMutableArray *testVector = [self.neuralNet multiplyVectorElements:vector1 by:vector2];

XCTAssertEqualObjects(correctVector, testVector, @"Vector element-wise multiplication failed.");


I believe this is down to the floating point arithmetic. Floating point comparisons can be tricky. The result of combining them doesn't produce exactly what you would expect if these were "real" numbers.

The output from XCTAssertEqualObjects() is using NSLog() to print the NSNumbers, which rounds them off for display. You can manually inspect and see more precise values:

NSUInteger row_idx = 0;
for( NSArray<NSNumber *> * row in testMatrix ){
    NSUInteger col_idx = 0;
    for( NSNumber * testProduct in row ){
        NSNumber * correctProduct = correctRow[row_idx][col_idx];
        NSLog(@"%.16lf %.16lf", [product doubleValue], correctProduct);
        /* This level of accuracy fails with your code. Drop at
         * least one 0 to pass the assertion.
        XCTAssertEqualWithAccuracy([product doubleValue],
                                   [correctProduct doubleValue],
        col_number += 1;
    row_number += 1;

From this you can see that the 12.6 that should have resulted from the multiplication is actually 12.6000000000000014, and the literal in correctMatrix 12.6 was stored as 12.5999999999999996. So they are extremely close, but not ==.

The XCTAssertEqualWithAccuracy() macro is designed for comparing floating point values. It lets you pass a third value to create a range within which the values are considered "equal enough".

Another option, if you're doing lots of numerical Cocoa computing, is to switch to NSDecimalNumber, which does give exact "real" values for arithmetic. The tradeoff is that it's even more of a pain in the neck than NSNumber, since all operations go through methods. [x decimalNumberByMultiplyingBy:y].