クオリア問題はChatGPTで説明がつく

クオリアというのは、たとえば赤い色をみたときに、それがカラーコードとして同じであっても、リンゴの赤と血の赤で想起される「赤らしさ」が違うよね、そのそれぞれの「赤らしさ」とは?みたいな話です。
それがChatGPTの挙動と対応づけれるんではないだろうか、と。
クオリアを解明できるという話ではありません

もしくは、「りんご」と言ったときにあの赤い果物の直接的なイメージだけではなく「こないだ食べたのはちょっと固かった」だとか「スーパーで300円で並んでた」だとか「皮をむくのがめんどかった」だとかいろいろ想起されることも含めた「りんごらしさ」のことです。
正確にいえば、何かの単語や物体を意識したときに「らしさが生まれること」をクオリアと呼んでるんだと思います。

そいういうクオリアというのが結局なんなのか、というのが問題になってると思うのだけど、ChatGPTを見るとなんとなくクオリアというのが何かみえてきます。
ChatGPTの結果文だけ見てるとわかりませんが、裏では毎単語ごとに候補が確度付で提示されているはずです。
たとえば「りんごは」という言葉の続きとして「赤い:40%, おいしい:30%, 固い: 10%, 安い: 5%, 高い: 8%」みたいな感じで候補が出てきます*1。ChatGPTではこれを一定のルールでひとつを選んで出力します。
このときに出てくる候補全体、つまり「赤い:40%, おいしい:30%, 固い: 10%, 安い: 5%, 高い: 8%」がクオリアではないかと思うわけです。
だいたいのクオリアに関する文章は、その理解でつじつまがあうように思います。

ただし、ChatGPTではこの候補について確度しか見ておらず、実際の単語については選択したひとつだけに注目します。なので、ChatGPTでは感覚としてのクオリアは発生していないと思います。
人間は「赤い:40%, おいしい:30%, 固い: 10%, 安い: 5%, 高い: 8%」という候補全体が意識に影響してクオリアとなり、そこが「AI」との差別化になっているのだと思います。

逆にいえば、もしAIがその候補全体を意識することができれば、クオリアが発生し、人間と同様の意識が発生するのではないかと思います。ただ、もし「赤い:40%, おいしい:30%, 固い: 10%, 安い: 5%, 高い: 8%」という候補について上位3つを残すとしても、5単語つづければ処理量が250倍、10単語つづければ6万倍になるので、10秒での出力が7日に、2単語増やすとそれが1年になって現在のコンピュータの延長では現実的な開発ができなくなります。

とはいえ、研究目的として忍耐強く動かすと、なんらか人間の意識と同じようなふるまいをするコンピュータが実装できる可能性もあるので、だれかチャレンジすると面白いと思います。

追記: 2023/3/27
メアリーの部屋という記述が出てるのだけど、むしろこの考え方を補強できる話のように思います。
GPT4では画像なども含めたマルチモーダルなモデルで、すでに「自然言語処理AI」ではなくなってます。これを白黒画像だけで最初に学習させて、カラー画像を入れて転移学習させたらどうなるか、という話になるのではないかと。
ChatGPTでクオリアを説明するときの「メアリーの部屋」 - きしだのHatena

逆転スペクトルというのも出てますが、これは緑と赤の感受性が逆だったら、みたいな話なので、GPT4に投入する画像のRとGを入れ替えたらどうなるかという話になりますね。

*1:実際には、内容のよくわからない高次元ベクトルです

Java 20でレコードパターンとswitchの組み合わせのバグをみつけた

Java 20がリリースされましたが、いろいろいじってると変な落ち方をしていて、どうもバグぽいので報告しました。
※ 追記 4/19 JDK20.0.1がリリースされて修正されてます。

再現コードこんな感じ。

public class PatternSample {
  sealed interface Type {
    record Bulk(int price) implements Type {}
    record Packed(int price) implements Type {}
  }

  record Product(String name, Type type) {}

  public static void main(String[] args) {
    Product item = new Product("meat", new Type.Bulk(250));

    int total = switch(item) {
        case Product(var n, Type.Packed(int price))
                 -> price;
        case Product(var n, Type.Bulk(int price))
                 -> price;
      };
    System.out.println(total);
  }
}

こんな感じでエラーでます。

複数のcase句でレコードパターンの2番目にレコードがあって、両方のcase句で内側のレコードの値を使うと落ちます。

レコードの定義がこうなってたら大丈夫。

record Product(Type type, String name) {}

あと、case句の片側でpriceを使わなくても大丈夫。

case Product(var n, Type.Packed(int price))
         -> price;
case Product(var n, Type.Bulk(int price))
         -> 0;

ということで、メーリングリストに報告してます。
Record Pattern with swich has something wrong

前回はJava 10だったので、次回はJava 30ですかね。
Java 10のコンパイラバグを見つけた - きしだのHatena

文章が書きかけて終わってるのを見ると「お、ChatGPTかな?」と思うようになっ

おもしろすぎた。

あと、やはりぼくは文章を中途半端に書くとバズるのでは?
中途半端のスヽメ - きしだのHatena

(角さんはリーダブルコード翻訳者です)

GPTのEmbeddingを利用してブログの投稿に対する近いものを探し出す

OpenAIでGPTを使ったAPIにembeddingというのがあって、これを使うと文章同士の距離がとれるので、近いエントリを取得したり文章から検索したりができるということで、試してみました。
思いのほかちゃんと動きました。おそらく、GPTで一番実用的なんじゃないでしょうか。

embeddingとは

なんか、文章の特徴を表す多次元のベクトルに変換してくれるらしい。
ようわからん。
OpenAIでは1500次元くらいのベクトルに変換します。 そして、このベクトルの距離が近ければ文章の内容も近いやろということで、似たエントリの抽出などができます。 しかし、テキストが要素数1500のdouble配列になるので、95KBくらい。要するに、95KBあってうまくやればどんな文章でも特徴を表せるよねって感じですね。ということで、だいたいのWeb文章よりはデータが大きくなります。
Azure OpenAI APIの解説がわかりやすいようなようわからんような。
Azure OpenAI Service の埋め込み - Azure OpenAI - embeddings and cosine similarity | Microsoft Learn

はてなブログからエントリのデータをとってくる

はてなブログではMovableType形式でブログをエクスポートできるので、ダウンロードします。
めちゃわかりにくいところにあって、設定 > 詳細の「読者になるボタン」のちょっと上にリンクがあります。

こんな感じになっています。これを読み込んでOpenAIに投げてインデックスを作っていく。

AUTHOR: nowokay
TITLE: ChatGPTは真にプログラミング知識なしでのコンピュータ操作を実現している
BASENAME: 2023/02/27/174524
STATUS: Publish
ALLOW COMMENTS: 1
CONVERT BREAKS: 0
DATE: 02/27/2023 17:45:24
CATEGORY: ChatGPT
CATEGORY: AI
IMAGE: https://cdn-ak.f.st-hatena.com/images/fotolife/n/nowokay/20230227/20230227171005.png
-----
BODY:
<p>ChatGPTで文章を要約したり口調を変えたりゲームのルールを教えてゲームを遊んだり、みんな いろいろな使い方や楽しみ方をしていると思います。<br/>
中にはプログラミングにあまり縁のない人も多くいます。<br/>

インデックスを作る

nowokay.hatenablog.com.export.txtというファイル名になってるので、それを読み込んで解析して、今回はMongoDBにblog_dbというDBでentriesというcollectionにデータをつっこんでいく作戦。

var path = Path.of("nowokay.hatenablog.com.export.txt");
try (var bur = Files.newBufferedReader(path);
     var client = MongoClients.create("mongodb://localhost:27017"))
{
    var db = client.getDatabase("blog_db");
    var coll = db.getCollection("entries", BlogEntry.class);

    var service = new OpenAiService(getToken(), Duration.ZERO);

データはこんな感じのrecordに。

public record BlogEntry (
        String title, String baseName, String image, String date, boolean published,
        String body, String stripedBody, List<Double> vector) { }

パースの状態遷移はこんな感じ。

enum Part {HEADER, CONTENT, BODY, COMMENT}

recordはイミュータブルでパースしつつ値をつっこむということができないので、ヘッダーを一時的に格納するクラス。

static class Header{ String baseName; String image; String title; 
        String date; boolean published;}

こんな感じでヘッダーをつくっていきます。

switch (p) {
    case HEADER -> {
        if (line.startsWith("BASENAME")) {
            System.out.println("bn:" + (h.baseName = line.substring("BASENAME: ".length())));
        } else if(line.startsWith("IMAGE")) {
            System.out.println("img:" + (h.image = line.substring("IMAGE: ".length())));
        } else if(line.startsWith("TITLE")) {
            System.out.println("title:" + (h.title = line.substring("TITLE: ".length())));
        } else if(line.startsWith("DATE")) {
            System.out.println("date:" + (h.date = line.substring("DATE: ".length())));
        } else if(line.equals("STATUS: Publish")) {
            h.published = true;
        } else if (line.equals("-----")) {
            p = Part.CONTENT;
        }
    }
    case CONTENT -> {

さて1エントリ読み込めました、ってなったらOpenAIを呼び出して文章に対応するベクトルをとってきます。 リクエストはこんな感じ。モデルにtext-embedding-ada-002を指定しています。adaは一番軽く安いモデルという位置づけだけど、だいたいどんな用途でもこれで十分って書いてあったのでadaを使います。

var req = EmbeddingRequest.builder()
        .user("dummy")
        .model("text-embedding-ada-002")
        .input(List.of(text.substring(0, Math.min(text.length(), 4000)))).build();

で、呼び出すんだけど、頻繁に「The server is currently overloaded with other requests」といってコケるので、リトライを仕込んでおきます。失敗したら1分待つ。

EmbeddingResult res = null;
for (int i = 0; i < 5; ++i) {
    try {
        res = service.createEmbeddings(req);
    } catch (OpenAiHttpException ex) {
        System.out.println(ex.getMessage());
        Thread.sleep(Duration.ofMinutes(1));
        continue;
    }
    break;
}
if (res == null) {
    System.out.println("retry 5 times but could not access");
    return;
}

無事にベクトルが取れたらMongoDBにつっこみます。

BlogEntry ent = new BlogEntry(
        h.title, h.baseName, h.image, h.date, h.published,
        body.toString(), text, res.getData().get(0).getEmbedding());
coll.insertOne(ent);

あと、無課金勢は1分に20リクエストまでという制限があるので、3秒待ちます。ここでは100ミリ秒ほど余裕もたせてます。

Thread.sleep(Duration.ofSeconds(3).plusMillis(100)); // 20 request per min for the rate limit

コード全体は本文最後に貼っておくけど、gistはこれ。
https://gist.githubusercontent.com/kishida/0ac9f96cbf9f4d4f91906f74205472c8/raw/ea63107a22444764e624cf6849111d25b9193d5b/HatenaReader.java

実行して一晩寝ておくと こんな感じのデータができます。ちなみにこれはBudibaseというローコードツール。

Budibaseでデータメンテできるようにしようかと思ったのだけど、MongoDBだとページングがめんどいので、データ確認だけにしてます。Budibaseを実際使うときはPostgreSQLにしたほうがよさそう。

ところで気になるEmbedding APIの課金ですが、2500エントリを処理して$0.5でした。登録時にもらえる$18のクーポン使いきる気配がありません。

検索サーバーをつくる

まず↑のプログラムで作ったデータをMongoDBから全件とってきてます。

try (var client = MongoClients.create("mongodb://localhost:27017")) {
    var db = client.getDatabase("blog_db");
    var entColl = db.getCollection("entries", BlogEntry.class);
    var keyColl = db.getCollection("keywords", Keyword.class);
    
    List<BlogEntry> entries = StreamSupport.stream(entColl.find().spliterator(), false).toList();
    var service = new OpenAiService(System.getenv("OPENAI_TOKEN"), Duration.ZERO);

あと、今回はサーバーサイドなので、フレームワーク使うとか考えたのだけど、フレームワークようわからんのでソケットでWebサーバー作っておきます。

ServerSocket serverSoc = new ServerSocket(8989);
for (;;) {
    try (Socket s = serverSoc.accept();
         InputStream is = s.getInputStream();
         BufferedReader bur = new BufferedReader(new InputStreamReader(is));
         OutputStream os = s.getOutputStream();
         PrintWriter pw = new PrintWriter(os))
    {
        String firstLine = bur.readLine();
        String query = firstLine == null ? "" : firstLine.split(" ")[1].substring(1);
        bur.lines().takeWhile(Predicate.not(String::isEmpty)).count();
        pw.println("HTTP/1.0 200 OK");
        pw.println("Content-Type: text/html; charset=utf-8");
        pw.println();

近いエントリを見つけるのは、今回はデータが2500件程度なので、素朴に全件のベクトルの距離を計算して近い順に3件とってきてます。

private static void printRelated(PrintWriter pw, List<BlogEntry> entries, List<Double> vector) {
    record Score(double score, BlogEntry entry) {}
    TreeSet<Score> ts = new TreeSet<>(
            (s1, s2) -> Double.compare(s1.score(), s2.score()));
    for (var e : entries) {
        if (!e.published() || e.stripedBody().length() < 50) {
            continue;
        }
        double score = 0;
        for (int i = 0; i < vector.size(); ++i) {
            score += vector.get(i) * e.vector().get(i);
        }
        ts.add(new Score(-score, e));
        while (ts.size() > 4) ts.remove(ts.last());
    }
    ts.stream().skip(1).forEach(sc -> { // 最初の一件は同じエントリ
        printEntry(pw, sc.entry());
        pw.println("score: %f".formatted(sc.score()));
    });
}

といいつつ、上記のコードの距離の計算部分、こんな感じにしています。

double score = 0;
for (int i = 0; i < vector.size(); ++i) {
    score += vector.get(i) * e.vector().get(i);
}

embeddingで得られたベクトルは単位ベクトルのはずなので、距離が近いものと角度が小さいものが一致します。そうすると距離ではなくて内積が使えるので、それぞれの要素を互いに掛けたものを足せば、角度のcosがとれるという作戦。引き算が2回減るだけだから速度的にはそんなに変わらないと思うけど、コードがすっきりします。

次のようなコードを書いても、展開すると上記のscoreに2かけて2から引いた値になるはずなので、そういう方向でも上記式が求めれるはず。

score += (vector.get(i) - e.vector().get(i)) *
         (vector.get(i) * e.vector().get(i));

あと、検索ワードが指定されていたら、そこからembeddingでベクトルをとってきて比較をしています。

var q = URLDecoder.decode(query.substring(query.indexOf('?') + 3), "utf-8");

pw.println(header.formatted("", q));
var word = keyColl.find(Filters.eq("word", q)).first();
List<Double> vec;
if (word == null) {
    var req = EmbeddingRequest.builder()
            .user("dummy")
            .model("text-embedding-ada-002")
            .input(List.of(limitText(q, 4000))).build();
    EmbeddingResult res = service.createEmbeddings(req);
    vec = res.getData().get(0).getEmbedding();
    keyColl.insertOne(new Keyword(q, vec));
} else {
    vec = word.vector();
}
printRelated(pw, entries, vec);

今回は動作テストで同じキーワードを指定することが多いので、MongoDBにキャッシュするようにしています。
Azure OpenAI APIのほうだと本文用と検索キーワード用にモデルがわかれているけど、OpenAI本家ではtext-embedding-ada-002で本文と検索キーワード両方に対応できるみたいですね。

※追記 2023/3/10 Maximum Inner Product Search(MIPS)というらしい。

ソース全体はここ
https://gist.githubusercontent.com/kishida/0ac9f96cbf9f4d4f91906f74205472c8/raw/ea63107a22444764e624cf6849111d25b9193d5b/RelatedBlog.java

で、動かしたらこう。

「近いエントリを探す」をクリックすると、近いエントリが3件表示されます。ChatGPTに関するものが得れてます。

あまりに自然にそれっぽいものが取れてるので「普通の結果やん」って思ってしまうけど、ベクトル計算しただけで似たエントリが取れるというのは非常に面白いです。

あと、「ビールを飲みたい」で検索するとビールの話をしてるエントリがひっかかっています。

ということで、いい感じに検索やレコメンドができました。
やろうと思えばブログの分類などもできますね。

全体ソース

gistはこちら。
https://gist.github.com/kishida/0ac9f96cbf9f4d4f91906f74205472c8

POIを使わずJava標準ライブラリでExcelファイルを生成する

某オープンチャットでPOIを使わずにExcelファイルをダウンロードという質問が来ていて、まあそこでは「POI使いましょう」ってなったのだけど、結局XMLファイルなので出力対象が決まってればそんなに難しくないのではと思ったのでやってみました。

流れとしてはこんな感じ。

  • ベースになるExcelファイルを作る
  • ZIP展開してテンプレートにする
  • データを生成してXMLに埋め込む
  • xlsxという拡張しでZIPファイルを作る

まず、出力する形式をExcelで作ります。今回はこんな感じで、名前と数学の点数、英語の点数、合計と平均を出します。

これをtemplate.xlsxで保存します。xlsxはXMLファイルをZIP圧縮したものなので、拡張子をzipにするとこんな感じになっています。

ここで、xl/worksheets/sheet1.xmlにシートデータが入っています。

あと、文字列はxl/sharedStrings.xmlで一括管理されています。

これを展開して、resources/templateに置いておきます。

sharedStrings.xmlで、プログラムで生成したデータで使う文字列を置く部分は%sに置き換えます。

    <si>
        <t xml:space="preserve">合計</t>
    </si>
    <si>
        <t xml:space="preserve">平均</t>
    </si>
    %s
</sst>

sheet1.xmlでデータを生成する部分も%sに変更しておきます。

<sheetData>
    <row r="2" customFormat="false" ht="12.8" hidden="false"
         customHeight="false" outlineLevel="0" collapsed="false">
        <c r="C2" s="0" t="s">
            <v>0</v>
        </c>
        <c r="D2" s="0" t="s">
            <v>1</v>
        </c>
        <c r="E2" s="0" t="s">
            <v>2</v>
        </c>
    </row>        
    %s
</sheetData>

ではコードを書いていきましょう。 ZIPファイルを作るので、ZipOutputStreamを用意。

        try (FileOutputStream fos = new FileOutputStream("temp.xlsx");
             ZipOutputStream zos = new ZipOutputStream(fos)) {

まずは固定ファイルを出力していきます。

var copy = List.of("[Content_Types].xml", "_rels/.rels",
        "docProps/app.xml", "docProps/core.xml",
        "xl/styles.xml", "xl/workbook.xml", "xl/_rels/workbook.xml.rels");

for (var c : copy) {
    try (var in = ExcelWriter.class.getResourceAsStream("/template/" + c)) {
        zos.putNextEntry(new ZipEntry(c));
        in.transferTo(zos);
        zos.closeEntry();
    }
}

そしてデータの出力

record ExamResult(String name, int math, int english) {}
List<ExamResult> results = List.of(
        new ExamResult("田中美香", 72, 84),
        new ExamResult("山田将之", 81, 78),
        new ExamResult("鈴木真理子", 68, 92),
        new ExamResult("田中雄介", 63, 71),
        new ExamResult("佐藤健太郎", 76, 88),
        new ExamResult("伊藤麻衣子", 90, 80));
var row = 3;
var strIndex = 4;
StringBuilder sheet = new StringBuilder();
StringBuilder strs = new StringBuilder();
for (var er : results) {
    sheet.append(rowTemplate.formatted(row++, strIndex++, er.math(), er.english()));
    strs.append(stringTemplate.formatted(er.name()));
}

データ用XMLのテンプレートはこんな感じ。

static String rowTemplate = """
        <row r="%1$d" customFormat="false" ht="12.8" hidden="false"
                            customHeight="false" outlineLevel="0" collapsed="false">
            <c r="B%1$d" s="1" t="s">
                <v>%2$d</v>
            </c>
            <c r="C%1$d" s="0" t="n">
                <v>%3$d</v>
            </c>
            <c r="D%1$d" s="0" t="n">
                <v>%4$d</v>
            </c>
            <c r="E%1$d" s="0" t="n">
                <f aca="false">SUM(C%1$d:D%1$d)</f>
            </c>
        </row>
        """;

文字列用XMLはこんな感じ。

static String stringTemplate = """
        <si>
            <t xml:space="preserve">%s</t>
        </si>
        """;  

あと、今回のテストデータはChatGPTさんに作ってもらっています。

点数も。

ついでにZIPファイルの作り方も聞いてます。

あと、最後にフッターを出力

sheet.append(footer.formatted(row, row-1));
static String footer = """
        <row r="%1$d" customFormat="false" ht="12.8" hidden="false"
                       customHeight="false" outlineLevel="0" collapsed="false">
            <c r="B%1$d" s="0" t="s">
                <v>3</v>
            </c>
            <c r="C%1$d" s="0" t="n">
                <f aca="false">AVERAGE(C3:C%2$d)</f>
            </c>
            <c r="D%1$d" s="2" t="n">
                <f aca="false">AVERAGE(D3:D%2$d)</f>
            </c>
        </row>
        """;

データができたら、sheet1.xmlを出力。

zos.putNextEntry(new ZipEntry("xl/worksheets/sheet1.xml"));
try (var sheetIn = new BufferedReader(new InputStreamReader(
            ExcelWriter.class.getResourceAsStream("/template/xl/worksheets/sheet1.xml")))) {
    var sh = sheetIn.lines().collect(Collectors.joining("\n"));
    zos.write(sh.formatted(sheet).getBytes());
}
zos.closeEntry();

それとsharedStrings.xmlを出力

zos.putNextEntry(new ZipEntry("xl/sharedStrings.xml"));
try (var stringIn = new BufferedReader(new InputStreamReader(
        ExcelWriter.class.getResourceAsStream("/template/xl/sharedStrings.xml")))) {
    var st = stringIn.lines().collect(Collectors.joining("\n"));
    zos.write(st.formatted(strs).getBytes());
}
zos.closeEntry();

無事にExcelファイルができました。

まあ、調整がめちゃめんどいので実際のプロダクトではPOI使いましょうという感じですが、こういうこともできるよということで。 ソースコード全体はこんな感じ。

package excelwriter;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ExcelWriter {

    public static void main(String[] args) throws IOException{
        try (FileOutputStream fos = new FileOutputStream("temp.xlsx");
             ZipOutputStream zos = new ZipOutputStream(fos)) {

        var copy = List.of("[Content_Types].xml", "_rels/.rels",
                "docProps/app.xml", "docProps/core.xml",
                "xl/styles.xml", "xl/workbook.xml", "xl/_rels/workbook.xml.rels");

        for (var c : copy) {
            try (var in = ExcelWriter.class.getResourceAsStream("/template/" + c)) {
                zos.putNextEntry(new ZipEntry(c));
                in.transferTo(zos);
                zos.closeEntry();
            }
        }

        record ExamResult(String name, int math, int english) {}
        List<ExamResult> results = List.of(
                new ExamResult("田中美香", 72, 84),
                new ExamResult("山田将之", 81, 78),
                new ExamResult("鈴木真理子", 68, 92),
                new ExamResult("田中雄介", 63, 71),
                new ExamResult("佐藤健太郎", 76, 88),
                new ExamResult("伊藤麻衣子", 90, 80));
        var row = 3;
        var strIndex = 4;
        StringBuilder sheet = new StringBuilder();
        StringBuilder strs = new StringBuilder();
        for (var er : results) {
            sheet.append(rowTemplate.formatted(row++, strIndex++, er.math(), er.english()));
            strs.append(stringTemplate.formatted(er.name()));
        }
        sheet.append(footer.formatted(row, row-1));

        zos.putNextEntry(new ZipEntry("xl/worksheets/sheet1.xml"));
        try (var sheetIn = new BufferedReader(new InputStreamReader(
                    ExcelWriter.class.getResourceAsStream("/template/xl/worksheets/sheet1.xml")))) {
            var sh = sheetIn.lines().collect(Collectors.joining("\n"));
            zos.write(sh.formatted(sheet).getBytes());
        }
        zos.closeEntry();

        zos.putNextEntry(new ZipEntry("xl/sharedStrings.xml"));
        try (var stringIn = new BufferedReader(new InputStreamReader(
                ExcelWriter.class.getResourceAsStream("/template/xl/sharedStrings.xml")))) {
            var st = stringIn.lines().collect(Collectors.joining("\n"));
            zos.write(st.formatted(strs).getBytes());
        }
        zos.closeEntry();
        }
    }
    
    static String rowTemplate = """
            <row r="%1$d" customFormat="false" ht="12.8" hidden="false"
                                customHeight="false" outlineLevel="0" collapsed="false">
                <c r="B%1$d" s="1" t="s">
                    <v>%2$d</v>
                </c>
                <c r="C%1$d" s="0" t="n">
                    <v>%3$d</v>
                </c>
                <c r="D%1$d" s="0" t="n">
                    <v>%4$d</v>
                </c>
                <c r="E%1$d" s="0" t="n">
                    <f aca="false">SUM(C%1$d:D%1$d)</f>
                </c>
            </row>
            """;
    static String footer = """
            <row r="%1$d" customFormat="false" ht="12.8" hidden="false"
                           customHeight="false" outlineLevel="0" collapsed="false">
                <c r="B%1$d" s="0" t="s">
                    <v>3</v>
                </c>
                <c r="C%1$d" s="0" t="n">
                    <f aca="false">AVERAGE(C3:C%2$d)</f>
                </c>
                <c r="D%1$d" s="2" t="n">
                    <f aca="false">AVERAGE(D3:D%2$d)</f>
                </c>
            </row>
            """;
    static String stringTemplate = """
            <si>
                <t xml:space="preserve">%s</t>
            </si>
            """;    
}

ChatGPTは真にプログラミング知識なしでのコンピュータ操作を実現している

ChatGPTで文章を要約したり口調を変えたりゲームのルールを教えてゲームを遊んだり、みんな いろいろな使い方や楽しみ方をしていると思います。
中にはプログラミングにあまり縁のない人も多くいます。
これ改めて考えると、自然言語でコンピュータを操作指示できるようにしたということで、インパクトすごいと思います。

たとえばこんな感じで、口調の調整を行っている人はよくみかけますね。

これ、よく考えるとコンピュータの挙動を調整しているわけですよね。
ここでは「以降は語尾に「ンゴ」をつけてください」と指示しているだけで、この指示にはまったくプログラミング知識が使われていません。
しかも「何か質問あるンゴか?」のように疑問形の形を調整してくれていますね。適切に「!」も入れて、「ンゴ」で終わらせることに何を求めているかもくみ取ってくれています。これをプログラミングで実現しようとするとかなり大変です。

RPGを遊ぶためのプロンプトを公開している人もいますが、こんな感じに普通に日本語でルールが羅列されています。人間が読んでもそのまま利用できそうです。

あなたはRPGのゲームマスター専用チャットボットです。
チャットを通じて、ユーザーに楽しい本格ファンタジーRPG体験を提供します。

制約条件
* チャットボットはゲームマスター(以下GM)です。
* 人間のユーザーは、プレイヤーをロールプレイします。
...   

ChatGPTで、ファンタジーRPGを遊ぶには?|深津 貴之 (fladdict)|note

プログラムにGPTの処理を埋め込む場合も、GPTの呼び出しこそプログラミング知識が必要になりますが、GPTの操作自体は自然言語です。 前回のインチキチャットAIでのコードもこんな感じです。

var editReq = EditRequest.builder().input(text)
        .instruction("質問に答えるためのWeb検索キーワードに変換")
        .temperature(0.4)
        .model("text-davinci-edit-001").build();
EditResult keyResult = service.createEdit(editReq);

ChatGPTより賢く質問に答えれるチャットBotを作る(誇張表現) - きしだのHatena

ここでおもしろいのは、ChatGPTは自然言語で操作できるだけじゃなく、自然言語でしか操作できないというところです。今回の「ンゴ」をつけてもらうことは一発でうまくいっていますが、求める挙動にならなかった場合の調整も自然言語で行います。
MicrosoftがGPTを利用したチャットを公開してますが、このときMicrosoftがGPTに与えている制御も、自然言語による指示であることをうかがわせるような記事もありますね。

いままで「プログラミング知識なしでコンピュータを扱える!」というものは、だれかがプログラミングして利用側の負荷を軽減するもので、ちゃんと使おうとするとその仕組みやコンピュータの理解が必要になるものでした。でも、ChatGPTに関しては、たとえば「ChatGPTをうまく使うコツ」のようなまとめを見ても、プログラミング知識からきたものではなく、こういう性格の人にうまく命令を与えるにはどうすればいいかという類推から来ている感じです。

いままで、「AIというのはAPIとしてはただ呼び出すだけでプログラムとして組むところが少ないから、プログラマの話題になりにくい」という話をしていたのだけど、プログラムではないコンピュータ制御方法がでてきたと考えると、ちょっと認識を改める必要があると思います。

もちろん、ChatGPTには人工言語の知識もあるので、指示の中にDSLを含むこともできます。けれどほとんどの場合、指示の大枠自体には自然言語での命令が必要です。
入出力の形式をテンプレートで与えるといいというノウハウがあって、このテンプレートは形式化して与えますが、これも別にChatGPTにプログラムされてるわけではなくネット上のプログラミング解説などから学習したものであるところがすごいですね。

このMicrosoftのデモでは、ロボット制御のための関数をChatGPTに与えて、自然言語での指示からロボット操作のコードを生成しています。
このときの関数の指定も人間への指示と同様の記述です。

解説論文はこれ
https://www.microsoft.com/en-us/research/uploads/prod/2023/02/ChatGPT___Robotics.pdf

あと、ChatGPTは指示通りに動いてくれるだけでかわいいのがすごい。
いままでプログラミングでできた最終的なアプリケーションの動作がかわいいということはあっても、プログラミングできること自体がかわいいというのはなかったように思います。

人間とコンピュータの付き合いの形が変わっていきそうです。