目次

目次

Few-shot Prompting によるタイトル分割

アバター画像
山田航大
アバター画像
山田航大
最終更新日2025/11/14 投稿日2023/08/08

はじめに

こんにちは。ソリューション事業部データアーキテクトグループの山田です。
当グループでは、音楽業界に向けたデータソリューションの開発と、レコチョクの各サービスのデータ分析を行っています。
今回は前者の中から、メタデータ整備の自動化に向けたPoCとして、「トラックタイトルの自動分割」を LangChain を用いて Few-shot Prompting で実装した内容を紹介させていただきます。

やりたいこと

# 入力
{
    'ja'='青空 Instrumental'
    'kana'='アオゾラインストゥルメンタル'
    'en'='Aozora Instrumental'
}

# 出力
{
    'title_ja': '青空',
    'title_kana': 'アオゾラ',
    'title_en': 'Aozora',
    'sub_ja': 'Instrumental',
    'sub_kana': 'インストゥルメンタル',
    'sub_en': 'Instrumental',
    'title_subtitle_ja': '青空 Instrumental',
    'title_subtitle_kana': 'アオゾラインストゥルメンタル',
    'title_subtitle_en': 'Aozora Instrumental'
}

端的に言うと、トラックタイトルをタイトルとバージョン情報に分割したいということです。 DDEX規格という音楽業界標準のメタ形式にすることで、様々な音楽配信サービスに配信可能になるという利点があります。気になる方はこちらの記事を読んでみてください。 タイトルには「日本語メタ」「カタカナメタ」「海外メタ」が必要になるので、それぞれを適切に変換する必要があります。現状この作業は手作業で行っているのですが、非常に時間がかかるため自動化することで工数を削減したいです。 ルールベースでもある程度はできるのですが、バージョン情報が〜や括弧に囲まれていたりと一貫性がないので、LLMの力を借りてよしなに分割できないかと考えました。

内容としては、LLMに対して 元のタイトル「タイトルとバージョン情報に分割して」という旨のプロンプト、そして 少数のサンプル の3つを与えることで自動分割してもらうというものです。

今回はChatGPTで作成したダミーデータを使って自動分割を行いました。

実装内容

環境情報

  • MacOS Monterey 12.5
  • Python 3.9.13

事前にChatGPTでダミーの分割サンプルを作成します。 今回は50件のサンプルを用意しました。こちらから Few-shot のサンプルが選択されます。

ダミーデータの作成に使用したプロンプトは下記になります。

下記のJSON形式で楽曲タイトルのダミーデータをN件作成してください。

## 制約条件
- 入力のタイトルには必ず (カラオケ) や [Instrumental] 等のバージョン情報を含むようにして、出力の sub_{} で分割されるようにしてください。
- sub_{} にバージョン情報を入れる際は括弧や〜等の記号は外してください。
- 英語版タイトルは基本的に日本語タイトルをローマ字変換した形式にしてください。
- カナタイトルのタイトルとバージョンの間にはスペースを入れないでください。

{
    'ja'='青空 Instrumental'
    'kana'='アオゾラインストゥルメンタル'
    'en'='Aozora Instrumental'
    'title_ja': '青空',
    'title_kana': 'アオゾラ',
    'title_en': 'Aozora',
    'sub_ja': 'Instrumental',
    'sub_kana': 'インストゥルメンタル',
    'sub_en': 'Instrumental',
    'title_subtitle_ja': '青空 Instrumental',
    'title_subtitle_kana': 'アオゾラインストゥルメンタル',
    'title_subtitle_en': 'Aozora Instrumental'
}

次に実装フェーズに入ります。 まずは Few-shot Prompting のテンプレートを作成します。

ExampleSelector は SemanticSimilarityExampleSelector を使用し、事前に準備した分割サンプルの中から、分割したいタイトルと類似度の高いサンプルを3つ取ってくるようにしました。

import pandas as pd
from langchain.prompts import PromptTemplate
from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.embeddings import OpenAIEmbeddings
from langchain.prompts import FewShotPromptTemplate

# 事前に準備した分割サンプル
df_XY = pd.read_csv(
    "split_sample.csv",
    names=["src_title_ja", "src_title_kana", "src_title_en",
           "title_ja", "title_kana", "title_en", 
           "subtitle_ja", "subtitle_kana", "subtitle_en",
           "title_subtitle_ja", "title_subtitle_kana", "title_subtitle_en"]
)

example_prompt = PromptTemplate(
    input_variables=df_XY.columns.to_list(),
    template="\n".join([
        "入力:",
        "ja={src_title_ja}",
        "kana={src_title_kana}",
        "en={src_title_en}",
        "出力:",
        "title_ja={title_ja}",
        "title_kana={title_kana}",
        "title_en={title_en}",
        "sub_ja={subtitle_ja}",
        "sub_kana={subtitle_kana}",
        "sub_en={subtitle_en}",
        "title_subtitle_ja={title_subtitle_ja}",
        "title_subtitle_kana={title_subtitle_kana}",
        "title_subtitle_en={title_subtitle_en}",
    ]),
)

examples = df_XY.to_dict('records')

example_selector = SemanticSimilarityExampleSelector.from_examples(
    examples,
    OpenAIEmbeddings(),
    Chroma,
    k=3,
)

prompt_from_string_examples = FewShotPromptTemplate(
    example_selector=example_selector,
    example_prompt=example_prompt,
    prefix="""
    # 命令書
    タイトルにバージョン違いの情報を持つ場合は分割してください。

    # 制約条件:
    - 日本語バージョンにおける英単語の頭文字を大文字に変換しないでください。
    - カナ表記は数字もカタカナで表記してください。
    - バージョン分割後の()や<>,~,-は除外してください。
    """,
    suffix="\n".join([
        "入力:",
        "ja={src_title_ja}",
        "kana={src_title_kana}",
        "en={src_title_en}",
        "出力:\n",
    ]),
    input_variables=["src_title_ja", "src_title_kana", "src_title_en"],
    example_separator="\n\n",
)

次にプロンプトをLLMに与えて実行し、結果をパースして返す関数を作成します。
LLMはOpenAIの gpt-4 を使用しました。

import os
os.environ["OPENAI_API_KEY"] = "sk-XXXXXXXX" # 有効なAPIキーを設定する

model_name = "gpt-4"

from langchain.llms import OpenAI

llm = OpenAI(model_name=model_name, temperature=0, max_tokens=256)

def question_answering(ja, kana, en):
    question = prompt_from_string_examples.format(
        src_title_ja=ja,
        src_title_kana=kana,
        src_title_en=en,
    )
    result = llm(question)
    try:
        result = result.split("\n")
        result = [r.split("=") for r in result]
        result = {r[0]: r[1] for r in result}
        return result
    except:
        print(result)
        return None

成功例

実際に分割した結果が下記です。

print(question_answering(
    "夏の終わりに (Unplugged Version)",
    "ナツノオワリニアンプラグドバージョン",
    "Natsu no Owari ni (Unplugged Version)"
))

# 出力
{
    'title_ja': '夏の終わりに',
    'title_kana': 'ナツノオワリニ',
    'title_en': 'Natsu no Owari ni',
    'sub_ja': 'Unplugged Version',
    'sub_kana': 'アンプラグドバージョン',
    'sub_en': 'Unplugged Version',
    'title_subtitle_ja': '夏の終わりに (Unplugged Version)',
    'title_subtitle_kana': 'ナツノオワリニアンプラグドバージョン',
    'title_subtitle_en': 'Natsu no Owari ni (Unplugged Version)'
 }

想定通りの分割ができています。

分割に使用されたプロンプト全体は下記です。

print(prompt_from_string_examples.format(
    src_title_ja="夏の終わりに (Unplugged Version)",
    src_title_kana="ナツノオワリニアンプラグドバージョン",
    src_title_en="Natsu no Owari ni (Unplugged Version)"
))

# 出力

    # 命令書
    タイトルにバージョン違いの情報を持つ場合は分割してください。

    # 制約条件:
    - 日本語バージョンにおける英単語の頭文字を大文字に変換しないでください。
    - カナ表記は数字もカタカナで表記してください。
    - バージョン分割後の()や<>,~,-は除外してください。

入力:
ja=夏の終わりのハーモニー (Acoustic Ver.)
kana=ナツノオワリノハーモニーアコースティックバージョン
en=Natsu no Owari no Harmony Acoustic Ver.
出力:
title_ja=夏の終わりのハーモニー
title_kana=ナツノオワリノハーモニー
title_en=Natsu no Owari no Harmony
sub_ja=Acoustic Ver.
sub_kana=アコースティックバージョン
sub_en=Acoustic Ver.
title_subtitle_ja=夏の終わりのハーモニー (Acoustic Ver.)
title_subtitle_kana=ナツノオワリノハーモニーアコースティックバージョン
title_subtitle_en=Natsu no Owari no Harmony Acoustic Ver.

入力:
ja=恋する夏の午後に〜Acoustic Ver.〜
kana=コイスルナツノゴゴニアコースティックバージョン
en=Koisuru Natsu no Gogo ni Acoustic Ver.
出力:
title_ja=恋する夏の午後に
title_kana=コイスルナツノゴゴニ
title_en=Koisuru Natsu no Gogo ni
sub_ja=Acoustic Ver.
sub_kana=アコースティックバージョン
sub_en=Acoustic Ver.
title_subtitle_ja=恋する夏の午後に〜Acoustic Ver.〜
title_subtitle_kana=コイスルナツノゴゴニアコースティックバージョン
title_subtitle_en=Koisuru Natsu no Gogo ni Acoustic Ver.

入力:
ja=夏の日のワルツ (カラオケ)
kana=ナツノヒノワルツカラオケ
en=Natsu no Hi no Waltz Karaoke
出力:
title_ja=夏の日のワルツ
title_kana=ナツノヒノワルツ
title_en=Natsu no Hi no Waltz
sub_ja=カラオケ
sub_kana=カラオケ
sub_en=Karaoke
title_subtitle_ja=夏の日のワルツ (カラオケ)
title_subtitle_kana=ナツノヒノワルツカラオケ
title_subtitle_en=Natsu no Hi no Waltz Karaoke

入力:
ja=夏の終わりに (Unplugged Version)
kana=ナツノオワリニアンプラグドバージョン
en=Natsu no Owari ni (Unplugged Version)
出力:

失敗例

一方でうまく分割できなかったタイトルもありました。

question_answering(
    "星降る夜のプレリュード (光の奇跡) - カラオケバージョン",
    "ホシフルヨルノプレリュード (ヒカリノキセキ) カラオケバージョン",
    "Hoshifuru Yoru no Prelude (Hikari no Kiseki) - Karaoke Version"
)

# 実際の出力
{
    'title_ja': '星降る夜のプレリュード',
    'title_kana': 'ホシフルヨルノプレリュード',
    'title_en': 'Hoshifuru Yoru no Prelude',
    'sub_ja': '光の奇跡 カラオケバージョン',
    'sub_kana': 'ヒカリノキセキ カラオケバージョン',
    'sub_en': 'Hikari no Kiseki Karaoke Version',
    'title_subtitle_ja': '星降る夜のプレリュード (光の奇跡) - カラオケバージョン',
    'title_subtitle_kana': 'ホシフルヨルノプレリュード (ヒカリノキセキ) カラオケバージョン',
    'title_subtitle_en': 'Hoshifuru Yoru no Prelude (Hikari no Kiseki) - Karaoke Version'
}

# 想定出力
{
    'title_ja': '星降る夜のプレリュード (光の奇跡)',
    'title_kana': 'ホシフルヨルノプレリュード (ヒカリノキセキ)',
    'title_en': 'Hoshifuru Yoru no Prelude (Hikari no Kiseki)',
    'sub_ja': 'カラオケバージョン',
    'sub_kana': 'カラオケバージョン',
    'sub_en': 'Karaoke Version',
    'title_subtitle_ja': '星降る夜のプレリュード (光の奇跡) - カラオケバージョン',
    'title_subtitle_kana': 'ホシフルヨルノプレリュード (ヒカリノキセキ) カラオケバージョン',
    'title_subtitle_en': 'Hoshifuru Yoru no Prelude (Hikari no Kiseki) - Karaoke Version'
}

バージョン情報に加えてサブタイトルが含まれてるパターンではうまく分割ができませんでした。 このケースでは「カラオケバージョン」だけを分割してほしかったです。

プロンプト全体は下記です。

    # 命令書
    タイトルにバージョン違いの情報を持つ場合は分割してください。

    # 制約条件:
    - 日本語バージョンにおける英単語の頭文字を大文字に変換しないでください。
    - カナ表記は数字もカタカナで表記してください。
    - バージョン分割後の()や<>,~,-は除外してください。

入力:
ja=星空(カラオケ)
kana=ホシゾラカラオケ
en=Hoshizora Karaoke
出力:
title_ja=星空
title_kana=ホシゾラ
title_en=Hoshizora
sub_ja=カラオケ
sub_kana=カラオケ
sub_en=Karaoke
title_subtitle_ja=星空 (カラオケ)
title_subtitle_kana=ホシゾラカラオケ
title_subtitle_en=Hoshizora Karaoke

入力:
ja=恋する季節 (カラオケ)
kana=コイスルキセツカラオケ
en=Koisuru Kisetsu Karaoke
出力:
title_ja=恋する季節
title_kana=コイスルキセツ
title_en=Koisuru Kisetsu
sub_ja=カラオケ
sub_kana=カラオケ
sub_en=Karaoke
title_subtitle_ja=恋する季節 (カラオケ)
title_subtitle_kana=コイスルキセツカラオケ
title_subtitle_en=Koisuru Kisetsu Karaoke

入力:
ja=星の光道〜Orchestra Ver.〜
kana=ホシノヒカリミチオーケストラバージョン
en=Hoshi no Hikarimichi Orchestra Ver.
出力:
title_ja=星の光道
title_kana=ホシノヒカリミチ
title_en=Hoshi no Hikarimichi
sub_ja=Orchestra Ver.
sub_kana=オーケストラバージョン
sub_en=Orchestra Ver.
title_subtitle_ja=星の光道〜Orchestra Ver.〜
title_subtitle_kana=ホシノヒカリミチオーケストラバージョン
title_subtitle_en=Hoshi no Hikarimichi Orchestra Ver.

入力:
ja=星降る夜のプレリュード (光の奇跡) - カラオケバージョン
kana=ホシフルヨルノプレリュード (ヒカリノキセキ) カラオケバージョン
en=Hoshifuru Yoru no Prelude (Hikari no Kiseki) - Karaoke Version
出力:

Few-shot のサンプルにおいてサブタイトルを含むものはありませんでした。 同じパターンのサンプルがあれば結果は違ったかもしれません。

むすび

今回は Few-shot Prompting によるタイトル分割を実装しました。 100%の精度とはいきませんでしたが、人手の作業の補助にはなるかなという感じでした。

分割サンプルの精査して、多様性を確保したり、ノイズになっているデータを除外すれば、少ないサンプルでもより精度の高い分割ができると思いました。 また、ExampleSelector を自作して、バージョンの表記形式が近いサンプルを取得しやすくすることも有効かと思います。 引き続きチューニングしていきたいと思います。

最後まで読んでいただきありがとうございます。

アバター画像

山田航大

データ分析をする人

目次