【ポートフォリオ】固有表現抽出用BERTモデルとGradioで、郷土料理検索アプリの作成

# 概要

[固有表現抽出用BERTモデルのファインチューニング](https://qiita.com/wolf4032/private/08b4f868397ef6d44da9)で作成したモデルと、Gradioを使って、郷土料理検索アプリを作成します。

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

この記事を読むだけでも、この記事の内容をある程度理解できるとは思いますが、プロジェクト全体を理解している前提で書かれています。
プロジェクト全体の概要については、[こちらの記事](https://qiita.com/wolf4032/private/9dd7423c706fa86bf005)をご覧ください。
:::


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

郷土料理検索アプリ

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

（公開したアプリと、この記事で作成するアプリでは、多少異なる部分があります。）

:::note
Gradioが実行できない場合は、[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/wolf4032/nlp-token-classification/blob/main/local_cuisine_search_app/notebooks/cuisine_search.ipynb)で、Gradioを使わずに固有表現抽出と料理の検索を行えます。
:::


# アプリの解説

例えば、「東京の肉料理を教えてください」と入力すると、入力文から「東京」と、「肉料理」を、料理の特徴を示す語彙として抽出し、それらをキーワードとして郷土料理を検索します。
検索対象の郷土料理には、農林水産省が公開している、[うちの郷土料理](https://www.maff.go.jp/j/keikaku/syokubunka/k_ryouri/)の、飯料理、肉料理、野菜料理、魚料理を使用させていただきました。

見た目
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784631/a6496fc1-b739-dc88-4d00-d71fc7b1f7cf.png)


# 補足情報

開発はGoogle Colaboratoryで行われ、[このノートブック](https://github.com/wolf4032/nlp-token-classification/blob/main/local_cuisine_search_app/notebooks/app.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`を省くと、そのパスやディレクトリの中身をリポジトリから確認することができます。


# 方針

[固有表現抽出用BERTモデルのファインチューニング](https://qiita.com/wolf4032/private/08b4f868397ef6d44da9)で作成したモデルと、Gradioを使って、アプリを作成します。
（この記事のコードで行われるのは、アプリの作成と実行だけで、作成したアプリの、Hugging Face Spacesへの公開は含みません。）


# コード

## import、install

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

!pip install gradio

!pip install transformers fugashi ipadic
!pip install unidic-lite

from typing import Dict, List, Tuple, Any
from __future__ import annotations
from functools import reduce
import operator
import pandas as pd

import gradio as gr

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

from utility import load_json_obj
from pandas_utility import read_csv_df
from pipeline import NaturalLanguageProcessing
from my_gradio import GrBlocks, GrLayout, GrComponent, GrListener
```

## クラスの定義

```python:クラスの定義
class App(GrBlocks):
    """
    アプリのクラス
    """
    @staticmethod
    def _create_children_and_listeners(
            model_dir: str, cuisine_df_path: str, unify_dics_path: str
    ) -> Tuple[Dict[str, Any] | List[Any], List[Any]]:
        """
        子要素とイベントリスナーの作成

        Parameters
        ----------
        model_dir : str
            ファインチューニング済みモデルが保存されているディレクトリ
        cuisine_df_path : str
            料理のデータフレームが保存されているパス
        unify_dics_path : str
            表記ゆれ統一用辞書が保存されているパス

        Returns
        -------
        Tuple[Dict[str, Any] | List[Any], List[Any]]
            子要素とイベントリスナーのタプル
        """
        cuisine_infos_num = 10
        label_info_dics: Dict[str, str | List[str]] = {
            'AREA': {
                'jp': '都道府県/地方',
                'color': 'red',
                'df_cols': ['Prefecture', 'Areas']
            },
            'TYPE': {
                'jp': '種類',
                'color': 'green',
                'df_cols': ['Types']

            },
            'SZN': {
                'jp': '季節',
                'color': 'blue',
                'df_cols': ['Seasons']
            },
            'INGR': {
                'jp': '食材',
                'color': 'yellow',
                'df_cols': ['Ingredients list']
            }
        }

        input = InputTextbox(
            model_dir, label_info_dics, cuisine_df_path, unify_dics_path,
            cuisine_infos_num
        )
        input_samples = InputSamplesDataset()
        extracted_words = ExtractedWordsHighlightedText(label_info_dics)
        cuisine_infos = CuisineInfos(cuisine_infos_num)

        input_submitted = GrListener(
            trigger=input.comp.submit,
            fn=input.submitted,
            inputs=input,
            outputs=[extracted_words, cuisine_infos],
            scroll_to_output=True
        )

        input_samples_selected = GrListener(
            trigger=input_samples.comp.select,
            fn=InputSamplesDataset.selected,
            outputs=input,
            thens=input_submitted
        )

        children = [input, input_samples, extracted_words, cuisine_infos]
        listeners = [input_submitted, input_samples_selected]

        return children, listeners


class InputTextbox(GrComponent):
    """
    入力欄のクラス

    Attributes
    ----------
    _nlp : NaturalLanguageProcessing
        固有表現を抽出するオブジェクト
    _jp_label_dic : Dict[str, str]
        固有表現のラベルとその日本語訳の辞書
    _cuisine_info_dics_maker : CuisineInfoDictionariesMaker
        検索結果の料理の情報の辞書のリストを作成するオブジェクト
    """
    def __init__(
            self,
            model_dir: str,
            label_info_dics: Dict[str, str | List[str]],
            cuisine_df_path: str,
            unify_dics_path: str,
            cuisine_infos_num: int
    ):
        """
        コンストラクタ

        Parameters
        ----------
        model_dir : str
            ファインチューニング済みモデルが保存されているディレクトリ
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書
        cuisine_df_path : str
            料理のデータフレームが保存されているパス
        unify_dics_path : str
            表記ゆれ統一用辞書が保存されているパス
        cuisine_infos_num : int
            表示する料理検索結果の最大数
        """
        self._nlp = NaturalLanguageProcessing(model_dir)
        self._jp_label_dic: Dict[str, str] = {
            label: dic['jp'] for label, dic in label_info_dics.items()
        }

        self._cuisine_info_dics_maker = CuisineInfoDictionariesMaker(
            cuisine_df_path, unify_dics_path, label_info_dics, cuisine_infos_num
        )

        super().__init__()

    def _create(self) -> gr.Textbox:
        """
        コンポーネントの作成

        Returns
        -------
        gr.Textbox
            入力欄のコンポーネント
        """
        label = self._create_label()
        placeholder = 'どんな料理をお探しでしょうか？'
        comp = gr.Textbox(placeholder=placeholder, label=label)

        return comp

    def _create_label(self) -> str:
        """
        ラベルの作成

        Returns
        -------
        str
            コンポーネントのラベル
        """
        categories = [f'"{jp}"' for jp in self._jp_label_dic.values()]
        label = f'入力文から、料理の{"、".join(categories)}を示す語彙を検出します'

        return label

    def submitted(
            self, classifying_text: str
    ) -> List[List[Tuple[str, str]] | List[gr.Textbox | gr.Button]]:
        """
        submitイベントリスナーの関数

        Parameters
        ----------
        classifying_text : str
            固有表現抽出対象

        Returns
        -------
        List[List[Tuple[str, str]] | List[gr.Textbox | gr.Button]]
            抽出結果のリストと、料理検索結果に応じた
            テキストボックスとボタンのリストのリスト
        """
        classified_words: Dict[str, List[str]] = self._nlp.classify(classifying_text)

        pos_tokens = [
            (word, self._jp_label_dic[label])
            for label, words in classified_words.items()
            for word in words
        ]

        cuisine_info_dics = self._cuisine_info_dics_maker.create(classified_words)
        cuisine_infos = CuisineInfos.update(cuisine_info_dics)

        return [pos_tokens] + cuisine_infos


class InputSamplesDataset(GrComponent):
    """
    入力例のクラス
    """
    def _create(self) -> gr.Dataset:
        """
        コンポーネントの作成

        Returns
        -------
        gr.Dataset
            入力例のコンポーネント
        """
        label = '入力例（クリック/タップすると入力されます）'
        input_samples = [
            'オオカミとムカデを使った肉料理を教えてください',
            '野菜料理で仙豆を使用したものはありますか？',
            'オールマイトの髪の毛を使った料理は？',
            '仙台の、宿儺の指を使った、夏に食べられる肉料理',
            '呪胎九相図が使われている料理を探しています'
        ]

        comp = gr.Dataset(
            label=label,
            components=[gr.Textbox()],
            samples=[[sample] for sample in input_samples]
        )

        return comp

    @staticmethod
    def selected(input: gr.SelectData) -> str:
        """
        selectイベントリスナーの関数

        Parameters
        ----------
        input : gr.SelectData
            _description_

        Returns
        -------
        str
            選択した入力例
        """
        return input.value[0]


class ExtractedWordsHighlightedText(GrComponent):
    """
    抽出結果のクラス
    """
    def __init__(self, label_info_dics: Dict[str, str | List[str]]):
        """
        コンストラクタ

        Parameters
        ----------
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書
        """
        super().__init__(label_info_dics)

    def _create(
            self, label_info_dics: Dict[str, str | List[str]]
    ) -> gr.HighlightedText:
        """
        コンポーネントの作成

        Parameters
        ----------
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書

        Returns
        -------
        gr.HighlightedText
            抽出結果のコンポーネント
        """
        color_map: Dict[str, str] = {
            dic['jp']: dic['color'] for dic in label_info_dics.values()
        }

        comp = gr.HighlightedText(
            color_map=color_map,
            combine_adjacent=True,
            adjacent_separator='、',
            label='検出語彙一覧'
        )

        return comp


class CuisineInfos(GrLayout):
    """
    全検索結果のクラス

    Attributes
    ----------
    layout_type : gr.Column
        GradioのColumn
    """
    layout_type = gr.Column

    def _create(self, cuisine_infos_num: int) -> List[CuisineInfo]:
        """
        子要素の作成

        Parameters
        ----------
        cuisine_infos_num : int
            表示する料理検索結果の最大数

        Returns
        -------
        List[CuisineInfo]
            全検索結果
        """
        children = [CuisineInfo() for _ in range(cuisine_infos_num)]

        return children

    @staticmethod
    def update(
            cuisine_info_dics: List[Dict[str, str]]
    ) -> List[gr.Textbox | gr.Button]:
        """
        全検索結果の更新

        Parameters
        ----------
        cuisine_info_dics : List[Dict[str, str]]
            検索で見つかった料理の情報を持つ辞書のリスト

        Returns
        -------
        List[gr.Textbox | gr.Button]
            全料理の情報のテキストボックスと、詳細ページへのボタンのリスト
        """
        cuisine_infos: List[gr.Textbox | gr.Button] = []

        for cuisine_info_dic in cuisine_info_dics:
            cuisine_info = CuisineInfo.update(cuisine_info_dic)

            cuisine_infos.extend(cuisine_info)

        return cuisine_infos


class CuisineInfo(GrLayout):
    """
    料理の情報とURLボタンのクラス

    Attributes
    ----------
    layout_type : gr.Row
        GradioのRow
    """
    layout_type = gr.Row

    def _create(self) -> List[InfoTextbox | UrlButton]:
        """
        子要素の作成

        Returns
        -------
        List[InfoTextbox | UrlButton]
            料理の情報のテキストボックスと、詳細ページへのボタンのリスト
        """
        info_textbox = InfoTextbox()
        url_btn = UrlButton()

        children = [info_textbox, url_btn]

        return children

    @staticmethod
    def update(cuisine_info_dic: Dict[str, str]) -> List[gr.Textbox | gr.Button]:
        """
        料理の情報とURLボタンの更新

        Parameters
        ----------
        cuisine_info_dic : Dict[str, str]
            検索で見つかった料理の情報を持つ辞書

        Returns
        -------
        List[gr.Textbox | gr.Button]
            料理の情報のテキストボックスと、詳細ページへのボタンのリスト
        """
        if cuisine_info_dic:
            textbox, btn = CuisineInfo._reset(cuisine_info_dic)

        else:
            textbox, btn = CuisineInfo._hide()

        return [textbox, btn]

    @staticmethod
    def _reset(cuisine_info_dic: Dict[str, str]) -> Tuple[gr.Textbox, gr.Button]:
        """
        料理の変更

        Parameters
        ----------
        cuisine_info_dic : Dict[str, str]
            検索で見つかった料理の情報を持つ辞書

        Returns
        -------
        Tuple[gr.Textbox, gr.Button]
            料理の情報のテキストボックスと、詳細ページへのボタンのタプル
        """
        cuisine_name = cuisine_info_dic['name']
        cuisine_info = cuisine_info_dic['info']
        cuisine_url = cuisine_info_dic['url']

        textbox = InfoTextbox.reset(cuisine_name, cuisine_info)
        btn = UrlButton.reset(cuisine_name, cuisine_url)

        return textbox, btn

    @staticmethod
    def _hide() -> Tuple[gr.Textbox, gr.Button]:
        """
        料理の非表示

        Returns
        -------
        Tuple[gr.Textbox, gr.Button]
            料理の情報のテキストボックスと、詳細ページへのボタンのタプル
        """
        textbox = InfoTextbox.hide()
        btn = UrlButton.hide()

        return textbox, btn


class InfoTextbox(GrComponent):
    """
    料理の情報のクラス
    """
    def _create(self) -> gr.Textbox:
        """
        コンポーネントの作成

        Returns
        -------
        gr.Textbox
            料理の情報のコンポーネント
        """
        comp = gr.Textbox(scale=9, visible=False)

        return comp

    @staticmethod
    def reset(cuisine_name: str, cuisine_info: str) -> gr.Textbox:
        """
        料理の情報の変更

        Parameters
        ----------
        cuisine_name : str
            料理名
        cuisine_info : str
            料理の情報

        Returns
        -------
        gr.Textbox
            料理の情報のコンポーネント
        """
        comp = gr.Textbox(value=cuisine_info, label=cuisine_name, visible=True)

        return comp

    @staticmethod
    def hide() -> gr.Textbox:
        """
        料理の情報の非表示

        Returns
        -------
        gr.Textbox
            非表示になった料理の情報のコンポーネント
        """
        comp = gr.Textbox(visible=False)

        return comp


class UrlButton(GrComponent):
    """
    URLボタンのクラス
    """
    def _create(self) -> gr.Button:
        """
        コンポーネントの作成

        Returns
        -------
        gr.Button
            詳細ページへのボタンのコンポーネント
        """
        comp = gr.Button(scale=1, visible=False)

        return comp

    @staticmethod
    def reset(cuisine_name: str, cuisine_url: str) -> gr.Button:
        """
        料理のボタンの更新

        Parameters
        ----------
        cuisine_name : str
            料理名
        cuisine_url : str
            料理の詳細ページへのURL

        Returns
        -------
        gr.Button
            詳細ページへのボタンのコンポーネント
        """
        value = cuisine_name + '\n詳細ページ'
        comp = gr.Button(value=value, link=cuisine_url, visible=True)

        return comp

    @staticmethod
    def hide() -> gr.Button:
        """
        料理のボタンの非表示

        Returns
        -------
        gr.Button
            非表示になった詳細ページへのボタンのコンポーネント
        """
        comp = gr.Button(visible=False)

        return comp


class CuisineInfoDictionariesMaker:
    """
    料理検索結果の辞書のリスト作成用クラス

    Attributes
    ----------
    _cuisine_searcher : CuisineSearcher
        料理を検索するオブジェクト
    _word_unifier : WordUnifier
        抽出結果の表記ゆれを統一するオブジェクト
    """
    def __init__(
            self,
            cuisine_df_path: str,
            unify_dics_path: str,
            label_info_dics: Dict[str, str | List[str]],
            cuisine_infos_num: int
    ):
        """
        コンストラクタ

        Parameters
        ----------
        cuisine_df_path : str
            料理のデータフレームが保存されているパス
        unify_dics_path : str
            表記ゆれ統一用辞書が保存されているパス
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書
        cuisine_infos_num : int
            表示する料理検索結果の最大数
        """
        self._cuisine_searcher = CuisineSearcher(
            cuisine_df_path, label_info_dics, cuisine_infos_num
        )
        self._word_unifier = WordUnifier(unify_dics_path)

    def create(
            self, classified_words: Dict[str, List[str]]
    ) -> List[Dict[str, str]]:
        """
        料理検索結果の辞書の作成

        Parameters
        ----------
        classified_words : Dict[str, List[str]]
            ラベルと、そのラベルに分類された固有表現の辞書

        Returns
        -------
        List[Dict[str, str]]
            料理検索結果の辞書のリスト
        """
        unified_words = self._word_unifier.unify(classified_words)
        cuisine_info_dics = self._cuisine_searcher.search(unified_words)

        return cuisine_info_dics


class CuisineSearcher:
    """
    料理検索用のクラス

    Attributes
    ----------
    _search_infos : List[str]
        料理のどの情報を取ってくるか示したリスト
    _df : pd.DataFrame
        料理のデータフレーム
    _label_to_col : Dict[str, List[str]]
        固有表現のラベルに対して、検索するデータフレームの列のリストの辞書
    _words_dic : Dict[str, List[str]]
        データフレームの列と、列に含まれる全ての要素の辞書
    _cuisine_infos_num : int
        表示する料理検索結果の最大数
    """
    _search_infos = [
        'Name', 'Prefecture', 'Types', 'Seasons', 'Ingredients', 'Detail URL'
    ]

    def __init__(
            self,
            cuisine_df_path: str,
            label_info_dics: Dict[str, str | List[str]],
            cuisine_infos_num: int
    ):
        """
        コンストラクタ

        Parameters
        ----------
        cuisine_df_path : str
            料理のデータフレームが保存されているパス
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書
        cuisine_infos_num : int
            表示する料理検索結果の最大数
        """
        self._df = read_csv_df(cuisine_df_path)
        self._label_to_col = self._create_label_to_col(label_info_dics)
        self._words_dic = {
            col: self._find_words(col)
            for cols in self._label_to_col.values() for col in cols
        }
        self._cuisine_infos_num = cuisine_infos_num

    def _create_label_to_col(
            self, label_info_dics: Dict[str, str | List[str]]
    ) -> Dict[str, List[str]]:
        """
        label_to_colの作成

        固有表現のラベルに対応したデータフレームの列を
        特定するための辞書を作成する

        Parameters
        ----------
        label_info_dics : Dict[str, str  |  List[str]]
            固有表現のラベルとラベルに対する各種設定情報の辞書

        Returns
        -------
        Dict[str, List[str]]
            固有表現のラベルに対して、検索するデータフレームの列のリストの辞書

        Raises
        ------
        ValueError
            label_info_dicsに、データフレームに存在しない列名が含まれている場合
        """
        label_to_col: Dict[str, List[str]] = {
            label: dic['df_cols'] for label, dic in label_info_dics.items()
        }

        df_cols = self._df.columns.tolist()
        for cols in label_to_col.values():
            for col in cols:
                if col not in df_cols:
                    raise ValueError(f'"{col}"という列名は存在しません')

        return label_to_col

    def _find_words(self, col: str) -> List[str]:
        """
        列に含まれる全要素の取得

        Parameters
        ----------
        col : str
            列名

        Returns
        -------
        List[str]
            列に含まれる全ての要素のリスト
        """
        words: List[str, List[str]] = self._df[col].value_counts().index.tolist()

        if isinstance(words[0], list):
            words_lst = words
            unique_words: List[str] = []

            for words in words_lst:
                for word in words:
                    if word not in unique_words:
                        unique_words.append(word)

            return unique_words

        return words

    def search(self, unified_words: Dict[str, List[str]]) -> List[Dict[str, str]]:
        """
        料理の検索

        Parameters
        ----------
        unified_words : Dict[str, List[str]]
            表記ゆれが統一された固有表現の辞書

        Returns
        -------
        List[Dict[str, str]]
            検索結果の料理の情報を持つ辞書のリスト
        """
        on_df_words_dic = self._create_on_df_words_dic(unified_words)

        if not on_df_words_dic:
            gr.Info('いずれの語彙もデータに存在しませんでした')

            return self._create_empty_dics()

        cuisine_info_dics = self._create_cuisine_info_dics(on_df_words_dic)

        return cuisine_info_dics

    def _create_on_df_words_dic(
            self, unified_words: Dict[str, List[str]]
    ) -> Dict[str, List[str]]:
        """
        データフレームに存在する固有表現だけの辞書の作成

        Parameters
        ----------
        unified_words : Dict[str, List[str]]
            表記ゆれが統一された固有表現の辞書

        Returns
        -------
        Dict[str, List[str]]
            データフレームに存在する表記ゆれが統一された固有表現の辞書
        """
        on_df_words_dic = {col: [] for col in self._words_dic}
        not_on_df_words: List[str] = []

        for label, words in unified_words.items():
            search_cols = self._label_to_col[label]

            for word in words:
                not_on_df = True

                for col in search_cols:
                    if word in self._words_dic[col]:
                        on_df_words_dic[col].append(word)

                        not_on_df = False

                        break

                if not_on_df:
                    not_on_df_words.append(word)

        if not_on_df_words:
            CuisineSearcher._show_not_on_df_words(not_on_df_words)

        on_df_words_dic = {
            col: words for col, words in on_df_words_dic.items() if words
        }

        return on_df_words_dic

    @staticmethod
    def _show_not_on_df_words(not_on_df_words: List[str]) -> None:
        """
        データフレームに存在しなかった固有表現の表示

        Parameters
        ----------
        not_on_df_words : List[str]
            データフレームに存在しなかった固有表現のリスト
        """
        words = '、'.join(not_on_df_words)
        message = f'無効な語彙:　{words}'

        gr.Info(message)

    def _create_empty_dics(self) -> List[Dict[Any, Any]]:
        """
        空の辞書のリストの作成

        検索結果に該当する料理がなかった場合は、CuisineInfosを非表示にする
        CuisineInfo.update()に空の辞書を渡すと、
        InfoTextboxとUrlButtonが非表示になる

        Returns
        -------
        List[Dict[Any, Any]]
            空の辞書のリスト
        """
        return [{} for _ in range(self._cuisine_infos_num)]

    def _create_cuisine_info_dics(
            self, words_dic: Dict[str, List[str]]
    ) -> List[Dict[str, str]]:
        """
        料理の情報を持つ辞書の作成

        Parameters
        ----------
        words_dic : Dict[str, List[str]]
            検索ワードのリストを持つ辞書

        Returns
        -------
        List[Dict[str, str]]
            料理の情報を持つ辞書のリスト
        """
        condition_lst: List[pd.Series] = []

        for col, words in words_dic.items():
            condition = self._create_condition(col, words)
            condition_lst.append(condition)

        conditions = reduce(operator.and_, condition_lst)

        cuisine_infos_lst = self._df.loc[conditions, self._search_infos].values.tolist()

        if len(cuisine_infos_lst) > self._cuisine_infos_num:
            cuisine_infos_lst = cuisine_infos_lst[:self._cuisine_infos_num]

        if not cuisine_infos_lst:
            gr.Info('検索条件が厳しすぎて、該当料理が見つかりませんでした')

            return self._create_empty_dics()

        cuisine_info_dics = self._lst_to_dics(cuisine_infos_lst)

        return cuisine_info_dics

    def _create_condition(self, col: str, words: List[str]) -> pd.Series:
        """
        検索条件の作成

        Parameters
        ----------
        col : str
            絞り込み対象列
        words : List[str]
            検索ワード

        Returns
        -------
        pd.Series
            該当料理の行がTrueになったboolのシリーズ
        """
        value_type = type(self._df.at[0, col])

        if value_type is list:
            condition = self._df[col].apply(
                lambda values: any(word in values for word in words)
            )

        else:
            conditions = [self._df[col] == word for word in words]
            condition = reduce(operator.or_, conditions)

        return condition

    def _lst_to_dics(
            self, infos_lst: List[List[str | List[str]]]
    ) -> List[Dict[str, str]]:
        """
        リストから辞書への変換

        料理の情報のリストのリストを、料理の情報の辞書のリストに変換する

        Parameters
        ----------
        infos_lst : List[List[str  |  List[str]]]
            料理の情報のリストのリスト

        Returns
        -------
        List[Dict[str, str]]
            料理の情報の辞書のリスト
        """
        dics: List[Dict[str, str]] = []

        for infos in infos_lst:
            infos = [
                '、'.join(info) if isinstance(info, list) else info
                for info in infos
            ]

            name = infos[0]
            info = ' | '.join(infos[1:-1])
            url = infos[-1]

            dic = {'name': name, 'info': info, 'url': url}
            dics.append(dic)

        dics_len = len(dics)

        if dics_len < self._cuisine_infos_num:
            dics = dics + [{} for _ in range(self._cuisine_infos_num - dics_len)]

        return dics


class WordUnifier:
    """
    表記ゆれ統一用のクラス

    Attributes
    ----------
    _not_unify_labels : List[str]
        表記ゆれ統一対象ではない固有表現のラベルのリスト
    _unify_dics : Dict[str, Dict[str, str]]
        ラベルと、そのラベルの固有表現の表記ゆれ統一用の辞書の辞書
    """
    _not_unify_labels = ['SZN']

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

        Parameters
        ----------
        unify_dics_path : str
            表記ゆれ統一用辞書が保存されているパス
        """
        self._unify_dics: Dict[str, Dict[str, str]] = load_json_obj(unify_dics_path)

    def unify(
            self, classified_words: Dict[str, List[str]]
    ) -> Dict[str, List[str]]:
        """
        表記ゆれの統一

        Parameters
        ----------
        classified_words : Dict[str, List[str]]
            ラベルと、そのラベルに分類された固有表現の辞書

        Returns
        -------
        Dict[str, List[str]]
            表記ゆれが統一された固有表現の辞書
        """
        for label, words in classified_words.items():
            if label in self._not_unify_labels:
                continue

            unify_dic = self._unify_dics[label]

            unified_words = [
                unify_dic[word] if word in unify_dic else word for word in words
            ]

            classified_words[label] = unified_words

        return classified_words
```

## 実行

```python:実行
model_dir = '/content/drive/MyDrive/local_cuisine_search_app/data/processed_data/05_finetuned_model/checkpoint/checkpoint-164'
cuisine_df_path = '/content/drive/MyDrive/local_cuisine_search_app/data/processed_data/02_local_cuisine_dataframe/local_cuisine_dataframe.csv'
unify_dics_path = '/content/drive/MyDrive/local_cuisine_search_app/data/processed_data/01_unifying_dictionaries/unifying_dictionaries.json'

app = App.create_and_launch(model_dir, cuisine_df_path, unify_dics_path)
```

# 参考資料

[AIアプリをさくっと作るなら、HuggingFace Spaceがおすすめ](https://qiita.com/NoriakiOshita/items/9cefe65477b9919b8f2e)
[Gradio](https://www.gradio.app/)

郷土料理の情報
出典:農林水産省Webサイト(https://www.maff.go.jp/j/keikaku/syokubunka/k_ryouri/)