注意
本项目代码需要使用GPU环境来运行:
 
 
   并且检查相关参数设置, 例如use_gpu, fluid.CUDAPlace(0)等处是否设置正确.
本项目将演示,如何使用PaddleHub语义预训练模型ERNIE完成从快递单中抽取姓名、电话、省、市、区、详细地址等内容,形成结构化信息。辅助物流行业从业者进行有效信息的提取,从而降低客户填单的成本。
PART A. 背景介绍
A.1 物流信息抽取任务
如何从物流信息中抽取想要的关键信息呢?首先需要定义下想要的结果应该如何表示。
比如现在拿到一个快递单,可以作为我们的模型输入,例如“张三18625584663广东省深圳市南山区学府路东百度国际大厦”,那么序列标注模型的目的就是识别出其中的“张三”为人名(用符号 P 表示),“18625584663”为电话名(用符号 T 表示),“广东省深圳市南山区百度国际大厦”分别是 1-4 级的地址(分别用 A1~A4 表示,可以释义为省、市、区、街道)。
如下表:
| 抽取字段 | 简称 | 抽取结果 | 
|---|---|---|
| 姓名 | P | 张三 | 
| 电话 | T | 18625584663 | 
| 省 | A1 | 广东省 | 
| 市 | A2 | 深圳市 | 
| 区 | A3 | 南山区 | 
| 详细地址 | A4 | 百度国际大厦 | 
A.2 序列标注模型
我们可以用序列标注模型来解决快递单的信息抽取任务,下面具体介绍一下序列标注模型。
在序列标注任务中,一般会定义一个标签集合,来表示所以可能取到的预测结果。在本案例中,针对需要被抽取的“姓名、电话、省、市、区、详细地址”等实体,标签集合可以定义为:
label = {P-B, P-I, T-B, T-I, A1-B, A1-I, A2-B, A2-I, A3-B, A3-I, A4-B, A4-I, O}
每个标签的定义分别为:
| 标签 | 定义 | 
|---|---|
| P-B | 姓名起始位置 | 
| P-I | 姓名中间位置或结束位置 | 
| T-B | 电话起始位置 | 
| T-I | 电话中间位置或结束位置 | 
| A1-B | 省份起始位置 | 
| A1-I | 省份中间位置或结束位置 | 
| A2-B | 城市起始位置 | 
| A2-I | 城市中间位置或结束位置 | 
| A3-B | 县区起始位置 | 
| A3-I | 县区中间位置或结束位置 | 
| A4-B | 详细地址起始位置 | 
| A4-I | 详细地址中间位置或结束位置 | 
| O | 不关注的字 | 
注意每个标签的结果只有 B、I、O 三种,这种标签的定义方式叫做 BIO 体系,也有稍麻烦一点的 BIESO 体系,这里不做展开。其中 B 表示一个标签类别的开头,比如 P-B 指的是姓名的开头;相应的,I 表示一个标签的延续。
对于句子“张三18625584663广东省深圳市南山区百度国际大厦”,每个汉字及对应标签为:
| 张 | 三 | 1 | 8 | 6 | 2 | 5 | 5 | 8 | 4 | 6 | 6 | 3 | 广 | 东 | 省 | 深 | 圳 | 市 | 南 | 山 | 区 | 百 | 度 | 国 | 际 | 大 | 厦 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| P-B | P-I | T-B | T-I | T-I | T-I | T-I | T-I | T-I | T-I | T-I | T-I | T-I | A1-B | A1-I | A1-I | A2-B | A2-I | A2-I | A3-B | A3-I | A3-I | A4-B | A4-I | A4-I | A4-I | A4-I | A4-I | 
注意到“张“,”三”在这里表示成了“P-B” 和 “P-I”,反过来讲,得到“P-B”和“P-I”这样的序列,也可以合并成“P” 这个标签。这样重新组合后可以得到以下信息抽取结果:
| 张三 | 18625584663 | 广东省 | 深圳市 | 南山区 | 百度国际大厦 | 
|---|---|---|---|---|---|
| P | T | A1 | A2 | A3 | A4 | 
我们可以通过以下例子,观察模型的输出结果。
# 解压数据集
%cd /home/aistudio/data/data16246/ 
!tar -zxvf express_ner.tar.gz 
!cd /home/aistudio/data/data12872 && unzip -q -o labeling_data.zip
# 解压模型
!cd /home/aistudio/data/data12872 && unzip -q -o models_example.zip && mv models_example /home/aistudio/work/
!ls /home/aistudio/work
!pip install --upgrade paddlehub -i https://pypi.tuna.tsinghua.edu.cn/simple# 查看预测的数据
!head -n 3 /home/aistudio/data/data12872/data/test.txtPART B. 常用模型
序列标注任务常用的模型是LSTM+CRF。如下图所示,LSTM 的输出可以作为 CRF 的输入,最后 CRF 的输出作为模型整体的预测结果。
 
 
  我们直接调用 LSTM-CRF 模型看下效果。
# 训练lstm-crf模型
!cd /home/aistudio/work/ && chmod 755 run.sh
!cd /home/aistudio/work/ && ./run.sh train lstm-crfPART C. 语义预训练模型ERNIE优化信息抽取
如果你对预训练模型感兴趣,如谷歌的 BERT 模型,或者百度的 ERNIE 模型,也值得在自己的任务试一试效果。
百度的预训练模型ERNIE经过海量的数据训练后,其特征抽取的工作已经做的非常好。借鉴迁移学习的思想,我们可以利用其在海量数据中学习的语义信息辅助小数据集(如本示例中的快递单数据集)上的任务。
 PaddleHub提供了丰富的预训练模型,并且可以便捷地获取PaddlePaddle生态下的所有预训练模型。下面展示如何使用PaddleHub一键加载ERNIE,优化信息抽取任务。
 PaddleHub提供了丰富的预训练模型,并且可以便捷地获取PaddlePaddle生态下的所有预训练模型。下面展示如何使用PaddleHub一键加载ERNIE,优化信息抽取任务。 
  C.1 PaddleHub加载自定义数据集
加载文本类自定义数据集,用户仅需要继承HubDataset类,替换数据集存放地址即可。 下面代码示例展示如何将自定义数据集加载进PaddleHub使用。这样我们只需要在小数据集上微调(Fine-tune)预训练模型即可。
具体详情可参考 加载自定义数据集
# 加载自定义数据集--快递单数据集
import os
import codecs
import csv
from paddlehub.dataset import InputExample, HubDataset
class Express_NER(HubDataset):
    """
    A set of manually annotated Chinese word-segmentation data about express information extraction.
    For more information please refer to
    https://aistudio.baidu.com/aistudio/projectdetail/131360
    """
    def __init__(self):
        # 快递单数据集存放地址
        self.dataset_dir = "/home/aistudio/data/data16246/express_ner"
        self._load_train_examples()
        self._load_test_examples()
        self._load_dev_examples()
    def _load_train_examples(self):
        train_file = os.path.join(self.dataset_dir, "train.txt")
        self.train_examples = self._read_file(train_file)
    def _load_dev_examples(self):
        self.dev_file = os.path.join(self.dataset_dir, "dev.txt")
        self.dev_examples = self._read_file(self.dev_file)
    def _load_test_examples(self):
        self.test_file = os.path.join(self.dataset_dir, "test.txt")
        self.test_examples = self._read_file(self.test_file)
    def get_train_examples(self):
        return self.train_examples
    def get_dev_examples(self):
        return self.dev_examples
    def get_test_examples(self):
        return self.test_examples
    def get_labels(self):
        return [
            "B-P", "I-P", "B-T", "I-T", "B-A1", "I-A1", "B-A2", "I-A2", "B-A3",
            "I-A3", "B-A4", "I-A4", "O"
        ]
    @property
    def num_labels(self):
        """
        Return the number of labels in the dataset.
        """
        return len(self.get_labels())
    def _read_file(self, input_file, quotechar=None):
        """Reads a tab separated value file."""
        with codecs.open(input_file, "r", encoding="UTF-8") as f:
            reader = csv.reader(f, delimiter="\t", quotechar=quotechar)
            examples = []
            seq_id = 0
            # 跳过表头
            header = next(reader)  # skip header
            for line in reader:
                example = InputExample(
                    guid=seq_id, label=line[1], text_a=line[0])
                seq_id += 1
                examples.append(example)
            return examplesdataset = Express_NER()
count = 0
sum_len = 0
for e in dataset.get_train_examples():
    count += 1
    sum_len += len(e.text_a)
    if count < 3:
        print("{}\t{}\t{}".format(e.guid, e.text_a, e.label))C.2 PaddleHub一键加载ERNIE
 
 
    
 
  import paddlehub as hub
module = hub.Module(name="ernie")如果想尝试其他语义模型(如ernie_tiny, RoBERTa等),只需要更换Module中的name参数即可.
| 模型名 | PaddleHub Module | 
|---|---|
| ERNIE, Chinese | hub.Module(name='ernie') | 
| ERNIE Tiny, Chinese | hub.Module(name='ernie_tiny') | 
| ERNIE 2.0 Base, English | hub.Module(name='ernie_v2_eng_base') | 
| ERNIE 2.0 Large, English | hub.Module(name='ernie_v2_eng_large') | 
| RoBERTa-Large, Chinese | hub.Module(name='roberta_wwm_ext_chinese_L-24_H-1024_A-16') | 
| RoBERTa-Base, Chinese | hub.Module(name='roberta_wwm_ext_chinese_L-12_H-768_A-12') | 
| BERT-Base, Uncased | hub.Module(name='bert_uncased_L-12_H-768_A-12') | 
| BERT-Large, Uncased | hub.Module(name='bert_uncased_L-24_H-1024_A-16') | 
| BERT-Base, Cased | hub.Module(name='bert_cased_L-12_H-768_A-12') | 
| BERT-Large, Cased | hub.Module(name='bert_cased_L-24_H-1024_A-16') | 
| BERT-Base, Multilingual Cased | hub.Module(nane='bert_multi_cased_L-12_H-768_A-12') | 
| BERT-Base, Chinese | hub.Module(name='bert_chinese_L-12_H-768_A-12') | 
C.3 构建Reader
接着生成一个序列标注的reader,reader负责将dataset的数据进行预处理,首先对文本进行切词,接着以特定格式组织并输入给模型进行训练。
SequenceLabelReader的参数有以下三个:
- dataset: 传入PaddleHub Dataset;
- vocab_path: 传入ERNIE/BERT模型对应的词表文件路径;
- max_seq_len: ERNIE模型的最大序列长度,若序列长度不足,会通过padding方式补到- max_seq_len, 若序列长度大于该值,则会以截断方式让序列长度为- max_seq_len;
 
 
  reader = hub.reader.SequenceLabelReader(
        dataset=dataset,
        vocab_path=module.get_vocab_path(),
        max_seq_len=128)C.4 选择Fine-Tune优化策略
适用于ERNIE/BERT这类Transformer模型的迁移优化策略为AdamWeightDecayStrategy。详情请查看Strategy。
AdamWeightDecayStrategy的参数:
- learning_rate: 最大学习率
- lr_scheduler: 有- linear_decay和- noam_decay两种衰减策略可选
- warmup_proprotion: 训练预热的比例,若设置为0.1, 则会在前10%的训练step中学习率逐步提升到- learning_rate
- weight_decay: 权重衰减,类似模型正则项策略,避免模型overfitting
 
 
  strategy = hub.AdamWeightDecayStrategy(
    weight_decay=0.01,
    warmup_proportion=0.1,
    learning_rate=5e-5)PaddleHub提供了许多优化策略,如AdamWeightDecayStrategy、ULMFiTStrategy、DefaultFinetuneStrategy等,详细信息参见策略
C.5 选择运行配置
在进行Finetune前,我们可以设置一些运行时的配置,例如如下代码中的配置,表示:
-  use_cuda:设置为False表示使用CPU进行训练。如果您本机支持GPU,且安装的是GPU版本的PaddlePaddle,我们建议您将这个选项设置为True;
-  num_epoch:Finetune时遍历训练集的次数,;
-  batch_size:每次训练的时候,给模型输入的每批数据大小为16,模型训练时能够并行处理批数据,因此batch_size越大,训练的效率越高,但是同时带来了内存的负荷,过大的batch_size可能导致内存不足而无法训练,因此选择一个合适的batch_size是很重要的一步;
-  checkpoint_dir:训练的参数和数据的保存目录;
-  eval_interval:每隔50 step在验证集上进行一次性能评估;
-  strategy:Fine-tune策略;
更多运行配置,请查看RunConfig
config = hub.RunConfig(
    use_cuda=True,
    num_epoch=1,
    checkpoint_dir="hub_ernie_express_demo",
    batch_size=16,
    eval_interval=50,
    strategy=strategy)C.6 组建Finetune Task
有了合适的预训练模型和准备要迁移的数据集后,我们开始组建一个Task。
- 获取module的上下文环境,包括输入和输出的变量,以及Paddle Program;
- 从输出变量中找到用于序列标注的单词级特征sequence_output;
- 在sent_feature后面接入一个全连接层,生成Task;
SequenceLabelTask的参数有:
-  data_reader:读取数据的reader;
-  config: 运行配置;
-  feature:从预训练提取的特征;
-  feed_list:program需要输入的变量;
-  max_seq_len:ERNIE模型的最大序列长度,若序列长度不足,会通过padding方式补到max_seq_len, 若序列长度大于该值,则会以截断方式让序列长度为max_seq_len;
-  num_classes:数据集的类别数量;
-  add_crf: 选择是否加入crf作为decoder。如果add_crf=True, 则在预训练模型计算图加入fc+crf层,否则只在在预训练模型计算图加入fc层;
 
 
  NOTE: Reader参数max_seq_len、Task参数max_seq_len、moduel的context接口参数max_seq_len三者应该保持一致
inputs, outputs, program = module.context(
    trainable=True, max_seq_len=128)
# Use "sequence_output" for token-level output.
sequence_output = outputs["sequence_output"]
feed_list = [
    inputs["input_ids"].name,
    inputs["position_ids"].name,
    inputs["segment_ids"].name,
    inputs["input_mask"].name,
]
seq_label_task = hub.SequenceLabelTask(
    data_reader=reader,
    feature=sequence_output,
    feed_list=feed_list,
    add_crf=True,
    max_seq_len=128,
    num_classes=dataset.num_labels,
    config=config)如果想改变迁移任务组网,详细参见自定义迁移任务
C.7 开始Finetune
我们选择finetune_and_eval接口来进行模型训练,这个接口在finetune的过程中,会周期性的进行模型效果的评估,以便我们了解整个训练过程的性能变化。
run_states=seq_label_task.finetune_and_eval()PART D. 使用模型进行预测
当Finetune完成后,我们使用模型来进行预测,整个预测流程大致可以分为以下几步:
- 构建网络
- 生成预测数据的Reader
- 切换到预测的Program
- 加载预训练好的参数
- 运行Program进行预测
 
 
   预测代码如下:
import numpy as np
inv_label_map = {val: key for key, val in reader.label_map.items()}
# test data
# set "\002" to seperate the sentence in order to seperate the number sequence
data = [
    ["\002".join(list(u"喻晓刚云南省楚雄彝族自治州南华县东街古城路37号18513386163"))],
    ["\002".join(list(u"河北省唐山市玉田县无终大街159号18614253058尚汉生"))],
    ["\002".join(list(u"台湾嘉义县番路乡番路乡公田村龙头17之19号宣树毅13720072123"))],
]
run_states = seq_label_task.predict(data=data)
results = [run_state.run_results for run_state in run_states]
for num_batch, batch_results in enumerate(results):
    infers = batch_results[0].reshape([-1]).astype(np.int32).tolist()
    np_lens = batch_results[1]
    cut = 0
    for index, np_len in enumerate(np_lens):
        # max_seq_len: 128
        # get the predicted label of the input sentence
        labels = infers[cut : cut+int(np_len)]
        cut += int(np_len)
        label_str = ""
        sent_out_str = ""
        last_word = ""
        last_tag = ""
        #flag: cls position
        flag = 0
        count = 0
        # batch_size : 1
        sentence_str = data[num_batch * 1 +
                            index][0].strip().split("\002")
        for label_val in labels:
            if flag == 0:
                flag = 1
                continue
            if count == np_len - 2:
                break
            label_tag = inv_label_map[label_val]
            cur_word = sentence_str[count]
            if last_word == "":
                last_word = cur_word
                last_tag = label_tag.split("-")[1]
            elif label_tag.startswith("B-"):
                sent_out_str += last_word + u"/" + last_tag + u" "
                last_word = cur_word
                last_tag = label_tag.split("-")[1]
            elif label_tag == "O":
                sent_out_str += last_word + u"/" + last_tag + u" "
                last_word = cur_word
                last_tag = label_tag
            elif label_tag.startswith("I-"):
                last_word += cur_word
            else:
                raise ValueError("Invalid tag: %s" % (label_tag))
            count += 1
        if cur_word != "":
            sent_out_str += last_word + "/" + last_tag + " "
        print(sent_out_str)总的来说,PaddleHub完成迁移学习过程只需下图所展示的6步即可完成。
 
 
  想了解更多资讯,可访问飞桨PaddlePaddle官网 https://www.paddlepaddle.org.cn/?fr=osc
想尝试在线运行,可关注项目链接:https://aistudio.baidu.com/aistudio/projectdetail/215711
>> 访问 PaddlePaddle 官网,了解更多相关内容。
来源:oschina
链接:https://my.oschina.net/u/4067628/blog/3185260