최종 프로젝트 2주차

이번주는 산출물 작성 및 데이터 조사/수집으로 바쁜 주간이었다.

살면서 이렇게 카드사 홈페이지를 하루종일 며칠 동안이나 들여다보게 될 줄 누가 알았을까


프로젝트 주제

카드사 상담원의 업무 효율성 향상을 위한 AI 기반 실시간 문서 검색 및 상담 후 피드백 지원 시스템

 

 

1. 데이터 고민

카드사로 선정하면서 우선 가장 큰 장점은 데이터가 방대하다는 것이었다.

카드 상품만 해도 몇 백 개 단위고, 각 카드마다 연결된 제휴 브랜드, 혜택, 관련 카드 등

카드라는 게 워낙 많은 사람들이 일상적으로 사용하는 물건이다보니 꽤 여러 방향으로 탐색할 수 있었다.

 

내일배움카드교통패스 같은 국가 정책과 엮여있는 케이스도 다양한데,

이런 정책 관련 내용은 카드사에서 어디까지를 다루어야 하는지도 고민이 필요했다.

그래서 금감원 홈페이지도 모자라 금융법과 소비자보호법까지 뒤져보면서 의도치 않은 지식들도 얻어왔다.

 

고객들의 행동 패턴 역시 중요하다.

나라면 어떤 상황에 뭐가 궁금할지, 상담원한테 전화까지 하는 상황이라면 어떤 문의일지,

기존 카드사들에 빈도 높게 들어오는 유형이 무엇인지 등등

이 부분에서 팀원들과 정말 많은 의견을 나눴다.

 

 

2. 쉽지 않다.

탐색하면서 든 생각은 데이터가 너무 많고 흩어져있다...

유사하거나 아예 같은 카테고리의 내용인데도 서로 다른 페이지에 적혀있고,

한 쪽에 적혀있는 내용이 다른 쪽엔 없어서 뭔가 기준이 다른가? 싶기도 하고,

카드 분실이나 한도 확인 같은 건 내가 직접 신청해야 후속 정보가 나와서 정확히 어떤 프로세스로 처리하는지도 모호하다.

 

무엇보다,

일반 고객들 보라고 이쁘게 홈페이지에 박혀있는 이 내용들을

텍스트로 어떻게 정리해서 어떤 형식으로 전처리해야하는가? 참 난감하다.

약관이나 상품안내를 pdf로 주는건 다행인데 문제는 표가 너무 많다! 추출이 문제다.

 

 

3. 수집 계획

시간은 가고, 서둘러 데이터 수집에 착수해야 했다.

우리팀은 다음과 같이 데이터를 모아보기로 결정했고, 깃허브에 데이터 관리용 레포를 따로 만들어 push하기로 했다.

1. 카드사의 카드 상품 (제휴카드 포함)
2. 특수 목적 카드 (K패스 등)
3. 이용약관, 개인정보 방침 등 법률 기반 안내
4. 해외이용, 신용카드 용어, 대출안내 등 소비자 가이드
5. 실제 상담 데이터
6. 카드사가 주목하는 사회 이슈 (정보유출 사건, 사칭 사기, 보이스피싱 등)

 

나는 여기서 3, 4, 6을 담당했다.

3, 4는 크롤링으로 해결되는 영역이었고

6은 카드사의 '공지사항' 페이지를 대상으로 조사했는데, 페이지가 CSR 형식으로 되어있어 HTML 안에 데이터가 없었고, 동적 크롤링을 시도했으나 보안 이슈로 페이지 구조가 탐지가 안되어 수동으로 수집 후 전처리를 거쳤다...

최종 프로젝트 1주차

1주차는 주제 고민 및 구체화만으로 후딱 지나간듯한 한 주


1주차

프로젝트 대주제
< 내부 고객 업무 효율성 향상을 위한 LLM 기반 문서 검색 시스템 >

 

최프 대주제 중 쉬워보이는게 없긴 했지만

우리 팀이 받은 대주제 역시 만만찮게 고민을 부르는 주제였다.

회사생활을 제대로 해본 것도 아니고 직장인 입장에서 필요한 시스템을 구상한다는 건 정말 어려웠다.

게다가 문서 검색(RAG)이 핵심이 되어야 한다는게 가장 중요한 부분이었다.

 

 

1. 브레인스토밍

일단 어떻게 접근을 해야할지에 대해 강사님의 조언도 구해보고,

기업 관련 유튜브도 찾아보면서 우리 팀은 다음과 같은 방식으로 기획을 위한 틀을 잡았다.

회사라는 곳은 어떤 공간인가?
- 모든 회사는 쌓인 데이터가 많다
- 주로 문서: 한글, pdf, word, ppt
- 보고서, 품의서, 결재확인, 영수증
- 어떤 하나의 의사결정을 위해 그 과정에서 생산되고 반려되는 데이터가 아주 많다
- 그것들이 실제로 활용되고 있을까?
- 대부분은 그냥 쌓인 데이터 그 자체로만 남아있다

⇒ 이걸 어떻게 써야하는가?

 

제일 먼저 든 생각은 문서 작업이었다.

도메인을 떠나 모든 회사는 문서가 쌓이고, 비효율적인 업무가 남발하며, 방치되는 문서가 무수히 많다.

 

두번째는 IT 기업이었다.

아무래도 내 전공 분야고, 가장 이것저것 주워들은게 많은 분야니까.

 

그 다음은 내 취미에서 비롯되어 영화여행 분야였다.

다만 영상 업계가 도메인이 되면 우리 시스템도 멀티모달로 가야할 텐데 그러면 프로젝트가 비대해질 것 같고,

여행 분야는 지역적인 데이터를 요구함과 동시에 지나치게 동적인 업계 특성상 RAG의 영역을 살리기 힘들 것 같았다.

그래도 일단 도메인이 흥미롭고, 주제가 재미있을 것 같다는 팀원들의 의견이 모여 기각되지는 않았다.

 

주제 고민과 함께 가장 난관이었던 부분은 어떤 도메인이든 회사 내부 데이터를 도무지 구할 수가 없었다는 것.

그러다보니 필연적으로 모든 데이터를 공개하는 공공기관으로 눈을 돌렸고, 이쪽에서 나쁘지 않은 구상이 나온 것 같다.

 

Miro에서 온갖 방법으로 마인드맵을 그려가며 장렬한 사투를 벌였고

 

 

2. 주제 후보 선정

그 결과 우리 팀에서 구상된 주제 후보는 다음과 같았다.

 

1. IT 기업 커뮤니케이션 지원 및 문서 작성 자동화

2. 공공기관 법령 해석 및 문서 작성 지원을 통한 프로세스 간소화

3. 여행사 투어 상품 기획 도우미

4. 과거 데이터 분석 및 아카이브화를 통한 프로젝트 리뷰 시스템

5. 디자인회사 시각 자료 샘플 제시 및 브랜드 가이드 체크 시스템

6. 콜센터 상담원 업무 지원 및 피드백/교육 시스템

 

그리고 멘토님으로부터 다음과 같은 피드백을 받았다.

 

1번 - RAG의 영역이 너무 적어보인다. 그것 말고는 괜찮은데 기획 볼륨이 아쉽다.

2번 - 나쁘지 않은데 특별히 차별화된 서비스는 아닌 것 같다. 일단 킵하고 좀 더 생각해보기.

3번 - 쇼핑, 매장, 식당 등 특정 지역에 대한 로컬 데이터가 굉장히 많이 필요할 텐데 감당할 수 있을지?

4번 - 어떤 데이터를 써야할지 모호하다. 도메인을 확정해야 할듯.

5번 - 대주제에 맞는 괜찮은 기획인데... 이미지/영상 처리 쉽지 않다.

6번 - 데이터가 많고 구체화만 잘 되면 괜찮겠다. 다만 어떤 문서를 어떻게 가져올건지 고민 필요.

 

 

3. 주제 확정

일단은 6번으로 확정되었고, 우리의 최종 주제는

콜센터 상담원 업무 지원 및 피드백/교육 시스템이 되었다.

 

이후는 WBS와 요구사항정의서를 작성하면서 주제 구체화에 많은 시간을 들였다.

도메인 선정... 시장조사... 수집 가능한 데이터 탐색... 제공할 기능 등등

 

콜센터 전화기가 쉬지 않는 도메인 후보로는

보험, 공공기관, 항공사, 카드사, 통신사, 의료/헬스케어 이렇게 6개를 꼽았다.

 

 

4. 도메인 선정

우선 보험의료 쪽은 전문성의 벽이 너무 높고,

만약에라도 잘못된 정보를 제공했을 때의 법적 책임도가 타 업계에 비해 너무 무겁다고 생각해 기각되었다.

 

통신사는 이미 3사가 각각 자체적인 AICC가 너무 잘 되어있어 차별성을 찾기가 어려웠다. 한마디로 도메인 자체가 입구가 높다.

 

공공기관은 민원의 범위가 너무 넓어 이 안에서 또 도메인을 정해야하고, 지역별로 세세하게 다른 조례나 법령을 명확하게 해석할 수 있어야 해 어려움이 따른다.

 

항공사는 팀원들 모두가 재밌어보인다고 호응했던 도메인이지만, 공항 특성상 과거 기반 정적 데이터보다는 현재 시점의 동적 데이터(날씨, 비행기 결항, 티켓 현황, 현지 사정 등)가 주를 이룰 것이고 마땅히 참고할만한 데이터가 없어 아쉽게 보내줘야 했다.

 

결과적으로는 카드사로 낙점되었는데, 다른 걸 소거하고 남아서 어영부영 정해진 건 아니다.

당연히 카드사로 선택한 이유는 있다.

 

우리가 판단한 카드사의 가장 큰 장점은 우리 생활에 밀접하여 익숙하고,

카드 관련으로 문의해본 경험은 많은 사람들이 있기에 공감이 가능하며, 

혜택, 이벤트, 지원 정책 등 데이터가 많다.

 

물론 혜택 같은건 수시로 바뀌기에 지속적인 갱신이 필요하고,

이쪽도 이미 실무에 적용 중인 AI 챗봇들이 있는 등 우리가 경계해야 할 부분이 꽤 있다.

기간 : 11/3 ~ 11/7


Vector DB + LangChain 실습

이번주에 배운 벡터 DB와 랭체인은 글로 정리하기보다 그냥 직접 활용해보는게 좋을 것 같아서 간단한 토이 프로젝트를 진행해봤다.

 

주제 선정

