场景简介
电力是推动国家经济发展的重要动力,在我国快速发展的过程中承担着不可替代的作用。电厂设备在高强度运行过程中难免会发生故障,传统的电力设备故障研判主要依赖于专工的经验和专家知识,这些专家经验主要来源于生产工作中的日积月累,多表现为碎片化的维护数据。
传统人工处理的局限性
长期以来,电厂设备的故障研判主要依赖专业人员的人工处理。这对从业人员的要求非常高,且在效率、研判标准等方面难以达成统一。随着浙能集团近年来信息化建设水平的不断提高,累积的历史维护数据越来越受到重视。
传统机器学习建模的局限性
传统的机器学习模型可以利用设备的历史维护数据助力故障研判,但是会存在一定的局限性,无法很好地满足业务需求:
过于依赖特征工程,对人力以及工程师个人经验要求较高。
对文本数据的处理缺乏上下文理解,无法关注到重点,可能导致理解偏差。
基于小样本的监督学习,无法利用海量数据的优势助力语义理解能力。
业务理解
以各电厂设备维护数据为分析对象,运用 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(0, 1)
if r > p:
new_words.append(word)
if len(new_words) == 0:
rand_int = random.randint(0, len(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], 1) for k, k2 in zip(cached_k, cached_k2)
]
past_cache_v = [
paddle.concat([v, v2], 1) for 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% 的可能性是气流激振引起的,等等。并推送相应的维护方案、相关案例、故障影响等。这些故障研判信息能够对实际消缺工作起到辅助作用,有效提高设备故障抢修效率,减少人力消耗,缩短维修时间,整体上提高电厂设备运行效率,降低因设备故障造成的经济损失。
关注公众号,获取更多技术内容~