본문 바로가기
  • 머킹이의 머신로그
프로젝트/개인 프로젝트

[자연어 개인 프로젝트] konlpy 환경설정과 Transformer 모델 만들기

by 머킹 2023. 9. 27.
728x90

konlpy 윈도우에서 사용하기 및 제주도 사투리 불용어 처리하기

 

안녕하세요. 머킹입니다.

오늘은 드디어 한국어 형태소 분석기인 konlpy를 로컬에서 사용하는 기록을 가져왔습니다.

드디어 로컬에서 돌릴 수 있다니 너무 기뻐요...

그리고 제주도 사투리 불용어 처리와 마지막으로 간략하게 모델에 대해서 말하고자 합니다.


일단 저는 이런 문제점이 있었습니다.

1. 불용어를 처리하지 않았음

2. 토큰화가 된 데이터를 제대로 사용하는지 의문이 들었음 (데이터 수가 너무 줄어서)

3. 기존의 모델을 사용하느라 변수가 달라서 어떤 부분이 어떤 변수인지 모르겠음

4. 임베딩과 positional encoding가 제대로 되지 않음

 

간단하게는 이렇게 문제였는데요.

제가 계속 헷갈렸던 것은 '진짜 제대로 데이터를 처리하고 있을까?'에 대한 의문이었습니다.

전처리가 반복될 수록 더더더 헷갈리더군요...

 

그래서 이런 문제점을 하나씩 해결해보고자 합니다.

 

일단 환경설정에 드디어 성공했습니다!!

제가 했던 방법은

1. python 모든 버전 지우기

2. 그리고 Java 환경변수 연결 다시 하기

3. 가상환경 설정하기

4. 재부팅하기

5. Vs 재부팅하기

 

거의 뭐 재부팅의 연속이라고 해도 무방하죠?

저는 3.10.1 버전을 이용했습니다.

 

옆자리 고수님의 도움을 받아 드디어 로컬에서도 돌릴 수 있게 되었습니다..

만만세


불용어 처리

일단 불용어를 처리하는 것은 굉장히 중요합니다.

그리고 데이터의 특성을 없애지 않고 처리하는 것도 매우 중요합니다.

 

불용어 처리의 필요 여부는 번역 작업의 성격과 목적에 따라 다를 수 있습니다. 일반적으로 불용어 처리는 다음과 같은 고려 사항을 기반으로 결정됩니다:

1. 번역 작업 목적: 불용어 처리는 번역의 목적과 사용 사례에 따라 다를 수 있습니다. 번역 결과에 불용어를 포함하고 싶은 경우와 불용어를 제외하고자 하는 경우가 있을 수 있습니다. 예를 들어, 번역 결과를 자연스럽게 읽히기 위해 불용어를 포함할 수도 있습니다.
    
2. 데이터 품질: 입력 데이터의 품질에 따라 불용어 처리를 적용할 수 있습니다. 데이터에 노이즈나 잘못된 정보가 포함되어 있을 경우 이를 제거하고자 할 때 불용어 처리가 유용할 수 있습니다.
    
3. 번역 모델의 특성: 번역 모델의 특성과 언어 처리 방식에 따라 불용어 처리가 필요한 경우가 있습니다. 일부 모델은 불용어를 무시하거나 포함하는 특성을 가질 수 있습니다.
    
4. 문장 읽기 편의성: 번역 결과 문장의 자연스러움과 읽기 편의성을 고려할 때 불용어 처리를 적용하는 경우가 있습니다. 이를 통해 번역된 문장이 더 자연스럽게 보일 수 있습니다.
    

따라서 제주도 사투리를 표준어로 번역하는 번역기를 개발할 때 불용어 처리 여부는 위의 고려 사항을 고려하여 결정해야 합니다. 번역 작업의 목적과 원하는 결과에 따라 불용어 처리를 적용하거나 미적용할 수 있습니다. 불용어 처리를 적용할 경우, 어떤 단어나 표현을 불용어로 처리할 것인지를 신중하게 결정해야 합니다.

 

저는 제주도 사투리를 사용하니까 언, 젠 등을 살리는 것이 매우 중요했습니다.

그리고 저는 제주도 사투리를 잘 몰라서 제주도 방언 사전을 열심히 참고했습니다.

import matplotlib.pyplot as plt
from collections import Counter
from konlpy.tag import Okt

# 한글 폰트 설정 (시스템에 설치된 폰트 중 선택)
plt.rcParams['font.family'] = 'Malgun Gothic'


