よっしーの私的空間

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

ImageDataGeneratorでデータ拡張してみた

ImageDataGeneratorを使って画像拡張を色々試してみます。使う画像は実家のワンコ(小次郎君)の写真です。
f:id:t-yoshi-book:20210917125733p:plain

1.画像の水増し

① 回転(rotation_range)
datagen = ImageDataGenerator(
    rotation_range=90,
)
g = datagen.flow(x, batch_size=x.shape[0], shuffle=False)

itr=3
generated_imgs = []
for i in range(itr):
    generated_img = g.next()
    generated_imgs.extend(generated_img)
  • 入力画像はx。x.shape=(1, 1478, 1108, 3)=(画像1枚、縦幅1478ピクセル、横幅1108ピクセル、カラー画像)。
  • 出力画像はgenerated_imgs。generated_imgs.shape=(3, 1478, 1108, 3)=(画像3枚、縦幅1478ピクセル、横幅1108ピクセル、カラー画像)。

拡張された画像を確認します。

generated_imgs = np.array(generated_imgs)
fig = plt.figure(figsize=(15,5))
fig.add_subplot(1,itr+1,1)
plt.title("original img")
plt.imshow(x[0])

for i,img in enumerate(generated_imgs):
    fig.add_subplot(1,itr+1,i+2)
    plt.title("aug img {}".format(i))
    plt.imshow(img)

plt.show()

f:id:t-yoshi-book:20210917131054p:plain

② 横にずらす(width_shift_range)
datagen = ImageDataGenerator(
    width_shift_range=0.5,
)

f:id:t-yoshi-book:20210917131531p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

③ 縦にずらす(height_shift_range)
datagen = ImageDataGenerator(
    height_shift_range=0.5,
)

f:id:t-yoshi-book:20210917140903p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

④ 明るさを変える(brightness_range)
datagen = ImageDataGenerator(
    brightness_range=[0.5,1.5],
)

f:id:t-yoshi-book:20210917141134p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

⑤ 斜めにひっぱる(shear_range)
datagen = ImageDataGenerator(
    shear_range=50,
)

f:id:t-yoshi-book:20210917141428p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

⑥ 拡大縮小(zoom_range)
datagen = ImageDataGenerator(
    zoom_range=0.5,
)

f:id:t-yoshi-book:20210917141657p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

⑦ RGB各チャネルに0~指定した値をランダムに加算または減算(channel_shift_range)
datagen = ImageDataGenerator(
    channel_shift_range=100,
)

f:id:t-yoshi-book:20210917142444p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

⑧ 水平方向に反転(horizontal_flip)
datagen = ImageDataGenerator(
    horizontal_flip=True,
)

f:id:t-yoshi-book:20210917143114p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

⑨ 垂直方向に反転(vertical_flip)
datagen = ImageDataGenerator(
    vertical_flip=True,
)

f:id:t-yoshi-book:20210917144750p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

参考:余白の埋め方(fill_mode, cval)

画像のずらしたりするときに、画像に余白ができることがあります。その処理の仕方が複数用意されているので、それぞれ試してみます。cvalの値によって、塗りつぶしの色が変わります。色は白(0)~黒(255)です。

① constant, cval

余白を単色で塗りつぶす。

datagen = ImageDataGenerator(
    width_shift_range=0.5,
    fill_mode='constant', 
    cval=0.0, #ココの値で色を変える
)

f:id:t-yoshi-book:20210917162446p:plain
f:id:t-yoshi-book:20210917162638p:plain
f:id:t-yoshi-book:20210917162730p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

② nearest

隣接しているピクセルの色をコピーして余白を埋めます。ちなみにfill_modeのデフォルトはnearestです。

datagen = ImageDataGenerator(
    width_shift_range=0.5,
    fill_mode='nearest', 
)

f:id:t-yoshi-book:20210917163239p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

③ reflect

画像を反転させて余白を埋めます。

datagen = ImageDataGenerator(
    width_shift_range=0.5,
    fill_mode='reflect', 
)

f:id:t-yoshi-book:20210917163936p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

④ wrap

画像を複製して余白を埋めます。

datagen = ImageDataGenerator(
    width_shift_range=0.5,
    fill_mode='wrap', 
)

f:id:t-yoshi-book:20210917170616p:plain
※コードは 「① 回転」とほぼ同じなので重複分は省略。

2.正規化

2.1.画像単位に正規化

① 平均を0にする(samplewise_center)
datagen = ImageDataGenerator(
    samplewise_center=True, 
)

画像のデータの平均が0になるように正規化します。以下を見てもらえれば分かると思いますが、全データに対して平均値を減算しています。
f:id:t-yoshi-book:20210917174639p:plain

標準偏差で正規化(samplewise_std_normalization)
datagen = ImageDataGenerator(
    samplewise_std_normalization=True, 
)

標準偏差で正規化します。イメージは以下の通りです。
f:id:t-yoshi-book:20210917182254p:plain

2.2.データセット単位に正規化

① 平均を0にする(featurewise_center)

実装する上での注意点ですが、データセット単位に正規化する場合はImageDataGenerator.fit()を呼び出す必要があります。fit()でデータセット全体の統計量を計算しているようです。

datagen = ImageDataGenerator(
    featurewise_center=True, 
)
datagen.fit(x)
g = datagen.flow(x, batch_size=x.shape[0], shuffle=False)

画像のデータをデータセット全体の平均値で正規化します。イメージは以下の通りです。samplewizseのときはチャネル関係なく全データの平均を使って平均していましたが、featurewiseの方はチャネル毎に平均を取って正規化しているようです。何故そのような仕様になっているのかは分からないです。分かる方いらっしゃったらコメントいただけると嬉しいです。
f:id:t-yoshi-book:20210917185006p:plain
※上記の数式はあくまでイメージです。雑な表現ではありますが、ご容赦ください。
※微妙に計算合わないですが、端数処理ですかね…?

標準偏差で正規化(featurewise_std_normalization)
datagen = ImageDataGenerator(
    featurewise_std_normalization=True, 
)
datagen.fit(x)
g = datagen.flow(x, batch_size=x.shape[0], shuffle=False)

こちらもチャネル毎に正規化しているようです。イメージは以下の通り。
f:id:t-yoshi-book:20210917190546p:plain
※こちらも雑な表現ご容赦ください。
※同じく、微妙に計算が合わない。

③ ZCA白色化(zca_whitening)

ZCA白色化は大量にメモリを食うようです。1280×960の画像でZCA白色化を実施しようとしたところ、メモリ49.4TiB必要だって怒られました。しょうがないので、めちゃめちゃ画像を縮小させました。

datagen = ImageDataGenerator(
    zca_whitening=True,
    zca_epsilon=1e-06,
)
datagen.fit(x)
g = datagen.flow(x, batch_size=x.shape[0], shuffle=False)

共分散行列が単位行列になるように変換しているようです。ZCAイプシロンがどう使われていて、どのような影響を及ぼすのかは正直分からないです。ZCAイプシロンをデフォルトの1e-06としたときに以下のように画像データが変換されていました。
f:id:t-yoshi-book:20210917193603p:plain

ImageDataGeneratorにおいてfeaturewise系のオプションがうまく機能しない問題

tensorflow.kerasの画像拡張用モジュールImageDataGeneratorを使ってデータ拡張する際に、featurewise系のオプションの使用方法で躓いていたのですが、解決策が分かったのでまとめます。結論は至極シンプルで、しかも公式ドキュメントにちゃんと書かれているので、この記事を公開するのも恥ずかしいのですが、意外と躓いている人がいるかもしれないので公開します。

躓いた事象

ImageDataGeneratorのfeaturewise系のオプションを指定しても画像データが何も変わらない。featurewise系のオプションとは具体的には以下。

  • featurewise_center:データセット全体で,入力の平均を0にする
  • featurewise_std_normalization:入力をデータセット標準偏差で正規化する
  • zca_whitening:ZCA白色化を適用する

結論

flow()の前に、fit()を使用する。以上。

datagen = ImageDataGenerator(
    featurewise_center=True, 
    featurewise_std_normalization=True,
    zca_whitening=True,
    zca_epsilon=1e-06,
)

datagen.fit(x)

g = datagen.flow(x, batch_size=x.shape[0], shuffle=False)

当たり前なのですが、データ全体の平均を0にしたりするためにはデータセットの統計量を計算する必要があります。fit()でこれらの統計量を計算しているようです。

機械学習における不均衡データへの対処方法(Over Sampling, Under Sampling)

機械学習における不均衡データへの対処方法としてアンダーサンプリングやオーバーサンプリングについてまとめます。不均衡データとは目的変数のクラスの度数が極端に偏っているデータのことです。今回はKaggleで公開されている「Credit Card Fraud Detection」のデータセットを使用して以下の手法を試してみます。

  • Random Under Sampling
  • Random Over Sampling
  • SMOTE

「Credit Card Fraud Detection」の目的変数の各クラスの度数は以下の通り。偏りが発生していることが分かります。
f:id:t-yoshi-book:20210731100835p:plain

1.何も対策せずに機械学習させるとどうなるか

試しに適当なニューラルネットワークを構築して試してみました。構築したニューラルネットワークは以下の通り。コード全文は本稿末尾にまとめました。

from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import InputLayer, Dense, Activation, Dropout
from tensorflow.keras.optimizers import Adam

def train_dnn(new_nx_train,new_t_train):
    model = Sequential()
    
    model.add(InputLayer(input_shape=new_nx_train[1].shape))
    model.add(Dense(8))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))
    model.add(Dense(8))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    
    callbacks = EarlyStopping(monitor='val_loss', patience=3, mode='auto')
    
    # モデルのコンパイル
    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
    # 学習
    ep=100
    model.fit(new_nx_train, new_t_train, epochs=ep, batch_size=126, validation_split=0.3, verbose=2, callbacks=callbacks)
    
    return model

上記で学習させた結果は以下の通り。ちなみに学習データ70%、テストデータ30%に分割して、テストデータに対して予測した結果です。
  f:id:t-yoshi-book:20210731124508p:plain

全部0と予測してしまっています。

2.不均衡データの均衡化

2.1.Random Under Sampling

少数派のクラスに合わせて、多数派のクラスのデータをランダムに削除する手法です。imblearn.under_sampling.RandomUnderSamplerを使用することで、簡単に実装できます。

from imblearn.under_sampling import RandomUnderSampler
# 少数派クラスのデータ数をカウント
positive_count_train = y_train["Class"].value_counts()[1]
# Random under sampling
rus = RandomUnderSampler(sampling_strategy={0:np.round(positive_count_train * 9).astype(int), 1:positive_count_train}, random_state=0)
X_train_undersampled, y_train_undersampled = rus.fit_resample(X_train, y_train)

上記を実行することで、以下の通り多数派クラスのデータが減ります。
f:id:t-yoshi-book:20210731130231p:plain
上記のデータを使って1.で構築したニューラルネットワークを学習させました。結果は以下の通り。予測精度が向上していることが分かります。
  f:id:t-yoshi-book:20210731131037p:plain

2.2.Random Over Sampling

多数派のクラスに合わせて少数派のクラスのデータをランダムに複製する手法です。imblearn.over_sampling.RandomOverSamplerを使います。

from imblearn.over_sampling import RandomOverSampler
# 多数派クラスのデータ数をカウント
negative_count_train = y_train["Class"].value_counts()[0]
# Random over sampling
ros = RandomOverSampler(sampling_strategy={0: negative_count_train, 1: negative_count_train//9}, random_state=0)
X_train_oversampled, y_train_oversampled = ros.fit_resample(X_train, y_train)

以下の通り少数派クラスのデータが増えます。
f:id:t-yoshi-book:20210731133841p:plain
上記のデータを学習させてみたところ、予測精度は以下の通りとなりました。Random Under Samplingの方が若干精度が良かったですが、何もしないよりはかなり良い結果となりました。
  f:id:t-yoshi-book:20210731133809p:plain

2.3.SMOTE

SMOTEとは元のデータから類似するデータを生成する手法で、以下の手順を繰り返すことでデータを生成します。

  1. 元データからランダムにデータを選択
  2. 選択したデータの近傍に存在するデータからランダムに1つを選択
  3. 選択したデータと近傍データの間に新たなデータを生成

f:id:t-yoshi-book:20210731142720p:plain

imblearn.over_sampling.SMOTEを使って実装できます。

from imblearn.over_sampling import SMOTE
# 多数派クラスのデータ数をカウント
negative_count_train = y_train["Class"].value_counts()[0]
# SMOTE
smote = SMOTE(sampling_strategy={0: negative_count_train, 1: negative_count_train//9}, k_neighbors=5, random_state=0)
X_train_smotesampled, y_train_smotesampled = smote.fit_resample(X_train, y_train)

以下の通り少数派クラスのデータが増えます。
f:id:t-yoshi-book:20210731133841p:plain
上記のデータを学習させてみたところ、予測精度は以下の通りとなりました。こちらもRandom Under Samplingの方が若干精度が良かったですが、何もしないよりはかなり良い結果となりました。
  f:id:t-yoshi-book:20210731135133p:plain

3.その他手法

SMOTEには多数の亜種があるようです。また、Under Samplingの手法も数多く存在します。imblearnには色々な手法が揃っていますので、興味がある人は一度見てみてください。

参考:ソースコード

再現性確保のためのおまじない
# 再現性確保のためのおまじない
import os
os.environ['PYTHONHASHSEED'] = '0'
import tensorflow as tf
os.environ['TF_DETERMINISTIC_OPS'] = 'true'
os.environ['TF_CUDNN_DETERMINISTIC'] = 'true'

import random as rn
import numpy as np

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
df = pd.read_csv("./creditcard.csv")

*** 各種関数の定義
>|pyhton|
# 標準化とNumpy化用の関数
def standard_and_toNumpy(X_train, X_test, y_train):
    # 平均、標準偏差計算
    X_train_mean = X_train.mean()
    X_train_std  = X_train.std()
    # データの標準化
    nx_train_df  = (X_train - X_train_mean)/X_train_std
    nx_test_df  = (X_test - X_train_mean)/X_train_std
    # numpyに変換
    nx_train = nx_train_df.values
    nx_test  = nx_test_df.values
    t_train  = y_train.values
    
    return nx_train, nx_test, t_train

# 学習用データをシャッフルするための関数
## シャッフルしないと、model.fitのvalidation_split時に目的変数に偏りが発生してしまうため
def shuffle_in_unison(a, b):
    assert len(a) == len(b)
    shuffled_a  = np.empty(a.shape, dtype=a.dtype)
    shuffled_b  = np.empty(b.shape, dtype=b.dtype)
    permutation = np.random.permutation(len(a))
    for old_index, new_index in enumerate(permutation):
        shuffled_a[new_index] = a[old_index]
        shuffled_b[new_index] = b[old_index]
    return shuffled_a, shuffled_b

from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import InputLayer, Dense, Activation, Dropout
from tensorflow.keras.optimizers import Adam

# 訓練用関数
def train_dnn(new_nx_train,new_t_train):
    model = Sequential()
    
    model.add(InputLayer(input_shape=new_nx_train[1].shape))
    
    model.add(Dense(8))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))

    model.add(Dense(8))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))
    
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    
    callbacks = EarlyStopping(monitor='val_loss', patience=3, mode='auto')
    
    # モデルのコンパイル
    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
    # 学習
    ep=100
    model.fit(new_nx_train, new_t_train, epochs=ep, batch_size=126, validation_split=0.3, verbose=2, callbacks=callbacks)
    
    return model

# 混同行列を見やすくする関数。以下を参考とした。
# https://qiita.com/makaishi2/items/9fb6bf94daa6208c8ed0
def make_cm(matrix, columns):
    # columns 項目名リスト
    n = len(columns)

    # '正解データ'をn回繰り返すリスト生成
    act = ['正解データ'] * n
    pred = ['予測結果'] * n

    #データフレーム生成
    cm = pd.DataFrame(matrix, columns=[pred, columns], index=[act, columns])
    return cm

from sklearn.metrics import confusion_matrix, precision_score, accuracy_score, recall_score, f1_score
# テストデータの予測精度を表示
def show_evaluation_index(y_test,pred):
    display(make_cm(confusion_matrix(y_test, pred),["0","1"]))
    print("accuracy_score:{:.5f}".format(accuracy_score(y_test, pred)))
    print("precision_score:{:.5f}".format(precision_score(y_test, pred)))
    print("recall_score:{:.5f}".format(recall_score(y_test, pred)))
    print("f1_score:{:.5f}".format(f1_score(y_test, pred)))
まずはシンプルにニューラルネットワークにデータを突っ込む
# 訓練データとテストデータの分割
from sklearn.model_selection import train_test_split
X = df.drop(["Class","Time"], axis=1)
y = df[["Class"]]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=100, stratify=y)

# 標準化とNumpy化
nx_train, nx_test, t_train = standard_and_toNumpy(X_train, X_test, y_train)
# 学習データのシャッフル実施
reset_random_seeds()
new_nx_train, new_t_train = shuffle_in_unison(nx_train, t_train)

# 訓練
reset_random_seeds()
model = train_dnn(new_nx_train,new_t_train)

# 予測
y_pred = model.predict(nx_test)
y_pred = np.round(y_pred)

show_evaluation_index(y_test,y_pred)
Under Sampling
# 少数派クラスのデータ数をカウント
positive_count_train = y_train["Class"].value_counts()[1]

# Random under sampling
rus = RandomUnderSampler(sampling_strategy={0:np.round(positive_count_train * 9).astype(int), 1:positive_count_train}, random_state=0)
X_train_undersampled, y_train_undersampled = rus.fit_resample(X_train, y_train)

# 標準化とNumpy化
nx_train, nx_test, t_train = standard_and_toNumpy(X_train_undersampled, X_test, y_train_undersampled)
# 学習データのシャッフル実施
reset_random_seeds()
new_nx_train, new_t_train = shuffle_in_unison(nx_train, t_train)

# 訓練
reset_random_seeds()
model_undersample = train_dnn(new_nx_train,new_t_train)

# 予測
y_pred_undersample = model_undersample.predict(nx_test)
y_pred_undersample = np.round(y_pred_undersample)

show_evaluation_index(y_test,y_pred_undersample)
Over Sampling
# 多数派クラスのデータ数をカウント
negative_count_train = y_train["Class"].value_counts()[0]

# Random over sampling
ros = RandomOverSampler(sampling_strategy={0: negative_count_train, 1: negative_count_train//9}, random_state=0)
X_train_oversampled, y_train_oversampled = ros.fit_resample(X_train, y_train)

# 標準化とNumpy化
nx_train, nx_test, t_train = standard_and_toNumpy(X_train_oversampled, X_test, y_train_oversampled)
# 学習データのシャッフル実施
reset_random_seeds()
new_nx_train, new_t_train = shuffle_in_unison(nx_train, t_train)

# 訓練
reset_random_seeds()
model_oversample = train_dnn(new_nx_train,new_t_train)

# 予測
y_pred_oversample = model_oversample.predict(nx_test)
y_pred_oversample = np.round(y_pred_oversample)

show_evaluation_index(y_test,y_pred_oversample)
SMOTE
# SMOTE
smote = SMOTE(sampling_strategy={0: negative_count_train, 1: negative_count_train//9}, k_neighbors=5, random_state=0)
X_train_smotesampled, y_train_smotesampled = smote.fit_resample(X_train, y_train)

# 標準化とNumpy化
nx_train, nx_test, t_train = standard_and_toNumpy(X_train_smotesampled, X_test, y_train_smotesampled)
# 学習データのシャッフル実施
reset_random_seeds()
new_nx_train, new_t_train = shuffle_in_unison(nx_train, t_train)

# 訓練
reset_random_seeds()
model_smotesample = train_dnn(new_nx_train,new_t_train)

# 予測
y_pred_smotesample = model_smotesample.predict(nx_test)
y_pred_smotesample = np.round(y_pred_smotesample)

show_evaluation_index(y_test,y_pred_smotesample)

pandas-datareaderで株価データを取得する際に日付を指定できない問題

※2021/11/4現在において「日付を指定できない問題」が解消していることを確認しました。
※同様の事象が再発した場合の参考になるかもしれないので、本記事は残しておきます。

株価データを取得する際に、多くの人がpandas-datareaderを使ってStooq(各種経済データを公開しているポーランドのサイト)からデータを取得していると思います。私もよく使うのですが、ひさしぶりに使ったところ、日付指定ができなくなっていることに気づきました。ちなみにpandas-datareaderのバージョンは0.10.0です。
事象の説明と解決策についてまとめます。

1.事象:日付が指定できない問題

以下のようなコードを実行したところ、問題が発生しました。

import pandas_datareader.data as web
from datetime import datetime

st = datetime(2015, 1, 1)
ed = datetime(2020, 1, 1)

stooq = web.DataReader('6902.JP','stooq',start=st,end=ed) #startとendを指定することで取得期間を指定
display(stooq.head(3))
display(stooq.tail(3))

結果は以下の通り。日付指定がうまくできていません。(2015/1/1~2020/1/1のデータを取得しようとしたのに、2010/1/4~2021/7/15となっている)
f:id:t-yoshi-book:20210718131644p:plain

2.解決策

使用する関数をpandas_datareader.stooq.StooqDailyReader.read()に変更したところ、解決できました。実行したコードは以下の通りです。

import pandas_datareader.stooq as web
from datetime import datetime

st = datetime(2015, 1, 1)
ed = datetime(2020, 1, 1)

stooq = web.StooqDailyReader('6902.JP',start=st,end=ed).read()
display(stooq.head(3))
display(stooq.tail(3))

結果は以下の通りです。指定した通りにデータが取得できていることが確認できます。
f:id:t-yoshi-book:20210718133236p:plain

LightGBMでOptunaを使用するときの再現性確保について

Optunaとはハイパーパラメータチューニングを自動で実施してくれる大変便利なフレームワークで、LightGBMを使う人は良く使うんじゃないかなと思います。今回はそんなOptunaを使用するときの再現性の確保方法についてまとめます。私が使用しているパッケージのバージョンは以下の通りです。

  • lightgbm 2.3.1
  • optuna 2.8.0

1.概要

主なポイントは以下です。

  • lightgbm.train()ではなく、lightgbm.LightGBMTuner()を使用して訓練する。
  • 以下のパラメータを使用する。
    • deterministic
    • force_row_wiseまたはforce_col_wise
    • optuna_seed
  • 乱数シードを固定する(numpy.random.seedおよびrandom.seed)。

上記を1つずつ解説します。てっとり早くソース全文を確認したい方は本稿末尾をご参照ください。sklearnのirisデータ(アヤメデータ)を使用したサンプルコードを掲載しています。

2.解説

2.1.訓練用の関数としてLightGBMTunerを使用する

LightGBMを使用するときに、多くの人は以下のような実装をするのではないかな思います。

import optuna.integration.lightgbm as lgb

'''
データの前処理等を実施(省略)
'''
# 訓練実施
lgb.train(params, lgb_train, lgb_eval)

上記のlightgbm.train()をlightgbm.LightGBMTuner()に変更します。そうすることで、後述するパラメータが使えるようになります。
注意点ですが、lightgbm.LightGBMTuner()を使用すると、訓練の実施やモデルの取得等を明示的に定義する必要があります。実装は以下の通りです。

# 訓練方法の定義
booster = lgb.LightGBMTuner(
    params = params, 
    train_set = lgb_train,
    valid_sets=lgb_eval,
    optuna_seed=123,
)

# 訓練の実施
booster.run()

# 訓練で得た最良のパラメータを表示
booster.best_params

# 訓練で得た最良のモデル(Boosterオブジェクト)を取得する
best_booster = booster.get_best_booster()

2.2.再現性確保用のパラメータの使用

以下のようにパラメータを使用します。

params = {
    'objective': 'multiclass ',
    'num_class':3,
    'metric': 'multi_logloss',
    'verbosity': -1,
    'boosting_type': 'gbdt',
    'deterministic':True, #再現性確保用のパラメータ
    'force_row_wise':True  #再現性確保用のパラメータ
    }

booster = lgb.LightGBMTuner(
    params = params, 
    train_set = lgb_train,
    valid_sets=lgb_eval,
    optuna_seed=123, #再現性確保用のパラメータ
)

2.3.乱数シードの固定

numpy等で使用する乱数シードを固定する。実装は以下の通りです。

import numpy as np
import random as rn

np.random.seed(123)
rn.seed(123)

参考:sklearnのirisデータを使用したサンプルコード

# 乱数シードの固定
import numpy as np
import random as rn

np.random.seed(123)
rn.seed(123)

# ライブラリのインポート
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix

import pandas as pd

from optuna.integration import lightgbm as lgb

# iris(アヤメ)データインポート
iris = load_iris()

# 訓練用データとテストデータに分割
x_train, x_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.3,random_state=123)

#LGB用のデータに変形
lgb_train = lgb.Dataset(x_train, y_train)
lgb_eval = lgb.Dataset(x_test, y_test)

# 訓練方法の定義
params = {
    'objective': 'multiclass ',
    'num_class':3,
    'metric': 'multi_logloss',
    'verbosity': -1,
    'boosting_type': 'gbdt',
    'deterministic':True, #再現性確保用のパラメータ
    'force_row_wise':True  #再現性確保用のパラメータ
    }

booster = lgb.LightGBMTuner(
    params = params, 
    train_set = lgb_train,
    valid_sets=lgb_eval,
    optuna_seed=123, #再現性確保用のパラメータ
)

# 訓練の実施
booster.run()

# 訓練で得た最良のパラメータを表示
booster.best_params

# 訓練で得た最良のモデル(Boosterオブジェクト)を取得する
best_booster = booster.get_best_booster()

# テストデータに対して予測を実施
pred = best_booster.predict(x_test)
pred = np.argmax(pred, axis=1)

# 混同行列(Confusion Matrix)の表示
cm = confusion_matrix(y_test, pred)
print(cm)

私の環境では、上記を実行することで、ハイパーパラメータが以下の通り固定されました。もしかしたら、環境によって結果は変わるかもしれませんが、同一環境であれば同一の結果になると思います。

TensorFlowとPytorchにおける画像のデータ構造の違いと変換方法について

本記事では以下についてまとめます。

  1. TensorFlowとPytorchにおける画像のデータ構造の違いについて解説
  2. データ構造の相互に変換する方法について解説

1.TensorFlowとPytorchの画像のデータ構造の違い

1.1.概要

画像データは4次元配列で表現され、各次元の要素で以下を表現します。

  • バッチ数(画像の枚数)
  • 高さ
  • チャネル(色の成分の数(例:カラー画像はRGBでチャネル数3))

TensorFlowとPytorchで上記の要素の順番が異なります。

  • TensorFlowの場合
     [バッチ数, 高さ, 幅 , チャネル]
  • Pytorchの場合
     [バッチ数, チャネル, 高さ, 幅]

TensorFlow用の画像のデータ構造は「channel last」と呼び、Pytorch用は「channel first」と呼びます。

1.2.具体例を使ってデータ構造の違いを確認

TensorFlow用の画像

配列の中が見えやすいように、numpyを使って、Tensorflow用の画像を作成してみます。

# 高さ2×幅6のカラー画像(チャネル数3)の4枚を作成
image_channel_last = np.array([
    # 1個目の画像
    [[[0,255,0],[0,0,255],[255,0,0],[0,255,0],[0,0,255],[255,0,0]],
     [[255,0,0],[0,255,0],[0,0,255],[255,0,0],[0,255,0],[0,0,255]]], 
    # 2個目の画像
    [[[0,255,255],[255,0,255],[255,255,0],[0,255,255],[255,0,255],[255,255,0]],
     [[255,0,255],[255,255,0],[0,255,255],[255,0,255],[255,255,0],[0,255,255]]],
    # 3個目の画像
    [[[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],
     [[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255]]],
    # 4個目の画像
    [[[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255]],
     [[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]]],
])

上記をnp.shape関数で各次元の要素数を確認してみると以下の通りとなり、channel lastになっていることが分かります。
f:id:t-yoshi-book:20210607174400p:plain

Pytorch用の画像

続いてPytorch用の画像の配列を作成してみます。

# 高さ2×幅6のカラー画像(チャネル数3)の4枚を作成
image_channel_first = np.array([
    # 1個目の画像
    [[[  0,   0, 255,   0,   0, 255],[255,   0,   0, 255,   0,   0]], #赤成分
     [[255,   0,   0, 255,   0,   0],[  0, 255,   0,   0, 255,   0]], #緑成分
     [[  0, 255,   0,   0, 255,   0],[  0,   0, 255,   0,   0, 255]]],#青成分
    # 2個目の画像
    [[[  0, 255, 255,   0, 255, 255],[255, 255,   0, 255, 255,   0]],
     [[255,   0, 255, 255,   0, 255],[  0, 255, 255,   0, 255, 255]],
     [[255, 255,   0, 255, 255,   0],[255,   0, 255, 255,   0, 255]]],
    # 3個目の画像
    [[[  0,   0,   0,   0,   0,   0],[255, 255, 255, 255, 255, 255]],
     [[  0,   0,   0,   0,   0,   0],[255, 255, 255, 255, 255, 255]],
     [[  0,   0,   0,   0,   0,   0],[255, 255, 255, 255, 255, 255]]],
    # 4個目の画像
    [[[255, 255, 255, 255, 255, 255],[  0,   0,   0,   0,   0,   0]],
     [[255, 255, 255, 255, 255, 255],[  0,   0,   0,   0,   0,   0]],
     [[255, 255, 255, 255, 255, 255],[  0,   0,   0,   0,   0,   0]]]
])

こちらもnp.shapeで各次元の要素数を確認してみます。channel firstになっていることが分かります。
f:id:t-yoshi-book:20210607174700p:plain

2.データ構造の変換方法

np.transpose関数を使用することで非常に簡単に変換が可能です。

2.1.Channel last ⇒ Channel first

image_channel_first = np.transpose(image_channel_last , [0,3,1,2])

2.2.Channel first ⇒ Channel last

image_channel_last = np.transpose(image_channel_first , [0,2,3,1])

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業種区分","規模コード","規模区分"]
             ]