---
# System prepended metadata

title: 410921216賴柏諺_深度學習作業二

---

# 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，最後結果還不錯。
