Comment :
Udacity DeepLearning - Project: Generate Faces을 진행하다가 network의 weight를 Normal Distribution(정규분포)로 바꿔주는 부분이 나왔다.
따라서 Python Class로 정의한 network(In Project, like Discriminator, Generator) 변수를 통해 접근해야 하는데 도무지 (Python Class로) 생성된 Instance, 즉 network의 각 layer에 접근하는 방법을 모르겠어서...! 아래와 같이 정리한다
먼저 아래와 같은 network가 있다고 해보자 (아직은 객체화, 즉 Instantiate되지 않은 상태)
코드가 다소 복잡하지만 핵심구조는 아래와 같다.
class SampleNet(nn.Module):
def __init__(self, conv_dim):
super(SampleNet, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(in_channels=3,
out_channels=conv_dim,
kernel_size=4,
stride=1,
padding=1,
bias=False)
)
self.layer2 = nn.Sequential(
nn.Conv2d(in_channels=conv_dim,
out_channels=conv_dim*2,
kernel_size=4,
stride=1,
padding=1,
bias=False),
nn.BatchNorm2d(conv_dim*2)
)
self.layer3 = nn.Sequential(
nn.Conv2d(in_channels=conv_dim*2,
out_channels=conv_dim*4,
kernel_size=4,
stride=1,
padding=1,
bias=False),
nn.BatchNorm2d(conv_dim*4)
)
self.fc = nn.Linear(4*4*conv_dim*4, 1)
def forward(self, x):
x = F.relu(self.layer1(x))
x = F.relu(self.layer2(x))
x = F.relu(self.layer3(x))
# Flatten
x = x.view(-1, self.conv_dim*4*4*4)
out = self.fc(x)
return out
또한 목표는 🔥으로 표시한 layer의 weight를 normal distribution(mean=0, std=0.02)으로 변환하는 것이다 Conv2d and Linear layer
먼저 아래와 같이 t
변수에 network를 instantiate(객체화)해주자
t = SampleNet(conv_dim = 64)
SampleNet(
(layer1): Sequential(
(0): Conv2d(3, 64, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
)
(layer2): Sequential(
(0): Conv2d(64, 128, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(layer3): Sequential(
(0): Conv2d(128, 256, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(fc): Linear(in_features=4096, out_features=1, bias=True)
)
Instance t
에 .named_children()
를 실행시키면 (아래 코드와 같이) Generator 객체가 생성된다.
t.named_children()
>>>
<generator object Module.named_children at 0x000002699AE9A350>
[!] generator object에 대해서는 나중에 정리하고, 일단은 for loop를 통해 generator를 이용해보자
for name, child in t.named_children(): # named_children() generator를 통하여 각 name, child 반환가능
print(f"이름: {name}")
print(child)
print(type(child))
print()
이름: layer1
Sequential(
(0): Conv2d(3, 64, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
)
<class 'torch.nn.modules.container.Sequential'>
이름: layer2
Sequential(
(0): Conv2d(64, 128, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
<class 'torch.nn.modules.container.Sequential'>
이름: layer3
Sequential(
(0): Conv2d(128, 256, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
<class 'torch.nn.modules.container.Sequential'>
이름: fc
Linear(in_features=4096, out_features=1, bias=True)
<class 'torch.nn.modules.linear.Linear'>
[+] module : n. 모듈(컴퓨터, 특정 기능을 하는 시스템이나 프로그램의 단위), 조립 부품
🚀 .named_children()
generator를 통해서 각 Layer의 이름(사용자 정의 이름)과 Child class(하위 클래스)을 반환
[!] Sequential
로 구성된 부분은 '통'으로 인지하고 있음 (속에 Convolutional, BatchNorm layer있음)
[!] 따라서 아래와 같이 child class에도 .named_children()
으로 접근하여 (child class 하위 child인) class에도 접근가능
[!] name
은 언제나 사용자 정의 이름인거를 체크하자!
for name, child in t.named_children():
print(f"이름: {name}")
print(child)
# module이 Sequential일 경우 하위 module도 출력
if isinstance(child, nn.Sequential):
for sub_name, sub_child in child.named_children():
print(f"이름: {sub_name}")
print(sub_child)
print()
이름: layer1
Sequential((0): Conv2d(3, 64, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False))
이름: 0
Conv2d(3, 64, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
이름: layer2
Sequential((0): Conv2d(64, 128, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True))
이름: 0
Conv2d(64, 128, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
이름: 1
BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
이름: layer3
Sequential((0): Conv2d(128, 256, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True))
이름: 0
Conv2d(128, 256, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
이름: 1
BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
이름: fc
Linear(in_features=4096, out_features=1, bias=True)
자 이제 named_children()
을 통하여 Linear layer(sub, child class)에 접근하여 weight를 변경해보자
for _, child in t.named_children():
if isinstance(child, nn.Linear):
print(f"변경 전 weight: {child.weight}")
child.weight.data.fill_(7)
print(f"변경 후 weight: {child.weight}")
변경 전 weight: Parameter containing:
tensor([[ 0.0002, -0.0105, 0.0028, ..., 0.0121, -0.0053, -0.0080]],
requires_grad=True)
변경 후 weight: Parameter containing:
tensor([[7., 7., 7., ..., 7., 7., 7.]], requires_grad=True)
named_children()
generator + for loop를 통해서 모든 layer를 돈다.isinstance
를 통해 해당 layer가 nn.Linear
일 경우이번에는 여러개의 Convolutional layer(Sequential의 하위클래스, 여러개있음)에 접근하여 weight를 변경해보자
[!] weight의 차원이 너무 커서 출력 시 보기힘들어서 [0,0,0]부분만 인덱싱하여 출력함
for _, child in t.named_children():
if isinstance(child, nn.Sequential): # Sequential 밑으로 접근
for _, sub_child in child.named_children():
if isinstance(sub_child, nn.Conv2d):
print(f"변경 전 weight: {sub_child.weight[0,0,0]}")
sub_child.weight.data.fill_(741)
print(f"변경 후 weight: {sub_child.weight[0,0,0]}")
변경 전 weight: tensor([ 0.0725, 0.0872, 0.0445, -0.0856], grad_fn=<SelectBackward0>)
변경 후 weight: tensor([741., 741., 741., 741.], grad_fn=<SelectBackward0>)
변경 전 weight: tensor([-0.0140, 0.0265, -0.0020, 0.0028], grad_fn=<SelectBackward0>)
변경 후 weight: tensor([741., 741., 741., 741.], grad_fn=<SelectBackward0>)
변경 전 weight: tensor([ 0.0049, 0.0044, -0.0071, 0.0044], grad_fn=<SelectBackward0>)
변경 후 weight: tensor([741., 741., 741., 741.], grad_fn=<SelectBackward0>)
귀찮지만 마지막으로 Convolutional, Linear layer에 접근하여 weight를 normal distribution(mean=0, std=0.02)로 변환, 즉 initiate(초기화)해보자
[+] normal distribution initiate : .normal_(mean, std)
[!] Convolutional layer의 weight의 차원이 너무 커서 출력 시 보기힘들어서 [0,0,0]부분만 인덱싱하여 출력함
for _, child in t.named_children():
# Linear layer
if isinstance(child, nn.Linear):
print(f"weight before initiate: {child.weight}")
child.weight.data.normal_(0.0, 0.02)
print(f"weight after initiate: {child.weight}")
# Sequential -> Conv layer
if isinstance(child, nn.Sequential):
for _, sub_child in child.named_children():
if isinstance(sub_child, nn.Conv2d):
print(f"weight before initiate: {sub_child.weight[0,0,0]}")
sub_child.weight.data.normal_(0.0, 0.02)
print(f"weight after initiate: {sub_child.weight[0,0,0]}")
weight before initiate: tensor([ 0.0821, -0.0893, 0.1116, 0.0880], grad_fn=<SelectBackward0>)
weight after initiate: tensor([0.0557, 0.0132, 0.0125, 0.0140], grad_fn=<SelectBackward0>)
weight before initiate: tensor([-0.0125, -0.0125, -0.0167, 0.0039], grad_fn=<SelectBackward0>)
weight after initiate: tensor([-0.0055, 0.0099, -0.0086, 0.0158], grad_fn=<SelectBackward0>)
weight before initiate: tensor([-0.0019, 0.0149, 0.0081, 0.0183], grad_fn=<SelectBackward0>)
weight after initiate: tensor([-0.0094, -0.0180, -0.0129, 0.0009], grad_fn=<SelectBackward0>)
weight before initiate: Parameter containing:
tensor([[ 0.0092, -0.0120, -0.0044, ..., 0.0151, -0.0154, -0.0080]], requires_grad=True)
weight after initiate: Parameter containing:
tensor([[ 0.0344, -0.0263, -0.0140, ..., 0.0181, -0.0210, 0.0099]], requires_grad=True)
_modules
를 network에 적용시키면 (해당 network class들이 나열된) OrderedDict를 반환해준다
[+] collections.OrderedDict : 순서가 있는 Dictionary, 자세한 내용은 링크 참조
print(type(t._modules))
print(t._modules)
collections.OrderedDict # t._modules의 type check!
OrderedDict([('layer1',
Sequential(
(0): Conv2d(3, 64, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
)),
('layer2',
Sequential(
(0): Conv2d(64, 128, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)),
('layer3',
Sequential(
(0): Conv2d(128, 256, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)),
('fc', Linear(in_features=4096, out_features=1, bias=True))])
위에서 보이는 각 layer들의 '이름'으로 (Python Dictionary 접근하듯이) 접근하면각 layer에 접근가능하다
t._modules['layer1']
>>>
Sequential((0): Conv2d(3, 64, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False))
t._modules['layer2']
>>>
Sequential((0): Conv2d(64, 128, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True))
[!] Dictionary처럼 _modules['...']['...']로 접근하는것이 아니라 계속 _modules로 접근해줘야한다
이는 _modules로 접근 시 반환받는 것이 단순 value가 아니라 module이기 때문인것 같다
[!] Sequential module class!
'layer1'
의'0'
이름의 layer class를 바로 반환받음!type(t._modules['layer1']._modules['0']) >>> torch.nn.modules.conv.Conv2d
t._modules['layer2']._moduels['1']
>>>
BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(방금 위에서 말한듯이) Dictionary 접근 시 어떤 str같은 값을 반환하는 것이 아닌 해당 module class를 반환해주므로 바로 weight, bias등에 접근할 수 있다.
[!] generator + for loop의 방식이 아닌 Ordered Dictionary 반환이므로 특정 layer 1개에 접근하는 것이 편하다
print(t._modules['layer1']._modules['0'])
print(t._modules['layer1']._modules['0'].weight)
>>>
Conv2d(3, 64, kernel_size=(4, 4), stride=(1, 1), padding=(1, 1), bias=False)
Parameter containing:
tensor([[[[ 5.5747e-02, 1.3203e-02, 1.2492e-02, 1.4003e-02],
...
[-1.3805e-04, 2.3481e-02, 1.0857e-02, 3.2397e-02]]]],
requires_grad=True
이제 (named_children()
으로 특정 layer의 weight를 초기화한것처럼) Initiate를 해보자
[+] 두번째 layer의 Convolutional layer의 weight만 normal distribution으로 initiate
print(f"Before Initiate: {t._modules['layer2']._modules['0'].weight[0,0,0]}")
t._modules['layer2']._modules['0'].weight.data.fill_(741)
print(f"After Initiate: {t._modules['layer2']._modules['0'].weight[0,0,0]}")
>>>
Before Initiate: tensor([-0.0100, 0.0191, -0.0037, -0.0068], grad_fn=<SelectBackward0>)
Before Initiate: tensor([741., 741., 741., 741.], grad_fn=<SelectBackward0>)
Udacity Project solution github 참고 여기
음... network는 하나의 Class에 여러개의 하위 Class가 있는 구조(?)
따라서 .apply(function)
을 적용하면 하위 Class에 (순차적으로) function
이 접근이 된다(?)
아래와 같은 function을 network에 apply하면 특정 layer(Convolutional, Linear)의 weight를 normali distribution으로 initiate할 수 있다.
from torch.nn import unit
def weights_init_normal(m):
classname = m.__class__.__name__
isConv = classname.find('Conv') != -1
isLinear = classname.find('Linear') != -1
if (hasattr(m, 'weight') and isConv or isLinear):
init.normal_(m.weight.data, 0.0, 0.02)
Udacity Project solution github 참고 [여기](Udacity Project solution github 참고)
def weights_init_normal(m):
classname = m.__class__.__name__
if hasattr(m, 'weight') and (classname.find('Conv') != -1 or classname.find('Linear') != -1):
m.weight.data.normal_(0.0, 0.02)
# The bias tern, if they exist, set to 0
if hasattr(m, 'bias') and m.bias is not None:
m.bias.data.zero_()