Preface:fine-tuning到底是什么?
在预训练模型层上添加新的网络层,然后预训练层和新网络层联合训练。
文本分类的例子最典型了,最后加一个Dense层,把输出维度降至类别数,再进行sigmoid或softmax。
比如命名实体识别,在外面添加BiLSTM+CRF层,就成了BERT+BiLSTM+CRF模型。
这个例子可能不太典型,因为还是加了繁重的网络结构。
做多分类和多标签分类时,只需要用到以下四个文件。
├── tokenization.py # 所需文件四:用于文本预处理(分字)的文件
├── modeling.py # 所需文件一:BERT的网络模型文件
├── optimization.py # 所需要文件二:优化器文件
├── run_classifier.py # 所需文件三:模型的fine-tuning文件
需要修改的文件:run_classifier.py。
按模型的输入格式处理数据和导入数据,在预训练层加下游任务,修改评估函数和输出结果等。

└── chinese_L-12_H-768_A-12├── bert_config.json                     # BERT 的配置文件├── bert_model.ckpt.data-00000-of-00001  # 预训练的模型├── bert_model.ckpt.index├── bert_model.ckpt.meta                └── vocab.txt                            # BERT字粒度的词表

#词汇表 vocab.txt 解读
词汇表中共有21128行,编号从0开始,编号1-99的为unused字符串。[CLS]在102行,id为101;[SEP]在103行,id为102。

├── bert                                        # 需要用到的 BERT 文件
│   ├── __init__.py
│   ├── modeling.py
│   ├── optimization.py
│   ├── run_classifier.py
│   └── tokenization.py
├── config.py                                   # 参数配置
├── data_processor.py                           # 对样本做拆字
├── **metrics.py                                  # 自定义 f1,precision和recall**
├── pretrained_model                            # 预训练的中文BERT模型
│   └── chinese_L-12_H-768_A-12
│       ├── bert_config.json
│       ├── bert_model.ckpt.data-00000-of-00001
│       ├── bert_model.ckpt.index
│       ├── bert_model.ckpt.meta
│       └── vocab.txt
├── run_classifier.py                           # 定义下游任务并做训练、验证和预测脚本
├── run.sh                                      # 执行的shell脚本
└── run_test.py                                 # 模型测试脚本
#**修改模型**,在预训练层外面增加一个dense层,做softmax/sigmoid变换
#**create_model这个函数是最关键的**
def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,labels, num_labels, use_one_hot_embeddings):
#**修改模型评估函数**,增加F1值、precision和recall
def model_fn_builder(bert_config, num_labels, init_checkpoint, learning_rate,num_train_steps, num_warmup_steps, use_tpu,use_one_hot_embeddings):

前方高能,避免踩大坑:
一是对于多分类任务,不能根据值大于0.5这个判断条件,来得到索引,而是根据是否为最大值。
值大于0.5,来得到索引,适用于二分类和多标签分类。

predictions = tf.argmax(logits, axis=-1, output_type=tf.int32)

二是用 np.argmax() 来得到索引,而不能用 tf.argmax(),否则会报错(好像也行!?)。
报错的意思是:计算图已经构建结束了,不能再调整计算图。
我的理解是,这个probabilities已经是numpy的格式,而不是tensorflow的格式,如果再用 tensorflow 的函数,那就会调整计算图。这是不允许的。

一、多标签多分类 VS 多分类任务
**
针对多标签多分类任务(Multi-label classification task),微调模型时的最后一层全连接层输出需要使用的是sigmoid转换。

而对于多分类任务(Multi-class classification task),则只需要进行softmax变换即可。
softmax适用于多分类问题中对每一个类别的概率判断。
小结:
如果模型输出为非互斥类别,且可以同时选择多个类别,则采用Sigmoid函数计算该网络的原始输出值。
如果模型输出为互斥类别,且只能选择一个类别,则采用Softmax函数计算该网络的原始输出值。

二、如何让BERT模型输出precision、recall、F1-score等指标
1、对于多分类任务:修改run_classifier.py中的代码如下

