よっしーの私的空間

機械学習を中心に興味のあることについて更新します

Pythonで株の銘柄コード一覧を取得する方法

Python上で銘柄コード一覧を取得して、Pandas.DataFrameに格納する方法についてまとめます。手っ取り早くソースコードを見たい方は本記事最下段の参考まで飛んでください。

1.概要

  1. JPX(日本取引所)から最新の銘柄コード一覧(Excel)をPythonでダウンロード
  2. ダウンロードしたExcelPythonで開いてPandas.DataFrameに取り込む

2.JPXから銘柄コード一覧をダウンロード

RequestsというHTTPライブラリを使用して、Excelのダウンロードを行います。Requestsの詳細については以下が良くまとまっています。
Requests の使い方 (Python Library) - Qiita

import requests
url = "https://www.jpx.co.jp/markets/statistics-equities/misc/tvdivq0000001vg2-att/data_j.xls"
r = requests.get(url)
with open('data_j.xls', 'wb') as output:
    output.write(r.content)

Excelダウンロード用のURLを指定して、requests.get関数を使用レスポンスボディを取得して、content関数でデータ取得するだけです。URLはJPXのホームページから以下の通り取得可能です。
f:id:t-yoshi-book:20210605164739p:plain

3.ダウンロードしたExcelをPandas.DataFrameに取り込む

pandas.read_excel関数を使ってダウンロードしたExcelを開いてDataFrameに取り込むだけです。

import pandas as pd
stocklist = pd.read_excel("./data_j.xls")
stocklist.loc[stocklist["市場・商品区分"]=="市場第一部(内国株)",
              ["コード","銘柄名","33業種コード","33業種区分","規模コード","規模区分"]
             ]

4.データの内容

以下のようなデータです。
f:id:t-yoshi-book:20210605165201p:plain

東証一部や二部以外にETF等も取得できます。
また、業種や規模区分も取得できるので、グループ分けして分析したりもできそうです。


参考:ソースコード全文

import requests
url = "https://www.jpx.co.jp/markets/statistics-equities/misc/tvdivq0000001vg2-att/data_j.xls"
r = requests.get(url)
with open('data_j.xls', 'wb') as output:
    output.write(r.content)

import pandas as pd
stocklist = pd.read_excel("./data_j.xls")
stocklist.loc[stocklist["市場・商品区分"]=="市場第一部(内国株)",
              ["コード","銘柄名","33業種コード","33業種区分","規模コード","規模区分"]
             ]

TensorflowによるBiT(Big Transfer)の実装

2019年にGoogle Brainから発表された画像認識モデルBiT(Big Transfer)をファインチューニングする方法についてまとめます。BiTにかかわる解説は以下が良くまとまっていました。
パラメータ数10億!最新の巨大画像認識モデル「BiT」爆誕 & 解説 - Qiita

簡単に特徴をまとめると…

  • Efficientnetよりも様々なデータセットで良い精度を出している。
  • 超巨大なモデルで、膨大な画像を事前学習した脳筋モデル。
  • 前処理方法や学習時のパラメータ等がBiT Hyper-Ruleというルールで規定されており、BiT Hyper-Ruleに則って実装することでハイパーパラメータチューニングの手間が省ける。BiT Hyper-Ruleは実装に大きく関わるので重要

手っ取り早くソースコードを見たい方は本記事最下段の参考まで飛んでください。

1.BiT(Big Transfer)の概要

1.1.モデルの種類

事前学習データセットモデルの大きさの違いによって分類されています。事前学習データセットの種類は3、モデルの大きさの種類は5なので、3×5=15種類のモデルがあります。

① 事前学習データセットの違い

事前学習データセットの違いによって以下3種類に分かれます。

  • BiT-S(事前学習データ:ImageNet-1k)
  • BiT-M(事前学習データ:ImageNet-21k)
  • BiT-L(事前学習データ:JFT-300M)
② モデルの大きさの違い

BiTの構造はResNetをベースにしていて、以下5種類に分かれます。

  • R50x1(ResNet-50の幅を1倍)
  • R50x3(ResNet-50の幅を3倍)
  • R101x1(ResNet-101の幅を1倍)
  • R101x3(ResNet-101の幅を3倍)
  • R152x4(ResNet-152の幅を4倍)
公開されているモデル

BiT-S,MのR50x1~R152x4の計10モデルがTensorflow Hubで公開されています。

1.2.BiT Hyper-Rule

BiT Hyper-RuleとはBiTを実装するにあたって実施する前処理方法や学習時のパラメータを規定したもので、ハイパーパラメータチューニングの手間が省くためのものです。具体的には以下を規定しており、学習するデータセットの大きさに応じて決定するようです。

  • 入力画像の大きさ
  • MixUpの使用の有無
  • 学習ステップ数および学習率スケジュール
① 入力画像の大きさ

BiT Hyper-Ruleでは訓練するデータセットの大きさに応じて、以下の通りリサイズやクロップ(切り抜き)をするようです。なお、学習データはリサイズとクロップの両方を実施しますが、テストデータに対してはリサイズのみ実施します。

例えばCIFAR10の場合は入力画像の大きさは32×32pxなので、以下の通り学習データをResize⇒Cropします。

(画像はCifar10より)

② MixUpの使用の有無

学習データの数が20,000件より大きい場合にMixUpを使用します。
MixUpにかかわる解説は以下が良くまとまっています。
複数の画像を組み合わせるオーグメンテーション (mixup, CutMix) - け日記

③ 学習ステップ数および学習率スケジュール

学習ステップ数は学習画像数に応じて決定します。具体的には以下の通りです。

また、学習率は学習スケジュールの30%、60%、90%のタイミングで1/10にします。

2.BiTの実装方法(CIFAR10でファインチューニング)

BiTの実装方法についてまとめます。なお、実装にあたって以下のサンプルコードを参考にしていますが、サンプルコードはtf_flowersを使用しているのに対して、本記事ではCIFAR10を対象にファインチューニングをしています。そのため、画像の前処理の方法等を変更しています。また、個人的に使いやすいように色々変更しています。
big_transfer/big_transfer_tf2.ipynb at master · google-research/big_transfer · GitHub

2.1.CIFAR10の画像データをインポート

from tensorflow.keras.datasets import cifar10

# CIFAR-10のインポート
(x_train_load, y_train), (x_test_load, y_test) = cifar10.load_data()

2.2.入力画像のリサイズ

入力画像の大きさに従って画像をリサイズ・クロップします。CIFAR10の場合は32pxなので160pxにリサイズします。なお、クロップについてはデータ拡張(Data Augmentation)時に実施します。

import numpy as np
import cv2

# 変数定義
IMAGE_SIZE  = 32

# BiT Hyper-Ruleに則って画像サイズを定義
if IMAGE_SIZE <= 96:
    RESIZE_TO = 160
    CROP_TO = 128
else:
    RESIZE_TO = 512
    CROP_TO = 480

# 画像をリサイズ(拡大)する関数を定義
def upscale(image):
    size = len(image)
    data_upscaled = np.zeros((size, RESIZE_TO, RESIZE_TO, 3,))
    for i in range(len(image)):
        data_upscaled[i] = cv2.resize(image[i], dsize=(RESIZE_TO, RESIZE_TO), interpolation=cv2.INTER_CUBIC)
    image = np.array(data_upscaled, dtype=np.int)
    
    return image

# 画像リサイズ
x_train = upscale(x_train_load)
x_test  = upscale(x_test_load)

2.3.BiTモデルの定義

① 使用モデルについて

Tensorflow Hubで公開されているBiT-MのR50x1(ImageNet-21kで事前学習されたResNet-50同等規模のモデル)を使用。BiT-MでもBiT-Sでもパラメータの数は変わらないので、より性能の良いBiT-Mを使用することにしました。どうでもいいですが、BiT-Sの存在価値は良く分からないです…。
公式のサンプルコードではモデルを呼び出す際に「trainable=True」を指定していないですが、これを指定しないと重みが更新されずファインチューニングされないはずなので、「trainable=True」を指定しています。

bit_model = hub.KerasLayer("https://tfhub.dev/google/bit/m-r50x1/1", trainable=True)
② 学習率のスケジューリング

optimizerの定義とあわせて学習率のスケジュールを実装します。学習率は学習スケジュールの30%、60%、90%のタイミングで1/10にします。

BiTモデルの定義(ソースコード
import tensorflow_hub as hub
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras import optimizers

# 変数定義
NUM_CLASSES = 10 # CIFAR10の予測対象クラス数(=10)
DATASET_SIZE = len(x_train_load) # CIFAR10の訓練用データ数は50k

# BiT Hyper-Ruleに則って学習ステップ数を定義
if DATASET_SIZE < 20000:
    SCHEDULE_LENGTH = 500
    SCHEDULE_BOUNDARIES = [200, 300, 400]
elif DATASET_SIZE < 500000:
    SCHEDULE_LENGTH = 10000
    SCHEDULE_BOUNDARIES = [3000, 6000, 9000]
else:
    SCHEDULE_LENGTH = 20000
    SCHEDULE_BOUNDARIES = [6000, 12000, 18000]

# BiTモデル定義
def buildModel_BiT():
    bit_model = hub.KerasLayer("https://tfhub.dev/google/bit/m-r50x1/1", trainable=True)
    model = tf.keras.Sequential([
        bit_model,
        tf.keras.layers.Dense(NUM_CLASSES, kernel_initializer='zeros', activation="softmax")
    ],
    name = 'BiT')
    
    lr = 0.003 * BATCH_SIZE / 512 
    
    lr_schedule = tf.keras.optimizers.schedules.PiecewiseConstantDecay(boundaries=SCHEDULE_BOUNDARIES, 
                                                                   values=[lr, lr*0.1, lr*0.001, lr*0.0001])
    optimizer = tf.keras.optimizers.SGD(learning_rate=lr_schedule, momentum=0.9)
    
    model.compile(optimizer=optimizer,
                  loss="categorical_crossentropy",
                  metrics=["accuracy"])
    
    return model

2.4.訓練用関数の定義

学習用データを訓練データと評価データに分割し、データ拡張(Data Augmentation)をし、訓練を実行する関数を定義します。データ拡張の際にBiT Hyper-Ruleに則ってMixUpとCropをするのですが、デフォルトのImageDataGenerator(tensorflow.keras.preprocessing.image.ImageDataGenerator)ではMixUpやCropはできないので、ImageDataGeneratorを継承した独自ジェネレータを使用しています。独自ジェネレータの作成にあたってこちらを参考にしました。本章では独自ジェネレータのソースについては割愛しますが、ソースを見たい方は参考をご確認ください。

# 訓練用関数の定義
def train_BiT(X, y, STEPS_PER_EPOCH, SCHEDULE_LENGTH, BATCH_SIZE):
    # 訓練データと評価データの分割
    X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2, stratify=y, shuffle=True)
    y_train = to_categorical(y_train)
    y_valid = to_categorical(y_valid)
    
    # Data Augmentation
    datagen = MyImageDataGenerator(horizontal_flip=True,
                                   mix_up_alpha=0.1, # データ数<20kの場合はMixUpは実施しない。
                                   random_crop=(CROP_TO, CROP_TO)
                                  )
    train_generator = datagen.flow(X_train, y_train,batch_size=BATCH_SIZE)
        
    # モデル構築
    model = buildModel_BiT()

    # 学習
    history = model.fit(train_generator,
                        steps_per_epoch=STEPS_PER_EPOCH,
                        epochs=10, #公式の推奨値はint(SCHEDULE_LENGTH/STEPS_PER_EPOCH)。時間がかかりすぎるので便宜上10を設定。
                        validation_data=(X_valid, y_valid),
                        shuffle=True
                       )
    
    return model, history

2.5.訓練実行

バッチサイズの公式推奨値は512なのですが、GPUメモリが足りずResourceExhaustedErrorになってしまうので、128まで下げています。(私の環境はGPUメモリを8GB積んでいるのですが、256だとエラーになっちゃいました。)

# 訓練開始
BATCH_SIZE = 128 #公式の推奨値は512。GPUメモリが足りないので仮で128を設定。
SCHEDULE_LENGTH = SCHEDULE_LENGTH * 512 / BATCH_SIZE
STEPS_PER_EPOCH = 10

model, history = train_BiT(x_train, y_train, STEPS_PER_EPOCH, SCHEDULE_LENGTH, BATCH_SIZE)


参考:ソースコード全文

# 再現性確保
import os
os.environ['PYTHONHASHSEED'] = '0'
import tensorflow as tf
os.environ['TF_DETERMINISTIC_OPS'] = 'true'
os.environ['TF_CUDNN_DETERMINISTIC'] = 'true'

import numpy as np
import random as rn

SEED = 123
def reset_random_seeds():
    tf.random.set_seed(SEED)
    np.random.seed(SEED)
    rn.seed(SEED)

reset_random_seeds()    

session_conf = tf.compat.v1.ConfigProto(intra_op_parallelism_threads=32, inter_op_parallelism_threads=32)
tf.compat.v1.set_random_seed(SEED)
sess = tf.compat.v1.Session(graph=tf.compat.v1.get_default_graph(), config=session_conf)


# ライブラリインポート
import pandas as pd

import tensorflow_hub as hub

from sklearn.model_selection import train_test_split
from tensorflow.keras.datasets import cifar10
import cv2
import matplotlib.pyplot as plt

from tensorflow.keras.utils import to_categorical
from tensorflow.keras.preprocessing.image import ImageDataGenerator

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras import optimizers

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix


# CIFAR-10のインポート
(x_train_load, y_train), (x_test_load, y_test) = cifar10.load_data()


# 変数定義
## CIFAR10の画像サイズ、データ数、予測対象クラス数を格納
IMAGE_SIZE  = 32
DATASET_SIZE = len(x_train_load) # CIFAR10の訓練用データ数は50k
NUM_CLASSES = 10

## BiT Hyper-Ruleに則って画像サイズを定義
if IMAGE_SIZE <= 96:
    RESIZE_TO = 160
    CROP_TO = 128
else:
    RESIZE_TO = 512
    CROP_TO = 480

## BiT Hyper-Ruleに則って学習ステップ数を定義
if DATASET_SIZE < 20000:
    SCHEDULE_LENGTH = 500
    SCHEDULE_BOUNDARIES = [200, 300, 400]
elif DATASET_SIZE < 500000:
    SCHEDULE_LENGTH = 10000
    SCHEDULE_BOUNDARIES = [3000, 6000, 9000]
else:
    SCHEDULE_LENGTH = 20000
    SCHEDULE_BOUNDARIES = [6000, 12000, 18000]


# 画像をリサイズ(拡大)する関数を定義
def upscale(image):
    size = len(image)
    data_upscaled = np.zeros((size, RESIZE_TO, RESIZE_TO, 3,))
    for i in range(len(image)):
        data_upscaled[i] = cv2.resize(image[i], dsize=(RESIZE_TO, RESIZE_TO), interpolation=cv2.INTER_CUBIC)
    image = np.array(data_upscaled, dtype=np.int)
    
    return image


# 画像リサイズ
x_train = upscale(x_train_load)
x_test  = upscale(x_test_load)
# データ正規化
# BiTでは0~1で表現された画像を使用
x_train  = np.array(x_train/255, dtype=np.float32)
x_test  = np.array(x_test/255, dtype=np.float32)

# リサイズ前後の画像を比較
plt.subplot(121).imshow(x_train_load[0])
plt.subplot(122).imshow(x_train[0])
plt.show()
print("※左図:リサイズ前(32*32)、右図:リサイズ後(160*160)")


