評価値の改善が止まった時に学習率を減らす

今回もkerasの学習率改善のコールバックの話です。
LearningRateScheduler を使って、エポックごとの学習率を変えられることを紹介しましたが、
実際、学習をやってみる前に最適な学習率の変化の計画を立てておくことは非常に困難です。
最初は大きめの値でどんどん学習して、それではうまくいかなくなった段階で徐々に下げるということをやりたくなります。

そして、 kerasにはそのためのコールバックの、ReduceLROnPlateau というのが用意されています。
監視する評価値、何エポック改善しなかったら学習率を落とすか、その変化の割合、最小値などを指定すると、
学習の進みに応じて調整してくれます。

さっそく適当なモデルで試してみましょう。
(今回は着目するのが学習率の変化なので、下のコードのモデルは対して良いものでもないことをご了承ください。)


from tensorflow.keras.datasets import fashion_mnist
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import MaxPooling2D
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.callbacks import ReduceLROnPlateau
import matplotlib.pyplot as plt
from sklearn.metrics import classification_report

# データの読み込み
(data_train, target_train), (data_test, target_test) = fashion_mnist.load_data()

# Conv2D の inputに合わせて変形
X_train = data_train.reshape(-1, 28, 28, 1)
X_test = data_test.reshape(-1, 28, 28, 1)

# 特徴量を0~1に正規化する
X_train = X_train / 255
X_test = X_test / 255

# ラベルを1 hot 表現に変換
y_train = to_categorical(target_train, 10)
y_test = to_categorical(target_test, 10)

# lr に少し大きめの値を設定しておく (デフォルトは lr =0.001)
adam = Adam(lr=0.01)

# モデルの構築
model = Sequential()
model.add(Conv2D(16, kernel_size=(3, 3),
                 activation='relu',
                 input_shape=(28, 28, 1)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(32, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(10, activation='softmax'))
model.compile(
    loss="categorical_crossentropy",
    optimizer=adam,
    metrics=['acc']
)
print(model.summary())
"""
Model: "sequential_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_6 (Conv2D)            (None, 26, 26, 16)        160       
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 13, 13, 16)        0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 11, 11, 32)        4640      
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 5, 5, 32)          0         
_________________________________________________________________
flatten_3 (Flatten)          (None, 800)               0         
_________________________________________________________________
dense_6 (Dense)              (None, 64)                51264     
_________________________________________________________________
dropout_7 (Dropout)          (None, 64)                0         
_________________________________________________________________
dense_7 (Dense)              (None, 10)                650       
=================================================================
Total params: 56,714
Trainable params: 56,714
Non-trainable params: 0
_________________________________________________________________
"""
early_stopping = EarlyStopping(
                        monitor='val_loss',
                        min_delta=0.0,
                        patience=10,
                )

# val_lossの改善が2エポック見られなかったら、学習率を0.5倍する。
reduce_lr = ReduceLROnPlateau(
                        monitor='val_loss',
                        factor=0.5,
                        patience=2,
                        min_lr=0.0001
                )

history = model.fit(X_train, y_train,
                    batch_size=128,
                    epochs=50,
                    verbose=2,
                    validation_data=(X_test, y_test),
                    callbacks=[early_stopping, reduce_lr],
                    )
"""
   (途中は省略。以下は最終的な結果)
Epoch 26/50
60000/60000 - 12s - loss: 0.2263 - acc: 0.9138 - val_loss: 0.3177 - val_acc: 0.8998
"""

さて、学習が完了したことで、history に結果が入りましたので、 監視していた val_loss と学習率 lrをみてみましょう。


# val_loss と lr を可視化
fig = plt.figure(figsize=(10, 10), facecolor="w")
ax = fig.add_subplot(2, 1, 1)
ax.set_title("val_loss")
ax.plot(range(len(history.history["val_loss"])), history.history["val_loss"])
ax = fig.add_subplot(2, 1, 2)
ax.set_title("lr")
ax.plot(range(len(history.history["lr"])), history.history["lr"])
plt.show()

学習率が段階的に半減していっているのが確認できますね・

kerasの学習率調整の結果をhistoryオブジェクトから確認する

昨日の記事に続いて、学習率調整の話です。
参考: kerasのLearningRateSchedulerで学習途中に学習率を調整する

昨日の記事では、動作確認のため、LearningRateSchedulerの引数、verboseに1を設定して、
エポックごとの学習率を出力して動きを見ていました。
しかし、見ての通り、出力がかなり煩雑になって、なかなか煩わしいです。
なので、普段は verbose=0 で使うのですが、そうすると、思うように学習が進まなかった時に、
調査の一環で学習率の変化の具合を見ようと思うと困っていました。
(verboseを1になおして再実行すると時間がかかるし、結果も変わるので。)

その時、何気なくhistoryオブジェクト(fit関数の戻り値)を可視化してみると、その中に lr も含まれているのを見つけました。
昨日の記事のコードの続きで実行してみるとこんな感じで。


print(history.history["lr"])
# [0.002, 0.002, 0.002, 0.001, 0.001, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005]

history.history には, accもlossも入っているので、学習率の変化やその影響の確認は,
これをみるのが一番良さそうです。

kerasのLearningRateSchedulerで学習途中に学習率を調整する

機械学習や深層学習において、より効率的にモデルを学習させるテクニックとして、
学習の進捗に応じて、学習率を変更するというものがあります。

kerasでは、これを手軽に実行するために LearningRateScheduler というコールバックが用意されています。
これに、 エポックのインデックスを受け取って学習率を返す関数を渡して、
それをcallbackに設定しておくと実現できます。

だいたい次のイメージで使えます。
例によって、モデルはすでに構築されているものとします。
(下のコードで動かしてるモデルはこのブログの CNNで手書き数字文字の分類 の記事からコピーして持ってきました。)


# 学習率を返す関数を用意する
def lr_schedul(epoch):
    x = 0.002
    if epoch >= 3:
        x = 0.001
    if epoch >= 5:
        x = 0.0005
    return x


lr_decay = LearningRateScheduler(
    lr_schedul,
    # verbose=1で、更新メッセージ表示。0の場合は表示しない
    verbose=1,
)

history = model.fit(X_train, y_train,
                    batch_size=128,
                    epochs=10,
                    verbose=2,
                    validation_data=(X_test, y_test),
                    callbacks=[lr_decay],
                    )

# 以下出力
"""
Train on 60000 samples, validate on 10000 samples
Epoch 1/10

Epoch 00001: LearningRateScheduler setting learning rate to 0.002.
 - 13s - loss: 0.4354 - acc: 0.8625 - val_loss: 0.0707 - val_acc: 0.9762
Epoch 2/10

Epoch 00002: LearningRateScheduler setting learning rate to 0.002.
 - 12s - loss: 0.1748 - acc: 0.9476 - val_loss: 0.0534 - val_acc: 0.9823
Epoch 3/10

Epoch 00003: LearningRateScheduler setting learning rate to 0.002.
 - 12s - loss: 0.1376 - acc: 0.9590 - val_loss: 0.0387 - val_acc: 0.9872
Epoch 4/10

Epoch 00004: LearningRateScheduler setting learning rate to 0.001.
 - 14s - loss: 0.1105 - acc: 0.9675 - val_loss: 0.0332 - val_acc: 0.9882
Epoch 5/10

Epoch 00005: LearningRateScheduler setting learning rate to 0.001.
 - 15s - loss: 0.1041 - acc: 0.9694 - val_loss: 0.0311 - val_acc: 0.9902
Epoch 6/10

Epoch 00006: LearningRateScheduler setting learning rate to 0.0005.
 - 15s - loss: 0.0960 - acc: 0.9725 - val_loss: 0.0293 - val_acc: 0.9899
Epoch 7/10

Epoch 00007: LearningRateScheduler setting learning rate to 0.0005.
 - 14s - loss: 0.0889 - acc: 0.9735 - val_loss: 0.0275 - val_acc: 0.9899
Epoch 8/10

Epoch 00008: LearningRateScheduler setting learning rate to 0.0005.
 - 18s - loss: 0.0880 - acc: 0.9747 - val_loss: 0.0273 - val_acc: 0.9899
Epoch 9/10

Epoch 00009: LearningRateScheduler setting learning rate to 0.0005.
 - 14s - loss: 0.0856 - acc: 0.9746 - val_loss: 0.0274 - val_acc: 0.9905
Epoch 10/10

Epoch 00010: LearningRateScheduler setting learning rate to 0.0005.
 - 13s - loss: 0.0811 - acc: 0.9764 - val_loss: 0.0264 - val_acc: 0.9906
"""

今回はお試しで学習率が変わっていることを見たかったので、 verbose=1 を指定して、LearningRateSchedulerにも
ログを出力させました。
最初の 0.002 から、 0.0005へと、学習率が変わっていっていることがわかります。

ただ、少し煩わしいので、普段の利用では verbose=0 (デフォルトなので未指定でも可)がおすすめです。

ImageDataGeneratorで拡張したデータでモデルを学習する

ImageDataGenerator で画像データを拡張(水増し)したら、それを使って機械学習のモデルを学習したいと思います。

一番単純なのは、欲しいデータ量を決めて、flowメソッドで必要なだけデータを生成し、
それを配列に格納しておいてがくしゅうする方法だと思います。

ただ、これを実行すると、生成したデータ量にもよりますがすぐメモリの消費量が増えてメモリエラー等が発生します。
このような時、拡張された具体的なデータではなく、ジェネレーターをそのまま渡して学習するメソッドがkerasには用意されています。

それが、モデルクラスの fit_generator です。

それぞれのメソッドの引数をみてみましょう。


fit(
    x=None,
    y=None,
    batch_size=None,
    epochs=1,
    verbose=1,
    callbacks=None,
    validation_split=0.0,
    validation_data=None,
    shuffle=True,
    class_weight=None,
    sample_weight=None,
    initial_epoch=0,
    steps_per_epoch=None,
    validation_steps=None
)

fit_generator(
    generator,
    steps_per_epoch=None,
    epochs=1,
    verbose=1,
    callbacks=None,
    validation_data=None,
    validation_steps=None,
    class_weight=None,
    max_queue_size=10,
    workers=1,
    use_multiprocessing=False,
    shuffle=True,
    initial_epoch=0
)

fitでは、最初のふたつの引数x,yでデータとラベルを渡しますが、fit_generatorではそれがgeneratorになります。
そして、このgeneratorが、バッチサイズ分のデータとラベルを一式生成し続けてそれを使って学習します。

次のようなコードで、利用することができます。
(例ではvalidation_dataは固定にしましたが、ここでもジェネレーターを使えます。)


from tensorflow.keras.preprocessing.image import ImageDataGenerator
datagen = ImageDataGenerator(
    # 設定は適当
    rotation_range=30,
    width_shift_range=0.2,
    height_shift_range=0.2,
    zoom_range = 0.2,
    channel_shift_range = 0.2,
    horizontal_flip=True,
)
datagen.fit(x_tr)

# モデル(model)と、データ(x_train,y_train)は別途準備されているものとします。
history = model.fit_generator(
            datagen.flow(x_train, y_train, batch_size=256),
            epochs=100,
            verbose=2,
            validation_data=(x_val, y_val),
        )

ImageDataGeneratorの引数

前々回の記事: kerasによる画像データ拡張 で紹介した、
ImageDataGenerator を使って、色々画像を変換してみます。
引数の一覧はドキュメントにある通りで、
平行移動や回転、色の変更や拡大など色々行えます。
同時に複数を設定することももちろんできるのですが、今回は個々の動作をみたいので、1個ずつ適用しました。
また、どれもランダムに変換するものなので1枚の画像を5回ずつ変換して出力しています。

次が書いてみたコードです。結果がわかりやすいように変数はどれも大きめの値を渡しました。


from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.datasets import cifar10
import matplotlib.pyplot as plt

# データ取得
(x_train, y_train), (x_test, y_test) = cifar10.load_data()

# 1データだけ抽出。
data = x_train[91: 92] / 255.
label = y_train[91: 92]

parameters = [
    # 整数.画像をランダムに回転する回転範囲
    {"rotation_range": 90},
    # 浮動小数点数(横幅に対する割合).ランダムに水平シフトする範囲
    {"width_shift_range": 0.5},
    # 浮動小数点数(縦幅に対する割合).ランダムに垂直シフトする範囲
    {"height_shift_range": 0.5},
    # 浮動小数点数.シアー強度(反時計回りのシアー角度)
    {"shear_range": 90},
    # 浮動小数点数または[lower,upper].ランダムにズームする範囲.
    # 浮動小数点数が与えられた場合,[lower, upper] = [1-zoom_range, 1+zoom_range]
    {"zoom_range": 0.5},
    # 浮動小数点数.ランダムにチャンネルをシフトする範囲
    {"channel_shift_range": 0.5},
    # 真理値.水平方向に入力をランダムに反転します
    {"horizontal_flip": True},
    # 真理値.垂直方向に入力をランダムに反転します
    {"vertical_flip": True},
]

fig = plt.figure(figsize=(11, 20))
for j, kwargs in enumerate(parameters):
    datagen = ImageDataGenerator(
                            **kwargs
                        )
    # 画像の基本的な統計量を学習する
    datagen.fit(data)

    for i in range(5):
        generated_data, generated_label = datagen.flow(data, label).next()

        ax = fig.add_subplot(len(parameters), 5, i+j*5+1)
        ax.set_xticklabels([])
        ax.set_yticklabels([])
        ax.imshow(generated_data[0])
        if i == 0:
            ax.set_title(kwargs)

plt.show()

結果がこちら。
猫の画像が変換されています。

平行移動や回転した時に、元々のと比べて隙間が発生してしまいますが、
それは、 fill_mode: {“constant”, “nearest”, “reflect”, “wrap”} で指定した方法で保管されます。
(デフォルトは ‘nearest’)

ドキュメントには下記のように記載があります。
“constant”: kkkkkkkk|abcd|kkkkkkkk (cval=k)
“nearest”: aaaaaaaa|abcd|dddddddd
“reflect”: abcddcba|abcd|dcbaabcd
“wrap”: abcdabcd|abcd|abcdabcd

constant は cval で指定した定数。
nearest は境界部分の色を引き延ばした形。
reflect は境界を境に線対称。
wrap は反対側からループするようなイメージです。

scikit-imageで画像データの読み書き

常にデータセットの画像データばかり使うのも飽きてくるので、そのほかの画像ファイルをpythonで読み込む方法を調べました。
いくつか方法があるようですが、anacondaに付いてきた、scikit-imageというライブラリが以前から気になっていたので今回はこれを使います。

ドキュメント: scikit-image

どうやら、 scikit-image.io.img_data で読み込めそうです。

試しに画像を読み込んでみて、numpy配列で読み込めたことを確認しました。
また、ついでにimsaveで別名をつけて保存しています。


from skimage import io
# 画像の読み込み
img_data = io.imread("./sample.jpg")
# 縦*横*チャンネル数のnumpy配列で読み込まれていることを確認。
print(type(img_data))
# 
print(img_data.shape)
# (1066, 1600, 3)

# 別名で保存
io.imsave("./sample_2.png", img_data)

このほか、 as_gray オプションで白黒画像としても読み込めました。
配列の次元数も違えば値の範囲も違うので扱いは要注意です。


# 白黒で読み込み
gray_image_data = io.imread("./sample.jpg", as_gray=True)
# チャンネルがなくなり、2次元のデータになる。
print(gray_image_data.shape)
# (1066, 1600)

# カラーで読み込むと、0〜255の整数値
print(img_data.max())
# 255
# 白黒で読み込むと、0.0〜1.0の浮動小数
print(gray_image_data.max())
# 0.9910556862745099

(最大値が中途半端なのは読み込んだ画像によるものです。)

これで、画像を扱うモデルを作ったら任意の画像で試せそうですね。
(あとはサイズの加工とか必要かな。)

kerasによる画像データ拡張

画像データを対象とする機械学習をするとき、画像に変更を加えて教師データを増やすことはとても有効な手段です。
(それで最近、numpyをつかって行列をシフトさせたり反転させたりしています。)

そして、kerasには画像データの拡張専用のツールが用意されています。
それが、ImageDataGeneratorクラスです。
今回の記事ではこれの基本的な使い方を紹介します。
非常に多くのオプションがあり、どんな変換をするか細かく指定できるのですが、
一旦今回はランダムに、最大90度画像を回転させてみます。
使うメソッドは.flowです。


from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.datasets import cifar10
import matplotlib.pyplot as plt

# データ取得
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
# 例なので、データを10件に絞る
data = x_train[:10]
label = y_train[:10]
# 0~1に正規化
data = data/255.

target_name = {
        0: "airplane",
        1: "automobile",
        2: "bird",
        3: "cat",
        4: "deer",
        5: "dog",
        6: "frog",
        7: "horse",
        8: "ship",
        9: "truck",
    }

# 例として、ランダムに90度以内画像を回転させるジェネレーターを生成する。
datagen = ImageDataGenerator(
                        rotation_range=90
                    )
# 画像の基本的な統計量を学習する
datagen.fit(data)

# 変換した画像を生成する(generatorなので、for文で順次生成する。)
for generated_data, generated_label in datagen.flow(data, label):
    cnt += 1
    fig = plt.figure(figsize=(11, 5))
    # 可視化
    for i in range(10):
        ax = fig.add_subplot(2, 5, i+1)
        ax.set_xticklabels([])
        ax.set_yticklabels([])
        ax.imshow(generated_data[i])
        ax.set_title(target_name[generated_label[i, 0]])
    plt.show()

    # 無限に生成し続けるのでforを抜ける。
    break

こうして生成された画像が次です。

回転する角度はランダムなので、中途半端な傾きになっていることがわかります。
また、結果は実行するたびに変わります。

numpy.flipで配列の反転

以前、numpy配列の要素をシフトさせるroll関数を紹介しましたが、
numpyにはこのほか、配列を反転させる、flipという関数も用意されています。
とはいえ、スライス “::-1″で実現できるので、普段はあまりありがたみもないのですが、
画像データの集合などの4次元の配列など、次元が高くなってくるとスライスで書くのは面倒になるので、便利な場面もありそうです。
(スライスで左右反転を書くと ary[:, :, ::-1, :] のようになり、可読性低いので。)

ドキュメント: numpy.flip
引数のaxisで、反転させる次元を指定するのですが、
axis=0(行列では縦の反転),とaxis=1(行列では横の反転) に対応した、
numpy.flipud と、numpy.fliplrという関数もあります。

ついでに紹介しておくと、90度回転させる numpy.rot90というのもあります。


import numpy as np
# サンプルの配列を作成
ary = np.arange(12).reshape(3, 4)
print(ary)
"""
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
"""

# 縦の反転
print(np.flip(ary, axis=0))
print(np.flipud(ary))
print(ary[::-1, :])
# 3つとも同じ出力
"""
[[ 8  9 10 11]
 [ 4  5  6  7]
 [ 0  1  2  3]]
"""

# 横の反転
print(np.flip(ary, axis=1))
print(np.fliplr(ary))
print(ary[:, ::-1])
# 3つとも同じ出力
"""
[[ 3  2  1  0]
 [ 7  6  5  4]
 [11 10  9  8]]
"""

# 90度回転
print(np.rot90(ary))
"""
[[ 3  7 11]
 [ 2  6 10]
 [ 1  5  9]
 [ 0  4  8]]
"""

CIFAR-10データセットの紹介

先日、Fashion-MNISTを触ってみたばかりなのですが、最近はカラー画像のセットであるCIFAR-10も試しています。
このデータセットには、次の10種類のカラー画像が含まれています。
飛行機/車/鳥/猫/鹿/犬/蛙/馬/船/トラック

大元の配布元はこちらのようです。
The CIFAR-10 dataset

ただ、これも kerasのデータセットに含まれていて、とても手軽に扱うことができます。
ドキュメント: CIFAR10 画像分類
(画像の種類数がさらに多い、CIFAR-100というのもあります)

今回もとりあえず読み込んで表示してみます。


from tensorflow.keras.datasets import cifar10
import matplotlib.pyplot as plt

(x_train, y_train), (x_test, y_test) = cifar10.load_data()

# Fashion-MNIST とは 配列の型が違うので注意
print("x_train.shape: ", x_train.shape)
print("y_train.shape: ", y_train.shape)
print("x_test.shape: ", x_test.shape)
print("y_test.shape: ", y_test.shape)
"""
x_train.shape:  (50000, 32, 32, 3)
y_train.shape:  (50000, 1)
x_test.shape:  (10000, 32, 32, 3)
y_test.shape:  (10000, 1)
"""
# 0〜9が なんの画像かの対応はdatasetに含まれないので別途作る
target_name = {
        0: "airplane",
        1: "automobile",
        2: "bird",
        3: "cat",
        4: "deer",
        5: "dog",
        6: "frog",
        7: "horse",
        8: "ship",
        9: "truck",
    }

fig = plt.figure(figsize=(15, 18), facecolor="w")
for i in range(100):
    ax = fig.add_subplot(10, 10, i+1)
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.imshow(x_train[y_train.ravel() == i // 10][i % 10], cmap="gray_r", vmin=0, vmax=255)
    if i % 10 == 0:
        # アイテムの最初の画像にタイトルつける
        ax.set_title(target_name[i//10])

plt.show()

結果がこちら。

numpy配列の中身をシフトさせる

pandasデータフレームにおけるshift操作のようなものを行いたくて調べたのでそのメモです。
numpyか、aryにshiftメソッドがあると勝手に決めつけていたのですが実際は無く、代わりにnumpy.rollというのを使います。

ドキュメント: numpy.roll

元々行いたかったのは、配列の要素を左右(2多次元配列の場合はそれぞれの軸方向にも)ずらして、欠損値になるところにはNoneか何か入るイメージだったのですが、
numpy.rollを使うと、名前の通り、回転させるような動きをします。
(例を見る方がわかりやすいです)

numpy.rollは次の引数を取ります。
a: 回転させる配列
shift: 回転させる幅 (値かタプル)
axis: 回転させる方向 (値かタプル。デフォルトはNone)

まず、1次元配列でやってみます。 1次元しかないのでaxisは意味がなく、shift幅だけ指定してます。


import numpy as np
# 元のデータ作成
ary1 = np.arange(7)
print(ary1)
# [0 1 2 3 4 5 6]

print(np.roll(ary1, 2))
# [5 6 0 1 2 3 4]

print(np.roll(ary1, -3))
# [3 4 5 6 0 1 2]

左右が繋がって回転してるようにずれていますね。

次に2次元です。
axisが未指定(もしくはNoneを渡す)場合と、それぞれの軸の方向を指定した場合で挙動が違うのでやってみます。


ary2 = np.arange(30).reshape(5, 6)
print(ary2)
"""
[[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]
 [15 16 17 18 19]]
"""

# axis未指定で、行列全体で回転。左上と右下が繋がる。
print(np.roll(ary2, 2))
"""
[[28 29  0  1  2  3]
 [ 4  5  6  7  8  9]
 [10 11 12 13 14 15]
 [16 17 18 19 20 21]
 [22 23 24 25 26 27]]
"""

# axis=1 で行ごとに回転。
print(np.roll(ary2, 2, axis=1))
"""
[[ 4  5  0  1  2  3]
 [10 11  6  7  8  9]
 [16 17 12 13 14 15]
 [22 23 18 19 20 21]
 [28 29 24 25 26 27]]
"""

# axis=0 で列ごとに回転。
print(np.roll(ary2, 2, axis=0))
"""
[[18 19 20 21 22 23]
 [24 25 26 27 28 29]
 [ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]
 [12 13 14 15 16 17]]
"""