\newsectionNoPB
\subsection{Quasi-Newton-Verfahren}
Falls $DF(x)$ zu teuer ist oder nicht zur Verfügung steht, können wir im Eindimensionalen das Sekantenverfahren verwenden.

Im höherdimensionalen Raum ist dies jedoch nicht direkt möglich und wir erhalten die Broyden-Quasi-Newton Methode:
\rmvspace
\begin{align*}
    J_{k + 1} := J_k + \frac{F(x^{(k + 1)}) (\Delta x^{(k)})^\top}{||\Delta x^{(k)}||_2^2}
\end{align*}

\drmvspace
Dabei ist $J_0$ z.B. durch $DF(x^{(0)})$ definiert.

\fancyremark{Broyden-Update} Das Broyden-Update ergibt bezüglich der $||\cdot||_2$-Norm die minimale korrektur der Jakobi-Matrix $J_k$ an, so dass die Sekantenbedingung erfüllt ist.
Die Implementierung erzielt man folgendermassen mit der \bi{Sherman-Morrison-Woodbury} Update-Formel:
\begin{align*}
    J_{k + 1}^{-1} = \left( I - \frac{J_k^{-1} F(x^{(k + 1)}) (\Delta x^{(k)})^\top}{||\Delta x^{(k)}||_2^2 + (\Delta x^{(k)})^\top J_k^{-1} F(x^{(k + 1)})} \right) J^{-1}_k
\end{align*}

Das Broyden-Quasi-Newton-Verfahren konvergiert langsamer als das Newton-Verfahren, aber schneller als das vereinfachte Newton-Verfahren. (\texttt{sp} ist \texttt{Scipy} und \texttt{np} logischerweise \texttt{Numpy} im untenstehenden code)

\begin{code}{python}
def fastbroyd(x0, F, J, tol=1e-12, maxit=20):
    x = x0.copy()  # make sure we do not change the iput
    lup = sp.linalg.lu_factor(J)  # LU decomposition of J
    s = sp.linalg.lu_solve(lup, F(x))  # start with a Newton corection
    sn = np.dot(s, s)  # squared norm of the correction
    x -= s
    f = F(x)  # start with a full Newton step
    dx = np.zeros((maxit, len(x)))  # containers for storing corrections s and their sn:
    dxn = np.zeros(maxit)
    k = 0
    dx[k] = s
    dxn[k] = sn
    k += 1  # the number of the Broyden iteration

    # Broyden iteration
    while sn > tol and k < maxit:
        w = sp.linalg.lu_solve(lup, f)  # f = F (actual Broyden iteration x)
        # Using the Sherman-Morrison-Woodbury formel
        for r in range(1, k):
            w += dx[r] * (np.dot(dx[r - 1], w)) / dxn[r - 1]
            z = np.dot(s, w)
            s = (1 + z / (sn - z)) * w
            sn = np.dot(s, s)
            dx[k] = s
            dxn[k] = sn
            x -= s
            f = F(x)
            k += 1  # update x and iteration number k
    return x, k  # return the final value and the numbers of iterations needed
\end{code}
