LLM に画像生成のプロンプトを書かせるとき、自分の作品の売れ筋とサイト全体の売れ筋を 1 本のヒントにまとめて渡していたら、生成が自分寄りに偏っていく構造になっていました。
私は LLM に書かせたプロンプトで画像を作り、自分で選んだものをストックフォトのサイトに出しています。
プロンプトには画像の系統を 1 つ抽選して割り当て、毎週集めた売れ筋を英語の短いヒントとして添える作りです。
きっかけは「自分の売れ筋と全体の売れ筋を統合していると、私の好みに寄ってしまいそう」という違和感で、実装を読み直すと偏りが染み出す経路が 3 つ見つかりました。
自分の実績を生成の手がかりに戻している仕組みなら、偏りの入り口と、出どころで枠を分ける直し方がそのまま当てはまります。
混ぜたヒントが染み出す 3 つの経路
変更前の作りを、何がどこへ入るかで分けると次の 3 つになります。
| 経路 | 変更前の状態 | 起きること |
|---|---|---|
| ヒントの入り方 | ヒント 6 行のうち 4 行が自分の実績で、どの系統を引いても 6 行全部が入る | 市場由来の系統にも自分の売れ筋の色が届く |
| 系統の本数 | 系統 10 本のうち自分の実績由来が 7 本、市場の売れ筋由来が 3 本 | 抽選の時点で自分寄りの系統が多く引かれる |
| 実績の母数 | 自分の実績は、自分が作って選んだ範囲の中で売れたもの | 戻すほど、自分が選んだ範囲の中へ寄っていく |
1 つ目は、どの系統にも全部のヒントを入れていたことです。
光のエフェクトのような市場由来の系統を引いても、自分の作品の売れ筋を書いた 4 行が一緒に入っていました。
少数派の枠のはずが、多数派の色に染まっていたことになります。
2 つ目は系統の本数の差です。
毎週の売れ筋から選ぶ優先の系統は、ほかの系統の約 2 倍の頻度で引かれます。
優先の系統は自作 2 本と市場 2 本でしたが、土台の本数が 7 対 3 なので、生成回数はざっくり 2 対 1 弱で自作寄りでした(実測ではなく概算です)。
3 つ目がいちばん根の深い経路なので、節を分けて書きます。
自分の実績は「自分が選んだ範囲」の中での評価
自分の作品で売れたものは、市場全体から選ばれたものではありません。
私が作り、私が選んで出品した範囲の中で、買い手が選んだものです。
手元の数字では、出品 1,174 点のうちダウンロードが 1 回以上あったのは 44 点でした(2026 年 9 月 23 日時点)。
この 44 点を手がかりに戻すと、次に作る絵が 44 点に似ていき、似た絵がまた売れ筋に入ります。
自分の実績は自分が選んだ範囲の中の評価なので、生成に戻すと自己強化のループになります。
推薦システムの分野では、これと似た現象がフィードバックループとして研究されてきました。
論文 Degenerate Feedback Loops in Recommender Systems は、システムの判断が利用者の好みを変え、それが学習に返るフィードバックを変えると説明しています。
その結果として挙げられているのが、いわゆるエコーチェンバーやフィルターバブルです。
私の場合は、ループに乗っているのがモデルの学習ではなくプロンプトのヒントですが、形はよく似ていました。
スタイルを 2 本に分けなかった理由
私の仕組みでは、生成を 1 回回すたびに「スタイル」と呼ぶ設定の束を 1 つ選びます。
最初に浮かんだのは、このスタイルを自分の実績用と市場の売れ筋用の 2 本に分ける案でした。
ただしそうすると、どちらを何回回すかを毎回私が決めることになります。
比率の判断が人の手に戻ると、その判断が止まった時点で運用も止まり、中身が古くなります。
そこでスタイルは 1 本のまま、中で出どころごとに層を分け、比率はコードに守らせることにしました。
出どころを先に選び、比率をコードに守らせる
直し方の中心は、系統を抽選する前に「どちらの出どころから引くか」を先に決めることです。
出どころが決まったら、その出どころに属する系統だけで抽選します。
直近 20 世代でいちばん足りない方を選ぶ
出どころの選び方は、直近 20 世代の履歴を数え、目標の比率に対していちばん足りていない方を取る形にしました。
不足分は「目標比率 ×(数えた世代 + 今回の 1)− 実績」で計算し、同点のときは比率で重みを付けて抽選します。
比率そのものは設定値に置き、最初は半々にしました。
実際のコードから要点だけを抜き出すと、次のようになります。
// ...(比率の読み込みは省略)
const count = new Map<string, number>();
let n = 0;
for (let i = history.length - 1; i >= 0 && n < ORIGIN_WINDOW; i--) {
const l = (history[i].lanes ?? []).find((x) => originOf.has(x));
if (!l) continue;
const o = originOf.get(l)!;
count.set(o, (count.get(o) ?? 0) + 1);
n++;
}
// 不足分 = 目標比率 ×(数えた世代 + 今回)− 実績
const deficit = (o: string) => (share.get(o)! / total) * (n + 1) - (count.get(o) ?? 0);
// ...(不足分が最大の出どころを選ぶ。同点は比率で重み付き抽選)
実コードを切り出したシミュレーションでは、履歴がない状態から 1,000 世代回すと、自作 500・市場 500 にきれいに分かれました。
同じ出どころの中では優先の系統が約 2 倍で、市場側は 203・195・102、自作側は 110・109・その他 56〜57 でした。
自作寄りの履歴が 20 世代(自作 14・市場 6)ある状態から始めると、市場が 5 回続いて追いつき、そのあとは交互に戻ります。
一方で副作用もあります。
市場由来の系統は 3 本しかないので、1 本あたりの頻度は自作側の 2 倍強になり、光のエフェクト系だけで全生成の約 2 割を占めます(シミュレーション値)。
絞った集合で間隔を数えると倍に見える
実装の途中で、出どころとは別の仕組みに落とし穴がありました。
系統の抽選には、最近使った系統を避け、優先の系統だけは一巡の半分で再登場を許すルールがあります。
その判定に使う「最後に使ってから何世代前か」を、出どころで絞った系統の集合のまま数えていました。
すると間に他方の出どころの世代が挟まるので、間隔が約 2 倍に膨らみ、半分で再登場させる判定が緩みます。
絞った集合で間隔を数えるときは、その集合の系統を含む世代だけを数える必要があります。
直したのは数え方だけで、どの系統を除外するかの順序は変わらないため、同じ関数を使うほかのスタイルには影響しません。
ヒントも引いた出どころの分だけ渡す
抽選を分けても、ヒントが混ざったままでは染み出しは止まりません。
そこでヒントを「自作」「市場」「共通」の 3 つに分け、プロンプトには引いた出どころの分と共通の分だけを入れるようにしました。
ヒントの見出しにも出どころを書き、自作の分は累計の実績、市場の分は直近数週の売れ筋だと LLM に分かるようにしています。
混ぜた 1 本のヒントを全部の枠に入れると、少数派の枠にも多数派の色が染み出します。
枠を分けるなら、手がかりも同じ境界で分けるところまでがセットです。
外の実績は 1 週分で判断しない
市場側にはもう 1 つ癖がありました。
私が見ている売れ筋のページは、直近 1 週間分しか表示されません。
1 週だけの顔ぶれでヒントや優先の系統を決めると、週ごとに大きく振れます。
そこで毎週の記録を残し、直近 3 回分を合算するようにしました(28 日より古い回は入れません)。
売れ筋のページを読めなかった週は、前の回の記録だけで合算し、警告を出します。
ダミーの記録を 4 回分置いて試し、古い回が除かれることと、週ごとに数えられることを確かめました。
自作側は累計の数字なので、合算はしていません。
ヒントの文面も今週の観測だけで書かず、前 2 回の記録を読んで、続けて出ている種類を先に、今週だけの新顔を後に書く運用にしています。
ここまでの変更を、変更前と並べると次のとおりです。
| 項目 | 変更前 | 変更後 |
|---|---|---|
| 系統の抽選 | 10 本から直接抽選 | 出どころを先に選び、その出どころの系統だけで抽選 |
| 比率 | 本数の差で自作寄り(概算で 2 対 1 弱) | 設定値の比率をコードが守る(最初は半々) |
| ヒント | 6 行全部をどの系統にも入れる | 引いた出どころの分と共通の分だけ |
| 市場の観測 | その週の 1 回分 | 直近 3 回分を合算 |
選別の段にも同じループがあった
層別にしたあとの生成を集計すると、最後の選別の段にも同じ形のループが残っていることが見えてきました。
私の仕組みでは、生成した絵を最後に私が選び、その手前で「好みモデル」が好みに合わなさそうな絵を自動で却下します。
好みモデルは、私の過去の承認と却下で学習した小さな分類器です。
10 月 3 日に、層別にしてから生成した分を集計したところ、承認になった割合は市場由来が 12.6%(190 枚中 24 枚)、自作由来が 2.3%(172 枚中 4 枚)でした。
判定の材料はまだ 1 日分ほどなので、参考値として見ています。
同じ集計で、好みモデルの承認確率が 0.30 未満になった絵は、市場由来で 45%(190 枚中 86 枚)、自作由来で 13%(172 枚中 22 枚)でした。
私が承認した市場由来の絵でも、承認確率がいちばん低いものは 0.30〜0.40 の間にありました。
自動で却下する線を 0.15 から 0.30 へ上げることも考えていましたが、そうすると市場由来の絵の半分近くを一度も見ずに捨てることになります。
自分の判断で学習した選別のモデルも、自分の実績の一種です。
そこで自動で却下する線は 0.15 未満のままにし、0.15〜0.30 の絵は私が流し見て拾う置き場に回しました。
あわせて出どころの比率を、自作 3・市場 7 に変えました。
比率が設定値 1 か所にまとまっていたので、変更はその値を書き換えるだけで済みました。
機械学習システムの技術的負債を論じた論文 Hidden Technical Debt in Machine Learning Systems も、見えにくいフィードバックループを機械学習に特有のリスクの 1 つに挙げています。
好みモデルを作ったときに、精度の数字を疑って測り直した経緯は別の記事に残しています。
まだ分かっていないこと
売れ行きへの効果は、まだ確かめていません。
層別の前と後で、全体の承認率が上がったかどうかも比べていません。
上の承認率は出どころどうしの比較で、層別そのものの効果を示す数字ではありません。
比率を自作 3・市場 7 にしたことで、今度は市場側の優先の系統 2 本が、それぞれ全生成の約 28% を占めるようになりました(シミュレーション値)。
偏りの向きが市場側の少ない系統に移っただけかもしれず、そこは生成履歴に記録し始めた出どころで数えて確かめます。
最後に
自分の実績は、自分が選んだ範囲の中での評価です。
私はそれを生成の手がかりに戻すにあたって、出どころで枠を分けて比率をコードに守らせ、手がかりも引いた枠の出どころの分だけを渡す形に落ち着きました。
同じループは、自分の判断で学習した選別のモデルにも潜んでいます。
手がかりを戻す経路を見つけたら、それが誰の選択を通った実績なのかを最初に確かめると、偏りの入り口が見えます。
以上です。








コメントを残す