目标:掌握在 Hugo 博客中使用 matplotlib 预生成 SVG 矢量图的完整工作流,包括配置、中文渲染、配色方案、批量生成脚本和常见问题排查。

前置要求:基本的 Python 和 matplotlib 使用经验,了解 Hugo 静态站点的基本结构。

在撰写技术博客时,公式、表格和文字往往不足以表达所有信息——函数图像、架构示意图、对比图表等可视化内容能大幅提升可读性。本文总结了在 Hugo + KaTeX 环境下,使用 matplotlib 生成 SVG 图片并嵌入 Markdown 的完整实践经验。


1. 为什么选择 matplotlib + SVG

1.1 为什么不用 LaTeX 原生绘图

  KaTeX 是 Hugo 中最常用的数学公式渲染方案,但它只支持公式渲染,不支持 TikZ、PGFPlots 等 LaTeX 绘图包。想在 Hugo 中直接用 LaTeX 画函数图像是不可行的。

1.2 为什么选择 matplotlib

  matplotlib 是 Python 生态中最成熟的绑图库,优势在于:

  • 函数绘图精确:支持 NumPy 数学表达式,可以精确绘制任意函数。
  • 输出格式丰富:支持 SVG、PNG、PDF 等多种格式。
  • 可编程:用脚本批量生成,修改方便,版本可控。

1.3 为什么选择 SVG 格式

特性SVGPNG
矢量/栅格矢量(任意缩放不失真)栅格(放大后模糊)
文件大小通常更小(20~60KB)同等质量下更大
渲染清晰度任意分辨率下都清晰取决于 DPI 设置
文本可选中是(SEO 友好)
浏览器兼容所有现代浏览器所有浏览器

结论:对于函数图像、流程图、概念示意图等,SVG 是首选格式。只有截图、照片等像素图像才需要 PNG。


2. 基础配置

2.1 最小可用脚本

import numpy as np
import matplotlib
matplotlib.use('Agg')  # 必须在 import pyplot 之前调用,使用非交互式后端
import matplotlib.pyplot as plt

# 创建画布
fig, ax = plt.subplots()

# 绑图
x = np.linspace(-3, 3, 200)
y = x ** 2
ax.plot(x, y, color='#2563eb', linewidth=2)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('y = x²')

# 保存为 SVG
fig.savefig('output.svg', format='svg', bbox_inches='tight')
plt.close(fig)

关键点matplotlib.use('Agg') 必须在 import matplotlib.pyplot 之前调用,否则在无 GUI 环境(如 CI/CD、远程服务器)中会报错。

2.2 全局样式设置

  在脚本开头统一设置样式,避免每张图单独配置:

plt.rcParams.update({
    'font.size': 12,
    'axes.titlesize': 14,
    'axes.labelsize': 12,
    'figure.facecolor': 'white',      # 透明背景在暗色主题下不可见
    'axes.facecolor': '#fafafa',       # 浅灰色坐标区背景
    'axes.grid': True,
    'grid.alpha': 0.3,
    'lines.linewidth': 2,
    'figure.figsize': (8, 5),          # 默认画布大小
    'savefig.dpi': 150,                # PNG 的 DPI(SVG 忽略此参数)
    'savefig.bbox': 'tight',           # 自动裁剪多余空白
    'savefig.pad_inches': 0.2,         # 保留少量边距
})

3. 中文支持——最常见的坑

3.1 问题现象

  matplotlib 默认字体不包含中文字形,图表中的中文会显示为方块()或报错:

Font 'default' does not have a glyph for '\u5347' [U+5347], substituting with a dummy symbol.

3.2 解决方案:指定中文字体

plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei',
                                     'Noto Sans CJK SC', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示为方块的问题

字体优先级:系统会按列表顺序查找第一个可用字体。

操作系统推荐字体
WindowsSimHei、Microsoft YaHei
macOSPingFang SC、Heiti SC
LinuxWenQuanYi Micro Hei、Noto Sans CJK SC

3.3 更稳妥的方案:避免中文

  如果图表用于英文博客或不需要中文,最稳妥的做法是直接用英文标注,完全避免字体问题:

