このチュートリアルは、LiquidAI のエンコーダーモデル「LFM2.5-Encoder-350M」を、Google Colaboratory で動かしながら学ぶためのものです。
エンコーダーモデルは、文の意味を捉えるのが得意です。そのため、次のような作業に使えます。
- 文の分類(感情、カテゴリ、ルーティングなど)
- 固有表現抽出(人名、地名、金額などを見つける)
- 文の意味の類似度
- 文の校正や個人情報の検出
このモデルは 15 言語に対応しています。日本語も含まれます。350M(3 億 5 千万)のパラメータで、CPU でも動く軽さが特徴です。
Warning
- 本チュートリアルは、GLM-5.2 に生成させたものです。ハルシネーションが含まれている可能性があります。
- 動作は未確認です。動かない可能性があります。
- Google Colaboratory で、新しいノートブックを開きます。
- この文書を上から順に読みます。
- 「コードセル」と書いた部分を、Colab のコードセルに貼り付け、実行します。
- 「解説」の部分は、読むだけで構いません。
コードセルは、上から順に実行してください。前のセルの結果を、次のセルで使います。
GPU があると速いです。Colab のメニュー「ランタイム → ランタイムのタイプを変更」で「T4 GPU」を選べます。GPU がなくても動きますが、遅くなります。
- 第 0 部 準備
- 第 1 部 モデルを触る(空所埋め)
- 第 2 部 文ベクトルと類似度
- 第 3 部 ゼロショット分類
- 第 4 部 分類のファインチューニング
- 第 5 部 固有表現抽出(NER)
- 第 6 部 発展(ポリシーチェック、マスク拡散生成)
この部では、必要なパッケージを入れ、実行環境を確認します。
!pip install -U transformers accelerate解説:transformers は、Hugging Face のモデルを扱うためのライブラリです。accelerate は、モデルを 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 という変数は、あとのセルでも使います。
このモデルは「空所埋め」ができます。文章の一部を隠し([MASK])、そこに入る言葉を予測します。これを masked language model(MLM)と呼びます。
例:「日本で一番高い山は [MASK] です。」の [MASK] を「富士山」と予測する。
空所を埋められるということは、モデルが言葉の意味を理解している、ということです。この能力が、分類や固有表現抽出の土台になります。
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]にあたる文字です。あとのセルで使います。
最初の読み込みには時間がかかります(重いファイルをダウンロードします)。
このモデルは Flash Attention に対応しています。でも、特別な設定は不要です。
通常の読み込み(何も指定しない)では、PyTorch の SDPA が使われます。SDPA は、GPU が対応していれば、自動的に Flash Attention のカーネルを選びます。L4 や A100 などの対応 GPU なら、SDPA の中身は Flash Attention と同じです。
つまり、高速化・省メモリの恩恵は、すでに得られています。そのまま進めて大丈夫です。
注意:attn_implementation="flash_attention_2" を明示的に指定すると、このモデル(双方向の独自コードと transformers v5 の組み合わせ)では sdpa に戻ることがあります。気にしなくて大丈夫です。SDPA で進めてください。
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 部以降)では、トークンをまとめて扱うので、影響は出ません。
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 行で済ませる仕組みです。結果にスコア(確率)が付いてきます。
次の文の [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}")解説:文脈によって、当てはまる言葉が変わる様子が分かります。
print(mlm)解説:モデルの構造が表示されます。見るべき点は次の 2 つです。
Lfm2BidirectionalForMaskedLMという名前から、このモデルが「双方向(前後の文脈を両方見る)」のエンコーダーだと分かります。- 層(layers)が 16 個あります。このうち 6 層は attention(前後を広く見る)、残り 10 層は shortconv(近所だけ見る)です。両方を混ぜるのが、このモデルの特徴です。これにより、長い文章でも速く動きます。
- このモデルは、文章の空所を埋めることができる。
- それは、モデルが言葉の意味を捉えている証拠である。
- この能力が、次の部で扱う「分類」や「類似度」の土台になる。
- 文を、数値の並び(ベクトル)に変換する
- 文どうしの「意味の近さ」を、数値で比べる
エンコーダーは、文の各トークンに対して「意味を表す数値の並び(ベクトル)」を出します。これを文全体で 1 つにまとめたものを、文ベクトルと呼びます。
文ベクトルが似ていれば、意味も似ている、と考えられます。これが、分類や検索の仕組みです。
from transformers import AutoModel
model = AutoModel.from_pretrained(MODEL, trust_remote_code=True).to(device).eval()解説:第 1 部で使った mlm は空所埋め用でした。ここでは、分類や検索に使う「本体」だけが欲しいので、AutoModel で読み込み直します。これは出力として、各トークンのベクトル(隠れ状態)を返します。
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で、文末の余分な枠(パディング)を計算から外します。
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)で表します。
- 「暑い」と「気温が高くて汗が出る」は違う言葉ですが、意味が近いので数値が高くなるはずです。
- 「猫が寝転がっている」「会議がある」は意味が遠いので、数値が低くなります。
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 つのベクトルにまとめた。
- コサイン類似度で、意味の近さを数値で比べられた。
- この仕組みが、次の部の「ゼロショット分類」の土台になる。
- 学習なしで、文をラベルに振り分ける(これをゼロショット分類と呼ぶ)
ふつう、分類モデルを作るには、ラベルごとに学習データを用意します。でも、文ベクトルがあれば、学習なしで分類できます。手順は次の通りです。
- 分類したいテキストの、文ベクトルを作る。
- 各ラベル(「予約」「天気」など)も、1 つの「言葉・文」と見なして文ベクトルを作る。
- テキストと各ラベルの、ベクトルの近さ(コサイン類似度)を比べる。
- 一番近いラベルを、分類結果とする。
これが、公式デモ「prompt-routing」の仕組みです。デモは、この仕組みをさらに学習させて精度を上げています。でも、ベースモデルだけでも、この方法は動きます。
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 の確率に直します。
labels = ["予約の変更", "天気についての質問", "ニュース", "料理のレシピ"]
text = "明日の東京の雨はいつ止みますか?"
for lbl, p in zero_shot_classify(text, labels):
print(f" {p*100:5.1f}% {lbl}")解説:「雨」という言葉から、「天気についての質問」に振り分けられるはずです。
ゼロショット分類では、ラベルの言葉選びが精度に効きます。短いラベルと、詳しく書いたラベルを比べてみます。
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 デモで「ラベルを自由な文章で書ける」とされている理由です。
この部でやった「ゼロショット分類」を、公式が学習させたモデルがあります。精度を高めた専用モデルです。
- LiquidAI/LFM2.5-Encoder-350M-Prompt-Router: https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M-Prompt-Router
- 学習なしで(ゼロショットで)、文を分類できた。
- 仕組みは、テキストとラベルのベクトルの近さを比べること。
- ラベルを詳しく書くと、当たりやすくなる。
- もっと高い精度が欲しい場合は、次の部の「ファインチューニング」へ。
- ベースモデルに「分類ヘッド」を足して、データで学習する(これをファインチューニングと呼ぶ)
- 日本語の感情分析(ポジティブ / ネガティブ)をやってみる
第 3 部のゼロショットは手軽ですが、精度に限界があります。専用の分類モデルを作るには、ベースモデルに分類ヘッドを足し、ラベル付きのデータで学習させます。これがファインチューニングです。
この部では、少ないデータで学習の流れを体験します。実際の運用では、もっと大量のデータを使います。
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 つの値を出す小さな層(分類ヘッド)が自動で付きます。
train_data = [
("この映画は本当に面白かった。", 1),
("最高の経験だった。", 1),
("とても感動的な話だった。", 1),
("サービスが良くて満足した。", 1),
("もう一度見たいと思う。", 1),
("退屈で途中で眠くなった。", 0),
("最悪の体験だった。", 0),
("期待外れだった。", 0),
("時間の無駄だった。", 0),
("二度と利用したくない。", 0),
]
# 1 = ポジティブ、0 = ネガティブ解説:1 件 1 文にラベルを付けた、小さなデータです。分かりやすさのための手作りデータです。実際は数百〜数万件を使います。
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に正解ラベルを入れます。学習時、これと比べて誤りを直します。
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を足すと速く、安定します。
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 件なので、過学習しやすいです。実データで学ぶと、もっと自然に判定できるようになります。
手作りデータの代わりに、公開データセットを使えます。例として、よく使われる日本語データを紹介します。使う前に、各データの利用条件を確認してください。
# 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 を、読み込んだデータセットに差し替えれば、同じ枠組みで学習できます。
- 分類ヘッドを足して、データで学習(ファインチューニング)できた。
- モデルカードの推奨ハイパーパラメータを使った。
- 同じ枠組みで、公開データセットに切り替えられる。
- 文の中の「人名・地名・組織名」を見つける(これを NER と呼ぶ)
NER(Named Entity Recognition)は、文の中の固有表現を見つける作業です。
例:「田中さんが東京で会議をした」→ 田中=人名、東京=地名。
NER は「トークン分類」の一種です。トークンごとにラベルを付けます。
1 つの単語が複数のトークンに分かれることがあります。そこで、先頭と続きを区別します。これを BIO タグと呼びます。
B-PER:人名の先頭(Begin)I-PER:人名の続き(Inside)B-LOC/I-LOC:地名B-ORG/I-ORG:組織名O:固有表現でない(Outside)
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 部)と違うのは、文全体ではなく、トークンごとにラベルを出す点です。
単語と、そのラベルのペアを用意します。ここでは分かち書き済み(単語ごとに分かれている)とします。
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"]),
]解説:データは少なめです。流れを体験するためです。実運用では、数百〜数千文を使います。
単語が複数のトークンに分かれたとき、ラベルを正しく割り当てます。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 の決まりです。
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)にします。
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 つの「範囲(スパン)」にまとめます。
LiquidAI は、このモデルで個人情報(PII)を見つけるデモを公開しています。
- デモ:pii-detection
- モデル:LiquidAI/LFM2.5-Encoder-350M-PII-Detector(https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M-PII-Detector)
- 対応:40 種類の PII、16 言語、11 ドメイン
このデモは、トークン分類モデルに正規表現を組み合わせて、精度を上げています。この部で学んだ「トークン分類」の、実用的な応用例です。
この PII モデルは公開されています(本稿執筆時点)。HF_TOKEN は不要です。ただし、モデルと一緒に補助コード(pii_hybrid_decode.py など)が配布されており、読み込みには trust_remote_code=True が必要です。
- トークン分類で、固有表現を見つける(NER)をやった。
- BIO タグで、先頭と続きを区別した。
- 単語とトークンのアラインメントを、標準的な方法でやった。
- 公式デモの PII 検出は、同じ仕組みの実用例である。
- これまでの技術を組み合わせて、2 つの応用を試す
- 残りの公式デモを紹介する
- 文ベクトル(第 2 部):文をベクトルにする
- ゼロショット分類(第 3 部):ベクトルの近さで分類する
- トークン分類(第 5 部):トークンごとに判定する
これらを組み合わせると、さまざまな応用が作れます。
ルールを文章で書き、テキストの「どの部分が、どのルールに触れそうか」を判定します。公式デモ 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は目安です。データで調整します。 - 公式デモは、この仕組みを学習させて精度を上げています。ベースモデルでの再現はあくまで簡易版です。
第 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 は、もっと本格的な生成をします。仕組みは次の通りです。
- 答えの部分を、すべて
[MASK]で埋める。 - モデルで、各
[MASK]の予測と、その確信度を出す。 - 確信度が高いものから順に、トークンを確定する。
- 残りの
[MASK]を、もう一度予測し直す。これを繰り返す。
これをマスク拡散(Masked Diffusion)と呼びます。
重要:ベースモデルのままでは、この生成はうまくいきません。ベースモデルは「質問をそのまま繰り返す(echo する)」傾向があるためです。デモは、この生成用に学習させた別モデル(LFM2.5-Encoder-350M-Diffusion)を使っています。
| デモ | すること | このチュートリアルの対応 |
|---|---|---|
| prompt-routing | ゼロショット分類 | 第 3 部 |
| policy-linting | ルールでテキスト検査 | 第 6 部 6-1 |
| spellchecker | スペル訂正 | (MLM の応用) |
| pii-detection | 個人情報の検出(NER) | 第 5 部 |
| masked-diffusion | テキスト生成 | 第 6 部 6-2 |
各デモは、ベースモデルにヘッドを足して学習させた、専用モデルを使っています。
各モデルのリンク:
- Prompt-Router: https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M-Prompt-Router
- Policy-Linter: https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M-Policy-Linter
- Spellchecker: https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M-Spellchecker
- PII-Detector: https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M-PII-Detector
- Diffusion: https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M-Diffusion
このチュートリアルで扱ったこと:
- モデルの基本的な使い方(空所埋め)
- 文ベクトルと、意味の類似度
- ゼロショット分類
- 分類のファインチューニング
- 固有表現抽出(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
このモデルは、軽くて多言語対応の、実用的なエンコーダーです。日本語のデータで、ぜひ自分のタスクを試してみてください。