转载本文请注明出处:https://yudonglee.me/ctc-explained 作者:yudonglee
现实应用中许多问题可以抽象为序列学习(sequence learning)问题,比如词性标注(POS Tagging)、语音识别(Speech Recognition)、手写字识别(Handwriting Recognition)、机器翻译(Machine Translation)等,其核心问题都是训练模型将一个领域的输入序列转换为另一个领域的输出序列。
近年来,基于 RNN 的序列到序列模型(sequence-to-sequence models)在这类任务中取得了显著的效果提升。本文介绍一种 RNN(Recurrent Neural Networks)的端到端训练方法——CTC(Connectionist Temporal Classification)算法。CTC 可以让 RNN 直接对序列数据进行学习,无需事先标注输入序列和输出序列之间的映射关系,从而打破了 RNN 应用于语音识别、手写字识别等领域的数据依赖约束,使模型在序列学习任务中取得更好的效果。
本系列文章总共分为三部分来全面阐述CTC算法(本篇为Part 1):
Part 1:Training the Network(训练算法篇),介绍CTC理论原理,包括问题定义、公式推导、算法过程等。Part 1链接。
Part 2:Decoding the Network(解码算法篇),介绍CTC Decoding的几种常用算法。Part 2链接。
Part 3:CTC Demo by Speech Recognition(语音识别实战篇),基于 TensorFlow 实现完整的 CTC 语音识别系统。Part 3链接。
接下来,我们先从“问题”的背景说起。
1. 背景介绍
在序列学习任务中,RNN模型对训练样本一般有这样的依赖条件:输入序列和输出序列之间的映射关系已经事先标注好了。比如,在词性标注任务中,训练样本中每个词(或短语)对应的词性会事先标注好,如下图(DT、NN等都是词性的标注,具体含义请参考链接)。由于输入序列和输出序列是一一对应的,所以RNN模型的训练和预测都是端到端的,即可以根据输出序列和标注样本间的差异来直接定义RNN模型的Loss函数,传统的RNN训练和预测方式可直接适用。


然而,在语音识别、手写字识别等任务中,音频数据和图像数据都是将现实世界的模拟信号转为数字信号后采集得到的,这些数据天然就很难进行”分割”,这使得我们很难获取到包含输入序列和输出序列映射关系的大规模训练样本(人工标注成本巨高,且启发式挖掘方法存在很大局限性)。因此,在这种条件下,RNN无法直接进行端到端的训练和预测。
如下图,输入是“apple”对应的一段说话音频和手写字图片,从连续的音频信号和图像信号中逐一分割并标注出对应的输出序列非常费时费力,在大规模训练下这种数据要求是完全不切实际的。而如果输入序列和输出序列之间映射关系没有提前标注好,那传统的RNN训练方式就不能直接适用了,无法直接对音频数据和图像数据进行训练。


因此,在语音识别、图像识别等领域中,由于数据天然无法切割,且难以标注出输入和输出的序列映射关系,导致传统的RNN训练方法不能直接适用。那么,如何让RNN模型实现端到端的训练成为了关键问题。
Connectionist Temporal Classification(CTC)[1]是Alex Graves等人在ICML 2006上提出的一种端到端的RNN训练方法,它可以让RNN直接对序列数据进行学习,而无需事先标注好训练数据中输入序列和输入序列的映射关系,使得RNN模型在语音识别等序列学习任务中取得更好的效果,在语音识别和图像识别等领域CTC算法都有很比较广泛的应用。总的来说,CTC的核心思路主要分为以下几部分:
- 它扩展了RNN的输出层,在输出序列和最终标签之间增加了多对一的空间映射,并在此基础上定义了CTC Loss函数
- 它借鉴了HMM(Hidden Markov Model)的Forward-Backward算法思路,利用动态规划算法有效地计算CTC Loss函数及其导数,从而解决了RNN端到端训练的问题
- 最后,结合CTC Decoding算法RNN可以有效地对序列数据进行端到端的预测
接下来,通过一个语音识别的实际例子来引出CTC的解决思路
2. 一个实际的例子–声学模型
语音识别的核心问题是把一段音频信号序列转化文字序列,传统的语音识别系统主要分为以下几部分,如下图。

其中,X表示音频信号,O是它的特征表示,一般基于LPC、MFCC等方法提取特征,也可以基于DNN的方式“学到”声学特征的表示。为了简化问题,我们暂且把O理解为是由实数数组组成的序列,它是音频信号的特征表示。Q是O对应的发音字符序列,即建模单元,一般可以是音素、音节、字、词等。W是音频信号X对应的文字序列,即我们最终的识别结果。
如图所示,核心问题是通过解码器找到令P(W|X)最大化的的W,通过贝叶斯公式可将其分解为P(O|Q)、P(Q|W)、P(W),分别对应声学模型、发音模型、语言模型。
其中,声学模型就是对P(O|Q)进行建模,通过训练可以“学到”音频信号和文字发音间的联系。为了简化问题,我们假定声学模型的建模单元Q选择的是音节,O选择的是MFCC特征(由39维数组组成的序列)。
以下图为例,输入序列是一段”我爱你中国”的音频,输出序列是音节序列 “wo3 ai4 ni3 zhong1 guo2″。如果训练样本中已经将音频”分割”好,并标注了音频帧与音节的对应关系,则 RNN 模型的结构如下:

然而,如前面所述,对音频进行精确”分割”并标注映射关系在实际中是不可行的。实际做法是按照固定时间窗口滑动提取特征,例如每 10 毫秒提取一帧,得到一个 N 维特征向量。这种方式下,输入序列的长度远大于输出标签的长度,如下图所示:

由于人说话发音是连续的,且中间也会有“停顿”,所以输出序列中存在重复的元素,比如“wo3 wo3”,也存在表示间隔符号“_”。需从输出序列中去除掉重复的元素以及间隔符,才可得到最终的音节序列,比如,“wo3 wo3 ai4 _ ni3 _ zhong1 guo2 _” 归一处理后得到“wo3 ai4 ni3 zhong1 guo2”。因此,输出序列和最终的label之间存在多对一的映射关系,如下图:

RNN模型本质是对𝒑(𝒛│𝒙)建模,其中x表示输入序列,o表示输出序列,z表示最终的label,o和l存在多对一的映射关系,即:𝒑(𝒛│𝒙)=sum of all P(o|x),其中o是所有映射到z的输出序列。因此,只需要穷举出所有的o,累加一起即可得到𝒑(𝒛│𝒙),从而使得RNN模型对最终的label进行建模。
经过以上的映射转换,解决了端到端训练的问题,RNN模型实际上是对映射到最终label的输出序列的空间建模。然而,对每一个z都“穷举所有的o”,这个计算的复杂度太大,会使得训练速度变得非常慢,因此怎么更高效地进行端到端训练成为待解决的关键问题。
通过以上的实际例子,我们对问题的解决思路有了更加直观的了解,接下来就开始正式介绍CTC的理论原理。
3. 问题定义
以RNN声学模型为例子,建模的目标是通过训练得到一个RNN模型,使其满足:

本质上是最大似然预估, S是训练数据集,X和Z分别是输入空间(由音频信号向量序列组成的集合)和目标空间(由声学模型建模单元序列组成的集合),L是由输出的字符集(声学建模单元的集合),且x的序列长度小于或等于z的序列长度。
接下来,在介绍如何计算Loss函数之前,我们需要对RNN输出层做一个简单的扩展。
4. RNN输出层扩展
为了便于读者理解,下图简化了 RNN 的结构:仅使用单向单层 LSTM,将声学建模单元设为字母 {a-z}。在此基础上,对建模单元字符集做了两项扩展:一是增加了 blank 符号(表示”无输出”),二是定义了从输出层序列到最终 label 序列的多对一映射函数 B。通过该映射函数,多条不同的输出路径可以映射到同一个最终 label 序列。

所以,计算𝒑(𝒛│𝒙)的思路就是:枚举所有经映射函数 B 映射到最终 label z 的输出序列(即”路径”),将它们的概率累加即可。如下图所示:

5. CTC Loss函数定义
CTC Loss 函数的定义基于一个重要的条件独立假设:RNN 在各时间步的输出相互独立。在此假设下,一条路径的概率等于路径上各时间步输出概率的乘积,进而可以写出 CTC Loss 函数的完整定义,如下图所示:

假定选择单层 LSTM 作为 RNN 的具体结构,则整体模型架构(从输入特征到 CTC Loss 计算)如下图所示:

6. CTC Loss函数计算
由于直接穷举所有路径来计算 𝒑(𝒛│𝒙) 的时间复杂度是指数级的,作者借鉴了 HMM 的 Forward-Backward 算法思路,利用动态规划将复杂度降至多项式级别。
为了更形象地表示问题的搜索空间,如下图所示,用 X 轴表示时间步,Y 轴表示扩展后的输出序列。具体地,对最终标签 l 做标准化处理:在每个字符之间以及首尾都插入 blank 符号,得到扩展序列 l’,其长度满足 |l’| = 2|l| + 1。例如:l = “apple”(长度 5),则 l’ = “_a_p_p_l_e_”(长度 11)。



需要注意的是,并非搜索空间中所有路径都是合法的。合法路径需要满足以下约束条件(例如:路径必须从 l’ 的前两个符号之一开始,必须在最后两个符号之一结束,且不能跳过非 blank 字符等),具体规则如下图所示:





所以,依据以上约束规则,遍历所有映射为“apple”的合法路径,最终时序T=8,标签labeling=“apple”的全部路径如下图:

接下来的问题是:如何高效地计算这些合法路径的概率总和?作者借鉴了 HMM 的 Forward-Backward 算法思路,利用动态规划求解。核心思想是定义前向概率 α(从起点到当前位置的路径概率之和)和后向概率 β(从当前位置到终点的路径概率之和),通过递推关系避免重复计算,如下图所示:





通过动态规划递推求出全部前向概率后,在最后一个时间步对 l’ 的末尾两个位置求和,即可得到 𝒑(𝒛│𝒙),进而计算 CTC Loss 函数。具体公式如下图所示:

类似的方式,我们可以定义后向概率 β,即从最后一个时间步反向递推到当前位置。同样地,后向概率也可以用来计算 CTC Loss 函数,如下图所示:



更进一步,将任意时间步 t 的前向概率 α 和后向概率 β 相乘,也可以计算 CTC Loss 函数。这一等价关系对后续的梯度求导推导至关重要,如下图所示:



总结一下,根据前向概率计算CTC Loss函数,得到以下结论:

根据后向概率计算CTC Loss函数,得到以下结论:

根据任意时刻的前向概率和后向概率计算CTC Loss函数,得到以下结论:

至此,我们已经得到了 CTC Loss 的高效计算方法。接下来,对其进行求导,以便通过反向传播算法训练 RNN 模型。
7. CTC Loss函数求导
我们先回顾 RNN 的网络结构。如下图所示,红色标注部分是 CTC Loss 函数求导的核心环节——即 CTC Loss 对 RNN 输出层(softmax 层)输出值的偏导数:

CTC Loss函数对 RNN 输出层元素的求导,核心思路是通过前向概率和后向概率将对总路径概率的求导分解为对每个时间步输出概率的求导,具体推导过程如下图所示:


8. 总结
本篇以 RNN 声学模型为例,从问题背景出发,逐步介绍了 CTC Loss 函数的定义、基于动态规划的高效计算方法,以及梯度求导过程,最终通过反向传播算法实现了对 RNN 模型的端到端训练。

