【Python · PyTorch】时域卷积网络 TCN
1. 概念
1.1 定义
TCN 是时域卷积网络(Temporal Convolutional Network)的简称。TCN是于2018年 Shaojie Bai 等人提出的一个处理时序数据的卷积模型。
TCN结合了CNN卷积并行性计算和RNN长期依赖的优势,CNN可在多个通道同时处理卷积核运算,而RNN则因随时间反向传播需要进行等待,这使得如果能利用CNN处理时序数据将节省计算时间。RNN长期依赖则可以使神经网络具有一定“记忆性”,而CNN本身则不具备这点优势,但可以利用 因果+膨胀卷积 让卷积核提取更早的信息,使得CNN间接实现长期依赖的功能,基于这种思想 TCN得以诞生。
1.2 设计原则
-
输入输出形状相同原则
- TCN采用1D全卷积网络( FCN )架构,其中每个隐藏层与输入层的长度相同,并添加长度为(内核大小- 1)的补零操作,以保持后续层与前一层的长度相同。
- 在卷积操作后进行适当的填充(Padding)和裁剪(Cropping)
-
仅考虑历史信息
- TCN使用因果卷积,即t时刻的输出只与前一层中来自t时刻和更早的元素卷积。
简单地说:TCN = 1D FCN +因果卷积。
2. 模型分析
import torch
import torch.nn as nn
import numpy as np
import pandas as pd
2.1 输入形状
TCN网络的输入是一个三维Tensor张量,其形状可以表示为(N, C, L),其中:
- N - 批次大小 (Batch Size):一次处理多少个时间序列样本。
- C - 通道数 (Channels)【类比:灰度图像单通道 / RGB图像三通道 / RGBA图像四通道】
- 对于单变量时间序列,通道数 = 变量个数 = 1
- 对于多变量时间序列,通道数 = 变量个数
- L - 时序长度: (Length):时间序列长度
2.1 查看网络结构
2.1.0 直接输出
我们先以普通ANN为例,定义简单ANN网络。
class ANN(nn.Module):
def __init__(self, intput_size, hidden_size, output_size):
super(ANN, self).__init__()
self.linear1 = nn.Linear(intput_size, hidden_size)
self.relu1 = nn.ReLU()
self.linear2 = nn.Linear(hidden_size, output_size)
self.relu2 = nn.ReLU()
self.layer = nn.Sequential(self.linear1, self.relu1, self.linear2, self.relu2)
def forward(self, x):
output = self.layer(x)
return output
创建ANN实例,并尝试输出其结构。
ann = ANN(12 ,8, 4)
print(ann)
运行代码,我们得到如下结果。虽然似乎可以对应起来,但是其并不直接表示神经网络的结构。
所以,我们利用下面两种方式查看神经网络结构。
2.1.1 利用torchsummary库
优点:
- 无需考虑batch_size 默认 -1
- 展开所有 Sequential 更加详细
构建输入数据:
- 批量数:batch_size=8(类比DataLoader依据批量数形成数据)
- 通道数:input_channel=1(单变量数据 → 单通道输入)
- 时序长度:sequence_length=6(与RNN类似 形为1×6数据)
from torchsummary import summary
net = TemporalConvNet(1, [12, 12, 12, 12], kernel_size=2).to("cuda")
print(summary(net, input_size=(1, 6)))
2.1.2 利用torchinfo库
优点:
- 输出类似表格 更为直观
- 不展开 Sequential 更加简约
构建输入数据:
- 批量数:batch_size=8(类比DataLoader依据批量数形成数据)
- 通道数:input_channel=1(单变量数据 → 单通道输入)
- 时序长度:sequence_length=6(与RNN类似 形为1×6数据)
import torchinfo
from torchinfo import summary
net = TemporalConvNet(1, [12, 12, 12, 12], kernel_size=2).to("cuda")
print(summary(net, input_size=(8, 1, 6)))
2.2 模型参数规范化
2.2.1 权重分解规范化 - weight_norm
weight_norm 将权重分解为 方向(v) 和 大小(g)。
w
=
g
⋅
v
∣
∣
v
∣
∣
w=g \cdot \frac{v}{||v||}
w=g⋅∣∣v∣∣v
其中,g是标量,表示权重大小,v是向量,表示权重方向。
这种方法通过分离权重的大小和方向,使得优化过程更加稳定。
归一化后的权重向量实际上是v的归一化形式,即 v n o r a m l i z e d = v ∣ ∣ v ∣ ∣ v_{noramlized=}\frac{v}{||v||} vnoramlized=∣∣v∣∣v,而weight的值为 g ⋅ v n o r m a l i z e d g \cdot v_{normalized} g⋅vnormalized。
主要用于规范化权重,特别适用于需要控制权重大小的场景。
例如,在某些生成模型或自注意力机制中,权重的大小对模型的稳定性和性能有重要影响。
weight_norm 提供了一种简单且直接的方式来实现权重的规范化。
2.2.2 谱范数规范化 - spectral_norm
谱范数(Spectral Norm):一种用于衡量矩阵大小的标准方法。它特别关注矩阵在作用于向量时的最大放大/拉伸比例。在实际应用中,谱范数常用于控制和优化理论、数值分析等领域。
谱范数 ∣ ∣ m ∣ ∣ 2 ||m||_2 ∣∣m∣∣2定义为矩阵M作用在单位向量上时的最大放大/拉伸因子,即矩阵 M M M的最大奇异值 ∣ ∣ m ∣ ∣ 2 = σ m a x ( M ) ||m||_2=\sigma_{max}(M) ∣∣m∣∣2=σmax(M)。
谱范数性质:
- 非负性: ∣ ∣ M ∣ ∣ 2 ≥ 0 ||M||_2 \ge 0 ∣∣M∣∣2≥0,并且 当且仅当 M M M是零矩阵时 ∣ ∣ M ∣ ∣ 2 = 0 ||M||_2 = 0 ∣∣M∣∣2=0
- 一致性:矩阵转置前后谱范数不变,即 ∣ ∣ M ∣ ∣ 2 = ∣ ∣ M ⊤ ∣ ∣ 2 ||M||_2=||M^\top||_2 ∣∣M∣∣2=∣∣M⊤∣∣2
- 次可加性:对于任意两个矩阵 A A A和 B B B,有 ∣ ∣ A + B ∣ ∣ 2 ≤ ∣ ∣ A ∣ ∣ 2 + ∣ ∣ B ∣ ∣ 2 ||A+B||_2\le||A||_2+||B||_2 ∣∣A+B∣∣2≤∣∣A∣∣2+∣∣B∣∣2
- 乘法不等式:对于任意两个矩阵 A A A和 B B B,有 ∣ ∣ A B ∣ ∣ 2 ≤ ∣ ∣ A ∣ ∣ 2 ∣ ∣ B ∣ ∣ 2 ||AB||_2\le||A||_2 ||B||_2 ∣∣AB∣∣2≤∣∣A∣∣2∣∣B∣∣2
- 欧几里得范数 (l2范数):对于任意向量 x ⃗ \vec{x} x,有 ∣ ∣ M x ⃗ ∣ ∣ 2 ≤ ∣ ∣ M ∣ ∣ 2 ∣ ∣ x ⃗ ∣ ∣ 2 ||M\vec{x}||_2\le||M||_2 ||\vec{x}||_2 ∣∣Mx∣∣2≤∣∣M∣∣2∣∣x∣∣2,其中 ∣ ∣ x ⃗ ∣ ∣ ||\vec{x}|| ∣∣x∣∣是向量 x ⃗ \vec{x} x的欧几里得范数。
奇异值
奇异值是矩阵里的概念,一般通过奇异值分解定理求得。
设 A A A为 m ∗ n m*n m∗n阶实矩阵, q = m i n ( m , n ) q=min(m,n) q=min(m,n), A A T AA^{T} AAT的 q q q个非负特征值的算术平方根叫作 A A A的奇异值。
奇异值分解
存在一个分解 使得 A = U Σ V ⊤ A=U\Sigma V^\top A=UΣV⊤
其中, U U U是m×m阶正交矩阵, Σ \Sigma Σ是m×m阶非负实数对角矩阵, V V V是n×n阶正交矩阵。
奇异值分解使得 非方阵 也能进行分解,由于 A A T AA^{T} AAT 必定为实对称矩阵,对它进行特征值分解,被称为对A进行奇异值分解。
其中,U和V均为正交矩阵,Sigma是对角矩阵,根据特征值分解可得:
- U被称为A的左奇异矩阵,其列组成的向量是方阵 A A ⊤ AA^\top AA⊤的特征向量,亦称A的左奇异向量
- V被称为A的右奇异矩阵,其列组成的向量是方阵 A A ⊤ AA^\top AA⊤的特征向量,亦称A的右奇异向量
- Σ \Sigma Σ对角线上的元素 σ i i \sigma_{ii} σii即为A的奇异值,它们等于 A A ⊤ AA^\top AA⊤及 A ⊤ A A^\top A A⊤A特征值的平方根( σ = λ \sigma=\sqrt{\lambda} σ=λ),行对应 U U U的列向量,列对应 V V V的列向量。
∣ ∣ M ∣ ∣ 2 = σ m a x ( M ) ||M||_2=\sigma_{max}(M) ∣∣M∣∣2=σmax(M)
spectral_norm 一种基于谱范数的规范化方法。
作用:通过限制矩阵的最大奇异值,控制权重矩阵的放大/拉伸能力,从而提高模型的稳定性和泛化能力。
计算机通过 幂迭代法(Power Iteration) 近似计算矩阵的最大奇异值。
迭代计算:
v
=
W
⊤
u
∣
∣
W
⊤
u
∣
∣
2
v=\frac{W^\top u}{||W^\top u||_2}
v=∣∣W⊤u∣∣2W⊤u
u = W v ∣ ∣ W v ∣ ∣ 2 u=\frac{Wv}{||Wv||_2} u=∣∣Wv∣∣2Wv
幂迭代法 - 举例说明(特征值)
而奇异值与
最大奇异值 σ m a x ( W ) = u ⊤ W v \sigma_{max}(W)=u^\top Wv σmax(W)=u⊤Wv
将权重规范化为:
W
n
e
w
=
W
o
l
d
σ
m
a
x
(
W
o
l
d
)
W_{new}=\frac{W_{old}}{\sigma_{max}(W_{old})}
Wnew=σmax(Wold)Wold
常用于生成对抗网络(GAN)中,通过限制生成器和判别器的最大奇异值,提高模型的稳定性和泛化能力。
能够有效控制矩阵的放大能力,适用于需要限制模型输出范围的场景
(本小节后续内容为扩展内容 可跳过)
① 特征值分解
特征值分解:亦称谱分解(Spectral Decomposition),将矩阵分解成特征值和特征向量表示的矩阵乘法的形式。
定义:
A
A
A是一个n×n方阵,且有n个线性无关的特征向量
q
i
(
i
=
1
,
.
.
.
,
n
)
q_{i}(i=1,...,n)
qi(i=1,...,n),可将
A
A
A分解为:
A
=
Q
Λ
Q
−
1
A=Q \Lambda Q^{-1}
A=QΛQ−1
其中,
Q
Q
Q是n×n方阵,且第i列为A的特征向量
q
i
q_{i}
qi。
Λ
\Lambda
Λ是对角矩阵,其对角线上的元素对应特征值,即
Λ
i
i
=
λ
i
\Lambda_{ii}=\lambda_{i}
Λii=λi。
只有可对角化矩阵(满秩)才能作特征分解,方阵其秩为λ≥0的个数。
若A并非满秩矩阵,即对角阵对角线上存在0,则会导致维度退化,这样就会使经该矩阵映射后的向量落入n维空间的子空间中。
(矩阵可视作空间变换,特征向量在变换中保持方向不变,特征值表示特征向量的伸缩幅度)
② 奇异值分解
奇异值是矩阵里的概念,一般通过 奇异值分解定理 求得。非方阵其秩为σ≥0的个数。
设 A A A为 m ∗ n m*n m∗n阶实矩阵, q = m i n ( m , n ) q=min(m,n) q=min(m,n), A ⊤ A A^\top A A⊤A的 q q q个非负特征值的算术平方根叫作 A A A的奇异值。
存在一个分解 使得 A = U Σ V ⊤ A=U\Sigma V^\top A=UΣV⊤。其中, U U U是m×m阶正交矩阵, Σ \Sigma Σ是m×n阶非负实数对角矩阵, V V V是n×n阶正交矩阵。
奇异值分解使得 非方阵 也能进行分解,由于 A A T AA^{T} AAT 必定为实对称矩阵,对它进行特征值分解,被称为对A进行奇异值分解。
其中,U和V均为正交矩阵,Sigma是对角矩阵,根据特征值分解可得:
- U被称为A的左奇异矩阵,其列组成的向量是方阵 A A ⊤ AA^\top AA⊤的特征向量,亦称A的左奇异向量
- V被称为A的右奇异矩阵,其列组成的向量是方阵 A v A AvA AvA的特征向量,亦称A的右奇异向量
- Σ \Sigma Σ对角线上的元素 σ i i \sigma_{ii} σii即为A的奇异值,它们等于 A A ⊤ AA^\top AA⊤及 A ⊤ A A^\top A A⊤A特征值的平方根( σ = λ \sigma=\sqrt{\lambda} σ=λ),行对应 U U U的列向量,列对应 V V V的列向量。
特征值&奇异值 对矩阵的影响:
- 特征值只能作用在一个n×n的方阵上,奇异值分解则可以作用在一个m×n的长方矩阵上。
- 奇异值分解同时包含了旋转、缩放和投影三种作用,式中,U和V负责旋转,Σ负责缩放。特征值分解只有缩放的效果。
奇异值分解步骤A
-
计算 A ⊤ A A^\top A A⊤A n个特征值 λ 1 , λ 2 , ⋯ , λ r , λ r + 1 = 0 , ⋯ λ n = 0 \lambda_1,\lambda_2,\cdots,\lambda_r, \lambda_{r+1}=0, \cdots\lambda_n=0 λ1,λ2,⋯,λr,λr+1=0,⋯λn=0(降序) 和对应的 标准正交 特征向量(正交化) v 1 ⃗ , v 2 ⃗ , ⋯ , v r ⃗ , v r + 1 ⃗ , ⋯ , v n ⃗ \vec{v_1}, \vec{v_2}, \cdots,\vec{v_r}, \vec{v_{r+1}},\cdots,\vec{v_n} v1,v2,⋯,vr,vr+1,⋯,vn。
- A ⊤ A = [ a 11 a 12 ⋯ a 1 n a 21 a 22 ⋯ a 2 n ⋮ ⋮ ⋱ ⋮ a n 1 a n 2 ⋯ a n n ] A^\top 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} A⊤A= a11a21⋮an1a12a22⋮an2⋯⋯⋱⋯a1na2n⋮ann
-
取标准正交的特征向量构成的 正交矩阵
V n × n = ( v 1 ⃗ , v 2 ⃗ , ⋯ , v r ⃗ , v r + 1 ⃗ , ⋯ , v n ⃗ ) n × n V_{n\times n} =(\vec{v_1}, \vec{v_2}, \cdots,\vec{v_r}, \vec{v_{r+1}},\cdots,\vec{v_n})_{n\times n} Vn×n=(v1,v2,⋯,vr,vr+1,⋯,vn)n×n
取正奇异值(即前r个奇异值),即对非零特征值开根号 λ 1 , λ 2 , ⋯ , λ r \sqrt{\lambda_1},\sqrt{\lambda_2},\cdots,\sqrt{\lambda_r} λ1,λ2,⋯,λr,构成 对角矩阵。
D r × r = [ λ 1 λ 2 ⋱ λ r ] D_{r\times r} =\begin{bmatrix} \sqrt{\lambda_1} & & & \\ & \sqrt{\lambda_2} & & \\ & & \ddots & \\ & & & \sqrt{\lambda_r} \end{bmatrix} Dr×r= λ1λ2⋱λr
添加额外的0组成的m×n的矩阵
Σ m × n = [ D r × r O O O ] m × n = [ λ 1 λ 2 O ⋱ λ r O O ] m × n \Sigma_{m\times n}=\begin{bmatrix} D_{r\times r} & O \\ O & O \\ \end{bmatrix}_{m\times n} =\begin{bmatrix} \sqrt{\lambda_1} & & & & \\ & \sqrt{\lambda_2} & & & O \\ & & \ddots & & \\ & & & \sqrt{\lambda_r} & \\ & O & & & O \end{bmatrix}_{m\times n} Σm×n=[Dr×rOOO]m×n= λ1λ2O⋱λrOO m×n -
计算前r个标准正交向量 u 1 ⃗ , u 2 ⃗ , ⋯ , u r ⃗ , u r + 1 ⃗ , ⋯ , u n ⃗ \vec{u_1}, \vec{u_2}, \cdots,\vec{u_r}, \vec{u_{r+1}},\cdots,\vec{u_n} u1,u2,⋯,ur,ur+1,⋯,un,其中 u i ⃗ = 1 λ i A v i ⃗ , i = 1 , 2 , ⋯ , r \vec{u_i}=\frac{1}{\sqrt{\lambda_i}}A\vec{v_i}, i=1,2,\cdots,r ui=λi1Avi,i=1,2,⋯,r。所得 u i ∈ R m u_i \in \mathbb{R}^m ui∈Rm,但由于 m ≤ n m\leq n m≤n数量不足,故需要进行基扩充。
-
利用 标准正交基扩充 方法,将 u 1 ⃗ , u 2 ⃗ , ⋯ , u r ⃗ , u r + 1 ⃗ , ⋯ , u n ⃗ \vec{u_1}, \vec{u_2}, \cdots,\vec{u_r}, \vec{u_{r+1}},\cdots,\vec{u_n} u1,u2,⋯,ur,ur+1,⋯,un扩充为m维向量空间 R m \mathbb{R}^m Rm的标准正交基 u 1 ⃗ , u 2 ⃗ , ⋯ , u r ⃗ , u r + 1 ⃗ , ⋯ , u n ⃗ , b 1 ⃗ , ⋯ , b m − r ⃗ \vec{u_1}, \vec{u_2}, \cdots,\vec{u_r}, \vec{u_{r+1}},\cdots,\vec{u_n},\vec{b_1},\cdots,\vec{b_{m-r}} u1,u2,⋯,ur,ur+1,⋯,un,b1,⋯,bm−r,组成 正交矩阵
基扩充定理:
设 V r V_r Vr是 n n n维线性空间 V n V_n Vn的一个子空间, α 1 ⃗ , α 2 , ⃗ ⋯ , α r ⃗ \vec{\alpha_1},\vec{\alpha_2,}\cdots,\vec{\alpha_r} α1,α2,⋯,αr是 V r V_r Vr的一个基,试证: V n V_n Vn中存在元素 α r + 1 ⃗ , ⋯ , α n ⃗ \vec{\alpha_{r+1}},\cdots,\vec{\alpha_n} αr+1,⋯,αn使得 α 1 ⃗ , α 2 , ⃗ ⋯ , α r ⃗ , α r + 1 ⃗ , ⋯ , α n ⃗ \vec{\alpha_1},\vec{\alpha_2,}\cdots,\vec{\alpha_r},\vec{\alpha_{r+1}},\cdots,\vec{\alpha_n} α1,α2,⋯,αr,αr+1,⋯,αn成为 V n V_n Vn的一个基。
证明:
若 r = n r=n r=n,结论成立。
若 r < n r<n r<n,因为 α 1 ⃗ , α 2 , ⃗ ⋯ , α r ⃗ \vec{\alpha_1},\vec{\alpha_2,}\cdots,\vec{\alpha_r} α1,α2,⋯,αr不是 V n V_n Vn的基(r<n 数量不足),所以存在KaTeX parse error: Undefined control sequence: \textbackslash at position 27: …_{r+1}}\in V_n \̲t̲e̲x̲t̲b̲a̲c̲k̲s̲l̲a̲s̲h̲ ̲V_r不可由 α 1 ⃗ , α 2 , ⃗ ⋯ , α r ⃗ \vec{\alpha_1},\vec{\alpha_2,}\cdots,\vec{\alpha_r} α1,α2,⋯,αr线性表出,进而 α 1 ⃗ , α 2 ⃗ , ⋯ , α r ⃗ , α r + 1 ⃗ \vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_r},\vec{\alpha_{r+1}} α1,α2,⋯,αr,αr+1线性无关。如果 r = n − 1 r=n-1 r=n−1,那么结论成立。若 r < n − 1 r<n-1 r<n−1,则可对 r + 1 r+1 r+1维线性子空间 V r + 1 = < α 1 ⃗ , α 2 , ⃗ ⋯ , α r ⃗ , α r + 1 ⃗ > V_{r+1}=<\vec{\alpha_1},\vec{\alpha_2,}\cdots,\vec{\alpha_r},\vec{\alpha_{r+1}}> Vr+1=<α1,α2,⋯,αr,αr+1>
重复此过程找到 α r + 2 , ⃗ ⋯ , α n ⃗ \vec{\alpha_{r+2},}\cdots,\vec{\alpha_n} αr+2,⋯,αn,使得 α 1 ⃗ , α 2 , ⃗ ⋯ , α r ⃗ , α r + 1 ⃗ , ⋯ , α n ⃗ \vec{\alpha_1},\vec{\alpha_2,}\cdots,\vec{\alpha_r},\vec{\alpha_{r+1}},\cdots,\vec{\alpha_n} α1,α2,⋯,αr,αr+1,⋯,αn成为 V n V_n Vn的一个基。
求解方式:解方程组
以3维向量空间 R 3 \mathbb{R}^3 R3为例,假设存在一单位向量 a 1 ⃗ = [ a 1 a 2 a 3 ] \vec{a_1}=\begin{bmatrix}a_1 \\a_2\\a_3\\ \end{bmatrix} a1= a1a2a3 ,此时可找到另外两个单位向量 b 1 ⃗ , b 2 ⃗ \vec{b_1},\vec{b_2} b1,b2,使得这三个向量构成 R 3 \mathbb{R}^3 R3的标准正交基: a 1 ⃗ , b 1 ⃗ , b 2 ⃗ \vec{a_1},\vec{b_1},\vec{b_2} a1,b1,b2。求解方式则是解方程组 a 1 ⃗ ⊤ x = 0 \vec{a_1}^{\top}x=0 a1⊤x=0 → a 1 x 1 + a 2 x 2 + a 3 x 3 + = 0 a_1x_1+a_2x_2+a_3x_3+=0 a1x1+a2x2+a3x3+=0。解得基础解系 ξ 1 ⃗ , ξ 2 ⃗ \vec{\xi_1},\vec{\xi_2} ξ1,ξ2,在进行施密特正交化、标准化,可得 b 1 ⃗ , b 2 ⃗ \vec{b_1},\vec{b_2} b1,b2。
U m × m = [ u 1 ⃗ , u 2 ⃗ , ⋯ , u r ⃗ , u r + 1 ⃗ , ⋯ , u n ⃗ , b 1 ⃗ , ⋯ , b m − r ⃗ ] m × m U_{m\times m}=[\vec{u_1}, \vec{u_2}, \cdots,\vec{u_r}, \vec{u_{r+1}},\cdots,\vec{u_n},\vec{b_1},\cdots,\vec{b_{m-r}}]_{m\times m} Um×m=[u1,u2,⋯,ur,ur+1,⋯,un,b1,⋯,bm−r]m×m
-
完成奇异值分解
A m × n = U m × m Σ m × n V m × m = U m × m [ λ 1 λ 2 O ⋱ λ r O O ] m × n V n × n ⊤ A_{m\times n}=U_{m\times m}\Sigma_{m\times n}V_{m\times m}=U_{m\times m}\begin{bmatrix} \sqrt{\lambda_1} & & & & \\ & \sqrt{\lambda_2} & & & O \\ & & \ddots & & \\ & & & \sqrt{\lambda_r} & \\ & O & & & O \end{bmatrix}_{m\times n}V^{\top}_{n\times n} Am×n=Um×mΣm×nVm×m=Um×m λ1λ2O⋱λrOO m×nVn×n⊤
奇异值分解步骤B
-
计算 A A ⊤ AA^\top AA⊤和 A ⊤ A A^\top A A⊤A(左 右)
-
计算 A A ⊤ AA^\top AA⊤和 A ⊤ A A^\top A A⊤A特征值(左 右)
-
奇异值为二者非0特征值的开方,二者特征向量分别构成左奇异矩阵和右奇异矩阵
举例奇异值分解
假设存在非方阵
A
A
A
A
=
[
3
2
3
2
3
−
2
]
A=\begin{bmatrix} 3 & 2 & 3 \\ 2 & 3 & -2 \\ \end{bmatrix}
A=[32233−2]
计算
A
A
⊤
AA^\top
AA⊤和
A
⊤
A
A^\top A
A⊤A
A
A
⊤
=
[
17
8
8
17
]
,
A
⊤
A
=
[
13
12
2
13
13
−
2
2
−
2
8
]
AA^\top=\begin{bmatrix} 17 & 8 \\ 8 & 17 \end{bmatrix}, A^\top A=\begin{bmatrix} 13 & 12 & 2 \\ 13 & 13 & -2 \\ 2 & -2 & 8 \end{bmatrix}
AA⊤=[178817],A⊤A=
131321213−22−28
计算
A
A
⊤
AA^\top
AA⊤和
A
⊤
A
A^\top A
A⊤A特征值(左 右)
A A ⊤ AA^\top AA⊤特征值: λ 1 = 25 \lambda_1=25 λ1=25, λ 2 = 9 \lambda_2=9 λ2=9;特征向量: u 1 ⃗ = [ 1 2 1 2 ] \vec{u_1}=\begin{bmatrix}\frac{1}{\sqrt{2}} \\ \frac{1}{\sqrt{2}} \end{bmatrix} u1=[2121], u 2 ⃗ = [ 1 2 − 1 2 ] \vec{u_2}=\begin{bmatrix}\frac{1}{\sqrt{2}} \\ -\frac{1}{\sqrt{2}} \end{bmatrix} u2=[21−21]。
A ⊤ A A^\top A A⊤A特征值: λ 1 = 25 \lambda_1=25 λ1=25, λ 2 = 9 \lambda_2=9 λ2=9, λ 3 = 0 \lambda_3=0 λ3=0;特征向量: v 1 ⃗ = [ 1 2 1 2 0 ] \vec{v_1}=\begin{bmatrix}\frac{1}{\sqrt{2}} \\ \frac{1}{\sqrt{2}} \\ 0 \end{bmatrix} v1= 21210 , v 3 ⃗ = [ 1 18 − 1 18 4 18 ] \vec{v_3}=\begin{bmatrix}\frac{1}{\sqrt{18}} \\ -\frac{1}{\sqrt{18}} \\ \frac{4}{\sqrt{18}}\end{bmatrix} v3= 181−181184 , v 3 ⃗ = [ 2 3 − 2 3 − 1 3 ] \vec{v_3}=\begin{bmatrix}\frac{2}{3} \\ -\frac{2}{3} \\ -\frac{1}{3}\end{bmatrix} v3= 32−32−31 。
奇异值为二者非0特征值的开方,二者特征向量分别构成左奇异矩阵和右奇异矩阵
U = ( u 1 ⃗ , u 2 ⃗ ) = [ 1 2 1 2 1 2 − 1 2 ] U=(\vec{u_1},\vec{u_2})=\begin{bmatrix} \frac{1}{\sqrt{2}} & \frac{1}{\sqrt{2}} \\ \frac{1}{\sqrt{2}} & -\frac{1}{\sqrt{2}} \\ \end{bmatrix} U=(u1,u2)=[212121−21]
V
=
(
v
1
⃗
,
v
2
⃗
,
v
3
⃗
)
=
[
1
2
1
18
2
3
1
2
−
1
18
−
2
3
0
4
18
−
1
3
]
V=(\vec{v_1},\vec{v_2},\vec{v_3})=\begin{bmatrix} \frac{1}{\sqrt{2}} & \frac{1}{\sqrt{18}} & \frac{2}{3} \\ \frac{1}{\sqrt{2}} & -\frac{1}{\sqrt{18}} & -\frac{2}{3} \\ 0 & \frac{4}{\sqrt{18}} & -\frac{1}{3} \end{bmatrix}
V=(v1,v2,v3)=
21210181−18118432−32−31
Σ
=
[
λ
1
λ
2
]
=
[
5
3
]
\Sigma=\begin{bmatrix} \sqrt{\lambda_1} & \\ & \sqrt{\lambda_2} \end{bmatrix}=\begin{bmatrix} 5 & \\ & 3 \end{bmatrix}
Σ=[λ1λ2]=[53]
奇异值σ与特征值λ类似,在矩阵Σ中降序排列,且σ降速极快,多数情况下前10%甚至1%的奇异值的和就占了全部的奇异值之和的99%以上了。故可以仅保留矩阵Σ中前k个主要奇异值,以求近似描述矩阵。而k是一个远小于m、n的数,这样就实现了矩阵的压缩。通过奇异值分解,我们可以通过更加少量的数据来近似替代原矩阵。
PyTorch - 特征值求奇异值
导入所需库
import torch
import numpy as np
A = torch.tensor([[1., 2., 3., 4.],
[2., 3., 4., 5.],
[3., 4., 5., 6.]])
奇异值分解
U, S, V = A.svd(False, True)
print("U: ",U)
print("S: ",S)
print("V: ",V)
与转置乘与被乘后特征值分解
vt, Vm = torch.linalg.eig(A.T @ A)
print("vt: ", vt)
print("Vm: ", Vm)
print("="*80)
ut, Um = torch.linalg.eig(A @ A.T)
print("ut: ", ut)
print("Um: ", Um)
还原真值(证明 仅有两个 非方阵奇异值/方阵特征值 大于等于0)
print(S - 2)
print("="*80)
print(vt - 2)
print("="*80)
print(ut - 2)
比对结果
vs = torch.sqrt(vt[:2])
us = torch.sqrt(ut[:2])
print(vs)
print("="*40)
print(us)
print("="*40)
print(S[:2])
PyTorch - 奇异值分解
torch.svd()已被弃用,取而代之的是torch.linear.svd(),并将在未来的PyTorch版本中删除。
U, S, V = torch.svd(A, some=True, compute_uv=True)
参数说明:
some
:是否简化- True:默认值 返回简化后奇异值分解结果(A为m×n矩阵:矩阵U、矩阵V 将只包含 m i n ( m , n ) min(m,n) min(m,n)个标准正交列)
- False:完整奇异值分解结果
compute_uv
:是否计算左右奇异矩阵数值- True:默认值 计算数值
- False:不计算数值 仅保留形状的零填充矩阵 致使参数
some
失效
举例说明:
-
定义矩阵A,并对比奇异值分解。
# 定义矩阵A A = torch.tensor([[1., 2., 3., 4.], [2., 3., 4., 5.], [3., 4., 5., 6.]]) # SVD分解 print(A.svd()) print("-------------------------------------------------") print(A.svd(False, True))
-
验证奇异值分解。
# 解包结果 U, S, V = A.svd(False, True) # 构造对角Sigma奇异值矩阵 Sigma = np.zeros((3, 4)) for i in range(3): Sigma[i][i] = S[i] # 输出结果 print((U @ Sigma) @ V.T.double())
图像举例
import cv2 import numpy as np import torch import matplotlib import matplotlib.pyplot as plt # 添加字体路径到配置中 matplotlib.rcParams['font.sans-serif'] = ['SimHei'] # 或者 'STSong' 等其他中文字体 matplotlib.rcParams['axes.unicode_minus'] = False # 正确显示负号 image_path = './pic/img1.png' lambda_max = 50 def torch_svd_compress(image_path, lambda_num): image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # 解包结果 U, S, V = torch.tensor(image, dtype=float).svd(False, True) # 构造对角Sigma奇异值矩阵(超出范围置0) Sigma = np.zeros((U.shape[0], V.shape[0])) for i in range(lambda_num): Sigma[i][i] = S[i] result = (U @ Sigma) @ V.T.double().numpy() # 绘图 fig, axes = plt.subplots(1, 3, figsize=(15, 3)) axes[0].plot(S, c='darkblue', lw=3) axes[0].plot([lambda_num, lambda_num],[0, S[0]], c='#00aaff', lw=2) axes[0].set_title("奇异值曲线") axes[0].set_ylabel("λ值") axes[0].set_xlabel("总数") axes[1].imshow(image) axes[1].set_title("原图") axes[2].imshow(result) axes[2].set_title(f"压缩图:前{lambda_num}个奇异值") fig.savefig(f'plot_{lambda_num}.png') plt.show() return result # 对比多种结果 for lambda_num in range(0 + 1, lambda_max + 1, 10): torch_svd_compress(image_path, lambda_num=lambda_num) torch_svd_compress(image_path, lambda_num=1000)
亦可根据压缩比率计算lambda_num
,这里不做赘述,感兴趣的小伙伴可以自行尝试。
lambda_num = np.argmax(np.cumsum(s) / np.sum(s) > ratio) # 根据压缩比率选择保留的奇异值数量
方阵的奇异值与特征值通常不同,当且仅当方阵A为对称正定矩阵时,奇异值与特征值相同。
2.3 因果卷积 Causal Convolutions
形式上表现为“斜腿卷积”:即确保未来时序数据不参与卷积过程
同时结合膨胀卷积,确保感受野足以覆盖底层Input所给定的序列长度 (膨胀因子 2 d 2^d 2d),膨胀因子作用如图所示。
2.4 剪切操作 Chomd1d
剪切操作层在代码中被命名为Chomd1d,其存在确保了输入输出的序列长度一致。
2.5 Resnet用途
Resnet残差连接示意图
较复杂CNN中包含的Resnet残差连接:
随着网络层数的增加,CNN能够由浅入深提取到更多不同层次的特征。同时,提取的特征也更抽象,更具有语义信息。
单纯地增加深度会导致梯度爆炸或梯度消失,可以通过权重参数初始化和采用正则化层解决,这样可以确保几十层网络的训练。
但随着层数的继续增加,网络在训练集上的准确率趋于饱和甚至下降,另一个问题随之出现:网络退化。
虽然70层网络具有更多参数,其解空间包含了30层网络的解空间;但由于网络退化现象 导致70层的网络有时甚至不如30层网络。例如,训练时陷入一个与全局最优解相差甚远的局部最优解。
假设针对某个问题存在一个20层最优的网络结构,但我们设计35层网络,则存在15层冗余网络。
而这14层往往无法实现恒等映射,则会干扰前20层已经近乎正确的抽象,致使模型需要花费更长的路径学习二者间的关系,最终表现出学习效果差的现象。
Resnet提供了在深度路径与恒等映射间权衡的手段,通过 区分主路径和捷径,使得输入信息可直接影响输出。
同时,Resnet可减少过拟合、梯度消失、信息流动等问题。
通常情况下,网络的某一层学习映射函数H(x)比较困难,但如果我们将网络设计为H(x)=F(x)+x,就有将学习恒等映射函数转化为学习一个残差函数F(x)=H(x)-x,只要F(x)=0,就构成了一个恒等映射。参数初始化时权重参数比较小,非常适合学习F(x)=0,因此拟合残差会更加容易,这就是残差网络的思想。
下图为残差模块的结构。
该模块提供了两种选择方式,也就是
- identity mapping( x :右侧“超车道”,称为 shortcut 连接)
- residual mapping( F(x) :左侧“普通车道” )
如果网络已经到达最优,继续加深网络,residual mapping 将被 push 为 0,只剩下 identity mapping,这样理论上网络一直处于最优状态了,网络的性能也就不会随着深度增加而降低了。
这种残差模块结构可以通过前向神经网络 + shortcut 连接实现。而且 shortcut 连接相当于简单执行了同等映射,不会产生额外的参数,也不会增加计算复杂度,整个网络依旧可以通过端到端的反向传播训练。
上图中残差模块包含两层网络。实验证明,残差模块往往需要两层以上,单单一层的残差模块 并不能起到提升作用。
当输入输出向量形状相同时,1×1卷积等同于x本身,这点体现在TCN代码的downsample层。
在TCN中,使用1×1卷积实现残差连接,确保输入与输出通道数相同。
其他残差连接方式:
- 加权残差连接(WRC)
- 跨阶段部分连接(CSP)
3. 模型实现
3.1 单层执行
import torch
import torch.nn as nn
import torchinfo
from torchinfo import summary
from TCN import *
%load_ext autoreload
%autoreload 2
net = TemporalConvNet(1, [12, 12], kernel_size=2).to("cuda")
print(summary(net, input_size=(8, 1, 6)))
# 1 12 12
# dilation_size = 2**0
dilation_size = 1
# padding = (2 - 1) * 1
padding = 1
x = torch.randn(8, 1, 6)
class Chomp1d(nn.Module):
def __init__(self, chomp_size):
super(Chomp1d, self).__init__()
self.chomp_size = chomp_size
def forward(self, x):
return x[:, :, :-self.chomp_size].contiguous() # 最后表示序列长度
conv1 = nn.Conv1d(in_channels=1, out_channels=12, kernel_size=2, stride=1, padding=1, dilation=1)
conv2 = nn.Conv1d(in_channels=12, out_channels=12, kernel_size=2, stride=1, padding=1, dilation=1)
relu = nn.ReLU()
dropout = nn.Dropout(0.2)
Chomp1d = Chomp1d(chomp_size=1)
dowmsample = nn.Conv1d(in_channels=1, out_channels=12, kernel_size=1)
out = conv1(x)
print(out.shape)
out = Chomp1d(out)
print(out.shape)
out = relu(out)
print(out.shape)
out = dropout(out)
print(out.shape)
out = conv2(out)
print(out.shape)
out = Chomp1d(out)
print(out.shape)
out = relu(out)
print(out.shape)
out = dropout(out)
print(out.shape)
plus = dowmsample(x)
out = (out + plus)
print(out.shape)
# 通道数相等时 12个通道 → 12个通道 等效于x 故无需计算
# 将不同通道数协调 : 通道数不同无法建立有效的残差连接
# so out = (out + x)
3.2 模型代码
导入所需库
import torch
import torch.nn as nn
from torch.nn.utils import weight_norm
修剪
class Chomp1d(nn.Module):
def __init__(self, chomp_size):
super(Chomp1d, self).__init__()
self.chomp_size = chomp_size
def forward(self, x):
return x[:, :, :-self.chomp_size].contiguous()
TCN块
# 1 12 12
# dilation_size = 2**0
dilation_size = 1
# padding = (2 - 1) * 1
padding = 1
x = torch.randn(8, 1, 6)
class Chomp1d(nn.Module):
def __init__(self, chomp_size):
super(Chomp1d, self).__init__()
self.chomp_size = chomp_size
def forward(self, x):
return x[:, :, :-self.chomp_size].contiguous() # 最后表示序列长度
conv1 = nn.Conv1d(in_channels=1, out_channels=12, kernel_size=2, stride=1, padding=1, dilation=1)
conv2 = nn.Conv1d(in_channels=12, out_channels=12, kernel_size=2, stride=1, padding=1, dilation=1)
relu = nn.ReLU()
dropout = nn.Dropout(0.2)
Chomp1d = Chomp1d(chomp_size=1)
dowmsample = nn.Conv1d(in_channels=1, out_channels=12, kernel_size=1)
out = conv1(x)
print("Conv1 : ", out.shape)
out = Chomp1d(out)
print("Chomp1d : ", out.shape)
out = relu(out)
print("ReLU : ", out.shape)
out = dropout(out)
print("Dropout : ", out.shape)
out = conv2(out)
print("Conv2 : ", out.shape)
out = Chomp1d(out)
print("Chomp1d : ", out.shape)
out = relu(out)
print("ReLU : ", out.shape)
out = dropout(out)
print("Dropout : ", out.shape)
plus = dowmsample(x)
out = (out + plus)
print("Res-Conn : ", out.shape)
# 通道数相等时 12个通道 → 12个通道 等效于x 故无需计算
# 将不同通道数协调 : 通道数不同无法建立有效的残差连接
# so out = (out + x)
TCN核心网络
class TemporalConvNet(nn.Module):
def __init__(self, num_inputs, num_channels, kernel_size=2, dropout=0.2):
super(TemporalConvNet, self).__init__()
layers = []
num_levels = len(num_channels)
for i in range(num_levels):
dilation_size = 2 ** i
in_channels = num_inputs if i == 0 else num_channels[i-1]
out_channels = num_channels[i]
layers += [TemporalBlock(in_channels, out_channels, kernel_size, stride=1, dilation=dilation_size,
padding=(kernel_size-1) * dilation_size, dropout=dropout)]
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
4. 简单应用
① 导入三方库
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from TCN import *
from datautils import *
import matplotlib.pyplot as plt
%load_ext autoreload
%autoreload 2
② 读取数据集
df = pd.read_csv('./data/AMZN_2006-01-01_to_2018-01-01.csv')
绘制数据
plt.plot(df["Close"].values)
plt.show()
标准化
from sklearn.preprocessing import StandardScaler
scaler_XY = StandardScaler()
df["Open_Standard"] = scaler_XY.fit_transform(df["Open"].values.reshape(-1, 1))
df["Close_Standard"] = scaler_XY.fit_transform(df["Close"].values.reshape(-1, 1))
建立时间序列
X, Y = build_time_sequence(df, "Close_Standard", "Close_Standard", 6)
划分 训练集 测试集
tt_split_num = 2000
batch_size = 8
train_X, train_Y = X[:tt_split_num], Y[:tt_split_num]
test_X, test_Y = X[tt_split_num:], Y[tt_split_num:]
train_dataset = StockDataset(train_X, train_Y)
test_dataset = StockDataset(test_X, test_Y)
train_dataloader = torch.utils.data.DataLoader(train_dataset, shuffle=True, num_workers=0, batch_size=batch_size)
test_dataloader = torch.utils.data.DataLoader(test_dataset, shuffle=True, num_workers=0, batch_size=batch_size)
③ 定义神经网络
class TCN(nn.Module):
def __init__(self, num_inputs, output_size, num_channels, kernel_size=2, dropout=0.2):
super(TCN, self).__init__()
self.num_inputs = num_inputs
self.tcn = TemporalConvNet(num_inputs, num_channels, kernel_size, dropout=dropout)
self.linear = nn.Linear(num_channels[-1], output_size)
def forward(self, x):
# x = x.reshape(x.shape[0], self.num_input, -1)
output = self.tcn(x)
output = self.linear(output[:,:,-1])
return output
④ 训练神经网络
定义超参数
# 设备
device = "cuda" if torch.cuda.is_available else "cpu"
# 输入层大小
input_size = 6
# 输出层大小
output_size = 1
# 卷积核大小
kernel_size = 2
# 步长
stride = 1
# 填充
padding = 1
# 学习率
lr = 0.0001
# 损失函数
loss_func = nn.MSELoss()
# 训练轮次
epochs = 200
# 网络
net = TCN(num_inputs=1, output_size=1, num_channels=[12, 12, 12, 12]).to(device)
# 优化器
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
net_final_losses = []
def train():
min_loss = 1.0
for epoch in range(epochs):
net.train()
net_loss = 0.0
for x_train, y_train in train_dataloader:
x_train = x_train.to(device=device)
y_train = y_train.to(device=device)
# 1. 正向传播
y_train_pred = net(x_train)
# 2. 计算误差
loss_func_result = loss_func(y_train_pred, y_train)
net_loss += loss_func_result.item()
# 3. 反向传播
optimizer.zero_grad()
loss_func_result.backward()
# 4. 优化参数
optimizer.step()
net_loss = net_loss / len(train_dataloader)
net_final_losses.append(net_loss)
# 对比
if(net_loss < min_loss):
min_loss = net_loss
torch.save(net.state_dict(), "net_test.pth")
print("Saved PyTorch Model State to net_test.pth")
if epoch % 10 == 0:
print("TCN:: Epoch: {}, Loss: {} ".format(epoch + 1, net_loss))
train()
⑤ 测试神经网络
# 使用训练好的模型进行预测
net.load_state_dict(torch.load("net_test.pth"))
net.eval()
with torch.no_grad():
# 准备所有输入序列
X_train = torch.stack([x for x, y in train_dataset])
train_predictions = net(X_train.to(device)).squeeze().cpu().detach().numpy()
with torch.no_grad():
# 准备所有输入序列
X_test = torch.stack([x for x, y in test_dataset])
test_predictions = net(X_test.to(device)).squeeze().cpu().detach().numpy()
# 将预测结果逆归一化
origin_data = df["Close"]
train_predictions = scaler_XY.inverse_transform(train_predictions.reshape(-1, 1))
test_predictions = scaler_XY.inverse_transform(test_predictions.reshape(-1, 1))
绘制结果
# 绘制结果
seq_length = 6
train_length = 2000
plt.figure(figsize=(12, 6))
plt.plot(origin_data, label='Original Data')
plt.plot(range(seq_length,seq_length+len(train_predictions)),train_predictions, label='RNN Train Predictions', linestyle='--')
plt.plot(range(seq_length+train_length,len(test_predictions)+seq_length+train_length), test_predictions, label='RNN Test Predictions', linestyle='--')
plt.legend()
plt.title("Training Set Predictions")
plt.xlabel("Time Step")
plt.ylabel("Value")
plt.show()
original = pd.DataFrame(origin_data)
df_train_predictions = pd.DataFrame(train_predictions)
df_test_predictions = pd.DataFrame(test_predictions)
import seaborn as sns
sns.set_style("darkgrid")
fig = plt.figure(figsize=(16, 6))
fig.subplots_adjust(hspace=0.2, wspace=0.2)
# 画左边的趋势图
plt.subplot(1, 2, 1)
ax = sns.lineplot(x = original.index, y = original["Close"], label="Original Data", color='blue')
ax = sns.lineplot(x = df_train_predictions.index + seq_length, y = df_train_predictions[0], label="RNN Train Prediction", color='red')
ax = sns.lineplot(x = df_test_predictions.index + seq_length + train_length, y = df_test_predictions[0], label="RNN Test Prediction", color='darkred')
ax.set_title('Stock', size = 14, fontweight='bold')
ax.set_xlabel("Days", size = 14)
ax.set_ylabel("Value", size = 14)
ax.set_xticklabels('', size=10)
# 画右边的Loss下降图
plt.subplot(1, 2, 2)
ax = sns.lineplot(data=net_final_losses, label="TCN Loss", color='steelblue', lw=2)
ax.set_xlabel("Epoch", size = 14)
ax.set_ylabel("Loss", size = 14)
ax.set_title("Training Loss", size = 14, fontweight='bold')
plt.show()