SimpleMatrixKit

概述

SimpleMatrixKit 是一个易于使用的 Swift 矩阵库。该库围绕泛型 Matrix 类型构建,该类型提供存储和操作平衡的二维对象数组的功能。 在任何您可能通常使用数组的数组但希望强制所有内部数组具有相同长度的情况下,都可以使用此结构体。 Matrix 包括用于访问单个元素、提取子矩阵和生成派生矩阵(例如矩阵转置)的方法。 Matrix 可以被视为一个 Collection,其中每个元素对应于矩阵的一行。 该库包含两个运算符 <|><->,用于水平和垂直连接适当大小的矩阵。

对于数值矩阵,可以使用更多的功能。 该库支持标准矩阵运算符 +-*,用于 Numeric 类型的矩阵,其中包括整数和浮点数。 大多数其他线性代数功能要求矩阵值符合 FloatingPoint 协议,其中包括 DoubleFloatCGFloatSquareRealMatrix 类型提供用于处理方阵的属性和方法。 SquareRealMatrix 类型可以报告它们的行列式、迹和 LUP 分解。 如果它们是非奇异的,则可以被求逆或用于线性系统求解。

该库的 MatrixRepresentable 协议允许用户定义自己的矩阵类型。 为了符合此协议,类型必须能够简单地提供其行的数组。 此协议提供了许多标准矩阵属性和方法,并且符合类型的类型可以与上述各种矩阵运算符一起使用。 MatrixSquareRealMatrix 都符合 MatrixRepresentable,因此,例如,可以使用 * 运算符将 Matrix 类型乘以 SquareRealMatrix 类型,前提是它们都保存相同类型的值。 但是,请注意,无论作为输入传递的类型如何,大多数矩阵运算符和方法都返回 Matrix 类型。

错误处理

操作矩阵非常容易出错。 有些错误,例如尝试添加两个不同大小的矩阵,是愚蠢但非常常见的。 其他错误,例如尝试反转奇异矩阵,可能要等到执行详细且有时代价高昂的计算后才能检测到。 错误处理是任何 API 的设计挑战,但在 Swift 中尤其重要,因为用户希望从强类型检查中受益。 原则上,许多类型的矩阵操作错误可以在编译时检测到,例如,将每个不同大小的矩阵视为不同的类型。 然而,在实践中,这种方法大大增加了 API 和代码库的复杂性,并且并非总是可行的。 该库试图采取一种中间路线,充分利用 Swift 的运行时错误处理机制。 广义上讲,如果您尝试不正确地初始化矩阵或访问超出范围的元素,您的代码将会崩溃。 这与 Swift 处理数组的方式一致。 另一方面,如果您尝试组合两个不兼容的矩阵或反转非奇异矩阵,该库将抛出一个错误,您可以根据需要解决该错误。 这种方法的一个结果是您的矩阵操作代码中会有很多 try,但我认为这是为运行时错误检查提供的灵活性和安全性而付出的一小部分代价。

使用示例

假设您有一个关于三个变量的五个观测值的数据集。 您可以选择将这些数据组织成一个矩阵,如下所示:

let data: Matrix<Double> = [ 
    [   4,  5,  5   ],
    [   1,  10, 7   ],
    [   8,  9,  12  ],
    [   12, 10, 11  ],
    [   1,  2,  3   ]
]

然后,平均值的列向量可以表示为:

let n = data.rows
let ones = Matrix<Double>.ones(rows: n, cols: 1)
let avg = try data.transpose * ones / Double(n)

残差可以计算为:

let resid = try data - ones * avg.transpose

残差协方差矩阵为:

let vCov = try resid.transpose * resid / Double(n)

vCov 是一个 3 x 3 矩阵,方差沿主对角线排列。 我们可以使用下标来提取第一个变量的方差。

let var0 = vCov[0,0]

为了说明涉及非奇异矩阵的运算,让我们添加一个组织为列向量的新的因变量。

let y = Matrix(rows: n, cols: 1, valueArray: [12.2, 14.2, 23.2, 8.0, 9.2])

我们现在可以计算 y 在原始变量加上常数上的回归系数。

let x = try ones <|> data
let xx = try x.transpose * x
let xy = try x.transpose * y
let beta = try SquareRealMatrix(xx).solve(xy)

这是计算 beta 的一种更简洁但效率较低的方法。

let beta2 = try SquareRealMatrix(x.transpose*x).inverse() * x.transpose * y

一般来说,最好避免计算不需要的矩阵逆。 beta2 需要的计算量大约是 beta 的四倍。

以下是上面的示例代码以及错误处理的样子。

do {
    let data: Matrix<Double> = [
        [   4,  5,  5   ],
        [   1,  10, 7   ],
        [   8,  9,  12  ],
        [   12, 10, 11  ],
        [   1,  2,  3   ]
    ]
    let n = data.rows
    let ones = Matrix<Double>.ones(rows: n, cols: 1)
    let avg = try data.transpose * ones / Double(n)
    let resid = try data - ones * avg.transpose
    let vCov = try resid.transpose * resid / Double(n)
    let var0 = vCov[0,0]
    print(var0)
    // 18.16
    let y = Matrix(rows: n, cols: 1, valueArray: [12.2, 14.2, 23.2, 8.0, 9.2])
    let x = try ones <|> data
    let xx = try x.transpose * x
    let xy = try x.transpose * y
    let beta = try SquareRealMatrix(xx).solve(xy)
    print(beta)
    // 4 x 1 Matrix:
    // [  4.5908 ]
    // [  -1.920 ]
    // [  -1.556 ]
    // [  3.9427 ]
    let beta2 = try SquareRealMatrix(x.transpose*x).inverse() * x.transpose * y
    print(beta2)
    // 4 x 1 Matrix:
    // [  4.5908 ]
    // [  -1.920 ]
    // [  -1.556 ]
    // [  3.9427 ]    
} catch MatrixError.singularMatrixTreatedAsNonsingular {
    print("Looks like you have have a problem with multicolinearity.")
    print("Better get some more data or drop a variable!")
} catch MatrixError.nonconformingMatrices {
    print("Check your matrix dimensions.")
} catch {
    print("Something terrible has happened and I don't know what it is.")
}

关于效率的几句话

关于在数值线性分析中有效利用处理周期和存储器的大量文献,而 SimpleMatrixKit 几乎没有利用其中的任何一个。 在设计 SimpleMatrixKit 时,我想生成一个依赖项少、易于使用且代码库可以被典型的 Swift 用户理解的库。 SimpleMatrixKit 中使用的所有算法均以 Swift 5.3 实现,并且不使用 Foundation 以外的外部依赖项。 我试图采用在一般条件下有效的算法,但我并没有痴迷于优化它们。 同样,该库没有区分密集矩阵和稀疏矩阵,并且在许多情况下,该库通过创建新的数据结构而不是改变现有数据结构来使用比它需要的更多的内存。 我将 SimpleMatrixKit 描述为最适合涉及小型或中等大小的密集矩阵的问题,在这种情况下,代码的清晰性和灵活性可能被认为比计算效率更重要。 对于不符合这些标准的问题,我建议查看其他更专业的库。 Apple 的 Accelerate framework 是一个很好的起点。

许可

本项目采用 MIT 许可证条款进行许可。