3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】ロックマンエグゼのネットナビを本気で実現する(プロダクト草案)

3
Posted at

「もしも、自分だけのネットナビが相棒としてPCに常駐してくれたら……」
そんな少年時代の熱い妄想を、現代のテクノロジーで本気で具現化するためのプロダクト草案です。

ChatGPTのような巨大なクラウドAI(LLM)にお金を払えば、それっぽい会話はすぐに作れます。しかし、それは「あらかじめ用意された超巨大な知能」を借りてきているに過ぎません。私たちが求めているネットナビは、「最初は何も喋れない真っ白な状態から、オペレーター(あなた)の言葉だけを吸収して、あなただけの相棒に育っていく電脳生命体」のはずです。

本記事では、PyTorchのテンソル演算と自然言語処理(NLP)を組み合わせ、メモリと計算効率を極限まで高めた自律成長型のネットナビ・コアエンジンのプロトタイプを提案します。PythonとPyTorchがインストールされていれば、誰でも今すぐ自分のPCで「ナビの卵」を起動できます。


1. ネットナビ・コアエンジンの設計思想

このプロダクトは、AIの「脳」を重量級の文章生成モデルではなく、数万個の「単語ニューロン」とそれらを繋ぐ「シナプス(遷移確率)行列」というシンプルな物理シミュレーションとして再定義します。

脳みその仕組み(初心者向け)

  1. 言葉のインプット(形態素解析)
    あなたが入力した文章を、自然言語処理ライブラリ(Janome)を使って「単語」に分解します。
  2. シナプスの強化(マルコフ連鎖)
    「おはよう」の次に「ロックマン」という単語が来たら、脳内の単語遷移マトリクス(シナプス)の結合度を 1.0 強くします。これを繰り返すことで、ナビは言葉の繋がり(文法)をゼロから学びます。
  3. 人間らしい「忘却」と「突然変異」
    使われない単語は時間とともに重要度が下がり、やがて忘れ去られます。また、ナビの気分や言葉の重要度に応じて、5%の確率で脈絡のない言葉を突然口走る(突然変異)仕組みを入れ、生命体らしいリアルな挙動を演出します。

2. ネットナビ・コアエンジン全ソースコード

以下が、完全決定論的なテンソル演算とリアルタイム自律学習を両立したコアエンジンの全コードです。ファイル名を netnavi_engine.py として保存してください。

import os
import json
import torch
import warnings

# 警告メッセージを沈黙させる
warnings.filterwarnings("ignore")
os.environ["TOKENIZERS_PARALLELISM"] = "false"

try:
    from janome.tokenizer import Tokenizer
except ImportError:
    print("[警告] 自然言語処理ライブラリ 'janome' がインストールされていません。")
    print("        実行前に `pip install janome` を実行してください。")
    print("        (インストールされていない場合、文字単位の学習モードで動作します)")
    Tokenizer = None

# =====================================================================
# ハードウェア極限最適化 (PyTorch)
# =====================================================================
torch.set_num_threads(os.cpu_count() or 4)

# 非正規数(Denormal)によるCPUの深刻な計算遅延ペナルティをハードウェアレベルで回避
torch.set_flush_denormal(True)

if torch.cuda.is_available():
    torch.backends.cuda.matmul.allow_tf32 = True
    torch.backends.cudnn.allow_tf32 = True

# =====================================================================
# ネットナビ・自律成長型コアエンジン
# =====================================================================