# 推荐:用英文标注
ax.set_title('FFN Structure: Expand -> Activate -> Compress')
ax.annotate('$d_{model} \\to d_{ff}$ (expand)', ...)

  如果必须用中文,可以将中文放在 Markdown 正文中,图表本身只用英文——这是我在系列博客中采用的策略。


4. 配色方案

4.1 推荐配色表

  定义一套统一的配色字典,在所有图表中复用:

COLORS = {
    'blue': '#2563eb',    # 主色:线条、主要数据
    'red': '#dc2626',     # 警告/对比:异常值、错误梯度
    'green': '#16a34a',   # 正面/正确:正确答案、推荐方案
    'orange': '#ea580c',  # 次要强调
    'purple': '#9333ea',  # 第三类数据
    'cyan': '#0891b2',    # 第四类数据
    'pink': '#db2777',    # 第五类数据
}

4.2 配色原则

  • 不超过 5~6 种颜色:超过后区分度下降。
  • 红绿对比:用于"错误 vs 正确"的语义对比。
  • 蓝橙对比:用于两个方案的对比。
  • 透明度(alpha):用于重叠区域或背景填充,避免遮挡。

5. 常用图表类型与模板

5.1 函数曲线对比图

fig, ax = plt.subplots()
x = np.linspace(-3, 3, 200)
ax.plot(x, x**2, color=COLORS['blue'], label='MSE')
ax.plot(x, np.abs(x), color=COLORS['red'], label='MAE')
ax.set_xlabel('$y - \\hat{y}$')
ax.set_ylabel('Loss')
ax.set_title('MSE vs MAE')
ax.legend()
ax.axhline(0, color='gray', linestyle='--', alpha=0.5)
fig.savefig('mse_vs_mae.svg', format='svg')
plt.close(fig)

5.2 热力图(注意力矩阵)

fig, ax = plt.subplots(figsize=(6, 5))
data = np.random.rand(8, 8)
im = ax.imshow(data, cmap='Blues', aspect='equal')
ax.set_xticks(range(8))
ax.set_yticklabels([f't{i+1}' for i in range(8)])
for i in range(8):
    for j in range(8):
        ax.text(j, i, f'{data[i,j]:.2f}', ha='center', va='center', fontsize=8)
fig.colorbar(im)
fig.savefig('heatmap.svg', format='svg')
plt.close(fig)

5.3 柱状图

fig, ax = plt.subplots()
formats = ['FP32', 'FP16', 'INT8', 'INT4']
sizes = [28, 14, 7, 3.5]
bars = ax.bar(formats, sizes, color=[COLORS['blue'], COLORS['green'],
                                       COLORS['orange'], COLORS['red']], alpha=0.7)
for bar, size in zip(bars, sizes):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
            f'{size} GB', ha='center', fontsize=10)
ax.set_ylabel('Model Size (GB)')
fig.savefig('bar_chart.svg', format='svg')
plt.close(fig)

5.4 流程图(矩形+箭头)

fig, ax = plt.subplots(figsize=(10, 4))
boxes = [
    (1, 'Input', '#2563eb'),
    (3, 'Process', '#16a34a'),
    (5, 'Output', '#9333ea'),
]
for x, label, color in boxes:
    ax.add_patch(plt.Rectangle((x-0.7, 0.2), 1.4, 0.6,
                                facecolor=color, alpha=0.3, edgecolor=color, linewidth=2))
    ax.text(x, 0.5, label, ha='center', va='center', fontsize=11, fontweight='bold')
for i in range(len(boxes)-1):
    ax.annotate('', xy=(boxes[i+1][0]-0.7, 0.5), xytext=(boxes[i][0]+0.7, 0.5),
                arrowprops=dict(arrowstyle='->', lw=2, color='gray'))
ax.set_xlim(0, 6.5)
ax.axis('off')
ax.set_title('Pipeline', fontsize=14)
fig.savefig('pipeline.svg', format='svg')
plt.close(fig)

6. 在 Hugo 中集成

6.1 目录结构

static/
  img/
    article-name/          # 按文章名归档
      chart1.svg
      chart2.svg
