728x90
반응형

23년 8월 어느 날, KT AI 해커톤공고가 올라왔다.
개발에 목말라있던 나에겐 당연히 좋은 기회라고 생각했다.

하지만 우려되었던 부분도 있었다.
PM으로 리딩하고 있던 프로젝트가 10월까지 계획되어 있어서 업무부담이 가중되지 않을까란 우려와
Java 개발에 익숙한 내가 Python과 AI기술을 활용하여 개발을 할 수 있을까란 우려가 있었다.

그렇지만 10월까지는 야근과 주말근무도 감수하면 될꺼고... AI에도 관심이 있던 상태였다. 시원하게 저질렀다. AI 해커톤 참여하는 것으로...!

예선

AI 해커톤 지원 후, 얼마 지나지 않아 예선 과제와 일정이 주어졌다.
예선 과제는 KT고객의 고장문의와 고객단말 시험결과를 기반으로 하여 현장기사 출동/무출동을 판단하는 문제였다.
그리고 출동/무출동을 판단하여 정확도/정밀도 순으로 상위 20팀을 선발되어 본선에 참여할 수 있는 기회를 얻게된다.

그리고 기한도 함께 나왔다.
하필 내 해외여행 일정이 겹쳤다;;; 사전에 잡은 일정이라 취소도 어려웠다.

팀에 최대한 누가 되지 않도록 나는 일정을 앞당겨 먼저 예선에 몰두했다.
먼저 정확도/정밀도가 뭔지 개념을 파악하고, 기본적인 머신러닝 이진분류 알고리즘의 원리를 파악했다. (👉이진분류 알고리즘(Logistic Classification) 글 참고)

정확도/정밀도

정확도와 정밀도의 개념은 사진을 참고하면 된다.

쉽게말하면,
정확도는 전체 데이터중 모델이 예측한 True/False와 실제 값이 일치하는 경우의 비율이다.
(ex. 모델 예측 : 1=True, 2=True, 3=False, 실제 : 1=True, 2=False, 3=False, 전체 데이터 갯수 : 3개 (1, 2, 3), 일치하는 데이터 갯수 : 2개 (1, 3), 정확도 = 2 / 3 = 66.67%)

정밀도는 True/False로 예측한 데이터 중에서 실제 True/False로 일치하는 경우의 비율이다.
아래 예시는 모델이 True로 예측한 데이터 중에서 실제 True의 비율을 나타내는 예시이다.
(ex. 모델 예측 : 1=True, 2=True, 4=True, 실제 : 1=True, 2=True, 4=False, True 예측 데이터 갯수 : 3개 (1, 2, 4), 일치하는 데이터 갯수 : 2개 (1, 2), True정밀도 = 2 / 3 = 66.67%)

이번 예선에서는 정확도/정밀도 순으로 상위 20팀을 선발한다.
정확도 뿐만 아니라 정밀도도 평가에 반영되기 때문에, 모든 데이터의 기본 값을 출동으로 하여 무출동 값을 선별하는 것이 아닌, 적절한 출동/무출동 예측을 진행해야 한다.

출동/무출동 판단

평가기준이 정확도 70%, 정밀도 30%로 순위를 나열해서 본선진출팀을 선발되는 방식이기 때문에 어느정도 전략을 잘 짜야했다.
다행히 같이 참여하시는 과장님께서는 전년도에도 AI해커톤을 참여하신 경력이 있으시기에 전략짜기 한결 수월했다.

일단 과제해결 방식은 이렇다.
python코드를 활용하여 출동/무출동을 판단하는 AI모델을 구현한 후에 출동/무출동 라벨이 달려있는 학습용 데이터를 지도학습시키고,
라벨이 없는 테스트용 데이터를 출동/무출동 판단하여 데이터에 출동/무출동을 매핑한 후에 시스템에 제출하면 된다.
그러면 시스템은 정확도와 정밀도를 계산하여 자동으로 팀별 순위를 나열해주는 방식이다.

테스트용 데이터의 출동/무출동 비율을 알아보기 위해 우리는 모든 테스트용 데이터에 출동 라벨을 붙여 시스템에 제출하였다.
출동/무출동 둘중 어떤 데이터를 디폴트로 하여 어떤 데이터를 찾을지 결정하기 위함이다.
테스트 결과, 대략 출동 데이터 75%, 무출동 데이터 25% 정도로 구성되어 있는 것으로 확인되었다.
결론적으로 테스트용 데이터에 출동데이터를 디폴트로 하여 라벨링을 한 후에 어떤 데이터가 무출동인지만 찾아서 해당하는 데이터에 무출동을 라벨링하여 제출하면 된다. 단, 위에서도 언급한 것처럼 정밀도도 평가에 반영되기 때문에 적절한 무출동 값 예측도 필요하다.

먼저 과장님이 기반코드를 공유해주셨다.

import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, precision_score, classification_report
import seaborn as sns
import numpy as np

