自分のブログ53記事をUMAPで並べてみたら、「実践記」と「Claude入門」はきれいには分かれなかった
約13分で読めます

自分のブログ53記事をUMAPで並べてみたら、「実践記」と「Claude入門」はきれいには分かれなかった


このブログのタグには、大きく2つのトラックがある。「実践記/機械学習」——Isomapで結び目をほどいたり、焼きなまし法でDatasaurusを描いたり、実際に手を動かして数字を出すもの。そして「Claude入門/比較」——ClaudeやAIツールの使い方、他サービスとの比較を説明するもの。書いている本人としては、この2つはかなり違う頭の使い方をしている感覚がある。

でもそれは、書いている本人の思い込みかもしれない。実際の文章として、この2つは本当に別物として離れているのか、それとも同じような言葉遣いで書かれていて、ラベルだけが違うのか。今回はこのブログ自身の53記事を材料に、自分のコーパスを自分で分析してみる。

埋め込みの作り方: LLM APIなしで、日本語を数値化する

やりたいことは単純だ。全記事の本文をベクトル化して、2次元に落として並べる。ただし今回はいくつか制約がある。

  • LLM の embedding API は使わない(呼び出せる環境にない)
  • 日本語の形態素解析ライブラリ(MeCab、Janome、SudachiPyなど)も入っていない——つまり「単語」に分割できない

そこで使ったのが TF-IDFの文字n-gram(sklearn.feature_extraction.text.TfidfVectorizeranalyzer='char_wb'ngram_range=(2,4))だ。単語分割を経由せず、2〜4文字の連続した文字列の出現頻度で文章を表現する、言語に依存しない古典的な手法。日本語は英語ほど分かち書きの恩恵が大きくないぶん、文字n-gramでも実用になることが知られている。

「文字n-gram」と言われてもイメージしづらいと思うので、具体例を1つ。たとえば「次元削減」という4文字を今回の設定(2〜4文字)で分解すると、「次元」「元削」「削減」「次元削」「元削減」「次元削減」——2〜4文字の窓を1文字ずつずらした断片が、全部まとめて取り出される。「次元削減」がひとつの単語だという知識はどこにも使っていない。この断片の出現頻度を記事ごとに数え、どの記事にも現れるありふれた断片の重みを下げたもの(これがTF-IDF)が、その記事を表す1本のベクトルになる。「同じ言い回し・同じ専門用語を使う記事同士は、断片の分布も似るはず」というのがこの表現方法の頼りどころだ。

各記事のfrontmatterとコードフェンス、Markdownの画像・リンク構文を取り除いて本文だけを抽出し、53記事(このバッチで同時執筆中の他記事とこの記事自身は除く)をベクトル化すると、53記事 × 34,056次元のTF-IDF行列になった。

次元削減は、まずimport umapを試したら普通に通った(umap-learn 0.5.12が入っていた)。なので今回は当初想定していたPCA→t-SNEのフォールバックではなく、UMAPを本採用にした。ただし1手法だけで判断すると恣意的になるので、線形次元削減(TruncatedSVD、疎行列版のPCA相当)とt-SNEも並べて比較する。UMAPはコサイン距離で計算し、t-SNEはいったん50次元まで線形圧縮してから通常のユークリッド距離で計算している。

結果: 3手法とも、ゆるく偏るが、きれいには分かれない

タグに「実践記」または「機械学習」を含む記事を赤、「Claude入門」または「比較」を含む記事を青、どちらでもない記事(副業・プロンプト・著作権・雑記など)を灰色にして、3手法で並べた。

線形次元削減・t-SNE・UMAPの3手法で53記事を2次元に並べた散布図。線形とUMAPでは右下に赤(実践記/機械学習)の塊、左上に青(Claude入門/比較)の塊がゆるやかに分かれて見えるが、それぞれの塊の中に少数の赤や灰色の点が紛れ込んでいる。t-SNEは全体が入り乱れており、色の偏りはあるものの視覚的な境界線はほとんど見えない

線形次元削減とUMAPでは、右下に赤(実践記/機械学習)の塊、左上に青(Claude入門/比較)の塊がなんとなく見える。ただし「なんとなく」止まりで、青の塊の中に赤い点が1〜2個混ざっているし、灰色(その他)はどちらの塊にも顔を出す。t-SNEに至っては、色の偏りこそあるものの視覚的な境界線と呼べるものがほとんどない。

数字で確認する。2群(実践記/機械学習33記事 vs Claude入門/比較12記事、計45記事)の分かれ具合をシルエットスコア(-1〜1、1に近いほど2群がきれいに分離、0前後は分離なし)で測った。

空間シルエットスコア
高次元の生TF-IDFベクトル(コサイン距離)0.064
線形次元削減(SVD/PCA相当) 2次元0.602
t-SNE 2次元0.280
UMAP 2次元0.549

正直、これは予想と違う数字だった。34,056次元の生の特徴量空間では、2群はほとんど分離していない(0.064はほぼ「ランダムに分けたのと変わらない」水準)。ところが同じデータを2次元に落とした瞬間、線形もUMAPも0.5〜0.6まで跳ね上がる。

これはおそらく、次元削減が「本当に存在する分離」を見つけているというより、コーパス全体の分散が一番大きい方向(≒記事の長さや専門用語の密度が効いてくる軸)をたまたま2本の軸に凝縮した結果、その軸がタグとゆるく相関している、という方が近い。証拠に、実践記/機械学習の記事は本文平均3,314文字、Claude入門/比較は平均1,567文字と、2倍以上の長さの差がある。数式やコードの説明が伴う実験記事はどうしても長くなる一方、Claude使い方説明は比較的コンパクトにまとまる。

これは平均値の比較だけでなく、図の上でも直接確かめられる。さっきと同じ2次元配置を、タグの色ではなく本文の文字数で塗り直してみた。

線形次元削減とUMAPの同じ2次元配置を、タグの色ではなく本文の文字数の濃淡(紫のグラデーション、濃いほど長い)で塗り直した散布図。どちらの手法でも、タグの図で青(Claude入門/比較)の塊があった側に薄い色の点、赤(実践記/機械学習)の塊があった側に濃い色の点が集まり、タグの分かれ目とほぼ同じ向きに文字数の勾配が現れている。パネル下の注記として、線形の第1軸と文字数の相関はr=0.88、UMAPは各軸との相関がr=+0.57と-0.66であることが示されている

タグの図で「青の塊」だった側は薄く(短い記事)、「赤の塊」だった側は濃く(長い記事)、タグの分かれ目とほぼ同じ向きに濃淡の勾配が走っている。実際、線形次元削減の第1軸と本文文字数の相関係数はr=0.88——この軸は事実上「記事の長さ軸」と呼んでいいくらい強く相関している(UMAPも各軸との相関がr=+0.57 / -0.66ある)。「話題」の違いというより「文章の量と密度」の違いを、次元削減後の低次元空間は拾いやすいのかもしれない。

図の上で「隣どうし」になった2記事は、実は全然似ていなかった

UMAPの2次元図をよく見ると、青(Claude入門/比較)の塊の内側に赤い点が入り込んでいる。「生成AIの『ハルシネーション』に要注意」(実践記/機械学習)の記事だ。この記事のUMAP平面上での最近傍3記事を機械的に調べると、「Claudeの最新モデル比較」「Claudeでリサーチに使える『エージェント』と『Skill』入門」「Claude vs Gemini」——3つとも全部Claude入門/比較トラックで、図の上では完全に「青の仲間」として配置されている。たとえば「エージェント/Skill入門」との2次元上の距離は0.51。全53記事について「一番近い隣の点までの距離」を測ると中央値が0.38なので、これはこの図で言う「すぐ隣」に相当する近さだ。

ところが、この隣どうしの2記事(ハルシネーション記事とエージェント/Skill入門)の高次元での生のコサイン類似度は0.143。コーパス全体の平均類似度が0.096程度なので、平均よりは少し高いが、図から受ける「同じ仲間」という印象ほどの近さでは全くない。2次元図の上でどれだけ近くても、それは低次元投影というレンズを通した近さであって、元の特徴量空間での近さを保証しない——UMAPやt-SNEの結果を見るときによく言われる注意点を、身をもって確認した形になった。

上のグラフは実際にホバーして動かせる。自分の記事がどれとどれの近くに配置されているか、気になったところを触ってみてほしい。

タグより解像度の高い単位: series はどの手法でもきれいに固まる

一方、はっきり効いたものがある。連載としてseriesを持つ記事群(三つ葉結び目・スイスロールなどの「多様体学習の実験」、Datasaurusなどの「最適化のライフレッスン」ほか計6シリーズ)は、同じseries内の記事同士のコサイン距離が、コーパス全体の平均距離(0.904)よりも一貫して短かった。

series記事数series内の平均コサイン距離全体平均との差
画像処理の周波数分析20.679-0.225
生成モデルを一から実装する30.716-0.188
Self-Attentionを実装する30.728-0.176
多様体学習の実験40.763-0.141
探索と活用のアルゴリズム30.769-0.135
最適化のライフレッスン40.806-0.098
全記事ペアの平均0.904

6シリーズ全部が、例外なく全体平均より近い。実際に「三つ葉結び目」の記事に本文が一番近い記事を機械的に探すと、上位2件は「ロールケーキに穴を開けたら」(スイスロールの穴あき実験)と「くるくる巻いたデータ」(スイスロール本編)——どちらも同じ「多様体学習の実験」seriesだった。

series(連載)ごとに色分けした53記事のUMAP散布図。seriesを持たない記事は薄い灰色で左上に広く散らばっているのに対し、6つのseriesはそれぞれ数点ずつの小さな塊として右下側にまとまっており、隣接するseriesの塊同士も互いに重ならず区別できている

タグ(実践記/機械学習 vs Claude入門/比較)という大きな2分類はぼやけて重なるのに、seriesという細かい単位はどの手法でもくっきり固まる。考えてみれば当然で、同じseriesの記事は「Isomap」「t-SNE」「コサイン距離」のような固有名詞を繰り返し使い、語彙レベルで似ている。実際、「三つ葉結び目」の記事でTF-IDF重みが大きい文字n-gramを上から見ると「び目」「結び目」「map」(IsomapやUMAPの断片)「復元度」……と、その連載でしか繰り返し出てこない語彙のかけらがずらりと並ぶ。文字n-gramはこういう「同じ道具を何度も呼ぶ」近さをまっすぐ拾う。逆に「実践記/機械学習」というタグは、扱っている技術がIsomapだろうとDropoutだろうとBPEだろうと同じ1つのタグが貼られるので、語彙的にはむしろバラバラな記事の集合になる。

手を動かして意外だったこと

一番意外だったのは、「2次元の図できれいに分かれて見える」ことと「高次元の元データで実際に分かれている」ことが、こんなにも乖離しうるという点だった。シルエットスコア0.064(生データ)から0.549(UMAP2次元)への跳ね上がりは、次元削減が「見せたい構造を強調する」という性質を持つことの、数字で見た実例だ。UMAPやt-SNEの図はいつも「きれいに分かれている!」という第一印象を与えてくれるが、その分かれ方が元の高次元空間でどれだけ実体を伴っているかは、別途確認しないと分からない。

そしてもう1つ。タグという粗い2分類より、series という細かい単位のほうが、埋め込み空間でずっと素直に姿を現した。これは自分の記事の書き方についても示唆的だった。「実践記/機械学習」対「Claude入門/比較」という自己認識は、書いている自分にとっては意味のある区分だが、文章そのものの類似性という観点では、その粗さゆえにほとんど何も語っていない。本当に似ているのは、同じ固有名詞・同じ実験装置を繰り返し使う、もっと具体的な単位——今回で言えばseriesだった。カテゴリ分けは、細かくするほど「実体のある近さ」を捉えやすくなり、粗くするほど「自分がそう思いたい区分」に寄っていくのかもしれない。

まとめ

  • ブログの既存53記事(このバッチの同時執筆記事と本記事自身は除く)の本文をTF-IDF文字n-gram(char_wb, 2-4gram)でベクトル化し、UMAP(実際にインストールされていたので本採用)・線形次元削減・t-SNEで2次元化した
  • 「実践記/機械学習」(33記事)と「Claude入門/比較」(12記事)は、線形・UMAPでは緩やかな塊として視覚的に偏るが、t-SNEではほぼ判別できず、どの手法でも完全な分離ではなかった
  • シルエットスコアは高次元の生ベクトルで0.064(ほぼ無分離)、UMAP2次元で0.549、線形2次元で0.602——次元削減後に「見かけ上の分離」が大きく強調される可能性がある
  • UMAP平面上で隣どうしに配置された異トラックのペア(「ハルシネーション」記事と「エージェント/Skill入門」記事、2次元距離0.51は最近傍距離の中央値0.38と同水準)の高次元コサイン類似度は0.143(全体平均0.096をわずかに上回る程度)で、2次元図上の近さは高次元の近さを保証しなかった
  • 一方、series(連載)というタグより細かい単位は、6シリーズすべてでseries内平均コサイン距離が全体平均(0.904)を明確に下回り、どの次元削減手法でも小さな塊として一貫して現れた