Gemini Advanced vs ChatGPT4: コード生成(Go言語)比較

こんにちは
ATOM事業部エンジニアの渡部です。
昨今OpenAIのみならず、色々な企業からLLM(Large Language Models 大規模言語モデル)がでて面白い状況が続いていますね。
 
そんな中、去年の年末あたりで色々話題になりました「Gemini Advanced」を実際に契約したので
私がよく使っている「プログラムコード生成」を実際にお願いしてみて、同じくWebから使用できるChatGPT4との比較を行っていきたいと思います。
また今回は実験ですので、コード生成を1から「丸投げ」して自分自身ではコード修正を行わないようにしていますが、実際にコード生成で使う場合、ある程度自分でプログラムを書いて修正依頼をするほうがいいと思います。
 
※日々進化しているLLM界隈にとってこの手の比較は古くなりやすいため、この記事が2024年4月25日に書かれていることを明記しておきます。

簡単なコード生成

まず手始めに
Go言語でHelloWorld1〜100を非同期で100回表示し、
表示し終わったらGoodByeを表示して終了するプログラムを書いて!
 

Gemini Advancedの結果

生成結果はこちらになります

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 100; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			fmt.Println("HelloWorld", i)
		}(i)
	}

	wg.Wait()
	fmt.Println("Goodbye")
}
    
 
実行してみると正常に実行できるようです。
まぁ、これは楽勝ですね!

ChatGPT4の結果

ChatGPT4もほぼ同じコードが生成されました(省略します)
 

「四則演算」コード生成

 
次は四則演算のプログラムを作ってみましょう
 
Go言語を用いて、四則演算をするプログラムを作成してほしい
入力に数式(文字列)を受け付け、その計算結果を出力する
四則演算の計算順を考慮して計算すること。
 
これはどうでしょう
 

Gemini Advancedの結果

生成コードはこちらになります
package main

import (
	"fmt"
	"strconv"
	"strings"
)

// 演算子の優先順位
var operatorPrecedence = map[string]int{
	"*": 3,
	"/": 3,
	"+": 2,
	"-": 2,
}

// 括弧内の式を評価する関数
func evalBracket(expr string) string {
	// 括弧の位置を取得
	left := strings.Index(expr, "(")
	right := strings.Index(expr, ")")

	if left == -1 || right == -1 {
		return expr
	}

	// 括弧内の式を取り出す
	innerExpr := expr[left+1 : right]

	// 括弧内の式を再帰的に評価
	result := eval(innerExpr)

	// 括弧内の式を置換
	return strings.Replace(expr, innerExpr, result, -1)
}

// 数式を評価する関数
func eval(expr string) string {
	// 括弧内の式を評価
	expr = evalBracket(expr)

	// トークン化
	tokens := strings.Fields(expr)

	// 演算子とオペランドのスタック
	operatorStack := []string{}
	operandStack := []string{}

	for _, token := range tokens {
		if isOperator(token) {
			// 演算子の処理
			for len(operatorStack) > 0 && precedence(operatorStack[len(operatorStack)-1]) >= precedence(token) {
				popOperator := operatorStack[len(operatorStack)-1]
				operatorStack = operatorStack[:len(operatorStack)-1]

				rightOperand := operandStack[len(operandStack)-1]
				operandStack = operandStack[:len(operandStack)-1]
				leftOperand := operandStack[len(operandStack)-1]
				operandStack = operandStack[:len(operandStack)-1]

				result := calculate(leftOperand, rightOperand, popOperator)
				operandStack = append(operandStack, result)
			}
			operatorStack = append(operatorStack, token)
		} else {
			// オペランドの処理
			operandStack = append(operandStack, token)
		}
	}

	// スタックに残っている演算子を処理
	for len(operatorStack) > 0 {
		popOperator := operatorStack[len(operatorStack)-1]
		operatorStack = operatorStack[:len(operatorStack)-1]

		rightOperand := operandStack[len(operandStack)-1]
		operandStack = operandStack[:len(operandStack)-1]
		leftOperand := operandStack[len(operandStack)-1]
		operandStack = operandStack[:len(operandStack)-1]

		result := calculate(leftOperand, rightOperand, popOperator)
		operandStack = append(operandStack, result)
	}

	// 結果を返す
	return operandStack[0]
}

// 計算を行う関数
func calculate(left, right, operator string) string {
	leftNum, _ := strconv.Atoi(left)
	rightNum, _ := strconv.Atoi(right)

	switch operator {
	case "+":
		return strconv.Itoa(leftNum + rightNum)
	case "-":
		return strconv.Itoa(leftNum - rightNum)
	case "*":
		return strconv.Itoa(leftNum * rightNum)
	case "/":
		return strconv.Itoa(leftNum / rightNum)
	default:
		panic("unsupported operator: " + operator)
	}
}

// 文字列が演算子かどうかを判定する関数
func isOperator(token string) bool {
	return operatorPrecedence[token] > 0
}

// 演算子の優先順位を返す関数
func precedence(operator string) int {
	return operatorPrecedence[operator]
}

func main() {
	var expr string
	fmt.Print("数式を入力してください: ")
	fmt.Scanln(&expr)

	result := eval(expr)
	fmt.Println("計算結果は", result)
}

 
なかなかよさそう?
ですが、実際に実行してみると
 

 
となり、うまくいきません。
その後、色々修正を伝えてみましたがちゃんと生成してはくれませんでした。
 

ChatGPT4の結果

生成コードはこちらになります
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	fmt.Print("Enter a mathematical expression: ")
	expr, _ := reader.ReadString('\n')
	expr = strings.TrimSpace(expr)
	expr = strings.ReplaceAll(expr, " ", "") // Remove any spaces

	value, err := evaluate(expr)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Result:", value)
}

// Tokenizes the input string into numbers and operators
func tokenize(expression string) ([]string, error) {
	var tokens []string
	last := 0
	for i, char := range expression {
		if char == '+' || char == '-' || char == '*' || char == '/' {
			if last != i {
				tokens = append(tokens, expression[last:i])
			}
			tokens = append(tokens, string(char))
			last = i + 1
		}
	}
	if last < len(expression) {
		tokens = append(tokens, expression[last:])
	}
	return tokens, nil
}

// Evaluates the expression and returns the result
func evaluate(expression string) (float64, error) {
	tokens, err := tokenize(expression)
	if err != nil {
		return 0, err
	}

	// Stack for numbers
	var numbers []float64

	// Stack for Operators
	var ops []string

	// Operator Precedence
	precedence := map[string]int{
		"+": 1,
		"-": 1,
		"*": 2,
		"/": 2,
	}

	// Apply operation to top two elements in the number stack
	applyOp := func(op string) {
		if len(numbers) < 2 {
			return
		}
		b := numbers[len(numbers)-1]
		numbers = numbers[:len(numbers)-1]
		a := numbers[len(numbers)-1]
		numbers = numbers[:len(numbers)-1]

		var res float64
		switch op {
		case "+":
			res = a + b
		case "-":
			res = a - b
		case "*":
			res = a * b
		case "/":
			res = a / b
		}
		numbers = append(numbers, res)
	}

	for _, token := range tokens {
		if num, err := strconv.ParseFloat(token, 64); err == nil {
			numbers = append(numbers, num)
		} else { // Operator
			for len(ops) > 0 && precedence[ops[len(ops)-1]] >= precedence[token] {
				applyOp(ops[len(ops)-1])
				ops = ops[:len(ops)-1]
			}
			ops = append(ops, token)
		}
	}

	for len(ops) > 0 {
		applyOp(ops[len(ops)-1])
		ops = ops[:len(ops)-1]
	}

	if len(numbers) != 1 {
		return 0, fmt.Errorf("error calculating the expression")
	}

	return numbers[0], nil
}
こちらは2回ほど修正のプロンプトを投げて生成したものです。
実行結果は以下の通り

うまくいってそうです!

 

【おまけ】
またGeminiで出力した四則演算プログラムを丸投げして修正させてみたところ
四則演算の優先順位で計算してくれるコードが作成できました。
 
生成コードはこちら
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

// 演算子の優先順位
var operatorPrecedence = map[string]int{
	"*": 3,
	"/": 3,
	"+": 2,
	"-": 2,
}

// 括弧内の式を評価する関数
func evalBracket(expr string) string {
	// 括弧の位置を取得
	left := strings.LastIndex(expr, "(")
	if left == -1 {
		return expr
	}
	right := strings.Index(expr[left:], ")") + left
	if right == -1 {
		return expr
	}

	// 括弧内の式を取り出す
	innerExpr := expr[left+1 : right]

	// 括弧内の式を再帰的に評価
	result := eval(innerExpr)

	// 括弧内の式を置換
	return expr[:left] + result + expr[right+1:]
}

// 数式をトークン化する関数
func tokenize(expr string) []string {
	var tokens []string
	current := ""
	for _, c := range expr {
		if c == ' ' {
			continue
		} else if strings.Contains("+-*/()", string(c)) {
			if current != "" {
				tokens = append(tokens, current)
				current = ""
			}
			tokens = append(tokens, string(c))
		} else {
			current += string(c)
		}
	}
	if current != "" {
		tokens = append(tokens, current)
	}
	return tokens
}

// 数式を評価する関数
func eval(expr string) string {
	// 括弧内の式を評価
	expr = evalBracket(expr)

	// トークン化
	tokens := tokenize(expr)

	// 演算子とオペランドのスタック
	operatorStack := []string{}
	operandStack := []string{}

	for _, token := range tokens {
		if isOperator(token) {
			// 演算子の処理
			for len(operatorStack) > 0 && precedence(operatorStack[len(operatorStack)-1]) >= precedence(token) {
				popOperator := operatorStack[len(operatorStack)-1]
				operatorStack = operatorStack[:len(operatorStack)-1]

				rightOperand := operandStack[len(operandStack)-1]
				operandStack = operandStack[:len(operandStack)-1]
				leftOperand := operandStack[len(operandStack)-1]
				operandStack = operandStack[:len(operandStack)-1]

				result := calculate(leftOperand, rightOperand, popOperator)
				operandStack = append(operandStack, result)
			}
			operatorStack = append(operatorStack, token)
		} else {
			// オペランドの処理
			operandStack = append(operandStack, token)
		}
	}

	// スタックに残っている演算子を処理
	for len(operatorStack) > 0 {
		popOperator := operatorStack[len(operatorStack)-1]
		operatorStack = operatorStack[:len(operatorStack)-1]

		rightOperand := operandStack[len(operandStack)-1]
		operandStack = operandStack[:len(operandStack)-1]
		leftOperand := operandStack[len(operandStack)-1]
		operandStack = operandStack[:len(operandStack)-1]

		result := calculate(leftOperand, rightOperand, popOperator)
		operandStack = append(operandStack, result)
	}

	// 結果を返す
	return operandStack[0]
}

// 計算を行う関数
func calculate(left, right, operator string) string {
	leftNum, _ := strconv.Atoi(left)
	rightNum, _ := strconv.Atoi(right)

	switch operator {
	case "+":
		return strconv.Itoa(leftNum + rightNum)
	case "-":
		return strconv.Itoa(leftNum - rightNum)
	case "*":
		return strconv.Itoa(leftNum * rightNum)
	case "/":
		return strconv.Itoa(leftNum / rightNum)
	default:
		panic("unsupported operator: " + operator)
	}
}

// 文字列が演算子かどうかを判定する関数
func isOperator(token string) bool {
	return operatorPrecedence[token] > 0
}

// 演算子の優先順位を返す関数
func precedence(operator string) int {
	return operatorPrecedence[operator]
}

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	fmt.Print("数式を入力してください: ")
	scanner.Scan()
	expr := scanner.Text()

	result := eval(expr)
	fmt.Println("計算結果は", result)
}

 
実行結果は以下の通り

 

結論

こちらはChatGPT4に軍配が上がりました。

「組み合わせ一覧出力」コード生成

 
次は条件が複雑な組み合わせ一覧を出力させるために、
このようなプロンプトを使ってみます。
 
boxes := map[string][]string{
 "box1": {a},
 "box2": {b},
 "box3": {c, f, g},
 "box4": {c, f, g},
 "box5": {c, f, g},
 "box6": {d},
 "box7": {e},
 }
上記の変数を定義した。
このbox1〜7にはそれぞれ割り当て可能なアルファベット(a〜g)がある。
このアルファベットをひとつずつ使用して各ボックスに入れたい。
ただし同じアルファベットは使ってはいけない。

以下のkumiawase関数を実装して, 以下のリザルトを出力するプログラムを完成させてほしい。
result = kumiawase(boxes)

resultの中身
---
box1 a
box2 b
box3 c
box4 f
box5 g
box6 d
box7 e

box1 a
box2 b
box3 c
box4 g
box5 f
box6 d
box7 e

<省略>
 

Gemini Advancedの結果

Gemini Advancedで生成した結果はだめでした
生成コードはこちら
package main

import (
	"fmt"
)

func kumiawase(boxes map[string][]int, used map[string]bool, result []string) []map[string]string {
	if len(boxes) == 0 {
		// すべてのボックスに割り当て完了
		fmt.Println(result)
		return []map[string]string{}
	}

	// 未使用のアルファベットを取得
	unused := getUnusedAlphabet(boxes, used)

	// ボックスとアルファベットの組み合わせをすべて試す
	for _, box := range boxes {
		for _, alpha := range unused {
			// 仮にボックスにアルファベットを割り当てる
			newUsed := copyMap(used)
			newUsed[box] = []string{alpha}
			newResult := append(result, map[string]string{box: alpha})

			// 再帰的に次のボックスへ進む
			subResult := kumiawase(boxesWithout(boxes, box), newUsed, newResult)

			// すべての組み合わせを試したら元に戻す
			if len(subResult) == 0 {
				delete(newUsed, box)
			}
		}
	}

	return []map[string]string{}
}

func getUnusedAlphabet(boxes map[string][]int, used map[string]bool) []string {
	allAlphabet := []string{"a", "b", "c", "d", "e", "f", "g"}
	usedAlphabet := []string{}

	for _, v := range used {
		for _, a := range v {
			usedAlphabet = append(usedAlphabet, string(a))
		}
	}

	unused := []string{}
	for _, a := range allAlphabet {
		if !contains(usedAlphabet, a) {
			unused = append(unused, a)
		}
	}

	return unused
}

func boxesWithout(boxes map[string][]int, boxName string) map[string][]int {
	newBoxes := map[string][]int{}
	for box, values := range boxes {
		if box != boxName {
			newBoxes[box] = values
		}
	}
	return newBoxes
}

func copyMap(m map[string]bool) map[string]bool {
	newMap := map[string]bool{}
	for k, v := range m {
		newMap[k] = v
	}
	return newMap
}

func contains(s []string, e string) bool {
	for _, a := range s {
		if a == e {
			return true
		}
	}
	return false
}

func main() {
	boxes := map[string][]int{
		"box1": {1},
		"box2": {2},
		"box3": {3, 6, 7},
		"box4": {3, 6, 7},
		"box5": {3, 6, 7},
		"box6": {4},
		"box7": {5},
	}

	result := kumiawase(boxes, map[string]bool{}, []string{})
}
 
実行結果は以下の通り
色々エラーがでておかしなことになっていますね。。。
そのあと何回かやりとりをしてもうまくいきませんでした。
 

ChatGPT4の結果

ChatGPT4で生成したコードは2回のやりとりで以下の通りちゃんと出力されました(一回目は./main.go:53:26: undefined: keysで初歩的なバグ)
生成コードはこちら
package main

import (
	"fmt"
)

var boxes = map[string][]string{
	"box1": {"a"},
	"box2": {"b"},
	"box3": {"c", "f", "g"},
	"box4": {"c", "f", "g"},
	"box5": {"c", "f", "g"},
	"box6": {"d"},
	"box7": {"e"},
}

func kumiawase(boxes map[string][]string, keys []string) [][]string {
	results := [][]string{}
	solve(keys, []string{}, &results, map[string]bool{})
	return results
}

func solve(keys []string, current []string, results *[][]string, used map[string]bool) {
	if len(current) == len(keys) {
		result := make([]string, len(current))
		copy(result, current)
		*results = append(*results, result)
		return
	}

	key := keys[len(current)]
	for _, option := range boxes[key] {
		if !used[option] {
			used[option] = true
			solve(keys, append(current, option), results, used)
			used[option] = false
		}
	}
}

func main() {
	keys := []string{"box1", "box2", "box3", "box4", "box5", "box6", "box7"}
	results := kumiawase(boxes, keys)
	for _, result := range results {
		for i, letter := range result {
			fmt.Printf("%s %s\n", keys[i], letter)
		}
		fmt.Println("---")
	}
}
 
実行結果はこちらになります。
 

 

結果

こちらも完全にChatGPT4の勝利でしょう。
 

結論

全体的にやはり「ChatGPT4」が精度が高いと感じました。
とくにコード修正の精度がすさまじく、
今回のケースではGeminiUltraはちゃんと修正してくれないことがほとんどに対して、
ChatGPT4は修正依頼を適切に読み取って、最終的に実行できるプログラムを生成してくれました。
 
LLMを用いたコード生成の使われ方は「1から」コード生成を依頼するよりも、今あるコードに対して修正依頼や加筆依頼をする事が多いとおもいます。
そのため現状、Goのコード生成で使用する場合は価格差を考慮してもChatGPT4がおすすめできるでしょう。
 
ただ、Gemini Advanced(GeminiUltra 1.0)も出始めなので、現在でもこれだけ出力できる、という受け取り方のほうが良いかなと思います。
今後のLLM界隈も競争が熱いと思いますので、ワクワクしながら今後のリリースを楽しみにしています。