简单介绍
自编码器是一种 神经网络,它的作用是 自动学习数据的压缩表示(也叫编码),然后再把它
还原回去。听起来有点像“数据复印机”,但它的真正价值在于能够 提取数据中的关键特征。
你可以把它想象成:
一个会画画的机器人:它看了一张图,记住其中最重要的部分,然后再凭记忆把它画出来
原理介绍
结构
自编码器将复杂的数据,转换成更简单、更有效的表示形式。(称作潜在空间)。自编码器
构了Unet等网络的骨干。自编码器就是学习数据有效表示特征的神经网络,模型尽可能用比较少的
特征描述比较大的数据。
自编码器的架构主要有3个组成部分。编码器,潜在空间,解码器。
编码器将数据压缩成潜在空间表示。这个潜在空间是低纬空间,捕捉输入数据的基本特征,瓶
颈层保存这个压缩表示。最后解码器从压缩表示中重建数据。自编码器不仅可以压缩这种简单的数
据,还可以压缩更高纬的数据(比如表格数据)或者图片。
训练这样一个网络的核心点在于最小化生成数据和原始数据的差异,目标是提高编码器从
原始数据提取特征的能力,保留关键信息和提高解码器从关键信息中重建数据的能力。
如何测量两张图片之间的差异?
可以逐个像素进行比较,例如可以计算对应像素的差异,然后取这些差异的平均值,这种差异就是
MSE损失函数。
为什么要使用这样的模型?
自编码器的优点是能够执行数据的降维,潜在空间的维度由瓶颈层神经元的数量决定,如果瓶
颈层有两个神经元,那么潜在空间就是二维的,我们可以把每个神经元的输出是为平面的方向进行
可视化每个数据点是如何编码。如果编码器训练的效果非常好,相同数字都会进入潜在空间的相同
区域。在训练的过程中,每个类别会占据自己的独特的区域。
自编码器设计中的一个重要选择,就是潜在空间的维度(潜在维度),也就是瓶颈层神经元的
数量是网络中最重要的部分。潜在空间太小自编码器可能难以识别数据的基本特征。
我们为啥这么关心重建的质量?
实际上重建质量差也就是潜在空间差
应用:
假设在一家繁忙的医院,每天接待很多的患者,需要进行数十万次的核磁共振的检查。但是他们
的性别数据丢失了。我们可以训练一个自编码器,根据脑部的成像来识别患者的性别。自编码器将
在更低的维度表示脑部的成像,这个时候我们更容易识别患者的性别。
手算模拟
现在我们用一个4 维输入来手动计算前向传播的过程,首先我们可以定义一个简单的自编码器
网络结构。网络接收 4 维的输入数据,通过编码器中的全连接层将高维数据压缩至 2 维潜在空
间,同时用 Tanh 激活函数将潜在向量输出范围约束在 (-1,1) 以增强特征区分度;接着,2 维潜在
向量被送入解码器,解码器再通过全连接层将低维向量恢复为 4 维数据,并借助 Sigmoid 激活函
数将重建结果限定在 [0,1] 区间以匹配原始输入的数值范围。
我们用一个4 维输入来手动计算前向传播的过程,首先我们可以定义一个简单的自编码器
网络结构。原始 2x2 输入数据 [[0.1, 0.8],[0.9, 0.2]] 被展平为 1x4 的向量 [0.1, 0.8, 0.9, 0.2],作为
模型输入;接着进入编码阶段,输入向量与 2x4 的编码器权重 [[0.2,-0.3,0.1,0.4],
[-0.1,0.5,-0.2,0.3]] 进行点积计算(无偏置),得到线性层输出 [-0.05, 0.27],再经过 Tanh 激活函
数处理,最终生成 2 维潜在向量 [-0.049958, 0.263625],完成高维到低维的压缩;随后进入解码
阶段,2 维潜在向量与 4x2 的解码器权重 [[0.3,-0.1],[0.2,0.4],[-0.3,0.2],[0.1,-0.5]] 再次进行无偏置
点积,得到线性层输出 [-0.04135, 0.095458, 0.067712, -0.136808],经 Sigmoid 激活函数将结果
约束在 [0,1] 区间,生成 1x4 的重建向量 [0.489664, 0.523846, 0.516922, 0.465851];最后将重建
向量重塑为 2x2 矩阵 [[0.4897, 0.5238],[0.5169, 0.4659]],完成从低维潜在向量到原始维度数据的
重建。
代码实现:
import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt import warnings # 解决中文显示和忽略警告 plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"] plt.rcParams["axes.unicode_minus"] = False warnings.filterwarnings("ignore") # 自定义自编码器(不使用偏置) class CustomAutoencoderNoBias(nn.Module): def __init__(self, input_dim=4, latent_dim=2): super(CustomAutoencoderNoBias, self).__init__() # 编码器线性层:设置bias=False,不使用偏置 self.encoder_linear = nn.Linear(input_dim, latent_dim, bias=False) self.encoder_activation = nn.Tanh() # 解码器线性层:设置bias=False,不使用偏置 self.decoder_linear = nn.Linear(latent_dim, input_dim, bias=False) self.decoder_activation = nn.Sigmoid() def forward(self, x): # 编码过程(无偏置) encoder_linear_out = self.encoder_linear(x) latent_vector = self.encoder_activation(encoder_linear_out) # 解码过程(无偏置) decoder_linear_out = self.decoder_linear(latent_vector) reconstructed_x = self.decoder_activation(decoder_linear_out) return encoder_linear_out, latent_vector, decoder_linear_out, reconstructed_x # 1. 准备输入数据 input_data_np = np.array([[0.1, 0.8], [0.9, 0.2]], dtype=np.float32) input_tensor = torch.from_numpy(input_data_np).flatten().unsqueeze(0) # 形状: (1,4) print("="*50) print("原始输入数据(2x2):") print(input_data_np) print(f"展平后的输入张量(1x4):\n{input_tensor.numpy()[0]}\n") # 2. 初始化模型(无偏置)并自定义权重 ae = CustomAutoencoderNoBias(input_dim=4, latent_dim=2) # 手动设置编码器权重(仅权重矩阵,无偏置) # 编码器权重: 2x4矩阵(输出维度x输入维度) ae.encoder_linear.weight.data = torch.tensor([ [0.2, -0.3, 0.1, 0.4], # 第一个潜在维度的权重 [-0.1, 0.5, -0.2, 0.3] # 第二个潜在维度的权重 ], dtype=torch.float32) # 手动设置解码器权重(仅权重矩阵,无偏置) # 解码器权重: 4x2矩阵(输出维度x输入维度) ae.decoder_linear.weight.data = torch.tensor([ [0.3, -0.1], # 第一个输出维度的权重 [0.2, 0.4], # 第二个输出维度的权重 [-0.3, 0.2], # 第三个输出维度的权重 [0.1, -0.5] # 第四个输出维度的权重 ], dtype=torch.float32) # 3. 打印自定义权重(无偏置) print("="*50) print("【无偏置设置】线性层已关闭偏置(bias=False)") print("编码器权重(2x4,无偏置):") print(ae.encoder_linear.weight.data.numpy()) print("\n解码器权重(4x2,无偏置):") print(ae.decoder_linear.weight.data.numpy()) # 4. 前向传播并打印每一步计算(无偏置) print("\n" + "="*50) print("前向传播计算过程(无偏置):") with torch.no_grad(): encoder_linear_out, latent_vector, decoder_linear_out, reconstructed_x = ae(input_tensor) # 编码过程计算(无偏置:仅输入·权重) print("\n【编码阶段(无偏置)】") input_flat = input_tensor.numpy()[0] encoder_weights = ae.encoder_linear.weight.data.numpy() # 手动计算线性层输出(无偏置:加权和 = 输入·权重) manual_encoder_linear = [] for i in range(2): sum_val = np.sum(input_flat * encoder_weights[i]) # 移除偏置项 manual_encoder_linear.append(round(sum_val, 6)) print(f"编码器线性层输出(仅加权和,无偏置):{manual_encoder_linear}") print(f"Tanh激活后(潜在向量):{latent_vector.numpy()[0].round(6)}") # Tanh(x) = (e^x - e^(-x))/(e^x + e^(-x)) # 解码过程计算(无偏置:仅潜在向量·权重) print("\n【解码阶段(无偏置)】") latent_np = latent_vector.numpy()[0] decoder_weights = ae.decoder_linear.weight.data.numpy() # 手动计算线性层输出(无偏置) manual_decoder_linear = [] for i in range(4): sum_val = np.sum(latent_np * decoder_weights[i]) # 移除偏置项 manual_decoder_linear.append(round(sum_val, 6)) print(f"解码器线性层输出(仅加权和,无偏置):{manual_decoder_linear}") print(f"Sigmoid激活后(重建结果):{reconstructed_x.numpy()[0].round(6)}") # Sigmoid(x) = 1/(1+e^(-x)) # 5. 整理结果 latent_output_np = latent_vector.squeeze().numpy().round(4) reconstructed_output_np = reconstructed_x.squeeze().numpy().round(4) reconstructed_image_np = reconstructed_output_np.reshape(2, 2) print("\n" + "="*50) print("最终结果(无偏置):") print(f"潜在向量 Z:{latent_output_np}") print(f"重建图像(2x2):\n{reconstructed_image_np}") # 6. 可视化(标注无偏置) fig, axes = plt.subplots(1, 3, figsize=(18, 6)) cmap = 'binary' # 原始输入 ax = axes[0] im = ax.imshow(input_data_np, cmap=cmap, vmin=0, vmax=1) ax.set_title('原始输入(2x2)', fontsize=14) ax.axis('off') cbar = fig.colorbar(im, ax=ax, orientation='vertical', fraction=0.046, pad=0.04) cbar.set_label('像素值', rotation=270, labelpad=15) # 潜在空间(标注无偏置) ax = axes[1] ax.scatter(latent_output_np[0], latent_output_np[1], c='red', s=300, marker='o', label='潜在向量 Z', edgecolors='black', linewidth=1.5) ax.text(latent_output_np[0] * 1.05, latent_output_np[1] * 1.05, f'Z = ({latent_output_np[0]:.2f}, {latent_output_np[1]:.2f})', fontsize=12, color='red', weight='bold') ax.text(-1.1, 1.0, '无偏置计算', fontsize=12, color='blue', weight='bold') # 标注无偏置 ax.set_xlim(-1.2, 1.2) ax.set_ylim(-1.2, 1.2) ax.grid(True, linestyle=':', alpha=0.7) ax.axhline(0, color='grey', linestyle='--', linewidth=1.0) ax.axvline(0, color='grey', linestyle='--', linewidth=1.0) ax.set_title('潜在空间(2D)', fontsize=14) ax.set_aspect('equal') # 重建输出 ax = axes[2] im = ax.imshow(reconstructed_image_np, cmap=cmap, vmin=0, vmax=1) ax.set_title('重建输出(2x2)', fontsize=14) ax.axis('off') cbar = fig.colorbar(im, ax=ax, orientation='vertical', fraction=0.046, pad=0.04) cbar.set_label('像素值', rotation=270, labelpad=15) plt.suptitle('自编码器前向传播完整过程(自定义权重·无偏置)', fontsize=16, weight='bold') plt.tight_layout(rect=[0, 0.03, 1, 0.95]) plt.show()
数字分类
训练
import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms # 补充缺失的数据集和变换模块 # -------------------------- # 1. 超参数与数据预处理配置 # -------------------------- EPOCH = 10 # 训练轮次 BATCH_SIZE = 64 # 批次大小 LR = 0.005 # 学习率 DOWNLOAD_MNIST = True # 首次运行设为True(下载数据),后续设为False N_TEST_IMG = 5 # 测试可视化的图片数量 # 定义数据预处理:将图像转为张量 + 归一化到[0,1](适配解码器Sigmoid输出) transform = transforms.Compose([ transforms.ToTensor() # 转为张量(自动将像素值从0-255缩放到0-1) ]) # -------------------------- # 2. 加载MNIST训练集(使用你提供的加载代码) # -------------------------- train_dataset = datasets.MNIST( root='./data', # 数据保存到本地./data文件夹 train=True, # 加载训练集(共60000张图片) download=DOWNLOAD_MNIST, # 本地没有时自动下载 transform=transform # 应用上面定义的预处理 ) # 构建数据加载器(批量读取数据,打乱顺序) train_loader = DataLoader( dataset=train_dataset, batch_size=BATCH_SIZE, shuffle=True # 训练时打乱数据,提升泛化能力 ) # -------------------------- # 3. 定义自编码器模型 # -------------------------- class AutoEncoder(nn.Module): def __init__(self): super(AutoEncoder, self).__init__() # 编码器:28*28=784维 → 3维(压缩到3维方便后续可视化) self.encoder = nn.Sequential( nn.Linear(784, 128), nn.Tanh(), nn.Linear(128, 64), nn.Tanh(), nn.Linear(64, 12), nn.Tanh(), nn.Linear(12, 3) ) # 解码器:3维 → 784维(重建回原始图像维度) self.decoder = nn.Sequential( nn.Linear(3, 12), nn.Tanh(), nn.Linear(12, 64), nn.Tanh(), nn.Linear(64, 128), nn.Tanh(), nn.Linear(128, 784), nn.Sigmoid() # 输出[0,1],与输入图像像素范围一致 ) def forward(self, x): encoded = self.encoder(x) # 编码:得到3维潜在向量 decoded = self.decoder(encoded) # 解码:重建784维图像 return encoded, decoded # -------------------------- # 4. 初始化模型、优化器、损失函数 # -------------------------- autoencoder = AutoEncoder() # 实例化自编码器 optimizer = torch.optim.Adam(autoencoder.parameters(), lr=LR) # Adam优化器 loss_func = nn.MSELoss() # 均方误差损失(衡量重建图像与原图的差异) # -------------------------- # 5. 开始训练(删除原代码中的重复训练循环) # -------------------------- print("开始训练自编码器...") for epoch in range(EPOCH): total_loss = 0.0 # 记录每轮总损失 for step, (x, _) in enumerate(train_loader): # x是图像,_是标签(自监督无需标签) # 预处理:将28×28的图像张量展平为784维向量 b_x = x.view(-1, 28*28) # 形状:(BATCH_SIZE, 784) b_y = b_x # 自编码器的目标是"重建自身",所以标签=输入 # 前向传播:得到潜在向量和重建结果 encoded, decoded = autoencoder(b_x) # 计算损失:重建结果与输入的均方误差 loss = loss_func(decoded, b_y) # 反向传播与参数更新 optimizer.zero_grad() # 清空上一轮梯度 loss.backward() # 计算梯度(反向传播) optimizer.step() # 用梯度更新模型参数 # 累加总损失 total_loss += loss.item() * b_x.size(0) # 打印每轮训练信息(平均损失=总损失/总样本数) avg_loss = total_loss / len(train_dataset) print(f"第 {epoch+1}/{EPOCH} 轮 | 平均损失:{avg_loss:.6f}") print("训练完成!")
可视化
import numpy as np import matplotlib.pyplot as plt import matplotlib import torch from torchvision import datasets, transforms # -------------------------- # 1. 基础配置与数据准备 # -------------------------- # 解决中文显示 plt.rcParams["font.family"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False # 数据预处理(与训练时保持一致) transform = transforms.Compose([transforms.ToTensor()]) # 加载MNIST测试集(取前200个样本,避免标签重叠) test_dataset = datasets.MNIST( root='./data', train=False, # 用测试集,不干扰训练数据 download=True, transform=transform ) # 处理数据:展平+归一化(确保与模型输入格式匹配) view_data = test_dataset.data[:200].view(-1, 28*28).type(torch.FloatTensor) / 255.0 view_labels = test_dataset.targets[:200].numpy() # 样本对应的数字标签(0-9) # -------------------------- # 2. 提取3维潜在向量(评估模式,无梯度计算) # -------------------------- autoencoder.eval() # 切换到评估模式(关键:避免训练层干扰) with torch.no_grad(): encoded_data, _ = autoencoder(view_data) # 仅获取编码器输出的潜在向量 # 转为NumPy数组,用于绘图 X = encoded_data[:, 0].numpy() # 潜在向量第1维 Y = encoded_data[:, 1].numpy() # 潜在向量第2维 Z = encoded_data[:, 2].numpy() # 潜在向量第3维 # -------------------------- # 3. 修复3D坐标轴创建逻辑(兼容所有matplotlib版本) # -------------------------- fig = plt.figure(figsize=(6, 6)) # 设置图的大小,提升显示效果 # 用add_subplot创建3D坐标轴(替代原Axes3D(fig),解决无坐标轴问题) ax = fig.add_subplot(111, projection='3d') # 为不同数字分配彩虹色(0-9对应不同颜色,增强区分度) cmap = matplotlib.colormaps['rainbow'] # 获取彩虹色谱 for x, y, z, label in zip(X, Y, Z, view_labels): # 计算颜色:将标签(0-9)映射到色谱范围 color = cmap(int(255 * label / 9)) # 9是最大标签,确保颜色均匀分布 # 在3D空间标记样本:显示数字标签+彩色背景 ax.text( x, y, z, str(label), fontsize=10, backgroundcolor=color, ha='center', va='center' # 文字居中,避免偏移 ) # -------------------------- # 4. 优化坐标轴与标题 # -------------------------- # 设置坐标轴范围(留少量余量,避免样本贴边) ax.set_xlim(X.min() - 0.1, X.max() + 0.1) ax.set_ylim(Y.min() - 0.1, Y.max() + 0.1) ax.set_zlim(Z.min() - 0.1, Z.max() + 0.1) # 坐标轴标签与标题(清晰说明图的含义) ax.set_xlabel('潜在向量第1维', fontsize=12, labelpad=10) ax.set_ylabel('潜在向量第2维', fontsize=12, labelpad=10) ax.set_zlabel('潜在向量第3维', fontsize=12, labelpad=10) ax.set_title('MNIST数字在自编码器3维潜在空间的分布', fontsize=16, pad=20) # 调整布局,避免元素重叠 plt.tight_layout() # 显示图像(可鼠标拖动旋转,查看3D视角) plt.show()