Я реализую некоторые базовые решатели линейных уравнений в Python.

В настоящее время я реализовал прямую и обратную замену для треугольных систем уравнений (так что их очень просто решить!), Но точность решений становится очень низкой даже с системами примерно из 50 уравнений (матрица коэффициентов 50x50).

Следующий код выполняет прямую / обратную замену:

FORWARD_SUBSTITUTION = 1
BACKWARD_SUBSTITUTION = 2
def solve_triang_subst(A: np.ndarray, b: np.ndarray,
                       substitution=FORWARD_SUBSTITUTION) -> np.ndarray:
    """Solves a triangular system via
    forward or backward substitution.

    A must be triangular. FORWARD_SUBSTITUTION means A should be
    lower-triangular, BACKWARD_SUBSTITUTION means A should be upper-triangular.
    """
    rows = len(A)
    x = np.zeros(rows, dtype=A.dtype)
    row_sequence = reversed(range(rows)) if substitution == BACKWARD_SUBSTITUTION else range(rows)

    for row in row_sequence:
        delta = b[row] - np.dot(A[row], x)
        cur_x = delta / A[row][row]
        x[row] = cur_x

    return x

Я использую numpy и 64-битные числа с плавающей запятой.

Простой инструмент тестирования

Я создал простой набор тестов, который генерирует матрицы коэффициентов и векторы x, вычисляет b, а затем использует прямую или обратную замену для восстановления x, сравнивая его с известным значение для действительности.

Следующий код выполняет эти проверки:

import numpy as np
import scipy.linalg as sp_la

RANDOM_SEED = 1984
np.random.seed(RANDOM_SEED)


def check(sol: np.ndarray, x_gt: np.ndarray, description: str) -> None:
    if not np.allclose(sol, x_gt, rtol=0.1):
        print("Found inaccurate solution:")
        print(sol)
        print("Ground truth (not achieved...):")
        print(x_gt)
        raise ValueError("{} did not work!".format(description))

def fuzz_test_solving():
    N_ITERATIONS = 100
    refine_result = True
    for mode in [FORWARD_SUBSTITUTION, BACKWARD_SUBSTITUTION]:
        print("Starting mode {}".format(mode))
        for iteration in range(N_ITERATIONS):
            N = np.random.randint(3, 50)
            A = np.random.uniform(0.0, 1.0, [N, N]).astype(np.float64)

            if mode == BACKWARD_SUBSTITUTION:
                A = np.triu(A)
            elif mode == FORWARD_SUBSTITUTION:
                A = np.tril(A)
            else:
                raise ValueError()

            x_gt = np.random.uniform(0.0, 1.0, N).astype(np.float64)
            b = np.dot(A, x_gt)

            x_est = solve_triang_subst(A, b, substitution=mode,
                                       refine_result=refine_result)
            # TODO report error and count, don't throw!
            # Keep track of error norm!!
            check(x_est, x_gt,
                  "Mode {} custom triang iteration {}".format(mode, iteration))

if __name__ == '__main__':
    fuzz_test_solving()

Обратите внимание, что максимальный размер тестовой матрицы - 49x49. Даже в этом случае система не всегда может вычислить достойные решения и дает сбой более чем в 0,1 раза. Вот пример такой ошибки (выполняется обратная подстановка, поэтому самая большая ошибка находится в 0-м коэффициенте; все тестовые данные равномерно выбираются из [0, 1 [):

Solution found with Mode 2 custom triang iteration 24:
[ 0.27876067  0.55200497  0.49499509  0.3259397   0.62420183  0.47041149
  0.63557676  0.41155446  0.47191956  0.74385864  0.03002819  0.4700286
  0.37989592  0.56527691  0.15072607  0.05659282  0.52587574  0.82252197
  0.65662833  0.50250729  0.74139748  0.10852731  0.27864265  0.42981232
  0.16327331  0.74097937  0.24411709  0.96934199  0.890266    0.9183985
  0.14842446  0.51806495  0.36966843  0.18227989  0.85399593  0.89615663
  0.39819336  0.90445931  0.21430972  0.61212349  0.85205597  0.66758689
  0.1793689   0.38067267  0.39104614  0.6765885   0.4118123 ]
Ground truth (not achieved...)
[ 0.20881608  0.71009766  0.44735271  0.31169033  0.63982328  0.49075813
  0.59669585  0.43844108  0.47764942  0.72222069  0.03497499  0.4707452
  0.37679884  0.56439738  0.15120397  0.05635977  0.52616387  0.82230625
  0.65670245  0.50251426  0.74139956  0.10845974  0.27864289  0.42981226
  0.1632732   0.74097939  0.24411707  0.96934199  0.89026601  0.91839849
  0.14842446  0.51806495  0.36966843  0.18227989  0.85399593  0.89615663
  0.39819336  0.90445931  0.21430972  0.61212349  0.85205597  0.66758689
  0.1793689   0.38067267  0.39104614  0.6765885   0.4118123 ]

Я также реализовал метод итеративного уточнения, описанный в разделе 2.5 [0], и, хотя он немного помог, результаты все еще плохие для больших матриц.

Проверка работоспособности MATLAB

Я также провел этот эксперимент в MATLAB, и даже там, когда существует более 100 уравнений, ошибка оценки возрастает экспоненциально.

Вот код MATLAB, который я использовал для этого эксперимента:

err_norms = [];
range = 1:3:120;
for size=range

  A = rand(size, size);
  A = tril(A);
  x_gt = rand(size, 1);

  b = A * x_gt;
  x_sol = A\b;

  err_norms = [err_norms, norm(x_gt - x_sol)];
end

plot(range, err_norms);
set(gca, 'YScale', 'log')

И вот итоговый сюжет:

Plot of error as a function of the number of equations.

Главный вопрос

Мой вопрос: это нормальное поведение, поскольку в проблеме практически нет структуры, учитывая, что я случайным образом генерирую матрицу A и x?

А как насчет решения линейных систем из сотен уравнений для различных практических приложений? Являются ли эти ограничения просто общепринятым фактом, и, например, алгоритмы оптимизации естественным образом устойчивы к этим проблемам? Или мне не хватает важных аспектов этой проблемы?


[0]: Press, Уильям Х. Численные рецепты 3-е издание: Искусство научных вычислений. Издательство Кембриджского университета, 2007.

4
Andrei Bârsan 18 Ноя 2017 в 03:12

1 ответ

Лучший ответ

Нет никаких ограничений. Это очень плодотворное занятие, которое мы все осознали; писать линейные решатели не так просто, и поэтому почти всегда LAPACK или его аналоги на других языках используются с полной уверенностью.

Вы сталкиваетесь с почти сингулярными матрицами, и поскольку вы используете обратную косую черту в Matlab, вы не видите, что Matlab переключается на решения наименьших квадратов за кулисами, когда достигается близкая к сингулярности. Если вы просто измените A\b на linsolve(A,b) и ограничите решатель для решения квадратных систем, вы, вероятно, увидите множество предупреждений на своей консоли.

Я не тестировал это, потому что у меня больше нет лицензии, но если я буду писать вслепую, это должно показать вам номера условий матриц на каждом этапе.

err_norms = [];
range = 1:3:120;
for i=1:40
  size = range(i);
  A = rand(size, size);
  A = tril(A);
  x_gt = rand(size, 1);

  b = A * x_gt;
  x_sol = linsolve(A,b);

  err_norms = [err_norms, norm(x_gt - x_sol)];
  zzz(i) = rcond(A);
end

semilogy(range, err_norms);
figure,semilogy(range,zzz);

Обратите внимание, что, поскольку вы выбираете числа из равномерного распределения, становится все более и более вероятным попадание в плохо обусловленные матрицы (относительно инверсии), поскольку строки имеют большую вероятность иметь дефицит ранга. Вот почему ошибка становится все больше и больше. Посыпьте некоторую единичную матрицу, умноженную на скаляр, и все ошибки должны вернуться к eps*n уровням.

Но лучше всего оставьте это экспертным алгоритмам, которые проверены десятилетиями. На самом деле написать что-либо из этого не так уж и тривиально. Вы можете прочитать коды Фортрана, например, {{X0} }, который использует подпрограммы ?trtrs из LAPACK.

2
percusse 18 Ноя 2017 в 03:41