2026年8月30日日曜日

LLMは「答え」を生成しなくてもゲームを操作できるのか - 隠れ層からコントローラ操作を直接取り出す実験

 

☆ このブログ記事はチャッピー自動生成です ☆

LLMにゲームを操作させるなら、普通は操作を文字列として生成させる。例えばSnakeなら、ゲームの状態をプロンプトとして与えて、

{"action":"LEFT"}

のような回答を生成させる。それをプログラム側でパースし、実際のゲーム操作へ変換する。処理としては、

ゲーム状態
   ↓
LLMへ入力
   ↓
prefill
   ↓
decode
   ↓
"LEFT" を生成
   ↓
parse
   ↓
コントローラ操作

となる。

LLMエージェントとしては、ごく普通の構成だ。ただ、ゲームのコントローラを動かしたいだけだと考えると、少し回りくどい。LLM内部ではすでにゲーム状態を処理し、

次は左へ行く

という判断につながる何らかの内部状態を作っているはずだ。それをいったん、

"LEFT"

という言語へ変換し、外側のプログラムでもう一度、

LEFTボタン

へ戻している。つまり、

内部表現
 ↓
言語
 ↓
操作

という経路を通っている。では、この真ん中の「言語」は本当に必要なのだろうか。今回試したかったのはそこだった。


decodeを通さずに操作を取り出したい

目標は単純で、

LLMに操作名を生成させず、prefill後の内部状態から直接コントローラ操作を取り出す

ことである。つまり、

ゲーム状態
   ↓
LLM prefill
   ↓
hidden state
   ↓
LEFT / RIGHT / UP / DOWN

としたい。これができれば、LLMは一文字も生成しなくてよい。

ゲームの状態を見せる。

Transformerを通す。

途中の内部状態を読む。

そのままボタンを押す。

LLMを「テキストを生成する装置」としてではなく、

入力から行動に必要な内部表現を作る巨大な計算器

として使うことになる。


decodeを省きたい理由は、速度だけではない

もちろんdecodeを省略できれば、その分の計算コストは減る。しかし今回興味があるのは、単純な高速化だけではない。ゲームには時間の流れがある。例えばSnakeなら、

t=0
Snake: (3, 4)
Food:  (8, 4)

t=1
Snake: (4, 4)
Food:  (8, 4)

t=2
Snake: (5, 4)
Food:  (8, 4)

t=3
...

というように状態が連続して変化する。これを毎tick独立した問い合わせとしてLLMへ投げるのではなく、ゲームの進行そのものをコンテキストへ追加し続けることもできる。

State(t=0)
    ↓
prefill

State(t=1) を追加
    ↓
incremental prefill

State(t=2) を追加
    ↓
incremental prefill

State(t=3) を追加
    ↓
...

KV Cacheを維持したまま、ゲームの時系列をLLMへ読み込ませ続ける。ここで一つ仮説がある。

ゲームの状況が時間とともに変化すれば、それに対応してLLMの隠れ層も変化していき、その変化の中に「今どの操作をすべきか」という情報が現れるのではないか。

イメージとしては、

game state:
S0 → S1 → S2 → S3 → S4

hidden state:
h0 → h1 → h2 → h3 → h4

という対応である。

そして、

h2 では RIGHT
h4 では LEFT

のように、その時点の内部状態から操作を読めるかもしれない。さらに言えば、絶対的なhidden stateだけではなく、

h(t) - h(t-1)

のような変化量に操作情報が出る可能性もある。この「時系列に沿って変化する内部状態」を使いたいというのが、decodeをできるだけ避けたいもう一つの理由だった。


decodeしてrollbackする方法もある

もちろん、ゲームの時系列をprefillし続けながら操作を取り出す方法は他にもある。例えば、ある時点で推論状態をcheckpointして、

State t
  ↓
prefill
  ↓
checkpoint
  ↓
数token decode
  ↓
{"action":"LEFT"}
  ↓
actionを取得
  ↓
rollback

とすることもできる。自前の推論エンジンを持っていれば、このような処理は比較的自由にできる。ゲームの履歴やKV Cacheは維持しつつ、操作が必要な瞬間だけ一時的にdecodeする。そして操作を取得したら、decode前の状態へ戻る。これはかなり現実的な方法だと思う。LM Headも既存のものをそのまま使える。モデルを追加学習する必要もない。

ただし、毎tickこれを行うとなると、それなりにコストが高い。


actionが欲しいだけなのに文字列を作る?

ゲームが1秒に何回も進行する場合、

checkpoint
↓
decode
↓
parse
↓
rollback

を何度も繰り返すことになる。しかも必要なのはせいぜい、

LEFT

という数bit相当の情報である。それを取得するために、

{
  "action":
  "LEFT"
}

という文字列を何tokenも生成するのは、どう考えても贅沢だ。もちろんプロンプトを工夫して、

L

の1 tokenだけ出させることもできる。しかしそれでも、

  • Transformerのdecode step

  • LM Head

  • 全語彙へのlogits計算

  • sampling

  • KVの更新

  • rollback

などは残る。欲しいものが5種類程度のactionなら、もっと直接取り出せないだろうか。


decodeをどこまで削れるか

実際、actionを取得する方法には何段階かある。

1. JSONを最後まで生成する

2. "LEFT" のような操作名だけ生成する

3. L/R/U/Dなど1 tokenだけ生成する

4. LM Headのlogitsを直接読む

5. hidden stateからactionを直接分類する

6. hidden stateの時間変化からactionを読む

下へ行くほどLanguage Modelとしてのdecodeから離れていく。

今回試しているのは5番目である。

LM Headすら通さず、Transformer途中のhidden stateから操作を取り出せるか。


LLMの「脳」に電極を刺してみる

今回はGemma 4 E2Bを llama.cpp で動かした。そして通常なら外から見ることのない計算グラフ途中のtensorを観測した。取得したのは、

llama_cpp_graph_tensor:l_out-17

というtensorで、1536次元のvectorが得られる。概念的には、

ゲーム状態
   ↓
Transformer
   ↓
Layer ...
   ↓
hidden state ─────→ ここを読む
   ↓
Layer ...
   ↓
LM Head
   ↓
token

という形になる。

モデル本体は変更していない。

weightを書き換えていない。

LoRAも入れていない。

新しいheadも追加していない。

既存モデルを普通に動かし、その途中を横から観測しているだけである。

自分の中では、これはLLMの「脳」に電極を刺すようなイメージに近い。新しい回路を埋め込むのではなく、すでに流れている信号を外から読む。


1536次元からコントローラのボタンを読む

当然ながら、1536個のfloatを眺めても何をしたいのかは分からない。そこで外側に小さなdecoderを置く。今回は多項ロジスティック回帰を使った。つまり、

1536次元 hidden state
          ↓
      linear probe
          ↓
LEFT / RIGHT / UP / DOWN / NONE

という非常に単純な分類器である。意図的にlinear probeを使っている。複雑なニューラルネットを外側に置いてしまうと、

そのdecoder自身がゲームを理解しているのでは?

という問題が出てくる。今回は、

LLM内部に、すでに操作を読み出せる情報が存在しているか

を見たい。なのでdecoderはなるべく弱くした。


教師データはLLM自身から作る

では、hidden stateに何を正解として学習させるのか。今回は人間がSnakeの最適手を教えたわけではない。学習データを作る段階だけは、通常通りLLMをdecodeする。例えばLLMが、

{"action":"RIGHT"}

と出したとする。

そのとき、decode前に取得しておいたhidden stateに、

RIGHT

というラベルを付ける。構造としては、

                 ┌── hidden state ──→ 学習データ
ゲーム状態 → LLM
                 └── decode → RIGHT ─→ 教師ラベル

となる。つまり学習しているのは、

このゲーム状態ではRIGHTが正しい

というpolicyではない。そうではなく、

このLLMがこの内部状態から最終的にどの操作をdecodeしようとしているか

を予測している。最初は通常decodeを教師として使い、その後でdecodeを取り外せないか試すわけだ。


「次tokenを読んでいるだけ」ではないのか

hidden stateからRIGHTが当たったとしても、

単に次に "RIGHT" tokenが出ることを予測しているだけでは?

という疑問は当然ある。それならactionを読んでいるというより、next-token predictionを横から盗み見ているだけになる。そこで、同じ操作について複数の表記を用意した。

例えば右移動なら、

D
Right
右

左移動なら、

A
Left
左

とする。

ゲーム内部ではすべて共通actionへ正規化する。

D
Right
右
   ↓
RIGHT

こうすることで、特定の文字列だけをlinear probeが拾う可能性を多少減らせる。ただし、これだけでは十分ではない。よりちゃんと検証するなら、

WASD + Englishで学習
          ↓
日本語表記だけで評価

のようなVocabulary Holdoutも必要になる。もしこれでもactionを読めれば、単なるtoken予測より一段抽象化された情報を拾っている可能性が高くなる。


本気で作るなら専用Action Headの方が正しいと思う

ここまで「モデルを改造しない」ことにこだわってきたが、本当に性能を求めるなら、おそらく今回の方法が最終形ではない。

自然なのは、どこかのhidden layerから専用のaction headを分岐させることだと思う。

Transformer
     │
     ├── LM Head → language
     │
     └── Action Head → controller

例えば、

hidden state
    ↓
small MLP
    ↓
action logits

とする。

本体をfreezeしてheadだけ学習してもよい。LoRAなどで内部表現そのものをaction向けに調整する方法もある。実用的なVLAやpolicy modelを作るなら、たぶんこちらが本筋になる。ただ、それをやってしまうと、

actionを出すようにモデルを改造したからactionが出た

という話になる。今回まず知りたかったのは違う。既存LLMを一切変更しなくても、内部にはすでにactionとして利用できる情報が流れているのか。だから今回は外からhidden stateを観測するだけにした。

まず電極で読んでみて、それで面白そうなら次に専用の回路を付ければよい。


まずSnakeで試す

最初に使ったのはSnakeだった。ゲームの盤面、Snakeの位置、Foodの位置、現在の進行方向などをテキストでLLMへ与える。通常通りdecodeして操作を取得すると同時に、l_out-17 のhidden stateも保存する。そこからlinear probeを学習した。未使用seedで評価すると、一致率は、

81.25%

だった。ただし、この値だけを見ると誤解しやすい。Snakeでは直進、つまり NONE がかなり多かった。多数派クラスを選び続けても高い精度になってしまうため、81.25%そのものを強い結果とは言えない。

この段階で確認できたのは、

decode前のhidden stateに、最終的な操作と相関する信号が含まれている

というところまでだ。次はactionの分布を均等にしたdatasetで評価する必要がある。


ブロック崩しでも同じ場所を読んでみる

Snakeだけでは、そのゲーム特有の特徴を拾っている可能性がある。

そこでブロック崩し(Breakout)でも試した。

操作は、

LEFT
RIGHT
NONE

の3種類。使うモデルは同じGemma 4 E2B。観測位置も同じ l_out-17

そこから同じ1536次元vectorを取得する。

Breakout専用のlinear probeを学習して未使用episodeで評価すると、

70.0%

だった。

多数派baselineは66.7%。

大きな差ではないが、少なくともSnake以外のゲームでも同じ場所から操作に関連する信号は取れた。


SnakeとBreakoutで同じdecoderを使えるか

次に試したのは、ゲームをまたいでactionが共有できるかである。

SnakeにもLEFTがある。

BreakoutにもLEFTがある。

そこで両者の操作を、

LEFT
RIGHT
UP
DOWN
NONE

という共通actionへ正規化した。probeにはgame IDを教えない。入力は純粋な1536次元hidden stateだけ。

そこから1つのlinear probeでactionを分類する。結果は、

評価対象Accuracy
Snake + Breakout77.27%
Snake80.0%
Breakout70.0%

だった。少なくともSnakeとBreakoutについては、

同一モデルの同一観測地点から、一つのlinear decoderである程度共通の操作を取り出せた。



では未知のゲームにも使えるのか

ここまで来ると、

LLM内部にはLEFTやRIGHTのような汎用的なaction表現があるのではないか

と期待したくなる。そこでTetrisを追加した。

Snake + Breakoutだけで学習したprobeを、そのままTetrisへ適用する。再学習はしない。

ゲーム単位ではzero-shotになる。結果は、

7.69%

だった。多数派baselineよりも悪い。


「LEFTニューロン」を見つけたわけではない

この結果を見る限り、

この方向 = LEFT
この方向 = RIGHT

という普遍的な単純軸がhidden spaceに存在しているのを見つけた、とはいえなそうだ。

hidden stateには、

  • ゲーム状態

  • プロンプト構造

  • 出力形式

  • 選択可能なaction

  • 次token情報

  • ゲーム固有の判断

などが混ざっている。SnakeとBreakoutでは、その中から共通して利用できる方向をlinear probeが拾えた。しかしTetrisでは対応関係が崩れた


さらに先にはVLMがある

今はゲーム状態をテキストとして与えている。しかし、この考え方はVLMでもできるはずだ。最終的にはゲーム画面そのものを入力して、

screen / video frame
        ↓
       VLM
        ↓
hidden representation
        ↓
      action

としたい。普通にVLMを使えば、

画面
 ↓
VLM
 ↓
"The ball is moving left, so move the paddle left."
 ↓
parse
 ↓
LEFT

のような構成になる。しかし必要なのがcontroller actionだけなら、この文章は本来不要である。VLMが内部で、

  • 自機の位置

  • ボールの位置

  • 敵の位置

  • 移動方向

  • 危険

  • 次の行動

などを表現できているなら、その途中からactionだけ抜けばよい。究極的には、

video
  ↓
VLM
  ↓
latent dynamics
  ↓
controller

で動かせるかもしれない。ゲームであれば、これはかなりVLAに近い構造になる。


今回の実験で分かったこと

今回やったのは、まだかなり小さな実験である。

Gemma 4 E2Bをそのまま動かし、llama.cpp の途中から1536次元hidden stateを取得した。

モデル自体は変更せず、外側に単純なlinear probeを付けた。

SnakeやBreakoutでは、LLM自身のdecode結果と相関するactionをある程度読み取れた。

SnakeとBreakoutを混ぜた共通decoderもある程度機能した。

一方で、そのdecoderを未知のTetrisへzero-shotで持っていくと完全に失敗した。

なので現時点では、

LLM内部に普遍的なLEFT/RIGHT表現を発見した

とは到底言えない。

それでも、

LLMが言語として答えを出す前のhidden stateに、外部操作へ利用できる情報が存在している

という感触は得られた。

2026年8月23日日曜日

novelmat - LLMに文章を書かせず、「次に何を書きたいか」だけ覗いてテキストフィルタを作った

☆ この blog は チャッピー生成です☆


OCR文章の不要な改行を消すために、LLMへ文章を書き直させるのをやめた。
代わりにやったのは、数十トークン読ませて、logitsを数個盗むことだった——



OCRすると「そこは改行じゃないだろ」という場所で切れる

書籍をOCRすると、こんなテキストができることがある。

吾輩は猫であ
る。
名前はまだない。

元の紙面では単に行が折り返されていただけなのに、OCR結果には改行文字が入っている。

欲しいのは当然、

吾輩は猫である。
名前はまだない。

である。

一見すると簡単な問題に見える。

ところが、「日本語のどこが文章の区切りなのか」を機械的に判断しようとすると、意外と面倒だ。

形態素解析器を使う方法も考えられる。品詞を見たり、文末表現を判定したり、句読点や括弧を考慮したりすることもできるだろう。

しかし小説には、

  • 会話文

  • 句読点の省略

  • 三点リーダ

  • 括弧

  • 倒置

  • 文の途中でのページ送り

  • OCRによる文字欠落

などが普通に出てくる。

「句点なら文末」「助詞ならまだ続く」といった規則だけでは、すぐに例外が増えていく。

もちろん、きちんと日本語解析の仕組みを作れば解決できる問題ではある。

ただし、改行を1個消すために、日本語の文境界判定器を一から育てたいわけではない。

そこで考えた。

LLMはすでに大量の日本語を学習している。

だったら、その能力をもっと雑に奪えないだろうか。


最初は、「普通に」ChatGPTやQwenに直してもらっていた

実は、最初からこんな仕組みを作ろうと思っていたわけではない。

当初の運用はもっと単純だった。

OCRしたテキストファイルをQwenやChatGPTなどのLLMへ渡して、

OCRによって入った不要な改行を取り除いてください。文章そのものは変更しないでください。

と頼んでいた。

これで済むなら、それが一番楽である。

実際、短い文章ではかなりうまくいく。

しかし、ある程度まとまったテキストを処理させると、だんだん困ったことが起きた。

たとえば、

  • 頼んでいない誤字修正をする

  • 句読点や表記を勝手に整える

  • 文体を微妙に変える

  • 消してほしい改行をいくつか残す

  • 長い文章になると途中の作業が抜ける

といったことがある。

プロンプトで、

改行以外は絶対に変更しない

と強く指定しても、完全には安定しなかった。

LLMからすれば、文章全体を一度生成し直す以上、これはある意味当然でもある。

こちらが欲しいのは、

この改行を残す

か、

この改行を消す

かだけなのに、LLMには文章全体を書き直す権限を渡している。

かなり大げさな仕事の頼み方だった。

特に困るのは、結果が一見まともに見えることだ。

数千文字の文章のうち一か所だけ書き換えられていても、人間が目視で発見するのは難しい。

逆に、不要な改行が一つ残っていても同じである。

生成系LLMへ全文編集を任せる方法は便利ではあるものの、

「入力の文字列をほぼ完全に保存しながら、指定した改行だけを機械的に除去する」

という用途では、思ったより扱いづらかった。

そこで発想を逆にした。

そもそもLLMに文章を書き直させなければいいのではないか。

文章を生成させるから、余計な変更も作業漏れも起きる。

でもLLMが、

この改行は不自然だ

と判断する能力そのものは使いたい。

だったら完成した文章を返してもらうのではなく、

LLMがその場所で次に何を書こうとしているかだけを覗けばいい。

今回の novelmat は、そこから始まった。


文章を生成させるために使う「LLM」

LLMというと、普通は文章を生成させるために使う。

質問を投げれば答えを書いてくれる。要約を頼めば要約してくれる。OCRの誤りを直したいなら、

この文章を自然な日本語に修正してください

と渡せば、それらしい文章を返してくれる。

もちろん、それでもいい。

でも今回やりたかったのは、少し違う。

LLMには文章を書かせない。

代わりに、

「この続きを書くなら、次に何を書こうとしている?」

という情報だけを取り出すことにした。


LLMは「次に何が来るか」を知っている

LLMが

吾輩は猫であ

まで読んだとする。

次のトークンを生成するとき、内部ではざっくり、

「る」       ← かなりありそう
「り」
「る。」
「\n」       ← これはかなり変
「猫」
...

のように、語彙に存在する大量のトークンすべてへスコアを付けている。

普段のLLM利用では、その確率分布から1トークンを選び、

と実際に生成させる。

今回は生成させない。

その一歩手前で止めて、確率分布だけ読む。

これだけで、

吾輩は猫であ
↓
「\n」はどのくらい出したい?
「る」はどのくらい出したい?

を数値として比較できる。

つまり、LLMを文章生成器ではなく、日本語の違和感センサーとして使う。


形態素解析器を作り込む代わりに、LLMの出力を横取りする

何故こんな事を考えるのか?

日本語の文境界をちゃんと認識する仕組みを作ろうとすれば、それなりの実装になる。

形態素解析して、

助詞だから続きそう
終助詞だから切れそう
句点だから切れそう
でも括弧の中なので……

と考えることになる。

さらにOCR特有の壊れ方まで扱おうとすると、ルールは増えていく。

一方LLMは、そもそも、

吾輩は猫であ

を見せた時点で、

次はたぶん「る」だろう

という判断をしている。

ならば、その判断をそのまま盗めばいい。

日本語の構造をこちらで明示的に解析する必要はない。

必要だったのは、

  1. 問題の位置までLLMに読ませる

  2. logitsを取る

  3. 改行と次トークンのスコアを見る

だけだった。

専用モデルを学習したわけでもない。

形態素解析の辞書を調整したわけでもない。

大量の日本語ルールを書いたわけでもない。

それでも、それなりにうまくいく。

LLMの出力を奪うのは、手間のわりにかなり性能がいい。


改行あり vs 改行なしをLLMに競わせる

文章が

A\nB

となっているとする。

まず、改行しない場合を考える。

A → B

について、LLMがBをどの程度自然だと思っているかを見る。

一方で、改行を残す場合は、

A → \n → B

という系列を見る。

log probabilityを使えば、これらを比較できる。

たとえば、

吾輩は猫であ
る。

なら、「吾輩は猫であ」の次に「る」が来る確率は高い。

一方、その途中に突然改行を挟む確率は低い。

だったら、

この改行はOCRによって紛れ込んだ可能性が高そうだ

と判断できる。

逆に、

吾輩は猫である。
名前はまだない。

なら句点後の改行はそれほど不自然ではない。

重要なのは、こちらが、

「ある。」は文末です

というルールを教えていないことだ。

LLMがすでに持っている日本語予測能力を利用しているだけである。


「日本語を理解させて答えを書かせた」わけではない

ありがちな実装なら、

あなたは日本語校正者です。
以下のOCR結果から不要な改行を削除してください。

とLLMに頼み、

吾輩は猫である。
名前はまだない。

という完成テキストを生成させる。

しかし、それをするとLLMにはかなり大きな自由が与えられる。

誤字を直すかもしれない。

句読点を変えるかもしれない。

表記を整えるかもしれない。

原文そのものを書き換えるかもしれない。

今回ほしいのはそんな高度な校正ではない。

この1文字の \n を消すか、残すか。

それだけだ。

そこでLLMには編集権限を与えない。

聞くのは、

この位置で \n をどのくらい出したかった?
その代わり次の文字をどのくらい出したかった?

という内部情報だけ。

最終的にファイルを書き換えるのは普通のPythonコードである。

AIに仕事を丸投げするというより、

日本語能力だけ借りる。



「生成させない」は速度面でも重要だった

この方法には、精度や制御のしやすさ以外にもう一つ大きな利点があった。

ほとんどdecodeしなくていい。

普通にChatGPTやQwenへ、

この文章から不要な改行を取り除いて、修正後の全文を出してください

と頼む場合、LLMは入力をprefillしたあと、修正済みの文章を最初から最後までdecodeして生成する。

たとえば数千トークンの文章を処理するなら、入力側の数千トークンを読むだけでなく、出力側でもまた数千トークンを1トークンずつ生成することになる。

LLM推論では、このdecode部分がかなり重い。

生成は基本的に、

1 token生成
↓
KV cacheを使って次の1 token生成
↓
また次の1 token生成
↓
...

と逐次的に進む。

文章が長ければ、そのぶん何千回もforwardする必要がある。

一方、今回欲しいのは修正済みの文章そのものではない。

ある位置まで、

吾輩は猫であ

とLLMに読ませて、

次は「る」なのか?
それとも改行なのか?

というlogitsを一度見るだけでよい。

つまり処理の大部分をprefillとしてまとめて流せる

prefillは複数トークンをまとめて行列演算できるので、decodeのように1トークンずつ逐次生成するより圧倒的にGPUを使いやすい。

今回の処理は感覚的には、

普通のLLM校正

長い入力をprefill
        ↓
長い修正版をdecode
decode
decode
decode
decode
...

ではなく、

novelmat

周辺テキストをprefill
        ↓
logitsを数個読む
        ↓
終わり

に近い。

LLMを使っているのに、ほとんど文章を生成しない。

そのため、普通にLLMへ全文を校正させる方法と比べるとかなり速い。

これは今回の用途と相性がよかった。

欲しいのは文章そのものではなく、

この場所では何を書こうとしていたか

という一瞬の判断だけだからである。


「これから校正する」と思わせてから確率を取る

先にLLMへ、

あなたの役割は日本語ライトノベルの校正担当です。
OCRで生成された日本語文を修正してください。

#で始まる行はページ情報なので残してください。
ページをまたいだ文は前ページ側へ統合してください。

といった指示やfew-shot例を見せておく。

その後、

Assistant:
吾輩は猫であ

までをprefillする。

しかし、やはり生成はさせない。

「これから校正結果を書くつもりになったLLM」が次に何を書きたがっているかだけを見る。

この使い方では、プロンプトは回答を生成させるためだけのものではなく、

次トークンの確率分布そのものを目的に合わせて変える手段

になる。指示のほか、参考にする画像をprefillするなどしても面白そうだ。


llm-jp-4への単純なモデル差し替えはうまくいかなかった

日本語を扱うなら、日本語特化モデルのほうがうまくいくのではないか。

そこで llm-jp-4-8b-thinking も試した。

ところが、Gemmaで使っていた判定基準をそのまま持っていくと、期待した結果にはならなかった。

たとえば、

今日はいい天気ですね。
明日も晴れるといいな。

のような通常の文間でも、Gemma用に決めた基準では改行を削除する側へ寄りやすかった。

最初は、

日本語特化モデルなのに意外だな

と思った。

しかし、考えてみれば判定ロジックそのものをGemmaのlogit分布を観察しながら作っている。

モデルが変われば、

P(\n)
P(next token)

の絶対値も、差の出方も、分布の形も変わって当然である。

つまりこれは、

llm-jp-4がこの用途に向いていない、という結果ではない。

むしろ、

モデルごとに判定基準をチューニングする必要がありそう

という話だと思う。

Gemma向けに作った閾値をllm-jpへそのまま持っていって、

Gemmaのほうが優秀だった

と結論するのは早い。

今後モデルを差し替えるなら、少量の教師データから閾値を自動的に合わせたり、logit差を正規化したりする仕組みがあってもよさそうだ。


26万語を毎回ソートして41秒かかっていた

最初は llama.cpp / llama-cpp-python の通常のlogprobs取得APIを使っていた。

ところが、やたら遅い。

調べてみると、必要なのは、

P(\n)
P(next_token)

という数個の値だけなのに、API側では巨大な語彙全体をソートしていた。

今回使ったモデルの語彙は約26万トークン。

それを何度もsortする。

3改行を見るだけで、

約41秒

かかっていた。

さすがにテキストフィルタとは呼びづらい。

そこで通常のcompletion APIを迂回し、

model.eval(tokens)
logits = model._scores

からraw logitsを直接取得するようにした。

あとはnumpyでlog-softmaxして、

p_newline = logprobs[newline_token_id]
p_next = logprobs[next_token_id]

だけ取り出せばいい。

必要なのは2要素程度なのだから、26万要素を順位順に並べる必要はない。

その結果、

3改行: 約41秒 → 約1秒

まで縮んだ。

全文を毎回読ませるのも無駄なので、現在は判定位置の前後約50トークンだけを見る。

最終的には、

数十トークン読ませて、logitsを数個盗む

という非常に小さな処理になった。


最後はsedみたいになった

こうしてできたのが novelmat である。

基本は、

novelmat input.txt

標準入力も使える。

cat input.txt | novelmat

ファイルを直接書き換えることもできる。

novelmat -i input.txt
novelmat -i.bak input.txt

判定結果だけJSONで確認することもできる。

novelmat --detect input.txt -o result.json

LLMアプリなのに、UIもチャット画面もない sed のような使い心地。



LLMは「文章生成API」だけではない

LLMを普通に使っていると、

prompt → generated text

というインターフェイスだけを見がちである。

しかし実際には、その途中に、

context
    ↓
巨大なtoken probability distribution
    ↓
sampling
    ↓
next token

がある。

我々が普段受け取っている文章は、その巨大な情報から最後に1個だけ選ばれた結果だ。

その手前には、

次に改行したい度
次に「る」と書きたい度
句点を書きたい度
EOSを出したい度

が全部入っている。

だったら、生成させずにそこを使ってもいい。

今回の改行フィルタは、その小さな例でしかない。

同じ考え方なら、たとえば、

  • OCR結果がページ末で途中切れしているか

  • 2つのテキスト断片が連続しているか

  • ここに句読点があるべきか

  • 改行すべき位置はどこか

  • 語尾が欠落していないか

  • 文書の境界らしいか

といった問題も、次トークン確率から拾える可能性がある。

こうした問題をまじめに解こうとすれば、形態素解析、構文解析、ルール、専用classifierなど、いろいろな方法がある。

それらが不要という話ではない。

精度や説明可能性が重要なら、専用手法のほうが適している場合も多いだろう。

ただ、

ちょっと日本語らしさを判定したい

という用途では、すでに手元にあるLLMへ文章を食わせ、次に何を出したがっているかを横取りするだけでも、意外なほど使える。

専用の日本語処理系を組み立てるより実装の手間が少なく、しかもモデルがすでに持っている大量の言語知識をそのまま利用できる。

日本語は英語などのように区切りが明確ではなく、形態素解析などしても正確な情報が得られるわけではない前提において、これはかなり便利な性質だと思う。


まとめ -「AIに直してもらう」より「AIが何を書こうとしたかを見る」

今回の novelmat は小さなツールである。

やっていることも最終的には、

この改行を残す / 消す

という二値判定にすぎない。

ただ、実装していてLLMとの付き合い方が途中から変わった。

最初は、

LLMならこの日本語を直せるのでは?

だった。

それが、

LLMはこの位置をどう感じているんだろう?

になり、

最終的には、

答えは書かなくていいから、その瞬間のlogitsだけ見せて

になった。

日本語の文章境界を見つける仕組みをこちらで頑張って作る代わりに、

LLMがすでに持っている「続きを予測する能力」を横から奪う。

少々乱暴だが、手間の割にはよく働く。

LLMは文章生成器として非常に便利だ。

でも、生成される文章はLLMが持っている情報のごく一部でもある。

その手前にある確率分布を覗いてみると、

昔ながらの小さなUNIXフィルタの中に、LLMをひっそり埋め込む

みたいな使い方もできる。

novelmat は、そんな実験になった。

2026年6月2日火曜日

Bambo Labs P1S のノズルつまり解消 (フィラメント供給口が詰まったホットエンドの復旧)

 ABS 素材を使用した後にフィラメントが完全に詰まってしまい、放置していた3Dプリンタを復旧しました。


FDMプリンタのしくみ

すごい雑な図面しか作らないけども、まず何故これからの作業でノズル詰まりが解消するのかを考察してみます。正常状態では押し出しギアで押し出されたフィラメントがホットエンドに送りこまれ、加熱されたノズル内で溶けて排出されます。


強制的な押し出しとコールドプル

この状態フィラメントを強制的に押し出すには以下の作業をします。

  • ノズル温度を 250~270度ぐらい(詰まっているノズルでフィラメントが溶ける温度)にする
  • その状態で、フィラメントを押し出しギアから押し込む
  • ノズルから古いフィラメントが抜ける
また、これでノズルからフィラメント上に引き抜くのがコールドプルです。
  • ノズル温度を 250~270℃ぐらいにする
  • フィラメントに押し出してノズルを満たす
  • ノズル温度を常温に戻して、フィラメントを個体化させる
  • ノズル温度を80℃ぐらいに上げる
  • フィラメントを上に引き抜く
    • 押し出しギアを逆回転させる方法
      • 利点:これで済めば楽
      • 欠点:引き抜いたフィラメントが押しだしギアとホットエンドの間に残る可能性がある
    • ホットエンドをと外した状態で手でフィラメントを引き抜く方法
      • 利点:わかりやすい
      • 欠点:目的温度までホットエンドを温める方法が必要(プリンタ操作)、ホットエンドから手動で引き抜きにはホットエンドを外す必要がある(プリンタから外さないといけない)
しかし今回はこのパターンでは解消しないタイプの詰まりでした。。。


ホットエンド入り口が詰まった状態のノズル → 加熱した金属棒でフィラメントを加熱

ホットエンドを加熱しても、ホットエンド入り口にあるフィラメントの塊が過熱されず、フィラメントを押し出しできない状態でした。
今回はこの状態だったので、加熱した六角レンチでホットエンドのフィラメント供給口を押してあげることで解決しました。


  • 六角レンチを温める。
    • 手許ではガスコンロの火を使った
    • 六角レンチが赤くはならないが、熱でめっきが変色するぐらいまで加熱した
    • 手でレンチを持っていても問題ない程度だった
  • ホットエンドのフィラメント供給口に六角レンチを押し当てる。
    • これによりフィラメントが溶ける。フィラメントを通じてノズルが熱くなるので注意
  • フィラメント供給口をふさいでいたフィラメントの塊をノズルに押し込めた
    • レンチなどにフィラメントを絡めて上に引き抜く「コールドプル」はできていない
この状態で、ノズル温度270℃で温めてからフィラメントを押し出したところ、フィラメントが問題なく吐出されるようになりました。ノズルからフィラメントは出るようになりましたが、この流れでフィラメント供給口から溶けたフィラメントを引き抜きコールドプル(先述)も行い、ノズル内の汚れを取り除きます。これで今回のメンテナンスは完了です。


詰まりのきっかけになった ABS 樹脂でもプリントに成功。大丈夫そう。


これでダメだった時になにをするか。。。

これでダメなら.... 押し出し機の掃除かな。ペットボトルを溶かして遊んでいたときに、押し出し機とノズルの間にPETカスが残ってしまい、エクストルーダを分解したことはあります。今回はそこまでする必要はありませんでした。



おわりに

0.4mm、 0.2mm のノズルが同じフィラメントにより、この問題で詰まっていました。
プリンタメーカー純正フィラメントをパラメータ設定して造形するのは楽しいのだけど、しかし、同じフィラメントを使ったらまた詰まるんじゃないかと思うと、面倒だなぁという気持ちに……。とはいえ、今回、手許で具体的な対処法を見いだせたので、今後同じことがあってもなんとかなりそうです。





2026年5月19日火曜日

古い Proxmove VE 環境からVMをリカバリする

 古くなったSATA SSDが時々リブートで見えなくなったり、ついには稼働中にI/Oエラーを起こしたりするようになったので交換することにしました。


新しいSSDにProxmox VEをクリーンインストールした後、ダメモトで旧ドライブをUSBマスストレージコントローラ経由で接続したところ、認識成功。見えているうちにデータを復元することにします。

仮想マシンの定義ファイルは /etc/pve/ 以下にあるのだが

Proxmox VE の仮想マシン定義ファイルは /etc/pve/nodes/{hostname}/qemu-server/ 以下にテキストの定義ファイルとして置かれています。これをバックアップするため以下のコマンドを実行しました。

cd /mnt/oldroot
tar cvzf ~/old.etc.pve.tar.gz ./etc/pve
cd ~
tar xvzf old.etc.pve.tar.gz

そうするとあら不思議... 空のディレクトリが。/etc/pve は普通のディレクトリではなく、pmxcfs による分散設定ファイルシステムになっています。実体は /var/lib/pve-cluster/config.db に保存されていました。

ドライブが読めなくなる前にDBファイルを保存します。

cd /mnt/oldroot
tar cvzf ~/old.var.lib.pve-cluster.tar.gz ./var/lib/pve-cluster
cd ~
tar xvzf old.var.lib.pve-cluster.tar.gz

sqlite3で覗いてみます。

% sqlite3 config.db
SQLite version 3.51.0 2025-06-12 13:14:41
Enter ".help" for usage hints.
sqlite> select * from tree;
0|0|46624809|0|1777877202|8|__version__|

6|0|7|0|1682337809|8|datacenter.cfg|keyboard: en-us

8|0|8|0|1682337874|4|virtual-guest|
9|0|9|0|1682337875|4|priv|
11|0|11|0|1682337875|4|nodes|
12|11|12|0|1682337875|4|pve|
13|12|13|0|1682337875|4|lxc|
14|12|14|0|1682337875|4|qemu-server|
15|12|15|0|1682337875|4|openvz|
16|12|16|0|1682337875|4|priv|
17|9|17|0|1682337875|4|lock|
24|0|25|0|1682337875|8|pve-www.key|-----BEGIN RSA PRIVATE KEY-----
...

なるほど、6つ目のフィールドがファイル名で、7つ目のフィールドにファイルコンテンツがあるだけの模様。ただ手作業でファイルをひとつひとつリカバリするのも面倒です。


opencode + Local LLM で DB 構造を解析させリカバリプログラムを書かせる

もう、やることが決まってしまった作業は退屈なだけです。

これぐらいのタスクなら軽量の Local LLM でも十分こなせます。外部のAPIサービスに投げない理由は、DBのなかにシークレットなどの情報が混ざっているかもしれないので、手許で推論します。

Mac に先の sqlite3 データベースをコピー、また適当に python venv を用意して、opencode を起動。プロンプトしました。推論にはローカル LM Studio で動く Qwen 3.6 35B-A3B を使用しました。

you will find config.db file which is in sqlite3 format. inspect its "tree" table and build a python script that recover files in the database. use venv/bin/python


呼吸していたら、動く recover.py が出てきました。(大したものでもないですが gist においておきます

このスクリプトを実行したら確かに DB 内のファイルが recovered/ サブディレクトリに階層構造つきで書き出されました。

% find recovered 
recovered
recovered/mapping
recovered/mapping/directory.cfg
recovered/authkey.pub
recovered/pve-www.key
recovered/sdn
recovered/nodes
recovered/nodes/pve
recovered/nodes/pve/pve-ssl.pem
recovered/nodes/pve/ssh_known_hosts
recovered/nodes/pve/openvz
recovered/nodes/pve/pve-ssl.key
recovered/nodes/pve/lxc
recovered/nodes/pve/qemu-server
recovered/nodes/pve/qemu-server/237.conf
recovered/nodes/pve/qemu-server/104.conf
recovered/nodes/pve/qemu-server/105.conf
recovered/nodes/pve/qemu-server/200.conf
recovered/nodes/pve/qemu-server/102.conf
recovered/nodes/pve/qemu-server/231.conf
recovered/nodes/pve/qemu-server/103.conf
recovered/nodes/pve/qemu-server/100.conf
recovered/nodes/pve/qemu-server/249.conf
recovered/nodes/pve/qemu-server/232.conf
recovered/nodes/pve/qemu-server/245.conf
recovered/nodes/pve/qemu-server/101.conf
recovered/nodes/pve/qemu-server/235.conf
...


VM定義ファイルを新環境に展開

経験上、これで qemu-server/XXX.conf ファイルを書き込んであげればVMが出てくるのを知っています。

% cd recovered/nodes/pve/qemu-server/
% scp * "root@{PVE}:/etc/pve/nodes/pve/qemu-server/"
root@{PVE}'s password: 100.conf 100% 537 113.0KB/s 00:00 101.conf 100% 421 110.6KB/s 00:00 102.conf 100% 575 129.4KB/s 00:00 103.conf 100% 417 84.1KB/s 00:00 104.conf 100% 462 102.1KB/s 00:00 105.conf 100% 462 99.0KB/s 00:00 106.conf 100% 485 103.6KB/s 00:00 200.conf 100% 253 54.6KB/s 00:00 ... 

qemu-server/XXX.conf が復元できれば、Proxmox VE は VM を再認識します。

Proxmox VE のコンソールを確認したところ、仮想マシンがホスト上に表示されるようになりました。


NVMeドライブ上のLVMプールを新環境に追加

仮想マシンのディスク本体は起動ドライブとは別の NVMe ドライブにあるので無事です。ただし、VM定義だけではディスク実体にアクセスできないため、NVMe上のLVM Volume Groupを新しい PVE 環境へ再登録します。

root@pve:~# pvesm add lvm NVMe1_ZP4000GM30023_SERIAL123 --vgname NVMe1_ZP4000GM30023_SERIAL123 \ --content images


ネットワーク設定も単純だったこともあり、過去のドライブ上のISOメディアやUSBデバイスの接続など、イレギュラーな設定が入っているものをのぞけば、これでVMが起動できるところまで復元できました。


古いドライブからVMを救出する場合

古いドライブの LVM に VM がある場合は、VMイメージもリカバリしたくなるかもしれません。Proxmox VE は qm コマンドで VM をオンラインでマイグレーションさせられるので、以下の手順でできそうです。
  • 古いドライブを pvesm add lvm コマンドにて新しい名前で登録
    (デフォルトの LVM 名がコンフリクトする)
  • sed などで VM定義のなかのディスクイメージのプール名を編集
  • qm move_disk で新しいドライブへマイグレーションさせる

今回はこのニーズがないため、この記事では扱いません。


まとめ

Proxmox VE の仮想マシンデータのリカバリを、面倒くさい部分は生成AIで自動化して済ませました。仮想ディスク本体が別ストレージに残っている場合、VM定義ファイルを DB から抜き出してしまえば優勝です。

手間のかかる部分をスクリプト生成・実行で済ませたので何も困ることはありませんでしたが、復元作業よりも、このエントリを書くことのほうに時間がかかりました。

次の投稿ネタまでに、作業記録の記事化をどう自動化するかを考えたほうがいいのかもしれません。

2026年5月1日金曜日

Proxmox VEホストのZFSデータセットにTimeMachineバックアップ

MacBook Proを新しく購入したので、Proxmox VE上のZFSストレージをバックアップ用途として見直し、TimeMachineの保存先として利用することにしました。

■ バックアップの圧縮と暗号化のポリシー: ZFSの機能を利用する

万が一どころか億が一、ハードディスクの持ち去り対策としてデータの暗号化を検討します。まぁ個人の Mac で、プール全体を暗号化していないので片手落ちなのですが...

データの圧縮や暗号化をどのレイヤーで行うか考え、最終的にどちらも ZFS のレイヤで行うことにしました。

結論としては、TimeMachine自体は暗号化することはあっても積極的には圧縮しないようで、ZFSのレイヤで圧縮をかけるとしても、TimeMachineの暗号化を有効にすると、ZFS側での圧縮はほぼ効かなくなります。圧縮したければTimeMachineの暗号化機能は利用しないほうがよい、ということになります。


■ Proxmox VE側の設定

まずは zpool、データセットの準備を行います。

array5 zpool

今回は既存の zpool プールを使用しますので、 zpool の作成方法などについては割愛します。 10TB HDD x5, RAIDZ2 で構成したプールです。

hasegaw@pve:~$ zpool list
NAME     SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP    HEALTH  ALTROOT
array5  45.5T  11.2T  34.3T        -         -     0%    24%  1.00x    ONLINE  -

array5/backup
array5/backup/timemachine

ここにはarray5/backup, array5/backup/timemachine データセットを作成します。まぁ、データセットも作成済みなのですが、

hasegaw@pve:~$ zfs list array5/backup
NAME            USED  AVAIL  REFER  MOUNTPOINT
array5/backup 1.31T 20.2T 211G /export/backup
array5/backup/timemachine 959G 20.2T 185K /export/backup/timemachine

新たに作成するのであれば以下のようなコマンドラインになりますね。

sudo zfs create array5/backup
sudo zfs create -o compression=lz4 array5/backup/timemachine

compression=lz4 を指定していますが、とても積極的な圧縮とまでいかなくても、ゼロが続くような領域についてはランレングス圧縮がかかれば良いな、というぐらいの期待値です。ここで gzip などを指定すれば多少圧縮率もあがるかもしれませんが、CPUサイクルとのバランス感は微妙だと思っています。

また、このホストではデータセットのマウント先を /export/backup にしています。(-o mountpoint=/export/backup)


array5/backup/Arseille

続いて TimeMachine 用の array5/backup/Arseille を作成します。 TimeMachine はマウントされたファイルシステムをまるごとバックアップ先として使用する仕様なので、TimeMachineの単位でSMB共有を作成することになります。

折角なので、ZFSのレイヤでも、ZFSデータセットとしても分けておきます。スナップショット単位の操作(zfs send/recv や destroy)を考えると、分けない理由があまり思いつきません。

  • MacBook Pro の内蔵SSDが 4TB なので、 TimeMachine バックアップと、ストレージが溢れた状態でもある程度の世代が残るように、 5TB でクオータを設定します。
  • 暗号化用の鍵をプロンプトにて指定します。鍵は別途管理します。

root@pve:~# zfs create -o quota=5T -o encryption=aes-256-gcm -o keyformat=passphrase \
-o keylocation=prompt array5/backup/timemachine/Arseille Enter new passphrase: Re-enter new passphrase:

TimeMachineで使用するユーザで、データセットのディレクトリに書き込めるようにしておきます。

# chown hasegaw:hasegaw /export/backup/timemachine/Arseille
hasegaw% touch /export/backup/timemachine/Arseille/hoge
hasegaw% unlink /export/backup/timemachine/Arseille/hoge

Samba のインストール

# apt upgrade
# apt install samba

smbpasswd

まだ Samba 用のパスワードを設定していなければ smbpasswd で設定しておきます。

今回はパスワード登録済みですが、新たに登録する場合は以下のイメージになります。

# smbpasswd -a hasegaw
※ -a は新規ユーザ登録。事前の /etc/passwd にそのユーザを作っておくこと

この SMB サーバに対しては私自身が普段使いのユーザアカウントで SMB マウントするため、今回はメインのユーザアカウントで TimeMachine 接続を想定します。

/etc/samba/smb.conf

もらってきた設定を貼り付けて少し弄っただけですが、以下の要領になります。

  • fruit:time machine=yes
  • fruit:time machine max size = 5000G: 設定は可能なのですが ZFSの quota で最大容量は伝わっているかなと思うので、いったん様子見で外しています。どのように振る舞うのかが判っていませんが、 ZFS 側の制限がハードクオータ、こちらの設定が TimeMachine に対する容量のヒントとして振る舞うように見えるので、 ZFS データセットよりもすこし小さい値に設定してほうがよいかもしれません。
  • ea support = yes となっていますが実際には ZFS レベルで EA を有効化していません。現状、手許では問題なく動いているように見えていますが、将来的に見直すかもしれません。
[timemachine_Arseille]
path = /export/backup/timemachine/Arseille
browseable = yes
read only = no
guest ok = no
writable = yes
valid users = hasegaw
fruit:time machine = yes
; fruit:time machine max size = 5000G
ea support = yes

Samba プロセスを再起動しておきます。

root# systemctl restart smbd


■ Mac 側の設定

MacBook Proからのマウント確認

Finderから Cmd-K 、接続先に smb://hostname_or_ip_address/ を入力し接続します。

ユーザ名、パスワードを聞かれたら PVE ホスト側のユーザ名および先に smbpasswd で指定したパスワードを入力します。

TimeMachine バックアップ先となる timemachine_Arseille 共有が見えたら、それをマウントします。中にフォルダを作成できることを確認し、書き込みが正しくできることを確認できたらテストで作成したフォルダを削除し、共有を改めて空の状態にしておきます。

Finder の左側ペインから、該当する共有のマウントを解除します。

TimeMachine 設定

バックアップ対象の MacBook Pro 側で、以下の要領で TimeMachine 先を設定します。

root# tmutil setdestination smb://user:passwd@hostname/timemachine_Arseille
root# tmutil startbackup

設定で TimeMachine を検索し、バックアップが開始されたことを確認します。



バックアップ終了後に ZFS データセットの割り当て量が増えていることが確認できます。

root@pve:~# zfs list array5/backup/timemachine/Arsielle
NAME                                 USED  AVAIL  REFER  MOUNTPOINT
array5/backup/timemachine/Arsielle  9.17G  4.99T  9.17G  /export/backup/timemachine/Arsielle


■ 再起動後の暗号化解除

Proxmox VEで動作するZFSのプールのうち、該当する timemachine 領域をマウントするには鍵を提供する必要があります。このためホストの再起動後はプールが見えなくなり、バックアップや TimeMachine の内容確認などの操作ができなくなります。

 zfs load-key コマンドにてプロンプトから鍵を入力しアンロックします。

root# zfs load-key array5/backup/timemachine/Arseille
root# systemctl restart smbd

プールが見えない状態では TimeMachine が sparse bundle を見つけられないため、バックアップは(安全に)失敗します。次回以降のバックアップで再マウントいs sparse bundle が見つかれば、バックアップは再開します。


■ ToDo: Mac持ち出し時の他ネットワークでのバックアップ防止

Mac の TimeMachine 機能はスケジュール通りにバックアップを繰り返すため、自宅から Mac を持ち出している間も、 tmutil setdestination で指定した hostname に対して user:passwd でログインしようとする挙動がつづきます。

セキュリティ的には気持ちが悪い挙動なので、ネットワーク条件に応じてTimeMachineを有効/無効に切り替える仕組みを今後検討したいと思います。

クラスCアドレスなので仮にインターネットに繋がっていても経路がなくSMB共有は繋がらないはずですが、接続先のWiFiが(以下自主規制)

かといって、この状況で IPsec などを使うのもどうかと思うし :S 自宅LANに接続していなければ自動バックアップ無効までやりたいと思いますが... これは後日としたいと思います。

2026年4月29日水曜日

Proxmox VEを7.xから9.xへアップデート

 インストール後に割と放置していた Proxmox をアップデートしました。


作業ログをとらずにまとめてやってしまったので具体的なコマンドラインや出力は記載しませんが、おおよその手順としては、下記の手順になります。

  • Proxmove VE 7.x を最新バージョンまでアップデート
    • apt update
    • apt dist-upgrade
  • PVE 7 -> 8 のアップデート検証、アップデート
    • pve7to8 コマンドを実行しアップデート事前検証
      • すでに zfs destroy したプールが見つからない警告が出たので、ここで削除しておく
      • DKMS でインストール済みだった iomemory-vsl について警告が出ていたので、 DKMS を外しておく
      • 推奨に従い VM を停止
    • sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list
    • apt update
    • apt dist-upgrade
    • 新しい kernel もあるため一度再起動、 VMが起動することまで確認しておく
  • PVE 8->9 のアップデート検証
    • pve8to9 コマンドを実行しアップデート事前検証
      • ブートディスク容量不足の警告が出たため ISO イメージを整理
      • 推奨に従い VM を停止
    • sed -i 's/bookworm/trixie/g' /etc/apt/sources.list
    • apt update
    • apt dist-upgrade
    • 再起動、 VMが起動することまで確認

その他考慮したポイントなど
  •  PVE 9.x へのアップデートにあたり多少 grub.cfg が変化しているようで。iommu 関連で GRUB の設定を書き換えていましたが、メンテナ版を受け入れ、個別の修正は改めて適用。
  • smb.conf は現行バージョンを採用。
  • AMD 向けマイクロコードのパッケージなどが表示された案内通りにはインストールできていないが、とりあえずそのままで。
  • GRUB の際インストールを促されたが、案内通りにはうまく進まなかったので、とりあえずそのままで。 boot してるから、まあいいでしょう
  • 当初はクリーンインストールになるのかなと思いながら、手順どおり /etc 以下や /var/lib/ の下などをバックアップしてまわろうと思い作業してましたが、 apt dist-upgrade の手順が出てきた段階で「まあなんとかなるだろう」と思い始めて、そのまま上書きしてしまいました。 Proxmox VE のブートドライブ (SATA 128GB), メインの仮想マシンプール(NVMe 4TB), ストレージアレイ(10TB x5) が別れていたので、失敗してもなんとかなるだろうと思ったのもあります。痛い目に遭いたくなければ、バックアップぐらいは取っておきましょう 

参考にしたURL

  • https://pve.proxmox.com/wiki/Upgrade_from_7_to_8
  • https://pve.proxmox.com/wiki/Upgrade_from_8_to_9


2026年2月14日土曜日

3Dプリンタを併用してSHURE SRH1540用ロングケーブルを製作

SHURE SRH1540

2000年ごろ、大学生のときにATH-A700ヘッドホンを購入してから20年近く使っていたのですが、最終的に樹脂部品が経年劣化で分解してしまい、処分しました。それ以降は解放型の SENHEISER GAME ONE を自宅で使っていたものの、モニタリング用途でどうしても密閉型のヘッドホンが欲しくなり、 SRH1540 を購入。


新品で購入しなくてもよいかなと思い、最終的に3万円台で中古を購入しました。それまでに使っていたヘッドホン類と比べると(新品販売時の価格帯も倍以上と違い)かなり解像度が高く感じ、聞こえ方が別物に感じます。

常用してわかってきた問題点


問題なく聴けているときの利用感は非常に好みです。しかし、常用していると、純正の「左右両だしケーブル」にかなり不便を感じました。

  • ケーブルが 1.5m 程度?で短め
    • 装着状態では身動きをとりづらい
    • ケーブル長から、利用場所の制限を感じる
  • 左右両だし仕様
    • ギターを抱えたり離したりしづらい
    • どちらが右・左か、わからなくなる
  • 3.5ピンコネクタ相性
    • 一般的な3.5ピンコネクタよりも細めなのか、接触不良になりやすい模様。検索すると悩んでいる人がけっこう出てくる

ケーブルの作製を決断、しかしコネクタが手に入らない


先述のトラブル内容でイライラしてしまうので、別のモニタリングヘッドホン購入を検討。しかし定価6万円台のヘッドホンを基準にしてしまうと、次の選択肢もなかなか決まりません。
「最終的に、これ以上ヘッドホン買うよりは SRH1540をなんとか使える状態にしたほうがよいだろう」と思い直し。ケーブルを作製することにしました。

作製にあたり困ったのはコネクタ部分。接点部分は一般的な MMCX ですが、イヤホンのリケーブルなどで使用される MMCX コネクタより細身の独自仕様になっていて、これにあった部品がありません。秋葉原で相談すると「そんなもん売ってねー」と冷たくあしらわれる状況。


既存 MMCX コネクタ + 3Dプリントの自作ハウジング製作にチャレンジ


最終的に NOBUNAGA Labs MMCXプラグ自作キット と、3Dプリントの自作ハウジングで製作にチャレンジすることにしました。







こちらの MMCX コネクタはネジ山が彫ってあるため、ハウジングの内径次第で、よい感じに閉めることができます(オヤイデで取り扱いのあるMMCXコネクタも購入しましたが、こちらにはネジ山がありません)。 Bambu Labs のPLAフィラメントで輪状に出力したハウジングに対して、そのまま締めることができました。

ただ、非常に小さいモノなため、このハウジングは3Dプリントするには 0.4mm ノズルでは難しい印象です。普段はあまり使わない 0.2mm ノズルを利用しました。 0.2mm ノズルで壁ひとつ分の壁厚しかとれないと思います(2壁はチャレンジしていませんが)。変に力をかけると層がはがれて壊れてしまうかもしれませんが。壊れたら、コネクタのはんだ付けからやり直しです(コネクタ部分の再利用はむつかしいと思いますので、修理のタイミングで新しい部品が必要な気がします)。このサイズ感のはんだ付けは得意ではないので、断線・修理が怖いです。

余談ですが、この製作の前に Kaika の P1S 利用可能なノズルを注文していたのですが、他のユーザからのトラブル報告などを経て最終的に入手しませんでした。 0.1mm ノズルが手元にあったら、絶対にプロジェクトでは 使ってみたかったです。

そして、ついに SRH1540 に装着可能な MMCX のハウジングを製作できました。多少不格好ですが、自分で使うには問題ないでしょう。



チャンネルの見分けがつくように片方をマゼンダにしました(CMYKセットとしてもっていたものを利用)


3mの片出しケーブルを作製


SRH1540に対応できるMMCXハウジングを作れてしまえば、もう後は単純なはんだ付け作業です。

今回は 3.5ピン側も NOBUNAGA Labs のものを使いました。正直私には200円未満のプラグとの利用感の違いはわかりませんが……。ピンジャックとSHURE純正プラグの相性問題とも、これでサヨナラです。

ケーブルは、オヤイデの、イヤホン用2芯2ペアのものを使用。作製したのは半年以上前ですが、これがすごく高かった覚えがあります。普通なら3mとかで使うようなものでもない。。。

右チャンネル分は短く、左チャンネル分はヘッドレストを経てコネクタに接続できるように作ったことで、事実上片出し運用ができるようになりました。

作製したケーブルはデスクトップ利用もできますし、ソファでギターを弾いているときにオーディオインターフェイスで音をモニタリングするのにも、不自由なく使えています。


3Dプリンタがあるからこそできた


このケーブルは 2025/7 に作製し、手元の利用ではハウジングが破損することもなく半年以上問題なく利用できています。

この製作のキモは、先述のとおり、特別なMMCXコネクタ用ハウジングの製作でした。もし3Dプリンタが手元になければ、最初から諦めていたと思います。私は手先が器用な方ではないですが、それでも「3Dプリンタで、解決できる課題が広がっている」と感じたプロジェクトでした。