# 410921216賴柏諺_深度學習作業二
## 零. 程式碼:
:::spoiler 程式碼 (將資料依照標籤分類)
```PYTHON
#處理資料
import os
import csv
import shutil
# 設定圖片和 CSV 檔案的路徑
image_dir = "./music_train"
csv_file = "./train_truth.csv"
# 逐行讀取 CSV 檔案
with open(csv_file, 'r') as file: #'r': 讀取模式
reader = csv.reader(file)
next(reader) # 跳過標題行
for row in tqdm(reader):
# 讀取檔案名稱和標籤
filename, label = row
# 將標籤轉換成整數
label = int(label)
# 確定標籤資料夾是否存在,若不存在就創建一個
label_dir = os.path.join(image_dir, str(label))
if not os.path.exists(label_dir):
os.makedirs(label_dir)
# 複製圖片到對應的標籤資料夾中
src_path = os.path.join(image_dir, filename)
dst_path = os.path.join(label_dir, filename)
shutil.copy(src_path, dst_path)
```
:::
:::spoiler 程式碼 (實作)
```PYTHON
import torch #PyTorch
import torch.nn as nn #神經網路
import torch.optim as optim #優化器
from torch.utils.data import random_split #數據集切割
import torchvision.datasets as datasets #數據集
import torchvision.transforms as transforms #圖片轉換
from torch.utils.data import DataLoader #數據加載器
import pandas as pd #處理CSV檔案
from tqdm.auto import tqdm #進度條
import os #處理資料
from PIL import Image #圖片處理
# 定義數據轉換
data_transform = transforms.Compose([
transforms.Resize((224, 224)), #將圖片轉換成224*224
transforms.ToTensor(), #將圖片轉換成Tensor
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) #數據標準化
])
# 加載數據集
dataset = datasets.ImageFolder('./music_classified', transform=data_transform)
# 將dataset切割成train和test
train_size = int(0.85 * len(dataset)) #訓練集佔85%
test_size = len(dataset) - train_size #測試集佔15%
train_dataset, test_dataset = random_split(dataset, [train_size, test_size])
# 定義數據加載器
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) #BATCH大小32,打亂數據
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=True) #BATCH大小32,打亂數據
# 定義模型
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.fc1 = nn.Linear(128 * 28 * 28, 128)
self.fc2 = nn.Linear(128, 88)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
def forward(self, x):
x = self.pool(nn.functional.relu(self.conv1(x)))
x = self.pool(nn.functional.relu(self.conv2(x)))
x = self.pool(nn.functional.relu(self.conv3(x)))
x = x.view(-1, 128 * 28 * 28)
x = nn.functional.relu(self.fc1(x))
x = self.fc2(x)
return x
# 初始化模型和損失函數
model = Net()
criterion = nn.CrossEntropyLoss()
# 配置GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')
model = model.to(device)
criterion = criterion.to(device)
# 定義優化器
optimizer = optim.AdamW(model.parameters())
# 訓練模型
model.train() #模型設為訓練模式
num_epochs = 10 #訓練10個epoch
for epoch in tqdm(range(num_epochs)):
running_loss = 0.0
for i, (inputs, labels) in enumerate(train_loader): #每個batch的loss
inputs, labels = inputs.to(device), labels.to(device) #將資料放入GPU
optimizer.zero_grad() #梯度歸零
outputs = model(inputs) #跑模型
loss = criterion(outputs, labels) #計算loss
loss.backward() #反向傳播
optimizer.step() #更新參數
running_loss += loss.item() #計算每個epoch的loss
epoch_loss = running_loss / len(train_loader) #計算每個epoch的loss
print('Epoch {} loss: {:.4f}'.format(epoch+1, epoch_loss))
# 評估模型
model.eval() #模型設為評估模式
with torch.no_grad(): #不計算梯度(純預測)
correct, total = 0, 0
for images, labels in test_loader: #每個batch的準確率
images = images.to(device) #將資料放入GPU
labels = labels.to(device) #將資料放入GPU
outputs = model(images) #跑模型
_, predicted = torch.max(outputs.data, 1) #取得預測結果
total += labels.size(0) #計算總數
correct += (predicted == labels).sum().item() #計算正確數
# 計算測試集的準確率
accuracy = 100 * correct / total
print('Test accuracy: {:.2f}%'.format(accuracy))
#加載music_test資料夾
exam_dir = './music_test'
exam_imgs = os.listdir(exam_dir)
#預測music_test的label,並將結果存入results
model.eval() #模型設為評估模式
results = [] #存放結果
with torch.no_grad(): #不計算梯度(純預測)
for img_name in exam_imgs: #每張圖片的預測結果
img_path = os.path.join(exam_dir, img_name) #圖片路徑
img = Image.open(img_path).convert('RGB') #讀取圖片
inputs = data_transform(img).unsqueeze(0).to(device)
outputs = model(inputs) #跑模型
_, predicted = torch.max(outputs.data, 1) #取得預測結果
predicted = predicted.cpu().numpy().tolist() #轉成list
results.append([img_name[:], dataset.classes[predicted[0]]]) #將結果存入results
#將結果存入ans.csv
df = pd.DataFrame(results, columns=['filename', 'category'])
df.to_csv('ans.csv', index=False)
```
:::
## 一. 遇到的問題:
1. 資料龐大,不好放到COLAB
2. 為訓練圖片分配標籤
3. 資料預處理
## 二. 怎麼解決問題:
1. 架設環境是本次作業在開始做之前的一大難題,上次的作業只要上傳csv即可,但這次的作業有大量圖片要上傳,而且實作之前不知道程式會跑多久,萬一跑到一半colab壞掉還要重傳圖片,很麻煩,所以這次的作業完全是在本機上的vscode實作完成。
首先第一步就是安裝pytorch,由於見識過gpu的美好,所以直接走困難路線,安裝gpu版本。在看了網路上不少的教學,默默地註冊cudnn,安裝cuda 11.8,核對python版本後重裝3.10.10,一切都裝好後實際運行,順跑。
但接下來嚴重的問題是pip裝不了東西,嘗試了網路上的方法也都會跳出錯誤訊息,不斷地將錯誤訊息一遍又一遍的查詢網路,是座很難跨過的高牆,最後只好聽室友的意見,用conda去安裝。
用conda安裝花的時間比我整晚查資料問網路還少的多了,簡單打幾條指令,等它跑完就好,最後成功在電腦上裝好gpu版本的pytorch。
2. 為訓練資料分配標籤,也就是人工標註的部分,也是一大難題,一開始在想要不要乾脆邊讀csv邊標註就好了,但感覺直接分類進對應標籤名的資料夾感覺更快。
問題是2000多張,用人工一張一張分類感覺又不切實際,所以打算用程式來解決這個問題,這時候chatgpt就幫了我很多忙。
我請它幫我寫一個簡易的架構,我再手動更改檔案位置,並且學習一些我不會用的語法,加上我的註解,最後成功將其分類。
3. 對於如何將圖片轉換為pytorch的tensor,又請來gpt大神來為我解惑,於是得到了以下的程式碼:
```python
data_transform = transforms.Compose([
transforms.Resize((224, 224)), #將圖片轉換成224*224
transforms.ToTensor(), #將圖片轉換成Tensor
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) #數據標準化
])
```
再看完程式碼後,前兩行都看得懂,也自己加上了註解,但最後一行normalize後面那一串就不知道在幹嘛了。
通過又一次的詢問,得知mean與std分別是指在通道(R, G, B)上的平均值與標準差,通過將像素值減去均值,然後除以標準差,可以將數據轉換為以零為中心的分佈,有助於模型更好地學習特徵並提高訓練的穩定性。
而數據標準化是使用imagenet數據集中的平均值與標準差,由於本次圖片並非特殊或手繪圖片,所以適合使用這組數據,根據先人嘗試的結果,可以使一般的圖像類模型訓練效果良好。
## 三. 模型設計思路(個人創意):
1. 我只使用三通道(rgb),三層捲基層(conv),兩個全連階層(fc)。捲機大小的分配採逐步放大,輸出到到全連接層再轉小,使用128因為大於88且又是2的次方。
2. kernel size就依上課聽過的選擇3*3,機活函數選用reRU。
3. 為了防止特徵地圖越捲越小,使用padding的技術在每次卷完後在外圍填1圈。
4. 為了減少計算量,將移動步數設為2(stride=2),相較一次動一步減少了約4倍的計算量(H*K) --> (H/2 * K/2) = 1/4(H*K)。
## 四. 學到了什麼:
1. 資料處理的方法:
這次的作業是初次實作cnn網路,初次學習如何處理圖片檔案,更重要的是學習對資料夾做讀寫,並使用with正確釋放資源。
對於圖像的預處理,知道要如何將其轉換為tensor的資料格式,並且使用對一般圖像預訓練過的標準化參數,讓訓練的模型表現更好。
2. CNN實作:
這次的網路實作用了很多cnn常用的模型設計,與許多課堂教受的技術,像是pooling和padding,實際跑起來速度是蠻快的,一開始把epoch調到100,結果跑到epoch 5時,loss就停在0.0001了(我有設定顯示到第4位)到第10步loss就0.0000了,於是馬上暫停,改為訓練10個epoch,最後結果還不錯。