# 【Pytorch 深度學習筆記】用 Tensors 表示現實世界的資訊 [TOC] 哈囉大家好我是 LukeTseng,感謝您點進本篇筆記,該篇筆記主要配合讀本 《Deep Learning with pytorch》 進行學習,另外透過網路資料作為輔助。本系列筆記是我本人奠基深度學習基礎知識的開始,若文章有誤煩請各位指正,謝謝! 本篇為 《Deep Learning with pytorch》 這本書第四章 Real-world data representation using tensors 的相關筆記。 ## 處理影像 一張彩色照片是如何用數字表示的? 用張量表示。假設有張 800×600 像素的照片: * 每個像素有 3 個顏色通道(Channel):RGB。 * 整張照片可以表示為形狀為 `(3, 800, 600)` 的張量。 * 第一個維度:3 個顏色 Channel。 * 第二、三個維度:圖片的寬和高。 由於書中範例是使用 imageio Module,但我比較習慣用 PIL,所以以下是 PIL 的範例: 有一張向日葵的圖片我拿來當範例(文章的圖片是壓縮過的,下載原檔執行程式才會是正確結果): ![Sunflower_from_Silesia2~1](https://hackmd.io/_uploads/H1lIJvsl-g.jpg) Image Source:https://commons.wikimedia.org/wiki/File:Sunflower_from_Silesia2.jpg 程式碼(在 Jupyter Notebook 上執行): ```python= import torch from PIL import Image from torchvision import transforms img = Image.open('Sunflower_from_Silesia2.jpg') img_tensor = transforms.PILToTensor()(img) print(img_tensor.shape) ``` Output: ``` torch.Size([3, 1697, 2434]) ``` 在以往的 numpy 跟 PIL 中,輸出的格式通常都是 HWC,H 就是 Height,W 就是 Width,C 就是 Channel。但到了 tensor 中,就變成了 CHW 的順序,因為在卷積神經網路運算中,CHW 格式比較有效率啦。 `transforms.PILToTensor()` 會自動重新排列維度,使用 `permute(2, 0, 1)` 將 HWC 轉換成 CHW。 也可直接輸出他的 tensor: ```python= import torch from PIL import Image from torchvision import transforms img = Image.open('Sunflower_from_Silesia2.jpg') img_tensor = transforms.PILToTensor()(img) print(img_tensor) ``` Output: ``` tensor([[[ 52, 56, 53, ..., 40, 36, 37], [ 50, 53, 56, ..., 41, 37, 38], [ 51, 51, 52, ..., 40, 38, 38], ..., [ 49, 50, 48, ..., 35, 35, 35], [ 48, 47, 50, ..., 35, 37, 36], [ 51, 48, 49, ..., 37, 40, 39]], [[104, 108, 105, ..., 87, 91, 89], [105, 105, 108, ..., 89, 89, 91], [106, 103, 103, ..., 88, 89, 89], ..., [ 94, 94, 94, ..., 81, 80, 80], [ 94, 93, 96, ..., 81, 81, 80], [ 97, 94, 95, ..., 81, 81, 80]], [[188, 194, 191, ..., 177, 173, 172], [188, 191, 194, ..., 174, 172, 171], [189, 189, 192, ..., 173, 170, 170], ..., [179, 181, 180, ..., 169, 171, 171], [182, 181, 182, ..., 167, 170, 169], [183, 180, 181, ..., 168, 171, 170]]], dtype=torch.uint8) ``` tensor 中的值代表每個像素點的顏色強度,數值範圍是 0 到 255(dtype=torch.uint8)。 看到佔有兩個中括號的 `[[` 有三個,就分別代表三個 Channel:RGB。 `torch.uint8` 為 8 bit 的 unsigned int,剛好對應 0 ~ 255。 ### 正規化(normalization) 之後若要訓練深度學習模型,通常會將剛才例子中的這些值除以 255,轉換成 0.0 到 1.0 的浮點數範圍,這個過程叫做正規化(normalization)。 正規化有兩種寫法,首先第一種就是直接除以 255,但記得要先把原本 tensor 裡面的 data type 轉成 float。 ```python= import torch from PIL import Image from torchvision import transforms img = Image.open('Sunflower_from_Silesia2.jpg') img_tensor = transforms.PILToTensor()(img) img_normalized = img_tensor.float() / 255.0 print(img_normalized) print(img_normalized.min(), img_normalized.max()) ``` Output: ``` tensor([[[0.2039, 0.2196, 0.2078, ..., 0.1569, 0.1412, 0.1451], [0.1961, 0.2078, 0.2196, ..., 0.1608, 0.1451, 0.1490], [0.2000, 0.2000, 0.2039, ..., 0.1569, 0.1490, 0.1490], ..., [0.1922, 0.1961, 0.1882, ..., 0.1373, 0.1373, 0.1373], [0.1882, 0.1843, 0.1961, ..., 0.1373, 0.1451, 0.1412], [0.2000, 0.1882, 0.1922, ..., 0.1451, 0.1569, 0.1529]], [[0.4078, 0.4235, 0.4118, ..., 0.3412, 0.3569, 0.3490], [0.4118, 0.4118, 0.4235, ..., 0.3490, 0.3490, 0.3569], [0.4157, 0.4039, 0.4039, ..., 0.3451, 0.3490, 0.3490], ..., [0.3686, 0.3686, 0.3686, ..., 0.3176, 0.3137, 0.3137], [0.3686, 0.3647, 0.3765, ..., 0.3176, 0.3176, 0.3137], [0.3804, 0.3686, 0.3725, ..., 0.3176, 0.3176, 0.3137]], [[0.7373, 0.7608, 0.7490, ..., 0.6941, 0.6784, 0.6745], [0.7373, 0.7490, 0.7608, ..., 0.6824, 0.6745, 0.6706], [0.7412, 0.7412, 0.7529, ..., 0.6784, 0.6667, 0.6667], ..., [0.7020, 0.7098, 0.7059, ..., 0.6627, 0.6706, 0.6706], [0.7137, 0.7098, 0.7137, ..., 0.6549, 0.6667, 0.6627], [0.7176, 0.7059, 0.7098, ..., 0.6588, 0.6706, 0.6667]]]) tensor(0.) tensor(1.) ``` 第二種方法則是使用 `ToTensor()`,取代掉原本的函式 `PILToTensor()`,那麼他就會自動正規化了: ```python= import torch from PIL import Image from torchvision import transforms img = Image.open('Sunflower_from_Silesia2.jpg') img_tensor = transforms.ToTensor()(img) print(img_tensor) print(img_tensor.dtype) ``` Output: ``` tensor([[[0.2039, 0.2196, 0.2078, ..., 0.1569, 0.1412, 0.1451], [0.1961, 0.2078, 0.2196, ..., 0.1608, 0.1451, 0.1490], [0.2000, 0.2000, 0.2039, ..., 0.1569, 0.1490, 0.1490], ..., [0.1922, 0.1961, 0.1882, ..., 0.1373, 0.1373, 0.1373], [0.1882, 0.1843, 0.1961, ..., 0.1373, 0.1451, 0.1412], [0.2000, 0.1882, 0.1922, ..., 0.1451, 0.1569, 0.1529]], [[0.4078, 0.4235, 0.4118, ..., 0.3412, 0.3569, 0.3490], [0.4118, 0.4118, 0.4235, ..., 0.3490, 0.3490, 0.3569], [0.4157, 0.4039, 0.4039, ..., 0.3451, 0.3490, 0.3490], ..., [0.3686, 0.3686, 0.3686, ..., 0.3176, 0.3137, 0.3137], [0.3686, 0.3647, 0.3765, ..., 0.3176, 0.3176, 0.3137], [0.3804, 0.3686, 0.3725, ..., 0.3176, 0.3176, 0.3137]], [[0.7373, 0.7608, 0.7490, ..., 0.6941, 0.6784, 0.6745], [0.7373, 0.7490, 0.7608, ..., 0.6824, 0.6745, 0.6706], [0.7412, 0.7412, 0.7529, ..., 0.6784, 0.6667, 0.6667], ..., [0.7020, 0.7098, 0.7059, ..., 0.6627, 0.6706, 0.6706], [0.7137, 0.7098, 0.7137, ..., 0.6549, 0.6667, 0.6627], [0.7176, 0.7059, 0.7098, ..., 0.6588, 0.6706, 0.6667]]]) torch.float32 ``` 其實還有一種寫法,是 DL 中常用的技巧,就是做比較進階的正規化,正規化到 `[-1, 1]`: ```python= import torch from PIL import Image from torchvision import transforms img = Image.open('Sunflower_from_Silesia2.jpg') transform = transforms.Compose([ transforms.ToTensor(), # 轉成 [0, 1] transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) # 轉成 [-1, 1] ]) img_tensor = transform(img) print(img_tensor) ``` Output: ``` tensor([[[-0.5922, -0.5608, -0.5843, ..., -0.6863, -0.7176, -0.7098], [-0.6078, -0.5843, -0.5608, ..., -0.6784, -0.7098, -0.7020], [-0.6000, -0.6000, -0.5922, ..., -0.6863, -0.7020, -0.7020], ..., [-0.6157, -0.6078, -0.6235, ..., -0.7255, -0.7255, -0.7255], [-0.6235, -0.6314, -0.6078, ..., -0.7255, -0.7098, -0.7176], [-0.6000, -0.6235, -0.6157, ..., -0.7098, -0.6863, -0.6941]], [[-0.1843, -0.1529, -0.1765, ..., -0.3176, -0.2863, -0.3020], [-0.1765, -0.1765, -0.1529, ..., -0.3020, -0.3020, -0.2863], [-0.1686, -0.1922, -0.1922, ..., -0.3098, -0.3020, -0.3020], ..., [-0.2627, -0.2627, -0.2627, ..., -0.3647, -0.3725, -0.3725], [-0.2627, -0.2706, -0.2471, ..., -0.3647, -0.3647, -0.3725], [-0.2392, -0.2627, -0.2549, ..., -0.3647, -0.3647, -0.3725]], [[ 0.4745, 0.5216, 0.4980, ..., 0.3882, 0.3569, 0.3490], [ 0.4745, 0.4980, 0.5216, ..., 0.3647, 0.3490, 0.3412], [ 0.4824, 0.4824, 0.5059, ..., 0.3569, 0.3333, 0.3333], ..., [ 0.4039, 0.4196, 0.4118, ..., 0.3255, 0.3412, 0.3412], [ 0.4275, 0.4196, 0.4275, ..., 0.3098, 0.3333, 0.3255], [ 0.4353, 0.4118, 0.4196, ..., 0.3176, 0.3412, 0.3333]]]) ``` ## 3D 影像體積資料(3D images: Volumetric data) 醫療掃描(如 CT 掃描、MRI)生成的資料即屬於 3D 影像。 例如一張 CT 掃描可能有 100 到 500 層,每層皆為一張 512x512 的影像。那 tensor shape 有可能就是 `(100, 512, 512)`。 以下是作者範例(From [dlwpt-code/p1ch4/2_volumetric_ct.ipynb at master · deep-learning-with-pytorch/dlwpt-code](https://github.com/deep-learning-with-pytorch/dlwpt-code/blob/master/p1ch4/2_volumetric_ct.ipynb)): ```python= import numpy as np import torch import imageio torch.set_printoptions(edgeitems=2, threshold=50) dir_path = "./data/p1ch4/volumetric-dicom/2-LUNG 3.0 B70f-04083" vol_arr = imageio.volread(dir_path, 'DICOM') print(vol_arr.shape) vol = torch.from_numpy(vol_arr).float() vol = torch.unsqueeze(vol, 0) print(vol.shape) %matplotlib inline import matplotlib.pyplot as plt plt.imshow(vol_arr[50]) # 顯示第 50 個 slice plt.show() ``` Output: ![image](https://hackmd.io/_uploads/ByRpvV3x-g.png) imageio 可讀取 `DICOM` 檔案,而 PIL 不行,PIL 需要搭配 pydicom 才能讀取。 而 DICOM 為醫療數位影像傳輸協定,全名是 Digital Imaging and Communications in Medicine。 ```python torch.set_printoptions(edgeitems=2, threshold=50) ``` 設定當印出 tensor 時,只顯示開頭和結尾各 2 個元素,超過 50 個元素就省略中間部分。 ```python= dir_path = "./data/p1ch4/volumetric-dicom/2-LUNG 3.0 B70f-04083" vol_arr = imageio.volread(dir_path, 'DICOM') print(vol_arr.shape) ``` 為讀取 DICOM 檔案的程式。 `imageio.volread(dir_path, 'DICOM')` 會讀取整個資料夾中的 DICOM 檔案。 `vol_arr.shape` 顯示 `(99, 512, 512)`,表示有 99 張 CT slice,512x512 像素。 這部分跟前面 Image 的 tensors 表示是一樣的。 `vol = torch.unsqueeze(vol, 0)` 在第 0 個位置增加一個維度。原本形狀 `(99, 512, 512)` 變成 `(1, 99, 512, 512)`。 這多出的一個維度是 batch(批次)維度。 ## 表格資料 最常見的就是像 .csv、.xlsx 這種二維的表格資料了,那該怎麼用 tensor 表示呢?如下(範例使用 pandas 分析讀取資料): ```python= import pandas as pd import torch data = pd.read_csv('data.csv') # 分離特徵和標籤 input_data = data.iloc[:, :-1] # 前幾欄為特徵 output_data = data.iloc[:, -1] # 最後一欄為標籤 # 轉換為Tensor input_tensor = torch.Tensor(input_data.to_numpy()) output_tensor = torch.tensor(output_data.to_numpy()) print('輸入格式:', input_tensor.shape, input_tensor.dtype) print('輸出格式:', output_tensor.shape, output_tensor.dtype) print(f'input_tensor = {input_tensor}') print(f'output_tensor = {output_tensor}') ``` 使用的 `data.csv` 檔案內容如下: ```csv 面積,房間數,屋齡,距離車站,房價 85.5,3,10,500,15000000 120.0,4,5,300,25000000 65.2,2,15,800,12000000 95.8,3,8,450,18000000 110.5,4,3,200,28000000 ``` Output: ``` 輸入格式: torch.Size([5, 4]) torch.float32 輸出格式: torch.Size([5]) torch.int64 input_tensor = tensor([[ 85.5000, 3.0000, 10.0000, 500.0000], [120.0000, 4.0000, 5.0000, 300.0000], [ 65.2000, 2.0000, 15.0000, 800.0000], [ 95.8000, 3.0000, 8.0000, 450.0000], [110.5000, 4.0000, 3.0000, 200.0000]]) output_tensor = tensor([15000000, 25000000, 12000000, 18000000, 28000000]) ``` 將資料分離成這樣的目的是為了可以預測房價輸出,而前面四項因素是影響房價的特徵,故而當作輸入特徵。 可以將這些資料建立 TensorDataSet,以利後續訓練模型(將資料打包成 TensorDataSet 的形式): 在此之前要先引入 `from torch.utils.data import TensorDataset, DataLoader`。 ```python= from torch.utils.data import TensorDataset, DataLoader # 建立TensorDataset dataset = TensorDataset(input_tensor, output_tensor) # 建立DataLoader進行批次讀取 dataloader = DataLoader(dataset, batch_size=32, shuffle=True) for batch_idx, (features, labels) in enumerate(dataloader): print(f'\n批次 {batch_idx + 1}:') print(f'特徵形狀: {features.shape}') print(f'標籤形狀: {labels.shape}') print(f'特徵範例:\n{features[:2]}') # 顯示前2筆 print(f'標籤範例: {labels[:2]}') ``` Output: ``` 批次 1: 特徵形狀: torch.Size([5, 4]) 標籤形狀: torch.Size([5]) 特徵範例: tensor([[ 95.8000, 3.0000, 8.0000, 450.0000], [120.0000, 4.0000, 5.0000, 300.0000]]) 標籤範例: tensor([18000000, 25000000]) ``` 批次只有 1 是因為 .csv 的資料筆數太少了,而 batch_size 又設定成 32,所以他會把那裡面所有筆的資料塞在同一個批次裡面,因而得到輸出只有批次 1 的內容。 ## 處理類別資料(非數字資料):One-hot encoding 假設有一列記錄「天氣情況」: * 1=晴天 * 2=陰天 * 3=下雨 * 4=下雪 若直接用 1 2 3 4 會有個問題,就是模型可能會認為 4 > 3 > 2 > 1,但天氣之間沒有順序關係。 所以解決方法就是用 One-Hot 編碼來處理這些資料。 假設天氣值:`[1, 2, 3, 4, 1]` One-hot 編碼過後: ``` 1 → [1, 0, 0, 0] 2 → [0, 1, 0, 0] 3 → [0, 0, 1, 0] 4 → [0, 0, 0, 1] 1 → [1, 0, 0, 0] ``` 在 PyTorch 中可以使用 `torch.nn.functional.one_hot()` 函數來實現 One-hot 編碼。 要使用這個之前要引入:`import torch.nn.functional as F` 範例: ```python= import torch import torch.nn.functional as F # 原始天氣資料:1=晴天,2=陰天,3=下雨,4=下雪 weather_data = torch.tensor([1, 2, 3, 4, 1]) # 因為 based-index 從 0 開始所以要 -1 # 或使用 0~3 表示也行 weather_indices = weather_data - 1 # 轉換為 [0, 1, 2, 3, 0] # 進行 One-Hot 編碼,num_classes=4 表示有 4 種類別 weather_onehot = F.one_hot(weather_indices, num_classes=4) print("原始天氣資料:") print(weather_data) print("\n轉換後的索引:") print(weather_indices) print("\nOne-Hot 編碼結果:") print(weather_onehot) print("\n形狀:", weather_onehot.shape) # torch.Size([5, 4]) ``` Output: ``` 原始天氣資料: tensor([1, 2, 3, 4, 1]) 轉換後的索引: tensor([0, 1, 2, 3, 0]) One-Hot 編碼結果: tensor([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 0, 0]]) 形狀: torch.Size([5, 4]) ``` 為什麼需要在 1, 2, 3, 4 後面又加一個 ,1 ?主要是避免模型他學到每種天氣的樣子,而無法學到某種天氣比較常見的統計特性。 而以下是 One-hot 編碼的結果: ``` One-Hot 編碼結果: tensor([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 0, 0]]) ``` 每一個陣列裡面的四個值代表晴天、陰天、下雨、下雪,如果當前索引值是晴天的話,那第一個值會是 1,其他都是 0,以此類推。 ### One-hot encoding One-hot encoding(獨熱編碼)是 ML 中將類別資料轉換成數值形式的常用方法。 原理大致上是為每個類別建立一個二元向量,在該向量中只有一個位置的值為 1,其他位置都是 0。如同剛才範例所見的 output。 優點: * 避免類別間被誤解為有數值大小順序。 * 讓機器學習模型能公平學習每個類別。 * 保留各類別的獨立性與非序關係。 缺點: * 當類別數量很多時,會導致特徵維度劇增(維度災難)。 * 產生稀疏矩陣,計算上可能較耗費資源。 ## 處理時間序列資料 time series 時間序列的意思是資料的順序是重要的,因為在時間上有因果關係。相反處理表格資料時,資料順序反而不是那麼重要,每一行都是獨立的。 實際的例子像是預測股票價格、天氣預測等等,前者的解釋可能是今天價格會受到昨天的影響,後者的解釋為今天下雨,而明天可能會繼續下雨之類的。 ~~由於我找不太到有什麼比較好的 dataset 來取代這本書的範例,所以就用它的吧XD。~~ 程式範例至:https://github.com/deep-learning-with-pytorch/dlwpt-code/blob/master/p1ch4/4_time_series_bikes.ipynb 這邊使用的資料集是華盛頓特區共享單車系統(Capital Bikeshare)2011-2012 年的每小時租車數量,加上天氣和季節資訊。 ```python= import numpy as np import torch torch.set_printoptions(edgeitems=2, threshold=50, linewidth=75) bikes_numpy = np.loadtxt( "./data/p1ch4/bike-sharing-dataset/hour-fixed.csv", dtype=np.float32, delimiter=",", skiprows=1, converters={1: lambda x: float(x[8:10])}) bikes = torch.from_numpy(bikes_numpy) bikes ``` 以下的程式碼,`skiprows` 為跳過標題行,`converters={1: lambda x: float(x[8:10])})` 為將日期字串轉換成日期數字。 ```python= bikes_numpy = np.loadtxt( "./data/p1ch4/bike-sharing-dataset/hour-fixed.csv", dtype=np.float32, delimiter=",", skiprows=1, converters={1: lambda x: float(x[8:10])}) ``` 為啥要把日期字串轉成日期數字,而且這個轉換結果會是什麼?請看以下範例: | 原始日期字串 | `x[8:10]` | `float(x[8:10])` | | ------------ | ------- | -------------- | | "2011-01-01" | "01" | 1.0 | | "2011-01-15" | "15" | 15.0 | | "2011-01-31" | "31" | 31.0 | | "2012-12-05" | "05" | 5.0 | 轉換後的「日期數字」就是一個月中的第幾天(1-31)。 這樣子設計的原因是為了簡化資料,一個 NN 不需要去知道完整的日期,只要知道這在一個月的哪幾天即可。 另外就是可以減少維度,用一個數字(1-31)比用完整日期字串來得簡單。 最後就是可以保留週期性的資訊,月份中的日期有週期性(每月重複),這在學習模式上有蠻大的幫助的。 輸出結果會是長下面這樣: ``` tensor([[1.0000e+00, 1.0000e+00, ..., 1.3000e+01, 1.6000e+01], [2.0000e+00, 1.0000e+00, ..., 3.2000e+01, 4.0000e+01], ..., [1.7378e+04, 3.1000e+01, ..., 4.8000e+01, 6.1000e+01], [1.7379e+04, 3.1000e+01, ..., 3.7000e+01, 4.9000e+01]]) ``` 用 shape 看的話會是這樣: ``` torch.Size([17520, 17]) ``` 其中表示有: * 17,520 行:代表 17,520 個小時(730 天 × 24 小時) * 17 列:代表 17 個特性(變數) 那程式碼當中又接著一個 `bikes.stride()`,`stride()` 是步長的意思。而這邊輸出是 `(17, 1)`,就是表示: * 第一個維度(行)前進 1 步 → 在 storage 中前進 17 個位置(每行有 17 個數字) * 第二個維度(列)前進 1 步 → 在 storage 中前進 1 個位置。 什麼是 stride?在多維 tensor 中,要從一個元素跳到「同一維度的下一個元素」時,在底層的一維記憶體中需要跳過多少個數字。 ### 新增時間維度 接下來再新增時間維度。資料是一個長序列(17,520 小時),可把它按天去分組,這樣 NN 可以學習一天之內的模式(如早上 8 點通勤、晚上 6 點下班)。 要做的事情就是把形狀從 `(17520, 17)` 變成 `(730, 24, 17)`,也就是 730 天、24 小時、17 個變數特性。 在程式上用 `.view()` 去重塑 tensor。 ```python= daily_bikes = bikes.view(-1, 24, bikes.shape[1]) daily_bikes.shape, daily_bikes.stride() ``` 最後就得到了 `(torch.Size([730, 24, 17]), (408, 17, 1))`。 `.view()` 的 `-1` 參數表示自動計算這個維度應該是多少。PyTorch 會算出: $17,520 ÷ 24 = 730$ 這個數字。 而第二個參數 24 表示第二個維度固定 24,第三個參數為維度固定 17。 ### 調整維度順序 NN 通常所要的格式是 `(N, C, L)`: * N:樣本數量(Number of samples)= 730 天。 * C:通道數(Channels)= 17 個特性。 * L:序列長度(Length)= 24 小時。 但現在的順序是 `(N, L, C)` ,所以需要轉置(transpose)第 2 和第 3 維: 因此可寫下程式碼: ```python= daily_bikes = daily_bikes.transpose(1, 2) daily_bikes.shape, daily_bikes.stride() ``` Output: ``` (torch.Size([730, 17, 24]), (408, 1, 17)) ``` ### 準備訓練資料 這部分就是處理類別資料的問題了,在原本的 dataset 中有個 weathersit,表示天氣情況,他是一個序數變數(ordinal),有 4 個等級: * 1 = 好天氣 * 2 = 霧 * 3 = 小雨/小雪 * 4 = 大雨/大雪 可把它當作分類變數,用 One-Hot 編碼,也可以當作「連續變數」直接使用。而作者在這邊用 One-Hot encoding: ```python= # 假設只看第一天的資料 first_day = bikes[:24].long() # 取前 24 小時 weather_onehot = torch.zeros(first_day.shape[0], 4) # 24 小時 × 4 種天氣 # 把天氣狀況(第 9 列)轉成索引 first_day[:, 9] ``` Output: ``` tensor([1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2]) ``` 接下來做 One-Hot encoding: ```python= weather_onehot.scatter_( dim=1, # 在「列」方向散佈 index=first_day[:,9].unsqueeze(1).long() - 1, # 天氣索引 value=1.0) # 填入 1 ``` Output: ``` tensor([[1., 0., 0., 0.], [1., 0., 0., 0.], ..., [0., 1., 0., 0.], [0., 1., 0., 0.]]) ``` 再接下來呢,就是做拼接的動作,把他接回去原始資料,好讓 NN 能去處理的 tensor: ```python torch.cat((bikes[:24], weather_onehot), 1)[:1] ``` Output: ``` tensor([[ 1.0000, 1.0000, 1.0000, 0.0000, 1.0000, 0.0000, 0.0000, 6.0000, 0.0000, 1.0000, 0.2400, 0.2879, 0.8100, 0.0000, 3.0000, 13.0000, 16.0000, 1.0000, 0.0000, 0.0000, 0.0000]]) ``` 在原本範例的後面,在做的事情基本上跟這邊一樣,只是在最後的最後多了去做正規化的動作。 ## 用 tensor 表示文字 Deep Learning 在近年來於 NLP(Natural Language Processing)自然語言處理領域上有革命性的發展,當中有個 NN 叫做 RNNs(循環神經網路),應用於文本分類、分析、生成、自動翻譯等。 有個問題就是,NN 只能處理一堆數字,也就是 tensor,那要如何將文字轉成數字呢? 要處理文字共分兩個層級,一個是字元級別的,一個是單字級別的。 | 層級 | 處理單位 | 優點 | 缺點 | | -------- | -------- | -------- | -------- | | 字元級別(Character-level) | 每次處理一個字元 | 字元種類少(只有 26 個英文字母 + 標點符號) | 每個字元資訊量少 | | 單字級別(Word-level) | 每次處理一個單字 | 單字資訊量大 | 單詞數量過大(需要處理未見過的單字) | ### Character-level 的 One-Hot encoding 這本書作者所用的是 Jane Austen 的 "Pride and Prejudice" 當作範例。 該節的範例程式碼在 https://github.com/deep-learning-with-pytorch/dlwpt-code/blob/master/p1ch4/5_text_jane_austen.ipynb ```python= lines = text.split('\n') line = lines[200] line ``` 選一行文字做後續編碼,也就是那個熟悉的 One-Hot encoding。 ```python= letter_t = torch.zeros(len(line), 128) letter_t.shape ``` 把每個字元轉換成一個長度為 128 的向量(ASCII 有 128 個字元)。 輸出得到:`torch.Size([70, 128])`。 就是表示說這行文字 line 有 70 個字元,然後每個字元都用 128 維的向量表示。 ```python= for i, letter in enumerate(line.lower().strip()): letter_index = ord(letter) if ord(letter) < 128 else 0 # 取得 ASCII Code letter_t[i][letter_index] = 1 # 在對應位置填 1 ``` 在這邊就是在做字元級別的 One-Hot encoding 了。 ### Word-level 的 One-Hot encoding ```python= def clean_words(input_str): punctuation = '.,;:"!?”“_-' word_list = input_str.lower().replace('\n',' ').split() word_list = [word.strip(punctuation) for word in word_list] return word_list words_in_line = clean_words(line) line, words_in_line ``` 這邊在做的事是做資料預處理,先把那些特殊字元拿掉,只要看單字本身即可。 Output: ``` ('“Impossible, Mr. Bennet, impossible, when I am not acquainted with him', ['impossible', 'mr', 'bennet', 'impossible', 'when', 'i', 'am', 'not', 'acquainted', 'with', 'him']) ``` 第二步就是建立字典,然後單字對應索引: ```python= word_list = sorted(set(clean_words(text))) word2index_dict = {word: i for (i, word) in enumerate(word_list)} len(word2index_dict), word2index_dict['impossible'] ``` 首先第一行 `word_list = sorted(set(clean_words(text)))` 用到 set 資料結構,就是要用於去除重複這件事情,把重複的單字都給去除。 第二行就是建立一個字典。 最後的 Output 會 show 出:`(7261, 3394)`,表示這本書有 7261 個不重複的單字,"impossible" 這個單字的索引在 3394。 最後一步就是做 Word-level One-Hot encoding 啦: ```python= word_t = torch.zeros(len(words_in_line), len(word2index_dict)) for i, word in enumerate(words_in_line): word_index = word2index_dict[word] word_t[i][word_index] = 1 print('{:2} {:4} {}'.format(i, word_index, word)) print(word_t.shape) ``` ## Text embeddings One-Hot 在前面有介紹過他的缺點,就是當資料量一旦大起來的時候,維度就會爆一個大的,然後讓你訓練的時候不好過。 但其實還有一個問題,就是不能表示單字之間的相似性,例如 "apple" 跟 "orange" 都是水果,但在 One-Hot encoding 中他們之間的距離都很遠,因此無法捕捉到語意關係。 解決上述問題的技術因而誕生,就是詞嵌入(Word Embeddings),或稱詞向量(Word vector)。 這主要是用一個低維度的浮點數向量(如 100 維)表示每個單字,並且語義相似的單字在這個空間中距離很近。 舉例: ``` One-Hot 編碼: apple → [0, 0, ..., 1, 0, ..., 0] (7261 維) orange → [0, 0, ..., 0, 1, ..., 0] (7261 維) Word Embedding: apple → [0.8, 0.2, -0.5, ..., 0.3] (100 維) orange → [0.75, 0.25, -0.4, ..., 0.35] (100 維) dog → [-0.1, 0.9, 0.6, ..., -0.2] (100 維) ``` One-Hot 不是 0 就是 1,之間的數字不連續,因此沒有辦法判斷的比較精準,相反 Word Embedding 就可以。可以發現 apple 跟 orange 的數值很相近,但兩者與 dog 的數值完全找不到任何關係。 ![image](https://hackmd.io/_uploads/HybHvSyWbx.png) Image Source:《Deep Learning with PyTorch》Page 100. 書中用了二維的空間去表示單字之間的遠近關係。 而向量之間的運算可以做到接近某個單字的向量,也就是說可以做一個類比: ```python= apple_vector = [0.1, 0.1] red_adjustment = [0, -0.1] yellow_adjustment = [0, 0.5] result = apple_vector + red_adjustment + yellow_adjustment # result ≈ [0.1, 0.5],接近 lemon 的 [0.2, 0.5] ``` `apple_vector + red_adjustment + yellow_adjustment` 運算完的結果能類比成 lemon。 ## 總結 ### 彩圖的 tensor 表示 一張 RGB 彩色照片可用三維 tensors 表示: * 3 個通道(RGB) * 寬 × 高像素矩陣 若影像為 800×600,則 tensor 形狀為 `(3, 800, 600)`(CHW 格式)。tensor 之所以使用 CHW,是因為卷積運算在此格式下較高效。 使用 `transforms.PILToTensor()` 可將 PIL Image 轉為此格式,並自動從 HWC → CHW。 tensor 內數值為 0~255 的 uint8,代表像素的顏色強度。 ### 正規化(Normalization) 訓練模型時需將像素值調整到更適合神經網路學習的範圍: 1. 簡單正規化到 `[0, 1]` - 將 tensor 轉為 float,再除以 255。 2. 使用 `transforms.ToTensor()` - 自動完成:HWC 轉 CHW,並除以 255。 3. 進階正規化到 `[-1, 1]` - 使用 `transforms.Normalize(mean=[0.5,...], std=[0.5,...])`。 ### 表格資料轉換為張量 以 pandas 讀取 CSV 後: * 前 n 欄 → 特徵(input) * 最後一欄 → 標籤(output) 如資料共有 5 列 4 特徵: * 特徵張量形狀 → (5, 4) * 標籤張量 → (5, ) 這些 Tensor 可使用 TensorDataset 與 DataLoader 打包,以便批次訓練模型。 ## 參考資料 《Deep Learning with PyTorch》第四章 Real-world data representation using tensors