LoginSignup
5
4
お題は不問!Qiita Engineer Festa 2023で記事投稿!

ChatGPTとCode interpreterを用いた効率的なコードコメント生成!実用的なプロンプトの紹介

Last updated at Posted at 2023-07-10

1. はじめに

ChatGPT の Code interpreter 機能が公開されて,ついにファイルの送付とコードの実行が ChatGPT 上でできるようになりました!

ChatGPT にファイルを送ることができるようになったので、この記事では,これまで個人的に ChatGPT に一番依頼することが多かった

「コードにコメントの追加をお願いする」

作業について、Code interpreter を使ってどのように変わるのかを紹介したいと思います。

Code interpreter を使わなくても使用できる、きれいなコメントを出してくれるプロンプトも併せて整備してみたので、参考にしていただければと思います。

1.1. Code interpreter を使う前...

Code interpreter を使う前は,以下のように冒頭にプロンプトを書いてから、その下にコードを貼り付けていました。

スクリーンショット 2023-07-10 200807.png

コードが短いときは苦労しないですが、文字数制限によりコードを1度に送ることができない場合、何回かに分ける必要がある上に、ChatGPTが途中で回答しないように「待て」を指示するのがなかなか難しい作業でした。

これが Code interpreterを使うことで、直接ファイルを送ることができるようになりました。

1.2. ファイル容量の制約

送れるファイルの容量についてChatGPTに聞いて見たところ、以下とのことでした。

スクリーンショット 2023-07-10 200956.png

本当に500MBも送れるのであれば、なにも気にせずにコードを送れますね!

2. プロンプトの紹介

2.1. 今回使用するコード

今回は題材として、私が以下の記事で公開した GUI 用のコードにコメントを追加してもらいます。

plot.png

このコードにはもともとコメントはあったのですが、ここでは結果をわかりやすくするために、ChatGPTに送付する前にすべてのコメントを削除しました。

plot_control.py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

class PlotControl:

    def __init__(self):

        self.fig = plt.figure()

        self.ax = self.fig.add_subplot(1, 1, 1)

        self.config = {"linewidth":2, "linetype":"line"}
        self.filepath = None
        self.df = None

    def replot(self, filename=None, config=None):
        if filename is not None:
            self.filepath = filename
            self.df = pd.read_csv(filename)
        if self.df is None:
            return

        if config is not None:
            self.config.update(config)
        
        self.ax.clear()

        if self.config["linetype"] == "line + marker":
            self.ax.plot(self.df["time"], self.df["y"], "o-", linewidth=self.config["linewidth"])
        elif self.config["linetype"] == "dashed":
            self.ax.plot(self.df["time"], self.df["y"], "--", linewidth=self.config["linewidth"])
        else:
            self.ax.plot(self.df["time"], self.df["y"], linewidth=self.config["linewidth"])

        self.ax.set_xlabel("time")
        self.ax.set_ylabel("y")

    def save_fig(self, export_path=None):
        if export_path is None and self.filepath is not None:
            file, ext = os.path.splitext(self.filepath)
            export_path = f"{file}.png"

        if export_path is None:
            return
        
        self.fig.savefig(export_path)
        return export_path

if __name__ == "__main__":
    filename = "sample_data.csv"
    time = np.linspace(0.0, 3.0)
    y = np.sin(2*np.pi*time)
    out_df = pd.DataFrame(data={"time": time, "y": y})
    out_df.to_csv(filename)

    plot_control = PlotControl()
    plot_control.replot(filename)
    plot_control.save_fig("test1.png")

    config = plot_control.config.copy()
    config["linewidth"] = 5
    plot_control.replot(config=config)
    plot_control.save_fig("test2.png")

2.2. プロンプトの紹介

使用したプロンプトは以下になります。

スクリーンショット 2023-07-10 184826.png
(中略)
スクリーンショット 2023-07-10 184726.png

プロンプト全文(コピペ用。長いので折りたたみます)
Pythonファイルを送ります。以下のルールと例に従いコメントを追加して返してください。解説は不要です。

## コメントのルール

1. 各関数や主要な部分について詳細に説明してください。
2. すべての行について詳細にコメントする必要はありませんが、重要な部分や主要な機能を持つ部分にはコメントを追加してください。
3. 関数やクラスのコメントは、NumPy スタイルの docstringで書いてください。
4. 読み手は日本人なので、コメントも日本語で書いてください。
5. 変数の型やコードの処理等に、わからない点がある場合は、コードから推測してくだい。


<回答例>

"""
def train_and_evaluate(features_train, labels_train, features_test, labels_test):
    """
    One-class SVMモデルで訓練、評価を行う。

    Parameters
    ----------
    features_train : list
        訓練データの特徴量
    labels_train : list
        訓練データのラベル
    features_test : list
        テストデータの特徴量
    labels_test : list
        テストデータのラベル

    Returns
    -------
    auc : float
        AUCスコア
    cm : ndarray
        混同行列
    """

    # モデル(One-class SVM)を訓練
    one_class_svm = svm.OneClassSVM(nu=0.1, kernel="rbf", gamma=0.1)
    one_class_svm.fit(features_train)

    # 訓練データに対するモデルのスコアを取得
    scores_train = one_class_svm.decision_function(features_train)

    # テストデータに対するモデルのスコアを取得
    scores_test = one_class_svm.decision_function(features_test)

    # 異常検知において、スコアが低いほど異常とみなす
    # そのため、スコアが0未満のものは-1(異常)、それ以外は1(正常)とする
    labels_test_pred = [-1 if score < 0 else 1 for score in scores_test]

    # 評価指標(AUCと混同行列)を計算
    # ラベルは'normal'なら1、それ以外('abnormal')なら-1として数値に変換
    auc = metrics.roc_auc_score(labels_test, scores_test)
    labels_test_num = [1 if item == 'normal' else -1 for item in labels_test]
    cm = metrics.confusion_matrix(labels_test_num, labels_test_pred)
    
    # ROC曲線をプロット
    # plot_roc_curve(labels_test_num, scores_test, auc)

    return auc, cm
"""

依頼事項は以上です。準備はいいですか?はい/いいえで答えてください。

プロンプトのポイント

  • 現状のバグなのかプロンプトと一緒にコードを送付すると、送付したコードとは別の(どこかから拾ってきた?)コードについて解説を始めることがありました。送ったコードを確実に解析してもらうために、一度プロンプトを切って、ChatGPT に確認を行っています。
  • 個人的にNumPy スタイルが好きなので、NumPy スタイルを選んでいますが、ここは好みなので好みに応じて変更してください(回答例も調整してください)。

このあと、以下のようにファイルを送付します。

スクリーンショット 2023-07-10 184746.png

「冒頭のルールに従って、コメントを追加してください。」とちゃんと書かないと、冒頭に書いた指示をちゃんと守ってくれないことがありました。

得られたコードが下記になります。

plot_control_comment.py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

class PlotControl:
    """
    プロットの制御を行うクラスです。
    """
    def __init__(self):
        # フィギュアとサブプロットの作成
        self.fig = plt.figure()
        self.ax = self.fig.add_subplot(1, 1, 1)

        # プロット設定とCSVファイルパス、データフレームの初期化
        self.config = {"linewidth":2, "linetype":"line"}
        self.filepath = None
        self.df = None

    def replot(self, filename=None, config=None):
        """
        プロットを更新します。

        Parameters
        ----------
        filename : str, optional
            CSVファイルのパス。指定がない場合、前回と同じパスを使用します。
        config : dict, optional
            プロット設定。指定がない場合、前回と同じ設定を使用します。
        """
        if filename is not None:
            # CSVファイルの読み込み
            self.filepath = filename
            self.df = pd.read_csv(filename)
        if self.df is None:
            return

        if config is not None:
            # プロット設定の更新
            self.config.update(config)
        
        # プロットのクリアと再プロット
        self.ax.clear()
        if self.config["linetype"] == "line + marker":
            self.ax.plot(self.df["time"], self.df["y"], "o-", linewidth=self.config["linewidth"])
        elif self.config["linetype"] == "dashed":
            self.ax.plot(self.df["time"], self.df["y"], "--", linewidth=self.config["linewidth"])
        else:
            self.ax.plot(self.df["time"], self.df["y"], linewidth=self.config["linewidth"])
        self.ax.set_xlabel("time")
        self.ax.set_ylabel("y")

    def save_fig(self, export_path=None):
        """
        プロットを画像ファイルとして保存します。

        Parameters
        ----------
        export_path : str, optional
            出力ファイルパス。指定がない場合、読み込んだCSVファイルと同じパス(拡張子を.pngに変更)に保存します。

        Returns
        -------
        export_path : str or None
            出力ファイルパス。出力パスが指定されていない場合、Noneを返します。
        """
        if export_path is None and self.filepath is not None:
            # 出力ファイルパスの生成
            file, ext = os.path.splitext(self.filepath)
            export_path = f"{file}.png"

        if export_path is None:
            return
        
        # プロットの保存
        self.fig.savefig(export_path)
        return export_path

if __name__ == "__main__":
    """
    メインエントリーポイント。このスクリプトが直接実行されたときにサンプルのプロットを生成、保存します。
    """
    # サンプルデータの生成とCSV出力
    filename = "sample_data.csv"
    time = np.linspace(0.0, 3.0)
    y = np.sin(2*np.pi*time)
    out_df = pd.DataFrame(data={"time": time, "y": y})
    out_df.to_csv(filename)

    # プロット制御オブジェクトの作成とプロットの更新、保存
    plot_control = PlotControl()
    plot_control.replot(filename)
    plot_control.save_fig("test1.png")

    # プロット設定の変更とプロットの更新、保存
    config = plot_control.config.copy()
    config["linewidth"] = 5
    plot_control.replot(config=config)
    plot_control.save_fig("test2.png")

素晴らしいコメントです。はじめは、自分が書いたコードと比較しようかなと思っていたのですが、恥ずかしくなったのでやめました。特に,

  • export_path : str, optional
    出力ファイルパス。指定がない場合、読み込んだCSVファイルと同じパス(拡張子を.pngに変更)に保存します。

の部分ですが、コードの中身をちゃんと解析して、書いてくれています。
自分は引数の説明にここまで書くことはあまりしないので、わかりやすくてよいですね。

2.3. 別の出力例

GUI のメインファイルである plot_view_gui.py を送った結果も貼っておきます。
こちらは、先ほどのコードよりも長い(65行→171行)ので、一つの回答におさまらず何個かに分割されました。

長いので折りたたみます。
plot_view_gui.py
import tkinter as tk
import customtkinter
import os
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from plot_control import PlotControl

# GUIのフォント設定
FONT_TYPE = "meiryo"

class App(customtkinter.CTk):
    """
    メインのアプリケーションクラス。GUIの生成と更新を担当します。
    """
    def __init__(self):
        super().__init__()

        # フォントの設定
        self.fonts = (FONT_TYPE, 15)
        # CSVファイルのパスを保持
        self.csv_filepath = None

        # GUIの形成
        self.setup_form()

    def setup_form(self):
        """
        GUIの形成を行います。
        """
        # GUIの外観設定
        customtkinter.set_appearance_mode("dark")  # Modes: system (default), light, dark
        customtkinter.set_default_color_theme("blue")  # Themes: blue (default), dark-blue, green

        # ウィンドウサイズとタイトルの設定
        self.geometry("1000x600")
        self.title("CSV plot viewer")
        self.minsize(300, 400)

        # フレームの配置
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(0, weight=1)

        # ファイル読み込みフレーム
        self.read_file_frame = ReadFileFrame(master=self, header_name="ファイル読み込み")
        self.read_file_frame.grid(row=0, column=0, padx=20, pady=20, sticky="ew")

        # プロット表示フレーム
        self.plot_main_frame = PlotMainFrame(master=self, header_name="プロット表示")
        self.plot_main_frame.grid(row=1, column=0, padx=20, pady=(0,10), sticky="nsew")

    def update_canvas(self, csv_filepath=None):
        """
        プロットを更新します。

        Parameters
        ----------
        csv_filepath : str, optional
            CSVファイルのパス。指定がない場合、前回と同じパスを使用します。
        """
        if csv_filepath is not None:
            self.csv_filepath = csv_filepath
        # プロット更新
        self.plot_main_frame.update(csv_filepath=self.csv_filepath)


class ReadFileFrame(customtkinter.CTkFrame):
    """
    ファイル読み込み部分のフレームを管理します。
    """
    def __init__(self, *args, header_name="ReadFileFrame", **kwargs):
        super().__init__(*args, **kwargs)
        
        # フォントとヘッダー名の設定
        self.fonts = (FONT_TYPE, 15)
        self.header_name = header_name

        # GUIの形成
        self.setup_form()

    def setup_form(self):
        """
        GUIの形成を行います。
        """
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

        # ヘッダーラベル
        self.label = customtkinter.CTkLabel(self, text=self.header_name, font=(FONT_TYPE, 11))
        self.label.grid(row=0, column=0, padx=20, sticky="w")

        # ファイルパス入力テキストボックス
        self.textbox = customtkinter.CTkEntry(master=self, placeholder_text="CSV ファイルを読み込む", width=120, font=self.fonts)
        self.textbox.grid(row=1, column=0, padx=10, pady=(0,10), sticky="ew")

        # ファイル選択ボタン
        self.button_select = customtkinter.CTkButton(master=self, 
            fg_color="transparent", border_width=2, text_color=("gray10", "#DCE4EE"),   # ボタンを白抜きにする
            command=self.button_select_callback, text="ファイル選択", font=self.fonts)
        self.button_select.grid(row=1, column=1, padx=10, pady=(0,10))
        
        # ファイルオープンボタン
        self.button_open = customtkinter.CTkButton(master=self, command=self.button_open_callback, text="開く", font=self.fonts)
        self.button_open.grid(row=1, column=2, padx=10, pady=(0,10))

    def button_select_callback(self):
        """
        ファイル選択ボタンが押されたときのコールバック関数。
        """
        file_name = ReadFileFrame.file_read()

        if file_name is not None:
            # 選択されたファイルパスをテキストボックスに表示
            self.textbox.delete(0, tk.END)
            self.textbox.insert(0, file_name)

    def button_open_callback(self):
        """
        ファイルオープンボタンが押されたときのコールバック関数。
        """
        if self.textbox.get() is not None:
            # テキストボックスからCSVファイルのパスを取得し、プロットを更新
            csv_filepath = self.textbox.get()
            self.master.update_canvas(csv_filepath)
            
    @staticmethod
    def file_read():
        """
        ファイル選択ダイアログを開き、選択されたファイルのパスを返します。

        Returns
        -------
        file_path : str or None
            選択されたファイルのパス。選択がキャンセルされた場合はNoneを返します。
        """
        # ファイル選択ダイアログを開く
        current_dir = os.path.abspath(os.path.dirname(__file__))
        file_path = tk.filedialog.askopenfilename(filetypes=[("csvファイル","*.csv")],initialdir=current_dir)

        if len(file_path) != 0:
            return file_path
        else:
            return None
        
class PlotMainFrame(customtkinter.CTkFrame):
    """
    プロット表示部分のフレームを管理します。
    """
    def __init__(self, *args, header_name="PlotMainFrame", **kwargs):
        super().__init__(*args, **kwargs)
        
        # フォントとヘッダー名の設定
        self.fonts = (FONT_TYPE, 15)
        self.header_name = header_name

        # プロット制御オブジェクト
        self.plot_control = PlotControl()

        # GUIの形成
        self.setup_form()

    def setup_form(self):
        """
        GUIの形成を行います。
        """
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)

        # プロット設定フレーム
        self.plot_edit_frame = PlotConfigFrame(master=self, header_name="プロット設定", plot_config=self.plot_control.config)
        self.plot_edit_frame.grid(row=0, column=0, padx=20, pady=20, sticky="ns")

        # プロット表示キャンバス
        self.canvas = FigureCanvasTkAgg(self.plot_control.fig,  master=self)
        self.canvas.get_tk_widget().grid(row=0,column=1, padx=20, pady=20, sticky="nsew")

        # 保存ボタン
        self.button_save = customtkinter.CTkButton(master=self, command=self.button_save_callback, text="保存", font=self.fonts)
        self.button_save.grid(row=0, column=2, padx=10, pady=20, sticky="s")   

    def update(self, csv_filepath=None, config=None):
        """
        プロットを更新します。

        Parameters
        ----------
        csv_filepath : str, optional
            CSVファイルのパス。指定がない場合、前回と同じパスを使用します。
        config : dict, optional
            プロット設定。指定がない場合、前回と同じ設定を使用します。
        """
        # プロット更新
        self.plot_control.replot(csv_filepath, config)
        self.canvas.draw()
    
    def button_save_callback(self):
        """
        保存ボタンが押されたときのコールバック関数。
        """
        # プロットを保存し、保存結果をメッセージボックスで表示
        filepath = self.plot_control.save_fig()
        if filepath is not None:
            tk.messagebox.showinfo("確認", f"{filepath} に出力しました。")

class PlotConfigFrame(customtkinter.CTkFrame):
    """
    プロット設定部分のフレームを管理します。
    """
    def __init__(self, *args, header_name="PlotConfigFrame", plot_config=None, **kwargs):
        super().__init__(*args, **kwargs)
        
        # フォントとヘッダー名の設定、プロット設定のコピー
        self.fonts = (FONT_TYPE, 15)
        self.header_name = header_name
        self.plot_config = plot_config.copy()

        # GUIの形成
        self.setup_form()

    def setup_form(self):
        """
        GUIの形成を行います。
        """
        self.grid_rowconfigure(0, weight=0)
        self.grid_columnconfigure(0, weight=1)

        # ヘッダーラベル
        self.label = customtkinter.CTkLabel(self, text=self.header_name, font=(FONT_TYPE, 11))
        self.label.grid(row=0, column=0, padx=20, sticky="nw")

        # ライン幅設定のラベルとスライダー
        self.slider_label = customtkinter.CTkLabel(self, text="ライン幅 2.5", font=(FONT_TYPE, 13))
        self.slider_label.grid(row=1, column=0, padx=20, pady=(20,0), sticky="ew")
        self.slider = customtkinter.CTkSlider(master=self, from_=0.5, to=5, number_of_steps=9, hover=False, width=150, command=self.slider_event)
        self.slider.grid(row=2, column=0, padx=20, pady=(0,20), sticky="ew")

        # 線種設定のラベルとコンボボックス
        self.combobox_label = customtkinter.CTkLabel(self, text="線種", font=(FONT_TYPE, 13))
        self.combobox_label.grid(row=3, column=0, padx=20, pady=(20,0), sticky="ew")
        self.combobox = customtkinter.CTkComboBox(master=self, font=self.fonts,
                                     values=["line", "dashed", "line + marker"],
                                     command=self.combobox_callback)
        self.combobox.grid(row=4, column=0, padx=20, pady=(0,20), sticky="ew")

    def slider_event(self, value):
        """
        ライン幅スライダーの値が変更されたときのイベントハンドラ。

        Parameters
        ----------
        value : str
            スライダーの新しい値。
        """
        old_label = self.slider_label.cget("text")
        new_label = f"ライン幅 {value}"
        if old_label != new_label:
            # ラベルとプロット設定を更新し、プロットを更新
            self.slider_label.configure(text=new_label)
            self.plot_config["linewidth"] = value
            self.master.update(config=self.plot_config)
    
    def combobox_callback(self,value):
        """
        線種コンボボックスの値が変更されたときのコールバック関数。

        Parameters
        ----------
        value : str
            コンボボックスの新しい値。
        """
        # プロット設定を更新し、プロットを更新
        self.plot_config["linetype"] = value
        self.master.update(config=self.plot_config)


if __name__ == "__main__":
    """
    メインエントリーポイント。このスクリプトが直接実行されたときにアプリケーションを起動します。
    """
    app = App()
    app.mainloop()

こちらも、完璧なコメントです!

2.4. ファイルに出力する

以下のようにお願いすると、Code interpreter の機能によりファイルに出力することもできます。

スクリーンショット 2023-07-10 210925.png

一見便利なのですが、使ってみると、以下の注意点がありました。

  1. 折りたたまれた部分を開いてみると、裏でいつものように応答を書いているようです。そのため、回答速度は画面に出すのとあまり変わらないです。

    スクリーンショット 2023-07-10 184425.png

  2. 1のように裏で応答を書いているためか、長いファイルは出力することができずにエラーが発生しました。

個人的にはエラーが出たときに困るので、これまで通り画面に書いてもらう形で使用しています。

3. おわりに

プラグイン機能はそこまで驚かなかったのですが、Code interpreterには感激しました。

寝てる場合じゃねえと、急いで2本書いたので、もう一本もよろしければどうぞ!

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4