这是论文 Cleaner Pretraining Corpus Curation with Neural Web Scraping 的速读笔记,同时简要分析这篇论文作者的实现代码. 论文的主要工作是提出了基于神经网络的高效crawler.
这里先澄清scraper和crawler的区别,一图胜千言.
Abstract
The web contains large-scale, diverse, and abundant information to satisfy the informationseeking needs of humans. Through meticulous data collection, preprocessing, and curation, webpages can be used as a fundamental data resource for language model pretraining. However, when confronted with the progressively revolutionized and intricate nature of webpages, rule-based/feature-based web scrapers are becoming increasingly inadequate. This paper presents a simple, fast, and effective Neural web Scraper (NeuScraper) to help extract primary and clean text contents from webpages. Experimental results show that NeuScraper surpasses the baseline scrapers by achieving more than a 20% improvement, demonstrating its potential in extracting higher-quality data to facilitate the language model pretraining. All of the code is available at https: //github.com/OpenMatch/NeuScraper.
网络包含大规模、多样化和丰富的信息,以满足人类的信息需求。通过精心的数据收集、预处理和整理,网页可以作为语言模型预训练的基本数据资源。然而,面对不断革新和复杂的网页性质,基于规则/基于特征的scraper越来越显得不够用。本文介绍了一种简单、快速且有效的神经网络网页scraper(NeuScraper),以帮助从网页中提取主要和干净的文本内容。实验结果表明,NeuScraper通过实现超过20%的改进,超过了基线scraper,展示了其在提取更高质量数据以促进语言模型预训练方面的潜力。所有代码可在 https://github.com/OpenMatch/NeuScraper 上找到。
摘要中指出了网页因为多样化,以及丰富的信息,可以作为LLM训练的基本数据源,而现有的scraper不够用。因此作者提出了NeuScraper
这种基于神经网络的方法,帮助从网页中提取主要的干净文本, 并且在和baseline比较的时候,得到20%的性能提升.
Introduction
简介中,介绍了当前已有的一些工作。
LLMs在各种NLP任务重,随着模型参数规模的扩大,展现出了非常令人印象深刻的性能. 但在对scaling law研究的过程中发现表面,模型参数大小和训练数据的大小应该等比例的扩大,这在过去足够大的训练数据集方面构成了挑战,甚至引发了对数据稀缺的担忧.
为了给预训练整理更多的数据,研究人员更加注重从网络上收集更有价值的数据。像CommonCrawl这样的网络爬取数据集已经被广泛用于预训练,促进了语言模型的发展。然而,先前的研究表明,即使经过积极的清洗,CommonCrawl提供的预提取文本的质量仍然未能达到预期,原因在于广告、横幅、超链接等有害内容通常混杂在页面的主要内容中,因此仅提取这些主要内容会为预训练带来大量噪声. 先前的工作主要是rule based和heuristic,但在网页越来越复杂的现在,不仅效果不好,且很难维护这些scraper.
因此论文提出了一个简单、快速且有效的神经网络scraper(NeuScraper),它被设计用来从网页中提取主要内容。NeuScraper采用了浅层神经网络架构,并整合了布局信息以实现高效的抓取。实验表明,NeuScraper超越了基线,性能提升了20%,并为语言模型预训练生成了更高质量的语料库,尤其在GPU上展现了非常好的处理速度.
上图展现了NeuScraper的Content Extraction处理流程,可以看到,将原始的html提取转化为序列,经过NeuScarper的处理(事实上为分类处理),提取出需要的内容.
Related Work
相关工作中主要介绍了从网页中提取内容的一些方法, 例如基于规则和基于特征.
基于规则
基于规则的网页抓取方法,以及其中涉及的一些关键技术和挑战:
- 网页包装器(Web Wrappers):
- 早期的网页抓取常使用网页包装器作为起点。
- 网页包装器通常需要人工设计或使用包装器归纳系统生成。
- 每个网页都需要定制包装器,难以处理大规模网页。
- 文档对象模型(DOM)树:
- 更常见的方法是创建DOM树,用于构建基于规则的抓取器或比较网页。
- DOM树表示网页的结构化数据,便于提取内容。
- 其他辅助技术:
- 标签累积分布(Tag Cumulative Distributions)
- 文本密度(Text Density)
- 标签比例(Tag Ratios)
- 这些技术可以帮助从网页中提取有用内容。
总的来说,基于规则的网页抓取方法需要针对不同网页定制规则和工具,难以扩展到大规模应用。研究者们提出了各种辅助技术,如DOM树、标签分布等,来改进内容提取的效果。但是,这些方法仍然需要大量的人工参与和专业知识,自动化程度有限。
基于特征
介绍了基于特征的网页内容提取方法,与基于规则的方法相比,这种方法更加灵活和智能。
-
基于特征的方法概述:
- 将网页分割成多个块
- 从这些块中提取大量手工设计的特征
- 将这些特征输入到机器学习模型中进行分类
-
网页分块方法:
- 基于HTML标签或DOM树结构制定规则
- 将网页划分为若干个块
-
特征提取:
从每个块中提取多种特征,包括:- 标记特征
- 文本/文档特征
- 语言学特征
- 结构特征
- 视觉特征
- 基于DOM树的特征
-
机器学习模型:
将提取的特征输入到各种机器学习模型中,如:- 支持向量机(SVM)
- 条件随机场(CRF)
- 逻辑回归
- 卷积神经网络(CNN)
-
分类目标:
判断每个块中的文本是否属于网页的主要内容
这种基于特征的方法相比于简单的规则基础方法更加灵活,能够处理更复杂的网页结构。通过结合机器学习技术,这种方法可以自动学习判断主要内容的规则,而不需要人工设计复杂的规则集。这种方法在处理多样化的网页时具有更好的适应性和准确性。
Neural Web Scraper
这部分介绍NeuScraper模型的工作流程和backbone.
先前的工作已经证明了结构和视觉特征在帮助识别主要内容方面的有效性。因此,为了保留网页布局信息,我们依赖DOM
树结构将网页转换为文本序列。具体来说,我们使用BeautifulSoup4
工具包为每个网页构建DOM
树,对树进行深度优先遍历,并将访问顺序视为表示节点的附加位置信息。在这个过程中,只有包含纯文本的节点、表格节点(用<table>
标记)和列表节点(用<ol>、<ul>或<dl>
标记)被保留下来,以产生最终的文本序列
X
=
{
x
1
,
x
2
,
⋯
,
x
n
}
X = \{x_1, x_2, \cdots, x_n\}
X={x1,x2,⋯,xn},其中n表示保留的DOM
节点的数量。处理后,网络爬取任务主要涉及确定节点xi是否包含网页的主要内容以供评估。
然后是这篇论文的最核心工作,如何处理这个序列 X = { x 1 , x 2 , ⋯ , x n } X = \{x_1, x_2, \cdots, x_n\} X={x1,x2,⋯,xn}.
为了保障高效处理,NeuScraper用了XML-Roberta对这个序列进行encode, 把DOM node编码成 dim=768的向量.
h
i
=
XLMRoberta-Layer
1
(
x
i
)
,
\begin{align} h_i=\operatorname{XLMRoberta-Layer}^1(x_i), \end{align}
hi=XLMRoberta-Layer1(xi),
这里的
h
i
h_i
hi实际上就是BERT中的[CLS]
标签.
经过这一步处理后,结果会被放到一个3层transformer架构中(8个Attention head)
h
i
^
=
Transformer
(
Linear
(
h
i
)
)
,
\begin{align} \hat{h_i} = \operatorname{Transformer}(\operatorname{Linear}(h_i)), \end{align}
hi^=Transformer(Linear(hi)),
根据前人的工作,这些DOM node可以被分为6类:
- primary content
- heading
- title,
- paragraph
- table
- list
根据这6个分类,用
y
k
y^k
yk表示第
k
k
k种标签(也就是类型), 计算DOM node属于某一种类型的概率
P
(
y
i
k
=
1
∣
x
i
)
P(y_i^k=1\mid x_i)
P(yik=1∣xi):
P
(
y
i
k
=
1
∣
x
i
)
=
Sigmoid
(
MLP
(
h
i
^
)
)
\begin{align} P(y_i^k=1\mid x_i) = \operatorname{Sigmoid}(\operatorname{MLP}(\hat{h_i})) \end{align}
P(yik=1∣xi)=Sigmoid(MLP(hi^))
然后与标签
Y
i
k
\mathcal{Y}_i^k
Yik计算交叉熵损失得到损失函数:
L
=
∑
k
=
1
6
∑
i
=
1
n
CrossEntropy
(
P
(
y
i
k
∣
x
i
)
,
Y
i
k
)
\begin{align} L = \sum_{k=1}^{6}\sum_{i=1}^n\operatorname{CrossEntropy}(P(y_i^k\mid x_i),\mathcal{Y}_i^k) \end{align}
L=k=1∑6i=1∑nCrossEntropy(P(yik∣xi),Yik)
至此,主要的工作就完成了.
实现
论文作者在github上给出了模型的实现, 本文简要分析一下模型结构代码src/scraper/model.py
import math
import torch.nn as nn
from metrics import *
from transformers import BertConfig, XLMRobertaConfig, XLMRobertaModel
from transformers.models.bert.modeling_bert import BertEncoder
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout=0.1, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
self.LayerNorm = nn.LayerNorm(d_model)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len / 2, dtype=torch.float).unsqueeze(1)
position = position.repeat(1, 2).view(-1, 1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0).transpose(0, 1)
self.register_buffer("pe", pe)
def forward(self, x):
x = x + self.pe[: x.size(0), :]
return self.dropout(x)
class MLP(nn.Module):
def __init__(self, input_dim, output_dim, hidden_dim):
super(MLP, self).__init__()
self.input_dim = input_dim
self.output_dim = output_dim
self.hidden_dim = hidden_dim
current_dim = input_dim
self.layers = nn.ModuleList()
if len(hidden_dim) >= 1:
for hdim in hidden_dim:
self.layers.append(nn.Linear(current_dim, hdim))
self.layers.append(nn.ReLU())
current_dim = hdim
self.layers.append(nn.Linear(current_dim, output_dim))
def forward(self, x):
for layer in self.layers[:-1]:
x = layer(x)
out = self.layers[-1](x)
return out
class ContentExtractionTextEncoder(nn.Module):
def __init__(self, config):
super().__init__()
self.model_version = config.model_version
self.sigmoid = nn.Sigmoid()
self.relu = nn.ReLU()
self.max_sequence_len = config.max_sequence_len
self.num_classes = config.num_classes
self.text_in_emb_dim = config.text_in_emb_dim
self.text_emb_dim = config.text_emb_dim
self.hidden = MLP(self.text_emb_dim, config.num_classes, [])
self.max_token_len = config.max_token_len
self.enable_positional_encoding = not config.disable_positional_encoding
print("Positional Encoding Enabled?: " + str(self.enable_positional_encoding))
if self.enable_positional_encoding:
self.pos_encoder = PositionalEncoding(d_model=self.text_emb_dim, max_len=config.max_sequence_len)
self.textlinear = nn.Linear(
config.text_in_emb_dim, config.text_emb_dim
) # 768 -> 256
configuration = BertConfig(
num_hidden_layers=config.num_layers,
num_attention_heads=config.num_heads,
hidden_size=self.text_emb_dim,
intermediate_size=1024,
output_hidden_states=False,
output_attentions=False,
)
self.encoder = BertEncoder(configuration)
text_roberta_config = XLMRobertaConfig.from_pretrained(
"xlm-roberta-base",
num_attention_heads=12,
num_hidden_layers=config.text_encoder_num_hidden_layer,
)
self.text_roberta = XLMRobertaModel(text_roberta_config)
def forward(self, x):
[token_ids, token_masks] = x
seq_len = self.max_sequence_len
text_in_emb_dim = self.text_in_emb_dim
max_token_len = self.max_token_len
token_ids = token_ids.view(-1, max_token_len) # [batch * max_sequence_len, max_token_len]
token_masks = token_masks.view(-1, max_token_len) # [batch * max_sequence_len, max_token_len]
features = []
text_output = self.text_roberta(input_ids=token_ids, attention_mask=token_masks)
all_text_emb = text_output.pooler_output.reshape(-1, seq_len, text_in_emb_dim)
text_x = self.textlinear(all_text_emb)
features.append(text_x)
text_visual_x = torch.cat(features, 2)
if self.enable_positional_encoding:
text_visual_x = text_visual_x.permute(1, 0, 2)
text_visual_x = self.pos_encoder(text_visual_x)
text_visual_x = text_visual_x.permute(1, 0, 2)
if 'bert' in self.model_version:
emb_output = self.encoder(text_visual_x, head_mask=[None, None, None])[0]
else:
emb_output = text_visual_x
x_hidden = self.hidden(emb_output)
output = self.sigmoid(x_hidden)
return output
代码很清晰,主要定义了:
PostionEncoding
实现位置编码,这个在主网络中属于可选项MLP
用来做最后的classificationContentExtractionTextEncoder
处理已经经过XLMRobertaModel
和BertEncoder
处理的序列
整个实现借助了XLMRoberta
这个强大的模型.
总结
这篇论文的内容不是非常多,也很容易理解,提出了对html内容提取的很好的思路,也很容易实现。但根据介绍训练的话需要8卡A100, 个人还是没法进行from scratch 的训练的,finetune方法正在研究中,欢迎沟通交流.