def metric_fn(per_example_loss, label_ids, logits, is_real_example):predictions = tf.argmax(logits, axis=-1, output_type=tf.int32)accuracy = tf.metrics.accuracy(labels=label_ids, predictions=predictions, weights=is_real_example)loss = tf.metrics.mean(values=per_example_loss, weights=is_real_example)auc = tf.metrics.auc(labels=label_ids, predictions=predictions, weights=is_real_example)precision = tf.metrics.precision(labels=label_ids, predictions=predictions, weights=is_real_example)recall = tf.metrics.recall(labels=label_ids, predictions=predictions, weights=is_real_example)return {"eval_accuracy": accuracy,"eval_loss": loss,'eval_auc': auc,'eval_precision': precision,'eval_recall': recall,}

2、对于多标签多分类任务:

三、除了直接基于预训练模型的获得词向量,如何基于微调训练获得词向量?
1.基于预训练模型直接获得768维的词向量
该方法简单,但是后期任务的效果很差,至少不会好!
直接基于肖涵博士的***bert-as-service***获得词向量。
准备工作:
安装bert服务端:pip install bert-serving-server
安装bert服务客户端:pip install bert-serving-client
在命令行输入: bert-serving-start -model_dir E:/chinese_L-12_H-768_A-12
-num_worker=2(训练好的中文预模型路径,num_worker的数量表示最高处理来自2个客户端的并发请求),如果成功开启则出现以下界面。

from bert_serving.client import BertClient
bc = BertClient()
a=bc.encode(['浙江投融界科技有限公司#服务:计算机软硬件、网络信息技术的技术开发、技术咨询、技术服务、成果转让,第二类增值电信业务中的信息服务业务(仅限互联网信息服务),计算机系统集成,实业投资、投资管理、投资咨询(以上项目除证券、期>货,未经金融等监管部门批准,不得从事向公众融资存款、融资担保、代客理财等金融服务),企业管理咨询,市场营销策划,网页>设计,承接网络工程(涉及资质证凭证经营),会展服务,经济信息咨询、商务信息咨询(除中介),设计、制作国内广告;其他无>需报经审批的一切合法项目。(依法须经批准的项目,经相关部门批准后方可开展经营活动)'])
print(a)
print(len(a[0]))

具体可参考:https://github.com/hanxiao/bert-as-service(肖涵博士Github)
2.基于run_classifier.py微调模型获得768维的词向量
只需修改run_classifier.py中的fine-tuning代码,从而输出相应的经微调训练之后的词向量。
该方法稍微有点耗时耗资源,但是后期任务的效果较好,至少不会很差!

def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,labels, num_labels, use_one_hot_embeddings):"""Creates a classification model."""model = modeling.BertModel(config=bert_config,is_training=is_training,input_ids=input_ids,input_mask=input_mask,token_type_ids=segment_ids,use_one_hot_embeddings=use_one_hot_embeddings)output_layer = model.get_pooled_output()     #分类任务的768维词向量hidden_size = output_layer.shape[-1].value # 768output_weights = tf.get_variable("output_weights", [num_labels, hidden_size],initializer=tf.truncated_normal_initializer(stddev=0.02))output_bias = tf.get_variable("output_bias", [num_labels], initializer=tf.zeros_initializer())with tf.variable_scope("loss"):if is_training:output_layer = tf.nn.dropout(output_layer, keep_prob=0.9)logits = tf.matmul(output_layer, output_weights, transpose_b=True)logits = tf.nn.bias_add(logits, output_bias)#probabilities是由输出向量经sigmoid变换得到的#多分类问题,但是每个样本只属于一个类别,softmax交叉熵算出来的是一个值#多分类问题,且一个样本可以同时拥有多个标签,一个样本会在每个类别上有一个交叉熵,使用tf.sigmoid(与tf.nn.sigmoid相同,但最好用tf.sigmoid)probabilities = tf.sigmoid(logits)label_ids = tf.cast(labels, tf.float32)per_example_loss = tf.reduce_sum(tf.nn.sigmoid_cross_entropy_with_logits(logits=logits, labels=label_ids), axis=-1)  #logits和labels必须有相同的类型和大小loss = tf.reduce_mean(per_example_loss)#         probabilities = tf.nn.softmax(logits, axis=-1)
#         log_probs = tf.nn.log_softmax(logits, axis=-1)#         one_hot_labels = tf.one_hot(labels, depth=num_labels, dtype=tf.float32)#         per_example_loss = -tf.reduce_sum(one_hot_labels * log_probs, axis=-1)
#         loss = tf.reduce_mean(per_example_loss)return (loss, per_example_loss, logits, probabilities, output_layer)  #只需在return中增加output_layer即可。
def model_fn_builder(bert_config, num_labels, init_checkpoint, learning_rate,num_train_steps, num_warmup_steps, use_tpu,use_one_hot_embeddings):"""Returns `model_fn` closure for TPUEstimator."""def model_fn(features, labels, mode, params):  # pylint: disable=unused-argument"""The `model_fn` for TPUEstimator."""tf.logging.info("*** Features ***")for name in sorted(features.keys()):tf.logging.info("  name = %s, shape = %s" % (name, features[name].shape))input_ids = features["input_ids"]input_mask = features["input_mask"]segment_ids = features["segment_ids"]label_ids = features["label_ids"]is_training = (mode == tf.estimator.ModeKeys.TRAIN)#此处增加一个output_layer(total_loss, per_example_loss, logits, probabilities, ***output_layer***) = create_model(bert_config, is_training, input_ids, input_mask, segment_ids, label_ids,num_labels, use_one_hot_embeddings)tvars = tf.trainable_variables()scaffold_fn = Noneif init_checkpoint:(assignment_map, initialized_variable_names) = modeling.get_assignment_map_from_checkpoint(tvars, init_checkpoint)if use_tpu:def tpu_scaffold():tf.train.init_from_checkpoint(init_checkpoint, assignment_map)return tf.train.Scaffold()scaffold_fn = tpu_scaffoldelse:tf.train.init_from_checkpoint(init_checkpoint, assignment_map)tf.logging.info("**** Trainable Variables ****")for var in tvars:init_string = ""if var.name in initialized_variable_names:init_string = ", *INIT_FROM_CKPT*"tf.logging.info("  name = %s, shape = %s%s", var.name, var.shape,init_string)output_spec = Noneif mode == tf.estimator.ModeKeys.TRAIN:train_op = optimization.create_optimizer(total_loss, learning_rate, num_train_steps, num_warmup_steps, use_tpu)output_spec = tf.contrib.tpu.TPUEstimatorSpec(mode=mode,loss=total_loss,train_op=train_op,scaffold_fn=scaffold_fn)elif mode == tf.estimator.ModeKeys.EVAL:def metric_fn(per_example_loss, label_ids, probabilities):predict_ids = tf.cast(probabilities > 0.5, tf.int32)label_ids = tf.cast(label_ids, tf.int32)elements_equal = tf.cast(tf.equal(predict_ids, label_ids), tf.int32)   #tf.equal():逐个元素判断是否相等row_predict_ids = tf.reduce_sum(elements_equal, -1)row_label_ids = tf.reduce_sum(tf.ones_like(label_ids), -1)accuracy = tf.metrics.accuracy(labels=row_label_ids, predictions=row_predict_ids)loss = tf.metrics.mean(per_example_loss)return {"eval_accuracy": accuracy,"eval_loss": loss,}eval_metrics = (metric_fn, [per_example_loss, label_ids, probabilities])output_spec = tf.contrib.tpu.TPUEstimatorSpec(mode=mode,loss=total_loss,eval_metrics=eval_metrics,scaffold_fn=scaffold_fn)else:output_spec = tf.contrib.tpu.TPUEstimatorSpec(mode=mode,predictions={"probabilities": probabilities, ***"output_layer":output_layer***},scaffold_fn=scaffold_fn)    #predictions增加"output_layer":output_layerreturn output_specreturn model_fn

OK,微调后的词向量提取成功!

四、多标签分类任务中,阈值一般真的是设0.5吗?
阈值绝大部分确实是设置为0.5的。
但自己在实际中会遇到一些样本预测的输出向量中的所有“概率”数据都小于0.5,因此这些样本就不会有预测标签。现实中也确实存在这种问题,模型对这部分的样本的确区分不出来,也有可能是因为相应标签对应的样本数很少,在划分数据集时,shuffle之后的trainset数据集中有的标签并未抽取到,因此验证时必定有的标签得不出预测标签。

BERT模型fine-tuning过程代码实战,以run_classifier.py为例。