# ImageDataGeneratorを継承してMix-upやRandom Croppingのできる独自ジェネレーターを定義
# 参考 https://qiita.com/koshian2/items/909360f50e3dd5922f32
class MyImageDataGenerator(ImageDataGenerator):
    def __init__(self, featurewise_center = False, samplewise_center = False, 
                 featurewise_std_normalization = False, samplewise_std_normalization = False, 
                 zca_whitening = False, zca_epsilon = 1e-06, rotation_range = 0.0, width_shift_range = 0.0, 
                 height_shift_range = 0.0, brightness_range = None, shear_range = 0.0, zoom_range = 0.0, 
                 channel_shift_range = 0.0, fill_mode = 'nearest', cval = 0.0, horizontal_flip = False, 
                 vertical_flip = False, rescale = None, preprocessing_function = None, data_format = None, validation_split = 0.0, 
                 random_crop = None, mix_up_alpha = 0.0):
        # 親クラスのコンストラクタ
        super().__init__(featurewise_center, samplewise_center, featurewise_std_normalization, samplewise_std_normalization, zca_whitening, zca_epsilon, rotation_range, width_shift_range, height_shift_range, brightness_range, shear_range, zoom_range, channel_shift_range, fill_mode, cval, horizontal_flip, vertical_flip, rescale, preprocessing_function, data_format, validation_split)
        # 拡張処理のパラメーター
        # Mix-up
        assert mix_up_alpha >= 0.0
        self.mix_up_alpha = mix_up_alpha
        # Random Crop
        assert random_crop == None or len(random_crop) == 2
        self.random_crop_size = random_crop

    # ランダムクロップ
    # 参考 https://jkjung-avt.github.io/keras-image-cropping/
    def random_crop(self, original_img):
        # Note: image_data_format is 'channel_last'
        assert original_img.shape[2] == 3
        if original_img.shape[0] < self.random_crop_size[0] or original_img.shape[1] < self.random_crop_size[1]:
            raise ValueError(f"Invalid random_crop_size : original = {original_img.shape}, crop_size = {self.random_crop_size}")

        height, width = original_img.shape[0], original_img.shape[1]
        dy, dx = self.random_crop_size
        x = np.random.randint(0, width - dx + 1)
        y = np.random.randint(0, height - dy + 1)
        return original_img[y:(y+dy), x:(x+dx), :]

    # Mix-up
    # 参考 https://qiita.com/yu4u/items/70aa007346ec73b7ff05
    def mix_up(self, X1, y1, X2, y2):
        assert X1.shape[0] == y1.shape[0] == X2.shape[0] == y2.shape[0]
        batch_size = X1.shape[0]
        l = np.random.beta(self.mix_up_alpha, self.mix_up_alpha, batch_size)
        X_l = l.reshape(batch_size, 1, 1, 1)
        y_l = l.reshape(batch_size, 1)
        X = X1 * X_l + X2 * (1-X_l)
        y = y1 * y_l + y2 * (1-y_l)
        return X, y

    def flow(self, x, y=None, batch_size=32, shuffle=True, sample_weight=None,
             seed=None, save_to_dir=None, save_prefix='', save_format='png', subset=None):
        batches = super().flow(x=x, y=y, batch_size=batch_size, shuffle=shuffle, sample_weight=sample_weight,
                               seed=seed, save_to_dir=save_to_dir, save_prefix=save_prefix, save_format=save_format, subset=subset)
        # 拡張処理
        while True:
            batch_x, batch_y = next(batches) 
            if self.mix_up_alpha > 0:
                while True:
                    batch_x_2, batch_y_2 = next(batches)
                    m1, m2 = batch_x.shape[0], batch_x_2.shape[0]
                    if m1 < m2:
                        batch_x_2 = batch_x_2[:m1]
                        batch_y_2 = batch_y_2[:m1]
                        break
                    elif m1 == m2:
                        break
                batch_x, batch_y = self.mix_up(batch_x, batch_y, batch_x_2, batch_y_2)
            # Random crop
            if self.random_crop_size != None:
                x = np.zeros((batch_x.shape[0], self.random_crop_size[0], self.random_crop_size[1], 3))
                for i in range(batch_x.shape[0]):
                    x[i] = self.random_crop(batch_x[i])
                batch_x = x
            # 返り値
            yield (batch_x, batch_y)


# BiTモデル定義
def buildModel_BiT():
    bit_model = hub.KerasLayer("https://tfhub.dev/google/bit/m-r50x1/1", trainable=True)
    model = tf.keras.Sequential([
        bit_model,
        tf.keras.layers.Dense(NUM_CLASSES, kernel_initializer='zeros', activation="softmax")
    ],
    name = 'BiT')
    
    lr = 0.003 * BATCH_SIZE / 512 
    
    lr_schedule = tf.keras.optimizers.schedules.PiecewiseConstantDecay(boundaries=SCHEDULE_BOUNDARIES, 
                                                                   values=[lr, lr*0.1, lr*0.001, lr*0.0001])
    optimizer = tf.keras.optimizers.SGD(learning_rate=lr_schedule, momentum=0.9)
    
    model.compile(optimizer=optimizer,
                  loss="categorical_crossentropy",
                  metrics=["accuracy"])
    
    return model

# 訓練用関数の定義
def train_BiT(X, y, STEPS_PER_EPOCH, SCHEDULE_LENGTH, BATCH_SIZE):
    # 訓練データと評価データの分割
    X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2, stratify=y, shuffle=True)
    y_train = to_categorical(y_train)
    y_valid = to_categorical(y_valid)
    
    # Data Augmentation
    datagen = MyImageDataGenerator(horizontal_flip=True,
                                   mix_up_alpha=0.1, # データ数<20kの場合はMixUpは実施しない。
                                   random_crop=(CROP_TO, CROP_TO)
                                  )
    train_generator = datagen.flow(X_train, y_train,batch_size=BATCH_SIZE)
        
    # モデル構築
    model = buildModel_BiT()

    # 学習
    history = model.fit(train_generator,
                        steps_per_epoch=STEPS_PER_EPOCH,
                        epochs=10, #公式の推奨値はint(SCHEDULE_LENGTH/STEPS_PER_EPOCH)。時間がかかりすぎるので便宜上10を設定。
                        validation_data=(X_valid, y_valid),
                        shuffle=True
                       )
    
    return model, history

# 訓練開始
BATCH_SIZE = 128 #公式の推奨値は512。GPUメモリが足りないので仮で128を設定。
SCHEDULE_LENGTH = SCHEDULE_LENGTH * 512 / BATCH_SIZE
STEPS_PER_EPOCH = 10

model, history = train_BiT(x_train, y_train, STEPS_PER_EPOCH, SCHEDULE_LENGTH, BATCH_SIZE)


# 予測
X = x_test
pred = model.predict(X)

# 予測結果の確認
df_pred = pd.DataFrame(pred)
pred = np.array(df_pred.idxmax(axis=1))
df_pred = pd.DataFrame(pred)
df_y = pd.DataFrame(y_test)
df_result = pd.concat([df_y, df_pred], axis=1, join_axes=[df_y.index])
df_result.columns = ['y','pred']
display(df_result)

# 予測結果の評価(混合行列、Accuracy、Precision、Recall、F_score)
print('Confusion Matrix:')
print(confusion_matrix(df_result['y'],df_result['pred']))
print()
print('Accuracy :{:.4f}'.format(accuracy_score(df_result['y'],df_result['pred'])))
print('Precision:{:.4f}'.format(precision_score(df_result['y'],df_result['pred'],average='macro')))
print('Recall   :{:.4f}'.format(recall_score(df_result['y'],df_result['pred'],average='macro')))
print('F_score  :{:.4f}'.format(f1_score(df_result['y'],df_result['pred'],average='macro')))

横持ちデータを縦持ちデータに変換する方法(Pandas)

PythonのPandasで横持ちデータを縦持ちにデータに変換する方法についてまとめます。 縦持ちデータを横持ちデータに変換する方法については過去記事にまとめています。

1.実施内容(概要)

Pandasの以下のような横持ちデータを

氏名 数学 国語 英語 ドイツ語 アラビア語
A君 100 70 60 60 NaN
B君 60 50 50 NaN 60

以下のような縦持ちデータに変換します。

氏名 科目 点数
A君 数学 100
A君 国語 70
A君 英語 60
A君 ドイツ語 60
B君 数学 60
B君 国語 50
B君 英語 50
B君 アラビア語 60

縦持ちに変換するためにMelt関数を使用します。(公式ドキュメントはこちら

Melt関数は以下の通り使用します。

pandas.melt(frame, id_vars=None, value_vars=None, var_name=None, value_name='value', col_level=None)

それぞれの引数の解説は以下の通り。
frame:縦持ちに変換するデータフレーム。
id_vars:そのまま変形せずに残す列。今回の場合は「氏名」列のこと。
value_vars:縦持ちに変換する列。指定しない場合は、id_varsで指定していない全列を使用。
var_name:変数の列名。今回の例の場合は縦持ちにしたときの「科目」のこと。
value_name:値の列名。今回の例の場合は縦持ちにした時の「点数」のこと。
col_level:データフレームがマルチインデックスの場合、インデックスの数を指定。

2.実装方法

まずは以下のように横持ちデータのサンプルを作成します。

df_score_horizontal = pd.DataFrame([['A君',100,70,60,60,np.nan],
                                  ['B君',60,50,50,np.nan,60]],
                                 columns=['氏名','数学','国語','英語','ドイツ語','アラビア語'])

以下で横持ちを縦持ちに変換できます。

pd.melt(df_score_horizontal,
        id_vars=df_score_horizontal.columns.values[:1],
        var_name="科目",
        value_name="点数"
       )
  • id_varsで指定した列(氏名)はそのまま残しつつ、その他の列(数学~アラビア語)は縦持ちに変換する列として使用。
  • 横持ちのときは列名だった「数学~アラビア語」は縦持ちに変換するとデータ内に組み込まれるので、「数学~アラビア語」を総称する列名としてvar_name="科目"と指定。
  • 最後に縦持ちに変換した際のデータの総称をvalue_name="点数"と指定。

機械学習による株価予測(LightGBM)

LightGBMを使用して株価予測をしていきたいと思います。ソースの全文は本記事の末尾に載せました。忙しい人は1.概要と末尾のソースで十分かもしれません。

1.概要

1.1.予測対象

将来のデンソー(6902)株価の上下動を予測して、買うか買わないか判断するプログラムを実装します。
具体的には20日後までの株価について以下を予測します。
① 最大株価が5%以上上昇するか
② 最小株価が5%以上下落しないか
③ 平均株価が0%以上上昇するか

上記をすべて満たす場合に購入することとします。株価を直接予測することもできるのですが、あまり精度が出ませんでした。

1.2.説明変数

説明変数は以下の通りです。
日経平均株価
② ダウ平均株価
NASDAQ総合指数
④ S&P500種指数
トヨタ株価
⑥ 各国の為替
⑦ 失業率
⑧ インフレ率
消費者物価指数
⑩ 各種コモディティ指数(金や麦等)
⑪ 不動産価格(J-REIT

1.3.分析期間

訓練データ:2016年1月1日~2018年12月31日
試験データ:2019年1月1日~2020年1月1日

コロナによる暴落は予測しようがないと思ったので、分析期間から除外しました。

1.4.予測結果

試験データを対象に予測を実施し、混同行列により評価を行いました。結果は以下の通りです。

AIの判断に従って購入した場合と完全にランダムに購入した場合の勝率の違いは以下の通りです。

  • AIの判断:24/(24+16)=60%
  • ランダム:(24+62)/(24+62+16+139)=36%

悪くない結果なんじゃないかなと思います。ただ、機会損失が62回発生している点は注意が必要。

今後の改善点は以下2点。

  • コロナ禍以降の株価予測もできるように、より汎用的なモデルにしていきたい
  • 機会損失を減らしたい

説明変数を増やしたりしたら改善できますかね。。色々試してみます。

2.株価データ等の取得

pandas-datareaderというライブラリでデータを簡単に取得できます。
取得先はStooqというポーランドサイトとFREDという米国サイトです。

pandas-datareaderの使い方は以下の通りです。

# ライブラリのインポート
import pandas_datareader.data as web

# Stooqからデータ取得
stooq = web.DataReader('6902.JP','stooq',start=st,end=ed)
# FREDからデータ取得
fred = web.DataReader('NIKKEI225','fred',start=st,end=ed)
① 取得先サイトの指定(pandas-datareader第2引数)

DataReaderの第2引数に取得先を定義します。Stooqの場合は'stooq'、FREDの場合は'fred'。
Stooqでは株価やETFの価格が取得できます。本当は各種指数や為替等の情報も取得できそうなのですが、なぜか私の環境ではうまく取得できませんでした。取得できなかった情報はFREDから取得するようにしました。

② 取得銘柄の指定(pandas-datareader第1引数)

・Stooqの場合
「6902.JP」のように「銘柄番号.JP」という形で定義することで取得可能です。同じ方法でETFも取得可能です。
・FREDの場合
FREDのサイト上でDataReaderに引き渡すキーを調べることができます。例えば、日経平均株価の場合は以下の通り。その他の指数等についても幅広く扱っているので、色々検索してみるとおもしろいかもしれません。

(引用:Nikkei Stock Average, Nikkei 225 (NIKKEI225) | FRED | St. Louis Fed

・複数銘柄を取得する場合
第1引数にリストを渡すことで複数銘柄を同時に取得することも可能です。

# 複数銘柄取得する場合の例
lst1=['6902.JP', #デンソー
      '1540.JP', #ETF(金)
      '1398.JP', #ETF(J-REIT(不動産))
      '7203.JP', #トヨタ
    ]
stooq = web.DataReader(lst1,'stooq',start=st,end=ed)

3.データの前処理

3.1.StooqとFREDから取得したデータを結合して、欠損値を補完する

データの種類によっては月次のデータだったりするので、強引ですが直前のデータで欠損値を補完しました。

# StooqとFredから取得したデータを結合する(結合キーはインデックス=Date)
stock = pd.merge(stooq['Close'], fred, how='left', left_index=True, right_index=True)
# 欠損値を直前のデータで補完する
stock = stock.fillna(method='ffill')

3.2.目的変数データの作成

概要でも触れたように、分析対象は以下3点。
① 基準日から20日間の最大株価の上昇率が5%以上になるか
② 基準日から20日間の最小株価の下落率が5%以下に留まるか
③ 基準日から20日間の平均株価の上昇率が0%以上になるか
なので、上記3点をフラグ化します。

特定の期間におけるデータの集計はrolling関数を使用することで簡単になります。例えば、rolling(5).max()とすると、基準日を含めた5日分のデータから最大値が抽出されます。

rollingを使用する際の注意点なのですが、基準日以前のデータが集計されます。なので、未来日の集計を行いたい場合は少し工夫が必要で、shift関数を組み合わる必要があります。例えば、翌日から5日間の最大値を抽出したい場合はrolling(5).max().shift(-5)となるわけです。

実装は以下の通りです。

# 20営業日先の最大株価(5%以上上昇した場合:1、しなかった場合:0)
stock['target.future.max'] = stock['6902.JP'].rolling(20).max().shift(-20)
stock['target.ratio.max'] = stock['target.future.max']/stock['6902.JP']
stock['target.binary.max'] = np.where(stock['target.ratio.max'] > 1.05, 1, 0)
# 20営業日先の最小株価(5%以上下落しなかった場合:1、した場合:0)
stock['target.future.min'] = stock['6902.JP'].rolling(20).min().shift(-20)
stock['target.ratio.min'] = stock['target.future.min']/stock['6902.JP']
stock['target.binary.min'] = np.where(stock['target.ratio.min'] < 0.95, 0, 1)
# 20営業日先の最小株価(0%以上上昇した場合:1、しなかった場合:0)
stock['target.future.mean'] = stock['6902.JP'].rolling(20).mean().shift(-20)
stock['target.ratio.mean'] = stock['target.future.mean']/stock['6902.JP']
stock['target.binary.mean'] = np.where(stock['target.ratio.mean'] > 1.00, 1, 0)
# 不要カラムの削除
stock = stock.drop(columns=['target.future.max','target.future.min','target.future.mean'])
stock = stock.drop(columns=['target.ratio.max','target.ratio.min','target.ratio.mean'])

3.3.説明変数データの作成

StooqとFREDから取得したデータをそのまま使ってもいいのですが、各データの時系列的な変化が表現されないので、各データの移動平均を算出し、現在値との乖離率を算出しました。移動平均の採用期間は5日、10日、・・・、55日、60日としました。

# 各指数の移動平均との乖離率を算出する
for stock_name in lst1:
    for i in [5,10,15,20,25,30,35,40,45,50,55,60]:
        exec('stock["{}_{}days_diverge"] = \
             (stock["{}"].rolling({}).mean() - stock["{}"])/stock["{}"].rolling({}).mean()' \
             .format(stock_name,i,stock_name,i,stock_name,stock_name,i)
            )
for stock_name in lst2:
    for i in [5,10,15,20,25,30,35,40,45,50,55,60]:
        exec('stock["{}_{}days_diverge"] = \
             (stock["{}"].rolling({}).mean() - stock["{}"])/stock["{}"].rolling({}).mean()' \
             .format(stock_name,i,stock_name,i,stock_name,stock_name,i)
            )
# 各指数の列を削除する
stock = stock.drop(columns=lst1)
stock = stock.drop(columns=lst2)

3.4.訓練用データと試験データを分割

# データ期間の定義
st_train = '2016/01/01'
ed_train = '2018/12/31'
st_test = '2019/01/01'
ed_test = '2020/01/01'
# TrainデータとTestデータを分割
df_train = stock[st_train : ed_train]
df_test  = stock[st_test : ed_test]
df_train = df_train.reset_index(drop=True)
df_test = df_test.reset_index(drop=True)

3.5.Numpyに変換し説明変数と目的変数に分割する

# numpyに変換する関数を定義
def toNumpy(x_df, t_max_df, t_min_df, t_mean_df):
    X = x_df.values
    
    y_max = t_max_df.values
    y_max = y_max.reshape(-1)
    
    y_min = t_min_df.values
    y_min = y_min.reshape(-1)
    
    y_mean = t_mean_df.values
    y_mean = y_mean.reshape(-1)
    
    return X, y_max, y_min, y_mean

# 目的変数と説明変数に分ける関数を定義
def split(inputData):
    ## 学習データの分割
    x_df = inputData.drop(columns=['target.binary.min','target.binary.max','target.binary.mean'])
    t_max_df  = inputData['target.binary.max']
    t_min_df  = inputData['target.binary.min']
    t_mean_df = inputData['target.binary.mean']
    ## Numpyに変換
    X, y_max, y_min, y_mean = toNumpy(x_df, t_max_df, t_min_df, t_mean_df)
    return X, y_max, y_min, y_mean

# データ作成
## 学習用データ
X_train, y_trainmax, y_trainmin, y_trainmean = split(df_train)

## 試験用データ
X_test, y_testmax, y_testmin, y_testmean = split(df_test)

4.LightGBMの実装と学習実施

LightGBMを使って分類問題を学習します。Optunaを使用して、ハイパーパラメータを自動チューニングしています。Optunaの使用方法は非常に簡単で、以下の通り、ライブラリをインポートして、通常通りtrain関数で学習するだけでOKです。

from optuna.integration import lightgbm as lgb

交差検証はKFoldを使用して5分割にしました。データリーク(学習用データと評価用データのデータ期間が被ること)を防ぐために、Shuffleはしないようにしています。実装は以下の通りです。以下を実行することでbests_max、bests_min、bests_meanというリストが作成されます。bests_maxには最大株価、bests_minには最小株価、bests_meanには平均株価の変動を予測したモデルがそれぞれ5つ格納されます(KFoldを5分割としたため)。

# ライブラリのインポート
from sklearn.model_selection import KFold
from optuna.integration import lightgbm as lgb

# 乱数シードの固定
SEED = 123

# LightGBM訓練用関数を定義
def trainGbdt(X_train, y_train):
    bests = []
    kf = KFold(n_splits=5, shuffle=False)
    for learn_index, valid_index in kf.split(X_train, y_train):
        X_learn, X_valid = X_train[learn_index], X_train[valid_index]
        y_learn, y_valid = y_train[learn_index], y_train[valid_index]
        
        #LGB用のデータに変形
        lgb_learn = lgb.Dataset(X_learn, y_learn)
        lgb_valid = lgb.Dataset(X_valid, y_valid)
        
        param = {
            'objective': 'binary',
            'metric': 'auc',
            'boosting_type': 'gbdt',
            'random_seed':SEED,
        }
        
        # 訓練実施
        best = lgb.train(param, 
                         lgb_learn,
                         valid_sets=[lgb_learn,lgb_valid],
                        )
        bests.append(best)
        
    return bests

# 勾配ブースティングによる学習実施(最大株価予測)
bests_max = trainGbdt(X_train, y_trainmax)
# 勾配ブースティングによる学習実施(最小株価予測)
bests_min = trainGbdt(X_train, y_trainmin)
# 勾配ブースティングによる学習実施(平均株価予測)
bests_mean = trainGbdt(X_train, y_trainmean)

5.テストデータによる予測

作成したモデルを使用してテストデータの予測を実施します。実装は以下の通り。

# テストデータを予測する(最大株価予測)
y_preds = []
for i in range(5):
    y_pred = bests_max[i].predict(X_test, num_iteration=bests_max[i].best_iteration)
    y_preds.append(y_pred)
y_predmax = (y_preds[0] + y_preds[1] + y_preds[2] + y_preds[3] + y_preds[4])/5
# テストデータを予測する(最小株価予測)
y_preds = []
for i in range(5):
    y_pred = bests_min[i].predict(X_test, num_iteration=bests_min[i].best_iteration)
    y_preds.append(y_pred)
y_predmin = (y_preds[0] + y_preds[1] + y_preds[2] + y_preds[3] + y_preds[4])/5
# テストデータを予測する(平均株価予測)
y_preds = []
for i in range(5):
    y_pred = bests_mean[i].predict(X_test, num_iteration=bests_mean[i].best_iteration)
    y_preds.append(y_pred)
y_predmean = (y_preds[0] + y_preds[1] + y_preds[2] + y_preds[3] + y_preds[4])/5

5.予測結果の表示

5.1.予測結果の取りまとめ

# 予測値と実値との比較
result = np.c_[y_predmax,y_testmax,
               y_predmin,y_testmin,
               y_predmean,y_testmean
              ]
df_result = pd.DataFrame(data=result,
                         columns=['pred_max','true_max',
                                  'pred_min','true_min',
                                  'pred_mean','true_mean'
                                 ])
display(df_result)

上記を実行すると以下の通り出力される。

上記の通り、予測値が確率表現となってしまっているので、閾値を設けて、1か0に分類する必要がある。今回は四捨五入して1と0に分類する。ついでに買うか買わないかの判断も結果に表示するようにします。判断基準は概要にも記載しましたが、最大株価が5%以上上昇し、最小株価が5%以上下落せず、平均株価が0%以上上昇した場合は買いと判断することとします。

# 閾値0.5として1,0に分類
df_result['pred_max'] = np.round(df_result['pred_max'])
df_result['pred_min'] = np.round(df_result['pred_min'])
df_result['pred_mean'] = np.round(df_result['pred_mean'])

df_result['buy_flg'] = df_result['pred_max'] * df_result['pred_min'] * df_result['pred_mean']
df_result['buy_flg_true'] = df_result['true_max'] * df_result['true_min'] * df_result['true_mean']
display(df_result)

結果は以下の通り。

5.2.予測結果の混合行列(Confusion Matrix)を表示

最後に混合行列を出力し、どの程度予測が的中したか確認します。コードは以下の通り。

# 混合行列(コンフュージョンマトリクス)を表示する関数を定義
def showConfusionMatrix(true,pred,pred_type):
    cm = confusion_matrix(true, pred, labels=[1, 0])
    labels = [1, 0]
    # データフレームに変換
    cm_labeled = pd.DataFrame(cm, columns=labels, index=labels)
    # 結果の表示
    print("◆混合行列(",pred_type,")◆")
    display(cm_labeled)

# 混合行列を表示
showConfusionMatrix(df_result['true_max'], df_result['pred_max'],"最大株価")
showConfusionMatrix(df_result['true_min'], df_result['pred_min'],"最小株価")
showConfusionMatrix(df_result['true_mean'], df_result['pred_mean'],"平均株価")
showConfusionMatrix(df_result['buy_flg_true'], df_result['buy_flg'],"買うか買わないか")

結果は以下の通りです。

混合行列の見方ですが、「混合行列(最大株価)」を例に解説します。

上記の図でなんとなく伝わると思いますが、29+127件はAIが正しく予測できた件数で、16+69件はAIが外した件数です。
株価予測では上記の内、TPとFPが重要になると考えます。なぜかというと、AIの言う通りに株を購入する場合、TP+FPが実際に購入する件数になるからです。また、TPは実際に株価が上昇する件数になりますので、TP/(TP+FP)が、AIの言う通りに株を購入した場合の勝率となります。混合行列を使った精度の評価についてはこちらのサイトが良くまとまっています。

続いて、買うか買わないかの判断をAIに任せた場合の勝率について見てみましょう。4つ目の混合行列(◆混合行列(買うか買わないか)◆)に着目します。24/(24+16)=60%の確率で勝てることになることが分かります。完全にランダムに買う場合は(24+62)/(16+139)=36%の勝率なので、悪くない結果なのではないでしょうか。

参考:ソースコード全文

# ライブラリのインポート
import pandas as pd
pd.set_option('display.max_rows', 500)
import numpy as np
import pandas_datareader.data as web

from sklearn import metrics
from sklearn.metrics import confusion_matrix,mean_squared_error
from sklearn.model_selection import KFold

from optuna.integration import lightgbm as lgb

# データ取得期間の定義
## 欠損値補完のために直前のデータを使うため、Startは少し長めに取得。
## 実際の分析期間は2016-01-01~とする。
st = '2015/01/01'
ed = '2021/01/01'
st_train = '2016/01/01'
ed_train = '2018/12/31'
st_test = '2019/01/01'
ed_test = '2020/01/01'

# 乱数シードの固定
SEED = 123

# STOOQから株価価格等の取得
lst1=['6902.JP', #デンソー
      '1540.JP', #ETF(金)
      '1398.JP', #ETF(J-REIT(不動産))
      '7203.JP', #トヨタ
    ]
stooq = web.DataReader(lst1,'stooq',start=st,end=ed)

# FREDから各種指数の取得
lst2=['NIKKEI225',       #日経平均
      'T10YIE',          #10-Year Breakeven Inflation Rate
      'T5YIE',           #5-Year Breakeven Inflation Rate
      'IRLTLT01USM156N', #Long-Term Government Bond Yields: 10-year: Main (Including Benchmark) for the United States
      'IRLTLT01EZM156N', #Long-Term Government Bond Yields: 10-year: Main (Including Benchmark) for the Euro Area
      'IRLTLT01JPM156N', #Long-Term Government Bond Yields: 10-year: Main (Including Benchmark) for Japan
      'IRLTLT01CAM156N', #Long-Term Government Bond Yields: 10-year: Main (Including Benchmark) for Canada
      'JPNCPIALLMINMEI', #Consumer Price Index of All Items in Japan
      'GOLDAMGBD228NLBM',#Gold Fixing Price 10:30 A.M. (London time) in London Bullion Market, based in U.S. Dollars
      'PWHEAMTUSDM',     #Global price of Wheat
      'IPUTIL',          #Industrial Production: Utilities: Electric and Gas Utilities (NAICS = 2211,2)
      'DJIA',            #Dow Jones Industrial Average
      'SP500',           #S&P 500
      'NASDAQCOM',       #NASDAQ Composite Index
      'DEXJPUS',         #Japan / U.S. Foreign Exchange Rate
      'DEXCHUS',         #China / U.S. Foreign Exchange Rate
      'DEXUSEU',         #U.S. / Euro Foreign Exchange Rate
      'DEXINUS',         #India / U.S. Foreign Exchange Rate
      'UNRATE',          #Unemployment Rate
      'LRUN64TTJPM156S', #Unemployment Rate: Aged 15-64: All Persons for Japan
    ]
fred = web.DataReader(lst2,'fred',start=st,end=ed)

# StooqとFredから取得したデータを結合する(結合キーはインデックス=Date)
stock = pd.merge(stooq['Close'], fred, how='left', left_index=True, right_index=True)

# 欠損値を直前のデータで保管する
stock = stock.fillna(method='ffill')

# 2016-01-01以降のデータを抽出
stock = stock[st:ed]

# 20営業日先の最大株価
stock['target.future.max'] = stock['6902.JP'].rolling(20).max().shift(-20)
stock['target.ratio.max'] = stock['target.future.max']/stock['6902.JP']
stock['target.binary.max'] = np.where(stock['target.ratio.max'] > 1.05, 1, 0)
# 20営業日先の最小株価
stock['target.future.min'] = stock['6902.JP'].rolling(20).min().shift(-20)
stock['target.ratio.min'] = stock['target.future.min']/stock['6902.JP']
stock['target.binary.min'] = np.where(stock['target.ratio.min'] < 0.95, 0, 1)
# 20営業日先の最小株価
stock['target.future.mean'] = stock['6902.JP'].rolling(20).mean().shift(-20)
stock['target.ratio.mean'] = stock['target.future.mean']/stock['6902.JP']
stock['target.binary.mean'] = np.where(stock['target.ratio.mean'] > 1.00, 1, 0)
# 不要カラムの削除
stock = stock.drop(columns=['target.future.max','target.future.min','target.future.mean'])
stock = stock.drop(columns=['target.ratio.max','target.ratio.min','target.ratio.mean'])

# 各指数の移動平均との乖離率を算出する
for stock_name in lst1:
    for i in [5,10,15,20,25,30,35,40,45,50,55,60]:
        exec('stock["{}_{}days_diverge"] = \
             (stock["{}"].rolling({}).mean() - stock["{}"])/stock["{}"].rolling({}).mean()' \
             .format(stock_name,i,stock_name,i,stock_name,stock_name,i)
            )
for stock_name in lst2:
    for i in [5,10,15,20,25,30,35,40,45,50,55,60]:
        exec('stock["{}_{}days_diverge"] = \
             (stock["{}"].rolling({}).mean() - stock["{}"])/stock["{}"].rolling({}).mean()' \
             .format(stock_name,i,stock_name,i,stock_name,stock_name,i)
            )
        
# 各指数の列を削除する
stock = stock.drop(columns=lst1)
stock = stock.drop(columns=lst2)

# TrainデータとTestデータを分割
df_train = stock[st_train : ed_train]
df_test  = stock[st_test : ed_test]

df_train = df_train.reset_index(drop=True)
df_test = df_test.reset_index(drop=True)

print('学習用', df_train.shape)
print('試験用', df_test.shape)

# numpyに変換する関数を定義
def toNumpy(x_df, t_max_df, t_min_df, t_mean_df):
    X = x_df.values
    
    y_max = t_max_df.values
    y_max = y_max.reshape(-1)
    
    y_min = t_min_df.values
    y_min = y_min.reshape(-1)
    
    y_mean = t_mean_df.values
    y_mean = y_mean.reshape(-1)
    
    return X, y_max, y_min, y_mean

# 目的変数と説明変数に分ける関数を定義
def split(inputData):
    ## 学習データの分割
    x_df = inputData.drop(columns=['target.binary.min','target.binary.max','target.binary.mean'])
    t_max_df  = inputData['target.binary.max']
    t_min_df  = inputData['target.binary.min']
    t_mean_df = inputData['target.binary.mean']
    ## Numpyに変換
    X, y_max, y_min, y_mean = toNumpy(x_df, t_max_df, t_min_df, t_mean_df)
    return X, y_max, y_min, y_mean

# データ作成
## 学習用データ
X_train, y_trainmax, y_trainmin, y_trainmean = split(df_train)

## 試験用データ
X_test, y_testmax, y_testmin, y_testmean = split(df_test)

# LightGBM訓練用関数を定義
def trainGbdt(X_train, y_train):
    bests = []
    kf = KFold(n_splits=5, shuffle=False)
    for learn_index, valid_index in kf.split(X_train, y_train):
        X_learn, X_valid = X_train[learn_index], X_train[valid_index]
        y_learn, y_valid = y_train[learn_index], y_train[valid_index]
        
        #LGB用のデータに変形
        lgb_learn = lgb.Dataset(X_learn, y_learn)
        lgb_valid = lgb.Dataset(X_valid, y_valid)
        
        param = {
            'objective': 'binary',
            'metric': 'auc',
            'boosting_type': 'gbdt',
            'random_seed':SEED,
        }
        
        # 訓練実施
        best = lgb.train(param, 
                         lgb_learn,
                         valid_sets=[lgb_learn,lgb_valid],
                        )
        bests.append(best)
        
    return bests

# 勾配ブースティングによる学習実施(最大株価予測)
bests_max = trainGbdt(X_train, y_trainmax)
# 勾配ブースティングによる学習実施(最小株価予測)
bests_min = trainGbdt(X_train, y_trainmin)
# 勾配ブースティングによる学習実施(平均株価予測)
bests_mean = trainGbdt(X_train, y_trainmean)

# テストデータを予測する(最大株価予測)
y_preds = []
for i in range(5):
    y_pred = bests_max[i].predict(X_test, num_iteration=bests_max[i].best_iteration)
    y_preds.append(y_pred)
y_predmax = (y_preds[0] + y_preds[1] + y_preds[2] + y_preds[3] + y_preds[4])/5

# テストデータを予測する(最小株価予測)
y_preds = []
for i in range(5):
    y_pred = bests_min[i].predict(X_test, num_iteration=bests_min[i].best_iteration)
    y_preds.append(y_pred)
y_predmin = (y_preds[0] + y_preds[1] + y_preds[2] + y_preds[3] + y_preds[4])/5

# テストデータを予測する(平均株価予測)
y_preds = []
for i in range(5):
    y_pred = bests_mean[i].predict(X_test, num_iteration=bests_mean[i].best_iteration)
    y_preds.append(y_pred)
y_predmean = (y_preds[0] + y_preds[1] + y_preds[2] + y_preds[3] + y_preds[4])/5

# 予測値と実値との比較
result = np.c_[y_predmax,y_testmax,
               y_predmin,y_testmin,
               y_predmean,y_testmean
              ]
df_result = pd.DataFrame(data=result,
                         columns=['pred_max','true_max',
                                  'pred_min','true_min',
                                  'pred_mean','true_mean'
                                 ])
display(df_result)

# 閾値0.5として1,0に分類
df_result['pred_max'] = np.round(df_result['pred_max'])
df_result['pred_min'] = np.round(df_result['pred_min'])
df_result['pred_mean'] = np.round(df_result['pred_mean'])
# 買うか買わないか判断する
df_result['buy_flg'] = df_result['pred_max'] * df_result['pred_min'] * df_result['pred_mean']
df_result['buy_flg_true'] = df_result['true_max'] * df_result['true_min'] * df_result['true_mean']
display(df_result)

# 混合行列(コンフュージョンマトリクス)を表示する関数を定義
def showConfusionMatrix(true,pred,pred_type):
    cm = confusion_matrix(true, pred, labels=[1, 0])
    labels = [1, 0]
    # データフレームに変換
    cm_labeled = pd.DataFrame(cm, columns=labels, index=labels)
    # 結果の表示
    print("◆混合行列(",pred_type,")◆")
    display(cm_labeled)

# 混合行列を表示
showConfusionMatrix(df_result['true_max'], df_result['pred_max'],"最大株価")
showConfusionMatrix(df_result['true_min'], df_result['pred_min'],"最小株価")
showConfusionMatrix(df_result['true_mean'], df_result['pred_mean'],"平均株価")
showConfusionMatrix(df_result['buy_flg_true'], df_result['buy_flg'],"買うか買わないか")

縦持ちデータを横持ちデータに変換する方法(Pandas)

データを縦持ちから横持ちに変換する方法について解説します。 横持ちデータを縦持ちデータに変換する方法についてはこちらでまとめています。

1.そもそも縦持ちと横持ちとは

縦持ちとは縦に長いデータで以下のようなデータのことです。

氏名 科目 点数
A君 数学 100
A君 国語 70
A君 英語 60
A君 ドイツ語 60
B君 数学 60
B君 国語 50
B君 英語 50
B君 アラビア語 60

一方、横持ちとは横に長いデータで以下のようなデータです。

氏名 数学 国語 英語 ドイツ語 アラビア語
A君 100 70 60 60 NaN
B君 60 50 50 NaN 60

上記の例ですと、横持ちの方がすっきりしていて、良いように感じますが、必ずしもそうではありません。どのようなデータの場合、縦持ちの方が良いか後述します。まずは、Pandasで縦持ちデータを横持ちに変換する方法について記載します。

2.縦持ちを横持ちに変換する方法

以下の通りです。まずは縦持ちのサンプルデータを作成します。

df_score_vertical = pd.DataFrame([['A君','数学',100],
                                  ['A君','国語',70],
                                  ['A君','英語',60],
                                  ['A君','ドイツ語',60],
                                  ['B君','数学',60],
                                  ['B君','国語',50],
                                  ['B君','英語',50],
                                  ['B君','アラビア語',60]],
                                 columns=['氏名','科目','点数'])

以下のコードで横持ちに変換できます。

df_score_horizontal = pd.pivot_table(df_score_vertical,
                                     index=['氏名'],
                                     columns=['科目'],
                                     values='点数',
                                     fill_value=0)

上記のコードを実行すると以下のようなDataFrameが作成されます。
f:id:t-yoshi-book:20210609230701p:plain

カラム名が2段になっていますし、「氏名」がIndexになってしまっています。こうならないようにするために以下のようなコードを実行します。多少強引ですが、カラムの貼り直しをしています。

df_score_horizontal = pd.pivot_table(df_score_vertical,
                                     index=['氏名'],
                                     columns=['科目'],
                                     values='点数',
                                     fill_value=0).reset_index()
df_score_horizontal.columns = df_score_horizontal.columns.to_list()

上記を実行することで以下のようなDataFrameができます。たぶん、多くの人が欲しい結果ではないでしょうか。
f:id:t-yoshi-book:20210609231546p:plain

3.縦持ちデータの方が良い例

極端な例ですが、以下のようなデータの場合は横持ちにしてしまうと無駄に表が大きくなってしまいます。

<横持ちの場合>

氏名 フランス語 ドイツ語 アラビア語 ベトナム語
A君 100 NaN NaN NaN
B君 NaN 50 NaN NaN
C君 NaN NaN NaN 80
B君 NaN NaN 50 NaN

欠損値だらけで無駄が多いです。上記のようなデータを疎行列(スパースマトリックス)といいます。以下のように縦持ちにすることですっきりして、データサイズも削減できます。

縦持ちで表現するとスマートです。

氏名 科目 点数
A君 フランス語 100
B君 ドイツ語 50
C君 ベトナム語 80
D君 アラビア語 50
関連記事

SQLで縦持ちデータを横持ちデータに変換する方法についてもまとめたので、良ければ参考にしてください。
book-read-yoshi.hatenablog.com

ViTモデルロード時のエラー(ValueError: Unknown layer: ClassToken)について

画像分類モデルViT(Vision Transformer)で学習したモデルをロードしようとしたところエラーが出ました。対処方法についてまとめます。ちなみにViTについてはこちらでまとめてます。機械学習フレームワークはKerasで構築しています。

1. エラー内容

以下を実行。

# 学習したモデルを以下で保存
model.save("model.h5", include_optimizer=False)

# 保存したモデルを以下で普通にロード
model = tf.keras.models.load_model('model.h5',compile=False)

すると、以下のエラーが出る。

ValueError: Unknown layer: ClassToken


2. 対処方法

学習時に使用したモデルを定義し直して、重みをロードすればOK。

# 学習時に使用したモデルを再定義
vit_model = vit.vit_b16(
    image_size = image_size_vitb16,
    activation = 'sigmoid',
    pretrained = True,
    include_top = False,
    pretrained_top = False)
model = tf.keras.Sequential([
    vit_model,
    tf.keras.layers.Flatten(),
    tf.keras.layers.BatchNormalization(),
    tf.keras.layers.Dense(11, activation = tfa.activations.gelu),
    tf.keras.layers.BatchNormalization(),
    tf.keras.layers.Dense(num_classes, 'softmax')
])
# ロードするモデルを指定。
model .load_weights('model.h5') 


3. 試行錯誤(長いので忙しい人は無視してください)

試行錯誤①:単純にvit-kerasをインポート

エラー文言をそのまま読むと、「Class TokenっていうLayerが定義されていないよ!」って意味になります。たぶん、Class TokenというのはViT独自のレイヤーだと思われます。vit-kerasのソースコードGitHubで確認したところ、Class TokenというLayerが確かに定義されていました。なので、vit-kerasをとりあえずインポートすればClass Tokenも使えるようになるかなと思い、以下のコードを実行しました。

import vit_keras

しかし、解決せず。
ちなみにvit-kerasのソースコードGitHub)については以下を参考にしてください。
vit-keras/layers.py at master · faustomorales/vit-keras · GitHub

試行錯誤②:モデルロード時にcustom_objectとしてClass Tokenを定義

次にロードするときにClass Tokenレイヤーを定義するようにソース修正。

from vit_keras import layers
model = tf.keras.models.load_model('model.h5',custom_objects={'ClassToken': layers.ClassToken},compile=False)

結果は以下の通り。

ValueError: Unknown layer: AddPositionEmbs

エラー内容が変わった!vit-kerasのソースを見直してみるとAddPositionEmbsというLayerも発見。
layers.pyに定義されているレイヤーを一通り定義するように修正。

from vit_keras import layers
model = tf.keras.models.load_model('model.h5',
                                   custom_objects={'ClassToken': layers.ClassToken,
                                                   'AddPositionEmbs': layers.AddPositionEmbs,
                                                   'MultiHeadSelfAttention': layers.MultiHeadSelfAttention,
                                                   'TransformerBlock': layers.TransformerBlock},
                                   compile=False)

結果は以下の通り。

TypeError: __init__() missing 3 required keyword-only arguments: 'num_heads', 'mlp_dim', and 'dropout'

TransformerBlockレイヤーを確認したところコンストラクタに、'num_heads', 'mlp_dim', 'dropout'が引数として定義されている。モデルロード時にこれらの引数が渡せていない模様。色々調べたのですが、引数の渡し方が分からず、custom_obejectを使用した方法は断念。

試行錯誤③:学習時に定義したモデルを再定義して重みをロード

以下GitHubのIssuesに記載されていたワークアラウンドを参考にしました。実施内容は2.の通りです。結果、無事ロードに成功しました。
[TF2.0] KerasLayer cannot be loaded from .h5 · Issue #26835 · tensorflow/tensorflow · GitHub

ViTとEfficientnetをCIFAR-10で試してみた

画像分類モデルには色々なものがありますが、個人的にはViT(Vision Transformer)とEfficientnetが気になってます。これらのモデルを実際に動かしてみて、速度や精度等を比較してみたいと思います。ViTとEfficientnetについては以下でまとめましたので、良ければ参考にしてください。
TensorflowによるEfficientNetの実装 - よっしーの私的空間
TensorflowによるViT(Vision Transformer)の実装 - よっしーの私的空間

1. 使用データ

CIFAR-10というデータを使用しました。10種類のカラー画像データで、大きさは32×32ピクセルです。訓練データが50,000データ、テストデータが10,000データ用意されています。
f:id:t-yoshi-book:20210327165147p:plain
(引用:CIFAR-10 and CIFAR-100 datasets

以下のコードで画像データをインポートします。

# CIFAR-10のインポート
(x_train, y_train), (x_test, y_test) = cifar10.load_data()

2. 比較結果

ViTとEfficientnetをfinetuneして速度や精度等を測りました。結果は以下の通りです。
f:id:t-yoshi-book:20210327174054p:plain

全体的にEfficientnetの方がViTよりも良さそうです。おそらくですが、画像サイズが小さすぎるせいなのではないかと考えます。ViTでは画像をパッチに分割して分析しますが、画像サイズ≒パッチサイズになってしまっているので、画像が分割されずViTの効果が十分に発揮されなかったのではないかと推測します。今度は大きめの画像でも試してみようと思います。

3. 実行コード

# 再現性確保
import os
os.environ['PYTHONHASHSEED'] = '0'
import tensorflow as tf

import numpy as np
import random as rn

SEED = 123
def reset_random_seeds():
    tf.random.set_seed(SEED)
    np.random.seed(SEED)
    rn.seed(SEED)

reset_random_seeds()

# ライブラリインポート
import pandas as pd

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D,BatchNormalization,Flatten
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.preprocessing.image import ImageDataGenerator

from efficientnet.tfkeras import EfficientNetB0 #使用するモデルにあわせて変更する(B0~B7) 
from tensorflow.keras import optimizers
from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping
import tensorflow_addons as tfa

from vit_keras import vit, utils

from sklearn.model_selection import train_test_split
from tensorflow.keras.datasets import cifar10
import datetime

# CIFAR-10のインポート
(x_train, y_train), (x_test, y_test) = cifar10.load_data()

# 変数定義
image_size = 32   #画像のサイズ(CIFAR-10は32×32)
input_shape=(image_size,image_size,3)
num_classes = 10 #画像の種類の数(CIFAR-10は10種類)

# モデル定義
## Efficientnetモデルの定義(ファインチューニング)
def buildModel():
    model = Sequential()
    model.add(EfficientNetB0(    #使用するモデルにあわせて変更する(B0~B7) 
        include_top=False,
        weights='imagenet',
        input_shape=input_shape))
    model.add(GlobalAveragePooling2D())
    model.add(Dense(num_classes, activation="softmax"))
    model.compile(optimizer=optimizers.Adam(learning_rate=1e-4), loss="categorical_crossentropy", metrics=["accuracy"])
    
    return model

## ViTモデルの定義(ファインチューニング)
def buildModel_ViT():
    vit_model = vit.vit_b16(    #使用するモデルにあわせて変更する(b16~l32) 
        image_size = image_size,
        activation = 'sigmoid',
        pretrained = True,
        include_top = False,
        pretrained_top = False)
    model = tf.keras.Sequential([
        vit_model,
        tf.keras.layers.Flatten(),
        tf.keras.layers.BatchNormalization(),
        tf.keras.layers.Dense(21, activation = tfa.activations.gelu),
        tf.keras.layers.BatchNormalization(),
        tf.keras.layers.Dense(num_classes, 'softmax')
    ],
    name = 'vision_transformer')
    model.compile(optimizer=optimizers.Adam(learning_rate=1e-4), loss="categorical_crossentropy", metrics=["accuracy"])
    
    return model

# 訓練用関数の定義
## efficientnet用訓練関数
def train_efficientnet(X, y, steps_per_epoch, epochs, batch_size, callbacks):
    # 訓練データと評価データの分割
    X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2, stratify=y, shuffle=True)
    y_train = to_categorical(y_train)
    y_valid = to_categorical(y_valid)

    # Data Augmentation
    datagen = ImageDataGenerator(rotation_range=20, horizontal_flip=True, width_shift_range=0.2, zoom_range=0.2)
    train_generator = datagen.flow(X_train, y_train,batch_size=batch_size)

    # モデル構築
    model = buildModel()

    # 学習
    history = model.fit_generator(train_generator,
                        steps_per_epoch=steps_per_epoch,
                        epochs=epochs,
                        validation_data=(X_valid, y_valid),
                        callbacks=callbacks,
                        shuffle=True
                       )
    
    return model, history

## ViT用訓練関数
def train_vit(X, y, steps_per_epoch, epochs, batch_size, callbacks):
    # 訓練データと評価データの分割
    X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2, stratify=y, shuffle=True)
    y_train = to_categorical(y_train)
    y_valid = to_categorical(y_valid)
    
    # Data Augmentation
    datagen = ImageDataGenerator(rotation_range=20, horizontal_flip=True, width_shift_range=0.2, zoom_range=0.2)
    train_generator = datagen.flow(X_train, y_train,batch_size=batch_size)
        
    # モデル構築
    model = buildModel_ViT()

    # 学習
    history = model.fit_generator(train_generator,
                        steps_per_epoch=steps_per_epoch,
                        epochs=epochs,
                        validation_data=(X_valid, y_valid),
                        callbacks=callbacks,
                        shuffle=True
                       )
    
    return model, history

# 訓練開始
steps_per_epoch = 1250
epochs = 1000
batch_size = 32

reduce_lr = ReduceLROnPlateau(monitor='val_accuracy',factor=0.2,patience=2,verbose=1,
                              min_delta=1e-4,min_lr=1e-6,mode='max')
earlystopping = EarlyStopping(monitor='val_accuracy', min_delta=1e-4, patience=5,
                                                 mode='max', verbose=1)
callbacks = [earlystopping, reduce_lr]

print('開始時間:',datetime.datetime.now())
# 以下はvitの場合。Efficientnetの場合はtrain_efficientnet()を呼び出す。
model, history = train_vit(x_train, y_train, steps_per_epoch, epochs, batch_size, callbacks)
print('終了時間:',datetime.datetime.now())

# 予測
preds = []
X = x_test
pred = model.predict(X)

# 予測結果の確認
df_pred = pd.DataFrame(pred)
pred = np.array(df_pred.idxmax(axis=1))
df_pred = pd.DataFrame(pred)
df_y = pd.DataFrame(y_test)
df_result = pd.concat([df_y, df_pred], axis=1, join_axes=[df_y.index])
df_result.columns = ['y','pred']
print(df_result)

# 予測結果の評価(混合行列、Accuracy、Precision、Recall、F_score)
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix
print('Confusion Matrix:')
print(confusion_matrix(df_result['y'],df_result['pred']))
print('Accuracy :{:.4f}'.format(accuracy_score(df_result['y'],df_result['pred'])))
print('Precision:{:.4f}'.format(precision_score(df_result['y'],df_result['pred'],average='macro')))
print('Recall   :{:.4f}'.format(recall_score(df_result['y'],df_result['pred'],average='macro')))
print('F_score  :{:.4f}'.format(f1_score(df_result['y'],df_result['pred'],average='macro')))