본문 바로가기

책/파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습

03 파이토치 기초 (5) 활성화 함수

활성화 함수

활성화 함수(Activation Function)란 인공 신경망에서 사용되는 은닉층을 활성화하기 위한 함수다. 여기서 활성화란 인공 신경망의 뉴런의 출력값을 선형에서 비선형으로 변환하는 것이다. 즉, 활성화 함수는 네트워크가 데이터의 복잡한 패턴을 기반으로 학습하고 결정을 내릴 수 있게 제어한다.

 

활성화 함수가 선형 구조라면, 미분 과정에서 항상 상수가 나오므로 학습을 진행하기가 어렵다. 다시 말해 활성화 함수는 입력을 정규화(Normalization)하는 과정으로 볼 수 있다.

 

이진 분류

이진 분류란 규칙에 따라 입력된 값을 두 그룹으로 분류하는 작업을 의미한다. 분류 결과가 맞는다면 1(True)을 반환하며, 아니라면 0(False)을 반환한다. 참 또는 거짓으로 결과를 분류하기 때문에 논리 회귀(Logistic Regression)라고도 부른다.

자세한 내용은 이전 포스팅을 참고하자.

2023.12.19 - [Machine Learning/Business Analytics 1] - Logistic Regression : Formulation

 

Logistic Regression : Formulation

자료 출처 결과가 오직 0 & 1로만 이루어져 있는 경우 선형 모델이 적합한가? 분류 문제에서 문제점 이진 분류에서는 오직 0과 1 두 가지 결괏값만 가능하다. 회귀식은 생성된 값에 제한이 없다.

ai-junha.tistory.com

2023.12.19 - [Machine Learning/Business Analytics 1] - Logistic Regression : Learning

 

Logistic Regression : Learning

자료 출처 Estimating the coefficients 두 개의 서로 다른 로지스틱 모델이 있다고 가정해보자. 각 모델은 아래와 같이 동일한 데이터 세트에 대해 예측한다. 어떤 모델이 더 좋은가? 모델 A가 정답 레이

ai-junha.tistory.com

2023.12.20 - [Machine Learning/Business Analytics 1] - Logistic Regression : Interpretation

 

Logistic Regression : Interpretation

자료 출처 Meaning of coefficient 선형 회귀의 경우 coefficient에 대한 해석이 용이하다. 예를 들면 $x_1$이 1 증가하면 예측 값은 $\hat {\beta_1}$ 만큼 증가하기 때문에 변수의 영향력이 어느정도인지 직관

ai-junha.tistory.com

 

시그모이드 함수

시그모이드 함수의 수식은 다음과 같다.

시그모이드 함수의 x의 계수에 따라 S자형 곡선이 완만한 경사를 갖게 될지, 급격한 경사를 갖게될지 설정할 수 있다.

  • 시그모이드 함수의 출력 값은 (0, 1)의 범위를 가지며 계수가 0에 가까워질수록 완만한 경사를 갖게 되며, 0에서 멀어질수록 급격한 경사를 갖게 된다. 
  • 분류 cut off를 0.5로 설정한다면 시그모이드 함수를 통해 나온 출력값이 0.5보다 낮으면 False, 0.5보다 크면 True로 분류할 수 있다.
  • 시그모이드 함수는 유연한 미분값을 가지므로, 입력에 따라 값이 급격하게 변하지 않는다는 장점이 있고, 출력값의 범위가 (0, 1)로 제한됨으로써 기울기 폭주 문제가 발생하지 않는다.
  • 대신에 시그모이드 함수를 활성화 함수로 사용하는 계층이 많아질수록 기울기가 0에 수렴되는 기울기 소실 문제를 일으킨다.

 

이진 교차 엔트로피

분류 문제에서 MSE를 오차 함수로 사용하면 예측값과 실젯값의 차이가 작으면 계산되는 오차의 크기가 작아져 학습을 원할하게 진행하기 어렵다. 

이러한 경우를 방지하고자, 이진 교차 엔트로피(Binary Cross Entropy, BCE)을 오차 함수로 사용한다. (입력 데이터의 분포가 가우시안 분포의 형태를 따르면 MSE를 사용하고, 베르누이 분포의 형태를 따른다면 교차엔트로피를 사용)

 

def BCE_1(y, yhat):
  return -(y*np.log(yhat))

def BCE_0(y, yhat):
  return -(1-y)*np.log(1-yhat)

def BCE(y, yhat):
  return BCE_1(y, yhat) + BCE_0(y, yhat)
  • BCE_1은 y=1일 때 적용되는 함수이다. y=1일 때 BCE_0의 값은 0이기 때문이다. 반대로 BCE_0은 y=0일 때 적용되는 함수이다.
  • 로그 함수의 경우 한쪽으로는 무한대로 이동하며 다른 한쪽으로는 0에 가까워지기 때문에 기울기가 0이되는 지점을 찾기 위해 두 가지 로그 함수를 하나로 합쳐 사용한다.
yhat = np.arange(0.01, 1, 0.01)
for y in [0, 1]:
  plt.plot(yhat, BCE(y, yhat), label=f'BCE #{y}')
  plt.legend()
  plt.grid()
plt.xlabel('H(x)')
plt.ylabel('Cost')

이진 분류: 파이토치

데이터셋

 

binary.csv 파일을 사용해 비선형 회귀를 구현해보자. 데이터는 다음과 같은 형태로 제공된다.

x, y, z와 pass의 관계는 x, y, z가 모두 40 이상이며, 평균이 60 이상일 때 True를 반환한다.

 

사용자 정의 데이터세트

class CustomDataset(Dataset):
  def __init__(self, file_path):
    df = pd.read_csv(file_path)
    self.x1 = df.iloc[:, 0].values
    self.x2 = df.iloc[:, 1].values
    self.x3 = df.iloc[:, 2].values
    self.y = df.iloc[:, 3].values
    self.lenght = len(df)

  def __getitem__(self, index):
    x = torch.tensor([self.x1[index], self.x2[index], self.x3[index]], dtype=torch.float)
    y = torch.tensor([self.y[index], dtype=torch.float])
    return x, y

  def __len__(self):
    return self.length

 

사용자 정의 모델

class CustomModel(nn.Module):
  def __init__(self):
    super().__init__()
    self.layer = torch.nn.Sequential(
        torch.nn.Linear(3, 1),
        torch.nn.Sigmoid()
    )

  def forward(self, x):
    x = self.layer(x)
    return x
  • Sequential을 활용해 여러 계층을 하나로 묶는다. 묶어진 계층은 순차적으로 실행되며 가독성을 높일 수 있다.
  • 입력 데이터 차원 크기는 x1, x2, x3으로 3개이므로 3을 입력하고, 출력 데이터 차원 크기는 1을 입력한다.
  • 시그모이드 함수를 활성화 함수로 적용할 예정이므로 이를 선형 변환 함수 뒤에 연결한다.

데이터 로더

dataset = CustomDataset(DATA_PATH / "binary.csv")
dataset_size = len(dataset)
train_size = int(dataset_size * 0.8)
validation_size = int(dataset_size * 0.1)
test_size = dataset_size - train_size - validation_size

train_dataset, validation_dataset, test_dataset = random_split(dataset, [train_size, validation_size, test_size], torch.manual_seed(4))
train_dataloader = DataLoader(train_dataset, batch_size=64, shuffle=True, drop_last=True)
validation_dataloader = DataLoader(validation_dataset, batch_size=4, shuffle=True, drop_last=True)
test_dataloader = DataLoader(test_dataset, batch_size=4, shuffle=True, drop_last=True)


device 설정

device = "cuda" if torch.cuda.is_available() else "cpu"
model = CustomModel().to(device)
criterion = nn.BCELoss().to(device) # 이진 교차 엔트로피
optimizer = optim.SGD(model.parameters(), lr=0.0001)
  • nn 모듈의 BCELoss 클래스로 criterion 인스터를 생성한다.

학습

for epoch in range(10000):
  cost = 0.0

  for x, y in train_dataloader:
    x = x.to(device)
    y = y.to(device)

    output = model(x)
    loss = criterion(output, y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    cost += loss

  cost = cost / len(train_dataloader)

  if (epoch + 1) % 1000 == 0:
    print(f"Epoch : {epoch+1:4d}, Model : {list(model.parameters())}, Cost : {cost:.3f}")

 

평가

with torch.no_grad():
  model.eval()
  for x, y in validation_dataloader:
    x = x.to(device)
    y = y.to(device)

    outputs = model(x)

    print(outputs)
    print(outputs >= torch.FloatTensor([0.5]).to(device))
    print("--------------------")

 

비선형 활성화 함수

계단 함수

계단 함수의 입력값의 합이 임곗값을 넘으면 0을 출력하고, 넘지 못하면 1을 출력한다.

 

딥러닝 모델에서는 사용되지 않는 함수로 임곗값에서 불연속점을 가지므로 미분이 불가능해 학습을 진행할 수 없다. 또한, 역전파 과정에서 데이터가 극단적으로 변경되기 때문에 적합하지 않다.

def step_function(x):
  y = x.copy()
  y[y >= 0] = 1
  y[y < 0] = 0
  return y

x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)
plt.xlabel("x")
plt.ylabel("y")
plt.savefig(SAVE_PATH / "step_function.png")

임곗값 함수

임곗값(threshold)보다 크면 입력값을 그대로 전달하고, 임곗값보다 작으면 특정 값으로 변경한다.

마찬가지로 기울기를 계산할 수 없으므로 네트워크를 최적화하기 어려워 사용되지 않는다.

def threshold_function(x, threshold=0.0):
  y = x.copy()
  y[y < threshold] = -1.0
  return y

x = np.arange(-5.0, 5.0, 0.01)
y = threshold_function(x)
plt.plot(x, y)
plt.grid()
plt.xlabel("x")
plt.ylabel("y")
plt.savefig(SAVE_PATH / "threshold_function.png")

 

하이퍼볼릭 탄젠트 함수

시그모이드와 유사한 형태를 지니지만, 출력값의 중심이 0이고, 출력 값의 범위는 (-1, 1)이다.

 

출력값의 범위가 더 넓고 다양한 형태로 활성화할 수 있지만, 입력값이 4보다 큰 경우에 출력값이 1에 수렴하므로 동일하게 기울기 소실이 발생한다.

def tanh_function(x):
  return np.tanh(x)
  
x = np.arange(-5.0, 5.0, 0.01)
y = tanh_function(x)
plt.plot(x, y)
plt.grid()
plt.xlabel("x")
plt.ylabel("y")
plt.savefig(SAVE_PATH / "tanh_function.png")

 

ReLU

0보다 작거나 같으면 0을 반환하며, 0보다 크면 선형 함수에 값을 대입하는 구조를 갖는다. ReLU 함수는 선형 함수에 대입하므로 입력값이 양수로하면 출력값이 제한되지 않아 기울기 소실이 발생하지 않는다.

 

또한 수식이 매우 간단해 순전파나 역전파의 연산이 빠르다. 하지만 입력값이 음수인 경우 항상 0을 반환하므로 가중치나 편향이 갱신되지 않을 수 있다.

 

def relu_function(x):
  return np.maximum(0, x)

x = np.arange(-5.0, 5.0, 0.01)
y = relu_function(x)
plt.plot(x, y)
plt.grid()
plt.xlabel("x")
plt.ylabel("y")
plt.savefig(SAVE_PATH / "relu_function.png")

LeakyReLU

음수 기울기를 제어하여 죽은 뉴런 현상을 방지하기 위해 사용된다. 양수인 경우 ReLU와 동일하지만, 음수인 경우 작은 값이라도 출력시켜 기울기를 갱신하게 한다.

def leaky_relu_function(x, alpha=0.1):
  return np.maximum(alpha * x, x)

x = np.arange(-5.0, 5.0, 0.01)
y = leaky_relu_function(x)
plt.plot(x, y)
plt.grid()
plt.xlabel("x")
plt.ylabel("y")
plt.savefig(SAVE_PATH / "leaky_relu_function.png")

비슷하게 PReLU가 있는데 이는 학습 과정에서 negative slope인 alpha 값을 학습을 통해 갱신되는 값으로 간주한다.

 

ELU

음수 부분을 지수함수를 사용하여 부드러운 곡선의 형태를 갖는다. ELU 함수는 음의 기울기에서 비선형 구조를 가지므로 입력값이 0인 경우에도 출력값이 급변하지 않아, 경사 하강법의 수렴속도가 비교적 빠르다.

 

하지만 더 복잡한 연산을 진행하게 되므로 학습 속도는 느려진다.

 

def elu_function(x, alpha=0.2):
  return np.where(x > 0, x, alpha * (np.exp(x) - 1))
  
x = np.arange(-2.0, 2.0, 0.01)
y = elu_function(x)
plt.plot(x, y)
plt.grid()
plt.xlabel("x")
plt.ylabel("y")
plt.savefig(SAVE_PATH / "elu_function.png")

 

소프트맥스 함수

차원 벡터에서 특정 출력값이 k 번째 클래스에 속할 확률을 계산한다. 클래스에 속할 확률을 계산하는 활성화 함수이므로, 은닉층에서 사용하지 않고 출력츠에서 사용된다.

def softmax_function(x):
  return np.exp(x) / np.sum(np.exp(x))

모든 객체에 대한 softmax의 출력값의 범위는 (0, 1)을 가지고 출력값의 합은 1이다. 따라서 출력을 가능한 클래스에 대한 확률 분포로 매핑한다. 이외에도 Softmin, Log Softmax 등이 있다.