日前,Flipboard 软件工程师 Yuchen Tian 在 GitHub 上发布了用于汉字字体的神经风格迁移的项目,该项目介绍了如何通过神经网络学习设计汉字新字体的方法。机器之心授权编译发布。
项目地址:https://github.com/kaonashi-tyc/Rewrite
创建字体是一件难事,创建汉字字体更是艰难。要做一个兼容 GBK(中国政府设定的字符集标准)的字体,设计师需要为超过 26000 个不同的汉字字符设计外观,这是一项艰巨的工作,可能需要数年时间才能完成。
可不可以让设计师仅设计其中一部分字符的字体,然后让计算机来确定剩下的字符应该是什么模样呢?毕竟,汉字是由一些被称为「偏旁部首」的基本元素构成的;在不同的汉字上,相同的偏旁部首看起来也都相当雷同。
这个项目是使用深度学习的一个探索性的应用。具体而言,整个字体设计流程可被表示成一个风格迁移问题(style transfer problem)——将标准样式字体(比如 SIMSUN 体)转换为目标风格的字体。本项目通过向一个神经网络提供配对样本的子集来训练该神经网络近似学会两种字体设计之间的转换。一旦学习完成,该神经网络就可被用来推理其它字符的外形。下面的框图大概说明了这个思想:
这个项目的灵感很大程度上来自于 Erik Bernhardsson 的博客《Analyzing 50k fonts using deep neural networks》和 Shumeet Baluja 的好论文《Learning Typographic Style》。
在尝试过了各种不同的架构(包括具有残差(residuals)和去卷积(deconvolution)的更复杂的架构)之后,我最终选择了一种更为传统的自上而下的 CNN 架构,如下所示。
每一个卷积层之后都有一个批规范化(Batch Normalization)层,然后是一个 ReLU 层,然后一直向下零填充(zero padding)。
该网络基于预测输出和 ground truth 之间的像素级的平均绝对误差(MAE)进行最小化,而没有使用 Erik 的博客中提到的更常用的均方误差(MSE)。MAE 往往能产出更锐利和更清晰的图像,而 MSE 则会得到更模糊和灰蒙蒙的结果。另外为了图像的平滑度,还使用了总变差损失(total variation loss)。
层数 n 是可配置的,更大的 n 往往会生成更详细和更清晰的输出,但也需要更长的训练时间,通常的选择是在 [2, 4] 之间。大于 4 的时候似乎会达到收益递减的点,即虽然运行时间增加了,但在损失或输出上不会有明显的提升。
大卷积能带来更好的细节。在我的实验过程中,我开始使用的是堆叠的平直的 3×3 卷积,但它最后表现并不好或无法在更困难和更奇异的字体上收敛。所以最后我选择了这种涓滴形状的架构(trickling down shape architecture),其不同的层有不同大小的卷积,每一个都具有差不多相同数量的参数,所以该网络可以获取不同层面的细节。
dropout 是收敛(convergence)的基础。没有它,该网络就只能放弃或受困于毫无价值的解决方案,比如全白或全黑图像。
在 Erik 和 Shumeet 的工作中使用的全连接层(fully-Connected layers)对汉字字符的效果不是非常好,会生成噪声更多和不稳定的输出。我猜想是汉字字符的结构比字母的结构要远远复杂得多,而且从本质上来说更接近于图像,所以一个基于 CNN 的方法在这种情况下更为合理。
和真实世界图像不一样的是,我们可以生成任意分辨率的字符图像。我们可以对这个事实加以利用:用高分辨率的源图像来逼近低分辨率的目标,从而可以保留更多的细节以及避免模糊和噪声。
上图展示了模型在多种字体上的训练过程中的验证集上所取得的进展。它们全都在 2000 个样本上进行了训练,层数设置为 3。看该模型如何从随机噪声收敛是很有意思的:首先获取一个字符的可识别的形状,然后获取更为细微的细节。下面是训练过程中某种字体的进展过程。
下面的图像给出了对比 ground truth 的预测结果。对于每一种字体,我们都选取了最常用的 2000 个字作为训练集,运行 3000 次迭代。另外还有一个包含 100 个字的测试集用于推理。对于所有的字体,源字体都是 SIMSUN。
对于其中大部分字体,该网络都能成功做出合理的猜测。实际上其中一些还跟 ground truth 非常接近。另外值得一提的是,该网络还保留了微小的但可辨认的细节,比如偏旁部首的弯曲的端部。
但正如许多其它的神经网络所驱动的应用一样,当该网络出错时,就会错得非常离谱。对于一些字体(尤其是笔画粗的字体),它只会得到一些模糊的字迹斑点。另一方面,对于这些笔画粗的字体,它会失去让该字符可被辨认的关键的空白处细节,而只会获取到整体的轮廓。即使是在成功的案例中,偏旁部首的损失问题似乎也很常见。此外,网络似乎在宋体上可以做的更好,但在楷体上表现并不好,这主要是因为 SIMSUN 字体本身也是一种宋体。
因为空间的限制,对于每一种字体,我们从测试集中仅随机取样了一个字符。如果你想看到在更大的字符测试集上的结果,请查阅这里。
2000 字也许只有 GBK 集的 10%,但也仍然很多了。这个数字是我靠直觉选择的,而且看起来这个选择在很多字体上都表现不错。但必需这么多吗?
为了搞清这一点,我选择了一种字体(在每种字体上都进行这个实验会太耗时间)进行了不同数量的训练样本的实验,数量的范围是从 500 到 2000,然后让该模型在一个常用的测试集上对字符进行渲染,下面是得到的结果。
从上到下,上图分别给出了训练集大小从 500 到 2000 增长时的不同结果。当训练集大小在 1500 到 2000 之间,表现的提升会变得更小,这表明 sweet point 就在这之间的某个地方。
要使用这个软件包,需要安装 TensorFlow(已在 0.10.0 上测试过)。其它的 Python 要求列在 requirements.txt 文件里。另外强烈推荐使用 GPU——如果你想在合理的时间内看到结果。
我的所有实验都运行在一个 Nvidia GTX 1080 上。以 16 的 batch 大小进行了 3000 次迭代,这需要小型模型计算 20 分钟、中规模模型计算 80 分钟、大模型则需要 2 小时。
在训练之前,你需要运行预处理脚本为源字体和目标字体生成字符位图(character bitmap):
python preprocess.py --source_font src.ttf \ --target_font tgt.otf \ --char_list charsets/top_3000_simplified.txt \ --save_dir path_to_save_bitmap
该预处理脚本支持 TrueType 和 OpenType 字体,它会获取一个字符列表(一些常见的字符集内置于本 repo 的 charsets 目录中,比如 3000 个最常用的简体汉字字符),然后将这些字符的位图保存为 .npy 格式。对于源字体,每个字体会被默认保存为字体大小为 128 的 160×160 的尺寸,而目标字体则是字体大小为 64 的 80×80 的尺寸。这里并不需要特别的对齐,只要确保字符不被截断即可。
在预处理步骤之后,你就能得到源字体和目标字体的位图,分别是 src.npy 和 tgt.npy,然后运行以下命令开始实际的训练:
python rewrite.py --mode=train \ --model=medium \ --source_font=src.npy \ --target_font=tgt.npy \ --iter=3000 \ --num_examples=2100 \ --num_validations=100 \ --tv=0.0001 \ --keep_prob=0.9 \ --num_ckpt=10 \ --ckpt_dir=path_to_save_checkpoints \ --summary_dir=path_to_save_summaries\ --frame_dir=path_to_save_frames
这里给出了一些解释:
mode:可以是 train 或 infer,前者不言而喻,后者我们将在后面讨论
model:表示模型的大小。它有三个可用选择:small、medium 或 big,分别对应的层数为 2、3、4
tv:总变差损失(total variation loss)的权重,默认 0.0001。如果输出看起来是损坏的或有波动,你可以选择增大权重迫使模型生成更平滑的输出。
keep_prob:表示训练过程中一个值通过 dropout 层的概率。这实际上是一个非常重要的参数——这个概率越高,图像越锐利,但输出可能会损坏。如果结果不好,你可以尝试减小这个值,这会得到噪声更多但更圆润的形状。通常的选择是 0.5 或 0.9
ckpt_dir:用于保存模型的 checkpoint 的目录,用于后续的推理步骤
summary_dir:如果你想使用 TensorBoard 来可视化一些指标(比如迭代中的损失),这就是保存所有总结的地方。默认在 /tmp/summary。你可以检查训练 batch 的损失,以及验证集上的损失及其 breakdown
frame_dir:保存在验证集上获取的输出的目录。用于选出用于推理的最好模型。在训练之后,你也可以找到一个名为 transition.gif 的文件,可以看到该模型在训练过程中的进展动画,同样也在验证集上。
对于其它选择,你可以使用 -h 查看确切的使用案例。
假设我们最后完成了训练(终于完成了!),我们就可以使用前面所提到的 infer 模式了,看模型在之前从未见过的字符上表现如何。你可以在 frame_dir 中参考获取的帧来帮助你选择你最满意的模型(说明一下:通常不是误差最小的那个)。运行以下命令:
python rewrite.py --mode=infer \ --model=medium \ --source_font=src.npy \ --ckpt=path_to_your_favorite_model_checkpoints \ --bitmap_dir=path_to_save_the_inferred_output
注意这里的 source_font 可以不同于训练中所使用的那个。事实上,它甚至可以是任何其它字体。但最好选择相同或相似的字体进行推理,以得到最佳的结果。在推理之后,你将能找到所有输出字符的图像序列以及一个包含了这些推理出的字符位图的 npy 文件。
该项目只是一个个人项目,来帮助我学习和理解 TensorFlow,但也顺利发展成了一件更为有趣的事,所以我认为值得与更多的人分享。
目前,该网络一次只能学习一种风格,如何将它扩展到一次性掌握多种风格会是一件有趣的事。2000 个字符比完整 GBK 数据集的 10% 还少,但它还是比较多的,少于 100 字符的情况下有可能学习到字体的风格吗?我的猜测是 GAN 可能会对此有所帮助。
在网络的设计上,该架构被证明在不同的字体上是有效的,但每个卷积层的数量的优化还需要搞清楚,或者一些卷积层是否有必要?
我想要探索的另一个有趣的方向是创造混合多种风格的字体。在损失函数上简单结合两种字体的表现并不好。可能我们应该为字体单独训练一个 VGG 网络,然后劫持(hijacking)特征映射?或者在网络设计上使用潜在更多的新变化,从而解决这个问题?
最后,该项目证明了更专门化的应用深度学习的可能性,CNN 帮助加速了汉字字体的设计流程。研究的结果很是振奋人心,但并非从无到有的创造新字体,这也不是该项目的内容。
谷歌的 TensorFlow 教程
来自 Justin Johnson 的在快速神经风格迁移网络上的补充材料
来自 Ian Goodfellow 的视频。非常好的东西,非常实用,说明了很多如何使用深度学习解决问题的要点;看完之后,我就觉得没必要自己再写一份笔记了。
感谢我的朋友 Guy 帮助我在合理的预算内搭建了一台 PC
GPLv3