毕设选了“基于深度学习的图像修复算法”,底层结构落在DCGAN上,这个选题我比较熟。这几年图像修复从传统的偏微分方程、PatchMatch一路演进到GAN、扩散模型,但毕设层面DCGAN依然是最合适的切入点——它把对抗训练、卷积架构、潜在空间映射这几个核心问题都覆盖了,既不会浅到没东西写,又不会难到几个月跑不出结果。这篇文章不打算复述论文公式,就按我实际调试一台图像修复模型的完整过程来写,包括网络怎么搭、损失怎么配、数据怎么喂、训练崩了怎么救,最后再说说毕设答辩时导师最关心的几个点。
1. 图像修复为什么需要GAN,以及DCGAN到底改了什么
1.1 图像修复任务到底在解决什么问题
图像修复,英文里常叫Image Inpainting,指的是图像缺失区域的补全。现实场景大概是三类:老照片扫描出来有折痕、划痕,仰拍建筑照片里出现碍眼的电线杆,或者被水印、Logo遮挡的文字图片。任务定义很直接:给定一张带有掩码(Mask)的残缺图,模型要把掩码覆盖的像素重新生成出来,并且要求结果在视觉上足够自然——纹理连续、边缘对齐、光照一致。
如果只用传统方法,问题会变得很棘手。以PatchMatch为例,这类方法从图像已知区域里寻找相似纹理块,再复制填充到缺失区。遇到天空、墙壁这类重复纹理,效果不错;可一旦缺失区域落在眼睛、嘴角这类高语义区域,PatchMatch基本无能为力——它没有“理解”五官应该怎么摆放,只是机械地把纹理搬过去。所以深度学习方法的目标很明确:让模型从大量数据里学到“缺失区域应该长什么样”的先验,再反过来补全。
1.2 为什么朴素CNN不够,而GAN能胜出
有人会问,用纯卷积网络做回归行不行?行,但结果往往“糊”。朴素的CNN图像修复模型本质上是在做一个逐像素回归任务,损失函数通常是L1或L2距离,这两类损失倾向于把预测值拉向所有可能结果的平均。眼睛有上百种合理画法,模型一平均,轮廓是有了,细节全没了,出来的就是一块蒙了层雾的区域。
GAN的思路是换一种监督信号:不再告诉生成器“每个像素应该等于哪个值”,而是让一个判别器去打分,“这张图看起来像不像一张真实的完整图”。对抗训练逼迫生成器产出足够锐利、足够真实的图像才能骗过判别器。这正好补上了回归模型“细节糊”的短板。所以图像修复里GAN能成为主流方案,本质是监督方式的胜利,而不是网络深度或参数量的胜利。
1.3 DCGAN给GAN带来的关键改进
DCGAN全称Deep Convolutional GAN,它并不是一种全新的图像修复模型,而是把GAN的生成器和判别器都换成了卷积结构,并且做了一系列稳定性改进。核心改动包括:去掉全连接层,改用全卷积网络;生成器用转置卷积上采样,判别器用带步长的卷积下采样;每个卷积层后接BatchNorm,同时生成器输出层用Tanh、判别器最后一层不用BatchNorm;激活函数方面,生成器用ReLU(输出层用Tanh),判别器用LeakyReLU。
这些改动直接决定了毕设能不能在有限算力下跑通。全连接层的参数量大又难以学到空间结构;不加BatchNorm很容易出现梯度爆炸;如果用普通ReLU做判别器,负区间的梯度被截断为0,生成器很容易通过“制造极暗像素”来骗过判别器。DCGAN把这些问题逐一堵上之后,训练稳定性和出图质量都有了质的提升。现在回看,我选DCGAN作为毕设底层,不是因为它最强,而是因为它最“稳”——训练曲线可解释,调参路径清晰,出了问题也好定位。
2. 训练之前必须搞懂的四个核心组件
2.1 生成器结构:从噪声到图像的转置卷积之路
在图像修复任务中,生成器的输入不是纯噪声,而是“残缺图像+掩码”的拼接。常见的做法是把3通道的残缺图和1通道的掩码在通道维上拼接,得到一个4通道输入,然后经过编码器-解码器结构。编码器逐步下采样,把图像压缩到低分辨率的高维特征;解码器再通过转置卷积逐步还原分辨率。
设计生成器时,最关键的是通道数变化和下采样次数。我实际用的结构大致是:输入4通道,经过Conv-BN-ReLU下采样到64通道,两次下采样后到256通道,空间尺寸从256×256缩到64×64,再通过转置卷积逐级上采样回256×256,最后接一层Tanh把输出映射到[-1,1]。需要注意转置卷积的kernel size、stride和padding需要匹配,否则会出现输出尺寸对不上的问题。一个常用的检查方式是:size_out = (size_in - 1) * stride - 2 * padding + kernel_size。
我在调整结构时踩过一个很典型的坑:把编码器下采样次数设得太多,比如一路下采样到16×16,生成器需要从极小的特征图里恢复出256×256的图像,结果细节完全丢失,只能靠记忆化生成纹理,修复区域全是重复的斑块。对256×256输入,下采样到64×64或32×32是相对合理的区间。
2.2 判别器结构:用感受野衡量伪造痕迹
判别器不需要恢复图像,它只需要输出一个概率分数。但需要注意的是,图像修复里的判别器通常不是对所有像素做全局判断,而是做PatchGAN式的局部判别。PatchGAN把图像切分成N×N个patch,对每个patch独立判断真伪,最后取平均。这样做的好处是两个:一是局部真实性比全局真实感更容易学;二是感受野的设计让判别器更关注纹理细节,而不是纠结整幅构图。
实际实现时,我用的判别器输入是“拼接图”或“生成图”的单张三通道图,经过4个卷积块逐步下采样,每层通道数从64翻倍到512,最后通过一个1×1卷积把特征映射成单通道响应图,再取均值作为判别分数。这里有个细节:BN层在判别器里不是万能的。DCGAN论文里也提到判别器第一层不加BN,否则输入分布会被提前规范化,边缘信息容易丢失。我在实验里对比过,加上BN之后显存占用略增,收敛速度也没明显变化,索性遵循原设计。
2.3 损失函数:对抗损失不是唯一的监督信号
如果只用对抗损失训练图像修复模型,训练会非常不稳定,原因是梯度信号缺少“恢复正确性”的引导。对抗损失只告诉生成器“你骗不过我”,但没告诉它“哪块区域错了、错在哪里”。所以我加了内容损失(重建损失),用L1距离约束生成图与真实图之间的差距。L1比L2更常用,因为L2会惩罚大误差,模型倾向于输出保守的平滑结果,而L1在细节保持上更有优势。
最终的损失函数是:
Loss = L1_loss(G(x), y) + λ * L1_loss(D_fake, 1) + 判别器的二分类交叉熵损失
λ一般设置为10。这里要补充说明:内容损失不只作用在整幅图像上,还要单独对掩码区域计算一次L1损失。否则模型学会了“只补齐非掩码区也能降低全局损失”——它会把已知区域画得几乎完美,而缺失区域随便糊弄一下,因为缺失区域只占图像的一小部分,对总损失影响有限。我当时用掩码把生成图切割出来单独算损失,这个改动直接让修复边界干净了很多。
关于LSGAN和WGAN-GP,很多人会在毕设里问要不要换。DCGAN原始论文用的是普通BCE损失,但普通对抗损失存在梯度消失的风险——当判别器过于自信时,生成器拿不到有效梯度,训练僵死。我实际训练中遇到过此问题,最后把损失函数换成了LSGAN的最小二乘形式,也就是把判别器输出经BCE换成MSE。LSGAN的梯度在判别器饱和时依然存在,训练稳定性有明显提升。如果时间充裕,也可以试WGAN-GP,但实现起来要多写梯度惩罚项,作为毕设系统而言,LSGAN的性价比更高。
2.4 超参数:Adam不背锅,背锅的是你没调好它
DCGAN训练中最容易被忽视的就是超参数选择。Adam是默认选择,但学习率的设置非常敏感。我训练初版时用了默认的0.001,前500步还行,到中期生成器和判别器loss开始交替震荡,图像出现大面积重复纹理。后来把学习率降到0.0002,再配合Beta1=0.5、Beta2=0.999,训练曲线明显平滑。Beta1=0.5是DCGAN论文中特别强调的:默认Beta1=0.9时,动量的滑动平均太大,生成器容易震荡。如果你发现训练到一半loss起伏不定,先检查学习率,再检查Beta1。
批量大小也会影响训练。在单块消费级显卡上,我用8作为默认batch size,256×256输入下显存占用大约10GB左右。batch size太小会导致每个batch的梯度方向跳动太大,判别器不稳定;太大又会让每次迭代时间过长。如果显存紧张,优先考虑缩小生成器通道数,而不是降低batch size。
3. 数据集准备与预处理:细节决定成败
3.1 数据从哪里来,怎么选
图像修复的数据集一般有两类选择:一是真实损坏图(老照片扫描件、带水印的图),二是正常图加掩码合成的数据。毕设建议选第二种,因为真实损坏图没有对应的“原图”作为监督标签,无法计算重建损失。合成数据的方式是:从数据集中随机取一张完整图,叠加一个随机生成的掩码,然后把掩码区域的像素置为0或噪声,形成了一个带标签的训练对。
数据集规模上,我用的CelebA-HQ子集大约2万张人脸图,对毕设来说足够了。如果做通用场景修复,可以用Places2,但10万张的规模对训练时间和显存压力都比较大。实操中还有一个取巧的办法:先用小幅图像、少量数据把训练流程跑通,确认没有bug之后再切到全量数据。我建议直接用一批20张图做冒烟测试,观察loss是否能在正常范围内变动,再决定是否全量运行。
3.2 掩码生成:别让模型在“容易题”上偷懒
掩码的形状直接决定模型学到的修复能力。如果掩码全是小方块或细线,模型只要学会周围插值就能蒙混过关;如果全是覆盖半个脸的大面积掩码,模型又难以生成合理结构。毕设中需要覆盖多样化的掩码类型。我的做法是生成三种掩码:小型随机多边形,用于模拟划痕和破损;中等面积的矩形或不规则形状,模拟遮挡;以及覆盖重要区域的半脸掩码,模拟大面积缺失。三种掩码在训练中按3:4:3的比例混合输入。
数据增强方面,我用了随机水平翻转和轻微旋转,但要注意一点:人脸数据翻转后依然是合理人脸,但如果是带方向性的场景(比如文字图像、交通标志),翻转就会制造错误标签。增强策略要跟着任务走,不能无脑套用。另一个容易被忽略的是像素归一化:图像输入网络前要缩放到[-1,1]区间,也就是 (img / 255) * 2 - 1,这会跟生成器输出的Tanh范围匹配。掩码通道保持0和1即可,不需要归一化。
3.3 训练集与验证集的划分细节
训练集和验证集的划分不能简单随机。我最初做法是随机划分,结果发现验证集loss很低,但实际修复效果不行——原因是数据集中存在大量相似样本,随机划分导致验证集与训练集高度重叠。后来我改成按身份划分:同一张脸只出现在训练集或验证集其中一边,评估才真正反映泛化能力。
验证评估还要注意固定掩码:每次评估用同一批掩码,才能横向比较不同训练阶段的效果。否则每次验证用的掩码不一样,loss浮动会掩盖模型真实的进步水平。我当时在验证时固定了8组掩码模板,每次只换图像不换掩码,这个细节让训练曲线变得可解释得多。
4. 实操训练:从搭建到收敛的完整过程
4.1 模型搭建与训练循环
搭建环节我用PyTorch,原因是有灵活的autograd和较快的迭代速度。生成器和判别器都定义成标准的nn.Module子类。训练循环的核心在于处理“真实图-生成图-判别器”三者的更新顺序。一个常见的错误是误将更新判别器的loss也用来更新生成器,导致生成器梯度走向混乱。正确流程是:先固定生成器,用真实图和生成图更新判别器;再固定判别器,用生成图经过判别器得到的分数更新生成器。这里涉及一个细节:判别器d_output = D(img_fake),更新生成器时,梯度应只回传到生成器参数,这可以通过将fake_img脱离计算图或只在生成器参数上调用optimizer.step()来实现。PyTorch中通常用分别维护两个optimizer来规避此问题。
训练中我设定每轮迭代先训练判别器再训练生成器。判别器更新步数在默认情况下是1,但有时候我会在训练前半程让判别器多更新一步,让判别器先学好判断能力,再让生成器与更成熟的判别器对抗。需要说明,这个做法并非越强越好:判别器太强,生成器梯度几乎为0,训练反而停滞。
4.2 训练过程实录
训练初期(约500步以内),判别器loss快速下降,生成器loss上升,这是正常现象。中期(2000步左右),生成器开始输出有结构的内容,但边缘仍有模糊。到8000步左右,生成图已经能看出清晰的五官轮廓,只是纹理细节还不够真实。我在约80个epoch时得到一版最佳模型,训练时长大约16小时,输入为256×256的CelebA-HQ子集。
这里分享一个我在训练中总结的监控习惯:除了记录loss,还要定期把生成图保存下来看。人的视觉反馈比loss曲线更直观,尤其当loss在下降但图像模糊时,说明模型陷入了“平均化”陷阱,需要调高对抗损失权重。
4.3 评估指标怎么选:PSNR和SSIM的局限
毕设论文中评估指标一般会用PSNR和SSIM。PSNR衡量像素误差,越高越好;SSIM衡量结构相似性,越接近1越好。但还是那句话,这两个指标与真实视觉感受相关,但并非完全一致。我见过PSNR很高但图像边缘出现了明显振铃效应的例子,也见过SSIM不错但颜色整体偏移的结果。原因是这两个指标都没有建模“语义是否合理”这一维度。因此在毕设实验中,我会同时提供三种评估:PSNR、SSIM以及人工主观评分。人工评分虽然听起来不够“硬核”,但在图像修复领域是标准做法之一,导师不会觉得它不专业。
指标计算也有讲究。评估时要先使生成图的尺寸和数据范围与真实图完全一致,再调用skimage.metrics.structural_similarity计算SSIM,psnr用10 * log10(1 / MSE)计算,其中1表示像素范围上限。如果在[0,255]范围内计算PSNR,公式里的分母要换成255的平方,不统一会导致指标偏大或偏小,答辩时容易被追问。
5. 常见问题与排查技巧实录
5.1 训练不收敛:loss震荡不止怎么办
最常见的情况是训练到中期loss忽高忽低,图像始终模糊。排查路径有四个:第一,确认图像和掩码的归一化范围是否一致——我遇到过批次内图像被归一化两次的情况,整个输出都偏暗;第二,检查学习率是否过高,把学习率从0.0002继续降到0.0001,同时观察一个完整epoch的损失曲线;第三,确认生成器是否用了Tanh作为输出层,如果输出层的激活函数配错,判别器无法准确判断数据分布;第四,确认判别器最后一层是否没有加BN或激活函数,保持输出为线性值。
5.2 模式坍塌:生成图变得千篇一律
当生成器发现某一种输出特别容易骗过判别器时,它会“偷懒”,把所有输入映射到同一个样式的输出。出现模式坍塌时,修复区域会呈现重复的纹理或者统一偏灰的外观。我的解决方法是引入LSGAN损失,并且将batch size从4提高到8,让判别器在每个batch里看到更多样本,有利于稳定估计分布。如果还是坍塌,试一下给生成器的输入拼接区域稍作数据增强(如给非掩码区域添加少量高斯噪声),削弱生成器对已知区域的过度依赖。
5.3 边缘伪影:修复边界处颜色突变
修复区域边缘出现的明显色带或接缝,是图像修复的常见问题之一。出现原因往往是内容损失在整图上权重过大,模型优先优化非掩码区域,而掩码边界附近约束不足。我采取的补救方法是:生成图与真实图的损失计算时,对掩码边缘区域做冲淡处理(dilate mask),让边界处的梯度信号更强;或者简单粗暴地提高掩码区域的权重,比如在实现中将L1损失函数在掩码区域乘以一个5~10的系数。
另外,如果生成图是整图作为判别器输入,而掩码是随机的,判别器可能学会“只看掩码边界”来判定真伪,导致修复边缘更加突兀。为避免这个问题,可以让生成图在进入判别器前经过一次轻微的高斯模糊或随机裁剪,迫使判别器关注整体纹理而不是边界几何。
5.4 常见问题速查表
| 现象 | 可能原因 | 快速排查建议 |
|---|---|---|
| 训练初期loss直接归零 | 判别器过强,生成器梯度消失 | 降低判别器学习率,检查是否误用了预训练判别器 |
| 生成图有结构但颜色整体偏灰 | Tanh输出范围与数据归一化不一致 | 确认输入范围是否为[-1,1],而不是[0,1] |
| 修复区域纹理重复 | 下采样次数过多,特征图过小 | 减少一次下采样,或改用膨胀卷积 |
| 边缘出现黑色边框 | 卷积padding方式不当导致边界像素异常 | 改用same padding或调整转置卷积参数 |
| 掩码区域与已知区域亮度明显不一致 | 内容损失权重过低 | 提高掩码区域的L1权重 |
| PSNR高但视觉差 | 模型回归到均值分布 | 调整对抗损失权重,或在评估中增加主观评分 |
6. 作为毕设,还能往哪些方向做深
6.1 从DCGAN走向条件生成与注意力机制
如果只停留在把DCGAN当作核心模型反复调参,毕设深度会显得单薄。我建议在基线模型基础上做两个扩展:第一个是基于局部条件的生成,比如把掩码区域的位置编码(positional embedding)加到特征图中,让模型知道“缺失区域在哪、周围有什么”;第二个是引入自注意力层,在生成器的低分辨率层后添加一个Self-Attention模块,让模型能在远距离像素之间建立联系,对人脸这种对称性明显的任务尤其有效。自注意力模块实现难度不大,几十行代码就能完成,但效果和论文创新点都提升明显。
6.2 从单阶段生成到粗到精两阶段修复
另一种有效扩展是两阶段修复。第一阶段用浅层网络快速生成一个粗糙修复结果,第二阶段把粗糙结果与原始残缺图拼接,再送入细化网络精修。这样做的理由很朴素:单阶段网络要同时完成“语义猜测”和“细节生成”,任务过重;拆开后每一阶段的任务更单纯,训练更稳定。编解码器的责任也能划分清楚,精修阶段可以专注纹理和边缘。两阶段结构写进论文里,就是“由粗到精的修复框架”,比直接套用原始DCGAN更有说服力。
6.3 答辩时的加分细节
毕设答辩中,导师大概率会问三个问题:为什么用GAN而不是纯CNN?训练为什么不收敛你怎么办?你的模型和已有方法相比提升在哪?提前准备对应的图和数值就能从容应对。最好整理一张对比表:DCGAN基线、DCGAN+自注意力、DCGAN+两阶段修复,三者在PSNR、SSIM和主观评分上的差异。哪怕提升只有0.3dB,只要有清晰的消融实验和趋势分析,就已经接近一篇合格毕设的工作量。
我个人在整理实验记录时还有一个习惯:把每个epoch的生成图拼成视频或网格图,答辩PPT里直接放几组关键帧,比放100行公式更直观,导师也更容易理解你做了什么。
图像修复这个坑,看起来不大,但实际做下来涉及的细节非常多。整个训练周期里,模型结构调整、损失函数设计、掩码策略、评估指标这些问题都会依次冒出来,每一个都需要实际动手才能理解背后的权衡。DCGAN虽然已经不是最新的方法,但它作为理解对抗生成网络和图像生成任务的地基,意义非常扎实。如果时间允许,把中间每个踩坑节点都记录下来,后面写论文、做答辩PPT的时候就是现成的一手素材,比临时补实验靠谱得多。