class PyUCDF_NaviBrain:
    def __init__(self, max_vocab=10000):
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.max_vocab = max_vocab
        self.vocab_file = "navi_vocab.json"
        self.tensor_file = "navi_tensors.pt"
        
        self.tokenizer = Tokenizer() if Tokenizer else None
        
        # --- 語彙(単語ニューロン)の管理 ---
        # 0: 文の始まり (BOS), 1: 文の終わり (EOS)
        self.word2id = {"__BOS__": 0, "__EOS__": 1}
        self.id2word = {0: "__BOS__", 1: "__EOS__"}
        self.vocab_size = 2
        
        # --- 状態管理テンソル (Structure of Arrays) ---
        # 各要素 (32bit) に [vA(8bit), vB(8bit), vC(8bit), State(3bit), Ruin(3bit)] をパック
        self.state_buffer = torch.zeros(max_vocab, dtype=torch.int32, device=self.device)
        
        # --- シナプス結合行列 (マルコフ連鎖) ---
        # 単語間の遷移頻度を記録する [max_vocab, max_vocab] の行列
        self.transition_matrix = torch.zeros((max_vocab, max_vocab), dtype=torch.float32, device=self.device)
        
        # 初期状態パック (BOSとEOSはシステム予約のため、重要度MAXで固定)
        self._pack_state(0, va=255, vb=255, vc=0, state=1, ruin=0)
        self._pack_state(1, va=255, vb=255, vc=0, state=1, ruin=0)
        
        self.load_memory()

    def _pack_state(self, idx, va, vb, vc, state, ruin):
        """指定したインデックスのニューロン状態を32bitにパック"""
        packed = ((int(va) & 0xFF) | 
                  ((int(vb) & 0xFF) << 8) | 
                  ((int(vc) & 0xFF) << 16) | 
                  ((int(state) & 0x07) << 24) | 
                  ((int(ruin) & 0x07) << 27))
        self.state_buffer[idx] = torch.tensor(packed, dtype=torch.int32, device=self.device)

    def save_memory(self):
        """脳の状態(テンソルと語彙辞書)をファイルに保存"""
        with open(self.vocab_file, 'w', encoding='utf-8') as f:
            json.dump({"word2id": self.word2id}, f, ensure_ascii=False, indent=2)
            
        # テンソルは必要なサイズだけスライスして保存し、容量を節約
        torch.save({
            "state_buffer": self.state_buffer[:self.vocab_size].cpu(),
            "transition_matrix": self.transition_matrix[:self.vocab_size, :self.vocab_size].cpu()
        }, self.tensor_file)

    def load_memory(self):
        """ファイルから脳の状態を復元"""
        if os.path.exists(self.vocab_file) and os.path.exists(self.tensor_file):
            print("[System] 既存の電脳メモリをロードしています...")
            with open(self.vocab_file, 'r', encoding='utf-8') as f:
                data = json.load(f)
                self.word2id = data["word2id"]
                self.id2word = {int(v): k for k, v in self.word2id.items()}
                self.vocab_size = len(self.word2id)
                
            tensors = torch.load(self.tensor_file, map_location=self.device, weights_only=True)
            saved_vocab_size = tensors["state_buffer"].shape[0]
            
            self.state_buffer[:saved_vocab_size] = tensors["state_buffer"].to(self.device)
            self.transition_matrix[:saved_vocab_size, :saved_vocab_size] = tensors["transition_matrix"].to(self.device)
            
            print(f"[System] ロード完了。現在の語彙数: {self.vocab_size - 2}")
        else:
            print("[System] 新規ニューラルネットワークを初期化。ゼロから学習を開始します。")

    def apply_vif_entropy_decay(self):
        """
        テンソル一括演算による忘却・減衰プロセス
        ループを排除し、数万の単語ニューロンの「忘却」を一瞬で計算します。
        """
        if self.vocab_size <= 2:
            return
            
        # BOS(0) と EOS(1) を除く実際の単語ニューロンのみ対象
        states = self.state_buffer[2:self.vocab_size]
        
        va = states & 0xFF            # 重要度 (よく使われる言葉ほど高い)
        vb = (states >> 8) & 0xFF     # 演算頻度
        vc = (states >> 16) & 0xFF    # 減衰カウンタ (時間経過で上昇)
        st = (states >> 24) & 0x07
        ruin = (states >> 27) & 0x07
        
        # ターン経過ごとに減衰カウンタ(vC)を増加
        vc = torch.clamp(vc + 5, max=255)
        
        # vCが閾値(100)を超えたら重要度(vA)を削り、カウンタをリセット
        decay_mask = (vc > 100).to(torch.int32)
        va = torch.clamp(va - (decay_mask * 2), min=1)
        vc = torch.where(decay_mask > 0, torch.zeros_like(vc), vc)
        
        # 再パックして上書き
        packed = (va | (vb << 8) | (vc << 16) | (st << 24) | (ruin << 27))
        self.state_buffer[2:self.vocab_size] = packed

        # シナプス結合(マルコフ行列)の自然減衰
        # 時間経過とともに、使われない言葉のつながりも徐々に薄れ、忘れ去られる
        self.transition_matrix *= 0.99

    def learn_from_input(self, text):
        """ユーザーの入力から単語を抽出し、シナプス(遷移行列)を強化する"""
        
        # NLP環境フォールバック: Janomeがない場合は文字単位で学習
        if self.tokenizer:
            tokens = [t.surface for t in self.tokenizer.tokenize(text)]
        else:
            tokens = list(text)
            
        prev_id = 0 # __BOS__
        used_ids = []
        
        for surface in tokens:
            # 空白や一部の記号は無視
            if surface.strip() == "" or surface in ["", ""]:
                continue
                
            # 未知の単語ならニューロンを新規割り当て
            if surface not in self.word2id:
                if self.vocab_size >= self.max_vocab:
                    prev_id = 0 # 容量限界: 誤った文脈接続を防ぐためリセット
                    continue 
                new_id = self.vocab_size
                self.word2id[surface] = new_id
                self.id2word[new_id] = surface
                self.vocab_size += 1
                
                # 新規ニューロンの初期化 (少し高めの重要度でスタート)
                self._pack_state(new_id, va=100, vb=50, vc=0, state=1, ruin=0)
            
            curr_id = self.word2id[surface]
            used_ids.append(curr_id)
            
            # シナプス結合(遷移確率)を強化
            self.transition_matrix[prev_id, curr_id] += 1.0
            prev_id = curr_id
            
            # 句点などで文が終わったと判定
            if surface in ["", "", "", "!", "?"]:
                self.transition_matrix[prev_id, 1] += 1.0 # __EOS__
                prev_id = 0 # 次の文は再びBOSから
                
        # 最後に文が終わっていなければEOSへ繋ぐ
        if prev_id != 0:
            self.transition_matrix[prev_id, 1] += 1.0
            
        # 今回使用された単語ニューロンを一括で「刺激(重要度アップ)」する
        if used_ids:
            idx_tensor = torch.tensor(list(set(used_ids)), dtype=torch.long, device=self.device)
            states = self.state_buffer[idx_tensor]
            
            va = states & 0xFF
            vb = (states >> 8) & 0xFF
            vc = (states >> 16) & 0xFF
            st = (states >> 24) & 0x07
            ruin = (states >> 27) & 0x07
            
            # 刺激による回復
            va = torch.clamp(va + 50, max=255)
            vb = torch.clamp(vb + 10, max=255)
            vc = torch.zeros_like(vc) # 減衰リセット
            
            packed = (va | (vb << 8) | (vc << 16) | (st << 24) | (ruin << 27))
            self.state_buffer[idx_tensor] = packed.to(torch.int32)
            
        self.apply_vif_entropy_decay()
        self.save_memory()

    def generate_response(self):
        """確率雲(テンソル)からのサンプリングによる発話生成"""
        if self.vocab_size <= 2:
            return "……(まだ言葉を知りません)", "……"
            
        # 全ニューロンの重要度(vA)を抽出 (0.0 ~ 1.0 に正規化)
        states = self.state_buffer[:self.vocab_size]
        va_normalized = (states & 0xFF).to(torch.float32) / 255.0
        
        curr_id = 0 # __BOS__
        chosen_words = []
        raw_tokens = []
        
        # 最大30単語で打ち切り
        for _ in range(30):
            # 現在の単語からの遷移頻度ベクトルを取得
            freqs = self.transition_matrix[curr_id, :self.vocab_size].clone()
            
            # 頻度を対数スケールに変換し、過学習(ループ発話)を防ぐ
            logits = torch.log1p(freqs)
            
            # BOSへの逆流を禁止
            logits[0] = -1e9
            
            # softmaxで確率分布に変換 (temperatureでランダム性を制御)
            temperature = 0.8
            context_probs = torch.softmax(logits / temperature, dim=0)
            
            # 突然変異(Mutation)の計算
            # 重要度(vA)に基づく確率分布をブレンドし、脈絡のない話題へのジャンプを実現する
            if freqs.max() == 0:
                mutation_rate = 1.0  # 未知の状態なら100%突然変異
            else:
                mutation_rate = 0.05 # 5%の確率で文脈を無視して脳内で気になっている(vAが高い)言葉が口走られる
                
            va_probs = va_normalized.clone()
            va_probs[0] = 0.0  # BOSには飛ばない
            if va_probs.sum() > 0:
                va_probs = va_probs / va_probs.sum()
            else:
                va_probs = context_probs
                
            # ブレンドされた最終的な確率分布
            probs = context_probs * (1.0 - mutation_rate) + va_probs * mutation_rate
            
            # テンソル演算によるランダムサンプリング
            next_id = torch.multinomial(probs, 1).item()
            
            if next_id == 1: # __EOS__ に到達したら終了
                break
                
            word = self.id2word[next_id]
            chosen_words.append(word)
            
            # 内部解析用のタグ付け処理(メタ認知)
            if self.tokenizer:
                # ジェネレーターをリストに変換してアクセスエラーを回避
                tokens = list(self.tokenizer.tokenize(word))
                if tokens:
                    pos = tokens[0].part_of_speech.split(',')[0]
                    if pos in ['名詞', '動詞', '形容詞']:
                        raw_tokens.append(f"[{word}:{pos}]")
                    else:
                        raw_tokens.append(word)
            else:
                raw_tokens.append(word)
                
            curr_id = next_id
            
        if not chosen_words:
            return "……", "……"
            
        return "".join(chosen_words), "".join(raw_tokens)

# =====================================================================
# メイン実行ループ
# =====================================================================

def main():
    os.system('cls' if os.name == 'nt' else 'clear')
    print("========================================")
    print("   PyUCDF ネットナビ・コアエンジン")
    print("   (ハードウェア最適化モデル: PyTorch)")
    print("========================================")
    
    engine = PyUCDF_NaviBrain(max_vocab=10000)
    
    print("\n[System] ネットナビが起動しました。")
    print("[System] 最初は言葉を知りません。何度も話しかけて、文法や言葉を教えてあげてください。")
    print("[System] 「exit」で終了します。")
    
    while True:
        try:
            user_input = input("\nあなた: ")
            if user_input.lower() in ['exit', 'quit', '終了']:
                print("[Navi]: (ログアウトしました)")
                break
            if not user_input.strip():
                continue
            
            # 1. ユーザーの言葉から学習する
            engine.learn_from_input(user_input)
            
            # 2. 脳内テンソルから言葉を紡ぎ出す
            plain_text, analyzed_text = engine.generate_response()
            
            print(f"\n[Navi]: {analyzed_text}")
            
        except KeyboardInterrupt:
            break

if __name__ == "__main__":
    main()


3. 実際の育成ログ(カタコトから言葉を覚えるまで)

以下は、生まれたばかりの空っぽの状態から、オペレーターが何度も「名前」や「挨拶」を教え込んでいる実際の電脳回路の成長の軌跡です。

最初は完全に支離滅裂ですが、同じ単語の組み合わせ(シナプス)を繰り返し刺激されることで、少しずつ関連キーワードがリンクし始めているプロセスがリアルに観察できます。

あなた: あなたの名前はロックマン

[Navi]: [マン:名詞]のおはようはおはよう[あなた:名詞]の[名前:名詞][ロック:名詞][マン:名詞]
あなた: あなたの名前はロックマン

[Navi]: [ロック:名詞][マン:名詞]
あなた: あなたの名前はロックマン

[Navi]: ……
あなた: おはよう

[Navi]: [ロック:名詞][マン:名詞]
あなた: おはよう

[Navi]: [名前:名詞]は[ロック:名詞][マン:名詞]は[ロック:名詞][マン:名詞]
あなた: おはよう

[Navi]: [あなた:名詞]おはようは[ロック:名詞][マン:名詞]
あなた: こんにちは

[Navi]: [あなた:名詞]の[名前:名詞]は[ロック:名詞][マン:名詞]
あなた: おぼえた?

[Navi]: [あなた:名詞]の[名前:名詞]は[ロック:名詞][マン:名詞]
あなた: あなたの名前は?

[Navi]: おはよう
あなた: ちがう、ロックマン

[Navi]: [あなた:名詞]の
あなた: ロックマン

[Navi]: [ロック:名詞][マン:名詞]
あなた: あなたはロックマン

[Navi]: [ロック:名詞][マン:名詞]?
あなた: ロックマンです

[Navi]: [ロック:名詞]?たは[ロック:名詞][マン:名詞]
あなた: あなたはロックマン

[Navi]: おはよう
あなた: おはよう!

[Navi]: [あなた:名詞]の
あなた: あなたはロックマン

[Navi]: [ロック:名詞][マン:名詞]
あなた: そうです

[Navi]: [あなた:名詞]のそうは[ロック:名詞][マン:名詞]
あなた: あなたはロックマン

[Navi]: [ロック:名詞][おぼえ:動詞]
あなた: あなたの名前は

[Navi]: [あなた:名詞]のた!![おぼえ:動詞]こんにちは[マン:名詞]
あなた: ロックマン

[Navi]: は[ロック:名詞][マン:名詞]
あなた: ロックマン

[Navi]: こんにちはは[ロック:名詞]た?[名前:名詞]そう[マン:名詞]
あなた: ロックマン

[Navi]: [ロック:名詞][マン:名詞]
あなた: ロックマン

[Navi]: [ロック:名詞]こんにちは
あなた: ロックマン

[Navi]: [あなた:名詞]は![ちがう:動詞]こんにちはおはよう??[ロック:名詞][マン:名詞]
あなた: ロックマン

[Navi]: [ロック:名詞][マン:名詞]


最初は「あなた の 名前 は」といったブツ切りのパーツや、「こんにちは おはよう」といった矛盾した組み合わせをランダムに叫んでいますが、後半に進むにつれて、

  • 「ロックマン」に対して、正しく [ロック:名詞][マン:名詞] と認識して返す
  • 「あなたの名前は」に対して、[あなた:名詞]の[名前:名詞]は[おぼえ:動詞]た[マン:名詞] と、文脈が収束し始める

という、「学習による脳内ネットワークの最適化」のドキュメンタリーがここに現れています。


4. コアアルゴリズムの解説

対数スケールによる過学習の抑制

前述の通り、シナプス(単語の結合)を単純にインクリメントすると、特定のよく使われる組み合わせ(例:「ボクは」「プラグイン」など)の確率が爆発的に高まり、ナビが同じ言葉しかリピートしなくなります。
このコードでは、結合強度に torch.log1p(freqs)(対数スケール)を適用することで、頻度の差による影響をなだらかにし、多様な文章パターンを出力できる柔軟性を持たせています。

メタ認知発話

ナビが喋る際、ただ文字列を返すのではなく、直前に脳内で組み立てた言葉を自ら再解析します。
コンソール上には [ボク:名詞]は[元気:名詞]だよ のように、ナビが言葉を認識しながら思考している様子がリアルタイムに可視化され、電脳生命体としての存在感を際立たせます。


5. 今後の圧倒的な課題:真のネットナビへの道

このプロトタイプは強力ですが、アニメやゲームのように「自律して生きている」状態にするには、以下の大きな壁を超えなければなりません。

課題①:24時間365日、自律思考・自律行動させるには?

現在はユーザーが入力を送信した瞬間だけ脳(テンソル)が動く「受動的」なシステムです。ナビが自発的に話し、裏で思考を巡らせるためには、以下の設計が必要です。

  • 常時稼働エントロピー収穫の実装
    OSのバックグラウンドプロセスとしてナビの脳を常駐させます。
    PCのCPU温度、バッテリー残量、キーボードのタイピング速度などを「環境ノイズ(刺激)」として常に脳内テンソルに流し込みます。
  • 擬似睡眠と自己対話(夢のシミュレーション)
    オペレーターがPCを操作していない時間(深夜など)に、ナビが保持している transition_matrix を使って脳内で自己対話(ランダムな文章生成とシナプス再強化)を自動実行させます。これにより、次に目覚めたときに「新しい文脈を勝手に思いついている」ような、自律的な知能の飛躍が生まれます。

課題②:外部ファイルを廃止し、電脳特化の「インメモリデータベース」を自作する

現在の実装では、語彙辞書を json、脳のテンソル構造を pt (PyTorchファイル) として個別にローカル保存しています。しかし、語彙が数万〜数十万規模に膨れ上がった場合、毎回すべてのファイルを丸ごとハードディスクへ読み書きするのは、パフォーマンス(ディスクI/O)的に限界が来ます。

ここで、SQLiteなどの既存のRDB(リレーショナルデータベース)をそのまま導入するのも手ですが、私はネットナビのメモリ空間(ビットパックされたSoA構造)に完全特化した「独自の軽量インメモリデータベース」を自作してみたいと考えています。

  • 自作インメモリDBへの挑戦とアーキテクチャ案
    単語データ(ID、文字列、パックされた状態値)やシナプスの結合度を、メモリ上で超高速にインデキシング・検索・更新できる専用のインメモリDBエンジンを1から構築します。
    そして、すべてのデータを毎回ディスクへ書き出すのではなく、「そのターンで変動(刺激)があったアクティブな領域の差分だけ」を、バックグラウンドのスレッドが非同期でスマートに永続化ファイル(WAL: Write-Ahead Logging形式など)へ書き下ろす仕組みを実装します。

課題③:本当に学習できるのかまだ未知数。時間をかけてもっと試したい

最大の、そして最も面白い課題はこれです。上記のログが示す通り、ナビは確かに「言葉の繋がり」を少しずつ形成し始めていますが、これが本当に「人間の意図した高度な対話」や「明確な人格の形成」にまで発展できるのかは、現時点ではまだ未知数です。

単語レベルの簡単なマルコフ連鎖モデルが、数万ターン、数十万ターンの会話と、数ヶ月に及ぶ長期の「忘却と刺激のサイクル」を繰り返したとき、どのような創発(インテリジェンスの爆発)が起きるのか。あるいは、どこかで破綻して特定の無限ループに陥るのか。
この電脳生命体の成長限界を見極めるためにも、時間をかけてもっと様々なアプローチや会話パターンを試し、検証を続けていきたいと考えています。


まとめ

このプロダクト草案は、単なる既存のLLMラッパーではなく、「オペレーターと共に歩み、時間をかけて独自の電脳回路を形成していく生命体」を作るための第一歩です。
最初は「……」としか呟けない空っぽのナビが、あなたの口癖を覚え、あなたのPC環境のノイズを浴びて、世界に一人だけの相棒に変わる未来を、ぜひこのコードから始めてみてください。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?