\u200E
AI如何助推电力行业新未来?飞桨ERNIE有妙招!
发布日期:2021-11-30T03:57:54.000+0000 浏览量:37次

电力是推动国家经济发展的重要动力,在我国快速发展的过程中承担着不可替代的作用。 电厂设备在高强度运行过程中难免会发生故障,传统的电力设备故障研判主要依赖于专工的经验和专家知识,这些专家经验主要来源于生产工作中的日积月累,多表现为碎片化的维护数据。


传统人工处理的局限性


长期以来,电厂设备的故障研判主要依赖专业人员的人工处理。 这对从业人员的要求非常高,且在效率、研判标准等方面难以达成统一。 随着浙能集团近年来信息化建设水平的不断提高,累积的历史维护数据越来越受到重视。



传统机器学习建模的局限性


传统的机器学习模型可以利用设备的历史维护数据助力故障研判,但是会存在一定的局限性,无法很好 地满足业务需求:
  1. 过于依赖特征工程,对人力以及工程师个人经验要求较高。
  2. 对文本数据的处理缺乏上下文理解,无法关注到重点,可能导致理解偏差。
  3. 基于小样本的监督学习,无法利用海量数据的优势助力语义理解能力。



业务理解


以各电厂设备维护数据为分析对象,运用 NLP 技术,深度挖掘企业数据价值,可以实现对电厂设备故障的智能研判。 具体来说,根据工作人员填写的设备故障文本,自动对故障的缺陷进行分类,迅速定位对应的检修专业部门,并智能推送针对该故障的消缺处理方法与建议,解决专工在遇到自己不擅长的故障的情况下,消缺处理效率低、研判准确率低等问题,为快速定位电厂设备故障原因和故障设备恢复提供有力支撑。



技术抽象


上述 故障智能维护 问题可以抽象分解为两个 NLP 任务:
  • 文本分类任务:基于故障描述文本,判断故障的类别或定位对应的检修专业部门。

  • 文本生成任务:基于故障描述文本,生成维护建议。


具体流程为:
1.当设备出现故障时,采集故障文本信息,如“废水排放口COD表计长时间无变化”。
2.对故障文本其进行一些基本的预处理(如分词、ID化等)。
3.将数据分别输入文本分类与文本生成的预训练语义模型,预测故障的类别、专业类型与维护建议等。
4.根据输出的专业类型呼叫对应的维护班组(这里是仪控部门),为其提示故障类别,提供维护建议,协助其维修设备。
5.当设备正常运行后,此次处理的记录也会归档到历史设备维护文档中去,用于进一步训练语义模型。




技术选型


本项目主要涉及 PaddleNLP 和 PaddleHub 两个库:
  • PaddleNLP 具备易用的文本领域 API,多场景的应用示例、和高性能分布式训练 三大特点:提供从数据集加载、文本预处理、模型组网、模型评估、到推理加速的领域 API 和多粒度多场景的应用示例,并且基于飞桨核心框架『 动静统一 』的特性与领先的自动混合精度优化策略,能够根据硬件情况灵活调配,高效地完成超大规模参数的模型训练。
  • PaddleHub 可以便捷地获取 PaddlePaddle 生态下的丰富的预训练模型,完成模型的管理和一键预测。配合使用 Fine-tune API,可以基于大规模预训练模型快速完成迁移学习,让预训练模型能更好地服务于我们特定场景的应用。

百度基于飞桨平台自研的语义理解框架ERNIE曾经在视觉媒体的关键文本片段挖掘、多语攻击性语言检测、混合语种的情感分析等多项任务中取得SOTA成绩。BERT对word进行mask,而ERNIE 1.0在此基础上进一步对entity和phrase进行mask,能够更好地捕捉语义信息。借助百度在中文社区的强大能力,中文的 ERNIE 预训练模型使用了各种异质(Heterogeneous)的数据集(中文维基百科、百度百科、百度新闻、百度贴吧等),在中文NLP任务上表现突出。

笔者由此选择使用动态图版的ERNIE-1.0作为文本分类任务的预训练模型,ERNIE-GEN作为文本生成任务的预训练模型。

以文本分类任务为例,使用以下代码加载预训练模型:
import paddlehub as hub
import paddle

model = hub.Module(name='ernie', task='seq-cls', num_classes=len(MyDataset.label_list))



数据处理




基本预处理

