本篇探究基于神经网络进行数据降维的可能性。试验算法为AutoEncoder和Transformer。

AutoEncoder

简介

参考文献:

https://tensorflow.google.cn/tutorials/generative/autoencoder

https://www.ibm.com/topics/autoencoder

自编码器(AutoEncoder, AE)是一种特殊类型的无监督神经网络,设计目的是高效地将输入数据压缩(encode)到其基本特征,然后从这种压缩的表示中重建(decode)原始输入数据。

自编码器被训练来发现输入数据的潜在变量。这些变量是隐藏或随机的变量,尽管无法直接观察到,但它们在根本上决定了数据的分布方式。例如,给定一个手写数字的图像,自编码器首先将图像编码为低维的潜在表示,然后将该潜在表示解码回图像。自编码器学习这一代表原始输入中包含的最重要信息的压缩数据(称作潜在空间,latent space),最大程度地减少重构误差。

从直观上来看,自编码器可以用于特征降维,类似主成分分析PCA,但是相比PCA性能更强,因为神经网络模型可以提取更有效的新特征。

创建一个AutoEncoder

使用pytorch创建一个这样的自编码器类AutoEncoder,其编码器(Encoder)架构为:

  • 输入层:接受一个展平后的8x8的输入(64个特征);
  • 隐藏层1:输入通过一个线性层,转化为100维度;
  • 隐藏层2:输入通过另一个线性层,转化为81维度;
  • 瓶颈层:最后一个线性层,将输入转化为3维度的表示;
  • 激活函数:使用LeakyReLU
  • 批归一化:使用BatchNorm1d

且其解码器(Decoder)的架构和编码器完全倒置。

Python
import torch.nn as nn


class AutoEncoder(nn.Module):
    def __init__(self):
        super(AutoEncoder, self).__init__()
        # 编码器
        self.encoder = nn.Sequential(
            nn.Linear(8 * 8, 100),
            nn.LeakyReLU(True),  # f(x) = max{alpha * x, x}
            nn.BatchNorm1d(100),  # normalization in fully connected layer
            nn.Linear(100, 81),
            nn.LeakyReLU(True),
            nn.BatchNorm1d(81),
            nn.Linear(81, 3)
        )
        # 解码器
        self.decoder = nn.Sequential(
            nn.Linear(3, 81),
            nn.LeakyReLU(True),
            nn.BatchNorm1d(81),
            nn.Linear(81, 100),
            nn.LeakyReLU(True),
            nn.BatchNorm1d(100),
            nn.Linear(100, 8 * 8),
            nn.Sigmoid()
        )
    
    # 前向传播
    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x

这样设计的自编码器可以用于MNIST手写数据集的特征提取,该数据集在前一节中出现过,不过之前是降至2维,本篇是降至3维。

模型训练

引用该AutoEncoder类进行训练:

Python
from AutoEncoder import AutoEncoder
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
from sklearn.datasets import load_digits
from sklearn.preprocessing import StandardScaler

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

digits = load_digits()
data = digits.data
targets = digits.target

scaler = StandardScaler()
data = scaler.fit_transform(data)  # 标准化

data_tensor = torch.tensor(data, dtype=torch.float32).to(device)
targets_tensor = torch.tensor(targets, dtype=torch.int64).to(device)

dataset = TensorDataset(data_tensor, targets_tensor)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

model = AutoEncoder().to(device)
criterion = nn.MSELoss()  # 误差考察对象:均方差
optimizer = optim.AdamW(model.parameters(), lr=0.001)  # 优化器:AdamW

num_epochs = 30

for epoch in range(num_epochs):
    for data_batch, _ in dataloader:
        # 前向传播
        output = model(data_batch)
        loss = criterion(output, data_batch)
        # 后向传播和优化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    if (epoch + 1) % 5 == 0:
        print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')

with torch.no_grad():
    data_encoded = model.encoder(data_tensor).cpu().numpy()

降维结果可视化:

Python
from pyecharts import options as opts
from pyecharts.charts import Scatter3D

colors = ['#FADADD', '#F7E7CE', '#FFB7C5', '#FFCBA4', '#FFF1C9', '#D2EBD8', '#FFFACD', '#FFDAB9', '#E6E6FA', '#AFEEEE']

scatter3D = Scatter3D()
for i in range(10):
    category_data = data_encoded[targets == i]
    scatter3D.add(
        series_name=f'{i}',
        data=category_data.tolist(),
        itemstyle_opts=opts.ItemStyleOpts(color=colors[i])
    )

scatter3D.render('digit_ae_3d.html')


Transformer

简介

Transformer模型是一种基于注意力机制的深度学习模型,主要用于自然语言处理(NLP)任务,如机器翻译、文本生成等。与传统的递归神经网络(RNN)和卷积神经网络(CNN)不同,它通过并行处理输入数据,大幅提升了训练和推理的效率。

所以说……比起试验效果上的考量,我更多的是想蹭一下Transformer的热度🤗
理解Transformer模型对于你更好地了解现代技术确实很有帮助哦!

Transformer模型的关键组件如下。

1 输入嵌入(Input Embedding

输入文本首先被转化为向量表示,这个过程叫做嵌入。每个单词或字符通过查找嵌入矩阵被转化为固定长度的向量。

2 位置编码(Positional Encoding

由于Transformer模型不具备递归结构,它无法隐式地获得序列位置的信息。因此,需要加入位置编码来显式地表示序列中每个位置的信息。位置编码是通过正弦和余弦函数计算得到的,这些编码与输入嵌入相加,使模型能够区分序列中的不同位置。

多头自注意力机制 (Multi-Head Self-Attention)

自注意力机制通过计算输入序列中每个位置的表示与所有其他位置的表示之间的相关性来捕获全局依赖关系。多头机制则是将这种操作并行化,使模型能够关注序列中的不同部分。

计算过程:

  • 查询 (Query)键 (Key) 和值 (Value) 向量的计算:从输入嵌入中通过线性变换得到查询、键和值向量。
  • 注意力权重的计算:通过点积计算查询和键向量的相似度,然后通过Softmax函数转化为权重。
  • 加权求和:使用这些权重对值向量进行加权求和,得到自注意力的输出。

4 前馈神经网络 (Feed-Forward Neural Network)

每个自注意力层后接一个前馈神经网络,对每个位置的表示进行独立的线性变换和非线性激活。这个过程增强了模型的表达能力。

5 残差连接和层规范化 (Residual Connection and Layer Normalization)

为了更好地传播梯度并加快训练速度,每个子层(自注意力和前馈神经网络)后都加了残差连接,并进行层规范化。这样可以缓解梯度消失和梯度爆炸问题。

6 编码器-解码器结构 (Encoder-Decoder Architecture)

Transformer模型通常由一个编码器和一个解码器组成:

  • 编码器 (Encoder):由N个相同的编码层堆叠而成,每个编码层由一个自注意力子层和一个前馈神经网络子层组成。
  • 解码器 (Decoder):结构与编码器类似,但每个解码层多了一个编码器-解码器注意力子层,用来结合编码器生成的信息。
嗯……本篇的数据降维只会用到Encoder。

整个模型的输入通过编码器和解码器的多层处理后,最终生成输出序列。Transformer模型的并行计算能力和长程依赖捕获能力,使其在许多NLP任务中表现出色。

此外,基于Transformer的模型如BERT(Bidirectional Encoder Representations from Transformers)和GPT(Generative Pre-trained Transformer)在各种NLP任务中取得了显著的成果。这些模型分别擅长于理解任务和生成任务,进一步展示了Transformer架构的强大适应性和应用广泛性。

创建一个TransformerEncoder

同样地,创建一个用于MNIST手写数据集特征提取的SimpleTransformerEncoder类。

为了更好地使Transformer的特性为数据降维服务,需要着重考虑以下三点:

  • 利用位置编码来增强模型的表现。虽然digit数据集本身是一个固定长度的特征向量,但位置编码仍然可以在处理序列数据时提供有益的信息,帮助模型理解位置关系。
  • 注意符合输入要求。Transformer的输入维度一般为 [seq_len, batch_size, embed_dim](输入序列长度、一次处理样本数、每个输入元素的特征维度),这与其设计原理和自注意力机制密切相关。本案例的每个样本是一个64维的向量,这并不是一个自然的“序列”数据。因此需要进行调整。
  • 利用Dropout防止过拟合,通过随机丢弃一部分神经元来增强模型的泛化能力。这在小数据集(如digit数据集)训练时尤为重要。
Python
import torch  
import torch.nn as nn  
import math

class SimpleTransformerEncoder(nn.Module):
    def __init__(self, input_dim, output_dim, nhead=4, num_layers=4):
        super(SimpleTransformerEncoder, self).__init__()
        self.positional_encoding = PositionalEncoding(input_dim, dropout=0.1)
        self.encoder_layer = nn.TransformerEncoderLayer(d_model=input_dim, nhead=nhead)
        self.transformer_encoder = nn.TransformerEncoder(self.encoder_layer, num_layers=num_layers)
        self.linear = nn.Linear(input_dim, output_dim)
        self.dropout = nn.Dropout(0.1)  # Dropout层

    def forward(self, x):
        x = x.unsqueeze(1)  # 增加一个维度以符合Transformer的输入要求  
        x = self.positional_encoding(x)
        x = self.transformer_encoder(x)
        x = x.squeeze(1)  # 移除多余的维度  
        x = self.dropout(x)  # 使用Dropout  
        x = self.linear(x)
        return x

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0).transpose(0, 1)
        self.register_buffer('pe', pe)

    def forward(self, x):
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)

模型训练

引用该SimpleTransformerEncoder类进行训练:

Python
from TransformerEncoder import SimpleTransformerEncoder
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn import datasets
from sklearn.preprocessing import StandardScaler

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

digits = datasets.load_digits()
X = digits.data  # 64维特征
y = digits.target

scaler = StandardScaler()
X = scaler.fit_transform(X)

X = torch.tensor(X, dtype=torch.float32).to(device)

model = SimpleTransformerEncoder(input_dim=64, output_dim=3).to(device)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)  # 学习率调度器

num_epochs = 50
batch_size = 64
for epoch in range(num_epochs):
    model.train()
    permutation = torch.randperm(X.size()[0])
    for i in range(0, X.size()[0], batch_size):
        indices = permutation[i:i + batch_size]
        batch_x = X[indices]
        optimizer.zero_grad()
        outputs = model(batch_x)
        loss = criterion(outputs, batch_x[:, :3])  # 降至3维
        loss.backward()
        optimizer.step()
    scheduler.step()  # 更新学习率

    if (epoch + 1) % 5 == 0:
        print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')

# 模型评估
model.eval()
with torch.no_grad():
    reduced_X = model(X).cpu().numpy()

降维结果可视化,思路和AutoEncoder大同小异:



嗯……就是效果嘛……