Danh sách bài viết

Bài 13: Tensor — cấu trúc dữ liệu cốt lõi của PyTorch

torch.Tensor là cấu trúc dữ liệu cốt lõi của PyTorch — tổng quát hoá ndarray của NumPy, thêm hỗ trợ GPU và autograd. Bài này đi qua cách tạo tensor, attributes, dtype, indexing, reshape, math operations, broadcasting, in-place vs out-of-place, contiguous memory, manual_seed và bảng so sánh với NumPy ndarray.

24/05/2026
13 phút đọc
1 lượt xem
1

Mục tiêu bài học

Sau bài học, bạn sẽ:

  • Hiểu torch.Tensor là gì và khác numpy.ndarray ở điểm nào.
  • Tạo tensor từ list, từ NumPy, từ các hàm khởi tạo (zeros, ones, rand, randn, arange, eye…).
  • Đọc và đổi 5 thuộc tính chính: shape, ndim, dtype, device, requires_grad.
  • Biết các dtype hay dùng (float32, float16, bfloat16, int64, bool) và khi nào dùng cái nào.
  • Thực hiện indexing, slicing, boolean mask, fancy index như với NumPy.
  • Phân biệt view vs reshape, dùng squeeze / unsqueeze / permute đúng chỗ.
  • Hiểu in-place vs out-of-place, biết hệ luỵ với autograd.
  • Hiểu khái niệm contiguous và khi nào cần gọi .contiguous().

Bài 12 đã giải thích vì sao chọn PyTorch. Bài này bắt đầu với data structure — viên gạch nền của mọi tính toán trong PyTorch. Các bài tiếp theo (GPU/CUDA — Bài 14, Autograd — Bài 15) đều xây trên nền này.

2

Recap: tensor là gì

Series 1 (Bài 20 — Tensor là gì) đã định nghĩa tensor như một generalization theo số chiều:

  • Scalar — tensor 0 chiều, ví dụ 3.14.
  • Vector — tensor 1 chiều, ví dụ [1, 2, 3].
  • Matrix — tensor 2 chiều, một bảng số.
  • Tensor n chiều — mảng số nhiều chiều bất kỳ. Ảnh batch RGB là tensor 4 chiều (batch, channel, height, width); sequence batch là tensor 3 chiều (batch, seq_len, dim).

Trong PyTorch, mọi thứ "đi vào" và "đi ra" model đều là tensor: input, weight, bias, gradient, output, loss. Hiểu thao tác trên tensor là điều kiện cần trước khi đụng tới nn.Module.

3

torch.Tensor — ndarray có thêm GPU và autograd

torch.Tensor nhìn từ ngoài rất giống numpy.ndarray: cùng có shape, dtype, indexing, slicing, broadcasting, ufunc. Phần lớn người đến PyTorch từ NumPy thấy quen mặt trong vài phút.

Ba điểm tensor có mà ndarray không có:

  • Chạy được trên GPU: chỉ cần tensor.to("cuda") hoặc tensor.to("mps") (Apple Silicon). Cùng một API hoạt động trên CPU và GPU — bài 14 sẽ đi sâu.
  • Hỗ trợ autograd: nếu tensor có requires_grad=True, PyTorch sẽ ghi lại đồ thị tính toán và tính gradient tự động khi gọi .backward() — bài 15.
  • Tích hợp với hệ sinh thái deep learning: layer (nn.Linear, nn.Conv2d…), optimizer, loss function đều nhận / trả tensor.

Đối lại, ndarray có ưu thế khi bạn không cần GPU và không cần gradient — interop với scikit-learn, matplotlib, pandas thuần. Trong code thực tế, hai bên hay được chuyển qua lại (mục 5).

4

Các cách tạo tensor

Một số hàm khởi tạo dùng thường xuyên:

import torch

torch.tensor([1, 2, 3])             # từ list -> tensor 1D, dtype suy ra
torch.tensor([[1., 2.], [3., 4.]])  # từ nested list -> tensor 2D

torch.zeros(3, 4)                   # ma trận 3x4 toàn 0
torch.ones(2, 3)                    # ma trận 2x3 toàn 1
torch.full((2, 2), 7)               # ma trận 2x2 toàn 7
torch.empty(3, 3)                   # 3x3 chưa init -> giá trị rác

torch.arange(0, 10, 2)              # [0, 2, 4, 6, 8]
torch.linspace(0, 1, 11)            # 11 điểm cách đều từ 0 đến 1

torch.rand(3, 4)                    # uniform [0, 1)
torch.randn(3, 4)                   # standard normal N(0, 1)
torch.randint(0, 10, (3, 4))        # int [0, 10)

torch.eye(4)                        # ma trận đơn vị 4x4

torch.zeros_like(x)                 # cùng shape + dtype với x, toàn 0
torch.ones_like(x)                  # cùng shape + dtype với x, toàn 1

Ghi nhớ:

  • torch.tensor(data) luôn copy dữ liệu. Khác torch.as_tensor(data) — share memory nếu được.
  • torch.empty không init giá trị; chỉ dùng khi sẽ overwrite ngay. Đừng nhầm với torch.zeros.
  • Mọi hàm khởi tạo nhận thêm tham số dtype=, device=, requires_grad=. Ví dụ torch.zeros(3, 4, dtype=torch.float64, device="cuda").
5

Qua lại với NumPy

Hai chiều chuyển đổi chính:

import numpy as np
import torch

arr = np.array([1.0, 2.0, 3.0])
t = torch.from_numpy(arr)   # tensor share memory với arr (CPU)

arr2 = t.numpy()            # ndarray share memory với t (CPU)

Cảnh báo:

  • Share memory: sửa arr sẽ thấy t đổi theo, và ngược lại. Đây là điểm rất khác torch.tensor(arr) (luôn copy).
  • Chỉ áp dụng cho CPU tensor. Nếu tensor đang trên GPU, phải gọi t.cpu().numpy().
  • Một số dtype không có ánh xạ 1–1 (ví dụ bfloat16 không có trong NumPy gốc) — chuyển sẽ lỗi, cần convert dtype trước.
6

Attributes của tensor

5 thuộc tính cần thuộc:

t = torch.randn(2, 3, 4)

t.shape           # torch.Size([2, 3, 4])
t.size()          # giống shape (gọi như hàm)
t.size(1)         # 3   -> chiều thứ 1
t.ndim            # 3   -> số chiều
t.dtype           # torch.float32
t.device          # device(type='cpu')
t.requires_grad   # False (mặc định)
t.numel()         # 24  -> tổng số phần tử (2*3*4)
  • shapesize() trả về cùng object torch.Size. shape dùng tiện hơn vì là attribute (không cần ngoặc).
  • device có thể là cpu, cuda:0, cuda:1, hoặc mps (Apple Silicon).
  • requires_grad=True bật autograd. Mặc định tensor mới tạo từ zeros, ones, randn… đều False; tensor là tham số của nn.Module mặc định True.
7

dtype — kiểu dữ liệu

Các dtype hay gặp trong deep learning:

  • torch.float32 (alias torch.float) — mặc định cho training. 4 byte mỗi số.
  • torch.float64 (alias torch.double) — 8 byte. Chỉ dùng cho code khoa học cần precision cao; chậm và tốn RAM hơn float32.
  • torch.float16 (alias torch.half) — 2 byte. Dùng trong mixed-precision training để tiết kiệm RAM và tăng tốc trên GPU có Tensor Core.
  • torch.bfloat16 — 2 byte, dải số (exponent) giống float32 nhưng độ chính xác (mantissa) thấp hơn. Ổn định hơn float16 khi train LLM; hỗ trợ tốt trên Ampere (A100), Hopper (H100), TPU.
  • torch.int32, torch.int64 (alias torch.long) — cho index, label, lookup. CrossEntropyLoss yêu cầu nhãn là int64.
  • torch.bool — cho mask (kết quả của comparison).

Convert dtype:

t = torch.randn(3, 3)         # float32 mặc định

t.to(torch.float64)           # cách tổng quát
t.float()                     # -> float32
t.double()                    # -> float64
t.half()                      # -> float16
t.long()                      # -> int64
t.bool()                      # -> bool

Convert tạo tensor mới — không thay đổi t. Gán lại nếu muốn giữ: t = t.float().

8

Indexing và slicing

API gần như sao chép NumPy:

t = torch.arange(12).reshape(3, 4)
# tensor([[ 0,  1,  2,  3],
#         [ 4,  5,  6,  7],
#         [ 8,  9, 10, 11]])

t[0, 1]         # tensor(1)        -> phần tử hàng 0, cột 1
t[:, 1]         # tensor([1, 5, 9])-> cả cột 1
t[1:3, 0:2]     # 2x2 submatrix
t[-1]           # hàng cuối: tensor([ 8,  9, 10, 11])

# Boolean mask
mask = t > 5
t[mask]         # tensor([ 6,  7,  8,  9, 10, 11])

# Fancy index
t[[0, 2]]       # chọn hàng 0 và 2

Một số khác biệt nhỏ so với NumPy:

  • Single-element indexing trả về tensor(...) 0-D, không phải Python scalar. Muốn lấy scalar thuần, gọi t[0, 1].item().
  • Slice trả về view chia sẻ bộ nhớ với tensor gốc; sửa view sẽ sửa tensor gốc.
  • Fancy index (list / tensor làm index) trả về copy.
9

Reshape: view, reshape, squeeze, permute

Các thao tác đổi shape (không đổi data):

t = torch.arange(12)             # shape (12,)

t.view(3, 4)                     # (3, 4) - share memory, yêu cầu contiguous
t.reshape(3, 4)                  # (3, 4) - flexible hơn, có thể copy
t.view(-1, 4)                    # tự suy ra chiều -1 = 3

t.flatten()                      # (12,)
t.unsqueeze(0)                   # (1, 12)  -> thêm chiều ở vị trí 0
t.unsqueeze(-1)                  # (12, 1)
t.unsqueeze(0).squeeze()         # bỏ mọi chiều = 1
t.unsqueeze(0).squeeze(0)        # bỏ chiều 0 (nếu = 1)

m = torch.arange(6).reshape(2, 3)
m.transpose(0, 1)                # đổi 2 chiều -> shape (3, 2)
m.T                              # alias cho transpose ở 2D

img = torch.randn(3, 224, 224)   # (C, H, W)
img.permute(1, 2, 0)             # (H, W, C) - thứ tự matplotlib mong đợi

Phân biệt view vs reshape:

  • view chỉ thay metadata (stride, shape), không di chuyển data. Yêu cầu tensor đang contiguous; nếu không sẽ raise RuntimeError.
  • reshape cố gắng dùng view trước; nếu không được sẽ copy data. An toàn hơn nhưng có thể "âm thầm" tạo copy.
  • Code nội bộ PyTorch hay dùng view khi đảm bảo contiguous (sau .contiguous()) để chắc chắn không có copy bất ngờ.

permute đổi thứ tự nhiều chiều — kết quả thường không contiguous; nếu cần view sau đó, gọi .contiguous() trước (xem mục 14).

10

Math operations

Element-wise (giống NumPy):

a = torch.tensor([1., 2., 3.])
b = torch.tensor([4., 5., 6.])

a + b      # tensor([5., 7., 9.])
a - b
a * b      # element-wise multiply, KHÔNG phải matrix multiply
a / b
a ** 2     # luỹ thừa từng phần tử
torch.exp(a); torch.log(a); torch.sqrt(a)

Matrix multiplication (cần phân biệt với *):

A = torch.randn(3, 4)
B = torch.randn(4, 5)

torch.matmul(A, B)   # (3, 5)
A @ B                # alias - dạng đẹp hơn
A.mm(B)              # chỉ cho 2D

Reduction theo axis:

x = torch.arange(12.).reshape(3, 4)

x.sum()              # scalar -> tổng tất cả
x.sum(dim=0)         # (4,) -> tổng theo cột (gộp hàng)
x.sum(dim=1)         # (3,) -> tổng theo hàng
x.sum(dim=1, keepdim=True)   # (3, 1) - giữ chiều để broadcast tiếp

x.mean(); x.std(); x.var()
x.max(); x.min(); x.argmax(); x.argmin()

# Với dim=, max/min trả về NamedTuple (values, indices)
val, idx = x.max(dim=1)

Mọi reduction đều nhận tham số dim= (axis nào để "gộp") và keepdim= (giữ lại chiều = 1 hay không).

11

Broadcasting

Quy tắc broadcasting trong PyTorch giống NumPy (Series 1 Bài 33). Hai tensor có thể thực hiện element-wise op nếu, đọc shape từ phải sang trái, mỗi cặp chiều thoả: bằng nhau, hoặc một bên = 1, hoặc một bên thiếu.

x = torch.randn(64, 128)   # (B=64, features=128)
b = torch.randn(128)       # (features=128,)

(x + b).shape              # (64, 128) - b broadcast lên mỗi hàng

m = torch.randn(64, 1)     # cột
r = torch.randn(1, 128)    # hàng
(m + r).shape              # (64, 128) - outer-add bằng broadcasting

Trong forward pass của nn.Linear, bias shape (out_features,) được broadcast lên batch — đây là broadcasting bạn dùng mỗi ngày dù không nhận ra.

12

Concatenate và stack

Hai thao tác hay nhầm:

a = torch.zeros(2, 3)
b = torch.ones(2, 3)

torch.cat([a, b], dim=0).shape    # (4, 3) - nối dọc theo dim 0 đã có
torch.cat([a, b], dim=1).shape    # (2, 6) - nối ngang

torch.stack([a, b], dim=0).shape  # (2, 2, 3) - tạo dim MỚI ở vị trí 0
torch.stack([a, b], dim=1).shape  # (2, 2, 3) - dim mới ở vị trí 1
  • cat: nối các tensor dọc theo một chiều đã tồn tại. Tổng số chiều không đổi.
  • stack: gom các tensor cùng shape thành tensor có thêm một chiều mới. Tổng số chiều tăng 1.
  • Ứng dụng quen thuộc: cat nối các batch lại với nhau; stack gom list các sample shape (C, H, W) thành batch shape (N, C, H, W).
13

In-place vs out-of-place

PyTorch quy ước: tên hàm có dấu gạch dưới ở cuối = in-place.

a = torch.tensor([1., 2., 3.])

a + 1            # OUT-of-place: tạo tensor mới, a không đổi
a.add(1)         # cũng out-of-place
a.add_(1)        # IN-place: sửa thẳng a -> a = [2, 3, 4]
a += 1           # in-place (giống add_)

a.zero_()        # set toàn 0, in-place
a.fill_(7)       # set toàn 7, in-place

Đánh đổi:

  • In-place tiết kiệm RAM (không cấp phát tensor mới) — quan trọng với GPU và batch lớn.
  • In-place có thể phá autograd: nếu một tensor đang nằm trong đồ thị tính toán và cần thiết cho backward(), sửa in-place sẽ raise lỗi hoặc cho gradient sai. Khi chưa nắm chắc, dùng out-of-place.
  • Quy tắc thực dụng: trong code training thông thường, để PyTorch tự quản; chỉ dùng in-place khi đang tối ưu RAM rõ ràng và biết tensor đó không cần cho backward (ví dụ optimizer.zero_grad() bản chất là gọi grad.zero_()).
14

Contiguous memory layout

Tensor lưu data trong một block bộ nhớ phẳng (1D), kèm metadata stride để biết cách "đi" qua các chiều. Một tensor được gọi là contiguous nếu thứ tự phần tử trong bộ nhớ khớp với thứ tự "row-major" của shape hiện tại.

Các thao tác như transpose, permute, một số slice không di chuyển data — chúng chỉ đổi stride. Kết quả thường không còn contiguous.

t = torch.arange(12).reshape(3, 4)
t.is_contiguous()                # True

tT = t.transpose(0, 1)           # shape (4, 3), stride đảo
tT.is_contiguous()               # False

tT.view(12)                      # RuntimeError: cần contiguous
tT.contiguous().view(12)         # OK - copy thành contiguous rồi view
tT.reshape(12)                   # cũng OK - reshape tự xử lý
  • view đòi contiguous; reshape không.
  • Một số phép trên GPU hoạt động nhanh hơn (hoặc chỉ hoạt động) với tensor contiguous.
  • Gọi .contiguous() khi cần đảm bảo layout — chi phí là 1 lần copy. Đừng gọi tràn lan; chỉ khi gặp lỗi view hoặc khi đo thấy chậm bất thường.
15

Random seed

torch.manual_seed(42)         # seed cho CPU RNG
torch.cuda.manual_seed(42)    # seed cho 1 GPU
torch.cuda.manual_seed_all(42)  # seed cho mọi GPU

# Reproducible across runs
torch.manual_seed(42)
torch.randn(3)   # luôn cho cùng kết quả mỗi lần chạy

Để reproducible hoàn toàn, ngoài seed PyTorch, bạn còn cần seed NumPy (np.random.seed(42)) và Python (random.seed(42)) nếu dùng các thư viện đó. PyTorch còn có cờ torch.use_deterministic_algorithms(True) để bắt buộc thuật toán deterministic — chậm hơn nhưng cần khi debug bug khó tái lập.

16

Tensor vs NumPy ndarray

Chủ đề                NumPy ndarray            PyTorch Tensor
─────────────────────────────────────────────────────────────────
Tạo                   np.array, np.zeros        torch.tensor, torch.zeros
Shape                 arr.shape                 t.shape / t.size()
dtype mặc định float  float64                   float32
Device                CPU                       CPU / CUDA / MPS
Autograd              không                     có (requires_grad)
Matrix multiply       arr @ arr2, np.matmul     t @ t2, torch.matmul
Reshape               arr.reshape               t.view / t.reshape
Transpose             arr.T, arr.transpose      t.T, t.transpose, t.permute
In-place suffix       không quy ước             dấu _ (add_, mul_, ...)
Random                np.random.randn(...)      torch.randn(...)
Seed                  np.random.seed(42)        torch.manual_seed(42)
Convert               t.numpy()                 torch.from_numpy(arr)

Điểm hay đụng:

  • Mặc định float: NumPy là float64, PyTorch là float32. torch.from_numpy(np.array([1., 2.])) sẽ ra float64 — mất 2× RAM nếu không convert.
  • Tensor có method .item() để lấy scalar Python từ tensor 0-D; NumPy có .item() tương tự.
  • API tên hàm gần như khớp 1–1: np.exptorch.exp, np.concatenatetorch.cat (lưu ý đổi tên), np.stacktorch.stack, np.linspacetorch.linspace.
17

Code Python — tổng hợp các thao tác

import numpy as np
import torch

torch.manual_seed(0)

# 1) Tạo các loại tensor
t0 = torch.tensor([[1., 2., 3.], [4., 5., 6.]])     # 2x3
t1 = torch.zeros(3, 4)                              # 3x4 toàn 0
t2 = torch.full((2, 2), 7)                          # 2x2 toàn 7
t3 = torch.arange(0, 10, 2)                         # [0,2,4,6,8]
t4 = torch.randn(3, 4)                              # 3x4 N(0,1)
t5 = torch.eye(3)                                   # I_3

print(t0.shape, t0.dtype, t0.device)
# torch.Size([2, 3]) torch.float32 cpu

# 2) Indexing & slicing
x = torch.arange(20).reshape(4, 5)
print(x[1, 2])           # tensor(7)
print(x[:, 0])           # cột 0 -> tensor([ 0,  5, 10, 15])
print(x[x % 2 == 0])     # các phần tử chẵn

# 3) Math ops + reduction
A = torch.randn(3, 4)
B = torch.randn(4, 2)
print((A @ B).shape)             # torch.Size([3, 2])
print(A.sum(dim=0).shape)        # torch.Size([4])
print(A.mean(dim=1, keepdim=True).shape)  # torch.Size([3, 1])

# 4) Reshape
v = torch.arange(12)
print(v.view(3, 4).shape)        # (3, 4)
print(v.reshape(2, 2, 3).shape)  # (2, 2, 3)
img = torch.randn(3, 32, 32)
print(img.permute(1, 2, 0).shape)  # (32, 32, 3)

# 5) NumPy bridge
arr = np.array([1.0, 2.0, 3.0])
t = torch.from_numpy(arr)
arr[0] = 99.0
print(t)        # tensor([99.,  2.,  3.], dtype=torch.float64)
# -> share memory, nên sửa arr cũng đổi t

