第2章:Tensorの操作

様々な方法でTensorを作成し、自在に操作する

📖 読了時間: 25-30分 📊 難易度: 初級 💻 コード例: 18個 📝 演習: 3問

この章では、PyTorchの中心的なデータ構造であるTensorを、思い通りに作成・変形・抽出・演算できるようになることを目指します。第1章で学んだTensorの基本を土台に、多彩な生成方法、形状操作、インデックス・スライシング、行列演算、そして初学者がつまずきやすいブロードキャスティング(Broadcasting)の仕組み、さらにCPUとGPU間でのデータ移動までを、実際に動くコードとともに一つずつ確認していきます。

学習目標

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を演算しようとすると、エラーになったり意図しない自動変換が起きたりすることがあります。特にfloat32float64が混在する場合は要注意です。演算前に.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. 形状を末尾(右端)の次元から順に比較する
  2. 各次元のサイズが等しいか、どちらか一方が1であれば互換性があるとみなす
  3. 一方のTensorの次元数が少ない場合、足りない次元は先頭にサイズ1が補われたものとして扱われる
  4. すべての次元で互換性があれば、サイズ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()で作成し、次の手順を実装してください。

  1. permute()を使って形状を(4, 2, 3)に並べ替える
  2. その結果に対して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を作成し、次の操作を行ってください。

  1. 対角成分(0行0列〜4行4列)だけを取り出す
  2. ブロードキャスティングを使って、各行から「その行の最大値」を引く

解答例:

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を自在に操作するための基礎を学びました。

🎉 次のステップ

Tensorを自在に操作できるようになったら、次はいよいよニューラルネットワークの構築です。続く第3章では、自動微分(autograd)をさらに深く掘り下げ、nn.Moduleを使ったモデル定義や、学習ループの実装へと進んでいきます。


参考リソース

免責事項