您的位置:首页 > 编程语言 > Python开发

利用Python学习线性代数 -- 1.1 线性方程组

2019-11-24 13:36 2571 查看

利用Python学习线性代数 -- 1.1 线性方程组

利用Python学习线性代数系列,第一节

本节实现的主要功能函数,在源码文件linear_system中,后续章节将作为基本功能调用。

线性方程

线性方程组由一个或多个线性方程组成,如
\[ \begin{array}\\ x_1 - 2 x_2 &= -1\\ -x_1 + 3 x_2 &= 3 \end{array} \]

求包含两个变量两个线性方程的方程组的解,等价于求两条直线的交点。
这里可以画出书图1-1和1-2的线性方程组的图形。
通过改变线性方程的参数,观察图形,体会两个方程对应直线平行、相交、重合三种可能。

那么,怎么画二元线性方程的直线呢?

方法是这样的:
假如方程是 \(a x_1 + b x_2 = c\) 的形式,可以写成 \(x_2 = (c - a x_1) / b\)。
在以 \(x_1\) 和\(x_2\)为两个轴的直角坐标系中,\(x_1\)取一组值,如 \((-3, -2.9, -2.8, \dots, 2.9, 3.0)\),
计算相应的 \(x_2\),然后把所有点 \((x_1, x_2)\) 连起来成为一条线。
当 \(b\) 为 \(0\) 时, 则在\(x_1 = c / a\)处画一条垂直线。

# 引入Numpy和 Matplotlib库
import numpy as np
from matplotlib import pyplot as plt

Matplotlib 是Python中使用较多的可视化库,这里只用到了它的一些基本功能。

def draw_line(a, b, c, start=-4,
stop=5, step=0.01):
"""根据线性方程参数绘制一条直线"""
# 如果b为0,则画一条垂线
if np.isclose(b, 0):
plt.vlines(start, stop, c / a)
else: # 否则画 y = (c - a*x) / b
xs = np.arange(start, stop, step)
plt.plot(xs, (c - a*xs)/b)
# 1.1 图1-1
draw_line(1, -2, -1)
draw_line(-1, 3, 3)

def draw_lines(augmented, start=-4,
stop=5, step=0.01):
"""给定增广矩阵,画两条线."""
plt.figure()
for equation in augmented:
draw_line(*equation, start, stop, step)
plt.show()
# Fig. 1-1
# 增广矩阵用二维数组表示
# [[1, -2, -1], [-1, 3, 3]]
# 这些数字对应图1-1对应方程的各项系数
draw_lines([[1, -2, -1], [-1, 3, 3]])

# Fig. 1-2
draw_lines([[1, -2, -2], [-1, 2, 3]])
# Fig. 1-3
draw_lines([[1, -2, -1], [-1, 2, 1]])


  • 建议:改变这些系数,观察直线,体会两条直线相交、平行和重合的情况

例如

draw_lines([[1, -2, -2], [-1, 2, 9]])

如果对Numpy比较熟悉,则可以采用更简洁的方式实现上述绘图功能。
在计算多条直线方程时,可以利用向量编程的方式,用更少的代码实现。

def draw_lines(augmented, start=-4,
stop=5, step=0.01):
"""Draw lines represented by augmented matrix on 2-d plane."""
am = np.asarray(augmented)
xs = np.arange(start, stop, step).reshape([1, -1])
# 同时计算两条直线的y值
ys = (am[:, [-1]] - am[:, [1]]*xs) / am[:, [0]]
for y in ys:
plt.plot(xs[0], y)
plt.show()

矩阵记号

矩阵是一个数表,在程序中通常用二维数组表示,例如

# 嵌套列表表示矩阵
matrix = [[1, -2, 1, 0],
[0, 2, -8, 8],
[5, 0, -5, 10]]
matrix
[[1, -2, 1, 0], [0, 2, -8, 8], [5, 0, -5, 10]]

实际工程和研究实践中,往往会采用一些专门的数值计算库,简化和加速计算。
Numpy库是Python中数值计算的常用库。
在Numpy中,多维数组类型称为ndarray,可以理解为n dimensional array。
例如

