ブロードキャストしたNumpy配列に代入するときにハマった話
Numpyのブロードキャストは便利ですが、ブロードキャストした配列に代入するときだけ思わぬ落とし穴があります。「ValueError: assignment destination is read-only」というエラーが出たとき、およびその解決方法を見ていきます。
目次
発生した状況
環境:Numpy 1.18.3
Numpyのブロードキャストは値のコピー感覚で使えて便利です。明示的にブロードキャストするにはnp.broadcast_toという関数で出来ます。
例えば(0, 1, 2)というベクトルを作り、行方向に4回コピーするには、
import numpy as np
def main():
x = np.arange(3).reshape(1, 3)
x = np.broadcast_to(x, (4, 3))
print(x)
# [[0 1 2]
# [0 1 2]
# [0 1 2]
# [0 1 2]]
if __name__ == "__main__":
main()
とします。ただしブロードキャストキャストした配列に値を代入しようとするとハマり、
def main():
x = np.arange(3).reshape(1, 3)
x = np.broadcast_to(x, (4, 3))
x[:, 0] = np.arange(4) # これはエラー
print(x)
x[:, 0] = np.arange(4)
ValueError: assignment destination is read-only
と怒られてしまいます。
イミュータブルな配列に強引に書き込む方法
結論からいうとこれはダメなやり方です。np.broadcast_toの結果はイミュータブル(書き込み禁止)になっているので、書き込み禁止フラグを外してしまえば代入できます。
書き込み禁止フラグを外すには「x.flags.writeable = True」とすればいいです。ただの数値の代入なら一見うまく行っているように見えます。
def main():
x = np.arange(3).reshape(1, 3)
x = np.broadcast_to(x, (4, 3))
x.flags.writeable = True # 書き込み禁止を強引に外す
x[:, 0] = 5
print(x)
#[[5 1 2]
# [5 1 2]
# [5 1 2]
# [5 1 2]]
ただし、ブロードキャストした配列に、ベクトルを代入しようとすると正常な動作になりません。
def main():
x = np.arange(3).reshape(1, 3)
x = np.broadcast_to(x, (4, 3))
x.flags.writeable = True # 書き込み禁止を強引に外す
x[:, 0] = np.arange(4) # ベクトルの代入はおかしくなる
print(x)
#[[3 1 2]
# [3 1 2]
# [3 1 2]
# [3 1 2]]
「np.arange(4)」の最後の値が全ての行にコピーされてしまいました。あくまでブロードキャストはブロードキャスト前の配列の参照をコピーしているのであって、値まではコピーしていないということでしょう。
書き込み禁止を外すのではなく配列をコピーする
ベクトルでも正常に動作するには、ブロードキャストした配列をコピーをします。「.copy()」というメソッドを使えばいいです。
def main():
x = np.arange(3).reshape(1, 3)
x = np.broadcast_to(x, (4, 3)).copy() # コピーする
x[:, 0] = np.arange(4) # ベクトルの代入でも正常
print(x)
#[[0 1 2]
# [1 1 2]
# [2 1 2]
# [3 1 2]]
ブロードキャストした配列を四則演算するときは内部的にコピーされているのでこのような配慮はいりませんが、代入するときだけ注意が必要です。
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー