前言
文章概述《深度学习详解》- 6
书中介绍了自注意力模型(Self-Attention Model, SAM)及其在深度学习中的应用,着重讨论了其在处理序列数据时的优势和机制。文章首先区分了深度学习模型中常见的三种类型的问题:输入与输出数量相同的任务、输入是一个序列但输出是一个标签的分类问题、以及输入和输出都是一系列标签的序列到序列问题。随后详细阐述了SAM如何应用于解决序列标注问题,尤其是通过对输入序列中的每个元素分配一个标签的任务。文中介绍了自注意力模型的基本运作原理,包括如何利用查询(query)、键(key)和值(value)来捕捉输入序列中元素之间的依赖关系,以及如何通过softmax函数进行权重分配以强调不同元素间的相对重要性。此外,文章还扩展讨论了多头自注意力(Multi-Head Self-Attention, MHSA)机制,指出其能够同时探索输入序列的不同方面,从而提高模型性能。通过引入这一进阶版本,文章强调了多头自注意力在处理自然语言处理和计算机视觉等领域复杂序列数据时的重要性。总而言之,本文深入探讨了自注意力模型的核心概念、实现方式及其在现代深度学习模型中的关键作用。
个人学习笔记以及概念梳理,望对大家有所帮助。
思维导图6
自注意力(Self-Attention)模型
关键组成部分:
查询(Query) - 通常表示为向量 Q,用来询问关于输入序列的信息。
键(Key) - 表示为向量 K,用于和查询进行匹配,以确定注意力权重。
值(Value) - 表示为向量 V,根据注意力权重加权求和得到输出。
特点:
并行性 - Transformer模型利用自注意力机制可以并行处理整个输入序列,这与需要逐个处理序列元素的循环神经网络(RNNs)形成对比。
长距离依赖 - 自注意力机制能够有效地捕获输入序列中的长距离依赖关系。
多头注意力 - 为了捕捉不同子空间中的信息,可以在同一层中使用多个并行的自注意力层,这被称为多头注意力机制。
补充
涉及的一些术语
术语 | 解释 |
向量 | 由一系列数值组成的列表,用于表示数据点的特征或属性。 |
卷积神经网络 (Convolutional Neural Network, CNN) | 一种专门设计用于处理具有网格结构的数据(如图像)的深度学习模型。 |
自注意力模型 (Self-Attention Model) | 一种能够使模型关注输入序列中不同部分的相对重要性的机制,特别适合处理序列数据。 |
输入序列 (Input Sequence) | 一系列连续的数据点或向量,这些数据点通常具有某种顺序关系。 |
独热编码 (One-Hot Encoding) | 一种将分类变量转换为二进制向量的方法,其中每个类别对应向量中的一个位置。 |
词嵌入 (Word Embedding) | 一种将单词映射到低维向量空间的技术,使得相似意义的词在向量空间中距离较近。 |
语音信号 (Speech Signal) | 人类说话时产生的声波,可以通过将其切分成一系列向量来表示。 |
帧 (Frame) | 语音信号处理中的一小段固定时间长度的音频样本。 |
窗口 (Window) | 在信号处理中,用于提取信号片段的时间间隔。 |
社交网络 (Social Network) | 由个体(节点)及它们之间的关系(边)构成的图形结构。 |
图 (Graph) | 由节点(顶点)和边组成的数学结构,用来表示实体间的连接关系。 |
序列标注 (Sequence Labeling) | 为序列中的每个元素分配一个标签的任务,例如词性标注。 |
词性标注 (Part-of-Speech Tagging, POS Tagging) | 为句子中的每个单词标记其语法功能(如名词、动词等)的任务。 |
情感分析 (Sentiment Analysis) | 评估文本中表达的情感倾向(如正面或负面情绪)的任务。 |
序列到序列 (Sequence to Sequence, Seq2Seq) | 一种模型架构,用于处理输入和输出均为序列的任务,如机器翻译。 |
全连接网络 (Fully Connected Network, FCN) | 每个神经元与下一层所有神经元相连的神经网络结构。 |
学习过程遇到的一些问题的理解:
1理解多头注意力
首先,注意力机制:注意力机制允许模型关注输入序列中最重要的部分,这对于理解句子中的依赖关系非常有用。
而多头:通过将注意力分成多个“头”(head),每个头可以独立地关注输入的不同部分或特征,从而增强模型捕捉不同位置关系的能力。
优点
并行性:多头注意力可以在GPU上高效并行计算,这使得Transformer能够比传统的RNN更快地训练。
多粒度注意力:不同的头可以关注输入的不同方面,比如长距离依赖和短距离依赖,或者不同的语法结构等。
应用
多头注意力广泛应用于机器翻译、文本生成、问答系统等NLP任务中。
补充
代码运行
实现功能
将长度为4秒的语音分类为5种:"car" "human" "barking" "music" "others"。
同时输出每一种分类的精度。
对于此声音分类的进一步试验
先前操作可见,
入门(选修):《深度学习详解》(Datawhale X 李宏毅苹果书 AI夏令营)-CSDN博客
Transformer架构
这里改变原有的模型结构,引入Transformer架构,来看看分类效果
代码如下
import pandas as pd
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.model_selection import train_test_split
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.metrics import ConfusionMatrixDisplay
import matplotlib.pyplot as plt
# 加载数据集
dataset_path = r'.\datasets\4s\mfcc_dataset.csv'
data = pd.read_csv(dataset_path)
# 特征和标签分离
X = data.drop(['filename', 'class'], axis=1)
y = data['class']
# 标签编码
label_encoder = LabelEncoder()
y_encoded = label_encoder.fit_transform(y)
# 特征标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y_encoded, test_size=0.2, random_state=42)
# 创建 PyTorch 数据集
class MFCCDataset(Dataset):
def __init__(self, X, y):
self.X = torch.tensor(X, dtype=torch.float32)
self.y = torch.tensor(y, dtype=torch.long)
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
train_dataset = MFCCDataset(X_train, y_train)
test_dataset = MFCCDataset(X_test, y_test)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
# 自定义 TransformerEncoderLayer
class CustomTransformerEncoderLayer(nn.Module):
def __init__(self, d_model, nhead):
super(CustomTransformerEncoderLayer, self).__init__()
self.self_attn = nn.MultiheadAttention(embed_dim=d_model, num_heads=nhead) # 将 nhead 设置为 1
self.linear1 = nn.Linear(d_model, d_model * 4)
self.dropout = nn.Dropout(0.1)
self.linear2 = nn.Linear(d_model * 4, d_model)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(0.1)
self.dropout2 = nn.Dropout(0.1)
self.activation = nn.ReLU()
def forward(self, src, src_mask=None, src_key_padding_mask=None):
src2 = self.self_attn(src, src, src, attn_mask=src_mask,
key_padding_mask=src_key_padding_mask)[0]
src = src + self.dropout1(src2)
src = self.norm1(src)
src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))
src = src + self.dropout2(src2)
src = self.norm2(src)
return src
# 定义 Transformer 模型
class TransformerClassifier(nn.Module):
def __init__(self, input_dim, num_classes):
super(TransformerClassifier, self).__init__()
self.encoder_layer = CustomTransformerEncoderLayer(input_dim, nhead=1) # 将 nhead 设置为 1
self.fc = nn.Linear(input_dim, num_classes)
def forward(self, x):
x = self.encoder_layer(x)
x = self.fc(x) # 直接将 x 传递给分类器
return x
model = TransformerClassifier(input_dim=X.shape[1], num_classes=len(label_encoder.classes_))
# 编译模型
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 训练模型
num_epochs = 50
for epoch in range(num_epochs):
for batch in train_loader:
inputs, labels = batch
optimizer.zero_grad()
outputs = model(inputs) # 注意这里没有使用 permute
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 测试模型
model.eval()
with torch.no_grad():
y_true = []
y_pred = []
for batch in test_loader:
inputs, labels = batch
outputs = model(inputs) # 注意这里没有使用 permute
_, predicted = torch.max(outputs.data, 1)
y_true.extend(labels.tolist())
y_pred.extend(predicted.tolist())
# 计算精度和召回率
report = classification_report(y_true, y_pred, target_names=label_encoder.classes_)
print("Classification Report:")
print(report)
Conformer
尝试构造transformer的变体:Conformer。
代码如下
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import classification_report
import numpy as np
# 定义ConformerBlock
class ConformerBlock(nn.Module):
def __init__(self, d_model, n_head, kernel_size, dropout=0.1):
super(ConformerBlock, self).__init__()
self.norm1 = nn.LayerNorm(d_model)
self.self_attn = nn.MultiheadAttention(d_model, n_head, dropout=dropout)
self.norm2 = nn.LayerNorm(d_model)
self.conv_module = nn.Sequential(
nn.Conv1d(d_model, 2 * d_model, kernel_size=1),
nn.GLU(dim=1),
nn.Conv1d(d_model, d_model, kernel_size, padding=(kernel_size - 1) // 2, groups=16),
nn.BatchNorm1d(d_model),
nn.SiLU(),
nn.Conv1d(d_model, d_model, kernel_size=1),
nn.Dropout(dropout)
)
self.norm3 = nn.LayerNorm(d_model)
self.feed_forward = nn.Sequential(
nn.Linear(d_model, 4 * d_model),
nn.SiLU(),
nn.Linear(4 * d_model, d_model),
nn.Dropout(dropout)
)
def forward(self, x, mask=None):
x = self.norm1(x + self.self_attn(x, x, x, attn_mask=mask)[0])
# 确保x的形状为 (batch_size, sequence_length, d_model)
# 将最后一个维度作为通道维度传递给卷积层
# 如果x的形状不是 (batch_size, sequence_length, d_model),则需要调整
if x.dim() == 2:
x = x.unsqueeze(1) # 添加sequence_length维度
elif x.dim() == 3:
pass # 形状已经正确
else:
raise ValueError("Unexpected tensor shape")
conv_x = self.conv_module(x.transpose(1, 2)).transpose(1, 2)
x = self.norm2(x + conv_x)
x = self.norm3(x + self.feed_forward(x))
return x
# 定义ConformerClassifier
class ConformerClassifier(nn.Module):
def __init__(self, input_dim, d_model, n_head, kernel_size, num_layers, num_classes, dropout=0.1):
super(ConformerClassifier, self).__init__()
self.input_projection = nn.Linear(input_dim, d_model)
self.conformer_blocks = nn.ModuleList([ConformerBlock(d_model, n_head, kernel_size, dropout) for _ in range(num_layers)])
self.classifier = nn.Sequential(
nn.LayerNorm(d_model),
nn.Linear(d_model, num_classes)
)
def forward(self, x):
x = self.input_projection(x)
for block in self.conformer_blocks:
x = block(x)
x = x.mean(dim=1)
return self.classifier(x)
# 数据集定义
class MFCCDataset(Dataset):
def __init__(self, X, y):
self.X = X
self.y = y
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
return torch.tensor(self.X[idx], dtype=torch.float32), torch.tensor(self.y[idx], dtype=torch.long)
# 加载数据集
dataset_path = r'.\datasets\4s\mfcc_dataset.csv'
data = pd.read_csv(dataset_path)
# 数据预处理
le = LabelEncoder()
data['class'] = le.fit_transform(data['class'])
y = data['class'].values
X = data.drop(['filename', 'class'], axis=1).values
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 创建数据集
train_dataset = MFCCDataset(X_train, y_train)
test_dataset = MFCCDataset(X_test, y_test)
# 数据加载器
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
# 模型参数
input_dim = X.shape[1]
d_model = 256
n_head = 4
kernel_size = 31
num_layers = 6
num_classes = len(le.classes_)
dropout = 0.1
# 初始化模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = ConformerClassifier(input_dim, d_model, n_head, kernel_size, num_layers, num_classes, dropout).to(device)
# 损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练循环
num_epochs = 50
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
for i, (inputs, labels) in enumerate(train_loader):
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
#print(f"Epoch {epoch+1}/{num_epochs}, Loss: {running_loss/len(train_loader):.4f}")
# 评估模型
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for inputs, labels in test_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
print(classification_report(all_labels, all_preds, target_names=le.classes_))
Transformer架构 + Self-Attention Pooling
进一步尝试,Self-Attention Pooling (自我注意力池化)加入后的分类效果
代码如下
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import pandas as pd
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.model_selection import train_test_split
import numpy as np
from sklearn.metrics import classification_report
# 加载数据集
dataset_path = r'.\datasets\4s\mfcc_dataset.csv'
data = pd.read_csv(dataset_path)
# 特征和标签分离
X = data.drop(['filename', 'class'], axis=1).values
y = data['class'].values
# 标准化特征
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 将类别标签转换为数值编码
label_encoder = LabelEncoder()
y_encoded = label_encoder.fit_transform(y)
# 将标签转换为 one-hot 编码
num_classes = len(np.unique(y))
y_one_hot = np.eye(num_classes)[y_encoded]
# 分割数据集
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y_one_hot, test_size=0.2, random_state=42)
# 定义 PyTorch 数据集类
class MFCCDataset(Dataset):
def __init__(self, features, labels):
self.features = torch.tensor(features, dtype=torch.float32)
self.labels = torch.tensor(labels, dtype=torch.float32)
def __len__(self):
return len(self.features)
def __getitem__(self, idx):
return self.features[idx], self.labels[idx]
# 创建数据加载器
train_dataset = MFCCDataset(X_train, y_train)
test_dataset = MFCCDataset(X_test, y_test)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
# 定义 Self-Attention Pooling
class SelfAttentionPooling(nn.Module):
def __init__(self, input_dim):
super(SelfAttentionPooling, self).__init__()
self.W = nn.Linear(input_dim, 1)
def forward(self, x, lengths=None):
# x shape: (batch_size, sequence_length, input_dim)
attn_weights = torch.tanh(self.W(x)).squeeze(-1) # (batch_size, sequence_length)
attn_weights = torch.softmax(attn_weights, dim=-1).unsqueeze(-1) # (batch_size, sequence_length, 1)
pooled_x = torch.sum(x * attn_weights, dim=1) # (batch_size, input_dim)
return pooled_x
# 定义 Transformer 编码器
class TransformerEncoder(nn.Module):
def __init__(self, input_dim, num_heads, dim_feedforward, dropout=0.1):
super(TransformerEncoder, self).__init__()
# 注意这里的 num_heads 被设置为 1,以避免嵌入维度不可整除的问题
self.self_attn = nn.MultiheadAttention(input_dim, num_heads, dropout=dropout)
self.linear1 = nn.Linear(input_dim, dim_feedforward)
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(dim_feedforward, input_dim)
self.norm1 = nn.LayerNorm(input_dim)
self.norm2 = nn.LayerNorm(input_dim)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
self.activation = nn.ReLU()
def forward(self, src, src_mask=None, src_key_padding_mask=None):
src2 = self.self_attn(src, src, src, attn_mask=src_mask,
key_padding_mask=src_key_padding_mask)[0]
src = src + self.dropout1(src2)
src = self.norm1(src)
src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))
src = src + self.dropout2(src2)
src = self.norm2(src)
return src
# 定义完整模型
class AudioClassifier(nn.Module):
def __init__(self, input_dim, num_heads, dim_feedforward, num_classes):
super(AudioClassifier, self).__init__()
# 注意这里的 num_heads 被设置为 1
self.encoder = TransformerEncoder(input_dim, num_heads, dim_feedforward)
self.pooling = SelfAttentionPooling(input_dim)
self.classifier = nn.Linear(input_dim, num_classes)
# 在模型的前向传播方法中
def forward(self, x):
# 将每个样本视为长度为 1 的序列
x = x.unsqueeze(1) # (batch_size, 1, input_dim)
x = self.encoder(x.permute(1, 0, 2)) # (sequence_length, batch_size, input_dim)
x = self.pooling(x.permute(1, 0, 2)) # (batch_size, input_dim)
x = self.classifier(x)
return x
# 创建模型实例
input_dim = X_train.shape[1] #这里有 13 个 MFCC 特征
num_heads = 1 # 调整 num_heads 为 1
dim_feedforward = 256
num_classes = num_classes
model = AudioClassifier(input_dim, num_heads, dim_feedforward, num_classes)
# 设置损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练模型
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
# 在训练循环中
for epoch in range(50): # 迭代次数可以根据实际情况调整
model.train()
for batch_features, batch_labels in train_loader:
batch_features, batch_labels = batch_features.to(device), batch_labels.to(device)
optimizer.zero_grad()
outputs = model(batch_features)
loss = criterion(outputs, batch_labels.argmax(dim=1))
loss.backward()
optimizer.step()
# 评估模型
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for batch_features, batch_labels in test_loader:
batch_features, batch_labels = batch_features.to(device), batch_labels.to(device)
outputs = model(batch_features)
_, predicted = torch.max(outputs.data, 1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(batch_labels.argmax(dim=1).cpu().numpy())
# 将预测结果和真实标签转换为 NumPy 数组
all_preds = np.array(all_preds)
all_labels = np.array(all_labels)
# 打印分类报告
print(classification_report(all_labels, all_preds, target_names=label_encoder.classes_))
比较(4种) 其中num_epochs = 50
原先使用CNN | 使用Transformer |
使用Conformer | Transformer+Self-Attention Pooling |
可得
CNN:从报告可以看出,该模型在所有类别的精度、召回率和 F1 分数方面表现良好,整体准确率为 89%。可能是这个分类任务并不需要太复杂的模型。
Transformer:虽然该模型在某些类别的精度和 F1 分数较高,但总体准确率较低(93%)。这可能表明该模型在某些特定类别的识别上存在困难,导致整体性能下降。
Conformer:该模型在所有类别的性能都较差,尤其是 barking 类别。这可能是由于模型没有正确地捕获音频特征或训练不足造成的。整体准确率仅为 48%,说明该模型在识别音频事件时的表现不佳。
同时,Transformer+Self-Attention Pooling:该模型在所有类别的性能均优于单独使用 Transformer 模型,并且整体准确率达到 93%。这表明 Self-Attention Pooling 层有助于提高模型的泛化能力,特别是在处理长序列数据时。
补充(ChatGBT)
小结(实验)
实验与分析
- 实验要求:对长度为4秒的语音进行分类,类别为"car" "human" "barking" "music" "others"。
- 模型:
- Transformer模型:采用自定义的TransformerEncoderLayer,包含单头或多头注意力机制。
- Conformer:Transformer的一种变体,结合了卷积模块,理论上能够更好地处理时序数据。
- Transformer + Self-Attention Pooling:在Transformer基础上增加自我注意力池化层,以提高模型对长序列数据的处理能力。
结果分析
- CNN模型:表现出较好的性能,尤其是在精度和F1分数方面,整体准确率为89%。
- Transformer模型:在某些类别上的精度和F1分数较高,但总体准确率略低于CNN模型(93%)。
- Conformer模型:在所有类别的性能较差,整体准确率仅为48%,可能存在训练不足或其他配置问题。
- Transformer + Self-Attention Pooling:整体准确率达到93%,表明自我注意力池化层有助于提高模型的泛化能力。
可得
- CNN模型在简单的分类任务中表现优异。
- Transformer模型在处理序列数据时展现出强大的潜力,特别是结合自我注意力池化后。