train_base = pd.read_csv('../data/dfBase_Train.csv') # load 학습데이터
train_detail = pd.read_csv('../data/dfDetail_Train.csv')
test_base = pd.read_csv('../data/dfBase_Test.csv') # load 테스트데이터
test_detail = pd.read_csv('../data/dfDetail_Test.csv')

train_base.columns = ['고장접수번호', '현장출동구분', '신고내용'] # 컬럼명 설정
test_base.columns = ['고장접수번호', '신고내용']
train_detail.columns = ['고장접수번호', '시험종류', '시험수행일', '시험수행성공여부', '시험수행상세결과']
test_detail.columns = ['고장접수번호', '시험종류', '시험수행일', '시험수행성공여부', '시험수행상세결과']

def clacul(row):
    target_id = row.고장접수번호
    detail_df = train_detail[train_detail.고장접수번호 == target_id]
    detail_df_len = len(detail_df)
    return detail_df_len
res = train_base.apply(lambda x: calcul(x), axis=1) 
train_base['시험횟수'] = res # 문의별 단말시험횟수 값 계산 및 저장

train_detail['시험요약'] = train_detail['시험종류'] + '-' + train_detail['시험수행성공여부']
train_detail_pivot = train_detail[['고장접수번호', '시험요약', '시험수행성공여부']].pivot_table(index='고장접수번호', columns='시험요약', aggfunc='count') # 고장접수번호별 시험종류-시험수행성공여부 갯수를 기록한 피벗테이블 생성

train_detail_pivot = train_detail_pivot.fillna(0) # NaN -> 0.0

train_detail_pivot = train_detail_pivot.reset_index().droplevel(level=0, axis=1).rename(columns={'':'고장접수번호'})
train_merged = train_base.merge(train_detail_pivot, left_on='고장접수번호', right_on='고장접수번호') # pivot테이블을 기존테이블에 merge

train_merged['장애안내'] = train_merged.신고내용.map(lambda x:1 if(('장애안내' in x) | ('장애 안내' in x)) else 0) # '장애안내' 키워드 갯수 카운트 및 매핑

rf = RandomForestClassifier() # RandomForest 모델 선언
X_data = train_merged.drop(columns=['현장출동구분', '고장접수번호', '신고내용'])
y_data = train_merged['현장출동구분']

rf.fit(X_data, y_data) # 학습

위 코드를 간단히 설명하면,
고장문의마다 해당 고객의 단말시험을 진행하는데
문의별로 어떤 단말시험을 진행했는지? 결과는 어땠는지? 그리고 문의내용 중 특정 키워드가 있는지의 정보를 만들어서
RandomForest 모델에 학습시키는 코드이다.

위 코드로 모든 데이터를 출동으로 명시한 경우의 75%에서 76%까지 1%의 정확도를 올릴 수 있었다.

그리고 나는 무출동 데이터를 판별하는 핵심 키워드를 추출하기 위해 아래 코드를 추가하였다.

# 문자열 가공
def extract_word(text):
    text = text.lower()
    text = re.sub('[^a-z가-힣.%]', ' ', text)
    text = re.sub('\s+', ' ', text)
    return text
train_base['신고내용'] = train_base['신고내용'].apply(lambda x: extract_word(x))

# 형태소 분석
from konlpy.tag import Okt

okt = Okt()
words = " ".join(train_base['신고내용'].tolist())
words = okt.morphs(words, stem=True)

# 키워드 중복제거 및 count
from collections import Counter
frequent = Counter(words).most_common()

# 키워드 추출 ( 조건 : 2 * 무출동 >= 출동 )
for i in range(len(frequent)):
    check = train_base[(train_base.신고내용.str.contains(frequent[i][0]))].현장출동구분.value_counts()
    if "무출동" in check:
        ngo = check['무출동']
    else:
        ngo = 0

    if "출동" in check:
        go = check['출동']
    else:
        go = 0

    if (2 * ngo >= go):
        print(frequent[i][0])

위 코드를 간단히 설명하면,
모델에 키워드를 학습시켜 정확도를 향상시키기 위해 키워드를 추출하는 코드이다.
키워드 분석을 진행해보면,
일반적으로 특정 키워드가 신고내용에 포함되어 있는 경우 중 출동/무출동 라벨이 붙어있는 비율을 보면 대략 (출동 : 무출동 = 3 : 1) 의 비율이 형성되어 있다. 그래서 이보다 더 높은 무출동 비율의 (ex. 출동 : 무출동 = 2 : 1) 키워드를 찾아내서 학습시키면 정확도를 올릴 수 있을 것이라고 판단했고,
심사기준에 정밀도가 포함되어 있음을 고려하여 출동:무출동=2:1 비율 이상의 무출동 비율의 키워드를 추출할 수 있도록 조건을 설정하였다.

그리고 해당 코드와 핵심 키워드 몇개를 추출하여 해커톤팀에 공유하였다.
그 후에 나는 팀원들에게 뒤를 맡기고 예정된 해외여행 일정을 소화했다...

결과

팀원들이 남은기간 키워들을 잘 찾아내어 학습시켜주었기 때문에, 정확도를 77.6512까지 올릴 수 있었고
105팀중 최종 7위로 인간지능팀인 우리가 본선진출에 성공했다.

이제 본선을 대비해서 사전 개발작업을 했는데, 본선 후기는 👉다음글에서...

반응형

'개발 > AI' 카테고리의 다른 글

[AI] pandas를 활용한 데이터 핸들링 및 전처리  (2) 2024.04.10
KT AI 해커톤 회고 (2)  (1) 2023.11.02
[AI] Softmax Regression  (0) 2023.10.01
[NLP] Embedding  (0) 2023.09.23
[AI] Logistic Regression  (0) 2023.09.10
728x90
반응형

👉이전글에서 이진분류의 대표 기술인 Logistic Regression에 대해 알아보았습니다.

이번엔 출력 값이 두 개가 아닌, 다수의 출력 값을 낼 수 있는 기술인 Softmax Regression에 대해 알아보겠습니다.
이번 글은 모두를 위한 딥러닝 강의를 수강한 후 작성하는 글입니다.

Multinomial Classification

성적에 따른 S~F등급 예측 등과 같은 다중 출력 모델은 기존 Logistic Classfication기술을 응용하여 만들 수 있습니다.

위 사진에서 나타낸 것처럼 출력 데이터(A, B, C)의 가지수를 3으로 가정해봅시다.
출력 데이터 A, B, C중 하나로 분류하기 위해 3개의 Hypothesis가 필요한데요.

3개의 Hypothesis를 활용하여 도출된 수식은 아래 사진과 같이 표현할 수 있습니다.

위 사진과 같이 출력 데이터(A, B, C)의 가지수 3에 맞게 크기 3의 W 1차원 벡터를 구성하고,
입력으로 들어온 입력 값을 위 W벡터에 각각 계산해주면
그에 맞게 도출된 출력 값에 따라 입력 값을 A, B, C중 하나로 분류하고 Hypothesis를 최적화할 수 있습니다.

그리고 w벡터들을 하나의 벡터로 표현하면 위 사진과 같은 형태의 수식이 탄생합니다.

Softmax Classifier

여러 가지수 중 하나로 예측하여 분류하기 위해서는 0과 1사이의 확률 값으로 변환할 필요가 있습니다.
위 사진은 Hypothesis의 출력 값을 Softmax분류기를 통해 0과 1사이의 확률 값으로 변환하는 도식입니다.

그리고 변환된 확률 값은 위 사진에서 보이는 것처럼 One-Hot Encoding과 같은 과정을 거쳐
분류를 위한 최종 벡터 값 설정을 진행합니다.

가장 간단한 방법은 가장 큰 확률 값은 1로 설정하고 나머지는 0으로 세팅하는 One-Hot Encoding이 있습니다.

Cost Function

Cost Function은 위 도식과 같은 형태로 표현할 수 있습니다.
Softmax에서는 Cross Entropy라고도 불립니다.

수식은 Logistic Regression의 Cost Fucntion과 비슷한 원리로,
예측값과 실제값에 차이가 있을 경우, 무한대로 수렴하는 Cost가 출력되고,
에측값과 실제값에 차이가 없을 경우, 0에 수렴하는 Cost가 출력되는 원리입니다.

그리고 아래 사진은 최종 형태입니다.

Softmax Regression in Tensorflow

아래 코드는 Softmax 예제 Tensorflow 코드입니다.

x_data = [[1, 2, 1, 1], [2, 1, 3, 2], [3, 1, 3, 4], [4, 1, 5, 5], [1, 7, 5, 5], [1, 2, 5, 6], [1, 6, 6, 6], [1, 7, 7, 7]]
y_data = [[0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 1, 0], [0, 1, 0], [1, 0, 0], [1, 0, 0]]

X = tf.placeholder("float", [None, 4])
Y = tf.placeholder("float", [None, 3])
nb_classes = 3

W = tf.Variable(tf.random_normal([4, nb_classes]), name='wieght')
b = tf.Variable(tf.random_normal([nb_classes]), name='bias')

hypothesis = tf.nn.softmax(tf.matmul(X, W) + b)

cost = tf.reduce_mean(-tf.reduce_sum(Y * tf.log(hypothesis), axis=1))
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.1).minimize(cost)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())

    for step in range(2001):
        sess.run(optimizer, feed_dict=(X: x_data, Y: y_data))
        if step % 200 == 0:
            print(step, sess.run(cost, feed_dict=[X; x_data, Y: y_data]))
반응형

'개발 > AI' 카테고리의 다른 글

KT AI 해커톤 회고 (2)  (1) 2023.11.02
KT AI 해커톤 회고 (1)  (0) 2023.10.27
[NLP] Embedding  (0) 2023.09.23
[AI] Logistic Regression  (0) 2023.09.10
[AI] Regression  (0) 2023.01.22
728x90
반응형

