Ubuntu日和

【第62回】LM StudioでPythonのGUIコードを生成させたら殊の外優秀だった

LM StudioでPython(tkinter)のコードを生成しているところ

 筆者はプログラマーというわけではないが、たまにプログラムを書く。主に使用する言語はPythonだ。

 とはいえ、ライブラリをいい感じに組み合わせて動かしているだけで、それをプログラムと呼んでいいのか自分でも葛藤があるくらいだ。自信はまったくないし、各ライブラリの作者にいくら感謝してもし足りない。

 GUIを表示するアプリケーションはあまり書く機会がないものの、ゼロではない。そういった際には、PythonデフォルトのGUIライブラリであるtkinterを使用している。正直なところ見た目はあまりよくないが、情報が豊富でライセンスに関してあまり難しく考えなくてもいいところが気に入っている。

 もともとプログラム自体が得意というわけではないので、何であってもGUIをイチから書くというのはなかなかにしんどく、を買って勉強したりしたものの、苦手感を払拭するには至っていない。まぁそれでも書くのだが。

 ある日、本誌の担当編集である劉デスクからLM StudioというローカルLLMを動作させるアプリケーションを紹介された。いろいろと遊んでいるうちに、こいつでGUIを生成させてみるとどのくらいの精度のものができるか気になった。そして、今回の記事がその成果となる。

今回使用するハードウェアとソフトウェア

 今回使用するハードウェアは次の通りだ。

分類メーカー型番
CPUIntelCore i5-13500
メモリーCrucialCT32G4DFD832A
マザーボードASRockB660M Pro RS
グラフィックボードMSIGeForce RTX 3060 AERO ITX 12G OC
SSDWDCEDS500G2B0C
ケースSilver StoneSST-SG11B
電源玄人志向KRPW-SXP600W/90+

 なんのことはない筆者の検証用PCだが、ポイントはGeForce RTX 3060を使用しているところだ。今回はNVIDIAのVRAMを12GB以上搭載したdGPUをターゲットとする。VRAMが8GB以下では、実用的なLLMを動作させるにはやや心許ない。

 執筆時点でのSystem Requirementsによると、Ubuntuのバージョンは20.04 LTSと22.04 LTSで、それより新しいバージョンはあまりテストしていないということだ。したがって今回は22.04 LTSを使用する。

 事前にプロプライエタリなドライバをインストールしておこう。「追加のドライバー」で確認できる。

「追加のドライバー」でバージョン550を使用していることを確認しているところ

ダウンロードと実行

 LM Studioはインストール不要なAppImageという形式で提供されている。AppImageに関しては第4回で紹介しているので、ここでは繰り返さない。

 LM StudioのサイトからLinux版をダウンロードしよう。

LM Studioのダウンロード

 ただし実行するには libfuse2 というパッケージが必要なので、起動前にインストールしておこう。

 実行するにはダウンロードしたファイルを右クリックして「アクセス権」にある「プログラムとして実行」にチェックを入れ、閉じたあとダブルクリックするだけだ。

コマンドラインを実行せずに実行権限を与える

設定の確認とモデルのダウンロード

 起動したら、右上の「Skip onboarding」をクリックしてウィザードを飛ばしてしまおう。紹介の都合上その方がいいからで、そのまま進んでしまって何か問題があるわけではない。

LM Studioの初期ウィンドウ

 通常の画面に遷移したら、何はなくとも右下の歯車をクリックする。

 「App Settings」が開くので、まずは「言語」を「日本語 (Beta)」に変更しよう。

表示言語の変更から

 続いて「Runtimes」をクリックし、「CUDA llama.cpp」が選択されていることを確認しよう。このようになっていない場合、NVIDIAのプロプライエタリなドライバが正しくインストールされていないことが考えられる。

ランタイムの確認

 「Model Search」をクリックし、モデル(いうまでもなくLLMのMだ)をダウンロードする。今回は「Qwen2.5-14B-Instruct」をダウンロードする。9GBくらいあるので、それなりに時間がかかるだろう。サブウィンドウが表示され、進捗が表示されるのは分かりやすい。

使用するモデルのダウンロード

 14B、14ビリオン、すなわち140億パラメータから構成されている。この規模のLLMを動かそうとすると、VRAMが12GB以上必要だ。よって最低でもGeForce RTX 3060程度は必要となる。少ないパラメータ数でコードに特化したQwen2.5-Coder-7B-Instructもあるが、やはりパラメータ数が少ないのはいかんともしがたく、あまりおすすめできないと判断した。

 ダウンロードが完了し、モデルを読み込むと使用可能となる。その際に「GPUオフロード」を最大値にするを忘れないでおこう。

「GPUオフロード」を確認する
モデルを読み込んだところ

tkinterのウィンドウを生成する

 では実際にtkinterを用いたPythonのコードを生成していく。

 ここでは、ボタンをクリックするとクリックしたボタンのメッセージが下部に表示されるウィンドウを作成することにする。具体的には次のようなものだ。

完成サンプル

 これをゴールに、最初に入力したのは「tkinterでボタンが2つあるウィンドウのソースコードを教えてください。なおボタンはgridで横に並べてください。ウィンドウサイズを変更したら、ボタンの位置も変更するようにしてください。」だ。

問い合わせて結果が表示されたところ

 生成したコードをgeditなどで保存する。ここではファイル名を「sampledialog.py」とする。動作確認のため、端末を起動しtkinterをインストールする。具体的には sudo apt install python3-tk を実行しよう。

 完了後、 python3 sampledialog.py を実行すると、次のようなウィンドウが表示された。

サンプル1

 なんと、ウィンドウが2つ表示されてしまっている。実はありがちなミスなので、「ウィンドウが2つ表示されています。1つは不要です。」と入力してやり直してもらう。

サンプル2

 無事に修正された。ボタンの距離が近すぎるので、「ボタンの左右に等間隔のスペースがほしいです」と入力した。

サンプル3

 ボタンの左側にはスペースができたが、右側にもほしい。「ボタンの右側にも等間隔のスペースを入れてください」と入力した。

サンプル4

 これだと何をしてほしいウィンドウかわからないので、「ボタンの上に、「どちらかのボタンをクリックしてください」というメッセージを追加してください」と入力した。

サンプル5

 「ボタンをクリックしたら、ボタンの下にテキストを表示するようにしてください。テキストの内容はボタンに書かれているものと同一にしてください」と入力し、完成形に近づけた。

サンプル6

 ボタンをクリックすると次のようになった。

ボタンをクリックして、表示内容が下部に表示されているところ

 下に余白がほしいので、「ボタンの下のテキストの下にもう少し余白がほしいです」と入力した。

サンプル7

 ウィンドウとしてはこれで完成だ。

 この段階でのソースコードを見てみよう。

import tkinter as tk

def create_button_panel(root):
    # ボタンパネルを作成するフレーム
    button_frame = tk.Frame(root)

    # 5つの列を定義: 左のマージン, ボタン1, 中央の余白, ボタン2, 右のマージン
    for col in range(5):
        button_frame.grid_columnconfigure(col, weight=1)  # 各列に均等な重みを設定

    # メッセージラベルを作成
    message_label = tk.Label(button_frame, text="どちらかのボタンをクリックしてください", pady=10)
    message_label.grid(row=0, columnspan=5)

    # 余白フレーム作成
    left_margin = tk.Frame(button_frame, width=50)  # 左端の余白
    right_margin = tk.Frame(button_frame, width=50)  # 右端の余白
    central_margin = tk.Frame(button_frame, width=50)  # 中央の余白

    # ボタンを作成
    def button_action(text):
        label_text.set(text)

    label_text = tk.StringVar()

    button1_label = "Button 1"
    button2_label = "Button 2"

    button1 = tk.Button(button_frame, text=button1_label, command=lambda: button_action(button1_label))
    button2 = tk.Button(button_frame, text=button2_label, command=lambda: button_action(button2_label))

    # テキストラベルを作成
    label = tk.Label(button_frame, textvariable=label_text)

    # 左端の余白フレーム配置
    left_margin.grid(row=1, column=0, sticky='nsew')

    # ボタン配置
    button1.grid(row=1, column=1, sticky=tk.W+tk.E)
    central_margin.grid(row=1, column=2, sticky='nsew')  # 中央の余白フレーム配置
    button2.grid(row=1, column=3, sticky=tk.W+tk.E)

    label.grid(row=2, columnspan=5)  # テキストラベルを下部に配置

    # 下部の余白フレームを作成と配置
    bottom_margin = tk.Frame(button_frame, height=20)
    bottom_margin.grid(row=3, columnspan=5, sticky='ew')  # 下部の余白

    # 右端の余白フレーム配置
    right_margin.grid(row=1, column=4, sticky='nsew')

    return button_frame

# Tkinter のルートウィンドウを作成
root = tk.Tk()
root.title("Button Dialog")

# ボタンパネルをメインウィンドウに配置
button_panel = create_button_panel(root)
button_panel.pack(fill=tk.X)  # X 方向(幅)に張り出すように表示

# Tkinter イベントループを開始
root.mainloop()

 関数の中に関数があるという、Pythonのソースコードとしてはかなり違和感があるものであった。よって「関数の中に関数があるのは不自然ではないでしょうか」とお願いしたら、何回かは動かない(エラーを出力する)ソースコードを出力し続け、根気強くエラーを提示して修正をお願いすると、最終的に次のようなソースコードを出力した。

import tkinter as tk

def handle_button_click(label_var, button_text):
    def inner():
        label_var.set(button_text)
    return inner

def create_button_panel(root):
    # ボタンパネルを作成するフレーム
    button_frame = tk.Frame(root)

    # 5つの列を定義: 左のマージン, ボタン1, 中央の余白, ボタン2, 右のマージン
    for col in range(5):
        button_frame.grid_columnconfigure(col, weight=1)  # 各列に均等な重みを設定

    # メッセージラベルを作成
    message_label = tk.Label(button_frame, text="どちらかのボタンをクリックしてください", pady=10)
    message_label.grid(row=0, columnspan=5)

    label_text = tk.StringVar()  # StringVar を使用してテキスト変更を管理

    # テキストラベルを作成
    label = tk.Label(button_frame, textvariable=label_text)  # 初期設定時は空白文字列を設定

    label.config(pady=5)  # 余白を追加

    label.grid(row=2, columnspan=5)  # テキストラベルを下部に配置

    # 余白フレーム作成
    left_margin = tk.Frame(button_frame, width=50)  # 左端の余白
    central_margin = tk.Frame(button_frame, width=50)  # 中央の余白
    right_margin = tk.Frame(button_frame, width=50)  # 右端の余白

    # ボタンを作成
    button1_label = "Button 1"
    button2_label = "Button 2"

    button1 = tk.Button(button_frame, text=button1_label, command=handle_button_click(label_text, button1_label))
    button2 = tk.Button(button_frame, text=button2_label, command=handle_button_click(label_text, button2_label))

    # 左端の余白フレーム配置
    left_margin.grid(row=1, column=0, sticky='nsew')

    # ボタン配置
    button1.grid(row=1, column=1, sticky=tk.W+tk.E)

    central_margin.grid(row=1, column=2, sticky='nsew')  # 中央の余白フレーム配置

    button2.grid(row=1, column=3, sticky=tk.W+tk.E)

    # 右端の余白フレーム配置
    right_margin.grid(row=1, column=4, sticky='nsew')

    return button_frame

# Tkinter のルートウィンドウを作成
root = tk.Tk()
root.title("Button Dialog")

# ボタンパネルをメインウィンドウに配置
button_panel = create_button_panel(root)
button_panel.pack(fill=tk.X)  # X 方向(幅)に張り出すように表示

# Tkinter イベントループを開始
root.mainloop()

 マージンをforで確保するなどギョッとする部分もあるが、Pythonのソースコードを1箇所も手修正することなくこの精度のものができたのは驚愕しかない。

 なお、同じ文章を入力しても生成されるソースコードには違いが出るようで、気に入るソースコードが出力されるまで何回も試すという手も取れそうだ。

 メンテナンスのことを考えるとこのソースコードをそのまま使うのは難しいかもしれないが、ひとまずこんなものを作りたいというプロトタイピングにはぴったりだ。

 第58回のような記事を書いておいて何だが、筆者はAI PCには懐疑的な立場であった。しかし自分で実際に動かしてみると、これはもう可能性しか感じない。高価なGPUを用意せずともいいようにNPUの性能が向上し、それがUbuntuでも手軽に使えるわりと近いであろう将来に期待したい。