LLMs and Programming in the first days of 2024

Salvatore Sanfilippo

2024年初頭のLLMとプログラミング

原文は Salvatore Sanfilippo により に公開されました。 このブログを購読する

まず断っておきたいのは、この記事はLLMを振り返るためのものではないということだ。2023年が人工知能にとって特別な年だったのは明らかで、それを改めて繰り返すことにあまり意味はない。むしろこの記事は、一人のプログラマーとしての証言である。ChatGPTの登場以来、そして後にローカルで動作するLLMを使うようになってから、私はこの新しい技術を徹底的に活用してきた。目的はコードを書く能力を加速させることだが、それだけではない。労力をかける価値のないプログラミングの側面に、精神的なエネルギーを浪費しないようにするという意図もある。知的に面白くもない細かな仕様のドキュメントを探して費やす無数の時間、しばしば正当な理由もなく複雑に作られたAPIを学ぶための努力、数時間後には捨ててしまうような、その場限りのプログラムを書くこと。そういったことは、もうやりたくない。とりわけ今やGoogleが、わずかな有用な情報を探すためのスパムの海と化してしまった中ではなおさらだ。

一方で、私は決してプログラミングの初心者ではない。何の支援もなしにコードを書くことができるし、実際にそうすることも多い。次第に、高水準のコード、特にPythonではLLMを使うことが増え、Cではずっと控えめになっている。LLMに関する私個人の経験で印象的なのは、いつ使うべきか、そしていつ使えばかえって足を引っ張るだけになるかを、正確に学んだということだ。また、LLMはWikipediaやYouTubeに散らばる動画講座と少し似ていることも学んだ。意欲と能力と規律のある者の助けにはなるが、遅れを取っている者にとっては得られる恩恵はわずかだ。少なくとも当初は、すでに優位にある者だけが恩恵を受けることになるのではないかと危惧している。

順を追って見ていこう。

全知か、オウムか

この機械学習における目新しさと進歩の新たな波の中で、最も懸念すべき現象の一つは、AIの専門家たちが自らの知識の限界を認められないことだ。ホモ・サピエンスはニューラルネットワークを発明し、さらに決定的に重要なこととして、ニューラルネットワークのパラメータを自動的に最適化するアルゴリズムを発明した。ハードウェアはますます大規模なモデルを学習できるようになり、処理すべきデータに関する統計的知識(事前分布)と、逐次近似のための膨大な試行錯誤を通じて、他よりもうまく機能するアーキテクチャが発見されてきた。しかし結局のところ、ニューラルネットワークは依然としてかなり不透明なままだ。

LLMのある種の創発的な能力を説明できないという事態に直面して、科学者たちにはもっと慎重な姿勢が期待されたはずだ。しかし実際には、多くの人がLLMを著しく過小評価し、結局のところ多少進歩したマルコフ連鎖に過ぎず、せいぜい学習データで見たものの極めて限られたバリエーションを吐き戻すだけだと言った。そして、この「オウム」という見方は、証拠を前にしてほぼ全面的に撤回された。

同時に、熱狂する大衆の多くは、実際には存在しない超自然的な力をLLMに帰属させた。残念ながら、LLMはせいぜい、学習時に見たデータが表す空間の中で補間できるに過ぎない。そして、それだけでもすでに大したことなのだ。実際には、その補間能力は限られている(とはいえ驚異的であり、予想外でもある)。ああ、もし今日の最大のLLMが、見てきたすべてのコードによって区切られた空間の中で連続的に補間できたなら! 真の新規性を生み出せなかったとしても、99%のプログラマーを代替できるだろう。現実は、いつものようにもっと控えめだ。LLMは確かに、そのままの形では見たことのないプログラムを書くことができ、学習データの中に一定の頻度で現れた異なるアイデアを混ぜ合わせる能力を示す。同時に、この能力には現時点では深い限界があり、繊細な推論が求められる場面ではLLMは悲惨なまでに失敗することも明らかだ。それでも、LLMはAIが誕生して以来、今日に至るまでの最大の成果である。これは否定し難いように思える。

愚かだが博識

確かに、LLMにできる推論はせいぜい初歩的なもので、しばしば不正確で、存在しない事実についての幻覚(ハルシネーション)に満ちていることも多い。しかし、彼らは膨大な知識を持っている。プログラミングの分野でも、質の高いデータが利用可能な他の分野でも、LLMはたくさんのことを知っている愚かな博識家のようなものだ。そんな相手とペアプログラミングをするのは悲惨だろう(私にとっては、そもそもペアプログラミング自体が一般的に悲惨なのだが)。彼らは支離滅裂なアイデアを出し、私たちは自分の考えを通すために絶えず戦わなければならなくなる。しかし、この物知りの愚か者がこちらの自由になり、投げかけたあらゆる質問に答えてくれるのであれば、話は変わる。現在のLLMは私たちを既知の道の先へ連れて行ってはくれないが、あまりよく知らないトピックに取り組もうとするとき、完全な無知の状態から、自力で先に進めるだけの知識がある地点まで引き上げてくれることが多い。

プログラミングの分野では、おそらく20年か30年前までなら、彼らの能力はさほど興味を引かなかっただろう。当時は2つほどのプログラミング言語と古典的なアルゴリズム、そして10個ほどの基本的なライブラリを知っていればよかった。残りは自分自身で付け加えるものだった。自分の知性、専門性、設計能力を。そうした要素を持っていれば、ほぼ何でもこなせる熟練プログラマーだった。時が経つにつれ、フレームワーク、プログラミング言語、あらゆる種類のライブラリが爆発的に増えるのを目の当たりにしてきた。しばしば完全に不要で正当化もできない複雑さの爆発だが、現実は現実としてある。そして、そうした状況においては何でも知っている愚か者は貴重な味方となる。

一例を挙げよう。私は機械学習の実験を少なくとも1年間はKerasを使って進めていた。それから様々な理由でPyTorchに切り替えた。embeddingや残差ネットワークが何かはすでに知っていたが、PyTorchのドキュメントを一から順に学ぶ気にはなれなかった(KerasはChatGPTがまだ存在しない頃に学んだので、そうやって覚えたのだが)。LLMがあれば、Torchを使ったPythonコードを書くのはとても簡単だった。組み立てたいモデルについて明確なイメージを持ち、正しい質問をすればよかったのだ。

実例を見てみよう

私が言っているのは、「ねえ、YをやるためのクラスXのメソッドは何?」といった簡単なことではない。もしそれだけのためなら、LLMに懐疑的な人々に同意したくもなるだろう。より高性能なモデルにできることは、はるかに洗練されている。数年前までなら、それはまさに魔法だった。私はGPT-4にこう言える。「見てくれ、これがPyTorchで実装したニューラルネットワークのモデルだ。これが私のバッチだ。バッチを出力する関数がニューラルネットワークの入力と互換性を持つようにテンソルをリサイズしたいんだが、こういう形で表現したい。リシェイプに必要なコードを示してくれないか?」 GPT-4がコードを書いてくれる。私がやるべきことは、PythonのCLIでテンソルが本当に自分にとって有用な次元を持っているか、データのレイアウトが正しいかをテストすることだけだった。

別の例を挙げよう。しばらく前、ESP32ベースのあるデバイス向けにBLEクライアントを実装しなければならなかった。調べた結果、クロスプラットフォームのBluetoothプログラミング用バインディングは、どれも多かれ少なかれ使い物にならないと分かった。解決策はシンプルだった。macOSのネイティブAPIを使ってObjective-Cでコードを書くことだ。こうして私は同時に二つの問題を抱えることになった。Objective-Cの煩雑なBLE APIを学ぶこと――私から見ればナンセンスなパターンだらけだ(私はミニマリストで、ああいうAPIは私が考える「良い設計」とは対極にある)――と、同時にObjective-Cでのプログラミング自体を思い出すことだ。最後にObjective-Cでプログラムを書いたのは10年前で、イベントループやメモリ管理などの詳細はすっかり忘れていた。

最終的な成果はこちらのコードだ。決して美しいとは言えないが、やるべきことは果たす。私はこれを極めて短時間で書き上げた。そうでなければ不可能だっただろう。

https://github.com/antirez/freakwan/blob/main/osx-bte-cli/SerialBTE.m

このコードは、やりたいことのうち、やり方がよく分からずうまく動かなかった部分をChatGPTにコピー&ペーストして、LLMに何が問題でどう解決すべきかを説明させる、ということを繰り返してほぼ書かれた。LLM自体がそのコードの多くを書いたわけではないのは確かだが、執筆を大幅に加速させたのも事実だ。ChatGPTなしでできたか? もちろんできた。しかし最も興味深いのは、時間がかかっただろうという点ではない。真実は、そもそも試みすらしなかっただろうということだ。割に合わなかったからだ。この事実は極めて重要だ。私のプロジェクトにとって二次的なものである、そうしたプログラムを書く際の労力と利益の比率は、見合わないものだった。さらに、この作業はプログラム自体よりもはるかに有用な副次的効果をもたらした。そのプロジェクトのために、私はlinenoise(私の行編集ライブラリの一つ)を多重化で動作するように改良したのだ。

もう一つの例は、今回はコードを書くというよりデータの解釈に関するものだ。ネットで見つけた畳み込みニューラルネットワークを使ってPythonスクリプトを組み立てたかったのだが、ドキュメントがかなり不足していた。そのネットワークはONNX形式だったのが幸いで、入力と出力のリストや、それらに割り当てられた名前を簡単に抽出できた。このconvnetについて私が知っていたのはただ一つ、画像内の特定の特徴を検出するということだけだった。入力画像の形式やサイズは分からず、とりわけネットワークの出力は想像していたよりはるかに複雑だった(二値分類器だと思っていた。観測した画像は正常か、何か問題があるか、という二つの出力があるのだろうと。ところが出力は数百もあったのだ)。私はまずONNXネットワークのメタデータ出力をChatGPTにコピー&ペーストした。そしてアシスタントに、ネットワークについて知っているわずかなことを説明した。ChatGPTは入力がどのように構成されているかを推測し、出力はおそらく潜在的な欠陥に対応する画像の部分を示す正規化されたボックスと、それらの欠陥の尤度を示す別の出力だろうと仮説を立てた。数分のやり取りの後、ネットワークの推論が可能なPythonスクリプトに加え、元の画像を入力に適したテンソルに変換するのに必要なコードなどが手に入った。そのセッションで印象的だったのは、テスト画像に対する生の出力値(基本的にはlogits)を観察したことで、ChatGPTがついにネットワークがどのように機能するかを「理解」したことだった。一連の浮動小数点数が、正確な出力の詳細、正規化、ボックスが中心基準なのか左上隅の指定なのかといったことを特定するためのコンテキストを提供したのだ。

使い捨てのプログラム

上で語ったような事例を、何十件でも記録することはできるだろう。しかし無意味だ。多少の違いはあれ、同じ話の繰り返しだからだ。問題を抱えていて、LLMがでたらめを言っていないか*検証できる*何かを素早く知る必要がある。まあ、そうしたケースでは、知識への欲求を加速させるためにLLMを使うのだ。

一方で、LLMにすべてのコードを書かせる別のケースもある。例えば、多少なりとも使い捨てのプログラムを書く必要があるときだ。例えばこんなものだ。

https://github.com/antirez/simple-language-model/blob/main/plot.py

小さなニューラルネットワークの学習中に損失曲線を可視化する必要があった。PyTorchのプログラムが学習中に生成するCSVファイルの形式をGPT-4に見せ、コマンドラインで複数のCSVファイルを指定した場合は、同じ実験の学習損失と検証損失の曲線ではなく、異なる実験の検証損失曲線同士を比較したいと頼んだ。上記がGPT-4によって生成された結果だ。合計30秒のことだった。

同様に、AirBnBのCSVレポートを読み込み、私のアパートを月と年ごとにグループ化するプログラムが必要だった。その上で、清掃費用と予約ごとの宿泊数を考慮して、年間の月ごとの平均賃料について統計を取るものだ。このプログラムは私にとって極めて有用だ。同時に、これを書くのは死ぬほど退屈で、何も面白いことはない。そこでCSVファイルの一部を抜き出してGPT-4にコピー&ペーストし、LLMに解くべき問題を書いた。プログラムは一発で動いた。以下に全文を示す。

