はじめに
NumPyのデフォルト浮動小数点型は float64(倍精度)ですが、PyTorchのデフォルトは float32(単精度)です。この1ビット違いに見える差が、実際には
- メモリ使用量が2倍(要素あたり8バイト vs 4バイト)
- GPU上での演算速度(多くのGPUはfloat32以下で最適化されており、float64は大幅に遅い)
- 勾配計算・最適化の数値安定性
に直結します。さらに、NumPy配列をPyTorchテンソルに変換する際に使う torch.tensor() と torch.from_numpy() は似ているようで挙動がまったく異なり、整数dtypeやデフォルトdtypeの変更もハマりどころが多い領域です。本稿ではこれらを実際に実行して確認しながら、教科書的に整理します。
1. NumPyとPyTorchのデフォルトdtypeの違い
import numpy as np
import torch
np.random.seed(0)
x = np.random.rand(5)
print(x)
print(x.dtype)
torch_x = torch.tensor(x)
print(torch_x)
[0.5488135 0.71518937 0.60276338 0.54488318 0.4236548 ]
float64
tensor([0.5488, 0.7152, 0.6028, 0.5449, 0.4237], dtype=torch.float64)
np.random.rand() が生成する配列は float64 です。torch.tensor() はNumPy配列のdtypeをそのまま引き継ぐため、変換後のテンソルも float64 になります。PyTorchのニューラルネットワークの重みはデフォルトで float32 なので、このまま演算すると型不一致エラーになったり、意図せず倍精度で計算が走って遅くなったりします。
torch_x32 = torch.tensor(x.astype(np.float32))
print(torch_x32)
print("float64 tensor element size:", torch.tensor(x).element_size(), "bytes")
print("float32 tensor element size:", torch_x32.element_size(), "bytes")
tensor([0.5488, 0.7152, 0.6028, 0.5449, 0.4237])
float64 tensor element size: 8 bytes
float32 tensor element size: 4 bytes
dtype を明示的に統一するには、NumPy側で astype(np.float32) してからテンソル化するのが確実です。要素あたりのバイト数が実際に半分になっていることも element_size() で確認できます。
なぜ精度差が問題になるか
IEEE 754の浮動小数点は符号・指数部・仮数部(mantissa)で構成され、仮数部のビット数 \(p\)
が相対誤差(machine epsilon)\(\epsilon \approx 2^{-p}\)
を決めます。実際に torch.finfo で確認すると次の通りです。
import torch
for dt in [torch.float16, torch.bfloat16, torch.float32, torch.float64]:
fi = torch.finfo(dt)
print(f"{dt}: bits={fi.bits:>2} eps={fi.eps:.3e} max={fi.max:.3e}")
torch.float16: bits=16 eps=9.766e-04 max=6.550e+04
torch.bfloat16: bits=16 eps=7.812e-03 max=3.390e+38
torch.float32: bits=32 eps=1.192e-07 max=3.403e+38
torch.float64: bits=64 eps=2.220e-16 max=1.798e+308
float32 の相対精度は約 \(1.2 \times 10^{-7}\)
(仮数部23ビット)、float64 は約 \(2.2 \times 10^{-16}\)
(仮数部52ビット)です。ディープラーニングの多くのタスクでは float32 の精度で十分な一方、float64 はメモリと演算速度で約2倍のコストがかかるため、明示的に float32 へ揃えるのが基本方針になります。
2. torch.tensor() と torch.from_numpy() の決定的な違い
この2つの変換関数は「NumPy配列からテンソルを作る」という点で似ていますが、メモリを共有するかどうかが根本的に異なります。
torch.from_numpy(arr)— 元の配列とメモリを共有するテンソルを返す(コピーしない)torch.tensor(arr)— 常にデータをコピーした新しいテンソルを返す
これは見落としがちですが、意図せず元データを書き換えてしまう典型的なバグの原因になります。実際に確認します。
import numpy as np
import torch
# --- torch.from_numpy(): メモリを共有する ---
a = np.array([1.0, 2.0, 3.0], dtype=np.float32)
t = torch.from_numpy(a)
print("before:", a, t)
t[0] = 100.0 # テンソル側をin-placeで書き換える
print("after tensor edit -> numpy array:", a)
a[1] = -999.0 # numpy配列側をin-placeで書き換える
print("after numpy edit -> tensor: ", t)
print("share memory (from_numpy):", np.shares_memory(a, t.numpy()))
before: [1. 2. 3.] tensor([1., 2., 3.])
after tensor edit -> numpy array: [100. 2. 3.]
after numpy edit -> tensor: tensor([ 100., -999., 3.])
share memory (from_numpy): True
テンソル t の要素を書き換えただけで、元のNumPy配列 a も変化しています。逆方向(NumPy配列を書き換えるとテンソルも変化する)も成立しており、np.shares_memory() で物理的に同一メモリを参照していることが確認できます。
# --- torch.tensor(): データをコピーする ---
b = np.array([1.0, 2.0, 3.0], dtype=np.float32)
u = torch.tensor(b)
u[0] = 100.0
print("after tensor edit -> numpy array (torch.tensor case):", b)
print("tensor itself:", u)
print("share memory (torch.tensor):", np.shares_memory(b, u.numpy()))
after tensor edit -> numpy array (torch.tensor case): [1. 2. 3.]
tensor itself: tensor([100., 2., 3.])
share memory (torch.tensor): False
一方 torch.tensor() はコピーを作るため、テンソル側を変更しても元のNumPy配列 b は影響を受けません。
実務上の指針: 大きな配列を読み取り専用で使う・変換コストを避けたい場合は torch.from_numpy() が高速ですが、元の配列を後で再利用する場合や、意図しない副作用を避けたい場合は torch.tensor() を使うべきです。なお torch.from_numpy() に読み取り専用(writeable=False)のNumPy配列を渡すと、以下のような警告が出ます(未定義動作になりうるためです)。
a_ro = np.array([1.0, 2.0, 3.0], dtype=np.float32)
a_ro.setflags(write=False)
t_ro = torch.from_numpy(a_ro)
print(t_ro)
UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at /Users/runner/work/pytorch/pytorch/torch/csrc/utils/tensor_numpy.cpp:219.)
tensor([1., 2., 3.])
3. 整数dtypeの罠:プラットフォーム依存とインデックス演算
NumPyの整数配列のデフォルトdtypeは、Linux/macOSでは int64 ですが、Windowsでは int32(Cのlong型に依存)になることがあります。一方でPyTorchの nn.functional.one_hot や埋め込み系のAPIの一部は、インデックスとして厳密に int64(torch.long)を要求します。この差分により「自分の環境では動くが、Windows環境ではエラーになる」という移植性バグが発生します。実際に int32 インデックスを渡すとどうなるか確認します。
import numpy as np
import torch
import torch.nn.functional as F
print("numpy default int dtype on this platform:", np.array([1, 2, 3]).dtype)
# Windows上のnp.array([...])はデフォルトでint32になることがある
idx_np_int32 = np.array([0, 2, 1], dtype=np.int32)
idx_np_int64 = np.array([0, 2, 1], dtype=np.int64)
idx64 = torch.from_numpy(idx_np_int64)
idx32 = torch.from_numpy(idx_np_int32)
print("idx64 dtype:", idx64.dtype, "/ idx32 dtype:", idx32.dtype)
out64 = F.one_hot(idx64, num_classes=5)
print("F.one_hot(int64) OK:\n", out64)
try:
out32 = F.one_hot(idx32, num_classes=5)
print("F.one_hot(int32) OK:\n", out32)
except Exception as e:
print(f"F.one_hot(int32) raised {type(e).__name__}: {e}")
numpy default int dtype on this platform: int64
idx64 dtype: torch.int64 / idx32 dtype: torch.int32
F.one_hot(int64) OK:
tensor([[1, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 1, 0, 0, 0]])
F.one_hot(int32) raised RuntimeError: one_hot is only applicable to index tensor of type LongTensor.
このように int32 のインデックスを渡すと RuntimeError になります。NumPy側でインデックス用の配列を作る際は、プラットフォームに依存しないよう dtype=np.int64 を明示するか、PyTorch側で .long()(.to(torch.int64) と同義)を呼んでから渡すのが安全です。
4. torch.set_default_dtype() は「今後作られるテンソル」にしか効かない
torch.set_default_dtype() でデフォルトの浮動小数点dtypeを変更できますが、これはすでに存在するテンソルやNumPy配列には遡って適用されません。この「今後 vs 既存」の区別を誤解しやすいポイントです。
import numpy as np
import torch
print("default dtype before:", torch.get_default_dtype())
existing = torch.tensor([1.0, 2.0, 3.0])
print("existing tensor dtype (created before change):", existing.dtype)
torch.set_default_dtype(torch.float64)
print("default dtype after set_default_dtype(float64):", torch.get_default_dtype())
new_tensor = torch.tensor([1.0, 2.0, 3.0])
print("new tensor dtype (created after change):", new_tensor.dtype)
print("existing tensor dtype (unchanged):", existing.dtype)
np_arr_f32 = np.array([1.0, 2.0, 3.0], dtype=np.float32)
print("numpy array dtype (unaffected by set_default_dtype):", np_arr_f32.dtype)
from_np = torch.from_numpy(np_arr_f32)
print("torch.from_numpy(np_arr_f32).dtype (still float32, ignores default):", from_np.dtype)
torch.set_default_dtype(torch.float32) # 元に戻す
default dtype before: torch.float32
existing tensor dtype (created before change): torch.float32
default dtype after set_default_dtype(float64): torch.float64
new tensor dtype (created after change): torch.float64
existing tensor dtype (unchanged): torch.float32
numpy array dtype (unaffected by set_default_dtype): float32
torch.from_numpy(np_arr_f32).dtype (still float32, ignores default): torch.float32
ポイントは3つです。
set_default_dtype()呼び出し前に作られたexistingテンソルは影響を受けないset_default_dtype()呼び出し後にtorch.tensor(python_list)のようにdtype未指定で新規作成したテンソルだけが新しいデフォルトになる- NumPy配列自体のdtype、および
torch.from_numpy()が引き継ぐdtype(ソース配列由来)はPyTorchのデフォルトdtype設定と無関係
「モデル全体をfloat64にしたつもりが、一部の層だけfloat32のままだった」という不具合は、大抵この2番目・3番目の誤解が原因です。
5. float16/bfloat16と縮約演算(sum/mean)の精度損失
GPUでの学習高速化のため float16(半精度)や bfloat16 を使う機会が増えていますが、sum や mean のような縮約演算では、桁落ちの蓄積により精度が大きく失われることがあります。100万要素の乱数配列で実際に確認します。
import torch
torch.manual_seed(0)
n = 1_000_000
x32 = torch.rand(n, dtype=torch.float32)
x16 = x32.to(torch.float16)
xb16 = x32.to(torch.bfloat16)
sum32 = x32.sum().item()
sum16 = x16.sum().item()
sumb16 = xb16.sum().item()
print("sum (float32): ", sum32)
print("sum (float16): ", sum16)
print("sum (bfloat16):", sumb16, " rel_err:", abs(sum32 - sumb16) / sum32)
sum (float32): 500268.03125
sum (float16): inf
sum (bfloat16): 499712.0 rel_err: 0.0011114666843904989
float16 の総和はなんと inf(無限大)になっています。これは丸め誤差の蓄積というより、オーバーフローが直接の原因です。float16 の表現可能な最大値は約 65504(torch.finfo(torch.float16).max)であり、100万要素(平均0.5程度)の和は約50万に達するため、途中で最大値を超えて inf に発散します。一方 bfloat16 は仮数部が7ビットしかなく精度は float16 より粗い(eps が約8倍大きい)ものの、指数部のビット数は float32 と同じ8ビットのため表現範囲が広く(最大値は float32 とほぼ同じ約 \(3.39 \times 10^{38}\)
)、オーバーフローせずに約0.11%の相対誤差で収まっています。
この「float16 は範囲が狭くオーバーフローしやすい/bfloat16 は範囲は広いが精度が粗い」というトレードオフは、混合精度学習(AMP)で float16 を使う際に勾配や損失のスケーリング(GradScaler など)が必要になる理由そのものです。
配列サイズを変えながら相対誤差を測定すると、オーバーフロー前後の挙動がより明確になります。
import numpy as np
import torch
torch.manual_seed(0)
sizes = [10, 30, 100, 300, 1_000, 3_000, 10_000, 30_000, 60_000, 90_000,
100_000, 110_000, 120_000, 125_000, 128_000, 130_000, 131_000,
135_000, 150_000, 300_000, 1_000_000]
x32_full = torch.rand(max(sizes), dtype=torch.float32)
for n in sizes:
x32 = x32_full[:n]
x16 = x32.to(torch.float16)
s32 = x32.sum().item()
s16 = x16.sum().item()
abs_err = abs(s32 - s16)
rel_err = abs_err / abs(s32) if np.isfinite(s16) else float("inf")
print(f"n={n:>8} sum32={s32:>12.4f} sum16={s16!s:>12} rel_err={rel_err}")
n= 10 sum32= 4.9010 sum16= 4.90234375 rel_err=0.0002822517364416764
n= 30 sum32= 13.9841 sum16= 13.984375 rel_err=2.1959499317959776e-05
n= 100 sum32= 48.8419 sum16= 48.84375 rel_err=3.686456272067971e-05
n= 300 sum32= 142.8603 sum16= 142.875 rel_err=0.00010296403991586856
n= 1000 sum32= 500.9248 sum16= 501.0 rel_err=0.00015011297463480633
n= 3000 sum32= 1491.0660 sum16= 1491.0 rel_err=4.429048565868344e-05
n= 10000 sum32= 5002.7432 sum16= 5004.0 rel_err=0.0002512293548324757
n= 30000 sum32= 15080.9707 sum16= 15080.0 rel_err=6.436609049302483e-05
n= 60000 sum32= 30031.0371 sum16= 30032.0 rel_err=3.20631825498763e-05
n= 90000 sum32= 45035.7656 sum16= 45056.0 rel_err=0.00044929568131439975
n= 100000 sum32= 50027.6211 sum16= 50016.0 rel_err=0.0002322935509610277
n= 110000 sum32= 55062.0156 sum16= 55072.0 rel_err=0.00018132963144681466
n= 120000 sum32= 60065.6875 sum16= 60064.0 rel_err=2.8094242657257524e-05
n= 125000 sum32= 62564.1562 sum16= 62560.0 rel_err=6.643180774934529e-05
n= 128000 sum32= 64081.5625 sum16= 64064.0 rel_err=0.00027406479047698
n= 130000 sum32= 65099.9414 sum16= 65088.0 rel_err=0.00018343190473061702
n= 131000 sum32= 65610.5000 sum16= inf rel_err=inf
n= 135000 sum32= 67610.8750 sum16= inf rel_err=inf
n= 150000 sum32= 75086.6797 sum16= inf rel_err=inf
n= 300000 sum32= 150034.8750 sum16= inf rel_err=inf
n= 1000000 sum32= 500268.0312 sum16= inf rel_err=inf

