0%

导向矢量

选定参考阵元后,后面阵元接收到的信号比参考阵元晚:

其中c为声速,δ为后续阵元相对参考阵元的间距(针对复杂阵元δ不为定值),θ为信号入射角度,将其反应到信号的相位变化上(假设输入为单频信号):

由上图可知,对于相对于参考点更晚到达的信号,其t为正值,因此导向矢量中取负相位来表示延后

其中a反应了不同a阵元的空间相位差,即导向矢量,受到以下变量影响:

  • 阵元布置结构,影响阵元间距
  • 来波方向,影响入射角
  • 参考阵元位置,影响其他阵元计算间距,不影响阵元间相对相位差

若多个声源从不同角度入射,则导向矢量变为一个MxN的矩阵,M为阵元个数,N为信号数量

利用导向矢量估计声源入射方向

当入射角未知时,当阵元布置和参考阵元确定时,导向矢量的形式是确定的,且只受到入射角影响,因此可以构造一个方向为α的导向矢量形式的向量(共轭转置)与输入信号做内积计算:

当角度重合时,内积最大

波束形成延迟求和成像

由上一章得知麦克风信号为对参考信号相位分别进行了减法的信号,因此只要对麦克风信号的相位分别进行相反的加法(加权)即可得到原始的信号波形:

因此,在进行某个方向的波束求和时,频率f为变量(f依次变化计算出每个频率上的响应)

(其实原理和上一小节的估计声源方向一样)

综上,对于(θ,φ)方向上,f频率下的功率为:(Y为f下各通道的复信号向量)

为了在今后计算中加以区分,以w代表延时权重,a代表导向矢量,这与clean-sc论文中不同,论文中的w为导向矢量(矩阵),因此本文中w的相位与论文是相反的。

使用交叉谱矩阵

在实际工业场景中,往往不使用上述的单快拍FFT的方法,因为该方法中的Y(f)通常只来源于一段极短的时域信号(DFT的原理,详见数学基础 | 小董的BLOG),也就是单快拍数据。这种约等于瞬时成像的方法容易受到随机噪声的干扰,从而产生一些虚假的声源信息。

因此,我们引入了多快拍计算方法,也就是在一次成像中使用多段时域信号进行计算,我们用一个矩阵表示运算结果:

这个矩阵就是交叉谱矩阵CSM,每个值代表了两个麦克风的相关程度,对角线则为当前麦克风的能量信息(无相位信息,因此在计算中可考虑不使用对角线元素)。其中X_n代表第n个麦克风在某个频率f0下的FFT 复数值。

在原来的单快拍计算中:

当引入CSM后并加入对能量取平均,则有:

因此对于延迟权重向量(矩阵)W,其与单快拍计算中的完全一致,连形状都不用变。

声学橡皮擦(HDR)

当环境中存在多个声源时,较弱的声源容易直接被强声源覆盖,若想显示该弱声源,则可以通过波束形成单独复原强声源波形,再将该波形从麦克风信号中减去。对于该波形的波束形成,有:

需要注意的是,这里与波束形成成像略有不同。此处在对每个通道的麦克风信号分别进行相位对其后,直接将所有通道信号相加再取平均,是最标准的波束形成。相加之后,在(θ,φ)方向上的信号会汇聚(因为对它们的相位进行了对齐),其他方向上的信号则因为相位不一致,最终被弱化。

由于延迟权重w是根据参考阵元计算得到的,因此最终复原的s1波形相位也与参考阵元相同。所有我们如果想在每个通道中都减去s1,就要也把每个通道数据的相位先对齐至参考阵元(其实也还是完全一样的延时操作):

减去s1后再把每个通道的麦克风数据按照反方向进行相位调整即可得到去除s1的各通道数据:

后续再进行前文中的成像即可,或者也可以直接使用以对齐的Y’进行成像(理论上可以,我没有试过,我是先用Y计算CSM后再计算的功率)

同时,我们也可以单独的对s1进行更清晰的成像,排除弱声源的干扰:

虽然感觉减来减去的有点多此一举,但是论文中确实也是这样写的。理论上对\hat f(s_1)根据延迟情况复制M个直接成像也可以?我懒得试了。总之最重要的是在进行波束形成时务必要通过求和,才可以获得估计的s1,也就是本小节第一个公式,其他的大家可以自由发挥。

1源码中的sLSTM无法使用,本文只分析基于mLSTM的xLSTM结构梳理

xLSTMLMModel()

实例化模型

1
2
3
4
5
6
def forward(self, idx: torch.Tensor) -> torch.Tensor:
# x = self.token_embedding(idx)
# x = self.emb_dropout(x)
x = self.xlstm_block_stack(idx)
logits = self.lm_head(x)#全连接层
return logits
  • 若用于语言模型,则先通过nn.Embedding(num_embeddings=.., embedding_dim=..)进行一次词索引

    对于该函数,其作用是定义一个embedding模块,包含num_embeddings个张量,每个张量大小为embedding_dim。对于每个张量的索引则通过输入整型数字进行索引。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import torch
    import torch.nn as nn

    embedding = nn.Embedding(5, 3)
    input = torch.LongTensor([2])#对embeding模块中下标为2的张量进行索引
    e = embedding(input)

    print(embedding.weight)
    print(e)

    '''
    Parameter containing:
    tensor([[ 1.0561, 1.7934, -0.1304],
    [ 1.4425, 0.8412, 0.1474],
    [-0.0995, 0.1439, -0.7001],
    [ 0.3784, -1.0610, 0.0362],
    [ 0.5086, -0.5861, -0.4548]], requires_grad=True)

    tensor([[-0.0995, 0.1439, -0.7001]], grad_fn=<EmbeddingBackward0>)
    '''
    • embedding()的输入是任意维度的tensor格式,但tensor中的值必须为<=num_embeddings-1的数

    • 若输入的维度为[a,b,c],则输出维度为[a,b,c,embedding_dim]

    • 语言模型中该方法可以使num_embeddings个词获得唯一的嵌入张量,张量大小为embedding_dim
  • 若用于语言模型,则将索引到的嵌入词向量通过nn.Dropout()进行正则化

    1
    2
    3
    m = nn.Dropout(0.2)
    input = torch.randn(20, 16)
    output = m(input) #将input中随机20%的值变0,其他值则除以0.8
  • 将要处理的tensor输入xLSTMBlockStack(config=...)模块中

  • 全连接层

xLSTMBlockStack()

主要由创建xlstm的block和归一化组成,该类主要实现多个lstm块的实例化、拼接和归一化

blocks通过列表创建+nn.ModuleList()拼接的方式进行实例化

详解PyTorch中的ModuleList和Sequential - 知乎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
self.blocks = self._create_blocks(config=config)
def _create_blocks(self, config: xLSTMBlockStackConfig):#先进行

blocks = []#block通过列表形式创建
for block_idx, block_type_int in enumerate(config.block_map):
if block_type_int == 0:#根据该对象值判断创建哪种block
config = deepcopy(self.config.mlstm_block)
if hasattr(config, "_block_idx"):
config._block_idx = block_idx
config.__post_init__()
blocks.append(mLSTMBlock(config=config))
elif block_type_int == 1:
config = deepcopy(self.config.slstm_block)
if hasattr(config, "_block_idx"):
config._block_idx = block_idx
config.__post_init__()
blocks.append(sLSTMBlock(config=config))
else:
raise ValueError(f"Invalid block type {block_type_int}")

return nn.ModuleList(blocks)#拼接block

1
2
3
4
5
6
7
8
def forward(self, x: torch.Tensor, **kwargs) -> torch.Tensor:

for block in self.blocks:
x = block(x, **kwargs)

x = self.post_blocks_norm(x)

return x

self.post_blocks_norm(x)调用的LayerNorm()会在下一节说到

根据config.block_map中的值会创建n个block

mLSTMBlock()

该类会继承父类xLSTMBlock(),该部分主要实现mLSTM的残差结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class xLSTMBlock(nn.Module):
"""An xLSTM block can be either an sLSTM Block or an mLSTM Block.

It contains the pre-LayerNorms and the skip connections.
"""

config_class = xLSTMBlockConfig

def __init__(self, config: xLSTMBlockConfig) -> None:
super().__init__()
self.config = config
embedding_dim = (
self.config.mlstm.embedding_dim if self.config.mlstm is not None else self.config.slstm.embedding_dim
)

self.xlstm_norm = LayerNorm(ndim=embedding_dim, weight=True, bias=False)

if self.config.mlstm is not None:
self.xlstm = mLSTMLayer(config=self.config.mlstm)
elif self.config.slstm is not None:
self.xlstm = sLSTMLayer(config=self.config.slstm)
else:
raise ValueError("Either mlstm or slstm must be provided")

if self.config.feedforward is not None:
self.ffn_norm = LayerNorm(ndim=self.config.feedforward.embedding_dim, weight=True, bias=False)
self.ffn = create_feedforward(config=self.config.feedforward)
else:
self.ffn_norm = None
self.ffn = None

self.reset_parameters()

def forward(self, x: torch.Tensor, **kwargs) -> torch.Tensor:
x = x + self.xlstm(self.xlstm_norm(x), **kwargs) #残差结构
if self.ffn is not None:
x = x + self.ffn(self.ffn_norm(x), **kwargs)
return x

def step(self, x: torch.Tensor, **kwargs) -> tuple[torch.Tensor, dict[str, tuple[torch.Tensor, ...]]]:
x_xlstm, xlstm_state = self.xlstm.step(self.xlstm_norm(x), **kwargs)
x = x + x_xlstm
if self.ffn is not None:
x = x + self.ffn(self.ffn_norm(x), **kwargs)
return x, xlstm_state

def reset_parameters(self) -> None:
self.xlstm.reset_parameters()
self.xlstm_norm.reset_parameters()
if self.ffn is not None:
self.ffn.reset_parameters()
self.ffn_norm.reset_parameters()

  • forward中可以看到,mLSTM使用了残差结构
  • self.xlstm_norm = LayerNorm实现了带有可选偏置(bias)和残差权重(residual_weight)的 归一化处理。
  • 通过mLSTMLayer(config=self.config.mlstm)建立mLSTM层

mLSTMLayer

mLSTM层的底层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
class mLSTMLayerConfig(UpProjConfigMixin):
conv1d_kernel_size: int = 4
qkv_proj_blocksize: int = 4
num_heads: int = 4
proj_factor: float = 2.0

# will be set toplevel config
embedding_dim: int = -1
bias: bool = False
dropout: float = 0.0
context_length: int = -1

_num_blocks: int = 1
_inner_embedding_dim: int = None

def __post_init__(self):
self._set_proj_up_dim(embedding_dim=self.embedding_dim)
self._inner_embedding_dim = self._proj_up_dim


class mLSTMLayer(nn.Module):
config_class = mLSTMLayerConfig

def __init__(self, config: mLSTMLayerConfig):
super().__init__()
self.config = config

self.proj_up = nn.Linear(
in_features=self.config.embedding_dim,
out_features=2 * self.config._inner_embedding_dim,
bias=self.config.bias,
)

num_proj_heads = round(self.config._inner_embedding_dim // self.config.qkv_proj_blocksize)
self.q_proj = LinearHeadwiseExpand(
config=LinearHeadwiseExpandConfig(
in_features=self.config._inner_embedding_dim,
num_heads=num_proj_heads,
bias=self.config.bias,
)
)
self.k_proj = LinearHeadwiseExpand(
config=LinearHeadwiseExpandConfig(
in_features=self.config._inner_embedding_dim,
num_heads=num_proj_heads,
bias=self.config.bias,
)
)
self.v_proj = LinearHeadwiseExpand(
config=LinearHeadwiseExpandConfig(
in_features=self.config._inner_embedding_dim,
num_heads=num_proj_heads,
bias=self.config.bias,
)
)

self.conv1d = CausalConv1d(
config=CausalConv1dConfig(
feature_dim=self.config._inner_embedding_dim,
kernel_size=self.config.conv1d_kernel_size,
)
)
self.conv_act_fn = nn.SiLU()
self.mlstm_cell = mLSTMCell(
config=mLSTMCellConfig(
context_length=self.config.context_length,
embedding_dim=self.config._inner_embedding_dim,
num_heads=self.config.num_heads,
)
)
self.ogate_act_fn = nn.SiLU()

self.learnable_skip = nn.Parameter(torch.ones(self.config._inner_embedding_dim, requires_grad=True))

self.proj_down = nn.Linear(
in_features=self.config._inner_embedding_dim,
out_features=self.config.embedding_dim,
bias=self.config.bias,
)
self.dropout = nn.Dropout(self.config.dropout)
self.reset_parameters()

def forward(self, x: torch.Tensor, **kwargs) -> torch.Tensor:
B, S, _ = x.shape #输入为张量维度为(batch,time,feature)
'''
batch:同dataloader输出的batch维度
time:时间序列
feature:每个序列的值,对于频谱则为特征频率的幅值
'''
# up-projection
x_inner = self.proj_up(x) #全连接层,输出第三维尺寸*2
x_mlstm, z = torch.split(x_inner, split_size_or_sections=self.config._inner_embedding_dim, dim=-1)
#
# mlstm branch
x_mlstm_conv = self.conv1d(x_mlstm)
x_mlstm_conv_act = self.conv_act_fn(x_mlstm_conv)

q = self.q_proj(x_mlstm_conv_act)
k = self.k_proj(x_mlstm_conv_act)
v = self.v_proj(x_mlstm)

h_tilde_state = self.mlstm_cell(q=q, k=k, v=v)

h_tilde_state_skip = h_tilde_state + (self.learnable_skip * x_mlstm_conv_act)

# output / z branch
h_state = h_tilde_state_skip * self.ogate_act_fn(z)

# down-projection
y = self.dropout(self.proj_down(h_state))
return y

def step(
self,
x: torch.Tensor,
mlstm_state: tuple[torch.Tensor, torch.Tensor, torch.Tensor] = None,
conv_state: tuple[torch.Tensor] = None,
) -> tuple[torch.Tensor, dict[str, tuple[torch.Tensor, ...]]]:
# B, S, _ = x.shape

# up-projection
x_inner = self.proj_up(x)
x_mlstm, z = torch.split(x_inner, split_size_or_sections=self.config._inner_embedding_dim, dim=-1)

# mlstm branch
x_mlstm_conv, conv_state = self.conv1d.step(x_mlstm, conv_state=conv_state)
x_mlstm_conv_act = self.conv_act_fn(x_mlstm_conv)

q = self.q_proj(x_mlstm_conv_act)
k = self.k_proj(x_mlstm_conv_act)
v = self.v_proj(x_mlstm)

h_tilde_state, mlstm_state = self.mlstm_cell.step(q=q, k=k, v=v, mlstm_state=mlstm_state)

h_tilde_state_skip = h_tilde_state + (self.learnable_skip * x_mlstm_conv_act)

# output / z branch
h_state = h_tilde_state_skip * self.ogate_act_fn(z)

# down-projection
y = self.dropout(self.proj_down(h_state))
return y, {"mlstm_state": mlstm_state, "conv_state": conv_state}

def reset_parameters(self):
# init inproj
small_init_init_(self.proj_up.weight, dim=self.config.embedding_dim)
if self.proj_up.bias is not None:
nn.init.zeros_(self.proj_up.bias)
# init outproj
wang_init_(self.proj_down.weight, dim=self.config.embedding_dim, num_blocks=self.config._num_blocks)
if self.proj_down.bias is not None:
nn.init.zeros_(self.proj_down.bias)

nn.init.ones_(self.learnable_skip)

def _init_qkv_proj(qkv_proj: LinearHeadwiseExpand):
# use the embedding dim instead of the inner embedding dim
small_init_init_(qkv_proj.weight, dim=self.config.embedding_dim)
if qkv_proj.bias is not None:
nn.init.zeros_(qkv_proj.bias)

_init_qkv_proj(self.q_proj)
_init_qkv_proj(self.k_proj)
_init_qkv_proj(self.v_proj)

self.mlstm_cell.reset_parameters()

激活函数

为什么要使用激活函数

如果没有激活函数,神经网络中将只存在大量的权重*输入(还有偏置)的线性叠加,无法拟合出非线性函数的效果。当引入非线性的激活函数后,神经网络便可以逼近任何线性、非线性函数

常见激活函数

sigmoid函数

指一类S型函数

  • 优点:logistic函数可以将输出归一到(0,1),而Tanh函数可以处理负数输入,因此Tanh会更常用一些
  • 缺点:在两个饱和处,其斜率变化较缓,导致w的变化对loss的影响较小,最终导致w的更新变慢(梯度消失)(我感觉这样说好理解一点)

ReLU函数

引入了负数全部置0的非线性元素,且其正部本身为线性,斜率为1,不会出现梯度消失问题

缺点:但如果参数在一次不恰当的更新后,导致某一个层输入到某个神经元的值为0时,该神经元输出到后面所有神经元的值全为0,此时该神经元的w无论怎么变化,输出到下一层所有神经元的值都为0(梯度为0),导致该神经元前的参数都无法更新,出现dead relu

反向传播(BP)

参数学习

以交叉熵损失函数为例,假设y为one-hot向量(仅标签位置为1,其他0),则可以延伸至多分类问题,且其输出仅与标签位置处的y^相关,y^越小,损失函数越大(y^<1)

由于y^是W(权重矩阵)和偏置的函数,因此Loss函数也为W和b的函数,通过优化迭代他们的值即可接近Loss的最小值,而要降低Loss,通常是借助各w的梯度实现

反向传播算法

当一次正向传播后,Loss值已经确定,此时Loss对所有w的偏导都已确定,因此计算出这些偏导即可完成一次梯度下降。如果从开头通过链式法则对所有w逐一计算效率较低。但由链式法则又可得:

loss对某一w的偏导可通过下一层的所有w偏导计算而得,因此从输出层对w偏导进行反向计算效率更高

注意力机制

向量点乘的几何意义

向量x在向量y方向上的投影再与向量y的乘积,能够反应两个向量的相似度。向量点乘结果越大,两个向量越相似。

作用

注意力机制通过计算输入序列中每个位置(时间步)的权重,来决定每个时间步的重要性。权重越高,说明该时间步包含的信息越关键,模型就会更“关注”该部分。

与之相比,卷积只能使输入局部相关,而全连接层则计算量大,注意力机制则可以根据整个输入序列实时调整权重