# Okt 형태소 분석기 초기화
okt = Okt()

# 모든 사투리 데이터와 표준어 데이터를 하나의 리스트로 통합
all_data = []
for item in parallel_data:
    all_data.extend(okt.nouns(item['Original']))
    all_data.extend(okt.nouns(item['Translation']))

# 명사 빈도수 계산
noun_counts = Counter(all_data)

# 가장 빈도가 높은 상위 50개의 명사 추출
top_nouns = noun_counts.most_common(50)

# 명사와 빈도수를 분리
nouns, counts = zip(*top_nouns)

# 그래프 그리기
plt.figure(figsize=(12, 8))
plt.barh(range(len(nouns)), counts, tick_label=nouns)
plt.xlabel('빈도수')
plt.ylabel('명사')
plt.title('명사 빈도수 상위 50개')
plt.gca().invert_yaxis()  # 빈도수가 높은 명사가 위로 오도록 변경
plt.show()

일단 객관적 지표를 보기 위해서 명사에 대해서 출력해 봤습니다.

그래프가 많이 붙어있네요.. 좀 여유롭게 할걸 

아무튼 여기서 '이'를 지우는 건 되게 애매했습니다.

왜냐면 이게 / 이거 / 이건 / 이곳 등 다양하게 사용했기 때문에 지워버린다면 

문장을 파악하는 게 좀 어려울 수 있겠다는 판단을 했습니다.

 

불용어를 직접 설정해서 지우려니 뭘 지워야 할지 정말 고민이 많이 됐습니다.

from konlpy.tag import Okt

# Okt 형태소 분석기 초기화
okt = Okt()

# 불용어 리스트 정의
stopwords = ['.', '(', ')', ',', "'", '%', '-', 'X', ').', '×','의','자','에','안','번','호','을','이','다','만','로','가','를','그']

# 데이터를 불용어를 제거하고 토큰화하여 다시 저장
for item in parallel_data:
    item['Original'] = okt.morphs(item['Original'])
    item['Original'] = [token for token in item['Original'] if token not in stopwords]

    item['Translation'] = okt.morphs(item['Translation'])
    item['Translation'] = [token for token in item['Translation'] if token not in stopwords]

결론적으로 이 불용어들을 없애기로 결정했습니다.

from konlpy.tag import Okt

# Okt 형태소 분석기 초기화
okt = Okt()

# 불용어 리스트 정의
stopwords = ['.', '(', ')', ',', "'", '%', '-', 'X', ').', '×','의','자','에','안','번','호','을','이','다','만','로','가','를','그']

# 불용어 포함 문장 찾기
sample_data = None
for item in parallel_data:
    original_tokens = okt.morphs(str(item['Original']))  # 수정된 부분
    translation_tokens = okt.morphs(str(item['Translation']))  # 수정된 부분
    
    # 원문 또는 번역문 중에서 불용어를 포함하는 문장을 찾으면 선택
    if any(token in stopwords for token in original_tokens) or any(token in stopwords for token in translation_tokens):
        sample_data = item
        break

if sample_data:
    # 불용어가 포함된 원문 출력
    print("불용어가 포함된 원문:")
    print(sample_data['Original'])

    # 불용어가 포함된 번역문 출력
    print("불용어가 포함된 번역문:")
    print(sample_data['Translation'])
    
    # 불용어 제거된 원문 출력
    original_without_stopwords = [token for token in original_tokens if token not in stopwords]
    print("불용어 제거된 원문:")
    print(' '.join(original_without_stopwords))

    # 불용어 제거된 번역문 출력
    translation_without_stopwords = [token for token in translation_tokens if token not in stopwords]
    print("불용어 제거된 번역문:")
    print(' '.join(translation_without_stopwords))
else:
    print("데이터셋에서 불용어를 포함하는 문장을 찾을 수 없습니다.")

이 코드는 아직 돌리는 중이라 결과가 없네요.

왜 이 코드를 아직 돌리는 중이냐면 데이터 처리를 다시 하고 있기 때문입니다.

 


여기서 바로 2번 문제점으로 이어가겠습니다.

제가 사용하는 데이터는 분명 csv로 열었을 때 (토큰화하고) 3백만 개인데 

병렬구조 후에 데이터 수가 200,000개 정도로 줄었습니다.

그래서 저만큼 줄어든다고?라는 의문이 들었습니다.

dataset_size = len(parallel_data)
print(f"데이터셋의 크기: {dataset_size}")
>> 데이터셋의 크기: 283929

 

출력해 보니 283929개네요.

그래서 문득 지금 잘 못 처리하고 있는 거 아닌가? 싶어서

전체 데이터를 다시 처리하며 출력해 보기로 했습니다.

import csv
import json
from konlpy.tag import Okt
import os
from tqdm import tqdm
import matplotlib.pyplot as plt
from matplotlib import font_manager, rc

# 한글 폰트 설정 (시스템에 설치된 폰트 중 선택)
plt.rcParams['font.family'] = 'Malgun Gothic'
# Okt 형태소 분석기 초기화
okt = Okt()

# 데이터 디렉토리 경로
base_directory = 'C:/Users/user/Desktop/vs/deeprunnning/patois/jeju'

# 모든 데이터 폴더 선택 (Training 및 Validation)
data_directories = [
    os.path.join(base_directory, 'Training'),
    os.path.join(base_directory, 'Validation')
]

# 형태소 분석 결과 저장 딕셔너리
results = {'Training': [], 'Validation': []}

def analyze_text(text):
    okt_result = okt.morphs(text)
    return {
        'Okt': okt_result
    }

# 모든 데이터 폴더에서 JSON 파일 목록 가져오기
original_data_count = {'Training': 0, 'Validation': 0}  # 토큰화 하기 전의 데이터 수 저장

for data_directory in data_directories:
    data_files = [os.path.join(root, file) for root, dirs, files in os.walk(data_directory) for file in files if file.endswith('.json')]

    for data_file in tqdm(data_files, desc=f"{data_directory} 데이터 처리중"):
        with open(data_file, 'r', encoding='utf-8') as file:
            data = json.load(file)
            original_data_count[data_directory.split(os.path.sep)[-1]] += len(data['utterance'])

            for idx, utterance in enumerate(data['utterance']):
                text = utterance['dialect_form']
                result = analyze_text(text)
                results[data_directory.split(os.path.sep)[-1]].append({
                    'Utterance': f'Utterance_{idx + 1}',
                    'Okt': result['Okt']
                })

# 결과 출력 (일부만 출력)
for data_type, data_results in results.items():
    print(f"\n{data_type} 데이터 형태소 분석 결과 (일부):")
    for data_result in data_results[:10]:
        print(f"\n{data_result['Utterance']}:")
        print(f"Okt 형태소 분석 결과: {' '.join(data_result['Okt'])}")

# CSV 파일 경로 설정
output_csv_file = 'C:/Users/user/Desktop/vs/deeprunnning/patois/parallel_data.csv'

# CSV 파일 헤더 설정
csv_columns = ['Data_Type', 'Utterance', 'Okt']

# CSV 파일에 형태소 분석 결과 저장
with open(output_csv_file, 'w', newline='', encoding='utf-8') as csvfile:
    writer = csv.DictWriter(csvfile, fieldnames=csv_columns)
    writer.writeheader()  # 헤더 쓰기

    for data_type, data_results in results.items():
        for data_result in tqdm(data_results, desc=f"{data_type} 데이터 처리중"):
            writer.writerow({
                'Data_Type': data_type,
                'Utterance': data_result['Utterance'],
                'Okt': ' '.join(data_result['Okt'])
            })

print(f"분석 결과가 {output_csv_file} 경로에 저장되었습니다.")

# 데이터 수 변화 출력
for data_type, count in original_data_count.items():
    print(f"{data_type} 데이터 수 (토큰화 하기 전): {count}개")
    print(f"{data_type} 데이터 수 (토큰화 후): {len(results[data_type])}개")

# 데이터 수 변화를 그래프로 시각화
data_types = list(original_data_count.keys())
original_counts = list(original_data_count.values())
tokenized_counts = [len(results[data_type]) for data_type in data_types]

plt.figure(figsize=(10, 6))
plt.bar(data_types, original_counts, label='Before Tokenization')
plt.bar(data_types, tokenized_counts, label='After Tokenization', alpha=0.7)
plt.xlabel('Data Type')
plt.ylabel('Data Count')
plt.title('Data Count Before and After Tokenization')
plt.legend()
plt.show()

그래서 코드가 많이 길어졌습니다.ㅎㅎ

추석 안에는 모델이 돌아갈까요? 일단 원격 연결 해놨으니까 계속 지켜봐야겠습니다.

 


3번에는 변수의 문제입니다.

제가 저번에 올린 Transformer에 있는 변수들을 쓰다 보니

eng_tokenizer 등의 변수가 지속적으로 발견됐습니다.

 

그리고 gpt가 자꾸 saturi 이런 식으로 변수를 계속 만들어서 혼동됐습니다.

그래서 코드 하나하나를 연결하면서 gpt에게 더블 체크를 진행했습니다.

 

이 부분은 앞으로 지속적으로 문제 될 것 같아서

진짜 초기에 모델 구성하는 게 진짜 중요하구나 싶었습니다..

 


4번은 3번과 좀 이어지는 문제인데요.

제가 올린 Transformer에서는 임베딩과 positional encoding을 사용하지 않는다는 겁니다..

그래서 하나하나 코드를 만들었습니다.

 

여기서!! 고민이 들었습니다.

사전 모델을 사용하는 게 맞을까 / 이렇게 내가 해본 예제에 넣어보는게 맞을까?

사전 모델을 하는게 더 성능이 좋을 것 같아서 찾아봤는데 

Hugging Face에 KE-T5: Korean-English T5라는 모델이 있어서 되게 사용해보고 싶었습니다.

하지만 일단 예제에 넣어서 해보고 잘 돌아간다면

사전 모델도 사용해보고 싶습니다.

 

그래서 열심히 임베딩과 positional encoding 코드를 적었습니다.

import torch
import torch.nn as nn
import random

# GPU 사용 가능한 경우 GPU를 사용하고, 그렇지 않은 경우 CPU를 사용합니다.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 임베딩 차원 랜덤 설정 (100에서 300 사이의 값)
embedding_dim = random.randint(100, 300)

# 임베딩 모델 정의
embedding_model = nn.Embedding(num_embeddings=len(word_to_index), embedding_dim=embedding_dim).to(device)

# 모든 데이터에 대한 임베딩 생성
embedded_data = []
for item in parallel_data:
    # 토큰화된 데이터를 정수 인덱스로 변환
    tokens = item['Translation'].split()
    indices = [word_to_index.get(token, word_to_index['[PAD]']) for token in tokens]
    
    # 임베딩 생성
    embeddings = embedding_model(torch.tensor(indices).unsqueeze(0).to(device))
    embedded_data.append(embeddings)

# embedded_data에는 모든 데이터에 대한 임베딩이 포함됩니다.

# 첫 번째 데이터에 대한 임베딩 출력
print("첫 번째 데이터에 대한 임베딩:")
print(embedded_data[0])

>>>
임베딩 차원 (embedding_dim): 274
예시 문장의 임베딩:
tensor([[[-3.2013e-01,  8.9300e-01, -1.5642e-01, -1.1617e+00, -9.9936e-01,
          -1.2362e+00, -5.8393e-01,  8.5305e-02, -6.2949e-01,  1.1705e+00,
          -4.5907e-01,  3.7055e-01, -2.5642e+00, -1.9076e+00,  2.0141e-01,
          -5.6503e-01, -2.5445e+00,  7.6639e-01, -1.1969e+00,  3.2083e+00,
          -8.5860e-01,  6.4544e-01, -9.4454e-01, -8.6809e-01, -6.2616e-01,
          -2.0979e+00, -1.6870e+00,  1.9868e-01,  6.0553e-01, -3.5960e-01,
           9.3706e-01, -3.6318e-01, -1.7007e+00,  1.2417e+00,  2.0647e+00,
           5.0176e-01, -1.4038e-01, -6.9572e-01, -2.0611e-01, -1.9721e+00,
          -1.2197e+00, -3.8668e-01,  4.2489e-02,  2.2858e+00, -6.8441e-01,
           1.3245e+00, -2.9854e-01, -1.9532e+00, -2.1559e-01,  1.0040e+00,
           1.4041e+00,  7.5854e-01,  1.2120e+00, -6.9265e-01, -2.6285e+00,
           4.5654e-01, -4.3540e-01,  1.8434e-01, -2.5295e-01, -1.2123e+00,
           1.3356e-01, -4.0755e-01, -5.3499e-01,  6.2490e-01, -2.1108e+00,
          -1.2799e+00, -2.9460e-01, -3.0130e-01,  3.8295e-01,  5.0995e-01,
          -9.7412e-01, -1.2716e+00,  6.7756e-02,  9.0849e-01, -4.2134e-01,
          -5.1248e-01,  1.6009e+00,  1.4864e+00,  1.4728e+00,  2.9776e-01,
          -6.4681e-01, -2.7097e-01,  9.8615e-01, -9.3145e-01,  3.7559e-01,
           1.0129e+00, -9.3753e-01,  1.2375e+00, -3.7421e-01,  7.8030e-01,
          -4.6174e-01, -1.0416e+00, -3.2362e-01, -1.0242e-02,  2.1748e+00,
           1.3396e-02,  9.6336e-02, -1.7605e+00,  6.9758e-01, -8.6163e-01,
          -6.2748e-01, -1.5422e-01,  9.8859e-01,  6.5999e-01, -1.0179e+00,
          -2.4505e-01,  1.0723e-01, -1.1064e+00, -1.7642e+00, -5.2585e-01,
           1.9505e-02, -3.4172e-01, -1.1603e+00,  7.1975e-01, -1.2105e-01,
...
           1.0493e+00, -1.2829e+00,  1.4061e+00,  1.9127e+00, -1.1162e+00,
          -1.9025e+00, -5.0160e-01,  6.0095e-02,  1.9979e-01, -6.4519e-01,
          -2.4767e-01,  6.3569e-01,  2.8796e-01, -6.5069e-01]]],
       grad_fn=<EmbeddingBackward0>)

라는 코드를 짰다가 문득 Positional encoding 안 하고 있지 않나..? 싶어서

제대로 다시 짜봤습니다.

그리고 출력 결과도 다시 만들었습니다.

 

import torch
import torch.nn as nn
import random

# GPU 사용 가능한 경우 GPU를 사용하고, 그렇지 않은 경우 CPU를 사용합니다.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 임베딩 차원 랜덤 설정 (100에서 300 사이의 값)
embedding_dim = random.randint(100, 300)

# 임베딩 모델 정의
embedding_model = nn.Embedding(num_embeddings=len(word_to_index), embedding_dim=embedding_dim).to(device)

# 모든 데이터에 대한 임베딩 생성
embedded_data = []

# 모든 데이터를 하나의 텐서로 변환
all_indices = []
for item in parallel_data:
    # 토큰화된 데이터를 정수 인덱스로 변환
    tokens = item['Translation'].split()
    indices = [word_to_index.get(token, word_to_index['[PAD]']) for token in tokens]
    all_indices.append(indices)

# 패딩 적용
max_len = max(len(indices) for indices in all_indices)
padded_indices = [indices + [word_to_index['[PAD]']] * (max_len - len(indices)) for indices in all_indices]
all_indices_tensor = torch.tensor(padded_indices).to(device)

# 임베딩 생성
embedded_data = embedding_model(all_indices_tensor)

# Positional Encoding 정의
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=512):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=0.1)
        self.pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))
        self.pe[:, 0::2] = torch.sin(position * div_term)
        self.pe[:, 1::2] = torch.cos(position * div_term)

    def forward(self, x):
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)

# Positional Encoding 적용
positional_encoder = PositionalEncoding(embedding_dim, max_len=max_len).to(device)
embedded_data_with_position = positional_encoder(embedded_data)

# embedded_data_with_position에는 모든 데이터에 대한 임베딩과 Positional Encoding이 포함됩니다.

# 첫 번째 데이터에 대한 임베딩과 Positional Encoding 출력
print("첫 번째 데이터에 대한 임베딩과 Positional Encoding:")
print(embedded_data_with_position[0])

이 코드도 돌아가고 나면 결과에 대해 말씀드리겠습니다.

그리고 저 예제에 없는 부분이 되게 많더라고요.

test와 training으로 나누지 않아서 그 부분도 추가했습니다.

 

import random

# 데이터를 섞습니다.
random.shuffle(parallel_data)

# 데이터의 일정 부분을 트레이닝 데이터로 선택합니다.
train_ratio = 0.8  # 트레이닝 데이터 비율 (예: 80%)
total_data_length = len(parallel_data)
train_data_length = int(train_ratio * total_data_length)

# 트레이닝 데이터와 테스트 데이터로 나눕니다.
train_data = parallel_data[:train_data_length]
test_data = parallel_data[train_data_length:]

# 트레이닝 데이터와 테스트 데이터의 크기를 확인합니다.
print(f"트레이닝 데이터 수: {len(train_data)}")
print(f"테스트 데이터 수: {len(test_data)}")

 


오늘은 이렇게 다시금 데이터를 전처리하게 되었다는 기록을 남깁니다..

확실히 제가 모델을 사용해 본 적이 없고 사전모델을 활용하는 방법을 모르니까

전처리를 계속 수정하게 되네요.

 

이번에 제대로 배우고 모델을 만들고자 한다면 

데이터 구상, 전처리, 코드 순서들을 파악해야겠습니다.

 

추석동안 모델 잘 돌리고 오겠습니다.