Wikiaを用いた固有表現抽出・エンティティリンキングデータセットの作製

# なんの記事？
Wikiaを用いて、固有表現抽出とエンティティ・リンキング用のデータセットを作製するツールを公開しました。

https://github.com/izuna385/Wikia-NER-and-EL-Dataset-Creator

# ツールで作製されるデータセットの中身
固有表現に対して、それに紐づくエンティティがセットでアノテーションされている形になります。下は、[VTuber](https://virtualyoutuber.fandom.com/)のwikiaを用いて作製されたデータセットの一部です。

```python3
    {
        "doc_title": "Melissa Kinrenka",
        "annotation": [
            {
                "0": {
                    "document_title": "Melissa Kinrenka",
                    "anchor_sent": "Melissa Kinrenka (メリッサ・キンレンカ) is a Japanese Virtual YouTuber and member of <a> Nijisanji </a>.",
                    "annotation_doc_entity_title": "Nijisanji",
                    "mention": "Nijisanji",
                    "original_sentence": "Melissa Kinrenka (メリッサ・キンレンカ) is a Japanese Virtual YouTuber and member of Nijisanji.",
                    "original_sentence_mention_start": 75,
                    "original_sentence_mention_end": 84
                },
                "1": {
                    "document_title": "Melissa Kinrenka",
                    "anchor_sent": "<a> Melissa Kinrenka </a> (メリッサ・キンレンカ) is a Japanese Virtual YouTuber and member of Nijisanji.",
                    "annotation_doc_entity_title": "Melissa Kinrenka",
                    "mention": "Melissa Kinrenka",
                    "original_sentence": "Melissa Kinrenka (メリッサ・キンレンカ) is a Japanese Virtual YouTuber and member of Nijisanji.",
                    "original_sentence_mention_start": 0,
                    "original_sentence_mention_end": 16
                }
            }
    }
```

各キーに紐づく値はそれぞれ以下のようになっています。

| key                             | value                                                                          | 
| ------------------------------- | ------------------------------------------------------------------------------------ | 
| `document_title`                  | アノテーションが存在する記事ページのタイトル                                              | 
| `anchor_sent`                     | アノテーション部分を `<a>` および `</a>` で囲んだ記事。エンティティ・リンキングを行う場合に使用します。| 
| `annotation_doc_entity_title`     | アノテーションの紐付け先記事のタイトル                           | 
| `mention`                         | 実際の文中のメンション（固有表現） | 
| `original_sentence`               | メンションが出現した、アンカー無しオリジナル文。                                                          | 
| `original_sentence_mention_start` | アンカー無しオリジナル文において、メンションの開始位置。                        | 
| `original_sentence_mention_end`   | 　アンカー無しオリジナル文において、メンションの終端位置。 | 

ライセンスはWikiaのライセンスに従います。

# Wikiaとは？
有志によるファンサイトコミュニティで、MediaWikiを利用したウィキ形式のウェブサイトです。MediaWiki形式なので、[WikiExtractor](https://github.com/attardi/wikiextractor)を用いて前処理を行うことが可能です。

下は[Star Wars](https://starwars.fandom.com/wiki/Star_Wars)からの引用です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/e9017188-70ef-9993-b900-245e21ddc735.png)

各コミュニティは、ウィキのページの集合から成り立ちます。それぞれのウィキ（例えば、上の"Ched Varga"の場合）を見てみると、登場人物の説明から始まり、説明文が続きます。

説明文の中には、同じコミュニティ内の別のページに飛ぶハイパーリンクが存在し、この部分を用いることで固有表現抽出とエンティティ・リンキング用のデータセットを作製することが可能です。

# 実際に行った前処理
## Wikiextractorを用いた、リンクあり記事の抽出

前処理を行いたいコミュニティウィキのダンプファイルが`[コミュニティ名].fandom.com/wiki/Special:Statistics`に存在するので、それをダウンロードして解凍した後、前処理を行います。(例：[リンク](https://virtualyoutuber.fandom.com/wiki/Special:Statistics))

wikiExtractorを用いて、リンクを残しつつ記事を抽出します。
`python -m wikiextractor.WikiExtractor ./dataset/virtualyoutuber_pages_current.xml --links --json`

## 外部リンクの削除
このままだと、外部リンクと`<a>`タグが残ったままの記事になり、固有表現抽出タスクには使用できません。なので、情報を残しつつ削除を行います。

### BeautifulSoupを用いた`<a>`タグ削除
```python3
     soup = BeautifulSoup(sentence, "html.parser")

     for link in soup.find_all("a"):
         if "http" in link.get("href"):
             link.unwrap()

     return str(soup)
```
これにより、ハイパーリンクを残しつつ外部リンクを削除します。例えば、`<a href="Nijisanji">Nijisanji</a>`などはハイパーリンクなので残ります。

## 正規表現を用いた、アノテーションスパンの抽出
この次に行われる、文章分割の精度を上げるため、テキストからハイパーリンク情報を分離します。

今回は正規表現を用いてアノテーション位置を同定し、`a`タグを除去した文章と位置情報を返す関数を実装しました。

```python3
    def _convert_a_tag_to_start_and_end_position(self, text_which_may_contain_a_tag: str):
        a_tag_regex = "<a>(.+?)</a>"
        pattern = re.compile(a_tag_regex)

        a_tag_remaining_text = copy.copy(text_which_may_contain_a_tag)
        mention_positions = list()

        while '<a>' in a_tag_remaining_text:
            result = re.search(pattern=pattern, string=a_tag_remaining_text)
            if result == None:
                break

            original_start, original_end = result.span()
            a_tag_removed_start = copy.copy(original_start)
            a_tag_removed_end = copy.copy(original_end) - 7
            mention = result.group(1)

            original_text_before_mention = a_tag_remaining_text[:original_start]
            original_text_after_mention = a_tag_remaining_text[original_end:]

            one_mention_a_tag_removed_text = original_text_before_mention + mention + original_text_after_mention
            assert mention == one_mention_a_tag_removed_text[a_tag_removed_start: a_tag_removed_end]

            mention_positions.append((a_tag_removed_start, a_tag_removed_end))

            a_tag_remaining_text = copy.copy(one_mention_a_tag_removed_text)

        return a_tag_remaining_text, mention_positions

```

## spaCyを用いた文章分割
`a`タグを除去することが出来たので、spaCyを用いて文章分割を行います。

wikia はその性質上、`lit.`という略語がよく現れます。（[参考](https://www.excite.co.jp/dictionary/ej/content/literally)）

この`lit.` による文章分割を防ぎつつ、文章分割を行います。

```python3
import spacy
from spacy.language import Language
from spacy.symbols import ORTH

nlp = spacy.load("en_core_web_md")

@Language.component('set_custom_boundaries')
def set_custom_boundaries(doc):
    for token in doc[:-1]:
        if token.text in ('lit.', 'Lit.', 'lit', 'Lit'):
            doc[token.i].is_sent_start = False
    return doc

nlp.add_pipe('set_custom_boundaries', before="parser")
nlp.tokenizer.add_special_case('lit.', [{ORTH: 'lit.'}])
nlp.tokenizer.add_special_case('Lit.', [{ORTH: 'Lit.'}])
```

## アノテーションの追加
wikiaのページでは、コミュニティの運営者によって、ドキュメント内の各文書にハイパーリンクが付与されますが、常にすべての固有表現にアノテーションがされるわけではありません。

例えば、以下の例を見てみましょう。（[Geroge Lucas](https://starwars.fandom.com/wiki/George_Lucas) のページから引用）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/1d2b7026-0d77-f0f8-7401-06264d036eec.png)

George Lucasはこの記事の中でも幾度となく出現しますが、それら全てにはアノテーションは付与されていません。なぜなら、このページがそもそも"George Lucas"を説明する記事であるからです。

また、以下のような例もあります。(同じく"Geroge Lucas"の記事内から引用)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/e373d174-473b-de54-7244-c486d473e085.png)

この場合は"Lucas"が"Geroge Lucas"を指すことは言うまでもありません。

今回のデータセット作製時には、このように、ハイパーリンクが付与されていないものの、エンティティであると認められる場合についてもアノテーションを付与しました。

実際には、コミュニティ内のタイトル集合との文字列マッチを用いて、アノテーションを行うようプログラムを作製しました。

# まとめ
wikiaのダンプファイルを用いて、固有表現抽出とエンティティ・リンキングに使用可能なデータセットを生成するツールを作製しました。

このツールを使用することで、[wikia](https://www.fandom.com/explore)に存在する各コミュニティに特化した固有表現抽出モデルや、エンティティ・リンキングモデルを訓練するためのデータセット作製が可能になります。

コードは[こちら](https://github.com/izuna385/Wikia-NER-and-EL-Dataset-Creator
)になります。

https://github.com/izuna385/Wikia-NER-and-EL-Dataset-Creator