この章では、PyTorchの中心的なデータ構造であるTensorを、思い通りに作成・変形・抽出・演算できるようになることを目指します。第1章で学んだTensorの基本を土台に、多彩な生成方法、形状操作、インデックス・スライシング、行列演算、そして初学者がつまずきやすいブロードキャスティング(Broadcasting)の仕組み、さらにCPUとGPU間でのデータ移動までを、実際に動くコードとともに一つずつ確認していきます。
学習目標
- ✅ 様々な方法でTensorを作成・初期化できる
- ✅ Tensorの形状を自在に操作できる
- ✅ インデックス・スライシングでデータを抽出できる
- ✅ ブロードキャスティングの仕組みを理解する
- ✅ CPU/GPU間でTensorを移動できる
1. Tensorの作成と初期化
第1章ではtorch.tensor()、torch.zeros()、torch.ones()、torch.rand()、torch.randn()といった基本的なTensor生成方法を紹介しました。ここでは、それらに加えて実務でよく使う生成関数と、データ型(dtype)の指定方法を整理します。
様々な生成関数
PyTorchには目的に応じた多様なTensor生成関数が用意されています。
import torch
# 等差数列(開始, 終了(含まない), ステップ)
r = torch.arange(0, 10, 2)
print(f"arange: {r}")
# 出力: arange: tensor([0, 2, 4, 6, 8])
# 指定した範囲を等間隔に分割
lin = torch.linspace(0, 1, steps=5)
print(f"linspace: {lin}")
# 出力: linspace: tensor([0.0000, 0.2500, 0.5000, 0.7500, 1.0000])
# 単位行列
identity = torch.eye(3)
print(f"eye:\n{identity}")
# 指定した値で埋めたTensor
filled = torch.full((2, 3), 7)
print(f"full:\n{filled}")
# 0〜9の整数からランダムに3x3のTensorを生成
randint_tensor = torch.randint(low=0, high=10, size=(3, 3))
print(f"randint:\n{randint_tensor}")
データ型(dtype)の指定とキャスト
Tensorには要素のデータ型を表すdtype(data type)があり、明示的に指定しない場合はPythonの数値リテラルから自動推論されます(整数のリストならint64、小数を含むリストならfloat32)。
import torch
# dtypeを明示的に指定して作成
int_tensor = torch.tensor([1, 2, 3], dtype=torch.int32)
float_tensor = torch.tensor([1, 2, 3], dtype=torch.float64)
print(f"int32: {int_tensor.dtype}, float64: {float_tensor.dtype}")
# dtypeの変換(キャスト)
x = torch.tensor([1.5, 2.7, 3.9])
print(f"元のdtype: {x.dtype}")
x_int = x.to(torch.int64) # .to()で変換
x_int2 = x.long() # 専用メソッドでも変換可能
x_float32 = x_int.float() # int から float32 へ
print(f"to(int64): {x_int}, dtype={x_int.dtype}")
print(f".long(): {x_int2}")
print(f".float(): {x_float32}, dtype={x_float32.dtype}")
💡 dtypeの不一致に注意
異なるdtype同士のTensorを演算しようとすると、エラーになったり意図しない自動変換が起きたりすることがあります。特にfloat32とfloat64が混在する場合は要注意です。演算前に.dtypeを確認する習慣をつけましょう。
既存Tensorを鋳型にした生成(_like関数)
既存のTensorと同じ形状・dtype・デバイスを持つ新しいTensorを作りたい場合、_likeが付く関数群が便利です。
import torch
original = torch.randn(2, 3, dtype=torch.float64)
print(f"元のTensor: shape={original.shape}, dtype={original.dtype}")
zeros_copy = torch.zeros_like(original)
ones_copy = torch.ones_like(original)
randn_copy = torch.randn_like(original)
print(f"zeros_like: shape={zeros_copy.shape}, dtype={zeros_copy.dtype}")
print(f"ones_like: shape={ones_copy.shape}, dtype={ones_copy.dtype}")
print(f"randn_like: shape={randn_copy.shape}, dtype={randn_copy.dtype}")
2. 形状操作(reshape, view, transpose)
ディープラーニングでは、畳み込み層の出力を全結合層に渡す前に平坦化したり、バッチ処理のために次元の並びを変えたりと、Tensorの形状を変換する場面が頻繁にあります。ここでは代表的な形状操作を見ていきます。
reshape() と view() の違い
第1章で軽く触れたreshape()とview()は、どちらも要素数を変えずに形状を変更しますが、内部的な挙動が異なります。view()はTensorがメモリ上で連続(contiguous)である場合にのみ使え、元のデータとメモリを共有します。reshape()は可能な限りview()と同じようにメモリを共有しますが、連続でない場合は自動的にコピーを作成して対応します。
import torch
x = torch.arange(12).reshape(3, 4)
print(f"x:\n{x}")
print(f"is_contiguous: {x.is_contiguous()}")
# 転置するとメモリレイアウトが不連続になる
x_t = x.t()
print(f"x_t.is_contiguous(): {x_t.is_contiguous()}")
# view()は不連続なTensorには使えない
try:
x_t.view(12)
except RuntimeError as e:
print(f"view()でエラー: {e}")
# reshape()は内部でコピーして自動的に対応する
y = x_t.reshape(12)
print(f"reshape()は成功: {y}")
# view()を使いたい場合はcontiguous()を先に呼ぶ
z = x_t.contiguous().view(12)
print(f"contiguous().view()も成功: {z}")
transpose() と permute()
transpose(dim0, dim1)は指定した2つの次元を入れ替えます。3次元以上のTensorで複数の次元を一度に並び替えたい場合はpermute()を使います。
import torch
# 例: 画像データ (channel, height, width) を (height, width, channel) に変換
image = torch.randn(3, 32, 32) # C=3, H=32, W=32
print(f"元の形状 (C, H, W): {image.shape}")
# transposeは2つの次元だけを入れ替える
transposed = image.transpose(0, 2)
print(f"transpose(0, 2): {transposed.shape}")
# permuteは次元の並びを自由に指定できる
permuted = image.permute(1, 2, 0) # (H, W, C)
print(f"permute(1, 2, 0): {permuted.shape}")
flatten() による平坦化
import torch
x = torch.randn(2, 3, 4)
# 全体を1次元に平坦化
flat_all = x.flatten()
print(f"flatten(): {flat_all.shape}") # torch.Size([24])
# start_dimを指定すると、それ以降の次元だけをまとめる
# (バッチ次元を保ったまま平坦化する典型的な用途)
flat_from_1 = x.flatten(start_dim=1)
print(f"flatten(start_dim=1): {flat_from_1.shape}") # torch.Size([2, 12])
3. インデックスとスライシング
NumPyに慣れている方には馴染み深いインデックス(index)とスライス(slice)の記法が、PyTorchでもほぼそのまま使えます。
基本的なインデックスとスライス
import torch
x = torch.arange(24).reshape(4, 6)
print(f"x:\n{x}")
print(f"x[0]: {x[0]}") # 1行目
print(f"x[0, 3]: {x[0, 3]}") # 1行4列目の要素
print(f"x[1:3]:\n{x[1:3]}") # 2〜3行目
print(f"x[:, 2]: {x[:, 2]}") # 全行の3列目
print(f"x[:, 1:4]:\n{x[:, 1:4]}") # 全行の2〜4列目
print(f"x[::2]:\n{x[::2]}") # 1つおきの行
# スカラーTensorから通常のPython数値を取り出す
scalar = x[0, 0]
print(f"scalar: {scalar}, item(): {scalar.item()}")
ブールマスクによるインデックス
条件を満たす要素だけを抽出したいときは、比較演算の結果(ブールTensor)をインデックスとして使います。
import torch
x = torch.tensor([[1, -2, 3], [-4, 5, -6]])
mask = x > 0
print(f"mask:\n{mask}")
positive_values = x[mask]
print(f"正の値のみ: {positive_values}")
# 条件を満たす要素だけを書き換える
x[x < 0] = 0
print(f"負の値を0に置換:\n{x}")
高度なインデックス(Fancy Indexing)とEllipsis
import torch
x = torch.arange(10) * 10 # tensor([0, 10, 20, ..., 90])
# 整数のTensorを使って複数要素を同時に取得
indices = torch.tensor([0, 2, 5])
print(f"fancy indexing: {x[indices]}")
# 多次元Tensorで、特定の次元だけ指定し残りは省略する
y = torch.randn(2, 3, 4, 5)
print(f"y[..., 0].shape: {y[..., 0].shape}") # 最後の次元だけ0番目を選択
print(f"y[0, ..., 0].shape: {y[0, ..., 0].shape}")
# 高度なインデックスは元データのコピーを返す(メモリを共有しない)
sub = x[indices]
sub[0] = -999
print(f"元のxは変化しない: {x[0]}")
⚠️ ビューとコピーの違い
通常のスライス(x[1:3]など)は元のTensorとメモリを共有する「ビュー(view)」を返しますが、ブールマスクや整数Tensorによる高度なインデックスは新しいメモリを確保した「コピー」を返します。ビューを変更すると元のTensorにも影響するため、この違いを意識しておくとバグを防げます。
4. 数学演算と行列演算
第1章では要素ごとの四則演算と単純な行列積を扱いました。ここでは集約演算(reduction)とバッチ行列積を含め、より実践的な演算を整理します。
集約演算(sum, mean, max, min など)
import torch
x = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print(f"合計: {x.sum()}")
print(f"平均: {x.mean()}")
print(f"最大値: {x.max()}")
print(f"最小値: {x.min()}")
# dim引数で集約する軸を指定できる
print(f"列方向(dim=0)の合計: {x.sum(dim=0)}") # tensor([5., 7., 9.])
print(f"行方向(dim=1)の合計: {x.sum(dim=1)}") # tensor([6., 15.])
# 最大値のインデックスを取得
print(f"全体の最大値のインデックス: {x.argmax()}")
print(f"行ごとの最大値のインデックス: {x.argmax(dim=1)}")
# keepdimで次元数を保ったまま集約する
print(f"keepdim=True: {x.sum(dim=1, keepdim=True).shape}") # torch.Size([2, 1])
行列積とバッチ行列積
import torch
# 2次元同士の行列積(第1章の復習)
A = torch.randn(3, 4)
B = torch.randn(4, 5)
C = A @ B # torch.matmul(A, B) と同じ
print(f"A @ B の形状: {C.shape}") # torch.Size([3, 5])
# バッチ行列積: 複数の行列を一度にまとめて計算する
batch_A = torch.randn(10, 3, 4) # バッチサイズ10、各3x4行列
batch_B = torch.randn(10, 4, 5) # バッチサイズ10、各4x5行列
batch_C = torch.bmm(batch_A, batch_B)
print(f"バッチ行列積の形状: {batch_C.shape}") # torch.Size([10, 3, 5])
# torch.matmul()はバッチ次元を自動認識するため、bmm()の代わりに使うこともできる
batch_C2 = torch.matmul(batch_A, batch_B)
print(f"同じ結果か: {torch.allclose(batch_C, batch_C2)}")
比較演算
import torch
x = torch.tensor([1, 2, 3, 4])
y = torch.tensor([4, 3, 2, 1])
print(f"x == y: {x == y}")
print(f"x > y: {x > y}")
print(f"torch.eq(x, y): {torch.eq(x, y)}")
# 全要素が等しいか / いずれかが条件を満たすか
print(f"全て等しいか: {torch.equal(x, x)}")
print(f"どれか1つでもTrueか: {(x > 2).any()}")
print(f"全てTrueか: {(x > 0).all()}")
5. ブロードキャスティング
異なる形状のTensor同士を演算しようとすると多くの場合エラーになりますが、一定の規則を満たす場合はPyTorchが自動的に形状を「拡張」して演算を可能にします。これをブロードキャスティング(Broadcasting)と呼びます。NumPyのブロードキャスティングと同じ規則です。
ブロードキャスティングの規則
2つのTensorを演算する際、PyTorchは以下の手順で形状を比較します。
- 形状を末尾(右端)の次元から順に比較する
- 各次元のサイズが等しいか、どちらか一方が1であれば互換性があるとみなす
- 一方のTensorの次元数が少ない場合、足りない次元は先頭にサイズ1が補われたものとして扱われる
- すべての次元で互換性があれば、サイズ1の次元はもう一方のサイズに合わせて仮想的に「拡張」される(実際にメモリはコピーされない)
| Tensor A の形状 | Tensor B の形状 | 結果の形状 | 可否 |
|---|---|---|---|
| (3, 4) | (4,) | (3, 4) | ✅ 可能 |
| (3, 1) | (1, 4) | (3, 4) | ✅ 可能 |
| (5, 3, 4) | (3, 4) | (5, 3, 4) | ✅ 可能 |
| (3, 4) | (3,) | — | ❌ 不可(末尾の次元が4と3で不一致) |
具体例
import torch
# 例1: スカラーとTensor
x = torch.tensor([1.0, 2.0, 3.0])
print(f"x + 10: {x + 10}") # 10がすべての要素に加算される
# 例2: 形状(3,4)と形状(4,)
a = torch.ones(3, 4)
b = torch.tensor([1.0, 2.0, 3.0, 4.0]) # 形状(4,)
print(f"a + b の形状: {(a + b).shape}") # torch.Size([3, 4])
print(f"a + b:\n{a + b}")
# 例3: 形状(3,1)と形状(1,4) を (3,4) に拡張
c = torch.tensor([[1.0], [2.0], [3.0]]) # 形状(3, 1)
d = torch.tensor([[10.0, 20.0, 30.0, 40.0]]) # 形状(1, 4)
print(f"c + d の形状: {(c + d).shape}") # torch.Size([3, 4])
print(f"c + d:\n{c + d}")
# 例4: 3次元と2次元
e = torch.ones(5, 3, 4)
f = torch.randn(3, 4)
print(f"e + f の形状: {(e + f).shape}") # torch.Size([5, 3, 4])
互換性のない形状の場合
import torch
x = torch.ones(3, 4)
y = torch.ones(3) # 形状(3,) は末尾の次元がxの4と一致しない
try:
z = x + y
except RuntimeError as e:
print(f"ブロードキャスティングエラー: {e}")
実践例:特徴量の正規化
ブロードキャスティングは、バッチデータの各特徴量を平均・標準偏差で正規化する処理で非常によく使われます。
import torch
# 5サンプル x 3特徴量のデータ
data = torch.tensor([
[1.0, 100.0, 0.5],
[2.0, 150.0, 0.7],
[3.0, 200.0, 0.9],
[4.0, 250.0, 1.1],
[5.0, 300.0, 1.3],
])
# 各特徴量(列)ごとの平均と標準偏差を計算
mean = data.mean(dim=0) # 形状(3,)
std = data.std(dim=0) # 形状(3,)
print(f"平均: {mean}")
print(f"標準偏差: {std}")
# ブロードキャスティングにより、(5,3)のdataから(3,)のmeanを
# 各行に対してまとめて引き算・除算できる
normalized = (data - mean) / std
print(f"正規化後:\n{normalized}")
print(f"正規化後の平均: {normalized.mean(dim=0)}") # ほぼ0
6. CPU/GPU間のデータ移動
第1章では基本的な.to(device)を紹介しました。ここでは、実務でよく使うパターンと注意点を確認します。
デバイスの指定とTensorの生成
import torch
# 利用可能なデバイスを自動判定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用デバイス: {device}")
# 方法1: 作成後に移動する
x = torch.randn(3, 3)
x = x.to(device)
# 方法2: 作成時に直接デバイスを指定する(中間コピーが発生せず効率的)
y = torch.randn(3, 3, device=device)
print(f"x.device: {x.device}, y.device: {y.device}")
# .cuda() / .cpu() でも移動できる(GPUが無い環境では.cuda()はエラーになる)
if torch.cuda.is_available():
z = torch.randn(3, 3).cuda()
print(f"z.device: {z.device}")
z_back = z.cpu()
print(f"z_back.device: {z_back.device}")
else:
print("GPUが利用できないため、.cuda()の例はスキップします")
デバイスの不一致によるエラー
異なるデバイス上にあるTensor同士を演算しようとすると、エラーになります。モデルとデータは必ず同じデバイスに揃える必要があります。
import torch
if torch.cuda.is_available():
x_cpu = torch.randn(3, 3)
x_gpu = torch.randn(3, 3).to("cuda")
try:
result = x_cpu + x_gpu
except RuntimeError as e:
print(f"デバイス不一致エラー: {e}")
# 正しい方法: 片方をもう一方に揃える
result = x_cpu.to("cuda") + x_gpu
print(f"揃えれば成功: {result.device}")
else:
print("この環境にはGPUがないため、デバイス不一致エラーは再現できません。")
print("概念としては、CPU上のTensorとGPU上のTensorを直接演算するとRuntimeErrorになります。")
複数のTensorをまとめて移動する
import torch
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tensors = {
"weights": torch.randn(10, 10),
"bias": torch.randn(10),
"input": torch.randn(5, 10),
}
# 辞書内包表記で一括して同じデバイスに移動
tensors = {name: t.to(device) for name, t in tensors.items()}
for name, t in tensors.items():
print(f"{name}: device={t.device}, shape={t.shape}")
⚠️ .to()の戻り値に注意
Tensorの.to()はデバイス変換が必要な場合、新しいTensorを返します(同じデバイス・dtypeなら元のTensorをそのまま返すこともあります)。そのためx = x.to(device)のように必ず戻り値を再代入してください。一方、モデル(nn.Module)の.to()はモデル自身をその場で書き換えるため、model.to(device)だけでも移動しますが、変数への再代入をしておくと挙動が統一され、可読性も上がります。
演習問題
演習1:形状操作の組み合わせ
形状(2, 3, 4)のランダムTensorをtorch.randn()で作成し、次の手順を実装してください。
permute()を使って形状を(4, 2, 3)に並べ替える- その結果に対して
flatten(start_dim=1)を適用し、形状を(4, 6)にする
解答例:
import torch
x = torch.randn(2, 3, 4)
permuted = x.permute(2, 0, 1) # (4, 2, 3)
print(f"permute後: {permuted.shape}")
flattened = permuted.flatten(start_dim=1) # (4, 6)
print(f"flatten後: {flattened.shape}")
演習2:インデックスとブロードキャスティング
torch.arange(25).reshape(5, 5)で5x5のTensorを作成し、次の操作を行ってください。
- 対角成分(0行0列〜4行4列)だけを取り出す
- ブロードキャスティングを使って、各行から「その行の最大値」を引く
解答例:
import torch
x = torch.arange(25).reshape(5, 5).float()
# 対角成分の取得
diagonal = torch.diagonal(x)
print(f"対角成分: {diagonal}")
# 別解: x[torch.arange(5), torch.arange(5)]
# 各行の最大値を引く(ブロードキャスティング)
row_max = x.max(dim=1, keepdim=True).values # 形状(5, 1)
result = x - row_max
print(f"各行の最大値を引いた結果:\n{result}")
演習3:CPU/GPU間のデータ移動
3x3のランダムTensorをCPU上に作成し、利用可能なデバイス(GPUがあればGPU、なければCPU)に移動して、そのTensor自身との行列積(A @ A)を計算し、結果をCPUに戻してください。
解答例:
import torch
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
A = torch.randn(3, 3).to(device)
result = A @ A
result_cpu = result.cpu()
print(f"計算デバイス: {A.device}")
print(f"結果 (CPU上):\n{result_cpu}")
まとめ
この章では、PyTorchのTensorを自在に操作するための基礎を学びました。
- ✅
arange・linspace・eye・full・_like系関数など、様々な方法でTensorを作成・初期化できる - ✅
reshape・view・transpose・permute・flattenでTensorの形状を自在に操作できる - ✅ 基本インデックス、スライス、ブールマスク、Fancy Indexingでデータを抽出できる
- ✅ 集約演算やバッチ行列積など、実践的な数学演算・行列演算を実行できる
- ✅ ブロードキャスティングの規則を理解し、異なる形状のTensor同士を安全に演算できる
- ✅
.to(device)を使ってCPU/GPU間でTensorを移動できる
🎉 次のステップ
Tensorを自在に操作できるようになったら、次はいよいよニューラルネットワークの構築です。続く第3章では、自動微分(autograd)をさらに深く掘り下げ、nn.Moduleを使ったモデル定義や、学習ループの実装へと進んでいきます。
参考リソース