TECH PLAY

プログラミング

イベント

マガジン

技術ブログ

AIは「1+1って、2になること多いなあ」と思っている!? ChatGPTに「1+1は?」と聞けば、当然「2」と返ってきます。 実はこのときのAIの内部で起こっていることは、割と大真面目に 「 私のデータによれば、1+1の答えは最も2が多いです 」なのです。 計算してるんじゃないの? ChatGPTのようなAI(大規模言語モデル)は、極端なことを言ってしまえば、次の単語予測マシンです。 たとえば「むかしむかし、あるところに」と言われたら、「おじいさんと」と返す。「今日の天気は」と言われたら、「晴れ」とか「曇り」とか返す。 膨大な文章を読んで「この言葉の次にはこの言葉が来やすい」というパターンを学習しているだけなんです。 AIにとっては、計算問題も文章の一種のため、数学の問題も同じやりかたで解いています。 「1+1=」という文字列を見て、「この後には2が来ることが多いな」と思って2を返しているだけ。 つまり: 人間: 1+1を理解して、演算して、2を導く AI: 「1+1=」の後に「2」が来るパターンで覚えてる そう、タイトルの通り、AIは「1+1って、2になること多いなあ」なのです。 ほんと? もしAIが計算を「理解」しているのではなく、単なる「パターンの暗記」だとしたら、 見たことがないパターンに出会ったときにボロが出るはず です。 その実態がよくわかる、2つの証拠を見てみましょう。 証拠1: ちょっと難しいかけ算ができない 普通の計算機(電卓)なら、2桁の足し算ができれば、4桁になっても10桁になっても、やることは同じなので間違えません。 しかし、OpenAIの研究論文「 Language Models are Few-Shot Learners 」(Brown et al., 2020)によると、当時のGPT-3は以下のような結果になりました。 2桁の足し算: ほぼ100%正解 4桁以上の計算: 正答率は 20%以下 に急落 つまり、ネット上にたくさん転がっている「よくある計算(2桁)」はパターンとして覚えているけれど、滅多に見かけない「大きな桁の計算」は、パターンがないのでお手上げになってしまうのです。   証拠2: 聞き方が変だとできなくなる たとえば「0.7 × 5 は?」と聞けばAIは即座に3.5と答えます。 でも同じ計算を「7×10⁻¹ × 5 は?」と科学的記数法で書くと、数学的にはまったく同じ計算なのに、急に怪しくなります。 Yang et al.(2024)の論文「 Number Cookbook 」では、LLMは標準的な整数計算には強い一方、分数や科学的記数法になると、精度が 20%以下 まで落ちることが示されています。 数学を理解しているなら「書き方が違うだけ」と分かりますが、AIにとっては 見たことがない珍しい文字列 に見えてしまうため、予測が外れてしまうのです。   やってみよう 実際に試してみましょう。ChatGPTに「4726 × 3891 は?」と聞いてみます。 あれ、普通に正解されました。 実は、今のAIはこの「弱点」を、 知能ではなく「仕組み」で克服 しつつあるんです。 今は解決してる ChatGPTの新しいもの(GPT-4o)やClaude 4.5などは、数学の実力テスト(MATHやGSM8Kといった、AIに数学の問題を解かせてスコアを測る標準テスト)で非常に高いスコアを出しています。その理由は主に2つあります。 1つ目はツールの利用です。 計算が必要な場面で、内部的にプログラミングコードを実行したり電卓を使ったりして、LLM自体は直接計算せずに正解を得る方法が発達しました。 2つ目は、Chain-of-Thoughtというもので、段階を踏んで思考することで、単純に答えるよりもはるかに複雑な問題を解けるようになりました。 例えば「23 × 47」を一発で出すのではなく、「23 × 40 = 920、23 × 7 = 161、920 + 161 = 1081」のように段階を踏めます。 これにより、実用上はAIは計算ができると言える水準になっています。   しかし、ここで注意したいのは、これらはいずれも AIが計算を「理解」しているわけではない ということです。 ツール利用は外部の計算機に丸投げしているだけですし、Chain-of-Thoughtも、本来1ステップでは処理しきれない問題を小さく分割して、トークン生成の過程で中間結果を一時的に保持する仕組みです。 LLMの本質は今も変わらず、次に来る言葉の予測であり、これらの間接的な手順を禁じた場合、AIは依然として大きな数の掛け算や見慣れない形式の計算で間違えます。 まとめ AIは計算を「理解」しているのではなく、「1+1の後には2が来やすい」というパターンで答えている そのため、桁が大きい計算や見慣れない書き方には弱い 最近はツール利用(コード実行)やChain-of-Thought(段階的思考)で実用上は高精度になった ただし、これらはあくまで補助手段であり、LLMの本質は変わっていない AIに計算させるときは、裏でちゃんとコードを実行しているか意識しておくと安心 おまけ 僕はブログを書くときに、AIにレビューをしてもらっています。 この記事をレビューさせてみたときのAIの反応がこちら:   AI: AIは計算ができない!?のレビューを開始します。 … ファクトチェック中… 計算例が間違っています。 本来ならば4726 × 3891 の正しい答えは 18,374,766 です。修正します.. … あ、そういえば私もAIでした。 Pythonコードを実行し、確認します。   最近のAIは賢いですね。 実は、計算例の部分は僕が「AIにこんな感じの例を提示して」と出力させたものなので、 つまりこのやりとりは、 AIが「AIは計算ができない!?」という記事をレビューし、 AIが計算した計算ミスをAIが指摘・修正し、 同時に自分がAIであることにはたと気づき、 AIは計算ができないため、直ちにプログラム実行によって確証を得る という、皮肉・メタ認知のミルフィーユであり、今のAIのアホさと賢さの両面が綺麗に凝縮されたやりとりでした。 関連記事 AIに嘘つかないでよーとお願いするとちょっと効くという記事を書いています: chatGPTに「ハルシネーションしないで」とお願いしたら効果がある?   ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post AIは「1+1って、2になること多いなあ」と思っている!? first appeared on SIOS Tech Lab .
開発2部の内原です。 Goは静的型付けで事前コンパイルされる言語なので、WebAssembly(WASM)にコンパイルしておけば、JavaScriptのJust-In-Time(JIT)コンパイルより速度的に有利であるように思えます。 なんとなくGoをWASMにすればJSより速くなるくらいのふわっとした認識でいましたが、果たしてどのような実装でも速くなるのかそうでないのか、速くなるとしたらどれくらいの差が出るのか、という疑問を持ったので調べてみました。 そこで、いくつかのアルゴリズムで実際にベンチマークを取って検証してみましたが、アルゴリズムの特性によって結果が様々であることがわかりました。 事前準備 実行環境 MacOS 26.2 Go 1.25 Node.js v25 Chrome (144) Go WASM のビルドと関数公開 Go側の関数公開は以下のように js.FuncOf でラップしてグローバルに登録します。 import "syscall/js" func main() { js.Global().Set( "goAdd" , js.FuncOf( func (this js.Value, args []js.Value) any { n1 := args[ 0 ].Int() n2 := args[ 1 ].Int() return add(n) })) select {} } Go側では syscall/js パッケージを使って関数をグローバルに公開し、以下のコマンドでWASMバイナリをビルドします。 $ GOOS=js GOARCH=wasm go build -o main.wasm main.go $ ls -lh main.wasm -rwxr-xr-x@ 1 uchihara staff 2.1M Feb 11 16:00 main.wasm 生成されるWASMバイナリのサイズは約2MBです。Goランタイムが含まれるため、それなりのサイズになります。 JS側では WebAssembly.instantiateStreaming でWASMをロードし、 go.run(instance) を呼ぶと、上記で登録した関数がグローバルから呼び出せるようになります。 const go = new Go () ; const { instance } = await WebAssembly . instantiateStreaming ( fetch ( "main.wasm" ) , go . importObject ) ; go . run ( instance ) ; const r = goAdd ( 1 , 2 ) ; 計測方法 ブラウザ版とCLI版(Node.js)の両方で計測(ただし一部を除いて性能差はさほど出なかった) 各テストは複数回計測の平均を採用 ベンチマーク関数は以下です。 function bench ( fn , args , iters ) { const times = [] ; for ( let i = 0 ; i < iters ; i ++ ) { const start = performance . now () ; fn ( ... args ) ; const end = performance . now () ; times . push ( end - start ) ; } return times . reduce (( a , b ) => a + b , 0 ) / times . length ; } フィボナッチ関数 まずはシンプルなフィボナッチ数列の計算で比較しました。その際、関数呼び出しのオーバーヘッドが性能に影響を与える可能性があると考えたため、再帰版とループ版の2パターンで確認します。 Go側とJS側でほぼ同一のロジックを実装しています。 func goFibRecursive(n int ) int { if n <= 1 { return n } return goFibRecursive(n- 1 ) + goFibRecursive(n- 2 ) } func goFibIterative(n int ) int { if n <= 1 { return n } a, b := 0 , 1 for i := 2 ; i <= n; i++ { a, b = b, a+b } return b } function jsFibRecursive ( n ) { if ( n <= 1 ) return n ; return jsFibRecursive ( n - 1 ) + jsFibRecursive ( n - 2 ) ; } function jsFibIterative ( n ) { if ( n <= 1 ) return n ; let a = 0 , b = 1 ; for ( let i = 2 ; i <= n ; i ++ ) { [ a , b ] = [ b , a + b ] ; } return b ; } 再帰版 fibRecursive(40) 実装 実行時間 倍率 JavaScript 660ms 1.0x Go WASM 1,560ms 2.4x ループ版 fibIterative(10000000) 実装 実行時間 倍率 JavaScript 9ms 1.0x Go WASM 15ms 1.7x CLI版だとどちらのパターンでもJSのほうが高速という結果になりました。 ただブラウザ版だとGo WASMのほうが3倍ほど速くなっていました。JSエンジンの最適化による差分かもしれません。 原因分析 フィボナッチ計算は計算自体が軽量で、関数呼び出しのオーバーヘッドが支配的になります。JITコンパイラはこの種のシンプルな数値計算を最適化している可能性が考えられます。 どうやら「WASMにすれば速くなる」という単純な話でもなさそうです。 行列乗算 計算量をもう増やせば差が出るかもと考えたので、比較的計算量の大きい512x512の行列乗算で試してみました。 Go/JS双方でikjループ順を使い、同一の決定的データを生成して計算しています。 Go側は []float64 スライスを使い、JS側では Float64Array を使っています。 func goMatMul() { n := 512 a := make ([] float64 , n*n) b := make ([] float64 , n*n) for i := 0 ; i < n*n; i++ { a[i] = float64 (i% 97 ) * 0.01 b[i] = float64 (i% 89 ) * 0.01 } c := make ([] float64 , n*n) for i := 0 ; i < n; i++ { for k := 0 ; k < n; k++ { aik := a[i*n+k] for j := 0 ; j < n; j++ { c[i*n+j] += aik * b[k*n+j] } } } sum := 0.0 for _, v := range c { sum += v } return sum } function jsMatMul () { const n = 512 ; const a = new Float64Array ( n * n ) ; const b = new Float64Array ( n * n ) ; for ( let i = 0 ; i < n * n ; i ++ ) { a [ i ] = ( i % 97 ) * 0 . 01 ; b [ i ] = ( i % 89 ) * 0 . 01 ; } const c = new Float64Array ( n * n ) ; for ( let i = 0 ; i < n ; i ++ ) { for ( let k = 0 ; k < n ; k ++ ) { const aik = a [ i * n + k ] ; for ( let j = 0 ; j < n ; j ++ ) { c [ i * n + j ] += aik * b [ k * n + j ] ; } } } let sum = 0 ; for ( let i = 0 ; i < n * n ; i ++ ) sum += c [ i ] ; return sum ; } 実装 実行時間 倍率 JavaScript 190ms 1.0x Go WASM 208ms 1.1x 差は縮まりましたが、まだ若干JSが優勢です。 原因分析 JS側ではTypedArrayに対して最適化が行われている可能性があります。 またGo WASM側では以下箇所がオーバーヘッドになっている可能性があります。 WASMではSIMD命令を十分に活用できない? Goのスライスにおけるbounds checkのコストがある? 行列乗算は計算量が大きいためJS-WASM境界のオーバーヘッドは相対的に小さくなりますが、依然としてJSが有利でした。 SHA-256 計算量をもっと増やすと変化が出てくるかもと考えたので、SHA-256関数を利用することにします。 その際、JSによる純粋な実装よりネイティブAPIによる実装のほうが効率的である可能性が高いと考えたため、SubtleCrypto:digest()も比較対象に含めました。 ただ、SubtleCrypto:digest()は非同期関数であり、ベンチ時に同期的に呼び出しを行う必要がある点に注意が必要でした。 チェインハッシュ 小さなデータのハッシュ結果を次のハッシュの入力にする、という処理を10万回繰り返しました。 実装 実行時間 倍率 Go WASM 41ms 1.0x JS 純粋実装 114ms 2.8x SubtleCrypto 200ms 4.9x Go WASMがJS純粋実装の約2.8倍速く、最速という結果になりました。また、SubtleCryptoはさらに遅いという結果になりました。 Go WASMが速い理由 SHA-256はビット演算・整数演算が中心のアルゴリズムで、Goのコンパイル済みWASMコードが有利な立場だったと言えそうです。また、10万回のハッシュ計算を1回の関数呼び出しでWASM内で完結させている点で、呼び出しオーバーヘッドの影響がほぼなく、効率的だったと考えられます。 js.Global().Set( "goSHA256" , js.FuncOf( func (this js.Value, args []js.Value) any { data := [] byte (args[ 0 ].String()) iterations := args[ 1 ].Int() h := sha256.Sum256(data) for i := 1 ; i < iterations; i++ { h = sha256.Sum256(h[:]) } return hex.EncodeToString(h[:]) })) JS-WASM境界を跨ぐのは最初の呼び出しと結果の返却の1往復だけで、ループ全体がWASM内で実行されます。これにより crypto/sha256 標準ライブラリの実装がそのまま適用されます。 フィボナッチではJS側から関数を1回呼ぶという意味では同じでしたが、計算自体が軽いためランタイムオーバーヘッドが目立ちました。SHA-256チェインでは1回の呼び出しの中で重い計算を行うため、オーバーヘッドが相対的に無視できるようになります。 SubtleCryptoが遅い理由 ネイティブAPIの crypto.subtle.digest() が最も遅い結果となりました。 async function jsSHA256Subtle ( str , iterations ) { const encoder = new TextEncoder () ; let data = encoder . encode ( str ) ; data = new Uint8Array ( await crypto . subtle . digest ( "SHA-256" , data )) ; for ( let i = 1 ; i < iterations ; i ++ ) { data = new Uint8Array ( await crypto . subtle . digest ( "SHA-256" , data )) ; } return Array . from ( data , b => b . toString ( 16 ) . padStart ( 2 , "0" )) . join ( "" ) ; } SubtleCrypto はasync APIのみを提供しているため、10万回のチェインハッシュでは毎回Promise生成 → microtask enqueue → await復帰を繰り返します。ハッシュ計算自体よりも非同期ディスパッチのコストが支配的になっているようです。 巨大バッファハッシュ SubtleCryptoは大きなデータを一括処理するケースで優位であることが予想されます。64MBのバッファを1回だけハッシュする形式に変更して計測しました。 async function jsSHA256BulkSubtle ( size ) { const data = new Uint8Array ( size ) ; for ( let i = 0 ; i < size ; i ++ ) data [ i ] = i % 251 ; const hash = new Uint8Array ( await crypto . subtle . digest ( "SHA-256" , data )) ; return Array . from ( hash , b => b . toString ( 16 ) . padStart ( 2 , "0" )) . join ( "" ) ; } 実装 実行時間 倍率 SubtleCrypto 350ms 1.0x Go WASM 430ms 1.2x JS 純粋実装 980ms 2.8x 非同期呼び出しは1回だけなのでネイティブの速度がそのまま活かされ、SubtleCryptoが最速という結果になりました。 対して、GO WASM版にはなんらかのオーバーヘッドが存在しているのか、もしくはダイジェスト関数実装における性能差があるのかもしれません。 SubtleCryptoは大きなデータを一括処理する用途向きであり、小さなデータを繰り返しハッシュするような用途には向いていなさそう、ということがわかりました。 おまけ 計測中に興味深い現象を発見しました。Mac Chrome(144.0.7559.110)で、DevToolsを開いた状態では閉じた状態に比べてGo WASMの性能が低下するというものです。 テスト Go WASM(DevTools閉) Go WASM(DevTools開) 劣化率 再帰 fib(40) 1,550ms 3,150ms 2.0x 行列乗算 512x512 203ms 453ms 2.2x SHA-256 チェイン 41ms 136ms 3.3x SHA-256 64MB 428ms 1,461ms 3.4x 一方、JS側にはほとんど影響がありませんでした。 原因 DevToolsを開くと、Chromeが内部的にChrome DevTools Protocol(CDP)の Debugger.enable() を発行するようです。これによりWASMバイトコードにデバッグ用コード(ブレークポイント判定等)を挿入するため、WASMの実行速度が大幅に低下しますが、一方JSのJITコードには同等の影響があまり発生しないようです。 WASMのベンチマーク時はDevToolsを閉じた状態で行う、またはDevToolsを開いている場合にはDebuggerを無効化した状態で行う必要があることが分かりました。 まとめ Go WASMがJSより優位なのは、1回の関数呼び出しで大量の計算をWASM内で完結させるパターン(SHA-256チェインなど) 逆に、関数呼び出しが頻繁・計算が軽い場合は、JS JITが有利(フィボナッチなど) SubtleCryptoなどのasync APIは呼び出し回数に注意が必要。大バッファの一括処理なら効果的 WASMのベンチマーク時はDevToolsを閉じるorDebugger無効化。そうしないと2〜3倍の劣化が発生する 「GoをWASMにすればJSより速くなる」、というのは条件次第で真偽いずれもあり得ることが分かりました。 JS-WASM境界を跨ぐ回数を最小化し、WASM内でまとまった計算を完結させる設計にすることが重要そうです。 WASMの導入を検討する際は、対象のアルゴリズムがこのパターンに合致するかを事前に見極める必要があります。
Prismaとは? Prismaは、Node.js向けのORM(Object-Relational Mapping)ツールです。 最大の特徴は 型安全なデータベースアクセス にあり、 簡潔なコードでDB操作を記述でき、TypeScriptとの相性が非常に良いことが強みです。 SQLを直接書かなくてもデータ操作ができる スキーマからTypeScriptの型が自動生成される 型ミスをコンパイル時に検知できる ※ ORMとは? ORM(Object-Relational Mapping)とは、 プログラミング言語のオブジェクトを使って SQLを書かずにデータベースを操作できる仕組み のことです。 P…

動画

書籍