🐱

イナババ怪文書生成AIをHugging Face Spaces + Streamlitでデプロイした

2023/05/05に公開

モデル公開しました

以下のサイトでモデル公開中です。
君だけのオリジナル怪文書を生成しよう!
https://huggingface.co/spaces/Oishiyo/zupposhi-maker

Hightlights

  • huggingface + streamlitで自作AIアプリをデプロイ
  • かんたん!😁

初めに

今回の目的

プログラミングの成果物を公表する方法としては

  • GitHubに上げる
  • Pyinstallerなどでexeに固めて配布する
  • Heroku、Render、GCP、AWSなどのPaaS(またはPaaSを含むサービス)を使って公開する
    などの方法がある。

しかし、機械学習に関連したサービスの公開だとなかなか手軽な候補がない。
(自分の調べ方が悪いのもあるかもしれないが・・・)

  • GitHub
    • サイズの大きなファイルが苦手。Git-LFSというサービスもあるが、1 GB以上のファイルを取り扱う際は有料になる。
    • 機械学習のモデルファイル保存するには1GBは心もとない
  • Pyinstaller
    • 生成されるexeのサイズが肥大化しやすい。また、生成したexeがマルウェアと誤認される可能性がある。
    • Nuitkaなどのより優秀な後継も生まれてきているが、そもそも素性の分からない人の配布したexeとかダウンロードしたくない。
  • PaaS
    • 無料枠だとしょぼいCPUやRAM、ストレージしか使えない。

前々から機械学習を利用したサービスの公開に興味があったのだが、上記のような障壁があり挫折していた。

しかし、つい先日Hugging Face Spacesという便利な方法を見つけた。
無料枠でもある程度のCPU、RAM、ストレージを貰えて、かつ直感的・お手軽にアプリがデプロイできる。

ちょうど前回の記事でイナババ怪文書自動生成AIを作成したので、これをデプロイすることを目標にHugging Face Spacesの使い勝手に慣れたいと思う。

Hugging Face Spaces

「流行りのAI(Stable DiffusionやTransformer系のモデルなど)を使いたい!」となったら謎の絵文字(🤗)が書かれたHugging Faceというサイトに行き当たることも多いのではないだろうか。
このサイトでは機械学習関係のデータセットやモデルを共有するためのプラットフォームを提供している。
また、同社はtransformersというpythonライブラリを作っていることでも有名。

Hugging Faceはモデルを使う方でお世話になることが多いが、
モデルを共有する際にもHugging Face Spacesという便利な機能がある。
このHugging Face Spacesだが、

  • 無料枠でも以下の環境を貰える。自分が調べた比較対象に比べるとかなり太っ腹
    • RAM : 16 GB
    • CPU : 2コア
    • ディスク : 50 GB
  • 使用するSDKをStreamlitやGradio、Dockerなどから選択できる。

など、機械学習アプリ開発者にはありがたい面をいくつも備えている。

Streamlit

綺麗なUIをpythonのみで簡単に作成できる。多少クセがあったり(後述)、一定以上複雑なUIを実装するには向かないものの、最小限の労力でフロントエンドを実装できる。
類似したライブラリとしてGradioというのもあるが、自分はStreamlitに慣れているのでこちらを選択。

開発

ローカルでのアプリ開発

まず、前回の記事でファインチューニングしたGPT-2を読み込み、推論を行わせるためのクラスを作る。

ポイントはno_gradを使う点。勾配の計算を省略してメモリ消費量を減らすことができるらしい。
コードは以下の通り

Zmaker.py
import torch
from transformers import AutoModelForCausalLM, T5Tokenizer
import csv, re, mojimoji

class Zmaker:

    #GPT2のモデル名
    gpt_model_name = "rinna/japanese-gpt2-medium"

    #文章の最大長
    min_len, max_len = 1, 128

    #予測時のパラメータ
    top_k, top_p = 40, 0.95 #top-k検索の閾値
    num_text = 1 #出力する文の数
    temp = 0.1
    repeat_ngram_size = 1

    #推論にCPU利用を強制するか
    use_cpu = True

    def __init__(self, ft_path = None):
        """コンストラクタ

          コンストラクタ

          Args:
              ft_path : ファインチューニングされたモデルのパス.Noneを指定すると
          Returns:
              なし
        """

        #モデルの設定
        self.__SetModel(ft_path)

        #モデルの状態をCPUかGPUかで切り替える
        if self.use_cpu: #CPUの利用を強制する場合の処理
            device = torch.device('cpu')
        else: #特に指定が無いなら,GPUがあるときはGPUを使い,CPUのみの場合はCPUを使う
            device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
        self.model.to(device)



    def __SetModel(self, ft_path = None):
      """GPT2の設定

        GPT2のTokenizerおよびモデルを設定する.
        ユーザー定義後と顔文字も語彙として認識されるように設定する.
        
        Args:
            ft_path : ファインチューニング済みのモデルを読み込む
                      何も指定しないとself.gpt_model_nameの事前学習モデルを
                      ネットからダウンロードする.
        Returns:
            なし
      """
      #GPT2のTokenizerのインスタンスを生成
      self.tokenizer = T5Tokenizer.from_pretrained(self.gpt_model_name)
      self.tokenizer.do_lower_case = True # due to some bug of tokenizer config loading

      #モデルの読み込み
      if ft_path is not None:
          self.model = AutoModelForCausalLM.from_pretrained(
              ft_path, #torch_dtype = torch.bfloat16
          )
      else:
          print("fine-tuned model was not found")
    
      #モデルをevalモードに
      self.model.eval()

    def __TextCleaning(self, texts):
        """テキストの前処理をする

          テキストの前処理を行う.具体的に行うこととしては...
          ・全角/半角スペースの除去
          ・半角数字/アルファベットの全角化
        """
        #半角スペース,タブ,改行改ページを削除
        texts = [re.sub("[\u3000 \t \s \n]", "", t) for t in texts]

        #半角/全角を変換
        texts = [mojimoji.han_to_zen(t) for t in texts]
        return texts
    

    def GenLetter(self, prompt):
      """怪文書の生成

        GPT2で怪文書を生成する.
        promptに続く文章を生成して出力する

        Args:
            prompt : 文章の先頭
        Retunrs:
            生成された文章のリスト
      """

      #テキストをクリーニング
      prompt_clean = [prompt]
      
      #文章をtokenizerでエンコード
      x = self.tokenizer.encode(
          prompt_clean[0], return_tensors="pt", 
          add_special_tokens=False
      )
      
      #デバイスの選択
      if self.use_cpu: #CPUの利用を強制する場合の処理
          device = torch.device('cpu')
      else: #特に指定が無いなら,GPUがあるときはGPUを使い,CPUのみの場合はCPUを使う
          device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
      x = x.to(device)

      #gpt2による推論
      with torch.no_grad():
          y = self.model.generate(
              x, #入力
              min_length=self.min_len,  # 文章の最小長
              max_length=self.max_len,  # 文章の最大長
              do_sample=True,   # 次の単語を確率で選ぶ
              top_k=self.top_k, # Top-Kサンプリング
              top_p=self.top_p,  # Top-pサンプリング
              temperature=self.temp,  # 確率分布の調整
              no_repeat_ngram_size = self.repeat_ngram_size, #同じ単語を何回繰り返していいか
              num_return_sequences=self.num_text,  # 生成する文章の数
              pad_token_id=self.tokenizer.pad_token_id,  # パディングのトークンID
              bos_token_id=self.tokenizer.bos_token_id,  # テキスト先頭のトークンID
              eos_token_id=self.tokenizer.eos_token_id,  # テキスト終端のトークンID
              early_stopping=True
          )
      
      # 特殊トークンをスキップして推論結果を文章にデコード
      res = self.tokenizer.batch_decode(y, skip_special_tokens=True)
      return res

次に、Zmakerの推論をUIから実行できるようにするコードをstreamlitというUI作成用ライブラリを使って記述する。

実装に当たって一番厄介だったのは、streamlitの変数保持の仕様である。
streamlitではボタンを押すなどのイベントを起こすと、コードが一番最初から再度実行されUIのウィジットに入力した変数の値が保持できない。
これを解決するためにsession_stateという機能を使った。
session_stateに変数を登録すると、ボタンなどを押してページが更新されても変数の値が保持される。
また、長時間かかる処理の部分はスピナー(st.spinner)を使って動きをつけるように意識した。

app.py
import streamlit as st
from Zmaker import Zmaker

if __name__ == "__main__":

    #ファインチューニング済みモデルの読み込み
    with st.spinner(text = "loading GPT-2..."):
        if not ("AI" in st.session_state.keys()):
            st.session_state["AI"] = Zmaker(
                ft_path = "./models/"
            )
    
    #設定用サイドバーの設定
    with st.sidebar:
        st.title("GPT-2のパラメータ")

        #max_lenの設定用スライダ
        sld_max_len = st.sidebar.slider(
            "length of the sentence", min_value = 0, max_value = 256, 
            value = (25, 75), step = 1, key = "length"
        )

        #temperatureの設定用スライダ
        sld_temp = st.sidebar.slider(
            "temperature", min_value = 0.1, max_value = 1.5, 
            value = 0.1, step = 0.1, key = "temp"
        )
        
        #top_kの設定用スライダ
        sld_top_k = st.sidebar.slider(
            "top_k", min_value = 0, max_value = 500, 
            value = 40, step = 1, key = "top_k"
        )
        
        #top_pの設定用スライダ
        sld_top_p = st.sidebar.slider(
            "top_p", min_value = 0.01, max_value = 1.0, 
            value = 0.95, step = 0.01, key = "top_p"
        )

        #repeat_ngram_sizeの設定用スライダ
        sld_top_p = st.sidebar.slider(
            "repeat_ngram_size ", min_value = 1, max_value = 10, 
            value = 1, step = 1, key = "repeat_ngram_size"
        )

    #メインフォームの設定
    with st.form(key = "Letter Form", clear_on_submit = False):
        st.title("おてがみ 入力欄")
        body = st.empty()
        if ("letter_body" in st.session_state.keys()): 
            ret = body.text_area(
                label = "おてがみを途中まで漢字+ひらがなで書くと続きをAIが生成します。"\
                        "生成できるのはおてがみ本文のみです。"\
                        "生成には数十秒~数分の時間がかかります。何卒ご了承ください。",
                value = st.session_state["letter_body"]
            )
        else:
            ret = body.text_area(
                label = "おてがみを途中まで漢字+ひらがなで書くと続きをAIが生成します。"\
                        "生成できるのはおてがみ本文のみです。"\
                        "生成には数十秒~数分の時間がかかります。何卒ご了承ください。",
                value = "ズッポシ村へようこそ!"
            )
        sub = st.form_submit_button("Generate")
    
    #注意事項
    with st.expander("zupposhi_makerについて/注意事項・免責事項"):
        st.markdown(
            """
            zupposhi_makerは稲葉百万鉄氏の[「どうぶつの森e+実況プレイ」](https://www.nicovideo.jp/mylist/45062007)の名物である、
            「イナババ怪文書」を用いて日本語GPT-2をファインチューニングして作成された生成AIです。
            本アプリの作成方法はエンジニア向け情報共有サービスzennにおいて[記事](https://zenn.dev/koujimachi2023/articles/b6bde79e73dd1d)としてまとめております。
            技術的な疑問点などありましたら、こちらの記事を参照ください。
            
            教師データの作成には以下のサイトを利用しました。
            - 稲葉百万鉄氏の[「どうぶつの森e+実況プレイ」](https://www.nicovideo.jp/mylist/45062007)において作成された文章
            - mintmama氏の[「ズッポシむら手紙集」](https://www.nicovideo.jp/series/85494)
            
            また、rinna社の配布する事前学習済みモデル(rinna/japanese-gpt2-medium)を
            ベースのモデルとして用いました。
            
            本アプリは稲葉百万鉄氏の動画作品に対するファン作品です。
            著作権法 第三十条の四に基づき今回の生成AIの学習は合法と解釈しておりますが、
            本アプリを利用する用途は個人利用の範囲に限定したいと考えております。
            そのため、本アプリおよびその生成データの**商用利用は一切禁止**とさせていただきます。
            
            また、以下の行為についても禁止とさせていただきます。
            - フェイクニュース作成への利用
            - 倫理上問題のある行為への利用(差別的な文章の生成・公開など)
            - わいせつ物の頒布
            - その他犯罪行為・迷惑行為への利用  
            
            本モデルの利用により利用者に何らかの不都合や損害が発生したとしても、
            モデルやデータセットの作者は何らの責任を負うものではありません。
            
            ただし、当方はWebアプリの公開など初心者です。
            調べられる範囲で対応しているつもりですが、
            著作権やライセンス、許可・禁止事項についての間違いがある可能性は大いにあります。
            つきましては、何かお気づきの点がありましたら遠慮なくご指摘いただけると幸いです。
            """
        )

       
    #submitボタンが押された
    if sub == True:
        #predictに必要な条件をGUIで設定した値に更新
        st.session_state["AI"].min_len = st.session_state["length"][0]
        st.session_state["AI"].max_len = st.session_state["length"][-1]
        st.session_state["AI"].top_k = st.session_state["top_k"]
        st.session_state["AI"].top_p = st.session_state["top_p"]
        st.session_state["AI"].temp = st.session_state["temp"]
        st.session_state["AI"].repeat_ngram_size = st.session_state["repeat_ngram_size"]

        #AIによる予測を実行
        with st.spinner(text = "generating..."):
            prompt = ret
            text  = str(st.session_state["AI"].GenLetter("<s>"+prompt)[0])
            text = text.replace('<s>', '')
            text = text.replace('</s>', '')
            st.session_state["letter_body"] = text
            st.experimental_rerun()

app.pyとZmaker.pyをファインチューニング済みモデルのconfig.jsonとpytorch_model.binが置かれているディレクトリと同じ場所に置き、
python環境を立ち上げたうえで以下のコマンドを実行するとUIが立ち上がるはずである。

streamlit run app.py

最後に、requirements.txtを作る。私の場合はvenvを使って環境を構築していたので、以下のコマンドで作成できた。

#venv環境立ち上げ。OSによって異なるので注意
.\[環境名]\Scripts\activate

#pipでインストールしたライブラリのリストをrequirements.txtに出力
pip freeze > requrements.txt

アプリのデプロイ

参考文献の2, 3とほぼ同じなのでここでは省略気味に書く。

  1. Hugging Faceのホームページからアカウント登録する。登録したメアドにメールが送られてくるので、添付の認証用のURLを押すと本登録が完了する。本登録を忘れるとアプリをデプロイしたときにエラー(hugging face , your account email address needs to be confirmed)がでるので注意。
  2. ホームページの左上にある「New」のボタン→「Space」をクリックし、スペースを作成
  3. 出てきた画面でスペースの設定を行う。重要なのはCPU/GPUのプランの選択(料金の問題があるため)とSpaceをPrivate/Publicのどちらにするか(Publicにすると初めから公開されてしまう)の設定。どちらもあとで設定から変更できる。また利用するSDKの種類を聞かれるのでStreamlitを選択。
  4. ファイルをデプロイする。GitHubからデプロイすることも可能だが、Filesタブの「Add file」→「Upload files」でローカルから直接ファイルをアップロードすることも可能。ディレクトリ構造は以下の通り
      ... ┬ models ┬ config.json : ファインチューニング済みモデル用JSON
          │        │
          │        └ pytorch_model.bin : ファインチューニング済みモデル
          │
          ├ app.py : streamlitによるUI
          │
          ├ requirements.txt : 依存ライブラリを書いたテキストファイル
          │
          └ Zmaker.py : ファインチューニング済みGPT-2による推論
    
  5. 成功すればapp.pyが実行され、Appタブに自分の作ったアプリが表示される。

デプロイ後のアプリの画面

デプロイが終わると、少し経ってから以下のような画面が出てくる。
テキストフォームに文章を打ち込み、Generateボタンを押すと続きをイナババ怪文書風に生成してくれる。
ちなみに画像は「ズッポシ村の代表」と入力して続きを生成してもらった結果

最後に

hugging faceでのモデル公開がここまで手軽だとは思っていなかった。
ここまで使いやすいにも関わらず、あまり情報や使用例が見つからないのが残念・・・

参考文献

  1. Hugging Face公式

    • 無料枠の仕様などはこちらを参考にした。
  2. 範晃 大下 氏「AIアプリをさくっと作るなら、HuggingFace Spaceがおすすめ」

    • Unet系の画像認識アプリでHugging Face Spacesを利用。
    • UIはGradioで作成している
  3. osn_Lofi氏「【初心者でもできる】HuggingFaceにGradioで作成したアプリをデプロイする方法」

    • こちらもHugging Face Spaces + Gradioでアプリをデプロイしている。

Discussion