sbintuitions/modernbert-ja-130mに追加学習をして文章からVTuberかどうかを判断する教師あり学習による2値分類モデルを作成する

初めに

1週間ほど前に sbintuitions/modernbert-ja-130mが公開されたので遊んでいきます。

今回は bertモデルなのでテキストクラスタリングをやってみます.文章を入れたら、その文章がVTuberっぽいのかどうかを判定するモデルを作ってみます。 (25/02/17時点では、データセットyoutube apiの制限上 あまり集められていないので精度は悪いです)

この記事の学習Colobおよび推論のリポジトリは以下で公開しています

学習Colob

colab.research.google.com

推論

github.com

データセット

huggingface.co

学習済みモデル

huggingface.co

開発環境

Google Colobの準備

以下のAPIキーを使うため シークレットキーを登録します

  • huggingface
  • youtube api(データセットを用意されているものを使う場合は必要ない)
  • wandb(使用しない場合は不要)

データセットの作成

まずは学習をするためのyoutubeからVTuberと非VTuberのチャンネルの情報を取得します。 (youtubeAPIの制限により多くは取得できないです)

# 必要なライブラリのインストール(初回のみ)
!pip install -U google-api-python-client huggingface_hub

# APIキーはGoogle Colabのシークレット値から取得
import os
from google.colab import userdata
API_KEY = userdata.get('YOUTUBE_API_KEY')
if API_KEY is None:
    raise ValueError("YOUTUBE_API_KEYが環境変数に設定されていません。ColabのシークレットにAPIキーを登録してください。")

from googleapiclient.discovery import build

# YouTube Data APIのクライアントを作成
youtube = build('youtube', 'v3', developerKey=API_KEY)

def fetch_channels(query, max_results=50, page_limit=3):
    """指定した検索クエリでチャンネル情報を取得する関数"""
    channels = []
    next_page_token = None
    for _ in range(page_limit):
        request = youtube.search().list(
            q=query,
            type="channel",
            part="id,snippet",
            maxResults=max_results,
            pageToken=next_page_token
        )
        response = request.execute()
        for item in response.get("items", []):
            channel_id = item["id"]["channelId"]
            title = item["snippet"]["title"]
            description = item["snippet"]["description"]
            channels.append({"channel_id": channel_id, "title": title, "description": description})
        next_page_token = response.get("nextPageToken")
        if not next_page_token:
            break
    return channels

# VTuber候補のチャンネル情報を取得(例:"VTuber"で検索)
vtuber_channels = fetch_channels(query="VTuber", max_results=500, page_limit=10)
print("VTuber候補のチャンネル数:", len(vtuber_channels))

# 非VTuber候補のチャンネル情報を取得(例:"料理"で検索)
non_vtuber_channels = fetch_channels(query="料理", max_results=10, page_limit=10)
print("非VTuber候補のチャンネル数:", len(non_vtuber_channels))

データをhuggingfaceにアップロード

作成したデータを整理して,huggingfaceにアップロードします

def add_label_and_text(item, label):
    # "title"と"description"を結合して"text"を作成し、ラベルを追加
    item["text"] = item["title"] + " " + item["description"]
    item["label"] = label
    return item

# VTuber候補にはラベル1を付与
vtuber_channels_labeled = [add_label_and_text(item, 1) for item in vtuber_channels]
# 非VTuber候補にはラベル0を付与
non_vtuber_channels_labeled = [add_label_and_text(item, 0) for item in non_vtuber_channels]

# 両方のリストを連結して1つのリストにする
all_channels = vtuber_channels_labeled + non_vtuber_channels_labeled

# JSONL形式で保存する
import json
def save_to_jsonl(data, filename):
    with open(filename, "w", encoding="utf-8") as f:
        for item in data:
            f.write(json.dumps(item, ensure_ascii=False) + "\n")

save_to_jsonl(all_channels, "vtuber_youtube_list.jsonl")


# Hugging Face CLIのインストール(必要な場合)
!pip install huggingface_hub

# Hugging Faceにログイン(アクセストークンを入力するプロンプトが表示されます)
!huggingface-cli login

# 必要なライブラリのインストール(初回のみ)
!pip install huggingface_hub

from huggingface_hub import HfApi, upload_file
from google.colab import userdata
import time

# Colabのシークレットからアクセストークンを取得("HF_TOKEN"として登録済み)
hf_token = userdata.get('HF_TOKEN')
if hf_token is None:
    raise ValueError("HF_TOKENがシークレットに登録されていません。")

# アップロード先のリポジトリID("your_username" をあなたのHugging Faceユーザー名に置き換えてください)
repo_id = "ayousanz/vtuber-youtube-list-dataset"

# HfApiを利用してリポジトリを作成(既に存在する場合はスキップ)
api = HfApi()
try:
    api.create_repo(repo_id=repo_id, repo_type="dataset", exist_ok=True, token=hf_token)
    print(f"リポジトリ '{repo_id}' が作成済み、または既に存在します。")
except Exception as e:
    print("リポジトリ作成時のエラー:", e)

# Hub上に反映されるまで待機(例:10秒)
print("Hub上に反映されるまで10秒待ちます...")
time.sleep(10)

# JSONLファイル(例:vtuber_channels.jsonl, non_vtuber_channels.jsonl)のアップロード
for filename in ["vtuber_youtube_list.jsonl"]:
    try:
        upload_file(
            path_or_fileobj=filename,
            path_in_repo=filename,
            repo_id=repo_id,
            repo_type="dataset",
            token=hf_token
        )
        print(f"{filename} のアップロードが完了しました。")
    except Exception as e:
        print(f"{filename} のアップロード時にエラーが発生しました:", e)

print("すべてのファイルのアップロードが完了しました。")

wandbの準備(必要なければスキップ可)

!pip install -U transformers>=4.48.0 datasets evaluate wandb

# wandbのログイン(初回のみ実行)
import wandb
from google.colab import userdata
wandb_api_key = userdata.get('WANDB_API_KEY')
!wandb login $wandb_api_key

学習・評価

以下で学習および評価(テスト推論)を行います

# -------------------------------
# 0. Flash Attention の無効化(GPUがAmpere未満の場合)
# -------------------------------
import os
os.environ["FLASH_ATTN_DISABLE"] = "1"

# -------------------------------
# 1. 必要なライブラリのインストール(初回のみ)
# -------------------------------
!pip install -U transformers datasets evaluate wandb huggingface_hub

# -------------------------------
# 2. 必要なモジュールのインポート
# -------------------------------
import torch
import numpy as np
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer
from datasets import load_dataset, DatasetDict, Features, Value
import evaluate
import wandb
from wandb import Settings
import time

# -------------------------------
# 3. wandb の初期化(project と entity を適切に設定)
# -------------------------------
wandb.init(
    project="modernbert-vtuber",
    entity="yousan",  # ご自身の wandb ユーザー名に変更してください
    config={
        "model_name": "sbintuitions/modernbert-ja-130m",
        "epochs": 3,
        "batch_size": 4,
        "learning_rate": 2e-5
    },
    settings=Settings(init_timeout=210)
)

# -------------------------------
# 4. Hugging Face Hub からデータセットの読み込み
# -------------------------------
# JSONL ファイルは、各レコードが "channel_id", "title", "description", "text", "label" を持つ前提
features = Features({
    "channel_id": Value("string"),
    "title": Value("string"),
    "description": Value("string"),
    "text": Value("string"),
    "label": Value("int64")
})

# ここでは、アップロード済みのデータセット名(例:"ayousanz/vtuber-youtube-list-dataset")を利用します
dataset = load_dataset("ayousanz/vtuber-youtube-list-dataset", features=features)

# もしデータセットに "train" と "validation" の split が存在しない場合は、単一の split から分割
if not ("train" in dataset and "validation" in dataset):
    single_split = list(dataset.keys())[0]
    split_dataset = dataset[single_split].train_test_split(test_size=0.2, seed=42)
    dataset = DatasetDict({
        "train": split_dataset["train"],
        "validation": split_dataset["test"]
    })

# JSONL に "text" フィールドが既に存在する前提ですが、念のため title と description を連結する処理を追加
def add_text_field(example):
    if not example.get("text"):
        example["text"] = example["title"] + " " + example["description"]
    return example

dataset = dataset.map(add_text_field)

# ★ 念のため、ラベルが None でないレコードのみ残す
dataset = dataset.filter(lambda x: x["label"] is not None)

print("サンプルデータ(train):")
print(dataset["train"][0])
print("サンプルデータ(validation):")
print(dataset["validation"][0])

# -------------------------------
# 5. モデルとトークナイザーの読み込み・前処理
# -------------------------------
model_name = "sbintuitions/modernbert-ja-130m"
tokenizer = AutoTokenizer.from_pretrained(model_name)
# モデルはデフォルト(FP32)でロードする(fp16はTrainerで管理)
model = AutoModelForSequenceClassification.from_pretrained(
    model_name, num_labels=2
)

# GPUが利用可能な場合、モデルをGPUに移動
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)

# 前処理:各レコードの "text" をトークナイズ(最大長128、padding)
def preprocess_function(examples):
    return tokenizer(examples["text"], truncation=True, max_length=128, padding="max_length")

tokenized_datasets = dataset.map(preprocess_function, batched=True)

# -------------------------------
# 6. 評価指標およびトレーニング設定(wandb連携)
# -------------------------------
accuracy_metric = evaluate.load("accuracy")
def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return accuracy_metric.compute(predictions=predictions, references=labels)

