以在 GitHub 上执行或查看/下载此 Notebook
复杂和四元数神经网络
本教程演示了如何使用 SpeechBrain 实现的复杂值和四元数值神经网络进行语音技术。它涵盖了高维表示的基础知识以及相关的神经网络层:线性层、卷积层、循环层和归一化层。
前提条件
介绍和背景
复数: 复数将实数的概念扩展到二维空间。复数 z
通常由实部和虚部组成,表示为 z = r + ix
,其中 r
是实部,ix
是虚部。这种数学扩展在现实世界中有着广泛的应用,提供了一个强大的代数框架来处理二维空间中的概念,例如旋转、平移和相位相关操作。复数自然地表示语音信号,傅里叶变换就是一个在复数空间中操作的著名例子,它捕获了振幅和相位信息。
四元数: 四元数将复数推广到三维空间,包含一个实部 (r
) 和一个虚部,虚部是一个三维向量 (ix + jy + kz`)。四元数
q
可以表示为 q = r + ix + jy + kz
。在实践中,四元数用于定义三维旋转,并在物理学、计算机科学、计算机图形学和机器人学中有着广泛的应用。它们为理解和解释三维空间中的运动提供了一个稳定自然的框架。
与神经网络的联系
随着现代深度学习的复兴势头增强,研究人员探索将复数和四元数整合到神经网络中以解决特定任务。复杂值神经网络 (CVNN) 可以直接处理快速傅里叶变换 (FFT) 的输出,而四元数神经网络 (QNN) 可以用于生成逼真的机器人运动。
除了它们对某些表示的天然适应性之外,CVNN 和 QNN 还共享一个引人注目的特性:权重共享。支配复数和四元数的代数规则与实数不同,这影响了四元数或复数的乘法。这种区别导致 Q-CVNN 中存在独特的权重共享机制,与实值网络中的传统点积不同。事实证明,这种机制对于学习多维输入的富有表现力的表示形式非常有用,同时保留了信号分量内部的关系,例如复数的振幅和相位。
在本教程中,由于这些特性的复杂性,我们不会深入探讨其所有细节。相反,我们的目标是提供一个详细指南,说明如何在 SpeechBrain 中有效地实现和利用 CVNN 和 QNN。
相关参考文献
Andreescu, T., & Andrica, D. (2006). Complex Numbers from A to… Z (Vol. 165). Boston: Birkhäuser.
Altmann, S. L. (1989). Hamilton, Rodrigues, and the quaternion scandal. Mathematics Magazine, 62(5), 291-308.
复杂神经网络综述: Hirose, A. (2012). Complex-valued neural networks (Vol. 400). Springer Science & Business Media.
四元数神经网络全解: Parcollet, T., (2019) Quaternion Neural Networks, PhD Thesis, Avignon Université
SpeechBrain 中复杂数和四元数的表示
在 SpeechBrain 中,代数运算被抽象到神经网络层中,用户无需关注初始表示。这种抽象确保用户可以处理实值张量,而无需显式声明复数或四元数的特定张量类型。底层操作以张量/矩阵格式表达,便于与现代 GPU 架构无缝集成。
实际上,你在 recipe 中生成的任何 PyTorch 张量都可以被解释为复杂值或四元数值张量,具体取决于处理它的层。例如
如果由
torch.nn.Linear
层处理,张量将是实数。如果由
nnet.complex_networks.c_linear.CLinear
层处理,张量将是复数。
张量如何被解释和构建?
让我们用一个例子来说明。假设我们要考虑一个包含 3
个复数或 3
个四元数的张量。这些数字的不同部分将按如下方式拼接
对于复数张量 (c_tensor
): [r, r, r, x, x, x]
对于四元数张量 (q_tensor
): [r, r, r, x, x, x, y, y, y, z, z, z]
这种灵活性允许你在代码中声明的任何张量在被 SpeechBrain 中的 {C/Q}-Layer 处理时,都可以被视为复数或四元数张量,前提是特征维度可以被复数的 2 或四元数的 4 整除。
为了进一步探索,我们继续安装 SpeechBrain。
%%capture
# Installing SpeechBrain via pip
BRANCH = 'develop'
!python -m pip install git+https://github.com/speechbrain/speechbrain.git@$BRANCH
!git clone https://github.com/speechbrain/speechbrain.git
现在,让我们尝试操作一些张量以更好地理解形式。我们首先实例化一个包含 8 个实数的张量。
import torch
T = torch.rand((1,8))
print(T)
然后,我们访问 SpeechBrain 库来操作复数,并简单地显示各个部分(实部、虚部)。
from speechbrain.nnet.complex_networks.c_ops import get_real, get_imag
print(get_real(T))
print(get_imag(T))
正如你所见,初始张量简单地被分成 2 份,对于 4 和四元数也一样。
复数和四元数乘积
QNN 和 CVNN 的核心是乘积。当然,还存在其他特性,例如权重初始化、特定的归一化、激活函数等。然而,基本乘积是所有神经网络层的核心:一个乘以输入向量的权重矩阵。
一个非常值得了解的事情是,复数可以用实值矩阵形式表示
四元数也一样
更有趣的是,如果我们乘以其中两个矩阵,那么我们就得到了对应于所考虑代数的乘积。例如,两个复数之间的复数乘积定义为
这等同于正式定义
好的,那么这在 SpeechBrain 中是如何实现的呢??
你可以在复数或四元数库中调用的每一个层都将遵循两个步骤
init(): 将复数/四元数权重定义为 torch.Parameters,并使用适配的方案进行初始化。
forward(): 调用实现特定乘积的相应操作。例如,一个复杂线性层会调用
speechbrain.nnet.complex_networks.c_ops
中的complex_linear_op()
。
实际上,speechbrain.nnet.complex_networks.c_ops.complex_linear_op
函数只是简单地
获取层的权重并构建相应的实值矩阵。
将输入与此矩阵相乘,以模拟复数/四元数乘积。
示例
def complex_linear_op(input, real_weight, imag_weight, bias):
"""
Applies a complex linear transformation to the incoming data.
Arguments
---------
input : torch.Tensor
Complex input tensor to be transformed.
real_weight : torch.Parameter
Real part of the quaternion weight matrix of this layer.
imag_weight : torch.Parameter
First imaginary part of the quaternion weight matrix of this layer.
bias : torch.Parameter
"""
# Here we build the real-valued matrix as defined by the equations!
cat_real = torch.cat([real_weight, -imag_weight], dim=0)
cat_imag = torch.cat([imag_weight, real_weight], dim=0)
cat_complex = torch.cat([cat_real, cat_imag], dim=1)
# If the input is already [batch*time, N]
# We do inputxconstructed_matrix to simulate the product
if input.dim() == 2:
if bias.requires_grad:
return torch.addmm(bias, input, cat_complex)
else:
return torch.mm(input, cat_complex)
else:
output = torch.matmul(input, cat_complex)
if bias.requires_grad:
return output + bias
else:
return output
# We create a single complex number
complex_input = torch.rand(1, 2)
# We create two Tensors (not parameters here because we don't care about storing gradients)
# These tensors are the real_parts and imaginary_parts of the weight matrix.
# The real part is equivalent [nb_complex_numbers_in // 2, nb_complex_numbers_out // 2]
# The imag part is equivalent [nb_complex_numbers_in // 2, nb_complex_numbers_out // 2]
# Hence if we define a layer with 1 complex input and 2 complex outputs:
r_weight = torch.rand((1,2))
i_weight = torch.rand((1,2))
bias = torch.ones(4) # because we have 2 (complex) x times 2 = 4 real-values
# and we forward propagate!
print(complex_linear_op(complex_input, r_weight, i_weight, bias).shape)
重要的是要注意,四元数的实现完全遵循相同的方法。
复杂值神经网络
一旦你熟悉了这个形式,你就可以轻松地推导出 speechbrain.nnet.complex_networks
中给出的任何复杂值神经网络构建块
一维和二维卷积。
批次和层归一化。
线性层。
循环单元(LSTM, LiGRU, RNN)。
根据文献,大多数复数和四元数神经网络依赖于分割激活函数(应用于复数/四元数值信号上的任何实值激活函数)。目前,SpeechBrain 遵循这种方法,并且不提供任何完整的复数或四元数激活函数.
卷积层
首先,我们定义一批输入(例如,可以是 FFT 的输出)。
from speechbrain.nnet.complex_networks.c_CNN import CConv1d, CConv2d
# [batch, time, features]
T = torch.rand((8, 10, 32))
# We define our layer and we want 12 complex numbers as output.
cnn_1d = CConv1d( input_shape=T.shape, out_channels=12, kernel_size=3)
out_tensor = cnn_1d(T)
print(out_tensor.shape)
正如我们所见,我们在输入张量上应用了复杂值一维卷积,并获得了一个特征维度等于 24 的输出张量。实际上,我们请求了 12 个 out_channels
,这相当于 24 个实数值。记住:我们总是使用实数,代数运算在层本身中被抽象化了!
二维卷积也可以这样做。
# [batch, time, fea, Channel]
T = torch.rand([10, 16, 30, 30])
cnn_2d = CConv2d( input_shape=T.shape, out_channels=12, kernel_size=3)
out_tensor = cnn_2d(T)
print(out_tensor.shape)
请注意,二维卷积应用于时间和特征轴。通道轴被视为实部和虚部:[10, 16, 30, 0:15] = 实部
,[10, 16, 30, 15:30] = 虚部
。
线性层
与卷积层类似,我们只需要实例化正确的模块并使用它!
from speechbrain.nnet.complex_networks.c_linear import CLinear
# [batch, time, features]
T = torch.rand((8, 10, 32))
# We define our layer and we want 12 complex numbers as output.
lin = CLinear(12, input_shape=T.shape, init_criterion='glorot', weight_init='complex')
out_tensor = lin(T)
print(out_tensor.shape)
请注意,我们添加了 init_criterion
和 weight_init
参数。这两个参数存在于所有复数和四元数层中,定义了权重的初始化方式。事实上,复数和四元数值权重需要一个仔细的初始化过程,正如 Chiheb Trabelsy 等人在 Deep Complex Networks 中详细描述的那样,以及 Titouan Parcollet 等人在 Quaternion Recurrent Neural Networks
中描述的那样。
归一化层
归一化一组复数(例如,复杂值层的输出)的方式与归一化一组实数的方式不同。由于任务的复杂性,本教程不会深入细节。请注意,代码在相应的 SpeechBrain 库中完全可用,并且严格遵循 Chiheb Trabelsy 等人在论文 Deep Complex Networks 中首次提出的描述。
SpeechBrain 支持复杂批次归一化和层归一化
from speechbrain.nnet.complex_networks.c_normalization import CBatchNorm,CLayerNorm
inp_tensor = torch.rand([10, 16, 30])
# Not that by default the complex axis is the last one, but it can be specified.
CBN = CBatchNorm(input_shape=inp_tensor.shape)
CLN = CLayerNorm(input_shape=inp_tensor.shape)
out_bn_tensor = CBN(inp_tensor)
out_ln_tensor = CLN(inp_tensor)
循环神经网络
循环神经单元只不过是带有时间连接的多个线性层。因此,SpeechBrain 提供了 LSTM、RNN 和 LiGRU 的复数变体的实现。事实上,这些模型与实值模型完全等效,只是线性层被 CLinear 层取代了!
from speechbrain.nnet.complex_networks.c_RNN import CLiGRU, CLSTM, CRNN
inp_tensor = torch.rand([10, 16, 40])
lstm = CLSTM(hidden_size=12, input_shape=inp_tensor.shape, weight_init='complex', bidirectional=True)
rnn = CRNN(hidden_size=12, input_shape=inp_tensor.shape, weight_init='complex', bidirectional=True)
ligru = CLiGRU(hidden_size=12, input_shape=inp_tensor.shape, weight_init='complex', bidirectional=True)
print(lstm(inp_tensor).shape)
print(rnn(inp_tensor).shape)
print(ligru(inp_tensor).shape)
请注意,输出维度是 48,因为我们有 12 个复数(24 个值)乘以 2 个方向(双向 RNN)。
四元数神经网络
幸运的是,SpeechBrain 中的 QNN 完全遵循相同的形式。因此,你可以轻松地从 speechbrain.nnet.quaternion_networks
中给出的构建块推导出任何四元数值神经网络
一维和二维卷积。
批次和层归一化。
线性层和旋量层。
循环单元(LSTM, LiGRU, RNN)。
根据文献,大多数复数和四元数神经网络依赖于分割激活函数(应用于复数/四元数值信号上的任何实值激活函数)。目前,SpeechBrain 遵循这种方法,并且不提供任何完整的复数或四元数激活函数.
我们刚刚在复杂神经网络中看到的一切在这里仍然有效。因此,我们可以将所有内容总结在一个代码片段中
from speechbrain.nnet.quaternion_networks.q_CNN import QConv1d, QConv2d
from speechbrain.nnet.quaternion_networks.q_linear import QLinear
from speechbrain.nnet.quaternion_networks.q_RNN import QLiGRU, QLSTM, QRNN
# [batch, time, features]
T = torch.rand((8, 10, 40))
# [batch, time, fea, Channel]
T_4d = torch.rand([10, 16, 30, 40])
# We define our layers and we want 12 quaternion numbers as output (12x4 = 48 output real-values).
cnn_1d = QConv1d( input_shape=T.shape, out_channels=12, kernel_size=3)
cnn_2d = QConv2d( input_shape=T_4d.shape, out_channels=12, kernel_size=3)
lin = QLinear(12, input_shape=T.shape, init_criterion='glorot', weight_init='quaternion')
lstm = QLSTM(hidden_size=12, input_shape=T.shape, weight_init='quaternion', bidirectional=True)
rnn = QRNN(hidden_size=12, input_shape=T.shape, weight_init='quaternion', bidirectional=True)
ligru = QLiGRU(hidden_size=12, input_shape=T.shape, weight_init='quaternion', bidirectional=True)
print(cnn_1d(T).shape)
print(cnn_2d(T_4d).shape)
print(lin(T).shape)
print(lstm(T)[0].shape) # RNNs return output + hidden so we need to filter !
print(ligru(T)[0].shape) # RNNs return output + hidden so we need to filter !
print(rnn(T)[0].shape) # RNNs return output + hidden so we need to filter !
四元数旋量神经网络
介绍: 四元数旋量神经网络 (SNN) 是四元数值神经网络 (QNN) 中的一个特殊类别。如前所述,四元数旨在表示旋转。在 QNN 层中,基本运算涉及哈密顿积 (输入 x 权重
),其中输入和权重都是四元数集。这个乘积本质上创建了一个新的旋转,等同于第一个旋转后接第二个旋转的组合。
旋转合成: 将两个四元数相乘会得到一个旋转,该旋转结合了每个四元数所代表的单独旋转。例如,给定 q3 = q1 x q2
,这意味着 q3 是一个旋转,等同于由 q1 执行的旋转后接由 q2 执行的旋转的组合。在旋量神经网络的上下文中,这个概念被用来合成新的旋转,不是为了物理上旋转物体,而是为了预测连续的旋转。例如,预测机器人接下来的运动涉及使用之前的运动(表示为一个四元数)作为输入,产生一个新的四元数作为输出,捕捉预期的下一个运动。
使用 SNN 建模旋转: 旋量神经网络 (SNN) 专门用于建模旋转。在机器人运动等场景中,SNN 将运动前物体的三维坐标 (x, y, z) 作为输入,并预测运动后物体的坐标作为输出。
正式旋转方程: 为了实现这一点,网络所有层中的标准乘积被以下方程替换
此方程正式定义了向量 \(\vec{v}\) 经过单位四元数 \(q_{weight}\) (范数为 1) 的旋转,其中 \(q^{-1}\) 代表四元数的共轭。此方程中的左右乘积都是哈密顿积。
总之,四元数旋量神经网络专门用于建模旋转,这使得它们特别适用于预测连续旋转或运动至关重要的应用,例如机器人学或动画。
好的,那么这在 SpeechBrain 中是如何实现的呢?
与标准哈密顿积完全相同的方式!事实上,这种旋转也可以表示为矩阵乘积
因此,我们只需要定义遵循相同常规过程的 quaternion_op
从不同的权重分量构建一个实值矩阵
对输入和这个旋转矩阵应用矩阵乘积!
将四元数层转换为旋量层
在所有四元数层中,可以通过一个布尔参数激活旋量层。这里有几个例子
from speechbrain.nnet.quaternion_networks.q_CNN import QConv1d
from speechbrain.nnet.quaternion_networks.q_linear import QLinear
# [batch, time, features]
T = torch.rand((8, 80, 16))
#
# NOTE: in this case the real components must be zero as spinor neural networks
# only input and output 3D vectors ! We don't do it here for the sake of compactness
#
# We define our layers and we want 12 quaternion numbers as output (12x4 = 48 output real-values).
cnn_1d = QConv1d( input_shape=T.shape, out_channels=12, kernel_size=3, spinor=True, vector_scale=True)
lin = QLinear(12, input_shape=T.shape, spinor=True, vector_scale=True)
print(cnn_1d(T).shape)
print(lin(T).shape)
关于旋量层的两点说明
为了训练深层模型,我们需要设置一个 vector_scale。vector_scale 只是另一组 torch.Parameters,它会按比例缩小每个旋量层的输出。实际上,SNN 层的输出是一组三维向量,这些向量是旋转后的三维向量之和。四元数旋转不影响旋转向量的幅度。因此,通过一遍又一遍地对旋转后的三维向量求和,我们可能会很快得到非常大的值(即训练会爆炸)。
你可能考虑使用
weight_init='unitary'
。实际上,只有当所考虑的四元数是单位四元数时,四元数旋转才有效。因此,从单位权重开始可能有助于学习阶段!
将所有东西整合在一起!
我们提供了复杂神经网络和四元数神经网络的最小示例
speechbrain/tests/integration/ASR_CTC/example_asr_ctc_experiment_complex_net.yaml
.speechbrain/tests/integration/ASR_CTC/example_asr_ctc_experiment_quaternion_net.yaml
.
如果我们看一下这些 YAML 参数文件中的一个,我们可以很容易地区分如何用不同的块构建我们的模型!
yaml_params = """
model: !new:speechbrain.nnet.containers.Sequential
input_shape: [!ref <N_batch>, null, 660] # input_size
conv1: !name:speechbrain.nnet.quaternion_networks.q_CNN.QConv1d
out_channels: 16
kernel_size: 3
act1: !ref <activation>
conv2: !name:speechbrain.nnet.quaternion_networks.q_CNN.QConv1d
out_channels: 32
kernel_size: 3
nrm2: !name:speechbrain.nnet.quaternion_networks.q_CNN.QConv1d
act2: !ref <activation>
pooling: !new:speechbrain.nnet.pooling.Pooling1d
pool_type: "avg"
kernel_size: 3
RNN: !name:speechbrain.nnet.quaternion_networks.q_RNN.QLiGRU
hidden_size: 64
bidirectional: True
linear: !name:speechbrain.nnet.linear.Linear
n_neurons: 43 # 42 phonemes + 1 blank
bias: False
softmax: !new:speechbrain.nnet.activations.Softmax
apply_log: True
"""
这里,我们有一个非常基本的四元数值 CNN-LiGRU 模型,可以用来执行端到端 CTC ASR!
%cd /content/speechbrain/tests/integration/ASR_CTC/
!python example_asr_ctc_experiment.py example_asr_ctc_experiment_quaternion_net.yaml
引用 SpeechBrain
如果你在研究或商业中使用 SpeechBrain,请使用以下 BibTeX 条目引用它
@misc{speechbrainV1,
title={Open-Source Conversational AI with {SpeechBrain} 1.0},
author={Mirco Ravanelli and Titouan Parcollet and Adel Moumen and Sylvain de Langen and Cem Subakan and Peter Plantinga and Yingzhi Wang and Pooneh Mousavi and Luca Della Libera and Artem Ploujnikov and Francesco Paissan and Davide Borra and Salah Zaiem and Zeyu Zhao and Shucong Zhang and Georgios Karakasidis and Sung-Lin Yeh and Pierre Champion and Aku Rouhe and Rudolf Braun and Florian Mai and Juan Zuluaga-Gomez and Seyed Mahed Mousavi and Andreas Nautsch and Xuechen Liu and Sangeet Sagar and Jarod Duret and Salima Mdhaffar and Gaelle Laperriere and Mickael Rouvier and Renato De Mori and Yannick Esteve},
year={2024},
eprint={2407.00463},
archivePrefix={arXiv},
primaryClass={cs.LG},
url={https://arxiv.org/abs/2407.00463},
}
@misc{speechbrain,
title={{SpeechBrain}: A General-Purpose Speech Toolkit},
author={Mirco Ravanelli and Titouan Parcollet and Peter Plantinga and Aku Rouhe and Samuele Cornell and Loren Lugosch and Cem Subakan and Nauman Dawalatabad and Abdelwahab Heba and Jianyuan Zhong and Ju-Chieh Chou and Sung-Lin Yeh and Szu-Wei Fu and Chien-Feng Liao and Elena Rastorgueva and François Grondin and William Aris and Hwidong Na and Yan Gao and Renato De Mori and Yoshua Bengio},
year={2021},
eprint={2106.04624},
archivePrefix={arXiv},
primaryClass={eess.AS},
note={arXiv:2106.04624}
}