BERT官方Github地址:https://github.com/google-research/bert ,其中对BERT模型进行了详细的介绍,更详细的可以查阅原文献:https://arxiv.org/abs/1810.04805 。

BERT本质上是一个两段式的NLP模型。第一个阶段叫做:Pre-training,跟WordEmbedding类似,利用现有无标记的语料训练一个语言模型。第二个阶段叫做:Fine-tuning,利用预训练好的语言模型,完成具体的NLP下游任务。

Google已经投入了大规模的语料和昂贵的机器帮我们完成了Pre-training过程,这里介绍一下不那么expensive的fine-tuning过程。

回到Github中的代码,只有run_classifier.py和run_squad.py是用来做fine-tuning 的,其他的可以暂时先不管。这里使用run_classifier.py进行句子分类任务。

代码解析

从主函数开始,可以发现它指定了必须的参数:

if __name__ == "__main__":flags.mark_flag_as_required("data_dir")flags.mark_flag_as_required("task_name")flags.mark_flag_as_required("vocab_file")flags.mark_flag_as_required("bert_config_file")flags.mark_flag_as_required("output_dir")tf.app.run()

从这些参数出发,可以对run_classifier.py进行探索:

data_dir

指的是我们的输入数据的文件夹路径。查看代码,不难发现,作者给出了输入数据的格式:

class InputExample(object):"""A single training/test example for simple sequence classification."""

def init(self, guid, text_a, text_b=None, label=None):
“”“Constructs a InputExample.
Args:
guid: Unique id for the example.
text_a: string. The untokenized text of the first sequence. For single
sequence tasks, only this sequence must be specified.
text_b: (Optional) string. The untokenized text of the second sequence.
Only must be specified for sequence pair tasks.
label: (Optional) string. The label of the example. This should be
specified for train and dev examples, but not for test examples.
“””
self.guid = guid
self.text_a = text_a
self.text_b = text_b
self.label = label

可以发现它要求的输入分别是guid, text_a, text_b, label,其中text_b和label为可选参数。例如我们要做的是单个句子的分类任务,那么就不需要输入text_b;另外,在test样本中,我们便不需要输入lable。

task_name

这里的task_name,一开始可能不好理解它是用来做什么的。仔细查看代码可以发现:

  processors = {"cola": ColaProcessor,"mnli": MnliProcessor,"mrpc": MrpcProcessor,"xnli": XnliProcessor,}
task_name = FLAGS.task_name.lower()

if task_name not in processors:
raise ValueError(“Task not found: %s” % (task_name))

processor = processors[task_name]()

task_name是用来选择processor的。

继续查看processor,这里以“mrpc”为例:

class MrpcProcessor(DataProcessor):"""Processor for the MRPC data set (GLUE version)."""

def get_train_examples(self, data_dir):
“”“See base class.”""
return self._create_examples(
self._read_tsv(os.path.join(data_dir, “train.tsv”)), “train”)

def get_dev_examples(self, data_dir):
“”“See base class.”""
return self._create_examples(
self._read_tsv(os.path.join(data_dir, “dev.tsv”)), “dev”)

def get_test_examples(self, data_dir):
“”“See base class.”""
return self._create_examples(
self._read_tsv(os.path.join(data_dir, “test.tsv”)), “test”)

def get_labels(self):
“”“See base class.”""
return [“0”, “1”]