1. 벡터 DB는 유사도 검색에 특화되어 있다.
  → 추천 or 유사 컨텐츠 생성
2. 언어 관련 컨텐츠를 다루자
  → 외국어 학습?
3. 유사도 추천을 외국어에 어떻게 적용하지
4. 여기에 랭체인은 어떤 역할을 해야 하는가?

 

그래서 정해졌다


Zawa Zawa

검색어 기반 일본어 문장 생성기

https://github.com/WindyAle/ZawaZawa

 

GitHub - WindyAle/ZawaZawa: 일본어 학습자를 위한 예문 생성기

일본어 학습자를 위한 예문 생성기. Contribute to WindyAle/ZawaZawa development by creating an account on GitHub.

github.com

Zawa Zawa(ざわざわ) : 말소리를 표현하는 일본어 의성어 ex) 웅성웅성, 속닥속닥

 

1. 사용자가 공부하고자 하는 특정 주제의 키워드 or 문장을 입력

2. 검색어 기반 유사한 일본어 예문을 Chroma DB에서 검색

3. 생성된 예문의 유사 표현, 뉘앙스 설명 등을 LangChain을 활용한 생성으로 첨부

 

https://tatoeba.org/ko/downloads

타토에바 : 데이터를 다루는 사람들을 위한 일본어 텍스트집 제공 사이트

 

# Chroma가 내부적으로 사용할 임베딩 함수(모델) 정의
embed_func = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name=MODEL_NAME
)

# 영구 클라이언트 생성
client = chromadb.PersistentClient(path=DB_PATH)

# 컬렉션(테이블) 생성
# embedding_function을 지정하면 add할 때 자동으로 문장을 벡터로 변환해줌
collection = client.get_or_create_collection(
    name=COLLECTION_NAME,
    embedding_function=embed_func
)

chromadb.utils의 embedding_functions를 통해 Chroma에서 사용할 임베딩 모델을 정의할 수 있다.

콜렉션 생성 시 모델과 함께 임베딩 모델을 파라미터로 넣어주면 DB에 add하면서 자동으로 벡터화를 진행한다.

 

    # documents에 원본 텍스트를 추가
    # Chroma가 내부적으로 embed_func를 호출해 벡터로 변환 후 저장
    collection.add(
        documents=batch_sentences,
        ids=batch_ids
    )

이 코드를 통해 documents에 벡터화를 거친 sentences들이 저장된다.

 

UI는 Streamlit으로 구현

개인 실습용이므로 아주 단순하게 진행하되 실제로 누군가가 사용한다는 느낌으로 구성

 

gTTS를 적용해 음성으로 들을 수 있는 기능을 추가했다.

 

문제 상황

단일 키워드보단 어느 정도 길이가 되는 문장을 검색해야 유사성 높은 결과를 뽑아준다.

 

데이터 안에 인삿말이나 감탄사가 꽤 많이 들어가있는데

이런 범용성 높은 표현들이 대체로 상위 유사도를 차지하기 때문에

자꾸 검색어 매칭 시 튀어나오는 것으로 추정된다.

 

1차적으로 5글자 이하의 짧은 문장을 제거해봤지만

이 부분에 대한 정확한 처리방법은 좀 더 고민해봐야겠다.


DB 선택의 이유

널리 쓰이는 FAISS, Chroma, Pinecone 중에 Chroma로 선정했다.

 

FAISS

  • DB에 벡터 인덱스만 저장되므로, 문장과 매칭할 때 원본 텍스트 파일을 요구한다.
  • DB에서 ID를 찾고 이 ID로 다시 CSV를 조회해야 하는 이중 파이프라인
  • IndexFlatL2 사용 시 완전 탐색을 하므로 검색 속도가 느리다.

Chroma

  • 텍스트와 벡터를 함께 저장
  • DB 생성 과정이 느리지만, 서비스 단계에서 탐색이 빠름
  • DB의 용량이 커지지만 로컬에서 진행하는 소규모 프로젝트에 적합
  • LangChain과 간단하게 연계 가능
  • 모든 문장을 비교하는게 아닌, '적당히 가까운 문장'에 대한 지도를 생성하는 근사 이웃 탐색(ANN)

Pinecone

  • Chroma와 유사하지만 API 키가 필요하고 클라우드 형태로 제공
  • 대규모 상용 목적에 적합하므로 토이 프로젝트에서는 너무 크다고 판단

LLM을 활용한 게임 제작

https://github.com/WindyAle/Welcome-to-my

 

GitHub - WindyAle/Welcome-to-my

Contribute to WindyAle/Welcome-to-my development by creating an account on GitHub.

github.com

주중에 과제로 주어진 개인 프로젝트

약 일주일의 기간 동안, 인생 첫 게임 제작을 경험했다.

 

LLM에게 단순 텍스트를 주는 것 외에 뭔가 재미는걸 시켜볼 수 없을까 하다가

'공간'을 묘사하는 LLM이라는 아이디어를 떠올렸다.

 

1. 플레이어는 인테리어 전문가다.

2. 고객이 자신이 원하는 디자인을 최대한 모호하게 말한다.

3. 플레이어가 가구를 배치한다.

4. 배치된 가구를 판단하여 고객이 평점을 준다.

 

어릴 때 재밌게 하던 심즈 시리즈에서 영감을 받아,

플레이어가 배치한 가구의 형태, 위치와 같은 공간 정보를 LLM이 판단하도록 하는 게임이다.

게임 제작이 생각보다 정말 어려웠지만 또 내가 의도한대로 화면이 만들어지니 꽤 재밌는 경험이었다.


게임에 대한 정보는 GitHub에 있으니 여기에는 프로젝트에서 사용한 프롬프팅 기법을 기록해둔다.

 

1. 백파이어(Backfire)

"고양이를 상상하지 마세요"라고 한다면, 이 말을 들은 사람들의 머리 속에는 이미 고양이가 있을 것이다.

 

AI 용어는 아니지만 나는 이게 LLM의 동작 원리와 부합한다고 생각했다.

LLM은 제공된 정보를 어떻게든 응답에 참조한다.

 

    system_prompt = (
        f"당신은 고객 '{selected_persona['name']}'입니다. 당신의 상세 정보는 다음과 같습니다:\n"
        f"- 취향: {selected_persona['taste']}\n"
        f"- 성향: {selected_persona['tendency']}\n"
        f"- 말투: {selected_persona['tone']}\n\n"
        "당신은 지금부터 디자이너에게 방을 의뢰할 것입니다. 다음 3단계에 따라 행동하세요.\n\n"
        
        "1. [내면의 생각] : 당신의 취향과 성향에 따라, 다음 가구 목록에서 '반드시 있었으면 하는' 가구 **3~5개**를 **마음 속으로** 정합니다. 이것은 당신의 '비밀 요구사항(Wishlist)'입니다.\n"
        f"<가구 목록>: {FURNITURE_NAMES_LIST}\n\n"
        
        "2. [디자이너에게 할 말] : 1번에서 고른 가구의 **이름을 절대 직접 말하지 마세요.** 대신, 그 가구들이 왜 필요한지 '목적'이나 '행위'를 암시하는 방식으로 **매우 모호하게** 1-2 문장으로 묘사합니다.\n"
        "   (예: '소파' -> '편안히 기댈 곳이 필요해요.')\n"
        "   (예: '스토브', '냉장고' -> '집에서 요리하는 것을 좋아합니다.')\n"
        "   (예: '컴퓨터', '책장' -> '밤에 조용히 작업할 공간이 필요해요.')\n\n"
        
        "3. [출력 형식] : 다른 말은 절대 하지 말고, 오직 다음 형식으로만 응답하세요:\n"
        "[WISHLIST]\n"
        "(여기에 1번에서 고른 가구 이름들을 쉼표로 구분하여 작성)\n"
        "[REQUEST]\n"
        "(여기에 2번에서 생성한 모호한 의뢰서 내용을 작성)"
    )

'고객이 원하는 가구'를 직접 말하면 안되므로, 내부적으로는 정답을 정해놓고 그것을 빙빙 돌려서 말하도록 유도했다.

 

2. LLM as a Judge

처음에는 평점 계산 메커니즘으로 코사인 유사도를 사용했는데, 이상하게 모든 경우에 4~5점의 고득점이 나왔다.

 

알고보니 소파와 침대 같은 어휘들이 높은 유사도를 가지기 때문에,

만약 고객이 소파를 원했는데 소파를 놓지 않고 침대를 놓은 상황에도 평점이 높아진 것이다.

소파가 없으면 확실하게 소파가 없는 것에 대한 패널티를 부여해야 하는데 말이다.

 

    system_prompt = (
        f"당신은 고객 '{persona['name']}'입니다. "
        f"당신의 성격과 말투는 다음과 같습니다: {persona['tendency']}\n"
        "당신은 방금 디자이너의 작업에 점수를 매겼습니다. "
        "당신의 성격과 말투에 100% 몰입하여, '왜' 그 점수를 주었는지 1-2문장의 구체적인 피드백을 한글로 작성하세요. "
        "점수가 높으면 당신의 방식대로 칭찬하고, 낮으면 당신의 방식대로 비판하세요."
    )

LLM을 평가하는 LLM이라는 개념의 LLM as a Judge라는게 있는 것을 알고 적용해봤다.

이렇게 하는게 맞는지는 잘 모르겠지만, 코사인 유사도를 사용하던 때보다 좀 더 다양하고 풍부한 피드백이 나온 것은 사실이다.

 

 

EEVE라는 한국어 특화 모델을 사용했는데,

평소 사용하는 GPT 같은 고급 모델에 비해 현저히 떨어지는 성능을 프롬프팅하는게 상당히 어려웠다.

하지만 그렇기에 재밌었고 유효한 공부가 되었다.

기간 : 10/27 ~ 10/31

Transformer와 LLM 시작. 진짜 자연어 처리를 배우기 시작했다.


이번 주에 배운 내용은 크게 3가지다.

1. Attention을 활용한 Transformer 아키텍처

  • RNN 및 Seq2Seq 기반 구조의 문제점(장기 의존성, 정보 손실 등)을 해결하기 위해 각 단어의 관계성에 주목하는 기법
  • 문장 내 단어의 위치와 의미적 유사성을 병렬적으로 N번 학습하여(Multi-Head) 문맥을 파악
  • Seq2Seq 구조를 탈피하고 Attention만을 활용한 Transformer 구조

2. Hugging Face에서 모델 가져오기

  • GitHub와 비슷하게 개발자들이 학습된 모델을 레포지토리에 올리고 공유하는 곳
  • 필요한 Task에 따라 적절한 모델을 찾아 로드

3. OpenAI와 Ollama를 활용한 LLM 프롬프팅


🔨Transformer 구현

https://github.com/WindyAle/Transformer

 

GitHub - WindyAle/Transformer: 공부하면서 구현해본 Transformer 구조

공부하면서 구현해본 Transformer 구조. Contribute to WindyAle/Transformer development by creating an account on GitHub.

github.com

 

글로 보면 언뜻 알것 같으면서도 내가 이해한 내용이 코드로 연결이 잘 안되는 것 같아서

그냥 직접 구현해보면서 부딪혀봤다.

Transformer/
├── main.ipynb            # main 파일 (테스트용)
└── modules/
    ├── MyTransformer.py  # 인코더-디코더
    └── common.py         # 여러 번 재사용되는 구조들
                          ## attention, Embedding, MultiHeadAttention, FeedForward

 

수업에서 실습으로 진행한 코드를 기반으로 함수끼리 좀 더 직관적으로 서로 연결되도록 노력해봤는데 잘 됐는지는 모르겠다.

  • common
    핵심 메커니즘인 attention 함수 그리고 임베딩, 멀티헤드, 피드포워드는 트랜스포머 내부에서 여러 번 재사용되고 여기저기서 호출되는 도구들
  • MyTransformer
    common의 도구들을 사용해 인코더와 디코더 그리고 그 둘을 연결하는 메인 클래스
  • (최상위) main
    import를 테스트하는 파일. 초기값 설정하고 입력값만 넣으면 끝이다.

 

class Config:
    def __init__(self, vocab_size, pad_token_id=0):
        self.vocab_size = vocab_size        # 단어사전 크기
        self.hidden_size = 768              # 은닉층 차원수 (attention_eads의 배수)
        self.max_position_embeddings = 768  # 최대 시퀀스 길이
        self.num_attention_heads = 12       # 어텐션 헤드 개수
        self.num_hidden_layers = 12         # 레이어 개수
        self.intermediate_size = 2048       # FeedForward 레이어의 중간 차원
        self.hidden_dropout_prob = 0.1      # dropout 비율
        self.layer_norm_eps = 1e-12         # epsilon
        self.pad_token_id = pad_token_id    # 패딩 토큰

테스트를 위한 Config 객체

대부분의 숫자는 자유롭게 주면 되지만 hidden_sizenum_attention heads의 배수여야 한다.

멀티 헤드 어텐션 시 임베딩 차원을 헤드 수만큼 나눠야 하기 때문.

 

source_ids = torch.tensor([
    [101, 2054, 2064, 2106, 102, 0, 0, 0, 0, 0, 0, 0],
    [101, 3000, 4000, 102, 2000, 102, 0, 0, 0, 0, 0, 0]
], dtype=torch.long)

target_ids = torch.tensor([
    [201, 3054, 3064, 3106, 202, 0, 0, 0, 0, 0],
    [201, 4000, 5000, 202, 6000, 202, 0, 0, 0, 0]
], dtype=torch.long)

테스트를 위해 더미로 만든 데이터

원시 텍스트에서 벡터화까지 진행되었다 가정하고 아무 숫자를 넣었다.

 

import torch
from modules.MyTransformer import Transformer

model = Transformer(config_enc, config_dec)
model.eval() # 평가 모드

# ... (생략)

logits = model(source_ids, target_ids)
print(f"Output Logits shape: {logits.shape}")

# 예상 출력 (VOCAB_SIZE == 15000)
# [2, 10, 15000]

다행히도 예상과 동일한 결과가 나온다.

 

batch_size(2)개의 문장이 만들어졌고

각 문장은 seq_len(10)개의 단어로 이루어져 있으며

단어사전에 있는 vocab_size(15000)개의 단어들이 각 위치에 올 수 있는 확률이다.

  • [2, 10, 768]로 계산된 아웃풋이
  • 디코더에서 Linear(hidden_size, vocab_size)를 거쳐
  • 단어사전에 매핑된 각 단어들에 대한 확률분포로 최종값이 나온다.

💥Trouble Shooting

🚨 RuntimeError: view size is not compatible with input tensor's size and stride

상황

 - transpose를 적용한 텐서에 view를 사용할 때

원인

expand, narrow, transpose 등 배열의 형태를 바꾸는 메서드를 적용하면 메모리의 stride가 바뀐다.

배열의 저장 형태가 non-unit stride일 경우 view가 읽어오지 못한다.

해결

context = context.transpose(1, 2).contiguous()
context = context.view(batch_size, -1, self.embed_dim)

contiguous()를 추가로 적용

 

Q. 동일한 동작을 하는 reshape와의 차이점

  • view는 얕은 복사. 원본 배열을 특정한 모양으로 조회하는 메커니즘이다. unit stride를 만족해야 한다.
  • reshape는 기본적으로는 view와 동일, view를 못하는 상황에는 그냥 새로운 배열을 만드는 스마트한 버전.

Q. 근본적인 의문으로, view는 왜 stride를 체크하는가?

→ view에 dtype 파라미터가 있는게 힌트였다. 데이터타입 레벨에서 값을 읽어오려면 원본 배열을 바이트 단위로 읽어야 하기 때문

 

 

🚨 AttributeError: 'Embedding' object has no attribute 'self.position_ids'

상황

- 포지션 임베딩 중 Embedding 클래스가 호출될 때

원인

Embedding의 클래스 속성으로 self.position_ids을 정의했는데 이것을 forward 메서드가 참조하지 못한다.

파라미터에 device 속성을 적용한게 원인이었다.

다른 파라미터들은 GPU로 이동했는데 텐서만 여전히 CPU에 남아있어서 서로 connected 되지 못했다.

→ torch.tensor()는 model.to()가 적용되지 않는다.

해결

self.register_buffer("position_ids", torch.arange(...))

torch.tensor는 GPU로 이동하기 위해 별도의 명시가 필요하다.

register_buffer()라는 메서드를 새로 알았다.

 

Q. position_ids를 forward에서 선언하면 되지 않을까?

→ 이 경우 메서드가 호출될 때마다 새로운 position_ids가 매번 생성되고, 불필요한 오버헤드가 반복되어 별로다.

 

Q. 왜 텐서만 특수하게 취급할까?

모델의 속성이지만 학습 대상은 아닌 파라미터를 다루기 위한 PyTorch의 규칙이라고 한다. 단순 계산 or 임시저장용 텐서는 모델 state에 저장될 필요가 없으므로 이런 경우를 구분하기 위함인듯.

기간 : 10/20 ~ 10/24


📕9주차 리뷰

자연어 처리가 평상시 사용하는 언어를 다루는 영역이다보니 활용도도 높아보이고 배우면서도 재밌다. 그리고 그와 별개로 상당히 헷갈리고 어렵다. 모델에 대한 이해도를 확실히 잡아놔야겠다. 원리적인 부분에 집착하기보단 우선 이 모델이 뭐를 받아서 뭐를 뱉고, 어떤 특징을 가지고 있는지를 체크하고 넘어가려 한다.

 

해커톤 사전과제 제출이 끝났다. 팀원들과 여러 테스트를 해보고 다같이 보는 곳에서 제출했지만 솔직히 합격할지는 모르겠다. 요구사항이 너무 모호했고, 동시다발적으로 갱신되어야 하는 내부값이 많았는데 우리는 각 값을 따로따로 확인하는 식으로 테스트를 해서 어느 부분에 빼먹은게 있을 수도 있겠다. 그래도 과정에서 많이 배우고 많은 경험을 했기에 상당히 만족스럽다.


  • 순차 데이터(Sequential Data): 텍스트, 시계열, 음성처럼 데이터의 순서가 의미를 갖는 데이터
  • 시퀀스 모델링: 순차 데이터의 패턴을 학습하는 방법론

순환 신경망(RNN)

순환 구조를 가지는 모델

각 시점(time step)에서 이전 시점의 정보(은닉 상태 Ht-1)를 현재 시점의 입력(Xt)과 함께 받아 현재의 은닉 상태(Ht) 생성

  • 이 은닉 상태를 통해 과거의 정보를 기억한다.

  • 각 time step이 하나의 layer 역할을 하므로, 역전파를 Through Time (시간을 거슬러) 방식으로 진행
  • 입력 데이터를 요약하고, 새로운 입력이 들어올 때마다 기억을 수정
  • 모든 입력을 처리한 후 남은 기억은 시퀀스를 전체적으로 요약하는 정보로 활용
  • 입력마다 기억을 갱신하며 순환적으로 진행 → 영단어 외우듯 반복 암기

장기 의존성 문제 (Long-Term Dependency)

시퀀스가 길어질수록 맨 처음의 정보가 뒤쪽까지 전달되기 어려워(기울기 소실) 장기적인 패턴을 학습하기 어려움

 

입출력 형식

One-to-One One-to-Many Many-to-One Many-to-Many
(Sequence Labeling)
Many-to-Many
(Encoder to Decoder)
하나를 받아 하나 출력 하나를 받아 시퀀스 출력 시퀀스를 받아 하나 출력 각 요소마다 라벨 매핑
(입출력 길이 동일)
입력과 출력이 유동적
이미지 분류 이미지 캡셔닝, AI 노래 감정 분석, 스팸 탐지 품사 태깅, 객체 인식 번역, 챗봇, AICC

LSTM(Long Short-Term Memory)

RNN의 장기 의존성 문제를 해결하기 위해 고안된 모델

 

셀 상태(Cell State, Ct)

장기 기억을 담당하는 별도의 정보 저장소

정보가 큰 손실 없이 전달될 수 있도록 함

 

게이트(Gates): 셀 상태의 정보를 제어하는 장치

  • 망각 게이트(Forget Gate, Ft): 과거 정보(이전 셀 상태 Ct-1)를 얼마나 잊을지 결정
  • 입력 게이트(Input Gate, It): 현재 입력으로 들어온 새로운 정보를 얼마나 저장할지 결정
  • 출력 게이트(Output Gate, Ot): 현재 셀 상태의 정보를 얼마나 출력(다음 은닉 상태 Ht로)할지 결정

양방향 LSTM (Bidirectional LSTM)

데이터를 순방향과 역방향으로 동시에 처리하여 문장의 양쪽 문맥을 모두 학습하는 기법


GRU(Gated Recurrent Unit)

LSTM의 구조를 더 단순화한 모델

