简介:本资源是一套面向MATLAB初学者与进阶开发者的生成对抗网络(GAN)实践项目,聚焦于在MATLAB环境下从零构建并训练基础GAN模型,解决深度学习中生成建模的入门实操难题。压缩包共63个文件,含56个核心.m函数文件(覆盖卷积、转置卷积、批归一化、全连接层搭建与反向传播等GAN关键模块)、3张可视化结果图(png)、1份LICENSE协议、1个README说明文档(md)及1份Word格式技术说明(docx),整体仅73KB,轻量易部署。已有1894人学习下载,资源经作者达摩老生实测校正,全部代码可一键运行,配套gan_train.m主训练脚本与多个example_x.m示例(含4个典型训练场景),并提供梯度计算、误差项推导、激活函数实现等底层细节模块,便于理解GAN数学原理与MATLAB工程实现的对应关系。
1. 这不是调用trainNetwork的“GAN”,而是手撕反向传播的 MATLAB GAN 实战项目
你可能在 MATLAB Deep Learning Toolbox 里见过ganTrainOptions和trainingOptions,但那套封装好的接口背后,梯度怎么流、误差怎么回传、生成器和判别器的权重如何独立更新——全被黑箱吞掉了。而这份「达摩老生出品」的源码包,是极少见的、完全脱离dlnetwork/layerGraph高阶 API,用纯.m文件逐层实现前向计算、误差项推导、梯度计算与参数更新的 GAN 全流程代码。它不依赖任何深度学习工具箱(甚至可在 R2018a+ 基础版 MATLAB 运行),所有卷积、转置卷积、BatchNorm、LeakyReLU 的forward/backward都以函数形式展开,连conv2d.m里 padding 模式、stride 步长、filter 维度对齐逻辑都写在注释里。适合两类人:一是刚学完《神经网络与深度学习》想亲手验证反向传播公式的本科生;二是需要在嵌入式 MATLAB 环境(如 Simulink Coder 或旧版工业控制平台)中部署轻量 GAN 的工程师——因为这里没有dlnetwork对 GPU 的隐式绑定,也没有dlarray的自动微分依赖。
2. 从nn_setup.m到gan_train.m:GAN 网络结构定义与训练循环的底层拆解
2.1 网络拓扑由nn_setup.m显式声明,而非layerGraph自动连接
MATLAB 深度学习工具箱中常见的layerGraph是声明式建模,而本项目采用命令式结构定义。打开nn_setup.m,你会看到生成器(Generator)和判别器(Discriminator)分别通过setup_*_layer.m函数链式构建:
% 在 nn_setup.m 中节选(已简化) G_layers = {}; G_layers{end+1} = setup_fully_connect_layer(100, 128*4*4); % 输入噪声 z (100-dim) → 全连接 G_layers{end+1} = setup_reshape_layer([4, 4, 128]); % reshape to 4x4x128 feature map G_layers{end+1} = setup_conv2d_transpose_layer(128, 64, 4, 2, 1); % transposed conv: 4x4x128 → 8x8x64 G_layers{end+1} = setup_batch_norm_layer(64); G_layers{end+1} = setup_activation_layer('leaky_relu', 0.2); G_layers{end+1} = setup_conv2d_transpose_layer(64, 3, 4, 2, 1); % final: 8x8x64 → 16x16x3 (RGB) G_layers{end+1} = setup_activation_layer('tanh'); % 输出归一化到 [-1,1]注意:
setup_conv2d_transpose_layer(in_ch, out_ch, kernel_size, stride, pad)的第 4、5 参数对应stride和output_padding(非传统 padding),这与 PyTorchConvTranspose2d语义一致,但不同于 MATLABtransposedConv2dLayer默认行为。若你直接套用工具箱文档参数,会发现输出尺寸错位——这是本项目第一个关键校验点。
判别器结构则反向对称:
D_layers = {}; D_layers{end+1} = setup_conv2d_layer(3, 64, 4, 2, 1); % 16x16x3 → 8x8x64 D_layers{end+1} = setup_leaky_relu_layer(0.2); D_layers{end+1} = setup_conv2d_layer(64, 128, 4, 2, 1); % 8x8x64 → 4x4x128 D_layers{end+1} = setup_batch_norm_layer(128); D_layers{end+1} = setup_leaky_relu_layer(0.2); D_layers{end+1} = setup_fully_connect_layer(4*4*128, 1); % flatten + linear → scalar logits所有层对象均含type,params,grads,state字段(如batch_norm的 running_mean/runing_var 存于state),为后续手动 BP 提供数据容器。
2.2 前向传播nn_ff.m与误差项get_error_term_from_*.m的双轨设计
本项目未使用自动微分,而是将误差项(error term δ = ∂L/∂z)作为独立模块分离。nn_ff.m执行纯前向,每层输出存入layer.output;而反向传播时,nn_bp_g.m(生成器)和nn_bp_d.m(判别器)按拓扑逆序调用get_error_term_from_*.m获取当前层输入误差:
% 在 nn_bp_d.m 中(判别器反向) for l = length(D_layers):-1:1 layer = D_layers{l}; if l == length(D_layers) % 最后一层:fully connect → sigmoid cross entropy delta = get_error_term_from_fully_connect_layer(layer, dL_dy, 'sigmoid_cross_entropy'); else delta = get_error_term_from_*.m(layer, next_delta); % next_delta 来自上层 get_error_term 输出 end % 更新 grads 并传递给前一层 D_layers{l}.grads = calculate_gradient_for_*(layer, delta, D_layers{l-1}.output); endget_error_term_from_conv2d_layer.m的核心逻辑是:
function delta_in = get_error_term_from_conv2d_layer(layer, delta_out, opts) % delta_out: [H_out, W_out, C_out, N] —— 上层传来的误差 % layer.params.W: [K, K, C_in, C_out] —— 卷积核 % 关键:delta_in = conv2d_transpose(delta_out, flip(W), 'same') % 但需处理 stride 和 padding —— 本项目用 insert_zeros_into_array.m 实现 upsampling upsampled = insert_zeros_into_array(delta_out, layer.stride); % 在 delta_out 间插零 flipped_W = flipall(layer.params.W, [1,2]); % 空间维度翻转 delta_in = conv2d(upsampled, flipped_W, 'full'); % full convolution 实现 transpose conv % 再根据原始输入尺寸 crop 到 [H_in, W_in, C_in, N] delta_in = crop_to_input_size(delta_in, layer.input_size); end提示:
flipall.m不是 MATLAB 内置函数,它对 kernel 的[1,2]维度做flip(),这是 CNN 反向传播中卷积核翻转的数学要求。若你误用rot90()或漏掉翻转,梯度更新将彻底失效——这也是新手调试时最常卡住的点。
2.3 生成器与判别器的梯度隔离:nn_applygrads_adam.m的双优化器实例
GAN 训练必须保证 G 和 D 的参数独立更新。本项目在gan_train.m中显式维护两套 Adam 状态变量:
% gan_train.m 节选 G_adam_state = struct('m', {}, 'v', {}, 't', 0); % 生成器 Adam momentums D_adam_state = struct('m', {}, 'v', {}, 't', 0); % 判别器 Adam momentums for epoch = 1:num_epochs for b = 1:num_batches % Step 1: Train Discriminator [D_loss, D_grads] = nn_bp_d(D_layers, real_batch, fake_batch); [D_layers, D_adam_state] = nn_applygrads_adam(D_layers, D_grads, D_adam_state, lr_D); % Step 2: Train Generator (freeze D, only update G) [G_loss, G_grads] = nn_bp_g(G_layers, D_layers, noise_batch); [G_layers, G_adam_state] = nn_applygrads_adam(G_layers, G_grads, G_adam_state, lr_G); end endnn_applygrads_adam.m中的关键是:它遍历layers列表,对每个含params.W和params.b的层,分别更新其grads.dW和grads.db,并用adam_update()函数计算带偏置校正的一阶/二阶矩。参数lr_G = 2e-4,lr_D = 2e-4在example_1.m中硬编码,但你可以根据sigmoid_cross_entropy.m返回的 loss magnitude 动态缩放——当D_loss接近 0 时,说明判别器过强,应降低lr_D或增加lr_G。
3. 从example_1.m到图像生成:数据加载、训练监控与结果可视化全流程
3.1 数据预处理:util/下的padding_height_width_in_array.m与save_images.m是图像 I/O 核心
本项目默认使用 MNIST 或自定义 16×16 RGB 图像(见readme_images/)。加载逻辑在example_1.m中:
% example_1.m 节选 img_dir = 'readme_images/'; img_files = dir(fullfile(img_dir, '*.png')); X_real = []; for i = 1:length(img_files) img = imread(fullfile(img_dir, img_files(i).name)); img = imresize(img, [16,16]); % 强制缩放到 16x16 img = im2double(img); % 归一化到 [0,1] img = img * 2 - 1; % 转换到 [-1,1] —— 与 tanh 输出匹配 X_real = cat(4, X_real, permute(img, [4,1,2,3])); % NHWC → NCHW? 注意:本项目用 NCHW! end % 但注意:conv2d.m 默认按 MATLAB 习惯处理为 [H,W,C,N],所以实际存储为 [16,16,3,N] % 因此 padding_height_width_in_array.m 的作用是:当输入尺寸不整除 stride 时,补零至可整除 X_real_padded = padding_height_width_in_array(X_real, 2, 2); % 为 stride=2 的 conv 准备padding_height_width_in_array.m的实现不是简单padarray(),而是计算最小填充量使H % stride == 0 && W % stride == 0,再调用padarray(X, [pad_h, pad_w], 'post')。若你跳过此步,conv2d.m中的imfilter会因尺寸不匹配报错。
生成图像保存由save_images.m完成:
function save_images(images, filename_prefix, epoch) % images: [H,W,C,N] —— N 张生成图 n = size(images,4); n_row = floor(sqrt(n)); n_col = ceil(n/n_row); canvas = zeros(H*n_row, W*n_col, C); for i = 1:n r = ceil(i/n_col); c = mod(i-1, n_col) + 1; canvas((r-1)*H+1:r*H, (c-1)*W+1:c*W, :) = images(:,:,:,i); end % 反归一化:[-1,1] → [0,1] → uint8 canvas = (canvas + 1)/2; canvas = im2uint8(canvas); imwrite(canvas, sprintf('%s_epoch_%d.png', filename_prefix, epoch)); end提示:
im2uint8()会截断超出 [0,1] 的值。若生成器输出存在较大震荡(如tanh饱和区梯度消失),部分像素可能为-1.2或1.05,导致保存后出现灰斑。建议在save_images.m前加images = max(-1, min(1, images));钳位。
3.2 训练过程监控:gan_train.m中的 loss 曲线与收敛判断
gan_train.m内置了简易 loss 记录机制:
loss_history = struct('D_real', {}, 'D_fake', {}, 'G', {}); for epoch = 1:num_epochs D_real_loss = 0; D_fake_loss = 0; G_loss = 0; for b = 1:num_batches % ... training steps ... D_real_loss = D_real_loss + mean(D_real_batch_loss); D_fake_loss = D_fake_loss + mean(D_fake_batch_loss); G_loss = G_loss + mean(G_batch_loss); end loss_history.D_real{end+1} = D_real_loss / num_batches; loss_history.D_fake{end+1} = D_fake_loss / num_batches; loss_history.G{end+1} = G_loss / num_batches; % 每 10 epoch 画图 if mod(epoch,10)==0 figure; plot(1:epoch, cell2mat(loss_history.D_real), 'r', ... 1:epoch, cell2mat(loss_history.D_fake), 'b', ... 1:epoch, cell2mat(loss_history.G), 'g'); legend('D on Real', 'D on Fake', 'G Loss'); xlabel('Epoch'); ylabel('Loss'); title(sprintf('GAN Training Loss (Epoch %d)', epoch)); saveas(gcf, sprintf('loss_epoch_%d.png', epoch)); end end典型收敛曲线特征:D_real快速下降至 ~0.3(判别器对真实图信心高),D_fake缓慢上升至 ~0.7(开始难区分假图),G_loss持续下降但波动大。若D_fake长期 < 0.2,说明生成器太弱,需检查leaky_relu的 alpha 是否设为 0.2(delta_leaky_relu.m中 hard-coded);若D_real> 0.6 且不降,可能是sigmoid_cross_entropy.m的 label smoothing 未启用(本项目未实现,需手动添加)。
3.3example_4.m:条件 GAN 的扩展入口与标签嵌入实践
example_4.m展示了如何将本框架扩展为条件 GAN(cGAN)。其核心改动在生成器输入拼接:
% example_4.m 节选 z = randn(100, batch_size); % 噪声 y = randi([0,9], 1, batch_size); % 类别标签 0-9 y_onehot = zeros(10, batch_size); y_onehot(sub2ind([10,batch_size], y, 1:batch_size)) = 1; z_cond = [z; y_onehot]; % 拼接成 110-dim 输入 % 修改 G_layers 第一层 G_layers{1} = setup_fully_connect_layer(110, 128*4*4); % 输入维度变为 110判别器则需在最后一层前拼接标签:
% 在 D_layers 最后一个 fully connect 前插入 D_layers{end-1} = setup_reshape_layer([1,1,128]); % flatten to [1,1,128, N] D_layers{end} = setup_fully_connect_layer(128+10, 1); % +10 for y_onehot此时nn_bp_d.m中的get_error_term_from_fully_connect_layer需支持多输入分支,但本项目未提供——你需要修改该函数,使其能接收next_delta和y_onehot,并在计算dL/dy_onehot时返回用于生成器更新的梯度。这是进阶改造的第一道门槛。
4. 手动梯度验证与 BatchNorm 状态同步:两个高频崩溃点的定位与修复
4.1 使用数值梯度检验calculate_gradient_for_*.m的正确性
当训练 loss 不降或 NaN 时,首要怀疑梯度计算错误。本项目未内置数值梯度检验,但可快速手写验证calculate_gradient_for_conv2d_layer.m:
% 在 test_convolution_process.m 中添加 layer = setup_conv2d_layer(3, 8, 3, 1, 1); % 3→8 ch, 3x3 kernel X = randn(32,32,3,5); % dummy input Y = conv2d(X, layer.params.W, 'same') + layer.params.b; % forward % 数值梯度:扰动 W 的 (1,1,1,1) 元素 h = 1e-5; W_orig = layer.params.W(1,1,1,1); layer.params.W(1,1,1,1) = W_orig + h; Y_plus = conv2d(X, layer.params.W, 'same') + layer.params.b; layer.params.W(1,1,1,1) = W_orig - h; Y_minus = conv2d(X, layer.params.W, 'same') + layer.params.b; num_grad = (sum(Y_plus(:)) - sum(Y_minus(:))) / (2*h); % 解析梯度(来自 calculate_gradient_for_conv2d_layer) analytic_grad = calculate_gradient_for_conv2d_layer(layer, ones(size(Y)), X); analytic_grad_at_1111 = analytic_grad.dW(1,1,1,1); fprintf('Numerical grad: %.6f, Analytic grad: %.6f, Error: %.2e\n', ... num_grad, analytic_grad_at_1111, abs(num_grad - analytic_grad_at_1111));若误差 > 1e-4,说明calculate_gradient_for_conv2d_layer.m中的conv2d调用模式('valid'/'full')、padding 处理或X与delta_out维度对齐有误。常见错误是conv2d.m内部用了'same'模式但反向时未对应调整。
4.2batch_norm.m的state.running_mean更新陷阱与setup_batch_norm_layer.m的初始化修正
batch_norm.m的前向包含训练/测试模式分支:
function Y = batch_norm(X, params, state, is_training) if is_training mu_batch = mean(X, [1,2,4]); % spatial & batch mean var_batch = var(X, 0, [1,2,4]); % 更新 running stats: state.running_mean = decay * running_mean + (1-decay) * mu_batch state.running_mean = 0.99 * state.running_mean + 0.01 * mu_batch; state.running_var = 0.99 * state.running_var + 0.01 * var_batch; Y = (X - mu_batch) ./ sqrt(var_batch + 1e-5); else Y = (X - state.running_mean) ./ sqrt(state.running_var + 1e-5); end end问题在于:setup_batch_norm_layer.m初始化state.running_mean = zeros(1,1,C,1),但若第一轮mu_batch维度为[1,1,C,N],mean(X,[1,2,4])实际返回[1,1,C,1],而state.running_mean是[1,1,C,1],维度匹配。但若你在nn_setup.m中误将C设为标量(如setup_batch_norm_layer(64)),而X的通道数实为 128,则mu_batch尺寸为[1,1,128,1],赋值给[1,1,64,1]的running_mean会触发 dimension mismatch error。
修复方法:在setup_batch_norm_layer.m中强制校验:
function layer = setup_batch_norm_layer(C) assert(isnumeric(C) && C>0, 'C must be positive integer'); layer.type = 'batch_norm'; layer.params.gamma = ones(1,1,C,1); layer.params.beta = zeros(1,1,C,1); layer.state.running_mean = zeros(1,1,C,1); layer.state.running_var = ones(1,1,C,1); % 关键:记录期望通道数,供 runtime 校验 layer.expected_C = C; end并在batch_norm.m开头添加:
assert(size(X,3) == layer.expected_C, ... sprintf('BatchNorm expected %d channels, got %d', layer.expected_C, size(X,3)));4.3conv2d_transpose.m的输出尺寸公式与setup_conv2d_transpose_layer.m参数映射表
转置卷积输出尺寸易错,本项目在setup_conv2d_transpose_layer.m注释中给出精确公式:
| 参数 | 含义 | 公式(输入 H_in×W_in) | 示例(H_in=4, W_in=4) |
|---|---|---|---|
kernel_size | 卷积核边长 K | — | K=4 |
stride | 步长 S | H_out = S×(H_in−1) + K − 2×pad | S=2, pad=1 → H_out = 2×3 + 4 − 2 = 8 |
pad | 外围补零数 P | W_out = S×(W_in−1) + K − 2×P | 同上 → W_out = 8 |
output_padding | 无(本项目未实现) | — | — |
若你传入setup_conv2d_transpose_layer(128,64,4,2,0)(pad=0),则 H_out = 2×3 + 4 − 0 = 10,但下一层conv2d_layer期望输入为 8×8 —— 尺寸不匹配直接 crash。因此example_1.m中pad=1是经过尺寸链推导的精确值,不可随意更改。
提示:所有
setup_*_layer.m函数的pad参数均指conv2d的pad,而conv2d_transpose的pad是为了补偿stride导致的尺寸损失,二者物理意义不同。混淆它们是本项目第二高发错误。
本文还有配套的精品资源,点击获取