def _create_examples(self, lines, set_type):
“”“Creates examples for the training and dev sets.”""
examples = []
for (i, line) in enumerate(lines):
if i 0:
continue
guid = “%s-%s” % (set_type, i)
text_a = tokenization.convert_to_unicode(line[3])
text_b = tokenization.convert_to_unicode(line[4])
if set_type “test”:
label = “0”
else:
label = tokenization.convert_to_unicode(line[0])
examples.append(
InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
return examples

可以发现这个processor就是用来对data_dir中输入的数据进行预处理的。
同时也能发现,在data_dir中我们需要将数据处理成.tsv格式,训练集、开发集和测试集分别是train.tsv, dev.tsv, test.tsv,这里我们暂时只使用train.tsv和dev.tsv。另外,label在get_labels()设定,如果是二分类,则将label设定为[“0”,”1”],同时_create_examples()中,给定了如何获取guid以及如何给text_a, text_b和label赋值。

到这里,似乎已经明白了什么。对于这个fine-tuning过程,我们要做的只是:

  • 准备好一个12G显存左右的GPU,没有也不用担心,可以使用谷歌免费的GPU

  • 准备好train.tsv, dev.tsv以及test.tsv

  • 新建一个跟自己task_name对应的processor,用于将train.tsv、dev.tsv以及test.tsv中的数据提取出来赋给text_a, text_b, label

  • 下载好Pre-training模型,设定好相关参数,run就完事了

“vocab_file”, "bert_config_file"以及"output_dir"很好理解,分别是BERT预训练模型的路径和fine-tuning过程输出的路径

fine-tuning实践

准备好train.tsv, dev.tsv以及test.tsv

tsv,看上去怪怪的。其实好像跟csv没有多大区别,反正把后缀改一改就完事。这里我要做的是一个4分类,示例在下面:

train.tsv: (标签+’\t’+句子)

dev.tsv:(标签+’\t’+句子)

test.tsv:(句子)

新建processor

这里我将自己的句子分类任务命名为”bert_move”:

processors = {"cola": ColaProcessor,"mnli": MnliProcessor,"mrpc": MrpcProcessor,"xnli": XnliProcessor,"bert_move": MoveProcessor}

然后仿照MrpcProcessor创建自己的MoveProcessor:

class MoveProcessor(DataProcessor):"""Processor for the move data set ."""

def get_train_examples(self, data_dir):
“”“See base class.”""
return self._create_examples(
self._read_tsv(os.path.join(data_dir, “train.tsv”)), “train”)

def get_dev_examples(self, data_dir):
“”“See base class.”""
return self._create_examples(
self._read_tsv(os.path.join(data_dir, “dev.tsv”)), “dev”)

def get_test_examples(self, data_dir):
“”“See base class.”""
return self._create_examples(
self._read_tsv(os.path.join(data_dir, “test.tsv”)), “test”)

def get_labels(self):
“”“See base class.”""
return [“0”, “1”, “2”, “3”]

def _create_examples(self, lines, set_type):
“”“Creates examples for the training and dev sets.”""
examples = []
for (i, line) in enumerate(lines):
guid = “%s-%s” % (set_type, i)
if set_type == “test”:
text_a = tokenization.convert_to_unicode(line[0])
label = “0”
else:
text_a = tokenization.convert_to_unicode(line[1])
label = tokenization.convert_to_unicode(line[0])
examples.append(
InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
return examples

其中,主要修改的是:

  • get_labels()中设置4分类的标签[‘0’, ‘1’, ‘2’,’ 3’]
  • _create_examples()中提取文本赋给text_a和label,并做一个判断,当文件名是test.tsv时,只赋给text_a,label直接给0
  • guid则为自动生成

设定参数,运行fine-tuning

相关的参数可以直接在run_classifier.py中一开始的flags里面直接做修改,然后运行就行。但是又研究了一下Github里面设置参数的方式:

python run_classifier.py \--task_name=MRPC \--do_train=true \--do_eval=true \--data_dir=$GLUE_DIR/MRPC \--vocab_file=$BERT_BASE_DIR/vocab.txt \--bert_config_file=$BERT_BASE_DIR/bert_config.json \--init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \--max_seq_length=128 \--train_batch_size=32 \--learning_rate=2e-5 \--num_train_epochs=3.0 \--output_dir=/tmp/mrpc_output/

对其中的一些参数做一些解释:

  • do_train, do_eval和do_test至少要有一个是True,一般做fine-tuning训练时,将do_train和do_eval设置为True,do_test设置为False(默认),当模型都训练好了,就可以只将do_test设置为True,将会自动调用保存在output_dir中已经训练好的模型,进行测试。
  • max_seq_length、train_batch_size可以根据自己的设备情况适当调整,目前默认的参数在GTX 1080Ti 以及谷歌Colab提供的免费GPU Tesla K80中经过测试,完美运行。
  • 关于预训练模型,官方给出了两种模型,Large和Base,具体可以看Github介绍以及论文,目前上面的两种设备经过多次测试,只能支持Base模型,Large模型显然需要更大显存的机器(TPU)。

BERT模型fine-tuning相关推荐

  1. Bert模型 fine tuning 代码run_squad.py学习

    文章目录 关于run_squad.py 分模块学习 SquadExample InputFeatures create_model model_fn_builder input_fn_builder ...

  2. [NLP自然语言处理]谷歌BERT模型深度解析

    BERT模型代码已经发布,可以在我的github: NLP-BERT--Python3.6-pytorch 中下载,请记得start哦 目录 一.前言 二.如何理解BERT模型 三.BERT模型解析 ...

  3. 自然语言处理——谷歌BERT模型深度解析

    BERT模型代码已经发布,可以在我的github: NLP-BERT--Python3.6-pytorch 中下载,请记得start哦 目录 一.前言 二.如何理解BERT模型 三.BERT模型解析 ...

  4. BERT模型的详细介绍

    1.BERT 的基本原理是什么? BERT 来自 Google 的论文Pre-training of Deep Bidirectional Transformers for Language Unde ...

  5. 干货 | 谷歌BERT模型fine-tune终极实践教程

    作者 | 奇点机智 从11月初开始,Google Research就陆续开源了BERT的各个版本.Google此次开源的BERT是通过TensorFlow高级API-- tf.estimator进行封 ...

  6. [深度学习] 自然语言处理 --- BERT模型原理

    一 BERT简介 NLP:自然语言处理(NLP)是信息时代最重要的技术之一.理解复杂的语言也是人工智能的重要组成部分.Google AI 团队提出的预训练语言模型 BERT(Bidirectional ...

  7. pytorch深度学习-微调(fine tuning)

    微调(fine tuning) 首先举一个例子,假设我们想从图像中识别出不同种类的椅子,然后将购买链接推荐给用户.一种可能的方法是先找出100种常见的椅子,为每种椅子拍摄1,000张不同角度的图像,然 ...

  8. BERT模型实战之多文本分类(附源码)

    BERT模型也出来很久了,之前看了论文学习过它的大致模型(可以参考前些日子写的笔记NLP大杀器BERT模型解读),但是一直有杂七杂八的事拖着没有具体去实现过真实效果如何.今天就趁机来动手写一写实战,顺 ...

  9. pretraining+fine tuning

    few shotlearning简单实现方法:在大规模数据做预训练模型,然后在小规模suport set上做fine tuning.方法简单准确率高. 基础数学知识: cos函数可以判断两个向量的相似 ...

  10. 如何fine tuning

    先看一个示例 keras入门 -在预训练好网络模型上进行fine-tune https://blog.csdn.net/hnu2012/article/details/72179437 我们的方法是这 ...

最新文章

  1. 1.4 w字,25 张图让你彻底掌握分布式事务原理
  2. 5G加速下的云办公时代来临?阿里云新品服务器 - 无影云桌面的服务开通与体验,本地客户端连接阿里云无影云桌面演示
  3. target和currentTarget
  4. 转我们经理的一篇文章,业务流程实现的讨论,希望大家集思广议。
  5. lamp架构-访问控制-禁止php解析、屏蔽curl命令访问
  6. 搭建微信公共平台的本地测试
  7. synchronized关键字实现同步
  8. android图片上加有汉字,Android 为图片添加文字水印
  9. FZU 2108 Mod problem
  10. 拖拽之路(四):自定义QListView实现美观的拖拽样式(拖拽不影响选中)
  11. paip.项目开发效率提升之思索
  12. 烧录工具Android Tool的使用
  13. centos配置静态ip和路由
  14. office基础操作
  15. SEO入门:网站站内优化流程
  16. SEO入门教程之入门相关
  17. 防火墙双机热备升级步骤
  18. PS3安装Linux Fedora Core 6教程
  19. 人脸识别技术禁令再来!美国又一城市禁止面部识别软件
  20. python中utf-8编码_Python 使用 UTF-8 编码(转)

热门文章

  1. mysql中的scn,oracle数据库SCN概念
  2. 父类能调用子类方法么
  3. [日语二级词汇]动词(3)
  4. 实训双绞线制作心得体会
  5. C - Emojis Gym - 102566C题解
  6. QSplitter设置比例
  7. Smart Link和Monitor Link
  8. Oracle表空间扩展
  9. SiT3807:高性能单端压控振荡器VCXO
  10. 集合—HashMap源码