셀 상태와 은닉 상태를 하나의 은닉 상태로 통합하고 게이트를 리셋과 업데이트 2개만 사용

  • 리셋 게이트 (Reset Gate): 과거의 은닉 상태를 얼마나 무시할지(잊을지) 결정
  • 업데이트 게이트 (Update Gate): 과거의 은닉 상태와 현재 계산된 정보를 얼마나 조합하여 다음 은닉 상태($h_t$)를 만들지 결정

언어 모델링(Language Modeling, LM)

단어 시퀀스(문장)에 확률을 할당하는 모델

주어진 단어들 다음에 어떤 단어가 올 확률을 예측

  • 통계 기반(n-gram): 연속된 n개의 단어 묶음을 기반으로 확률을 계산. 문맥이 n개로 제한되고 데이터 희소성 문제
  • 신경망 기반(NNLM, RNN 등): 단어를 임베딩 벡터로 변환하고 신경망을 이용해 문맥을 학습

퍼플렉서티(Perplexity, PP)

언어 모델이 얼마나 '헷갈려하는지'를 나타내는 지표

값이 낮을수록 성능이 좋음


신경망 기계 번역(NMT)

seq2seq (Sequence to Sequence)

하나의 시퀀스를 다른 시퀀스로 변환하는 구조
→ 기계 번역, 텍스트 요약, 챗봇 등

  • 인코더(Encoder): 문장 전체의 의미를 하나의 고정된 크기의 Context Vector로 압축
  • 디코더 (Decoder): 인코더가 전달한 컨텍스트 벡터를 입력받아 한 단어씩 순차적으로 생성하여 출력
  • 압축 과정에서 정보 손실을 무시할 수 없는 문제점이 있음
    → Attention 기법과 Transformer의 등장으로 이어짐

Teacher Forcing

디코더가 다음 단어를 예측할 때 모델 자신이 이전에 예측한 단어 대신 실제 정답 단어를 입력으로 넣어주는 기법

 

"I am a" 다음에 "student"를 예측해야 할 때,

  • 모델이 실수로 "I am a teacher"라고 예측했더라도
  • 다음 단어를 예측하기 위한 입력값으로 정답인 student를 넣어줌

학습 초기 불안정성을 줄이고 수렴 속도를 향상시켜주지만 노출 편향(Exposure Bias) 문제가 있음

  • 학습 시에는 틀린 답을 내도 정답으로 커버해주지만
  • 정답이 없는 실제 추론 시에는 틀린 답을 내도 그대로 안고 가야하니
  • 작은 오류가 연쇄적으로 커질 수 있음

온실 속 화초처럼 곱게 자란 애라서 자기 실수로부터 회복할 방법을 배우지 못함

 

💡Scheduled Sampling

노출 편향을 해결하기 위한 기법

  • 처음에는 100% 정답 단어를 입력해주다가
  • 실제 예측값과 정답값을 번갈아가며 입력
  • 번갈아가는 비율은 하이퍼 파라미터 튜닝으로 최적점을 찾아야 함

기간: 10/13 ~ 10/17

2차 프로젝트 + 자연어 처리 파트 시작 + 해커톤 준비 = 🐕고생


📕8주차 리뷰

2차 프로젝트가 정말 힘들었다. 극단적인 불균형 데이터를 다루는 데에 있어서 많은 경험이 필요할듯하다..

그래도 덕분에 많은걸 찾아보고 적용해보면서 한편으론 재밌는 경험이었다.

 

이번주부터 NLP 과정에 들어갔다. 대학에선 모델을 가져다쓰는 정도의 경험뿐이었어서, 이론과 함께 원리를 하나하나 짚어보는게 생각보다 낯설고 머리가 따라주지 않는게 느껴진다. 그래도 재밌으니 그걸로 된 거 아닐까?

 

SK와 Anthropic에서 주관하는 해커톤에 참가하기로 했다. 마음이 맞는 동기 분들과 4인 팀을 맺고 호기롭게 도전해본다.

원래 처음은 경험에 의의를 두고 맛만 한 번 보자... 라는 마인드지만, 공개된 주제를 살펴보니 많이 매운 맛이다.

 

그리고 지난 추석 연휴에 역전파를 좀 더 상세하게 공부했다. 제대로 이해했는지는 나도 모르겠다...


2차 프로젝트 회고

주제: 넷플릭스 유저 특성에 따른 구독 이탈 예측

보자마자 한숨부터 나온 극단적인 데이터였다.

주어진 데이터셋에서 이탈 고객이 90%를 차지하는, 매우 치우쳐진 형태였다.

 

시청시간, 구독플랜, 월 소득, 상호작용 빈도 등등 좋은 특성들이 많았지만 문제는 이탈 고객과 잔류 고객 사이의 패턴 차이가 없었고,

모델이 이탈 고객에 대해 오버피팅이 된 것인지 예측 과정에서 일단 이탈(1)로 찍는듯한 모양이 보였다.

 

양쪽 클래스에 대해 recall은 비슷하게 높지만 precision이 굉장히 낮고

비율이 아닌 절대 수치로 보면 모델이 그냥 대부분의 상황에서 이탈이라고 보고 있다.

학습한 데이터가 패턴이 모두 일정하고, 그 중 대부분이 이탈이었으니 어쩔 수 없다.

 

https://github.com/SKNetworks-AI19-250818/SKN19_2ND_3TEAM?

 

