fasttextをローカルで簡単に使用できるようにしてみた

# 初めに
社内でfasttextを使用する機会があったためその時に使用したfasttextのノウハウをもとにローカル環境で簡単に使えるようなコードを書いてみましたので参考にして使っていただければと思います

# 作成したもの
 今回作成したものはこちらに上げております。
 特にライセンスとかつけないので中の処理はご自由にお使いください。
 分かりづらい部分があれば随時直していきます
 
https://github.com/KiharaTakahiro/fasttext-test

## どんなことができるのか？
fasttextをPythonで使用するにあたりいくつか必要な準備がありますがそれらを簡単にするための処理になっています

* 対象文章のクレンジング
クレンジングは学習の疎外になりそうなデータを削除することでより精度の高い学習を行うために実施します。この辺の知識が詳しいわけではないのでなんとなくいらない記号を除いた程度ですが何もないよりはいいのではと思います
```python
  def _cleansing(self, content: str) -> str:
    """ 学習データのクレンジング

    Args:
        content (str): 文章

    Returns:
        str: 学習用に変換した文章
    """
    self._logger.info(f'学習データのクレンジング 開始 クレンジング前のデータ: {content}')
    # 半角記号の除去
    content = re.sub(r'[!-/:-@[-`{-~]', '', content)
    # 全角記号の除去
    content = re.sub(r'/[！-／：-＠［-｀｛-～、-〜”’・]', '', content)
    self._logger.info(f'学習データのクレンジング 開始 クレンジング後のデータ: {content}')
    return content

```

 
* 対象の文章を分かち書き
分かち書きは文章を品詞に区切る処理です。fasttextでは品詞ごとにスペースを空けて学習データを作成する必要があるため品詞を分けています。他の辞書をダウンロードする必要がないように「ipadic.MECAB_ARGS」を使っていますがこれが良くないのかこの辺の処理が少し微妙な気はしています。
また、今回は品詞で名詞が連続する場合はつなげるようにしています。
これがいいかどうかは分かりませんが個人的にはこっちの方がそれっぽいなと思っています。

```python
  def _tokenization(self, content: str) -> List[str]:
    """ 文章を分かち書きする

    Args:
        content (str): 分かち書きする対象の文章を指定する

    Returns:
        List[str]: 文章を分かち書きした後の文言を返却する
    """
    self._logger.info(f'分かち書き 開始 分かち書き前のデータ: {content}')
    tokens = []
    node = self._tagger.parseToNode(content)
    previous_node_name = ''
    while node:
      if not '' == node.surface:
        node_names = node.feature.split(",")[0]
        print(f'{previous_node_name} {node.surface} {node_names}')
        # 連続する文字が名詞だったらつなげるいいかどうかは別としてなんとなく
        if previous_node_name == '名詞' and node_names == '名詞':
          tokens[-1] = tokens[-1] + node.surface
        else:
          tokens.append(node.surface)
          previous_node_name = node_names
      node = node.next
    self._logger.info(f'分かち書き 終了 分かち書き後のデータ: {tokens}')
    return tokens
```

* fasttextの学習データの形式に変換
分かち書きまでができれば後は余裕ではあるのですが、その結果をもとにテストデータの形式に変換します。fasttextのテストデータの形式は`__label__{カテゴリ名} {分かち書きをスペースウ切りした結果}`で出力する必要があるのでその形式になるように変換を加えています
```python
  def _edit_train_data(self, category: str, tokens: List[str]) -> str:
    """ fasttextの教師ありデータの形式に変換

    Args:
        category (str): 学習対象のカテゴリ
        tokens (List[str]): 文章を分かち書きした結果

    Returns:
        str: fasttextの教師ありデータの形式変換した値
    """
    self._logger.info(f'fasttext用形式変換 開始 変換前のデータ: {category}')
    edit_data =  f"{self.LABEL_NAME}{category} {' '.join(tokens)}"
    self._logger.info(f'fasttext用形式変換 終了 変換後のデータ: {edit_data}')
    return edit_data