笔者使用的数据均来自某电厂2019到2020年度设备维护真实数据。
  • 筛去维护建议中诸如“已处理”、“现正常”、“可投运”以及空值等无意义的数据。
  • 筛去部分无意义的 #等特殊符号,保留大部分标点与停用词。
  • 分词和嵌入直接使用预训练模型处理(例如:使用ERNIE模型内置分词器ernieTokenizer进行分词,并将明文处理为ID)。

最终,得到 36746条训练数据,4537条验证数据,以及4083条测试数据。
将数据集目录设定为如下格式:
├──mydata: 数据目录
   ├── train.txt: 训练集数据
   ├── dev.txt: 验证集数据
   └── test.txt: 测试集数据

数据文件的第一列是文本类别标签,第二列为文本内容,列与列之间以Tab键分隔。示例如下:
label    text_a
锅炉专业    捞渣机液压张紧油站处地沟堵塞
脱硫专业    脱硫冷却水至机喷淋装置进口电动阀54%处卡涩,无法关闭。
电气专业    网源协调分析装置屏柜内部分端口未禁用

定义文本分类数据集:
from paddlehub.datasets.base_nlp_dataset import TextClassificationDataset

class MyDataset(TextClassificationDataset):
    # 数据集存放目录
    base_path = '/home/aistudio/data/' + dataID
    # 数据集的标签列表
    f = open(base_path + '/label_list.txt''r')
    label_list = f.read().strip().split('\t')
    f.close()

    def __init__(self, tokenizer, max_seq_len: int = 128, mode: str = 'train'):
        if mode == 'train':
            data_file = 'train.txt'
        elif mode == 'test':
            data_file = 'test.txt'
        else:
            data_file = 'dev.txt'
        super().__init__(
            base_path=self.base_path,
            tokenizer=tokenizer,
            max_seq_len=max_seq_len,
            mode=mode,
            data_file=data_file,
            label_list=self.label_list,
            is_file_with_header=True)




类别均衡

对于分类任务,数据存在部分类别样本量过少,各类别数据不均衡的问题。针对此问题,采用下列三种方法尝试解决:

  • 补充外部数据: 针对数量较少的故障类别与专业类型,从未被选中的其他电厂提取部分数据进行补充。
  • 欠采样: 从样本量较大的类别(其它、失灵、泄漏)中随机抽取部分样本使用,弃用其他样本;对于数量过少(<100)的故障类别(爆炸、着火、冰冻 等),直接删去。
  • 文本增强(Easy Data Augmentation): 对于所属类别样本量较少,且难以从其他电厂补充的文本数据,使用简单的文本数据增强方法(EDA)处理(每一条语料中改动的词占比 10% )。

使用 jieba 库分词,并用百度提供的停用词表筛去停用词后,对剩下的词随机进行如下操作:
  • 同义词替换 (SR: Synonyms Replace):在句子中随机抽取 n 个词,然后从同义词词典中随机抽取同义词,并进行替换。
  • 随机插入 (RI: Randomly Insert):随机抽取一个词,然后在该词的同义词集合中随机选择一个,插入原句子中的随机位置,重复 n 次。
  • 随机交换 (RS: Randomly Swap):随机选择两个词交换位置,重复 n 次。
  • 随机删除 (RD: Randomly Delete):以概率 p 随机删除文本中的各个词。

以随机删除为例,可以定义如下函数简单地实现以概率 p 随机删除文本 words 中的各个词:
def random_deletion(words, p):
    if len(words) == 1:
        return words

    new_words = []
    for word in words:
        r = random.uniform(01)
        if r > p:
            new_words.append(word)

    if len(new_words) == 0:
        rand_int = random.randint(0len(words)-1)
        return [words[rand_int]]

    return new_words



训练策略




文本分类任务

分类任务实际训练过程中的部分参数如下:


根据上述配置与策略训练模型:
optimizer = paddle.optimizer.Adam(learning_rate=5e-5, parameters=model.parameters())  # 优化器的选择和参数配置
trainer = hub.Trainer(model, optimizer, checkpoint_dir='./ckpt', use_gpu=True)        # fine-tune任务的执行者

训练过程中 loss 与 accuracy 的变化如图所示:



文本生成任务

使用预热学习(Warmup)与学习率衰减(Decay)结合的策略,即设置学习率先升后降的方法进行训练:

  • 在刚开始训练时,模型权重还未接近局部最优点,若学习率过大,可能导致模型不稳定(震荡),因此在前几个 step 中使用较小的学习率,使模型慢慢趋于稳定。
  • 等模型相对稳定后,逐渐升高学习率,使模型收敛速度变快。
  • 当模型训练到一定阶段后,权重已经比较接近局部最优点,如果沿用较大的学习率,容易导致模型在局部最优点附近震荡、难以收敛,因此再逐渐降低学习率。

笔者设置预热阶段的步数为总训练步数的的 10%,设置基准学习率为 2e-5,在预热阶段,实际学习率为:

当步数超过预热阶段后,实际学习率为:

生成任务实际训练过程中的部分参数如下:


按照上述策略创建优化器的代码如下:
import paddle.nn as nn

num_epochs = 30
learning_rate = 2e-5
warmup_proportion = 0.1
weight_decay = 0.1

max_steps = (len(train_data_loader) * num_epochs)
lr_scheduler = paddle.optimizer.lr.LambdaDecay(
    learning_rate,
    lambda current_step, num_warmup_steps=max_steps*warmup_proportion, num_training_steps=max_steps: float(current_step) / float(max(1, num_warmup_steps))
    if current_step < num_warmup_steps else max(0.0, float(num_training_steps - current_step) / float(max(1, num_training_steps - num_warmup_steps))))

optimizer = paddle.optimizer.AdamW(
    learning_rate=lr_scheduler,
    parameters=model.parameters(),
    weight_decay=weight_decay,
    grad_clip=nn.ClipGradByGlobalNorm(1.0),
    apply_decay_param_fun=lambda x: x in [
        p.name for n, p in model.named_parameters()
        if not any(nd in n for nd in ["bias""norm"])
    ])

根据上述配置与策略训练模型:
import os
import time

from paddlenlp.utils.log import logger

global_step = 1
logging_steps = 100
save_steps = 1000
output_dir = "output_model"
tic_train = time.time()
for epoch in range(num_epochs):
    for step, batch in enumerate(train_data_loader, start=1):
        (src_ids, src_sids, src_pids, tgt_ids, tgt_sids, tgt_pids, attn_ids,
            mask_src_2_src, mask_tgt_2_srctgt, mask_attn_2_srctgtattn,
            tgt_labels, _) = batch
        # import pdb; pdb.set_trace()
        _, __, info = model(
            src_ids,
            sent_ids=src_sids,
            pos_ids=src_pids,
            attn_bias=mask_src_2_src,
            encode_only=True)
        cached_k, cached_v = info['caches']
        _, __, info = model(
            tgt_ids,
            sent_ids=tgt_sids,
            pos_ids=tgt_pids,
            attn_bias=mask_tgt_2_srctgt,
            past_cache=(cached_k, cached_v),
            encode_only=True)
        cached_k2, cached_v2 = info['caches']
        past_cache_k = [
            paddle.concat([k, k2], 1for k, k2 in zip(cached_k, cached_k2)
        ]
        past_cache_v = [
            paddle.concat([v, v2], 1for v, v2 in zip(cached_v, cached_v2)
        ]
        loss, _, __ = model(
            attn_ids,
            sent_ids=tgt_sids,
            pos_ids=tgt_pids,
            attn_bias=mask_attn_2_srctgtattn,
            past_cache=(past_cache_k, past_cache_v),
            tgt_labels=tgt_labels,
            tgt_pos=paddle.nonzero(attn_ids == attn_id))

        if global_step % logging_steps == 0:
            logger.info(
                "global step %d, epoch: %d, batch: %d, loss: %f, speed: %.2f step/s, lr: %.3e"
                % (global_step, epoch, step, loss, logging_steps /
                    (time.time() - tic_train), lr_scheduler.get_lr()))
            tic_train = time.time()

        loss.backward()
        optimizer.step()
        lr_scheduler.step()
        optimizer.clear_gradients()
        if global_step % save_steps == 0:
            model_dir = os.path.join(output_dir,
                                        "model_%d" % global_step)
            if not os.path.exists(model_dir):
                os.makedirs(model_dir)
            model.save_pretrained(model_dir)
            tokenizer.save_pretrained(model_dir)

        global_step += 1



项目成果


使用处理后的样本训练两个模型: 一个文本分类模型,用于预测故障的专业类型; 一个文本生成模型,用于提示故障维护建议。


文本分类任务

在全新的测试数据上测试文本分类模型:

import numpy as np
import pandas as pd

# Data to be predicted
test_tokens = pd.read_table('/home/aistudio/data/' + dataID + '/test.txt')['tokens']
test_labels = pd.read_table('/home/aistudio/data/' + dataID + '/test.txt')['labels']

test_input = []
for token in test_tokens:
    test_input.append([token])

f = open('/home/aistudio/data/' + dataID + '/label_list.txt''r')
label_list = f.read().strip().split('\t')
f.close()
label_map = { 
    idx: label_text for idx, label_text in enumerate(label_list)
}

model = hub.Module(
    name='ernie',
    task='seq-cls',
    load_checkpoint='./model.pdparams',
    label_map=label_map)
results = model.predict(test_input, max_seq_len=128, batch_size=1, use_gpu=True)
# 展示 10 条结果
for idx, text in enumerate(test_input[:10]):
    print('Data: {} \t Lable: {} \t Prediction: {}'.format(text, test_labels[idx], results[idx]))
# 保存结果
output= pd.DataFrame({'token': test_tokens, 'label': test_labels, 'prediction': results})
output.to_csv('output.csv')



文本生成任务

在全新的测试数据上测试文本生成 模型:
from tqdm import tqdm

from paddlenlp.metrics import Rouge1


rouge1 = Rouge1()

vocab = tokenizer.vocab
eos_id = vocab[tokenizer.sep_token]
sos_id = vocab[tokenizer.cls_token]
pad_id = vocab[tokenizer.pad_token]
unk_id = vocab[tokenizer.unk_token]
vocab_size = len(vocab)

paddle.seed(2021) # set random seed

evaluated_sentences_ids = []
reference_sentences_ids = []

logger.info("Evaluating...")
model.eval()
for data in tqdm(test_data_loader):
    (src_ids, src_sids, src_pids, _, _, _, _, _, _, _, _,
        raw_tgt_labels) = data  # never use target when infer
    output_ids = greedy_search_infilling(
        model,
        src_ids,
        src_sids,
        eos_id=eos_id,
        sos_id=sos_id,
        attn_id=attn_id,
        pad_id=pad_id,
        unk_id=unk_id,
        vocab_size=vocab_size,
        max_decode_len=max_decode_len,
        max_encode_len=max_encode_len,
        tgt_type_id=tgt_type_id)

    for ids in output_ids.tolist():
        if eos_id in ids:
            ids = ids[:ids.index(eos_id)]
        evaluated_sentences_ids.append(ids)

    for ids in raw_tgt_labels.numpy().tolist():
        ids = ids[1:ids.index(eos_id)]
        reference_sentences_ids.append(ids)

evaluated_sentences = []
reference_sentences = []
for ids in reference_sentences_ids[:]:
    reference_sentences.append(''.join(vocab.to_tokens(ids)))
for ids in evaluated_sentences_ids[:]:
    evaluated_sentences.append(''.join(vocab.to_tokens(ids)))

模型在测试数据集上的输出结果如下表所示。可以看到,预测的专业类型都能与真实的专业类型相匹配,而模型生成的维护建议也能准确地提示故障的消缺方法,与真实消缺情况几乎相同。比如,针对故障 “1号炉A侧脱硝声波吹灰器上层2,5,6,7不响”,模型输出的专业类别为脱硫专业,与真实专业类型一致;同时,模型给出的维护建议为“清洗膜处理”,与真实情况的“清洗膜片处理”相同,准确定位了故障元件,并给出了处理办法。


分类任务的混淆矩阵如图,最终的准确率为 96.91%,达到了较高的水准。




长线规划


基于本项目的成果,后续可以发展出更加完善、全面的设备故障智能维护系统。以汽轮机的异常振动为例,当实时监测系统监测到设备故障时,会生成故障文本:“设备振动参数超过规定标准范围”,故障文本与设备的检修日期、使用年限等标签信息和设备画像信息相匹配,一起输入到模型中。根据算法结果调用知识库,输出故障的原因分析:有 50% 的可能性是转子不平衡引起的,有 40% 的可能性是气流激振引起的,等等。并推送相应的维护方案、相关案例、故障影响等。这些故障研判信息能够对实际消缺工作起到辅助作用,有效提高设备故障抢修效率,减少人力消耗,缩短维修时间,整体上提高电厂设备运行效率,降低因设备故障造成的经济损失。


关注公众号,获取更多技术内容~