【ポートフォリオ】固有表現抽出データセットの作成　＃１　基本的な文頭表現の作成（start_of_sentences_dataframe_v1）

# 概要

固有表現抽出データセットに必要な、文頭表現のデータフレームを作成します。

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

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

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

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


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

文頭表現のデータフレーム
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784631/61185e5d-ffd4-9eaa-3503-457676878d35.png)


# 補足情報
開発は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_dependencies/01_dataset_template_list/dataset_template_list_dependencies/01_start_of_sentences_dataframe/start_of_sentences_dataframe_v1.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`を省くと、そのパスやディレクトリの中身をリポジトリから確認することができます。


# 方針

各固有表現を表す文字列と、各固有表現に続く文字列をもとに、文頭表現のデータフレームを作成します。


# 大まかな手順

1. ”[AREA]”、”[TYPE]”、”[SZN]”、”[INGR]”の４つの固有表現をもとに、全組み合わせと、各組合せの全並び替えのリスト（`token_combs`）の作成
    ```python:token_combs
    [
        ('[AREA]',),
        ('[TYPE]',),
        ('[SZN]',),
        ('[INGR]',),
        ('[AREA]', '[TYPE]'),
        ('[TYPE]', '[AREA]'),
        ('[AREA]', '[SZN]'),
        ('[SZN]', '[AREA]'),
        ('[AREA]', '[INGR]'),
        ('[INGR]', '[AREA]'),
        ('[TYPE]', '[SZN]'),
        ('[SZN]', '[TYPE]'),
        ('[TYPE]', '[INGR]'),
        ('[INGR]', '[TYPE]'),
        ('[SZN]', '[INGR]'),
        ('[INGR]', '[SZN]'),
        ('[AREA]', '[TYPE]', '[SZN]'),
        ('[AREA]', '[SZN]', '[TYPE]'),
        ('[TYPE]', '[AREA]', '[SZN]'),
        ('[TYPE]', '[SZN]', '[AREA]'),
        ('[SZN]', '[AREA]', '[TYPE]'),
        ('[SZN]', '[TYPE]', '[AREA]'),
        ('[AREA]', '[TYPE]', '[INGR]'),
        ('[AREA]', '[INGR]', '[TYPE]'),
        ('[TYPE]', '[AREA]', '[INGR]'),
        ('[TYPE]', '[INGR]', '[AREA]'),
        ('[INGR]', '[AREA]', '[TYPE]'),
        ('[INGR]', '[TYPE]', '[AREA]'),
        ('[AREA]', '[SZN]', '[INGR]'),
        ('[AREA]', '[INGR]', '[SZN]'),
        ('[SZN]', '[AREA]', '[INGR]'),
        ('[SZN]', '[INGR]', '[AREA]'),
        ('[INGR]', '[AREA]', '[SZN]'),
        ('[INGR]', '[SZN]', '[AREA]'),
        ('[TYPE]', '[SZN]', '[INGR]'),
        ('[TYPE]', '[INGR]', '[SZN]'),
        ('[SZN]', '[TYPE]', '[INGR]'),
        ('[SZN]', '[INGR]', '[TYPE]'),
        ('[INGR]', '[TYPE]', '[SZN]'),
        ('[INGR]', '[SZN]', '[TYPE]'),
        ('[AREA]', '[TYPE]', '[SZN]', '[INGR]'),
        ('[AREA]', '[TYPE]', '[INGR]', '[SZN]'),
        ('[AREA]', '[SZN]', '[TYPE]', '[INGR]'),
        ('[AREA]', '[SZN]', '[INGR]', '[TYPE]'),
        ('[AREA]', '[INGR]', '[TYPE]', '[SZN]'),
        ('[AREA]', '[INGR]', '[SZN]', '[TYPE]'),
        ('[TYPE]', '[AREA]', '[SZN]', '[INGR]'),
        ('[TYPE]', '[AREA]', '[INGR]', '[SZN]'),
        ('[TYPE]', '[SZN]', '[AREA]', '[INGR]'),
        ('[TYPE]', '[SZN]', '[INGR]', '[AREA]'),
        ('[TYPE]', '[INGR]', '[AREA]', '[SZN]'),
        ('[TYPE]', '[INGR]', '[SZN]', '[AREA]'),
        ('[SZN]', '[AREA]', '[TYPE]', '[INGR]'),
        ('[SZN]', '[AREA]', '[INGR]', '[TYPE]'),
        ('[SZN]', '[TYPE]', '[AREA]', '[INGR]'),
        ('[SZN]', '[TYPE]', '[INGR]', '[AREA]'),
        ('[SZN]', '[INGR]', '[AREA]', '[TYPE]'),
        ('[SZN]', '[INGR]', '[TYPE]', '[AREA]'),
        ('[INGR]', '[AREA]', '[TYPE]', '[SZN]'),
        ('[INGR]', '[AREA]', '[SZN]', '[TYPE]'),
        ('[INGR]', '[TYPE]', '[AREA]', '[SZN]'),
        ('[INGR]', '[TYPE]', '[SZN]', '[AREA]'),
        ('[INGR]', '[SZN]', '[AREA]', '[TYPE]'),
        ('[INGR]', '[SZN]', '[TYPE]', '[AREA]'),
    ]
    ```
    　
1. `token_combs`と、各固有表現に続く文字列をもとに、文頭表現のデータフレームの作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784631/4b835103-457b-d6aa-4f7c-5042085d8505.png)

    （各固有表現には、後の処理で、具体的な固有表現を入れます。）
    （”[PRON]”には、”料理”、”レシピ”などの、文頭表現の末尾として適切な文字列が入ります。）


# コード

## import

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

from typing import Dict, List, Iterator, Tuple
import itertools
import pandas as pd

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

from pandas_utility import save_csv_df
```

## 関数とクラスの定義

```python:関数とクラスの定義
def create_and_save(file_name: str, save_dir: str) -> pd.DataFrame:
    """
    データフレームの作成と保存

    文頭表現のデータフレームを作成して保存する

    Parameters
    ----------
    file_name : str
        保存するファイル名
    save_dir : str
        保存先のディレクトリ

    Returns
    -------
    pd.DataFrame
        文頭表現のデータフレーム
    """
    df = DataframeMaker.create()

    save_csv_df(df, file_name, save_dir)

    return df


class DataframeMaker:
    """
    文頭表現のデータフレームを作成するクラス

    Attributes
    ----------
    _token_particles_dic : Dict[str, Tuple[str]]
        各トークンと、それに続く表現の辞書
    _pronoun_token : str
        代名詞のトークン
    _not_add_pronoun_tokens : Tuple[str]
        直後に代名詞が続かないトークン
    _not_last_particles : Tuple[str]
        最後のトークンには、これのいずれかの文字列を含む表現を使わない
    _not_double_strs : Tuple[str]
        これらの文字列は、一つの文頭表現で２回以上は採用しない
    """
    _token_particles_dic: Dict[str, Tuple[str]] = {
        '[AREA]': ('の', 'で', 'で食べられる', 'に'),
        '[TYPE]': ('で', 'に'),
        '[SZN]': ('の', 'に', 'に食べられる'),
        '[INGR]': ('を使った',)
    }
    _pronoun_token: str = '[PRON]'
    _not_add_pronoun_tokens: Tuple[str] = ('[TYPE]',)  # ※１
    _not_last_particles: Tuple[str] = ('に', 'で')
    _not_double_strs: Tuple[str] = ('に', '食')

    @classmethod
    def create(cls) -> pd.DataFrame:
        """
        データフレームの作成

        作成した全トークンの組み合わせのリストをもとに、
        文頭表現のデータフレームを作成する

        Returns
        -------
        pd.DataFrame
            文頭表現のデータフレーム
        """
        tokens = list(cls._token_particles_dic.keys())

        token_combs = DataframeMaker._create_token_combs(tokens)

        all_start_of_sentences = DataframeMaker._create_all_start_of_sentences(
            token_combs
        )

        df = pd.DataFrame(all_start_of_sentences)

        return df

    @staticmethod
    def _create_token_combs(tokens: List[str]) -> List[Tuple[str]]:
        """
        トークンの組み合わせの作成

        トークンの全ての組み合わせのイテレータを作成する

        Parameters
        ----------
        tokens : List[str]
            組み合わせ作成対象のトークン

        Returns
        -------
        List[Tuple[str]]
            トークンの全組み合わせのリスト
        """
        token_combs: List[Tuple[str]] = []

        max_tokens_num = len(tokens) + 1

        for tokens_num in range(1, max_tokens_num):
            combs: Iterator[Tuple[str]] = itertools.combinations(
                tokens, tokens_num
            )

            for comb in combs:
                permutations: Iterator[Tuple[str]] = itertools.permutations(comb)

                token_combs.extend(permutations)

        return token_combs

    @staticmethod
    def _create_all_start_of_sentences(
            token_combs: List[Tuple[str]]
    ) -> List[str]:
        """
        全ての文頭表現の作成

        全ての文頭表現のリストを作成する

        Parameters
        ----------
        token_combs : List[Tuple[str]]
            トークンの全組み合わせ

        Returns
        -------
        List[str]
            全ての文頭表現のリスト
        """
        all_start_of_sentences: List[str] = []

        for comb in token_combs:
            start_of_sentences = DataframeMaker._create_start_of_sentences(comb)

            all_start_of_sentences.extend(start_of_sentences)

        return all_start_of_sentences

    @classmethod
    def _create_start_of_sentences(cls, comb: Tuple[str]) -> List[str]:
        """
        文頭表現の作成

        渡されたトークンの組み合わせに該当する全ての文頭表現を作成する

        Parameters
        ----------
        comb : Tuple[str]
            トークンの組み合わせ

        Returns
        -------
        List[str]
            文頭表現のリスト
        """
        start_of_sentences: List[str] = []
        soss = start_of_sentences

        last_token_idx = len(comb) - 1

        for token_idx, token in enumerate(comb):
            if token_idx == last_token_idx:
                soss = DataframeMaker._add_last_strs(soss, token)

            else:
                particles = cls._token_particles_dic[token]
                tokens_added_particle = [token + partcl for partcl in particles]

                if token_idx == 0:
                    soss = tokens_added_particle

                else:
                    soss = DataframeMaker._add_following_strs(
                        soss, tokens_added_particle
                    )

        return soss

    @classmethod
    def _add_last_strs(cls, soss: List[str], token: str) -> List[str]:
        """
        最後の表現の追加

        特定の組み合わせの最後のトークンの表現を作成し、追加する

        Parameters
        ----------
        soss : List[str]
            作成途中の文頭表現のリスト
        token : str
            最後に追加するトークン

        Returns
        -------
        List[str]
            文頭表現のリスト
        """
        if token in cls._not_add_pronoun_tokens:
            if soss:
                soss = [sos + token for sos in soss]

            else:
                soss.append(token)

        else:
            particles = cls._token_particles_dic[token]
            tokens_added_particle = [
                token + partcl for partcl in particles
                if partcl not in cls._not_last_particles
            ]

            if soss:
                soss = DataframeMaker._add_following_strs(
                    soss, tokens_added_particle
                )
                soss = [sos + cls._pronoun_token for sos in soss]

            else:
                soss = [
                    t_a_p + cls._pronoun_token for t_a_p in tokens_added_particle
                ]

        return soss

    @classmethod
    def _add_following_strs(
            cls, soss: List[str], following_strs: List[str]
    ) -> List[str]:
        """
        続く表現の追加

        作成途中の文頭表現に続くトークンと付随する全ての表現を追加する

        Parameters
        ----------
        soss : List[str]
            作成途中の文頭表現
        following_strs : List[str]
            トークンとそれに続く表現のリスト

        Returns
        -------
        List[str]
            following_strsが付与された文頭表現のリスト
        """
        former_soss = soss.copy()
        soss.clear()

        for fmr_sos in former_soss:
            not_double_strs = [
                string for string in cls._not_double_strs if string in fmr_sos
            ]

            if not_double_strs:
                the_following_strs = [
                    f_str for f_str in following_strs
                    if not any(n_d_str in f_str for n_d_str in not_double_strs)
                ]

            else:
                the_following_strs = following_strs

            soss.extend([fmr_sos + f_str for f_str in the_following_strs])

        return soss
```

## 実行

```python:実行
file_name = 'start_of_sentences_dataframe_v1'
save_dir = '/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_dependencies/01_start_of_sentences_dataframe'

df = create_and_save(file_name, save_dir)
```

# メモ

**※１**
”[PRON]”に入るのは、”料理”とか、”もの”とか、”やつ”とか、検索対象そのものを示す言葉です。
そして、”[TYPE]”に入るのは、”飯料理”とか、”肉料理”とか、料理の種類を示す言葉です。
”[TYPE]”の直後に、”[PRON]”が続くような表現はおかしいので、（「飯料理の料理」とか、「肉料理のもの」とか）ので、`_not_add_pronoun_tokens`で”[PRON]”が続かないトークンを指定しています。