Skip to content

Instantly share code, notes, and snippets.

@kun432
Last active August 1, 2026 07:45
Show Gist options
  • Select an option

  • Save kun432/4364ddcc057b55f139471777f27c533a to your computer and use it in GitHub Desktop.

Select an option

Save kun432/4364ddcc057b55f139471777f27c533a to your computer and use it in GitHub Desktop.
LFM2.5-Encoder-350M を試すチュートリアル

LFM2.5-Encoder-350M を試すチュートリアル

はじめに

このチュートリアルは、LiquidAI のエンコーダーモデル「LFM2.5-Encoder-350M」を、Google Colaboratory で動かしながら学ぶためのものです。

エンコーダーモデルは、文の意味を捉えるのが得意です。そのため、次のような作業に使えます。

  • 文の分類(感情、カテゴリ、ルーティングなど)
  • 固有表現抽出(人名、地名、金額などを見つける)
  • 文の意味の類似度
  • 文の校正や個人情報の検出

このモデルは 15 言語に対応しています。日本語も含まれます。350M(3 億 5 千万)のパラメータで、CPU でも動く軽さが特徴です。

Warning

  • 本チュートリアルは、GLM-5.2 に生成させたものです。ハルシネーションが含まれている可能性があります。
  • 動作は未確認です。動かない可能性があります。

このチュートリアルの使い方

  1. Google Colaboratory で、新しいノートブックを開きます。
  2. この文書を上から順に読みます。
  3. 「コードセル」と書いた部分を、Colab のコードセルに貼り付け、実行します。
  4. 「解説」の部分は、読むだけで構いません。

コードセルは、上から順に実行してください。前のセルの結果を、次のセルで使います。

実行環境について

GPU があると速いです。Colab のメニュー「ランタイム → ランタイムのタイプを変更」で「T4 GPU」を選べます。GPU がなくても動きますが、遅くなります。

目次

  • 第 0 部 準備
  • 第 1 部 モデルを触る(空所埋め)
  • 第 2 部 文ベクトルと類似度
  • 第 3 部 ゼロショット分類
  • 第 4 部 分類のファインチューニング
  • 第 5 部 固有表現抽出(NER)
  • 第 6 部 発展(ポリシーチェック、マスク拡散生成)

第 0 部 準備

この部では、必要なパッケージを入れ、実行環境を確認します。

コードセル 0-1:パッケージのインストール

!pip install -U transformers accelerate

解説transformers は、Hugging Face のモデルを扱うためのライブラリです。accelerate は、モデルを GPU に効率よく載せるための補助ライブラリです。

インストールが終わったら、メニュー「ランタイム → セッションを再起動」を実行してください。新しいバージョンを読み込むためです。

コードセル 0-2:バージョンと GPU の確認

import torch, transformers

print("transformers:", transformers.__version__)
print("torch:", torch.__version__)
print("GPU が使える:", torch.cuda.is_available())

device = "cuda" if torch.cuda.is_available() else "cpu"
print("使うデバイス:", device)

解説:GPU が使えるときは「True」と表示されます。以降のセルでは、モデルをこの device に載せます。device という変数は、あとのセルでも使います。


第 1 部 モデルを触る(空所埋め)

このモデルができること

このモデルは「空所埋め」ができます。文章の一部を隠し([MASK])、そこに入る言葉を予測します。これを masked language model(MLM)と呼びます。

例:「日本で一番高い山は [MASK] です。」の [MASK] を「富士山」と予測する。

空所を埋められるということは、モデルが言葉の意味を理解している、ということです。この能力が、分類や固有表現抽出の土台になります。

コードセル 1-1:モデルとトークナイザを読み込む

from transformers import AutoTokenizer, AutoModelForMaskedLM

MODEL = "LiquidAI/LFM2.5-Encoder-350M"

tok = AutoTokenizer.from_pretrained(MODEL, trust_remote_code=True)
mlm = AutoModelForMaskedLM.from_pretrained(MODEL, trust_remote_code=True).to(device).eval()

print("読み込み完了")
print("隠し文字(mask_token):", tok.mask_token)

解説

  • trust_remote_code=True が必要です。このモデルは独自のプログラムを使うため、その実行を許可します。
  • mlm は、空所埋め用のモデルです。.to(device) で GPU に載せ、.eval() で推論モードにします。
  • tok.mask_token[MASK] にあたる文字です。あとのセルで使います。

