【ポートフォリオ】固有表現抽出データセットの作成　＃７　トークン化前のデータセットの作成（untokenized_dataset_list）

# 概要

固有表現抽出データセットを作成します。

:::note
ポートフォリオとして、**自作のデータセットでファインチューニングした言語モデルを使ったアプリ**を公開しました。

この記事を読むだけでも、この記事の内容をある程度理解できるとは思いますが、[データセット作成の導入記事](https://qiita.com/wolf4032/private/df5522799f3d6b30aaff)や、前段階の記事を読んでいる前提で書かれています。
:::

固有表現抽出データセット

https://huggingface.co/datasets/wolf4032/token-classification-japanese-search-local-cuisine


# この記事で作成されるもの

固有表現抽出データセット（トークン化前のデータセット）
```python
[
    {
        'text': '山梨または、青森県のやつを、検索してくれませんか？',
        'entities': [
            {'name': '山梨', 'span': [0, 2], 'type': 'AREA'},
            {'name': '青森県', 'span': [6, 9], 'type': 'AREA'}
        ]
    },
    {
        'text': '富山県または、静岡県のお料理が、あれば探して？',
        'entities': [
            {'name': '富山県', 'span': [0, 3], 'type': 'AREA'},
            {'name': '静岡県', 'span': [7, 10], 'type': 'AREA'}
        ]
    },
    {
        'text': '群馬と北東北で食べられる、料理を知ってたら、教えて？',
        'entities': [
            {'name': '群馬', 'span': [0, 2], 'type': 'AREA'},
            {'name': '北東北', 'span': [3, 6], 'type': 'AREA'}
        ]
    },
    ...,
    {
        'text': 'ぶりかエノキを使用した春に食べられている肉料理で、北陸地方の郷土料理があったら探して',
        'entities': [
            {'name': 'ぶり', 'span': [0, 2], 'type': 'INGR'},
            {'name': 'エノキ', 'span': [3, 6], 'type': 'INGR'},
            {'name': '春', 'span': [11, 12], 'type': 'SZN'},
            {'name': '肉料理', 'span': [20, 23], 'type': 'TYPE'},
            {'name': '北陸地方', 'span': [25, 29], 'type': 'AREA'}
        ]
    },
    {
        'text': 'フグあるいは練り辛子が使われている通年に食べられている飯料理で、北海道地方の料理をご存じでしたら、教えて',
        'entities': [
            {'name': 'フグ', 'span': [0, 2], 'type': 'INGR'},
            {'name': '練り辛子', 'span': [6, 10], 'type': 'INGR'},
            {'name': '通年', 'span': [17, 19], 'type': 'SZN'},
            {'name': '飯料理', 'span': [27, 30], 'type': 'TYPE'},
            {'name': '北海道地方', 'span': [32, 37], 'type': 'AREA'}
        ]
    },
    {
        'text': '鰌あるいはナガネギが使われている夏に食べられている野菜系で、滋賀県のものがあったら調べて下さい',
        'entities': [
            {'name': '鰌', 'span': [0, 1], 'type': 'INGR'},
            {'name': 'ナガネギ', 'span': [5, 9], 'type': 'INGR'},
            {'name': '夏', 'span': [16, 17], 'type': 'SZN'},
            {'name': '野菜系', 'span': [25, 28], 'type': 'TYPE'},
            {'name': '滋賀県', 'span': [30, 33], 'type': 'AREA'}
        ]
    }
]
```


# 補足情報

開発はGoogle Colaboratoryで行われ、[このノートブック](https://github.com/wolf4032/nlp-token-classification/blob/main/local_cuisine_search_app/notebooks/create_data/04_encoded_dataset_dataframe/encoded_dataset_dataframe_dependencies/01_untokenized_dataset_list/untokenized_dataset_list.ipynb)で作成しました。
[このノートブックを含むリポジトリ](https://github.com/wolf4032/nlp-token-classification/tree/main)の構造は、実際の開発環境と同一です。
`from`で参照されている自作モジュールは、[このディレクトリ](https://github.com/wolf4032/nlp-token-classification/tree/main/local_cuisine_search_app/modules)にあるものです。
その他のコード内で参照しているパスやディレクトリから`/content/drive/MyDrive`を省くと、そのパスやディレクトリの中身をリポジトリから確認することができます。


# 方針

テンプレートの固有表現（と”[PRON]”）を、ランダムに具体的な語彙へ置き換えて、データセットを作成します。


# 大まかな手順

1. [テンプレートの作成](https://qiita.com/wolf4032/private/8071f68c13cfeb561b3a)で作成したデータセットのテンプレートの読み込み
    ```python
    [
        '[AREA]または、[AREA]の[PRON]を、検索してくれませんか？',
        '[AREA]または、[AREA]の[PRON]が、あれば探して？',
        '[AREA]と[AREA]で食べられる、[PRON]を知ってたら、教えて？',
        '[AREA]と[AREA]で食べられる、[PRON]が、あったら探してくれませんか？',
        '[AREA]か[AREA]で食べられている、[PRON]を、ご存じでしたら、調べて？',
        ...,
        '[INGR]または、[INGR]を使った、[SZN]に食べられている[TYPE]で[AREA]の[PRON]があれば教えて欲しいです',
        '[INGR]か[INGR]を使用した[SZN]に食べられている[TYPE]で、[AREA]の[PRON]を知ってたら、教えて欲しいです',
        '[INGR]か[INGR]を使用した[SZN]に食べられている[TYPE]で、[AREA]の[PRON]があったら探して',
        '[INGR]あるいは[INGR]が使われている[SZN]に食べられている[TYPE]で、[AREA]の[PRON]をご存じでしたら、教えて',
        '[INGR]あるいは[INGR]が使われている[SZN]に食べられている[TYPE]で、[AREA]の[PRON]があったら調べて下さい'
    ]
    ```
    　
1. テンプレートを１行ずつ参照し、固有表現をランダムな語彙へ置き換えつつ、データセットを作成
    ```python
    [
        {
            'text': '山梨または、青森県のやつを、検索してくれませんか？',
            'entities': [
                {'name': '山梨', 'span': [0, 2], 'type': 'AREA'},
                {'name': '青森県', 'span': [6, 9], 'type': 'AREA'}
            ]
        },
        {
            'text': '富山県または、静岡県のお料理が、あれば探して？',
            'entities': [
                {'name': '富山県', 'span': [0, 3], 'type': 'AREA'},
                {'name': '静岡県', 'span': [7, 10], 'type': 'AREA'}
            ]
        },
        {
            'text': '群馬と北東北で食べられる、料理を知ってたら、教えて？',
            'entities': [
                {'name': '群馬', 'span': [0, 2], 'type': 'AREA'},
                {'name': '北東北', 'span': [3, 6], 'type': 'AREA'}
            ]
        },
        ...,
        {
            'text': 'ぶりかエノキを使用した春に食べられている肉料理で、北陸地方の郷土料理があったら探して',
            'entities': [
                {'name': 'ぶり', 'span': [0, 2], 'type': 'INGR'},
                {'name': 'エノキ', 'span': [3, 6], 'type': 'INGR'},
                {'name': '春', 'span': [11, 12], 'type': 'SZN'},
                {'name': '肉料理', 'span': [20, 23], 'type': 'TYPE'},
                {'name': '北陸地方', 'span': [25, 29], 'type': 'AREA'}
            ]
        },
        {
            'text': 'フグあるいは練り辛子が使われている通年に食べられている飯料理で、北海道地方の料理をご存じでしたら、教えて',
            'entities': [
                {'name': 'フグ', 'span': [0, 2], 'type': 'INGR'},
                {'name': '練り辛子', 'span': [6, 10], 'type': 'INGR'},
                {'name': '通年', 'span': [17, 19], 'type': 'SZN'},
                {'name': '飯料理', 'span': [27, 30], 'type': 'TYPE'},
                {'name': '北海道地方', 'span': [32, 37], 'type': 'AREA'}
            ]
        },
        {
            'text': '鰌あるいはナガネギが使われている夏に食べられている野菜系で、滋賀県のものがあったら調べて下さい',
            'entities': [
                {'name': '鰌', 'span': [0, 1], 'type': 'INGR'},
                {'name': 'ナガネギ', 'span': [5, 9], 'type': 'INGR'},
                {'name': '夏', 'span': [16, 17], 'type': 'SZN'},
                {'name': '野菜系', 'span': [25, 28], 'type': 'TYPE'},
                {'name': '滋賀県', 'span': [30, 33], 'type': 'AREA'}
            ]
        }
    ]
    ```


# コード

## import

```python:import
from google.colab import drive
drive.mount('/content/drive')

from typing import List, Dict
import random
import pandas as pd

import sys
sys.path.append('/content/drive/MyDrive/local_cuisine_search_app/modules')

from utility import load_json_obj, RandomPicker, search_strs, dump_obj_as_json
```

## 関数とクラスの定義

```python:関数とクラスの定義
def create_and_save(
        templates_path: str, unify_dics_path: str, file_name: str, save_dir: str
) -> List[Dict[str, str | List[Dict[str, str | List[int]]]]]:
    """
    データセットの作成と保存

    Parameters
    ----------
    templates_path : str
        データセットのテンプレートが保存されているパス
    unify_dics_path : str
        表記ゆれ統一用辞書が保存されているパス
    file_name : str
        保存するデータセットのファイル名
    save_dir : str
        データセットの保存先ディレクトリ

    Returns
    -------
    List[Dict[str, str | List[Dict[str, str | List[int]]]]]
        データセット
    """
    templates = load_json_obj(templates_path)
    data_maker = DataMaker(unify_dics_path)

    random.seed(42)

    dataset = [data_maker.create(template) for template in templates]

    dump_obj_as_json(dataset, file_name, save_dir)

    return dataset


class DataMaker:
    """
    データ作成用のクラス

    Attributes
    ----------
    _pron_label : str
        代名詞のラベル
        固有表現として言語モデルに抽出させる表現ではないため、エンティティの作成対象ではない
    _word_pickers : Dict[str, RandomPicker]
        ラベルと、ラベルに応じた語彙のRandomPickerの辞書
    _tokens : List[str]
        全トークンのラベルのリスト
    """
    _pron_label = 'PRON'

    def __init__(self, unify_dics_path: str):
        """
        コンストラクタ

        _word_pickerと_tokensの作成

        Parameters
        ----------
        unify_dics_path : str
            表記ゆれ統一用辞書が保存されているパス
        """
        self._word_pickers = DataMaker._create_word_pickers(unify_dics_path)
        self._tokens = list(self._word_pickers.keys())

    @staticmethod
    def _create_word_pickers(unify_dics_path: str) -> Dict[str, RandomPicker]:
        """
        word_pickersの作成

        Parameters
        ----------
        unify_dics_path : str
            表記ゆれ統一用辞書が保存されているパス

        Returns
        -------
        Dict[str, RandomPicker]
            ラベルと、ラベルに応じた語彙のRandomPickerの辞書
        """
        unify_dics: Dict[str, Dict[str, str]] = load_json_obj(unify_dics_path)
        word_pickers: Dict[str, RandomPicker] = {}

        for label, unify_dic in unify_dics.items():
            words = [word for words in unify_dic.items() for word in words]
            word_pickers[label] = RandomPicker(words)

        word_pickers['SZN'] = RandomPicker(['春', '夏', '秋', '冬', '通年'])
        word_pickers[DataMaker._pron_label] = RandomPicker(
            ['料理', 'お料理', '郷土料理', 'レシピ', 'もの', 'やつ']
        )

        return word_pickers

    def create(
            self, template: str
    ) -> Dict[str, str | List[Dict[str, str | List[int]]]]:
        """
        データの作成

        Parameters
        ----------
        template : str
            データのテンプレート

        Returns
        -------
        Dict[str, str | List[Dict[str, str | List[int]]]]
            データ
            文章と、その文章に含まれる固有表現の情報を持つ
        """
        include_tokens = search_strs(template, self._tokens)

        text = template
        entities: List[Dict[str, str | List[int]]] = []
        for token in include_tokens:
            replace_word = self._pick_word(token)

            if token != DataMaker._pron_label:
                entity_dic = self._create_entity_dic(text, token, replace_word)
                entities.append(entity_dic)

            text = text.replace(f'[{token}]', replace_word, 1)

        data = {'text': text, 'entities': entities}

        return data

    def _pick_word(self, token: str) -> str:
        """
        具体的な語彙の取得

        特殊トークンに入れる具体的な語彙をランダムに取得する

        Parameters
        ----------
        token : str
            置き換え対象の特殊トークンのラベル

        Returns
        -------
        str
            具体的な語彙
        """
        picker = self._word_pickers[token]
        word = picker.pick()

        return word

    def _create_entity_dic(
            self, text: str, token: str, replace_word: str
    ) -> Dict[str, str | List[int]]:
        """
        entity_dicの作成

        データに含まれる特定の固有表現の情報を持つ辞書を作成する

        Parameters
        ----------
        text : str
            データの文章
        token : str
            固有表現のラベル
        replace_word : str
            固有表現

        Returns
        -------
        Dict[str, str | List[int]]
            データに含まれる特定の固有表現の情報を持つ辞書
        """
        name_start_idx = text.find('[')
        name_end_idx = name_start_idx + len(replace_word)
        span = [name_start_idx, name_end_idx]

        entity_dic = {'name': replace_word, 'span': span, 'type': token}

        return entity_dic
```

## 実行

```python:実行
templates_path = '/content/drive/MyDrive/local_cuisine_search_app/data/processed_data/04_encoded_dataset_dataframe/encoded_dataset_dataframe_dependencies/01_untokenized_dataset_list/untokenized_dataset_list_dependencies/01_dataset_template_list/dataset_template_list.json'
unify_dics_path = '/content/drive/MyDrive/local_cuisine_search_app/data/processed_data/01_unifying_dictionaries/unifying_dictionaries.json'
file_name = 'untokenized_dataset_list'
save_dir = '/content/drive/MyDrive/local_cuisine_search_app/data/processed_data/04_encoded_dataset_dataframe/encoded_dataset_dataframe_dependencies/01_untokenized_dataset_list'

dataset = create_and_save(templates_path, unify_dics_path, file_name, save_dir)
```