XCTAssertEqualObjects сравнение NSArrays NSNumber завершается неудачей, даже если массивы выглядят идентичными


Я разрабатываю искусственную нейронную сеть в Objective-C, поэтому я написал несколько методов для матрично-векторной арифметики. Например, ниже приведен код для расчета внешнего продукта. Код работает нормально и возвращает желаемые результаты, но мой модульный тест терпит неудачу при сравнении возвращаемого методом объекта NSMutableArray с объектом, созданным в модульном тесте. Вот уже несколько дней, как я теряюсь в догадках. Кто-нибудь знает, почему XCTAssertEqualObjects() терпит неудачу, несмотря на то, что объекты кажутся идентичными?

Вот соответствующий код для возврата внешнего произведения 2 векторов (NSArrays) в MLNNeuralNet.М:

-(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;
}

А вот код для модульного теста:

@interface MLNNeuralNetTests : XCTestCase

@property (strong, nonatomic) MLNNeuralNet *neuralNet;

@end

@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");
}

И вот ошибка, которую я получаю:

Я подумал, что это может быть связано с моим созданием литералов NSNumber в версии модульного теста, такой как @(4.2) etc...

Поэтому я попытался сначала создать doubles, а затем обернуть в NSNumber Вот так:

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

Но и этого не произошло. работа.

Я что-то упустил?

Когда я пытался проверить равенство объектов в аналогичных тестах, у меня не было никаких проблем. Например, следующий тест не дает сбоя:

-(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.");
}
1 4

1 ответ:

Я полагаю, что это сводится к арифметике с плавающей запятой. Сравнение с плавающей точкой может быть сложным. Результат их объединения не дает в точности того, что вы ожидали бы, если бы это были "реальные" числа.

Выход из XCTAssertEqualObjects() использует NSLog() для печати NSNumbers, что округляет их для отображения. Вы можете вручную проверить и увидеть более точные значения:

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],
                                   0.000000000000001);
        col_number += 1;
    }
    row_number += 1;
}

Из этого вы можете видеть, что 12.6, которое должно было произойти в результате умножения, на самом деле 12.6000000000000014, а литерал в correctMatrix 12.6 сохранялся как 12.5999999999999996. Таким образом, они чрезвычайно близки, но не ==.

Макрос XCTAssertEqualWithAccuracy() предназначен для сравнения значений с плавающей запятой. Он позволяет передавать третье значение для создания диапазона, в пределах которого значения считаются "достаточно равными".

Другой вариант, если вы делаете много численных вычислений какао, это переключиться на NSDecimalNumber, который делает точные "реальные" значения для арифметики. Компромисс заключается в следующем что это даже больше боли в шее, чем NSNumber, так как все операции проходят через методы. [x decimalNumberByMultiplyingBy:y].