training_args = TrainingArguments(
    output_dir="./modernbert_vtuber_model",
    evaluation_strategy="epoch",   # 将来的には eval_strategy に変更
    learning_rate=2e-5,
    per_device_train_batch_size=4,
    per_device_eval_batch_size=4,
    num_train_epochs=3,
    weight_decay=0.01,
    save_strategy="epoch",
    logging_dir="./logs",
    report_to=["wandb"],
    run_name="modernbert_vtuber_finetuning",
    fp16=True  # Trainer による混合精度トレーニングを有効化
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    compute_metrics=compute_metrics,
)

# -------------------------------
# 7. ファインチューニング実行
# -------------------------------
trainer.train()

# 学習済みモデルとトークナイザーの保存
model.save_pretrained("./modernbert_vtuber_model")
tokenizer.save_pretrained("./modernbert_vtuber_model")

wandb.finish()

# -------------------------------
# 8. 推論関数の定義と使用例
# -------------------------------
def classify_vtuber(text, threshold=50.0):
    """
    入力文章に対して VTuber 判定を行い、VTuber である確信度 (rate) を算出します。
    threshold 以上なら VTuber と判定します。
    """
    inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding="max_length")
    inputs = {k: v.to(device) for k, v in inputs.items()}
    outputs = model(**inputs)
    logits = outputs.logits
    probabilities = torch.softmax(logits, dim=-1).squeeze().tolist()  # [非VTuber確率, VTuber確率]
    vtuber_rate = probabilities[1] * 100
    is_vtuber = vtuber_rate >= threshold
    return {"isVTuber": is_vtuber, "rate": round(vtuber_rate, 3)}

# 使用例
input_text = "この動画では、バーチャルなキャラクターがリアルタイムに動く様子を配信しています。"
result = classify_vtuber(input_text)
print("入力文章の判定結果:")
print(result)

推論の結果は以下のようになります

入力文章の判定結果:
{'isVTuber': True, 'rate': 100.0}

学習したモデルをhuggingfaceにアップロード

以下で学習したモデルをhuggingfaceにアップロードします

# huggingface_hub ライブラリからリポジトリ作成用の関数をインポート
from huggingface_hub import create_repo

# アップロード先のリポジトリ名を指定(既に作成済みならこのステップはスキップ可能)
repo_id = "ayousanz/modernbert-vtuber-finetuned-1"  # ご自身のユーザー名とリポジトリ名に変更してください
create_repo(repo_id, exist_ok=True)

# 学習済みモデルとトークナイザーをアップロード
model.push_to_hub(repo_id)
tokenizer.push_to_hub(repo_id)

モデルをWindowsで推論する

(ColobのT4を使い切ってしまったので) Windowsで推論を行なっていきます。推論だけ試したい方はこちらのみで良さそうです

環境作成

uv venv -p 3.11
source venv/bin/activate
uv pip install -r requirements.txt

推論

以下のコードを実行します

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline

# GPU が利用可能か確認
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Using device:", device)

# Hugging Face Hub 上のリポジトリからモデルとトークナイザーをロード
model_name = "ayousanz/modernbert-vtuber-finetuned"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)
model.to(device)  # GPU が利用可能なら GPU に移動

# 推論用パイプラインの作成
vtuber_classifier = pipeline("text-classification", model=model, tokenizer=tokenizer, device=0 if device=="cuda" else -1)

# 5つのサンプルテキストで推論例を実行
sample_texts = [
    "この動画では、バーチャルなキャラクターがリアルタイムに動く様子を配信しています。",
    "このチャンネルは、料理レシピの動画を投稿しています。",
    "最新のVTuberがライブ配信を行っており、視聴者との交流が盛んです。",
    "旅行動画を中心に、世界各地の観光地を紹介しています。",
    "ここでは、3Dモデルを使ったアニメーション動画を配信しています。"
]

for text in sample_texts:
    result = vtuber_classifier(text)
    print("入力:", text)
    print("推論結果:", result)
    print("-" * 50)

実行結果は以下のようになります

Using device: cuda
Device set to use cuda:0
入力: この動画では、バーチャルなキャラクターがリアルタイムに動く様子を配信しています。
推論結果: [{'label': 'LABEL_1', 'score': 1.0}]
--------------------------------------------------
入力: このチャンネルは、料理レシピの動画を投稿しています。
推論結果: [{'label': 'LABEL_0', 'score': 1.0}]
--------------------------------------------------
入力: 最新のVTuberがライブ配信を行っており、視聴者との交流が盛んです。
推論結果: [{'label': 'LABEL_1', 'score': 1.0}]
--------------------------------------------------
入力: 旅行動画を中心に、世界各地の観光地を紹介しています。
推論結果: [{'label': 'LABEL_0', 'score': 1.0}]
--------------------------------------------------
入力: ここでは、3Dモデルを使ったアニメーション動画を配信しています。
推論結果: [{'label': 'LABEL_1', 'score': 0.8205193877220154}]
--------------------------------------------------