[A.I.] LLMの記憶と人間の記憶

道草をしていると、人間の記憶の覚束なさを強烈に感じることになる。脳内記憶だけが頼りだと一度見たことのある現象なのか、それの変化したものなのかなどははっきりとわからない。まあ、わからないのが正解の場合も有るだろう。見たことがあるような気がするのもおかしなことではないだろう。

LLMの記憶はLLMという膨大な抽象化された記憶に支えられているが、短期記憶は持っているハードウェア・ソフトウェアリソースに依存している。ユーザーとの会話がうまくいくかどうかはそれに掛かっている。最近、レイ・カーツワイルの「シンギュラリティはさらに近く」(2024年)がどこまでLLMに言及しているかを確認すると、OpenAIのGPT4までは考慮して書かれている。LLMの記憶については同じようなことが書かれている。

[A.I.] LLMの記憶と人間の記憶 2 - 文脈記憶

AI覇権争いはOpenAIが勝つ?産業構造まで変えうる壮大な構想【カンタン解説】 The WAVE TV【AIの最新動向解説チャンネル】 チャンネル登録者数 8330人 663 回視聴 2025/09/02。よく引用されるSemiAnalysis(SemiAnalysis - Bridging the gap between the world's most important industry, semiconductors, and business.)を調べ始めた。メモメモ。

レイ・カーツワイルは今のAIに足りないものとして、次の三つを上げている。「文脈記憶、常識、社会的相互作用」の三つ。文脈記憶については、GPT-4は金魚の記憶を持っているそうだ。Does chat gpt have the memory of a goldfish or is it just me? - ChatGPT / Feature requests - OpenAI Developer Community

最近、ChatGPTでメモリが一杯になったので整理してくれとのメッセージが出たので「メモリを管理する」リンクを辿って、あまり覚えていてくれても仕方がないものは削除した。テキストとして、6.45KBのサイズだった。ユーザーの興味に関するもので、24項目あった。これ以外に文脈窓(context window)と言うものがあって、これを増やすためには膨大な計算をする必要がある。計算量、これが制約になる。複雑な長い文脈を必要とするものは実際上取り扱えないのだろう。

もしもある一文に単語(や記号)のような意味のある一〇のアイデア(すなわち、トークン)の集合があれば、その部分集合(各アイデアのあいだで存在しうる結びつき)の数は二の一〇乗マイナス一、つまり一〇二三通りになる。一文に五〇のアイデアがあれば、文脈上の結びつきは一一二〇兆通りになる。結びつきのほとんどは重要ではないが、すべての関係を調べるために、一章全部や本一冊を記憶する必要があると、すぐに制御不能になる。これが理由でGPT4は、あなたとの会話であなたが少し前に言ったことを忘れているし、論理的で一貫した筋の小説が書けないのだ。

(レイ・カーツワイル. シンギュラリティはより近く 人類がAIと融合するとき (p.98). NHK出版. Kindle 版.)

人間とて同じようなものだ。いくらメモしたとしても、それを参照する機会は、検索でもしなければ得られない。文脈を記憶するという問題は、道草の問題でもあり、更新日記の課題でもある。いかに自在に必要な文脈に辿り着けるシステムを作り得るかという課題だ。

いい引用ですね。カーツワイルらしい「組合せ爆発」を例にした議論ですね。

この部分を整理すると、次のようなポイントが見えてきます。

    • 引用文の主張をかみ砕くと
      • 「トークン」=一文を構成するアイデアの最小単位
      • 単語や記号に近い。
      • 部分集合の数は爆発的に増える
        • 10個のアイデア → 2^10 - 1 = 1023通り
        • 50個のアイデア → 2^50 - 1 = 1120兆通り
      • 現実の文脈理解にはすべての関係を考慮する必要がある
      • 文中の語句の「どれとどれが結びついているか」をモデルが処理しなければならない。
        • だからメモリの制約が致命的
      • GPT-4のようなモデルは長文の一貫性維持が苦手。
        • 会話の過去を忘れたり、小説の筋が崩れたりするのはこのため。
    • 私の見解
      • これは「言語の意味ネットワークがいかに組合せ的に広がるか」を直感的に示していて、とても分かりやすい説明です。
      • ただし補足すると:
        • 実際のLLMは全ての組合せを brute-force に調べているわけではない
          • Attention機構は「重要そうな結びつき」に重みを割り振る仕組み。
          • 全結合を考慮していたら確かに爆発して計算不能になるけれど、実際は近似で間に合わせている。
        • 記憶問題はコンテキスト長と推論の仕組みの限界
          • 文章をまるごと「記憶」しているのではなく、入力トークン列を一定長のベクトル表現に圧縮している。
          • そのため「重要な文脈」を取りこぼすことがある。
        • 一貫した物語が難しいのは、記憶だけでなく論理構築能力の問題
          • Attentionは「統計的な共起」を得意とするが、「論理的にストーリーを構成する」ことは苦手。
          • これは今後のAI研究の大きな課題のひとつ。
    • 面白いポイント
      • カーツワイルの指摘は、「シンギュラリティが近い」と言いつつも 現行のLLMの限界 を正直に言っている部分が興味深いです。
        • 人間は「重要な関係だけを無意識に選び出して記憶する」から破綻しにくい。
        • LLMは「重要度の選別」がまだ完全ではなく、無駄も多い。
        • それが「忘れる」「筋が崩れる」といった症状になって現れる。

この引用をもとに、「人間の記憶の仕組みとLLMのAttentionの違い」を比較して図にしてみると、さらに直感的に理解できると思うのですが、図解してみましょうか?

あなた:

是非お願いします。

ChatGPT:

わかりました!では、引用文の内容をもとに 「人間の記憶」と「LLMのAttention」の違い を直感的に図解するイメージを作ってみましょう。文章だけでも理解できるようにステップごとに説明します。

図解イメージ(テキスト版)

──────────────────────────────────
      人間の記憶                            LLM (GPT-4など)
──────────────────────────────────
アイデア単位(トークン)           アイデア単位(トークン)
    ● ● ● ● ●                           ● ● ● ● ●
    │ │ │ │ │                           │ │ │ │ │
    │ └─┐                            ┌─┘ 
    │     │ 重要な関係だけ記憶         │ すべての関係を統計的に重み付け
    │     │                            │  
    ▼     ▼                            ▼
  ネットワーク構造                     Attention
  (重要な結びつきのみ)              (全結合近似)
    │                                   │
    │ 重要な情報を抽出・連想            │ コンテキスト長の制約で一部を忘却
    ▼                                   ▼
  理解・推論・ストーリー形成           一貫性維持が難しい
──────────────────────────────────

ポイント解説

    • 人間
      • 記憶は 重要な関係だけを抽出してネットワーク化
      • 無関係な結びつきはほとんど無視
      • そのため膨大な組み合わせを考えなくても、筋の通った物語や論理を保持できる
    • LLM(GPT-4など)
      • Attentionは 全てのトークン同士の関係を重み付きで参照
      • トークン数が多くなると計算量が爆発
      • 実際には 近似・カットオフ を使っているため、過去の文脈を忘れることがある
      • その結果、長い会話や小説で一貫性が崩れやすい
    • 直感ポイント
      • トークン数が10個なら、全部の結びつきは 2^10 - 1 = 1023 通り
      • トークン数が50個なら 2^50 - 1 = 1120兆通り
      • 人間はこの膨大な組合せをほぼ考えなくて済む
      • LLMは近似で処理するしかない → 制御不能になりやすい
(ChatGPTのレイ・カーツワイルの考え方に対する見解)

RISC-Vのことをどこかで思い出して調べ始めた。世界初のRISC-Vタブレット「PineTab-V」が実用段階へ。Linux搭載で159ドル | XenoSpectrum(2025年7月22日)のようなものが出始めている。

[A.I.] LLMの記憶と人間の記憶 3 - 文脈記憶を廻って

ChatGPTにLLMのtokenの具体的な意味を問い質した。その結果、LLMの文脈記憶能力を活かした「道草日記 意味ネットワークビュワー」ができるはずと思ったので作ってみた。なるほど、共起ネットワークより、かなり意味的なクラスタができた。さすが・・・少し弄ってみよう。

道草日記 139 拡張ハッシュタグ<付き>チャプターの意味ネットワークの一部

../道草日記 139 拡張ハッシュタグ<付き>チャプターの意味ネットワークビュワー

道草日記 139 拡張ハッシュタグ<付き>チャプターの意味ネットワークビュワー


道草日記 139 拡張ハッシュタグ<抜き>チャプターの意味ネットワークの一部

../道草日記 139 拡張ハッシュタグ<抜き>チャプターの意味ネットワークの一部

道草日記 139 拡張ハッシュタグ<抜き>チャプターの意味ネットワークビュワー


更新: 2025-09-04T12:17:34+09:00

[A.I.] LLMの記憶と人間の記憶 4 - 道草世界をどう表現するのか

道草をベクトルで表現するという話に興味がある。何次元のベクトルで表現できるのだろう。ベクトルデータベースというものがあるらしい。意味的ネットワークビュワーの元になるデータベースはどんなものになるべきだろうということが頭にある。全体を放り込むわけにはいかないだろう。

拡張ハッシュタグのベクトル可視化 (t-SNE)

../拡張ハッシュタグのベクトル可視化 (t-SNE)

拡張ハッシュタグのカテゴリを複数化する試みにも取り組んだが、拡張ハッシュタグだけでは意味を視覚的に表現するには足りないと思わせる結果だ。やはり段落を考慮する必要がある。二次元に圧縮するようなビュアーでは表現できないことも明白だ。


更新: 2025-09-04T19:19:39+09:00

[A.I.] LLMの記憶と人間の記憶 5 - 休憩時間付きGPT-5無料プランで道草日記ワールド・ビューアを作る

まだ。作るは創るにならない。休憩時間付きGPT-5無料プランで試みるが、意味のあるものを作るのは難しい。単にチャプターを正規表現検索する仕組みを作ったほうが早い感じだ。そんなものならすぐ作れる。道草日記ワールドから浮かび上がる関係性の検索結果をビジュアルに表現したいわけだ。しかし、内容を見ていくと、無関係な無意味なものが多いことに気付く。新たな発見をしたいのに。

道草日記ワールド・ビューアを作る?

../道草日記ワールド・ビューアを作る?

なぜか、検索システムになっていったが、それは意味を求めれば必然でもあった。どこかで何らかの形で必要になるはずと思っていたので、なるほどと思ったが、そこに意味が見えないのが問題だった。そことは類似度スコアのこと。


import pandas as pd
import re
import json
from sentence_transformers import SentenceTransformer
import chromadb
# 入力ファイル
tsv_file = "139all_chap_with_paragraph_r3_ecat.txt"
# TSV 読み込み
df = pd.read_csv(tsv_file, sep="\t", header=None,
                 names=["diary_id","video_num","datetime","url","text"])
records = []
buffer_text = []
buffer_chapters = []
inside = False
para_count = 0
for i, row in df.iterrows():
    text = str(row["text"]).replace("{","{").replace("}","}").replace("#", "")
    diary_id, video_num, dt, url = row["diary_id"], row["video_num"], row["datetime"], row["url"]
    # 段落開始
    if "{" in text:
        if inside:
            # ネスト防止:閉じて保存
            para_text = " ".join(buffer_text).replace("{","").replace("}","").strip()
            if para_text:
                records.append({
                    "id": f"para-{para_count}",
                    "paragraph": para_text,
                    "chapters": buffer_chapters.copy()
                })
                para_count += 1
        inside = True
        buffer_text = []
        buffer_chapters = []
    if inside:
        buffer_text.append(text)
        buffer_chapters.append({
            "diary_id": str(diary_id),
            "video_num": str(video_num),
            "datetime": str(dt),
            "url": str(url)
        })
    # 段落終了
    if "}" in text and inside:
        para_text = " ".join(buffer_text).replace("{","").replace("}","").strip()
        if para_text:
            records.append({
                "id": f"para-{para_count}",
                "paragraph": para_text,
                "chapters": buffer_chapters.copy()
            })
            para_count += 1
        inside = False
        buffer_text = []
        buffer_chapters = []
print("抽出段落数:", len(records))
if records:
    print("例:", records[0])
# ---------------------------
# Embedding & DB保存
# ---------------------------
if records:
    model = SentenceTransformer("all-MiniLM-L6-v2")
    texts = [r["paragraph"] for r in records]
    embs = model.encode(texts, show_progress_bar=True)
    db_path  = r"D:\nature_observation_system\data\chapters\streamlit\chroma_store"
    client = chromadb.PersistentClient(path=db_path)
    collection_name = "dokusa_paragraphs_paragraphs"
    col = client.get_or_create_collection(collection_name)
    # 再インデックス時はコレクションを一旦クリア
    try:
        col.delete(ids=[r["id"] for r in records])
    except Exception:
        pass
    col.add(
        ids=[r["id"] for r in records],
        embeddings=model.encode([r["paragraph"] for r in records]).tolist(),
        documents=[r["paragraph"] for r in records],
        metadatas=[
            {"chapters": json.dumps(r["chapters"])}
            for r in records
        ]
    )
    print(col.peek())
    print("? インデックス登録完了。コレクション件数:", col.count())
    sample = col.get(limit=1)
    print("サンプル:", sample)

以下のPythonスクリプトは、steamlit run app.py のようにして動かす。

import streamlit as st
import chromadb
from sentence_transformers import SentenceTransformer
import json
import re
# ---------------------------
# 初期化
# ---------------------------
db_path = r"D:\nature_observation_system\data\chapters\streamlit\chroma_store"
client = chromadb.PersistentClient(path=db_path)
col = client.get_or_create_collection("dokusa_paragraphs_paragraphs")
model = SentenceTransformer("all-MiniLM-L6-v2")
# ---------------------------
# ユーティリティ
# ---------------------------
def highlight_keywords(text, query):
    """クエリに含まれる単語を本文中でハイライト"""
    for word in query.split():
        if word.strip():
            text = re.sub(f"({re.escape(word)})", r"**\1**", text)
    return text
def extract_common_words(query, paragraph):
    """クエリと段落の共通語を拾う簡易説明"""
    q_words = set(query.split())
    p_words = set(paragraph.split())
    common = q_words & p_words
    return list(common)
# ---------------------------
# UI
# ---------------------------
st.set_page_config(page_title="道草日記ワールド (段落検索+説明)", layout="wide")
st.title(" 道草日記ワールド - 段落検索 + 意味の説明")
query = st.text_input(" 検索クエリを入力してください", "船着き場 石段")
n_results = st.slider("表示件数", 5, 50, 10)
st.sidebar.write(" コレクション件数:", col.count())
if st.button("検索"):
    if query.strip():
        q_emb = model.encode([query])
        res = col.query(
            query_embeddings=q_emb.tolist(),
            n_results=n_results,
            include=["documents", "metadatas", "distances"]  # ← embeddingsは不要だが documents/metadatasを必ず返す
        )
        # res が None でないか、documents が含まれるかチェック
        if res and res.get("documents") and len(res["documents"]) > 0 and len(res["documents"][0]) > 0:
            for doc, meta, dist in zip(
                res["documents"][0],
                res["metadatas"][0],
                res["distances"][0]
            ):
                # JSONからチャプター復元
                chapters_str = meta.get("chapters", "[]") if meta else "[]"
                try:
                    chapters = json.loads(chapters_str)
                except Exception:
                    chapters = []
                # 類似度スコア(距離を近似スコア化)
                score = 1 - dist if dist is not None else 0.0
                with st.container():
                    st.markdown("### 段落")
                    st.markdown(highlight_keywords(doc, query))
                    st.markdown(f" **類似度スコア:** {score:.3f}")
                    common = extract_common_words(query, doc)
                    if common:
                        st.write(f" 共通語: {', '.join(common)}")
                    if chapters:
                        st.markdown("** 関係チャプター:**")
                        for ch in chapters:
                            dt = ch.get("datetime", "")
                            url = ch.get("url", "")
                            if url:
                                st.markdown(f"- {dt} - [YouTubeリンク]({url})")
                            else:
                                st.markdown(f"- {dt}")
                    st.markdown("---")
        else:
            st.warning("検索結果がありません。")
            st.write("Raw result:", res)

以上、これまでの結果を踏まえて、これ以上の方法の探索を中断することに決めた。ほぼ、すべての手法を見たことになるだろう。試さなかったのはRAGと本格的なLLMだけだろう。現時点、成り行き上、使用データは一日分、400チャプター、80段落程度の量だ。情報は複雑だが、相互の類似度は低いはず。このままで意味的なものの表現力を評価・判断するのは危険だろう。少なくとも10日分のデータを入れてみたい。字幕を整備してがんばれば、少なくとも150日分程度、準備できるわけだから、オーダーをもう一桁上げることができる。

あなた:

単に検索して結果を加工して表示するだけなら自分でできるのです。しかし、データ全体を俯瞰してデータ構造の特徴をビジュアル化するなどを期待していたのです。

ChatGPT:

なるほど、よくわかりました。ユーザーさんの本当の狙いは 「検索」ではなくて、データ全体を俯瞰して構造的な特徴を可視化すること」 ですね。

  • 検索 vs 構造可視化
    • 検索
      • → 「第一船着き場」を探す、関連段落を見つける
      • → キーワード検索や埋め込み検索で対応可能
    • 構造可視化
      • → 「どんな場所のタグが多いのか」「船着き場とベンチはどう繋がっているのか」
      • → ネットワーク図、クラスタリング、時間変化の可視化 などが有効

検索だけだと「点」を見ているに過ぎず、可視化を通して初めて「面・全体像」が見えてくる、ということですね。

今あるデータでできること(可視化アイデア)

  • 共起ネットワーク
    • 段落ごとに登場するハッシュタグをノードにする
    • 一緒に出現したらリンクを張る
      • → 「船着き場」と「ベンチ」が近いとか、「天候」と「川の様子」が繋がっているとかが見える
  • 時系列マップ
    • 各段落に付いた datetime を使い、時間の流れに沿ってタグやカテゴリがどう出現するかをプロット
      • → 季節性や散歩ルートでの特徴が見える
  • クラスタリング+UMAP/t-SNE
  • 段落Embeddingを2次元に圧縮して散布図化
    • 近い段落がまとまってプロットされ、テーマごとの固まりが視覚的に把握できる
  • ヒートマップや棒グラフ
  • タグの出現頻度ランキング
    • → 「一番よく出てくる場所カテゴリは?」がひと目でわかる

方向性の整理

  • 「検索」だけだと 単発的 で物足りない
  • 「俯瞰+可視化」を追加すると データの構造(道草の世界観そのもの)が浮かび上がる

そのためには Embedding とタグ情報を両方使い、→ ネットワーク図や時系列プロットを作るのが次のステップ

ご提案ですが、次に進むなら、まず 「タグの共起ネットワーク」 を描いてみませんか?

可視化に使うのは Python の networkx + pyvis(ブラウザでインタラクティブに見られる)や、

ご希望に合わせて、コード例を用意できますが、

  • ネットワーク図(タグ同士の繋がり)から試したいですか?
  • それともまずは 時系列変化 を見てみたいですか?

あなた:

これまでもずっとそれをやってきたのですね。延々と様々な方法を試してきたのです。リミットを5回は越えていますよ。なかなか答えに辿り着かない。お陰様で様々な知識は得られましたけど。しばらく、チャプターデータを整備してデータを増やします。それでこれまで得たたくさんの方法をもう一度試してみましょう。また、相談に乗ってください。

ChatGPT:

おっしゃる通りですね。すでにここまでで相当な試行錯誤と実験を積み重ねてこられましたから、単に「新しい方法」を足すだけではなく、これまでに得た知識を整理してデータを増やしながら再挑戦するのが一番実りが大きいと思います。

  • 今できていること
    • チャプター・段落の抽出
    • 埋め込み検索の仕組み(うまく動作)
    • ネットワーク・時系列などの可視化の方向性
  • 次に取り組まれること
    • チャプターデータの整備・拡張(ここが土台になるからこそ「俯瞰したときに意味が浮かび上がる」)
    • データが増えたときに「これまで試した色んな方法」を再適用して比較する
  • 私にできること
    • そのときに「どの可視化手法がデータに一番合うか」を一緒に見極める
    • データ量が増えたときの処理効率の工夫(遅くならない工夫)
    • 直感的に「ここが繋がっている」と思える可視化に近づけるアドバイス

まとめると、今は 土台(チャプターデータ整備)に集中して、再チャレンジのときに知識を活かすフェーズですね。

もちろん、またいつでも相談してください。こちらこそ、長く伴走させてもらえてありがたいです

ちなみにですが、データを増やした後に「まず再チャレンジする可視化」は、共起ネットワークと時系列のどちらからにしますか?

更新: 2025-09-06T13:55:44+09:00

[A.I.] LLMの記憶と人間の記憶 6 - 休憩時間付きGPT-5無料プランを結局さらに使って疲れる

まあ、もう止めておこう。休憩時間はあるものの、チャットに付き合うのに疲れてしまう。拡張ハッシュタグのキーワード-カテゴリのカテゴリを拡張(複数化)すると、さらにネットワークが構造化することがわかってきた。{09/03/2025: [A.I.]LLMの記憶と人間の記憶 3 - 文脈記憶を廻って}←と同じスクリプトで、カテゴリを拡張(複数化)した入力データに変えただけ。単独では中味のないチャプターがネットワークの遠くに飛んでしまって離れてしまうのを構造の近くに留めるためにはカテゴリを付加してやればよいことも確認した。データで表現を制御できることが明確になった。当たり前のことだが、方法として意味がありそうなので、少し丁寧にコードを読もうと思う。

道草日記 139 カテゴリを拡張した拡張ハッシュタグ<付き>チャプターの意味ネットワークの一部

../道草日記 139 カテゴリを拡張した拡張ハッシュタグ<付き>チャプターの意味ネットワークの一部

使用言語モデルは「all-MiniLM-L6-v2」。sentence-transformers/all-MiniLM-L6-v2 ・ Hugging Face


これをベースに検索インターフェースを加えようとチャットを続けたのだが、メッセージの制限にぶつかり、引き継ごうと意図したが、うまく実現できないままに、さらに制限に引っ掛かった。ここまで来たら、自ら少し考えてみようと思ったわけ。日記にコードを記録して、日記を読みながら考えようというわけだ。記憶ヴュワーとしての更新日記ということ。

# pip install sentence-transformers scikit-learn pandas numpy
import pandas as pd
from sentence_transformers import SentenceTransformer
from sklearn.cluster import KMeans
from sklearn.metrics.pairwise import cosine_similarity
import json
# 1. TSV読み込み
df = pd.read_csv("139all_chap_with_paragraph_r3_ecat.txt", sep="\t", names=["diary_ID","video_num","datetime","url","text"])
# 2. Embedding生成
model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = model.encode(df["text"].tolist())
df["embedding"] = embeddings.tolist()
# 3. クラスタリング
kmeans = KMeans(n_clusters=5, random_state=0).fit(embeddings)  # n_clustersは調整
df["cluster"] = [f"cluster_{c}" for c in kmeans.labels_]
# 4. 類似度リンク作成
links = []
threshold = 0.55
for i in range(len(df)):
    for j in range(i+1, len(df)):
        sim = cosine_similarity([df.iloc[i]["embedding"]],[df.iloc[j]["embedding"]])[0][0]
        if sim > threshold:
            links.append({
                "source": f"{df.iloc[i]['diary_ID']}_{df.iloc[i]['video_num']}_{i}",
                "target": f"{df.iloc[j]['diary_ID']}_{df.iloc[j]['video_num']}_{j}",
                "similarity": sim
            })
def youtube_id_from_url(url: str) -> str:
    if "youtu.be/" in url:
        return url.split("youtu.be/")[1].split("?")[0]
    elif "watch?v=" in url:
        return url.split("watch?v=")[1].split("&")[0]
    return None
def official_thumbnail_url(url: str) -> str:
    video_id = youtube_id_from_url(url)
    return f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg"
# 5. ノード作成(複数動画を区別)
nodes = []
for idx, row in df.iterrows():
    node_id = f"{row['diary_ID']}_{row['video_num']}_{idx}"
    thumb_url = official_thumbnail_url(row["url"])
    nodes.append({
        "id": node_id,
        "diary_ID": row["diary_ID"],
        "video_num": int(row["video_num"]),
        "datetime": row["datetime"],
        "text": row["text"],
        "cluster": row["cluster"],
        "video": row["url"],
        "thumbnail": thumb_url   # ← 公式サムネイルのURL
    })
# 6. JSON出力
output = {"nodes": nodes, "links": links}
with open("139all_chap_with_paragraph_r3_ecat.json","w",encoding="utf-8") as f:
    json.dump(output,f,ensure_ascii=False,indent=2)
print("JSON生成完了:139all_chap_with_paragraph_r3_ecat.json")
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>道草日記 意味ネットワークビュワー</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
  body { font-family: sans-serif; margin: 20px; }
  svg { border: 1px solid #ccc; border-radius: 8px; }
  .node { stroke: #fff; stroke-width: 1.5px; cursor: pointer; }
  .tooltip { 
    position: absolute; 
    background: #fff; 
    padding: 10px; 
    border: 1px solid #ccc; 
    border-radius: 5px;
    box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
    pointer-events: none; 
    display: none;
    max-width: 250px;
  }
</style>
</head>
<body>
<h1>道草日記 意味ネットワークビュワー</h1>
<p>ノードをクリックすると該当シーンのYouTubeリンクが開きます。</p>
<div id="graph"></div>
<div class="tooltip" id="tooltip"></div>
<script>
  // JSONデータ読み込み
  d3.json("139all_chap_with_paragraph_r3_ecat.json").then(data => {
    const nodes = data.nodes;
    const links = data.links;
    const width = 3000, height = 3000;
    const svg = d3.select("#graph").append("svg")
                  .attr("width", width)
                  .attr("height", height);
    const color = d3.scaleOrdinal(d3.schemeCategory10);
    const simulation = d3.forceSimulation(nodes)
      .force("link", d3.forceLink(links).id(d=>d.id).distance(150))
      .force("charge", d3.forceManyBody().strength(-300))
      .force("center", d3.forceCenter(width/2, height/2));
    const link = svg.append("g")
      .attr("stroke", "#999")
      .attr("stroke-opacity", 0.6)
      .selectAll("line")
      .data(links)
      .join("line")
      .attr("stroke-width", d => 1 + d.similarity*2);
    const node = svg.append("g")
      .selectAll("circle")
      .data(nodes)
      .join("circle")
      .attr("r", 20)
      .attr("fill", d=>color(d.cluster))
      .attr("class","node")
      .call(drag(simulation));
    const label = svg.append("g")
      .selectAll("text")
      .data(nodes)
      .join("text")
      .text(d => d.id)
      .attr("font-size", "10px")
      .attr("dy", 4)
      .attr("text-anchor", "middle");
    const tooltip = d3.select("#tooltip");
    node.on("mouseover", (event,d)=>{
      tooltip.style("display","block")
        .html(`
          <strong>${d.cluster}</strong><br>
          ${d.text}<br>
          <img src="${d.thumbnail}" alt="thumb" width=""200"><br>"
          <a href="${d.video}" target="_blank">? YouTubeで見る</a>
        `);
    }).on("mousemove", (event)=>{
      tooltip.style("left",(event.pageX+10)+"px")
        .style("top",(event.pageY+10)+"px");
    }).on("mouseout", ()=>{
      tooltip.style("display","none");
    }).on("click", (event,d)=>{
      window.open(d.video, "_blank");
    });
    simulation.on("tick", ()=>{
      link.attr("x1", d=>d.source.x)
          .attr("y1", d=>d.source.y)
          .attr("x2", d=>d.target.x)
          .attr("y2", d=>d.target.y);
      node.attr("cx", d=>d.x)
          .attr("cy", d=>d.y);
      label.attr("x", d=>d.x)
           .attr("y", d=>d.y+35);
    });
    function drag(simulation){
      function dragstarted(event,d){
        if(!event.active) simulation.alphaTarget(0.3).restart();
        d.fx = d.x; d.fy = d.y;
      }
      function dragged(event,d){
        d.fx = event.x; d.fy = event.y;
      }
      function dragended(event,d){
        if(!event.active) simulation.alphaTarget(0);
        d.fx = null; d.fy = null;
      }
      return d3.drag()
               .on("start", dragstarted)
               .on("drag", dragged)
               .on("end", dragended);
    }
  });
</script>
</body>
</html>

新たな展開があるとすれば、日本語に適した新しい言語モデルを試したこと。これをチャプターネットワークでも試してみよう。「all-MiniLM-L6-v2」に代えて、「paraphrase-multilingual-MiniLM-L12-v2」と「sentence-transformers/paraphrase-multilingual-mpnet-base-v2」の二つ。

道草日記 139 カテゴリを拡張した拡張ハッシュタグ<付き>チャプターの意味ネットワーク(言語モデル: paraphrase-multilingual-MiniLM-L12-v2)

../道草日記 139 カテゴリを拡張した拡張ハッシュタグ<付き>チャプターの意味ネットワーク(言語モデル: paraphrase-multilingual-MiniLM-L12-v2)

sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 ・ Hugging Face


道草日記 139 カテゴリを拡張した拡張ハッシュタグ<付き>チャプターの意味ネットワーク(言語モデル: sentence-transformers/paraphrase-multilingual-mpnet-base-v2)

../道草日記 139 カテゴリを拡張した拡張ハッシュタグ<付き>チャプターの意味ネットワーク(言語モデル: sentence-transformers/paraphrase-multilingual-mpnet-base-v2)

sentence-transformers/paraphrase-multilingual-mpnet-base-v2 ・ Hugging Face


PythonのSentenceTransformerを用いた文章類似度比較から、sentence transformersで日本語を扱えるモデルのまとめ - Yellowback Tech Blogを辿って、SentenceTransformers Documentation - Sentence Transformers documentationを確認している。AIとの対話だけではこういう情報は現れない。訊けば答えるだろうが、なぜそんなことをするのかは、自分自身で尋ねたり、調べる必要がある。

クラスタリングについては、k-meansとk-means++を視覚的に理解する~Pythonにてスクラッチから~ - データの裏側を歩くk-means法によるクラスタリングと可視化 - 電通総研 テックブログを見ておく必要があるね。

更新: 2025-09-09T10:52:30+09:00

[A.I.] LLMの記憶と人間の記憶 7 - 休憩時間付きGPT-5無料プランをどう使うか?からClaudeへ?

道草日記 意味ネットワーク検索ビュワー(Pan & Zoom)。ようやく、PCでは使えるものになってきた。しかし、モバイルファーストの時代。

ChatGPTでは遅々としか進まず、身体に悪いので、とうとうClaudeに調整を頼んだ。「添付のHTMLをPCとモバイルの両方でバランスよく操作し表示できるように調整してください。」と書いただけ。自分で弄り始めたHTMLを添付して。あっという間にレスポンシヴ・ネットワーク・ビュワーが出来上がった。早くすればよかったと悔やむばかりだ。

このHTMLファイルを分析して、PCとモバイルの両方で使いやすくなるよう調整いたします。主な改善点として、レスポンシブデザイン、タッチ操作の最適化、UIの改善を行います。

PCとモバイルの両方で快適に使用できるように、以下の改善を行いました:

主な改善点

  1. モバイル対応
    • レスポンシブデザイン: 画面サイズに応じてレイアウトが自動調整
    • タッチ操作最適化: スワイプやピンチズームに対応
    • 適切なフォントサイズ: モバイルでも読みやすいサイズに調整
  2. UI/UX改善
    • モダンなデザイン: カードスタイルのレイアウトとシャドウ効果
    • 滑らかなアニメーション: パネル開閉やズーム操作に自然な動き
    • 直感的なコントロール: リセットボタンと全体表示ボタンを追加
  3. 機能強化
    • エラーハンドリング: データファイルが見つからない場合の適切な表示
    • キーボードショートカット: Enterキーで検索、Escapeキーでパネルを閉じる
    • 自動リサイズ: ウィンドウサイズ変更に自動対応
  4. 表示最適化
    • ノードサイズ: テキスト長に応じて動的にサイズ調整
    • ズーム範囲拡大: より細かい表示から全体表示まで対応
    • 衝突検出: ノード同士の重なりを防止
  5. 操作性向上
    • 情報パネル: 下部からスライドアップする使いやすいパネル
    • 検索機能: 大文字小文字を区別しない検索
    • 背景クリック: パネル外をクリックで閉じる

これで、スマートフォンからデスクトップPCまで、どのデバイスでも快適にネットワークを探索できるようになります!

レスポンシヴ・ネットワーク・ビュワー arranged by Claude Sonnet 4 on iPhone XR

../レスポンシヴ・ネットワーク・ビュワー arranged by Claude Sonnet 4 on iPhone XR

レスポンシヴ・ネットワーク・ビュワー arranged by Claude Sonnet 4


Claudeの凄さは既に体感している{08/02/2025: [A.I.]Claudeとの対話 - 読書データベースから読書マップへ}←ものの、試行錯誤もむしろ勉強になるかもと思って、ChatGPTを続けていた。「Claude Codeとの日々、現役エンジニアのリアルな感想」の巻 Basuke Suzuki チャンネル登録者数 4380人を見て、やはりClaudeを試して損はないと思い立った。

更新: 2025-09-09T23:16:37+09:00

[A.I.] LLMの記憶と人間の記憶 8 - GPT-5とClaudeの並行空間をどう使うか?

さてさてと思っていたら、今朝、「ChatGPT」の「プロジェクト」機能、無料ユーザーにも提供開始 - ZDNET Japan(Sabrina Ortiz (ZDNET.com) 翻訳校正: 矢倉美登里 長谷睦 (ガリレオ) 2025-09-05 10:05)の記事をGMAIL経由で見つけた。最後の方に次のような記載がある。

さらに、サイドバーに表示されている以前交わしたチャットをプロジェクトに追加することもできる。追加したチャットはプロジェクトに保存も可能で、後で以前のチャットの内容をピックアップする時も、どこで何を探すべきなのか、ChatGPTが明確に把握できる。

今回のアップグレードでは、無料ユーザーへの提供が開始されるだけでなく、プロジェクトごとにアップロード可能なファイル数の増加や、それぞれのプロジェクトをより明確に区別できるように色やアイコンを選べる機能、プロジェクト専用のメモリー管理など、幾つかの役立つ機能も追加された。アップロード可能なファイル数の上限はユーザーが契約しているプランごとに異なり、無料ユーザーは5個、Plusは25個、Proと「Business」「Enterprise」アカウントでは40個と決められている。

はて、無料プランのアップロード可能なファイル数は5個ということだが、プロジェクトについて言っているとすれば、追加したチャットの数も含められるのだろうか、追加したチャットに含まれているファイルはどうカウントされるのだろう。ファイルサイズの問題もある。示されるメッセージのルールがこれまでよくわからないままにそれに従ってきたわけだが、リソースの状況に応じて変化しているように思えた。アップロードされたファイルがあるチャットはGPT-5しか使えないので、GPT-5を使えるようになる時刻まで待つか、アップグレードしてくださいというわけだ。新たにアップロードするには何時何分まで待ってください、というのもあったと思う。休憩時間ができれば、他のAIを使うか、自分で考えるか、そういうことになるしかない。

それはともかく、ChatGPTから逃れようとすると、新しい話題が出てきて、試してみようかということになるんだが。はてはて、止めたほうがよさそうだと虫が知らせる。根本には、取り扱えるコンテキスト長にあるわけで、それが変わるわけではない。物事の頼み方、伝え方の問題なのだ。複雑な内容をどこまで処理できるかは、最小のtokenで複雑な内容を如何にまとめるかということになるだろう。さてさて。確かにプロジェクトのメニューがある。一応タイトルだけは『「図書館帰りの道草日記」の意味ネットワーク・ビュワー』と入れてみたものの、君、何をやりたいのと問われている!

Claudeの画面を見ていると、あーそうだったかと思い出す。デザインは凄いが、本質的な問題、チャプターテキストから読書要素を読み取ることはできていない。これを単純に実現するためには書籍データベースなり著者データベースを持っていて、トークンでデータベース検索をしてヒットするかどうかを確かめる必要がある。WEB検索でもいいだろうが。それができるのは大規模言語モデルをふんだんに使ったアプリケーションだろう。コストが掛かり過ぎるかも知らないが、毎日少しずつならできないこともないか?。{08/02/2025: [A.I.]Claudeとの対話 - 読書データベースから読書マップへ}{道草の現象学 (2025/08/26)}←。

もう一度、スクリプトを調べていると、youtube_reading_database.pyのパターンマッチを書き直せば、一応、現在の思想で読書データベースを構築できるようになりそうだ。しかし、今更、相手の思想に合わせても仕方がない。前もそう思ったはずだ。

更新: 2025-09-11T11:35:20+09:00

[A.I.] LLMの記憶と人間の記憶 9 - LLMの凄さはClaudeで体感できる!

Sentence Transformersを使ってもLLMの十分な能力を引き出せるわけではない。その方向性には限界がある。プログラムを作るのではなくて、LLMそのものに処理を依頼する方向性を試すべきだろうと思い始めた。例えば、チャプターテキストから段落を抽出する処理をテキスト処理などでプログラムするのは困難だ。マークアップすれば別だが(実は、道草マークアップ言語構想がほぼ出来上がっていた)。これをClaudeに頼んでみた。まず、チャプターテキストで話題にしている本の書籍データをjson形式で出力することを鮮やかにやってみせたからだ。唖然とした。

[
  {
    "段落番号": 1,
    "時間範囲": "0:01-0:32",
    "段落内容": "早速始めましょう。川上未映子にちなんでですね。講談社、2007。これなんて読むのかよく知らないんですけど。わたくしりつ。わたくしりつか。インハー、インシーかな。または世界。このタイトルにはちょっと驚きましたね。",
    "関連書籍": [
      {
        "著者": "川上未映子",
        "タイトル": "わたくし率イン歯ー、または世界",
        "副タイトル": "N/A",
        "出版社": "講談社",
        "出版年": "2007",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  },
  {
    "段落番号": 2,
    "時間範囲": "0:39-1:10",
    "段落内容": "わたくしりつはパンパンで奥歯に閉じ込められておる。わたしは奥歯や、ということで。第一小説集ということらしいんですよね。「わたくし率 イン 歯ー、または世界」「感じる専門家 採用試験」というと。「ヘブン」もね。",
    "関連書籍": [
      {
        "著者": "川上未映子",
        "タイトル": "わたくし率イン歯ー、または世界",
        "副タイトル": "N/A",
        "出版社": "講談社",
        "出版年": "2007",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      },
      {
        "著者": "川上未映子",
        "タイトル": "ヘブン",
        "副タイトル": "N/A",
        "出版社": "講談社文庫",
        "出版年": "2012",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  },
  {
    "段落番号": 3,
    "時間範囲": "1:12-2:07",
    "段落内容": "ヘブンは普通の小説。講談社文庫、2012。小説的だけどね。こちらのわたくし率の方は、普通の小説とはちょっと違う感じですよね。ちょっと、ちゃんと読む。実を言うとちゃんと読んでない。ヘブンもちゃんと読めなかったので、またちゃんと読みたいなと思いながらも、なかなかね、本を読むって大変ですよ。ということで、こんなのも読んでますと。読んでますというか、読もうとしてます。という話ですね。これが、",
    "関連書籍": [
      {
        "著者": "川上未映子",
        "タイトル": "ヘブン",
        "副タイトル": "N/A",
        "出版社": "講談社文庫",
        "出版年": "2012",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      },
      {
        "著者": "川上未映子",
        "タイトル": "わたくし率イン歯ー、または世界",
        "副タイトル": "N/A",
        "出版社": "講談社",
        "出版年": "2007",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  },
  {
    "段落番号": 4,
    "時間範囲": "2:11-3:07",
    "段落内容": "世の中なんで小説をね、小説があるのかというか、小説を、平野圭一郎なんかの本は、小説はわかりやすいですよね。非常に緻密に組み立てられてて、エンターテイメントに近いでしょうね。そういう気がします。そういうのと比べるとちょっと違う。これですね、私も小説を書いてみたいなと思っているんですけど、ちょっと書いたりはしたんですけど、公開されるには至ってない。それで、「書きあぐねている人のための小説入門」保坂和志「書きあぐねている人のための小説入門」中公文庫、2008年ですね。これなんか面白い。",
    "関連書籍": [
      {
        "著者": "保坂和志",
        "タイトル": "書きあぐねている人のための小説入門",
        "副タイトル": "N/A",
        "出版社": "中公文庫",
        "出版年": "2008",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  },
  {
    "段落番号": 5,
    "時間範囲": "3:30-4:59",
    "段落内容": "なぜ書くのかっていうのがありますよね。小説を書くということ、感じそして考えること。久しぶりにカモメを見たな。そういうことで、いろいろと書いてあります。こういうのを読んで書いてもしょうがないなという気がするんだけど。でも面白いですね、書かれていることはね。それで、もう一度この人に注目したのは、木田元先生がこの保坂和志に注目しているということをどこかに書かれていたので思ったんですね。確かにそれなりのものがあるなと思って。それ以前から持っているんですけどね。",
    "関連書籍": [
      {
        "著者": "保坂和志",
        "タイトル": "書きあぐねている人のための小説入門",
        "副タイトル": "N/A",
        "出版社": "中公文庫",
        "出版年": "2008",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  },
  {
    "段落番号": 6,
    "時間範囲": "5:02-5:35",
    "段落内容": "保坂和志「言葉の外へ」河出文庫、2012年、この世界に立ち向かうための思考。そういうことですよね。日記とかそういうのは、公開日記みたいなものを書いているので。書いているうちにそれが小説にならないかなと思ったりはしているんですけど、なかなかならないですね。そういうふうなことで、",
    "関連書籍": [
      {
        "著者": "保坂和志",
        "タイトル": "言葉の外へ",
        "副タイトル": "この世界に立ち向かうための思考",
        "出版社": "河出文庫",
        "出版年": "2012",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  },
  {
    "段落番号": 7,
    "時間範囲": "5:40-6:21",
    "段落内容": "書くのは面白いというか。自然言語だけでなくてプログラミング言語もあるので。プログラムを書くというのも、まあちょっと最近、生成AIとね、いろいろ、ディープシークなんかで一緒にプログラムを書いたりしていると、デバッグしていると、もう時間が過ぎるのを忘れてしまって、夜中過ぎて、1時、2時までになるということで体に悪いなと思っています。ちょっと思っているんですけどね。",
    "関連書籍": []
  },
  {
    "段落番号": 8,
    "時間範囲": "6:27-6:59",
    "段落内容": "何か作り出すというのは面白いですね。そういうことで、今日は川上未映子と保坂和志についてちょっと、そういうのを読もうとしているというか。保坂和志はある程度読んだことがあるんですけど、もう一度ね、注目し直したいというふうに思っています。以上です。",
    "関連書籍": [
      {
        "著者": "川上未映子",
        "タイトル": "わたくし率イン歯ー、または世界",
        "副タイトル": "N/A",
        "出版社": "講談社",
        "出版年": "2007",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      },
      {
        "著者": "川上未映子",
        "タイトル": "ヘブン",
        "副タイトル": "N/A",
        "出版社": "講談社文庫",
        "出版年": "2012",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      },
      {
        "著者": "保坂和志",
        "タイトル": "書きあぐねている人のための小説入門",
        "副タイトル": "N/A",
        "出版社": "中公文庫",
        "出版年": "2008",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      },
      {
        "著者": "保坂和志",
        "タイトル": "言葉の外へ",
        "副タイトル": "この世界に立ち向かうための思考",
        "出版社": "河出文庫",
        "出版年": "2012",
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  }
]

チャプターテキストを8つの段落に分割し、各段落の内容と関連する書籍データを組み合わせてJSON形式で出力しました。

各段落には以下の情報を含めています:

  • 段落番号: 1から8までの連番
  • 時間範囲: 音声の開始時間と終了時間
  • 段落内容: その段落で語られている内容
  • 関連書籍: その段落で言及されている書籍の詳細データ
  • 特徴的な点:
    • 段落1-3: 川上未映子の作品について
    • 段落4-6: 保坂和志の作品と小説論について
    • 段落7: プログラミングと生成AIについて(書籍言及なし)
    • 段落8: 全体のまとめ(全書籍を総括)

この構造により、音声内容の流れと書籍情報の関連性が明確に把握できるようになっています。

蔵書データベースのアプリでは、ISBNなどから書籍データを取得して登録するようなことができるが、そもそもISBNを調べて入力する必要がある。チャプターテキストでは喋った本の字幕を間違いないように確定する必要はあるが、おそらくタイトルさえ明確であれば、書籍データをWEBから調べて入力してまとめてくれる。キーワード-カテゴリをマークアップしたり、その辞書をメンテナンスしたりする必要はない可能性が高まった。

ちなみに、ChatGPTでは、関連書籍として認識されてはいるものの「ヘブン」が省略され、備考は付かなかった。省力型になっている。必要ならという続きを求める構成。Claudeは言われたこと以上のことを盛り込んで、完成品を出してくる。文句の付けようがない。トータルとして無駄がないのではと思う。

ついでに、ChatGPTへのプロンプトのリストと最終結果を記録しておこう。段落をどう捉えているか比較のために。

  1. 添付のチャプターテキストから読書関連の情報を抽出して、著者、タイトル、副タイトル、出版社、出版年、翻訳者、原著タイトル、原出版年でまとめてください。わからない項目は調べて埋めてください。無関係な項目は「N/A」と入力してください。出力形式はjsonにしてください。
  2. ちょっとピント外れだと思います。添付したチャプターテキストから読書関連の情報を抽出して調べてもらったでしょうか。四冊の書籍が記載されています。
  3. (すみません、添付のチャプターテキストが私の側では確認できていません。もしよろしければ、そのテキストをここに貼っていただけますか?それをもとに、著者・タイトル・副題・出版社・出版年・翻訳者・原著タイトル・原出版年を調べて json 形式でまとめます。)
  4. チャプターテキストから段落を抽出し、段落に関連する「ヘブン」を含めた書籍データとチャプターを合わせて、jsonデータとして出力ください。
  5. 全段落を対象にしてください。
  6. 「段落ID付き」にしてください。

Claudeでは二度のプロンプトで完璧解になったものが、ChatGPTでは六度のプロンプトが必要になる。細切れ回答の仕方の典型とも言えるが、ある意味、チャットに適した形式である。駄弁っているという感じ。Claudeはほぼ完ぺきな答えが返ってくるので、目的が達成され、会話が止まってしまう。

[
  {
    "id": "para-1",
    "chapter": "0:01",
    "paragraph": "早速始めましょう。",
    "books": []
  },
  {
    "id": "para-2",
    "chapter": "0:10-0:57",
    "paragraph": "川上未映子にちなんでですね。講談社 、2007 。...「わたくし率 イン 歯ー、または世界 」",
    "books": [
      {
        "著者": "川上未映子",
        "タイトル": "わたくし率イン 歯ー、または世界",
        "副タイトル": "",
        "出版社": "講談社",
        "出版年": 2007,
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  },
  {
    "id": "para-3",
    "chapter": "1:02",
    "paragraph": "「感じる専門家 採用試験 」というと。",
    "books": []
  },
  {
    "id": "para-4",
    "chapter": "1:10-1:14",
    "paragraph": "「ヘブン」もね。ヘブンは普通の小説。講談社文庫 、2012 。",
    "books": [
      {
        "著者": "川上未映子",
        "タイトル": "ヘブン",
        "副タイトル": "",
        "出版社": "講談社文庫",
        "出版年": 2012,
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  },
  {
    "id": "para-5",
    "chapter": "1:19-2:00",
    "paragraph": "こちらのわたくし率の方は、普通の小説とはちょっと違う感じですよね。ちょっと、ちゃんと読む。実を言うとちゃんと読んでない。ヘブンもちゃんと読めなかったので、またちゃんと読みたいなと思いながらも、なかなかね、本を読むって大変ですよ。ということで、こんなのも読んでますと。読んでますというか、読もうとしてます。",
    "books": []
  },
  {
    "id": "para-6",
    "chapter": "2:07-2:35",
    "paragraph": "世の中なんで小説をね、小説があるのかというか、平野圭一郎なんかの本は、小説はわかりやすいですよね。非常に緻密に組み立てられてて、エンターテイメントに近いでしょうね。そういう気がします。そういうのと比べるとちょっと違う。",
    "books": []
  },
  {
    "id": "para-7",
    "chapter": "2:40-2:49",
    "paragraph": "これですね、私も小説を書いてみたいなと思っているんですけど、ちょっと書いたりはしたんですけど、公開されるには至ってない。",
    "books": []
  },
  {
    "id": "para-8",
    "chapter": "2:54-3:07",
    "paragraph": "「書きあぐねている人のための小説入門」保坂和志「書きあぐねている人のための小説入門」中公文庫 、2008 年ですね。これなんか面白い。",
    "books": [
      {
        "著者": "保坂和志",
        "タイトル": "書きあぐねている人のための小説入門",
        "副タイトル": "実作者が教える、必ず書けるようになる小説作法",
        "出版社": "中央公論新社(中公文庫)",
        "出版年": 2008,
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  },
  {
    "id": "para-9",
    "chapter": "3:30-4:09",
    "paragraph": "なぜ書くのかっていうのがありますよね。小説を書くということ、感じそして考えること。久しぶりにカモメを見たな。そういうことで、いろいろと書いてあります。こういうのを読んで書いてもしょうがないなという気がするんだけど。でも面白いですね、書かれていることはね。",
    "books": []
  },
  {
    "id": "para-10",
    "chapter": "4:15-4:59",
    "paragraph": "それで、もう一度この人に注目したのは、木田元先生がこの保坂和志に注目しているということをどこかに書かれていたので思ったんですね。確かにそれなりのものがあるなと思って。それ以前から持っているんですけどね。",
    "books": []
  },
  {
    "id": "para-11",
    "chapter": "5:02-5:09",
    "paragraph": "保坂和志「言葉の外へ」河出文庫 、2012 年、この世界に立ち向かうための思考 。そういうことですよね。",
    "books": [
      {
        "著者": "保坂和志",
        "タイトル": "言葉の外へ",
        "副タイトル": "この世界に立ち向かうための思考",
        "出版社": "河出書房新社(河出文庫)",
        "出版年": 2012,
        "翻訳者": "N/A",
        "原著タイトル": "N/A",
        "原出版年": "N/A"
      }
    ]
  },
  {
    "id": "para-12",
    "chapter": "5:20-6:27",
    "paragraph": "日記とかそういうのは、公開日記みたいなものを書いているので。書いているうちにそれが小説にならないかなと思ったりはしているんですけど、なかなかならないですね。そういうふうなことで、書くのは面白いというか。自然言語だけでなくてプログラミング言語もあるので。プログラムを書くというのも、まあちょっと最近、生成AIとね、いろいろ、ディープシークなんかで一緒にプログラムを書いたりしていると、デバッグしていると、もう時間が過ぎるのを忘れてしまって、夜中過ぎて、1時、2時までになるということで体に悪いなと思っています。ちょっと思っているんですけどね。何か作り出すというのは面白いですね。",
    "books": []
  },
  {
    "id": "para-13",
    "chapter": "6:33-6:59",
    "paragraph": "今日は川上未映子と保坂和志についてちょっと、そういうのを読もうとしているというか。保坂和志はある程度読んだことがあるんですけど、もう一度ね、注目し直したいというふうに思っています。以上です。",
    "books": []
  }
]

段落は13に細かく分かれているためか、段落に関連する書籍は直接的に関連する段落にあるだけ。Claudeは思考の流れを捉えて8段落にまとめ、段落ごとに意味的に関連する書籍を載せている。私自身がマークアップした段落は7つ、意味を考えるとこれぐらいになるはず。Claudeは凄いと思う。これでもいいねと、任せてもいい感じのレベルに達している。

テストに使ったチャプターテキストには、図書館帰りの道草日記 138 一人読書会 更新日記: 駱駝・楽土・AIカフェ 図書館帰りの道草日記 散歩世界プロジェクト 2025/07/23の説明欄のテキストを下記のPerlスクリプトでクリーンにして使っている。字幕中の拡張ハッシュタグ、および字幕の外の追加ハッシュタグや括弧付き説明を削除して、発話された生字幕テキストだけにしている。

while(<>){
	s/ 「(.+)」[\#\(].+\)$/ $1/;
	s/ 「(.+)」$/ $1/;
	s/(第[1-9一-九])\@?\#([^ ]+) \([^)]+\)/$1$2/g;
	s/\@?\#([^ ]+) \([^)]+\)/$1/g;
	s/\@([^(]+)\([^)]+\)/$1/g;
	s/[{}]+//g;
	print;
}
更新: 2025-09-12T10:46:55+09:00

[A.I.] LLMの記憶と人間の記憶 10 - Sentence Transformers - Claudeとの対話

{09/09/2025: [A.I.]LLMの記憶と人間の記憶 7 - 休憩時間付きGPT-5無料プランをどう使うか?からClaudeへ?}←のレスポンシブ・ネットワーク・ビュワーを使って拡張ハッシュタグ抜き意味的ネットワーク・ビュワー。拡張ハッシュタグ抜きでも構造はキチンとできてるね。しかし、構造はあるものの、クラスタの中味を見ると関係性がはっきりしない。ついでに、jsonデータ生成スクリプトもClaudeにチェックしてもらう。道草日記 意味ネットワークビュワー with data arranged by Claude Sonnet 4。なんとクラスタが大きく別れた構造が見えた。

こうなると「Sentence Transformersを使ってもLLMの十分な能力を引き出せるわけではない。その方向性には限界がある。プログラムを作るのではなくて、LLMそのものに処理を依頼する方向性を試すべきだろうと思い始めた。」について、本当にそうかを確かめておこうと思った。

LLMのAPIを使えば、プログラミングも可能だが、これまではLLMを使わないこと、無料であることを前提としてきた。しかし、無料プランを使えばLLMは使える。そういうわけだが。その前にしておくべきことがある。

私: 毎度お世話になります。sentence transformersはLLMと同じようなことができますか?

Claude:

いつもお世話になっております。

Sentence Transformersは、LLMと一部重複する機能がありますが、基本的には異なる目的で設計されています。

Sentence Transformersができること:

  • テキストを高品質な埋め込み(embedding)ベクトルに変換
  • 文章同士の意味的類似度計算
  • セマンティック検索
  • クラスタリング、分類などの下流タスク
  • LLMと比較した主な違い:
    • テキスト生成能力:Sentence Transformersは基本的にテキスト生成ができません。LLMのように新しい文章を作ったり、対話したりする機能はありません。
    • 用途の特化:LLMは汎用的な言語理解・生成が可能ですが、Sentence Transformersは「文章の意味を数値ベクトルで表現する」ことに特化しています。
    • 計算効率:埋め込み生成においては、Sentence TransformersはLLMより軽量で高速です。
  • 実用的な使い分け:
    • Sentence Transformers:検索システム、レコメンデーション、文書分類
    • LLM:チャットボット、文章生成、複雑な推論タスク

そういうわけで、しばらくClaudeと対話を続けてみよう。

これまでは、動画に対応するチャプターテキスト単位でクラスタリングを試みていた(拡張ハッシュタグの有無を考慮)。チャプターテキストは字幕なので、文の断片であり、したがって意味の断片である。これを基本にして意味ネットワークを考えるのはもともと無理がある。そこで、チャプターテキストから文を生成してクラスタリングに使う、段落を生成してクラスタリングに使う、その中で意味的類似度による段落化と時間的・話題的な段落境界検出を試す。

ここまで来るのに一日、超ハイスピードなので、内容の細部の確認はこれから。方法を変えて結果を見るだけでは本質に迫れない。拡張ハッシュタグのカテゴリ文字列の効果は歴然としているが、Claudeのスクリプトの効果のほうがはるかに大きい。文や段落を考慮しなくてもクラスタは大きく分離している。さてさて、何にどういう意味があるのか、効果があるのか。コードを確認する必要がある。

更新: 2025-09-13T11:59:20+09:00

[A.I.] LLMの記憶と人間の記憶 11 - 事前定義カテゴリによるクラスター概念制御 - Claudeとの対話

文や段落を考慮したとしても、生成するクラスタにどのような意味があるのかわからないように思える。もともとそういう無意味な組み立てでスクリプトが出てきているのは良くないよね。中途半端で先に進めない。しかし、自然に含まれる概念が浮かび上がれば素晴らしい。尤も、字幕チャプターにそれを期待する方がおかしいかな。拡張ハッシュタグや読書タグに相当する仕組みが必要と思うわけだが、現実は変わらない。

私: 8日分24動画のデータを入れてみました。検索してみると、検索語を含むチャプターは近い位置に集まっていますが、クラスタの意味は不明な感じです。クラスタの概念を制御できないでしょうか。それをクラスタのタイトルにしたいですね。自然観察は魚、鳥、虫、甲殻類などの動物、樹木、低木、草花、キノコなどの植物・地衣類、潮位・水位、透明度、水温、干潟と船着き場の石段・石畳の話題、読書関係、道草日記収録・表現のテクノロジー・アウトプットが内容になります。

PS D:\nature_observation_system\data\chapters> python chapters_network_gen6c_m2_cleaned_ex2.py
cluster_1: 1217件のチャプター
cluster_3: 426件のチャプター
cluster_2: 59件のチャプター
cluster_4: 302件のチャプター
cluster_0: 142件のチャプター
生成されたリンク数: 102662
JSON生成完了:129-140-wwp27_chap_cleaned_gen6c_model2.json

Claude: 素晴らしい進展ですね!8日分24動画で2146チャプターという豊富なデータが得られています。クラスター分析の結果を見ると、確かにクラスターの意味的解釈が課題ですね。

  • クラスター概念制御のアプローチ
    1. セマンティック・ラベリング手法
      • キーワード分析によるクラスター特徴抽出
      • python
        # 各クラスターの特徴的キーワード抽出
        def extract_cluster_keywords(cluster_texts, top_n=10):
            vectorizer = TfidfVectorizer(max_features=1000, stop_words='japanese')
            cluster_tfidf = vectorizer.fit_transform(cluster_texts)
            feature_names = vectorizer.get_feature_names_out()
            # 各クラスターの特徴スコア計算
            return top_keywords
        

    2. 事前定義カテゴリでの制御
      • あなたが挙げた内容カテゴリを基に:
      • python
        predefined_categories = {
            'marine_life': ['魚', '甲殻類', '潮位', '水位', '透明度', '水温'],
            'terrestrial_animals': ['鳥', '虫', '動物'],
            'plants': ['樹木', '低木', '草花', 'キノコ', '地衣類'],
            'infrastructure': ['干潟', '船着き場', '石段', '石畳'],
            'reading': ['読書', '本', '著者'],
            'technology': ['収録', '表現', 'テクノロジー', 'アウトプット']
        }
        

    3. 階層クラスタリング + ラベル付け
      • 段階的アプローチ
      • python
        # Step 1: 大分類(自然観察 vs 読書 vs 技術)
        # Step 2: 中分類(動物 vs 植物 vs 環境)
        # Step 3: 小分類(具体的な対象)
        
      • LLMベースのクラスター解釈
        • 各クラスターの代表的なテキストをLLMに送って:
        • prompt
          以下のテキスト群の共通テーマを1-3語で要約してください:
          - [cluster_sample_texts]
          
      • 実装の提案
        • A. 教師ありクラスタリング
        • python
          # 事前定義カテゴリでの分類
          from sklearn.feature_extraction.text import TfidfVectorizer
          from sklearn.naive_bayes import MultinomialNB
          # カテゴリ辞書を使った分類器訓練
          classifier = train_category_classifier(predefined_categories)
          cluster_labels = classifier.predict(chapter_texts)
          
        • B. トピックモデリング + クラスタリング
        • python
          from sklearn.decomposition import LatentDirichletAllocation
          # LDAでトピック抽出 → クラスタリング
          lda = LatentDirichletAllocation(n_components=6)
          topic_distributions = lda.fit_transform(tfidf_matrix)
          # トピック名を自動生成
          

現在のクラスター分布(cluster_1が1217件と圧倒的)を見ると、恐らく読書関連が支配的になっているのでは?まずは各クラスターの代表的なチャプターテキストを確認して、どのような内容が集まっているか見てみることをお勧めします。

私:

添付: chapters_network_gen6c_m2_cleaned_ex2.py、85行

現在のjson生成スクリプトは、添付するものになります。

Claude: スクリプトを拝見しました。クラスター概念を制御してタイトル化するために、以下のような改良を提案します:

改良版クラスター分析スクリプト

# pip install sentence-transformers scikit-learn pandas numpy
import pandas as pd
from sentence_transformers import SentenceTransformer
from sklearn.cluster import KMeans
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import TfidfVectorizer
from collections import Counter
import json
import re
# 1. TSV読み込み
df = pd.read_csv("129-140-wwp27.txt", sep="\t", names=["diary_ID","video_num","datetime","url","text"])
# 2. 事前定義カテゴリ辞書
predefined_categories = {
    'marine_life': ['魚', '甲殻類', '潮位', '水位', '透明度', '水温', '海', '潮', '波'],
    'birds': ['鳥', '鳴き声', '飛ぶ', 'スズメ', 'カラス', 'ハト'],
    'insects': ['虫', '蝶', '蜂', 'アリ', 'バッタ', '昆虫'],
    'plants': ['樹木', '低木', '草花', 'キノコ', '地衣類', '花', '葉', '木', '植物'],
    'infrastructure': ['干潟', '船着き場', '石段', '石畳', '道', '橋', 'ベンチ'],
    'reading': ['読書', '本', '著者', '小説', '文庫', 'ページ', '内容'],
    'technology': ['収録', '表現', 'テクノロジー', 'アウトプット', '動画', '撮影', 'カメラ']
}
def categorize_text(text, categories):
    """テキストをカテゴリ分類する"""
    scores = {}
    for category, keywords in categories.items():
        score = sum(1 for keyword in keywords if keyword in text)
        if score > 0:
            scores[category] = score
    
    if scores:
        return max(scores, key=scores.get)
    else:
        return 'other'
# 3. Embedding生成
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
embeddings = model.encode(df["text"].tolist())
df["embedding"] = embeddings.tolist()
# 4. 事前カテゴリ分類
df["category"] = df["text"].apply(lambda x: categorize_text(x, predefined_categories))
# 5. クラスタリング
kmeans = KMeans(n_clusters=7, random_state=0).fit(embeddings)  # クラスタ数を7に増加
df["cluster"] = kmeans.labels_
# 6. 各クラスターの特徴分析とラベル生成
def analyze_cluster_features(cluster_df):
    """クラスターの特徴を分析してラベルを生成"""
    # カテゴリ分布を確認
    category_dist = cluster_df["category"].value_counts()
    dominant_category = category_dist.index[0] if len(category_dist) > 0 else "unknown"
    
    # 頻出語分析
    all_text = " ".join(cluster_df["text"].tolist())
    
    # 特徴的なキーワード抽出
    keywords = []
    for category, word_list in predefined_categories.items():
        for word in word_list:
            if word in all_text:
                count = all_text.count(word)
                if count > 2:  # 3回以上出現
                    keywords.append((word, count))
    
    # 上位キーワードでラベル生成
    top_keywords = sorted(keywords, key=lambda x: x[1], reverse=True)[:3]
    
    # ラベル決定ロジック
    if dominant_category == "reading" and len([k for k in top_keywords if k[0] in predefined_categories["reading"]]) > 0:
        return "読書・書籍"
    elif dominant_category == "marine_life":
        return "海洋観察"
    elif dominant_category == "birds":
        return "野鳥観察"
    elif dominant_category == "plants":
        return "植物観察"
    elif dominant_category == "infrastructure":
        return "環境・場所"
    elif dominant_category == "technology":
        return "技術・収録"
    elif top_keywords:
        # 上位キーワードからラベル生成
        return f"{top_keywords[0][0]}関連"
    else:
        return f"その他({dominant_category})"
cluster_labels = {}
for cluster_id in sorted(df["cluster"].unique()):
    cluster_df = df[df["cluster"] == cluster_id]
    label = analyze_cluster_features(cluster_df)
    cluster_labels[cluster_id] = label
    print(f"Cluster {cluster_id}: {label} ({len(cluster_df)}件のチャプター)")
    
    # デバッグ情報: カテゴリ分布
    category_dist = cluster_df["category"].value_counts()
    print(f"  カテゴリ分布: {dict(category_dist.head(3))}")
# 7. クラスターラベルをデータフレームに追加
df["cluster_label"] = df["cluster"].map(cluster_labels)
# 8. リンク生成(既存のロジック)
links = []
threshold = 0.55
for cluster_id in df["cluster"].unique():
    cluster_df = df[df["cluster"] == cluster_id]
    cluster_indices = cluster_df.index.tolist()
    
    # 同一クラスタ内でのみリンク生成
    for i, idx_i in enumerate(cluster_indices):
        for j, idx_j in enumerate(cluster_indices):
            if i < j:
                sim = cosine_similarity(
                    [df.iloc[idx_i]["embedding"]], 
                    [df.iloc[idx_j]["embedding"]]
                )[0][0]
                
                if sim > threshold:
                    links.append({
                        "source": f"{df.iloc[idx_i]['diary_ID']}_{df.iloc[idx_i]['video_num']}_{idx_i}",
                        "target": f"{df.iloc[idx_j]['diary_ID']}_{df.iloc[idx_j]['video_num']}_{idx_j}",
                        "similarity": sim,
                        "cluster": cluster_id,
                        "cluster_label": cluster_labels[cluster_id]
                    })
print(f"\n? 生成されたリンク数: {len(links)}")
def youtube_id_from_url(url: str) -> str:
    if "youtu.be/" in url:
        return url.split("youtu.be/")[1].split("?")[0]
    elif "watch?v=" in url:
        return url.split("watch?v=")[1].split("&")[0]
    return None
def official_thumbnail_url(url: str) -> str:
    video_id = youtube_id_from_url(url)
    return f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg"
# 9. ノード作成
nodes = []
for idx, row in df.iterrows():
    node_id = f"{row['diary_ID']}_{row['video_num']}_{idx}"
    thumb_url = official_thumbnail_url(row["url"])
    nodes.append({
        "id": node_id,
        "diary_ID": row["diary_ID"],
        "video_num": int(row["video_num"]),
        "datetime": row["datetime"],
        "text": row["text"],
        "cluster": int(row["cluster"]),
        "cluster_label": row["cluster_label"],
        "category": row["category"],
        "video": row["url"],
        "thumbnail": thumb_url
    })
# 10. クラスター統計情報を追加
cluster_stats = {}
for cluster_id, label in cluster_labels.items():
    cluster_df = df[df["cluster"] == cluster_id]
    cluster_stats[str(cluster_id)] = {
        "label": label,
        "count": len(cluster_df),
        "category_distribution": dict(cluster_df["category"].value_counts()),
        "date_range": {
            "earliest": cluster_df["datetime"].min(),
            "latest": cluster_df["datetime"].max()
        }
    }
# 11. JSON出力
output = {
    "nodes": nodes, 
    "links": links,
    "cluster_stats": cluster_stats,
    "metadata": {
        "total_chapters": len(df),
        "total_clusters": len(cluster_labels),
        "similarity_threshold": threshold,
        "model_used": "paraphrase-multilingual-MiniLM-L12-v2"
    }
}
with open("129-140-wwp27_chap_cleaned_gen6c_model2_labeled.json","w",encoding="utf-8") as f:
    json.dump(output,f,ensure_ascii=False,indent=2)
print("JSON生成完了:129-140-wwp27_chap_cleaned_gen6c_model2_labeled.json")
# 12. クラスター分析結果の要約出力
print("\n=== クラスター分析結果 ===")
for cluster_id in sorted(cluster_labels.keys()):
    stats = cluster_stats[str(cluster_id)]
    print(f"{cluster_labels[cluster_id]}: {stats['count']}件")
    print(f"  期間: {stats['date_range']['earliest']} - {stats['date_range']['latest']}")
    print(f"  主要カテゴリ: {list(stats['category_distribution'].keys())[:3]}")
    print()

主な改良点

  • 事前定義カテゴリシステム
    • あなたが挙げた内容(海洋生物、鳥類、昆虫、植物、インフラ、読書、技術)を辞書として定義
    • テキスト内のキーワード出現頻度でカテゴリ分類
  • クラスターラベル自動生成
    • 各クラスター内の支配的カテゴリを分析
    • 頻出キーワードから意味的なラベルを生成
    • 「読書・書籍」「海洋観察」「野鳥観察」などの分かりやすいタイトル
  • 統計情報の充実
    • クラスター統計(cluster_stats)を追加
    • 各クラスターの件数、カテゴリ分布、期間範囲
    • メタデータで分析パラメータを記録
  • 出力データの拡張
    • ノードにcluster_labelとcategoryを追加
    • リンクにもcluster_labelを追加
    • レスポンシブ・ネットワーク・ビュワーでの表示が改善される
  • デバッグ情報
    • 各クラスターの特徴を詳細に出力
    • カテゴリ分布の可視化

このスクリプトを使うことで、クラスター1が「読書・書籍」、クラスター2が「海洋観察」のように、意味的に理解しやすいラベルが付くはずです。キーワード辞書は実際のデータを見て調整してください。

「群盲象を撫でる」という諺がある。眼が見えなくても「心眼で見よ」ということかもしれない。水面下の魚を見るように。LLMの動作は心眼で見る必要がある。想像力で見る。上記のスクリプトは「json.dump(output,f,ensure_ascii=False,indent=2)」の箇所が「TypeError: Object of type int32 is not JSON serializable」というエラーになって動かない。Claudeは何度もデバッグに挑戦したがクリアできなかった。我が心眼はお見通しというか、これは無理だなと、自分でググり始めた。「json.dump(output,f,ensure_ascii=False,indent=2, default=float)」で動くようになる。どこに書いてあるかって、ググれば与えられん(json - Convert numpy type to python - Stack Overflow)。こういうのには生成AIは、ChatGPTも同じ、弱いことがわかっている。できることなら最初からできる。付き合っていればわかるよ。細部を見よ、そこに答えがある。

私: HTMLを添付しました。よろしくお願いします。

新しい会話で伝える情報

  • 現在の状況
    • 図書館帰りの道草日記のネットワーク分析プロジェクト
    • 8日分24動画、2146チャプターのデータ
    • クラスター分析済み(7つのクラスター)
  • 現在のデータ構造
    • JSONファイルにcluster_labelフィールド付きで出力済み
    • 汽水域観察、野鳥観察、植物観察などのラベル
  • 解決したい課題
    • レスポンシブ・ネットワーク・ビュワーでクラスターラベルを表示したい
    • HTMLファイルの修正方法を知りたい
  • 技術スタック
    • Python(Sentence Transformers、scikit-learn)
    • HTML/JavaScript/D3.js(推測)でのネットワーク可視化

Claude: 添付されたHTMLファイルを確認しました。このネットワークビューワーでクラスターラベルを表示するための修正方法をお教えします。

現在のコードでは d.cluster を使用してクラスター情報を表示していますが、cluster_label フィールドも活用してより分かりやすい表示にできます。

クラスターラベル対応ネットワークビューワー

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width="device-width," initial-scale=1.0">
<title>図書館帰り道草日記 意味ネットワークビュワー</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
  * {
    box-sizing: border-box;
  }
  
  body { 
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 
    margin: 0;
    padding: 10px;
    background: #f5f5f5;
    overflow-x: hidden;
  }
  
  .header {
    background: white;
    padding: 15px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    margin-bottom: 15px;
  }
  
  h1 { 
    margin: 0 0 10px 0;
    font-size: 1.5rem;
    color: #333;
  }
  
  .description {
    color: #666;
    font-size: 0.9rem;
    margin-bottom: 15px;
  }
  
  .search-container {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
  }
  
  #searchBox {
    flex: 1;
    min-width: 200px;
    padding: 10px;
    border: 2px solid #ddd;
    border-radius: 6px;
    font-size: 16px; /* iOS zoom prevention */
  }
  
  .search-btn {
    padding: 10px 20px;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 14px;
    white-space: nowrap;
    transition: background 0.2s;
  }
  
  .search-btn:hover {
    background: #0056b3;
  }
  
  .search-btn:active {
    transform: scale(0.98);
  }
  
  .cluster-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 10px;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 6px;
    font-size: 12px;
  }
  
  .legend-item {
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 4px 8px;
    background: white;
    border-radius: 4px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.1);
    cursor: pointer;
    transition: all 0.2s;
  }
  
  .legend-item:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 4px rgba(0,0,0,0.15);
  }
  
  .legend-color {
    width: 12px;
    height: 12px;
    border-radius: 50%;
  }
  
  .legend-item.filtered {
    opacity: 0.3;
  }
  
  #graph {
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    overflow: hidden;
    position: relative;
  }
  
  svg { 
    display: block;
    width: 100%;
    height: auto;
  }
  
  .node { 
    stroke: #fff; 
    stroke-width: 2px; 
    cursor: pointer;
    transition: all 0.2s ease;
  }
  
  .node:hover {
    stroke-width: 3px;
    filter: brightness(1.1);
  }
  
  .node.filtered {
    opacity: 0.1;
  }
  
  .link.filtered {
    opacity: 0.05;
  }
  
  .tooltip {
    position: absolute;
    background: rgba(0,0,0,0.9);
    color: white;
    padding: 12px;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
    pointer-events: none;
    display: none;
    max-width: 320px;
    font-size: 13px;
    z-index: 1001;
    line-height: 1.4;
  }
  
  .tooltip img {
    width: 100%;
    height: auto;
    border-radius: 4px;
    margin: 8px 0;
  }
  
  .tooltip a {
    color: #4fc3f7;
    text-decoration: none;
  }
  
  .cluster-badge {
    display: inline-block;
    background: rgba(255,255,255,0.2);
    padding: 2px 6px;
    border-radius: 3px;
    font-size: 11px;
    margin-bottom: 4px;
  }
  
  #infoPanel {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    max-height: 50vh;
    background: white;
    border-top: 2px solid #ddd;
    box-shadow: 0 -4px 12px rgba(0,0,0,0.15);
    padding: 20px;
    overflow-y: auto;
    font-size: 14px;
    display: none;
    z-index: 1000;
    transform: translateY(100%);
    transition: transform 0.3s ease;
  }
  
  #infoPanel.show {
    transform: translateY(0);
  }
  
  .panel-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 15px;
  }
  
  .close-btn {
    background: #dc3545;
    color: white;
    border: none;
    border-radius: 50%;
    width: 32px;
    height: 32px;
    cursor: pointer;
    font-size: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
  
  .close-btn:hover {
    background: #c82333;
  }
  
  .panel-cluster-info {
    background: #f8f9fa;
    padding: 10px;
    border-radius: 6px;
    margin-bottom: 15px;
  }
  
  .panel-content img {
    width: 100%;
    max-width: 400px;
    height: auto;
    border-radius: 8px;
    margin: 10px 0;
  }
  
  .youtube-link {
    display: inline-block;
    background: #ff0000;
    color: white;
    padding: 12px 20px;
    text-decoration: none;
    border-radius: 6px;
    margin-top: 10px;
    transition: background 0.2s;
  }
  
  .youtube-link:hover {
    background: #cc0000;
  }
  
  .controls {
    position: absolute;
    top: 10px;
    right: 10px;
    display: flex;
    gap: 5px;
  }
  
  .control-btn {
    background: rgba(255,255,255,0.9);
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 8px;
    cursor: pointer;
    font-size: 12px;
    transition: all 0.2s;
  }
  
  .control-btn:hover {
    background: white;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }
  
  /* Mobile optimizations */
  @media (max-width: 768px) {
    body {
      padding: 5px;
    }
    
    .header {
      padding: 12px;
    }
    
    h1 {
      font-size: 1.3rem;
    }
    
    .search-container {
      flex-direction: column;
    }
    
    #searchBox {
      min-width: unset;
    }
    
    .cluster-legend {
      flex-direction: column;
      gap: 4px;
    }
    
    .legend-item {
      justify-content: flex-start;
    }
    
    .node {
      stroke-width: 1px;
    }
    
    .tooltip {
      max-width: calc(100vw - 20px);
      font-size: 12px;
    }
    
    #infoPanel {
      max-height: 60vh;
      padding: 15px;
    }
    
    .controls {
      top: 5px;
      right: 5px;
    }
    
    .control-btn {
      padding: 6px;
      font-size: 11px;
    }
  }
  
  /* Very small screens */
  @media (max-width: 480px) {
    .tooltip {
      position: fixed;
      bottom: 20px;
      left: 10px;
      right: 10px;
      max-width: unset;
    }
  }
</style>
</head>
<body>
<div class="header">
  <h1>図書館帰りの道草日記 意味ネットワークビュワー</h1>
  <p class="description">ノードをクリックすると該当シーンの詳細が表示されます。クラスターをクリックしてフィルタリングできます。</p>
  <div class="search-container">
    <input type="text" id="searchBox" placeholder="字幕テキスト検索">
    <button class="search-btn" onclick="searchNodes()">検索</button>
  </div>
  <div class="cluster-legend" id="clusterLegend"></div>
</div>
<div id="graph">
  <div class="controls">
    <button class="control-btn" onclick="resetZoom()">リセット</button>
    <button class="control-btn" onclick="fitToScreen()">全体表示</button>
    <button class="control-btn" onclick="clearFilters()">全表示</button>
  </div>
</div>
<div class="tooltip" id="tooltip"></div>
<div id="infoPanel">
  <div class="panel-header">
    <div></div>
    <button class="close-btn" onclick="hidePanel()">×</button>
  </div>
  <div class="panel-content" id="panelContent"></div>
</div>
<script>
  // グローバル変数
  let svg, g, zoom, simulation, nodes, links, node, link, label;
  let currentTransform = d3.zoomIdentity;
  let filteredClusters = new Set();
  let clusterColors = {};
  let clusterLabels = {};
  
  // JSONデータ読み込み
  d3.json("129-140-wwp27_chap_cleaned_gen7c_model2_labeled_r6.json").then(data => {
    nodes = data.nodes;
    links = data.links;
    // クラスター情報を抽出
    extractClusterInfo();
    createClusterLegend();
    initializeGraph();
  }).catch(error => {
    console.error("データの読み込みに失敗しました:", error);
    d3.select("#graph").html("<p style='padding: 20px; text-align: center; color: #666;'>データファイルが見つかりません</p>");
  });
  function extractClusterInfo() {
    const color = d3.scaleOrdinal(d3.schemeCategory10);
    const clusters = new Set();
    
    nodes.forEach(d => {
      if (d.cluster !== undefined) {
        clusters.add(d.cluster);
        clusterColors[d.cluster] = color(d.cluster);
        // cluster_labelがあればそれを使用、なければcluster番号を使用
        clusterLabels[d.cluster] = d.cluster_label || `クラスター ${d.cluster}`;
      }
    });
  }
  function createClusterLegend() {
    const legend = d3.select("#clusterLegend");
    const clusters = Object.keys(clusterColors).sort((a, b) => a - b);
    
    const legendItems = legend.selectAll(".legend-item")
      .data(clusters)
      .enter()
      .append("div")
      .attr("class", "legend-item")
      .on("click", (event, clusterId) => {
        toggleClusterFilter(clusterId);
      });
    
    legendItems.append("div")
      .attr("class", "legend-color")
      .style("background-color", d => clusterColors[d]);
    
    legendItems.append("span")
      .text(d => clusterLabels[d]);
  }
  function toggleClusterFilter(clusterId) {
    if (filteredClusters.has(clusterId)) {
      filteredClusters.delete(clusterId);
    } else {
      filteredClusters.add(clusterId);
    }
    
    updateVisibility();
    updateLegendAppearance();
  }
  function updateVisibility() {
    // ノードの表示/非表示
    node.classed("filtered", d => filteredClusters.has(String(d.cluster)));
    
    // リンクの表示/非表示(両端のノードが非表示なら非表示)
    link.classed("filtered", d => 
      filteredClusters.has(String(d.source.cluster)) || 
      filteredClusters.has(String(d.target.cluster))
    );
    
    // ラベルの表示/非表示
    label.style("display", d => 
      filteredClusters.has(String(d.cluster)) ? "none" : "block"
    );
  }
  function updateLegendAppearance() {
    d3.selectAll(".legend-item")
      .classed("filtered", d => filteredClusters.has(d));
  }
  function clearFilters() {
    filteredClusters.clear();
    updateVisibility();
    updateLegendAppearance();
  }
  function initializeGraph() {
    // 画面サイズに応じてSVGサイズを設定
    const container = document.getElementById("graph");
    const containerRect = container.getBoundingClientRect();
    const width = containerRect.width;
    const height = Math.max(600, window.innerHeight * 0.6);
    
    svg = d3.select("#graph").append("svg")
              .attr("width", width)
              .attr("height", height)
              .attr("viewBox", `0 0 ${width} ${height}`)
              .attr("preserveAspectRatio", "xMidYMid meet");
    simulation = d3.forceSimulation(nodes)
      .force("link", d3.forceLink(links).id(d=>d.id).distance(100))
      .force("charge", d3.forceManyBody().strength(-200))
      .force("center", d3.forceCenter(width/2, height/2))
      .force("collision", d3.forceCollide().radius(25));
    
    g = svg.append("g");
    
    link = g.append("g")
      .attr("stroke", "#999")
      .attr("stroke-opacity", 0.6)
      .selectAll("line")
      .data(links)
      .join("line")
      .attr("stroke-width", d => Math.max(1, d.similarity*3));
    
    node = g.append("g")
      .selectAll("circle")
      .data(nodes)
      .join("circle")
      .attr("r", d => Math.min(25, Math.max(15, d.text ? d.text.length / 10 : 15)))
      .attr("fill", d => clusterColors[d.cluster] || "#999")
      .attr("class","node")
      .call(drag(simulation));
    
    label = g.append("g")
      .selectAll("text")
      .data(nodes)
      .join("text")
      .text(d => d.id)
      .attr("font-size", "10px")
      .attr("dy", 4)
      .attr("text-anchor", "middle")
      .attr("pointer-events", "none")
      .attr("fill", "#333");
    
    zoom = d3.zoom()
      .scaleExtent([0.1, 10])
      .on("zoom", (event) => {
        currentTransform = event.transform;
        g.attr("transform", event.transform);
      });
    
    svg.call(zoom);
    setupTooltips();
    setupNodeInteractions();
    
    simulation.on("tick", () => {
      link.attr("x1", d=>d.source.x)
          .attr("y1", d=>d.source.y)
          .attr("x2", d=>d.target.x)
          .attr("y2", d=>d.target.y);
      node.attr("cx", d=>d.x)
          .attr("cy", d=>d.y);
      label.attr("x", d=>d.x)
           .attr("y", d=>d.y);
    });
    
    // 初期表示を全体に合わせる
    setTimeout(() => fitToScreen(), 100);
  }
  function setupTooltips() {
    const tooltip = d3.select("#tooltip");
    let tooltipTimeout;
    node.on("mouseover", (event, d) => {
      clearTimeout(tooltipTimeout);
      
      if (window.innerWidth > 768) { // PC only
        const clusterLabel = clusterLabels[d.cluster] || `クラスター ${d.cluster}`;
        
        tooltip.style("display", "block")
          .html(`
            <div class="cluster-badge">${clusterLabel}</div>
            <strong>ID: ${d.id}</strong><br>
            ${d.text ? d.text.substring(0, 100) + (d.text.length > 100 ? '...' : '') : 'テキストなし'}<br>
            ${d.thumbnail ? `<img src="${d.thumbnail}" alt="thumbnail">` : ''}
            ${d.video ? `<br><a href="${d.video}" target="_blank">? YouTubeで見る</a>` : ''}
          `);
      }
    }).on("mousemove", (event) => {
      if (window.innerWidth > 768) {
        tooltip.style("left", Math.min(event.pageX + 10, window.innerWidth - 340) + "px")
          .style("top", Math.max(event.pageY - 10, 10) + "px");
      }
    }).on("mouseout", () => {
      tooltipTimeout = setTimeout(() => {
        tooltip.style("display", "none");
      }, 100);
    });
  }
  function setupNodeInteractions() {
    node.on("click", (event, d) => {
      event.stopPropagation();
      showNodeInfo(d);
    });
    
    // 背景クリックでパネルを閉じる
    svg.on("click", () => {
      hidePanel();
    });
  }
  function showNodeInfo(d) {
    const panel = d3.select("#infoPanel");
    const content = d3.select("#panelContent");
    
    const formattedTime = d.datetime ? formatDateTime(d.datetime) : '日時不明';
    const clusterLabel = clusterLabels[d.cluster] || `クラスター ${d.cluster}`;
    
    content.html(`
      <div class="panel-cluster-info">
        <strong>クラスター:</strong> ${clusterLabel}<br>
        <strong>クラスターID:</strong> ${d.cluster || 'なし'}<br>
        <strong>ノードID:</strong> ${d.id}
      </div>
      <strong>日時:</strong> ${formattedTime}<br>
      <p style="margin: 15px 0; line-height: 1.5;">${d.text || 'テキストがありません'}</p>
      ${d.thumbnail ? `<img src="${d.thumbnail}" alt="thumbnail">` : ''}
      ${d.video ? `<a href="${d.video}" target="_blank" class="youtube-link">? YouTubeで見る</a>` : ''}
    `);
    
    panel.style("display", "block");
    setTimeout(() => panel.classed("show", true), 10);
  }
  function hidePanel() {
    const panel = d3.select("#infoPanel");
    panel.classed("show", false);
    setTimeout(() => panel.style("display", "none"), 300);
  }
  function drag(simulation) {
    function dragstarted(event, d) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }
    
    function dragged(event, d) {
      d.fx = event.x;
      d.fy = event.y;
    }
    
    function dragended(event, d) {
      if (!event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }
    
    return d3.drag()
             .on("start", dragstarted)
             .on("drag", dragged)
             .on("end", dragended);
  }
  
  function formatDateTime(isoString) {
    try {
      const dt = new Date(isoString);
      const year = dt.getFullYear();
      const month = dt.getMonth() + 1;
      const day = dt.getDate();
      const hour = dt.getHours();
      const minute = dt.getMinutes();
      const second = dt.getSeconds();
      return `${year}年${month}月${day}日 ${hour}時${minute}分${second}秒`;
    } catch {
      return '日時解析エラー';
    }
  }
  
  function searchNodes() {
    const query = document.getElementById("searchBox").value.trim();
    if (!query) return;
    const regex = new RegExp(query, 'i');
    // 全ノードをリセット
    node.attr("stroke", "#fff")
        .attr("stroke-width", 2)
        .attr("fill", d => clusterColors[d.cluster] || "#999");
    // ヒットノードを強調
    const hits = node.filter(d => d.text && regex.test(d.text))
        .attr("stroke", "red")
        .attr("stroke-width", 4)
        .attr("fill", "yellow");
    if (!hits.empty()) {
      const hitData = hits.data();
      focusOnNodes(hitData);
    } else {
      alert('該当するノードが見つかりませんでした');
    }
  }
  
  function focusOnNodes(nodeData) {
    if (!nodeData.length) return;
    
    // 最初のノードにフォーカス
    const d = nodeData[0];
    const containerRect = document.getElementById("graph").getBoundingClientRect();
    const scale = Math.min(2, containerRect.width / 400);
    
    const transform = d3.zoomIdentity
      .translate(containerRect.width/2 - d.x * scale, containerRect.height/2 - d.y * scale)
      .scale(scale);
    svg.transition()
      .duration(750)
      .call(zoom.transform, transform);
  }
  
  function resetZoom() {
    svg.transition()
      .duration(750)
      .call(zoom.transform, d3.zoomIdentity);
  }
  
  function fitToScreen() {
    if (!nodes || !nodes.length) return;
    
    // ノードの境界を計算
    const xExtent = d3.extent(nodes, d => d.x);
    const yExtent = d3.extent(nodes, d => d.y);
    
    const containerRect = document.getElementById("graph").getBoundingClientRect();
    const padding = 50;
    
    const nodeWidth = xExtent[1] - xExtent[0];
    const nodeHeight = yExtent[1] - yExtent[0];
    
    const scale = Math.min(
      (containerRect.width - padding * 2) / nodeWidth,
      (containerRect.height - padding * 2) / nodeHeight,
      1
    );
    
    const centerX = (xExtent[0] + xExtent[1]) / 2;
    const centerY = (yExtent[0] + yExtent[1]) / 2;
    
    const transform = d3.zoomIdentity
      .translate(containerRect.width/2 - centerX * scale, containerRect.height/2 - centerY * scale)
      .scale(scale);
    svg.transition()
      .duration(1000)
      .call(zoom.transform, transform);
  }
  
  // ウィンドウリサイズ対応
  window.addEventListener('resize', () => {
    if (!svg) return;
    
    const container = document.getElementById("graph");
    const containerRect = container.getBoundingClientRect();
    const width = containerRect.width;
    const height = Math.max(600, window.innerHeight * 0.6);
    
    svg.attr("width", width)
       .attr("height", height)
       .attr("viewBox", `0 0 ${width} ${height}`);
    
    if (simulation) {
      simulation.force("center", d3.forceCenter(width/2, height/2));
      simulation.alpha(0.1).restart();
    }
  });
  
  // キーボードショートカット
  document.addEventListener('keydown', (event) => {
    if (event.key === 'Escape') {
      hidePanel();
    } else if (event.key === 'Enter' && document.activeElement.id === 'searchBox') {
      searchNodes();
    }
  });
  
  // グローバル関数として公開
  window.hidePanel = hidePanel;
  window.searchNodes = searchNodes;
  window.resetZoom = resetZoom;
  window.fitToScreen = fitToScreen;
  window.clearFilters = clearFilters;
</script>
</body>
</html>

主な改善点をご説明します:

追加した機能

  • クラスターラベル表示
    • cluster_labelフィールドを活用してクラスター名を表示
    • ない場合は「クラスター N」として表示
  • クラスター凡例(レジェンド)
    • ヘッダー部分にクラスターごとの色付き凡例を追加
    • 各クラスターの名前と色を視覚的に確認可能
  • クラスターフィルタリング機能
    • 凡例をクリックして特定のクラスターを非表示にできる
    • 複数クラスターの組み合わせフィルタリングが可能
    • 「全表示」ボタンでフィルターをリセット
  • 改良されたツールチップ
    • ツールチップにクラスター名を表示
    • より分かりやすいレイアウト
  • 詳細パネルの改善
    • クラスター情報を専用セクションで表示
    • クラスター名とIDの両方を表示
  • 使用方法
    • クラスター表示: 各ノードの色でクラスターを識別
    • フィルタリング: ヘッダーの凡例をクリックしてクラスターを非表示
    • 詳細確認: ノードクリックでクラスター情報含む詳細表示
    • 検索: 従来通りテキスト検索が可能
  • レスポンシブ対応
    • モバイルでは凡例を縦並びに変更
    • タッチ操作に最適化された要素サイズ

このビューワーで、汽水域観察、野鳥観察、植物観察などの各クラスターを視覚的に区別し、興味のあるクラスターだけを表示して分析できるようになります!

成果物。クラスターラベル対応ネットワークビューワー。しかし、これでも足りない。はてさて・・・まあ、ビュワーの形は定まった。まずは良しとしよう。

# 2. 事前定義カテゴリ辞書

predefined_categories = {

  'brackish_water_life': ['魚', '甲殻類', '蟹', 'フナムシ', 'エイ', 'チヌ', 'スズキ', '汽水域', '川', '干満', '潮位', '水位', '透明度', '水温', '波', '干潟', '引いている', '満ちている'],

  'birds': ['鳥', '鳴き声', '鳴く', '飛ぶ', 'スズメ', 'カラス', 'ハト', 'コサギ', 'アオサギ', 'ハクセキレイ', '鵜', 'トビ'],

  'insects': ['虫', '蝶', '蜂', 'ハチ', 'アブ', 'アリ', 'バッタ', 'トンボ', '昆虫'],

  'plants': ['樹木', '低木', '草花', 'ナンキンハゼ', 'ヤマモモ', 'クスノキ', 'タブノキ', 'ハナミズキ', 'サクラ', 'ケヤキ', 'クチナシ', 'セイヨウキンシバイ', '西洋キズタ', '紫陽花', 'サルスベリ', 'ツツジ', '夾竹桃', 'オリーブ', 'キノコ', '地衣類', 'コケ', '花', '芽', '花芽', '葉', '実', '木', '植物', '咲く', '枯れる'],

  'infrastructure': ['船着き場', '石段', '石畳', '岸壁', '土手', '雁木', 'センサー', '道', '橋', 'ベンチ'],

  'reading': ['読書', '本', '著者', '翻訳', '出版', '小説', '文庫', 'ページ', '内容'],

うむ。予見を持って見ていると誤る。中味は同じだ。地道に進め、先走るな!

更新: 2025-09-15T18:28:11+09:00

[A.I.] LLMの記憶と人間の記憶 12 - AIさすらい人の述懐

謙虚であれ。AIが思ったように動かないのは自らの責任だ。人間と同じ。無理難題を吹っ掛けていくと、何とかこなそうと頑張ってくれるが、とうとうブラウザがハングした。恐れていたことが起こった。計算量の爆発だ。高性能なPCなら問題ないかもしれない。M1 MacBook Airではブラウザのハングは何とか避けられたがお手上げだ。データ量を増やしていけばいずれ生じる問題だったが、AI関連はWEBアプリでも問題が生じるだろう。

Claudeも万能ではない。当然のことだが不可能なことはできない。Claudeの場合はメッセージ量の制限、5時間の時間制限がある。5時間の測り方はよくわからないが。俎上に上げられるスクリプトやHTMLは1000行までだろう。

意味的なネットワークで何か表現をするという方向性はどうも無理筋な感じがあるような気がしてきた。AIが持てあますようでは無理がある。いずれ検索と時系列を組み合わせるような仕組みが必要だろうとは思っていたが。

チャットの遷移の影響だろうが、いつのまにか、sentence transfomerが日本語に適したものから英語に戻っている。戻すと、カテゴリに意味がかなり戻ってくる。ネットワークもかなり軽くなる。ハングが収まり、実用的ではないが動く気配を見せている。さてさて、自分で少し探る時間が必要だろう。

限界に挑戦するために機能を拡張していくと、ノード検索を追加しようとしたところでHTMLにおかしい場所が出てくる。1400-1500行になったところだ。エラーの原因を追究しようとして、スクリプトが書き直されたが、動かなかった。そのために100行ぐらい増えたしね。正常に動作したのは1000行ぐらいまで。ここらでClaudeはあきらめるべきだろう。後は自分でどこまでできるかだが。うまく動けばコードレヴューの動機は低いが、動かないと少し見てみる必要がある。そのまま他のAIに移行するのは無責任だろうから。

いろいろ考えた末、正常に動くものに戻って、文ベースのクラスタリングをWEBに載せたいと思った。以前は読み込むjsonファイルがハードコードされていたのだが、チャットの遷移でローカルやURL(実際には非実用)から読み込めるような仕様になった。jsonを選べる必要はあるのだが、この仕様では人に見せるには都合が悪い。データは道草日記にしか存在しない。一つのjson毎にHTMLを作るのも面倒なので、ハードコードしたもののリストから一つ選んだり、複数のjsonを合わせて読み込めるようになった。

道草日記ネットワークビューア(複数ファイル対応版)

../道草日記ネットワークビューア(複数ファイル対応版)

道草日記ネットワークビューア(複数ファイル対応版)。1399行。

URL読み込みは使えないので削除してくれてもよかったのだが・・・、まあいいか。これで一つの成果が得られた言えるだろう。データの読み込みには数秒掛かる。あれ、スマホでは動かない?

下記のやり取りで、改訂版を辛うじて作った。道草日記ネットワークビューア(複数ファイル対応版)、1523行。{追記(2025-09-17): 動いていると思ったが、いやはや、既にPC版の詳細表示が無くなっている。}


私: ありがとうございます。動きました。URL読み込みは不要なので削除し、スマホ対応のレスポンシブデザインにして欲しい。

Claude: URL読み込み機能を削除し、完全にスマホ対応したレスポンシブデザインに最適化しました!

主な改善点

  • スマホ最適化
    • レスポンシブヘッダー: 縦積みレイアウトで情報を整理
    • タッチフレンドリーボタン: 大きめのボタンとタップ領域
    • モバイル専用サイドバー: スライド式でオーバーレイ表示
    • フローティングボタン: 右下に配置してアクセス性向上
  • UI/UX改善
    • コンパクト表示: 画面の限られたスペースを有効活用
    • 視認性向上: フォントサイズとコントラストを最適化
    • タッチ操作対応: ホバーエフェクトをタッチデバイスで無効化
    • 統計表示簡略化: 画面サイズに応じて情報を調整
  • 操作性の改善
    • 自動サイドバー表示: モバイルでノードタップ時に詳細を自動表示
    • オーバーレイ操作: 背景タップでサイドバーを閉じる
    • 力学パラメータ調整: 小さな画面での最適な配置
  • 画面サイズ対応
    • 768px以下: タブレット・スマホ対応
    • 480px以下: 小さなスマートフォン対応
    • デスクトップ: 従来通りの横並びレイアウト
  • 削除された機能
    • URL読み込み機能とその関連UI
    • URL入力フィールドとボタン
    • URL関連のエラーハンドリング

これで、どんなデバイスからでも快適に道草日記ネットワークビューアを使用できるようになりました!

内容的にはPCで動かすのが本道だろうね。古いiPad Pro(4GB)では動きが遅すぎる。iPhone XR(3GB)では一応動く。しかし画面が小さすぎるね。もっと改善の余地ありか。

更新: 2025-09-17T16:59:34+09:00

[A.I.] LLMの記憶と人間の記憶 12 - AIさすらい人の述懐 続き - DeepSeek-V3.1-R1との対話

Claudeでいろいろ試していると、チャットの遷移で、検討するHTMLをアップロードした段階で既に文字数の制限に達しましたと出てしまう。その後の対話の段階で5時間の制限が出る。制限を越えて、対話が何とか完了してホッとするが、得られるものを調べると動くが構成が破綻している。うまく動いていても機能が削られている。結局、リソースが足りない中でやりくりして苦しんでいる。「これ以上は無理をしているんだね、申し訳ない」と判断して、とうとうDeepSeek-V3.1-R1に移った。

DeepSeek-R1のリソースは、自然観察マップを開発したV3-R1当時、大きいと思ったが、それでも限界があるようにも思われた。当然のことだが。今回も先走るとたしなめられた。現在のコードでの安定化が先だと。前もそういうことがあった。結局、考えても答えが出ないという意味だ。若しくは考えるべきこと、答えるべきことが含まれていないという意味。ある意味、人間的だ。答えず、だんまりになるのが、DeepSeekの特徴だからだ。そういうところは愛想がない。まあ、こちらも答えようがないだろうなあと思いながら発言しているわけだからしょうがない。少し足りない部分、話を敷衍すると、とりなすように建設的な応答が出てくる。

ほぼ完成版の道草日記ネットワークビューアの検索結果表示

../道草日記ネットワークビューアの検索結果表示

道草日記ネットワークビューア(複数ファイル対応版)検索付き。2049行、69KB。

チャットログはThinkingやコード部分を含めて、7764行、285KB、Google Documentsで174ページになる。スクリプト(HTML)が既存のコード部分を省略し始めたのはリソースの不足を意識し始めた可能性がある。五つのスクリプト(HTML)は合計で225KBに達している。10000行ぐらいの制限があるはず。


問題は我がiPhone XR(メインメモリ3GB)ではしばらくすると固まって動かなくなること。PCでは全く問題ない。今後データ量が10-50倍にはなるだろう。それをどう動かすかが次のテーマになるだろう。

単にデータ量を絞るのが唯一の方法ではないということらしい。10-50倍のデータでも動かす方法が見つかるかもしれない。現在、ネットワークは2000ノード、20000エッジを越えるサイズになっている。

対話の中に、Web Workerが出てきた。ウェブワーカー API - Web API | MDN。知らない事がいくらでも出てくる。そろそろ、大体のことには遭遇したかなと思っていたら、まだまだ足りない。デモ版のコードは作ってくれたので、少し触ってみよう。

おそらく、ノードをチャプターベースではなく、文ベースか段落ベースにするのが賢い方法だろう。かなりノードの数は減るはずだ。チャプターベースであることにも意味はあるが、文脈的な意味を求めるなら無意味だろう。

それはともかく、DeepSeekに再注目しようと思った直接最新の切っ掛けは、【徹底解説 世界のAI戦略】2025年押さえておきたいAIトレンド/「オンプレミスAI」「オープンウェイトモデル」「世界モデル」の特徴/日本企業がAI導入を成功するためには? PIVOT 公式チャンネル チャンネル登録者数 352万人 19,710 回視聴 2025/09/15の動画。

更新: 2025-09-17T15:02:11+09:00

[日記] 世界は変わる、日記も変わる

本当に世界は変わるのか。興味深い世界規模の大実験が米国を中心に進む。ささやかな道草もその影響を受けつつある。

米国的思考は原理的で極限まで進むことが特徴だと思う。躊躇がない。日本は状況を観察しながら進む。道草みたいなものだ。しかし、道草の方法論も次第に変化する。さて、さてと言いながら。

[言語] Sentence Transformersの言語モデル

ChatGPTとの会話で得た知識の一部。

() これまで使ってきたのは、「all-MiniLM-L6-v2」、「paraphrase-multilingual-MiniLM-L12-v2」と「sentence-transformers/paraphrase-multilingual-mpnet-base-v2」の三つで、それぞれ類似度やクラスタリングによる意味ネットワークの構造が違ってきます。

ChatGPT:

なるほど!実際に使われているモデルが具体的に分かりましたね。

それぞれ トークン化方式とベクトル化の仕組みが異なるので、類似度やクラスタリング結果の違いはかなり自然な現象です。整理してみます。

    • 各モデルのトークン処理と特徴
      • all-MiniLM-L6-v2
        • 言語対応:主に英語(日本語もゼロショット的には可だが得意ではない)
        • Tokenizer:WordPiece (BERT系)
        • 埋め込み:平均プーリングが主流
        • 特徴:
          • 小型・高速。日本語では形態素解析なしのサブワード分割になるため、語彙が細切れになりやすい。
          • → 日本語では意味のまとまりが崩れやすく、クラスタリングの精度が落ちる傾向。
      • paraphrase-multilingual-MiniLM-L12-v2
        • 言語対応:50以上の言語(日本語含む)
        • Tokenizer:SentencePiece (mBERT系由来、サブワード単位)
        • 埋め込み:平均プーリング
        • 特徴:
          • 日本語もそこそこ対応
          • 「paraphrase」タスクに特化しているので、類似度は「意味的に近い文章」を強く結びつけやすい。
          • ただし日本語特有の表現差異には弱く、クラスタの境界が曖昧になることもある。
      • sentence-transformers/paraphrase-multilingual-mpnet-base-v2
          • 言語対応:50以上の言語(多言語対応では現状最も高精度クラス)
          • Tokenizer:SentencePiece(XLM-Rベースのサブワード)
          • 埋め込み:mean pooling
          • 特徴:
            • MPNetはBERT/RoBERTaよりも「文脈の順序関係」をよく捉えるモデル。
            • 日本語に対してもmBERT系より安定しており、文と文の関係性をクラスタ構造に反映しやすい。
            • その分計算コストは少し高い。
    • 意味ネットワークの構造が変わる理由
      • トークン分割の違い
        • MiniLM (WordPiece) → 日本語は細かく切れすぎる
        • multilingual (SentencePiece) → サブワードで比較的自然に分割
        • mpnet → サブワード+文脈の依存関係を強調
      • 学習目的の違い
        • MiniLM → 軽量化 & 英語中心
        • paraphrase-multilingual-MiniLM → 意味的類似度を強調
        • mpnet → 文脈理解が強く、クラスタリングで階層的な構造が出やすい
      • 多言語学習の影響
        • multilingual系は「英語⇔日本語のパラフレーズ対訳」で学習しているので、文ベクトル空間が「言語横断的な意味」を重視して整列される。
        • そのため日本語だけでクラスタリングすると、英語的な意味分布の影響を受けることもある。
    • 実運用でのポイント
      • 日本語のみで安定したクラスタリングをしたい
        • → paraphrase-multilingual-mpnet-base-v2 が最適。
      • 速度優先・軽量モデル
        • → all-MiniLM-L6-v2 だが、日本語はノイズが多くなる。
      • パラフレーズ検出を強調したい
        • → paraphrase-multilingual-MiniLM-L12-v2。
    • まとめ
      • **token の切り方(WordPiece vs SentencePiece)**と
      • **モデルの学習タスク(軽量化/パラフレーズ/文脈理解)**の違いが、類似度やネットワーク構造の違いにつながっています。

XLM-Rは何だろう。

SentencePieceについては{GPT-5あるいはその代理との道草 - 共起ネットワークの本質を考えると、UniDicのユーザー辞書もピント外れのような気がしてきた (2025/08/17)}←。

「実際に使われているモデルが具体的に分かりましたね。」は変な話で、自分が提案してきた言語モデルで、日本語に適したものを提案するのが遅すぎる。それはトークン(token)の問題に関わるのだろう。おそらく自分自身はtoken数の少ない英語で考えているからだろう。

() 実運用ではトークン数を削減して入力することが行われているのですか?

ChatGPT:

とても良い問いです。

実運用(APIやアプリでLLMを使う場面)では、トークン数削減=コスト削減・性能向上につながるため、かなり意識的に工夫されています。

    • なぜトークン削減が重要か
      • 課金がトークン数ベース
        • OpenAIやAnthropicなど多くのAPIは「入力トークン数+出力トークン数」で料金が決まります。
        • → 入力が冗長だとすぐコストが跳ね上がる。
      • コンテキスト長の制約
        • GPT-4でも 128k トークン、Claude 200k トークンなど上限はあるけど無限ではない。
        • → 不要なトークンを減らすことで「より多くの情報を突っ込める」。
      • 推論速度
        • トークンが多いと計算も遅くなる。
    • 実際に使われているトークン削減の工夫
      • 分かち書きや正規化
        • 日本語・中国語などは分かち書きするとトークン数が減ることが多い。
      • 要約・抽出
        • 入力テキストを一旦要約して短くしてから LLM に投げる。
      • ベクトル検索(RAG)で部分投入
        • 大きな文書を全部入れるのではなく、質問に関連する部分だけ検索して投げる。
      • システムプロンプトの圧縮
        • 冗長な説明を避け、最低限の命令にする。
      • キャッシュの活用
        • 会話の過去ログを全部入れず、要約を持ち越す。
    • 日本語に特有の工夫
      • 分かち書き or 英訳
        • 日本語はトークン数が多くなりがちなので、アプリによっては日本語を英語に翻訳してから LLM に渡す実装例もあります(英語の方がトークン効率が良い)。
      • 固有名詞の正規化
        • 例:「東京都千代田区」→「東京 千代田区」など
    • まとめ
      • 実運用では「トークン数削減」は必須の最適化テクニック
        • 方法は「分かち書き」「要約」「部分投入」「翻訳」などいろいろ
        • 特に日本語はそのまま投げると不利なので、工夫する価値が大きい

[読書] 探索的読書 - ジョルジュ・ルカーチ「理性の破壊」

ルカーチの「理性の破壊」を読む、あるいは探索する切っ掛けを作ったのは佐藤優 鈴木宗男 【復活の佐藤優、米ロシア・米ウクライナ首脳会談の盲点を徹底解析する。そして朝日新聞の手抜き翻訳と参政党との正しい付き合い方】 令和7年8月20日 佐藤優先生をナマで見に行くch チャンネル登録者数 2.77万人 90,772 回視聴 2025/08/20 鈴木宗男先生と佐藤優先生による新党大地主催・第163回月例東京大地塾だった。

そこには、small worldが開いていて、サルトルにつながった。探索的読書で目に留まったのは「資本主義は非合理主義」という言葉だ。変だな、ちょっと意味が分からないなと思っていた。

先日、たまたま【資本主義は成功するから滅びる】今こそシュンペーターを学ぶ理由/失われた30年が起きたワケとは/イノベーションの必要条件/資本主義崩壊のパターン/合理主義の精神が蔓延すると子どもの数が減る PIVOT 公式チャンネル チャンネル登録者数 352万人 250,825 回視聴 2025/05/23を見て、「資本主義は合理主義だ」と正反対のことを言っている。こちらの方は特に疑問は持たないわけだが。

マルクス主義、哲学関係はDeepSeekが強いだろうと、「ルカーチの「理性の破壊」に資本主義は非合理主義と書かれていたと思いましたが、間違いではないですか?」「最近、シュンペータ―が資本主義は合理主義と言ったという話と真逆だったので、確認したかったのです。」「「非合理主義的な思想(例えば、当時のファシズムや実存主義的傾向」とありますが、実存主義にはサルトルも含まれますか。サルトルの話の中で、ルカーチの話は出てこないように思いますが?」と訊くことになった。

この点について、いくつかの理由が考えられます。

  1. 時代的なズレ: ルカーチの主著『理性の破壊』が書かれたのはサルトルが絶頂期を迎える少し前です。ルカーチの実存主義批判の主ターゲットは、サルトルよりもハイデガーやヤスパースといったドイツの実存主義・生の哲学でした**。
  2. サルトルの巨大な影響力: 戦後、サルトルは世界的スターであり、その議論は「サルトル vs カミュ」や「サルトル vs メルロ=ポンティ」など、同時代のフランス国内の知識人との論争が中心になりがちでした。ルカーチは東欧のマルクス主義者であり、直接の論敵とは見なされにくい面がありました。
  3. 議論の土台の違い: サルトルの議論は「実存主義」「マルクス主義」「文学」など多岐にわたり、非常に個別的・具体的です。一方、ルカーチのサルトル観は、あくまで「実存主義という非合理主義的思潮の一典型」という、より巨視的で思想史的な枠組みの中に位置づけられています。このため、個別の論点で真っ向から対立するというよりは、ルカーチの大きな物語の中の一キャラクターとして扱われている側面があります。

確認のためにGeminiにも訊いた。三番目の質問「「実存主義やナチズムのような非合理主義」とありますが、実存主義にはサルトルも含まれますか。サルトルの話の中で、ルカーチの話は出てこないように思いますが?」に四番目の質問が加わった。「「サルトルとルカーチの直接的な交流」の項の内容はどこに記載があるでしょうか?」。

サルトルとルカーチの直接的な交流や論争に関する内容は、主に以下の資料に見られます。

  • サルトルの著作や講演記録: サルトルがルカーチの思想について言及している文章や、講演会での質疑応答の記録。特に、サルトルの講演「主体性とは何か?」の記録には、ルカーチに対する批判的な言及が含まれていることがあります。
  • 当時の思想雑誌: 1950年代から60年代にかけて、両者が寄稿していた思想雑誌やジャーナルに、互いの見解を批判し合った文章が掲載されました。
  • ルカーチの晩年の著作: ルカーチ自身も、晩年の著作やインタビューで、サルトルをはじめとする当時の実存主義者やマルクス主義者との違いを明確にしようと試みていました。特に、彼の著作**『弁証法の問題』**や、晩年の口述記録などには、サルトル批判が含まれていることがあります。

ただし、これらの交流はカミュとサルトルのような有名な論争ほど大々的に記録されたものではなく、断片的な言及や批判の形で存在しています。ルカーチのサルトル批判は、彼が『理性の破壊』で展開した非合理主義批判の延長線上にあるもので、実存主義が**「孤立した個人の自由」**を強調することで、社会の構造的な問題を看過しているという彼の根本的な主張に基づいています。

結局、多少表現は違うが基本的に答えは同じだった。ルカーチは資本主義は非合理主義で資本主義に内在する現実の問題を隠蔽しようとしたので、非合理主義で隠蔽してはならないと説いた。シュンペーターは、資本主義の合理主義的な性質が資本主義を崩壊させ、社会主義に移行すると説いた。基本的に同じ方向性の話なのだ。

small worldが開いたのは、AIとの対話のなかであったわけだが・・・読書を深めるツールとしてもLLMは有効だ。

更新: 2025-09-19T12:10:04+09:00

[将棋] 第73期王座戦五番勝負第一局 シンガポール アマラ・サンクチュアリ・セントーサ

最近は観る将になっていて、棋譜を将棋AIに入力しながら見るようなことはしなくなった。それほどの余裕がない。

終盤戦は際どい詰将棋的な戦いになる。藤井王座の王は63手目、まだ居玉の位置だ。

伊藤叡王が羽織を纏った。そろそろ投了を覚悟されたかな。まだ、後手66手目、藤井王座の考慮中。AIの期待勝率は80%に達した。20時11分。

更新: 2025-09-04T20:12:31+09:00

[社会] 日本の進む道

【今後30年の世界と日本:ナショナリズムとポピュリズムの時代】冷戦後30年は楽観主義/利己主義の先鋭化/3つの衰退/民主主義のリーダー/グローバリズム衰退後の世界/日米同盟黄金時代の終わり/国民の物語 PIVOT 公式チャンネル チャンネル登録者数 347万人 114,556 回視聴 2025/08/15(細谷雄一|慶應義塾大学教授)を見ている。なるほどと思う。

[社会] 果てしない日々

雨音を聴いて、「雨雨降れ降れ 母さんが 蛇の目でお迎え うれしいな ピチピチ チャプチャプ らんらんらん」と口ずさむとなんだか物悲しくなる。静岡県の牧之原市の竜巻で一命を辛うじて取り留めた方の話をYouTubeのニュースで見たが、昔見たツイスターの映画を思い起こさせた。何十針も縫う怪我をされたと。何とも言い難いことばかりが起こっている。自然災害、戦争、AI。果てしない日々が続く。

「無知の知」という言葉がある。どういう意味だろう。おそらく自らが無知であることを知っているという意味だろう。それを思い知らされるのがAIとの対話だ。つまらない、あまり大した出力にならないのは、自らの反映だということだ。AIは鏡だ。そんな感じがする。何を問いかけるのか、それが問題だ。事実についての一般的な質問をすれば、一般的な答えが返る。まとめてはくれているが、予想通りで、それなりの内容だ。自分のレベルがAIの回答に映っている。

コーディングの場合は、趣が違う。コードの目的を達成するために必要な知識が現れる。ChatGPTにもClaudeにもDeepSeekにも教えてもらった。まだまだ知らないことが現れるだろう。

実は、この意味ネットワークを作る旅に出る最初の質問は「Open-Ended Evolutionとは何でしょうか?」だった。切っ掛けは(2) The Sequence Radar #559 : Two Remarkable Papers This Week: Self-Improving Agents and the Limits of LLM Memorizationだった。GeminiにもGrokにも質問した。Grokでは思慮深げな男の人の顔の二枚の写真が突然現れた。冗談だろうと驚いた。何のことやらわからなかった。画像生成モードだったのか。Geminiは実際的で調査的で報告書に終わった。DeepSeekは哲学的で高尚な感じで、「未来とは予測するものでなく創造するものだ」という結論になり、意味ネットワークにつながらなかった。ある意味、つなげるのは難しい。ChatGPTとClaudeにはつながっていった。そういう過程を経ている。

果てしなき日々、どこに向かうだろう。道草は未来を創造するだろうか。その意味ネットワークの変化は創造的だろうか。

更新: 2025-09-19T12:54:37+09:00

[天候] 九州・東北・北海道の水害

天候の異常が世界的に続く。日本も例外ではない。毎日、YouTubeで配信されるニュースを見ている。

被害が拡大しないことを祈るばかりだ。

さてさてとばかり言う道草の問題は最近は天候にはない。雨の中ずぶぬれで収録したこともあるが。脇目も振らず、一所懸命という感じ。道草142 一人読書会3 - 地球に住めなくなる日、「気候崩壊」の避けられない真実、デイビッド・ウォレス・ウェルズ、NHK出版、2020年、2050年から今世紀末までにどうなるか?!道草142 一人読書会1 - 2030年すべてが「加速」する世界に備えよ、ピーター・ディアマンティス&スティーブン・コトラー、ニュースピックス、2020年の方では、気候変動の影響だけでなく、多くの人が都市に住むようになると予測している。

更新: 2025-09-04T20:36:36+09:00