from __future__ import annotations from typing import Dict, List, Tuple, Any from functools import reduce import operator import pandas as pd import gradio as gr from src.utility import load_json_obj from src.pandas_utility import read_csv_df from src.pipeline import NaturalLanguageProcessing from src.my_gradio import GrBlocks, GrLayout, GrComponent, GrListener 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_search = InputSearch( model_dir, label_info_dics, cuisine_df_path, unify_dics_path, cuisine_infos_num ) input_samples = InputSamplesDataset() extracted_words = ExtractedWordsHighlightedText(label_info_dics) links = Links() cuisine_infos = CuisineInfos(cuisine_infos_num) input = input_search.children['input'] search_btn = input_search.children['search_btn'] input_submitted = GrListener( trigger=[input.comp.submit, search_btn.comp.click], 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_search, input_samples, extracted_words, links, cuisine_infos ] listeners = [input_submitted, input_samples_selected] return children, listeners class InputSearch(GrLayout): """ 入力欄と検索ボタンのクラス Attributes ---------- layout_type : gr.Row GradioのRow """ layout_type = gr.Row 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 表示する料理検索結果の最大数 """ super().__init__( model_dir, label_info_dics, cuisine_df_path, unify_dics_path, cuisine_infos_num ) def _create( self, model_dir: str, label_info_dics: Dict[str, str | List[str]], cuisine_df_path: str, unify_dics_path: str, cuisine_infos_num: int ) -> Dict[str, InputTextbox | SearchButton]: """ 子要素の作成 Parameters ---------- model_dir : str ファインチューニング済みモデルが保存されているディレクトリ label_info_dics : Dict[str, str | List[str]] 固有表現のラベルとラベルに対する各種設定情報の辞書 cuisine_df_path : str 料理のデータフレームが保存されているパス unify_dics_path : str 表記ゆれ統一用辞書が保存されているパス cuisine_infos_num : int 表示する料理検索結果の最大数 Returns ------- Dict[str, InputTextbox | SearchButton] 入力欄と検索ボタン """ input = InputTextbox( model_dir, label_info_dics, cuisine_df_path, unify_dics_path, cuisine_infos_num ) search_btn = SearchButton() children = {'input': input, 'search_btn': search_btn} return children 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, scale=9) 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 SearchButton(GrComponent): """ 検索ボタンのクラス """ def _create(self) -> gr.Button: """ コンポーネントの作成 Returns ------- gr.Button 検索ボタンのコンポーネント """ comp = gr.Button(value='🔍', scale=1) return comp 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 Links(GrComponent): """ 外部リンクのクラス """ def _create(self) -> gr.HTML: """ コンポーネントの作成 Returns ------- gr.HTML 外部リンクのコンポーネント """ qiita_link = 'https://qiita.com/wolf4032/items/9dd7423c706fa86bf005' qiita = 'アプリに関する情報(Qiita)' github_link = 'https://github.com/wolf4032/nlp-token-classification/tree/main' github = 'GitHub' value = f"""
{qiita}
{github}
""" comp = gr.HTML(value=value) 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 model_name = 'wolf4032/bert-japanese-token-classification-search-local-cuisine' cuisine_df_path = 'src/local_cuisine_dataframe.csv' unify_dics_path = 'src/unifying_dictionaries.json' app = App.create_and_launch(model_name, cuisine_df_path, unify_dics_path)