最初の読み込みには時間がかかります(重いファイルをダウンロードします)。

解説:高速な attention について(sdpa で十分)

このモデルは Flash Attention に対応しています。でも、特別な設定は不要です。

通常の読み込み(何も指定しない)では、PyTorch の SDPA が使われます。SDPA は、GPU が対応していれば、自動的に Flash Attention のカーネルを選びます。L4 や A100 などの対応 GPU なら、SDPA の中身は Flash Attention と同じです。

つまり、高速化・省メモリの恩恵は、すでに得られています。そのまま進めて大丈夫です。

注意:attn_implementation="flash_attention_2" を明示的に指定すると、このモデル(双方向の独自コードと transformers v5 の組み合わせ)では sdpa に戻ることがあります。気にしなくて大丈夫です。SDPA で進めてください。

コードセル 1-2:空所を予測する

text = f"日本で一番高い山は{tok.mask_token}です。"

enc = tok(text, return_tensors="pt").to(device)
with torch.no_grad():
    logits = mlm(**enc).logits

# [MASK] の位置を探す
pos = (enc["input_ids"][0] == tok.mask_token_id).nonzero()[0].item()

# スコアを付けて、上位 5 件を表示
probs = torch.softmax(logits[0, pos], dim=-1)
top = probs.topk(5)

print("入力:", text)
print("予測 上位 5 件:")
for p, t in zip(top.values.tolist(), top.indices.tolist()):
    print(f"  {tok.decode([t]).strip():>8}  スコア {p:.3f}")

解説

  • enc は、文字をモデル用の数値(トークン ID)に変換したものです。
  • mlm(**enc).logits で、各位置の各言葉の「らしさ(スコア)」が出ます。
  • [MASK] の位置 pos だけを抜き出し、スコアの高い上位 5 件を見ます。
  • softmax で、スコアを確率(0〜1)に直します。pipeline(セル 1-3)のスコアと同じものです。
  • torch.no_grad() は、学習用の計算を省いてメモリを節約する設定です。推論時によく使います。

解説:固有名詞がうまく当たらないことがある

「富士山」を期待して [MASK] を埋めさせると、「山」や「高」が出ることがあります。これは不具合ではなく、仕組みの限界です。理由は 2 つあります。

理由 1:単語が複数のトークンに分かれる

このモデルは、単語を「トークン」と呼ぶ小さい単位に分けます。「富士山」は、次のように 3 つに分かれます。

富士山  →  富 + 士 + 山
東京    →  東京(1 つ)

でも穴埋めは、一度に 1 つのトークンしか予測できません。だから [MASK] の位置には「富士山」全体でなく、最初の「富」を予測しなければなりません。文脈だけでは、それが無理です。その結果、1 トークンで文脈に合う「山」や「高」が出やすくなります。一方「東京」は 1 トークンなので、当てやすくなります。

自分で確認できます。

for w in ["富士山", "富士", "東京", "パリ", "Paris"]:
    ids = tok.encode(w, add_special_tokens=False)
    print(f"{w} -> {[tok.decode([i]) for i in ids]}")

理由 2:トークンの文字が化けて見える

入力のトークンを表示すると、å±± のように文字化けすることがあります。これは、このモデルが「バイトレベル」という方式で文字を分けているためです。正常な動作です。tok.decode() を通せば、正しい文字(この場合は「山」)に戻ります。

理由 3:このモデルは多言語モデルだから

「富士山」が細かく分かれるのは、このモデルが 15 言語に対応する多言語モデルだからです。語彙を多くの言語で分け合うため、日本語の語彙が粗くなります。日本語特化のモデル(modernbert-ja など)なら、「富士山」が 1〜2 トークンになり、当たるようになります。

ファインチューニングでも直らない

ファインチューニング(第 4 部)では、モデルの重みは学習されますが、トークナイザ(分割ルール)は変わりません。つまり、「富士山」の分割の粗さは、ファインチューニング後も同じです。日本語の固有名詞が重要な場合は、最初から日本語特化の語彙を持つモデルを選ぶのが現実的です。

この限界は、穴埋め(MLM)特有のものです。分類や文ベクトル(第 2 部以降)では、トークンをまとめて扱うので、影響は出ません。