```

* fasttextの実行
学習したデータをfasttextに食わせることで情報を学習します。
今回はなんとなく認識制度がよさそうなパラメータに調整してみましたが、ここは試行錯誤が必要なポイントかと思います。今回めんどくさくてそこまでやらなかったのですが、ここも引数で指定できるようにすればよかったなと思っています。
```python
      # モデルを学習し保存
      self._logger.info(f'モデルの作成 開始 学習データパス: {path}')
      # この辺のパラメータはなんとなくなので要調整
      model = fasttext.train_supervised(input=path, epoch=20, wordNgrams=2)
      self._logger.info(f'モデルの作成 終了 学習データパス: {path}')
      self._logger.info(f'モデルの保存 開始 モデルデータパス: {model_path}')
      model.save_model(model_path)
      self._logger.info(f'モデルの保存 終了 モデルデータパス: {model_path}')
```

* 学習データをもとにカテゴリ分析
ほとんど学習に使うコストのが高いのでやっていることはモジュールをロードして学習と同様にクレンジングした結果をもとに識別処理を実行します。
```python
  def predict(self, model_path: str, content: str):
    """識別処理

    Args:
        model_path (str): _description_
        content (str): _description_

    Raises:
        Exception: _description_

    Returns:
        _type_: _description_
    """
    if not os.path.isfile(model_path):
      raise ApplicationException(f"学習データが存在しません model_path: {model_path}")
    model = fasttext.load_model(model_path)
    pre_data = self._cleansing(content)
    estimate = model.predict(pre_data)
    predict = {}
    for (label, degree) in zip(estimate[0], estimate[1]):
      label = re.sub(f"^{self.LABEL_NAME}", '', label)
      predict[label] = degree
    return predict
