本文是《使用PaddleNLP进行恶意网页识别》系列第四篇,该系列持续更新中……

《使用PaddleNLP进行恶意网页识别》系列项目,围绕当前网络安全管理中面临的最大挑战之一——如何防止用户有益/无意中访问恶意网页,进而给企业造成损失的问题展开,尝试使用人工智能技术,针对此问题提出相应的解决方案。

系列目录

  • 使用PaddleNLP进行恶意网页识别(一)
    • 使用PaddleNLP的文本分类模型,做正常网页与被黑网页的简单二分类,根据HTML网页内容处理结果判断网页是否正常。
  • 使用PaddleNLP进行恶意网页识别(二)
    • 使用PaddleNLP的预训练模型Fine-tune,大幅提高根据HTML网页内容处理结果判断网页准确率。
    • 特别感谢社区@没入门的研究生大佬的指导。
  • 使用PaddleNLP进行恶意网页识别(三)
    • 使用PaddleNLP的文本分类模型,做正常网页与恶意网页的简单二分类,提取HTML标签信息判断网页是否正常。
  • 使用PaddleNLP进行恶意网页识别(四)
    • 尝试使用人工判断条件,设计提取HTML标签信息识别恶意网页的流程。
    • 使用PaddleNLP的预训练模型Fine-tune,尝试提高提取HTML标签信息判断网页是否正常的效果。
    • 将动态图训练的网页分类模型导出并使用Python部署。
  • 使用PaddleNLP进行恶意网页识别(五)
    • 该项目直接对标系列第二篇,对比BERT中文预训练模型和Ernie预训练模型在HTML网页内容分类流程和效果上的差异。
    • 项目进一步完善和优化了HTML网页内容提取和数据清洗流程。
    • 验证集上模型准确率可以轻松达到91.5%以上,最高达到95%,在BERT预训练模型上进行finetune,得到了目前在该HTML网页内容分类任务上的最好表现。
  • 使用PaddleNLP进行恶意网页识别(六)
    • 该项目直接对标系列第四篇,对比BERT中文预训练模型和Ernie预训练模型在HTML网页标签分类效果上的差异。
    • 项目进一步完善和优化了HTML的tag内容提取和数据清洗流程。
    • 验证集上模型准确率可以轻松达到96.5%以上,测试集上准确率接近97%,在BERT预训练模型上进行finetune,得到了目前在该HTML网页标签序列分类任务上的最好表现。

项目介绍

本项目使用的数据集来自《基于数据科学的恶意软件分析》约书亚·萨克斯(Joshua Saxe),希拉里•桑德斯(Hillary Sanders)著

在该书的第11章中,有一个数量大约为10万的正常和恶意HTML文件集。

数据集包括两个文件夹,分别是:

  • 正常HTML文件ch11/data/html/benign_files/
  • 恶意HTML文件ch11/data/html/malicious_files/

记住不要在浏览器中打开这些文件!

这些HTML文件的内容,涵盖了英文、中文、俄文等各国语言,如果尝试从网页内容的角度进行恶意网页分类,将面临多语言处理的巨大挑战。

因此,在项目使用PaddleNLP进行恶意网页识别(三)中,通过尝试提取多语言HTML页面的“共性”——HTML标签序列进而进行文本分类,实现了较好的效果(demo准确率大于84%)。

但是,HTML标签序列分类的识别准确率能否进一步提升?在本项目中,将展开探讨。

环境准备

# 需要从源码安装安装最新的PaddleNLP develop分支
!pip install --upgrade git+https://gitee.com/PaddlePaddle/PaddleNLP.git
!pip install lxml
!pip install bs4
!pip install html5lib
!pip install HTMLParser
import os
import sys
import codecs
import chardet
import shutil
import time
from tqdm import tqdm, trange
from bs4 import BeautifulSoup
import re
from html.parser import HTMLParser
import numpy as np
from functools import partial

import paddle
import paddlenlp

清洗HTML标签提取结果

在运行上一个项目使用PaddleNLP进行恶意网页识别(三)时发现,其实有个别样本的标签提取结果不太理想,大部分是恶意网页,有可能是BeautifulSoup的HTMLParser方法不够完善,也有可能是这些网页针对通用的网络安全措施做了处理,试图绕过浏览器或者网页的防护手段,又或者是数据集本身的问题。

with open("train_list.txt","r",encoding="utf-8") as f:
    lines = f.readlines()
    for i,line in enumerate(lines):
        # 仅展示部分未完全清洗的html标签
        if i <  1000:
            if ";" in line:
                print(line)
            elif "+" in line:
                print(line)

可以看出,主要是<script>内的标签信息清理不完整,且大部分原本就是恶意网页。那么,如果用户比较谨慎,或是属于旁路检测的情况,为保险起见,我们可以设计“不放过任何可能漏网之鱼”的逻辑,把这部分内容留给下一个流程(比如提取文本信息或人工核验),因为从样本看,误判率不会太高,这种方法也许是可行的。

with open("train_list.txt","r",encoding="utf-8") as f:
    lines = f.readlines()
    #print(lines)
with open("train_list2.txt","w",encoding="utf-8") as f_w:
    for line in lines:
        if ";" in line:
            continue
        elif "+" in line:
            continue
        f_w.write(line)

with open("eval_list.txt","r",encoding="utf-8") as f:
    lines = f.readlines()
    #print(lines)
with open("eval_list2.txt","w",encoding="utf-8") as f_w:
    for line in lines:
        if ";" in line:
            continue
        elif "+" in line:
            continue
        f_w.write(line)

使用预训练模型Fine-tune

自定义数据集

from paddlenlp.datasets import load_dataset

def read(data_path):
    with open(data_path, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip('\n').split('\t')
            words = ''.join(line[:-1])
            labels = line[-1]
            yield {'text': words, 'label': labels}

# data_path为read()方法的参数
train_ds = load_dataset(read, data_path='train_list2.txt',lazy=False)
dev_ds = load_dataset(read, data_path='eval_list2.txt',lazy=False)
# 查看样例数据
train_ds.data[:5]
# label_list手动添加进去就行
train_ds.label_list = ['0', '1']
dev_ds.label_list = ['0', '1']
# 查看效果,确认自定义数据集完成
print(train_ds.label_list)

for data in train_ds.data[:1]:
    print(data)

参考项目:『NLP经典项目集』02:使用预训练模型ERNIE优化情感分析
,该项目提供了数据处理脚本utils.py,本文已经内置。

PaddleNLP一键加载预训练模型

PaddleNLP对于各种预训练模型已经内置了对于下游任务-文本分类的Fine-tune网络。以下教程ERNIE为例,介绍如何将预训练模型Fine-tune完成文本分类任务。

  • paddlenlp.transformers.ErnieModel()一行代码即可加载预训练模型ERNIE。

  • paddlenlp.transformers.ErnieForSequenceClassification()一行代码即可加载预训练模型ERNIE用于文本分类任务的Fine-tune网络。

    • 其在ERNIE模型后拼接上一个全连接网络(Full Connected)进行分类。
  • paddlenlp.transformers.ErnieForSequenceClassification.from_pretrained() 只需指定想要使用的模型名称和文本分类的类别数即可完成网络定义。

# 设置想要使用模型的名称

MODEL_NAME = "ernie-2.0-large-en"

ernie_model = paddlenlp.transformers.ErnieModel.from_pretrained(MODEL_NAME)

model = paddlenlp.transformers.ErnieForSequenceClassification.from_pretrained(MODEL_NAME, num_classes=len(train_ds.label_list))

调用ppnlp.transformers.ErnieTokenizer进行数据处理

这里用的是Ernie-2.0-Large-EN。PaddleNLP对于各种预训练模型已经内置了相应的tokenizer。指定想要使用的模型名字即可加载对应的tokenizer。

tokenizer作用为将原始输入文本转化成模型model可以接受的输入数据形式。

但是在本文这个场景中,html标签绝大多数并不是标准的英文单词,在默认的vocab.txt里没有,可能会对Fine-tune带来的效果提升产生一定影响。

tokenizer = paddlenlp.transformers.ErnieTokenizer.from_pretrained(MODEL_NAME)

数据读入

使用paddle.io.DataLoader接口多线程异步加载数据。

from functools import partial
from paddlenlp.data import Stack, Tuple, Pad
from utils import  convert_example, create_dataloader

# 模型运行批处理大小
batch_size = 128
max_seq_length = 64

trans_func = partial(
    convert_example,
    tokenizer=tokenizer,
    max_seq_length=max_seq_length)
batchify_fn = lambda samples, fn=Tuple(
    Pad(axis=0, pad_val=tokenizer.pad_token_id),  # input
    Pad(axis=0, pad_val=tokenizer.pad_token_type_id),  # segment
    Stack(dtype="int64")  # label
): [data for data in fn(samples)]

train_data_loader = create_dataloader(
    train_ds,
    mode='train',
    batch_size=batch_size,
    batchify_fn=batchify_fn,
    trans_fn=trans_func)
dev_data_loader = create_dataloader(
    dev_ds,
    mode='dev',
    batch_size=batch_size,
    batchify_fn=batchify_fn,
    trans_fn=trans_func)
from paddlenlp.transformers import LinearDecayWithWarmup

# 训练过程中的最大学习率,要设小一点
learning_rate = 5e-6
# 训练轮次
epochs = 5
# 学习率预热比例
warmup_proportion = 0.1
# 权重衰减系数,类似模型正则项策略,避免模型过拟合
weight_decay = 0.1

num_training_steps = len(train_data_loader) * epochs
lr_scheduler = LinearDecayWithWarmup(learning_rate, num_training_steps, warmup_proportion)
optimizer = paddle.optimizer.AdamW(
    learning_rate=lr_scheduler,
    parameters=model.parameters(),
    weight_decay=weight_decay,
    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"])
    ])

criterion = paddle.nn.loss.CrossEntropyLoss()
metric = paddle.metric.Accuracy()
# checkpoint文件夹用于保存训练模型
!mkdir /home/aistudio/checkpoint

模型训练与评估

模型训练的过程通常有以下步骤:

  1. 从dataloader中取出一个batch data
  2. 将batch data喂给model,做前向计算
  3. 将前向计算结果传给损失函数,计算loss。将前向计算结果传给评价方法,计算评价指标。
  4. loss反向回传,更新梯度。重复以上步骤。

每训练一个epoch时,程序将会评估一次,评估当前模型训练的效果。

import paddle.nn.functional as F
from utils import evaluate
from visualdl import LogWriter

global_step = 0
for epoch in range(1, epochs + 1):
    with LogWriter(logdir="./visualdl") as writer:
        for step, batch in enumerate(train_data_loader, start=1):
            input_ids, segment_ids, labels = batch
            logits = model(input_ids, segment_ids)
            loss = criterion(logits, labels)
            probs = F.softmax(logits, axis=1)
            correct = metric.compute(probs, labels)
            metric.update(correct)
            acc = metric.accumulate()
            global_step += 1
            if global_step % 50 == 0 :
                print("global step %d, epoch: %d, batch: %d, loss: %.5f, acc: %.5f" % (global_step, epoch, step, loss, acc))
                # 向记录器添加一个tag为`loss`的数据
            writer.add_scalar(tag="loss", step=global_step, value=loss)
            # 向记录器添加一个tag为`acc`的数据
            writer.add_scalar(tag="acc", step=global_step, value=acc)
            loss.backward()
            optimizer.step()
            lr_scheduler.step()
            optimizer.clear_grad()
        evaluate(model, criterion, metric, dev_data_loader)

model.save_pretrained('/home/aistudio/checkpoint')
tokenizer.save_pretrained('/home/aistudio/checkpoint')


可以发现,如果“炼丹”炼得好,使用PaddleNLP的预训练模型Fine-tune同样能大幅提升HTML标签提取结果预测网页属性的准确率。

问题:为什么第一轮评估后,准确率开始“飞升”?

回答:第一个step初始化参数是随机生成的(参数初始化方式),每一个step进行一次反向传播更新参数,网络训练的初参数更新的梯度方向不是局部最优的方向,所以ACC较低,当找到贴近局部最优的更新方向后,ACC开始提高。from @友军的奸细

模型导出

程序运行时将会自动进行训练,评估,测试。同时训练过程中会自动保存模型在指定的save_dir中。 比如本项目指定的checkpoint目录:

./
├── checkpoint
│   ├── model_config.json
│   ├── model_state.pdparams
│   ├── tokenizer_config.json
│   └── vocab.txt
└── ...

NOTE:

  • 如需恢复模型训练,则可以设置init_from_ckpt, 如init_from_ckpt=checkpoints/model_state.pdparams
  • 如需使用ernie-tiny模型,则需要提前先安装sentencepiece依赖,如pip install sentencepiece
  • 使用动态图训练结束之后,还可以将动态图参数导出成静态图参数。静态图参数保存在output_path指定路径中。
import argparse
import os
from functools import partial

import numpy as np
import paddle
import paddle.nn.functional as F
import paddlenlp as ppnlp
from paddlenlp.data import Stack, Tuple, Pad

