机器学习的课程,老师布置了一个实验报告,当我看到实验内容,傻眼了,手写计算矩阵特征值和特征向量的函数,这给我整无语了,直接调用已有的不好吗, 我直接摆烂。
实验报告放这了,有哪个大佬会的评论区分享一下你的想法
实验图片 example.jpg
一、实验内容及要求
矩阵分解在机器学习领域有重要的地位。本实验通过动手编写程序,实现矩阵分解和图像压缩应用。要求如下:
1、利用numpy库编写两个函数,分别实现特征分解和奇异值分解。具体要求如下:
(1)输入为一个矩阵(ndarray格式),输出为分解后的2个(特征分解)或3个(SVD)矩阵。
(2)必须自己编写,不能调用numpy库或其他库中已有的矩阵分解方法/函数。
(3)自定义一个矩阵,作为参数传递给前面编写的函数,把结果和numpy库的线性代数包中的两个函数 numpy.linalg.eig()、numpy.linalg.svd()进行比较,观察结果是否一致。
2、利用前面编写的函数,分别对图像进行压缩并比较。具体要求如下:
(1)利用PIL库或其他库读入图像example.jgp,并转化为ndarray格式。
(2)利用前面编写的函数,对图像进行矩阵分解。
(3)取其TOP-K(K分别为10,50,100)个特征值/奇异值,和对应的特征向量/奇异向量,重建图像。
(4)把特征分解、奇异值分解两种方法得到的重建结果进行比较,画出图。
本实验旨在通过编写程序,实现矩阵分解和图像压缩应用
二.实验过程
1.求特征值和特征向量函数
eigens(A)
由于图像的矩阵是大型矩阵,使用 (入E -A)X=0 这种常规方法计算特征值和特征向量明显是不可靠的,这就要找一种能够快速计算大型矩阵的特征值和特征向量的方法,幂迭代和反迭代法是一个计算大矩阵的方法,该方法的基本思想是通过不断地将矩阵乘以一个向量,并对结果向量进行归一化,最终可以得到矩阵的最大特征值以及对应的特征向量。
eigens(A)函数通过调用power_iteration(A, num_iterations)函数进行幂迭代、通过调用inverse_iteration(A, mu, num_iterations)函数进行反迭代,完成矩阵A的特征值和特征向量的计算。
2.特征分解函数的实现:
myEIG(A)
对于一个 n*n 的实对称矩阵 A , 其特征分解就是将 A 分解为下列形式:
A=QΛQ-1 。其中,Q是由特征向量构成的正交矩阵,Q-1 是其逆矩阵,Λ是对角矩阵,其对角线上的元素是矩阵A的特征值。
利用eigens(A)函数计算出矩阵的特征值和特征向量后,将特征值从大到小排序,可得出Q矩阵、Λ矩阵, 然后返回特征值向量Λ和特征向量矩阵Q
3.奇异值分解函数的实现:
mySVD(A)
任意一个矩阵都能写成 SVD 分解的形式。其形式如下:A=UΣV
其中,U与V是正交矩阵,Σ是一个对角线上有奇异值的矩阵,它表示了矩阵 A“压缩”或“放大”的程度。
利用eigens()函数计算出A.T * A矩阵(左乘)的特征值和特征向量,则右奇异矩阵V为此矩阵的特征向量 ,奇异值为此矩阵的 特征值开根号
利用eigens()函数计算出A * A.T矩阵(右乘)的特征值和特征向量,则左奇异矩阵 U为此矩阵的特征向量。
推导过程如下:
通过前面函数的计算,我们先验证特征值分解和奇异值分解的计算结果对不对:
定义一个矩阵A为
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
调用自写的特征值分解函数,则输出以下结果
自写evd的分解结果为:
t: [ 1.61168440e+01 -1.11684397e+00 -2.27320163e-10]
vec: [[ 0.23197069 -0.02992263 -0.02992263]
[ 0.52532209 -0.45894233 -0.45894233]
[ 0.8186735 -0.88796203 -0.88796203]]
调用numpy库的特征值分解函数,则输出以下结果:
numpy库的evd分解结果为:
t: [ 1.61168440e+01 -1.11684397e+00 -4.22209278e-16]
vec: [[-0.23197069 -0.78583024 0.40824829]
[-0.52532209 -0.08675134 -0.81649658]
[-0.8186735 0.61232756 0.40824829]]
比较输出的结果后,可以发现得到的矩阵分解结果是基本相同,就是有些精度损失。
但是,当把图片的大矩阵代入计算,计算了很久,代码直接崩溃,
我一步步debug,查看每个变量的矩阵长什么样,查找原因,发现了有除0错误,不知怎么解决
计算的结果相差很多,我百思不得其解,说明上面那个函数的方法不适合计算大矩阵
同理,调用自写的奇异值分解函数,输出以下结果
自写svd的分解结果为:
u: [[ 0.21483724 -0.88723069 0.08677199]
[ 0.52058739 -0.24964395 0.73492663]
[ 0.82633754 0.38794278 0.67257228]]
sigma: [1.68481034e+01 1.06836951e+00 1.19249288e-07]
v: [[ 0.47967118 -0.77669099 0.62161018]
[ 0.57236779 -0.07568647 -0.24462121]
[ 0.66506441 0.62531805 0.74415136]]
调用numpy库的奇异值分解函数,输出以下结果
numpy库的svd分解结果为:
u: [[-0.21483724 0.88723069 0.40824829]
[-0.52058739 0.24964395 -0.81649658]
[-0.82633754 -0.38794278 0.40824829]]
sigma: [1.68481034e+01 1.06836951e+00 4.41842475e-16]
v: [[-0.47967118 -0.57236779 -0.66506441]
[-0.77669099 -0.07568647 0.62531805]
[-0.40824829 0.81649658 -0.40824829]]
比较输出的结果后,可以发现得到的矩阵分解结果是基本相同,就是有些精度损失。
但是,当把图片的大矩阵代入计算,计算了很久,都计算不出特征值,说明计算的方法有误或者代码有bug
计算的结果相差很多,我百思不得其解,说明上面那个函数的方法不适合计算大矩阵
4.evd分解重构函数的实现
eig_restore()
对于一个 n*n 的实对称矩阵 A , 其特征分解就是将 A 分解为下列形式:
A=QΛQ-1
通过前面的函数,我们已经算出矩阵Q和矩阵Λ,此时我们利用v = np.linalg.inv(vec) 这个函数算Q-1逆矩阵
根据k 的值,压缩对应的矩阵:
对于Q矩阵 ,取矩阵所有的行和前K列 Vex_k = vec[:, :K]
对于Q-1矩阵,取矩阵前K行和所有的列 V_k = v[:K, :]
对应Λ矩阵,取前k个值,并构造对角矩阵 t_k = np.diag(t[:K])
QΛQ-1这三个矩阵相乘,可得重构的结果 A_k
A_k = np.dot(Vex_k, np.dot(t_k, V_k))
A_k矩阵不能直接显示图片,需要转为uint8类型
img_k = A_k.astype('uint8')
5.svd分解重构函数的实现
svd_restore()
对于一个 m*n 的矩阵 A , 其奇异值分解就是将 A 分解为下列形式:
A=UΣV
通过前面的函数,我们已经算出矩阵U, 奇异值向量Σ和矩阵V
根据k 的值,压缩对应的矩阵:
对于U矩阵 ,取矩阵所有的行和前K列 u_k = u[:, :K]
对于V矩阵 , 取矩阵前K行和所有的列 V_k = v[:K, :]
对应Σ矩阵,取前k个值,并构造对角矩阵 s_k = np.diag(sigma[:K])
UΣV这三个矩阵相乘,可得重构的结果
A_k = np.dot(u_k, np.dot(s_k, V_k))
A_k矩阵不能直接显示图片,需要转为uint8类型
img_k = A_k.astype('uint8')
6.图像压缩函数的实现
main()
读取图像,并把它转换成矩阵类型
接下来,分别调用前面实现的特征分解和奇异值分解函数对图像进行矩阵分解
接着,我们通过取TOP-K(K=10、50、100)个奇异值/特征值和对应的奇异向量/特征向量, 对分解后的矩阵进行压缩
用plt.imshow(image) 显示图像
观察最终结果
三.对实验中参数和结果的分析
实验输入图片如下:
(由于自写计算特征值和特征向量的函数压缩不得最后的结果,此图片压缩为调用numpy库计算的结果)
实验的图片大小为 512 x 512 ,所以计算出的特征值或奇异值也有512个。
当K=10,(大约占50分之一),取最大的前10个特征值或奇异值,从压缩的图片可以看出,svd压缩和eig压缩的图片都模糊,svd压缩的图片比eig压缩的图片清晰一点
当K=50,(大约占10分之一),取最大的前50个特征值或奇异值,从压缩的图片可以看出,svd压缩的图片有一些噪点,eig压缩有许多噪点,svd分解的图片比eig分解的图片清晰一些。
在k=50以前,图片都存在着相当多的噪点,基本属于不可用状态
当K=100,(大约占5分之一),取最大的前100个特征值或奇异值,从压缩的图片可以看出,svd压缩的图片几乎没有噪点,eig压缩的图片有一点噪点,svd压缩的图片和eig压缩的图片比较清晰
在 K 值较小的情况下,特征分解的重构效果与奇异值分解相差不大。而随着 K 值的增大,奇异值分解的重构效果越来越接近原图,而特征分解的重构效果则稍有下降。我们可以这样理解:K 值越大,保留的信息越多,因此奇异值分解的重构效果更好。
经过比较,两种分解方法得到的结果明显不一样,svd算法只取k=50进行压缩,图片的质量就比较好了,而eid算法要取100才得,所以svd算法分解矩阵压缩图片的性能要比eig要好。
四.总结
本实验使我了解到了矩阵分解在机器学习中的重要性,并通过编写函数实现了特征分解和奇异值分解,掌握了矩阵分解的实际应用,同时也加深了对图像压缩的理解。
通过这两种分解算法对图片压缩进行分析比较,svd算法能在保持图像表现的情况下将我们原图片压缩至一个非常小的水准,比较适用于图像压缩。而我们的evd不太适合直接使用,容易产生较多噪点影响观感。通过实验我们发现,在保留图像信息的同时,通过削减图像信息可以显著减少图像占用的存储空间,对于一些需要大量存储图片的应用具有很好的优化效果。同时,我们也认识到,在不同的应用场景下,对于 K 值的选择需要谨慎,需要在压缩率和图像重构质量之间进行平衡。
我认为这次的实验时非常有意义的,在检验了我们这一学期掌握的知识以外,还留给了我们很多的思考和精进的空间,虽然结果不尽人意,但也学到了很多东西。
代码
import numpy as np
import matplotlib.pyplot as plt
# ------------------求解特征值和特征向量---------------------
def power_iteration(A, num_iterations):
n = A.shape[0]
# 生成一个随机的初始向量
x = np.random.rand(n)
# 进行 num_iterations 次迭代
for i in range(num_iterations):
x = A @ x
x = x / np.linalg.norm(x)
# 计算特征值
eigenvalue = x @ A @ x
# 返回特征向量和特征值
return x, eigenvalue
def inverse_iteration(A, mu, num_iterations):
n = A.shape[0]
# 生成一个随机的初始向量
x = np.random.rand(n)
# 进行平移变换
A = A - mu * np.eye(n)
# 进行 num_iterations 次迭代
for i in range(num_iterations):
x = np.linalg.solve(A, x)
x = x / np.linalg.norm(x)
# 计算特征值
eigenvalue = 1 / x @ A @ x + mu
# 返回特征向量和特征值
return x, eigenvalue
def eigens(A):
num_iterations = 200 # 迭代次数
eps = 1e-10 # 迭代精度
n = A.shape[0]
# 初始化特征值和特征向量
eigenvalues = np.zeros(n)
eigenvectors = np.zeros((n, n))
# 进行幂迭代
for i in range(n):
x, eigenvalue = power_iteration(A, num_iterations)
eigenvalues[i] = eigenvalue
eigenvectors[:, i] = x
# 对 A 进行 Rayleigh 商的计算,用于反迭代
mu = eigenvalue
for k in range(num_iterations):
x, eigenvalue = inverse_iteration(A, mu, num_iterations)
if np.abs(eigenvalue - mu) < eps:
break
mu = eigenvalue
# 将已经求解出来的特征值和特征向量从 A 中删除
A = A - eigenvalue * np.outer(x, x)
# 返回特征值和特征向量
return eigenvalues, eigenvectors
# --------------------evd分解----------------------------
def myEIG(A):
# 计算A的特征值和特征向量
# eig_vals, eig_vecs = np.linalg.eig(A) #调用numpy库的函数
eig_vals, eig_vecs = eigens(A)
"""
# 对特征值进行降序排序,并获取排序后的索引
idx = np.argsort(eig_vals)[::-1]
# 根据索引重新排列特征值和特征向量
eig_vals = eig_vals[idx]
eig_vecs = eig_vecs[:, idx]
"""
# 返回特征分解后的两个矩阵
return eig_vals, eig_vecs
# -------------svd分解------------------
def mySVD(A):
# 计算A的转置矩阵
A_T = A.T
# 计算A_T * A的特征值和特征向量
# eig_vals, eig_vecs = np.linalg.eig(A_T @ A) #调用numpy库的函数
eig_vals, eig_vecs = eigens(A_T @ A)
eig_vals = np.abs(eig_vals) # 取绝对值
# 对特征值进行降序排序,并获取排序后的索引
idx = np.argsort(eig_vals)[::-1]
# 根据索引重新排列特征值和特征向量
eig_vals = eig_vals[idx]
eig_vecs = eig_vecs[:, idx]
# 构造对角矩阵,存放奇异值(特征值的平方根)
sigma = np.sqrt(eig_vals)
# 计算右奇异向量矩阵V(就是特征向量矩阵)
V = eig_vecs
# t, U = np.linalg.eig(A @ A_T) #调用numpy库的函数
t, U = eigens(A @ A_T)
U = U[:, idx]
# 返回奇异值分解后的三个矩阵
return U, sigma, V
# -------------------evd重建图像矩阵--------------
def eig_restore(t, vec, K):
K = min(len(t) - 1, K) # 当K超过sigma的长度时会造成越界
v = np.linalg.inv(vec) # 求特征向量逆矩阵
Vex_k = vec[:, :K] # 取矩阵所有的行和前K列
V_k = v[:K, :] # 取矩阵前K行和所有的列
# 构造对角矩阵
t_k = np.diag(t[:K]) # 取前K个特征值
# 压缩后的图像矩阵
A_k = np.dot(Vex_k, np.dot(t_k, V_k)) # A=V*T* V^-1
img_k = A_k.astype('uint8')
return img_k
# ----------------------svd重建图像矩阵----------------
def svd_restore(sigma, u, v, K):
K = min(len(sigma) - 1, K) # 当K超过sigma的长度时会造成越界
u_k = u[:, :K] # 取矩阵所有的行和前K列
V_k = v[:K, :] # 取矩阵前K行和所有的列
# 构造对角矩阵
s_k = np.diag(sigma[:K]) # 取前K个特征值
# 压缩后的图像矩阵
A_k = np.dot(u_k, np.dot(s_k, V_k))
SigRecon = A_k.astype('uint8')
return SigRecon
# -----------------------比较自写的两种算法-----------------
def compare():
# 自定义一个矩阵,作为参数传递给前面编写的函数,测试
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# A = np.array([[1, 2, 3,4], [5, 6, 7,8], [9,10,11,12],[13,14,15,16]])
# ----------------evd-----------------
t, vec = myEIG(A)
print("自写evd的分解结果为:")
print('t:', t, '\n', 'vec:', vec)
T, Vec = np.linalg.eig(A)
print("numpy库的evd分解结果为:")
print('t:', T, '\n', 'vec:', Vec, '\n')
# -------------------svd--------------
u, s, v = mySVD(A)
print("自写svd的分解结果为:")
print('u:', u, '\n', 'sigma:', s, '\n', 'v:', v)
U, Sigma, V = np.linalg.svd(A)
print("numpy库的svd分解结果为:")
print('u:', U, '\n', 'sigma:', Sigma, '\n', 'v:', V, '\n')
def main():
# 导入灰度图片,单矩阵
img = plt.imread('./example.jpg')
print('图像矩阵大小为::', img.shape)
img = np.array(img)
# ------分解算法-----------
# evd
T, Vec = np.linalg.eig(img)
# T, Vec = myEIG(img)
# svd
u, sigma, v = np.linalg.svd(img)
# u, sigma, v = mySVD(img)
i = 0
for k in [10, 50, 100]:
image = eig_restore(T, Vec, k)
i += 1
plt.subplot(3, 3, i) # 表示第i张图片
plt.imshow(image)
plt.xlabel(f'eig decomposition-{k}')
plt.xticks([])
plt.yticks([])
i = 3
for k in [10, 50, 100]:
image = svd_restore(sigma, u, v, k)
i += 1
plt.subplot(3, 3, i)
plt.imshow(image)
plt.xlabel(f'svd decomposition-{k}')
plt.xticks([]) # 两行是消除每张图片自己单独的横纵坐标,不然每张图片会有单独的横纵坐标,影响美观
plt.yticks([])
plt.show()
if __name__ == '__main__':
compare() # 比较两种算法
main() # 压缩图片
参考资料:微信公众平台 (qq.com)