Commit 8675c176 authored by insun park's avatar insun park
Browse files

문서 업데이트

parent a59e2e71
# Part 3: 파이썬 컬렉션 심화 (3주차)
# Part 3: 파이썬 컬렉션 심화
**⬅️ 이전 시간: [Part 2: 파이썬 핵심 문법 마스터하기](./part_2_python_core_syntax.md)** | **➡️ 다음 시간: [Part 4: 파이썬 객체 지향 프로그래밍](./part_4_object_oriented_programming.md)**
**⬅️ 이전 시간: [Part 2: 파이썬 핵심 문법 마스터하기](part_2_python_core_syntax.md)**
**➡️ 다음 시간: [Part 4: 객체 지향 프로그래밍 (OOP)](part_4_object_oriented_programming.md)**
---
## 📜 실습 코드 바로가기
- **`part_2_3_python_syntax_collections.py`**: [바로가기](./source_code/part_2_3_python_syntax_collections.py)
- 본 파트의 모든 예제 코드는 위 파일에서 직접 실행하고 수정해볼 수 있습니다. 리스트, 딕셔너리, 셋 등의 다양한 활용법을 직접 확인해보세요.
---
## 1. 학습 목표 (Learning Objectives)
지난 시간에는 변수, 제어문, 함수 등 파이썬의 기본적인 뼈대를 세웠습니다. 이는 마치 우리가 **목공의 기본 연장(톱, 망치, 드라이버) 사용법**을 익힌 것과 같습니다.
이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.
이번 주차에는 그 연장들을 넣어둘 **'전문가용 공구함(컬렉션)'**을 깊이 있게 탐구합니다. 단순히 물건을 담는 상자가 아니라, 용도에 따라 칸이 나뉘고, 특정 작업에 최적화된 특수 공구들을 꺼내 쓰는 법을 배우게 됩니다. 상황에 맞는 최적의 '데이터 그릇'을 선택하는 능력을 길러, 코드의 효율성과 가독성을 한 단계 끌어올려 보겠습니다.
- 리스트의 다양한 메서드를 활용하여 데이터를 자유자재로 조작할 수 있습니다.
- `sort()``sorted()`의 차이점을 명확히 구분하여 사용합니다.
- `Counter`, `defaultdict`, `deque`, `namedtuple` 등 특수 목적 컬렉션의 장점을 이해하고 적재적소에 활용할 수 있습니다.
- [튜플 언패킹](glossaries/part_3_glossary.md#튜플-언패킹-tuple-unpacking)을 사용하여 간결하고 가독성 높은 코드를 작성합니다.
- 문제 상황에 가장 적합한 [자료구조](glossaries/part_3_glossary.md#자료구조-data-structure)를 선택하여 코드의 효율성을 높일 수 있습니다.
---
## 2. 핵심 키워드 (Keywords)
## 1일차(월): 리스트(List) 심화와 Counter
`리스트 메서드(List Methods)`, `sort() vs sorted()`, `collections.Counter`, `collections.defaultdict`, `collections.deque`, `collections.namedtuple`, `[튜플 언패킹(Tuple Unpacking)](glossaries/part_3_glossary.md#튜플-언패킹-tuple-unpacking)`
### 🎯 오늘의 목표
- 리스트의 핵심 메서드를 능숙하게 사용하여 데이터를 조작합니다.
- `sort()``sorted()`의 차이점을 명확히 이해합니다.
- `collections.Counter`로 데이터의 빈도를 효율적으로 계산합니다.
## 3. 전문가의 공구함, 컬렉션 깊이 알기 (Introduction)
---
지난 시간에 파이썬의 기본 뼈대를 세웠다면, 이번 주차에는 그 연장들을 넣어둘 **'전문가용 공구함(컬렉션)'**을 깊이 있게 탐구합니다. 단순히 데이터를 담는 것을 넘어, 용도에 따라 최적화된 특수 공구들을 꺼내 쓰는 법을 배우게 됩니다. 상황에 맞는 최적의 '데이터 그릇'을 선택하는 능력을 길러, 코드의 효율성과 가독성을 한 단계 끌어올려 보겠습니다.
### 1. 리스트 핵심 메서드 정복
> [!TIP]
> 본 파트의 모든 예제 코드는 `source_code/part_2_3_python_syntax_collections.py` 파일에서 직접 실행해볼 수 있습니다.
| 메서드 | 설명 | 예제 |
|---|---|---|
| [`append(x)`](./glossary.md#리스트list) | 리스트 맨 끝에 항목 `x`를 추가 | `[1, 2].append(3)``[1, 2, 3]` |
| `insert(i, x)`| `i` 위치에 항목 `x`를 삽입 | `[1, 3].insert(1, 2)``[1, 2, 3]` |
| `remove(x)` | 리스트에서 첫 번째로 나타나는 `x`를 삭제 | `[1, 2, 1].remove(1)``[2, 1]` |
| `pop(i)` | `i` 위치의 항목을 제거하고 반환 (생략 시 마지막 항목) | `[1, 2, 3].pop(1)``2` (리스트는 `[1, 3]`) |
| [`sort()`](./glossary.md#sort-vs-sorted) | **원본 리스트**를 오름차순으로 정렬 | `x=[3,1,2]; x.sort()``x``[1,2,3]` |
| `reverse()` | **원본 리스트**의 순서를 뒤집음 | `x=[1,2,3]; x.reverse()``x``[3,2,1]` |
### 2. `.sort()` vs `sorted()`: 원본을 바꿀 것인가, 새것을 얻을 것인가?
---
이 둘의 차이점을 이해하는 것은 매우 중요합니다.
## 4. 리스트(List) 다시 보기
- **`list.sort()` 메서드**: **원본 [리스트](./glossary.md#리스트list) 자체를** 정렬합니다. 반환값은 `None`입니다.
- **`sorted()` 내장 함수**: 정렬된 **새로운 [리스트](./glossary.md#리스트list)를 반환**합니다. 원본 리스트는 바뀌지 않습니다.
> **🎯 심화 목표 1:** 리스트의 핵심 메서드를 능숙하게 사용하고, `sort()`와 `sorted()`의 차이를 명확히 이해하며, `Counter`로 데이터 빈도를 효율적으로 계산합니다.
```python
# .sort() 메서드
my_list = [3, 1, 4, 2]
print(f"sort() 전: {my_list}")
result = my_list.sort()
print(f"sort() 후: {my_list}")
print(f"반환값: {result}") # None
### 4.1. `sort()` vs `sorted()`: 원본을 바꿀 것인가, 새것을 얻을 것인가?
# ⚠️ 흔한 실수: sort()의 반환값을 변수에 담지 마세요!
wrong_list = [3, 1, 4, 2]
wrong_list = wrong_list.sort() # wrong_list.sort()는 None을 반환하므로...
print(f"잘못된 사용: {wrong_list}") # None이 출력됩니다!
- **`list.sort()` 메서드**: **원본 리스트 자체를** 정렬합니다. 반환값은 `None`입니다. (내 방 정리)
- **`sorted()` 내장 함수**: 정렬된 **새로운 리스트를 반환**합니다. 원본은 바뀌지 않습니다. (정리된 모습 촬영)
# sorted() 함수
```python
my_list = [3, 1, 4, 2]
print(f"sorted() 전: {my_list}")
new_list = sorted(my_list)
print(f"sorted() 후 원본: {my_list}") # 원본은 그대로
print(f"반환된 새 리스트: {new_list}")
# wrong_list = my_list.sort() #! 잘못된 사용! wrong_list는 None이 됩니다.
new_list = sorted(my_list) # 올바른 사용
print(f"원본: {my_list}, 정렬된 사본: {new_list}")
```
#### 💡 쉽게 이해하기: `.sort()`는 '내 방 정리', `sorted()`는 '정리된 모습 촬영'
> - **`my_list.sort()`**: '내 방 서랍(원본 리스트) 속 옷을 직접 정리정돈하는 것'과 같습니다. 서랍 속의 내용물 자체가 바뀌고, 정리 행위 자체는 어떤 결과물(반환값)을 남기지 않습니다 (`None`).
> - **`new_list = sorted(my_list)`**: '서랍 속 옷은 그대로 둔 채(원본 유지), 침대 위에 옷을 가지런히 배열해보고(새로운 리스트 생성) 그 모습을 사진으로 찍어(새 변수에 저장) 간직하는 것'과 같습니다.
### 3. 스마트한 개수 세기: `collections.Counter`
리스트나 문자열에 포함된 각 항목이 몇 번 등장하는지 세어야 할 때, [`collections.Counter`](./glossary.md#collectionscounter)를 사용하면 매우 간결하고 효율적입니다.
### 4.2. 스마트한 개수 세기: `collections.Counter`
리스트나 문자열에 포함된 각 항목이 몇 번 등장하는지 세어주는 '똑똑한 재고 관리인'입니다.
```python
from collections import Counter
# 리스트에서 각 과일의 개수 세기
fruit_basket = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
fruit_counts = Counter(fruit_basket)
print(fruit_counts)
# 출력: Counter({'apple': 3, 'banana': 2, 'orange': 1})
# 가장 흔한 항목 2개 찾기
print(fruit_counts.most_common(2))
# 출력: [('apple', 3), ('banana', 2)]
# 특정 항목의 개수 확인 (딕셔너리처럼 사용)
print(f"사과는 몇 개? {fruit_counts['apple']}") # 3
print(f"포도는 몇 개? {fruit_counts['grape']}") # 키가 없어도 에러 없이 0을 반환
fruit_counts = Counter(['apple', 'banana', 'apple'])
print(f"과일 개수: {fruit_counts}")
# 출력: Counter({'apple': 2, 'banana': 1})
print(f"가장 흔한 과일: {fruit_counts.most_common(1)}")
# 출력: [('apple', 2)]
```
#### 💡 쉽게 이해하기: Counter는 '똑똑한 재고 관리인'
> `Counter`는 '재고 관리인'입니다. 과일 바구니(`list`)를 통째로 이 관리인에게 넘겨주면, "사과 3개, 바나나 2개, 오렌지 1개 있습니다"라고 즉시 보고해주는 것과 같습니다. 수작업으로 하나씩 셀 필요가 없어 매우 편리합니다.
---
## 2일차(화): 튜플(Tuple)과 딕셔너리(Dictionary) 심화
## 5. 튜플(Tuple)과 딕셔너리(Dictionary) 깊이 알기
### 🎯 오늘의 목표
- 튜플 언패킹(Unpacking)을 자유자재로 활용합니다.
- `defaultdict`를 사용하여 딕셔너리를 초기화하는 코드를 간소화합니다.
> **🎯 심화 목표 2:** 튜플 언패킹을 자유자재로 활용하고, `defaultdict`로 딕셔너리 초기화 코드를 간소화합니다.
---
### 1. 튜플의 힘: 언패킹(Unpacking)
튜플의 각 항목을 여러 변수에 한 번에 할당하는 기능입니다. 코드를 매우 간결하고 직관적으로 만들어줍니다.
### 5.1. 튜플의 힘: [언패킹(Unpacking)](glossaries/part_3_glossary.md#튜플-언패킹-tuple-unpacking)
튜플의 각 항목을 여러 변수에 한 번에 할당하는 '우아한 소포 전달' 기능입니다.
```python
# 기본 언패킹
name, age, city = ("John Doe", 30, "New York")
print(f"이름: {name}")
# 변수 값 교환(swap)
a, b = 10, 20
a, b = b, a # 임시 변수 없이 값을 교환
print(f"a: {a}, b: {b}") # a: 20, b: 10
a, b = b, a # a=20, b=10
# `*`를 이용한 언패킹
numbers = (1, 2, 3, 4, 5)
first, *rest, last = numbers
print(f"first: {first}") # 1
print(f"rest: {rest}") # [2, 3, 4] (리스트로 반환)
print(f"last: {last}") # 5
first, *rest, last = (1, 2, 3, 4, 5)
# first: 1, rest: [2, 3, 4], last: 5
```
#### 💡 쉽게 이해하기: 튜플 언패킹은 '우아한 소포 전달'
> 함수가 `("John Doe", 30, "New York")`이라는 소포(튜플)를 반환할 때, 우리가 `name`, `age`, `city`라는 세 사람이 팔을 벌리고 서서 각자의 물건을 한 번에 받는 모습과 같습니다. 임시 변수라는 창고에 일단 내려놓고 하나씩 꺼낼 필요 없이, 각자의 위치에 맞게 내용물을 바로 전달받아 코드가 매우 깔끔해집니다.
### 2. 키가 없을 때의 구원투수: `collections.defaultdict`
일반 [딕셔너리](./glossary.md#딕셔너리dictionary)는 존재하지 않는 키에 접근하면 `KeyError`가 발생합니다. [`defaultdict`](./glossary.md#collectionsdefaultdict)는 키가 없을 경우, **지정된 기본값을 자동으로 생성**하여 이러한 오류를 방지하고 코드를 간결하게 만들어줍니다.
### 5.2. 키가 없을 때의 구원투수: `collections.defaultdict`
존재하지 않는 키에 접근할 때 `KeyError` 대신 지정된 기본값을 자동으로 생성하는 '자동 완성 노트'입니다. 데이터 그룹핑 시 `if`문을 줄여 코드를 매우 간결하게 만듭니다.
```python
from collections import defaultdict
# 일반 딕셔너리로 리스트에 항목 추가하기
regular_dict = {}
words = ['apple', 'ant', 'bee', 'banana', 'cat']
for word in words:
first_char = word[0]
if first_char not in regular_dict:
regular_dict[first_char] = [] # 키가 없을 때 빈 리스트를 직접 생성
regular_dict[first_char].append(word)
print(f"일반 딕셔너리: {regular_dict}")
# defaultdict 사용하기 (훨씬 간결!)
# 키가 없을 때 기본값으로 빈 리스트(list)를 생성하도록 지정
default_dict = defaultdict(list)
for word in words:
default_dict[word[0]].append(word) # if문 없이 바로 append 가능
print(f"defaultdict 사용: {default_dict}")
# 키가 없을 때 빈 리스트(list)를 기본값으로 생성
d = defaultdict(list)
d['a'].append('apple')
d['b'].append('banana')
d['a'].append('ant')
print(d) # defaultdict(<class 'list'>, {'a': ['apple', 'ant'], 'b': ['banana']})
```
#### 💡 쉽게 이해하기: defaultdict는 '자동 완성 노트'
> 일반 딕셔너리가 '일반 노트'라면, `defaultdict`는 '자동 완성 기능이 있는 마법 노트'입니다.
>
> 일반 노트에서는 'a'라는 페이지가 없으면 직접 'a' 페이지를 만들고 줄을 그은 뒤(`if 'a' not in...: regular_dict['a'] = []`), 내용을 써야 합니다.
>
> 하지만 `defaultdict(list)`라는 마법 노트에서는, 존재하지 않는 'a' 페이지를 펼치려는 순간, 노트가 자동으로 'a'라는 제목의 새 페이지를 만들고 목록을 쓸 수 있도록 준비해줍니다. 우리는 `if`문 확인 없이 그냥 내용을 추가하기만 하면 됩니다.
**언제 `defaultdict`를 사용할까?**
- **데이터 그룹핑**: 학생들을 학급별로, 단어들을 첫 글자별로 묶는 등, 특정 기준에 따라 아이템을 리스트나 셋에 누적할 때 `if`문 없이 깔끔한 코드를 작성할 수 있습니다.
- **빈도수 계산**: 각 항목의 출현 횟수를 셀 때, 키가 없을 때마다 0으로 초기화하는 코드를 생략할 수 있습니다. (`defaultdict(int)`)
---
## 3일차(수): 셋(Set)과 특수 목적 컬렉션
### 🎯 오늘의 목표
- 셋(Set)을 이용한 집합 연산을 마스터합니다.
- 양방향 큐 자료구조인 `deque`의 사용법을 익힙니다.
- 이름으로 항목에 접근하는 `namedtuple`의 편리함을 학습합니다.
---
### 1. 셋(Set)으로 다루는 집합 연산
| 연산자 | 메서드 | 설명 |
|---|---|---|
| `A | B` | `A.union(B)` | **합집합**: A와 B의 모든 항목 |
| `A & B` | `A.intersection(B)` | **교집합**: A와 B에 공통으로 있는 항목 |
| `A - B` | `A.difference(B)` | **차집합**: A에는 있지만 B에는 없는 항목 |
| `A ^ B` | `A.symmetric_difference(B)` | **대칭차집합**: A 또는 B에만 있는 항목 |
```python
set_a = {1, 2, 3, 4}
set_b = {3, 4, 5, 6}
print(f"합집합: {set_a | set_b}")
print(f"교집합: {set_a & set_b}")
print(f"차집합: {set_a - set_b}")
print(f"대칭차집합: {set_a ^ set_b}")
```
## 6. 특수 목적 컬렉션
### 2. 고속 큐(Queue): `collections.deque`
[`deque`](./glossary.md#collectionsdeque)(데크)는 "double-ended queue"의 약자로, 리스트와 달리 **양쪽 끝에서 항목을 추가하거나 제거하는 속도가 매우 빠른** 자료구조입니다.
> **🎯 심화 목표 3:** 양방향 큐 `deque`와 이름으로 접근하는 `namedtuple`의 편리함을 학습합니다.
리스트에서 `my_list.pop(0)``my_list.insert(0, ...)` 연산은 모든 항목을 한 칸씩 뒤로 밀어야 해서 매우 비효율적입니다. `deque`는 이런 단점을 완벽하게 보완합니다.
### 6.1. 고속 큐(Queue): `collections.deque`
데이터의 **양쪽 끝에서 항목을 추가(`append`, `appendleft`)하거나 제거(`pop`, `popleft`)하는 속도가 매우 빠른** '효율적인 대기줄'입니다. 최근 사용 목록(LRU 캐시) 등을 구현할 때 유용합니다.
```python
from collections import deque
d = deque([1, 2, 3])
print(f"초기 deque: {d}")
d.append(4) # 오른쪽 끝에 추가 (리스트의 append와 동일)
print(f"append(4): {d}")
d.appendleft(0) # 왼쪽 끝에 추가 (매우 빠름!)
print(f"appendleft(0): {d}")
right_val = d.pop() # 오른쪽 끝 항목 제거
print(f"pop(): {right_val}, 결과: {d}")
left_val = d.popleft() # 왼쪽 끝 항목 제거 (매우 빠름!)
print(f"popleft(): {left_val}, 결과: {d}")
# 최대 3개까지만 저장되는 deque
recent_plays = deque(maxlen=3)
recent_plays.append("Hype Boy")
recent_plays.append("Ditto")
recent_plays.append("OMG")
print(recent_plays) # deque(['Hype Boy', 'Ditto', 'OMG'], maxlen=3)
recent_plays.append("Super Shy") # 'Hype Boy'는 자동으로 삭제됨
print(recent_plays) # deque(['Ditto', 'OMG', 'Super Shy'], maxlen=3)
```
#### 💡 쉽게 이해하기: deque는 '효율적인 대기줄'
> - **리스트**: '한 줄로 서 있는 긴 대기줄'입니다. 맨 앞사람(`list[0]`)이 빠져나가면, 뒤에 있는 모든 사람이 한 칸씩 앞으로 움직여야 합니다. 매우 비효율적이죠.
> - **`deque`**: '양쪽 끝에 문이 달린 대기 공간'입니다. 사람들이 앞문으로든 뒷문으로든 들어오고 나갈 수 있어서, 다른 사람들의 위치 이동 없이 입구와 출구에서 빠르게 작업이 이루어집니다. 데이터의 앞뒤에서 추가/삭제가 빈번할 때 `deque`를 쓰는 이유입니다.
### 3. 이름을 가진 튜플: `collections.namedtuple`
튜플의 각 항목에 이름을 붙여, 인덱스뿐만 아니라 **이름으로도 접근**할 수 있게 만든 자료구조입니다. 코드의 가독성을 획기적으로 높여줍니다.
### 6.2. 이름을 가진 튜플: `collections.namedtuple`
튜플의 각 항목에 이름을 붙여, 인덱스(`p[0]`)뿐만 아니라 **이름(`p.x`)으로도 접근**할 수 있게 만든 '이름표가 붙은 칸막이 상자'입니다. 코드의 가독성을 획기적으로 높여줍니다.
```python
from collections import namedtuple
# 'Point'라는 이름의 namedtuple 클래스 생성
Point = namedtuple('Point', ['x', 'y', 'z'])
p1 = Point(10, 20, 30)
print(f"p1: {p1}")
# 인덱스로 접근 (튜플처럼)
print(f"p1[0]: {p1[0]}")
# 이름으로 접근 (객체처럼!)
print(f"p1.y: {p1.y}")
print(f"p1.z: {p1.z}")
Point = namedtuple('Point', ['x', 'y'])
p1 = Point(10, 20)
print(f"좌표: ({p1.x}, {p1.y})") # 인덱스보다 훨씬 명확함
```
#### 💡 쉽게 이해하기: namedtuple은 '이름표가 붙은 칸막이 상자'
> - **[튜플](./glossary.md#튜플tuple)**: `(10, 20, 30)`은 '내용물만 있는 투명한 칸막이 상자'입니다. 첫 번째 칸, 두 번째 칸... 하고 순서로만 내용물을 구분할 수 있습니다.
> - **[`namedtuple`](./glossary.md#collectionsnamedtuple)**: `Point(x=10, y=20, z=30)`은 각 칸마다 'x좌표', 'y좌표', 'z좌표'라고 **이름표를 붙여준 상자**입니다. `p1.y`처럼 이름으로 내용물을 꺼낼 수 있으니, `p1[1]`보다 훨씬 의미가 명확하고 실수를 줄여줍니다.
---
## 4일차(목): 파이썬 자료구조 종합 실습
### 🎯 오늘의 목표
- 지금까지 배운 여러 자료구조를 활용하여 실제 미니 문제를 해결합니다.
---
## 7. 직접 해보기 (Hands-on Lab)
지금까지 배운 여러 자료구조를 활용하여 다음 문제들을 해결해보세요.
### 문제 1: 최빈 단어 분석기
**요구사항**: 주어진 영어 텍스트에서 가장 많이 등장하는 단어 3개를 찾아 출력하세요. 단, 대소문자는 구분하지 않고, 구두점은 무시합니다. (`collections.Counter` 활용)
......@@ -259,27 +135,14 @@ print(f"p1.z: {p1.z}")
import re
from collections import Counter
text = """
Python is an interpreted, high-level, general-purpose programming language.
Python's design philosophy emphasizes code readability with its notable use of
significant whitespace. Its language constructs and object-oriented approach
aim to help programmers write clear, logical code for small and large-scale projects.
"""
# 풀이
# 1. 정규표현식으로 단어만 추출하고 소문자로 변환
text = """Python is an interpreted, high-level, general-purpose programming language."""
words = re.findall(r'\w+', text.lower())
# 2. Counter로 단어 빈도 계산
word_counts = Counter(words)
# 3. 가장 흔한 단어 3개 출력
print(f"가장 많이 나온 단어 3개: {word_counts.most_common(3)}")
# 예상 출력: [('python', 3), ('code', 2), ('its', 2)]
```
### 문제 2: 연락처 그룹 관리
**요구사항**: 친구들의 이름을 그룹('가족', '회사' 등)별로 관리하는 프로그램을 만드세요. 그룹에 친구를 추가하는 기능을 구현하세요. (`collections.defaultdict` 활용)
**요구사항**: 친구들의 이름을 그룹('가족', '회사' 등)별로 관리하고 출력하는 `ContactManager` 클래스를 만드세요. (`collections.defaultdict` 활용)
```python
from collections import defaultdict
......@@ -287,30 +150,21 @@ from collections import defaultdict
class ContactManager:
def __init__(self):
self.contacts = defaultdict(list)
def add_contact(self, name, group):
self.contacts[group].append(name)
print(f"'{name}'님을 '{group}' 그룹에 추가했습니다.")
def display_contacts(self):
for group, names in self.contacts.items():
print(f"--- {group} 그룹 ---")
for name in names:
print(f"- {name}")
print(f"- {group}: {', '.join(names)}")
# 풀이
# 사용 예시
manager = ContactManager()
manager.add_contact("엄마", "가족")
manager.add_contact("아빠", "가족")
manager.add_contact("김대리", "회사")
manager.add_contact("이과장", "회사")
manager.add_contact("동생", "가족")
print("\n--- 전체 연락처 ---")
manager.add_contact("엄마", "가족"); manager.add_contact("아빠", "가족")
manager.add_contact("김대리", "회사"); manager.add_contact("이과장", "회사")
manager.display_contacts()
```
### 문제 3: 최근 재생 목록
**요구사항**: 노래를 재생할 때마다 '최근 재생 목록'에 추가합니다. 이 목록은 최대 5개까지만 유지됩니다. 5개를 초과하면 가장 오래된 노래가 자동으로 삭제되어야 합니다. (`collections.deque` 활용)
**요구사항**: 노래를 재생하고 최근 5곡의 재생 목록을 보여주는 `Playlist` 클래스를 만드세요. 5개를 초과하면 가장 오래된 노래가 자동으로 삭제되어야 합니다. (`collections.deque` 활용)
```python
from collections import deque
......@@ -318,60 +172,39 @@ from collections import deque
class Playlist:
def __init__(self, max_size=5):
self.playlist = deque(maxlen=max_size)
def play(self, song):
self.playlist.append(song)
print(f"▶️ 재생: {song}")
def show_recent(self):
print("\n--- 최근 재생 목록 ---")
for song in reversed(self.playlist): # 최근 항목부터 보여주기
print(f"- {song}")
# 풀이
playlist = Playlist()
playlist.play("아이유 - 라일락")
playlist.play("BTS - Dynamite")
playlist.play("BLACKPINK - 뚜두뚜두")
playlist.play("폴킴 - 모든 날, 모든 순간")
playlist.play("AKMU - 어떻게 이별까지 사랑하겠어")
playlist.show_recent()
print(f"최근 재생 목록: {list(self.playlist)}")
# 6번째 노래 추가 -> 가장 오래된 '라일락'이 삭제됨
playlist.play("NewJeans - Hype Boy")
# 사용 예시
playlist = Playlist(max_size=3)
playlist.play("라일락"); playlist.play("Dynamite"); playlist.play("뚜두뚜두")
playlist.play("Hype Boy") # '라일락' 삭제됨
playlist.show_recent()
```
---
## 5일차(금): 3주차 리뷰 및 Q&A
### 🎯 오늘의 목표
- 3주차에 배운 컬렉션 심화 내용을 정리하고, 언제 어떤 자료구조를 써야 할지 감을 잡습니다.
- 궁금했던 점을 질문하고 동료들과 함께 해결합니다.
---
## 8. 되짚어보기 (Summary)
### 컬렉션 선택 가이드: 어떤 공구를 언제 쓸까?
3주차 동안 우리는 파이썬의 강력한 컬렉션들을 배웠습니다. 이제 어떤 상황에 어떤 자료구조를 선택하는 것이 가장 효율적인지 한눈에 정리해봅시다.
어떤 상황에 어떤 [자료구조](glossaries/part_3_glossary.md#자료구조-data-structure)를 선택하는 것이 가장 효율적인지 한눈에 정리해봅시다.
| 자료구조 | 주요 특징 | 핵심 사용 사례 |
|---|---|---|
| **`list`** | 순서 O, 변경 O | 순서가 중요하고, 데이터 추가/삭제/변경이 잦을 때 (e.g., 쇼핑 목록, 작업 큐) |
| **`tuple`** | 순서 O, 변경 X | 순서가 중요하지만, 내용이 절대 바뀌면 안 될 때 (e.g., 좌표, DB 레코드, 상수 모음) |
| **`dict`** | 순서 X (3.7+부터는 입력 순서 유지), Key-Value, 변경 O | 의미 있는 키(Key)로 데이터를 빠르게 찾고 싶을 때 (e.g., 사용자 정보, JSON 데이터) |
| **`set`** | 순서 X, 중복 X, 변경 O | 중복 제거하거나, 항목의 존재 여부를 빠르게 확인할 때, 집합 연산이 필요할 때 |
| **`Counter`** | `dict`의 자식 | 항목 별 개수를 세거나, 가장 빈번한 항목을 찾을 때 (e.g., 단어 빈도수 분석) |
| **`defaultdict`** | `dict`의 자식 | `KeyError` 없이, 키가 없을 때 기본값을 자동으로 생성하고 싶을 때 (e.g., 데이터 그룹핑) |
| **`deque`** | 양방향 큐, `list`와 유사 | 데이터의 양쪽 끝에서 추가/삭제가 매우 빈번할 때 (e.g., 최근 사용 목록, 너비 우선 탐색) |
| **`namedtuple`** | `tuple`의 자식 | 튜플에 이름표를 붙여 가독성을 극대화하고 싶을 때 (e.g., 간단한 데이터 객체 정의) |
| **[`list`](glossaries/part_3_glossary.md#리스트-list)** | 순서 O, 변경 O | 순서가 중요하고, 데이터 추가/삭제/변경이 잦을 때 |
| **[`tuple`](glossaries/part_3_glossary.md#튜플-tuple)** | 순서 O, 변경 X | 내용이 바뀌면 안 되는 값의 묶음 (e.g., 좌표, DB 레코드) |
| **[`dict`](glossaries/part_3_glossary.md#딕셔너리-dictionary)** | Key-Value, 순서 X | 의미 있는 키(Key)로 데이터를 빠르게 찾고 싶을 때 (e.g., 사용자 정보) |
| **`set`** | 순서 X, 중복 X | 중복 제거, 항목의 존재 여부 확인, 집합 연산이 필요할 때 |
| **`Counter`** | `dict`의 자식 | 항목 별 개수를 세거나, 가장 빈번한 항목을 찾을 때 (e.g., 단어 빈도수) |
| **`defaultdict`** | `dict`의 자식 | 키가 없을 때 기본값을 자동으로 생성하여 그룹핑 등을 간소화할 때 |
| **`deque`** | 양방향 큐 | 양쪽 끝에서 추가/삭제가 빈번할 때 (e.g., 최근 목록 관리) |
| **`namedtuple`** | `tuple`의 자식 | 인덱스 대신 이름으로 데이터에 접근하여 가독성을 높이고 싶을 때 |
이러한 자료구조의 특징을 잘 이해하고 적재적소에 사용하면, 여러분의 파이썬 코드는 훨씬 더 효율적이고 읽기 좋아질 것입니다.
---
컬렉션의 세계를 깊이 탐험한 것을 축하합니다! 이제 여러분은 데이터를 단순히 담는 것을 넘어, 특성에 맞게 분류하고 효율적으로 관리하는 능력을 갖추게 되었습니다.
다음 시간에는 지금까지 배운 모든 문법과 자료구조를 '객체'라는 하나의 단위로 묶어, 더 크고 체계적인 프로그램을 설계하는 '객체 지향 프로그래밍(OOP)'의 세계로 떠나보겠습니다.
## 9. 더 깊이 알아보기 (Further Reading)
**➡️ 다음 시간: [Part 4: 객체 지향 프로그래밍 (OOP)](part_4_object_oriented_programming.md)**
\ No newline at end of file
- [파이썬 공식 문서: `collections` — 컨테이너 데이터형](https://docs.python.org/ko/3/library/collections.html)
- [파이썬 공식 튜토리얼: 자료구조 심화](https://docs.python.org/ko/3/tutorial/datastructures.html)
\ No newline at end of file
# Part 4: 객체 지향 프로그래밍 (OOP) 마스터하기 (4주차)
# Part 4: 객체 지향 프로그래밍 (OOP) 마스터하기
**⬅️ 이전 시간: [Part 3: 파이썬 컬렉션 심화](./part_3_python_collections.md)** | **➡️ 다음 시간: [Part 5: AI 핵심 라이브러리](./part_5_ai_core_libraries.md)**
**⬅️ 이전 시간: [Part 3: 파이썬 컬렉션 심화](./part_3_python_collections.md)**
**➡️ 다음 시간: [Part 5: AI 핵심 라이브러리](part_5_ai_core_libraries.md)**
---
## 📜 실습 코드 바로가기
- **`part_4_object_oriented_programming.py`**: [바로가기](./source_code/part_4_object_oriented_programming.py)
- 본 파트의 모든 예제 코드는 위 파일에서 직접 실행하고 수정해볼 수 있습니다. 클래스, 상속, 다형성 등 OOP의 핵심 개념을 코드로 확인해보세요.
---
지금까지 우리는 파이썬의 문법과 다양한 데이터 구조(컬렉션)를 배웠습니다. 이는 프로그래밍의 '재료'를 다듬는 법을 익힌 것과 같습니다. 이번 주차에는 이 재료들을 조합하여 '요리'를 하는 법, 즉 더 크고 체계적인 프로그램을 만드는 '[객체 지향 프로그래밍(OOP)](./glossary.md#객체-지향-프로그래밍oop)' 패러다임을 학습합니다.
## 1. 학습 목표 (Learning Objectives)
**왜 OOP가 AI 개발에서 중요할까요?**
이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.
여러분이 앞으로 사용하게 될 `Scikit-learn`, `PyTorch` 같은 AI 라이브러리의 동작 방식이 바로 OOP에 기반하기 때문입니다.
- 클래스(설계도)를 정의하고, 그로부터 고유한 속성과 행동을 가진 객체(제품)를 생성할 수 있습니다.
- 상속을 통해 기존 클래스의 기능을 재사용하고, 메서드 오버라이딩으로 기능을 확장/변경할 수 있습니다.
- 다형성을 활용하여 유연하고 확장성 높은 코드를 작성할 수 있습니다.
- 캡슐화로 데이터를 안전하게 보호하고, 추상화로 클래스의 필수 기능을 강제할 수 있습니다.
- 현실 세계의 문제를 객체들 간의 상호작용으로 모델링하여 체계적인 프로그램을 설계할 수 있습니다.
- `model = RandomForestClassifier()`: 이 코드는 `RandomForestClassifier`라는 **클래스(설계도)**`model`이라는 **객체(실체)**를 만드는 과정입니다.
- `model.fit(X, y)`: `model` 객체가 가진 `fit`이라는 **메서드(행동)**를 호출하여 모델을 학습시킵니다.
## 2. 핵심 키워드 (Keywords)
이처럼 OOP를 이해하면, 복잡한 AI 라이브러리의 구조를 꿰뚫어 보고, 각 부분이 어떻게 상호작용하는지 명확하게 파악할 수 있습니다.
`클래스(Class)`, `객체(Object)`, `상속(Inheritance)`, `다형성(Polymorphism)`, `캡슐화(Encapsulation)`, `추상화(Abstraction)`, `@dataclass`, `super()`, `매직 메서드(Magic Methods)`
### 💡 쉽게 이해하기: OOP는 '붕어빵 비즈니스'입니다
> OOP의 여러 개념을 이해하기 위해, 우리는 '붕어빵 장사'라는 비즈니스 모델을 사용할 것입니다. 앞으로 나오는 모든 개념을 이 붕어빵 가게에 빗대어 생각해 보세요!
>
> - **[클래스(Class)](./glossary.md#클래스class)**: 붕어빵의 모양, 크기, 굽는 방식을 정의하는 **'붕어빵 틀'** 그 자체입니다.
> - **[객체(Object)](./glossary.md#객체object--인스턴스instance)**: 붕어빵 틀에서 막 찍어낸, 세상에 단 하나뿐인 **'붕어빵'** 한 개입니다.
> - **[속성(Attribute)](./glossary.md#속성attribute)**: 각각의 붕어빵이 가진 고유한 특징, 예를 들어 '팥 앙금', '슈크림 앙금' 같은 **'속재료'** 입니다.
> - **[메서드(Method)](./glossary.md#메서드method)**: 붕어빵 틀을 이용해 할 수 있는 행동, 즉 **'굽기()', '뒤집기()', '포장하기()'** 같은 기능입니다.
## 3. OOP와 함께 '붕어빵 비즈니스' 시작하기 (Introduction)
파이썬은 모든 것이 객체(Object)로 이루어진 강력한 객체 지향 프로그래밍(OOP) 언어입니다. Scikit-learn, PyTorch 등 우리가 사용할 대부분의 AI 라이브러리는 OOP 원칙에 기반하여 설계되었습니다. OOP를 이해하면 라이브러리의 구조를 파악하고 더 깊이 있게 활용할 수 있으며, 재사용과 확장이 용이한 코드를 직접 작성할 수 있게 됩니다.
지금까지 우리는 프로그래밍의 '재료'를 다듬는 법을 익혔습니다. 이번 주차에는 이 재료들을 조합하여 '요리'를 하는 법, 즉 더 크고 체계적인 프로그램을 만드는 **[객체 지향 프로그래밍(OOP)](glossaries/part_4_glossary.md#객체-지향-프로그래밍oop)** 패러다임을 학습합니다.
---
Scikit-learn, PyTorch 등 대부분의 AI 라이브러리는 OOP에 기반하며, `model = RandomForestClassifier()` 와 같은 코드는 OOP를 이해해야 그 구조를 꿰뚫어 볼 수 있습니다.
## 1일차(월): 클래스와 객체 - OOP의 시작
> **💡 핵심 비유: OOP는 '붕어빵 비즈니스'입니다**
> OOP의 모든 개념을 '붕어빵 장사'에 빗대어 이해해 봅시다!
> - **클래스(Class)**: 붕어빵을 만드는 **'붕어빵 틀'**
> - **객체(Object)**: 붕어빵 틀에서 찍어낸 **'붕어빵'** 하나하나
> - **속성(Attribute)**: 붕어빵의 '팥', '슈크림' 같은 **'속재료'**
> - **메서드(Method)**: 붕어빵 틀의 **'굽기()', '뒤집기()'** 같은 **'행동'**
### 🎯 오늘의 목표
- 클래스(Class)와 객체(Object)의 개념을 이해하고 직접 클래스를 정의합니다.
- [`__init__`](./glossary.md#init)[`self`](./glossary.md#self)를 사용하여 객체의 고유한 상태를 만듭니다.
- [`@dataclass`](./glossary.md#dataclass) 데코레이터로 상용구(boilerplate) 코드를 획기적으로 줄입니다.
> [!TIP]
> 본 파트의 모든 예제 코드는 `source_code/part_4_object_oriented_programming.py` 파일에서 직접 실행해볼 수 있습니다.
---
### 1. 클래스(Class)와 객체(Object)
- **[클래스(Class)](./glossary.md#클래스class)**: 객체를 만들기 위한 **'설계도'** 또는 '틀'. 객체가 가질 속성(데이터)과 행동(메서드)을 정의합니다. (예: '자동차' 설계도, **'붕어빵 틀'**)
- **[객체(Object)/인스턴스(Instance)](./glossary.md#객체object--인스턴스instance)**: 클래스로부터 생성된 **'실체'**. '자동차' 클래스로부터 '빨간색 페라리', '검은색 아반떼' 같은 구체적인 객체를 만듭니다. (예: **'팥 붕어빵', '슈크림 붕어빵'**)
```python
# 'Dog'라는 이름의 클래스를 정의합니다.
class Dog:
# 클래스 속성: 모든 Dog 객체가 공유하는 값
species = "Canis familiaris"
# 초기화 메서드: 객체가 생성될 때 자동으로 호출되어 속성을 초기화합니다.
def __init__(self, name: str, age: int):
# 인스턴스 속성: 각 객체마다 고유하게 가지는 값
self.name = name
self.age = age
# 인스턴스 메서드: 객체가 수행할 수 있는 행동
def speak(self, sound: str):
return f"{self.name}가 '{sound}' 하고 짖습니다."
```
> **붕어빵 비유**:
> - `__init__`: 붕어빵 틀(`Dog`)에 반죽을 붓고, 각 붕어빵마다 다른 앙금(`name`, `age`)을 넣는 과정입니다.
> - `self`: 지금 막 만들어지고 있는 **'바로 그 붕어빵'** 자신을 의미합니다. 이 `self` 덕분에 "보리"라는 붕어빵과 "짱구"라는 붕어빵이 서로 다른 이름(`self.name`)을 가질 수 있습니다.
## 4. 클래스와 객체: 붕어빵 틀과 붕어빵
### 2. `self`의 정체
`self`**메서드를 호출한 객체 자기 자신**을 가리키는, 파이썬 OOP의 핵심 규칙입니다.
> **🎯 1일차 목표:** 클래스(Class)와 객체(Object)의 개념을 이해하고, `__init__`과 `self`를 사용하여 객체의 고유한 상태를 만듭니다.
#### 💡 쉽게 이해하기: `self`는 객체의 '나 자신'이라는 대명사
> "보리야, 짖어봐!" 라고 `bory.speak("멍멍")`을 실행하면, `Dog` 클래스는 `bory`라는 객체를 `self`라는 이름으로 받습니다. 그리고 `speak` 메서드 안에서 `self.name`이라고 쓰는 것은, `bory` 객체가 " **나(bory)의** 이름은 '보리'야 " 라고 자기 자신의 속성에 접근하는 것과 같습니다. 이 `self` 덕분에 수많은 Dog 객체들이 각자 자신의 이름과 나이를 헷갈리지 않고 사용할 수 있습니다.
- 메서드를 정의할 때 첫 번째 매개변수로 반드시 `self`를 써주어야 합니다.
- `bory.speak("멍멍")` 코드는 내부적으로 `Dog.speak(bory, "멍멍")`과 같이 동작합니다. `self` 매개변수로 `bory` 객체 자신이 전달되는 것입니다.
### 3. 보일러플레이트 줄이기: `@dataclass`
데이터를 담는 용도의 클래스를 만들 때, `__init__`, `__repr__` (객체 출력) 같은 반복적이고 지루한 코드(Boilerplate)를 자동으로 생성해주는 매우 유용한 [데코레이터](./glossary.md#데코레이터decorator)입니다.
#### 💡 쉽게 이해하기: `@dataclass`는 '클래스 자동 생성기'
> [`@dataclass`](./glossary.md#dataclass)는 마치 **'클래스의 귀찮은 부분들을 알아서 만들어주는 기계'**와 같습니다. 우리는 `name: str`, `age: int`처럼 필요한 속성들만 선언해주면, 이 기계가 객체를 초기화하는 `__init__` 메서드나, 객체를 예쁘게 출력해주는 `__repr__` 메서드 같은 것들을 보이지 않는 곳에서 자동으로 뚝딱 만들어줍니다.
- **클래스(Class)**: 객체를 만들기 위한 **'설계도'** 또는 '붕어빵 틀'.
- **객체(Object)**: 클래스로부터 생성된 **'실체'** 또는 '팥 붕어빵', '슈크림 붕어빵'.
```python
from dataclasses import dataclass
@dataclass
class DogData:
name: str
age: int
species: str = "Canis familiaris" # 기본값 지정도 가능
def speak(self, sound: str):
return f"{self.name}가 '{sound}' 하고 짖습니다."
# __init__을 직접 작성하지 않았지만 자동으로 생성됨!
bory = DogData("보리", 3)
print(bory) # __repr__도 자동으로 생성되어 출력이 깔끔함
# 출력: DogData(name='보리', age=3, species='Canis familiaris')
```
@dataclass # __init__, __repr__ 등 반복적인 코드를 자동 생성
class Bungeoppang:
# --- 속성 (붕어빵의 속재료) ---
flavor: str
price: int = 1000 # 기본값 설정
---
# --- 메서드 (붕어빵 틀의 행동) ---
def sell(self):
print(f"{self.flavor} 붕어빵을 {self.price}원에 판매합니다.")
## 2일차(화): 상속 - 코드 재사용의 마법
# --- 객체 생성 (붕어빵 틀로 붕어빵을 찍어냄) ---
red_bean_bpp = Bungeoppang("팥")
shu_cream_bpp = Bungeoppang("슈크림", price=1200)
### 🎯 오늘의 목표
- 상속을 통해 부모 클래스의 기능을 물려받아 코드 중복을 줄입니다.
- 메서드 오버라이딩으로 부모의 기능을 자식에 맞게 재정의합니다.
- `super()`를 사용하여 부모 클래스의 메서드를 호출합니다.
# --- 메서드 호출 (붕어빵 판매) ---
red_bean_bpp.sell() # "팥 붕어빵을 1000원에 판매합니다."
shu_cream_bpp.sell() # "슈크림 붕어빵을 1200원에 판매합니다."
```
> **`__init__`과 `self`**: `@dataclass`가 자동으로 만들어주는 `__init__`은 붕어빵 틀에 반죽과 **앙금(`flavor`, `price`)**을 넣는 과정입니다. `self`는 지금 만들어지고 있는 **'바로 그 붕어빵'** 자신을 가리키는 대명사로, 붕어빵들이 서로의 앙금을 헷갈리지 않게 해줍니다.
---
### 1. 상속(Inheritance)과 메서드 오버라이딩
**[상속](./glossary.md#상속inheritance)**은 부모 클래스(Superclass)의 속성과 메서드를 자식 클래스(Subclass)가 물려받는 것입니다. 이는 마치 **'기본 설계도'를 바탕으로 '수정 설계도'를 만드는 것**과 같습니다. 자식 클래스는 부모의 기능을 그대로 사용하거나, **[메서드 오버라이딩(Method Overriding)](./glossary.md#메서드-오버라이딩method-overriding)**을 통해 자신에게 맞게 수정할 수 있습니다.
## 5. 상속: 새로운 맛의 붕어빵 출시하기
> **🎯 2일차 목표:** 상속으로 코드 중복을 줄이고, 메서드 오버라이딩과 `super()`로 기능을 확장합니다.
> **붕어빵 비유**:
> '기본 붕어빵 틀(`Dog`)'을 물려받아, 기능이 추가된 '피자 붕어빵 틀(`GoldenRetriever`)'을 새로 만드는 것과 같습니다. 모양은 같지만, 굽는 방식(`speak`)은 피자 토핑에 맞게 살짝 달라질 수 있습니다.
**상속(Inheritance)**은 '기본 붕어빵 틀'을 물려받아 '피자 붕어빵 틀' 같은 신제품을 만드는 것입니다.
```python
class GoldenRetriever(Dog): # Dog 클래스를 상속
# 메서드 오버라이딩: 부모의 speak 메서드를 재정의
def speak(self, sound="월! 월!"):
return f"{self.name}가 행복하게 '{sound}' 하고 짖습니다."
```
@dataclass
class PizzaBungeoppang(Bungeoppang): # Bungeoppang 클래스를 상속
topping: str
### 2. 부모의 능력을 빌리는 `super()`
자식 클래스에서 부모 클래스의 메서드를 호출하고 싶을 때 [`super()`](./glossary.md#super)를 사용합니다. 이를 통해 부모의 기능을 완전히 덮어쓰는 대신, **기능을 확장**할 수 있습니다.
# --- 메서드 오버라이딩(Method Overriding) ---
# 부모의 sell() 메서드를 신제품에 맞게 재정의
def sell(self):
print(f"'{self.topping}' 토핑이 올라간 {self.flavor} 피자 붕어빵을 {self.price}원에 판매합니다!")
> **붕어빵 비유**:
> '프리미엄 붕어빵 틀(`SpecialDog`)'을 만들 때, 기존 붕어빵 틀(`Dog`)의 모양과 앙금 넣는 방식(`__init__`)은 그대로 쓰고 싶습니다. 이때 `super().__init__()`를 호출하면, 마치 기본 틀의 기능을 그대로 가져와 사용하고, 우리는 프리미엄 재료(`ability`)를 추가하는 작업에만 집중할 수 있습니다.
# super() 사용 예시
@dataclass
class PremiumBungeoppang(Bungeoppang):
origin: str
```python
class SpecialDog(Dog):
def __init__(self, name: str, age: int, ability: str):
# super()를 이용해 부모 클래스(Dog)의 __init__을 호출하여
# name과 age 속성 초기화를 위임합니다.
super().__init__(name, age)
# 자식 클래스만의 속성을 추가로 초기화합니다.
self.ability = ability
def describe(self):
# 부모의 describe가 있다면 호출하고, 없다면 직접 구현
# (Dog 클래스에 describe가 없으므로 직접 구현)
return f"{self.name}{self.age}살이며, '{self.ability}' 능력이 있습니다."
special_dog = SpecialDog("슈퍼독", 5, "비행")
print(special_dog.describe())
# 출력: 슈퍼독는 5살이며, '비행' 능력이 있습니다.
def __post_init__(self):
# super()는 부모 클래스를 의미. 부모의 기능을 확장할 때 사용.
# 여기서는 Bungeoppang의 속성을 그대로 쓰되, 가격만 500원 인상
self.price += 500
```
> **메서드 오버라이딩과 `super()`**: 기본 붕어빵의 판매 방식(`sell`)을 피자 붕어빵에 맞게 바꾸는 것이 **오버라이딩**입니다. 프리미엄 붕어빵을 만들 때, 기존 가격 정책에 추가 금액만 더하는 것처럼 부모의 기능을 그대로 활용하며 확장할 때 **`super()`**를 사용합니다.
---
## 3일차(수): 다형성과 캡슐화 - 유연하고 안전한 코드
## 6. 다형성과 캡슐화: 유연하고 안전한 비즈니스
### 🎯 오늘의 목표
- 다형성(Polymorphism)을 통해 코드의 유연성과 확장성을 높입니다.
- 캡슐화(Encapsulation)를 통해 객체의 데이터를 안전하게 보호합니다.
> **🎯 3일차 목표:** 다형성(Polymorphism)으로 코드의 유연성을 높이고, 캡슐화(Encapsulation)로 데이터를 안전하게 보호합니다.
---
### 1. 다형성(Polymorphism): 같은 이름, 다른 행동
"여러(Poly) 개의 형태(Morph)"라는 뜻으로, 동일한 이름의 메서드가 객체의 종류에 따라 다르게 동작하는 것입니다. 이것이 바로 [다형성](./glossary.md#다형성polymorphism)입니다.
> **붕어빵 비유**:
> 손님이 "붕어빵 하나 주세요!"라고 말합니다. 가게 주인은 손님이 보는 앞에서 `판매하기()`라는 동일한 동작을 취합니다. 하지만 손님에게 전달되는 것은 '팥 붕어빵'일 수도, '피자 붕어빵'일 수도 있습니다. 이처럼 **같은 `판매하기()` 요청에 대해, 대상(객체)에 따라 다른 결과가 나타나는 것**이 다형성입니다.
- **다형성(Polymorphism)**: "주문하신 것 나왔습니다!" 라는 **동일한 요청(`sell()`)**에 대해, 대상 객체가 '팥 붕어빵'이냐 '피자 붕어빵'이냐에 따라 **다른 결과가 나오는 것**입니다. 코드의 유연성을 크게 높여줍니다.
- **캡슐화(Encapsulation)**: 붕어빵 맛의 핵심인 **'반죽의 비밀 레시피(`__secret_recipe`)'**를 외부에서 함부로 바꾸지 못하게 숨기고, 정해진 방법으로만 접근하게 하여 데이터를 보호하는 것입니다.
```python
class Cat:
def __init__(self, name):
self.name = name
def speak(self): # Dog 클래스와 동일한 이름의 메서드
return "야옹"
# Horse 클래스를 새로 추가해도 animal_sound 함수는 바꿀 필요가 없습니다!
class Horse:
def __init__(self, name):
self.name = name
def speak(self):
return "히히힝"
bory = Dog("보리", 3)
nabi = Cat("나비")
pony = Horse("포니")
# animal_sound 함수는 객체의 클래스가 무엇인지(개가, 고양이인지, 말인지) 신경쓰지 않고
# .speak() 메서드만 호출합니다. 이것이 다형성의 힘입니다.
def animal_sound(animal):
print(f"{animal.name}: {animal.speak()}")
animal_sound(bory) # Dog 객체의 speak()가 호출됨
animal_sound(nabi) # Cat 객체의 speak()가 호출됨
animal_sound(pony) # Horse 객체의 speak()가 호출됨
```
> **AI 라이브러리 속 다형성**: Scikit-learn의 모든 모델(`LogisticRegression`, `DecisionTree` 등)은 `.fit()`과 `.predict()`라는 동일한 이름의 메서드를 가집니다. 덕분에 우리는 모델의 종류와 상관없이 일관된 방식으로 코드를 작성할 수 있습니다.
### 2. 캡슐화(Encapsulation): 소중한 데이터 보호하기
객체의 속성을 외부에서 직접 수정하지 못하도록 숨기고, 정해진 메서드(getter/setter)를 통해서만 접근하도록 제어하는 것, 즉 [캡슐화](./glossary.md#캡슐화encapsulation)입니다.
class BungeoppangFactory:
def __init__(self, initial_flour_kg):
self.__flour_kg = initial_flour_kg # 밀가루 재고(비밀 데이터)
> **붕어빵 비유**:
> 붕어빵 맛의 핵심인 **'반죽의 비밀 레시피(`__balance`)'**는 아무나 볼 수도, 수정할 수도 없게 주방 깊숙한 곳에 숨겨둡니다. 레시피를 변경하려면, 반드시 점장님의 허락(메서드 호출)을 거쳐 정해진 절차(`deposit`)에 따라서만 가능합니다. 이렇게 소중한 데이터를 외부로부터 보호하는 것이 캡슐화입니다.
def get_stock(self): # 데이터를 확인하는 정해진 통로 (getter)
return self.__flour_kg
- `_` (Protected): "내부용이니 가급적 건드리지 마세요" 라는 개발자 간의 약속.
- `__` (Private): 이름 앞에 `_클래스명`이 붙어([Name Mangling](./glossary.md#네임-맹글링name-mangling)) 외부에서 직접 접근이 거의 불가능해짐.
def add_flour(self, amount): # 데이터를 변경하는 정해진 통로 (setter)
if amount > 0: self.__flour_kg += amount
```python
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # 잔액은 외부에서 직접 수정 불가 (private)
# getter: 잔액을 외부로 알려주는 통로
def get_balance(self):
return self.__balance
# setter: 잔액을 정해진 규칙에 따라 수정하는 통로
def deposit(self, amount):
if amount > 0:
self.__balance += amount
print(f"{amount}원 입금 완료. 현재 잔액: {self.__balance}원")
else:
print("입금액은 0보다 커야 합니다.")
my_account = BankAccount("홍길동", 10000)
# my_account.__balance = 999999 # 직접 수정 시도 -> AttributeError 발생
my_account.deposit(5000)
print(f"최종 잔액 조회: {my_account.get_balance()}")
factory = BungeoppangFactory(100)
# factory.__flour_kg = 999 #! 직접 수정 불가 (AttributeError)
factory.add_flour(20) # 정해진 방법으로만 수정 가능
print(f"현재 밀가루 재고: {factory.get_stock()}kg")
```
---
## 4일차(목): 추상화와 매직 메서드
## 7. 추상화와 매직 메서드: 비즈니스 규칙과 마법
### 🎯 오늘의 목표
- 추상화를 통해 클래스가 반드시 구현해야 할 메서드를 '규칙'으로 강제합니다.
- 매직 메서드로 파이썬 내장 기능(출력, 비교 등)을 커스터마이징합니다.
> **🎯 4일차 목표:** 추상화로 클래스의 필수 기능을 강제하고, 매직 메서드로 파이썬 내장 기능을 커스터마이징합니다.
---
### 1. 추상화(Abstraction): 필수 기능 강제하기
[추상 클래스](./glossary.md#추상-클래스abstract-class)는 '미완성 설계도'와 같습니다. `abc` 모듈과 `@abstractmethod`를 사용하여 자식 클래스가 반드시 구현해야 할 메서드를 명시할 수 있습니다. 이것이 [추상화](./glossary.md#추상화abstraction)입니다.
> **붕어빵 비유**:
> 붕어빵 프랜차이즈 본사에서 '가맹점 운영 매뉴얼(`AbstractAnimal`)'을 내놓았습니다. 이 매뉴얼에는 "모든 가맹점은 반드시 '굽기(`speak`)' 기능을 스스로 구현해야 합니다"라는 **규칙**이 명시되어 있습니다. 본사는 어떻게 구울지(how)는 알려주지 않고, 무엇을 해야 하는지(what)만 강제합니다. 이 규칙을 따르지 않은 가게는 오픈할 수 없습니다(에러 발생).
- **추상화(Abstraction)**: 프랜차이즈 본사에서 "모든 가맹점은 반드시 **'굽기()' 기능을 스스로 구현해야 한다**"는 '운영 매뉴얼'을 만들어 규칙을 강제하는 것입니다.
- **매직 메서드(Magic Methods)**: `print(붕어빵)`을 했을 때 예쁜 설명이 나오게 하거나(`__str__`), `붕어빵1 + 붕어빵2` 처럼 객체 간의 연산을 정의하는 등, 파이썬의 내장 기능을 우리 객체에 맞게 만드는 마법입니다.
```python
from abc import ABC, abstractmethod
# ABC를 상속받아 추상 클래스로 지정
class AbstractAnimal(ABC):
def __init__(self, name):
self.name = name
# @abstractmethod: 자식 클래스는 이 메서드를 반드시 구현해야 함
@abstractmethod
def speak(self):
pass
class FranchiseManual(ABC): # 추상 클래스
@abstractmethod # 추상 메서드
def bake(self): pass
# 추상 클래스를 상속받는 자식 클래스
class Lion(AbstractAnimal):
# 부모의 추상 메서드인 speak()를 반드시 구현해야 합니다.
# 만약 구현하지 않으면 TypeError가 발생합니다.
def speak(self):
return "어흥"
class MyStore(FranchiseManual): # 가맹점
def bake(self): # '굽기' 규칙을 반드시 구현해야 함
print("우리 가게만의 방식으로 붕어빵을 굽습니다.")
# 추상 클래스는 직접 객체로 만들 수 없습니다.
# my_animal = AbstractAnimal("동물") # -> TypeError 발생
# --- 매직 메서드 예시 ---
# Bungeoppang 클래스에 아래 메서드를 추가했다고 가정
# def __str__(self): return f"{self.flavor} 붕어빵"
# def __eq__(self, other): return self.flavor == other.flavor
lion = Lion("사자")
print(f"{lion.name}: {lion.speak()}")
```
### 2. 파이썬의 매직 메서드(Magic Methods)
[매직 메서드](./glossary.md#매직-메서드magic-methods)(또는 던더 메서드, Dunder Methods)는 `__이름__` 형태를 가지며, 파이썬의 내장 연산자나 함수(`+`, `len()`, `str()` 등)가 우리 클래스의 객체와 함께 사용될 때 호출되는 특별한 메서드입니다.
> **붕어빵 비유**:
> 우리가 만든 '붕어빵' 객체를 `print(my_bungeoppang)` 했을 때 "팥이 든 맛있는 붕어빵"처럼 예쁜 설명이 나오게 하거나, `bungeoppang1 + bungeoppang2` 처럼 붕어빵을 합치는 새로운 규칙을 만들 수 있게 해주는 마법 같은 기능입니다.
| 매직 메서드 | 호출되는 상황 | 설명 |
|---|---|---|
| `__str__(self)` | `str(obj)`, `print(obj)` | 객체를 사람이 읽기 좋은 **문자열**로 표현할 때 |
| `__repr__(self)`| `repr(obj)` | 객체를 **개발자가 명확히** 이해할 수 있는 문자열로 표현할 때 |
| `__len__(self)` | `len(obj)` | 객체의 길이를 반환 |
| `__add__(self, other)` | `obj1 + obj2` | 덧셈 연산 정의 |
| `__eq__(self, other)` | `obj1 == obj2`| 동등 비교 연산 정의 |
```python
@dataclass
class Word:
text: str
count: int
def __str__(self):
return f"단어 '{self.text}' ({self.count}번 등장)"
def __add__(self, other):
# Word 객체끼리 + 연산을 하면 count를 합친 새 Word 객체 반환
if isinstance(other, Word):
return Word(self.text, self.count + other.count)
else:
raise TypeError("Word 객체끼리만 더할 수 있습니다.")
def __eq__(self, other):
# 두 Word 객체는 text가 같으면 같은 것으로 취급
return self.text == other.text
bpp1 = Bungeoppang("팥")
print(bpp1) # __str__ 호출 -> "팥 붕어빵"
bpp2 = Bungeoppang("팥")
print(bpp1 == bpp2) # __eq__ 호출 -> True
```
---
## 5일차(금): 주간 마무리 및 종합 실습
## 8. 직접 해보기 (Hands-on Lab)
### 문제: 온라인 서점 시스템 모델링
- `Book` (책), `Member` (회원), `Order` (주문) 이라는 3개의 클래스를 `dataclass`를 사용하여 정의하고, 각 클래스 간의 관계를 표현하는 간단한 온라인 서점 시스템을 만들어보세요.
### 문제 1: 온라인 서점 시스템 모델링
- `Book`(책), `Member`(회원), `Order`(주문) 클래스를 `dataclass`를 사용하여 정의하고, 회원이 여러 권의 책을 주문하는 시나리오를 코드로 구현하세요.
**요구사항:**
1. **`Book` 클래스**:
- 속성: `isbn` (고유번호), `title` (제목), `author` (저자), `price` (가격)
2. **`Member` 클래스**:
- 속성: `member_id`, `name`, `address`
3. **`Order` 클래스**:
- 속성: `order_id`, `member` (`Member` 객체), `books` (`Book` 객체의 리스트)
- 메서드: `total_price()` (주문한 책들의 총 가격을 계산하여 반환)
4. 객체를 생성하고, 회원이 여러 권의 책을 주문하는 시나리오를 코드로 구현하세요.
- **`Book`**: `isbn`, `title`, `price` 속성
- **`Member`**: `member_id`, `name` 속성
- **`Order`**: `member`(`Member` 객체), `books`(`Book` 객체 리스트) 속성 및 `total_price()` (총 가격 계산) 메서드
```python
from dataclasses import dataclass, field
from typing import List
### 문제 2: 게임 캐릭터 클래스 만들기
- 기본 `Character` 클래스와 이를 상속받는 `Warrior`, `Wizard` 클래스를 만드세요.
@dataclass
class Book:
isbn: str
title: str
author: str
price: int
@dataclass
class Member:
member_id: str
name: str
address: str
@dataclass
class Order:
order_id: str
member: Member
books: List[Book] = field(default_factory=list)
def total_price(self) -> int:
return sum(book.price for book in self.books)
# 1. 객체 생성
member1 = Member("user001", "김파이", "서울시 강남구")
book1 = Book("978-0-13-235088-4", "Clean Code", "Robert C. Martin", 30000)
book2 = Book("978-0-201-63361-0", "Design Patterns", "Erich Gamma", 35000)
# 2. 주문 생성 및 실행
order1 = Order("order123", member1, [book1, book2])
# 3. 결과 출력
print(f"주문자: {order1.member.name}")
print("주문 내역:")
for book in order1.books:
print(f" - {book.title} ({book.author})")
print(f"총 주문 금액: {order1.total_price()}원")
```
**요구사항:**
- **`Character`**: `name`, `hp`, `power` 속성 및 `attack(other)` 메서드
- **`Warrior`**: `attack` 메서드를 오버라이딩하여, 20% 확률로 2배의 데미지를 입히도록 수정
- **`Wizard`**: `attack` 메서드를 오버라이딩하고, `super()`를 사용하여 부모의 공격을 먼저 실행한 뒤, 자신의 `hp`를 회복하는 기능 추가
---
## 📝 4주차 요약
이번 주차에는 프로그램을 체계적으로 설계하는 패러다임, 객체 지향 프로그래밍(OOP)을 '붕어빵 비즈니스'에 빗대어 배웠습니다.
## 9. 되짚어보기 (Summary)
- **클래스와 객체**: '붕어빵 틀'과 '붕어빵'의 관계를 통해, 설계도(클래스)로 실체(객체)를 만드는 기본 개념과 `__init__`, `self`의 용법을 익혔습니다. `@dataclass`로 반복적인 코드를 줄이는 법도 배웠습니다.
- **상속**: 부모 클래스의 기능을 물려받아(상속) 코드 중복을 줄이고, 자신에 맞게 기능을 재정의(오버라이딩)하거나 `super()`로 부모 기능을 확장하는 방법을 학습했습니다.
- **다형성과 캡슐화**: 동일한 요청에 대해 객체별로 다르게 반응하는 다형성으로 코드 유연성을 높이고, `__`를 이용한 캡슐화로 데이터를 안전하게 보호하는 방법을 배웠습니다.
- **추상화와 매직 메서드**: 추상 클래스로 자식 클래스가 구현해야 할 기능을 강제하고, `__str__`이나 `__len__` 같은 매직 메서드로 파이썬의 내장 기능을 우리 객체에 맞게 커스터마이징했습니다.
이번 주차에는 '붕어빵 비즈니스'를 통해 객체 지향 프로그래밍(OOP)을 배웠습니다.
## 🔍 4주차 연습 문제
- **클래스와 객체**: '붕어빵 틀'과 '붕어빵'으로 설계도와 실체를 구분했습니다.
- **상속**: '기본 틀'을 물려받아 '신제품 틀'을 만들며 코드 재사용성을 높였습니다.
- **다형성**: "주문하신 것 나왔습니다!" 라는 동일한 요청에 여러 종류의 붕어빵이 나갈 수 있는 유연성을 배웠습니다.
- **캡슐화와 추상화**: '비밀 레시피'를 보호하고(캡슐화), '프랜차이즈 매뉴얼'로 필수 기능을 강제(추상화)했습니다.
**문제 1: 게임 캐릭터 클래스 만들기**
- 기본 `Character` 클래스를 만드세요.
- 속성: `name`, `hp`, `power`
- 메서드: `attack(other)` (상대방(`other`)의 `hp`를 자신의 `power`만큼 감소시킴)
- `Character`를 상속받는 `Warrior``Wizard` 클래스를 만드세요.
- `Warrior`: `attack` 메서드를 오버라이딩하여, 20% 확률로 2배의 데미지를 입히도록 수정하세요.
- `Wizard`: `attack` 메서드를 오버라이딩하고, 공격 후 자신의 `hp``power`의 절반만큼 회복하는 기능을 추가하세요. (최대 hp는 넘지 않도록)
- 전사와 마법사 객체를 만들어 서로 공격하는 시나리오를 구현해보세요.
OOP는 복잡한 AI 라이브러리의 구조를 이해하고, 더 크고 체계적인 프로그램을 만드는 핵심 열쇠입니다.
**➡️ 다음 시간: [Part 5: AI 핵심 라이브러리 (NumPy & Pandas)](./part_5_ai_core_libraries.md)**
\ No newline at end of file
## 10. 더 깊이 알아보기 (Further Reading)
- [파이썬 공식 튜토리얼: 클래스](https://docs.python.org/ko/3/tutorial/classes.html)
- [Real Python: Object-Oriented Programming (OOP) in Python 3](https://realpython.com/python3-object-oriented-programming/)
\ No newline at end of file
# Part 5: AI 핵심 라이브러리 마스터하기 (5주차)
# Part 5: AI 핵심 라이브러리 마스터하기
**⬅️ 이전 시간: [Part 4: 객체 지향 프로그래밍(OOP)](./part_4_object_oriented_programming.md)** | **➡️ 다음 시간: [Part 6: 머신러닝 모델링과 평가](./part_6_machine_learning.md)**
**⬅️ 이전 시간: [Part 4: 객체 지향 프로그래밍(OOP)](./part_4_object_oriented_programming.md)**
**➡️ 다음 시간: [Part 6: 머신러닝 모델링과 평가](./part_6_machine_learning.md)**
---
## 📜 실습 코드 바로가기
- **`part_5_ai_core_libraries.py`**: [바로가기](./source_code/part_5_ai_core_libraries.py)
- 본 파트에서 다루는 NumPy, Pandas, Matplotlib의 핵심 기능 예제 코드를 직접 실행하고 수정해볼 수 있습니다.
---
## 1. 학습 목표 (Learning Objectives)
지난 4주간 우리는 파이썬이라는 언어 자체에 대한 이해와 객체 지향 설계 능력을 길렀습니다. 이제부터는 이 강력한 언어를 날개 삼아, 데이터의 세계를 본격적으로 탐험할 시간입니다.
2부에서는 데이터 분석과 머신러닝 프로젝트의 실질적인 '주연' 배우들인 NumPy, Pandas, Scikit-learn 등의 핵심 라이브러리 사용법을 깊이 있게 학습합니다.
이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.
---
- NumPy `ndarray`를 생성하고, 벡터화 연산과 브로드캐스팅을 활용하여 고성능 수치 계산을 수행할 수 있습니다.
- Pandas `DataFrame`을 사용하여 CSV, Excel 등의 데이터를 불러오고, `loc`/`iloc`으로 원하는 데이터를 정밀하게 선택하고 필터링할 수 있습니다.
- `groupby`를 사용하여 데이터를 특정 기준으로 나누고, 각 그룹에 대한 집계(평균, 합계 등)를 효율적으로 계산할 수 있습니다.
- 여러 `DataFrame``merge``concat`을 사용해 비즈니스 목적에 맞게 결합할 수 있습니다.
- 데이터 분석 프로젝트에서 NumPy와 Pandas가 어떻게 상호작용하며 문제를 해결하는지 큰 그림을 이해합니다.
## 2. 핵심 키워드 (Keywords)
`NumPy`, `ndarray`, `Pandas`, `DataFrame`, `Series`, `Vectorization`, `Broadcasting`, `Indexing`, `Filtering`, `loc`, `iloc`, `groupby`, `merge`, `concat`, `pivot_table`
## 3. 도입: 데이터 과학자의 새로운 연장통 (Introduction)
지난 4주간 우리는 파이썬이라는 언어와 객체 지향 설계 능력을 길렀습니다. 이제 이 강력한 언어를 날개 삼아, 데이터의 세계를 본격적으로 탐험할 시간입니다. 이번 주부터는 데이터 분석과 머신러닝 프로젝트의 실질적인 '주연 배우'들인 NumPy, Pandas 사용법을 깊이 있게 학습합니다.
## ⭐️ 2부의 시작: 핵심 라이브러리 관계도 (The Big Picture)
> [!TIP]
> 본 파트의 모든 예제 코드는 `source_code/part_5_ai_core_libraries.py` 파일에서 직접 실행해볼 수 있습니다.
본격적으로 각 라이브러리를 배우기 전에, 이들이 데이터 분석 및 머신러닝 프로젝트에서 어떻게 함께 사용되는지에 대한 전체적인 그림을 이해하는 것이 중요합니다.
### AI 프로젝트의 큰 그림: 라이브러리 관계도
각 라이브러리를 배우기 전에, 이들이 프로젝트에서 어떻게 함께 사용되는지 전체적인 그림을 이해하는 것이 중요합니다.
```mermaid
graph TD
......@@ -43,32 +55,28 @@ graph TD
B -->|"plt.plot(array)"| D
C -->|"결과 분석"| D
```
- **시작은 [Pandas](./glossary.md#pandas)**: 대부분의 프로젝트는 Pandas를 사용해 다양한 소스(CSV 등)로부터 데이터를 불러와 `DataFrame`으로 만드는 것에서 시작합니다.
- **계산을 위해 [NumPy](./glossary.md#numpy)로**: 정제된 데이터는 고성능 수치 계산을 위해 NumPy 배열(`ndarray`)로 변환되어 머신러닝 모델의 입력으로 준비됩니다.
- **[Scikit-learn](./glossary.md#scikit-learn)으로 모델링**: NumPy 배열로 변환된 데이터를 사용하여 Scikit-learn으로 모델을 학습하고 예측합니다.
- **모든 과정에서 시각화**: Pandas와 NumPy 데이터는 [Matplotlib/Seaborn](./glossary.md#matplotlib--seaborn)을 통해 시각화되어 데이터의 특징을 파악(EDA)하거나 모델의 성능을 분석하는 데 사용됩니다.
이 흐름을 염두에 두고 각 라이브러리를 학습하면, 지식이 파편적으로 남지 않고 하나의 큰 그림으로 연결될 것입니다.
- **시작은 Pandas**: 대부분의 프로젝트는 Pandas `DataFrame`으로 데이터를 불러와 정제하는 것에서 시작합니다.
- **계산은 NumPy로**: 정제된 데이터는 고성능 수치 계산을 위해 NumPy 배열(`ndarray`)로 변환되어 머신러닝 모델의 입력으로 준비됩니다.
- **모델링은 Scikit-learn으로**: NumPy 배열을 사용하여 Scikit-learn으로 모델을 학습하고 예측합니다.
- **모든 과정은 Matplotlib으로 시각화**: 데이터의 특징을 파악(EDA)하거나 모델의 성능을 분석하는 데 사용됩니다.
이 흐름을 염두에 두면, 지식이 파편적으로 남지 않고 하나의 큰 그림으로 연결될 것입니다.
---
---
# 1부: 데이터 과학자의 연장통 - NumPy & Pandas (5주차)
## 4. NumPy: 고성능 수치 계산의 초석
### 🎯 5주차 학습 목표
- **[NumPy](./glossary.md#numpy)**를 사용하여 고성능 과학 계산 및 다차원 배열을 다룹니다.
- **[Pandas](./glossary.md#pandas)**를 사용하여 표 형식의 데이터를 자유자재로 읽고, 선택하고, 필터링하고, 그룹화하고, 병합합니다.
> **🎯 1일차 목표:** NumPy를 사용하여 고성능 과학 계산 및 다차원 배열을 다룹니다.
---
**NumPy(Numerical Python)**는 파이썬에서 과학 계산을 위한 가장 기본적인 패키지입니다. `ndarray`라는 효율적인 다차원 배열 객체를 중심으로, 벡터, 행렬 등 고성능 수치 연산을 위한 방대한 함수들을 제공합니다.
## 1일차(월): NumPy - 고성능 수치 계산의 초석
- **NumPy(Numerical Python)**: 파이썬에서 과학 계산을 위한 가장 기본적인 패키지. [`ndarray`](./glossary.md#ndarray)라는 효율적인 다차원 배열 객체를 중심으로, 벡터, 행렬 등 고성능 수치 연산을 위한 방대한 함수들을 제공합니다.
> **💡 비유: NumPy 배열은 '계란판', 파이썬 리스트는 '장바구니'**
> - **파이썬 리스트 ('장바구니')**: 뭐든 담을 수 있어 유연하지만, 내용물이 제각각이라 전체를 한 번에 계산하기는 어렵고 느립니다.
> - **NumPy 배열 ('계란판')**: 오직 한 종류(숫자)만 담을 수 있는 '계란판'과 같습니다. 모든 칸이 균일하고 규칙적이어서, '모든 계란에 도장 찍기' 같은 작업을 기계로 한 번에 처리할 수 있습니다. 이 **균일성** 덕분에 NumPy는 엄청나게 빠른 속도로 대규모 숫자 계산을 할 수 있습니다.
#### 💡 쉽게 이해하기: NumPy 배열은 '계란판', 파이썬 리스트는 '장바구니'
> - **파이썬 리스트**: 뭐든 담을 수 있는 만능 '장바구니'입니다. 사과, 책, 신발 등 종류에 상관없이 담을 수 있어 유연하지만, 내용물이 제각각이라 전체를 한 번에 계산하기는 어렵고 느립니다.
> - **NumPy 배열**: 오직 한 종류(숫자)만 담을 수 있는 '계란판'과 같습니다. 모든 칸의 모양과 크기가 같아, '모든 계란에 도장 찍기' 같은 작업을 기계로 한 번에 처리할 수 있습니다. 이런 **균일성**과 **규칙성** 덕분에 NumPy는 엄청나게 빠른 속도로 숫자 계산을 할 수 있습니다. AI와 데이터 과학에서 대규모 데이터를 다룰 때 NumPy가 필수인 이유입니다.
### 4-1. `ndarray` 생성 및 연산
### 1. `ndarray` 생성 및 연산
```python
import numpy as np
......@@ -77,18 +85,19 @@ py_list = [[1, 2, 3], [4, 5, 6]]
np_array = np.array(py_list)
print("Shape:", np_array.shape) # (2, 3)
# 배열 연산: for문 없이 모든 요소에 대해 한 번에 연산
# 벡터화(Vectorization) 연산: for문 없이 모든 요소에 대해 한 번에 연산
result = np_array * 2 + 1
print("배열 연산 결과:\n", result)
# 브로드캐스팅: 서로 다른 크기의 배열 간에도 연산이 가능하도록 확장
# 브로드캐스팅(Broadcasting): 서로 다른 크기의 배열 간 연산
matrix = np.array([[1, 2, 3], [4, 5, 6]]) # shape: (2, 3)
vector = np.array([10, 20, 30]) # shape: (3,)
result_broadcast = matrix + vector
print("브로드캐스팅 결과:\n", result_broadcast)
```
### 2. 인덱싱과 슬라이싱
### 4-2. 인덱싱과 슬라이싱
```python
arr = np.arange(10) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
......@@ -102,10 +111,15 @@ print("짝수만 추출:", arr[arr % 2 == 0])
```
---
## 2일차(화): Pandas - '슈퍼 엑셀' DataFrame의 등장
- **Pandas**: [`DataFrame`](./glossary.md#dataframe)(2차원 테이블)과 [`Series`](./glossary.md#series)(1차원 배열) 자료구조를 사용하여, 엑셀이나 DB 테이블처럼 데이터를 직관적으로 다루게 해주는 라이브러리. 데이터 전처리에 없어서는 안 될 필수 도구입니다.
### 1. `Series`와 `DataFrame`
## 5. Pandas: '슈퍼 엑셀' DataFrame 다루기
> **🎯 2-4일차 목표:** Pandas를 사용하여 표 형식의 데이터를 자유자재로 읽고, 선택하고, 필터링하고, 그룹화하고, 병합합니다.
**Pandas**`DataFrame`(2차원 테이블)과 `Series`(1차원 배열) 자료구조를 사용하여, 엑셀이나 DB 테이블처럼 데이터를 직관적으로 다루게 해주는 라이브러리입니다. 데이터 전처리에 없어서는 안 될 필수 도구입니다.
### 5-1. `Series`, `DataFrame` 생성 및 탐색
```python
import pandas as pd
......@@ -116,28 +130,14 @@ data = {'제품': ['사과', '바나나', '체리', '딸기'],
df = pd.DataFrame(data)
print("DataFrame:\n", df)
# Series: DataFrame의 한 열
prices = df['가격']
print("\nSeries (가격):\n", prices)
```
### 2. 데이터 읽고 쓰기 및 기본 탐색
```python
# df.to_csv("fruits.csv", index=False, encoding="utf-8-sig") # CSV 파일로 저장
# df_from_csv = pd.read_csv("fruits.csv") # CSV 파일 읽기
print("\n--- head() ---")
# 기본 탐색
print(df.head()) # 맨 위 5개 행
print("\n--- info() ---")
df.info() # 각 열의 정보와 결측치 확인
print("\n--- describe() ---")
print(df.describe()) # 숫자형 열의 기술 통계량
```
---
## 3일차(수): Pandas - 데이터 선택과 필터링의 기술
### 5-2. 데이터 선택과 필터링 (`loc`, `iloc`)
### 1. `.loc` vs `.iloc`: 이름과 순서로 선택하기
> - **`.loc` (Label-based)**: 학생의 **'이름'**으로 자리를 찾는 방식 (`df.loc[행_이름, 열_이름]`)
> - **`.iloc` (Integer-based)**: 학생이 앉아있는 **'자리 번호'**로 찾는 방식 (`df.iloc[행_번호, 열_번호]`)
......@@ -147,27 +147,21 @@ print(df.loc[1, ['제품', '재고']])
# iloc: 0번 행의 0, 2번 열
print(df.iloc[0, [0, 2]])
```
### 2. 조건으로 필터링하기
```python
# 가격이 1000원 이상인 제품
# 조건 필터링: 가격이 1000원 이상인 제품
print("\n가격 1000원 이상:\n", df[df['가격'] >= 1000])
# 가격 1000원 미만이고(and) 재고 10개 미만인 제품
# 복합 조건 필터링: 가격 1000원 미만이고(and) 재고 10개 미만인 제품
# & (and), | (or), ~ (not)
print("\n가격 1000원 미만 AND 재고 10개 미만:\n", df[(df['가격'] < 1000) & (df['재고'] < 10)])
```
> 🚫 **`SettingWithCopyWarning` 주의!**: 값을 변경할 때는 `.loc`를 사용해 `df.loc[행조건, 열이름] = 값` 형태로 한 번에 접근해야 의도치 않은 오류를 막을 수 있습니다. 경고는 "원본을 바꿀지, 복사본을 바꿀지 애매하니 명확히 해달라"는 신호입니다.
> 🚫 **`SettingWithCopyWarning` 주의!**: 값을 변경할 때는 `df.loc[행조건, 열이름] = 값` 형태로 한 번에 접근해야 의도치 않은 오류를 막을 수 있습니다.
---
## 4일차(목): Pandas - 데이터 분해와 조립 (`groupby`)
`groupby()`는 데이터를 특정 기준으로 그룹화하여 각 그룹에 대한 집계(합계, 평균 등)를 수행하는 강력한 기능입니다.
### 5-3. 데이터 분해와 조립 (`groupby`)
#### 💡 쉽게 이해하기: `groupby`는 '반별 성적 평균 계산' (Split-Apply-Combine)
> `groupby`는 '반별 성적을 계산하는 선생님'과 같습니다.
> 1. **Split (나누기)**: 전체 학생 명단을 '1반', '2반', '3반'으로 나눕니다 (`df.groupby('반')`).
> 2. **Apply (적용하기)**: 각 반별로 '평균 점수 계산'이라는 동일한 작업을 수행합니다 (`.agg('mean')`).
> **💡 비유: `groupby`는 '반별 성적 평균 계산' (Split-Apply-Combine)**
> 1. **Split (나누기)**: 전체 학생 명단을 '1반', '2반'으로 나눕니다 (`df.groupby('반')`).
> 2. **Apply (적용하기)**: 각 반별로 '평균 점수 계산'을 수행합니다 (`.agg('mean')`).
> 3. **Combine (합치기)**: 각 반의 평균 점수를 모아 하나의 최종 성적표로 합칩니다.
```python
......@@ -179,52 +173,81 @@ team_agg = df_player.groupby('팀')['득점'].agg(['sum', 'mean', 'count'])
print(team_agg)
```
---
## 5일차(금): Pandas - 여러 데이터 합치고 모양 바꾸기
### 5-4. 데이터 병합과 연결 (`merge`, `concat`)
> - **`merge`**: '학생 명단'과 '도서관 대출 기록'을 '학번'이라는 공통 열을 기준으로 합치는 것 (SQL의 JOIN과 유사).
> - **`concat`**: '1반 학생 명단' 아래에 '2반 학생 명단'을 그대로 이어 붙이는 것.
### 1. 데이터 병합: `pd.merge()`
> `merge`는 '학생 명단'과 '도서관 대출 기록'을 '학번'이라는 공통 열을 기준으로 합쳐, 학생별 대출 정보를 만드는 것과 같습니다. (SQL의 JOIN과 유사)
```python
import pandas as pd
df1 = pd.DataFrame({'id': ['A01', 'A02', 'A03'], '이름': ['김파이', '이판다', '박넘파']})
df2 = pd.DataFrame({'id': ['A01', 'A02', 'A04'], '과목': ['수학', '영어', '과학']})
# 공통 'id'를 기준으로 병합 (inner: 양쪽 모두에 id가 있는 경우만)
inner_join = pd.merge(df1, df2, on='id', how='inner')
print("Inner Join:\n", inner_join)
# df1을 기준으로 병합 (left: df1의 모든 id를 유지)
# merge: 공통 'id'를 기준으로 병합 (left: df1의 모든 id 유지)
left_join = pd.merge(df1, df2, on='id', how='left')
print("\nLeft Join:\n", left_join)
print("Left Join:\n", left_join)
df3 = pd.DataFrame({'이름': ['최사이'], '나이': [21]})
# concat: df1 아래에 df3을 이어 붙이기
concatenated_df = pd.concat([df1, df3], ignore_index=True)
print("\nConcatenated DataFrame:\n", concatenated_df)
```
### 2. 데이터 연결: `pd.concat()`
> `concat`은 '1반 학생 명단' 아래에 '2반 학생 명단'을 그대로 이어 붙여 전체 학생 명단을 만드는 것과 같습니다.
---
## 6. 직접 해보기 (Hands-on Lab)
> **🎯 5일차 목표:** NumPy와 Pandas의 기술을 종합하여 실제적인 데이터 문제를 해결합니다.
아래는 어느 온라인 상점의 일자별 판매 데이터입니다. 이 데이터를 사용하여 아래의 질문에 답하는 코드를 작성해보세요.
```python
df1 = pd.DataFrame({'이름': ['김파이', '이판다'], '나이': [20, 22]})
df2 = pd.DataFrame({'이름': ['박넘파', '최사이'], '나이': [25, 21]})
import pandas as pd
import numpy as np
concatenated_df = pd.concat([df1, df2], ignore_index=True) # 기존 인덱스 무시
print("\nConcatenated DataFrame:\n", concatenated_df)
data = {
'Date': ['2024-01-01', '2024-01-01', '2024-01-02', '2024-01-02', '2024-01-03', '2024-01-03'],
'Category': ['Electronics', 'Books', 'Electronics', 'Books', 'Electronics', 'Books'],
'Product': ['Laptop', 'Python Intro', 'Mouse', 'AI for All', 'Keyboard', 'Deep Learning'],
'Price_USD': [1200, 45, 25, 55, 75, 60],
'Quantity': [5, 10, 20, 8, 15, 12]
}
sales_df = pd.DataFrame(data)
```
### 3. 데이터 재구조화: `pivot_table()`
> `pivot_table`은 '일자별, 상품별 판매 기록'이라는 긴 목록을 '일자는 행, 상품은 열, 값은 판매량'인 엑셀 보고서 형태로 바꾸는 것과 같습니다.
```python
data = {'날짜': ['2023-01-01', '2023-01-01', '2023-01-02', '2023-01-02'],
'상품': ['사과', '바나나', '사과', '바나나'],
'판매량': [100, 80, 120, 90]}
df_sales = pd.DataFrame(data)
### 문제 1: 매출 분석
1. `Price_KRW` (원화 가격) 열을 새로 만드세요. 환율은 1 USD = 1300 KRW로 가정하고, NumPy를 이용해 한 번에 계산하세요.
2. `Total_Sales_KRW` (총 판매액) 열을 만드세요. (`Price_KRW` * `Quantity`)
3. 카테고리(`Category`)별 총 판매액 합계를 계산하세요 (`groupby` 사용).
### 문제 2: 재고 관리
1. 현재 `sales_df`에는 없는 '재고(Stock)' 정보를 담은 아래 `stock_df`를 만드세요.
2. `sales_df``stock_df`를 상품명(`Product`)을 기준으로 합쳐(`merge`), 판매된 상품의 현재 재고를 함께 볼 수 있는 `report_df`를 생성하세요.
pivot = df_sales.pivot_table(index='날짜', columns='상품', values='판매량')
print("\nPivot Table:\n", pivot)
```python
stock_data = {
'Product': ['Laptop', 'Python Intro', 'Mouse', 'AI for All', 'Keyboard', 'Deep Learning', 'Monitor'],
'Stock': [50, 100, 150, 80, 120, 90, 40]
}
stock_df = pd.DataFrame(stock_data)
```
---
---
### 📚 5주차 마무리 및 다음 단계
이번 주 우리는 데이터 과학의 양대 산맥인 **NumPy****Pandas**의 기본적인 사용법을 마스터했습니다. 이제 여러분은 대부분의 정형 데이터를 자유자재로 불러오고, 원하는 형태로 가공하고, 기본적인 통계를 파악할 수 있는 강력한 무기를 갖추게 되었습니다.
## 7. 되짚어보기 (Summary)
다음 시간에는 이어서 데이터를 시각적으로 탐색하는 **Matplotlib****Seaborn**을 배우고, 드디어 첫 **머신러닝 모델**을 만들어보는 흥미로운 여정을 시작합니다.
이번 주차에는 데이터 과학의 양대 산맥인 **NumPy****Pandas**를 마스터했습니다.
- **NumPy '계란판'**: 균일한 데이터(`ndarray`)를 빠르고 효율적으로 계산하는 방법을 배웠습니다.
- **Pandas '슈퍼 엑셀'**: 표 형태의 데이터(`DataFrame`)를 자유자재로 불러오고, 선택/필터링하고, `groupby`로 요약하고, `merge`/`concat`으로 합치는 강력한 데이터 전처리 기술을 익혔습니다.
- **큰 그림**: Pandas로 데이터를 준비하고, NumPy로 계산을 수행하는 데이터 분석의 핵심 흐름을 이해했습니다.
이제 여러분은 대부분의 정형 데이터를 원하는 형태로 가공하고, 기본적인 통계를 파악할 수 있는 강력한 무기를 갖추게 되었습니다.
## 8. 더 깊이 알아보기 (Further Reading)
- [Pandas 공식 10분 완성 튜토리얼](https://pandas.pydata.org/docs/user_guide/10min.html)
- [NumPy 공식 시작하기 가이드](https://numpy.org/doc/stable/user/absolute_beginners.html)
- [Chris Albon's Notes On pandas and NumPy](https://chrisalbon.com/): 특정 상황에 필요한 코드 스니펫을 찾기에 매우 유용한 사이트
---
**➡️ 다음 시간: [Part 6: 머신러닝 모델링과 평가](./part_6_machine_learning.md)**
\ No newline at end of file
# Part 6: 머신러닝 모델링과 평가 (6주차)
# Part 6: 머신러닝 모델링과 평가
**⬅️ 이전 시간: [Part 5: AI 핵심 라이브러리](./part_5_ai_core_libraries.md)** | **➡️ 다음 시간: [Part 7: 딥러닝 기초와 PyTorch](./part_7_deep_learning.md)**
**⬅️ 이전 시간: [Part 5: AI 핵심 라이브러리](./part_5_ai_core_libraries.md)**
**➡️ 다음 시간: [Part 7: 딥러닝 기초와 PyTorch](./part_7_deep_learning.md)**
---
## 📜 실습 코드 바로가기
- **`part_6_machine_learning.py`**: [바로가기](./source_code/part_6_machine_learning.py)
- 본 파트에서 다루는 Matplotlib, Seaborn, Scikit-learn의 핵심 기능 예제 코드를 직접 실행하고 수정해볼 수 있습니다.
---
## 1. 학습 목표 (Learning Objectives)
이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.
- Matplotlib와 Seaborn을 사용하여 데이터의 분포, 관계, 추세를 시각화하고 인사이트를 도출할 수 있습니다.
- 지도학습, 훈련/테스트 데이터 분할의 개념을 설명할 수 있습니다.
- Scikit-learn의 `fit()`, `predict()` 인터페이스를 사용하여 머신러닝 모델을 훈련하고, 새로운 데이터에 대한 예측을 만들 수 있습니다.
- 피처 스케일링(StandardScaler, MinMaxScaler)과 같은 핵심 전처리 기법을 모델 성능 향상을 위해 적용할 수 있습니다.
- 데이터 불러오기부터 전처리, 훈련, 평가까지 이어지는 머신러닝의 전체 파이프라인을 코드로 구현할 수 있습니다.
## 2. 핵심 키워드 (Keywords)
`데이터 시각화`, `Matplotlib`, `Seaborn`, `Scikit-learn`, `지도학습(Supervised Learning)`, `훈련/테스트 분리(Train/Test Split)`, `데이터 전처리(Preprocessing)`, `피처 스케일링(Feature Scaling)`, `모델 평가(Evaluation)`, `정확도(Accuracy)`
## 3. 도입: 데이터에서 지혜를 캐내는 기술 (Introduction)
이전 파트에서 우리는 NumPy와 Pandas로 데이터를 자유자재로 다루는 법을 배웠습니다. 하지만 숫자로만 가득한 표는 우리에게 많은 것을 알려주지 않습니다. 이번 파트에서는 데이터에 '생명'을 불어넣는 두 가지 핵심 기술, **시각화****머신러닝**을 배웁니다.
### 🎯 6주차 학습 목표
- **[Matplotlib](./glossary.md#matplotlib--seaborn)****[Seaborn](./glossary.md#matplotlib--seaborn)**으로 데이터를 시각화하여 인사이트를 발견합니다.
- **[Scikit-learn](./glossary.md#scikit-learn)을 사용하여 머신러닝의 기본 프로세스(학습, 예측, 평가)를 이해합니다.
- 모델 성능을 높이기 위한 핵심 데이터 전처리 기법을 학습합니다.
- **시각화**는 데이터 속에 숨겨진 패턴과 이야기를 발견하는 '눈'이 되어줄 것입니다.
- **머신러닝**은 그 패턴을 학습하여 미래를 예측하는 '두뇌' 역할을 합니다.
Scikit-learn이라는 강력한 도구를 통해, 우리는 이 모든 과정을 직접 체험해볼 것입니다.
> [!TIP]
> 본 파트의 모든 예제 코드는 `source_code/part_6_machine_learning.py` 파일에서 직접 실행하고 수정해볼 수 있습니다.
---
## 1-2일차(월-화): 데이터 시각화 - 그림으로 데이터
## 4. 데이터 시각화: 그림으로 데이터와 대화하
데이터를 숫자로만 보는 것은 마치 숲을 보지 못하고 나무 하나하나만 보는 것과 같습니다. 시각화는 데이터 속에 숨겨진 패턴, 관계, 이상치를 한눈에 파악하게 해주는 강력한 도구입니다.
> **🎯 1-2일차 목표:** Matplotlib와 Seaborn으로 데이터를 시각화하여 인사이트를 발견합니다.
- **[Matplotlib](./glossary.md#matplotlib--seaborn)**: 파이썬 시각화의 근간. 거의 모든 종류의 그래프를 그릴 수 있는 강력한 라이브러리. (비유: 하얀 도화지)
- **[Seaborn](./glossary.md#matplotlib--seaborn)**: Matplotlib을 더 쉽고 예쁘게 사용할 수 있도록 만든 고수준 라이브러리. 통계적인 그래프 기능이 특화되어 있습니다. (비유: 밑그림과 색칠 도구가 갖춰진 스케치북)
데이터를 숫자로만 보는 것은 숲을 보지 못하고 나무만 보는 것과 같습니다. 시각화는 데이터 속에 숨겨진 패턴, 관계, 이상치를 한눈에 파악하게 해주는 강력한 도구입니다.
#### 💡 쉽게 이해하기: 어떤 그래프를 언제 쓸까?
> - **선 그래프 (Line Plot)**: '시간'에 따른 주가 변화처럼, 연속적인 데이터의 '추세'를 볼 때
> - **막대 그래프 (Bar Plot)**: 반별 평균 키처럼, 여러 '그룹' 간의 값을 '비교'할 때
> - **산점도 (Scatter Plot)**: 공부 시간과 성적처럼, 두 변수 간의 '관계'나 '분포'를 볼 때
> - **히스토그램 (Histogram)**: 우리 반 학생들의 키 분포처럼, 특정 데이터 구간에 얼마나 많은 값이 있는지 '분포'를 볼 때
> - **히트맵 (Heatmap)**: 여러 변수들 간의 상관관계를 한눈에 '색상'으로 파악할 때
- **Matplotlib**: 파이썬 시각화의 근간. 거의 모든 종류의 그래프를 그릴 수 있습니다. (비유: 하얀 도화지)
- **Seaborn**: Matplotlib을 더 쉽고 예쁘게 사용하도록 만든 고수준 라이브러리. 통계 기능이 특화되어 있습니다. (비유: 밑그림과 색칠 도구가 갖춰진 스케치북)
#### 4-1. 어떤 그래프를 언제 쓸까?
- **선 그래프 (Line Plot)**: 시간에 따른 주가 변화처럼, 연속적인 데이터의 **추세**를 볼 때
- **막대 그래프 (Bar Plot)**: 반별 평균 키처럼, 여러 그룹 간의 값을 **비교**할 때
- **산점도 (Scatter Plot)**: 공부 시간과 성적처럼, 두 변수 간의 **관계**나 분포를 볼 때
- **히스토그램 (Histogram)**: 우리 반 학생들의 키 분포처럼, 데이터의 **분포**를 볼 때
- **히트맵 (Heatmap)**: 여러 변수들 간의 상관관계를 한눈에 **색상**으로 파악할 때
#### 4-2. Seaborn 실전 예제
```python
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
# Seaborn에 내장된 'tips' 데이터셋 사용
tips = sns.load_dataset("tips")
# 1. 산점도: 총 식사 금액과 팁 사이의 관계
plt.figure(figsize=(8, 6)) # 그래프 크기 조절
plt.figure(figsize=(8, 6))
sns.scatterplot(x="total_bill", y="tip", data=tips, hue="smoker", size="size")
plt.title("Total Bill vs Tip Amount")
plt.xlabel("Total Bill ($)")
plt.ylabel("Tip ($)")
plt.grid(True)
# plt.savefig("scatter_plot.png") # 그래프를 파일로 저장
plt.show()
# 2. 막대 그래프: 요일별 평균 팁
plt.figure(figsize=(8, 6))
sns.barplot(x="day", y="tip", data=tips, errorbar=None)
sns.barplot(x="day", y="tip", data=tips, errorbar=None) # errorbar=None은 신뢰구간 제거
plt.title("Average Tip by Day of the Week")
plt.xlabel("Day")
plt.ylabel("Average Tip ($)")
plt.show()
```
---
## 3-4일차(수-목): 첫 머신러닝 모델 만들기
### 1. 머신러닝이란?
- **[지도학습](./glossary.md#지도학습-supervised-learning)**: **입력(문제)****정답**이 있는 데이터를 사용해, 문제와 정답 사이의 '패턴'을 기계에 학습시키는 방법.
## 5. Scikit-learn으로 첫 머신러닝 모델 만들기
> **🎯 3-4일차 목표:** Scikit-learn을 사용하여 머신러닝의 기본 프로세스(학습, 예측, 평가)를 이해합니다.
#### 💡 쉽게 이해하기: '훈련'과 '시험'
> 머신러닝 모델 평가는 '공부'와 '시험' 과정과 같습니다.
> - **[훈련 데이터](./glossary.md#훈련-데이터-vs-테스트-데이터) (교과서/모의고사)**: 모델이 학습하는 데 사용하는 데이터입니다.
> - **[테스트 데이터](./glossary.md#훈련-데이터-vs-테스트-데이터) (수능)**: 모델의 최종 성능을 평가하기 위해, 학습 과정에서 **전혀 사용하지 않은** 새로운 데이터입니다.
### 5-1. 머신러닝의 핵심: 훈련과 시험
- **지도학습(Supervised Learning)**: **입력(문제)****정답**이 있는 데이터를 사용해, 문제와 정답 사이의 '패턴'을 기계에 학습시키는 방법입니다.
> **💡 비유: 머신러닝은 '공부'와 '시험' 과정입니다.**
> - **훈련 데이터 (Training Data)**: 모델이 학습하는 데 사용하는 **'교과서/모의고사'**.
> - **테스트 데이터 (Test Data)**: 모델의 최종 성능을 평가하기 위해, 학습 과정에서 **전혀 사용하지 않은 새로운 '수능 문제'**.
>
> 훈련 데이터로만 반복해서 시험 보면 100점이 나오겠지만, 진짜 실력이라고 할 수 없겠죠? 그래서 우리는 훈련/테스트 데이터를 엄격히 분리하여 모델의 **일반화 성능**(처음 보는 데이터에 대한 예측 능력)을 측정합니다.
### 2. Scikit-learn 기본 사용법
[Scikit-learn](./glossary.md#scikit-learn)은 파이썬의 대표적인 머신러닝 라이브러리로, 다음과 같은 통일된 인터페이스를 제공합니다.
### 5-2. Scikit-learn 기본 4단계 프로세스
Scikit-learn은 다음과 같은 통일된 인터페이스를 제공하여 누구나 쉽게 머신러닝 모델을 만들 수 있게 해줍니다.
1. **모델 객체 생성**: `model = ModelName()`
2. **모델 학습**: `model.fit(X_train, y_train)` (훈련 데이터)
3. **예측**: `predictions = model.predict(X_test)` (테스트 데이터)
4. **평가**: `score = accuracy_score(y_test, predictions)`
2. **모델 학습**: `model.fit(X_train, y_train)` (훈련 데이터로 공부)
3. **예측**: `predictions = model.predict(X_test)` (테스트 데이터로 시험)
4. **평가**: `score = accuracy_score(y_test, predictions)` (채점)
```python
from sklearn.model_selection import train_test_split
......@@ -100,28 +117,18 @@ model.fit(X_train, y_train)
# 3. 예측
predictions = model.predict(X_test)
print("예측값:", predictions)
# 4. 평가 (정확도)
accuracy = accuracy_score(y_test, predictions)
print(f"정확도: {accuracy:.4f}")
# 새로운 데이터 예측해보기
# 꽃받침 길이, 너비, 꽃잎 길이, 너비
new_data = np.array([[5.0, 3.5, 1.5, 0.2]]) # 'setosa' 품종으로 예상
new_prediction = model.predict(new_data)
print(f"새로운 데이터 예측 결과: {new_prediction[0]}")
```
---
## 5일차(금): 모델 성능 향상을 위한 데이터 전처리
### 5-3. 모델 성능을 높이는 데이터 전처리
모델의 성능은 알고리즘만큼이나 **'데이터의 품질'**에 크게 좌우됩니다. "Garbage In, Garbage Out" (쓰레기를 넣으면 쓰레기가 나온다) 라는 말이 있을 정도입니다.
- **결측치 처리**: 비어있는 값을 채우거나(평균, 중앙값 등) 해당 행/열을 제거합니다.
- **[피처 스케일링](./glossary.md#피처-스케일링feature-scaling)**: 변수들의 단위를 맞춰줍니다. (예: 키(cm)와 몸무게(kg) -> 0~1 사이 값으로 표준화)
- **StandardScaler**: 평균 0, 표준편차 1로 변환.
- **MinMaxScaler**: 최솟값 0, 최댓값 1로 변환.
- **피처 스케일링(Feature Scaling)**: 변수들의 단위를 맞춰줍니다. (예: 키(cm)와 몸무게(kg) -> 0~1 사이 값으로 표준화)
- **StandardScaler**: 평균 0, 표준편차 1로 변환. (분포가 정규분포에 가까울 때 효과적)
- **MinMaxScaler**: 최솟값 0, 최댓값 1로 변환. (모든 값을 0과 1 사이로 만들고 싶을 때)
```python
from sklearn.preprocessing import StandardScaler, MinMaxScaler
......@@ -129,20 +136,49 @@ import numpy as np
data = np.array([[170, 70], [180, 85], [160, 55]], dtype=float)
# StandardScaler
scaler_std = StandardScaler()
scaled_std = scaler_std.fit_transform(data)
print("StandardScaler 결과:\n", scaled_std)
# MinMaxScaler
scaler_minmax = MinMaxScaler()
scaled_minmax = scaler_minmax.fit_transform(data)
print("\nMinMaxScaler 결과:\n", scaled_minmax)
```
### 📚 6주차 마무리 및 다음 단계
이번 주 우리는 데이터를 시각적으로 탐색하고, Scikit-learn을 사용해 머신러닝의 전 과정을 직접 체험했습니다. 이제 여러분은 데이터를 분석하고 예측 모델을 만들 수 있는 기본적인 역량을 갖추었습니다.
---
## 6. 직접 해보기 (Hands-on Lab): 타이타닉 생존자 예측
다음 시간에는 드디어 **딥러닝**의 세계로 들어갑니다. **PyTorch**를 사용하여 인공 신경망을 직접 만들고, 컴퓨터 비전과 자연어 처리의 기초를 맛보는 흥미로운 여정이 시작됩니다.
> **🎯 5일차 목표:** 데이터 전처리, 시각화, 모델링, 평가의 전 과정을 종합하여 문제를 해결합니다.
고전적인 '타이타닉' 데이터셋을 사용하여, 어떤 승객이 생존했을지 예측하는 모델을 만들어 봅시다.
### 문제:
1. Seaborn에서 `titanic` 데이터셋을 불러옵니다.
2. 간단한 탐색적 데이터 분석(EDA)을 수행하세요. (`survived``pclass`, `sex` 간의 관계를 막대 그래프로 시각화)
3. 모델링에 사용할 특성(`features`)으로 `pclass`, `sex`, `age`, `sibsp`, `parch`, `fare`를, 타겟(`target`)으로 `survived`를 선택하세요.
4. 결측치가 있는 `age` 열을 `age` 열의 **중앙값(median)**으로 채워주세요.
5. `sex` 열의 문자열('male', 'female')을 숫자(0, 1)로 변환해주세요.
6. 데이터를 훈련/테스트 세트로 분리하고 `StandardScaler`를 적용하여 스케일링하세요.
7. `DecisionTreeClassifier`로 모델을 학습시키고, 테스트 데이터에 대한 **정확도**를 계산하세요.
---
## 7. 되짚어보기 (Summary)
이번 주 우리는 데이터에서 패턴을 찾아 미래를 예측하는 머신러닝의 세계를 여행했습니다.
- **시각화는 눈**: Matplotlib와 Seaborn으로 데이터 속에 숨겨진 이야기를 보는 법을 배웠습니다.
- **머신러닝은 뇌**: Scikit-learn을 사용해 '훈련'과 '시험'이라는 과정을 거쳐 예측 모델을 만드는 법을 익혔습니다.
- **데이터는 생명**: "Garbage In, Garbage Out." 좋은 예측을 위해서는 결측치 처리, 피처 스케일링과 같은 데이터 전처리가 필수임을 이해했습니다.
이제 여러분은 데이터를 분석하고 예측 모델을 만들 수 있는 기본적인 역량을 갖추었습니다.
## 8. 더 깊이 알아보기 (Further Reading)
- [Scikit-learn 공식 예제 갤러리](https://scikit-learn.org/stable/auto_examples/index.html): 다양한 모델과 데이터셋에 대한 예제 코드의 보고
- [Seaborn 공식 튜토리얼](https://seaborn.pydata.org/tutorial.html): 더 아름답고 전문적인 시각화를 위한 가이드
- [Kaggle](https://www.kaggle.com/): 전 세계 데이터 과학자들이 모여 경쟁하는 플랫폼. 'Titanic'과 같은 다양한 입문용 프로젝트를 체험할 수 있습니다.
---
**➡️ 다음 시간: [Part 7: 딥러닝 기초와 PyTorch](./part_7_deep_learning.md)**
# Part 7.5: LLM 애플리케이션 개발과 LangChain
# Part 7.5: LangChain으로 LLM 애플리케이션 개발 맛보기
**⬅️ 이전 시간: [Part 7: 딥러닝 기초와 PyTorch](./part_7_deep_learning.md)** | **➡️ 다음 시간: [Part 8: FastAPI를 이용한 모델 서빙](./part_8_model_serving_with_fastapi.md)**
**⬅️ 이전 시간: [Part 7: 딥러닝 기초와 PyTorch](./part_7_deep_learning.md)**
**➡️ 다음 시간: [Part 8: FastAPI를 이용한 모델 서빙](./part_8_model_serving_with_fastapi.md)**
---
## 📜 실습 코드 바로가기
- **`part_7_5_llm_application_development.py`**: [바로가기](./source_code/part_7_5_llm_application_development.py)
- 본 파트에서 다루는 LangChain RAG 파이프라인 예제 코드를 직접 실행하고 수정해볼 수 있습니다.
---
## 1. 거대 언어 모델(LLM)의 발전과 새로운 패러다임
최근 몇 년간 딥러닝 분야, 특히 자연어 처리(NLP) 분야는 [거대 언어 모델(LLM)](./glossary.md#llm-large-language-model)의 등장으로 인해 혁명적인 변화를 겪고 있습니다. GPT-3, LLaMA, PaLM과 같은 모델들은 수십억 개에서 수천억 개의 파라미터를 가지며, 방대한 양의 텍스트 데이터로 사전 훈련(pre-training)되었습니다.
이러한 LLM들은 단순히 텍스트를 생성하는 것을 넘어, 번역, 요약, 질의응답, 코드 생성 등 다양한 작업에서 인간에 가까운 성능을 보여주며, 이는 '파운데이션 모델(Foundation Model)'이라는 새로운 패러다임의 등장을 이끌었습니다. 파운데이션 모델은 특정 작업에 국한되지 않고, 다양한 다운스트림 작업(downstream task)에 약간의 [미세조정(fine-tuning)](./glossary.md#미세조정fine-tuning)이나 [프롬프트 엔지니어링(prompt engineering)](./glossary.md#프롬프트-엔지니어링prompt-engineering)만으로도 적용될 수 있는 범용적인 모델을 의미합니다.
LLM의 등장은 개발자들이 AI 애플리케이션을 구축하는 방식에도 큰 변화를 가져왔습니다. 과거에는 특정 작업을 위해 모델을 처음부터 훈련하거나 복잡한 파이프라인을 구축해야 했다면, 이제는 강력한 LLM을 '두뇌'로 활용하고, 다양한 외부 도구 및 데이터와 연결하여 훨씬 더 복잡하고 지능적인 애플리케이션을 손쉽게 개발할 수 있게 되었습니다.
## 2. LLM 애플리케이션의 핵심, LangChain
LLM을 기반으로 애플리케이션을 개발할 때, 단순히 모델에 질문하고 답변을 받는 것을 넘어, 외부 데이터베이스를 참조하거나, 특정 API를 호출하거나, 여러 단계를 거쳐 복잡한 추론을 수행해야 하는 경우가 많습니다. 이러한 과정은 생각보다 복잡하며, 많은 반복적인 코드를 요구합니다.
**[LangChain](./glossary.md#langchain)**은 바로 이러한 문제점을 해결하기 위해 등장한 프레임워크입니다. LangChain은 LLM을 활용한 애플리케이션 개발을 위한 강력하고 유연한 도구 모음을 제공하며, 개발자가 LLM을 외부 데이터 소스나 계산 로직과 쉽게 '연결(chain)'할 수 있도록 돕습니다.
### LangChain의 핵심 컴포넌트
LangChain은 여러 핵심 컴포넌트들의 조합으로 이루어져 있으며, 이를 통해 모듈식으로 애플리케이션을 구성할 수 있습니다.
- **Models**: 다양한 종류의 언어 모델(LLM, Chat Models, Text Embedding Models 등)을 표준화된 인터페이스로 제공합니다.
- **Prompts**: 사용자 입력과 추가적인 정보를 결합하여 모델에 전달할 프롬프트를 동적으로 생성하고 관리합니다.
- **Chains**: 가장 핵심적인 개념으로, LLM 호출과 다른 컴포넌트(예: 다른 Chain, 외부 API, 데이터 처리 로직 등)들을 순차적으로 연결하여 복잡한 작업을 수행하는 파이프라인을 구성합니다.
- **Indexes & Retrievers**: 외부 데이터 소스를 LLM이 활용할 수 있도록 구조화하고(Indexing), 사용자의 질문과 관련된 정보를 효율적으로 찾아오는(Retrieval) 기능을 담당합니다.
- **Agents**: LLM을 '추론 엔진'으로 사용하여, 어떤 도구(Tool)를 어떤 순서로 사용해야 할지 스스로 결정하고 작업을 수행하도록 만듭니다.
LangChain을 사용하면 개발자는 LLM 자체의 한계를 넘어, 외부 세계의 정보와 상호작용하는 훨씬 더 강력하고 실용적인 애플리케이션을 만들 수 있습니다.
## 2.5. RAG의 핵심 부품: 벡터 데이터베이스 (Vector Database)
RAG 파이프라인을 구축하려면 대량의 텍스트 조각(Chunk)들을 벡터로 변환하고, 사용자 질문 벡터와 가장 유사한 벡터들을 빠르게 찾아내야 합니다. 이 과정을 효율적으로 처리해주는 것이 바로 **[벡터 데이터베이스(Vector Database)](./glossary.md#벡터-데이터베이스vector-database)**입니다.
> **💡 비유: '의미로 책을 찾는 도서관 사서'**
>
> 일반적인 데이터베이스가 '정확한 제목'이나 '저자'로 책을 찾는다면, 벡터 데이터베이스는 "사랑과 희생에 관한 슬픈 이야기"처럼 **추상적인 의미**로 책을 찾아주는 똑똑한 사서와 같습니다.
> - **인덱싱(책 정리)**: 사서는 도서관의 모든 책(Chunk)을 읽고, 각 책의 핵심 내용을 요약한 '의미 태그(Vector)'를 붙여 특수한 서가(DB)에 정리합니다.
> - **검색(책 찾기)**: 사용자가 질문하면, 사서는 질문의 '의미'를 파악하고, 서가에서 가장 비슷한 '의미 태그'를 가진 책들을 순식간에 찾아 건네줍니다.
## 1. 학습 목표 (Learning Objectives)
수백만, 수억 개의 벡터 속에서 가장 가까운 벡터를 찾는 것은 엄청난 계산량을 요구하는 어려운 문제입니다('차원의 저주'). 벡터 데이터베이스는 **ANN(Approximate Nearest Neighbor, 근사 최근접 이웃)** 같은 알고리즘을 사용하여, 100% 정확하지는 않지만 매우 빠른 속도로 충분히 정확한 결과를 찾아줍니다.
이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.
RAG에서 주로 사용되는 벡터 데이터베이스는 다음과 같습니다.
- LLM의 한계점(최신성 부족, 환각 등)을 설명하고, RAG가 이를 어떻게 해결하는지 이해할 수 있습니다.
- LangChain의 역할을 이해하고, RAG 파이프라인의 핵심 5단계(Load, Split, Embed, Store, Retrieve)를 설명할 수 있습니다.
- 문서 분할(Chunking)의 중요성을 이해하고, 기본적인 분할 전략을 적용할 수 있습니다.
- LangChain을 사용하여 외부 문서의 내용을 기반으로 질문에 답변하는 RAG 시스템을 직접 코드로 구현할 수 있습니다.
- **FAISS (Facebook AI Similarity Search)**
- **특징**: Facebook(Meta)에서 개발한, 매우 빠른 유사도 검색에 특화된 **라이브러리**입니다.
- **장점**: 인메모리(in-memory) 기반으로 작동하여 속도가 매우 빠르고, 별도의 서버 설치 없이 쉽게 사용할 수 있어 프로토타이핑이나 연구에 적합합니다.
- **단점**: 기본적으로 데이터를 메모리에 저장하므로, 프로세스가 종료되면 데이터가 사라집니다. (파일로 저장하고 불러올 수는 있습니다.)
## 2. 핵심 키워드 (Keywords)
- **Chroma (또는 ChromaDB)**
- **특징**: AI 네이티브(AI-native)를 표방하는 오픈소스 **벡터 데이터베이스**입니다.
- **장점**: 데이터를 디스크에 영속적으로(persistently) 저장하며, 클라이언트-서버 모드로 실행할 수 있어 여러 애플리케이션이 동시에 접근하는 실제 서비스 환경에 더 적합합니다. SDK가 사용하기 쉽게 설계되었습니다.
- **단점**: FAISS보다는 설정이 조금 더 필요할 수 있습니다.
`거대 언어 모델(LLM)`, `LangChain`, `검색 증강 생성(RAG)`, `환각(Hallucination)`, `임베딩(Embedding)`, `벡터 저장소(Vector Store)`, `FAISS`, `Chroma`, `문서 분할(Chunking)`
이 외에도 Pinecone, Weaviate, Milvus 등 다양한 상용/오픈소스 벡터 데이터베이스가 있으며, 각기 다른 특징과 장단점을 가지고 있습니다.
## 3. 도입: 똑똑한 LLM을 더 똑똑하게 만들기 (Introduction)
## 3. 외부 지식의 통합: 검색 증강 생성 (Retrieval-Augmented Generation, RAG)
우리는 앞에서 LLM이라는 강력한 '두뇌'를 배웠습니다. 하지만 이 두뇌는 몇 가지 결정적인 한계를 가집니다. 훈련 데이터에 없는 최신 정보나, 우리 회사 내부 문서 같은 사적인 내용은 전혀 알지 못합니다. 가끔은 그럴듯한 거짓말, 즉 '환각' 현상을 보이기도 하죠.
LLM은 방대한 지식을 학습했지만, 몇 가지 본질적인 한계를 가집니다.
이번 시간에는 이 한계를 극복하는 가장 효과적인 기술인 **검색 증강 생성(RAG, Retrieval-Augmented Generation)**을 배웁니다. RAG의 핵심 아이디어는 간단합니다.
1. **지식의 최신성 문제(Knowledge Cut-off)**: 모델이 훈련된 특정 시점 이후의 정보는 알지 못합니다.
2. **[환각(Hallucination)](./glossary.md#환각hallucination)**: 사실이 아닌 내용을 그럴듯하게 지어내는 경향이 있습니다.
3. **정보 출처의 부재**: 답변이 어떤 근거로 생성되었는지 알기 어렵습니다.
4. **사적인 데이터 접근 불가**: 기업 내부 문서나 개인적인 파일과 같은 비공개 데이터에 대해서는 알지 못합니다.
> **"LLM에게 정답을 바로 묻지 말고, 먼저 관련 정보를 찾아서 '오픈북 시험'을 보게 하자!"**
**[검색 증강 생성(RAG)](./glossary.md#rag-retrieval-augmented-generation)**는 이러한 한계를 극복하기 위한 매우 효과적인 방법론입니다. RAG의 핵심 아이디어는 LLM이 답변을 생성하기 전에, 먼저 외부 지식 소스(Knowledge Base)에서 사용자의 질문과 관련된 정보를 검색하고, 검색된 정보를 LLM에게 '참고 자료'로 함께 제공하는 것입니다.
그리고 이 복잡한 '오픈북 시험' 과정을 손쉽게 만들어주는 도구가 바로 **LangChain**입니다. LangChain을 통해 LLM이 외부 지식통하게 하여, 훨씬 더 정확하고 신뢰성 있는 AI 애플리케이션을 만드는 방법을 탐험해 봅시다.
### RAG의 작동 방식
> [!TIP]
> 본 파트의 모든 예제 코드는 `source_code/part_7_5_llm_application_development.py` 파일에서 직접 실행하고 수정해볼 수 있습니다.
RAG 파이프라인은 크게 **Indexing****Retrieval & Generation** 두 단계로 나뉩니다.
1. **Indexing (인덱싱)**
- **Load**: PDF, TXT, HTML 등 다양한 형식의 외부 문서를 불러옵니다.
- **Split (Chunking)**: 불러온 문서를 LLM이 처리하기 좋은 크기의 작은 조각(Chunk)으로 나눕니다. 이 과정이 바로 **'Chunking'**입니다.
- **Embed**: 각 Chunk를 텍스트 임베딩 모델을 사용하여 고차원의 벡터(Vector)로 변환합니다. 이 벡터는 해당 Chunk의 의미적인 내용을 담고 있습니다.
- **Store**: 변환된 벡터와 원본 Chunk 텍스트를 특수한 데이터베이스인 '벡터 저장소(Vector Store)'에 저장합니다.
---
2. **Retrieval & Generation (검색 및 생성)**
- **Retrieve**: 사용자의 질문(Query) 또한 [임베딩 모델](./glossary.md#임베딩-모델embedding-model)을 통해 벡터로 변환한 뒤, 벡터 저장소에서 이와 유사한(가까운) 벡터를 가진 Chunk들을 찾아냅니다.
- **Generate**: 검색된 Chunk들을 사용자의 원본 질문과 함께 프롬프트에 담아 LLM에 전달합니다. LLM은 이 정보를 바탕으로 신뢰성 높고 정확한 답변을 생성합니다.
## 4. RAG 파이프라인: 5단계로 완전 정복
## 4. RAG의 핵심, 문서 분할 (Document Splitting / Chunking)
RAG는 크게 **'인덱싱(Indexing)'****'검색 및 생성(Retrieval & Generation)'** 두 단계로 나뉩니다. 이 과정을 5개의 세부 단계로 나누어 살펴보겠습니다.
RAG 파이프라인의 성능은 '얼마나 관련성 높은 정보를 잘 찾아오는가'에 크게 좌우되며, 이는 Chunking 전략에 직접적인 영향을 받습니다. 문서를 어떻게 의미 있는 단위로 잘 나누는가(Chunking)가 RAG 시스템 전체의 성공을 결정하는 핵심 요소 중 하나입니다.
![RAG Pipeline](https://raw.githubusercontent.com/gogodov/Images/main/ai_expert_course/rag_pipeline.png)
### Chunking이 왜 중요한가?
### [인덱싱 단계: 도서관에 책 정리하기]
- **컨텍스트 유지**: 너무 작게 자르면 문맥 정보가 손실되고, 너무 크게 자르면 관련 없는 정보가 많이 포함되어 LLM의 집중력을 방해할 수 있습니다.
- **검색 정확도**: Chunk가 의미적으로 완결된 단위일 때, 사용자의 질문과 관련된 Chunk를 더 정확하게 찾아올 수 있습니다.
- **비용 및 성능 효율성**: 대부분의 LLM API는 입력 토큰 길이에 따라 비용이 책정됩니다. 적절한 크기의 Chunk는 불필요한 비용을 줄이고, 모델의 응답 속도를 향상시킵니다.
#### 1단계: Load (문서 불러오기)
- PDF, 웹사이트, DB 등 다양한 소스에서 원본 문서를 불러옵니다. (`Document Loaders`)
### 주요 Chunking 전략
#### 2단계: Split (문서 분할하기)
- 문서를 LLM이 처리하기 좋은 작은 조각(Chunk)으로 나눕니다. 의미가 잘 유지되도록 자르는 것이 중요합니다. (`Text Splitters`)
- **왜 중요할까?** 너무 작게 자르면 문맥을 잃고, 너무 크면 관련 없는 내용이 섞여 LLM의 집중력을 방해합니다.
다양한 Chunking 전략이 있으며, 문서의 종류와 구조에 따라 적절한 방법을 선택해야 합니다.
#### 3단계: Embed (의미의 벡터화)
- 각 Chunk를 텍스트 임베딩 모델을 사용해, 의미를 담은 숫자들의 배열, 즉 '벡터'로 변환합니다.
1. **고정 크기 분할 (Fixed-size Chunking)**
- 가장 간단한 방식으로, 문서를 정해진 글자 수나 토큰 수로 자릅니다.
- **장점**: 구현이 매우 쉽습니다.
- **단점**: 문장의 중간이나 단어의 중간에서 잘릴 수 있어 의미 단위가 깨질 위험이 큽니다.
- **개선**: Chunk 간에 일부 내용을 겹치게(Overlap) 설정하여, 문맥이 끊어지는 문제를 어느 정도 완화할 수 있습니다. `CharacterTextSplitter`가 대표적입니다.
#### 4단계: Store (벡터 저장소에 저장)
- 변환된 벡터와 원본 Chunk를 '벡터 저장소(Vector Store)'에 저장하여 언제든 빠르게 검색할 수 있도록 준비합니다.
- > **💡 비유: 벡터 저장소는 '의미로 책을 찾는 도서관 사서'**
> 일반 DB가 '정확한 제목'으로 책을 찾는다면, 벡터 저장소는 "사랑과 희생에 관한 이야기"처럼 **추상적인 의미**로 책을 찾아주는 똑똑한 사서와 같습니다. **FAISS** (빠른 임시 저장), **Chroma** (영구 저장) 등이 대표적입니다.
2. **재귀적 문자 분할 (Recursive Character Text Splitting)**
- 가장 널리 사용되는 방법 중 하나입니다. `["\n\n", "\n", " ", ""]` 와 같이 여러 종류의 구분자(separator)를 순서대로 시도하며 문서를 분할합니다.
- 큰 의미 단위(단락)부터 시작하여, Chunk가 원하는 크기보다 크면 더 작은 의미 단위(문장)으로 나누는 방식으로 재귀적으로 작동합니다.
- **장점**: 고정 크기 분할보다 의미 단위를 더 잘 보존하면서, 원하는 Chunk 크기를 비교적 일정하게 유지할 수 있습니다. `RecursiveCharacterTextSplitter`가 대표적입니다.
### [검색 및 생성 단계: 똑똑하게 질문하고 답변하기]
3. **구문 기반 분할 (Syntax-aware Chunking)**
- 문서의 구조나 특정 프로그래밍 언어의 구문을 이해하고, 이를 바탕으로 Chunk를 나눕니다.
- 예를 들어, Markdown 문서의 경우 제목(#), 목록(-), 코드 블록(```) 등을 기준으로 분할하거나, Python 코드의 경우 클래스나 함수 단위로 분할할 수 있습니다.
- **장점**: 매우 높은 수준의 의미적 일관성을 유지할 수 있습니다.
- **단점**: 특정 문서 유형이나 언어에 대한 파서(parser)가 필요하며, 구현이 복잡합니다. `MarkdownTextSplitter`, `PythonCodeTextSplitter` 등이 있습니다.
#### 5단계: Retrieve & Generate (검색 후 생성)
- **Retrieve**: 사용자의 질문도 벡터로 변환한 뒤, 벡터 저장소에서 의미적으로 가장 유사한 Chunk들을 찾아냅니다.
- **Generate**: 검색된 Chunk들을 사용자의 원본 질문과 함께 프롬프트에 담아 LLM에게 전달합니다. LLM은 이 '참고 자료'를 바탕으로 신뢰성 높은 답변을 생성합니다.
4. **에이전틱 분할 (Agentic Chunking)**
- 최근 제안된 가장 진보된 방식으로, LLM 자체를 사용하여 최적의 Chunk를 결정하는 방법입니다.
- 문서의 각 문장이나 섹션에 대해 "이 내용을 독립적으로 이해할 수 있는가?" 또는 "이웃한 내용과 합쳐져야 더 나은 컨텍스트를 형성하는가?"와 같은 질문을 LLM에게 던져 분할 여부를 결정합니다.
- **장점**: 문서의 내용에 따라 동적으로 최적의 Chunk를 생성하여 매우 높은 품질을 보장합니다.
- **단점**: Chunking 과정에서 LLM API 호출 비용이 발생하며, 처리 속도가 느립니다.
---
어떤 Chunking 전략이 가장 좋은지는 정답이 없습니다. 문서의 특성(텍스트, 코드, 테이블 등), RAG 시스템의 목적, 그리고 가용한 리소스(비용, 시간)를 종합적으로 고려하여 최적의 전략을 선택하는 것이 중요합니다.
## 5. 직접 해보기 (Hands-on Lab): 나만의 RAG 챗봇 만들기
## 5. 실습: LangChain으로 간단한 RAG 파이프라인 만들기
이제 LangChain을 사용하여, 주어진 텍스트에 대해 답변할 수 있는 간단한 RAG 시스템을 직접 구축해 보겠습니다.
이제 LangChain을 사용하여 간단한 RAG 시스템을 구축하는 예제를 살펴보겠습니다. 우리는 텍스트 파일을 불러와 Chunking하고, 이를 벡터 저장소에 저장한 뒤, 사용자 질문에 가장 관련성 높은 정보를 찾아 답변을 생성하는 과정을 구현할 것입니다.
> [!WARNING]
> 이 실습은 OpenAI의 `gpt-3.5-turbo` 모델을 사용하므로, `OPENAI_API_KEY`가 필요합니다. [이곳](https://platform.openai.com/api-keys)에서 API 키를 발급받아 아래 코드에 입력해주세요.
이번 예제에서는 OpenAI의 모델 대신, **Hugging Face의 오픈소스 모델**을 사용하여 임베딩을 생성하고, **FAISS**와 **Chroma** 벡터 저장소를 모두 활용하는 방법을 보여줍니다.
### 문제:
아래 `my_document` 텍스트를 RAG 파이프라인에 넣어, 마지막 `query`에 대한 답변을 생성하는 전체 코드를 완성하세요.
```python
# 필요한 라이브러리 설치
# !pip install langchain langchain-openai langchain-community faiss-cpu chromadb tiktoken sentence-transformers
# !pip install langchain langchain-openai faiss-cpu tiktoken sentence-transformers
import os
from langchain_community.document_loaders import TextLoader
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_openai import ChatOpenAI
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.vectorstores import FAISS, Chroma
from langchain_community.vectorstores import FAISS
from langchain.chains import RetrievalQA
# (주의: 이 코드는 OpenAI LLM을 사용하므로, OPENAI_API_KEY 환경변수 설정이 필요합니다.)
# os.environ["OPENAI_API_KEY"] = "YOUR_API_KEY"
# --- 1. 문서 로드 및 분할 ---
# RAG를 시연하기 위해 방금 생성한 샘플 문서를 로드합니다.
loader = TextLoader("./docker/ai lecture/sample_document.txt", encoding='utf-8')
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
docs = text_splitter.split_documents(documents)
# 1. OpenAI API 키 설정
# os.environ["OPENAI_API_KEY"] = "sk-..." # 여기에 자신의 API 키를 입력하세요.
print(f"문서 조각(Chunk) 수: {len(docs)}")
# --- 2. 임베딩 모델 준비 ---
# Hugging Face의 오픈소스 한국어 임베딩 모델을 사용합니다.
# https://huggingface.co/jhgan/ko-sroberta-multitask
model_name = "jhgan/ko-sroberta-multitask"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': True}
hf_embeddings = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
# 2. RAG를 위한 샘플 문서 정의
my_document = """
AI 기술이 발전하면서, 많은 사람들이 AI 비전공자를 위한 최고의 입문서가 무엇인지 궁금해합니다.
여러 전문가들은 '핸즈온 머신러닝(2판)'을 최고의 책으로 꼽습니다.
이 책은 머신러닝의 기초부터 딥러닝, 그리고 실제 프로젝트에 적용하는 방법까지 폭넓게 다룹니다.
특히, 복잡한 수학적 이론보다는 실제 코드를 통해 개념을 익힐 수 있도록 구성되어 있어,
프로그래밍에 익숙한 비전공자들이 접근하기에 매우 용이합니다.
물론, '밑바닥부터 시작하는 딥러닝' 시리즈도 훌륭한 선택지이지만,
이 책은 이론적인 깊이가 상당하여 전공자에게 더 적합할 수 있습니다.
"""
# --- 3. 벡터 저장소(Vector Store)에 저장 ---
# 3. 문서 분할 (Split)
# Hint: CharacterTextSplitter를 사용하여 문서를 chunk_size=200, chunk_overlap=0 으로 분할
text_splitter = CharacterTextSplitter(chunk_size=200, chunk_overlap=0)
docs = text_splitter.create_documents([my_document])
# 방법 A: FAISS (인메모리, 임시 저장)
# 가장 빠르고 간단하게 시작할 수 있습니다.
db_faiss = FAISS.from_documents(docs, hf_embeddings)
retriever_faiss = db_faiss.as_retriever()
# 방법 B: Chroma (영속적 저장)
# 데이터를 디스크에 저장하여 나중에 다시 불러와 사용할 수 있습니다.
persist_directory = "./chroma_db"
db_chroma = Chroma.from_documents(docs, hf_embeddings, persist_directory=persist_directory)
retriever_chroma = db_chroma.as_retriever()
# 4. 임베딩 모델 준비 (Embed)
# Hugging Face의 오픈소스 한국어 임베딩 모델 사용
model_name = "jhgan/ko-sroberta-multitask"
hf_embeddings = HuggingFaceEmbeddings(model_name=model_name)
# 5. 벡터 저장소 생성 (Store)
# FAISS를 사용하여 docs를 임베딩하고 벡터 저장소(db)를 생성
db = FAISS.from_documents(docs, hf_embeddings)
# --- 4. RAG 체인 생성 및 실행 ---
# LLM은 OpenAI의 gpt-3.5-turbo를 사용합니다.
# 6. RAG 체인 생성 및 실행 (Retrieve & Generate)
# OpenAI 모델을 LLM으로 사용
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
# FAISS를 사용한 RAG
qa_chain_faiss = RetrievalQA.from_chain_type(
# RetrievalQA 체인 생성
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever_faiss,
retriever=db.as_retriever(),
)
# Chroma를 사용한 RAG
qa_chain_chroma = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever_chroma,
)
# --- 5. 질문 및 답변 ---
query = "AI 비전공자를 위한 최고의 입문서는 무엇인가?"
# FAISS 기반 RAG 실행
print("\n--- [FAISS 기반 RAG] ---")
response_faiss = qa_chain_faiss.invoke(query)
print(response_faiss["result"])
# Chroma 기반 RAG 실행
print("\n--- [Chroma 기반 RAG] ---")
response_chroma = qa_chain_chroma.invoke(query)
print(response_chroma["result"])
# Chroma DB 영속성 확인
# 위에서 생성한 Chroma 객체를 메모리에서 삭제했다고 가정
del db_chroma
# 디스크에서 다시 불러오기
db_chroma_loaded = Chroma(persist_directory=persist_directory, embedding_function=hf_embeddings)
retriever_chroma_loaded = db_chroma_loaded.as_retriever()
qa_chain_chroma_loaded = RetrievalQA.from_chain_type(
llm=llm, chain_type="stuff", retriever=retriever_chroma_loaded
)
print("\n--- [Chroma DB 로드 후 RAG] ---")
response_chroma_loaded = qa_chain_chroma_loaded.invoke(query)
print(response_chroma_loaded["result"])
# 7. 질문 및 답변
query = "AI 비전공자가 입문하기 좋은 책은 무엇인가요? 그리고 그 이유는 무엇인가요?"
response = qa_chain.invoke(query)
print(response["result"])
```
이 예제는 RAG의 가장 기본적인 흐름을 보여줍니다. 실제 애플리케이션에서는 문서의 종류에 맞는 `Loader`와 `TextSplitter`를 선택하고, `retriever`의 검색 옵션을 조정하며, `chain_type`을 변경하는 등 다양한 방식으로 파이프라인을 고도화할 수 있습니다.
---
## 6. 또 다른 선택지: LlamaIndex
LangChain이 LLM 애플리케이션을 위한 '범용 스위스 칼'이라면, **LlamaIndex**는 'RAG 전문 수술 도구'에 비유할 수 있습니다. LlamaIndex는 LLM이 외부 데이터를 더 쉽게 활용할 수 있도록, 데이터의 수집(ingestion), 인덱싱(indexing), 검색(retrieval) 과정에 특화된 기능들을 매우 상세하고 강력하게 제공하는 데이터 프레임워크입니다.
### LangChain vs. LlamaIndex
| 구분 | LangChain | LlamaIndex |
|:--- |:--- |:--- |
| **핵심 철학** | LLM을 중심으로 다양한 컴포넌트(Tool, Chain)를 연결하는 범용 프레임워크 | 데이터를 중심으로 LLM과 연결하는 데이터 프레임워크 |
| **주요 강점** | 에이전트(Agent)와 다양한 종류의 체인(Chain)을 이용한 복잡한 워크플로우 생성 | 고성능 RAG를 위한 고급 인덱싱, 데이터 수집, 검색 전략 제공 |
| **주요 사용 사례** | 챗봇, 자율 에이전트, API 연동 등 | 고도화된 RAG, 지식 관리 시스템, 문서 기반 질의응답 |
두 프레임워크는 경쟁 관계라기보다는 상호 보완적이며, 실제로 LlamaIndex의 데이터 인덱싱/검색 기능을 LangChain의 에이전트와 함께 사용하는 경우도 많습니다.
### 실습: LlamaIndex와 오픈소스 모델로 RAG 파이프라인 만들기
이번에는 LlamaIndex를 사용하여 RAG 파이프라인을 구축해 보겠습니다. 임베딩 모델뿐만 아니라 LLM까지 Hugging Face의 무료 추론 API를 사용하여, 완전한 오픈소스 스택으로 RAG를 구현하는 방법을 알아봅니다.
```python
# 필요한 라이브러리 설치
# !pip install llama-index llama-index-embeddings-huggingface llama-index-vector-stores-chroma llama-index-llms-huggingface
import os
import chromadb
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.huggingface import HuggingFaceInferenceAPI
from llama_index.core import Settings
# --- 0. Hugging Face API 토큰 설정 ---
# Hugging Face Hub(hf.co)에서 무료 API 토큰을 발급받아 아래에 입력하거나 환경변수로 설정하세요.
# HF_TOKEN = "YOUR_HUGGINGFACE_API_TOKEN"
# os.environ["HF_TOKEN"] = HF_TOKEN
# --- 1. LLM 및 임베딩 모델 설정 ---
# Settings를 사용하여 전역적으로 모델을 설정할 수 있습니다.
# LLM: Hugging Face의 공개 모델을 Inference API를 통해 사용합니다.
# https://huggingface.co/google/gemma-2-9b-it
Settings.llm = HuggingFaceInferenceAPI(
model_name="google/gemma-2-9b-it",
tokenizer_name="google/gemma-2-9b-it"
)
# Embedding Model: LangChain 예제와 동일한 한국어 모델 사용
Settings.embed_model = HuggingFaceEmbedding(
model_name="jhgan/ko-sroberta-multitask"
)
# --- 2. 데이터 로드 ---
# SimpleDirectoryReader는 디렉토리 내의 모든 문서를 편리하게 불러옵니다.
documents = SimpleDirectoryReader(
input_files=["./docker/ai lecture/sample_document.txt"]
).load_data()
print(f"로드된 문서 수: {len(documents)}")
## 6. 되짚어보기 (Summary)
# --- 3. 인덱싱 및 저장 (ChromaDB 사용) ---
이번 시간에는 LLM의 한계를 넘어, 외부 세계와 소통하는 AI를 만드는 강력한 기술인 RAG를 배웠습니다.
# ChromaDB 클라이언트 초기화 및 컬렉션 생성
db = chromadb.PersistentClient(path="./chroma_db_llamaindex")
chroma_collection = db.get_or_create_collection("rag_tutorial")
- **RAG의 필요성**: LLM의 최신성 부족, 환각 현상과 같은 문제를 해결하기 위해, LLM에게 '오픈북 시험'을 보게 하는 방식이 RAG임을 이해했습니다.
- **RAG 파이프라인**: **Load → Split → Embed → Store → Retrieve & Generate** 로 이어지는 5단계의 과정을 통해 RAG 시스템이 어떻게 작동하는지 파악했습니다.
- **실전 RAG 구축**: LangChain을 사용하여, 주어진 문서의 내용을 기반으로 질문에 답변하는 RAG 챗봇을 직접 코드로 구현하는 경험을 쌓았습니다.
# 벡터 저장소 설정
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
이제 여러분은 단순히 LLM을 사용하는 것을 넘어, LLM을 다른 데이터, 다른 도구와 '연결'하여 훨씬 더 지능적이고 실용적인 애플리케이션을 만들 수 있는 첫걸음을 뗐습니다.
# 데이터 인덱싱
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_documents(
documents, storage_context=storage_context
)
# --- 4. 쿼리 엔진 생성 및 질문 ---
# 인덱스로부터 쿼리 엔진을 생성합니다.
query_engine = index.as_query_engine(streaming=True)
query = "AI 비전공자를 위한 최고의 입문서는 무엇인가?"
print(f"\n--- [LlamaIndex + Gemma-2 기반 RAG] ---")
print(f"질문: {query}")
print(f"답변: ", end="")
## 7. 더 깊이 알아보기 (Further Reading)
- [LangChain 공식 문서: RAG](https://python.langchain.com/v0.2/docs/concepts/#retrieval-augmented-generation-rag): LangChain이 설명하는 RAG의 개념과 다양한 활용법
- [Pinecone: What is RAG?](https://www.pinecone.io/learn/retrieval-augmented-generation/): 대표적인 벡터 DB 회사인 Pinecone이 설명하는 RAG 가이드
- [Hugging Face 블로그: RAG](https://huggingface.co/docs/transformers/main/en/rag): 허깅페이스에서 제공하는 RAG에 대한 기술적인 설명
# 스트리밍 형태로 답변 받기
streaming_response = query_engine.query(query)
streaming_response.print_response_stream()
print()
```
이 코드는 LlamaIndex가 어떻게 데이터 소스, 임베딩 모델, 벡터 저장소, LLM을 유연하게 조합하여 RAG 파이프라인을 구성하는지 보여줍니다. `Settings`를 통해 기본 컴포넌트를 설정하고, `SimpleDirectoryReader`로 데이터를 쉽게 로드하며, `VectorStoreIndex`로 인덱싱과 검색 과정을 아름답게 추상화하는 LlamaIndex의 디자인 철학을 엿볼 수 있습니다.
---
LangChain, LlamaIndex와 같은 프레임워크와 RAG, Chunking, 벡터 데이터베이스에 대한 이해는 최신 AI 트렌드를 따라가고, LLM의 잠재력을 최대한 활용하는 지능형 애플리케이션을 개발하는 데 있어 필수적인 역량이 될 것입니다.
\ No newline at end of file
**➡️ 다음 시간: [Part 8: FastAPI를 이용한 모델 서빙](./part_8_model_serving_with_fastapi.md)**
\ No newline at end of file
# Part 7: 딥러닝 기초와 PyTorch (7주차)
# Part 7: 딥러닝 기초와 PyTorch
**⬅️ 이전 시간: [Part 6: 머신러닝 모델링과 평가](./part_6_machine_learning.md)** | **➡️ 다음 시간: [Part 7.5: LangChain으로 LLM 애플리케이션 개발 맛보기](./part_7.5_llm_application_development_with_langchain.md)**
**⬅️ 이전 시간: [Part 6: 머신러닝 모델링과 평가](./part_6_machine_learning.md)**
**➡️ 다음 시간: [Part 7.5: LangChain으로 LLM 애플리케이션 개발 맛보기](./part_7.5_llm_application_development_with_langchain.md)**
---
## 📜 실습 코드 바로가기
- **`part_7_deep_learning.py`**: [바로가기](./source_code/part_7_deep_learning.py)
- 본 파트에서 다루는 PyTorch의 핵심 기능 예제 코드를 직접 실행하고 수정해볼 수 있습니다.
---
## 1. 학습 목표 (Learning Objectives)
### 🎯 7주차 학습 목표
- **[PyTorch](./glossary.md#pytorch)**를 사용하여 간단한 딥러닝 모델을 직접 구현하고 학습시켜 봅니다.
- 인공 신경망의 기본 구성 요소(Linear Layer, Activation Function)를 이해합니다.
- 딥러닝 학습 과정(Forward/Backward Propagation, Optimizer)의 원리를 파악합니다.
이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.
---
- 딥러닝과 머신러닝의 차이점을 설명하고, PyTorch의 역할을 이해할 수 있습니다.
- PyTorch `Tensor`를 다루고, `nn.Module`을 상속받아 직접 인공 신경망 모델을 설계할 수 있습니다.
- 손실 함수, 옵티마이저의 역할을 이해하고, 딥러닝의 핵심인 학습 루프(Training Loop)를 구현할 수 있습니다.
- 순전파(Forward Propagation)와 역전파(Backward Propagation)의 기본 개념을 설명할 수 있습니다.
- `torchvision`을 사용하여 표준 데이터셋(MNIST)을 불러오고, 간단한 CNN 모델을 만들어 이미지 분류 문제를 해결할 수 있습니다.
## 2. 핵심 키워드 (Keywords)
`딥러닝(Deep Learning)`, `PyTorch`, `텐서(Tensor)`, `인공 신경망(ANN)`, `nn.Module`, `활성화 함수(Activation Function)`, `손실 함수(Loss Function)`, `옵티마이저(Optimizer)`, `순전파(Forward Propagation)`, `역전파(Backward Propagation)`, `에포크(Epoch)`, `CNN(Convolutional Neural Network)`
## 1-3일차(월-수): PyTorch로 구현하는 딥러닝 첫걸음
## 3. 도입: 모델을 직접 조립하는 즐거움, PyTorch (Introduction)
드디어 딥러닝의 세계에 입문합니다. 우리는 **PyTorch**라는 강력한 딥러닝 프레임워크를 사용하여 인공 신경망을 직접 만들어보겠습니다.
드디어 딥러닝의 세계에 입문합니다. 지난 시간까지 우리는 Scikit-learn이라는 강력한 '완성품'을 사용했다면, 이제부터는 **PyTorch**라는 '조립 키트'를 사용하여 우리만의 모델을 직접 만들어 볼 것입니다.
> **💡 비유: PyTorch는 '레고 블록', Scikit-learn은 '완성된 장난감'**
> - **Scikit-learn**: '트럭', '비행기'처럼 이미 완성된 모델을 가져와 사용하는 **'완성품 장난감'**과 같습니다. 사용하기 편리하고 대부분의 정형 데이터 문제에 효과적입니다.
> - **PyTorch**: 다양한 모양의 **'레고 블록'**을 제공하여, 사용자가 원하는 어떤 복잡한 구조(신경망)라도 직접 조립할 수 있게 해줍니다. 이미지, 텍스트 등 비정형 데이터를 다루는 딥러닝에 필수적이며, 연구와 산업 현장에서 가장 널리 쓰이는 도구 중 하나입니다.
> [!TIP]
> 본 파트의 모든 예제 코드는 `source_code/part_7_deep_learning.py` 파일에서 직접 실행하고 수정해볼 수 있습니다.
---
- **[PyTorch](./glossary.md#pytorch)**: 페이스북(메타)에서 개발한 딥러닝 프레임워크. 유연성과 직관적인 코드로 연구자와 개발자들에게 널리 사랑받고 있습니다.
## 4. PyTorch 딥러닝의 기본 구성 요소
#### 💡 쉽게 이해하기: PyTorch는 '레고 블록', Scikit-learn은 '완성된 장난감'
> - **Scikit-learn**: '트럭', '비행기'처럼 이미 완성된 모델을 가져와 사용하는 '완성품 장난감'과 같습니다. 사용하기 편리하고 대부분의 정형 데이터 문제에 효과적입니다.
> - **PyTorch**: 다양한 모양의 '레고 블록'을 제공하여, 사용자가 원하는 어떤 복잡한 구조(신경망)라도 직접 조립할 수 있게 해줍니다. 이미지, 텍스트 등 비정형 데이터를 다루는 딥러닝에 필수적입니다.
> **🎯 1-2일차 목표:** PyTorch의 텐서를 이해하고, 인공 신경망의 기본 구조를 코드로 구현합니다.
### 1. 딥러닝의 핵심, 텐서(Tensor)
- **텐서**: PyTorch에서 데이터를 다루는 기본 단위. NumPy의 `ndarray`와 매우 유사하지만, GPU를 사용한 가속 연산이 가능하고 자동 미분(Autograd) 기능을 지원한다는 결정적인 차이가 있습니다.
### 4-1. 딥러닝의 데이터, 텐서(Tensor)
- **텐서**: PyTorch에서 데이터를 다루는 기본 단위입니다. NumPy의 `ndarray`와 매우 유사하지만, 두 가지 결정적인 차이가 있습니다.
1. **GPU 가속**: `.to('cuda')` 코드 한 줄로 GPU를 사용한 초고속 연산이 가능합니다.
2. **자동 미분(Autograd)**: 딥러닝 학습의 핵심인 '역전파'를 위해 자동으로 미분값을 계산해줍니다.
### 2. 인공 신경망(Artificial Neural Network) 모델 만들기
PyTorch에서는 `torch.nn.Module`을 상속받아 우리만의 신경망 클래스를 정의합니다.
### 4-2. 인공 신경망(ANN) 모델 만들기
PyTorch에서는 `torch.nn.Module`을 상속받아 우리만의 신경망 클래스(레고 조립 설명서)를 정의합니다.
- **`__init__(self)`**: 모델에 필요한 레고 블록들(레이어)을 정의하는 곳.
- **`forward(self, x)`**: 레고 블록들을 어떤 순서로 조립할지(데이터 흐름) 정의하는 곳.
```python
import torch
import torch.nn as nn
# 모델 정의
# 모델 정의 (레고 조립 설명서)
class SimpleNet(nn.Module):
def __init__(self):
# 1. 필요한 레고 블록 선언
def __init__(self, input_size, hidden_size, num_classes):
super(SimpleNet, self).__init__()
# nn.Linear(input_features, output_features)
# 입력 특성 4개(붓꽃 데이터), 출력 클래스 3개(품종)
self.fc1 = nn.Linear(4, 50) # 첫 번째 완전 연결 계층
self.relu = nn.ReLU() # 활성화 함수
self.fc2 = nn.Linear(50, 3) # 두 번째 완전 연결 계층
self.fc1 = nn.Linear(input_size, hidden_size) # 완전 연결 계층
self.relu = nn.ReLU() # 활성화 함수
self.fc2 = nn.Linear(hidden_size, num_classes)
# 2. 레고 블록 조립 순서 정의
def forward(self, x):
# 순전파: 데이터가 입력층부터 출력층까지 나아가는 과정
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
out = self.fc1(x)
out = self.relu(out) # 💡 비선형성을 추가하여 모델의 표현력을 높임
out = self.fc2(out)
return out
model = SimpleNet()
model = SimpleNet(input_size=784, hidden_size=500, num_classes=10) # MNIST 예시
print(model)
```
> **활성화 함수(ReLU)는 왜 필요할까?**
> 선형 계층(`nn.Linear`)만 계속 쌓으면 결국 하나의 큰 선형 계층과 다를 바 없습니다. 활성화 함수는 여기에 '비선형성'이라는 마법을 더해, 복잡하고 구불구불한 데이터 패턴도 학습할 수 있게 해주는 필수 요소입니다.
---
## 4-5일차(목-금): 딥러닝 모델 학습시키기
### 1. 손실 함수(Loss Function)와 옵티마이저(Optimizer)
- **[손실 함수](./glossary.md#손실-함수loss-function)**: 모델의 예측이 실제 정답과 얼마나 다른지를 측정하는 '오차 측정기'. (예: `CrossEntropyLoss` - 분류 문제)
- **[옵티마이저](./glossary.md#옵티마이저optimizer)**: 손실 함수가 계산한 오차를 기반으로, 모델의 파라미터(가중치)를 어느 방향으로 얼마나 업데이트할지 결정하는 '조율사'. (예: `Adam`, `SGD`)
## 5. 딥러닝 모델 학습 과정
> **🎯 3-4일차 목표:** 손실 함수, 옵티마이저의 개념을 이해하고 전체 학습 루프를 구현합니다.
### 5-1. 모델의 나침반과 조율사
- **손실 함수(Loss Function)**: 모델의 예측이 실제 정답과 얼마나 다른지를 측정하는 **'오차 측정기'**. (예: `nn.CrossEntropyLoss` - 분류 문제)
- **옵티마이저(Optimizer)**: 손실 함수가 계산한 오차를 기반으로, 모델의 파라미터(가중치)를 어느 방향으로 얼마나 업데이트할지 결정하는 **'조율사'**. (예: `torch.optim.Adam`)
### 2. 학습 루프(Training Loop)
딥러닝 모델 학습은 '에포크(Epoch)'라는 단위로 반복됩니다. 1 에포크는 전체 훈련 데이터를 한 번 모두 사용하는 것을 의미합니다.
### 5-2. 핵심 프로세스: 학습 루프(Training Loop)
딥러닝 모델 학습은 '에포크(Epoch)'라는 단위로 반복됩니다. 1 에포크는 전체 훈련 데이터를 한 번 모두 사용하는 것을 의미하며, 학습은 다음 4단계로 이루어집니다.
1. **`model(inputs)`**: 모델에 입력을 넣어 예측값(`outputs`)을 계산합니다 (**순전파**).
2. **`loss = criterion(outputs, labels)`**: 예측값과 실제 정답을 비교하여 오차(`loss`)를 계산합니다.
3. **`loss.backward()`**: 계산된 오차를 기반으로, 각 파라미터가 오차에 얼마나 기여했는지 미분값을 계산합니다 (**역전파**).
4. **`optimizer.step()`**: 계산된 미분값을 바탕으로 모델의 파라미터를 업데이트하여 오차를 줄이는 방향으로 나아갑니다.
> **💡 순전파와 역전파**
> - **순전파 (Forward)**: 내가 만든 레시피로 요리를 해서(모델 예측) 손님에게 내놓는 과정.
> - **역전파 (Backward)**: 손님의 피드백("너무 짜요!")을 듣고, 소금을 얼마나 많이 넣었는지, 간장을 얼마나 넣었는지 원인을 거슬러 올라가 분석하는 과정. `optimizer.step()`은 이 분석을 바탕으로 다음 요리에서는 소금을 덜 넣는 행동입니다.
---
## 6. 직접 해보기 (Hands-on Lab): MNIST 손글씨 분류
> **🎯 5일차 목표:** PyTorch를 사용하여 딥러닝의 "Hello, World!"인 MNIST 손글씨 분류기를 직접 만듭니다.
### 문제:
`torchvision`을 사용하여 MNIST 데이터셋을 불러오고, 간단한 CNN(합성곱 신경망) 모델을 구축하여 손글씨 숫자를 분류하는 전체 코드를 완성하세요.
```python
# (데이터 로드 및 전처리는 Scikit-learn 예제와 유사하게 진행되었다고 가정)
# X_train, y_train을 PyTorch 텐서로 변환
# X_train_tensor = torch.FloatTensor(X_train.values)
# y_train_tensor = torch.LongTensor(pd.Categorical(y_train).codes)
# criterion = nn.CrossEntropyLoss()
# optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# epochs = 100
# for epoch in range(epochs):
# # 1. 순전파 (Forward pass)
# outputs = model(X_train_tensor)
# loss = criterion(outputs, y_train_tensor)
# # 2. 역전파 및 가중치 업데이트 (Backward and optimize)
# optimizer.zero_grad() # 이전 그래디언트 초기화
# loss.backward() # 그래디언트 계산
# optimizer.step() # 파라미터 업데이트
# if (epoch+1) % 10 == 0:
# print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# 1. 데이터셋 및 로더 준비
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
train_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
test_dataset = torchvision.datasets.MNIST(root='./data', train=False, transform=transform)
train_loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)
# 2. CNN 모델 정의
class ConvNet(nn.Module):
def __init__(self):
super(ConvNet, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2))
self.layer2 = nn.Sequential(
nn.Conv2d(32, 64, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2))
self.fc = nn.Linear(7*7*64, 10)
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = out.reshape(out.size(0), -1) # Flatten
out = self.fc(out)
return out
# 3. 모델, 손실함수, 옵티마이저 설정
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = ConvNet().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 4. 학습 루프 구현
# for epoch ...
# for i, (images, labels) in enumerate(train_loader):
# - images, labels를 device로 이동
# - 순전파, 손실 계산, 역전파, 파라미터 업데이트 코드 작성
# 5. 평가 루프 구현
# with torch.no_grad():
# - test_loader에서 데이터를 가져와 예측
# - 전체 테스트 정확도 계산 및 출력 코드 작성
```
위 코드는 개념 설명을 위한 것으로, 실제 실행 가능한 전체 코드는 `source_code/part_7_deep_learning.py`에서 확인해주세요.
### 📚 7주차 마무리 및 다음 단계
이번 주 우리는 PyTorch를 사용하여 딥러닝의 기본 원리를 이해하고, 직접 신경망을 만들어 학습시키는 과정을 경험했습니다.
---
## 7. 되짚어보기 (Summary)
이번 주 우리는 PyTorch라는 '레고 블록'으로 딥러닝 모델을 직접 조립하는 방법을 배웠습니다.
다음 시간에는 지금까지 배운 머신러닝/딥러닝 지식을 바탕으로, 최근 가장 뜨거운 분야인 **거대 언어 모델(LLM)****LangChain**을 활용하여 간단한 챗봇 애플리케이션을 만들어보는 시간을 갖습니다.
- **PyTorch의 구성요소**: GPU 연산이 가능한 `Tensor`와, `nn.Module`을 상속받아 만드는 우리만의 모델 구조를 이해했습니다.
- **딥러닝 학습 과정**: '오차 측정기'인 **손실 함수**와 '조율사'인 **옵티마이저**를 사용하여, **순전파 → 손실 계산 → 역전파 → 파라미터 업데이트**로 이어지는 학습 루프의 원리를 파악했습니다.
- **실전 경험**: MNIST 손글씨 분류 실습을 통해 이미지 데이터를 다루는 CNN 모델을 직접 구현하고 학습시키는 전 과정을 체험했습니다.
## 8. 더 깊이 알아보기 (Further Reading)
- [PyTorch 공식 60분 튜토리얼](https://tutorials.pytorch.kr/beginner/deep_learning_60min_blitz.html): PyTorch의 핵심 기능을 빠르게 훑어볼 수 있는 최고의 가이드
- [cs231n: Convolutional Neural Networks for Visual Recognition](https://cs231n.github.io/): 스탠포드 대학의 전설적인 딥러닝 강의. CNN의 원리를 깊이 있게 이해하고 싶다면 필독.
- [A Comprehensive Introduction to Torch.nn for Deep Learning](https://www.assemblyai.com/blog/a-comprehensive-introduction-to-torch-nn-for-deep-learning/): `torch.nn` 모듈에 대한 상세한 설명
---
**➡️ 다음 시간: [Part 7.5: LangChain으로 LLM 애플리케이션 개발 맛보기](./part_7.5_llm_application_development_with_langchain.md)**
\ No newline at end of file
# Part 8: FastAPI를 이용한 모델 서빙 (8주차)
# Part 8: FastAPI를 이용한 모델 서빙
**⬅️ 이전 시간: [Part 7.5: LLM 애플리케이션 개발과 LangChain](./part_7.5_llm_application_development_with_langchain.md)** | **➡️ 다음 시간: [Part 9: 프로덕션 레벨 API와 Docker](./part_9_production_ready_api.md)**
**⬅️ 이전 시간: [Part 7.5: LLM 애플리케이션 개발 맛보기](./part_7.5_llm_application_development_with_langchain.md)**
**➡️ 다음 시간: [Part 9: 프로덕션 레벨 API와 Docker](./part_9_production_ready_api.md)**
---
## 📜 실습 코드 바로가기
- **`part_8_model_serving_with_fastapi`**: [바로가기](./source_code/part_8_model_serving_with_fastapi)
- 본 파트에서 다루는 FastAPI 서버 예제 코드(`main.py`, `requirements.txt` 등)를 직접 실행하고 수정해볼 수 있습니다.
---
## 1. 학습 목표 (Learning Objectives)
지금까지 우리는 Scikit-learn과 PyTorch를 이용해 강력한 AI 모델을 만드는 방법을 배웠습니다. 하지만 이 모델들은 아직 우리 컴퓨터 안에만 머물러 있습니다. 어떻게 하면 이 모델의 예측 능력을 다른 사람이나 서비스에게 제공할 수 있을까요?
이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.
> **💡 비유: '나만 아는 맛집 레시피를 세상에 공개하기'**
>
> 우리가 AI 모델을 만든 것은, 세상에 없는 '비법 레시피'를 개발한 것과 같습니다. 하지만 이 레시피를 내 서랍 속에만 둔다면 아무도 그 맛을 볼 수 없습니다.
>
> **[API](./glossary.md#api-application-programming-interface) 서버**는 이 비법 레시피로 요리를 만들어 손님에게 판매하는 **'레스토랑'**을 차리는 과정입니다. 손님(클라이언트)은 주방(모델 서버)이 어떻게 돌아가는지 몰라도, 메뉴판(API 문서)을 보고 주문하면 맛있는 요리(예측 결과)를 받을 수 있습니다.
>
> 이번 주차에는 파이썬의 최신 웹 프레임워크인 **[FastAPI](./glossary.md#fastapi)**를 이용해 빠르고 세련된 AI 레스토랑을 여는 방법을 배워보겠습니다.
- API의 개념을 '레스토랑 주문'에 비유하여 설명할 수 있습니다.
- FastAPI를 사용하여 기본적인 웹 서버를 구축하고 `uvicorn`으로 실행할 수 있습니다.
- `lifespan` 이벤트를 사용하여 서버 시작 시 머신러닝 모델을 효율적으로 로드할 수 있습니다.
- Pydantic을 사용하여 API의 요청/응답 데이터 형식을 정의하고 자동으로 검증할 수 있습니다.
- FastAPI의 자동 생성 문서(Swagger UI)를 통해 브라우저에서 직접 API를 테스트하고 디버깅할 수 있습니다.
---
## 2. 핵심 키워드 (Keywords)
`모델 서빙(Model Serving)`, `API`, `FastAPI`, `uvicorn`, `Pydantic`, `경로 매개변수(Path Parameter)`, `요청 본문(Request Body)`, `Swagger UI`, `lifespan`
## 3. 도입: 나만 알던 맛집 레시피, 세상에 공개하기 (Introduction)
### 📖 8주차 학습 목표
지금까지 우리는 Scikit-learn과 PyTorch로 강력한 AI 모델, 즉 세상에 없는 '비법 레시피'를 개발했습니다. 하지만 이 레시피를 내 서랍 속에만 둔다면 아무도 그 맛을 볼 수 없습니다.
- **API의 개념 이해**: '레스토랑 주문 카운터' 비유를 통해 API가 왜 필요하며, 어떤 역할을 하는지 이해합니다.
- **FastAPI로 서버 구축**: FastAPI를 설치하고, uvicorn으로 내 컴퓨터에서 웹 서버를 실행하는 방법을 배웁니다.
- **예측 API 구현**: 우리가 만든 머신러닝 모델을 FastAPI 서버에 탑재하여, 외부 요청에 따라 예측 결과를 반환하는 API를 만듭니다.
- **데이터 유효성 검사**: **[Pydantic](./glossary.md#pydantic)**을 이용해 '깐깐한 주문서 양식'처럼 API로 들어오는 데이터의 형식을 정의하고 자동으로 검증하는 방법을 익힙니다.
- **자동 API 문서 활용**: FastAPI의 강력한 기능인 **[Swagger UI](./glossary.md#swagger-ui--openapi)**를 통해, 코드를 건드리지 않고 브라우저에서 직접 API를 테스트하는 방법을 배웁니다.
> **API 서버는 이 비법 레시피로 요리를 만들어 손님에게 판매하는 '레스토랑'을 차리는 과정입니다.**
> 손님(클라이언트)은 주방(모델 서버)이 어떻게 돌아가는지 몰라도, 메뉴판(API 문서)을 보고 주문하면 맛있는 요리(예측 결과)를 받을 수 있습니다.
이번 주차에는 파이썬의 최신 웹 프레임워크인 **FastAPI**를 이용해, 빠르고 세련된 'AI 레스토랑'을 여는 방법을 배워보겠습니다.
> [!TIP]
> 본 파트의 모든 예제 코드는 `source_code/part_8_model_serving_with_fastapi` 폴더에서 직접 실행하고 수정해볼 수 있습니다.
---
### 1일차: FastAPI 입문 - 우리의 AI 레스토랑 개점
## 4. FastAPI 우리의 AI 레스토랑 개점하기
#### **API란 무엇일까?**
[API(Application Programming Interface)](./glossary.md#api-application-programming-interface)는 소프트웨어들이 서로 정보를 주고받는 '약속된 방식'입니다. 우리 'AI 레스토랑'에서는 "이런 형식으로 주문(요청)하면, 저런 형식으로 요리(응답)를 내어드립니다"라고 정해놓은 **'주문 창구'** 와 같습니다.
> **🎯 1-2일차 목표:** FastAPI와 uvicorn으로 서버를 실행하고, Pydantic으로 데이터 형식을 정의합니다.
#### **왜 FastAPI일까?**
**[FastAPI](./glossary.md#fastapi)**는 우리 레스토랑을 위한 최고의 선택지입니다.
- **엄청난 속도**: 주문이 밀려들어도 막힘없이 처리하는 '베테랑 직원'처럼 매우 빠릅니다. (Node.js, Go와 비슷한 성능)
### 4-1. 왜 FastAPI일까?
- **엄청난 속도**: 주문이 밀려들어도 막힘없이 처리하는 '베테랑 직원'처럼 매우 빠릅니다.
- **쉬운 메뉴 관리**: '레시피(코드)'가 간결하고 명확해서, 실수를 줄이고 새로운 메뉴를 빠르게 추가하기 쉽습니다.
- **스마트 메뉴판 자동 생성**: 코드를 짜면 손님들이 직접 보고 테스트할 수 있는 **자동 대화형 문서([Swagger UI](./glossary.md#swagger-ui--openapi))**를 공짜로 만들어줍니다.
#### **개발 환경 준비**
먼저 FastAPI와 우리 레스토랑의 '서버 매니저' 역할을 할 [uvicorn](./glossary.md#uvicorn--gunicorn)을 설치합니다.
- **스마트 메뉴판 자동 생성**: 코드를 짜면 손님들이 직접 보고 테스트할 수 있는 자동 대화형 문서(Swagger UI)를 공짜로 만들어줍니다.
### 4-2. 서버 실행과 데이터 형식 정의
먼저 FastAPI와 서버 매니저 `uvicorn`을 설치합니다.
```bash
pip install fastapi "uvicorn[standard]"
```
#### **우리의 첫 FastAPI 앱**
`main.py` 파일을 만들어 레스토랑의 기본 틀을 잡아봅니다.
이제 `main.py` 파일에 레스토랑의 기본 틀을 잡고, 손님의 '주문서 양식'을 **Pydantic**으로 정의합니다.
```python
# main.py
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
# 1. Pydantic으로 '주문서 양식' 정의
class IrisFeatures(BaseModel):
features: List[float]
# 1. FastAPI 인스턴스 생성 (레스토랑 개점)
class Config: # 스마트 메뉴판에 보여줄 주문 예시
json_schema_extra = {"example": {"features": [5.1, 3.5, 1.4, 0.2]}}
# 2. FastAPI 인스턴스 생성 (레스토랑 개점)
app = FastAPI()
# 2. API 엔드포인트 정의 (메뉴판에 메뉴 추가)
# "/" 주소로 GET 요청(가장 기본적인 방문)이 오면, 아래 함수를 실행
# 3. API 엔드포인트 정의 (메뉴판에 메뉴 추가)
@app.get("/")
def read_root():
return {"message": "어서오세요! 저의 AI 레스토랑입니다!"}
```
- `app = FastAPI()`: FastAPI 레스토랑을 개점합니다.
- `@app.get("/")`: 손님이 가게의 정문(`/`)으로 들어와 **"여기는 뭐하는 곳인가요?"(GET 요청)** 라고 물어보면, 아래 `read_root` 직원이 응답하라는 의미입니다.
return {"message": "어서오세요! 붓꽃 품종 예측 레스토랑입니다!"}
#### **서버 실행하기**
터미널에서 `main.py`가 있는 디렉토리로 이동한 뒤, 서버 매니저 uvicorn을 실행시킵니다.
# 주문(POST 요청)을 받는 엔드포인트 - 아직 내용은 비어있음
@app.post("/predict")
def predict_iris(data: IrisFeatures):
return {"주문 접수 완료": data.features}
```
- `@app.get("/")`: 손님이 가게 정문(`/`)으로 들어와 **"여기는 뭐하는 곳인가요?"(GET 요청)**라고 물어보면 응답합니다.
- `@app.post("/predict")`: `/predict` 창구에서는 **"이 재료로 요리해주세요"(POST 요청)**라는 주문을 받습니다.
- `data: IrisFeatures`: 손님의 주문(JSON)을 우리가 정의한 `IrisFeatures` 양식에 맞춰 자동으로 확인하고, 형식이 틀리면 FastAPI가 알아서 오류를 보냅니다.
이제 터미널에서 서버를 실행합니다. `--reload`는 코드가 바뀔 때마다 서버를 자동으로 재시작해주는 편리한 옵션입니다.
```bash
uvicorn main:app --reload
```
- `main`: `main.py` 파일 (우리 레스토랑의 레시피 북)
- `app`: 파일 안에 있는 `app` 객체 (레스토랑 그 자체)
- `--reload`: 레시피를 수정할 때마다 가게를 재오픈할 필요 없이, 매니저가 알아서 반영해주는 편리한 옵션입니다.
서버가 실행되면 웹 브라우저에서 http://127.0.0.1:8000 주소로 접속해보세요. 직원의 환영 메시지가 보인다면 성공입니다!
서버 실행 후, 웹 브라우저에서 `http://127.0.0.1:8000` 주소로 접속하여 환영 메시지를 확인하세요.
---
### 2-3일차: 예측 API 구현 - 레스토랑의 시그니처 메뉴 만들기
이제 개점한 레스토랑에 6주차에서 만들었던 **붓꽃 품종 예측 모델(iris_model.pkl)**을 올린 '시그니처 메뉴'를 추가해보겠습니다.
## 5. 예측 API 구현: 레스토랑의 시그니처 메뉴 만들기
> **[중요]** `iris_model.pkl` 파일을 `main.py` 파일과 같은 폴더에 두어야 합니다. 필요한 라이브러리도 설치해주세요.
> ```bash
> pip install scikit-learn joblib numpy
> ```
> **🎯 3-4일차 목표:** 6주차에서 만든 붓꽃 예측 모델을 FastAPI에 탑재하여, 실제 예측 결과를 반환하는 API를 완성합니다.
#### **모델과 데이터 형식 정의하기**
손님이 어떤 형식으로 주문해야 하는지 명확한 '주문서 양식'이 필요합니다. **[Pydantic](./glossary.md#pydantic)** 라이브러리가 이 역할을 멋지게 수행합니다.
또한, 무거운 주방기구(모델)를 손님이 올 때마다 껐다 켰다 하는 것은 비효율적입니다. 서버가 시작될 때(가게 오픈) **단 한번만** 모델을 메모리에 로드하도록 **lifespan** 이벤트를 사용합니다.
### 5-1. 모델 로딩 최적화: `lifespan` 이벤트
무거운 주방기구(모델)를 손님이 올 때마다 켜는 것은 비효율적입니다. 서버가 시작될 때 **단 한번만** 모델을 메모리에 로드하도록 `lifespan` 이벤트를 사용합니다.
```python
# main.py (수정)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
# main.py (수정 및 추가)
from contextlib import asynccontextmanager
import joblib
import numpy as np
from contextlib import asynccontextmanager
# --- '가게 오픈/마감 준비'를 위한 Lifespan 이벤트 ---
# ... (IrisFeatures 정의는 그대로) ...
MODELS = {} # 모델을 저장할 딕셔너리
@asynccontextmanager
async def lifespan(app: FastAPI):
# 서버 시작 시 (가게 오픈)
print("레스토랑 오픈: 시그니처 메뉴(모델)를 준비합니다.")
try:
# 무거운 주방기구(모델)를 미리 켜놓습니다.
app.state.model = joblib.load("iris_model.pkl")
app.state.class_names = ['setosa', 'versicolor', 'virginica']
print("모델 준비 완료!")
except FileNotFoundError:
print("경고: 모델 파일(iris_model.pkl)을 찾을 수 없습니다.")
app.state.model = None
print("레스토랑 오픈: 모델을 준비합니다.")
MODELS["iris_model"] = joblib.load("iris_model.pkl")
print("모델 준비 완료!")
yield # 여기서 애플리케이션이 실행됨 (손님 받기)
yield # 여기서 애플리케이션이 실행됨
# 서버 종료 시 (가게 마감)
MODELS.clear()
print("레스토랑 마감")
# --- FastAPI 앱 생성 및 Lifespan 연결 ---
app = FastAPI(lifespan=lifespan)
# --- Pydantic으로 '주문서 양식' 정의 ---
class IrisFeatures(BaseModel):
features: List[float] # 4개의 float 값으로 이루어진 리스트
# 스마트 메뉴판에 보여줄 주문 예시
class Config:
json_schema_extra = {
"example": {
"features": [5.1, 3.5, 1.4, 0.2] # setosa 품종 데이터
}
}
# --- 기본 엔드포인트 ---
@app.get("/")
def read_root():
return {"message": "붓꽃 품종 예측 레스토랑"}
# ... (@app.get("/") 정의는 그대로) ...
```
#### **예측 엔드포인트 구현**
이제 실제 요리(예측)를 하고 결과를 내어주는 `/predict` 창구를 만듭니다. '요리 주문'은 새로운 것을 만들어달라는 요청이므로 POST 방식으로 받습니다.
### 5-2. 예측 엔드포인트 완성
이제 `/predict` 창구에서 실제 요리(예측)를 하고 결과를 반환하도록 코드를 완성합니다.
```python
# main.py (아래에 추가)
# main.py (@app.post("/predict") 수정)
# --- 예측 엔드포인트 (시그니처 메뉴 주문 창구) ---
@app.post("/predict")
def predict_iris(data: IrisFeatures):
"""
붓꽃의 특징 4가지를 주문서(JSON)로 받아 요청하면,
예측된 품종과 그에 대한 신뢰도 점수를 요리(JSON)로 만들어 반환합니다.
붓꽃의 특징 4가지를 주문서로 받아, 예측된 품종과 신뢰도 점수를 반환합니다.
"""
model = app.state.model
if model is None:
raise HTTPException(status_code=503, detail="죄송합니다. 주방(모델)이 준비되지 않았습니다.")
# 주문서 양식 확인
if len(data.features) != 4:
raise HTTPException(status_code=400, detail="주문서에 4개의 특징(float)을 정확히 기입해주세요.")
model = MODELS["iris_model"]
# 주방(모델)이 알아듣는 형태로 재료 손질
model_input = np.array(data.features).reshape(1, -1)
......@@ -174,73 +142,56 @@ def predict_iris(data: IrisFeatures):
prediction_proba = model.predict_proba(model_input)[0]
# 결과 플레이팅
class_name = app.state.class_names[prediction_idx]
class_names = ['setosa', 'versicolor', 'virginica']
class_name = class_names[prediction_idx]
confidence = prediction_proba[prediction_idx]
return {
"주문하신 재료": data.features,
"예측된 요리": class_name,
"셰프의 자신감": float(confidence)
"predicted_class": class_name,
"confidence": float(confidence)
}
```
- `@app.post("/predict")`: `/predict` 창구에서는 **POST** 주문을 받습니다.
- `data: IrisFeatures`: 손님의 주문(JSON)을 우리가 정의한 `IrisFeatures` 양식에 맞춰 자동으로 확인하고, 문제가 없으면 `data` 변수에 담아줍니다. 형식이 틀리면 FastAPI가 알아서 "주문 잘못하셨어요" 하고 오류를 보냅니다.
> **[필수]** 6주차 실습에서 저장한 `iris_model.pkl` 파일을 `main.py`와 같은 폴더에 준비하고, `scikit-learn`, `joblib`을 설치해주세요.
> `pip install scikit-learn joblib numpy`
---
### 4-5일차: API 테스트와 종합 - 레스토랑 시식
## 6. 직접 해보기 (Hands-on Lab): 스마트 메뉴판으로 시식하기
이제 완성된 우리 레스토랑의 시그니처 메뉴를 맛볼 시간입니다. FastAPI의 가장 강력한 기능, **스마트 메뉴판(자동 대화형 API 문서)**을 이용해봅시다.
> **🎯 5일차 목표:** FastAPI의 자동 API 문서를 사용하여, 코드를 건드리지 않고 브라우저에서 직접 API를 테스트합니다.
#### **Swagger UI로 API 테스트하기**
FastAPI의 가장 강력한 기능, **스마트 메뉴판(Swagger UI)**을 이용해봅시다.
1. `uvicorn main:app --reload` 명령으로 서버가 실행 중인지 확인합니다.
2. 웹 브라우저에서 http://127.0.0.1:8000/docs 로 접속합니다.
1. `uvicorn main:app --reload` 명령으로 서버가 실행 중인지 확인합니다.
2. 웹 브라우저에서 **`http://127.0.0.1:8000/docs`** 로 접속합니다.
> **💡 비유: '스마트 메뉴판 Swagger UI'**
>
> `.../docs`는 단순한 메뉴판이 아닙니다.
> **💡 비유: '스마트 메뉴판' Swagger UI**
> `/docs`는 단순한 메뉴판이 아닙니다.
> - **모든 메뉴(엔드포인트)가 목록으로** 보입니다.
> - 각 메뉴를 클릭하면 **필요한 재료(요청 형식)와 나오는 요리(응답 형식)**에 대한 상세한 설명이 나옵니다.
> - 가장 멋진 기능! **`Try it out`** 버튼을 누르면 메뉴판에서 바로 **'맛보기 주문'**을 넣어볼 수 있습니다. 주방에 직접 가지 않아도 그 자리에서 바로 요리 결과를 확인할 수 있습니다.
3. `POST /predict` 메뉴를 클릭하여 펼친 뒤 `Try it out` 버튼을 누릅니다.
4. **Request body**에 예시로 채워진 주문서를 그대로 두거나 다른 값으로 바꿔봅니다.
```json
{
"features": [6.7, 3.0, 5.2, 2.3]
}
```
5. `Execute` 버튼을 눌러 주문을 주방으로 전송하고, 그 아래에서 방금 나온 따끈따끈한 요리(예측 결과)를 실시간으로 확인할 수 있습니다.
### 문제:
1. `/docs` 페이지에서 `POST /predict` 메뉴를 클릭하여 펼치고 `Try it out` 버튼을 누르세요.
2. **Request body**에 다른 붓꽃 데이터를 넣어보세요. (예: `versicolor` 품종인 `[6.0, 2.2, 4.0, 1.0]` 또는 `virginica` 품종인 `[6.7, 3.0, 5.2, 2.3]`)
3. `Execute` 버튼을 눌러 예측 결과를 확인하고, 모델이 올바르게 예측하는지 검증해보세요.
---
## 📝 8주차 요약
## 7. 되짚어보기 (Summary)
이번 주에는 FastAPI를 이용하여 머신러닝 모델을 API로 만드는 첫걸음을 떼었습니다.
- **FastAPI**: 'AI 레스토랑'을 만드는 웹 서버 프레임워크 FastAPI를 배우고 **Uvicorn**으로 실행했습니다.
- **lifespan**: 서버 시작 시 모델('주방기구')을 효율적으로 로드했습니다.
- **Pydantic**: 입력 데이터('주문서')의 형식을 강제하고 유효성을 검사했습니다.
- **Swagger UI**: '/docs'의 '스마트 메뉴판'을 통해 API를 쉽고 빠르게 테스트했습니다.
다음 시간에는 이 '1인 식당'을 여러 셰프와 직원이 협업하는 '프랜차이즈 레스토랑'으로 확장하는 방법을 배웁니다. 프로젝트의 구조를 체계적으로 개선하고, **[Docker](./glossary.md#docker)**를 이용해 우리 레스토랑을 어떤 쇼핑몰에든 쉽게 입점시키는 '밀키트' 기술을 배우겠습니다.
## 🔍 8주차 연습 문제
**문제 1: 7주차 Fashion-MNIST 모델 서빙하기**
- 7주차에서 만들었던 PyTorch CNN 모델(`SimpleCNN`)을 저장하고, 이 모델을 로드하여 예측하는 FastAPI 서버를 만들어보세요.
- **요구사항**:
1. `torch.save(model.state_dict(), 'fashion_mnist_cnn.pth')`로 모델의 가중치를 저장합니다.
2. 서버 시작 시(lifespan)에 모델 구조를 만들고 저장된 가중치를 `model.load_state_dict(...)`로 불러옵니다. `model.eval()` 모드로 설정하는 것을 잊지 마세요.
3. 입력 데이터는 28x28 픽셀 값을 펼친 784개의 float 리스트로 받도록 Pydantic 모델을 정의하세요.
4. 예측 엔드포인트(`POST /predict/fashion`)는 입력 리스트를 받아 `torch.Tensor`로 변환하고, 모델에 맞는 모양(`1, 1, 28, 28`)으로 reshape하여 예측을 수행해야 합니다.
5. 예측 결과로 가장 확률이 높은 클래스의 이름(e.g., 'T-shirt/top', 'Sneaker')을 반환하세요.
**문제 2: 책 정보 조회 API 만들기**
- 간단한 인-메모리(in-memory) 딕셔너리를 '데이터베이스'로 사용하여, 책 정보를 조회하는 API를 만들어보세요.
- **요구사항**:
1. 서버에 `books_db = {1: {"title": "클린 코드", "author": "로버트 C. 마틴"}, 2: ...}` 와 같이 책 데이터를 미리 만들어두세요.
2. `GET /books/{book_id}` 엔드포인트를 만드세요.
3. **경로 매개변수**로 받은 `book_id`를 이용하여 `books_db`에서 책을 찾아 정보를 반환하세요.
4. 만약 해당 `book_id`가 DB에 없다면, FastAPI의 `HTTPException`을 이용하여 `404 Not Found` 오류와 함께 "해당 ID의 책을 찾을 수 없습니다." 메시지를 반환하세요.
- **AI 레스토랑**: 모델을 '레시피', API 서버를 '레스토랑'에 비유하여 모델 서빙의 개념을 이해했습니다.
- **FastAPI와 Uvicorn**: 파이썬 웹 서버를 구축하고 실행하는 방법을 배웠습니다.
- **Pydantic과 lifespan**: API의 데이터 형식을 강제하고, 서버 시작 시 모델을 효율적으로 로드하는 방법을 익혔습니다.
- **Swagger UI**: `/docs`의 '스마트 메뉴판'을 통해 API를 쉽고 빠르게 테스트하는 강력한 기능을 체험했습니다.
## 8. 더 깊이 알아보기 (Further Reading)
- [FastAPI 공식 튜토리얼](https://fastapi.tiangolo.com/tutorial/): FastAPI의 모든 것을 배울 수 있는 최고의 가이드
- [Pydantic 공식 문서](https://docs.pydantic.dev/latest/): 데이터 유효성 검사를 더 깊이 있게 활용하는 방법
- [Serving Machine Learning Models](https://www.manning.com/books/serving-machine-learning-models): 모델 서빙에 대한 전반적인 내용을 다루는 전문 서적
---
**➡️ 다음 시간: [Part 9: 프로덕션 레벨 API와 Docker](./part_9_production_ready_api.md)**
# Part 9: 프로덕션 레벨 API와 Docker (9주차)
# Part 9: 프로덕션 레벨 API와 Docker
**⬅️ 이전 시간: [Part 8: FastAPI를 이용한 모델 서빙](./part_8_model_serving_with_fastapi.md)** | **➡️ 다음 시간: [Part 10: 전문가로 가는 길](./part_10_expert_path.md)**
**⬅️ 이전 시간: [Part 8: FastAPI를 이용한 모델 서빙](./part_8_model_serving_with_fastapi.md)**
**➡️ 다음 시간: [Part 10: 전문가로 가는 길](./part_10_expert_path.md)**
---
## 📜 실습 코드 바로가기
- **`part_9_production_ready_api`**: [바로가기](./source_code/part_9_production_ready_api)
- 본 파트에서 다루는 프로덕션 레벨의 FastAPI 프로젝트 구조(`app` 폴더), `Dockerfile`, `docker-compose.yml` 등의 전체 코드를 확인하고 직접 실행해볼 수 있습니다.
---
## 1. 학습 목표 (Learning Objectives)
지난 시간에는 FastAPI를 이용해 '1인 식당'처럼 빠르게 AI 모델 API를 만들어보았습니다. 프로토타입으로는 훌륭하지만, 메뉴가 다양해지고(기능 추가) 여러 셰프가 협업해야 하는(팀 개발) 실제 '프랜차이즈 레스토랑' 환경에서는 한계가 명확합니다.
이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.
이번 주차에는 `main.py` 하나에 모든 것을 담던 방식에서 벗어나, 유지보수와 확장이 용이한 **프로덕션 레벨의 프로젝트 구조**로 우리 레스토랑을 리팩터링합니다. 또한, 어떤 백화점에 입점하든 동일한 맛과 서비스를 보장하는 **'밀키트([Docker](./glossary.md#docker) 컨테이너)'** 기술을 배워, 우리 AI 서비스를 세상 어디에든 쉽고 안정적으로 배포할 수 있는 능력을 갖추게 될 것입니다.
- 유지보수와 협업에 용이하도록 FastAPI 프로젝트를 기능별로 구조화할 수 있습니다.
- `APIRouter`를 사용하여 API 엔드포인트를 모듈화하고 관리할 수 있습니다.
- "제 컴퓨터에선 되는데요..." 문제를 해결하는 Docker의 원리를 이해하고, 이미지/컨테이너 개념을 설명할 수 있습니다.
- `Dockerfile`을 작성하여 파이썬 애플리케이션을 컨테이너 이미지로 만들 수 있습니다.
- `docker-compose.yml`을 사용하여 여러 서비스(API 서버, DB 등)를 한 번에 실행하고 관리할 수 있습니다.
---
### 📖 9주차 학습 목표
## 2. 핵심 키워드 (Keywords)
`프로젝트 구조화`, `관심사 분리(SoC)`, `APIRouter`, `의존성 주입(DI)`, `Docker`, `컨테이너(Container)`, `이미지(Image)`, `Dockerfile`, `Docker Compose`
## 3. 도입: '1인 식당'에서 '프랜차이즈 레스토랑'으로
- **프로젝트 구조화**: '1인 식당'에서 '프랜차이즈 본사'로 확장하며, 유지보수와 협업에 용이한 전문적인 프로젝트 구조를 이해합니다.
- **관심사 분리**: API의 각 기능(설정, DB, 모델, 스키마, 라우터)을 독립적인 파일로 분리하고, APIRouter로 엔드포인트를 기능별('한식/중식 코너')로 모듈화합니다.
- **[Docker](./glossary.md#docker) 개념 이해**: "제 컴퓨터에선 되는데요..." 문제를 해결하는 컨테이너 기술의 원리를 이해하고, 이미지/컨테이너/도커파일의 개념을 익힙니다.
- **[Dockerfile](./glossary.md#dockerfile) 작성**: 'AI 레스토랑 밀키트 레시피'인 Dockerfile을 직접 작성하여 우리 앱을 이미지로 만듭니다.
- **[Docker Compose](./glossary.md#docker-compose) 활용**: API 서버, 데이터베이스 등 여러 서비스를 명령어 하나로 관리하는 `docker-compose.yml`의 원리를 익힙니다.
지난 시간에는 FastAPI로 '1인 식당'처럼 빠르게 AI 모델 API를 만들었습니다. 프로토타입으로는 훌륭하지만, 메뉴가 다양해지고(기능 추가) 여러 셰프가 협업해야 하는(팀 개발) 실제 '프랜차이즈 레스토랑' 환경에서는 한계가 명확합니다.
이번 주차에는 `main.py` 하나에 모든 것을 담던 방식에서 벗어나, **유지보수와 확장이 용이한 프로젝트 구조**로 우리 레스토랑을 리팩터링합니다. 또한, 어떤 백화점에 입점하든 동일한 맛과 서비스를 보장하는 **'밀키트(Docker 컨테이너)'** 기술을 배워, 우리 AI 서비스를 세상 어디에든 쉽고 안정적으로 배포하는 능력을 갖추게 될 것입니다.
> [!TIP]
> 본 파트의 모든 예제 코드는 `source_code/part_9_production_ready_api` 폴더에서 직접 실행하고 수정해볼 수 있습니다.
---
### 1-2일차: 프로덕션 레벨 프로젝트 구조 설계
#### **왜 구조를 바꿔야 할까?**
`main.py` 파일 하나에 모든 코드가 있는 것은, 주방장이 혼자 주문받고, 요리하고, 서빙하고, 계산까지 하는 동네 분식집과 같습니다. 처음엔 빠르지만, 손님이 많아지면 금방 한계에 부딪힙니다.
## 4. 1단계: 프로덕션 레벨 프로젝트 구조 설계
> **💡 비유: '1인 식당'에서 '프랜차이즈 레스토랑'으로**
>
> 우리는 이제 레스토랑을 프랜차이즈화할 것입니다. 이를 위해서는 역할 분담이 필수적입니다.
> - **`settings.py`**: 본사의 운영 방침 (환경 설정)
> - **`database.py`**: 식자재 창고와의 연결 통로 (DB 접속)
> - **`models.py`**: 창고에 보관된 식자재의 형태 정의 (DB 테이블 구조)
> - **`schemas.py`**: 모든 지점에서 통일된 '주문서 양식' (데이터 형식 정의)
> - **`crud.py`**: 식자재를 창고에서 꺼내고 넣는 '표준 작업 절차' (DB 처리 로직)
> - **`routers/`**: '한식 코너', '중식 코너' 등 메뉴별로 분리된 주방 (기능별 API)
> - **`main.py`**: 모든 코너를 총괄하고 손님을 맞는 '총지배인'
> **🎯 1-2일차 목표:** '관심사 분리' 원칙에 따라 FastAPI 프로젝트를 기능별 파일과 폴더로 재구성합니다.
`main.py` 하나에 모든 코드가 있는 것은, 주방장이 혼자 주문받고, 요리하고, 서빙까지 하는 것과 같습니다. 우리는 이제 역할을 분담하여 '프랜차이즈 본사'처럼 체계적인 시스템을 만들 것입니다.
#### **1단계: 프로젝트 폴더 및 기본 설정 파일 생성**
- **`settings.py`**: 본사의 운영 방침 (환경 설정)
- **`schemas.py`**: 모든 지점에서 통일된 '주문서 양식' (데이터 형식 정의)
- **`routers/`**: '한식 코너', '중식 코너' 등 메뉴별로 분리된 주방 (기능별 API)
- **`main.py`**: 모든 코너를 총괄하고 손님을 맞는 '총지배인'
먼저 아래 구조에 따라 `fastapi_project` 폴더 및 하위 폴더들을 만듭니다.
### 4-1. 프로젝트 폴더 및 라우터 분리
먼저 아래와 같이 프로젝트 구조를 만듭니다. `main.py`에 있던 예측 관련 로직을 `routers/predictions.py`로 옮깁니다.
```
/fastapi_project
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── settings.py
│ ├── database.py
│ ├── models.py
│ ├── schemas.py
│ ├── crud.py
│ └── routers/
│ ├── __init__.py
│ └── predictions.py
......@@ -60,160 +56,48 @@
└── iris_model.pkl
```
**`app/settings.py` (운영 방침)**: 환경변수로 설정을 관리합니다.
```python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = "sqlite:///./iris_prediction.db"
class Config:
env_file = ".env"
settings = Settings()
```
**`app/database.py` (창고 연결)**: 데이터베이스 연결을 설정합니다.
```python
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from .settings import settings
engine = create_engine(
settings.DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
```
- **[SQLAlchemy](./glossary.md#sqlalchemy)**: 파이썬 코드로 데이터베이스와 상호작용할 수 있게 해주는 ORM(Object-Relational Mapping) 라이브러리입니다. SQL 쿼리를 직접 작성하지 않고, 파이썬 객체(모델)를 통해 DB 테이블을 조작할 수 있습니다.
#### **2단계: 데이터 모델과 로직 분리**
**`app/models.py` (식자재의 형태 정의)**: DB 테이블 구조를 정의합니다.
```python
from sqlalchemy import Column, Integer, String, DateTime, Float
from .database import Base
import datetime
class PredictionLog(Base):
__tablename__ = "prediction_logs"
id = Column(Integer, primary_key=True, index=True)
input_features = Column(String)
predicted_class = Column(String, index=True)
confidence = Column(Float)
created_at = Column(DateTime, default=datetime.datetime.utcnow)
```
**`app/schemas.py` (표준 주문서)**: API가 주고받을 데이터 형식을 정의합니다.
**`app/routers/predictions.py` (예측 코너 주방)**:
`APIRouter`를 사용하여 예측 기능만을 위한 독립적인 API 그룹을 만듭니다.
```python
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List
import datetime
import joblib
import numpy as np
# 1. 이 라우터만의 '주문서 양식' 정의
class PredictionRequest(BaseModel):
features: List[float]
class PredictionResponse(BaseModel):
id: int
input_features: str
predicted_class: str
confidence: float
created_at: datetime.datetime
# 2. APIRouter 인스턴스 생성
router = APIRouter(prefix="/predict", tags=["predictions"])
class Config:
from_attributes = True
```
# 3. 모델 로드 (임시)
model = joblib.load("iris_model.pkl")
class_names = ['setosa', 'versicolor', 'virginica']
**`app/crud.py` (표준 작업 절차)**: 데이터베이스 처리 함수를 정의합니다.
```python
from sqlalchemy.orm import Session
from . import models, schemas
import json
def create_prediction_log(db: Session, request: schemas.PredictionRequest, predicted_class: str, confidence: float):
features_str = json.dumps(request.features)
db_log = models.PredictionLog(
input_features=features_str,
predicted_class=predicted_class,
confidence=confidence
)
db.add(db_log)
db.commit()
db.refresh(db_log)
return db_log
```
---
### 3일차: 기능별 API 라우터 분리
**APIRouter**를 이용하면 '한식 코너', '중식 코너'처럼 기능별로 API를 분리할 수 있습니다.
**`app/routers/predictions.py` (예측 코너 주방)**
```python
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
import joblib
import numpy as np
from .. import crud, schemas
from ..database import SessionLocal
router = APIRouter(
prefix="/predict",
tags=["predictions"]
)
# 모델 로드
try:
model = joblib.load("iris_model.pkl")
class_names = ['setosa', 'versicolor', 'virginica']
except FileNotFoundError:
model = None
# 의존성 주입: '주방 보조'가 DB 연결을 가져다 줌
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@router.post("/", response_model=schemas.PredictionResponse)
def create_prediction(request: schemas.PredictionRequest, db: Session = Depends(get_db)):
if model is None:
raise HTTPException(status_code=503, detail="모델을 로드할 수 없습니다.")
# 예측
# 4. 예측 엔드포인트 정의
@router.post("/", summary="붓꽃 품종 예측")
def predict_iris(request: PredictionRequest):
model_input = np.array(request.features).reshape(1, -1)
prediction_idx = model.predict(model_input)[0]
confidence = model.predict_proba(model_input)[0][prediction_idx]
predicted_class = class_names[prediction_idx]
# DB에 로그 저장 (CRUD 함수 사용)
return crud.create_prediction_log(db, request, predicted_class, confidence)
return {
"predicted_class": class_names[prediction_idx],
"confidence": float(confidence)
}
```
- **[`Depends(get_db)`](./glossary.md#의존성-주입dependency-injection-di) (의존성 주입)**: `create_prediction`이라는 '셰프'가 요리를 시작하기 전에, FastAPI라는 '총지배인'이 `get_db`라는 '주방 보조'를 시켜 DB 연결이라는 '깨끗한 프라이팬'을 가져다주는 것과 같습니다. 셰프는 프라이팬을 어떻게 가져왔는지 신경 쓸 필요 없이 요리에만 집중할 수 있습니다.
#### **최종 통합 (`app/main.py`)**
분리된 모든 조각을 '총지배인' `main.py`에서 하나로 조립합니다.
### 4-2. 최종 통합 (`main.py`)
'총지배인' `main.py`에서 분리된 '예측 코너'를 전체 레스토랑에 포함시킵니다.
```python
from fastapi import FastAPI
from .database import engine, Base
from .routers import predictions
# DB 테이블 생성
Base.metadata.create_all(bind=engine)
app = FastAPI(
title="AI 레스토랑 (프랜차이즈 ver)",
description="프로덕션 레벨 구조로 개선된 붓꽃 예측 API입니다."
)
app = FastAPI(title="AI 레스토랑 (프랜차이즈 ver)")
# "예측 코너" 라우터 포함
app.include_router(predictions.router)
......@@ -222,18 +106,22 @@ app.include_router(predictions.router)
def read_root():
return {"message": "AI 레스토랑에 오신 것을 환영합니다."}
```
이제 `uvicorn app.main:app --reload`로 서버를 실행하고 `http://127.0.0.1:8000/docs`에 접속하면, `predictions`라는 태그로 그룹화된 API를 확인할 수 있습니다.
---
### 4일차: Docker로 우리 앱 포장하기
#### **왜 Docker를 사용해야 할까?**
## 5. 2단계: Docker로 우리 앱 포장하기
> **🎯 3-4일차 목표:** Dockerfile을 작성하여 우리 앱을 이미지로 만들고, docker-compose로 실행합니다.
### 5-1. 왜 Docker를 써야 할까?
> **💡 비유: '어디서든 똑같은 맛을 내는 밀키트'**
>
> 우리 레스토랑의 레시피와 재료를 완벽하게 준비해도, 지점마다 주방 환경(OS, 라이브러리 버전 등)이 다르면 음식 맛이 달라질 수 있습니다. "제 주방(컴퓨터)에서는 되는데요?" 라는 문제는 개발자들의 흔한 악몽입니다.
>
> **[Docker](./glossary.md#docker)**는 우리 레스토랑의 모든 것(코드, 라이브러리, 설정)을 하나의 **'밀키트(이미지)'** 로 완벽하게 포장하는 기술입니다. 이 밀키트는 어떤 주방(서버)에서 데우든 항상 똑같은 환경에서, 똑같은 맛의 요리를 만들어냅니다.
> **Docker**는 우리 레스토랑의 모든 것(코드, 라이브러리, 설정)을 하나의 **'밀키트(이미지)'**로 완벽하게 포장하는 기술입니다. 이 밀키트는 어떤 주방(서버)에서 데우든 항상 똑같은 환경에서, 똑같은 맛의 요리를 만들어냅니다.
#### **Dockerfile 작성하기**
### 5-2. Dockerfile: 밀키트 레시피 작성
`Dockerfile`은 이 밀키트를 만드는 '레시피'입니다. 프로젝트 최상위 폴더에 `Dockerfile`이라는 이름으로 파일을 만듭니다.
```dockerfile
......@@ -241,113 +129,70 @@ def read_root():
FROM python:3.9-slim
# 2. 작업 디렉토리 설정 (조리대 위치 지정)
WORKDIR /app
WORKDIR /code
# 3. 의존성 파일 먼저 복사 (소스가 바뀌어도 의존성이 같으면 캐시 재사용)
COPY ./requirements.txt .
COPY ./requirements.txt /code/requirements.txt
# 4. 의존성 설치 (필요한 조미료 채우기)
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# 5. 소스 코드 복사 (메인 레시피와 도구들 넣기)
COPY ./app /app/app
COPY ./iris_model.pkl /app/
# 5. 소스 코드와 모델 파일 복사 (레시피와 식재료 넣기)
COPY ./app /code/app
COPY ./iris_model.pkl /code/iris_model.pkl
# 6. 실행 명령어 (서버 매니저에게 가게 오픈 지시)
# 0.0.0.0 주소는 '어느 손님이든 다 받으라'는 의미
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
# 6. 서버 실행 (레스토랑 개점!)
# 0.0.0.0 주소로 실행해야 Docker 외부에서 접속 가능
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
#### **Docker 이미지 빌드 및 실행**
터미널에서 `Dockerfile`이 있는 폴더로 이동한 뒤, 아래 명령어를 실행합니다.
```bash
# 1. '프랜차이즈 1호점' 밀키트(이미지) 만들기
docker build -t franchise-restaurant:1.0 .
# 2. 밀키트를 데워 레스토랑(컨테이너) 열기
# -p 8000:80 -> 내 컴퓨터 8000번 문과 컨테이너 80번 문을 연결
docker run -d -p 8000:80 --name my-first-restaurant franchise-restaurant:1.0
```
---
### 5일차: Docker Compose로 여러 서비스 관리하기
#### **왜 Docker Compose가 필요할까?**
> **💡 비유: '푸드코트 전체를 한 번에 오픈하기'**
>
> 지금까지는 '레스토랑' 하나만 Docker로 열었습니다. 하지만 실제 서비스는 API 서버 외에도 데이터베이스, 캐시 서버 등 여러 컴포넌트가 함께 동작하는 '푸드코트'와 같습니다.
>
> **[Docker Compose](./glossary.md#docker-compose)**는 이 푸드코트의 전체 설계도(`docker-compose.yml`)와 같습니다. 각 코너(컨테이너)의 종류, 위치, 연결 관계를 모두 정의해두면, **`docker-compose up`** 이라는 명령어 한 번으로 전체 푸드코트를 열고 닫을 수 있습니다.
#### **docker-compose.yml 작성하기**
프로젝트 최상위 폴더에 `docker-compose.yml` 파일을 만듭니다.
### 5-3. Docker Compose: 프랜차이즈 매장 동시 오픈
`docker-compose.yml`은 여러 '밀키트'(서비스)들을 정의하고 한 번에 관리하는 '프랜차이즈 오픈 계획서'입니다. 지금은 API 서버 하나지만, 나중에 DB 등이 추가되면 그 위력을 실감하게 됩니다.
```yaml
version: '3.8' # 파일 형식 버전
# docker-compose.yml
version: '3.8'
services: # 여기에 우리 푸드코트에 입점할 가게들 목록을 정의
# 1. API 서버 (우리의 메인 레스토랑)
services:
# 서비스 이름 정의 (우리 레스토랑 지점)
api:
build: . # 현재 폴더의 Dockerfile을 사용해 이미지를 빌드
container_name: franchise_api
# 어떤 Dockerfile을 사용할지 지정
build: .
# 포트 매핑 (외부 8000번 포트 -> 컨테이너 내부 8000번 포트)
ports:
- "8000:80" # 내 컴퓨터 8000번과 컨테이너 80번 포트 연결
volumes:
- ./app:/app/app # 소스 코드를 실시간으로 동기화 (개발 시 유용)
environment:
- DATABASE_URL=postgresql://user:password@db/mydatabase
# 2. 데이터베이스 서버 (PostgreSQL 식자재 창고)
db:
image: postgres:13 # PostgreSQL 공식 이미지 사용
container_name: franchise_db
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydatabase
volumes: # 데이터 영구 보관 장소
postgres_data:
- "8000:8000"
# 컨테이너에 이름 부여
container_name: fastapi_iris_app
```
#### **Docker Compose 실행**
터미널에서 `docker-compose.yml` 파일이 있는 폴더로 이동하여 실행합니다.
---
```bash
# 푸드코트 전체 오픈 (백그라운드에서 실행)
docker-compose up -d
## 6. 직접 해보기 (Hands-on Lab): Docker로 레스토랑 오픈하기
# 푸드코트 전체 폐점 (컨테이너, 네트워크 등 모두 정리)
docker-compose down
```
이제 우리는 명령어 한 번으로 복잡한 멀티 컨테이너 애플리케이션을 손쉽게 관리할 수 있게 되었습니다!
> **🎯 5일차 목표:** Dockerfile과 docker-compose.yml을 사용하여, 단 한 줄의 명령어로 FastAPI 서버를 실행합니다.
---
## 📝 9주차 요약
### 문제:
1. **`requirements.txt` 파일 생성**: 프로젝트에 필요한 라이브러리(`fastapi`, `uvicorn`, `scikit-learn`, `joblib`, `numpy`) 목록을 `requirements.txt` 파일에 작성하세요.
2. **`Dockerfile` 작성**: 위 템플릿을 참고하여 프로젝트 최상위 경로에 `Dockerfile`을 완성하세요.
3. **`docker-compose.yml` 작성**: 위 템플릿을 참고하여 프로젝트 최상위 경로에 `docker-compose.yml`을 완성하세요.
4. **Docker로 서버 실행**: 터미널에서 `docker-compose up --build` 명령어를 실행하여 Docker 이미지를 빌드하고 컨테이너를 실행하세요.
5. **결과 확인**: 빌드가 성공적으로 완료되고 서버가 실행되면, 웹 브라우저에서 `http://localhost:8000/docs`에 접속하여 API 문서가 이전과 동일하게 잘 보이는지 확인하세요.
- **프로젝트 구조화**: `main.py` 하나에 있던 코드를 역할(라우터, 스키마, CRUD 등)에 따라 여러 파일로 분리하여 재사용성과 유지보수성을 높였습니다.
- **APIRouter**: 기능별로 API 엔드포인트를 모듈화하여 프로젝트를 체계적으로 관리했습니다.
- **Docker**: '밀키트' 비유를 통해 "제 컴퓨터에선 되는데요?" 문제를 해결하는 컨테이너 기술을 배웠고, `Dockerfile`로 우리 앱을 이미지로 만들었습니다.
- **Docker Compose**: '푸드코트' 비유를 통해 여러 서비스(컨테이너)를 `docker-compose.yml` 파일 하나로 손쉽게 관리하는 방법을 익혔습니다.
---
이제 여러분은 단순히 모델을 만드는 것을 넘어, 실제 서비스로 배포하고 운영할 수 있는 견고한 기반을 다졌습니다.
## 7. 되짚어보기 (Summary)
## 🔍 9주차 연습 문제
이번 주차에는 '1인 식당'을 '프랜차이즈 레스토랑'으로 확장하는 여정을 통해, 프로덕션 레벨의 AI 서비스를 구축하는 핵심 기술을 배웠습니다.
**문제 1: 라우터 분리 연습**
- 8주차 연습 문제 2번에서 만들었던 책 조회 API(`GET /books/{book_id}`)를 `routers/books.py` 파일로 분리하고, `main.py`에 포함시켜 보세요.
- `books.py` 라우터의 `prefix``/api/v1/books`로 설정하고, 태그는 `books`로 지정해보세요.
- **프로젝트 구조화**: `APIRouter`를 사용해 기능별로 코드를 분리하여, 유지보수와 협업이 용이한 프로젝트 구조를 설계했습니다.
- **Docker 컨테이너화**: '밀키트' 비유를 통해 Docker의 필요성을 이해하고, `Dockerfile`을 작성하여 우리 앱을 재현 가능한 이미지로 만들었습니다.
- **Docker Compose**: `docker-compose.yml`을 통해 여러 서비스를 명령어 하나로 관리하는 방법을 익혀, 복잡한 애플리케이션 배포의 기초를 다졌습니다.
**문제 2: Dockerfile 개선**
- 현재 `Dockerfile`은 소스 코드가 바뀔 때마다 `pip install`을 다시 수행할 수 있습니다. 어떻게 하면 의존성이 바뀌지 않았을 때 `pip install` 과정을 건너뛰고 캐시를 사용할 수 있을까요? (`COPY` 명령어의 순서를 조정해보세요.)
## 8. 더 깊이 알아보기 (Further Reading)
- [TestDriven.io: FastAPI and Docker](https://testdriven.io/blog/dockerizing-fastapi/): Docker를 사용한 FastAPI 배포에 대한 상세한 튜토리얼
- [Docker 공식 문서: Get Started](https://docs.docker.com/get-started/): Docker의 기본 개념부터 차근차근 배울 수 있는 최고의 자료
- [『코어 자바스크립트』 저자의 Docker/k8s 강의](https://www.inflearn.com/course/%EB%8F%84%EC%BB%A4-k8s-%ED%95%B5%EC%8B%AC%EA%B0%9C%EB%념-%EC%9D%B5%ED%9E%88%EA%B8%B0): Docker와 쿠버네티스의 핵심 원리를 비유를 통해 쉽게 설명하는 강의
**문제 3: Docker Compose에 Redis 추가하기**
- `docker-compose.yml` 파일에 **Redis** 캐시 서버를 추가해보세요.
- 서비스 이름은 `cache`, 이미지는 공식 `redis:alpine` 이미지를 사용합니다.
- 포트는 내 컴퓨터의 `6379`번과 컨테이너의 `6379`번을 연결하세요.
---
**➡️ 다음 시간: [Part 10: 전문가로 가는 길](./part_10_expert_path.md)**
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment