生成 AI の文章に「ウォーターマークが入っている」と聞くと、文章のどこかに見えない文字列が埋め込まれているように思えます。しかし、LLM のテキストウォーターマークには、生成するトークンの確率をわずかに偏らせ、その偏りを文章全体から統計的に検出する方式があります。この仕組みと、その先にある生成 AI の学習データへの影響は以前に扱いました[1]。
この記事では話をウォーターマークそのものに絞ります。Kirchenbauer らが提案した方式を題材に、Green トークンと Red トークン、ロジットへのバイアス、z 値による検出までを確認し、最後に Hugging Face Transformers で実際に生成と検出を試します。
ウォーターマークというのは「文章を読んで AI らしさを推測する技術」ではありません。生成時に統計的な信号を埋め込み、同じ規則を知っている検出器が後からその偏りを確認する技術です。
AI 文検出とウォーターマークは別の技術
AI 文検出器とウォーターマークは、どちらも「AI が生成した文章か」を扱うため混同されやすいですが、前提が違います。
一般的な AI 文検出では、すでに存在する文章から特徴を調べ、AI 生成らしさを推定します。一方、ここで扱うウォーターマークは、文章を生成する段階から処理に介入します。
Kirchenbauer らの方式では、次に生成できるトークンの一部を Green トークンとして選び、そのトークンが少しだけ選ばれやすくなるように確率分布を調整します。人間が文章を読んでも通常は分かりませんが、十分な長さの文章を調べると Green トークンが偶然より多く現れるため、統計的に検出できます[2]。
つまり、任意の AI 生成文を後から万能に見分ける仕組みではありません。ウォーターマークを付ける生成処理と、それに対応する検出処理が対になっています。
LLM のウォーターマークは生成時に埋め込む
次に出すトークンは確率分布から選ばれる
LLM は文章を一度に完成させるのではなく、直前までの文脈から次のトークン候補に点数を付け、1 トークンずつ生成します。
この点数がロジットです。たとえば次の 3 候補があるとします。
| トークン | ロジット |
|---|---|
cat |
2.0 |
dog |
1.7 |
bird |
0.8 |
ロジットはそのまま確率ではありません。Softmax を通すことで、各トークンを選ぶ確率へ変換されます。
ウォーターマークは、この確率分布が確定する直前に介入します。
語彙を Green と Red に分ける
Kirchenbauer 方式では、その時点で選択可能な語彙を擬似乱数的に 2 群へ分けます。
- Green: ウォーターマークとして少し優遇するトークン
- Red: それ以外のトークン
どのトークンが Green になるかを固定してしまうわけではありません。直前のトークンなどの文脈を使って Green の集合を決めるため、生成位置ごとに対象が変わります[2]。
Hugging Face Transformers の WatermarkingConfig では、Green にする語彙の割合を greenlist_ratio で設定します。現在の既定値は 0.25 です。また、Green トークンのロジットへ加える値は bias で、既定値は 2.0 です[3]。
Green トークンのロジットに bias を加える
たとえば Green に選ばれた dog のロジットへ 2.0 を加えると、先ほどの例は次のようになります。
| トークン | 元のロジット | ウォーターマーク適用後 |
|---|---|---|
cat |
2.0 | 2.0 |
dog |
1.7 | 3.7 |
bird |
0.8 | 0.8 |
dog を強制的に出力するわけではありません。選ばれる可能性を高くしているだけです。
この操作を生成中に繰り返すと、完成した文章には Green トークンが通常より多く含まれるようになります。文章中に特定の秘密文字列を差し込むのではなく、トークン選択の統計的な偏りそのものがウォーターマークになります。
ウォーターマークを統計的に検出する
Green トークンの出現数を数える
検出側では、生成時と同じ規則を使って各位置の Green 集合を再現します。
生成したトークン数を T、Green トークンの割合を γ、実際に観測した Green トークン数を G とします。
ウォーターマークが存在しないと仮定すると、Green トークンは平均して次の個数だけ現れるはずです。
E[G] = \gamma T
たとえば γ = 0.25 で 200 トークンを調べるなら、ウォーターマークがなければ Green は平均 50 トークン程度です。
ところが生成時に Green を継続的に優遇していれば、観測される G はこの期待値より大きくなります。
z 値で偶然かどうかを判定する
Green が期待値より多かったとしても、それだけではウォーターマークとは断定できません。通常の文章でも偶然 Green が多くなることがあるからです。
そこで、期待値からどの程度離れているかを z 値で測ります。
z = \frac{G - \gamma T}{\sqrt{T\gamma(1-\gamma)}}
z 値が大きいほど、「ウォーターマークがない」という仮定では説明しにくい偏りになります。
Transformers の WatermarkDetector は、この判定に使う z_threshold の既定値を 3.0 としています。閾値を高くすると検出感度は下がり、低くすると感度は上がります[3]。
ここで重要なのは、1 語や 1 文を見て判定しているわけではないことです。ある程度のトークン数を集め、その全体に残った偏りを統計的に判定しています。
Transformers でウォーターマークを試す
実行環境を用意する
Python 3.10 以上の仮想環境を用意し、PyTorch と Transformers をインストールします。
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install torch transformers
インストール後は、実際に使用したバージョンを記録しておきます。Transformers の API は更新されるため、実験結果と一緒に残しておくと再現しやすくなります。
python - <<'PY'
import torch
import transformers
print("torch:", torch.__version__)
print("transformers:", transformers.__version__)
PY
この実験では openai-community/gpt2 を使います。初回実行時には Hugging Face からモデルを取得するため、インターネット接続が必要です。CPU だけでも実行できます。
ウォーターマークあり・なしを生成する
次のコードでは、同じプロンプトから通常の文章とウォーターマーク付き文章を生成します。
実験で信号を確認しやすくするため、bias=2.5、seeding_scheme="selfhash" を指定しています。これは Transformers 公式ドキュメントの例でも使われている設定です[3]。
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
WatermarkDetector,
WatermarkingConfig,
)
model_id = "openai-community/gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id)
model.eval()
prompt = (
"Artificial intelligence systems are increasingly used "
"to generate text for software documentation and technical writing."
)
inputs = tokenizer(prompt, return_tensors="pt")
input_length = inputs["input_ids"].shape[-1]
watermarking_config = WatermarkingConfig(
greenlist_ratio=0.25,
bias=2.5,
seeding_scheme="selfhash",
)
generation_options = {
"max_new_tokens": 160,
"do_sample": False,
"pad_token_id": tokenizer.eos_token_id,
}
plain_ids = model.generate(
**inputs,
**generation_options,
)
watermarked_ids = model.generate(
**inputs,
watermarking_config=watermarking_config,
**generation_options,
)
plain_text = tokenizer.decode(
plain_ids[0],
skip_special_tokens=True,
)
watermarked_text = tokenizer.decode(
watermarked_ids[0],
skip_special_tokens=True,
)
print("=== plain ===")
print(plain_text)
print()
print("=== watermarked ===")
print(watermarked_text)
do_sample=False にしているため、同じモデルと設定であれば結果を比較しやすくなります。ウォーターマーク側では Green トークンのロジットが変更されるため、同じ入力でも生成結果が変わります。
WatermarkDetector で検出する
続けて、生成したトークンを WatermarkDetector に渡します。
検出器には、生成時と同じ watermarking_config を渡す必要があります。Green 集合を作った規則を再現できなければ、同じ統計検定ができないためです。Transformers の公式ドキュメントでも、生成時と検出時に同じウォーターマーク設定が必要だと明記されています[3]。
また、プロンプト部分はウォーターマークを付けて生成したトークンではありません。検出対象から除外します。
plain_completion_ids = plain_ids[:, input_length:]
watermarked_completion_ids = watermarked_ids[:, input_length:]
detector = WatermarkDetector(
model_config=model.config,
device="cpu",
watermarking_config=watermarking_config,
)
plain_result = detector(
plain_completion_ids,
return_dict=True,
)
watermarked_result = detector(
watermarked_completion_ids,
return_dict=True,
)
print("plain:", plain_result.prediction)
print("watermarked:", watermarked_result.prediction)
期待する確認点は、通常生成側が False、ウォーターマーク付き生成側が True になることです。ただし、これは統計的な判定なので、文章長や生成内容、設定によって結果は変わります。
1 回の短い出力だけを見て方式全体の精度を評価するのではなく、生成トークン数や bias を変えながら挙動を見るほうが仕組みを理解しやすくなります。
ウォーターマークが弱くなる条件
短い文章では判定しにくい
この方式は文章全体の統計的な偏りを見るため、観測するトークンが少ないほど判断材料も少なくなります。
たとえば γ = 0.25 のとき、20 トークン中に Green が 7 個あったとしても、期待値 5 個との差は小さく、偶然でも起こり得ます。200 トークン、500 トークンと観測量が増えれば、継続的な偏りを通常の揺らぎから区別しやすくなります。
そのため、ウォーターマーク検出結果を見るときは「何文字あるか」より、実際に検定へ使われたトークン数を意識する必要があります。
編集や言い換えで信号は薄くなる
ウォーターマークは暗号学的な署名ではありません。文章を書き換えればトークン列も変わるため、生成時に作られた統計的な偏りは弱くなります。
Kirchenbauer らは後続研究で、人間による書き換え、ウォーターマークを付けない LLM による言い換え、手書き文章への混入などを評価しています。強い言い換えでは信号が薄くなる一方、十分な長さがあれば元の n-gram などが残り、検出できる場合があると報告しています[4]。
したがって、「少し編集したら必ず消える」「一度付いたら絶対に消えない」のどちらも正確ではありません。検出可能性は、方式、文章長、変更量、閾値によって変わります。
ウォーターマークを外したいなら生成経路から考える
ウォーターマークや AI 由来のメタデータを除去することを目的とした watermarks-remover のような実装も存在します。このプロジェクトは不可視 Unicode や C2PA などに加え、統計的なテキストウォーターマークについては文章の書き換えを利用する処理を扱っています[5]。
ただし、生成した後でウォーターマークを除去する運用を最初から前提にするなら、設計を一段前へ戻したほうが単純です。生成経路を自分で管理する必要があるなら、最初からオープンウェイトモデルを自前の推論環境で使い、モデル、推論コード、生成設定を管理する方法があります[6]。
オープンウェイトモデルなら自動的にウォーターマークがなくなる、という意味ではありません。実際、この記事で使った GPT-2 にも Transformers 側からウォーターマーク処理を追加できました。違いは、ウォーターマークを付けるかどうかを含めて生成処理を自分で決められることです。
まとめ
LLM のテキストウォーターマークは、完成した文章の中へ固定文字列を隠す仕組みとは限りません。Kirchenbauer 方式では、生成時に Green トークンを少し優遇し、文章全体へ統計的な偏りを作ります。その偏りを生成時と同じ規則で再現し、z 値によって検出します。
Hugging Face Transformers では WatermarkingConfig と WatermarkDetector が用意されているため、通常生成とウォーターマーク付き生成を同じモデルで比較できます。仕組みを理解するには、検出器だけを試すより、生成時にロジットがどのように変更されるかから追うほうが明確です。
そして、ウォーターマークを後処理で消すこと自体を運用要件にするなら、除去方法を積み重ねるより、最初から生成経路を管理できるモデルと推論環境を選ぶほうが設計として単純です。
参考文献
- id774, 生成 AI が書き換えた世界を、次の AI が学ぶ(2026-08-18). https://blog.id774.net/entry/2026/08/18/5505/
- John Kirchenbauer, Jonas Geiping, Yuxin Wen, Jonathan Katz, Ian Miers, Tom Goldstein, A Watermark for Large Language Models(2023-01-24). https://arxiv.org/abs/2301.10226
- Hugging Face, Utilities for Generation(2026-08-18). https://huggingface.co/docs/transformers/internal/generation_utils
- John Kirchenbauer, Jonas Geiping, Yuxin Wen, Manli Shu, Khalid Saifullah, Kezhi Kong, Kasun Fernando, Aniruddha Saha, Micah Goldblum, Tom Goldstein, On the Reliability of Watermarks for Large Language Models(2023-06-07). https://arxiv.org/abs/2306.04634
- Guillaume Meyer, watermarks-remover(2026-08-11). https://github.com/guillaumemeyer/watermarks-remover
- id774, ウォーターマークを消す最良の方法(2026-08-17). https://sizu.me/id774/posts/1sd7wm83fdd8