부스트캠프 3주차 학습정리

1. Cumtom Model

1) einsum

파이토치, 넘파이, 텐서플로에서 제공하는 수학 연산자들의 표기법이 헷갈릴 때, einsum을 이용하면 오히려 더 복잡한 연산도 쉽게 할 수 있습니다.

기본적으로 많은 Einsum Framework는 다음과 같은 함수 형태를 갖습니다.

Result = einsum("dimension notation of A, dimension notation of B,...-> Result Dimension", A, B, ...)

임의의 갯수의 Tensor를 입력으로 받아, 임의의 차원을 갖는 Tensor를 결과로 돌려주는 방법을 말합니다.

# Transpose
torch.einsum("ij->ji", A)

# diag
torch.einsum('ii->i', A)

# trace
torch.einsum('ii->', A)

# sum
torch.einsum("ij->", A)

# row sum
torch.einsum("ij->i", A)

# dot product
torch.einsum('i,i->', x, y )

# elementwise-vec
torch.einsum('i,i->i', x, y)

# elementwise-mat
torch.einsum('ij,ij->', A, B)

# matrix-vec multiplication
torch.einsum('ij,j->i', A, x)

# mat-mat multiplication
torch.einsum('ik,kj->ij', A, B)

# batched matrix multiplication
torch.einsum('bik,bkj->bij',A, B)

2) Buffer

일반적인 Tensor는 Parameter와 다르게 gradient를 계산하지 않아 값도 업데이트 되지 않고, 모델에 저장도 되지 않습니다. 하지만 값이 업데이트 되지 않는다 해도 저장하고 싶은 tensor가 있을 수도 있습니다. 그럴때는 buffer로 tensor를 등록해주면 됩니다.

용도  

네트워크를 구성함에 있어서 네트워크를 end2end로 학습시키고 싶은데 중간에 `업데이트를 하지않는 일반 layer`를 넣고 싶을 때 사용할 수 있다
- "Tensor"
    - ❌ gradient 계산
    - ❌ 값 업데이트
    - ❌ 모델 저장시 값 저장
- "Parameter"
    - ✅ gradient 계산
    - ✅ 값 업데이트
    - ✅ 모델 저장시 값 저장
- "Buffer"
    - ❌ gradient 계산
    - ❌ 값 업데이트
    - ✅ 모델 저장시 값 저장
class Model(nn.Module):
    def __init__(self):
        super().__init__()

        self.parameter = Parameter(torch.Tensor([7]))
        self.tensor = torch.Tensor([7])
        self.register_buffer('buffer', self.tensor)

3) torchsummary

from torchsummary import summary

# GPU를 사용하는 경우
cnn_model = CNN().cuda()
summary(cnn_model, input_size=(channels, H, W))

# CPU를 사용하는 경우
cnn_model = CNN()
summary(cnn_model, input_size=(channels, H, W), device='cpu')

4) hook

hook은 패키지화된 코드에서 다른 프로그래머가 custom 코드를 중간에 실행시킬 수 있도록 만들어놓은 인터페이스입니다.

용도  

- 프로그램의 실행 로직을 분석하거나
- 프로그램에 추가적인 기능을 제공하고 싶을 때

- gradient값의 변화를 시각화
- gradient값이 특정 임계값을 넘으면 gradient exploding 경고 알림
- 특정 tensor의 gradient값이 너무 커지거나 작아지는 현상이 관측되면 해당 tensor 한정으로 gradient clipping
Pytorch의 hook

- Tensor에 적용하는 hook
- Module에 적용하는 hook
Tensor에 적용가능한 hook

# 텐서에는 backward hook만 적용가능

- register_hook
def tensor_hook(grad):
    answer.append(grad)

model.W.register_hook(tensor_hook)
Module에 적용가능한 hook

- register_forward_pre_hook
- register_forward_hook
- register_backward_hook
- register_full_backward_hook
def module_hook(module, grad_input, grad_output):
    answer.extend([grad_input[0], grad_input[1], grad_output[0]])
    

model.register_full_backward_hook(module_hook)

5) apply

apply를 통해 적용하는 함수는 module을 입력으로 받습니다. 모델의 모든 module들을 순차적으로 입력받아서 처리합니다. apply 함수는 일반적으로 가중치 초기화(Weight Initialization)에 많이 사용된다고 합니다. Parameter로 지정한 tensor의 값을 원하는 값으로 지정해주는 것을 의미합니다.

import torch
from torch import nn

@torch.no_grad()
# 입력 m에 순차적으로 받은 net의 모듈들 중에 그 모듈의 type이 Linear이면 그 모듈의 weight를 1로 초기화한다,,? 이거 맞나?
def init_weights(m):
    print(m)
    print(type(m))
    print("-"*50)
    if type(m) == nn.Linear:
        m.weight.fill_(1.0)
        print(m.weight)
        print("-"*100)
        

net = nn.Sequential(nn.Linear(2, 2), nn.Linear(2, 2))
# 내 생각은 이렇게 하면 net에 있는 모든 모듈들이 init_weights(m)의 m으로 들어가는 것 같다
net.apply(init_weights)
---------------------------------------------------------------------------

Linear(in_features=2, out_features=2, bias=True)
<class 'torch.nn.modules.linear.Linear'>
--------------------------------------------------
Parameter containing:
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)
----------------------------------------------------------------------------------------------------
Linear(in_features=2, out_features=2, bias=True)
<class 'torch.nn.modules.linear.Linear'>
--------------------------------------------------
Parameter containing:
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)
----------------------------------------------------------------------------------------------------
Sequential(
  (0): Linear(in_features=2, out_features=2, bias=True)
  (1): Linear(in_features=2, out_features=2, bias=True)
)
<class 'torch.nn.modules.container.Sequential'>
--------------------------------------------------
Sequential(
  (0): Linear(in_features=2, out_features=2, bias=True)
  (1): Linear(in_features=2, out_features=2, bias=True)
)
model = Model()

def weight_initialization(module):
    module_name = module.__class__.__name__ 
    
    if module_name.startswith("F"):
        module._parameters['W'] = torch.tensor(1, dtype=torch.float ,requires_grad=True)
        print(module._parameters)

# apply는 apply가 적용된 module을 return 해줘요
model.apply(weight_initialization)

2. Dataset, DataLoader

- torch.utils.data: 데이터셋의 표준을 정의하고 데이터셋을 불러오고 자르고 섞는데 쓰는 도구들이 들어있는 모듈입니다. 
- torchvision.dataset: torch.utils.data.Dataset을 상속하는 이미지 데이터셋의 모음입니다. MNIST나 CIFAR-10과 같은 데이터셋을 제공해줍니다.
- torchtext.dataset: torch.utils.data.Dataset을 상속하는 텍스트 데이터셋의 모음입니다. 기본적으로 IMDb나 AG_NEWS와 같은 데이터셋을 제공해줍니다.
- torchvision.transforms: 이미지 데이터셋에 쓸 수 있는 여러 가지 변환 필터를 담고 있는 모듈입니다. 
- torchvision.utils: 이미지 데이터를 저장하고 시각화하기 위한 도구가 들어있는 모듈입니다.

1) Dataset

Dataset 클래스는 크게 아래와 같이 3가지 메서드로 구성됩니다. __init__ 메서드와 __len__ 메서드와 마지막으로 __getitem__ 메서드로 구성됩니다.

__init__ 메서드
일반적으로 해당 메서드에서는 데이터의 위치나 파일명과 같은 초기화 작업을 위해 동작합니다. 일반적으로 CSV파일이나 XML파일과 같은 데이터를 이때 불러옵니다. 이렇게 함으로써 모든 데이터를 메모리에 로드하지 않고 효율적으로 사용할 수 있습니다. 여기에 이미지를 처리할 transforms들을 Compose해서 정의해둡니다.

__len__ 메서드
해당 메서드는 Dataset의 최대 요소 수를 반환하는데 사용됩니다. 해당 메서드를 통해서 현재 불러오는 데이터의 인덱스가 적절한 범위 안에 있는지 확인할 수 있습니다.

__getitem__ 메서드
해당 메서드는 데이터셋의 idx번째 데이터를 반환하는데 사용됩니다. 일반적으로 원본 데이터를 가져와서 전처리하고 데이터 증강하는 부분이 모두 여기에서 진행될 겁니다. 이는 이후 transform 하는 방법들에 대해서 간단히 알려드리겠습니다.
class IrisDataset(Dataset):
    def __init__(self):
        iris = load_iris()
        self.X = torch.tensor(iris['data'])
        self.y = torch.tensor(iris['target'])
        self.feature_names = iris['feature_names']
        
    def __len__(self):
                return len(self.y)

    def __getitem__(self, idx):
        X, y = self.X[idx], self.y[idx]
                return X, y

2) DataLoader