# yapf: disable
# parser = argparse.ArgumentParser()
# parser.add_argument("--params_path", type=str, required=True, default='./checkpoint/model_state.pdparams', help="The path to model parameters to be loaded.")
# parser.add_argument("--output_path", type=str, default='./static_graph_params', help="The path of model parameter in static graph to be saved.")
# args = parser.parse_args()
# yapf: enable

if __name__ == "__main__":

    # The number of labels should be in accordance with the training dataset.
    label_map = {0: 'negative', 1: 'positive'}
    model = ppnlp.transformers.ErnieForSequenceClassification.from_pretrained(
        MODEL_NAME, num_classes=len(label_map))

    # if args.params_path and os.path.isfile(args.params_path):
    state_dict = paddle.load('./checkpoint/model_state.pdparams')
    model.set_dict(state_dict)
    print("Loaded parameters from %s" % './checkpoint/model_state.pdparams')
    model.eval()

    # Convert to static graph with specific input description
    model = paddle.jit.to_static(
        model,
        input_spec=[
            paddle.static.InputSpec(
                shape=[None, None], dtype="int64"),  # input_ids
            paddle.static.InputSpec(
                shape=[None, None], dtype="int64")  # segment_ids
        ])
    # Save in static graph model.
    paddle.jit.save(model, './static_graph_params')
# 或者使用项目提供的export_model.py文件
# !python export_model.py --params_path=./checkpoint/model_state.pdparams --output_path=./static_graph_params

模型导出完成后,会出现下面三个文件:

./
├── static_graph_params.pdmodel
├── static_graph_params.pdiparams
├── static_graph_params.pdiparams.info
└── ...

导出模型的预测

注意,这里有个比较奇怪的地方,调用ppnlp.transformers.ErnieTokenizer进行数据处理时还要去下载ernie-tinytokenizer,这里可能是个bug,不同vocab.txt会对预测结果有影响,读者可以尝试看看——因此,可能最简单的做法是,使用ernie-tiny进行finetune。该问题还要进一步确认。

# !ls /home/aistudio/.paddlenlp/models/ernie-2.0-large-en
!cp /home/aistudio/.paddlenlp/models/ernie-2.0-large-en/vocab.txt /home/aistudio/.paddlenlp/models/ernie-tiny/vocab.txt
# 解析一个典型的恶意网页的html页面
html = BeautifulSoup(open('5578a3b1369a0921641b2b28b041833dbd267a65f1fdf5e08be965559fb9dccf'),'html.parser', from_encoding='utf-8')
# 查看HTML信息
html.get_text

该网页恰好是个中文网页,从HTML代码信息中我们可以发现,该网页看似一个正常的论坛页面,直到最后一行代码:

<script src="http://web.nba1001.net:8888/tj/tongji.js" type="text/javascript"></script>

我们知道<script>标签在HTML页面中属于危险性特别高的,因为脚本会自动运行,而指向用户也未知,而最后这行代码,显然也不是典型的JavaScript包调用,所以很可能是恶意网页。

现在,我们可以部署导出模型,看是否能正常识别该网页的属性。在这之前,首先需要提取网页标签信息。

def read_tags(text):
    tags = []
    class MyHTMLParser2(HTMLParser):
        # 这里先只尝试提取结束标记进行训练
        def handle_endtag(self, tag):
            tags.append(tag)
    parser = MyHTMLParser2()
    parser.feed(text)
    return tags
 text = ','.join(read_tags(str(html.get_text)))
 text
# 本文将标签提取结果硬编码到predict.py中作为演示,后续会尝试形成自动化流程
yHTMLParser2()
    parser.feed(text)
    return tags
 text = ','.join(read_tags(str(html.get_text)))
 text
# 本文将标签提取结果硬编码到predict.py中作为演示,后续会尝试形成自动化流程
!python predict.py --model_file=static_graph_params.pdmodel --params_file=static_graph_params.pdiparams

小结

  • 至此,在基于HTML的恶意网页识别方面,已经初步具备了两种利器——HTML标签提取分类、网页内容分类;同时,还设计了从逻辑判断过滤html标签提取结果的方案。
  • 使用预训练模型Fine-tune后,两种分类模型预测准确率都能轻松达到94%以上。
  • 接下来,将考虑将上述流程串接起来工程化,或继续研究网页内容的重要组成部分:url链接、图片信息等。
Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