<대학원생 때 알았더라면 좋았을 것들>은 전반적인 대학원 생활, 논문을 읽는 방법, 논문 리뷰 발표하는 방법 등에 대해 다루고 있고

<그렇다면 실험실 죽순이가 될 수밖에>는 실험 실패, 대학원 생활 시 느끼는 감정과 다짐, 그리고 극복 방법 등을 위주로 서술되어 있다.

 

사실 이 책들은 어쩌다보니 대학원 입학 후 1년간 방황하고 난 뒤 읽게 되었는데, 마인드 셋에 큰 도움이 됐다.

요즘 회사를 퇴사하고 대학원 가시는 분들도 많아졌고, 대학원에 갈지 말지 고민하는 분들이 주변에 많이 생겼다. 회사 경험, 대학원 경험 모두 삶을 살아가는데 중요하다고 할 수 있어서 대학원을 갈지, 회사를 갈지 둘 중에 하나를 선택하는 양자택일의 관점으로 생각하기보다는 둘 중에 무엇을 먼저 할지 선후의 문제로 바라보면 고민이 간단해지는 것 같다.

 

대학원 생활에서 배울 수 있는 점은 크게 4가지라고 할 수 있다.

  • 논문을 통해 지식을 얻는 습관
  • 문제 정의 및 과학적 방법론으로 문제 해결 경험
  • 시간 관리부터 연구에 대한 실행까지 스스로 규율을 세우고, 스스로 탐색해나가는 경험
  • 실패했다면 왜 실패했는지 분석

 

우리 연구실은 비교적 자유롭기 때문에 시간 관리는 스스로 해야했고, 이로 인해 나태해지기 쉬운 환경이었다. 처음 대학원에 왔을 때는 학부 때와는 달리 동기나 선배가 없어서 적응하기가 조금 힘들었다. 그리고 내 벼락치기 성격으로 인해 결과를 내야 할 때는 며칠간 밤을 새서 수면 사이클이 엉망이 됐었다. 그래도 요즘에는 새로 들어오신 박사 과정 언니랑 연구 얘기도 하고, 플래너를 다시 쓰기 시작하면서 많이 안정된 것 같다.

 

아무튼 대학원에 와서 배운 가장 큰 것은 지식 습득뿐만 아니라, 더 크게 와닿는 점은 자율적인 시간 컨트롤 방법을 깨닫게 된 것이다. 논문 작성을 목표로 한정된 내 시간을 분배하고 관리하면서 목표를 이뤄나가기 위해 일의 우선순위대로 처리하는 과정에 대해 배웠다.

 

그렇지만 목표를 세우더라도 실험이 항상 성공하는 것은 아니었다. 주제를 잡는데 결과가 잘 안나와서 몇 번이나 갈아엎었고, 지금도 사실 연구가 잘 되고 있진 않는 것 같다. 그~래~도 이전에는 뭐가 잘 안되면 바로 구글링해서 문제를 해결하거나 다른 방법을 시도했지만, 지금은 적어도 실험자의 책임감을 가지고 실패했다면 왜 실패했는지에 대한 원인을 분석하게 됐다. 만약 명확한 이유를 모르겠더라도 가장 가능성이 높은 이유에 대해 생각해보고 동기 언니와 의견을 공유할 수 있게 된 것 같다.

 

대학원에 오니까 다시 느끼는거지만, 커뮤니케이션 능력의 중요성을 매번 느끼고 있다. 수업 때, 내가 하고 있는 연구를 다른 사람이 이해하기 쉽게 전달하는 것이 너무 어렵고, 논문 읽은 것을 다른 사람들과 공유하는 것도 너무 어렵다.

 

학기 중에는 매 달 소프트 런치를 진행하면서 다른 사람들의 연구를 듣곤 하는데, 혼자 연구만 하는 것 보다 확실히 생각 환기도 되는 것 같고 좋더라. 자주 만나진 못하지만, 코스웍에서 만난 다른 분야의 분들이나 종종 연구실 외 사람들, 그리고 이미 취업한 친구들에게 말을 걸다 보면 지금의 기술 트렌드와 내 연구 분야에 대해 새로운 의견을 들으면서 생각이 확장되는 것 같다. 

 

 


 

대학원에 처음 들어왔을땐 (지금도 그렇지만) 논문 읽는게 너무너무 어려웠다. 영어인데 집중도 안되고, 10페이지도 넘으면서 배경지식도 없어서 논문 하나를 일주일 걸려 읽었었다. 논문을 읽기 전에 논문을 선택하는것부터도 너무 어려웠다.

 

이 책에도 나오듯이 논문은 google scholar에서 주제와 키워드를 넣어서 검색하도록 하고, 출판연도/논문 인용수/저자 확인을 하는 것이 좋다.

키워드: review(검토), overview(훑어보기), survey(조사), tutorial(쉽게 설명)

 

그리고 논문을 읽을 땐 왜 읽는지 생각하면서 읽는 것이 좋다. 

💡 논문을 읽으며 생각할 것들

1. 이 논문이 왜 중요한가?
논문을 읽을 때 다 이해할 필요는 없으나 해당 논문의 중요한 내용(컨트리뷰션)이 무엇인지, 왜 accept 되었는지 이해하며 읽자

2. 결론이 무엇인가?
결국 이 논문을 어떻게 써먹을 수 있을까? 여기서 얻어갈 것은 무엇이 있을까? 에 대한 대답은 결론에서 나온다.
⇒ 왜 읽었는지, 이 논문에서 알고 넘어가야 할 것은 무엇인지 읽으면서 항상 생각

3. 세미나에서 어떻게 설명할 것인가?
데이터 셋, 풀려고 하는 문제, 목적, 결과 (큰 그림을 보자)
논문에 있는 그림과 글을 매칭시키며 읽고, 그림에 있는 것들의 물리적인 의미를 잘 생각할 것
⇒ 그림의 순서 (일반적으로 본격적인 설계에 앞서 기본 이론을 설명하기 위한 개념도, 기본 설계 구조, 설계 시 발생하는 문제점 소개, 어떻게 해결했는지, 성능 지표와 비교표 등의 순서)

4. 논문 리뷰 시 대략적인 틀
1) ~을 제안한 논문
2) ~을 사용
3) 풀려고 하는 문제는 ~을 목적으로 ~ 데이터 셋을 사용하여 ~ 라는 결과를 얻었다
4) 기존연구에는 ~라는 한계가 존재했다
5) 그래서 이 연구는 ~라는 컨트리뷰션해서 accept 되었다
6) 우리는 이걸 읽고 ~에 필요한 ~를 얻으면 된다

 

마지막으로 논문 읽은 것을 정리해 둘 땐, 만약 이 논문을 인용한다면 어떻게 할지 각 단락을 한 문장으로 요약해보도록 하자.

  • abstract: 나는 이런 문제를 풀거야
    • 무슨 문제를 풀고있고, 어떤 기여를 담았는지 관한 내용
    • ex) 딥 러닝을 웨어러블에 적용할 때, 데이터가 부족한 경우, 이를 증강하는 방법 연구
  • introduction: 이 문제는 이런 동기에서 연구가 시작됐는데,
    • 관련 연구 요약 (1. 풀려는 문제가 무엇인지 2. 왜 이 연구가 중요한지 3. 다른 연구와의 차별점)
    • ex) 파킨슨병 환자의 증상을 생활속에서 진단하려면 딥 러닝이 필요한데, 수집할 수 있는 데이터 수가 적다.
  • related works: 관련해서 이런 접근들이 있었다
    • ex) 데이터가 적은 경우 비전, 음성과 같은 다른 도메인에서는 어떻게 다루었는지 조사 & 파킨슨 병 환자의 모니터링 연구에 대한 조사
  • method: 나는 이런 방식으로 접근해보려 하는데
    • ex) data augmentation 방법 제시
  • experiment: 정말 이게 효과적인지 실험을 해 보니
    • 10% 가량의 성능 향상
  • discussion: 실험 결과는 이렇게 해석할 수 있다.
    • 적절한 data augmentation 방법은 타깃 태스크마다 다를 수 있어 직접 실험, 검증하는 것이 더 좋다는 한계점
  • conclusion: 요약
    • abstract와 비슷하나, 과정, 결과, 의의가 좀 더 자세하다

 

 

리뷰논문은 조금 다르다. 나는 이 블로그 글이 많이 도움 됐다.

리뷰논문은 구성이 어떻게 되는지 파악하고(abstract, introduction 이 중요하다), 주제의 발전 과정과 각 연구의 차별점/한계점을 중점으로 알아둔다. 

 

 


 

랩 미팅을 할 땐 무작정 가지 말고 준비를 해서 가도록 하자.

💡 미팅 체크리스트
1. 주제
2. 연구 목적/배경
    - 논문의 연구목적 연구 배경에 들어갈만한 내용
    - 연구를 해야 하는 이유를 문장으로 작성
3. 연구 문제 및 가설
    - 가설이나 연구 문제는 자질구레한 설명 없이 문장으로 모두 완성해야 피드백을 받을 수 있다.
4. 연구 방법
    - 가설을 이렇게 세웠기 때문에 이런 분석 방법을 통해 검증하고자 합니다