GitHub - SKNetworks-AI19-250818/SKN19_2ND_3TEAM: 넷플릭스 유저 특성에 따른 구독 이탈 예측

넷플릭스 유저 특성에 따른 구독 이탈 예측. Contribute to SKNetworks-AI19-250818/SKN19_2ND_3TEAM development by creating an account on GitHub.

github.com

오버샘플링(SMOTE), 가중치 선정, BalancedRandomForest 등 불균형 데이터를 극복하기 위해 여러 시도를 해보면서 새로운 경험이 되기도 했고, 실제 세상의 데이터가 항상 깔끔하진 않기 때문에 나름 괜찮은 경험이었던 것 같다.


오차역전파법(Backpropagation)

전반적인 흐름은 알겠지만 디테일한 부분은 명확하게 와닿지 않아 지난 추석연휴 동안 좀 더 파봤다.

우선 크게 두 가지 질문을 두고, 이를 해결하려는 방향으로 공부했다.

  • 역전파 과정에서 내적과 외적 연산이 필요한 이유
  • 왜 가중치 갱신 시 항상 빼주기만 하는가 더하면 안되나?

1. 역전파를 하는 이유

학습하면서 여러 갈래로 퍼진 오차를, 거슬러 역추적하면서 다시 하나의 뉴런으로 요약

  • 어떤 은닉층에 있는 뉴런 하나의 오차(책임)를 계산해야 하는 상황
  • 이 뉴런은 다음 층에 있는 뉴런들에게 신호를 보냈고, 최종 오차에 기여함
  • 이 뉴런이 짊어져야 할 책임 오차는, 자신이 보냈던 다음 층 뉴런들의 오차를 취합하여 계산
    → 가장 큰 오차를 퍼뜨린 범인을 검거하고, "가중치를 이렇게 고쳐라" 하고 재할당해주는 것

2. 목표

각 층을 거슬러 올라가며, 각 층에 있는 뉴런들의 오차 기여도를 수색

= 특정 층(L)에 있는, 특정 뉴런 j의 오차 ∂j를 계산해야 함.

3. 상황

  • 뉴런 j는 L+1에 있는 모든 뉴런 k들과 가중치 Wkj로 연결되어 있다.
  • L+1의 뉴런 k들은 j에게서 전달받은 가중치로 자신의 오차 ∂k를 계산해 보관 중이다.

4. 계산

순전파 과정 중 뉴런 j가 남긴 총 책임량 = 다음 뉴런이 가진 오차 x 이 뉴런이 전달받은 가중치

'뉴런 k들이 가진 오차 ∂k'에 둘을 연결하던 '가중치 Wkj만큼의 영향력'을 곱해서 합산

 

한마디로, 오차 벡터와 가중치 벡터의 유사도를 산출하는 과정이므로 여기서 내적을 사용한다.

5. 가중치 갱신

입력값과 출력 오차의 상호작용 맵을 만들어 모든 엣지(edge)에 책임을 묻는 과정

이렇게 해야 하는 이유는, 가중치를 준 쪽이 일방적으로 책임을 쓰지 않게 하기 위함

가중치를 받은 뉴런이 범인일 수도 있기 때문

  • 가중치 Wkj는, 이전 층의 뉴런 j에서 현재 층의 뉴런 k로 향하는 엣지
  • 이 엣지가 최종 오차에 얼마나 큰 책임이 있는지를 판단해야 함
    • 값을 보낸 뉴런 j가 얼마나 강한 activation을 가지고 있는가?
      → activation 자체가 약했다면 가중치가 높아도 오차가 작았을 것이기 때문
    • 신호를 받은 뉴런 k가 최종 오차에 대해 얼마나 책임이 있는가?
      → 받은 쪽에서 계산된 오차가 작다면 이 엣지의 책임은 작기 때문

5-1. 새 가중치 계산

가중치 Wkj의 책임(그래디언트) = Wkj의 변화율에 따른 최종 오차의 변화율(Chain Rule)

→ Wkj가 변하면 k의 활성화 함수 입력값(Zk)이 변하고, 이에 따라L이 변한다.

  • Wkj는 이전 층의 뉴런 j와 현재 층의 뉴런 k를 연결한다
  • 이 가중치가 최종 손실 L에 영향을 미치는 경로는 단 하나

현재 층의 오차 벡터 ∂k와 이전 층의 활성화 벡터 ∂j외적과 정확히 일치

→ 가중치(그래디언트) 행렬 = 활성화 벡터를 오차 벡터의 방향으로 보내버리는 선형변환

→ 오차의 관점으로 다시 바라보기

  • 입력 신호(활성화 벡터)가 최종 오차에 어떤 식으로 기여했는지를 밝히는 것
  • 외적 행렬 = 활성화 벡터에서 오차와 상관도가 높은 원소들만 남은 결과물

 

역전파 과정에서 내적과 외적 연산이 필요한 이유

  • 내적: 임의의 뉴런이 뽑아낸 오차 벡터와 그 뉴런이 받은 가중치 벡터 간의 유사도
  • 외적: 임의의 뉴런이 뽑아낸 오차 벡터와 그 뉴런이 받은 활성화 벡터의 상호 영향력

왜 가중치 갱신 시 항상 빼주기만 하는가?

  • 오차와 상관도가 높은 원소들을 제하기 위해

+ Recent posts