【機械学習】Pythonで学習データの組み合わせによる識別率の比較

イントロ

Myoで計測した8動作の積分筋電位(IEMG波形)の50データのうち、2データをランダムで抽出して、識別器の学習データに使用する。5回ランダムで学習データを抽出して異なる5つの学習データの組み合わせがあるときに、学習データの組み合わせによって識別率がどの程度変化するかをグラフにプロットして調べる。

方法

識別率の指標として、
・学習データをさらにtrainデータとtestデータに7:3に分割して、trainデータを5回交差検証した時の識別率の平均
・trainデータで学習した識別器でtestデータを分類した時の識別率
・学習データに使用した以外の残りの48データを識別した時の識別率
をグラフにプロットして比較する。

識別器にはSVM(4カーネル)、LDA、k-NNを使用する。SVMのパラメータはgammaとCのみをグリッドサーチによって決定してそのベストスコアを交差検証の識別率として使用する。

予想

予想として、各識別器において学習データの組み合わせが変化した時の識別率の変化は、学習データの精度によって識別率がかなり変わるため、±10%程度であると考える。

実装

1. データのファイルパスの設定
import numpy as np
import random

# 各ジェスチャのファイルパス
path1 = "../8hand_gesture_dataset/HandGesture01.txt"
path2 = "../8hand_gesture_dataset/HandGesture02.txt"
path3 = "../8hand_gesture_dataset/HandGesture03.txt"
path4 = "../8hand_gesture_dataset/HandGesture04.txt"
path5 = "../8hand_gesture_dataset/HandGesture05.txt"
path6 = "../8hand_gesture_dataset/HandGesture06.txt"
path7 = "../8hand_gesture_dataset/HandGesture07.txt"
path8 = "../8hand_gesture_dataset/HandGesture08.txt"

path = [path1, path2, path3, path4, path5, path6, path7, path8]
2. データを呼び出すための関数の定義

以下のpath_to_data関数ではジェスチャのファイルパスとデータ番号を引数として渡すことで、引数に該当する筋電波形のデータのリストを取得できる。

# 取得したいジェスチャのデータをファイルパスとデータ番号から筋電データを取得する
def path_to_data(path, rand_num): 
    # 8hand_gesture_dataset内のテキストファイルの読み込み
    with open(path) as f:
        s = f.read()

    # テキストファイルの内容は全て文字列型なので、Numpy配列の機械学習しやすいように変換する
    s_l = s.split('\n\n')
    del s_l[0]

    all_data = np.array([])   
    k = 0 #1周目のループを判断するためのブール値 
    
    # 分割した50データはまだ文字列型の配列でまとめられているから、機械学習がしやすいようにデータを変換して、int型の配列に変換
    # rand_numにリスト型で指定した値で取得するデータを指定
    for num in rand_num:
        # 1データをセンサ数で分割
        data = s_l[num].split("\n")
        del data[0]

        # サンプルを8センサで読み取った、1センサあたり100サンプルの8×100のint型配列に変換
        sensor8 = []
        for i in range(len(data)):
            data[i] = data[i].replace('{', '').replace('}', '').replace(' ', '')
            data_str = data[i].split(",")
            for j in range(len(data_str)):
                if data_str[j] == '':
                    del data_str[j]
            data_int = [int(s) for s in data_str]
            sensor8.append(data_int)
        
        # 8×100を100×8に転置
        arr_sensor8 = np.array(sensor8).T
          
        if len(arr_sensor8) == 100:
            if k != 0:
                all_data = np.concatenate([all_data, arr_sensor8], 0)
            elif k == 0:
                all_data = arr_sensor8     
                k = 1
        
    # 変換したデータを連結した配列を返す
    return all_data


以下のdata_load関数では、50データから任意のデータ番号をリストで指定することで、該当するデータ番号のサンプルのリストとサンプルとマッチするクラスラベルのリストを取得できる。

# データの読み込み
# rand_num: データ番号リスト
def data_load(rand_num):
    gesture = 1 # 動作のクラス値の変数
    all_data = [] # 全ての動作データの保存配列
    
    for p in path:
        # print(p)
        # データの取得
        data = path_to_data(p, rand_num)

        # 全ての特徴データとクラスデータを1つの配列にまとめる処理
        # ループ1周目の処理
        if p == path1:
            all_data = data
            gesture_array = np.full(len(data), gesture)
        # 1周目以降の処理
        else:
            all_data = np.concatenate([all_data, data])
            gesture_array = np.concatenate([gesture_array, np.full(len(data), gesture)])
        # rand_num_ls.append(rand_num)

        gesture += 1  

    #print(len(all_data))

    # emgの特徴量[sensor1, sensor2, ... , sensor8]
    emg_data = all_data
    # classラベル
    # gesture_array
    return emg_data, gesture_array


以下のload_train_test()関数では、random_stateで乱数シード値を指定して、kで学習データに使用したいデータの数を指定することで、50データから学習データと未知データを生成する。

# データの読み込み
def load_train_test(random_state = 0, k = 2):
    random.seed(random_state)
    train_num = random.sample(range(50), k = k) # 50データ中から学習データに使用するデータ番号を選択

    # 学習データの読み込み
    emg_train, gesture_train = data_load(train_num)
    print(train_num)

    # 未知データの番号リスト
    test_num = list(range(50))
    for num in train_num:  
        test_num.remove(num)
    print(test_num)

    # テストデータの読み込み
    emg_test, gesture_test = data_load(test_num)
    
    return emg_train, emg_test, gesture_train, gesture_test
3. 識別器を生成するための関数の定義
from sklearn.svm import SVC # SVMライブラリ
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis # 線形判別分析ライブラリ
from sklearn.neighbors import KNeighborsClassifier # k近傍法ライブラリ
from sklearn.model_selection import train_test_split # データ分割ライブラリ
from sklearn.model_selection import cross_val_score # 交差検証ライブラリ

# 各識別モデル生成用の関数
def generate_dicriminater(x_trainval, y_trainval):
    
    x_train, x_test, y_train, y_test = train_test_split(x_trainval, y_trainval, test_size=0.3, random_state=0)
    
    # 識別器の保存リスト
    model_list = []
    
    # 識別器名リスト
    name_list = ["svm_linear", "svm_poly", "svm_rbf", "svm_sigmoid", "lda", "knn"]
    
    # score_list
    val_score_list = []
    test_score_list = []
    
    # SVM識別器の生成
    kernel_str = ["linear", "poly", "rbf", "sigmoid"]
    param_list = [0.001, 0.01, 0.1, 1, 10, 100]
    for k in kernel_str:
        print(k)
        best_score = 0
        best_parameters = {}
        # 交差検証を用いたグリッドサーチ
        for gamma in param_list:
            for C in param_list:
                # SVCのインスタンス生成
                model = SVC(kernel = k, gamma=gamma, C=C, random_state=None)
                # 交差検証 パラメータcvで分割方法を設定
                scores = cross_val_score(model, x_train, y_train, cv=5)
                # 交差検証による評価値の平均
                score = np.mean(scores)
                if score>best_score:
                    best_score = score
                    best_parameters = {'gamma' : gamma, 'C' : C}

        # もっともスコアの高いパラメータで識別器をインスタンス
        model = SVC(kernel=k, **best_parameters)
        
        # モデルの保存
        model_list.append(model)
    
        # best_parametersにおける識別器を作成
        model.fit(x_train, y_train)
        
        # testデータの評価
        test_score = model.score(x_test, y_test)
        
        # スコアの保存
        val_score_list.append(best_score)
        test_score_list.append(test_score)

        print('Best score on validation set: {}'.format(best_score))
        print('Best parameters: {}'.format(best_parameters))
        print('Test set score with best parameters: {}\n'.format(test_score))
        

    # 線形判別分析の識別器生成
    lda = LinearDiscriminantAnalysis()
    model_list.append(lda)

    # 交差検証
    scores = cross_val_score(lda, x_train, y_train, cv=5)
    val_score = np.mean(scores)

    # testデータの評価
    lda.fit(x_train, y_train)
    test_score =  lda.score(x_test, y_test)
    
    # スコアの保存
    val_score_list.append(val_score)
    test_score_list.append(test_score)

    print("LDA")
    print("Score on validation set: {}".format(val_score))
    print("test_set score: {}\n".format(test_score))


    # k近傍法の識別器生成
    knn = KNeighborsClassifier(n_neighbors = 3)
    model_list.append(knn)

    # 交差検証
    scores = cross_val_score(knn, x_train, y_train, cv=5)
    val_score = np.mean(scores)

    # testデータの評価
    knn.fit(x_train, y_train)
    test_score =  knn.score(x_test, y_test)
    
    # スコアの保存
    val_score_list.append(val_score)
    test_score_list.append(test_score)

    print("knn")
    print("Score on validation set: {}".format(val_score))
    print("test_set score: {}\n".format(test_score))
    
    return model_list, val_score_list, test_score_list, name_list
4. 識別率のリストを生成する
# データ番号による識別率の変動の取得
for i in [0,1,2,5,6]:
    print("Number {}\n".format(i))
    # emgとgestureの学習データと未知データの呼び出し
    emg_train, emg_unknown, gesture_train, gesture_unknown = load_train_test(random_state=i, k=2)

    # 識別器の生成と生成時の識別率の呼び出し
    model_list, val_score_list, test_score_list, name_list = generate_dicriminater(emg_train, gesture_train)
    
    un_score_list = []
    for m in model_list:
        m.fit(emg_train, gesture_train)
        un_score = m.score(emg_unknown, gesture_unknown)
        un_score_list.append(un_score)

    # list型からnp.array型に変換
    val_score_arr = np.array(val_score_list)
    test_score_arr = np.array(test_score_list)
    un_score_arr = np.array(un_score_list)
    
    if i == 0:
        val_score_summary = val_score_arr
        test_score_summary = test_score_arr
        un_score_summary = un_score_arr
    elif i > 0:
        val_score_summary = np.vstack([val_score_summary, val_score_arr])
        test_score_summary = np.vstack([test_score_summary, test_score_arr])
        un_score_summary = np.vstack([un_score_summary, un_score_arr])
    
    print(val_score_summary)
    print(test_score_summary)
    print(un_score_summary)
5. スコアのリストを転置する
val_summ = val_score_summary.T
test_summ = test_score_summary.T
un_summ = un_score_summary.T

print("valiation score")
print(val_summ)
print("\n")
print("test score")
print(test_summ)
print("\n")
print("unknown score")
print(un_summ)
6. 識別率をグラフにプロット
import matplotlib.pyplot as plt

data_num = np.arange(1,len(val_summ[0])+1)


fig = plt.figure(figsize = (13,15))

ax1 = fig.add_subplot(311)
for i in range(len(val_summ)):
    # ドットの配置
    ax1.scatter(data_num, val_summ[i], label=name_list[i]) 
    # ラインの配置
    ax1.plot(data_num, val_summ[i])
# グラフタイトル
plt.title("cross validation score")
# 軸ラベルの表示
plt.xlabel("Number of trials")
plt.ylabel("Identification rate")
# 横軸のtickを変更
plt.xticks(data_num)
plt.yticks(np.arange(0, 1.1, 0.1))
# グリッド線
plt.grid()
# 凡例の表示とグラフの表示
plt.legend(loc='lower right')

ax2 = fig.add_subplot(312)
for i in range(len(val_summ)):
    # ドットの配置
    ax2.scatter(data_num, test_summ[i], label=name_list[i]) 
    # ラインの配置
    ax2.plot(data_num, test_summ[i])

plt.title("test score")
# 軸ラベルの表示
plt.xlabel("Number of trials")
plt.ylabel("Identification rate")
# 横軸のtickを変更
plt.xticks(data_num)
plt.yticks(np.arange(0, 1.1, 0.1))
# グリッド線
plt.grid()
# 凡例の表示とグラフの表示
plt.legend(loc='lower right')

ax3 = fig.add_subplot(313)
for i in range(len(val_summ)):
    # ドットの配置
    ax3.scatter(data_num, un_summ[i], label=name_list[i]) 
    # ラインの配置
    ax3.plot(data_num, un_summ[i])

plt.title("unknown score")
# 軸ラベルの表示
plt.xlabel("Number of trials")
plt.ylabel("Identification rate")
# 横軸のtickを変更
plt.xticks(data_num)
plt.yticks(np.arange(0, 1.1, 0.1))
# グリッド線
plt.grid()
# 凡例の表示とグラフの表示
plt.legend(loc='lower right')
実行結果

cross validation score:学習データを分割した内のtrainデータで交差検証をした時の識別率
test score:学習データを分割した内のtestデータを識別した時の識別率
unknown score:50データの内、学習データ以外の未知データを識別した時の識別率

f:id:Yunos:20200508190953p:plain

グラフの変動から学習データの組み合わせの違いによる識別率の変化は±10%程度である。
また、サポートベクターマシンカーネルがrbfとsigmoidの場合、識別率が低いことからデータがIEMGであるときには、識別器の使用にはサポートベクタマシンのrbfもしくはsigmoidの使用は向かないと考えられる。