值得注意的是,CTC 算法也存在一些局限性:首先,条件独立假设意味着模型在各时间步的输出相互独立,无法建模输出序列内部的依赖关系(例如语言模型信息需要外部引入);其次,CTC 要求输入序列的长度不小于输出序列的长度,这在某些任务场景下可能成为限制;此外,CTC 训练出的模型倾向于产生”尖峰”(peaky)的后验概率分布,大部分时间步的输出集中在 blank 符号上。这些局限性在后续的 Attention-based 模型和 RNN-Transducer 等方法中得到了不同程度的改进。
至此,CTC 算法的模型训练过程与原理已介绍完毕。下一篇将详细介绍 CTC 算法的推理解码过程与原理,Part2链接。
References
- Graves et al., Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with RNNs. In ICML, 2006. (Graves提出CTC算法的原始论文)
- Graves et al., A Novel Connectionist System for Unconstrained Handwriting Recognition. In IEEE Transactions on PAML, 2009.(CTC算法在手写字识别中的应用)
- Graves et al., Towards End-to-End Recognition with RNNs. In JMLR, 2014.(CTC算法在端到端声学模型中的应用)
- Alex Graves, Supervised Sequence Labelling with Recurrent Neural Networks. In Studies in Computational Intelligence, Springer, 2012.( Graves 的博士论文,关于sequence learning的研究,主要是CTC)
- Watanabe et al., Hybrid CTC/Attention Architecture for End-to-End Speech Recognition. IEEE Journal of Selected Topics in Signal Processing, 2017.(CTC/Attention 联合解码的代表性工作)
- Miao et al., EESEN: End-to-End Speech Recognition using Deep RNN Models and WFST-Based Decoding. In ASRU, 2015.(基于 WFST 的 CTC 解码框架)
- TensorFlow CTC API 文档:https://www.tensorflow.org/api_docs/python/tf/nn/ctc_loss
- Librosa 文档:https://librosa.org/doc/latest/index.html
![]()
2019-03-07 at 7:40 pm
写的非常好,看起来一共要有五个部分,目前是只写完第一部分吗?
2019-03-08 at 9:48 am
第二部分很快完成,敬请关注,谢谢:)
2019-07-11 at 9:31 am
好文啊!先点赞为敬。然后就是,我想咨询一个问题,就是假设我现在手上有一套初始的音频信号,没有任何标签,那么如何给原始的音频信号加上标签呢(比如音素啥的)?然后才可以进行训练。传统的人工上标签确实如你文中所说的太要命了。
2019-07-16 at 10:08 pm
主要是声学模型的“数据增强”、“半监督训练”和“迁移学习”这几种流派,完全无监督的现在看还不切实际
2020-07-07 at 8:57 am
写的非常好,看起来一共要有五个部分,目前是只写完前两部分吗?
2019-03-08 at 4:30 pm
谢谢!Section2里面𝒑(𝒛│𝒙)公式显示有点小问题。
有个疑问望解释一下:前后向概率计算CTC Loss函数公式里的 l’ 指的是什么?好像没有定义过。
2019-03-12 at 9:41 am
l就是输入音频x对应的文本吧
2019-03-13 at 10:52 am
好问题,section2的公式显示问题已经fix,l’和l的定义补充在section6中了~
2019-03-10 at 8:18 pm
写得非常详细清晰, 感谢作者如此清晰的指导~
2019-03-12 at 9:37 am
很清楚!期待第二部分
2019-03-31 at 9:55 pm
谢谢关注,Part2发布了~https://xiaodu.io/ctc-explained-part2/
2019-03-14 at 3:53 pm
我来催催第二部分,坐等。。。。。
2019-03-31 at 9:54 pm
谢谢关注,Part2发布了~https://xiaodu.io/ctc-explained-part2/
2019-03-22 at 3:54 pm
问一下,ctc算法的源代码从哪里看呢?
2019-04-16 at 8:34 pm
可以参考part2:https://xiaodu.io/ctc-explained-part2/,有具体的算法步骤,也可以直接看TensorFlow的ctc实现:https://github.com/tensorflow/tensorflow/blob/r1.13/tensorflow/python/ops/ctc_ops.py
2019-03-29 at 3:26 pm
写的非常好,赞一个,坐等第二部分~
2019-03-31 at 9:54 pm
谢谢关注,Part2发布了~https://xiaodu.io/ctc-explained-part2/
2019-04-12 at 3:45 pm
写的很好,比较清楚。
2019-04-16 at 8:23 pm
写的相当清楚,必须要赞一下。
2019-04-25 at 10:29 am
老哥,快更新啊
2019-04-29 at 5:31 pm
作者写的太好了,网上没有比这个更清楚的了,希望作者多更新,学习学习,膜一下大佬。
2019-05-06 at 10:07 am
“所以,如果要计算𝒑(𝒛│𝒙),可以累加其对应的全部输出序列(也即映射到最终label的“路径”)的概率即可。”
您好,在第四小节,概率为什么是累加的呢,它的全部的输出序列每个不是相互独立的吗
2019-05-06 at 10:14 am
不好意思,已经搞清楚了。每个输出序列是互斥的,不是相互独立的。
2019-05-06 at 4:39 pm
您好,ctc loss反向求导中,公式(1)对y_k'(t)求导时,结果是不是少了一个负号?(1/x)的导数不是(-1/square(x))吗?
2019-07-16 at 5:59 pm
原论文就是没有 负号的,关于这点,我也没有想通,为什么没有负号
2020-05-19 at 10:51 pm
推荐看这篇文章!~也许有帮助…我之前看了好像明白一点为啥没有负号了。https://zhuanlan.zhihu.com/p/41674645
2019-10-21 at 7:44 pm
自己推导一下的话,会发现就是没有负号的,建议仔细看一下原论文
2019-12-21 at 10:29 am
您好,原论文好像有两个版本,一个是有负号的,一个是没有负号的。我自己推导了下,发现是没负号的,但是分母没有平方项,不知道哪里搞错了,能帮忙解下惑吗,谢谢~!
2019-05-10 at 12:09 am
写的很好a~~ 么么哒
2019-06-03 at 3:12 pm
作者您好,您的文章写的很棒,但是我有一个问题想问下就是,CTC求解了前向概率为什么还需要计算后向概率,是因为联系上下文吗?
2019-06-04 at 11:15 am
好问题,原因是后面对ctc目标函数求导需要用到,可以仔细看下ctc目标求导的过程介绍那一节。
2019-06-06 at 2:31 pm
而无需事先标注好训练数据中输入序列和输出序列的映射关系?请问这句话怎么理解?不需要标注数据?还是说不需要输出是定长?
2019-06-11 at 10:58 am
需要样本,但不需要标注出“输入和输出映射关系”,比如:输入“ABCDEF” => 输出“XYZ”,不需要再标注出映射关系:ABC=>X,DE=>Y,F=>Z
2019-06-07 at 1:14 am
所以对于反向概率存在的意义,就是为了方便ctc loss的反向传播吗?谢谢
2019-06-11 at 11:02 am
可以这么说,训练阶段的核心就是定义和求导ctc loss函数
2019-07-11 at 2:34 pm
且x的序列长度小于或等于z的序列长度。。。。这句话写错啦。
2019-09-04 at 9:26 pm
您好!写的超棒!我可以转载吗?我回复上你的原始链接的!!!
2019-09-05 at 4:53 pm
可以呀,注明作者,原始链接
2019-09-12 at 10:24 am
时隔半年,再看一遍,此处还是要对作者表示感谢。
第6部分,“则由2|l| + 1 = 2|l’|”这个表达式左边奇数右边偶数,显然不等,右边是不是要减个1?
2020-06-18 at 4:25 pm
我觉得应该是2|l| + 1 = |l’|
2020-10-10 at 11:37 am
作者应该是笔误了:
2|l| + 1 = |l’|
2019-09-26 at 3:08 pm
您好,CTC Loss求导推导时,求和的下标符号lab(l,k’)是不是应该为lab(l’k’)呢,
L’={a-z,-}
l=apple -> l’=_a_p_p_l_e_ ,
k’=1,L’[k’] = a, lab(l’,k‘)= {2}
k’=2,L’[k’]=b,lab(l’,k’)={}
…
k’=16 L[k’]=p,lab(l’,k’) = {3,5}
是这个意思吧,或者lab上文中的lab(l,k’)和我这里描述意思一致
2019-10-08 at 3:43 pm
本质上是最大似然预估, S是训练数据集,X和Z分别是输入空间(由音频信号向量序列组成的集合)和目标空间(由声学模型建模单元序列组成的集合),L是由输出的字符集(声学建模单元的集合),且x的序列长度小于或等于z的序列长度。
你的图片写大于或者等于,但是你的文字那里写的是小于或者等于,应该是后面写错了,要修改下吧。
2019-10-24 at 8:06 pm
写得很好,特别是那些图,满分。
2020-02-08 at 7:39 pm
非常优秀的一篇文章 学到了很多 期待你接下来的更新
2020-02-26 at 8:17 am
请问第五章还没有吗
2020-03-17 at 10:13 pm
想问下,某一时刻某一节点的全部路径概率总和,您的文章是直接给出的,能麻烦您具体讲解一下是如何计算出来的吗,谢谢
2020-03-26 at 2:37 am
我来催更了,作者能更新吗,期待您的文章
2020-04-23 at 9:07 am
作者,我有一些小小的疑惑,本文中所讲的预测最终序列的概率是由所有序列概率之和相加的到,可是对于一条语音,送入RNN后,经过softmax输出后,不是只能产生出一条预测序列,其他的那些序列是咋来的
2020-04-23 at 9:10 am
我想问一下,文中讲的是最终序列的概率是由所有可能的序列得概率相加得到,但是一个样本送入rnn后,只能得到一个预测序列,怎么同时得到所有可能序列。
2020-04-25 at 10:08 pm
你好,前向概率和后向概率计算规则的各自第三条,当s在某些区间内,前向概率/后项概率=0, 这些区间具体是怎么计算出来的,比如当s<l‘-2(T-t)-1 时,前向概率at_s均为0?
2020-05-19 at 9:56 pm
写的超级好!!我看了好几个博客,只有这个让我真真正正地明白了每一步的来源和推导!!谢谢博主!!!!期待后面的部分!!
2020-06-18 at 4:23 pm
不知道是我没理解,还是有错误,前面RNN的Loss和后面CTC中的Loss,一个是p(x,z),另一个是p(z|x)。还有就是2|l| +1 应该等于|l`|吧
2020-07-12 at 3:46 pm
请问在搜索路径中,处于非空字符行的时候,可以向右移动吗?
比如 在“_a_p_p_l_e_”这个例子中,”_lle_”是一条合法路径吗?它映射到的是 “le” 还是”lle” 呢?
2021-02-10 at 4:20 pm
这个属于合法路径,它映射到 le,因为 CTC Loss 的 prediction 最终要去重,假设你的 prediction 最终是 _ap_p_lle__,包含了你说的 “_lle_”,去重的结果就是 _ap_p_le_,再去掉 blank 字符”_”后就是 apple,所以是合法的
2020-08-04 at 11:20 pm
你好,请问有part3吗 我最近在做这部分相关的内容 想看一看学习下。
2020-09-08 at 11:50 am
66666
2020-12-02 at 10:21 pm
既然单独前向或者后向也能计算loss,为什么又要同时使用前向、后向来计算loss呢?
2021-03-24 at 4:34 pm
pytorch中自带的CTCLoss原理是一样的吗
2021-03-25 at 11:59 am
看了很多解释CTC的文章,完全懵逼状态。最后找到这篇文章,非常深入浅出的解释了CTC的原理。特别喜欢这种推导过程带解释图的文章,一张好的图的作用可以抵好几页的文字解释!感谢作者!
2021-08-12 at 1:18 pm
这篇写的很好理解起来很舒服
2022-01-06 at 12:25 am
你好,里面有些图看不到了,能再重新更新以下吗?之前看过感觉写的很好,最近还想再回顾下,发现有些图看不到了
2022-01-28 at 10:06 am
之前换域名到https://yudonglee.me出了点问题,现在已经恢复正常
2022-05-21 at 12:14 pm
你好,图又看不到了,麻烦修复一下
2022-07-15 at 4:38 pm
您好!后面前后向算法相关的几张图看不到了。
2025-07-29 at 10:05 pm
大佬你好,图片还是显示不出来。
2026-04-10 at 1:32 am
图片问题已经统一修复了~
2022-03-11 at 5:00 pm
写得真好,请问我可以在毕业论文中引用这篇博客吗?
2026-04-10 at 1:33 am
欢迎引用
2022-03-13 at 10:55 am
谢谢佬的解释,想请教一个问题,既然ctc的前向计算是得到所有路径的概率,那么自己用pytorch实现ctc的时候,是不是只实现前向计算,得到所有路径概率和的值后调用backward()就可以自动反向传播呢(感觉似乎不需要手动求导?)
2022-03-15 at 9:35 am
为什么后半部分的图看不到呢,怎么解决?
2022-04-24 at 11:30 am
后面的图片看不到了,谢谢!
2022-06-15 at 7:18 pm
您好,后面的图看不见了,能更新一下吗,目前我见过写的最好的,谢谢。
2022-06-27 at 10:44 am
你好,有些图片挂掉了,能更新下嘛
2022-12-17 at 7:01 pm
大佬你好,图片还是看不到
2023-01-12 at 10:08 pm
你好,好像那些图还是看不到
2023-01-12 at 10:08 pm
你好,好像那些图还是看不到
2023-01-12 at 10:08 pm
你好,好像那些图还是看不到
2023-02-06 at 7:28 pm
你好,后面的图看不到
2023-02-06 at 7:28 pm
你好,后面的图看不到
2023-02-13 at 1:44 pm
你好,文中部分图显示不出来,请问可以调试一下吗