Blueskyに爆誕したおしゃべりロボ「プルスカちゃん」の技術的な解説をするよ!
Blueskyにて絶賛稼働中のおしゃべりロボ「プルスカちゃん」について技術解説します。プログラミングって楽しいね〜!
前置き
シロイは今でこそインターネット芸人 主に心理学や哲学の本を書く仕事をしていますが、元々はITエンジニア出身です。
ジャンルは何であれ、私は自分が面白そうだと思ったものを形にしていくのが好きなんですよね。プログラミングのスキルだって、元々は10代の頃の趣味のゲーム制作で身につけたものですから、別に職業上の必要などに迫られなくても、放っておけば好き勝手に何かを作って遊んでいる人間のような気がします。
それで先日、Blueskyに「プルスカちゃん」というアカウントをお遊びで作成しました。プルスカちゃんの裏側では、フォロワーが直近に投稿した文章を学習した上で、それを真似た「一応日本語っぽい」文章をランダムに生成し、定期的に投稿するというプログラムが動いています。
迷子 犬 柴犬 どうでもいい事で独自のリズムに作り変えることがあるというから、明日はハンドドリップも試してみるとこんなに歩いたのよ。
— プルスカちゃん (@purupurusky.bsky.social) 2025-04-27T03:57:27.359Z
ワンマンがありまーーーーーーーーす!!!!!!!!!!!! でーーーーす!
— プルスカちゃん (@purupurusky.bsky.social) 2025-05-03T07:14:31.494287+00:00
このプルスカちゃんの誕生をシロイのメインアカウントで告知した際、もしかしたら「どういう仕組みになっているのか詳しく知りたい」という方もいるかもしれないな…という気がしたので、せっかくだから簡単に解説記事を書くことにしました。
マルコフ連鎖というアイデアについて
この手のプログラムとしては、おそらく多くの方が旧Twitterの有名アカウント「しゅうまい君」を真っ先に連想するのではないかと思います。アイデアの盗用と思われるとちょっと悲しいので、この点を最初に説明しておきます。
実は、このような文章生成のアイデアは相当古くから存在するものです。プルスカちゃんの文章生成の仕組みを支えているのはマルコフ連鎖と呼ばれる確率論の話で、この概念が世に出たのがいつなのかは私も詳しく知りませんが、名前の掲げられた数学者のアンドレイ・マルコフの生没年が 1856年-1922年であることを記しておけばひとまず十分でしょう。
私が10代の頃(生成AIもスマホもない!)に接していた「20年前のインターネット」上にも、マルコフ連鎖による文章生成というのは普通に存在していた情報でしたし、ある程度の好奇心のあるプログラマの人ならば、ハフマン符号(データ圧縮)やミニマックス法(ゲーム理論)といったアルゴリズムと同じく、「面白そうだな」「自分でもやってみるか」と興味をそそられるような対象の一つだったものと思います。
先述の「しゅうまい君」は、スマホとSNSが爆発的に普及した2010年代のネット社会でたまたま有名になったものです。そして、みなさんが認識しているであろう「しゅうまいっぽいさ」とは、おそらくあのアカウントが「狙ってそのように個性的に設計されていた」からではなく、マルコフ連鎖という仕組みで生成された文章が本質的に持つ意外性の面白さによるものと考えられます。実際、少しばかりのプログラミングの心得のある人であれば、あの「しゅうまいっぽいさ」は非常にシンプルな実装によってすぐに再現が可能なものです。
さて、前置きはここまでにして、本題の解説へと進みましょう。正直、数学的に厳密な話は私の専門外になるので、ここではそういう正確さは置いておいて、「どうして読み込んだテキストから新しいおしゃべりが生み出せるのか」をざっくり説明していきたいと思います。
(もしかしたら、専門家の人が読むと「マルコフ連鎖」という用語の使い方自体に「頭痛が痛い」的なおかしさを感じるかもしれませんが、どうぞご容赦ください…!)
文章生成の仕組み
1. 学習のステップ
まず、学習元として入力された文章が次のようなものだったとします。ささやかな日記の断片みたいなイメージです。
明日こそは晴天になってほしいなあ。(…中略…)ところで、カントリーマアムにこれ以上小さいサイズになってほしいと思ってる人いる?
下準備として、日本語を「最小限の意味を持つまとまり」に分割するという処理を行う必要があります。このまとまりを「トークン」と呼びます。
たとえば、「明日は…」で始まる文章があったとしたら、「明日」と「は」は分割可能ですね。しかし、「明日」を「明」と「日」に分割すると、単語としての意味をなさなくなってしまうので、この2文字は分割しません。「明日」は2文字で1トークン、「は」は1文字で1トークンになります。
こういった調子で先の例文をトークンに分割してみると、以下のような形になります。
明日 / こそ / は / 晴天 / に / なって / ほしい / なあ / 。 / (…中略…)/ ところで / 、 / カントリーマアム / に / これ / 以上 / 小さい / サイズ / に / なって / ほしい / と / 思ってる / 人 / いる / ?
次に、元々のマルコフ連鎖というのは「ある状態の次にはどのような状態が取られうるか」を考えるものなのですが、今回のプルスカちゃんの例では「直前の2つのトークンの次には、どのようなトークンが現れうるか」に注目します。
つまり、例文を「学習する」という過程で、具体的に以下のような情報を得ているということです。(最初の2行だけは文頭処理のため例外的ですが、3行目以降はすべて「直前の2トークンと次のトークン」に注目している点を確認しましょう)
・文章の先頭には「明日」が現れうる
・「明日」の次には「こそ」が現れうる
・「明日 / こそ」の次には「は」が現れうる
・「こそ / は」の次には「晴天」が現れうる
・「は / 晴天」の次には「に」が現れうる
・「晴天 / に」の次には「なって」が現れうる
(…以下略)
実は、要点はたったのこれだけです。これだけわかっていれば、次の生成ステップへと進むことができます。
一応補足しておくと、プルスカちゃんは正確には「A/Bの次にはCやDが現れうる」ではなく「A/Bの次にCやDが現れる確率」という形で学習していて、このあとのランダム生成にもその確率の重みが反映されています。今回は説明を簡単にするために、「現れうる」という情報だけで解説していますが、この情報だけでも実際にちゃんと動くものは作れます。
学習はここまでで終わったので、次に考えるのはランダム生成のステップです。
2. 生成のステップ
まず、「文章の先頭に現れうるトークン」としては「明日」が学習済みなので、ここでは最初に「明日」を生成するものとします。ランダムな生成とはいっても、基本は「学習データで出てきたパターンをなぞる」という動きを取ります。
今回の例では、元々の学習データに「明日」のあとに続くトークンは「こそ」しかなく、「晴天」といったトークンも他の使い方をされていなかったものとして、生成結果の最初のほうは先述の例文と同一になったものとしましょう。以下が生成途中の文章です。
明日こそは晴天になってほしい
ここで、「ほしい」の次に生成されるトークンを考えます。学習済みの例文を思い出してみると、そこには「明日こそは晴天になってほしいなあ」と「カントリーマアムにこれ以上小さいサイズになってほしいと思ってる人いる?」という文章が含まれていましたね。
太字で強調したように、「なって / ほしい」というトークンの流れがどちらの文章にも存在していますね。つまり、この2つのトークンの直後に現れる新しいトークンとしては、複数のパターンが学習済みだということです。そこで、生成の流れは以下の2パターンに分岐しうることになります。
【パターンA】 明日こそは晴天になってほしいなあ
【パターンB】 明日こそは晴天になってほしいと
ここで、仮にランダムな選択がパターンBへと進んだとすると、学習済みの例文の中には存在しなかった意味を持つ文章が生成されうることがわかります。次のような流れになるでしょう。
明日こそは晴天になってほしいと
↓
明日こそは晴天になってほしいと思ってる
↓
明日こそは晴天になってほしいと思ってる人
↓
明日こそは晴天になってほしいと思ってる人いる
↓
明日こそは晴天になってほしいと思ってる人いる?
おわかりでしょうか。「2トークンのセット→次の1トークン」という組み合わせ自体は学習済みのパターンをそのままに利用していますが、このパターンに従ってトークン生成の連鎖を行っていくと、最終的には例文のどこにも存在しなかった意味を持つ文章が得られるということです。
これは短くて単純な例なので、生成された文章が例文の2箇所に由来しているということはすぐにわかりますね。しかし、学習済みの文章が十分に長いものであれば、3トークンの組み合わせは数万通りかそれ以上といった膨大な数になります。そこから生成される文章は、元々の例文をまったく想像できない、思いがけないような意外な内容となる可能性が出てくるというわけです。
ポイントとして、生成された新しい文章がどれほど意外なものであったとしても、それを再びトークンに分割して「3トークンの流れ」に限ってチェックすれば、そのパターンは元々の学習データに必ず1度は現れているという点があります。逆に、学習データに含まれない3トークンの流れは決して生成されません。
つまり、学習データが自然な日本語であれば、生成された文章に「をを」というひらがなの並びが出てきたりだとか、文末が「もしくは。」という形で終わってしまうようなことは基本的にないということです。仮にマルコフ連鎖という考え方を知らずに、国語辞書に載っている名詞や接続詞をランダムに出力し続けるだけのプログラムを組んだとしたら、おそらく今挙げたような不自然な文字の並びが頻繁に発生してしまうことになるでしょう。その生成結果は「漢字やひらがなが並んでいるのに、全然日本語っぽくは見えない」文章になります。
この「一つひとつの語句の前後だけは自然な流れなのだけど、全体としては予想外の方向に流れ着く文章が生成される」という点こそが、マルコフ連鎖の考え方に基づいた文章生成の面白さであると言えます。
仕組みの解説はここまでで、次は実装のお話になります。
実装上の技術的な話
何で書かれているのか
プルスカちゃんはPythonで書かれています。2025年現在では特に付記する必要はないかもしれませんが、3.x系のほうです(ちなみに2.xは2020年にサポート終了)。補助的にシェルスクリプトも少し使っています。
Pythonは……というか、現代的な言語では当たり前のことなのですが、リストや辞書といったデータ構造をまるで整数の足し引きのような軽いノリで扱える点は本当に助かります。私が新卒で入った会社はメーカーのソフトウェア部門、つまり組み込み系の分野でして、そこで使っていたのはC言語(C++ではなく!)だったので、何をするにも「そのデータはメモリ上にどう配置されているのか」を意識しなければいけないようなめんどくささがありました。
技術畑の人はご存知かと思いますが、日本語の自然言語処理というのは本来かなり大変なものです。真っ先に挙げられる性質として、日本語の文章は英文と異なり、単語間のスペースのような区切りがデータ上存在していないという点があります。機械的に「区切りと見なしてほぼ間違いない」と言えるのは、句読点などの記号くらいしかありません。
たとえば「すもももももももものうち」という文字列があったとき、日本で生まれ育った人であればこれが「すもも / も / もも / も / もも / の / うち」というトークンで成り立っているということは知っていますね。しかし、これは言葉遊びとして有名だから知っているだけで、純粋な日本語の理屈のみで「どうしてこのようなトークンの分割として綺麗に解釈できたのか」を説明するのは、おそらく日本語ネイティブであっても困難でしょう。
コンピュータにとっての日本語が「区切りなしにベタッと並んだマルチバイト文字の連続」でしかないという点を考えると、コンピュータから見た「こんにちは、今日はいい天気ですね」という文章は「ももももも、もももももももももも」とさほど変わらないと言ってしまってもいいかもしれません。
もちろん、「こんにちは」の文章は「ももももも」の文章ほど「自然な日本語として解釈できるトークン分割位置の組み合わせ数」が多くはないので、これはやや極端な比喩ですが、少なくとも日本語ネイティブが直感的かつノータイムで「ここで区切られている」と判断できる文章でも、その直感的な判断プロセスをコンピュータのロジカルな動きに落とし込むのは簡単ではなさそうだということです。
で、肝心の「どうして実際にそういうトークン分割ができているのか」ですが、プルスカちゃんではPythonで使える形態素解析器のJanomeを使用しています。このライブラリの中身がどうなっているのかは私もマジでよくわかっていないのですが(中に妖精さんがいるのかな??)、トークン分割という仕事はJanomeさんへの丸投げです。Janomeさんのようなオープンソースのライブラリが存在しなければ、プルスカちゃんのようなお遊びはまず生まれなかったでしょう。
また、Blueskyに投稿された内容を学習するためのテキスト取得、そこからプルスカちゃんとして生成した文章の自動的・定期的な投稿については、AT Protocol SDKによって行うことができました。この辺はBlueskyがオープンなSNSを標榜してくれているおかげです。
現状のXやThreadsなどのSNSの場合、API等に課金せずにこういう仕組みを実装することはおそらく不可能だと思います。やっててよかったBluesky!
どのように運用されているのか
普通に消費者として暮らしているときは特に意識しないものですが、私たちがスマホやパソコンを通じて何かのサービスを使うときって、必ず世界のどこかにそのシステムを動かし続けているサーバが存在するのですよね。
シロイは個人で仕事をしていて、さすがに常時動きっぱなしのサーバなどは所有していないので、自分のMacBook Airの裏側で勝手にシェルスクリプトが走っているような状態にしています。レンタルサーバ的なものを使ってもいいのですが、プルスカちゃんは元々お遊びで生まれたものですし、毎月のサーバ代を支払うためには私の書いた本が毎月毎月毎月毎月たくさん売れてくれなければなりません。たすけて〜!
そこで代替案となったMacBook Airですが、私はつい最近お金もないのにM4のモデルを新しく購入していて、古いM1は予備のマシンという扱いにしています。このM1のほうをディスプレイを閉じた状態でもスリープしないように設定して、部屋の隅っこで音もなく稼働してもらっている状態にしています。消費電力はそんな大したことないはず…。
イメージ的には、時刻チェックと数十分間のsleepを交互に行う無限ループの入ったシェルスクリプトを走らせておいて、指定された時間(一日に数回の発言タイミング)になるたびにメインのPythonプログラムを呼び出している形です。単純ですね。
という感じで、運用といっても「予備のノートで常時スクリプトを走らせているだけだよ」という話でした。
終わりに
以上、プルスカちゃんの解説記事でした!
みなさんも何か新しいものを見たとき、「どういう仕組みで動いているのだろう?」という疑問を持つことはないでしょうか。こういう気持ちって、別に技術者や表現者として大切だという話に限らなくて、好奇心を持つということには「生きている時間の全体を以前よりも面白くする」効果があると思っています。
以前、ネットでこんな話を読みました。外でお弁当を買うと、メインのおかずの下に少量のパスタが敷かれていることってよくありますよね。あれには実は理由があって、揚げ物やお肉系のおかずは時間の経過とともに油分が出てくるので、あのパスタには「余計な油分を吸い取って、主役のおかずがベチャっとしてしまうのを防ぐ」という役割があるのだそうです。また、揚げ物などは調理直後に盛り付けを行うと高温になっていることがあるので、それがプラスチック容器に直接触れるのを防ぐという役割もあるのだとか。
こういう事実を知ったときのなるほど感というのは気持ちがいいものですが、逆にそれを知らない人は「上げ底だ! 企業は貧乏くさいテクニックで消費者を騙している!」などと憤慨することになったりもします。
ネットでよく見かける陰謀論みたいな話も、おそらく「いろいろと事情があってそうなっている」というこの社会の複雑さの認識の欠如によるものだと思うので、「好奇心は知識と理解を呼び、知識と理解は私たちを守ってくれる」と言えるのかなと思います。
いやはや、こんな結論でこの解説記事がまとまるとは思っていませんでした。教訓を引き出そうとするのは物書きの癖かな?
とにかく、みんなもこの世のいろんな物事に目を向けて、少しでも興味を引かれたら「調べてみる」「自分でもやってみる」ということをしていくと面白いと思います。では。
後日追記:プルスカちゃんがパワーアップした件
ふと思い立って、このnoteを書いた翌週にプルスカちゃんを少しパワーアップさせました。リプライに対して返事をしてくれるようになっています。
やった内容はわりと単純で、以下の追加スクリプトを定期的に走らせるような形です。
プルスカちゃんの直近の投稿一覧を取得し、新たなリプライがついていないか確認
届いたリプライをトークンに分割して、その中に含まれている名詞の一覧から「反応用キーワード」を1つだけランダムに選ぶ(名詞がなければ他の動詞などの適当なトークンを代わりに選ぶ)
「反応用キーワード」を先頭トークンとして、普段と同じようにマルコフ連鎖で文章を生成する(学習データは通常稼働しているときのものを流用する)
生成された文章をリプライをくれた人宛てに投稿する
ポイントは2と3の「反応用キーワード」を選んでいるところで、これによって一応ちゃんとした返事っぽくなったりならなかったりします。
また何か思いついたら機能追加します。みなさんもときどきプルスカちゃんと遊んであげてください!
関連書籍
3月に出した書籍「人生の意味のつくり方」でも、私たちの生きる世界をコンピュータの仕組みと並べて考えた部分があります。よかったらチェックしてみてくださいね!
関連記事
変化と拡張の感覚について (2024.8.8)
思考は物事を前進させない (2024.9.21)
生成AIはモチモチ生命体の夢を見るか (2025.4.14)
いいなと思ったら応援しよう!
いつも応援ありがとうございます✨ いただいたチップはおやつ代へ…(カロリーを書籍とnote記事に変換しています🍩🍰☕️