手写字图片修复这事,我断断续续折腾了大半年。起因很朴素:手里有一堆扫描的老笔记、试卷截图,上面的手写批注比正文还抢眼。用PS仿制图章一点点抠,效率低到让人怀疑人生,而且修完总能看到明显的涂抹痕迹。后来转念一想,这事其实是个标准的图像修复问题——模型要做的,就是识别出哪些区域是手写笔迹,再用周围的背景纹理把这些区域重新“长”出来。于是就有了这个项目:PmrNet,一个专门针对手写字干扰场景的修复网络,名字的全称是Partial Mask Refinement Network,核心思路是先预测一个不完美的掩膜,再在修复过程中把掩膜和图像一起逐步细化。整套代码和数据集构建思路我都整理出来了,这篇文章就把里面的关键设计、训练细节和踩过的坑一次性讲透。
这个项目适合谁?如果你正在做图像修复、去水印、老照片还原,或者单纯想学掩膜预测类网络怎么搭,这篇文章应该能给你不少参考。你不一定需要多深的数学基础,但最好对PyTorch有一定了解。文里的每个设计决策,我都会讲清楚为什么这么做,以及我实际测试下来踩到了哪些问题。
1. 项目需求与方案选型:为什么自己炼丹,而不是直接套现成模型
1.1 手写字修复到底是个什么问题
先把需求拆清楚。手写字图片修复,输入是一张带有手写笔迹的图片,输出是去除笔迹后、背景完整连贯的干净图片。这个场景和普通的水印去除、划痕修复最大的区别在于:手写笔迹的形态极不稳定,同一个人的同一个字,换个姿势写出来就完全不一样;而且笔迹颜色和背景可能很接近,也可能形成强烈对比,比如红笔批注、铅笔草稿、墨迹洇染,这些都是实际的场景形态。
从图像修复的角度归类,这属于“非规则区域修复”。非规则的意思是,损坏区域的形状不是矩形、圆形这种规则几何体,而是根据内容任意变化的。传统方法对这种问题束手无策,因为它们的核心假设是“损坏区域的形状可知且规整”,修复算法只需要填充空白区域即可。但手写字不是空白,它是实实在在的、有颜色的像素覆盖,你得先判断哪些像素属于笔迹,哪些属于背景,然后只对笔迹像素做重建。这一步的判断,恰恰是深度神经网络最擅长的事情。
1.2 为什么不用传统图像处理方案
可能有人会说,我直接用阈值分割把深色像素提出来,然后插值填充不就行了?理论上可行,实际上全是坑。首先是笔迹颜色不统一,红笔、铅笔、蓝墨水的阈值完全不同,同一个算法换个笔就得重新调参数。其次是笔迹和背景的交界处,像素灰度往往是渐变的,硬阈值会把背景边缘也一起切掉,导致修复后出现一圈白边。更棘手的是,有些笔迹写在文字行间,去掉之后你希望露出的是原有的印刷文字,这就要求修复模型必须理解“这里原本有文字”,而不是简单地把缺口填平。
传统方法还有一个致命问题:完全没有语义认知。它不明白自己修的区域是什么,只是在做像素级别的插值。所以一旦损坏区域面积较大,或者背景纹理比较复杂,插值结果就是一团糊。我在早期试过OpenCV的inpaint算法,用Telea和Navier-Stokes两种方案,小区域效果尚可,但只要笔迹稍微粗一点、覆盖面积超过几十个像素,修复结果就开始出现明显的模糊条纹。这也是我下定决心自己训练专用网络的核心原因。
1.3 网络选型:生成式修复 vs 逐像素重建
深度图像修复的主流方案其实分两派。一派是生成式对抗网络,用生成器输出修复结果,判别器负责评判修复结果是否“真实”,两者对抗训练。优点是在大面积残缺区域的处理上效果惊艳,但训练极不稳定,调判别器和生成器的平衡就像踩钢丝,动不动就模式坍塌。另一派是逐像素重建,本质上是个回归任务——输出的每个像素值都直接和真实值算损失,网络只需要学会把掩膜区域的像素填得像样即可。
PmrNet站在了第二派这边,但不是纯回归,而是引入了一个关键的“掩膜细化”机制。原因很直观:手写笔迹的掩膜预测不可能百分之百准确,总会有边缘误差、遗漏区域和误检背景。与其要求模型必须在第一次就给出完美掩膜,不如让网络在修复过程中持续修正掩膜。这个思路是:将初始掩膜和待修复图像一起输入网络,网络在多个尺度上逐步细化掩膜,同时根据细化的掩膜重建图像内容。掩膜不再是一成不变的输入,而是参与整个前向传播过程、不断被校正的动态信息。
2. PmrNet网络结构拆解:掩膜细化与多尺度修复的核心设计
2.1 整体流程:三个模块的分工与协作
PmrNet的整体流程可以拆成三个环节。第一是掩膜初始化,这一步用传统的图像处理和简单规则,生成一个“看起来差不多”的初始掩膜。第二是掩膜细化与特征提取,这个阶段是网络的绝对核心。第三是图像重建,把细化后的掩膜信息与多尺度特征融合,逐像素输出修复结果。
这里需要强调一下掩膜初始化的作用。我们当然可以直接用神经网络从原图预测掩膜,但我实测下来,纯数据驱动的掩膜预测在背景复杂时容易把远处相似的背景误判为笔迹。反而用传统方法先给出一个粗糙但是“有依据”的掩膜,再让神经网络做局部修正,效果更稳。这也体现了Partial Mask(部分掩膜)设计的存在意义:网络不必从零开始发现所有笔迹,只需要在已有基础上做细化,任务简单了,自然更容易收敛。
2.2 掩膜细化分支:从粗糙到精细的迭代机制
掩膜细化模块的设计借鉴了老式图像分割算法里的“活动轮廓”思想。活动轮廓的大致逻辑是:初始化一个大致包围目标的轮廓,然后根据图像梯度等信息让轮廓逐步变形,直到贴合真实边缘。PmrNet里没有用传统的曲线演化,而是用卷积层代替了这个过程。
具体实现上,我设计了一个两分支结构。主分支接收初始掩膜和RGB图像拼接后的四通道输入,通过多层空洞卷积捕捉不同感受野下的上下文信息;辅助分支则直接从原始图像中提取边缘特征,帮助细化分支区分“笔迹内部”和“笔迹边缘”。两个分支的特征以门控方式融合,所谓门控,就是学习一个权重图,决定边缘特征在多大程度上影响掩膜更新。这样做的好处是:笔迹内部的粗大区域主要由主分支负责修正,而边缘处精细的锯齿状误差交给辅助分支解决,各司其职。
细化不是一次完成的,而是采用了“渐进式细化”策略。网络内部有三个不同分辨率的阶段,分别在原图尺度、二分之一尺度和四分之一尺度上依次修正掩膜。低分辨率阶段负责捕捉大范围的结构性错误,比如某条笔迹被整体遗漏了;高分辨率阶段负责修正细节误差,比如笔迹边缘多出或缺少几个像素。这种从粗到细的路径,比我一开始尝试的单尺度细化收敛更快,最终掩膜质量也明显更高。
2.3 特征提取骨干网络:空洞卷积与感受野的取舍
特征提取骨干采用编码器-解码器的对称结构,编码器经过四次下采样,从256×256降到16×16,通道数依次增加到64、128、256、512。下采样使得网络感受野变大,能够看到更大范围的背景上下文,但分辨率损失也很明显。为了弥补这一点,在编码器的最后两层使用了空洞卷积,膨胀率分别设为2和4,在不增加参数量的情况下进一步扩大感受野。
举个例子解释感受野的意义。假设你在修复一个面积较大的手写字块,字块本身可能已经覆盖了几十甚至上百像素。如果网络只看得到字块周围十几像素的范围,它根本不知道这个区域外面是什么纹理,自然无从修复。空洞卷积扩大了网络的观察半径,就像你在拼图时先退后两步看看整幅图的图案,而不是贴着拼图碎片找线索。
解码器部分使用双线性插值配合跳跃连接,将编码器各层的特征逐级恢复分辨率。跳跃连接在这里的作用非常直接:低层的特征保留了笔迹边缘的锐利细节,高层的特征包含了背景结构信息,两者融合后,重建出的图像既有结构上的连贯性,又不会因为过度平滑而显得“塑料感”很重。
2.4 重建头与掩膜指导的动态融合
最后一个重建模块,把细化后的掩膜和骨干特征融合,生成修复图像。这里有个细节值得多说一句:普通修复网络通常直接把RGB图像和掩膜拼在一起送进网络,掩膜作为额外通道贯穿整条路径。但PmrNet在重建阶段做了一次“显式混合”——用细化掩膜作为权重,对输入图像和网络生成图像做加权平均。
具体公式可以写成:最终输出 = 细化掩膜 × 原始图像 + (1 - 细化掩膜) × 网络重建图。这个设计的逻辑是:掩膜覆盖的地方是笔迹区域,用网络重建图替代;掩膜没有覆盖的地方本来就是干净的背景,直接保留原始像素,避免网络在无意义区域上做无效重建。坦白说,这是受了图像编辑软件里“图层蒙版”概念的启发。把这个混合操作放进网络训练循环后,我发现模型收敛速度显著加快,修复结果也更干净,因为网络不需要浪费精力去恢复那些根本没被损坏的像素,只需要专注于掩膜指示的区域。
网络训练代码中重建头部分的关键结构如下:
class ReconstructionHead(nn.Module): def __init__(self, in_channels=512, out_channels=3): super().__init__() self.up1 = nn.Sequential( nn.Conv2d(in_channels, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True)) self.up2 = nn.Sequential( nn.Conv2d(256, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True)) self.up3 = nn.Sequential( nn.Conv2d(128, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True)) self.out = nn.Conv2d(64, out_channels, 3, padding=1) def forward(self, x, refined_mask): x = F.interpolate(x, scale_factor=2, mode='bilinear', align_corners=False) x = self.up1(x) x = F.interpolate(x, scale_factor=2, mode='bilinear', align_corners=False) x = self.up2(x) x = F.interpolate(x, scale_factor=2, mode='bilinear', align_corners=False) x = self.up3(x) generated = torch.tanh(self.out(x)) # 掩膜加权混合 mask_3ch = refined_mask.repeat(1, 3, 1, 1) result = mask_3ch * generated + (1 - mask_3ch) * x return result, generated3. 数据集构建与预处理:没有现成数据,就自己造一个
3.1 数据问题:开放数据集为什么不够用
训练这类修复网络最头疼的事情,不是网络结构设计,而是数据。公开的修复数据集大多是针对“规则矩形缺失区域”或者“自然物体缺失”设计的,用手写字作为损坏类型的公开数据集非常少。但这不代表项目做不了。既然真实数据难找,那就自己造,合成数据在这个场景下是有说服力的。
合成数据的核心逻辑很直白:找一堆干净的背景图,在上面随机位置、随机角度、随机字体、随机颜色贴上一些模拟手写字,这样我们就同时有了“损坏图”和“真值图”,也就能自动生成对应的掩膜了。听起来简单,但要把合成数据做得“像真的”,里面有不少细节。
3.2 手写笔迹的模拟:字体、墨迹与物理效果
手写笔迹最大的特征是随机性。同一个字,普通人写出来每次都不一样,所以合成时不能只用一两种字体。我在实践中准备了大约十几种手写风格字体,并且在渲染时给字体施加随机小角度旋转、随机缩放、随机水平错切。错切是什么?就是让字形产生一种向右倾斜、被“推了一把”的变形效果,模拟写字时手部倾斜带来的笔迹侧倾。这些几何变换幅度都不大,但组合起来就能让文字看起来不那么像印刷体。
墨迹效果是另一个关键。真实手写笔迹,笔尖力度不同,墨迹的浓度也不均匀。我的处理方式是:渲染文字时先生成灰度掩膜,然后对掩膜做高斯模糊,模糊半径随机取0.3到1.5像素,再把模糊后的值映射到透明度上。这样一来,笔迹中心浓度高、边缘浓度低,模拟出墨水在纸张上轻微洇开的效果。墨迹颜色也做了随机采样,涵盖了红笔、蓝笔、黑笔、铅笔灰等常见类型,确保模型不会对颜色产生偏见。
3.3 背景多样性:从纯色纸到真实扫描件
背景方面,如果只用纯白背景,训练出的模型一遇到真实扫描件就会崩。因为扫描纸张有纹理、有底色偏黄、可能有网格线、有之前的印刷文字,甚至还有折痕和污渍。我的做法是把背景分为三个层次:第一层是纯色纸,占30%,给模型打好基础;第二层是带纹理的纸面,比如细腻的布纹、颗粒感较强的再生纸、老旧的泛黄纸页,占40%;第三层是真实扫描场景,我用手机拍摄了不同纸张在不同光照下的照片,以及从网络上收集的老笔记扫描件,占30%。
这里要特别提醒一点:第三层数据里的干净背景图要严格保证“干净”,也就是不能包含任何手写痕迹。因为我们在合成时会在背景上叠加手写文字,如果背景里本来就有文字或笔迹,生成的掩膜就会把真实背景中的内容误判为待修复区域,给训练数据引入大量噪声。我最初就吃过这个亏:用了一批包含印刷文字的扫描件做背景,结果模型在验证集上PSNR一直卡在28dB上不去,排查了很久才发现是背景自我污染的问题。后来加了一道人工筛查流程,只保留完全没有文字内容的纸面背景,问题才得到解决。
3.4 数据增强与目录结构
数据增强策略上,我用了随机亮度、对比度、饱和度的调整,以及随机的局部模糊和噪声注入。特别值得注意的是,我没有做随机的水平翻转。原因很简单:手写文字翻转后会改变笔画方向,比如横折钩经过水平翻转就变成了类似镜像的形态,这在真实场景中几乎不存在,反而是给学习任务引入了错误先验。
最终的数据集目录结构如下:
dataset/ train/ clean/ # 干净背景图,用于合成 corrupted/ # 合成后的带笔迹图片 mask/ # 笔迹掩膜,与corrupted一一对应 val/ clean/ corrupted/ mask/合成流程用Python脚本批量生成,每一轮迭代会随机组合背景、文字内容、字体、颜色、几何变换,保证样本的丰富度。我最终合成了约四万张训练图片,验证集两千张,配合后续的数据增强,足够撑起一个两千万参数级别的网络训练。
4. 训练过程与核心细节:损失函数、优化器与收敛策略
4.1 损失函数设计:多目标联合怎么平衡
训练PmrNet用到的损失函数是联合的,包含四个部分。重建L1损失是基础,计算修复结果和真值图之间逐像素绝对值差距,它保证了修复结果和原图的整体接近程度。感知损失用预训练的VGG网络提取中间层特征,在特征空间计算差距。它的作用,是让修复结果在视觉感知上更像真实图像,而不是在像素层面精打细算。
感知损失解决的是一个实际问题:纯L1损失训练出的模型,修复区域容易出现整体偏色或者纹理过于平滑。这是因为L1损失关心的是“平均绝对误差最小”,它对高频细节不敏感。感知损失通过高层特征约束了修复结果的语义内容,迫使网络在纹理和结构上更像原图,而不是简单地平均化。
结构一致性损失用来强化边缘区域的锐利度,我是用拉普拉斯算子提取真值和修复结果的边缘轮廓,再计算它们之间的L1距离。这个损失分量直接对边界处的边缘信息施加惩罚,避免修复后的笔迹区域边缘产生模糊的过渡带。掩膜平滑损失则对细化掩膜施加总变分正则,鼓励掩膜在平滑区域内保持连续,而不是出现大量的椒盐噪点。掩膜如果本身噪声很大,重建模块就会根据错误的信息去“修复”本来就不需要修复的区域,导致背景被改动。
联合损失表达式如下:
total_loss = 10.0 * L1_loss + 0.1 * perceptual_loss + 0.5 * edge_loss + 0.05 * mask_tv_loss各组件的权重系数不是一次就确定的,我前后调了四轮。第一轮L1权重过高,模型学了个“复制粘贴”——凡是掩膜区域就用周围颜色直接糊上,短期指标好看,但细节完全缺失。后来降低了L1权重、加入感知损失,效果立刻改善,但边缘又开始发虚。最终把边缘损失加上,才算稳定下来。
4.2 优化器与学习率策略
优化器用的Adam,初始学习率2e-4,beta设置为默认的(0.9, 0.999)。批大小选了8,输入尺寸256×256。在RTX级别的显卡上,这个配置显存占用大约6到8G,训练速度相对可以接受。学习率调度用了余弦退火,总共训练300个epoch,前30个epoch采用线性warmup从5e-5升到2e-4,避免模型在初始阶段因为学习率过大而震荡。
完整训练配置参考如下:
| 配置项 | 数值 | 说明 |
|---|---|---|
| 输入尺寸 | 256 × 256 | 统一缩放,保持纵横比不变 |
| 批大小 | 8 | 显存受限时降到4,配合梯度累积 |
| 初始学习率 | 2e-4 | Adam优化器 |
| Warmup | 30 epochs | 线性升温 |
| 调度策略 | Cosine Annealing | 总周期300轮 |
| 权重初始化 | Kaiming Normal | 激活函数前的卷积层 |
| 归一化 | BatchNorm | 配合小批大小 |
| 梯度裁剪 | 最大范数1.0 | 防止梯度爆炸 |
4.3 训练监控与细节技巧
训练过程中我重点盯三个指标:训练集上的损失值、验证集上的PSNR和SSIM、以及每隔若干轮保存的样本图。只盯损失数值远远不够,损失下降掩盖不了修复结果里的大片伪影,因此定期可视化样本图是必须养成的习惯。我每五个epoch保存一组验证样本的修复对比图,肉眼检查修复区域是否自然,是否有明显的颜色不连续、纹理断裂和边缘白边。
还有一个实践中的小技巧:混用不同难度的样本。训练初期,大部分是笔迹面积小、形态简单或者背景干净的样本,帮助模型快速掌握基本修复能力。从第150轮开始,逐步加入笔迹面积大、背景纹理复杂的难样本,让模型逐渐适应更严峻的场景。直接一开始就混着训练,模型容易被难样本带偏,损失下不去;分阶段训练能让学习过程更平滑。
4.4 模型参数规模与训练开销
PmrNet的参数总量大约在1800万左右,其中骨干特征提取网络占了大头,约1200万参数,掩膜细化分支约400万,重建头约200万。这个规模在各家基础网络面前不算大,属于“够用就行”的范畴。实测下来,单卡训练300轮大约耗时18个小时左右。如果显存不够,将批大小降到4,同时开启梯度累积,每两步累积一次梯度,效果几乎没有差别,但显存可以省下来。
5. 评估方法与效果分析:指标怎么选,结果怎么看
5.1 客观指标:PSNR、SSIM与LPIPS
评估修复质量我用了三个客观指标。PSNR表示峰值信噪比,值越高代表像素级失真越小,但它有一个众所周知的缺陷:在某些情况下与人的主观视觉感受不一致。一个PSNR很高的图像可能看起来很模糊,因为平均误差小的图像通常更平滑。所以只报PSNR是不值得信任的。
SSIM衡量两张图的结构相似性,比PSNR更贴近人的感知。它从亮度、对比度、结构三个维度分别比较两个图像块的相似度,取值范围从-1到1,越接近1代表越像。在笔迹修复场景,SSIM提升0.01都是一个肉眼可感知的改进。
LPIPS是深度学习时代的感知相似度指标,它直接用预训练网络的中间层特征来度量两张图的距离。我在实际对比中发现,有时候PSNR只提升了0.2dB,但LPIPS明显下降,说明人眼观感大幅改善。所以在最终汇报模型效果时,我会同时报三个指标。
5.2 消融实验与效果对比
消融实验的结果比较清晰。去掉掩膜细化分支,只保留初始掩膜直接参与重建,PSNR下降约1.7dB,修复结果的边缘位置出现大量渗色现象,也就是笔迹边缘周围一圈被涂抹的痕迹。去掉渐进式细化、改成单尺度,损失收敛速度明显变慢,最终PSNR下降约0.9dB,对大笔迹区域的修复完整性变差。去掉门控融合、改为直接相加辅助特征,效果也有小幅下降,高频边缘区域有轻微金属感衍射伪影。
这些结果说明两个问题:第一,掩膜细化机制带来的增益是稳定且显著的,它让网络能够根据图像内容动态修正初始掩膜的误差;第二,渐进式和门控融合虽然不是决定性环节,但在细节质量上起着锦上添花的作用。
5.3 推理性能与落地考虑
推理性能我用实际环境做了测试。一张256×256的图片,单次前向推理在消费级显卡上耗时约15到25毫秒,CPU上约120到180毫秒。这个速度对于静态图片修复场景完全够用,甚至支持批量处理。如果要部署到移动端或者Web端,可以做量化处理,把权重从FP32降到INT8,模型体积从大约72MB缩减到18MB左右,推理速度能提升数倍,代价是PSNR下降约0.4dB,对于一些要求不高的预览场景完全可接受。
6. 常见问题与排查技巧实录
6.1 训练损失不下降
这部分经验价值最高,每一条都有对应的排查路径。训练损失不下降,首先要排查输入数据是否正常。我遇到过一次很隐蔽的问题:合成脚本中掩膜和损坏图没有严格对齐,因为某一批图片在裁剪时用了不同的随机裁剪参数,导致掩膜整体偏移了几个像素。网络要同时面对错位的输入和错位的监督信号,等于任务本身都定义错了,自然学不出来。排查方法是把每个batch的输入图、真值图、掩膜拼成一张大图可视化,用肉眼检查它们之间的对齐关系。
如果数据没问题,再看学习率。学习率过大时,损失曲线通常会出现锯齿状震荡;学习率过小则表现为平滑但极其缓慢的下降。我习惯把训练前50轮放入一个“快速验证”流程:用一个较小的子数据集,跑50轮,画损失曲线,看是否能在合理范围内收敛。这个流程能在一小时内暴露大部分训练配置层面的问题,而不是等到训练了10个小时后才发现方向错了。
6.2 修复结果出现色斑或伪影
色斑通常有两种来源。第一种是感知损失的权重过高,逼着网络在特征空间“过度模仿”,导致局部区域产生了不符合物理规律的像素组合。第二种是掩膜误检导致重建模块在原本干净的背景区域也执行了重建操作。修复方法是先检查背景区域是否被模型改动过,如果要改动的区域占比超过5%,基本都是掩膜预测出了问题,需要加强掩膜平滑损失。
伪影还有个低频但让人头疼的来源:BatchNorm在训练和推理时行为不一致。训练时BatchNorm使用当前batch的均值和方差做归一化,推理时使用的是训练阶段累计的滑动平均。一旦训练集和真实场景数据分布差异较大,推理结果就可能出现奇怪的颜色偏移。解决方案是训练结束后用一部分验证集数据做一次统计校准,或者直接用GroupNorm替代BatchNorm。
6.3 小字迹清除干净,大字迹修复模糊
这是一个典型的“尺度不平衡”问题。训练数据中笔迹面积的分布不均匀,小笔迹样本占大多数,模型对大面积笔迹的学习不充分。解决方法是调整合成数据的分布:把笔迹面积从10%到40%区间内均匀分布,而不是让大量样本集中在5%以下的小面积区域。加入这一调整后,模型对大块笔迹的修复能力显著提升,边缘位置也不再出现因为上下文不足导致的模糊。
6.4 修复区域边缘出现白边
白边几乎是所有修复模型都会遇到的边缘问题,根源在于掩膜边缘和真实笔迹边缘没有完全对齐。掩膜稍微比真实笔迹小一圈,原始像素就有残留;掩膜稍微大一整圈,重建区域就会覆盖到背景。我的经验是:在掩膜真值生成时,对掩膜做一次轻微的膨胀,大约2到3像素,同时配合边缘损失对修复结果进行约束。膨胀让模型学习“稍微多修一点,也不要不修”,边缘损失保证多修的这部分要有正确的纹理延续,而不是简单地露出背景色。
我遇到过一个极端情况:当笔迹边缘和背景对比度极低时,边缘损失会让网络刻意生成一条“虚拟边界”,反而让修复区域更突兀。后来我把边缘损失的权重做了动态衰减——在前100轮权重保持0.5,之后逐步降低到0.1。这样做的逻辑是,训练初期边缘损失帮助网络快速学会结构布局,训练后期则更依赖感知损失和重建损失来优化细节,避免过强的边缘先验束缚模型的灵活性。
6.5 显存不足
显存不足是最容易解决的问题。把批大小降到4是最直接的方案。还不行,就开启梯度累积,每两步再更新一次参数,效果等价于批大小为8。再不够,可以关闭输入图像的随机裁剪,把训练尺寸从256降到192,感受野天差地别但显存占用降低将近一半。
6.6 真实场景测试效果总是不如验证集
验证集也是合成数据,和真实场景天然存在分布差异,所以这个现象几乎是必然的。缓解方法是在模型基本训练成型后,用一批人工标注的真实手写图片做几十轮的微调。微调时学习率要大幅降低,比如降到5e-6,并配合早停,防止模型在少量真实数据上过拟合。经过微调的模型,在真实扫描件上的主观效果提升非常明显,PSNR可能只上升了0.3dB,但人眼看到的修复结果会更自然、更可信。
6.7 一点经验总结
回看整个项目,最重要的经验其实不是某个网络结构设计得巧妙,而是“先定义清楚问题,再选择技术路径”这个过程。手写字修复表面上是图像修复问题,但真正解决起来最关键的是掩膜质量和对笔迹形态的模拟。如果一开始就直接套用现成的修复网络,不做针对性的数据设计和细化机制,结果大概率是模型在公开数据集上表现得很好,一到真实笔迹场景就露馅。
另一个让我印象深刻的体会是,混合加权这个“小技巧”带来的收益远超预期。它没有增加任何参数,只是改变了一次特征融合的方式,就把网络从“什么都想学”变成了“只修需要修的”。这种思路在处理其他图像编辑任务时也值得借鉴——当你觉得模型在无用区域上浪费太多注意力时,不妨试试用某种形式的掩膜或权重,把模型的注意力强制聚焦到真正需要修改的区域上。
后面我计划把这个网络结构推广到表格去除、日期印章去除等场景,核心的掩膜细化机制应该能平滑迁移。如果你在复现过程中训练阶段出了问题,优先检查数据对齐和损失权重这两个方向,八成问题都出在这两处。