1. 서론

지난 8월, 사내 AI 해커톤에 참여하여 본선진출까지 성공했습니다.
이에 따라 본선 과제를 받았는데요.

본선 과제는 생성형AI 모델을 활용하여 업무에 적용할 수 있는 과제를 발굴하고 개발하는 것입니다.
생성형AI 모델의 예로 ChatGPT의 GPT모델이 있죠.

LLM (Large Language Model)


GPT모델은 대표적인 LLM(Large Language Model) 입니다.
과제발굴에 앞서 LLM모델에 대한 이해가 필요했어요.

기본적으로 LM(Language Model) 은 인간의 언어(자연어)를 이해하고 생성할 수 있도록 훈련된 인공지능 모델입니다.
이러한 LM에 대량의 언어데이터를 학습하여 문장구조나 문법, 의미 등을 이해할 수 있도록 한 모델이 LLM(Large Language Model) 입니다.
또한, LLM은 딥러닝 알고리즘과 통계모델링을 활용하는 모델이기 때문에 자연어처리(Natural Language Processing, NLP) 작업을 수행하는 데에도 사용됩니다.

저는 이 자연어처리 기술에 초점을 맞추기로 했어요. 왜냐하면 LLM모델을 활용하는 대부분의 아이디어가 자연어로 입력받고 출력되는 컨셉이 기본이 되고 있거든요. 가령 챗봇이나 AI비서 등이 있겠죠.

그렇다면, 자연어처리를 수행하는 LLM은 어떻게? 우리 인간의 말(자연어)을 이해할 수 있을까요?
답은 본 글의 주제인 Embedding기술에 있습니다.

2. Embedding이란?

embedding
Embedding은 텍스트 기반의 자연어를 기계가 이해할 수 있는 실수벡터 형태로 표현하는 기술입니다.
Embedding을 통해 생성된 벡터들을 활용하면 자연어의 의미와 자연어간 문맥을 파악할 수 있습니다.

Embedding을 이용하는 이유는 또 있습니다.
자연어를 단순 인코딩한 숫자형태의 텍스트는 일반적으로 길이가 매우 길고, 가변적이어서 AI 모델이 쉽게 이해하지 못합니다.
이러한 맹점을 보완하기 위해 Embedding을 활용하여 고정된 차원의 벡터형태로 자연어를 변환하는 것입니다.

Word embedding vs Sentence/Document embedding

원본 텍스트를 기준으로 하면 Embedding은 크게 Word EmbeddingSentence/Document Embedding으로 나눌 수 있습니다.

google tokenizer

Word Embedding은 말그대로 입력으로 들어온 자연어를 단어별로 Embedding을 수행하는 것인데요.
전처리 과정에 따라 Word를 어떻게 구성할지 달라지겠지만, 입력으로 들어온 문장형태의 텍스트가 Tokenizer를 통해 Word 단위로 토큰화된다고 가정한다면, Word단위의 토큰별로 Embedding 모델을 통해 실수벡터를 생성하는 방식이 Word Embedding이 될 것입니다.

반면에 Sentence/Document Embedding은 말그대로 문장단위의 Embedding을 수행하는 것인데요.
예를들어 자연어 형태의 텍스트 문장이 입력으로 들어오면, Word마다 임베딩된 실수벡터들을 평균내어 하나의 Embedding으로 집계하는 방식으로 Sentence/Document Embedding을 수행할 수 있습니다.

3. Embedding의 원리

이제 Embedding을 통해 자연어가 실수벡터로 변환되는 과정을 알아보겠습니다.

One-hot Encoding

onehot

초기에는 비교적 단순한 방식을 채택했습니다.
모델 학습을 위한 방대한 단어집이 있다고 가정했을 때, 각 단어마다 숫자 인덱스를 부여하고 단어집 내 총 단어갯수만큼의 길이를 가진 벡터에 해당 인덱스에 1을 체크 하고 나머지는 0을 넣는 방식으로 Embedding된 벡터를 생성했습니다.

이러한 방식은 문제가 있었습니다.
Embedding벡터의 차원이 지나치게 커지는 경향이 있어 성능문제를 야기할 수 있고, 이보다 더 큰 문제는 단어간 유사성을 확인할 수 없다는 점이 있었습니다. 단어간 유사성을 알 수 없다는 것은 LLM을 통해 주로 활용되는 검색시스템에서는 치명적인 단점이 될 수 있습니다.

Word2Vec

13년 구글의 연구원들은 Word2Vec이라는 기술을 개발하게 됩니다.
Word2VecOne-hot Encoding이 단어간 유사성을 확인할 수 없다는 단점을 보완할 수 있고, 앞서 개발된 NNLM모델에 비해서도 은닉층 제거, 계층적 소프트맥스, 네거티브 샘플링 등과 같은 기술들을 도입하여 효율성도 향상시킨 기술이라고 합니다.

