빅데이터 분석 (7-8주차) 10월26일 10월28일
Pytorch CNN (MNIST 3/7), CAM
- import
- data
- 1. 지난시간까지의 모형 (직접 네트워크 설계, pytorch)
- 2. 드랍이웃, 배치추가 (직접 네트워크 설계, pytorch+fastai)
- 3. resnet34 (기존의 네트워크 사용, 순수 fastai)
- 모형을 뜯어보는 방법 (lrnr1.model)
- lrnr2.model 분석
- 딥러닝 연구의 네가지 축
- 설명가능한 CNN모형
- CAM: observation을 1개로 고정하고 net2에서 layer의 순서를 바꿔서 시각화
- 숙제
import torch
from fastai.vision.all import *
import graphviz
def gv(s): return graphviz.Source('digraph G{ rankdir="LR"'+ s + ';}')
-
download data
path = untar_data(URLs.MNIST_SAMPLE)
path.ls()
-
list
threes=(path/'train'/'3').ls()
sevens=(path/'train'/'7').ls()
-
list $\to$ image
Image.open(threes[4])
-
image $\to$ tensor
tensor(Image.open(threes[4]))
- 여기에서 tensor는 파이토치가 아니라 fastai에서 구현한 함수임
-
여러개의 리스트를 모두 텐서로 바꿔보자.
seven_tensor = torch.stack([tensor(Image.open(i)) for i in sevens]).float()/255
three_tensor = torch.stack([tensor(Image.open(i)) for i in threes]).float()/255
-
$X$와 $y$를 만들자.
seven_tensor.shape, three_tensor.shape
y=torch.tensor([0.0]*6265+ [1.0]*6131).reshape(12396,1)
X=torch.vstack([seven_tensor,three_tensor]).reshape(12396,-1)
X.shape, y.shape
X=X.reshape(12396,1,28,28)
X.shape
c1=torch.nn.Conv2d(1,16,5) # 입력채널=1 (흑백이므로), 출력채널=16, 윈도우크기5
X.shape, c1(X).shape
m1=torch.nn.MaxPool2d(2)
X.shape,c1(X).shape,m1(c1(X)).shape
a1=torch.nn.ReLU()
X.shape,c1(X).shape, m1(c1(X)).shape, a1(m1(c1(X))).shape
class Flatten(torch.nn.Module):
def forward(self,x):
return x.reshape(12396,-1)
flatten=Flatten()
X.shape,c1(X).shape, m1(c1(X)).shape, a1(m1(c1(X))).shape, flatten(a1(m1(c1(X)))).shape
l1=torch.nn.Linear(in_features=2304,out_features=1)
X.shape,\
c1(X).shape, \
m1(c1(X)).shape, \
a1(m1(c1(X))).shape, \
flatten(a1(m1(c1(X)))).shape, \
l1(flatten(a1(m1(c1(X))))).shape
plt.plot(l1(flatten(a1(m1(c1(X))))).data)
net = nn.Sequential(c1,m1,a1,flatten,l1)
## 마지막의 sigmoid는 생략한다. torch.nn..BCEWithLogitsLoss()에 내장되어 있을것이므로
-
손실함수와 옵티마이저 정의
loss_fn=torch.nn.BCEWithLogitsLoss()
optimizer= torch.optim.Adam(net.parameters())
-
step1~4 (CPU로 돌아가는 거기 때문에 시간 필요)
for epoc in range(200):
## 1
yhat=net(X)
## 2
loss=loss_fn(yhat,y)
## 3
loss.backward()
## 4
optimizer.step()
net.zero_grad()
a2= torch.nn.Sigmoid()
학습 전 상황
plt.plot(y)
plt.plot(a2(yhat.data),'.')
학습 후 상황
plt.plot(y)
plt.plot(yhat.data,'.')
ypred=a2(yhat.data)>0.5
sum(ypred==y)/12396
ds=torch.utils.data.TensorDataset(X,y)
ds.tensors[0].shape
- torch.utils.data.random_split: Randomly split a dataset into non-overlapping new datasets of given lengths. Optionally fix the generator for reproducible results
ds1,ds2 = torch.utils.data.random_split(ds,[10000,2396])
미니배치 안하고
dl1 = torch.utils.data.DataLoader(ds1,batch_size=500)
dl2 = torch.utils.data.DataLoader(ds2,batch_size=2396)
dls=DataLoaders(dl1,dl2)
class Flatten(torch.nn.Module):
def forward(self,x):
return x.reshape(x.shape[0],-1)
net=torch.nn.Sequential(
torch.nn.Conv2d(1,16,5),
torch.nn.MaxPool2d(2),
torch.nn.ReLU(),
torch.nn.Dropout2d(),
Flatten(),
torch.nn.Linear(2304,1))
loss_fn=torch.nn.BCEWithLogitsLoss()
#optimizer= torch.optim.Adam(net.parameters())
# learner에서 option으로 넣어주기 때문에 빼기
$\uparrow$ 모형을 뜯어보는 방법에서 왜 만들어놓은 것 사용하면 안 되는지 설명
lrnr1 = Learner(dls,net,opt_func=Adam,loss_func=loss_fn)
$\uparrow$ for문의 step 1,2,3,4 하는 과정
lrnr1.fit(10)
$\uparrow$ GPU 를 사용하여 빨리 실행되는 모습
-
결과를 시각화하면 아래와 같다.
$\downarrow$ 네트워크의 파라미터가 cuda,즉 GPU에 올라가있기 때문에 cuda에 올려줬다가 CPU로 찍어야 데이터가 그려지겠지!, sigmoid인 a2를 씌워준 결과
plt.plot(a2(net(X.to("cuda:0")).to("cpu").data),'.')
-
빠르고 적합결과도 좋음
요약: 세가지 방법을 사용해 옴.
- 직접네트워크+순수pytorch
- 드랍아웃, 배치추가: 직접 네트워크 설계, pytorch와 fastai leaner를 이용
- 기존 네트워크 사용, 순수 fastai 로만 사용($\downarrow$ 내용)
-
데이터로부터 새로운 데이터로더스를 만들고 이를 dls2라고 하자.
path=untar_data(URLs.MNIST_SAMPLE) # dls 새로 만들기
path
path.ls() # 여기서 train 에 접근할 것
dls2=ImageDataLoaders.from_folder(
path,
train='train',
valid_pct=0.2) # 2주차 이미지크롤링 참고
Learner?
cnn_learner?
-
러너오브젝트를 생성하고 학습하자.
lrnr2=cnn_learner(dls2,resnet34,metrics=error_rate)
lrnr2.fine_tune(1)
-
결과관찰
lrnr2.show_results()
-
우선 드랍아웃, 배치추가: 직접 네트워크 설계, pytorch와 fastai leaner를 이용한 방법2로 돌아가자.
net(X.to("cuda:0")) # 적합 결과를 볼 수 있음
-
네트워크 구조
net
net[0] # 이런식으로 층별 접근 가능
-
층별변환과정
print(X.shape, '--> input image')
print(net[0](X.to("cuda:0")).shape, '--> 2dConv')
print(net[1](net[0](X.to("cuda:0"))).shape, '--> MaxPool2d')
print(net[2](net[1](net[0](X.to("cuda:0")))).shape, '--> ReLU')
print(net[3](net[2](net[1](net[0](X.to("cuda:0"))))).shape, '--> Dropout2d')
print(net[4](net[3](net[2](net[1](net[0](X.to("cuda:0")))))).shape, '--> Flatten')
print(net[5](net[4](net[3](net[2](net[1](net[0](X.to("cuda:0"))))))).shape, '--> Linear')
피드 forward 과정...뭐....
-
최종결과
net[5](net[4](net[3](net[2](net[1](net[0](X.to("cuda:0")))))))
net(X.to("cuda:0"))
두 결과가 일치하는 모습
-
lrnr1자체를 활용해도 층별변환과정을 추적할수 있음. (lrnr1.model = net 임을 이용)
지금까지는 lrnr1를 이용해서 for문 돌림으로써 원하는 층별 세부과정을 관찰했지만, lrnr1자체에서도 모형을 확인할 수 있음
lrnr1.model
lrnr1.model[0]
lrnr1.model(X.to("cuda:0"))
$\uparrow$ 2d colv이 진행되는 과정
print(X.shape, '--> input image')
print(lrnr1.model[0](X.to("cuda:0")).shape, '--> 2dConv')
print(lrnr1.model[1](lrnr1.model[0](X.to("cuda:0"))).shape, '--> MaxPool2d')
print(lrnr1.model[2](lrnr1.model[1](lrnr1.model[0](X.to("cuda:0")))).shape, '--> ReLU')
print(lrnr1.model[3](lrnr1.model[2](lrnr1.model[1](lrnr1.model[0](X.to("cuda:0"))))).shape, '--> Dropout2d')
print(lrnr1.model[4](lrnr1.model[3](lrnr1.model[2](lrnr1.model[1](lrnr1.model[0](X.to("cuda:0")))))).shape, '--> Flatten')
print(lrnr1.model[5](lrnr1.model[4](lrnr1.model[3](lrnr1.model[2](lrnr1.model[1](lrnr1.model[0](X.to("cuda:0"))))))).shape, '--> Linear')
-
정리: 모형은 항상 아래와 같이 2d-part 와 1d-part로 나누어진다.
torch.Size([12396, 1, 28, 28]) --> input image
torch.Size([12396, 16, 24, 24]) --> 2dConv
torch.Size([12396, 16, 12, 12]) --> MaxPool2d
torch.Size([12396, 16, 12, 12]) --> ReLU
torch.Size([12396, 16, 12, 12]) --> Dropout2d
===============================================================
torch.Size([12396, 2304]) --> Flatten
torch.Size([12396, 1]) --> Linear
엄밀히 말하면 Flatten은 2d를 1d로 바꿔주는 과정이긴 해..
엄밀히 말하면 공식적인 분류는 아닌 1d-part 2d-part
-
2d-part:
- 2d선형변환: torch.nn.Conv2d()
- 2d비선형변환: torch.nn.MaxPool2d(), torch.nn.ReLU()
-
1d-part:
- 1d선형변환: torch.nn.Linear()
- 1d비선형변환: torch.nn.ReLU()
_net1=torch.nn.Sequential(
net[0],
net[1],
net[2],
net[3])
_net2=torch.nn.Sequential(
net[4],
net[5])
1d-part랑 2d-part 몰아서 아키텍쳐 설계 $\to$ 이렇게 층별로 몰아서 관리하기도 함
_net1
_net2
_net=torch.nn.Sequential(_net1,_net2)
_net[1](_net[0](X.to('cuda:0')))
-
아래의 모형은 현재 가장 성능이 좋은 모형(state of the art)중 하나인 resnet이다.
lrnr2.model
-
특징
- lrnr2.model[0] _ 2d-part: 입력채널이 3이다, Conv2d에 padding/stride의 옵션이 있다, 드랍아웃이 없다, 배치정규화BatchNorm2d가 있다.
- lrnr2.model[1] _ 1d-part: 배치정규화BatchNorm1d가 있다, 출력의 차원이 2이다.
우리가 네트워크 만들때 입력 채널은 1이기 때문에 resnet 기본 입력채널 3과 다르다. 그래서 Data Load Set 만들때도 network만들때도 이에 맞는 dls 따로 만들어줘야 한다. 왜 3일까.. 컬러이미지를 기본으로 생각하고 있는 resnet34.. padding은 shape 변하지 않음..
DLS, Networks
- 네트워크의 형태에 따라서 dls의 형태도 다르게 만들어야 한다.
- MLP모형: 입력이 $784$, 첫 네트워크의 형태가 $784 \to 30$ 인 torch.nn.Linear()
- CNN모형: 입력이 $1\times 28 \times 28$, 첫 네트워크의 형태가 $1\times 28 \times 28 \to 16 \times 24 \times 24$ 인 torch.nn.Conv2d()
- Resnet34: 입력이 $3\times 28 \times 28$, 첫 네트워크의 형태가 $3\times 28 \times 28 \to ??$
Multi Linear Perceptron(= Deep Neural Network)
참고
$y$ | 분포가정 | 마지막층의 활성화함수 | 손실함수(파이토치) |
---|---|---|---|
3.45, 4.43, ... (연속형) | 정규분포 | Linear | MSEloss |
0 or 1 | 이항분포(베르누이) | Sigmoid | BCEloss |
[0,0,1], [0,1,0], [1,0,0] | 다항분포 | Softmax | CrossEntropyLoss |
(1) 아키텍처 $(\star)$
- 한 영역의 전문적인 지식이 필요한 것이 아닌것 같다.
- 끈기, 약간의 운, 직관, 좋은컴퓨터..
- yhat <- X,net
(2) 손실함수
- 통계적지식필요 // 기존의 손실함수를 변형하는 형태 (패널티텀활용)
- 수렴하는 해가 달라지기 때문
- loss <- (y,yhat)
(3) 미분계산 (컴공)
- 병렬처리등에 대한 지식 필요
- (d/dw)loss <- loss
(4) 옵티마이저 (산공)
- 최적화에 대한 이론적 토대 필요
-
딥러닝 이전까지의 아키텍처에 대한 연구
- 파라메트릭 모형: 전문가 (실제 전문가가 해석한 거 바탕으로)
- 넌파라메트릭 모형: 전문가(보통 통계 전문가)
- 딥러닝: 상대적으로 비전문가(지금 우리가 할 수 있는 수준...)
-
특징: 비전문가도 만들수 있다 + 블랙박스 (내부연산을 뜯어볼 수는 있지만 우리가 해석하기 어려움)
- 신뢰받기 어려워
-
설명가능한 딥러닝에 대한 요구 (XAI)
-
현재까지의 모형
- 1단계: 2d선형변환 $\to$ 2d비선형변환
- 2단계: Flatten $\to$ MLP
-
lrnr1(교수님꼐서 만들었던 모형)의 모형을 다시 복습
lrnr1.model
net1=torch.nn.Sequential(
lrnr1.model[0],
lrnr1.model[1],
lrnr1.model[2],
lrnr1.model[3])
net1, lnnr1에서 구성했던 파라미터들 가져오기
net1(X.to('cuda:0')).shape
observation 16channel 12 * 12
-
1단계까지의 출력결과를 시각화
fig, axs = plt.subplots(8,8)
k=0
for i in range(8)
for j in range(8):
axs[i,j].imshow(net1(X.to("cuda:0"))[0][k].to("cpu").data) # 첫번쨰 저[0]obseervation으로 고정
k=k+1
fig.set_figheight(8)
fig.set_figwidth(8)
fig.tight_layout()
왜? 설명은 아래에서
lrnr1.model
-
계획
- 변경전net2: $(n,16,12,12) \overset{flatten}{\Longrightarrow} (n,?) \overset{Linear(?,1)}{\Longrightarrow} (n,1)$
- 변경후net2: $(n,16,12,12) \overset{gap+flatten}{\Longrightarrow} (n,16) \overset{Linear(16,1)}{\Longrightarrow} (n,1)$
gap(global activate pooling) - 16개의 이미지가 $12\times12$픽셀로 정리되어 있는데 여기서 평균으로 값을 하나씩만 뽑고 싶다.
flatten - 추가해서 dimention 줄여주기
-
gap: 12$\times$12 픽셀을 평균내서 하나의 값으로 대표하자 (왜?)
ap=torch.nn.AdaptiveAvgPool2d(output_size=1)
ap(net1(X.to("cuda:0"))).shape
이미지 하나당 하나의 평균아 나타나 16개가 찍힌 모습
--
보충학습:ap(average pooling)는 그냥 평균
torch.tensor([[0.1,0.2],[0.3,0.4]])
ap(torch.tensor([[0.1,0.2],[0.3,0.4]]))
--
-
flatten
flatten(ap(net1(X.to("cuda:0")))).shape
-
linear
_l1=torch.nn.Linear(16,1,bias=False)
_l1.to("cuda:0")
_l1(flatten(ap(net1(X.to("cuda:0"))))).shape
-
이걸 net2로 구성하자. $\to$ (net1,net2)를 묶어서 하나의 새로운 네트워크를 만들자.
net2=torch.nn.Sequential(
torch.nn.AdaptiveAvgPool2d(1),
Flatten(),
torch.nn.Linear(16,1,bias=False))
net=torch.nn.Sequential(net1,net2)
net
새로 데이터를 만들어서 위에서 두 번째 sequantial이 학습이 안 되어 있을걸?
-
수정된 네트워크로 lrnr3을 만들고 재학습
ds=torch.utils.data.TensorDataset(X,y)
ds1,ds2=torch.utils.data.random_split(ds,[10000,2396])
dl1=torch.utils.data.DataLoader(ds1,batch_size=1000)
dl2=torch.utils.data.DataLoader(ds2,batch_size=2396)
dls=DataLoaders(dl1,dl2)
lrnr3=Learner(dls,net,opt_func=Adam,loss_func=loss_fn,lr=0.1)
lrnr3.fit(10)
Class Activation Map
-
계획
- 변경전net2: $(n,16,12,12) \overset{flatten}{\Longrightarrow} (n,?) \overset{Linear(?,1)}{\Longrightarrow} (n,1)$
- 변경후net2: $(n,16,12,12) \overset{gap+flatten}{\Longrightarrow} (n,16) \overset{Linear(16,1)}{\Longrightarrow} (n,1)$
- CAM: $(1,16,12,12) \overset{Linear(16,1)+flatten}{\Longrightarrow} (12,12) \overset{gap}{\Longrightarrow} 1$
cam에서 observation 을 1로 고정, linear도 16$\times$1 vector니까 곱해질 수 있겠지 flatten 후 gap하면 fix한 observation으로 dimention이 변경
-
준비과정1: 시각화할 샘플을 하나 준비하자.
x=X[100] # 이미지 임의로 하나 선택
X.shape,x.shape
- 차원이 다르므로 나중에 네트워크에 넣을때 문제가 생길 수 있음 $\to$ 차원을 맞춰주자
x=x.reshape(1,1,28,28)
plt.imshow(x.squeeze())
squeeze 로 차원을 28$\times$28로 변환
x자체는 2d라 그려지지 않지
fastai로 훈련해서 gpu에 있는 네트워크를 cpu로!
-
준비과정2: 계산과 시각화를 위해서 각 네트워크를 cpu로 옮기자. (fastai로 학습한 직후라 GPU에 있음)
net1.to('cpu')
net2.to('cpu')
-
forward확인: 이 값을 기억하자.
activation 취하기 전에는 음수/양수로 되어있고,
seventensor 가 three tensor보다 먼저 정의됨
따라서 음수이면 7, 양수이면 1이라고 cnn이 판단
net2(net1(x)) ## 음수이므로 class=7 이라고 CNN이 판단
-
net2를 수정하고 forward값 확인
net1, net2 적용했을때 나오는 값을 확인한다는 뜻
net2
- net2에서 Linear와 AdaptiveAvgPool2d의 적용순서를 바꿔줌
차원확인
net1(x).squeeze().shape
net2[2].weight.squeeze().shape
불필요한 값들 안 나오게 차원 조절해주기
Linear(in_features=16, out_features=1, bias=False) 를 적용: 16 $\times$ (16,12,12) $\to$ (12,12)
net2[2].weight.squeeze() @ net1(x).squeeze()
- 실패..
matrix 계산할때 사용할 수 있고, matrix보다 한 차원 높은 tensor라 적용을 할 수 없다.
camimg=torch.einsum('i,ijk -> jk',net2[2].weight.squeeze(), net1(x).squeeze())
camimg.shape
- 성공
einsum 에 변하고 싶은 차원, 벡터,벡터 입력
AdaptiveAvgPool2d(output_size=1) 를 적용
ap(camimg)
!!!!
똑같다?
-
아래의 값이 같다.
net2(net1(x)),ap(camimg)
-
왜냐하면 ap와 선형변환 모두 linear이므로 순서를 바꿔도 상관없음
-
아래와 결국 같은 이치
_x= np.array([1,2,3,4])
_x
np.mean(_x*2+1)
2*np.mean(_x)+1
-
이제 camimg 에 관심을 가져보자.
camimg
ap(camimg), torch.mean(camimg)
- 이미지의 값은 대부분 0이지만 궁극적으로는 평균을 내서 음수의 값이 나와야 한다.
-
결국 특정픽셀에서 큰 음의 값이 나오기 떄문에 궁극적으로는 평균이 음수가 된다.
- 평균이 음수이다. $\leftrightarrow$ 이미지가 의미하는것이 7이다.
- 특정픽셀이 큰 음수값을 가진다. $\leftrightarrow$ 그 픽셀에서 이미지가 7임을 뚜렷하게 알 수 있다.
-
그 특정픽셀이 어딘가?
plt.imshow(camimg.data)
- 초록색으로 표현된 부분은 CNN모형이 이 숫자를 7이라고 생각한 근거가 된다.
-
원래의 이미지와 비교
plt.imshow(x.squeeze())
-
두 이미지를 겹쳐서 그리면 멋진 그림이 될 것 같다.
step1: 원래이미지를 흑백으로 그리자.
plt.imshow(x.squeeze(),cmap='gray',alpha=0.5)
-
step2: 원래이미지는 (28,28)인데 camimg는 (12,12)픽셀 $\to$ camimg의 픽셀을 늘리자.
plt.imshow(camimg.data,alpha=0.5, extent=(0,27,27,0),interpolation='bilinear',cmap='magma')
fixel 사이의 값을 smoothing하기 위해 interpolation에 bilinear 옵션 취하기
fastai 교재에 많이 사용되는 cmap의 magma 옵션 사용
-
step3: 합치자.
plt.imshow(x.squeeze(),cmap='gray',alpha=0.5)
plt.imshow(camimg.data,alpha=0.5, extent=(0,27,27,0),interpolation='bilinear',cmap='magma')
-
숫자3이 그려진 이미지를 observation으로 선택하고 위와 같이 cam을 이용하여 시각화하라.
-
준비과정1: 시각화할 샘플을 하나 준비하자.
_x=X[-100].reshape(1,1,28,28)
_camimg=torch.einsum('i,ijk -> jk',net2[2].weight.squeeze(), net1(_x).squeeze())
plt.imshow(_x.squeeze(),cmap='gray',alpha=0.5)
plt.imshow(_camimg.data,alpha=0.5, extent=(0,27,27,0),interpolation='bilinear',cmap='magma')