🐥

GitHub Copilot で取り組む言語処理100本ノック

はじめに

この記事では GitHub Copilot を活用して言語処理100本ノックに取り組んでいきます。

言語処理100本ノックは,実用的でワクワクするような課題に取り組みながら,プログラミング,データ分析,研究のスキルを楽しく習得することを目指した問題集です.

引用: 言語処理100本ノック

使用する環境は, GitHub Copilot individual + Visual Studio Code です。

導入に関しては公式ドキュメントをご覧ください。
https://docs.github.com/ja/copilot/quickstart?tool=vscode

また、今回Copilotによって生成したコードは GitHub レポジトリ にまとめてあります。もしよろしければ、見てみてください。

基本編(100本ノック 第1章: 準備運動)

まずは言語処理100本ノック第1章 準備運動を通してGitHub Copilotの基本的な使い方に慣れておきます。

  • コード補完 (問00, 問01)
  • インラインチャット (問02, 問03)
  • チャット (問04, 問05)
  • doc (問06)
  • fix (問07)
  • test (問08, 問09)

コード補完 (問00, 問01)

問00, 問01の2問はコード補完を用いて解いてみます。

コード補完の使い方は簡単で、editorの画面に書きたい処理を途中まで書くと GitHub Copilot がその続きの候補を表示してくれます。

問00では文字列を受け取って逆順にしたものを返す関数を作成したいので, reverse_str という関数名まで入力しようとしてみます。
すると、薄い文字でその先のコードの候補が表示されます。


補完候補が表示される様子

補完内容に問題がなければ、tabキーを押すことで、その補完候補を採用することができます。 この例の補完候補では、スライス構文のstepとして−1を設定することで、文字を逆順にする操作が実現されています。そこで、tabキーを押して問00は完了です。

問01に移ります。今度はコメントを入力してそこから補完させてみます。
問01では文字列を受け取って奇数番目の文字を連結した文字列を返す関数を作成したいです。
そこで

# 文字列の1,3,5,7,...文字目を取り出して連結した文字列を返す関数を定義してください。

とコメントをeditorに入力してみます。
すると、 次の行にdef とのみ入力した段階でコメントに従った関数の中身を実装した補完が出ました。これを採用して問01は完了です。


def と打つのみで正しく補完される

インラインチャット (問02, 問03)

続く2問はインラインチャット機能を用いてみます。

editorを開いた状態で ctrl+I キーを押すことで、Copilotとのチャットができる小窓が開きます。(windowsの場合)

そこに今回実装したい関数の説明を入力してみます。


作りたい関数を入力

すると、実装案が提示されます。同意するを押すとそれが実際にeditor上に入力されます。


Copilotによる実装案

もし生成されたものが望み通りでなければ再生成したり追加で指示を出すこともできます。


問03作成


問03修正

チャット (問04, 問05)

今度はチャットボックスでのチャットを用いてみます。
editor左側のチャットアイコンを押すとチャット画面が開きます。
ここに先ほどと同様に関数の要件を入力します。
すると先ほどと違い、設計や疑似コードが説明された後にコードが生成されます。


チャット欄での生成の様子 (問04)

また、質問を投げると解説をしてくれるのも便利です。


質問と回答例 (問05)

前節のインラインチャット及び、今節のチャット欄でのチャットの区別としては、

  • 各コード内でさっと使いたい場合→インラインチャット
  • 他のコードに跨ったり、長い回答を得たい場合→チャット欄でのチャット

といった使い分けができそうです。

/doc,/explain コマンド (問06)

ここからはチャット小窓で使えるコマンドについて触れていきます。

まずは/doc, /explain コマンドを使ってみます。

問06は、二つの文字列からbigramの集合を作り、それらの集合を操作する問題です。
以下はこの問題の解答をCopilotによって生成したコードです。

問06のコード
from q05 import generate_n_grams
def create_bigram_set(s):
    return set(generate_n_grams(s, 2))

def union_bigrams(set1, set2):
    return set1.union(set2)

def intersection_bigrams(set1, set2):
    return set1.intersection(set2)

def difference_bigrams(set1, set2):
    return set1.difference(set2)

def contains_bigram(bigram_set, bigram):
    return bigram in bigram_set

プログラムの意味を知りたいときに使えるのが /explain コマンドです。

インラインチャットの小窓に /explain と打ち込むと、Copilotからコードの内容を説明してもらえます。

このコードは、src.q05モジュールからgenerate_n_grams関数をインポートしています。この関数は、文字列を入力として受け取り、指定された長さのn-グラム(連続するn個の要素からなる部分列)を生成するために使用されます。
次に、create_bigram_set関数が定義されています。この関数は、文字列sを入力として受け取り、generate_n_grams関数を使用して2-グラム(バイグラム)を生成し、それを集合(set)として返します。集合を使用することで、重複するバイグラムが自動的に排除されます。

さらに、union_bigrams関数とintersection_bigrams関数が定義されています。union_bigrams関数は、2つのバイグラムの集合を受け取り、それらの和集合を返します。和集合は、両方の集合に含まれるすべての要素を含む新しい集合です。一方、intersection_bigrams関数は、2つのバイグラムの集合を受け取り、それらの積集合を返します。積集合は、両方の集合に共通する要素のみを含む新しい集合です。
これらの関数を組み合わせることで、文字列のバイグラムを生成し、それらの集合に対して集合演算を行うことができます。これは、テキスト解析や自然言語処理のタスクにおいて非常に有用です。

また、説明をプログラム上に記述したい場合は /doc を使うと説明がコードに追加できます。

doc コマンドで追加されるテキスト
"""
This module provides functions to create and manipulate bigram sets from strings.

Functions:
    create_bigram_set(s: str) -> set:
        Generates a set of bigrams from the input string `s`.

    union_bigrams(set1: set, set2: set) -> set:
        Returns the union of two bigram sets `set1` and `set2`.

    intersection_bigrams(set1: set, set2: set) -> set:
        Returns the intersection of two bigram sets `set1` and `set2`.

    difference_bigrams(set1: set, set2: set) -> set:
        Returns the difference of two bigram sets `set1` and `set2`.

    contains_bigram(bigram_set: set, bigram: str) -> bool:
        Checks if a bigram `bigram` is present in the bigram set `bigram_set`.

Usage example:


        print("Union:", union_bigrams(X, Y))
        print("Intersection:", intersection_bigrams(X, Y))
        print("Difference:", difference_bigrams(X, Y))
        print("Contains 'se' in X:", contains_bigram(X, 'se'))
        print("Contains 'se' in Y:", contains_bigram(Y, 'se'))
"""

/fix コマンド (問07)

今回はfixコマンドを使います。
問07は引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を作成する問題です。
これのコードとして下図のような実装をしたとします。

# 引数x, y, zを受け取り「x時のyはz」という文字列を返す関数
def generate_sentence(x,y,z)->str:
    return x+"時の"+y+"は"+"z"

ここで、インラインチャットを開いて /fix と入力してみます。
すると、変数としてのzが使われずに文字列としての"z"が結合されているのはおかしいという指摘が入り、修正案が提示されます。


Copilotによる修正案

このように、fixコマンドによってミスの発見や修正をすることができます。

/tests コマンド (問08, 問09)

最後に /tests コマンドを使ってみます。

以下は問08で求められている、適当な変換規則によって文字列の各文字を変換するプログラムをCopilotによって生成したコードです。

Copilotが生成した問08の解答コード
def cipher(text):
    result = []
    for char in text:
        if 'a' <= char <= 'z':
            result.append(chr(219 - ord(char)))
        else:
            result.append(char)
    return ''.join(result)

生成したコードをテストするコードも生成してもらいましょう。

chat小窓を開き、 /tests と打ち込みます。すると、テストをするプログラムを書いてくれます。


testの生成


Tips:

よく見ると、出力されたテストには正しくないものが含まれています。

cipher(”aBcXyZ”)の出力は”zBxXbZ”となるはずです。

このような時に、生成時の挙動をいったん説明させると精度が上がる場合があります。

インラインチャットに、

cipher("Abc")の挙動を説明してください。

と打ち込んでみます。

すると、以下のように正しく挙動を説明した返答があります。

Copilot の返答
cipher("Abc")の挙動を説明します。この関数は、与えられた文字列の各文字を特定のルールに従って変換するものです。以下にその詳細を説明します。

まず、コード全体を確認しましょう。cipher関数の定義は以下のようになっていると仮定します:

def cipher(text):
    result = []
    for char in text:
        if 'a' <= char <= 'z':
            result.append(chr(219 - ord(char)))
        else:
            result.append(char)
    return ''.join(result)
この関数の動作をステップバイステップで説明します。

初期化:

result = []
空のリストresultを初期化します。このリストに変換された文字を追加していきます。

ループ:

for char in text:
入力文字列textの各文字charに対してループを実行します。

条件分岐:

if 'a' <= char <= 'z':
    result.append(chr(219 - ord(char)))
else:
    result.append(char)
if 'a' <= char <= 'z':: 文字charが小文字のアルファベット('a'から'z')であるかをチェックします。
chr(219 - ord(char)): 文字charをASCIIコードに変換し(ord(char))、それを219から引きます。その結果を再び文字に変換します(chr(...))。 例えば、'a'のASCIIコードは97なので、219 - 97 = 122となり、これは'z'に対応します。同様に、'b'は'y'に、'c'は'x'に変換されます。
変換された文字をresultリストに追加します。
それ以外の文字(小文字アルファベット以外)はそのままresultリストに追加します。
結合:

return ''.join(result)
resultリストの全ての文字を結合して、最終的な文字列を返します。

具体的にcipher("Abc")を実行すると:

'A'は小文字アルファベットではないので、そのままresultに追加されます。
'b'は小文字アルファベットなので、219 - ord('b') = 219 - 98 = 121、これは'y'に対応します。
'c'も小文字アルファベットなので、219 - ord('c') = 219 - 99 = 120、これは'x'に対応します。
したがって、cipher("Abc")の結果は"Ayx"となります。

正しい答えが返ってきました。

次に再度テストを生成してみます。以下をインラインチャットに打ち込みます。

cipher("Abc")の挙動を説明した時のように, 動作の様子を記述しながらテストケースを作成してください。
Copilotによって生成されたテストコード
import pytest
from src.q08 import cipher
def test_cipher_with_lowercase():
    # 'a' -> 'z', 'b' -> 'y', 'c' -> 'x'
    assert cipher("abc") == "zyx"
    # 'x' -> 'c', 'y' -> 'b', 'z' -> 'a'
    assert cipher("xyz") == "cba"

def test_cipher_with_mixed_case():
    # Only lowercase letters are transformed
    # 'a' -> 'z', 'B' remains 'B', 'c' -> 'x', 'X' remains 'X', 'y' -> 'b', 'Z' remains 'Z'
    assert cipher("aBcXyZ") == "zBxXbZ"

def test_cipher_with_non_alpha():
    # Non-alphabetic characters remain unchanged
    assert cipher("123!@#") == "123!@#"

def test_cipher_with_empty_string():
    # An empty string should return an empty string
    assert cipher("") == ""

def test_cipher_with_full_sentence():
    # 'h' -> 's', 'e' -> 'v', 'l' -> 'o', 'o' -> 'l', 'w' -> 'd', 'r' -> 'i', 'd' -> 'w'
    assert cipher("hello world") == "svool dliow"

def test_cipher_with_uppercase():
    # Uppercase letters remain unchanged
    assert cipher("HELLO WORLD") == "HELLO WORLD"

def test_cipher_with_mixed_characters():
    # Mixed characters, only lowercase letters are transformed
    # 'a' -> 'z', '1' remains '1', 'b' -> 'y', '!' remains '!', 'c' -> 'x'
    assert cipher("a1b!c") == "z1y!x"

def test_cipher_with_repeated_characters():
    # Repeated characters, 'a' -> 'z', 'b' -> 'y'
    assert cipher("aaa") == "zzz"
    assert cipher("bbb") == "yyy"

def test_cipher_with_long_mixed_string():
    # Long string with mixed characters
    # 'a' -> 'z', 'b' -> 'y', 'c' -> 'x', 'd' -> 'w', 'e' -> 'v', 'f' -> 'u', 'g' -> 't'
    assert cipher("abcdefgABCDEFG") == "zyxwvutABCDEFG"

今回は正しいテストのようです。

その上で誤りを含む場合もありますが、今回のように説明を要求すると成功する確率が体感的には上がりました。


問09もテストを生成してみます。

問09は、文字列の各単語を、長さが4以上の場合に両端以外の文字の順番をシャッフルする問題です。

Copilotが生成した問09 の解答コード
import random

def shuffle_middle(word):
    if len(word) <= 4:
        return word
    middle = list(word[1:-1])
    random.shuffle(middle)
    return word[0] + ''.join(middle) + word[-1]

def shuffle_sentence(sentence):
    words = sentence.split()
    shuffled_words = [shuffle_middle(word) for word in words]
    return ' '.join(shuffled_words)

今回はプログラムにランダム性があるため、一対一でassert関数を書くのは困難です。

しかし、そのような場合でもよしなにテストを作成してくれました。

/tests コマンドで生成したテストコード
import pytest
from src.q09 import shuffle_middle, shuffle_sentence

def test_shuffle_middle_short_word():
    assert shuffle_middle("cat") == "cat"
    assert shuffle_middle("dog") == "dog"

def test_shuffle_middle_long_word():
    word = "shuffle"
    shuffled = shuffle_middle(word)
    assert shuffled[0] == 's'
    assert shuffled[-1] == 'e'
    assert sorted(shuffled[1:-1]) == sorted(word[1:-1])

def test_shuffle_sentence():
    sentence = "This is a test sentence"
    shuffled = shuffle_sentence(sentence)
    words = sentence.split()
    shuffled_words = shuffled.split()
    assert len(words) == len(shuffled_words)
    for original, shuffled in zip(words, shuffled_words):
        if len(original) > 4:
            assert original[0] == shuffled[0]
            assert original[-1] == shuffled[-1]
            assert sorted(original[1:-1]) == sorted(shuffled[1:-1])
        else:
            assert original == shuffled

チャレンジ編(100本ノック 第6章: 機械学習)

ここからはチャレンジ編と題して、より複雑な問題にwith Copilotで取り組んでみたいと思います。
取り組む問題は言語処理100本ノック第6章 機械学習です。

目標はニュース記事のカテゴリと見出しのデータからロジスティック回帰モデルを作り、見出しからカテゴリを推測することです。
大雑把な流れとしては、

  • 記事データを学習用, 検証用, 評価用に分類する。
  • 各データから特徴量を抽出し、ロジスティック回帰モデルを作る。
  • 回帰モデルから予測を行う。
  • 結果の分析を行う。

となります。

微修正を除いて全てCopilotに任せようと思います。

準備

記事のデータセットには付属のreadme.txtがついています。そこでまずは Copilot に要約をお願いしてみます。editorでreadme.txtを開き、チャットに以下を打ち込みます。

#editor 要約と説明をお願いします。

ここで、#editor とは、現在editorで開いているコードを表す変数で、これを用いることでCopilotにどの内容について指示を出しているかを明示することができます。
同様のコマンドとして、

  • file : ワークスペースのファイルを表す変数
  • #selection : editorで開いたコードの選択している部分を表す変数
  • #terminalLastCommand : アクティブなターミナルで最後に実行したコマンド

などがあります。詳しくは
https://code.visualstudio.com/docs/copilot/copilot-chat#_chat-context
をご覧ください。


すると、descriptionの部分の要約がなされます。

要約
このデータセットは、ニュースウェブページへの参照(URL)のコレクションです。

説明
このデータセットは、2014年3月10日から8月10日までの期間にオンラインアグリゲーターから収集されたニュースウェブページへの参照を含んでいます。リソースは同じニュースストーリーを議論するページを表すクラスターにグループ化されています。また、コレクション内のニュースページへのリンクを含むウェブページへの参照も含まれています。

readme.txt 内にはそのほかデータ内容の説明等も含まれており、それらについての質問も正確な返答が得られました。

記事データの分類

それでは実際にプログラムの作成をしていきます。

まずは記事の分類をする関数の作成を依頼します。

#editor news+aggregator/newsCorpora.csv の中から, 情報源(publisher)が”Reuters”, “Huffington Post”, “Businessweek”, “Contactmusic.com”, “Daily Mail”の事例(記事)のみを抽出する関数を作成してください。
Copilotによって生成されたコード
import pandas as pd

def extract_specific_publishers(file_path):
    # 読み込み
    df = pd.read_csv(file_path, sep='\t', header=None, names=['ID', 'TITLE', 'URL', 'PUBLISHER', 'CATEGORY', 'STORY', 'HOSTNAME', 'TIMESTAMP'])
    
    # 指定された情報源
    target_publishers = ["Reuters", "Huffington Post", "Businessweek", "Contactmusic.com", "Daily Mail"]
    
    # 抽出
    filtered_df = df[df['PUBLISHER'].isin(target_publishers)]
    
    return filtered_df

# 使用例
file_path = 'news+aggregator/newsCorpora.csv'
filtered_articles = extract_specific_publishers(file_path)
print(filtered_articles.head())

さらにここから

次に、抽出した記事たちの順番をランダムにシャッフルする関数を作ってください。
最後に, 抽出された事例の80%を学習データ,残りの10%ずつを検証データと評価データに分割し,それぞれtrain.txt,valid.txt,test.txtというファイル名で保存したいです。ファイルには,1行に1事例を書き出すこととし,カテゴリ名と記事見出しのタブ区切り形式とします。また, 標準出力として各データの各カテゴリの事例数を出力してほしいです。

と二段階でコードを書き足してもらいます。

出力されたのは以下のものです。

Copilotによる出力
import pandas as pd
from sklearn.model_selection import train_test_split

def extract_specific_publishers(file_path):
    # 読み込み
    df = pd.read_csv(file_path, sep='\t', header=None, names=['ID', 'TITLE', 'URL', 'PUBLISHER', 'CATEGORY', 'STORY', 'HOSTNAME', 'TIMESTAMP'])
    
    # 指定された情報源
    target_publishers = ["Reuters", "Huffington Post", "Businessweek", "Contactmusic.com", "Daily Mail"]
    
    # 抽出
    filtered_df = df[df['PUBLISHER'].isin(target_publishers)]
    
    return filtered_df

def shuffle_articles(df):
    # ランダムにシャッフル
    shuffled_df = df.sample(frac=1).reset_index(drop=True)
    return shuffled_df

def split_and_save_data(df):
    # データを分割
    train, temp = train_test_split(df, test_size=0.2, random_state=42)
    valid, test = train_test_split(temp, test_size=0.5, random_state=42)
    
    # ファイルに保存
    train.to_csv('train.txt', columns=['CATEGORY', 'TITLE'], sep='\t', index=False, header=False)
    valid.to_csv('valid.txt', columns=['CATEGORY', 'TITLE'], sep='\t', index=False, header=False)
    test.to_csv('test.txt', columns=['CATEGORY', 'TITLE'], sep='\t', index=False, header=False)
    
    # 各データセットのカテゴリごとの事例数を出力
    print("Train data category counts:")
    print(train['CATEGORY'].value_counts())
    print("\nValidation data category counts:")
    print(valid['CATEGORY'].value_counts())
    print("\nTest data category counts:")
    print(test['CATEGORY'].value_counts())

# 使用例
file_path = 'news+aggregator/newsCorpora.csv'
filtered_articles = extract_specific_publishers(file_path)
shuffled_articles = shuffle_articles(filtered_articles)
split_and_save_data(shuffled_articles)

必要ライブラリの導入も

@terminal sklearnの導入

とチャットすれば

pip install scikit-learn

コマンドが返ってきます。


ここで、@terminal とは質問内容のジャンルがターミナルに関することであると明示することで、Copilotの回答がより適切になるよう図れるコマンドです。

同様のコマンドとして、

  • @workspace : 質問ジャンルがワークスペース内のコードであることを明示
  • @vscode : 質問ジャンルがVisual Studio Codeの機能や設定に関するものであることを明示
  • @github : 質問ジャンルがGitHubに関するものであることを明示

詳しくは
https://code.visualstudio.com/docs/copilot/copilot-chat#_chat-participants
をご覧ください。


特徴量の抽出, 回帰モデルの作成

次にデータから特徴量を抽出します。用いる特徴量も含めてCopilotに相談してみます。

学習データ,検証データ,評価データから特徴量を抽出し,それぞれtrain.feature.txt,valid.feature.txt,test.feature.txtというファイル名で保存したいです。特徴量としてはどのようなものが考えられますか?
Copilotからの提案
GitHub Copilot
特徴量は、機械学習モデルがデータを理解し、予測を行うための重要な要素です。ニュース記事の見出しから抽出できる特徴量には以下のようなものがあります:

単語の出現頻度:
各単語が見出しに何回出現するかをカウントします。

TF-IDF (Term Frequency-Inverse Document Frequency):
単語の重要度を測るための指標で、頻繁に出現する単語に低い重みを与え、特定の文書にのみ出現する単語に高い重みを与えます。

N-gram:
単語の連続した組み合わせ(バイグラム、トライグラムなど)を特徴量として使用します。

文の長さ:
見出しの単語数や文字数を特徴量として使用します。

品詞タグ:
各単語の品詞(名詞、動詞、形容詞など)を特徴量として使用します。

固有表現:
人名、地名、組織名などの固有表現を抽出して特徴量とします。
以下に、単語の出現頻度を特徴量として抽出し、それぞれのデータセットをファイルに保存するコードを示します。

計画
データを読み込む。
単語の出現頻度を特徴量として抽出する。
特徴量をファイルに保存する。
Copilotによる生成コード
import csv
from sklearn.feature_extraction.text import CountVectorizer

def read_data(file_path):
    data = []
    labels = []
    with open(file_path, 'r', encoding='utf-8') as file:
        reader = csv.reader(file, delimiter='\t')
        for row in reader:
            labels.append(row[0])
            data.append(row[1])
    return data, labels

def extract_features(data):
    vectorizer = CountVectorizer()
    features = vectorizer.fit_transform(data)
    return features, vectorizer

def save_features(features, labels, vectorizer, file_path):
    feature_names = vectorizer.get_feature_names_out()
    with open(file_path, 'w', encoding='utf-8') as file:
        writer = csv.writer(file, delimiter='\t')
        writer.writerow(['LABEL'] + list(feature_names))
        for label, feature_row in zip(labels, features.toarray()):
            writer.writerow([label] + list(feature_row))

# データの読み込み
train_data, train_labels = read_data('train.txt')
valid_data, valid_labels = read_data('valid.txt')
test_data, test_labels = read_data('test.txt')

# 特徴量の抽出
train_features, vectorizer = extract_features(train_data)
valid_features = vectorizer.transform(valid_data)
test_features = vectorizer.transform(test_data)

# 特徴量の保存
save_features(train_features, train_labels, vectorizer, 'train.feature.txt')
save_features(valid_features, valid_labels, vectorizer, 'valid.feature.txt')
save_features(test_features, test_labels, vectorizer, 'test.feature.txt')

その後、プログラムの実行進捗をバーで表示するようにしたり、途中結果の確認できるようにしたり、高速化をしたりといったコードの変更をCopilotにしてもらいました。

引き続き、回帰モデルの作成コードを書いてもらいます。

Copilotにより作成された回帰モデルを作成するコード
import csv
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import joblib

def load_features(file_path):
    features = []
    labels = []
    with open(file_path, 'r', encoding='utf-8') as file:
        reader = csv.reader(file, delimiter='\t')
        for row in reader:
            labels.append(row[0])
            features.append([float(x) for x in row[1:]])
    return np.array(features), np.array(labels)

# 学習データの読み込み
train_features, train_labels = load_features('features/train.feature.txt')
valid_features, valid_labels = load_features('features/valid.feature.txt')

# ロジスティック回帰モデルの定義
model = LogisticRegression(max_iter=1000)

# モデルの学習
model.fit(train_features, train_labels)

# 検証データでの予測
valid_predictions = model.predict(valid_features)

# 精度の評価
accuracy = accuracy_score(valid_labels, valid_predictions)
print(f"Validation Accuracy: {accuracy}")

# モデルの保存(オプション)
joblib.dump(model, 'logistic_regression_model.joblib')

これを実行してみますが、長時間待ったのちターミナルにkilledと表示されます。
その旨を問うと、原因はメモリ使用量が多さの可能性があり大量のデータを一度に処理するのではなく、バッチ処理すると改善するかもと告げられます。
提示された改善コードを実行すると、時間はかかるものの実行が完了しました。

続いて作ったモデルをもとに実際に予測を行う予定でしたが、うまくいかなくなります。Copilotにエラーが出た際に一度チャットを切り替えてしまった影響か分かりませんが、以前書いたコードとの一貫性がないコードが生成されてしまい、修正も難航しました。

改善

Copilotとのやりとりもとり散らかってしまったので一旦状況を整理することにします。以下をコメント チャット欄に打ち込んでみます。

なかなかうまくいかないため、再度要件の確認からすり合わせたいです.

目標: 記事の見出しからカテゴリを予測

datafolderに, 学習用, 検証用, 評価用に記事のカテゴリと見出しが保存されている

q51.py: datafolderのデータから特徴量を抽出し保存  
q52.py: q51で構築した学習データを用いて,ロジスティック回帰モデルを学習.  
q53.py: 52で学習したロジスティック回帰モデルを用い,与えられた記事見出しからカテゴリとその予測確率を計算するプログラムを実装.

といった感じです。

すると, 先ほどまでとは違うコードが出力されました。今度は一貫性がありそうです。

Copilotによって生成されたデータから特徴量を抽出するコード
import csv
from sklearn.feature_extraction.text import TfidfVectorizer
import joblib

# データの読み込み
def load_data(file_path):
    features = []
    labels = []
    with open(file_path, 'r', encoding='utf-8') as file:
        reader = csv.reader(file, delimiter='\t')
        for row in reader:
            labels.append(row[0])
            features.append(row[1])
    return features, labels

# 学習データの読み込み
train_features, train_labels = load_data('datafolder/train.txt')

# TF-IDFベクタライザの学習
vectorizer = TfidfVectorizer()
X_train = vectorizer.fit_transform(train_features)

# 特徴量とラベルの保存
joblib.dump(X_train, 'features/train_features.joblib')
joblib.dump(train_labels, 'features/train_labels.joblib')
joblib.dump(vectorizer, 'features/tfidf_vectorizer.joblib')

# 検証データの読み込み
valid_features, valid_labels = load_data('datafolder/valid.txt')

# 学習済みのTF-IDFベクタライザを使用して検証データを変換
X_valid = vectorizer.transform(valid_features)

# 特徴量とラベルの保存
joblib.dump(X_valid, 'features/valid_features.joblib')
joblib.dump(valid_labels, 'features/valid_labels.joblib')
Copilotによって生成されたロジスティック回帰モデルを作成するコード
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import joblib

# 特徴量とラベルの読み込み
X_train = joblib.load('features/train_features.joblib')
train_labels = joblib.load('features/train_labels.joblib')

# ロジスティック回帰モデルの定義と学習
model = LogisticRegression(max_iter=1000)
model.fit(X_train, train_labels)

# 検証データの読み込み
X_valid = joblib.load('features/valid_features.joblib')
valid_labels = joblib.load('features/valid_labels.joblib')

# 検証データでの予測
valid_predictions = model.predict(X_valid)

# 精度の評価
accuracy = accuracy_score(valid_labels, valid_predictions)
print(f"Validation Accuracy: {accuracy}")

# モデルの保存
joblib.dump(model, 'models/logistic_regression_model.joblib')
Copilotによって作成された作成した回帰モデルからカテゴリの予測を行うコード
import joblib
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

# 学習済みのロジスティック回帰モデルとTF-IDFベクタライザをロード
model = joblib.load('models/logistic_regression_model.joblib')
vectorizer = joblib.load('features/tfidf_vectorizer.joblib')

def preprocess_text(text):
    # 必要に応じて前処理を行う(例:小文字化、特殊文字の除去など)
    return text.lower()

def predict_category(headline):
    # 記事見出しを前処理
    processed_headline = preprocess_text(headline)
    
    # 前処理された見出しをTF-IDFベクタライザで変換
    X = vectorizer.transform([processed_headline])
    
    # モデルで予測
    probabilities = model.predict_proba(X)[0]
    predicted_category = model.classes_[np.argmax(probabilities)]
    
    return predicted_category, probabilities

# 評価データの読み込み
with open('datafolder/eval.txt', 'r') as file:
    headlines = file.readlines()

# 各記事見出しに対してカテゴリと予測確率を計算し、結果を表示
for headline in headlines:
    category, probabilities = predict_category(headline.strip())
    print(f"見出し: {headline.strip()}\nカテゴリ: {category}, 予測確率: {probabilities}\n")

これを動かしてみるとどうやら正しい挙動をしていそうなため、先に進みます。

結果の表示

結果の表示パートについては、

  1. 混同行列作成の実装をしてもらう。
  2. 行列のサイズが学習用と評価用で異なる旨を質問。
    →両者のデータでクラスのサイズが違うかもと指摘, 確認用コードが提示される。
  3. 学習用データのフォーマットの関係で一部カテゴリ処理がうまくいっていないことが判明。
    →フォーマットがうまくいっている行のみ残すコードが提示される。
  4. 適合率等の計測の実装をしてもらう。

といった流れで進行しましたが、どれもスムーズに生成され、自分はコードにほぼ触らぬままこのタスクが完了しました。

Copilotによって生成された混同行列を作成するコード
from sklearn.metrics import confusion_matrix, accuracy_score
import joblib
import matplotlib.pyplot as plt
import seaborn as sns

# 学習済みのロジスティック回帰モデルとTF-IDFベクタライザをロード
model = joblib.load('models/logistic_regression_model.joblib')
vectorizer = joblib.load('features/tfidf_vectorizer.joblib')

# データの読み込み
def load_data(file_path):
    features = []
    labels = []
    with open(file_path, 'r', encoding='utf-8') as file:
        for line in file:
            label, feature = line.strip().split('\t', 1)
            labels.append(label)
            features.append(feature)
    return features, labels

# 学習データの読み込み
train_features, train_labels = load_data('datafolder/train.txt')
X_train = vectorizer.transform(train_features)

# 学習データでの予測
train_predictions = model.predict(X_train)

# 学習データでの混同行列
train_conf_matrix = confusion_matrix(train_labels, train_predictions)
train_accuracy = accuracy_score(train_labels, train_predictions)
print(f"Train Accuracy: {train_accuracy}")

# 混同行列のプロット
plt.figure(figsize=(10, 7))
sns.heatmap(train_conf_matrix, annot=True, fmt='d', cmap='Blues')
plt.title('Confusion Matrix - Training Data')
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.savefig('confusion_matrix_train.png')  # プロットをファイルに保存


# 評価データの読み込み
eval_features, eval_labels = load_data('datafolder/test.txt')
X_eval = vectorizer.transform(eval_features)

# 評価データでの予測
eval_predictions = model.predict(X_eval)

# 評価データでの混同行列
eval_conf_matrix = confusion_matrix(eval_labels, eval_predictions)
eval_accuracy = accuracy_score(eval_labels, eval_predictions)
print(f"Evaluation Accuracy: {eval_accuracy}")

# 混同行列のプロット
plt.figure(figsize=(10, 7))
sns.heatmap(eval_conf_matrix, annot=True, fmt='d', cmap='Blues')
plt.title('Confusion Matrix - Evaluation Data')
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.savefig('confusion_matrix_eval.png')  # プロットをファイルに保存
Copilotによって生成された適合率等を計測するコード
from sklearn.metrics import precision_score, recall_score, f1_score, classification_report
import joblib

# 学習済みのロジスティック回帰モデルとTF-IDFベクタライザをロード
model = joblib.load('models/logistic_regression_model.joblib')
vectorizer = joblib.load('features/tfidf_vectorizer.joblib')

# データの読み込み
def load_data(file_path):
    features = []
    labels = []
    with open(file_path, 'r', encoding='utf-8') as file:
        for line in file:
            label, feature = line.strip().split('\t', 1)
            labels.append(label)
            features.append(feature)
    return features, labels

# 評価データの読み込み
eval_features, eval_labels = load_data('datafolder/test.txt')
X_eval = vectorizer.transform(eval_features)

# 評価データでの予測
eval_predictions = model.predict(X_eval)

# 適合率、再現率、F1スコアの計測
precision = precision_score(eval_labels, eval_predictions, average=None, labels=model.classes_)
recall = recall_score(eval_labels, eval_predictions, average=None, labels=model.classes_)
f1 = f1_score(eval_labels, eval_predictions, average=None, labels=model.classes_)

# カテゴリごとの性能を表示
for i, category in enumerate(model.classes_):
    print(f"Category: {category}")
    print(f"  Precision: {precision[i]}")
    print(f"  Recall: {recall[i]}")
    print(f"  F1 Score: {f1[i]}")

# マイクロ平均とマクロ平均の計測
micro_precision = precision_score(eval_labels, eval_predictions, average='micro')
micro_recall = recall_score(eval_labels, eval_predictions, average='micro')
micro_f1 = f1_score(eval_labels, eval_predictions, average='micro')

macro_precision = precision_score(eval_labels, eval_predictions, average='macro')
macro_recall = recall_score(eval_labels, eval_predictions, average='macro')
macro_f1 = f1_score(eval_labels, eval_predictions, average='macro')

print("\nMicro-Average Metrics")
print(f"  Precision: {micro_precision}")
print(f"  Recall: {micro_recall}")
print(f"  F1 Score: {micro_f1}")

print("\nMacro-Average Metrics")
print(f"  Precision: {macro_precision}")
print(f"  Recall: {macro_recall}")
print(f"  F1 Score: {macro_f1}")

# 詳細な分類レポートの表示
print("\nClassification Report")
print(classification_report(eval_labels, eval_predictions, target_names=model.classes_))

混合行列の出力結果は以下の通りです。


学習用データの混合行列


評価用データの混合行列

また、適合率等を計算するコードを実行結果は以下の通りです。

適合率等の計算結果
Category: b
  Precision: 0.9049919484702094
  Recall: 0.9509306260575296
  F1 Score: 0.9273927392739274
Category: e
  Precision: 0.8703374777975134
  Recall: 0.9722222222222222
  F1 Score: 0.9184629803186504
Category: m
  Precision: 0.9607843137254902
  Recall: 0.6125
  F1 Score: 0.7480916030534351
Category: t
  Precision: 0.8383838383838383
  Recall: 0.5220125786163522
  F1 Score: 0.6434108527131783

Micro-Average Metrics
  Precision: 0.8875562218890555
  Recall: 0.8875562218890555
  F1 Score: 0.8875562218890555

Macro-Average Metrics
  Precision: 0.8936243945942628
  Recall: 0.7644163567240261
  F1 Score: 0.8093395438397979

Classification Report
              precision    recall  f1-score   support

           b       0.90      0.95      0.93       591
           e       0.87      0.97      0.92       504
           m       0.96      0.61      0.75        80
           t       0.84      0.52      0.64       159

    accuracy                           0.89      1334
   macro avg       0.89      0.76      0.81      1334
weighted avg       0.89      0.89      0.88      1334

この結果をテキストファイルに保存した後、このテキストファイルを開いた状態でチャット欄に結果の読み解き方を聞きます。すると、以下の通りの回答を得ました。

Copilotからの回答
もちろんです。出力結果の読み解き方について説明します。
    
カテゴリごとの性能指標
各カテゴリ(b, e, m, t)について、適合率(Precision)、再現率(Recall)、F1スコア(F1 Score)が表示されています。
    
適合率(Precision)
適合率は、モデルが正と予測したうち、実際に正であった割合を示します。計算式は以下の通りです: [ \\text{Precision} = \\frac{\\text{True Positives}}{\\text{True Positives} + \\text{False Positives}} ]
    
再現率(Recall)
再現率は、実際に正であったもののうち、モデルが正と予測した割合を示します。計算式は以下の通りです: [ \\text{Recall} = \\frac{\\text{True Positives}}{\\text{True Positives} + \\text{False Negatives}} ]
    
F1スコア(F1 Score)
F1スコアは、適合率と再現率の調和平均を示します。計算式は以下の通りです: [ \\text{F1 Score} = 2 \\times \\frac{\\text{Precision} \\times \\text{Recall}}{\\text{Precision} + \\text{Recall}} ]
    
カテゴリごとの性能
Category: b
    
Precision: 0.903
Recall: 0.944
F1 Score: 0.923
Category: e
    
Precision: 0.905
Recall: 0.991
F1 Score: 0.946
Category: m
    
Precision: 0.960
Recall: 0.552
F1 Score: 0.701
Category: t
    
Precision: 0.878
Recall: 0.656
F1 Score: 0.751
マイクロ平均(Micro-Average Metrics)
マイクロ平均は、全てのカテゴリの予測結果を一つの集合として計算します。これは、全てのTrue Positives、False Positives、False Negativesを合計してから計算します。
    
Precision: 0.904
Recall: 0.904
F1 Score: 0.904
マクロ平均(Macro-Average Metrics)
マクロ平均は、各カテゴリの指標を個別に計算し、その平均を取ります。これは、各カテゴリが同じ重みを持つことを意味します。
    
Precision: 0.912
Recall: 0.786
F1 Score: 0.830
読み解き方
カテゴリごとの性能: 各カテゴリに対して、モデルの適合率、再現率、F1スコアが表示されています。これにより、特定のカテゴリに対するモデルの性能を評価できます。
マイクロ平均: 全体の予測性能を評価するために使用されます。全てのサンプルを一つの集合として扱うため、データの不均衡に対して敏感です。
マクロ平均: 各カテゴリの性能を均等に評価するために使用されます。データの不均衡に対して鈍感です。
この結果を基に、モデルの性能を評価し、必要に応じて改善点を見つけることができます。例えば、カテゴリmの再現率が低いことから、このカテゴリに対するモデルの性能を向上させるための追加のデータ収集や特徴量エンジニアリングが必要かもしれません。

終わりに

今回は GitHub Copilot によるコードの生成について試してみました。

現時点でも十分優秀なコーディングをしてくれましたが、 現在も日々進化中の GitHub Copilot 等生成AI技術の展望が楽しみとなりました。

参考文献, 使用サイト

Discussion