Dataloader는 모델 학습을 위해서 데이터를 미니 배치(Mini batch)단위로 제공해주는 역할을 합니다

DataLoader(dataset, batch_size=1, shuffle=False, sampler=None,
           batch_sampler=None, num_workers=0, collate_fn=None,
           pin_memory=False, drop_last=False, timeout=0,
           worker_init_fn=None)

collate_fn

입력 데이터의 시퀀스가 다른 경우 배치로 입력하게 되면 오류가 납니다. 이 때 입력 데이터의 시퀀스를 맞추기 위해 패딩을 할 수 있는데 이 때 collate_fn인자를 사용하여 해결할 수 있습니다. 데이터가 로더에 의해 return 되기 전에 collate_fn을 거쳐서 그 결과가 return됩니다.

def my_collate_fn(batch):
    collate_X = []
    collate_y = []
    # 각 배치마다 인풋의 최대 길이 구하기
    max_len = 0
    for d in batch:
        if torch.numel(d['X']) > max_len:
            max_len = torch.numel(d['X'])
    # 하나의 배치에 있는 인풋마다 패딩이 필요한 길이 pad_len을 구한 후 d['X']에 concatenate해주기
    for d in batch:
        pad_len = max_len - torch.numel(d['X'])
        zero_pad = torch.zeros(pad_len)
        padded_data = torch.cat((d['X'], zero_pad))
        collate_X.append(padded_data)
        collate_y.append(d['y'])
        
    return {'X': torch.stack(collate_X),
             'y': torch.stack(collate_y)}
dataloader_example = torch.utils.data.DataLoader(dataset_example, 
                                                 batch_size=2,
                                                 collate_fn=my_collate_fn)
for d in dataloader_example:
    print(d['X'], d['y'])

-----------------------------------------------------------------------------
tensor([[0., 0.],
        [1., 1.]]) tensor([0., 1.])
tensor([[2., 2., 2., 0.],
        [3., 3., 3., 3.]]) tensor([2., 3.])
tensor([[4., 4., 4., 4., 4., 0.],
        [5., 5., 5., 5., 5., 5.]]) tensor([4., 5.])
tensor([[6., 6., 6., 6., 6., 6., 6., 0.],
        [7., 7., 7., 7., 7., 7., 7., 7.]]) tensor([6., 7.])
tensor([[8., 8., 8., 8., 8., 8., 8., 8., 8., 0.],
        [9., 9., 9., 9., 9., 9., 9., 9., 9., 9.]]) tensor([8., 9.])

3) transform

url = 'https://images.unsplash.com/photo-1583160247711-2191776b4b91?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTN8fGdvbGRlbnJldHJpZXZlcnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60'
im = Image.open(requests.get(url, stream=True).raw)

def get_transforms_img(im):
    transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.RandomHorizontalFlip(p=1),
        transforms.CenterCrop((150, 150))]
    )

    im = transform(im)
    return im

get_transforms_img(im)

이런 transform 객체는 custom Dataset에서 설정할 수 있다 다음의 예시는 파이토치 공식문서에서 가져왔습니다.

class FaceLandmarksDataset(Dataset):
    """Face Landmarks dataset."""

    def __init__(self, csv_file, root_dir, transform=None):
        """
        Args:
            csv_file (string): Path to the csv file with annotations.
            root_dir (string): Directory with all the images.
            transform (callable, optional): Optional transform to be applied
                on a sample.
        """
        self.landmarks_frame = pd.read_csv(csv_file)
        self.root_dir = root_dir
        self.transform = transform

    def __len__(self):
        return len(self.landmarks_frame)

    def __getitem__(self, idx):
        if torch.is_tensor(idx):
            idx = idx.tolist()

        img_name = os.path.join(self.root_dir,
                                self.landmarks_frame.iloc[idx, 0])
        image = io.imread(img_name)
        landmarks = self.landmarks_frame.iloc[idx, 1:]
        landmarks = np.array([landmarks])
        landmarks = landmarks.astype('float').reshape(-1, 2)
        sample = {'image': image, 'landmarks': landmarks}

        if self.transform:
            sample = self.transform(sample)

        return sample
transformed_dataset = FaceLandmarksDataset(csv_file='data/faces/face_landmarks.csv',
                                           root_dir='data/faces/',
                                           transform=transforms.Compose([
                                               Rescale(256),
                                               RandomCrop(224),
                                               ToTensor()
                                           ]))

Tags:

Categories:

Updated: