人工知能ChatGPTのソフト開発力:開発速度とコード品質の決め手は人間の知恵

この記事では、ChatGPTを利用する際に、ユーザーが持つ専門知識がどのように役立つのかをご説明します。具体的には、以下の3点をご説明します。

  • ChatGPTに専門用語を用いて的確に指示する

  • ChatGPTのミスを見抜き、修正を依頼する

  • リスクを察知し、ChatGPTに変更を求める

なお説明に用いる具体例として、マージソートというアルゴリズムを使った数字を並べ替えるプログラムの作成を取り上げます。したがって、プログラミング経験が無い方には分かりづらい部分が多いかもしれませんが、エッセンスだけでも伝われば幸いです。
また、プログラミング経験のある方には、人間が上手に誘導してあげることで、ソフトウェア開発においてChatGPTがいかに力を発揮するかを見ていただける事例になっていると思います。

ChatGPTに専門用語を用いて的確に指示する

まず、ある分野の知識をきちんと持っている人であれば、その分野の専門用語を使って、やりたいことをChatGPTに的確に伝えることが出来ます。例えば、ソフトウェア開発をしていて、マージソートというアルゴリズムを使いたいと思ったら、グダグダと説明せずとも、マージソートというアルゴリズム名を伝えるだけでOKです。

ここが、ユーザーに知識があるかどうかで変わってくる1つ目の分かれ道になります。マージソートの名前と中身を知らない人は、「マージソート」という名前を初手で繰り出すことができません。

ユーザー:

マージソートについて説明して下さい。

ChatGPT:

マージソートは、高速な比較ベースのソーティングアルゴリズムで、分割統治法というアプローチに基づいています。マージソートは次の手順で動作します。
1. 分割:与えられたリストを、ほぼ同じサイズの2つのサブリストに分割します。リストが1つの要素だけを持っている場合、それ以上分割は行われず、そのリストはすでにソートされていると見なされます。
2. 再帰的にソート:分割された2つのサブリストをそれぞれ再帰的にマージソートでソートします。これは、サブリストが1つの要素になるまで繰り返されます。
3. 統合(マージ):再帰的にソートされた2つのサブリストを統合して、元のリストを1つのソート済みリストに戻します。これは、2つのソートされたリストから最小の要素を選択し、新しいソート済みリストに追加することで行われます。これを、両方のリストが空になるまで繰り返します。

マージソートの時間計算量は、O(n log n)です。ここでnはリストの要素数です。これは、分割と統合の両方のフェーズで要素のペアを比較しながらリストが分割および統合されるためです。この性能は、最悪の場合、平均の場合、および最良の場合のすべてのシナリオで保持されます。

マージソートは、安定性(同じ値を持つ要素の相対的な順序が維持される)という利点もあります。ただし、マージソートは、追加のメモリを必要とするため、インプレースソートアルゴリズム(追加のメモリを必要としないアルゴリズム)と比較すると、メモリ効率が低くなります。

このように、ChatGPTはマージソートがどのようなものであるのかを知っています。そして、これはChatGPTの利用ノウハウなのですが、このように1度ChatGPT自身に説明をさせた方が、その後の回答精度が向上する傾向があります。またユーザーの方も、このような説明を読むことで、分割・統治と再帰を利用しているといったマージソートの特徴を思い出すことができ、この後のやり取りをより良いものにしやすくなります。

では次に、マージソートをChatGPTがどのくらいきちんと理解できているか確認してみましょう。具体的には、ChatGPTにマージソートを実演してもらいます。

ユーザー:

数字を5つ適当に選んで下さい。

ChatGPT:

もちろんです。5つの適当な数字を選んでみます:

17, 42, 8, 23, 61

こちらが選んだ5つの数字です。どのようにお使いになりますか?

ユーザー:

それらの5つの数字を、マージソートで、小さい方が前に来るように並べて下さい。手順を示すこと。

ChatGPT:

マージソートを使用して、次の5つの数字をソートします:17, 42, 8, 23, 61。手順は以下の通りです。
1. 分割:
元のリスト:[17, 42, 8, 23, 61]
分割後のサブリスト:[17, 42, 8] と [23, 61]
2. 再帰的にソート:
a. サブリスト [17, 42, 8] をソート:
i. 分割:[17] と [42, 8]
ii. [17] は1つの要素だけなので、すでにソート済み
iii. サブリスト [42, 8] をソート:
分割:[42] と [8]
両方のサブリストが1つの要素だけなので、すでにソート済み
iv. マージ:[17] と [8, 42]
→ [8] と [17, 42]
→ [8, 17] と [42]
→ [8, 17, 42]

b. サブリスト [23, 61] をソート:
i. 分割:[23] と [61]
ii. 両方のサブリストが1つの要素だけなので、すでにソート済み
3. 統合(マージ):
サブリスト [8, 17, 42] と [23, 61] をマージ:
→ [8] と [17, 42] と [23, 61]
→ [8, 17] と [42] と [23, 61]
→ [8, 17, 23] と [42, 61]
→ [8, 17, 23, 42, 61]

最終的なソート済みリスト:[8, 17, 23, 42, 61]

ここでのChatGPTの利用ノウハウとして、「手順を示すこと」という1文の使用があります。この1文には2つの狙いがあります。1つめは、「Let's think step by step」の呪文と同様の効果で回答精度の向上を図る狙いです。2つめの狙いは、ソートの結果だけでなく、結果に至るまでの手順を見ることで、ChatGPTがマージソートをきちんと理解しているかを見るというものです。

ChatGPTのミスを見抜き、修正を依頼する

このChatGPTの回答を読んでミスの可能性に気づけるレベルまで専門知識がユーザーの血肉になっているかどうかが次の分かれ道になります。
具体的に言えば、先ほどChatGPTが示した手順を見て、「あれ?再帰的な処理になっているか怪しいぞ?キャッシュヒット率低くならない?空間計算量大丈夫?ソートするものの個数がちょっと増えただけで処理時間が一気に遅くなったりしない?」といったことが頭に浮かぶか、浮かばないかです。

先ほどは自然言語で簡易的に手順を示してもらっただけですので、軽量プログラミング言語を使ってChatGPTの頭の中をもう少し詳しく見せてもらいましょう。自然言語では学習データが多いほどベンチマークのスコアが良いことを考えますと、学習データが多そうなPythonあたりが良さそうです。

ユーザー:

この問題とこの手法でpythonコードを作成して下さい。

ChatGPT:

以下に、与えられた問題を解決するためのマージソートを実装したPythonコードを示します。

def merge_sort(arr):
    if len(arr) <= 1:
        return arr

    mid = len(arr) // 2
    left = arr[:mid]
    right = arr[mid:]

    left = merge_sort(left)
    right = merge_sort(right)

    return merge(left, right)

def merge(left, right):
    result = []
    left_index = 0
    right_index = 0

    while left_index < len(left) and right_index < len(right):
        if left[left_index] < right[right_index]:
            result.append(left[left_index])
            left_index += 1
        else:
            result.append(right[right_index])
            right_index += 1

    result += left[left_index:]
    result += right[right_index:]

    return result

# 与えられた数字のリスト
arr = [17, 42, 8, 23, 61]

# マージソートでリストをソート
sorted_arr = merge_sort(arr)

# 結果を表示
print(sorted_arr)

このコードを実行すると、次のソート済みリストが表示されます:`[8, 17, 23, 42, 61]`

このPythonコードでは再帰関数を用いた実装(オーソドックスで安心感のあるよくあるマージソートの実装)になっていることが分かります。まあ普通はこういう実装ですよね、ChatGPTさん。でも、先ほどの手順では再帰的かどうか少し怪しかったですよね、ChatGPTさん。ここはぜひChatGPTさんに、先ほどの手順とPythonコードとの違いについてどう思っているかを確認したいところです。どうすれば良いでしょうか?

と、こういうところで、「そうだ『参照の局所性』の観点から説明してもらおう」と、ユーザーがいい感じの専門用語を思い出せるとChatGPTとの対話はスムーズに行きます。

このちょっと難しいことを考えて説明してもらう前に、まずはChatGPTに参照の局所性について思い出してもらいましょう。

ユーザー:

参照の局所性について知っていますか?

ChatGPT:

はい、参照の局所性(Locality of reference)とは、コンピュータプログラムが実行される際にメモリへのアクセスが一部のメモリ位置に集中するという現象です。参照の局所性は、キャッシュや仮想メモリシステムなど、メモリ階層の設計において重要な役割を果たします。参照の局所性は主に次の3つのタイプに分類されます。
1. 時間的局所性(Temporal locality):
時間的局所性は、プログラムが最近参照したメモリ位置が近い将来に再び参照される可能性が高いことを示します。この性質は、キャッシュメモリにデータを保存することで効率的に利用できます。キャッシュメモリは、最近アクセスされたデータを高速アクセス可能な小さなメモリ領域に保持します。再利用されるデータがキャッシュに存在する場合、プロセッサは高速なキャッシュメモリからデータを取得でき、パフォーマンスが向上します。
2. 空間的局所性(Spatial locality):
空間的局所性は、プログラムがメモリのある位置にアクセスした場合、その周辺のメモリ位置にもアクセスする可能性が高いことを示します。これは、配列や構造体などの連続したメモリ領域にデータが格納されている場合に特に顕著です。空間的局所性を利用することで、キャッシュライン(キャッシュメモリ内の連続したデータブロック)を効率的に使用し、パフォーマンスを向上させることができます。
3. 相対的局所性(Sequential locality):
相対的局所性は、プログラムがメモリアクセスのパターンを繰り返すことを示します。これは、ループや再帰などの繰り返し処理によく現れます。相対的局所性を利用することで、キャッシュやプリフェッチ(予めメモリからキャッシュへデータを取得する)の戦略を最適化し、パフォーマンスを向上させることができます。

これらの局所性の原則を理解しておくことで、メモリアクセスを効率的に行い

参照の局所性についてChatGPTは分かっているようですので、ちょっと難しいかもしれませんが、参照の局所性の観点を投入して、先ほどの手順とPythonコードとの違いについてChatGPT自身に説明してもらいましょう。

ユーザー:

では、あなが先ほど提示した手順とPythonコードを、参照の局所性の観点から比較して、違いについて説明して下さい。

ChatGPT:

マージソートの手順と先ほど提示したPythonコードについて、参照の局所性の観点から比較して違いを説明します。
1. 時間的局所性:
手順において、マージソートは再帰的にサブリストをソートして統合するため、最近処理されたデータが再び参照される可能性が高いです。Pythonコードでは、`merge_sort`関数が再帰的に呼び出され、`merge`関数でデータが統合されるため、時間的局所性が発生します。キャッシュメモリを効率的に使用することで、パフォーマンスが向上する可能性があります。
2. 空間的局所性:
手順において、マージソートは連続したメモリ領域のデータを分割および統合するため、空間的局所性があります。Pythonコードでは、リスト`arr`がメモリ上で連続して配置されており、`merge`関数で左右のサブリストを連続した領域から参照しています。このため、空間的局所性を利用してキャッシュラインを効率的に使用し、パフォーマンスが向上します。
3. 相対的局所性:
マージソートの手順では、繰り返し処理が行われますが、そのパターンは固定ではありません。再帰的な処理が行われるため、アクセスパターンは入力データとアルゴリズムの状態によって異なります。Pythonコードでも、`merge_sort`関数と`merge`関数の再帰的呼び出しにより、相対的局所性が発生しますが、繰り返しアクセスパターンは入力データに依存します。

総じて、マージソートの手順とPythonコードでは、時間的局所性と空間的局所性が顕著であり、これらを利用してキャッシュ効率を向上させることができます。一方で、相対的局所性は入力データに依存するため、一定の繰り返しアクセスパターンが得られるとは限りません。

答えて欲しかったポイントが残念ながらよく分からない回答です。仕方がないので、こちらからズバリな指摘をして、手順を考え直してもらいましょう。

ユーザー:

先ほどあなたが示した手順では、まず分割を全て行っているため、Pythonコードと違い、空間的局所性について問題があると思います。この点を修正した手順を表示して下さい。

ChatGPT:

失礼いたしました。おっしゃる通り、先ほどの説明では分割が先に完了しているように見えました。こちらが空間的局所性を考慮した修正版の手順です。
1. 分割&再帰的にソート:
元のリスト:[17, 42, 8, 23, 61]
リストを分割し、それぞれ再帰的にマージソートでソート
a. [17, 42, 8] をソート:
i. [17] と [42, 8] に分割し、さらに分割
ii. [42] と [8] に分割
iii. [42] と [8] は1要素なので、そのまま統合:[8, 42]
iv. [17] と [8, 42] を統合:[8, 17, 42]

b. [23, 61] をソート:
i. [23] と [61] に分割
ii. 両方のサブリストが1要素なので、そのまま統合:[23, 61]
2. 統合(マージ):
サブリスト [8, 17, 42] と [23, 61] をマージ:
→ [8, 17, 23, 42, 61]

この修正版の手順では、分割と統合が同時に進行しており、空間的局所性が適切に考慮されています。この手順により、キャッシュラインの効率的な使用が期待でき、パフォーマンスが向上する可能性があります。

そうそう。こういう風に手順を示してくれれば再帰的に処理していることが明白で、局所性も活かせるし、問題も少なそうだよね、ChatGPTさん。

それでは、ChatGPTさんは一連の流れ・文脈をきちんと覚えていますので、ChatGPTさんにこのままの流れでさらに頭を使ってみてもらいましょう。

ユーザー:

より良い手順になったと思います。では、ここまでの流れを踏まえ、さきほどのPythonコードをさらに良くすることは出来ますか?

ChatGPT:

先ほどのPythonコードは、再帰的にマージソートを実行し、分割と統合が同時に進行しています。そのため、空間的局所性が適切に考慮されていると言えます。ただし、コードの最適化や可読性の向上を目指す場合、いくつかの改善点があります。以下に、改良版のPythonコードを示します。

def merge_sort(arr, start, end):
    if end - start <= 1:
        return

    mid = (start + end) // 2
    merge_sort(arr, start, mid)
    merge_sort(arr, mid, end)

    merge(arr, start, mid, end)

def merge(arr, start, mid, end):
    left = arr[start:mid]
    right = arr[mid:end]
    left_index, right_index = 0, 0

    for i in range(start, end):
        if right_index >= len(right) or (left_index < len(left) and left[left_index] < right[right_index]):
            arr[i] = left[left_index]
            left_index += 1
        else:
            arr[i] = right[right_index]
            right_index += 1

# 与えられた数字のリスト
arr = [17, 42, 8, 23, 61]

# マージソートでリストをソート
merge_sort(arr, 0, len(arr))

# 結果を表示
print(arr)

この改良版のコードでは、`merge_sort`関数に`start`と`end`という2つの引数を追加しています。これにより、元のリスト`arr`を分割する代わりに、部分リストを表すインデックスを使用します。この変更により、新しいリストの作成が減り、メモリ使用量が削減されます。また、`merge`関数も同様にインデックスを使用するように更新されています。

この改良版のコードは、空間的局所性が適切に考慮されており、さらにメモリ使用量も削減されています。これにより、パフォーマンスが向上する可能性があります。

確かにパフォーマンスが向上しそうなコードに改良されています。凄いぞ、ChatGPTさん。

リスクを察知し、ChatGPTに変更を求める

ここまで来たら、後は目的のプログラミング言語で最終的に利用するソースコードを書いてもらいましょう。今回はC言語のコードにしてもらいました。既に自然言語でアルゴリズムを確認し、Pythonも使ってパフォーマンスの検討もしてもらっていますので、この方向性のままさっくりとC言語で書き直してもらえば良いはずです。

ユーザー:

ありがとうございます。改良できていると思います。では、このPythonコードをC言語のコードにして下さい。

ChatGPT:

こちらがC言語でのマージソートの実装です。

#include <stdio.h>

void merge(int arr[], int start, int mid, int end) {
    int left_size = mid - start;
    int right_size = end - mid;
    int left[left_size], right[right_size];
    int left_index = 0, right_index = 0;

    for (int i = 0; i < left_size; i++) {
        left[i] = arr[start + i];
    }

    for (int i = 0; i < right_size; i++) {
        right[i] = arr[mid + i];
    }

    for (int i = start; i < end; i++) {
        if (right_index >= right_size || (left_index < left_size && left[left_index] < right[right_index])) {
            arr[i] = left[left_index];
            left_index++;
        } else {
            arr[i] = right[right_index];
            right_index++;
        }
    }
}

void merge_sort(int arr[], int start, int end) {
    if (end - start <= 1) {
        return;
    }

    int mid = (start + end) / 2;
    merge_sort(arr, start, mid);
    merge_sort(arr, mid, end);

    merge(arr, start, mid, end);
}

int main() {
    int arr[] = {17, 42, 8, 23, 61};
    int arr_size = sizeof(arr) / sizeof(arr[0]);

    merge_sort(arr, 0, arr_size);

    printf("Sorted array: ");
    for (int i = 0; i < arr_size; i++) {
        printf("%d ", arr[i]);
    }

    return 0;
}

このC言語のコードは、先ほどの改良版のPythonコードと同様のアプローチを使用しています。`merge_sort`関数と`merge`関数に`start`と`end`(および`mid`)という引数を追加し、部分リストを表すインデックスを使用しています。これにより、メモリ使用量が削減されます。

コードをコンパイルし実行すると、ソート済みの配列が表示されます:`Sorted array: 8 17 23 42 61`。

なかなか良さそうなC言語のコードが出てきました。実際にコンパイルして実行してみましょう。

$ clang -Wall gpt_mergesort_1.c
$ ./a.out
Sorted array: 8 17 23 42 61

エラーも警告もなくコンパイルに成功し、エラー無く実行もでき、出力結果を見るとソートも出来ています。

めでたし、めでたし・・・とはならないのが、一筋縄では行かないソフトウェア開発というものです。

ここまで手間をかけてきました。アルゴリズムの基本的な部分を確認しました。懸念点を指摘し、パフォーマンスの改善も行いました。
コンパイルではエラーも警告も出ていません。実行時エラーも発生していません。どこに問題があるのでしょうか?

答えを言うと、移植などでコンパイラを変更するとコンパイル出来なくなるリスクがあるのです。例えば、Windowsへの移植でMicrosoft製のコンパイラを使うと多分コンパイルできません。

原因は「可変長配列」というC言語の機能にあります。この機能、C99というC言語規格のバージョンで導入されたものの、C11でオプション扱いになりました。そのため2023年現在、可変長配列をサポートしないC言語コンパイラが規格上認められているのです。(現実問題としては、メジャーなコンパイラがどの程度言語規格を守っているかという話があり、さらにややこしい実態があります。この話題は、本題からそれますので、割愛します。)

ということで、このことを知っている人は、先ほどのC言語ソースコードを見て、将来コンパイルできなくなるリスクを感じてモヤッとするのですが、ChatGPTにそこまで先回りして配慮させるのは可哀想な話です。また、経験や知識が浅いプログラマはこのリスクには気が付かないでしょう。なにせ、メジャーなコンパイラであるclangを使ってエラーも警告も出なければ、実行時エラーもないのですから。(でもMicrosoftのコンパイラに持っていくと多分トラブルになります。)

では、C言語の知識や経験があるユーザーがこのリスクに気が付いた場合はどうすれば良いかというと、ChatGPTさんに一言懸念を伝えるだけで問題が解決します。

ユーザー:

ありがとうございます。ただ、C11では可変長配列がオプションに格下げになったことは知っていますか?

ChatGPT:

はい、C11(C言語の2011年の標準)では、可変長配列(Variable Length Array, VLA)がオプションとして取り扱われており、そのサポートはコンパイラによって異なります。VLAを使用せずに動的にメモリを割り当てる方法として、malloc関数を使用することができます。以下に、VLAを使わずにmallocを用いたC言語のコードを示します。

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

void merge(int arr[], int start, int mid, int end) {
    int left_size = mid - start;
    int right_size = end - mid;
    int *left = (int *)malloc(left_size * sizeof(int));
    int *right = (int *)malloc(right_size * sizeof(int));
    int left_index = 0, right_index = 0;

    for (int i = 0; i < left_size; i++) {
        left[i] = arr[start + i];
    }

    for (int i = 0; i < right_size; i++) {
        right[i] = arr[mid + i];
    }

    for (int i = start; i < end; i++) {
        if (right_index >= right_size || (left_index < left_size && left[left_index] < right[right_index])) {
            arr[i] = left[left_index];
            left_index++;
        } else {
            arr[i] = right[right_index];
            right_index++;
        }
    }

    free(left);
    free(right);
}

void merge_sort(int arr[], int start, int end) {
    if (end - start <= 1) {
        return;
    }

    int mid = (start + end) / 2;
    merge_sort(arr, start, mid);
    merge_sort(arr, mid, end);

    merge(arr, start, mid, end);
}

int main() {
    int arr[] = {17, 42, 8, 23, 61};
    int arr_size = sizeof(arr) / sizeof(arr[0]);

    merge_sort(arr, 0, arr_size);

    printf("Sorted array: ");
    for (int i = 0; i < arr_size; i++) {
        printf("%d ", arr[i]);
    }

    return 0;
}

このコードでは、leftおよびright配列のメモリを動的に割り当てるためにmalloc関数を使用しています。統合操作が終了した後、free関数を使用して割り当てたメモリを解放しています。この方法で、VLAを使わずにメモリを動的に割り当てることができ、C11での可変長配列のオプション扱いに関する問題を回避できます。

懸念点をさらっと伝えただけで、きちんと修正されたコードが提案されました。ChatGPTさん、優秀ですね。

終わりに

ご覧いただいたとおり、ChatGPTさんは優秀です。専門用語を使ってズバリな指摘をすれば、期待に十分応える返答をしてくれます。したがって、ユーザーの側が専門知識を体系的にきちんと身につけていればいるほど、スムーズに対話が行え、短時間で高品質なソフトウェアを開発できると期待できます。
また、ミスやリスクの観点からも、ユーザー側に専門知識が求められると考えられます。最新のGPT-4があいうえお順をポカミスするというのが2023年3月における現在地であり、徐々にGPTのミスは減っていくのでしょうが、GPTが絶対にミスをせずリスクも全て自動で回避するようになるのはまだまだ時間がかかりそうです。(と言いますか、学習データ中の誤りやGPTを操作する際のヒューマンエラーまで考えれば、GPTの結果を鵜呑みに出来る未来はやって来ないと思います。)したがって、ミスが完全に無くなるまで待つという戦略には無理がありそうです。GPTの導入をためらった個人や企業は、GPTのミスを人間がカバーしながらGPTを使いこなす個人や企業に競争の中で敗れ去ってしまうのではないでしょうか。
しかしながら、高い専門性を身に付けた一握りの鬼がGPTという金棒を手にして富を独占する未来は見たくありません。
AIの時代にこそ、専門知識を体系的にきちんと身につけられること、つまり、教育が重要なのではないか。プログラミング領域でChatGPTの活用を試みる中で、この思いが強まっています。
優れた教育の機会が全ての人に開かれており、全ての人にGPTを活用するチャンスがあること、そして、GPTによって全ての人の生活水準が底上げされることを願ってやみません。

この記事が参加している募集

#AIとやってみた

28,976件

この記事が気に入ったらサポートをしてみませんか?