분류기의 학습 과정 내부 살펴보기

JH.SUNG·2021년 12월 7일
0

인공지능(머신러닝)

목록 보기
11/13
post-thumbnail

분류기의 학습 과정 내부 분석

mnist 데이터 불러오기

path = untar_data(URLs.MNIST_SAMPLE)

mnist 샘플 데이터셋 다운로드 & 압축풀기

path.ls()

(#3) [Path('valid'),Path('labels.csv'),Path('train')]

훈련데이터, 검증데이터 폴더 와 라벨이 적힌 csv가 있다.

threes = (path/'train'/'3').ls().sorted()
sevens = (path/'train'/'7').ls().sorted()
threes

(#6131) [Path('train/3/10.png'),Path('train/3/10000.png'),Path('train/3/10011.png'),Path('train/3/10031.png'),Path('train/3/10034.png'),

각 폴더 안에는 숫자별로 폴더가 나뉘어져 있고, 그 안에는 수많은 숫자 이미지 파일이 담겨져 있다.

im3_path = threes[1]
im3 = Image.open(im3_path)
im3

threes에는 숫자 3의 훈련데이터가 리스트 형태로 담겨있고, 이중에 하나를 불러와서 이미지로 확인해본다.

Image는 파이썬 영상 처리 라이브러리(python Imaging Library)에서 제공하는 이미지를 열고 조작할때 사용하는 클래스이다.

array(im3)[4:10,4:10]

array([[  0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,  29],
       [  0,   0,   0,  48, 166, 224],
       [  0,  93, 244, 249, 253, 187],
       [  0, 107, 253, 253, 230,  48],
       [  0,   3,  20,  20,  15,   0]], dtype=uint8)
       
       
tensor(im3)[4:10,4:10]       

tensor([[  0,   0,   0,   0,   0,   0],
        [  0,   0,   0,   0,   0,  29],
        [  0,   0,   0,  48, 166, 224],
        [  0,  93, 244, 249, 253, 187],
        [  0, 107, 253, 253, 230,  48],
        [  0,   3,  20,  20,  15,   0]], dtype=torch.uint8)
        

컴퓨터는 이미지를 숫자로 인식하고, 이미지는 넘파이 배열 혹은 파이토치 텐서를 이용하여 숫자로 변환된다.

im3_t = tensor(im3)
df = pd.DataFrame(im3_t[4:25,4:32])
df.style.set_properties(**{'font-size':'6pt'}).background_gradient('Greys')

판다스 데이터프레임을 통해서 숫자의 값에 따라 그라데이션 색상으로 표현하였다.

픽셀 유사성

seven_tensors = [tensor(Image.open(o)) for o in sevens]
three_tensors = [tensor(Image.open(o)) for o in threes]
len(three_tensors),len(seven_tensors)

(6131, 6265)

리스트 컴프리헨션(List Comprehension)을 통해 위의 모든 이미지들을 텐서 형태로 변환하여 리스트에 담았습니다.
아래 숫자는 이미지들의 개수 입니다.

three_tensors[0]

이미지가 세밀하게 나누어져서(28*28) 숫자의 형태로 표현되었습니다.

stacked_sevens = torch.stack(seven_tensors).float()/255
stacked_threes = torch.stack(three_tensors).float()/255
stacked_threes.shape

torch.Size([6131, 28, 28])

전체 이미지의 평균을 구하여 개별 이미지와 비교하려고 하는데, 그럴려면 리스트 내의 모든 이미지를 3차원 텐서 형태로 결합해야 합니다. 3차원 텐서는 랭크3 텐서라고 합니다. 이를 위해서 파이토치의 stack이라는 함수를 이용합니다.

픽셀값을 0~1로 맞춰주기 위해서 255로 나누어 줍니다. 0~1값으로 되기 위해서 float를 적용해 부동소수형으로 형 변환을 해줍니다.

len(stacked_threes.shape)

3

stacked_threes.ndim

3

shape 는 텐서 각 축의 크기를 표현한다.
ndim 속성을 통해 텐서의 랭크를 확인할 수 있다.

mean3 = stacked_threes.mean(0)
show_image(mean3);


랭크3 텐서에서 0번째 차원(0)의 평균을 구해서 모든 이미지 텐서의 평균을 계산합니다.
위의 이미지는 이 과정에서 출력된 평균 숫자 3의 모습입니다.

mean7 = stacked_sevens.mean(0)
show_image(mean7);
mean7.shape

torch.Size([28, 28])

숫자 7의 평균 이미지 모습입니다.

![](https://velog.velcdn.com/images%2Fmn99134%2Fpost%2Fe06176af-0ecd-469c-ab5b-63990409635a%2Fimage.png)


임의로 선택한 숫자3의 샘플입니다.
이 샘플과 위의 3의평균,7의평균과의 거리를 비교해서 어느쪽과 더 가까운지를 판단하는 방식으로 분류를 해보려고 합니다.

거리를 비교하는 방식에는 2가지가 있습니다.
L1 평균절대차(mean absolute difference)
-차이의 절대값에 대한 평균
L2 평균제곱근오차(root mean squared error)
-차이의 제곱에 대한 평균의 제곱근

L2(평균제곱근오차)는 L1(평균절대차)에 비해서 큰 실수에 강한 페널티를 부과하고, 작은 페널티에 상대적으로 관대한 방법이다.

dist_3_abs = (a_3 - mean3).abs().mean()
dist_3_sqr = ((a_3 - mean3)**2).mean().sqrt()
dist_3_abs,dist_3_sqr

(tensor(0.1114), tensor(0.2021))

dist_7_abs = (a_3 - mean7).abs().mean()
dist_7_sqr = ((a_3 - mean7)**2).mean().sqrt()
dist_7_abs,dist_7_sqr

(tensor(0.1586), tensor(0.3021))

위의 샘플 이미지는 평균3에 대한 거리가 7에 대한 거리보다 짧다. 그러면 3에 가까운 이미지라는 의미이고, 샘플은 3에서 뽑았으니 모델이 올바르게 작동했다고 볼 수 있다.

F.l1_loss(a_3.float(),mean7), F.mse_loss(a_3,mean7).sqrt()

(tensor(0.1586), tensor(0.3021))

파이토치에서 이 2가지 방법에 대한 손실 함수를 제공한다.
MSE - 평균 제곱오차
L1 - 절대평균값

넘파이 배열과 파이토치 텐서

넘파이는 컴퓨터 과학에서 가장 널리 사용되는 라이브러리이다.
파이토치의 텐서는 넘파이의 배열과 비슷하지만 GPU를 사용한 그레디언트 계산 기능까지 제공한다.

배열과 텐서가 중요한 이유는 이들이 파이썬으로 래핑되어 있지만 실제 내부 코드는 C로 되어 있어, 순수 파이썬에 비해 수천 배 빠르게 계산하기 때문이다.

그 중에서도 GPU적재를 통한 빠른 계산 그리고 자동으로 미분 계산(그레디언트)라는 2가지 장점때문에 넘파이 대신에 파이토치 텐서를 사용한다.

import numpy as np
data = [[1,2,3],[4,5,6]]
arr = array (data)
tns = tensor(data)

array([[1, 2, 3],
       [4, 5, 6]]) # 넘파이
       
tensor([[1, 2, 3],
        [4, 5, 6]]) #파이토치
tns[1]

tensor([4, 5, 6])

tns[:,1]

tensor([2, 5])

콜론(:)으로 열을 선택할 수있다. 첫번째 콜론은 첫번째 축의 모든 것을 의미한다.

tns.type() #torch.LongTensor

tns+1

tensor([[2, 3, 4],
        [5, 6, 7]])
        
tns*1.5        

tensor([[1.5000, 3.0000, 4.5000],
        [6.0000, 7.5000, 9.0000]])

텐서에는 자료형이 있으며, 표준 연산자도 사용할 수 있다.

브로드캐스팅

평가지표란 레이블과 모델이 도출한 예측을 비교해서 모델이 얼마나 좋은지를 평가하는 숫자이다.
앞서 살펴본 지표들은 직관적으로 이해하기 어려우므로 정확도(accuracy)를 분류 모델의 평가지표로 사용한다.
또한 평가지표를 계산하려는 대상은 우리가 훈련한 데이터가 아니고 검증용 데이터셋이다. 그 이유는 우리가 훈련한 데이터에 대한 과적합(overfit)을 피하기 위해서이다.

valid_3_tens = torch.stack([tensor(Image.open(o)) 
                            for o in (path/'valid'/'3').ls()])
valid_3_tens = valid_3_tens.float()/255
valid_7_tens = torch.stack([tensor(Image.open(o)) 
                            for o in (path/'valid'/'7').ls()])
valid_7_tens = valid_7_tens.float()/255
valid_3_tens.shape,valid_7_tens.shape

(torch.Size([1010, 28, 28]), torch.Size([1028, 28, 28]))

검증용 데이터셋을 텐서로 변환한다.

def mnist_distance(a,b): return (a-b).abs().mean((-1,-2))
mnist_distance(a_3, mean3)

tensor(0.1114)

입력된 검증용 이미지가 3인지 7인지 판단하는 함수를 만든다.

valid_3_dist = mnist_distance(valid_3_tens, mean3)
valid_3_dist, valid_3_dist.shape

(tensor([0.1488, 0.1113, 0.1495,  ..., 0.1202, 0.1378, 0.1117]),
 torch.Size([1010]))

브로딩캐스팅을 통해서 낮은 랭크의 텐서를 높은 랭크의 텐서와 같은 크기로 자동 확장한다.

tensor([1,2,3]) + tensor(1)

tensor([2, 3, 4])

브로딩캐스팅의 예

(valid_3_tens-mean3).shape

torch.Size([1010, 28, 28])

파이토치는 mean3를 1,010번 복사하지 않는다. 그 모양의 텐서이인 척하지만, 실제로 메모리를 할당하지 않는다.

텐서의 개별 요소마다 abs메소드가 적용된다. 그리고 mean(-1,-2)를 통해서 모든 값에 대한 평균을 구한다.
-1,-2는 축의 범위를 의미한다. -1은 마지막, -2는 마지막에서 두번째라는 의미이고, 이겨시넌 가로와 세로를 의미한다.

def is_3(x): return mnist_distance(x,mean3) < mnist_distance(x,mean7)

is_3(a_3), is_3(a_3).float()
is_3(valid_3_tens)

tensor([True, True, True,  ..., True, True, True])

is_3 함수와 브로드캐스팅을 통해서 검증용 데이터셋 전체에 대하여 3과 7중에 어디가 가까운지 판단할 수 있다.

accuracy_3s =      is_3(valid_3_tens).float() .mean()
accuracy_7s = (1 - is_3(valid_7_tens).float()).mean()

accuracy_3s,accuracy_7s,(accuracy_3s+accuracy_7s)/2

(tensor(0.9168), tensor(0.9854), tensor(0.9511))

3과 7에서 모두 90%가 넘는 정확도를 얻었다.

확률적 경사 하강법(SGD)

def pr_eight(x,w): return (x*w).sum()

최종 목표는 숫자3에서는 높지만 그 외의 숫자에서는 낮은 W(가중치)값을 찾는 것이다.

머신러닝 분류 모델을 만드는 과정

  1. 가중치를 초기화합니다.
  2. 현재 가중치로 이미지가 3인지 7인지를 예측합니다.
  3. 예측한 결과로 모델이 얼마나 좋은지 계산합니다.(손실 측정)
  4. 가중치 갱신 정도가 손실에 미치는 영향을 측정하는 그레디언트를 계산합니다.
  5. 4번 단게에서 게산한 그레디언트로 가중치의 값을 한 단계(step) 조정합니다.
  6. 2번 단계로 돌아가서 과정을 반복(repeat)합니다.
  7. 학습 과정을 멈춰도 좋다는 판단이 설 때까지 계속해서 반복합니다.

초기화

-파라미터의 값을 무작위로 초기화한다. 물론 초기의 파라미터를 설정하는 다른 방법을 생각해볼 수 있다.

손실

-모델의 성능이 좋을때 낮은 값을 반환하는 함수가 필요하다.

가중치 갱신

-미적분을 사용하여 그레디언트를 계산하여, 갱신을 어떤 방향으로 할지를 판단하고, 학습률에 따라 정해진 크기에 따라 가중치를 조금씩 갱신한다.

훈련 종료

-모델을 학습할 에포크(epoch) 횟수를 정하고 적용한다. 모델의 정확도가 최고 높은 순간 혹은 시간이 부족한 시점까지 설정할 수 있다.

def f(x): return x**2

plot_function(f, 'x', 'x**2')


간단한 2차함수를 정의하고 이 함수를 손실 함수, x를 함수의 가중치 파라미터라고 가정한다.

plot_function(f, 'x', 'x**2')
plt.scatter(-1.5, f(-1.5), color='red');


임의의 파라미터를 선택하고 손실값을 계산한다.


파라미터를 약간 크거나 작게 조정했을때 변화를 살펴본다.


손실을 계산하고 가중치를 조절하는 과정을 수차례 반복하다 보면, 결국 2차 함수 곡선의 가장 낮은 부분에 도달하게 된다.

그레디언트(gradient) 계산

그레디언트
-딥러닝 학습의 마법
-모델이 더 나아지려면 어떻게 갱신을 해야되는지 정도를 알려줌
-y의 변화량/x의 변화량

파이토치에서는 직접 계산할 필요가 없다. 모든 함수에 대한 미분을 자동으로 계산하는 능력이 있다.

xt = tensor(3.).requires_grad_()
xt

tensor(3., requires_grad=True)

requiresgrad는 파이토치에 특정 값의 변수에 대한 그레디언트를 계산해 달라고 하는 마법의 주문이다.

yt = f(xt)
yt

tensor(9., grad_fn=<PowBackward0>)

xt에 대한 함수값 계산

yt.backward()

backward 메소드를 통해 그레디언트를 계산한다.
backward라는 이름은 미분을 계산하는 과정인 역전파에서 따왔다.
역전파는 역방향 전파라고도 불린다.

xt.grad

tensor(6.)

텐서 grad의 속성을 통해서 실제 계산된 그레디언트를 알 수 있다.

xt = tensor([3.,4.,10.]).requires_grad_()
xt

def f(x): return (x**2).sum()

yt = f(xt)
yt

tensor(125., grad_fn=<SumBackward0>)

함수에 단일 숫자 대신에 벡터를 입력해서 벡터의 그레디언트를 구한다.

yt.backward()
xt.grad

tensor([ 6.,  8., 20.])

그레디언트는 함수의 기울기(조정 방향)만 알려준다. 얼마나 조정해야하는지는 알려주지 않는다. 다만 기울기의 크기에 따라 얼마나 조정을 해야하는지 유추할 수 있다.

학습률을 통해 단계 밟아 나가기

그레디언트로 파라미터를 조절하는 방식은 학습률(Learning Rate)이라는 작은 값을 그레디언트에 곱하는 아이디어로 시작한다.

w -= gradient(w) * lr

이 과정은 최적화 단계를 사용한 파라미터의 갱신 단계이다.

학습률이 너무 크면 상황이 나빠집니다.

SGD를 활용하여 모델 처음부터 끝까지 완성하기

time = torch.arange(0,20).float(); time

tensor([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12., 13., 14., 15., 16., 17., 18., 19.])
speed = torch.randn(20)*3 + 0.75*(time-9.5)**2 + 1
plt.scatter(time,speed);

임의의 노이즈를 약간 추가한 2차함수 형태

def f(t, params):
    a,b,c = params
    return a*(t**2) + (b*t) + c

입력 T와 파라미터에 대한 함수 정의
데이터 가장 적합한 2차 함수를 찾는 문제는 가장 적합한 a,b,c 값을 찾는 문제이다.

def mse(preds, targets): return ((preds-targets)**2).mean().sqrt()

분류가 아닌 연속적인 값을 에측하는 회귀문제에서는 평균제곱오차라는 손실 함수를 사용한다.

params = torch.randn(3).requires_grad_()
params

1단계 : 파라미터 초기화
-requiresgrad메소드를 통해서 파이토치가 그레디언트를 추적할 수 있도록 설정

preds = f(time, params)

2단계 : 예측 계산

def show_preds(preds, ax=None):
    if ax is None: ax=plt.subplots()[1]
    ax.scatter(time, speed)
    ax.scatter(time, to_np(preds), color='red')
    ax.set_ylim(-300,100)
    
show_preds(preds)    

예측과 실제 타깃의 그래프

loss = mse(preds, speed)
loss

tensor(312.0075, grad_fn=<SqrtBackward0>)

3단계 : 손실 계산

loss.backward()
params.grad

tensor([-167.1069,  -10.7561,   -0.7718])

params.grad * 1e-4

tensor([-1.6711e-02, -1.0756e-03, -7.7180e-05])

4단계 : 그레디언트 계산

params

tensor([-1.6086, -1.3940, -0.2386], requires_grad=True)
lr = 1e-4
params.data -= lr * params.grad.data
params.grad = None

5단계 : 가중치 한 단계 갱신

preds = f(time,params)
mse(preds, speed)

show_preds(preds)


손실이 개선되었는지 확인

def apply_step(params, prn=True):
    preds = f(time, params)
    loss = mse(preds, speed)
    loss.backward()
    params.data -= lr * params.grad.data
    params.grad = None
    if prn: print(loss.item())
    return preds

지금까지의 과정을 반복해야하므로 함수로 변경

for i in range(10): apply_step(params)

306.3997802734375
303.5965270996094
300.7936706542969
297.9913024902344
295.1893310546875
292.38775634765625
289.586669921875
286.7860412597656
283.98590087890625
281.186279296875

6단계 : 과정 반복하기

_,axs = plt.subplots(1,4,figsize=(12,3))
for ax in axs: show_preds(apply_step(params, False), ax)
plt.tight_layout()

함수 그래프를 직접 출력하여 과정을 시각적으로 확인

7단계 : 학습 종료

MNIST 손실 함수

train_x = torch.cat([stacked_threes, stacked_sevens]).view(-1, 28*28)

train_x.shape

torch.Size([12396, 784])

파이토치의 view 메소드를 통해서 데이터는 건드리지 않고 텐서의 모양만 변경 -> -1은 해당 축을 모든 데이터에 맞게끔 크게 만들어라는 의미

train_y = tensor([1]*len(threes) + [0]*len(sevens)).unsqueeze(1)
train_x.shape,train_y.shape

(torch.Size([12396, 784]), torch.Size([12396, 1]))

3은 1으로, 7은 0으로 하여 레이블 생성

dset = list(zip(train_x,train_y))
x,y = dset[0]
x.shape,y

(torch.Size([784]), tensor([1]))

파이토치의 Dataset은 (x,y)로 된 튜플을 요구한다. 파이썬 zip함수와 list를 통해서 (x,y)로 연결된 튜플로 변환한다.

valid_x = torch.cat([valid_3_tens, valid_7_tens]).view(-1, 28*28)
valid_y = tensor([1]*len(valid_3_tens) + [0]*len(valid_7_tens)).unsqueeze(1)
valid_dset = list(zip(valid_x,valid_y))

검증용 데이터셋도 똑같은 과정을 적용한다.

def init_params(size, std=1.0): return (torch.randn(size)*std).requires_grad_()

weights = init_params((28*28,1))

bias = init_params(1)

파라미터(가중치와 편향) 초기화 설정

(train_x[0]*weights.T).sum() + bias

tensor([0.5112], grad_fn=<AddBackward0>)

단일 이미지에 대한 예측 계산
w*x는 행렬 곱셈을 통해서 계산되며, 파이썬에서는 @연산자가 행렬 곱셈을 표현한다.

def linear1(xb): return xb@weights + bias
preds = linear1(train_x)
preds

tensor([[ 0.5112],
        [12.2981],
        [10.6589],
        ...,
        [ 2.5798],
        [-6.0481],
        [-6.4096]], grad_fn=<AddBackward0>)
corrects = (preds>0.5).float() == train_y
corrects

tensor([[ True],
        [ True],
        [ True],
        ...,
        [False],
        [ True],
        [ True]])
        
 corrects.float().mean().item()      
 #0.5002258896827698

예측이 0.5보다 큰지를 검사해서 정확도를 검사

weights.data[0] *= 1.0001

preds = linear1(train_x)
((preds>0.0).float() == train_y).float().mean().item()
#0.6974024176597595

가중치가 하나 변경되었을때의 정확도 변화

SGD로 모델을 향상시키려면 그레디언트가 필요하다. 그럴려면 현재 모델에 대한 손실 함수가 필요하다.
정확도는 3에서 7로, 7에서 3으로 변할때만 변하기때문에 대부분에 곳에서 그레디언트가 0이다. 따라서 정확도를 손실 함수로 설정하면 모델의 학습이 이루어지지 않는다.

trgts  = tensor([1,0,1])
prds   = tensor([0.9, 0.4, 0.2])

def mnist_loss(predictions, targets):
    return torch.where(targets==1, 1-predictions, predictions).mean()

예측과 레이블 사이의 거리를 측정하는 손실 함수를 만든다.

torch.where(trgts==1, 1-prds, prds)

tensor([0.1000, 0.4000, 0.8000])

torch.where은 trgts가 1일때는 예측이 1과 떨어진 정도를, 0일때는 0과 떨어진 정도를 측정하고, 이렇게 구한 거리의 평균을 구하는 일을 한다.

mnist_loss(prds,trgts)

tensor(0.4333)
mnist_loss(tensor([0.9, 0.4, 0.8]),trgts)

tensor(0.2333)

가령 거짓 타겟에 대한 예측을 0.2에서 0.8로 바꾸면 손실이 줄어들어 더 나은 예측이 된다.

시그모이드(Sigmoid)

def sigmoid(x): return 1/(1+torch.exp(-x))

시그모이드는 항상 0과1사이의 숫자를 출력하는 함수이다.

lot_function(torch.sigmoid, title='Sigmoid', min=-4, max=4)

또한 증가만 하는 부드러운 곡선이라서 SGD가 의미있는 그레디언트를 쉽게 찾게 해준다.

def mnist_loss(predictions, targets):
    predictions = predictions.sigmoid()
    return torch.where(targets==1, 1-predictions, predictions).mean()

시그모이드가 적용되도록 mnist_loss 함수 갱신

평가지표 - 사람의 이해를 도움,직관적 이해
손실함수 - 손실의 연속적 계산,수치적 자료

SGD와 미니배치

미니배치 - 한 번에 일정 개수(현실적인)의 데이터에 대한 손실의 평균을 계산하는 방식
배치크기 - 미니배치에 포함된 데이터의 개수

미니배치로 해야하는 또 다른 이유는 학습을 GPU에서 수행하기 때문이다. ( 한 번에 많은 데이터를 병렬적으로 할당 )

coll = range(15)
dl = DataLoader(coll, batch_size=5, shuffle=True)
list(dl)

파이토치와 fastai는 임의로 데이터셋을 뒤섞은 다음 미니배치를 만드는 DataLoader 클래스를 제공한다.

DataLoader는 파이썬이 제공하는 모든 컬렉션을 주어진 배치 크기 단위로 분할된 여러 배치에 접근하는 반복자로 만들어준다.

ds = L(enumerate(string.ascii_lowercase))
ds

#26) [(0, 'a'),(1, 'b'),(2, 'c'),(3, 'd'),(4, 'e'),(5, 'f'),(6, 'g'),(7, 'h'),(8, 'i'),(9, 'j')...]
dl = DataLoader(ds, batch_size=6, shuffle=True)
list(dl)

[(tensor([ 3, 16, 17,  8,  7,  2]), ('d', 'q', 'r', 'i', 'h', 'c')),
 (tensor([10, 22,  4, 14, 25, 15]), ('k', 'w', 'e', 'o', 'z', 'p')),
 (tensor([ 1, 12, 19,  5,  9, 11]), ('b', 'm', 't', 'f', 'j', 'l')),
 (tensor([ 0, 23, 21, 13, 18, 20]), ('a', 'x', 'v', 'n', 's', 'u')),
 (tensor([ 6, 24]), ('g', 'y'))]

DataLoader의 간단한 예시이다.

배운 내용을 모델에 적용하기

for x,y in dl:
    pred = model(x)
    loss = loss_func(pred, y)
    loss.backward()
    parameters -= parameters.grad * lr

매 에포크마다 구현되어야 하는 과정의 코드이다.

weights = init_params((28*28,1))
bias = init_params(1)

파라미터 초기화

dl = DataLoader(dset, batch_size=256)
xb,yb = first(dl)
xb.shape,yb.shape

(torch.Size([256, 784]), torch.Size([256, 1]))

DataLoader를 통한 Dataset 생성

valid_dl = DataLoader(valid_dset, batch_size=256)

검증용 데이터셋에 같은 작업 적용

batch = train_x[:4]
batch.shape

torch.Size([4, 784])

preds = linear1(batch)
preds

tensor([[ 0.6950],
        [ 4.1958],
        [14.3890],
        [ 8.2895]], grad_fn=<AddBackward0>)
        
loss = mnist_loss(preds, train_y[:4])
loss  

tensor(0.0870, grad_fn=<MeanBackward0>)        

크기가 4인 미니배치를 통해서 간단히 검사

loss.backward()
weights.grad.shape,weights.grad.mean(),bias.grad

(torch.Size([784, 1]), tensor(-0.0085), tensor([-0.0592]))

그레디언트 계산

def calc_grad(xb, yb, model):
    preds = model(xb)
    loss = mnist_loss(preds, yb)
    loss.backward()

지금까지의 과정 함수화

calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(),bias.grad

(tensor(-0.0085), tensor([-0.0592]))

calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(),bias.grad

(tensor(-0.0085), tensor([-0.0592]))

같은 코드를 2번 호출하니 그레디언트가 변함
-> 계산할때마다 그레디언트를 0으로 초기화(설정)해주어야 함

weights.grad.zero_()
bias.grad.zero_();

제자리 연산자(마지막에 _이 포함됨)은 해당 객체를 제자리에서 조작한다.

def train_epoch(model, lr, params):
    for xb,yb in dl:
        calc_grad(xb, yb, model)
        for p in params:
            p.data -= p.grad*lr
            p.grad.zero_()

지금까지의 일련의 학습 과정 함수화
파이토치가 그레디언트를 2번 계산하지 않게 해야한다.
그래서 텐서의 data속성에 값을 할당한다.(data에 할당하면 해당 단계에서 그레디언트를 계산하지 않는다)

(preds>0.0).float() == train_y[:4]

tensor([[True],
        [True],
        [True],
        [True]])

학습이 잘 이루어졌는지 확인한다.

def batch_accuracy(xb, yb):
    preds = xb.sigmoid()
    correct = (preds>0.5) == yb
    return correct.float().mean()

배치 단위의 평균 정확도를 계산하는 함수

batch_accuracy(linear1(batch), train_y[:4])

tensor(1.)
def validate_epoch(model):
    accs = [batch_accuracy(model(xb), yb) for xb,yb in valid_dl]
    return round(torch.stack(accs).mean().item(), 4)

validate_epoch(linear1)
0.4874

모든 배치 단위에 대한 정확도의 평균을 계산하는 함수

lr = 1.
params = weights,bias
train_epoch(linear1, lr, params)
validate_epoch(linear1)

한 에포크 동안 모델을 학습시켰을때 다음 정확도의 변화

for i in range(20):
    train_epoch(linear1, lr, params)
    print(validate_epoch(linear1), end=' ')

0.8491 0.9257 0.9472 0.9579 0.9633 0.9638 0.9667 0.9677 0.9696 0.9696 0.9716 0.9731 0.9731 0.9736 0.974 0.975 0.976 0.9765 0.9765

에포크를 여러 번 반복한다.

옵티마이저

옵티마이저 - 파이토치에서 SGD단계를 포장하여 객체로서 다루게 하는 도구

지금까지 직접 만든 함수를 파이토치의 nn.linear 모듈로 대체가 가능하다.
모듈 - 파이토치의 nn.module 클래스를 상속받은 클래스 객체

linear_model = nn.Linear(28*28,1)

nn.linear는 init_params와 linear 작동을 동시에 수행한다.

w,b = linear_model.parameters()
w.shape,b.shape

(torch.Size([1, 784]), torch.Size([1]))

모든 파이토치 모듈은 파라미터의 존재를 자체적으로 알고 있다. parameters 메소드로 접근할 수 있다.

class BasicOptim:
    def __init__(self,params,lr): self.params,self.lr = list(params),lr

    def step(self, *args, **kwargs):
        for p in self.params: p.data -= p.grad.data * self.lr

    def zero_grad(self, *args, **kwargs):
        for p in self.params: p.grad = None

opt = BasicOptim(linear_model.parameters(), lr)
opt

이 파티미터 정보를 통해서 옵티마이저를 정의하는데 활용할 수 있다.
파라미터를 넣어주어 옵티마이저 객체를 생성한다.

def train_epoch(model):
    for xb,yb in dl:
        calc_grad(xb, yb, model)
        opt.step()
        opt.zero_grad()

옵티마이저 객체를 통해서 학습 과정을 단순화

validate_epoch(linear_model)

0.7021

검증용 데이터셋의 정확도를 구하는 과정은 그대로 잘 작동한다.

def train_model(model, epochs):
    for i in range(epochs):
        train_epoch(model)
        print(validate_epoch(model), end=' ')

학습과정과 검증용 데이터셋 정확도 구하는 과정을 함수화

train_model(linear_model, 20)

0.4932 0.7388 0.8604 0.9184 0.9355 0.9492 0.958 0.9633 0.9658 0.9682 0.9702 0.9726 0.9746 0.9746 0.976 0.977 0.9775 0.9775 0.9785 0.9785 

결과는 앞과 동일하다.

linear_model = nn.Linear(28*28,1)
opt = SGD(linear_model.parameters(), lr)
train_model(linear_model, 20)

fastai에서 제공하는 SGD클래스는 방금 만든 BasicOptim과 동일한 방식으로 작동한다.

dls = DataLoaders(dl, valid_dl)
first(valid_dl)[1].shape

torch.Size([256, 1])

fastai는 train_model 함수 대신 Learner.fit도 제공한다.
Learner.fit을 샤용하려면 우선 Learner를 생성해야 하고 그럴러면 DataLoaders를 먼저 만들어야 한다.

learn = Learner(dls, nn.Linear(28*28,1), opt_func=SGD,
                loss_func=mnist_loss, metrics=batch_accuracy)

Learner 생성

learn.fit(10, lr=lr)


Learner 학습

비선형성 추가

모델을 좀 더 복잡하게 구성하기 위해서는 두 선형 분류 모델 사이에 비선형을 추가하면 된다.

def simple_net(xb): 
    res = xb@w1 + b1
    res = res.max(tensor(0.0))
    res = res@w2 + b2
    return res

간단한 신경망을 정의한 코드

w1 = init_params((28*28,30))
b1 = init_params(30)
w2 = init_params((30,1))
b2 = init_params(1)

w1과 w2는 가중치 텐서이며, b1과 b2는 편향 텐서이다.

plot_function(F.relu)


res.max(tensor(0.0)) 함수는 렐루(relu)라고 알려져 있다.

일반 근사 정리 - 가중치를 충분히 크게 구성하고 행렬의 올바른 값을 찾을 수만 있다면, 이 단순한 함수는 모든 계산 가능한 문제를 높은 정확도로 풀어낼 수 있다.

simple_net = nn.Sequential(
    nn.Linear(28*28,30),
    nn.ReLU(),
    nn.Linear(30,1)
)

파이토치 모듈을 사용한 간단한 신경망이다.
첫 번째와 세 번째는 선형 계층
두 번째는 비선형성 혹은 활성화 함수이다.

nn.ReLU는 F.relu 함수와 동일한 일을 하는 파이토치 모듈이다.

learn = Learner(dls, simple_net, opt_func=SGD,
                loss_func=mnist_loss, metrics=batch_accuracy)
                
learn.fit(40, 0.1)                

더 깊은 모델을 사용한 학습

plt.plot(L(learn.recorder.values).itemgot(2));

학습 과정은 learn.recorder에 기록된다.
이를 활용해 출력 결과를 담은 테이블은 values속성에 저장된다.

learn.recorder.values[-1][2]

0.98233562707901

마지막에 기록된 정확도(-1)

지금까지 딥러닝의 핵심 요소

신경망 - 올바른 파라미터 집합이 주어지면 모든 문제를 원하는 정확도로 풀어낼 수 있는 함수
SGD - 모든 함수에 대한 최적의 파라미터 집합을 찾는 방법

좀 더 깊은 모델

dls = ImageDataLoaders.from_folder(path)
learn = cnn_learner(dls, resnet18, pretrained=False,
                    loss_func=F.cross_entropy, metrics=accuracy)
learn.fit_one_cycle(1, 0.1)

epoch	train_loss	valid_loss	accuracy	time
0	0.129798	0.025476	0.994112	00:15

18개의 계층으로 구성된 모델(resnet18)

profile
후회없이

0개의 댓글