PyTorch原始碼分析之torchvision.transforms
PyTorch框架中有一個非常重要且好用的包:torchvision,該包主要由3個子包組成,分別是:torchvision.datasets
、torchvision.models
、torchvision.transforms
。這3個子包的具體介紹可以參考官網:http://pytorch.org/docs/master/torchvision/index.html。具體程式碼可以參考github:https://github.com/pytorch/vision/tree/master/torchvision。
這篇部落格介紹torchvision.transformas
。torchvision.transforms這個包中包含resize、crop等常見的data augmentation操作,基本上PyTorch中的data augmentation操作都可以通過該介面實現。該包主要包含兩個指令碼:
使用例子:
import torchvision
import torch
train_augmentation = torchvision.transforms.Compose([torchvision.transforms.Resize(256),
torchvision. transforms.RandomCrop(224),
torchvision.transofrms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
torch vision. Normalize([0.485, 0.456, -.406],[0.229, 0.224, 0.225])
])
Class custom_dataread(torch.utils.data.Dataset):
def __init__():
...
def __getitem__():
# use self.transform for input image
def __len__():
...
train_loader = torch.utils.data.DataLoader(
custom_dataread(transform=train_augmentation),
batch_size = batch_size, shuffle = True,
num_workers = workers, pin_memory = True)
這裡定義了resize、crop、normalize等資料預處理操作,並最終作為資料讀取類custom_dataread的一個引數傳入,可以在內部方法__getitem__中實現資料增強操作。
主要程式碼在transformas.py指令碼中,這裡僅介紹常見的data augmentation操作,原始碼如下:
首先是匯入必須的模型,這裡比較重要的是from . import functional as F,也就是匯入了functional.py指令碼中具體的data augmentation函式。__all__列表定義了可以從外部import的函式名或類名。
from __future__ import division
import torch
import math
import random
from PIL import Image, ImageOps, ImageEnhance
try:
import accimage
except ImportError:
accimage = None
import numpy as np
import numbers
import types
import collections
import warnings
from . import functional as F
__all__ = ["Compose", "ToTensor", "ToPILImage", "Normalize", "Resize",
"Scale", "CenterCrop", "Pad", "Lambda", "RandomCrop",
"RandomHorizontalFlip", "RandomVerticalFlip", "RandomResizedCrop",
"RandomSizedCrop", "FiveCrop", "TenCrop","LinearTransformation",
"ColorJitter", "RandomRotation", "Grayscale", "RandomGrayscale"]
Compose這個類是用來管理各個transform的,可以看到主要的__call__方法就是對輸入影象img迴圈所有的transform操作
class Compose(object):
"""Composes several transforms together.
Args:
transforms (list of ``Transform`` objects): list of transforms to compose.
Example:
>>> transforms.Compose([
>>> transforms.CenterCrop(10),
>>> transforms.ToTensor(),
>>> ])
"""
def __init__(self, transforms):
self.transforms = transforms
def __call__(self, img):
for t in self.transforms:
img = t(img)
return img
def __repr__(self):
format_string = self.__class__.__name__ + '('
for t in self.transforms:
format_string += '\n'
format_string += ' {0}'.format(t)
format_string += '\n)'
return format_string
ToTensor類是實現:Convert a PIL Image or numpy.ndarray to tensor 的過程,在PyTorch中常用PIL庫來讀取影象資料,因此這個方法相當於搭建了PIL Image和Tensor的橋樑。另外要強調的是在做資料歸一化之前必須要把PIL Image轉成Tensor,而其他resize或crop操作則不需要。
class ToTensor(object):
"""Convert a ``PIL Image`` or ``numpy.ndarray`` to tensor.
Converts a PIL Image or numpy.ndarray (H x W x C) in the range
[0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0].
"""
def __call__(self, pic):
"""
Args:
pic (PIL Image or numpy.ndarray): Image to be converted to tensor.
Returns:
Tensor: Converted image.
"""
return F.to_tensor(pic)
def __repr__(self):
return self.__class__.__name__ + '()'
ToPILImage顧名思義是從Tensor到PIL Image的過程,和前面ToTensor類的相反的操作。
class ToPILImage(object):
"""Convert a tensor or an ndarray to PIL Image.
Converts a torch.*Tensor of shape C x H x W or a numpy ndarray of shape
H x W x C to a PIL Image while preserving the value range.
Args:
mode (`PIL.Image mode`_): color space and pixel depth of input data (optional).
If ``mode`` is ``None`` (default) there are some assumptions made about the input data:
1. If the input has 3 channels, the ``mode`` is assumed to be ``RGB``.
2. If the input has 4 channels, the ``mode`` is assumed to be ``RGBA``.
3. If the input has 1 channel, the ``mode`` is determined by the data type (i,e,
``int``, ``float``, ``short``).
.. _PIL.Image mode: http://pillow.readthedocs.io/en/3.4.x/handbook/concepts.html#modes
"""
def __init__(self, mode=None):
self.mode = mode
def __call__(self, pic):
"""
Args:
pic (Tensor or numpy.ndarray): Image to be converted to PIL Image.
Returns:
PIL Image: Image converted to PIL Image.
"""
return F.to_pil_image(pic, self.mode)
def __repr__(self):
return self.__class__.__name__ + '({0})'.format(self.mode)
Normalize類是做資料歸一化的,一般都會對輸入資料做這樣的操作,公式也在註釋中給出了,比較容易理解。前面提到在呼叫Normalize的時候,輸入得是Tensor,這個從__call__
方法的輸入也可以看出來了。
class Normalize(object):
"""Normalize an tensor image with mean and standard deviation.
Given mean: ``(M1,...,Mn)`` and std: ``(S1,..,Sn)`` for ``n`` channels, this transform
will normalize each channel of the input ``torch.*Tensor`` i.e.
``input[channel] = (input[channel] - mean[channel]) / std[channel]``
Args:
mean (sequence): Sequence of means for each channel.
std (sequence): Sequence of standard deviations for each channel.
"""
def __init__(self, mean, std):
self.mean = mean
self.std = std
def __call__(self, tensor):
"""
Args:
tensor (Tensor): Tensor image of size (C, H, W) to be normalized.
Returns:
Tensor: Normalized Tensor image.
"""
return F.normalize(tensor, self.mean, self.std)
def __repr__(self):
return self.__class__.__name__ + '(mean={0}, std={1})'.format(self.mean, self.std)
Resize類是對PIL Image做resize操作的,幾乎都要用到。這裡輸入可以是int,此時表示將輸入影象的短邊resize到這個int數,長邊則根據對應比例調整,影象的長寬比不變。如果輸入是個(h,w)的序列,h和w都是int,則直接將輸入影象resize到這個(h,w)尺寸,相當於force resize,所以一般最後影象的長寬比會變化,也就是影象內容被拉長或縮短。注意,在__call__方法中呼叫了functional.py指令碼中的resize函式來完成resize操作,因為輸入是PIL Image,所以resize函式基本是在呼叫Image的各種方法。如果輸入是Tensor,則對應函式基本是在呼叫Tensor的各種方法,這就是functional.py中的主要內容。
class Resize(object):
"""Resize the input PIL Image to the given size.
Args:
size (sequence or int): Desired output size. If size is a sequence like
(h, w), output size will be matched to this. If size is an int,
smaller edge of the image will be matched to this number.
i.e, if height > width, then image will be rescaled to
(size * height / width, size)
interpolation (int, optional): Desired interpolation. Default is
``PIL.Image.BILINEAR``
"""
def __init__(self, size, interpolation=Image.BILINEAR):
assert isinstance(size, int) or (isinstance(size, collections.Iterable) and len(size) == 2)
self.size = size
self.interpolation = interpolation
def __call__(self, img):
"""
Args:
img (PIL Image): Image to be scaled.
Returns:
PIL Image: Rescaled image.
"""
return F.resize(img, self.size, self.interpolation)
def __repr__(self):
return self.__class__.__name__ + '(size={0})'.format(self.size)
CenterCrop是以輸入圖的中心點為中心點做指定size的crop操作,一般資料增強不會採用這個,因為當size固定的時候,在相同輸入影象的情況下,N次CenterCrop的結果都是一樣的。註釋裡面說明了size為int和序列時候尺寸的定義
class CenterCrop(object):
"""Crops the given PIL Image at the center.
Args:
size (sequence or int): Desired output size of the crop. If size is an
int instead of sequence like (h, w), a square crop (size, size) is
made.
"""
def __init__(self, size):
if isinstance(size, numbers.Number):
self.size = (int(size), int(size))
else:
self.size = size
def __call__(self, img):
"""
Args:
img (PIL Image): Image to be cropped.
Returns:
PIL Image: Cropped image.
"""
return F.center_crop(img, self.size)
def __repr__(self):
return self.__class__.__name__ + '(size={0})'.format(self.size)
相比前面的CenterCrop,這個RandomCrop更常用,差別就在於crop時的中心點座標是隨機的,並不是輸入影象的中心點座標,因此基本上每次crop生成的影象都是有差異的。就是通過 i = random.randint(0, h - th)和 j = random.randint(0, w - tw)兩行生成一個隨機中心點的橫縱座標。注意到在__call__中最後是呼叫了F.crop(img, i, j, h, w)來完成crop操作,其實前面CenterCrop中雖然是呼叫 F.center_crop(img, self.size),但是在F.center_crop()函式中只是先計算了中心點座標,最後還是呼叫F.crop(img, i, j, h, w)完成crop操作。
class RandomCrop(object):
"""Crop the given PIL Image at a random location.
Args:
size (sequence or int): Desired output size of the crop. If size is an
int instead of sequence like (h, w), a square crop (size, size) is
made.
padding (int or sequence, optional): Optional padding on each border
of the image. Default is 0, i.e no padding. If a sequence of length
4 is provided, it is used to pad left, top, right, bottom borders
respectively.
"""
def __init__(self, size, padding=0):
if isinstance(size, numbers.Number):
self.size = (int(size), int(size))
else:
self.size = size
self.padding = padding
@staticmethod
def get_params(img, output_size):
"""Get parameters for ``crop`` for a random crop.
Args:
img (PIL Image): Image to be cropped.
output_size (tuple): Expected output size of the crop.
Returns:
tuple: params (i, j, h, w) to be passed to ``crop`` for random crop.
"""
w, h = img.size
th, tw = output_size
if w == tw and h == th:
return 0, 0, h, w
i = random.randint(0, h - th)
j = random.randint(0, w - tw)
return i, j, th, tw
def __call__(self, img):
"""
Args:
img (PIL Image): Image to be cropped.
Returns:
PIL Image: Cropped image.
"""
if self.padding > 0:
img = F.pad(img, self.padding)
i, j, h, w = self.get_params(img, self.size)
return F.crop(img, i, j, h, w)
def __repr__(self):
return self.__class__.__name__ + '(size={0})'.format(self.size)
RandomHorizontalFlip類也是比較常用的,是隨機的影象水平翻轉,通俗講就是影象的左右對調。從該類中的__call__方法可以看出水平翻轉的概率是0.5。
class RandomHorizontalFlip(object):
"""Horizontally flip the given PIL Image randomly with a probability of 0.5."""
def __call__(self, img):
"""
Args:
img (PIL Image): Image to be flipped.
Returns:
PIL Image: Randomly flipped image.
"""
if random.random() < 0.5:
return F.hflip(img)
return img
def __repr__(self):
return self.__class__.__name__ + '()'
同樣的,RandomVerticalFlip類是隨機的影象豎直翻轉,通俗講就是影象的上下對調。
class RandomVerticalFlip(object):
"""Vertically flip the given PIL Image randomly with a probability of 0.5."""
def __call__(self, img):
"""
Args:
img (PIL Image): Image to be flipped.
Returns:
PIL Image: Randomly flipped image.
"""
if random.random() < 0.5:
return F.vflip(img)
return img
def __repr__(self):
return self.__class__.__name__ + '()'
RandomResizedCrop類也是比較常用的,個人非常喜歡用。前面不管是CenterCrop還是RandomCrop,在crop的時候其尺寸是固定的,而這個類則是random size的crop。該類主要用到3個引數:size、scale和ratio,總的來講就是先做crop(用到scale和ratio),再resize到指定尺寸(用到size)。做crop的時候,其中心點座標和長寬是由get_params方法得到的,在get_params方法中主要用到兩個引數:scale和ratio,首先在scale限定的數值範圍內隨機生成一個數,用這個數乘以輸入影象的面積作為crop後圖像的面積;然後在ratio限定的數值範圍內隨機生成一個數,表示長寬的比值,根據這兩個值就可以得到crop影象的長寬了。至於crop影象的中心點座標,也是類似RandomCrop類一樣是隨機生成的。
class RandomResizedCrop(object):
"""Crop the given PIL Image to random size and aspect ratio.
A crop of random size (default: of 0.08 to 1.0) of the original size and a random
aspect ratio (default: of 3/4 to 4/3) of the original aspect ratio is made. This crop
is finally resized to given size.
This is popularly used to train the Inception networks.
Args:
size: expected output size of each edge
scale: range of size of the origin size cropped
ratio: range of aspect ratio of the origin aspect ratio cropped
interpolation: Default: PIL.Image.BILINEAR
"""
def __init__(self, size, scale=(0.08, 1.0), ratio=(3. / 4., 4. / 3.), interpolation=Image.BILINEAR):
self.size = (size, size)
self.interpolation = interpolation
self.scale = scale
self.ratio = ratio
@staticmethod
def get_params(img, scale, ratio):
"""Get parameters for ``crop`` for a random sized crop.
Args:
img (PIL Image): Image to be cropped.
scale (tuple): range of size of the origin size cropped
ratio (tuple): range of aspect ratio of the origin aspect ratio cropped
Returns:
tuple: params (i, j, h, w) to be passed to ``crop`` for a random
sized crop.
"""
for attempt in range(10):
area = img.size[0] * img.size[1]
target_area = random.uniform(*scale) * area
aspect_ratio = random.uniform(*ratio)
w = int(round(math.sqrt(target_area * aspect_ratio)))
h = int(round(math.sqrt(target_area / aspect_ratio)))
if random.random() < 0.5:
w, h = h, w
if w <= img.size[0] and h <= img.size[1]:
i = random.randint(0, img.size[1] - h)
j = random.randint(0, img.size[0] - w)
return i, j, h, w
# Fallback
w = min(img.size[0], img.size[1])
i = (img.size[1] - w) // 2
j = (img.size[0] - w) // 2
return i, j, w, w
def __call__(self, img):
"""
Args:
img (PIL Image): Image to be flipped.
Returns:
PIL Image: Randomly cropped and resize image.
"""
i, j, h, w = self.get_params(img, self.scale, self.ratio)
return F.resized_crop(img, i, j, h, w, self.size, self.interpolation)
def __repr__(self):
return self.__class__.__name__ + '(size={0})'.format(self.size)
FiveCrop類,顧名思義就是從一張輸入影象中crop出5張指定size的影象,這5張影象包括4個角的影象和一個center crop的影象。曾在TSN演算法的看到過這種用法。
class FiveCrop(object):
"""Crop the given PIL Image into four corners and the central crop
.. Note::
This transform returns a tuple of images and there may be a mismatch in the number of
inputs and targets your Dataset returns. See below for an example of how to deal with
this.
Args:
size (sequence or int): Desired output size of the crop. If size is an ``int``
instead of sequence like (h, w), a square crop of size (size, size) is made.
Example:
>>> transform = Compose([
>>> FiveCrop(size), # this is a list of PIL Images
>>> Lambda(lambda crops: torch.stack([ToTensor()(crop) for crop in crops])) # returns a 4D tensor
>>> ])
>>> #In your test loop you can do the following:
>>> input, target = batch # input is a 5d tensor, target is 2d
>>> bs, ncrops, c, h, w = input.size()
>>> result = model(input.view(-1, c, h, w)) # fuse batch size and ncrops
>>> result_avg = result.view(bs, ncrops, -1).mean(1) # avg over crops
"""
def __init__(self, size):
self.size = size
if isinstance(size, numbers.Number):
self.size = (int(size), int(size))
else:
assert len(size) == 2, "Please provide only two dimensions (h, w) for size."
self.size = size
def __call__(self, img):
return F.five_crop(img, self.size)
def __repr__(self):
return self.__class__.__name__ + '(size={0})'.format(self.size)
TenCrop類和前面FiveCrop類類似,只不過在FiveCrop的基礎上,再將輸入影象進行水平或豎直翻轉,然後再進行FiveCrop操作,這樣一張輸入影象就能得到10張crop結果
class TenCrop(object):
"""Crop the given PIL Image into four corners and the central crop plus the flipped version of
these (horizontal flipping is used by default)
.. Note::
This transform returns a tuple of images and there may be a mismatch in the number of
inputs and targets your Dataset returns. See below for an example of how to deal with
this.
Args:
size (sequence or int): Desired output size of the crop. If size is an
int instead of sequence like (h, w), a square crop (size, size) is
made.
vertical_flip(bool): Use vertical flipping instead of horizontal
Example:
>>> transform = Compose([
>>> TenCrop(size), # this is a list of PIL Images
>>> Lambda(lambda crops: torch.stack([ToTensor()(crop) for crop in crops])) # returns a 4D tensor
>>> ])
>>> #In your test loop you can do the following:
>>> input, target = batch # input is a 5d tensor, target is 2d
>>> bs, ncrops, c, h, w = input.size()
>>> result = model(input.view(-1, c, h, w)) # fuse batch size and ncrops
>>> result_avg = result.view(bs, ncrops, -1).mean(1) # avg over crops
"""
def __init__(self, size, vertical_flip=False):
self.size = size
if isinstance(size, numbers.Number):
self.size = (int(size), int(size))
else:
assert len(size) == 2, "Please provide only two dimensions (h, w) for size."
self.size = size
self.vertical_flip = vertical_flip
def __call__(self, img):
return F.ten_crop(img, self.size, self.vertical_flip)
def __repr__(self):
return self.__class__.__name__ + '(size={0})'.format(self.size)
LinearTransformation類是用一個變換矩陣去乘輸入影象得到輸出結果。
class LinearTransformation(object):
"""Transform a tensor image with a square transformation matrix computed
offline.
Given transformation_matrix, will flatten the torch.*Tensor, compute the dot
product with the transformation matrix and reshape the tensor to its
original shape.
Applications:
- whitening: zero-center the data, compute the data covariance matrix
[D x D] with np.dot(X.T, X), perform SVD on this matrix and
pass it as transformation_matrix.
Args:
transformation_matrix (Tensor): tensor [D x D], D = C x H x W
"""
def __init__(self, transformation_matrix):
if transformation_matrix.size(0) != transformation_matrix.size(1):
raise ValueError("transformation_matrix should be square. Got " +
"[{} x {}] rectangular matrix.".format(
相關推薦
PyTorch原始碼分析之torchvision.transforms
PyTorch框架中有一個非常重要且好用的包:torchvision,該包主要由3個子包組成,分別是:torchvision.datasets、torchvision.models、torchvision.transforms。這3個子包的具體介紹可以參考官網:http://pytorch.
PyTorch原始碼解讀之torchvision.transforms(轉)
原文地址:https://blog.csdn.net/u014380165/article/details/79167753
PyTorch框架中有一個非常重要且好用的包:torchvision,該包主要由3個子包組成,分別是:torchvision.dat
PyTorch原始碼解讀之torchvision.transforms
PyTorch框架中有一個非常重要且好用的包:torchvision,該包主要由3個子包組成,分別是:torchvision.datasets、torchvision.models、torchvision.transforms。這3個子包的具體介紹可以參考
PyTorch原始碼分析之torchvision.models
PyTorch框架中有一個非常重要且好用的包:torchvision,該包主要由3個子包組成,分別是:torchvision.datasets、torchvision.models、torchvision.transforms。這3個子包的具體介紹可以參考官網:http://pytorch.
PyTorch原始碼解讀之torchvision.models(轉)
原文地址:https://blog.csdn.net/u014380165/article/details/79119664
PyTorch框架中有一個非常重要且好用的包:torchvision,該包主要由3個子包組成,分別是:torchvision.datasets、torchvision.mode
PyTorch原始碼解讀之torchvision.models
這篇部落格介紹torchvision.models。torchvision.models這個包中包含alexnet、densenet、inception、resnet、squeezenet、vgg等常用的網路結構,並且提供了預訓練模型,可以通過簡單呼叫來讀取
PyTorch源碼解讀之torchvision.transforms(轉)
visio warnings class this small ews release vfl pretty 原文地址:https://blog.csdn.net/u014380165/article/details/79167753
版權聲明:本文為博主原創文章,未經
Spark原始碼分析之Spark Shell(上)
https://www.cnblogs.com/xing901022/p/6412619.html
文中分析的spark版本為apache的spark-2.1.0-bin-hadoop2.7。
bin目錄結構:
-rwxr-xr-x. 1 bigdata bigdata 1089 Dec
Netty 原始碼分析之拆包器的奧祕
為什麼要粘包拆包
為什麼要粘包
首先你得了解一下TCP/IP協議,在使用者資料量非常小的情況下,極端情況下,一個位元組,該TCP資料包的有效載荷非常低,傳遞100位元組的資料,需要100次TCP傳送,100次ACK,在應用及時性要求不高的情況下,將這100個有效資料拼接成一個數據包,那會縮短到一個TCP資
Android原始碼分析之為什麼在onCreate() 和 onResume() 獲取不到 View 的寬高
轉載自:https://www.jianshu.com/p/d7ab114ac1f7
先來看一段很熟悉的程式碼,可能在最開始接觸安卓的時候,大部分人都寫過的一段程式碼;即嘗試在 onCreate() 和 onResume() 方法中去獲取某個 View 的寬高資訊:
但是列印輸出後,我們會發
netty原始碼分析之服務端啟動
ServerBootstrap與Bootstrap分別是netty中服務端與客戶端的引導類,主要負責服務端與客戶端初始化、配置及啟動引導等工作,接下來我們就通過netty原始碼中的示例對ServerBootstrap與Bootstrap的原始碼進行一個簡單的分析。首先我們知道這兩個類都繼承自AbstractB
SNMP原始碼分析之(一)配置檔案部分
snmpd.conf想必不陌生。在程序啟動過程中會去讀取配置檔案中各個配置。其中幾個引數需要先知道是幹什麼的:
token:配置檔案的每行的開頭,例如
group MyROGroup v1 readSec
這行token的引數是group。
【kubernetes/k8s原始碼分析】kubelet原始碼分析之cdvisor原始碼分析
資料流
UnsecuredDependencies -> run
1. cadvisor.New初始化
if kubeDeps.CAdvisorInterface == nil {
imageFsInfoProvider := cadv
【kubernetes/k8s原始碼分析】kubelet原始碼分析之容器網路初始化原始碼分析
一. 網路基礎
1.1 網路名稱空間的操作
建立網路名稱空間: ip netns add
名稱空間內執行命令: ip netns exec
進入名稱空間: ip netns exec bash
1.2 bridge-nf-c
【kubernetes/k8s原始碼分析】kubelet原始碼分析之資源上報
0. 資料流
路徑: pkg/kubelet/kubelet.go
Run函式() ->
syncNodeStatus () ->
registerWithAPIServer() ->
【kubernetes/k8s原始碼分析】kubelet原始碼分析之啟動容器
主要是呼叫runtime,這裡預設為docker
0. 資料流
NewMainKubelet(cmd/kubelet/app/server.go) ->
NewKubeGenericRuntimeManager(pkg/kubelet/kuberuntime/kuberuntime
Android系統原始碼分析之-ContentProvider
距離上一次寫部落格已經半年多了,這半年發生了很多事情,也有了很多感觸,最主要是改變了忙碌了工作,更加重視身體的健康,為此也把工作地點從深圳這個一線城市換到了珠海,工作相對沒有那麼累,身體感覺也好了很多。所以在工作完成之餘,也有了更多的時間來自我學習和提高,後續會用更多時間來寫更多實用的東西,幫助我們理解
Vue 原始碼分析之proxy代理
Vue 原始碼分析之proxy代理
當我們在使用Vue進行資料設定時,通常初始化格式為:
let data = {
age: 12,
name: 'yang'
}
// 例項化Vue物件
let vm = new Vue({
data
})
Qt原始碼分析之事件分發器QEventDispatcherWin32
分析Qt原始碼一則想自己在開發學習中有積累,同時自己也一直有一種理念,使用她那麼就更深入的認識她。 如果有分析不正確的,還煩請各位看官指正。
事件分發器建立 在QCoreApplication建構函式中
if (!QCoreApplicationPrivate
lodash原始碼分析之isArguments
lodash原始碼分析之isArguments
有人命中註定要過平庸的生活,默默無聞,因為他們經歷了痛苦或不幸;有人卻故意這樣做,那是因為他們得到的幸福超過了他們的承受能力。
——卡爾維諾《煙雲》
本文為讀 lodash 原始碼的第二十一篇,後續文章會更新到這個倉庫中,歡迎 star:poc