https://arxiv.org/pdf/2407.19813

Self-Reasoning이라는 프레임워크를 통해 RAG의 성능을 높이고자 연구한 논문. 2,000개의 작은 데이터셋으로 LLM 튜닝했을 때 원하는 성과가 나왔다고 한다.

핵심 포인트만 짚어서 가볍게 논문 리뷰를 해보도록 하겠다.



논문 리뷰에 앞서 내가 직접 이해하고 요약한 Self-Reasoning의 흐름은 아래와 같다.

  1. RAG의 고질병은 무관한 문서를 검색할 경우 모델 성능이 저하될 수 있으며, 인용이 명확하지 않으면 생성된 출력의 신뢰성을 검증하기 어렵다는 데에 있다.
  2. 그래서 본 연구는 이를 해결하기 위한 Self-Reasoning이라는 새로운 프레임워크를 제안한다. 이를 통해 관련성 높은 문서를 검색하고, 문서로부터 출처를 명확하게 인용하도록 할 수 있다고 한다.
  3. 이 과정이 구체적으로는 3단계(RAP → EAP → TAP)로 나뉜다.
    • RAP에서는 Retrieve한 문서가 질문에 관련성이 있는지 없는지, 그 이유는 무엇인지 LLM이 생성한다.
    • EAP에서는 가장 관련성이 높은 문서에서 증거(Evidence)에 해당하는 근거 문장들을 찾아내고, 그 문장을 답변의 출처로 인용해도 되는 이유를 LLM이 생성한다.
    • TAP에서는 RAP와 EAP에서 생성한 모든 내용을 종합해서 질문에 대한 답변을 2가지 버전으로 출력하는데, 긴 버전을 Analysis, 짧은 버전을 answer라고 부른다.
  4. 이러한 Self-Reasoning이 잘 굴러가기 위해서 먼저 RAP/EAP/TAP 단계를 모두 이해하도록 LLM 파인튜닝했는데, 그 데이터는 GPT-4로 가공 후 필터링, 총 2,000개 샘플을 제작해서 사용했으며,
  5. 이게 chain구조로 된 꽤 어려운 task이기 때문에 각 단계마다 학습률을 다르게 설정하여 모델이 점진적으로 긴 추론 경로를 생성할 수 있도록 유도하는 '점진적 학습 방법'이라는 걸 적용했다. 
  6. 그래서 실험을 해 보니 원하는대로 문제점이 잘 해결되더라!
 

Self-Reasoning Framework

전체 프로세스를 논문 Figure 2에 해당하는 아래 도식과 함께 이해하는 것이 좋음

 


RAP(Relevance-Aware Process)

instructs the LLM to judge the relevance between the retrieved documents and the question

LLM이 검색된 문서와 질문의 관련성을 평가하도록 instruct

  • 본 연구에서 top-k relevant documents를 가져오는 리트리버는 DPR()과 Contriever을 사용하였음
  • LLM이 직접 retrieved된 문서 D와 주어진 질문 q와의 관련성을 판단(T/F)하고, 그에 대한 이유를 생성하게 한다. 도표에 보이는 것처럼 각각 RelevantRelevant Reason으로 생성됨.
  • 만약 가져온 문서중에 관련 있는 문서가 하나도 없으면, LLM이 가지고 있는 내부 지식을 바탕으로 답변하는 수밖에 없음.
4.4 데이터 생성 및 품질 관리 (Data Generation and Quality Control)에 따르면, RAP단계를 위해 GPT-4로 ground truth에 해당하는 label data를 생성했다고 한다. Relevant가 True일 때에 해당하는 Positive samples뿐만 아니라 False일 때 Relevant Reason으로 why the given documents cannot answer the question - 찾아진 문서가 왜 질문에 답할 수 없는지도 답할 수 있는 Negative samples 데이터까지 균형 있게 만들었다고 함.

EAP : Evidence-Aware Selective Process

directs the LLM to choose and cite relevant documents, and then automatically select snippets of key sentences as evidence from the cited documents

LLM이 관련 문서를 선택하고 인용할 때, 인용한document로부터 snippets of key sentences를 증거로 선택하도록 direct

즉, 검색된 문서 중에서 질문에 가장 관련이 있는 문서를 우선적으로 선택하고, "증거" 문장과 함께 해당 문장이 왜 답변에 도움이 되는지에 대한 이유를 생성하도록 유도

  • We define the selected sentence as evidence in our paper - 관련성 있는 문서로부터 찾아낸 질문과 관계 있는 구절을 본 논문에서는 ‘evidence’라고 부름. snippets of key sentences이라고도 함. EAP 단계에서는 그 Evidence를 찾아내도록 먼저 명령을 함. 이게 곧 도식에서 cite content에 해당.
  • the reason why the selected snippets can answer the question - 증거를 찾았으면, 다음 단계로는 그 증거가 왜 질문에 대한 답변이 될 수 있는지에 대한 이유를 생성하도록 함. 이는 도식에서 reason to cite에 해당.

Trajectory Analysis Process, TAP (궤적 분석 프로세스)

requires the LLM to generate a concise analysis based on all gathered self-reasoning trajectories generated by previous two processes and subsequently provide the final inferred answer.

앞의 두 과정에서 생성된 자체 추론 궤적(self-reasoning trajectories)을 분석하여 최종 답변 생성

앞의 두 과정에서 생성된 관련성 및 증거 선택 정보를 기반으로 요약된 분석을 생성하고, 최종적으로 질문에 대한 답을 도출

  • TAP단계에서는 we ask the LLM to analyze the reasoning trajectories within itself - 지금까지 RAP, EAP 단계에서 생성한 모든 것들을 종합해서 Analysis를 최종 출력하고, and ultimately to output a concise analysis and a short answer - 최종 답변을 짧고 명확하게 도출하도록 유도함.
  • 즉 최종 답변은 analysis와 answer, 두 가지 형태로 출력되는데,
  • the analysis output is defined as a longform answer - analysis를 ‘롱폼 답변'으로,
  • and the answer output is defined as a short-short answer - answer를 ‘숏폼 답변’으로 생각함.

마무리

실험 결과 Self-Reasoning 프레임워크는 소량의 학습 데이터(2,000개 샘플)로도 효율적인 성능을 낼 수 있음을 입증하였고

각 단계(RAP, EAP, TAP)의 개별 효과를 평가했을 때 세 가지 단계가 모두 포함된 완전한 Self-Reasoning 프레임워크가 가장 높은 성능을 나타냈다고 하는데, 특히 RAP를 제거하면 특히 사실 검증 정확도가 크게 떨어져, 관련 문서 필터링이 매우 중요함을 확인했다고 한다.

인용의 정확성과 재현율을 평가하기 위해 사람의 평가와 비교했을 때 유사한 점이 있어서 이는 Self-Reasoning이 인용의 신뢰성을 높이는 데 효과적임을 시사했다고 한다.

공식 깃허브나 코드 등이 공개되지 않아 조금 아쉽고, 실험한 모델이 좀 철 지난 모델들인 점이 또 아쉽지만, 원하는 task에 맞게 체인 형태의 논리 구조로 데이터를 가공하고 고성능의 RAG를 유도하는 아이디어 자체는 reference로 차용하기 좋은 논문인 것 같다.

끝!

(*) 본 논문 리뷰 포스팅은 수리링이 직접 BERT 논문 원문을 처음부터 끝까지 읽고 작성했습니다. 포스팅을 참고하시는 경우 반드시 출처를 밝혀주시기를 미리 부탁드립니다. 감사합니다.

https://arxiv.org/pdf/1810.04805

본 논문 리뷰는 BERT 원문을 직접 읽고 버트의 핵심 아이디어와 구조에 대해서 살펴봅니다. 구체적인 실험과 학습 결과, 성능 지표 등에 대한 리뷰는 생략하는 점 양해 바랍니다.


초록 Abstract

  • 트랜스포머의 인코더는 Recurrent(순차) 구조 없이 입력을 '통으로' 받습니다. 따라서 입력 시퀀스의 각 위치에서 왼쪽, 오른쪽 양방향(Bidirectional) 문맥을 모두 고려할 수 있습니다.
  • 이와 반대로 디코더는 마스킹을 통해 현재 시점까지의 토큰들만을 참조하는 단방향(unidirectional)으로 작동합니다. 

BERT는 Bidirectional Encoder Representations from Transformers의 약자로, 이름에서도 알 수 있듯 트랜스포머의 '인코더'에만 집중한 모델입니다. 따라서 양방향(Bidirectional) 문맥을 고려하여 언어를 잘 이해하도록 학습(pre-train)이 되었습니다. 

이 때 BERT의 R(Representation)은 무슨 의미일까요? NLP 태스크에서 자주 언급되는 Word Representation은 인간의 언어를 다차원 벡터로 표현하여 컴퓨터가 이해할 수 있게 하는 작업이나 그 결과물을 의미합니다. BERT의 R(Representation) 역시 입력된 단어나 문장의 의미를 벡터 형태로 표현하여 모델이 해당 언어의 문맥과 의미를 이해할 수 있도록 하는 작업을 포함합니다. 본 포스팅에서는 따로 '표현'이라는 한국어로 직역하지 않고 그대로 reperesentation으로 표기할 것임을 미리 밝히겠습니다.

BERT is designed to pre-train deep bidirectional representations from unlabeled text by jointly conditioning on both left and right context in all layers. As a result, the pre-trained BERT model can be fine-tuned with just one additional output layer to create state-of-the-art models for a wide range of tasks, such as question answering and language inferences, without substantial task-specific modifications.

초록에서는 BERT가 모든 레이어에서 deep한 bidirectional representations을 학습하기 위해 고안되었으며, 사전 학습이 완료된 BERT 모델에 딱 1개의 output layer만 추가해서 파인튜닝이 가능할 정도로 '파인튜닝이 쉽고 용이하다'고 강조하고 있습니다. 이를 통해 QA(Question-Answering)이나 Langue Inferences과 같은 다양한 작업을 수행할 수 있는 것이죠 :-)



1 서론 Introduction

There are two existing strategies for applying pre-trained language representations to downstream tasks: feature-based and fine-tuning. .... (중략) they use unidirectional language models to learn general language representations. We argue that current techniques restrict the power of the pre-trained representations, especially for the fine-tuning approaches. 

사전 학습된 언어 representation을 다운스트림 작업에 적용하는 두 가지 기존 전략으로는 특징(feature-based) 기반 접근법과 파인튜닝 접근법이 있습니다.

  • 딥러닝에서 다운스트림 태스크(downstream tasks)는 특히 사전 학습(pre-trained)된 모델에 Transfer Learning/Fine Tuning을 통해 수행하고자 하는 구체적인 하위 작업을 뜻합니다. 예를 들어, 자연어 처리 분야에서는 텍스트 분류, 감정 분석, 명명된 개체 인식(NER), 질문 응답(QA) 등이 다운스트림 작업에 해당할 수 있겠죠.
  • Feature-based 방법은 사전 학습된 언어 모델을 사용하여 텍스트 데이터를 처리한 후, 그 결과로 나온 특징(feature) 벡터를 다른 기계 학습 모델에 입력으로 사용하는 방법입니다. 따라서 전체 과정이 하나의 모델로 통합되어 한 번에 학습되고 예측되는 end-to-end 방식이라고 볼 수 없습니다. (대표적으로 ELMo)
  • Fine-tuning 방법은 사전 학습된 언어 모델을 특정 작업에 맞게 추가 학습시켜 사용하는 방식으로, 전체 과정을 하나의 모델로 통합하여 end-to-end 방식으로 학습과 예측을 수행합니다. (대표적으로 OpenAI GPT)

논문에서는 ELMo나 GPT같은 기존의 모델이 representation을 학습하기 위해 unidirectional - 단방향 언어모델을 사용하면서 사전 학습된 representation을 온전히 활용하지 못했고, 그래서 특히 파인튜닝 단계에서 문제가 많았다고 지적합니다.

부록 Figure 3.

The major limitation is that standard language models are unidirectional, and this limits the choice of architectures that can be used during pre-training. For example, in OpenAI GPT, the authors use a left-to-right architecture, where every token can only attend to previous tokens in the self-attention layers of the Transformer (Vaswani et al., 2017). Such restrictions are sub-optimal for sentence-level tasks, and could be very harmful when applying fine-tuning based approaches to token-level tasks such as question answering, where it is crucial to incorporate context from both directions.

단방향 모델은 문맥의 일부만을 참조할 수 있습니다. 예를 들어, OpenAI GPT는 디코더 특화 모델로, 자신의 이전 토큰까지만 참조할 수 있는 좌->우 단방향 아키텍처를 사용하여 출력을 생성하는 특징이 있습니다.

  • 예를 들어 GPT가 QA(Question-Answering) 작업을 수행한다고 해 봅시다. QA 작업에서는 질문(Q)을 받아 문서(Context)의 어느 부분이 정답에 해당하는지를 알아내야 하고, 이 과정에서 정답의 앞뒤 문맥이 모두 중요하게 고려되어야 합니다. 하지만 GPT는 문서를 왼쪽->오른쪽 단방향으로만 읽기 때문에, 정답의 뒷부분에 있는 중요한 정보를 참조하기 어렵습니다. 따라서 QA 작업을 위한 fine-tuning을 수행할 때 전체 문맥을 이해하는 데 어려움이 있을 것이고, 그만큼 정확한 답을 찾아내도록 fine-tuning이 어려울 수 있다는 것이죠.

대놓고 OpenAI 저격하는거 꿀잼...

In this paper, we improve the fine-tuning based approaches by proposing BERT: Bidirectional Encoder Representations from Transformers. (중략) The masked language model randomly masks some of the tokens from the input, and the objective is to predict the original vocabulary id of the masked word based only on its context. Unlike left-to-right language model pre-training, the MLM objective enables the representation to fuse the left and the right context, which allows us to pretrain a deep bidirectional Transformer.

In addition to the masked language model, we also use a “next sentence prediction” task that jointly pre-trains text-pair representations.

본 논문은 BERT를 통해 파인튜닝 기반 접근법을 개선하는데, 대표적으로 "MLM(Masked Language Model)"을 도입합니다.

  1. 입력 토큰에서 무작위로 마스킹을 한 다음,
  2. '양방향' 문맥을 고려하여 마스킹한 부분을 예측하도록 학습을 시킵니다.
  3. deep한 스트럭처를 실현합니다.

또한 BERT는 MLM 외에도 텍스트 쌍 표현을 공동으로 사전 학습하는 "NSP(Next Sentence Prediction)"를 함께 사용합니다.



3 버트 BERT

BERT는 크게 pre-training과 fine-tuning의 2스텝으로 이루어져 있는데요.

During pre-training, the model is trained on unlabeled data over different pre-training tasks.

For finetuning, the BERT model is first initialized with the pre-trained parameters, and all of the parameters are fine-tuned using labeled data from the downstream tasks. Each downstream task has separate fine-tuned models, even though they are initialized with the same pre-trained parameters. 
  1. 우선 버트는 라벨이 따로 없는 데이터로 '비지도 학습'을 통해 사전 학습(pre-training)을 합니다.
  2. 다음 파인튜닝 단계에서는 라벨이 있는 데이터로 '지도 학습'을 하면서 사전에 학습한 파라미터 전체를 업데이트하게 됩니다. 나의 다운스트림 태스크가 무엇이냐에 따라서 사용하는 데이터도 다를거고, 당연히 그에 따라 업데이트 되는 파라미터 값도 달라지겠죠.
A distinctive feature of BERT is its unified architecture across different tasks. There is minimal difference between the pre-trained architecture and the final downstream architecture.

BERT는 초록에서 살펴본 바와 같이 fine-tuned with just one additional output layer : 단 하나의 아웃풋 레이어만 추가해서 파인튜닝을 합니다. 그래서 다운스트림 태스크에 따른 파인튜닝 전후에 구조 차이가 거의 없는 편이고, 그만큼 다양한 NLP 작업에서 일관된 성능을 발휘할 수 있는 장점이 있습니다.

We primarily report results on two model sizes: BERT BASE (L=12, H=768, A=12, Total Parameters=110M) and BERT LARGE (L=24, H=1024, A=16, Total Parameters=340M).

버트에는 크게 두가지 사이즈의 모델이 있어요.

  • BERT BASE
    • 768차원(H)을 12개의 벡터로 나눠서(64차원씩) 멀티헤드 어텐션수행(A)
    • 인코더 블록 총 12번 반복(L)
  • BERT LARGE
    • 1024차원(H)을 16개의 벡터로 나눠서(64차원씩)멀티헤드 어텐션수행(A)
    • 인코더 블록 총 24번 반복(L)

바닐라 트랜스포머가 512차원을 8개의 벡터로 나눠서(64차원씩) 멀티헤드 어텐션 수행, 인코더 블록을 총 6번 반복(후 디코더를 사용)했던 것과 비교하면 버트는 그보다 훨씬 더 deep한 인코더를 구성했다고 볼 수 있고, 이로 인해 버트는 문맥 정보를 더욱 깊이 학습하게 되어 다양한 NLP작업에서 높은 성능을 발휘할 수 있게 됩니다. 논문 초록에서 BERT가 deep한 bidirectional(both left and right) representations을 학습하도록 고안되었다고 한 이유를 여기서 찾아볼 수 있겠네요.

BERT BASE was chosen to have the same model size as OpenAI GPT for comparison purposes. Critically, however, the BERT Transformer uses bidirectional self-attention, while the GPT Transformer uses constrained self-attention where every token can only attend to context to its left.

논문에서 계속 OPENAI GPT와의 비교를 하면서 우리는 양방향이고 쟤네는 단방향이라 우리가 더 좋다!는 뉘앙스의 문장이 빈번하게 나오는데요. (ㅋㅋㅋㅋ) BERT와 GPT는 애초에 타겟하는 목적이 다르기 때문에, 무엇이 더 좋고 나쁘다고 비교할 문제는 아닌것 같고, 저는 각자가 목적에 맞는 구조를 알맞게 잘 선택한 것으로 이해했습니다.


[CLS] is a special symbol added in front of every input example, and [SEP] is a special separator token.

To make BERT handle a variety of down-stream tasks, our input representation is able to unambiguously represent both a single sentence and a pair of sentences (e.g., h Question, Answeri) in one token sequence. We use WordPiece embeddings (Wu et al., 2016) with a 30,000 token vocabulary.

BERT는 fine-tuning을 통해 다양한 다운스트림 task를 수행할 수 있도록 input representation으로 하나의 문장(a single sentence)을 받을 수도 있고, 한 쌍의 문장(a pair of sentence)을 받을 수도 있도록 설계되었습니다. 토큰 임베딩을 위해서는 30,000개의 토큰 어휘를 가진 WordPiece 임베딩(Wu et al., 2016)을 사용하구요.

The first token of every sequence is always a special classification token ([CLS]). The final hidden state corresponding to this token is used as the aggregate sequence representation for classification tasks. Sentence pairs are packed together into a single sequence. We differentiate the sentences in two ways. First, we separate them with a special token ([SEP]). Second, we add a learned embedding to every token indicating whether it belongs to sentence A or sentence B. 

모든 시퀀스는 다음과 같이 구성되어 있습니다.

  • 첫 번째 토큰 : 분류 작업에 필요한 토큰 [CLS]
  • 첫 번째 문장(A) 토큰 
  • 가운데 [SEP] 토큰 : 두 문장을 구분하는 역할
  • 두 번째 문장(B) 토큰
  • 마지막 [SEP] 토큰 : 시퀀스의 끝을 알려주는 역할

이 때 각 토큰이 문장 A에 속하는지 B에 속하는지 구별할 수 있는 Segment Embeddings 작업이 추가 수행됩니다. 아래 Figure 2에서 이 부분을 조금더 시각화해서 살펴볼 수 있습니다. 

트랜스포머가 Token Embedding과 Positional Encoding 정보를 더해서 입력값을 완성하는 것에서 나아가 BERT는 Segment Embeddings까지 한 번 더 더해주게 됩니다. [SEP] 토큰을 기준으로 각 토큰이 문장 A에 속하는지 B에 속하는지를 구분하는 것이죠.

이 작업은 이어서 자세히 살펴볼 문장 예측(NSP)을 위해 사용됩니다.


3-1. Pre-training BERT

BERT의 Pre-training의 핵심 unsupervised-task, MLM과 NSP를 살펴봅시다. 

[1] Masked LM(MLM) : 마스킹된 언어 모델

In order to train a deep bidirectional representation, we simply mask some percentage of the input tokens at random, and then predict those masked tokens. (중략) In all of our experiments, we mask 15% of all WordPiece tokens in each sequence at random. In contrast to denoising auto-encoders (Vincent et al., 2008), we only predict the masked words rather than reconstructing the entire input.

MLM은 deep bidirectional representation을 가능하게 하기 위해서 input tokens의 15%를 무작위로 랜덤 마스킹하고, 그 부분을 예측하도록 학습을 시키는 과정을 의미합니다.

Although this allows us to obtain a bidirectional pre-trained model, a downside is that we are creating a mismatch between pre-training and fine-tuning, since the [MASK] token does not appear during fine-tuning. To mitigate this, we do not always replace “masked” words with the actual [MASK] token.

The training data generator chooses 15% of the token positions at random for prediction. If the i-th token is chosen, we replace the i-th token with (1) the [MASK] token 80% of the time (2) a random token 10% of the time (3) the unchanged i-th token 10% of the time. 

그런데, 문제가 있습니다. 우리가 downstream 태스크를 위해 BERT를 파인튜닝할 때, 파인튜닝용으로 준비한 데이터에 15%를 지켜 마스킹이 준비되어있기는 아무래도 어렵겠죠. 따라서 pre-training data와 fine-tuning 데이터가 서로 일치하지 않는 문제점이 발생할 수가 있습니다.

  1. 따라서 버트는 input tokens의 15%를 무작위로 선택한 다음, 선택된 i번째 토큰을
  2. 80% 확률로 진짜 마스크 토큰([MASK])으로 대체합니다. 예를 들어 '감자'를 [MASK]로 대체하고 이를 예측하도록 학습시킵니다.
  3. 10% 확률로 무작위 토큰으로 대체합니다. 예를 들어 '감자'를 '생선'으로 바꾸어 버립니다.
  4. 10% 확률로 원래 토큰을 그대로 둡니다. 예를 들어 '감자'를 그냥 그대로 '감자'로 둡니다.

부록 A - Additional Details for BERT

The advantage of this procedure is that the Transformer encoder does not know which words it will be asked to predict or which have been replaced by random words, so it is forced to keep a distributional contextual representation of every input token.

부록에 따르면 이러한 방법을 사용함으로써 인코더가 예측할 단어나 무작위로 대체된 단어를 알지 못해, 모든 입력 토큰의 문맥을 이해하도록 상황이 강제되는 이점이 있다고 합니다. 즉, 항상 [MASK] 토큰에만 의존하지 않고 문맥을 기반으로 예측할 수 있는 능력을 기르고, 파인튜닝 데이터에 [MASK] 토큰이 등장하지 않아도 문제가 없도록 설계한 것입니다.


[2] Next Sentence Prediction(NSP) : 다음 문장 예측

Many important downstream tasks such as Question Answering (QA) and Natural Language Inference (NLI) are based on understanding the relationship between two sentences, which is not directly captured by language modeling.

질의응답(QA)나 자연어 추론(NLI) 같은 작업에서는 '두 문장의 관계'를 파악할 필요성이 있습니다. 그런데 기존의 language modeling은 주로 다음 단어를 예측하거나, 문장 내에서 단어의 순서와 패턴을 학습하는 등의 작업을 중점적으로 수행합니다. 두 문장 사이의 관계를 파악하는 것과는 거리가 멀죠. 그래서 BERT는 QA나 NLI같은 다양한 downstream task를 수행할 수 있도록, 모델이 두 문장 사이의 관계를 파악할 수 있도록 pre-train을 시켰습니다.

In order to train a model that understands sentence relationships, we pre-train for a binarized next sentence prediction task that can be trivially generated from any monolingual corpus.

BERT에서 NSP(Next Sentence Prediction)는 입력 시퀀스 내에서 문장 A와 문장 B가 서로 연속된 문장인지 아닌지를 판단하는 이진 분류(binary classification) 작업입니다. 주어진 두 문장이 실제로 연속된 문장(True)인지, 아니면 무작위로 선택된 다른 문장(False)인지를 예측합니다. NSP는 monolingual corpus(단일 언어 코퍼스) - 한 가지 언어로 된 텍스트 데이터 집합 내에서 학습이 가능합니다.

이 때, 앞서 살펴본 [CLS] 토큰이 바로 다음 문장 예측(NSP)에 사용되는 토큰이구요. MLM의 경우 입력 시퀀스 내에서 선택된 15%의 토큰이 80%의 확률로 마스킹되지만, NSP의 경우 모든 입력 시퀀스에 대해서 100% 수행이 됩니다.

Specifically, when choosing the sentences A and B for each pretraining example, 50% of the time B is the actual next sentence that follows A (labeled as IsNext), and 50% of the time it is a random sentence from the corpus (labeled as NotNext). As we show in Figure 1, C is used for next sentence prediction (NSP).5 Despite its simplicity, we demonstrate in Section 5.1 that pre-training towards this task is very beneficial to both QA and NLI.
  • 50%의 입력 시퀀스는 실제로 문장 A와 문장 B가 연속된 문장으로 구성되며, 레이블 IsNext(True)를 가집니다.
  • 나머지 50%의 입력 시퀀스는 무작위로 선택된 문장으로 구성되며, 레이블 NotNext(False)를 가집니다.

부록 A - Additional Details for BERT

어찌 보면 굉장히 단순한 작업이지만, NSP 작업을 통해 BERT는 두 문장 간의 관계를 이해하는 능력을 학습하게 되고, 이로 인해 QA(Question Answering)나 NLI(Natural Language Inference)와 같은 다운스트림 작업을 더 잘 수행하게 된다고 하네요.

The NSP task is closely related to representationlearning objectives used in Jernite et al. (2017) and Logeswaran and Lee (2018). However, in prior work, only sentence embeddings are transferred to down-stream tasks, where BERT transfers all parameters to initialize end-task model parameters

이 부분은 - BERT의 NSP task가 이전 연구 (Jernite et al., 2017; Logeswaran and Lee, 2018)에서 영감을 받기는 했지만, 두 문장 사이의 관계를 파악하도록 CLI 토큰을 사용한 것은 BERT의 독창적인 아이디어다 - 정도로 이해하고 넘어가겠습니다.


3-2. Fine-tuning BERT

For each task, we simply plug in the task-specific inputs and outputs into BERT and finetune all the parameters end-to-end.

BERT의 파인튜닝(fine-tuning)은 사전 학습된 모델의 모든 파라미터를 end-to-end 방식으로 업데이트합니다.

At the input, sentence A and sentence B from pre-training are analogous to
(1) sentence pairs in paraphrasing,
(2) hypothesis-premise pairs in entailment,
(3) question-passage pairs in question answering, and
(4) a degenerate text-∅ pair in text classification or sequence tagging.

BERT의 사전 학습에서 사용된 문장 A와 문장 B의 구조는 파인튜닝을 통해 다양한 다운스트림 작업에 적용될 수 있어요. 예를 들어서,

  • Paraphrasing: 두 문장이 서로 같은 의미를 전달하는지 평가할 수 있습니다.
  • Entailment: 문장 A는 가설(hypothesis), 문장 B는 전제(premise)일 때, 이 두 문장이 포함 관계에 있는지 판단할 수 있습니다.
  • Question Answering: 문장 A는 질문(question), 문장 B는 답변을 포함한 문맥(context)일 때, 문맥에서 질문에 대한 답을 찾는 질의응답 작업을 수행할 수 있습니다.
  • Text Classification or Sequence Tagging: 문장 A는 텍스트, 문장 B는 빈 값(∅)일 때, 단일 문장 분류 작업이나 시퀀스 태깅 작업을 수행할 수 있습니다.

At the output, the token representations are fed into an output layer for tokenlevel tasks, such as sequence tagging or question answering, and the [CLS] representation is fed into an output layer for classification, such as entailment or sentiment analysis.

출력 단계에서는 2가지 작업이 이루어집니다.

  1. Token-Level Tasks (토큰 레벨 작업)
    • 입력 시퀀스의 각 토큰에 대해 BERT는 벡터 representation을 생성하고, 이 벡터는 각 토큰의 의미와 문맥 정보를 담고 있습니다. 이 representation이 출력 레이어로 전달되면  Sequence Tagging (시퀀스 태깅)이나 Question Answering (질문 응답) 등의 다운스트림 태스크를 수행할 수 있습니다.
      • Sequence Tagging (시퀀스 태깅): 예를 들어, 입력 문장에서 각 단어를 명사, 동사 등으로 태깅하는 작업입니다. "I am eating an apple"라는 문장이 주어지면, 각 단어에 대해 'I: PRON', 'am: VERB', 'eating: VERB', 'an: DET', 'apple: NOUN'와 같이 태깅합니다.
      • Question Answering (질문 응답): 문맥 내에서 질문에 대한 답을 찾는 작업입니다. 예를 들어,  "What is the color of the sky?"라는 질문이 주어졌을 때, "The sky is blue"라는 문맥에서 "blue"라는 단어를 찾아내는 작업입니다.
  2. Sentence-Level Tasks (문장 레벨 작업)
    • BERT에서 입력 시퀀스의 첫 번째 토큰인 [CLS] 토큰이 벡터로 변환되고, 이 벡터는 입력 시퀀스 전체를 대표하는 의미를 담고 있습니다. 이 CLS representation이 출력 레이어로 전달되면 entailment 또는 sentiment analysis 등의 문장 전체에 대한 예측을 수행할 수 있습니다.
      • Entailment (포함 관계 판단): 두 문장이 포함 관계에 있는지 판단하는 작업입니다. 예를 들어, "The cat is on the mat" (전제)와 "There is a cat on the mat" (가설)가 주어졌을 때, 포함 관계(True/False)를 예측합니다.
      • Sentiment Analysis (감정 분석): 입력 문장의 감정이 긍정적인지 부정적인지 분류하는 작업입니다. 예를 들어, "I love this movie"라는 문장이 주어졌을 때, 감정이 긍정적인지 부정적인지 예측합니다.
Compared to pre-training, fine-tuning is relatively inexpensive. 

사전 학습에 비해 파인튜닝은 상대적으로 굉장히 비용 효율적이고, 논문의 모든 결과는 단일 Cloud TPU에서 최대 1시간, GPU에서는 몇 시간 내에 재현할 수 있다고 하네요.



마무리

BERT 논문을 리뷰하면서 언어 모델의 사전 학습과 전이 학습이 자연어 처리에서 얼마나 중요한 역할을 하는지 깊이 이해할 수 있었습니다. 특히, BERT가 문맥을 이해하는 인코더를 구현하기 위해 양방향 아키텍처, MLM, NSP 작업을 적절히 고안하고 배치한 점이 무척 인상깊었어요. 자본과 기술력 뿐만 아니라 창의적이고 새로운 아이디어로 인공지능 혁신을 이어가는 모델들을 리뷰할 때마다 경이로움을 느끼게 됩니다.

저는 Long-Context를 처리해야 하는 LLM / QA task 솔루션 개발이라는 과제를 맡으면서 버트 논문을 리뷰하게 되었는데요. 최신 QA task들은 Long-Context를 처리하기 위해 버트 기반의 인코더 중심 모델뿐만 아니라 RAG 기술을 적용한 GPT 기반 디코더 중심 모델을 사용하고 있기도 한다는 사실을 알았습니다. 따라서 다음 논문 리뷰로는 OpenAI의 ChatGPT 초기 모델을 선정하여 구조를 파악해보려고 합니다. 

이것으로 BERT 논문 리뷰를 마치겠습니다. 읽어주셔서 감사합니다 :-)

Dice Score

U-Net 코드화 작업을 통해 image segmentation에서 흔히 사용되는 'dice score'라는 평가 지표와 친숙해지게 되었습니다. 다이스 스코어는 아래와 같은 수식을 통해 계산합니다.

Dice Score는 Precision과 Recall 점수를 나타내는 F1-Score와 비슷합니다. 민감도와 정밀도중에 무엇이 좋고 나쁜지는 까봐야 아는 것도 똑같고요.

도식화하면 위와 같은 그림으로 나타낼 수 있어요. 여기서 X와 Y는 각각 true value와 predicted value라고 생각하면 되겠습니다. Image segmentation에서 Dice score는 predicted value와 true value 간의 유사성을 측정할 수 있는 포괄적인 평가 방식인 거죠. 

예측한 마스크와 실제 마스크가 정확하게 일치하는 경우 dice score는 1의 값을 가지게 되고, 전혀 일치하지 않는 경우 0의 값을 가지게 됩니다. 따라서 계산된 dice score는 범위 [0, 1] 사이의 값을 가지게 되고, 마치 accuracy처럼 1에 가까운 값을 가지게 될수록 모델이 학습을 잘했다고 판단하게 됩니다.


문제 상황

그런데 코드 작업을 하면서, 아래와 같이 dice score가 범위 [0, 1]를 벗어난 값을 가지게 되는 것을 확인하게 되었습니다.

dice score가 -7부터 1.3까지.. 다이나믹 한 값을 가지는군요

https://www.kaggle.com/datasets/faizalkarim/flood-area-segmentation

 

Flood Area Segmentation

Segment the flooded area.

www.kaggle.com

저는 U-Net을 코드 구현하면서 kaggle의 Flood Area Segmentation 데이터를 가지고 작업중이었는데요. Kaggle에서 다른사람들이 쓴 코드는 어떤지 확인을 한 번 해 보았습니다.


다른 사람 코드 1

https://www.kaggle.com/code/gon213/flood-area-by-gontech

같은 Flood Area Segmentation 이미지셋으로 코딩하는 다른사람 코드

Dice score가 0, 1 사이로 제대로 나오고 있군요.

다른 사람 코드 2

https://www.kaggle.com/code/hugolearn/practice-image-segmentation-with-u-net/notebook

Carvana 데이터셋으로 코딩하는 다른사람 코드

요건 저랑 다른 데이터셋 (Carvana dataset) 사용하는 다른사람 코드인데, 보시면 dice score가 음수값이 나옵니다. 더 보면 -5가 나오기도 하고요... 그런데 다들 코드 잘썼다고 칭찬만 하고 이부분을 지적하는 사람이 없더라고요.


문제 해결

Dice score를 정확하게 계산하기 위해서 저는 아래와 같은 방법을 적용해서 문제를 해결했습니다 :) 참고로 제가 다루고 있는 데이터는 '홍수가 난 부분'과 '아닌 부분'으로 binary label을 가집니다.


1. 수식 변경

def dice_score(pred: torch.Tensor, mask: torch.Tensor, threshold: float = 0.5, smooth: float = 1e-6):
    # 시그모이드 적용 후 임계값을 통해 이진화
    pred = torch.sigmoid(pred)
    pred = (pred > threshold).float()
    
    # 마스크가 float 타입인지 확인
    mask = mask.float()
    
    # 교집합과 합집합 계산
    intersection = (pred * mask).sum(dim=[1, 2, 3])
    union = pred.sum(dim=[1, 2, 3]) + mask.sum(dim=[1, 2, 3])
    
    # Dice score 계산
    dice = (2. * intersection + smooth) / (union + smooth)
    
    # 배치의 평균 Dice score 반환
    return dice.mean().item()

처음에는 intersection과 union만 계산했는데, 0.5의 threshold와 smooth값을 추가해서 dice score를 정밀하게 계산했습니다.


2. 데이터 정규화

먼저 가지고 있는 Image와 Mask 데이터의 픽셀값이 0과 1 사이의 수치로 들어와 있는 것을 확인했습니다. 이후 Image data에만 Normalization transform을 적용해주었습니다. (기존에는 정규화를 아예 안했습니다.)

class Mydataset(Dataset):
    def __init__(self, root_dir='flood/', train=True, image_transforms=None, mask_transforms=None):
        super(Mydataset, self).__init__()
        self.train = train
        self.image_transforms = image_transforms
        self.mask_transforms = mask_transforms
        
        # 파일 경로 지정
        file_path = os.path.join(root_dir, 'train')
        file_mask_path = os.path.join(root_dir, 'masked')
        
        # 이미지 리스트 생성
        self.images = sorted([os.path.join(file_path, img) for img in os.listdir(file_path)])
        self.masks = sorted([os.path.join(file_mask_path, mask) for mask in os.listdir(file_mask_path)])
        
        # train, valid 데이터 분리
        split_ratio = int(len(self.images) * 0.8)
        if train: 
            self.images = self.images[:split_ratio]
            self.masks = self.masks[:split_ratio]  # train은 80%
        else:
            self.images = self.images[split_ratio:]
            self.masks = self.masks[split_ratio:]  # valid는 20%
            
    def __getitem__(self, index: int):
        original = Image.open(self.images[index]).convert('RGB') # index 번째의 이미지를 RGB 형식으로 열음
        masked = Image.open(self.masks[index]).convert('L') # 얘는 마스크를 L(grayscale) 형식으로 열음
        
        if self.image_transforms:  # 나중에 image augmentation에 사용됨
            original = self.image_transforms(original)
        if self.mask_transforms:   # 나중에 image augmentation에 사용됨
            masked = self.mask_transforms(masked)
            
        return {'img': original, 'mask': masked} # transform이 적용된 후 텐서를 반환
    
    def __len__(self):
        return len(self.images)  # 이미지의 파일 수를 반환함 -> train = True라면 train 데이터셋의 크기를, False라면 valid 데이터셋의 크기를 반환
            
            
 # ------------------------ data loader 정의하기 ------------------------ #
width = 360
height = 240
batch_size = 4
n_workers = 2

# 이미지만 정규화!
image_transforms = T.Compose([
    T.Resize((width, height)),
    T.ToTensor(),
    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# 마스크는 정규화 안할거임
mask_transforms = T.Compose([
    T.Resize((width, height)),
    T.ToTensor()
])

train_dataset = Mydataset(root_dir = "flood/", 
                          train = True, # train 용 지정
                          image_transforms = image_transforms, 
                          mask_transforms = mask_transforms)

valid_dataset = Mydataset(root_dir = "flood/", 
                          train = False, # valid 용 지정 
                          image_transforms = image_transforms, 
                          mask_transforms = mask_transforms)

train_dataset_loader = DataLoader(dataset = train_dataset, 
                                  batch_size = batch_size, 
                                  shuffle = True, 
                                  num_workers = n_workers)

valid_dataset_loader = DataLoader(dataset = valid_dataset, 
                                  batch_size = batch_size, 
                                  shuffle = True, 
                                  num_workers = n_workers)

이후 Dice Score 값이 [0, 1] 범위로 잘 계산되는 것을 확인할 수 있었습니다.


감사합니다 :)

https://smartest-suri.tistory.com/49

 

딥러닝 | U-Net(2015) 논문 리뷰

[주의] 본 포스팅은 수리링이 직접 U-Net 논문 원문을 읽고 리뷰한 내용을 담았으며, 참고 문헌이 있는 경우 출처를 명시하였습니다. 본문 내용에 틀린 부분이 있다면 댓글로 말씀해 주시고, 포스

smartest-suri.tistory.com

지난 번 포스팅에서 리뷰한 U-Net 논문을 파이토치를 이용한 코드로 구현한 과정을 정리해 보겠습니다.


1. [연습] Class 없이 한줄씩 구현

직관적인 이해를 위해서 파이토치 코드로 클래스 없이 한줄씩 유넷 구조를 구현해 보도록 하겠습니다. 

# 먼저 필요한 모듈을 임포트 해줍니다.

import torch
import torch.nn as nn
import torchvision.transforms.functional as TF

1-1. Contracting path - 인코딩 파트

  • 논문에서는 valid padding(패딩 없음)을 사용하지만 코드상의 편의를 위해서 모든 콘볼루션 레이어를 same padding(패딩 1)로 구현하겠습니다.
  • 이렇게 할 경우 나중에 skip-connection 파트에서 concatenate할 때 크기가 딱 맞아서 crop할 필요가 없습니다.
input_channels = 3 # 일단 RGB라고 생각하고 3으로 설정하겠습니다.

# 첫번째 블락
conv1 = nn.Conv2d(input_channels, 64, kernel_size = 3, padding = 1)
conv2 = nn.Conv2d(64, 64, kernel_size = 3, padding = 1)
pool1 = nn.MaxPool2d(kernel_size = 2, stride = 2)

# 두번째 블락
conv3 = nn.Conv2d(64, 128, kernel_size = 3, padding = 1)
conv4 = nn.Conv2d(128, 128, kernel_size = 3, padding = 1)
pool2 = nn.MaxPool2d(kernel_size = 2, stride = 2)

# 세번째 블락
conv5 = nn.Conv2d(128, 256, kernel_size = 3, padding = 1)
conv6 = nn.Conv2d(256, 256, kernel_size = 3, padding = 1)
pool3 = nn.MaxPool2d(kernel_size = 2, stride = 2)

# 네번째 블락
conv7 = nn.Conv2d(256, 512, kernel_size = 3, padding = 1)
conv8 = nn.Conv2d(512, 512, kernel_size = 3, padding = 1)
pool4 = nn.MaxPool2d(kernel_size = 2, stride = 2)

1-2. Bottleneck - 연결 파트

bottleneck

  • 연결 구간에 해당하는 Bottleneck 파트를 작성하겠습니다.
  • 여기까지 오면 최종 채널의 수는 1024가 됩니다.
conv9 = nn.Conv2d(512, 1024, kernel_size = 3, padding = 1)
conv10 = nn.Conv2d(1024, 1024, kernel_size = 3, padding = 1)

1-3. Expanding path - 디코딩 파트

  • 디코딩 파트에는 Skip-connection을 통한 사이즈와 필터의 변화에 주목해서 보시면 좋습니다.
# 첫 번째 블락
up1 = nn.ConvTranspose2d(1024, 512, kernel_size = 2, stride = 2)
# 위에 아웃풋은 512지만 나중에 코드에서 skip-connection을 통해 다시 인풋이 1024가 됨
conv11 = nn.Conv2d(1024, 512, kernel_size = 3, padding = 1)
conv12 = nn.Conv2d(512, 512, kernel_size = 3, padding = 1)

# 두 번째 블락
up2 = nn.ConvTranspose2d(512, 256, kernel_size = 2, stride = 2)
conv13 = nn.Conv2d(512, 256, kernel_size = 3, padding = 1) # Skip-connection 포함
conv14 = nn.Conv2d(256, 256, kernel_size = 3, padding = 1)

# 세 번째 블락
up3 = nn.ConvTranspose2d(256, 128, kernel_size = 2, stride = 2)
conv15 = nn.Conv2d(256, 128, kernel_size = 3, padding = 1) # Skip-connection 포함
conv16 = nn.Conv2d(128, 128, kernel_size = 3, padding = 1)

# 네 번째 블락
up4 = nn.ConvTranspose2d(128, 64, kernel_size = 2, stride = 2)
conv17 = nn.Conv2d(128, 64, kernel_size = 3, padding = 1) # Skip-connection 포함
conv18 = nn.Conv2d(64, 64, kernel_size = 3, padding = 1)

# 마지막 아웃풋에서는 1x1 사이즈 콘볼루션을 사용한다고 이야기함.
output = nn.Conv2d(64, 2, kernel_size = 1, padding = 1)

1-4. Forward-pass

포워드 학습 과정을 한줄씩 구현해 보겠습니다.

def unet_forward(x):
    # 인코더 파트
    x = TF.relu(conv1(x))
    x1 = TF.relu(conv2d(x))
    x = pool1(x1)

    x = TF.relu(conv3(x))
    x2 = TF.relu(conv4(x))
    x = pool2(x2)

    x = TF.relu(conv5(x))
    x3 = TF.relu(conv6(x))
    x = pool3(x3)

    x = TF.relu(conv7(x))
    x4 = TF.relu(conv8(x))
    x = pool4(x4)

    # 연결 bottleneck 파트
    x = TF.relu(conv9(x))
    x = TF.relu(conv10(x))

    # 디코더 파트
    x5 = up1(x)
    x = torch.cat([x5, x4], dim = 1) # skip-connection
    x = TF.relu(conv11(x))
    x = TF.relu(conv12(x))

    x6 = up2(x)
    x = torch.cat([x6, x3], dim = 1) # skip-connection
    x = TF.relu(conv13(x))
    x = TF.relu(conv14(x))

    x7 = up3(x)
    x = torch.cat([x7, x2], dim = 1) # skip-connection
    x = TF.relu(conv15(x))
    x = TF.relu(conv16(x))

    x8 = up4(x)
    x = torch.cat([x8, x1], dim = 1) # skip-connection
    x = TF.relu(conv17(x))
    x = TF.relu(conv18(x))

    # 아웃풋 파트
    output = output(x)

    return output

지금까지 클래스 없이 파이토치로 코드를 짜면서 유넷 구조를 직관적으로 이해해 보았습니다.



2. [실전] Class 이용해서 구현

이번엔 파이토치를 사용하는 만큼 클래스를 이용해서 실전 유넷 코드 구현을 해보겠습니다. 먼저 코드를 짜는 과정은 유튜브 [PyTorch Image Segmentation Tutorial with U-NET: everything from scratch baby (Aladdin Persson)]를 참고하여 작성했음을 미리 밝히겠습니다. 

https://youtu.be/IHq1t7NxS8k?si=776huHRjVsIlf_rS


2-1.  모든 블락의 2개 콘볼루션(Conv2d) 레이어를 정의하는 클래스

 

먼저 unet.py 파일을 만들고 클래스들을 작성하여 파일을 모듈화 하겠습니다.

class DoubleConv(mm.Module):
    def __init__(self, in_channels, out_channels):
        super(DoubleConv, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size = 3, stride = 1, padding = 1, bias = False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace = True),
            nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias = False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace = True)
        )

    def forward(self, x):
        return self.conv(x)
  • 1장과 마찬가지로 편의를 위해서 valid padding 대신 same padding을 사용합니다. Carvana 대회 1등한 팀도 same padding을 사용했다고 하네요. (저는 다른걸로 할거지만, 영상에서 Carvana 데이터셋을 이용해서 학습을 합니다)
  • Batch Normalization이 추가되었습니다. 논문에서 따로 Batch Normalization을 해준다는 언급은 없었는데요. 영상에 따르면 UNet이 2015년도 발표되었고, BatchNorm은 2016년에 고안된 아이디어라서 그렇다고 합니다. 찾아보니 유넷을 구현하는 많은 코드가 Batch Normalization을 추가하여 Gradient Vanishing/Exploding 문제를 보완하는 것 같습니다.
  • bias를 False로 설정해 주는 이유는 중간에 BatchNorm2d를 추가해주기 때문입니다. bias가 있어봤자 BatchNorm에 의해서 상쇄(cancel)되기 때문에 굳이 bias가 필요 없다고 영상에서 말하고 있습니다. 이부분은 나중에 BatchNorm 논문을 통해 따로 확인해 보겠습니다.

2-2. 유넷 전체 구조를 정의하는 클래스

해당 파트는 코드가 길어지기 때문에 주석을 이용해서 각 부분을 설명했습니다 :) 비교적 간단하지만 그래도 코드를 이해하시려면 유넷 전체 구조에 대한 이해가 필수적입니다.

  • 논문에서는 마지막 아웃풋의 채널이 2였습니다만, 저는 레이블이 0, 1로 binary인 데이터를 다룰 예정이기 때문에 output_channels의 default 값을 1로 두었습니다.
  • features 리스트는 각 블록에서 피처맵의 사이즈를 순서대로 정의하는 역할을 합니다. (contracting path의 경우 순서대로 64, 128, 256, 512 - expanding path의 경우 그 반대)
class UNET(nn.Module):
    def __init__(
            self, in_channels = 3, out_channels = 1, features = [64, 128, 256, 512]
    ):
        super(UNET, self).__init__()
        self.downs = nn.ModuleList() # Contracting path - 인코딩 파트의 모듈을 담을 리스트 선언
        self.ups = nn.ModuleList()   # Expanding path - 디코딩 파트의 모듈을 담을 리스트 선언
        self.pool = nn.MaxPool2d(kernel_size = 2, stride = 2) # 풀링은 모든 블럭에서 공통 사용됨

        # Contracting path (Down - 인코딩 파트) ------------------------
        for feature in features:
            self.downs.append(DoubleConv(in_channels, feature)) # 블록마다 더블콘볼루션 해주고 아웃풋은 feature맵 리스트 순서대로 할당(64, 128...)
            in_channels = feature # 다음 모듈의 인풋 사이즈를 feature로 업데이트

        # Bottleneck (인코딩, 디코딩 연결 파트) ---------------------------
        size = features[-1] # 512
        self.bottleneck = DoubleConv(size, size * 2) # 인풋 512 아웃풋 1024

        # Expanding path (Up - 디코딩 파트) ----------------------------
        for feature in features[::-1]: # 피처맵 사이즈 반대로!
            # 먼저 초록색 화살표에 해당하는 up-conv 레이어를 먼저 추가해 줍니다.
            self.ups.append(
                nn.ConvTranspose2d(
                    feature*2, feature, kernel_size = 2, stride = 2
                    # 인풋에 *2 해주는 이유 : 나중에 skip-connection을 통해서 들어오는 인풋 사이즈가 더블이 되기 때문!
                    # kernel과 stride size가 2인 이유는.. 논문에서 그렇게 하겠다고 했음 '_^ 
                )
            )
            # 이제 더블 콘볼루션 레이어 추가
            self.ups.append(DoubleConv(feature * 2, feature))

        # Output (아웃풋 파트) -----------------------------------------
        last_input = features[0] # 64
        self.final_conv = nn.Conv2d(last_input, out_channels, kernel_size = 1)

    ####################### **-- forward pass --** #######################
    def forward(self, x):
        skip_connections = []

        # Contracting path (Down - 인코딩 파트) ------------------------
        for down in self.downs: # 인코딩 파트를 지나면서 각 블록에서 저장된 마지막 모듈 하나씩 이터레이션
            x = down(x)
            skip_connections.append(x) # skip_connection 리스트에 추가
            x = self.pool(x)

        # Bottleneck (인코딩, 디코딩 연결 파트) ---------------------------
        x = self.bottleneck(x)
        skip_connections = skip_connections[::-1] # 디코딩 파트에서 순서대로 하나씩 뽑기 편하게 리스트 순서 반대로 뒤집어주기

        # Expanding path (Up - 디코딩 파트) ----------------------------
        for i in range(len(skip_connections)):
            x = self.ups[i * 2](x) # self_ups에는 순서대로 ConvTranspose2d와 DoubleConv가 들어가 있음. 
            # 0, 2, 4... 짝수번째에 해당하는 인덱스만 지정하면 순서대로 ConvTranspose2d와(up-conv) 모듈만 지정하게 됨
            skip_connection = skip_connections[i] # skip_connection 순서대로 하나씩 뽑아서
            # concatenate 해서 붙여줄(connection) 차례!
            # 그런데 만약 붙일때 shape이 맞지 않는다면... (특히 이미지의 input_shape이 홀수인 경우 이런 뻑이 나게됨)
            if x.shape != skip_connection.shape:
                x = TF.resize(x, size = skip_connection.shape[2:]) # 간단히resize로 맞춰주겠음
            concat_skip = torch.cat((skip_connection, x), dim = 1) # 이제 붙임!
            x = self.ups[i * 2 + 1](concat_skip)
            # 1, 3, 5... 홀수번째에 해당하는 인덱스만 지정하면 순서대로 DoubleConv 모듈만 지정하게 됨
            
        return self.final_conv(x)

2-3. 테스트

  • 간단한 랜덤 텐서를 생성해서 우리가 구현한 유넷 모델이 제대로 작동하는지 확인하겠습니다.
  • 모델 인풋과 아웃풋의 shape이 정확히 같은지 확인하는 작업이 들어가는데, image segmentation 작업의 특성상 output mask의 크기가 인풋과 동일해야 하기 때문에 그렇습니다.
  • assert 문법을 사용해서 인풋 아웃풋 shape이 다른 경우가 감지되면 AsserstionError를 발생시켜서 Debug에 활용하도록 합니다.
# 유넷 모델 테스트하는 함수 작성

def test():
    # 3장의 1채널(grayscale), width height가 각각 160인 랜덤 텐서 생성 (테스트용으로!)
    x = torch.randn((3, 1, 160, 160))
    # in_channels 1로 설정 (그레이스케일 이미지), out_channels 1로 설정 (binary output)
    model = UNET(in_channels = 1, out_channels = 1)
    # forward pass
    preds = model(x)
    # preds와 x의 shape 확인하기 - 두개가 같아야 함
    print(preds.shape)
    print(x.shape)
    assert preds.shape == x.shape 
        # True : 오케이
        # False : AssertionError

if __name__ == "__main__": # 메인 파일에서만 작동하고 모듈로 import된 경우에는 작동하지 않도록 함
    test()
  
# 실행 결과 ---------------------------
# torch.Size([3, 1, 160, 160])
# torch.Size([3, 1, 160, 160])

구동 결과 shape이 정확히 같아서 test 함수가 제대로 작동한 것을 확인했습니다.

 

여기까지 작성한 코드를 unet.py 파일로 저장해서 모듈화 해주었습니다.

이후 데이터를 로드에 필요한 dataset.py를 작성한 다음 학습에 필요한 utils.py train.py를 차례대로 작성하는 순서로 학습을 진행합니다. 해당 파트는 본 포스팅에서 제외하도록 하겠습니다.

지금까지 유넷 논문에서 살펴본 구조를 파이토치 코드로 구현하는 과정을 포스팅해 보았습니다 :-) 다음 포스팅에서는 트랜스포머 코드화 작업을 수행해 보도록 하겠습니다. 감사합니다!

[주의] 본 포스팅은 수리링이 직접 U-Net 논문 원문을 읽고 리뷰한 내용을 담았으며, 참고 문헌이 있는 경우 출처를 명시하였습니다. 본문 내용에 틀린 부분이 있다면 댓글로 말씀해 주시고, 포스팅을 출처 없이 불법 공유하지 말아주시기 바랍니다. 감사합니다.


U-Net: Convolutional Networks for BiomedicalImage Segmentation

https://arxiv.org/pdf/1505.04597

이미지 세그멘테이션(image segmentation)에서 빼놓을 수 없는 근본 모델 유넷. 유넷은 픽셀 기반으로 이미지를 분할하여 구분하는 모델로, 많은 최신 모델 속에서 그 구조가 사용되며 활약하고 있습니다.

유넷은 ISBI cell tracking challenge 2015 대회에서 등장한 모델로, 세포 이미지로부터 뛰어난 세그멘테이션 성능을 보여주며 우승을 차지했습니다. 놀라운 점은, 이 대회에서 제공된 학습 데이터가 고작 이미지 30장이었다고 합니다. 저는 논문 리뷰를 마치고서야 이 사실을 알게 되어서, 논문에서 계속 데이터 수가 부족했다고 우는 소리를 하는 이유가 있었구나.....! 대박적이다...! 라는 생각을 했습니다.

따라서 유넷 논문을 처음 공부하시는 분들께서는, 유넷이 어떻게 이러한 한계를 극복하며 뛰어난 segmentation을 구현할 수 있었는지에 주목하면서 리뷰를 해보시면 좋을 것 같습니다.

https://hpc.nih.gov/apps/UNet.html




Abstract

In this paper, we present a network and training strategy that relies on the strong use of data augmentation to use the available annotated samples more efficiently.

초록에서는 주어진 데이터를 더욱 효율적으로 사용하기 위해 data augmentation - 데이터 증강을 통한 네트워크와 학습 전략을 제시하겠다고 밝히고 있습니다. 

The architecture consists of a contracting path to capture context and a symmetric expanding path that enables precise localization. 

U넷의 네트워크 구조는 크게 2가지 path로 구성됩니다.

  1. 수축 경로 'contracting path' : capture context - 넓은 범위에서 이미지의 주요한 특징과 의미 추출
  2. 확장 경로 'expanding path' : 정교한 localization - 앞에서 추출한 의미를 위치정보와 결합하여 각 픽셀마다 어떤 객체에 속하는지를 구분
  3. (+) 'bottleneck' 연결 구간 : 수축 경로와 확장 경로를 연결 (논문에 언급되어 있지 않지만 흔히 보틀넥이라고 칭합니다.)
We show that such a network can be trained end-to-end from very few images and outperforms the prior best method (a sliding-window convolutional network) on the ISBI challenge for segmentation of neuronal structures in electron microscopic stacks.

구조를 그렇게 설계했더니 아주 적은 수의 이미지로도 end-to-end 학습이 가능했을 뿐더러 기존에 sliding-window 형식의 CNN구조를 사용하고 있던 베스트 모델보다도 성능이 훨씬 뛰어났다는 이야기를 하고 있습니다.

end-to-end 딥러닝이 뭔지 궁금하신 분께서는 아래 유튜브 동영상을 참고하시면 도움이 될거예요. end-to-end을 실현하려면 데이터가 많이 필요한 것이 핵심인데, 유넷은 이 부분을 augmentation으로 해결했다는 거죠. 어떤 증강기술을 사용했는지는 차차 설명하게 됩니다.


https://youtu.be/ImUoubi_t7s?si=JYQRmitCcbqp2CsD

이제는 이 교수님이 삼촌같이 느껴지네요...
Moreover, the network is fast. Segmentation of a 512x512 image takes less than a second on a recent GPU. 

게다가 처리속도도 아주 빠르다고 합니다.


여기까지 논문의 초록을 살펴봤는데요. 저는 초록에서 강조하고 있는 키워드가 아래와 같은 것으로 정리했습니다.

  • Data Augmentation
  • Contracting path & Expanding path
  • end-to-end 
  • Not a sliding window
  • Outperform, fast

그럼 본격적으로 본문을 살펴보면서 위와 같은 키워드들이 어떻게 설명되는지 살펴보도록 하겠습니다.



Introduction

The typical use of convolutional networks is on classification tasks, where the output to an image is a single class label. However, in many visual tasks, especially in biomedical image processing, the desired output should include localization, i.e., a class label is supposed to be assigned to each pixel.

전형적인 CNN 모델은 이미지를 입력하면, 이미지에 해당하는 하나의 class label이 출력되는 방식으로 '분류'작업을 수행합니다. 그런데 실제로 많은 과업에서 이렇게 이미지 전체에 해당하는 하나의 레이블이 필요한 경우보다는, localization을 포함한 결과가 필요한 경우가 많습니다. 즉, 이미지 전체가 아닌 이미지 내부의 각 픽셀에 해당하는 labeling이 필요한 거죠.

위의 예시에서 맨 왼쪽은 "이 사진은 고양이 사진이다!"라고 전체에 대한 레이블링을 수행하였습니다. 하지만 localization은 모든 픽셀에 대해서 레이블링을 수행합니다. 그래서 그 옆의 그림처럼 이 사진의 어떤 부분은 고양이고, 또 다른 부분은 잔디이고, 이렇게 다양한 결과값을 출력할 수가 있는 것입니다.

Moreover, thousands of training images are usually beyond reach in biomedical tasks.

특히 바이오메디컬(생의학) 분야에서는 데이터 수가 부족한 경우가 많다는 것도 고려할 필요가 있는데요. 레이블링을 의사같은 전문가가 해야하기 때문에, dog-cat 분류같이 아무나 할수 있는게 아니라는 이유에서 그렇다고 합니다. 대회에 참가해서 30장 데이터로 승부를 봐야 했던 유넷처럼 말이죠......... (아니 근데 아무리 그래도 30장은 좀 너무했다)

Hence, Ciresan et al. [1] trained a network in a sliding-window setup to predict the class label of each pixel by providing a local region (patch) around that pixel as input. First, this network can localize. Secondly, the training data in terms of patches is much larger than the number of training images. The resulting network won the EM segmentation challenge at ISBI 2012 by a large margin.

유넷 이전 다수의 모델은 sliding window를 적용했습니다. sliding window를 적용하면 하나의 이미지로부터 여러 개의 patch가 생기면서 데이터 수 부족 현상을 해결할 수 있었기 때문이죠.

출처 : https://www.mathworks.com/help/vision/ug/anchor-boxes-for-object-detection.html

Obviously, the strategy in Ciresan et al. [1] has two drawbacks. First, it is quite slow because the network must be run separately for each patch, and there is a lot of redundancy due to overlapping patches. Secondly, there is a trade-off between localization accuracy and the use of context. Larger patches require more max-pooling layers that reduce the localization accuracy, while small patches allow the network to see only little context. More recent approaches [11,4] proposed a classifier output that takes into account the features from multiple layers. Good localization and the use of context are possible at the same time.

그런데 sliding window를 적용하면 계산량이 많아지면서 전체 네트워크가 느려진다는 단점이 있습니다. 게다가, localization accuracy와 use of context - 즉 전체와 부분 사이의 tradeoff가 발생하는 문제점도 있었는데요.

드디어 그 유명한 유넷의 구조가 언급됩니다. 

In this paper, we build upon a more elegant architecture, the so-called “fully convolutional network” [9]. We modify and extend this architecture such that it works with very few training images and yields more precise segmentations; see Figure 1. 

유넷 페이퍼에서는 이러한 문제점들을 해결하기 위해 U모양의 "fully convolutional network"를 제안합니다. 적은 수의 트레이닝 이미지 데이터로도 학습이 잘 되고, 정확한 segmentation이 가능하도록 말이죠.

The main idea in [9] is to supplement a usual contracting network by successive layers, where pooling operators are replaced by upsampling operators. Hence, these layers increase the resolution of the output. 

핵심 아이디어는 초록에서 살펴본 바와 같이 contracting - expanding network구조를 사용하는 것입니다. 입력된 이미지는 contracting network의 연속적인 레이어 구조를 거치며 사이즈는 줄어들고 채널 수는 늘어납니다. 이어서 expanding network의 연속적인 레이어 구조에서 pooling 대신 upsampling을 거치며 사이즈가 다시 커지고, 채널 수는 줄어드는데요. 

In order to localize, high resolution features from the contracting path are combined with the upsampled output. A successive convolution layer can then learn to assemble a more precise output based on this information.

One important modification in our architecture is that in the upsampling part we have also a large number of feature channels, which allow the network to propagate context information to higher resolution layers. As a consequence, the expansive path is more or less symmetric to the contracting path, and yields a 
u-shaped architecture.

Feature 1 이미지에서 회색 화살표를 보시면 대칭 구조를 이루며 레이어들이 서로 짝을 이루고 있습니다. contracting path에서 추출한 Feature map의 정보가 업샘플링 과정에서 skip-connection 방식으로 결합되는 과정을 도식화한 것입니다.

논문에서 'skip-connection'이라는 용어가 직접적으로 언급되지는 않지만, 이러한 방식을 많은 사람들이 그렇게 명명하고 있습니다. 스킵 연결방식을 사용해서 expanding path의 콘볼루션 레이어는 contracting path로부터 정보를 제공받게 되고, 보다 정확한 출력을 조합하는 방법을 학습할 수 있게 됩니다. 깊은 레이어 구조를 거치며 다양한 정보가 손실될 가능성이 있는데, 그런 점을 skip-connection을 사용하여 보완해 학습이 잘 되도록 구조를 설계한 것이라고 보시면 되겠습니다.

The network does not have any fully connected layers and only uses the valid part of each convolution
, i.e., the segmentation map only contains the pixels, for which the full context is available in the input image. This strategy allows the seamless segmentation of arbitrarily large images by an overlap-tile strategy (see Figure 2). 

유넷 아키텍처는 기본적으로 fully connect layers로 이루어져 있지 않습니다. 대신, 앞에서 살펴본 바와 같이 모든 레이어가 Convolution(fully convolutional layers)으로 이루어져 있어요. 또 유넷 네트워크는 각 컨볼루션 연산에서 유효한 부분(valid part)만을 사용합니다. 이는 입력 이미지의 가장자리를 패딩 없이 처리하는 방식을 의미하는데요, 따라서 결과적으로 출력 이미지의 크기가 입력 이미지보다 작아지게 되고, 모든 출력 픽셀은 입력 이미지의 전체 컨텍스트(문맥 정보)를 갖게 됩니다.

저는 여기서 처음에 들었던 의문이.... '패딩 없이 처리'를 한다고 해서 어떻게 전체 컨텍스트 정보를 갖게 된다는 것인가? 라는 거였어요. 패딩을 사용 안하면 안하는거지, 그걸 굳이 저렇게 거창하게 말할 필요가 있나.. 싶었거든요. 이 부분은 바로 이어서 언급되는 '오버랩 타일 전략'을 살펴보면서 간단히 해결할 수 있었습니다.

(+) 그런데 실제로 코드로 구현하는 경우 계산의 편의를 위해 valid padding 대신 same padding을 사용하는 경우가 많은 것 같습니다.

유넷은 이미지의 가장자리에 패딩을 사용하지 않는 대신, 오버랩-타일(overlap-tile) 전략이라는 걸 사용합니다. 이 방식은 특히 큰 이미지를 분할할 때 유용한데요. 경계 영역에서의 정보 손실을 최소화할 수 있는 장점이 있다고 언급하고 있습니다. 아래 그림을 함께 보시죠.

잘 보면, 가장자리 부분이 내부 이미지를 상하좌우 대칭으로 미러링하여 채워져 있습니다.

To predict the pixels in the border region of the image, the missing context is extrapolated by mirroring the input image. This tiling strategy is important to apply the network to large images, since otherwise the resolution would be limited by the GPU memory.

U-Net에서 설명하는 타일링 전략(overlap-tile strategy)은 하나의 큰 이미지를 여러 개의 작은 타일로 나누어 처리하는 방식입니다. 이 때 경계 부분에서 패딩을 사용하는 대신, 입력 이미지를 반사하는 미러링(mirroring)하는 방법을 사용하고, 타일끼리 서로 겹치는 부분이 발생하기 때문에 '오버랩'이라는 단어가 앞에 붙게 되었어요. 이 오버랩 타일링 전략을 통해 유넷은 이미지 경계에서 패딩을 사용했을때 발생할 수 있는 정보 손실을 최소화할 수 있었고, 덕분에 정확한 경계 예측을 할 수가 있었어요.

생각해 보면 segmentation을 수행할 때 경계값을 명확하게 파악할 필요성이 있다고 납득이 되더라고요. 이 타일링 전략은, 앞에 서론에서 잠깐 살펴보았던 슬라이딩 윈도우(sliding window)의 문제점을 해결한 전략이라고 할 수 있겠습니다. 이러한 타일링 전략은 특히 큰 고해상도의 이미지에 유넷 네트워크를 적용할 때 중요했다고 하는데, 그렇지 않으면 해상도가 GPU 메모리에 의해 제한될 수가 있었다고 하네요.

https://youtu.be/O_7mR4H9WLk?si=9aNrE8Olud-7bq-Z

슬라이딩 윈도우와 타일링 전략, 미러링 부분에 추가 보충이 필요하신 분들께서는 위의 유투브 영상을 참고하시는 것을 추천합니다. 시각적으로 설명이 깔끔하게 잘 되어 있어서 저도 도움을 많이 받았습니다 :)


As for our tasks there is very little training data available, we use excessive data augmentation by applying elastic deformations to the available training images. This allows the network to learn invariance to such deformations, without the need to see these transformations in the annotated image corpus.

U-Net은 고작 30장밖에 되지 않는 데이터셋으로 성능을 뽑아야 했기 때문에, augmentation을 빡세게 굴렸다! 라고 밝히고 있습니다. 특히 그 중에서도 elastic deformation을 사용했다고 밝히고 있어요.

https://towardsdatascience.com/review-u-net-biomedical-image-segmentation-d02bf06ca760
https://towardsdatascience.com/review-u-net-biomedical-image-segmentation-d02bf06ca760

Pixel이 랜덤하게 다른 방향으로 뒤틀리도록 변형하는 'elastic deformation'을 통해 자연스럽고 현실세계에 있을법한 새로운 데이터를 만들어내는 의의가 있었다고 해요. 위의 그림을 보면 elastic deformation을 거쳤을 때 기본 이미지의 특성은 어느정도 유지되면서 이미지가  다양하게 변형되는 것을 볼 수 있습니다. 논문에 따르면 이런 방법의 변형이 특히 biomedical 생의학 분야에서 의미있다는 연구 결과가 있었다며 참고 문헌을 밝히고 있네요. (세포라는 게 아무래도 원래 찌글짜글 거리니까…..)

Another challenge in many cell segmentation tasks is the separation of touching objects of the same class; see Figure 3. To this end, we propose the use of a weighted loss, where the separating background labels between touching cells obtain a large weight in the loss function. The resulting network is applicable to various biomedical segmentation problems. In this paper, we show results on the segmentation of neuronal structures in EM stacks (an ongoing competition started at ISBI 2012), where we outperformed the network of Ciresan et al. [1]. Furthermore, we show results for cell segmentation in light microscopy images from the ISBI cell tracking challenge 2015. Here we won with a large margin on the two most challenging 2D transmitted light datasets.

세포 분할 작업에서 해결해야 하는 또 다른 도전 과제가 있는데요. 바로 같은 클래스의 서로 접촉하는 객체를 분리하는 것입니다. 위의 그림을 보면, 여러 개의 같은 세포가 서로 매우 가깝게 접촉해 있는 것을 확인할 수 있습니다. 얘네가 서로 다른 개체임에도 불구하고 label이 같아서 하나의 큰 뭉텅이로 간주될 위험성이 있단 말이죠.

유넷은 이러한 문제점을 해결하고자 접촉하는 세포 사이의 배경 레이블에 큰 가중치를 부여하는 weighted loss를 제안합니다. 탐지된 객체가 서로 가까이 붙어 있을 수록 그 사이의 배경이 큰 weight값을 갖게 되는 원리인데요. 위의 Fig. 3. 그림에서 d를 보시면 되고, 세포 사이의 거리가 가까울수록 배경이 빨간 값을 가지게 되는 점에 주목하시면 되겠습니다. 이 방법을 적용하면서 뛰어난 segmentation 성능을 확보하게 되었고, 이 덕분에 대회를 우승할 수 있었다고 밝히고 있었습니다.



Network Architecture

다음으로 유넷 네트워크의 구조에 대해서 조금 더 자세히 살펴보도록 하겠습니다. 이 부분은, 유넷 그림의 구조를 보면서 스스로 설명할 수 있어야 합니다.

Contracting path (left side)

The contracting path follows the typical architecture of a convolutional network. It consists of the repeated application of two 3x3 convolutions (unpadded convolutions), each followed by a rectified linear unit (ReLU) and a 2x2 max pooling operation with stride 2 for downsampling. At each downsampling step we double the number of feature channels.

Contracting path는 기본적인 콘볼루션 네트워크 구조로 되어 있습니다. (3x3) 필터를 사용해서 패딩 없이 콘볼루션을 계산한 뒤 ReLU를 걸어주는 과정2번 반복하고, (2x2) 사이즈와 stride 2의 max poolng을 적용하여 다운샘플링을 진행합니다. 이렇게 한 셋트의 다운샘플링을 진행할 때마다 채널의 수는 2배씩 증가하게 됩니다. 그림에선 다운샘플링을 4번 했네요.

이렇게 이미지의 사이즈가 줄어드는 과정에서 이미지의 특징이 추출되고, 채널이 늘어나는 과정에서 이미지의 다양한 특징을 확보하게 됩니다.

Expansive path (right side)

Every step in the expansive path consists of an upsampling of the feature map followed by a 2x2 convolution (“up-convolution”) that halves the number of feature channels, a concatenation with the correspondingly cropped feature map from the contracting path, and two 3x3 convolutions, each followed by a ReLU. The cropping is necessary due to the loss of border pixels in every convolution.

Expansive path에서는 (2x2) 필터 사이즈의 up-convolution을 통해 특징 맵(feature map)의 사이즈를 키우는 업샘플링(upsampling)을 수행하게 되는데, 이 과정에서 반대로 채널의 수는 다시 절반으로 점차 줄어들게 됩니다.

이후에 contracting path에서 해당 단계의 크롭된(cropped) 특징 맵과 연결(concatenation)한다는 이야기가 나오는데요. 이 부분은 구조도에서 회색 화살표를 확인하시면 됩니다. 위의 그림에서 빨간색 표시한 부분을 잘 보면, Contracting path의 (64 * 64 * 512)이 copy and crop으로 Extracting path의 (56 * 56 * 512)와 concatenate되어 (56 * 56 *  1024)가 됩니다. 64와 56이 사이즈가 맞지 않으니 64를 56으로 crop해주어야 했을 것이고, 이후 필터 512장과 512장이 concat으로 이어붙어 1024장이 된 것이죠.
논문에서는 언급되지 않은 용어이지만 흔히들 이 과정을 Skip-connection이라고 부르는데, element-wise summation이 아닌 concatenate를 사용하는 이유에 대해서는 아래 포스팅을 참고해보시면 도움이 되실 거예요.


https://at0z.tistory.com/164

 

Skip connection에서 add(summation) vs concatenation

U-Net architecture를 공부하다가 어떤 네트워크는 image size와 channel이 동일해서 add를 하기도 하고, 어떤 네트워크는 image size는 동일하지만 channel이 달라 (보통 2배 차이남) concatenation해 주기도 한다.

at0z.tistory.com


저는 여기서 앞서 살펴본 '미러링 타일 전략'과 개념 혼돈이 오면서 헷갈리는 시기가 있었습니다. upsampling과 downsampling 과정에서 콘볼루션을 계산할 때, 패딩을 사용하는 대신 미러링을 사용하는 것인가? 라는 오개념이 자리잡은 것인데요. 저와 비슷한 분을 위해 짚고 넘어가자면, 미러링은 이미지를 여러 개의 타일로 나누고 난 다음에, 그 각각의 타일 테두리에 미러링을 추가해 주어 경계 정보를 보완해주는 것으로, 그렇게 완성된 타일 이미지에 upsampling과 downsampling의 u-net 구조가 적용되는 것입니다. 즉, 

  1. 전체 이미지를 작은 타일로 나누어서 사용한다
  2. 이때 각각의 작은 타일의 가장자리에는 미러링이 적용된다
  3. 이렇게 미러링된 하나의 패치 이미지에 각각 u넷 구조가 적용된다

이렇게 순서를 이해하시면 되겠습니다 :)


 At the final layer a 1x1 convolution is used to map each 64- component feature vector to the desired number of classes. In total the network has 23 convolutional layers.

Skip-connection 이후에 두 번의 3x3 컨볼루션과 각각의 ReLU 활성화 함수를 적용하게 되는데요. 전체 네트워크는 총 23개의 컨볼루션 레이어로 구성되고, Expanding path의 마지막 레이어에서는 1x1 컨볼루션을 사용하여 결과값의 필터 수를 원하는 값으로 조정합니다. 

(노란색 부분) 마지막 1x1 콘볼루션

To allow a seamless tiling of the output segmentation map (see Figure 2), it is important to select the input tile size such that all 2x2 max-pooling operations are applied to a layer with an even x- and y-size.


seamless tiling을 위해서는 입력 타일 크기를 신중하게 선택할 필요가 있는데, 특히 모든 2x2 max-pooling 연산이 x와 y 크기가 짝수인 레이어에 적용되도록 해야 한다고 밝히고 있습니다. 이는 각 풀링 연산이 끝나는 레이어의 크기가 짝수여야 다음 연산에서도 크기가 정확히 맞아 떨어지기 때문인데요.

논문에서 표현하는 'seamless tiling'이란, U-Net이 큰 이미지를 처리할 때 '타일링 전략'을 사용해서 하나의 이미지를 여러 개의 작은 타일로 나누었던 것을, 마지막에 다시 이어 붙이는 작업이 매끄럽고 이쁘게 잘 되는 것을 의미한다고 보시면 돼요. 마치 원래부터 하나의 이미지였던 것처럼 이쁘게 잘 이어지기 위해서는 각 풀링 연산이 끝나는 레이어의 크기가 짝수가 되도록 잘 맞춰주라! 라는 겁니다. 



Training

The input images and their corresponding segmentation maps are used to train the network with the stochastic gradient descent implementation of Caffe [6].
  • 인풋 이미지와 그 이미지의 segmentation map 이미지가 네트워크 학습 데이터로 사용되었다고 합니다.
  • SGD(stochastic gradient descent)를 이용하여 학습을 진행했습니다.
Due to the unpadded convolutions, the output image is smaller than the input by a constant border width. To minimize the overhead and make maximum use of the GPU memory, we favor large input tiles over a large batch size and hence reduce the batch to a single image. Accordingly we use a high momentum (0.99) such that a large number of the previously seen training samples determine the update in the current optimization step.

 U-Net은 '타일링 전략'을 사용한다고 앞서 밝혔는데요. 콘볼루션을 계산할 때 패딩 없이 진행이 되기 때문에 이미지가 너무 작은 경우 경계값 정보가 많이 손실되어 학습이 제대로 이루어지지 않을 위험성을 가지고 있습니다. 따라서 여러 개의 작은 타일로 나누는 것보다 타일 크기를 크게 해서 전체 타일 수가 적은 것을 선호한다고 밝히고 있습니다.

그런데 큰 타일 크기를 사용하면 그만큼 SGD(Stochastic Gradient Descent) 배치 수는 줄어야 합니다. GPU 메모리가 한정되어 있기 때문이죠. 배치 수가 줄어든다면 그만큼 학습할 수 있는 데이터 다양성도 함께 줄어드는 단점이 있습니다. 다르게 말하자면 경사 하강법 과정에서 그래디언트 변동성이 커질 수 있게 되고, 이는 최적화 과정을 불안정하게 만들 수가 있는 것이죠.

따라서 논문에서는 이러한 문제점을 해결하기 위해 높은 모멘텀 momentum(0.99)을 사용한다고 밝히고 있습니다. 높은 모멘텀을 사용하면 이러한 변동성을 완화하고, 모델이 더 안정적으로 최적화를 할 수 있기 때문인데요. 즉 큰 타일 크기 사용으로 인한 불안정성을 높은 모멘텀이 보완해주는 효과가 있는 것입니다.

모멘텀은 Local Minimum에 빠지는 경우를 대처할 수 있다는 특징이 있으며, 높은 모멘텀은 현재 그래디언트 업데이트에 이전 단계들의 그래디언트 영향을 더 많이 반영합니다. 모멘텀에 대한 더 깊은 이해가 필요하시면, 아래 포스팅을 참고하시면 도움이 될 것입니다.


https://heytech.tistory.com/382

 

[Deep Learning] 최적화(Optimizer): (1) Momentum

본 포스팅에서는 딥러닝 최적화(optimizer) 기법 중 하나인 Momentum의 개념에 대해 알아봅니다. 먼저, Momentum 기법이 제안된 배경인 경사 하강법(Gradient Descent)의 한계점에 대해 다루고 알아보도록 하

heytech.tistory.com


Training에 사용한 U-Net의 Loss Function은 각 픽셀에 대한 에너지 함수(E)의 총합으로 구성되는데요. 에너지 함수는 다음과 같은 과정으로 계산됩니다.

𝑙은 특정 클래스의 레이블을 의미
특정 클래스 𝑙에 대한 로그 확률
loss function!

  1. 먼저 각 픽셀의 예측값에 소프트맥스(SoftMax) 함수 P를 적용해 예측값을 확률값으로 변환해 줍니다. 여기서 𝑙은 특정 클래스의 레이블을 의미합니다.
  2. 그런 다음, 특정 클래스 𝑙에 대한 로그 확률을 계산합니다. 예측 확률은 0과 1 사이의 값을 가지게 되므로, 예측 확률이 1에 가까워질수록 로그 값은 0에 가까워집니다. 반대로 예측 확률이 0에 가까울수록 로그 값은 매우 큰 음수가 되겠죠.
  3. 이제 각 픽셀의 실제 클래스 레이블과 예측된 확률 값을 비교하여 손실 값을 계산합니다. 이를 위해 교차 엔트로피 손실(Cross-Entropy Loss)을 사용합니다. 교차 엔트로피 손실 함수, 즉 '에너지 함수(E)'는 위와 같은데요. 특이하게 크로스 엔트로피에 픽셀 고유의 weight 𝑤(𝑥)을 곱함으로써 픽셀의 Loss값을 계산하는 것을 볼 수가 있습니다. 
  4. 𝑤(𝑥) 식은 아래 수식과 같이 나타냅니다. 경계선(border)라인에 더 강한 학습을 시키기 위해서 가우시안 분포(Gaussian distribution)을 가정하고 경계선 근처의 픽셀에 더 높은 가중치를 부여하여 학습이 집중되도록 했다고 합니다.

마지막으로 모든 픽의 에너지 함수(E)를 합산하여 전체 이미지에 대한 손실을 계산합니다.

마지막 부분에는 초기 가중치 설정의 중요성을 강조하며, 가중치를 Gaussian 분포로부터 표준편차가 루트 2/𝑁인 값으로 초기 설정하였다고 합니다. 예를 들어, 3x3 convolution과 이전 레이어에 64개의 feature 채널이 있는 경우, 𝑁은  9⋅64 = 576이 되는 거죠. 이 부분은 가볍게 읽고 지나가겠습니다.


3.1. Data Augmentation

Data augmentation is essential to teach the network the desired invariance and robustness properties, when only few training samples are available. In case of microscopical images we primarily need shift and rotation invariance as well as robustness to deformations and gray value variations. Especially random elastic deformations of the training samples seem to be the key concept to train a segmentation network with very few annotated images. We generate smooth deformations using random displacement vectors on a coarse 3 by 3 grid. The displacements are sampled from a Gaussian distribution with 10 pixels standard deviation. Per-pixel displacements are then computed using bicubic interpolation. Drop-out layers at the end of the contracting path perform further implicit data augmentation.

논문에서 언급한 data augmentation 방식은 총 4가지 입니다.

  • Shift
  • Rotation
  • Gray value
  • Elastic Deformation

이 중에서도 Elastic Deformation이 key 역할을 했다고 밝히고 있습니다. 이 부분은 앞에서 짚고 넘어간 바가 있으니 추가 설명은 생략하겠습니다.



Experiments

UNet의 우수한 성능으로 각종 대회에서 최고 결과를 도출했다는 파트로, 이부분은 자세한 정리를 생략하겠습니다.



Conclusion

드디어! 마지막 결론입니다.

The u-net architecture achieves very good performance on very different biomedical segmentation applications. Thanks to data augmentation with elastic deformations, it only needs very few annotated images and has a very reasonable training time of only 10 hours on a NVidia Titan GPU (6 GB). We provide the full Caffe[6]-based implementation and the trained networks. We are sure that the u-net architecture can be applied easily to many more tasks.

U-Net 아키텍처는 매우 다양한 생의학적 세그멘테이션 응용에서 매우 우수한 성능을 발휘하는데, Elastic deformation을 이용한 Data Augmentation형을 이용한 데이터 증강 덕분에 가능했다고 밝히고 있습니다. 유넷의 성능 확보에 정말 핵심적인 역할을 한 게 분명합니다.

마지막으로 U-Net 아키텍처는 더 많은 작업에 쉽게 적용될 수 있을 것이라고 자신감을 나타내며 논문이 마무리되는데, 실제로 U-Net은 Biomedical 분야뿐만 아니라 이미지 segmentation이 필요한 다양한 최신 모델에 두루 널리 쓰이며 사랑받고 있습니다.



본 논문 리뷰를 통해 다양한 최신 모델에서 자주 보이는 U-Net의 구조와 원리에 대해서 이해할 수 있어서 영광이었습니다 :-) 다음에도 좋은 논문 리뷰로 찾아뵙겠습니다. 감사합니다!


 

[참고] 본 포스팅은 수리링 본인이 Attention is all you need 논문을 처음부터 끝까지 직접 읽으며 분석하고 리뷰하여 작성했습니다. 불펌 절대 금지! 본문 내용에 잘못된 부분이 있다면 댓글 달아주세요.


Transformer

Transformer

이전 포스팅에서 다루었던 LSTM과 GRU와 같은 새로운 모델들은 기존 RNN 모델의 Long-term Dependency, Exploding Gradient 문제를 해결하기 위해 고안되었었죠. 하지만 안타깝게도 근본적인 문제가 완전히 해결된 것은 아니었다고 해요. 그 이유는 RNN이나 CNN이 가지는 연쇄적인 계산구조 때문이었는데요. 따라서 Recurrent 구조가 아닌 새로운 구조의 모델로 Sequence Data를 다루고자 하는 시도가 계속되었다고 합니다. 그리고 2015년, 인공지능 역사에 한 획을 긋는 Attention이라는 개념이 새롭게 도입됩니다.

You got me looking for attention...?

간단하게 말하면, 어텐션이란 모든 기억을 동등하게 기억하지 않고 연관성 있는 기억에 집중해서 기억하도록 구조화하는 기법을 말합니다. 어텐션이 정말 획기적인 메커니즘이긴 했지만, 초기에는 RNN, CNN 구조와 함께 사용되었기 때문에 여전히 시퀀스의 길이가 길어질수록 같은 문제가 발생하는 한계가 있었는데요. 2017년 구글은 Attention is all you need이라는 정말 멋있는,,, 제목으로 Transformer (트랜스포머) 모델을 제안합니다. 트랜스포머 모델은 RNN, CNN 구조를 완전히 배제하고 오롯이 어텐션 그 잡채! 에만 집중하는 아이디어로 기존의 문제점을 기냥,, 해결해버렸습니다.

본 포스팅에서는 트랜스포머 논문을 처음부터 끝까지 직접! 읽으며 분석한 내용을 정리하고 공유하고자 합니다 :)

논문 출처 :  https://arxiv.org/pdf/1706.03762


초록 Abstract

먼저 논문의 전체적인 아이디어와 내용을 파악할 수 있는 초록을 읽어 보도록 하겠습니다.

The dominant sequence transduction models are based on complex recurrent or convolutional neural networks that include an encoder and a decoder. The best performing models also connect the encoder and decoder through an attention mechanism. We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely.

기존 모델들의 인코더-디코더에는 복잡한 RNN 구조나 CNN 구조가 포함되어 있는데, 우리는 RNN/CNN구조를 완전히 배제하고 온전히 어텐션 메커니즘에만 기반한 새로운 네트워크 구조 '트랜스포머'를 제안한다! 라고 되어 있습니다. Attention is all you need이라는 논문 제목도 그렇고, Transformer이라는 모델 이름도 그렇고, 너무 멋있어서 소름이 돋습니다.

We show that the Transformer generalizes well to other tasks by applying it successfully to English constituency parsing both with large and limited training data.

트레이닝 데이터가 많든지 적든지간에 관계없이 트랜스포머가 영어 구문 분석에 굉장히 성공적으로 적용이 되었는데, 영어 구문 분석 뿐만 아니라 다른 과업에도 트랜스포머가 잘 일반화되어 적용될 수 있다는 것을 보여주겠다고 합니다.

이제 서론을 봅시다.



서론 Introduction

Recurrent neural networks, long short-term memory [13] and gated recurrent [7] neural networks in particular, have been firmly established as state of the art approaches in sequence modeling and transduction problems such as language modeling and machine translation [35, 2, 5]. 

RNN, LSTM, GRU같은 모델들은 언어모델이나 번역기같은 sequence modeling과 transduction problems에 특히 잘 적용되어 왔는데요.

they generate a sequence of hidden states ht, as a function of the previous hidden state ht−1 and the input for position t. This inherently sequential nature precludes parallelization within training examples, which becomes critical at longer sequence lengths, as memory constraints limit batching across examples. ...... The fundamental constraint of sequential computation, however, remains.

이런 RNN 구조의 모델들은 시퀀스가 길어질수록  어쩔 수 없이 맛탱이가 가버렸고..... 이런 문제를 해결하기 위해 factiorization trick이나 conditional computation같은 방법이 고안되기도 했지만, 결국 Recurrent 구조는 그대로였기때문에 근본적인 문제점은 여전히 해결되지 못한 상태였습니다.

Attention mechanisms have become an integral part of compelling sequence modeling and transduction models in various tasks, allowing modeling of dependencies without regard to their distance in the input or output sequences [2, 19]. In all but a few cases [27], however, such attention mechanisms are used in conjunction with a recurrent network.

이때 혜성처럼 등장한 어텐션 메커니즘은 서로간의 dependency를 계산하면서 입출력 시퀀스의 길이가 길어져도 학습이 잘 되도록 도왔는데요. 안타깝게도 문제점의 근원이 되는 Recurrent 구조가 여전히 그대로 사용이 되었기 때문에, 상황이 좀 나아지긴 했으나, 시퀀스 길이가 길어지면 또 같은 문제점이 발생했다고 합니다.... 이쯤 되니 '이거 그냥 Recurrent 구조 자체를 쓰지 말아야 겠다'는 생각을 슬슬 시작했던 것 같습니다.

In this work we propose the Transformer, a model architecture eschewing recurrence and instead relying entirely on an attention mechanism to draw global dependencies between input and output. ...

그래서 구글이 제안하는 트랜스포머는 입출력 사이의 Global dependency를 계산할 수 있도록 오롯이 어텐션 메커니즘에만 집중하고 Recurrence 구조를 완전히 배제한다고 합니다. 그렇게 했더니.. 학습도 잘되고... 성능이.. 쩐다고 하네요!



연구 배경 Background

연구 배경에서는 다양한 이전 모델들에 대해서 거론하고 있습니다.

언급된 주요 모델 중 4가지에 대해 찾아보았는데요.

  • [9] ConvS2S (2017): CNN 구조로 시퀀스 간의 종속성을 학습, 병렬 처리를 통해 효율성을 높인 모델
  • [16] Extended Neural GPU (2016): CNN 구조로 긴 시퀀스를 병렬로 처리하는 데 중점
  • [17] Neural GPU (2016): CNN 구조로 알고리즘을 학습하고 긴 시퀀스 데이터를 병렬로 처리할 수 있도록 설계
  • [18] ByteNet (2016): 깊고 확장 가능한 CNN 구조로 입력 시퀀스와 출력 시퀀스 간의 종속성을 효과적으로 학습

대부분 어텐션을 적용하지 않은 모델들이었습니다. 이 밖에도 대체로 RNN, CNN 구조를 사용한 기존 모델들에 대해서 언급을 하면서, 우리가 걔네보다 월등하다는 걸 증명하겠다!는 이야기를 하고 있습니다.

To the best of our knowledge, however, the Transformer is the first transduction model relying entirely on self-attention to compute representations of its input and output without using sequencealigned RNNs or convolution.

마지막으로 현재까지(발표 시점까지) 트랜스포머가 입출력 사이의 관계를 규명하기위해 self-attention 계산에만 온전히 집중하며, 연쇄적인 RNN이나 CNN 구조를 전혀 사용하지 않는 유일한! 모델이라고 거듭 강조하고 있습니다.

그럼 이제 본격적으로 모델 구조에 대해서 알아보겠습니다.



모델 구조 Model Architecture

관심있는 분이라면 한번쯤은 보셨을 트랜스포머의 구조도입니다. 처음 봤을때 저는 이게 도대체 뭘까 굉장히 심란했었는데요, 관련 자료를 열심히 찾아 읽고 논문도 계속 뒤져보고 하면서 한 번 이해하고 나니, 생각보다 심플한 구조구나- 느껴져 재밌었습니다.


3-1. 인코더 - 디코더

먼저 왼쪽 구조도는 인코더, 오른쪽 구조도는 디코더에 해당합니다. 구조 양옆에 N* 이라고 표시된 부분은 해당 작업을 N번 반복하겠다는 것을 의미합니다. 논문에서는 인코더와 디코더에서 모두 N = 6을 사용한다고 밝히고 있습니다.

  • 인코더 레이어는 2개의 하위 레이어로 구성되어 있습니다. 첫 번째 레이어는 멀티헤드 어텐션을 수행하며, 두 번째 레이어는 간단한 Feed-Forward 구조로 이루어져 있습니다. 두 하위 레이어는 ResNet에서 제안했던 잔차연결(Residual connection)으로 연결되는데, 각 하위 레이어의 아웃풋은 인풋과 더해진 뒤(add) 정규화(Norm)됩니다.
  • 이런 더하기 작업은 모든 레이어의 아웃풋이 같은 차원으로 전부 통일되기 때문에 가능합니다.
  • 모든 결과물의 차원을 논문에서는 512로 고정하고 있습니다. 차차 알아보겠지만 레이어 안팎으로 벡터끼리 서로 계속 더하는 일이 많기때문에, 모든 벡터의 shape과 차원을 동일하게 통일할 필요가 있습니다.

  • 디코더는 3개의 하위 레이어로 구성되어 있는데, 인코더와 마찬가지로 모든 하위 레이어는 Residual connection으로 연결됩니다.
  • 특히 첫 번째 하위 레이어에서 Masked 멀티헤드 어텐션을 사용하는 이유 cheating을 방지하기 위함입니다. 해당 내용은 뒤에서 더 자세히 살펴보도록 하겠습니다.

3-2. 어텐션

An attention function can be described as mapping a query and a set of key-value pairs to an output, where the query, keys, values, and output are all vectors. 

어텐션을 하나의 함수의 관점에서 생각해봅시다. 함수는 입력을 받아 결과를 출력하잖아요? 어텐션 함수는 인풋으로 3가지를 받아요. 바로 query, key, value인데요. 이렇게 3가지 인풋을 받아서 맵핑이란 걸 해가지고 결과물을 출력(output)합니다. 인풋으로 사용된 query(Q), key(K), value(V)와 출력물 output은 전부 벡터 형태로 이루어져 있습니다.

여기서부터 저는 이런 의문이 들었습니다. 그래서 Q는 뭐고, K는 뭐고, V는 뭔데? 의문을 해결하기 위해 잠깐 순서를 건너뛰고 3-2 대신 3-5를 먼저 보겠습니다.


3-5. 포지셔널 인코딩 (Positional Encoding)

포지셔널 인코딩을 먼저 이해하면 좋습니다. 간단하게 Input은 'I am hungry'이라는 영어 문장이고, Output은 '나는 배가 고프다'라는 한국어 문장이라고 생각해 봅시다. 논문 초록과 서론에서 누누이 봤지만, 트랜스포머는 이 문장들을 순차적으로 입력받지 않잖아요. 그렇다면 트랜스포머는 이 정보들을 도대체 무슨 수로 sequence data처럼 핸들링할 수 있는 걸까요? 그걸 가능하게 해주는 것이 바로 포지셔널 인코딩입니다.

Since our model contains no recurrence and no convolution, in order for the model to make use of the order of the sequence, we must inject some information about the relative or absolute position of the tokens in the sequence. To this end, we add "positional encodings" to the input embeddings at the bottoms of the encoder and decoder stacks. The positional encodings have the same dimension dmodel as the embeddings, so that the two can be summed. There are many choices of positional encodings, learned and fixed [9].

영어든 한국어든 단어로 된 문장을 받았다면, 기본적인 임베딩 작업을 해서 문장을 벡터화 해야겠죠. 포지셔널 인코딩은 쉽게 말해 이 임베딩 벡터값에 '위치(position) 정보'를 더해주는 일입니다. 위의 구조도에 잘 보면 Input Enbedding과 Positional Encoding 사이에 더하기(+) 기호가 있지요. 물론 두 벡터를 더해줘야 하므로 인풋 임베딩과 포지셔널 인코딩의 shape는 당연히 같도록 설계되었습니다. 어쨌든 이렇게 두 정보를 더함으로써 인코더와 디코더에 위치 정보가 더해진 벡터가 제공되고, 덕분에 마치 유사 sequence data처럼 인식을 할수 있게 되는 겁니다. 또, 이렇게 완성된 벡터에 각각 서로 다른 weight를 걸어서 어텐션 레이어의 서로 다른 입력 Q, K, V가 되는 것입니다. 어떤 weight를 걸어야 최적일지 찾는 건 결국 컴퓨터가 우리에게 해주는 일이 되겠죠!

포지셔널 인코딩을 하는 방법은 다양한데, 트랜스포머는 위의 식과 같은 삼각함수 형태의 식을 취하고 있습니다. 임베딩 벡터 내의 각 차원의 인덱스가 짝수인 경우에는 사인함수의 값을 사용하고, 홀수인 경우에는 코사인 함수의 값을 활용합니다. 이렇게 해야만 하는 건 아니고, 이렇게 했더니 계산과 학습이 용이하면서 성능도 잘 나오더라! 정도로 나름 간단하게 설명을 하고 있습니다. 

https://velog.io/@gibonki77/DLmathPE

포지셔널 인코딩에 대해서 좀더 직관적인 이해가 필요하시면, 위의 포스팅을 참고하시는 것을 추천합니다. 정리가 엄청 잘돼있습니다.


3-2. 어텐션

그럼 다시 Attention 설명파트로 돌아오겠습니다. 먼저 우리가 기존의 전체 구조도에서 살펴본 멀티헤드 어텐션이란 'Scaled Dot-Product Attention'이라는 걸 여러 번 실행하는 것을 의미하는데요. 굉장히 직관적인 작명을 해서... 크기를 줄이는 Scale과 Dot-product(행렬곱 내적)이 사용되는 싱글 어텐션이라고 생각을 하면 됩니다. 

  • 먼저 Q와 K를 행렬곱 계산합니다. Q와 K는 shape이 같은 벡터입니다. 따라서 벡터 내작 계산을 하기 위해 K를 Transpose 합니다. 따라서 두 행렬을 내적한 결과로 행렬은 정방형이 됩니다. (shape이 (a, b)라고 하면, 결과는 (a, a))
  • 그 다음 벡터 안의 숫자값을 작게 줄이는 Scaling 작업을 해줍니다. 논문에서는 벡터 안의 숫자값을 작게 해줌으로써 vanishing gradient 문제를 방지할 수 있게 된다고 밝히고 있습니다.
  • 만약 디코더라면 치팅 방지를 위해 Mask 작업을 추가합니다.
  • 이제 거기다가 Softmax 함수를 걸어주면, 벡터 안의 값이 확률화 되면서 weigt metrix로 간주할 수 있게 됩니다.
  • 그렇게 해서 나온 결과 벡터를 마지막으로 V와 행렬곱 계산합니다. 이 때, 결과 벡터의 shape은 기존 Q, K, V와 동일하게 유지됩니다.((a,a)•(a,b)=(a,b)) 여기까지가 바로 'Scaled Dot-Product Attention' 싱글 어텐션입니다.

  • 트랜스포머는 멀티 헤드 어텐션을 이용합니다. 위에서 살펴본 'Scaled Dot-Product Attention' 형태의 싱글헤드 어텐션을 h번 각각 수행하여 concat해서 이어 붙여준다는 건데요. 예를 들어서 논문에서는 512차원의 벡터를 다루고 있으므로, 이걸 64차원씩 8세트로 나누어서 각각 어텐션을 따로 수행하고 나서 Concat, 즉 이어 붙여서 다시 512차원으로 만들어 준다는 거예요.
Multi-head attention allows the model to jointly attend to information from different representation subspaces at different positions. With a single attention head, averaging inhibits this.
  • 멀티헤드 어텐션은 트랜스포머 모델의 성능을 높이는 핵심 요소 중 하나인데요. 그 이유를 살펴보겠습니다.
  • 먼저 Single head 어텐션은 하나의 문장에서 각 단어의 중요도를 계산한 후, 이 중요도를 이용해 모든 단어의 정보를 결합하여 하나의 벡터를 출력합니다. 이 과정에서 다양한 단어의 정보가 섞이게 되어 특정 단어의 중요한 세부 정보가 희석될 수 있는 가능성이 있고, 논문에서는 이것을 'averaging'이라고 부르고 있습니다. 중요한 패턴이나 특이점이 평균화되면서 사라지게 된다는 것이죠.
  • 반면 Multi head 어텐션은 여러 개의 어텐션 헤드를 사용하여 각 헤드가 특정 위치나 특징에 더 집중할 수 있게 돕고, 다양한 정보를 더 풍부하게 학습할 수 있게 됩니다. 즉 다양한 관점에서 입력 데이터를 이해하고 처리할 수 있게 되는 건데요. 이를 통해 모델은 입력 데이터의 여러 측면을 동시에 학습하고, 중요한 정보를 놓치지 않게 됩니다. 복잡하고 다양한 패턴을 효과적으로 인식하고 처리할 수 있을 뿐만 아니라 모델의 표현력도 향상시키는 거죠. 그래서 멀티헤드 어텐션이 트랜스포머 모델의 성능을 크게 향상시키는 핵심 요소라는 겁니다.

트랜스포머에서 어텐션은 크게 3곳에서 적용됩니다.

The encoder contains self-attention layers. In a self-attention layer all of the keys, values and queries come from the same place, in this case, the output of the previous layer in the encoder. 
  • 먼저 인코더는 self-attention layer를 사용하는데, 위에서 살펴봤듯이 인코더에 맨 처음 들어오는 입력값은 임베딩과 포지셔널 인코딩된 값이 더해진 벡터입니다. 이 벡터에 각각 서로 다른 weight가 걸려서 Q, K, V라는 입력값으로 들어오게 되지만, 실제로는 전부 같은 값을 가진 하나의 벡터로부터 근원합니다. 논문에서도 'keys, values and queries come from the same place'라고 밝히고 있죠. 
Similarly, self-attention layers in the decoder allow each position in the decoder to attend to all positions in the decoder up to and including that position. We need to prevent leftward information flow in the decoder to preserve the auto-regressive property. We implement this inside of scaled dot-product attention by masking out (setting to −∞) all values in the input of the softmax which correspond to illegal connections. See Figure 2.
  • 디코더에서도 똑같이 self-attention layer를 사용합니다. output에 해당하는 문장을 임베딩한 값과 포지셔널 임베딩한 값을 더한 벡터에 서로 다른 weight가 걸려 Q, K, V가 되고, Masked 작업을 추가한 멀티헤드 어텐션을 수행하죠. 그림의 2번에 해당합니다.
  • 우리가 마스크를 쓰면 마스크 아래 얼굴이 가려져서 보이지 않는 것처럼, 마스킹 작업은 어떤 값이 보이지 않게 가려버리는 것을 말합니다. 여기서는 벡터에서 마스킹할 부분을 마이너스 무한대로 발산하는 -inf값으로 바꿔버리는데, 그러면 이 -inf값이 다음 단계의 softmax 함수를 지나면서 0값을 가지게 됩니다.

참조

  • 기존의 RNN 모델들은 시퀀스가 순차적으로 입력되기 때문에, 앞쪽부터 순차적으로 업데이트 되어온 hidden state를 다음 시퀀스에 제공하면서 sequence 예측을 합니다. 하지만 트랜스포머 모델은 데이터가 순차적으로 제공되지 않고 한번에 제공됩니다. 따라서 현재 시점보다 뒤에 올 시퀀스의 정보까지 알 수 있게, 즉 'cheating'을 할 수 있게 되는 거죠.
  • 논문은 디코더에서 leftward information flow를 방지해야 한다고 언급합니다. 마스킹은 참조용 그림에서 보이는 것처럼 벡터의 대각선 왼쪽 아래 부분을 아예 0값으로 만들어버리고, 그래서 현재 시점보다 뒤에 있는 시퀀스를 참조하지 않도록 돕습니다. 쉽게 말하자면 현재 토큰보다 나중에 해당하는 토큰 값이 계산에 포함되는 부분을 모두 가려버린 것입니다.
In "encoder-decoder attention" layers, the queries come from the previous decoder layer, and the memory keys and values come from the output of the encoder. This allows every position in the decoder to attend over all positions in the input sequence. This mimics the typical encoder-decoder attention mechanisms in sequence-to-sequence models such as [38, 2, 9].
  • 마지막으로 그림 3번에 해당하는 어텐션입니다. 이 어텐션에도 Q, K, V가 필요하겠죠.
  • 먼저 디코더의 첫 번째 하위레이어를 지난 결과물이 Q가 됩니다. (queries come from the previous decoder layer) 그리고 인코더의 최종 결과물에 서로 다른 weight가 걸려서 각각 K, V,가 되고요. 이 결합 메커니즘을 통해 디코더의 각 위치가 입력 시퀀스의 모든 위치를 참조할 수 있게 됩니다.
  • 즉, 디코더의 특정 위치에서 다음 토큰을 예측할 때, 입력 시퀀스 전체의 정보를 활용할 수 있게 되는 것입니다. 이는 시퀀스-투-시퀀스(sequence-to-sequence) 모델에서 일반적으로 사용되는 인코더-디코더 어텐션 메커니즘과 유사한데요. 덕분에 디코더가 보다 정확하고 문맥에 맞는 출력을 생성할 수 있겠습니다.

3-3. Feed-Forward 네트워크

이전에 살펴본 바와 같이 인코더와 디코더는 각각 2개, 3개의 하위 레이어로 구성되어 있는데, 마지막 하위 레이어는 feed-forward 네트워크로 구성되어 있으며, 활성함수로는 ReLU가 사용됩니다. 레이어마다 사용된 웨이트는 모두 다르다고 밝히고 있습니다.


3-4. 임베딩 & 소프트맥스

  • input과 output 토큰을 벡터화하기 위해 learned embedding을 사용하는데, 이 임베딩에 필요한 weight matrix는 같은 것을 쓴다고 밝히고 있습니다. 또한 디코더 결과물을 선형 변환하는 과정에서도 같은 weight matrix를 쓴다고 합니다. 즉, 아래 그림에 빨간색으로 표시한 3곳에서 모두 같은 가중치 행렬을 사용합니다.

  • 디코더의 마지막 feed-forward를 지난 결과물은 linear transformation과 softmax를 차례대로 거쳐 다음 토큰의 probabilities를 예측하는 아웃풋이 됩니다.

3-5. 포지셔널 인코딩

위에서 미리 봤으므로 생략



Why self attention

4장에서는 Self-attention의 장점을 recurrent, convolution 구조와 비교하여 크게 3가지 측면에서 비교합니다.

One is the total computational complexity per layer. Another is the amount of computation that can be parallelized, as measured by the minimum number of sequential operations required. The third is the path length between long-range dependencies in the network.
  1. 레이어마다 발생하는 계산의 복잡도가 상대적으로 낮아 효율적입니다.
  2. 병렬화가 용이해 동시에 많은 계산을 수행할 수 있습니다.
  3. 입력과 출력 위치 간의 경로 길이가 짧아 장기 의존성을 더 쉽게 학습할 수 있습니다.
Learning long-range dependencies is a key challenge in many sequence transduction tasks. One key factor affecting the ability to learn such dependencies is the length of the paths forward and backward signals have to traverse in the network. The shorter these paths between any combination of positions in the input and output sequences, the easier it is to learn long-range dependencies [12]. Hence we also compare the maximum path length between any two input and output positions in networks composed of the different layer types.

n이 시퀀스의 길이, d가 차원의 크기, k가 convolution에서 커널의 사이즈, r이 길이가 제한된 self-attention에서 이웃의 사이즈를 의미할 때 계산의 복잡도를 비교한 표입니다. Self-attention 레이어는 모든 위치를 일정한 수의 순차적 연산으로 병렬 연결할 수 있기 때문에 Recurrent, Convolution 구조보다 훨씬 적은 비용으로 계산을 처리할 수 있습니다.

특히 길이가 제한된 self-attention이란, 입력 시퀀스의 각 위치가 특정 범위 내의 이웃 위치들만 제한적으로 참조하여 어텐션을 계산하는 것을 의미합니다. 예를 들어 시퀀스 길이 n = 100이고 길이 제한 범위 r = 5인 경우, 일반적인 self-attention에서는 각 위치가 99개의 다른 위치와 전부 상호 작용하지만, 제한된 self-attention에서는 각 위치가 최대 10개의 이웃 위치(왼쪽 5개, 오른쪽 5개)와만 상호 작용합니다. 딱 봐도 계산량이 확 줄어서 효율적인 장점이 있겠죠. 특히 입력 시퀀스의 길이가 매우 길고 계산 자원이 제한된 상황에서 유리할 수 있다고 합니다.

추가적으로 self-attention을 사용한 모델은 해석이 조금 더 용이한 장점이 있다고 합니다.



학습과 결과 Training / Results

학습과 결과 부분은 가볍게 읽어만 보고, 자세한 리뷰는 생략하겠습니다.

영어->독어 번역, 영어->불어 번역 문제에서 다른 기존의 SOTA 모델들과 비교하여 월등한 최고 성능을 보여주었다는 것이 핵심입니다.



결론 Conclusion

결론에서는 앞으로 트랜스포머가 언어 번역뿐만 아니라 이미지, 오디오, 비디오와 같은 대용량 입출력 처리를 할 수 있도록 어텐션 메커니즘을 더 연구하고 발전시켜 나가겠다고 밝히며, 트랜스포머에 대한 자부심과 앞으로에 대한 기대감으로 마무리를 하고 있습니다.

2017년에 발표되었던 논문인 만큼 2024년인 현재까지 트랜스포머는 많은 발전에 발전을 거듭하였고, 최신 생성형 모델의 근간이 되고 있습니다. 어느 분야든 마찬가지겠지만 특히 딥러닝 분야에서 기존의 문제점을 해결하기 위한 새로운 아이디어를 떠올리고 그를 구현해내는 이런 멋진 작업물들을 볼 때면 정말 경이롭습니다. 이번 논문 리뷰를 통해서 트랜스포머의 기본 개념과 원리, 구조에 대해 파악할 수 있어서 영광이었습니다.

이로서 Attention is all you need, 트랜스포머 논문 리뷰를 마치겠습니다. 감사합니다 :)

포스팅 참고 문헌

1. Do it! 딥러닝 교과서 (윤성진 지음)
2. MIT 6.S191: Recurrent Neural Networks, Transformers, and Attention (바로가기)
3. https://colah.github.io/posts/2015-08-Understanding-LSTMs/
4. SK플래닛 T아카데미 강사님 수업자료

RNN

[1] 시퀀스 데이터

흔히 '시계열 데이터', '순차 데이터'라고도 부르는 Sequence data에는 시공간적 순서 관계가 포함되어 있습니다. 예를 들어서 지금 저는 커피 한 모금을 마시고 컵을 내려놓은 뒤 타자를 치고 있고, 창 밖에는 차와 사람들이 움직이고 있으며, 옆에 있는 친구는 음악을 듣고 있어요. 이러한 일들이 일어날 때 자연스레 시간이 흐르고 공간상의 움직임도 나타나게 되지요. 이런 시퀀스 데이터는 문맥(context)을 가지기 때문에 어느 한 순간의 데이터만 살펴봐서는 그 특성을 이해하기가 어렵습니다.

예를 들어 우리가 위의 그림만 보고 공이 다음에 어느 방향으로 움직일지 논리적으로 예측하기는 무척 어렵습니다. 공이 옆에서 굴러왔을지, 밑에서 떨어졌을지, 위에서 던졌을지, 시공간 정보에 대한 어떤 문맥도 주어지지 않았기 때문입니다.

반면에 이렇게 공이 움직여 온 과정 대한 정보가 주어진다면, 우리는 아주 자연스럽게 공이 오른쪽 방향으로 움직일 것이라고 합리적으로 예측할 수 있게 됩니다. 이렇게 데이터의 순차 구조를 인식하고 처리할 수 있는 인공 신경망이 바로 RNN : 순방향 신경망(Recurrent Neural Network)입니다. RNN은 아래와 같은 구조로 이루어져 있습니다.


[2] RNN 기본 구조와 원리

RNN의 구조

첫 번째 입력 x1에 weight X를 곱한 값 => 히든 레이어 h1
{(두 번째 입력 x2에 weight X를 곱한 값) + (전 단계 h1에 weight H를 곱하고 bias를 더한 값)}에 하이퍼탄젠트(tanh) 액티베이션 펑션을 걸어주면 => 히든 레이어 h2
{(세 번째 입력 x3에 weight X를 곱한 값) + (전 단계 h2에 weight H를 곱하고 bias를 더한 값)}에 하이퍼탄젠트(tanh) 액티베이션 펑션을 걸어주면 => 히든 레이어 h3
....
{(t 번째 입력 xt에 weight X를 곱한 값) + (전 단계 h(t-1)에 weight H를 곱하고 bias를 더한 값)} 하이퍼탄젠트(tanh) 액티베이션 펑션을 걸어주면 => 히든 레이어 ht

이를 수식으로 나타내면 아래와 같아집니다.


[3] RNN의 weight

RNN에서 사용되는 가중치 Wx, Wh, Wy는 모든 시간 단계에서 공유되는데, 그 이유는 다음과 같습니다.

  1. 순차 구조를 포착할 수 있기 때문입니다.
    :  RNN의 주된 목적 중 하나는 시퀀스 데이터의 순차적 특성을 학습하는 것입니다. 이를 위해서는 각 시간 단계에서 입력과 은닉 상태를 동일한 방식으로 처리해야 합니다. Wx(입력 가중치), Wh(은닉 상태 가중치), Wy(출력 가중치)를 시간 단계마다 공유함으로써 네트워크는 시간 순서에 따라 일관된 방식으로 데이터를 처리할 수 있습니다.
  2. 가변 길이 데이터 처리가 용이하기 때문입니다.
    :  RNN은 고정된 길이의 입력만 처리하는 것이 아니라 가변 길이의 시퀀스 데이터도 처리할 수 있습니다. 모든 시간 단계에서 동일한 가중치를 사용하면, 시퀀스의 길이가 어떻게 되든 간에 동일한 모델 구조로 일관된 학습과 예측이 가능해집니다. 따라서 입력 시퀀스의 길이가 다르더라도 동일한 가중치 매트릭스를 사용하여 각 단계의 데이터를 처리할 수 있습니다.
  3. 파라미터 수가 절약되고, 정규화 효과가 생깁니다.
    :  각 시간 단계마다 다른 가중치를 사용한다면 파라미터 수가 급격히 증가하여 학습이 비효율적이 될 수 있습니다. 모든 시간 단계에서 가중치를 공유함으로써 파라미터 수를 크게 줄일 수 있습니다. 이는 모델의 복잡도를 낮추고, 과적합(overfitting)을 방지하는 데 도움이 됩니다. 또한, 적은 수의 파라미터를 통해 더 나은 일반화 성능을 얻을 수 있습니다. 이는 정규화 효과와 유사한 역할을 하여 모델이 더 일반화된 패턴을 학습하는 데 기여합니다.

[4] RNN Loss Function & Backpropagation

RNN의 전체 Loss Function은 모든 단계의 Loss Function을 더해서 정의합니다. 각 단계의 Loss Function은 회귀 문제라면 주로 MSE, 분류 문제라면 주로 Cross Entropy로 정의하게 됩니다.

RNN의 Backpropagation은 BPTT(Back Propagation Through Time), 시간펼침 역전파 알고리즘이라고도 불리는데요. 말그대로 시간 순서대로 네트워크를 펼쳐서 역전파를 수행하는 알고리즘입니다. BPTT는 아래와 같은 단계로 이루어집니다.

BPTT

1. 모든 시간 단계(t)마다 오차를 계산하고, 이를 이전 시간 단계로 전파합니다.
2. 모든 단계에서 가중치 및 바이어스에 대한 기울기를 구합니다.
3. 구한 기울기를 이용하여 각 가중치와 바이어스를 업데이트합니다.
-> 이를 통해 모델이 점차 학습하여 더 나은 예측을 할 수 있게 됩니다.

 


[5] RNN 한계점

예를 들어서, 한국에서 태어난 수리링은 인공지능 공부를 열심히 하고 있는데, 수리링은 영어도 잘 하지만 사실 한국말을 제일 잘한다. 라는 문장이 있다고 해 봅시다. RNN 모델에게 빈칸에 들어갈 말이 무엇인지 추측하도록 시킨다고 할 때, 모델이 정답을 잘 맞추기 위해서는 문장의 가장 처음에 있는 '한국'이라는 단어의 정보를 제대로 활용할 수 있어야 합니다. 만약 '한국'이라는 단어의 중요도가 점점 소실된다면 뜬금없이 빈칸에 '스페인어'나 '영어' 등의 오답이 들어가는 경우가 발생하겠죠. 이것이 바로 RNN의 근본적인 문제점입니다. (아, 참고로 빈 칸에 들어갈 정답은 '한국말' 입니다. ^^)

Long-term Dependency

RNN은 오차가 멀리 전파될수록(시간이 지날수록) 기울기가 점차 작아지는(0으로 수렴하는) 'Vanishing Gradient'로 인해 입력 데이터의 영향이 점점 사라지는 'Long-term Dependency'라는 명확한 한계를 가지고 있습니다. 또 가중치가 반복적으로 곱해지는 과정에서 기울기가 폭발적으로 발산하는 불상사로 인해 정상적인 학습이 불가능해지는 Exploding Gradient 문제도 쉽게 발생하곤 합니다. 이러한 문제점을 해결하기 위해 gradient clipping이나 오차를 몇 단계까지만 전파시키는 생략된-BPTT(truncated BPTT) 등을 사용하기도 했지만, 시간이 지나면서 사람들은 LSTM 및 GRU를 많이 사용하게 되었습니다.



LSTM

[1] 핵심

LSTM은 기본 RNN 구조를 변경해서 만든 모델입니다. LSTM의 핵심 아이디어는 바로 Gate & Cell State인데요.

이전의 RNN은 모든 정보를 연쇄적으로 곱해주는 방식으로 계산했기 때문에 역전파 과정에서 Vanishing/Exploding Gradient 같은 문제점이 발생했습니다. 이를 방지하고자 LSTM은 게이트가 있는 Gated cell을 사용하여 선택적으로 정보를 조절해 학습할 수 있도록 합니다. 이는 장기 기억(Long Term Memory)과 단기 기억(Short Term Memory)이라는 두 가지 메커니즘(LSTM)으로 구현되는 것으로 볼 수 있어 LSTM이라는 이름이 붙게 되었습니다.

기본 RNN은 위처럼 매우 심플한 구조로 이루어져 있습니다. (Xt * Wx + Ht-1 * Wh)에 활성함수로 하이퍼탄젠트(tanh)를 곧바로 걸어주게 되고, 이것이 순차 반복적으로 곱해지면서 Vanishing/Exloding Gradient 문제가 발생하게 되는 건데요.

LSTM의 경우, 내부 구조가 위의 사진처럼 세부 단계로 나뉘어져 있습니다. 처음 보면 무척 복잡해 보이지만, 한 번 이해하고 나면 꽤나 심플한 아이디어로 구성되어 있음을 알 수 있게 됩니다.


[2] Gate

Cell State : C

LSTM을 이끄는 핵심은 Cell state, 그림 상단에 보이는 Ct 라인입니다. Cell state는 마치 컨베이어 벨트와 역할을 하면서 레이어 사이를 지나가는데요. LSTM은 이 Cell state에 새로운 정보를 선택적으로 추가하기도 하고, 기존의 정보를 삭제하기도 합니다. 근데 도대체 어떻게 이런 일이 가능한 걸까요? 만약 이런 의문이 드신다면, 당신은 정상인입니다. 바로 게이트(Gate)가 있기 때문에 가능합니다.

그럼 정확히 Gate가 무엇이냐?라고 하신다면, 참고문헌 [3]은 They are composed out of a sigmoid neural net layer and a pointwise multiplication operation이라고 기술하고 있습니다.

gate
sigmoid neural net layer
: 시그모이드 함수는 0부터 1 사이의 값을 출력하기 때문에, 비율의 관점에서 바라볼 수 있습니다. 시그모이드 출력값이 0이라면 정보를 삭제하는 것과 같을 것이고, 시그모이드 출력값이 1이라면 정보를 100% 통과시켜야 함을 의미하겠죠.

pointwise multiplication operation
: 
시그모이드 함수의 출력값, 즉 '비율'을 곱한 다음, 이렇게 중요도가 결정된 정보가 게이트를 통과하게 되는 것입니다.

즉, 그냥 단순히 정보를 마구잡이로 곱하는 방식이 아니라, 각 정보의 중요도를 반영한 값을 처리할 수 있는 매커니즘을 구현하겠다는 겁니다. 만약 쓸데 없는 정보가 들어온다면 입뺀을 먹이고 통과를 시켜주지 않을 것이고, 필요한 정보가 들어온다면 중요도에 따라 필터링을해서 통과시켜 주겠다는 것이죠. LSTM은 세 가지 주요 게이트를 사용하여 정보를 조절합니다.


[3] Gate 종류

  • Forget Gate : 과거 정보를 얼마나 유지할 것인지
  • Input Gate : 현 시점의 입력 정보를 Cell State에 얼만큼 반영할 것인지
  • Output Gate : 현 시점에서 정보를 얼만큼 출력해서 다음 시점에 제공할지

3-1. Forget Gate 

현 시점의 입력 정보 Xt와 지난 레이어h(t-1)를 종합한 정보에 시그모이드를 걸어 비율값을 만들어 준 다음, 이전의 Cell State와 곱해줍니다. 시그모이드 값이 작을수록 이전의 정보는 많이 소실되고, 시그모이드 값이 클수록 이전의 정보는 많이 유지되겠죠. 즉, forget gate는 과거 정보를 얼마나 유지할 것인지 판단하는 게이트입니다. 

3-2. Input Gate 

인풋 게이트는 다음과 같은 구조로 흘러갑니다.

현 시점의 입력 정보 Xt와 지난 레이어h(t-1)를 종합한 정보에 

  1. 하나는 시그모이드를 걸어 0과 1사이의 비율값으로 만들어줍니다.
  2. 또 하나는 RNN처럼 활성함수로 하이퍼탄젠트(tanh)를 걸어줍니다.
  3. 두 개의 값을 곱해줍니다.
  4. Forget Gate를 지난 Cell State에 그 값을 더해줍니다.

즉, 인풋 게이트는 현 시점의 입력 정보를 Cell State에 얼만큼 반영할 것인지 판단하는 게이트입니다. 인풋 게이트를 통과한 정보가 더해지고 나면, 현 시점 LSTM에서의 Cell State가 완성됩니다. 이 Cell State는 다음 시점의 LSTM으로 전달되어, 또 다시 Forget Gate를 통과한 정보와 곱해지게 되겠죠. 

3-3. Output Gate 

아웃풋 게이트는 다음과 같은 구조로 흘러갑니다.

  1. 현 시점의 입력 정보 Xt와 지난 레이어h(t-1)를 종합한 정보에 시그모이드를 걸어 0-1사이의 비율값으로 만들어 줍니다.
  2. Cell State 값에 활성함수로 하이퍼탄젠트(tanh)를 걸어줍니다.
  3. 두 값을 곱해서 다음 시점의 h값으로 제공합니다.

즉, 현 시점의 output payer를 통과한 값은 다음 시점의 입력 정보와 만나는 h값이 되는 것입니다. 따라서 Output layer는 현 시점에서 정보를 얼만큼 출력해서 다음 시점에 제공할지 판단하는 레이어라고 볼 수 있습니다. 참고문헌 [3]에서는 Output gate를 지난 output을 다음과 같이 표현하고 있습니다. This output will be based on our cell state, but will be a filtered version. 


[4] LSTM의 장점

LSTM에서 셀들의 상태를 연결하는 경로를 다시 한 번 살펴보겠습니다.

LSTM은 셀들의 상태를 연결하는 경로에서 행렬곱을 생략하고, 대신 Cell State 개념을 도입했습니다. 기억 정보들은 Gate를 지나면서 선택적으로 형성, 지속, 망각되는 과정을 거치고, 그 과정이 담긴 Cell State에서 단기 기억과 장기 기억이 상호작용하며 유지됩니다. 이러한 획기적인 아이디어가 기존 RNN에서 Vanishing Gradient를 유발하는 요인이었던 반복적인 행렬 곱 연산을 대체하게 되면서 중요한 정보가 시퀀스의 초반부에서 후반부까지 소멸되지 않고 유지될 수 있었고, Long-term Dependency 문제를 해결할 수 있었습니다.



GRU

GRU(Gated Recurrent Unit)는 LSTM(Long Short-Term Memory)의 단순화된 버전으로, 구조를 단순화해서 계산 효율성을 높인 모델입니다.

출처 : http://dprogrammer.org/rnn-lstm-gru

  • Cell state와 Hidden state의 통합
    • 가장 도드라지는 차이점은 Cell State가 사라졌다는 점입니다.
    • LSTM은 Cell state(상단 C)와 Hidden state(하단 h)를 별도로 유지합니다. Cell state는 장기 기억을 저장하고, Hidden state는 단기 기억과 출력을 담당합니다.
    • 반면에 GRU는 둘을 통합하여 단일 Hidden state로 처리합니다. 이를 통해 모델이 더 간단해지고, 계산 효율성이 향상됩니다.
  • 게이트의 변화
    • LSTM은 세 가지 게이트를 가지고 있습니다. (Forget/Input/Output Gate)
    • GRU는 두 가지 게이트를 가지고 있습니다. (Reset/Update Gate)
    • 리셋 게이트(r)는 이전 은닉 상태를 얼마나 초기화할지를 결정하고, 업데이트 게이트(z)는 새로운 정보와 기존 정보를 어떻게 결합할지를 결정합니다.

GRU는 이러한 구조적 단순화를 통해 LSTM의 복잡성을 줄이면서도 유사한 성능을 유지할 수 있도록 설계되었습니다. 만약 컴퓨팅 리소스가 제한된 환경에 있다면, LSTM보다 계산량이 적으면서도 비슷한 성능을 가진 GRU를 사용하는 것이 효율적이겠습니다.


이것으로 RNN / LSTM / GRU 포스팅을 마치도록 하겠습니다. 다음 포스팅에서는 Transformer 논문 리뷰를 해보겠습니다 :) 감사합니다!

VGGNet

VGGNet(Visual Geometry Group Network)은 2014년 1000개의 이미지 클래스를 분류하는 이미지넷 이미지 인식 대회에서 준우승을 한 모델입니다. 옥스포드 대학의 연구팀 VGG에 의해 개발되었다고 하는데요. 이번 포스팅에서는 VGGNet 논문에서 중요한 내용을 살펴보고 직접 코드화하는 과정을 정리해 작성해 보겠습니다.


<VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION>
논문 링크 : https://arxiv.org/pdf/1409.1556

VGG의 핵심 장점은 다음과 같습니다.

1. convolution의 depth를 깊게 구성
2. 연산량을 획기적으로 줄여 좋은 성능 확보
3. 깊은 레이어 구조를 통해 Activation function을 여러 번 집어넣어서 비선형성을 더 많이 확보

왜 이런 장점이 있는지 논문 내용과 함께 살펴봅시다.


2.1. Architecture

VGG 이전의 기존 CNN 모델들은 주로 Convolution 레이어와 Pooling 레이어를 번갈아 연속적으로 사용하는 것이 일반적이었습니다. VGGNet은 이러한 기존의 틀에서 벗어나 Convolution을 2번 또는 3번 연속해서 쌓은 뒤 Pooling 레이어를 배치하는 새로운 구조를 제시합니다. 

VGG16 구조

위 사진은 대표적인 VGG16모델의 레이어 설계도입니다. 책이나 인터넷에서 쉽게 찾아볼 수 있는 그림인데요. 보이는 것처럼 파란색의 Convolution 레이어가 연속해서 2-3번 쌓이고, 이어서 빨간색 max pooling layer가 배치된 것을 볼 수 있습니다. 논문 2.1 Architecture에 관한 부분을 함께 살펴보도록 하겠습니다.

the input to our ConvNets is a fixed-size 224 × 224 RGB image
  • 입력 이미지의 shape은 (224, 224, 3)인 것을 알 수 있습니다.
The image is passed through a stack of convolutional (conv.) layers, where we use filters with a very small receptive field: 3 × 3 (which is the smallest size to capture the notion of left/right, up/down, center). (중략) The convolution stride is fixed to 1 pixel
  • 콘볼루션 레이어를 여러겹 쌓고 필터는 (3 x 3) 작은 사이즈로 고정하여 사용한다고 명시하고 있습니다. VGG 이전의 CNN 모델들은 전통적으로 필터 사이즈가 7, 9, 11 정도로 큰 것이 일반적이었다고 합니다. 그래서 사이즈가 3인 필터는 'very small'이라고 표현되는 것으로 보입니다.
  • 콘볼루션 레이어의 stride는 1픽셀로 고정됩니다.
Max-pooling is performed over a 2 × 2 pixel window, with stride 2.
  • 사용된 Maxpooling의 필터 사이즈와 stride 모두 (2, 2)인 것을 알 수 있습니다.
A stack of convolutional layers is followed by three Fully-Connected (FC) layers: the first two have 4096 channels each, the third performs 1000- way ILSVRC classification and thus contains 1000 channels (one for each class). The final layer is the soft-max layer.
  • CNN 레이어 이후에는 순서대로 4096 - 4096 - 1000개의 필터가 사용된 FC(Fully-Connected) 레이어가 3번 배치되었습니다.
  • 마지막 필터가 1000개인 이유는 VGG모델이 총 1000개의 이미지 클래스를 분류하기 때문입니다.
  • 마지막 레이어의 activation function로는 소프트맥스가 사용된 것을 알 수 있습니다.

2.2. Configuration

다음으로 여러 가지 버전의 VGG모델을 표현된 논문의 Table 1, 2를 함께 살펴보겠습니다.

The width of conv. layers (the number of channels) is rather small, starting from 64 in the first layer and then increasing by a factor of 2 after each max-pooling layer, until it reaches 512. 
  • 콘볼루션 레이어의 필터의 수는 64개로 작게 시작해서 마지막으로 512가 될때까지 2의 배수로 늘려 가겠다고 말하고 있습니다.
The convolutional layer parameters are denoted as “conv[receptive field size]-[number of channels]”. The ReLU activation function is not shown for brevity.
※ [2-1] All hidden layers are equipped with the rectification (ReLU (Krizhevsky et al., 2012)) non-linearity.
  • Table 1에서 표현된 레이어 수식(conv0-000형식)을 읽을 수 있어야 합니다. 예를 들어 conv3-256의 경우 필터 사이즈 (3x3)인 콘볼루션 레이어가 256장 쌓인 것으로 해석할 수 있습니다.
  • 간결한 표현을 위해 아래 배치도(Table 1)에서 활성함수 ReLU는 생략해서 표현되었으나, 논문 2.1 Architecture에 언급되었듯이 모든 히든레이어에는 ReLU가 적용되었습니다.

Table 1

  • 우리가 맨 위에서 살펴본 빨강파랑 레이어 구조도는 VGG16모델입니다. Convolution 레이어와 Dense 레이어를 합해 16개의 레이어가 사용되었습니다. 여기에서 레이어가 3개 늘어나면 VGG19(E모델)이 됩니다.
  • VGG16에 해당하는 모델 C와 D의 차이점은 3겹의 콘볼루션 레이어에서 마지막에 필터 사이즈 1짜리 레이어가 쓰였느냐 3짜리가 쓰였느냐 정도로 구분이 되네요.

VGG16 구조
Table 2

In Table 2 we report the number of parameters for each configuration. In spite of a large depth, the number of weights in our nets is not greater than the number of weights in a more shallow net with larger conv. layer widths and receptive fields (144M weights in (Sermanet et al., 2014)).
  • 테이블 2에는 백만 단위의 파라미터 수가 표현이 되어 있는데요. VGG16에 해당하는 모델 C-D의 경우 1억 3천만 개 정도의 파라미터를 찾아야 하네요.
  • 그렇다면 사이즈 3의 작은 필터를 사용한 3개의 콘볼루션 레이어를 연속적으로 사용해서 얻은 이점이 무엇일까요?

Table 2 아래에 중요한 내용이 담겨있습니다.

So what have we gained by using, for instance, a stack of three 3×3 conv. layers instead of a single 7×7 layer? First, we incorporate three non-linear rectification layers instead of a single one, which makes the decision function more discriminative. Second, we decrease the number of parameters
예: (10, 10) 이미지에 (7, 7) 필터 1번 적용
* 콘볼루션 후 최종 사이즈 : (4, 4)
* 찾아야 할 파라미터  : (7, 7) -> 49개
예 : (10, 10) 이미지에 (3, 3) 필터 3번 적용
* 콘볼루션 후 최종 사이즈 : (4, 4)
* 찾아야 할 파라미터 : (3, 3) 3개 -> 9 * 3 -> 27개
  • 최종적으로 얻는 이미지의 사이즈는 (4, 4)로 동일한데 찾아야 할 파라미터의 수는 49개에서 27개로 줄어드는 것을 확인할 수 있습니다.
  • 콘볼루션 레이어를 연속해서 쌓는 VGG 구조를 통해 찾아야 할 파라미터 수를 획기적으로 줄여 연산량을 줄이고 성능을 개선할 수 있다는 것입니다.
  • 또한 한 번의 콘볼루션 레이어만 추가했을 때보다 세 번의 연속적인 콘볼루션 레이어를 추가하게 되면서 비선형성을 더욱 강조해 모델의 유연성을 확보할 수 있게 되었고 이 역시 성능 개선으로 이어지게 되었습니다.

코드화

VGG16 모델 D - 코드로 구현해보기

from tensorflow.keras.layers import Conv2D, MaxPool2D, Flatten, Dense, Dropout

필요한 라이브러리를 호출합니다.

# 논문 VGG16 D모델 그대로 설계해보기
    
model_D_VGG_origin = tf.keras.Sequential([
    Conv2D(64, 3, input_shape = (224, 224, 3), padding = "same", activation = "relu"),
    Conv2D(64, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Conv2D(128, 3, padding = "same", activation = "relu"),
    Conv2D(128, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Conv2D(512, 3, padding = "same", activation = "relu"),
    Conv2D(512, 3, padding = "same", activation = "relu"),
    Conv2D(512, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Flatten(),
    Dense(4096, activation = "relu"),
    Dense(4096, activation = "relu"),
    Dense(1000, activation = "softmax") # 1000가지 이미지를 분류하는 대회였음
])


VGG16 모델 D - Fashion Mnist에 맞게 튜닝하기

VGG16 Model D를 튜닝해서 fashion_mnist 분류모델을 구현하는 코드로 작성해 보겠습니다.

  • 논문 상 입력 데이터는 (224, 224, 3)이지만 Fashion Mnist의 데이터는 (28, 28, 1)입니다. 28을 224로 늘리는 것은 해상도가 너무 깨지고 특징이 소실되므로 의미가 없다는 판단 하에, 입력 부분을 28로 튜닝해서 사용하기로 결정했습니다.
  • MaxPool 횟수를 4회에서 3회로 줄이기로 했습니다. 이미지 사이즈가 가로 세로 (28, 28)이기 때문에 논문 그대로 4번 사용하면 사이즈가 소실될 수도 있습니다.
  • 논문 속 레이블은 1000개이지만 Fashion Mnist의 레이블은 10개입니다. 따라서 마지막 Dense 레이어의 필터 수를 1000에서 10으로 줄이겠습니다.
import tensorflow as tf

# 데이터 불러오기
fashion_mnist = tf.keras.datasets.fashion_mnist
(X_train, y_train), (X_test, y_test) = fashion_mnist.load_data()

# 정규화
X_train, X_test = X_train/255. , X_test/255.

# 3D를 4D로 변환
X_train = X_train.reshape(-1, 28, 28, 1)
X_test = X_test.reshape(-1, 28, 28, 1)
# 논문 VGG16 D모델 -> 패션 엠니스트에 맞는 모델로 바꾸어보기
    
model_D_VGG_fashion = tf.keras.Sequential([
    Conv2D(64, 3, input_shape = (28, 28, 1), padding = "same", activation = "relu"),
    Conv2D(64, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Conv2D(128, 3, padding = "same", activation = "relu"),
    Conv2D(128, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Flatten(),
    Dense(4096, activation = "relu"),
    Dense(4096, activation = "relu"),
    Dense(10, activation = "softmax") # 10가지 이미지로 분류하기
])
model_D_VGG_fashion.summary()

model_D_VGG_fashion.compile(loss = tf.keras.losses.SparseCategoricalCrossentropy(),
                 optimizer = Adam(),
                 metrics = ['accuracy'])
import os

# callbacks
early = tf.keras.callbacks.EarlyStopping(patience = 5)
cp_path = "training/cp-{epoch:04d}.ckpt"
cp_dir = os.path.dirname(cp_path)
cp_callback = tf.keras.callbacks.ModelCheckpoint(cp_path,
                                                 monitor = 'val_loss',
                                                 verbose = 1,
                                                 save_weights_only=True)

# train
history_D_fashion = model_D_VGG_fashion.fit(X_train,
                                            y_train,
                                            validation_split = 0.25,
                                            batch_size = 128,
                                            epochs = 200,
                                            callbacks = [early,cp_callback])

model_D_VGG_fashion.evaluate(X_test, y_test)

# 313/313 [==============================] - 2s 5ms/step - loss: 0.2621 - accuracy: 0.9195
# [0.2621194124221802, 0.9194999933242798]

VGG16모델을 튜닝해서 사용한 결과 단 한 번의 시도만에 0.92에 가까운 valid accuracy를 확보하게 되었습니다 :)


 
 
이것으로 간단하게 살펴본 VGG 논문 리뷰를 마치겠습니다. 감사합니다 :)

 

주제 : 심장과 혈관 의학 분야에서의 인공지능과 데이터과학

일시 : 2024년 4월 4일 (목) 20:00 - 21:10

강사 : 미국 Mayo Clinic 이은정

 

 

서론

4월 4일 목요일 Zoom을 이용한 실시간 화상 온라인 방식으로 진행된 이은정 강사님의 <심장과 혈관 의학 분야에서의 인공지능과 데이터과학> 세미나에 참여했습니다. 이은정 강사님께서는 서울대학교에서 석박사 과정을 마치신 뒤 미국에서 가장 큰 병원 중 하나인 Mayo Clinic에서 Senior Data Science anlayst로 계시면서 다양한 의학 인공지능 모델을 개발하고 데이터과학을 연구하고 계신 멋진 분이셨습니다. 저는 현재 고려사이버대학교에서 최대영 교수님의 빅데이터 개론 수업을 듣고 있는데, 교수님께서 본 세미나에 참가할 수 있도록 초대해 주신 덕분에 좋은 기회로 참여할 수 있었습니다.

 

제 주변에 의학 관련 분야에 종사하는 지인이나 친인척이 몇 있습니다. 그래서 저는 평소 의학 분야에도 어느 정도 관심을 가지고 있었습니다. 특히 의료 분야에서 데이터의 중요성, 인공지능의 빠른 성장에 대해서는 익히 들어본 바가 있어 큰 호기심을 가지고 있었습니다. 그러나 의료 분야는 어쩐지 진입장벽이 있다고 느껴졌어요. 어디서부터 관련 도메인 지식을 수집해 나갈지 막막했습니다. 그렇게 멀게만 느껴지던 의학 도메인에 조금 더 가까워지는 계기가 되기를 바라며 강의를 듣기 시작하였습니다.

 

본론

(1)

 

먼저 강의는 미국의 심혈관 질환에 대한 통계 자료와 분석으로 시작하였습니다. 2016년 데이터를 기준으로 미국 인구의 약 절반이 고혈압을 가지고 있는 것으로 추정되며, 2035년까지 미국 인구의 약 45%가 심장 관련 질병 가지게 될 것으로 예상된다고 하는데요. 이를 뒷받침하는 자료로 미국에서는 성인 5명 중 오직 1명만이 적정한 양의 운동을 하고, 전자담배의 사용으로 흡연률이 치솟고 있는 등 다양한 통계를 함께 볼 수 있었습니다.

 

2022년 잠깐이나마 뉴욕에서 어학연수를 했던 기억을 떠올려 보았습니다. 뉴욕에는 확실히 한국보다 다양한 체형을 가진 사람들이 있었어요. 다양한 사이즈의 체형이 용납되는 개방적이고 자유로운 사회적 분위기가 저는 참 좋았던 기억이 있습니다. 지금 돌아보니 빅 사이즈 국민들의 건강 관리가 그만큼 중요한 과제가 될 수도 있겠다는 생각이 드네요. 이렇게 미국에서는 큰 사회적 이슈가 되고 있는 심혈관 질병 진단과 관리를 위해 인공 지능이 다양하게 개발되고 활용되고 있다고 합니다.

미국에서 심혈관 질환 관련 의료 마켓에서 AI의 성장률을 확인해볼 수 있는 그래프 자료입니다.

(2) - 1

다음으로 심혈관 질환을 위한 인공지능 모델 개발을 위한 데이터 자료에 관해 말씀해 주셨습니다. AI 트레이닝, 테스팅 데이터로 활용되는 가장 대표적인 데이터 자료 두 가지는 환자 기록과 검사 결과입니다. 환자 기록으로는 몸무게, 키, 혈압, 피검사, 환자 내원시 상담 내용 등이 있습니다. 검사 결과로는 ECG라고 불리는 심전도 검사 결과, 심초음파, 혈관조영상, 망막이미지 등이 있습니다. (심혈관 질병 관련)

 

요즘은 스마트 워치로도 간단하게 심전도 측정이 가능한 세상입니다. 이렇게 스마트 워치를 활용하여 측정한 심전도 자료 역시 인공지능 개발에 활용이 되기도 한다고 합니다. 물론 병원의 전문 장비를 이용한 측정 결과랑 비교하자면 신뢰도가 많이 떨어지기 때문에, 이렇게 신뢰도가 낮은 데이터를 가지고 모델 개발을 하면 그만큼 정확도가 떨어질 수밖에 없다고 하셨습니다. 그래도 집에서 간단하게 간이 방식으로 심전도를 측정하여 문제 상황을 조금이나마 예측하고 예방할 수 있다면 큰 도움이 되겠지요. 언젠가 병원의 전문 장비만큼 실력이 짱짱한 스마트 워치가 보급될 지도 모를 일입니다.

 

망막 이미지와 목소리에 관련해 말씀해 주신 부분이 굉장히 흥미로워 기억에 남는데요. 요즘은 망막 이미지 하나만으로도 나이, 성별, 흡연여부, 혈압 비만도 등의 건강 정보를 예측을 할 수 있는 수준으로 모델 개발이 이루어져 있다고 합니다. 굉장히 신기했어요. 또 목소리를 이용하여 심장 관련 진환을 예측을 할 수 있다고도 하셨습니다. 목소리라고 하면 기관지 컨디션이나 기분 정도만 짐작해볼 수 있는게 아니냐고들 생각하지만 목소리에는 예상 외로 몸에서 발생하는 이상 신호들이 잘 반영된다고 합니다. 생각보다 예측율이 높은 편이라고 해서 정말 신기했어요. 이렇게 상식을 뛰어 넘는 재밌는 모델링 연구 작업에 저도 참여할 수 있다면 얼마나 좋을지 기대가 되었습니다. 의료 도메인에도 꾸준한 관심을 가지고 포트폴리오를 구축해 나가 봐야 겠다는 욕심이 들었어요.

 

(2) - 2

다음으로 데이터를 활용해 어떤 모델을 개발할 수 있는지에 대해 말씀해 주셨습니다. 가장 큰 연구가 이루어지고 있는 분야는 바로 질병을 예측할 수 있는 모델 개발이라고 합니다. 가격이 비싼 혈관조영상 촬영 없이 상대적으로 저렴한 심장 박동, 심초음파 검사 결과만으로 좌심방의 크기와 대동맥 판막 협착증을 예측하는 모델을 예시로 들어 주셨습니다. 초기에 잡아냈다면 미리미리 관리하여 쉽게 치료할 수 있었던 질병들을 뒤늦게 발견해서 큰 문제가 되는 경우가 많잖아요. 저희 외조모께서도 대장암을 초기에 발견하지 못해 결국 투병하시다가 2년만에 돌아가셨었거든요. 인공지능 모델을 통해 세계적으로 질병 예측의 시기가 앞당겨지고 정확성이 크게 증진될 수 있기를 바라는 마음입니다.

 

또 기존에는 상위 검사만으로 측정이 가능했던 수치들을 하위 검사로 측정 가능할 수 있도록 돕는 모델 개발에 관해서도 언급하셨습니다. 상위 검사는 검사 방법이 복잡한 대신 정확도가 높은 특징이 있습니다. 그만큼 비용이 높을 수밖에 없는데, 특히 미국의 악랄한 의료비에 대해서는 다들 잘 알고 계시지요. 대부분의 상위 검사들을 한국에서는 어렵지 않게 받을 수 있지만 미국에서는 그렇지가 못한 현실이라고 해요. 이렇게 상위 검사만으로 측정 가능했던 수치들을 하위 검사 결과로부터 예측해낼 수 있는 모델들을 개발하는 겁니다. 이게 보편화가 된다면 비용때문에 의료 서비스를 받지 못했던 사회적 약자들에게 커다란 도움이 되겠다는 생각을 했습니다. 저도 뉴욕에 있을 때 갑자기 엄청난 복통이 찾아와 응급실에 갔다가 아픈 것보다도 병원비를 걱정하느라 마음 고생을 했던 경험이 있거든요. 질 좋은 의료 서비스를 모두가 평등하게 받을 수 있는 세상을 만드는 데 저도 도움이 될 수 있으면 좋겠습니다.

 

그 밖에 인공지능 모델 개발을 위해 데이터 레이블링하는 작업에 큰 시간과 비용이 소요되는데, 이런 레이블링을 대신하는 모델을 역시 활발하게 개발이 이루어지고 있다고 알려 주셔서 흥미로웠습니다. 인공지능 모델 개발을 돕는 모델이라니! 미래에는 인공지능끼리 주르륵 체인을 이루면서 인간의 개입이 전혀 필요하지 않은 세상이 언젠가 오겠다는 귀엽고도 무서운 생각을 해 봤어요. 그 안에 나의 역할이 무언가 있기를 바랄 뿐입니다.

 

(3)

마지막으로 메이요 클리닉에서 개발하고 있는 인공지능 모델 예제를 몇가지 알려 주셨습니다. 간단히만 정리해 보겠습니다.

 

[1] 정상 심장 리듬에서 심방세동을 예측하는 모델

  • 심방세동은 간헐적으로 발생하며 특이한 증상이 없는 경우가 많아서 진단에 어려움이 많다.
  • 심방세동의 정확한 진단 및 예측을 위해서는 환자가 병원에 긴 시간 내원해야 한다. 대체로 환자가 24시간동안 몸에 리드줄을 부착하고서 수집한 데이터로 판단을 내리는 방식으로 진단이 이루어졌다고 한다.
  • 메이요 클리닉에서 보관하고 있는 18만명이 넘는 환자들의 데이터, 65만개가 넘는 ECG 기록을 가지고 단시간 측정한 정상 리듬에서도 심방세동을 예측해낼 수 있는 인공지능 모델을 개발하였다.
  • 기존의 진단이 여러 전문 인력의 수작업으로 이루어져 시간과 자원이 과하게 소모되었으나 이 과정들을 인공지능으로 대체할 수 있게 되면서 시간과 자원 비용을 크게 절약하게 되었다.

[2] 비대성 심근병증 (HCM) 분류

  • 비대성 심근병증은 심장 근육이 비정상적으로 두꺼워져 형태가 변형되고 기능이 악화되는 질환으로, 특히 미국에서는 HCM으로 돌연사하는 프로, 아마추어 운동선수가 많아서 사회적 이슈라고 한다.
  • 메이요 클리닉에서 저렴한 심장 박동 검사 결과만을 가지고 HCM을 예측할 수 있는 모델을 개발하였다. 심장 박동 검사는 비용이 저렴할 뿐만 아니라 다양한 시설에 보편화가 많이 되어 있어서 접근성이 무척 높은 하위 검사이다.
  • 테스팅 어큐러시가 0.95 - 0.97으로 무척 높았는데, 미국 뿐만 아니라 다른 여러 나라들에서도 이벨류에이팅을 해 보니 역시 높은 어큐러시 결과가 나왔다고 한다.

[3] ECG(심전도)를 이용한 좌심실 이완기능 평가

  • 좌심실의 이완 기능은 심장 기능 평가에 있어서 아주 중요한 사항이다. 좌심실 이완 시 높은 filling pressure는 다양한 심혈관 질환과 관련되는 악조건이다. 현재 미국에서 70세 이상의 노인의 70%가 불완전한 좌심실 이완 기능을 가지고 있다는 통계 결과가 있다고 한다.
  • 좌심실 이완 기능 평가는 혈관조영상으로 판단하는 것이 가장 이상적이다. 그러나 혈관조영상 촬영은 굉장히 어렵고 복잡하며 비용이 높은 상위 검사이다. 이를 대체하기 위해 심초음파 결과를 이용하는 경우가 많은데, 정확성이 매우 떨어진다고 한다.
  • 메이요 클리닉에서 ECG(심전도) 검사 결과를 통해 좌심실 이완 기능을 평가하는 인공지능을 개발하였다. 검사 결과로 Grade 1-2-3 세 단계의 등급을 매기게 되는데, 등급이 높을 수록 위험한 상태를 의미한다.

 

결론

이번 이은정 강사님의 세미나는 심혈관 질환의 예측과 진단에 데이터와 인공지능이 어떻게 활용되고 있는지 그 실제 사례를 알아볼 수 있는 무척 좋은 학습 기회였습니다. 평소 '미래에는 인공지능이 의사를 대체할 것이다!'는 무시무시한 이야기를 들어만 보았지, 실제로 병원에서 어떤 식으로 개발이 되고 활용이 되고 있는지 그 개별 사례를 알아보기가 쉽지는 않았거든요. 강사님께서 실제 사례와 경험을 토대로 강의해 주신 덕분에 앞으로 심혈관 질환 뿐만 아니라 다른 분야에서도 인공지능 적용 사례를 찾아보기가 수월해질 것 같습니다.

 

이런 의료 도메인에서의 인공지능의 개발은 질 높은 의료 서비스를 더 많은 사람들이 받을 수 있도록 돕습니다. 그만큼 사회적, 인도적으로 큰 의미가 있다고 생각해요. 과학 기술 발달의 바람직한 예라고 할 수 있겠죠. 특히 저는 초등교육에 제 20대 모두를 바쳤던 만큼 어린이와 청소년의 신체건강, 정신건강에 특히 큰 관심을 가지고 있는데요. 학생 개인정보 보호를 위해 자세한 사례를 여기에 세세히 밝힐 수는 없지만, 타고난 유전병으로 인해 자유로운 활동이 어려운 학생을 가르쳐 보았고, 신체적 장애를 가지고 있어 신체활동에 제약이 있는 학생도 가르쳐 보았고, 자폐 스펙트럼을 가지고 태어나 친구를 사귀기 어려워하는 학생도 가르쳐 보았습니다. 가정환경이 어려워 필요한 만큼 의료 서비스를 받지 못하는 친구도 있었는데 제가 도울 방법이 제한적이라 참 안타깝고 미안했었어요. 이렇게 다양한 어려움을 가지고 있는 학생들을 도울 수 있는 인공지능 모델에는 무엇이 있을지 앞으로 계속해서 고민해 보고, 관련 데이터를 찾아보고 분석해 보고자 합니다.

 

질의응답 시간에 이은정 강사님께 어린이를 대상으로 인공지능 모델을 개발해 본 경험이 있으신지 질문을 드렸었는데요. 어린이들은 신체적으로 어른과 무척이나 다르기 때문에 어른을 대상으로 개발한 모델을 어린이들에게 동일하게 적용하기는 어렵다고 하셨습니다. 따라서 영유아나 어린이를 대상으로 한 모델의 경우 성인을 대상으로 개발한 성공적인 모델을 가지고 수정 보완하여 만들어내는 경우가 많다고 하셨어요. 좋은 답변이 되었습니다. 추후에 시간을 내어 구체적인 사례를 찾아보기로 하였습니다.

 

마지막으로 이렇게 개발된 인공지능 모델이 완전히 전문인력을 대체하고 있는 상황은 아니라고 말씀을 해 주셨습니다. 의료진이 진단을 하고 판단을 내리는 데 근거가 되는 하나의 수단으로 인공지능 모델의 예측 결과를 활용하고 있다고 하셨어요. 데이터를 분석하고 모델을 개발할 줄 아는 능력 있는 데이터 사이언티스트는 이렇게 원하는 분야의 전문 인력과 협업할 수 있구나! 좋은 자극이 되었습니다. 열심히 노력해서 저 역시 이렇게 세상의 발전에 기여하고 다른 이들에게 영감이 되는 전문가가 되겠다고 다짐하며, 이번 세미나 리뷰를 마칩니다.

 

 

 

읽어주셔서 감사합니다.

 

+ Recent posts