n が131,000未満では相対誤差は \(10^{-5}〜10^{-4}\)
程度で不規則に増減していますが、n=131,000 付近を境に float32 での総和が float16 の最大値(約65504)を超え、float16 の総和が突然 inf に発散します。これは「精度が少しずつ落ちる」のではなく「ある閾値を境に完全に破綻する」タイプの不具合であり、大規模なテンソルを float16 のまま sum/mean すると再現しうる実務上のリスクです。PyTorchの多くのAMP実装が sum 系の縮約を内部的に float32 へ自動アップキャストする、あるいは損失計算を float32 で行うのはこのためです。
6. 最近の動向:NumPy 2.0 のスカラー昇格ルール変更(NEP 50)
dtype変換に関連する最近の変更として、NumPy 2.0で導入された NEP 50(Promotion rules for Python scalars) があります。これは、Python組み込みの int/float/complex と NumPy配列を混在させたときの型昇格ルールを変更するもので、代表的な例として np.float32(3) + 3.0 の結果が、NumPy 1.x では float64 に格上げされていたのに対し、NumPy 2.0以降では float32 のまま保たれるようになりました。これはPyTorchのdtype伝播の考え方(明示しない限り既存テンソルのdtypeを保持する)に近づく変更であり、NumPy・PyTorch間で「意図せずdtypeが変わってしまう」事故を減らす方向の改善です。ただし既存コードが暗黙の float64 昇格に依存していた場合は、NumPy 2.0への移行時に挙動差として顕在化する可能性があるため注意してください。
まとめ
- NumPyのデフォルトは
float64、PyTorchのデフォルトはfloat32。変換時にdtypeを明示しないと精度・メモリ・速度の面で意図しない挙動になる torch.from_numpy()はメモリを共有し、torch.tensor()はコピーする。前者は高速だが元データへの副作用に注意- NumPyのデフォルト整数dtypeはプラットフォーム依存(Windowsでは
int32になりうる)で、PyTorchの一部API(F.one_hotなど)はint64を要求するためRuntimeErrorの原因になる torch.set_default_dtype()は今後作成されるテンソルにのみ効き、既存テンソルやNumPy配列には影響しないfloat16は表現範囲が狭くオーバーフローしやすく、bfloat16は範囲は広いが精度が粗い。大規模なsum/meanではfloat32へのアップキャストを検討する- NumPy 2.0のNEP 50によるスカラー昇格ルール変更は、暗黙のdtype変化を減らす方向の改善だが、移行時の挙動差には注意が必要
これらを踏まえ、NumPyとPyTorchを組み合わせるコードでは、変換の入口で dtype を明示的に指定する習慣をつけることが、再現性のあるバグの少ないパイプラインへの近道です。