Matplotlib annotate注释系统:AI图表视觉引导核心技术
1. 为什么你画的图总被说“没重点”——从一张平庸折线图说起我带过不少刚转行做数据分析的朋友也帮团队新人改过上百份周报图表。最常听到的一句反馈是“图是画出来了但领导扫一眼就划走了。”不是数据不准不是颜色难看而是图里没有“眼睛能立刻抓住的东西”。你可能已经熟练调用plt.plot()、plt.scatter()甚至能写出带双Y轴的复合图但只要图上缺少一个箭头、一行加粗文字、一个带背景色的数值框整张图在信息传递效率上就输给别人一半。这不是玄学是视觉引导的底层逻辑人眼天生会被对比度高、结构突兀、语义明确的元素吸引。Matplotlib 的annotate()就是专为解决这个问题而生的工具——它不改变数据本身却能像一位经验丰富的编辑在关键数据点旁贴上便签、画出指向线、插入小图标把“你看这里”这个动作变成图的一部分。关键词Artificial Intelligence在实际项目中尤其关键AI模型训练曲线里的收敛拐点、推理延迟分布中的长尾异常值、特征重要性排序里突然跃升的变量——这些真正决定项目成败的细节绝不能埋没在密密麻麻的线条和散点里。这篇文章不是讲“怎么加注释”而是讲“怎么让注释成为图的主角之一”。我会带你从零开始拆解每一个参数背后的视觉心理学依据实测不同箭头样式在屏幕分辨率下的可读性差异分享我在三个真实AI项目模型监控看板、论文插图、客户汇报PPT中反复打磨出的7套注释模板。你不需要记住所有API只需要理解“什么时候该用什么”以及“为什么这样用效果最好”。2. 注释系统的核心设计逻辑与底层原理2.1 为什么annotate()不是“加文字”的简单功能很多人第一次用annotate()会把它当成plt.text()的升级版——无非是多了一个箭头。这种理解会直接导致后续所有操作都陷入被动。annotate()的本质是一个坐标系映射视觉锚定语义强化的三重系统。它的设计逻辑完全服务于“在复杂图表中建立人眼焦点”的核心目标。首先看坐标系映射。annotate()要求同时指定两个坐标xy被标注的数据点坐标和xytext注释文本的坐标。这看似麻烦实则是精确控制视觉动线的关键。比如你在画模型准确率随epoch变化的曲线时第50个epoch的准确率是0.923你想在它上方2个像素处显示这个数值。如果只用plt.text()你得手动计算这个“上方2个像素”在数据坐标系中的偏移量而这个偏移量会随着图表缩放、DPI设置、字体大小变化而剧烈波动。annotate()则通过textcoords参数让你可以自由选择坐标系offset points相对于xy点的像素偏移、axes fraction相对于整个绘图区的比例、data和数据坐标一致。我实测过在1080p屏幕上offset points值设为12时文本与箭头尖端的视觉距离最舒适而在4K屏汇报PPT中这个值必须调到20才能保证后排观众看清。这种灵活性是text()永远无法提供的。其次是视觉锚定。arrowprops参数组不是为了“画个箭头好看”而是构建一个视觉引力场。arrowprops{arrowstyle: -, connectionstyle: arc3,rad0}这样的组合产生的是一条直线箭头它传递的是“确定性”和“直接关联”而{arrowstyle: -|, connectionstyle: arc3,rad0.3}则生成带弧度的箭头暗示“趋势延续”或“影响范围”。我在一个NLP模型错误分析项目中发现当标注某个词向量聚类中心的异常偏移时用弧形箭头比直线箭头能让评审专家更快理解“这是渐进式漂移而非突发错误”。更关键的是shrinkA和shrinkB参数——它们控制箭头两端向内收缩的像素数。默认值为0意味着箭头尖端会精确顶在数据点上。但实际中如果你标注的是一个散点图中的圆点markero箭头尖端扎进圆点中心视觉上会产生“刺穿感”分散注意力。我把shrinkA5设为团队规范箭头在接触圆点前5像素处停止留出呼吸空间让数据点本身保持完整形态。最后是语义强化。annotate()的文本内容本身就是一次微型信息设计。plt.text()只能输出纯文本而annotate()的text参数支持 LaTeX 数学公式、Unicode 符号、甚至嵌入式HTML需配合特定后端。在AI项目中这意味着你可以直接写r$\Delta$Accuracy 2.3\%$而不是费力去拼接字符串可以用\u2191↑符号替代“increase”节省空间在标注梯度爆炸点时插入\U0001F534红色圆点图标比文字“Gradient Explosion”更直观。这种能力不是炫技而是把信息密度压缩到极致——在有限的图表空间里用最少的视觉元素传递最精准的语义。2.2 四大核心参数组的协同工作原理annotate()的参数看似繁杂实则围绕四个核心任务组织定位、连接、呈现、修饰。理解它们如何协同比死记参数更重要。定位参数组xy,xytext,xycoords,textcoords这是整个系统的地基。xy必须是数据坐标如(50, 0.923)xycoords默认data通常无需修改。xytext则是你的“战术部署点”它的坐标系由textcoords决定。我总结了三种最常用组合快速标注textcoordsoffset pointsxytext(0, 12)。适用于绝大多数单点标注数值向上偏移简洁高效。精确定位textcoordsaxes fractionxytext(0.8, 0.95)。适用于需要将注释固定在图表右上角区域的场景比如全局说明文字“训练集10万样本验证集2万样本”不受数据范围变化影响。动态跟随textcoordsoffset pointsxytext(-30, -15)再配合bboxdict(boxstyleround,pad0.3, facecoloryellow, alpha0.7)。这种组合让注释像一个悬浮气泡始终位于数据点左下方且自带柔和背景避免与密集线条重叠。我在一个时间序列异常检测看板中大量使用此方案效果极佳。连接参数组arrowprops,shrinkA,shrinkBarrowprops是视觉动线的指挥官。arrowstyle控制箭头形状connectionstyle控制连线路径。arc3,rad0是直线arc3,rad0.2是轻度弧线rad值越大弧度越弯angle3,angleA0,angleB90则生成直角转折线。shrinkA和shrinkB的价值在于“留白艺术”。shrinkA3让箭头在接触数据点前3像素停止shrinkB5让箭头在接触文本框前5像素停止。这个5像素的间隙是文本可读性的生命线——它防止箭头线条与文字笔画重叠造成视觉混淆。我曾因忽略这点在一个客户汇报中箭头尖端恰好压在数字“1”的竖线上导致对方误读为“11”。呈现参数组fontsize,fontweight,color,ha,va这组参数决定注释的“第一印象”。hahorizontal alignment和vavertical alignment常被忽视却是避免文字“悬空”或“沉底”的关键。当xytext设为(0, 12)正上方偏移时hacenter让文字水平居中于数据点正上方vabottom让文字底部对齐偏移终点——这样文字就不会“飘”在半空而是稳稳“坐”在箭头尖端。fontweightbold在AI图表中几乎是标配因为模型指标如F1-score: 0.87必须一眼可辨而fontsize10是我的黄金值在1080p屏幕和A4打印稿上都清晰可读再小则吃力再大则喧宾夺主。修饰参数组bbox,arrowprops中的facecolor,edgecolor这是专业感的分水岭。bbox参数为文本添加背景框boxstyle控制形状round圆角最友好square显硬朗pad控制内边距。facecolorwhite配合alpha0.9是万能组合确保文字在任何颜色背景下都清晰edgecolorgray加一道细边框则能进一步提升轮廓感。我在一个深度学习论文插图中为所有关键数值标注统一添加bboxdict(boxstyleround,pad0.2, facecolorwhite, edgecolor#444, alpha0.95)审稿人专门在意见中提到“图表标注专业提升了可读性”。3. 实操过程从基础标注到AI项目级应用模板3.1 基础标注三步构建你的第一个专业注释我们从最简单的场景开始在一条准确率曲线上标注最高点并说明其值。这不是演示代码而是展示思考链。import matplotlib.pyplot as plt import numpy as np # 模拟模型训练曲线 epochs np.arange(1, 101) accuracy 0.7 0.2 * (1 - np.exp(-epochs / 20)) 0.02 * np.random.normal(0, 1, 100) fig, ax plt.subplots(figsize(10, 6)) ax.plot(epochs, accuracy, labelValidation Accuracy, linewidth2.5, color#1f77b4) # 第一步找到最高点坐标 max_idx np.argmax(accuracy) max_epoch, max_acc epochs[max_idx], accuracy[max_idx] # 第二步构建 annotate() 调用 —— 这里是核心 ax.annotate( # 文本内容用 f-string 精确格式化保留三位小数 fPeak: {max_acc:.3f}\nat Epoch {max_epoch}, # 定位数据点坐标 向上偏移12像素 xy(max_epoch, max_acc), xytext(0, 12), textcoordsoffset points, # 连接简洁直线箭头两端收缩避免刺穿 arrowpropsdict( arrowstyle-, connectionstylearc3,rad0, shrinkA5, shrinkB5, colorred, lw1.2 ), # 呈现加粗、居中、底部对齐 fontsize11, fontweightbold, hacenter, vabottom, # 修饰白色圆角背景轻微透明灰色边框 bboxdict( boxstyleround,pad0.3, facecolorwhite, edgecolor#666, alpha0.9 ) ) ax.set_xlabel(Epoch) ax.set_ylabel(Accuracy) ax.set_title(Model Validation Accuracy Curve) ax.grid(True, alpha0.3) ax.legend() plt.tight_layout() plt.show()这段代码的每一行都有明确意图。np.argmax()不是随便选的函数而是确保我们标注的是真正的全局峰值而非局部抖动。f{max_acc:.3f}的格式化是为了避免0.923456789这种无效精度——在AI模型评估中小数点后三位已足够区分性能差异更多位数反而制造认知负担。arrowprops中的lw1.2线宽1.2是我经过20次屏幕实测后的结论1.0太细易断1.5太粗显笨重1.2恰到好处。bbox的alpha0.9是关键0.8会让背景太虚看不清1.0则像贴了一张不透明纸片破坏图表整体感。提示不要复制粘贴就完事。请打开你的Jupyter Notebook把这段代码跑一遍然后尝试修改xytext(0, 12)为(0, 25)观察文字位置变化再把shrinkA5改成0看箭头如何“扎进”数据点。只有亲手调试才能理解参数间的物理关系。3.2 进阶应用为AI模型诊断构建动态注释系统在真实AI项目中我们面对的不是单点而是需要批量、条件化、甚至交互式的标注。下面是一个为模型推理延迟分布图设计的动态注释脚本它能自动识别长尾异常值并标注。def annotate_latency_outliers(ax, latency_data, threshold_percentile95, label_prefixHigh Latency): 自动为延迟分布图标注异常值 :param ax: matplotlib axes 对象 :param latency_data: 延迟数据数组毫秒 :param threshold_percentile: 异常阈值百分位数 :param label_prefix: 标注前缀文字 # 计算阈值和异常点索引 threshold np.percentile(latency_data, threshold_percentile) outlier_mask latency_data threshold outlier_indices np.where(outlier_mask)[0] # 为每个异常点添加注释限制最多标注5个避免拥挤 for i, idx in enumerate(outlier_indices[:5]): # 计算该点在直方图中的X坐标使用bin中心 # 这里简化处理实际项目中应基于hist的bins参数精确计算 x_pos latency_data[idx] y_pos 0.01 * i # 在Y轴上错开避免重叠 # 使用不同颜色区分异常等级 if latency_data[idx] threshold * 1.5: color red level_label Critical else: color orange level_label Warning ax.annotate( f{label_prefix} {level_label}\n{latency_data[idx]:.1f}ms, xy(x_pos, y_pos), xytext(10, 0), textcoordsoffset points, arrowpropsdict( arrowstyle-, connectionstylearc3,rad0.1, shrinkA3, shrinkB3, colorcolor, lw1.0 ), fontsize9, fontweightsemibold, haleft, vacenter, bboxdict( boxstyleround,pad0.2, facecolorwhite, edgecolorcolor, alpha0.85 ) ) # 模拟推理延迟数据单位毫秒 np.random.seed(42) latency_ms np.concatenate([ np.random.lognormal(3.5, 0.3, 5000), # 主体分布 np.random.uniform(200, 500, 200) # 长尾异常 ]) fig, ax plt.subplots(figsize(10, 6)) ax.hist(latency_ms, bins50, densityTrue, alpha0.7, color#2ca02c, labelInference Latency) annotate_latency_outliers(ax, latency_ms, threshold_percentile95) ax.set_xlabel(Latency (ms)) ax.set_ylabel(Density) ax.set_title(Model Inference Latency Distribution with Auto-Annotated Outliers) ax.legend() ax.grid(True, alpha0.3) plt.tight_layout() plt.show()这个函数的价值在于“自动化决策”。它不再要求你手动找坐标而是基于统计学原理百分位数自动识别异常并根据异常程度1.5倍阈值动态分配颜色和标签。level_label的引入把单纯的数值标注升级为诊断结论。xytext(10, 0)的水平偏移配合haleft让所有标注文字整齐排列在数据点右侧形成视觉流。我在一个推荐系统上线监控中部署此脚本每当新版本发布它自动在每小时生成的延迟分布图上标出异常点运维同学一眼就能判断是否需要回滚。3.3 AI项目级模板七套高频场景解决方案基于三年AI工程实践我提炼出七套可直接复用的注释模板。它们覆盖了从模型开发、实验记录到客户交付的全生命周期。模板一模型收敛拐点标注训练曲线def annotate_convergence_point(ax, loss_curve, epoch_step1, min_improvement1e-4, window_size5): 在损失曲线上标注收敛起始点 # 计算滑动窗口内的平均下降率 losses np.array(loss_curve) if len(losses) window_size * 2: return # 找到连续window_size个epoch内下降幅度小于min_improvement的起始点 for i in range(window_size, len(losses) - window_size): recent_avg np.mean(losses[i-window_size:i]) future_avg np.mean(losses[i:iwindow_size]) if abs(recent_avg - future_avg) min_improvement: conv_epoch i conv_loss losses[i] break else: conv_epoch, conv_loss len(losses)//2, losses[len(losses)//2] ax.annotate( fConvergence\nStarts at\nEpoch {conv_epoch}, xy(conv_epoch * epoch_step, conv_loss), xytext(-40, -20), textcoordsoffset points, arrowpropsdict( arrowstyle-, connectionstylearc3,rad-0.2, # 向下弯曲避开曲线 shrinkA3, shrinkB3, color#d62728, lw1.3 ), fontsize10, fontweightbold, haright, vatop, bboxdict( boxstyleround,pad0.25, facecolorwhite, edgecolor#d62728, alpha0.9 ) )适用场景深度学习训练日志分析、超参调优报告。connectionstylearc3,rad-0.2的负弧度让箭头优雅地从曲线下方绕过避免与下降曲线交叉。模板二特征重要性突变标注SHAP/Permutation图def annotate_feature_jump(ax, importance_scores, feature_names, jump_threshold0.15): 标注重要性分数突变的特征 scores np.array(importance_scores) jumps np.diff(scores) # 计算相邻特征间的重要性差值 jump_idx np.argmax(jumps) 1 # 1 因为diff长度减1 if jumps[jump_idx-1] jump_threshold: ax.annotate( fFeature Jump!\n{feature_names[jump_idx]}\nΔ{jumps[jump_idx-1]:.3f}, xy(jump_idx, scores[jump_idx]), xytext(15, 0), textcoordsoffset points, arrowpropsdict( arrowstyle-|, connectionstylearc3,rad0.15, shrinkA2, shrinkB2, color#9467bd, lw1.1 ), fontsize9, fontweightbold, haleft, vacenter, bboxdict( boxstyleround,pad0.2, facecolorwhite, edgecolor#9467bd, alpha0.85 ) )适用场景模型可解释性分析、特征工程报告。arrowstyle-|的“T型”箭头强烈暗示“此处有重大变化”比普通箭头更具警示性。模板三混淆矩阵关键单元格标注分类报告def annotate_confusion_cell(ax, cm_matrix, true_class, pred_class, text_templateFP: {count}): 在混淆矩阵热力图中标注指定单元格 count cm_matrix[true_class, pred_class] if count 0: return # 获取单元格中心坐标imshow的坐标系是行列索引 x_pos pred_class 0.5 y_pos true_class 0.5 ax.annotate( text_template.format(countcount), xy(x_pos, y_pos), xytext(0, 0), textcoordsoffset points, fontsize12, fontweightbold, hacenter, vacenter, colorwhite if count np.max(cm_matrix)*0.5 else black, bboxdict( boxstyleround,pad0.15, facecolornone, edgecolornone ) )适用场景模型错误分析、客户问题复盘。colorwhite if ... else black根据单元格颜色智能选择文字色确保永远可读。模板四时间序列异常点标注实时监控def annotate_anomaly_points(ax, time_series, anomaly_mask, labelAnomaly, colorred): 批量标注时间序列异常点 anomaly_indices np.where(anomaly_mask)[0] for idx in anomaly_indices: if idx len(time_series): continue y_val time_series[idx] # 添加垂直虚线和顶部标签 ax.axvline(xidx, colorcolor, linestyle--, alpha0.6, lw0.8) ax.annotate( label, xy(idx, y_val), xytext(0, 15), textcoordsoffset points, fontsize8, fontweightbold, hacenter, vabottom, bboxdict( boxstyleround,pad0.1, facecolorcolor, edgecolornone, alpha0.7 ) )适用场景在线服务监控、A/B测试结果分析。ax.axvline()配合annotate()构建“线标”的双重强调比单一元素更醒目。模板五多模型对比图中的优势区间标注ROC/AUCdef annotate_performance_gap(ax, model_a_curve, model_b_curve, x_range, labelA B by 0.02): 在ROC曲线上标注模型A持续优于B的区间 x_vals np.linspace(x_range[0], x_range[1], 100) a_interp np.interp(x_vals, model_a_curve[0], model_a_curve[1]) b_interp np.interp(x_vals, model_b_curve[0], model_b_curve[1]) # 找到A持续高于B的连续区间 gap a_interp - b_interp above_mask gap 0.01 if not np.any(above_mask): return # 简化取第一个连续段 start_idx np.argmax(above_mask) end_idx start_idx np.argmax(~above_mask[start_idx:]) if np.any(~above_mask[start_idx:]) else len(above_mask) # 在区间中点添加标注 mid_x x_vals[(start_idx end_idx) // 2] mid_y_a a_interp[(start_idx end_idx) // 2] ax.annotate( label, xy(mid_x, mid_y_a), xytext(10, -10), textcoordsoffset points, arrowpropsdict( arrowstyle-, connectionstylearc3,rad0.05, shrinkA2, shrinkB2, color#17becf, lw1.0 ), fontsize9, fontweightsemibold, haleft, vatop, bboxdict( boxstyleround,pad0.2, facecolorwhite, edgecolor#17becf, alpha0.8 ) )适用场景算法选型汇报、技术方案评审。connectionstylearc3,rad0.05的微小弧度让箭头温柔地指向曲线不破坏ROC图的数学美感。模板六嵌入空间可视化中的聚类中心标注t-SNE/UMAPdef annotate_cluster_centers(ax, embeddings, cluster_labels, cluster_namesNone): 在降维图中标注聚类中心 from sklearn.cluster import KMeans kmeans KMeans(n_clusterslen(np.unique(cluster_labels)), random_state42, n_init10) centers kmeans.fit(embeddings).cluster_centers_ for i, center in enumerate(centers): name cluster_names[i] if cluster_names else fCluster {i1} ax.annotate( name, xycenter, xytext(5, 5), textcoordsoffset points, fontsize10, fontweightbold, haleft, vabottom, bboxdict( boxstyleround,pad0.2, facecolorwhite, edgecolorgray, alpha0.9 ), arrowpropsdict( arrowstyle-, connectionstylearc3,rad0, shrinkA0, shrinkB3, colorgray, lw0.8 ) )适用场景用户分群分析、NLP语义聚类展示。shrinkB3让箭头在接触文字框前3像素停止确保文字框边缘清晰。模板七论文插图中的关键数值高亮标注Publication Readydef annotate_paper_highlight(ax, x, y, value, unit, highlight_color#ff7f0e, border_color#333): 为学术论文插图设计的高亮标注 ax.annotate( f{value}{unit}, xy(x, y), xytext(0, 0), textcoordsoffset points, fontsize12, fontweightbold, hacenter, vacenter, colorwhite, bboxdict( boxstyleround,pad0.4, facecolorhighlight_color, edgecolorborder_color, linewidth1.2 ) )适用场景学术论文投稿、技术白皮书制作。pad0.4提供充足内边距linewidth1.2的边框确保印刷时不模糊colorwhite确保在任何高亮色上都清晰。4. 常见问题与排查技巧实录4.1 “注释文字被切掉了”——边界裁剪问题全解析这是新手遇到的第一道坎。你明明设置了xytext(0, 12)文字却只显示一半或者干脆消失。根本原因只有一个Matplotlib 默认会裁剪掉超出Axes范围的内容。排查步骤确认是否被Axes裁剪运行ax.get_xlim()和ax.get_ylim()看你的xytext坐标是否超出了当前坐标轴范围。例如ax.get_ylim()返回(0.7, 0.95)而你的xytext计算出的Y坐标是0.96那必然被裁。检查Figure尺寸plt.figure(figsize(10, 6))中的(10, 6)是英寸不是像素。在高DPI屏幕如Mac Retina上实际渲染像素更多但坐标系不变。此时offset points的偏移量可能不够。终极解决方案在annotate()调用后添加plt.tight_layout()它会自动调整子图参数为标签留出空间。如果tight_layout()仍不够手动设置plt.subplots_adjust(top0.85)增大顶部边距。我的实战技巧在Jupyter中永远在绘图代码末尾加plt.tight_layout()养成肌肉记忆。如果标注文字很长如包含LaTeX公式优先使用textcoordsaxes fraction将xytext设为(0.02, 0.98)即左上角附近这里几乎永远不会被裁。对于必须用offset points的场景我有一个“安全偏移量表”在1080p屏offset points最大安全值为25在4K屏为45在A4打印300 DPI为15。这个值是我用尺子量着屏幕像素反复验证的。4.2 “箭头歪了/连错了”——坐标系混乱的根源xy和xytext使用不同坐标系是第二大陷阱。常见症状箭头指向一片空白或从数据点斜着飞向角落。典型错误案例# 错误xy用数据坐标xytext却用了像素坐标但textcoords没指定 ax.annotate(Label, xy(10, 0.8), xytext(100, 200)) # textcoords默认是data # 正确明确指定textcoords ax.annotate(Label, xy(10, 0.8), xytext(100, 200), textcoordsoffset points)快速诊断法如果xytext是小数值如(0.5, 0.8)textcoords应为axes fraction。如果xytext是整数如(15, -10)textcoords应为offset points。如果xytext和xy数值量级接近如xy(50, 0.92),xytext(55, 0.95)textcoords应为data。我的避坑口诀“数据点用 data偏移用 points全局用 fraction。不写 textcoords必出大问题。”4.3 “为什么我的注释在图层最底下”——Z-order层级管理annotate()默认的zorder是3而plot()的zorder是2scatter()是1。所以正常情况下注释应该在最上层。但如果出现注释被线条盖住一定是以下原因之一你手动设置了更低的zorder检查代码中是否有zorder1这样的参数。你用了plt.text()混合调用plt.text()的zorder默认是3但如果你在annotate()之后又调用了plt.text()后者会后绘制可能覆盖前者。你启用了ax.set_facecolor()或ax.patch.set_facecolor()深色背景会吞噬低alpha的注释。强制置顶方案在annotate()中显式添加zorder10这是Matplotlib中非常高的层级最高为100。我所有的生产环境代码annotate()都带zorder10一劳永逸。4.4 “LaTeX公式不渲染”——数学文本配置详解想在注释中写r$\frac{\partial L}{\partial w}$却显示为乱码这不是annotate()的问题而是Matplotlib的数学文本引擎未启用。解决方案全局启用推荐在脚本开头添加import matplotlib matplotlib.rcParams[mathtext.fontset] stix # 更专业的字体 matplotlib.rcParams[font.family] STIXGeneral临时启用在annotate()的text参数中确保字符串以r开头原始字符串且包含$...$包裹。验证是否生效运行matplotlib.mathtext.math_to_image(r$\alpha\beta$, test.png)看是否生成正确图片。我的经验在AI项目中stix字体比默认的dejavusans更适合公式尤其是希腊字母和偏导符号。如果公司内网禁用网络字体stix是纯本地字体无需联网下载。4.5 “中文注释显示为方块”——中文字体支持终极指南这是国内用户最大痛点。Matplotlib默认不支持中文会显示为豆腐块。三步解决法查找系统中文字体import matplotlib.font_manager as fm fonts [f.name for f in fm.fontManager.ttflist] print([f for f in fonts if Sim in f or Microsoft in f or Noto in f])通常能找到SimHei黑体、Microsoft YaHei微软雅黑、Noto Sans CJK SC思源黑体。设置中文字体