#はじめに
この記事では画僧分析の入門としてKaggleの開発環境(Kernel notebook)とKerasを用いたMNISTデータセットの分類を紹介します。
Kaggle上ではKernelを公開しているので、自分でも動かしたい場合などは併せてご覧になってください。
間違い、質問、コメントなどあればぜひお声がけください。
LGTM頂けると励みになります!
#Kaggleとは
Kaggleはオンラインで行う世界最大の分析コンペティションです。
また、分析にすぐに取り組めるKernel notebookというオンライン開発環境が用意されており、データ分析の入門には最適だと思います。
本記事ではKaggleに既に登録しており、Kernelの使い方がわかる方を対象にしています。
(ご存じない方も参考になる記事はたくさんあるのですぐにキャッチアップできると思います。)
より詳しくは以下を参照ください。
#Kerasとは
KerasはTensorflowを基軸としたディープラーニングライブラリであり、非常に素早くディープラーニングモデルを組むことができることで知られています。
より詳しくは以下を参照ください。
#MNISTとは
非常に有名な画像データセットであり、手書き数字(0~9)と、それが真に表している数字(正解ラベル)からなるデータセットです。
より詳しくは以下を参照ください。
#本題
繰り返しになりますが、本記事はKaggleのKernelにも投稿していますので、forkしたい場合などはそちらも併せてご覧ください。
データのロードと確認
学習データは42000個のデータから成ることがわかります。
また、列は785個ありますが、最初は正解ラベルなので、学習に用いる特徴量は784個から成ることがわかります。
ラベルは整数で、特徴量はほとんどの要素で0だとわかります。
# 学習データのロード
train = pd.read_csv("../input/train.csv")
print(train.shape)
train.head()
テストデータは28000個です。
正解ラベルはないため、784列から成ります。
# テストデータのロード
test= pd.read_csv("../input/test.csv")
print(test.shape)
test.head()
型の変換、扱いやすいnumpyのデータへの変換を行います。
# 学習データのうち、正解ラベルを除いた特徴量部分を切り抜きます。
X_train = train.iloc[:,1:].values.astype('float32')
# 学習データのうち、正解ラベルだけを切り抜きます。
y_train = train.iloc[:,0].values.astype('int32')
# テストデータです。
X_test = test.values.astype('float32')
データ中の0の割合を見てみましょう。
0の割合は8割ほどと、要素の大部分が0であることがわかります。
このデータセットでは、0は空白=文字の書かれていない領域を意味します。
print(f"要素が0でない割合は{round((X_train > 0).sum()/(X_train >= 0 ).sum()*100)}%です")
print(f"要素が0の割合は{round((X_train == 0).sum()/(X_train >= 0 ).sum()*100)}%です")
正解ラベルの分布を見てみると、0~9のどのラベルもおよそ10%程度を構成していることがわかります。
そのため、クラスの不均衡による調整は必要なさそうです。
# 正解ラベルの割合を円グラフで表示
f, ax = plt.subplots(1,figsize=(6,6))
y_count = pd.Series(y_train).value_counts().sort_index()
ax.pie(y_count.values ,
labels=y_count.index,
autopct='%1.1f%%',
counterclock = False,
startangle = 90)
plt.show()
次に、データを画像として扱いやすい形に変形します。
先にみたように、各画像は784要素からなりますが、これは実は縦28要素横28要素からなる正方形のデータを一次元に潰したものでした。
すべての学習データ(42000個=X_train.shape[0])について28x28に変形します。
# データの変形
X_train = X_train.reshape(X_train.shape[0], 28, 28)
変形を行った生のデータを可視化してみます。
ここでは生のデータの意味合いを考察するため、画像系のライブラリは用いずに文字列でデータを表現してみます。
0でない要素が存在する部分を#として表示してみます。
すると、手書き数字っぽい出力が得られます。
元のデータは人間にはよくわかりにくい数字の羅列ですが、実はインクのある意図(0でないかどうか)とその濃さ(数字の値)から成るデータだったのです。
# データを文字列に変換して表示
def visualize_str(d):
d = d.astype("int32").astype("str")
d[d != "0"] = "# "
d[d == "0"] = ". "
d = pd.DataFrame(d)
for i in range(d.shape[0]):
print("".join(d.iloc[i,:]))
print("")
for i in range(1):
visualize_str(X_train[i])
画像で表示してみると、やはり先ほどの文字列の表示と同様の出力が得られます。
# 画像で可視化
f, ax = plt.subplots(1,3)
for i in range(3):
ax[i].imshow(X_train[i], cmap=plt.get_cmap('gray'))
画像の分析では色付きの画像を用いることもあるので、モデルへの入力では色(主に三原)の濃淡を含んだカラーチャネルという次元があることを想定されています。
今回はグレースケールなので新しいデータは何もありませんが、上記の考えに合わせてテンソルの変換をします。
さらに、One hotエンコーディングと乱数の設定も学習前の準備として行います。
#カラーチャネルの追加
X_train = X_train.reshape(X_train.shape[0], 28, 28,1)
X_test = X_test.reshape(X_test.shape[0], 28, 28,1)
# One hot エンコード
from keras.utils.np_utils import to_categorical
y_train= to_categorical(y_train)
# 再現性のために乱数を固定
seed = 0
np.random.seed(seed)
モデル1:線形モデル
いよいよモデリングですが、まずは非常に単純な線形モデルを試してみます。
と、その前に、必要なモジュールのインポートと標準化関数を定義します。
from keras.models import Sequential
from keras.layers.core import Lambda , Dense, Flatten, Dropout
#from keras.callbacks import EarlyStopping
from keras.layers import BatchNormalization, Convolution2D , MaxPooling2D
# 標準化関数の定義
mean_X = X_train.mean().astype(np.float32)
std_X = X_train.std().astype(np.float32)
def standardize(x):
return (x-mean_X)/std_X
それでは、モデルを作成します。
# 線形モデルの定義
model_linear= Sequential()
# 標準化
model_linear.add(Lambda(standardize,input_shape=(28,28,1)))
# 全結合層に入れるため一次元に直す
model_linear.add(Flatten())
# 全結合層
model_linear.add(Dense(10, activation='softmax'))
# モデルの可視化
print("model_linear")
model_linear.summary()
Kerasでは、モデルを定義した後にcompileを行います。
compileでは学習で最適化する指標=損失と、真に最適化したい指標を指定します。
# モデルのコンパイル
# 最適化する指標と観察する指標を指定
from keras.optimizers import Adam ,RMSprop
model_linear.compile(optimizer=RMSprop(lr=0.001),
loss='categorical_crossentropy',
metrics=['accuracy'])
これでモデルの準備はできたので、モデルにデータを投入したいのですが、そのためにはデータの準備が必要です。
ジェネレイターを準備し、クロスバリデーション用にデータの分割をします。
(これは本来ホールドアウト法というべきですが、Kaggleでは慣習的にクロスバリデーションとひとくくりに表現されるようなので、誤解を承知でそのように表現しています。)
# 入力するデータの準備
# ジェネレイターの定義
from keras.preprocessing import image
generator = image.ImageDataGenerator()
# 提出時に用いる全学習データ
X = X_train
y = y_train
# クロスバリデーション
from sklearn.model_selection import train_test_split
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.15, random_state=seed)
train_generator = generator.flow(X_train, y_train, batch_size=64)
val_generator = generator.flow(X_val, y_val, batch_size=64)
学習の状況や結果を見やすくするtensorboardも準備しておきましょう。
標準出力でurlが出力されるので、ブラウザでそれを開いてください。
# tensorboard の立ち上げ
import tensorflow as tf
!rm -rf ./logs/
!mkdir ./logs/
!wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
!unzip ngrok-stable-linux-amd64.zip
import os
import multiprocessing
import datetime
pool = multiprocessing.Pool(processes = 10)
results_of_processes = [pool.apply_async(os.system, args=(cmd, ), callback = None )
for cmd in [
f"tensorboard --logdir ./logs/ --host 0.0.0.0 --port 6006 &",
"./ngrok http 6006 &","y"
]]
! curl -s http://localhost:4040/api/tunnels | python3 -c \
"import sys, json; print(json.load(sys.stdin)['tunnels'][0]['public_url'])"
log_dir = "logs/fit/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=0)
- 標準出力の例: https://1aa43df57b90.ngrok.io
準備ができたので、いよいよ学習をしてみます。
学習の進捗をプログレスバーとcompileで指定した指標で示してくれます。
# epoch3でのデータの学習
# ~15分ほどかかると思います
import tensorflow as tf
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
history_linear=model_linear.fit_generator(generator=train_generator,
steps_per_epoch=train_generator.n,
epochs=3,
validation_data=val_generator,
validation_steps=val_generator.n,
callbacks=[tensorboard_callback]
)
上記の結果で学習の様子はわかりますが、より直観的な理解のために結果を可視化します。
損失をみると、バリデーションの結果は学習が進むにつれて上昇しているので、過学習が起こっていることがわかります。
一方でACC(正解率)のバリデーションは単調に減少していないことに注意してください。
ACCのように非連続な指標は扱いが難しいため、代わりに扱いやすい損失関数で最適化を行っているのですね。
# 結果の可視化
# 結果をプロットする関数の定義
def plt_history(history,keys):
history_dict = history.history
n = len(keys)
f, ax = plt.subplots(n,figsize=(8,4*n))
for i in range(n):
train_value = history_dict[keys[i][0]]
val_value = history_dict[keys[i][1]]
epochs = range(1, len(train_value) + 1)
if n==1:
ax.plot(epochs, train_value, 'bo',label = keys[i][0])
ax.plot(epochs, val_value, 'b+',label = keys[i][1])
ax.legend()
ax.set_xlabel('Epochs')
ax.set_ylabel(keys[i][0])
else:
ax[i].plot(epochs, train_value, 'bo',label = keys[i][0])
ax[i].plot(epochs, val_value, 'b+',label = keys[i][1])
ax[i].legend()
ax[i].set_xlabel('Epochs')
ax[i].set_ylabel(keys[i][0])
plt.show()
# 可視化
plt_history(history_linear, [["loss","val_loss"],["acc","val_acc"]])
モデル2:全結合モデル
このモデルでは全結合層をひとつ付け足して層を深くしています。
また、最適化アルゴリズムをAdamに変更しています。
(これは本来実験的に比較して最適なものを決めるべきものです。)
また、さきほどは逐次的にモデルの定義や処理をしてきましたが、一般的にはクラスや関数でモデルの型を定義し、複数の異なるパラメータや学習データで異なる学習モデルを作ります。
そのため、ここからは関数でモデルを定義します。
# モデルの定義
def get_fc_model():
model = Sequential()
model.add(Lambda(standardize,input_shape=(28,28,1)))
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(Dense(10, activation='softmax'))
model.compile(optimizer = Adam(),
loss='categorical_crossentropy',
metrics=['accuracy'])
return model
model_fc = get_fc_model()
model_fc.optimizer.lr=0.01
model_fc.summary()
先ほど同様にモデルを学習します。
複数のモデルを短時間で試したい場合は、エポック数を下げるなどトレードオフが必要です。
結果を見ると、先ほどよりもバリデーションのACCが高いことがわかります。
# モデルの学習
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
history_fc=model_fc.fit_generator(generator=train_generator,
steps_per_epoch=train_generator.n,
epochs=1,
validation_data=val_generator,
validation_steps=val_generator.n,
callbacks=[tensorboard_callback]
)
# 学習結果
history_dict_fc = history_fc.history
history_dict_fc
モデル3:CNNモデル
続いて、畳み込み層、プーリング層を含むCNNモデルを試します。
CNNでは、広域な空間を効率的に学習できるため、画像分析問題に対して高い精度が期待できます。
今回は層の深さを2種類用意してその違いも見てみます。
# モデルの定義
from keras.layers import Convolution2D, MaxPooling2D
# 畳み込みとプーリングを2回ずつ持つモデル
def get_cnn_model1():
model = Sequential([
Lambda(standardize, input_shape=(28,28,1)),
Convolution2D(32,(3,3), activation='relu'),
MaxPooling2D(),
Convolution2D(64,(3,3), activation='relu'),
MaxPooling2D(),
Flatten(),
Dense(512, activation='relu'),
Dense(10, activation='softmax')
])
model.compile(optimizer = Adam(), loss='categorical_crossentropy',
metrics=['accuracy'])
return model
# 畳み込みとプーリングを3回ずつ持つモデル
def get_cnn_model2():
model = Sequential([
Lambda(standardize, input_shape=(28,28,1)),
Convolution2D(32,(3,3), activation='relu'),
MaxPooling2D(),
Convolution2D(64,(3,3), activation='relu'),
MaxPooling2D(),
Convolution2D(128,(3,3), activation='relu'),
MaxPooling2D(),
Flatten(),
Dense(512, activation='relu'),
Dense(10, activation='softmax')
])
model.compile(optimizer = Adam(), loss='categorical_crossentropy',
metrics=['accuracy'])
return model
model_cnn1 = get_cnn_model1()
model_cnn2 = get_cnn_model2()
model_cnn1.optimizer.lr=0.01
model_cnn2.optimizer.lr=0.01
浅めのモデルが学習するパラメータは843,658個です
model_cnn1.summary()
深めのモデルが学習するパラメータは163,850と、model_cnn1よりも少ないです。
これは畳み込み層とプーリング層の周辺効果により、全結合層に投入されるデータが大きく削減されているからです。
実際、flattenの次元をみてみると、cnn1では1600でしたが、cnn2では128しかありません。
画像のサイズが大きい場合はこのようにしてデータの大きさを削減することが望ましいですが、今回のようにコンパクトなデータではどのような結果になるでしょう。
model_cnn2.summary()
それぞれ学習を行います。
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
history_cnn1=model_cnn1.fit_generator(generator=train_generator,
steps_per_epoch=train_generator.n,
epochs=1,
validation_data=val_generator,
validation_steps=val_generator.n,
callbacks=[tensorboard_callback]
)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
history_cnn2=model_cnn2.fit_generator(generator=train_generator,
steps_per_epoch=train_generator.n,
epochs=1,
validation_data=val_generator,
validation_steps=val_generator.n,
#callbacks=[tensorboard_callback]
)
結果を確認してみます。
層の浅いCNNモデルと比較して、層の深いCNNモデルは学習データに対してもバリデーションデータに対しても、損失、正解率ともに芳しくない結果です。
この理由としてはいくつか考えられます。
①モデルが悪い
モデルが悪い原因としては学習パラメータが少なくなってしまったことが考えられます。
調整できるパラメータが多い、ということは、すなわちより変化に富んだ表現ができるということです。
このモデルはパラメータ数が少ないので、十分な表現力がなかったのかもしれません。
②学習データが悪い
学習データの数が十分でないときは過学習が起きやすいです。
今回は学習データの正解率はそこまで悪くありませんが、バリデーションの結果は乖離が大きく、典型的な過学習状態です。
これから試すデータの拡張で改善できるかもしれません。
③学習不足
まだモデルが完全に学習しきれていない可能性もあります。
今回の場合、学習データの損失がcnn1に比べて大きいので、エポックを大きくすれば学習が進む可能性があります。
しかし、バリデーションとの乖離はまた別問題なので、この状態でエポックを増やしても本質的な解決にはならないでしょう。
history_cnn1.history
history_cnn2.history
データ拡張
学習データは多いほど汎化性能が高まる=過学習が起こりにくいと考えられています。
限られたデータ数から、疑似的に学習データを増加する方法として、データ拡張(Data augumentation)があります。
データ拡張は与えられたデータに軽微な変化を加えることでデータを水増しする手法です。
# データの拡張
from keras.preprocessing import image
DA_generator =image.ImageDataGenerator(rotation_range=10,
width_shift_range=0.1,
shear_range=0.1,
height_shift_range=0.1,
zoom_range=0.1)
train_DA_generator = DA_generator.flow(X_train, y_train, batch_size=64)
val_DA_generator = DA_generator.flow(X_val, y_val, batch_size=64)
# 拡張したデータの例
tmp_gen = DA_generator.flow(X_train[0].reshape((1,28,28,1)), batch_size = 1)
for i, tmp in enumerate(tmp_gen):
plt.subplot(330 + (i+1))
plt.imshow(tmp.reshape((28,28)), cmap=plt.get_cmap('gray'))
if i == 8:
break
学習には15分ほどかかると思います。
データ拡張の結果、バリデーション結果が改善されています。
エポック毎に学習データの結果もバリデーションの結果も改善するので、時間に余裕があればエポックを大きくして実験してみてください。
# CNN1 層の浅いCNN
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
model_cnn1.optimizer.lr=0.005
history_cnn1_DA=model_cnn1.fit_generator(generator=train_DA_generator,
steps_per_epoch=train_DA_generator.n,
epochs=1,
validation_data=val_DA_generator,
validation_steps=val_DA_generator.n,
callbacks=[tensorboard_callback]
)
history_cnn1_DA.history
モデル4: バッチノーマリゼーション
これまでのモデルでは入力の標準化を行っていました。
しかし、それでは各層の出力が標準化されていることまでは保障できません。
すると、ある層以降では出力がとても大きくなってしまい、パラメータが非常に小さくなってしまう、ということが起こりえます。
あるいはその逆で、パラメータが非常に大きくなりえます。
そのような場合にはうまく学習が行えないのですが、各層毎に標準化を行うバッチノーマリゼーション層を加えることでこの問題が解決されます。
これにより、汎化性能と学習速度の両方が向上するといわれています。
今回もエポック数を1にしていますが、時間に余裕があればエポック数を増やして過学習の具合を観察してみてください。
from keras.layers.normalization import BatchNormalization
def get_bn_model():
model = Sequential([
Lambda(standardize, input_shape=(28,28,1)),
Convolution2D(32,(3,3), activation='relu'),
BatchNormalization(axis=1),
MaxPooling2D(),
BatchNormalization(axis=1),
Convolution2D(64,(3,3), activation='relu'),
BatchNormalization(axis=1),
MaxPooling2D(),
BatchNormalization(axis=1),
Flatten(),
BatchNormalization(),
Dense(512, activation='relu'),
BatchNormalization(),
Dense(10, activation='softmax')
])
model.compile(optimizer = Adam(), loss='categorical_crossentropy', metrics=['accuracy'])
return model
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
model_bn= get_bn_model()
model_bn.optimizer.lr=0.01
history_bn=model_bn.fit_generator(generator=train_DA_generator,
steps_per_epoch=train_DA_generator.n,
epochs=1,
validation_data=val_DA_generator,
validation_steps=val_DA_generator.n,
callbacks=[tensorboard_callback]
)
history_bn.history
モデル5: 最適モデル
最後に、本記事で紹介するモデルのうち、最もスコアの高いモデルを紹介します。
このモデルを設計するにはこれまで見てきたような層の追加や最適化関数の変更など、試行錯誤が必要です。
この最適モデルは(現状の私の能力と開発環境で)一通りの試行錯誤を経たモデルです。
ドロップアウト層、Heの初期値設定、学習率を段階的に変化させるコールバックを加えています。
結果として、バリエーションで99.4%の正解率をマークしています。
from keras.layers import Dense, Dropout, Flatten, Convolution2D, MaxPooling2D
from keras.layers.normalization import BatchNormalization
from keras.callbacks import ReduceLROnPlateau
def get_opt_model():
model = Sequential([
Lambda(standardize, input_shape=(28,28,1)),
Convolution2D(32,(3,3), activation='relu',kernel_initializer='he_normal'),
Convolution2D(32,(3,3), activation='relu',kernel_initializer='he_normal'),
MaxPooling2D(),
Dropout(0.20),
Convolution2D(32,(3,3), activation='relu',kernel_initializer='he_normal'),
Convolution2D(32,(3,3), activation='relu',kernel_initializer='he_normal'),
MaxPooling2D(),
Dropout(0.25),
Convolution2D(32,(3,3), activation='relu',kernel_initializer='he_normal'),
Dropout(0.25),
Flatten(),
Dense(128, activation='relu'),
BatchNormalization(),
Dropout(0.25),
Dense(10, activation='softmax')
])
model.compile(optimizer=Adam(),
loss='categorical_crossentropy',
metrics=['accuracy'])
return model
learning_rate_reduction = ReduceLROnPlateau(monitor='val_loss',
patience=3,
verbose=1,
factor=0.5,
min_lr=0.0001)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
model_opt = get_opt_model()
history_opt = model_opt.fit_generator(generator=train_DA_generator,
steps_per_epoch=train_DA_generator.n,
epochs=3,
validation_data=val_DA_generator,
validation_steps=val_DA_generator.n,
callbacks=[tensorboard_callback, learning_rate_reduction]
)
Y_pred = model_opt.predict_classes(X_val,verbose = 0)
Y_pred_prob = model_opt.predict(X_val,verbose = 0)
# 結果の可視化
plt_history(history_opt, [["loss","val_loss"],["acc","val_acc"]])
history_opt.history
結果の確認
最適モデルでもいくつかの不正解がありましたが、それはどのようなデータに対する不正解なのでしょうか。
混合行列を確認すると、ほとんど不正解はない様子ですが、1を7、7を2に間違える例が多少ある様子です。
# 混同行列を表示する関数の定義
import itertools
def plt_confusion_mtx(confusion_mtx):
cmap=plt.cm.Reds
title='Confusion matrix'
f, ax = plt.subplots(1,figsize=(6,6))
im = ax.imshow(confusion_mtx, interpolation='nearest', cmap=cmap)
ax.set_title(title)
ax.set_xticks(np.arange(10))
ax.set_yticks(np.arange(10))
ax.set_xlabel('Predicted label')
ax.set_ylabel('True label')
f.colorbar(im)
thresh = confusion_mtx.max() / 2
for i, j in itertools.product(range(confusion_mtx.shape[0]), range(confusion_mtx.shape[1])):
ax.text(j, i, confusion_mtx[i, j],
horizontalalignment="center",
color="white" if confusion_mtx[i, j] > thresh else "black")
# 混同行列の表示
from sklearn.metrics import confusion_matrix
Y_true = np.argmax(y_val,axis=1)
confusion_mtx = confusion_matrix(Y_true, Y_pred)
plt_confusion_mtx(confusion_mtx)
# 正解クラスごとにpresision, recall等の確認
from sklearn.metrics import classification_report
target_names = ["Class {}".format(i) for i in range(10)]
print(classification_report(Y_true, Y_pred, target_names=target_names))
実際に不正解データを見てみると、人間にも判別が困難、というより、ラベル付けを間違えているのではと疑うような例が散見されます。
# 不正解データの確認
errors = (Y_pred - Y_true != 0)
Y_pred_errors = Y_pred[errors]
Y_pred_prob_errors = Y_pred_prob[errors]
Y_true_errors = Y_true[errors]
X_val_errors = X_val[errors]
def display_errors(errors_index,img_errors,pred_errors, obs_errors):
""" This function shows 6 images with their predicted and real labels"""
n = 0
nrows = 2
ncols = 3
fig, ax = plt.subplots(nrows,ncols, figsize = (8,8))
for row in range(nrows):
for col in range(ncols):
error = errors_index[n]
ax[row,col].imshow((img_errors[error]).reshape((28,28)))
ax[row,col].set_title("Predicted label :{}\nTrue label :{}".format(pred_errors[error],obs_errors[error]))
n += 1
errors = (Y_pred - Y_true != 0)
tmp = Y_pred_prob[errors] - to_categorical(Y_pred[errors])
display_index = np.argsort(tmp.max(axis=1))[:6]
display_errors(display_index, X_val_errors, Y_pred_errors, Y_true_errors)
最適モデルは十分に実用に耐えうる性能だと言えるのではないでしょうか。
おわりに
以上でKerasによるDeep Learning画像分析の入門を終わりにします。
ここから先はより複雑なデータセットで学習を試してみたり、学習済みモデルを用いた転移学習に挑戦したり、有名なディープラーニングモデルを試してみたり、とても奥の深い分野がたくさん待っています。
今回の記事がもしどなたかのお役に立てれば、今後もこうしたチュートリアル記事を更新していきたいと思いますので、ぜひLGTMで応援ください。
また、常に求職中なので機会があればお声がけください。