import pandas as pd
pd.set_option('display.max_rows', None)
df = pd.read_csv('listings.csv')
reservations = df[df['Type'] == 'Reservation']
reservations['Start Date'] = pd.to_datetime(reservations['Start Date'])
reservations['Year'] = reservations['Start Date'].dt.year
reservations['Month'] = reservations['Start Date'].dt.month
reservations['Nightly Rate'] = (reservations['Amount'] - reservations['Cleaning Fee']) / reservations['Nights']
all_listings = reservations['Listing'].unique()
all_years = reservations['Year'].unique()
all_months = range(1, 13)
index = pd.MultiIndex.from_product([all_listings, all_years, all_months], names=['Listing', 'Year', 'Month'])
all_data = pd.DataFrame(index=index).reset_index()
merged_data = pd.merge(all_data, reservations, on=['Listing', 'Year', 'Month'], how='left')
average_nightly_rates = merged_data.groupby(['Listing', 'Year', 'Month'])['Nightly Rate'].mean().fillna(0)
print(average_nightly_rates)

そうでなければかなり散在し無秩序なデータを、正確にどうグループ化するかを理解するには、単純な推論が必要だ。それはLLMが見てきた言語に近似するものをただ吐き出すだけで解決できるような問題ではない。GPT-4は学習中に類似のプログラムを確実に見てきただろうが、私が求めた通りの正確な方法で、しかもその特定の形式のCSVファイルにおいてグループ化しなければならないプログラムを見たことは、ほぼ確実にない。これは(私見では)LLMが学習データ内の異なるプログラムによって描かれた空間の中で、ある程度補間できることを示している。

この種のプログラムを書くことは、私見では時間の不賢明な使い方だろう。LLMが代わりにやってくれるという事実は、コードを生み出す能力をかなり直接的に加速させてくれる。重要なことに集中できるのだ。

うまくいかないとき:システムプログラミング

LLMとプログラミングでの成功にもかかわらず、Cでプログラムを書くときは、結局LLMをより便利なドキュメントの形としてしか使わないことがほとんどだ。私はシステムプログラミングの専門家だが、その文脈でLLMが役に立つのは、複雑な推論が可能な場合に限られる。そうしたシナリオでは、彼らはほぼ常に失敗する。

この実験のプロンプトから始めよう。

ブルームフィルタの、きちんとした、小さいながらも動作するC実装の要件を概説してください。ハッシュ関数の重要な側面に焦点を当ててください。その後、品質の高いCで記述してください。

また、実装例は10万件の要素を格納できるサイズとし、偽陽性確率は最大5%としてください。追加される要素はヌル終端文字列です。

出力は次の通りだ。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#define N 100000  // Number of elements
#define P 0.05    // False positive probability
#define M (int)(-(N * log(P)) / (log(2) * log(2)))  // Size of bit array
#define K (int)(M / N * log(2))  // Number of hash functions

unsigned char bitArray[M / 8 + 1];

void setBit(int n) {
    bitArray[n / 8] |= 1 << (n % 8);
}

int getBit(int n) {
    return bitArray[n / 8] & (1 << (n % 8));
}

// Basic hash functions (for demonstration purposes)
int hash1(char *str) {
    int hash = 5381;
    int c;
    while ((c = *str++))
        hash = ((hash << 5) + hash) + c;
    return abs(hash % M);
}

int hash2(char *str) {
    unsigned long hash = 0;
    int c;
    while ((c = *str++))
        hash = c + (hash << 6) + (hash << 16) - hash;
    return abs(hash % M);
}

void add(char *str) {
    setBit(hash1(str));
    setBit(hash2(str));
    // Add more hash functions if needed
}

int is_in(char *str) {
    return getBit(hash1(str)) && getBit(hash2(str));
    // Check more hash functions if used
}

int main() {
    char *testStr = "testString";
    add(testStr);

    if (is_in(testStr)) {
        printf("%s is probably in the set.\n", testStr);
    } else {
        printf("%s is definitely not in the set.\n", testStr);
    }

    return 0;
}

GPT-4が生成した実装は、決して素晴らしいとは言えない。ブルームフィルタはかなり普及している。そこまでエキゾチックなデータ構造ではない。しかし、まともなブルームフィルタを書くには、より高度な抽象化能力が必要なことは明らかだ。例えば、同じ文字列をN回ハッシュする効果的な方法を見つけ、ハッシュ同士が十分に無相関であることを確かめるという問題がある。

しかし、GPT-4にはっきりと、N個の無相関な出力を生成するようにハッシュ関数を修正するよう求めると、提案される解決策はかなりまともになる。

unsigned int hash(const char* str, unsigned int hash_id) {
    unsigned int hash = 5381;
    int c;
    while ((c = *str++)) {
        hash = ((hash << 5) + hash) + c + hash_id;
    }
    return hash % BF_SIZE;
}

もしこれを自力で思いついていたなら、単一のハッシュ関数を使ってK個のビットを一度にセットする形で、ブルームフィルタを違った書き方にしたはずだ。

GPT-4は、適切でより汎用的なハッシュ関数を書くというタスクを、単独では実行できた。しかし、ブルームフィルタを書くというより大きなプロジェクトの中では、十分な推論能力を示すことに失敗し、代わりに二つの異なるが似たようなハッシュ関数を吐き出した。これは、出来の悪いブルームフィルタの記事のサンプルに期待されるようなものだ。

大規模言語モデルの現在の弱い推論能力に、この主題に関する資料の不足の可能性、さらには質の低い資料が多数存在する可能性が相まって、この乏しい結果を生んでいる。しかし、これは孤立したケースではない。アルゴリズムやシステムプログラミングの領域でLLMを使おうとした数少ない機会でも、結果は極めて貧弱だ。求められる推論能力が、Pythonプログラミングのような文脈でLLMが持っているように見える能力を上回らない場合でさえそうなのだ。

同時に、GPT-4は(別のセッションで)自身が生成した関数を逆コンパイルし、その目的を正確に理解することもできる。

https://chat.openai.com/share/f54b64d7-d19a-4be4-8f10-08553aa812f1

したがって、システムプログラミングの文脈では使い道がないと言うのは、確かに不正確だ。

一つ興味深く、ほぼ予想通りのことは、こうしたケースでは小さいモデルと大きいモデルの差が実に大きいということだ。2つのローカルモデルがどうなるか見てみよう。

例えば、MistralのMoEであるMixtralは、同じ質問に対して次のように答えた。

unsigned int hash(const char* str, unsigned int hash_id) {
    unsigned int hash = 5381;
    int c;
    while ((c = *str++)) {
        hash = ((hash << 5) + hash) + c;
    }
    return hash + hash_id;
}

明らかに、最後に `hash_id` を足すだけというのは極めて貧弱な解決策だ。Mixtralは多くの用途で優れたモデルだが、もともと弱いLLMの推論能力に関しては、これまでのところ法則は確かに「大きければ大きいほど良い」だ。

これはローカルのLLMであるdeepseek-coderとのチャットで、より高い精度でモデルを使うのに十分なRAMがないため4ビットに量子化している。それでも340億パラメータという大きさで、この件に関する推論能力はかなり高度に見える。チャットのログは次の通りだ(llama.cppで実行)。

同じデータに対してN個の異なるハッシュを出力すべきハッシュ関数があります。しかし、`hash_id` の値を変えても、生成されるハッシュの分布が非常に悪いように見えます。

unsigned int hash(const char* str, unsigned int hash_id) {
    unsigned int hash = 5381;
    int c;
    while ((c = *str++)) {
        hash = ((hash << 5) + hash) + c;
    }
    return hash + hash_id;
}

もしかすると、私が使っているソルティング戦略が貧弱なのでしょうか? どう変えればよいでしょうか?

実装における起こりうる問題の一つは、ハッシュの末尾に単に `hash_id` を足していることだ。これは分布の悪化につながる可能性があり、特に `hash_id` が大きすぎる場合や均等に分布していない場合に顕著だ。

`hash_id` を混ぜ込むためにビット演算を使うことを検討してほしい。XORを使った例を以下に示す。

unsigned int hash(const char* str, unsigned int hash_id) {
    unsigned int hash = 5381;
    int c;
    while ((c = *str++)) {
        hash = ((hash << 5) + hash) ^ c + hash_id; // Using XOR to mix in the hash_id
     }
    return (hash ^ hash_id); // Mixing the hash_id one more time at the end
}

これは単に足し合わせるよりも `hash_id` をハッシュ全体にうまく拡散させ、より均一でない分布になるのを防ぐ。[中略……実際の返答にはさらに有益な内容が含まれていた]

MacBook M1 Maxで動作するモデルとしては悪くない。加算とXORも混ぜ合わせている。このケースでは、解くべき問題についての手がかりを私が与えたことでモデルは確かに助けられたが、トラブルの真の原因を特定し、有効な代替案を提案したのはモデル自身だ。上記のようなことは、どんな本やドキュメント、Google検索でも得られない。そして、それがどれほど原始的で、補間の結果であろうと、どう捉えようと、モデルは何らかの形で推論を行ったのだ。ここで言う推論を、この特定のケースにおける問題の起源の特定とその潜在的な解決策と認めるならば。LLMについてどう考えようと、プログラマーにとって役に立たないと言うのは極めて軽率だ。

しかし同時に、ここ数ヶ月の私の経験からすると、システムプログラミングに関しては、すでに熟練したプログラマーである場合、LLMが許容できる解決策を提供することはほとんどない。もう一つ実例を示そう。私の現在のプロジェクトであるggufflibは、GGUF形式のファイルを読み書きするライブラリを書くもので、これはllama.cppが量子化されたモデルを読み込む際の形式だ。当初、量子化エンコーディングがどのように機能するかを理解するために(速度上の理由から各quantのビットは凝った方法で格納されている)、ChatGPTを使ってみたが、結局llama.cppのコードをリバースエンジニアリングすることにした。その方がはるかに速かったからだ。システムプログラマーをまともに支援できるLLMなら、データエンコーディングの「struct」宣言とデコード関数を見れば、データ形式のドキュメントを再構築できるはずだ。llama.cppの関数はGPT-4のコンテキストに完全に収まるほど小さかったのに、出力はまったく役に立たなかった。こういうケースでは、昔ながらのやり方でやることになる。紙とペンでコードを読み、デコーダが抽出したビットがどこに格納されるかを確認するのだ。

上記のユースケースについて、もし試してみたい人のためにもう少し詳しく説明しよう。llama.cppの実装から取ったこの構造体がある。

// 6-bit quantization
// weight is represented as x = a * q
// 16 blocks of 16 elements each
// Effectively 6.5625 bits per weight
typedef struct {
    uint8_t ql[QK_K/2];      // quants, lower 4 bits
    uint8_t qh[QK_K/4];      // quants, upper 2 bits
    int8_t  scales[QK_K/16]; // scales, quantized with 8 bits
    ggml_fp16_t d;           // super-block scale
} block_q6_K;

そして、非量子化を行うために使われるこの関数がある。

void dequantize_row_q6_K(const block_q6_K * restrict x, float * restrict y, int k) {
    assert(k % QK_K == 0);
    const int nb = k / QK_K;

    for (int i = 0; i < nb; i++) {

        const float d = GGML_FP16_TO_FP32(x[i].d);

        const uint8_t * restrict ql = x[i].ql;
        const uint8_t * restrict qh = x[i].qh;
        const int8_t  * restrict sc = x[i].scales;
        for (int n = 0; n < QK_K; n += 128) {
            for (int l = 0; l < 32; ++l) {
                int is = l/16;
                const int8_t q1 = (int8_t)((ql[l +  0] & 0xF) | (((qh[l] >> 0) & 3) << 4)) - 32;
                const int8_t q2 = (int8_t)((ql[l + 32] & 0xF) | (((qh[l] >> 2) & 3) << 4)) - 32;
                const int8_t q3 = (int8_t)((ql[l +  0]  >> 4) | (((qh[l] >> 4) & 3) << 4)) - 32;
                const int8_t q4 = (int8_t)((ql[l + 32]  >> 4) | (((qh[l] >> 6) & 3) << 4)) - 32;
                y[l +  0] = d * sc[is + 0] * q1;
                y[l + 32] = d * sc[is + 2] * q2;
                y[l + 64] = d * sc[is + 4] * q3;
                y[l + 96] = d * sc[is + 6] * q4;
            }
            y  += 128;
            ql += 64;
            qh += 32;
            sc += 8;
        }
    }
}

GPT-4に使われているフォーマットの概要を書くよう頼むと、重みの位置に応じてブロックが「ql」の下位/上位4ビットにどのように格納されているかを明確に説明するのに苦労する。このブログ記事のために、よりシンプルにデータがどのように格納されるかを示す関数を書かせてみたこともある(言葉では説明できなくても、コードならできるかもしれないと思って)。生成された関数は多くの点で壊れており、インデックスは間違っているし、6ビットから8ビットへの符号拡張も間違っている(単にuint8_tにキャストしているだけだ)し、他にもいろいろだ。

ちなみに、これは結局自分で書いたコードだ。

    } else if (tensor->type == GGUF_TYPE_Q6_K) {
        uint8_t *block = (uint8_t*)tensor->weights_data;
        uint64_t i = 0; // i-th weight to dequantize.
        while(i < tensor->num_weights) {
            float super_scale = from_half(*((uint16_t*)(block+128+64+16)));
            uint8_t *L = block;
            uint8_t *H = block+128;
            int8_t *scales = (int8_t*)block+128+64;
            for (int cluster = 0; cluster < 2; cluster++) {
                for (uint64_t j = 0; j < 128; j++) {
                    f[i] = (super_scale * scales[j/16]) *
                           ((int8_t)
                            ((((L[j%64] >> (j/64*4)) & 0xF) |
                             (((H[j%32] >> (j/32*2)) & 3) << 4)))-32);
                    i++;
                    if (i == tensor->num_weights) return f;
                }
                L += 64;
                H += 32;
                scales += 8;
            }
            block += 128+64+16+2; // Go to the next block.
        }
   }

上記の関数からは、このコードの実際の貢献であった部分、すなわちllama.cppのQ6_Kエンコーディングで使われている正確なフォーマットを文書化した長いコメントを取り除いてある。GPTがこれを代わりにやってくれればとてつもなく有用なのだが、これはあと数ヶ月の問題だと賭けてもいい。なぜなら、この種のタスクはブレークスルーなしでも、少しスケールさせるだけで到達可能な範囲にあるからだ。

物事を俯瞰する

残念ながら、これは事実だ。今日のプログラミングの大部分は、同じことを少しだけ形を変えて吐き戻すことで成り立っている。高度な推論は必要とされない。LLMはこの点でかなり優秀だ。コンテキストの最大サイズによって強く制限されているとはいえだ。このことはプログラマーに真剣に考えさせるはずだ。この種のプログラムを書くことに価値はあるのか? もちろん報酬は得られるし、かなり高額だ。しかし、LLMがその一部を担えるのであれば、5年後や10年後にいるべき最良の場所ではないかもしれない。

そして、LLMには何らかの推論能力があるのか、それともすべてハッタリなのか? おそらく時には、記号論者が言うところの「シニフィアン」が、実際には存在しない意味の印象を与えているだけだから推論しているように見えるだけなのかもしれない。LLMを十分に使ってきた人々は、その限界を認めつつも、そうではないと確信している。彼らが以前に見たものを混ぜ合わせる能力は、単語をランダムに吐き戻すレベルをはるかに超えている。学習の大部分が事前学習中に次のトークンを予測する形で行われたとはいえ、この目標がモデルに何らかの抽象モデルの作成を強いるのだ。このモデルは弱く、まだらで、不完全だが、私たちが観察していることを観察する限り、存在しなければならない。数学的な確信が疑わしく、最も優れた専門家たちでさえしばしば対立する立場にいるのであれば、自分の目で見たものを信じるのが賢明なアプローチに思える。

最後に、今日プログラミングにLLMを使わないことにどんな意味があるだろうか? LLMに正しい質問をすることは基本的なスキルだ。練習しなければするほど、AIのおかげで仕事を改善できる可能性は低くなる。そして、問題を記述する能力を磨くことは、他の人間と話すときにも役立つ。私たちの言いたいことを時々理解しないのはLLMだけではない。下手なコミュニケーションは大きな制約であり、多くのプログラマーは特定の分野では非常に有能であるにもかかわらず、コミュニケーションが極めて下手だ。そして今やGoogleは使い物にならない。圧縮されたドキュメントの一形態としてでさえLLMを使うのは良い考えだ。私自身は、これからも大いに使い続けるつもりだ。私はこれまで、難解な通信プロトコルの詳細や、自分がいかに優れているかを見せたがっている誰かが書いたライブラリの入り組んだメソッドを学ぶことを好んだことがない。それは私には「ジャンク知識」に思える。LLMは日増しに、そうしたすべてから私を救ってくれている。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント