트랜스포머·PEFT 2일 과정1장 · 준비 수학과 Transformer 개요15 / 17← 이전목차다음 →MetaContents

1장. 준비 수학과 Transformer 개요

프로젝트 1 — 취향 점수를 추천 확률로 바꾸는 도우미

작은 동네 영화 모임에서 이번 주 상영작 다섯 편 가운데 회원마다 무엇을 권할지 정한다고 해 봅시다. 회원 세 명은 장르 취향을 점수로 적어 냈고, 영화마다 장르 성격도 점수로 적혀 있습니다. 회원과 영화가 얼마나 잘 맞는지는 내적으로, "이 영화를 권할 확률"은 Softmax 로 정합니다. 이 장에서 배운 행렬곱, 내적, Softmax 만으로 추천 도우미를 만듭니다. 모든 계산은 NumPy 로 CPU 에서 끝납니다.

요구사항

  1. 영화 5편 × 장르 4개(액션, 로맨스, 코미디, 공포) 성격표와 회원 3명 × 장르 4개 취향표를 NumPy 배열과 DataFrame 으로 만들고 shape 를 찍습니다.
  2. 행렬곱 한 번으로 회원 3명 × 영화 5편의 내적 점수표를 만들고, 한 칸을 손으로 계산해 맞는지 확인합니다.
  3. 노트북 셀 14의 softmax 로 점수표를 행마다 확률로 바꾸고, 행마다 합이 1인지 확인합니다.
  4. 회원마다 확률이 가장 높은 영화 두 편을 골라 퍼센트와 함께 찍습니다.
  5. 회원, 영화, 점수, 확률을 한 표로 모아 out_ch01_recommend.csv 로 저장하고 다시 읽어 행 수를 확인합니다.

힌트

  1. 점수표는 users @ movies.T 입니다. (3, 4) @ (4, 5) 라서 결과는 (3, 5) 입니다. .T 는 행과 열을 바꿉니다.
  2. 행마다 확률을 만들려면 softmax 의 axis=-1 을 그대로 쓰면 됩니다.
  3. 큰 값부터 고르려면 np.argsort(-p[i])[:2] 처럼 음수를 붙여 정렬합니다.
  4. 표로 모을 때는 pd.DataFrame(점수표, index=회원, columns=영화).stack() 으로 긴 표를 만들 수 있습니다.
  5. 장르 점수는 설명을 위해 정한 값입니다. 숫자를 바꿔 가며 추천이 어떻게 달라지는지 보면 더 좋습니다.

채점 기준

항목기준점수
표 만들기성격표 (5, 4) 와 취향표 (3, 4) 를 만들고 shape 를 찍는다15
내적 점수표행렬곱 한 번으로 (3, 5) 점수표를 만들고 한 칸을 손계산과 맞춘다25
Softmax행마다 확률로 바꾸고 행 합이 1인지 확인한다25
추천 고르기회원마다 확률 상위 두 편을 퍼센트와 함께 찍는다20
저장한 표로 모아 csv 로 저장하고 다시 읽어 확인한다15

코드의 흐름

프로젝트 코드의 실행 흐름도. 영화 성격표 movies 와 회원 취향표 users 를 만들고, users @ movies.T 로 (3, 5) 점수표를 만든 뒤 한 칸을 손계산 0.95 와 맞춘다. softmax 로 행마다 합 1 인 확률을 만들고, 회원마다 도는 반복에서 argsort 로 두 편을 골라 찍은 뒤, 긴 표로 펴서 csv 로 저장하고 다시 읽어 15 행을 확인한다
단계하는 일반복과 조건핵심 한 줄
1영화 성격표 (5, 4) 와 회원 취향표 (3, 4) 만들기없음users = np.array([...])
2행렬곱 한 번으로 (3, 5) 점수표 만들고 한 칸 손계산없음scores = users @ movies.T
3점수를 행마다 합 1 인 확률로 바꾸기없음probs = softmax(scores)
4회원마다 확률 상위 두 편 고르기회원 3명을 한 명씩 도는 반복(for i, name)top2 = np.argsort(-probs[i])[:2]
5점수와 확률을 긴 표로 펴서 csv 로 저장하고 다시 읽기없음table.to_csv("out_ch01_recommend.csv", ...)
  • 반복 고리: 4단계의 마름모 "회원 남았나?"에서 "예"로 내려가 두 편을 골라 찍고, 왼쪽 선을 따라 다시 마름모로 돌아옵니다. 세 명을 다 돌면 "아니요"로 빠져나가 5단계로 갑니다.
  • 반복이 필요 없는 곳: 2, 3단계는 회원 3명 × 영화 5편을 행렬곱과 softmax 한 번으로 한꺼번에 계산합니다. pandas 에서 반복문 대신 열 단위 계산을 쓰는 것과 같습니다.
  • 짜임: 2단계의 내적 점수표가 "얼마나 잘 맞나"를, 3단계의 Softmax 가 "얼마의 비율로 권할까"를 맡습니다. Attention 이 내적으로 관련도를 매기고 Softmax 로 비율을 정하는 것과 같은 짜임입니다.
결과 보기 (예시 풀이)

1단계. 영화 성격표와 회원 취향표 만들기

Python
import numpy as np
import pandas as pd

# 장르 4개: 열의 차례
genres = ["액션", "로맨스", "코미디", "공포"]
# 영화 5편의 장르 성격(0~1, 클수록 그 장르 색이 짙다). 설명을 위해 정한 값이다
movie_names = ["질주하는 도시", "봄날의 편지", "웃음 택배", "검은 복도", "우주 구조대"]
movies = np.array([
    [0.9, 0.1, 0.2, 0.1],   # 질주하는 도시: 액션
    [0.0, 0.9, 0.3, 0.0],   # 봄날의 편지: 로맨스
    [0.2, 0.2, 0.9, 0.0],   # 웃음 택배: 코미디
    [0.3, 0.0, 0.1, 0.9],   # 검은 복도: 공포
    [0.8, 0.3, 0.4, 0.2],   # 우주 구조대: 액션에 코미디가 조금
])
# 회원 3명의 장르 취향(-1~1, 음수는 싫어한다는 뜻). 설명을 위해 정한 값이다
user_names = ["민지", "준호", "서연"]
users = np.array([
    [1.0, -0.5, 0.5, 0.0],   # 민지: 액션을 좋아하고 로맨스는 별로
    [-0.5, 1.0, 0.5, -1.0],  # 준호: 로맨스를 좋아하고 공포는 싫어한다
    [0.0, 0.0, 0.5, 1.0],    # 서연: 공포를 좋아한다
])

print("영화 성격표 shape:", movies.shape)   # (영화 수, 장르 수)
print("회원 취향표 shape:", users.shape)    # (회원 수, 장르 수)
# DataFrame 으로 보면 행과 열 이름이 붙어 읽기 쉽다
print(pd.DataFrame(users, index=user_names, columns=genres))
출력
영화 성격표 shape: (5, 4)
회원 취향표 shape: (3, 4)
     액션  로맨스  코미디   공포
민지  1.0 -0.5  0.5  0.0
준호 -0.5  1.0  0.5 -1.0
서연  0.0  0.0  0.5  1.0

2단계. 내적으로 점수표 만들기

Python
# (3, 4) @ (4, 5) → (3, 5): 회원마다 영화 5편과의 내적을 한 번에
scores = users @ movies.T
score_df = pd.DataFrame(scores, index=user_names, columns=movie_names)
print("점수표 shape:", scores.shape)
print(score_df.round(2))

# 한 칸 손계산: 민지 × 질주하는 도시
hand = 1.0 * 0.9 + (-0.5) * 0.1 + 0.5 * 0.2 + 0.0 * 0.1   # 0.9 - 0.05 + 0.1 + 0 = 0.95
print("손계산:", round(hand, 2), "/ 행렬곱:", round(scores[0, 0], 2))
출력
점수표 shape: (3, 5)
    질주하는 도시  봄날의 편지  웃음 택배  검은 복도  우주 구조대
민지     0.95   -0.30   0.55   0.35    0.85
준호    -0.35    1.05   0.55  -1.00   -0.10
서연     0.20    0.15   0.45   0.95    0.40
손계산: 0.95 / 행렬곱: 0.95

3단계. Softmax 로 추천 확률 만들기

Python
# 노트북 셀 14와 같은 softmax: 마지막 축(행마다) 기준으로 합이 1
def softmax(z):
    e = np.exp(z - z.max(axis=-1, keepdims=True))
    return e / e.sum(axis=-1, keepdims=True)

probs = softmax(scores)          # (3, 5): 회원마다 영화 5편의 추천 확률
prob_df = pd.DataFrame(probs, index=user_names, columns=movie_names)
print(prob_df.round(2))
# 행마다 합이 1인지 확인한다
print("행 합:", probs.sum(axis=1).round(6))
출력
    질주하는 도시  봄날의 편지  웃음 택배  검은 복도  우주 구조대
민지     0.29    0.08   0.20   0.16    0.27
준호     0.11    0.44   0.26   0.06    0.14
서연     0.15    0.14   0.20   0.32    0.19
행 합: [1. 1. 1.]

4단계. 회원마다 두 편 고르기

Python
for i, name in enumerate(user_names):
    # argsort 는 작은 값부터 번호를 돌려주므로, 음수를 붙여 큰 값부터 고른다
    top2 = np.argsort(-probs[i])[:2]
    picks = [f"{movie_names[j]}({probs[i, j]:.0%})" for j in top2]
    print(f"{name}: {', '.join(picks)}")
출력
민지: 질주하는 도시(29%), 우주 구조대(27%)
준호: 봄날의 편지(44%), 웃음 택배(26%)
서연: 검은 복도(32%), 웃음 택배(20%)

5단계. 한 표로 모아 저장하기

Python
# stack(): (회원, 영화) 두 이름을 행 번호로 삼는 긴 표로 편다
table = pd.DataFrame({
    "점수": score_df.stack(),
    "확률": prob_df.stack(),
}).reset_index()
table.columns = ["회원", "영화", "점수", "확률"]
table = table.round(4)
# utf-8-sig: 엑셀에서 열어도 한글이 깨지지 않게 저장한다
table.to_csv("out_ch01_recommend.csv", index=False, encoding="utf-8-sig")

check = pd.read_csv("out_ch01_recommend.csv", encoding="utf-8-sig")
print("저장: out_ch01_recommend.csv,", len(check), "행")
print(check.head(5))
출력
저장: out_ch01_recommend.csv, 15 행
   회원       영화    점수      확률
0  민지  질주하는 도시  0.95  0.2932
1  민지   봄날의 편지 -0.30  0.0840
2  민지    웃음 택배  0.55  0.1965
3  민지    검은 복도  0.35  0.1609
4  민지   우주 구조대  0.85  0.2653

점수표의 숫자 차이가 Softmax 를 거치며 비율 차이로 바뀌는 모습을 보았습니다. 민지는 액션 두 편(질주하는 도시 29%, 우주 구조대 27%), 준호는 로맨스(봄날의 편지 44%), 서연은 공포(검은 복도 32%)의 확률이 가장 높게 나옵니다. 민지의 점수 0.95 와 0.85 는 0.1 차이라서 확률도 29% 와 27% 로 가깝고, 준호의 1.05 와 0.55 는 0.5 차이라서 44% 와 26% 로 더 벌어집니다. 점수 사이의 차이가 클수록 확률이 한쪽으로 몰립니다. 장르 점수를 바꾸면 추천도 바로 바뀝니다. 이 프로젝트의 요점은 "내적으로 잘 맞는 정도를 재고, Softmax 로 합이 1인 비율로 바꾼다"입니다. 3장의 Self-Attention 은 회원 자리에 낱말을, 영화 자리에도 낱말을 놓고 똑같은 계산을 합니다.

← 이전 절1장 · 실습 2 — 행렬곱, 내적, Softmax 계산하기다음 절 →1장 · 정리