コードセル 1-3:pipeline を使うと、もっと簡単

from transformers import pipeline

fill = pipeline("fill-mask", model=mlm, tokenizer=tok)
result = fill(f"フランスの首都は{tok.mask_token}です。")
for r in result:
    print(f"{r['token_str']:>10}  スコア {r['score']:.3f}")

解説pipeline は、よくある作業を 1 行で済ませる仕組みです。結果にスコア(確率)が付いてきます。

コードセル 1-4:試してみよう

次の文の [MASK] を、いくつか試してください。

examples = [
    f"雨が降ったので、出かける時に{tok.mask_token}を持った。",
    f"このレストランはとても{tok.mask_token}だった。",
    f"パソコンが動かないので、{tok.mask_token}を押して再起動した。",
]
for ex in examples:
    print("\n", ex)
    for r in fill(ex):
        print(f"   {r['token_str']:>8}  {r['score']:.3f}")

解説:文脈によって、当てはまる言葉が変わる様子が分かります。

コードセル 1-5:モデルの中身を見る

print(mlm)

解説:モデルの構造が表示されます。見るべき点は次の 2 つです。

  • Lfm2BidirectionalForMaskedLM という名前から、このモデルが「双方向(前後の文脈を両方見る)」のエンコーダーだと分かります。
  • 層(layers)が 16 個あります。このうち 6 層は attention(前後を広く見る)、残り 10 層は shortconv(近所だけ見る)です。両方を混ぜるのが、このモデルの特徴です。これにより、長い文章でも速く動きます。

この部のまとめ

  • このモデルは、文章の空所を埋めることができる。
  • それは、モデルが言葉の意味を捉えている証拠である。
  • この能力が、次の部で扱う「分類」や「類似度」の土台になる。

第 2 部 文ベクトルと類似度

この部でやること

  • 文を、数値の並び(ベクトル)に変換する
  • 文どうしの「意味の近さ」を、数値で比べる

解説:文ベクトルとは

エンコーダーは、文の各トークンに対して「意味を表す数値の並び(ベクトル)」を出します。これを文全体で 1 つにまとめたものを、文ベクトルと呼びます。

文ベクトルが似ていれば、意味も似ている、と考えられます。これが、分類や検索の仕組みです。

コードセル 2-1:ベースモデルを読み込む

from transformers import AutoModel

model = AutoModel.from_pretrained(MODEL, trust_remote_code=True).to(device).eval()

解説:第 1 部で使った mlm は空所埋め用でした。ここでは、分類や検索に使う「本体」だけが欲しいので、AutoModel で読み込み直します。これは出力として、各トークンのベクトル(隠れ状態)を返します。

コードセル 2-2:文をベクトルにする関数

def embed(text):
    enc = tok(text, return_tensors="pt").to(device)
    with torch.no_grad():
        out = model(**enc).last_hidden_state   # (1, トークン数, 1024)
    mask = enc["attention_mask"].unsqueeze(-1) # (1, トークン数, 1)
    summed = (out * mask).sum(1)               # 余分な部分を除外して合計
    counts = mask.sum(1)                       # トークン数
    return (summed / counts).squeeze(0)        # 平均 = 文ベクトル (1024,)

v = embed("今日はいい天気です。")
print("ベクトルの長さ:", v.shape)

解説

  • last_hidden_state は、各トークンのベクトルです。形は (1, トークン数, 1024) です。
  • このままではトークンごとにバラバラです。これを 1 つの文ベクトルにまとめます。
  • まとめ方を mean pooling(平均プーリング)と呼びます。トークンのベクトルを平均します。公式デモ(prompt-routing)も、この平均を使っています。
  • attention_mask で、文末の余分な枠(パディング)を計算から外します。

コードセル 2-3:意味の近さを比べる(コサイン類似度)

import torch.nn.functional as F

def similarity(a, b):
    return F.cosine_similarity(a.unsqueeze(0), b.unsqueeze(0)).item()

sentences = [
    "今日はとても暑い。",
    "気温が高くて汗が出る。",
    "猫が寝転がっている。",
    "明日は会議がある。",
]
vecs = [embed(s) for s in sentences]

print("比較元:", sentences[0])
for i in range(1, len(sentences)):
    print(f"  {similarity(vecs[0], vecs[i]):.3f}  {sentences[i]}")

解説

  • コサイン類似度は、2 つのベクトルの向きの近さを 0〜1(完全に一致で 1)で表します。
  • 「暑い」と「気温が高くて汗が出る」は違う言葉ですが、意味が近いので数値が高くなるはずです。
  • 「猫が寝転がっている」「会議がある」は意味が遠いので、数値が低くなります。

コードセル 2-4:応用、意味で文を探す(簡易検索)

docs = [
    "新商品のスマートフォンを発売しました。",
    "決算発表によると売上が増加しました。",
    "台風が接近しており、警戒が必要です。",
    "来週の火曜日は臨時休業となります。",
    "新しい社員研修プログラムを開始します。",
]
doc_vecs = [embed(d) for d in docs]

query = "休みのお知らせ"
q = embed(query)

for d, s in sorted(zip(docs, [similarity(q, dv) for dv in doc_vecs]), key=lambda x: -x[1]):
    print(f"  {s:.3f}  {d}")

解説:問い合わせ「休みのお知らせ」に一番近いのは「臨時休業」のはずです。言葉が違っても、意味が近い文を探せるのが、文ベクトルの強みです。

この部のまとめ

  • 文をベクトルに変換できた。
  • mean pooling で、文全体を 1 つのベクトルにまとめた。
  • コサイン類似度で、意味の近さを数値で比べられた。
  • この仕組みが、次の部の「ゼロショット分類」の土台になる。

第 3 部 ゼロショット分類

この部でやること

  • 学習なしで、文をラベルに振り分ける(これをゼロショット分類と呼ぶ)

解説:ゼロショット分類とは

ふつう、分類モデルを作るには、ラベルごとに学習データを用意します。でも、文ベクトルがあれば、学習なしで分類できます。手順は次の通りです。

  1. 分類したいテキストの、文ベクトルを作る。
  2. 各ラベル(「予約」「天気」など)も、1 つの「言葉・文」と見なして文ベクトルを作る。
  3. テキストと各ラベルの、ベクトルの近さ(コサイン類似度)を比べる。
  4. 一番近いラベルを、分類結果とする。

これが、公式デモ「prompt-routing」の仕組みです。デモは、この仕組みをさらに学習させて精度を上げています。でも、ベースモデルだけでも、この方法は動きます。

コードセル 3-1:ゼロショット分類の関数

def zero_shot_classify(text, labels, temperature=5.0):
    q = embed(text)                                   # テキストのベクトル
    label_vecs = [embed(lbl) for lbl in labels]       # 各ラベルのベクトル
    scores = torch.tensor([similarity(q, lv) for lv in label_vecs])
    probs = torch.softmax(scores * temperature, dim=0) # 確率っぽく整える
    return sorted(zip(labels, probs.tolist()), key=lambda x: -x[1])

解説

  • temperature(温度)は、類似度の差を強める係数です。大きくするほど、1 位が際立ちます。公式デモでも、同じような温度パラメータを使っています。
  • softmax で、スコアを合計 1 の確率に直します。

コードセル 3-2:問い合わせを振り分ける

labels = ["予約の変更", "天気についての質問", "ニュース", "料理のレシピ"]

text = "明日の東京の雨はいつ止みますか?"
for lbl, p in zero_shot_classify(text, labels):
    print(f"  {p*100:5.1f}%  {lbl}")

解説:「雨」という言葉から、「天気についての質問」に振り分けられるはずです。

コードセル 3-3:ラベルの書き方を工夫する

ゼロショット分類では、ラベルの言葉選びが精度に効きます。短いラベルと、詳しく書いたラベルを比べてみます。

labels_short = ["予約", "天気", "ニュース", "レシピ"]
labels_long  = ["ホテルやレストランの予約・変更・キャンセル",
                "明日の天気・気温・雨・晴れ",
                "最新のニュース・出来事",
                "料理の作り方・レシピ・材料"]

for name, labels in [("短い", labels_short), ("詳しい", labels_long)]:
    print(f"\n[{name}ラベル] 予約をキャンセルしたい")
    for lbl, p in zero_shot_classify("予約をキャンセルしたい", labels):
        print(f"  {p*100:5.1f}%  {lbl}")

解説:ラベルを「予約」だけにするより、「予約・変更・キャンセル」と詳しく書くほうが、当たりやすくなることが多いです。これが、prompt-routing デモで「ラベルを自由な文章で書ける」とされている理由です。

補足:公式の学習済みモデル

この部でやった「ゼロショット分類」を、公式が学習させたモデルがあります。精度を高めた専用モデルです。

この部のまとめ

  • 学習なしで(ゼロショットで)、文を分類できた。
  • 仕組みは、テキストとラベルのベクトルの近さを比べること。
  • ラベルを詳しく書くと、当たりやすくなる。
  • もっと高い精度が欲しい場合は、次の部の「ファインチューニング」へ。

第 4 部 分類のファインチューニング

この部でやること

  • ベースモデルに「分類ヘッド」を足して、データで学習する(これをファインチューニングと呼ぶ)
  • 日本語の感情分析(ポジティブ / ネガティブ)をやってみる

解説:ファインチューニングとは

第 3 部のゼロショットは手軽ですが、精度に限界があります。専用の分類モデルを作るには、ベースモデルに分類ヘッドを足し、ラベル付きのデータで学習させます。これがファインチューニングです。

この部では、少ないデータで学習の流れを体験します。実際の運用では、もっと大量のデータを使います。

コードセル 4-1:分類モデルを読み込む

from transformers import AutoModelForSequenceClassification

NUM_LABELS = 2  # ポジティブ / ネガティブ
clf = AutoModelForSequenceClassification.from_pretrained(
    MODEL, num_labels=NUM_LABELS, trust_remote_code=True
).to(device)

解説num_labels=2 で 2 値分類にします。ベースモデルの上に、2 つの値を出す小さな層(分類ヘッド)が自動で付きます。

コードセル 4-2:学習データを用意する

train_data = [
    ("この映画は本当に面白かった。", 1),
    ("最高の経験だった。", 1),
    ("とても感動的な話だった。", 1),
    ("サービスが良くて満足した。", 1),
    ("もう一度見たいと思う。", 1),
    ("退屈で途中で眠くなった。", 0),
    ("最悪の体験だった。", 0),
    ("期待外れだった。", 0),
    ("時間の無駄だった。", 0),
    ("二度と利用したくない。", 0),
]
# 1 = ポジティブ、0 = ネガティブ

解説:1 件 1 文にラベルを付けた、小さなデータです。分かりやすさのための手作りデータです。実際は数百〜数万件を使います。

コードセル 4-3:データをモデル用の形にする

import torch

class ReviewDS(torch.utils.data.Dataset):
    def __init__(self, data):
        self.data = data
    def __len__(self):
        return len(self.data)
    def __getitem__(self, i):
        text, label = self.data[i]
        enc = tok(text, truncation=True, padding="max_length",
                  max_length=64, return_tensors="pt")
        return {
            "input_ids": enc["input_ids"][0],
            "attention_mask": enc["attention_mask"][0],
            "labels": torch.tensor(label),
        }

train_ds = ReviewDS(train_data)

解説

  • 各文を、トークン ID に変換します。
  • max_length=64 で長さをそろえ、足りない部分は詰め物(パディング)します。
  • labels に正解ラベルを入れます。学習時、これと比べて誤りを直します。

コードセル 4-4:Trainer で学習する

from transformers import TrainingArguments, Trainer

args = TrainingArguments(
    output_dir="./clf_out",
    num_train_epochs=5,
    per_device_train_batch_size=4,
    learning_rate=2e-5,     # モデルカードの推奨範囲 1e-5〜5e-5
    warmup_ratio=0.1,       # 推奨
    weight_decay=0.1,       # 推奨
    logging_steps=5,
    save_strategy="no",
    report_to="none",
)

trainer = Trainer(model=clf, args=args, train_dataset=train_ds)
trainer.train()

解説

  • 学習率、warmup、weight decay は、モデルカードが示す推奨値を使っています。
  • GPU があると速いです。T4 GPU で数分、CPU でも動きますが時間がかかります。
  • Colab の GPU が Ampere 世代以降(A100 など)なら、bf16=True を足すと速く、安定します。

コードセル 4-5:分類してみる

clf.eval()

def predict_sentiment(text):
    enc = tok(text, return_tensors="pt", truncation=True, max_length=64).to(device)
    with torch.no_grad():
        logits = clf(**enc).logits
    prob = torch.softmax(logits, dim=-1)[0]
    label = "ポジティブ" if prob.argmax().item() == 1 else "ネガティブ"
    return label, prob[1].item()

for t in ["とても楽しかった。", "全く良くなかった。", "普通だった。"]:
    lbl, pos = predict_sentiment(t)
    print(f"  [{lbl}] ポジ度 {pos:.2f}  {t}")

解説

  • softmax で、スコアを確率に直します。
  • 「ポジ度」はポジティブである確率です。1.0 に近いほどポジティブ、0.0 に近いほどネガティブです。
  • 手作りデータ 10 件なので、過学習しやすいです。実データで学ぶと、もっと自然に判定できるようになります。

コードセル 4-6:本格的なデータに切り替える

手作りデータの代わりに、公開データセットを使えます。例として、よく使われる日本語データを紹介します。使う前に、各データの利用条件を確認してください。

# datasets ライブラリが必要です: !pip install datasets
#
# from datasets import load_dataset
#
# 感情分析(WRIME):llm-book/wrime など
# 金融文の感情(chABSA):llm-book/chABSA など
# ニュースのカテゴリ分類:livedoor ニュース コーパス
#
# 名前や分割名は変更されていることがあるため、
# Hugging Face Datasets で検索して、最新の指定方法を確認してください。

解説:データが大きくなるほど、モデルの精度と安定性が上がります。第 4 部 4-3 の ReviewDS を、読み込んだデータセットに差し替えれば、同じ枠組みで学習できます。

この部のまとめ

  • 分類ヘッドを足して、データで学習(ファインチューニング)できた。
  • モデルカードの推奨ハイパーパラメータを使った。
  • 同じ枠組みで、公開データセットに切り替えられる。

第 5 部 固有表現抽出(NER)

この部でやること

  • 文の中の「人名・地名・組織名」を見つける(これを NER と呼ぶ)

解説:NER とは

NER(Named Entity Recognition)は、文の中の固有表現を見つける作業です。

例:「田中さんが東京で会議をした」→ 田中=人名、東京=地名。

NER は「トークン分類」の一種です。トークンごとにラベルを付けます。

解説:BIO タグ

1 つの単語が複数のトークンに分かれることがあります。そこで、先頭と続きを区別します。これを BIO タグと呼びます。

  • B-PER:人名の先頭(Begin)
  • I-PER:人名の続き(Inside)
  • B-LOC / I-LOC:地名
  • B-ORG / I-ORG:組織名
  • O:固有表現でない(Outside)

コードセル 5-1:トークン分類モデルを読み込む

from transformers import AutoModelForTokenClassification

LABELS = ["O", "B-PER", "I-PER", "B-LOC", "I-LOC", "B-ORG", "I-ORG"]
ID2LABEL = {i: l for i, l in enumerate(LABELS)}
LABEL2ID = {l: i for i, l in enumerate(LABELS)}

ner = AutoModelForTokenClassification.from_pretrained(
    MODEL, num_labels=len(LABELS),
    id2label=ID2LABEL, label2id=LABEL2ID,
    trust_remote_code=True,
).to(device)

解説:分類(第 4 部)と違うのは、文全体ではなく、トークンごとにラベルを出す点です。

コードセル 5-2:学習データを用意する

単語と、そのラベルのペアを用意します。ここでは分かち書き済み(単語ごとに分かれている)とします。

train_data = [
    (["田中", "さん", "が", "東京", "へ", "行っ", "た"],
     ["B-PER", "O", "O", "B-LOC", "O", "O", "O"]),
    (["山田", "さん", "は", "株式会社", "ABC", "に", "勤め", "ている"],
     ["B-PER", "O", "O", "B-ORG", "I-ORG", "O", "O", "O"]),
    (["大阪", "で", "鈴木", "部長", "が", "講演", "し", "た"],
     ["B-LOC", "O", "B-PER", "I-PER", "O", "O", "O", "O"]),
    (["京都", "大学", "の", "佐藤", "教授", "が", "発表", "し", "た"],
     ["B-ORG", "I-ORG", "O", "B-PER", "I-PER", "O", "O", "O", "O"]),
]

解説:データは少なめです。流れを体験するためです。実運用では、数百〜数千文を使います。

コードセル 5-3:単語とトークンを合わせる(アラインメント)

単語が複数のトークンに分かれたとき、ラベルを正しく割り当てます。Hugging Face の標準的な方法を使います。

def align_labels(words, tags):
    enc = tok(words, is_split_into_words=True, truncation=True, max_length=64)
    word_ids = enc.word_ids()
    labels = []
    prev = None
    for wid in word_ids:
        if wid is None:
            labels.append(-100)                 # 特殊トークンは無視
        elif wid != prev:
            labels.append(LABEL2ID[tags[wid]])  # 単語の先頭トークンにラベル
        else:
            labels.append(-100)                 # 続きのトークンは無視
        prev = wid
    enc["labels"] = labels
    return enc

tokenized = [align_labels(w, t) for w, t in train_data]

解説

  • is_split_into_words=True で、単語のリストを入力します。
  • 単語が複数トークンに分かれたとき、最初のトークンにラベルを付け、残りは -100 にします。
  • -100 は「ここは学習に使わない」という PyTorch の決まりです。

コードセル 5-4:Trainer で学習する

from transformers import TrainingArguments, Trainer, DataCollatorForTokenClassification

args = TrainingArguments(
    output_dir="./ner_out",
    num_train_epochs=10,
    per_device_train_batch_size=2,
    learning_rate=2e-5,
    warmup_ratio=0.1,
    weight_decay=0.1,
    save_strategy="no",
    report_to="none",
)

# バッチごとに長さをそろえる(ラベルも同時に)
collator = DataCollatorForTokenClassification(tokenizer=tok)

trainer = Trainer(
    model=ner, args=args,
    train_dataset=tokenized,
    data_collator=collator,
)
trainer.train()

解説

  • DataCollatorForTokenClassification が、バッチ内の長さをそろえ、ラベルも正しく調整します。トークン分類では、この仕組みが必須です。
  • データが少ないので、epoch を多め(10)にします。

コードセル 5-5:固有表現を見つける

ner.eval()

def predict_ner(text):
    enc = tok(text, return_tensors="pt", truncation=True, max_length=64).to(device)
    with torch.no_grad():
        preds = ner(**enc).logits.argmax(-1)[0]
    tokens = tok.convert_ids_to_tokens(enc["input_ids"][0])
    return [(t, ID2LABEL[p.item()]) for t, p in zip(tokens, preds)
            if ID2LABEL[p.item()] != "O"]

for text in ["田中さんが東京へ行った。", "京都大学の佐藤教授が発表した。"]:
    print(text)
    for token, label in predict_ner(text):
        print(f"   {label:6}{token}")

解説

  • 各トークンにラベルを予測します。
  • 学習データが少ないため、精度は限られます。実データで学ぶと改善します。
  • 実運用では、B-I- をつなげて、1 つの「範囲(スパン)」にまとめます。

コードセル 5-6:公式デモの PII 検出を知る

LiquidAI は、このモデルで個人情報(PII)を見つけるデモを公開しています。

このデモは、トークン分類モデルに正規表現を組み合わせて、精度を上げています。この部で学んだ「トークン分類」の、実用的な応用例です。

この PII モデルは公開されています(本稿執筆時点)。HF_TOKEN は不要です。ただし、モデルと一緒に補助コード(pii_hybrid_decode.py など)が配布されており、読み込みには trust_remote_code=True が必要です。

この部のまとめ

  • トークン分類で、固有表現を見つける(NER)をやった。
  • BIO タグで、先頭と続きを区別した。
  • 単語とトークンのアラインメントを、標準的な方法でやった。
  • 公式デモの PII 検出は、同じ仕組みの実用例である。

第 6 部 発展(ポリシーチェック、マスク拡散生成)

この部でやること

  • これまでの技術を組み合わせて、2 つの応用を試す
  • 残りの公式デモを紹介する

解説:ここまでの 3 つの道具

  • 文ベクトル(第 2 部):文をベクトルにする
  • ゼロショット分類(第 3 部):ベクトルの近さで分類する
  • トークン分類(第 5 部):トークンごとに判定する

これらを組み合わせると、さまざまな応用が作れます。

コードセル 6-1:ポリシーチェック(policy-linting 風)

ルールを文章で書き、テキストの「どの部分が、どのルールに触れそうか」を判定します。公式デモ policy-linting の仕組みを、ベースモデルで簡易に再現します。

def check_policy(text, rules):
    enc = tok(text, return_tensors="pt").to(device)
    with torch.no_grad():
        token_vecs = model(**enc).last_hidden_state[0]   # 各トークンのベクトル (T, 1024)
    rule_vecs = [embed(r) for r in rules]
    tokens = tok.convert_ids_to_tokens(enc["input_ids"][0])

    scores = {r: [] for r in rules}
    for i, tvec in enumerate(token_vecs):
        for r, rvec in zip(rules, rule_vecs):
            s = F.cosine_similarity(tvec.unsqueeze(0), rvec.unsqueeze(0)).item()
            scores[r].append((tokens[i], s))
    return scores

rules = ["個人情報を含む内容", "会社を批判する内容"]
text = "私の電話番号は090-1234-5678です。この会社は最悪だ。"
scores = check_policy(text, rules)

specials = {tok.cls_token, tok.eos_token, tok.pad_token, tok.bos_token}
for r in rules:
    print(f"\nルール: {r}")
    for token, s in scores[r]:
        if s > 0.40 and token not in specials:
            print(f"   {s:.2f}  {token}")

解説

  • トークンのベクトルと、ルールのベクトルを比べます。
  • スコアが高いトークンは、そのルールに触れそう、と判断できます。
  • 閾値 0.40 は目安です。データで調整します。
  • 公式デモは、この仕組みを学習させて精度を上げています。ベースモデルでの再現はあくまで簡易版です。

コードセル 6-2:マスクを複数埋めて、簡易生成

第 1 部の空所埋め(fill-mask)は、1 つの空所を埋めました。複数の空所を一度に埋めると、簡単な生成ができます。

def fill_all(text):
    enc = tok(text, return_tensors="pt").to(device)
    ids = enc["input_ids"][0].clone()
    mask_pos = (ids == tok.mask_token_id).nonzero().flatten()
    with torch.no_grad():
        logits = mlm(**enc).logits[0]
    for p in mask_pos:
        ids[p] = logits[p].argmax()
    return tok.decode(ids, skip_special_tokens=True)

prompt = f"質問: 日本で一番高い山は? 答え: {tok.mask_token}{tok.mask_token}。"
print(fill_all(prompt))

解説:複数の [MASK] を、一度に予測して埋めます。知識を問う質問なら、それなりの答えが出ます。

解説:マスク拡散生成(masked-diffusion デモ)

公式デモ masked-diffusion は、もっと本格的な生成をします。仕組みは次の通りです。

  1. 答えの部分を、すべて [MASK] で埋める。
  2. モデルで、各 [MASK] の予測と、その確信度を出す。
  3. 確信度が高いものから順に、トークンを確定する。
  4. 残りの [MASK] を、もう一度予測し直す。これを繰り返す。

これをマスク拡散(Masked Diffusion)と呼びます。

重要:ベースモデルのままでは、この生成はうまくいきません。ベースモデルは「質問をそのまま繰り返す(echo する)」傾向があるためです。デモは、この生成用に学習させた別モデル(LFM2.5-Encoder-350M-Diffusion)を使っています。

コードセル 6-3:公式デモ 5 つのまとめ

デモ すること このチュートリアルの対応
prompt-routing ゼロショット分類 第 3 部
policy-linting ルールでテキスト検査 第 6 部 6-1
spellchecker スペル訂正 (MLM の応用)
pii-detection 個人情報の検出(NER) 第 5 部
masked-diffusion テキスト生成 第 6 部 6-2

各デモは、ベースモデルにヘッドを足して学習させた、専用モデルを使っています。

各モデルのリンク:

おわりに

このチュートリアルで扱ったこと:

  • モデルの基本的な使い方(空所埋め)
  • 文ベクトルと、意味の類似度
  • ゼロショット分類
  • 分類のファインチューニング
  • 固有表現抽出(NER)
  • ポリシーチェックと簡易生成

次に進むためのリソース:

  • モデルカード:huggingface.co/LiquidAI/LFM2.5-Encoder-350M
  • 公式ドキュメント:docs.liquid.ai/lfm/models/lfm25-encoder-350m
  • 公式デモ:Hugging Face Spaces(LiquidAI のページ)
  • 評価ハーネス:github.com/Liquid4All/encoder_eval

このモデルは、軽くて多言語対応の、実用的なエンコーダーです。日本語のデータで、ぜひ自分のタスクを試してみてください。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment