greatsangho의 이야기

캠프37일차 - 자연어 처리 기초(코러스, 문장구조 이해) 본문

프로그래밍/SK AI 캠프

캠프37일차 - 자연어 처리 기초(코러스, 문장구조 이해)

greatsangho 2024. 10. 18. 19:53

- 딥러닝 기반 자연어 처리
  - 트랜스퍼 러닝
    - 특정 task를 학습한 모델을 다른 타스크 수행에 재사용하는 기법
      - 배경지식을 많이 가진 모델을 이용
      - 기존 모델의 학습 속도가 빨라지는 장점
    - BERT : 빈칸 맞추기
    - GPT : 다음 단어 맞추기
    - 트랜스퍼 모델 문맥을 모델에 내재화 후 성능 향상
  - 업스트림 task
    - 다음 단어 맞추기 - GPT 이 태스크로 프리트레인을 수행 (티끌 모아 ??)
    - 빈칸 채우기 - BERT 이 태스크로 프리트레인을 수행 (티끌 ?? 태산)
    - 빈칸 채우기로 업스트림 task를 수행하는 모델을 마스크 언어 모델
    - 비지도 학습
      - 레이블을 달아주지 않는 비지도 학습 - 문맥으로 파악
      - 문장을 주면 벡터를 통해 앞의 문장과 위치를 고려한 카운팅 진행함
      - 어떤 단어 다음에 어떤 단어가 나왔는지 학습
      - 단어 사이의 유사한 관계를 업데이트
      - 수치로 판단, 1차원 벡터를 만들고 이를 2차원으로 만들어 학습함
    - 대량의 데이터 뉴스, 웹문서, 백과사전과 같이 글만 있으면 다량의 데이터를 생성가능
    - 데이터 안에서 정답을 만들고 이를 바탕으로 모델을 학습시키는 자기지도학습(self supervised learning)
  - 다운스트림 task
    - 프리트레인을 마친 모델을 그대로 사용하거나 또는 다른 task 모듈을 덧붙인 형태로 수행
    - 본질은 분류 : 입력받은 문장이 어떤 범주에 해당하는지 확률 형태로 판단
    - 학습 방법은 파인튜닝 : 프리트레인을 맞춘 모델을 다운스트림에 맞게 업데이트
    - 예를 들어 문서를 분류 : 프리트레인을 마친 BRRT 모델 전체를 문서 분류 데이터로 업데이트, or 개체명 인식을 한다면 마찬가지로 BERT모델 전체를 해당 데이터로 업데이트
  - 문서 분류
    - 문장을 입력 받아서 해당 입력이 긍정 중립 부정에 속하는지 확률값
    - 프리트레인을 마친 마스크 언어 모델 위에 작은 모듈을 하나 더 쌓는다
    - 각 문장의 시작과 끝을 나타내는 특수기호 CLS, SEP 각각 붙여서 토큰화
  - 자연어 추론
    - 문장 두개를 입력 받아서 두 문장 사이의 관계가 참 거짓 중립 등 어떤 범주인지 확률
  - 개체명 인식
    - 자연어를 입력받아서 단어별로 기관명, 인명, 지명 등 어떤 개체명 범주에 속하는지 확률값
  - 질의 응답
    - 질문+정답(지문) 입력 받아서 각 단어가 정답의 시작일 확률과 끝일 확률을 반환
  - 문장 생성
    - 문장 생성은 GPT 계열 언어모델에 가장 많이 사용
    - 자연어를 입력 받아서 문장 전체에 대한 확률값(다음 문장에 올 단어가 얼마나 적합한지)
  - 파인튜닝
    - 다운스트림 task 데이터 전체를 사용, 주어진 데이터에 맞게 모델 전체를 업데이트
  - 프롬프트 튜닝(prompt)
    - 다운스트림 task 데이터 전체를 사용, 주어진 데이터에 맞게 모델 일부를 업데이트
  - 인컨텍스트 튜닝(in-contex)
    - 다운스트림 task 데이터 일부만 사용, 모델을 업데이트 안 함
    - 3가지 방식 : 다운스트림 데이터를 몇 번 참고하느냐의 차이가 있을 뿐 모두 모델을 업데이트 하지 않는다
      - 제로샷 러닝
        - 다운스트림 데이터를 전혀 사용 안함. 모델 바로 다운스트림 타스크를 수행
      - 원샷 러닝
        - 다운스트림 데이터를 1건만(1건도 용량 많음) 사용, 모델은 1건의 데이터가 어떻게 수행되는지 참고한다음 다운스트림 task를 수행
      - 퓨샷 러닝
        - 다운스트림 데이터를 몇 건 사용, 모델은 1건의 데이터가 어떻게 수행되는지 참고한다음 다운스트림 task를 수행

  - 파인튜닝이 아닌 프롬프트 튜닝과 인컨텍스트 튜닝을 이용해 비용절감

학습 파이프라인 형태로 진행

send = ("휴일 인 오늘 도 서쪽 을 중심 으로 폭염 이 이어졌는데요, 내일 은 반가운 비 소식 이 있습니다.", 
        "폭염 을 피해서 휴일 에 놀러왔다가 갑작스런 비 로 인해 망연자실 하고 있습니 다.")
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(send) # 문장 벡터화
tfidf_matrix.toarray() # 2차원 배열로 변환

tfidf_vectorizer.idf_, tfidf_vectorizer.get_feature_names_out()

- 유사도(Simularity)
  - 문장을 적당한 기법으로 수치벡터화를 해서 두개의 객체(문서, 문장, 벡터) 얼마나 비슷한지 표현
  - 값이 높을수록 유사
  - 코사인 유사도
    - 두 벡터간의 각도에 기반, 각도가 0에 가까울수록 유사
    - -1 ~ 1
    - 0 연관 없음
    - -1 정반대 방향(완전히 다른)
    - 1 완벽히 동일방향(완벽하게 유사)
    - A@B (dot product) 내적연산
    - ||A|| 벡터의 크기(노름)
    - A@B / ||A||||B||
  - 유클리디안 거리
    - 두 벡터간의 직선의 거리
    - np.sqrt(mean((Ai - Bi)^2))
  - 자카드 유사도: 두 집합간의 교집합과 합집합의 비율
    - |A 교집합 B| / |A 합집합 B|
  - 맨해튼 거리 : 두 벡터간의 각 차원에서 절대적인 차이값들의 합
    - mean(|Ai - Bi|)

# 자카드
set1 = set(v1) ; set2 = set(v2)
len(set1.intersection(set2)) / len(set1.union(set2)) # 교집합
 
# 코사인 유사도
from sklearn.metrics.pairwise import cosine_similarity
cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2]), cosine_similarity([v1],[v2])

# 유클리디안 거리
# 거리기반
# 정규화
from sklearn.preprocessing import StandardScaler
sc = StandardScaler().fit_transform(tfidf_matrix.toarray())
from sklearn.metrics.pairwise import euclidean_distances
euclidean_distances(sc[0:1], sc[1:2]), euclidean_distances([v1],[v2])

코사인 유사도는 각도를 본다. 유클리디안 거리는 거리 기반이므로 정규화 적용이 필요하다.

자연어 처리

데이터 불러오기

train_neg_path = glob('/content/datasets/aclImdb/train/neg/*.txt')
train_pos_path = glob('/content/datasets/aclImdb/train/pos/*.txt')
import pandas as pd
# review / sentiment(pos 1 neg 0)
# 파일로 목록에 있는 데이터를 읽어서 리스트에 추가한 후  데이터 프레임으로 변환
train_neg_list = []
train_pos_list = []
train_neg_path[0]
train_pos_path[0]
for filepath in train_neg_path:
  with open(filepath, 'r', encoding='utf-8') as f:
    train_neg_list.append([f.read(),0])

for filepath in train_pos_path:
  with open(filepath, 'r', encoding='utf-8') as f:
    train_pos_list.append([f.read(),1])

train_df = pd.DataFrame(train_neg_list + train_pos_list, columns=['review', 'sentiment'])
train_df

문자열 토큰화 하기

reviews = train_df['review'].values
reviews[0].split()
# 문자열을 토큰화
tokenizer_reviews = [ context.split() for context in reviews]
# tokenizer_reviews[0]
# 각 토큰화 된 리스트의 개수
review_len_by_token = [len(review) for review in tokenizer_reviews]
# 공백을 제거한 상태의 문장의 길이
review_len_by_token_len = [len(s.replace(' ','')) for s in reviews]

단어의 길이 시각화 

# 토큰을 시각화해서 앞의 토큰 단위하고 비교, 데이터 간의 차이
import matplotlib.pyplot as plt
plt.hist(review_len_by_token, bins=50, alpha=0.5, color='r', label='word')
plt.hist(review_len_by_token_len, bins=50, alpha=0.5, color='b', label='alphabet')
plt.legend(loc='upper right')
plt.yscale('log')
plt.xlabel('Review Length')
plt.ylabel('Number of Reviews')
plt.show()

대표적인 통계값 구하기

# 적절한 문장의 길이 구하기
import numpy as np
# np.percentile(review_len_by_token, 95)
print(f'문장의 최대 길이:{np.max(review_len_by_token)}')
print(f'문장의 최소 길이:{np.min(review_len_by_token)}')
print(f'문장의 평균 길이:{np.mean(review_len_by_token)}')
# 중앙값(중위수) : 중간에 위치한 값, 평균은 이상치에 의해 왜곡될 수 있어서
print(f'문장의 중앙값 길이:{np.median(review_len_by_token)}')
# 표준편차 : 평균에서 떨어진 정도, 높은 표준편차는 문장의 길이의 분포가 넓다
print(f'문장의 표준편차:{np.std(review_len_by_token)}')
# 1사분위 : 하위 25%
print(f'문장의 1사분위:{np.percentile(review_len_by_token, 25)}')
# 3사분위 : 상위 25%
print(f'문장의 3사분위:{np.percentile(review_len_by_token, 75)}')
# 각각에 대해 boxplot
plt.boxplot(review_len_by_token)
plt.show()

워드 클라우드

from wordcloud import WordCloud
wordcloud = WordCloud(width=800, height=800, background_color='white').generate(' '.join(train_df['review']))
plt.figure(figsize=(10,10))
plt.imshow(wordcloud)
plt.axis('off')
plt.show()

정답 분포

# 정답에 대한 개수 분포
import seaborn as sns
train_df['sentiment'].value_counts()
sns.countplot(x='sentiment', data=train_df)
plt.show()

LLM을 위한 전처리

# 벡터화 한 데이터의 크기를 같게 맞추어 주어야 함
# 데이터 크기
# 개수
# 각 문자의 길이 분포
# 많이 사용된 단어
# 긍정 부정의 분포
# 단어개수
# 특수문자, 대소문자 비율

 

# 동시출현 행렬 : 고유단어 행렬, 각 단어의 쌍이 문장에서 동시에 나타난 횟수
# 윈도위 크기 : 윈도우 크기 기준으로 단어가 얼마나 자주 나타나는지.. 앞뒤로 2개까지 출현 횟수
# 고양이가 나무에 올라갔다
# 고양이가 개를 쫓았다

# 단어간의 관계를 분석
#         고양이가 나무에 올라갔다 개를 쫓았다
# 고양이    0       1       0       1     0
# 나무에    0       0       1       0     0
# 올라갔다
# 개를
# 쫓았다

이런 형태로 앞 뒤에 단어가 들어가 있는지 확인하여 표시한다.

따옴표가 포함된 csv 파일 열기

# 필드에 따옴표가 포함된 경우에는 따로 처리하지 않고그대로 사용 quoting=3
train_data = pd.read_csv('unlabeledTrainData.tsv', sep='\t',quoting=3) # 따옴표 무시
train_data.head()

문자열 길이 시각화

# 리뷰문자의 길이
# train_length = [len(review) for review in train_data['review']]
train_length = train_data['review'].str.len()
train_length.head()
train_length.hist(bins=200) # 빈도 vs 길이

# 자연어 데이터 전처리
# 불필요한 토큰 제거(불용어, 특수기호 등..)
# 전부 소문자로 변경
# 다시 해당 단어를 문장으로 결합(공백을 기준으로)

# 데이터 로드
import pandas as pd
train_df = pd.read_csv('labeledTrainData.tsv', sep='\t', quoting=3)
# html tag 제거
from bs4 import BeautifulSoup
train_df['review'] = train_df['review'].apply(lambda x: BeautifulSoup(x, 'html5lib').get_text())
# 모든 문자열은 소문자로 변환
train_df['review'] = train_df['review'].str.lower()
# 각 라인별 영문자 및 숫자만 존재하도록 정규화를 이용해서 수정
import re
train_df['review'] = train_df['review'].apply(lambda x: re.sub('[^a-z0-9 ]', '', x))
# 각 라인별 문자열을 공백을 기준으로 토큰화(공백 기준으로 split)
train_df['review'] = train_df['review'].str.split() # str 문자열, dt 날짜, apply와 같이 라인별로 적용됨
# 불용어(부사 전치사 등등) 리스트를 가져와서 해당 문자열의 각 단어에서 불용어가 아닌 토큰만 수집
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords
stop_words = set(stopwords.words('english'))
train_df['review'] = train_df['review'].apply(lambda x: [word for word in x if word not in stop_words]) # str은 정해진 함수를 적용할 때
# 토큰들을 공백을 기준으로 합쳐
train_df['review'] = train_df['review'].apply(lambda x: ' '.join(x))
train_df['review']
# 단어빈도수 구하기
from collections import Counter
c = Counter()
for review in train_df['review'].values:
  c.update(review.split())
c.most_common(10)

 

반응형