言葉で定義した問題を解いてくれるAIを作ろう：OpenPromptを試してみた

# はじめに

昨今、AIの利用が様々な場面で進んでおり、企業の実業務での利用も珍しくありません。
しかし、AIを作るためには様々な知識が必要になるため、作れる人はまだまだ限られています。
理論を学んで、TensorflowやPytorch等のFWの使い方を覚えて、コードを書いて、試行錯誤して・・・とても大変ですね。
創作のように言葉で問題を与えたら解いてくれるAIが実現できたら、誰もが気軽にAIを作成・利用できるようになるでしょう。
そうすれば、人々は解くべき問題を考えることにより集中できるようになるのではないでしょうか。
そんな未来を予感させるAIモデルがすでに2020年に登場しています。
そう、GPT-3です。

# GPT-3って？
GPT-3は、[OpenAI](https://openai.com/)が開発した超大規模言語モデルです。
詳細はググってもらった方が早いので説明は省略しますが、雑にいうと超大規模なコーパスを材料に物理（計算資源）で殴って作ったモデルです（一度訓練するだけで数億円かかるともいわれています）。
このGPT-3により生成される文章はとても自然で、それゆえ悪用が懸念されるためか、モデル自体は公開されていません（公開されても動かすマシンが無いという説もある）。
ですが、[OpenAIはAPIを公開しており](https://openai.com/api/)、これを介して利用することができます。
このAPIを用いて、[多くの人が様々なデモを作って公開しています。](https://gpt3demo.com/)
[言葉で定義した仕様をもとにSQLクエリを生成したり](https://gpt3demo.com/apps/sourceai-dev)、[言葉で説明した顔を生成したり](https://gpt3demo.com/apps/gpt-3-x-gans)等々、見ているだけでもワクワクするデモがたくさんあります。

これらの例を見ていてわかるように、GPT-3を利用する際には「プロンプト」という文章を渡して、その続きを予測させるという形で問題を解くことになります。
このような手法は、Prompt Learning もしくは Prompt Programming と呼ばれています（詳しくは[こちら](https://www.hitachi-hri.com/keyword/k135.html)や[こちら](https://github.com/thunlp/PromptPapers)）。
実用上はまだまだ多くの課題があるものの、言葉で指示を与えると解いてくれるAIの実現可能性を感じていただけたのではないかと思います。

# 使ってみたいけど・・・

そろそろ、皆さんも早く使ってみたいと感じてきた頃かと思います。
[OpenAIのAPI](https://openai.com/api/)は最初の3ヶ月利用可能な18ドル分のクレジットがもらえるそうですので、これが一番手軽に試せる手段でしょう。

しかし、以下のような理由からこのAPIを使いたくないということがあるのでは無いでしょうか？

* 営業秘密や機微情報等を扱いたいので、外部サービスの利用が難しい
* 自分たちの業務ドメインに特化した言語モデルを使ってPrompt Learningしたい
* ブラックボックスなままだと気持ち悪いので、仕組みを理解しながら使いたい

そんな方たちにおすすめなのが、今回ご紹介する[OpenPrompt](https://github.com/thunlp/OpenPrompt)です。

# OpenPromptって？

[OpenPrompt](https://github.com/thunlp/OpenPrompt)は、中国の清華大学の研究室が作っているPrompt LearningのためのOSSなフレームワークです。
事前学習済みの言語モデルを用意することで、自分たちの環境上でPrompt Learningを行うことができます。
いまのところ[Hugging Face/Transformers](https://huggingface.co/transformers/)で公開されている言語モデルであれば簡単に扱えるようになっているため、手待ちのモデルも[Transformers](https://github.com/huggingface/transformers)に対応させれば比較的容易に利用できるはずです。

前置きが長くなりましたが、早速動かしてみましょう。

# 動かしてみよう

公式のイントロダクションやチュートリアルはスクリプト形式になっているため、Jupyter NotebookやGoogle Colab上で動かせるようにしました。
[こちらのリポジトリ](https://github.com/wwwcojp/openprompt-notebooks-japanese)からご利用になれます。

## 公式のイントロダクションを日本語対応してみた

どのように使うのか概要を理解するため、まずは[公式のイントロダクション](https://github.com/thunlp/OpenPrompt#use-openprompt)を動かすことにしました。
とはいえ、単に動かすだけだとつまらないので、以下を目標にやってみました。

+ Google Colabで動作させる
+ 日本語モデルで動作させる

詳細な手順は[ノートブック](https://github.com/wwwcojp/openprompt-notebooks-japanese/blob/main/Introduction-japanese.ipynb)をご覧いただくとして、いくつか重要な手順を抜き出して説明していきます。

ノートブックをColabで開く際は以下をクリックしてください。
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](http://colab.research.google.com/github/wwwcojp/openprompt-notebooks-japanese/blob/main/Introduction-japanese.ipynb)

### 日本語BERTモデルの利用設定

現状、OpenPromptでは言語モデルの種類（BERT,T5,GPT,...）ごとに利用するトークナイザがハードコーディングされています。
今回利用したい日本語BERTモデル（東北大が作成した[cl-tohoku/bert-large-japanese](https://huggingface.co/cl-tohoku/bert-large-japanese)）ではそれらと異なるトークナイザを利用するため、その設定をやや無理矢理入れ込みます。
（OpenPromptのコードを変更してしまうのが確実ですが、直接要素を変更してしまっても大丈夫そうだったので雑に変更します）

```python
import openprompt.plms as plms
from openprompt.plms.mlm import MLMTokenizerWrapper
from transformers import BertConfig, BertForMaskedLM, BertJapaneseTokenizer

plms._MODEL_CLASSES['bert-ja'] = plms.ModelClass(**{
    'config': BertConfig,
    'tokenizer': BertJapaneseTokenizer,
    'model':BertForMaskedLM,
    'wrapper': MLMTokenizerWrapper,
})
```

### タスク定義

今回は公式イントロダクションにならって、文のポジティブ・ネガティブ分類をゼロショット（学習なし）で試してみます。
以下のように、分類クラスの定義と判定対象のデータ作成を行います。

```python
from openprompt.data_utils import InputExample
classes = [ # There are two classes in Sentiment Analysis, one for negative and one for positive
    "negative",
    "positive"
]
dataset = [ # For simplicity, there's only two examples
    # text_a is the input text of the data, some other datasets may have multiple input sentences in one example.
    InputExample(
        guid = 0,
        text_a = "私はそのホラー映画が面白くて三度の飯より好きです。", #positive
    ),
    InputExample(
        guid = 1,
        text_a = "このサメ映画はとてもつまらない駄作だと思います。", #negative
    ),
    InputExample(
        guid = 2,
        text_a = "その映画は私の中でベスト3に入る良い作品だった。", #positive
    ),
    InputExample(
        guid = 3,
        text_a = "この感動巨編なラブロマンスはとても退屈で眠ってしまった。", #negative
    ),
]
```

### テンプレート・バーバライザ定義

テンプレートとバーバライザは、Prompt Learningにおいてとても重要です。

```python
from openprompt.prompts import ManualTemplate
# template_text = '{"placeholder":"text_a"}: この意見は{"mask"}という評価です。'
# template_text = '{"placeholder":"text_a"}この意見は{"mask"}な評価です。'
template_text = '{"placeholder":"text_a"}: この意見は{"mask"}な評価です。'

promptTemplate = ManualTemplate(
    text = template_text,
    tokenizer = tokenizer,
)

from openprompt.prompts import ManualVerbalizer
promptVerbalizer = ManualVerbalizer(
    classes = classes,
    label_words = {
        "negative": ["ネガティブ", "否定的", "つまらない", "駄作"],
        "positive": ["ポジティブ", "肯定的", "面白い", "名作"],
    },
    tokenizer = tokenizer,
)
```

テンプレートは、要するにプロンプトのひな形です。
テンプレートの`{placeholder:"text_a"}`にタスク定義で作成したデータがセットされ、`{"mask"}`の部分に出現する単語の生起確率を予測します。

その単語の生起確率を元に、どのクラスに分類すべきかを決めるのがバーバライザです。
今回用いたManualVerbalizerは、各クラスに分類すべき単語を手動で設定するバーバライザです。
例えば、「ポジティブ」「肯定的」等の単語よりも「ネガティブ」「否定的」等の単語の方が`{"mask"}`の箇所での生起確率が高い場合、negativeクラスに分類されます。
（ゼロショットの場合はこのように明示的に単語を指定する必要がありますが、フューショットラーニング等の場合はクラスに該当する単語自体を自動的に予測するバーバライザも使用可能です）

テンプレートをいかにうまく作るかがPrompt Learningを成功させる秘訣のようで、上記でコメントアウトしているような微妙な違いが予測に影響を与えていました。
私もまだコツをつかめておらず、現状ではこのテンプレートを作るところに職人芸が要求される気がしますね。
ちなみにですが、クラス名はバーバライザに影響を与えないようで、例えばnegativeとpositiveを入れ替えても生起確率は変化しませんでした。

### 推論

以下のコードで4つの文についてクラス分類してみます。

```python
import torch
promptModel.eval()
with torch.no_grad():
    for batch in data_loader:
        logits = promptModel(batch)
        print(logits)
        preds = torch.argmax(logits, dim = -1)
        print(classes[preds])
```

```
# 私はそのホラー映画が面白くて三度の飯より好きです。
tensor([[-4.1447, -3.2642]])
positive

# このサメ映画はとてもつまらない駄作だと思います。
tensor([[-3.3193, -3.5390]])
negative

# その映画は私の中でベスト3に入る良い作品だった。
tensor([[-3.7302, -3.4930]])
positive

# この感動巨編なラブロマンスはとても退屈で眠ってしまった。
tensor([[-3.2176, -3.5782]])
negative
```

上記のように、一応正しい分類ができているようです。
3つめと4つめのようにバーバライザでlabel_wordsとして定義した単語が登場しない文についても正しく予測できているのは特筆すべき点でしょう。
ただ、クラスごとのlogitsを見ていただければわかるように割と僅差なので、まだまだ工夫すべきところはありそうです。

## livedoor ニュースコーパスの分類問題を解いてみた

続けて、日本語NLPにおける定番である[livedoor ニュースコーパス](https://www.rondhuit.com/download.html)を使った分類問題を、OpenPromptを使ってフューショットラーニングで解いてみました。
上記分類問題用のデータ生成処理は、[sonoisaさん](https://qiita.com/sonoisa)の[こちらのノートブック](https://github.com/sonoisa/t5-japanese/blob/main/t5_japanese_classification.ipynb)を参考にしています（いつもお世話になっております！）。

詳細な手順は[ノートブック](https://github.com/wwwcojp/openprompt-notebooks-japanese/blob/main/Tutorial-Classification-japanese.ipynb)をご覧いただくとして、いくつか重要な手順を抜き出して説明していきます。
ノートブックをColabで開く際は以下をクリックしてください。
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](http://colab.research.google.com/github/wwwcojp/openprompt-notebooks-japanese/blob/main/Tutorial-Classification-japanese.ipynb)

### フューショットラーニング用サンプラー

trainデータのうち、各クラスごとに使用するデータ数を指定することができます。
全量で学習したい場合は、num_examples_per_labelにNoneを設定してください。

```python
from openprompt.data_utils.data_sampler import FewShotSampler

if num_examples_per_label is not None:
    sampler  = FewShotSampler(num_examples_per_label=num_examples_per_label)
    dataset['train'] = sampler(dataset['train'], seed=seed)
```

### テンプレート・バーバライザ

```python
from openprompt.prompts import ManualTemplate

mytemplate = ManualTemplate(tokenizer=tokenizer, text='{"placeholder":"text_a"} {"placeholder":"text_b"} この記事のジャンルは{"mask"}。')

from openprompt.prompts import SoftVerbalizer
import torch

myverbalizer = SoftVerbalizer(tokenizer=tokenizer, plm=plm, classes=target_genres, num_classes=9, multi_token_handler=multi_token_handler)
```

テンプレートでは、記事のタイトルを`{"placeholder":"text_a"}`に、本文を`{"placeholder":"text_b"}`に当てはめています。
（mask部分の文言は、もう少し工夫の余地がありそう）

バーバライザは、イントロダクションと異なりSoftVerbalizerを利用しました。
これを用いることで、手動でクラスに対応する単語を指定しなくても自動的に推定してくれます。
[詳しくはこちらをご覧ください。](https://thunlp.github.io/OpenPrompt/modules/verbalizer.html#soft-verbalizer)

### テストデータによる性能評価

今回は、イントロダクションと同じ日本語BERTモデル（[cl-tohoku/bert-large-japanese](https://huggingface.co/cl-tohoku/bert-large-japanese)）を利用し、フューショットのサンプル数を何パターンか変えて評価してみました。
以下の表のAccuracyは1000ステップ学習させた際の最高性能モデルのものです（時間が無くあまり回せませんでした・・・）。

|サンプル数|Accuracy|
|---|---|
|8|0.73|
|16|0.79|
|all|0.91|

今回はSoftVerbalizerを使いましたが、手動で単語を指定しなくてもそれなりの精度で分類できているようです。
サンプル数少なめでもそれなりの精度が出ている気がしますが、Prompt Learningではなく普通にファインチューニングした時の結果とも比較したいところですね。。。
ちなみに、記事を書きながら全量データで数千ステップ学習させたところvalidatinデータに対するAccuracyが0.95位まで上がってきたので、限界は上記よりもう少し上っぽいです。
適当なプロンプトでも、学習データをそれなりに用意すれば精度は高くなるようですね（Prompt Learningのうまみが減りますが・・・）。

# まとめ

以上のように、OpenPromptを用いることで事前学習済み言語モデルを利用して手軽にPrompt Learningができることができることがわかりました。
私がOpenPromptの仕組みを理解しきれていなかったり、OpenPromptが絶賛開発中であったりするため、まだまだ改善・工夫の余地がたくさんありそうです。
個人的にはPrompt Learningがより利用しやすくなれば、誰もが自分たちの課題解決のためのAIを容易に作れるようになり「AIの民主化」につながっていくのではと感じています。
そうなれば、人間に求められるスキルは「如何に重要な課題を発見し、それをAIにわかりやすい言葉で表現できるか」になっていくのかもしれません。
そんな未来が来ることを夢想しつつ、その実現に必要となりそうな技術をこれからも探索し続けていきたいと思います。