유사도비교

그렇다면 Word2Vec은 어떻게 단어간 유사성을 표현할까요?
이전 One-hot Encoding에서 벡터의 0과 1로 표현하던 방식을 희소표현(Sparse Representation)이라고 한다면,
Word2Vec에서 벡터를 표현하는데 사용하는 방식은 분산표현(Distributed Representation)이라고 하는데,
이는 "비슷한 문맥에서 등장하는 단어들은 비슷한 의미를 가진다." 라는 가정을 전제로 하는 표현방식입니다.
가령, 강아지란 단어는 '귀엽다.', '예쁘다.' 등의 단어가 주로 함께 등장하는데, 해당 단어들을 임베딩 과정을 통해 벡터화한다면, 해당 단어벡터들은 유사한 벡터값을 가지게 됩니다.

learned embedding

위와 같은 Embedding방식은 LLM기반 Learned Embedding 모델을 통해 진행될 수 있는데, Embedding과정에서 모델은 다양한 단어들을 입력받게 되고 그 과정에서 문맥상 의미적으로 유사한 단어들의 Embedding 벡터들 간의 거리는 가깝게, 그렇지 않은 단어들의 Embedding 벡터들 간의 거리는 멀게 만드는 방식으로 의미적 관계를 학습합니다.

이러한 분산표현 방식을 통해 Embedding된 벡터들은 단어집의 단어 갯수만큼의 차원을 가질 필요도 없고, 단어간의 유사도도 표현할 수 있게됩니다.

CBOW (Continuous Bag of Words)

Word2Vec 학습 방식에는 CBOW방식과 Skip-Gram방식 두 가지 방식이 있습니다.
이 중 CBOW방식은 주변 단어들을 기반으로 중간에 있는 단어들을 예측하는 방식입니다. 아래와 같은 예문이 있다고 가정해봅시다.

ex) "The fat cat sat on the mat"
LLM의 CBOW는 ['The', 'fat', 'cat', 'on', 'the', 'mat'], 주변단어로부터 sat, 중심단어를 예측하게 됩니다.
중심단어를 예측하기 위해서 앞, 뒤로 몇개의 단어를 볼지 결정해야 하는데, 이 범위를 Window 라고 합니다.

CBOW

윈도우를 옆으로 움직여서 중심단어의 선택을 변경해가며 학습을 위한 데이터셋을 만드는데, 이를 슬라이딩 윈도우 라고 합니다.
CBOW의 인공신경망 모델을 도식화하면 아래와 같습니다.

인공신경망(CBOW)

Skip-Gram

Skip-GramCBOW와는 반대로 중심 단어에서 주변 단어를 예측하는 기술입니다. 윈도우의 크기를 2로 가정했을때, 데이터셋은 다음와 같이 구성됩니다.

skip-gram 데이터셋

Skip-Gram의 인경신경망 모델을 도식화하면 아래와 같습니다.

인공신경망(Skip-Gram)

4. Embedding의 활용

Embedding은 크게 아래 두 가지 케이스에 활용될 수 있습니다.

여러 문서들이 존재할 때, 이들 중 하나를 선택하거나 이들을 서로 비교해야하는 경우

해당 Case의 예로 Semantic Search(의미 기반 검색), Recommendation(추천), Clustering(군집화) 등이 있습니다.

A. Semantic Search

Semantic Search

Semantic Search는 사용자가 입력한 텍스트 형태의 자연어와 의미적으로 연관성이 높은 문서들을 찾아서 제시해주는 기능입니다.

  1. 문서 모음집에 포함되어 있는 각각의 문서에 대한 embedding을 계산하여 별도의 저장소(e.g. local drive, vector database 등)에 저장해 놓음.
  2. 입력 텍스트에 대한 embedding을 계산함.
  3. 입력 텍스트에 대한 embedding 벡터와 각 문서 embedding 벡터 간의 cosine similarity(코사인 유사도)를 계산하고, 그 값을 기준으로 전체 문서들을 내림차순 정렬함.
  4. 정렬 결과 중 상위 k개에 해당하는 문서들의 텍스트를 불러온 뒤 이를 반환함.

B. Recommendation

Recommendation

Recommendation은 사용자가 현재 보고 있는 문서와 의미적으로 연관성이 높은 다른 문서들을 찾아서 제시해주는 기능입니다.

  1. 문서 모음집에 포함되어 있는 각각의 문서에 대한 embedding 벡터를 계산하여 별도의 저장소(e.g. local drive, vector database 등)에 저장해 놓음.
  2. 현재 보고 있는 문서의 embedding 벡터와 그 외의 문서들 각각의 embedding 벡터 간의 cosine similarity(코사인 유사도)를 계산하고, 그 값을 기준으로 전체 문서들을 내림차순 정렬함.
  3. 정렬 결과 중 상위 k개에 해당하는 문서들의 텍스트를 불러온 뒤 이를 반환함.

C. Clustering

Clustering

Clustering은 여러 문서들 간의 의미적 유사성을 바탕으로 이들을 몇 개의 그룹으로 묶어서 정리해 주는 기능입니다. Semantic Search과의 차이는 많은 수의 문서 쌍들의 embedding 벡터간의 거리를 계산해야한다는 것입니다.

Cosine Similarity (코사인 유사도)

코사인 유사도 (Cosine Similarity)는 두 벡터간의 코사인 값을 이용하여 구할 수 있는 두 벡터간의 유사도를 말합니다.
두 벡터간의 방향이 동일한 경우 1, 방향이 90도인 경우 0, 방향이 반대인 경우 -1 값을 가집니다.
즉, 코사인 유사도 값이 1에 가까울 수록 유사도가 높다고 판단할 수 있습니다.

아래는 두 벡터 A, B간의 유사도를 계산하는 수식입니다.

코사인 유사도

5. Embedding 구현

Embedding에 대해서 알아봤으니, 이제 Embedding 기술을 활용한 간단한 챗봇 샘플을 만들어보겠습니다.

import openai
import faiss
import numpy as np

with open('config.txt', 'r') as file:
    lines = file.readlines()

settins = {}
for line in lines:
    key, value = line.strip().split('=')
    settings[key] = value

azure_openai_api_type = settins.get('API_TYPE')
azure_openai_api_base = settins.get('API_BASE')
azure_openai_api_version = settins.get('API_VERSION')
azure_openai_api_key = settins.get('API_KEY')

Embedding에 활용할 라이브러리를 모두 import 합니다.
import 대상들은 사전에 설치되어있어야 합니다. 저는 Embedding에 GPT모델 활용에 필요한 OpenAI 라이브러리를 사용할 것입니다.
그리고 OpenAI 라이브러리 활용을 위한 Config 정보들을 읽어들입니다.

def get_embedding(text, engine="text-embedding-ada-002"):
    text = text.replace("\n", " ")
    return openai.Embedding.create(input=[text], engine=engine)["data"][0]["embedding"] 

텍스트를 입력받아 Embedding을 수행하여 Embedding 벡터를 반환하는 함수를 선언합니다.
Embedding을 위해 사용할 모델은 OpenAI의 text-embedding-ada-002 모델입니다.

class Chatbot:
    def __init__(self, index, embeddings, documents, system_message):
        self.index = index
        self.embeddings = embeddings
        self.documents = documents
        self.system_message = system_message
        self.chat_history = []

    def get_embedding(self, text engine="text-embedding-ada-002"):
        text = text.replace("\n", " ")
        return openai.Embedding.create(input=[text], engine=engine)["data"][0]["embedding"] 

    def find_similar_document(self, user_embedding):
        _, top_indices = self.index.search(np.array([user_embedding]), 1)
        top_index = top_indices[0][0]
        return self.documents[top_index]

    def chat(self, user_input):
        user_embedding = self.get_embedding(user_input)
        similar_document = self.find_similar_document(user_embedding)
        system_message = self.system_message + " " + similar_document

        messages = [{"role": "system", "content": system_message}]
        for message in self.chat_history:
            messages.append(message)
        message.append({"role": "user", "content": user_input})

        response = openai.ChatCompletion.create(
            engine="gpt-35-turbo",
            messages=message
        )
        assistant_message = response.choices[0].message.content

        self.chat_history.append({"role": "user", "content": user_input})
        self.chat_history.append({"role": "assistant", "content": assistant_message})

        return assistant_message

챗봇 클래스를 선언합니다. 선언된 챗봇의 동작 flow는 아래와 같습니다.

  1. 챗봇 인스턴스를 생성하기 위해 아래 파라미터를 입력받아 인스턴스 메모리에 저장합니다.
    1) index: 유사도를 계산하여 가장 유사한 문서의 인덱스를 반환하는 함수 (faiss 라이브러리 활용)
    2) embeddings: 문서들을 임베딩한 벡터 list
    3) documents: 실제 문서 list
    4) system_message: LLM에 System role로 입력할 content 파라미터 값 (프롬프트에 활용)
  2. 사용자가 입력한 자연어 메세지를 챗봇 인스턴스의 chat함수에 파라미터로 입력하여 실행시킵니다.
  3. 사용자가 입력한 자연어 메세지를 Embedding합니다.
  4. 챗봇 인스턴스의 find_similar_document함수를 통해 입력 Embedding벡터와 문서 Embedding벡터들을 비교하여 유사도를 계산하고, 가장 유사한 문서를 반환합니다.
  5. 반환된 문서를 system_message에 추가합니다. (system 프롬프트에 활용)
  6. 챗봇 인스턴스에 chat_history가 저장되어 있다면, 해당 값을 system_message에 추가합니다.
  7. system_message를 system role로, 사용자가 입력한 자연어 메세지를 user role로 GPT모델에 프롬프트 명령을 내립니다. (gpt-35-turbo 모델 활용)
  8. 응답 메세지를 챗봇 인스턴스의 chat_history에 저장합니다.
  9. 응답 메세지를 반환합니다.

아래 코드는 챗봇 인스턴스를 생성하여 chat함수를 실행시키는 코드입니다.

documents = [(documents의 텍스트 배열)]
embeddings = [get_embedding(doc) for doc in documents]
embedding_matrix = np.array(embeddings) # numpy.ndarray 타입 변경
index = faiss.IndexFlatL2(embedding_matrix.shape[1]) # 유사도를 계산하여 가장 유사한 문서를 반환하는 함수 생성, 입력 파라미터는 문서 배열의 크기
index.add(embedding_matrix)

system_message = "너는 친절한 챗봇이며 다음 내용을 참고하여 적절히 답변해줘."

chatbot = Chatbot(index, embeddings, documents, system_message)

user_input = "사용자 입력 메세지"
response = chatbot.chat(user_input)
print(response)

위 코드에서 볼 수 있듯이 유사도 계산에는 faiss 라이브러리의 IndexFlatL2 함수가 활용됩니다.

위 코드들을 통해 사전에 문서들의 리스트를 입력받아 생성된 챗봇에 사용자가 자연어를 입력하면 그에 맞게 답변해주는 챗봇을 구현할 수 있습니다.

Reference

반응형

'개발 > AI' 카테고리의 다른 글

KT AI 해커톤 회고 (1)  (0) 2023.10.27
[AI] Softmax Regression  (0) 2023.10.01
[AI] Logistic Regression  (0) 2023.09.10
[AI] Regression  (0) 2023.01.22
[AI] Machine Learning (머신러닝) - 개념  (0) 2023.01.18
728x90
반응형

👉이전글 에서 Regression에 대해 살펴봤습니다.

이번 글에서 살펴볼 Logistic Regression 모델은 True/False 2가지 범주 중 하나의 값을 예측하여 출력하는 이진분류의 가장 기본이 되는 모델입니다.
이번 글은 모두를 위한 딥러닝 강의를 수강한 후 작성하는 글입니다.

Linear Regression

Linear Regression은 아래 사진과 같은 HypothesisCost Function을 갖습니다.

  • Hypothesis : 입출력의 관계를 나타내는 함수(가설), 해당 함수는 학습데이터 기반의 모델학습 과정을 거쳐 확정됩니다.
  • Cost Function : 예측(가설)을 통한 출력 값과 실제 값의 차이를 나타내는 함수, Cost Function은 아래 사진에서 나타내는 것처럼 2차 함수 형태의 표를 갖습니다. 해당 Cost Function의 값이 최소가 되는 W를 찾는 것이 모델 학습의 목표입니다.

Linear Regression 모델을 통해 HypohesisCost Function이 어떻게 구성되는지 쉽게 알 수 있었습니다.

Logistic Regression

Linear Regression의 맹점

Logistic Regression모델은 이진분류에 강점을 가지고 있는 모델입니다.
이진분류(Binary Classification)의 경우에 Linear Regression을 사용한다고 가정해보겠습니다.

위 사진은 공부한 시간에 따른 시험 pass/fail 여부를 예측하는 모델의 Hypothesis를 나타낸 표입니다.
해당 예시를 Linear Regression모델에 적용하여 특정 y값 이상이 되면 pass로 판단할 수 있도록 구성할 수도 있습니다.

하지만 Linear Regression 모델은 맹점이 있습니다.
입력 학습데이터의 편차가 클 경우, 모델 학습이 잘 안될 가능성이 있습니다.
위 예시에서 만약 학습시간이 50으로 큰 값이 들어온다면, 해당 Hypothesis는 기존 기울기보다 우측으로 크게 치우쳐진 표를 그리는 함수가 될 것입니다.
이렇게 되면 낮은 x(hour)값이 입력되어 pass가 나와야하는 상황에서도 fail이 나올 수 있게 됩니다.

이러한 Linear Regression의 맹점을 보완한 모델이 Logistic Regression입니다.

Logistic Regression Hypothesis

Linear Regression의 맹점을 보완하기 위해 학습데이터의 y값은 아무리 큰 x값이 입력되어도 일정 수준(0 or 1)으로 유지가 되어야 합니다.
그래서 아래 사진의 sigmoid함수를 적용하였습니다.

sigmoid함수는 양의 무한대로 갈 수록 1로 수렴하고, 음의 무한대로 갈 수록 0으로 수렴하는 특징을 가지고 있습니다.
즉, 아무리 편차가 큰 입력 값이 들어와도 출력 값은 일정 수준(0 or 1)으로 유지할 수 있게 되는 것이죠.
이러한 sigmoid함수를 적용하여 Hypothesis를 구성하면 아래 사진과 같이 표현할 수 있습니다.

위 사진처럼 sigmoid함수의 x값에 Linear RegressionHypothesis를 넣으면
Logistic RegressionHypothesis가 구성됩니다.

이를 통해 Logistic Regression모델을 통해 데이터를 학습시키면 보다 정확한 True/False 값을 기대할 수 있게됩니다.

Logistic Regression Cost Function

Logistic RegressionHypothesis를 정했으니, 이제 Cost Function을 정해야 합니다.

Logistic RegressionHypothesis를 그대로 활용하여 Cost Function을 정한다면,
위 사진에서 보는 것처럼 최소의 Cost를 가지는 W를 찾는데 문제가 발생합니다.

모델 학습과정은 Cost Function그래프에서 왼쪽에서든 오른쪽에서든 기울기를 따라 서서히 내려오면서 최소의 Cost를 가지는 W값을 찾는 과정입니다.
위 사진 속 Linear Regression의 그래프에서 보이는 것처럼 그래프는 매끈하게 구성되어 있기 때문에 최솟값을 찾기 쉽습니다.
하지만 오른쪽 Logistic RegressionCost Function그래프는 울퉁불퉁하게 구성되어 있어서 그래프의 기울기만 따라 내려오는 것만으로는 최솟값을 찾기 힘듭니다. (중간의 울퉁불퉁한 곡선에 걸쳐질 확률이 높습니다.)

이를 방지하기 위해 Logistic RegressionCost Function은 새롭게 구성되어야 합니다.


Linear RegressionCost Function처럼 매끄러운 형태의 그래프를 구성하기 위해
Logistic RegressionCost Function에서는 log함수가 사용됩니다. 해당 log함수는 Logistic Regression Hypothesis 자연대수e와 상극인 함수이기 때문입니다.

이를 통해 위 두번째 사진처럼 그래프를 구성할 수 있습니다.
그리고 해당 log함수는 2차 함수처럼 최솟값을 가지는 그래프가 아닌 한쪽으로 편향된 형태를 가지는 그래프이기 때문에,
실제값이 0이냐 1이냐에 따라 함수 구성을 달리하여 Cost Function을 구성하였습니다.
이를 통해 보다 정확한 Cost를 구할 수 있습니다.

예를들어 실제값이 1인 경우에 예측값이 0이 나올경우 Cost는 무한대로 수렴하고,
실제값이 0인 경우에 예측값이 0이 나올경우 Cost는 0으로 수렴하기 때문에 보다 정확한 학습이 이뤄지게 됩니다.
그리고 이러한 Cost Function은 아래 사진처럼 통합하여 표현될 수 있습니다.

Logistic Regression in Tensorflow

Logistic Regression모델은 아래와 같은 Tensorflow코드를 통해 구현될 수 있습니다.
(해당 Tensorflow 버전은 1.0이므로 2.0버전과는 호환이 안될 수 있습니다.)

x_data = [[1, 2], [2, 3], [3, 1], [4, 3], [5, 3], [6, 2]]
y_data = [[0], [0], [0], [1], [1], [1]]

# placeholders for a tensor that will be always fed.
X = tf.placeholder(tf.float32, shape=[None, 2])
Y = tf.placeholder(tf.float32, shape=[None, 2])

W = tf.Variable(tf.random_normal([2, 1]), name='weight')
b = tf.Variable(tf.random_normal([1]), name='bias')

# Hypothesis using sigmoid: tf.div(1., 1. + tf.exp(tf.matmul(X, W)))
hypothesis = tf.sigmoid(tf.matmul(X, W) + b)

# cost/loss function
cost = -tf.reduce_mean(Y * tf.log(hypothesis) + (1 - Y) * tf.log(1 - hypothesis))
train = tf.train.GradientDescentOptimizer(learning_rate=0.01).minimize(cost)

# Accuracy computation
# True if hypothesis > 0.5 else False
predicted = tf.cast(hypothesis > 0.5, dtype=tf.float32)
accuracy = tf.reduce_mean(tf.cast(tf.equal(predicted, Y), dtype=tf.float32))

# Launch graph
with tf.Session() as sess:
    # Initialize TensorFlow variables
    sess.run(tf.global_variables_initializer())

    for step in range(10001):
        cost_val, _ = sess.run([cost, train], feed_dict={X: x_data, Y: y_data})
        if step % 200 == 0:
            print(step, cost_val)

    # Accuracy report
    h, c, a = sess.run([hypothesis, predicted, accuracy], feed_dict={X: x_data, Y: y_data})
    print("\nHypothesis: ", h, "\nCorrect (Y): ", c, "\nAccuracy: ", a)
반응형

'개발 > AI' 카테고리의 다른 글

[AI] Softmax Regression  (0) 2023.10.01
[NLP] Embedding  (0) 2023.09.23
[AI] Regression  (0) 2023.01.22
[AI] Machine Learning (머신러닝) - 개념  (0) 2023.01.18
[AI] 서론  (2) 2023.01.18

+ Recent posts