Output rút gọn:

torch.Size([2, 3]) torch.float32 cpu
tensor(7)
tensor([ 0,  5, 10, 15])
tensor([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18])
torch.Size([3, 2])
torch.Size([4])
torch.Size([3, 1])
torch.Size([3, 4])
torch.Size([2, 2, 3])
torch.Size([32, 32, 3])
tensor([99.,  2.,  3.], dtype=torch.float64)
18

Bài tập

  1. Tạo tensor shape (3, 4) toàn 0, dtype float32, in ra shape, dtype, device, numel().
  2. Tạo NumPy array arr = np.arange(6, dtype=np.float32).reshape(2, 3). Convert sang tensor bằng torch.from_numpy. Sửa phần tử arr[0, 0] = 100.0 — kiểm tra tensor có đổi không. Giải thích vì sao.
  3. Cho ma trận M = torch.arange(12.).reshape(3, 4). Tính mean theo từng hàng (axis=1) và theo từng cột (axis=0). In shape của kết quả.
  4. Cho tensor 1D v = torch.arange(12). Reshape thành (3, 4) rồi tiếp (2, 2, 3). In shape và in tensor cuối ra để xác nhận thứ tự phần tử.
  5. Tạo x = torch.randn(4, 3). Tính x.T @ xx @ x.T — shape mỗi kết quả là gì? Hai kết quả có cùng giá trị không?
  6. Tạo y = torch.randn(64, 10). Lấy argmax theo dim 1 — đây là cách convert logits 10 lớp thành dự đoán cho 64 sample. In shape và 5 giá trị đầu.
Đáp án ngắn
  1. torch.zeros(3, 4, dtype=torch.float32). Shape (3,4), dtype float32, device cpu, numel 12.
  2. Tensor cũng đổi: phần tử [0,0] thành 100.0. from_numpy share memory với mảng NumPy gốc (chỉ áp dụng CPU + dtype tương thích).
  3. M.mean(dim=1) shape (3,), M.mean(dim=0) shape (4,). Thêm keepdim=True sẽ ra (3,1)(1,4).
  4. v.view(3,4) rồi .view(2,2,3) — phần tử đi theo thứ tự row-major: [[[0,1,2],[3,4,5]], [[6,7,8],[9,10,11]]].
  5. x.T @ x shape (3,3), x @ x.T shape (4,4). Khác giá trị; đều là ma trận đối xứng nửa xác định dương (Gram matrix).
  6. y.argmax(dim=1) shape (64,), dtype int64 — đây chính là format input cho CrossEntropyLoss ở phía label.
19

Tóm tắt

  • torch.Tensor = ndarray + GPU + autograd.
  • Tạo từ list, từ NumPy (share memory), từ các hàm khởi tạo: zeros, ones, rand, randn, arange, linspace, eye, full, empty, các bản _like.
  • 5 attribute cần thuộc: shape, ndim, dtype, device, requires_grad.
  • dtype mặc định float32. Mixed-precision dùng float16 / bfloat16; label dùng int64; mask dùng bool.
  • Indexing / slicing / boolean mask / fancy index — gần như NumPy. Slice trả view (share memory), fancy index trả copy.
  • view share memory, đòi contiguous; reshape linh hoạt hơn, có thể copy. squeeze / unsqueeze đổi chiều = 1. permute đổi thứ tự nhiều chiều.
  • Math ops element-wise như NumPy. Matrix mul dùng @ hoặc torch.matmul. Reduction nhận dim=keepdim=.
  • Broadcasting giống NumPy.
  • In-place có suffix _ (như add_). Tiết kiệm RAM nhưng có thể phá autograd.
  • Contiguous: data layout khớp shape. transpose / permute phá contiguous; gọi .contiguous() khi cần view.
  • torch.manual_seed cho CPU; torch.cuda.manual_seed_all cho GPU.

Bài 14 sẽ học cách chuyển tensor giữa CPU và GPU (CUDA, MPS), cách viết code device-agnostic, và những gotcha thường gặp khi mix tensor ở các device khác nhau.