실험은 성공하기도, 실패하기도 한다. 주의할 점은 실험결과가 긍정적일수록 몇번을 더 확인하고, 보수적인 자세로 결과를 해석해야 한다는 것이다. 좋은 결과든 나쁜 결과든 결과 보고를 할때는 신중하게 팩트만 보고하고, 주관적인 해석보다는 데이터를 객관화 시켜 결론을 도출하는 것이 좋다. 그리고 다음에는 이번 실험 결과를 바탕으로 어떻게 해 보겠다는 내용으로 마무리 지어야 한다. 실험 결과가 좋고 나쁘고는 의미가 없으며, 그 결과를 바탕으로 다음 방향을 의미있게 설정하는 것이 중요하다.

 

  • 실패했으면 죄송해하지 말고 원인을 찾아보자
    • 실험을 기획한 사람 입장에서 잘 모르겠거나 애매모호한 부분은 최대한 가능성 있는 대안을 첨부하자
    • 다음에 추가로 꼭 확인해볼 것을 분명히 해서 책임감있는 태도를 전하자
    • 마지막으로 랩미팅 자료에 무엇이 들어있어야 하는지, 교수님과 무엇을 논의해야 하는지 생각하자

 


 

첫 논문을 작성할 때는 아무리 논문 수정을 해도 비슷한 내용 반복에 어떻게 써야하는지 감이 전혀 잡히지 않았었다. 이 책에서도 나오는데, 논문 작성은 일단은 써야 수정과정을 거칠 수 있다.

 

논문은 연구를 다 하고 쓰는 순차적인 과정이 아닌, 연구 <-> 논문쓰기 를 반복 하면서 연구만 할 때 몰랐던 논문조사/추가실험 필요성 보완하는 것이 중요하다. 따라서 일단 목차를 나누고, 각 섹션에 들어갈 내용을 정리한 뒤, 어떤 그래프가 들어가야 할지 일단 생각해둔다.

  • abstract 과 introduction: 서론에서는 넓은 범위에서의 문제 제기부터 시작하여 본인의 연구 영역까지 점진적으로 범위를 좁히며 초점을 맞춘다. 그 과정에서 다른 사람들이 문제를 해결해온 방식도 간략히 소개한다. 그리고 기존 연구의 한계점을 설명한 뒤 본 연구의 필요성을 역설하고 마지막으로 본 연구의 기여를 요약해준다.
    • 초록과 서론을 읽고 난 뒤 독자가 어떤 느낌을 받으면 좋겠는지
      • 이 논문이 다루는 문제는 정말 꼭 해결해야 하는 문제같아
      • 이전의 솔루션들은 아직 많은 한계점을 가지고 있군
      • 여러 시도와 비교해도 이 논문의 기여는 의미가 있겠어
      • 이 논문의 기여 중 핵심은 ~~이군
      • 앞으로 논문의 나머지 부분에선 이런 내용이 나올 것 같아
  • related works: 다른 사람의 연구를 평가할 땐 객관적인 수치와 함께 조심스럽게 그 한계점을 드러낸다.
    • ex) B연구는 어떤 뿐에 어떤 아이디어를 적용하여 A연구를 정확도 ~에서 ~로 개선하였다.
  • methods 와 experiments
  • conclusion
    • abstract 내용을 반복하지 않는다. abstract는 논문을 읽지 않은 사람들이 처음 읽는 글이고, 결론은 논문을 모두 읽은 뒤 전체 내용을 조망하는 글이다.
    • abstract는 문제 제기와 연구의 중요성에 조금 더 큰 방점을 두고 결론은 실험을 통해 얻은 지식과 의의에 대해 조금 더 큰 방점을 두고 풀어낸다.
    • 결론에서 갑자기 새로운 내용을 꺼내는 것은 부적절하다

 


 

논문을 쓰고 나면 컨퍼런스 등에 발표를 하게 되는데,

우리 지도 교수님께서 가장 중요하게 생각하시는 점은 다른 사람들이 이해하기 쉽게 발표를 하는 것이다. 

  • 그래프의 x축과 y축이 무엇인지 짚어주자
  • 그래프를 보고 그 경향성이 의미하는 바가 무엇인지 짚어주자
  • 발표 내용을 이해하는데 중심이 되는 이론을 설명하는데 인색하지 말자
  • 서론에서 자신의 연구 분야의 필요성이 잘 전달되어야 한다. 문제 제기를 명확히 하고, 자신의 연구가 어떤 솔루션을 제시하는지 밝힌다.
  • 매 슬라이드가 넘어갈 때 앞장과 뒷장의 인과관계가 있도록 만들자. 슬라이드가 넘어갈 때 적절한 연결어를 적극적으로 활용하자

 

 

 

 

후기

이제 벌써 석박 통합 5학기 차인데, 그동안 배운점도 많고 아직 부족한 점도 많은 것 같다.

이 책을 읽고 계속 되새겨볼만한 점과 대학원 생활을 하면서 깨달은 점을 정리해봤다. 

이 책은 목차만 보고 읽기로 결정했는데, 책의 전반부는 코드를 잘 읽는 방법, 후반부는 코드를 잘 작성하는 방법을 다루고 있다. 흥미로웠던 점은 기초적인 뇌과학 원리를 설명하면서, 동시에 프로그래밍을 할 때 어떠한 방식으로 행동해야하는지 여러 연구를 통해 보여준다. 요즘 선배들이 연구했던 코드를 볼 일이 생겨서 틈틈이 보고 있는데.. 시간만 가고 잘 이해가 되지 않았는데.. 흑흑 😭 이 책에서 언급하는 방법들을 통해 처음 보는 코드도 잘 이해할 수 있도록 해봐야겠다 

 

 

1. 코드 더 잘 읽기

프로그래밍을 하다보면, 마냥 "이해가 안된다" 또는 "어렵다" 는 단순한 혼란이 아닌, 알고보면 서로 다른 3가지 유형의 혼란이 발생한다. 그리고 각 혼란은 뇌의 서로 다른 부분에서 발생한다.

코드 분석 시 3가지 인지 과정

 

(1) 지식의 부족: 우리는 APL 언어에 대해 알지 못하고, 특히 T 가 의미하는 바를 모르기 때문에 혼란스럽다. = LTM

// APL에서의 이진수 표현
2 2 2 2 2 T n

(2) 정보의 부족: Integer.toBinaryString 이라는 메서드 이름으로 어떤 일을 하는지 유추할 수 있지만, 내부적으로 어떻게 동작하는지에 대한 정보를 얻으려면 메서드 내부 코드를 살펴봐야 한다. = STM

// Java 에서 이진수 표현
public class BinaryCalculator {
	public static void main(Integer n){
    	System.out.println(Integer.toBinaryString(n));
    }
}

(3) 처리능력의 부족: 변수에 임시로 저장되는 값 등을 따라가기 위해 머릿속으로만 처리하기 어려워서 종이에 따로 표시 등을 해둬야 한다. = 작업기억공간

// 베이직에서 이진수 표현
LET N2 = ABS(INT(N))
LET B$ = ""
FOR N1 = N2 TO 0 STEP 0
	LET N2 = INT(N1/2)
    LET B$ = STR$(N1-N2*2) + B$
    LET N1 = N2
NEXT N1
PRINT B$

 

 

그래서 코드를 잘 읽기 위해서는 뇌의 다양한 부분이 사용되고 있다는 점을 인지하고, 뇌의 각 부분에 대한 특징을 이해해야한다.

우리가 코드를 기억할 때는 짧은 기간동안 정보를 저장하는 STM 과 장기적으로 정보를 저장하는 LTM 이 함께 작용하는데, STM 의 용량이 적고 정보를 저장하는 시간이 짧기 때문에 이해하거나 기억한 정보를 처리할 때는 LTM 과 상호작용한다. 이때 기억 공간에는 `청크` 단위로 저장된다. 청크는 배경 지식에 따라 큰 단위가 될 수도, 작은 단위가 될 수도 있다. 숙련된 프로그래머들은 LTM 에 저장된 익숙한 키워드, 구조, 도메인 개념 등을 활용하여 코드를 이해한다. 실험 결과, 특히 디자인 패턴에 대한 지식을 가지면 청킹 능력이 향상되고, 코드를 빠르게 이해 및 수정 할 수 있었다. 주석도 도움이 될 수 있다.

 

이 책에서는 쉽게 정보를 찾을 수 있는 환경에서는 문법을 기억하지 않아도 된다는 인식이 발생하여 인출 강도가 약해지고(알긴 아는데 계속 입 안에서만 맴도는 상태), 악순환이 이어진다고 주장한다. 이를 해결하기 위해서는 검색하기 전에 먼저 의도적으로 기억하려고 노력해야 한다.


 

복잡한 코드를 읽는 일은 작업 기억 공간에 과부하를 일으키기도 한다. 작업 기억 공간은 STM과는 다르게, 정보를 처리하는 프로세서 역할을 한다는 차이점이 있다. 작업 기억 공간 또한 STM 과 마찬가지로 용량(인지 부하)이 정해져있으며, 청크 단위로 이루어진다. 만약 청크 단위로 나뉘지 않는 너무 많은 요소를 가진 문제를 풀려고 하면 과부하가 걸린다.

 

여기서 말하는 인지부하는 크게 3가지로 나뉘는데,

  • 내재적 부하: 문제 자체가 얼마나 복잡한지
  • 외재적 부하: 외부적 요인에 의해 문제에 추가된 것
"""
10보다 큰 항목을 선택하는 파이썬 프로그램 = 동일한 내재적 인지 부하 (문제 그 자체)
"""

# List Comprehension 에 대한 배경지식에 따라 외재적 인지 부하가 달라질 수 있다
above_ten = [a for a in items if a > 10]

# 일반적인 for 문
above_ten = []
for a in items:
	if a > 10:
    	above_ten.append(a)
  • 본유적 부하: 생각을 LTM 에 저장하는 과정에서 일어나는 인지적 부하

 

 

인지 부하를 줄이기 위해서는 리팩터링을 하면 된다. 리팩터링은 코드의 기능은 유지하되, 구조를 개선하는 것이다. 일반적으로는 코드의 유지 보수를 쉽게 하기 위한 목적으로 이루어지지만, 저자는 장기적으로 가독성이 높은 코드를 작성하도록 리팩터링 하는, 인지적 리팩터링도 중요하다고 한다.

 

의존 그래프(왼쪽)와 상태표(오른쪽)

그러나 리팩터링을 해도 코드의 구조가 복잡하다면, 예를 들어 정확히 코드의 어디를 파악해야 하는지 모르거나 코드가 서로 밀접하게 연결되어 많은 코드를 읽어야 하는 경우 작업 기억 공간 용량을 초과할 수 있다. 이런 상황에서는 코드 기반으로 의존 그래프 또는 상태표를 만들어 작업 기억 공간을 보조하면 도움이 된다. 

 

 

 

 

변수 역할의 11가지 분류 - 사야니에미의 프레임워크

 

프로그램을 깊이 이해하기 위해서는 변수가 중심적인 역할을 하는데, 변수를 파악할 때에는 변수 이름뿐만 아니라 타입과 역할을 살펴보는 것이 도움된다.

 

앞서 코드를 이해하는데 도움이 되는 의존 그래프  상태표를 소개했는데, 이 방식은 다소 국지적이라는 단점이 있다. 코드를 보다 깊게 이해하기 위해서는 텍스트 구조 지식 계획 지식에 대해 살펴봐야 한다.

  • 텍스트 구조 지식: 키워드가 하는 일이나 변수의 역할 등과 같은 프로그램의 표면적인 이해와 관련됨
  • 계획 지식: 프로그래머가 프로그램을 작성할 때, 계획한 것이 무엇인지 혹은 무엇을 달성하려고 했는지

 

뿐만 아니라, 코드가 어떤 구조로 되어 있고 어떻게 연결되어 있는지 살펴봐야 하는데

다음 4가지 단계를 거쳐 표면적 지식으로부터 좀 더 깊은 이해로 진행할 수 있다.

  1. 프로그램의 시작점(초점, focal point)을 찾는다. ex) main()
  2. 초점으로부터 지식을 확장 ex) 초점에서 시작해서 변수, 메서드, 클래스 등의 연결 파악
  3. 관련된 개체로부터 개념 이해 ex) 입력값에 대한 함수의 결과 이해, 클래스가 가지는 필드의 지식 이해 등
  4. 여러 개체에 걸쳐있는 개념 이해 ex) 제약조건이나 오류 등

코드를 읽을 때에는 모든 라인을 순차적으로 읽기보다는, 전체적으로 스캔 후 함수의 콜 스택을 따라 읽으면 좋다.

 

 

 

2. 코드에 대해 생각하기

지금까지 코드를 읽고 이해하는 데 도움이 되는 방법들에 대해 알아봤으니, 문제를 해결을 위한 통찰력을 얻는 방법에 대해 알아보자. '모델링'은 소프트웨어 설계 결정을 내리는 데 도움이 된다. 상태표, 의존 그래프, 개체 관계도 등 실제 모델이

아니라, 우리가 머릿속에서 생각하는 정신적 모델을 사용하는 것도 문제 해결에 도움이 된다.

  • 정신모델: 풀어야 할 문제에 대해 추론하기 위해 사용할 수 있는 작업 기억 공간 내의 추상화
    • 방향/무방향 그래프 또는 다양한 형태의 리스트와 같은 자료구조
    • 디자인 패턴
    • 모델 뷰 컨트롤러와 같은 아키텍쳐 패턴
    • 개체 관계도 또는 시퀀스 다이어그램과 같은 도표
    • 상태도 또는 페트리 넷과 같은 모델링 도구 등

 

또한, 이미 배운 지식은 다른 영역에서도 유용하다. 이를 학습 전이학습 도중 전이라고 한다. 

그림과 같이 새로운 정보를 습득하면, 먼저 감각 기억 공간과 STM에 의해 처리되며 작업 기억 공간으로 전송된다. 작업 기억 공간은 이에 대해 생각하는 동시에 LTM에서 관련 정보를 검색하여 가져온다. 그래서 자바를 알고 있는 상태에서 파이썬 메서드에 대해 배우면 조금 다르게 동작하더라도 파이썬 메서드에 대해 더 빨리 학습할 수 있는 것이다.

  • 저도 전이: 자동화된 기술을 이전하는 것 ex) Ctrl+C, Ctrl+V
  • 고도 전이: 복잡한 작업이 전이되는 것 ex) 프로그래밍 시 변수를 사용하려면 먼저 선언을 해야한다고 생각하는 것
  • 근거리 전이: 서로 가까운 영역 사이에서 지식이 전이되는 것 ex) 미적분학과 대수학
  • 원거리 전이: 서로 먼 영역 간에 일어나는 전이 ex) 라틴어와 논어학

 

그렇다고 해서 전이가 항상 도움되는 것은 아닐 수 있다. 

  • 긍정적 전이: 무언가를 알고 있어 새로운 작업을 할 때 도움이 되는 전이
  • 부정적 전이: 기존 지식이 새로운 것을 배우는 데 방해가 되는 경우

긍정적 전이가 일어나면, 새로운 상황에 대한 정신 모델을 처음부터 만들 필요 없이 LTM에 저장된 정신 모델을 부분적으로 활용하여 형성할 수 있다. 그러나 이미 알고 있던 지식이 새로운 지식을 배울 때 유사하지 않음에도 불구하고 비슷하다고 인식될 경우 오개념을 발생시킬 수 있는데, 이러한 상황을 부정적 전이라고 한다. 예를 들어 자바에서는 변수를 초기화하지 않으면 사용할 수 없지만, 파이썬에서는 이러한 가정이 혼란을 야기할 수 있다. 오개념을 방지하는 데 코드베이스 내의 테스트문서화가 도움이 된다.

 

 

 

3. 좋은 코드 작성하기

좋은 코드를 작성하기 위해서는 변수, 클래스 및 메서드 등 코드 내 구성 요소들에 이름을 잘 명명하는 것이 중요하다. 변수명은 그자체로 코드를 설명하는 주석문처럼 작용하기도 하며, 초기에 부여한 이름이 대부분 유지되기 때문이기도 하다.

 

좋은 변수명을 인지적 관점으로 바라보면, 의미가 담긴 구성요소가 명확한 이름이라고 할 수 있다. 1부에서 배운대로 변수명을 읽을 때는 각 부분을 분할하고, 작업 기억 공간으로 전송될 때 각 부분과 관련된 정보를 LTM에서 검색하여 작업 기억 공간으로 전송한다. 

nmcntrlst 보다 name_counter_list 가 이해하기 쉽다

 

이 책에서는 더 나은 변수명을 위해 페이텔슨은 3단계 모델을 제시하고 있다.

  1. 이름에 포함할 개념을 선택: 의도를 고려하여 개체가 어떤 정보를 보유하고 있으며 무엇을 위해 사용되는지 나타낸다.
  2. 각 개념을 나타낼 단어 선택: 일관성 있는 단어를 사용한다.
  3. 단어들을 사용하여 이름 구성: 자연어에 맞추어 이름 틀을 사용 ex) points_max (x), max_points (o)

주의할 점은 클린 코드를 위반하는 구조적 안티패턴뿐만 아니라 언어적 안티패턴도 고려하여 이름을 지어야 한다는 점이다. 이러한 안티패턴은 우리 뇌에 혼란을 초래하여 인지 부하를 일으킬 수 있다. 

 

 


 

좋은 코드를 작성하면서 어려운 문제를 해결하는 것은 LTM을 강화하는 것이 가장 도움 된다. 문제 해결은 문제 이해, 계획 수립, 계획 실행 단계로 구성되며, 각 단계에서 LTM이 중요한 역할을 한다. 뿐만 아니라 LTM에 저장된 다양한 유형의 기억이 문제 해결을 돕는데 기여한다.

기억의 종류

  • 절차적(암시적) 기억: 운동 능력이나 의식하지 않고 발휘하는 기술로, 반복 연습에 의해 형성된다. ex) 단축키, 괄호를 열면 자동으로 괄호를 닫는 코드 작성 등
  • 선언적(명시적) 기억: 기억할 수 있는 사실이 있고, 그 사실을 자신이 알고있다는 것도 안다. 명시적 기억은 명시적 주의가 필요하다.  
    • 일화적 기억(경험): 과거에 문제를 어떻게 해결했는지 기억할 때 사용된다. 전문가들은 주로 익숙한 문제를 '해결' 하기보다는 '재현'한다. 즉 새로운 해결책을 찾는 대신 이전에 유사한 문제에 효과가 있었던 해결책에 의존하는 경향이 있다. ex) 계층 구조와 관련된 문제를 해결할 땐 트리를 사용했었지~
    • 의미적 기억(사실): 
    • 일화적 기억은 추가적인 노력을 하지 않아도 생성되지만, 의미적 기억과 마찬가지로 기억을 많이 생각해야만 인출 강도가 높아진다.

위 기억 분류에 따르면 암시적 기억은 작업을 신속하게 실행하는데 도움이 됨을 알 수 있으며, 암시적 기억이 형성되기 위해서는 인지 단계, 연상 단계, 자율 단계가 필요하다.

  • 인지 단계: 새로운 것을 배우는 때로, 새로운 정보를 더 작은 부분으로 나누고 당면한 작업에 대해 명시적으로 생각한다. ex) 배열의 인덱스는 0부터 시작하니까, 인덱스 3인 원소는 4번째 원소구나!
  • 연상 단계: 응답 패턴이 나타날 때까지 새 정보를 적극적으로 반복한다. ex) 인덱스를 계산할 때는 항상 1을 빼면 되네!
  • 자율 단계: 기술을 자동화 한 상태로, 인지 부하가 증가하지 않는다. ex) 리스트와 리스트 연산을 마주하면 숫자를 세거나 명시적으로 생각하지 않고도 인덱스를 즉시 알 수 있는 상태

 

문제 해결 능력을 향상시키는 또 다른 방법은 본유적 부하이다. 프로그래밍 능력을 향상시키기 위해 모든 프로그램을 처음부터 작성하는 것이 중요하다고 생각하는 사람들도 있지만, 연구 결과에 따르면 다른 사람이 작성한 코드를 읽고 해당 코드를 분석하는 것이 프로그래밍 능력 향상에 도움이 될 수 있다고 한다. 이는 작업 기억 공간의 인지 부하(본유적 부하)와 관련 있다.

세번째 유형의 인지 부하, 본유적 부하 - 두뇌가 정보를 LTM에 다시 저장하기 위해 수행하는 노력

작업 기억 공간의 내용이 LTM 에 다시 저장되기 위해서는 본유적 부하가 뒤따른다. 그러나 작업 기억 공간의 부하가 너무 많으면 결과가 LTM으로 옮겨지는 것이 어려울 수 있다. 따라서 본인만의 힘으로 프로그래밍 구현을 연습하는 것은 물론 실력이 향상되지만, 내재적 부하와 외재적 부하로 인해 본질적 부하가 부족할 수 있다. 다른 사람의 코드를 읽을 때는 인지 부하가 그렇게 높지 않으므로 코드 구조를 자세히 검토하고 이를 기억할 수 있게 된다. 따라서 프로그래밍 실력을 향상시키기 위해서 GitHub 탐색, 오픈 소스 코드 분석 등을 통해 코드를 연구하는 것이 유용하다.

 

 

 

4. 코딩에서의 협업

실제로 소프트웨어는 팀에 의해 개발되므로 동료와 함께 몰입하며서 코드를 작성하는 것이 중요하다. 특히 다른 사람들과 함께 작업할 때는 업무 중단이 흔하고 다시 돌아와 원래 하던 업무를 계속하기가 어려운데, 어떻게 하면 중단에 더 잘 대비할 수 있을까?

  • 정신 모델 저장(brain dump): 정신 모델은 중단으로부터 복구하는데 유용할 수 있으므로 정신 모델에 대한 메모를 주석문으로 남기는 것이 도움된다. 코드는 프로그래머의 사고 과정을 모두 설명하진 못하므로 코드에 특정 접근 방식을 선택한 이유, 코드의 목표 또는 구현을 위해 고려한 다른 대안과 같은 내용은 주석으로 남겨두면 좋다. 
  • 미래 기억 향상: 미래 기억은 미래에 무언가를 할 것에 대한 기억으로, to-do 주석문 등을 활용할 수 있다. 
  • 하위 목표 라벨링: 문제를 어떤 작은 단계로 나눌 수 있는지 명시적으로 기록함으로써 업무가 중단된 후 하던 업무를 계속할 때 자신의 생각을 정리하는데 유용하다.

 

 

 

후기

뇌가 코드를 처리하는 방식에 대해 새롭게 알 수  있었다.

저자가 LTM의 중요성에 대해 정말 많이 강조하고 있어서 그동안 모르면 구글링 하면 되지~ㅋ 하고 대수롭지 않게 생각했던 나를 반성함 🥲

새로운 코드를 읽을 때는 왠지 집중도 안되고 코드 분석도 어려워서 그냥 ADHD인줄 알았는데,,,

이 책을 읽고나서부터는 여기 나오는 방법대로 새로운 코드를 마주칠 때 잘 대처할 수 있을 것 같다.

 

4부도 딱히 정리는 안했지만 흥미로웠던 부분이, 사람은 멀티태스킹이 안된다고 한다. 그런데 멀티태스킹을 하는 사람들은 자신이 매우 생산적이라고 느낀다고 한다...!! 그들 자신의 수행능력이 만족스럽다고 느끼지만, 실상은 한가지 일만 하는 사람들에 비해 결과도 좋지 않고 시간도 오래쓴다는 것이다

 

아무튼 뇌의 행동 기반으로 인간이 왜 이렇게 되는지 설명해줘서 좋았고, 사회 과학 분야에서 이루어진 실험과 함께 예시를 들어줘서 이해가 쉬웠다. 앞으론 학교에서 수업을 듣거나.. 시험공부하거나.. 연구할땐 이 책 내용을 떠올리면서 의식적으로 노력해야겠다.

머신 러닝 문제의 복잡도를 평가하는 가장 좋은 방법 중 하나는, 필요한 데이터의 종류와 이런 데이터를 사용할 수 있는 기존 모델이 있는지 살펴보는 것이다.

 

데이터 패턴을 찾을 후 모델을 선택할 때

  • 특성 스케일이 차이나는 경우 ex. 나이(1~100)와 수입(1~1억) 특성의 크기와 관계 없이 가장 예측 성능이 좋은 특성을 활용해야 한다.
    • 방법1) 특성의 scale을 정규화하는 전처리를 수행해 평균이 0이고 단위 분산을 가지도록 한다. = 정규화
    • 방법2) 특성 scale의 차이에 영향을 받지 않는 모델로 바꾼다. = decision tree, random forest, gradient-boosted decision tree
  • 특성의 선형 조합만 사용하여 좋은 예측을 만들 수 있는 경우
    • 회귀 문제에는 linear regression, 분류 문제에는 logistic regression 또는 naive bayes => 간단하고 효율적이며 종종 모델의 가중치를 직접 보고 중요한 특성을 식별할 수 있다.
    • 만약 feature과 target의 관계가 더 복잡한 경우 다층 신경망 등 비선형 모델을 사용하거나 교차특성을 생성하도록 한다
  • 데이터에 시계열 특징이 있는 경우 = 현재 값이 이전 시간의 값에 의존하는 경우
    • 시계열 정보를 명시적으로 인코딩할 수 있는 모델을 사용한다. = ARIMA 모델, RNN 등
  • 각 샘플이 패턴의 조합인 경우 = 이미지 분야, 문장 분류, 음성 인식 등
    • CNN의 경우 translation invarient학습하는 능력 때문에 유용하다. 이미지에서의 위치와 상관없이 패턴 추출 가능

https://scikit-learn.org/stable/tutorial/machine_learning_map/index.html

 

 

데이터 분할은 일반적으로 학습 70% 검증 20% 테스트 10% 사용한다.

데이터 분할 방법은 검증을 위해 매우 중요한 부분이며, 검증 셋과 테스트 셋을 본적 없는 데이터와 비슷하게 만드는 것을 목표로 해야한다.

종종 랜덤하게 샘플링하여 훈련, 검증, 테스트 셋을 나누는데, 경우에 따라 이로 인해 데이터 누수가 일어난다.

데이터 누수는 실전에서 사용자가 사용할 때 얻을 수 없는 정보를 훈련 과정에서 모델이 얻을 때 일어난다.

데이터 누수를 일으키기 쉬운 랜덤 데이터 분할

  • 시계열 데이터 누수: 모델이 과거 데이터에서 학습하여 아직 일어나지 않은 미래 이벤트를 예측하게 되는데, 이런 시계열 데이터를 랜덤하게 나누면 데이터 누수가 발생한다. 모델을 랜덤하게 나눈 데이터에서 훈련하고, 남은 데이터에서 평가하면 예측하려는 이벤트 뒤에 발생한 데이터에 모델이 노출되게 된다. => 검증 셋과 테스트 셋에는 잘 동작하지만, 실전에는 나쁜 성능
    • 실제 세상에서는 불가능한 미래 정보를 사용하여 학습했기 때문
  • 샘플 오염: 데이터 누수의 일반적인 원인은 랜덤한 작업이 일어나는 수준 때문이다. 어려운 작업에서 너무 잘 동작한다면 버그나 데이터 누수가 없는지 확인하도록 해야한다.
    • ex. 학생 수필 점수 예측 프로그램: 동일한 학생의 여러 수필이 데이터셋, 테스트셋에 모두 들어가게 되면 모델이 학생을 식별하는 특성을 감지하여 점수를 예측하게 된다 => 한 학생이 작성한 수필은 모두 비슷한 점수를 받게 된다 => 따라서 각 학생의 데이터가 훈련 셋이나 검증 셋 중 한 곳에만 나타나야 한다.

 

 

과적합을 피하는 방법 중 하나는 regularization이다. = 정보를 표현하는 모델의 능력에 벌칙을 부과하여 관련 없는 많은 패턴에 초점을 맞추는 모델의 능력을 제한하고, 적은 개수의 더 유용한 특성을 사용하게 한다.

  • 가중치의 절댓값을 cost로 부과한다 => ex. 선형 회귀나 로지스틱 회귀 같은 모델에서 L1/L2 regularization은 loss function에 큰 가중치에 대한 cost항을 추가한다
    • L1: 가중치의 절댓값의 합 => 유용하지 않은 특성을 0으로 만들어 정보가 풍부한 특성을 선택하도록 돕는다. 일부 특성에 상관관계가 있을 때 L1이 유용하다. ex) Lasso
    • L2: 가중치의 제곱합
  • drop out: 신경망이 자주 사용하는 regularization으로, 훈련 과정에서 신경망의 뉴런 중 일정 비율을 랜덤하게 무시한다. => 하나의 뉴런이 과도한 영향력을 발휘하지 못하도록
  • random forest와 같은 트리 기반 모델에서 트리의 최대 깊이를 줄이면 각 트리가 데이터에 과대적합되는 능력이 감소된다.
    • random forest에서는 트리 개수를 늘려도 regularization의 효과가 있다

 

 

머신 러닝 애플리케이션의 배포 방식에는 크게 두 가지가 있는데, 배포 방식을 선택 할 때 속도, 하드웨어, 네트워크 요구 사항, 개인 정보 보호, 비용 복잡도 등 여러 요인을 고려해야 한다. 

server-side deployment

  • server-side deployment: 클라이언트로부터 요청을 받아 inference 파이프라인을 실행하여 결과를 반환하는 웹 서버로 구성된다. 서버 측 모델 작업에는 스트리밍과 배치 두가지로 이루어질 수 있다. 호스팅 서버가 필요하여 제품의 인기가 높아지면 비용이 빠르게 증가하고, 호스팅 서버가 애플리케이션의 단일 장애점이 될 수 있다.
    • 스트리밍: 요청을 받자마자 즉시 처리 => 사용자가 개별적인 inference 요청을 보내기 떄문에 동시 사용자 수에 맞추어 선형적으로 인프라를 늘려야 한다. 서버가 처리할 수 있는 용량 이상으로 트래픽이 증가하면 지연/실패하게 된다.
      • 스트리밍 애플리케이션은 속도에 대한 요구 사항이 높을 때 필요하다. 모델에서 필요한 정보가 예측 시점에서 제공되고 모델의 예측이 즉시 필요하다면, 스트리밍 방식이 적합하다.
    • 배치: 한 번에 많은 개수의 요청을 처리 => inference 파이프라인을 여러 샘플에서 동시에 실행할 수 있는 하나의 job으로 간주한다. 배치 잡은 많은 샘플에서 모델을 실행하고 필요할 때 사용할 수 있도록 결과를 저장한다.
      • batch job: 모델의 inference가 필요하기 전에 모델의 필요한 특성을 얻을 수 있을 때 적합하다. (미리 저녁에 돌려놓고 다음날 아침에 확인한다던가)
      • 배치 방식은 스트리밍 방식과 동일한 횟수만큼 추론을 실행한다. 하지만, 자원을 더 효율적으로 사용할 수 있다. => 자원을 할당하고 병렬화 하기가 쉬움
      • 배치 방식은 미리 계산된 결과를 저장하고, inference 시에는 저장한 내용을 추출하기만 하면 되므로 추론 속도가 매우 빠르다 == 캐싱이랑 비슷하다고 생각하면 된다.
    • 가능한 많은 샘플을 계산하고, 추론 시에 사전에 계산된 결과를 추출하도록 하는 하이브리드 방식을 사용하는 것도 가능하다. 만약 사전 계산한 결과가 없거나 오래되었다면 즉시 계산을 수행한다. => 계산할 수 있는 모든 것을 미리 수행하기 때문에 결과를 신속하게 제공한다. 단 배치 파이프라인 + 스트리밍 파이프 라인을 모두 유지하기 위한 비용과 시스템 복잡도가 단점이라고 할 수 있다.

client-side deployment

  • client-side deployment: 클라이언트 측에 모델을 배포하는 목적은 모델 실행 서버가 없더라도 모든 계산을 클라이언트에서 실행하기 위해서이다. 모델은 앱에 포함되어 디바이스에서 사용되거나 웹 브라우저에 로딩될 수 있다. => 모든 사용자에 대한 inference를 실행하기 위해 인프라 구축할 필요가 없다. 또한 디바이스에서 모델 실행 시 디바이스-서버 사이 전송할 데이터 양이 줄어든다. (네트워크 지연을 줄일 수 있다) 또한 inference에 필요한 데이터가 민감한 정보를 담고 있을 때 장치에서 모델을 실행하게 되면 데이터를 원격 서버로 전송할 필요가 없으므로 제 3자가 데이터에 접근할 위험이 줄어든다. 단, 일반적으로 사용자 디바이스는 서버보다 컴퓨팅 성능이 떨어지므로, 디바이스에서 모델을 실행할 수 있도록 모델의 복잡도를 제한해야 한다.
    • on-device: 태블릿이나 핸드폰 프로세서는 일반적으로 머신러닝 모델 실행에 최적화 되어 있지 않아 inference 파이프라인을 느리게 실행한다. 배터리를 많이 소모하지 않고 클라이언트 측 모델을 빠르게 실행하려면 모델이 가능한 한 작아야 한다. => 모델 크기를 줄이려면 간단한 모델을 사용하거나, 모델의 파라미터 개수, 계산 정밀도를 줄여야 한다
      • 스마트폰과 같은 장치에서 실행하기 너무 복잡한 최첨단 모델에 의존하는 경우 서버에 배포되어야 한다. 일반적으로 장치에서 (처리 지연 시간) > (네트워크 지연 시간) 일 경우 클라우드 상에서 모델을 실행하는 것을 고려해봐야 한다.
      • 온 디바이스 배포는 속도, 인프라, 개인 정보에 대한 이득이 커서 엔지니어링 작업에 노력을 투자할만큼 충분한 가치가 있다.
    • browser: 대부분의 스마트 장치는 브라우저를 가진다. 브라우저는 종종 빠른 그래픽 계산을 지원하기 위해 최적화되어 있다. 사용자는 추가 애플리케이션 설치 없이 브라우저를 통해 모델과 상호작용할 수 있으며, 사용자 디바이스에서 계산이 수행된다. 서버 인프라는 모델의 가중치가 포함된 웹 페이지만 사용자에게 제공하면 된다. 단, 대역폭 비용을 늘린다는 단점이 있다. (애플리케이션 설치 방식은 모델을 한 번만 다운로드 하면 되지만, 브라우저 방식을 클라이언트가 웹 페이지를 열때마다 모델을 다운로드 함) => 사용하는 모델이 매우 작거나 빠르게 다운로드 가능하다면 브라우저 방식이 용이하다.
      • ex. tensorflow.js: 대부분의 미분 가능한 모델을 브라우저 상에서 자바스크립트로 훈련하거나 추론을 실행할 수 있다. 다른 언어로 훈련한 모델을 사용할 수도 있음

정리하자면, 서버 측 모델의 경우 가장 큰 latency는 종종 서버로 데이터를 전송할 때 발생한다.(네트워크 지연)

클라이언트 측 모델에서는 시간 지연 없이 실행되지만, 하드웨어 성능 제약으로 서버보다 느리게 샘플을 처리하게 된다. (처리 지연)

 

 

클라이언트 상에 배포 뿐만 아니라 훈련도 할 수 있다. 이는 특히 사용자마다 다른 모델을 원할 경우 유용하다.

이 아이디어가 federated learning(연합 학습) 의 핵심이다. 연합 학습에서는 클라이언트가 각자의 모델을 가지고, 각 모델은 사용자의 데이터에서 학습 및 요약되어 업데이트를 서버로 보낸다. 서버는 모든 업데이트를 사용해 모델을 향상시키고 새로운 모델의 업데이트를 개별 클라이언트에 전달한다. => 각 사용자는 각자의 요구에 맞는 개인화 모델을 얻을 수 있고 & 여전히 다른 사용자로부터 집계된 정보에서 도움 받을 수 있다.

연합 학습에서는 사용자 데이터를 서버로 바로 전송하지 않고, 집계된 모델 업데이트 값만 전송하기 때문에 개인 정보가 더욱 보호될 수 있다. 단, 개별 모델이 잘 동작하면서 서버로 전송된 데이터를 적절하게 익명화시키는 것은 단일 모델을 훈련하는 것보다 복잡성이 증가한다.

 

 

머신 러닝 모델을 제품으로 배포할 때 성능과 트래픽을 관리하는 것은 매우 어렵다.

머신러닝은 늘어난 트래픽을 처리하기 위해 캐싱을 사용할 수 있다. 미래에 동일한 매개변수로 함수를 호출한다면, 저장된 결과를 추출하면 되므로 함수를 빠르게 실행할 수 있다.

  • 추론 결과 캐싱하기: LRU 캐시는 가장 간단한 캐싱 방법이다. 가장 최근 사용된 모델 입력과 해당 결과를 추적하여 캐시에 유지한다. 새로운 입력에서 모델을 실행하기 전 캐시에서 이 입력을 조회하고 해당 항목을 찾으면 캐시에서 결과를 바로 제공한다.  -> 사용자가 동일한 종류의 입력을 제공하는 애플리케이션에 적합하다.
    • 캐시 확인 후 캐시에 이미지 없음을 확인 => 모델 실행(병목 지점) => 캐시에 추가 => 결과 반환
    • 캐시 확인 후 캐시에 이미지 있음을 확인 => 결과 반환
    • 주의) 캐싱을 사용할 때 부수 효과가 없는 함수만 캐싱해야 한다. ex) 결과 반환 및 데이터 저장하는 함수는 캐싱하면 데이터베이스에 값을 저장하지 않게 됨
  • 인덱스 캐싱: ex. 검색 시스템을 만들 때 일반적인 접근 방법은 먼저 모든 인덱싱 문서를 의미있는 벡터로 임베딩 -> 사용자가 검색 쿼리를 입력하면 추론 시 임베딩 벡터로 변환되어 데이터베이스에서 가장 비슷한 임베딩을 찾는다. => 이런 방싱은 대부분 계산을 미리 해 놓으므로 추론 속도를 크게 높여준다.
    • 주의) 캐싱 성능을 향상시킬 수 있지만 복잡도가 증가한다. 모델이나 내부 데이터가 업데이트 된 경우 캐시를 삭제해야 하며, 캐시 크기도 조정하며 최적의 값을 찾아야 한다.

 

 

 

 

*** 용어정리

  • 분류: 두 개 이상의 카테고리로 데이터를 분류
  • 회귀: 연속적인 값을 예측
  • 지식 추출: 큰 모델(Teacher network)에서 추출한 지식을 작은 모델(Student network)로 transfer 하는 과정
  • 카탈로그 구성: 데이터 메타데이터 관리 서비스 
  • 생성 모델: 주어진 데이터를 학습하여 데이터 분포를 따르는 유사 데이터를 생성하는 모델 ex. GAN
  • 시계열: 미래 사건을 예측하기 위해 여러 개의 과거 데이터 포인트를 사용

 

후기

이 책은 머신러닝 전반적인 내용 뿐만 아니라 실제 머신러닝 기반 애플리케이션을 만들 때 전체 파이프라인에 대해 배울 수 있다.

 

그리고 데이터의 중요성을 끊임없이 말하고 있으며 데이터셋 자체를 효과적으로 조사하는 팁들이 있다. 데이터를 수집할 때에는 대표적이고 다양성을 가진 데이터 셋을 준비해야 하며, 데이터를 살펴보고 특정 종류의 데이터가 표현되지 않으면 더 많은 데이터를 모아야 한다. 이 과정에서 데이터 셋에 클러스터링 알고리즘을 적용해 보고 이상치를 살펴보면 좋다고 한다. 

 

머신러닝에 대해 전혀 모르는 내가 처음 들어보는 내용들도 많았는데,

모델에서 특성 중요도를 평가할 때 도움이 되는 방법 중 하나인 `블랙박스 설명 도구`라는 것이 있는데, 이는 내부 동작 방식과 상관 없이 모델의 예측을 설명할 수 있다고 한다.

https://github.com/rickiepark/ml-powered-applications/blob/main/notebooks/black_box_explainer.ipynb

 

GitHub - rickiepark/ml-powered-applications: <머신러닝 파워드 애플리케이션>의 코드 저장소입니다. 원서

<머신러닝 파워드 애플리케이션>의 코드 저장소입니다. 원서 깃허브는 https://github.com/hundredblocks/ml-powered-applications 입니다. - GitHub - rickiepark/ml-powered-applications: <머신러닝 파워드 애플리케이션>의

github.com

이러한 도구의 예시로 LIME과 SHAP이 있다.

 

마지막으로 전문가 인터뷰와 모델 서빙, 모니터링쪽 내용이 많은 도움이 된다.

 

 

컴파일러의 목적은 한 언어를 다른 언어로 변환하는 것이다. 그리고 어떤 컴파일러는 시스템에서 바로 실행할 수 있는 저수준 기계어로 컴파일하지만, 어떤 컴파일러는 가상 머신에서 실행하기 위한 중간 언어로 컴파일한다.

  • 닷넷 CLR, 자바 등은 여러 시스템 아키텍쳐에서 사용할 수 있는 중간 언어로 컴파일한다.
  • C, Go, C++, 파스칼 등은 바이너리 실행 파일로 컴파일한다. 바이너리는 컴파일한 플랫폼과 동일한 플랫폼에서만 사용할 수 있다.

파이썬 애플리케이션은 보통 소스코드 형태로 배포한다. 파이썬 인터프리터는 소스 코드를 변환 후 한 줄씩 실행한다.

CPython 런타임이 첫 번째 실행될 때 코드를 컴파일 하지만, 이 단계는 사용자에게 노출되지 않는다.

 

python interpreter 내부 동작 과정

 

파이썬 코드는 기계어 대신 `바이트코드` 라는 저수준 중간 언어로 컴파일된다.

바이트코드는 .pyc 파일에 저장되고, 실행을 위해 캐싱된다.

코드를 변경하지 않고 같은 파이썬 애플리케이션을 다시 실행하면 매번 다시 컴파일하지 않고 컴파일 된 바이트코드를 불러와서 빠르게 실행할 수 있다.

 

 

 

CPython 컴파일러는 왜 C로 작성되는가?

윈도우와 리눅스 커널 api는 모두 C로 작성되었기 때문에, 여러 표준 라이브러리모듈이 저수준 OS api에 접근하기 위해 C로 작성되었다. 

 

컴파일러는 크게 두 가지 유형이 있는데,

  1. 셀프 호스팅 컴파일러는 Go 컴파일러처럼 자기 자신으로 작성한 컴파일러다. 셀프 호스팅 컴파일러는 부스트래핑이라는 단계를 통해 만들어진다.
  2. Source to Source 컴파일러는 컴파일러를 이미 가지고 있는 다른 언어로 작성한 컴파일러이다.

c.f. PyPy라는 파이썬으로 작성된 파이썬 컴파일러도 있다.

 

 

 

메모리 관리

먼저 C에서의 메모리 할당에 대해 알아보자면, 

  • 정적 메모리 할당: 필요한 메모리는 컴파일 시간에 계산되고, 실행 파일이 실행될 때 할당된다.
    • C에서는 타입 크기가 고정되어 있으며, 컴파일러는 모든 정적/전역 변수에 필요한 메모리 계산 후 필요한 메모리 양을 애플리케이션에 컴파일해 넣는다. 메모리 할당 시에는 시스템 콜을 이용한다. 
  • 자동 메모리 할당: 스코프에 필요한 메모리는 프레임에 진입 시 콜스택 내 할당되고, 프레임이 끝나면 해제된다.
    • 마찬가지로 컴파일 시간에 필요한 메모리를 계산한다.
#include <stdio.h>
static const double five_ninths = 5.0/9.0; // const: 정적 할당

double celsius(double fahrenheit){
	double c = (fahrenheit - 32) * five_ninths; // 함수 내 변수는 자동 할당
    return c;
}

int main(){
	double f = 100; // 함수 내 변수는 자동 할당
    printf("%f F is %f C\n", f, celsius(f)); // celsius(f) 반환값은 자동 할당
    return 0;
}
  • 동적 메모리 할당: 메모리 할당 api를 호출하여 런타임에 메모리를 동적으로 요청하고 할당한다
    • 사용자 입력에 따라 필요한 메모리가 결정되는 경우
    • OS는 프로세스가 메모리를 동적 할당 할 수 있도록 시스템 메모리의 일부를 예약해 두는데, 이 공간을 heap 이라 한다.
    • 동적 할당 메모리는 제대로 반환하지 않으면 메모리 누수가 발생한다.

 

 

C 기반인 CPython은 C 메모리 할당 방식들의 제약조건을 따르기 쉽지 않다. 왜냐하면

  1. 파이썬은 동적 타입 언어로, 변수의 크기를 컴파일 시간에 계산할 수 없다.
  2. 코어 타입의 크기는 대부분 동적이다. 리스트 타입은 크기를 정할 수 없고, 딕셔너리 타입은 제한없이 키를 가질 수 있다.
  3. 값의 타입이 달라도 같은 변수명을 재사용할 수 있다.

따라서 C 메모리 할당 방식의 제약을 극복하기 위해 CPython은 동적 메모리 할당에 크게 의존하며 `참조 카운팅`과 `가비지 컬렉션`을 사용하여 메모리를 자동 해제하도록 했다. Python의 객체 메모리는 개발자가 직접 할당하는 대신, 하나의 통합 api를 통해 자동으로 할당된다.

CPython은 세가지 동적 메모리 할당자 도메인을 제공한다.

  • 저수준 도메인: 시스템 힙이나 대용량 메모리 또는 비객체 메모리 할당 시 사용된다.
  • 객체 도메인: 파이썬 객체와 관련된 메모리 할당 시 사용된다.
  • PyMem 도메인: PYMEM_DOMAIN_OBJ 와 동일하며, 레거시 api 용도로 제공된다.

또한 CPython은 두가지 메모리 할당자를 사용한다.

  • malloc: 저수준 메모리 도메인을 위한 OS 할당자
  • pymalloc: PyMem 도메인과 객체 메모리 도메인을 위한 CPython 할당자
    • 기본적으로 CPython에 같이 컴파일 된다. pyconfig.h에서 WITH_PYMALLOC=0으로 설정 후 파이썬을 컴파일하면 pymalloc을 제거할 수 있으며, 이 경우 PyMeM과 객체 메모리 도메인 api도 시스템 할당자를 사용한다.

 

CPython 메모리 할당자에 대해 알아보기 전에 먼저 Python 의 메모리 구조 레벨에 대해 알아보자.

  • Arenas
    • 힙 메모리에 할당 된 고정된 크기의 256KiB, 가장 큰 메모리 단위를 나타낸다.
      • 아레나는 익명 메모리 매핑을 지원하는 시스템에서는 mmap()으로 할당한다.
    • 파이썬 할당자 pymalloc에서 사용하는 메모리 매핑이다.
    • 시스템 페이지 크기에 맞추어 정렬하며, 시스템 페이지 경계는 고정 길이의 연속 메모리 청크다.
  • Pools
    • arena 당 pool 개수는 64개로 고정되어 있다.
    • pool의 크기는 OS의 default 메모리 페이지 사이즈와 일치해야 한다.
    • 고정된 크기의 4KB 크기를 가지며, 세 가지 상태를 가질 수 있다.
      • Empty; pool이 비어있으므로 할당할 수 있다.
      • Used; pool에 비어있지도 가득차지도 않게 개체가 포함되어 있다.
      • Full; pool이 가득차서 더 이상 할당할 수 없다.
    • pool은 요청이 들어오면 할당되며, 요청된 크기 인덱스에 맞는 사용 가능한 pool이 없는 경우 새로운 pool을 생성한다.
  • Blocks
    • 블록의 크기는 고정되어 있지 않으며, 블록 사이즈는 8 ~ 512 bytes 인 8의 배수이다.
    • 각 블록은 특정 크기의 파이썬 객체를 하나만 저장할 수 있으며, 세 가지 상태를 가질 수 있다.
      • Untouched; 할당되지 않음
      • Free; 할당 되었지만, 해제되어 할당에 사용할 수 있다.
      • Allocated; 할당됨
    • 아래 그림과 같이 사용되지 않은 블록은 풀 내부의 freeblock 단일 연결 리스트에 연결된다. 할당 해제된 블록은 freeblock 리스트의 맨 앞에 추가되며, 풀이 초기화 되면 첫 두 블록은 freeblock 리스트에 연결된다.

할당된 블록과 해제된 블록, 사용되지 않은 블록들을 포함한 사용중인 풀

python 디버그 api `debugmallocstats`를 사용하면 할당된 아레나의 개수와 사용된 블록의 총 개수를 알 수 있다.

크기 인덱스 테이블과 할당 상태를 비롯한 다양한 통계 정보가 출력된다.

더보기

Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys._debugmallocstats()
Small block threshold = 512, in 32 size classes.

class   size   num pools   blocks in use  avail blocks
-----   ----   ---------   -------------  ------------
    0     16           1             128           893
    1     32           3            1143           387
    2     48          10            3147           253
    3     64          47           11794           191
    4     80          33            6697            35
    5     96           7            1102            88
    6    112           5             595           130
    7    128           3             370            11
    8    144          15            1641            54
    9    160           2             150            54
   10    176          27            2451            33
   11    192           2              96            74
   12    208           1              73             5
   13    224           3             206            10
   14    240           1              57            11
   15    256           1              52            11
   16    272           1              46            14
   17    288           1              34            22
   18    304           5             232            33
   19    320           1              31            20
   20    336           1              20            28
   21    352           1              18            28
   22    368           1              23            21
   23    384           1              17            25
   24    400           2              42            38
   25    416           2              71             7
   26    432           2              54            20
   27    448           2              48            24
   28    464           2              43            27
   29    480           2              34            34
   30    496           1              29             3
   31    512           2              48            14

# arenas allocated total           =                    3
# arenas reclaimed                 =                    0
# arenas highwater mark            =                    3
# arenas allocated current         =                    3
3 arenas * 1048576 bytes/arena     =            3,145,728

# bytes in allocated blocks        =            2,795,680
# bytes in available blocks        =              261,600
2 unused pools * 16384 bytes       =               32,768
# bytes lost to pool headers       =                9,024
# bytes lost to quantization       =               13,888
# bytes lost to arena alignment    =               32,768
Total                              =            3,145,728

arena map counts
# arena map mid nodes              =                    1
# arena map bot nodes              =                    1

# bytes lost to arena map root     =              262,144
# bytes lost to arena map mid      =              262,144
# bytes lost to arena map bot      =              131,072
Total                              =              655,360

            8 free PyDictObjects * 48 bytes each =                  384
           4 free PyFloatObjects * 24 bytes each =                   96
          6 free PyFrameObjects * 360 bytes each =                2,160
           21 free PyListObjects * 40 bytes each =                  840
   2 free 1-sized PyTupleObjects * 32 bytes each =                   64
  16 free 2-sized PyTupleObjects * 40 bytes each =                  640
   5 free 3-sized PyTupleObjects * 48 bytes each =                  240
   2 free 4-sized PyTupleObjects * 56 bytes each =                  112
   2 free 5-sized PyTupleObjects * 64 bytes each =                  128
   3 free 6-sized PyTupleObjects * 72 bytes each =                  216
   3 free 7-sized PyTupleObjects * 80 bytes each =                  240
   3 free 8-sized PyTupleObjects * 88 bytes each =                  264
   0 free 9-sized PyTupleObjects * 96 bytes each =                    0
 0 free 10-sized PyTupleObjects * 104 bytes each =                    0
 3 free 11-sized PyTupleObjects * 112 bytes each =                  336
 0 free 12-sized PyTupleObjects * 120 bytes each =                    0
 3 free 13-sized PyTupleObjects * 128 bytes each =                  384
 2 free 14-sized PyTupleObjects * 136 bytes each =                  272
 2 free 15-sized PyTupleObjects * 144 bytes each =                  288
 0 free 16-sized PyTupleObjects * 152 bytes each =                    0
 1 free 17-sized PyTupleObjects * 160 bytes each =                  160
 0 free 18-sized PyTupleObjects * 168 bytes each =                    0
 0 free 19-sized PyTupleObjects * 176 bytes each =                    0

 

또는 표준 라이브러리 `tracemalloc` 모듈로 객체 할당자의 메모리 할당 동작을 디버깅할 수도 있다. 이 모듈은 객체 할당 위치나 할당된 메모리 블록의 개수 등에 대한 정보를 제공한다. 메모리 추적을 활성화 하려면 파이썬 실행 시 -X tracemalloc=1 옵션을 추가하면 된다. 여기서 1은 추적할 프레임 수이며, 더 늘릴수도 있다.

 

 

CPython 할당자에 대해 다시 돌아가보자면, 시스템 메모리 할당자 위해 구축된 할당자로 독자적인 할당 알고리즘을 구현한다. 이 알고리즘은 시스템 할당자와 비슷하지만 CPython에 특화되어 있다.

참고💡 대부분의 메모리 할당 요청은 고정된 크기의 작은 메모리를 요구한다.
- PyObject는 16byte, PyASCIIObject는 42byte, PyCompactUnicodeObject는 72byte, PyLongObject는 32byte 크기
- pymalloc 할당자로는 메모리 블록을 최대 256kb까지 할당할 수 있고, 그보다 큰 할당 요청은 시스템 할당자로 처리한다.

그리고 중앙 레지스터가 블록 위치와 사용할 수 있는 블록 개수를 관리한다. 한 풀이 모두 사용되면 다음 풀이 사용된다.

이러한 방식은 CPython에서 주로 만들어지는 작고 수명이 짧은 객체를 할당할 때 적합하다. 그리고 힙 할당 대신 메모리 맵(mmap())을 사용하게 된다.

 

이렇게 CPython은 C의 동적 메모리 할당 시스템 위에 구축되었으며, 메모리 요구 사항은 런타임에 결정되고 PyMem api를 사용하여 메모리를 시스템에 할당한다고 했다. 파이썬은 메모리 관리를 단순화 하기 위해 `객체 메모리 관리 전략`을 도입했다. 여기에는 참조 카운팅과 가비지 컬렉션이 있다.

 

 

파이썬 프로세스 내부 구조

컴퓨터의 CPU는 프로세스를 실행할 때 다음과 같은 추가 데이터가 필요하다.

  • 실행 중인 명령이나 명령을 실행하는 데 필요한 다른 데이터를 보관하는 레지스터
  • 프로그램 시퀀스의 어떤 명령을 실행 중인지 저장하는 프로그램 카운터(명령 포인터)

CPython 프로세스는 컴파일된 CPython 인터프리터+모듈로 구성된다.

모듈을 런타임에 불러들여서 CPython 평가 루프를 통해 명령으로 변환한다.

프로그램 레지스터와 프로그램 카운터는 프로세스에 저장된 명령 중 한 명령만을 가리킨다.

즉, 한번에 하나의 명령만 실행할 수 있다. = CPython은 한 번에 하나의 파이썬 바이트 코드 명령만 실행할 수 있다.

c.f. 따라서 프로세스의 명령을 병렬로 실행하려면 다른 프로세스를 포크하거나 스레드를 스폰하는 방법을 사용한다.

 

 

 

 

후기

이 책에는 CPython 컴파일러가 어떻게 동작하는지, 파이썬 코드를 어떻게 파싱하는지에 관한 설명이 8장까지 매우매우 자세하게 나와있고,

9장 이후 파이썬에서 메모리 관리가 어떻게 구현되는지 나와있다. 9장 이후의 내용이 굉장히 많이 도움이 되었다.

 

이책을 읽다가 새롭게 알게된 사실은 python의 long과 c의 long이 다르다는 점이었다...!!

python의 long은 숫자 리스트로, 12378562834를 [1,2,3,7,8,5,6,2,8,3,4]로 표현한다고 한다.

Python 정수 표현 시 메모리 사용량 변화 - https://mortada.net/can-integer-operations-overflow-in-python.html

(그치만 numpy/pandas 같은 패키지를 사용할 때는 C 스타일이 유지되기 때문에 오버플로우 발생을 고려해야 한다.)

 

그리고 파이썬에서 가비지 컬렉터가 카운터=0일때 회수된다는것은 여러 곳에서 찾아볼 수 있었지만

어느 주기로 실행되는지는 잘 나와있지 않아 그동안 궁금했는데, 

가비지 컬렉션은 정해진 양만큼의 연산이 일어났을 때 주기적으로 실행된다는 것을 알게 되었다. 

 

이 책에는 CPython을 위해 설계된 다양한 도구를 이용하는 벤치마킹, 프로파일링, 실행 추적 등이 자세하게 나와있어서 쉽게 따라할 수 있었다.

 

 

참고:

https://scoutapm.com/blog/python-memory-management

1장. 화폐 예제

  • 객체를 만들면서 시작하는 것이 아니라 테스트를 먼저 만들어야 한다.
  • 테스트를 작성할 때는 메소드의 완벽한 인터페이스에 대해 상상 해보는 것이 좋다.
  • 테스트와 코드 사이의 결합도를 낮추기 위해, 테스트하는 객체의 새 기능을 사용한다.

 

TDD를 할 때에는 아래와 같은 순서로 코드를 짜게 된다.

1. 테스트를 작성 -> fail
2. 컴파일 되게 하기
3. 테스트 pass
4. 중복 제거 (추상화)

화폐 예제를 위 순서에 따라 조금씩 기능을 추가하는 것을 따라가다 보니 어느새 필요한 여러 기능이 구현되어있었다.

`어떤 테스트들이 추가로 필요할까?` 생각하는 것, 그리고 `중복을 제거하기 위한 리팩토링`을 예제를 통해 볼 수 있는 챕터였다.

 

2장. xUnit

xUnit 챕터는 테스트 프레임워크를 만드는데, 그 것을 위한 테스트 케이스를 작성하고 있다.

테스트 프레임워크에서 테스트 케이스 진행 순서는 아래와 같다.

1. 테스트 메서드 호출
2. setUp - 자원 할당
3. tearDown (실패하더라도) - 자원 반납
4. 여러개의 테스트 실행
5. 결과 수집 및 출력
  • 테스트 커플링을 만들지 말아야 한다. -> 테스트(객체) 간에 영향을 주지 않도록 해야 함

나도 테스트 툴을 만들었던 경험이 있는데, 이 챕터를 읽었다면 좀 더 쉽게 구조를 잡을 수 있지 않았나 싶은 생각이 들었다.

큰 테스트를 작게 쪼개며 테스트를 격리하는 것에 대해 생각해 보게 되었다.

 

3장. 테스트 주도 개발의 패턴

  • 테스트 주도 개발 패턴
    • 테스트 데이터의 경우 산발적으로 쓰지 말자. (1과 2 중에 선택해야 한다면 1)
    • 테스트 데이터로는 여러 의미를 담는 동일한 상수를 쓰지 말자. (1+1 보다는 1+2를 사용)
  • 빨간 막대 패턴: 테스트를 언제 어디에서 작성할 것인지, 테스트 작성을 언제 멈출지
    • 오퍼레이션이 아무 일도 하지 않는 경우를 가장 먼저 테스트 하도록 하자
  • 테스팅 패턴
    • 큰 테스트 케이스는 작은 테스트케이스들로 나누자 (A, B, C를 한번에 동작하게 하기 보다는 A->B->C 순서대로 동작하도록)
    • 비용이 많이 들거나 복잡한 리소스에 의존하는 객체(ex. DB)를 테스트 할 땐, Mock 객체를 만들자.
      • Mock 객체: 스스로 검증할 수 있는 능력이 있고, 외부에서 결과값을 조정할 수 있다.
    • 한 객체가 다른 객체와 올바르게 대화하는지 보기 위해서는 Self Shunt 패턴을 이용 하자. 즉, 원래의 대화 상대가 아니라 테스트 케이스와 대화하도록 한다. => 테스트 케이스가 Mock 객체가 됨
      • Self Shunt 패턴의 경우 테스트 케이스가 구현 할 인터페이스를 얻기 위해 인터페이스 추출 과정이 필요하기 때문에, 인터페이스 추출이 쉬운지 존재하는 클래스를 블랙박스로 테스트 할 것인지 정해야 한다.
    • 로그 문자열 등을 가지고 있다면 테스트 시 문자열 집합을 저장하고 있다가 집합 비교 등을 수행하면 된다. 로그 문자열은 Self Shunt 패턴과도 잘 동작한다.
    • 발생하기 힘든 에러 상황의 경우, 그냥 예외를 발생시키는 특수한 객체를 익명 내부 클래스 등으로 만들어 이를 호출하도록 하면 된다.
      • 파일 시스템의 여유 공간이 없을 경우 발생 할 문제 = 실제로 파일을 채우기 보다는 fileSystemError() 등을 호출하도록
  • 초록 막대 패턴: 코드가 테스트를 통과하게 만들기 위해서
    • 일단은 깨진 테스트에 대해 상수를 반환하게 하자
    • 컬렉션을 다루는 연산은 먼저 컬렉션 없이 구현하고, 그 다음 컬렉션을 사용하게 하자
  • xUnit 패턴
    • 판단 결과는 true/false
    • 객체를 세팅하는 코드가 여러 테스트에 걸쳐 동일 할 때에는 테스트 fixture을 만들어 두는 것을 선택하는 것도 괜찮다 = setUp()
      • setUp을 만든다면 메서드가 자동으로 호출된다는 점과, 어떻게 초기화 되는지 기억해야 한다.
      • fixture 중 외부 자원이 있을 경우 tearDown()으로 해제 함으로써 테스트가 실행되기 전화 후의 외부 세계가 동일하게 유지되도록
  • 디자인패턴
    • 커맨드: 간단한 메서드 호출보다 복잡한 형태의 계산 작업에 대한 호출이 필요할 때 => 계산 작업에 대한 객체를 생성하여 이를 호출하도록 (ex. Java Runnable)
    • 값 객체: 널리 공유해야 하지만, 동일성은 중요하지 않을 때 => 객체가 생성될 때, 객체의 상태를 설정 후 상태가 변할 수 없도록 + 연산 결과는 항상 새로운 객체를 반환. 즉, 객체를 값처럼 행동하도록 만든다.
    • 널 객체
    • 템플릿 메서드: 작업 순서는 변하지 않지만 각 작업 단위에 대한 미래의 개선 가능성을 열어두고 싶은 경우 => 다른 메서들을 호출하는 내용으로만 이루어진 메서드를 만든다. => setUp, run, tearDown 구현
    • 플러거블 객체: 변이를 표현할 때 가장 간단한 방법은 명시적인 조건문을 사용하는 것이다. 그런데 이 명시적인 조건문은 중복될 수 있는데, 이 때 중복을 제거하기 위해 플러거블 객체를 사용하게 된다.
    • 플러거블 셀렉터: 인스턴스별로 다른 메서드가 동적으로 호출되게 하려면, 메서드의 이름을 저장하고 있다가 그 이름에 해당하는 메서드를 동적으로 호출하면 될 것이다. 플러거블 셀렉터란 이러한 경우 리플렉션을 이용하여 동적으로 메서드를 호출하는 것이다. (리플렉션이므로 남용하진 않도록 하자)
    • 팩토리 메서드: 새 객체를 만들 때 유연성을 원하는 경우
    • 임포스터: 기존의 코드에 새로운 변이를 도입하기 위해 기존의 객체와 같은 프로토콜을 갖지만 구현은 다른 새로운 객체를 추가한다. 이를 임포스터라고 하는데, 이 예로서는 널 객체와 컴포지트 패턴이 있을 수 있다.
    • 컴포지트 패턴: 하나의 객체가 다른 객체 목록의 행위를 조합한 것처럼 행도하게 만들기 위해서는 객체 집합을 나타내는 객체를 단일 객체에 대한 임포스터로 구현하게 된다. 이를 컴포지트 패턴이라고 한다. 
      • 중복이 나타나는 순간 컴포지트패턴을 도입해 보자
    • 수집 매개 변수: 여러 객체에 걸쳐 존재하는 오퍼레이션의 결과를 수집해야 할 때에는 결과가 수집될 객체를 각 오퍼레이션의 매개변수로 추가해 주면 된다.
      • ex. java.io.Externalizable - writeExternal 메서드는 객체와 그 객체가 참조하는 모든 객체를 기록한다. 이를 위해서 메서드는 수집 매개 변수로서 ObjectOutput을 전달한다

 

  • 리팩토링
    • 메서드 추출하기
    • 메서드 인라인
    • 인터페이스 추출/객체 추출하기
    • 메서드 객체 - 여러 개의 매개 변수 및 지역 변수를 갖는 복잡한 메서드의 경우 꺼내서 객체로 만든다

 

느낀 점

일단 책이 말하는 식으로 진행이 되어서 읽기 쉬웠고, 양도 얼마 되지 않아서 금방 다 읽었다.

내용 자체는 무엇을 말하려는지 알 것 같은데.. 흠,, 프로젝트에 직접 적용해 보아야 좀 더 이해한 내용이 명확해 질 것 같다.

+ Recent posts