このブログの全エントリで学習した極小規模言語モデルを作った - Copilot試してみた日記

ブログの全エントリを読み込むコード作ったので、とりあえずこれで学習して極小規模言語モデルを作ったら面白かろう、とやってみました。
というとかっこいいけど、まあ形態素解析して続く単語の頻度を覚えておいて、頻度に応じた単語をつなげていうという、Twitter老人会の方ならご存じの圧縮新聞ですね。圧縮きしだのHatenaか。

まあ、ここではてなブログアーカイブを読み込むコードを書いたので、これで何かしたら面白かろうなと。
GPTのEmbeddingを利用してブログの投稿に対する近いものを探し出す - きしだのHatena

それで、以前 圧縮新聞ぽいものを作っていたので、これを改めてはてなブログをデータに作ってみた感じです。
Igoという形態素解析器をつかって圧縮新聞っぽいものを作ってみる - きしだのHatena

圧縮新聞のアカウントは凍結されていますが、雰囲気はこちらを。
圧縮新聞さん迷言集 - Togetter

圧縮新聞を作ったphaさんの話はこちら。
「圧縮新聞」を作った - phaの日記

圧縮新聞では4単語の連結を見ているけど、今回はそれっぽくシンプルなコードになったほうがいいので(そしてめんどいので)2単語のつながりだけ見てます。

最初は前回と同じIgoで作ってたのだけど、Mavenが用意されてないのでローカルリポジトリに登録する必要があって面倒だし、新しい形態素解析エンジン使うのがいいかなと、探してみたらSudachiというのがよさそう。
ワークス徳島NLPリソース | Sudachi

と思ってやってたのだけど、ドキュメントがない・・・
動かしてる人はいたのだけど、最適なのかどうかもわからないので、あきらめ。
Javaで形態素解析!SudachiをSpring bootで利用してみよう - エキサイト TechBlog.

やはりKuromojiか
https://github.com/atilika/kuromoji

こんなDependency追加するだけでいけそう。

<dependency>
    <groupId>com.atilika.kuromoji</groupId>
    <artifactId>kuromoji-ipadic</artifactId>
    <version>0.9.0</version>
</dependency>

そしてKuromojiSampleというクラスを作ってmainメソッドを作ってみたら、こんな処理をCopilotが作ってくれた。

public static void main(String[] args) {
    Tokenizer tokenizer = new Tokenizer();
    tokenizer.tokenize("すもももももももものうち").forEach(token -> {
        System.out.println(token.getSurface() + " " + token.getAllFeatures());
    });
}

getSurface()のところだけ別のメソッドになってたので修正して、そのまま実行。

すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも 名詞,一般,*,*,*,*,もも,モモ,モモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも 名詞,一般,*,*,*,*,もも,モモ,モモ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
うち 名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ

うまく動きました。

ということで圧縮きしだのHatenaのコードを書いていく。
ここでもCopilotがそれっぽいコードを書いてくれました。
void addSuccessorというメソッド名を入れると、このコードを生成してくれています。修正なし。

void addSuccessor(Morph m) {
    successor.compute(m, (k, v) -> v == null ? 1 : v + 1);
    ++successorCount;
}

Map.computeとかはよく覚えてないのでどのメソッドを使うべきかと悩む覚悟だったのだけど、なんの心配もなし。

頻度計算はこんな感じですが、だいたいこのまま生成してくれていました。

var current = START;
line = line.trim();
if (line.isEmpty() || line.charAt(0) < 0x80) return;
List<Token> tokens = tokenizer.tokenize(line);
for (var token : tokens) {
    // System.out.println(token.getSurface() + "\t" + token.getAllFeatures());
    var m = morphs.computeIfAbsent(token.getSurface(), s -> new Morph(s, token));
    current.addSuccessor(m);
    if (m.token.getAllFeatures().contains("句点")) {
        m.addSuccessor(START);
        current = START;
    } else {
        current = m;
    }
}
if (current != START) {
    current.addSuccessor(START);
}

ただ、これはそれなりに修正してます。けど、処理の骨格を書いてくれるのはとてもありがたい。たぶんこういうマルコフ過程を書く人が多いのだと思います。
ここでもcomputeIfAbsentだっけ?とか思ってたのを書きたいように書いてくれていました。

実際に文章を生成する部分、以前のIgoを使ったコードではnをどんどん増やしていましたが、ここでは減らしていくコードを生成してくれていますね。

int n = r.nextInt(current.successorCount);
for (var e : current.successor.entrySet()) {
    n -= e.getValue();
    if (n < 0) {
        current = e.getKey();
        break;
    }
}

そしてJFrameで画面を作ってこんな感じに。

なんとなくうまく動いてます。Igoを使ったときよりも文章がちゃんとできてる気がします。気のせいかもしれないけど。

ということで、どちらかというとGitHub Copilotがどういう動きするかちゃんとコードを書きながら試したかったのもあって、こういうコードを書いてみました。
この仕組みを超絶賢くしていけばChatGPTになるわけですね。

※追記 2023/4/8 ソース置いてなかった
https://gist.github.com/kishida/0899fed6c7b504f767ad2020f896ea40