使用核心 API 进行矩阵近似

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 下载笔记本

简介

此笔记本使用 TensorFlow 核心低级 API 来展示 TensorFlow 作为高性能科学计算平台的功能。访问 核心 API 概述 了解有关 TensorFlow 核心及其预期用例的更多信息。

本教程探讨了 奇异值分解 (SVD) 技术及其在低秩近似问题中的应用。SVD 用于分解实数或复数矩阵,在数据科学中具有多种用例,例如图像压缩。本教程的图像来自 Google Brain 的 Imagen 项目。

svd_intro

设置

import matplotlib
from matplotlib.image import imread
from matplotlib import pyplot as plt
import requests
# Preset Matplotlib figure sizes.
matplotlib.rcParams['figure.figsize'] = [16, 9]
import tensorflow as tf
print(tf.__version__)

SVD 基础

矩阵 \({\mathrm{A} }\) 的奇异值分解由以下分解确定

\[{\mathrm{A} } = {\mathrm{U} } \Sigma {\mathrm{V} }^T\]

其中

  • \(\underset{m \times n}{\mathrm{A} }\):输入矩阵,其中 \(m \geq n\)
  • \(\underset{m \times n}{\mathrm{U} }\):正交矩阵,\({\mathrm{U} }^T{\mathrm{U} } = {\mathrm{I} }\),其中每列 \(u_i\) 表示 \({\mathrm{A} }\) 的左奇异向量
  • \(\underset{n \times n}{\Sigma}\):对角矩阵,其中每个对角线元素 \(\sigma_i\) 表示 \({\mathrm{A} }\) 的奇异值
  • \(\underset{n \times n}{ {\mathrm{V} }^T}\):正交矩阵,\({\mathrm{V} }^T{\mathrm{V} } = {\mathrm{I} }\),其中每行 \(v_i\) 表示 \({\mathrm{A} }\) 的右奇异向量

当 \(m < n\) 时,\({\mathrm{U} }\) 和 \(\Sigma\) 的维度均为 \((m \times m)\),而 \({\mathrm{V} }^T\) 的维度为 \((m \times n)\)。

svd_full

TensorFlow 的线性代数包有一个函数,tf.linalg.svd,它可以用来计算一个或多个矩阵的奇异值分解。首先定义一个简单的矩阵并计算它的 SVD 分解。

A = tf.random.uniform(shape=[40,30])
# Compute the SVD factorization
s, U, V = tf.linalg.svd(A)
# Define Sigma and V Transpose
S = tf.linalg.diag(s)
V_T = tf.transpose(V)
# Reconstruct the original matrix
A_svd = U@S@V_T
# Visualize 
plt.bar(range(len(s)), s);
plt.xlabel("Singular value rank")
plt.ylabel("Singular value")
plt.title("Bar graph of singular values");

tf.einsum 函数可以用来直接从 tf.linalg.svd 的输出中计算矩阵重建。

A_svd = tf.einsum('s,us,vs -> uv',s,U,V)
print('\nReconstructed Matrix, A_svd', A_svd)

使用 SVD 进行低秩近似

矩阵 \({\mathrm{A} }\) 的秩由其列向量所跨越的向量空间的维数决定。SVD 可以用来用更低的秩来近似一个矩阵,这最终会降低存储矩阵所表示的信息所需数据的维数。

根据 SVD,\({\mathrm{A} }\) 的秩为 r 的近似值由以下公式定义:

\[{\mathrm{A_r} } = {\mathrm{U_r} } \Sigma_r {\mathrm{V_r} }^T\]

其中

  • \(\underset{m \times r}{\mathrm{U_r} }\):由 \({\mathrm{U} }\) 的前 \(r\) 列组成的矩阵
  • \(\underset{r \times r}{\Sigma_r}\):由 \(\Sigma\) 中的前 \(r\) 个奇异值组成的对角矩阵
  • \(\underset{r \times n}{\mathrm{V_r} }^T\):由 \({\mathrm{V} }^T\) 的前 \(r\) 行组成的矩阵

svd_approx

首先编写一个函数来计算给定矩阵的秩为 r 的近似值。这种低秩近似过程用于图像压缩;因此,计算每个近似的物理数据大小也很有帮助。为简单起见,假设秩为 r 的近似矩阵的数据大小等于计算近似值所需的元素总数。接下来,编写一个函数来可视化原始矩阵 \(\mathrm{A}\),其秩为 r 的近似值 \(\mathrm{A}_r\) 和误差矩阵 \(|\mathrm{A} - \mathrm{A}_r|\)。

def rank_r_approx(s, U, V, r, verbose=False):
  # Compute the matrices necessary for a rank-r approximation
  s_r, U_r, V_r = s[..., :r], U[..., :, :r], V[..., :, :r] # ... implies any number of extra batch axes
  # Compute the low-rank approximation and its size
  A_r = tf.einsum('...s,...us,...vs->...uv',s_r,U_r,V_r)
  A_r_size = tf.size(U_r) + tf.size(s_r) + tf.size(V_r)
  if verbose:
    print(f"Approximation Size: {A_r_size}")
  return A_r, A_r_size

def viz_approx(A, A_r):
  # Plot A, A_r, and A - A_r
  vmin, vmax = 0, tf.reduce_max(A)
  fig, ax = plt.subplots(1,3)
  mats = [A, A_r, abs(A - A_r)]
  titles = ['Original A', 'Approximated A_r', 'Error |A - A_r|']
  for i, (mat, title) in enumerate(zip(mats, titles)):
    ax[i].pcolormesh(mat, vmin=vmin, vmax=vmax)
    ax[i].set_title(title)
    ax[i].axis('off')
print(f"Original Size of A: {tf.size(A)}")
s, U, V = tf.linalg.svd(A)
# Rank-15 approximation
A_15, A_15_size = rank_r_approx(s, U, V, 15, verbose = True)
viz_approx(A, A_15)
# Rank-3 approximation
A_3, A_3_size = rank_r_approx(s, U, V, 3, verbose = True)
viz_approx(A, A_3)

正如预期的那样,使用更低的秩会导致精度较低的近似值。然而,在现实世界中,这些低秩近似的质量通常足够好。还要注意,使用 SVD 进行低秩近似的主要目标是降低数据的维数,而不是降低数据本身的磁盘空间。但是,随着输入矩阵变得更高维,许多低秩近似最终也会从降低的数据大小中受益。这种减少的好处是为什么该过程适用于图像压缩问题。

图像加载

以下图像可在 Imagen 主页上找到。Imagen 是由 Google Research 的 Brain 团队开发的一种文本到图像扩散模型。一个 AI 根据提示创建了这张图像:“一张柯基犬骑着自行车在时代广场的照片。它戴着太阳镜和沙滩帽。” 太酷了!您也可以将下面的 URL 更改为任何 .jpg 链接,以加载您选择的自定义图像。

首先读取并可视化图像。在读取 JPEG 文件后,Matplotlib 输出一个形状为 \((m \times n \times 3)\) 的矩阵 \({\mathrm{I} }\),它表示一个二维图像,具有 3 个颜色通道,分别代表红色、绿色和蓝色。

img_link = "https://imagen.research.google/main_gallery_images/a-photo-of-a-corgi-dog-riding-a-bike-in-times-square.jpg"
img_path = requests.get(img_link, stream=True).raw
I = imread(img_path, 0)
print("Input Image Shape:", I.shape)
def show_img(I):
  # Display the image in matplotlib
  img = plt.imshow(I)
  plt.axis('off')
  return
show_img(I)

图像压缩算法

现在,使用 SVD 计算示例图像的低秩近似值。回想一下,图像的形状为 \((1024 \times 1024 \times 3)\),并且 SVD 理论仅适用于二维矩阵。这意味着示例图像必须被批处理成 3 个大小相等的矩阵,分别对应于 3 个颜色通道中的每一个。这可以通过将矩阵转置为形状为 \((3 \times 1024 \times 1024)\) 来实现。为了清楚地可视化近似误差,将图像的 RGB 值从 \([0,255]\) 重新缩放到 \([0,1]\)。请记住,在可视化之前将近似值剪裁到此区间内。 tf.clip_by_value 函数对此很有用。

def compress_image(I, r, verbose=False):
  # Compress an image with the SVD given a rank 
  I_size = tf.size(I)
  print(f"Original size of image: {I_size}")
  # Compute SVD of image
  I = tf.convert_to_tensor(I)/255
  I_batched = tf.transpose(I, [2, 0, 1]) # einops.rearrange(I, 'h w c -> c h w')
  s, U, V = tf.linalg.svd(I_batched)
  # Compute low-rank approximation of image across each RGB channel
  I_r, I_r_size = rank_r_approx(s, U, V, r)
  I_r = tf.transpose(I_r, [1, 2, 0]) # einops.rearrange(I_r, 'c h w -> h w c')
  I_r_prop = (I_r_size / I_size)
  if verbose:
    # Display compressed image and attributes
    print(f"Number of singular values used in compression: {r}")
    print(f"Compressed image size: {I_r_size}")
    print(f"Proportion of original size: {I_r_prop:.3f}")
    ax_1 = plt.subplot(1,2,1)
    show_img(tf.clip_by_value(I_r,0.,1.))
    ax_1.set_title("Approximated image")
    ax_2 = plt.subplot(1,2,2)
    show_img(tf.clip_by_value(0.5+abs(I-I_r),0.,1.))
    ax_2.set_title("Error")
  return I_r, I_r_prop

现在,计算以下秩的秩为 r 的近似值:100、50、10

I_100, I_100_prop = compress_image(I, 100, verbose=True)
I_50, I_50_prop = compress_image(I, 50, verbose=True)
I_10, I_10_prop = compress_image(I, 10, verbose=True)

评估近似值

有许多有趣的方法可以衡量矩阵近似的有效性并更好地控制矩阵近似。

压缩因子与秩

对于上述每个近似值,观察数据大小如何随秩变化。

plt.figure(figsize=(11,6))
plt.plot([100, 50, 10], [I_100_prop, I_50_prop, I_10_prop])
plt.xlabel("Rank")
plt.ylabel("Proportion of original image size")
plt.title("Compression factor vs rank");

根据此图,近似图像的压缩因子与其秩之间存在线性关系。为了进一步探索这一点,回想一下,近似矩阵 \({\mathrm{A} }_r\) 的数据大小定义为计算它所需的元素总数。以下等式可用于找到压缩因子和秩之间的关系

\[x = (m \times r) + r + (r \times n) = r \times (m + n + 1)\]

\[c = \large \frac{x}{y} = \frac{r \times (m + n + 1)}{m \times n}\]

其中

  • \(x\):\({\mathrm{A_r} }\) 的大小
  • \(y\):\({\mathrm{A} }\) 的大小
  • \(c = \frac{x}{y}\):压缩因子
  • \(r\):近似的秩
  • \(m\) 和 \(n\):\({\mathrm{A} }\) 的行和列维数

为了找到将图像压缩到所需因子 \(c\) 所需的秩 \(r\),可以重新排列上述等式以求解 \(r\)

\[r = ⌊{\large\frac{c \times m \times n}{m + n + 1} }⌋\]

请注意,此公式独立于颜色通道维数,因为每个 RGB 近似值都不会相互影响。现在,编写一个函数,在给定所需压缩因子时压缩输入图像。

def compress_image_with_factor(I, compression_factor, verbose=False):
  # Returns a compressed image based on a desired compression factor
  m,n,o = I.shape
  r = int((compression_factor * m * n)/(m + n + 1))
  I_r, I_r_prop = compress_image(I, r, verbose=verbose)
  return I_r

将图像压缩到其原始大小的 15%。

compression_factor = 0.15
I_r_img = compress_image_with_factor(I, compression_factor, verbose=True)

奇异值的累积和

奇异值的累积和可以作为秩为 r 的近似值捕获的能量量的有用指标。可视化示例图像中 RGB 平均的奇异值累积比例。 tf.cumsum 函数对此很有用。

def viz_energy(I):
  # Visualize the energy captured based on rank
  # Computing SVD
  I = tf.convert_to_tensor(I)/255
  I_batched = tf.transpose(I, [2, 0, 1]) 
  s, U, V = tf.linalg.svd(I_batched)
  # Plotting average proportion across RGB channels 
  props_rgb = tf.map_fn(lambda x: tf.cumsum(x)/tf.reduce_sum(x), s)
  props_rgb_mean = tf.reduce_mean(props_rgb, axis=0)
  plt.figure(figsize=(11,6))
  plt.plot(range(len(I)), props_rgb_mean, color='k')
  plt.xlabel("Rank / singular value number")
  plt.ylabel("Cumulative proportion of singular values")
  plt.title("RGB-averaged proportion of energy captured by the first 'r' singular values")
viz_energy(I)

看起来此图像中超过 90% 的能量被前 100 个奇异值捕获。现在,编写一个函数,在给定所需能量保留因子时压缩输入图像。

def compress_image_with_energy(I, energy_factor, verbose=False):
  # Returns a compressed image based on a desired energy factor
  # Computing SVD
  I_rescaled = tf.convert_to_tensor(I)/255
  I_batched = tf.transpose(I_rescaled, [2, 0, 1]) 
  s, U, V = tf.linalg.svd(I_batched)
  # Extracting singular values
  props_rgb = tf.map_fn(lambda x: tf.cumsum(x)/tf.reduce_sum(x), s)
  props_rgb_mean = tf.reduce_mean(props_rgb, axis=0)
  # Find closest r that corresponds to the energy factor
  r = tf.argmin(tf.abs(props_rgb_mean - energy_factor)) + 1
  actual_ef = props_rgb_mean[r]
  I_r, I_r_prop = compress_image(I, r, verbose=verbose)
  print(f"Proportion of energy captured by the first {r} singular values: {actual_ef:.3f}")
  return I_r

压缩图像以保留其 75% 的能量。

energy_factor = 0.75
I_r_img = compress_image_with_energy(I, energy_factor, verbose=True)

误差和奇异值

近似误差和奇异值之间也存在有趣的关系。事实证明,近似的弗罗贝尼乌斯范数的平方等于被遗漏的奇异值的平方和

\[{||A - A_r||}^2 = \sum_{i=r+1}^{R}σ_i^2\]

使用本教程开头示例矩阵的秩为 10 的近似值来测试这种关系。

s, U, V = tf.linalg.svd(A)
A_10, A_10_size = rank_r_approx(s, U, V, 10)
squared_norm = tf.norm(A - A_10)**2
s_squared_sum = tf.reduce_sum(s[10:]**2)
print(f"Squared Frobenius norm: {squared_norm:.3f}")
print(f"Sum of squared singular values left out: {s_squared_sum:.3f}")

结论

本笔记本介绍了使用 TensorFlow 实现奇异值分解的过程,并将其应用于编写图像压缩算法。以下是一些可能有所帮助的额外提示

有关使用 TensorFlow Core API 的更多示例,请查看 指南。如果您想了解有关加载和准备数据的更多信息,请查看有关 图像数据加载CSV 数据加载 的教程。