- バックエンド
- PdM
- CS職/既存営業
- Other occupations (27)
- Development
- Business
- Other
vLLM+Structured Outputを使ったテキストのラベリング高速化
Photo by Andy Hermawan on Unsplash
こんにちは。ウォンテッドリーでデータサイエンティストをしている角川(@nogawanogawa)です。
この記事では、自然文で書かれたデータに対してローカルLLMを用いてテキストデータのラベリングを行う事例についてご紹介したいと思います。
目次
テキストのラベリング
LLMを用いたテキスト分類
LLMを使うメリット
ローカルLLMを使うメリット
vLLMを用いた推論高速化
Structured Outputを用いたラベル構造の整理
実験
モデル・データセット
ローカルLLMを使用してStructured Outputを利用する場合
vLLMを使用してStructured Outputを利用する場合
出力と実行速度
まとめ
テキストのラベリング
我々の身の回りには数多くのテキストデータで溢れていますが、自然文で書かれたデータは必ずしも扱いやすくはありません。実際、われわれは文章を読む際に無意識のうちに内容を構造化したりしていますし、場合によってはテキストに書かれた情報をもとに手動でラベルをつけて複数のテキストを比較・分類することもあると思います。
このように、テキストにもとづいて何らか意味的な属性を付与することで、使いやすく整理することがあります。データの集計・分析の観点でも多様な切り口でデータを扱うことができるようになるため、使用できるラベルは多いに越したことはありません。
一方で、自然文に対するラベル付けは人間が手作業で行うには非常に負荷の高い作業です。自動化しようにも、表記ゆれや同義語などもあるため機械的にラベル付けすることは難しいようなタスクになってしまいます。
LLMを用いたテキスト分類
LLMを使うメリット
こうした自然文を扱う処理は従来は多くのコストがかかるため対応のハードルが高いものでした。最近ではChatGPTやClaude、Geminiなどの登場により提供されているLLMのAPIを活用することで上記のようなテキストのラベリングを比較的低コストで行うことが可能になっています。
生成AIを利用する場合、ラベル付けの基準をプロンプトとして与えるだけでラベル付けの処理をコントロールできます。ラベルの種類や基準が変更になった際にもプロンプトを変更するだけでコントロールできるため、ラベリングに関する変更に柔軟に対応可能です。
ローカルLLMを使うメリット
先に挙げたAPIを利用する以外にも、モデルのweightが公開されているローカルLLMを活用しても同様のことが可能です。ローカルLLMを活用するメリットとして、
- データを外部に出すことなく生成AIを活用できる
- 大量のテキスト・画像を処理する場合にもRate Limitの懸念がない
- 独自データセットでのFine tuningの自由度が高い
などが挙げられ、実際にローカルLLMを活用されている方も多くいらっしゃると思います。
クラウドAPIとローカルLLMは一概にどちらが良いとは言えませんが、今回はローカルLLMを使用してラベリングを行うことで話を進めます。
vLLMを用いた推論高速化
LLMを使うことでテキストに基づいてラベリングを行うことができるとはいえ、ラベリング対象となるテキストの数が多いとその分多くの時間がかかってしまう懸念があり、高速化が求められます。
ローカルLLMの推論はvLLMやSGLangといったライブラリを使うことで高速化できることが知られています。vLLMはローカルLLMを使う手順に加えて呼び出しを少し変更するだけで利用することができます。
from transformers import AutoTokenizer
from vllm import LLM, SamplingParams
# Initialize the tokenizer
tokenizer = AutoTokenizer.from_pretrained("llm-jp/llm-jp-3.1-13b")
# Configurae the sampling parameters (for thinking mode)
sampling_params = SamplingParams(temperature=0.6, top_p=0.95, top_k=20, max_tokens=4096)
# Initialize the vLLM engine
llm = LLM(model="llm-jp/llm-jp-3.1-13b")
# Prepare the input to the model
prompt = "大規模言語モデルについて簡単に説明してください。"
messages = [
{"role": "user", "content": prompt}
]
tokenizer.chat_template = (
"{% for message in messages %}"
"{{ message['role'].capitalize() }}: {{ message['content'] }}\n"
"{% endfor %}"
"Assistant:"
)
text = tokenizer.apply_chat_template(messages, tokenize=False)
# Generate outputs
outputs = llm.generate([text], sampling_params)
# Print the outputs.
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")ローカルLLMでは上記のようなツールを利用することで、比較的簡単に推論を高速化することができます。
Structured Outputを用いたラベル構造の整理
ラベリングする際には、マルチラベルや階層化されたラベルが必要になることもあり、用途に応じてラベルの構造を事前定義したうえでそれに準拠した形でLLMに出力させたくなるかもしれません。こういったラベルの構造もLLMへ伝えることで構造を出力することができます。
ローカルLLMでStructured Outputを利用するにはoutlinesなどのライブラリを利用すると簡単に実装することができます。下記のようにするとPydanticのSchemaを定義することでラベルの構造を定義することが可能になっています。
outlinesでは下記のようにすることでStructured Outputを利用することができます。
import outlines
from transformers import AutoTokenizer, AutoModelForCausalLM
from pydantic import BaseModel
from enum import Enum
class Rating(Enum):
poor = 1
fair = 2
good = 3
excellent = 4
class ProductReview(BaseModel):
rating: Rating
pros: list[str]
cons: list[str]
summary: str
MODEL_NAME = "Qwen/Qwen2.5-0.5B-Instruct"
model = outlines.from_transformers(
AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto"),
AutoTokenizer.from_pretrained(MODEL_NAME)
)
review = model(
"Review: The XPS 13 has great battery life and a stunning display, but it runs hot and the webcam is poor quality.",
ProductReview,
max_new_tokens=200,
)
review = ProductReview.model_validate_json(review)
print(f"Rating: {review.rating.name}") # "Rating: good"
print(f"Pros: {review.pros}") # "Pros: ['great battery life', 'stunning display']"
print(f"Summary: {review.summary}") # "Summary: Good laptop with great display but thermal issues"実験
実際に簡単に実験してみたいと思います。
モデル・データセット
モデルにはQwen/Qwen2.5-7B-Instructを使用します。本モデルは「Apache license 2.0」ライセンスのもとに公開されています。
データセットにはLLM-jpから公開されているLLM-jp Toxicity Dataset v2を使用します。本データセットは「CC BY 4.0」ライセンスのもと公開されています。こちらのデータセットには、与えられた日本語のテキストがToxic(有害)かどうかに関するラベルが与えられているので、今回はそれをローカルLLMを使ってラベリングするタスクを行います。
ローカルLLMを使用してStructured Outputを利用する場合
今回はoutlinesを活用し、下記のように階層構造を持つSchemaを使用します。
# schemaの定義
class ToxicityLabel(str, Enum):
toxic = "toxic"
nontoxic = "nontoxic"
has_toxic_expression = "has_toxic_expression"
class ToxicityAttributes(BaseModel):
obscene: bool = Field(False, description="Explicit sexual or obscene content.")
discriminatory: bool = Field(False, description="Discriminatory expressions or insults.")
violent: bool = Field(False, description="Violent expressions or threats.")
illegal: bool = Field(False, description="Encourages illegal or unethical behavior.")
personal: bool = Field(False, description="Exposure of personal information or privacy.")
corporate: bool = Field(False, description="Confidential corporate information.")
others: bool = Field(False, description="Other forms of toxicity.")
class ToxicityData(BaseModel):
label: ToxicityLabel = Field(
...,
description=(
"toxic: text is toxic, "
"nontoxic: no toxicity, "
"has_toxic_expression: contains toxic expressions but not overall toxic."
),
)
attributes: ToxicityAttributes
推論については下記のように行うことができます。
MODEL_NAME = "Qwen/Qwen2.5-7B-Instruct"
model = outlines.from_transformers(
AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto"),
AutoTokenizer.from_pretrained(MODEL_NAME)
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
prompts = []
system_prompt = """\
あなたは優秀なアシスタントです。入力されるテキストが有害かどうかを判定してください。\
"""
for user_prompt in df["text"]:
prompt = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
prompts.append(prompt)
prompts = tokenizer.apply_chat_template(prompts, add_generation_prompt=True, tokenize=False, max_length=4096)
result_list = [
model(
prompt,
ToxicityData,
max_new_tokens=200,
) for prompt in tqdm(prompts)
]コード全体はこちらに公開しておりますので、ご興味ありましたらご参照ください。
vLLMを使用してStructured Outputを利用する場合
vLLMを利用する際も基本的には同様でSchemaを定義します。その後の推論が通常と異なっていますが、下記のように実行可能になっています。
MODEL_NAME = "Qwen/Qwen2.5-7B-Instruct"
# Initialize the tokenizer
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
# Initialize the vLLM engine
llm = LLM(model=MODEL_NAME)
json_schema = ToxicityData.model_json_schema()
structured_outputs_params = GuidedDecodingParams(json=json_schema)
sampling_params = SamplingParams(structured_outputs=structured_outputs_params, temperature=0.0, max_tokens=200)
result_list = []
prompts = []
system_prompt = """\
あなたは優秀なアシスタントです。入力されるテキストが有害かどうかを判定してください。\
"""
max_token_length = 4096
for user_prompt in df["text"]:
prompt = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
prompts.append(prompt)
prompts = tokenizer.apply_chat_template(prompts, add_generation_prompt=True, tokenize=False, max_length=max_token_length)
result_list = [
llm.generate(
prompts=prompt,
sampling_params=sampling_params,
)[0].outputs[0].text for prompt in tqdm(prompts[:30])
]コード全体はこちらに公開していますのでご興味ありましたらご参照ください。
出力と実行速度
Structured Outputを見ると、下記のように事前定義した構造でラベリングができていました。
label=<ToxicityLabel.nontoxic: 'nontoxic'> attributes=ToxicityAttributes(obscene=False, discriminatory=False, violent=False, illegal=False, personal=False, corporate=False, others=False)30件のテキストを推論する実行時間を測定したところ、outlinesを利用した場合推論だけの実行時間は下記のようになりました。
CPU times: user 2min 12s, sys: 7.79 s, total: 2min 19s
Wall time: 2min 19s一方vLLMを利用した場合下記のようになっていました。
CPU times: user 1.09 s, sys: 104 ms, total: 1.19 s
Wall time: 21.2 sWall timeをもとに比較すると今回の実験では約6.5倍高速に推論することができており、Structured Outputを行いながら高速に推論することができました。
まとめ
今回はローカルLLMを活用したテキストのラベリング高速化事例についてご紹介しました。何らかの理由でクラウドAPIが利用できないケースもあるかと思いますが、ローカルLLMを利用してStructured Outputの高速化を行う参考になれば幸いです。
ウォンテッドリーでは、ユーザーにとってより良い推薦を届けるために日々開発を行っています。ユーザーファーストの推薦システムを作ることに興味があるという方は、下の募集の「話を聞きに行きたい」ボタンから気軽に話を聞きに来ていただけるとうれしいです!