BE

Backend #


2025-08-18 ⋯ LLM #1 LLM 이해와 Transformer

1. LLM 기본이해 Word Embedding (p.27-28) Word Embedding - 핵심 아이디어는 단어가 어떤 맥락에서 자주 함께 등장하는지를 학습. - “you say goodbye and I say hello”에서 - ‘goodbye’주변에는 ‘you’, ‘say’, ‘and’, ‘I’ 같은 단어가 함께 등장하고 그 관계를 학습하도록 신경망을 훈련시킨다. - 학습이 반복되면 각 단어는 벡터로 표현되고 의미가 비슷한 단어일수록 벡터 공간에서 가깝게 위치한다. - Input이 ‘goodbye’이고 Target이 ‘you’, ‘say’, ‘and’, ‘I’여도 된다. Word Embedding - 신경망 구조 그림 - 왼쪽 단어 목록, 가운데는 은닉층, 오른쪽에 단어 목록 - "eat"이 입력으로 들어가면 은닉층을 거쳐서 출력 쪽에서 "apple", "juice", "rice" 같은 주변 단어들이 활성화되고 이 과정에서 모델은 입력 단어와 주변 단어 사이의 연관성을 학습한다. Reasoning - 만들어진 벡터는 단순한 유사성뿐 아니라 관계까지 담고 있다. - ‘king - man + woman = queen’ - ‘왕에서 남성을 빼고 여성을 더하면 여왕’이라는 단어의 의미 관계가 수학적으로 표현된다. - Reasoning? - 놀이터에서 ‘( )가 나를 바라보고 있다’라는 문장에서 빈칸에 들어갈 수 있는 적절한 단어를 유사한 문장들의 패턴을 통해 추론 가능하다. - 단어 벡터 공간에서의 위치 관계 그림 - king, queen, man, woman 같은 단어들이 점으로 표시되고 상대적인 거리와 벡터 방향이 있으니까 King - Man + Woman = Queen 같은 의미적 연산이 가능하다. 의문점 - Word Embedding이 분포가설의 구현이라고했는데 분포랑 무슨상관이지? - 단어가 어떤 단어들과 자주 같이 나타나느냐의 분포가 그 단어의 의미를 규정한다는 게 분포가설. - Word Embedding은 비슷한 맥락에서 쓰이는 단어들은 비슷한 임베딩 벡터로 표현된다 즉 주변 단어와의 관계가 임베딩 공간에 투영된다. - 단어의 분포적 특성이 수치화되어 벡터 공간에 반영된다 = 분포가설을 계산가능한 형태로 구현한것이다. - 정리 - 어떤 분포를 (즉 평균 분산을) 진짜로 구현한다기보다 '단어의 분포(유사한 단어와 자주 나타나는 정도)적 특성이 있다'라는 이론을, Word Embedding은 주변 단어와의 관계를 비슷한 임베딩 벡터로 표현 즉 수치화함으로써 '구현'했다. RNN, seq2seq, attention (p.30-31) - RNN은 입력된 단어를 임베딩이라는 연속형 벡터로 바꿔서 모델에 넣고 바로 앞까지 처리된 hidden state 벡터와 함께 계산한다. - 예를 들어 what, will, the, fat 같은 단어가 순서대로 들어오면 모델은 마지막 시점에 얻어진 벡터로 다음 단어를 예측한다. 이때 소프트맥스를 사용해 확률 분포를 만들고 가장 높은 값, 예를 들어 0.7이 나온 단어를 선택하는 식이다. 하지만 이 구조는 오래된 정보가 뒤로 갈수록 점점 희미해져서 30~50칸 전의 단어는 사실상 기억하기 어렵다는 장기의존성 문제가 생긴다. - seq2seq 모델에서는 인코더가 전체 입력 시퀀스를 읽고 그 맥락을 하나의 컨텍스트 벡터로 압축, 컨텍스트 벡터를 디코더가 받아서 출력 시퀀스를 생성한다. 영어 문장을 인코더에 넣으면 전체 문장이 하나의 벡터로 변환되고 그 벡터를 토대로 디코더가 프랑스어 문장을 순서대로 만들어낸다. 하지만 문장이 너무 길면 이 하나의 벡터가 과도하게 많은 정보를 담아야 해서 정보 소실이 발생한다. - 어텐션은 입력 문장을 하나로 압축하지 않는데 인코더가 만들어낸 모든 hidden state도 사용한다. - 사용 = 디코더가 단어를 출력할 때마다 인코더의 전체 hidden state 중에서 어떤 부분을 주목할지 점수를 계산하고 그 점수에 따라 필요한 정보를 골라온다. (hidden state = 인코더의 output인 문맥 벡터) - 예를 들어 번역에서 “it”이라는 단어를 생성하려 할 때, 인코더 입력 중에서 “animal”인지 “street”인지 같은 후보들에 대해 각각 점수를 매기고 가장 관련이 높은 단어를 참고하는 방식이다. - 이렇게 하면 입력 전체를 다시 들여다볼 수 있으므로 문장이 길어도 특정 단어와의 연결 관계를 놓치지 않는다. - 또한 입력을 순차적으로만 처리하지 않고 병렬적으로 계산할 수 있다. RNN처럼 컨베이어 벨트 방식으로 단어를 하나하나 넘기는 대신 전체 입력 문장에서 각 단어와의 연관성을 한 번에 계산하기 때문에 연산 효율이 좋아지고 장기의존성 문제도 해결된다. Contextual Embedding (p.32) - Word Embedding은 “단어 하나 = 벡터 하나”라는 고정 표현을 만든다. - 그림에서 “bank”라는 표기가 좌표평면에 점 하나로 찍혀 있고 이건 돈의 bank인지 강둑의 bank이든 한 벡터에 섞여버린다. - 트랜스포머 기반의 Contextual Embedding은 같은 철자라도 문맥이 바뀌면 다른 벡터를 생성한다. self attention에서 입력 문장의 각 토큰이 주변 모든 토큰을 참고해 자기만의 문맥 표현을 만들기때문에 최종 hidden state(=그 토큰의 임베딩)가 글자가 같아도 문맥에 의존해 달라진다. - “He deposited money in the bank”에서 bank의 벡터는 money, deposit, loan 같은 단어에 높은 어텐션 가중치를 주며 금융 의미 쪽으로 이동한다. - “They had a picnic on the river bank”에서는 river, shore, picnic에 주목해 물가 의미 쪽으로 이동한다. - 좌표평면 그림에서 - bank가 문맥에 따라 “돈/대출” 근처에 위치하기도 하고 “강/물가” 근처에 위치하기도 한다. Transfer Learning (p.33) - 딥러닝의 기본은 복잡한 문제를 풀기 전에 여러 중간 단계를 거쳐 추상적인 개념을 점차 쌓아가는 표현 학습(임베딩). - 비슷한 문제에서 사전 학습된 모델이 이미 학습해 둔 개념들(임베딩 벡터)을 가져와 특정 문제를 푸는 방식이 Transfer Learning. 2. 유사도 Cosine Similarity (p.45-48) 개념 - 두 벡터가 서로 얼마나 같은 방향을 가리키는가? - 수학적 개념: 두 벡터의 내적을 각 벡터의 크기로 나누어 정규화한 값 (정규화=크기는 사라지고 각도 즉 코사인만 남는다) 유사도 판단 (수치) - 두 벡터 사이 각도는 0도이면 코사인은 1이 되고 유사도는 최대치인 1로 계산된다. - 두 벡터가 정반대 방향이라면 각도는 180도가 되고 코사인은 -1이 되고 유사도가 최소가 된다. - 두 벡터가 직각이라면 각도가 90도가 되고 코사인 값이 0이 되고 벡터 사이에 방향성의 유사성이 전혀 없다고 해석한다. - cf) 유사도가 최소이다 vs 방향성의 유사성이 전혀 없다. - 최소는 -1이고 0이기만해도 유사성은 전혀 없다. - 실제 계산에서는 보통 모든 성분이 양수인 경우가 많기 때문에 유사도의 최소값은 0으로 취급하는 경우가 많다고함. 유사도 판단 (실제 case) - 단어의 빈도수가 달라져도 방향이 비슷하다면 코사인 유사도가 높다. - 문서 하나가 apple과 banana를 각각 한 번씩 포함하고 또 다른 문서가 apple과 banana를 세 번씩 포함했다면 두 문서의 벡터는 크기는 다르지만 방향은 같다. - 그래서 코사인 유사도는 1이 되어 두 문서가 같은 주제를 다루고 있다고 판단한다. case study - 문서1은 apple과 banana를 포함해 (1,1,0)/ 문서2는 apple, banana, carrot을 포함해 (1,1,1) / 문서3은 apple과 banana가 여러 번 반복되어 (3,3,0) 벡터로 표현. - 유클리드 거리를 기준으로 보면 문서1은 문서2와 더 가깝지만 코사인 유사도를 기준으로 보면 문서1과 문서3이 더 가깝다. - 코사인 유사도가 벡터 크기의 차이를 무시하고 방향만 보기 때문에 텍스트 데이터처럼 길이가 달라도 같은 주제를 다룰 수 있는 상황에 유용하다(데이터가 크기와는 무관하게 같은 맥락이나 주제를 향하고 있는지를 확인). 3. Transformer Self-Attention 인코딩과 디코딩 (p.55) - 인코딩은 비정형적인 입력을 의미 있는 벡터로 바꾸는 과정이다. - 디코딩은 이 벡터를 기반으로 새로운 대상을 생성하는 과정이다. - 예를 들어 문장을 입력하면 인코더가 문장을 수치 벡터로 변환하고 디코더가 이를 이용해 번역된 문장을 만들어낸다. Query, Key, Value (p.56) - 입력으로 들어온 벡터의 크기가 4×10 - 여기에 가중치 행렬을 곱해서 차원을 줄이거나 변형한다. - 예를 들어 10×5 크기의 가중치 행렬을 곱해주면 입력은 4×5 크기로 변환되어 원래 10차원이었던 단어 임베딩 벡터가 5차원 표현으로 바뀌게 된다. - 선형 변환으로 차원을 바꾼 뒤 Query, Key, Value 벡터로 나눈다. - Query는 “내가 누구를 참고할지, 어디에 집중할지” - Key는 “Query가 참고할 수 있는 정보” - Value는 “실제로 전달될 정보” cf) Key가 Query가 참고할수있는 정보라는게 무슨말인지? - Query는 질문이고, Key는 후보 답변. Query는 지금 단어는 누구한테서 힌트를 얻어야 하지? Key는 이 특징이 지금 단어가 찾는 '누구'랑 얼마나 맞을까? - 예시 “I love pizza” - Query: 현재 내가 집중하는 단어 -> “love” - Key: 문장 안의 모든 단어가 가짐 -> I(Key), love(Key), pizza(Key). - Query("love")가 Key들과 내적을 해서 유사도를 보니 Key("pizza")랑 점수가 높으면 “love는 pizza랑 관련이 크다" / Key("I")랑 점수는 낮으면 “love는 I와는 관련이 약하다” 그럼 “love”라는 단어는 “pizza”의 정보를 더 많이 참고해야겠구나 하고 판단. Self-Attention에서 토큰이 자기 자신과 다른 모든 토큰들 사이의 관련성을 계산하는 법 (p.57-60) 1. Query와 Key를 내적해 4×4 크기의 score 행렬을 만든다. (i, j) 위치의 값은 i번째 토큰이 j번째 토큰을 얼마나 주목해야 하는지 score인데 내적 값이 크면 유사성이 높다는 뜻이고 주목해야 할 대상이라는 뜻이다. (이처럼 토큰들 사이의 관련성을 자기 자신 안에서 계산하기 때문에 Self-Attention이라고한다) 2. 내적 값이 너무 커질 경우 특정 항목만 지나치게 강조될 수 있으므로 score를 Key 차원의 제곱근으로 나누어 스케일링하는 과정을 통해 값의 분산이 안정화해서 학습을 안정화한다. 3. Softmax 함수를 적용해 각 행이 합이 1이 되도록 확률 분포로 바꾼다. 이렇게 변환된 값이 Attention Score로, 각 토큰이 다른 토큰을 얼마나 참고할지를 확률 형태로 표현한 것이다. 4. Value 벡터와 이 Attention Score를 곱한다 즉 원래의 정보(Value)를 점수에 비례해 가중합한 새로운 벡터를 만든다. 이 결과는 원래 토큰 벡터를 업데이트한 것과 같다. 즉, 각 토큰이 문맥 속에서 어떤 다른 토큰과 얼마나 연결되어 있는지를 반영해 다시 표현된 새로운 벡터가 생성된다. Multi-Head Attention (p.61-62) Multi-Head 필요성 - Self-Attention을 한 번만 거치면, 특정한 기준(맥락, 의미, 어휘적 유사성 등)에서만 관계를 포착할 수 있다. - 멀티 헤드 어텐션은 이런 Self-Attention을 여러 개 병렬로 실행해서, 서로 다른 관점에서 입력을 바라볼 수 있도록 한다. 예를 들어 어떤 헤드는 단어의 순서적 맥락에 집중할 수 있고, 또 다른 헤드는 의미적 유사성에 주목할 수 있으며, 또 다른 헤드는 특정 어휘 패턴을 따라가며 관계를 본다. 이렇게 여러 헤드가 만들어내는 다양한 관점을 합치면 일종의 앙상블처럼 작동해서 모델은 훨씬 다차원의 표현을 생성하게된다. 학습 과정 - 각 헤드는 Q, K, V를 각각 독립적인 가중치 행렬로 변환한다. 따라서 같은 입력이라도 헤드마다 Q, K, V가 달라지고 그 결과로 나온 Attention Output도 서로 다르다 즉 헤드별로 서로 다른 방식으로 “무엇을 주목할지”를 학습한다. - 각 헤드의 Attention Output은 보통 4×5와 같은 크기의 행렬로 나오는데 여러 헤드의 출력들을 옆으로 이어붙이는 방식으로 결합한다(Concat). - 단순히 붙인 결과는 각 헤드의 특징이 분리된 채로 남아 있어, 모델이 이를 자연스럽게 활용하기 어려우므로 이어붙인 벡터를 다시 한 번 선형 변환(Linear Transformation)해서 하나의 통합된 표현으로 만든다. 이렇게 하면 맥락 정보, 의미 정보, 어휘 정보 등 다양한 관점의 결과가 하나의 일관된 벡터 공간 안에서 재표현되어 이후 레이어들이 이 표현을 자연스럽게 사용할 수 있다. 정규화, 잔차 연결, Feed Forward Layer, Positional Encoding (p.63-65) 정규화(Normalization) - 정규화는 LayerNorm을 통해 이루어진다. 이는 각 토큰 벡터 차원별 평균과 분산을 정규화해서 입력 분포가 일정하게 유지되도록 만든다. 이렇게 하면 학습이 빠르고 안정적이 되며, 그래디언트 소실이나 폭주를 막을 수 있다. 실제로 Multi-Head Attention 같은 연산을 통과하면 값의 크기가 커지거나 불안정해질 수 있는데, LayerNorm을 거치면서 다시 안정된 값으로 조정된다. 잔차 연결(Residual Connection) - 연산 과정에서 원래 입력 정보를 보존하기 위해 사용된다. 예를 들어 어텐션 결과만 계속 쌓아가면 초기 입력의 정보가 소실될 수 있다. 이를 방지하기 위해 원래 입력을 연산 결과에 더해주는 방식으로 정보 흐름을 유지한다. 이렇게 하면 기존 정보 위에 추가적인 정보를 겹쳐 쌓는 구조가 되고, Gradient가 지나갈 통로도 유지되어 학습이 더 잘 이뤄진다. 즉, 단순히 변형된 표현만 쓰는 게 아니라 원래 입력과 변형된 출력을 함께 사용하는 것이다. Feed Forward Layer - 어텐션만으로는 거의 선형 결합만 수행되므로 모델의 표현력이 부족할 수 있다. 딥러닝의 핵심은 비선형성을 주입하는 것인데, 이를 위해 활성함수(Activation Function)를 사용한다. 트랜스포머에서는 보통 ReLU 같은 비선형 함수를 적용해 입력 표현을 더 복잡하고 풍부하게 바꾼다. 이 과정에서 단순한 선형 결합을 넘어 복잡한 패턴을 학습할 수 있게 된다. Positional Encoding - 어텐션 메커니즘은 모든 토큰을 동시에 바라보기 때문에 토큰의 순서를 직접적으로 알 수 없다. 예를 들어 “나는 밥을 먹었다”와 “밥이 나를 먹었다”는 순서가 바뀌면 의미가 완전히 달라지지만, 어텐션만 사용하면 두 문장을 구분하기 어렵다. 이를 해결하기 위해 입력 임베딩에 순서 정보를 더해주는 것이 Positional Encoding이다. 위치 정보를 단순히 정수로 추가하면 값의 범위가 커져서 다루기 힘들기 때문에, 사인(sin)과 코사인(cos) 함수를 이용해 주기적인 패턴으로 위치를 표현한다. 이렇게 하면 어떤 위치든 간결하게 표현할 수 있고, 모델은 순서를 반영한 연산을 할 수 있다. 사인과 코사인은 시간적, 순차적 데이터를 표현할 때 자주 쓰이는 방식인데, 여기서는 토큰의 위치 정보를 수학적 벡터로 만들어 임베딩과 더해주어 순서를 구분할 수 있도록 돕는다. 디코더에서의 Masked Self-Attention (p.66-67) 마스킹(masking) - 트랜스포머의 기본적인 어텐션 메커니즘은 모든 단어가 한꺼번에 보일 때 서로 간의 문맥을 파악하는 구조지만 디코더는 문장을 생성할 때 미래 단어까지 동시에 볼 수 없도록 제한해야 한다. 예를 들어 "I study"까지 입력이 주어졌다면, 그 시점에서 "AI hard"라는 단어들은 아직 주어지지 않은 정보이므로 모델이 참고하면 안 된다. - 이를 위해 마스킹 과정을 거친다. 어텐션 스코어 매트릭스를 계산할 때, 미래 토큰에 해당하는 위치는 마스크 처리해서 무시한다. 따라서 주어진 입력이 "I study"라면, 모델은 오직 "I"와 "study" 사이의 관계만 학습하고 그 이후 단어와는 내적을 하지 않는데 이렇게 하면 디코더는 항상 현재까지의 단어들만을 기반으로 다음 단어를 예측하게 된다. - 마스킹에서 중요한 연산은 Q(쿼리)와 K(키)의 내적을 통해 어텐션 스코어 행렬을 만드는 것이다. 그런데 마스킹이 적용되면 아직 주어지지 않은 단어는 스코어 계산에서 제외된다. 따라서 어텐션 스코어 행렬에는 주어진 토큰까지만 반영된다. 예를 들어 "I"가 입력이면 자기 자신만 참조할 수 있고, "I study"라면 "I"와 "study"만 참조할 수 있다. - 훈련(학습) 과정에서는 모델이 만든 출력과 실제 정답(ground truth)을 비교하면서 학습한다. 예를 들어 "I study"까지 입력했을 때 다음 단어로 "AI"가 올 확률을 모델이 예측하고, 확률 분포를 생성해서 생성한 확률 분포로부터 얻은 예측값을 실제 정답과 비교해서 Loss를 계산한다. 이 손실을 역전파하면서 모델의 weight가 업데이트된다.


2025-08-13 ⋯ python #2 객체지향 프로그래밍, 병렬처리

1. 객체지향 프로그래밍 property & dataclass (p.139-140) @property - diameter 메서드는 사실 _radius * 2라는 계산을 수행하지만 외부에선 c.diameter라고 쓰면 바로 10이라는 결과를 얻을 수 있다. - @diameter.setter를 사용하면 c.diameter = 20 형태로 diameter을 수정할수있고 - 내부에서는 diameter을 받아 _radius=10으로 변환 저장한다. - fastapi에서 젤많이쓰는 기능이 속성화이다. @dataclass - 보통 클래스를 만들면 __init__으로 생성자, __repr__으로 객체 출력 형식, __eq__로 동등성 비교 등을 직접 정의해야 하는데 @dataclass를 붙이면 이런 메서드들이 자동 생성된다. - Point 클래스는 x, y 좌표만 Point(1,2)로 정의했는데 이 상태로 객체 p1, p2를 생성하고 출력하면 Point(x=1, y=2)처럼 형식맞춰 나온다. - 그리고 == 비교 시 자동으로 True도 나온다. @property - class Order - 주문 정보를 저장하는 클래스 - 속성: beverage, quantity - __slots__를 사용해 이 두 속성만 인스턴스에 저장할 수 있도록 제한했기 때문에 메모리 사용량이 줄고 실수로 다른 속성을 추가하는 것도 방지함. - total_price 메서드 - @property로 정의됨 - 주문 금액을 계산하는 로직을 담고 있지만 속성 접근처럼 쓸 수 있다 즉 order.total_price()가 아니라 order.total_price로 쓸수있다. cf) @property 안썼으면? - @property 사용하면 order.total_price로 괄호 없이 접근했을때, 내부에서 계산된 결과가 바로 반환되고 - @property 사용 안하면 order.total_price()로 호출하면 6000이 나오고 괄호없이 호출하면 메서드 객체 참조만 나온다. @dataclass - class Beverage - 음료 정보를 저장하는 데이터 전용 클래스 - 속성: name(문자열), price(실수형), tags(문자열 리스트) - @dataclass로 자동으로 `__init__`(생성자), `__repr__`(객체를 보기 좋게 출력), `__eq__`(값 비교) 같은 기본 메서드가 생성. cf) @dataclass 안썼으면? - `__init__` : 매개변수를 받아 속성을 초기화 / `__repr__` : 객체를 보기 좋게 문자열로 표현 / `__eq__` : 객체 간 동등성 비교 로직 작성 이렇게 하나하나 추가해야한다. decorator & closer (p.168-169) decorator - 데코레이터 (timer) - 함수 실행시간을 자동으로 측정 - 내부에 wrapper 함수를 정의해서 slow function을 감싼다. - 흐름 - wrapper는 시작시간기록, slow function 실행결과를 result에 저장하고 종료시간 기록, 걸린시간 계산, result를 반환 - @timer -> slow function을 호출하면 사실상 wrapper가 실행된다. wrapper 안에서 slow function이 호출 -> 2초 대기 -> 작업완료 출력 -> 실행시간 result 출력 - 의의 - 함수를 호출하기 전후에 원하는 로직을 끼워 넣어 원래 함수의 기능은 그대로 두고 부가적인 기능을 쉽게 추가할 수 있게. closure - outer()가 실행되면? - x = 10이 만들어지고 inner 함수가 정의됨 - outer()는 inner 함수를 그 자체로 반환함 (inner의 결과를 반환하는게 아니고) - closure = outer()? - closure에 **inner 함수**가 저장 - 이때 inner 함수는 자신이 정의될 당시의 환경(= x=10이 있던 outer의 스코프)을 함께 기억함 - 그래서 outer가 끝나서 x 변수가 사라진 것처럼 보여도 closure()를 실행하면 여전히 x = 10에 접근 가능. - closure는 decorator처럼 @문법을 붙이지 않아도 적용된다. decorator와 closure 함께사용하기 decorator - 데코레이터 measure_time - 실행 시간 측정 - 내부에 wrapper 함수를 정의해서 run_typed을 감싼다. - 흐름 - wrapper는 run_typed 실행결과를 elapsed_time에 저장한 뒤 반환 - @measure_time -> run_typed을 호출하면 사실상 wrapper가 실행된다. wrapper 안에서 run_typed가 호출 -> 실행시간 elapsed_time 출력 closure - measure_time이 실행되면? - wrapper 함수가 정의됨, measure_time은 wrapper 함수를 그 자체로 반환함 (wrapper의 결과를 반환하는게 아니고) - run_typed에 @measure_time이 적용되면? - run_typed 함수 객체가 measure_time의 매개변수 func로 전달 - measure_time 안에서 정의된 wrapper 함수는 자신이 정의될 당시의 환경(자기 바깥 함수의 지역 변수인 func)를 기억 - 그래서 measure_time이 종료되어 원래 지역 변수 func가사라진 것처럼 보여도 wrapper 함수 내부에는 여전히 func에 대한 참조가 살아 있다. 2. 병렬처리 multithreading (p.189) - 스레드가 같은 프로세스 내부에서 실행되며 메모리와 실행 환경을 공유 - 예제 코드 - print_numbers와 print_letters를 각각 thread1 thread2로 실행 - 결과 - 숫자 1부터 5까지와 알파벳 A부터 E까지가 1초 간격으로 번갈아 출력 mutliprocessing (p.191) - 함수가 완전히 독립된 프로세스로 실행 - 예제 코드 - print_numbers와 print_letters를 독립적인 프로세스 process1 process2로 실행 - 결과 - 두 프로세스가 동시에 시작되더라도 실행 타이밍과 OS 스케줄링 우선순위, 프로세스 생성 시점의 지연 때문에 한 프로세스가 먼저 실행을 많이 진행하고 다른 프로세스가 뒤따라 실행되게되고 - 그 결과 숫자 1-5를 전부 찍고 난 후 알파벳 A-E를 찍는 식으로 출력이 묶음 단위로 나타난다. multithreading & mutliprocessing multithreading - 두 스레드가 같은 프로세스 내부에서 실행되며 메모리와 실행 환경을 공유한다. - 예제에서 숫자를 찍는 함수와 알파벳을 찍는 함수 각각이 독립적인 스레드로 동작하지만 동일한 프로세스의 GIL(Global Interpreter Lock)을 공유하기 때문에 한 번에 한 스레드만 실제로 파이썬 바이트코드를 실행한다. - time.sleep(1)로 실행 권한을 번갈아 준 결과 숫자를 하나 찍고 잠시 멈춘 사이 다른 스레드가 알파벳을 찍는 식으로 출력이 교차되고 실행 타이밍에 따라 순서가 조금씩 섞여 나타난다 즉 두 작업이 거의 동시에 진행되는 것처럼 보이지만 사실은 GIL과 sleep 호출에 의해 미세하게 번갈아 실행된다. mutliprocessing - 각 함수가 완전히 독립된 프로세스로 실행된다. - 두 프로세스가 동시에 시작되더라도 실행 타이밍과 OS 스케줄링 우선순위 때문에 한 프로세스가 먼저 실행을 많이 진행하고 다른 프로세스가 뒤따라 실행되게되고 그 결과 숫자 1-5를 전부 찍고 난 후에 알파벳 A-E를 찍는 식으로, 출력이 묶음 단위로 나타나게 된다. ~*사실잘모르겟다...어렵다,,,,*~ 결론 - 멀티스레딩은 하나의 프로세스 안에서 협력적으로 실행을 나누기 때문에 출력이 교차되거나 순서가 섞이기 쉽고, 멀티프로세싱은 프로세스 단위로 완전히 병렬 실행되지만 OS 스케줄링 특성상 한쪽이 먼저 실행을 마쳐 출력이 블록처럼 모이는 경우가 많다. MutClust에서 mutliprocessing 코드 MutClust 예전 utils 코드중에서 병렬처리 코드 있었던거같아서 찾아봣다 흐름은 1. multiprocessing.Pool을 이용해 최대 50개의 프로세스를 동시에 실행할 수 있도록 풀을 생성 2. target_dir 디렉토리 내 파일이 meta_df의 인덱스 이름에 포함되어 있는 경우만 남겨서 mutInfo_files 생성 3. mutInfo_files를 pool.map(process_mutInfo, mutInfo_files)에 전달 - process_mutInfo: 병렬로 process_mutInfo 함수에 의해 처리(mutInfo_files를 읽고 sid, mutInfo_df 생성) 4. key가 sid, 값이 mutInfo_df인 딕셔너리 seq_dict로 만들고 total_df로 정리 결론 - 변이 정보를 병렬 프로세스(50개)로 빠르게 처리하고 결과를 df로 정리해서 저장해놓고 썼다. - 결과파일 저장해놓은뒤로 사용한적없어서 utils에서 빠진거같고 기억에서도 빠진것같다(..)


2025-08-12 ⋯ python #2 리스트 vs 제너레이터 비교 실습

1. 100만 개의 숫자 합 구하기 1) 리스트 방식 - numbers=list(range(1000000)) -> sum(numbers) - 0~999,999를 리스트(numbers)로 만들어 합계를 구함 - sys.getsizeof(numbers) - 리스트 객체의 크기를 바이트 단위로 반환 2) 제너레이터 방식 - gen = number_gen() -> sum(gen) - 제너레이터 객체 생성, 내부적으로 하나씩 값을 생성해 합산 - sys.getsizeof(gen) - 제너레이터 객체의 크기를 바이트 단위로 반환 3) 결과 비교 - list(range(1000000)) - list()로 감싸면 메모리에 100만 개 원소의 배열이 만들어지므로 크기가 크다(O(N)). - gen = number_gen() - 제너레이터 객체는 “다음에 뭘 생산할지에 대한 상태”만 저장하고 실제 값(0, 1, 2, …)을 미리 메모리에 올리지 않아서 크기가 작다(O(1)). 2. 짝수의 제곱 총합을 계산 코드 - even_squares_list = [i * i for i in range(N) if i % 2 == 0] → sum(even_squares_list) - 모든 짝수 제곱을 리스트로 생성 후 리스트의 모든 원소를 합산 - even_square_gen(n) → sum(even_square_gen(N)) - 짝수 제곱을 생성하는 제너레이터 함수를 이용해 짝수 제곱을 하나씩 생성하며 합산 결과 - 리스트 방식의 메모리 사용량이 4167352 bytes로 제너레이터의 메모리 사용량 208 bytes보다 컸다. - 리스트 방식의 sum 연산 실행 시간이 0.0649869441986084 초로 제너레이터 방식의 0.10016107559204102 초보다 빨랐다. - 두 방식의 속도 차이는 여러 번 수행 결과 리스트 방식이 빠른 경우도 있었고, 제너레이터 방식이 빠른 경우도 있음. cf) N = 100000000에서의 비교 - N=100000000 (100배)로 수행 결과 리스트의 sum 연산 실행 시간이 8.67517375946045 초로 제너레이터 방식의 6.631064176559448 초보다 느리게 나옴. - 근데 누가 빨리나와야되고 이런건 없다고하심.


2025-08-12 ⋯ python #1 기본문법, 가상환경, 로깅

1. 기본문법 break와 continue의 차이 (p.29) - break - 0부터 9까지 세는 반복문에서 i가 5가 되는 순간 break를 만나면 그 뒤의 숫자는 전혀 세지 않고 반복이 끝난다. - continue - 0부터 4까지 세는 반복문에서 i가 2인 경우 continue를 만나면 2를 출력하지 않고 바로 다음 숫자인 3으로 넘어가고 반복문 자체는 끝나지 않는다. 가변 인자 (p.78) - *args는 인자들을 하나의 튜플로 묶어서 받는다. - **kwargs는 인자들을 하나의 딕셔너리로 묶어서 받는다. - `mix_example(a, b, *args, **kwargs)`일때 `mix_example(1, 2, 3, 4, 5, name="철수", age=30)` - 1과 2는 매개변수 a와 b에 저장 - a:1, b:2 - 3, 4, 5는 args라는 튜플에 저장 - args: (3,4,5) - name="철수"와 age=30은 kwargs라는 딕셔너리에 저장 - kwargs: {'name':'철수', 'age':30} 클로저 (p.86) 1. multiplier(2)를 호출 2. factor가 2로 고정된 multiply 함수가 만들어짐. 3. 이 함수는 나중에 호출해도 2라는 값을 기억하고 있다. 4. double(10)을 하면 10에 2를 곱한 20이 나온다. 2. 가상환경 지금 환경을 그대로 뜨는 방법 (p.109) - pip freeze > requirements.txt를 하면 현재 환경에 설치된 모든 패키지와 그 버전이 기록되고 - 다른 환경에서 똑같은 설정을 만들고 싶다면 pip install -r requirements.txt를 실행하면 된다. .env (p.115) - 데이터베이스 비밀번호나 API 키처럼 코드에 직접 적으면 안 되는 값들은 .env라는 파일에 따로 저장하고 코드에서는 이 파일을 읽어서 사용하는 것이 안전하다. - 사용법 - from dotenv import load_dotenv로 불러오고 - load_dotenv()를 실행하면 .env 파일 안의 값들이 환경 변수로 등록된다. - os.getenv("DB_USER")로 필요한 값을 꺼낼 수 있다. - 깃허브에 올릴때는 gitignore에 넣어야된다. 3. Logging 실습 문제 .env를 위와같이 작성했을때 app.log에 다음 로그 출력하기 - INFO 레벨 메시지: "앱 실행 시작" - DEBUG 레벨 메시지: "환경 변수 로딩 완료" - ERROR 레벨 메시지: ZeroDivisionError 예외 발생 시 출력 코드 결과 app.log


2025-08-11 ⋯ Devops #1 Python 프로젝트 CI/CD & 클라우드 빌드

실습 - 메이크파일, 린팅, 테스트와 같이 파이썬 프로젝트 스캐폴딩에 필수적인 요소가 포함된 깃허브 저장소를 생성해보자. 그리고 간단하게 코드 포매팅을 수행하도록 메이크파일 스크립트를 작성해보자. - 깃허브 액션을 사용하여 두개 이상의 파이썬 버전에 대해 깃허브 프로젝트 테스트를 수행해보자. - 클라우드 네이티브 빌드 서버(AWS 코드빌드, GCP 클라우드 빌드, 애저 DevOps 파이프라인)를 사용하여 지속적 통합을 수행해보자. - 깃허브 프로젝트를 도커 파일로 컨테이너화하고, 자동으로 컨테이너 레지스트리에 새로운 컨테이너가 등록되도록 만들어보자. - locust 또는 loader io와 같은 부하 테스트 프레임워크를 사용하여 애플리케이션에 대한 간단한 부하 테스트 코드를 작성한다. 그리고 스테이징 브랜치에 변경 사항을 푸시할 때 이 테스트가 자동으로 수행되도록 만들어보자. 1. 파이썬 프로젝트 스캐폴딩 + 메이크파일/린팅/테스트 + 포매팅 새 프로젝트 만들기 (로컬) 최소 패키지/테스트 코드 넣기 개발 도구 설치 파일 - ruff(린터+포매터), pytest(테스트), mypy(타입체크)만 사용 메이크파일 작성 (포매팅/린팅/테스트 일괄 실행) 의존성 설치 & 동작 확인 깃허브 저장소 만들고 푸시 2. 여러 파이썬 버전으로 GitHub Actions 테스트 리포지토리 준비 브랜치 생성 워크플로우 폴더 만들기 CI 설정 파일 생성 - 파일 경로: .github/workflows/ci.yml - 내용: 매트릭스로 3.9~3.12 테스트, Makefile 타깃 사용 커밋 & 푸시 PR 생성 - GitHub에서 ci-setup → main으로 Pull Request 생성 - PR이 생성되면 Actions 탭에서 파이썬 3.9/3.10/3.11/3.12 네 개 잡이 병렬로 도는 걸 볼 수 있음. 배지 추가 - README.md에 아래 한 줄 추가(리포지토리 경로는 본인 것으로 교체) 1. Secrets 불필요: 단순 테스트만 하면 깃허브 액션 기본 권한으로 충분. 2. 기본은 ubuntu-latest지만, OS 매트릭스를 늘리고 싶으면 다음과같이 설정 3. Makefile 없이도 가능. 위 Lint/Type/Test 단계를 ruff/mypy/pytest 직접 실행으로 바꿔도 동작. 3. 클라우드 네이티브 빌드 서버로 CI 1. AWS CodeBuild 리포에 buildspec 추가 - 리포 루트에 buildspec.yml 생성 - 여러 파이썬 버전을 돌리고 싶다면 CodeBuild 프로젝트를 버전별로 2~3개 만들거나, Docker 이미지를 바꿔 실행하는 별도 프로젝트를 추가하는 방식이 단순함. CodeBuild 프로젝트 만들기(콘솔) 2. GCP Cloud Build 리포에 cloudbuild.yaml 추가 트리거 연결 cf) cloudbuild.yaml 예시(루트에 있어야 함) cf2) 여러 파이썬 버전으로 돌리기 – 두 가지 방법 1. 스텝을 여러 개 두기 2. 트리거를 2개 만들고, 각각 Substitution으로 버전 넘기기 - 트리거1: _PY_VERSION=3.9 - 트리거2: _PY_VERSION=3.12 - cloudbuild.yaml에서 ${_PY_VERSION} 사용: 3. Azure DevOps Pipelines 리포에 buildspec 추가 - 리포 루트에 azure-pipelines.yml 생성 Azure DevOps 파이프라인 생성 cf) - 만약 GitHub 권한 에러가 나면? - 좌측 하단 Project settings → Service connections → New service connection → GitHub → Grant access(또는 OAuth) → 연결 생성 - 다시 Pipelines → Create pipeline부터 진행 - 이미 리포에 있어야 하는 파일 예시 - azure-pipelines.yml (다중 파이썬 버전 매트릭스)


2025-08-04 ⋯ Docker #5 kubernetes 환경에 나의 앱을 배포해보자

구조 실습과의 차이? 1. cicd.sh를 쓴다. 2. deploy 디렉토리를 쓴다. 3. docker-build.sh와 docker-push.sh에서 amd였던걸 arm으로 바꿔줬는데 이걸다시 amd로 바꿔준다. ~1. cicd.sh 작성 **(불필요)**~ cicd.sh 사용하는 부분이 나오는데 ppt랑 workspace 디렉토리 안에 아무리찾아봐도 없어서... 일단 챗지피티에넣고 만들었는데 막상 뒤에서는 cicd.sh 쓰는대신 그냥 `kubectl apply -f deploy.yaml` `kubectl apply -f service.yaml` 만 해줬다. 2. deploy 디렉토리 deploy 디렉토리의 deploy.t와 service.t는 각각 .sh로 바꿔준다. - deploy.sh에서 amd를 유지해주고 - service.sh는 원래 8080 돼있었는데 8888 아닌가 싶어서 바꿔줬다. env.properties의 CPU_PLATFORM=amd64으로 설정했고 deploy.sh에서 amd64로 해주고 service.sh에서 port: 8888로 변경했다. 3. docker-build.sh와 docker-push.sh 재수정 arm을 amd로 다시바꿔줫다. 마찬가지 arm을 amd로 바꿔줌. 4. Docker 이미지 빌드, 푸시, kubernetes 환경에 배포 ~이렇게 하면 나와야되는데 계속 404 에러 나옴.~ *해결.. ㅠㅠ* 1. deploy.sh: 챗지피티에서 amd64 떼라고해서 마지막엔 `image: amdp-registry.skala-ai.com/skala25a/${USER_NAME}-posts-get:1.0`도 썼다 2. deploy.yaml: 마찬가지로 amd64 떼라고해서 `amdp-registry.skala-ai.com/skala25a/sk019-posts-get:1.0`도 썼다. 3. defalut.conf는 다음 3가지 버전을 시도했다. 4. cicd.sh -y 스크립트가 존재하지 않는 경우에 `kubectl apply -f deploy.yaml`와 `kubectl apply -f service.yaml`로 대체 가능하대서 그냥 패스했는데 그래도 되는게 맞는지 모르겟음 교수님께 질문사항 디엠 보냈는데 > 우선 deploy를 통해 자신이 만들어놓은 컨테이너 이미지를 클라우드 환경으로 잘 배포했습니다. > > 그리고 service를 통해 나의 컨테이너 내 80포트를 노출하고 있는 nginx를 외부에서 접속 가능하도록 잘 연결했습니다. 이것은 어디서든 접속가능하게 하기 위한 ingress 설정이 있는데 이것은 제가 미리 만들어놓아서 위의 URL로 접속됩니다. > > 단지 내가 외부 접속을 위한 ingress 설정에 등록했던 service 이름인 sk019-posts-get이였는데 sk019-posts-get-svc로 만들어 놓아서 이름만 변경해놓았습니다. 라고 오셔서 확인해보니까 말도안되게 service.yaml이 다음과같이 작성돼있었다 아니근데 위 작업 하면서 쓴 챗지피티 대화창에 'sk019-posts-get' 치면 어디서도 'sk019-posts-get-svc'라는 단어가 없는데....... 어디서 나온건지 모르겟음 아무튼 링크를 확인해보니까 잘들어가있다 ㅎㅎ


2025-08-04 ⋯ Docker #4 자신의 Frontend 개발 코드를 컨테이너로 만들고 이것을 실행시켜 보자

1. nginx:alpine 이미지를 사용 2. 노출 Port는80 3. nginx를실행하는방식은 -nginx -g daemon off; 4. nginx의 routing 설정은 default.conf에설정한다. 1. docker-build.sh와 docker-push.sh 복사 docker-build.sh에서 amd였던걸 arm으로 바꿔줫고 docker-push.sh에서 마찬가지 amd를 arm으로 바꿔줌. 2. Dockerfile과 default.conf 작성 원래 코드에 `COPY src/ /usr/share/nginx/html/`이 없었는데 필요한거아닌가 싶어서 넣어줬다. 3. 파일 구조 index.html이랑 이미지 디렉토리 media는 src 디렉토리에 넣었다. 4. 이미지 push build + docker run 마찬가지 run 주소도 arm으로 넣어줌. 잘 나온다 ㅎㅎㅎ 5. 헷갈리는점 1. docker-build.sh에서 - IMAGE_NAME을 healthcheck-server로 바꿔주라고 ppt에 나와있었는데 안바꾸고 webserver를 썼는데 마지막에 `sudo docker run -d --name posts-get --network bridge -p 8888:80 sk019-posts-get.arm64:1.0` 했을때 제대로 나왔다. - 근데 chatgpt 치니까 IMAGE_NAME="healthcheck-server" 해놓고 `sudo docker run -d --name posts-get` 해버리면 안된다고나옴 빌드한 이미지와 실행한 이미지 이름이 다르다고 근데 원래는 달랐는데 잘되던데... 확인 필요할듯. 2. default.conf는 사실 아래 코드로 바꿔넣어줬었다. index.html;을 추가한것임. 이부분도 확인 필요. *1에 추가: env.properties에서 SERVICE_NAME="posts-get"가 나오긴한데 docker-build.sh와 docker-push.sh 가 앞에 source ./env.properties가 붙는 식으로 진행되면 IMAGE_NAME="posts-get"이 적용되고 이미지 이름이 sk019-posts-get:1.0.0으로 만들어지고, run/push 시 모두 일관성이 유지되는게 맞는데? env.properties를 불러오지도 않고, IMAGE_NAME에 healthcheck-server이 하드코딩 대있어서 연관성을찾기 어려운상태.


2025-08-04 ⋯ Docker #3 레지스트리 접속, 이미지 관리

1. 레지스트리에 접속하고 이미지를 pull/push하기 2. Docker Hub에서 우분투 이미지 받아보기 3. Dockerfile : 명령어 CMD 실습 4. 의문점 정리 CMD? Dockerfile에서 CMD는 도커 컨테이너가 실행될 때 자동으로 수행할 기본 명령어를 지정하는 역할인데 예를 들어 `CMD echo "This is the default command"`는 사용자가 docker run 명령을 통해 별도의 명령을 전달하지 않았을 때 이 기본 명령이 실행된다. 그래서 `sudo docker build -t my-image .`로 이미지를 만들고, `sudo docker run my-image`라고 실행하면 `This is the default command`라는 메시지가 출력됨. Override? (`docker run` 명령에 인자를 추가로 넘기면?) docker run 뒤에 인자를 직접 주면 예를 들어 `sudo docker run my-image echo "Overridden command"` 이렇게 실행하면 `CMD ["echo", "This is the default command"]`를 쓰지 않고, `echo "Overridden command"`를 실행한다. 결론적으로 이렇다: 1. `sudo docker run my-image` -> CMD가 그대로 실행됨 (`echo "This is the default command"`) 2. `sudo docker run my-image echo "Hello"` -> CMD는 무시되고 사용자가 입력한 `echo "Hello"`만 실행됨 CMD 작성 형식? `sudo docker run my-image "Override CMD"`처럼 인자를 넘기면 원래 CMD의 `echo` 명령은 유지되고 `"Override CMD"`라는 문자열이 인자로 전달되어 `echo "Override CMD"`가 실행될 것 같지만 Docker가 전달한 문자열을 실행 가능한 명령어로 인식하지 못하기 때문에 오류가난다. 이문장은 `sudo docker run my-image sh -c "echo Override CMD"`처럼 `sh -c`를 통해 쉘 명령어로 감싸주면 `"Override CMD"`가 정상적으로 출력된다. 결론적으로 이렇다: 1. `CMD ["echo", "Hello"]` 이렇게 (명령어와 인자를 분리해서) 써주거나 2. `sudo docker run my-image sh -c "echo Hello"` /bin/sh -c로 감싸서 실행되거나.