content/
  posts/
    tech/
      article.md           # 引用图片

6.2 Markdown 引用

![图表描述](/img/article-name/chart1.svg)
  • 路径以 /img/ 开头(对应 static/img/
  • 必须填写 alt 文本(SEO + 无障碍)
  • Hugo 构建时自动将 static/img/ 复制到 public/img/

6.3 批量生成脚本模板

import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import os

os.makedirs('static/img/article-name', exist_ok=True)

# 统一样式
plt.rcParams.update({
    'font.size': 12, 'axes.titlesize': 14,
    'figure.facecolor': 'white', 'axes.facecolor': '#fafafa',
    'axes.grid': True, 'grid.alpha': 0.3,
    'lines.linewidth': 2, 'figure.figsize': (8, 5),
    'savefig.dpi': 150, 'savefig.bbox': 'tight',
})

COLORS = {'blue':'#2563eb','red':'#dc2626','green':'#16a34a',
          'orange':'#ea580c','purple':'#9333ea'}

def save(fig, name):
    fig.savefig(f'static/img/article-name/{name}.svg', format='svg')
    plt.close(fig)
    print(f'  Saved {name}.svg')

# 生成每张图
fig, ax = plt.subplots()
# ... 绑图代码 ...
save(fig, 'chart1')

print('All done!')

7. 常见踩坑与解决方案

7.1 中文显示为方块

原因:默认字体无中文字形。

解决:设置 plt.rcParams['font.sans-serif'] 或改用英文标注。

7.2 SVG 中文字被 Hugo HTML 预处理器干扰

现象:SVG 中的 <text> 标签被 Hugo 的 Goldmark 解析器误处理。

解决:避免在 SVG 中使用 <> 等特殊字符作为标签文本。matplotlib 生成的 SVG 通常不会有此问题,但手动编辑 SVG 时需注意。

7.3 负号显示为方块

原因:某些中文字体不包含 Unicode 负号(U+2212)。

解决plt.rcParams['axes.unicode_minus'] = False(改用 ASCII 减号)。

7.4 SVG 文件过大

原因:数据点过多(如 10000 个点的散点图)。

解决:降采样数据,或改用 rasterized=True 参数将密集元素栅格化嵌入 SVG。

7.5 图片在深色主题下不可见

原因:默认白色线条在深色背景上不可见。

解决:设置 figure.facecolor='white' 确保图片自带白色背景,不依赖页面主题。

7.6 plt.close() 忘记调用

后果:批量生成时内存泄漏,脚本越来越慢。

解决:养成 savefig() 后立即 plt.close(fig) 的习惯。


8. 与 KaTeX 公式的配合技巧

8.1 图表中使用 LaTeX 数学符号

  matplotlib 原生支持 LaTeX 数学符号,用 $...$ 包裹即可:

ax.set_xlabel('$y - \\hat{y}$')
ax.set_title('Huber Loss: $\delta = 1.0$')
ax.annotate('$d_{model} \\to d_{ff}$', (1, 0.5))

  注意:在 Python 字符串中需要双重转义 \\\hat 写成 \\hat)。

8.2 图表标注与文章公式呼应

  图表中的变量名应与文章正文中的 KaTeX 公式保持一致。例如文章中写 $d_{\text{model}}$,图表中也用 $d_{model}$,让读者能对应起来。


9. 总结:最佳实践清单

实践建议
输出格式SVG 首选(矢量、小文件、清晰)
中文处理优先用英文标注,必要时指定中文字体
配色定义统一配色字典,不超过 5~6 种颜色
画布背景设置白色背景,不依赖页面主题
样式使用 rcParams 全局配置,保持一致性
资源管理savefig() 后立即 plt.close(fig)
存放路径static/img/<文章名>/,按文章归档
批量生成编写独立脚本,版本控制,可重复执行
MathJax/KaTeX 协作图表中用 $...$ 数学符号,与正文公式保持一致

相关文章:本博客系列中大量使用了 matplotlib 生成 SVG 图片的技术,详见 Loss Function 合集大语言模型训练机制全解·第一篇 等文章。