#include <stdio.h>

int main(void)
{
	int A = 0;
	int B = 0;
	
	scanf("%d %d", &A, &B);
	
	if (A < B)
		printf("<");
	
	if (A > B)
		printf(">");
		
	if (A == B)
		printf("==");
		
	return 0;
}

 

import sys

def check_group_word(word):
    cache = []
    for i in word:
        if i in cache:
            if i == cache[-1]:
                pass
            else:
                return False
        else:
            cache.append(i)
    return True

if __name__ == "__main__":
    input = sys.stdin.readline
    N = int(input())
    cnt = 0
    for _ in range(N):
        word = input()
        bool = check_group_word(word)
        if bool:
            cnt +=1

    print (cnt)

 

import sys
from itertools import combinations
input = sys.stdin.readline
N, S = list(map(int, input().split()))
nums = list(map(int, input().split()))
cnt = 0
for i in range(1, len(nums)+1):
    result = combinations(nums, i)
    for num in result:
        if (sum(num) == S):
            cnt += 1
print (cnt)

 

 

합이 S만 되면 되기 때문에 순서성이 필요 없는 조합을 사용.

import sys
input = sys.stdin.readline
N = int(input())

words = []
for _ in range(N):
    words.append(input().strip('\n'))

words = list(set(words))
words.sort(key=lambda x: (len(x), x))
for i in words:
    print (i)

리스트 정렬할 때 sort의 파라미터로 lambda 식을 사용해서 정렬 기준들을 사용할 수 있음.

N, K = list(map(int, input().split()))
peoples = [i+1 for i in range(N)]
sequence = []
idx = K - 1
        
for _ in range(N):
    if idx < len(peoples):
        sequence.append(peoples.pop(idx))
        idx = idx + K - 1
    
    elif idx >= len(peoples):
        idx = idx % len(peoples)
        sequence.append(peoples.pop(idx))
        idx = idx + K - 1

print ("<", ', '.join(str(i) for i in sequence), ">", sep='')

처음에 조건문을 idx <= len(peoples), idx > len(peoples)로 해서 오답으로 나왔음.

idx랑 len(peoples)가 같은 경우는 인덱스 초과이기 때문.

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int main(void)
{
	string s;
	cin >> s;
	int max = 0; 
	int temp = 0;
	bool flag = false;
	int arr[26] = { 0, };
	
	transform(s.begin(), s.end(), s.begin(), ::toupper); // 소문자화
	
	for (int i = 0; i < s.length(); i++)
		arr[int(s[i] - 65)] += 1;

	for (int i = 0; i < 26; i++)
	{
		if (arr[i] > max)
		{
			max = arr[i];
			temp = i;
		}
	}

	for (int i = 0; i < 26; i++)
	{
		if ((max == arr[i]) and (i != temp))
			flag = true;
	}

	if (flag)
		std::cout << "?" << endl;
	else
		std::cout << char(temp+65) << endl;

	return 0;
}
#include <iostream>
#include <string>
using namespace std;

int main(void)
{
	string s;
	int cnt = 1;

	getline(cin, s);
	char delim = 0x20;

	for (int i = 0; i < s.length(); i++)
	{
		if (s[i] == delim)
			cnt = cnt + 1;
	}

	if (s[0] == delim)
		cnt--;

	if (s[s.length() - 1] == delim)
		cnt--;

	cout << cnt << endl;
	return 0;
}

 

#include <iostream>
using namespace std;

int main(void)
{
	int N = 0;
	int count = 0;
	int quote = 0;
	int remain = 0;
	int sum = 0;
	int newNumber = 0;
	int tempN = 0;
	
	cin >> N;
	
	if (N>=0 and N<=99)
	{
		tempN = N;	
		while  (true)
		{
			quote = tempN / 10; //2, 6, 8, 4
			remain = tempN % 10; // 6, 8, 4, 2
			sum = quote + remain; // 8, 14, 12, 6
			
			if (sum <10)
			{
				sum = (sum * 10) + sum;
			}
			
			tempN = (remain * 10) + (sum % 10); // 8, 84, 42, 2
			count = count + 1; // 1, 2, 3, 4
			
			if (N == tempN)
			{
				cout << count << '\n';
				break;
			}	
		}
	}
	
	return 0;
}
import sys
input = sys.stdin.readline

def get_binary(X):
    if X == 0:
        return 

    Q, R = divmod(X, 2)
    binary.append(R)
    get_binary(Q)
    binary.reverse()

X = int(input())
binary = []
get_binary(X)
print (sum(binary))

 

import sys
input = sys.stdin.readline
x, y, w, h = list(map(int, input().split()))
print (min([x, w-x, h-y, y]))

최소값이 될 수 있는 경우는 크게 4가지가 있다. 경계선에 닿기만 하면 되는 최소 거리(직선)이므로 (x, y)에서 직선이 되는 경우 4가지 경우를 생각해서 최소값을 구하면 된다.

 

#include <iostream>
using namespace std;

bool hansoo(int i)
{
	if (i < 100)
		return true;
		
	int a = i / 100;
	int b = i / 10 % 10;
	int c = i % 10;
	
	if (a+c == 2*b)
	{
		return true;
	}
	
	return false;
}

int main(void)
{
	int N = 0;
	int count = 0;
	
	cin >> N;
	
	if (N>=1 and N<=1000) // Initial condition
	{
		for (int i=1; i<=N; i++)
		{
			if(hansoo(i))
				count = count + 1;
		}
		
		cout << count << endl;
	}
	
	return 0;
}
N = int(input())
A = list(map(int, input().split()))
B = list(map(int, input().split()))
S = 0

A = sorted(A)
B = sorted(B, reverse=True)

for idx in range(N):
    s = A[idx] * B[idx]
    S = S + s

print (S)

파이썬 내장함수 sorted를 이용해서 풀었다. 편리하게 사용 후 지적 태만감을 느낀다. 파이썬으로 어느정도 백준 문제 풀이에 익숙해지면 C++로 다시 풀 예정이다.

네 번 중첩되는 for 문이 사용될까 싶었는데 사용된다. 앞으로 브루트포스 문제를 만날 때면 시간 복잡도가 높아지는 것에 대해서 심적 부담감을 조금 내려 놓아 봐야 겠다.

N, M = list(map(int, input().split()))
chessboard = []
counts = []

for i in range(N):
    value = input()
    if len(value) != M:
        raise ValueError('Check size of chessboard')
    chessboard.append(value)
#print (chessboard)

for x in range(0, N - 8 + 1): # Abstract
    for y in range(0, M - 8 + 1): # Abstract
        white_start, black_start = 0, 0
        for i in range(x, x + 8): # Concrete
            for j in range(y, y + 8): # Concrete
                if (i + j) % 2 == 0:
                    if chessboard[i][j] != "W":
                        white_start +=1
                    if chessboard[i][j] != "B":
                        black_start +=1

                elif (i + j) % 2 == 1:
                    if chessboard[i][j] != "B":
                        white_start +=1
                    if chessboard[i][j] != "W":
                        black_start +=1
        counts.append(white_start)
        counts.append(black_start)

print (min(counts))
#include <stdio.h>

int main(void)
{
	int A = 0;
	int B = 0;
	
	scanf("%d %d", &A, &B);
	
	if (A>0 and B<10)
	{
		printf("%.9lf", (double)A/B);
	}
	
	return 0;
}

초기 코드 (실패)

가장 기본이 되는 재귀 형식으로 구현했더니 시간 초과 발생

import sys
input = sys.stdin.readline

def fibonacci(N):
    if N == 0:
        global zero
        zero += 1
        return 0
    elif N == 1:
        global one
        one += 1
        return 1
    else:
        return fibonacci(N-2) + fibonacci(N-1)

T = int(input())
for i in range(T):
    N = int(input())
    zero, one = 0, 0
    fibonacci(N)
    print (zero, one)

 

정답 코드 (성공)

핵심은 동적 프로그래밍의 메모이제이션 기법을 사용하는 것

이미 했던 계산을 반복 하는 경우, 기존에 계산 해두었던 값을 활용

 

import sys
input = sys.stdin.readline

zero = [1, 0, 1]
one = [0, 1, 1]

def fibonacci(N):
    length = len(zero)
    if N >= length:
        for i in range(length, N+1):
            zero.append(zero[i-1] + zero[i-2])
            one.append(one[i-1] + one[i-2])
    print (f"{zero[N]} {one[N]}")

T = int(input())
for i in range(T):
    N = int(input())
    fibonacci(N)
#include <stdio.h>

int main(void)
{
	int i = 0;
	int j = 0;
	
	scanf("%d %d", &i, &j);
	
	printf("%d", i-j);
	
	return 0;
}

1. C++

#include <stdio.h>

int main(void)
{
	int i = 0;
	int j = 0;
	
	scanf("%d %d", &i, &j);
	
	printf("%d", i+j);
	
	return 0;
}

 

2. Python

A, B = list(map(int, input().split()))
print (A+B)

이 문제를 해결하는 데는 두 가지 방법이 존재함.

 

두 문제에 공통적으로 사용되는 요소는 조합.

 

조합 = 서로 다른 n개 중에 r개를 선택하는 경우의 수 의미.

 

${}_nC_r = {n! \over (n-r)!r!}$

 

1. 다이나믹 프로그래밍 적용 X

def factorial(N):
    if N > 1:
        return (N * factorial(N-1))
    else:
        return 1

T = int(input())
for _ in range(T):
    N, M = list(map(int, input().split()))
    print (factorial(M) //  (factorial(M-N) * factorial(N)))

 

2. 다이나믹 프로그래밍 적용 O

T = int(input())
dp = [[0] * 30 for _ in range(30)]
for i in range(30):
    for j in range(30):
        if i == 1:
            dp[i][j] = j
        else:
            if i == j:
                dp[i][j] = 1
            elif i < j:
                dp[i][j] = dp[i-1][j-1] + dp[i][j-1]

for _ in range(T):
    N, M = list(map(int, input().split()))
    print (dp[N][M])

 

NAVER CLOVA 이기창님의 『BERT와 GPT로 배우는 자연어 처리』 책 내용 요약과, 알고 있는 내용을 기반으로 각색하여 작성되었습니다. 이 책은 전반적으로 누구나 이해하기 쉬운 비유와 그림을 사용하고 핵심만 이야기 해주기 때문에 정보의 홍수로부터 자유로울 수 있다는 것이 장점인 것 같습니다. 쉬운 실습도 함께 포함되어 있어 자연어처리에 입문하는 사람이 보면 큰 맥락을 파악하기에 좋은 책이 되는 것 같습니다.

 

1. Transformer: 최근 자연어처리의 핵심 모델

최근 자연어처리를 이해하는 데 있어서 알아야 할 핵심 한 가지가 있다면 Transformer이다. 이 책에서 설명하는 BERT 모델과 GPT 모델은 모두 Transformer를 기반으로 하기 때문이다. BERT와 GPT 모델이 자연어처리 트렌드에 있어 의의가 있는 것은 크게 3가지 이유가 있다. 첫 번째는 Transformer를 통해 대규모 언어 모델 학습을 할 수 있게 되었고, 두 번째는 대규모 언어 모델 학습을 통해 전이 학습(Transfer Learning)이 가능해졌다는 것이며, 세 번째는 대규모 언어 모델 학습과 전이 학습을 통해 기존 존재하던 모델들의 성능을 압도했기 때문이다. 다른 말로 표현하면 자연어처리 패러다임의 변화(paradigm shift)를 가져온 것이다. Transformer에 대해 자세히 알기 전에 먼저 전이 학습이란 무엇인가를 살펴보자.

 

2. 전이 학습 (Transfer Learning)

전이 학습이란 쉽게 말해 축구, 배구, 농구, 탁구 등의 여러 운동을 넓게 미리 가르쳐 일종의 운동신경을 만들어두면 배우지 않았던 특정 종목인 피구, 야구도 쉽게 배울 수 있는 것을 의미한다. 물리학, 화학, 지구과학을 미리 배워두면 배웠던 지식들로 하여금 생명과학이라는 분야에도 "전이"를 통해 쉽게 배울 수 있는 것과 같다. 정리하자면 특정 태스크를 학습한 모델을 다른 태스크 수행에 재사용하는 기법이라 할 수 있다. 이 전이 학습의 장점으로는 모델학습 속도 향상이 가능하고, 새로운 태스크를 더 잘 수행할 수 있다는 점이다. 이러한 전이 학습을 활용하는 대표적인 모델의 예시가 BERT와 GPT인 것이다. 전이 학습 방법은 크게 두 가지로 나뉜다. 업스트림 태스크(upstream task)와 다운스트림 태스크(downstream task)다.

 

2.1 업스트림 태스크 (upstream task)

업스트림 태스크는 쉽게 말해 일단 넓게 배우는 것이다. 넓게 배우기 위해 대규모 말뭉치(corpus)를 학습한다. 학습하기 위한 대표적인 방법으로 크게 2가지로 다음 단어(문장) 맞히기 (Next Sentence Prediction, NSP)와 빈칸 맞히기 (Masked Language Model, MLM)가 있다. NSP는 일종의 문장간의 연관성을 학습하여 어떤 단어 뒤에 나올 다음 단어는 무엇인가 맞히는 것이며, 빈칸 맞히기는 단어에 빈칸을 둔 뒤 이를 맞히는 것이다. 이러한 방법을 통해 업스트림 태스크를 수행하는 것을 pre-training이라고 한다. 대표적으로 BERT 모델은 위 두 가지 방식 모두를 사용하여 업스트림 태스크(=pre-training)를 수행하고, GPT는 NSP 방법만을 통해 업스트림 태스크를 수행한다. 

 

NSP에 대해 조금더 구체적으로 이야기하면, "티끌 모아 __"일 때, "티끌 모아"는 문맥이 되고, "__"는 맞춰야할 다음 단어가 된다. 정답이 "태산"일 경우 태산에 대한 확률 값을 높이고 이외의 모든 단어는 확률을 낮추는 방향으로 모델을 업데이트 하는 것이다.

 

MLM 또한 예를 들어 "티끌 __ 태산"일 때, 티끌과 태산은 문맥이 되며 "__"은 맞혀야 할 빈칸 단어가 된다. 정답이 "모아"일 경우 "모아"에 대한 확률이 높아지며 이외의 단어는 확률이 낮아지는 방향으로 모델이 업데이트 된다.

 

이러한 업스트림 태스크의 장점은 self-supervised 학습이 가능하다는 것이다. 다른 말로 손수 라벨을 붙인 데이터가 필요한 지도 학습과 달리 수작업 라벨링 작업 없이 학습 데이터 생성이 가능하다는 것이다. 즉 데이터 내에서 정답을 생성하고 이를 바탕으로 학습하는 방법을 의미한다.

 

2.2 다운스트림 태스크 (downstream task)

다운스트림 태스크는 쉽게 말해 깊게 배우는 것을 의미한다. 업스트림 태스크를 통해 넓게 배운 다음 깊게 배우는 것이다. 다른 말로 downstream task는 실제로 구체적으로 풀고자하는 문제를 수행하는 것을 의미한다. 여러가지 task를 생성할 수 있지만 대표적인 예시는 아래와 같다.

- 문장 분류 (SC): → 문장에 대한 긍정, 부정, 중립에 대한 확률 값을 반환하는 태스크이다.

- 자연어 추론 (NLI) → 문장에 대한 참, 거짓, 중립 확률 값 반환하는 태스크이다.

- 개체명 인식 (NER) → 기관명, 인명, 지명 등 개체명 범주 확률값 반환하는 태스크이다.

- 질의 응답 (QA) → 질문이 주어질 때 답변에 대한 확률값을 반환하는 태스크이다 (각 단어가 정답의 시작일 확률값 + 끝일 확률값 반환

- 문장 생성 (SG) → 문장을 입력 받고 어휘 전체에 대한 확률 값 반환하는 태스크이다.

 

다운스트림 태스크에는 크게 3가지 방법이 존재한다. 파인 튜닝(fine-tuning)프롬프트 튜닝(prompt-tuning)인컨텍스트 러닝(in-context learning)이다. 

 

2.2.1 파인튜닝 (fine-tuning)

다운스트림 태스크 데이터 전체를 사용하는 것으로, 모델 전체를 업데이트 하는 것이 특징이다. 단점으로는 언어 모델이 크면 클수록 모델 전체 업데이트에 필요한 계산 비용이 발생한다. 이러한 단점으로 인해 프롬프트 튜닝과 인컨텍스트 러닝이 주목을 받는다.

 

2.2.2 프롬프트 튜닝(prompt tuning)

다운스트림 태스크 데이터 전체를 사용해서 모델을 일부 업데이트 하는 방법을 의미한다. 

 

2.2.3 인컨텍스트 러닝(in-context learning)

다운스트림 태스크 데이터 일부만 사용하는 방법으로 모델을 업데이트하지 않고 다운스트림 태스크를 수행하는 방법이다. 인컨텍스트 러닝은 크게 3가지 방식으로 나뉜다. 제로샷 러닝(zero-shot learning), 원샷 러닝(one-shot learning), 퓨샷 러닝(few-shot learning)이다.

 

- 제로샷 러닝: 다운스트림 태스크 데이터를 전혀 사용하지 않고 모델이 바로 다운스트림 태스크를 수행하는 것이다.

- 원샷 러닝: 다운스트림 태스크 데이터를 1건만 사용하는 것을 의미한다. 모델이 1건의 데이터가 어떻게 수행되는지 참고한 뒤 다운스트림 태스크를 수행한다.

- 퓨샷 러닝: 다운스트림 태스크 데이터를 몇 건만 사용하는 것을 의미한다. 모델은 몇 건의 데이터가 어떻게 수행되는지 참고한 뒤 다운스트림 태스크를 수행한다.

 

전이 학습에 대한 큰 범주의 내용은 위와 같다. 다음은 이제 Transformer란 무엇이고 BERT와 GPT에 어떻게 영향을 주었는지 알아보자.

 

3. 트랜스포머 (Transformer)

트랜스포머는 2017년 구글(Google)에서 제안한 시퀀스 투 시퀀스(sequence to sequence) 모델로 자연어처리를 위해 만들어졌고 이후에는 컴퓨터 비전에도 사용되고 있다(Vision Transformer). 앞서 말했듯 BERT와 GPT 또한 Transformer를 기반으로 만들어졌다. 트랜스포머 모델은 크게 인코더와 디코더로 구성되는데 BERT는 트랜스포머의 인코더, GPT는 트랜스포머의 디코더를 사용하여 만들어진 것이 특징이다. BERT는 Bidirectional Encoder Representation from Transformer의 약자이며 GPT는 Generative Pre-trained Transformer의 약자이다. 이름에서 모두 Transformer가 사용된 것을 알 수 있다. 트랜스포머에 대해 구체적으로 알아보기 위해 간단히 배경, 활용, 특징, 구조에 대해 차례대로 알아보자.

 

3.1 트랜스포머 모델 탄생 배경

트랜스포머 모델이 나오게 된 배경은 기존 자연어 처리 모델의 단점 때문이다. 트랜스포머 모델은 시퀀스 투 시퀀스 모델이라 했다. 이 시퀀스 투 시퀀스 모델을 처리하는 기존의 모델은 RNN 계열 모델이었다. 대표적으로 LSTM이 있었는데 이 방식의 단점은 크게 2가지 였다. long-term dependency 문제와 비병렬성 문제였다. long-term dependency 문제란 문장의 길이가 길고 단어 사이의 간격이 클수록 모델이 "잊게" 되어 단어 간의 관계 파악이 어려워지는 문제이며, 비병렬성의 경우 문장을 처리하는데 있어 LSTM 모델이 순차적으로 처리하기 때문에 시간 복잡도가 높다는 단점이다. 이를 극복하는 모델이 바로 트랜스포머 모델이다. 병렬처리를 통해 속도가 빠르면서도, 긴 문장 또한 관계 파악을 분명하게 처리할 수 있는 것이다.

 

3.2 트랜스포머 모델 활용 및 특징

이러한 트랜스포머 모델이 활용된 부분은 자연어처리 태스크 중 기계번역에 가장 처음으로 사용되었다. 예를 들면 소스 언어 (source language)인 한국어를 타겟 언어(target language)인 영어로 번역하는 것이다. 인코더를 통해 소스 언어의 시퀀스를 압축하며, 디코더를 통해 타겟 언어의 시퀀스를 생성하는 것이다.

소스 언어: 어제, 카페, 갔었어, 거기, 사람, 많더라

타겟 언어: I, went, to, the, cafe, there, were, many, people, there

 

여기서 특징은 소스 언어의 길이(시퀀스)와 타겟 언어의 길이(시퀀스)가 달라도 해당 태스크를 수행할 수 있다는 것이다. 트랜스포머의 특징 중 하나는 임의의 시퀀스나 속성이 다른 시퀀스 변환 작업 또한 가능하다. 예를 들면 필리핀 앞바다 한 달치 기온 데이터를 기반으로 1주일간 하루 단위 태풍 발생 예측이 가능하다. 즉, 기온 시퀀스로 태풍 발생 여부 시퀀스를 예측할 수 있는 것이다. 

 

트랜스포머의 최종 출력값 즉, 디코더의 출력은 타겟 언어의 어휘 수 만큼의 차원으로 구성된 벡터이다. 학습은 인코더와 디코더에 입력이 주어질 때 정답에 해당하는 단어의 확률값을 높이는 방식으로 수행된다. "어제 카페 갔었어 거기 사람 많더라"라는 입력이 들어갈 경우 출력으로는 "I went to cafe there were a lot of people"이 나오는데, 이 때 가장 처음 번역되어야 할 "I"에 대한 확률값이 높아지고 나머지 went, to, cafe, there, were, a, lot, of, people는 확률값이 낮아지고 "I"다음에 나올 단어는 "went"일 때 "went"에 대한 확률값이 높아지고 나머지 단어는 확률이 낮아지는 것이다.

 

트랜스포머의 활용에 있어 특징 중 하나는 학습 도중의 디코더 입력과 학습 후 인퍼런스 때의 디코더의 입력이 다르다는 것이다. 학습 과정에는 디코더의 입력에 맞혀야 할 단어가 went라면 이전의 정답 타겟 시퀀스인 "<s> I"를 입력한다. 반면 인퍼런스 과정에는 현재 디코더 입력에 직전 디코딩 결과를 사용한다. 

 

3.3 트랜스포머 모델 구조

트랜스포머 모델은 앞서 언급한대로 아래와 같이 크게 인코더와 디코더로 구성되어 있다. 

왼쪽 회색 박스에 해당하는 영역이 인코더이며 오른쪽에 회색박스에 해당하는 영역이 디코더이다. 실제 트랜스포머를 사용할 때는 인코더를 N개 디코더를 N개 만큼 쌓아 사용한다. 인코더와 디코더에는 공통적인 요소는 멀티 헤드 어텐션(Multi-Head Attention), 피드포워드 뉴럴넷(Feedforward Neural Network), 잔차 연결 & 레이어 정규화(Residual-Connection & Layer Normalization)이다. 그 중 트랜스포머 모델의 가장 핵심이 되는 것은 멀티 헤드 어텐션이다. 따라서 먼저 멀티 헤드 어텐션에 대해 알아보자.

 

 

3.3.1 셀프 어텐션(self attention)

트랜스포머가 다른 sequence to sequence 모델에 비해 경쟁력을 가질 수 있는 원천은 셀프 어텐션(self attention)에 있다. 정확히는 멀티 헤드 어텐션인데 이 멀티 헤드 어텐션이란 단순히 셀프 어텐션을 여러 개(head)를 붙인 것에 불과하다. 먼저 어텐션(attention)이란 단어 시퀀스 요소 중 중요한 요소에만 집중해 특정 태스크의 성능을 올리는 기법을 뜻한다. 앞서 언급한대로 셀프 어텐션은 RNN의 단점인 long-term dependency 문제와 비병렬성 문제를 극복하는 방식으로 동작한다. 또한 어텐션은 CNN의 단점 또한 극복하는 방법이기도 하다. CNN의 단점은 컨볼루션 필터의 크기를 넘어서는 문맥을 읽어내기 어렵다는 것이다. 때문에 어텐션은 RNN과 CNN의 핵심 단점인 전체 문맥을 고려할 수 없다는 것을 극복하는 방법이다. 개별 단어와 전체 입력 시퀀스를 대상으로 어텐션 계산을 진행하는 방식으로 동작한다. 모든 경우의 수를 고려하기 때문에 지역적 문맥만 고려할 수 있는 CNN보다 강하며, 시퀀스의 길이가 길어도 RNN처럼 정보를 잊거나 (기울기 소실로 인해) 정보가 왜곡되지 않는다. 셀프 어텐션은 이 어텐션 개념을 자신에게 수행하는 것이다. 가령 "I love you"라는 시퀀스가 있을 경우 아래와 같이 모든 경우의 수에서 어텐션 스코어를 계산해 특정 시퀀스 요소(I, love, you)가 어떤 것과 가장 연관성이 높은지 판단한다.

I → I

I → love

I → you

love → I

love → love

love → you

you → I

you → love

you → you

 

3.3.1.1 셀프 어텐션 동작 원리

셀프 어텐션의 어텐션 스코어를 계산하기 위해 필요한 것은 Query, Key, Value이다. 다른 말로 셀프 어텐션이란 Query, Key, Value의 세 요소 사이의 문맥적 관계성을 추출하는 과정이라할 수 있다. Query, Key, Value로부터 문맥적 관계성을 추출하는 절차는 아래와 같다.

 

(1) Query, Key, Value 생성 하기

먼저 Q, K, V 행렬을 생성해야 한다. 이는 입력 벡터 시퀀스인 $X$를, 랜덤 초기화된 행렬 $W_Q$, $W_K$, $W_V$과 곱해서 Q, K, V 행렬을 생성한다. 수식으로 나타내면 아래와 같다. 

$Q = X \times W_Q$

$K = X \times W_K$

$V = X \times W_V$

이후 $W_Q, W_K, W_V$ 세 행렬은 태스크를 잘 수행하는 방향으로 학습 과정에서 업데이트 된다.

 

(2) 셀프 어텐션 출력값 계산

(1)의 과정을 통해 Q, K, V 행렬을 계산했다면 셀프 어텐션을 계산할 수 있게 된다. 과정을 단일 수식으로 나타내면 다음과 같다.

 

$Attention(Q, K,V) = softmax$ $({QK^T\over \sqrt{d_K}})V$

 

이를 풀어서 이야기하면 $Q \times K^T$한 뒤, 모든 요소 값을 $K$ 차원 수의 제곱근으로 나누고(if 차원 = 3, $d_K=3$), 이 행렬을 행 단위로 소프트맥스를 취해 스코어 행렬로 만들어 주는 것이다. 그리고 이 스코어 행렬에 V를 행렬곱하면 셀프 어텐션 계산이 된다.

 

이러한 수식을 가지게 되는 이유는 다음과 같다.

1. $QK^T$라는 내적을 통해 시퀀스 요소 간의 유사도를 구할 수 있다. 예를 들어 $Q$에는 I, love, you라는 시퀀스가 올 경우 3개의 행과 전체 어휘 차원수의 열을 가지게 되고, $K^T$는 I, love, you라는 시퀀스가 3차원 열을 만들고 전체 어휘 차원수 만큼이 행이 되는 것이다. 결과적으로 $QK^T$는 I, love, you라는 시퀀스 간의 유사도, 정확히는 벡터의 유사도를 구할 수 있는 것이다. 

 

2. $\sqrt{d_K}$로 나누어줌으로써 안정적인 경사값(gradient)를 계산할 수 있게 된다. 참고로 이러한 셀프 어텐션에서 $QK^T$의 내적을 계산한 다음 $\sqrt{d_K}$로 나누는 것을 스케일 닷 프로덕트 어텐션(scaled dot product attention)이라고도 부른다.

 

3. softmax 함수를 통해 정규화를 한다. 이를 통해 모든 요소는 0~1 사이의 값을 갖게 된다. 참고로 softmax 함수를 수식으로 나타내면 $softmax (x_i)= {exp(x_i) \over \sum_jexp(x_j)}$이다.

 

4. 가중치 행렬 $V$를 행렬곱함으로써 단어 벡터를 계산할 수 있게 된다. 

 

3.3.1.2 셀프 어텐션 동작 원리 실습

이러한 위의 셀프 어텐션을 구하는 과정을 파이토치로 계산하는 과정은 다음과 같다. 

import torch
import numpy as np
from torch.nn.functional import softmax

# 1. define input vector sequence: 3 x 4 = (number of words what entered) * (word embedding dimension)
x = torch.tensor([
    [1.0, 0.0, 1.0, 0.0],
    [0.0, 2.0, 0.0, 2.0],
    [1.0, 1.0, 1.0, 1.0],
])

# 2. define weighted query, weighted key, weigted value
w_query = torch.tensor([
    [1.0, 0.0, 1.0],
    [1.0, 0.0, 0.0],
    [0.0, 0.0, 1.0],
    [0.0, 1.0, 1.0]
])

w_key = torch.tensor([
    [0.0, 0.0, 1.0],
    [1.0, 1.0, 0.0],
    [0.0, 1.0, 0.0],
    [1.0, 1.0, 0.0]
])

v_value = torch.tensor([
    [0.0, 2.0, 0.0],
    [0.0, 3.0, 0.0],
    [1.0, 0.0, 3.0],
    [1.0, 1.0, 0.0]
])

# 3. create query, key, value
queries = torch.matmul(x, w_query)
keys = torch.matmul(x, w_key)
values = torch.matmul(x, v_value)

# 4. create attension score
attn_scores = torch.matmul(queries, keys.T)

# 5. apply softmax function
key_dim_sqrt = np.sqrt(keys.shape[-1])
attn_probs = softmax(attn_scores / key_dim_sqrt)

# 6. weighted sum with values
weighted_values = torch.matmul(attn_probs, values)
print (weighted_values)

 

3.3.1.3 멀티 헤드 어텐션

멀티 헤드 어텐션이란 앞서 언급한대로 셀프 어텐션을 동시에 여러 번(multi-head) 수행하는 것을 의미한다. 여러 헤드가 독자적으로 셀프어텐션을 계산하는 것이다. 비유를 하자면 같은 문서(입력)를 두고 여러 명(헤드)이 함께 읽는 것이다. 여러 번의 셀프 어텐션을 수행하여 결과값을 더함으로써 일종의 앙상블 효과를 낸다. 치우치지 않고 정확성이 높은 결과를 도출할 수 있는 것이다. 

 

개별 헤드의 셀프 어텐션 수행 결과 = $\times$ $W^O$

$W^O$의 크기 = 셀프 어텐션 수행 결과 행렬 열 수 $\times$ 목표 차원 수

최종 수행 결과 → 입력 단어 수 $\times$ 목표 차원 수

 

여기까지 트랜스포머의 구성요소 중 멀티 헤드 어텐션에 대해 알아보았다. 다음으로는 피드포워드 뉴럴넷, 잔차 연결, 레이어 정규화, 드롭 아웃에 대해 알아보자.

 

3.3.2 피드포워드 뉴럴넷(feedforward neural network)

피드포워드 뉴럴넷의 구성요소는 DNN의 형태로 Input layer, hidden layer, output layer로 3가지로 구성되어 있으며, 피드포워드 뉴럴넷의 학습대상은 weight와 bias이다. 흔히 알고 있는 가장 기본적인 뉴럴넷 구조이다.

 

 

3.3.3 잔차 연결(residual connection)

잔차 연결이란 블록 또는 레이어 계산을 건너뛰는 경로를 하나 두는 것을 의미한다. 책의 그림상 3개의 잔차 연결을 두면 8개의 새로운 경로가 생기는 것을 확인할 수 있었다. 이러한 잔차 연결의 역할은 모델이 다양한 관점에서 블록이나 레이어 계산을 수행 가능하다. 딥러닝 모델은 레이어가 많아질수록 학습이 어려워진다. 그이유는 모델 업데이트를 위한 gradient가 전달되는 경로가 길어지기 때문이다. 하지만, 잔차 연결을 통해 모델 중간에 블록을 건너뛰는 경로를 설정함으로써 학습을 쉽게하는 효과가 있다.

 

 

3.3.4 레이어 정규화(layer normalization)

미니 배치 인스턴스 별로 평균 ($\mathbb{E}[x])$을 빼주고 표준편차($\sqrt{\mathbb{V}[x]}$로 나눠 정규화를 수행하는 기법이다. 이를 통해 학습이 안정되고 속도가 빨라지는 효과를 얻을 수 있다. 레이어 정규화에 사용되는 수식 요소는 엡실론($\epsilon$), 감마($\\gamma$), 베타($\beta$)이다. 감마, 베타는 학습 과정에서 업데이트 되는 가중치이며 엡실론은 분모가 0이 되는 것을 방지하기 위해 사용된다. 보통 $1e^{-5}$로 설정한다. 딥러닝 프레임워크 중 하나인 파이토치에서 레이어 정규화를 위한 LayerNorm 객체는 감마와 베타를 각각 1과 0으로 초기화 하며 이후 업데이트 과정을 거친다.

 

3.3.5 드롭아웃(dropout)

드롭아웃은 트랜스포머에서 추가적인 요소로 더 나은 일반화를 위해 사용된다. 드롭아웃은 과적합(overfit) 방지 기법으로 모델이 표현력이 좋아서 외워버리는 것을 방지한다. 드롭아웃을 적용할 때 딥러닝 프레임워크 중 하나인 파이토치의 특징은 torch.nn.Dropout을 사용할 때 안정적인 학습을 위해 각 요소 값에 1/(1-p)를 곱하는 역할을 수행한다. 예를 들면 드롭아웃 비율을 p=0.2로 설정할 경우 1/(1-p)에 의해 드롭아웃 적용후 살아남은 요소값에 각각 1.25를 곱하게 된다. 드롭아웃은 일반적으로 0.1을 사용한다. 또 드롭아웃은 학습 과정에만 적용하고 인퍼런스에는 적용하지 않는 것이 특징이다.

 

 

4. 토큰화

토큰화란 문장을 작은 단위로 분리하는 것을 의미한다. BERT와 GPT와 같은 자연어처리 모델에 입력을 위해서 우선적으로 토큰화 절차를 필요로 한다. 토큰화 방법은 크게 3가지가 존재한다. 단어 단위, 문자 단위, 서브워드 단위이다..

 

단어 단위 토큰화 (word-level)

단어(어절) 단위로 토큰화를 수행하는 것을 의미한다. 예시로는 "어제 카페 갔었어"라는 문장은 "어제", "카페", "갔었어"로 분리 되는 식이다. 이 방식의 단점은 가능한한 많은 단어에 대해 고려해야 하기 때문에 어휘 집합(lexical set)의 크기가 커질 수 있고, 커지면 모델 학습이 어려워지게 된다.

 

문자 단위 토큰화 (character level)

문자 단위로 토큰화를 수행하는 것을 의미한다. 문자 단위란 ㄱ, ㄴ, ㄷ, ㄹ / a, b, c, d를 의미한다. 참고로 한글로 표현 가능한 글자는 총 11,172개이다. 문자 단위 토큰화 방식의 장점은 해당 언어의 모든 언어를 포함할 수 있기 때문에 미등록된 토큰(Out Of Vocabulary, OOV) 문제로부터 자유롭다. 반면 단점으로는 각 문자 토큰은 의미있는 단위가 어렵다는 것이다. 예를 들면 "제"의 어와 "어미"의 어의 구분이 사라지게 된다. 또한 토큰 시퀀스 길이가 길어지기 때문에 학습 성능이 저하된다는 단점도 존재한다. 예시로는 "어제 카페 갔었어" → 어, 제, 카, 페, 갔, 었, 어로 변환되어 토큰 시퀀스가 길어지는 것이다.

 

서브워드 단위 토큰화 (sub-word level)

단어 단위 토큰화와 문자 단위 토큰화의 중간 형태로 둘의 장점만 취한 것이다. 특징은 어휘 집합의 크기가 지나치게 크지도 않고, 미등록 토큰 문제를 해결하며, 토큰 시퀀스가 너무 길어지지 않게 하는 특징을 가진다. 대표적인 구현 예시가 BPE (Byte Pair Encoding)이다.

 

4.1 Byte Pair Encoding (BPE)

BPE는 1994년에 처음제안된 정보 압축 알고리즘이다. 하지만 근래에는 자연어처리의 토큰화 기법으로 사용된다.(BPE는 기계 번역분야에서 가장 먼저 쓰임) 토큰화를 진행 할 때 빈도수 높은 문자열을 병합해 데이터를 압축한다. 압축 알고리즘의 동작 절차 예시는는 다음과 같다.

 

1. aaabdaaabac가 있을 때 빈도수가 가장 높은 aaa를 Z로 치환해 ZabdZabac로 치환한다.

2. 다음 빈도수가 높은 ab를 Y로 치환해 ZYdZYac로 만든다.

3. ZY를 다시 X로 치환하여 XdXac로 치환한다. 

 

기존의 어휘수는 (a, b, c, d) 였으나 압축 어휘수는 (a, b, c, d, Z, Y, X)로 늘었고, 압축 전 글자수는 11글자에서 압축 후 글자수는 5글자가 되었다. 이러한 BPE 알고리즘의 장점은 사전 크기 증가를 적당히 억제하면서도 정보를 효율적으로 압축 가능하며, 분석 대상 언어에 대한 지식이 필요하지 않다는 것이다. 이러한 BPE 알고리즘의 대표적인 활용 예시는 GPT 모델에서 사용된다. GPT 모델은 BPE를 통해 토큰화를 수행한다. 반면 BERT는 wordpiece로 토큰화를 수행한다.

 

4.2 BPE 어휘 집합 구축

BPE 어휘 집합은 구축은 한 마디로 요약하면 고빈도 bi-gram 쌍을 병합하는 방식으로 구축한다. 구체적으로는 먼저, pre-tokenize를 통해 corpus의 모든 문장을 공백기준으로 나눈다. 이후 사용자가 정의한 크기(어휘 집합)이 될 때 까지 가장 높은 빈도수대로 추가한다. 높은 빈도수란 n-gram을 기준으로 하며, 일반적으로 성능과 계산량의 trade-off 관계로 인해 5-gram 아래를 사용하는 것이 좋다고 알려져 있다. 

 

4.3 WordPiece (워드피스)

워드피스 알고리즘 또한 토큰화를 수행하는 방법 중 하나로 자주 등장한 문자열을 토큰으로 인식한다는 점에서 BPE와 본질적으로 유사성을 가진다. 차이점으로는 병합 기준에 있다. 워드피스는 BPE 처럼 단순 빈도수 기반이 아니라, 우도(likelihood)를 가장 높이는 쌍을 병합하는 것이다. 한 마디로 워드피스는 우도를 가장 높이는 글자 쌍을 병합한다.

 

4.4 어휘 집합 구축하기

BPE & wordpiece 기반 토크나이저 만들기

 

Q. 왜 wordpiece에는 special char인 [PAD]를 사용하지 않는가? 정확히는 BPE는 special char [PAD]를 추가해주는데 wordpiece tokenizer에는 그런 과정이 없는가?

A. ?

 

Q. 왜 위 둘의 결과물은 유니코드로 되어 있어 보기 힘들까?

A. 학습 데이터가 한글이고, 한글은 3개의 유니코드 바이트로 표현됨. GPT 모델은 바이트 기준 BPE를 적용하기 때문임.

 

indexing: 토큰 → 인덱스

  • 예시: “별루 였다..” → 4957, 451, 363, 263, 0, 0, 0, ...

input_ids: vocab.txt 또는 vocab.json에 순서대로 부여된 번호로 나타내는 토큰 인덱스 시퀀스

attention_mask: 일반 토큰(1), 패딩 토큰(0) 구분하는 역할

token_type_ids: BERT 모델은 기본적으로 문장 2개 이상을 입력받기에 문장 번호를 이것으로 구분

 

 

5. Pretrained Language Model (PLM)

언어 모델이란 단어 시퀀스에 확률을 부여하는 모델이다. 수식은 $P(w_1, w_2, w_3, ..., w_n)$로 표현 가능하다. 넓은 의미에서의 언어 모델은 맥락이 전제된 상태서 특정 단어(w)가 나타날 조건부 확률이다. 수식으로는 $P(w|context)$로 표현 한다. 예시는 $P(운전|난폭) = {P(난폭, 운전) \over P(난폭)}$이다. 만약 잘 학습된 한국어 모델이 있을 경우 $P(무모|운전)$ 보다 $P(난폭|운전)$이 확률이 더 높을 것이다. 

 

단어가 3개 동시에 등장 확률은 $P(w_1, w_2,w_3) = P(w_1) * P(w_2|w_1) * P(w_3|w_1,w_2)$이 된다. 

언어 모델을 조건부 확률로 다시 쓰면, $P(w_1, w_2,w_3, w_4, \dots, w_n) = \prod_{i=1}^nP(w_i|w_1,w_2,w_3,\dots,w_{i-1})$이다.

 

언어 모델 종류에는 크게 순방향과 역방향이 있다. 순방향의 예시는 "어제 → 카페 → 갔었어 → 거기 → 사람 → 많더라" 순으로 순차적으로 예측하는 것을 의미한다. 대표적인 예시로는 GPT와 ELMo가 이 방식으로 pretrain을 수행한다. 역방향의 예시는 "많더라 → 사람 → 거기 → 갔어어 → 카페 → 어제"로 역순으로 예측하는 것을 의미한다. 대표적으로 ELMo가 이 방식으로 pretrain을 수행한다. 참고로 ELMo는 순방향, 역방향을 모두 활용하는 기법이다.

 

5.1 마스크 언어 모델(Masked Language Model)

마스크 언어 모델이란 학습 대상 문장에 빈칸을 만들고 빈칸에 올 적절한 단어를 분류하는 과정으로 학습하는 것이다. 대표적으로 BERT가 이 방식을 이용하여 업스트림 태스크를 수행한다. 마스크 언어 모델의 장점은 문장 전체의 맥락을 참고 가능하다는 것이다. 때문에 마스크 언어 모델에 양방향 성질이 있다고 말한다.

 

5.2 스킵-그램 모델(skip-gram model)

단어 앞뒤 특정 범위(window size)를 지정 후, 범위 내에 어떤 단어 올지 분류하는 과정으로 학습하는 것이다. 예시로는 window size = 2라 가정할 경우, 아래와 같이 center word 앞 뒤로 2개씩 단어(context word)가 있고 context word를 기반으로 center word를 예측(분류)하는 방식이다.

  1. 어제 카페 갔었어 거기 사람 많더라
  2. 어제 카페 갔었어 거기 사람 많더라

 

 

 

6. 자연어 처리 태스크 종류

6.1 문서 분류 태스크

문서 분류 모델은 (이 책에서 사용하는) 입력 문장을 토큰화한 뒤 [CLS]와 [SEP]를 토큰 시퀀스 앞뒤에 붙인다. 이후 BERT 모델에 입력하고 문장 수준 벡터인 pooler_output을 뽑는다. 이 벡터에 추가 모듈을 덧붙여 모델 전체의 출력이 긍정 확률, 부정 확률 형태로 만든다.

  • pooler_output 벡터 뒤 붙는 추가 모듈은 pooler_output 벡터에 드롭아웃 적용 일부를 768차원중 일부를 0으로 변경한다.
  • 만약 분류 대상이 2가지라면 가중치 행렬 크기는 768 * 2가 됨.

여기서 pooler_output이란 무엇일까? pooler_output은 메서드의 하나로 last_hidden_state라는 메서드와 연관이 있다. pooler_output과 last_hidden_state는 모두 BERT 모델에서 단어와 문장을 벡터로 변환한 것이다. 구체적으로 pooler_output은 BERT 모델에서의 최종 출력값인 벡터 시퀀스이다. 예를 들어 768차원으로 임베딩된다 가정하고, 입력 문장이 [”안녕하세요”, “저의”, "이름은“, "로이입니다”]일 경우 pooler_output은 4 x 768의 shape을 가지게 된다. 즉 아래와 같은 형태로 특정 단어에 대한 확률값을 벡터를 가진 요소이다. (예시를 위해 확률값은 임의 설정함)

 

안녕하세요 = [0.01, 0.04, 0.09, ... , 0.02]

저의         = [0.02, 0.03, 0.04, ... , 0.01]

이름은      = [0.04, 0.07, 0.09, ... , 0.09]

로이입니다 = [0.03, 0.06, 0.01, ... , 0.1]

 

이러한 pooler_output을 다르게 말해 문장 수준 임베딩이라 할 수 있다. 다시 말해 4개의 문장(단어)이 768차원의 벡터로 바뀐 것이다. 1개의 벡터 (1x768)은 하나의 문장 전체를 표현한다. pooler_output 메서드를 통해 이 결과값을 확인할 수 있다.

 

반면 last_hidden_state는 단어 수준 임베딩을 의미하는 것으로 예를 들어 4x10x768의 3차원 형태로 표현될 수 있다. 이는 4개의 문장이 길이 10을 가지고 있고, 768차원 형태라는 것이다.

 

 

 

 

6.1.1 문서 분류 모델 학습하기

Korpora 라이브러리를 통해 데이터를 내려받을 수 있다. 문서 분류 모델 학습을 위해 NSMC 데이터셋을 다운로드 받는다. 이후 데이터로더를 통해 학습 데이터를 배치 단위로 모델에 공급한다. 배치 단위로 공급할 때 데이터셋 내에 있는 인스턴스를 배치 크기 만큼으로 뽑는다. 데이터셋이 100개가 있다고 가정할 경우 인스턴스는 개별 요소 1개가 되며 배치크기가 10이라면 인스턴스 10개가 모여 1개의 배치를 이루게 된다.

  •  

6.1.2 인퍼런스 실습

 

토크나이저 초기화

from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained(
	args.pretrained_model_name, 
	do_lower_case=False,
)

체크포인트 로드

import torch
fine_tuned_model_ckpt = torch.load(
	args.downstream_model_checkpoint_fpath,
	map_location=torch.device("cpu"),
)

BERT 설정 로드

from transformers import BertConfig
pt_model_config = BertConfig.from_pretrained(
	args.pretrained_model_name,num_labels=fine_tuned_model_ckpt["stte_dict"]["model.classifier.bas"].shape.numel(),
)

BERT 모델 초기화

from transformers import BertForSequenceClassification
model = BertForSequenceClassificiation(pt_model_config)

체크포인트 주입

model.load_state_dict({k.replace(”model.”, “”): v for k, v in fine_tuned_model_cpkt[’state_dict’].items()})

평가 모드 전환

model.eval()

 

 

6.2 문장 쌍 분류 태스크

문장 2개가 주어졌을 때 문장 사이 관계가 어떤 범주일지 분류하는 것을 의미한다. 두 문장 관계는 참(entailment), 거짓(contradiction), 중립 또는 판단 불가(neutral)로 가려낼 수 있다. 예를 들어 "나 출근했어 + 난 백수야"는 거짓이 된다. 반면 "나 출근했어 + 난 개발자다"면 중립이 되는 식이다. 문장 쌍 분류 태스크를 위해 업스테이지가 공개한 KLUE-NLI 데이터셋을 활용하여 실습을 진행하였다.

 

모델 구조

문장 쌍 분류 모델은 전제와 가설 두 문장을 사용한다. 따라서 "[CLS] + 전제 + [SEP] + 가설 + [SEP]"의 형태로 이어 붙인다. 이후 토큰화 후 모델에 입력한 뒤, 문장 수준 벡터(pooler_output)을 추출한다. pooler_output에는 전제와 가설의 의미가 응축되어 있음. 여기에 추가 모듈을 붙여 모델 전체 출력이 아래의 형태가 되도록 한다.

  • 전제에 대해 가설이 참일 확률
  • 전제에 대해 가설이 거짓일 확률
  • 전제에 대해 가설이 중립일 확률

 

 

6.3 개체명 인식 태스크

범주 수가 m개이고 입력 토큰이 n개일 때 문서 분류, 문장 쌍 분류 모델은 모델 출력은 m차원의 확률 벡터 1개이다. 시퀀스 레이블링은 m차원 확률벡터가 n개 만들어진다.

 

방법론 입력 출력 대표 과제

문서 분류 문장 1개 한 문서가 속하는 범주에 대한 확률 감성 분석
문장 쌍 분류 문장 2개 두 문서를 아우르는 범주에 대한 확률 자연어 추론
시퀀스 레이블링 문장 1개 토큰 각각의 범주 확률 개체명 인식

 

 

6.4 질의응답 태스크

질의응답 태스크의 유형은 다양하지만 본 책에서는 지문(context)에서 답을 찾는 것으로함 즉, open-book qa이다.

모델의 입력은 질문과 지문(Question, Context)가 되며, 모델 출력은 입력한 각 토큰이 [정답 시작일 확률, 정답 끝일 확률]이 된다. 질의응답 모델은 출력 확률을 적당한 후처리 과정을 통해 사람이 이해 가능한 형태로 가공한다. 실습을 위해 LG CNS가 공개한 KorQuAD 1.0 데이터를 활용하였다. 모델 구조는 토큰화 후, "[CLS] 질문 [SEP] 지문 [CLS]"의 형태로 입력된다.

 

 

6.5 문장 생성 태스크

문장 생성은 컨텍스트(context)가 주어졌을 때 다음 단어로 어떤 단어가 오는 게 적절한지 분류하는 것이다. 모델 입력은 컨텍스트가 되며 모델 출력은 다음 토큰이 등장할 확률이 된다. 수식으로 일반화하여 나타내면 $P(w|context)$이다. 조금 구체적으로는 $P(w|안녕)$일 경우 "안녕" 다음에 올 단어의 확률이 출력된다. 이런 문장 생성 태스크는 문서 분류, 문서 쌍 분류, 개체명 인식, 질의응답 태스크와 특성이 다르다. 가장 큰 차이는 모델 구조이다.

 

모델 구조 GPT BERT
pretrain task 다음 단어 맞히기 빈칸 맞히기
fine tuining 다음 단어 맞히기 각 다운스트림 태스크

 

GPT는 BERT와 달리 pretrain task와 fine tuning 자체가 다음 단어를 맞히는 것으로, 문장 생성 Task에 더 적합한 모델이라 할 수 있다. 

 

6.5.1 토크나이저

  • eos_token은 문장 마지막에 붙는 스페셜 토큰으로 SK Telecom이 모델을 pretrain할 때 지정했으므로 같은 방식으로 사용
  • from transformers import PreTrainedTokenizerFast tokenier = PreTrainedTokenizerFast.from_pretrained( args.pretrained_model_name, eos_token=”</s>”, )

 

6.5.2 문장 생성 전략 수립

문장 생성을 위해서는 문장 생성 전략 수립이 제일 중요하다. 이를 위해, 문장 생성을 위한 단어를 탐색하는 테크닉 종류 두 가지가 있다. 크게 빔서치그리드 서치로 나뉜다. 두 방법 모두 최고 확률을 내는 단어 시퀀스를 찾는 방법이다. 차이점이 있다면 빔서치의 계산량이 그리디 서치의 계산량보다 많다는 것이다. 그 이유는 그리디 서치는 최고 확률을 내는 한 가지 경우의 수를 내지만, 빔서치의 경우 빔 크기만큼의 경우의 수를 낼 수 있기 때문이다. 빔서치는 그리디 서치보다 조금 더 높은 확률을 내는 문장생성이 가능하다는 장점이 있다. 이런 빔서치와 그리드 서치에 도움을 주는 내부 파라미터가 4개 있다. repetition_penalty, top-k sampling, top-p sampling, temperature scaling이다. 

 

repetition_penalty

반복되는 표현을 줄여주는 역할을 한다. default 값은 1.0이다. 만약 이 값을 적용하지 않는다면 그리디 서치와 동일한 효과를 낸다.

 

top-k sampling

모델이 예측한 토큰 확률 분포에서 확률값이 가장 높은 k개 토큰 가운데 하나를 다음 토큰으로 선택하는 기법이다. top_k를 1로 설정할 경우 그리디 서치와 동일한 효과를 낸다.

 

top-p sampling

확률값이 높은 순서대로 내림차순 정렬 후 누적 확률값이 p 이상인 최소 개수의 토큰 집합 가운데 하나를 다음 토큰으로 선택하는 기법이다. 뉴클리어스 샘플링이라고도 한다. 특징은 0에 가까울수록 후보 토큰이 줄어 그리디 서치와 비슷해지며, 1이 되면 모든 확률값을 고려하기 때문에 이론상 어휘 전체를 고려하게 된다.

 

템퍼러처 스케일링(temperature scaling)

토큰 확률 분포의 모양을 변경하는데 이는 모델의 출력 로짓의 모든 요소값을 temperature로 나누는 방식이기 때문이다. 예를 들어 로짓: [-1.0 2.0 3.0], temperature: 2라면 템퍼러처 스케일링 후 로짓은 [-0.5 1.0 1.5]가 된다. 이 템퍼러처 스케일링의 특징은 0에 가까울수록 확률분포모양이 원래보다 뾰족해진다. 또 1보다 큰 값을 설정할 시 확률분포가 평평(uniform) 해진다. 핵심은 이 값이 1보다 적으면 상대적으로 정확한 문장, 1보다 크면 상대적으로 다양한 문장이 생성되는 특징을 가진다. 템퍼러처 스케일링은 top-k 샘플링과 top-p 샘플링과 함께 적용해야 의미가 있다고 한다.

 

 

 

 

7. BERT와 GPT 비교와 모델 크기를 줄이는 기법

BERT는 의미 추출에 강점을, GPT는 문장 생성에 강점을 지닌다. BERT는 트랜스포머의 인코더를 사용했고 GPT는 디코더만을 사용했다. 최근 자연어처리 트렌드는 모델 크기를 키우는 것으로 크기를 키움으로써 언어 모델의 품질이 향상되고 다운스트림 태스크의 성능도 좋아지게 하는 것이다. 하지만 모델의 크기가 너무 커지면 계산 복잡도가 높아지는데 이러한 계산량 또는 모델 크기를 줄이려는 여러 시도가 있는데 대표적으로 4가지가 있다.

디스틸레이션 (distillation)퀀타이제이션 (quantization)프루닝 (pruning), 파라미터 공유 (weight sharing)

 

 

8. 학습 파이프라인

자연어 처리 모델을 학습시키기 위한 일련의 파이프라인은 다음과 같다.

1. 하이퍼파라미터 설정 (learning rate, batch size, epochs, ...)

2. 학습 데이터셋 준비

3. pretrain된 모델 준비

4. tokenizer 준비

5. 데이터 로더 준비

6. 태스크 정의 

7. 모델 학습

의 과정으로 이루어진다.

 

여기서 데이터 로더란 데이터를 모델의 입력에 필요한 형태인 배치(batch)로 만들어주는 역할을 한다. 배치를 만들기 위해 전체 데이터셋 가운데 일부 샘플(instance)를 추출해 배치로 구성한다. 배치 구성을 위한 인스턴스 추출방식에는 랜덤 샘플링과 시퀀셜 샘플링 방식이 있다. 랜덤 샘플링 방식은 주로 train 데이터셋을 구성할 때 사용되고 시퀀셜 샘플링은 valid, test 데이터셋을 구성할 때 사용한다. 배치란 인스턴스의 합이다. 여기서 인스턴스란 문장과 라벨을 갖는 하나의 가장 작은 요소이다. 여기서 인스턴스란 문장과 라벨을 갖는 하나의 가장 작은 요소이며, 인스턴스가 모여서 배치를 이루게 된다. 데이터 로더에는 컬레이트라는 과정이 있다. 컬레이트는 모델의 최종 입력으로 만들어주는 과정이다. 태스크 정의에는 주로 요즘은 파이토치 라이트닝을 사용한다. 그 이유는 딥러닝 모델 학습시 반복적인 내용을 대신 수행해줘 사용자가 모델 구축에만 신경쓸 수 있도록 돕기 때문이다.

 

 

 

9. 자연어 처리 관련 양질 국내 강의

 

 

 

10. 기타 내용

극성 레이블 (polarity label)

  • 예시: 부정 = 0, 긍정 = 1

model.eval()

  • 학습때만 필요한 기능들을 꺼서 평가 모드로 동작하도록 함

tokenizer.encode()

  • 입력 문장 토큰화 후 정수 인덱싱하는 역할 수행
  • 인자로 return_tensors=’pt’를 주게되면 인덱싱 결과를 파이토치 텐서 자료형으로 변환

언어 모델 유용성

  • NSP, MLM: 데이터 제작 비용 절감
  • Transfer Learning

언어 모델 최종 또는 중간 출력값 = 임베딩(Embedding) or 리프레젠테이션(representation)

인간의 위대한 성장은 도약, 그리고 비약이라는 두 번에 걸친 발산에 의해 이루어진다. 이 중 상대적으로 더 어려운 것은 도약이다. 도약전까지는 공허한 자기 확신이 있을 뿐이다. 고통, 두려움, 불확실성이라는 그림자와 함께 나아갈 뿐이다. 도약을 거치기 전, 비약 이후를 바라볼 때면, 다른 세상, 도달할 수 없는 어떤 곳 또는 경지라는 경외심이 자신을 압제한다. 벗어나야 한다. 도약을 위해서는 소위 축적의 시간을 가진다. 파동의 진폭처럼 위 아래로 움직이며 점진적으로 양의 방향으로 축적 하다보면 반드시 도약하는 구간을 거치게 된다. 이를 위해 도약전까지의 고통과 두려움과 불확실성을 기꺼이 팀으로 만들어야 한다. 촛불은 바람이 불면 꺼진다. 하지만 모닥불은 더 활활 타오른다. 모닥불이 되어 두려움과 불확실성이라는 바람을 이용해 더 커져야 한다. 지금 당장 촛불이여도 좋다. 후에 꺼지지 않는 커다란 태양이 되어 세상을 드넓게 비출 수 있다.

도약까지 과정이 눈물겹지 않았다면 그 도약을 용의자로 하여 희미한 향기만 배여 있지 않은지 추궁해볼 필요가 있다. 도약까지는 비약과 동일하게, 더 이상 나아가지 못하는 구간 즉, 소위 슬럼프가 있기 때문이다. 음의 방향으로 진폭이 커지거나 더 이상 양의 방향으로 유의미한 진폭이 나오지 않을 때다. 하지만 세상에 마찰이 존재하지 않는 곳은 없다. 마찰력을 짓누르고 다시 가속 운동이 되도록 인고해야 할 뿐이다. 이 인고의 시간은 도약에 짙은 향기를 새긴다. 도약기를 거치고나면 더 넓은 세계를 마주하게 된다. 본 게임 시작이다. 하지만 도약 이후 비약을 바라볼 때는 도약 이전에 도약을 바라보는 것보다 상대적으로 쉽다. 가속 운동에 대한 관성이 계속 작용하고 있기 때문이며, 축적으로부터 파생된 자신감이 옅은 오로라가 되어 주위를 감싸고 있기 때문이다.

도약을 거치고 나면 비로소 발언에 힘이 실리게 된다. 소위 전문가가 되는 것이다. 하지만 그 발언은 반쪽이다. 도약을 거친자는 많다. 때문에 여전히 상대성에 머무르게 된다. 절대성이 필요하다. 절대성으로 가는 것, 그것이 곧 비약이다. 위대한 성장을 위한 도약과 비약을 가로지르는 것이 있다. 그것은 곧 몰입이다. 몰입이란 절대적 선택을 의미한다. 다른 것의 배제가 아니다. 또한 대상과의 합일을 의미한다. 그 대상이 되는 것이다. 물아일체다. 하지만 몰입 속에 있더라도 넘어설 수 없는 영역이 존재하는 것이 느껴질 때가 있다. 그 지점이다. 그 지점을 공략해야 한다. 절대성으로 향하는 길이다. 절대성으로 가는 마지막 계단이다. 그 지점을 넘어서는 순간, 퇴보할 수 없다. 역치를 넘은 것이다. 고달한 것이다. 상대성으로부터의 탈각이며, 절대성으로부터의 평화이다. 비로소 완전한 발언이 되고, 비로소 자신감은 짙은 오로라가 되어 주위를 영구히 회전하게 된다. 이는 곧 삶의 궁극의 비기가 된다.

한 줄 핵심 요약

Word2Vec의 내부 구조로 CBOW와 Skip-gram 방식이 있고 이 두 개의 아키텍처가 기존 NNLM 모델보다 뛰어남.

 

1. Word2Vec 모델의 배경

Word2Vec이라 불리는 이 논문의 핵심은 Word2Vec의 구조에 있다. 우선 Word2Vec은 단어를 분산 표상(distributed representation)하는 방법이다. 분산 표상이란 고차원 공간상에 단어를 continous하게 벡터화시키는 것이다. Word2Vec이 나오기 전인 2003년 가장 초기에는 NNLM(Nueral Network Language Model) 모델이라고 하여 단어를 컴퓨터로 하여금 이해 시키기 위해 내부적으로 one-hot encoding 방법을 사용했다. 하지만 이 one-hot encoding 방법의 sparse matrix라는 것이다. 예를 들면 one-hot encoding은 강아지, 고양이, 호랑이가 있을 때 강아지를 [1, 0, 0]으로, 고양이를 [0, 1, 0]으로 호랑이를 [0, 0, 1]로 표현하는 방식이다.

 

하지만 단점은 표현하고자 하는 단어의 수가 늘어날 수록, 단어를 표현하는 matrix의 크기가 커지지만 matrix 상에 실제 단어를 나타내는 위치 이외에는 전부 0으로 표현된다. 때문에 고차원 공간을 간헐적(?)으로 사용하므로 비효율적이다. 다른 말로 표현하면, one-hot encoding을 사용할 경우 단어를 discrete하게 학습시키는 것이다. 또한 sparse matrix의 단점은 단어간 유사도를 계산할 수 없다는 것이다. 예를 들면 big과 bigger는 의미상 유사성을 띠지만 one-hot encoding은 각각의 단어를 모두 일종의 독립된 개인으로 간주하기 때문에 유사성을 표현할 수 없다. 이러한 one-hot encoding 방식의 한계를 극복하기 위해 나온 것이 분산 표상 방법이다. 분산 표상 방식의 특징은 one-hot encoding과 달리 단어 벡터를 continuous하게 표현가능하다. 이 분산 표상 방식을 제안한 모델이 Word2Vec이다. 

 

 

2. Word2Vec의 구조

앞서 언급한대로 Word2Vec 모델의 핵심은 그 구조에 있다. Word2Vec의 동작에는 내부적으로 2가지 방식이 있다. 첫 번째는 CBOW (continous Bag-of-Words)Skip-gram 방식이다. 아키텍처는 아래 그림과 같다. 

먼저 CBOW 방법은 주변 단어를 의미하는 context에 기반해 중심 단어(centric word)를 예측하는 방식이며, Skip-gram 방식은 중심 단어에 기반해 주변 단어를 예측하는 방식이다. CBOW와 Skip-gram 방식 모두 간단한 구조인 3가지 레이어로 구성되어 있다. Input layer, projection layer, output layer이다. 참고로 Input layer에는 one-hot encoding된 값이 들어가는데 그 이유는 word2vec 모델이 기본 뼈대를 NNLM 모델로 취했기 때문이다.

 

2.1 CBOW

CBOW 방법을 통해 중심 단어를 예측하기 위해서는 주변 단어(앞뒤)의 개수를 결정해야 하는데 이를 window size를 통해 결정 가능하다. 예를 들어 window size가 2일 경우 앞뒤로 2개씩 하여 총 4개의 단어를 CBOW 모델의 입력으로 사용하는 것이다. 다음은 『딥러닝을 이용한 자연어 처리 입문』에서 가져온 CBOW의 동작 원리이다.

window size가 2인 경우 중심 단어를 예측 하기 위해 주변 앞뒤 단어를 2개씩 Input layer에 넣어주게 된다. 만약 중심 단어 앞에 window size만큼의 단어가 없을 경우 가능한 만큼만 입력으로 넣게 된다. 만약 "sat"을 예측하고 싶다면 아래와 같다. 

"sat"에 대한 one-hot encoding된 값인 one-hot vector를 output layer에 label로 두며, Input layer에는 window size 2에 의해 앞 뒤로 두 개의 단어를 one-hot encoding시킨 값을 Input layer에 입력으로 넣어주게 된다. Projection layer의 역할은 lookup table 연산을 담당한다. lookup table이란 주어진 연산에 대해 미리 계산된 결과들의 집합을 의미하는 행렬이다. 쉽게 말해 사전이라 표현할 수 있다. CBOW에서의 lookup table의 구체적인 연산 과정은 아래와 같다.

Projection layer는 M차원을 가진다. M은 하이퍼파라미터로서 임의로 설정될 수 있다. 기존의 one-hot vector의 차원인 7에서 Input layer를 거치게 되면 M이 된다. 위의 예시에서는 "The fat cat sat on the mat"의 단어의 개수인 7개를 차원으로 두고 W라하는 랜덤 초기화된 가중치 행렬과 계산하게 되면 M차원이 된다. W는 차원의 크기를 나타내는 V와 Projection layer의 크기를 나타내는 M의 곱으로 표현된다. 이 때 W를 살펴보면, Input layer에서 입력된 2번째 index에 1이라는 값을 가지는 one-hot vector와 가중치 행렬 W의 곱은 사실상 W 행렬의 2번째 행을 그대로 읽어 오는 것과 동일하다. 이 가중치 행렬 W를 lookup table이라 한다.

 

word2vec에 의해 단어가 학습되면 W가 업데이트 되는데 W의 각 행 벡터는 M차원, 위의 예시로는 5차원 임베딩 벡터로 표현된다. 예를 들면 2번째 index의 값인 cat이라는 단어는 5차원 임베딩 벡터로 [2.1, 1.8, 1.5, 1.7, 2.7]이라는 값을 갖게 된다. W'는 M차원 벡터에서 다시 one-hot vector가 가지고 있던 기존의 차원으로 바뀌는 과정에서 업데이트 된다. 일종의 decoding을 진행하며 가중치 행렬 W'를 학습시킨다 볼 수 있다. 구체적인 과정은 아래와 같다.

 

만약 중심 단어 sat을 예측하고자 하고, window size가 2일 경우 주변 단어를 총 2N개를 input layer에 입력해준다. 이후 가중치 행렬 W에 의해 생성된 결과 벡터들은 Projection layer에서 벡터들의 평균값을 구하게 된다. 이후 구해진 평균 벡터와 가중치 행렬 W'와 곱하여 기존의 one-hot vector와 같은 차원의 값이 도출된다. 여기서 output layer에 label인 "sat"을 예측하기 위해서 내부적으로 아래와 같이 softmax 함수를 사용한다.  

 

softmax 함수를 사용하여 one-hot vector들의 각 원소를 0~1사이의 확률값으로 표현한다. 확률값 중 가장 높은 것이 중심 단어일 가능성이 가장 높은 것이다. 여기까지의 과정을 요약하면 CBOW는 주변단어로 중심 단어를 잘 예측하기 위해 W와 W'를 업데이트해 나가는 방법이라 할 수 있다.

 

이외의 CBOW의 특징은 기존의 BOW와 달리 continuous distributed representation을 사용한다. 또한 Input layer와 projection layer 사이의 가중치 행렬은 NNLM과 같은 방식으로 모든 단어 위치에 대해 공유된다. 또 순서가 projection에 영향을 미치지 않기 때문에 Bag-of-words라 한다.

 

 

2.2 Skip-gram

Skip-gram 방식은 CBOW 방식과 매우 유사하다. 다만 크게 2가지 차이점이 있다. CBOW는 주변단어로 중심단어를 예측했다면 Skip-gram 방식은 아래 그림과 같이 중심단어로 주변단어를 예측하는 것이 첫 번째이다. 

두 번째는 Skip-gram의 Input layer의 입력 값이 중심단어 하나이기에 projection layer에서 벡터의 평균을 구하지 않는다는 것이다. Skip-gram의 전체 과정을 도식화 하면 아래와 같다.

Input layer에 중심 단어를 입력하며, 벡터 평균 계산이 없는 projection layer를 거쳐 주변단어의 label이 위치한 output layer로 학습이 이루어진다. Skip-gram이 CBOW보다 성능이 좋다고 알려져 있다 한다. 

 

 

3. 모델 성능 및 결론

기존 모델은 RNNLM과 NNLM을 뛰어넘는 성능을 보임

여러 NNLM 변형과 CBOW, Skip-gram의 성능 비교 결과 저자들이 내세운 아키텍처의 성능이 전반적으로 높은 것을 확인 가능

 

 

논문에는 연구 배경이나 연구 목표들의 여러 내용이 있었지만 핵심만 요약하자면 Word2Vec의 구조인 CBOW와 Skip-gram 방식의 메커니즘이 그 핵심이며 또한 그 결과 CBOW와 Skip-gram 방식이 기존 NNLM 모델보다 뛰어나다는 것이다.

 

이외의 장점은

1. 벡터 산술 연산이 가능하다. ex: King - Man + Woman = Queen

2. Very simple한 모델 아키텍처로 고퀄리티로 단어 벡터를 학습 가능하다.

3. 기존 모델들 대비 낮은 계산 복잡도를 가진다.

4. continous representation of word를 계산하기 위해 기존엔 LSA, LDA를 사용했으나 word2vec은 LSA를 뛰어 넘었고 LDA는 이제 계산 비용이 높은 알고리즘이 되었다.

 

 

4. 기타 추가로 알게된 부분

  • word2vec은 feedforward NNLM의 한계인 context length 문제를 해결하기 위해 고안됨.
  • RNNLM에서의 벡터는 Context Vector 또는 Thought Vector라 부름
  • projection layer: 기존 입력층이 이산 표상인데 비해 입력층에서 넘어오게 되면 연속 표상으로 바뀜.
  • word2vec은 은닉층이 1개인 shallow NN임
  • word2vec은 일반적 은닉층과 달리 활성화 함수가 존재X, lookup table 연산을 담당하는 projection layer가 있음
  • DistBelief → 병렬 실행 가능이 핵심
    • 모델을 여러개로 복제해서 병렬로 실행하고, 중앙집중화 서버를 통해 gradient 업데이트를 동기화가능하게 함.
    • DistBelief 프레임워크에서는 일반적으로 100개 이상의 모델 복제본을 사용함.
  • paraphrase detection (의역 예측 = 같은지 다른지)
    • 집가서 밥 먹었다.
    • 귀가 후 식사 했다.

 

Reference

[1] https://wikidocs.net/22660

[2] https://jiho-ml.com/weekly-nlp-28/

[3] https://sonsnotation.blogspot.com/2020/11/11-attention-transformer-models.html

 

 

 

최고가 되는 가장 빠른 길은 최고의 곁에서 그를 배우는 것이다.

이 책은 타이탄이라는 여러 성공한 사람들의 통찰을 모은 책이다. 내 것으로 만들만한 여러 행동의 기준점으로써 삶의 무기가 되어줄 일종의 격언집이다. 타이탄들이 사용하는 '도구'는 누구나 이해할 수 있으며 따라할 수 있다. 때문에 사소해보일 수 있으나 사소한 차이가 축적되어 크나큰 성공의 차이를 가져올 수 있다는 오래됐지만 자명한 사실을 생각하면 조금더 깊이 생각해볼 수 있을 것이다.

 

우리는 무언가를 배우기 위해 강의를 듣고, 세미나에 참여하면서 시간을 보낸다. <타이탄의 도구들>의 가장 좋은 장점은 '책' 이상이라는 것이다. 책은 내가 무언가를 배우기 위해 많은 시간을 들이지 않아도, 즉 매우 효율적으로 한 사람의 생각과 지혜를 배울 수 있다는 것을 우리는 안다. <타이탄의 도구들>과 같은 경우는 사회적으로 성공한 여러 많은 거장들의 삶을 분석하고 그 삶의 비법을 담았으니, 이것이야 말로 우리가 배우고 따라야할 지침이 아닐까 한다.

이 책이 가지는 가장 큰 장점은 수 많은 경험과 철학에 기반한 삶의 통찰이 담겼다는 것이며 또 다른 장점으로는 타이탄들이 공통적으로 읽은 도서를 알 수 있었다는 점이다. 유발 하라리의 <사피엔스>, 빅터 플랭크의 <죽음의 수용소에서>, 헤르만 헤세의 <싯다르타> 등의 도서가 있었다. 반면 이 책이 가지는 유일한 단점은 그 통찰이 많아 다 외기 힘들다는 것이다. 하지만 단언컨대 타이탄의 격언들을 모두 체화하여 살아간다면 백만장자, 천만장자 그 이상의 삶을 살 수 있을 것이다.

여러 격언이 있었지만 특히 와닿았던 세 가지 격언으로 독서 후기를 갈음한다.

📌 만일 당신이 무엇인가 도달하는 데 10년이 걸리는 계획을 갖고 있다면, 당신은 다음의 질문을 스스로에게 던져야 한다. "아니, 왜 이걸 6개월 안에는 해낼수 없는 거지?" - 피터 틸

📌 인생을 걸 만한 계획이나 목표가 있다면, 가장 먼저 해야 할 일은 타인이 절대 대체할 수 없는 나만의 사명을 찾는 것이다. 아무도 못할 일이라고 생각했더니 웃음이 사라지고 진지해지기 시작했다. - 일론 머스크

📌 성공은 그 사람이 얼마나 많은 불편한 대화를 기꺼이 할 준비가 되어 있는지로 측정된다 - 팀 페리스

별점: ⭐️⭐️⭐️

"내가 그로록 힘들었던 이유는 타인의 삶을 벤치마킹하는 데 소질이 없었기 때문이다" - 책 속에서

 

 

배움의 지름길을 선택하는 법


이 책의 핵심은 제목과도 같다. 가장 빨리 무언가를 배우는 법을 알려준다. 대학원 처음 들어갔을 때 쯤 산 책으로, 누구나 빨리 배울 수 있음을 알려 주었고 이후로 늘 효율적인 학습 방법에 대해 고민하게 된 시초가 된 책 인 것으로 기억한다.

저자는 과학의 발전으로 인해 변화하는 세계에서 빠른 습득 능력이 중요하다고 서두에서 말하며, 이 방법의 특징은 지능이 아닌 효율적인 전략의 사용이다.

세계를 돌며 누군가는 3개월이면 언어를 하나씩 구사하는 사람이 있다는 것과, MIT 4년 과정을 1년만에 독학한 사람이 있다는 사례가 책을 흥미롭게 읽어나가게 된 요소 중 하나였다.

책은 전반적으로 어떻게 효율적으로 배울 수 있는가에 대한 어렵지 않은 뇌과학의 연구 결과들을 바탕으로하며, 의지만 있다면 누구나 따라할 수 있도록 방법을 설명해준다.

총 9개의 챕터가 있다. 순서대로 메타 학습, 집중 하기, 직접 하기, 특화 학습, 인출, 피드백, 유지, 직관, 실험이다. 전반적으로 느끼기엔 챕터들 간의 유기적으로 연결성을 확립할 수 없겠지만 개별 챕터만을 독립적으로 지식으로 확보하여도 많은 도움이 되리라 생각한다.

"고정적 마인드셋이 아닌 성장 지향적 마인셋을 갖고 자신의 잠재력을 바라보라" - 스콧 영

별점: ⭐️⭐️⭐️

 

 

고귀한 삶이란 무엇인가?


우리가 살아가며 지향해야한다 생각하는 가치를 무게 있게 말하는 모습에 이끌려 읽게 된 책이다. 이 책은, 이름을 들으면 누구나 아는 하버드 대학, 서울 대학, 북경 대학, 스탠퍼드 대학을 다녔고, 늘 최 상위권 성적을 유지했던 엘리트라 불리는 사람의 자서전이다. 이 책은 자신이 하버드에 가고 싶어했던 10대 때의 동기와 노력, 20대 때의 학업과 직장, 30대 때 초의 삶의 일부를 다룬다. 읽는 동안 자신의 삶의 소명에 대한 짙은 고뇌, 그리고 숭고한 정신과 함께 끊임 없이 깨어 있으려고 하는 생각이 돋보였다.

아쉬운 부분이자 반면교사를 삼을 수 있다고 생각든 부분은, 역사의 진보를 이루고자 하는 그토록 숭고한 정신을 가지고 있었음에도 자신이 원하는, 그 정신을 구현하는 삶을 좇는 모습을 보기 어려웠다는 것이다. 목표를 정한 뒤 삶의 경로를 설정하는 것이 아니라 오히려 그때 그때 필요로 하는 것을 따른 뒤, 그에 걸맞는 의미를 부여 한 것 같은 생각이 들었다. 진정 역사의 진보를 이루고자 한다면 당장이라도 그가 있는 곳에서 뛰쳐나와 더 넓은 세계를 상대해야할 것이다.

단순히 삶을 다해 이루고자 하는 저자의 꿈인 인류 역사 속에서 영생을 얻고자 하는 원대한 목표를 이루거나 그에 더 가까운 삶을 추구하기 위한 모습을 보지 못했던 부분은 아쉽긴 했다. 하지만 책 전반에서 일반적으로 뛰어나다고 여기는 한 사람의 삶에 대한 여러 질문, 고뇌, 그리고 이에 대한 자신의 통찰, 확고한 신념과 함께 하는 성장과정을 볼 수 있고, 또한 이러한 가치들을 저자의 뛰어난 표현력과 문장력으로 풀어낸 것을 볼 수 있었다는 것이 좋았다.

요약하면, 이 책을 통해 인간이 마땅히 지향해야할 숭고한 정신을 배우되, 저자가 보이지 못했던 그것을 실천하는 삶을 살아간다면 가장 바람직한 방향일 것이다.

"내 젊음을 연소시켜 이루고픈 그 뜻은 무엇인가? - 책 속에서

별점: ⭐️⭐️⭐️

 

 

철학이 아닌 철학함이 필요하다.


이 책은 철학이 아니라 철학하는 방법을 알려주며, 철학적 사고를 직접 해봄으로써 마주친 문제에 대한 해결할 수 있도록 돕는 것을 목표로 한다.

이를 위해 여러 철학자들이 실제로 논의 했던 문제를 제시하며 독자로 하여금 생각을 자극한다. 주요 내용들로는 결정론과 자유의지, 튜링 테스트와 중국어 방, 유신론과 무신론 등의 내용을 담고 있다.

저자는 또한 말한다. 철학이 필요한 이유는 철학의 범주에 속하는 형이상학과 인식론은 여러 학문에 근간이 되기에 이것들이 없다면 학문 자체가 성립할 수 없기 때문이라고.

실제로 형이상학에 해당하는 인식론과 존재론의 경우 종교철학이나 과학철학에도 지대한 영향을 미치기에 나 또한 마찬가지로 중요하다고 생각한다.

이 책은 철학자들이 논의 했던 대표적인 문제들을 접할 수 있고, 어렵게 쓰이지 않았으며, 중간중간 생각을 자극하는 질문들이 있다는 것이 장점이다. 반면 여러 문제들을 제시한 페이지들을 앞뒤로 반복해서 찾아보도록 내용을 구성했기 때문에 번거롭다는 단점이 있다.

그럼에도 불구하고 철학에서 논의되는 것들은 무엇이고 일상생활에서는 어떻게 사용할 수 있는지에 대한 감을 잡기에는 좋은 책이라 생각된다.

"철학적 사고는 누구나 할 수 있다. 하지만 훈련이 없다면 개똥 철학이 된다" - 최훈

별점: ⭐️⭐️

+ Recent posts