From b40425ae5d01f9ab1fd056335539f495d602aef4 Mon Sep 17 00:00:00 2001 From: SANGDONKIM Date: Tue, 24 Dec 2024 04:06:13 +0900 Subject: [PATCH 1/2] update --- 00-install-torch.Rmd | 77 +++++++ 00-install-torch.qmd | 77 +++++++ 01-intro-to-tensor.qmd | 122 +++++++++++ 02-tensor-calculation.qmd | 174 +++++++++++++++ 03-transferdevice.qmd | 81 +++++++ 04-class-and-tensor.qmd | 172 +++++++++++++++ 05-forward-propagation.qmd | 206 ++++++++++++++++++ 06-autograd.qmd | 273 ++++++++++++++++++++++++ 07-myfirst-nn.Rmd | 4 +- 07-myfirst-nn.qmd | 146 +++++++++++++ 08-train-mynn.qmd | 207 ++++++++++++++++++ 09-dataloader.qmd | 419 +++++++++++++++++++++++++++++++++++++ 12 files changed, 1955 insertions(+), 3 deletions(-) create mode 100644 00-install-torch.Rmd create mode 100644 00-install-torch.qmd create mode 100644 01-intro-to-tensor.qmd create mode 100644 02-tensor-calculation.qmd create mode 100644 03-transferdevice.qmd create mode 100644 04-class-and-tensor.qmd create mode 100644 05-forward-propagation.qmd create mode 100644 06-autograd.qmd create mode 100644 07-myfirst-nn.qmd create mode 100644 08-train-mynn.qmd create mode 100644 09-dataloader.qmd diff --git a/00-install-torch.Rmd b/00-install-torch.Rmd new file mode 100644 index 0000000..b0d3a1c --- /dev/null +++ b/00-install-torch.Rmd @@ -0,0 +1,77 @@ +# 파이토치 설치하기 + +```{r, echo=FALSE, message=FALSE, warning=FALSE, include=FALSE} +Sys.setenv(RETICULATE_PYTHON = "/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python") +library(reticulate) +use_python("/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python", required = T) +``` + +파이토치(PyTorch)는 Python 환경에서 설치할 수 있다. 공식 웹사이트에서 제공하는 명령어를 참고하면 쉽게 설치할 수 있다. + +1. 파이토치 공식 설치 명령어 찾기 + +파이토치 공식 웹사이트에서 운영 체제와 컴퓨팅 환경에 맞는 명령어를 선택하면 된다. [파이토치홈페이지](https://pytorch.org/get-started/locally/)로 가서 아래 항목을 선택해보자. + +- PyTorch 빌드: Stable (안정된 버전) 또는 Nightly (개발 버전) + +- 운영 체제: Windows, macOS, Linux + +- 패키지 관리자: pip 또는 conda + +- 컴퓨팅 플랫폼: CPU 또는 CUDA(GPU 버전) + +2. 설치 명령어 예제 + + + +(1) CPU만 사용하는 경우 + +Python과 pip를 사용한다면: + +```{python} +pip install torch torchvision torchaudio +``` + +conda를 사용한다면: + +```{python} +conda install pytorch torchvision torchaudio cpuonly -c pytorch +``` + +(2) GPU(CUDA) 사용하는 경우 + +CUDA 11.8을 사용하는 GPU 환경이라면: + +pip install torch torchvision torchaudio --index-url + +CUDA 11.7을 사용하는 경우: + +pip install torch torchvision torchaudio --index-url + +conda를 사용하려면: + +conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia + +3. 설치 확인 + +설치가 제대로 되었는지 확인하려면 아래 코드를 실행해보자. + +```{python} +import torch +print(torch.__version__) # PyTorch 버전을 출력 +print(torch.cuda.is_available()) # GPU 사용 가능 여부 확인 +``` + +torch.cuda.is_available()가 True라면 GPU 설정까지 완료된 것이다. + +4. 문제 해결 + +설치 중 문제가 생기면 다음을 확인해보자. + +- Python 버전은 3.9 이상이어야 한다. + +- GPU 드라이버가 최신 버전인지 확인해야 한다. + +- CUDA Toolkit 설치가 필요하다면 CUDA Toolkit 다운로드로 가서 설치한다. + +특정 버전을 설치하려면 아래 명령어를 사용하면 된다. diff --git a/00-install-torch.qmd b/00-install-torch.qmd new file mode 100644 index 0000000..b0d3a1c --- /dev/null +++ b/00-install-torch.qmd @@ -0,0 +1,77 @@ +# 파이토치 설치하기 + +```{r, echo=FALSE, message=FALSE, warning=FALSE, include=FALSE} +Sys.setenv(RETICULATE_PYTHON = "/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python") +library(reticulate) +use_python("/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python", required = T) +``` + +파이토치(PyTorch)는 Python 환경에서 설치할 수 있다. 공식 웹사이트에서 제공하는 명령어를 참고하면 쉽게 설치할 수 있다. + +1. 파이토치 공식 설치 명령어 찾기 + +파이토치 공식 웹사이트에서 운영 체제와 컴퓨팅 환경에 맞는 명령어를 선택하면 된다. [파이토치홈페이지](https://pytorch.org/get-started/locally/)로 가서 아래 항목을 선택해보자. + +- PyTorch 빌드: Stable (안정된 버전) 또는 Nightly (개발 버전) + +- 운영 체제: Windows, macOS, Linux + +- 패키지 관리자: pip 또는 conda + +- 컴퓨팅 플랫폼: CPU 또는 CUDA(GPU 버전) + +2. 설치 명령어 예제 + + + +(1) CPU만 사용하는 경우 + +Python과 pip를 사용한다면: + +```{python} +pip install torch torchvision torchaudio +``` + +conda를 사용한다면: + +```{python} +conda install pytorch torchvision torchaudio cpuonly -c pytorch +``` + +(2) GPU(CUDA) 사용하는 경우 + +CUDA 11.8을 사용하는 GPU 환경이라면: + +pip install torch torchvision torchaudio --index-url + +CUDA 11.7을 사용하는 경우: + +pip install torch torchvision torchaudio --index-url + +conda를 사용하려면: + +conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia + +3. 설치 확인 + +설치가 제대로 되었는지 확인하려면 아래 코드를 실행해보자. + +```{python} +import torch +print(torch.__version__) # PyTorch 버전을 출력 +print(torch.cuda.is_available()) # GPU 사용 가능 여부 확인 +``` + +torch.cuda.is_available()가 True라면 GPU 설정까지 완료된 것이다. + +4. 문제 해결 + +설치 중 문제가 생기면 다음을 확인해보자. + +- Python 버전은 3.9 이상이어야 한다. + +- GPU 드라이버가 최신 버전인지 확인해야 한다. + +- CUDA Toolkit 설치가 필요하다면 CUDA Toolkit 다운로드로 가서 설치한다. + +특정 버전을 설치하려면 아래 명령어를 사용하면 된다. diff --git a/01-intro-to-tensor.qmd b/01-intro-to-tensor.qmd new file mode 100644 index 0000000..8102714 --- /dev/null +++ b/01-intro-to-tensor.qmd @@ -0,0 +1,122 @@ +# 딥러닝 첫걸음, 텐서 (tensor) 만들기 {#intro} + +```{r, echo=FALSE, message=FALSE, warning=FALSE, include=FALSE} +Sys.setenv(RETICULATE_PYTHON = "/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python") +library(reticulate) +use_python("/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python", required = T) +``` + +## torch와의 첫 만남 + +PyTorch를 설치했으니, 한번 만나보자. 아래 명령어로 torch를 불러온다. + +```{python} +import torch +``` + + +## 텐서(Tensor) 만들기 + +텐서는 다차원 배열이다. 우리가 많이 사용하는 행렬(matrix)의 개념을 확장한 것이다. Python의 numpy 배열과 비슷하지만, GPU 계산이 가능하다는 점에서 차별화된다. + +## 빈 텐서 만들기 + +5행 3열의 빈 텐서를 선언한다. 빈 텐서는 초기화되지 않은 임의의 값으로 채워진다. + +```{python} +x = torch.empty(5, 3) +# 텐서 x값 확인 +print(x) +# 텐서 x의 크기 확인 +print(x.size()) +``` + +empty 텐서는 초기화되지 않은 값이 채워진다. 이후 값이 정의되기 전에는 신뢰할 수 없는 데이터가 포함되어 있으니 주의하자. + +### 랜덤 텐서 + +0과 1 사이의 난수로 채워진 텐서를 선언한다. + +```{python} +rand_tensor = torch.rand(5, 3) +print(rand_tensor) +``` + +Python에서는 리스트와 비슷한 문법을 사용해 텐서에 접근할 수 있다. + +```{python} +print(rand_tensor[:, 1]) # 두 번째 열 +print(rand_tensor[:3, :]) # 첫 3행 +print(rand_tensor[2:4, [0, 2]]) # 3~4행의 1, 3열 +``` + + +### 단위 텐서 + +4행 4열의 단위 텐서를 선언한다. + +```{python} +x = torch.eye(4) +print(x) +``` + + +### 영(0) 텐서 + +모든 값이 0으로 채워진 3행 5열 텐서를 선언한다. + +```{python} +x = torch.zeros(3, 5) +print(x) +``` + +## 고급 기술: 영리하게 만들기 + +지금까지는 초기화 함수들로 텐서를 선언했지만, 직접 값을 지정해 텐서를 선언할 수도 있다. + +### 텐서 직접 선언 + +리스트 또는 2D 배열로 텐서를 만들 수 있다. + +```{python} +y = torch.tensor([[1, 2], [3, 4], [5, 6]]) +print(y) +``` + +### range 함수 사용 + +Python의 range를 사용해 텐서를 선언해보자. + +```{python} +y = torch.tensor([i for i in range(1, 7)]).reshape(3, 2) +print(y) +``` + +### torch.linspace 함수 사용 + +torch.linspace를 사용하면 특정 범위의 값을 지정해 텐서를 만들 수 있다. + +```{python} +y = torch.linspace(0.1, 1, steps=10).reshape(5, 2) +print(y) +``` + +## 텐서와 행렬은 같을까? + +PyTorch의 텐서와 Numpy의 행렬은 비슷하지만 동일하지 않다. PyTorch 텐서는 GPU 연산에 최적화되어 있다. 다만 단순한 행렬곱 연산자 \@와 토치 연산자인 torch.matmul은 같은 결과를 도출한다. + +```{python} +x = torch.zeros(3, 5) + +# 행렬 곱 +result = x @ x.T +print(result) + +# 텐서 연산을 위해 torch.matmul() 사용 +result = torch.matmul(x, x.T) +print(result) +``` + +## 텐서를 다룰 때 주의사항 + +PyTorch 텐서의 인덱싱은 Python 리스트와 동일하게 0부터 시작한다. R과는 다르니 주의하자. 다음 장에서는 텐서의 연산에 대해 더 자세히 다뤄보자. diff --git a/02-tensor-calculation.qmd b/02-tensor-calculation.qmd new file mode 100644 index 0000000..61f6601 --- /dev/null +++ b/02-tensor-calculation.qmd @@ -0,0 +1,174 @@ +# 텐서(Tensor) 연산 {#operation} + +```{r, echo=FALSE, message=FALSE, warning=FALSE, include=FALSE} +Sys.setenv(RETICULATE_PYTHON = "/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python") +library(reticulate) +use_python("/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python", required = T) +``` + +지난 챕터에서 텐서가 Python의 일반 배열과는 다르며, 텐서 연산에서 주의해야 할 점이 있다는 것을 배웠다. 이번 챕터에서는 텐서의 다양한 연산에 대해 알아보자. + +## PyTorch 불러오기 및 준비 + +PyTorch를 불러오고, 이번 챕터에서 사용할 텐서 A, B, C를 준비한다. 난수 고정을 통해 재현 가능성을 보장한다. + +```{python} +import torch + +# 난수 생성 시드 고정 +torch.manual_seed(2021) +``` + +```{python} +# 텐서 정의 +A = torch.tensor([1, 2, 3, 4, 5, 6]) +B = torch.rand(2, 3) +C = torch.rand(2, 3, 2) + +print(A) +print(B) +print(C) +``` + +## 텐서의 속성 확인 + +PyTorch 텐서는 자료형과 모양(Shape) 정보를 제공한다. 이를 통해 텐서의 구조를 확인할 수 있다. + +```{python} +print(A.dtype) # 자료형 확인 +print(B.dtype) +print(A.shape) # 모양(Shape) 확인 +print(B.shape) +``` + +## 텐서의 연산 + +### 자료형 변환 + +A는 현재 정수형 텐서이다. 이를 실수형 텐서로 변환해보자. + +```{python} +A = A.to(dtype=torch.float64) # 자료형 변환 +print(A) +``` + +### 모양(Shape) 변환 + +현재 A와 B는 자료형이 같지만 모양이 달라 더할 수 없다. 모양을 맞추기 위해 view()를 사용한다. + +```{python} +A = A.view(2, 3) # 2행 3열로 변환 +print(A) +``` + +특정 차원을 -1로 설정하면 자동으로 계산된 차원이 지정된다. + +```{python} +A_reshaped = A.view(1, -1) # 1행으로 변환 +print(A_reshaped) +``` + +### 덧셈과 뺄셈 + +모양과 자료형이 맞으면 텐서끼리 덧셈과 뺄셈이 가능하다. + +```{python} +print(A + B) +print(A - B) +``` + +### 상수와의 연산 + +상수와의 연산도 각 원소별로 적용된다. + +```{python} +print(A + 2) +print(B ** 2) +print(A // 3) # 정수 나눗셈 +print(A % 3) # 나머지 +``` + +### 제곱근과 로그 + +제곱근과 로그 함수도 각 원소에 적용된다. 다만, 자료형이 맞지 않으면 오류가 발생할 수 있다. + +```{python} +print(torch.sqrt(A)) # 제곱근 +print(torch.log(B)) # 로그 +``` + +### 텐서 곱셈 + +C는 3차원 텐서이다. 이 중 첫 번째 2차원 텐서를 떼어내 B와 곱해보자. + +```{python} +D = C[0, :, :] # 첫 번째 2차원 텐서 선택 +print(D) +``` + +```{python} +result = torch.matmul(B, D) # 텐서 곱셈 +print(result) +``` + +PyTorch에서는 다양한 방법으로 텐서 곱셈을 수행할 수 있다. + +```{python} +print(torch.mm(B, D)) # 2차원 전용 +print(B.mm(D)) # 텐서의 메서드 +print(B.matmul(D)) # 텐서의 메서드 +``` + +### 텐서의 전치(Transpose) + +텐서의 차원을 전치하려면 transpose() 또는 permute()를 사용한다. + +```{python} +print(A) +print(A.T) # 2차원 텐서 전치 +``` + +3차원 이상의 텐서에서는 전치할 차원을 지정해야 한다. + +```{python} +print(torch.transpose(C, 1, 2)) # 2번째와 3번째 차원을 교환 +``` + +### 다차원 텐서와 1차원 텐서의 연산 + +다차원 텐서와 1차원 텐서의 연산은 자동으로 차원을 맞춰준다(브로드캐스팅). + +```{python} +print(A) +print(A + torch.tensor([1, 2, 3])) +``` + +### 1차원 텐서 간의 연산 (내적과 외적) + +1차원 텐서끼리의 연산도 가능하지만, 모양을 명확히 정의해야 한다. + +```{python} +A_1 = A.view(1, -1) # 1행 벡터 +A_2 = A.view(-1, 1) # 1열 벡터 + +print(A_1.mm(A_2)) # 내적 (결과는 스칼라) +print(A_2.mm(A_1)) # 외적 (결과는 행렬) +``` + +#### 모양 오류 확인 + +차원이 맞지 않으면 연산이 불가능하다. + +```{python} +#| error: true +A_3 = torch.tensor([1, 2, 3, 4, 5, 6]) +print(A_1.mm(A_3)) # 오류 발생 +``` + +```{python} +print(A_1.shape) +print(A_3.shape) # 차원이 맞지 않음 +``` + +각 연산은 데이터의 모양과 자료형을 명확히 맞추는 것이 중요하다. + diff --git a/03-transferdevice.qmd b/03-transferdevice.qmd new file mode 100644 index 0000000..5db24e9 --- /dev/null +++ b/03-transferdevice.qmd @@ -0,0 +1,81 @@ +# 텐서의 이동: CPU ⇔ GPU + +```{r, echo=FALSE, message=FALSE, warning=FALSE, include=FALSE} +Sys.setenv(RETICULATE_PYTHON = "/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python") +library(reticulate) +use_python("/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python", required = T) +``` + +딥러닝에서는 계산량이 많아지기 때문에 GPU는 필수적이다. PyTorch에서는 텐서를 다룰 때, 텐서가 현재 어디에 저장되어 있는지에 대한 정보를 제공한다. GPU를 활용하는 방법을 알아보자. + +GPU 사용 가능 여부 확인 + +현재 시스템에서 GPU를 사용할 수 있는지 확인하려면 torch.cuda.is_available()을 사용한다. + + +```{python} +import torch + +# GPU 사용 가능 여부 확인 +print(torch.cuda.is_available()) +``` + +## CPU에서 GPU로 이동 + +텐서를 GPU로 이동하려면 .to(device) 또는 .cuda()를 사용한다. + +```{python} +# CPU에서 텐서 생성 +a = torch.tensor([1, 2, 3, 4]) +print(a) +``` + +```{python} +# GPU로 이동 +a_gpu = a.cuda() # 또는 a.to('cuda') +print(a_gpu) +``` + +## 자료형 변환과 함께 이동 + +GPU로 이동할 때 자료형을 동시에 변경할 수도 있다. + +```{python} +a_gpu_double = a.to(device='cuda', dtype=torch.float64) +print(a_gpu_double) +``` + +## GPU에서 CPU로 이동 + +GPU 상에서 생성된 텐서를 다시 CPU로 이동하려면 .to('cpu') 또는 .cpu()를 사용한다. + +```{python} +# GPU에서 텐서 생성 +b = torch.tensor([1, 2, 3, 4], device='cuda') +print(b) + +# CPU로 이동 +b_cpu = b.cpu() # 또는 b.to('cpu') +print(b_cpu) +``` + + +## GPU 상에서 텐서 생성 + +GPU 상에서 바로 텐서를 생성할 수도 있다. 아래는 GPU에 직접 텐서를 만드는 코드다. + +```{python} +b = torch.tensor([1, 2, 3, 4], device='cuda') +print(b) +``` + +## 주의사항 + +GPU로 이동된 텐서를 사용하려면, 해당 텐서를 포함한 연산도 GPU에서 수행되어야 한다. 만약 CPU와 GPU 간의 텐서를 혼합해서 연산하면 오류가 발생한다. + +```{python} +#| error: true +# 오류 예시 (CPU 텐서와 GPU 텐서를 함께 사용) +a = torch.tensor([1, 2, 3, 4]) # CPU 텐서 +b = torch.tensor([1, 2, 3, 4], device='cuda') # GPU 텐서 +``` diff --git a/04-class-and-tensor.qmd b/04-class-and-tensor.qmd new file mode 100644 index 0000000..d3ad4fb --- /dev/null +++ b/04-class-and-tensor.qmd @@ -0,0 +1,172 @@ +# Python OOP와 텐서 {#class} + +```{r, echo=FALSE, message=FALSE, warning=FALSE, include=FALSE} +Sys.setenv(RETICULATE_PYTHON = "/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python") +library(reticulate) +use_python("/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python", required = T) +``` + +Python의 PyTorch는 OOP(Object-Oriented Programming, 객체지향 프로그래밍) 기반으로 설계되어 있다. 텐서와 신경망 등의 구조는 모두 Python 클래스와 메서드로 정의된다. Python에서 OOP 개념을 활용하는 방법과 PyTorch의 관련성을 살펴보자. + +## 클래스(Class), 메서드(Method), 속성(Attribute) + +Python에서 클래스를 정의하려면 class 키워드를 사용한다. 클래스는 속성(변수)과 메서드(함수)를 포함하는 설계도를 제공한다. + +```{python} +class ExampleClass: + # 속성 (필드) + variable = None + + # 생성자 (클래스 초기화) + def __init__(self, value): + self.variable = value + + # 메서드 + def print_variable(self): + print(f"Value: {self.variable}") +``` + +### 클래스 사용하기 + +위에서 정의한 클래스를 사용해 객체를 생성하고, 메서드와 속성에 접근할 수 있다. + +```{python} +example = ExampleClass("Hello") +example.print_variable() # 출력: Value: Hello +``` + +### 학생 클래스 예제 + +객체지향 프로그래밍의 개념을 이해하기 위해 “학생” 클래스(Student)를 만들어보자. 이 클래스는 학생의 이름, 이메일, 중간고사 점수, 기말고사 점수를 저장하고, 총점과 평균을 계산하는 메서드를 제공한다. + +```{python} +class Student: + def __init__(self, first, last, midterm, final): + self.first = first + self.last = last + self.email = f"{first.lower()}-{last.lower()}@gmail.com" + self.midterm = midterm + self.final = final + + # 메서드: 총점 계산 + def calculate_total(self): + return self.midterm + self.final + + # 메서드: 평균 계산 + def calculate_average(self): + return (self.midterm + self.final) / 2 +``` + +```{python} +# 객체 생성 +issac = Student("Issac", "Lee", 70, 50) +bomi = Student("Bomi", "Kim", 65, 80) + +print(issac.first, issac.last, issac.email, issac.calculate_total()) +print(bomi.first, bomi.last, bomi.email, bomi.calculate_average()) +``` + + +### __str__ 메서드를 사용해 출력 커스터마이징 + +__str__ 메서드를 사용하면 객체를 출력할 때의 형식을 정의할 수 있다. + +```{python} +class Student: + def __init__(self, first, last, midterm, final): + self.first = first + self.last = last + self.email = f"{first.lower()}-{last.lower()}@gmail.com" + self.midterm = midterm + self.final = final + + def calculate_total(self): + return self.midterm + self.final + + def calculate_average(self): + return (self.midterm + self.final) / 2 + + # __str__ 메서드 + def __str__(self): + return ( + f"Student: {self.first} {self.last}\n" + f"Email: {self.email}\n" + f"Midterm: {self.midterm}, Final: {self.final}\n" + f"Total: {self.calculate_total()}, Average: {self.calculate_average()}" + ) + +soony = Student("Soony", "Kim", 70, 20) +print(soony) +``` + +## 상속(Inheritance) + +OOP에서 상속은 기존 클래스의 속성과 메서드를 그대로 가져오면서, 새로운 속성과 메서드를 추가하거나 기존 것을 변경할 수 있는 강력한 기능이다. 예를 들어, Student 클래스를 상속받아 특정 대학교의 학생 클래스를 만들어보자. + +```{python} +class UspStudent(Student): + university_name = "University of Statistics Playbook" + + def __init__(self, first, last, midterm, final, year): + super().__init__(first, last, midterm, final) # 상위 클래스 초기화 + self.year = year + + def __str__(self): + base_info = super().__str__() + return f"{base_info}\nYear: {self.year}, University: {self.university_name}" + +# 객체 생성 +sanghoon = UspStudent("Sanghoon", "Park", 80, 56, 2023) +print(sanghoon) + +``` + +### 비공개 속성 (Private Attribute) + +Python에서는 비공개 속성을 _ 또는 __로 시작하는 이름으로 정의해 사용할 수 있다. + +```{python} +class UspStudent(Student): + university_name = "University of Statistics Playbook" + + def __init__(self, first, last, midterm, final, year): + super().__init__(first, last, midterm, final) + self.year = year + self.__average = None # 비공개 속성 + + def calculate_average(self): + self.__average = super().calculate_average() + return self.__average + + def get_average(self): + return self.__average + +connie = UspStudent("Connie", "Kim", 78, 82, 2023) +connie.calculate_average() +print(connie.get_average()) +``` + +### 읽기 전용 속성 (Property) + +Python에서는 @property 데코레이터를 사용해 읽기 전용 속성을 정의할 수 있다. + +```{python} +class UspStudent(Student): + university_name = "University of Statistics Playbook" + + def __init__(self, first, last, midterm, final, year): + super().__init__(first, last, midterm, final) + self.year = year + self._average = None + + @property + def average(self): + if self._average is None: + self._average = super().calculate_average() + return self._average + +connie = UspStudent("Connie", "Kim", 78, 82, 2023) +print(connie.average) +``` + +OOP를 활용하면 재사용 가능한 코드 작성이 가능해지고, 더 복잡한 구조도 관리하기 쉬워진다. \ No newline at end of file diff --git a/05-forward-propagation.qmd b/05-forward-propagation.qmd new file mode 100644 index 0000000..1dc61bf --- /dev/null +++ b/05-forward-propagation.qmd @@ -0,0 +1,206 @@ +# 순전파 (Forward propagation) {#forward} + +```{r, echo=FALSE, message=FALSE, warning=FALSE, include=FALSE} +Sys.setenv(RETICULATE_PYTHON = "/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python") +library(reticulate) +use_python("/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python", required = T) +``` + +```{python} +import torch +import torch.nn.functional as F +``` + +## 신경망의 구조 + +딥러닝의 시작점인 신경망(Neural network)을 공부하기 위해서, 앞으로 우리가 다룰 모델 중 가장 간단하면서, 딥러닝에서 어떤 일이 벌어지고 있는지 상상이 가능한 신경망을 먼저 학습하기로 하자. 우리가 오늘 예로 생각할 신경망은 다음과 같다. + +```{r neuralnet-example, echo=FALSE, fig.cap="세상에서 가장 간단하지만 있을 건 다있는 신경망", fig.align='center', out.width = '100%'} +knitr::include_graphics("./image/neuralnet1.png") +``` + +위의 그림과 같은 신경망을 2단 신경망이라고 부른다. 일반적으로 단수를 셀 때 제일 처음 입력하는 층은 단수에 포함하지 않는 것에 주의하자. 각 녹색, 회색, 그리고 빨간색의 노드(node)들은 신경망의 요소를 이루는데, 각각의 이름은 다음과 같다. + +- 입력층(input layer) - 2개의 녹색 노드(node) +- 은닉층(hidden layer) - 3개의 회색 노드(node) +- 출력층(output layer) - 1개의 빨강색 노드(node) + +자 이제부터, 녹색 노드에는 무엇이 들어가는지, 그리고, 어떤 과정을 거쳐서 빨강색의 값이 나오는지에 대하여 알아보자. 딥러닝에서 녹색이 입력값을 넣어서 빨간색의 결과값을 얻는 과정을 **순전파(Forward propagation)**라고 부른다. `propagation`의 뜻은 증식, 혹은 번식인데, 식물이나 동물이 자라나는 것을 의미하는데, 녹색의 입력값들이 어떠한 과정을 거쳐 빨간색으로 자라나는지 한번 알아보자. + +## 순전파(Forward propagation) + +우리가 사용할 데이터 역시 아주 간단하다. + +$$ +X =\left(\begin{array}{cc} +1 & 2\\ +3 & 4\\ +5 & 6 +\end{array}\right) +$$ + +가로 행이 하나의 표본을 의미하고, 세로 열 각각은 변수를 의미한다. 즉, 위의 자료 행렬은 2개의 변수 정보가 들어있는 세 개의 표본들이 있는 자료을 의미한다. + +### 표본 1개, 경로 1개만 생각해보기 + +주의할 것은, 우리가 그려놓은 신경망의 입력층의 노드는 2개이고, 자료 행렬은 3행 2열이라는 것이다. 우리가 그려놓은 신경망으로 샘플 하나 하나가 입력층에 각각 입력되어 표본별 결과값 생성되는 것이다. 따라서 신경망을 잘 이해하기 위해서 딱 하나의 표본, 그리고 딱 하나의 경로만을 생각해보자. + +``` +> 목표: 첫번째 표본인 $(1, 2)$가 다음과 같은 경로를 타고 어떻게 자라나는지 생각해보자. +``` + +```{r neuralnet-path, echo=FALSE, fig.cap="예시 경로 1", fig.align='center', out.width = '100%'} +knitr::include_graphics("./image/neuralnet3.png") +``` + +그림에서 $\beta$는 노드와 노드 사이를 지나갈 때 부여되는 웨이트들을 의미하고, $\sigma()$는 다음의 시그모이드(sigmoid) 함수를 의미한다. + +$$ +\sigma(x) = \frac{1}{1+e^{-x}} = \frac{e^x}{e^x+1} +$$ + +자료 행렬을 위에 색칠된 경로로 보낸다는 의미는 다음과 같은 계산과정을 거친다는 것이다. +```{python} +# 입력 데이터 +X = torch.tensor([[1.0, 2.0]], dtype=torch.float64) # 1행 2열 +print(X) + +# 첫 번째 은닉층 가중치 (beta_1) +beta_1 = torch.tensor([[0.5], [0.8]], dtype=torch.float64) # 2행 1열 +print(beta_1) + +# 은닉층 값 계산 +z_21 = X @ beta_1 # 행렬 곱 +print(z_21) + +# 활성화 함수 통과 (시그모이드 함수) +a_21 = torch.sigmoid(z_21) +print(a_21) + +# 출력층 가중치 (gamma_1) +gamma_1 = torch.tensor([[0.7]], dtype=torch.float64) # 상수 + +# 출력층 값 계산 +z_31 = a_21 * gamma_1 +print(z_31) + +# 최종 출력 (시그모이드 함수) +y_hat = torch.sigmoid(z_31) +print(y_hat) +``` + +즉, 우리가 생각하는 표본은 빨간색 노드에 도착하기 위해서 두번째 은닉층의 첫번째 노드를 통과하여 올 수 있다. 하지만 빨간색 노드에는 방금 우리가 생각한 경로 뿐만 아니라 두 개의 선택지가 더 존재한다. + +### 1개의 표본, 경로 한꺼번에 생각하기 + +세가지의 경로를 모두 생각해보면, 우리의 표본은 다음의 경로를 통해서 도착한다. + +``` +> 목표: 첫번째 표본인 $(1, 2)$가 다음과 같은 세가지 경로를 타고 어떻게 하나로 합쳐지는지 이해해보자. +``` + +```{r neuralnet-allpath, echo=FALSE, fig.cap="3가지 경로", fig.align='center', out.width = '100%'} +knitr::include_graphics("./image/neuralnet3.png") +``` + +이 과정을 우리가 통계 시간에 배운 회귀분석에 연결지어 생각해보면, 다음의 해석이 가능하다. 두번째 은닉층의 각각의 노드들이 하나의 회귀분석 예측 모델들이라고 생각하면, 신경망은 세 개의 회귀분석을 한 대 모아놓은 거대한 회귀분석 집합체라고 생각할 수 있게 된다. 즉, 각 회귀분석 모델들이 예측한 표본에 대한 대응변수 예측값들을 은닉층에 저장한 후, 그 예측값들을 모두 모아 마지막 빨간색 노드에서 합치면서 좀 더 좋은 예측값을 만들어 내는 것이다. 이 때, $\gamma$ 벡터를 통해 가중치를 부여하는 것이라고 해석이 가능하다. + +이 과정을 `torch` 텐서를 사용하여 깔끔하게 나타내보자. + +```{python} +import torch +import torch.nn.functional as F + +# 1개 표본 +# 1 by 2 +X = torch.tensor([[1.0, 2.0]], dtype=torch.float64) +print(X) + +# 베타벡터가 세 개 존재함 +# 2 by 3 +beta_1 = torch.tensor([[0.5], [0.8]], dtype=torch.float64) +beta_2 = torch.tensor([[0.3], [0.2]], dtype=torch.float64) +beta_3 = torch.tensor([[0.1], [0.4]], dtype=torch.float64) + +# 정의된 베타벡터를 cbind in torch (열 방향 결합) +beta = torch.cat((beta_1, beta_2, beta_3), dim=1) +print(beta) + +# 2번째 레이어 z_2 +# 1 by 3 +z_2 = X @ beta +print(z_2) + +# 2번째 레이어 sigmoid 함수 통과 +# 1 by 3 +a_2 = torch.sigmoid(z_2) +print(a_2) + +# 2번째 레이어에 관한 웨이트 (감마) 벡터 +# 다음 레이어의 1번째 노드에 대한 베타값에 임의의 값을 부여 +# gamma vector 3 by 1 +gamma = torch.tensor([[0.7], [0.5], [0.9]], dtype=torch.float64) +print(gamma) + +# 3번째 레이어 z_3 +# 1 by 1 +z_3 = a_2 @ gamma +print(z_3) + +# 마지막 레이어에서 시그모이드 함수 통과 +# 1 by 1 +y_hat = torch.sigmoid(z_3) +print(y_hat) +``` + + +### 전체 표본, 경로 전체 생각해보기 + +이제 자료 행렬 전체를 한꺼번에 넣는 방법을 생각해보자. 입력값이 자료 행렬 전체이므로, 결과값은 이에 대응하도록 행의 갯수와 같은 벡터 형식이 될 것이라는 것을 예상하고 코드를 따라오도록 하자. + +``` +> 목표: 전체 표본이 신경망을 통해서 예측되는 구조를 이해하자. +``` + +```{python} +import torch +import torch.nn.functional as F + +# 데이터 텐서 +# 3 by 2 +X = torch.tensor([[1.0, 2.0], + [3.0, 4.0], + [5.0, 6.0]], dtype=torch.float64) +print(X) + +# 베타벡터가 세 개 존재함 +# 2 by 3 +beta = torch.tensor([[0.5, 0.3, 0.1], + [0.8, 0.2, 0.4]], dtype=torch.float64) +print(beta) + +# 2번째 레이어 z_2 +# 3 by 3 +z_2 = X @ beta +print(z_2) + +# 2번째 레이어 sigmoid 함수 통과 +# 3 by 3 +a_2 = torch.sigmoid(z_2) +print(a_2) + +# 2번째 레이어에 관한 웨이트 (감마) 벡터 +# gamma vector 3 by 1 +gamma = torch.tensor([[0.7], [0.5], [0.9]], dtype=torch.float64) +print(gamma) + +# 3번째 레이어 z_3 +# 3 by 1 +z_3 = a_2 @ gamma +print(z_3) + +# 마지막 레이어에서 시그모이드 함수 통과 +# 3 by 1 +y_hat = torch.sigmoid(z_3) +print(y_hat) +``` diff --git a/06-autograd.qmd b/06-autograd.qmd new file mode 100644 index 0000000..d5e8f0a --- /dev/null +++ b/06-autograd.qmd @@ -0,0 +1,273 @@ +# 미분 자동추적 기능 (Autograd) 에 대하여 + +```{r, echo=FALSE, message=FALSE, warning=FALSE, include=FALSE} +Sys.setenv(RETICULATE_PYTHON = "/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python") +library(reticulate) +use_python("/opt/homebrew/Caskroom/miniconda/base/envs/torch/bin/python", required = T) +``` + +이번 장에서는 `torch` 및 다른 딥러닝 라이브러리의 근본을 이루는 기능인 미분 자동 추적 기능에 대하여 알아보도록 하자. 예를 들어 설명하는 것을 좋아하므로, 이번 챕터에 쓸 예제 함수를 먼저 정의하자. + +## 예제 함수 + +$n$개의 데이터 $x_1, ..., x_n$이 주어졌다고 할 때, 우리는 다음의 함수 $f$를 정의 할 수 있다. + +$$ +f(\mu) = \frac{1}{n}\sum_{i=1}^{n}(x_i - \mu)^2 +$$ + +위의 함수는 다음과 같이 해석해 볼 수 있다. $x$ 데이터에 담겨있는 정보를 단 하나의 지표 $\mu$로 압축해서 나타낸다고 할 때, 함수 $f$는 각 관찰값에 대한 오차들, $x_i - \mu$,의 제곱의 평균을 나타낸다. + +통계학에서는 나름 유명한 함수인데, 왜냐하면 위의 함수값을 최소화시키는 $\mu$를 찾게되면 표본평균($\bar{x}$) 나오기 때문이다. 오늘은 이 함수를 통하여 `torch`의 자동 미분 기능에 대하여 알아보고자 한다. + +## 데이터 생성 + +`torch`패키지를 불러 임의로 난수를 발생시킨 후, 텐서 `x`에 집어넣도록 하자. + +```{python} +import torch +import matplotlib.pyplot as plt + +# 난수 생성 +torch.manual_seed(2021) +x_tensor = torch.rand(7) * 10 +print(x_tensor) +``` + + +위의 코드에서 쓰인 함수 두 개를 알아보자. + +- `torch.manual_seed()`: 시뮬레이션 할 때 시드를 고정하는 역할을 한다. +- `torch.rand()`: 균등분포(Uniform distribution) 분포에서 원하는 갯수만큼 표본을 뽑는다. + +## 함수 만들기 및 오차 그래프 + +앞에서 살펴본 함수 $f$는 모수($\mu$)를 입력값으로 하는 함수이므로, 다음과 같이 함수를 정의 할 수 있다. + +```{python} +def f(mu, x): + return torch.mean((x - mu) ** 2) +``` + +위에서 알 수 있듯, $\mu$ 값이 2인 경우에 대한 오차들의 제곱의 평균값은 `round(f(2, x_tensor).item(), 4)`이다. 여러 $\mu$ 값에 대하여 `f` 함수의 값을 구해보자. + + +```{python f-errormean, echo=TRUE, message=FALSE, warning=FALSE, fig.cap="$\\mu$ 값에 따른 `myf` 함수값의 변화", fig.align='center', out.width = '100%'} +import matplotlib.pyplot as plt +# 여러 μ 값에 대한 f(μ) 계산 +mu_vec = torch.arange(0, 10, 0.02) +result = [f(mu, x_tensor).item() for mu in mu_vec] + +# 그래프 그리기 +plt.figure(figsize=(8, 5)) +plt.plot(mu_vec, result, label="f(μ)") +plt.xlabel("μ") +plt.ylabel("f(μ)") +plt.title("Function f(μ) vs μ") +plt.legend() +plt.show() +``` + +우리의 목표는 바로 저 곡선을 최소로 만드는 $\mu$ 값이 무엇인지 찾아내는 것이다. 이 최소값을 찾기위해서는 [경사하강법](https://ko.wikipedia.org/wiki/%EA%B2%BD%EC%82%AC_%ED%95%98%EA%B0%95%EB%B2%95) 같은 방법을 사용해야 하는데, 이러한 알고리즘들의 핵심은 바로 주어진 $\mu$값에 대응하는 기울기값을 구하는 것이다. + +우리가 임의로 정한 시작점 $\mu_i$에서 목표인 $\mu_{*}$까지 찾아가기 위해서 경사하강법을 통하면 다음의 과정을 $\mu$값이 수렴할 때까지 반복하면 된다. + +$$ +{\displaystyle \mathbf {\mu} _{i+1}=\mathbf {\mu} _{i}-\gamma _{i}\nabla f(\mathbf {\mu} _{i})}, \quad i \in \mathbb{N} +(\#eq:graddecent) +$$ + +위의 수식에서 $\gamma _{i}$은 탐색을 할 때 움직이는 거리 (step size)라고 부르고, 딥러닝 분야에서는 나중에 학습률(learning rate)의 개념이 된다. 또한, $\nabla f(\mathbf {\mu} _{i})$ 부분이 바로 기울기값을 나타내는 부분이다. + +## Autograd 기능 없이 기울기 구하기 + +먼저 `torch`의 자동기울기 기능를 사용해서 기울기값 계산을 하기에 앞서, 계산 결과를 구해보자. $y$를 $\beta$에 대하여 미분하면 다음과 같다. + +$$ +\begin{align*} +f'(\beta) & =\frac{d}{d\beta}\left(\frac{1}{n}\sum_{i=1}^{n}\left(x_{i}-\beta\right)^{2}\right)\\ + & =\frac{1}{n}\sum_{i=1}^{n}\frac{d}{d\beta}\left(x_{i}-\beta\right)^{2}\\ + & =-\frac{1}{n}\sum_{i=1}^{n}2\left(x_{i}-\beta\right) +\end{align*} +$$ + +따라서 `mu`값이 `2.5`로 주어졌을때, 기울기 값은 다음과 같다. + +```{python} +# 기울기 함수 정의 +def f_prime(mu, x): + return -2 * torch.mean(x - mu) + +# μ = 2.5에서 기울기 계산 +mu = 2.5 +slope = f_prime(mu, x_tensor).item() +print(f"Slope at μ = {mu}: {slope}") +``` + + +이것이 실제로 그러한지 그림을 그려보자. + +```{python} +import matplotlib.pyplot as plt +import numpy as np + +# 값 계산 +mu = 2.5 +my_slope = f_prime(mu, x_tensor).item() +my_intercept = f(mu, x_tensor).item() - f_prime(mu, x_tensor).item() * mu + +# 데이터 생성 (f(mu) vs mu) +mu_vec = torch.arange(0, 10, 0.02) +result = [f(mu, x_tensor).item() for mu in mu_vec] + +# 그래프 그리기 +plt.figure(figsize=(8, 5)) +plt.plot(mu_vec, result, label="f(μ)") + +# 직선 추가 (abline) +x_line = np.linspace(0, 10, 100) +y_line = my_slope * x_line + my_intercept +plt.plot(x_line, y_line, color="red", label="Tangent at μ = 2.5") + +plt.xlabel("μ") +plt.ylabel("f(μ)") +plt.legend() +plt.title("Function f(μ) with Tangent Line") +plt.show() +``` + + +## 자동미분(Autograd) 기능 + +`torch`에는 우리가 계산한 기울기 구하는 과정들을 자동으로 해주는 기능이 있다. 바로 자동미분 (Auto gradient) 기능이다. 기울기값 계산을 위해서 해야할 일은 기울기 계산기능을 `activate` 해주는 옵션을 실행시켜주기만 하면 된다. + +함수는 $\mu$에 대한 함수이므로, 기울기값을 추적할 텐서 $\mu$를 선언할 때 `requires_grad = TRUE` 옵션을 붙여줘서 선언하면 끝이다. 이 옵션이 활성화 되면 `torch`는 이 변수와 관련된 다른 변수들에 대하여 기울기값을 자동으로 추적한다. 추후 복잡한 신경망을 다루는 딥러닝 분야에서는 기울기를 구하는 것이 학습에 아주 핵심적인 기능이고, 이러한 기울기를 구하는 이러한 기울기를 계산하는 방법을 역전파 (backpropagation)라고 부른다. + +```{python} +mu = torch.tensor(2.5, requires_grad=True) +print(mu) +``` + + +`mu` 텐서가 기울기 추적 옵션을 달고 있어서, 이와 관련되어 생성되는 모든 텐서에 기울기 추적 옵션 grad_fn 태그가 달려서 생성된다. 다음과 같이 y를 정의를 하면, y에도 역시 grad_fn이 붙어서 생성되는 것을 알 수 있다. + +```{python} +# f(μ) 계산 +y = torch.mean((x_tensor - mu) ** 2) +print(y) +``` + +기울기 값 계산을 위해서 해야 할 일은 기울기 계산을 `activate` 해주는 함수를 실행시켜주기만 하면 된다. y에 대한 베타의 기울기 값을 구하는 것이므로, 다음과 같이 `backward()`를 이용하여 역전파(backward propagation)를 통하여 기울기 계산을 한다. + +```{python} +y.backward() +``` + +자동 기울기 추적 기능을 사용한 auto grad가 구한 베타의 기울기값이 우리가 구한 값과 동일한지 확인해보자. + +```{python} +print(f"Autograd computed gradient: {mu.grad.item()}") +f_prime(2.5, x_tensor) +``` + +앞에서 구한 `f_prime(2.5, x_tensor)`값이 동일하게 `mu.grad`에 담겨 있다는 것을 알 수 있다. + +## 자동 미분 관련 함수들 + +기울기 자동 추적기능을 사용한다는 것은 그것을 돌리는 컴퓨터의 메모리를 많이 차지한다는 이야기이다. 따라서 우리가 생각하는 변수에 대한 것에만 추적 옵션을 붙여야 하고, 더 이상 필요가 없어지면 기능을 꺼주기도 해야 할 것이다. 이러한 자동 미분 추척 기능들을 자유자재로 다루기 위해서 알아두어야 할 함수들이 있다. + +### `$detach()` + +현재 y는 기울기 자동추적 기능이 붙어있다. 우리가 다음과 같이 y를 사용해서 텐서 `z`를 생성하면 그 역시 옵션이 딸려 생성이 될 테지만, y 텐서 이후 부터는 추적 기능을 사용하고 싶지 않을때, `detach()`를 사용해서 추적기를 떼어낼 수 있다. + +```{python} +z = y ** 2 +z_detached = z.detach() # 기울기 추적 중단 +print(f"Detached z:", z_detached) +``` + + +### `requires_grad` 변수와 `requires_grad_()` + +이 함수는 이미 선언된 텐서에 미분 추적기능을 붙이고 싶을 때, `requires_grad_()`을 사용할 수 있다. 일반 텐서 `a`를 생성하도록 하자. + +```{python} +a = torch.tensor([1.0, 2.0]) +print(f"Before requires_grad: {a.requires_grad}") +``` + +```{python} +a.requires_grad_() # 기울기 추적 활성화 +print(f"After requires_grad: {a.requires_grad}") +``` + +`a.requires_grad` 값이 `False`라는 말은 `a`에 대한 추적 옵션은 현재 꺼져있는 상태이다. 자동 추적 기능이 없이 생성된 텐서에 추적 기능을 붙일 때에는 `a.requires_grad`을 `True`로 바꿔주면 된다. TRUE를 직접 할당해도 되고, `requires_grad_(True)`을 사용하여 바꿔줘도 된다. + +```{python} +a.requires_grad_(True) +``` + +### `torch.no_grad()` + +만약 특정 코드를 실행함에 있어서 추적 기능을 떼고 계산하고 싶은 경우, `torch.no_grad()`가 유용하다. + +```{python} +with torch.no_grad(): + y_no_grad = y ** 2 + print(f"No gradient tracking: {y_no_grad}") +``` + + +## 경사하강법 + +이왕 자동 미분기능을 알았으니, 이 기능을 이용하여 식 \@ref(eq:graddecent)의 경사하강법으로 함수값을 최소로 만드는 $\mu$ 값을 찾아보도록 하자. + +```{python} +learning_rate = 0.1 +mu = torch.tensor(0.5, requires_grad=True) + +result = [] + +for _ in range(100): + result.append(mu.item()) + y = torch.mean((x_tensor - mu) ** 2) + y.backward() + + with torch.no_grad(): + mu -= learning_rate * mu.grad + mu.grad.zero_() # 기울기 초기화 + +print(f"Estimated μ: {mu.item()}") +print(f"Sample mean: {x_tensor.mean().item()}") +``` + + +`mu.grad.zero_()` 부분은 미분값을 초기화 해주는 부분이라고 이해하면 좋다. 그렇지 않을 경우, 이전의 값이 남아있어서 계속 누적되므로 주의하자. + +### 시각화 + +```{python} +# 경사하강법 결과 시각화 +mu_points = torch.tensor(result) +y_points = [f(mu, x_tensor).item() for mu in mu_points] + +plt.figure(figsize=(8, 5)) +plt.plot(mu_vec, result, label="f(μ)") +plt.scatter(mu_points, y_points, color="blue", label="Gradient Descent Steps") +plt.xlabel("μ") +plt.ylabel("f(μ)") +plt.title("Gradient Descent Progress") +plt.legend() +plt.show() +``` + + +이 챕터의 제일 첫부분에서 말했든 이론적인 정답은 데이터의 표본평균이 함수값을 최소로 만드는 값이다. 실제로 그렇게 나왔는지 확인해보면 두 값이 같다는 것을 알 수 있다. + +```{python} +print(f"Result from Gradient Descent: {result[-1]}") +print(f"Theoretical Sample Mean: {x_tensor.mean().item()}") +``` + +이것으로 자동 미분 기능에 대하여 알아보았다. 이 기능을 활용하면 훨씬 복잡한 구조의 함수(예를 들어 딥러닝에서의 신경망 같은)에 대한 미분값 역시도 쉽게 구할 수 있다. 응용 코드들은 신경망 예제에서 다루기로 하자. + diff --git a/07-myfirst-nn.Rmd b/07-myfirst-nn.Rmd index 029060a..6d8dd85 100644 --- a/07-myfirst-nn.Rmd +++ b/07-myfirst-nn.Rmd @@ -131,6 +131,4 @@ my_net(X) 위의 코드를 한번 살펴보자. 먼저 `zeallot` 패키지는 `%<-%`를 포함하는 패키지인데, 여러 개의 변수에 한꺼번에 값을 부여하는 연산자이기 때문에 알아두면 편한 패키지 이다. -새로 정의된 `TwoLayerNet` 클래스에는 \@ref(fig:neuralnet-example2)의 2단 신경망의 순전파(forward propagation)가 구현된 멤버함수 `forward`가 정의되어 있다. 이 함수는 입력 텐서 `X`가 신경망으로 들어오게 되면, 은닉층(hidden_layer) $\rightarrow$ 활성함수 (activation function; 여기서는 nn_sigmoid 함수) $\rightarrow$ 출력층(output_layer) $\rightarrow$ 활성함수 순으로 내보내게 된다. - - +새로 정의된 `TwoLayerNet` 클래스에는 \@ref(fig:neuralnet-example2)의 2단 신경망의 순전파(forward propagation)가 구현된 멤버함수 `forward`가 정의되어 있다. 이 함수는 입력 텐서 `X`가 신경망으로 들어오게 되면, 은닉층(hidden_layer) $\rightarrow$ 활성함수 (activation function; 여기서는 nn_sigmoid 함수) $\rightarrow$ 출력층(output_layer) $\rightarrow$ 활성함수 순으로 내보내게 된다. \ No newline at end of file diff --git a/07-myfirst-nn.qmd b/07-myfirst-nn.qmd new file mode 100644 index 0000000..72b2ce6 --- /dev/null +++ b/07-myfirst-nn.qmd @@ -0,0 +1,146 @@ +# `torch_nn` 모듈로 첫 신경망 정의하기 + +이제까지 `torch`의 자동미분(auto grad) 기능과 순전파(forward propagation)에 대하여 알아보았다. 오늘은 드디어, `torch` 라이브러리에서 제공하는 함수들을 이용해서 챕터 \@ref(forward) 에서 정의해본 신경망을 정의해 보도록 한다. + +```{r neuralnet-example2, echo=FALSE, fig.cap="다시 두두등장! 세상 간단한 신경망", fig.align='center', out.width = '100%'} +knitr::include_graphics("./image/neuralnet1.png") +``` + +## 신경망 정의 (Custom nn Modules) + +토치를 사용해서 신경망을 정의할 때 사용하는 함수가 있다. 바로 `nn.Module`이라는 함수인데, `torch`에서 신경망을 정의할 때, 이 함수를 사용해서 "클래스"를 만들어 정의한다! 왜 우리가 챕터 \@ref(class)에서 클래스 내용을 그렇게도 공부했었는지에 대한 답을 바로 이 챕터에서 찾을 수 있을 것이다. + +### `nn.Module`과 클래스 상속 + +`nn.Module`이 어떤 역할을 하는지에 대하여 알아보기 위해 가장 간단한 신경망을 작성해보도록 하자. 바로 우리가 앞서 살펴본 2단 레이어 네트워크 예제에서 사용한 데이터를 만들어 보자. + +```{python} +# PyTorch로 첫 신경망 정의하기 +import torch +import torch.nn as nn +import torch.nn.functional as F + +# 데이터 정의 +X = torch.tensor([[1, 2], [3, 4], [5, 6]], dtype=torch.float32) +print("Input tensor X:\n", X) +``` + +먼저, `TwoLayerNet`이라는 이름의 신경망 클래스를 정의한다(기억하시나? 클래스의 이름은 카멜 형식이다!). `nn.Module()` 함수는 클래스를 정의하는 함수인데, 이 함수를 사용해서 만들어진 클래스는 자동으로 신경망과 관련한 클래스인 `basic-nn-module` 클래스를 상속하게 만든다. 즉, `nn.Module`안에는 신경망 관련 클래스들 속에는 신경망과 관련한 많은 함수가 정의되어 있을 것이고, 이것을 다 상속받아서 클래스가 만들어지는 것이다. 다음의 코드는 위의 신경망을 정의한 코드이다. + +```{python} +# TwoLayerNet 신경망 정의 +class TwoLayerNet(nn.Module): + def __init__(self, data_in, hidden, data_out): + super(TwoLayerNet, self).__init__() + print("Initiation complete!") + + self.hidden_layer = nn.Linear(data_in, hidden, bias=False) + self.output_layer = nn.Linear(hidden, data_out, bias=False) + +# 모델 생성 +myfirst_model = TwoLayerNet(2, 3, 1) +print(myfirst_model) +``` + +결과를 살펴보면 `TwoLayerNet` 클래스에 의하여 만들어진 `myfirst_model`는 두 개의 층이 들어있는 것을 확인할 수 있다. 이 두개 층에 관련한 모수 갯수를 그림과 한번 연결 시켜보면 잘 정의가 되어있다는 것을 알 수 있다. + +* hidden_layer: 그림에서 첫번째와 두번째 층을 연결하는 다리가 6개라는 것을 주목하자. 모수의 갯수는 그래서 6개! +* output_layer: 그림에서 두번째와 마지막 층을 연결하는 다리는 3개이므로, 모수의 갯수는 3개가 된다. + +## `nn.Linear` 클래스 + +`nn.Linear`의 입력값은 입력변수의 갯수, 출력변수의 갯수, 그리고 bias 항의 유무를 나타내는 옵션 이렇게 세개가 된다. 예제의 경우, 데이터 텐서 $X$의 features 갯수가 2개이므로, 히든 레이어의 입력값 갯수가 2개가 되어야 한다. 또한 히든 레이어의 노드 갯수가 3개이므로 결과 행력의 features 갯수가 3개가 되어야 한다. + +### bias 없는 경우 + +우리가 예전에 다루었던 예제에서는 `bias` 항이 없었으므로, `bias=False`를 해주어야 함에 주의하자. + +```{python} +# nn.Linear 사용: Bias 없는 경우 +mat_op = nn.Linear(2, 3, bias=False) +print("Weights:\n", mat_op.weight) +``` + + +`mat_op`을 nn.Linear(2, 3) 클래스로 만들어진 클래스 생성자로 이해 할 수 있다. 그리고 이것의 수학적 의미는 행렬 연산으로 이해할 수 있겠다. `mat_op`가 생성될 때 임의의 `weight` 텐서, $W$, 와 `bias`, $b$,가 생성이 되고, 입력값으로 들어오는 `X`에 대하여 다음의 연산을 수행한 후 결괏값을 내보낸다. + +$$ +y = X\beta = XW^T +$$ + +결과를 코드로 확인해보자. + +```{python} +# 행렬 연산 확인 +print("Matrix multiplication output:\n", X @ mat_op.weight.T) + +# nn.Linear 사용 +print("nn.Linear output:\n", mat_op(X)) +``` + + +### bias 있는 경우 + +`bias=True`를 해주면 `weight` 텐서 $W$와 더불어 bias 텐서가 생성이 된다. + +```{python} +# nn.Linear 사용: Bias 있는 경우 +mat_op2 = nn.Linear(2, 3, bias=True) +print("Weights:\n", mat_op2.weight) +print("Bias:\n", mat_op2.bias) +``` + +따라서 정의된 신경망의 연산 역시 다음과 같이 바뀐다. + +$$ +y = X\beta + b = XW^T + b +$$ + +```{python} +# 행렬 연산 확인 +print("Matrix multiplication with bias:\n", X @ mat_op2.weight.T + mat_op2.bias) + +# nn.Linear 사용 +print("nn.Linear output:\n", mat_op2(X)) +``` + + +## 순전파(Forward propagation) 정의 + +`torch`를 공부하면서 신기한 걸 많이 배우고 있다. 그 중 한가지가 바로 객체지향 프로그래밍을 사용해서 신경망을 정의한다는 것이다. 앞선 예제를 이어가보면, 우리는 신경망의 순전파를 구현해야 한다. + +순전파의 경우 다음과 같이 `forward` 멤버 함수를 정의해서 구현할 수 있다. + +```{python} +# TwoLayerNet 클래스 수정: 순전파 정의 추가 +class TwoLayerNet(nn.Module): + def __init__(self, data_in, hidden, data_out): + super(TwoLayerNet, self).__init__() + print("Initiation complete!") + + self.hidden_layer = nn.Linear(data_in, hidden, bias=False) + self.output_layer = nn.Linear(hidden, data_out, bias=False) + self.sigmoid = nn.Sigmoid() + + # 순전파 정의 + def forward(self, X): + z1 = self.hidden_layer(X) + a1 = self.sigmoid(z1) + z2 = self.output_layer(a1) + y_hat = self.sigmoid(z2) + return y_hat + +# 입력과 출력 차원 정의 +D_in, H, D_out = 2, 3, 1 + +# 모델 생성 +my_net = TwoLayerNet(D_in, H, D_out) + +# 순전파 수행 +output = my_net(X) +print("Output of forward propagation:\n", output) +``` + +새로 정의된 `TwoLayerNet` 클래스에는 \@ref(fig:neuralnet-example2)의 2단 신경망의 순전파(forward propagation)가 구현된 멤버함수 `forward`가 정의되어 있다. 이 함수는 입력 텐서 `X`가 신경망으로 들어오게 되면, 은닉층(hidden_layer) $\rightarrow$ 활성함수 (activation function; 여기서는 nn_sigmoid 함수) $\rightarrow$ 출력층(output_layer) $\rightarrow$ 활성함수 순으로 내보내게 된다. + + diff --git a/08-train-mynn.qmd b/08-train-mynn.qmd new file mode 100644 index 0000000..95a7479 --- /dev/null +++ b/08-train-mynn.qmd @@ -0,0 +1,207 @@ +# 나의 첫 신경망 학습 + +저번 시간 우리는 토치에서 신경망을 정의하는 방법에 대하여 알아보았다. 오늘은 정의한 신경망을 어떻게 학습하는가에 대하여 알아보도록 하자. + +## 학습 준비 - 데이터 만들기 + +필자는 [유튜브에 R을 사용한 통계관련 수업](https://youtube.com/playlist?list=PLKtLBdGREmMnLbQnqGEfpCBtkGj2g_d-B)들을 올려놓았다. 이 수업에서 큰 축을 이루는 것 중 하나가 바로 회귀분석이다. 회귀분석은 주어진 데이터를 모델링할 때 신경망의 가장 큰 장점은 회귀직선과 같은 선형모형들이 가지는 한계를 넘어서, 비선형 모델링을 할 수 있게 해준다는 것이다. 이러한 장점들을 잘 확인해보기 위해서 비선형 모델에서 관찰값을 뽑아 모의 데이터로 만들어 보도록 하자. + +```{python} +import torch +import torch.nn as nn +import torch.optim as optim +``` + + +```{python} +# 필요한 라이브러리 로드 +import numpy as np +import matplotlib.pyplot as plt + +# 데이터 생성 +np.random.seed(2021) +torch.manual_seed(2021) + +# x 값 생성 +x = np.sort(np.random.choice(range(1, 101), 100, replace=False)) + +# 모델 함수 f(x) 정의 +def f(x): + return x + 30 * np.sin(0.1 * x) + +# y 값 생성 (f(x) + 잡음) +y = f(x) + np.random.normal(0, 5, size=100) + +# 데이터 시각화 +plt.scatter(x, y, color="#E69F00", label="Observed Data") +plt.plot(x, f(x), linestyle="--", label="True Function (f(x))") +plt.xlabel("x") +plt.ylabel("f(x)") +plt.legend() +plt.show() +``` + + +관찰값 $y$가 발생되는 코드를 살펴보면, $y$는 발생되는 실제 함수 $f$는 다음과 같이 비선형성을 가지고 있고, 거기에 잡음이 섞여서 관찰되는 형태를 띄고 있다. + +$$ +\begin{equation} +\begin{aligned} +f(x) &= x + 30 sin(0.1 x), \\ +y & = f(x) + \epsilon, \quad \epsilon \sim \mathbb{N}(0, 5^2) +\end{aligned} +(\#eq:model-sample) +\end{equation} +$$ + +## 신경망과 블랙박스(Black-box) + +Figure \@ref(fig:add-regression) 을 보면 회귀 직선도 사실 자료의 `x`값에 따른 함수값의 변화를 아주 잘 잡아내는 것을 알 수 있다. 하지만, 우리가 자료를 발생시키는 함수의 구조가 비선형을 띈다는 것을 알고 있는 상태에서 보면(현실에서는 아무도 모른다.), 비선형성을 잡아내지 못하는 회귀 직선의 한계가 뚜렷하게 보인다. 따라서 통계학에서는 이러한 비선형성을 잡아내기 위해서 일반화선형모형(General Linear Model)이나 [일반화 가법모형](https://bookdown.org/cardiomoon/gam/)(Generalized Additive Model; GAM)^[링크는 가톨릭대 문건웅 교수님이 쓴 일반화 가법모델에 대한 내용이다. R코드와 함께 친절하게 설명이 되어있다.] 등 여러가지 기법들이 발달했다. 신경망 역시 이것들의 연장선 상에 위치하는 모델이라고 생각해도 무방하다. + +다만 모델의 해석적인 측면에서 일반화 선형모형에서는 모델을 만드는 사람들이 비선형성을 부여하는 툴들을 (예를 들어 link 함수를 자료를 보고 사용자가 선택한다.) 조절해서, 모델의 결과 해석력이 우수했다면, 일반화 가법모형과 신경망으로 갈 수록 사용자가 모델의 비선형성을 조절한다기모다 기법 자체가 비선형성을 잘 다루도록 설계가 되어있어서 해석력은 떨어지고 예측력은 증가했다. 신경망은 특히 모델 자체에 자유도를 높이고, 대량의 데이터를 사용하여 학습하면서 실제 함수를 찾아가는 방식이라서, 학습된 모델의 해석이 거의 불가능하게 되어버려서 블랙박스(Black box - 안이 어떻게 돌아가는지 모름) 모델이라는 별명이 생겼다. + +모델러 입장에서는 '뭐가 뭔지는 모르겠는데, 예측은 잘한다'라는 느낌이 드는 아이인데, 실제 성능적인 측면에서 기존 모델보다 월등하게 잘 예측을 하기 때문에, 신경망의 핫하게 된 이유가 되었다. + +개인적으로 필자는 책의 뒷부분에 소개할 신경망의 여러 구조들이 결국에는 신경망의 모수 학습시(실제 함수를 찾아갈 때) 데이터를 넣었을 때 발산하지 않고 실제 함수로 잘 수렴하도록 잘 이끌어주는 신경망 구조를 만들어가는 것이라고 생각하고 있다. 즉, 대략적인 구조를 잡아주고, 그 안에서 데이터를 사용하여 tuning을 하는 방식이다. 어찌보면 이러한 과정은 통계에서 특정 조건을 만족하는 함수들의 집합을 정의하고 (회귀분석의 경우는 선형 함수들만을 생각하고), 그 안에서 최적 모델을 찾아가는 방식과 유사하다. + +## 신경망 학습 + +앞에서 만들어낸 데이터를 사용하여, 신경망을 학습하도록 하자. +신경망을 학습한다는 이야기를 통계적으로 보면 주어진 혹은 설정한 손실 함수(loss function) 값을 최소화 시키는 신경망의 모수(weights)값을 찾는다는 이야기이다. 이런 최적 모수값 찾는 방법에는 여러가지가 있는데, `torch`에서는 이제까지 제안된 많은 방법들이 최적화 함수 (optimizer) 클래스 형식으로 제공이 된다. 당연한 것이겠지만 어떤 최적화 함수를 사용하느냐에 따라서 학습 결과가 달라진다. + +앞에서 정의한 신경망 코드를 가져오자. + +```{python} +# 신경망 정의 +class TwoLayerNet(nn.Module): + def __init__(self, data_in, hidden, data_out): + super(TwoLayerNet, self).__init__() + print("Initiation complete!") + self.hidden1 = nn.Linear(data_in, hidden) + self.hidden2 = nn.Linear(hidden, hidden) + self.hidden3 = nn.Linear(hidden, hidden) + self.output_layer = nn.Linear(hidden, data_out) + self.tanh = nn.Tanh() + + def forward(self, X): + x = self.tanh(self.hidden1(X)) + x = self.tanh(self.hidden2(x)) + x = self.hidden3(x) + y_hat = self.output_layer(x) + return y_hat + +# GPU 사용 가능 여부 확인 +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +print("Device:", device) + +# 데이터 텐서로 변환 +x_tensor = torch.tensor((x - np.mean(x)) / np.std(x), dtype=torch.float32, device=device).view(-1, 1) +y_tensor = torch.tensor(y, dtype=torch.float32, device=device).view(-1, 1) + +# 모델 생성 +D_in, H, D_out = 1, 10, 1 +my_net = TwoLayerNet(D_in, H, D_out).to(device) +print(my_net) +``` + + +### 손실함수와 최적화 방법 선택 + +토치에서는 많은 손실함수 (loss function)와 최적화 함수 (optimizer) 를 [모두 제공](https://pytorch.org/docs/stable/generated/torch.nn.functional.mse_loss.html)하는데, 그 중 가장 기본적인 손실함수인 MSE(Mean Squared Error)와 최적화 방법 SGD(Stochastic Gradient Desent) 방법을 사용하도록 하자. 둘은 다음과 같은 방법으로 선언한다. + +```{python} +# 손실 함수와 최적화 방법 정의 +mse_loss = nn.MSELoss(reduction="mean") +optimizer = optim.SGD(my_net.parameters(), lr=1e-5) +``` + + +손실함수와 최적화 방법에 대한 깊은 내용은 다른 챕터에서 다루도록 하고, 일단 간단하게 정리만 해보자. 지금은 신경망이 어떤 식의 구조를 가진 코드로 학습할 수 있는지 집중한다. + +1. `nn.MSELoss` + +`nn.MSELoss` 함수의 경우, 다음의 두 가지 타입 손실함수를 제공한다. `reduction` 옵션을 `sum` 설정할 경우 손실함수는 다음과 같다. + +$$ +L(\hat{\boldsymbol{y}}, \boldsymbol{y}) = \sum_i^{n}(\hat{y_i}-y_i)^2 +$$ +혹은 `reduction` 옵션을 `mean`으로 설정할 경우 손실함수는 MES를 반환한다. + +$$ +L(\hat{\boldsymbol{y}}, \boldsymbol{y}) = \frac{1}{n}\sum_i^{n}(\hat{y_i}-y_i)^2 +$$ + +참고로 `none`으로 설정시 입력한 두 벡터의 차이의 제곱값들이 벡터 형식으로 나온다. + +앞에 신경망 정의에서 보았듯 히든 레이어를 지날 때, activation 함수를 통과하므로, **로스값 역시 어떤 activation 함수를 사용하느냐에 따라서 달라질 수 있다는 것을 염두해두자**. 주어진 데이터에 대한 손실 함수 값은 다음과 같이 구할 수 있다. + +```{python} +# 손실 함수 테스트 +y_hat = my_net(x_tensor) +print("Initial Loss:", mse_loss(y_hat, y_tensor).item()) +``` + + +1. `optim.SGD()` + +최적화 함수에 대하여는 나중에 따로 포스트로 다루겠다. 현재는 `optim.SGD`가 토치에서 제공하는 최적화 함수 중 하나이며, 입력값으로 신경망의 모수(weights)와 학습률(learning rate), `lr`,을 받는다는 것을 알아두자. + +학습률(learning rate)은 자동 미분 챕터에서 다뤘던 경사하강도 알고리즘을 설명했던 부분에서도 다뤘는데, 신경망 학습 과정에서 중요한 역할을 차지한다. 이것에 따라서 학습이 잘 될 수도, 그렇지 않을 수도 있다. 보통 신경망이 복잡해 질 수록 학습률은 좀 더 세밀한 탐색을 위해 작게 잡아준다. 하지만, 학습률이 작은 경우에는 신경망을 학습하는 시간이 길어지게 된다. 최적은 학습률을 정하는 주제는 학문적으로도 아주 중요하고 방대한 주제이다. 한가지 예만 들면, 굳이 우리가 신경망을 학습시킬때 [학습률을 동일하게 고정할 필요가 있을까?](https://www.jeremyjordan.me/nn-learning-rate/) 어떻게 보면 너무나 중용하고, 실무적인(당장 신경망 학습에 막대한 영향을 미치므로), 연구 주제같다. + +### 학습 구현 + +경사하강법에서 모수가 점점 업데이트 되면서 최적값으로 수렴하는 것을 보았다. 이렇게 업데이트 한번 진행이 되는 단계 단계를 딥러닝에서는 epoch라고 한다. 보통 데이터가 너무 많은 경우 전체 데이터를 한꺼번에 사용하는 것이 아니라 작은 단위로 잘라서 컴퓨터 메모리에 올리게 되는데, 이렇게 작게 잘린 데이터 단위를 배치(batch)라고 하며, 배치의 크기는 배치 안에 몇 개의 데이터가 들어가 있는가를 의미한다. 이와 관련한 내용은 추후에 데이터셋(Dataset) 클래스와 데이터 로더(Data loader) 클래스를 다룰 때 다시 자세하게 이야기하도록 한다. + +다음의 코드는 `mse_loss` 값을 업데이트 단계마다 저장하고, 총 1000번의 모수 업데이트를 수행하여 신경망의 모수를 학습시키는 코드이다. + +```{python} +# 학습 루프 +num_epochs = 1000 +loss_store = [] + +for epoch in range(1, num_epochs + 1): + optimizer.zero_grad() # 기울기 초기화 + output = my_net(x_tensor) # 순전파 + loss = mse_loss(output, y_tensor) # 손실 계산 + loss.backward() # 역전파 + optimizer.step() # 가중치 업데이트 + + # 손실 저장 + loss_store.append(loss.item()) + + # 학습 진행 출력 + if epoch % 100 == 0: + print(f"Loss at epoch {epoch}: {loss.item():.2f}") +``` + + + +### 시각화 + +이전 섹션에서 우리는 신경망의 학습이 진행되면서 손실함수(loss)값이 점점 줄어드는 것을 확인할 수 있었다. 최종적으로 학습된 신경망은 어떻게 생겼을까? . + +```{python vis-result, cache=TRUE, echo=FALSE, message=FALSE, fig.cap="학습된 신경망과 회귀직선 비교", fig.align='center', out.width = '100%'} + +# 학습된 신경망 결과 시각화 +x_predict = torch.tensor((np.arange(1, 101) - np.mean(x)) / np.std(x), dtype=torch.float32, device=device).view(-1, 1) +y_hat = my_net(x_predict).cpu().detach().numpy() + +# 학습된 신경망 결과 +plt.scatter(x, y, color="#E69F00", label="Observed Data") +plt.plot(x, f(x), linestyle="--", label="True Function (f(x))") +plt.plot(x, y_hat, color="blue", label="Learned Function (Neural Network)") +plt.xlabel("x") +plt.ylabel("f(x)") +plt.legend() +plt.show() +``` + +학습된 신경망이 데이터가 발생되는 함수의 비선형성을 잘 반영하고 있는 것을 확인할 수 있다. + +## 과적합(overfitting)과의 싸움 + +이렇게 학습한 신경망은 너무나도 완벽해 보이지만, 사실 중대한 문제점이 있다. 바로 학습에 사용된 데이터에 나타난 패턴을 너무나도 잘 반영하고 있는 것이 문제이다. 사실 뭐가 문제냐 싶지만, 우리가 흔히 말하는 "이론과 현실은 달라요." 라는 말이 신경망 학습에서도 그대로 적용이 된다고 생각하면 된다. + +즉, 학습 데이터를 너무나 잘 반영하는 것도 좋지만, 이렇게 학습된 신경망을 사용해서 예측을 할 때, 신경망에 입력 될 데이터는 대부분 학습 데이터와 비슷하기도 하겠지만, 비슷하지 않은 전혀 다른 입력값이 들어올 수 있다. 이런 상황에 잘 대비하기(?) 위해서 혹은 신경망이 성능을 잘 내기 위해서는 신경망을 학습을 할 때 성과측정을 신경망의 학습에 한번도 사용되지 않는 새로운 데이터로 평가를 해야만 한다. + +이렇게 모델이 학습 데이터 패턴을 너무나 많이 반영하고 있는 현상을 과하게 적합이 되어 있다고 하여 모델 과적합(overfitting) 상태라고 부른다. 기계학습과 딥러닝에서는 일단 베이스라인 모델이 정해진 후에는 어떤 모수값(weights)이 최적의 모수인지를 찾아내야하는 과정을 거친다. 이 과정을 거치는 가장 큰 이유는 모델 과적합 방지에 있다. 어떻게 하면 학습 데이터의 패턴은 잘 반영하면서, 새로이 들어올 데이터에도 잘 반응할 수 있는 모델을 세울 수 있을지 앞으로의 학습을 통해서 차근차근 배워보자. diff --git a/09-dataloader.qmd b/09-dataloader.qmd new file mode 100644 index 0000000..66d2973 --- /dev/null +++ b/09-dataloader.qmd @@ -0,0 +1,419 @@ +# `Dataset`과 `Dataloader` 클래스 + +이번 챕터에서는 실제 딥러닝을 사용하여 모델링을 할 때 사용될 데이터를 어떻게 `torch`에 넣을수 있는지에 대하여 알아보자. 이 과정에서 우리가 알아야하는 클래스가 두개가 있는데, 바로 `Dataset` 클래스와 `Dataloader` 클래스이다. + +## 예제 데이터 + +언제나 그렇듯, 본 공략집은 예제를 통해서 설명하는 것을 선호한다. 이번 챕터에서는 가상의 학생들의 공부시간과 연습한 문제 갯수, 그리고 시험의 합격 여부에 대한 자료를 만들어보았다. + +```{python} +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import Dataset, DataLoader +``` + +```{python} +import numpy as np +import matplotlib.pyplot as plt +import pandas as pd + +# 데이터 생성 +np.random.seed(2021) + +# 독립변수 생성 +x1 = np.concatenate([np.random.normal(5, 2, 300), np.random.normal(10, 2, 200)]) +x2 = np.maximum(np.random.normal(10, 2, 300), 0).astype(int) +x2 = np.concatenate([x2, np.maximum(np.random.normal(13, 2, 200), 0).astype(int)]) + +# 종속변수 생성 +y = np.concatenate([np.zeros(300), np.ones(200)]) + +# 데이터프레임 생성 +data = pd.DataFrame({'study_time': x1, 'n_question': x2, 'pass_exam': y}) +data = data.sample(frac=1, random_state=2021).reset_index(drop=True) + +``` + + +### 데이터 나누기 + +이전 챕터에서 우리는 전체 데이터를 모두 사용하여 신경망을 학습했었다. 하지만, 이럴 경우 과적합의 문제가 발생하기 때문에 언제 학습을 할 때, 우리 모델이 새로운 데이터에 얼마나 잘 되어있는지, 혹시 학습이 과적합은 일어나고 있는게 아닌지 판단해야 한다. 이런 것들을 하기 위해서 주어진 데이터를 두 개로 쪼갠다; 하나는 학습용(train data set), 하나는 평가용(test data set)으로 나눈다. + +다음의 코드는 주어진 `study_data`의 70%는 학습용, 30%는 평가용으로 나누는 코드이다. + +```{python} +from sklearn.model_selection import train_test_split +# 데이터 나누기 (train 70%, test 30%) +train_data, test_data = train_test_split(data, test_size=0.3, random_state=2021) + +print(f"Train data shape: {train_data.shape}") +print(f"Test data shape: {test_data.shape}") +``` + +### 시각화 + +학습용 데이터를 사용하여 데이터를 시각화보면 다음과 같다. + +```{python echo=FALSE, fig.align="center", fig.cap="`study_data` 학습용 데이터(train data) 시각화"} +import matplotlib.pyplot as plt +import seaborn as sns + +# 학습용 데이터 시각화 +plt.figure(figsize=(8, 6)) +sns.scatterplot( + data=train_data, + x="study_time", + y="n_question", + hue="pass_exam", + palette={0: "red", 1: "blue"}, + alpha=0.7 +) + +plt.xlabel("Study Time") +plt.ylabel("Number of Questions") +plt.title("Train Data Visualization") +plt.legend(title="Exam", labels=["Fail", "Success"], loc="upper left") +plt.grid(alpha=0.3) +plt.show() +``` + +## `Dataset` 클래스 + +`Dataset` 클래스는 우리가 가지고 있는 데이터를 `torch`에서 접근할 때 어떻게 접근을 해야하는지 알려줄때 사용한다. 예를 들어, 예제 데이터와 같이 행과 열로 구성된 데이터에서 어떤 열이 독립변수(independant variables)를 의미하고 있는지, 어떤 열이 종속변수(dependant variable)를 의미하는지 알려줘야하고, 만약 모델에 넣기 전, 특정 전처리가 필요하다면, 그 과정 역시 넣어줄 수 있다. + +다음은 `dataset` 함수를 사용하여 `StudyDataset` 클래스 생성자를 정의하는 코드이다. 일단 `StudyDataset` 생성자는 객체를 만들때, `__init__`가 실행된다. 이것을 통하여 결과값이 클래스의 `x`, `y`로 저장된다. + +```{python} +from torch.utils.data import Dataset, DataLoader + +class StudyDataset(Dataset): + def __init__(self, data): + # 데이터를 Tensor로 변환 + self.x = torch.tensor(data[["study_time", "n_question"]].values, dtype=torch.float32) + self.y = torch.tensor(data["pass_exam"].values, dtype=torch.float32).view(-1, 1) + + def __len__(self): + return len(self.x) + + def __getitem__(self, index): + return self.x[index], self.y[index] + +# Dataset 객체 생성 +train_dataset = StudyDataset(train_data) +test_dataset = StudyDataset(test_data) + +test_loader = DataLoader(test_dataset, batch_size=8, shuffle=False) +``` + + +위의 코드를 살펴보면 앞에서 정의한 `data` 데이터셋을 가져온다. 그리고, `__getitem__()`과 `__len__()`의 멤버함수를 통하여 데이터에 접근할 수 있도록 만들어져 있다. 정의된 `StudyDataset()` 클래스 생성자를 통하여 데이터를 만들어보자. + +```{python} +# Dataset 클래스 생성 및 객체 생성 +study_dataset = StudyDataset(train_data) +# Dataset 내용 확인 +print(study_dataset) +``` + +```{python} +# 특정 인덱스 데이터 확인 +for i in range(6): + x, y = study_dataset[i] + print(f"Index {i}: x = {x.numpy()}, y = {y.numpy()}") + +# 원래 train_data 확인 +print(train_data.head(6)) +``` + +위 코드를 보면 study_dataset[i]를 통해 특정 인덱스 i의 데이터를 확인합니다. 그 다음 numpy()를 사용하여 PyTorch 텐서를 numpy 배열로 변환하여 값을 출력합니다. 마지막으로, 원래의 train_data 데이터프레임과 비교하여 데이터가 잘 전달되었는지 확인합니다. + +## 데이터로더 (Dataloader) 클래스 + +`study_dataset()` 생성자를 통하여 데이터를 `torch` 상에서 접근하도록 만들었다. 앞선 신경망 학습 예제에서 신경망을 학습할 때, 주어진 데이터 전체를 사용하여 미분값(gradient)을 구했다. 하지만, 실제 많은 딥러닝 문제의 경우 데이터의 크기가 너무 커서 한꺼번에 모든 표본을 메모리에 올린 후 학습을 하지 않고, 잘게 쪼갠 여러 개의 배치(batch)를 만든 뒤에 학습을 진행한다. + +다음 차례는 torch에서 신경망을 학습시킬때 데이터의 부분부분을 잘라서 접근 할 수 있도록 만들어 줘야하는데, 이 부분은 `dataloader` 클래스에서 담당하고 있다. 다음의 코드는 앞에서 정의한 `torch_data`를 학습할 때 한번에 불러오는 표본 갯수(batch_size)를 8개로 설정한다. + +```{python} +train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True) +``` + +8개를 기준으로 한 세트를 이루므로, 전체 350개의 표본은 총 44개의 `batch`로 이루어져 있다는 것을 확인 할 수 있다. + +```{block, type='rmdwarning'} +### 주의하기 +마지막 배치의 경우는 8개가 아닌 6개의 표본들로 이루어져 있다는 것도 짐작할 수 있어야 한다. +``` + +```{python} +print(train_loader.__len__()) +``` + + +## 모델 설정 - 로지스틱 회귀모형 + +자료를 `torch`에 보내고, 어떻게 접근하는지까지 알아보았다. 이번에는 분류 문제를 푸는 통계 모델 중에서 가장 유명한 모델인 로지스틱 회귀모형을 `torch`로 정의해보자. 로지스틱 회귀모형은 일반화 선형모형(GLM)의 한 종류이다. 이름에서도 느껴지겠지만, 이 모형은 선형모형의 연장선에 있다. 왜 연장선 상에 있다고 하는 것일까? + +일반적인 회귀모형에서는 종속변수인 $Y$값이 정규분포를 따른다고 가정한다. 왜냐하면 데이터의 발생이 다음과 같은 가정에서 출발하기 때문이다. + +$$ +Y = X \beta + \epsilon, \quad \epsilon \sim \mathcal{N}(0, \sigma^2I) +$$ + +이것을 $Y$의 입장에서 생각해보면, 결국 $Y$라는 확률변수는 정규분포를 따르고, $X$가 정해졌을때의 평균값은 $X\beta$가 된다. + +$$ +Y \sim \mathcal{N}(X\beta, \sigma^2 I) +$$ +따라서, 다음의 관계가 성립하게 된다. + +$$ +\mathbb{E}[Y|X] = X\beta = f(X\beta) +$$ + +위의 등식에서 $f(x)$는 $f(x)=x$인 항등함수(identity function)를 나타낸다. 즉, 일반적인 회귀모형의 경우, 종속변수 $Y$를 정규분포를 따르는 확률변수로 생각하고 있고, 그 평균과 $X\beta$를 항등함수로 이어놓은 형태인 것이다. 만약 우리가 종속변수를 다른 확률변수라고 생각하고, 그것의 평균을 이어주는 어떤 함수 $f$를 찾는다면 어떨까? + +수리 통계학에서 가장 먼저 배우는 함수 중 하나가 바로 0과 1을 갖는 베르누이 확률변수인데, 베르누이 확률변수의 평균은 바로 1이 나오는 확률을 의미하는 $p$이다. $p$값이 그 의미상 0과 1사이에 위치해야 하므로, $X\beta$에서 나오는 값들을 0과 1사이로 모아줘야 하는데 여기서 시그모이드(sigmoid) 함수, $\sigma(x)$,를 사용한다. + +$$ +f(x) := \sigma(x) = \frac{e^x}{1+e^x} +$$ + +따라서, 로지스틱 회귀모형에서 종속변수 $Y$는 다음과 같이 모수 $\sigma(X\beta)$인 베르누이 확률변수를 따른다고 생각하면 된다. + +$$ +Y \sim Bernulli(\sigma(X\beta)) +$$ + +```{block logistic, type='rmdnote'} + +### 알아두기 + +로지스틱 회귀모형은 종속변수를 베르누이 확률변수로 가정하고, 그것의 평균인 $p$와 $X\beta$를 시그모이드(sigmoid) 함수로 이어놓은 형태로 볼 수 있다. + +``` + + +문제는 왜 그럼 로지스틱 회귀모형인가 하는건데, [사실은 시그모이드(sigmoid) 함수가 바로 로지스틱 함수이기 때문이다.](https://en.wikipedia.org/wiki/Sigmoid_function) 좀 더 엄밀하게 말하면 우리가 알고 있는 시그모이드 함수의 정확한 명칭은 로지스틱 함수이고, 시그모이드 함수는 S 자 곡선 형태를 띄는 함수들을 통칭해서 부르는 말이다. 우리가 딥러닝에서 많이 쓰는 활성함수(activation function)중 하나인 `Hyperbolic tangent` 역시 시그모이드 함수이다. + +```{block, type='rmdwarning'} +### 주의하기 +우리가 알고 있는 시그모이드(sigmoid) 함수의 정확한 명칭은 로지스틱(logistic) 함수이다. +``` + +### torch 코드 구현 + +이제 `torch`로 특정 신경망 구조를 구현하는 코드는 익숙해졌으리라고 생각한다. 로지스틱 회귀모형은 단층 모형이고, 마지막 활성함수를 `sigmoid()` 함수로 감싸줘야하는 것이 특징이다. + +```{python} +import torch.nn as nn + +class LogisticRegressionModel(nn.Module): + def __init__(self): + super(LogisticRegressionModel, self).__init__() + self.fc = nn.Linear(2, 1) # 입력 변수 2개, 출력 변수 1개 + self.sigmoid = nn.Sigmoid() # 시그모이드 활성화 함수 + + def forward(self, x): + return self.sigmoid(self.fc(x)) + +# 모델 초기화 +model = LogisticRegressionModel() +print(model) +``` + + +코드에서 볼 수 있다시피, 로지스틱 회귀모형은 입력값을 독립변수 갯수인 2개로 받고, 출력값은 하나로 나가는 모형이다. 마지막 층에 `sigmoid()` 함수는 마지막 출력값을 0과 1사이로 보내기 위하여 사용되었다. 최적화 알고리즘은 `optim.SGD`으로 설정하였다. + +```{python} +# 최적화 함수 (Stochastic Gradient Descent) +optimizer = torch.optim.SGD(model.parameters(), lr=0.05) +``` + + +### 손실함수 설정 + +로지스틱 회귀모형의 구현에서 핵심 파트는 손실함수(loss function)를 설정하는 부분이다. 앞에서 로지스틱 회귀모형이 종속변수 $Y$를 베르누이 확률변수(Bernoulli random variable)로 모델링을 한다는 것을 살펴보았다. + +로지스틱 회귀분석의 계수를 구하기 위해서는 우도함수(Likelihood function)를 정의한 후, 그것을 최대로 만드는 최대우도 추정량(Maximum likelihood estimator; MLE) 값을 찾아야 한다. + +확률변서 $Y$가 베르누이 확률변수를 따를 때, 확률질량함수(p.m.f)는 다음과 같다. + +$$ +f_Y(y; p) = p^{y}(1-p)^{1-y}, \text{ for }y = 1, 0, \text{ and } 0 \le p \le 1. +$$ +따라서, 로지스틱 회귀모형의 가정을 위의 확률질량함수와 같이 생각해보면, 주어진 데이터에 대한 우도함수 $p$는 다음과 같다. + +$$ +\begin{align} +p\left(\beta|\mathbf{X}, \underline{y}\right) & =\prod_{i=1}^{n}p\left(\beta|\mathbf{x}_{i},y_{i}\right)\\ + & =\prod_{i=1}^{n}\sigma\left(\mathbf{x}_{i}^{T}\beta\right)^{y_{i}}\left(1-\sigma\left(\mathbf{x}_{i}^{T}\beta\right)\right)^{1-y_{i}}\\ + & =\prod_{i=1}^{n}\pi_{i}^{y_{i}}\left(1-\pi_{i}\right)^{1-y_{i}} +\end{align} +$$ +위의 수식에서 계산의 편의를 위하여 $\pi_i$를 사용하여 다음의 항을 간단히 표현했음에 주의한다. + +$$ +\pi_{i}:=\sigma\left(\mathbf{x}_{i}^{T}\beta\right) +$$ + +보통의 최적화 알고리즘의 경우, 손실함수 값을 최소로 만드는 값을 찾는 알고리즘이다. 하지만 MLE의 경우 주어진 우도함수를 최대로 만드는 값이기 때문에 최적화 알고리즘에 사용할 수 있도록 음수값을 붙여주고, 함수를 좀 더 완만하게 만들기 위해서 로그값을 취해준, 음우도함수(negative log-likelihood function)을 사용한다. + +$$ +\begin{align*} +-\ell\left(\beta\right) & =-log\left(p\left(\underline{y}|\mathbf{X},\beta\right)\right)\\ + & =-\sum_{i=1}^{n}\left\{ y_{i}log\left(\sigma\left(\mathbf{x}_{i}^{T}\beta\right)\right)+\left(1-y_{i}\right)log\left(1-\sigma\left(\mathbf{x}_{i}^{T}\beta\right)\right)\right\} \\ + & =-\sum_{i=1}^{n}\left\{ y_{i}log\left(\pi_{i}\right)+\left(1-y_{i}\right)log\left(1-\pi_{i}\right)\right\} +\end{align*} +$$ + +위의 함수 $-\ell(\cdot)$은 `torch`에서 `nnf_binary_cross_entropy()` 함수에 정의되어 있다. 따라서 로지스틱 회귀모형의 계수는 다음과 같이 데이터 행렬($X$)과 레이블($y$)가 주어졌을때 $-\ell(\cdot)$ 함수를 최소로 만드는 $\beta$값으로 표현된다. + +$$ +\hat{\beta} \overset{set}{=} \underset{\beta}{arg \ min} \ \ -\ell\left(\beta; X, y\right) +$$ +이 값을 찾기 위해서 경사하강법을 이용해 $\hat{\beta}$을 찾아나아가는 과정이 `torch`에서는 단 두 줄로 표현이 된다. + +```{python} +# 손실 함수 (Binary Cross-Entropy Loss) +criterion = nn.BCELoss() +``` + +앞에서 정의한 데이터로더를 사용하여 로지스틱 회귀모형을 학습시키는 코드는 다음과 같다. + +```{python} +# 학습 함수 +def train_model(model, train_loader, criterion, optimizer, epochs=1000): + model.train() + for epoch in range(epochs): + epoch_loss = 0 + for x_batch, y_batch in train_loader: + optimizer.zero_grad() # 그래디언트 초기화 + y_pred = model(x_batch) # 예측값 계산 + loss = criterion(y_pred, y_batch) # 손실 계산 + loss.backward() # 역전파 + optimizer.step() # 가중치 업데이트 + epoch_loss += loss.item() + if (epoch + 1) % 100 == 0: + print(f"Epoch {epoch + 1}/{epochs}, Loss: {epoch_loss / len(train_loader):.4f}") + +# 모델 학습 실행 +train_model(model, train_loader, criterion, optimizer, epochs=1000) +``` + + +## 학습 결과 + +```{python} +# 평가 함수 +def evaluate_model(model, test_loader): + model.eval() + predictions, actuals = [], [] + with torch.no_grad(): + for x_batch, y_batch in test_loader: + y_pred = model(x_batch) + predictions.extend(y_pred.squeeze().numpy()) + actuals.extend(y_batch.squeeze().numpy()) + return np.array(predictions), np.array(actuals) + +# 평가 실행 +predictions, actuals = evaluate_model(model, test_loader) +``` + +학습된 로지스틱 회귀모형의 계수값을 사용하여 평가셋의 반응 변수가 1일 확률, 즉, 시험에 통과할 확률을 예측 할 수 있다. + +예측한 확률값이 0.5가 넘을 경우, 학생이 시험에 통과를 할 수 있다고 예측을 하고, 그렇지 않을 경우 통과하지 못한다고 예측해보자. + +```{python} +# 예측값 이진화 +predicted_classes = (predictions >= 0.5).astype(int) +``` + +이렇게 예측한 값과 실제 평가셋에 들어있는 학생들의 시험 통과 여부값을 사용하여 결과 비교하여 표로 만들어보면 다음과 같다. + +```{python} +from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay + +# 혼동 행렬 계산 +cm = confusion_matrix(actuals, predicted_classes) +disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=["Fail", "Pass"]) +disp.plot(cmap="Blues") +plt.title("Confusion Matrix") +plt.show() +``` + + +아래 그림에서처럼 로지스틱 함수는 입력값이 0일때 함수값이 0.5가 된다. 즉, 로지스틱 회귀에서 $X\beta$값이 로지스틱 함수에 입력이 되므로, $X\beta$ 값이 0이 되는 값들을 경계로 모델의 합격 불합격 예측값이 갈리는 것이다. + +```{python} +import numpy as np +import matplotlib.pyplot as plt + +# 데이터 생성 +x = np.arange(-5, 5.1, 0.1) +y = 1 / (1 + np.exp(-x)) # 시그모이드 함수 + +# 시각화 +plt.figure(figsize=(8, 6)) +plt.plot(x, y, label="Sigmoid Function") +plt.axhline(0.5, color="red", linestyle="--", label="y = 0.5") +plt.axvline(0, color="red", linestyle="--", label="x = 0") +plt.axhline(0, color="black") +plt.axvline(0, color="black") +plt.text(-3, 0.55, "y = 0.5", color="red", fontsize=10) +plt.text(1.5, 0.05, "x = 0", color="red", fontsize=10) +plt.xlabel("x") +plt.ylabel("Sigmoid(x)") +plt.title("Sigmoid Function with Decision Boundary") +plt.legend() +plt.grid() +plt.show() +``` + + +이러한 선을 의사결정선(decision boundary)라고 부른다. + +$$ +\hat{\beta}_0 + \hat{\beta}_1 x_1 + \hat{\beta}_2 x_2 = 0 +$$ + +주어진 예제의 평가셋을 시각화 시키고, 학습한 계수를 바탕으로 의사결정선을 구해보면 다음과 같다. + +```{python} +# 학습된 계수와 절편 +learned_beta = model.fc.weight.detach().numpy().flatten() +learned_bias = model.fc.bias.detach().numpy().item() + +# 의사결정선 계산 함수 +def decision_boundary(beta, bias, x_vals): + slope = -beta[0] / beta[1] + intercept = -bias / beta[1] + return slope * x_vals + intercept + +# 평가 데이터 시각화 및 의사결정선 추가 +plt.figure(figsize=(8, 6)) +sns.scatterplot( + data=test_data, + x="study_time", + y="n_question", + hue="pass_exam", + palette={0: "red", 1: "blue"}, + alpha=0.7, + legend="full" +) + +# 의사결정선 추가 +x_vals = np.linspace(test_data["study_time"].min(), test_data["study_time"].max(), 100) +y_vals = decision_boundary(learned_beta, learned_bias, x_vals) +plt.plot(x_vals, y_vals, color="black", linestyle="--", label="Decision Boundary") + +# 설정 +plt.xlabel("Study Time") +plt.ylabel("Number of Questions") +plt.title("Test Data with Decision Boundary") +plt.legend(title="Exam Result", labels=["Fail", "Pass", "Decision Boundary"], loc="upper left") +plt.grid(alpha=0.3) +plt.show() +``` + + From 1e351bfa52d35cf072483144bfb3e1ed2cc3e358 Mon Sep 17 00:00:00 2001 From: SANGDONKIM Date: Wed, 25 Dec 2024 01:18:39 +0900 Subject: [PATCH 2/2] python qmd update --- 09-dataloader.Rmd | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/09-dataloader.Rmd b/09-dataloader.Rmd index a6637a5..2543aef 100644 --- a/09-dataloader.Rmd +++ b/09-dataloader.Rmd @@ -354,24 +354,24 @@ knitr::kable(confusion_mat, format="html", booktabs = TRUE, 아래 그림에서처럼 로지스틱 함수는 입력값이 0일때 함수값이 0.5가 된다. 즉, 로지스틱 회귀에서 $X\beta$값이 로지스틱 함수에 입력이 되므로, $X\beta$ 값이 0이 되는 값들을 경계로 모델의 합격 불합격 예측값이 갈리는 것이다. ```{r logistic-fcn, echo=FALSE, fig.align="center", fig.cap="로지스틱 함수는 x가 0일때 0.5를 지나게 된다."} -x <- seq(-5, 5, by = 0.1) -y <- 1 / (1 + exp(-x)) - -ggplot(data = tibble(x = x, y = y), aes(x = x, y = y)) + - geom_line() + - geom_hline(yintercept = 0.5, - linetype = "dashed", col = "red") + - geom_vline(xintercept = 0, linetype = "dashed", col = "red") + - geom_vline(xintercept = 0) + - geom_hline(yintercept = 0) + - annotate("text", - x = - 3, - y = 0.5 + 0.05, - label = paste("y =", round(0.5, 3))) + - annotate("text", - x = 1.5, - y = 0.05, - label = paste("x =", 0)) + x <- seq(-5, 5, by = 0.1) + y <- 1 / (1 + exp(-x)) + + ggplot(data = tibble(x = x, y = y), aes(x = x, y = y)) + + geom_line() + + geom_hline(yintercept = 0.5, + linetype = "dashed", col = "red") + + geom_vline(xintercept = 0, linetype = "dashed", col = "red") + + geom_vline(xintercept = 0) + + geom_hline(yintercept = 0) + + annotate("text", + x = - 3, + y = 0.5 + 0.05, + label = paste("y =", round(0.5, 3))) + + annotate("text", + x = 1.5, + y = 0.05, + label = paste("x =", 0)) ``` 이러한 선을 의사결정선(decision boundary)라고 부른다.