机器学习的数学基础2—矩阵
本文最后更新于37 天前,其中的信息可能已经过时,如有错误请发送邮件到2031853749@qq.com

矩阵

什么是矩阵

定义: 一般的, 由$m\times n$个元素$a_{ij}$ $(i=1,2,\cdots,m;j=1,2,\cdots,n)$按确定的位置排列成的矩形阵列称为矩阵.

向量也是一种形式的矩阵, 或者说矩阵中的每行或者每列为向量.

$$
\boldsymbol{A}=\begin{bmatrix}
a_{11} & a_{12} & \cdots & a_{1n} \\
a_{21} & a_{22} & \cdots & a_{2n} \\
\vdots & \vdots & \ddots & \vdots \\
a_{m1} & a_{m2} & \cdots & a_{mn}
\end{bmatrix}
$$

可以简写为$\boldsymbol{A}=(a_{ij})$或者$\boldsymbol{A}=(a_{ij})_{m\times n}$

如果$m=n$, 则称此矩阵为$n$阶方阵
$$
\boldsymbol{A}=\begin{bmatrix}
a_{11} & a_{12} & \cdots & a_{1n} \\
a_{21} & a_{22} & \cdots & a_{2n} \\
\vdots & \vdots & \ddots & \vdots \\
a_{n1} & a_{n2} & \cdots & a_{nn}
\end{bmatrix}
$$
在Python中, 可以用Numpy的二维数组表示矩阵

import numpy as np
matrix_I=np.array([[1,0,0],[0,1,0],[0,0,1]])
print(matrix_I)
'''
[[1 0 0]
 [0 1 0]
 [0 0 1]]
'''

或者用专门的矩阵类:

import numpy as np
a=np.asmatrix("0 0 1;0 1 0;0 0 1")
print(a)
'''
[[1 0 0]
 [0 1 0]
 [0 0 1]]
'''

但是这两种对象是不同的, 需要注意.

单位矩阵

在三维向量中有这样一个标准基$\{ \begin{bmatrix}1\\ 0 \\ 0\end{bmatrix},\begin{bmatrix}0\\ 1 \\0\end{bmatrix} ,\begin{bmatrix}0\\0 \\1\end{bmatrix}\}$, 写成矩阵就是$\begin{bmatrix}1&0&0\\ 0&1&0 \\ 0&0&1\end{bmatrix}$, 称为单位矩阵, 用$\boldsymbol{I}$或$\boldsymbol{E}$表示.
$$
\boldsymbol{I}_n=\begin{bmatrix}
1 & 0 & \cdots & 0 \\
0 & 1 & \cdots & 0 \\
\vdots & \vdots & \ddots & \vdots \\
0 & 0 & \cdots & 1
\end{bmatrix}
$$
单位矩阵是方阵.

零矩阵

所有元素都为零的矩阵称为零矩阵.
$$
\boldsymbol{O}_{mn}=\begin{bmatrix}
0 & 0 & \cdots & 0 \\
0 & 0 & \cdots & 0 \\
\vdots & \vdots & \ddots & \vdots \\
0 & 0 & \cdots & 0
\end{bmatrix}
$$

对角矩阵

对于$n$阶方阵中$i=j$的那些元素构成了矩阵的主对角线, 方阵中除了主对角线的元素外, 其他元素都为零的矩阵称为对角矩阵.
$$
\boldsymbol{A}=\begin{bmatrix}
a_{11} & 0 & \cdots & 0 \\
0 & a_{22} & \cdots & 0 \\
\vdots & \vdots & \ddots & \vdots \\
0 & 0 & \cdots & a_{nn}
\end{bmatrix}
$$

对称矩阵

以主对角线为对称轴两侧元素对称分布的矩阵, 即$a_{ij}=a_{ji},i\neq j$, 这样的矩阵称为对称矩阵.

分块矩阵

我们可以把矩阵分块, 每个块都是一个新的矩阵, 称为子矩阵或者分块.

初等变换

我们可以对矩阵中的行或列进行以下操作($r_i$表示行, $c_i$表示列:

  • 互换两行(列): $r_i\leftrightarrow r_j,c_i\leftrightarrow c_j$
  • 用一个非零数乘以一行(列): $kr_i, kc_i $
  • 一行(列)的元素的某倍加到另一行(列): $r_j+kr_i,c_j+kc_i$

这些操作称为矩阵的初等行(列)变换, 即初等变换.

设 $\boldsymbol{A}$ 是一个矩阵, 若满足以下条件, 则称其为阶梯型矩阵:

  1. 所有元素全为$0$ 的行(如果存在的话), 都在矩阵的最下方.
  2. 每一行第一个非零元素(称为首非零元)的列标, 随着行标的增大而严格增大. 也就是说, 若第 $i$ 行的首非零元在第 $j_i$列, 第 $k$ 行的首非零元在第 $j_k$ 列,当 $i < k$时, 有 $j_i < j_k$.

设 $\boldsymbol{A}$ 是一个阶梯型矩阵, 若还满足以下条件,则称$\boldsymbol{A}$为简化阶梯型矩阵(也称为行最简形矩阵):

  1. 每个首非零元都为1
  2. 每个首非零元所在列的其余元素都为0

单位矩阵经过一次初等变换得到的矩阵称为初等矩阵.

使用Python能很快的得到简化阶梯型矩阵, 这里我们使用sympy库:

from sympy.matrices import Matrix
A = Matrix([[1, 2, 3], [4, 5, 6]])
print(f'矩阵A: {A}')
Arref,Apivots=A.rref()
print(f'简化阶梯矩阵: {Arref}')
print(f'主元: {Apivots}')
'''
矩阵A: Matrix([[1, 2, 3], [4, 5, 6]])
简化阶梯矩阵: Matrix([[1, 0, -1], [0, 1, 2]])
主元: (0, 1)
'''

矩阵加法

定义: 设矩阵$(\boldsymbol{A}_{ij})_{m\times n}$和$(\boldsymbol{B}_{ij})_{m\times n}$有相同的形状. $\boldsymbol{C}=\boldsymbol{A}+\boldsymbol{B}$, 则矩阵的元素为$c_{ij}=a_{ij}+b_{ij}$

用二维数组表示矩阵, 加法运算如下:

import numpy as np

a=np.array([[1,3,5],[2,4,6]])
b=np.array([[1,2,3],[4,5,6]])
c=a+b
print(c)
'''
[[ 2  5  8]
 [ 6  9 12]]
'''

np.asmatrix也可以进行计算

import numpy as np

a=np.asmatrix('1 2 3; 4 5 6')
b=np.asmatrix('1 3 5; 2 4 6')
c=a+b
print(c)

同时也要注意计算时的广播机制, 3×3 的二维数组与长为 3 的一维数组相加, 等效于把数组 b 在二维上重复 3 次再运算

import numpy as np   
a = np.array([[0 ,0 ,0 ],
              [10,10,10],
              [20,20,20]]) 
b = np.array([0,1,2]) 
print(a + b)
'''
[[ 0  1  2]
 [10 11 12]
 [20 21 22]
'''

矩阵的加法也可以转换为向量的加法, 即将矩阵写成向量形式后再进行加法.

矩阵数乘

定义: 矩阵$(\boldsymbol{A}_{ij})_{m\times n}$与标量$c$的数量乘法记为$\boldsymbol{B}=c\boldsymbol{A}$, $(\boldsymbol{A}_{ij})_{m\times n}$和$(\boldsymbol{B}_{ij})_{m\times n}$有相同的形状,且$b_{ij}=ca_{ij}$.

这个时候我们也可以定义矩阵的减法:
$$
\boldsymbol{A}-\boldsymbol{B}=\boldsymbol{A}+(-1)\boldsymbol{B}
$$
用Python操作如下:

import numpy as np

a = np.array([[1, 2], [3, 4]])
b =2 * a
print(b)
'''
[[2 4] 
 [6 8]]
''' 

我们可以把向量的概念进行扩展, 把矩阵也看成一种向量. 两个矩阵的加法符合加法封闭原则, 矩阵的数乘符合乘法封闭原则, 我们可以说$m\times n$的矩阵集合$\mathbb{M}_{mn}$是向量空间.矩阵也可以像向量一样有线性组合的形式.

矩阵乘法

定义: 矩阵$(\boldsymbol{A}_{ij})_{m\times r}$和$(\boldsymbol{B}_{ij})_{r\times n}$相乘, 记作$\boldsymbol{C=AB}$, 其中$\boldsymbol{C}=(c_{ij})_{m\times n}$,$c{ij}=a_{i1}b_{1j}+\cdots+a_{in}b_{nj}=\sum_{k=1}^n a_{ik}b_{kj}$

显然$\boldsymbol{AB}\neq\boldsymbol{BA}$, 交换律不可用.

这时候用numpy中的数组表示矩阵乘法*的时候是对应元素相乘, 与矩阵乘法的定义不同.

我们使用np.dot()来表示矩阵乘法

import numpy as np

a=np.array([[1,2],[3,4]])
b=np.array([[3,4,5],[6,7,8]])
c=np.dot(a,b)
print(c)
'''
[[15 18 21]
 [33 40 47]]
'''

或者创建矩阵对象, 这时候就可以用*

import numpy as np

a=np.asmatrix('1 2;3 4 ')
b=np.asmatrix('3 4 5;6 7 8')
c=a*b
print(c)
'''
[[15 18 21]
 [33 40 47]]
'''

矩阵运算的性质:

  • 加法和数乘
  • $\boldsymbol{A}+\boldsymbol{B}=\boldsymbol{B}+\boldsymbol{A}$
  • $\boldsymbol{A}+(\boldsymbol{B}+\boldsymbol{C})=(\boldsymbol{A}+\boldsymbol{B})+\boldsymbol{C}$
  • $\boldsymbol{A}+\boldsymbol{O}=\boldsymbol{O}+\boldsymbol{A}=\boldsymbol{A}$
  • $(kl)\boldsymbol{A}=k(l\boldsymbol{A})$
  • $(k+l)\boldsymbol{A}=k\boldsymbol{A}+l\boldsymbol{A}$
  • $k(\boldsymbol{A}+\boldsymbol{B})=k\boldsymbol{A}+k\boldsymbol{B}$
  • 矩阵乘法
  • $\boldsymbol{A}(\boldsymbol{BC})=(\boldsymbol{AB})\boldsymbol{C}$
  • $\boldsymbol{A}(\boldsymbol{B}+\boldsymbol{C})=\boldsymbol{AB}+\boldsymbol{AC}$
  • $(\boldsymbol{A}+\boldsymbol{B})\boldsymbol{C}=\boldsymbol{AC}+\boldsymbol{BC}$
  • $\boldsymbol{AI}=\boldsymbol{IA}=\boldsymbol{A}$
  • $r(\boldsymbol{AB})=(r\boldsymbol{A})\boldsymbol{B}=\boldsymbol{A}(r\boldsymbol{B})$

方阵还有幂运算
$$
\boldsymbol{A}^k=\underbrace{\boldsymbol{AA}\cdots\boldsymbol{AA}}_{k\text{个}}
$$
性质:

  • $\boldsymbol{A}^r\boldsymbol{A}^s=\boldsymbol{A}^{r+s}$
  • $(\boldsymbol{A}^r )^s=\boldsymbol{A}^{rs}$

numpy中的指数运算函数np.power()并不能完成对方阵的幂运算(哪怕是矩阵对象), 他对里面的元素进行了幂运算, 与我们的定义要求不符.

如果想要进行矩阵指数运算, 可以这么做

from numpy.linalg import matrix_power
import numpy as np

M=np.asmatrix('1 2; 3 4 ')
print(matrix_power(M, 4))
'''
[[199 290]
 [435 634]]
'''

或者直接使用**运算符

import numpy as np

M=np.asmatrix('1 2; 3 4 ')
print(M**4)

我们还有一个重要结论: 初等行变换相当于左乘一个相应的初等矩阵, 初等列变换相当于右乘一个相应的初等矩阵.

线性映射

线性

首先我们定义线性

定义: 如果函数$f(x)$是线性的, 那么他必须满足:

  • 可加性: $f(x_1+x_2)=f(x_1)+f(x_2)$
  • 齐次性: $f(cx)=cf(x)$

直线不等于线性函数. 对于$n$元函数, 我们有$f(x_1,x_2,\cdots,x_n)=k_1x_1+k_2x_2+\cdots+k_nx_n$

观察以下线性方程组
$$
\begin{cases}
y_1=a_{11}x_1+\cdots+a_{1n}x_n\\
\vdots\\
y_m=a_{m1}x_1+\cdots+a_{mn}x_n
\end{cases}
$$
改写为矩阵形式
$$
\begin{bmatrix}y_1\\ \vdots \\ y_n\end{bmatrix}=\begin{bmatrix}a_{11} & \cdots &a_{1n}\\ \vdots &\ddots & \vdots \\ a_{m1} & \cdots & a_{mn}\end{bmatrix}\begin{bmatrix}x_1\\ \vdots \\ x_n\end{bmatrix}
$$

$$
\boldsymbol{y=Ax}
$$
如果将上式看成函数, 其符合线性的要求; 如果看成向量, 则符合加法和数乘封闭, 向量$\boldsymbol{x}$通过矩阵$\boldsymbol{A}$变换成了向量$\boldsymbol{y}$.

在机器学习中线性回归指的则是, 对于如下线性回归模型
$$
\boldsymbol{Y}=\beta_1\boldsymbol{x_1}+\beta_2\boldsymbol{x_2}+\cdots+\beta_n\boldsymbol{x_n}
$$
即$\beta_1\boldsymbol{x_1}+\beta_2\boldsymbol{x_2}+\cdots+\beta_n\boldsymbol{x_n}$是参数$\beta_i$的线性函数, 而非$\boldsymbol{x_i}$的线性函数.

线性映射

映射: 设两个非空集合$\mathbb{A}$和$\mathbb{B}$之间存在着对应关系$\boldsymbol{T}$, 且对于$\mathbb{A}$中的每一个元素$a$, $\mathbb{B}$中总有唯一一个元素$b$与之对应, 这种对应称为从$\mathbb{A}$到$\mathbb{B}$的映射.$b$称为元素$a$在映射$\boldsymbol{T}$下的像, $a$称为$b$关于映射$\boldsymbol{T}$的原像.

线性代数将$\mathbb{R}\mapsto\mathbb{R}$的映射推广到了$\mathbb{R}^n\mapsto\mathbb{R}^m$的映射, 其他函数的概念也得到了推广

函数映射
函数$f:\mathbb{R}\mapsto\mathbb{R}$线性映射$\boldsymbol{T}: \mathbb{R}^n\mapsto\mathbb{R}^m$
输入$x\in\mathbb{R}$输入$\boldsymbol{x}\in\mathbb{R}^m$
输出$f(x)\in\mathbb{R}$输出$\boldsymbol{T}(\boldsymbol{x})=\boldsymbol{Ax}\in\mathbb{R}^m$

定义: $\mathbb{V}$和$\mathbb{V}’$是实数域上的向量空间, $\mathbb{V}$和$\mathbb{V}’$的一个映射$\boldsymbol{T}$如果具有加法和数乘运算, 即
$$
\boldsymbol{T(u+v)=T(u)+T(v)}\
\boldsymbol{T}(c\boldsymbol{u})=c\boldsymbol{T(u)}
$$
则称$\boldsymbol{T}$为$\mathbb{V}$到$\mathbb{V}’$的线性映射.

我们可以用矩阵表示线性映射, 显然其符合加法和数乘封闭的要求, 符合线性映射的定义.

前面讲过的坐标变换公式$\boldsymbol{x=Px’}$, 表示的正是向量在同一向量空间不同基下的映射.

对于在同一向量空间发生的线性映射, 我们称为线性变换.

矩阵和线性映射

如果将矩阵作为线性映射或线性变换, 以$\boldsymbol{Av}$的形式就能实现$\boldsymbol{v}$的线性映射输出另一个向量.

二维空间中的向量$\begin{bmatrix}x\\y\end{bmatrix}$在标准基$\{\begin{bmatrix}1\\0\end{bmatrix},\begin{bmatrix}0\\1\end{bmatrix}\}$构建的坐标系下经过映射$\boldsymbol{T}=\begin{bmatrix}1 &0\\0 &1\end{bmatrix}$变换为$\begin{bmatrix}x\\-y\end{bmatrix}$, 称为相对$x$轴的对称变换.

来看一个旋转变换的实例, 二维向量空间中, 向量旋转$\theta$角变换为另一个向量.
$$
\begin{bmatrix}x’\\y’\end{bmatrix}=\begin{bmatrix}\cos\theta & -\sin\theta\\\sin\theta & \cos \theta\end{bmatrix}\begin{bmatrix}x\\y\end{bmatrix}
$$
这里$\begin{bmatrix}\cos\theta & -\sin\theta\\\sin\theta & \cos \theta\end{bmatrix}$即为熟悉爱你旋转变换的矩阵.

上面从线性映射的角度讲了矩阵乘向量的含义, 那矩阵乘矩阵呢?

不难推广, $\boldsymbol{AB}$即用映射$\boldsymbol{A}$对矩阵/向量$\boldsymbol{B}$实施线性映射(或线性变换), 矩阵和矩阵的乘法其实就是连续发生的线性映射.

我们可以总结为一句话: 矩阵就是线性映射.

齐次坐标系和仿射变换

平移不是线性变换, 就不能用矩阵的乘法表示. 出于形式的统一, 数学上引入了齐次坐标系, 在笛卡尔坐标系中, 每个点的坐标用$(x,y)$表示, 而在齐次坐标系中变成了$(x’,y’,w)$, 其中$x=x’/w,y=y’/w$, 通常设$w=1$, 这样平移也可以用矩阵乘法表示了.

对于二维向量空间的齐次坐标系, 以下几个矩阵分别实现了齐次坐标中的旋转伸缩平移变换.

  • 旋转:$\boldsymbol{A}=\begin{bmatrix}\cos\theta &-\sin\theta &0\\\sin\theta&\cos\theta&0\\0&0&1\end{bmatrix}$,$\theta$表示旋转的角度.
  • 伸缩:$\boldsymbol{C}=\begin{bmatrix}r&0&0\\0&r&0\\0&0&1\end{bmatrix}$,$r$为伸缩倍数.
  • 平移:$\boldsymbol{E}=\begin{bmatrix}1&0&h\\0&1&k\\0&0&1\end{bmatrix}$,$h,k$分别为$x,y$轴移动的长度.

对于某个向量实现伸缩旋转平移变换则可以写成:
$$
\boldsymbol{EAC}\begin{bmatrix}x\\y\\1\end{bmatrix}=\begin{bmatrix}1&0&h\\0&1&k\\0&0&1\end{bmatrix}\begin{bmatrix}\cos\theta &-\sin\theta &0\\\sin\theta&\cos\theta&0\\0&0&1\end{bmatrix}\begin{bmatrix}r&0&0\\0&r&0\\0&0&1\end{bmatrix}\begin{bmatrix}x\\y\\1\end{bmatrix}\\=\begin{bmatrix}r\cos\theta&-r\sin\theta&hr\cos\theta-kr\sin\theta\\r\sin\theta&r\cos\theta&hr\sin\theta+kr\cos\theta\\0&0&1\end{bmatrix}\begin{bmatrix}x\\y\\1\end{bmatrix}
$$

缩放, 旋转, 平移变换统称为仿射变换.

OpenCV中可以实现图形变换.例如:

平移:

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('image.jpg')
M = np.float32([[1,0,100 ],[0,1,50]])
#平移变换矩阵
rows,cols,ch=img.shape

res = cv2.warpAffine(img,M,(cols,rows))
#仿射变换函数
plt.subplot(121)
plt.imshow(img)
plt.title('input')

plt.subplot(122)
plt.imshow(res)
plt.title('output')

缩放:

M = np.float32([[0.5,0,0],[0,0.5,0]])
res = cv2.warpAffine(img,M,(row//2,cols//2))

旋转:

M = np.float32([[0.702,-0.702,0],[0.702,0.702,0]])

res = cv2.warpAffine(img,M,(4000,4000))

但是用这种方式进行旋转有个问题, 就是其是按照原点(左上角)为旋转中心旋转, 会造成图片损失居多, 可以使用专用的旋转变换矩阵:

M = cv2.getRotationMatrix2D((rows/2,cols/2),45,1)
#getRotationMatrix2D(center,angle,scale)设置旋转中心, 旋转角度, 缩放角度
res = cv2.warpAffine(img,M,(rows,cols))

矩阵的逆和转置

逆矩阵

定义: 设$\boldsymbol{A}$是$n\times n$的方阵, 如果存在矩阵$\boldsymbol{B}$, 使得
$$
\boldsymbol{AB=BA=I}_n
$$
其中$\boldsymbol{I}_n$是单位矩阵, 我们就说矩阵$\boldsymbol{A}$是可逆的(非奇异的), 称矩阵$\boldsymbol{B}$为$\boldsymbol{A}$的逆矩阵, 记作$\boldsymbol{A}^{-1}$
$$
\boldsymbol{AA}^{-1}=\boldsymbol{A}^{-1}\boldsymbol{A}=\boldsymbol{I}_n
$$
可逆矩阵都是方阵, 单位矩阵的逆矩阵还是单位矩阵.

颜色学中会用到逆矩阵的应用和计算, 使用三个相对独立的向量描述颜色, 组成颜色空间.

  • RGB: 通过 Red, Green, Blue三原色来描述颜色的颜色空间. 常用十六进制的6个数字, 如#336699表示一种颜色, 这里的6个数字被分为三组, 即(33,66,99), 分别对应红(R), 绿(G), 蓝(B) 三种颜色. 每种颜色的成分可以用从0到255(十进制)的数表示, 0是最低级, 255是最高级. 此外还可以用诸如 rgb(255,0,0)或rgb(100%,0%,0%)(这两者都表示红色)的形式表示颜色.
  • YIQ:是NTSC制式的模拟彩色电视所使用的颜色空间, Y表示图像的亮度, I, Q是色调的两个分量, 1代表从橙色到青色的颜色变化,Q代表从紫色到黄绿色的颜色变化.
  • YUV: 是 PAL 制式的模拟彩色电视所使用的颜色空间. Y代表亮度, U,V代表色差, 构成彩色的两个分量. YUV颜色空间将YUV分离,如果只有Y分量而没有UV分量, 那么表示的图像就是黑白灰度图像, 从而让黑白电视机也能接收彩色电视机的信号.

下面为RGB变换为YUV的方式
$$
\begin{bmatrix}
Y \\
U \\
V
\end{bmatrix}
\begin{bmatrix}
0.299 & 0.587 & 0.114 \\
-0.14713 & -0.28886 & 0.436 \\
0.615 & -0.51499 & -0.10001
\end{bmatrix}
\begin{bmatrix}
R \\
G \\
B
\end{bmatrix}
$$
反过来通过逆矩阵就可以有YUV变换为RGB的关系

$$
\begin{bmatrix}
R \\
G \\
B
\end{bmatrix}
\begin{bmatrix}
0.299 & 0.587 & 0.114 \\
-0.14713 & -0.28886 & 0.436 \\
0.615 & -0.51499 & -0.10001
\end{bmatrix}^{-1}
\begin{bmatrix}
Y \\
U \\
V
\end{bmatrix}
$$
当然计算逆矩阵可以通过numpy提供的inv()函数进行计算.

import numpy as np
from numpy.linalg import inv

a = np.array([[0.299, 0.587, 0.114],
              [-0.147, -0.289, 0.436],
              [0.615, - 0.515 ,-0.1]])

# 计算矩阵 a 的逆矩阵,并将结果四舍五入到小数点后三位
inverse_a = np.round(inv(a), 3)
print(inverse_a)

逆矩阵有一些性质

  • $(\boldsymbol{A}^{-1})^{-1}=\boldsymbol{A}$
  • $(c\boldsymbol{A})^{-1}=\dfrac{1}{c}\boldsymbol{A}^{-1}$
  • $\boldsymbol{AB}^{-1}=\boldsymbol{B}^{-1}\boldsymbol{A}^{-1}$
  • $(\boldsymbol{A}^n)^{-1}=(\boldsymbol{A}^{-1})^n$
  • $(\boldsymbol{A}^T)^{-1}=(\boldsymbol{A}^{-1})^T$
  • $\boldsymbol{A}^{-k}=\underbrace{\boldsymbol{A}^{-1}\boldsymbol{A}^{-1}\cdots\boldsymbol{A}^{-1}}_{k\text{个}}$
  • $\boldsymbol{A}$的各列线性无关
  • $\boldsymbol{A}^T$是可逆矩阵
  • 当且仅当$|\boldsymbol{A}|\neq0$时, $\boldsymbol{A}$可逆.

转置矩阵

将矩阵$\boldsymbol{A}=(a_{ij})_{m\times n}$的行列互换后所得的矩阵称为$\boldsymbol{A}$的转置矩阵, 记为$\boldsymbol{A}^T$, 转置后第$(i,j)$个元素变成了第$(j,i)$个元素.矩阵的形状由$m\times n$变成了$n\times m$.

如果用np.asmatrix()创立矩阵用其就可以计算转置矩阵.

import numpy as np
A = np.asmatrix('2 3 5; 7 11 13')
print(A)
print(A.T)

计算结果

[[ 2  3  5]
 [ 7 11 13]]
[[ 2  7]
 [ 3 11]
 [ 5 13]]

如果用二维数组表示矩阵, 则使用np.transpose()也能得到相同结果.

转置在机器学习中有着重要作用, 这里仅简单提一下在点积中的作用.

设向量$\boldsymbol{u}=\begin{bmatrix}u_1\\\vdots\\u_n\end{bmatrix},\boldsymbol{v}=\begin{bmatrix}v_1\\\vdots\\ v_n\end{bmatrix}$, 则$\boldsymbol{u}^T\boldsymbol{v}=u_1v_1+\cdots+u_nv_n=\boldsymbol{u}\cdot\boldsymbol{v}$

转置矩阵有一些运算性质:

  • $(\boldsymbol{A}^T)^T=\boldsymbol{A}$
  • $(\boldsymbol{A}+\boldsymbol{B})^T=\boldsymbol{A}^T+\boldsymbol{B}^T$
  • $(\boldsymbol{AB})^T=\boldsymbol{B}^T\boldsymbol{A}^T$
  • $(c\boldsymbol{A})^T=c\boldsymbol{A}^T$

定义: 矩阵$\boldsymbol{A}$是$n\times n$方阵, 若满足$\boldsymbol{A}^T=\boldsymbol{A}$, 则$\boldsymbol{A}$称为对称矩阵.

其有一个特殊的性质:$\boldsymbol{AB}=(\boldsymbol{AB})^T=\boldsymbol{B}^T\boldsymbol{A}^T=\boldsymbol{BA}$

矩阵$\boldsymbol{LU}$分解

上三角矩阵: 对角线以下的元素都为零的矩阵

下三角矩阵: 对角线以上的元素都为零的矩阵

单位下三角矩阵$\boldsymbol{L}$: 主对角线元素都为1的下三角矩阵.

定义:将$n$阶方阵$\boldsymbol{A}$表示为两个$n$阶三角矩阵的乘积:
$$
\boldsymbol{A}=\boldsymbol{LU}
$$
其中$\boldsymbol{L}$是单位下三角矩阵, $\boldsymbol{U}$是上三角矩阵, 这种形式称为矩阵$\boldsymbol{A}$的$\boldsymbol{LU}$分解.

注:不是所有的可逆矩阵都可以进行$\boldsymbol{LU}$分解, 至少主对角线上的第一个元素不能为零.

如果不能分解, 就引入一个置换矩阵$\boldsymbol{P}$, 即
$$
\boldsymbol{PA}=\boldsymbol{LU}\
\boldsymbol{P}^{-1}\boldsymbol{PA}=\boldsymbol{P}^{-1}\boldsymbol{LU}\
\boldsymbol{A}=\boldsymbol{PLU}
$$
这种分解称为$\boldsymbol{PLU}$分解

对于一个 $n\times n$ 的矩阵 $\boldsymbol{A}$可借助高斯消元法来完成分解。在高斯消元过程中,通过一系列行变换将矩阵 $\boldsymbol{A}$ 转化为上三角矩阵 $\boldsymbol{U}$,而这些行变换所对应的初等矩阵乘积的逆就是下三角矩阵 $\boldsymbol{L}$。

以 $3\times3$ 矩阵为例,设矩阵 $\boldsymbol{A}=\begin{bmatrix}a_{11}&a_{12}&a_{13}\\a_{21}&a_{22}&a_{23}\\a_{31}&a_{32}&a_{33}\end{bmatrix}$,经过一系列消元操作后,会得到下三角矩阵 $\boldsymbol{L}=\begin{bmatrix}1&0&0\\l_{21}&1&0\\l_{31}&l_{32}&1\end{bmatrix}$ 和上三角矩阵 $\boldsymbol{U} = \begin{bmatrix}u_{11}&u_{12}&u_{13}\\0&u_{22}&u_{23}\\0&0&u_{33}\end{bmatrix}$,使得 $\boldsymbol{A} =\boldsymbol{ LU}$.

即$\boldsymbol{L}$记录了行变换的过程, $\boldsymbol{U}$记录了行变换的结果, 即$\boldsymbol{LU}$分解的本质是高斯消元法. 原来的矩阵能用比较简单的矩阵进行表示, 可以简化矩阵的运算.

scipy中提供了专用的函数lu():

import pprint
from scipy.linalg import lu
import numpy as np

A=np.array([[7,3,-1,2],[3,8,1,-4],[-1,1,4,-1],[2,-4,-1,6]])
P,L,U=lu(A)

print("A:")
pprint.pprint(A)
print("P:")
pprint.pprint(P)
print("L:")
pprint.pprint(L)
print("U:")
pprint.pprint(U)

输出

A:
array([[ 7,  3, -1,  2],
       [ 3,  8,  1, -4],
       [-1,  1,  4, -1],
       [ 2, -4, -1,  6]])
P:
array([[1., 0., 0., 0.],
       [0., 1., 0., 0.],
       [0., 0., 1., 0.],
       [0., 0., 0., 1.]])
L:
array([[ 1.        ,  0.        ,  0.        ,  0.        ],
       [ 0.42857143,  1.        ,  0.        ,  0.        ],
       [-0.14285714,  0.21276596,  1.        ,  0.        ],
       [ 0.28571429, -0.72340426,  0.08982036,  1.        ]])
U:
array([[ 7.        ,  3.        , -1.        ,  2.        ],
       [ 0.        ,  6.71428571,  1.42857143, -4.85714286],
       [ 0.        ,  0.        ,  3.55319149,  0.31914894],
       [ 0.        ,  0.        ,  0.        ,  1.88622754]])

行列式

计算方法和意义

行列式是一些按照某种特定方式排列的方阵所确定的一个数.

$$
\begin{vmatrix}
a_{11} & a_{12} & \cdots & a_{1n}\\
a_{21} & a_{22} & \cdots & a_{2n}\\
\vdots&\vdots&\ddots&\vdots\\
a_{n1}&a_{n2}&\cdots&a_{nn}
\end{vmatrix}=\sum_{j_1j_2\cdots j_n}(-1)^{\tau(j_1j_2\cdots j_n)}a_{1j_1}a_{2j_2}\cdots a_{nj_n}
$$

其中$\tau{(j_1j_2\cdots j_n)}$表示逆序数.

这里不再详细介绍.

通常$|\boldsymbol{A}|$或$\det\boldsymbol{A}$表示方阵的行列式.

行列式表征矩阵中线性无关的列向量在空间围成的多面体的体积(二维则退化为平面面积)

矩阵列向量和行列式有如下关系:
$$
矩阵列向量线性无关\leftrightarrow|\boldsymbol{A|}\neq0\
矩阵列向量线性相关\leftrightarrow|\boldsymbol{A|}=0\
$$
行列式的计算可以用NumPy库的np.linalg.det()函数来实现

import numpy as np
a=np.array([[6,1,1],[4,-2,5],[2,8,7]])
print(np.linalg.det(a))#输出-306.0

行列式也有一些性质如下:

矩阵$\boldsymbol{A}=(a_{ij})_{n\times n}$, $c$为非零的标量, 则:

  • $|c\boldsymbol{A}|=c^n|\boldsymbol{A}|$
  • $|\boldsymbol{AB}|=|\boldsymbol{A}||\boldsymbol{B}|$
  • $|\boldsymbol{A}^T|=|\boldsymbol{A}|$
  • $|\boldsymbol{A^{-1}}|=\dfrac{1}{|\boldsymbol{A}|}$
  • $|\boldsymbol{A}|=\prod_{i=1}^n\lambda_i$, $\lambda_i$是$\boldsymbol{A}$的特征值

线性方程组

线性方程组是由若干个线性方程组成的方程组. 一般形式可以表示为:
$$
\begin{cases}
a_{11}x_1 + a_{12}x_2 + \cdots + a_{1n}x_n = b_1 \\
a_{21}x_1 + a_{22}x_2 + \cdots + a_{2n}x_n = b_2 \\
\vdots \\
a_{m1}x_1 + a_{m2}x_2 + \cdots + a_{mn}x_n = b_m
\end{cases}
$$
其中, $x_1,x_2,\cdots,x_n$ 是未知量( 变量), $a_{ij}$( $i = 1,2,\cdots,m$;$j = 1,2,\cdots,n$)是方程组的系数, $b_1,b_2,\cdots,b_m$ 是常数项. $m$ 表示方程的个数, $n$ 表示未知量的个数, $m$ 和 $n$ 可以相等也可以不相等。

方程组的解是指一组使得方程组中每个方程都成立的未知量的值. 求解线性方程组的方法有很多, 例如高斯消元法, 矩阵变换法等.

也可以简写为
$$
\boldsymbol{Ax=b}
$$
其中$\boldsymbol{A}$为系数矩阵, $\boldsymbol{b}$为方程组等号右侧的常数项, $\boldsymbol{x}$为未知量.

齐次线性方程组

齐次线性方程组是特殊的线性方程组,它的常数项都为 $0$。其一般形式为:
$$
\begin{cases}
a_{11}x_1 + a_{12}x_2 + \cdots + a_{1n}x_n = 0 \\
a_{21}x_1 + a_{22}x_2 + \cdots + a_{2n}x_n = 0 \\
\vdots \\
a_{m1}x_1 + a_{m2}x_2 + \cdots + a_{mn}x_n = 0
\end{cases}
$$

齐次线性方程组一定有解, 因为 $x_1 = x_2 = \cdots = x_n = 0$ 就是它的一组解, 称为零解.

判断齐次线性方程组是否有非零解是一个重要的问题, 当系数矩阵的秩 $r(\boldsymbol{A}) < n$($n$ 为未知量个数)时, 齐次线性方程组有非零解;

当 $r(\boldsymbol{A})=n$ 时,只有零解.

未知量和方程个数相同的方程组

当线性方程组中未知量的个数 $n$ 和方程的个数 $m$ 相等时,即 $m = n$,此时方程组的形式为:
$$
\begin{cases}
a_{11}x_1 + a_{12}x_2 + \cdots + a_{1n}x_n = b_1 \\
a_{21}x_1 + a_{22}x_2 + \cdots + a_{2n}x_n = b_2 \\
\vdots \\
a_{n1}x_1 + a_{n2}x_2 + \cdots + a_{nn}x_n = b_n
\end{cases}
$$
对于这种方程组,可以通过计算系数矩阵 $\boldsymbol{A}=(a_{ij})$ 的行列式 $|\boldsymbol{A}|$ 来判断解的情况

当 $|\boldsymbol{A}|\neq 0$ 时,方程组有唯一解,可以使用克莱姆法则求解,即 $x_j = \dfrac{|\boldsymbol{A_j}|}{|\boldsymbol{A}|}$($j = 1,2,\cdots,n$),其中 $|\boldsymbol{A}_j|$ 是将系数矩阵 $\boldsymbol{A}$ 的第 $j$ 列元素替换为常数项 $b_1,b_2,\cdots,b_n$ 后得到的行列式.

当 $|\boldsymbol{A}| = 0$ 时, 方程组可能无解或有无穷多解, 需要进一步分析.

关于求解线性方程组的Python解法, 这里不再列出.

矩阵的秩

任何一个矩阵, 其行/列向量能生成空间, 虽然他们是不同维度向量空间的子空间, 但是都是相同维数的子空间, 这个维数表征了矩阵的本色.

定义: 矩阵的列(行)向量生成的子空间维数成为矩阵的列(行)秩, 对于任何矩阵, 列秩等于行秩, 统称为矩阵的秩, 记作$\mathbf{rank}(\boldsymbol{A})$或者$r(\boldsymbol{A})$.

阶梯型矩阵的秩等于非零行的个数, 且主元所在的列构成了列向量组的一个极大线性无关组.

矩阵的初等行变换不改变矩阵的列向量组的线性无关性 , 从而不改变矩阵的列秩

那么我们比较容易算出矩阵的秩

矩阵$\boldsymbol{A}$, 经过初等行变换得到阶梯型矩阵$\boldsymbol{J}$,则$\boldsymbol{A}$的秩等于$\boldsymbol{J}$的非零行个数.

如果$n$阶矩阵的秩为$n$, 那么称其为满秩矩阵.

前面提到过, 如果用矩阵乘以某个向量, 相当于把该向量映射到矩阵的列空间, 即所得向量的维度就是此映射矩阵的秩.

例如$\boldsymbol{T}=\begin{bmatrix}2 &3& 4\\5& 5& 6\\7& 8& 9\end{bmatrix}$,经$\boldsymbol{A}=\begin{bmatrix}1 &0& 0\\0& 1& 0\\0& 0& 0\end{bmatrix}$映射得到

$\boldsymbol{T}’=\boldsymbol{AT}=\begin{bmatrix}1 &0& 0\\0& 1& 0\\0& 0& 0\end{bmatrix}\begin{bmatrix}2 &3& 4\\5& 5& 6\\7& 8& 9\end{bmatrix}=\begin{bmatrix}2 &3& 4\\5& 5& 6\\0&0&0\end{bmatrix}$

接下来我们用np.linalg.matrix_rank()求矩阵的秩

import numpy as np

T=np.matrix(" 2 3 4;5 5 6;7 8 9")
A=np.matrix("1 0 0;0 1 0;0 0 0")
T2=A*T
rank_T=np.linalg.matrix_rank(T)
rank_A=np.linalg.matrix_rank(A)
rank_T2=np.linalg.matrix_rank(T2)
print(f"rank(T)={rank_T}, rank(A)={rank_A}, rank(T2)={rank_T2}")
'''
rank(T)=3, rank(A)=2, rank(T2)=2
'''

可以通过映射矩阵$\boldsymbol{A}$的秩得到像$\boldsymbol{T}’$的秩.

对于方阵$\boldsymbol{A}_{n\times n}$以下的几个结论相互等效:

  • $\boldsymbol{A}$是可逆矩阵
  • 方程$\boldsymbol{Ax=b}$有唯一解
  • $\boldsymbol{A}$的行(列)线性无关
  • $\boldsymbol{A}$的秩为$n$
  • $\boldsymbol{A}^T$可逆
  • $|\boldsymbol{A}|\neq0$
  • $\boldsymbol{AA}^{-1}=\boldsymbol{I}$

稀疏矩阵

稀疏矩阵在机器学习和深度学习中有着比较重要的地位, 这里将其单独拉出来一章探讨

定义与基本概念

稀疏矩阵 (Sparse Matrix) 是指矩阵中大部分元素为零 (或可忽略) 的矩阵. 设矩阵 $\boldsymbol{A} \in \mathbb{R}^{m \times n}$, 若其非零元素个数 $nnz \ll m \times n$, 则称 $\boldsymbol{A}$ 为稀疏矩阵.$nnz$是非零元素个数 (Number of Non-Zeros)

指标公式说明
稀疏度$sparsity = 1 – \frac{nnz}{m \times n}$值越大越稀疏
密度$density = \frac{nnz}{m \times n}$值越小越稀疏

稀疏矩阵的生成

手动创建

坐标格式 (COO) 示例

import numpy as np
from scipy.sparse import coo_matrix

# 定义非零元素的位置和值
rows = np.array([0, 1, 2])     # 行索引
cols = np.array([0, 1, 2])     # 列索引
data = np.array([5, 8, 3])     # 元素值

# 创建 COO 格式稀疏矩阵
A = coo_matrix((data, (rows, cols)), shape=(3,3))
print(A.toarray())

输出:

[[5 0 0]
 [0 8 0]
 [0 0 3]]

自动生成方法

随机稀疏矩阵生成

from scipy.sparse import random

# 生成 1000x1000 稀疏矩阵, 密度 0.1%
A = random(1000, 1000, density=0.001, format='csr')
print(f"非零元素数: {A.nnz}")

稀疏矩阵的压缩存储

压缩原理

通过仅存储非零元素及其位置信息, 减少内存占用:

存储方式稠密矩阵稀疏矩阵 (CSR)
存储空间$O(mn)$$O(nnz + m)$

主要压缩格式

(1) COO 格式 (Coordinate Format)
  • 数据结构
  class COO:
      rows:    [i1, i2, ..., innz]  # 行索引
      cols:    [j1, j2, ..., jnnz]  # 列索引
      data:    [v1, v2, ..., vnnz]  # 元素值
  • 特点: 构建简单, 适合增量修改
(2) CSR 格式 (Compressed Sparse Row)
  • 数据结构
  class CSR:
      row_ptr: [0, 2, 5, ...]  # 行指针数组 (长度 m+1)
      cols:    [j1, j2, ..., jnnz]
      data:    [v1, v2, ..., vnnz]
  • 寻址公式
    第 $i$ 行的元素范围为 cols[row_ptr[i] : row_ptr[i+1]]
(3) CSC 格式 (Compressed Sparse Column)
  • 与 CSR 类似,但按列压缩存储

压缩效率对比

格式存储空间适用场景
COO$3 \times nnz$矩阵构建/转换
CSR$2 \times nnz + m + 1$行操作密集型
CSC$2 \times nnz + n + 1$列操作密集型

压缩存储的数学表示

CSR 格式示例

矩阵:
$\boldsymbol{A} = \begin{bmatrix}
0 & 3 & 0 \\
1 & 0 & 4 \\
0 & 2 & 5
\end{bmatrix}$

转换为 CSR 格式:

row_ptr = [0, 1, 3, 5]  # 行指针
cols    = [1, 0, 2, 1, 2]  # 列索引
data    = [3, 1, 4, 2, 5]  # 元素值

寻址过程:

  • 第 0 行: cols[0:1] = [1], data[0:1] = [3]
  • 第 1 行: cols[1:3] = [0,2], data[1:3] = [1,4]

存储格式转换代码

from scipy.sparse import csr_matrix

# 稠密矩阵 -> CSR
dense_matrix = np.array([[0,3,0],[1,0,4],[0,2,5]])
csr = csr_matrix(dense_matrix)

# CSR -> COO
coo = csr.tocoo()

print("CSR row_ptr:", csr.indptr)
print("CSR columns:", csr.indices)
print("CSR data:   ", csr.data)

输出

CSR row_ptr: [0 1 3 5]
CSR columns: [1 0 2 1 2]
CSR data:    [3 1 4 2 5]

图与矩阵

图的基本概念

图的定义

图(Graph)是由 顶点集合(Vertices)和 边集合(Edges)组成的数学结构, 记为 $G = (V, E)$, 其中:

  • $V = {v_1, v_2, …, v_n}$是顶点的集合.
  • $E = {e_1, e_2, …, e_m} $是边的集合, 每条边连接两个顶点(可以相同, 称为自环).

图的分类

  • 无向图: 边没有方向, 记为 $ (u, v) $, 等价于 $(v, u) $.
  • 有向图: 边有方向, 记为 $ \langle u, v \rangle $, 表示从顶点$u$指向$v$.
  • 加权图: 边带有权重(数值).
  • 简单图: 无自环且无多重边.

基本术语

  • 度(Degree): 顶点连接的边数. 对于有向图分为 入度出度.
  • 路径: 顶点序列 $ v_1, v_2, …, v_k $, 满足相邻顶点间有边.
  • 连通图: 任意两顶点间存在路径.
  • 子图: 顶点和边均为原图子集的图.

特殊图

  • 完全图: 每对顶点间均有边, 记为 $ K_n $(n 个顶点).
  • : 无环且连通的图, 边数为 $ n-1 $.
  • 二分图: 顶点分为两组, 边仅存在于组间.

邻接矩阵(Adjacency Matrix)

1. 定义

邻接矩阵$\boldsymbol{A}$是描述顶点间邻接关系的方阵, 大小为 $ |V| \times |V|$, 元素$ A_{ij} $表示顶点$i$到 $j$ 的边数.

2. 构造规则

  • 无向图: $ A_{ij} = A_{ji} $, 矩阵对称.
  • 有向图: $ A_{ij} $示从$i$ 到$j$的边数.
  • 加权图: $A_{ij} $为边权重(无边时通常为 0 或 ∞).

3. 例子

无向图邻接矩阵示例: 顶点集合 $V = {1, 2, 3}$, 边集合 $E = {(1,2), (1,3), (2,3)}$

邻接矩阵为:
$$
\boldsymbol{A} = \begin{bmatrix}
0 & 1 & 1 \\
1 & 0 & 1 \\
1 & 1 & 0
\end{bmatrix}
$$
4. 性质

  • 矩阵幂的意义: $\boldsymbol{A}^k$的元素 $ (A^k)_{ij}$ 表示从$i$到 $j$ 的长度为$k$ 的路径数.
  • 特征值与谱: 邻接矩阵的特征值反映图的结构特性(如连通性).

关联矩阵(Incidence Matrix)

1. 定义

关联矩阵$\boldsymbol{A}$ 描述顶点与边的关联关系, 大小为 $|V| \times |E| $, 元素 $M_{ie} $表示顶点$i$与边 $e$ 的关系.

2. 构造规则

  • 无向图: $ M_{ie} = 1 $(若顶点$i$是边$e$的端点), 否则 0.
  • 有向图: $M_{ie} = +1 $(顶点$i$是边$e$的起点), $ M_{ie} = -1$(顶点$i$是边$e$的终点), 否则 0.

3. 例子

有向图示例(边 $e_1: 1 \to 2 , e_2: 2 \to 3 , e_3: 3 \to 1$):
$$
\boldsymbol{M} = \begin{bmatrix}
1 & 0 & -1 \\
-1 & 1 & 0 \\
0 & -1 & 1
\end{bmatrix}
$$
4. 应用

  • 网络流分析: 基尔霍夫定律(电路理论).
  • 图的空间分解: 行空间对应顶点, 列空间对应边.

拉普拉斯矩阵(Laplacian Matrix)

1. 定义

拉普拉斯矩阵$\boldsymbol{L}$是图的重要算子, 定义为:
$$
\boldsymbol{L=D-A}
$$

其中$\boldsymbol{D}$是度矩阵(对角矩阵, $D_{ii} = \text{deg}(v_i) $), $\boldsymbol{A}$是邻接矩阵.

2. 归一化拉普拉斯矩阵

常用形式:
$$
L_{\text{sym}} = D^{-1/2} L D^{-1/2} \quad \text{或} \quad L_{\text{rw}} = D^{-1} L
$$
3. 例子

无向图邻接矩阵为$\boldsymbol{A}$ , 度矩阵 ( $\boldsymbol{D} = \text{diag}(2, 2, 2) $), 则:
$$
L = \begin{bmatrix}
2 & -1 & -1 \\
-1 & 2 & -1 \\
-1 & -1 & 2
\end{bmatrix}
$$
4. 性质

  • 对称性: 对无向图, $\boldsymbol{L}$是实对称矩阵.
  • 半正定性: 所有特征值非负.
  • 零特征值: 零特征值的重数等于图的连通分支数.
  • 应用: 谱聚类、图信号处理、热扩散方程.

总结

  • 邻接矩阵: 直接描述顶点间连接关系, 适合路径分析.
  • 关联矩阵: 描述顶点与边的关系, 适合空间分解.
  • 拉普拉斯矩阵: 结合度与邻接信息, 适合谱分析和动态过程建模.

通过矩阵表示, 图论问题可转化为线性代数问题, 为算法设计提供数学基础.

关于图论的Python形式我们以后再探讨.

拾柒bb

真不是鸽太久,是这一块东西有点太多了再加上最近学业紧张所以写的比较少了:(

尽量还是保持一周好几更吧)

本文为2305拾柒原创.
文章作者:拾 柒
文章链接:机器学习的数学基础2—矩阵
版权声明:本博客所有文章除特别声明外,均采用CC BY-NC-SA 4.0许可协议. 转载请注明来自拾 柒
如果觉得有用,可以分享出去~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