В более общем случае мы имеем не одно уравнение (1), а систему нелинейных уравнений $$ \begin{equation} \tag{2} f_i(x_1, x_2, \ldots, x_n) = 0, \quad i = 1, 2, \ldots n. \end{equation} $$ Обозначим через \( \mathbf{x} = (x_1, x_2, \ldots, x_n) \) вектор неизвестных и определим вектор-функцию \( \mathbf{F}(\mathbf{x}) = (f_1(\mathbf{x}), f_2(\mathbf{x}), \ldots, f_n(\mathbf{x})) \). Тогда система (2) записывается в виде $$ \begin{equation} \tag{3} \mathbf{F}(\mathbf{x}) = 0. \end{equation} $$ Частным случаем (3) является уравнение (1) (\( n = 1 \)). Второй пример (3) — система линейных алгебраических уравнений, когда \( \mathbf{F} (\mathbf{x}) = A \mathbf{x} - \mathbf{f} \).
При итерационном решении уравнений (1), (3) задается некоторое начальное приближение, достаточно близкое к искомому решению \( x^* \). В одношаговых итерационных методах новое приближение \( x_{k+1} \) определяется по предыдущему приближению \( x_k \). Говорят, что итерационный метод сходится с линейной скоростью, если \( x_{k+1} - x^* = O(x_k - x^*) \) и итерационный метод имеет квадратичную сходимость, если \( x_{k+1} - x^* = O(x_k - x^*)^2 \).
В итерационном методе Ньютона (методе касательных) для нового приближения имеем $$ \begin{equation} \tag{4} x_{k+1} = x_k + \frac{f(x_k)}{f^\prime(x_k)}, \quad k = 0, 1, \ldots, \end{equation} $$
Вычисления по (4) проводятся до тех пор, пока \( f(x_k) \) не станет близким к нулю. Более точно, до тех пор, пока \( |f_(x_k)| > \varepsilon \), где \( \varepsilon \) — малая величина.
Простейшая реализация метода Ньютона может выглядеть следующим образом:
def naive_Newton(f, dfdx, x, eps):
while abs(f(x)) > eps:
x = x - float(f(x))/dfdx(x)
return x
Чтобы найти корень уравнения \( x^2 = 9 \) необходимо реализовать функции
def f(x):
return x**2 - 9
def dfdx(x):
return 2*x
print naive_Newton(f, dfdx, 1000, 0.001)
Данная функция хорошо работает для приведенного примера. Однако, в общем случае могут возникать некоторые ошибки, которые нужно отлавливать. Например: пусть нужно решить уравнение \( \tanh(x) = 0 \), точное решение которого \( x = 0 \). Если \( |x_0| \leq 1.08 \), то метод сходится за шесть итераций.
-1.0589531343563485
0.9894042072982367
-0.784566773085775
0.3639981611100014
-0.03301469613719421
2.3995252668003453e-05
Число вызовов функций: 25
Решение: 3.0000000001273204
Теперь зададим \( x_0 \) близким к \( 1.09 \). Возникнет переполнение
-1.0933161820201083
1.104903543244409
-1.1461555078811896
1.3030326182332865
-2.064923002377556
13.473142800575976
-126055892892.66042
Возникнет деление на ноль, так как для \( x_7 = -126055892892.66042 \) значение \( \tanh(x_7) \) при машинном округлении равно \( 1.0 \) и поэтому \( f^\prime(x_7) = 1 - \tanh(x_7)^2 \) становится равной нулю в знаменателе.
Проблема заключается в том, что при таком начальном приближении метод Ньютона расходится.
Еще один недостаток функции naive_Newton
заключается в том, что
функция f(x)
вызывается в два раза больше, чем необходимо.
Учитывая выше сказанное реализуем функцию с учетом следующего:
f(x)
def Newton(f, dfdx, x, eps):
f_value = f(x)
iteration_counter = 0
while abs(f_value) > eps and iteration_counter < 100:
try:
x = x - f_value/dfdx(x)
except ZeroDivisionError as err:
print("Ошибка: {}".format(err))
sys.exit(1)
f_value = f(x)
iteration_counter += 1
if abs(f_value) > eps:
iteration_counter = -1
return x, iteration_counter
Метод Ньютона сходится быстро, если начальное приближение близко к решению. Выбор начального приближение влияет не только на скорость сходимости, но и на сходимость вообще. Т.е. при неправильном выборе начального приближения метод Ньютона может расходиться. Неплохой стратегией в случае, когда начальное приближение далеко от точного решения, может быть использование нескольких итераций по методу бисекций, а затем использовать метод Ньютона.
При реализации метода Ньютона нужно знать аналитическое выражение для
производной \( f^\prime(x) \). Python содержит пакет SymPy, который можно
использовать для создания функции dfdx
. Для нашей задачи это можно
реализовать следующим образом:
from sympy import symbol, tanh, diff, lambdify
x = symbol.Symbol('x') # определяем математический символ x
f_expr = tanh(x) # символьное выражение для f(x)
dfdx_expr = diff(f_expr, x) # вычисляем символьно f'(x)
# Преобразуем f_expr и dfdx_expr в обычные функции Python
f = lambdify([x], # аргумент f
f_expr)
dfdx = lambdify([x], dfdx_expr)
tol = 1e-1
sol, no_iterations = Newton(f, dfdx, x=1.09, eps=tol)
if no_iterations > 0:
print("Уравнение: tanh(x) = 0. Число итераций: {}".format(no_iterations))
print("Решение: {}, eps = {}".format(sol, tol))
else:
print("Решение не найдено!")
def F(x):
y = np.zeros_like(x)
y[0] = (3 + 2*x[0])*x[0] - 2*x[1] - 3
y[1:-1] = (3 + 2*x[1:-1])*x[1:-1] - x[:-2] - 2*x[2:] -2
y[-1] = (3 + 2*x[-1])*x[-1] - x[-2] - 4
return y
def J(x):
n = len(x)
jac = np.zeros((n, n))
jac[0, 0] = 3 + 4*x[0]
jac[0, 1] = -2
for i in range(n-1):
jac[i, i-1] = -1
jac[i, i] = 3 + 4*x[i]
jac[i, i+1] = -2
jac[-1, -2] = -1
jac[-1, -1] = 3 + 4*x[-1]
return jac
n = 10
guess = 3*np.ones(n)
sol, its = Newton_system(F, J, guess)
if its > 0:
print("x = {}".format(sol))
else:
print("Решение не найдено!")
Идея метода Ньютона для приближенного решения системы (2) заключается в следующем: имея некоторое приближение \( \pmb{x}^{(k)} \), мы находим следующее приближение \( \pmb{x}^{(k+1)} \), аппроксимируя \( \pmb{F}(\pmb{x}^{(k+1)}) \) линейным оператором и решая систему линейных алгебраических уравнений. Аппроксимируем нелинейную задачу \( \pmb{F}(\pmb{x}^{(k+1)}) = 0 \) линейной $$ \begin{equation} \tag{5} \pmb{F}(\pmb{x}^{(k)}) + \pmb{J}(\pmb{x}^{(k)})(\pmb{x}^{(k+1)} - \pmb{x}^{(k)}) = 0, \end{equation} $$ где \( \pmb{J}(\pmb{x}^{(k)}) \) — матрица Якоби (якобиан): $$ \pmb{\nabla F}(\pmb{x}^{(k)}) = \begin{bmatrix} \frac{\partial f_1(\pmb{x}^{(k)})}{\partial x_1} & \frac{\partial f_1(\pmb{x}^{(k)})}{\partial x_2} & \ldots & \frac{\partial f_1(\pmb{x}^{(k)})}{\partial x_n} \\ \frac{\partial f_2(\pmb{x}^{(k)})}{\partial x_1} & \frac{\partial f_2(\pmb{x}^{(k)})}{\partial x_2} & \ldots & \frac{\partial f_2(\pmb{x}^{(k)})}{\partial x_n} \\ \vdots & \vdots & \ldots & \vdots \\ \frac{\partial f_n(\pmb{x}^{(k)})}{\partial x_1} & \frac{\partial f_n(\pmb{x}^{(k)})}{\partial x_2} & \ldots & \frac{\partial f_n(\pmb{x}^{(k)})}{\partial x_n} \\ \end{bmatrix} $$ Уравнение (5) является линейной системой с матрицей коэффициентов \( \pmb{J} \) и вектором правой части \( -\pmb{F}(\pmb{x}^{(k)}) \). Систему можно переписать в виде $$ \pmb{J}(\pmb{x}^{(k)})\pmb{\delta} = - \pmb{F}(\pmb{x}^{(k)}), $$ где \( \pmb{\delta} = \pmb{x}^{(k+1)} - \pmb{x}^{(k)} \).
Таким образом, \( k \)-я итерация метода Ньютона состоит из двух стадий:
1. Решается система линейных уравнений (СЛАУ) \( \pmb{J}(\pmb{x}^{(k)})\pmb{\delta} = -\pmb{F}(\pmb{x}^{(k)}) \) относительно \( \pmb{\delta} \).
2. Находится значение вектора на следующей итерации \( \pmb{x}^{(k+1)} = \pmb{x}^{(k)} + \pmb{\delta} \).
Для решения СЛАУ можно использовать приближенные методы. Можно также
использовать метод Гаусса. Пакет numpy
содержит модуль linalg
,
основанный на известной библиотеке LAPACK, в которой реализованы
методы линейной алгебры. Инструкция x = numpy.linalg.solve(A, b)
решает систему \( Ax = b \) методом Гаусса, реализованным в библиотеке
LAPACK.
Когда система нелинейных уравнений возникает при решении задач для нелинейных уравнений в частных производных, матрица Якоби часто бывает разреженной. В этом случае целесообразно использовать специальные методы для разреженных матриц или итерационные методы.
Можно также воспользоваться методами, реализованными для систем линейных уравнений.