```

これらの機能を備えたものを作成してみました。

# 使い方
今回は適当にCopilotにテストデータを作成してもらってそのカテゴリ分析を行ってみますテストデータはなんとなく最近流行っている所得税とか消費税にしてみました。
```python
  processor = FastTextProcessor()
  model_path = processor.train_for_list([
    {
        "category": "所得税について",
        "contents": [
            "所得税について教えてください。",
            "103円の壁について詳しく知りたいです。",
            "確定申告はいつまでに行えばいいですか？",
            "扶養控除の条件を教えてください。",
            "年末調整の手続き方法を教えてください。",
            "所得税の計算方法を教えてください。",
            "所得税の控除項目には何がありますか？",
            "所得税の納付方法を教えてください。",
            "所得税の還付金について知りたいです。",
            "所得税の申告書の書き方を教えてください。",
            "所得税の税率はどのくらいですか？",
            "所得税の減免制度について教えてください。",
            "所得税の特例措置には何がありますか？",
            "所得税の申告期限はいつですか？",
            "所得税の節税対策を教えてください。",
            "所得税の控除対象には何がありますか？",
            "所得税の申告書類には何が必要ですか？",
            "所得税の納付期限はいつですか？",
            "所得税の還付申請の方法を教えてください。",
            "所得税の計算例を教えてください。",
            "所得税の申告方法を教えてください。",
            "所得税の控除額はどのくらいですか？",
            "所得税の申告書提出先はどこですか？",
            "所得税の申告書記入例を見せてください。",
            "所得税の控除申請の方法を教えてください。",
            "所得税の申告書作成の手順を教えてください。",
            "所得税の納付方法を詳しく教えてください。",
            "所得税の還付金受取方法を教えてください。",
            "所得税の控除対象者には誰が含まれますか？",
            "所得税の申告書提出方法を教えてください。",
            "所得税の控除申請方法を教えてください。",
            "所得税の申告書作成方法を教えてください。",
            "所得税の納付期限延長の手続き方法を教えてください。",
            "所得税の還付金申請方法を教えてください。",
            "所得税の控除対象経費には何がありますか？",
            "所得税の申告書提出期限はいつですか？",
            "所得税の控除申請書類には何が必要ですか？",
            "所得税の申告書作成例を見せてください。",
            "所得税の納付方法の詳細を教えてください。",
            "所得税の還付金受取方法の詳細を教えてください。",
            "所得税の控除対象経費の詳細を教えてください。",
            "所得税の申告書提出方法の詳細を教えてください。",
            "所得税の控除申請書類の詳細を教えてください。",
            "所得税の申告書作成方法の詳細を教えてください。",
            "所得税の納付期限延長の詳細を教えてください。",
            "所得税の還付金申請方法の詳細を教えてください。",
            "所得税の控除対象経費の詳細を教えてください。",
            "所得税の申告書提出期限の詳細を教えてください。"
        ]
    },
    {
        "category": "消費税について",
        "contents": [
            "消費税率について教えてください。",
            "預り金なのか教えてください。",
            "軽減税率の対象品目を教えてください。",
            "インボイス制度とは何ですか？",
            "消費税の申告方法を教えてください。",
            "消費税の計算方法を教えてください。",
            "消費税の控除項目には何がありますか？",
            "消費税の納付方法を教えてください。",
            "消費税の還付金について知りたいです。",
            "消費税の申告書の書き方を教えてください。",
            "消費税の税率はどのくらいですか？",
            "消費税の減免制度について教えてください。",
            "消費税の特例措置には何がありますか？",
            "消費税の申告期限はいつですか？",
            "消費税の節税対策を教えてください。",
            "消費税の控除対象には何がありますか？",
            "消費税の申告書類には何が必要ですか？",
            "消費税の納付期限はいつですか？",
            "消費税の還付申請の方法を教えてください。",
            "消費税の計算例を教えてください。",
            "消費税の申告方法を教えてください。",
            "消費税の控除額はどのくらいですか？",
            "消費税の申告書提出先はどこですか？",
            "消費税の申告書記入例を見せてください。",
            "消費税の控除申請の方法を教えてください。",
            "消費税の申告書作成の手順を教えてください。",
            "消費税の納付方法を詳しく教えてください。",
            "消費税の還付金受取方法を教えてください。",
            "消費税の控除対象者には誰が含まれますか？",
            "消費税の申告書提出方法を教えてください。",
            "消費税の控除申請方法を教えてください。",
            "消費税の申告書作成方法を教えてください。",
            "消費税の納付期限延長の手続き方法を教えてください。",
            "消費税の還付金申請方法を教えてください。",
            "消費税の控除対象経費には何がありますか？",
            "消費税の申告書提出期限はいつですか？",
            "消費税の控除申請書類には何が必要ですか？",
            "消費税の申告書作成例を見せてください。",
            "消費税の納付方法の詳細を教えてください。",
            "消費税の還付金受取方法の詳細を教えてください。",
            "消費税の控除対象経費の詳細を教えてください。",
            "消費税の申告書提出方法の詳細を教えてください。",
            "消費税の控除申請書類の詳細を教えてください。",
            "消費税の申告書作成方法の詳細を教えてください。",
            "消費税の納付期限延長の詳細を教えてください。",
            "消費税の還付金申請方法の詳細を教えてください。",
            "消費税の控除対象経費の詳細を教えてください。",
            "消費税の申告書提出期限の詳細を教えてください。"
        ]
    }])  
  print(processor.predict(model_path, "所得税の申告書提出期限"))
  print(processor.predict(model_path, "消費税の控除申請書類"))
```
上記のように記載して実行すると下記のような結果を得られます

一個目が所得税として認識されて二個目が消費税として分類されています。
なんとなくあってそうですね！
```bash
処理を開始します
Warning : `load_model` does not return WordVectorModel or SupervisedModel any more, but a `FastText` object which is very similar.
処理が終了しました
{'所得税について': 0.5003361105918884}
処理を開始します
Warning : `load_model` does not return WordVectorModel or SupervisedModel any more, but a `FastText` object which is very similar.
処理が終了しました
{'消費税について': 0.501374363899231}

```

# 終わりに
fasttextは使ってみるとものすごく簡単にカテゴリ分類を行えますが、分かち書きとかテストデータの形式変換とか地味にめんどくさいのでクラス化して使えるようにしておくと便利なんじゃないかと思って作成しました。
ご使用はご自由にバグってたら教えてください！