spacy-llmで色々なNLPタスクをzero-shotで解いてみる

こんにちは
AIチームの戸田です

固有表現抽出(NER)や品詞タグ付けなどのNLPタスクを行うためのPythonライブラリにspaCyがあります。シンプルなAPIで拡張性も高く、AI ShiftでもプロダクトのNLPロジック部分やデータ分析など多くの場面で利用しています。

そんな非常にお世話になっているspaCyですが、処理パイプラインにLLMを統合できるspacy-llmがリリースされました。今回の記事ではそのspacy-llmを使って極性分類やNERなどのNLPタスクをzero-shotで解いてみようと思います。

事前準備

以下のコマンドでインストールすることができます。

pip install spacy-llm

日本語を処理するためのspacy[ja]がインストールされていない場合はここで一緒にインストールしておいてください。

本記事ではLLMとしてGPT-3.5 turboを利用しますので、環境変数OPENAI_API_KEYを設定しておきます。

設定ファイルの準備

spaCyで処理パイプラインを設定する際は、Pythonコードで設定する方法と設定ファイル(*.cfg)で設定する2パターンがありますが、今回は後者の設定ファイルで設定する方法で行います。

以下のような内容のファイルを準備しておきます。

[nlp]
lang = "ja"
pipeline = ["llm"]

[components]

[components.llm]
factory = "llm"

[components.llm.task]
# ここにタスク設定を記述する

[components.llm.model]
@llm_models = "spacy.GPT-3-5.v1"
config = {"temperature": 0.0}

前述の通りLLMにはOpenAIのGPT-3.5を使用します。このほかにspaCyではAnthropicのClaudeCohereのCommandをサポートしています。加えてMetaのLlama2などHuggingface Hubで公開している、いわゆるOpenLLMも使用することができます。

様々なNLPタスクを解いてみる

極性分類

極性分類を行う際は設定ファイルのcomponents.llm.taskに以下のような設定をしてください。

[components.llm.task]
@llm_tasks = "spacy.Sentiment.v1"

これをsentiment.cfgとして保存し、これを読み込むことで極性分類のパイプラインを作ります。

from spacy_llm.util import assemble

nlp = assemble("sentiment.cfg")

texts = [
    "ここのラーメン美味しいね!",  # Positiveを期待
    "急に雨降ってきたけど傘忘れちゃった",  # Negativeを期待
]

for text in texts:
    doc = nlp(text)
    print(f"{text}: score = {doc._.sentiment}")

# 出力
# ここのラーメン美味しいね!: score = 1.0
# 急に雨降ってきたけど傘忘れちゃった: score = 0.2

scoreの範囲は0〜1で、高いほどPositiveといえるので、結果は期待通りとみえます。

文章分類

文章分類を行う際は設定ファイルのcomponents.llm.taskに以下のような設定をしてください。labelsには分類ラベルをリストで指定します。

[components.llm.task]
@llm_tasks = "spacy.TextCat.v3"
labels = ["経済", "エンタメ", "スポーツ", "国際"]

これをclassify.cfgとして保存し、これを読み込むことで文章分類のパイプラインを作ります。
テストデータはこの記事を書いた時のYahooニュースのタイトルから作成しました。

nlp = assemble("classify.cfg")

texts = [
    "日銀 マイナス金利解除の公算大",  # 経済を期待
    "磯部磯兵衛物語 実写ドラマ化",  # エンタメを期待
    "大谷 栗山氏とWBC以来の再会",  # スポーツを期待
    "TikTok禁止法案 米下院で可決",  # 国際を期待
]

for text in texts:
    doc = nlp(text)

    # 結果は.catsにdefaultdict形式で{'エンタメ': 0.0, 'スポーツ': 0.0, '国際': 1.0, '経済': 0.0}のような形式で出力されているので、valueが最大のkeyを取得する
    pred = max(doc.cats, key=doc.cats.get)
    print(f"{text}: {pred}")

# 出力
# 日銀 マイナス金利解除の公算大: 経済
# 磯部磯兵衛物語 実写ドラマ化: エンタメ
# 大谷 栗山氏とWBC以来の再会: スポーツ
# TikTok禁止法案 米下院で可決: 国際

期待通りにニュース記事のタイトルを分類できました。

固有表現抽出(NER)

NERを行う際は設定ファイルのcomponents.llm.taskに以下のような設定をしてください。labelsには抽出する固有表現のラベルをリストで設定します。

[components.llm.task]
@llm_tasks = "spacy.NER.v3"
labels = ["組織名", "日付"]

これをner.cfgとして保存し、これを読み込むことでNERのパイプラインを作ります。
テストデータにはAI ShiftのMessageの最初の一文を使います。

nlp = assemble("ner.cfg")

text = "株式会社AI Shiftは、サイバーエージェントの子会社として2019年8月30日に設立いたしました。 AI Shiftという社名には、AIにシフトすることで人ならではの仕事に向き合える社会を作るという想いが込められています。"
doc = nlp(text)

spacy.displacy.render(doc, style="ent", jupyter=True)  # jupyter notebookの場合
print([(ent.text, ent.label_) for ent in doc.ents])  # CLIの場合
# [("株式会社AI Shift", "組織名"), ("サイバーエージェント", "組織名"), ("2019年8月30日", "日付"), ("AI Shift", "組織名")]

ここではjupyter notebookでの結果を示します。

AI Shiftやサイバーエージェントなど、適切に固有表現を認識できていることがわかります。

翻訳

翻訳を行う際は設定ファイルのcomponents.llm.taskに以下のような設定をしてください。target_langに翻訳対象の言語を指定してください。

[components.llm.task]
@llm_tasks = "spacy.Translation.v1"
target_lang = "English"

これをtranslation.cfgとして保存し、これを読み込むことで翻訳のパイプラインを作ります。

nlp = assemble("translation.cfg")

texts = [
    "昨日初めてキャビアを食べました",
    "大きな犬が川沿いの道を歩いている",
]

for text in texts:
    doc = nlp(text)
    print("日:", text)
    print("英:", doc._.translation)
    print()

# 出力
# 日: 昨日初めてキャビアを食べました
# 英: I ate caviar for the first time yesterday
# 
# 日: 大きな犬が川沿いの道を歩いている
# 英: A large dog is walking along the riverbank.

一般的な文章ですが、うまく翻訳できているのではないでしょうか?

要約

要約を行う際は設定ファイルのcomponents.llm.taskに以下のような設定をしてください。

[components.llm.task]
@llm_tasks = "spacy.Summarization.v1"

これをsummary.cfgとして保存し、これを読み込むことで要約のパイプラインを作ります。
テストデータには以前書いたブログの導入の文を利用します。

nlp = assemble("summary.cfg")

text = """近年注目を集めている大規模言語モデル、ChatGPTやClaudeなどのAPIから利用できるサービスはもちろんですが、最近はMetaのLlama2などのオープンモデル、つまりLocalにダウンロードして使うことのできるモデルの開発も活発です。

オープンモデルを試すには強力なGPU環境が必要です。例えばLlama2 70Bモデルを動かすとなると、GPUのメモリは80GB以上は必要になってきます。しかし現在GPUは不足していると言われており、ハードウェアはもちろんのこと、クラウドサービスでもなかなかA100のような十分なスペックを持つGPUを確保することが困難です。量子化などのテクニックはありますが、70Bクラスになるとこれにも限界があります。

私が以前参加したKaggleのコンペティション、LLM Science Examではsimjeg氏によって、LLMの層ごとに推論処理を行うことで、メモリ16GBのT4上でもPlatypus2-70B-instructの推論を行う手法の提案がありました。
airllmはこのアプローチを参考にし、シンプルなインターフェースで使えるようにしてくれたライブラリです。理論上GPUのメモリはなんと4GBでよいと言われています。

今回はこのairllmを使ってT4上でmeta-llama/Llama-2-70b-chat-hfの推論を試してみたいと思います。"""

doc = nlp(text)
print(doc._.summary)

# 出力
# 大規模言語モデルの注目が高まっており、オープンモデルの開発も活発化している。強力なGPU環境が必要であり、LLM Science Examではメモリ16GBのT4上で推論を行う手法が提案された。airllmはこのアプローチを参考にしたライブラリで、GPUのメモリは4GBで十分とされている。

LLMを使った要約はhallucinationが起こる可能性がありますが、今回は過不足なく要約できているように見えます。

おわりに

本記事ではspacy-llmを使って様々なNLPタスクをzero-shotで解いてみました。

以前LLMをZero Shotテキスト分類器として利用する方法を紹介しましたが、プロンプトの調整が面倒だったり、NERのような出力が可変なタスクへの対応などができなかったりと、実タスクで応用はまだ難しそうでしたが、spacy-llmでは非常に簡単に実装することができ、予測結果も非常に正確でした。

今回試したタスクのほかにも独自にカスタムタスクを定義できたり、パラメータ調整(例えば要約で出力語彙数を制限する)やfew-shot learningなども導入することができますので、興味のある方はドキュメントをご参照いただければと思います。

最後までお読みいただきありがとうございました!

PICK UP

TAG