# Numpy ndarray 表示矩阵
matrix = np.array([[1, -2, 1, 0],
[0, 2, -8, 8],
[5, 0, -5, 10]])
matrix
array([[ 1, -2,  1,  0],
[ 0,  2, -8,  8],
[ 5,  0, -5, 10]])

解线性方程组

本节解线性方程组的方法是 高斯消元法,利用了三种基本行变换。

  1. 把某个方程换成它与另一个方程的倍数的和;
  2. 交换两个方程的位置;
  3. 某个方程的所有项乘以一个非零项。

假设线性方程的增广矩阵是\(A\),其第\(i\)行\(j\)列的元素是\(a_{ij}\)。
消元法的基本步骤是:

  • 增广矩阵中有 \(n\) 行,该方法的每一步处理一行。
      在第\(i\)步,该方法处理第\(i\)行 若\(a_{ii}\)为0,则在剩余行 \(\{j| j \in (i, n]\}\)中选择绝对值最大的行\(a_{ij}\) 若\(a_{ij}\)为0,返回第1步。
    1. 否则利用变换2,交换\(A\)的第\(i\)和\(j\)行。
  • 利用行变换3,第\(i\)行所有元素除以\(a_{ii}\),使第 \(i\) 个方程的第 \(i\)个 系数为1
  • 利用行变换1,\(i\)之后的行减去第\(i\)行的倍数,使这些行的第 \(i\) 列为0
  • 为了理解这些步骤的实现,这里先按书中的例1一步步计算和展示,然后再总结成完整的函数。
    例1的增广矩阵是

    \[ \left[ \begin{array} &1 & -2 & 1 & 0\\ 0 & 2 & -8 & 8\\ 5 & 0 & -5 & 10 \end{array} \right] \]

    # 增广矩阵
    A = np.array([[1, -2, 1, 0],
    [0, 2, -8, 8],
    [5, 0, -5, 10]])
    # 行号从0开始,处理第0行
    i = 0
    # 利用变换3,将第i行的 a_ii 转成1。这里a_00已经是1,所不用动
    # 然后利用变换1,把第1行第0列,第2行第0列都减成0。
    # 这里仅需考虑i列之后的元素,因为i列之前的元素已经是0
    #   即第1行减去第0行的0倍
    #   而第2行减去第0行的5倍
    A[i+1:, i:] = A[i+1:, i:] - A[i+1:, [i]] * A[i, i:]
    A
    array([[  1,  -2,   1,   0],
    [  0,   2,  -8,   8],
    [  0,  10, -10,  10]])
    i = 1
    # 利用变换3,将第i行的 a_ii 转成1。
    A[i] = A[i] / A[i, i]
    A
    array([[  1,  -2,   1,   0],
    [  0,   1,  -4,   4],
    [  0,  10, -10,  10]])
    # 然后利用变换1,把第2行第i列减成0。
    A[i+1:, i:] = A[i+1:, i:] - A[i+1:, [i]] * A[i, i:]
    A
    array([[  1,  -2,   1,   0],
    [  0,   1,  -4,   4],
    [  0,   0,  30, -30]])
    i = 2
    # 利用变换3,将第i行的 a_ii 转成1。
    A[i] = A[i] / A[i, i]
    A
    array([[ 1, -2,  1,  0],
    [ 0,  1, -4,  4],
    [ 0,  0,  1, -1]])

    消元法的前向过程就结束了,我们可以总结成一个函数

    def eliminate_forward(augmented):
    """
    消元法的前向过程.
    
    返回行阶梯形,以及先导元素的坐标(主元位置)
    """
    A = np.asarray(augmented, dtype=np.float64)
    # row number of the last row
    pivots = []
    i, j = 0, 0
    while i < A.shape[0] and j < A.shape[1]:
    A[i] = A[i] / A[i, j]
    if (i + 1) < A.shape[0]: # 除最后一行外
    A[i+1:, j:] = A[i+1:, j:] - A[i+1:, [j]] * A[i, j:]
    pivots.append((i, j))
    i += 1
    j += 1
    return A, pivots

    这里有两个细节值得注意

    1. 先导元素 \(a_{ij}\),不一定是在主对角线位置,即 \(i\) 不一定等于\(j\).
    2. 最后一行只需要用变换3把先导元素转为1,没有剩余行需要转换
    # 测试一个增广矩阵,例1
    A = np.array([[1, -2, 1, 0],
    [0, 2, -8, 8],
    [5, 0, -5, 10]])
    A, pivots = eliminate_forward(A)
    print(A)
    print(pivots)
    [[ 1. -2.  1.  0.]
    [ 0.  1. -4.  4.]
    [ 0.  0.  1. -1.]]
    [(0, 0), (1, 1), (2, 2)]

    消元法的后向过程则更简单一些,对于每一个主元(这里就是前面的\(a_{ii}\)),将其所在的列都用变换1,使其它行对应的列为0.

    for i, j in reversed(pivots):
    A[:i, j:] = A[:i, j:] - A[[i], j:] * A[:i, [j]]
    A
    array([[ 1.,  0.,  0.,  1.],
    [ 0.,  1.,  0.,  0.],
    [ 0.,  0.,  1., -1.]])
    def eliminate_backward(simplified, pivots):
    """消元法的后向过程."""
    A = np.asarray(simplified)
    for i, j in reversed(pivots):
    A[:i, j:] = A[:i, j:] - A[[i], j:] * A[:i, [j]]
    return A

    至此,结合 eliminate_forward 和eliminate_backward函数,可以解形如例1的线性方程。

    然而,存在如例3的线性方程,在eliminate_forward算法进行的某一步,主元为0,需要利用变换2交换两行。
    交换行时,可以选择剩余行中,选择当前主元列不为0的任意行,与当前行交换。
    这里每次都采用剩余行中,当前主元列绝对值最大的行。
    补上行交换的前向过程函数如下

    def eliminate_forward(augmented):
    """消元法的前向过程"""
    A = np.asarray(augmented, dtype=np.float64)
    # row number of the last row
    pivots = []
    i, j = 0, 0
    while i < A.shape[0] and j < A.shape[1]:
    # if pivot is zero, exchange rows
    if np.isclose(A[i, j], 0):
    if (i + 1) < A.shape[0]:
    max_k = i + 1 + np.argmax(np.abs(A[i+1:, i]))
    if (i + 1) >= A.shape[0] or np.isclose(A[max_k, i], 0):
    j += 1
    continue
    A[[i, max_k]] = A[[max_k, i]]
    A[i] = A[i] / A[i, j]
    if (i + 1) < A.shape[0]:
    A[i+1:, j:] = A[i+1:, j:] - A[i+1:, [j]] * A[i, j:]
    pivots.append((i, j))
    i += 1
    j += 1
    return A, pivots

    行交换时,有一种特殊情况,即剩余所有行的主元列都没有非零元素
    这种情况下,在当前列的右侧寻找不为零的列,作为新的主元列。

    # 用例3测试eliminate_forward
    aug = [[0, 1, -4, 8],
    [2, -3, 2, 1],
    [4, -8, 12, 1]]
    echelon, pivots = eliminate_forward(aug)
    print(echelon)
    print(pivots)
    [[ 1.   -2.    3.    0.25]
    [ 0.    1.   -4.    0.5 ]
    [ 0.    0.    0.    1.  ]]
    [(0, 0), (1, 1), (2, 3)]

    例3化简的结果与书上略有不同,由行交换策略不同引起,也说明同一个矩阵可能由多个阶梯形。

    结合上述的前向和后向过程,即可以给出一个完整的消元法实现

    def eliminate(augmented):
    """
    利用消元法前向和后向步骤,化简线性方程组.
    
    如果是矛盾方程组,则仅输出前向化简结果,并打印提示
    否则输出简化后的方程组,并输出最后一列
    """
    print(np.asarray(augmented))
    A, pivots = eliminate_forward(augmented)
    print(" The echelon form is\n", A)
    print(" The pivots are: ", pivots)
    pivot_cols = {p[1] for p in pivots}
    simplified = eliminate_backward(A, pivots)
    if (A.shape[1]-1) in pivot_cols:
    print(" There is controdictory.\n", simplified)
    elif len(pivots) == (A.shape[1] -1):
    print(" Solution: ", simplified[:, -1])
    is_correct = solution_check(np.asarray(augmented),
    simplified[:, -1])
    print(" Is the solution correct? ", is_correct)
    else:
    print(" There are free variables.\n", simplified)
    print("-"*30)
    eliminate(aug)
    [[ 0  1 -4  8]
    [ 2 -3  2  1]
    [ 4 -8 12  1]]
    The echelon form is
    [[ 1.   -2.    3.    0.25]
    [ 0.    1.   -4.    0.5 ]
    [ 0.    0.    0.    1.  ]]
    The pivots are:  [(0, 0), (1, 1), (2, 3)]
    There is controdictory.
    [[ 1.  0. -5.  0.]
    [ 0.  1. -4.  0.]
    [ 0.  0.  0.  1.]]
    ------------------------------

    利用 Sympy 验证消元法实现的正确性

    Python的符号计算库Sympy,有化简矩阵为行最简型的方法,可以用来检验本节实现的代码是否正确。

    # 导入 sympy的 Matrix模块
    from sympy import Matrix
    Matrix(aug).rref(simplify=True)
    # 返回的是行最简型和主元列的位置
    (Matrix([
    [1, 0, -5, 0],
    [0, 1, -4, 0],
    [0, 0,  0, 1]]), (0, 1, 3))
    echelon, pivots = eliminate_forward(aug)
    simplified = eliminate_backward(echelon, pivots)
    print(simplified, pivots)
    # 输出与上述rref一致
    [[ 1.  0. -5.  0.]
    [ 0.  1. -4.  0.]
    [ 0.  0.  0.  1.]] [(0, 0), (1, 1), (2, 3)]

    综合前向和后向步骤,并结果的正确性

    综合前向和后向消元,就可以得到完整的消元法过程。
    消元结束,如果没有矛盾(最后一列不是主元列),基本变量数与未知数个数一致,则有唯一解,可以验证解是否正确。
    验证的方法是将解与系数矩阵相乘,检查与原方程的b列一致。

    def solution_check(augmented, solution):
    # 系数矩阵与解相乘
    b = augmented[:, :-1] @ solution.reshape([-1, 1])
    b = b.reshape([-1])
    # 检查乘积向量与b列一致
    return all(np.isclose(b - augmented[:, -1], np.zeros(len(b))))
    def eliminate(augmented):
    from sympy import Matrix
    print(np.asarray(augmented))
    A, pivots = eliminate_forward(augmented)
    print(" The echelon form is\n", A)
    print(" The pivots are: ", pivots)
    pivot_cols = {p[1] for p in pivots}
    simplified = eliminate_backward(A, pivots)
    if (A.shape[1]-1) in pivot_cols: # 最后一列是主元列
    print(" There is controdictory.\n", simplified)
    elif len(pivots) == (A.shape[1] -1): # 唯一解
    is_correct = solution_check(np.asarray(augmented),
    simplified[:, -1])
    print(" Is the solution correct? ", is_correct)
    print(" Solution: \n", simplified)
    else: # 有自由变量
    print(" There are free variables.\n", simplified)
    print("-"*30)
    print("对比Sympy的rref结果")
    print(Matrix(augmented).rref(simplify=True))
    print("-"*30)

    测试书中的例子

    aug_1_1_1 = [[1, -2, 1, 0],
    [0, 2, -8, 8],
    [5, 0, -5, 10]]
    eliminate(aug_1_1_1)
    # 1.1 example 3
    aug_1_1_3 = [[0, 1, -4, 8],
    [2, -3, 2, 1],
    [4, -8, 12, 1]]
    eliminate(aug_1_1_3)
    eliminate([[1, -6, 4, 0, -1],
    [0, 2, -7, 0, 4],
    [0, 0, 1, 2, -3],
    [0, 0, 3, 1, 6]])
    eliminate([[0, -3, -6, 4, 9],
    [-1, -2, -1, 3, 1],
    [-2, -3, 0, 3, -1],
    [1, 4, 5, -9, -7]])
    
    eliminate([[0, 3, -6, 6, 4, -5],
    [3, -7, 8, -5, 8, 9],
    [3, -9, 12, -9, 6, 15]])
    [[ 1 -2  1  0]
    [ 0  2 -8  8]
    [ 5  0 -5 10]]
    The echelon form is
    [[ 1. -2.  1.  0.]
    [ 0.  1. -4.  4.]
    [ 0.  0.  1. -1.]]
    The pivots are:  [(0, 0), (1, 1), (2, 2)]
    Is the solution correct?  True
    Solution:
    [[ 1.  0.  0.  1.]
    [ 0.  1.  0.  0.]
    [ 0.  0.  1. -1.]]
    ------------------------------
    对比Sympy的rref结果
    (Matrix([
    [1, 0, 0,  1],
    [0, 1, 0,  0],
    [0, 0, 1, -1]]), (0, 1, 2))
    ------------------------------
    [[ 0  1 -4  8]
    [ 2 -3  2  1]
    [ 4 -8 12  1]]
    The echelon form is
    [[ 1.   -2.    3.    0.25]
    [ 0.    1.   -4.    0.5 ]
    [ 0.    0.    0.    1.  ]]
    The pivots are:  [(0, 0), (1, 1), (2, 3)]
    There is controdictory.
    [[ 1.  0. -5.  0.]
    [ 0.  1. -4.  0.]
    [ 0.  0.  0.  1.]]
    ------------------------------
    对比Sympy的rref结果
    (Matrix([
    [1, 0, -5, 0],
    [0, 1, -4, 0],
    [0, 0,  0, 1]]), (0, 1, 3))
    ------------------------------
    [[ 1 -6  4  0 -1]
    [ 0  2 -7  0  4]
    [ 0  0  1  2 -3]
    [ 0  0  3  1  6]]
    The echelon form is
    [[ 1.  -6.   4.   0.  -1. ]
    [ 0.   1.  -3.5  0.   2. ]
    [ 0.   0.   1.   2.  -3. ]
    [-0.  -0.  -0.   1.  -3. ]]
    The pivots are:  [(0, 0), (1, 1), (2, 2), (3, 3)]
    Is the solution correct?  True
    Solution:
    [[ 1.   0.   0.   0.  62. ]
    [ 0.   1.   0.   0.  12.5]
    [ 0.   0.   1.   0.   3. ]
    [-0.  -0.  -0.   1.  -3. ]]
    ------------------------------
    对比Sympy的rref结果
    (Matrix([
    [1, 0, 0, 0,   62],
    [0, 1, 0, 0, 25/2],
    [0, 0, 1, 0,    3],
    [0, 0, 0, 1,   -3]]), (0, 1, 2, 3))
    ------------------------------
    [[ 0 -3 -6  4  9]
    [-1 -2 -1  3  1]
    [-2 -3  0  3 -1]
    [ 1  4  5 -9 -7]]
    The echelon form is
    [[ 1.   1.5 -0.  -1.5  0.5]
    [-0.   1.   2.  -3.  -3. ]
    [-0.  -0.  -0.   1.  -0. ]
    [ 0.   0.   0.   0.   0. ]]
    The pivots are:  [(0, 0), (1, 1), (2, 3)]
    There are free variables.
    [[ 1.  0. -3.  0.  5.]
    [-0.  1.  2.  0. -3.]
    [-0. -0. -0.  1. -0.]
    [ 0.  0.  0.  0.  0.]]
    ------------------------------
    对比Sympy的rref结果
    (Matrix([
    [1, 0, -3, 0,  5],
    [0, 1,  2, 0, -3],
    [0, 0,  0, 1,  0],
    [0, 0,  0, 0,  0]]), (0, 1, 3))
    ------------------------------
    [[ 0  3 -6  6  4 -5]
    [ 3 -7  8 -5  8  9]
    [ 3 -9 12 -9  6 15]]
    The echelon form is
    [[ 1.         -2.33333333  2.66666667 -1.66666667  2.66666667  3.        ]
    [ 0.          1.         -2.          2.          1.33333333 -1.66666667]
    [ 0.          0.          0.          0.          1.          4.        ]]
    The pivots are:  [(0, 0), (1, 1), (2, 4)]
    There are free variables.
    [[  1.   0.  -2.   3.   0. -24.]
    [  0.   1.  -2.   2.   0.  -7.]
    [  0.   0.   0.   0.   1.   4.]]
    ------------------------------
    对比Sympy的rref结果
    (Matrix([
    [1, 0, -2, 3, 0, -24],
    [0, 1, -2, 2, 0,  -7],
    [0, 0,  0, 0, 1,   4]]), (0, 1, 4))
    ------------------------------
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: