半年遅れでChatGPTとGitHub Copilot使って開発してみた!

これはアソビュー! Advent Calendar 2023の22日目(A面)です🎄 今年のアドベントカレンダーは2面公開なので、ぜひそちらも御覧ください。

はじめに

みなさん、こんにちはー。アソビューでバックエンドエンジニアをしている東郷です。

今年は生成AI元年と言われるだけあって、AIの活用事例のニュースをほぼ毎日耳にした気がします。 弊社内でもAI活用事例がいくつかあるので是非ご覧ください!

tech.asoview.co.jp

tech.asoview.co.jp

tech.asoview.co.jp

tech.asoview.co.jp

今回のテーマ

さて、時代はAIだ!と思いながらも

  • 実はChatGPTは最初に使ったきりで触っていない、、

  • GitHub Copilot便利そうだけど、なくても開発はできるから利用していない、、

なんて開発者も実は多いのではないでしょうか??(はい、それは先月までの私です)

今回はChatGPTとGitHub Copilotをフル活用して個人的に作りたかったツールを開発した経験を通して、 それぞれの最大限活用できる利用シーンをお伝えできればと思います。

開発したツール

  • ツール1:フォルダの階層を入れ替えるツール(難易度:★☆☆)
  • ツール2:文書マスキングツール(難易度:★★☆)
  • ツール3:マークシート読み取りツール(難易度:★★★)

難易度について

難易度は以下の肌感で決めてます。

  • 難易度:★☆☆(特に何か参照しなくても書けそうなレベル)
  • 難易度:★★☆(なんとなく実装方針検討つくけど、コアな部分はわからないレベル)
  • 難易度:★★★(実装方針がほとんど検討つかないレベル)

開発時のルール

ChatGPT、GitHub Copilotの使いどころを探りたいので以下のルールに則って開発します。

  1. まずはじめにChatGPTを使う
  2. GitHub Copilotは常にON
  3. ルール1とルール2で解決できない場合、Google検索する
  4. 人に聞かない(自分で考える)

使用したサービス、開発ツール

  • ChatGPT 3.5
  • GitHub Copilot
  • Python 3.9
  • Visual Studio Code
  • Google検索

ツール1:フォルダの階層を入れ替えるツール(難易度:★☆☆)

概要

運用業務をしていると「日付_XXX」でフォルダを作って、そこに作業で使う資料を格納していくことはありませんか?
このやり方は1人でやってる時はいいんですが複数人で情報管理目的で利用しようとするとタスクを担当していなかった人は該当ファイルをうまく探せない問題に悩まされます。

そこである程度情報が溜まってきたらタスクベースでフォルダ管理したいなぁと思った時に使えそうなツールを作ります。

まずはChatGPT

プロンプト

日々の作業内容を以下のような作業履歴フォルダを作って保管しています。

作業履歴フォルダ
- 20231101_データパッチ_ユーザー追加
- 20231102_サーバー再起動_検証環境

フォルダの形式はyyyyMMdd_作業区分_作業名です

作業履歴内のフォルダを作業区分毎に配置し直すプログラムを作りたいです。

作業履歴フォルダ
- 作業区分
 - 元のフォルダ名

レスポンス

import os
import re
from shutil import move

work_history_path = "/path/to/your/work/history/folder"

for folder_name in os.listdir(work_history_path):
    if os.path.isdir(os.path.join(work_history_path, folder_name)):
        # Extract task type from the folder name using regular expression
        match = re.match(r"\d{8}_(\w+)_(\w+)", folder_name)
        if match:
            task_type = match.group(1)
            
            # Create task type subdirectory if not exists
            task_type_path = os.path.join(work_history_path, task_type)
            os.makedirs(task_type_path, exist_ok=True)
            
            # Move the folder to the task type subdirectory
            move(os.path.join(work_history_path, folder_name), os.path.join(task_type_path, folder_name))

調整作業

  • インプットフォルダの位置変更
  • 最初フォルダ移動だったが、フォルダコピーに変えた
  • アウトプットフォルダの指定など機能追加はGitHub CopilotとGoogle検索で対応

完成形

import os
import re
from shutil import copytree

input_folder_name = "作業履歴"
work_history_path = os.path.join(os.path.dirname(__file__), input_folder_name)

# Specify the output folder name
parent_folder = os.path.dirname(work_history_path)
output_folder_name = "output"

output_folder_path = os.path.join(parent_folder, output_folder_name)
os.makedirs(output_folder_path, exist_ok=True)

for folder_name in os.listdir(work_history_path):
    if os.path.isdir(os.path.join(work_history_path, folder_name)):
        # Extract task type from the folder name using regular expression
        match = re.match(r"\d{8}_(\w+)_(\w+)", folder_name)
        if match:
            task_type = match.group(1)

            # Create task type subdirectory in the output folder if not exists
            task_type_path = os.path.join(output_folder_path, task_type)
            os.makedirs(task_type_path, exist_ok=True)

            # Copy the folder to the task type subdirectory in the output folder
            copytree(os.path.join(work_history_path, folder_name), os.path.join(task_type_path, folder_name))

実行結果

元のフォルダ構成

tool1_元のフォルダ構成の画像

ツール実行後のフォルダ構成

tool1実行後のフォルダ構成

評価コメント

  • ChatGPT:★★★(ChatGPTのアウトプットで8割完成。やりたいことほぼできた!)
  • GitHub Copilot:★★☆(機能追加時にコメントから目的のコードを出力してくれた)

以下の条件だとChatGPTで完結しそう

  • ツールが小さい
  • やりたいことが明確(プロンプトへの指示が明確)
  • 既に類似の解決策が多く存在している

ツール2:文書マスキングツール(難易度:★★☆)

概要

ChatGPTに社内文書を入れてアドバイスもらいたいけど、社外秘の文書そのまま入れるわけにはいかないし、、そんなお悩みありませんか?

そこで文書の一部をマスクしてGhatGPTに入れても問題なさそうな出力を得るツールを作ってみます。

まずはChatGPT

プロンプト

ChatGPTにテキストを入力する前に入力されたテキストに含まれる固有名詞をマスキングするアプリをPythonで作りたいです。 マスキングの形式は最初の1文字を残して残りを■で置き換えてください。 例:アソビューの場合、ア■■■■

レスポンス

import spacy

def mask_proper_nouns(text):
    nlp = spacy.load("ja_core_news_sm")
    doc = nlp(text)

    masked_text = []
    for token in doc:
        if token.ent_type_ == "PERSON" or token.ent_type_ == "ORG":
            masked_token = token.text[0] + "■" * (len(token.text) - 1)
            masked_text.append(masked_token)
        else:
            masked_text.append(token.text)

    return " ".join(masked_text)

if __name__ == "__main__":
    input_text = "アソビューの場合、アイウエオやカキクケコが含まれています。OpenAIは素晴らしい会社です。"
    masked_text = mask_proper_nouns(input_text)
    print(masked_text)

ぱっと見の理解

  • spacyなるものを使って日本語辞書?をロードして解析
  • トークンが人か組織化だったらマスクする

おー、これでコアな部分はできてそう!

調整作業

  • 実際実行してみると何もマスクされなかったので調査
    • 調べてみるとtoken.ent_type_が空でマスク対象になってない
    • ja_core_news_smがspaCyの日本語標準モデル(小)なので拾えてなさそう
    • ja_ginza_electra(GiNZA 日本語NLPライブラリ(transformerバージョン))に変更
    • ent_type_に何か入ってたらマスクするで良さそう
  • マスクレベルを設定できるように変更
  • 環境設定(spaCy認識しない問題)

完成形

# pip install spacy
import spacy

def mask(text, mask_char="■", non_mask_len=0):
    """
    non_mask_lenの長さだけtextの前を残してマスキングする
    """
    masked_text = text[:non_mask_len]
    return masked_text + mask_char * (len(text) - non_mask_len)

def mask_proper_nouns(text, non_mask_len=0):
    """
    固有表現をマスキングする関数
    """
    # pip install ja_ginza_electra
    nlp = spacy.load("ja_ginza_electra")
    doc = nlp(text)

    masked_text = []
    for token in doc:
        #print(token.text, token.ent_type_)
        if token.ent_type_ != "":
            masked_token = mask(token.text, non_mask_len=non_mask_len)
            masked_text.append(masked_token)
        else:
            masked_text.append(token.text)

    return "".join(masked_text)

if __name__ == "__main__":
    input_text = """
アソビュー株式会社(所在地:東京都品川区、代表執行役員CEO:山野 智久、以下、当社)は、Great Place to Work® Institute Japan(以下、GPTWジャパン)が実施する2024年度版日本における「働きがいのある会社」に認定されました。
当社は、「生きるに、遊びを。」をミッションに掲げ、「遊び」が衣食住に並ぶ人生を彩る豊かなものとして、Well-Beingな社会の実現を目指し、遊びが見つかる「アソビュー!」、大切な人に思い出を送る体験ギフト「アソビュー!ギフト」、観光・レジャー・文化施設向けDXソリューション事業を提供しています。
事業・サービスの拡充に伴い、エンジニア・セールス職を中心に積極的な人材採用を行い、直近1年間で約50名の新規入社(新卒・中途含む・期間:2022年12月1日〜2023年11月30日)、従業員は2023年11月現在で220名に至ります。    
"""
    masked_text = mask_proper_nouns(input_text)
    print(masked_text)

実行結果

マスク前

アソビュー株式会社(所在地:東京都品川区、代表執行役員CEO:山野 智久、以下、当社)は、Great Place to Work® Institute Japan(以下、GPTWジャパン)が実施する2024年度版日本における「働きがいのある会社」に認定されました。 当社は、「生きるに、遊びを。」をミッションに掲げ、「遊び」が衣食住に並ぶ人生を彩る豊かなものとして、Well-Beingな社会の実現を目指し、遊びが見つかる「アソビュー!」、大切な人に思い出を送る体験ギフト「アソビュー!ギフト」、観光・レジャー・文化施設向けDXソリューション事業を提供しています。 事業・サービスの拡充に伴い、エンジニア・セールス職を中心に積極的な人材採用を行い、直近1年間で約50名の新規入社(新卒・中途含む・期間:2022年12月1日〜2023年11月30日)、従業員は2023年11月現在で220名に至ります。

ツール実行後

ア■■■■株■■■(所在地:東■■■■■、代■執■■■C■■:山■智■、以下、当社)は、G■■■■P■■■■t■W■■■®I■■■■■■■■J■■■■(以下、G■■■ジ■■■)が実施する2■■■年■版日■における「働きがいのある会社」に認定されました。 当社は、「生きるに、遊びを。」をミッションに掲げ、「遊び」が衣食住に並ぶ人生を彩る豊かなものとして、Well-Beingな社会の実現を目指し、遊びが見つかる「ア■■■■!」、大切な人に思い出を送る体験ギフト「ア■■■■!ギフト」、観光・レジャー・文化施設向けDXソリューション事業を提供しています。 事業・サービスの拡充に伴い、エ■■■■・セ■■■職を中心に積極的な人材採用を行い、直■1年■で約5■名の新規入社(新卒・中途含む・期間:2■■■年1■月1日〜2■■■年1■月3■日)、従業員は2■■■年1■月現■で2■■名に至ります。

マスクされた文章が出来上がりました。

ChatGPTに実行結果を入力して、どんな企業か聞いてみました。

プロンプト

次の文章を読んでこの会社の特徴を説明してください。

ア■■■■株■■■(所在地:東■■■■■、代■執■■■C■■:山■智■、以下、当社)は、G■■■■P■■■■t■W■■■®I■■■■■■■■J■■■■(以下、G■■■ジ■■■)が実施する2■■■年■版日■における「働きがいのある会社」に認定されました。 当社は、「生きるに、遊びを。」をミッションに掲げ、「遊び」が衣食住に並ぶ人生を彩る豊かなものとして、Well-Beingな社会の実現を目指し、遊びが見つかる「ア■■■■!」、大切な人に思い出を送る体験ギフト「ア■■■■!ギフト」、観光・レジャー・文化施設向けDXソリューション事業を提供しています。 事業・サービスの拡充に伴い、エ■■■■・セ■■■職を中心に積極的な人材採用を行い、直■1年■で約5■名の新規入社(新卒・中途含む・期間:2■■■年1■月1日〜2■■■年1■月3■日)、従業員は2■■■年1■月現■で2■■名に至ります。

レスポンス

この企業の特徴は以下のとおりです:

働きがいのある会社認定: この企業は「働きがいのある会社」に認定されており、従業員にとって良い労働環境を提供していることが特徴です。これは、従業員の幸福度や満足度を高めるための取り組みが行われていることを示しています。

ミッションと価値観: この企業は「生きるに、遊びを。」というミッションを掲げており、人生における遊びを重要視しています。遊びが豊かな生活を彩る要素であると捉え、Well-Beingな社会の実現を目指しています。このミッションは、企業の価値観を反映しており、遊びを通じて人々の幸福感を高めることを重視しています。

多様な事業領域: この企業は、遊びに関連するさまざまな事業を展開しています。具体的には、遊びが見つかるプラットフォーム「ア■■■■!」、体験ギフト「ア■■■■!ギフト」、観光・レジャー・文化施設向けのDXソリューション事業を提供しています。これにより、多様な顧客層に向けたサービスを提供しており、事業の多角化が特徴です。

成長と人材採用: この企業は急速に成長しており、事業・サービスの拡充に伴い、積極的な人材採用を行っています。特にエ■■■■・セ■■■職に焦点を当て、新卒から中途まで幅広い経験レベルの人材を採用しています。従業員数も急増しており、成長志向の企業であることが特徴です。

マスクされた文章からでも十分特徴を抽出できていますね、素晴らしい!

評価コメント

  • ChatGPT:★★★(提示されたコードでとりあえず動かすところまでいけた!)
  • GitHub Copilot:★★★(機能を足していく時の補完力がエグい!)

今回もChatGPTのおかげで実装の8割くらいお任せできました。
GitHub Copilotは機能追加したいと思ったときの補完がそのまま使えて驚きでした。

一番時間を使ったのは環境設定で、spaCyをインストールしたけど実行時に「No module named 'spacy'」と出て、 ChatGPTに聞いてみたんですが、解決までは至らず Google検索で最終的に解決しました。(仮想環境を入れてたのを忘れていてspaCyを入れてない環境のPythonをVisual Studio Codeから呼び出していた)

ツール3:マークシート読み取りツール(難易度:★★★)

いよいよラスト!初代AIといえば個人的にこれ!マークシート読み取り!
物理世界とPCを繋ぐ未来感を感じた技術でした。今日はそれを自分で作ってみることにします!

今回読み込ませる画像(DALL·E 3で作成)

マークシート

まずはChatGPT

プロンプト

マークシートの画像を読み込んでCSVを出力するツールをPythonで作りたいです。 マークされている場合は1、されていない場合は0を出力してください。

レスポンス

画像からCSVファイルを生成するアプリを作成するためには、いくつかのステップがあります。以下は、PythonとOpenCVを使用してこのタスクを実現する基本的な例です。

1.必要なライブラリのインストール: 最初に、必要なライブラリをインストールします。OpenCVを使用するために、以下のコマンドを使用できます。

pip install opencv-python

2.アプリケーションの作成: 次に、Pythonスクリプトを作成します。以下は、簡単な例です。

import cv2
import numpy as np
import csv

def image_to_csv(input_image_path, output_csv_path):
    # 画像を読み込む
    img = cv2.imread(input_image_path, cv2.IMREAD_GRAYSCALE)

    # 二値化する(白を1、黒を0にする)
    _, binary_img = cv2.threshold(img, 128, 1, cv2.THRESH_BINARY)

    # 転置することで、行と列を入れ替える
    transposed_img = np.transpose(binary_img)

    # CSVファイルに書き込む
    with open(output_csv_path, 'w', newline='') as csvfile:
        csv_writer = csv.writer(csvfile)
        csv_writer.writerows(transposed_img)

if __name__ == "__main__":
    input_image_path = 'input_image.png'  # 入力画像のパス
    output_csv_path = 'output.csv'  # 出力CSVのパス

    image_to_csv(input_image_path, output_csv_path)

ぱっと見の理解

  • 画像を読み込んで白と黒で表現する
  • 黒だったら0にしてくれる(これは思ってたのと違うけど逆にすれば良さそう)

今回もChatGPTでおよそできた感!

調整作業

今回はめっちゃ苦労しました。。

まず出力されたCSV見ると0と1の羅列が想像以上に並んでいて、???となりました。

csv

ChatGPTに確認したところ、画像の各行を1つのCSV行として扱えていなかったとのこと。
再チャレンジするもダメで、さらに質問すると1番目と全然違うコードが出てきてそれを使ってもダメで。。 どう考えればいいんだろうと、一度Google検索して他の人のコードを参考にするも糸口がつかめず。。

一回諦めてお風呂に入っていた時に、「画像を枠毎に分解して、それぞれの枠の中の黒の多さを取得すれば良くない?」と思いついた後は、 その方針でGitHub Copilot中心に開発ガンガン進みました。

完成形

#pip install opencv-python
import cv2
import numpy as np

def load_img(input_image_path):
    img = cv2.imread(input_image_path, cv2.IMREAD_UNCHANGED)
    # 画像が真っ黒になるので、アルファチャンネルを取り出して反転させて足し合わせる
    if img.shape[2] == 4:     # we have an alpha channel
        a1 = ~img[:,:,3]        # extract and invert that alpha
        img = cv2.add(cv2.merge([a1,a1,a1,a1]), img)   # add up values (with clipping)
        img = cv2.cvtColor(img, cv2.COLOR_RGBA2GRAY)    # strip alpha channel

    return img

def show_img(img):
    # 画像を表示する
    cv2.imshow('image', img)
    cv2.waitKey(0)

def img_to_array(img, num_rows, num_columns):
    # 画像の縦横を取得
    height, width = img.shape
    # セルの縦横を計算
    cell_height = int(height / num_rows)
    cell_width = int(width / num_columns)

    output = []
    # 画像を分割して各セルごとに黒か白かを判定する
    for row in range(num_rows):
        for col in range(num_columns):
            # セルを取得
            cell = img[row*cell_height:(row+1)*cell_height, col*cell_width:(col+1)*cell_width]
            # マークの部分だけを取得
            mark = cell[int(cell_height/4):int(cell_height - cell_height/4), int(cell_width/4):int(cell_width - cell_width/4)]
            # 確認用
            show_img(mark)
            # print(np.mean(mark))
            # 平均値が128より小さい場合は黒と判定する
            checked = 1 if np.mean(mark) < 128 else 0
            
            # 出力用の配列に追加
            output.append(checked)

    return output

if __name__ == "__main__":
    input_image_path = 'myomr/input/6x7.png'  # 入力画像のパス 
    output_csv_path = 'myomr/output/6x7.csv'  # 出力CSVのパス
    
    img = load_img(input_image_path)

    num_rows = 6  # マークシート画像の縦列数
    num_columns = 7  # マークシート画像の横列数

    output = img_to_array(img, num_rows, num_columns)

    # CSVに出力。横列数の分だけ改行する
    with open(output_csv_path, 'w') as f:
        for i in range(len(output)):
            f.write(str(output[i]))
            if (i+1) % num_columns == 0:
                f.write('\n')
            else:
                f.write(',')

評価コメント

  • ChatGPT:★☆☆(最初のコードは雰囲気掴めたが、2回目以降提示されたコードがどんどん複雑化して途中で使うのやめた)
  • GitHub Copilot:★★★(候補をたくさん試して動きを調整していった)

画像解析の分野は全く経験がなかったので、作り上げるのに苦労しました。
ChatGPTとやりとりを続けていると途中から理解できなくなってきて、GitHub CopilotとGoogle検索を主軸に切り替えて対応しました。
よく知らない分野のコードを書こうとすると、例え小さな規模でも「理解できない、エラーが解決できない、質問も的外れ、頭がパンクする」ので、 ChatGPTで生成された後、作り上げるには知識を埋めないと難しいなと感じました。

まとめ

開発時にChatGPT、GitHub Copilotそれぞれの最大限活用できるシーンを探ってみました。
個人的に思うそれぞれの最大限活用できるシーンは以下です。

ChatGPT

  • アイデアレベルを形にするとき

〇〇ができるツールを作りたいのでPythonで出力して。で大体いけた感あるので、とりあえずChatGPTに聞いてみるが これからは良さそうと思いました。

GitHub Copilot

  • 作り上げていく時

GhatGPTで出力されたコードを修正したり、機能追加するときに補完機能が大変役に立ちました。なので実装時は常にONにしたいと思いました。

ChatGPTとGitHub Copilotを活用しにくいシーン

  • 個別の環境のこと
  • 込み入ったエラー
  • 専門知識がない領域

ではうまく機能しない印象でした。知識がない領域は基本を押さえて難易度を下げるとChatGPTとGitHub Copilotで扱える形になるかと思いました。

おわりに

いかがだったでしょうか?話題の生成AIを使って開発するイメージが伝わったでしょうか? 生成AI分野はこれからも発展していくと思うので、どんどん使って使いこなしたいと思っていただければ幸いです!

そんなアソビューでは一緒に働くメンバーを大募集しています!
ご興味をお持ちのエンジニアの方がいらっしゃいましたら、カジュアル面談もありますので、お気軽にご応募いただければと思います!

www.asoview.com

speakerdeck.com