スクレイピング時に本文だけを抽出する最高精度ツールが優秀！【日本語版も公開】

# はじめに
スクレイピングでテキストを抽出するとルールベースでは除外できないヘッダーなどが大量に混ざってノイズになりますよね。
それをフィルタリングするツールの最高精度モデル(BoilerNet)が良い感じだったので紹介します。日本語対応版のソースコードも公開しています。
# 要約
- BoilerNetとは深層学習を使った本文だけを抽出するツールです
- BoilerNetは日英ともに高精度(F値が0.90)です
- 軽量でCPUでも1ページ0.02秒程度の時間で予測できます

また日本語の本文抽出ツールとしての要点は以下です。
## 要点1:日本語対応版も元論文と同程度の精度が出る

|       | f1[n,p]                                  | precision[n,p]                            | recall[n,p]                               | support[n,p]       |
| ---------- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- | -------------- |
|元論文|[0.90, 0.78]|[0.95, 0.70]|[0.86, 0.88]|[38691, 14122]|
| 再現学習(英語) | [0.91, 0.79] | [0.95, 0.73] | [0.88, 0.87] | [38691, 14122] |
| 日本語対応版 | [0.90, 0.79] | [0.95, 0.69] | [0.86, 0.89] | [38691, 14122] |
n,pはそれぞれnegative(ノイズ),positive(本文)を基準とした時の評価を表します。
例としてノイズ部分をN,本文をPとし,正解をT(True)、間違いをF(False)とした時にPresicionはそれぞれ以下の式です。
$$
Presicion_{[n]} = \frac{TN}{TN + FN}
$$
$$
Presicion_{[p]} = \frac{TP}{TP + FP}
$$
nのf値はどれだけ正確にノイズを除去できているかを、pのf値はどれだけ正確に本文を抽出できているかを評価していると考えればよいでしょう。
元のデータでの再現学習も問題無く行えました。また日本語対応版もおおよそ元論文と同程度の精度が出ています。
## 要点2:軽量でCPUでも1ページ0.02s程度の時間で予測できる
Core i7,8コアのCPU環境でも1ページ0.02s程度の時間で予測が可能です
学習もデータセット180件に対して上記環境で30分から40分で終了します(GPUを使えばもっと速いです)
# 使い方
日本語対応版のソースコード、モデルは[こちら](https://github.com/sakata-yusuke-75/boilernet_japanese)に置いてます。
モデルは元のデータセットの文章部分だけをgoogle翻訳で日本語に変換したもので学習しています。

使用手順は以下です。
1. 本文抽出を行いたいhtmlファイルを用意する
2. 上記ファイルに前処理をあてる
3. 前処理済みデータをモデルに入力し予測を行う

出力として、本文として抽出された文章のtxtファイルと、削除された文章のtxtファイルが得られます。
詳細は上記githubの[README](https://github.com/sakata-yusuke-75/boilernet_japanese/blob/master/README.md)をご覧ください。

<!--
データセットを上げないほうが安心
モデルだけ上げる、前処理済みデータだけ上げる
-->
## 背景：既存のツールは精度が少し物足りない

スクレイピングしたデータを自然言語処理(NLP)タスクに用いようとしたら、本文以外の情報が大量で精度が出なかったので本文抽出ツールを探していました。
既存のツール([boilerpipe](https://github.com/kohlschutter/boilerpipe ),[BTE](https://github.com/alatani/body_text_extraction )など)は本文がぽろぽろ落ちたりする...
この後に自然言語処理をかませる事を考えるとrecallの高いモデルが欲しかったため,探していると本文抽出タスクのSOTA(最高精度)を教えて頂いたので日本語に対応してみました。
## BoilerNetについて
### 概要
* 論文:https://arxiv.org/abs/2004.14294
* 公式実装:https://github.com/mrjleo/boilernet

現在の本文抽出タスクのSOTA(最高精度)、実装と論文は上記。chromeの拡張機能に対応しているらしい。
名前は本文抽出タスクが英語だと"boilerplate removal"なのでそこから。
ライセンスはGPLなので注意が必要です。

### 本文抽出タスクとデータセットについて
本文抽出タスク(boilerplate removal)とはhtmlファイルのヘッダーやフッター、広告などの本文と関係の無い文章を除外するタスクです。
元論文では下図の3,4,5,6が本文(positive)ラベル、1,2が本文以外(negative)ラベルと定義されています。ほかにも

- 最近のおすすめ記事！などの他の記事へのリンクなどはnegative
- 記事のタグはnegative
- 画像のキャプションなどはpositive

と定義されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343740/d584bba0-57ff-456c-2da6-ad57da1aa9be.png)
元論文より引用: https://arxiv.org/pdf/2004.14294.pdf

データセットは元論文の著者らが2017年のGoogle Trendsのキーワードをもとにランダムにサンプルした180件のhtmlファイルに上記のラベル付けをしたものです。(大変そう...)

### htmlファイルをモデルの入力形式(Bag of Words)に前処理
前処理ではhtmlファイルを受け取って以下の変換を行います。
1. htmlファイルをテキストが含まれるleafノード毎に分割
2. 各leafノードのrootまでのhtmlタグとテキストの単語をBag of Words形式に収集
3. タグの頻出上位n件と単語の頻出上位m件の辞書を作成(n,mは引数で指定可能)

上記手順によってデータセットとhtmlタグの辞書、単語の辞書を作成します。学習にはもちろん、予測の際もタグの辞書、単語の辞書が必要なことに注意してください。

また注意すべき点として文章の途中で単語hogeが強調やリンクのためにタグで分割されていた場合、hoge以前、hoge、hoge以後はそれぞれ別の文章として扱われます。
(実用上も精度上もこれを一文として扱えるようになると良くなりそう)

### 入力と出力
前処理によって作成したデータセットをモデルの入力とします。

モデルの入力 : テキストが含まれるleafノードのrootまでのhtmlタグとテキスト内に含まれる単語のBag of Words
出力 : 各leafノードが本文か否か

また入力は元のhtmlファイルの順番に従って入力されます。これは記事の配置情報を予測に活用するためです。

### HTMLタグと文章情報を埋め込んで双方向LSTMに入力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343740/bcce8a97-158f-48c1-a937-02b2e282ee9b.png)

元論文より引用: https://arxiv.org/pdf/2004.14294.pdf

前の項の形式の入力を最初に全結合層で畳み込み、それを複数層からなる双方向LSTMに渡しています。
上図の白い四角がタグのベクトルを、黒い四角がテキストのベクトルを表しています。
最後に双方向LSTMの出力を全結合層を通して二値分類に落とし込んでいます。
これによってある程度前後の情報を考慮した予測が行われています。

## 日本語対応
### 元論文のgoogletrendsデータセットを日本語に翻訳
Google Cloudの[Translation API](https://cloud.google.com/translate)を使用しています(有料)。はじめは[googletrans](https://pypi.org/project/googletrans/ )を使用していたのですが動作が安定しないのでデータセットが数十件を超えるならおとなしくTranslation APIを使う方が良さそうです。
中身はちょこちょこ変な翻訳になっていますが、全体では問題ない精度で変換できている気がします。
### tokenizerを日本語版Bertのtokenizerに変更
元のコードではtokenizerにnltkのword_tokenizeを使用していますが、日本語に対応していないのでHuggingface/Transformersの[bert-japanese](https://github.com/cl-tohoku/bert-japanese)に使われているtokenizerを使用しています。
中身はMeCab+WordPieceで、max sequence lengthは512です。

### 精度
|       | f1[n,p]                                 | precision[n,p]                             | recall[n,p]                                | support[n,p]         
| ---------- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- | -------------- |
|英語(元論文)|[0.90, 0.78]|[0.95, 0.70]|[0.86, 0.88]|[38691, 14122]|
| 日本語版(語彙数5000) | [0.90, 0.79] | [0.95, 0.69] | [0.86, 0.89] | [38691, 14122] |
| HTML情報のみ(語彙数0) | [0.90, 0.78] | [0.95, 0.70] | [0.87, 0.88] | [38691, 14122] |


精度は元論文と同様かなりの高精度が出ます。実際に手元のデータに適用してみてもかなり良い結果が得られました。
実は言語情報を入れなくても(語彙数0にしても)結構良い精度が出ています。
実際に手元のデータに適用してみると日本語対応版の方が不要なテキストを落とせる傾向にありますが、あまりに未知語が多いデータセットに対しては語彙数0版でも良いかもしれません。

### エラー分析
以下に手元の数件のデータから見受けられた日本語対応版の予測が外れやすい傾向を紹介します。青い線で強調されている所は本文と予測された所、黄色い線で強調されている所は正解データで本文とされる所です。
#### エラー傾向1:formタグが落とせない
![nssol_form落とせない.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343740/5d4172c2-2dc9-4a41-d6fa-346ed13d5062.png)

https://www.nssol.nipponsteel.com/cgi-bin/form/inquiry.pl よりスクリーンショットを加工

訓練データセットにformタグが存在しないためかお問い合わせ欄などは非常に落としにくいです。
実用上はformタグはルールベースで落としてしまうのが良さそうですね
#### エラー傾向2:xhtml形式だと閾値がかなり偏る
xhtml形式の文書は全体的にかなり落ちやすくなっておりデフォルトでは全文書が落ちてしまったりします。
おそらく訓練データにxhtml形式の文書が無いせいでしょう。
ヒューリスティックですが予測値0.5以上で本文と判定している所を0.1以上に変更すると、それなりに良い結果が得られます。xhtml以外の文書への影響も2,3文ノイズが増える程度です。
#### エラー傾向3:1つのページに複数の文章が並列に展開されていると本文が落ちやすい

![nssol_並列してると取れない.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343740/e9c3dc7d-826b-dc01-41fd-c05a6f0129e2.png)

https://www.nssol.nipponsteel.com/press/2020/20201028_110000.html よりスクリーンショットを加工

4つ並列の注記があるのですが途中から落ちてしまっています。
双方向LSTMによって前後の情報を参照していますが、並列の要素が多いと意味的なひとつ前のブロックの情報や後のブロックの情報が参照しにくいのか予測が不安定になる傾向があるようです。

<!--
### パラメータなど
学習パラメータは単語の語彙数5000、タグの語彙数50、レイヤー数2、隠れ層のサイズは256、エポック数50です。
予測の際はworking_dir直下にあるtrain.csvを参照しテスト結果が一番良かったモデルを使用しています。(今回は9エポック目を使用)

### 使用時の注意点など
英語版の再学習を行う際は環境構築後、実行前に

```
python -c "import nltk; nltk.download('punkt')
```
でnltkのダウンロードを済ませておかないとパースに失敗します。(エラーが出ないので気づきにくい)

-->

## 参考文献
[Boilerplate Removal using a Neural Sequence Labeling Model](https://arxiv.org/pdf/2004.14294.pdf)
[Web2Text: Deep Structured Boilerplate Removal](https://arxiv.org/abs/1801.02607)

## 免責事項

著者は本記事を掲載するにあたって、その内容、機能等について細心の注意を払っておりますが、内容が正確であるかどうか、安全なものであるか等について保証をするものではなく、何らの責任を負うものではありません。
本記事内容のご利用により、万一、ご利用者様に何らかの不都合や損害が発生したとしても、著者や著者の所属組織（日鉄ソリューションズ株式会社）は何らの責任を負うものではありません。