.. _sec_image_augmentation:
Hình ảnh Augmentation
=====================
Trong :numref:`sec_alexnet`, chúng tôi đã đề cập rằng các tập dữ liệu
lớn là điều kiện tiên quyết cho sự thành công của các mạng thần kinh sâu
trong các ứng dụng khác nhau. *Tăng cường hình ảnh* tạo ra các ví dụ đào
tạo tương tự nhưng khác biệt sau một loạt các thay đổi ngẫu nhiên đối
với hình ảnh đào tạo, từ đó mở rộng quy mô của bộ đào tạo. Ngoài ra,
tăng cường hình ảnh có thể được thúc đẩy bởi thực tế là các tinh chỉnh
ngẫu nhiên của các ví dụ đào tạo cho phép các mô hình ít dựa vào các
thuộc tính nhất định, do đó cải thiện khả năng tổng quát của chúng. Ví
dụ, chúng ta có thể cắt một hình ảnh theo những cách khác nhau để làm
cho đối tượng quan tâm xuất hiện ở các vị trí khác nhau, do đó làm giảm
sự phụ thuộc của một mô hình vào vị trí của đối tượng. Chúng tôi cũng có
thể điều chỉnh các yếu tố như độ sáng và màu sắc để giảm độ nhạy của mô
hình với màu sắc. Có lẽ đúng là nâng hình ảnh là không thể thiếu cho sự
thành công của AlexNet vào thời điểm đó. Trong phần này, chúng tôi sẽ
thảo luận về kỹ thuật được sử dụng rộng rãi này trong tầm nhìn máy tính.
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
from mxnet import autograd, gluon, image, init, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
.. raw:: html
.. raw:: html
Phương pháp Augmentation hình ảnh phổ biến
------------------------------------------
Trong cuộc điều tra của chúng tôi về các phương pháp nâng hình ảnh phổ
biến, chúng tôi sẽ sử dụng hình ảnh :math:`400\times 500` sau đây một ví
dụ.
.. raw:: html
.. raw:: html
.. code:: python
d2l.set_figsize()
img = image.imread('../img/cat1.jpg')
d2l.plt.imshow(img.asnumpy());
.. figure:: output_image-augmentation_7d0887_12_0.svg
.. raw:: html
.. raw:: html
.. code:: python
d2l.set_figsize()
img = d2l.Image.open('../img/cat1.jpg')
d2l.plt.imshow(img);
.. figure:: output_image-augmentation_7d0887_15_0.svg
.. raw:: html
.. raw:: html
Hầu hết các phương pháp nâng hình ảnh đều có một mức độ ngẫu nhiên nhất
định. Để giúp chúng ta dễ dàng quan sát hiệu ứng của việc nâng hình ảnh
hơn, tiếp theo chúng ta xác định một hàm phụ ``apply``. Chức năng này
chạy phương pháp augmentation hình ảnh ``aug`` nhiều lần trên hình ảnh
đầu vào ``img`` và hiển thị tất cả các kết quả.
.. raw:: html
.. raw:: html
.. code:: python
def apply(img, aug, num_rows=2, num_cols=4, scale=1.5):
Y = [aug(img) for _ in range(num_rows * num_cols)]
d2l.show_images(Y, num_rows, num_cols, scale=scale)
.. raw:: html
.. raw:: html
.. code:: python
def apply(img, aug, num_rows=2, num_cols=4, scale=1.5):
Y = [aug(img) for _ in range(num_rows * num_cols)]
d2l.show_images(Y, num_rows, num_cols, scale=scale)
.. raw:: html
.. raw:: html
Lật và cắt xén
~~~~~~~~~~~~~~
.. raw:: html
.. raw:: html
Lật lại hình ảnh bên trái và phải thường không thay đổi danh mục của đối
tượng. Đây là một trong những phương pháp nâng hình ảnh sớm nhất và được
sử dụng rộng rãi nhất. Tiếp theo, chúng ta sử dụng mô-đun ``transforms``
để tạo phiên bản ``RandomFlipLeftRight``, nó lật một hình ảnh sang trái
và phải với cơ hội 50%.
.. code:: python
apply(img, gluon.data.vision.transforms.RandomFlipLeftRight())
.. figure:: output_image-augmentation_7d0887_31_0.svg
Flipping lên và xuống không phổ biến như lật trái và phải. Nhưng ít nhất
đối với hình ảnh ví dụ này, lật lên xuống không cản trở sự nhận dạng.
Tiếp theo, chúng ta tạo một phiên bản ``RandomFlipTopBottom`` để lật ảnh
lên xuống với 50% cơ hội.
.. code:: python
apply(img, gluon.data.vision.transforms.RandomFlipTopBottom())
.. figure:: output_image-augmentation_7d0887_33_0.svg
.. raw:: html
.. raw:: html
Lật lại hình ảnh bên trái và phải thường không thay đổi danh mục của đối
tượng. Đây là một trong những phương pháp nâng hình ảnh sớm nhất và được
sử dụng rộng rãi nhất. Tiếp theo, chúng ta sử dụng mô-đun ``transforms``
để tạo phiên bản ``RandomHorizontalFlip``, nó lật một hình ảnh sang trái
và phải với cơ hội 50%.
.. code:: python
apply(img, torchvision.transforms.RandomHorizontalFlip())
.. figure:: output_image-augmentation_7d0887_37_0.svg
Flipping lên và xuống không phổ biến như lật trái và phải. Nhưng ít nhất
đối với hình ảnh ví dụ này, lật lên xuống không cản trở sự nhận dạng.
Tiếp theo, chúng ta tạo một phiên bản ``RandomVerticalFlip`` để lật ảnh
lên xuống với 50% cơ hội.
.. code:: python
apply(img, torchvision.transforms.RandomVerticalFlip())
.. figure:: output_image-augmentation_7d0887_39_0.svg
.. raw:: html
.. raw:: html
Trong hình ảnh ví dụ chúng tôi đã sử dụng, con mèo nằm ở giữa hình ảnh,
nhưng điều này có thể không phải là trường hợp nói chung. Trong
:numref:`sec_pooling`, chúng tôi giải thích rằng lớp tổng hợp có thể
làm giảm độ nhạy của một lớp phức tạp đến vị trí mục tiêu. Ngoài ra,
chúng ta cũng có thể cắt ngẫu nhiên hình ảnh để làm cho các đối tượng
xuất hiện ở các vị trí khác nhau trong ảnh ở các thang đo khác nhau,
điều này cũng có thể làm giảm độ nhạy của một mô hình đến vị trí mục
tiêu.
Trong mã dưới đây, chúng ta ngẫu nhiên crop một khu vực có diện tích
:math:`10\% \sim 100 \%` of the original area each time, and the ratio
of width to height of this area is randomly selected from
:math:`0.5 \sim 2`. Sau đó, chiều rộng và chiều cao của vùng đều được
thu nhỏ thành 200 pixel. Trừ khi có quy định khác, số ngẫu nhiên giữa
:math:`a` và :math:`b` trong phần này đề cập đến một giá trị liên tục
thu được bằng cách lấy mẫu ngẫu nhiên và thống nhất từ khoảng
:math:`[a, b]`.
.. raw:: html
.. raw:: html
.. code:: python
shape_aug = gluon.data.vision.transforms.RandomResizedCrop(
(200, 200), scale=(0.1, 1), ratio=(0.5, 2))
apply(img, shape_aug)
.. figure:: output_image-augmentation_7d0887_45_0.svg
.. raw:: html
.. raw:: html
.. code:: python
shape_aug = torchvision.transforms.RandomResizedCrop(
(200, 200), scale=(0.1, 1), ratio=(0.5, 2))
apply(img, shape_aug)
.. figure:: output_image-augmentation_7d0887_48_0.svg
.. raw:: html
.. raw:: html
Thay đổi màu sắc
~~~~~~~~~~~~~~~~
Một phương pháp nâng cao khác là thay đổi màu sắc. Chúng ta có thể thay
đổi bốn khía cạnh của màu hình ảnh: độ sáng, độ tương phản, độ bão hòa
và màu sắc. Trong ví dụ dưới đây, chúng ta thay đổi ngẫu nhiên độ sáng
của hình ảnh thành giá trị giữa 50% (:math:`1-0.5`) và 150%
(:math:`1+0.5`) của ảnh gốc.
.. raw:: html
.. raw:: html
.. code:: python
apply(img, gluon.data.vision.transforms.RandomBrightness(0.5))
.. figure:: output_image-augmentation_7d0887_54_0.svg
.. raw:: html
.. raw:: html
.. code:: python
apply(img, torchvision.transforms.ColorJitter(
brightness=0.5, contrast=0, saturation=0, hue=0))
.. figure:: output_image-augmentation_7d0887_57_0.svg
.. raw:: html
.. raw:: html
Tương tự, chúng ta có thể ngẫu nhiên thay đổi hue của hình ảnh.
.. raw:: html
.. raw:: html
.. code:: python
apply(img, gluon.data.vision.transforms.RandomHue(0.5))
.. figure:: output_image-augmentation_7d0887_63_0.svg
.. raw:: html
.. raw:: html
.. code:: python
apply(img, torchvision.transforms.ColorJitter(
brightness=0, contrast=0, saturation=0, hue=0.5))
.. figure:: output_image-augmentation_7d0887_66_0.svg
.. raw:: html
.. raw:: html
Chúng ta cũng có thể tạo một phiên bản ``RandomColorJitter`` và đặt cách
thay đổi ngẫu nhiên ``brightness``, ``contrast``, ``saturation`` và
``hue`` của ảnh cùng một lúc.
.. raw:: html
.. raw:: html
.. code:: python
color_aug = gluon.data.vision.transforms.RandomColorJitter(
brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
apply(img, color_aug)
.. figure:: output_image-augmentation_7d0887_72_0.svg
.. raw:: html
.. raw:: html
.. code:: python
color_aug = torchvision.transforms.ColorJitter(
brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
apply(img, color_aug)
.. figure:: output_image-augmentation_7d0887_75_0.svg
.. raw:: html
.. raw:: html
Kết hợp nhiều phương pháp Augmentation ảnh
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Trong thực tế, chúng ta sẽ kết hợp nhiều phương pháp tăng cường hình
ảnh. Ví dụ: chúng ta có thể kết hợp các phương thức augmentation hình
ảnh khác nhau được xác định ở trên và áp dụng chúng cho mỗi hình ảnh
thông qua một phiên bản ``Compose``.
.. raw:: html
.. raw:: html
.. code:: python
augs = gluon.data.vision.transforms.Compose([
gluon.data.vision.transforms.RandomFlipLeftRight(), color_aug, shape_aug])
apply(img, augs)
.. figure:: output_image-augmentation_7d0887_81_0.svg
.. raw:: html
.. raw:: html
.. code:: python
augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug])
apply(img, augs)
.. figure:: output_image-augmentation_7d0887_84_0.svg
.. raw:: html
.. raw:: html
Đào tạo với hình ảnh Augmentation
---------------------------------
Hãy để chúng tôi đào tạo một mô hình với nâng hình ảnh. Ở đây chúng tôi
sử dụng bộ dữ liệu CIFAR-10 thay vì tập dữ liệu Fashion-MNIST mà chúng
tôi đã sử dụng trước đây. Điều này là do vị trí và kích thước của các
đối tượng trong bộ dữ liệu Fashion-MNIST đã được chuẩn hóa, trong khi
màu sắc và kích thước của các đối tượng trong tập dữ liệu CIFAR-10 có sự
khác biệt đáng kể hơn. 32 hình ảnh đào tạo đầu tiên trong tập dữ liệu
CIFAR-10 được hiển thị bên dưới.
.. raw:: html
.. raw:: html
.. code:: python
d2l.show_images(gluon.data.vision.CIFAR10(
train=True)[0:32][0], 4, 8, scale=0.8);
.. figure:: output_image-augmentation_7d0887_90_0.svg
.. raw:: html
.. raw:: html
.. code:: python
all_images = torchvision.datasets.CIFAR10(train=True, root="../data",
download=True)
d2l.show_images([all_images[i][0] for i in range(32)], 4, 8, scale=0.8);
.. parsed-literal::
:class: output
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ../data/cifar-10-python.tar.gz
97.4%
.. raw:: html
.. raw:: html
Để có được kết quả dứt khoát trong quá trình dự đoán, chúng ta thường
chỉ áp dụng nâng hình ảnh cho các ví dụ đào tạo và không sử dụng nâng
hình ảnh với các phép toán ngẫu nhiên trong quá trình dự đoán. Ở đây
chúng tôi chỉ sử dụng phương pháp lật trái phải ngẫu nhiên đơn giản
nhất. Ngoài ra, chúng ta sử dụng một phiên bản ``ToTensor`` để chuyển
đổi một minibatch ảnh thành định dạng theo yêu cầu của khung học sâu,
tức là các số điểm nổi 32 bit giữa 0 đến 1 với hình dạng (kích thước lô,
số kênh, chiều cao, chiều rộng).
.. raw:: html
.. raw:: html
.. code:: python
train_augs = gluon.data.vision.transforms.Compose([
gluon.data.vision.transforms.RandomFlipLeftRight(),
gluon.data.vision.transforms.ToTensor()])
test_augs = gluon.data.vision.transforms.Compose([
gluon.data.vision.transforms.ToTensor()])
Tiếp theo, chúng tôi xác định một chức năng phụ trợ để tạo điều kiện đọc
hình ảnh và áp dụng nâng hình ảnh. Hàm ``transform_first`` được cung cấp
bởi các bộ dữ liệu của Gluon áp dụng nâng hình ảnh cho phần tử đầu tiên
của mỗi ví dụ đào tạo (hình ảnh và nhãn), tức là hình ảnh. Để biết giới
thiệu chi tiết về ``DataLoader``, vui lòng tham khảo
:numref:`sec_fashion_mnist`.
.. code:: python
def load_cifar10(is_train, augs, batch_size):
return gluon.data.DataLoader(
gluon.data.vision.CIFAR10(train=is_train).transform_first(augs),
batch_size=batch_size, shuffle=is_train,
num_workers=d2l.get_dataloader_workers())
.. raw:: html
.. raw:: html
.. code:: python
train_augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor()])
test_augs = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()])
Tiếp theo, chúng ta xác định một hàm phụ trợ để tạo điều kiện đọc hình
ảnh và áp dụng tăng cường hình ảnh. Đối số ``transform`` được cung cấp
bởi tập dữ liệu của PyTorch áp dụng tăng cường để biến đổi hình ảnh. Để
biết giới thiệu chi tiết về ``DataLoader``, vui lòng tham khảo
:numref:`sec_fashion_mnist`.
.. code:: python
def load_cifar10(is_train, augs, batch_size):
dataset = torchvision.datasets.CIFAR10(root="../data", train=is_train,
transform=augs, download=True)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
shuffle=is_train, num_workers=d2l.get_dataloader_workers())
return dataloader
.. raw:: html
.. raw:: html
Đào tạo Multi-GPU
~~~~~~~~~~~~~~~~~
Chúng tôi đào tạo mô hình ResNet-18 từ :numref:`sec_resnet` trên bộ dữ
liệu CIFAR-10. Nhớ lại phần giới thiệu về đào tạo đa GPU trong
:numref:`sec_multi_gpu_concise`. Sau đây, chúng tôi định nghĩa một hàm
để đào tạo và đánh giá mô hình bằng cách sử dụng nhiều GPU .
.. raw:: html
.. raw:: html
.. code:: python
#@save
def train_batch_ch13(net, features, labels, loss, trainer, devices,
split_f=d2l.split_batch):
"""Train for a minibatch with mutiple GPUs (defined in Chapter 13)."""
X_shards, y_shards = split_f(features, labels, devices)
with autograd.record():
pred_shards = [net(X_shard) for X_shard in X_shards]
ls = [loss(pred_shard, y_shard) for pred_shard, y_shard
in zip(pred_shards, y_shards)]
for l in ls:
l.backward()
# The `True` flag allows parameters with stale gradients, which is useful
# later (e.g., in fine-tuning BERT)
trainer.step(labels.shape[0], ignore_stale_grad=True)
train_loss_sum = sum([float(l.sum()) for l in ls])
train_acc_sum = sum(d2l.accuracy(pred_shard, y_shard)
for pred_shard, y_shard in zip(pred_shards, y_shards))
return train_loss_sum, train_acc_sum
#@save
def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices=d2l.try_all_gpus(), split_f=d2l.split_batch):
"""Train a model with mutiple GPUs (defined in Chapter 13)."""
timer, num_batches = d2l.Timer(), len(train_iter)
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
# Sum of training loss, sum of training accuracy, no. of examples,
# no. of predictions
metric = d2l.Accumulator(4)
for i, (features, labels) in enumerate(train_iter):
timer.start()
l, acc = train_batch_ch13(
net, features, labels, loss, trainer, devices, split_f)
metric.add(l, acc, labels.shape[0], labels.size)
timer.stop()
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[2], metric[1] / metric[3],
None))
test_acc = d2l.evaluate_accuracy_gpus(net, test_iter, split_f)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {metric[0] / metric[2]:.3f}, train acc '
f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on '
f'{str(devices)}')
.. raw:: html
.. raw:: html
.. code:: python
#@save
def train_batch_ch13(net, X, y, loss, trainer, devices):
"""Train for a minibatch with mutiple GPUs (defined in Chapter 13)."""
if isinstance(X, list):
# Required for BERT fine-tuning (to be covered later)
X = [x.to(devices[0]) for x in X]
else:
X = X.to(devices[0])
y = y.to(devices[0])
net.train()
trainer.zero_grad()
pred = net(X)
l = loss(pred, y)
l.sum().backward()
trainer.step()
train_loss_sum = l.sum()
train_acc_sum = d2l.accuracy(pred, y)
return train_loss_sum, train_acc_sum
#@save
def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices=d2l.try_all_gpus()):
"""Train a model with mutiple GPUs (defined in Chapter 13)."""
timer, num_batches = d2l.Timer(), len(train_iter)
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1],
legend=['train loss', 'train acc', 'test acc'])
net = nn.DataParallel(net, device_ids=devices).to(devices[0])
for epoch in range(num_epochs):
# Sum of training loss, sum of training accuracy, no. of examples,
# no. of predictions
metric = d2l.Accumulator(4)
for i, (features, labels) in enumerate(train_iter):
timer.start()
l, acc = train_batch_ch13(
net, features, labels, loss, trainer, devices)
metric.add(l, acc, labels.shape[0], labels.numel())
timer.stop()
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[2], metric[1] / metric[3],
None))
test_acc = d2l.evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {metric[0] / metric[2]:.3f}, train acc '
f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on '
f'{str(devices)}')
.. raw:: html
.. raw:: html
Bây giờ chúng ta có thể xác định hàm ``train_with_data_aug`` để đào tạo
mô hình với khả năng tăng cường hình ảnh. Chức năng này nhận được tất cả
các GPU có sẵn, sử dụng Adam làm thuật toán tối ưu hóa, áp dụng nâng
hình ảnh cho tập dữ liệu đào tạo và cuối cùng gọi hàm ``train_ch13`` vừa
được định nghĩa để đào tạo và đánh giá mô hình.
.. raw:: html
.. raw:: html
.. code:: python
batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10)
net.initialize(init=init.Xavier(), ctx=devices)
def train_with_data_aug(train_augs, test_augs, net, lr=0.001):
train_iter = load_cifar10(True, train_augs, batch_size)
test_iter = load_cifar10(False, test_augs, batch_size)
loss = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(), 'adam',
{'learning_rate': lr})
train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices)
.. raw:: html
.. raw:: html
.. code:: python
batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10, 3)
def init_weights(m):
if type(m) in [nn.Linear, nn.Conv2d]:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
def train_with_data_aug(train_augs, test_augs, net, lr=0.001):
train_iter = load_cifar10(True, train_augs, batch_size)
test_iter = load_cifar10(False, test_augs, batch_size)
loss = nn.CrossEntropyLoss(reduction="none")
trainer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices)
.. raw:: html
.. raw:: html
Hãy để chúng tôi đào tạo mô hình bằng cách sử dụng nâng hình ảnh dựa
trên lật trái phải ngẫu nhiên.
.. raw:: html
.. raw:: html
.. code:: python
train_with_data_aug(train_augs, test_augs, net)
.. parsed-literal::
:class: output
loss 0.169, train acc 0.942, test acc 0.848
3132.6 examples/sec on [gpu(0), gpu(1)]
.. figure:: output_image-augmentation_7d0887_130_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train_with_data_aug(train_augs, test_augs, net)
.. parsed-literal::
:class: output
loss 0.176, train acc 0.939, test acc 0.814
5458.4 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)]
.. figure:: output_image-augmentation_7d0887_133_1.svg
.. raw:: html
.. raw:: html
Tóm tắt
-------
- Nâng hình ảnh tạo ra hình ảnh ngẫu nhiên dựa trên dữ liệu đào tạo
hiện có để cải thiện khả năng tổng quát hóa của các mô hình.
- Để có được kết quả dứt khoát trong quá trình dự đoán, chúng ta thường
chỉ áp dụng nâng hình ảnh cho các ví dụ đào tạo và không sử dụng nâng
hình ảnh với các phép toán ngẫu nhiên trong quá trình dự đoán.
- Khung học sâu cung cấp nhiều phương pháp nâng hình ảnh khác nhau, có
thể được áp dụng đồng thời.
Bài tập
-------
1. Đào tạo mô hình mà không cần sử dụng nâng hình ảnh:
``train_with_data_aug(test_augs, test_augs)``. So sánh độ chính xác
của đào tạo và kiểm tra khi sử dụng và không sử dụng nâng hình ảnh.
Thí nghiệm so sánh này có thể hỗ trợ lập luận rằng việc tăng cường
hình ảnh có thể giảm thiểu quá mức không? Tại sao?
2. Kết hợp nhiều phương pháp nâng hình ảnh khác nhau trong đào tạo mô
hình trên tập dữ liệu CIFAR-10. Nó có cải thiện độ chính xác của thử
nghiệm không?
3. Tham khảo tài liệu trực tuyến của khung học sâu. Những phương pháp
nâng hình ảnh nào khác mà nó cũng cung cấp?
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html